pax_global_header00006660000000000000000000000064152466070050014517gustar00rootroot0000000000000052 comment=b4c49b0aef3f2187c84e71e5d8c06e7213a5e0fd scikit-hep-boost-histogram-b4c49b0/000077500000000000000000000000001524660700500173075ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/.all-contributorsrc000066400000000000000000000156351524660700500231520ustar00rootroot00000000000000{ "projectName": "boost-histogram", "projectOwner": "scikit-hep", "repoType": "github", "repoHost": "https://github.com", "files": [ "README.md" ], "imageSize": 100, "commit": false, "commitConvention": "none", "contributors": [ { "login": "henryiii", "name": "Henry Schreiner", "avatar_url": "https://avatars1.githubusercontent.com/u/4616906?v=4", "profile": "http://iscinumpy.gitlab.io", "contributions": [ "maintenance", "code", "doc" ] }, { "login": "HDembinski", "name": "Hans Dembinski", "avatar_url": "https://avatars0.githubusercontent.com/u/2631586?v=4", "profile": "https://github.com/HDembinski", "contributions": [ "maintenance", "code" ] }, { "login": "LovelyBuggies", "name": "N!no", "avatar_url": "https://avatars3.githubusercontent.com/u/29083689?v=4", "profile": "http://lovelybuggies.github.io", "contributions": [ "test", "doc" ] }, { "login": "jpivarski", "name": "Jim Pivarski", "avatar_url": "https://avatars0.githubusercontent.com/u/1852447?v=4", "profile": "https://github.com/jpivarski", "contributions": [ "ideas" ] }, { "login": "nsmith-", "name": "Nicholas Smith", "avatar_url": "https://avatars3.githubusercontent.com/u/6587412?v=4", "profile": "https://github.com/nsmith-", "contributions": [ "bug" ] }, { "login": "physicscitizen", "name": "physicscitizen", "avatar_url": "https://avatars3.githubusercontent.com/u/45843291?v=4", "profile": "http://www-pnp.physics.ox.ac.uk/~huffman/", "contributions": [ "bug" ] }, { "login": "ChanchalKumarMaji", "name": "Chanchal Kumar Maji", "avatar_url": "https://avatars1.githubusercontent.com/u/31502077?v=4", "profile": "https://www.linkedin.com/in/chanchal-kumar-maji-9230a9145/", "contributions": [ "doc" ] }, { "login": "douglasdavis", "name": "Doug Davis", "avatar_url": "https://avatars2.githubusercontent.com/u/3202090?v=4", "profile": "https://ddavis.io/", "contributions": [ "bug" ] }, { "login": "pgrimaud", "name": "Pierre Grimaud", "avatar_url": "https://avatars1.githubusercontent.com/u/1866496?v=4", "profile": "https://github.com/pgrimaud", "contributions": [ "doc" ] }, { "login": "beojan", "name": "Beojan Stanislaus", "avatar_url": "https://avatars0.githubusercontent.com/u/3727925?v=4", "profile": "https://github.com/beojan", "contributions": [ "bug" ] }, { "login": "Popinaodude", "name": "Popinaodude", "avatar_url": "https://avatars2.githubusercontent.com/u/20911987?v=4", "profile": "https://github.com/Popinaodude", "contributions": [ "bug" ] }, { "login": "colizz", "name": "Congqiao Li", "avatar_url": "https://avatars2.githubusercontent.com/u/44885400?v=4", "profile": "https://github.com/colizz", "contributions": [ "bug" ] }, { "login": "alexander-held", "name": "alexander-held", "avatar_url": "https://avatars0.githubusercontent.com/u/45009355?v=4", "profile": "https://github.com/alexander-held", "contributions": [ "bug" ] }, { "login": "chrisburr", "name": "Chris Burr", "avatar_url": "https://avatars3.githubusercontent.com/u/5220533?v=4", "profile": "https://github.com/chrisburr", "contributions": [ "doc" ] }, { "login": "kgizdov", "name": "Konstantin Gizdov", "avatar_url": "https://avatars.githubusercontent.com/u/3164953?v=4", "profile": "https://keybase.io/kgizdov", "contributions": [ "platform", "bug" ] }, { "login": "cranmer", "name": "Kyle Cranmer", "avatar_url": "https://avatars.githubusercontent.com/u/4458890?v=4", "profile": "http://theoryandpractice.org", "contributions": [ "doc" ] }, { "login": "amangoel185", "name": "Aman Goel", "avatar_url": "https://avatars.githubusercontent.com/u/10528392?v=4", "profile": "http://amangoel.me", "contributions": [ "doc", "code" ] }, { "login": "gohil-jay", "name": "Jay Gohil", "avatar_url": "https://avatars.githubusercontent.com/u/59703162?v=4", "profile": "http://jay-gohil.me", "contributions": [ "doc" ] }, { "login": "pfackeldey", "name": "Peter Fackeldey", "avatar_url": "https://avatars.githubusercontent.com/u/18463582?v=4", "profile": "https://github.com/pfackeldey", "contributions": [ "code" ] }, { "login": "jonas-eschle", "name": "Jonas Eschle", "avatar_url": "https://avatars.githubusercontent.com/u/17454848?v=4", "profile": "https://github.com/jonas-eschle", "contributions": [ "code" ] }, { "login": "Saransh-cpp", "name": "Saransh Chopra", "avatar_url": "https://avatars.githubusercontent.com/u/74055102?v=4", "profile": "https://saransh-cpp.github.io/", "contributions": [ "code" ] }, { "login": "matthewfeickert", "name": "Matthew Feickert", "avatar_url": "https://avatars.githubusercontent.com/u/5142394?v=4", "profile": "http://www.matthewfeickert.com/", "contributions": [ "code" ] }, { "login": "andrzejnovak", "name": "Andrzej Novak", "avatar_url": "https://avatars.githubusercontent.com/u/13226500?v=4", "profile": "http://andrzejnovak.github.io/", "contributions": [ "code" ] }, { "login": "veprbl", "name": "Dmitry Kalinkin", "avatar_url": "https://avatars.githubusercontent.com/u/245573?v=4", "profile": "https://github.com/veprbl", "contributions": [ "code" ] }, { "login": "klieret", "name": "Kilian Lieret", "avatar_url": "https://avatars.githubusercontent.com/u/13602468?v=4", "profile": "https://www.lieret.net/", "contributions": [ "doc" ] }, { "login": "riga", "name": "Marcel Rieger", "avatar_url": "https://avatars.githubusercontent.com/u/1908734?v=4", "profile": "https://github.com/riga", "contributions": [ "code" ] }, { "login": "Superharz", "name": "Florian Harz", "avatar_url": "https://avatars.githubusercontent.com/u/11306732?v=4", "profile": "https://github.com/Superharz", "contributions": [ "code" ] } ], "contributorsPerLine": 7, "skipCi": true, "commitType": "docs" } scikit-hep-boost-histogram-b4c49b0/.clang-format000066400000000000000000000053761524660700500216750ustar00rootroot00000000000000--- Language: Cpp AccessModifierOffset: -2 AlignAfterOpenBracket: Align AlignConsecutiveAssignments: true AlignConsecutiveDeclarations: false AlignEscapedNewlines: Right AlignOperands: true AlignTrailingComments: true AllowAllParametersOfDeclarationOnNextLine: true AllowShortBlocksOnASingleLine: false AllowShortCaseLabelsOnASingleLine: false AllowShortFunctionsOnASingleLine: All AllowShortIfStatementsOnASingleLine: false AllowShortLoopsOnASingleLine: false AlwaysBreakAfterDefinitionReturnType: None AlwaysBreakAfterReturnType: None AlwaysBreakBeforeMultilineStrings: false AlwaysBreakTemplateDeclarations: true BinPackArguments: false BinPackParameters: false BraceWrapping: AfterClass: false AfterControlStatement: false AfterEnum: false AfterFunction: false AfterNamespace: false AfterObjCDeclaration: false AfterStruct: false AfterUnion: false BeforeCatch: false BeforeElse: false IndentBraces: false # SplitEmptyFunctionBody: true BreakBeforeBinaryOperators: All BreakBeforeBraces: Attach BreakBeforeInheritanceComma: false BreakBeforeTernaryOperators: true BreakConstructorInitializersBeforeComma: false BreakConstructorInitializers: BeforeComma BreakAfterJavaFieldAnnotations: false BreakStringLiterals: true ColumnLimit: 88 CommentPragmas: "^ IWYU pragma:" CompactNamespaces: false ConstructorInitializerAllOnOneLineOrOnePerLine: false ConstructorInitializerIndentWidth: 4 ContinuationIndentWidth: 4 Cpp11BracedListStyle: true DerivePointerAlignment: false DisableFormat: false # ExperimentalAutoDetectBinPacking: false FixNamespaceComments: true ForEachMacros: [foreach, Q_FOREACH, BOOST_FOREACH] IncludeCategories: - Regex: '^"(llvm|llvm-c|clang|clang-c)/' Priority: 2 - Regex: '^(<|"(gtest|isl|json)/)' Priority: 3 - Regex: ".*" Priority: 1 IncludeIsMainRegex: "$" IndentCaseLabels: false IndentWidth: 4 IndentWrappedFunctionNames: false JavaScriptQuotes: Leave JavaScriptWrapImports: true KeepEmptyLinesAtTheStartOfBlocks: false MacroBlockBegin: "" MacroBlockEnd: "" MaxEmptyLinesToKeep: 1 NamespaceIndentation: None ObjCBlockIndentWidth: 2 ObjCSpaceAfterProperty: false ObjCSpaceBeforeProtocolList: true PenaltyBreakAssignment: 2 PenaltyBreakBeforeFirstCallParameter: 19 PenaltyBreakComment: 300 PenaltyBreakFirstLessLess: 120 PenaltyBreakString: 1000 PenaltyExcessCharacter: 1000000 PenaltyReturnTypeOnItsOwnLine: 60 PointerAlignment: Left ReflowComments: true SortIncludes: true SpaceAfterCStyleCast: false SpaceAfterTemplateKeyword: true SpaceBeforeAssignmentOperators: true SpaceBeforeParens: Never SpaceInEmptyParentheses: false SpacesBeforeTrailingComments: 1 SpacesInAngles: false SpacesInContainerLiterals: true SpacesInCStyleCastParentheses: false SpacesInParentheses: false SpacesInSquareBrackets: false Standard: Cpp11 TabWidth: 4 UseTab: Never scikit-hep-boost-histogram-b4c49b0/.clang-tidy000066400000000000000000000012751524660700500213500ustar00rootroot00000000000000FormatStyle: file Checks: " modernize-*, readability-*, bugprone-*, performance-*, portability-*, clang-analyzer-*, misc-*, -bugprone-easily-swappable-parameters, -misc-include-cleaner, -misc-no-recursion, -misc-non-private-member-variables-in-classes, -misc-redundant-expression, -modernize-avoid-c-arrays, -modernize-use-trailing-return-type, -readability-braces-around-statements, -readability-function-cognitive-complexity, -readability-identifier-length, -readability-magic-numbers, -readability-named-parameter, google-explicit-constructor, google-readability-casting, llvm-namespace-comment, " HeaderFilterRegex: "bh_python/.*hpp" UseColor: true scikit-hep-boost-histogram-b4c49b0/.cmake-format.yaml000066400000000000000000000001601524660700500226140ustar00rootroot00000000000000format: line_width: 99 # Causes a few issues - can be solved later, possibly. markup: enable_markup: false scikit-hep-boost-histogram-b4c49b0/.gitattributes000066400000000000000000000001161524660700500222000ustar00rootroot00000000000000*.py diff=python *.pyi diff=python *.cpp diff=cpp *.hpp diff=cpp *.svg -diff scikit-hep-boost-histogram-b4c49b0/.github/000077500000000000000000000000001524660700500206475ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/.github/CONTRIBUTING.md000066400000000000000000000210701524660700500231000ustar00rootroot00000000000000See the [Scikit-HEP Developer introduction][skhep-dev-intro] for a detailed description of best practices for developing Scikit-HEP packages. [skhep-dev-intro]: https://scikit-hep.org/developer/intro # Contributing ## Building from source This repository has dependencies in submodules. Check out the repository like this: ```bash git clone --recursive https://github.com/scikit-hep/boost-histogram.git cd boost-histogram ```
Faster version (click to expand) ```bash git clone https://github.com/scikit-hep/boost-histogram.git cd boost-histogram git submodule update --init --depth 10 ```
## 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 -l # List all the defined sessions $ nox -s lint # Lint only $ nox -s tests # Tests only $ nox -s docs -- serve # Build and serve the docs $ nox -s make_pickle # Make a pickle file for this version ``` Nox handles everything for you, including setting up a temporary virtual environment for each run. ### Pip While developers often work in CMake, the "correct" way to develop a python package is in a virtual environment. This is how you would set one up with Python's built-in venv: ```bash python3 -m venv .venv source ./.venv/bin/activate pip install -ve. --group dev ``` Or if you use uv: ```bash uv sync ```
Optional: External Jupyter kernel (click to expand) You can set up a kernel for external Jupyter then deactivate your environment: ```bash python -m ipykernel install --user --name boost-hist deactivate ``` Now, you can run notebooks using your system JupyterLab, and it will list the environment as available!
To rebuild, rerun `pip install -ve .` from the environment, if the commit has changed, you will get a new build. Due to the `-e`, Python changes do not require a rebuild. ### CMake CMake is common for C++ development, and ties nicely to many C++ tools, like IDEs. If you want to use it for building, you can. Make a build directory and run CMake. If you have a specific Python you want to use, add `-DPython_EXECUTABLE=$(which python)` or similar to the CMake line. If you need help installing the latest CMake version, [visit this page](https://cliutils.gitlab.io/modern-cmake/chapters/intro/installing.html); one option is to use pip to install CMake. You have three options for running code in python: 1. Run from the build directory (only works with some commands, like `python -m pytest`, and not others, like `pytest` 2. Add the build directory to your PYTHONPATH environment variable 3. Set `CMAKE_INSTALL_PREFIX` to your site-packages and install (recommended for virtual environments). Here is the recommendation for a CMake install, using uv: ```bash uv venv uv pip install --group dev cmake --workflow default # Option 3 only: cmake --install --preset default --prefix $(python -c "import distutils.sysconfig; print(distutils.sysconfig.get_python_lib(plat_specific=False,standard_lib=False))") ``` Note that option 3 will require reinstalling if the python files change, while options 1-2 will not if you have a recent version of CMake (symlinks are made). ## Testing Run the unit tests (requires pytest and NumPy). ```bash python3 -m pytest ``` For CMake, you can use `ctest --preset default` (the workflow above will run the tests). The build requires `setuptools_scm`. The tests require `numpy`, `pytest`, and `pytest-benchmark`. `pytest-sugar` adds some nice formatting. ## Benchmarking You can enable benchmarking with `--benchmark-enable` when running tests. You can also run explicit performance tests with `scripts/performance_report.py`. ```bash python3 -m pytest --benchmark-enable --benchmark-sort fullname ``` For example, if you want to benchmark before and after a change: ```bash python3 -m pytest --benchmark-enable --benchmark-autosave # Make change python3 -m pytest --benchmark-enable --benchmark-autosave pytest-benchmark compare 0001 0002 --sort fullname --histogram ``` Note, while the histogram option (`--histogram`) is nice, it does require `pygal` and `pygaljs` to be installed. Feel free to leave it off if not needed. ## Formatting Code should be well formatted; CI will check it and one of the authors can help reformat your code. If you want to check it yourself, you should use [`prek`](https://github.com/j178/prek). Just [install prek](https://github.com/j178/prek), probably using brew on macOS or pip on other platforms, then run: ```bash prek install ``` Now all changed files will be checked every time you git commit. You can check it yourself (even without installing the hooks) using: ```bash prek run -a ``` Developers should update the prek dependencies once in a while, you can do this automatically with: ```bash prek auto-update ``` ## Clang-Tidy To run Clang tidy, the following recipe should work. Files will be modified in place, so you can use git to monitor the changes. ```bash docker run --rm -v $PWD:/pybind11 -it silkeh/clang:20 apt update && apt install -y python3-dev python3-pip git ninja-build python3 -m pip install setuptools_scm --break-system-packages cmake --preset tidy cmake --build --preset tidy ``` To autofix, use: ```bash cmake --preset --preset tidy-fix cmake --build --preset tidy-fix ``` We also provide matching `--workflow`'s, but you'll need a newer CMake for that (you can use pip to get it, though). ## Include what you use To run include what you use, install (`brew install include-what-you-use` on macOS), then run: ```bash cmake -S . -B build-iwyu -DCMAKE_CXX_INCLUDE_WHAT_YOU_USE=$(which include-what-you-use) cmake --build build ``` ## Timing steps Make time/memory taken can be set `CMAKE_CXX_COMPILER_LAUNCHER`/`CMAKE_CXX_LINKER_LANCHER`. Some examples: ``` # Linux: # "time" # "time;-v" # "time;-f;'%U user %S system %E elapsed %P CPU %M KB'" # macOS: # "time" # macOS with brew install gnu-time: # "gtime;-f;'%U user %S system %E elapsed %P CPU %M KB'" # ``` ## Common tasks
Updating dependencies (click to expand) This will checkout new versions of the dependencies. ```bash nox -s bump_boost -- 1.88.0 ```
Making a new release (click to expand) - Finish merging open PRs that you want in the new version - Add most recent changes to the `docs/changelog.md` - Sync master with develop using `git checkout master; git merge develop --ff-only` and push - Make sure the `cmake --preset tidy` build runs on master without issues (manually trigger if needed) - Make the GitHub release in the GitHub UI. Copy the changelog entries and links for that version; this has to be done as part of the release and tag procedure for archival tools (Zenodo) to pick them up correctly. - Title should be `"Version "` - Version tag should be `"v" + major + "." + minor + "." + patch`. - GHA will build and send to PyPI for you when you release. - Conda-forge will automatically make a PR to update within an hour or so, and it will merge automatically if it passes.
Making a compiler flamegraph (click to expand) This requires LLVM 9+, and is based on [this post](https://aras-p.info/blog/2019/01/16/time-trace-timeline-flame-chart-profiler-for-Clang/). ```bash brew install llvm # macOS way to get clang-9 python3 -m venv .env_core # general environment (no install will be made) . .env_core/bin/activate pip install dependency-groups pip-install-dependency-groups dev CXX="/usr/local/opt/llvm/bin/clang++" cmake -S . -B build-llvm \ -DCMAKE_CXX_FLAGS="-ftime-trace" \ -DPYTHON_EXECUTABLE=$(which python) cmake --build build-llvm/ ``` Now open a browser with [SpeedScope](https://www.speedscope.app), and load one of the files.
Adding a contributor (click to expand) First, you need to install the [all contributor CLI](https://allcontributors.org/docs/en/cli/installation): ```bash yarn add --dev all-contributors-cli ``` Then, you can add contributors: ```bash yarn all-contributors add henryiii maintenance,code,doc ```
scikit-hep-boost-histogram-b4c49b0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001524660700500230325ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/.github/ISSUE_TEMPLATE/bug-report.md000066400000000000000000000004361524660700500254450ustar00rootroot00000000000000--- name: Bug report about: Create a report title: "[BUG] " labels: unverified-bug --- **Describe the bug** **Steps to reproduce** scikit-hep-boost-histogram-b4c49b0/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000005271524660700500250260ustar00rootroot00000000000000blank_issues_enabled: True contact_links: - name: Start a discussion url: https://github.com/scikit-hep/boost-histogram/discussions/new about: Please ask and answer questions here, or propose new ideas. - name: Chat on Gitter url: https://gitter.im/HSF/PyHEP-histogramming about: Ask a very simple question or just say hi. scikit-hep-boost-histogram-b4c49b0/.github/dependabot.yml000066400000000000000000000003411524660700500234750ustar00rootroot00000000000000version: 2 updates: # Maintain dependencies for GitHub Actions - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" groups: actions: patterns: - "*" scikit-hep-boost-histogram-b4c49b0/.github/release.yml000066400000000000000000000001261524660700500230110ustar00rootroot00000000000000changelog: exclude: authors: - dependabot[bot] - pre-commit-ci[bot] scikit-hep-boost-histogram-b4c49b0/.github/workflows/000077500000000000000000000000001524660700500227045ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/.github/workflows/copilot-setup-steps.yml000066400000000000000000000015541524660700500273770ustar00rootroot00000000000000name: "Copilot Setup Steps" permissions: {} on: workflow_dispatch: push: paths: - .github/workflows/copilot-setup-steps.yml pull_request: paths: - .github/workflows/copilot-setup-steps.yml jobs: copilot-setup-steps: name: Copilot Setup Steps runs-on: ubuntu-latest permissions: contents: read steps: - uses: actions/checkout@v7 with: persist-credentials: false submodules: true fetch-depth: 0 - uses: actions/setup-python@v7 with: python-version: "3.x" allow-prereleases: true - uses: astral-sh/setup-uv@v10.0.1 - name: Install tooling run: | uv tool install nox uv tool install prek - name: Pre-install checks run: | prek prepare-hooks nox -s pylint --install-only scikit-hep-boost-histogram-b4c49b0/.github/workflows/examples.yml000066400000000000000000000017231524660700500252500ustar00rootroot00000000000000name: Examples on: workflow_dispatch: pull_request: paths: - "examples/*.py" - "notebooks/*.ipynb" - ".github/workflows/examples.yml" push: branches: - master - develop permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: examples: runs-on: ubuntu-latest timeout-minutes: 30 steps: - uses: actions/checkout@v7 with: fetch-depth: 0 submodules: true - uses: actions/setup-python@v7 with: python-version: "3.10" - uses: astral-sh/setup-uv@v10.0.1 - name: Build and install kernel run: uv run --group examples -m ipykernel install --user --name boost-hist - name: Examples run: for f in examples/*.py; do uv run "$f"; done - name: Notebooks run: uv run jupyter nbconvert --execute --ExecutePreprocessor.timeout=90 --inplace notebooks/*.ipynb scikit-hep-boost-histogram-b4c49b0/.github/workflows/tests.yml000066400000000000000000000106151524660700500245740ustar00rootroot00000000000000name: Tests on: workflow_dispatch: pull_request: push: branches: - master - develop env: FORCE_COLOR: 3 permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: clang-tidy: name: Clang-Tidy runs-on: ubuntu-latest container: silkeh/clang:20 timeout-minutes: 30 steps: - name: Install requirements run: apt-get update && apt-get install -y python3-dev python3-pip git ninja-build - uses: actions/checkout@v7 with: fetch-depth: 0 submodules: true - name: Install extra requirements run: python3 -m pip install setuptools_scm --break-system-packages - name: Configure run: cmake --preset tidy - name: Build run: cmake --build --preset tidy pylint: runs-on: ubuntu-latest name: PyLint timeout-minutes: 15 steps: - uses: actions/checkout@v7 with: fetch-depth: 0 submodules: true - name: Run PyLint run: pipx run nox[uv] -s pylint -- --output-format=github cmake: name: CMake � ${{ matrix.python-version }} runs-on: ubuntu-latest timeout-minutes: 30 strategy: fail-fast: false matrix: include: - python-version: "3.10" cmake-extras: "-DCMAKE_CXX_STANDARD=17" - python-version: "3.11" - python-version: "3.14" - python-version: "3.14t" - python-version: "3.15" - python-version: "pypy3.11" steps: - uses: actions/checkout@v7 with: fetch-depth: 0 submodules: true - uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} allow-prereleases: true - uses: rui314/setup-mold@v1 - uses: astral-sh/setup-uv@v10.0.1 - uses: hendrikmuhs/ccache-action@v1.2 with: key: ${{ matrix.python-version }} create-symlink: true - name: Install python tools run: uv pip install --system --python=python --group github - name: Configure run: cmake --preset default ${{ matrix.cmake-extras }} - name: Build run: cmake --build --preset default - name: Test run: ctest --preset default -j 4 hist: name: Hist � ${{ matrix.python-version }} runs-on: ubuntu-latest timeout-minutes: 30 strategy: fail-fast: false matrix: python-version: ["3.10", "3.14"] steps: - uses: actions/checkout@v7 with: fetch-depth: 0 submodules: true - uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} allow-prereleases: true - uses: astral-sh/setup-uv@v10.0.1 - name: Run Hist tests run: uvx nox -s hist build_wheels: name: ${{ matrix.only }} runs-on: ${{ matrix.os }} timeout-minutes: 60 strategy: fail-fast: false matrix: include: - os: ubuntu-latest only: cp314t-manylinux_x86_64 - os: ubuntu-24.04-arm only: cp313-manylinux_aarch64 - os: windows-latest only: cp310-win32 - os: windows-latest only: cp313-win_amd64 - os: windows-latest only: cp314t-win_amd64 - os: macos-15-intel only: cp314t-macosx_x86_64 - os: macos-latest only: cp312-macosx_arm64 - os: windows-11-arm only: cp311-win_arm64 - os: ubuntu-latest only: cp315-manylinux_x86_64 - os: ubuntu-latest only: gp312_250-manylinux_x86_64 steps: - uses: actions/checkout@v7 with: submodules: true fetch-depth: 0 - uses: astral-sh/setup-uv@v10.0.1 - uses: pypa/cibuildwheel@v4.2 with: only: "${{ matrix.only }}" - uses: actions/upload-artifact@v7 with: path: wheelhouse/* name: test-wheels-${{ strategy.job-index }} - name: Check wheels run: pipx run twine check wheelhouse/* shell: bash pass: if: always() needs: [clang-tidy, pylint, cmake, hist, build_wheels] runs-on: ubuntu-slim timeout-minutes: 5 steps: - uses: re-actors/alls-green@release/v1 with: jobs: ${{ toJSON(needs) }} scikit-hep-boost-histogram-b4c49b0/.github/workflows/weekly.yml000066400000000000000000000013441524660700500247310ustar00rootroot00000000000000name: Weekly update on: workflow_dispatch: schedule: - cron: "0 12 * * 0" permissions: contents: read jobs: basic: name: Head on Python 3.13 runs-on: ubuntu-latest timeout-minutes: 30 steps: - uses: actions/checkout@v7 with: submodules: true # Note: you need to add a git pull here if you are not on a clean checkout, but CI always is - name: Update to latest run: git submodule foreach "git checkout develop || git checkout master" - uses: actions/setup-python@v7 with: python-version: "3.13" - name: Build and install wheel run: | pip install -v . --group test - name: Test run: python -m pytest scikit-hep-boost-histogram-b4c49b0/.github/workflows/wheels.yml000066400000000000000000000136331524660700500247240ustar00rootroot00000000000000name: Wheels on: workflow_dispatch: inputs: overrideVersion: description: Manually force a version release: types: - published pull_request: paths: - .github/workflows/wheels.yml schedule: - cron: "34 3 * * *" permissions: actions: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: SETUPTOOLS_SCM_PRETEND_VERSION: ${{ github.event.inputs.overrideVersion }} jobs: build_sdist: name: Build SDist runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v7 with: fetch-depth: 0 submodules: true - name: Build SDist run: pipx run build --sdist - name: Check metadata run: pipx run twine check --strict dist/*.tar.gz - uses: actions/upload-artifact@v7 with: path: dist/* name: wheels-sdist build_wheels: name: ${{ matrix.build }} ${{ matrix.arch }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - os: macos-15-intel arch: auto64 build: "*" - os: macos-latest arch: auto64 build: "*" - os: windows-latest arch: auto64 build: "cp*" - os: windows-latest arch: auto64 build: "{p,g}p*" - os: windows-latest arch: auto32 build: "*" - os: windows-11-arm arch: auto build: "*" - os: ubuntu-latest arch: auto64 build: "*manylinux*" - os: ubuntu-latest arch: auto64 build: "*musllinux*" - os: ubuntu-24.04-arm arch: auto64 build: "*manylinux*" - os: ubuntu-24.04-arm arch: auto64 build: "*musllinux*" steps: - uses: actions/checkout@v7 with: fetch-depth: 0 submodules: true - uses: astral-sh/setup-uv@v10.0.1 - uses: pypa/cibuildwheel@v4.2 env: CIBW_BUILD: ${{ matrix.build }} CIBW_ARCHS: ${{ matrix.arch }} - name: Verify clean directory run: git diff --exit-code shell: bash - name: Upload wheels uses: actions/upload-artifact@v7 with: path: wheelhouse/*.whl name: wheels-${{ strategy.job-index }} build_pyodide_wheels: name: Pyodide runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 with: fetch-depth: 0 submodules: true - uses: pypa/cibuildwheel@v4.2 env: CIBW_PLATFORM: pyodide - name: Verify clean directory run: git diff --exit-code shell: bash - name: Upload wheels uses: actions/upload-artifact@v7 with: path: wheelhouse/*.whl name: wheels-pyodide build_ios_wheels: name: iOS ${{ matrix.runs-on }} runs-on: ${{ matrix.runs-on }} strategy: fail-fast: false matrix: runs-on: [macos-latest, macos-15-intel] steps: - uses: actions/checkout@v7 with: fetch-depth: 0 submodules: true - run: brew uninstall cmake; brew install cmake - uses: pypa/cibuildwheel@v4.2 env: CIBW_PLATFORM: ios - name: Verify clean directory run: git diff --exit-code shell: bash - name: Upload wheels uses: actions/upload-artifact@v7 with: path: wheelhouse/*.whl name: wheels-ios-${{ matrix.runs-on }} # build_android_wheels: # name: Android ${{ matrix.runs-on }} # runs-on: ${{ matrix.runs-on }} # strategy: # fail-fast: false # matrix: # runs-on: [ubuntu-latest, macos-latest] # steps: # - uses: actions/checkout@v7 # with: # fetch-depth: 0 # submodules: true # # - uses: mhsmith/cibuildwheel@android # env: # CIBW_PLATFORM: android # # - name: Verify clean directory # run: git diff --exit-code # shell: bash # # - name: Upload wheels # uses: actions/upload-artifact@v7 # with: # path: wheelhouse/*.whl # name: wheels-android-${{ matrix.runs-on }} upload_all: name: Upload if release needs: [build_wheels, build_pyodide_wheels, build_ios_wheels, build_sdist] runs-on: ubuntu-latest if: github.event_name == 'release' && github.event.action == 'published' environment: name: pypi url: https://pypi.org/p/boost-histogram permissions: id-token: write attestations: write contents: read steps: - uses: actions/download-artifact@v8 with: pattern: wheels-* merge-multiple: true path: dist - name: List all files run: ls -lh dist - name: Generate artifact attestation for sdist and wheels uses: actions/attest-build-provenance@v4 with: subject-path: "dist/boost_histogram-*" - uses: pypa/gh-action-pypi-publish@release/v1 upload_nightly_wheels: name: Upload nightly wheels to Anaconda Cloud if: | (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule') && !github.event.repository.fork && github.ref == 'refs/heads/develop' needs: [build_wheels] runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v8 with: pattern: wheels-* merge-multiple: true path: dist - name: List all files run: ls -lh dist - name: Upload wheel to Anaconda Cloud as nightly uses: scientific-python/upload-nightly-action@e76cfec8a4611fd02808a801b0ff5a7d7c1b2d99 # 0.6.4 with: artifacts_path: dist anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }} scikit-hep-boost-histogram-b4c49b0/.gitignore000066400000000000000000000020171524660700500212770ustar00rootroot00000000000000# Prerequisites *.d # Compiled Object files *.slo *.lo *.o *.obj # Precompiled Headers *.gch *.pch # Compiled Dynamic libraries *.so *.dylib *.dll # Fortran module files *.mod *.smod # Compiled Static libraries *.lai *.la *.a *.lib # Executables *.exe *.out *.app # Build directories /*build* /docs/_build/* /examples/*/*build* /pip-wheel-metadata/* # Outputs /examples/*.png /examples/*.pkl /tests/*.pkl /*.pkl /notebooks/*.pkl # Python binary files *.pyc *__pycache__* *.egg-info .ipynb_checkpoints /wheelhouse/* /dist/* *.mypy_cache # Other editor files *.swp *~ *.clangd /.cache/* compile_commands.json # Venvs such as /.env /*env* # Generated version /src/boost_histogram/version.py # Allow all contributors to be installed /node_modules/* /yarn.lock /package.json /package-lock.json # Testing /.benchmarks/* /.hypothesis/* # PyCharm /.idea/* # VSCode /.vscode/* # Cache /emsdk-cache/* # Nox .nox/* /*venv* /build .DS_Store # Locking uv.lock *pylock # CLAUDE.md should be a symlink to AGENTS.md CLAUDE.md .claude/ scikit-hep-boost-histogram-b4c49b0/.gitmodules000066400000000000000000000011431524660700500214630ustar00rootroot00000000000000[submodule "extern/histogram"] path = extern/histogram url = ../../boostorg/histogram.git [submodule "extern/core"] path = extern/core url = ../../boostorg/core.git [submodule "extern/mp11"] path = extern/mp11 url = ../../boostorg/mp11.git [submodule "extern/config"] path = extern/config url = ../../boostorg/config.git [submodule "extern/throw_exception"] path = extern/throw_exception url = ../../boostorg/throw_exception.git [submodule "extern/assert"] path = extern/assert url = ../../boostorg/assert.git [submodule "extern/variant2"] path = extern/variant2 url = ../../boostorg/variant2.git scikit-hep-boost-histogram-b4c49b0/.pre-commit-config.yaml000066400000000000000000000046171524660700500236000ustar00rootroot00000000000000ci: autoupdate_commit_msg: "chore(deps): update pre-commit hooks" autofix_commit_msg: "style: pre-commit fixes" autoupdate_schedule: monthly repos: - repo: https://github.com/adamchainz/blacken-docs rev: "1.20.0" hooks: - id: blacken-docs additional_dependencies: [black==25.*] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: check-added-large-files - id: check-case-conflict - id: check-merge-conflict - id: check-symlinks - id: check-yaml - id: debug-statements - id: end-of-file-fixer - id: mixed-line-ending - id: trailing-whitespace - repo: https://github.com/cheshirekow/cmake-format-precommit rev: v0.6.13 hooks: - id: cmake-format additional_dependencies: [pyyaml] - repo: https://github.com/rbubley/mirrors-prettier rev: "v3.9.6" hooks: - id: prettier types_or: [yaml, markdown, html, css, scss, javascript, json] - repo: https://github.com/astral-sh/ruff-pre-commit rev: "v0.16.1" hooks: - id: ruff-check args: ["--fix", "--show-fixes"] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy rev: v2.3.0 hooks: - id: mypy files: ^(src|examples) additional_dependencies: [numpy~=2.4.0, pytest, uhi] - repo: https://github.com/codespell-project/codespell rev: v2.4.3 hooks: - id: codespell args: ["-L", "hist,nd,circularly,ba,fo", "-w"] exclude: ^(notebooks/xarray.ipynb|notebooks/BoostHistogramHandsOn.ipynb|.*\.svg)$ - repo: local hooks: - id: disallow-caps name: Disallow improper capitalization language: pygrep entry: PyBind|Numpy|Cmake|CCache|Github|PyTest exclude: .pre-commit-config.yaml - repo: https://github.com/pre-commit/mirrors-clang-format rev: v22.1.8 hooks: - id: clang-format types_or: [c++] - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.10.0 hooks: - id: rst-backticks - id: rst-directive-colons - id: rst-inline-touching-normal - repo: https://github.com/python-jsonschema/check-jsonschema rev: 0.37.4 hooks: - id: check-readthedocs - id: check-github-workflows - repo: https://github.com/henryiii/validate-pyproject-schema-store rev: 2026.08.01 hooks: - id: validate-pyproject scikit-hep-boost-histogram-b4c49b0/.readthedocs.yml000066400000000000000000000007511524660700500224000ustar00rootroot00000000000000# .readthedocs.yml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 build: os: "ubuntu-22.04" tools: python: "3.12" commands: - asdf plugin add uv - asdf install uv latest - asdf global uv latest - uv venv - uv pip install --group docs - .venv/bin/sphinx-build -T -b html -d docs/_build/doctrees -D language=en docs $READTHEDOCS_OUTPUT/html submodules: exclude: all scikit-hep-boost-histogram-b4c49b0/AGENTS.md000066400000000000000000000133401524660700500206130ustar00rootroot00000000000000# CLAUDE.md This file provides guidance for agents when working with code in this repository. ## What this is Python bindings for [Boost.Histogram](https://github.com/boostorg/histogram), a header-only C++14 histogramming library. This is a hybrid Python/C++ project: pybind11 wraps the C++ library into a compiled `_core` extension module, and a pure-Python layer in `src/boost_histogram/` provides the user-facing API on top of it. ## Commands Prefer `uv run ` — it rebuilds the C++ extension as needed before running. | Task | Command | | ---------------------------------- | -------------------------------------------------- | | Install for dev | `uv sync` (or `pip install -ve. --group dev`) | | Run tests | `uv run pytest` | | Run one test | `uv run pytest tests/test_histogram.py::test_name` | | Tests in parallel, no benchmark | `uv run pytest -n auto --benchmark-disable` | | Lint / format / typecheck | `prek -a --quiet` | | Type check only | `prek run mypy -a` (strict on `src/`, `examples/`) | | Build docs (serves if interactive) | `nox -s docs` | | C++ (CMake) build + test | `cmake --workflow default` | `nox -l` lists all sessions. `prek` is used instead of `pre-commit run`. The C++ build needs git submodules (`extern/`): `git submodule update --init --depth 10`. ## Testing notes - `pytest-benchmark` is a required plugin; CI passes `--benchmark-disable`. Benchmark tests live in `tests/test_benchmark_*.py`. - `filterwarnings = error` and `xfail_strict = true` — warnings fail tests, and unexpected xpasses fail. - Property-based tests use Hypothesis (`tests/test_pbt.py`); some systems need `CI=1` to pass health checks. - Python 3.14t (free-threading) is supported; thread-safety tests are in `tests/test_threaded_fill.py`. ## Architecture ### Two layers, bridged by `cast`/`register` The C++ extension exposes raw types under `_core` submodules (`_core.hist`, `_core.axis`, `_core.storage`, `_core.accumulators`, `_core.algorithm`, `_core.axis.transform`) — see `src/module.cpp`. These raw C++ objects are wrapped by Python classes in `src/boost_histogram/`. The bridge is in `src/boost_histogram/_utils.py`: - `@register({cpp_type, ...})` decorates a Python class to claim ownership of one or more C++ types. - `cast(self, cpp_object, ParentClass)` converts a raw C++ object into the right Python wrapper by walking subclasses of `ParentClass` and matching the registered C++ type. Classes that can't be constructed directly from the C++ object provide a `_convert_cpp` classmethod (pybind11 can't downcast — see the comment in `_cast_make_object`). - The `_family` attribute lets downstream libraries (e.g. [Hist](https://github.com/scikit-hep/hist)) subclass these wrappers and have `cast` return _their_ subclass instead. `boost_histogram` is the fallback family. This is why most wrapper classes are decorated and carry `_family`. When adding a new axis/storage/accumulator wrapper, follow this pattern: register the C++ type, set `_family`, and add a `_convert_cpp` if direct construction doesn't work. ### Key modules - `histogram.py` — the `Histogram` class wrapping `_core.hist`, plus UHI indexing (`loc`, `rebin`, `sum`, `underflow`, `overflow` from `tag.py`), `__getitem__`/`__setitem__`, projection, views. - `axis/__init__.py` — axis wrappers (`Regular`, `Variable`, `Integer`, `IntCategory`, `StrCategory`, `Boolean`) and `AxesTuple`; `axis/transform.py` — log/sqrt/pow/function transforms. - `storage.py`, `accumulators.py`, `view.py` — storage types, accumulators, and NumPy structured-array views (`WeightedSumView`, `MeanView`, `WeightedMeanView`). - `numpy.py` — drop-in replacements for `numpy.histogram*` functions. - `serialization/` — pickle/serialization helpers for axes and storages. ### C++ side - `src/register_*.cpp` — each registers one category (axes, storages, histograms, accumulators, transforms, algorithms) into the `_core` module defined in `src/module.cpp`. - `include/bh_python/` — the binding headers (fill logic, pickling, metadata, NumPy interop, etc.). - `extern/histogram/` and other `extern/*` are git submodules (Boost headers); don't edit them. `nox -s bump_boost -- ` updates them. - `.pyi` stubs for the compiled module live in `src/boost_histogram/_core/`. ## Conventions - `from __future__ import annotations` is required at the top of every Python file (ruff-enforced). - Use `boost_histogram._compat.typing.Self`, not `typing.Self`/`typing_extensions.Self` (banned-api). - mypy runs in strict mode on `src/` and `examples/`; tests are exempt from untyped-def rules. - Version is dynamic from git tags via `setuptools_scm`, generated at build time into `boost_histogram/version.py` (do not commit/edit it). ## Supported Python versions When the supported versions change (a new Python, a dropped one, a new cibuildwheel version, or a new wheel platform), update all of these: - `requires-python` and the `Programming Language :: Python :: *` classifiers in `pyproject.toml` - the matrix in `.github/workflows/tests.yml` and the jobs in `.github/workflows/wheels.yml` - the "Binaries available" wheel table in `README.md` Get the true list of wheels from cibuildwheel instead of writing it by hand, one call per platform: ```bash uvx cibuildwheel --platform linux --print-build-identifiers ``` Free-threaded and alternative-implementation builds are opt-in and change between cibuildwheel releases, so a version can disappear from the table without an intentional drop. scikit-hep-boost-histogram-b4c49b0/CMakeLists.txt000066400000000000000000000173441524660700500220600ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.24...3.31) project(BOOST_HISTOGRAM LANGUAGES CXX) # Version is added later include(FetchContent) # boostorg/histogram requires C++14 set(CMAKE_CXX_STANDARD 14 CACHE STRING "The C++ standard to compile with, 14+") set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) # Adding folders to keep the structure a bit nicer in IDE's set_property(GLOBAL PROPERTY USE_FOLDERS ON) # This will force color output at build time if this env variable is set _at # configure time_. This is useful for CI. if($ENV{FORCE_COLOR}) if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") add_compile_options(-fdiagnostics-color=always) elseif("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang") add_compile_options(-fcolor-diagnostics) endif() endif() # This is a standard recipe for setting a default build type set(_default_build_type "Debug") if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) message(STATUS "Setting build type to '${_default_build_type}' as none was specified.") set(CMAKE_BUILD_TYPE "${_default_build_type}" CACHE STRING "Choose the type of build." FORCE) # Set the possible values of build type for cmake-gui set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo") endif() # If no Python venv set, and .venv exists, use it. if(NOT DEFINED ENV{VIRTUAL_ENV} AND NOT DEFINED Python_ROOT_DIR AND NOT DEFINED Python_EXECUTABLE AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.venv") set(Python_ROOT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/.venv") endif() # Adding pybind11 and setting up Python # Will display pybind11 version set(PYBIND11_FINDPYTHON TRUE) set(Python_ARTIFACTS_INTERACTIVE TRUE) FetchContent_Declare( pybind11 GIT_REPOSITORY https://github.com/pybind/pybind11.git GIT_TAG d03662f0984f652b60e7ddce53d3868002275197 # v3.0.4 FIND_PACKAGE_ARGS 3 NAMES pybind11) FetchContent_MakeAvailable(pybind11) # Display versions message(STATUS "CMake ${CMAKE_VERSION}") message(STATUS "Python ${Python_VERSION_MAJOR}.${Python_VERSION_MINOR}") if(DEFINED pybind11_VERSION) message(STATUS "pybind11 ${pybind11_VERSION}") endif() # This is completely optional and just adds hints to IDEs - no affect on build at all. file(GLOB_RECURSE BOOST_HIST_FILES "extern/histogram/include/*.hpp") file(GLOB_RECURSE BOOST_HIST_PY_HEADERS "include/bh_python/*.hpp") # List the source files for the Python extension # On some backends (like make), this will regenerate if a file was added/removed file(GLOB BOOST_HIST_PY_SRC CONFIGURE_DEPENDS src/*.cpp) # This is the Python module pybind11_add_module(_core MODULE ${BOOST_HIST_PY_HEADERS} ${BOOST_HIST_PY_SRC} ${BOOST_HIST_FILES}) # Add the include directory for boost/histogram/python target_include_directories(_core PRIVATE include) # These are the Boost header-only libraries required by Boost::Histogram target_include_directories( _core SYSTEM PUBLIC extern/assert/include extern/config/include extern/core/include extern/histogram/include extern/mp11/include extern/throw_exception/include extern/variant2/include) # Support older Windows from newer MSVC target_compile_options(_core PRIVATE $<$:/d2FH4->) # Better error messages target_compile_definitions(_core PRIVATE PYBIND11_DETAILED_ERROR_MESSAGES) # Atomic is required for armv7l if(CMAKE_SYSTEM_PROCESSOR MATCHES "armv7l") target_link_libraries(_core PRIVATE atomic) endif() # This makes IDE's like XCode mimic the Boost Histogram structure source_group( TREE ${CMAKE_CURRENT_SOURCE_DIR}/extern/histogram/include PREFIX "Header Files" FILES ${BOOST_HIST_FILES}) source_group( TREE ${CMAKE_CURRENT_SOURCE_DIR}/include PREFIX "Header Files" FILES ${BOOST_HIST_PY_HEADERS}) source_group( TREE ${CMAKE_CURRENT_SOURCE_DIR}/src PREFIX "Source Files" FILES ${BOOST_HIST_PY_SRC}) # Cause warnings to be errors (not recommended for MSVC, since pybind11 might cause a few there) option(BOOST_HISTOGRAM_ERRORS "Make warnings errors (for CI mostly)") # Adding warnings # Boost.Histogram doesn't pass sign -Wsign-conversion if("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang" OR "${CMAKE_CXX_COMPILER_ID}" MATCHES "GNU") target_compile_options( _core PRIVATE -Wall -Wextra -pedantic-errors -Wconversion -Wsign-compare -Wno-unused-value -Wno-sign-conversion) if(BOOST_HISTOGRAM_ERRORS) target_compile_options(_core PRIVATE -Werror) endif() elseif("${CMAKE_CXX_COMPILER_ID}" MATCHES "MSVC") target_compile_options(_core PRIVATE /W4) if(BOOST_HISTOGRAM_ERRORS) target_compile_options(_core PRIVATE /WX) endif() endif() # This allows setting the maximum number of axes in a histogram set(BOOST_HISTOGRAM_DETAIL_AXES_LIMIT "" CACHE STRING "Set the maximum number of axis in a histogram (affects compile time and size)") if(NOT "${BOOST_HISTOGRAM_DETAIL_AXES_LIMIT}" STREQUAL "") target_compile_definitions( _core PRIVATE BOOST_HISTOGRAM_DETAIL_AXES_LIMIT=${BOOST_HISTOGRAM_DETAIL_AXES_LIMIT}) endif() # Make the output library be in boost_histogram/_core... # This is a generator expression to avoid Debug/Release subdirectories in IDEs, # which confuses Python. set_property(TARGET _core PROPERTY LIBRARY_OUTPUT_DIRECTORY "$<1:boost_histogram>") # Collect all the python files and symlink them into the build directory # Protects from in-source builds (don't do this, please) if(NOT "${CMAKE_CURRENT_SOURCE_DIR}" STREQUAL "${CMAKE_CURRENT_BINARY_DIR}" AND NOT DEFINED SKBUILD) file( GLOB_RECURSE BOOST_HIST_PY_FILES LIST_DIRECTORIES false RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/src" CONFIGURE_DEPENDS "src/boost_histogram/*.py") foreach(F IN LISTS BOOST_HIST_PY_FILES) file(REMOVE "${CMAKE_CURRENT_BINARY_DIR}/${F}") file(CREATE_LINK "${CMAKE_CURRENT_SOURCE_DIR}/src/${F}" "${CMAKE_CURRENT_BINARY_DIR}/${F}" COPY_ON_ERROR SYMBOLIC) endforeach() endif() # Support installing install(TARGETS _core DESTINATION "boost_histogram") if(NOT DEFINED SKBUILD) install(DIRECTORY "src/boost_histogram" DESTINATION ".") install(FILES "${CMAKE_CURRENT_BINARY_DIR}/boost_histogram/version.py" DESTINATION "boost_histogram") # Tests (Requires pytest to be available to run) include(CTest) endif() if(DEFINED SKBUILD) # Don't worry about the version elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/boost_histogram/version.py") set(VERSION_REGEX [=[version[ \t]*=[ \t]*["']([0-9]+\.[0-9]+\.[0-9]+)]=]) # Read in the line containing the version file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/src/boost_histogram/version.py" VERSION_STRING REGEX [=[version[ \t]*=]=]) # Pick out just the version string(REGEX MATCH [=[[0-9]+\.[0-9]+\.[0-9]+]=] VERSION_STRING "${VERSION_STRING}") else() pybind11_find_import(setuptools_scm REQUIRED) execute_process( COMMAND ${Python_EXECUTABLE} -c "from setuptools_scm import get_version; print(get_version())" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" OUTPUT_VARIABLE VERSION_FULL_STRING OUTPUT_STRIP_TRAILING_WHITESPACE COMMAND_ERROR_IS_FATAL ANY) string(REGEX MATCH [=[^[0-9]+\.[0-9]+\.[0-9]+]=] VERSION_STRING "${VERSION_FULL_STRING}") string(REPLACE "-" "." VERSION_STRING "${VERSION_STRING}") file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/boost_histogram/version.py" "version = '${VERSION_FULL_STRING}'") message(STATUS "Full version output: ${VERSION_FULL_STRING}") endif() if(NOT DEFINED SKBUILD) project( BOOST_HISTOGRAM LANGUAGES CXX VERSION ${VERSION_STRING}) message(STATUS "boost-histogram ${BOOST_HISTOGRAM_VERSION}") if(BUILD_TESTING) add_subdirectory(tests) endif() endif() scikit-hep-boost-histogram-b4c49b0/CMakePresets.json000066400000000000000000000042651524660700500225370ustar00rootroot00000000000000{ "version": 6, "configurePresets": [ { "name": "default", "displayName": "Default configure", "generator": "Ninja", "binaryDir": "build", "warnings": { "dev": true, "deprecated": true }, "cacheVariables": { "BOOST_HISTOGRAM_ERRORS": "ON", "CMAKE_BUILD_TYPE": "Debug", "CMAKE_EXPORT_COMPILE_COMMANDS": true } }, { "name": "tidy", "displayName": "Clang-tidy", "inherits": "default", "binaryDir": "build-tidy", "cacheVariables": { "CMAKE_CXX_CLANG_TIDY": "clang-tidy;--warnings-as-errors=*" } }, { "name": "tidy-fix", "displayName": "Clang-tidy autofix", "inherits": "tidy", "cacheVariables": { "CMAKE_CXX_CLANG_TIDY": "clang-tidy;--warnings-as-errors=*;--fix" } } ], "buildPresets": [ { "name": "default", "displayName": "Default build", "verbose": true, "configurePreset": "default" }, { "name": "tidy", "displayName": "Clang-tidy build", "configurePreset": "tidy", "nativeToolOptions": ["-k0"] }, { "name": "tidy-fix", "displayName": "Clang-tidy autofix build", "configurePreset": "tidy-fix", "jobs": 1, "nativeToolOptions": ["-k0"] } ], "testPresets": [ { "name": "default", "displayName": "Default test", "configurePreset": "default", "output": { "outputOnFailure": true } } ], "workflowPresets": [ { "name": "default", "displayName": "Default workflow", "steps": [ { "type": "configure", "name": "default" }, { "type": "build", "name": "default" }, { "type": "test", "name": "default" } ] }, { "name": "tidy", "displayName": "Clang-tidy workflow", "steps": [ { "type": "configure", "name": "tidy" }, { "type": "build", "name": "tidy" } ] }, { "name": "tidy-fix", "displayName": "Clang-tidy autofix workflow", "steps": [ { "type": "configure", "name": "tidy-fix" }, { "type": "build", "name": "tidy-fix" } ] } ] } scikit-hep-boost-histogram-b4c49b0/LICENSE000066400000000000000000000030031524660700500203100ustar00rootroot00000000000000BSD 3-Clause License Copyright (c) 2018-2019, Henry Schreiner and Hans Dembinski All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. scikit-hep-boost-histogram-b4c49b0/README.md000066400000000000000000000642361524660700500206010ustar00rootroot00000000000000boost-histogram logo # boost-histogram for Python [![Actions Status][actions-badge]][actions-link] [![Documentation Status][rtd-badge]][rtd-link] [![PyPI version][pypi-version]][pypi-link] [![Conda-Forge][conda-badge]][conda-link] [![PyPI platforms][pypi-platforms]][pypi-link] [![DOI](https://zenodo.org/badge/148885351.svg)](https://zenodo.org/badge/latestdoi/148885351) [![GitHub Discussion][github-discussions-badge]][github-discussions-link] [![Gitter][gitter-badge]][gitter-link] [![Scikit-HEP][sk-badge]](https://scikit-hep.org/) [![SPEC 4 — Using and Creating Nightly Wheels][spec4-badge]][spec4-link] Python bindings for [Boost::Histogram][] ([source][Boost::Histogram source]), a C++14 library. This is one of the [fastest libraries][] for histogramming, while still providing the power of a full histogram object. See [what's new](./docs/changelog.md). Other members of the boost-histogram family include: - [Hist][]: The first-party analyst-friendly histogram library that extends boost-histogram with named axes, many new shortcuts including UHI+, plotting shortcuts, and more. - [UHI][]: Specification for Histogram library interop, especially for plotting. - [mplhep][]: Plotting extension for matplotlib with support for UHI histograms. - [histoprint][]: Histogram display library for the command line with support for UHI. - [dask-histogram][]: Dask support for boost-histogram. [uhi]: https://uhi.readthedocs.io [dask-histogram]: https://dask-histogram.readthedocs.io/en/stable/ [mplhep]: https://mplhep.readthedocs.io/en/latest/ [histoprint]: https://github.com/scikit-hep/histoprint ## Usage ![Slideshow of features. See expandable text below if the image is not readable.](https://github.com/scikit-hep/boost-histogram/raw/develop/docs/_images/banner.gif)
Text intro (click to expand) ```python import boost_histogram as bh # Compose axis however you like; this is a 2D histogram hist = bh.Histogram( bh.axis.Regular(2, 0, 1), bh.axis.Regular(4, 0.0, 1.0), ) # Filling can be done with arrays, one per dimension hist.fill( [0.3, 0.5, 0.2], [0.1, 0.4, 0.9], ) # NumPy array view into histogram counts, no overflow bins values = hist.values() # Make a new histogram with just the second axis, summing over the first, and # rebinning the second into larger bins: h2 = hist[::sum, :: bh.rebin(2)] ```
We support the [uhi][] [PlottableHistogram][] protocol, so boost-histogram/[Hist][] histograms can be plotted via any compatible library, such as [mplhep][]. [uhi]: https://github.com/scikit-hep/uhi [PlottableHistogram]: https://uhi.readthedocs.io/en/latest/plotting.html [mplhep]: https://github.com/scikit-hep/mplhep ## Cheatsheet
Simplified list of features (click to expand) - Many axis types (all support `metadata=...`) - `bh.axis.Regular(n, start, stop, ...)`: Make a regular axis. Options listed below. - `overflow=False`: Turn off overflow bin - `underflow=False`: Turn off underflow bin - `growth=True`: Turn on growing axis, bins added when out-of-range items added - `circular=True`: Turn on wrapping, so that out-of-range values wrap around into the axis - `transform=bh.axis.transform.Log`: Log spacing - `transform=bh.axis.transform.Sqrt`: Square root spacing - `transform=bh.axis.transform.Pow(v)`: Power spacing - See also the flexible [Function transform](https://boost-histogram.readthedocs.io/en/latest/usage/transforms.html) - `bh.axis.Integer(start, stop, *, underflow=True, overflow=True, growth=False, circular=False)`: Special high-speed version of `regular` for evenly spaced bins of width 1 - `bh.axis.Variable([start, edge1, edge2, ..., stop], *, underflow=True, overflow=True, circular=False)`: Uneven bin spacing - `bh.axis.IntCategory([...], *, growth=False)`: Integer categories - `bh.axis.StrCategory([...], *, growth=False)`: String categories - `bh.axis.Boolean()`: A True/False axis - Axis features: - `.index(value)`: The index at a point (or points) on the axis - `.value(index)`: The value for a fractional bin (or bins) in the axis - `.bin(i)`: The bin edges (continuous axis) or a bin value (discrete axis) - `.centers`: The N bin centers (if continuous) - `.edges`: The N+1 bin edges (if continuous) - `.extent`: The number of bins (including under/overflow) - `.metadata`: Anything a user wants to store - `.traits`: The options set on the axis - `.size`: The number of bins (not including under/overflow) - `.widths`: The N bin widths - Many storage types - `bh.storage.Double()`: Doubles for weighted values (default) - `bh.storage.Int64()`: 64-bit unsigned integers - `bh.storage.Unlimited()`: Starts small, but can go up to unlimited precision ints or doubles. - `bh.storage.AtomicInt64()`: Threadsafe filling, experimental. Does not support growing axis in threads. - `bh.storage.Weight()`: Stores a weight and sum of weights squared. - `bh.storage.Mean()`: Accepts a sample and computes the mean of the samples (profile). - `bh.storage.WeightedMean()`: Accepts a sample and a weight. It computes the weighted mean of the samples. - Accumulators - `bh.accumulator.Sum`: High accuracy sum (Neumaier) - used by the sum method when summing a numerical histogram - `bh.accumulator.WeightedSum`: Tracks a weighted sum and variance - `bh.accumulator.Mean`: Running count, mean, and variance (Welfords's incremental algorithm) - `bh.accumulator.WeightedMean`: Tracks a weighted sum, mean, and variance (West's incremental algorithm) - Histogram operations - `h.ndim`: The number of dimensions - `h.size or len(h)`: The number of bins - `+`: Add two histograms (storages must match types currently) - `*=`: Multiply by a scaler (not all storages) (`hist * scalar` and `scalar * hist` supported too) - `/=`: Divide by a scaler (not all storages) (`hist / scalar` supported too) - `.kind`: Either `bh.Kind.COUNT` or `bh.Kind.MEAN`, depending on storage - `.storage_type`: Fetch the histogram storage type - `.sum(flow=False)`: The total count of all bins - `.project(ax1, ax2, ...)`: Project down to listed axis (numbers). Can also reorder axes. - `.to_numpy(flow=False, view=False)`: Convert to a NumPy style tuple (with or without under/overflow bins) - `.view(flow=False)`: Get a view on the bin contents (with or without under/overflow bins) - `.values(flow=False)`: Get a view on the values (counts or means, depending on storage) - `.variances(flow=False)`: Get the variances if available - `.counts(flow=False)`: Get the effective counts for all storage types - `.reset()`: Set counters to 0 (growing axis remain the same size) - `.empty(flow=False)`: Check to see if the histogram is empty (can check flow bins too if asked) - `.copy(deep=False)`: Make a copy of a histogram - `.axes`: Get the axes as a tuple-like (all properties of axes are available too) - `.axes[0]`: Get the 0th axis - `.axes.edges`: The lower values as a broadcasting-ready array - `.axes.centers`: The centers of the bins broadcasting-ready array - `.axes.widths`: The bin widths as a broadcasting-ready array - `.axes.metadata`: A tuple of the axes metadata - `.axes.size`: A tuple of the axes sizes (size without flow) - `.axes.extent`: A tuple of the axes extents (size with flow) - `.axes.bin(*args)`: Returns the bin edges as a tuple of pairs (continuous axis) or values (describe) - `.axes.index(*args)`: Returns the bin index at a value for each axis - `.axes.value(*args)`: Returns the bin value at an index for each axis - Indexing - Supports [UHI Indexing](https://uhi.readthedocs.io/en/latest/indexing.html) - Bin content access / setting - `v = h[b]`: Access bin content by index number - `v = h[{0:b}]`: All actions can be represented by `axis:item` dictionary instead of by position (mostly useful for slicing) - Slicing to get histogram or set array of values - `h2 = h[a:b]`: Access a slice of a histogram, cut portions go to flow bins if present - `h2 = h[:, ...]`: Using `:` and `...` supported just like NumPy - `h2 = h[::sum]`: Third item in slice is the "action" - `h[...] = array`: Set the bin contents, either include or omit flow bins - Special accessors - `bh.loc(v)`: Supply value in axis coordinates instead of bin number - `bh.underflow`: The underflow bin (use empty beginning on slice for slicing instead) - `bh.overflow`: The overflow bin (use empty end on slice for slicing instead) - Special actions (third item in slice) - `sum`: Remove axes via projection; if limits are given, use those - `bh.rebin(n)`: Rebin an axis - NumPy compatibility - `bh.numpy` provides faster [drop in replacements](https://boost-histogram.readthedocs.io/en/latest/usage/numpy.html) for NumPy histogram functions - Histograms follow the buffer interface, and provide `.view()` - Histograms can be converted to NumPy style output tuple with `.to_numpy()` - Details - All objects support copy/deepcopy/pickle - Fully statically typed, tested with MyPy.
## Installation You can install this library from [PyPI](https://pypi.org/project/boost-histogram/) with pip: ```bash python3 -m pip install boost-histogram ``` All the normal best-practices for Python apply; Pip should not be very old (Pip 9 is very old), you should be in a virtual environment, etc. Python 3.10+ is required; for older versions of Python (3.5 and 2.7), `0.13` will be installed instead, which is API equivalent to 1.0, but will not be gaining new features. 1.3.x was the last series to support Python 3.6. 1.4.x was the last series to support Python 3.7. 1.5.x was the last series to support Python 3.8. 1.6.x was the last to support Python 3.9. #### Binaries available: The easiest way to get boost-histogram is to use a binary wheel, which happens when you run the above command on a supported platform. Wheels are produced using [cibuildwheel](https://cibuildwheel.readthedocs.io/en/stable/); all common platforms have wheels provided in boost-histogram: | System | Arch | Python versions | PyPy versions | GraalPy versions | | --------- | ------ | ------------------------------------------------ | ------------- | ---------------- | | manylinux | 64-bit | 3.10, 3.11, 3.12, 3.13, 3.14, 3.14t, 3.15, 3.15t | 3.11 | 25.0 | | manylinux | ARM64 | 3.10, 3.11, 3.12, 3.13, 3.14, 3.14t, 3.15, 3.15t | 3.11 | 25.0 | | musllinux | 64-bit | 3.10, 3.11, 3.12, 3.13, 3.14, 3.14t, 3.15, 3.15t | | | | musllinux | ARM64 | 3.10, 3.11, 3.12, 3.13, 3.14, 3.14t, 3.15, 3.15t | | | | macOS | 64-bit | 3.10, 3.11, 3.12, 3.13, 3.14, 3.14t, 3.15, 3.15t | 3.11 | 25.0 | | macOS | Arm64 | 3.10, 3.11, 3.12, 3.13, 3.14, 3.14t, 3.15, 3.15t | 3.11 | 25.0 | | Windows | 32-bit | 3.10, 3.11, 3.12, 3.13, 3.14, 3.14t, 3.15, 3.15t | | | | Windows | 64-bit | 3.10, 3.11, 3.12, 3.13, 3.14, 3.14t, 3.15, 3.15t | 3.11 | 25.0 | | Windows | ARM64 | 3.10, 3.11, 3.12, 3.13, 3.14, 3.14t, 3.15, 3.15t | | | | iOS | Device | 3.13, 3.14, 3.15 | | | | iOS | AS Sim | 3.13, 3.14, 3.15 | | | | iOS | Intel | 3.13, 3.14, 3.15 | | | | Pyodide | wasm32 | 3.13, 3.14, 3.15 | | | PowerPC, IBM-Z, and RISC-V wheels are not provided but are available on request. If you are on a Linux system that is not part of the "many" in manylinux or musl in musllinux, such as ClearLinux, building from source is usually fine, since the compilers on those systems are often quite new. It will just take longer to install when it is using the sdist instead of a wheel. All dependencies are header-only and included. #### Conda-Forge The boost-histogram package is available on [conda-forge](https://github.com/conda-forge/boost-histogram-feedstock), as well. All supported variants are available. ```bash conda install -c conda-forge boost-histogram ``` #### Source builds For a source build, for example from an "SDist" package, the only requirements are a C++14 compatible compiler. The compiler requirements are dictated by Boost.Histogram's C++ requirements: gcc >= 5.5, clang >= 3.8, or msvc >= 14.1. Boost is not required or needed (this only depends on included header-only dependencies). You can install directly from GitHub if you would like. ```bash python -m pip install git+https://github.com/scikit-hep/boost-histogram.git@develop ``` ## Developing See [CONTRIBUTING.md](.github/CONTRIBUTING.md) for details on how to set up a development environment. ## Contributors We would like to acknowledge the contributors that made this project possible ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
Henry Schreiner
Henry Schreiner

🚧 💻 📖
Hans Dembinski
Hans Dembinski

🚧 💻
N!no
N!no

⚠� 📖
Jim Pivarski
Jim Pivarski

🤔
Nicholas Smith
Nicholas Smith

�
physicscitizen
physicscitizen

�
Chanchal Kumar Maji
Chanchal Kumar Maji

📖
Doug Davis
Doug Davis

�
Pierre Grimaud
Pierre Grimaud

📖
Beojan Stanislaus
Beojan Stanislaus

�
Popinaodude
Popinaodude

�
Congqiao Li
Congqiao Li

�
alexander-held
alexander-held

�
Chris Burr
Chris Burr

📖
Konstantin Gizdov
Konstantin Gizdov

📦 �
Kyle Cranmer
Kyle Cranmer

📖
Aman Goel
Aman Goel

📖 💻
Jay Gohil
Jay Gohil

📖
Peter Fackeldey
Peter Fackeldey

💻
Jonas Eschle
Jonas Eschle

💻
Saransh Chopra
Saransh Chopra

💻
Matthew Feickert
Matthew Feickert

💻
Andrzej Novak
Andrzej Novak

💻
Dmitry Kalinkin
Dmitry Kalinkin

💻
Kilian Lieret
Kilian Lieret

📖
Marcel Rieger
Marcel Rieger

💻
Florian Harz
Florian Harz

💻
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. ## Talks and other documentation/tutorial sources The [official documentation is here](https://boost-histogram.readthedocs.io/en/latest/index.html), and includes a [quickstart](https://boost-histogram.readthedocs.io/en/latest/usage/quickstart.html). - [2019-4-15 IRIS-HEP Topical meeting](https://indico.cern.ch/event/803122/) - [2019-10-17 PyHEP Histogram session](https://indico.cern.ch/event/833895/contributions/3577833/) - [repo with talks and workbook](https://github.com/henryiii/pres-bhandhist) - [2019-11-7 CHEP](https://indico.cern.ch/event/773049/contributions/3473265/) - [2020-07-07 SciPy](https://www.youtube.com/watch?v=ERraTfHkPd0&list=PLYx7XA2nY5GfY4WWJjG5cQZDc7DIUmn6Z&index=4) - [2020-07-17 PyHEP](https://indico.cern.ch/event/882824/contributions/3931299/) --- ## Acknowledgements This library was primarily developed by Henry Schreiner and Hans Dembinski. 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/boost-histogram/workflows/Tests/badge.svg [actions-link]: https://github.com/scikit-hep/boost-histogram/actions [conda-badge]: https://img.shields.io/conda/vn/conda-forge/boost-histogram [conda-link]: https://github.com/conda-forge/boost-histogram-feedstock [github-discussions-badge]: https://img.shields.io/static/v1?label=Discussions&message=Ask&color=blue&logo=github [github-discussions-link]: https://github.com/scikit-hep/boost-histogram/discussions [gitter-badge]: https://badges.gitter.im/HSF/PyHEP-histogramming.svg [gitter-link]: https://gitter.im/HSF/PyHEP-histogramming?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge [pypi-link]: https://pypi.org/project/boost-histogram/ [pypi-platforms]: https://img.shields.io/pypi/pyversions/boost-histogram [pypi-version]: https://badge.fury.io/py/boost-histogram.svg [rtd-badge]: https://readthedocs.org/projects/boost-histogram/badge/?version=latest [rtd-link]: https://boost-histogram.readthedocs.io/en/latest/?badge=latest [sk-badge]: https://scikit-hep.org/assets/images/Scikit--HEP-Project-blue.svg [Boost::Histogram]: https://www.boost.org/doc/libs/release/libs/histogram/doc/html/index.html [Boost::Histogram source]: https://github.com/boostorg/histogram [Hist]: https://github.com/scikit-hep/hist [fastest libraries]: https://iscinumpy.gitlab.io/post/histogram-speeds-in-python/ [spec4-badge]: https://img.shields.io/badge/SPEC-4-green?labelColor=%23004811&color=%235CA038 [spec4-link]: https://scientific-python.org/specs/spec-0004/ scikit-hep-boost-histogram-b4c49b0/docs/000077500000000000000000000000001524660700500202375ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/docs/Makefile000066400000000000000000000011051524660700500216740ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) scikit-hep-boost-histogram-b4c49b0/docs/_images/000077500000000000000000000000001524660700500216435ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/docs/_images/BoostHistogramPythonLogo.png000066400000000000000000001100511524660700500273360ustar00rootroot00000000000000‰PNG  IHDR%J©ð9 pHYsÃÃÇo¨dtEXtSoftwarewww.inkscape.org›î< IDATxœìÝ{xTÕ¹?ðï»f&÷pM@Q!°ZAHHÈÔšªÕ‚/G´§­öòkµö(—ÖV=m-§o­U´7=õ´GÛÚJ=V‚R/­x! šÖ ’€ˆr„Ü3³×ûû#A“!ÉLfÖÞ{vò~žÇG™Ù{­×�0³ß½ÖzA Å×L"å{˜µZ†Ú;7»ŽB!„]Æ.;Zw¤�ƒA�3:±wÏS{ZÝŽK$†Ü@CT²t €"Ü¡Ñþ=ÔÜv;0!„Bˆ„]‰@î;ùåL|ˆf˜  €¿Ÿ£[¼`mòiÞxx;7ŒV$H’’ábö²eD|gß¹š}þËQý³7Ü J!„"ð�•wžfºÝ\s’hm+Œ òP{ÕÁ�†"†IR2_{ )µ@f?ï¶3øFÔÜ}7v82!„Bˆ¸�>ôX˸LW8Öpó æ¿øqËšýë ·-’$I‰×M»6�F«jteÀ.Ç÷säã¬Â_Á¦_¾ç\pB!„±�:oÔ8+=íbú€\Û;$¼À ¶®Þû´í}‰¸øÜ@$GËoèÂ#f¢ÿæ¿ÑYÒzú1bßplhÞÝøšó‘ !„BDY•�3árø}« ô)éõ<…€ËÓNÊùDúÉ™»^o{ß¡~Åd¤ÄËæ\{&iõêÈKÌôoØr×ÿaÎòBÒú§EŸFÀ*ÝÕu^ùÕA'ÃB!„8"ëü¼¥è·Nu9”v0nn±Fÿk·uºˈ¥b"RRѲ1¤Õèg¸éÎíœ;¶‚�ô©ÀÅÀbJK¯ÅœkÏt R!„Bˆ�TÀŸ[™½Rêe¸Ÿ�@&?ÌñÞ’½hÂÌØ‡ ;HRâQÊÏ¿0ùèwôG‹Ù×­ˆ fåmLêt€£*pñÒêYU²t%¦]ëÔP©B!F°ÑçO,Ì•¿��[N‹}†“øbÞ�³pÂ×ÜŽd$’¤Ä‹f/û"3.ë÷=RGWØÚ|g5§ùŠˆ®ÀE ,¡Ñj3æ,+²'X!„B «2¾¥x3JÜŽe™ þUNeþ#y‹òì_p/>$I‰×”]$╾Oºÿ²¿ëïl×5+—2ô§ìŽzwi^�’¥×+äwB!„&QNeÞM Xð8·ƒ‰ÓÅŒõ£/œ0Õí@F ¹õ”Š"þß5ð14ø^$5÷<ÅM'ࡨw2¸•f|s–&ªB!Äâã3s*óÿÐÁs÷�4ݲP�½h§܎d$ðØ/ÇWrð» |qºîgúV´º»>Ð5+?ËÀ¥úVà"Ì#­k1{ɕɄ*„Bˆ‘-·rR^NGçSè¾ßð(GÌke�‰ý$)ñŠÙKJøn옾՟š•«XGЬ‹zgÝK%KV¡ì?Æ%L!„Bˆì &ž tmpºÛ±àëYg²²�†m$)ñ‚™×eá�ÆÛ®ýÅ®Yyƒ—ˆªÍM—�åoÀì¥ �÷+„BˆaiÔ¢¼óIë—4ÅíX [’½0ÿ7X!÷Ïv�‹ê*Ðu@‹ë`ᦤŒš»W²R%ê¢Þ›H„ÕªtÙ½˜y]vm !„b„È]”w¥fú+€aY¹Š_ΩÉû¹Ûq G’”¤ºÒ%0è+q?Põ­xlº³�érn`õn•™¯¤@x+J—µC¼B!F¸ øsåýŠ™îàw;{ÑÕ9•¾îvÃ�Ì‹Ke3¯›@�ðVã=…‰`ó]k“î{Î’y¤éÑ¥ð"Ü¡Ñþ=ÔÜîïT!„BŒy‹òr;˜þà\·cqÐa+bMo_{`—Û� 2R’ºˆáßb @'1RÒÛ¦»_f¿žMD÷E½ãgàzBÆ‹(ûF|SÊ„B1,åÌ?&¿“éY Œòù}ßw;ˆáD’’TU²ôZ †~bBkJú·ñžÃzó]W1áû£ú)#˪CÉ’¥�7!„bÄsá1ð[/0Pêv,.ù\îEÇI•RC$)IEÅמBÀ­ �›Ìš’�l^ùû1Àš¨w2 t•,ûН™d¼_!„B¤¤ì &ž±¬—œäv,Fê@|_ðc9:“#�—8Úpçs;eڵ锩Ö8!±ø÷x·ú-£1À;[ñîÆ?áØÐn": @Z¯w§©/àØÐ6¼»ñ5ã} !„"e䞟ÀOÈw;ƒ: øZKÕ¾¯u½Þö\×mµ/çSó•ö´ùa zkšu ú›Bîz½íÏ·;üÈHIŠQ£Õ�%Üű£{â[î¾�•š २÷òˆèÿTÉÒ‡qêÕcmŒA!„.É]”·ˆž0ÆíX ÒD|isÕ¾û|xåóë/PñµÖ´bbº¾ß3Å8ìIR’JŠ—~‚�åIµaoRÒmÓ�Û9wl7èS�‹�Å”–^‹Òe¶Ç!„BÇä.Êÿ23= ÓíXL"Ð�Í«÷¯îçõÏ÷üç‰/îÏzž�žÆc³.Î;ÖÞGIJREѲ1¤ð ’ý™D,û“X·"‚š•·±Âi^ïû&O!樒¥+1íÚtGâB!„m²+'\ÇŒß`øíA²ßŸþaô‹ ÓsÌ>òg­é ú["ÑǪN¶7Ä‘A’’¡ÿ Àä¤rb¤¤·M+7qš*&ànôòDw°%4ZÕ`βħ£ !„ÂUÙ•®#ðO0 «mÑW<ýz¸³­Ï¥0½¹êý×x&úXfcs˜#‚$%© téLøŒ‘¶ì¨¾Ëú;ÛuÍÊ¥ ýiïD½;�4o@ÉÒë�òû&„BxHî¢ü{’a‰´Õÿ"uMÓûü™14è�Gµʳ#¶‘FnÝVvM�÷kÏé‘’ÞjîyŠ-šA@ô_Øtn¥Ù_@ɵÑ;Ä !„"å.šðmfÜìv6j;Üòþ–þÞ æ>ëDÝ£!~MÑ…~@@–=á�,’”¸j…¢ˆÿ·Fk’ZS2�º»>Ð5+?ÇÀ¥öy�0� ¶`ö’+Ý N!„ñè^C·¹‡�hÀº£×ˆ€&Ήz)=¾§@ôt¯a7­Í ’”¸©äà‚øL·Ã°EÍÊUÜ]Úx]Ô;£ˆè^š½ô/(ûÙU!„H1Ù‹ò¿@àÛÝŽÃnØ;Ð{Ä�”äpwòÁö÷=X’$)qËì%%Üd¼Ýˆ‹Ó·¢Õ¬ÜÉ5+Ïbð2�}Þ#üYþÌ^ºÐ�à„B-gaþ%ĸ#àé?-¾©Žš’å[?÷øŒžÿ>Üçþ×Ê ’”¸aæuÙDôãm»¹¦¤Œš»W²R%j£Þ›H„ÕªtÙ½˜y]¶Á !„¢[vå„sÑ}âs;'00ð½£#ê=wý®#Xsú*I‰ ’”¸@¥E~à$·ãpÔ¦;ø�žK„ŸÐ½Þ!f¾’ü‘ÇÝ M!„é²M˜I࿜æv,N!`àªYLÑ£(­ôÑýKŸó¨Ÿ½KÄÐIRâ´Ò%0³} ½ëعdÛ=�zóÊo³¦Ó¼Õç=â‰î%„BŒlÙ ò�!Fzs� 3°x€Q!ÅÑII3dVŽ� `lï7h³%ºF’'Íùú1Äôßn‡áºÚ»Ö32JÐw.§ } !„N«œ”¥|¨8ù œ½''»5ÿÔ~ßá£ÁïûçFJÌÑS½D$)q�²ß�ok'nlž˜ˆšÛèúè–¤D!„pÒ ¨„d ÔíP\£piÿoÐ?{ÿ‰Áÿì9þ²è#µRí6D6âHRâ$Eö/æN¥ê[B!„HY¹5ù7¸Øí8â°žÀXPW¬ƒ‡‚_@夣6?T�HCÔ�  Ž�¦ó�jƒ!I‰’” 7nož84½b%)B!’³(ï“ üÀí8bx�4>ÑRµo^sÕþ+Zªö}Ò:„' ö1)‡º¾ýâÌwðú‘?+‹6øýÖ�û+@¤Ãã±$)q;0E)õJÇK’!„ÂYç ¦?"µKÿvj‚æÇ÷½�½ ¿({aþ͹•.ËH‰BaШóF�#À“£$@ î)g¤ßq9”D\šµ(¯Ôí ¼Êéˆøyj¤D¦o !„&Yii_ Ô[$Îÿ“@›™ð6ºËþv—þeö0 P“:ÈŒ·€@»¼tCÓƒÓÿ`þ´9X»­Óí`¼F’'1“í÷Þ䥑’>$)B!’CºÒí ºQˆWÓà¼Ðüè;ïí|Õh{B³×©¹þ~Ô |Ëí@¼F’’áFFJ„Bˆ)gaÞ™Nv9ŒCD¸�Ãê7-kßÛ—h#>_çs+`ðŒÍ úf΢¼š–Õûÿäv,^"kJåÀBwo‘¤D!„0…”›£$@÷"â;±yõ¾[’IHàƒ¿~𵦂s�éþÜENs;/‘¤ÄYößx+ÏŽ”!„"A¹•“ò¾Ø¥îßgЂ–ª½_K6éM?kª-d1óšì…f¹ˆWHR2ÜtyjM‰Œ”!„Ft]Àù  u~Ÿ¯´µjïS64þóm:j ÿ}ÔÂñenâ’”8‰e¤d’”!„ Ò  �ï•TgçÙüõ½&;ZÏéP/h³£m�פžÉ©–Û�¤:IJ†OUß’}J„Bˆdå-ÊË%àlG;%ì²`-8üäávu±ç©=­ÖØÕ¾ƒrõDNå„s;�T&I‰“”#%~%%$I‰B‘¤M d8Øe'“ZÐ^up§ÝÓp©`•ðŸræ}Õí@R•$%NbªoQØCI‰¬)B!’Fpt�;ßo}lÏ+NôÕÜ’µÀ!'úr€Ÿ‰îË©Ì_‰rM.ÈpÓ契©¾%„B$‰:ÇÁþ64gìû©c½­kê ÂcŽõçŒ%9›óÿ€Š'G·Rž$%Žb èKFJ„Bˆ$ä\0îã�sª?µ«`9Õhà!'ûsá39£ZŸsá˜1n‡’*$)n¼5}«7IJ„Bˆ!"K9¸AýãpÕž Îõ×­5}ßÓÞát¿¶cœ±ü/fÎw¼Û¡¤IJÄNŒ”xj¡»Œ”!„ÉÐDóœê‹™nvª¯>VÁb¦ß¸Ò·íhºòû^È]”w’Û‘¸M’'9±ÐÝ[û”HR"„B$�€¹uõjëš=w¨¯£°Æoêr«;PÀLÏg/š0ÓíXÜäw;a˜òÔô-Ûc­-*ãËÑ®%<>nxúº†·úBŒ<É|îÍ|qçAÓñûä^tÜxŽt}̑ΘÿâH?h}bß{9•ù�¸ÌÍ8l4�˜ŸÎ9|EËãïÿËí`Ü I‰“ÈöÛð™¾Õ¥S“ûFÛÑvæHq}Â-�ïyÕ�žÕRµÿ¹œÊüWÌp;Û0ŽW *TN:U#ë¡£¬)q�ÈïAÈŽîø|Œ€K|³Ö¼µ¶¬`[m¨àæWBÁ‰n'„Â>šq¢#1žq¤Ÿ80èv·cpÀìD~ævN“¤ÄYR}«–¤ÄD4•@7ZÀöºò‚Ÿ5Ì=Þ±úõB!œCp&)!æ:'ú‰Gë±{ÿÄ@“ÛqØ�¯Ê­Ì[èvN’¤d¸Q]JJ¤ú–ͲÀ´<¬ÿ¬/VºŒBãYänjœè'.÷!L„;Üà Z‰ùÓÒÝŽÃ)’”8ʉ’À¯&%Â>™ñXm¨ð®g+*d™B Sè#ÜÓà@?qkIO¿À^·ãp@aŽÿЕnáIJ†›vÏ&%2Rb/"ðÒ1í;þ¾åŒiùn#„"9Ï�˜ Ë�®ÞBªUd[µ«�w¹†#×`„Ü#IRâ$v`Gw_§—’’ÞFÄ_8·ð Õe=¿å´&¹‹BˆÄN‹Œq¨«=õ3$éàŸØïvvaà·|ŒcsåŸáv®�GßA è„b8!h§’’}õ3d­-9¿ð¶Ûq˜ÆüàÈ·®Þû{ïfìç§¥Ù~ÛíoóNR¢Á}¯Ç V¤jüOáa­q”B§êžg«­V?ù»€|ô‡»8ðY~eîÝÀûþÂæŠuëô+§O}䵈ö�Bc|PãYé<€ƒ`š`€™pêÁÁ)7(„ÆC#%€òDRrxõ¾m9•ùwøŽÛ± †�]ý½NDï0 M¬þ!„‹áËì{œþÉå:  å�x<Í-«÷]ãÈ=Žu¢SšÛûrveþO ¸ÞíXúG…ý½ª5¦öÚÍîFäõœÊü`žéôÎpÏJ$)n|YJJ˜û¬moÞ�º ÝŽ�”D+ªnz²6¼Ž€ŸÛÑ>Þy&„ÃÀ¨…ù'ZDå.ø$€ Ð�ÉÝ_æÝ‹ pä¿»ÿXtWßrh·¯£^§¸ÖÈèïç�F*V–c)pØ;I ÷¹“R6)ar?)€â��¿¨ /Paºm2M·)„¢Ê=?ÿtVôy€/Ò@þGË+ãû $î™ÊËL­ô\R‚µÛ:­ùã*}~ßǹN4 üôÉÉœ?îx@Íø‘Þà�aE’’á&-Û;I‰&{çû)›”(æ”HJ€À72h½évJ’!ôðbøN~wò¨Þ¯q8-›tWÚ‘?‡ýÐÝçÖY3«_"t¯pH}Ù” 1Çõ©“¨ôØþA$¸uÎä®ÄÎîö~váΞi§âˆŠ‚ŒœÜÖ+XÎ@A2(™?\_èÔwf*àÇ:xêgÚ¾öÀ®ìù ɇ猊y‚ƒˆÔ²¬…ùϵ­Ù÷ÄØÅcG‡;}sÚÀgð+ÎEçIJßJRÒz')!îëñÍ„m.ÅCªŒ”À¬�Mêʃ›À˜c²]§äNý̉ٔ™s2“>N3Nè™fÖ3Ռ҈8Œjep �÷k ‰™š´EM¥5Û¹ú?à¢ú™³­Œì�ûOê¹vǘÐýîG׎‰[ÀÔJàý jTŒ¦VøšÊ7n;„=ÛéÍ Ö'öÕe_0ñth½š€·ãù§)šœÊüWÂ8ÀØA�&™Š$)q3�lÎKvåz')ñN�5%}ÑŸ6�”¸~Ê€ÚZ>¥Hk:Ds(b �¡¸¿Ç‚Œ�RÛ#3°»ÊϨ ›ØL /Ìœ¼a8>Åe@½šZ¬É:LsÌb P�Ǻv=ÏKÝ3L-°P 62Pã…kÇ€ª/+\Âçþ„L@Ô³z­gölÏèl÷ Ç}O—²Ã¸)(ÒÙ>Ü×ãÆ%kQÞ$ÕAp¦Ù–û/)k'MÖtžKJ õ±=¯ä\41„ˆþ?§¹O/„îýÈbòi-I‰6ë{?Ð96e¿÷•J­¤Dkk½"³½™•+7 Ó§§uå´Î'¨ÏÔƒÏ#Ïpî^Ðý„Œ/€±í;>¨+ >Âjoû¿Y[?Ú Êk¦OOëÊn]@DŸ©ÎtžáÙ Á^×î`]Yðiª¨½íÑT¹vµåSO«gý €g¹‹€ž¹uO›ÛA¸-wᄹÌü€|ã�u?8jb)š Ê¡ÞŒkytÏ^ÌŸvvnàÐ}̸Âíx†‚�ÖÙï×¹‡$)q’¢èíÍK陋’¾±vµ¦lRÂ:uÖ”€¶ÒßT~³ËHÙÝÑõ¥SgÀ§¯îBÛeïà¶5c@¸À¥œ™õ‹ºPÁ*V¾û‹×¿õ²S$këÜÂS-K_¦¶Ë4.öÆŒíuíšSáÚÕ•¿ ÖÿÙw+U´Ú_Ñ%µåœ?q“^ ô]?dʇkJhðuHFûMwª/Û¬ÝÖÙ |!{QÞVbº@Àí�âADÅ*$µÎË+Ì'%%W2¥ëà�¬Eõ/Þ7Þ‡˜uŸélþ”½©à)ÉÌ t„ÛÍ&% ô†ÑP*(× º°}Bc,¹}™´þr](ø"˜TTÝô¤»#å¥* IDAT! lKÙ”¹ êF­y!‘Û—®Ïµ{À�Š66>ådõ¡Â;™y™“}Š˜†}ÙÒAÍŸ–uèaØ”�½ÖÿEW°´ó ‡z²]ëêýwŒº`â‹–Öu�§6þ×íœbþ&�³.eàÿáA²ü{¨déf”.[�ÙKJŒ÷å5N ´æ;÷äd$Iµ‘’H[‹é)‡glzk—á6ûØ:gra](¸†Aë X„Ô›¦:ˆþV ¾\_>e¶ÛÁôöʼiSëC�+R/ƒP‰Ô»vgx²®¬à¥º¹SŠ�è°®¼ð IHRЈNJ²ý‡¯ðq{{¡0;ú}2*†ÏìšÃ�íÙ˜–)FªîØ|aWëì½w; §OJˆô’^ô(!æïÑf*Yº]•.»³—WbúŠAJŸ [$% Þ6““j7Zò¥ØH ”:Õh{„çíšrñæüiéuåÁïjå{Àùvôas™Uu]Yðî ¡i®–‘|sþ´ôºPð{–e½Â nÆ¢yÐjS](¸ÒÎkW;§p˜oµ«}‘ÁIÉ•x¹ýÑ‘ê[N~ß§çŽÊ›ê`¶;¸êà¡–ª}—ñUÚÝŽ§_Œ_cÅÈyØl6))]v@eƒdæ+‰ôjÊ8x€J–Vaö’+1çëÞÛ˜'U­šî�¤„Ú‹Ö‹yÐr�Nãî‘cˆù“íQ;wê¼ÖV=?„·6gô�pm:G¶ÔÎ)peGàÚò©§µ°¶ø¼ví€%°j¶”�/€Iv÷CÔS}K;¼vG«SíÏ!Í«÷ßçóÑ©žv;–(û3ˆïv;'MJc 0è›ü tál6€…Dt/éÀ.*Yòig´ÂÙ~Ìíòá¶ÜN¼ãÓoQWççà¥û9CÌ$%%Wf1øÿÅ<®æŽýÐÀ¨øbZÇž®´®dàB“8ëÈ4¯…Ð�_¢di­�ÖUØrw�‘ø�¢ØÙei©.zã§NJººÜOJêæL›XÀä·aͬMÛ�TöØ\Rh9pàa† ÉLøq}Yp׬êÆL¶»¹¤$ÐzðÀ_à…Åì ã[êBÁ]EŸp }ÞÈo?áUÒxˆ”®�hµÛÏVs;Y‡Æ�Ïl9qí6×ÿ®{ÛÈ�¾Å–?àÈŒ*B设ev¯Ô#jýïá5ïW8+{Qþ§‰q€¹¦ÚÎÖ„³;ý8·Ã�™TϯÍʃ™á?fóâžÙwÀT_^bh¤$ã ã‡tʺßuxFÏXŠâkgÁç[HÌ•æ ÿ©eGªy•€èû(Yº�UzêîoaÕ*+Ùÿ‹aÀ[)OtŠ–“‘²IÉøqî&%[Θ–�.« �.&¢…™¾aª1ààÝ`³ ð¡#B6€,‡ú~];§`kñ¦&c;ìúý~ÆBSíÅàÚµpï–²à+³«ë‡zbCÅôœp{Û…Iöo1øº¢ M+½¶Á� ¤Çyx‰?¼8 ÀpÝ¿+AÐÄ8Ä€E„Ç™©ŒN€ÚX¡ƒ Ûj#pu‚ýz“•FìáÉGº;ÿ˜Óüîôкzßßü-§2ïL]ß3Ó'©´EŒs;ü( ÷Y;FSà›Ítß7gN¬˜µuOk2}x‘‰¤„´$öa1ÔÞS ž�aš×4&,Ǯ㿃T-çÖ3Ù<Ôê©/YáŽC8~ëœÉ®\ß°? T—õ[Þ䉘®,ªÞþ¦‰¶êÊ ®ó×L´Õ�ƒž"ÆÓPü*ùõ3_Üy°÷�ÃÖ8Ÿ¥ŽaöúôžQ‡‰6Ä“ Â_6—LšYZ³»-ÙÆêË ¯fæ+MÖ�žè)"ÝëÚA«b&:àù°çÚe)ª—ç?kÞú]CúÌ··ÍCrk©À—Š76=˜D®™µ±1îÊGu¡`;€ŒDúÉç› £E‰ñ)_€µ_úHI`rºþ–¹µŒÔRµÿ9Ï�®œ´HŒ/8!‘¶:¸nL;~úAæQ‰ ¥ÈÌúó³~rݺˆ�Ð=#ù¤¤tÙ§Ál¶bVÿÓ¼Îé^HOG/´êjMÙ'ìóZRÒ7^«+UŽdYÖ6(æ ÷çíXËȷΪn|ÈDK5åS>¦Ÿ™h+ÊëÄøIÖxßïcÝ$×5uØÝóÏÿóðbøNÜYxž/gà“�ÑT ãû®O¦�-¡É§0ó†Âêí5€~hÉüýô††®ÁìçÚÝÿðbøNÜQøiR¼Œ³ Çvb–ößàÆ¡œÄ„9IÍŒ!ÜT´¡Ñ“ ‰ðÖ:àÄ"ú¨$°£ßšä¥Òä¶9Tµ§À÷Ü”U9¾” .&à\3ÿ}µÕ¡ðòåv>ø—Y—ûº7 ííüqí;~ À®V))餄ÀÉ�’ ¦ï4¯P|í,Rê�>zjÔáOÙR²óvRVòstƒo)ÞØôŸfÚÕkú Èè 0@·Z2ë†z0—®‚lÀµ¡à§¸&G›˜—×—N}pÖæ·^Mèt€êá»f÷ ó�ÃÖø›Kkj‰6Òsíðx](x.º¯]�© úfÝœi¿/Ú´­!Þ3cNpuoœÐx 6$Þ€±°òÈ–‡H�ÆÃZÓÛiÙáÆ#/ÚÒÙ€8 ðcFÔÓûAp[Õû›lpcþâüœöNžV' �Áǃ)ÐY€jƒÒ»ìÖL›3"ÖKÖ8Ü û´“þÔéZ à´>�_­-î(ÞÐøc;ÿ'6—N¦€•]²aÇ¿ìì'É%%¥KOãýÚi'Íêˆt=s ñì×·¨Lä亲‚+Èì~‡µâE³×7=4k´hcãSBÓf¥Cÿ‰º§u™`eÝ à‚xOàÞ¢†ˆ?ïN´„°[:�løJc`MKÕþË�üùÈBͤ�Þªd\測8pØÑN=bߪ}-žíùgP½çýžüÒëͯ�vÒüŽH×Nï}1~X[Ü[¼¡ñ¿ÍFÛ­®¬p!ˆïŽ„Ã3ìh¨’ºIVËád=º#¢ks§ð¦{b¬tÉPìó”žWd0!Ù\R` SíhÖLg™NHŽ8ù¥×›ß˜ÜxVk”±°¶|êi±ìksII€ˆþËXÀauVwBb^ùÆm‡ßœ¼½À#¦Ú$`Ñ–²)C©f“p‘ Øœè¹BÄËGÊ®}Jú�u¡žggÀ/S¸lpòK¯7·©ð¹ý|?1~]_üŠÉþž­¨ðׇ‚·ƒx5ϘXiBâ#%§^=– Ÿ7Ë÷HáR²}0ÈæÎ[#%šž;"Y ºct†þ~pÝŽ“ ûïL¦¦Ci"|~öÆí¶Þ<^º VÃô¬Ï‡sÚN€¡Q Åú[èÞ6n~ÿÁËL1Ñ? ¦Ïͬ~ËÖé=×î³áœ¶ç„L´©”ï[.Žóðœ„ûñÅ]µJˆ„Y°ÞVP÷}øs;âúÜ%æv"êÿXê¿¢Am!X7|Ø¡•ÀñOye²˜0¤Q�Vö�Ø}hì6oý®v.« l#Pï5wŠ÷Ö–ÉÄˆÉæ’ÂÉþ¶¦‡˜h†¥îN¶MSOJÒÒ®‚[•ˆûVœÈ”‘’ÞJJ(*¹Ì KŠbýŠ¿T´~»-%:‰éjSm1áÖ¢ �«Mµ7˜é ][æ?£,¼Š$nt�``áæ’ÂÉ¥5ÛwÆ{�¯6ø—õæ¢êíkÌ57°é ]µåŸ!¦WaâóŸ¹ò•9SO8uÓ[oz@õI”.f‹+ŒØRµÂmUû·¸Ê©þZªÞk÷º,»Œ>ôX clï׺TW$Mëf8”uè0dúd\`llúÏÚò‚7ˆé^i=o)bÜ[_V0iVuS£ìµeñý�0žLt]¤KJ*Vø©ùà× Ç¿£6ÝKÙªM})Û[½•”€ûÆ«%)1ˆAú�ÑéØjGãõ¥Sg0t™¡æ^Ëëû�¡¶â2ûåÆõ¡Â›lbwvŸ/À—ˆk1â+¡àL (5Ð/úgö8õ#3mŧxCSSmyðb˜èׯÀ-ƒD×uﲞÐ."º¦±¢àîžJcB hô…¦ZŠ@¬XS¦R:4(SñÐË,3!‡vMë: �Æö}…€ÑQ‡ùÀÜ÷ïQ€¨ïCf¤á臙è[n:×üˆšUïƒVÏã✎| -Þx@�5WíÞ?„ÿµ¥xCÓïjË šˆéãz^&&ZQ*,hS]_JYõ7çOKo=hýŒÿÀGË.4|ÚHÑSKJZ\PBµ™� ¨ß~¯Tm²²’Ç’’¨xuš$%æ˜n=ÔŽ%µ¡àÍVdÜ}ÉTcŠÆ>ëbSËɘøj7öEðgfÞno»}à'ŒºlÇ•”Dˆ.&6óWUiw®]¦/íîŽH×7Œ�yp ¾1’ àN|]É ‡ÛÕ-–'x¾¾(û‚‰g+m]Á  Ëâî{îÌçžb,„¿`Ùé…·qFÙÏŠ|CKÉp@'8›¾#»2ÿ–å_ÑñÄ»;líÙ£Š74­«-/(!�? {ªUþbJëK§þ{<£õ¡‚òÖÖ}NíóÑ}EëwÔšŽ; ÝÌÓRÓ� QÔþ²@º‡·’’èq#‘Ÿ£y“øyÀ ¾§´«!dª‚ÕÓÅšÖjkH¦¯khð[#�1J_ ãÚlP1›ºvOÎÜÔø¼¡¶†ää—^oñï 57gËÓbîÍHnÔ�ÁKkË ¾˜LbxÉY˜INå„WIë§t9ÜO$$@Àý>ë�œÊü¡ÂÈfÞÃNñ†¦¦ƒYgø!Ðk cûô–ºòà= s ŽéïÜ­s N® `ÐK8*!Á«�ŒÌoÙzB†ž”//…Ù2–C]}+½Í#%ö?0ñVRM$)± ðd]¨ð×/Ï=>©ê)õ3'f˜m$.R&+P ‘þ£©¦,¦9±j¨˜žÃ@‘‰5kW¯�ÖÊÔµSÔ¥c^;ÿH²"¦ÿ© þzëÜÂS€-RFþ¢üi£å?Â*€.5-Là4ßÉÉÍ!ó‚q’öã“ëÖEŠ66Þ¤4ÎÐ{ý]ŒkŠvÕ‡‚OוÞZ[VðíÚPá]u¡àf­é_.ÇÑ÷úÛ"a}nσ¹”2äÌT)ý ×ï|£HË´or|7ZðUY:P^7÷øó‹Öïz'‘(=«„Ml¼JØY´á­!U­2m憵õ¡à{ú}Ò4$¤gtÁyWGk)�L<lš]½c½�vV\½½¦>Ü `B²mñlO vL@óê°¢{ðÑÂÏ„ºø*­qU}(x°ØÁ wñ^Öôˆ÷¡{íJ3:‰pˆ‰»|ìkS¤-`qg;Y‡r²Æt¦âºˆmRåÄ :Yÿ/½ÎB¸«\ißó£+'žÕ³cºˆ2sSãó[OŸ¬Ì�é[™xá•yÓ>uêËÛÞêÉšø¿Ç ô&Ô p=áf,Nº1¦�Å>F�bâ¯)Ÿ�wBÏâóü[Ò�1b^»é›šÞ«/>ÄŒ/$Ý_·±Æ¸ˆÝ•æ{92垘 ¡ác­€tønoC](tïc× à:hcûzþ½›û؇×"]�¯¦Ê^#Õä…+»˜ÿÀ/ÄRÖ³9M,kytÏ^·ãIE3_ÜyÀg¶–Où_Íêç ‡pú~búj¢$�0¤›y¥Ô5€sU$½Á+7³lÇ^¯}{°¹}£(ú.@¦o9)hYÖ3ñ®ƒè�H�d$­ë�´“,�7Œ´C4)æ!`#׎�”¸v 6tíóÚuwèû.€T¡ÈFw¡„ž)’% |Àå |ÀO™ð¿ÐØè÷§¿_ ®©ŸSx¶«�PÁóóÏ`æ?ð{ê‹r„aÐDôï±"¹Í½‡»™v¬mSáÄø1€x÷¦Éq]}¨ðÆg+*Rr Oü?ô¹Ë3t¥�±Ä�£GJ«ºñ»é"ëâ<-“Á7�mßñB}ÙS›¦Ôi]¢ñ6Æ7BTAM¯Tm’}J—Â?ÇH„ŠJk¶²£í7çOKooîÊŠhß(²(��eMy`�ODyÌ<••‚1€Ïp÷å-­›|'î3ˆòMüª±ò§Äð¼Vz»ÒÉÿêÅÞÜO1òÙÀo93§ÄµcÒÛ,âÝRÜ#oÜþ˺Pðè;§Úk>îh+ª™;õ¢’õoms;˜ÞF§�ö»Üèˆ/¼„c$!ñÝ”{Ñq4?úÎûnÇ’êJ6ìø€OÖ•M¹¤nψ|9“ª«- ~¥¸ºq•Í!Æ-ˆþÃÎ@†„ÐwG÷€WÖ”ØÎ[ŸµÄ챈mѳÏD'ºGOT77X�Go²b|»¾|Ê#³6ìØßœTõ®#[)±Ã/Ã÷NôÆ_ µÃ±`3Á̵󩔸v`ß;Ñž Òh]› )KŽpº‰Î]Á˜ác½¾¾|Êyñÿݳ_{s»é®š±`§ÂÀç%!ñœ1ˆtÞ åÊÖ¦ª¢ê�>[QQ5¶}Ç—übpE„?ו–̪ÞþŸdâ‹0IñÝÌ—\{.€™ö†2Ñ%�Ù#kdóľtT¼^ù9º¤h}c5˜ÎFŒä%~fsóº^ÌÜ3¹¿> wq¼óqc‰ý�‡9b¢#f•×NiS×nH7ÂóÖïj�Dh!W«·�Ǭþ^;§ÀH™h,_gJÎ5O1ý0:!11Z)ìÇ ¯ rRÜ£¨âÃòÁ÷Q{Û4߀îj‚ƒ!_¿µ<ø§Í%î_븒‚Jµ¹}Q›'zfA”|ö=™--u§o¥Š¢êío`ÇÚ®skçN�û0€fn¬I§ÄÍ�ÊùÿÐó"# “•׎(6ÔTìk¥´fû!jo;�€Ç Åà–1¤èÑÍ%Ë3Øf¨2üÃf¤¤xþ1ó½ãC6&‘ä«û�@³¶îi-ÚÐt[$¢Oã0:;ž‹ýþôg7—L‰k}Ÿ]bßÌ-9À§íe(j¤$µH;ÉkŸ·2R’€Yÿàiã k}U<‡±2“”�¢!MÙ±�JwðÆšŒ\;¥SãÚùtÀ¹„®³¶îi�¹±ñ"b|€©Ÿ£ üþ®Û ¶—ð4ŒpgdØ$%¾²÷¨ˆ$&Þ£ÀŸq;/+­ÙñnQu㫺ƒN–ùýªzëÜÂS9ÆV1“åÇÒxŽs”ŽšÄì÷Ìš™¾ÕGUß’‘’xÙ±:ÿÖ³[ûà´™›?ÖdòÉpÂ(‰ýÿO;ÀáØG±™kÇ©qí4k3ÃýsŠÁ §‚gU7Þ¬sàéé\ô…º9Ó¦j,‰9_JL LVhþ¸Q8èûÅè±/É�� Td¸‡×�ºé­·‹6nÿZ$BÓÜ øpñx­ù¹º¹Á2ÃûÐà7óEËÆ0“©�ªÌ‰)a)éáíÏ[öËÏ1NÅÞz‰�­†›ÍFvæqg¦ S¾‘v’e)#q0xgǹvŠtJ\;ö™¹v`ļv±Ì®n¬/ÚØx:˜? ¢G‘àè‹‹)ËÔ÷mÂ#%¢Q†bp•FàL�£GG¤¾ŠÇde�n÷nA‹SZ³}gÑÆÆ«t €ß£ÿc¡ñd}¨ Üáðb,ÌTüU)XK=ú »Œ”ôðÖg-‘|;$�À�d¶…¦³ümÐc½ýW0D±÷õp„‚¡kjŠ}ÙI—Y¥Äµc‹òÉLõ­&�@QuÓ“ž|x1|'î,,TÄ'Cc&�‘€³ 4Š £ÁPdqß `9è»Qp.†RB?Ašùbß6ÑTÂ'ú1Ú@ÿ)€Î<òóá¿Ék_’ÈÒåžq;Žá¤xãö7\^_6å&&u€ÿ‡¾GÆ0è™ú²©‹fU¿õ§âøCvñbmÇÕN2$̽7G÷{å ;Ùý‘è­Ï[Mܧœ¨¬ ¥i�6œŽ3#޹¤ldƒøú²Ÿ"L1�c*ö�µï2Q„�H§ÄµaŠ‘vØ\RrÄ¥«`Ûßð¦‰ö*¦ç„;�F$c HA|3N&ÂyN0Ñ6Ì=~Üôõ»$ÙTâ¥=5�I²ï”À̳ÑëW¦py˜¢“Ýa¸šU½£ÀU5åSîò±ºÀgñQr’ͤ«jCÁ ‹76š_ÇÚ��“’Æã.8åv{Ðý„½7�bk^Üã­ÏÚ¨=0‘æ•ä25XaÞJé†/aj¬C4è_ÊįaVò�$�Y—šĤ8žö3è_Iw€¡RâÚÌ\; žQ&wM_×РÀ;¬hkyA3nè¬$»!+˜àù$ÛI|M‰ÂpHJˆEG•v+‘­'ºÂp׳ãus¦Ý²nát/ñÈ"`u]hÊ‚¢�;žµ;Ž“bNµ2À½E­)1°³#l¯Žî±Ï\â>!ËHÉ�×5}P v±7킘 ¨Ó-ýjXùQ[[^PP¼¡©ÉDc‰£9&Z ‡©>Ö1ÚyUELÌâã6—N.­ÙžôZŒ¤Í1ñ©ÃJ¿š|+î €±¡i#çÔ—¿Æ“iOûŒLkL|¤„½?Rš?.7ŒŽNH’ýue •ˆ~ª˜þF{“lÎó˜"yZÑ—Àô5Û:!JÁeÃSѦm .Ûš|Š‚ït�œdê±-e…gͮ޾ÙÎþûÿv,Y6àÔ]XDÄ}æ´û<³¦D F  Éæ’’pÀô<÷˜¥f§ojz¯.Ü `R²�¨À=ɶ“¨WBÁ‰pŠ�¦¶Å“ Ì~éíÝu¡à{ˆ½ÓnL>?WøE²í$ªaNÁ1aÆÇ 4õ®û‰iòº““ÆÕ†‚Ÿ"àI´“ôSafn#Jøã´ ÙþÝñg¤!b ˜�$‘˜¼ë×êôC�ïÙžDxÃÍvÕÙ ó‘‰õPG!Ös‰¾foÜùO9QÖõ>§ˆ×n M>³ç=[ôûƒVÄË�̱¶ ³W«oÉHIQ¿eR}kH(°X™NÈãzÂJÄë˜é³ú».&%ü€’Þ—�€! kÓ:¨½OÝ×夤Ká34Ä�Ø�ìT¥”¢2'ž”@x*LÔšø©ðþüý®®T÷¯&hmÉÐñ¿–�dÎw¼/à››Týõ z½uõÞ£ª-æ^tÜx¶º’�28$-xU»Û¢_÷søV‹Ò–0^¾—I’·ôŒœ\Q_:õvøõMŠ}OÖ–œa׃¤£Ð%ËŽeæKíèÌ ¨Í=3R"IIo$µ“ácu‰ ÍÆ5AƒÖQ÷°nrgm�[pòÌõM¯%ÝV"Ý}ÎÈï SüI ëu 2±!ØÙõ¥Á“fmn|Ý@[C¦@Ÿ3ré˜×h&eh¦ç(‰_*­(éé˜ hM8ÆIÉöï¶L+-Ц>ZVchêÖK-UûŸì_À7Œ‡“ëæh ú§¾ý3Pë6 IDAT:[]Óíèo0t;¢_?ôø¡ƒ9•yèBã�²$%n›µù­W\ºuná©–æ+_ Wžº±q�é~Žº™Wà¯Ãìuó¢H³Ož°wóÖ-¾Žþ9zfÄËu[OŸ<À564WÉÚ4‹«�̼õ�kú†�v†¬6üº7ÜKVÄJWq—«ÔËÔµSì£åÚ²ºPð\J 4ætÿÚI�z_Rç³6ñý›ðH €Â7çO‹9�3•é@$t!5u+Áoê)l�JX«”Úè’ÙL!�~HR’"f®ßþJñƦÿÔím-v´ß7)™vm:_µ£#£XõýB÷y¦ú–Œ”ô]EÍ3¥�Ýg…}w˜`¼aBÌÅÚ@÷ºb¼`¢K¾¸un¡£%nPüÄHc„¿Í~a[Ü7¢³_z{7Œí<Î_®/�:ÃL[qö(6tíX;”kçŒ)HêËZ1%�°2(™|­Âó’�ÁM�Z·õW8É©[)WŒ�ÉHk Þ³©iIJR̬­{’yð1 ¾7ócÔça`‘�ý¢Ÿ°{¥ú–í¼•”õs”‘’xÔ•\OÀv´Íš‡RYãACÝ´æû^ld}B\êCÁ候’Ĭùþ!ŸCæ®+ë^îgÔÛ.[CÁo`dÓNEòµKu:·%µ#:w%'÷“¨2ùÜS8jÿ^VïQ‘d;ö$Ã2ΕRI ˆÆÙÔ²W<‹$õùAÓµn2$$kJ౤$ŠLË©¾¼ðj0ÝbSóÚO4ønî½´úÂdà}C}—lgðg†ÚT]hÊ'Üj¨¹mEÕM«‡z’îü€ƒF" š·5Tx‡‘¶b¨/›z¦~ÿÞ˜¹¡1å¦Ä$«K§�Oæ|‚ 'ï&s>3]±¹dRV²q¸eÕ*X Ôæ6MTГ“‹Ê#ù‘&¼�Ì8ãýþô|/ÙXÜBÄ÷w^7 LÝ  ´Asg¼í1QÌèLê ‚®I0äiî­+´(ÀxƒQáþ?#�;°À–N£D‹aëÃ/òa©gž³{w¤Äf»É�Þo&…§á)l�3¹Ù­þY©Jf¾ö&¶ÿ=Ô(ͺƒÃ¾¯£g£²¤1ÝZW^X �]RZS“ôãÞ¶–O™¯™`d#.þu0cÊo�Æ„ÎOS‘Ÿ†uàjÀÌÚD¸½>,8�9eé'×­‹˜hóˆÚPáêNæ²Í´Hÿ|}Êöß¡ÚLk©âáÅðag²(è@²q°æ·“اäˆê˃›fmhòH`*¨}bï¯üÚÉ>[Ûó Ì€ˆ¯¿Õûþ îÑm;åvä–ãØ|7!R§sÄèNJf/ŸÖç»ËDíÎf¶—v€Íq¦ôî2GÓÜwÄ+•“B�N~;‹Tön %ëÏC=iæ‹;Ö‡‚7›Zø `þšß ¼.TxmÑÆí/&Û\CÅôœH{û5óµ0³¯€™—'só?}ý®µe·‘±‘!¾>¶mÇÜÚò©×ox+éÅô¯�vRng$ü†ÑkÇ ^vé*X±õŽÍ%“²ü;3~�$GJ´ÒIoÌç#ßÛ:ù‡Ë~f7³¾ˆwb·—·’’èbþ~YSâýhzCCBÓFdN¹klÛŽÏ‚Pl0¤"€Ÿ¯ Ÿëû#VøÉÒš£7ìÌk§�”Ûiu]no» À c€‹ª›žL¶Ë§ßà߈©¡˜X¿PW|œ5ßßî‹<9oý®!-ÒÝš6*�¬Ë:"]߃ákG„Š64>m²M·4LŸžÖ™Ý>“ŸGŒ«NúZé.•ØÐ[/>ÕÙ¨u€‘üƒ0?À7…ÛÛ–Ö—<Κ֓�·iö5ûˆ#´OD!Ã�@&´# ËGiÚ—A�~   Ú�ÊíÐåûÿìÝyœœU•ðñß¹U½eëÂîDp‹$�®„ˆ€DÅDGˆâ:¸ ŽJÒe\F_§�‡!éPq\pˈ ê0Š !é„-®! ˆl t’NÒKUÝóþQÝIWuwz{žzêvŸïçHª«îsjé§îyîr èQuÙ”Ög�¬¨ëAê»�œ0¥[|MèFge�GmV´®È:™Ù#^z(ôd{P²Y�-õΧ�ªŸ’wRã<µŠÖ9G½Âñ(Ç€‹è³²þà<'ÔDxæÝ¾°mûÃãmdîúGž¾gñ©;€9ÄP¯*oAx‹zAðx…¾ªéE”î¾Ï²<¥°·àð â CÍy‡ï­º®ª…Z"RûWÀ#hïìä¾™<­Ô®zx®Oß'Eý‘1×)N`ó"T†'.ì�ÿ ¦¤éìy9¸¶0,?õEý{Ð(O¼B;-_° '¹œH²âøûñN¥Y°qç=÷¾ðÔeª£_—2BU g‰ÈY¨à(ôX<þÐÊU¤Ð“‰±Ûº_½.]ض#²/Ɇõm¹gñ©Ë�ÏGÕf‰jD^$P¨=‘àk—‚KÚ¶�ùµSq[UbI–ó‡_§ } £‹Î®½½åˆ_+U�ò3p'Ñ%%ã6dÝ�A‹Âß{“éÿ¯A "ùø#ÿß„CýÈŠúšð9\eß-íÌVîZ„bñ^áÃηf”TùxÃú‘,7ž׎ÿFYE[*§È›lÜyOÔ 7lØq#p}ÔíV�œ8]zƆ÷%HûS>ßs]T�©r[Tm�×È“Qbq¤Ÿ ¸ÏI‹ ˆèýI‡`ÊÃUSZ <Êî[ãßeB0 /”ärbѯ7ܽ#ÒúóïÞÑLtE+IÑ·-ذýÇq`þ†Ë�¯ÅÕ~‚²o�¿~gEìT¡ò^ýe£]7u$�©ì#¬#4nC':hb1â¤c”÷1áqêÆ8¹Ø„Æóüܘ:R»°Î»¾$¹´é[e#ðýlöèË¢®!àÿôÌï”1l/\Á*º´á®�£Þ�l4|{ÝÉïdbU9? ÂÅ wíøvÒ�T¸«ï~h}” ¾hý#�Nµ"F.%‡ÖxhÉÏŽj øÅkD†{ü ‰�}ÃHžî¨{r‚mn†âÐð®ÞûÒ‘ e¤Ä¶>¢pŠ`NnÎæŽZu�>o\K~þ†—«ð Â/Ž÷î‚ ;¿_Žƒ½dݺ\Ã];Þ|’À_;V¯çÌ¿kGÅL#ªLzëü ;>GËR¹ÏÛâh{´Æ2âq¤Ÿ�õñ&,‚ÞÆÛ>Ü Íá|(úü/>¿8[è^`ñÄþ©7*/ª-ó7lW\ I îÚñ¯âåÀãq+Ê·]U~þü»Ú\îC7lØñ)/òJB}íà[’Î7ıþf"ØTU7õï$¦>ó‹Ö?Ò‰O½h�£ý‘4i�!F3úþßïÛ êÇ›p¨êÍIÇ`ÊǹΡ´¢{8�Y[è^ÄÖ””ÑnT_3ÿî�WÅÕÌü�Û–ËU�z�|>Exõoh¸{Ç›æýæáÄ:s�wm¿£ðÚÉÍòÚ©êß6lØqi’¯]Ögsr~ÜE 6nÛ*y÷b`ÜEÇ£œ£!Gº� Êû¸û—IaÊ'Ð5%6}k��wÙ –|³ÊëQû‹…mÚݰaç»Ô¹sD©Ü/e—*Îf»Ÿ×p÷CßM:è{í¶¿SÅ�«ð«¤ã’²Kàs¹îç/¸{ç÷’§òÉÍéº)¯X8Ží‘Gcþ¦¨ª›2Ñ•j–UéZ�IÃ`õFJï3ÆŸõ_½b_8a•O\¿ÆŒGu\ß°´¢»³+ì½Âz#EKö´÷1B ²NÅÿË‚»v¬K:€ëü-°äžÅ'¿qDy50HõµrÓ­(×IWçWî{∪“²à®ïλ÷Ìg½T�_�ò¨€i«Âx½^º:¿:¿B_» Ò¡èUuMÔ‹ÚG¢wD惛2sV¥ªô½¢¼8®\Ç×nZÃ%-C&=zèñ&0ªwtüp—]à˜dÒ¸B­ È¡:°6R¦BQßÃl÷­ñ؃ðˆªÞ/ê6:Í}ÞÆ‡�®1”† ýøÅæ�zr*§—©ÈÅÀe @émq?$/·Íßôàe;ö8Í¿ûÁŸ?ßòÂSNw™ £¼ Œ!tm‚ü(´×®¬”nø½ªü^DÛfÔêON]·³+éÐz+Æ|ëܹWõL=ø:ç¸@•§Æ}ìøFCއ vßк:“ØÞ¼äß�½e“N:Ìkj[.ì_àòì¾5Ô<÷ >­!t þÝ+È€’TéGQçC==¥W’³¨Ï'Í"zè6ÁíóhÏЬ:·ß)]éÔ¼ß�ó=Ù|ö‰(ë”Kãow<\\µeñœÓA_á„óT98!‚Cä�‡(ì>ôgÙš÷lôùY÷–c¡œܵs'дô½vÀ�s€ã#8Dßk÷g`ª8ImìÉͼ¯‚^»ý@i,{(>z hŠ”~Ï‹~ûÿΪê~�B»"{ð¨ 9uÚÑûøNUé*ü]÷y•¼{Eüî¼sOÕTOy*îu"Q˜»uk°¶÷w->mÆôtU}6¢'©rÂ1À±i”™½�5LÓY ªÿ }½ é÷wE«@¦õ¾Y3µo>ïFE(~üà÷æñ¦âåU¸´óí'ˆ)¿taKàÀ~eEµ(—r6RÒ+¬7RT‹^’6,hذcæð÷2I[°aûŸ)t€¯ØÒpÊLªÜé’òswÊLUê}°°Gð�Šë@yÒÃã.ÕóX®ç¸'+¨›ÉôÚ5lØ1%é&ªnضhëý“ˆ«Ïž=]jêŽuÞ£ªÇˆKãUOWáù sAçÐo1â‘FC,1 ’ôöÿ`·Z�¤Ò¨Jp“Š´tÿþPv‹= ì¼Úb&S. îÙ¹ØØûgŒ‰*œ ØkgBõ‘;wwP™~p°Ÿ¿çeÇ'’z9È ¯êƺ—©8ÝŠ¾kÿm»¿žt &9½Ó·éÓâ|ÑéÅXk%��sK×…8•ÐcL9ü÷Ïž|øðµ·¾ú¨Y_{1^ߡ‹7 1d*—©Hz¯o:ø£Ý‰�Ò™Ê�F*`—Ñ*ݵ)˜‘’¸;Ý��< Ø}+˜z3ÆctËÿ>½¸ ¸é¥¯9ádÍñD_«p.�KH°Ñkö×Ô^ÃÚG:“Æ$/ .¼5%ª¾ht'œ5%1ÓÀÞȶa�1ƘQúù�{X ¬lxýÌ™tW/^¼Pa!Pgãð•C‘»¾\+þ«»°{ˆ�dÌdèî[¥Åƒy¶Ð½?/ZôV–g÷-cŒ1Ô=ßÛ³ø^ï2ª9þ¸ç¨Óçáõù ÏEø »Ô�Ô%n²ÀþÞéÐ{zoë!§J'ª]"’ÓC»HJß”ª‡ï?,�) 5½=¨Ð=øý¤GÐC»ÛyÑœƒ¯t8\§ˆ>°ïOn£´1E »o6ëÁ—¬j e¤Ä:ÝýIÉÈŽÕ)1Æ¡¶6²ðÄÀ 5uf-�UŸ=èNDä8Ç ÌP¤Z”©*R§ªµýï/Â4Tmy¬h•À´ÁÚV¥CDrýnèT9¼]¼YÑÃ}sE÷ˆ Š´ãQM¹=âsÞ{Ù+*ù´¸}=®'Wí}Gõ”ê쮵»Êׯ_Jj³¦µ¯mß;ü��›4â]p}åÒ+ì.˜‘’¸–]¯1Ƙ ¤·“½ø}Ò±ŒTßâ‹©¯;î ñþMÚ»†F”ZDzG~ÔÁá­À¦«jºð©)Œiá¢nÿ-Ãg©Þ¿×Ò7’ÔYȲ”i¬´¦—1ã–g‘x?R²%°�”ô ¬“_Z§Ä¦ocŒ ×Ô g_-âúÕÇÒ™ô~Ñ•$SE¨(ŒÆô&B5ÊÔÞ§�é‚~­ã¶Ýo/=–óþµ ?ôÅ)0T7 °Ùá¯Ø1®@­šzà˜£°ëñ1=Ú˜aº%°�½è^�$°¤DÄvß2Æ3aˆÈ‡GŸHÉ�Šyä^}Z ÿ»íð:Ž¥Tk•¸¥ÓKJL,\�ÁÒâ‰ÎFJJVÚ„ÇFJŒ1Ƙ"§LOïùÂì‹fO˜}ÑìéÓ»fxn¹cñ©|Õð÷2fl¬S2`e~0kJâ�³táx¥óªE³Ó¶ÐÝcŒ)¥ÈÛ;•7L{í±u©> ¨I&ŽªÔð÷2flÂÜØ•Œ ø«8¶�š”níl#%ÆcÌ`¦‚>?Ñ ²Þ’dRâµxúV8Ï!”8Ë$°$ÊcŒ™ÄTÔ’‡¸û®¸3ä´XÖÉ/­Sb#%ÆcL¥J[Rbbä‚ìлґ’`ëǼ¦$´¤¤Dˆ›.cŒ1“„’²¤ÄÄÆ䔢Ò+ìÁ$V¶ûV?¾tMI8ÓðŒ1ƘÉ'bŸÑ„"�tcâ�/ZŽ ~üIÉ‚eÏç¾úˆŠÜ�¸Ÿ²qåöq·[,Þ¤!´‘ï‹wßr1Œ”44ŸB•D—´n¬ß -~Øû™äØ{nŒ1Æ'�â‚Ë{l};Îu1�ÍoÑÏÓ@åÔC¦i»ˆÜáUï ÚýŒõ+Ÿ×qˆ}D'¬¤dÀHI ‡Hé=x­�ª=ÍtΤ�½Qµg¢')½¯Ó¢jOç>9�­ì�ª=cŒ1Æ ”Ɖ”Άªx¢¾x¦Ï§ý,¹¬ÖuÔ_­èò!î1GU/¸œŸ'Ót�ÀÞ¹;hÏýšmkº‡x\BB{#U‹g´ÙBwcŒ1ƘÉ(�zܲ/Z²ŒaªÆ‚œ,©o+œ9ÂG¤€ŒBF¼ÿõî �Ë«"w z›Wofø‘ŠÀ^è2³…îÆc¦°CàÔ¤ã0&DéqO}J‚P<¿{´Û/\þ:Q¹˜YÔ¬r�GoFä|Q=‘ó€ª!Z™‚Èùç#™¦'~åUï Ÿú÷®üë ‘Ç© Ó¡¢UZ<ѺcŒ —×¢¬I:ŽØä›&n‚’FU‚ËKD´x¦Òº/iI»ŽöO«òaK96­nÚ®fîû§1¥æ…ÎûóÎ2Ghý8…¥"²”tÉz”ÚšÛ¹ó3£x†cãKJ¼�6RbŒ1&\û�ßõùi�ó^àIǃû:N|rSÒA˜‰+�sá­)ñ>_”H�¤ÖÊ‚œ,íßRX<âãl½a?p‡‡;X´bê_.ªçƒ¼8ê�>¼¥«»“LÓo€©#>öX Ø Ây-ž†çlM‰1Ƙ€ÝH–צ.…üz`zÒáD¨Ë —q#Ù¤1WÚ©vy§EË7†›¾•i¾@Яpä$bx…-‚?¯ðyhq4>½ ßT¯s�š!Y¼|\Ç™°ÞJ -dcŒ1æHößöøÖis!Ê€ÈvLPåíoÛµ%é@ÌÄ–&þmj£'^K6Ü<)YÒ’fÿžOˆêÿ#òçÙâÙÌá©^™Ë§à¦¾¨ßT¯Æ!ãŠOà=|[SbŒ1&|û°ëWõ×è�ÿ¦Â¢¤ã‡]мíÀŸüIÒ�˜‰/Ì5%>å‘~kÝ«è>ïÊceû×Q^V–˜Ún"Þß &$)[ÖÜ 1/ó�-tÀFJŒ1ÆL<{´·}ÿmOþóþŽ©'€\‚ð] R 0çP~ |ÂWùS:nÛýKHLÒ0†Âƒ‰+Y莙Ír_^™TT#`uJú+-‚iŒ1ÆLdëvví‡ïßaé3ê¦ì^HJÎRõ/y.p2Ps�£ú" ò몚ìÏÚ×¶ï�ù˜Æ +�tcâÕS´#0‹!÷^àÄÄbª�%%¢Z”§¹7]0ÆcÆbí#�ðk úÈ”‹fŸ�wŠ÷þdAŽ™ ~6ÈqÀ쿵˜F¡ÀsØìö+t8´]‘Ý(O‰Ó'ñn¢»¤§ûwûþoßÓe®ÆŒ@Á…·¦ÄkÉfZ¯I*”QŠy\ ôé[Æc̤¦°ûQàQà·IcL9¹ak|T"—öÃß©"Å<}+´ô²dd'ÄõMÆcŒ1fÜÒAÖ†Èå5Ð5ÑñíCLJúÁŒã³(;A£«ª[«¡&ÄÆcŒ1+�â‚[lì°ŽáD ªÅiTô£vÚ¶ª!ê6�1ÆcL´\€) ¸`¯VÛô­þ|` ó�1ÆcL,Â\S’sÖ™\د‹ p*¡1ÆcŒ7äš’p§oÅýZ‡•” (‚æB!cŒ1Æ3> °6DhE‹yúVh[Ûî[ÆcŒ1\�SfòÁŽ”Ä+¸5%ITˆS �1Æc̸…¹¦Dò�u¾‰y¤$°¤d@¼&ÈÆcŒ1fÜÒÄ¿Î!z.í v®8…6}« šO¡J¢›¢¸±~'´„õá[¼ls©A6¥º練9¢‘¿Žcyÿ¼�vJj]ú­8Y{Wî ´õ_&K.«å©ºº·W×ÔâdàíqjÏý•mkºÇôØ3¯8­Š®2–ß»Ló Ï<Èl”©ˆÔ9ÕZ ‘Zïýíl^ýÝQdz°éb‡j©TîÐíyõà÷ÝWµ“tº«è6éÙK§?|õ’eë ûGÇDµhÅœ¤Cˆœó8ØÓ3¢ûúy³N‰x ô+=´W:^^´hYI S %¥÷àµ>ªö4Ó9“6öÏÌm©fÊÞÓQÿ\¼ü�ÃÏFäxUŽf#Ôõ U 3€Âe©P]=ø1r@³!õìƒ3¸“Žáî')½¯ÓÊÓ�1¨ÜåE�wå4î»æ@d Æ%ó¡ÙàOÀç�E\=øz„ µˆ¤�,Jêá Ð�в؇ÈÐÝt§å¾kž,ÿ @¡ó;ôxTOt"Ç£r’¢õ¨N©G˜LúÎgu~ç‹uƒþº+”î-3�ž^lËc%Ÿúø!N\cˆå´eµÃ&H‹›Ž#ËEâxÊ9 Çúý¾šú—×RÅÁ.£NJDùP…ôË- ‡*ù*,½é�ïs¦©ô0€þ�ØzÏú½Úûý=‡êás²H—@gá69ˆpÀÃà ÈAнˆìÿRú0Öì;ò3./ñþOÀàãBåú»|0?¥ï®÷¶>ÝÎá]¨vâè•®Þ÷¿õûA²^ôi�ÇQÿ8¤#íŸdCëczN%Ò8‘à.°çÕÇPg¯l÷­þ,ÌóM�Ì’Ëjé˜ù2¯Tô|h?OQ´ïã3àU ëm7fÞ•Ç’î9Û‰[ âPy.ð È\'>cCtÌú9t=ôŸÃ¸UYÈ4uEù£÷zåR©ß²ñÚ¿DÿÄ*Tæ#õÐõ ç(¼˜zôá´ï+¹÷†�O‹•TSì¶!~Ö¸ì…NR+4§‹P5�N©S{ÿô™uÄ{—|Ö´ä6)þIïï¶@N Óô°Q•_!þVÚÖ<8žÀM,\HŽôž" }ïtß_ Ÿ9týؾ�Údšþ*¢ßó¸ÿaÓªuc 0äŽGâÔ:bƒ­x"Rü>†øYŒÂ¼+�uU¹+´Cÿô˜ÀÞD¢Æ¦yNäU}’ÍNªZŽe]5À„9 ¯|2M¿åvïø_N}ôç¬]›�;�²:ûÃÓéìy‡ˆ^]çéIñ{îó•ó>vTœÖÙÐ|Š8½á jkÇë8àB.÷2M¿Qå:žõèÿL¸ßg3˜“Tå‚~€LÓ¹ŠM«þw´�¤ ßB�ý.:| ûoÅûBûÐ’¯ÅW_']�¡±é="Ù«u¸+Xƌע÷�¯ú;�·/P´’NýÏSáy¢¬`û‰OH¦ùÛ½…¶Ö I6.‹—=Ãå݇´«û�‘M# FJ*§3š�Qüio\þF½&áûRçˆpÛOü£.l¾ŠM3¿ÜzL3V‹EõÇdš~®©ü{¸ûº#}`˜kJòꃋ¹B«S"êÚ ñ;ûÃÓ¥»ë”×&Š™à4Ÿåœ^¡žKbµA…9NÑeËXØ´I=­tÏúªj– IDAT6[[F¶À³œµ¢Žÿ�’ã# S’'1ù š¾5Ý)ilnÑk©¤´|âzލ~�…íËT¯xm×ý!é€LÙ¼Tò©û´±ù£l^u#˜âäpNXjW6Ö×ZJ‹LJ÷Y‹ÌGꥫû—¨XBbâ³`ùˤ±y�8ý­Â[#!)¦,á«RÛ¾�LóûÈ\^•tHÃZ´â|éÑß \ÅdNHR®rFJr�…ï—ÆåïÑ•XBR^ÊYBj �ÍÍI‡bÊjšˆ^ç2M·pÖŠawÿs‡WÏÄ:y+ö“``I‰/Ùq“`úVæò*‘®ï ’ÅLP�Ë^(™¦ßˆ“;=/ép"r’ 7u¿#Ó´4é`µä²Z—i^%Þÿôä¤Ã©¹ )éN9›Ÿ'"×%Ê$V+¢+]¦éË,¹làÎqfÂRx³ôäבi>áH÷sAní‘Ë…Õù6C˜ŒÅë>†ò²¤£0Ђœì6}SÄý8;épbršÀ·%Ót‹>ø7IsÈâ¦ã¤£þŠ6aWà«JUÎHIu6Õ»†dr�^U…wȾ™·3ïÊò×È2 ’3ý%óWœ4Ô=\�ÁJÚfptb®èàš’þ4Ày4.�À'’ÃL4-ŽÌò&qéTy“£S|¡øü46½-é@hl~žäX¼0éP*N®§r’Ÿz7pNÒa˜^¢çIUö‡d.·$qr9]ÒþgC�˜8„h«—ƒ¤Âê|—Kh»o•&Q|K`QùPùsâM8šO‘Æö_ ² H´ÈdfˆðU—iº�Ó–Õ$AæŠçŠè/€S9~¥K×TÌDQùç¤c0,j¿Ìä¸�b{Ž ƒŽ”¹ ;‚•4Outb) ,)™L,›¼.é0Ì’i~½¤t3‹’%I ï“zùi¡(a5,?]HýŒB}3˜J)±i[J.¡±écIGaÊnž«Î~‘’~q˜kJ\°#%VѽÈäYSâœ{/v5ÈD%Óü1AoÅêÛô’s…®uÌ»òز®¡y¦¤ä6àIJ/TiêDSF"\EãòLÒq˜òRåMd–/ï[š;J©`‹'Æ,°5%ÞkQNâöÔ#±ä²ZíàÍ ý ÐM!aÝ“P uXgeŒÄešV)º|ø»N: R•ý¹žñ¾s¹ÿ³í±eéÒ”l×[€çÄvŒ‰"WA ÝM%K‹ÈtIË™¬kÉ%Œ)A>­‹>x+¯ý @Á…v}É*¤’Ž¢ò”.¯xN‹wBœJ8ûëÏfÆÐòŸù¾ßî/ˆ<†Ëõp0w€iµ]¬_ÙÃ1Ëm'£Ÿvq DºVn'Dxdj~Ìm¹LÓ*TBBÒC!ÉÝßûï* Ó˜’®…2Wªj¾«§-{%ÛÖtÇr„í'þ#ðšXÚžhjÓ–”˜‘ZÀþ=ï¾”t ¦¬¦I>¿Rá€taÇ£Àú²Yõ!.ÏO:€Š#^‹_–‰Y§Ä©¼"âÒõªÊ'xÖ£WëÚµúK_ÛZÏíc$ÓÔA„‹¾µ+{[oØ?ü=c–iú„‡8r•ߨc^×£¹?°å˜¿@Kir%dš�Gô¨,ô|à\ʽ¹ƒèy®Þ}ÎÃ;#o»±iž@KäíÆ£o”´»ðwÍ�t ZIr²O”¼ÂÅD›ÈtVÔúϧ9 tEØöPê�æÿ3’–澚¨~:’¶F¦ME^¢m«ŸãÛV}’-+7 w•¿"µ»bFJLD¶¬þ™¦ýb`GÍ+jëµ*M[ëoü¦ÖKUÝy–8œÀÜ÷OshhÃ$€KÖù.—ÀFJJ§›¹ 8}Ë×i{cž¿l‚”Ùó^”³Êp¤í*ºˆÍ­ÿkÒ»®%G[ëÕêݹÀñ§—*o¤qŹãn¨®êï�A+G,¯h³¶Í:“M«Ö•áxñꨜé[&BÖ<¢ä_Ãáu9Q:†ÅËÊqÆŒÖæ•¿V—= ¸/–ökkNu7w H8R²knü¯spÅK’’‰¸û–èÑ‘¶—Ï%¿àºòö{0„ÌåS½ª GºKI/fÓêÊp¬‚-+7©Ë.&®/·ÃDÐO�«…ÌåU¢4GÏ‘t«ð&ÚV·Æ9w½¬æ´OŒçaj»îŠþS,m÷pj,íšñÛxÃãêRODÞ¶øSÃ,žØY51:Qó��” Xàg±ÜDž™t¦L¤î]@´#mÝ£yy5mÿµ;æã ´ñ†Çµ§g èݱGô<5-ûã§\Äþ{§È 6µ~'îã”Ueî¢d¢ÒvÔ„_EÞnJN‰¼M��×þEaYäíz=Ú!Á­‡ªGJ:µ÷däó‘Î�wN^e{¦r‰rĖئ٪Wö.FOÆýŸm×Tþ5Àâ<Œó¬ëcEõeÐcÀZÚV}6îã”YxßÓf”Z¼æcØP]ÜcÌxMŸõ] ÛlGÈÕ» §ºÂè:± Ó·[è> Þ 8}‹ÔÞ([Så�44ÇQˆÑT’ËæoÅð¬:Þ2­~ãu÷õO©sÀ¡‘Sxsß?úÚ5™æ€ó£�¨È~ŸŽáªcòl”d2زúg ÷Fܪ­)©t…µ‡¿�´MÑa®)é´mØô-|Éš’ X<±{æƒê D¥Þ¥te„í™ ä\êâ8ÛWäŸÙغ1ÎcŒÊÆ•ÛU¥)Æ#L¡¶ê¢Q?Jôb }8‡©ê5lh�~~vò,)™$>iƒbII‰t„ÛA IIª&°Î7Ðý”m \j2ì¾UØÆóÏQ6©p™åñʪé¥Ê‹clþAºfþWŒí�ÍæU_ÚâjÞÁëGûQâž.ÙNw®òÞ‹hØÅÃÉBôçQ6ç¼ZR�$Úµ>Ì-�kòv²LhÓ·&C�@�ŸÆÐæjרô -;�³VÔEݾIÐÒ¥)D3q5¯¢Ÿ¨ÐšªHlµ@F·5ðÜ–j`I,Áôå¶Þ0QwÔ³‘’É¢sÖ"ÝõPë£kËÄF\ÄÓÝŒ4Bx Ý÷ç=µ�…ýŒ¡#æß³àvßš kJÀ{½U‘OMQáRñîRz|ŽLÓCÀn`· »Á?åq»íÀkÎu šCØC^=ÂTs¤Ó¸\s8º³“u7—£¨�9’mÇ?Çè×?ŒÌN6õí˜Ú¿®™?¦¶}íçx–ŸÎ=«G6rYÛ¾b{ðð…8ÛO˜]<œ,¦î>‰|*²ïoµé[að¾=ÊMS�’.lÃV_–Ô�vš~tÏ’ØÏÑÁÕ)™$¶Ìú ™ö?Ï�éiàY½P�ÂÇAéýïýh(зtG¼ï º:ª!Ó°ŸÂ/Ø W;÷öþ{¯ÀSÀnßû”§pòùüŸØ²fWLÏor7'®¦Uõ抮�±µ¥G›nWáÒXÚO“a¤Ó)…†˜Ï¨mln�»NK’l¤$I Í3Éwî1¦Su¤«j�øŸT†¸  U¤ÒÓð: ÕÐzD¦;a®æy]”¡£25ÒöL<œëŠx)sM:È«ÓSóžJœ|�¼°’-þ@øY‘¯4}Jà–¤#¡¾«Ã³JÐ÷nz£„BÂãdše�wú}ªRßgýÊÎ2Ä:˜°~J‰žßR¿TްIˆ))QNé]�gAœg$~öuX–”Œ×¼+§’ê>�”; å§r,Ž™¨ÎRÕY8™‰ROa3†zÀþ¿Bªº¸½a÷rG8õÈáÇË¡ÿij½ŽPC«&j>ß…D8kI¥:�ªW³®¦]é lÊaÏ›ú_lr¬)`ΣßâÁ›Æ^Ä­ò¯Â¥¢r)=~—fš®eúÞU6%ltî™O^õ0›Wn‹£áH ÷Å•V:u§�t˜H§Å™ÞzôÎøZ¯•;"WÉ–\VËþ™Šê[!ûjp5}ŸCíwe(¼I.£`IIR©®ÒMTÇE´Æ!Q¦9e²ë;Ù .¬SÔdØ}«ÏÚµyu¼8�t(erŒÀ¿KGý4.UÒÁEýQq4+*¿Œ£Ýȉ{4®¦Uô¤‘ß™‘ßw ¡�×h÷ø¯<6R2K.«%³üJé¨ÿ«¨®¥°[ÜäìœÛHI|.ê Ž5an \ûhX�o€ìŒ2O l¡û€:%x¤`SëU彄–<ŽÏ³Dä]fù8mY˜_43§•õs©n¨9ÝããEG»‘ó.¾š£[@ûŒØâ€½“` –]<©…MKÇÌ?òŸ@,%‚¢–”!Ÿ¶¤€9íá�ìòã�KGÂÞgq´6¯ºEÑ8‹ÄU$EÞ#3Ý/z+d‡¥«Ì£ÉËÎS òP,íFmúSñm‘[˜?B¬;oiÄ[iV"±‘’ád.¯r™æÕ¢üèÉI‡S9Ô’’TõÄ�”„xuzíÜÐ;ßñ­N‰¸°â�JÛê5ªr�K:”²RÎü:7ó‘¢½hѳ¯ÜIÉ‘wÈs»þñXÚ�ÚºSâÜÆdd��Ó–U§ñ�‰>JhxËéŒ÷Íê~¡¨ÂÀYR‚.편ŇøðÖ”Tò––C)Ë�À¦oIé ©ä±Ú¼êzõú*àé¤C)/y¶ääÿ8ã}vöŠP´Whë\yÏ‘qÕŽr¡t[<ñMýYÂs\]ÜIÉdX[f#%C9kE�TWßœ�t(•ÉFJ‚P=#êót• ¸#V¼,))�W'ðB÷ÁlYý3õù3½-éPÊKçKuõ—b<@´'Ê|m™?—à…¢-¹¬b+ê;²¤¤jJÌÛYëd(gIÉ`–.MI·ÿ&–�IÌL$Ò£îsº0×”rÕ¯W®3Ô×9>¾dú– 6A»-×=ª›V_¤ðV ¶]‡*ÐëYØôÞ˜ÚŽ¶3”-óš’¸vDÌ§ŽŽ¥Ý¨ugõ‘%ëZrÀÁãˆgÝPe ë;º\¶Ÿ°ᢤèp“ûÂL(öç£þw 6) kT 7Åvß t÷­I6RÒ_[ë×5[õlU>L‚ùæ ʲ`Ù114퉲æ`¹¿ ÷ÆÒªø¿‰¥Ý¨åóq&%��â¾ñ¼a$ˆãc#%¥­˜#ÈUI‡KJB�šuŸ3åpÁ&%v¦”,Q3Åî»æ›[ÿM«Ýɪr±¨~ ¸Ÿ‰» ~º÷±Ú�¶3”+ïšQy*Žv�Êiq´9çcë°‹ÈÈG#…�qÅÃüqÖA©õ¼5fâý`JÒq •tf¦F?R’FƒÍHÃê€ç»%ö_³ÐFJ¤yZ€I;î㔕³¤¤Ÿè ‚ªŒx{kcBaÓ·& mMI ì¨�/!ºÚž=Ãßi4d –E½ãذùRœí ÒBãò¿�ó#åj«®æÄÔüãìÓ¯�ùÑÒù-â/rX+ª?žH‰‰Øî[ýEÕE8åÕ˜ rRÖHI¾¦ Ý)ÉåÊ×<ê™Q7é‘Ýѵ¥{£j«W•÷±ˆÛžÏ•ˆ§¢•p"òU4ŵ3ÐÈ46¿Uáïãj^áz¶­ésm7fýt„! e†¨ÞÁÂå1׳(Û}«Ÿ§‡¿Ëè8ïßu›Æ$-ä¤Ä:´¡³é[aòùêHèÈ‹Ú G¢)šY¸üÒ¨Û=¢-kv ÜóQ¦Šãv›.Œù8ƒ[ØüfýrŒGx’´_=îVæ<öàžñ‡3¬ZQùŠd–¯eÁN.Ãñâ£b Ý‹´˜€Â¥e?'3G¸ûA‡5RR—�¿ÃÚô-Û}+D"ý¢Ü|îÞÈÚRõH €ˆÊ×È,o¢Œr¼÷WC„SÛW'ÂwÉ4}´ŒÛp ™æŠêW‰±Pš¢ÿIQεkóªn9e»&—ˆKos�Mß`aó%d>R_žãFÈê”ô÷p mЍ|…ÆæËbhÛ˜D¤±‘’òðY�ª˜ZRâ´dm~¨ŸÅÉ#Ó¼Õ…·úG¶\áây},º¶Š¤YE¦é"Å5Ó¶2ºÑ�¡lY³K›þC„�ùHi�'Óþ u+ÞÃÆ•Ûc;ÒâeÏ�¼»Õ—Åv e#3ŽúïÈÚÛ¼ò×ÒØt½ WDÖæ‘¥U¸TT/….%Ó´ø�À_¼È¨>‰jä B7*yRRHÀ¼ï(ülž}T¥òxý¹×vß:DE׋ÊëchºJDo’LÓ+¼ºO²yå¶ŽaLÙ¤ Û°Ù k¤¤,[S"y-ÚüMm¤¤bÍ»rª«Ê^¥èЍ›ôÖH?¸^·“Šõ£ôRÁßK¦éÇ*ÜD•û1ëWÆ·öcƬÏÐÑþ Û1{‰xÿ{ilºÑçª>Å}×<YË‹›Žs9þQs¼˜Y»ƒëTÉ¿ƒu-‘vŒ}�û°ôø%@¹ë¼p*pªÒ¯$UÿS¦(ô|Éé4øØ¾JmúVŸZ�Ä×eQx³ˆ3™¦{ÝäUv ìAÙ‡cÈ>¼|Ä5åò ãMŒ#©5“R:à)3auÀ}µÄ²mú–©`-Ž3÷�F.¿À ¯R²+ÄQÙ\½KÝi‹Ù£"Õž£0.åzüA2Mw ºÞ+&•ú ù|;Nö#.[ô¨�+w0ÚÁº–œ.\~™¨lj"{C«Vá ©Ê¾‡ÌòªºoP#ëX¿rô‹u3—WAÍK²Ts¼EãOFPôƒ½5F¢µ~e§fV¼EðwQ¦ç,)9lFûF:꟎ŠùH Š4êÕ ½gº«ç•кSfâ yú–�””ò¥k4*\ÎiÑŠ&7�º74ŸB•D?_?›Oá˜1àvçj©p»×i Uˆ¤ñ~:H=è'Ì@äXUNŽƒö“É3•¸Siå66®Œ¶ÇÖ–2Í?}E¤ím °D‘%"®ç9•œ¬°˜Ô•óÄ$ð9m[ý¹Øжò~ml~»ˆ®%Ü5™ñ³:%‡­»¹K2ËoVäƒI‡bL%KîI5¬¸Ï ®² ('Nœå–xú–¤ô^¼LÆk¨iJªGžÍ§ý¯œÉ¡‹ie¦*ü[, _¥\IIylZõ%É4=_áC ]€¹*Ì)|b”’Ϙ;ôJìä,ÜîµsyìÇÙ¼êVÍ4ý“ÀÄ~¬PY�’">•þ¼äó+÷B°1q“�·k¤ÄWÙî[¥ì¾5�GJÌŸ§­uC,�oZõ ž¶äç<úA¾�t•I~¢Óöþ-m7f‡¿oÚZ¯ÿvÖښ’þî¾öOqnmLð,)™PB[è^šDYR2‰<ìÓþ#1¶¯ªú 'Æc”ßÚµy?ç¯oøJÒ¡TáV­–×³îæ®rÖ·µ6[b2„‘ìþ5Éøj÷! ºÍ#Œ™`\¸ Ýë€×æÊð:6RRjOß2E¨ê"©q$›W·©ðÑX�‘„µkó¾­õ2E?‰íp„¨|F7ÍZëhCÓBb¢×&pìŠæ�³¤¤Ôú•O«ê?`U�”£hOÖ�ĸ¿^¨‚Ÿ¾e&�¼ oaóê¶²mSë*A£«UQ9”¶ÕŸRük€'’&!O«p‰ß¼ê#Ð’ä÷�ú¶ÕR•+Àwb#%ƒÛ¼ú»*4%†1•(Î-3ãù—�*¸…Í…m½ö ð"…BTŠ-T‹vt¢Ò;UÀïÅ¥=ä²äe?âàªz�¬Ò™ßC^fFoðrNqEûêš ›êR‘·³iÕÊxLõsû·ý¤о±ŒÇ-�¶5?ѳV<ßuë*}{Òá”�p»fÝ{¸wå_“å�Í«®×Ʀm"|˜•t8‰Sº“¡bmj½N6׋ê§w ½1‘ yKàþWÙ÷ªø—€¼Ày÷}È\ГGÙæéªz:Pôª.TÕïE×ø]o*×ïq©ÞÛRP�*Ïv4>°)m’×âÍßlMÉö´ª{=m+]ö#¯]›÷K—¾Åm?q·ÂûË~ü¸­_ù´‡w°°é«¢\Kù û•Ó겂¶U·&È 6·þŸ.^6OrîKÀË“'Q½õÌ6­úWm\þ;¹ ¨O:c*ÁÄÙxÓš-À–¢á“ÅËf�•Óq2×)ÏW˜K¡*ò å ³Œ$°yªâÂJ¢ÌXýŸ¦ý{ØÐúHb¬]›÷ð2ÍôÎÿ¯M,–¸ljý©.]ÚÀƒ'¼[D> œ”tHz\U®¦F>ŸÐÚ‘‘Û°æ…W’izŸÀ¿G'R2Ô’’ál^ý]mXþ€¤äKÀ9I‡cLÒBNJúwÀ¿Â^XHÛ´õÖÏxß,ª«ç¢ú|'2WUŸ�H0;¶hÍ@â´¸¸œ-tŸ`ÚUø›Zo¤Rê µ­ú¬66Ý)¢_93ép"·vm¸QO[öefÈ߉ȇ�g%Ö8üNázªÝMŸŒSÚZoÐÌGnqt]©Ð LK:¨ò’­l†IDATƒIG„{VÿYá\2ͯô?€ç$’1I™(kJF—XÝÿÙvà7ÀoJFVžA6] ùÂPjÿªØêëQÜáJØ€“©(Õ½æ 5 …u)ªÓ�´ )”〳GùG«2:~#%^‹"V›¾5Ax�/zï?Ζ5»’f€Í­÷)-gѸç"þc ÏN:¤Èm[Ó Ü¨K—~‘Ÿñÿ^àÕ„qj¯À÷½ÈMlZµ.é`Æ¥íê½þgø3t÷,ÕwRø˜øç:±‘’Qi[õ=]ÒòCöí¹HDß ¼Š°ûhÆŒZÈ#%ý»³Ñœà7¬óô’açM�yÅ©’Omkû#"�­)1�°^ón¹nY¹)éPެų™›•–¯Ðøô+œ¸W)úbà &RG 0rr›Âmdšž r‰¨^ŒðB*ëÜ¿SÐ;¼¦~À¾ÜO´�TMw~¦ø’—X´b^_êÔŸ¥"s)ŒdM¼QzKJFo]K¸UáV2Í' z±S^¤ð< Ÿ“é GhL¬&ÊB÷JúrMŽlKàœS\¿tÎÅ1R";A£;‘×êX×íìa`M‰öÁï*0ØvšzpÐmDz„A• dï ‡Pö VqÙ«v"2H:É¢~ÿàáJû¡6ÛZBP#v-žÍÜîáv–´¤Ù³ç¤ü©¨;Ñ£�ê,DfQ M«jïÎJ® ÑÑLÉ™ÊÔ|rë¾ÚZ®U¸–Eï?M/q*ç*œœFy:•ÿ´Jb¯—ÚH‰äµèw0†â‰Ú¶ª!ê6ÇBÛZG»ÜèÚ�³ñɤp¥rgï`‚V9ÛxÃãÀ7=|óÐm‹›ŽÃë³È§Ž†ül„£r4P…Èt¼O#2TâRHf{·R÷°až=Àð»É§ÿ³y¼wôÆ>³ý^“'¦Ú÷¸?‹}�Ú4GA~6Èïæ�·éú¶Ç#mk}nT¡L…uUíý3¨ s¾g%+mk jZ®¶µž–t c¡m­‘®Y w¤DÕ÷ =Ìç¹ÀFJÄiI×ÄÞGc’²¡õ J 1FÞÁ¹7êͨ ìÔþB3úº�‹~íW�LÖ¢žÆ˜1rÛ”°¦où\^çÀ’’�ý,cL ²ÒIôùg7ëW1=ÕcnR"#ظ’hºòc,7ɇžDcLتr/'ò {ò86CÏ3Já&%޺ܚ’Tq¼ègÑc‚ÔâDõŸ¢oW�¾McÌDçµCÚH‰ÏÇcp»o•Œ”8+žhŒ1e“i_�°(†–‹¡McÌîH F}VR"®d¤ÄŠ'cLYd–¿Dà_ãhZ„ßÅÑ®1fb 7)QW¼0oéÒÊñ©.Çš’ÀFJJ“,)1ƘØ-\q¦ ß…xêOxØG»Æ˜‰-ܤ¤týÄ®¹•ý<Ê1}kàèQ`lú–1ÆÄ*Ótލÿ)Ѩ\Ê’cÌè…»¦¤t Ã]Á>�èHh#%¶û–1Ɣ͢ç ܽ•áã±£·æ�1ÆŒJ¸Å-Ú«û©Ê~šH%Ee‘”¢ýrK[SbŒ ECóLòÝ…sV]j&ZU|þÊçgý;%\ñèDÞ§� BªÓŠorUHémZí�©Å7Q�p[ èEï/j‡rc'ðm»ÚdŒ‹p“-)9&íØ–P,•"¸Ý·rŠë7Àe»oc!)}”TuyP°ôtæ»ÏP�—þ`°S» ¼U8Â,Þ²œ^³Þ¹Ëq cÌÄîš’Ò³{Ï�Ê~å(žhuJŒ1Æ$D�ظr{ÒqcÂîš’Ò‹D]éÊ~ZŽ…î��” `#%ƨû}µ|,é Œ1á w¤¤túÖÌia>�H…V§$g[cLøžÖTêÖ¯ìL:cL¸ÂMJJ'ÙvIe�”¤Ë1}+°‘’Òé[ÆcB³O¯âîkÿ”t Ƙ°…›”ˆ�”ôí‚2™ùÀÖ”¸Ò5%6}Ëcr@á6¶nL:cLøœÂ�ö¤µÒ�¦ê*|¤¤,kJÓS2}ËÙô-cŒ D»zy9m­¿I:cÌÄàhk]«>·ëÄ"%kJò=•ݡՔ-t/5`¤Ä’cŒ À“êä¥lYµ>é@Œ1GataËõéôY/Q‘«èÝq=ÅÚ¬«ì‘’rmMÉ6}Ëc*\›:wWÝ“t Ƙ‰åpG~]KŽM«ZTý9@åï3^ºûVu…'%ê­NI©»ocŒ©P*Êuº×ŸmµHŒ1qؑ߼æ.¥¶Qàë Ä3r¥pŸµ«ìÁMßJ—Ækï¡1ÆTž_¨—³ýæÖel[Ó�t0Ƙ‰)=è­mWïõðV6ýŸ(×ÓÊÖðçµÿ ®\#%q¯Å÷¡Õ)É*¤ÿÛ*ºcL%ÈUõv�ï²¹õ¾¤2ÆL|ƒ'%}6µ~Eϼâ×âS· œU¦˜FFÕua}UewhÓ)‰¿¶¡ +)éI+ý׺;KJŒ1¦LòvÞÜìäÏ^t^·Q“º›õ+ŸN8>cÌ$sä¤àîëvè’–³Ï'Dõÿ1Ø”¯$HÉB÷ê ߸$°‘’RV§ÄŽ®Þ?¥öBÉšGè rßTsE·ˆäö ¸§HñÖýªÀ¡ÇzÕNDº@óhïãû£Ú�’%¥O“Kµ“ªi§íê½EÍ ö �1¦Œ†OJ °Z´±ùW"úà¤X£õEK|…×ѼTJ>W1$«ÅAÛØmk=*¶¶ãjØc*ØèzÉ›Wý\I7 ü ¦xF®tûÛ¼�”·%ðÀ…îÆcŒ1fÙHImÿµ[áõdšÞ'p P7Æc g¼o&SÒSÉ»©x�†2IMÁå§â™�0dŠS™Šc&ªÓ¦ :M•†¢Öª*<)Ñ”Øõ¯.¥øÃeqTm(ÉcŒ1f2}RR ´µÞ ‹VüR¼ÿpFÿ:ô£,lÚ�2M‘zЩÀT`0½÷ïS€Ã3bEzgcùÂ,ž~yT´¸??ØÒŸ³©?V§Äeoyˆ1ÆcÌd7Ö¤¤`ãÊ­zÚ²E®Þ]¥ð�ôNSXz8‰(S?YÓ•�”¨—Ø –{Wº¸2,¶û–1Æc̤4þËÔÛÖtû¶Ö�*rðäøC•vàà�hO¶ÌÇ6ãÕ£ÅI”Õ)1ÆcŒ™”Æ7RÒ_Ûªé¼+Ï�ªì—€ †¸W…D¢èìû·@'*]ˆ¶{‘vT;A»iÇ»vRtâ}ε“˶“ËwòÜÝûX»6?Äq*�¦‰y à ؆24–”cŒ1ÆLBÑ%%÷]ó¤ÂkY¸¬ŸêBô==Ã%cšàuÿØÃLDÚKð)CÔ\z‹…Œ1ÆcÌDmRR lZ³¥è–Ј8¨“�õ´¢XñD×£øª¤£0ÆcŒ1 ³­�&Z�’ªâxÕŠ'cŒ1ÆLF–””‹ú2t¸Û¸”³¤ÄcŒ1f2²¤d" ­¢{géH‰­)1ÆcŒ™Œ,))—rŒ”„–”¤ºÃŠ×cŒ1ÆÄÂ’’rÑ”Mß*åªK㵑cŒ1ƘIÈ’“œT—%%ÆcŒ1Æ’’²I—aúVh»o•²ŠîÆcŒ1“’%%J`uJR%Ó·œ%%ÆcŒ1“‘%%墮 Ý]XIÉþ’é[6RbŒ1Æ3)YR2‘Hh#%5aÅkŒ1ÆcbaII¹”¥Zy`kJ$%V§ÄcŒ1f2²¤¤\Ê1}Ë6}+}°$^«ènŒ1Æ3YRb*GYF“Œ1ÆcL¥±¤¤\ÊQÑ=´Ý·Òu%»oÙô-cŒ1ƘÉÈ’’‰D[S²g¿í¾eŒ1Æc,))›rO n÷­)aÅkŒ1ÆcbaII¹hª £�-t¯ÚW¯�”cŒ1ÆLB–”L$>°‘’�,)1ÆcŒ™„ÒI0i¨ës#,XöËáž÷<ÃÌææÜ+"²©”¬+-vнkäcŠõý@jP;ðwô� [‹ˆˆˆlD*%+%Øs"u ¸l{¬öA÷dpzƒÑëX õ/€BÓëõœ‘ I¥d¥T/Uýààî0Êß[—|[Áƒîý§Ú™Ùv§×Œ> Ø…ÁëME™~7'‹Ø¯ãËXDDDDZ•~™^ éé­ÌÎ~cØðéR�õŽQ½x}©æ½!)‚Û�`1õ×ÅJ€öœSþÁý¶‡�­Fôq~{¶ iEDDDd Q)YM�£mlyzÒœÓ@çbÇyô£L]¾±è\ý£›x1½—F# „’›'Àç ˜ôÐܲh±†Sáîö)�‹Î'""""ëŠJIk0º‡O�!ÌŽ.tˆ»áîØÍÜ “3Ûs=¸—,Ƴ>ࣜSž÷ *ÑCƦ¹ n_}’;‹ˆˆˆˆl8*%­&)w§ìæ_“ç�8àøWÔ.ÿôÁ {†wã±°ÔÝKÀ>òT"ž�Õàß[Ô®ÕsÎQ)iYÉàž@røØÒÌ7;Ì�±Ÿßx±s ƒöâ—ü[Þ>`{Î4uà¾A¡B½ø ÷Ï?Î9CDDDDä­TJZ]×ÈNÚæÌ}�” 7;Lcþ/B! PrH�ý@ȹë# æP2¦c•Wþ[Øy?•’µ¢s ƒÍmßšù0ðÙ;®zIþ³ uð)#LF÷I … ªþ^\X‘æ©”¬9£�îéãF<‹Ñ³€Ó¸W=„ ³ ñŵk/—<¦ˆˆˆˆH“TJֲ䇒Áà8oÿ,ÀŸæV‹Ä …BFõâ4ùdC‘• R²$åîàþ£Ç0ªîLÂmÅ[dçfV;žˆˆˆˆÈû¼Q×’ì©ÓÃIEND®B`‚scikit-hep-boost-histogram-b4c49b0/docs/_images/BoostHistogramPythonLogo.svg000066400000000000000000001275711524660700500273700ustar00rootroot00000000000000 image/svg+xml scikit-hep-boost-histogram-b4c49b0/docs/_images/axis_category.png000066400000000000000000000056451524660700500252240ustar00rootroot00000000000000‰PNG  IHDRm@{MîõgAMA±� üabKGDÿ‡�Ì¿ pHYs––qFþðtIMEã 5óÝN ¥IDATxÚí�Í™¤8†ß˜g`L Æ�yT&P{Ø;eiieiiÀÀ0Lˆ=@’ü$?Ù[›Ý �E‹ ú�"BRHK–~FúͪÀ’…¶%KÚ–,Yh[²d¡mÉ’…¶% m«K?'ýþ:¢ŠCˆƒKΗ–»sO¸R’ãáQêewþ.À!²;„íÏ#¸B.ôøêõ%£/rá�à*8ÄTx»ó¿ÿŒ�>j$Æ!Ý[v"E!Dñw—=ÝH_1ò*³‘s񼒣 j}Û™Æõ°~/ýj»Dú±+÷ˆ��fœ%׿w–>"§îþ ùÜ_C¿¶AâãËßZ‚Örųó°Xëé0ÙÝÞ(YŠ·3÷zø†FöKï#½¼°_É�,g~¿ÕvàØÙRÕ³ŠžÄö·ƒ¿:`»xúõ2Z×¼ÈPÜ�y¦B"ÂÆnÝÙ®PR\<ÒýùwïIÈŽã®Éý@ïéeDv�põ¨Z§©Øû³QðÈP*²ƒtbˆH�…áKáä¡�LoLHøc¢&¾püpIŠWê³_ÚDǺ ÑÞæÎÍ Á%E�ê¹[Ã';¬ÏÎ^ )ßìFJ$é3 pôo­·––T‚mÓbæb³Ïx’J´I�„³Ö ¥¾s“L©¤OúM5ùfîÑ“Ü õ3íô\»þŒƒ³Ya>ž~6¾ºl w¹˜�a±„ôiÙ ۚϽ*õÌuãS³-"/…8�nÕüsÁ×ü©pçíú«ÿïlÀß™yΩ×,õ¡SÉùS ÙRw£üÝ^Ž~!`'\ZÀÁ4‘ÜáÖ‚Y ç�…ÿ’ð&¯8x;O]n3¨â`È÷_ÿòšô:³‘ n·h÷‰ =I$%!_º7´OÄq\<>væþI$.%qÝ�»…öáSKoó?‰‡OÉÇþÉZó!.>—ýgó´æSŸåSóƒÔ“sâËBûEIsò¹—»›9ÿ3ÙAë#¥ÿåÝHK–,´-Y²Ð¶d¡mÉ’u#Ÿ¤³Tqþø×a¾Iz¤jäþסÜÿü~X}ó2 ˆmÿòSÒø÷÷îÇùö½‘¯8…ki•ß½ÑLlbKÖ�´dÉBÛ’% mK–,´-YÚNvyÔ “8xýMâÜwؼH^¾õ:Þ#hyãà†Kj¯½ð(†[qÛ=óŠRü$ut©º?ÌèþÊöïI¯-!8 Wþ>¾»Û—éÜ2ú=ý¬KÔÛ>Usú‘xª¸„xÔä\ˆ÷Κw�ÎR.Lö먌úõ—œëòJqqˆ{z,»­z�Ë6\ËõõçâàâPs]Þâ!Õhan®'-ù£í»3§ZÊ)©¼Ï£fjk×ä˜ÉÞÃÇw÷¢�€Ÿº)É)1í¿k�ð§ïâ‘a¸’�­m›}Äá(&×Ïåî@\ |r.ÔIJ<ïÛlÄÈÛ«^vÚr͉$[‘ Å#çJN¸2±ï Þ¿²mXkÞqHž4Hgg:&=o›?#øÇÙ“Öb‚žèAöޤ?>ËáêðQ®¢¡ì$÷´8øè’–ŸÐ`Üãê¡K9¥ˆî\×Þ@t«Å­E–ž]æø!Ñú[“$†Àçm ;f›”8²oê‚`S²d·~]/\ÙgQƒKÙq͹.¦Ã8sîÌŒyõnJÆ÷Y|âÊ•àÙàŸ#‰¨¨$ÃÁWbQQI—JIEEE%»%|¯½“€T¢¢R€xío®%‘PŠŽCö4�)ƒ¡´ˆŠbï’àsé ì.³u˜ã€Dm=â»fÄk9Ä’RQ5\ÄiËV·ô<�fBñZ­§·w‰JØÕGom!î¦�ÏŸ#[|ŸÏ+Ç»ÕR<Ì’$Zë݇ ©·lsŸÏ�ž×uÁHžÍ––`ðɨºa¢ÉXçãS¯š1Cr2p0DT˜v˜òÛs 34vðQ" ŸbÍ@˜M¸˜©´8 Y+™éÎ)xx’Àl&R*‚VÚ¬gÚ¤** Ió–®l0‘+CIðI¨Ú»U3HãQ`Æ…óxø_Lš­h0¥@©ÖÒbQS­§¸Ä!""[6ˆ¹“­¦Ç’I2w3íY=qw·è¬­tjN—2X1Ÿ’#hG÷ á–\sÓ*ÎI;¶”ç•3[‡1‡í@nn U{ Gp·‹'e½׉/CÔùÉH?:gÙ.XÈÕR&C JÑ~tÅr` CFEŠ6‡°lÈx˜RQm9Œ¿¯•íßg�v@‘ ag÷\ºxɪ$®8x¸ƒÒŸd¤k�(¾¤àBI©çÞK½MÚòé:ŒÉ'ÇéT^ûæ›EZ6öiký^oT½J9H7VNRï|Qˆ«¥8ø¼m‘yÞD#X9¨#çÜd&¹RÌé]bNm²úˆLÞ—CšóÞ–Mäc5�O¾Oˆù· ÖÌ�8M î8Œ¤5g¼5°jÉ_x„¤RIx¨ [6îÖ `µX‡‡�³´»îðóZ¸û½DkÎÀÚ,�Æü�l�›ä“ëèTˆíëÜÅ‘ŸÏåî¤ok¹èrFœoj=‘mÔô‰+ëmižBœÃl"¸ß6¼êyŠ0¼ë›~èiØÃˆKÄy1Ù†ùõ¬ïúÆœ‰ŽÙ® Aë ]ú®ˆ„Ëu˜p¨¹¨ô®S×%¤’JFÐsæê�>½Ð�<>`¸Œ"õÖvi£Ðï«S+‰$›á”>H³È5: «-ï<ms�Öl…¶ß)Æ_‡‰'Ñ ’âò53ßpÖåÊô�OÔô�Zëy¨¼É»~|mBÜ*ú ·72˜bóus¸âß?>‰ÛÆtÉù$çÊ[VW¼.ά7£~áˆ�Ïå!ä·ÌYfÀ›æãì¶=z÷ ž¸ý¾sR²¼{ Û¤¬?³Y,ÛHñD:ÎtÎú–q?BÒÆbŒ‚!&Ã܈ơyÐC„R`ðp0ÄÍa”ÓEîѧ ôgø3pâ†ïj8„ Ç6’± -1>†øææµï‹pp›ØÍ\æ8Sµ�¸{ÞCÛ˜¾«EÖò g"7îÄ5,¦Ž4î£xÇdÊÆ§êôhHGî´R�&W"¥‰óÜÝÈIIÿæ>â‘ Ïðœ”M¨¶•]t�OÙ$K'5Ì�ëS´á3¯erûÏû™á@-ÁÃ…:JBx?p´ãæá�^}ƒUÚ•)†~õø]x#Þ²´8¤-_oô1ŽÞ6­Ã<‡¶Dïy‚Þ½„ŠCŒ¢whŽj�<8¹'œ9¨4X�v5z(™´�y ÿ”ôÛÚêø€×E®ÙZÙ¦ôVh¯ÌnÚt=õív/nx¸Ä�›l#'wöÞh–ˆˆhË�JS™î#çjIw:ÂÌ–õ¶–Å–kcâÑÆ$kqr é£ÖxdE›}8 €8^iq‹Ó$¦Qª-ka¶—<¢,N/öäîÇk‘…áQk©‡Ãrp·ìŸ8ñ+|4½µÓ¨’n;*o{ÉãÊþäg´¿IHÔ�µàãpzv ˜T”8”únµùC-`¡}˜j]|<jr¾žßh!>ùm†Ð’…¶%Kg#-Y²Ð¶dÉBÛ’% mK–,´-Y²Ð¶d¡mÉÒKÒ!þE¸Ãù¸%tEXtdate:create2019-11-22T16:53:24+00:00$ë1%tEXtdate:modify2019-11-22T16:53:24+00:00U¶¡�tEXtpdf:VersionPDF-1.5S6ZÁIEND®B`‚scikit-hep-boost-histogram-b4c49b0/docs/_images/axis_circular.png000066400000000000000000000125311524660700500252030ustar00rootroot00000000000000‰PNG  IHDRäÉXâ‰.gAMA±� üabKGDÿ‡�Ì¿ pHYs––qFþðtIMEã 5óÝNYIDATxÚí�=ŽëLv†Ÿúð“N}NÆ»‚oæÀ€Q½�؉ ‡ì%HK 0+ bGì ˆð ÄÙ�˜NÅI0¦�b‹)QݺÙ÷}4ºÕT±X§^žS?¬2!ÄÔùEE „„,„��…²BBBBBHÈBˆûñ«Š`º˜˜€…/¯žk™a ÈYúBe'!‹±È8¥ðs0![óý²”�%eî cIØšŸ«ü¾XmÐÌ®Ézã™ÿVÿž`ýÛÑÏ+©ËŽòðM¡6²x.Ž÷9'6öâÙ;ø’ŒÀ8 „,Æ@Ä{x\áų‹žß…„,žXÛ}ÿÝÿÖtq¹w1›Äì�o+•îQg×4 ?üYT¢6 –W2B —¿–�Å4HÉýÀ8J?3cís’©/[¡µ˜FHž`y©ÃíÌ/*ïîóæ§�G�Ãgæô£r€Œc¬ÿ~jW*UydñxJÚ^Wý©‰«Ñf˜wÙÆõ7C©„,Ïúȇ:JŸoË„¬X×X\3'¬™fa'ç¿�¿ñòøyW¾ôßYò Gs¾$dñ)¯8ãÏ”¸g½©äßXò;VFKÜk®µø°y"VÀë3ßPªìÜ—=ä‘ÅÇÚÇ)°x¾ä…’Ф²ˆ„,>â WX–~þôvz%åØÄ²ŠBkq«i¶„伌ca“/>“eä‘Åpá$„”c‘1ø% P§—„,n «càuLËäù9–™¬#!‹¡$XÖ£ c@l™Gmd1Ä(;à›/F—³”X³¼ä‘ÅP ËñÉX>YBýž#¢d1Ƽù‚eý ²¸Èl´þ`NIdœÌ$!‹ËþØ�Õø’eý°²è%£Þ�iA‰S;YB—ˆ`Ü/ ú’u�O!!‹ÎÀ:‚ ì–˜IȲ¸„F¿©š_Sj²¦„,.ÖSØQ>YB½�uˆ%íÀÓ©�5%!‹^<�××@¤àZBS¬=×w÷ÉF yò�u@@ñ¬Eön&‡f{õû¡‰&äWÁh€b2¹ýB6î0ôf,3,ùˆ'«Ê#‹NÂZÓò½gwEU�±¤,ý¯X¶&TÕ��§Ä¤Z‡¾üBëõBÞ|¾d¬T5$ä©…ÖSZÚ.ƒ{θ6qs÷»*e_’è]+ ùá˜Äì�oC=ŠʉÝî=}²k昷ŠÕB„:»î,c,¯dM›·\‘ñù„n6Ç�!LB|ÒxXû×£ÿ[‚C·VkO '1KÈ�–±£ôs03Ö>7!Ù¤„ùÛõpq׺ Y¨ßZB~(>«ýSèÍÏîjýUø7óÏŸþ—ÿË͸ˆ—Ž”Ròçï´!!ÿœ~9¸Ú~ü:ÓþÈ;?ÿËm8Pœ¿¼il§¼…„üâz4¸üsñeäý¿üO—G¾ùŸ·µMŒÕ�ò3…ür¨ºÝ­»î`ÑLQÈÿqCà{éçN%k"Âjýl€ZÉ×ÐðÓ½똲i~ýñÏ[†Ë¯{:úlÜéŒ6dŒÞ{–�ŸBÔô¾æ7 9ƒIMG¼iJéÅ\Ôî±6!+0‰ILbRbùc…Ö�gÞ «¼Þ8½£dbÓ4¹e„·ý€k//žlB³"8ê1ÐF®òãyX¹Ù�€›Pµu7ÝcïΜutùoªGòt™Tiìm9¾ð€sh¤Xmä/&äé´‘Ãû‡�ïJÈbòÁõ$ö¨’�ų˜ÄžJ&ÂN`ÏH Y<-¸žÆ>‡òDzà“ãQûc;�­m$dñ,–ÀlÔ>y†%So³„,.×v¼«…KLß'BB í T‚eí5ô4¶¬WŒÏ()1Ë“wtÇ‘³€ðM�µ<²˜²ON@û#JÈbX;¹`Y‹f\þØQêía Y eNI4ºÍËfÀRA$d1Ô'—,�Õ˜†¡L‚£Tµ„,n‘òœ Ëf,R613àUþXB·ñJA8Ž–² I�7 ;IÈâöðú•’øù[¾ËXz­å1Z4Žu%¯„�‚WYCY|ÆDÿÉŸùî9›¦›”˜¿óO_~ËvydñC…ò/ÀïÙ˜‡¿Üh¬Ùó~7î+…„<~oø=K–XÒÇv{™�-!%ÿNIlRYcÜ­0£=ÙãI=b<žöA׎ÙãÙbÛùÐ1Òº¢"˜‚Œ=\-­à×Nð­kKʲŽ{ȸþl‹gOüC¯°Âã™]Ë� YÇÍ2öx°µÈ¶¸r][ûâ=ÑÐ<é��uÜ$ã¦õºÃãÙÞ]Äû*¤îß%e YÇ-‚º*—k’ûPçÖ€ÇC-噬$!ë"ªt�ÿ¬~M>f0kDìD ›Gõœë~hf×”G™fõT�’5Ùm/šGTo®^°ÐKš¢)ž'æÇa.öšlÈ.‰ÆáˆêmÕKÖd^ ÎKÈbrŽ�v�Ê()([[‘[B $à°¬_Áš\–�ŸŒi‰p8®¯*’‘³Ö‹²·YC,�?ñþÊž‚’‚Bû(JÈbŠ&ÞàxÑ"=_½ý$„„,„��…²BBBBBHÈB Y!! ñÅøUEðåÉMÊüâhЦ ­…²BBBHÈBHÈB Y!!‹Ïa"cU ²˜:3Á×B3»¦ìWm#È¥/nøž{_€ï£i YÜOÆ _‚™±3¯7¬O‘}: 1.´kÎdwˆJð‡ÍOñloøæöóièס6òt)¡Ù3âO“}6 ¡Î.q¯Xjá�_˜¾nµcùé4„„,îÎŒœEãq³7¾u¬Z-ë ³S«•†Pg—xd‡—ÃRðÒôB'X^ È©ÞF>nÌúZb‚5Aï#OÞ„ –W_‚q„~fFæóêçÉÙÛ.Á§!$dñ#ZvþûÑ'+ÿzøy*ØÓÏúÒj#‹‡âKrBɵâ¼?:îëÐ:MCHÈâ1~xkv­Óá‘\ó“O8¿˜†��ÅC Z[šçç~×´|²qäƒÓ²xk^ª�$cqä‡é•&¦lº¸\ëÑi�u_brñ™:»&k:KBN$”ï=ÎfCîçUàLqܵe¶§�Y}i Y<Ò|!Ëúx�É„• M@ù.MT¿ž†��ÅXM½b®ÕFSÄÉXBSçlj¦��Å�Þи§gâè�'1Á:äÔF~¶ BV”ÏžühXO¹l/¿Ör&d3Ãóvf÷§w¬ê1ó� }׬8•Sø;úœúŽ¡ðoŸ¼· ë÷»3–!ëëù5!3r×dؤ›“ù§ùÞ�[ò±éÞ˺g¹LN>*ïÕÁh,è—òyh]’ãˆ}z/fÄ|4ø,p䄤f{ÇE^Krr âO†Å)¹;’ñ†ˆœ59‰Ù^}Lm�Œ€­ ¯<±wuº%©IŸæ:>cÉG¦{/붉puºU�̉îµþŠ/yÁ²ê?¡kM'O2ôÓ»¬>e‰?ü]‡Çy<„ìI&õ~†'8ú;e‡­ñÌ.–Js?lØ\¼Îê(Ý\ž¬¶ŽØ',ùøt?kÝ®UÔNêä}Ó¿�ßQtvùòÁ ÏYÞ3û€EÚã“EfƒÃd ð9ÙÅçu„å0�cŽ»è;4é®ÉZÞ+ š–%—î�Ys:áfÁ{&|FF<4´®°fU-ÓéLj¼ñfsYÆšM½ÈÌöðbœ ß—�©—¢Ù� ›ei\» cvM ÛÁwj9jA4KÞ¤ï¹5a�³Ôlس7Q}GQ“ÃÕmwÖÜ×Ì„u™mŽÚLÁÉkƒ9á!7&Ä]\%Ë‘‰¾¸Z¾�T! `b³9ØÏ8³1QcÁ¸mÉú¯Y·Õ;jÀÙ™}–¼±%™m�Ê!gé^,óV ŸµnßµŒ5‰Ù´Ž|îOKòs_ô¤p!g]5·&ë}(÷„ÖžŽˆ-ûChˆÇ³!"ºÂâð¤8«&Ȱ8ö¸Ã"¬„õ¹×Û‰ð$8»*\¹n`™§Â†=q�¶ 2÷lpÄìÙãX®ŽÅ‘¶CØÓ`æüΰ8[¶xVD¬Ø·îÞ…_{Rö—kûV™\ ®OBò¤Þ7x6xVx6XO}¿'–l>=³úYê½gvYòÆÐ1nJ÷x™Þ³tûËü4…ÏZ·ïZlNŽU_º½)ô䬫æ^®û„¼i2±kÚiž]ÓÒÛ\�æ-¶mëW¤Xv§-ž!'G+/Ï.¯¶\W«êH[U"<:#jDd›ÿçW'¹lê¾;cÓU�m¦“ïoÙ×â .ÞY»LÒÁBN�eHŒÇWУ”;,Ùmõáõã³=)ï ªr_ë¡9/óî>oÝ>ûÞÒ–íJ¡+gÝ5÷º�ûßËm³fÆ[Ó8ôù]�æM`b,!Aëì7¶l®�,‰ÍŽ5…_ XÛqNN@Œ3¶F#rާa„uÞ-Ô¢j¯~¤�ÒsgEÇ g雘”y½mÂÖ¼ ÐØûiRbÞír3cF‰R³8*õnKv[}xýølŸ¯=LZñå ¥yOËüö†Z÷äZGí4ýà~qcnûJ¡»æ^áú*šÙí~KJTwÀíN ³ åõJqæ[\¦dqµˆrŸ�ÉØ5ó—B6 �óQúºÿÎ:_É/ ¾Þ�äýnüÜ„$¼\(ù ÕêÍä.!âí X“0cAΊo$¤†Á]FížÝmC¸nsÍ?�ÂPëž^ËcO´7æ¶�®š{Ü÷‘LÈéºHp¼Tø¤³, aÁŠïWÖ]å‰�%&1ùÑù¼û­’uÇ"s°1–ðƒ^¤÷κ¼»ÏLyÒ¡Õ–c~Qù{¥ ¯M±4– Ç>~ÁÒ¦êÑ~3Ë"÷μ>#€&¢ü�O¦0Ôº'×ò¿}äþ�p¯ýë…Ò.ºïï—^¨æ™¤uãËé­Xø9Å•) IµH�/ý‚ü¸²œ]ël…‰«Þ½£^ÎÔÄu¾rÞÈÉøÖã›®°ýwÖ7 �3jÝçúÂ�-±fÖLˆ(/ž‹ Ùß|&hJ¯xŸ}~µŠwZ½³Ä?W?úʪ|ŸeRs»§¿–½­{?‚ã~鎚{µ´û12ÉQÛhΪz/Ø„‡²mÝYüþ¤÷…Y03¥_š1o-žœk"R u^gð²ûÜüÖŠ‰ú-ybõëvžyÁ’g¹í+6sRSµrÝÁf]éÖŸ�•yw Ÿ²nUoûíÛéJêtë—úrÛ�³žš[‘Rö>6{>Rvõ EX÷6þ7¿ÍZ{úÅg;z<;vxVÌêO’£ÔB¢“Kz—7Í9»“~»ÖµšTgGEÍ™»v Äì�’ÚõÎL©Ýq“ïÎ;Û´ÎZu©Ä­¡µÝéõ[)»ŽÏög½ü'ç¶î«3Gã=–ijj[½×ºgv[²ÿÎzr±mßqWº„ý÷Ú‘Âg¬»ºt­Î;x?öÉI ]9뮹õÃ…2üj[�†ýC:­G@@ú>âûCóŸ “¸®|Ÿçû>$ríÜOæsðËíCL÷ÏíO·‰îêôqú¥…|eÞóöìæÝC®ì<Ém5UdàˆëàsŸ/ä“ÛŸLÆ–´¿ýÌû#g„ÇÓMLù˜uœ}æ_L/ Y i¼íÜç3­ÜŽ_ú·Ë3~ª…ÌŒ„¼Š°Ì½VÍ8”Œ«_½{»Ü·=üLñH~ªmUý¬‰±”,Yª"õŸæ£“ágŠG>еÔ�ÓG‹ï !! !$d!„„,„��…��…²âŽü?ؤ�"qèTA%tEXtdate:create2019-11-22T16:53:24+00:00$ë1%tEXtdate:modify2019-11-22T16:53:24+00:00U¶¡�tEXtpdf:VersionPDF-1.5S6ZÁIEND®B`‚scikit-hep-boost-histogram-b4c49b0/docs/_images/axis_integer.png000066400000000000000000000042451524660700500250370ustar00rootroot00000000000000‰PNG  IHDRð@Ž–2lgAMA±� üabKGDÿ‡�Ì¿ pHYs––qFþðtIMEã 5óÝN¥IDATxÚíœí‘㸆ŸÞÚpNàŠël^˜¸e'@…À � �“€]TT¤�€Ér¦Bhÿ¿¥á‡V¼šÙCëǨ8PÝ/Ðx¢!Š“ŸY¾88€�8€�8€�8€�8€�8€� åë¶ââpÔË> C¤¯{iÆàQð¦Õ.Ú¯²ƒö®K¤/›~¢>$DŠ‚O‰QžûÁ#&!GŸ­YQ0¤x †„ÿéÚã«GˆP‚=,P0”d³¡hHÙ}�Ivh¾Uˆw8i»$†º·äIÚãV”|'€cò­o™ƒ-}è)Å<7üèEÏ;NF¥x×z8ã‰}ªö 4“×n">›'Æ/›\TŒ ò?Û¨ÞùþœÎyTÑcìÓQ}Û‘E߯ö3á«¿é/ù±{€ÜHDÁq‡ññöÈïÖ³èÏ5^ç\åásÜ�G[,>/Ï_aˆ�µø�¸N(öXˆéY_9`HŸÍN€@Œ 8€%Æð²—v­8à“}ŒðÜ,™Äkúç]æ¸Â¡’íW(™ø[Êcô·µ!T|É$ܺ À—`•þUž4<_=ÿuYº0=Åʹ{O2æaY%À×C3¯qšÁ®³Rr ÃŽãsZµð\#ÓuK%1Õ*>íc·mUž+=˾«Ö}Ö‘ ¼@° €õâfÓŠç©Ös¿ì’ˆjØð¹˜¦—ú�Ï k–d»ðДS3 VŸ»”9µ;Ðb°zú(ÝzÀZÈ�T\ØÜ�V‚�5c!“ËÓ™nŠ×½xþVÄ�X<* æÌ÷]ºý•Ëø’mñͦ·Iú&g,'-ö0A_öëÉúm×m” ñ±ûøæQïl|]¨Õž¡ùs‹¯Uî…ÿO.`°°“�»úWDùÿÞ­†¿ð'öÛù3¿ò/ê�´ÿÂ_ù/ÿÙ­õvoÏ«ˆ"ÿào®§ÿ”òOý»(b~žw½NFRèE\ú¨#YNÀNÀNÀNÀŸXÄ<ó°ýZ]àß ^ŸœøiÚÍrnõ^'–Gå¼8a.䆧Xrê6Ä (AffH‚Å’¶¡ƒ%¦nS½ ®¹öX,vþ Pb,–€rMZø8dáao[‹ÁbÉ›–Ù>ןŒš°©-ïtÔdXBjj,é5Íû¶l§7GI H©vŒîC Ù9¤±(%JÝ'±¯ÒóÿÊW$ÝӲ˙ÏÞfÕ礃ç) †r:çNŽûLy¢5Yó·æ¾×ÚÛ9˜mÁƶùúÄÔÍÕ a?�Þ/Ûè½á(·wLìL. ±„”ä“®²ðü©Ê¢�åDD›pê8õâ!ž„|¼Qé9ç…9÷�PJNTTz|8çvmk †[mòIÕÓ ð~þ^Ùkù×~?/ÞÇöÊ9SnóÄ×Õ±>ÚNIH:Šï�é”I–އk%ßš0èÉ…£wæP÷²+|±z‚Q²Îû„Å4:zoýaæŽÛꥣch%gì`ÃBšÌZ€¡1–—«a’æs$å·…Í{¹b‰¥Ø#JBNz.œôû�Ž ™œ1ø]{¯ì;ãUÏrÁŽhVÑs_ñæÓ€$ý>ƒK5¿Õñe!lµc1X>ð.¾Ä# =ÞÞ�$娯T’,Էצè‘bØÉnêz|G7iγœ ú +I“6êQp àÌ·Á„r¿ìÜ4L&#Q·½s&ÂÝXæ÷£]¼qGY…ËÿJ†,ºá¥ VÁ’�cû<:¢RqÃ4-1J‰ÅÇ`I(ñ‚a;Ú³nÓ]M”tìˆ$�ëºjˆ®Æ>ÓZj,IK¢šgWÖžt$ËG›ØÑVÅMÙ ;÷&7€ÕcêuõM³N¶`´åß�bŒreìC’Eº|_ÙÜ¿jÊf¡á7 ۆݷhÔ”ðæö(E)‰)QR¢æI<ÐæLªnlÖ•)Ç—‹MëŸhðç[‹!kôú#­å¸6Bê�Öl°¨š”�´ ½ÃÑïù¦¾YI\ŸÛIgÐf¸™Q'[±o‡ºÏÜÝ{XðH†ck£®pº€ÂôËÈIÉúÎbiR–tÚeÀ�ì!ç7î²?°Sž®º±^Þ'Ç�¬»ŽÑÁ8ç,Ã~ÆÞ-fdý¤÷Œ�;6;ϲ#bІ¹^õ“¥Ï:€—�ø.¼íu‰²ØÉƒâÎd9€�8€�8€�8€�8€�8€�Œäÿ9b ˆG¤›%tEXtdate:create2019-11-22T16:53:24+00:00$ë1%tEXtdate:modify2019-11-22T16:53:24+00:00U¶¡�tEXtpdf:VersionPDF-1.5S6ZÁIEND®B`‚scikit-hep-boost-histogram-b4c49b0/docs/_images/axis_regular.png000066400000000000000000000042441524660700500250420ustar00rootroot00000000000000‰PNG  IHDR LÐvZ¹gAMA±� üabKGDÿ‡�Ì¿ pHYs––qFþðtIMEã 5óÝN¤IDATxÚí�Ý•³6†ŸÉÙ”øÒ@)—€s‘{¶¶\”€K€r%˜L9G¹0Æ`óëÝMöcçåÃXÒH/£‘#±(÷øE«@¡´P(-J …ÒB¡´P(-_/ã·$Àà`ë­©=­›¤äT”¸¸Tö¸9í ¡}›±#1¡Å‚Ë cÙÒ1§[ç4Ú˜æ1vFnärÀ©=�ˆ7U5³ºQál‹ ž…èYZ¤dí¹�Ý’½˜×ív›Ç<-Æ\NŸ²=¯wC]ë–uûL—SÌÃ%�|3îÖÝÄÃÅP“lÏÝ~~€ºåçÇ]$ãØƒ}£¦Gi¡¸`o›P+-—Aû­SÉñ¾£½øe¦Zº®ÙÖš|D7ñÄë]ø†´›å¬é»få†tžÓ-Å :±óŒxÔ6ß�΃º‰CÝŒ:ÊÎÈÄÙ˜îïô-¼v(ç’lJçÝÄáDÑZ�¼Ìz¶Ùðõîݱ%¾àóJMˆ±¯«’ÝÄPPÚ}s?¢¢Â’ϼRúùtÏCIM9¦�Œ¯ü‡£-7ø¬Ìè&.>¹­¾§o!úA€Bç-J …ÒB¡´P(-_‹I¶bPãJ&ÑšÙÉdÅ+|ÉV—&X!­.ÍZ]W–æÿ¬ù—Éÿ»x+(fVIƒƒ‡Y!¿.uƒ·êMŽû©¥qž(ÍÿXóÚ‰(” ¥…Bi¡PZ(” ¥…Bi¡PZ(~"¼Ì ¬˜„ýø}…üoÀAÎkŠ»²4®˜Îþ}uiÖ꺲4Ÿ\óÓõ<µ:K‚ïùMÕ7Àßü5µ qfÑžxZƒ›D5½JU×r*ÔåT(-J …ÒB¡´øù!æk�èæK£´øüFp)ˆ¾PyR)dÝ¢= %{\:|õã*Nb1Ïñ^²æˆÖ,¡]�rؤ¿›9»»«±øu�J!ñ’Ø;kds³?€lªÆ­EM‰‡¿èêG!$àY3[á5�˜ã¹g`jJJ*‚wv1¥}íÇ럠ÚM< Ç¡˜{øÖÈçfkvÒ‰?�Äp�–^ý Ø²ÁÓÿõ°xM¨æóGǾ¥þäÿCl?0.ç~]bn%'›»Fv8·%š} Z|TÃq^EFóé´8Ý•”ÿŽÁ-–0îtŽkd‡s»Å*&]Å×H*V¬¤}3%±X±’ÍÄR1’‰+VŠk¯&ns%9‹+'·9·}S-¡œÚŠÅÛtãIÔ¤Ýñ[ÄmJKÆ™³ø�F~[Ât�f­^¡¸M�e¿Â¹k÷ö�‡=<òk7cKªÉu�ìpnWäãNÁ-`LJ¬ãØ„8ìÙãÎxÖ. ;vT¤MƒWì8P“ °g¶dÏŽ½ÏqÄ'âÈŽ{̲à±b$$ ­ÉxcÇ�¬õÁ3`Ç+>.;ŽÀ�— ®;’™Oˆ5«Ø±£Ä§ö;åu`Qôãþ§EÕ¤Ækd§QN UG:‘¬5´­Ä^#ëÏöi^k芮©"%Æpº÷%îŒjDÑé�‹Y3=âž©u;þµ“¹”‹ Ûçßr'êkö؉ kF6Ø�†\¿Ó·ÿk²v×ÈÎ8äË8“.>« ¹ÆÎ:¶^ú l.Ž\œžô+¹�Ò–ȉ#•=,jöF‰C€'¦1°>%ÝI$·){Õܯ‡'B �jV Ä¡z.Ä’óI²:ËùDøA1’r"À¡ìSÈÖpçÚVü Á%$“³„óæÐæ6a�Óé-]²öè˜Ü†*þs�Ç5ü´Z¸b%ï5¯™¤ÓÙ9Ol4ÞêË‚~o="¯ÇyÏàâeßžS‰›P‡’WJr~ŒtcsFy\³aM&˜Ö&†Ô'ô]#;i÷¦ZvÌZ8sÄb8€88€Ïæâa0âu~Ex8컌–Š ðð¨Ä£¦ÂÅÇp2©(m.·qÆÅ([þ¦r <\Þîž×6¯æÿ®\ÊR!�ØD\RIÈŸ ‘¯q©ÉGè,[«pÄ<|Ê‹f�Æ—Ô/¾C5¨Ù?¼@S#�‹áøu½Ž&ÝFß Ål%B©m"!ý9Ñ�€_;6q±ìpníìk=ñÀz®gbNX,ÙÅŸ'moíYØõ¸ûc ¢æäDÄ KJxÛ®MÍÅ¿Ëú:bÈZ™ÓåÚp^mªaç—ßJžú)pî$•uÆ*WÉøšZGãÞfvše=©tpú)è�ź·Í]��Æh—ëÞÀµ²£¹Å3“fÛ…Ëßv§G(‡x͜軶Ì[4?Œ¹ ~ïþ~�ìàܧ�~ݰé­ÚÞOÊ{ÿÑ}úôÛ¢ˆó²—Ke1ÄW;©´xxƒ@p�ÜúÒû–f·ÎúãdçŽoõA€„DmüÛMP ÕÔ÷úNDüfÏÂò»îZ¨´P|Úä·Bi¡P(-J …ÒB¡´P(-ÿþWÔ´:ë„ãIÙ¯ûc‹OÝ:àuVºòïéw¨—�”ÿ乿Œ—µU… @ÏÅ:²çæ†ÇÆGÏR\[ÉX^ù ß£´Ô�§âå‹Õ⋾YIR!$ÃÇ{vArll<*Òö8!Äá(È–‹ø–~—ºBLˆcGñjµr…ŒŒ#J±å‚#[þM|>Z>ßL�-o-wSô%`Ç�Ür˜+ïŸýJÒ`yLƒÁÓ`Ši0˜bL1 SLƒáiŠ)*_¦j,n[nDÿroÛÏÕVLƒ™rƒÁÓ`Ši0˜bL1 SLƒÁÓ`Ši0˜b^W–ìù×…Êw^1Ù |Ql|gmI3˜�i0˜bL1 SLƒ)¦ÁðYø×DðY� »œx—Í_e¸Î>¡ewCqq“3Tû|ÎÖTU—9âö°z™fUÇöwu¾¸¿•Ù�)—H2É$˜söaÏU ɺ}ÐÅI&™¤â�ø’J&™¸­x¸+›L’%£‘‘ß&©UTTôG‡«T IÖn–-�$’I!™DkÖôF¢ æ'Éewf=èÿTT®ç©~Ù=yßú˜5%ŽÝ¬³�BDøíV.í j‡•›jÏáa\6%Â�DB©¦ºeˆ£r|Š5‹‚ød”ä”D+~!zÚ±®l>3ä*;Â9»3kÍ/<Ò…¦|¸âv}ñW_ë.f‚pýv¢sy“B—�IzÛÌïº Çv÷ÍŒlåöªn9kæ¤s’Ï0�ûÔ¿DT®õ©ùȪks.  ‚¾‹ý,#œÕñw‡Ç¹Ãín…‹²gßnâ½ý>�Ž?‹î\N¸4]äI***iß„H"**Ù}oL<ÉNÎ’g�C‚æL r• $h޵/v‰¤j)3Ì[ãáÎ{Ÿßå ’ ™]ÖœàaˆÂ}Ù È!n(÷¼W ðg5 täçÇLKË�­õ­}P#êm4èoý¹°v~Ç9Ýÿ:m¶j>ž�Š’±cw11wL@‚Ñž—k<1ÇsïVv§}Êq8\ß°C‰q8vTS}^‰NßÀïtÔãÀQP ¤ìH9¶ß¾åa€Â¸l† ÇfÃë^üÍnØá¸1åÇ?«ŒyÓ;¦@—»K8”Œ åH2ÏIšoÊïó±“Yggñ¤=[µÝº³É)ymƒä´s>%Á£ºÒ•RÄ—I4µu?Áiz}s”ƒlÔK¼:?LaX6·B&DÛ®Û}¬¥ò�böe’¬TÌ Ù3ý¸¢+»C©q„TS žÇ*æX‚½<ù j=‡�6�ôÄ—�¿÷í?däþÜ;¡T|pà û©¤ZJ�#ï{˜£ÀAßæù@#†esë!–xç h¥·ÎÛ[™0Òò”¬‘˜T~/4µ%{ö'™JNE¸¬ì¶%É5ñ$¥"Ä¿N2hÍž`RÕüà�€ˆLŽ32p'/Ó]x瀙=}îR˜'›ÓZuúôŒ^EäÞ¬ºÊèý]ø¢oä,Ìev}T=�?¼ƒ²7~7¦K’k˜‰qüjÚ2W™µ˜=)?'ÒÏî´RŠGH,åD«$¿3ÉQf¯X÷(Ì‘M͇þY‰ëîCtgVaGn‹³’ölS¹T1%…á9<ŽÃXåŸQCt®ßî¦"�Ľ›çó>¢J){}ã0QEØŸjZëž²«7c�W®¨·‚�s0ßtŽQ˜'›œÝe]”Dº‰‘�Y¥Šw¼ÖZDÔ|LHáöì®—ìv‹)—5[üþÃ4|ý2»zW~£QÒ�<ñÏQª‚#¡Àu"Ò íq�:b” G€‡#¡ÂoœÞ×FÊ—Økû–‚» ^ÒŽ�{‘ÉïÄ­Ã<ÜP¸#GtšÓ…2 Ç&·�tƒŸ¦VO?gš„|xñë9§0.RŽ„ø )Çs06—1JŒ§œ2 ÝàçöúF�g$ØSªñpjøä‘„ªI ‘ó?Ãö(êM |!¡"¦BI‰š3q‡ZÀîjè]suÖ~§êG“·cµüŽKÒ^ú憇! “²igØR¨†æÐÄìá”b¶<‡¾{{³ïÃù5Š¢«–s)t®¿JÝ^%ï¾b’ܯ*½Ê»:ÞÒrÛ§s>J11q·û$Þ%muuýqvÂÈÝ@çS¸X‘¥×79ÔøV I§òª/ò>¦Ö|³7õ]W¹œ ñ ëû3�ˆpn04ÔTp …!O{öõA;·Cïꚟ÷[%ÚÏw¿!Ä'!×ýçQø{&G0Å4|EØo~ ¦˜ƒ)¦ÁÓ`0Å4˜b ¦˜Cÿlÿ(9ÙT9 %tEXtdate:create2019-11-22T16:53:25+00:00‚œ…%tEXtdate:modify2019-11-22T16:53:25+00:00óÁª9tEXtpdf:VersionPDF-1.5S6ZÁIEND®B`‚scikit-hep-boost-histogram-b4c49b0/docs/_images/banner.gif000066400000000000000000002652231524660700500236110ustar00rootroot00000000000000GIF89a±÷'''777GGGXXXhhhwwwI¥wSª~44²HHºSS¾^^ÂhhÆxxÌY­ƒ_°‡d³‹n¸’x¼šj¦µt¬¹}±¾‡‡‡———€³¿§§§···†Ã¥‘É­šÍ³¦Ó¼��χ‡ÑššÙˆ¸Ã’¾È¥¥Ý®®à¸¸äšÃÌ ÆÏ®×¸ÜɧËÓ¶ÓÚÇÇÇÀÙß×××ÇãÕÒéÝÈÈêÅÝâÔÔîÛÛñÍáæØèëçèèæòìåìóêóôÿÿÿ888xxxJ¦xT«IIº\\Áf´�m·’v»˜u¬ºÀŸ˜˜˜’É­™Í³ŸÐ·¥Ò»ˆˆÒ™™Ø‰¸Ä­ÖÁ¶ÛȧÊÓÇÇêÆÝâçèéåòëêòô%%%555EEEfffvvvTT¾[[ÁiiÆww˹¹¹€€Ï‰‰Ò––×··ãÈÈÈØØØÉÉëÜÜñéééééö&&&JJºww̆††——×ÓÓîæææHHHWWWggg]]ÁY­‚c²Šl·‘k§µ¨¨¨¶¶¶‡Ä¥ŸÏ·¤Ò»˜˜Ø¦¦Ý¼ÞͶÔÚÆÆÆÒèÝÉÉêÜÜòØçëâñêæìô!ÿ NETSCAPE2.0!ù,ÿ!þ Created with ezgif.com GIF maker,±ÿ@ŸpH,�ȤrÉl:ŸÐ¨tJ­Z¯Ø¬vËíz¿à°xL.›Ïè´zÍn»ßð¸|N¯Ûïø¼~Ïïûÿ€�‚ƒ„…†‡ˆ‰Š‹Œ�Ž��‘’“”•–—˜™š›œ�žŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýV=6Nœ°ÑÃHç ôǰ¡ÃV'(˜8ÑD‘ DÌA¡€ÁÃ� Cr1qÁƒ„ BR£ŽEÊœIS�‰#†ô1Å�ÿ–;ÖJ´è Aê`ˆ‰åœj´ªÕ«j&Ò0ÂQAž©1±ŠKK�‰7 Ž‘Ð Cܽ1â�[‰Øa7Š„zßAÁ÷�_ÀDÀ"á1"Ä]< „»EÞ!4øâMÒƒðÉ'Þ°ŒÄF]Ã#"ûèìóÈ�§�U—�M;–ÄÖJd‚Q„ ŠD‚—…íj\AÎ"Š‹Ü@Ú\¸“ÞI~7`i\©)�[ÔnÄuã"N4r£¸qëµãË?…4l B02˜(B`q=P÷À „¥”\o$)ÐÀ(œ0Â~ 8€LEÜ4T(4FnJàwÿyBü–a8QÜ#8‚€¿5Es0¸âD XX!E1fè^róõè£&7ÐÈãE»)WRZCܰß~ Èæ (HF)q(O¾H•6Lä’Cð@˜I`‡D‹CHôœ<ÙW\zE  ‘DÛÅE#™>Ü%„zyéä“cþ(è •LEQCúà!F ia£åiÅExbVÑÑ ±_�GD¹f~(åjh…éÀ u•P‰Ä’HD¹j‚«AÒ§BPíD¸q¤J„úݨ>LtB±*!"§]UjçzÓ*ðÿ§µ’öêî»… àÞ­“ºTl–G0…„nö«ÄµyIëƒnÞÝ—ÔÚ¢H-`¥­*`.›AðÅY|pnÃëñÇ}ìäe½“®tFARÁ çëÜ´aI• &Œ@óÍ4ëfŸ³Än+kÄ †–ØqL�Ä©šKˆ8Ä~3ãì´Î G-5"æjËF¦Ì]Á$ »o‘™6'6EÐ&ÁovÜæéÞ߆˜6ô®úóЃ�”Ö{¸ò‚/Ï5µJt)1ÌCð$+ RúÛ¹¢Oĥׂ~ĵ²½DJáã›DƒG¯ÿþW=ës×Óž‡…;#Ø S[ ’f©ï)ÁB®»V�Ô,†½l`Çq�ŠÃ)Î)´‚( å�(T�÷g($�g–ÛÚ¤.Å3l)nKŠšˆùˆp‰Ä Áž‘ð%¾"ÜF„^*ž¡ÒVœ6�y9\€EèÃZ…oZÇhw/!fm€9ŒR„œ¤§!Ð> `z`(Ì�/#kÔ �¸dF6qÿO†rxPœãYKŒlÑ –”¦@"f\WL¤þz F9 %Ò˜½<¤²þ¤c½¡AJà€GR¤‚ÂyÐÉàïk^*¥‚– 8B $['‰4ý18h¤ n²3 –D•:¥"‡)5‘Ç}9ìWà.ùÃ/i%|lÎ*ÊÁ4r" @fÀcœô€s"*I3W�´pSrÕV¦âFÅ]³#Ú$¦<÷Æ ¡ î9…zîó„L�e„¹šÑd n±Œód³05ˆ`BYêI …Îó¢•Ö>�´àIdœ ©Hµ°QO`iŠÃZAGÊÒ–6�’ÿ£ ö¦CC—Úô¦MHœ)ÎÀ“¤¤8 ªPMà †é J…ÊÔ¦¦ÂŸuªT§JÕªZõªXͪV·ÊÕ®zõ«` «XÇJÖ²šõ¬hM«Z×ÊÖ¶ºõ­p�«\çJ׺Úõ®xÍ«^÷Ê×¾úõ¯€ ¬`KØÂö°ˆM¬bËØÆ:ö±��¬d'KÙÊZö²˜Í¬f7ËÙÎzö³  ­hGKÚÒšö´¨M­jWËÚÖºöµ°�­lgKÛÚÚö¶¸Í­nwËÛÞúö·À ®p‡KÜâ÷¸ÈM®r—ËÜæ:÷¹Ð�®t§KÝêZ÷ºØÍ®v·ËÝîz÷»à ¯xÇKÞòš÷¼èMÿ¯z×ËÞöº÷½ð�¯|çKßúÖãýÐA ì+׿—(ð €OȺ!`+¡Áü=†þû ì× 1ØÀì`á+èàxÂl…C^B†7ábô +¥Œa˘ÆV±ô{…#6NB Üb‘€X²|Œ›�ÉÃ2¤\ä`dXÖAt€å ðA @€ ¡XFòl€0�dnó°Œ-ëàË•`€ÐÙÌR�r @0oÐ…®3àLÚc>ðb€d=c€Ï~Æò££âÚ8>3�ÿŒ.Ô/вöl:c`ÓD`´£‘àéB›ùY®1PiÀB 0 àlzØs�ìë žfó³‰é£8ÐFtò\)o ÎÓ®²-®|éJëÀ8 îd€À/š ðå2g¸x¶¼�{ç»WÀ‡Àë_¯úÒ­þó€ o ¼Îñ.Â��‡OÜÜg>p¿×üo"l É~�†�ìV»ãPÀ�ì PÜtÖñb€¼àá ðÁù|r#è@ã"Ç÷´/]q_˜Šö® P˜@°_0GSºÑÂ>@º3àÿr ê�‚�°qÀÚùȱíò¢ 8ò À�D=Üâ¶Å²�@p] Ò0€ž x"d€ÈB wRl„ª ت”«@óDÞðÌá»ö!làAöAć0a#L¾ @¼˜enÖ¡ÕE@¨Oo„Ï{<ÈìF º¹íqô]—?Bç}0a£[ÕG¾B `„ÑŸäN¨üåáMm�ºäMÏ».öÞü„èÀÆßÖ3ó`ªÁØÍ'€0ih�öB¾ž ¯ôÐ'~ÓHï¶éCM׋>øCàC``݆+É—æW{�&{Íçf±¦rP@ÁFeÿhGj³vþ—ÔÖ~ábz:0~Ú‡ ÜWà}"~«–}çfXv*H‡ì4÷fÙ÷ªGz"ø‚FƒÄç{ÖoGPƒœÖ|xç8o×hW„üG>¸xô'€8¾¶¾6°m΂Cƒø=�øVuèæsBHvØ7hY(JF`8‚$ˆrBp‚À~ôxsÆmà€D ƒ�—„’…6(‚ƒHdp†N6†‹hvWp}‡˜þ'm¨„Kðbß&;èFè�ˆ…ˆg…‚(qHƒööa.yàGjd!¸l1v}¤7‰5hÿŠ(l6F�À÷‹v8 %HzH|)؇Äè X�sf‰Îhˆ¢ˆˆPà_G�&ŒC@ˆB„(�t((7ŠO�‰›¸„«a@—˜à¸j¤öŒÈ˜Š˜Ç‚?ŠHz|È„g§WL‚x¨bºXr¤Æ�uXŒµpŒC�Œß‡…}HxE@€À'«ñx…¨ˆaÀ{Icpv¶G|Ù)�ì’¢X�R�‰I¸„¹x;Ø‘G ‚Á–{R,y�Iˆ[ö…)h�ÿ˜–×mЧ�+iƒÙ|46‡¤§Ž Ù ™‡(†}XoGI©g3ø}☒H€îÈeÿ›(d\è€?çŽ4÷h=@f‹G–?w}w7ˆR'qgib)×|éˆþµiß—ŠfI‡I·jt©nIq"WhBY«˜�±&_Ø�BPŠKp…^çzwÙ�yy�k©XŒE€’U ª ª©ša†“¬‰dv#ÆbL¹…˜‰yé&`hx«ÙšYš¾ÇtЋPPœ–G‰¼ù“@&¼yÕ»Yœ©øm^iøFŽEÈg^iœ ¬ÉmÒÉš…‡“­ ¬©š�×hÎIf>¹�Äé�X8�½Ùm„†~S(z¿ÙšÓfΙšðsÀ|ÇšZ6bDövâ<\hÅI`ä©•ÿ^IœÚyœé¶oÿùžžžPiZÇšGyšV¶ 3·ÖøÆi‰¢&*L”ö•aÔHi&ª)º¢h `¢iy£6J„Nð˜6ÈcY r'*‡;š¤nØ¢"š L9 G¤;J¥Uº£Ô86ù¤‰¥Â@‰”¦b¦!:`\ŠB?'©·�eÁ–š€£i =7w§´ña7g¤uÚ§~ú§€¨‚:¨„Z¨†z¨ˆš¨Šº¨ŒÚ¨Žú¨�©’:©”Z©–z©˜š©šº©œÚ©žú© ª¢:ª¤Zª¦zª¨šªªºª¬Úª®úª°«²:«´Z«¶z«¸š«ºº«ÿ¼Ú«¾ú«À¬Âú^5ÅJ =À§Ã:O$ ¬^Ð)@.9P$�OP5°¬8Õ°á.@€F@$ P�)��Ïê9PÅ5 ­Þ =5 >-0$P)P-�ó Í*ЮW°°ûú<%€®% ¯Q`òºGЯRа�À$€±ªcB0²P®>`€‘­*ЭaâÓšèj9²Oâ:[³DP-@)P­U�K*`±ä°2Kk8 ³.€´N;­%P­Ë.е-àÅÓKÛ_ëØZ­ÿ`µC�µB ¶A[šK[µE ³`[M[®BK²åà°·°·€±P€;±B�µ> €Qt‹9àp·i+´õJ­N²GË8Pã*댫·÷ŠAK­9p³8KOë³G´8Ð8 µMë�«®P«³Gà¸QÛ¸ µº[ºÓj·€¡³ P8`wÛF[¬50°D ·L{³*@ºÀ³xK¸ÛµSÐÝj¸*ë(k³§;4û$ÔÊ*ð'*à¯XÀ½Rpå+¾I�°Fàî;$ÀºB�ü{-0»BP¯Ñ[E�½Ë*À´ÛZÜ*ÿ8`¯B¿ÙK KЪ¿Úš±ÙJ»ó[8�¿Cð¬Ïë:ëk›k/;&ëÀI0¾D »GFÀ²J�¹­«¯ k³P-z+µÿ’Â×¹\ÁâÐ¥Û»É[ºRÐÂâ‹¿… ¹9P½@·û[šPN\5𸷠µ[ü50­´öû8¼GìzC<$°À+|a\ÃÅs¾>à½ê{v<- ¹L{°Ñ;»)`±slÄÜÐÇPÅ`;,æj¾<‹­ÅŠÄÉû'É‹� ÀAûÅ:û¿L®4›¬¼;¸Û³"½�û>�‹ÀØë‚û/…¼²ª±Ñ;ÃEpÆÿqû¸9p8�¾æKÈ:ñÁ†Ü *€®{ {f€áÄmëÂqk¶ðÊw ¯YpRÛÆQP± ÊýKÍIPa|ÊHp³GÀkìœË«,³ü¯¼ÎÂÅ,-üÎKpÌHàÄs ÌÍ´+ÁD¸lÚl¾íÜslËE0ËáKæLÉ|=l'<Á©\ ÂŒ=ÐÖRϸÜÖ|ÏÞ°Ã-! ÁÝ 1æ*À'½s¬¼qKÌ7\¾8 ×+=0Ç0|f³ÁHË€Kp&4]½½±¹<È�»±CMÎ銽ÓZ¾î:x É¡‹ÒÓ°½ ½ÿõÊ·€¡~;ÃZ ´ ,­«¿�ÎH½·å ²�)àÓbËÒwM°}ûÑNà׫½·úZ~»ØY¬Ê Œ×@Ô\¯e¬Å$pÒC€Ö+¿Õ2·Ôš¬å«ØŒÝب[@›È8 ÚÚzØîë×0»·_ ÖÐpÁ±]° ÄT€¹8PÛl̸u Û…¾ÍŒË§>;°ûº²úŠÛù€ÍË]\ÒJÓÑÍ[TûÆÞ\ݺUº9ÀÛÚýÝàÞâ=Þä]Þæ}Þè�Þê½ÞìMLÚÞ)ar ßI@ßgà_ï 5 ßÕ°bsàß?ŠSp\8Au\xvøm;Z ÿ8I§ü­ RÜ8sh*?—‘%Y€%ö“� `n î˜.™›Y€$º >âÔàmd6i&žm÷‰¸¢dÖi~~('g†GgiY†ÅÖo;i`Hçj€™ãÅSkè·kªÆe>¨l°¶t¶f-]Fhai!Jà2Þßê¶lv÷™(z¥;ºD ¤Ñ7’Œ qq7æ�)¤Wn j‹pæ� ä®âWt·˜E×`Hç}XbgÊuf×�êÖopJ\–n6×nïæ|&âaÞ þu}+:zsDJêu—¥œ÷•ª˜Œbf}½7mv‹úµoGYˆêÿDàgÉéz¶ƒ4yÔדöØ„§~:¡šˆŒÜö'.~“ÍÞéÒ �Íh1þ~¼GíS¦”9lj�‰ç’/�ì¤|ù)|è‡B0ƒù“=àêO0y"8“hçŽÝ¨.íÆpâ8)ê·øï?.‡¸Š I•õäµî’%']ÈkѨÞØm"h…îŽð[èœ÷lä¾�ñˆy0ðú. Ühš×˜(Ÿò*ŸßrØŒR™€S¹í �¶®b@àdé켔€G�™'ð§éo™EÀìù>òQ‚Õ^Øh^zâ&oç(‡çDp•g&õ¡wî݆¦H�ÿ9ìêˆëD ï<¨•3ç~LÏ ßþõ“Î÷öGðtGó3äHÀêÏW‘/îŒxX’ìvyèFöI ä<ˆc)‚�(|;ßöÄ@õOO@æn¤7šr¸ƒU_à‰ïzÿ~àît.œLˆ�õ·FjNmõIŠ]Oqæ˜�ªÇnp‡ŒÞ‡ô'à�_ À¡ö¡«oȉ¡Ë™õ¾/ÀvÀš�Æ¡pàjc©¹šè©š›ænÀt½_Õ¢uL ÁלÏ9s÷ifùÙûÎoý¯çœÛ/ Ì›J|Šv€¦Å·û!‘a0z=��X4‘I%ñµ.uNlé‹I{ÿUã!£UF9Yï˜h(�ÑiõšÝv¿áqùœ^·ßñyýžß÷ÿ ù8 “b†!#%')+-/135791*3;IKMOQSUWY[]_µ8faqsuwy{}�ƒSu^Šo…‘“•—™›�Ÿ¡£¥§©«­¯±³µ·¹»½¿ÁÃÅÇÉËÍÏÑÓÕ×ÙÛÝßáãåçéëíïñóõ÷ùûýÿ8�`Aƒ&T¸�á3TF”8‘b°h¹˜ñÒÍ Ì"1ІIv€˜P¥‡‡ :ˆÙÓáe‡|z°è0cÉ�c�…c¨*D�Zò¸§è§N l€ÿ#Æ‹C‚t IDÆÈD2DÄÞ‘ÂGÎ7ñì S‚Œ% Õ»—]Œ¤KbÀô�Oà T¯Ì’T` gŽìÈA$ÁŒ|ø ‡…Ì›ñôà飃]%1ü仚u7¿^^?+pÒˆ ‘fBM[I‚´E"Œ&²£rÂ}ì@ЧtŒ­¡GÏö:FÈÒö±!ä�­j^˜eà» ç!r8€¡K‘çÏ ‰Q‡Ô5W�XEž@ïIt� t0*b@¯ˆ ÎãíÆ–xƒˆ*‰ Ö"Àëzú+‰â’Xî àºCD#HäƒÓ”à€éÿ`ŒñÃÈà ÀÁ8€±øŽ 0šbì#BøÏ  ="Þ;ï#%6�’ª ¸R �Ȳ"r4IÂ’$‚0¸ñ$ÙDüdÊ;‚Ç“%¯#Î ò6¡ü 6µð % � 9:v`ôGùh® 4eÌTÓ`à˜5�@�#"Ä”#�øDŒA…,"€%è,"+6r[0È03BHÒáÖ"^ ×"À,‚ %ŠõBÕ|è‘WY Ï•Èa¥#:`k‘°To•ÀÐMÉ-וÝ�°­ˆØdEUŽWSõtÍV]ý�ˆõ¤µ¿‹ÿjùîÅ#d1ö¶#BzÐAŽM"Y-FJbìÕS«Díå[ë˜áZ#<€¡RŒÖÜ‘I.e‘$ \×Ã]ÛÓ)ÛàE"ž½ j1`fǸN 8øTøˆÆÀKÞnL"N‰…�€ª7“n–^€SKT‰H»y®'õÚp“0ÀÃ’ÑN›’SIJ;]ÝMc׫ÊÀË#Ž\V‹˜¿xAÌFM£ØSwd³høŠ@‰ Dö¡Vcï4j¦8Œ˜#«àê¨'®Ä#zøœ &»#t$RœIlÉÏVÛõ×AíÂQÙU2n4Ô=‚ï—=róݵ`Ú16±ÿ€OuàÚ–ö¡xÈ‘~»Æ�oUó1²’ÞE?,‘ˆu•bs/Þ:üu¥àY�lö÷á/ €å‹ˆÕŠ•oÏOg‡¿“ýˆ™á»%$o;�+ýÖå4<l\ªÂ®¶â¢Æ%«ˆ±¤§@@�d‘À=$x4D¸Li½Ál) 0ƒ„.e-dCÒ?îp~À”t@€qÕnƒÉëAT¨šÜá»ã’Xæ¥f hùùÕ Â2í`GäMj(‘¹¨Mê©\ó&¤²0*LzŒ‹šªx¨ñ«uÛ[qF³ƒã8 uD‡³Ç#de~s°ÿ!çÔÇCE.R €§ €‹|PrHŒã,¹EàxHÐÁË8pòOPá%‘¸Â"€•œdå¹C€ Q(Úc–¥SÞÉ#cZ’°Ì€”‡H|õ "€„† e+_‰)2I2”¶eܨhÉWºo��ËLÎp 9Hæ"6BVM æœ@:“à¢A/g8%…En2’ŸýÔQ´§†0Æ$»Ã‹9zárXó˜�Ð7\Á¡l0fð(#ô’½ .˜Ô,ÔŸ%5i>`@N &‘p0eKé€üí/'µéMû,˜*¥¢oÀ>ÿñ�•-êÁ;ÅéQ‘*ŽÀТfÖ i“U©R¤*/0êT±šU­n•«]õêWÁV±Ž•¬e5ëYÑšVµ®•­muë[áW¹Î•®uµë]ñšW½î•¯}]HžêWÁ–/G"ìa £Z&–±�Ê'YÉ2$+ìœìe1»!f–³�ÕÇ›<ZÑÆc)£5íiÏá‘«¢–µ­}†Ý\[ÙZcN³µím›±YÜî–·½8âP{\áž‚ŠÃ5îqM!%ä.—¹J™is¡Ý@´IºÕµîu±›]ín—»ÝõîwÁ^ñŽ—¼å5ïyÑ›^õ®—½íuï{á_ùΗ¾õµï}ñ›ÿ_ýî—¿ýõï`˜À6ð�œ`/˜Á vðƒ!a O˜Â¶ð…1œa o˜Ãöð‡Ab�˜Ä%6ñ‰Qœb¯˜Å-vñ‹acϘÆ5¶ñ�qœcï˜Ç=ž/xàƒ'DžDŒ,‰ÒùøÄÈ‚“ûà ,%  \°„+— M R@‰T Éih TÀä÷àmi³zpe ÌY¸ .�f$¤ ÌJèAx ‚,+�<ó*Pƒ%è9R^B –œ¸ Ï¼`´šG[ƒyÓkèA ZÐQ#ùÓƒ�—‰@‚H'!%˜Ãª�€ÿ ÉZ.3\`j]TÓ§M|PX“AØ{v¦¬tz¹®D¼Pl^8»×£…¶®Í†dÁÕFX6µY¢/üú “Îõ±��ë ¤ ×-pºÑ�ëw!ßöv»SïT Ý¹þÍ”spæ › “æs ’Üy‡š8h·¢½­0ÛþV8˜=vïu»;×Çu À\QW›¯.¨ÀÉQ~r‚/�ÍE`6èÚýæ*ÔàÒJ€²êí4'!×ü–· V¾„\ÀI¸t-ïlÓÛVþ¹®©t�üš5¨@З sH×;ÑE˜tÜ]‡ƒÝ8 : k°ît'šÿ@6{ÒMþtX÷À-ÈAjP‚ Pœäx ;¸Õ€ƒðàÏ2Wͽ �/aéoPAÆ‘ÐqÃ!³¦R–�º/!‹OºŽù—“!£ïAÄÒö#ÀàÒ5 AZ.ïÓw›90¼›£MqÔ÷�¯¯÷�P-t ØÙ5GBص@‚[Aðs¾p`ü5ðÀòV/ÝÍ��ƒJ#ûJ@>æ¡î…l¿ä½ZîžÛñE¨üxpûæk�èðç}^ÿ<æäÀûgƒò‰ < TÍ ¶-ë¯ Ï¾Ï ¨�,€mþp-!Ð ÐJ€ïè�ãžËèÍòˆÿ@Úˆ JàérM¶ð„¬æXðó,ÀØ�ïo®ÎÌÎJà"�üÈ Û°Î°ZDp H°Ü†ð2ð âÌ\@в¬5�àxM Ђý|] -v¯�é´è¢ð —OóŽ,à k0®ÈÍü¦ï«à•Í� Ý ó”`Ùà§Œæ–P ðü´€ïÎÖOË&#ïð ñ0 ‰€ÇMúÚ0®’MÇ@ÓРýª@hPýæÀ MÙ ÎÍ*¯ 5oò’•@ÅŽöÐ` ÉÔ‚�ïØÍ ÒÏ/þ´@ÉÐ+±­Ð× óÐ@«À÷ªÿ úæÀ ­ñjàY®¶Í©ŒƒM×&í/Ý„ñðhÑý�‘ùÚNúv`ïЯ.�kïy !�»/í®pÑÊÙ^p L.åx�. åRnÉx@ QûTÀ O.‘ SÎNS J 4 øî =ìŽ`"êÆ ÌÔå:�ÿNî•­D2°-¬1bøoÝr°þ¨"+@æ<ò'²"I T@,NÐ*²ˆ2#ñѬ¯�pÈ&æÑr~Ð œÐà ê r rÒ�2)ͪt®R,Íò,Ñ2-Õr-Ù²-Ýò-á2.år.é².íÿò.ñ2/õr/ù²/ýrã/ýr#î`B Àœd£îFODB¥ÈD$žÊ'¾@$€k \$@[ ¡+ðiua: uá 35ê˜f�^D07)ê zD 8ÀFg1Ñï`ó4ª?¼À°NƒW¬ª×b8& „ áH‡f 3#á"@3Ãò(À² 95ªz3 �Ä55…ˆà€;jŽÞ¦ &' Ö¨mÞà\¨  ,ódô>€¦3šÓÀ†�–`f¼3SÊS4È ÀóizʨȠ>i=‘€=+á|a"€>`.&zXn±þF^#–È£ 0ÿ@<®>Ž¡JB6üƒÆ GŽâ¹ÖÓB'! à:F%�ldÀ0´'¨gCWÃ/|d ¨(°Šàp t�´ ²(E×€8¼Çz žR3Ddfà>í�Ó$À3ø3BhÈG[CT6ªGÉ`@“`žTÆ ÎS1ÿIJÓ R²t$ œ<óz`F;&tÔJˇH(Ÿœt pM¡ƒˆX¦ Ü à”šTrz hêtz .(aXÀSa&  •4Nµdà+T¢r@Q1uo@ÈQ…"@7ÈEÇ`RuEå´§rdSã S5³v ªT †¨ÿDt>s>À.d@YU ~ŠVYRqÕ tÕ*õ~zÕ 4õ(ìÔ ôhX-ÁAªô�¡…vVfÕZ+[Ý`[]6Ø4Vµ ´c\«@�úTM¤€À”IÓ§& J)EO°@ˆ#bæ”U^÷R ˆ;6Ç^‰ “Äõ[ÿ —:ö_I¨Z©Ž•TÈ1×€< OQÇkfhµ¶Æl rôÐtTÈ_µ`< `‹€,– b_À›(6(lŒ’ h [5ÖºõRUÔ X´_µ`PÀ\©Ju Rè_Þ Ä) V`a‰ ¤•JT(^ÕkµE†µÂôITÿè@½@œŽ5û$ U ´ €g�ö ¢„“F“8)‡©G!•^évvå•\‰•HÂE ·q�à™´‰•*3KBv < œ4ŠÆ ì)Y¥�‚5œ6æBU:`Pçv;8ÉK±t«€\Ò¶ _Õœ ”X T% 84¡”+pË% V!=™ÔNg oÑ +ºõ�6d K•€IBª ¢—\e b6‰H« |—`Ì×c/¤z•®¸¤_ dì¦2 A¦Ö¦™*¡@|S†è·}çJ2„fœ×̆²‡T‹Ö„fØI Ê�+~ׯÈ‚-ø‚18ƒ5xƒ9¸ƒ=ÿøƒA8„Ex„I¸„Mø„Q8…Ux…Y¸…]ø…a8†ex†i¸†mø†q8‡ux‡y¸‡}ø‡�8ˆ…xˆ‰¸ˆ�øˆ‘8‰•x‰™¸‰�ø‰¡8Š¥xŠ©¸Š­øŠ±8‹µx‹¹˜÷ rȺÇž¯ûô¡¸Œ!Ø®ZnÙ@ʲíºQ¦ÒÎ+SòÒ˜¯4F lo kÏÎÐØ ð0±˜2�óŠÜ � جÌN�#á�íàý¨P‘ýêÚ(Y“@q@àOá6­é²ŒÎìØL�›Ì"MáİöÎÍý–­î~Î^>Ì™ùVùérÖ¢ÍìEá@áÞ€ÒBnäˆÿ •e¢r�È®*ù, \O%1™/úQ û/ Zm â¬óªîÊÜcß°ŒÿtN*w�ëd­€LÊLMábOìtÎÔêíÊx9ûÐÌírp ˜9Ï~nǯô­ð0ž÷OÊ^+¥îî\Oa¹ }¤¬t\�6PàìŽú¸šYCsy ñV�$Q± ÏL ž/™‘JÈÌÈ ]±!ɘµ ÕT ›ÞJt $ð Ó—nç`âÜQ£[Ã÷R: 6Ù 9 1ÕŒlÛ@ï7îØ=ððª½,‘71°ÌHpüZQ’“à£ÿNü¤OòŠ€·Ía‘=n›¿ ±Ò. »©¹ªP&h{:ðbË8°}Àßñ©}`àØš5nðÎt°"Û‰;ò%ó®òZùø®90¥Y°¨ýZ T0 º ÖÚýd÷; ‹® z ¡Ñš:=Üà°°3(Dñ¥y“70 –Û¶'›ÑD1ÃOÖ<Úå n¶} ð~›ç Ž·:rúͰüÐmÛØ¸¸÷â½Tõ²�Ùlqõh�²³Ûã˜ñônÏöÄ¢·o²Ì�Û\�6³�Ç€‘¹1�´eBæŠZ½ƒ¢A{ Æ Þ\àŒ,’ÿp•1 ³�æT€¬¡™)-Ý\@°Ï”¿ðº+¼ VZ–�€Œ!±Ë1ë±û R&†n¥ºÒ°[Â#‚£Ãvø@é�vP Û'!îä‚lÌ’-ìÆ£9Ðä® é¬<ûrÒ]þÔ�Ït°ŽÕ€Ë—œ÷íäŽíš“Ž%MðÎÄzϤ ÍRà( °,òÄMÎÕúñÍLNÏ~²£�Ü –’ Ñ êý+=ùÝ뺒åž2èœrÒuû)#Ý ¾òî(,½h÷Ѓ‚,Ia©IÝÃŒ1ÕI,Ît�¢7�Õ1lÿh]ÖmýÖq=×u}×y½×}ý×�=Ø…}؉=ŬÿbT6ó\.€‹=SD8 Ñ@7‹b*ÜÈØjêÙ3åyÞ >è@j�Ü;¡¸˜ôL½]:ÀÝ Ä½‡Wl5% žcÝ£#æ1ác¨z ºcZ8€H haB*„~ç�:4$ŠŠ@çÃ'òDß“Ä@¦Jz@<º Ä£2Q–g\dÔñÝ ôý§ˆf‹NEgb€K–fà�”hö£Gƒq€Ê[Û€6�À ¤À �HdHÙ Kl¤ º$L FIéé8 G~ÈElÉw'vä[£ÝeZPØVxÆÜ#D n¤ÀÞn×¥Ip… ìgƒ“ˆŽÈê½hGÿC¨5Ù›¥æ©(ö7UóŠ|àÝÑ aÁwËØ_òãæ�oôæV¨@ª ŽþîWÃê/·jö§H·^M(¤BÞ„^¼ž7#ô©B èu ‚¨¸@5ÀónâfA‰@<“`êË&Þ%?!(¿®ÇkéÇï¦rbàrÒ>Th¿2 ø�ÿø3 ç _·ý&J <_ \;¿{™Fðk?"n¿ †*Yxo¨çö?¿0–d3O’6úm§ñ©uë+$ûùbûŸälPèžO©©ø ¾áP "}/'9Ìœ 26HrŽDA¯xn ÎaA86ç5»í~Ããÿò9½n¿ãóú=¿ïÿ "ˆé`è @q4!½$e`úl˜U ¼œY%²�"él:õÄ8e”½Ì"éah qµ~2:qt�%S¢>CGKOSW[_cgkosK D%a ˆ“ޏ;û”ÉÇgðð{V–­!íü©I"Àߘ0Ò©óDdƒ»xtø‹ƒ`Œ mðÅÉ*cè¦r%Ë–._ÂŒ)s&Í>16p˜çäæ®3:pöLƒ‡e5ÛôÈ™Šè­1:ˆú¡8&†¬£V¯bͪu+×®^¿‚Õ& !ÿÇ¢M«v-Û¶nßÂuÃdY�qïâÍ«w/ß¾~ÿ,x0á†#N¬x1ãÆŽCŽ,y2åÊ–/cάy3çΞ?ƒ-z4éÒ¦O£N­z5ëÖ®_ÃŽ-{6íÚ¶oãέ{7ïÞ¾.|8ñâÆ�#O®|9óæÎŸC�.}:õêÖ¯cÏ®};÷îÞ¿ƒ/~<ùòæÏ£O¯~=ûöîßÃ�/>ýúöïãϯ?ÿþþÿ €X �"˜ ‚ 2Ø ƒB¡„RX¡…b˜¡†rØ¡‡‚¢ˆ#’X¢‰'¢˜¢Š+²Ø¢‹/£Œ3ÒX£�7☣Ž;òØ£�?«¤�CY¤‘G"™¤’K2Ù¤“OB¥”SRY¥•Wb™¥–[rÙ¥—_‚¦˜c’Y¦™g¢™¦šk²Ù¦›o§œsÒY§�wâ™§ž{òÙ§Ÿ¨ ƒZ¨¡‡"š¨¢‹2Ú¨£�B©¤“RZ©¥—b𩦛rÚ©§Ÿ‚ª¨£’Zª©§¢šªª«²Úª«¯Â«¬³ÒZ«­·âš«®»òÚ«¯¿¬°Ã[¬±Çb!ù,ÿ,±ÿ€>‚ƒ„…†‡ˆ‰Š‹Œ�Ž��‘’“”•–—˜™š›œ�žŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç� CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›8sêÜɳç&-<´hâÁ^ÐFZ„ŽJªtiQŸP£$¢@ASKTQ¸C¡€# ¢�z J©¬RÓªÝw¢ª™ÿжkËàkXR!„(Å@�Öµ€Ç+¢ ¯WLrÙq­»¬ØQyÍ’J,¸²år6ªÞ¨ú«_wtí>Ev/)(¬\^ÍúÙ> ((‚©oçu¡ßEVrëßÀ÷ñ¨ªz³�x˜áàAä‚ÐÞ@¡Z‘7 ÝXþÀ9t(P<õq½¨–Ì�g_t#ÊPL|O´ý}ãi±‚ÚAˆJ-&Ú ”†ïA !6„‡Èt…7^y>œBwQ¬g]P0(Èt'㌣D1Û `™–d¹åÈIWUƒ„hQÕw<€åcÿUd—XV(PµdD b…’K:0!<.Éd~…˜ å’PœWØ€‚Ð¥E�>q[!²y馑bE_^úv g†ð@Õc¶ å˜nÁ†H”^*p‚6:@ã£�vBÕ¢‚peU"V¸å€ V '%¢9–ˆ 8j¥”›¢€ž[™)@a_=>„ÝhH[U=p‚ªlRJH¦¹F�‚{nÅ8H�D´ˆ‚ JA*cŠˆZiW}!¬ |Úg­Ö†…”¨éUˆ�]Ý7l³*ú@*`‚E[tŸ Ò—’ºBÈ-‘qñ€ öVE£‘,ð$m&èn†G„±]ºbW‰ÈÕêÂ…¬ÙÿqÏ–.xþbi`j¡äm­:æ JnœWY† J•Á‰HËqU£ "²fEÖêë äv<©e5haD\–©‚=^ ð¦¥z2škƒ.—:ðÕX+BXÔ6ÂLaET›Ã¿9­ƒ-vœ®‚çÆII U†BVÑnÛzXÐÎ ×!~Võn´[Zw‘ç–ƒ+®Í>'L•žÃ½õ"Ÿeç­ÜR_*Hå5s©iÖ¤—lȘb©±…ÔÚ©H"­¥Œ¢ž¯Zí`ê²±«_!Tg)˜Eb,a±·\•×{Žó!6º"+û–×à‡{•¥Ù¦£¼ñú+ÿ"Z$–WßÊë^úú¿f·úÇ*pxö©¹¹vU=å¾"«çú׊:Øß±¿Dì¯rœ ö!3K%‚++#ª¢#ª „¹ � Ó»= a† Œ¯,¸ª$�}( Nåîc‚º°G["á"RW¡Ç B6HëËÎg<L*f»A $èæÂ"‘,Œ� #ž…&H­D^æ×<É€ÎQ0bsT1<µ­‰aÚû#DÂ�0…h\MÏU¨Bô0„!YWšb? ½=£!êäƒ(B‚�l< ©xéf4†ä¡çd–ÙH¦U€\�õ½¼u†2…èÍž<—€¥ñÿ“«)_© @ÊR–²GùÁä!ôèƒ þìL™SDå(¤J)>Î8bCÝLÉK^¦ –ü&)ÌBTN,[�ÓK(„ —¬á ñA³ÙR2ÆI`üÎÊnJ¥`¹ô! ó¿ŠÑЉÀìËûRÎ_õ°–‚à£ýjGË=Â�‹� úšGÌ}(‚À!#¼¹ãLÂRã+„YIˆ¼˜¦Ž‹˜—7'º–­-¢kŠ+æ­Î)NÕh/£þÌèÅiÍx&Ó`¡¢"Ê?@Ç~;¤¦F ÑÀ™~ äyÞ‡(ˆ#Â'ÈOàRtN1Âsƒ¾éKÔŒÉ$Š:*§[„ý:cœvú ráÿ»¡üPjÃMZÕ8#e¨-u„+Ö âE(Â_*WÒœÞOw#žVm*;„忯� µ¹‚ÉU%YÞE®T†ï¨}¥AØÔ§:V'TÄ`ÊÑ«bÉlÆÜzdMÔj¤ˆ=i+»R»Àõmeœ»U¾g“òGMLdŸp:³¥Ô-zª–\-uî¥m«– ."ø/�ýtt�MnMfÙˆV͇^ùA”jɦ3j¡Vˆ® ó˜ÕB÷ªRŠÂUpE„-ÕªL¶!�5'ÛÅŠ®=íçŽh;¶ÙÌç†áâ?»²3ĉrU¹ qFxØîRVO`½ÍÍ’\å:&ú|ÿag1Á9`º‚¥5WÒ›YÃ¥Òp,‘X.y±(‚”7wÅŸu@•G„æ|VUðDM™·ßõŒï©UyK<7·Ƹ-ÄÑ ¡4x]ÒÀ×,„ ¤Tc$I6•|°–SrÔô�éJK"Âcài˜E à³ËoG…KbÑâ^\^4U‡æÂ´µDßZR�£ÄC‚+„<+}_ĦBÅB†È³�Š`¬&‡Pw …²I³¬EzƒVݲ¦}²ŸðØ œ”àA§\* QSç¯}ºvÑ¡SÇBÕV5!¦s+ ÚVÖ Zmë¼ÍyÓÀ6MŒCÏ>IØÈNöLÀÿâ;C\WoÊŽ¶´QÂÃìðNÇž¶¶·-’À1é=T6+·ÇMîŠhALyºu¹×ÍzWân·¼çMïzÛûÞøÎ·¾÷Íï~ûû߸ÀNð‚üàO¸ÂÎð†;üá�¸Ä'NñŠ[üâϸÆ7ÎñŽ{üã ¹ÈGNò’›üä(O¹ÊWÎò–»üå0�¹ÌgNóšÛüæ8ϹÎwÎóžûüç@ºÐ‡Nô¢ýèHOºÒ—Îô¦;ýéP�ºÔ§Nõª[ýêXϺַÎõ®{ýë`»ØÇNö²›ýìhO»Ú×Îö¶»ýíp�»ÜçN÷ºÛýîxÏ»Þ÷Î÷¾ûýàOøÿÂþðˆO¼âÏøÆ;þñ��¼ä'OùÊ[þò˜Ï¼æ7ÏùÎ{þó ½èGOúÒ›þô¨O½êWÏúÖ»þõ°�=�pP”+¨ûI©E`rÙFB¹€¬¥º£”@ )`„T@ T`øœÀ�Jpûu¸`úˆ¨|-\ D߯D’�„ HA U0Drà‚0¢. … ’   $ú„¨þ©¡…+hà9PÕw éÇ}qé( Þ‚pà~‡à(Kµ«à}“@䀀ˆ0€—Pè)°}%°{“P()°€…�(Ø8�|‘�8Upÿ­`ƒ#Ø"è?8 A8Wƒ…pX¨ƒ;RR U�Z`„¨ÈG…�@�X%Wà»ç) ‚ûWW�WèX9PƃdØ~RàJ˜†ê†ÈÇ„¡æ„Ë'…ăl˜xh. %à†jH�Ûçk˜!‡úçR�k(‡X¨ˆ.>€V¨Uâ‡fhˆD舨„ˆpVЇ*°~=ˆóW°‹IP‰Š C(Uá€Ẋ×g{<0°‚80Œ´wI@ƒ’ ‡.ð|‡pè‚<`ƒR0 <°|8�0�8p‚ù§‚ë׌ùÑÿ…�Zè�ø€%P´çÃH ×G{Á˜ïh;wÈWð„†PŒQÈ~I°‚–‡ZPˆk8„90Œë7~¶X8@��Ž‚`ƒ)Ђ9À Ylè…ì'€„@Žs¸„†�^ˆ�å¸�ÿ˜Áx·ØŠô@Št8 )pW k9 Œ‰�ƒˆ`ŽÆÄ‘X�…p‰H x�ùáŒ�à}F¨å·¤ø“cȃ’ i‹1¨”…‡„Ð�Wa‹ÄSˆP‰Òw8�>iÀ‡UPFXA©–éH˜ùh@YHù9CÉ’õp~�8† “ɈjO©‘x3xÊȹØÿ‘½8 ‰™}†°’ʇ…*ðŠ é¨�‘`Š’™/HR€…R ~DR˜ƒP6Y$ÂgIà‚„@™‚0š/ø–‡ ™Ó˜}¢›UX$<9´È—ì0…¸~lœ� ÍH‹‡¹²Ù𙩂ç8Š«™…q‘gi3{Ù59�°Y†RPŽó×  ™9`…°Ž%‘Œ0—tY�à7€øŒ°©˜9�×)¶‘78™vÙ à)žî)åi9�¨«ÙŒâ‰œÂ¹lxIPä‡|šP°™šŒPšE²�‡0ŒJ�ªÞè� ù˜Žð’„P‚¶SŽ8�)ˆ×i;Æg{SXÿ– ¨�Œ0Ÿ!yƒ:¨ée˜͉—U¢¢\’�‚°—ÞW‚0:2ª ¢•È~8¡Z*°}¨ ( ¤×y�Œ ‡È›ÿé :[Š‘3š,Z1¿y—„À¡‚P¢9—²©9 ¤Š¹Ÿ>ð¦@ˆÐ ¤˜ˆ?š‰iú§~ê È�ý9‹jØŸuŠœi¹�왥ìÀ�yº<@™FIŸŒ°Š’élã¡“œ |:ƒ:›9І_ú§AH§>@•þ©z} H™(j0`“úi …*š¨¬*©ƒ�¨³j›/)«¦›‹ð¬‰°¦Y9¢˜Zbº§”�ýÉ0£8¹ÿ«J…XóƒY }ER¯úži¨µš”li�f8Ò·•RÀ�“™8ˆyÉ% ÚåJçj;ˆVÈ�¿9�íº©eÙ)­¶£¬ãj§÷j¯ùºŸ¿i‰\¢®¶Ã®„�ž™ª×ªiˆ¤�€Ž)�Œf(§ª‹2;¯Ÿ3³±x±.[”�¸•ô8‘P���6 ‹X±øÍX‡º·|ṑÐO0W,LJ ý«/AÆÅ=¼Û» APÃ�P9¬Ã/ì~ŒKÀÇŒìȘPd| ïK TÀ�ð“L �ÌK�Ä‘¼É¥PÉj˜‚0—Ð2àÉ[ÜŸŒ¼‹Œ ƒ, †L ßëüÀ ¬Ë�P˧pÈŽpÉ�€¹ÌÉ»\¯Ì ’Œ À,̃€Ê–ÐTÀZ É®œÌ±Çï[ ÿ¢<û›€æ΄@ÄPA°ÄÜKX€ïkмÿké;Ë‘ É1€ë|Ǭ°Èö ¿QŒ±¼ÀåËЂðÊKð¾ß| ÍLÎ÷ìЄ ÐëüÍè,ApÌ‘@ÌÖÌŒðKðÎÊ,¾/°Ð �ÐZÑ0Ñ–Pш Ñ-]Óÿ[KΉðÑ‹0n¬Ê�‰¼Ë<‚ =À/€ÒâìÔƒPÎò¬Í ÍÀK-@ÈZŒ,¬ÅI¬àË- ELÈŽ Ó@ÀÀë[’Àn½nÍpíÖû÷d-É&}ƇpÐ)ýѼ¾€Ò>°ÿ,làÿƾìlüûÜ ‚íó[J�Ø?Ùƒ@¾•@HÀF0Fð‰@ÈPýÉ~ÝÂà+Ø„ ¿,‰=Ó ÖõÛÂ}¿aíØ²­×† ÙŠÔÓÜH0C ÂG ˜ÍË#hÆWá Õ)ÍÈl]$Y=/0Û„Ó„àÍ…€ØfÝÄ„`Þ�àØê-ÖŽ-Ö‘Ì|ÑÓÍÈ?p݃ö=ØØ<É�@Ó…Þ„° -àÚÍ܇€ÐÑ� G@B‘HЈ Û¦�Ú‡ Åùíø=à þÝãýßmà]àfýn Îà§| N@Å‚ °ÿÊÎ�Ð=xÍ9¼Û…À׌œÌ—½ÀL‡|ÍJa×Îö!¾î7½ >Ž­l>>KðD"ŒIlÏ´}Hn:­ÝPmU‚â<Ž CP‰KŒPÎÍ}åYNÀG-^^ M.aàw¼ç„`æ`inĽ Yˆðþ]Ý=¸Ý@ÀÎ�KUîÜ=^Ô‰ èæœé Å\çƒ0æ‘@äá½ä‰¦~ê¨>è€ ÒR>ß¾á‚@ÃVÞå¥�çèl·p烽è5�âšð l,߆ ÏƒÝå°.⇀ëÂ^ëÉ®¾¾ŽÒÌ G0ÂÁ­Í³Î—XPÿΰßTNݾËÜ̘®é™^ÐÈNë~Ö9ü䎰ò>ïô¾Óémé%ò®NéÆ�Lº> yÎá!~ËMê�®ê›�ÉÂîæ•@ØÍîïÊþåÇ^ðEÝî^,è?í BmŠîÉÚÞzDòAî ¿×â>8¼ }iMË¢ ïNÌêSÞãÔMà‡@Óž¾Ë^ÈèðÛþÐ!_+Î Mì…`áÕ:ŸÄ-oë¾ËÎí� àÐÍn|F`ʆ`ãžÌ뮇ÏÁ-Ì“ÎècæVÞó÷[òŽðˆÐä2ïÌøÞê(ÏÈ$M?Ü;í…`ÏW/oÆ‘­Z.ÎÜ ÿÏÐ,܈ L@ã…�D2\_¤Í~Ÿáwoøz?ñ¹NÏLŽÎ‚_1…/¡OôÐüñÃ-UOà“?§†0ÕžÎÓþã=˜Û®]óxŸòBÄàîÛÍ÷¤Ý�=€Þ>Àê� êîö¢åú^÷¯>ú¾oÇã^ån(¸/æ]ü»½¿IlÌ<¾?pã.îZ ìßú1>ޝJÀðãõx|ÚÝ?ØåMÀ? >ƒ>K„ƒˆ„/Œ’’‰/„XAŒ@1ˆ?‚–A@–ˆCZ©ƒNBB–LF9„YF2¦‘­„K�½ÁÂÃÄÅÆÇÈÉÊËÌÍÿÎÏÐÑÒÓÔÕ½ ˆ?â?㼃ã�㋈/Þ?@¥ˆéÛò•àó@ AÄ…ö‚܃´%ÁFBl”šÓBÒ*0Òï5ƒp€¢Îf£Ëq¢o4Æé³L†F1ŸjÄœN?¸LJ­c™ FéƒpV�–|cÞ2‹áÐÀIp§\ó¦-aɈ¶¡5¡Ð šK(/¨¼#jQ+p�Ð4Šel…M-Q´‚”e@MBå*VeðF}Þ(û¦@BÀÀ§Áhk‰z×Ar�¡ãjUGÝõ/aËÿ�„H—‚±„¡%\€Tc–Cåêò¾Y¾ŒÆÔdù|é1pпB¯M kcÏÏ] ­¯£m:{1ÓA`ó­+»[S�ÑÙuŠÎ²ë„\=£&Ëa Cm’€nf“øQ^£ªË¸n}°ÖA T ‹ü«%4@Ychžˆ°n/äi Ôuw©`®ç¶ŒÞöÛµ™0.ÐX:wu©� (‡ \ˆ%1£Ã°ßð ˜Õw~í ´To3�QX¼Öp§Å+}S�àbt÷yð¬'!Ô©LI¸Ï�ÄXi*ð+ €2¢’©à&[�›×ú¡WšŒ(hãøarÖ°¨Ì�} ±˜JVV%ÿ·WýñO�êÅ®:œš&§÷·ˆ¸€d/JÀ×XÅËàoz5M3õȈ`zSpãaÔ]Æk>QwX&ô³Y#r‰1CÕ°v‘ÂLàäR{Û]nø¼ÂPo/QæI"Ûkqºm1I(…%šøjR `ш—„÷•L~Ûô1r�Êe:Ò*˜'b2MB \y ìtüä×è¢Q.•8°Ia&ëR¦à}i­dP&4y²°¶œu¬Àa§ •°«?è1¹ª& ³–ußø¾Ä{×Ä¥«DY7[ÞÚ…îS¥o݇ºT ÚUS6°³,ouçÜŽ†˜~õË ‘²6¼¨5r`ÿ0g±à�)dŸ‘œuxBpðïóø[àZ xÃD*¸ÈÀ7ßûÎÃA™ “üä(–SÎò–Öxã¦ËgNsD`1i&­¹ÎwÎóžûüç@ºÐ‡Nô¢ýèHOºÒ—Îô¦;ýéP�ºÔ×S�©ëK ñ @U$ñ­s¢Fк1ÂN�i@#âÁ¡ò°½2‘ßÚ>ˆ)ahB*‚Ð0´§Ýê·z�àÐ Ás#3ÂBAŒQ€-‡L/Ý1Ä›Ç3–¯�P¤qIt GÓ-,ÑùÚD~ò€—ák“y­ž”`yzƒ­_¾õ´oô}6�ÿ �øKn ºÙ§¾V¯¿îO”|a@íe2~*Z“ê[ƶ÷Ü™”² žÒ?þ¡8à�Æka €‚@€œ`û3Ñ‚"´ÿ0¢ù–°¾%È_ö%àfP"ƒ°Àw˜Ax‘· &{…·öÇ#ÜC€e·X /€¥€H `¡°Ûð€Ñ ²! X°�W�ðñ`R{4�}`‚Ç€ŠÜ Z@� (¨§Ðx:€ð€ð�…ê× A‘‚–à{ûW‹ñ}â(@1��Þ0 2Að@°á0Ðx/ÿÙЂƒ€’ ˆ€vÐA~˜ÀÀfh Ï10„HÇ@ i'gØ A˜�°|ø“§6±ä÷‡0n˜‡‰ð ŠÁ[K(ì÷Ø×ðRH�¿p Ô±úqA@ /°r BxKp†RïwX‹·Hgøà‰¾€ …¢È cy@Ä� ˆA W˜…ˆhy°‰Žá7Ø! rHyË×~¹q�’�‹Ñ¨ Æ`š!xÏñ8˜ q1ÐŽ>@ ÄA ‰…@‰–@‹Œ`m÷ˆa£b3Vhÿ «­h‘�`Áhv„�‹îB€Rókx&�Έ[�@  �“ЊK ‘ˆ€ ­àÀÂg ~ø�àx(?œA“,IŠPxÙçŠ-‚{dh ›ˆ‡•q‘„ &‘’°„ýèড•uèpÁA0•À ˜•_É +¸‘`Ø�à‡•Yùt˜”Ê@’P1•ˆp–œÈ{9é—„À•¸Ž` ’.¡˜©ÐzçˆM ~X8”}ò6aŒ´ñzX€xäW‡ì§ ™> ��{a‡„:ˆ©ˆ–h °›±éIøg‹!—ÐŒ6Á„“A`³×ÿ–"ñ–˜a’óh4X‡bBÉ ¨‰†@S�ûǘƒ`˜…€˜¢Ø AÙ ZðyÁÀ“Á�Ž”(±vÞ˜•+yK91†ˆ¢Išœhš�÷œ:xœÌ�KПþéŸVé�’ pˆ�¬ˆ@@ Œ ¬©”l¶éK€˜è0�™§'Ù Û€ŸaM‰�Ò·�©Pv½€Là|9€“Yžy‚qH‰xíÉqƒ­`—ù‡{$: ¿™ÚÈ j  °˜y>Ñ ,J2è–”÷汸„y¹œŒ$�?ªy°8y"Úv=Ê˧Gð“½Ð�½@ž.º'‰ÿΈ”ìÙê óù”©Ð’½�¥ç�dI¤¸©ª ò€4™@AZ˜[a‰ù NŠ@БÎ@„­ék:‚ɉƒÚua¥`i&"�ø¡y„y¨Ñ¥h�¾ ‡Z`S�cþ©ãÑ¢ij'[¨£À¦+É™ØáŒXsÊz©ð[ñ~tg9¡{Ô §*—Ž ½€�@¡£¹ŽZÐö’.’¨L{š é `ÐÇ‘~ÝðïPy)‡Ëª©Ù¬ßÐ ZŸ|˜@Ûp›©0¢¦ †8ÉpÜû7®g «±J'ØÀ«kè �åP Añb‘�ñÛðÿÔA !? á æàŽ�°?`€œãðìú;Q›å­E:¢ –�ª'±kø€ÿÐ ˜°BKƒ`|É ¡”å`¬ ù&2v©$‰[X�… ƒ0é¹F; (;¨ð"„ð1Y¶7A˜fÁ±ÆÑ{šÌš Ôš@vq·CpC@¤ù*™;(ϱËp~‚+ ÄБ#EêÍA{&¢§�ƒüØ HëªÈP¸Þ‰ˆ…—¸ÍиÌ0˜›šá´ÈJ{•Ú hú·zTHœ¢²‘‚ÊPzP¨ ˺Mc†ÿ@ºéÿ…‘2­µû•,i¶º{¼ÌЋȻ¼ÌÛ¼Îû¼Ð½Ò;½Ô[½Ö{½Ø›½Ú»½ÜÛ½Þû½à¾â;¾ä[¾æ{¾è›¾ê»¾ìÛ¾îû¾ð¿ò;¿ô[¿ö{¿ø›¿ú»¿üÛ¿þû¿À<À\À|ÀœÀ ¼À ÜÀüÀÁ<Á\Á|ÁœÁ¼ÁÜÁüÁ Â"<Âê›0#qî‚Â$ì+“„gP‚;üV(ÕK07r+Ü*1#bÈ03�ÖIÅ5 g"Ee~¢2©�X$e7L+Q#^Éà2v„0Ë– =¶ˆv(cLK|+w³gÅ0;ƒ <ÇÐfêñL”br[,ÿ+ DĽ€P8Ñ#c™µ;BãZQŒ ¬C9ÿ;Å#ÆŒp:”c o Zð4Æo/6hUY …_ÚãÿU=l?³@YÌ=U€2ÞcíÀY” Ÿƒ2) pµC9×C?\óQ›d¤N›ƒÈi¼)”møÆHlœSsÃI´SHÖJôAÛã>Œc0L´=ÄXB–?^ü\fõE›åB<à3I A6÷XXÄfÞ¤]äD`ÔM‹\Èîæ\g4ÍoÔQÆpÌF”Kb•Ɉ“E ¥ÇG@>õÊK% *ð_cUúƒ?°˲<4ƒ< ÏVq�`b† `†QiUb ¤eÜÓÿi*'>~ 3øæ2´1Cˆ sw´LÒõR´�Î>ÀdÜ $Iúœ^ÃÜYÊu…JÀ±gf П²o* Ú4lucp,ÓÕPÓ7õbeæ[ôƒHÎðYBí6­P¹æ6�Õ`>�Й%`î”Ó,e  ŠæCõb­På5>�ŒT:lÓ™âo$Ej9Ç V#6$ÄU™Ì'm ÐSioóÒxµ=[]KuU?ÜbÏfÊðäÂÓ ´UP¥ÔòÓJ•nf• 3ÄU_mf®Æ6R-FWƒBâÃÔ³a%íM!�Ö™W%@3Fì öj¢Un m Æc0÷̓ä\êõf‚•b¯ÆÈÐ3ÿJWÜ ¹m ³%Zˆ‘>™ó\hÇðT\ÕÏeeD"ÅSˆ¡4¿¶ÒSÖb@öM óq¤Ý)Þã_Êð×m£Ä�iLCX% Æÿõ7[dÒ´aThÖ UlfáØYÖ_­@ÞT%> _÷\Ò& rl *04}ÕÝž²7uí�å2pmÞ•eÝÅ€^Œ“Õ»Eœ]lœ[,%>6—E[úcÒ _%:ùŒc%áÿ-C¢“à�4à“Ýp¦ÓäÙ ž)FFã'.J3.:ò4LÿÓY)P^>ƒ èŒA84X9–Oó5 ÄIÇPaE5nÞIxeoòT7’5J}Ab33ª¡FªQÿV»äãWËEnO�¤ytGµÌÖ©€Y÷Óàï&á;.)�¦ ™ãn¨Si‘\Í3tÖ|o”A“µÃWsÚ%p2òóNïãkó–Yˆ2Z–nœŒQe5nçó>Ûè(“°l5!ãèÿõkÃju:tÅfZÞÑ3ã1§mã:Mètè 5lãö1•^T¹¤­öak$o&oW“ *´çœ¢_ëÍ '6´p©!dÔ¾JAì ÖÞ —Ó6dÐÌ“íÓ­A½`íÄÁq×® G”î�DîÁpp¾[ †Íì•"ro5Úôžïñ…ØúNï:“2Äïý¾ã ‡sX=ðŸð ¿ð ßðÿðÿñ?ñ_ññ°‹ñZòvÄàwáàu§û)ÔqO 娴Hú�¹Ëñ˜Qyp‡ U‡�Ûu¸é)YòÖ@óÅ ‰í¹ïš•;/ó呜Ã@‡æ±¸ž«Áô´·ñ•Qô¸²Ho˜J R¿NÏ)qõ+!V?ûšõìÑüwƒÊ·_ïKX ø„ €™ Š~h¹÷#Ÿ 8}ØÇ žëž°…†‰„¬óFØ ²ä—¤î0ø61釛Ÿ¦�yKx0OË1ùœÈ ®K­ÁƒeG÷à€„ÒÈ„sO† ña纩 úb)GƒÏ ¡PöQÿ³h�]—‹ÐÁ‹Ó7ö”ñö*(‡Æ0òÔ(xf€Ê [HxOõÈ ›1Œ’0 K xRZ¨îY†ÊÁü’€ûH»‚¿†©;Qdã_¯š`šè±©+ŸúB/K?K>‰>XA?>K@KŠX„/Š>1K/œ?Ÿ A Š¯//AX ‘’·±˜š§?1±/A„K@ÉÊ»±×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñê1°ŠKÙ1@òåå´lxê–¸%*2€�×±"¥‹!àš'PXÿtÓ… H®i‹bEÒ0‘ni¹ÈËÞµŒ cýÒÇʘ&8iR�’Š–ñ õÈP”ŽÞs1!(†± ü°–Nˆ@  #d×´°Ô¯¬Ù³hÓª]˶­Û·pÑíVÛܸQq¾]qU¾{ucQuîQ¬ŸÊº¨©ªØ88G€p¢%¸BÖ €q¢C×fÅËTkæXA€Ò³œêµ%@#ö•õWQ²Š‚dõñ‚ßµVŠ]{ÑÛ�Òx“+_μ¹óçУ»{Éð5êÊÿ™‹Á½»÷ÍÝ–”æPRùà±½iÁH Ö/@@7x¤€xÿÏR°g‘ç…–ß~¹°æCâi#Ù}S7Z|±`ðS+b`Ài1@˜yØ$Øáx§%¢(©Ý3a…úæCú–EÈI'ãŒ4Öhã�8Â%Ê0&‚]r#ŽƒÅDi$nßl"/ú8XìÇw/lУM…HUZ&Ä€E•ý�" 7j“_—_RâÙ§¥Yq×4çƒÚHˆ"…»Q„Í !.•�‡´å…ÚnvÞ‰ncZôßq96êè£�F*)�?®¤�ŠÙéå –÷LG-jã(?(VW‰°yMeýç—6ªÖâĔMF-‚&¦Ÿ)Ød,ô˜ªä‡Žw^�ÿ‰è(7 iœ±Œ5é´ÔVkíµØvSi`ÀòV#÷Éb€¦±Ôêªmä²Å)~•&¡]Ê^“hP®ëèÍÒÕâ¼Ù”‰M¼—Є�ª@„«Èé}&pÀ¬iqe"aÚ@Â_–ðeÆ Jân+…*‹·@T5Ù–lòÉ(§œÜ�X\ùão`kœ1LÜbƒì^/uf•Œ 1;{ol¾Ô›ŠVK§b4Mûg“¯°>-@Ô±03ó¿ÞºHI3/`PJXó†j"XàÒ>’È·5ÆÅŠÔçΛ�SÚLÕ2T$ü&۵ʀ.øà„_#M;‚¸Ñzn#ÙÄÛ€¸©ÿ—ÄܧáéªDÌ)…Œ³ŸäÑbôB1¨E{| â³ÕtêÕ ì?€ÉL÷üðâº?”õî¼ëÎØìÔ~{Î©Šœ ®sb¯0&Õî½ë Ê>ûpu<}e”öÁ ŒZï1÷>›ä8óßçD!~Ñ2_¯ìÛ+åôû�^”L»îÿÿ  à^v�™„C(Š�EÇ^ÛxÁ¹ªdÊÅ€„Ðè,làTD­¾Q‹9µ Bµ 0ƒÙÈ”`U*Úð†8Ìa£ö�…¦¥‡¬¢uX^�d¤j!³Q‰%:ñ‰PŒ¢<¢$€±Á…ÿU4 s†¨Ã‚cz)›/ñ¶1šñŒhLc1QÅL±pÄ(}Þˆ  1R£÷ÈÇ>ú‘p·¤ È@–ãK�¤ãÉÈF:ò‘�Œ¤$'IÉJZò’˜Ì¤&7ÉÉNzò“  ¥(GIÊRšò”¨L¥*WÉÊVºò•°Œ¥,gIËZÚò–¸Ì¥.wÉË^úò—À ¦0‡IÌbó˜ÈL¦2—ÉÌf:ó™ÐŒ¦4§IÍjZóšØÌ¦6·ÉÍnzó›à §8ÇIÎršóœèL§:×ÉÎvºó�ðŒ§<çIÏzÚóžøÌ§>÷ÉÏ~úóŸ ¨@JЂô M¨BÊІ:ô¡ÿ�¨D'JÑŠZô¢ͨF7ÊÑŽzô£ ©HGJÒ’šô¤(M©JWÊÒ–ºô¥0�©LÍrø ¦9ÊÁ gÊÓ“JÁ¦Ø)s*�ƒÿ¥ UÀÖQ“ÚÓ¦>Q °ITÅ¡)XU Ѐ˜zT@¨äP�ŒxP®V‹¬fuªZox±Â ­ßЂ ª -ð )êYìÚ�”`F)¸B¶ª صÖ†%À�JPTr06)pÁ¤\ Yi¬7ºìa7 À$$¢æðl,( Š+PÖ¦7M]¯ÑÖ¨ ±±Èe]P…ÓbÃ*(A ›�+”@ %�,l){Õζ°ãÐÿªP¤*…¤Í� ¤�Ê‘¸L­8LëÔâ b� –ŠÚkȶ¹•�wQ{ð>· VuÁID ŠåR7½¥µ­rU‹Y›à½×X® «…Ù*å¼Áeàx©[ÓþrÖ¤.¨€„'\�$À6®SÅcX8RpÁwSŠ\  8È ’€ßDà`¶”µp,ppä€W(î5x°U�â5 Úõe+c—C·6Æ� 蛼¦@µw�ð5\ �$§@ ~Ç�ïZ僂Ê0È�l+€\“ä¶»ßM‚RLKÔtùÉ_±˜¯Œƒ¿*‚Ç*Àê¬ÖFø®!þ®�ÁÿQUÂæà ÔmRÐX-TaÑCÑÂQkšƒG—7¹RŠ+}«÷¾CN¹ˆgƒæëVb¼»!rìoÿí-†Ó;ókT¡êâ8¿¼»÷)ÃÿUÞV Û߯ùägÿçà[× rŠ`b7ká^ÔÇS¢EyÞPä· R x>�Xk�Õb•Ç�>paÙwgìYŠ·yŸu`.åws<7c} !ˆy±¦‚é%€ßЂà@z½ÇZgx®�W(Ø€/µz 6|þ[— †W^UÅt%'ƒ(gU pwan9ÀrWð‚ÛPVq&rà'q  j&†Ü�„Ú�c—†We¶U_ÖmæÕ„ذk¦UÆ|o‡‡Mfrâ`kNgU�¥€. pGuˆV^ÚÆˆÞg„.%jH¶h²w‰ÆvF]P‚Ü0ÿ‚M–Ôµw5ÆZ ¹…x<—Ð\�*€Y™V\±Ø]VY&a…•iva®Ø\šöu˜W@v‰œ˜ŠWFŠ6WUˆy øjš˜y¸h�'VVuUµh˸Z&qŒ&„ÚàUÔÅw/ö‹''a1(� *ðŠËÈhNçaºyÖ&‰1eo¨9€;%�¼—b!thlˆk8 gçÐ�ùGZð…Ó'ýX�{V�qå�Û°gÙÊå�Y‘Û@�‰�ÕX½&’(™’£m*Ù’ b¿µ|Kè’4iXb¦S5™“:¹“<Ù“>ù“@”B9”DY”Fy”H™”J¹ÿ”LÙ”Nù”P•R9•þä%Ty•P y,ÓDn¡•’;ÎÁ=¼ç€O`Bp3Ð!Z„•=ð7ZÑDHq6l!—ti#b)$ !O0GÀFpL€Yà3mY.È—BÙÝÀ/|")ÜÑÂV 3°˜Yp˜e‘C 2@š2‘9’——3 Ó7jA@9ô$I4šfÁO FÀs¾ÐšE9®t4o1œ@ >Ô!‡ÂòJ6ƒÐ),býs w2:œàKÀ@ËYቬ� �@!ÜàÛ ›eÁ–±ÀTðÿ–ÂI“ûÿðaDs9´`F@ `% úé Xã |! ¥p ¹€a£7ÁÀÏZÀ%§�ŒÁRB‘ S C ]¹ 'çy"°�ÃPŸí©šò0Jñ½¹*¿rŸ:IL+ýâ&àPB*¤â@=úšÜ�!;Q—›1@qïQ.{É. €u ¥ÛI�a›Ý°£f!×@Lð/‘©£;Ê+Q¢ ËМeÁ2gº ôà%² Ò²']CžŽS™y uÊ]cGÁ¥ÜàÜàžý ¦±@¦ò¥hê“;‚W¢&åà–º™áP)bd‘)¯’ÿ „š*e”/Pv°Ñ ©ã'ÝЙÞ� ‡*£ñ@£‹Úƒ›� ©¼b)¡±0ä`$ÀJ$Ú'k*—9çá/× �{‚¤� …Ò�¹á¢ut1 *Fˆ*GÐnM0�š«ºJ½ªHf¡©ã0§~šÊ1ÂETú¬ÜàEܪڀ±º /D®ØÐó G u§àª’}ªB �a•nQ°hs¦/0.vQGñr²,·ò"*Q1Ÿj{=@Š6–áªÚ£Ú�­~3¬Ù ©Yb§949:°)© s©ùâ2›²<à±Ï` (Z1ë ™ � [ ¯A�ÿ—x­üb%j¼�9ìY²²*0¥Þ`ÞšG°µ£10[“‡“8ÔC<ä3:g1¶ª 8?`4’SCÝ�Ѓ®v5ÔÓ;žA îñÔ ×1?ùq›a%@�:?à•¹Q;ƒk; "· €C ”+C0^‹6¥“Z`FÐCp£¿!°a[º‰@v�µÆJƒ4´ Ô%ÄBÚ�¯Þð™âà¥/+Y  ä0kºÀ›—ª+°±EÃk8Êã—¤¼Ì[®Çû¢¯g!¨ä £úÕÙ¼Ú»ˆË%gÁ'™ð){#Ò»½æk#1p „জ«¾å{¾ð¿ò;ÿ¿ô[¿ö{¿ø›¿ú»¿üÛ¿þû¿À<À\À|ÀœÀ ¼À ÜÀüÀÁ<Á\Á|ÁœÁ¼ÁÜÁüÁ Â"<Â$\Â&|Â(œÂ*¼Â,ÜÂ.üÂ0Ã2<Ã4\Ã6|Ã8œÃ:¼Ã<ÜÃ>üÃ@ÄB<ÄD\ÄF|ÄHœÄJ¼ÄLÜÄNüÄPÅR<ÅT\ÅV|ÅXœÅZ¼Å\ÜÅ^üÅ`Æb<Æd\Æf|ÆhœÆj¼ÆlÜÆnüÆpÇr<Çt\Çv|ÇxœÇz¼Ç|ÜÇ~üǀȂ<È„\Ȇ|ȈœÈŠ¼ÈŒÜÈŽüÈ�É’<É”\É–|ɘœÉš¼ÉœÜ=Éžüɠʢ<ʤ\ʦ|ʨœÊª¼Ê¬ÜÊ®üʰ˲<Ë´\˶|˸œËº¼Ë¼Ü˾üËÀÌÂ<Ìÿ!ù,ÿ,±ÿ€>‚ƒ„…†‡ˆ‰Š‹Œ�Ž��‘’“”•–—˜™š›œ�žŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç� CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›8sêÜɳ'¥7@! šŒh&£—�ú\Ê´©$6 ¢¢yUeUß`zUë¥4 ˆ8K¶¬!¨QÅ62QuL2°QÿMlíŠ l³xó.µ‘VAšFaÂ*p‹LŒ‚Àj-qUàÕ’]½�#ÛD‹f0#´g¬"ã« MÔ¿ŠÉ�i\ lbɨS«ä¼¸Í¢1 ÎT&lŒŒæÌ´a™VÍ»7I´>l›Q´øM櫇ޜ8#ú hlÒ¸d#� AlÌŒ!ƒ†M¡7iž'ÿh±wÏ�E÷®> û4'¾£!s&þ¢èi2 /½:{DÑ]çy‚´�†¢¡!à}óÑwi…°ÁÜÿùfá…Ôp]ˆàæCeÈb\U$á�mS bFl6Fâ`Óù€^……À"#•‰1lÃÂcgEå]qƒœðbBÄÿ‹U¡ñ£"KÆWܰ19Æ‚gÆä‡dFâ�†)f2Àù˜\ˆ,éÝlßi)𡱤'ØÆ¥ ™‰±ä'¤�F•DÄh#"‹a¹H”ƒÀ•¤°åæÃKÞI¤œ�‘† {*ÂU"HÆ_q!òä£BÎy† l˜`[TöRYXf˜àg•b�ö£s¯¢9殼S¦‘§¬ lò†–ºòê’)ú°¢�…´±ä]‚(Ú¥::2,!Ì&×í ¶eK*�>xH‡Šc!?Âö!¶!>È»�~Çá«;2x‘QY®À]/b®b+’0£ay�÷ê¬{²7ê9�ãÒ*žH㵃I[¹•�ÝÖÄEE† įj߈ ¶ç�¹H‹"r6!v+{ØØä&RfáÆ;i¼ °Á•ÿ­¶nþù–=`çÖçV,ê™›áTbd»ëgÿ¸#¯2ù¢îÖó‹ÎÐeœ9E,z³Z!òÄ-:a�èÑ güç¿¶­*,CŸ7ˆˆ×‘.b=à×hc'F¼ëk±K þ|·B=bOgˆ¡ e¸ªèÍ q“Úݜ斈é âe‚¸—ç È>œÍðˆ3¤Øˆ¢’B:щêóÖ‚¨@……ht‰hš Jˆˆû¹ [r{Ĩá/BD­tLËÞ�ÁBlˆ¹“a2çÂtÑg?©’ßžÈÇóyÐPƼö-àâ[¨rÍ æA<:ÜgoûÛ¿÷GÍ9Ò§NûÏñÿ²»ýì³&{É€. ÂÔ‡3•‘iÃçÝè°FtΡ†»»ÜòˆoL¦Ïlš�ùôlæ‚Ø QèÊ—½ìFļPö³À+´Gý’|éA¬ðgß=!6°…)¿ÞË%/ÃÙ—_¾sšÑ|73ÙKïS;œòb>{»÷ ú1¡änà{äçüµ³Öày¬Oä×Gê:"¾>ˆÛbœ¶|÷içv§v�×wžvugzŸ†h°^±\°€rçv»Çi¦{ˆ°v({|çx>PÂWo[ÐndW|fdnÇgŽ6fíæ‚pk~¶f$Øÿd�—k÷}²Gv9èhf÷t.g‚Èiªw…ƒ)¸m¦¢'f0€@XeoàôWnÀi½grÈiç‡e%Èvô¶dg€H�á¶v‘–�šxFha‡|çck&v]iƒgr|·z�²v,¸mxwÄ7�^¡‘6k>°‚-ˆfwn€ÈgkÐe€h¾FAUAmt¦F}će]v}z¶bx�Ôx¨ˆƒÐ{µfraWr@6fV8f±‡hh|'Ц†eIXH|ª8†Å¶f’†‚qÖŠ|{kf€…�‹†`jqFŒ–‡€h®V›‹›‰à‘*†öh®±š÷Âu˜ ln†�ùš$›z€L¹~cÆg}I&¦�Žºi–qɪ‡±�7|)È¢ƒPà�mØ tw§Š8” šî¨sÑz¢‡@êç®Û†¬Ô—ø•zÉcZ©ˆ}Ùyui‚®F|¢H�y†h ûx&ÙŠ_g}Ñ u`t,J‰zÙµfG}o f®fµî†d¨)³y¶[=ø°ÁWªEŸi•h‚^pnniF×eWk‚DÛ•Û¶vb7›kÿ‘Ù’õ—\à� Kl~ ¸r‹cPhhaWzd�õ'×J³ù:­~J¹††«Õzª¾é´…`xfv¨×Èw ékçp[¹zdwµj°³&8µ•iŒ{zÿéø”R¨™–•9‘:§�çeÌæ¹7kÀ[»»ƒí¹­››Yz¹ç°t;üÇwê'�`Ðp` f×G{p¶úHl|Gyk—p*v½È›kÀ†@m´×Ú¡7)¹©ævç«r2X²W[‚h{m�{•ÉZ¡[§ì7µ†ÐeÛ‰¾Ç”àk~ÝJvWVÆç¥l§:xÁõëÀ¦§ˆ×øìÛcÞ6f-粓úiùëÿeû›´·cÓY�y»ˆ&©´Çr¦ö©ÅGÀÜ­ùYÀé»{w÷{6Üp†Çs×m <·LŠþ¶ÄÄ·Áá �ø®8Éy€'ÔWnÁ�dgh†'yK¡õ·ÄGJ¼&K¥llcLleLŒy¼ÆdÆsuú”¬s•ꈈz]{l½ÝªgÁ³­÷¹ˆZeÐv’w‹¼¢"ÙÈ};¹¾ËŒpn]_Ø|Ɇr:‡Û"©m'Grõ&qò&ŠLt[xp§—úm8mÓ±Ýy�_væÄˆ€,raÆ~Ë—sžfydøœoظfç}Í–wlŒÉÍ}ÞìfC†‘öi½[“ Ìi�kt§7ƒOè¤ŒÇæ·våÛÇ\Ó‘œè=àç°vqû²a첑6�¬kz¾ú�ÇÈÃHiv^�´<;ÈžáçP½:�ïM›[�$ýéhþ¢*Æ„à¦9˱®á.>dô £�'ÿµ5I¨çl–æ*›˜ÚÏív�†€á/äCÖâÕ‡“ñÉx# Èàºâcr¤KÕD.ä<ÌZ_æ�¡ §–>޵¯ ÉHÊÔwä¦Ú¢SNå:±cû5³Øw4Ž¢ºifK~®�,Ç‹×yÖã¯zæhŽжæ4i‚yväYþÈFëÆo8æ'¨¶ ä}îj®_€§»DûáYþʼnº¤¼xGâ×gÁ|þè5éùf#xÃd÷vÉåþÈæªé‚‚]\‚e_kƒ‚kŒ‚>�‰nŽ>“•Š˜še‡on`^`�š‚§Š_£›”­µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇȾ^ÉÅÿ]]ÍÑÒÓÔÕÖ·i ÚÚclÄ\µ[²k][[è[[´]`è>ó™ƒèê[]k\ gnߺzƒÊ¼s¤/]¿.ø\�‘…ËÍ—}ý¾¬ÙòEм†ìmYsÑ!º/o¹ÙÒ˺ƒ>Îcw¨—}ˆ¶dZYÀåÈ�![Ø*`Átë08Rˆ°PÃ~ Å{º¡žß©ãw²Õ†~6ͽF¶¬Ù³hÓªM{o­-.`ÜÊ�Kwí‰mxä¦�–£€‚Ô åC�K—´nnY%èbÄþÔ¸Ò�h¼›ðf™0ªÈX¹€Uá-JsÝ íê¢ËŽ"—IöqñÿãJ‰®ìèÆ P7)c¢»­/­7eÀ�Ëô¦*¾å¡hÖº sàs°}0%4ß×-³…/ö®nvsØ%½°Rs.µ ʲÞ|7¶®ýûøóë—Û%èýD´ß€x^y�Œ@³jÜ…7HIð@8È<¬¸ãß k¨CÔM¶™,î äNv�1˜…·ÌãÞ îpÔ˜?ŽÌDQ>‹¹ÂÙ ;r×#C èÃçèt:7êcÕ@!Ö¢iƒŒ(IT7½xšŠ>´§‰`*î––ƒðƒÏF:Òáõ¨æšl¶‰_áä·RšnÖig4Ù ˆ &ür”.e⎕®¨“ØÁeÿÅ 8B¾‡¦Tž9 @ ÑÂ�-º²”­�i d°Ísã<(:è‘Ù5·á9‰^tã ‚Ù\™aŽ%Ø�ÓEªÒPS6…h+. rS¢ƒ;¤Œ"½êÃ8³ýY‹;tÞ)í´ÔVk‹B¸â笵Üv‹�žàîÕË­*! §F�&meËœÂnq˹›ÐÔk¨�¯¾k”Ø‹’¶œ¨h�àÙðyƒ ,lœ‚\ú–‘XirQxôÚrμ#iG%ÃŽ8FÇB¼ã<N È ?êíÊ,·ŒßM�Úç­.×lgžàjs±¼¬›Õl>×Ò×@õeª¡µJE³#ãŒâ% ½$õp½�ÿlËFèŗť0ªŽâÈ`jÙN=rˆìJekòÌ-MkÜÊ"-Jg¦^ÖÝÊv>�} `6.øàÈl„´}+qMøâtá¬'<÷âoLeB'ŽºEI®-¼2ÙbÁÛ VÀ$“”Aú$%ãw-‚…šqºèaÄ‚¤ xíKCʲniõ-A·bõÓ„Ð9+Árü±Aeª ß4Ú¢2ãÔWo½ÆŠ×ÕÅá×w�ãÚðiLƒr§«¬ ˆ},qÆ+å~šeZ·]¥Gn¨Iö^å(4²m¥ë»ä›ŽV5pD®a"³Ÿ£êÑ9àůw‚¢ÒÝ^—¼�åÍcÏ“ÎÅj±ºyÿïƒ ”–J¥8…ð„ÕpŸ y�c‚É]¬à‘9|¸ä|.ùPÚ:”ÉA&uYz .ÔÑ(!k[YáBä'U� Eà`ï¤(«):b(ÃÅjj¡� ðívRDžñ2¦0ç9B0ÑG¶Š5=ºñ�z�ƒ?8Ú1ƒ¸ Çý¥«ÄRƒbh¸>ÑðãU3�ŸÃVÐï*CádÜÓ¡%qÑrƒO\¥ô¨ ²È›‹×;KÆ(�˜rþŠ´F5Qv'»Ü¢è¶¼2 ±|ÑÕ<xÇ^úò,ˆ~Vò¿_ÓlèÆ4|‚®‚a À8²þmBÿ‹(n†Û¤6(ƒ'ÒA‘ÉiG_E¸yF}n1“ƒ:ôá:¹(ÏÜ€<ýøµ¾mŒ•¸ÉöÄ ™þT‘„+Zä[¸N‚’F˜«…óÄ&<öñOŽ�± @È& œc�î;¦HG †²IÑ(&IWZ¯¶I*dsñ²T$^2ë¨NL,cˆu¾Ä š©+pŠ.¼J0Ü;c‘Šª†¼™Q$n`ÏK¸ !Qö �i\�Ú $b)QCÄ%®ˆÛpˆ£™ð- ‘!JX¢>E\eS&"�,|E¾@ Nè÷ #ÜødÖògÑDæÏô -H®�‰'ØL‘(ƒÑ_%Îàˆ¢0˜yº•Nñ«DBàX÷Å-S»õ¬§AʆóÀñ”~øej¯³Fº²%Å€EéñS»I¦8¨­HB䎘)Ù B�„¾„aG�4|¿ƒ&tüƒ¢�(8úÑ}Ò�) AŠ? ä³£¹Ýð±<ßÑ›´)‚ïZ¾N¡ÒŽ{{ýôš ·šLˆú²€Oz ÆD»S¼ö¦DûÎ%ˆ[ÐvÿV'ÆÕÚ¸9¦ÚtM1V¼ÍJûkÚ^íMÕL:¬"ÕlÁ'-†Õôae›ãÏùZþ ›hŠcŠFÍ¿7Õl½Ð47@KmYÉôS2št$jøãyÄûMßzÔ+„Þh±=ýG¯· b  ¾0vît%ò?¡Rã€+–c*èÀ ó�úãM‚>š–wê²€\B0Í@•=’â!Rñf©g‚¨812a1$,ñ‚-8È3.1kµlŽâXQü7€Y×UùñDø=9³ âà ËD�¢|õ¢Q;´0½ÂAè� îâ$qâ­Dg�Q!‘#…Ö”E÷ÃV>ˆ#a†’²ÿ#^Xmm$E‰Qƒ ¥1©ñ†[b!\…‚W¤"[èU`˜„çЦ Rn‚ˆ ¨'{” E7m%oúðlø²/øb5Ô“˜/•X‰Vƒm¼09ÅÖVf4"•È/ø²Aˆ× *cЧX‰ÆŠúU‡�°#æ÷kGò#²èT¯S…;%;�v�‰Øز65ŒÉp K¸3U“V´$GòWh‡$> ……$Ó¸£ ofVtcFÖ´TÙ˜êW!ŽÓ˜²ø ¦=>ÃmµÓC¸èOãDò'Ì0CˆŒêöh íÃ�Ò Œ�Óg$F�â5v:¤c:™pdÀb$ é�ÿAév 3µŠÊ#Gf„éZ6LJš 2 y:¦C:&ƒ ð¨#*Ryó¸#ÜÈ#*V„>øh‚e¶�ný‘ò�=ù-{Ò'Áð„)ùKõ.ø [µðJGã”Lù |“”zÓy°C•Âq5*ó;AæóØxWŒ4µ’¬(�M††�ÐO+8%‡�8”ɦ±•bjt *d™Jv“�¦�mAdäs¹Óv³D‡)T’#A—fô‡ò#jT 9T‹åf–39‹iÈ�‰K%éE¦72¨,M$&­0C{™u/$”«å‹­ y¢ÆFAߨ�ÿ£œç 7!™ ¥ÿ¯¤1½yç0 øÂT•ÅŽ�[¢šÜQL»DóÐJÒÙ’ÞW2!�]“jY lY?˜É0îPœa(³)p ‚PiÑVë9 Éä ï–VcdÜFù´OÅ”C6N#ô+Ž"ñ¤žh%QqVOUfƒ&ádqé%µàQ¨q`Ž\Ð�Ñ4˜ûè…èO7&·X_2YšÍsPÓÁ1»”O_Àú —ñk Áa×'…X£µ…› �yç;„7#2B*~bIÖ˜?±TQ¦lEÊ‘“gÇ@eL/�ˆ¸GTÛ÷tˆ¢òH¦Zi„8Ùv?uÿ ŸÉ£È6îI‘TnÚ-QDî7t†•4ˆu—öñW±óYCRX¢E‹€Š „ª&>æ †…gS§ê ~: Ñu�J8‘?t:oq8#“Ê8A¨ ]—jióp¤* £ñb:UªÖ#œ7bƒ6£�–8J=nP%gt4êª�vA°Ù FT­ã«ÕS23²£Ô’HæE6É8Õ ^"s‰¬&O¦AcЫÄêєں8ouæF8Bv­ý®Ý³PÛ3¬åºcÁVPðê np äJqz¯;°k_7)8µ© ˆN©°û°«­‹€ k€ÿ BYÂUÁ-²�û± ²%!M°v®E‚ˆ!›²*»²Qedh øÒ¦Ðù¢_�dÐY+»³<Û³§€Žh§ ÓÚ³D[´»h©À#NFë2CÛ´PÛaªµj�jÎz]0je ûÐ: ¨f¯‚C$ ûjùÀb+\ôjYKµj6µn;XÓöUº ‹± >¦œé�­Ÿ“¶·%›¹•÷¡�Ë—‘q›c#t¸¸ÀnO ˜V©+g²{Ï£%²G·o$J¸€Dà6�™ �«¸:· « 83G Ê Ø†£F$mª ðvB˜{ ùVo÷€„;º &ºº+àÿv•[r�°p áp,Ñp™À�‘N­àp9§Ž¥ r.� ')7s`ø #Ts*Å^(Ç)GXQæíP §°¯ ã¼é” ┣àp™Ào ¾ë +ÑÛpÓ[½$7(²h=[ï! çã[¾Aöt�rugu‰Àù`¼' þû½‰s0Wð5Á÷ ‡Ùá 4×»Ôû½K�؈Íh7T�g75�à0¾ ¬Eµ6«±òw a8á`�y«x\Ñk�!Q1rxw]vyä´‡=Q�»!Ã…÷PéUSGcwCR’GVGÄDüt;4ùƒ!VAœhÇÿ #¶vTª¦’÷­Ö$wg\™X|\�w"Ü2$¬»&œ ¿0{ùwNχ{™°9áeÌ‘á·B¹w}/¥“ä‹|ØGjÚÑ}3B6ÈY‡0 ºP´ðXÊ!2Ó·;ô2ìx}û#Ý…@Ûȯ‘²!;ÃD 31®…A¹·‡Éø€’[ ïç{'Uç`ħ(¨�F•x¸Ìy<8‹ZÍ>°ˆLh”»ð„ºÚN`"‰<)ªhÒ(Šf„[)Ø$²4g¢C%ȃ$©ËvÈF¹ÀD朅G"Ɇd™JñcF‰θ#®C3ô<(úˆÏ¥ o°]ä:#*uÿ� S‚Zªs‡é´µŸŠÍ ]-ÚŒ(ü9ÈsKRhmKÉ ?„…?Fì�dܦÐòƒXè ¶uÄlj0—RÒ)ú<+ŽÐ–{¬Yé S‚+ž‚™õ*ÓD�\óH®²“@ê”6ßÙd•ª…² “Ùò!Ö„3Ò ÀŒÙ棹?æ4£Êàm0#¸ru]–xŠsu–æÛŠ–D3Â)l£6(Ë9n­5i�€m»Ö´Öc)0Gí’j§“#¥ž!Δ؉ù‚‰y­‰ì@Ùž:½fD9+Ö í2‹Hf�Âê¥aØ K“CБ�ëà åøh瘠³¹–¼0Û*ÿ2(Òxl,Ö$О¡ÛfrQèR1PyI¼L·ÖfNÒ8�£Ð¤[ƒ�°í4¿¶œÚÜÍ-¯Gh vö)K©-ͱœ¬(LhFh’‰;·`’šÖ)lÅHØZR§ÒOh ){=Å Ú‘£fÔö�# ÂW“sŒ§a* Oî½™à,t*éÞùå˜,ØÝn'Ù„â�<Ï¢KRƹ`À*Æ9/Åæ ÜÕ�!ÆMƒ33—"ƾ�âQyš:"LÑ3ܺÙx¶§Z)aµ��´¹Ö&oëxø0ãľ ˜016–“ŒÇþ¬~K°y{Å0QDâ)øºÕÐÙÿ¼ú\ä“Üd9=‰µÀ¡+âR ÁI—LÞ† —¹“‚]*Œ@3¥ iøÜ ÝÅ13MQ‡ M·hÑ;Ř‚zÎQDÑÃ�kÞ€›åtAd76etQ;ŸÈ€”cTÕÅ5ðÕfióQžùÒ€¢+ú`¢ºBDØÙ$Ƕê@(Ê© ®žwýàymÈÂÑQ&@Žy£P¢Il·ëHA§3¥äâÅ�¼™Iç@-ìÃyí†WÃÆÌéøÀ§ <‡µ)áÁä.¸‰® �®„&ÏäP µQ5NEЊªêt4³QêªMí¡¡æíŸÐ$ÅÍ58… Ê!ÿN+ç�j�š>¯¥M9dšä4§OótQ ŸæËG Þ誔øT ºâŸ Qûi ó?³> êpµíN^�dïɾ·Ö`.˜å]f�FćµOëФxKN¦-�:U×0Ë%ԦغiS/#{ˆ"XökäœÊŽ‹RµTuAyíõ)ÔÎธ¤¾ ZõH*Žw?…´Ã=¿‚±£MöXøÅ0Y…hY�[»ùu‚u¨~ôã²§Á`Yª YÀ°ÜhñX@DzzY C'¢ï „õш¾‘雀s=¡™Ç×Öúƒ3'‘ÿú‰O’—ëûöA‚šŠûƒ3¯‡ (ËÂ3¹Óf[NüúÁ\ÝÚÐÉðÔ½Dýú!œ8/�Î_3 "!�`?#6äE¿ýwB^\­Åi ï~Gôœ2¼ /4¡äß2áÑ^0ò¢*Á³¡òó>‚ƒ„…†‡ˆ‰Š‹‰k`^\]_eŒ•„nj–›œ�žŸ„oš ¥œ¢^]\^¦®¯°±²³´µ¶·¸¦[k‚^ƒn>[o„­„[[‚j[¹ÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáØnnÁ‚k^‚\¼ƒÎ„änÇâ÷øùúûüýþÿ 8P R‚ÞóáNP††ñÀ,$H±¢Å‹ÿ3jÜȱ£ÇnŠ}I²¤É“(Sª\ÉÒP™_-cÊœI³¦Í›8©A´æebÎ|>v)‹¨Ð£H“Z[³Æ¨7k‚ÆRøi�¦RK1½jÊ ˜/„¬îDd5k!¨\¯Q¥e”ØYh­¦uõ¦žRFôl PÀç›.6½Ô»· 9Sº´-ù7°¢ \ß�\sC0…c®Uf§ [¾ØbÏU0`ˆ•ê²N�Ö¨¹,ê † ÂCk –t­s-.ð`aÓ…R¬Ë©¿¤–¼)@k”NMåÞRƒ—½ß„®´¦K²`0à²7¸§Ôc¡oW´œ²û£e6Šo«Ü'7[(©éÿw sDø5ô…l‚|áØ!¯-óÜ!Õ¹æß LEc-·ÅR€q°ð‡%ÂRF‡&¥¡mÀrB y6ŒŠdé%Q!exw`'íÅDÝ"è½§#Nj€¡FÅ·Fé]ò£ ©� •Ad(jÙ–vÆ-(Wv?®±�D?úðF”¡€èÃ�O6ÙÔ� e0ãxïÌXÁPZ!4ä/Q9 U“P1É'š�‰¦HCÎå‰@–‘ä�)ÙgCnD©É�Â8YÈ$„Dég“HW”¼³!C_Y*/–¾Q¦—GòE…#‰¨Àˆl¸²Å‚>�7ˆv`)òy⬕äØ~ÿ½&2áŽÌÎÄÌ…—-së)H“Œ&Œ#Á€Q€&ˆ–± xel`›²ùù€½°�¹À€qŽh™ì‚¨ŠøMôá(\P2¤wà0H_ozñ®… Ó ˆTË�sä­³ÆÓÖ£`™|… I]ŒÊ<å$»‡’L÷Ê÷/_`ìhĸƘ•ñ*!‚ìK0­°º^´gT2òâ)óaT<ÇEÌ_�,ˆÍ(É:«cÔZÕûPF`ò€fØ(>Èã5¦ ­ˆ"Ê^|‘A1eÌmNbHg0ç–q†` Ï»†l ZuA­ñ�€U7"zsÂ8xÿ¤„½ÅÓ•íŒÍúùó˜âÙ…ÈÉÕá™ùÔ¢ÍÙìêÞˆëZkex³²"uÑ�Æ "µg¡›Ì.’¢ñÃɼøŠiœžqѵROíiŠïÜÜ©öÔ^Æ^`kj´æÃèF;„D¿Ép®ùD!A‡¢[:ËlD˜½Ð‚©X2*!Î+ðÔ!Ã9È2ÄB›šý`ÇEEüñ6¬ù¼Ö5/Üf !›�ýð,G`Ë…–4F*ºÀšçüo`kR�­ÑpŒj'±Úg†t‚b/:K9¨3!ÞoL\ûLyD»xGTl³W’öb�ðs é—k¨Å Bƒ ÙÏ3½– inO}£qGÚÔX táG6 Ád_²Â_^ïAø)�¨Ò–»óq-I£¹Ž30é…b¤¦ŒZ (5Þ=EVÑZЫBƧ¾Áã âÿA—qØ…1Ch' ²ã%@$Æm='Y™âÓ‘IC€C2LÄ1uæ5M¨�n‹¬Ÿîñœ“†tI%!’/Ùï69KGª¿ÿl/XÈ3„*¯e­Ù…h–P%‚ 6ñšdx ¼JŸÚ2Rÿi0(ö‘±Ìh=Ý¥‹!tJ/W¾1Âo •ÃÎàºÑçŒSDDr#³Î(GÚ¡+!Òv Š45Ô›"<0¦:�:ödÚ/Dñ"8Bn8+Ç€ã†7l–q~+‡bt‡ͪ³$…‡ÁÀ"‰rHñ)r"0Õa¤Ó^×ê‰9SŽužÅ­š§=^‘>Ž(ë‚”ÿÁ˜6‘=%ä4 ݪeŽd­Ë'ö:ÇBD»,uÒ%¼�XB̑˧Ç'qdZ+Ôð¨g¼õ°�:4™b¼FQÙ Noƒ2ÔÝŠªNPéBp¨��œªvà&6PÂt­¯ìÑ]71¤’T¬ º¡!ðŠ¥'š\uIÙ5Å®1¤iÃ�–s"D¬'ÂF¡˜¨°hç8ˆ|äÙ„Ëãi°7bÅPâ­’q Ä¥›- \20– æM&�é¦OŒ,Aà鳊zéÕZŸHŠ^î˜BN†ý¬Jž.93 ¢‘�¬"LîØÉÆ‹$ä ®ã%ämÅdÁ[‘"ˆ¿›Hj,ÿœB¹"o‚ë Øï ¢f”•™b�1¡U÷|¹Œ)úÌŠaºÛÍùè%ˆx"�»®Iè�ÈÁg(0^0à"d€´šÆ8«% H ß®LK®¯cF†‰@áD„w ˜Ò¶š$rP´£""¾ žof»ö…ˆ’µ8=ÎöQ<›Ô–Íe¹H ŒÆ"[�:²è‘%"™ˆïNæi*9õSý ò¸ƒÃésáE\½Ijk5‚Þþ [’@î6ð>P ïÛæJF{] @pï™Ì>„³Õ%XC€ɼ@µMr¡L|e½s60I:õS]»7,ºU#ž¥Te1Åÿ:‚tã(÷õ"h|ˆëÚzú|—Ò—¢+•@×QµŒËVl±‹ˆ7‡‘w6ˆ¬|_ xø6„\–bD¤Í<^ùaÉ×~u¢FN4Á€Ý­×±Ó�í$É9-§ ë$î™ag‡¯)ÖXcÀCW ÃðÑ‹ÔÙ“…­pîx!òÊÍ‘{øÄ®Mpv ô=/äÅésÙÃÔÕkÎï¨_‰X²þ¨Dhm¹N}¼gÕ@Wœ½ŠEùÏ}½Vr^ùeÛl2Íï~h3 á÷uÄ †}¨W=?D=Ü Œa(úÛk¶¯ zݹì P”Š[�šŠËþüèO¿úAÁ†¬Á“‘`‡2nlaÿÐêà|Õ§8|€‹)5&~fUFô´A®Tj €•‡+„0�ANê¤W²z”7Sxðn’S QMTl^bLë7‚$X‚&ø V„u£ö�Uå#tc¢\€P7JÔ5›A4Ø44užG'—£‚ ÆDa1R´.H o5ƒò h×WuД 6aˆp“�6Dfdq‘g[ÑæcGV'[r9 ¡"Çr¥w‚l؆nx‚jÖMHÅy0è%r7BAeR1L>yc‡{ñÑñ-ó;pgcñ°‡c4k¸F0x†¾"d¶Cq/ølîÐ}[ˆ€fv+æ X€ÈooÿŠ¢8Š%G)±)«’i@Âzé�ŠFó—%$R⊕€} )5 m#2HRNxTªFŠÄXŒÆxŒÏpN‡@1äô Þ1Œâ`mƒˆÈX�Öx�Ø(Ô„hp醉ú`O3¢À’�æxŽè˜Žx9©`U¬¨ ø9hL�� #§Žø˜�úx‚Ž ]�‡ €_G ÕT`c}�_z Õ” FuÏà ²b•ÐyÏ&h¹qx°¶QnBptE­±Ö©Ôó,øW˜ïú«Že1Œˆ¬ñžCÉ é°¡¬&è£P¤8b=˜PŽíêÁ±L]7­í�kW±G±e¯ªs±��Z"z!¦±¹æ’gšT5³PQ˜Êˆ¢‡mQ˜�+»©Ñ¥üú؃e+± _ÄÙ4#»ÿIK‚ {5ôö¬)‡Iüñ r#T‹ 7r±ò:AS+¬5v¥�‰š‰P¡Tg²_¸�ˆµµ£êªd|^›0` rŠö^‘§¶£ú³¬Ã ûã«!ˆ¨„'�g:C¨ñrÇØ´h@™þ7ÖC�‡Ž‹ÛWiÈtJ‡†} yqÙ¶ÎXªkø…ïš6dh¹ïR "ö…‘{�§š®QˆÎ@X¤û.¶ˆ·Íbus•3·;s1$GØW!(¸[‚̪R¥¸b˜r‡¶Kä“{øÑ¼T䂤©3ö…§$·ªÛ�Âfq —0 ƒl›ë6>Ნ»aÖ L ›¯²qµ•´û¾ð‹ ²DÿˆšŸÊKkûI,Þá-föV©\ÕXƒ&A#·|Ä6rb»½Ý:y\ÈMà¦AÑÑCRöå+v�y$vD³q¼a"¿ Â× +aÀ°}—rq†¾’åz9+,¬ö{QhJP„Tˆ•¾Jõx®Ü»†Éw eÆ»ª3òFüF³5ülšû�Ç#ÁÒÂ�Uå³"<ÅTì í§5(¨7z¡gß1s‡¸¢fró5³FÉÒ·3xÓFÑha\¦”Ǻ!…RL²e{F¸%T�§ï⺱–�ée0‰„µ�½Ìð4 ôD†‚\Å’<É÷�¶{± ssaw47ÿA£†Æ…Àj†ØX,úD„ÃL…ŽÔÃUÊœšjü;wRý“½k³É§\²CnËJÉÀÌQ*’AÌœÀ(‹P*Rl ¨¸‹ˆB,‹Ù­R QbÏ,ÌØœÍ9Eë͡¸´p¦½¼ÌÇV‡™6ƒž0ä\>âÊèÜ ßâ áó ñìÍ?Göœ�æ �„Û1w[ Éà {º ×a°¯zÁg ¡Ô Þã ^`ÐùÑ�WôÜÏGÑ(WR–0Ð ýÏŒ@«‚™Ð’„ÑÒãŒ$=Ñ(=ÑûÜnMjžÐ -ÒŒPÐ/ÝÒ{Æ·ñ)Ð+-l'�Ò>íÍ \ AË ˜´ÎªÿÑ•ÀÑ’äÑCgÓ‘‡ÓbwÒ Ý 8ûÓV}Õ¹RÑž€I' ÓÉÕ´ÌÔý†Ð` ?Fí5;�ÓX½Ö)�Ö† ™Õ'Ö‰àÕ‚!Ó‹@Ó�ÒP[ÖǪÓ&ÍÖ€-Ñnír[-×U)ÐvMS†­ºN €-Õƒ]°�=ÙÞÙãÔ\ýÒHÍJ=Ó‹�ÃdíÐg=ÕœðД}Ú¼ m¹Ú¬-4PÔÚ°-4ÞQJ±ÝÚVUÛ­íl‰ÛASJ]ÌÛ¬½À½Ú28ÜmùÚÆÝÛ·’ÜeÙÓ�%”‹#K û�Ñ©pÖ�¡Õé×UÕÙ� 1âÜ‚QÕL hv{Þè�Þê½ÞìÝÞîýÞðÿ߆ØÍÚ¶hŽt±@Ç‹Àí¼Qýíué7¦Óнò¢Dù˜³¾'.NÒàþàá>á^á~ážá¾áÞá~Ö=vB}*µPX ¾ÊF7‡)s–íXÓû ¸x²Öpâ´@à¨}ãØ@dûuæ!ŽC¡‘ºT…¼^à-þ 6þDN ¸x 4ŽãPþ ÔÑä»r+'Î7ˆƒÞ’KzC1õà7í<û Mª³»Ÿ“[nj 7P¤7å’É·^™Ü#´ANt^:‡89¹z®b§“Æ¡8¡Ò8qcafS6��刀å¾ äà£Àæ±…¬X~�a‘9“S虑ÿ^®7Âpæ:Ãé}ã„£8çiRÙ{®9ª“^‚œ–e eþ]Nžþ2”8�ò�¤_ÖÁ—Qì܉±‡�báË1TžZCÂ@põ¼\UF?lzìÀÌ^ìW�ˆyè¡DªVê¢zÙ¢‰­Û­f³þ]² Z¢aA±lfªƒì(d�¤!qþù;t'ïø­îOD³|Ôs6 u +äM[8¢fPëÎBúã^£B\–³GÆÎÎa&$N†Èî'ºV„B"ìP~#e·èÎ r’MÞ!¨©^‚ ¤Ñ]‡0À!(tý =vÕÄ � ¯ �…àDW\É5îÀÿd.ô+÷”l:C›±R…L<ú�¡ÐAê áVVzòËà eò~£Mò�X -Z>¯jnçb?CÞñïK:F®õòa…ýR o fÁÑN áe@5¥XßÇ`›”Oúz�!5OD¡âòóФm¾lê¦VZN”ÐC òõ!ÞÔྎ•µzMm››ó ./¦Œø–ÀàBÌø[’Ç€o"kú*åBÜ íuka !«š¼c¶ F,>øš0õ.ÈãJtº=ÖVcÑ¿©�0/nVÍ/X"ÒÌðž]E¹Áj¥€2R±Ïñ!›Úiè>�Ü�©Û…li£ì]OO8ÿþ�W�€#�—�ù³@Žà\ ŸE‚}_[k>…>]n>e[…[†“k[]…[\Š“…Œœ >�ކ• Œ_j[§…o[_˜š¡¡ˆ“«±†`[e…»¹ ][Ц´\¾�´“»¤“ŒÎ†¯¹Œ\´[Ÿ‡Ù¡nÈ>‘¡•—>»Ú“]ÖËâ’ å�™´è†Ð†n°…[j¡�` qÊËCß6qÇR:Nü�eÖªV£YštLá$†…„ùƒ·Á㲓(Sª\ɲ¥Ë—0Aaðâe䤙ïb¶Tófš^ÀDÓy�&F¢…ÔøbÒÇ—ž>Ôxù"Q¥š^ª®dº°KÿS¤`Ê{’UÒ- UJ{ÏGDZÂ’qÉ`²íI‘kòæØTPÂIfÏm @7¥-ŽÞ ç´-qŽÜé�¼f—¿5XÙì–YÕ$Šœ^µb”“S¥\—½}[ë`c­!‡bl‹bÒ´ŠuâV(_«oË6’mH±À ò±³Ë)Ê“x™ä7'»¾K!ç¤|w0_ÉŠO¾<Ë »Ò~3Ýü¤G} bo)(�3Ù�u(Á× c©ªW\,üp±Y‚é©ndI:ŸâaT™m·O‰O¦Äˆ¢_h FÊ©!Ì"o Ì'#ÂVÂá°üÉ+$b Æ@>eè{trñ0D:Ͷœ`cñÕo÷&�%S !j‚Â×nLâ3.(�Mâ�Óó‚¬OhcÃ%ì…¸03°!R9”4_(ª¦«ìY÷‘È>£é6ᤈep±ƒ%'ëœÄx²^nÀ»1ü6•Ê¢6±`„äéåóè-÷2n̺v3×!I¦Û£Ÿõ9?@cmûí-‘øóÌUéëR.7RíjÍÜË.û•¡¼Â:+ï§k'1êÿ6ê ˜O2bæÅß ¿Ào”áÙ{@ éð¼,RÆ“‘ [ö^'Š4¡ôÝ$õ¯DÐ�µãî?YÛAÞ1 GŠU(l7±ØüÌ…¡»Ý$}œÑ?ü�‰äI� y\-„•#AlfŸ[›+Ð4+³™£Î(­Rh–z�b£!‘³D"�·(0 E*𕥓äðN¹€ÕÔá8}+ÑZ“6ÂV5l„IYNÛ2EsMj‚ÛÐʽ܆¬'†"Š>ЖMÂõ¿2š±Lþá0ƒ3•Lª G™ŠæRN€Á&ˆ]ëJ qT¥Ú˜™"üØ$õÜ"@K�!àhƒõ«�éfë„D’â]×× ñégV9ÊÌ6ðØL€z 1ë7V½¾bwˆÛÝöˆAª2=~GHÖìú´± tk… ?ë61éŸìµ×^ h·¹ÀQŽ‘L‡ê7,&Hl Å Ä™]ªqÓ(Oxá£�ÍñŽ“EŒÖ N…ŒËÿ�äO¹ÊU¾”�Ô-%Â*‰2XÅ9Í®" ȵxÒ£@Š,=Y×SÇ…�ýAÚ32±‘Êð'5ŽÃÇ$JK%d<3ûÊ8µ\™‰<²‹"Ãh;ÚÛŽu…T¢¢+O»ÚCñÞû6©(Ññ@—ºöºÿÛýîÿS°+á pwBâà39Âs«nTºð+¡#Þ_wR'Åõ–1Lî?”3þò˜Ï|ªòØ^´›gxHqÃÌŸœÑ¯Ä ¤Õ¼ê×ë+Zðdã�D=³;1á2Ê~õ¸Ï½îò „ÝÊØþ#ó©²å…áïþøÈO¾ò—Ïüæ‹]d½îüê[ÿúØÏ¾ö·?PBë…ûà¿øÇOþò›ÿüèO¿ú×Ïþö»ÿýð�¿üçOÿúÛÿþøÏ¿þ÷Ïÿþûÿ°÷8€xõ˜¿d|/�)’¢kfÿàî1<)#©!B%XH(6ãyû@\8‚$ø"·Hÿ*s4aå@·YVed6…îãW„ò‚Kw‚Bw Fy%X„Fh~i fÐa‘‚ý"*„‚P#2Èq4ø- HÎÆ=�fShDƒby=t„fx†è—„  dÀH� 3´‚.ñ:D‘R^R{Ív…A3{0!Uœô‚ (L„"„j|h˜ˆŠx|j¸† 0nNx2BQQ¡ƒ€{jðqHQØBe>PqQ‹ )À§ÈWcRð!)Ÿ‘�hãyÀ‰€Ä�Ñzk0*¶¨j˜Ú ‡ìô_¸xŽP©Çµ‡Ë‘z»Xo TÀ8ÿŸ >§…нBˆ»ØŠVnDZ!2=Þh� H³h>µCŽû¶ˆîøŽv×ˆŽ¨b�01‰‡á×r�âó AÒ0��2’ *v&62_"s"e…°[nô…jAMòÕ+¼�&Xu�{#sÙ`Â’0Ò`VaÑÐ ºC»t Ê@]³a)c¶1l“ =²=_RtƒA"ù)°XcU5 E�– ‡Í•2ØB"x�Nù”9%�ó¨ap.��à�ƒ±Šñ[5+op,n™€ 1)Ï3)ÒŽnàmùpÇ;Ùng•‡r+åów«ð!›$ô´ÿ–�–›!—#�zßT¤SŽëcC;b$2qS¨ÓTÈÅT¹À�uSÔÄ–n©qY$…¹U!w˜#T¦C–š 8šÐT˜üà‚Ð ¡•¶y›¾%•S©D`,�•ß’hAã ¬£]oƒ£ƒÌ" 0‚I ßõCRBޤ6Kæ__Ó1ÁØZga/-8uÃ\ vVxVD»Sµ¢Ô&�%¢cRµ Ñœ¬ÂÐ ?‘Oe)èNßFúdƒaXD.3òh¸™  z5'°›j�*1‰ò-Ÿ3.• P‚Ð ª]¤0朼ñ 7›wh6ƒQOUÿxn WÐsMÚE¯Pæ@¢ó=ûåk‘$Èåx )…I%¢-I›Kö'U8œ¶RØŒó0LšE Z¥Vz; ê Žx'(Mqˆ/~6YwC¿ñÐ�`Èu… ú §A qU'þxGÖŸÞTiah  Ð%þÔ^ÄAH†¤¶ä“ø§.iŒ1¡3¢Ü’z§JÑë õɦ-¸i�¨º 9!t¨9|º! {[Œðˆx¥¦zªH¡›Žˆ]úBC¤‚G$k‚Qe°%'(¾|9^‚ªz»/‘ÄªÆ Ô”‹)c?"‘Ùª´=r &™“H7ãPÞ ÎÑb�áÊgå]«¥¼ ¶S³ÙÙú àx–]é…k¢�¨þЖ¢H�¹Vóå3.‡¶sã‚‚åÈg稪óØ›+Ñ£Áæ#¥àŸŠk7�2Œ:¶ �RTä çÎ]ãCç²ãɇ瘥Z¾†*"W-j¹¼¡êUÜ 2¤¥¡ßw£ŸŽß�N1.4†Ž{–>€&�é À¥}>Icݼ]àŸxLíeÓ©hQg)¶Yí­NY½êÌÝÿêª"é2ë¹Gé#ˆé»ÉªŸ”4OÒíÝn'tHáÄkDï{Ä•‰­�Ç íìÕ í©"íbíÇGíÿ÷¯¾ Œ‹éI¶«›"P’¦—г’y¿ÉаÂîï-çXóêuë«Gïþºç\)­ð‰œc�ÀP¹ÊP ’€©�[ðwÒ“ßÜÖzò(Ÿò*¿ò,ßò.ÿò0ó2ÿò¾†a°éDaÆGè\ô@§RÆŽïRöŸìý¬_òá­ÒÊk ÆQNÿôMßôÉFõPÿôNojW_õRõ^õSöRßõ\¯õS_ö__öbÿõ]¿õdïö †öµö`ÿöYŸöoÿöO¯Þñ×À‘(7cðcE‡>0Vò 0FLº¬ÎŽôH®ô�ùê·-ŽåÈY%DÆ…½Ð„êêþø¦_û¶�}*½ÛQáÓ·ßû¾ÿûÀüÂ?üĿܣÆsŸÖ—üuQüÎÿü…¢�²#WÇx&†0ÈÉ÷пýÚ‡3_2�!<±(ãE X!ƾVÙ(1áÿ�FJñvPøÉÅ…­…8QDÑc@^_>„…†‡>_ˆ>e[e‡n]]oŒ–—˜™š›œ�žŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²£n[`ˆoÿk¶³„‹¥n^…`[•¤^n›nº·‘kÁ�žnÈň_]kÞ\ɶÞ\Ì¢ŽeÞÞÍk_»ˆéŒ`ç¼÷øùúûüýþÿ ˜"ƒ3•�–¨Ë>„‡ PëÄeм-‘¶ØS³Y(7 }<åȧ]6@$$�‘— cÊœI³¦Í›8sêTµåK�-[ ¨)dëÝ–eš‚5ȨP \|¨ à.@'mZ㥆^–¦$T (5?£J¥ t¢¥@CŠôt¨¡/n …íiò�1KÚ ©éjˆËTkê‘Ô(je£‘Œ0`ÜI¹²å˘3kÞœÐi™ ]ÿyÔW‹˜á-Ã…Ú›…�jm°[¦¶CC*Á|ƺu²®_µVÿ¤f»KPÐ…j; ©ºvkCžC�&$ÑËÝ·É2¾xH�ÜnÀÀÐÆn‹ñÓ&59n2ç÷ðãËŸO¿þÀ£…ÜT$´Em" !ƒVD_`@ž!Fvd‡‰_�d°]#öp“^Žñ'W^Èaú¡–H^&÷!º\X!ÞòÅt¢ æE¿”'Q\t‘&ë¹w{öÁçQ�Diä‘A"˜A`$ˆÉ€Á…“ú9Yˆƒ>$N^™p 7‘D9c‚^øÇÈJPJ™ ““Q——](·\~mfSg#rÿya—)¿ÑÂ�.ápÒ#—,ùˆäNÞ±Õ0Jç,xøJ„Ê‹$á¬q ¥œvJs“1i%"n¤U€Vf ו¸4¸E�jrª_b*=dJzPH¥öwêš 9bë›K-ÅP-•®ˆ¢®à¡g*¨v2¨%*íèé)C¢bT``ðÅ^!©´©+\ËXUzí+AeÙ_£ëÆkŸ’¡2Ô¤&_@ W‡$¦«^Ýê†dê"YŸ(ê«™«BR¾ûKL�xEô¯!^vgb†èbíI*ûI¤ŽjÉ´Œ|a¦¼¥¤¡m¤bœ�®ñ€ŽŒÛJ+«b�Â,Ëâ—ESÿ‰ôÑòõ—ß~"Ùë¤w{úàÅŸN‚£†ˆ¬RÌtÜ FÔ„pôÎF!E«Wß"H5tj½Á´›‡HhϪ¬ñ!~Åšï�P3Ò7Q"v¡ëlŒ€uŽ…ê5Øi·G¨5±"-ŠË ¼lƒ)?¡íC1±~Ì =§‚ã’«â—‡Z–®:f@ášÑÍ5ÂMÔaI:žmúªáF*»Q �ìn[mç'lnÕÎH8˜¥°µ¶†s«M'|sÅ_ƒ{vµÎšÑÓ—áðeÌíEmïÔ˜eÆvfs�’m–¹Öµ[H×–A`3ψ.¶ûK9t„ Wõ«6=©Í� Es¬.ÿ'¨œÇÀ†Å˜ŒÐÐÔh|!»‰¼aߢ[z6 35d(b¾`7L±‰*T‹ÈSD5` I´ÄÀ© ž7z c¢†5¨�{j@†œÜðFutLwäžÕ±»7F9oáž ñ˜¥6¾‘�‰C$n P³:²|ެ�õùF{„ï‘£yäžö(H;&Ž; #±5oX’ŒØWÓ/EÒ'ÇPá„ Q¾~e ŒBWfyI@fÉÊ˺à%®1ÿ” Å�†P„^—PÉ/ÊUŒ-`C Xé["—B;�&43Ϋؒi"4ÕÔÙüg"�á¢'›³fUÞbæ9E NØÂ–çü¨Ÿ@áߨÞqŽfŸP�Š[Êu‘ÃÔ"R… ØÞ`”WÝÓ~Âá17ÊQy•AWâCÕKšAÍ}"&õDI9!RTÁÈ _ ]G-A9eJ0 'øDó…X‰–x‰‡gH `3 Ç Ägìó eÐv%÷Àp†‰¬ØŠn8+·ˆ¨ 2¯À<È0°àtaÓl«`BfãŠÀŒnˆ2 ´è ¡Q<îå _ ‹§p ¿(ŒÒ8�5X ÌÁ‹ªpŒ²€j÷°‰±àBR³ŠÔ8ŽäÈqóÇâøŒŸø ž5‰M"‰­àE„�åX�ö¸qçØؘ Ú Ïr6“ˆ¥éx�y�"–ÿ�¥È ýè "�ü¸�¥ŽóX�y‘)/Sã A‰ž�nšÐ�—`}yx 0 ƒQrš€0,åŒF9ô¸ Ž˜‘4Y“ô!w›p´’–�k™ ’c†‡+ìÀ ù¨ ¡� í(( ‰âqXgç çc“TY•œ�“øâ Aä >‰ @yBÉ"ÓA‘›p”™@w�—€•i‘ó–V—rlù °, °w—àFaщ£g'pI“'i".Égså; ’gâ–(Â\P’.!™sY™–Éu™ V´•ŸÐ•¬P pY fy^Q²„ᘥ0•—¹š¬Ù™ù ž¹  ™ÿŠ-Æ ¯é ªÙšº¹›±p›­ —­0›¼0šì‚š¤ÐU¼™œÊ™ ¾ù9ù œ³@œ®Ðœ«�›Ëy�ØÙ–ù›Ù¸Œ­ �ghœ£`�ÙYžæyšÛùœÞÉ ài›â) äyžòYž¯ÒRöyŸø™Ÿú‰*G±ŸþùŸø‰Ä êŸÆ1%š ùYŸ Ú ö 4ó¡Ù™yúW¡z¡š¡º¡Ú¡ú¡ ¢":¢ú—z¢—é;nð*ê;oÀ¢* £1:£0*£3ê¢+z£:ú¢:º£9Š£-*£,Z£- ¤9*¤EФHZ¤?ú£K¤Lº¢6j£MJ¥Kú¤=ª£(º¥\Úÿ¥^ú¥`¦b:¦dZ¦fz¦hš¦jº¦3±;¤È¦p§k„M… Í‘rš§Âˆ¨Â8|ŠœBCE¦ð¿"R……ë$¨*×/ ʀ*Ä„’@@zZ©÷¨*+�©‡I ,I âñÍ8“ÑD™²0ñð3Bª`ô”âsSb©°j�R›ªÀ˜Ÿ$š°‘ÿ@3xc4ḠÕq ¸«Ä Œã@<ãF¤6jЬƒ…„‡4¯QIº­Ëún!I{VÎq<`ÃIŸ·­ˆ˜HMØijG´ã>Sg«Áz vS¬ðj¬A±Ó^°`T– „‹�ƒP ð…@r¡ µQë¸ÿ@Ùj¯¦1Âèu¤‰Úƒ s>ûóQ¿0#”‚8�TŠ�†¯òµBˆ0Š¿š ß‚èá8ñš²˜(wÞBjÁ/e™Tº€\0.Á™ Ò ¸zpߢ6·'u¬Ä«B´´‘WR³gº±+w±ŠP³¬—#é1¬*[µh)¢!(R�c4&Cɨg"ÖÐC¡¹³N㳦u<µ�K ’í 'r‚€ªrŒ#·«XKKUUTkµ|«‡Q �³šÙ}j’дâc²Og fK S?ú ÁtN¹Me’¶Ÿ@�*aš}ºcˆµm‚‹ 3�X"{EXBÙÿ¸ ò¸aCé¦ GAL¶Â“DkêºTìjZNò®¢[¼uxp-«µYb%j Eº:²÷ãùJerѪWƒ°Ôë¸h›¹QäšÂ]pˆÇ7ts¹–`%³`¬6œicãvo+Æ[¿sè=åðB±“q#eBJÚsXܳ;òÀ³± „·ôP†ÈS@ UN§@�Q1¼cŠF#¼Ãù¢¥ø iY`³Á¼ó K)ðb½Zñq<Æ-Ì0@&ñ´xj¿4\§‘M�B¡ò„|Äð/Õô['ÁE¡Í[¡Q·ý1�]çhÚFÅÒ~r+#%­Sÿ·sKÅvÿ6ú÷/µ0.ç1.Õä1×:&Qo5œÆVûÅwœœ°rñð1û“`x;w¶ªÆx,§øy‚G„&a½»’ ›Ç„¼¦’Á-Mò>…¼ÈŒ< Pr*QÙÈ’<É”\É–|ɘœÉš¼ÉœÜÉžüɠʢ<ʤ\ʦ|ʨœÊª¼Ê¬ÜÊ®üʰ˲<Ë´\˶|˸œËº¼Ë¼Ü˾üËÀ|Ê �KÌ'”²;Û7 ‡É�ȪÃy¯ršÁ<ÍÿP*Ì�/×: MÆhÁ�ÍÈðEÊ\¡ïIÍèÌ Šj ÜI Þ!Å\ p 6 cÆÈÀµÈéÜÏQ.£�ƯΦÀÿ§UtÎI�·n3 ê“JþüÐ5Á�çÁÝ2Ãáz|iѤ°˜ù ¨ýѱ”� 8ÏÚ|uj !ù�‹ Ò0½ÿøˆî†Ð+ÙŒöªÑ¢`3w,::Ó@�”Š�Å´¼Ê÷ˆí …)ÔNMJG /� ÁÐÓÉAia9‡éOJ§–éûž[¬R&�8¨I’OÓaaÑ0EÙ›Z'Щ Ž`‘_ö¼›žÀ3²©ñY–›jwÒ|Öº\kŸÐ"ÄÏ"½ "‚?ŸE�D‘ ^}&Ke‚Ö�Ðײò×&Ø �Öž€MOI¼ný 59�m‘œn‰×�ЮÖÿ sÛ Í qÍÙýìÙ¡™Ø+I) ±©s] ¸TS4Ã¬Í 0=ã Û�Ͷ PÛ¶�θíÈ´ŠÞщ ÜŸ )ï+-“mݪBÐRØBÛëݾ<ݲ`�ø Ý÷pܼÛö|ÞèÍË„Í ¼ îÍ ð= ˜í�ô]߃½oçRÝ©°ß³ÐßÛ(Þ£ÝþËê ùMŠŽÍœß­ ÿœþà·ê  z  âç1â n¯&Þ ¤•â J�Íá° Å$:ã4^ã6~ã8žãjx0ÌÑüã@äB>äD^äF~äHžäJ¾äLÞäGþÓ=åR>åT^åV~åXžÿåZ¾å\Þå˜è¦‘ '�ÍñàåòiJ…„çYmäFc­imþ .â#Æ_Ã=ãFéæmÞYnÞ ¹ðç:è †ÄrŽ>³ô bÕf^™ª²gªbÓ�…´£0FŪÅ\)S!la”7ájÝ�^réúª(| oÀ3â1ÓOÒ<«.Ž=Ú×ö;â6MÝèrÙl·N Œž½BÇÖTq6�Šó�2Ü K̸¿ž¬”7! È’cù/%¡jPŽë4©ë˜1í9üõ•ÒK„ƒ, †ýÃ_#¥^p�,­™âL¡9Ô&ò1ü¬í»é}uán\•ˆÇµ,Rý´ÿ·7”l¡Èm´w9]´]ì"bªo±cáðHi|!añ-yGÐÇÛl öú½f�» Ïų´¸¡>1‰²�@çà‘ý?í» øªj¶öâøN“óZ¾ºh [–Çï»3•KG).»qáåӓĘOûìHÚ¡–Ž÷Ê<;�×õdQvÅØ»ã%‡Au¬*R’c+×G�X‰XR 2ÕxÃé½~îíñ1¼³óÊénƒòÕ³[q%’âúa³ÕË uêíÐaˆì´ä�!8.ò³…"´ÌðkV|òŽ@;}¬ âÜ£"“/&êÿ5 ÃreÏDÕõ<ÕØÎ¸<)øs©•m’÷ b²a†`ëVçºVDž!1Áa,V0+äµwaj; R(‹6V6Þ¦ ‡Qú%‚î{¹ìšÉ6’B–àÜ©™ð´Ìø»Ïûqùz¿Ï½I:¸Dü*Wú€àã³&hèãöŵ€qøé¶¥¹µ&æõX9Xxx‰XPÆ¥yÈùèµÕêú™±Uæópèõ)è…ª« ‰ëÖY¶åÖé6Ûœqìl¸ûì�ë-ý+Œ-H}Lüv¼Q½M^n~Žž®¾ÎÞîþ/?O_o�Ÿ¯¿¨�¹eÈ_¦m_ÿ¾@ãÅ Œ¿C„©q„ƒ,r’”1 5ð�AAŒù(Àå�DAµ<^3ä«ãG_�ִ겥ʅݠ�3”�ˈ�„‚†R¤.7�`yòØ– ‡À”V@æ?rj&v"æ¨P~\»zý 6¬Ø±dËš=‹ö˜-An¸l´f(£!5AWýLúðã[C ›>DôЛº‘¬^Ä„ð£ bʰB i-WRZ‹'ƲøEf™.Àüä8 ´ÅÈÎH}6ôeë£d�ÔpѶæ´¦‡øsÉÓ]p†¸°ôí oA[öva©[8¤XY9Žä3­ôéÔ«[¿Ž=»ö핾”ÙÐ%úšÍ³Ê|éÿRæîä` ”)ã”K5oÊx¹D Ã,7ï‹¿÷èT2ðæƒKÈñ·Áú)3^L>˜'_V]l`^YeòÆNÑFÆ74–DR˜·” UQ˜Þ#áe°Añ·™w§a$k$×h>Ä8á{“y±V\„¸á†mÃYš! pI¨Æ¤j£d唫4é—3Ïm’#wbŽIf™fž‰fšò¨Áfz�§FnÄùŸ!ã5'Èxó­gll^Âç{oðg¡o:žõáùà{óÑWà|rJªÆOsη¤n¤Öœžj\jé£Kú)4´ÙÞ^z~ö^›7¸×#Üÿ±yäh¡-‚ž|–r£^HÅ]QeZà°-©ñE0º#±‹ÝÛ̵ԅŒªÉm·Þ~ n¸âŽK®3,Dœ�é`@¥CÈ=¸‹ïã†\ÛÔ]9ø¾ê̾÷Æ;Y"í–KpÁŒp /¬"�RnK$ ñ<;VÜÉŽ¹m£ñ9ÂnÌpÈ"�LrÉ&Ÿ|–DD™b";¶�‚rÌ2ÏLsÍ6ßòNòNÜά``ŒsÐBMtÑF�tÒJ/ÍtÓN? uÔROMuÕV_�uÖZoÍu×^ vØb�MvÙfŸ�vÚj¯ÍvÛn¿ wÜrÏMwÝvß�wÞzïÍwß~ÿ xàÕ‚NxᆎxâŠ/ÎxãŽ?yä’ONyå–_ŽyæšoÎyçžzè¢�Nz馟ŽzꪯÎz뮿{ì²ÏN{í¶ßŽ{îºïÎ{ï¾ÿ|ðÂO|ñÆ�|òÊ/Ï|óÎ?}ôÒOO}õÖ_�}öÚoÏ}÷Þ~øâ�O~ùæŸ�~úê¯Ï~ûî¿üòÏOýöß�þúïÏÿþÿÀ p€, ˆÀ*p� l ÁJp‚¬ /ˆÁ jpƒì ?Šp„$,!õ!ù,ÿ,±ÿ€>‚ƒ„…†‡ˆ‰Š‹Œ�Ž��‘’“”•–—˜™š›œ�žŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç� CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›8sêÜɳç¦7@5}7´Ñ+&P¼ITÔ§Ó§På(P I* w'00:gªWK ½™ªÙ³7Mx­“ɪ;Zÿ��ê „¯7Ä’EË·/L1ZÈi«àj»¬[Ýe�W�©bô*(ë·²å‘vÈN5lÉíḊ¬LmØq—’´=XÈ W*9•çE©$[*ÂÛ"I*Y[‹³a)GŒ„„ À¦�Æ&–‚¡ÖÕŠéc‰6®Iˆ~N%‡Ž†då#`Òè裓HU¢ p¹ŸWs$uÂQBY˜ w±…ˆZ‚Ö逦m*�™�‚üvå'Ø’‰bk„^Q(¡SIw`’ZÒ£ÿÉ:•jé†Èªº¡{½Î‚¨>¼ñmIe�ƒÈy%S bí±j;U† P+H›rdŠÂ³âG[!6 ã�ßr7—(˜ • �,ð#•"•º„ )G°<úh\ƒ!"UŒ«2ÀpÿWQ:ièj•òIH¥`‰u%…«:°á•ëõèñÇb*‹H�ùí8\‹×êÚ𜄤:é QÂ9H½R­|^¯Kå*!Šž±¯Êz;Ȫ�,õÀ¿!lcƆµ!©rvÛÇ‚ òÛÖ…¤úàµ,2XÖ\Ü‚qYG#²ØÀ!òF”.òƬ K¥j…Èi©!õ†'¸Ï²‰H¥é¶8ŽӹРKÅí]>Û'ßÔœK µdÍšl!³rFê!¡î:Õè„d,¶m Ǫõ‚ùÆ!dW©í«¿°�†:bß´­WíÌÇ×�ÈoÜÊIö\�ó ³åMƒ‚ÊÙÛ¥T� {çà‡/ÿl‡tõ»䇶Yäë Vú‰(ªqô‚çÄ�Àÿò÷ÉG=÷#ô¹–ò"Gµ�­VÝó�Ó®6E@�g”ؼªÁCà�v�ŸÆ6xðƒèR•Á*xˆõ g9ãŸ~³3»õŽ‚-,DWD¸GX„¬Ã»’ˆ1"Y“°Âµ´TÃñMp>ŠS »¥ÂDL\ILÄ]Ö=Zñ.+»–b¶Á.ʨ^xòQì^¨À�l*… #AƯÉ…«ŠDñ3-"7”ÀŠˆ¾%²‘Ur¢\êƒB÷-"PŽ™#ž¸u;1)Í‹�lÍ¿Î@ÉJV²G”qã!~£+û Âÿ€­û”Û(ÊC²;Èc„å,ÉJV ˆw¼‘$vG@é5/”¢NH¹M6N9©tÛ‡ÔÊbÂÇþ:V$—é-≃ ý"§«¬`í7>«Ú"D÷ºfQ…È�„'aFØÈz-ºÁæ">Îf{ÜkÌ)Sx>Cd0yÞ$DÜæbp)8ƒ8Ø�§eJr’†ðçƒ(¸K¡�0˜�oðpðo`}IO¸5ø„>@p@ß·„Ôdž@(1€‚FHlƒ†mø€`8>PˆqRˆ%s mïg~>p‚8IH„€~ƒàƒ°Løa±?hp‚sèqðoHlè‡p0„‚Ø€à~ƒXpˆ%™•ˆârŠ$Õ†‘X}ƒPÞw�h„¡ˆ`8‡�x>ˆ|>@}jè£øŠQ¨Hb@Q‡0‚¸hª˜‹£øqÜÈ�/à�¸èƒ€ÿ�8‡¼ø�1€ŽÝè‚@}ÍØ‰?éçŒWˆt!�Vh~>ø‰„í¨‹‚ð‚邨؂�xu8‡C8�/¸î�5X‚óH�÷�cF åW‹ãhþ˜Œ½�q°"9’#Éóׂzx-8‡lH’.)’‚†)à«H‘§e‡h W¸‰†�‘G�>Б…p[X�†�„sø�ÆLy¼¨†8ÙpQ‚”à�%8�Šè‘£˜„w˜‹8ȆRY-)8‡u¸‘…Љ)ù”1P�ø‹S qÒኈ”$È�ƒÐ‘’8-YIˆŒK¨��(åç“™’ƒð?ðÖ'ÿ“ƒp–ƒ@p0–qirÔˆ–¤¸•OéB؆tÀýwŠ”¡‰?˜)¨–„À‹q𙈂�¸™rÈ’y“•™rÔˆ—„©™By„3h‡ÆÈ‹ù�pp�ƒ°¿ùƒ”±—(œ§ØŒ·it/À3éðKñÓù†Ih€†�‹pß�U§‰º Š˜hžr'‡éwlhœì wÂ9‘ˆ9Ÿq÷_0q0¨Ÿ: Z z š  º  jl0¡„o ¢P€Ù G—‰}@öù¡zH‚˜é ר¡—}u•[Y†ù þ‰¢'~&À¡yÙÿ¢-’•¡@”2šqì']’À£ø¢Õr�0�ˆ �øÞišôÇ‹É8ˆ€ �=:q±h•Ž ¤K¨£49ƒ3h††@`z‚×(™9Ž�H„Œy¥99Gw&‚8Z‹ŸX†8øÈ Þ)¬ù€û§�Ú(.w}w`”êù}šX¢pºp±(FU˜ˆéø”Ì·�C–¸˜™è¥�©‚ýx“˜“ùÙ¨ŽªH˜õ ù‚)©£çxš˜’¡X�eª„@ƒ6jªýö¨Ç‚�‰¸¬Àú}w)‹™¶™ÆHƒ©h›ÔP�ºŠpö˜%AZ§Õ¢£Hi�Ò׉òYƒzÿ£…@¦(ø­ÓJ­Xr‘“À¥â�¨ˆÿIÿ‡¬¶)¤Pƒéy®ç';Y \ºˆsŒË „”ˆ-X¯¶™5˜«ú:o×¢¥×j7:*™æê‘ÞÙ—?ɬyy“ЃA)ž +ps© @y‹sø5ا‚ ‰€‹)•:X¯Šè•| ![rìj˜/Pƒð™tÀ†ÛH2X‚òG5'*þˆ²ÅÉÃx³$÷¯žšŒ¿�Áù‚?€�I;”¶ù é±PksÛ¹#Ó9˜�žÒ¶j»¶lÛ¶nû¶p·r;·t[·v{·x›· À°z«S…Z±ýG™`ð®� |i‹Z¸Ë×ÿ ý²â‚´(HfÛ·µ®…�‡·˜¡‹†Ž›[››™ ¡›Šm|H™”;5|»‡½™­+´pð•‹Àœ[š°ˆ@»¢û‘cZœŸŠ­©K5Ú‡ š(»�Xƒ9ºLq¸ðê»ØØ È;”-J}“û»#~Óä$šƒ|»…ð¹x¤ùW¸NK€tð}1�o£/�€@ïÉñ€„ð½Hª7ö­\‰?@¸‚P諾‡ ÿ›õG€}z`@ À~Y€ÔK…r½Œ@”ñê©>ˆ˜�I„'ø•Ì Ÿ68ƒŸÉ§Jûjú›FØ‚©ˆ‘Ü›– ¦§©»—ÛÿºÇùÂ.º‘Ô§ƒ3ˆ|¤¹˜'¨²lÄ¥Á-a�Ë’Ùˆu¨‡™„Sj‡aáµóú�ÙøÖ×–mH x‚Vê{ ¨_è}‡ú¿Rü¿þ—ŸIøÞùPƒ5è¸ ²pY”>©ƒ)ùqHƒo¨˜«JKÃFÌÕêT‘j(éˆaèK\³â}â)–ÿؘ\œ‹/œÆâ»­› É;¥Ú8‘z‚�ˇ¿õ‹Ã·:‡`X¢¼¸ž‰é“œK“n9È/Á«½‚ˆàJ¸¼H‡¼�I³†Ð®>Ëǯ\l¼›‹éÌv¼‘ˆ�¢¬ü9ƒ`Û‰*«‘ÿ(•‹jýû�²KÿË.aË©ú¼h}?˜Ýìu•ÝØÎ�“ºXdž@}Þ)°…à˜“¼‹“éÎÝȆÆê»lع„šÈ(Ï·ªÃ‚œÈÝ»Š.[‚ Î&QÈ}³¢Š ‰h�¬ë�¹ª ý}� «\,¤Où”Í�K±­ÚûЫɽ}‹ÎܺM‹ÿˆ§8½m�Êg“P¬OÙ� x“-ù’"¬ù,™¨+™ÀX§’9ŠO9ÔD “‹è¿…À‹,=‰YiÐCº”‚ÜÄÙ¢Z,Š9½…8Ÿ¶¥Òבû˜ÁA‰Óe¼¿šX±>À‹Þ£ó¬»Ì)½Ij}þG%qì‹0–Ý<ӄЂ=‘„-ÿ®IYÅc�üzÈJ<™S«ŠPü©ŒªÎU,‰o°˜3©ÂuÍÔx­»Êš–U,™El�ьȌ½ÕÙª¹©Ê]½Ú%ì§w˜½�íØS.™Ð‚þwÍÔÇŽ¹XªŠù¦û‹Ù0�hš‚õl»Åí‘ó¸ÃâË©5K¼.è¸58˜QI˜‹œ˜)xØ™l®ðgÏ }Û&1²œ0ƒÙ¦®ÍšŸ9€(8Šøì‘ñ(’`­# ÚN-Èì š­ÜŒ…zŠt°î  ‚ƒ^,àlˆ–-øø÷š¼èÝ…ÝËýh~’Iž‚WMÞ:Áš·ˆÊÇYµØÙ¼‰éÂm¸Ûý¹ÿ›6¼§6øÕI®.ˆ�ÏKÿ.lÂ⻪.~DZ]Äþ¨ƒ(ˆºþ(àÉÒ`p�àùÖWΜ0äŒpäÏwá™PŽä� �8ä�DÏßyÎXîrL(áꜚ]îå5Ø¿÷™…csëŸ=«¼iþæpçr>çt^çv~çxþ Z‰ŠP¨šç7„ßlÉ— èõ¶º7šÎü[ƒ…n輘}!mÃ;µŽ^o4J~Œ©Òâjéùû7FŠÌšâBÀÜÀ Á—¾_R¤‘À}JžÉzè©-\„œ¦6ÈŸºH¢Â ¦ƒéD¼êú•¥t™‘ã­Ïßç¥ ~‡T¬ÆY˜�o|Š»î‚ÞÉ…ŠÈ¡ìæÂÞE¼Jÿ…¯n~D)Ì^ÊÉ5Œƒ—ÊCh}Œè—½ŒÏ0ËÛŽWâܯÐg~ÌÌÈ“éËñçáCÓ”©Â—j‹†P�J﬎ª²´%k¹>`¹®ºÏéèÎþÜð¾‹Ì‹èÚ ø®_þWnð‘dËÑH§A‰ƒÃ;:zÒ™Òï§ÓžžþHÅm˜á>;}ÖÊÂ'ïÊ‹¸‘PÕîšlÈ|ÅúÕ… Å>ó^d‘6¯YÎÇ®£ÏûÖ}Í¿Ó~Ùö*ÛHŸô�Ê“[©–±lò9Ú°›’¦}j*ô–½´�£�žõáó°|kÁ)¨¹^ Ý…ýÉy æ`ì‚hïòæg¹Çîö]dÞ‘Þ¢I˜ÿïÝÒ­¬ý„5XÊü}ŠŽ9ª—Kám(à-øÇF”›m»S‹†/âóœœ@ˆÏißã.xƒšïnL¾e›«V¾ú²?û´_û¶û¸Ÿûº¿û¼ßû¾ÿûÀü¿ä¥<À0½ýù† ‰«Ä‹Ë ˯’:è‘¡2?ü�è OÕÚ]Ä›½ò‡0ãVÈÜw­í�ð¼mʼ›�ÚØ?Ùf�ßµøºNýáoË`ÏŠ þ“`p1>…†…pqp‡ŽtpƒŽ”•–—˜™š›œ�žŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µœr ru¥pމ‹„‡‚Ä›ow q›ÿ‚ʪƖ‰?—qqÒ¶ÜÝÞßàáâãäåæçèéꥸ¹¹¼¢opŽ_Œ¿ŽÙŽ__o½ ð0 Ð!t'1}/ÀPÍGà à€Éì?-½ÉÐm”¨ zQIå¤C1èLÃáRÄ™,}dÈá�ƒ¾ÔtC"˜Šë’*]Ê´©Ó§P£JUÊÀ�Õ3? ø‘ïÚ<˜ Å�¤ŽÙŽ…žºSÀ쳿|5ò±œ·g ½€ —k¡±få &Xð� (ƒýŠçK¥ŒoÛý�wRoìÝÅ[€±ãŸpè6; k¢»w/O]ͺµë×°cËž�©�ÿUw!n|‚”Óc0>ø-”ÎPÆ?о�¼�=hoýÛ6ã\Á_ mx›óNf8/’ý½«œù¡â‡¾á°hØ4|�‰6ö�²mçB‘)#]tß·½ŒŸù¢ÈPlÁA€!qü Œ/ŸÑæàƒF(á„VhÛmîÌaG'¿RK¼ÒVA[cHFCmcÏ\Ř5—y‡ìu !j!øƒ‰… ø×qd…Y8²0‰Ðáˆ4�Ã`J‘Ð%x—�>´ÅÒ_9×\…\véå—`†)æ(b˜ žX#"z=Yˆ�ÄáC –ôЇ2tZ²È‹wR’Q‹‡Ö!yú`ÏPò½ÿÄdGúòÆ3n“ %Ò06£Ÿó�¶¥!E¾Ù㘠†*ꨤ–úTUfæ" Ÿ´ ¤†¨ ZGV¾í1Ø XZ“PC©œ“Ròœ“ÇX™+®¸JRe—d$"rBÂbqsU:®ºÞê‹‘‚Z²¤¾ Z–Ÿ ‡¸¦¦«îºì¶ë®%¨b(†¡ÈåÃ^F2¢�Ä�Å׿p¤Vˆ•ØëC£G*Š’¿Ãœ¿>;+~<œ4§5 p+Yâ´,’%}oý�ï»(§¬òÊ,ÃV¦sÐÏ|(W3'2²ÁÎp°³ÏsÙCˆ ˆ>ÂH![U’¤“YÇóÓ=ÏÕ¤%mIÿ<¤!BƒvòÒ{5Ô ]É^R¢ëÃÈĽUgËl·íöÛpË/̺•’tÒk5†å–Wr4Ãú²é!m]× %KóZQßsB³6Š*†ÖÇ®‹ÚŽh–Ò™&²!˜c¶ˆjq—núé¨Ç�*V¨@’ÈÚ…dc³X¡kÊ+1úQ²—Øx'ÜJŒÁÎéæÅ gäɉ4xhoce% ¾ª %�fn%_mPÀâȧîý÷à‡êuPY c‹ˆ+ˆ²-þBîvB“HÅ� ïõxŒd› IYV×vÌ"1_ÂÜÑ~xÔ}¹ 40Ñ2Ò9±9NÝÊ›ÄÇÁzÿðƒïÂËeNs©5Å�>kË�,Ç«€aDrŒ»ÃmÈkË Sx .bt€a[�˜ä‰ë-×P "Þòˆ&SNÒHò–â” {ôx”É8`��ð‹` £!¤Cê fx1‹ù#Á˜…~BZþ ‘86îÃ��1Ÿè�FšuZâ’VÂ$ظġhBbX^@®Bì‚uz�  Ç1Zò’˜Ì$lbÀ‰#_wà#HºQpÒ“be%Yñ†„'¡´Õù8PJMÚò–¸Ì%Tñ9ßÀJ—À ¦0‡IÌRÜá-'»×"YÌf:ó™ÐÔ¤ýî2¦—ÿÑ̦6·ÉMÓi� C-»IÎršóœèL§:×ÉÎvºó�ðŒ§<çIÏzÚóžøÌ§>÷ÉÏ~úóŸ ¨@JЂô M¨BÊІ:ô¡�¨D'JÑŠZô¢ͨF7ÊÑŽzô£ ©HGJÒ’šô¤(M©JWÊÒ–ºô¥0�©LgJÓšÚô¦8Í©NwÊÓžúô§pË5¸š”P+ÉœÈ+2`™JôkG=àr²l@ÇrõÔ« S�È[˜•Þ}¢P±›c4þØ;³z„†óê,ä!�#ö°¡<Úƒ¨†¬8‡,ˆ§£8àdÜ!AÉH‚ñ­Ìým[žF´“À5õ'?I®•‡ÃèÑÂØ@¤BDû_N„9c.­œ1qfRü€Jq')âL¡D€•#î!‚Ìbì%9ÐXN¦þ¼X;pÌÙîÖÑ›Q· Üz�¸,ÙFkR3@DʯÏ&c¸žgsò¨˜£F7ÿ.EPé˜Iþ%pVðµ}¡Ø�=„tR±V/‰ºf+h“ÑŽsäP€d6QÇ"Úe!_øÜ@¾pÂ8Ç)1ŒÃ\ıÉ$!½ü2Œšr‘c"F�/%ÔmGÄ0Ξñi¿àJÓÐá´öuù4”k‰ˆœ¶!ˆ^â<”�3] 7ú_è��;H¸&;ïOÁ¸­»©Ñå�oRFX[‹CëR¯›Ó[ !‰ Ib(’Æ­½5ù‚/pFÔtT–ºh Ià‰€-ÉÔóÛ^.³Š�vŒÓ«îCÔ›YNýã5¾Y�Á¥ëÊ«D³cUâü÷ WÁ7�–Ó\¼ÍÌ]ÞËåÿ¼ð\^c Á0€ Èþ´¥\½î´ÑÉ œ–JÏÎŽ6çCzÌ…¶r¾¿�xZš£ë F5þìq®h/¬á,È@”þ Äá¾7~ðÅkd`'@v£y̶è:ã!pG;—¢YËÂõWÑS æGűlŒ‘D*6x¹ÄTS³o™Â8F''ç zØ“D…Åp¼r.«:°, ¨ Ás,Èb~ÈBŽ ^Sƒ5è:!w @T`6:ñ_Ãæ§­l ¦'0÷k$TCVÑATO'}k¶f§–|h6s3…&'"ƒ³CÈC¸GÉ‘ðu2'x0�%CÁGÐÿlÞƒãQ'fU5–—Ó ÁSua‘‡™@‡�±A‹¶^ˆIm �”7u"•´À!ÅY(q~Q#ç|t[x‡éµ^czc èµY÷†s�euè¹ÂTÅVf†ú2dž ÉÖ �vˆ °¥|ç¥_³È#Ì� §z³…0�8™¸ªÇ"SE 'Ä$hV‡½q‡XcU¾ðŒScVìzÄqÐbryÒ)™°�ÂÒ+ô¶bƒØX_ñk�dGLb$hµpävC'‚ÝC‚¶X[”€6ƒEG“Ð5_4ÆŠà=×kí•z”Pðu ÆŠçµlüÀzbv„?Áÿ� ‹tuEf£‹'b_ÚEvqV‡@t’‡`x ñ,Ôv /¹ ²^ÿ�i©‘Ç �'G#Vµoo2#ƒUo Ùy#ÈH‰l¨zx[‚˜ŽÄe̤&Œ!gc'VE4—ã"“DäH(™2,ùà•øŒØ#Xf) òP„˜q ‡:—‚�¨çƒúòŒÀµlѰ‹Ír„P|k&{é!ú“„[au{.W4jAM˜ =ÁrQ…˜”°—6+N….ÚÃl‚™+ø·“64‘Îa�ú($›cVX‰"Ó mùR㊗Y�•Àp�¤]R)FK™7Ž�%Q%u’ÿi~Ã#ÐpF-Ò� _–玛eC@Ç=c€ŸÅ9ÄPYµÙyòQW‰°%Œv�9H5i˜•‘â™H‰ Îç' „ªGV*I™+Ðf…f¶F–itúŊ˘r¶spUÉÆ“Ž´%=y4w5#‘®‰6*�Ä #Àµƒ÷² ‰ð@™›_Tk4ªLÄ+°F(G1w�H¾€qÑ‘xóv)”(ŠˆDéÒÙ�…�8Çô~ŽÐ™h0�–Ý™�vY%xitËvL—ñ„‰lxùB™S˜q2rŸ²�d%Igè�™’dErûpTtPl‘kgÙ`ø&“™”ò9§ÿ�ÓH ¦ 8"[Ò�má>ðó&°ƒqú@È�H ¶&vÊŠUf©¡t4a� ò>�q‚�‰wñBˆ�LŠÇ�2EO—y?j („Š€C~§i”�BŒw µÅY(ÑcMôD`êKóà¨U´‰VV—ÌV¤çñ_ø�&9i+Ä)�äE#VéÉ)Ù°ž„ÚöcUï©�)$n(ŸÒ™–xÒ(.’dtEšbÑ«tt.À—˜‰“ÿiǺmê“ïQ�zü‘^þ{Šð©öðEã�ÄDšºGŨ0E˜kòÊ|$q¢DoQi¨á#nnq_ «F…"4 Dÿ.‘Á&£Gyæ`à#Y3ƒùó°”t Ò©ª‘S †„«QH��HAj«S2”!›bH2«„„‘‘Âa6ÛÂ1‹˜aM†gõF)Ä#aö“‘Ì€HÂQc6;†a ý ;ùz3r‰.t"6ë”Á"FÛgz™?0[±=Á`µ;;6Öd'‰`K8ÂrFô#Í0@ÓóhÎÑ?F4Aû96 .}D?4:(ëfó}¡_Š;<2ŠŽ B¡”t¢¤5’+´„EB"JšpJ¦ÀIšg ­ÄWJ£®ôfè`~ ÖJy æº ¨Jš {á)™=“ÿºÆÄ»«J¸ë1µë ñ)b=Óºœ`0w|°K¤ ««yÀ'm¦›¹ëD–˜€¯ÚÛÑúX,ú½4åY‹{“ä‹t@³Þ�¡éëRæ+–èû¾¶åZ�¼³à¾ô»R/0¤Ñ»ûK ­äJÀ[ w°ÑÀ ¼À ÜÀüÀÁ<Á\Á|ÁœÁ¼ÁÜÁüÁ Â"<Â$\Â&|Â(œÂ*¼Â,ÜÂ.üÂ0Ã2<Ã4\Ã6|Ã8œÃ:¼Ã<ÜÃ>üÃ@ÄB<ÄD\ÄF|ÄHœÄJ|Ã1€¿KüÄ¥`o{®¥4]ÅX¬.Rì½u¶^¹‚¹Y<Æ¡’Àä´æÿ• KÆlLh\jÜÆrü%#ÙÅkAm à5og,ñM5Y0(‡M.ù„:¡{4—Z"g‘у¾�¥ør{uxÇV÷uÓÿê#Ú…uœ¬us<Êå@}ë‹H} )ÆEDܵ«ñt`ÐâÇ�HAÅòÇ�8g "I(ġȼ—Y|V\F¶S´‘1ø'ý›‘— Æ^(~çB£§ÕI\¥1Ä]àê®*KÊÞ, ïÙŒ:¬X ƒÃT„s_èÌŒd»“žÀØ�/{‘_*"öº£ÿ¥Æ%sN1¥¾†’TÀß\ЫÎÐpjÔ îW›T%›b·Y°ÿJoü 6$ àÒ ¾L£‚ÇùPÈx™|Õ=öeÐ&m á\Mš:Ñ‘/yŸ½p¨TÑžPt1šm6o|‹'Ɍǀ$gÚÕU¾46nyÒH� )=.8í!.�/NЦé5Óð¬'èÚ“¯ø°åÑWOǘqÆ"öcÔI]Ö®ÎW� m–ûIž®·pãDÓ�0¥Ñ±Ð"RWÇd \� Áe¥KÒÉkÖ‚í ÍÔv�6ƒCŒaš¤::­ÍÒ͘0 íH};jr{;&¥×W1ÂÓƒÚ¡€Öã2� -Å'{´ì�íÞ:Õ˜a„‚{s�Ц-"^˜rýk´ÏÜÿÛÝFEcv[2î·îZÕ¢�ÜQ‹6Ûül°XRt)úÖ;çØ”c:Kµ"µ6[W°'s Ùµ‚d5�% 1g6Fe\ýµRK „v¶=†³bÁ jëBYAr˜†bÉÝßÞ€›œ »§û»°Ð¬°J `ìëß > nî \Úàn Þ ^áþ ” §…ÜIÁeÖpÅâp´W×á*Î ®Ôâ³1À.> KÀ+^ã6~ã8žã:¾ã<Þã>þã@äB>äD^äF~äHžäJŽQM¼äN ¦ÅZD‘âåÀeµ¸ ²EuÝ„óڗбOÃw�+` ÿ_þe­¾’I›¢À�ݽ®T¬£°F›ð^d~G�5v>æ- ày‰r-¥Ó¦´çš ÆmŽ#n»Øéçþ_J õœ � ŠŽ DÝ­œæ�®ÀNçAá€a Ÿ|/¡¼Jþe@—~~ssìÍ[½eZ4! ;˜_ a6ubFeǘg¡As³ëoõ�ÑaU@›êï»sð•]Úpf]•ÍÅ |õ7[€ŽŸaU_M'äJî7Ü¢±Ì¢ÄTóÛ,Låóg¤¢a^FÛI!Þ?ñtÄ.Ô¦Ì2×Iîw“™ž¯tàJ¢a†Ð~ÿ{ æ VgÖ¿¿º�Œ¬íß»mx)üÇÏÝF¤ÓêÔþ¯$lIuÏé „ºÙZê-G……Ù‡–M8h^„öc"o è Ä76‚ t�×矾ÔXš|=ARÒÀ%é]èEûé«„ãÝkÜžõàƒÍ:Єr†~×ÔÆ4ËæáÙþÙ蓽�ˆ~ô†šô†‚(Måfé—åèáE&Ô$ ¥}"U ´J¥^) [_ŸÕ2­àz�„"ã Ðç1pTk_ îö ÷ƒoÓü<6ÙêQ’`�t"¢ÓU ’㺣GÈIÄ×Ú7ŸŸ#–µðu­Õ�O‹iT�Bÿ5m�ùRÉÒ!††L/ÒãiräÙ™›õfJ•9WOø+ɬ�½ÎtûV_ñzM:sÔüX#›j_¨þàûû«ù,ÂùÖà#ïSò’~_,ÝÒšP7Ï…pˬD•ú„ƒfë~9�ðªîÿ9þ >‚ƒ1ƒ‡>``ˆ>ˆpŒ’ƒ?’/�‡wq“p“ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼·‹‚_ƒqw‚…q/`q/“ÀŒttˆ_ˆ_o‡Æ‚qÂ/Ù’?ppáˆЇ_Ÿ’Ãëê‚o_ˆ/œeÀ'Žœ9túÇ¥bÚê záPÒ8ÿ Òw½BŠI²¤É“(Sª\ɲ¥K[`ÚeúòXœÃb3xpHÈ#7˜| ñˆÆ68]:$ߨ³µ¡ {£…  ÿ�é »yó ÎØX|B�œ‚íß lÏ@bøñ> u¿žß;Aý ð§Ý€NÅz É!yÝ)¨ày �!b((ÀGA^(P…õE¤ ~ò×È~ÿ‘Xà‰(.Å�y†(ô!ƒ-þÇÈÝ9�X…ò Ä¡Ž tÑ‹ýH�‰C ™â‘H u í)°B÷ÁˆÐ’ŒPè^AŒ�q�>>tUÉ$dHHЈF&©æš8­èCy]eLåÅxPy!X™#B8^¢B²)è 0-ä–éC#(„y�(àç¤(‡B�aˆ€“ž@F¦†iª Az¨•2ÿ„—œ+s�æwȧ!œÀ*$†|JÆ!¬R—«@3~zÆ!œ&4«²¾ Äh˜‘Îê^µëAr¬ô ÄH°µ2;% ߉)¥Ùé!Ý)#ŒJz�¶Žjï½¹éC…N4ç¾=&„¦tÝÙÀ„ ‚ìƒç‘a©µw¡‚ˆÄîž ™Š°vÄîÆó(# 7æÇ*LÐŽ6„‡°™¡œ2· Tòy[ö¹aÀ-oB±>èŸdll!€õŽ!¾L7Í�¾>�±Í îç™ÝM ‚‚‚!3¹$^Rž6ñA¦^hêÔ)¨2 a“)Ø5}¶^ÿ]7§ü*8ì g¸ÜˆÆ¸kˆËÓ¸­(˜Ý ¨j7Ø·!g‹¶@_~Ú!�1ÞŽV?~Bä{StÕC“èi™Ÿ—Ùby„¦êNçî´¡ÔQlпÝ"„¦Ö,¤qwÓ\µ@k?¼4Òñ ´AaÏÍcÀ•sìqwîÜ]½Z”÷`Ò!Éow!oè`±E+�þ¾ †×ý|Øsy=ú‘ì§s¶ž¦¹×kçj¦»æjS@ÒE:Ûu‡`TÚæ R´ù5� KÂOÑÒ¦´<-„Fý:ÈŠìÄ/ya°]×{ŸŠg‘Ð<&tN�V†Ây™g~ÃXÏÃÞ--n¬3Q ÿŠÔ,¹¤;=4 ÙÄ»�H�Uÿj A†W¤D‰iâ›´G-Ìh†ÞkšÊ•%CZHxj‡�öð  hÄA~±ñŠ!ôAx8h1îaèJ4ô‘Â/Q ÍDhâÔ8µ‹-ñ‘öBà¤|DII± PCSÒÒU0* U{sÂøÀ*¤||Œ 2F@uRy1{`»Ç/9ä`1¢ ^2x ´å@V±þ ‘H‹Bf2('.Bò™j’¤¨TIû!j‹„à.e¨2ž Bràƒè ò¶.bq!å»#CšÄäí n9äÓ*�Yª êL!4òå*é'ÿÈú%•Õ\æ˜�™Í-ÞqIÓƒ¦B¤Å�”Ob`§(K¤ “"R�„óœ2~o ŽdÈ®«9/F—Ù”VY¡T$>aÙ6A•’G.Ýž�Œi¤E´I üáB‡Z iNò‡Qô'&+šHþ�›ÞtfCÊÃÁƒY/!ESaBBªˆÑ“†ð¼g†"Ó˜*ŽXœøÕYª1ˆFjª6•°"ᎨxåMC½õQEíõzÄ{*åæI•!KºŽôbªœ:«¢ ©aJß¹R ¦´'dRbÖš d@©¢cþO®¡Q‡ò™×ÖbǨy¨þEÅ>ÿZv°ÌQæ¹ »Å3&H{xrHb!òQ�v²ñ¼bŽš´É‚1£LRˆüxÓê*/‰@œÛ!{JPÝ.s=ݱ¢kÇ«›¿‘ ÿZáòšy[�:u›»íæ@Fº�·Ýî!½Ä'8(¾„n±Ÿ&M®AîÙ$x0MRfûô©ÃúοåKOú?Œn±Wû$¯†i[NÂ� Äã'q{QïªW@ô•.÷jÚ~iUc’B“:ãËž•²Ê½ßŒ½g=49`zMb±Záã`ó8¶IÛ.…ib~árÃPæðf Âõ®ÈœºXE1ªKÂÊ×°¾]HûÖËXîí²íó �{T¯FœMÿÍCÎqYêžðŒì?ísi�å‰Ì¥õ³VÙÁøP/2Q÷­ø‘+jÝ‹�ó3Ê�>M‡ßJf”ñÁ_ÖŠ4½.çö˽•Hà&‚²ý: lî{éy.Ͱô!÷žXšg� ñ 9`Õ;N`�È€êÈ¥ÖqV+$P-N\ÿ®K.‘`iü¹H;»5æ%ÈŠbÈT›y`œvª§ ê/Ä�AóË„ ‰ÜÌ›|u†mgCœ»¹Ò17„ð�²‚õ�ž”»!ćn7°Luþ^ùì‚H['€—Nku¡à®×|—7"EFHü!gģނ„oÜ#¬5¸ÈGž�àN¹bÿá¶xIÎr’¯âLAyX¸Úòš¼|3}ŠÌÁbJ›û<Ò܆wSvî•áþüèF¦­ÂˆL¥laFºÔ§Nõª[ýêXϺַÎõ®{ýë`»ØÇNö²›ýìhO»Ú×Îö¶»ýíp�»ÜçN÷ºÛýîxÏ»Þ÷Î÷¾ûýàOøÂþðˆO¼âÏøÆ;þñ��¼ä'OùÊ[þò˜Ï¼æ7ÏùÎ{þó ½èGOúÒ›þô¨O½êWÏúÖ»þõ°�½ìgOûÚÛþö¸Ï½îwÏûÞûþ÷À¾ð‡OüâÿøÈO¾ò—Ïüæ;ÿùÐ�¾ô§Oýê[ÿúØÏ¾ö·Ïýî{ÿûà¿ÿøÇOþò›ÿüèO¿ú×Ïþö»ÿýð�¿üçOÿúÛÿþøÏ¿þ÷ÏÿþûÿÿÐw�à8ðqOÑQ$ 0qQA)pZ�)P-�8ŽP$P˜€P¦0)0$p$ (˜8€�&á�Ž€WƒÑ_ Ž@�[¡ƒ W ƒ0„ €ZðWpFØh��1‚`‘8ˆ8PA)à>À‚ %X€†eá8Ž@aQhˆXØ…�a‡Q„‘Oxƒ4(W�-4CH-‡ù�…בø…¿ÿ�…‰pñ‘H€-@*`€�…$@�ˆ�1�*Ð�YŠŒX*àŽ�*8��ȇ�ˆW0Šb¸‡s8)0P¥è~ˆ†�8ˆE�È)=ŠŒ�ˆhx(‹-À�À8‰—hˆ«…Ò!Œ-@�>@‰W Í†jXć˜Ø‚�…ÁqŠ0Pމpº‰’H‡…‘€0�ô�Œ ¡ïH[H7h�І‰€)°†> ƒ-P€ò˜…=x†žˆ ¹‹QŒxè�4 òH…¶h‹ÿ¡- €WÐ��@ƒ=¨Ëèÿ˜Ÿ(‘(™‘Mh�Á‡vÿX’‘héÿ¨Š3¸…éŠ0 Šò¸� Ù† 0ð‚¡ex˜�€*`…åXè0`Г7™’FiHY@Wp/‡- þ˜’X“帖ˆPŠXø‚Ÿˆ�ðsx‹Ñ€'i“>—8=8°€]�Ί٘šx* ‘Ñ€@ŽA|é-Ù�Y˜¿‚•!˜ Q’q˜9ŽÀŽOH†ù˜!ˆ¡¬â˜¬¢Ixù$€�a¸‹H‡0yQS8®8!9Ëhš�šfÈ…ª•ç"š}‰$p™f¸‰Àé”óBšq�ªÿI›1œq�¡™±™š•í)�“’›á›"(ð ˆ*°‡gˆ�à�ƒyµH€ä›ûž”‰àI€„†ˆº�è)XÙ†çrŸ.™Ÿ�nÙEÚ  Z��¡ªx†Ž@¢A¡¡ Úš‘:zx“,ê#YŸ©œQœ1‹Òň(Ø-@˜òx0É—j¨*ÐŽ1Y�åÉž1„ á‘1ɉû˜Z5š¢›é¨SI�:šN8¥Œ˜…X n(0€ƒH¦AJ€ ˆ­)ž jÍx`–ìø¡ §ç’¦¡�Q:=têh)•É€zQ‹ÿJ‚‘¥›(–ZP•žˆL¹Ž�Ê¥*¡dŠ…ø˜ !¨�r—�š*‡zšÓC�ꄫȃoêªФJ¦Z¨78Žè)Ù©ˆ ¡`™™ÚY§xሾ Çê%ð¢ºê_7Ú¬q—'™©Á‡ÌX»I`KÌIê¹ð¢ßŠ YŸªñ¬f(“ÔŠ¦ßY,qŠ¢ a® ¥ ¤‰®¦Âº�ÄzV¨® Á¯4z¥éª§JËژ𹚉ZžÌú«/Z¨«™é›Ž@��:ã*¡ až ±�a±ºq¸Ù°ú£êŠ€ˆ¸ù(b'ª†:!ôÿ ²ózŸ� ú¯ƒá®ò*-Ù†«šÔš¡Œù§;‹*®�ˆ*:ú ­ »¥ ´Í™š)p‚Í �q‰ÊHŒ¥ˆ„©É�,ê§�µqüª†û8„-®Žµ •¬4ú„®8 á�åx� zØè¶k³X™­ùq™]ôªa¸�µ�@�W`µ>K¹©}Z�ö˜«°Š¹ö¸‰Ž™*˜Œ5J�†è¤ ñ¸œÛ¹7I�ûùHȹԉŸ‹§\i�q艀€Š$�œú �½ ‡‘±X�’ü˜ºG+] »ž ¼g™š5ˆè¤�º~K^¹µw»»X»ÿ º ¯ŸÛ»$@Ž%:�×Q¢e(�kH¼ˆ‚Y(‚ó•)0�6y¿H€�;¹mÑ›†:¬"QƒÓ#ÀP!¢8à€"¹þ¥8‘ QƒŽ@À!1·«˜¢ŒüqUY€ìÀ�Áÿk5Âìh(¹3!°´áÂ)œwîÊ3çƛ§QK�À�:¼Á6¬vª8ĺ1ĪÄHœÄJ¼ÄLÜÄNüÄPÅR<ÅT\ÅV|ÅXÌwk YLrA�z@‚�y° 1Æeœ:@Æ�ð€n¡A°k@Æ‚Ð:0Ç]œk0~œ¡0y0: ?@Zÿ ypÇóÊÈŽŒŠzÜ?Ç‘ÇV±{Àа€‚ÐÆ!¤¼Ç¡°rÈ?ÀÅ4‘ÈU¡‘œz k�˜Ü[lŒ\k€¸ÌAʡˑ€ÊÑÇ¡(5¡²L·Ì´¼°Ë$ñËT±…ÜÅ,ÈœZ€µÌÌá̡РZ�z�{pΑÇ`ŒÂ1ÍÏyf¬ËµüÎñ|ÍaÏò¬ŠPÂqÍüÃL·lÏzpÊ!pÇüŒÏ±ºlƺ<Ì-Š {`Æ� ϰlÑplÐþ¼ÎAÐÑ!1ÿmÌ‘sœÑ@£ºŒ}�° øÉ½oÜÓË¡Ï/m�ÏN�ýκÌÅyLÔ±üÌÕ‘Í^ÝÏÁ�y ÍMmÎÔí`Ó>ÀÓMÑ>`Ñd¬Ôh-:ðÕy=_ŒÎ]½Ìë¬Ï±$�ÒÀÔ}ÉŠ@Ó«¬Ï†=Ï<-Š€=ªŒØk0Æ‹ÝÕ?àÐ��j]Ñ£<Ê� ¨-KË`LÒ � ÑÊœ� Æ!&­=ÉÀÔ<=‘ ÒʺíÇ‚Û{@Ë]× ÌŽ]ÙÆ¬{pÉíÜx× �ÓýŒÐAÿ ÅÂÒýœBkÀÓ? Ø>pÔ—¬ÔŒ-‚ݱ�ÖñíÓ�o|ݺüçýƺ=Û“2 ìÝ�*Ê<ÔŸ Õc\ÉxPÐl¬�0”lÓŠ@áæ=ÆîÜwÝÓÚ­nÛ“\ੌïÍ̆­:°[¼„­Z`ÙaÖýÛ5®Ïy@Ñìüàõ,ãñÔ±h=?ÝMÒ�}ä$‚lÓZ�6�«L>PÚ± îÕLáÜØ°LÆñ©òSÞãµ\å�Óq嵬Ú½æ[^Ï<>)]ÜÌÇü½ÎÂÝã××?ÆAágî'ÑÿíÍ�Ùé¬ÞAîèÖ‘.qNåï�snÍô<)x@×(Ž0È¡ê�é‘ÝÈ�Ó<ÆúlêƒþÓ4¾Ñ ­ñl.Ñ缞¾Ý^ׂéçüå>ÔáëaãñžîêvþÍË~è Áæ�ìaíñEþçq-í¡ê]ÝÎ>ӡ胞‰:Î�ÉÌÎÎá>ìš é“ò•>¨>ã}½ì^¡ Ø¡ÎÎñâ!ÏþÖf½Ëd®ëÓÜïz]ç>ÐϯËÙ|ÖAþ`œÞî,ÞñÏâLçpÝ]žì¿^í[®ÛQòaìÚ®ìÁì>ÿ°ç¡HØU®íV¾å� ïa�é¡ÊC�Ø;MïþŽÌžï î1ò ñÉRÞÏ’îó¼ì6Kî!à‘ÍçqÜÀÛ….²®kàÍõû.ÛßèMMð!ñx Ðc^Ð Ÿ×:€ãB÷™­ëA}ömž€ø‚ßÚ¿Žô\Þë'¯8/ì+_ìÆ•<õñÙÍ~è„}ç1Oí(ßàÁ¾Ï>_Ïvýçœç�bõAäaøvÎó1¿ñ�”®+®ÛÚ,ù>ÞzmúøÎúÆN  Ñ`Þ‡,jîÖûþúo,ûk°óiüY,ðç¢ûq­Ú·ÞûFâñ­ÙJìÿoýOi¼ÝQ?íÄ䉟‹¯ò]ä.ù§,óönÐN希ímŽÖö_‹ïºxtø èCQž‚  ¨ÐÇ-ƒ`p¸G�CÖ`ô¡¢B  êT�!A8îA¨0ÐŒy2 tQaE Erôذ †™ñåXÔèQ¤I•.eÚÔéS¨Q¥N¥ZÕêU¬Yµnu¸f€B�µàÁóêš“F1À¼ù#až@ Sº-ˆÖ'O³ µäzTÏ�»>ö&Ô“3ᢠµ`˜`† ÙY² ™4#ÜcÙGÚ‡6^`X/ÂŽëÉÑ!d—”fØ©°³ÿÃÒ ½ Â6aÊ)GôƒoB�'+Ô¢§öm‚\§ä˜�Cá  -ˆlî„!� ò=²vÁíªû®gßÞý{øñåϧ_ãn¹*%“õüCrôòèO€�´Ø#ºòª3E0(-A‚þÎ ëâO–† 0Âúóa�¸€¬ò4“L6€«kˆ d³=íE~B‘ÐФތʃ¬g£‘ —ª;’°Ôô‚Ð-~˜P�Ä´ ð7Ð|„@E B€š0°+мŠñ0§Ó;­Y£ïïyÖ�G¡F}ïѧ½|øã—~úë·ÿ~üó×þû÷ÿP€$` x@&P� d`ø@FP‚¤`-xA fPƒä`=øÿA†P„#$a MxB¦P…+da ]øBÆP†3¤a mxCæP‡;äa}øC QˆC$b�xD$&Q‰Kdb�øD(FQŠS¤b­xE,fQ‹[äb½øE0†QŒc$cÍxF4¦Q�kdcÝøF8ÆQŽs¤cíxG<æQ�{äcýøG@R�ƒ$d! yHD&R‘‹dd#ùHHFR’“¤d%-yILfR“›äd'=ùIP†R”£$e)MyJT¦R•«¬Ê áWvO äce-GI‚ˆ\€–¾#A ¥…�>K Z@•D� R�D J�‚$‚W(A0¥iËJÿjáPÚ¦SHp „S)pâq…ö´`—yD †Ù3H$â˜SѺù”d^Áޏ:·2OïiaŸ*¸¦öQPlÚð Ç„@™¢@@b–�¸Âú³r¶ÇH!Á+}¦Nå *€Ê#Hà,3~%Ȩöñ΄Ú08ðKŸ 8-•Ÿ#Nz@ÍëdJ BÚ”hº'ý^P¹GžÆt†NuªSêy&ä Kõè I>y’@<�g""’ˆb’*e]*F‘t.T¬e aÍ¥¶U!�8)TÐK°fJheªZM³º­½¤)FšI\sÿ£ ÁA zYX‚há®ÆŒHG›¢ÙD\ y-ìA‹Y‚Æ:ä±%P'O—*ZÒµ š-f è:Û¼$ž)�mRò-xµ°•½,eÛªÙ`ªà¶{ý)Fh Y̧m­C[‚ÄÄ®Ž˜n ^IMu2g±¶¯4aYµâ6€Èk4£*Õ®ó¥¯Þ{w:$¥ �A"ìëÓDLW¥q„Z°Ï+X  ÝidqOö>å wõ¥u«9ZG@˜¨Ù*lóÚ[¤`´88èP²Ô ^!²-¨Šùàó+.1L ’ $bŸÅôiBø:ÖG¤T}sÄtÌ`HL–)ÿfb¸:°1AZpW–øž>@1iuŒÕÈîÓ¿U¦í2-‹D´)(†¥ƒ�\“¶A.Á–z�"y½©¥l…ÅbÌ+ DWï_ט4äI¯ïW_üí²õ:ß_èL X é¦S\%ÂD�'Hå}@Ð×w>éNѾcãO}÷‹®‚�»ÙŠ:‚0¹Ÿ‹¬ë[8å“(î²·„Ø<ÝR²­Õ ©ý›¼áÃæser%�J0à )û´Â"�äˆÏ½åJ»©°?±ë;ªÔS ôH ¶K 3-€„ZC¨º+Š\@‚è/îk)%[A/»-ÉCÀ‚p>óS¡±ŠAúCŠâKˆàSº³‹5Žp7’KˆZ‹Š÷›»‹º¼§?Žp=«#;!ļÿ•+,�{|¼µ¾�ÂÌúÃW³Š?´C¤x^³2Ìb<­Š¿šr-$ˆ ,Š+T2À껂Ø<¡‹Âš,ADŠ²Û¶Ì+ Ø+´�‚(OtB§¸Â�“­1$E+$6Þ :ô1ìD0k>ëúB´â(bƒ¨\T Ô¼^ì:ò©A„D²KÁ£˜Å¦8ÆÃ+Æ'³XK¦uÓ¿¨ÒC0,F‰Äšs�´[C €FŽx:0Œ?iÅÒ¾Ol u+²Ä@gÔ.@HA¤ã6¸2›® t  H4\Š| :¦rEp¹¼3ŠE›1�‚¨ªÉÁ7+´ýƒÿÈ⩊¼1Âû¶ªñ)Ó[³è’Ȃ૥Û5„&¦z„ÍÓ'þrÅËë­‚„ŠvT0ÅÐÆ¶ ­™:(zã…6žRpÒ¼”,¦ªyÈG¤+%3Æ,B› :ž ;Ydª»É tG ZµI, ùª/›²Aúª/°Ê½ÐûR_ó*cªB£H²°¬¯°£/¹ùº¯h$KP¿–¶ùj,ÿš¯kJ¹·„b6±ÊËôš;Â\¿ŽJ@ð¹ÀÆ:;¦ùò3¾ÂƲ´¨¾Y¯^R?„9,Š}L\¯jb¶ÏT²vª3ËKÌrÌÖä·ÍÄKf;*¾|Ë­ü¹Á̽?ÃL ÐÌ®{%ÿ û2]ÜMuK¹^º<Mž*2ÑJåŒ7Ç|¦¼üJùê¦õª08§#KÆL­¿«ÌNLŸÉTÎÜÄJ ò¬ò;™4œà"1÷A»¤(¶Ô-À¢òAúD»÷l´KÅØÏ-$°+x0isÉCPÔ»ŠzrTã' Uî"Ðq#±«ä ÝPõ¼ ŒÂÐDÅ%0k,Qu ¾JQ®LO}Qæ(³T¦ÅÊi³€H;QÝÑ Û§}âQ¿Û'åQ"-R#=R$MR%]R&mR'}R(�R)�R*­R+½R,ÍR-ÝR.ý™5xŸ. S®aA(½)ˆ3�ÿýÁ ¸XŠ+ñÛ9Šƒ¨�ÞYSê±ë±Óõ( @ìp(õ _ Š^‘DÑ×�…Á £X˜Xú¡AÅH‘ pIž=ЇPïh ©QN½ŒÈISDQ„9aGð&(Œø+UÑ ÇÙQ”E%’@ŠÂàÚÈ0å†ñÇ1Õðø¤�DY£ØUŸa ö €E(W} ɉRü �E¹ÕÑÉU¿�Tú��÷ÖÞa ¥pÖ¦@VDQÖ¢pTŸŠõ@„Að�B çj+½V”ÐÖ©8níŠåÉ…yæ8•qa ‘S!žø×=à‘!½Š\%X7) Xÿ=@¥„ƒmâ Øº  ÃÐØ¤á��È}i�¥„A• ©X‘UÓ�0V£  SéXòñ’˜- ò�  ôX ˜ˆ�øY¿Áâ)XìøW…Y�ý‰�pq…Xƒ Ø9i�ßÀ«E•YZž[É,{¬mØ–Ñ�é•‹•Œåù“¢½«ÅÚ9yX¤€Y­‹5±gÛ(ÓŒ�Ý¥Øfÿèø 5áˆäEh¦¡Y…&rf w�e]YŽ›iæˆ�ææÊéâ�®ä,Ñ€. �hžVK Û&Å׎Ð×§à�¢¥��ØA¾�Ùæ|% ÄÀÛ¶ ãÀ—<¾ŒÈ�j©~êT-çá`Öîàè¢hâ`wn\E.éqŒ(QcÓ€dó=Ø!hž…æõšNŇp�@ÞF¬YNAa®‘Î,–)ˆMak‡¨j5=Tæ]cXþe¬Tj‘îVÛø¹‰ ¢Å[TÜCÅAk‚0ìÇhç‘à™j©ö  €„ˆfܨT&ÕiêÐ }Qê�ðŒ° ‹ÇÜv|¡íÿ­ ž—pl§Èâ.nãÖÚ¥ðf…6�…~çá^’ÊþliÜéŽ|dÕx~e¥@k®þ ×Fæå¡í�ëüàsöv= 1�A`@›¾î×véˆÀv—2 éÉfÈ=Ÿ™ÙbœŽ�Q„ø>S­ž ZÆ¬æ Œ�î›èm¥6�” ‰È•uÅା/�‘áHÝã6nÓ?øÿƒ>@&àˆK¥R�.ëNý]¨ÀðWuŒâqNw®Âïõèë¿þìz>ðG†nLYð³ èî(’¡l¹Î�ïNëØ¨Ž°%Ú½ KžqK-‡> ˆn¿©áyŽ4A‰ûNåÄåÔÿ: ¹^ï07Š> �[éA8aUiÛhpˆþc¤8桦 NÍÅ V§Ð‚G(ôBg&Ø¥“fñb‰mÅ ‹„F Y…› i‹·ãdéb¸Ñi5Wg0ïôʸsçb Õ¨î¸ñêþЂ47u¿&/ C¶k„àîË@ Zòî<÷l|Æ u‰Þ¸”%�)ñdÜ8÷�¾®èúÞ\�Iðì¨äò-iÀðñ(S‡HGp;ÇqÕ½‹Œ'á06ïp'�çõ1ÿqGGw»þ•¬¥‰:ƒviÅØš÷@‰woUŠ0Ö�>˜†?lµø7åãð€—øZIŒH‘�™Šu×òH_ÒFy”HQ–JAóÕVábîPù Šx–OÚ�hM!X‘ }¡” ðõÈ•®1ŽCípGÁ›–�œÓ¹[Ü`—†h–g‰–i©–k!‹–@‘zoé2UÚfÿtÁÝÆw–†xN–‡™—­?Œ| ÇÎ妎�1Áw샱X…±dÂ)|ÿÕOÂqýÒ8‘›!cÆÿ �鈎Áí�˜Máð’iˆY)=P–qâI™†~§µ˜îÈ–©@ ^‘–V¦]ËÏ×lϘz™—\^{ñ¯_ÁZ$&'áçOþŒø}‹ø:g(ø(hÐÇ� 2lèð!Ĉ'R¬hñ"ÆŒ7rìèñcGxÖ€\HI‡: ÒAQˇ&[>\H‘K�JÉq�"�ŠPrŒY1ЉDu>Ô²†¥O†ZxFe¸’æDAz2NÅ RÇH‡Šn>|*HKD³U ^ÍéPäZ�c¡e*ód\&ƒF›· –±^íÌ`”0âÄŠÿ3nìøñâ5x’Bƨh@åÌšž8àïæÐ>‚ÝPOiÓ%SVíúuA�`Ó®mû6n��𸽭C€ ÜÂ5ÿ¨+1ÐÖá�ýèíZKká»�+¯ÞQGtëÚ·sïîÑém=yò ø1Û;z�òàŸÚêøÈàD�%H…j¸!‡zø!ˆ!Š8"‰%šx"Š)ª¸"‹-ºø"Œ1Ê8#�5Úx#Ž9ê¸#�=úø#�A 9$‘Ey$’I*¹$“M:ù$”QJ9%•UZy%–Yj¹%—]zù%˜aŠ9ÿ&™ešy&šiª¹&›mºù&œqÊ9'�uÚy'žyê¹'Ÿ}úù' � :(¡…z(¢‰*º(£�:ú(¤‘J:)¥•Zz)¦™jº)§�zú)¨¡Š:*©¥šz*ª©ªº*«­ºú*¬±Ê:+­µÚz+®¹êº+¯½úú+°Á ;,±Å{,²É*»,³Í:û,´ÑJ;-µÕZ{-¶Ùj»­GW<â��¨…¸Üš{.F$ u�q!’ B�*”¯]‰¤PB ‰8r¤#%ø›Â�öΛȷ¢%¢ €$‚n–Z\àƒdDÂh  )ìË 0‰½L9¬¯#8$R®]Wl\¢# —@ÿb ív¤ $›ŒrfŽÔ\$,lP"SLBÍ23\ä-øpÀi$äBr…Jo·2�‰ü R X+F‚×$jM" L¥€là ‰ÄµÐBÔZ8Ò‚FTÎGçx¶]gôÈà 6”~ëT¶cˆ£XÚ"öË”âª9rCmÄ÷A‰¼ìPäz爸å%ÍÐØ]¡u]LÇÌÐ*�ÐÖ '‚–æ7Ž‘ì‰PÎÈ”ãຠvCäˆîZÈ»¯#÷¢¬õîk½õAW”@B ß^�ÄsgLøD§Óý<ì i‘ˆëÕ?C _¡…q · p ÏÛ�C Co:ê©.ÿüCÄ'ÒøÜð&>{½ gãÓ¸Ò—/ˆ„Ž!&S ÷ Q¿×9‚\¤«æƒ:Í-L‚$¸^Aèg?"�iE‹^ížW:ŒœNPaØÆ—¾Õ-ät a èÕ4Í ucHç â0Ïå( ;bÂ,À½‡¨ y¦Û\A`�%êì)è¡q�ÿMî…!Þà(ØÅûQÄïjH¿ð¥/�‚h,�Hp…+( l-ÐÐJ`5€,} �DÍZ–< „ndgcã!¢5ê©°]4k�·®@=©T­eÒÄíˆ7=’�-ÀÁ#pP‚ÀDŠT¬™(±8âí]Ä ]ÐH6Åæ�ïÿLK�Ê$I‡ Ñ!-¨À˜7$BŽäšâ£·EGLî= PùB9Ò±}ï¥(Ù¥L2sz(t¦ �h‘EVïyy‹%3�èµÓA­Š„ëÊ-z�‡ éèBªO‘@\3ÿňWûéƒG(Ì!5Ú qÐF†¬T#muˆM#šÈ†� `>°Ü# è´$ƒ>øëBpàD¡fŽŒñjSHYQqÖ‡nü§ø4˳ºzðÔßi qú} ‘+DúŠÃ†ô�£ iжVXVt˜½´*Å×ÀŽ × ¢$¬× AÚ�”t–UÕi�$ò´kU‘ܺÊÌÁ �H=dMÓ…ÈV¤@iAÔšÌ~µrnlˆâ™Û˜2­L›HxÛ]^ŠV|-HÁ^÷_R� äíC®°ENjm¸£ûN+?— yŽ(®YØÛ¦œŒÅªCÒÇàD ¸ %µœÿ|—¶`‡Ng¡]Hòtò[÷Ò„þHÈ�Ë�ÇÝ”}?dßucôËŠÁÑøÂliÆ�ŒrÎnD5JbO�<â»I±‹ËR¤ÞX¶.žgèÊû�á‚Ø"¿{:É…ƒŸòŽ•eæ±/ó5bÖªàÀÁ-çF¬µå�¶ nC8ª±¨�ÌY1ƒWf9-àQVû¯ž�HâȪ¶É0%r�Î6•µ\1çhK¸/™F|s¬EPýXÖ2VpÁêr�«Ø‰Z–˜\µn‚÷¨±§Î÷ 1†rw± -OÄÒÄ´òc9MÛ‘;½µ>+C/¢^áN䊙æ² r¯Á9‹Ù^H«¥ÿBÊ3ÙÏÕ¡\7ìcÛvÇÈžrGª¼ÄD‹‘b©kÔ6öZfoƒ/D¢|�üb€í…ß[;û@ÌÍv“kœ0‹ÔÎFcôFŠét)» ùÍbçnÜë»%Rs!öEÔ½ DÛ‡,Ÿ§À3ŽFx.0¦F³`cŸÂ=Ö™×­�õ«e¦r”§k‰{÷0ùM‘æ$oxnú¦CÌð|¯H¾¤¾=¿¨�ñÖûÛ!|9`ôx�ßµÀ–“KŸÂ(¼V«\"3Í:¸›‚=j±§_4ê36ptë<"ì�y 06TÆRMfŸëý’™9d_�#ü†Øë-ÛˆQM_ô»Ã#>vÿÍêÌ ´væ¶Ýt¼ó¬:T/h{/ ‘¢«tL±F¼� \^ÆÑ"k�‘_jk™è5�vª—ˆŸ}wˆ‘h�]ƒ‰I|m4�ù´.ŒiÎÛœÿü‚$µ£MØ~Aˆv5pFìaW©gÚˆ‘\ÑTA Œ|;:&Ìj]M®5­ýöë5cs €ßÝͼÀQ  ]YU‘@ÁU]‘T AM—ó•~í_ØQÆŒRÌŘ#¸Ž˜-ÖþÙ�Ç- €ÕW‚biAü]UÑÐY€3˜â±Ÿ÷}ÕËuÕ¢Q¾hÀÀà Ž_¹$_i`úYø%.™ Æÿß[A *ßzíŸâØ`±/ŸŒüÕnÁFÉØÍLj“DLN=Ç–¡Ê¸‘d%R(… m”ÌjÔñU„J$HVÇ}Q0 ßCä!rÄâ�¡ÒÉÆ$¡j(°´Ý!>âˆÛ±$Vb†hÁ(Å‘‹ëD“Y"(z3�â±<Â(*b(¦¢*®"+¶¢+¾",Æ¢,Î"-Ö¢-Þ".æ¢.î"/ö¢/þ"0£0#1£AÐ…1&£c¬A$Çk`@yÀ{l†xLcC¬GÌ6V„dðŒ@ãH£ÂÂx`È1*„GA8£ˆ(ÂxGhè€5ã €=¢ÿ£Øc6†Àf¼†ì�K`G@n†I¤J0ÇŒ8TXxcFðD† dAîÁ>B„ƒðD Ày\CbDv¤‡"ì:ZE~„l£"ðG]8HsˆNè…BІV,„IŠÆd$DtFŒød_t`…ÜäAä¤D|ÆCSåD<ä€ÁIVÄP‚DÔ$=Ö¤ÀkÔãkd@UOf†WJ„SºÈY&Ì åBˆ¥gü…¥N¤%D@¥€`ÀTRDU~„à< cY²W*F`¤fC æbèˆÇHäC%BbÆ fPGL(‚:FfFdèÿ€c®ã^‚æG:f_úh®AäA°ÏzÐ¥jbÀl”ä:b@»Bè%[ %,ÈAlfgJfAdÀnþfM¨cD&2„ `£o¦æ[bä|äBpæ:ÖæPægbçxDç�jÁxÔi>VE?.)r †„fAŒçètä� †O\Fô„X¶†cÎÇŸþ£lp4…�.�¾¨"ܨXæFZÅ(hJhEBXè0h~†OêA¤&H?új*htçzŒ®$a`­ÄsâÁIæ‡{&$iL¥;vj F¡hˆŠÏr^)@Ú�"à§` €h®Áè�F¨šÄzÈVÚEKÊL¤6ÿE€bÄJ<ˆ‘„XDŸÊèA˜j»´‡qôiFìÁDE»NêX¨AÄ*ÄäjA ¦dÔdŸº92„¹bÄäAÁ¦eb:ÄÈflüèZFD½Z¨B ´K‚’lA�ã\²EDl¬A̬Œ+<çCÜë<�*[”G^H£C@c]´¤D^ìAÐ(‹"Cà%7Ö(a,lqNcÊ„L)DجAH'E¼hCüå{ú¢dìÇ쇰¨zÀlA @ƒ¦(a`ªT‡C †jt#CŒlpj¬9BD‡åT¦(LåGÚìoðiX,ÄnÕò,ÇòmÝîiAmF¨íFd¬q-Ü®¨ë„èÒ.ÛJÄå„Ä fM®(b,çP‚æóú¬ªÆkSB¯õöcƒ¤F8ºª+º~oß.¦SR¦Ã2nœbí�j(Ÿ6lg€ml¼d�®+CÜîšò$×ZÔÆF^ÌnC ì·ÆÅñvmñžnù%Û’/|æcyb/ãÂ¤h/CðopÆ…ØBÄ�räBЭªú¤<æ�,ç‰.Dþ†í|ÚãÈêîF,,«&[¶é[*0jp®»¯ªbd«£ ­òæ§Ôê„k.¨oJ.ÜÚE® ñPVÿ¢²%E°f<1Cñ‘ÊÄ3ìÞ.ÄýR�Fàj%Ôº¯�nAîÄŽ/;†1_8îãb±9n±ø˜D“z,ñB„ú¾eF:%R,�yô„ƒè¨Â–î{+×GChpëB„Žfq³Eg0ÇŸªk²oDüFb¢k«ª°Fðn÷F®ðzdºJ©dp×*) >ÅÕb1ïbŠ’ñu²„*nð«F²Ä'å‚hÁذfH†Ñ¨q#w2Flñ' nû¦«hü²®{è>22E¥4û€Æ[£)«&�¦æðbñŸ±…6ì7¿ï›nt ª/W§T83ãú²Žê@<³ÿ1CBgPmÎ6l ã$ÛîóUœï[E ã)�šiý2„!×°Frð37´ã+å_âõ®F57„Tôhà`nÄ|â‡AD.3†d°- #/äÎ//„1wñ[&óÍ:p·¬HÇ$�ÅOêJ_1ã¶qƒf4Cpòä¶23;ÇV*MD—z®8?ì{þF �q�pFï¥D²,C$èþ6lq,ž"òhÌ£|P¶&îvdU,CGKîEd¬W;²sŒëI~23—²/ó§8¯ò1’k0"s=·5G@cT¸#kôPtG,goB\++aêä=²År’°Æ®Lp4†´ôûÿ8ÿ.3,×£a i—6hŒ†ïh¶C_vä.&è­U'5Äœ„…"rFk3*?êâÆðCµ©þÀè r̶d†OVª[´¦}v0Ç5&ì; Ñ ,ãz£ä#9[çx°»&vC$¶F[„4ò( ;kyž$pï‡dΫÙöÄú®=/@ã¬âAòÊ~[ç¬rõE˜jyf­Öòñ¬6GørôHODí®£|Èíãw�¯ö„‡FyÈhÖŠGò'…Ó¥=ã÷H&6È �rtF²ÇdઆXHwbûilü@p´¸Kxe{F*ÿìhÍr´Ì~{8…·†|ø�#8e7ÿé@Ê}ÿ€¯–‡8‡=—§|Œ­^¨c‡~rÚy�W’ë“çG8ª(…Ï8~»Än`F‹Oø�WqƒgxFÏÇ[À¬8_t©`�±—‹Ñâwj´är–ÇMì÷Òúc¥îÇTf4†ÊÇk‚øA€,8EL‡CD¹¯A‚Út“bä—ËÇw©¯ÊÇ“«xÖºí¥GLÔ²2Ɔ;oÄL´…âASËÅI„ïmìÅ,‡í`d„<7gFì¤E47/åzbد[T@„­S„]7±,?Æ/ˆŽrD[ð’_HÄ\Ï\(2¬ëFNO*YÈD³ßòR»°ãê­ÿuŠÓÆœ»:™±C ´‰@ÿø�˜�Ì*»;°WÇq§zPûõkðp�¸{xßû�@c†X:�è8H °r`¿C€ÐÆœ;™äcǺ§C´æx´G^kˆXŽGÓº–LüuÄÇ|$ôë"P$H¹«(Ë÷ëˆ8È…Š„$ŧ<Îç¼Îï<Ï÷¼Ïÿ<нÐ=ѽÑ=Ò'½Ò/=Ó7½Ó?=ÔG½ÔO=ÕW½Õ_=Ög½Öo=×w½×=؇½Ø�=Ù—½ÙŸ=Ú§½Ú¯=Û·½Û¿=ÜǽÜÏ=Ý×½Ýß=Þç½Þï=ß÷½ßÿ=à¾à>á¾á>â'¾â/>ã7¾ã?>äû ^AËy �E~ÒkS¸‰ÿ‰ÞÙŠ$àc“×õIbQOþßÑEPL€ Š*H1˜¸ô Æ<—§ÙaæÉ#4 W\dÈ %6”c”Ý': ÆHèLòSÌá…†Ê,¿Ë¨" (MõsEü°�ÜÐ Å;â‡SÓ=ÿuD"ø~ <\Ýb×G0¢_ÒÑwÉÍæwDÏà€Ô|¾jx¿F ?ÕD >ú0xa„%%L‘HaD…€$V´xcF� ZPxA GŽ$Š4yeJ•+Y²$É2Ä–c¶tDb&Ƙ !Ä‘"gBŠ -< jrhN†G™6uújT©SMŠTaÍ„‰T�P1ÿ!¤D-,´ˆ3äÁ˜Wj9›GL£>®¤h‘ÖGLHnK”�yRlׂ'µ\˜˜PìZ‹�Z” q·¤·‰�5ykט&Jñ8dL°tõ¬{7¡£ŸSô<ב£­é–úTK �i™‹ÆçÍ 'û`L"fGgÿ2Çì6E‰®.OV5mÉ1µ0“³Æ‚‰úªïc9ëÑÄíNÁv½�—yßrŒë˜ù_¸ºß£Lî½3 š,2ëØÚ¢ö Ê­„õ Ái¡ÈÎ-±DðÚè'üÒ"2±ÈŠé» ­³P-ð"ºJ!è¤£Žª]|Ƨ¢ �m´à<‹ ûÿ3.HÄH®¸à6ƒ–Û-¿­¤;H¬Ï¶¢,6€´M­Ê‚Ä‘OºBÊ+IDK“®P®"�Œ©2%jAƒ+°LÄÃôÌ‚üX4òG¹¢ÎÔ~Œ N�<è 8cËs6„Vr9@"ãr7Õ�Ûì1§Ú»B ±âð Gê´Ï¶Z°Ò‘G®p´ÓQá4/U%ü³Ñ;UP Ûâòȸ±êdîN�.¸©¾¬,!.¯+Ìê¤  ˆ$ê ºÓ/£B-P�9Y™DNOv;É-´¸Š„¹�íH™ÜdÙÊH¸âT¼`há°ƒ\�•X‘�c–,zÏD’¹Ô,x¨3 æChÿG‰Ñ@ÑVƈ%ž˜âŠé»xK�”!./ôá‚øT€X¡‘Ÿ¨RÉ6触°E,hº�ºÇ|uîÙ Z�à°ÂU¹ƒ/¼‚k”ØýÍ8@’V¶X3 JV29Ûëu£±+*Œ3Ž9J˜„ÑPÔû ;d.ݺ€º ö h…–æh¬… ¬¢ÊÃ’S¡¨#² 1Â-�ôÒMGéj ÞΨ"?³ààº#3!¢-RÝ¡øÚ¦î°4h 5õ�ó‚â/¨:!ð¦Ktqr\GÎýËÿöªtwDkÖµMÊ‘JdÊs‡Úºúäï;ãú]ŸÌ¬ës’Y¶ú•p@Þ Ã+ aéÕbm-üN W‰ÝÆDâ9. -g]SH1&ŸÈ0P8€Ä#–C·úi«{'ITX>²ºƒ<‚�\ïè"7ƒ`ît)Tá GÇ!ìÅG9ºÈ»pp«�}&? òBBx¢*d^Ñ ¡Ò–Õ”àÒ!ÁÍ6âµÄk)»Þó6ÜU¤? A›‘Lص¤mÑ"u!�R½›˜ÏK9ŠèÚ7»ž 0‹jéÙ;ò�ü@.ÑÀò('<„˜,UH|L nÈ‹4&{' ß캘°� Ñm>ÿb§vØé©Ñ|PUvƒ±Hþó ŽHé`*"O\ ^†H6¶-ÎŽÌÁ# iYK[J¥1°@ ,°ËJr„�Cª]Ï¢œ_FäŒÐ*ɤÈCnÄ1nâŸøX‘Mæ1"�H 2Ñf4¡\„�YÚä˜ôÄøE¤0_y‡(Ó™¿$ç ªT‰äÏ·M й“#~wBmÑ“˜h‰ÛE¢hû%/2}чp€#Ž’iï 'åH-fâà˜ÁäÔÎÙ@.Ve[™$z2ŠÎ¤ Ð$ìL�â“AŒ„횉"@ Ñ[Þ§9Í�™”l ýŒÀYÆó›Á˜T2À…è.�ˆµ!á±Tœ¬òí%+£VäW%̉0ÝY­*Ïš[ͤNI[ZÓšWPňT�ÊÀ°]“‰cýH¨Ú¸*ðo^kÏI¾ò<&ZT"ú“kù¨»8º¬3‰®DÚzBýH/€ì«N�ŠØDtF&}ÑíªÖ���‡¯`5/k¬òî`ºM`f¢˜!€ÑpJ½„D0„!,Ìo¤ëƒs2.ZŠ•œ…Dÿ‹‘ÛzË!2ü¦ ~ œêå´–ð„7Ù\ŽôÔ¡BC^n¸öW±,DcYâ«…Õ‡3¤àH´ªEÀ Ô創-ÙóµvÖ–D°¨¡ÔñU£ù 2<.�‚µŠ¼†Ï¨¥¼+FàÛö2OÇM«,qÿÈfÂÀ�JÁoU›3¹e¨›bd´©Ÿ¬‡eÙŠX‡¤ŸV† ),ZдÕ“ NÇÄhÚs•DÈ*Ë£]†"…’S)çÉÎ KE5°šC*vHrÞò„)]iÖ¤¼:±ò(â8E§x¼ÃÓ¼ÄÔ°‹—) g†é¹é1°Õ€àœ´L)Êhh—��ÖDf#ÿÑ:_Xuõ«ñ"$Î&Í$Ô%¨-˜*:놳qƒà³° u.1‚Ñì1 "º0BP\RPýq ÷ "H±!û‘»P$ÊPfp>q'Ò"’p!!" @&ø1!#Q!.’ãPHQcò$ B!õàcõ%×2@�Ñnr"z!’P1€‡Q aCP ÂñÀ$1@#/ò#¡Âíñ%#" I±(_rB%—282"Ö`Kr*[ò-aR&©" "(YR'¿²-dró1/ÿM¢ ½RäQ!º2y%®1!´1%'2B!i�)ŸÑ'÷ M2.ÂA�"Â+ó.�‰p"CÂ#K‘*ËRÚ!r 2 R4#â(ÅÑ´x1œÑ'`,+¢ ‡S8á7ÛrA2@aÀ uÓ!|Ó$2`9€9ëò6í4iÐ9IðEp Sr¦2H2"W+2òã3"É’™!Öà‚@ÖàÁQ$1@öÀ$Y²U0?×óôAC. "&÷@´`;½PüÐ8op}À-@;ã’³:ÿ7ó Œ0"á¶3@â±’:­s@úó*?Q6Aâ“As¢5µ³9iP!A‘Q2€7U”P~Ð$÷Ó(—“@g42”9'sGÓ.ƒ  q1”“ëpB4FÔ ?ô(Ëôz°9�8íÔN§Ò7 €44Eð`Hi•–‚‘3 6*ŽQ!(–D‘RÄ6]Y’!ÿ)}€÷à -t`aònÏÕ ÍU$ƒ6#ÿü³dB\S0!æ]cõm} 5BÂES’k9¢“ö  Ò(‡ô çQ1;ö"ŠÖ #Ô6%„²j»–tc5`“r3-6YTg÷a¢Tr÷¶nóÖ9+RS³±O× Òô:@@Š—$sÖoÃus2[Ïõ_Ö(wZbò`dGQ5å¶EeQ òw×k¿m½7#.p Î}Ñ÷zgqò ´6)ÉW#|ׂ`?Áö Nñtév@òöu±[Û"p9B-3×(#âpõ|Ö¦±mSòi‰u=�tF‰l-÷w‡Tð€E§±F“r#50T!ÿ@a`1TtQ‚~�—ÿU$Ý´$i˜ucµy¡7\#Âl8/¸0ù–do·r·‘gÓ7}ñÂaD'q‚ s,“7k‡·017q�Øq÷õq5"à8Õ1LX]B†µÕˆ÷gÃ÷k›5mE‚:3€Žë¸Ž™T"x˜h%Ž_ØF#Ó~ÿ³YµàYù‘%ß6‡ù–ƒ³‹“úšð9óàcøµ"Y‡óׇw—ŠÓÕQ­0>]1!8�‰5 ·_OQf³4[ÅÖnySàP…Y"bÑØtñ/U’x#|Ѳ‚5Q‚ã•�-ÂA70H™V�©r”·‘Cíš½0uÿQù ,—¯Á¸Š/ Sðô²y—V"*±�ï3Œ3<™sWÏøwç–{åW[Ù�S9bv0i õl‰ž1‚~×Òm‰ãYçv�};ÃxÍY[×·" x,Å5i!Ø!ÛØ,¶qË5^ÝW"z¢5x”•°”ïõ_ xS‚ˆ-4[ky\wwgHU"†M—†sÖ]å¹á~¡6vã5‚+£«Ùv©w-Ú"æt{ñ–×§/BŸ ¶‚R"’fãÒs™šëWz›WŒÇpš‚uµ·ˆ‰:""wžku|ïk"£It�ÁUĺlù~‘RºE ºZõ±yÿ¡‚_T@!¹\¹\)·1›•~"m7³{‰pT/$<9¹‚Ç•%í– ¯9"d×(ƒ64±˜ù:«%¢«[rƒ tt¡¸©{¬“’Uö"”ÖF#²]/DLw·{ÕJ-ë¶™+2[øŸ‡4 ]S!ž:³·Ù‡K::·¸³“[ÓV ïúiK[2Û˜�Ç• ÉšhOù¬k©žãXFV;9sM:)Á5µAx†“ó× S4n»;§ûv=ó3†5I÷{d¯úp ;�á5)Ï!N8)™Ò!‰˜±‘Kƒ”ã˜�ºÛ©™¹rEºkU¥KQCÅ¥‰P Ö3[ÿÙ³%³W5é;Œñ œ7b©);]kú~…‰uTÆNgö5´Y#ü “˜ÅU›m²x‡ºƒ›€÷`ðÂÃw)¼šS{H•û¯�Ö“ã²:ººQ²{UsE‰õRy9¨w,v*yq9«Ö¾íV!f4¼k©añ ÃŔλ*ÀMu fA0bÎëüîÜqéÜÝŧ¬F×3@ÁÐ:€ÎK�`×Ò9�1's¤ËÕ¨©2µ³µ:é\ïšÎ‘øNÑQÝ×”$×ñÏ¢Œ1”`g3%±ø³}Ú-¿’u 6µQ½‰ŸóAV>{a[0À9°�µý¶ A³aA�Oÿá6@-4Rõ›¯ð›ÿsx×±x÷3i£ö³E°Ôa_Qb•?�*=?ûÔéœÎ³�q«sMŸ=iÄÔU…½p‰×Ïi½pç²~÷sÎß�Ы–$?;óž0méÜÒéÙ^#ì\ Ä4Øcñ}ÝMÓ=µßvRû%�Î÷Pz=óÝ߽ƫAi°fó`ÝÕÛš*¸åYÔ#KR �P«ãQýg«ó‡7&éœA^+]2›:α¯ Crž�¡¹UB¹/B™ûµ¦™”¡¾-ót «>â™~ …S/âÆAS8¹5ç�#‚�áÞ"Zp²e¤í¥¾"`ú8ÿuÃLÊ5‚@áìqíYBp6'‚ó7µ10ï ?#8Z$f9¦Þ©`ð›žV¯¨;Ýó¬™bªš–ô¼]ô=0¬˜* Qó[ÿbU?!°ûôR´tvœ…¢ýõpQWÙò�B<ßõÃ{Ø‹·>ÁR%½6?OËØz¹ÏñEoù÷¼Q‹ñƒû]$·u¬pû�õó¼f³_%Ä¿÷pû¯ŸüןýÛßýßþã_þçŸþëßþïÿó_ÿ÷Ÿÿûßÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ?þÞ2òN°þ¨3¹uùÿÿÿþy1ò‚_Fÿæò¨°%àPûP¼1ûùÿÿ_ÿGõùÉðü¥"´!¯ž¹§soË#â¶þÿÿÿëy·KÇ�wñÅEB¶±�³+ºÿÿÿñ¼û¹%BK†=yôò�0!Â5AòäÁ ÅÇ…>êÉd� *8`O Š u4̳G‘"$[º| “âš>̳¥ 9út)ˆ Èœ-×`Èà#(F�%9zô¡ˆãH:8šÌƒTÁ©Q1hä‰ñ)B A:&4Ê1"IAxX¶Ô�GѹtëÚ½‹7¯Þ½|ûúý 8°àÁ„ >Œ8±bº3)ê˜HX‘€=‚Örè6a  2` ÿd„:& Š*€­Y„’)¯”g@æ…?‚(z�'®]A‚õöý[�Ú¹Šq�áSº;{úDJQ‹Ã@Šé m—¡qƒŠ2ü�Kƒ £êуPKrŒÅƒÈ H€\£…<½}B x‚<5ZÛp J‹ˆ`‚ .È`ƒ>a„N˜�l:¬±Æ…Vxûå¡ÐÌ&âh> 0ià¡P‡‰GÑä)$Ÿ]*�æYŽ* ˜Óý}¤Ÿ]dŤGÙw—�FjâG3&$H�>pÆšn>d°¤zü ––Oú £L4¹$@–-¹Gašj®Éf›n¾ gœrå£ ÿ`§&ÆQKx˜G—�%I�˜8$I*$ž€Ë-ØX‹ÞÕçKŵ¤GŠs‰ÝžXRThB¨Ušf{$tI�µøƒ€½„t.eÀ✶ފk®ºîÊk¯‰qÊ*aq”Ás%Ô*I¢í÷%iµ"¤‡ŒÅË“™`þ€R�2v!†Þz›]“%í9W¥.™ûÑ©uEªÐ³}ªP¨�JÚ˜5©«¥¦ -šP˜ ÙK’xè{$<úŠp /ÌpÃÆ®ŠÂÊèSÅO‘KÕhè’f"JWì¡BÕí!@Ç”AÊ*¯œÁÉ/ELÕ¤B6KQC%î¦2#KQ± JIo̦¢ÿzs�Ц֫°ÊÚ­? uÔROMõ›0—J’gÂåÓdÎÆ¡9#t¨B]çUdƒWcœÓÆ=[;Í1Á¬…§ÖÊ;eI2…ª”ó&Ì$QùRrUŽxâŠ/Î8LWïœxœ]Ð…Ã�GÇš–殈`+KyMc#ø¸K6m»Ü’në⺡Û]³”ÉRe¯M¨þ ïxªN4L©¾d`ãÂO|ñÆÃY:¨’ ZØ•hBZŽÐ{˜("e¨)âîFϳ–GŠ>n>€×¤S¾jÖ’C^ÀZø¦Ðfê€Q’”×M‘dji¡‡@“*3ÛAk3Ú# Kö€¦˜°ÅepaÞÿñÁJp‚v¹~0£Ü\PS{ÀÃèîŸ�äÁN^ÓÁ–.‚A�ý(!¸HXB®„„-B‰ÿ‚Ámé‚àIÎ#¹"$?8ÏŠ6Ñ|9éà="ˆ\ðQî¹îز!Ñ<‘*d [Æôª 8$%/�ËÍ'·AQp�ll£ßH‘…l/–©Lyó9X¹¤Ž¨+Ioá26é`k/ã㨣Ëô‘.J£ûLǶ0Ž”¬¤%/Ù0ákM—ÂdK´ÖÞˆ2”‚È#bô0G˜ôŽ(õ1Ô#= ËXÊr– D[ÚTb²yê¥/Ïx˜FÆk›¢Yghÿ‰Ìd*s™…Y¤ƒ.Ü13M°ùÁ â2‚Œ°dºœ¦7¿ Îpúj *I¥8¤…rªäeê\É9ß ÏxÊsžô¬§=ï‰Ï|êsŸüì§?ÿ Ѐ t -¨AŠÐ„*t¡ m¨C шJt¢­¨E/ŠÑŒjt£í¨G? Ò�Št¤$-©IOŠÒ”ªt¥,m©K_ Ó˜Êt¦4­©MoŠÓœêt§<í©O Ô  u¨D-ªQ�ŠÔ¤*u©LmªSŸ Õ¨JuªT­ªU¯ŠÕ¬ju«\íªW¿ Ö°Šu¬d-«YÏŠÖ´ªu­lm«[ß ×¸Êu®t­«]ïŠ×¼êu¯|í«_ÿ ØØÀ v°„-¬a‹ØÄ*v±Œm¬c ÙÈJv²”­¬e/‹ÙÌjv³œí¬g? ÚЊv´¤-­iO‹ÚÔªvµ¬m­k_ ÛØÊv¶´­­mo‹ÛÜêv·¼í­o Üà w¸Ä-®q�‹Üä*w¹Ìm®sŸ ÝèJwºÔ­®u¯‹Ýìjw»Üí®w¿ ÞðŠw¼ä-¯yÏ‹Þôªw½ìm¯{ß ßøÊw¾ô­¯}ï‹ßüêw¿üí¯ÿ à xÀ.°�Œà+xÁ n°ƒ áKx®°…/Œá kxÃâ ;scikit-hep-boost-histogram-b4c49b0/docs/_images/ex_hist_density.png000066400000000000000000000524161524660700500255630ustar00rootroot00000000000000‰PNG  IHDR€à5ÑÜäsBIT|dˆ pHYsaa¨?§i8tEXtSoftwarematplotlib version3.1.2, http://matplotlib.org/%‹¡J IDATxœìÝtUõ�ïÿ×>'9ç$@¢ˆ$‚� Òª£åGê·àj¦±u]'�ø:Y.\Ó+­Ç[pÚÛiÓŽèJ§²¤t®µwÝqÉõ¶ZÇ:tœ´èí’– ²¦T¥µS šZ $çÇÞß?(G#Éç Ù9 ÙûùXë,å|ö�ÏÞçœ�÷ÙŸ½_Ç ‚ b#1ÖÀ袈 @€˜¡ˆ @€˜¡ˆ @€˜¡ˆ @€˜¡ˆ @€˜¡ˆ @€˜¡ˆ @€˜¡ˆ @€˜¡ˆ @€˜¡ˆ @€˜¡ˆ @€˜¡ˆ @€˜¡ˆ @€˜¡ˆ @€˜¡ˆ @€˜¡ˆ @€˜¡ˆ @€˜¡ˆ @€˜¡ˆ @€˜¡ˆ @€˜¡ˆ @€˜¡ˆ @€˜¡ˆ @€˜¡ˆ @€˜¡ˆ @€˜¡ˆ™X€=ô�>ð�¨ªªJUUUjllÔ¿þ뿎u·JÊ ‚ ëNŒ•ù—Q2™ÔìÙ³�¾ó�ïè¾ûîÓK/½¤?ù“?ëî”D¬ ÀÁLžMœ8QO<ñ„.¿üòA§íïïWñß¾ïëÍ7ßÔyç�'ÏóF«Ë€q(½ýöÛš6mš‰Ò]‚ß××§l6z9©TJ™Lfz„³Q쇀³Ù¬öíÛ§îînýŸÿóôOÿôOzî¹ç-¿ð…/è‹_üâôû÷ï×…^X’e÷õõiÖ̉êö±�iÆŒzûí·õè£�jÛ¶múá8Ö]`Ø �¯BˆïÎG^¬ƒ :¤åË—ëýï¿>ò‘�èç?ÿ¹~øÃêOÿôOǺk ›¯ ôc86nܨúúze2544hÇŽCNûË_þRŸüä'U__/ÏóÔÞÞ>ètÐ_ÿõ_ë¼óÎSEE…®¼òJ½øâ‹ÃêÞë3€ÿãü�±î‘°e˵¶¶jÓ¦MjhhP{{»š››µwï^M�:õ”é�;¦‹.ºHK—.ÕwÜ1è2ÿð‡?èCú�®½öZýë¿þ«Î?ÿ|ýú׿ֹçž[ê͉¼X€D‘/?Ô՞Ùû�ЪU«´råJIÒ¦M›ôƒü@?ü°Ö¬YsÊô ,Ђ $iÐvIúÚ×¾¦ºº:}ûÛß.>7kÖ¬3îNEˆÒ)ñ݈Ö]º%½ÓÖ¼Ðޘݺ¿$ä�žæ¶ów„„}‡´AQâ»”ßX~˜ýò}mî;óæ�Wî„|ïrñøPB„|œé¼ÙlV;wîÔÚµk‹Ï% 555iûöíÃîÇSO=¥ææf-]ºTÏ=÷œ¦OŸ®ÿú_ÿ«V­Z5ìeâ„X_†ÖÓÓ3à1TJÆ‘#GT(TSS3àùššuvv{ýÿùŸÿ©‡zH³gÏÖøC}æ3ŸÑç>÷9}ç;ßö2q 3R7�ÔÕÕ©ºººøhkkÝíð}]}õÕúÊW¾¢«®ºJ·Ür‹V­Z¥M›6�j?¢ˆ!`"ÆW Â0ïä=9¿tâWKÞ=X®$M™2EÉdR]]]žïêêRmmí°ûqÁœòË\—]v™¾ûÝï{™8�3€`PUUUC€©TJóæÍSGGGñ9ß÷ÕÑÑ¡ÆÆÆa¯ÿCú�öîÝ;à¹_ýêWš9sæ°—‰8@Ä„Éò;9ÿ™jmmÕŠ+4þ|-\¸Píííêíí-Þ¼|ùrMŸ>½8ŒœÍfõòË/ÿÿÀ�Ú½{·&Nœ¨K.¹D’tÇwhÑ¢EúÊW¾¢¿ü˿Ԏ;´yófmÞ¼yØÛ†(ˆ˜Ñ¾ X’–-[¦Ã‡kݺuêììÔܹsµuëÖâ�!ûöíS"ñÎÀãÁƒuÕUWÿ½aÃmذA‹/Ö¶mÛ$�ˆŠyâ‰'´víZý÷ÿþß5kÖ,µ··ëSŸúÔ°· 'xAâs't{‰þŒßLؘ�Q!gs ŒiœÇÀXó[ÂÇÀ„<¬•0&ìü¡·-,b`†-ä´MßWww÷€ëêFÒÉ¿K¿z¥F“& ÿ8õöÛ¾ÞwYWIûбÅ@Œ��E”Yà…Ì[s.{Ìsö ÖüÖ¾+7 Æ�ÙÛûÇhù½ÔÌù3‹(ÇÂæô•¸€ ûÅÑàÿñf~D Syp˜y1>P1…àÄ#Ìüˆ6b`b†3€D ×ÂB@ÄøòTÐðï¦÷CÌ‹ñ�!`€˜á †¯ÔYxÖê�¨;K.DÔJÈ,7+fÅ+s·‡Ž‘1bVßê¿‘{i册�1û—0bxü¼{ýIÇü��c½­Ì}c¼·¬ùCÆÌØ12c›ƒˆÓã§‘vdÌ�h£ b !‡€ÃÌ‹ñ�!`€˜á Ã@X(ˆ?ðä׫Zó#ÚˆÎ1 ÃB@Ä”P!Ä Ÿ6„ Œ³1Îã2³ê¬ù­œ?kû¬ùAXVŽŸŒm r9÷ü3‡ÏÊÙ³–bßH2·ß쟱~¯`eÍYY}Fÿ\Y{Ʋ­ŒFsßû6lNŸ™Sös64ìq‰œAIRòÀ€k#�kb†3€D ×ÂB@Ä‚„ Aˆkù)¸Èc f8@Äøòä‡8Çã‹S€QG@Äp ,€ãY‰ãÂÆ´„�ƒ°ã.Œ¸Œò�°dˆþ[1$©”{~+%d”ˆRFŒŒÅ7Þ[f{ÈþeÃÅèÖgÃÕÿ„³b¯ÜÝì}3â9<ÏØ·Æ¶—:FÆúÜ€DLø›@(Ô£Ž€ˆ9q àðGAÂÌ‹ñ�»€b†3€DŒò·€¹ 8ú(ˆ®„…€ˆñ• N€g³°1/ÆüVÌJX¡—oõ?emŸ±ÿŒ¨¹¢V¬˜–²�-«oa·ÍRnô¿`¼÷¬×ÞŠ±Î>¤�+†'ŸwÏïzý¬m7…‚³Ý³bbrFNÒØ÷îÕ›1/VÿíãNȘ˜Ç_qA@ÄO#3ÒšÑF@ÄBÞR`8òˆ�ˆÎ1~��â.`Ÿ»€#�€ˆa†€ÀˆØ¸q£êëë•ÉdÔÐР;v 9í/ùK}ò“ŸT}}½<ÏS{{»sÙ_ýêWåyžn¿ýö‘îv,Q1¾Þ¹x8�á„élÙ²E­­­Z¿~½víÚ¥9s樹¹Y‡túcǎ颋.ÒW¿úUÕÖÖ:—ýóŸÿ\ßüæ7õ�|`=Ã`>›…Ìù+5Ï•“w:¬¬<ã”Ð9FÖ�çÚ¿IcÝÖ¾1ÛC.?dV›2FÎ^È,/kdáY×/™9‹Žö¼±o­œ?ëµµö­o´[9‚Ö¾1rø<ãs䌌ErGEø è3Ÿ÷�ЪU«´råJIÒ¦M›ôƒü@?ü°Ö¬YsÊô ,Ђ $iÐö“Ž=ªO}êSúÖ·¾¥¿ÿû¿?ã~apœƒêééðèïïtºl6«�;wª©©©ø\"‘PSS“¶oߪ·Þz«®¿þúËFx€DÌÉßó�¤ºº:UWWmmmƒ®ïÈ‘#* ª©©ð|MM�:;;‡½�=ö˜víÚ5äz1| 1¾<ùþ¯yœœwÿþýªªª*>ŸN§C÷ítíß¿_·Ýv›ž}öYe2™Q[o\P1ï>‹7Üù%©ªªj@8”)S¦(™Lª««kÀó]]]æ CÙ¹s§:¤«¯¾ú�~ zþùçõàƒª¿¿_ɰעÇX¬‡€ÛÚÚ´`ÁMš4IS§NUKK‹öîÝ;ÖÝ`\I¥Rš7ož:::ŠÏù¾¯ŽŽ566k™ùÈGô‹_üB»wï.>æÏŸ¯O}êSÚ½{7Å_H±>øÜsÏéÖ[oÕ‚ ”Ïçu÷Ýwë£ý¨^~ùeM˜0a¬»À°„‚>óy[[[µbÅ ÍŸ?_ .T{{»z{{‹w/_¾\Ó§O/^Ï—ÍfõòË/ÿÿÀ�Ú½{·&Nœ¨K.¹D“&MÒW\1`&LÐyç�wÊó8s±.·nÝ:àß�<òˆ¦N�ª�;wêÃþðõêôy®¨ …�K°–oÆ-qfŒKؘkþT¹»ÝbDa„Ž�±^Û2cßûÖšßËQi÷¾ó¬(ký9#j%0¢HŒ˜Ïzí]ýïü.ÈÓšW²c\¬ÏMʈè±äÝ1-A6jñVLŒ“øVáa¼7,¥ŒÏ:‹"füÀ“„¸pó.[¶L‡ÖºuëÔÙÙ©¹sçjëÖ­ÅCöíÛ§Ä»ŽMÔUW]Uü÷† ´aÃ-^¼XÛ¶mvßqzb]¾Www·$iòäɃ¶÷÷÷¸¾§§gTúÀx°zõj­^½zж÷uõõõv.¥± _¬¯|7ß÷uûí·ëCúÐ�§–ÛÚÚÜ_WW7ʽÀæÿqx¸�0!Òx…ÿèÖ[oÕž={ôØc� 9ÍÚµkÕÝÝ]|ìß¿{ÀéñƒDè¢�!`�8eýôÓOëùçŸ×…^8ätétzT3�J!Ö`úìg?«'žxBÛ¶mÓ¬Y³ÆºK„V�§Bˆ è0ób|ˆuxë­·êÑGÕ÷¿ÿ}Mš4©øs5ÕÕÕª¨¨ãÞ0%�¼-#çO®œÔì^ÊÊt‚l¸ Cë¸j溎›VqhþHH\[‡³ Ã/�ÀB@Äp ,€DŒ¯�?Ç5€‘G‰3œ b‚�wœŒ<Î1~à…~ ÇÆ�U__¯L&£††íرcÈiùË_ꓟü¤êëëåyžÚÛÛO™¦­­M ,ФI“4uêTµ´´hïÞ½Ãêâ àX²"ŒÈ�Ð12�‘G`ĬxåÆÛ§Üˆa1bhdÄIe Ì½|¿Â½|¿lèý[H»—�›dÄÄ»&Ÿv¿¶~¹»=;ѽü„‘fá/MÂJ 1æ·–Ÿ —"kö²cF{ÿÐû7ðÜ/^"ïþ\y…p9 žñ¹õ²Æ‹c}.ÍÇ�¼õæ02ެW/lNŸó¸kdÄ #î+N¶lÙ¢ÖÖVmÚ´I jooWss³öîÝ«©S§ž2ý±cÇtÑEiéÒ¥ºãŽ;]æsÏ=§[o½U ,P>Ÿ×Ýwß­�~ô£zùå—5a„RoR¤Q1cqð< U«ViåÊ•’¤M›6é?ø�~øa­Y³æ”é,X  HÒ í’´uëÖÿ~ä‘G4uêTíܹSþð‡Ï¸�x f÷äü’ÔÓÓ3àùt:­t:}ÊôÙlV;wîÔÚµk‹Ï% 555iûöíÃîÇ{uwwK’&Ožú¨¾ÿýïkÒ¤IÅë «««UQQ1ìíà˜ îÌ)/iåe¹™9}!—o ™3hæüU¸óÌü´•Å.Ë/[=t{vR¸?Kµ»½`\¦ã/M`¼5¬¿ V‚„gD¹™9ƒFÿSÝîö|…µÿ‡^A>ãÞ¸ô[F~g…{ç–ˆg¼ïÖçÎÈñórFŸñ¹ô¬ÏuÖ=ÊÈ)4¶/ð�7WÖ½}aòUϦâq, ÀeË–éðáÃZ·n�:;;5wî\mݺµxcȾ}û”H¼óù9x𠮺êªâ¿7lØ  6hñâÅÚ¶m›$顇’$-Y²dÀº¾ýío릛n:ã>â€DÌX€Ò‰kõV¯^=hÛÉ¢î¤úúzVAo}¡Á°q @Ìp€ˆ ¤P10œw‹> @"f¬†€1~0 3œ@’¼„w�œ„…px OÞ`Ñ žq‚Õ8ИqVŒ‹÷ #.ÁËY"åF\C*ånO¸÷O�±bb¬wÿ#ÆÕRH»÷m>ãn/dœÍv»ñÒä&–ö žDÎØþjwÄQâ¸{ß'Œ$ùÆ{;ä߮±¡Pvܘ׈‰ ŒÏµŸrÏŸêvïkù‰¬ûµ±Æ…¼·�¹'°ŽKÉc®Í3v­?Žìò·Ýó—[)+}îÏ…oÄ#•çŒ H¹?×^ÚÈ òÝï sþ¬s“ͺ緎[Îãªû}XÛfs¼À³V1b¸–X_ØÛÛ«9sæhãÆ�cÝ€Që3€ûØÇô±�}l¬»Àˆb–X€DCÀ°Pž�þþ~õ÷÷ÿÝÓÓ3†½žX_x¦ÚÚÚT]]]|ÔÕÕ�u—8EðÇ!àá>8}€g`íÚµêîî.>öïß?Ö]à�¤ ñë @É1|Òé´ÒV„`\ò’I…QÊiÆX¬ À£G�êµ×^+þû·¿ý­vïÞ­É“'kÆŒ§½œÀx§~_23¡Œv‹gåü2¯Ìxù“î¼1ÆwD#g00òÆü”‘wVî^~ÁÈùËY~ùŠ¡Ûs�³*7ÉÝî»ÞÌù›è~m+Ïu‡ÉYQi–dÂÝ?o�ÏûYÃKÙœ{eûŒxŽ»9÷¶;C²P9ô{ÏO…ËùÓ¤p‡e}FÎßѼ³Ýϸ?Weo¹sö#ßÓ³òE­+äq1,Ï:î9XÅc`3G�/O¿‡X€/¾ø¢®½öÚâ¿[[[%I+V¬Ð#�<2F½ î†%Öà’%KFõÀÙ Ö QäÞ‰Ÿž 1?¢�€ˆ9y7o˜ùm€D ×ÂB @Ìppx O^Ø\�a­Øˆ™I¥Üó[q V‚‘‰hżeÆú�íóËŒ—J#Æó"I…Œ£Íˆƒ,»>Ž…QáŽòHOtGuLH»Ûs¾ûµ�˜îw¶OJ¹ÛFŒl_!Ü¡ç÷ÝUÎvëìE¾Ìc“ó†~�ó9÷¾KäŒÏ¥ñÒûåFß�ø"ë°^îÞtåÏ©p/½ÇýÚ›Ç�¤Ñßh·–&fÆ7Æ=£ogÎÂB@Äp, Ä gˆî†…€ˆ9Q†¹p;ƒ³CÀ1Ã@"†»€aá `)yìÞ¨ÊU1>Wf„¢kÓƒx ÇÆ�U__¯L&£††íرcÈiùË_ꓟü¤êëëåyžÚÛÛC/§�3€¥æ€áÊ«’Ì‹4#ËK•‡[¿ÅÈó ŒÂ|¥ûí™­vç�å�¬>ߨüÜD£ÝQæ«Ýû¾ü\w–ZY™{þIîùgVýÁÙžÜûþüôQg{ΘZºÛÙÞ“wgÍì«v¶Ëhî7r�eÝA�os¼9r gèFN�qf%éŽpT!eä Z1z)÷k—ÈŸ[#¿Ó<®˜Y{îvÏÊ 4Ž{AÞ�±irÓ½„»o^àIþþ°e˵¶¶jÓ¦MjhhP{{»š››µwï^M�:õ”é�;¦‹.ºHK—.ÕwÜ1"ËÄé?_gÀi99æq¦xà­ZµJ+W®Ôå—_®M›6©²²R?üð Ó/X°@÷ÝwŸþê¯þJé!~XàL—‰ÓG@ÔŒÐpOOÏ€Gÿà£ÙlV;wîTSSSñ¹D"¡¦¦&mß¾}X›PŠeâ€DMس<XWW§êêê⣭­mÐÕ9rD…BA555ž¯©©Qggç°6¡ËÄ;¸ jÿþýªªzç··‡ªÅøC@ÄŒÔ/�TUU (‡2eÊ%“Iuuu x¾««KµµµÃêC)–‰w0 @ÄŒöM ©TJóæÍSGGGñ9ß÷ÕÑÑ¡ÆÆÆamC)–‰wpp,Y1žñ4Ú½ò�/o™1¿óûåîí·Ž?VÜ…_î^@®Òšè¶BÆøj�rw.ŸuG…\0Ù£r~E¯³ýÒIîëc’ÆÎë32rÞ—q/Rò¸³ýwÙ)Îö # ¥'�q/¿w²³}RÊ£ÓÛ7tLL§û�c½o}w� F»o|,“Y#Ê8ì!ã§T0>˜Öò­˜+F&aÄÔ$‡þì¾;"Æ5¯$ùY#‚&⿯ÖÚÚª+VhþüùZ¸p¡ÚÛÛÕÛÛ«•+WJ’–/_®éÓ§¯#Ìf³zùå—‹ÿàÀíÞ½['NÔ%—\rZËÄðQ5ﺑcØóŸ¡eË–éðáÃZ·n�:;;5wî\mݺµxǾ}û”xW�~ðàA]uÕUÅoذA6lÐâÅ‹µmÛ¶ÓZ&†�Ãí“ IDAT€ˆ©kÏÔêÕ«µzõêAÛNu'Õ×ןÖYQ×21|\3œ jÂü ïÉùi€DÌpÎíÝó#ÚˆΖ�gÅXí#îÀdDX13A¹{þ m,߸ø×O¹×ŸÏý3VoÅqä+‡îŸ_éŽ{ÈT¹cFfLþƒ³}rÚ£2©¼ÏÝžt·'Œ˜Tìs¶'=÷k—ñÜ1.ÿ_寜í;õÎößgÏq¶W$sÎö×{Ïs¶WU ½ÿÞ8×}Ø,”¹#t$÷3y܈/šän/7æOñS‰¼#“ro�2¢TŽ»?;æq-i´»W¯À3cE[åÂÅÄx�oöoD1Œ  @"†!`X(ˆn��kb†3€DŽ÷ÇG˜ùe€D CÀ00 3œ j8` ¾û䕹¯±°2¥¬œ>³=¤À蟟6Þ^!s+,m´WÝ^Þ½r+B¡»¯ÂÙ>s¢;'ðœrwNà…©7�íåž;ŒlzÙ[ÎöŒ1_îÐr~Y�±~wÎß/Ž]èl÷�×Çw\ÿ”Hº3 eF¾eÚÈÙKYtÆûÞ7Ž+2>å½îöÀ:®X9~a�[F~¨™˜~ece»ZÇüQx'aæG¤1 3œ b‚À>YjÍ�h£ j¸†€b†3€D 7�À@@ÄxÁ‰G˜ùm€%dƸ˜qîùCÇÄXŒ8ÏwÇa$úÝQ!¹*wE¾2Ü f‰±ûýÔÐeîm¯Hg�íuUBÈoßýÎösÇœí• wÌJÒØy5I÷ú»Ý»O‡Œ˜—·åŽÑ™‘vÇàtöU9ÛËCw°Ìxís¾ûµK7"„¬Ã†»Y¹ ÷ú+޹ûï14Öç>(7Ž[寳ÏýÞQy¹»=ë~ï8�‹ã�i°bb¼Ñ<«Æ5€0p @Ìp€¨á@(ˆ†€a` f8@Ôp @¢††€K)0"|÷',Ⱥ£Dkù#²@…B¸åGX"ãÞ7�qõÇ+�í³*Þp¶×§;Ûk“ÝFûQgû�×~(åeî}“¬tÇ�*Œ#&a¼4V~›Ÿr¿7þòWÀ1ãgG€—LÊó†8j»Š4/á.“ž³ô2igèÉøK"…*ƒ2wŽ_`äúåÆº�¿C…´•Wæž¿¬ÏÝž=×±l•¸¬ßEàÔä$g»/÷º­œÂT¹;cò˜Ÿr¶¿b—³½¯0tÖÜkýSœE`VeÎ"08žv�ÉþÄø.­|SKÒ8/Ñïþblqå§!sO,ÃqÌ F±@ä.`(ˆ~ –ØoܸQõõõÊd2jhhÐŽ;ƺK%ëpË–-jmmÕúõëµk×.Í™3GÍÍÍ:tèÐXw €á Fà�H‹uøÀhÕªUZ¹r¥.¿ürmÚ´I•••zøá‡Çºk%Û0›ÍjçÎ�jjj*>—H$ÔÔÔ¤íÛ·:O¿zzz<8Ûxzç:Àa=ÆzPr±-�9¢B¡ šššÏ×ÔÔ¨³³sÐyÚÚÚT]]]|ÔÕÕ�FW£!l´0ŽpðX»v­Z[[‹ÿîééq��‘ó—ȸwà‡Ë”#î@’乿ãiw”†¬c¥ñ2‘w÷¿ì¸{¹J#¦àn/¸SlTÖ;ô÷£\…{ßzÆ-t5•î?ä÷ï·ü gû¡‚;¦eQæMg{Òè_gÁ½}㣺²^gûïòá¾»vöW9ÛóþÐË·rËÒîö¼o|îŒMó‡N¨9ḻ9‘uï{Ï8®ø)÷{?iEÈXQ+Öq-oD©X12Öq5LèYïËQ<ç2F107nÔ}÷ݧÎÎNÍ™3GßøÆ7´páÂ!§üñÇuï½÷êõ×_×ìÙ³õµ¯}MÿøÇ‹íG�Õš5kôä“Oê�7ÞЬY³ô¹Ï}Nó73¬þá±=8eÊ%“Iuu ÌëêêRmmí ó¤ÓiUUU xpÖƒ›@ÎôÆÊ^xA7Þx£n¾ùf½ôÒKjiiQKK‹öìÙSœ¦µµU[·nÕÿú_ÿK¯¼òŠn¿ýv­^½ZO=õÔ™wĶL¥Rš7ož:::ŠÏù¾¯ŽŽ566ŽaÏÎôÆÊ¯ýëºîºët×]wé²Ë.Ó—¾ô%]}õÕzðÁ‹Ó¼ð Z±b…–,Y¢úúzÝrË-š3g‘m# ¶ tâ›Å·¾õ-}ç;ßÑ+¯¼¢Ï|æ3êííÕÊ•+Ǻk ß�|ï��ýýýƒ®n87Vnß¾}Àô’ÔÜÜ<`úE‹é©§žÒ��~üãëW¿ú•>úÑ�žáÁ{ÅúÀeË–éðáÃZ·n�:;;5wî\mݺõ”COFê—@Þ{�ûúõëõ…/|á”é]7V¾ú꫃®£³³Ó¼óßø†n¹å]xá…*++S"‘з¾õ-}øÃÆVáÝb]JÒêÕ«µzõê±îg�ýû÷¸Þ=�6îžaßøÆ7ôÓŸþTO=õ”fΜ©çŸ^·Þz«¦M›vÊÙCœ™Ø€DNØ_óøã¼§{Ããpn¬¬­­uNüøqÝ}÷Ýzâ‰'týõ×K’>ð�h÷îÝÚ°a`H€# ðƒœk÷F܃Gà%B^¢Y0âRî˜ÏŒ¡q·I#FÀJ‹HÛ2©4‘s·ç' ÝÁDʽo“ ÷Æ:>ÁÙžINq¶OœÔçlÈy™›þÎöƒF„δ¤{ûÞ2¢N*�±©·wÔÈ�ü¹Îö\à>´•{î÷î±¼•µ2´þ·ÜgH’ƶ'û¬x#÷ú­‹_æ^@¢ßÝ/›w¯ 0>øV»q\U.Äò­è,+¢&0*£™38Bàéz÷�•---’Þ¹±r¨Q¶ÆÆFuttèöÛo/>÷ì³ÏoÄÌårÊårJ¼çoa2™”oý}‚‰„ÖÚÚª+VhþüùZ¸p¡ÚÛÛÜX¹|ùrMŸ>]mmm’¤Ûn»M‹/Öý÷߯믿^�=ö˜^|ñEmÞ¼YÒ‰³�‹/Ö]wÝ¥ŠŠ Íœ9SÏ=÷œþçÿüŸzà�Æl;£‚€ˆ©›@΄ucå¾}ûœÍ[´h‘}ôQÝsÏ=ºûî»5{öl=ù䓺âŠ+ŠÓ<öØcZ»v­>õ©OéÍ7ßÔÌ™3õå/™ è@@ÔŒÑ/�¸n¬Ü¶mÛ)Ï-]ºTK—.ryµµµúö·¿=¬¾À�€¨åk1þÄ: Ž8@ÄŒÅ5€_(�NļX13ãCÀ0PŽ%+ïÊ y ²r’rFfU™‘‰’_îÞ¾DÞ½9#/ÍÊ̸Û]ykÁq÷Gç­·*�íçMëu¶ûÆØý¾;§.)÷k ïv�š<êl-gl¿_ál?'qÜÙ^n„Ýe¼¬³ý˜ïθ|3ç~}ÞîúÍÑ—5›ÖÅóV»Õl|¬“Y÷?° 㸔ȅË }\ {\´¸²þF3Ç(1 @¢&ä0g£�€¨aîˆÎ5œ„�€ˆ!†€b†3€%ÜQ^ÒˆC°bb®ߊS°ÖoÅEô¹×Ÿ,wo!ãþ~XiF÷Ë{Üíý罂„µaD}t½=ÉÙž+¸7®¯àŽ�ÉU¹çŸ‘zÃÙþfa¢³}vªËÙÞcÄÀŒïžoäÝëïÊW;Û;ûÝíVÌÎñìÐû7gDà$úÜÛæéK £ÝHÈQ"ïn/?j|.ûCF�X³g­ ób·\Ç]ã˜ä8-†è  j¸ @"†kaá@€˜á QÄY<8P5\CÀ1ÃÀRòŒúÚ·â Œå[1/©Œ{~+.!ïΓðrîz)w„_îŽ{(;fÌ_æžßOºÛ­×EÐÉ·Ý#©C}wŒKÚ½üTÒýÚʺcfú|÷úg¥;Û_8v‰³=a\Aþ«üg{e"ëlÿÅÛ:Ûó�û³·ÿè9ÎvWLL!gÄAeÇÃ}ïN÷¸—Ÿìs·[÷{ÆqÉë3Ž ¾‘c·dÎ2¦ÆZ¿ƒ—0Ž)¾õÚŽÞ9n�…€¨a†€b†3€D CÀ°P5 ÃÀ0@Ìp€¨á €H'"b’VþÖøÀ5€°PŽ„À—4H6••„̳²XyY�;kMeîvÏ�‰ååÝëOö¹ó¸rÕdÖ}„Jä�ö¬»ÿå=®V#ƒ°ÜýG$Ÿu·÷çÝÍÿì>ÏÙ~áÄngûïƒj÷ú}÷úß̺sü*’î÷VÞa,óÜï�cù”³ýטâl÷}÷ëwìhzèÆ7ÝëNõ¸?÷eG�Í*ëw·Ü«W"çn7sþrc}\²òQ�ãjÖ½ü lNáxÁ@¸ f8@Ôp @"†kaa f8@Ô0  Ã0,€g± ëŽÒð*Ü1-�gà%Ý1+¦¼{ùA…ãr,ïl÷ÓF”J…·Ñç>‚RFŒ�có’Ç�³Êºº"—pgy¼e|Óî¬�dÂu‘/¸÷í}•Îöó2Çœí~àÞ·�Ç'9Û-ow¿÷û³îCÛñ·Œˆ£ÜÐýO2ž‘2b$ä(a´'�˜˜ò^wFLŠÅ “bżäÝÇë¸frÅÐÑ]溭h¯RGg€kˆš`ðqãFÕ××+“ɨ¡¡A;vìpNÿøã�ëÒK/U&“Ñ•W^©gžyæ”i^yåÝpà ª®®Ö„ ´`ÁíÛ·oxD Q3à–-[ÔÚÚªõë×k×®]š3gŽš››uèСA§á…tã�7êæ›oÖK/½¤––µ´´hÏž=Åi~ó›ßèšk®Ñ¥—^ªmÛ¶é?þã?tï½÷*“Éœy1 í�ЪU«´råJ]~ùåÚ´i“*++õðÃ:ý׿þu]wÝuºë®»tÙe—éK_ú’®¾új=øàƒÅiþîïþNÿøÇõÿðºêª«tñÅë†nÐÔ©SGk³"‹€ˆñFà!I===ýýƒ_„šÍfµsçN555ŸK$jjjÒöíÛ�gûöí¦—¤æææâô¾ïë?ø�Þ÷¾÷©¹¹YS§NUCCƒž|òÉ3ß!8 Q3BCÀuuuª®®.>ÚÚÚ]Ý‘#GT(TSS3àùššuvv:Ogg§súC‡éèÑ£úêW¿ªë®»Nÿöoÿ¦O|âúó?ÿs=÷Üsg¸Cð^Ü @ÄŒT ÌþýûUUUU|>�N‡ìÙéóýwMÿÙŸý™î¸ãIÒܹsõ /hÓ¦MZ¼xñ¨õ%Š(�aHöI®A¢%ðÍ(˜¸©ªªPeÊ”)J&“êêêð|WW—jkk�§¶¶Ö9ý”)STVV¦Ë/¿|À4—]v™~ò“ŸœÉf`€# ðƒ|ÕòÜQk6+“ªÏ楬œ?£=ëΚ³ò¼¼œ±|Ï�§–è7r Óîýcå¡ùåîõ»ÚsN�C ŒOVò˜»ï¹s�gÝo®ß9x™Œûµ ÷·ü®nwŽ_U…cçH:Úç^~ÁwïŸ|¿{ûýœñáË»_ûäÑ¡ç/Û˜×Ȉ,sï•÷º?Wasþ<ßøÜæ­,;ã´’q\²>÷fÖž+Ço$¸²úÆSŽß(ÿH*•Ò¼yóÔÑÑ¡––I'ÎàutthõêÕƒÎÓØØ¨ŽŽÝ~ûíÅçž}öY566—¹`ÁíÝ»wÀ|¿úÕ¯4sæÌ3ë NA@�ò¯y´¶¶jÅŠš?¾.\¨öövõööjåÊ•’¤åË—kúôéÅëo»í6-^¼X÷ß¿®¿þz=öØczñŵyóæâ2ïºë.-[¶Lþð‡uíµ×jëÖ­ú—ùmÛ¶mt7.‚(@hË–-ÓáǵnÝ:uvvjîܹںukñF�}ûö)‘xçìþ¢E‹ôè£�êž{îÑÝwß­Ù³gëÉ'ŸÔW\QœæŸø„6mÚ¤¶¶6}îsŸÓûßÿ~}÷»ßÕ5×\3êÛ5^Xçó1”žžUWWk‰÷ •y§wzI÷0”—p…„½Åö*Œ‹ØŒþ«Üýý!¨šànO¹çÏOtƒæªÂý”]ÿ9îíë¯r»ä'í•ÆÏÔUCMîa²d…ûç´ì!`÷ê­!Ú³~8ë^~òíÒ §»ÝíæðQ÷{£ì˜û½QvÜÝž8î~ï$Ž—ž5v€1„sÿÌ 9ìËw]Úb ñ†ýº|�Ó� ßSww÷i]W7'ÿ.]qËW”L ÿBåB¶O{6ß]Ò¾blq€¨åk1þÄöv§/ùËZ´h‘*++uÎ9çŒuwFMl Àl6«¥K—ê3ŸùÌXw€u20ÌÑÛ!à/~ñ‹’¤GydÌúXq VŒŒu ¡}!—»ÝºÐêÿ1#Âè_™‘4®›àî¿u-U¡|èå{�±ï ~™{~/ïî{Áˆ1ñ�º?ÚÇÎ5"†òÆwCã¯Ã[÷üÙ·Ý×wz ã½{ܽýîõ'Œ„£²£Cïßò^÷¼åGÝíÉ~÷¶%rÖ5€ÆõŸ}Æ5|}Æ5€}Yg»wÜÝ®œ{ýÊ;?dÌ‹y�žã¸ûÞ¼.ûlЉa†Ø€ÃÑßß?àw{zzư7ÃÛ!àáhkkð›ˆuuucÝ%NÁ0,‘*׬Y#Ïóœ�W_}uØË_»v­º»»‹�ýû÷�`ï!Á<i‘¾óÎ;uÓM79§¹è¢‹†½üt:=ª?„ À°p ‘*Ï?ÿ|�þùcÝ €³Z¤ À3±oß>½ùæ›Ú·oŸ …‚vïÞ-Iºä’K4q¢ñ3œÅÂ^ÇÇ5€ÑÛpݺuúÎw¾Sü÷UW]%Iúñ�¬%K–ŒN'ÌÈ€p1,2¢F,A¿ñ“Oãg†òî8/o¼ý²Æü¾;J¤ü¨{þÜD÷ú3úõÉN²â Üû¾ÌýKiÊ¿¼”4~ʬ`\©àçÜxI÷{+06?ßmü a¹{ùÉ£îx#FÇøhd¹Û]FÊIù1#Æ¥×ý¹Ow·{F|’W0bfú­cçŸk%Œ7‡uܲ⟲F �õSs!Î͹lcÛFõ—W†!R7�œ‰GyDAœòµâ`ŒÄö Qå�y¶ØšÑF@Ô0 Cl‡€âŠ3€D wÃB@Ô0 CÀ¨´1ÀÙ†3€#ÀKœø�á÷²&^ÒÈù³r=wýXy]/åÎÙ“u°,3¶¯Ï�3hå‰%{�<³d¸DWN`êm÷kã—}76ÝÊÌWºgO7–Ÿ0²ÖŒvßó'Ïzë†ü;ëYQtF»•ÃXÖ7ôé�d¿‘ahÄÔ%rFN_ÞÈüƒ»ó^ÎØ¹y#gð¸ñæ´rü¬œ>ã½%cûòîTϱþ à>n»æ=Û0  QÃ0 €D gaá@€˜á QÃ0 €Døpa f88?P0ÈW-+2ÀΜrǨxFÊŠÅ\¿#c"øFÖ†3ãeÝ1/I#Ÿ�v¶'úÜÛŸ,úû‘WîÞú²ãîö„‘`cŨ”÷ºÛóáb\ã½eŸ˜11ÆüÖö[1/Iã­g ㌗£îÎ'�ocùò�˜ãs%ßxqŒãF`-ßbô?¬’fýYÑ]VûH ‚p‘9!ãvpö£ b¸ †€b†3€D wÃ@@Äx¾}-­5?¢�!`€˜á QÃ0 €À0”Í+7‘�%^ÂSàG£òá.`Xø 6_Ò©L~iGØ­<+ÏÈ4/°ò¾’îx # ÎÊû2r•0Öß—u·'Ý9�e½Cç™e wž›‘é—)ŠFΟÅÊɳrúÂ.?0Þ[~Ò½ýÉ\¸¿>eÇ�¬¾×7•õù‘ýî…'ú�ÏmÎhÏùœV~[_¿»ÝøÜ9#çÏxïËÈ ¬ã‚u\² 8Ïñæ |g~ë¸*É„�kÀˆØ¸q£êëë•ÉdÔÐР;v8§üñÇu饗*“ÉèÊ+¯Ô3Ï<3ä´ó7#ÏóÔÞÞ>ÒÝŽ% @"æäp˜Ç™Ú²e‹Z[[µ~ýzíÚµKsæÌQss³:4èô/¼ð‚n¼ñFÝ|óÍz饗ÔÒÒ¢––íÙ³ç”iŸxâ ýô§?Õ´iÓμc QŒÀã =ðÀZµj•V®\©Ë/¿\›6mRee¥~øáA§ÿú׿®ë®»NwÝu—.»ì2}éK_ÒÕW_­|pÀtÐg?ûYýó?ÿ³ÊËC^¿‚" @0¨žžž�þþÁ¯!Íf³Ú¹s§šššŠÏ% 555iûöíƒÎ³}ûöÓKRssó€é}ß×§?ýiÝu×]ú“?ù“Ø"œD@ÄŒÔp]]�ª««‹�¶¶¶A×wäÈ ÕÔÔ x¾¦¦F���ƒÎÓÙÙiNÿµ¯}MeeeúÜç>bo`0Ü @ÔŒÐ]Àû÷ïWUUUñétÚ�ž0’vîÜ©¯ýëÚµk—<ϸ»gŒ°”#ÎÀGp:ó[«w§%È ŒTÒˆ‘)¸ûÜq^ʸ–ÃúÀ7/‘q¶'z�8 ‡Â¤ g{™+"F’_nDØdÜíVÌJÒ�€£BÊX€µë�ÙFŒ‹Ÿr¯ÀŽqq·›oí>÷{7™u´ëvÅ�˜ß½n¯ÏÈØ±¼mdYñJyãÀa±^+æÅø\‡Žb s\ yL�ªªª€C™2eВɤººº<ßÕÕ¥ÚÚÚAç©­­uNÿÿïÿÕ¡C‡4cÆŒb{¡PÐ�wÞ©ööv½þúëg¸5x7†€ˆ˜Ñ¾ 8•JiÞ¼yêèè(>çû¾:::ÔØØ8è<���¦—¤gŸ}¶8ý§?ýiýÇü‡vïÞ]|L›6MwÝu—~øÃžYq Î5cðSp­­­Z±b…æÏŸ¯… ª½½]½½½Z¹r¥$iùòåš>}zñ:ÂÛn»M‹/Öý÷߯믿^�=ö˜^|ñEmÞ¼Y’tÞyçé¼óΰŽòòrÕÖÖêýïˆ�ƒDFÀ²eËtøða­[·N���š;w®¶nÝZ¼Ñcß¾}J¼ëœ-Z¤G}T÷Üs�î¾ûnÍž=[O>ù¤®¸âŠ±Ú„X¡ bÆê·€W¯^­Õ«WÚ¶mÛ¶Sž[ºt©–.]zÚË线‘C@Ôø�yCŽ9?"�€¨ƒk1¾P–’ób°â¼Dis‘‚¬‘%bÄÄx©”{ù9w…õ!#æE½ÇÝËO»û§²¡·/iDÈ$ú�—r÷¾+;êž¿Páþè&ûÝQ¹‰îù­á+fÅšß/s/À+X10îå'�(k~Ïqäå­ãsc­»ß˜ßj·²ßŒ–ÀЉ)¸ß[�ÑöÌ’uÜ3×nåîöÆÄ`ü¢ b<…¼pÄz‚³ Q3B¿‚è" f8@ÄŒU Æ @¢†»€a` f8€NDo•:^k´xA /Ä�aæÅø@XJV&TÈL©ÀwÏï¹£æÂç Zy`FŽ —N»ç7–ïõ»³øTf¼½�¹s•*w¬;瞷½mž‘S§¤{ß'²îm³rSo¹÷�ŸrÏï ëú¡¤ñÇ%‘5Þ[ ÷{?‘³²èŒ,¿ìЯ�gôMIãs�5Þ;as쌜À’æäI’gPî×>lÿ¬ãZ©ç?kø23'Íùi Ä gˆ†€a¡ j¸  @¢†_��kb†3€D ¿K, À×_]_úÒ—ô£ýH���š6mšþú¯ÿZ÷w§T*5z cÆ´„\¾“4rfŒù­˜SÞˆRqĸH’gÅ𸔻?:Þq+¢ÆÚwÆkŸ7"‚Œ‹µ|ÏŠY1¢@‚²pÛçŠi‘$Œýcì/çX¾54Ö{ÌÝnDØ(äç"°>F޵}�kßHöq',cùÖq±ä18g †€aˆeøê«¯Ê÷}}ó›ßÔ%—\¢={öhÕªUêííÕ† ƺ{%Ëðºë®Óu×]Wü÷E]¤½{÷ꡇ¢Œ{žâf~D[, ÀÁtwwkòäÉcÝ Âc @I¯½öš¾ñ�o˜gÿúûûÕÿ®Ÿëéé)u×F\¤b`Ö¬Y#Ïóœ�W_}uÀ<Ðu×]§¥K—jÕªUÎå·µµ©ºººø¨««+åæ0<Á<i‘:xç�wꦛnrNsÑEÿÿàÁƒºöÚkµhÑ"mÞ¼Ù\þÚµkÕÚÚZüwOOE à¬ÃOÁÁ©ðüóÏ×ùçŸZÓ8p@×^{­æÍ›§oûÛJXÑ ’Òé´ÒétØn" ú³Rz#ƒpöÈå¤rwÄœí‰zã”TIDAT"Už®hÉ’%š9s¦6lؠÇÛjkkǰgïaä]FVœÅK„»Í+°¢à¬œ@릑%g¶[9„ro€—w,?‘8QºÚ]òƾ±ŠK#ëÍë3Öod¥Y¯]�6r�œ>ï¸ñÚ[YmÖkoíŸ�”Êå†n³ún´®÷•dæø…Î¹32òòE­þ›9Æò�ù§¥Î0MÜC, ÀgŸ}V¯½öš^{í5]xá…ÚÞô€ñ.�¦žåOaäEê&�ÓuÓM7)‚AŒw'¯ ó@´Å²ˆ³Xi�B^8b=ÁYŠ€¨á&ˆÎŽ%WÁé°" Œå›q 3F&°bZ¬¨cñFÔ‡·áQ-aöŽ2ÆÅ3"IeÆG×úd[I!Y÷ú½þ�=Ö{ßÚþ¤5¿±�(çÙ��gF̘–°1.Öú­×ÖŒo •rþ°Ç­HE½¸ø’Œ�¡9?"�€ˆá—@`aŒˆ�7ª¾¾^™LF Ú±c‡súÇ\—^z©2™Œ®¼òJ=óÌ3Ŷ\.§ÏþóºòÊ+5aÂM›6MË—/×ÁƒK½±@@Ôœ¼ $Ìã mÙ²E­­­Z¿~½víÚ¥9s樹¹Y‡tú^xA7Þx£n¾ùf½ôÒKjiiQKK‹öìÙ#I:vì˜víÚ¥{ï½W»víÒ÷¾÷=íÝ»W7ÜpC¨]ƒ¼€ôãaëééQuuµ–èÏTæ ã·AÇøÀ°¬Ÿd2¯%²úg,ßd\‡f]h^gçZ¶u  u Zؾ•ûÞbý˜õÚp àг[×ø×è™Ë·~*nŒ¯´¶?ô5~fÆîâ¶|�Ó6}_ÝÝݪªª*É:Nþ]úÈ嫲äð»>_èWÇËΨ¯ Z°`�|ðAI’ïûª««Óg?ûY­Y³æ”é—-[¦ÞÞ^=ýôÓÅç>øÁjîܹڴiÓ ëøùÏ®… êw¿û�f̘1Œ-ÃIœ jFè `OOÏ€Gÿ «Ëf³Ú¹s§šššŠÏ% 555iûöíƒÎ³}ûöÓKRssó�ÓKRww·<ÏÓ9çœs¦{ïAUWW§êêê⣭­mÐéŽ9¢B¡ šššÏ×ÔÔ¨³³sÐy:;;Ïhú¾¾>}þóŸ×�7ÞX²3¨qÂ]Àc©ÔC!‡ˆ­!Þ°13fŒLØ4 c(Ë*ó\Q-Ö²û³ÎvsÕŠ°É;'ômÈáw‹5›2†Ð�÷…\¿û½iÓºXóZCÈÆ¾·â�cøÛú\ùpCÐsˆ7.1-¥6B10û÷ïPl¥ÓÃV#—Ëé/ÿò/�zè¡1éCÔP1#SUUuZgÛ¦L™¢d2©®®®Ïwuu©¶¶vÐyjkkOkú“Åßï~÷;ýèG?âìßa„’J¥4oÞ;`ú“Å߯ýkýû¿ÿ»Î;ï¼Òl@ q€¨ƒßnmmÕŠ+4þ|-\¸PíííêííÕÊ•+%IË—/×ôéÓ‹×ÞvÛmZ¼x±î¿ÿ~]ýõzì±Çôâ‹/jóæÍ’Nñ¡]»véé§ŸV¡P(^8yòd¥¬KEàD@Ôø�ä…(‡dzlÙ2>|XëÖ­Sgg§æÎ�«­[·oôØ·oŸZ´h‘}ôQÝsÏ=ºûî»5{öl=ù䓺âŠ+$IÐSO=%Iš;wî€uýøÇ?Ö’%K†¹q�È %tàX+ñM öêK|£�™C"'0ä²Ãfza3ÇûM Yã&sýÜ2äü¥þ-âß2š9€Mß:ðßÓ^Ò¾blq€¨ƒ!`Œ/€DNÈP€QG8žYÀÖPŠÑX#AÆõ%ö¯•Óîä…  \üÖf©YCÐÖOÉ…^¿±|ë§ä¬ù�?³þ¼—õÞ1†Q�¬!Ú°CÀ!*Íâ-ñO­ÅyˆO(ˆ†€a  jü@¡†qK|¦c�€¨ üpÃí ÕG¿3œ j¸ @¢†ka ÏJ}�FÈ_ú°â ßøÅ+êÄŠ±ñÃýÒ‰ë̾…eŤ„Žqï;篠Hf Ž…âùF�5¿Ñ¿°ÛWÒ_Ã(õ/m„Œ‘€D CÀ0P5�B€#Öœ¥¸  f8@Ô0  Qãû’BÜ(8Ö¿wŽ’c f8 Gà;cr‚BÁcÌ�³X¡ ¹^[« Ã@ˆ¡…Í ›#hfÁ¹süÌœÀPqlÉÓXþÐ+ðž{~£oAÁxm¬×.d�äÝŽòŒŒÅ°ó;2OGèýgeù9ÚÍ>kÝgû~Cöì@ QÃ/�Àp–•ÀHã ¾‚ÃñaæÅø@@ÔA¸a\®Œ<†€b†3€DMò&ÎF Jg¼ÇY9a¾µíá–ïÌ ÔiĨXÛg¾vî?fÄ�Õÿ�AfK)…½¶ÊŠ »m\û ¾/y!^KÞ‘w–ÿÀHã QÃ0 €DLàû B } Ä gˆ†€a  jü@ò(14 @Œ��טVRŠUbF…8¢F¬yM%¾¾&È»—oƬXQ"AÎÝnÅÄXQ&Fÿ­å[11Öö¹^ß1�� \ÛA )L Ì8ŸÃÄ5€1Ã@"&ð!†€ÎF^lÏÞpà š1c†2™Œ.¸à}úÓŸÖÁƒǺ[„øáðqãFÕ××+“ɨ¡¡A;vìpNÿøã�ëÒK/U&“Ñ•W^©gžyfàf�Ö­[§ .¸@jjjÒ¯ýëaõ Ŷ¼öÚkõ¿ÿ÷ÿÖÞ½{õÝï~W¿ùÍoôñcÝ-Æ¥-[¶¨µµUëׯ׮]»4gÎ577ëСCƒNÿ /èÆoÔÍ7߬—^zI---jiiÑž={ŠÓüÃ?üƒþñÿQ›6mÒÏ~ö3M˜0AÍÍÍêëë­ÍŠ,/à<¯$é©§žRKK‹úûûU^^~Zóôôô¨ººZKôg*óNoŒ¢°7�8.ö}È HÈßy{#BØ›L¸ c ä´MßWww·ªªªJ²Žâß%ï¡þ.僜¶OœQ_´`Á=øàƒ’$ß÷UWW§Ï~ö³Z³fÍ)Ó/[¶L½½½zúé§‹Ï}ðƒÔܹsµiÓ&A iÓ¦éÎ;ïÔßþíßJ’º»»USS£GyDõW5ìí×J’Þ|óMýó?ÿ³-Zä,þúûûÕßß_üwww·$)¯\¨¸%”ŠQFèøndÍ{¶ó¬Éú^h%.C.ßÚ>×ë;î¿3SŽ™¼NÜ=?ï¡|Ðêµ>ÙמžžϧÓi¥ÓéS¦Ïf³Ú¹s§Ö®][|.‘H¨©©IÛ·otÛ·oWkkë€çš››õä“OJ’~ûÛߪ³³SMMMÅöêêj544hûöí€!źüüç?¯|PÇŽÓ?øÁßBÓÖÖ¦/~ñ‹§<ÿ=3ÈÔsÖ1ÖŠ‘)Õ¼gƒñÞÿRcÿ „Þxã UWW—dÙ©TJµµµúIgø¿K'NT]]Ý€çÖ¯_¯/|á §L{äÈ ÕÔÔ x¾¦¦F¯¾úê ËïììtúÎÎÎbûÉ熚épÍš5úÚ×¾æœæ•W^Ñ¥—^*Iºë®»tóÍ7ëw¿û�¾øÅ/jùòåzúé§åyƒû_»ví€o+o½õ–fΜ©}ûö•ìÃ<ôôô¨®®Nû÷ï/Ù°ÆxÀ~8�ýðöŠ쇺»»5cÆ Mž<¹dëÈd2úío«l6zYAœò÷p°³Ÿ"UÞyç�ºé¦›œÓ\tÑEÅÿŸ2eЦL™¢÷½ï}ºì²ËTWW§Ÿþô§jlltÞ¡N}WWWÇú vRUUûA쇓Øï`_œÀ~8!‘(íý—™LF™L¦¤ëx¯)S¦(™Lª««kÀó]]]ª­­tžÚÚZçô'ÿÛÕÕ¥ .¸`À4sçÎÉîÇR¤î>ÿüóu饗:©TjÐy}ÿĵï¾ÆØR©”æÍ›§ŽŽŽâs¾ï«££cÈ“*���¦—¤gŸ}¶8ý¬Y³T[[;`šžžýìg?r™8}‘:xº~ö³Ÿéç?ÿ¹®¹æ�{î¹úÍo~£{ï½W_|1o*†¡µµU+V¬ÐüùóµpáBµ··«··W+W®”$-_¾\Ó§OW[[›$é¶ÛnÓâÅ‹uÿý÷ëúë¯×c�=¦_|Q›7o–$yž§Ûo¿]ÿ÷¯Ù³gkÖ¬Yº÷Þ{5mÚ4µ´´ŒÙvFE, ÀÊÊJ}ï{ßÓúõëÕÛÛ« .¸@×]w�î¹çž3º¾!�Nkýúõ±¿&‚ýpûáöÃ;Ø'°Nˆú~X¶l™>¬uëÖ©³³SsçÎÕÖ­[‹7qìÛ·oÀð÷¢E‹ôè£�êž{îÑÝwß­Ù³gëÉ'ŸÔW\Qœæ¿ý·ÿ¦ÞÞ^ÝrË-zë­·tÍ5×hëÖ­£>ÄEäÄL¤®€� f(b† f(GÈ 7Ü 3f(“Éè‚ .Ч?ýi>®˜Ž•S###Êd2joo×ðð°.\¸ +W®èÔ©S¦£å\*•RMM�êêêLGÉ©k×®©©©IgΜÑãÇ�UVV¦={öèíÛ·¦£åT2™TYY™._¾l:Š1ýýý ƒÐÝ»wõùógíÞ½[ÉdÒt´œZ¹r¥Î�;§¡¡!=zôH;wîÔþýû5<ŸÊËË …~›ò'ý¿óÁ.—KåååêééÉ–�L&£žžÕ××N‡\sG ‡Ãêëë£üýM&“ù-~ðk£Î€ÁÁA=|øPÛ·o×Ò¥K5>>®Ó§O«´´ô—¿û÷#âñ¸ªªªTRR¢óçÏ+‘Hdß[±b…Ád¹‹Å499©X,¦t:­h4*IZ»v­ §›=MMM Ú¸q£*++uñâE%“I:tÈt´œúôé“ÆÆÆ²¯_½z¥h4*·Û-¯×k0YîƒAuvv*‰¨°°0;tÉ’%Z¸p¡át¹sòäIíÛ·O^¯W?~Tgg§úúútçÎÓÑ`;ÿÛÓ§OŸÏç¸Ýn'??ßYµj•S[[ëLLL˜Ž–S¡PÈ‘ôÍÍ6�@à›ãÐÛÛk:Ú¬kmmu¼^¯ãr¹œÊÊJg``Àt¤œëííýæ÷LGË™»„B!ÓÑrêðáÃNII‰ãr¹�ÇãìÚµËéîî6 p˜`™ßg‚þ €e(€–¡X†` €e(€–¡X†` €e(€–¡X†` €e(€–¡X†` €e(€–¡X†` €e(€–¡X†` €e(€–¡X†` €e(€–ùCéRᆲe6IEND®B`‚scikit-hep-boost-histogram-b4c49b0/docs/_images/histogram_design.png000066400000000000000000000422071524660700500257040ustar00rootroot00000000000000‰PNG  IHDR+^¾0KgAMA±� üabKGDÿÿ«1Í pHYs––qFþðtIMEã 5„Ú3ØC‡IDATxÚí½Á‹+[»Ÿ·úpÈj÷ lî7Ò>&|³>§zr�8wâ&œj�;Šá$¥ÁÙƒ˜4GßÙ�±„<ðÆAòà´d’9�ªÉ�„ĨîžØ`¶0†{zÔ]!Ü î¨3ø�õ­’T’JRI]ê~{·TZµj•ÖZ¿z×û¾ëìùÙÔ–¯h@¬ V±€X@¬b Þœ?ôzç¾OKTÑ–žwþ0�?xb ’Éu00ƘÇ÷iJkìÏãû,3&IŒ‰ãªË×4/¼]¡ÒíÒU –áðüÁcâØ˜Ë˪Ê=#ƒ-¼-¡EÆDÑãûê&S•ëûÆ„¡þšÍŒ�ßgÙùC>¾�ߦl·+[M¢�†1“‰1Íf•Ë?罞1žgL·+�âûÆD‘DK<¾o6�­¾oL¯'Ñ4÷Üž§vîv«jø¬Àb00f8¬V¨„¡1¾ÿø¾Ý–߆ü`dU‚âïDQuç_UV¸×qQ;t»Æ Uø¯ VàM IÝ÷�é÷«-9 �Y%~–}b4y7Õ\Óê²ß÷ûÆ4›ÆT³³½`�Õ.�b �ެ*²~T‡çéU4a§©–‚ÅM•Bi�`H’ê¯wú}c:-¿!VV"«ŠçUoU1Ƙ,3&ŠVçkq>š´7[Î�ó‡ X—¦|Y«—aÎ|_ç©>7ŠKkûYW+ðètltNõe[çÕÉDIÑ:�¼ÈЄmÌùCÈáÕóŒ Ãó‡8ÖkÞçDκÖÏ$t̼hÙT–DHŸ?�5e6ÛF´Õ@¬XeWÜh­©çµ¦A å¤Õä—mÜw–B´Åç)[ãÖ´ÑXU~Q™ªÏv ¶ì9¶¥¨‹mÜš"à5âûòW9ä$íû«òˆ¸˜uKù1z}.…¯›×Ï;.ï˪úÒ§4Mwͦ{t±¢‰3 �I-S,~ÞéÈùG�.‡ÜùÉ»5íõZÓÑȘFCŸÎqGžÏιt>l«5�¢Öt2Ñû圞ZÓù02•›Jóeêø(2æË—²ÎÄ›Îá®{0ÐgOOvM×úôä˜}Õõ»6Ž"+uîÉ„A  Þ(µ×ÓË…‚v‚y båž«ÒÊ[ñ‘+Y6/6 ©üïyóÇo[–1Zš ÃåpèNç°–9ï’ îëÃÞ<=½Ï7¸ïÓíÞ],;;ÙIôîÂ…gµ¦YfL·¦>Ü]d™&è(2F3·¦“‰BŽ“¤5Ux–|cšÍÖTâÅ¢ó‡šÄˈˆNGõ¾¾–#n’Hp�FòüΗicÖ»]Ýð^¯ÌRW™s諾——vì×y1eÛa{W‡¯]±ù ÔZ â¸5½¼œOg=¶³lù)߯«™¼¶1}m�Ƈ„æ0²íUéöç0FJ}2ÑÎb;oR¶Y¦%ª$ÑßÅKrP6m”7ïTYåFy§��» ”w‚ÕÿË�o�c[ Ç®‘Cæž)ªãùÃnß}ËJÞœUô£ZÕ­â�ñ}k�ø ‚Ãì÷`­>�Ž–xF¶Û9$ôÚmY£Ê×G©fSíe­Yq¬¥%¨'/¹QÞ©·ÔWDÖÆ²²Me“Dçl¶ÞJ0›Ù}ZScôôÐíÞ]fÃ9ËZcªöïØýV¤9ŸžÍç’ÈË'ë“ÏL¯×š&É¡ÚöÁn”7? m”W&dÖŽ3iº.ZæüÁóä'£ÉñX�³Ñ0&Ë6=©Ÿ?ø¾Kñnó�ÌϽ¿»Å`yÞ€Sæ72t&ÊÖÔó$P²l•ârQ56’¨ÛÕëúzÛ v»�byr¯eB¬q µ�LŽËU­n÷ù§.· õÚ2ø¼ªÞ(OŸ9ËC§³˜ŸCÑF“‰6³Ÿ+Ê1}TfÓ={Üùƒ¢8u ËQœ:n>"TÇî:6ižÙ%c*ŠÝ–�^@¬ØM£áDC鉿ÛuÿÏO̲¸¸ï÷z­iÚI\¯Uë³E ³¯‰t]YU¬>‡m'n®¯µ4Vv_£"Ç;ÏÃÉ ž¸L¨Y¦ñKéΞžòûÎ<¾O’Ç÷××÷¬sg³™wœtî]_ËÉ2I¬Ó¦„-k8”fšêAƦcwþ0:w“$*§ß/zxÒ9£È˜v[çÔ˜eŒçéÚäü©ò¢HޤI"gÑáPÑ.»,é° T?V%ÖQé2�œCó»N*ׇ¢wÔî.f³Ö´ß—5!ŠZÓáP4³™Â³Ì˜Ñ¨5�]ØÖlæ¢|’Dç°ñàÎ\(‡Ñëëy …]&ét\§³ŸG‘êlÔl9q,«EšÚ02Õ«8ŒL!ÚöéB9NŒ�‹Â³‹Ù|Ž»‹ñXçC j/'¾ä" L’èûó×ä•“DeYg[ùɰÛ4ÀñÐäe2œZñpþ0«¿Ûh“FCa®vO™2ôzÅIdžCYM$`æ'ÏsË4ggóß³‘Œö�RKúùåë <NÅ©hµ‡¬Ì‹BMN™Šzä—óØ^Dž•Ë6R=²„X Çòˆ�ŠÉû¯ØH"cÚíʼnUå�Fv‰¨XH)]ùZ¬*g´Ë0å£nvi“cœÃóœ_Ì|›Ð�Ž%RlbFÏ3æÃ‡]—)ÜFyƨõc-ñ¤éb˜«Ž�c¥r_g´ä3*×¾W\Öòwó‘Œ6]¼]žÒò�,5öøÁ@bM¹¤´´4éáqñÁ©×“õf}–ëÇ:Ç¢À‚—ûýïvO^ÌÁV"aµJ.~ÊWÖբϜŦ8·Àâw¶‡Ç=‡%XR^R¤XkÆæ…ó‡0,²ÀÈêÐnk’÷ýÍ‘�eüë¶ yÖÒMÊškŒ-f·K9Úq×Z•æç€M¡ø�¼¶8þú´.p<ÖR‡³̳.@�DJ¿_Þ¢b}F–y|?oÊ]qþÐëÉ*Qf|,?‘8§Û¼U§H¹(NÕU>ŠóÌE„V—E‹qýØmì“+²ž(E°ÒËÛ n4ìþ$7€ã Ï+Õˆ'V¬U¢øÕÆÔ2Ðá}<Štë«aŒ›„!R,Õl”§¨"í-¦Mìl8ôl–w`U¸qÚsh[œ¼Êmº§:Gqªa®n£‘"Ÿ\DèniáË/éu»²-·‡SjÓÉäüá{é¼A಩âw¸—ÏϼxñâÅk›×»_‚àÝ/ÏÏîõùó»_žžÜ߃Á»_�êÎçûùÿ¿û¥Óy÷K¯—u=W×CeÁ»_t:Ú§i~ï¨ù2–÷œS™“‰QTTßbìf—;zÖŸyñâÅk³ÏȼOʪ×`@{­kGϳþ-Uúô¬?§õ/)çQÆge¾Üyÿ‹w¿|ùRìƒ3™,ÖáÝ/q,ÿ¦(šo£ççw¿„¡{¯ÓÑ÷]›éü_¾×m]¹ƒÁ¼Tµ�ʘo×6Eßßì_¤¶�v¹�ø¬üŠöÊ©ÂTh÷Zõtæv€SàÝ;c¾ùƘÿï]®ÝWKOî«òŒÀ¢•å%¢8m²9©thÆcY\h¸,nEû1ÉJ’ßÞ@áåvÉ$_ß2{0•)wÑÈÖ©Lþ“Á@a‹ç˲M¡ájµÃ.­ŠX™û!”IE½‰MëwUœŽKñ^:p É&›;–X%ŽÂÐMÌë6aܼ$¢M*õýù=˜ÖÍ9«ÊÝMXë¼�Æb*Q_™vµõÝí Vr‘õB£ß7æ»ïÎÎ~þùùùóçÕÇ•u:¦¹ênÎþÙ<®«{]°Ö§×ée_ndøñGóøÃ§sÏ^+UY\áe�«P´~0‹“÷l¦1QdÌphP÷µ¾•Ûƒé�ìQ²Bîæ€ŽXù•M›úµ¦OOÆóüüùsó)íüAu7�Ÿ?ŸÂÓ¡3U¾Ý'Ùß§)?‘{özïË;¯G¬DÑùCTÑŸÜFŒIR¼c7nô}‰Ýý«•߃é�ìÞÔ^a(£Àn ¯ ”ÙÌFîì�µhO¾î|QdÌæ}‰Ê�o•Ï‹„ˡŋDÙl¶*²J[¬Â~¶»hC¬À ß7& ·Ë ²l!pVŽfs}âºáÐn9p˜ëY܃éVÀv[m¸2m·QX…–ÄöY c^=Úw¨Ó‘•bÙ"â6ï³bFùFæ�ò<ùЏå˜Mb¥hÿ w.»?R«N¾ï¬�ï›Íò{0mWî|î–^Oû4�Ç‹e¨ÖYÙæÌ±~3vï¦âå5Y|Šö¯Úbÿ˽®?ÇñõçççëÏûîU¡| ÇÍmÇeãàë‘‹áømT¿6°9Nãž½ÞûpZ}‡×¦ûEÊ—Síž7«sÊl¿ή{0½Lî¡2¹U⸊vÀ²oȺ†«¬+û å¥FÃ- ié£Úú×ÇÙ¾ÌÞMjë ¬*,@•Ü^Ù( kv^tÆKS™ŽõïÍ=Ñ6plÚmc&“ó‡¢äfû`—˜ÎÎ$\<ï-ï¥6èõŒi·«HšˆX€=Š&e+ÚPNû]+\Æã›ûÃlf0o˜Íκ]ù]4›ÕeNù¹�úÁª$po›í¶šD|ˆØQ¤xž{š\´õZ 6v0Ï¿:�Û+eéÄÚ‡,ásFu)é÷+3IÎÒÔß?ìÆŒõGŽÌÆTÙˆØA¨�žœì2O¿/q²~M]â%MíSç핵ÈXÑ·Wí6V8¼`i·µò`�ßCg¿2_b •(’¥µÚ ääY€-…J¯§- �ËË›ûnw“P)/ãñÍ}·kÌå¥Êò}c&�àð‚Ř,[ŸÐ Ê •FC–Ò*—×–()R|_ÖësÒïKhì�,)——)²²Èzƒ•-XÞö’Mµm9›UùS–(!T¢È˜ÉDBÅYSª>�Êl6•õÓZY¢ˆ;ð¶A¬À¡ÒhÈÑùš4›‡´vh9éòÒESôzª¼UX€ JëmýK�"‚º]‰”0TªuØ€ÓË ¬äöJ:Ú¤¬šˆ‰í�¤¢„C¤±o\¨Ø<Æs}ýùOtN+’:–ƒ+9ýÛïï–\�`�ݳ¶N€X€7�Bˆ}_Q9uHÎrbà÷BŦїH¨CúûÅå îbÞ4v3ÂÍéó�+XÆc¥2÷<%ŒÄ ¼Ql†Ú:îqbó» V+ð†q–•úÕÍn7�X@¬À›D¡Á¾oL–ÕqOÕ)ËŒñ}ùÖbÞÖba-uÄZ|Ø)±oë¯Rç�ŽmÝH€X€7ˆ³YýÅ V+ˆxU€X€7Ì)$]³NÀ/¿ VàèœBÒµ:'­Ä ”º']«{Ò:@¬ÀQ¨sÒµSHZˆ80Öba}CêÄ)$­Ä kµÃ:-©.ÖÚƒ¿ bÞ,7÷³™Ä€ç3Ô§fƒ�ê”$ª# Và ÓnËÑ6 o¯^ÞwEuCÕ©Ýæþ Và�#ËE¿¯¿ƒ—\Ò¹­…§ßǪ€XøU°ôûõXÊ/ÿXˆcÌK/±üˆXËòrP£q<¡âû΢Ón³ü€XX!XòËA“ÉíU§sx¡Òé3™ãyæÝŸÿ9yU+¸¾VþÏ3¦×»½ŠãCXYn¯�Û«86¦×Ó;ýþ×ÿéßû{ÆÄñíU¯WÇ-±5àæ>Ën%Z²Li﫵²8kJ3›ÓlÞÜw»ÿÃ?úÿAKQ�Ž1_¾H´o9 +pR¢e<6æÃ‡*­,óÖÏSÙ——7÷.KíÍýp¨¥ Ïs¢e0¨C8<_Ó°�`É2c®¯%YB¾|¹½JSù¶è_W$NÃnÑ>ˆ€E~)áò°¤éÍ}·ëö z=‚EùdœÃò[æöÊón¯‚@/Ú±p‚¥ÝÎ;æjù'/Xì2ÑéNΊp"�Œ°á[Ä ÀÉ ˜¼`étn¯ì&ˆ§ˆ)ˆÝÛÙL´ý�¨Y*D¬ÔH°DÑé ß×RW£Á²GuÈbÅV §Ñ@¿Òšözù„VK¼ûö[ódŒ1ý~kúô´º¤n÷îb³ú?Ègâ<4ß~«ûýó‡uu¯Çm£ºñÍ7§xÏjÁÿlÌwßý›ó_Ýþö·ÿÉ8Šþû?ûã?þ_ÿ»÷ïöë;ÇœPídš$6K¯`°Xª+'OQfÍ¿Ÿ.¾ûn}9ew…]s®ƒ±©îuã%Ú¨nœÚ=«ñÆüòÞ˜þĘÿì¿ùÍoþëþ›ßüïÿ÷)M¨ã±2öf™þÞ,Vd�ñ厓€éõô€�$7÷ê{òïˆ"Ûï/ÛïË�Ä÷�%Hþ%nGîÙLï�Ç6Î6盿� öx½×ëéöš‚@[78Ñ"ñEú,Ët¬1ù:¹6Š"µ‘Ý]Ü[�Åúé[Y¦v]·¯`Y©ˆMK7véçùùó绋ý­c |H¿.#<~þ|Ìóî^ßã·QýÚà×'»¹gµåS’Ü^ýëmLÿÍï¿ûî¿5ú§ y.÷|ì%8=ýÛ‰Ô'‚ È" Ix4Ò.ÕÃô¤?iŸ$ývÊ×lZ1`ÑD>Þ^¹ÇZ}ßëyya¤Í,ŸžTöpèÎeŒÎ5ßÜ[ëG¹ó­o¯NGms}}s?›“$öº´;¸-W[;è|6Ü}±¬(’踼t¿�$Qù` 4[¿ÉD‚¥Û•Xéõð…9,8ØÀ«ÆNŒšT|ߘ8¶¦ûú¡% W÷ñXõ^åk1È:°ø°•·lsܶAÞ2‘�óç²ïÂYØ÷ç…B’ìæ˜Üë©�…ìp(gíü²t–I¤¥©"•ÎÎnî­¥°¬ì(Xn¯šMMÚV°”·°� ÐîÑù‰1IŒ ÃÛ«0Ì/3h2n4-.:fûãvc•Ð9N»J ؽ¨ìR¾m»ò‚TííyjëÅsÌf² …áüçX<±¬ÌÂb},êeaY^ZœÎËÖ½®V¤ªÚ­ÓѲŒõGÙED”qæg™±pÁ’eó‚e2©O“(’S©|HÜË:ĆἸ*k½x½Q*r®ítä‡Ô﯋:Zý}cÊ%›#Ú±ð"‚¥Ñ�…¥‚¥Ñ�“hã±,$ÎwEól¶ê‰_‘-å�[=¡×2³ÝùÝ´:¼Xû ­þ¾=W’ä£æË°¿ –}+/&X<屢"QÖ=¹¯Z j·eq™¯»¢dòå•=®ÈÂpÈDj‡8_¾�‹ï÷û·WV %ÉíÕh¤è˜nWoÉÁ4 ÝD>ßÜÛ w(¾/4›é¼Q´Î'äö* U—áPËc�Ž"¦ºÝõ×EúžïkO&¿\¾d³™¾“e¶ìÍ×Òé¨|íª­6ì÷‰B¬¼aÁÇ·WÍæ¦ÉÐZfn¯âxõûžgŒç¹I?M�yzº½JÓ›ûñXB"ŽN\nò.'TâØ˜ü5$‰ ‹ÇG‘„Íå¥[ÎJ’Û«^ïöj0¸¹o·W_ëphÌpx{õü¼ºFA ºØ²ÇãÛ«ÑÈ–½Y¨DÑÍý‡î½,Ó=úðáÐ!ØP ,T$XÜ’�çi2¬"Ä7òV7¹2|x00&I–ÅÖ*ñÕëÉš³8ñ‡ ñÞwé%Ë–Ëî÷Uöê((-+u:ÆŒÇó÷ÊFÕ=\+,ã±]šÙ¿ÔÕÉÐ�–ï—=¯„ˆç…»DnÕ;;!µNÙºe™ó¥±~+³™Kru‡e €ÊËõõíUíë7ò2l;�—±š2¬z]}×YŸºÝc A@¬ÔL´¼œP¹½êõv÷_ÙÖ‡£Œ“ê!ýBÖ•�$ÖIØ-ýÀ)Â2À«cÙÚp{Õh” ÖòÊ6K$Iâ²ù.žÓZ6Ö …]½˔-�b÷•ᖧʶ V`gëB~©eÞÚ <)_¾3™”+¯ÛU(ôâòͲ Ѳ—=~q²—s뼕©È³É§ÅóÊ•½êZ¢hñZ%¤e íÛŽ Ë@/„Ë=¢'ÿÛ«8¾¹o6‹ÞפëûnbW^•›ûfS‘1q¬ Øó–A–éåyúΦ�êñøöÊcƒÛ«ñX“z^¨ ·W³™Î­%/…·WÖš†6Ì|éùºÚü1V\E‘»¦|ÝÛmåmIS»‰d¾ìUíusŸ¦®n£‘®%Mu-³™k‡íÚŽÏÙºÈvp´¦qlcýï.v_û<P‹?¾?;;VÝÏ\Ýß×Ýö%Ú¨~mÐëéÉñ4îÙë½§Õwl”‹‹Â)ú<˶™Œ%<ïæ>IdÝð}åt)ö±Ç¯;¦è¸Muw�FcSÙe®¥ªö,+°›�Hƒ`[ÇÛüÄ-�°þe'úÅãÊ8ÀJÈìÁ³¹nÛ·|VÞ8�Ò>ˆ¨9Q$ }N–�Þ7÷LÄ´Ïé‚e+ˆ@¬ Vr ð qÙf�yÍYY]&\cvÍl õË À«Äó´UÀh¤}sæ'øÛ«8¾½Š¢Cˆ‡C•]Œï+GJ¯Ú]+PCnîíFE• p¯ªY]ö!ÌÍýpxs}ÍG¬À«2ý¾1ͦv3>NÙZ–j4h}Ø|VÞ¤`9Ü.ÒÅe‡!­»‚e ¬ÚH¯èýü{r0­îœeÊ.sÎùr c:�2ß¹½ ‚Û«rËS*ÞXV^L ø¾œ_ƒÀ˜$Ñò‰õíˆ"9ŒöûÆt»óÇöû·WiªÏ“äöj42Æú¨lβeß^YÁ1›Ý^õzÆŒÇ7÷Ãáú²ƒ@×âyÆ„¡:‹ßíttÎáÐÏÓyûýÅ(&}¿Ó1f6Sý¢¨Ø/+°7šˆ›ÍÛ«8ž84f8¼½z~.>ÖóŒñ<+N$.žžn¯Òôæ~<ÞöœåÊö ”,[ea¹½R~ù©¸e¡rmØhlÛŽ«¾Sôþ.åÃn`Yù•Ö´×[kŽ|÷í·æÉcúýÖôéiuIÝîÝÅfg¯ó‡yïÿÃòí·ú·ß?XW÷zqÜ6ªß|sŠ÷ìõaûΡ±¡Ây^[µ,›Ï`›·”t»Æôz·WÃáͽó™Q”OÞÚÒí3Ü^õûùãŠSû7Æ|ù"�㢌6 •¢ï½¿Kù°;g.0î­‹•8®fŸŒfóîbµ)óü�8=šÍÇ÷‡sìÔÓy+"Æ —,SôK–é=MèNÔØ÷|_Â&4qÎf7÷ùÜ)‹ÇSüþ6eÛ\*¾¯c“DyX–ÏysosÇØ°ã~_Ö‘ù0kYPlTPšÚsæ#“ÜqQ¤ãf37nÛÜ+¶Žž'1¡ûWf×i}g2QÝÜ~CEï¯:++ùmÆ‹è÷�ù³Ÿ~~þüyÝÒÝÅêµU'VÔ��ƒênÎþÙ<®«{]°•c¶QÝÌJ?þhøátîÙkå×¾s`±â&E;¥éͽ�Ž™Íæ­§Í¦k²m°i;€üqNùPkw¾,+#V Æå¼ûåùùÝ/ÏÏǬû»_âXçݯîÇ«ïñÛ¨~mÐë�Ò={½÷á´ú¯å×?þ»½ípú/làU‚óë뱯”(ÒRœ:DÀ«äæ¡òZÀ²ˆÄ V+ˆ@¬ V±€X@¬b±ˆÄ b+ˆ@¬ V+€X@¬b…&Ä b+ˆÄ V+€X@¬ V±€XÄ b±ˆÄ V+ˆ@¬ V±€X@¬b±€X€Óàkš@´¦QdL£±úˆo¾1ÆóîÇ[Ó X}Üpxw1›m:ßùC¯w¼«ûµîç?þxþ°®îõâ¸mT7~÷»S¼g¯�_û¼(gÏÏ4‚ÄJSŤÐlÞ]$Éê ˜8=šÍÇ÷«û5,+¿§Û5ÆóVÞïóÝwgg?ÿüüüùóêãÒ´ìàw¼kSÝÍùÏ?›Çuu¯ q|ü6ªç?þhøátîÙkå×¾ˆ•:pw±^d´¦OOÆóüüùó:ËIYŽù”vþ º›ÇÏŸOáéðüáømT¿6øÕÊw"÷ìõÞ‡_û¼(8Øb±ˆÄ b+ˆ@¬ V+€X@¬b±€XÄ b+ˆÄ V+€X¡ ±€XÄ b±ˆÑšú~k†45ÔR¬EÆŒF­i§Cs@ ÅJ£¡“„怊ß7Ƙ»‹4¥¹ †bÅóhf¨¥XiM­PÁª5+Ϊ‚uj(Vî.f3ýÏ:ÙÔH¬Eµ¦A@s@ ÅŠµ®°µ+Y¦ P3±b“Á!V –b%Me] üV vbåî"ËŒõû@ÍÄŠè÷�u…0f¨™XÁºµ+Š•(ºeùúX'º»˜ÍZÓáИ(2f42æò’æX�öX[M9›¹Láu®w–Ý]°G÷ew¾:îéº]%‰óýÖ´×ãg° Ï3&ô ÇÆ ú[ﵦqÜšŽF­iÖ³Þ£‘1¯g¼oM}_mE§Y—Ó¼/G+ò]¹¾Ö_�Ný:@½¸»˜Íî.º]·”®¿Ý«ÙTÃhÔšõ«÷k³¨X¡X‡TëëR$bNõ¾|uüpšÊÂbŒ1ƒAkJ²8€ýÇU-³×á‰ÿu·u¿oL³iL»]çºh¹çõø‡~õr <Ë5¨Q`wìÖ&0~K­Ô¹.¯kåâ«—;u»-3”ïÇ€}°chñ$Úšz^kº9“xÞÚí¾3o_5^o;Ž·¦�FQù«ÊU}sŽmY¬Çbû3êu¹.�ƶiBê¾ÊñõK�øî"ËZÓfScV°4›uQ«§‚&«04&Id¹^ü¼ÓÑ8;ãy­éhdL¿Ÿ�QÐC£aÌx¬ã³LijÇg9d�ö|k6õ½(’ó¯ïËwÆ.óoªs¯§òg3% íõŒévm�ËmM­ëtZÓvûîb<Þ÷î»dâûú|8ÔäÇnËÛ®Em`…A’¨}{=ÕýÝ»õõ‹"gñ<ç—4ÿžê3™81:«^Eu±ÎØúM82ß]ØòóuC]w’èwa}Z+kËhdŠ-(óÖ‘ ÈO²ÅB%Šî.>|pïe™ȇ‡­0Ðß,“‰B[“D’ijL³ÙšÆñü8>3¶¦ÏÏåÅU§£z__+ô:I$¸F#cTOWîdb¯OµUûŸCçÑÄ,çd‰5½Ÿ¦­išJÔÙ y�Yl‰� �£³%IÊì…'2óôdL»mïŸ{¯Ù´ç¾»¸¼T[äÅVÑýH�_Bk½ðh4Œñ<×ijÌÓSkš¦eÄà1ùê¥+ ŽÑlºtü„4lG£Qݡɹӱðü„fŒŠ}’OÓyËvšÚÏ“‹Ã÷çEG’Óh,/Gd™&Ô4U$ËÙY‘õh¿sswÑnë\Ѝ’€[*ÅâQe. ¨r“½‡ùˆ½§9qþè$©ú^ä®»ÿõ[úº•�²¿¾–…¥ÓiM“dó¤ZôÃ\§Ö¿ùÆcÌ»\¯˜‡Ã2IšÎŽ)Ê~­ûù�?ž?œÎÎ×Çm£ºñ»ß�â={�" nÈÚ1ÿïy²”\^Î�?A É4Ë–Ç­ÙlÞÏ¥èZ³,/±!Á!á`sØÉ‘íçƒíÏaŒRkL&zh^lçUØe¢/_ZÓ$ÑßÅËq«I›4J †0Qd—Õô^Õ¢±ž k+V¬ÒoMû}=ŒFÖl‘³BÝ]ôûnž²B#I$®ò>Fvy(M÷,§�䫺UHë­ ¯c  v9§ÑpÂÁîpßíív¯§û½ÙL–€0´¹Bô*òc)z°«"YÞê!?›êÛiõ9l{8qs}]~»¢¬Áž·�À�h”S­}xvïEÑöÔ‹~H§�ä¬|°Ùñ�â�c5î±}Wê4ú¾–Åí“z–I0̇¥jRít$N†Cc:’†î‰=M5iÏf.TÖóŒQ¨²�¾qKJI¢ð_»”"g^W–­Ó``ëvwÑlºzÛr´ÕŠB‚£HŸõû®>ÖïÆó\nëKh¯¹8WHq»m>‡\¬¯H£¡÷Ö¬ïÛ½–ä4ë|Kò×Ôïëû¶Ž.ŠgÛeçØë¬4V,-–UÔ¾ùði—'¦ß×õ*yÕ}Ñß¶½ÕþóáØˆ•7-ŽíÚá[v*(?n6ÎÊ1.>èÙ¨˜ù¥+>Úíŧw•7i‰¨h²´“ �6šÍÊDCÎ?˜î»Üó²çÐr›róm²KYªkÞâ³üÞö׿Ý}Á²²£uåîb}@Øu¬U˪§hYe¢¨NOÙðöøª®“ NSekdgf€Ã °ØÕN²‡Èï°_×»zv=´8;#ìû`8›))§�:±c­�rIÓíœTÏWõ®ÞñSÿ~ £ècÇÃܳ5�ãŽMkÚëé¼§a¡z‰6ª_DÑ)ݳ×{Ûw4Ö¼|¦æ»‹$¹»¸¾vé#?•v¡uà,+Vá‹FÃ¥¨>/‘:Ý&Ú;•8{ÒËþwõè;õú­Ë‰“\PP?jnYq¹¸Uˆ•ÚáÂè0… Vj Iá+5Ä9µ ðV©¹ƒ-Î…ð¶qâ£$™<¼íÞ–QäÒê—KÛˆ•Øêy.”Õ¥3vûGèÿtbx}xž¢†¢Hc™dw|q¬Ðì$1&Ž[Ó,ÓF�€XÙ»ÇE‘ƒ­ï/¾¯­Ð‡ÃuÛ‚—§ßÿ>=Uw-Ýî§±SGÍ#òîÛoÍ“®©5­òš=À¼å\+ß|c‡§tÏ^¿ï;âcxüߤötévë’s§5�¢Ó³JhW介áPûÙ é€XÙ»CØì‰ã±~X³YÞªÏó¹(¬xét´ú¾¢å»ïª:ÊsÄü O‡º¦CC¾•Ó»g¯Œ'~“ËŠSL%awn¶âëb¥2²Ì˜wïV‰ ýàf³|#m~¨M¹ôo§#ÑÒín/ZšÍOãõÉ‘>†£‘ÛÜn—ž¦y Êê§¥³³ãZ(‚À˜fó�»›VW_m¯yÌ6ª_ôzú ŸÆ={½÷Áöñi¼þ7ù1ìtŒñ¼Oãr;Å 뺕ì*¬µà¸Ôx#Ã,ÛV`(et³)ÕlÍ”QdÌ—/v›ðji·íf‹:Ï``Ìdò1|~¶i´õÂBð6°o�-ª‚@¯ÍA ùå¤ÖÔóô½ÍKL²fw:›�STör}ZÓF£ü9Ý÷×ï®Cm¯ºV߆›¯ms}ÙB㕉•ý„Žö´0æÃY^ä\¥§Õêø4Î2c®¯‹óÀ8ëyb^?CÏ“åÁ÷?†ÇY*iMÃPÖ;Iv:«Æ9ís4Óh´¦£Qk:X_9Ÿ®ŸÈ�éõt�S/}_{YM&²ŠÚ:ØEg�ÑûVÌAѾK­©ïë}[Vê{ž§zÏ__kª¥;ÎêóÉ$_–u(ÚwlSnº¶²õmM;ÕµÑÈ·”ãë×|qvmÒ™Ô;uŒëëýp­`™Í>†í¶1£Qñý~™e!8uòK$ahÌa7Ôd×ésy鯳$‘( ôÀ–?6ŠŒùðAÇŽÇ­éd"?À$iM×ûph)2I4©§éÝÅü2—n‡C•éûÆt»½žõqÑDµ„/KÄh¤K[–"žœ€ð<{>í ýôÔš¦éÝÅx¬ñ<dQ·$‰µ°Ì—eÌüqåÚpÓµ)¢hS}=ϘáÐù\cÌhÔšŽÇUÍEXV^…hévµ4”eê,q\Æ\Z^°ŒÇó»•Zþú¯Ù à­�_þ9†oG¯gLÑd7*È`±>i:lšº(™ª¨²L“ušÞ]Ìfwggó»6ûþ¼ƒn’ÈÒ°ji$ò‘G®þ.­…¾¿hÉ*’¼Mnº¶uõ]lãÅëÄÊÜ“Á‡ê ¾_µ`‘Ú¶/ZþàŒ±¾+$¸x­hÙ'/P|ÿcx¸‰H“¨ç= Éj±(˜4Ñ‹‹ª)~@»»è÷5Á'‰–N‚À ¼Uãã☺Hšê¾|ÑÒJ¯×šÁ²ˆ¨¢ W_Ûæúb=9ªXÙÆ1ª|™óŽQeŽÙÅyÊ©Üfs^°Ts-ò_É›Fû}gîtäx‹£-Àë¤Èÿà�> eÆ’ü89ÊgÃ.ÉØDs‡]ªZË;-§ØPâý,ÏnLï÷%<:�ëƒAõm'!V69FÙ5>½ÿôä¯|_DZœ�òßétA#e½ø¹;¦Øyª:ÁR�ÉVËAJ6ôiÜí*|ñòRçj4Œ‰ã¿ùÿsö[~x¯Çªb£ ÃÃYTË,ÛäŸæg3MèQ¤1t0PJ‡ý’»m´0ŽßïW±ô$ñ%‘»‹ËK¥;èvu�›ÆõmÛ^’¯Ëý 6;F9‡¢Á@³Ö ï.ÒTNFý~>_„!wαJhã¸5•Ø&ç©}‹Ê™ùF£Ö´ß¿»°£\Ž„bæ¿+çÚËË�¡:êßø§Æü�:›}4A°)�KU,:•Õ�·œ_ŵÁ¾¿C8^ß Ãâe û¤¿ú>nÊÛ²š$q>xóãˆ{àË¿†Æ$Iõ9{¶c�κD�Î)v›:Úä¡ÎBtwÑï»ý”Öù®lÛ†p–•²ŽQò@Ï2k1‘…eQ¨ØN<ÿCrÇØ§”}�§Ö –»‹ëk7�È<¹ëS^œEÿYYðex V•uÖ…(:Dׄßí*Œx±|�­óV“Ù̘^Oaµ6ŸHl?ÞeÙü<°¯å!ÿàéy»•WdÕò¼MóÄöm/ÉÙ¶ù%Nìzgq†Mu€ÉDë¤Y¶èì$óÜh¤ÊâÄÞëé  Ûu›Où¾ÛË¡ú§]“òè…ªjcDkeqƒH»},+ R¼hDÝÝb²j|êõ\äHšj 'M5–Êj¢‰> µ©ë¼5Gc©B•�³mÞzP.�ƒêÇÎGĆG‘Îm}QÒtq²×1½ž¾›¦:v<–Øð<[ïU׫¿í9ló~ße·Î¶ÖB³ª¬ùÐëMm¸éÚ¶©ïÝ…M¥úÜÎiXQ++Z¶ C·W�ÅéÀ� ¹¼\œôÝÚe‘XÑn½±�ÆÞ\‰‰|�jû„”WéI²hªnPó}YŸì5É!WŽº€XÙå!r1<9¿ãp»½<7§·›0µdãÆéíêºËrÏêqÛ^ïº6Ø· áDÄŠËØg ÙÜ:±2H…Îoþ»××ë¶è¶&Ê|‡�hêõ6}w÷£¡:[µoEKš:u\t½SeBæ°² VŽ'dô°·ÊÿF–Qtj¾mð¶(é³ÒéÈš±Ú1*ïô*qÓíJ©§ébvWMöY¶*!Žóâ^Ì] ç)Y!“Ã@É~º]9÷ûάh#—”V¹5ýòŦ`Všå8¶kÂÛœ_8,ã±Â–W�)JG;Á++E;FIÅÏfNØ\_«£,:¡Ùð²y/û}÷ÎnÎSû‹–,s¢ÅÆñ‡n]Óz¡[‹H’èóíÍ¢rʽ¼$/ âLãð` <'Ö¹6Šd_NŸP7J.mvŒr ½Ñ�XQH²¾ks©Ì;É a£‚l¹³™s\ ‚uÎS/ÙpùµÒ*Á—àt©ã2Ð⸕ÈÚÅç ¶bÅýЫsŒš/WŽMËQEÕ9O�Þ —÷ei6ñc@¬ V †Ÿ’â}7%6b $_Ñ€X@¬b±àøúP+]¼MgŒÇ�Ço9¹™ÚÄ&dʲOãz¦¸VÈ´MÍ�¦‡¼g:—Mü§<:Æ$ IðêøÛ¥?Ÿb¦OÂkà«ÃüØ{=í”eŸÆÝ®ö¿™Íì¾7ÇêpÃ8þ¥ë) ˆ•?ö 0æúúÓØíߣ ¬Í¦1Að1œLª;ߪÁ+Ü«|Ïfèëýöi<~__~ôýOãvÛ=±‘ú»ž“ýù”û3}+…?ö(2&ÿcÏwî,³VñD&3ìüFˆî\ý¾Óv›Û\G|_›?æïÙa7§ú3Ð'á�‹ 4Q$‡ÕOŸÆZë6¦ÓùLå ÃõO>¬³П±ò{ìÌe6ܳƒ_±ÉW¥ýñ¨÷íF›ÝUŸ�^ëŽÉ×eóºíǰÑP™Õ¯ñ®ªgÑûÛÖ{S{•oÛoS}uìê6._·Í÷ êÕŸËôå*úsÙ߯6ýâ�}ù¿gú$¼f*Œ²¤Ìú­=Æ Z«C•Óïüý3V–©ó¥©Ö‡ÕAÜ`†n0�?�‡CýÝë©,»¶žï`a¨ï‡r ët>†Æ¸òóßï÷?†ijM¤ÃÑÈ»V½Øq{=­ñÎfZËïõŒév÷‰Xu-j¯(R½ú}cºÝÝëíûš(f3«'êÕuêtTîphŒç©ì~ÿÓ8Më¥ûh۸ݶ÷LŸ7åà¦ûe;ƨüápþI]ïé·aý+²Ìþ^ªíÏÛôå}ûó¦ßÆ®ýù}y»ß³ú‡^ã±ûl0Ðgó¿_ú$¼zžŸ«yýô}ÿôýóóOßÁæcƒ@Ç>?/6™üôý—/?}ïyËï�FËçìõÖ×)Žçß‹¢âò{½Ÿ¾ –¿?üô}¹÷ E†,.(Gçqš*jâìLN’‡¹_ú|<ÎG‹è½,[Ž )º^¨¾?×­/oÓ/Õ—·©³Þ[^.sß§OÂ[á«êª2�Ù%+[ºóJß=tQ�¨x^Ž‚y3¥¥\èÞ§q¿¯ÎŸ$Ê ‘³|‰õÜÍõÖÀëûåÃmûÉœŸ©ŒÅë,ãïPåýJM‚šddª�ó¶ÞcP|Éþür}¹\¿8T_Þ¾ÎvI'/8ìÒ }+;ble4í¸ËtŸ‰¿Ìà¸{DÃǰÓÑúkh°ßo`8<۶庉«Û�D«±mw¿Æcµ»,}_õ²¢Þ#³.ýù­õåíê¬É_“ý¼(È[4è“ð6¨ÐÁv8”9²Ì�Ù³KGZºø1ìõŠH—àÝÊ_?¸Ùœ>8m}XUs­Ibq�™ùf¹kø4β�áx¬'9ý¶Ü{A°h*‡—îÏëëûó)öå]ê<3É2±œ…> XV¶D�ºß׺îêµhuzy¥;³a™ĪðurÓ“Z’¯—æÃwíð�Ž¼Û‹ÈM!…» ªû-/i�¸ÈT¼êxÛ~ÅOsîUÅ.÷+IT¿ü:¾5EGƒãË÷çr}yS>ž¼}�eq˜Íl4Ï¢ ¡Obe§®ßד@¯W䈦§ƒÑHÁ: H£±<ë‡>Ö–eó&ÇõOR|º]u’2åïK¾Ck]¹Ú'š*"Û‹&óUƒq·+'ºùã:Ymjî]î—w ¦ÖaϽE8ñ»?—íËÛõçSìË»×Ùúx¬:'}^?gÏÏÕ*5ßéH=Ûn¸'–ÕèÃ8v\ùdS󹜒�c=jwÐOãñØåUpkéŸÆ‹¹ÂÐ=%„a>×BÑ÷ÕAín¤Öym6³åê ¡×S]ÒÔ™8mþ›és¹Ü2�UC« ›'"ËÍ“e®ŽÛÕÛµ‡}ÂQ^ 7ø¬:Þ>%Ùk�Í”3!ŸcÃÖ+Ÿ¢ès ~zÏ:üæÛvýýZn«^O›î¹ß™M¿^$C•ýy›¾¼k.óÛØ¶?—éËŸÆù¼FÛõçíÏ�†1“I~Yjõ=¢Obe‡A.ïEŸ$eÖT5À£AÃ&EJÓui¶­çû6ËJn³ƒb5i¼]ØÞaÖ�ë¼ëµ¯+[Qž§AjuÛä�?ΤYî~Ù'¾ü1EïÁaûó.}y×þüúòúc�2íAŸÄÊÑD7Àq{NYÜЗ ¾¢  ÎÔÆ²²jÝç+€Ó‚¾ ¯V¬Á2 V+€X@¬ V±Pÿ?>Gˆ\è™B0%tEXtdate:create2019-11-22T16:53:25+00:00‚œ…%tEXtdate:modify2019-11-22T16:53:25+00:00óÁª9tEXtpdf:VersionPDF-1.5S6ZÁIEND®B`‚scikit-hep-boost-histogram-b4c49b0/docs/_src/000077500000000000000000000000001524660700500211655ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/docs/_src/images/000077500000000000000000000000001524660700500224325ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/docs/_src/images/.gitignore000066400000000000000000000000141524660700500244150ustar00rootroot00000000000000*.pdf *.png scikit-hep-boost-histogram-b4c49b0/docs/_src/images/BoostHistogramPythonSource.svg000066400000000000000000001340211524660700500305030ustar00rootroot00000000000000 image/svg+xml scikit-hep-boost-histogram-b4c49b0/docs/_src/images/Makefile000066400000000000000000000004011524660700500240650ustar00rootroot00000000000000SRC = $(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 $@ scikit-hep-boost-histogram-b4c49b0/docs/_src/images/axis_category.tex000066400000000000000000000017211524660700500260160ustar00rootroot00000000000000\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|bh.axis.IntCategory([2,5,8,3,7])|}; \end{tikzpicture} \end{document} scikit-hep-boost-histogram-b4c49b0/docs/_src/images/axis_circular.tex000066400000000000000000000017251524660700500260110ustar00rootroot00000000000000\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|bh.axis.Regular(8,0,2*np.pi, circular=True)|}; \end{tikzpicture} \end{document} scikit-hep-boost-histogram-b4c49b0/docs/_src/images/axis_integer.tex000066400000000000000000000015661524660700500256450ustar00rootroot00000000000000\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|bh.axis.Integer(0,5)|}; \end{tikzpicture} \end{document} scikit-hep-boost-histogram-b4c49b0/docs/_src/images/axis_regular.tex000066400000000000000000000016161524660700500256450ustar00rootroot00000000000000\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|bh.axis.Regular(10,0,1)|}; \end{tikzpicture} \end{document} scikit-hep-boost-histogram-b4c49b0/docs/_src/images/axis_variable.tex000066400000000000000000000016611524660700500257710ustar00rootroot00000000000000\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|bh.axis.Variable([0,.3,.5,1])|}; \end{tikzpicture} \end{document} scikit-hep-boost-histogram-b4c49b0/docs/_src/images/histogram_design.tex000066400000000000000000000044161524660700500265070ustar00rootroot00000000000000\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}] % Main grid \draw [presDark] (-2,-2) rectangle (2,2); \foreach \i in {-1,0,1} { \draw [presDark] (-2,\i) -- (2,\i); } \foreach \i in {-1.50465122, -0.76393202, 0.34370152} { \draw [presDark] (\i, -2) -- (\i, 2); } \draw [latex-, presDark] (2,2) -- (2.4,2.2) node [right] {Storage $\begin{pmatrix}\textrm{Static}\\ \textrm{Dynamic}\end{pmatrix}$}; % Left axis \draw [presDark2] (-2.3,-2) -- (-2.3,2); \foreach \i in {-2,-1,0,1,2} { \draw [presDark2] (-2.45,\i) -- (-2.15,\i); } \node at (-2.3,2) [above, presDark2] {Regular axis}; % Bottom axis \draw [presDark2] (-2,-2.3) -- (2,-2.3); \foreach \i in {-2. , -1.50465122, -0.76393202, 0.34370152, 2.} { \draw [presDark2] (\i, -2.45) -- (\i, -2.15); } \node at (2,-2.2) [presDark2, right=.4cm, align=left] {Regular axis with\\log transform}; % Axes \begin{scope}[xshift=-2.9cm, yshift=-2.3cm, rotate=45] \draw [presDark2, decorate, decoration={brace,amplitude=10pt}, xshift=-4pt, yshift=0pt] (0.5,-0.7) -- (0.5,0.7); \node at (-.3,0) [presDark2] {axes}; \end{scope} % Overflow bin \draw [presLight2] (2.05,-2.3) -- (2.4, -2.3); \draw [presLight2] (2.4, -2.45) -- (2.4, -2.15); \draw [presLight2, latex- ] (2.2, -2.3) -- (2.2, -2.6) node [below] {Optional overflow}; % Underflow bin \draw [presLight2] (-2.05,-2.3) -- (-2.4, -2.3); \draw [presLight2] (-2.4, -2.45) -- (-2.4, -2.15); \draw [presLight2, latex- ] (-2.25, -2.3) -- (-1.8, -2.6) node [below] {Optional underflow}; % Accumulators \node (accum) at (1.2,1.5) [circle, minimum width=.7cm, draw, presLight] {}; \draw [presLight, latex-] (accum) -- (2.25, .5) node [right, align=left] {Accumulator\\int, double,\\unlimited, ...}; \end{tikzpicture} \end{document} scikit-hep-boost-histogram-b4c49b0/docs/api/000077500000000000000000000000001524660700500210105ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/docs/api/boost_histogram.axis.rst000066400000000000000000000004601524660700500257100ustar00rootroot00000000000000boost\_histogram.axis ===================== .. automodule:: boost_histogram.axis :members: :undoc-members: :show-inheritance: boost\_histogram.axis.transform =============================== .. automodule:: boost_histogram.axis.transform :members: :undoc-members: :show-inheritance: scikit-hep-boost-histogram-b4c49b0/docs/api/boost_histogram.rst000066400000000000000000000022271524660700500247500ustar00rootroot00000000000000boost\_histogram ================ .. automodule:: boost_histogram :members: :undoc-members: :show-inheritance: .. include:: boost_histogram.axis.rst boost\_histogram.accumulators ============================= .. automodule:: boost_histogram.accumulators :members: :undoc-members: :show-inheritance: boost\_histogram.histogram ========================== .. automodule:: boost_histogram.histogram :members: :undoc-members: :show-inheritance: boost\_histogram.numpy ====================== .. automodule:: boost_histogram.numpy :members: :undoc-members: :show-inheritance: boost\_histogram.storage ======================== .. automodule:: boost_histogram.storage :members: :undoc-members: :show-inheritance: boost\_histogram.tag ==================== .. automodule:: boost_histogram.tag :members: :undoc-members: :show-inheritance: boost\_histogram.typing ======================= .. automodule:: boost_histogram.typing :members: :undoc-members: :show-inheritance: boost\_histogram.view ===================== .. automodule:: boost_histogram.view :members: :undoc-members: :show-inheritance: scikit-hep-boost-histogram-b4c49b0/docs/api/modules.rst000066400000000000000000000000621524660700500232100ustar00rootroot00000000000000 .. toctree:: :maxdepth: 4 boost_histogram scikit-hep-boost-histogram-b4c49b0/docs/banner_slides.md000066400000000000000000000037641524660700500234030ustar00rootroot00000000000000--- aspectratio: 169 fontfamily: bookman orphan: true --- # Simple usage ```python import boost_histogram as bh # Make a histogram h = bh.Histogram(bh.axis.Regular(10, 0, 1)) # Fill it with events h.fill([0.2, 0.3, 0.6, 0.9]) # Compute the sum total = h.sum() ``` # Advanced Indexing ```python # Slice in data coordinates sliced_h = h[bh.loc(0.5) : bh.loc(1.5)] # Sum over and rebin easily smaller = h[::sum, :: bh.rebin(2)] # Set and access easily h[...] = np.asarray(prebinned) # Project and reorder axes h2 = h.project(1, 0) ``` # Many Axis types ::: columns ::: {.column width="60%"} - Regular: evenly or functionally spaced binning - Variable: arbitrary binning - Integer: integer binning - IntCategory: arbitrary integers - StrCategory: strings - Boolean: True/False Most Axis types support optional extras: - Underflow and/or Overflow bins - Growth - Circular (wraparound) ::: ::: {.column width="40%"} \begin{center} \includegraphics[width=1.05\textwidth]{docs/\_images/axis_circular.png} \includegraphics[width=.7\textwidth]{docs/\_images/axis_regular.png} \includegraphics[width=.9\textwidth]{docs/\_images/axis_variable.png} \includegraphics[width=.65\textwidth]{docs/\_images/axis_integer.png} \includegraphics[width=\textwidth]{docs/\_images/axis_category.png} \end{center} ::: ::: # Advanced Storages - Integers - Floats - Weighted Sums - Means - Weighted Means Supports the UHI `PlottableHistogram` protocol! ```python import mplhep mplhep.histplot(h) ``` # NumPy compatibility ```python # Drop in faster replacement H, edges = bh.numpy.histogram(data) # Drop in threaded faster replacement H, edges = bh.numpy.histogram(data, threads=4) # Drop in much faster replacement for 2D+ H, e1, e2 = bh.numpy.histogram2d(xdata, ydata) # Return a Histogram, convert back to NumPy style h = bh.numpy.histogram(data, histogram=bh.Histogram) H, edges = h.to_numpy() ``` scikit-hep-boost-histogram-b4c49b0/docs/changelog.md000066400000000000000000001533411524660700500225170ustar00rootroot00000000000000# Changelog ## Version 1.8 ### Version 1.8.1 #### Changes - `MultiCell` now requires a cell count of one or more; the argument has no default by @henryiii in [#1179][] - Filling an integer axis with a value that does not fit in an `int` raises `ValueError` instead of wrapping around by @henryiii in [#1181][] - `boost_histogram.numpy` matches NumPy more closely: empty input, 1D `histogramdd` input, a single edge array in `histogram2d`, and range validation by @henryiii in [#1178][] #### Fixes - Encode UTF-8 for non-ASCII strings filled into `StrCategory` from a list or array by @henryiii in [#1177][] - Avoid C++ aborts in axis metadata compare, `MultiCell` fill, and `Boolean.value()`; correct `empty()` for `MultiCell` by @henryiii in [#1179][] - Keep growth axes valid after a fill or merge, and raise worker exceptions from a threaded fill by @henryiii in [#1182][] - Correct UHI indexing with flow locators, disabled flow bins, and sliced group rebin by @henryiii in [#1183][] - Give each axis copy its own metadata dict, and correct serialization, `register()`, integer-storage multiplication, transform equality, and repr by @henryiii in [#1184][] - Avoid Boost indexed-range UB in rank-0 histograms and add Windows 3.14t testing by @ikrommyd in [#1175][] #### Performance - Cheaper fill argument variant, faster dtype dispatch, and move support for `MultiCell` by @henryiii in [#1181][] - Release the GIL on copy, reset, compare, and in-place histogram operations by @henryiii in [#1187][] #### Documentation - Document that `view()` can be invalidated by a growing fill by @henryiii in [#1182][] - Restore the `Histogram` API page on Read the Docs and correct the user guide and wheel table by @henryiii in [#1180][] #### Developer changes - Hold the GIL when copying or destroying Python objects in axes by @henryiii in [#1172][] - Remove the dead `regular_numpy` Python axis, factor out pybind11 helpers, and correct stub drift and weak tests by @henryiii in [#1185][] - Config and CI fixes: uv cache keys, workflow permissions and timeouts, and `AGENTS.md` commands by @henryiii in [#1180][] - Test Python 3.15 now that NumPy wheels are on PyPI by @henryiii in [#1170][] - Fix the scheduled wheel and weekly-update failures by @henryiii in [#1173][] - Bump astral-sh/setup-uv from 9.0.0 to 10.0.1 by @dependabot in [#1174][] ### Version 1.8.0 #### Features - Python 3.15 wheels, including free-threading and Pyodide, by @henryiii in [#1163][] - Support `flow=False` in histogram projections by @Rishabh-git10 in [#1109][] - Add `Histogram.allclose` for approximate histogram comparison by @gohil-jay in [#778][] - Add a pytest plugin with rich assertion reporting for histogram comparisons by @gohil-jay in [#763][] #### Changes - Support subtraction for the `MultiCell` storage by @henryiii in [#1131][] - Support vectorized NumPy-array indexing for getting and setting bins by @henryiii in [#1134][] - Support addition for `Mean`/`WeightedMean` views by @henryiii in [#1137][] - Add reflected `__rsub__` and `__rtruediv__` operators by @henryiii in [#1155][] - Accept a scalar `sample` argument in `fill` by @henryiii in [#1124][] - Rebin with groups: `MultiCell` support, flow handling, and a `group_mapping` protocol by @henryiii in [#1148][] #### Fixes - Reject arithmetic with mismatched storages by @divyanshu-iitian in [#1166][] - Return a zero-filled vector from `MultiCell` `sum()` over zero bins instead of an empty list by @henryiii in [#1132][] - Raise `TypeError` instead of `ValueError` for a non-integer locator `offset` and a string `sample=` by @henryiii in [#1163][] - Clear error for unsupported histogram in-place operations by @henryiii in [#1130][] - Support common transformed axes in the `numpy` shortcuts by @henryiii in [#1122][] - Correct `Mean` storage string rejection and `__setitem__` error message by @henryiii in [#1117][] - Several fixes from automated review by @henryiii in [#1116][] and [#1118][] - Promote integer storages to double on division by @henryiii in [#1138][] - Do not mutate input edges and validate `histogramdd` dimensions in the `numpy` module by @henryiii in [#1144][] - Fix `out=` handling, scalar division, and broadcasting in structured views by @henryiii in [#1146][] - Fix non-contiguous array handling and memory safety in the C++ bindings by @henryiii in [#1147][] - Fix indexing, `__setitem__` flow transfer, and fill edge cases by @henryiii in [#1149][] - Avoid Boost indexed-range UB in rank-0 `sum()`/`empty()` by @henryiii in [#1153][] - Normalize negative integer slice bounds in indexing by @henryiii in [#1156][] - Remove ODR-violating `operator<<` for `accumulators::sum` by @henryiii in [#1161][] #### Performance - Avoid deep copies in comparisons and hot paths by @henryiii in [#1150][] #### Documentation - Cleanup, modernization, and gap-filling in the user guide by @henryiii in [#1141][] - Note that fancy indexing on views returns a copy by @henryiii in [#1136][] - Correct the wheel table and add a version-support checklist by @henryiii in [#1168][] #### Tests - Use `pytest.approx` instead of NumPy assertion helpers by @gohil-jay in [#780][] - Validate UHI serialization against the uhi JSON schema by @henryiii in [#1140][] - Broaden `MultiCell` storage coverage by @henryiii in [#1127][] - Add regression test for scalar-broadcast fill on non-inclusive axes by @henryiii in [#1125][] - Add ND projection test for `flow=False` by @Rishabh-git10 in [#1111][] - Disable cloudpickle tests on PyPy by @henryiii in [#1120][] - Skip the pytest plugin end-to-end test on Emscripten by @henryiii in [#1152][] #### Internal - Bump to Boost 1.91 by @henryiii in [#1112][] - Bump setup-uv to the maintained tag scheme by @henryiii in [#1110][] - Update cibuildwheel to v4.0 by @henryiii in [#1119][] - Update AGENTS instructions and ignore the CLAUDE symlink by @henryiii in [#1121][] - Make the nox `hist` session fail on a missing dir and mypy override errors by @henryiii in [#1135][] - Set up uv cache keys by @henryiii in [#1142][] - pybind11 v3 pin, MSVC `/WX`, config and stub cleanups by @henryiii in [#1145][] - Remove dead code and minor C++ cleanups by @henryiii in [#1151][] - Remove the needs-changelog labeler by @henryiii in [#1164][] [#763]: https://github.com/scikit-hep/boost-histogram/pull/763 [#778]: https://github.com/scikit-hep/boost-histogram/pull/778 [#780]: https://github.com/scikit-hep/boost-histogram/pull/780 [#1109]: https://github.com/scikit-hep/boost-histogram/pull/1109 [#1110]: https://github.com/scikit-hep/boost-histogram/pull/1110 [#1111]: https://github.com/scikit-hep/boost-histogram/pull/1111 [#1112]: https://github.com/scikit-hep/boost-histogram/pull/1112 [#1116]: https://github.com/scikit-hep/boost-histogram/pull/1116 [#1117]: https://github.com/scikit-hep/boost-histogram/pull/1117 [#1118]: https://github.com/scikit-hep/boost-histogram/pull/1118 [#1119]: https://github.com/scikit-hep/boost-histogram/pull/1119 [#1120]: https://github.com/scikit-hep/boost-histogram/pull/1120 [#1121]: https://github.com/scikit-hep/boost-histogram/pull/1121 [#1122]: https://github.com/scikit-hep/boost-histogram/pull/1122 [#1124]: https://github.com/scikit-hep/boost-histogram/pull/1124 [#1125]: https://github.com/scikit-hep/boost-histogram/pull/1125 [#1127]: https://github.com/scikit-hep/boost-histogram/pull/1127 [#1130]: https://github.com/scikit-hep/boost-histogram/pull/1130 [#1131]: https://github.com/scikit-hep/boost-histogram/pull/1131 [#1132]: https://github.com/scikit-hep/boost-histogram/pull/1132 [#1134]: https://github.com/scikit-hep/boost-histogram/pull/1134 [#1135]: https://github.com/scikit-hep/boost-histogram/pull/1135 [#1136]: https://github.com/scikit-hep/boost-histogram/pull/1136 [#1137]: https://github.com/scikit-hep/boost-histogram/pull/1137 [#1138]: https://github.com/scikit-hep/boost-histogram/pull/1138 [#1140]: https://github.com/scikit-hep/boost-histogram/pull/1140 [#1141]: https://github.com/scikit-hep/boost-histogram/pull/1141 [#1142]: https://github.com/scikit-hep/boost-histogram/pull/1142 [#1144]: https://github.com/scikit-hep/boost-histogram/pull/1144 [#1145]: https://github.com/scikit-hep/boost-histogram/pull/1145 [#1146]: https://github.com/scikit-hep/boost-histogram/pull/1146 [#1147]: https://github.com/scikit-hep/boost-histogram/pull/1147 [#1148]: https://github.com/scikit-hep/boost-histogram/pull/1148 [#1149]: https://github.com/scikit-hep/boost-histogram/pull/1149 [#1150]: https://github.com/scikit-hep/boost-histogram/pull/1150 [#1151]: https://github.com/scikit-hep/boost-histogram/pull/1151 [#1152]: https://github.com/scikit-hep/boost-histogram/pull/1152 [#1153]: https://github.com/scikit-hep/boost-histogram/pull/1153 [#1155]: https://github.com/scikit-hep/boost-histogram/pull/1155 [#1156]: https://github.com/scikit-hep/boost-histogram/pull/1156 [#1161]: https://github.com/scikit-hep/boost-histogram/pull/1161 [#1163]: https://github.com/scikit-hep/boost-histogram/pull/1163 [#1164]: https://github.com/scikit-hep/boost-histogram/pull/1164 [#1166]: https://github.com/scikit-hep/boost-histogram/pull/1166 [#1168]: https://github.com/scikit-hep/boost-histogram/pull/1168 [#1172]: https://github.com/scikit-hep/boost-histogram/pull/1172 [#1175]: https://github.com/scikit-hep/boost-histogram/pull/1175 [#1170]: https://github.com/scikit-hep/boost-histogram/pull/1170 [#1173]: https://github.com/scikit-hep/boost-histogram/pull/1173 [#1174]: https://github.com/scikit-hep/boost-histogram/pull/1174 [#1177]: https://github.com/scikit-hep/boost-histogram/pull/1177 [#1178]: https://github.com/scikit-hep/boost-histogram/pull/1178 [#1179]: https://github.com/scikit-hep/boost-histogram/pull/1179 [#1180]: https://github.com/scikit-hep/boost-histogram/pull/1180 [#1181]: https://github.com/scikit-hep/boost-histogram/pull/1181 [#1182]: https://github.com/scikit-hep/boost-histogram/pull/1182 [#1183]: https://github.com/scikit-hep/boost-histogram/pull/1183 [#1184]: https://github.com/scikit-hep/boost-histogram/pull/1184 [#1185]: https://github.com/scikit-hep/boost-histogram/pull/1185 [#1187]: https://github.com/scikit-hep/boost-histogram/pull/1187 ## Version 1.7 ### Version 1.7.2 #### Features - No storage option/support for serialization, by @henryiii in [#1098][] #### Fixes - Correct repr for MultiCell by @henryiii in [#1087][] - Fix serialization round-trip for 3D histograms with empty axes by @Copilot in [#1094][] - Fix `__setitem__` axis index mismatch when slices are not leading indices by @Copilot in [#1097][] - Support 0D MeanView/WeightedMeanView by @Copilot in [#1100][] - Broadcast scalar axis args to match array sample length in Mean/WeightedMean fill by @Copilot in [#1101][] #### Typing - Slightly better getitem type by @henryiii in [#1086][] #### Internal - Use pattern matching in another place by @henryiii in [#1092][] - Move to using prek by @henryiii in [#1105][] - Add `AGENTS.md` by @henryiii in [#1106][] [#1098]: https://github.com/scikit-hep/boost-histogram/pull/1098 [#1087]: https://github.com/scikit-hep/boost-histogram/pull/1087 [#1094]: https://github.com/scikit-hep/boost-histogram/pull/1094 [#1097]: https://github.com/scikit-hep/boost-histogram/pull/1097 [#1100]: https://github.com/scikit-hep/boost-histogram/pull/1100 [#1101]: https://github.com/scikit-hep/boost-histogram/pull/1101 [#1086]: https://github.com/scikit-hep/boost-histogram/pull/1086 [#1092]: https://github.com/scikit-hep/boost-histogram/pull/1092 [#1105]: https://github.com/scikit-hep/boost-histogram/pull/1105 [#1106]: https://github.com/scikit-hep/boost-histogram/pull/1106 ### Version 1.7.1 #### Typing - Make the `Histogram` type generic, by @henryiii in [#1081][], [#1082][], [#1083][] - The `MultiCell` storage takes an argument (missed in typing), by @henryiii in [#1080][] [#1080]: https://github.com/scikit-hep/boost-histogram/pull/1080 [#1081]: https://github.com/scikit-hep/boost-histogram/pull/1081 [#1082]: https://github.com/scikit-hep/boost-histogram/pull/1082 [#1083]: https://github.com/scikit-hep/boost-histogram/pull/1083 ### Version 1.7.0 #### Features - `MultiCell` storage, supporting a variable number of values, by @Superharz in [#1008][] - Record `growth=True` in writer-info (serialization), and respect it if present by @henryiii in [#1074][] - Drop Python 3.9 by @henryiii in [#1057][] - Drop Python 3.13t (use 3.14t instead) by @henryiii in [#1079][] #### Bug fixes - Don't add empty metadata dicts by @henryiii in [#1048][] - Avoid compiler warning: declaration of 'i' hides previous local declaration by @Superharz in [#1068][] #### Internal - Refactor `__getitem__` using pattern matching by @henryiii in [#1067][] - pytest `log_level` is better than `log_cli_level` by @henryiii in [#1059][] - Setting `__file__` no longer required (pybind11 3.0) by @henryiii in [#1066][] - This should work on iOS now in cibuildwheel by @henryiii in [#1049][] [#1008]: https://github.com/scikit-hep/boost-histogram/pull/1008 [#1048]: https://github.com/scikit-hep/boost-histogram/pull/1048 [#1049]: https://github.com/scikit-hep/boost-histogram/pull/1049 [#1057]: https://github.com/scikit-hep/boost-histogram/pull/1057 [#1059]: https://github.com/scikit-hep/boost-histogram/pull/1059 [#1066]: https://github.com/scikit-hep/boost-histogram/pull/1066 [#1067]: https://github.com/scikit-hep/boost-histogram/pull/1067 [#1068]: https://github.com/scikit-hep/boost-histogram/pull/1068 [#1074]: https://github.com/scikit-hep/boost-histogram/pull/1074 [#1079]: https://github.com/scikit-hep/boost-histogram/pull/1079 ## Version 1.6 ### Version 1.6.1 This release fixes several issues with serialization as it's being prepared in Hist/uhi. #### Features - Support `__dict__` on histogram like axes [#1041][] - Allow library to be specified in `writer_info` removal [#1042][] #### Bug fixes - Avoid mutating lists given in indexing [#1047][] - Histogram metadata not being serialized correctly [#1038][] - Unset classic metadata shouldn't show up in serialization [#1039][] - Set dict correctly when unserializing [#1040][] - Serialization was broken for N>1D complex storage histograms [#1043][] [#1038]: https://github.com/scikit-hep/boost-histogram/pull/1038 [#1039]: https://github.com/scikit-hep/boost-histogram/pull/1039 [#1040]: https://github.com/scikit-hep/boost-histogram/pull/1040 [#1041]: https://github.com/scikit-hep/boost-histogram/pull/1041 [#1042]: https://github.com/scikit-hep/boost-histogram/pull/1042 [#1043]: https://github.com/scikit-hep/boost-histogram/pull/1043 [#1047]: https://github.com/scikit-hep/boost-histogram/pull/1047 ### Version 1.6.0 This release adds support for UHI serialization for use with the `uhi` library, supported in the next release of `uhi`. A new diagnostic test has been added in the wheel, so you can quickly check your compile of boost-histogram without the full test suite. Quite a few fixes are present, as well, including a fix for `*=`, setting a range with a scalar, setting with a callable in a slice. Backend changes include many new clang-tidy checks that reduce the number of copies and refcount changes. We now build with pybind11 3.0. This release drops support for Python 3.8, and adds Python 3.14(t), iOS, Windows ARM, and GraalPy. #### Features - Initial support for uhi's serialization [#997][], [#1030][], [#1033][], [#1034][], [#1035][] - Drop Python 3.8 [#976][] - Support Windows ARM [#1001][] - Support Python 3.14 and 3.14t [#1018][] - Support iOS [#1020][], [#1027][] - Added diagnostics test [#1022][] - Recommend/use `np.s_` for slicing in dicts [#1002][] #### Bug fixes - Support setting ranges with histograms (fixes `*=` too) [#1036][] - Support a callable in range setting [#1036][] - Support setting a range with a scalar [#1000][] - Account for fuzzy edge values in rebinning with edges [#999][] - Rename C++ metadata to `raw_metadata` [#979][] - Address lots of clang-tidy suggestions, should be faster with less copies/refcount bumps [#1009][], [#1010][], [#1011][], [#1012][], [#1013][], [#1014][], [#1015][] #### CI and testing - Add CMake presets for testing and clang-tidy [#1009][] [#976]: https://github.com/scikit-hep/boost-histogram/pull/976 [#979]: https://github.com/scikit-hep/boost-histogram/pull/979 [#997]: https://github.com/scikit-hep/boost-histogram/pull/997 [#999]: https://github.com/scikit-hep/boost-histogram/pull/999 [#1000]: https://github.com/scikit-hep/boost-histogram/pull/1000 [#1001]: https://github.com/scikit-hep/boost-histogram/pull/1001 [#1002]: https://github.com/scikit-hep/boost-histogram/pull/1002 [#1009]: https://github.com/scikit-hep/boost-histogram/pull/1009 [#1010]: https://github.com/scikit-hep/boost-histogram/pull/1010 [#1011]: https://github.com/scikit-hep/boost-histogram/pull/1011 [#1012]: https://github.com/scikit-hep/boost-histogram/pull/1012 [#1013]: https://github.com/scikit-hep/boost-histogram/pull/1013 [#1014]: https://github.com/scikit-hep/boost-histogram/pull/1014 [#1015]: https://github.com/scikit-hep/boost-histogram/pull/1015 [#1018]: https://github.com/scikit-hep/boost-histogram/pull/1018 [#1020]: https://github.com/scikit-hep/boost-histogram/pull/1020 [#1022]: https://github.com/scikit-hep/boost-histogram/pull/1022 [#1027]: https://github.com/scikit-hep/boost-histogram/pull/1027 [#1030]: https://github.com/scikit-hep/boost-histogram/pull/1030 [#1033]: https://github.com/scikit-hep/boost-histogram/pull/1033 [#1034]: https://github.com/scikit-hep/boost-histogram/pull/1034 [#1035]: https://github.com/scikit-hep/boost-histogram/pull/1035 [#1036]: https://github.com/scikit-hep/boost-histogram/pull/1036 ## Version 1.5 ### Version 1.5.2 Fix for axis metadata not passing though non-uniform rebinning correctly. Flow bins are now preserved when doing a non-uniform rebinning. Also adds the ability to rebin by edges or an existing axis. This is expected to be the last release with Python 3.8 support. Manylinux support will likely be bumped from `2024` to `2_28` soon, as well. #### Features - Support `edges=` and `axis=` in `bh.rebin` [#977][] - Allow axis to be passed through `bh.rebin` [#980][], [#982][] #### Bug fixes - Axis metadata was broken when rebinning [#978][] - Flow bins were lost when using variable rebinning [#977][] - Scikit-build-core 0.11, with PEP 639 license metadata [#986][], [#988][] - Prepare for an potential upcoming pybind11 3 change [#994][] #### CI and testing - Remove extras like `[test]` in favor of dependency-groups [#973][] - Multithreaded test running [#987][] [#959]: https://github.com/scikit-hep/boost-histogram/pull/959 [#973]: https://github.com/scikit-hep/boost-histogram/pull/973 [#977]: https://github.com/scikit-hep/boost-histogram/pull/977 [#978]: https://github.com/scikit-hep/boost-histogram/pull/978 [#980]: https://github.com/scikit-hep/boost-histogram/pull/980 [#982]: https://github.com/scikit-hep/boost-histogram/pull/982 [#986]: https://github.com/scikit-hep/boost-histogram/pull/986 [#987]: https://github.com/scikit-hep/boost-histogram/pull/987 [#988]: https://github.com/scikit-hep/boost-histogram/pull/988 [#994]: https://github.com/scikit-hep/boost-histogram/pull/994 ### Version 1.5.1 #### Bug fixes - Make non-uniform rebinning work for Weight() and friends [#972][] [#972]: https://github.com/scikit-hep/boost-histogram/pull/972 ### Version 1.5.0 #### Features - Support variable rebinning [#913][] - Removed support for Python 3.7 [#952][] - Added support for Python 3.13 and free-threaded Python 3.13t [#950][] - Add GitHub artifact attestations to package distribution [#933][] #### Backend and docs - Move build to scikit-build-core [#887][] - Clean up sdist files [#928][] - Minor touchup to build backend, use scikit-build-core 0.10+ [#948][] #### CI and testing - Try cibuildwheel pyodide [#935][] - Try to fix pyodide build [#934][] - Update emscripten build [#937][] - Bump to Pyodide 3.12 [#938][] - Speed up pyodide job by a few seconds [#941][] - Update jobs and noxfile [#929][] - Use pylint 3.2 github formatter instead [#932][] - Add some CI speedups [#939][] - Faster cibuildwheel [#942][] - Adapt to final release of NumPy 2.1.0 [#951][] [#887]: https://github.com/scikit-hep/boost-histogram/pull/887 [#913]: https://github.com/scikit-hep/boost-histogram/pull/913 [#928]: https://github.com/scikit-hep/boost-histogram/pull/928 [#929]: https://github.com/scikit-hep/boost-histogram/pull/929 [#932]: https://github.com/scikit-hep/boost-histogram/pull/932 [#933]: https://github.com/scikit-hep/boost-histogram/pull/933 [#934]: https://github.com/scikit-hep/boost-histogram/pull/934 [#935]: https://github.com/scikit-hep/boost-histogram/pull/935 [#937]: https://github.com/scikit-hep/boost-histogram/pull/937 [#938]: https://github.com/scikit-hep/boost-histogram/pull/938 [#939]: https://github.com/scikit-hep/boost-histogram/pull/939 [#941]: https://github.com/scikit-hep/boost-histogram/pull/941 [#942]: https://github.com/scikit-hep/boost-histogram/pull/942 [#948]: https://github.com/scikit-hep/boost-histogram/pull/948 [#950]: https://github.com/scikit-hep/boost-histogram/pull/950 [#951]: https://github.com/scikit-hep/boost-histogram/pull/951 [#952]: https://github.com/scikit-hep/boost-histogram/pull/952 ## Version 1.4 ### Version 1.4.1 #### Features - NumPy 2 support. [#918][] - 32-bit Windows Python 3.12 wheel added (matching NumPy). [#920][] #### Bugfixes - Support filling Integer axes with unsigned integers [#917][] - Avoid triggering NumPy 2 dev release install on Python 3.12. [#914][] #### Backend and docs - Add missing API docs [#909][] - Use boost 1.84 [#920][] [#909]: https://github.com/scikit-hep/boost-histogram/pull/909 [#914]: https://github.com/scikit-hep/boost-histogram/pull/914 [#917]: https://github.com/scikit-hep/boost-histogram/pull/917 [#918]: https://github.com/scikit-hep/boost-histogram/pull/918 [#920]: https://github.com/scikit-hep/boost-histogram/pull/920 ### Version 1.4.0 #### Features - `overflow=False` is now supported for `IntCategory` and `StrCategory`. [#883][] #### Changes - Using `_storage_type` now produces a `DeprecationWarning` instead of `PendingDeprecationWarning`. [#801][] - Updated Boost to 1.82. The upper limit on Regular axes without overflow is now inclusive like NumPy. [#802][] - Produce more detailed error messages on C++ errors [#848][] #### Bugfixes - Make filling an integer axis with a float array (also) an error. [#876][] - Include `-latomic` on `armv7l` [#823][] #### Backend and docs - Add Python 3.12 support and binary wheels, also latest PyPy. `manylinux2014+` required. [#880][], [#878][] - Drop Python 3.6 support. [#798][] - Drop pre-built wheels for 32-bit Linux (NumPy also dropped). [#849][] - Add testing for WebAssembly (Pyodide). [#850][] - Use Ruff [#829][] [#798]: https://github.com/scikit-hep/boost-histogram/pull/798 [#801]: https://github.com/scikit-hep/boost-histogram/pull/801 [#802]: https://github.com/scikit-hep/boost-histogram/pull/802 [#823]: https://github.com/scikit-hep/boost-histogram/pull/823 [#829]: https://github.com/scikit-hep/boost-histogram/pull/829 [#848]: https://github.com/scikit-hep/boost-histogram/pull/848 [#849]: https://github.com/scikit-hep/boost-histogram/pull/849 [#850]: https://github.com/scikit-hep/boost-histogram/pull/850 [#876]: https://github.com/scikit-hep/boost-histogram/pull/876 [#878]: https://github.com/scikit-hep/boost-histogram/pull/878 [#880]: https://github.com/scikit-hep/boost-histogram/pull/880 [#883]: https://github.com/scikit-hep/boost-histogram/pull/883 ## Version 1.3 ### Version 1.3.2 #### Changes - Added `storage_type` operator and `storage_type()` function [#781][], with pending deprecation for `_storage_type`. [#786][] [#790][] - Better errors generated for missing or incorrect sample to mean storage. [#782][] - Better error message when views are set with an incompatible array. [#794][] #### Bug fixes - Patch broken sum with fully empty (0 bin) axis. [#718][] - Fix zero range `bh.numpy.histogram` to match `numpy.histogram` behavior. [#721][] - Avoid triggering `__init__` when copying (better support for subclasses with custom init's). [#759][] - IntCategory now supports numbers larger than 2^24 (now 2^53). [#792][] - Pick a subset now supported inside a larger expression. [#793][] #### Backend and docs - Minor optimizations for UFuncs. [#771][] - Added Python 3.11 wheels. [#789][] - Include PyPy 3.9 binary wheels. [#730][] - Using pybind11 2.10 [#767][] - Explicit `reset()` documentation. [#783][] - Minor cleanup and further removal of a little Python 2 back-compat code. - Warnings have better stacklevel settings. [#718]: https://github.com/scikit-hep/boost-histogram/pull/718 [#721]: https://github.com/scikit-hep/boost-histogram/pull/721 [#730]: https://github.com/scikit-hep/boost-histogram/pull/730 [#759]: https://github.com/scikit-hep/boost-histogram/pull/759 [#767]: https://github.com/scikit-hep/boost-histogram/pull/767 [#771]: https://github.com/scikit-hep/boost-histogram/pull/771 [#781]: https://github.com/scikit-hep/boost-histogram/pull/781 [#782]: https://github.com/scikit-hep/boost-histogram/pull/782 [#783]: https://github.com/scikit-hep/boost-histogram/pull/783 [#786]: https://github.com/scikit-hep/boost-histogram/pull/786 [#789]: https://github.com/scikit-hep/boost-histogram/pull/789 [#790]: https://github.com/scikit-hep/boost-histogram/pull/790 [#792]: https://github.com/scikit-hep/boost-histogram/pull/792 [#793]: https://github.com/scikit-hep/boost-histogram/pull/793 [#794]: https://github.com/scikit-hep/boost-histogram/pull/794 ### Version 1.3.1 #### Bug fixes - Fixed regression with invalid `.project` input causing segfaults. [#708][] - Minor skips for specific tests on ppc64le, primarily for a NumPy bug. [#707][] - Avoid using EH for program control, better on Pyodide. [#709][] - Fix regression with exact float not being accepted for `.index` for `IntCategory` in 1.3.0. Add `hist` nox session to check downstream (manually for the moment). [#710][] [#707]: https://github.com/scikit-hep/boost-histogram/pull/707 [#708]: https://github.com/scikit-hep/boost-histogram/pull/708 [#709]: https://github.com/scikit-hep/boost-histogram/pull/709 [#710]: https://github.com/scikit-hep/boost-histogram/pull/710 ### Version 1.3.0 #### User changes - PyPy 3.8 now supported with binary wheels. [#677][] - The GIL is released a little more often now. [#662][] - AxesTuple does not allow construction of non-axes. [#680][] - KeyError is now thrown when accessing a non-existent item in a Category Axis [#689][] - WeightedViews now support `np.cumsum` [#699][] #### Bug fixes - Fixed WeightedMean storages producing NaN for .variances() [#695][] - Modify local include slightly to enable WebAssembly compilation in Pyodide [#702][] #### Developer changes - Use PyLint in CI to check for some style issues [#690][] - Developer (CMake) installs no longer require toml [#698][] [#662]: https://github.com/scikit-hep/boost-histogram/pull/662 [#677]: https://github.com/scikit-hep/boost-histogram/pull/677 [#680]: https://github.com/scikit-hep/boost-histogram/pull/680 [#689]: https://github.com/scikit-hep/boost-histogram/pull/689 [#690]: https://github.com/scikit-hep/boost-histogram/pull/690 [#695]: https://github.com/scikit-hep/boost-histogram/pull/695 [#698]: https://github.com/scikit-hep/boost-histogram/pull/698 [#699]: https://github.com/scikit-hep/boost-histogram/pull/699 [#702]: https://github.com/scikit-hep/boost-histogram/pull/702 ## Version 1.2 ### Version 1.2.1 #### User changes - musllinux wheels now provided along with manylinux [#656][] #### Bug fixes - Fixed single-element negative growth fill [#654][] #### Developer changes - No longer require Docker for clang-format, runs online too [#610][] - Using pybind11 2.8.0 [#658][] [#610]: https://github.com/scikit-hep/boost-histogram/pull/610 [#654]: https://github.com/scikit-hep/boost-histogram/pull/654 [#656]: https://github.com/scikit-hep/boost-histogram/pull/656 [#658]: https://github.com/scikit-hep/boost-histogram/pull/658 ### Version 1.2.0 #### User changes - Python 3.10 officially supported, with wheels. - Support subtraction on histograms [#636][] - Integer histograms are now signed [#636][] #### Bug fixes - Support custom setters on AxesTuple subclasses. [#627][] - Faster picking if slices are not also used [#645][] or if they are [#648][] (1000x or more in some cases) - Throw an error when an AxesTuple setter is the wrong length (inspired by zip strict in Python 3.10) [#627][] - Fix error thrown on comparison with axis and non-axis object [#631][] - Static typing no longer thinks `storage=` is required [#604][] #### Developer changes - Support NumPy 1.21 for static type checking [#625][] - Use newer Boost 1.77 and Boost.Histogram 1.77+1 [#594][] - Provide nox support [#647][] [#594]: https://github.com/scikit-hep/boost-histogram/pull/594 [#604]: https://github.com/scikit-hep/boost-histogram/pull/604 [#625]: https://github.com/scikit-hep/boost-histogram/pull/625 [#627]: https://github.com/scikit-hep/boost-histogram/pull/627 [#631]: https://github.com/scikit-hep/boost-histogram/pull/631 [#636]: https://github.com/scikit-hep/boost-histogram/pull/636 [#645]: https://github.com/scikit-hep/boost-histogram/pull/645 [#647]: https://github.com/scikit-hep/boost-histogram/pull/647 [#648]: https://github.com/scikit-hep/boost-histogram/pull/648 ## Version 1.1 ### Version 1.1.0 #### User changes - Experimentally support list selection on categorical axes [#577][] - Support Python 3.8 on Apple Silicon [#600][] - Scaling and addition with a scalar affect flow bins too [#580][] - Change `sum_of_deltas_squared` to `_sum_of_deltas_squared` (was an implementation detail) [#602][] #### Bug fixes - Fix "picking" on a flow bin [#576][] - Better error message on getattr [#596][] #### Developer changes - Test on Python 3.10 beta releases [#600][] - Provide a CMakeLists for quick standalone Boost.Histogram C++ experiments [#591][] - Adding logging with pytest failure output [#575][] [#575]: https://github.com/scikit-hep/boost-histogram/pull/575 [#576]: https://github.com/scikit-hep/boost-histogram/pull/576 [#577]: https://github.com/scikit-hep/boost-histogram/pull/577 [#580]: https://github.com/scikit-hep/boost-histogram/pull/580 [#591]: https://github.com/scikit-hep/boost-histogram/pull/591 [#596]: https://github.com/scikit-hep/boost-histogram/pull/596 [#600]: https://github.com/scikit-hep/boost-histogram/pull/600 [#602]: https://github.com/scikit-hep/boost-histogram/pull/602 ## Version 1.0 ### Version 1.0.2 - Fix scaling a weighted storage [#559][] - Fix partial summation over a Categorical axis [#564][] - Support running type checking from Python < 3.8 [#542][] [#542]: https://github.com/scikit-hep/boost-histogram/pull/542 [#559]: https://github.com/scikit-hep/boost-histogram/pull/559 [#564]: https://github.com/scikit-hep/boost-histogram/pull/564 ### Version 1.0.1 #### Subclassing Histogram changes - A `family=` is no longer required if you _only_ subclass Histogram. [#533][] #### Bug fixes - Fix summing of Mean/WeightedMean accumulators [#537][] - Added missing dependency on `typing_extensions` for Python 3.6 & 3.7 [#529][] #### Typing changes - Added Ellipsis support to typing. [#525][] - Better typing for Views. [#530][] - Fixed issue with Histogram copy constructor requiring metadata [#532][] [#525]: https://github.com/scikit-hep/boost-histogram/pull/525 [#526]: https://github.com/scikit-hep/boost-histogram/pull/526 [#529]: https://github.com/scikit-hep/boost-histogram/pull/529 [#530]: https://github.com/scikit-hep/boost-histogram/pull/530 [#532]: https://github.com/scikit-hep/boost-histogram/pull/532 [#533]: https://github.com/scikit-hep/boost-histogram/pull/533 [#537]: https://github.com/scikit-hep/boost-histogram/pull/537 ### Version 1.0.0 Dropped support for Python 2 and 3.5; removed large numbers of workarounds. Fully statically typed. API compatible with the final `0.x` release for most uses, except for subclassing; subclassing histogram components now uses Python 3 class keyword syntax to set families. #### User changes - Dropped Python 2.7 and 3.5 support [#512][] - Removed deprecated `.options` from axes. Use `.traits` instead. [#503][] - Full static typing available, UHI 0.1.2+ supported. [#516][], [#517][], [#519][], [#520][], [#521][], [#523][] #### Subclassing Histogram changes - Use keyword class family setting when subclassing histogram components instead of custom decorator. [#513][] - Structure of internal repr creation changed and made slightly more public. [#518][] #### Bug fixes - Consistently show `metadata=` in repr if present; refactored internal repr handling [#518][] - Minor typing related fixes for rare bugs (especially in `numpy.py`, [#521][]) [#503]: https://github.com/scikit-hep/boost-histogram/pull/503 [#512]: https://github.com/scikit-hep/boost-histogram/pull/512 [#513]: https://github.com/scikit-hep/boost-histogram/pull/513 [#516]: https://github.com/scikit-hep/boost-histogram/pull/516 [#517]: https://github.com/scikit-hep/boost-histogram/pull/517 [#518]: https://github.com/scikit-hep/boost-histogram/pull/518 [#519]: https://github.com/scikit-hep/boost-histogram/pull/519 [#520]: https://github.com/scikit-hep/boost-histogram/pull/520 [#521]: https://github.com/scikit-hep/boost-histogram/pull/521 [#523]: https://github.com/scikit-hep/boost-histogram/pull/523 ## Version 0.13 ### Version 0.13.2 - Backport fix scaling a weighted storage - Backport fix partial summation over a Categorical axis ### Version 0.13.1 - Backport fix for Mean/WeightedMean summing. - Backport fix for `boost_histogram.numpy` density. - Backport missing metadata from the repr's. - Ignore `family=` on Histogram subclassing to make subclassing Histogram only possible in 1.x + 0.x code. ### Version 0.13.0 PlottableProtocol provides a way to plot in different libraries, and easy access to common quantities. This is expected to be the final release for Python 2, and mostly equivalent in API to 1.0. #### User changes - Support for PlottableProtocol. You can now access `.values()`, `.counts()`, and `.variances()` on all storages; used by plotting libraries. `.kind` describes the Kind of the histogram (`bh.Kind.COUNT` or `bh.Kind.MEAN`). `.options` has been renamed to `.traits`, and a few more useful traits were added, like `.discrete`. Most other portions of the Protocol were already present. [#476][] - Removed deprecated `.rank` on histograms (since 0.8). Use `.ndim` instead. [#505][] - Supports converting user histogram objects that provide a `_to_boost_histogram_` method. [#483][] - A `view=True` parameter must now be passed to get a View instead of a standard NumPy values array from `to_numpy()`. [#498][] #### Bug fixes - Added additional support for typing, fixing a couple of rare Python 2 bugs in the process [#493][]. - The resulting histogram from `bh.numpy.*` functions is now reducible [#508][] #### Developer changes - Use GitHub Actions for ARM compiling [#474][] - Apple Silicon support (since 0.12) [#495][] - Support compiling with C++17 [#502][] - Rename `NPY_NUM_BUILD_JOBS` to `CMAKE_BUILD_PARALLEL_LEVEL` for consistency with other Scikit-HEP projects. [#502][] [#474]: https://github.com/scikit-hep/boost-histogram/pull/474 [#476]: https://github.com/scikit-hep/boost-histogram/pull/476 [#483]: https://github.com/scikit-hep/boost-histogram/pull/483 [#493]: https://github.com/scikit-hep/boost-histogram/pull/493 [#495]: https://github.com/scikit-hep/boost-histogram/pull/495 [#498]: https://github.com/scikit-hep/boost-histogram/pull/498 [#502]: https://github.com/scikit-hep/boost-histogram/pull/502 [#505]: https://github.com/scikit-hep/boost-histogram/pull/505 [#508]: https://github.com/scikit-hep/boost-histogram/pull/508 ## Version 0.12 ### Version 0.12.0 Pressing forward to 1.0. #### User changes - You can now set all complex storages, either on a Histogram or a View with an (N+1)D array [#475][] - Axes are now normal `__dict__` classes, you can manipulate the `__dict__` as normal. Axes construction now lets you either use the old metadata shortcut or the `__dict__` inline. [#477][] #### Bug fixes - Fixed slicing projection with one-sided slices [#479][] - Fixed issue if final bin of Variable histogram was infinite by updating to Boost 1.75 [#470][] - NumPy arrays can be used for weights in `bh.numpy` [#472][] - Vectorization for WeightedMean accumulators was broken [#475][] #### Developer changes - Bumped to pybind11 version [#470][] - Black formatting used in notebooks too [#470][] #### Upgrade warning If you are using `Axis.options`, please transition to `Axis.traits`. `traits` includes all the old options, along with some new traits, and matches the PlottableProtocol requirements. [#470]: https://github.com/scikit-hep/boost-histogram/pull/470 [#472]: https://github.com/scikit-hep/boost-histogram/pull/472 [#475]: https://github.com/scikit-hep/boost-histogram/pull/475 [#477]: https://github.com/scikit-hep/boost-histogram/pull/477 [#479]: https://github.com/scikit-hep/boost-histogram/pull/479 ## Version 0.11 ### Version 0.11.1 Updating pybind11 to 2.6.0. [#443][] Features: - Python 3.9 support - PyPy2 / PyPy3.6 / PyPy3.7 support - Warnings on latest AppleClang fixed - 40% faster accumulator fills, simpler implementation - Segfaults when passing an object with a throwing repr fixed - kwargs replaced older workarounds (partially at the moment) - Using new `py::type` instead of `pybind11::detail` usage - Enhanced CMake support, finds conda and venv now, uses `pybind11_find_import` - Using setuptools support from pybind11 (previously vendored, so benefits have been available since 0.11.0) Also cleans up SDists a bit. [#467][] [#443]: https://github.com/scikit-hep/boost-histogram/pull/443 [#467]: https://github.com/scikit-hep/boost-histogram/pull/467 ### Version 0.11.0 A release focused on preparing for the upcoming Hist 2.0 release. #### User changes - Arbitrary items can be set on an axis or histogram. [#450][], [#456][] - Subclasses can customize the conversion procedure. [#456][] #### Bug fixes - Fixed reading pickles from boost-histogram 0.6-0.8 [#445][] - Minor correctness fix [#446][] - Accidental install of typing on Python 3.5+ fixed - Scalar ND fill fixed [#453][] #### Developer changes - Updated to Boost 1.74 [#442][] - CMake installs version.py now too [#449][] - Updated setuptools infrastructure no longer requires NumPy [#451][] - Some basic clang-tidy checks are now being run [#455][] [#442]: https://github.com/scikit-hep/boost-histogram/pull/442 [#445]: https://github.com/scikit-hep/boost-histogram/pull/445 [#446]: https://github.com/scikit-hep/boost-histogram/pull/446 [#449]: https://github.com/scikit-hep/boost-histogram/pull/449 [#450]: https://github.com/scikit-hep/boost-histogram/pull/450 [#451]: https://github.com/scikit-hep/boost-histogram/pull/451 [#453]: https://github.com/scikit-hep/boost-histogram/pull/453 [#455]: https://github.com/scikit-hep/boost-histogram/pull/455 [#456]: https://github.com/scikit-hep/boost-histogram/pull/456 ## Version 0.10 ### Version 0.10.2 Quick fix for extra print statement in fill. #### Bug fixes - Fixed debugging print statement in fill. [#438][] #### Developer changes - Added CI/pre-commit check for print statements [#438][] - Formatting CMakeLists now too [#439][] [#438]: https://github.com/scikit-hep/boost-histogram/pull/438 [#439]: https://github.com/scikit-hep/boost-histogram/pull/439 ### Version 0.10.1 Several fixes were made, mostly related to Weight storage histograms from Uproot 4. #### Bug fixes - Reduction on `h.axes.widths` supported again [#428][] - WeightedSumView supports standard array operations [#432][] - Operations shallow copy (non-copyable metadata supported) [#433][] - Pandas Series as samples/weights supported [#434][] - Support NumPy scalars in operations [#436][] [#428]: https://github.com/scikit-hep/boost-histogram/pull/428 [#432]: https://github.com/scikit-hep/boost-histogram/pull/432 [#433]: https://github.com/scikit-hep/boost-histogram/pull/433 [#434]: https://github.com/scikit-hep/boost-histogram/pull/434 [#436]: https://github.com/scikit-hep/boost-histogram/pull/436 ### Version 0.10.0 This version was released during PyHEP 2020. Several improvements were made to usability when plotting and indexing. #### User changes - AxesTuple array now support operations via ArrayTuple [#414][] - Support `sum` and `bh.rebin` without slice [#424][] - Nicer error messages in some cases [#415][] - Made a few properties hidden for accumulators that were not public [#418][] - Boolean now supports reduction, faster compile [#422][] - AxesTuple now available publicly for subprojects [#419][] #### Bug fixes - Histograms support operations with arrays, no longer take the first element only [#417][] [#414]: https://github.com/scikit-hep/boost-histogram/pull/414 [#415]: https://github.com/scikit-hep/boost-histogram/pull/415 [#417]: https://github.com/scikit-hep/boost-histogram/pull/417 [#418]: https://github.com/scikit-hep/boost-histogram/pull/418 [#419]: https://github.com/scikit-hep/boost-histogram/pull/419 [#422]: https://github.com/scikit-hep/boost-histogram/pull/422 [#424]: https://github.com/scikit-hep/boost-histogram/pull/424 ## Version 0.9 ### Version 0.9.0 This version was released just before PyHEP 2020. Several important fixes were made, along with a few new features to better support downstream projects. #### User changes - `metadata` supported and propagated on Histograms (slots added) [#403][] - Added `dd=True` option in `to_numpy` [#406][] - Deprecated `cpp` module removed [#402][] #### Developer changes - Subclasses can override axes generation [#401][] - `[dev]` extra now installs `pytest` [#401][] #### Bug fixes - Fix `numpy.histogramdd` return structure [#406][] - Travis deploy multi-arch fixes [#399][] - Selecting on a bool axes supports 2D+ histograms [#398][] - Warnings fixed on NumPy 1.19+ [#404][] [#398]: https://github.com/scikit-hep/boost-histogram/pull/398 [#399]: https://github.com/scikit-hep/boost-histogram/pull/399 [#401]: https://github.com/scikit-hep/boost-histogram/pull/401 [#402]: https://github.com/scikit-hep/boost-histogram/pull/402 [#403]: https://github.com/scikit-hep/boost-histogram/pull/403 [#404]: https://github.com/scikit-hep/boost-histogram/pull/404 [#406]: https://github.com/scikit-hep/boost-histogram/pull/406 ## Version 0.8 ### Version 0.8.0 This version was released just before SciPy 2020 and Boost 1.74. Highlights include better accumulator views, simpler summing, better NumPy and Pandas compatibility, and sums on growing axes. Lots of backend work, including a new wheel building system, internal changes and better reliance on Boost.Histogram's C++ tools for actions like cropping. #### User changes - Weighted histogram cells can now be assigned directly from iterables [#375][] - Weighted views can be summed and added [#368][] - Sum is now identical to the built-in sum function [#365][] - Adding growing axis is better supported [#358][] - Slicing an AxesTuple now keeps the type [#384][] - `ndim` replaces `rank` for NumPy compatibility [#385][] - Any array-like supported in fill [#391][], any iterable can be used for Categories [#392][] - Added Boolean axes, from Boost.Histogram 1.74 [#390][] - Division between histograms is supported [#393][] - More deprecated functionality removed #### Bug fixes - Support older versions of [CloudPickle][] (issue also fixed upstream) [#343][] - Drop extra printout [#338][] - Throw an error instead of returning an incorrect result in more places [#386][] #### Developer changes - Update Boost to 1.73 [#359][], pybind11 to 2.5.0 [#351][], Boost.Histogram to pre-1.74 [#388][] - Cropping no longer uses workaround [#373][] - Many more checks added to [`pre-commit`][] [#366][] - Deprecating `cpp` interface [#391][] - Wheelbuilding migrated to [`cibuildwheel`][] and GHA [#361][] [CloudPickle]: https://github.com/cloudpipe/cloudpickle [`cibuildwheel`]: https://cibuildwheel.readthedocs.io/en/stable [`pre-commit`]: https://pre-commit.com [#338]: https://github.com/scikit-hep/boost-histogram/pull/338 [#340]: https://github.com/scikit-hep/boost-histogram/pull/340 [#343]: https://github.com/scikit-hep/boost-histogram/pull/343 [#351]: https://github.com/scikit-hep/boost-histogram/pull/351 [#358]: https://github.com/scikit-hep/boost-histogram/pull/358 [#359]: https://github.com/scikit-hep/boost-histogram/pull/359 [#361]: https://github.com/scikit-hep/boost-histogram/pull/361 [#365]: https://github.com/scikit-hep/boost-histogram/pull/365 [#366]: https://github.com/scikit-hep/boost-histogram/pull/366 [#368]: https://github.com/scikit-hep/boost-histogram/pull/368 [#373]: https://github.com/scikit-hep/boost-histogram/pull/373 [#375]: https://github.com/scikit-hep/boost-histogram/pull/375 [#384]: https://github.com/scikit-hep/boost-histogram/pull/384 [#385]: https://github.com/scikit-hep/boost-histogram/pull/385 [#386]: https://github.com/scikit-hep/boost-histogram/pull/386 [#388]: https://github.com/scikit-hep/boost-histogram/pull/388 [#390]: https://github.com/scikit-hep/boost-histogram/pull/390 [#391]: https://github.com/scikit-hep/boost-histogram/pull/391 [#392]: https://github.com/scikit-hep/boost-histogram/pull/392 [#393]: https://github.com/scikit-hep/boost-histogram/pull/393 ## Version 0.7 ### Version 0.7.0 This version removes deprecated functionality, and has several backend improvements. The most noticeable user-facing change is the multithreaded fill feature, which can enable significant speedups when you have a dataset that is much larger than the number of bins in your histogram and have free cores to use. Several small bugs have been fixed. #### User changes - Added `threads=` keyword to `.fill` and NumPy functions; 0 for automatic, default is 1 [#325][] - `.metadata` is now settable directly from the AxesTuple [#303][] - Deprecated items from 0.5.x now dropped [#301][] - `cpp` mode updates and fixes [#317][] #### Bug fixes - Dict indexing is now identical to positional indexing, fixes "picking" axes in dict [#320][] - Passing `samples=None` is now always allowed in `.fill` [#325][] #### Developer changes - Build system update, higher requirements for developers (only) [#314][] - Version is now obtained from `setuptools_scm`, no longer stored in repo - Removed `futures` requirement for Python 2 tests - Updated Boost.Histogram, cleaner code with fewer workarounds [#301]: https://github.com/scikit-hep/boost-histogram/pull/301 [#303]: https://github.com/scikit-hep/boost-histogram/pull/303 [#314]: https://github.com/scikit-hep/boost-histogram/pull/314 [#317]: https://github.com/scikit-hep/boost-histogram/pull/317 [#320]: https://github.com/scikit-hep/boost-histogram/pull/320 [#325]: https://github.com/scikit-hep/boost-histogram/pull/325 ## Version 0.6 ### Version 0.6.2 Common analysis tasks are now better supported. Much more complete documentation. Now using development branch of Boost.Histogram again. #### Bug fixes - Fix sum over category axes in indexing [#298][] - Allow single category item selection [#298][] - Allow slicing on axes without flow bins [#288][], [#300][] - Sum repr no longer throws error [#293][] #### Developer changes - Now using scikit-hep/azure-wheel-helpers via subtree [#292][] [#288]: https://github.com/scikit-hep/boost-histogram/pull/288 [#292]: https://github.com/scikit-hep/boost-histogram/pull/292 [#293]: https://github.com/scikit-hep/boost-histogram/pull/293 [#298]: https://github.com/scikit-hep/boost-histogram/pull/298 [#300]: https://github.com/scikit-hep/boost-histogram/pull/300 ### Version 0.6.1 Examples and notebooks are now up to date with the current state of the library. Using Boost 1.72 release. #### User changes - Slices and single values can be mixed in indexing [#279][] - UHI locators supported on axes [#280][] #### Bug fixes - Properties on accumulator views now resolve correctly [#273][] - Division of a histogram by a number is supported again [#278][] - Setting a histogram with length one slice fixed [#279][] - NumPy functions now work with NumPy ints in `bins=` [#282][] - In-place addition avoids a copy [#284][] [#273]: https://github.com/scikit-hep/boost-histogram/pull/273 [#278]: https://github.com/scikit-hep/boost-histogram/pull/278 [#279]: https://github.com/scikit-hep/boost-histogram/pull/279 [#280]: https://github.com/scikit-hep/boost-histogram/pull/280 [#282]: https://github.com/scikit-hep/boost-histogram/pull/282 [#284]: https://github.com/scikit-hep/boost-histogram/pull/284 ### Version 0.6.0 This version fills out most of the remaining features missing from the 0.5.x series. You can now use all the storages without the original caveats; even the accumulators can be accessed array-at-a-time without copy, pickled quickly, and set array-at-a-time, as well. The API has changed considerably, providing a more consistent experience in Python. Most of the classic API still works in this release, but will issue a warning and will be removed from the next release. Please use this release to transition existing 0.5.x code to the new API. #### User changes - Histogram and Axis classes now follow PEP 8 naming scheme (`histogram`->`Histogram`, `regular`->`Regular`, `int`->`Int64` etc.) [#192][], [#255][] - You can now view a histogram with accumulators, with property access such as `h.view().value` [#194][] - Circular variable and integer axes added [#231][] - Split Category into `StrCategory` and `IntCategory`, now allows empty categories when `growth=True` [#221][] - `StrCategory` fills are safer and faster [#239][], [#244][] - Added axes transforms [#192][] - `Function(forward, inverse)` transform added, allowing ultra-fast C function pointer transforms [#231][] - You can now set histogram contents directly [#250][] - You can now sum over a range with endpoints [#185][] - `h.axes` now has the functions from axis as well. [#183][] - `bh.project` has become `bh.sum` [#185][] - `.reduce(...)` and the reducers in `bh.algorithm` have been removed in favor of dictionary based UHI slicing [#259][] - `bh.numpy` module interface updates, `histogram=bh.Histogram` replaces cryptic `bh=True`, and `density=True` is now supported in NumPy mode [#256][] - Added `hist.copy()` [#218][] and `hist.shape` [#264][] - Signatures are much nicer in Python 3 [#188][] - Reprs are better, various properties like `__module__` are now set correctly [#200][] #### Bug fixes - Unlimited and AtomicInt storages now allow single item access [#194][] - `.view()` now no longer makes a copy [#194][] - Fixes related to string category axis fills [#233][], [#230][] - Axes are no longer copies, support setting metadata [#238][], [#246][] - Pickling accumulator storages is now comparable in performance simple storages [#258][] #### Developer changes - The linux wheels are now 10-20x smaller [#229][] - The hist/axis classes are now pure Python, with a C++ object inside [#183][] - Most internal names changed, `core->_core`, etc. [#183][] - The `uhi` module is now `tag`. [#183][] - `boost_histogram.cpp as bh` provides C++ high-compatibility mode. [#183][] - Indexing tags now use full UHI instead of workarounds [#185][] - Removed log and sqrt special axes types[#231][] - Family and registration added, new casting system [#200][] [#183]: https://github.com/scikit-hep/boost-histogram/pull/183 [#185]: https://github.com/scikit-hep/boost-histogram/pull/185 [#188]: https://github.com/scikit-hep/boost-histogram/pull/188 [#192]: https://github.com/scikit-hep/boost-histogram/pull/192 [#194]: https://github.com/scikit-hep/boost-histogram/pull/194 [#200]: https://github.com/scikit-hep/boost-histogram/pull/200 [#218]: https://github.com/scikit-hep/boost-histogram/pull/218 [#221]: https://github.com/scikit-hep/boost-histogram/pull/221 [#229]: https://github.com/scikit-hep/boost-histogram/pull/229 [#230]: https://github.com/scikit-hep/boost-histogram/pull/230 [#231]: https://github.com/scikit-hep/boost-histogram/pull/231 [#233]: https://github.com/scikit-hep/boost-histogram/pull/233 [#238]: https://github.com/scikit-hep/boost-histogram/pull/238 [#239]: https://github.com/scikit-hep/boost-histogram/pull/239 [#244]: https://github.com/scikit-hep/boost-histogram/pull/244 [#246]: https://github.com/scikit-hep/boost-histogram/pull/246 [#250]: https://github.com/scikit-hep/boost-histogram/pull/250 [#255]: https://github.com/scikit-hep/boost-histogram/pull/255 [#256]: https://github.com/scikit-hep/boost-histogram/pull/256 [#258]: https://github.com/scikit-hep/boost-histogram/pull/258 [#259]: https://github.com/scikit-hep/boost-histogram/pull/259 [#264]: https://github.com/scikit-hep/boost-histogram/pull/264 ## Version 0.5 ### Version 0.5.2 #### User changes: - `bh.loc` supports an offset [#164][] - Nicer reprs in several places [#167][] - Deprecate `.at` and `.axis` [#170][] #### Bug fixes: - Use relative paths in setup.py to avoid resolving WSL paths on Windows [#162][], [#163][] - Better pybind11 support for Python 3.8 [#168][] #### Developer changes: - Serialization code shared with Boost.Histogram [#166][] - Avoid unused PEP 517 isolation for now [#171][] (may return with proper PEP 518 support eventually) [#162]: https://github.com/scikit-hep/boost-histogram/pull/162 [#163]: https://github.com/scikit-hep/boost-histogram/pull/163 [#164]: https://github.com/scikit-hep/boost-histogram/pull/164 [#166]: https://github.com/scikit-hep/boost-histogram/pull/166 [#167]: https://github.com/scikit-hep/boost-histogram/pull/167 [#168]: https://github.com/scikit-hep/boost-histogram/pull/168 [#170]: https://github.com/scikit-hep/boost-histogram/pull/170 [#171]: https://github.com/scikit-hep/boost-histogram/pull/171 ### Version 0.5.1 #### User changes: - Removed the `bh.indexed`/`h.indexed` iterator [#150][] - Added `.axes` AxisTuple, with direct access to properties [#150][] - Cleaned up tab completion in IPython [#150][] #### Bug fixes: - Fixed a bug in the sdist missing Boost.Variant2 [#154][] - Fixed filling on strided inputs [#158][] [#150]: https://github.com/scikit-hep/boost-histogram/pull/150 [#154]: https://github.com/scikit-hep/boost-histogram/pull/154 [#158]: https://github.com/scikit-hep/boost-histogram/pull/158 [#159]: https://github.com/scikit-hep/boost-histogram/pull/159 ### Version 0.5.0 First beta release and beginning of the changelog. #### Known issues: - Unlimited storage does not support pickling or classic multiprocessing - Some non-simple storages do not support some forms of access, like `.view` - Indexing and the array versions (such as centers) are incomplete and subject to change - The numpy module is provisional and subject to change - Docstrings and signatures will improve in later versions (especially on Python 3) scikit-hep-boost-histogram-b4c49b0/docs/conf.py000066400000000000000000000105321524660700500215370ustar00rootroot00000000000000# To regenerate API docs: # nox -s build_api_docs from __future__ import annotations # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import contextlib import importlib.metadata import shutil import sys from pathlib import Path # Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # http://www.sphinx-doc.org/en/master/config # -- Path setup -------------------------------------------------------------- DIR = Path(__file__).parent BASEDIR = DIR.parent sys.path.append(str(BASEDIR / "src")) # -- Project information ----------------------------------------------------- project = "boost_histogram" copyright = "2020-2026, Henry Schreiner, Hans Dembinski" author = "Henry Schreiner, Hans Dembinski" with contextlib.suppress(ModuleNotFoundError): version = importlib.metadata.version("boost_histogram") # passed if no version (latest/git hash) # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "myst_parser", "nbsphinx", "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.mathjax", "sphinx.ext.napoleon", "sphinx_copybutton", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = [ "_build", "**.ipynb_checkpoints", "Thumbs.db", ".DS_Store", ".env", "api/boost_histogram.*.rst", ] # Read the Docs needs this explicitly listed. master_doc = "index" # Intersphinx setup intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "numpy": ("https://numpy.org/doc/stable/", None), } # -- Options for Notebook input ---------------------------------------------- html_logo = "_images/BoostHistogramPythonLogo.png" html_title = "boost-histogram docs" nbsphinx_execute = "never" # Can change to auto highlight_language = "python3" nbsphinx_execute_arguments = [ "--InlineBackend.figure_formats={'png2x'}", "--InlineBackend.rc={'figure.dpi': 96}", ] # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "sphinx_book_theme" # Config for the Sphinx book html_baseurl = "https://boost-histogram.readthedocs.io/en/latest/" html_theme_options = { "home_page_in_toc": True, "repository_url": "https://github.com/scikit-hep/boost-histogram", "use_repository_button": True, "use_issues_button": True, "use_edit_page_button": True, } # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = [] # _static # Simpler docs (no build required) # boost_histogram.version is generated at build time and does not exist # when Sphinx imports boost_histogram from src/ directly. autodoc_mock_imports = ["boost_histogram._core", "boost_histogram.version"] def prepare(app): outer_nb = BASEDIR / "notebooks" inner_nb = DIR / "notebooks" notebooks = outer_nb.glob("*.ipynb") for notebook in notebooks: shutil.copy(notebook, inner_nb / notebook.name) outer_cont = BASEDIR / ".github" inner_cont = DIR contributing = "CONTRIBUTING.md" shutil.copy(outer_cont / contributing, inner_cont / "contributing.md") def clean_up(app, exception): inner_nb = DIR / "notebooks" for notebook in inner_nb.glob("*.ipynb"): notebook.unlink() inner_cont = DIR (inner_cont / "contributing.md").unlink() def setup(app): # Copy the notebooks in app.connect("builder-inited", prepare) # Clean up the generated notebooks app.connect("build-finished", clean_up) scikit-hep-boost-histogram-b4c49b0/docs/index.rst000066400000000000000000000102351524660700500221010ustar00rootroot00000000000000.. boost-histogram documentation master file, created by sphinx-quickstart on Tue Apr 23 12:12:27 2019. You can adapt this file completely to your liking, but it should at least contain the root ``toctree`` directive. .. image:: _images/BoostHistogramPythonLogo.png :width: 70% :alt: Boost histogram logo :align: center Welcome to boost-histogram's documentation! =========================================== |Actions Status| |Documentation Status| |PyPI version| |Conda-Forge| |PyPI platforms| |DOI| |GitHub Discussion| |Scikit-HEP| Boost-histogram (`source `__) is a Python package providing Python bindings for Boost.Histogram_ (`source `__). You can install this library from `PyPI `__ with pip or you can use Conda via `conda-forge `__: .. code-block:: bash python -m pip install boost-histogram .. code-block:: bash conda install -c conda-forge boost-histogram All the normal best-practices for Python apply; you should be in a virtual environment, etc. See :ref:`usage-installation` for more details. An example of usage: .. code-block:: python3 import boost_histogram as bh # Compose axis however you like; this is a 2D histogram hist = bh.Histogram(bh.axis.Regular(2, 0, 1), bh.axis.Regular(4, 0.0, 1.0)) # Filling can be done with arrays, one per dimension hist.fill([0.3, 0.5, 0.2], [0.1, 0.4, 0.9]) # NumPy array view into histogram counts, no overflow bins counts = hist.view() See :ref:`usage-quickstart` for more. .. toctree:: :maxdepth: 1 :caption: User Guide: user-guide/installation user-guide/quickstart user-guide/histogram user-guide/axes user-guide/storage user-guide/accumulators user-guide/transforms user-guide/indexing user-guide/plotting user-guide/analyses user-guide/numpy user-guide/subclassing user-guide/comparison .. toctree:: :maxdepth: 1 :caption: Examples: notebooks/SimpleExample notebooks/MultiCellExample notebooks/ThreadedFills notebooks/PerformanceComparison notebooks/xarray notebooks/BoostHistogramHandsOn .. toctree:: :maxdepth: 2 :titlesonly: :caption: Developers :glob: contributing support changelog .. toctree:: :caption: API Reference: api/modules Acknowledgements ---------------- This library was primarily developed by Henry Schreiner and Hans Dembinski. 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. Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` .. _Boost.Histogram: https://www.boost.org/doc/libs/release/libs/histogram/doc/html/index.html .. |Actions Status| image:: https://github.com/scikit-hep/boost-histogram/workflows/Tests/badge.svg :target: https://github.com/scikit-hep/boost-histogram/actions .. |Documentation Status| image:: https://readthedocs.org/projects/boost-histogram/badge/?version=latest :target: https://boost-histogram.readthedocs.io/en/latest/?badge=latest .. |PyPI version| image:: https://badge.fury.io/py/boost-histogram.svg :target: https://pypi.org/project/boost-histogram .. |Conda-Forge| image:: https://img.shields.io/conda/vn/conda-forge/boost-histogram :target: https://github.com/conda-forge/boost-histogram-feedstock .. |PyPI platforms| image:: https://img.shields.io/pypi/pyversions/boost-histogram :target: https://pypi.org/project/boost-histogram/ .. |DOI| image:: https://zenodo.org/badge/148885351.svg :target: https://zenodo.org/badge/latestdoi/148885351 .. |GitHub Discussion| image:: https://img.shields.io/static/v1?label=Discussions&message=Ask&color=blue&logo=github :target: https://github.com/scikit-hep/boost-histogram/discussions .. |Scikit-HEP| image:: https://scikit-hep.org/assets/images/Scikit--HEP-Project-blue.svg :target: https://scikit-hep.org/ scikit-hep-boost-histogram-b4c49b0/docs/make.bat000066400000000000000000000013601524660700500216440ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% :end popd scikit-hep-boost-histogram-b4c49b0/docs/notebooks/000077500000000000000000000000001524660700500222425ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/docs/notebooks/conf.sh000066400000000000000000000001301524660700500235150ustar00rootroot00000000000000--InlineBackend.figure_formats="{'svg', 'pdf'}" --InlineBackend.rc="{'figure.dpi': 96}" scikit-hep-boost-histogram-b4c49b0/docs/support.rst000066400000000000000000000014321524660700500225050ustar00rootroot00000000000000Support ================ If you are stuck with a problem using Boost-histogram, please do get in touch at our `Issues `_ or `GitHub Discussions `_. The developers are willing to help. You can save time by following this procedure when reporting a problem: * Do try to solve the problem on your own first. Read the documentation, including using the search feature, index and reference documentation. * Search the issue archives to see if someone else already had the same problem. * Before writing, try to create a minimal example that reproduces the problem. You'll get the fastest response if you can send just a handful of lines of code that show what isn't working. scikit-hep-boost-histogram-b4c49b0/docs/template/000077500000000000000000000000001524660700500220525ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/docs/template/module.rst_t000066400000000000000000000003031524660700500244100ustar00rootroot00000000000000{%- if show_headings %} {{- [basename, "module"] | join(' ') | e | heading }} {% endif -%} .. automodule:: {{ qualname }} {%- for option in automodule_options %} :{{ option }}: {%- endfor %} scikit-hep-boost-histogram-b4c49b0/docs/template/package.rst_t000066400000000000000000000017251524660700500245270ustar00rootroot00000000000000{%- macro automodule(modname, options) -%} .. automodule:: {{ modname }} {%- for option in options %} :{{ option }}: {%- endfor %} {%- endmacro %} {%- macro toctree(docnames) -%} {% for docname in docnames %} .. include:: {{ docname }}.rst {%- endfor %} {%- endmacro %} {%- if is_namespace %} {{- [pkgname, "namespace"] | join(" ") | e | heading }} {% else %} {{- pkgname | e | heading }} {% endif %} {%- if modulefirst and not is_namespace %} {{ automodule(pkgname, automodule_options) }} {% endif %} {%- if subpackages %} {{ toctree(subpackages) }} {% endif %} {%- if submodules %} {% if separatemodules %} {{ toctree(submodules) }} {%- else %} {%- for submodule in submodules %} {% if show_headings %} {{- submodule | e | heading }} {% endif %} {{ automodule(submodule, automodule_options) }} {% endfor %} {%- endif %} {% endif %} {%- if not modulefirst and not is_namespace %} Module contents --------------- {{ automodule(pkgname, automodule_options) }} {% endif %} scikit-hep-boost-histogram-b4c49b0/docs/template/toc.rst_t000066400000000000000000000001501524660700500237100ustar00rootroot00000000000000 .. toctree:: :maxdepth: {{ maxdepth }} {% for docname in docnames %} {{ docname }} {%- endfor %} scikit-hep-boost-histogram-b4c49b0/docs/user-guide/000077500000000000000000000000001524660700500223105ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/docs/user-guide/accumulators.rst000066400000000000000000000150001524660700500255400ustar00rootroot00000000000000.. _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 showing how non-accurate sums fail to produce the obvious answer, 2.0:: import math import numpy as np import boost_histogram as bh 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"{bh.accumulators.Sum().fill(values) = }") .. code-block:: text sum(values) = 0.0 (simple) math.fsum(values) = 2.0 np.sum(values) = 0.0 (pairwise) bh.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"{bh.accumulators.Sum().fill(values) = }") .. code-block:: text math.fsum(values) = 2.0 bh.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 boost_histogram as bh values = [10]*10 smooth = bh.accumulators.WeightedSum().fill(values) print(f"{smooth = }") values = [1]*9 + [91] rough = bh.accumulators.WeightedSum().fill(values) print(f"{rough = }") .. code-block:: 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 boost_histogram as bh values = [10]*10 smooth = bh.accumulators.Mean().fill(values) print(f"{smooth = }") values = [1]*9 + [91] rough = bh.accumulators.Mean().fill(values) print(f"{rough = }") .. code-block:: 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 boost_histogram as bh values = [1]*9 + [91] wm = bh.accumulators.WeightedMean().fill(values, weight=2) print(f"{wm = }") .. code-block:: 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. You can set a view's contents with a stacked array, and each item in the stack will be used for the (computed) values that a normal constructor would take. For example, WeighedMean can take an array with a final dimension four long, with ``sum_of_weights``, ``sum_of_weights_squared``, ``value``, and ``variance`` elements, even though several of these values are computed from the internal representation. The ``WeightedSumView``, ``MeanView``, and ``WeightedMeanView`` support arithmetic that respects the accumulator semantics. ``WeightedSumView`` adds and subtracts values while always adding the variances. ``MeanView`` and ``WeightedMeanView`` combine two views by merging the underlying samples (the same parallel merge used by the C++ ``operator+=``), and support scaling by a scalar; subtraction and scalar addition are undefined for means. .. note:: Like all NumPy structured arrays, **fancy indexing (boolean masks, integer arrays) returns a copy**, not a view. Assigning to a masked result will not modify the original histogram data. Use the mask directly in the assignment instead: .. code:: python v = h.view() # This does NOT work — masked selection returns a copy: v[v.value == 2].value[:] = 3 # This DOES work — assign through the mask in one step: v.value[v.value == 2] = 3 Slice indexing (``v[1:3]``) does return a true view. scikit-hep-boost-histogram-b4c49b0/docs/user-guide/analyses.rst000066400000000000000000000024531524660700500246650ustar00rootroot00000000000000.. _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 boost-histogram, you can make a single histogram, and use an axis for each: .. code-block:: python3 value_ax = bh.axis.Regular(100, -5, 5) bool_ax = bh.axis.Integer(0, 2, underflow=False, overflow=False) run_number_ax = bh.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-block:: python3 h1 = hist[:, True, sum] You can expand this example to any number of dimensions, boolean flags, and categories. scikit-hep-boost-histogram-b4c49b0/docs/user-guide/axes.rst000066400000000000000000000113021524660700500237770ustar00rootroot00000000000000.. _usage-axes: Axes ==== In boost-histogram, a histogram is collection of Axis objects and a storage. 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:: bh.axis.Regular(bins, start, stop, *, 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:: bh.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:: bh.axis.Regular(..., transform=bh.axis.transform.sqrt) :noindex: This is an axis with bins transformed by a sqrt. .. py:function:: bh.axis.Regular(..., transform=bh.axis.transform.log) :noindex: Transformed by log. .. py:function:: bh.axis.Regular(..., transform=bh.axis.transform.Pow(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:: bh.axis.Variable([edge1, ...], *, 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:: bh.axis.Integer(start, stop, *, 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-block:: python3 bool_axis = bh.axis.Integer(0, 2, underflow=False, overflow=False) Another could be for an IntEnum if the values are contiguous. Category axis ------------- .. image:: ../_images/axis_category.png :alt: Regular axis illustration :align: center .. py:function:: bh.axis.IntCategory([value1, ...], *, metadata="", growth=False, overflow=True) :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-block:: python3 import enum class MyEnum(enum.IntEnum): a = 1 b = 5 my_enum_axis = bh.axis.IntCategory(list(MyEnum)) .. py:function:: bh.axis.StrCategory([str1, ...], metadata="", growth=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. Boolean axis ^^^^^^^^^^^^ .. py:function:: bh.axis.Boolean(metadata="") :noindex: This is an axis that holds either ``True`` and ``False`` only. 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. scikit-hep-boost-histogram-b4c49b0/docs/user-guide/comparison.rst000066400000000000000000000054351524660700500252230ustar00rootroot00000000000000.. _usage-comparison: Comparison with Boost.Histogram =============================== ``boost-histogram`` was based on the C++ library Boost.Histogram. In most ways, it mimics the spirit of this library; if you learn to use one, you probably can use the other. There are a few differences, however, mostly around adhering to Python conventions: Removals ^^^^^^^^ There are a few parts of the Boost.Histogram interface that are not bound. They are: The call operator This is provided in C++ to allow single item filling, and was designed to mimic the accumulator syntax used elsewhere in Boost. It also works nicely with some STL algorithms. It was not provided in Python because using call to modify an object is not common in Python, using call makes duck-typing more dangerous, and single-item fills are not encouraged in Python due to poor performance. The ``.fill`` method from Boost.Histogram 1.72 is bound, however - this provides fast fills without the drawbacks. If you want to fill with a single item, Python's ``.fill`` does support single item fills. Histogram make functions These functions, such as ``make_histogram`` and ``make_weighted_histogram``, are provided in Boost.Histogram to make the template syntax easier in C++14. In C++17, they are replaced by directly using the ``histogram`` constructor; the Python bindings are not limited by old templating syntax, and choose to only provide the newer spelling. Custom components Many components in Boost.Histogram are configurable or replaceable at compile time; since Python code is precompiled, a comprehensive but static subset was selected for the Python bindings. Changes ^^^^^^^ Naming The bindings follow modern Python conventions, with CamelCase for classes, etc. The Boost.Histogram library follows Boost conventions. Serialization The Python bindings use a pickle-based binary serialization, so cannot read files saved in C++ using Boost.Serialize. Properties Many methods in C++ are properties in Python. ``.axis(i)`` is replaced with ``.axes[i]``. Indexing The Python bindings use standard Python indexing for selection and setting. You can recover the functionality of ``.at(i)`` at endpoints with ``bh.tag.at(i)``. Renames The ``.rank()`` method is replaced by the ``.ndim`` property to match the common NumPy spelling. Additions ^^^^^^^^^ Unified Histogram Indexing The Python bindings support UHI, a proposal to unify and simplify histogram indexing in Python. Custom transforms Custom transforms are possible using Numba or a C pointer. In Boost.Histogram, you can use templating to make arbitrary transforms, so a run time transform is not as necessary (but may be added). NumPy compatibility The Python bindings do several things to simplify NumPy compatibility. scikit-hep-boost-histogram-b4c49b0/docs/user-guide/histogram.rst000066400000000000000000000155341524660700500250470ustar00rootroot00000000000000.. _usage-histogram: Histogram ========= The Histogram object is the core of boost-histogram. Filling ^^^^^^^ You call ``.fill`` to fill. You must have one 1D array (or scalar value) per dimension. For maximum performance, numeric arrays should be contiguously laid out in memory, and either 64-bit floats or ints. If any other layouts or numeric datatypes are supplied, a temporary copy will be made internally before filling. All storages support a ``weight=`` parameter, and some storages support a ``sample=`` parameter. If supplied, they must be a scalar (applies to all items equally) or an iterable of scalars/1D arrays that matches the number of dimensions of the histogram. The summing accumulators (not ``Mean()`` and ``WeightedMean()``) support threaded filling. Pass ``threads=N`` to the fill parameter to fill with ``N`` threads (and using 0 will select the number of virtual cores on your system). This is helpful only if you have a large number of entries compared to your number of bins, as all non-atomic storages will make copies for each thread, and then will recombine after the fill is complete. Data ^^^^ The primary values from a histogram are always available as ``.values()``. The variances are available as ``.variances()``, unless you fill an unweighed histogram with weights, which will cause this to return None, since the variances are no longer computable (use a weighted storage instead if you need the variances). The counts are available as ``.counts()``. If the histogram is weighted, ``.counts()`` returns the effective counts; see `UHI `_ for details. Views ^^^^^ While Histograms do conform to the Python buffer protocol, the best way to get access to the raw contents of a histogram as a NumPy array is with ``.view()``. This way you can optionally pass ``flow=True`` to get the flow bins, and if you have an accumulator storage, you will get a View, which is a slightly augmented ndarrray subclass (see :ref:`usage-accumulators`). Views support setting as well for non-computed properties; you can use an expression like this to set the values of an accumulator storage: .. code-block:: python3 h.view().value = values A view shares memory with the histogram. If the histogram has a growth axis, a fill that grows an axis moves the data, and the view then points at freed memory. Take a copy of the view before such a fill. You can also used stacked arrays (N+1 dimensional) to set a histogram's contents. This is especially useful if you need to set a computed value, like variance on a Mean/WeightedMean storage, which cannot be set using the above method: .. code-block:: python3 h[...] = np.stack([values, variances], axis=-1) If you leave endpoints off (such as with ``...`` above), then you can match the size with or without flow bins. Operations ^^^^^^^^^^ * ``h.ndim``: The number of dimensions * ``h.size``: The number of bins * ``+``: Add two histograms, or add a scalar or array (storages must match types currently) * ``*=``: Multiply by a scalar, array, or histogram (not all storages) (``hist * scalar`` and ``scalar * hist`` supported too) * ``/=``: Divide by a scalar, array, or histogram (not all storages) (``hist / scalar`` supported too) * ``[...]``: Access a bin or a range of bins (get or set) (see :ref:`usage-indexing`) * ``.sum(flow=False)``: The total count of all bins * ``.project(ax1, ax2, ..., flow=True)``: Project down to listed axis (numbers); pass ``flow=False`` to drop the flow bins of the removed axes instead of summing them in * ``.to_numpy(flow=False, view=False)``: Convert to a NumPy style tuple (with or without under/overflow bins, and either return values (the default) or the entire view for accumulator storages.) * ``.view(flow=False)``: Get a view on the bin contents (with or without under/overflow bins) * ``.values(flow=False)``: Get a view on the values (counts or means, depending on storage) * ``.variances(flow=False)``: Get the variances if available * ``.counts(flow=False)``: Get the effective counts for all storage types * ``.reset()``: Set counters to 0 * ``.empty(flow=False)``: Check to see if the histogram is empty (can check flow bins too if asked) * ``.copy(deep=False)``: Make a copy of a histogram * ``.axes``: Get the axes as a tuple-like (all properties of axes are available too) * ``.axes[0]``: Get the 0th axis * ``.axes.edges``: The lower values as a broadcasting-ready array * ``.axes.centers``: The centers of the bins broadcasting-ready array * ``.axes.widths``: The bin widths as a broadcasting-ready array * ``.axes.metadata``: A tuple of the axes metadata * ``.axes.traits``: A tuple of the axes traits * ``.axes.size``: A tuple of the axes sizes (size without flow) * ``.axes.extent``: A tuple of the axes extents (size with flow) * ``.axes.bin(*args)``: Returns the bin edges as a tuple of pairs (continuous axis) or values (discrete axis) * ``.axes.index(*args)``: Returns the bin index at a value for each axis * ``.axes.value(*args)``: Returns the bin value at an index for each axis Saving a Histogram ^^^^^^^^^^^^^^^^^^ You can save a histogram using pickle: .. code-block:: 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 used to be default on Python 2. The most recent versions provide performance benefits. You can nest this in other Python structures, like dictionaries, and save those instead. UHI dictionary serialization ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ You can also convert a histogram to and from a plain dictionary following the `UHI serialization schema `_. This is useful for sharing histograms as JSON (or any other format that supports nested dictionaries and arrays): .. code-block:: python3 from boost_histogram.serialization import to_uhi, from_uhi data = to_uhi(h) h2 = from_uhi(data) assert h == h2 If you only want to share the *structure* of a histogram -- its axes, metadata, and storage type -- without the (potentially large) bin contents, pass ``keep_storage=False``. The resulting storage entry contains only its ``type``: .. code-block:: python3 data = to_uhi(h, keep_storage=False) assert data["storage"] == {"type": "double"} # Loading restores an empty histogram with the same axes and storage type h_empty = from_uhi(data) This is handy for transmitting just the "initialization" of a histogram (for example, to set up a matching histogram on a server) without serializing all of the zeros. ``from_uhi`` accepts both full and metadata-only dictionaries. scikit-hep-boost-histogram-b4c49b0/docs/user-guide/indexing.rst000066400000000000000000000046441524660700500246570ustar00rootroot00000000000000.. _usage-indexing: Indexing ======== Boost-histogram implements the UHI indexing protocol. You can read more about it on the `UHI Indexing `_ page. Boost-histogram specific details -------------------------------- Boost-histogram implements ``bh.loc``, ``builtins.sum``, ``bh.rebin``, ``bh.underflow``, and ``bh.overflow`` from the UHI spec. A ``bh.tag.at`` locator is provided as well, which simulates the Boost.Histogram C++ ``.at()`` indexing using the UHI locator protocol. Boost-histogram allows "picking" using lists, similar to NumPy. If you select with multiple lists, boost-histogram instead selects per-axis, rather than group-selecting and reducing to a single axis, like NumPy does. You can use ``bh.loc(...)`` inside these lists. Example:: h = bh.Histogram( bh.axis.Regular(10, 0, 1), bh.axis.StrCategory(["a", "b", "c"]), bh.axis.IntCategory([5, 6, 7]), ) minihist = h[:, [bh.loc("a"), bh.loc("c")], [0, 2]] # Produces a 3D histogram with Regular(10, 0, 1) x StrCategory(["a", "c"]) x IntCategory([5, 7]) This feature is considered experimental. Removed bins are not added to the overflow bin currently. Vectorized indexing ------------------- While *lists* select per-axis (as described above), *NumPy integer arrays* are treated as vectorized fancy indexing, following NumPy's broadcasting rules. This gathers (or scatters, when assigning) many individual cells in a single operation, instead of building a new histogram. It is much faster than looping over scalar indices, which matters for high-dimensional histograms with many categories. Example:: import numpy as np h = bh.Histogram( bh.axis.IntCategory(range(100)), bh.axis.IntCategory(range(100)), bh.axis.Regular(50, 0, 1), ) datasets = np.array([3, 7, 42]) categories = np.array([1, 1, 9]) # Gather the Regular-axis contents for three (dataset, category) pairs at once values = h[datasets, categories, :] # shape (3, 50) # Assignment works the same way h[datasets, categories, :] = 0 The array indices use the same (non-flow) numbering as scalar indexing, and you can mix them with integers, ``bh.loc(...)``, and plain integer slices. This path covers the common case directly; for anything more advanced (rebinning, ``sum``-projection, or locator-based slices alongside arrays) index ``.view()`` explicitly instead. scikit-hep-boost-histogram-b4c49b0/docs/user-guide/installation.rst000066400000000000000000000041411524660700500255430ustar00rootroot00000000000000.. _usage-installation: Installation ============ Boost-histogram (`source `__) is a Python package providing Python bindings for Boost.Histogram_ (`source `__). .. _Boost.Histogram: https://www.boost.org/doc/libs/release/libs/histogram/doc/html/index.html You can install this library from `PyPI `__ with pip: .. code-block:: bash python -m pip install boost-histogram or you can use Conda through `conda-forge `__: .. code-block:: bash conda install -c conda-forge boost-histogram All the normal best-practices for Python apply; you should be in a virtual environment, etc. If you built from source, you can quickly verify the build with: .. code-block:: bash python -m boost_histogram.test Or, from Python: .. code-block:: python import unittest assert unittest.main("boost_histogram.test", warnings="error", exit=False).result == 0 Supported platforms ------------------- Binaries available: ^^^^^^^^^^^^^^^^^^^ The supported platforms are listed in the README - All common linux machines, all common macOS versions, and all common Windows versions. Conda-Forge ^^^^^^^^^^^ The boost-histogram package is available on Conda-Forge, as well. All supported versions are available. :: conda install -c conda-forge boost-histogram Source builds ^^^^^^^^^^^^^ For a source build, for example from an “sdistâ€� package, the only requirements are a C++14 compatible compiler. The compiler requirements are dictated by Boost.Histogram’s C++ requirements: gcc >= 5.5, clang >= 3.8, msvc >= 14.1. A reasonably recent version of pip is recommended. NumPy is downloaded during the build (enables multithreaded builds). Boost is not required or needed (this only depends on included header-only dependencies).This library is under active development; you can install directly from GitHub if you would like. .. code-block:: bash python -m pip install git+https://github.com/scikit-hep/boost-histogram.git@develop scikit-hep-boost-histogram-b4c49b0/docs/user-guide/numpy.rst000066400000000000000000000075271524660700500242250ustar00rootroot00000000000000.. _usage-numpy: NumPy compatibility =================== Histogram conversion -------------------- Accessing the storage array ^^^^^^^^^^^^^^^^^^^^^^^^^^^ You can access the storage of any Histogram using ``.view()``, see :ref:`usage-histogram`. NumPy tuple output ^^^^^^^^^^^^^^^^^^ You can directly convert a histogram into the tuple of outputs that ``np.histogram*`` would give you using ``.to_numpy()`` or ``.to_numpy(flow=True)`` on any histogram. This returns ``edges[0], edges[1], ..., values``, and the edges are NumPy-style (upper edge inclusive). NumPy adaptors -------------- You can use boost-histogram as a drop in replacement for NumPy histograms. All three histogram functions (``bh.numpy.histogram``, ``bh.numpy.histogram2d``, and ``bh.numpy.histogramdd``) are provided. The syntax is identical, though boost-histogram adds three new keyword-only arguments; ``storage=`` to select the storage, ``histogram=bh.Histogram`` to produce a boost-histogram instead of a tuple, and ``threads=N`` to select a number of threads to fill with. 1D histogram example ^^^^^^^^^^^^^^^^^^^^ If you try the following in an IPython session, you will get: .. code-block:: python3 import numpy as np import boost_histogram as bh norm_vals = np.concatenate( [ np.random.normal(loc=5, scale=1, size=1_000_000), np.random.normal(loc=2, scale=0.2, size=200_000), np.random.normal(loc=8, scale=0.2, size=200_000), ] ) .. code-block:: ipython %%timeit bins, edges = np.histogram(norm_vals, bins=100, range=(0, 10)) .. code-block:: text 17.4 ms ± 2.64 ms per loop (mean ± std. dev. of 7 runs, 100 loops each) Of course, you then are either left on your own to compute centers, density, widths, and more, or in some cases you can change the computation call itself to add ``density=``, or use the matching function inside Matplotlib, and the API is different if you want 2D or ND histograms. But if you already use NumPy histograms and you really don’t want to rewrite your code, boost-histogram has adaptors for the three histogram functions in NumPy: .. code-block:: ipython %%timeit bins, edges = bh.numpy.histogram(norm_vals, bins=100, range=(0, 10)) .. code-block:: text 7.3 ms ± 55.7 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) This is only a hair slower than using the raw boost-histogram API, and is still a nice performance boost over NumPy. You can even use the NumPy syntax if you want a boost-histogram object later: .. code-block:: python3 hist = bh.numpy.histogram(norm_vals, bins=100, range=(0, 10), histogram=bh.Histogram) You can later get a NumPy style output tuple from a histogram object: .. code-block:: python3 bins, edges = hist.to_numpy() So you can transition your code slowly to boost-histogram. 2D Histogram example ^^^^^^^^^^^^^^^^^^^^ .. code-block:: python3 data = np.random.multivariate_normal((0, 0), ((1, 0), (0, 0.5)), 10_000_000).T.copy() We can check the performance against NumPy again; NumPy does not do well with regular spaced bins in more than 1D: .. code-block:: ipython %%timeit np.histogram2d(*data, bins=(400, 200), range=((-2, 2), (-1, 1))) .. code-block:: text 1.31 s ± 17.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) .. code-block:: ipython %%timeit bh.numpy.histogram2d(*data, bins=(400, 200), range=((-2, 2), (-1, 1))) .. code-block:: text 101 ms ± 117 µs per loop (mean ± std. dev. of 7 runs, 10 loops each) For more than one dimension, boost-histogram is more than an order of magnitude faster than NumPy for regular spaced binning. All axes combinations share a common code base, so you can expect *at least* this level of performance regardless of the axes types or number of axes! Threaded filling can give you an even larger performance boost if you have multiple cores and a large fill to perform. scikit-hep-boost-histogram-b4c49b0/docs/user-guide/plotting.rst000066400000000000000000000007241524660700500247050ustar00rootroot00000000000000.. _usage-plotting: Plotting ======== Boost-histogram does not contain plotting functions - this is outside of the scope of the project, which is histogram filling and manipulation. However, it does follow ``PlottableProtocol``. Any plotting library that accepts an object that follows the ``PlottableProtocol`` can plot boost-histogram objects. Read about the ``PlottableProtocol`` in the `UHI plotting `_ page. scikit-hep-boost-histogram-b4c49b0/docs/user-guide/quickstart.rst000066400000000000000000000110221524660700500252300ustar00rootroot00000000000000.. _usage-quickstart: Quickstart ========== All of the examples will assume the following import: .. code-block:: python3 import boost_histogram as bh 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-block:: python3 hist = bh.Histogram(bh.axis.Regular(bins=10, start=0, stop=1)) If you’d like to type less, you can leave out the keywords: .. code-block:: python3 hist = bh.Histogram(bh.axis.Regular(10, 0, 1)) The exact same syntax is used for 1D, 2D, and ND histograms: .. code-block:: python3 hist3D = bh.Histogram( bh.axis.Regular(10, 0, 100, circular=True), bh.axis.Regular(10, 0.0, 10.0), bh.axis.Variable([1, 2, 3, 4, 5, 5.5, 6]), ) 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-block:: python3 hist = bh.Histogram(bh.axis.Regular(10, 0.0, 1.0)) 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 using ``bh.loc(v)``. You can also rebin with ``bh.rebin(n)`` or remove an entire axis using ``sum`` (technically as the third slice argument, though it is allowed by itself as well): .. code-block:: python3 hist = bh.Histogram( bh.axis.Regular(10, 0, 1), bh.axis.Regular(10, 0, 1), bh.axis.Regular(10, 0, 1), ) mini = hist[1:5, bh.loc(0.2) : bh.loc(0.9), 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-block:: 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-block:: 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 set three bins of a Weight histogram, you can pass the value/variance pairs: .. code-block:: 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-block:: python3 ax0 = hist.axes[0] X, Y = hist.axes.centers See :ref:`usage-axes`. Saving Histograms ----------------- You can save histograms using pickle: .. code-block:: 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 Pickling is fast and efficient; see :ref:`usage-histogram` for details and tips on protocol selection. Computing with Histograms ------------------------- As a complete example, let's say you wanted to compute and plot the density: .. literalinclude:: ../../examples/simple_density.py :language: python .. image:: ../_images/ex_hist_density.png :alt: Density histogram output :align: center Comparing with Boost.Histogram ------------------------------ This is built on the Boost.Histogram library. See :ref:`usage-comparison`. scikit-hep-boost-histogram-b4c49b0/docs/user-guide/storage.rst000066400000000000000000000116231524660700500245110ustar00rootroot00000000000000.. _usage-storage: Storages ======== There are several storages to choose from. To select a storage, pass the ``storage=bh.storage.`` argument when making a histogram. Simple storages --------------- These storages hold a single value that keeps track of a count, possibly a weighted count. Double ^^^^^^ By default, boost-histogram 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-block:: python3 h = bh.Histogram(bh.axis.Regular(10, 0, 1)) # Double() is the default h.fill([0.2, 0.3], weight=[0.5, 2]) # Weights are optional print(f"{h[bh.loc(.2)]=}\n{h[bh.loc(.3)]=}") .. code-block:: text h[bh.loc(.2)]=0.5 h[bh.loc(.3)]=2.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, unweighted counts. .. code-block:: python3 h = bh.Histogram(bh.axis.Regular(10, 0, 1), storage=bh.storage.Int64()) h.fill([0.2, 0.3], weight=[1, 2]) # Integer weights supported print(f"{h[bh.loc(.2)]=}\n{h[bh.loc(.2)]=}") .. code-block:: text h[bh.loc(.2)]=1 h[bh.loc(.3)]=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. MultiCell ^^^^^^^^^ This storage is like the ``Double`` storage but supports storing multiple entries per bin independently. This is useful if one has to deal with many independent weights per event that all correspond to the same parameter (they will a be binned into the same bin) and have to be summed independently (e.g. to track the effect of systematic variations). It is supposed to be filled much faster compared to filling many histograms with a ``Weight`` storage type in a loop if one deals with a large number of different weights. The number of entries per bin has to be fixed and is provided to the storage at its construction through .. code-block:: python3 bh.Histogram(…, storage=MultiCell(nelem)) where ``nelem`` is the number of entries per bin. The entries have to provided as a 2-dimensional array ``(n, nelem)`` with the first dimension being the events to histogram and the second axis being the entries per event. To fill a histogram ``h`` one has to provide the entries via the ``weight`` keyword: .. code-block:: python3 h.fill(..., weight=weights) Any slicing or projection operation works for ``MultiCell`` histograms identical to any other histogram with different storage type, the entries are here not considered an additional axis for the histogram. Calling ``h.view()`` returns an array where the entries are indexed as the first axis (e.g. ``h.view()[0]`` is the histogram content for the first entry per bin). Contrary to the ``Weight`` storage the ``MultiCell`` storage does not track variances (it does not track the sum of weights squared) because this might not be necessary for every entry index. Instead, the user is supposed to track the variances themselves if required. This could be achieved by providing the variances as a separate entry to the ``MultiCell`` histogram by increasing the number of entries that are stored per bin (e.g. one could provide the square of ``weights[:, 0]`` as ``weights[:, 1]`` to track the sum of weight squared of the first entry index in the second entry index). Note: If you should ever need to use the lowlevel ``h._hist.fill()`` function with a ``MultiCell`` storage you will have to use the ``sample`` keyword to pass the weights instead of the ``weight`` keyword because that is used on the C++ side, but the highlevel python boost_histogram API hides this from the user. 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 must add a ``sample=`` term. WeightedMean ^^^^^^^^^^^^ This is similar to Mean, but also keeps track a sum of weights like term as well. scikit-hep-boost-histogram-b4c49b0/docs/user-guide/subclassing.rst000066400000000000000000000053701524660700500253640ustar00rootroot00000000000000.. _usage-subclassing: Subclassing (advanced) ====================== Subclassing boost-histogram components is supported, but requires a little extra care to ensure the subclasses do not return un-wrapped boost-histogram components when a subclassed version is available. The issue is that various actions make the C++ -> Python transition over again, such as using ``.project()``. For example, let's say you have a ``MyHistogram`` and a ``MyRegular``. If you use ``project(0)``, that needs to also return a ``MyRegular``, but it is reconverting the return value from C++ to Python, so it has to somehow know that ``MyRegular`` is the right axis subclass to select from for ``MyHistogram``. This is accomplished with families. When you subclass, you will need to add a family. Any object can be used - the module for your library is a good choice if you only have one "family" of histograms. Boost-histogram uses ``boost_histogram``, Hist uses ``hist``. You can use anything you want, though; a custom tag object like ``MY_FAMILY = object()`` works well too. It just has to support ``is``, and be the exact same object on all your subclasses. .. code-block:: python3 import boost_histogram as bh import my_package class Histogram(bh.Histogram, family=my_package): ... class Regular(bh.axis.Regular, family=my_package): ... If you only override ``Histogram``, you can leave off the ``family=`` argument, or set it to ``None``. It will generate a private ``object()`` in this case. You must add an explicit family to ``Histogram`` if you subclass any further components. If you use Mixins, special care needs to be taken if you need a left-acting Mixin, since class keywords are handled via ``super()`` left to right. This is a Mixin that will work on either side: .. code-block:: python3 class AxisMixin: def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) # type: ignore Mixins are recommended if you want to provide functionality to a collection of different subclasses, like ``Axis``. There are customization hooks provided for subclasses as well. ``self._generate_axes_()`` is called to produce an ``AxesTuple``, so you can override that if you customize ``AxesTuple``. ``_import_bh_`` and ``_export_bh_`` are called when converting an object between histogram libraries. ``cls._export_bh_(self)`` is called from the outgoing class (being converted from), and ``self._import_bh_()`` is called afterward on the incoming class (being converted to). So if ``h1`` is an instance of ``H1``, and ``H2`` is the new class, then ``H2(h1)`` calls ``H1._export_bh_(h2)`` and then ``h2._import_bh_()`` before returning ``h2``. The internal repr building for axes is a list produced by ``_repr_args_`` representing each item in the repr. scikit-hep-boost-histogram-b4c49b0/docs/user-guide/transforms.rst000066400000000000000000000140641524660700500252450ustar00rootroot00000000000000.. _usage-transforms: Using Transforms ================ The boost-histogram library provides a powerful transform system on Regular axes that allows you to provide a functional form for the conversion between a regular spacing and the actual bin edges. The following transforms are built in: * ``bh.axis.transform.sqrt``: A square root transform * ``bh.axis.transform.log``: A logarithmic transform * ``bh.axis.transform.Pow(power)`` Raise to a specified power (``power=0.5`` is identical to ``sqrt``) There is also a flexible ``bh.axis.transform.Function``, which allows you to specify arbitrary conversion functions (detailed below). Simple custom transforms ------------------------ The ``Function`` transform takes two ctypes ``double(double)`` function pointers, a forward transform and a inverse transform. An object that provides a ctypes function pointer through a ``.ctypes`` attribute is supported, as well. As an example, let's look at how one would recreate the ``log`` transform using several different methods: Pure Python ^^^^^^^^^^^ You can directly cast a python callable to a ctypes pointer, and use that. However, you will call Python *every* time you interact with the transformed axis, and this will be 15-90 times slower than a compiled method, like ``bh.axis.transform.log``. In most cases, a Variable axis will be faster. .. code-block:: python import ctypes ftype = ctypes.CFUNCTYPE(ctypes.c_double, ctypes.c_double) # Pure Python (15x slower) bh.axis.Regular( 10, 1, 4, transform=bh.axis.transform.Function(ftype(math.log), ftype(math.exp)) ) # Pure Python: NumPy (90x slower) bh.axis.Regular( 10, 1, 4, transform=bh.axis.transform.Function(ftype(np.log), ftype(np.exp)) ) You can create a Variable axis from the edges of this axis; often that will be faster. You can also use ``transform=ftype`` and just directly provide the functions; this provides nicer reprs, but is still not picklable because ftype is a generated and not picklable; see below for a way to make this picklable. You can also specify ``name="..."`` to customize the repr explicitly. Using Numba ^^^^^^^^^^^ If you have the numba library installed, and your transform is reasonably simple, you can use the ``@numba.cfunc`` decorator to create a callable that will run directly through the C interface. This is just as fast as the compiled version provided! .. code-block:: python import numba @numba.cfunc(numba.float64(numba.float64)) def exp(x): return math.exp(x) @numba.cfunc(numba.float64(numba.float64)) def log(x): return math.log(x) bh.axis.Regular(10, 1, 4, transform=bh.axis.transform.Function(log, exp)) Manual compilation ^^^^^^^^^^^^^^^^^^ You can also get a ctypes pointer from the usual place: a library. Let's say you have the following ``mylib.c`` file: .. code-block:: c #include double my_log(double value) { return log(value); } double my_exp(double value) { return exp(value); } And you compile it with: .. code-block:: bash gcc mylib.c -shared -o mylib.so You can now use it like this: .. code-block:: python import ctypes ftype = ctypes.CFUNCTYPE(ctypes.c_double, ctypes.c_double) mylib = ctypes.CDLL("mylib.so") my_log = ctypes.cast(mylib.my_log, ftype) my_exp = ctypes.cast(mylib.my_exp, ftype) bh.axis.Regular(10, 1, 4, transform=bh.axis.transform.Function(my_log, my_exp)) Note that you do actually have to cast it to the correct function type; just setting ``argtypes`` and ``restype`` does not work. Picklable custom transforms --------------------------- The above examples to not support pickling, since ctypes pointers (or pointers in general) are not picklable. However, the ``Function`` transform supports a ``convert=`` keyword argument that takes the two provided objects and converts them to ctypes pointers. So if you can supply a pair of picklable objects and a conversion function, you can make a fully picklable transform. A few common cases are given below. Pure Python ^^^^^^^^^^^ This is the easiest example; as long as your Python function is picklable, all you need to do is move the ctypes call into the convert function. You need a little wrapper function to make it picklable: .. code-block:: python import ctypes, math # We need a little wrapper function only because ftype is not directly picklable def convert_python(func): ftype = ctypes.CFUNCTYPE(ctypes.c_double, ctypes.c_double) return ftype(func) bh.axis.Regular( 10, 1, 4, transform=bh.axis.transform.Function(math.log, math.exp, convert=convert_python), ) That's it. Using Numba ^^^^^^^^^^^ The same procedure works for numba decorators. NumPy only supports functions, not builtins like ``math.log``, so if you want to pass those, you'll need to wrap them in a lambda function or add a bit of logic to the convert function. Here are your options: .. code-block:: python import numba, math def convert_numba(func): return numba.cfunc(numba.double(numba.double))(func) # Built-ins and ufuncs need to be wrapped (numba can't read a signature) # User functions would not need the lambda bh.axis.Regular( 10, 1, 4, transform=bh.axis.transform.Function( lambda x: math.log(x), lambda x: math.exp(x), convert=convert_numba ), ) Note that ``numba.cfunc`` does not work on its own builtins, but requires a user function. Since with the exception of the simple example I'm showing here that is already available directly in boost-histogram, you will probably be composing your own functions out of more than one builtin operation, you generally will not need the lambda here. Manual compilation ^^^^^^^^^^^^^^^^^^ You can use strings to look up functions in the shared library: .. code-block:: python def lookup(name): mylib = ctypes.CDLL("mylib.so") function = getattr(mylib, name) return ctypes.cast(function, ftype) bh.axis.Regular( 10, 1, 4, transform=bh.axis.transform.Function("my_log", "my_exp", convert=lookup) ) scikit-hep-boost-histogram-b4c49b0/examples/000077500000000000000000000000001524660700500211255ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/examples/README.md000066400000000000000000000005421524660700500224050ustar00rootroot00000000000000# Boost Histogram examples The examples require Python 3.10. It is left as an exercise for the reader to convert back to older versions if they so desire. ### Setup You can run these examples in a virtual environment using: #### Conda ```bash conda create -p .env python=3.9 boost-histogram matplotlib numpy -c conda-forge conda activate ./.env ``` scikit-hep-boost-histogram-b4c49b0/examples/cpp/000077500000000000000000000000001524660700500217075ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/examples/cpp/CMakeLists.txt000066400000000000000000000017741524660700500244600ustar00rootroot00000000000000# This file lets you quickly build projects with the exact same boost-histogram # settings that the Python bindings use. cmake_minimum_required(VERSION 3.24...3.31) project(BOOST_HISTOGRAM_CPP LANGUAGES CXX) add_library(boost_histogram_cpp IMPORTED INTERFACE) # these are the boost header-only libraries required by boost::histogram target_include_directories( boost_histogram_cpp INTERFACE ../../extern/assert/include ../../extern/config/include ../../extern/core/include ../../extern/histogram/include ../../extern/mp11/include ../../extern/throw_exception/include ../../extern/variant2/include) target_compile_options( boost_histogram_cpp INTERFACE $,/fp:fast,-funsafe-math-optimizations>) target_compile_features(boost_histogram_cpp INTERFACE cxx_std_14) if(PROJECT_NAME STREQUAL CMAKE_PROJECT_NAME) add_executable(simple simple.cpp) target_link_libraries(simple PRIVATE boost_histogram_cpp) endif() scikit-hep-boost-histogram-b4c49b0/examples/cpp/simple.cpp000066400000000000000000000003331524660700500237030ustar00rootroot00000000000000#include #include int main() { namespace bh = boost::histogram; auto h = bh::make_histogram(bh::axis::category{}); std::cout << bh::algorithm::sum(h) << std::endl; } scikit-hep-boost-histogram-b4c49b0/examples/simple_2d.py000077500000000000000000000015601524660700500233620ustar00rootroot00000000000000#!/usr/bin/env python3 from __future__ import annotations import matplotlib.pyplot as plt import numpy as np import boost_histogram as bh # Create 2d-histogram with two axes with 20 equidistant bins from -3 to 3 h = bh.Histogram( bh.axis.Regular(20, -3, 3, metadata="x"), bh.axis.Regular(20, -3, 3, metadata="y") ) # Generate some NumPy arrays with data to fill into histogram, # in this case normal distributed random numbers in x and y x_data = np.random.randn(1_000) y_data = 0.5 * np.random.randn(1_000) # Fill histogram with NumPy arrays, this is very fast h.fill(x_data, y_data) # Get numpy.histogram compatible representation of the histogram w, x, y = h.to_numpy() # Draw the count matrix fig, ax = plt.subplots() ax.pcolormesh(x, y, w.T) ax.set_xlabel(h.axes[0].metadata) ax.set_ylabel(h.axes[1].metadata) ax.set_aspect("equal") plt.savefig("simple_2d.png") scikit-hep-boost-histogram-b4c49b0/examples/simple_density.py000077500000000000000000000012631524660700500245340ustar00rootroot00000000000000#!/usr/bin/env python3 from __future__ import annotations import functools import operator import matplotlib.pyplot as plt import numpy as np import boost_histogram as bh # Make a 2D histogram hist = bh.Histogram(bh.axis.Regular(50, -3, 3), bh.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.view() / 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") scikit-hep-boost-histogram-b4c49b0/examples/simple_log_weight.py000077500000000000000000000022461524660700500252070ustar00rootroot00000000000000#!/usr/bin/env python3 from __future__ import annotations import boost_histogram as bh # make 1-d histogram with 5 logarithmic bins from 1e0 to 1e5 h = bh.Histogram( bh.axis.Regular(5, 1e0, 1e5, metadata="x", transform=bh.axis.transform.log), storage=bh.storage.Weight(), ) # fill histogram with numbers x = (2e0, 2e1, 2e2, 2e3, 2e4) h.fill(x, weight=4) # increment bin counter by 4 # iterate over bins and access bin counter for idx, (lower, upper) in enumerate(h.axes[0]): val = h[idx] print(f"bin {idx} [{lower:g}, {upper:g}): {val.value} +/- {val.variance**0.5}") # under- and overflow bin lo, up = h.axes[0][bh.underflow] print( f"underflow [{lo:g}, {up:g}): {h[bh.underflow].value} +/- {h[bh.underflow].variance ** 0.5}" ) lo, up = h.axes[0][bh.overflow] print( f"overflow [{lo:g}, {up:g}): {h[bh.overflow].value} +/- {h[bh.overflow].variance ** 0.5}" ) # prints: # bin 0 x in [1.0, 10.0): 4.0 +/- 4.0 # bin 1 x in [10.0, 100.0): 4.0 +/- 4.0 # bin 2 x in [100.0, 1000.0): 4.0 +/- 4.0 # bin 3 x in [1000.0, 10000.0): 4.0 +/- 4.0 # bin 4 x in [10000.0, 100000.0): 4.0 +/- 4.0 # underflow [0.0, 1.0): 0.0 +/- 0.0 # overflow [100000.0, inf): 0.0 +/- 0.0 scikit-hep-boost-histogram-b4c49b0/examples/simple_numpy.py000077500000000000000000000016241524660700500242260ustar00rootroot00000000000000#!/usr/bin/env python3 from __future__ import annotations import matplotlib.pyplot as plt import numpy as np import boost_histogram as bh # Create 2d-histogram with two axes with 10 equidistant bins from -3 to 3 h = bh.Histogram( bh.axis.Regular(10, -3, 3, metadata="x"), bh.axis.Regular(10, -3, 3, metadata="y") ) # Generate some NumPy arrays with data to fill into histogram, # in this case normal distributed random numbers in x and y x_data = np.random.randn(1000) y_data = 0.5 * np.random.randn(1000) # Fill histogram with numpy arrays, this is very fast h.fill(x_data, y_data) # Get representations of the bin edges as NumPy arrays x = h.axes[0].edges y = h.axes[1].edges # Creates a view of the counts (no copy involved) count_matrix = h.view() # Draw the count matrix plt.pcolor(x, y, count_matrix.T) plt.xlabel(h.axes[0].metadata) plt.ylabel(h.axes[1].metadata) plt.savefig("simple_numpy.png") scikit-hep-boost-histogram-b4c49b0/examples/simple_pickly.py000077500000000000000000000011561524660700500243510ustar00rootroot00000000000000#!/usr/bin/env python3 from __future__ import annotations import pickle from pathlib import Path import boost_histogram as bh h1 = bh.Histogram(bh.axis.Regular(2, -1, 1)) h2 = h1.copy() h1.fill(-0.5) h2.fill(0.5) # Arithmetic operators h3 = h1 + h2 h4 = h3 * 2 print(f"h4[0] = {h4[0]}, h4[1] = {h4[1]}") h4_saved = Path("h4_saved.pkl") # Now save the histogram with h4_saved.open("wb") as f: pickle.dump(h4, f, protocol=-1) # And load with h4_saved.open("rb") as f: h5 = pickle.load(f) assert h4 == h5 print("Succeeded in pickling a histogram!") # Delete the file to keep things tidy h4_saved.unlink() scikit-hep-boost-histogram-b4c49b0/examples/simple_weight.py000077500000000000000000000012571524660700500243470ustar00rootroot00000000000000#!/usr/bin/env python3 from __future__ import annotations import boost_histogram as bh # Make 1-d histogram with 5 logarithmic bins from 1e0 to 1e5 h = bh.Histogram( bh.axis.Regular(5, 1e0, 1e5, metadata="x", transform=bh.axis.transform.log), storage=bh.storage.Weight(), ) # Fill histogram with numbers x = (2e0, 2e1, 2e2, 2e3, 2e4) # Doing this several times so the variance is more interesting h.fill(x, weight=2) h.fill(x, weight=2) h.fill(x, weight=2) h.fill(x, weight=2) # Iterate over bins and access bin counter for idx, (lower, upper) in enumerate(h.axes[0]): val = h[idx] print(f"bin {idx} in [{lower:g}, {upper:g}): {val.value} +/- {val.variance**0.5}") scikit-hep-boost-histogram-b4c49b0/extern/000077500000000000000000000000001524660700500206145ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/extern/assert/000077500000000000000000000000001524660700500221155ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/extern/config/000077500000000000000000000000001524660700500220615ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/extern/core/000077500000000000000000000000001524660700500215445ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/extern/histogram/000077500000000000000000000000001524660700500226115ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/extern/mp11/000077500000000000000000000000001524660700500213725ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/extern/throw_exception/000077500000000000000000000000001524660700500240355ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/extern/variant2/000077500000000000000000000000001524660700500223425ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/include/000077500000000000000000000000001524660700500207325ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/include/bh_python/000077500000000000000000000000001524660700500227245ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/include/bh_python/accumulators/000077500000000000000000000000001524660700500254265ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/include/bh_python/accumulators/mean.hpp000066400000000000000000000064421524660700500270650ustar00rootroot00000000000000// Copyright 2015-2019 Hans Dembinski and Henry Schreiner // // Distributed under the Boost Software License, version 1.0. // (See accompanying file LICENSE_1_0.txt // or copy at http://www.boost.org/LICENSE_1_0.txt) // // Based on boost/histogram/accumulators/mean.hpp // Changes: // * Internal values are public for access from Python // * A special constructor added for construction from Python #pragma once #include #include #include namespace accumulators { /** Calculates mean and variance of sample. Uses Welford's incremental algorithm to improve the numerical stability of mean and variance computation. */ template struct mean { using value_type = ValueType; using const_reference = const value_type&; mean() = default; mean(const value_type& n, const value_type& mean, const value_type& variance) noexcept : count(n) , value(mean) , _sum_of_deltas_squared(variance * (n - 1)) {} mean(const value_type& sum, const value_type& mean, const value_type& _sum_of_deltas_squared, bool /* Tag to trigger python internal constructor */) : count(sum) , value(mean) , _sum_of_deltas_squared(_sum_of_deltas_squared) {} void operator()(const value_type& x) noexcept { count += static_cast(1); const auto delta = x - value; value += delta / count; _sum_of_deltas_squared += delta * (x - value); } void operator()(const boost::histogram::weight_type& w, const value_type& x) noexcept { count += w.value; const auto delta = x - value; value += w.value * delta / count; _sum_of_deltas_squared += w.value * delta * (x - value); } mean& operator+=(const mean& rhs) noexcept { if(rhs.count == 0) return *this; const auto mu1 = value; const auto mu2 = rhs.value; const auto n1 = count; const auto n2 = rhs.count; count += rhs.count; value = (n1 * mu1 + n2 * mu2) / count; _sum_of_deltas_squared += rhs._sum_of_deltas_squared; _sum_of_deltas_squared += n1 * (value - mu1) * (value - mu1) + n2 * (value - mu2) * (value - mu2); return *this; } mean& operator*=(const value_type& s) noexcept { value *= s; _sum_of_deltas_squared *= s * s; return *this; } bool operator==(const mean& rhs) const noexcept { return count == rhs.count && value == rhs.value && _sum_of_deltas_squared == rhs._sum_of_deltas_squared; } bool operator!=(const mean& rhs) const noexcept { return !operator==(rhs); } value_type variance() const noexcept { if(count <= 1) return std::numeric_limits::quiet_NaN(); return _sum_of_deltas_squared / (count - 1); } template void serialize(Archive& ar, unsigned) { ar& boost::make_nvp("count", count); ar& boost::make_nvp("value", value); ar& boost::make_nvp("_sum_of_deltas_squared", _sum_of_deltas_squared); } value_type count{}; value_type value{}; value_type _sum_of_deltas_squared{}; }; } // namespace accumulators scikit-hep-boost-histogram-b4c49b0/include/bh_python/accumulators/ostream.hpp000066400000000000000000000063031524660700500276130ustar00rootroot00000000000000// Copyright 2015-2020 Henry Schreiner and Hans Dembinski // // Distributed under the 3-Clause BSD License. See accompanying // file LICENSE or https://github.com/scikit-hep/boost-histogram for details. // // Based on boost/histogram/axis/ostream.hpp // String representations here evaluate correctly in Python. #pragma once #include #include #include #include #include #include #include #include #include /** \file boost/histogram/accumulators/ostream.hpp Simple streaming operators for the builtin accumulator types. Mostly similar to boost/histogram/accumulators/ostream.hpp */ template std::basic_ostream& handle_nonzero_width(std::basic_ostream& os, const T& x) { const auto w = os.width(); os.width(0); std::streamsize count = 0; { auto g = ::boost::histogram::detail::make_count_guard(os, count); os << x; } if(os.flags() & std::ios::left) { os << x; for(auto i = count; i < w; ++i) os << os.fill(); } else { for(auto i = count; i < w; ++i) os << os.fill(); os << x; } return os; } // Note that the names are *not* included here, so they can be added in pybind11. namespace accumulators { template std::basic_ostream& operator<<(std::basic_ostream& os, const weighted_sum& x) { if(os.width() == 0) return os << "value=" << x.value << ", variance=" << x.variance; return handle_nonzero_width(os, x); } template std::basic_ostream& operator<<(std::basic_ostream& os, const mean& x) { if(os.width() == 0) return os << "count=" << x.count << ", value=" << x.value << ", variance=" << x.variance(); return handle_nonzero_width(os, x); } template std::basic_ostream& operator<<(std::basic_ostream& os, const weighted_mean& x) { if(os.width() == 0) return os << "sum_of_weights=" << x.sum_of_weights << ", sum_of_weights_squared=" << x.sum_of_weights_squared << ", value=" << x.value << ", variance=" << x.variance(); return handle_nonzero_width(os, x); } template std::basic_ostream& operator<<(std::basic_ostream& os, const ::boost::histogram::accumulators::count& x) { os << x.load(); return os; } } // namespace accumulators // Python style output instead of C++ inline std::string shift_to_string(const ::boost::histogram::accumulators::sum& x) { std::ostringstream out; out << x.large_part() << " + " << x.small_part(); return out.str(); } scikit-hep-boost-histogram-b4c49b0/include/bh_python/accumulators/weighted_mean.hpp000066400000000000000000000103621524660700500307410ustar00rootroot00000000000000// Copyright 2018-2019 Hans Dembinski and Henry Schreiner // // Distributed under the Boost Software License, version 1.0. // (See accompanying file LICENSE_1_0.txt // or copy at http://www.boost.org/LICENSE_1_0.txt) // // Based on boost/histogram/accumulators/weighted_mean.hpp // // Changes: // * Internal values are public for access from Python // * A special constructor added for construction from Python #pragma once #include #include #include namespace accumulators { /** Calculates mean and variance of weighted sample. Uses West's incremental algorithm to improve numerical stability of mean and variance computation. */ template struct weighted_mean { using value_type = ValueType; using const_reference = const value_type&; weighted_mean() = default; weighted_mean(const value_type& wsum, const value_type& wsum2, const value_type& mean, const value_type& variance) : sum_of_weights(wsum) , sum_of_weights_squared(wsum2) , value(mean) , _sum_of_weighted_deltas_squared( variance * (sum_of_weights - (sum_of_weights_squared / sum_of_weights))) { } weighted_mean(const value_type& wsum, const value_type& wsum2, const value_type& mean, const value_type& _sum_of_weighted_deltas_squared, bool /* tag to trigger Python internal constructor */) : sum_of_weights(wsum) , sum_of_weights_squared(wsum2) , value(mean) , _sum_of_weighted_deltas_squared(_sum_of_weighted_deltas_squared) {} void operator()(const value_type& x) { operator()(boost::histogram::weight(1), x); } void operator()(const boost::histogram::weight_type& w, const value_type& x) { sum_of_weights += w.value; sum_of_weights_squared += w.value * w.value; const auto delta = x - value; value += w.value * delta / sum_of_weights; _sum_of_weighted_deltas_squared += w.value * delta * (x - value); } weighted_mean& operator+=(const weighted_mean& rhs) { if(rhs.sum_of_weights == 0) return *this; const auto mu1 = value; const auto mu2 = rhs.value; const auto n1 = sum_of_weights; const auto n2 = rhs.sum_of_weights; sum_of_weights += rhs.sum_of_weights; sum_of_weights_squared += rhs.sum_of_weights_squared; value = (n1 * mu1 + n2 * mu2) / sum_of_weights; _sum_of_weighted_deltas_squared += rhs._sum_of_weighted_deltas_squared; _sum_of_weighted_deltas_squared += n1 * (value - mu1) * (value - mu1) + n2 * (value - mu2) * (value - mu2); return *this; } weighted_mean& operator*=(const value_type& s) { value *= s; _sum_of_weighted_deltas_squared *= s * s; return *this; } bool operator==(const weighted_mean& rhs) const noexcept { return sum_of_weights == rhs.sum_of_weights && sum_of_weights_squared == rhs.sum_of_weights_squared && value == rhs.value && _sum_of_weighted_deltas_squared == rhs._sum_of_weighted_deltas_squared; } bool operator!=(const weighted_mean& rhs) const noexcept { return !operator==(rhs); } value_type variance() const { const value_type denom = sum_of_weights - (sum_of_weights_squared / sum_of_weights); if(denom <= 0) return std::numeric_limits::quiet_NaN(); return _sum_of_weighted_deltas_squared / denom; } template void serialize(Archive& ar, unsigned /* version */) { ar& boost::make_nvp("sum_of_weights", sum_of_weights); ar& boost::make_nvp("sum_of_weights_squared", sum_of_weights_squared); ar& boost::make_nvp("value", value); ar& boost::make_nvp("_sum_of_weighted_deltas_squared", _sum_of_weighted_deltas_squared); } value_type sum_of_weights{}; value_type sum_of_weights_squared{}; value_type value{}; value_type _sum_of_weighted_deltas_squared{}; }; } // namespace accumulators scikit-hep-boost-histogram-b4c49b0/include/bh_python/accumulators/weighted_sum.hpp000066400000000000000000000052231524660700500306250ustar00rootroot00000000000000// Copyright 2015-2019 Hans Dembinski and Henry Schreiner // // Distributed under the Boost Software License, version 1.0. // (See accompanying file LICENSE_1_0.txt // or copy at http://www.boost.org/LICENSE_1_0.txt) // // Based on boost/histogram/accumulators/weighted_mean.hpp // // Changes: // * Internal values are public for access from Python // * A special constructor added for construction from Python #pragma once #include #include namespace accumulators { /// Holds sum of weights and its variance estimate template struct weighted_sum { using value_type = ValueType; using const_reference = const value_type&; weighted_sum() = default; /// Initialize sum to value and allow implicit conversion explicit weighted_sum(const_reference value) noexcept : weighted_sum(value, value) {} /// Allow implicit conversion from sum template explicit weighted_sum(const weighted_sum& s) noexcept : weighted_sum(s.value(), s.variance()) {} /// Initialize sum to value and variance weighted_sum(const_reference value, const_reference variance) noexcept : value(value) , variance(variance) {} /// Increment by one. weighted_sum& operator++() { value += 1; variance += 1; return *this; } /// Increment by weight. template weighted_sum& operator+=(const bh::weight_type& w) { value += w.value; variance += w.value * w.value; return *this; } /// Added another weighted sum. weighted_sum& operator+=(const weighted_sum& rhs) { value += rhs.value; variance += rhs.variance; return *this; } /// Scale by value. weighted_sum& operator*=(const value_type& x) { value *= x; variance *= x * x; return *this; } bool operator==(const value_type& rhs) const noexcept { return value == rhs && variance == rhs; } template bool operator==(const weighted_sum& rhs) const noexcept { return value == rhs.value && variance == rhs.variance; } template bool operator!=(const T& rhs) const noexcept { return !operator==(rhs); } // lossy conversion must be explicit explicit operator const_reference() const { return value; } template void serialize(Archive& ar, unsigned /* version */) { ar& boost::make_nvp("value", value); ar& boost::make_nvp("variance", variance); } value_type value{}; value_type variance{}; }; } // namespace accumulators scikit-hep-boost-histogram-b4c49b0/include/bh_python/array_like.hpp000066400000000000000000000021751524660700500255640ustar00rootroot00000000000000// Copyright 2018-2019 Hans Dembinski and Henry Schreiner // // Distributed under the 3-Clause BSD License. See accompanying // file LICENSE or https://github.com/scikit-hep/boost-histogram for details. #pragma once #include #include /// Generate empty C-contiguous array with same shape as argument /// /// Note: the result is always C-contiguous regardless of the input's memory /// layout (strides are *not* copied); callers fill it linearly in C order from /// a C-contiguous copy of the input. template py::array_t array_like(const py::object& obj) { if(!py::isinstance(obj)) { py::ssize_t shape[1] = {0}; // if scalar if(py::isinstance(obj) && !py::isinstance(obj)) { // if sequence auto seq = py::cast(obj); shape[0] = static_cast(seq.size()); } return py::array_t(shape); } auto arr = py::cast(obj); return py::array_t{boost::span{ arr.shape(), static_cast(arr.ndim())}}; } scikit-hep-boost-histogram-b4c49b0/include/bh_python/axis.hpp000066400000000000000000000323641524660700500244110ustar00rootroot00000000000000// Copyright 2018-2019 Henry Schreiner and Hans Dembinski // // Distributed under the 3-Clause BSD License. See accompanying // file LICENSE or https://github.com/scikit-hep/boost-histogram for details. #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace axis { namespace option = bh::axis::option; using ogrowth_t = decltype(option::growth | option::overflow); using uogrowth_t = decltype(option::growth | option::underflow | option::overflow); using circular_t = decltype(option::circular | option::overflow); // Must be specialized for each type (compile warning if not) template inline const char* string_name(); // Macro to make the string specializations more readable #define BHP_SPECIALIZE_NAME(name) \ template <> \ inline const char* string_name() { \ return #name; \ } // These match the Python names using regular_none = bh::axis::regular; using regular_uflow = bh::axis::regular; using regular_oflow = bh::axis::regular; using regular_uoflow = bh::axis::regular; using regular_uoflow_growth = bh::axis::regular; using regular_circular = bh::axis::regular; BHP_SPECIALIZE_NAME(regular_none) BHP_SPECIALIZE_NAME(regular_uflow) BHP_SPECIALIZE_NAME(regular_oflow) BHP_SPECIALIZE_NAME(regular_uoflow) BHP_SPECIALIZE_NAME(regular_uoflow_growth) BHP_SPECIALIZE_NAME(regular_circular) using regular_pow = bh::axis::regular; using regular_trans = bh::axis::regular; BHP_SPECIALIZE_NAME(regular_pow) BHP_SPECIALIZE_NAME(regular_trans) using variable_none = bh::axis::variable; using variable_uflow = bh::axis::variable; using variable_oflow = bh::axis::variable; using variable_uoflow = bh::axis::variable; using variable_uoflow_growth = bh::axis::variable; using variable_circular = bh::axis::variable; BHP_SPECIALIZE_NAME(variable_none) BHP_SPECIALIZE_NAME(variable_uflow) BHP_SPECIALIZE_NAME(variable_oflow) BHP_SPECIALIZE_NAME(variable_uoflow) BHP_SPECIALIZE_NAME(variable_uoflow_growth) BHP_SPECIALIZE_NAME(variable_circular) using integer_none = bh::axis::integer; using integer_uoflow = bh::axis::integer; using integer_uflow = bh::axis::integer; using integer_oflow = bh::axis::integer; using integer_growth = bh::axis::integer; using integer_circular = bh::axis::integer; BHP_SPECIALIZE_NAME(integer_none) BHP_SPECIALIZE_NAME(integer_uoflow) BHP_SPECIALIZE_NAME(integer_uflow) BHP_SPECIALIZE_NAME(integer_oflow) BHP_SPECIALIZE_NAME(integer_growth) BHP_SPECIALIZE_NAME(integer_circular) using category_int_none = bh::axis::category; using category_int = bh::axis::category; using category_int_growth = bh::axis::category; BHP_SPECIALIZE_NAME(category_int_none) BHP_SPECIALIZE_NAME(category_int) BHP_SPECIALIZE_NAME(category_int_growth) using category_str_none = bh::axis::category; using category_str = bh::axis::category; using category_str_growth = bh::axis::category; BHP_SPECIALIZE_NAME(category_str_none) BHP_SPECIALIZE_NAME(category_str) BHP_SPECIALIZE_NAME(category_str_growth) class boolean : public bh::axis::integer { public: explicit boolean(metadata_t meta = {}) : integer(0, 2, std::move(meta)) {} explicit boolean(const boolean& src, bh::axis::index_type begin, bh::axis::index_type end, unsigned merge) : integer(src, begin, end, merge) {} boolean(const boolean& other) = default; bh::axis::index_type index(int x) const noexcept { return integer::index(x == 0 ? 0 : 1); } // Redeclared so &boolean::value binds on boolean; pybind11 does not know // the base class, so a base member pointer would reject a boolean self. int value(bh::axis::index_type i) const noexcept { return integer::value(i); } // We can't specify inclusive, since this could be sliced }; // Built-in boolean requires bool fill, slower compile, not reducible // using boolean = bh::axis::boolean; BHP_SPECIALIZE_NAME(boolean) // Not exposed to Python (nothing constructs it anymore), but kept in // axis_variant below: removing it would shift the "which" index that old // pickle files use to select an axis type, breaking their unpickling. BHP_SPECIALIZE_NAME(regular_numpy) #undef BHP_SPECIALIZE_NAME // How edges, centers, and widths are handled // // We distinguish between continuous and discrete axes. The integer axis is a borderline // case. It has discrete values, but they are consecutive. It is possible although not // correct to treat it like a continuous axis with bin width of 1. For the sake of // computing bin edges and bin center, we will use this ansatz here. PS: This behavior // is already implemented in Boost::Histogram when you create an integer axis with a // floating point value type, e.g. integer. In this case, the integer axis acts // strictly like a regular axis with a fixed bin with of 1. We don't use it here, // because it is slightly slower than the integer axis when the input values are // truly integers. // // The category axis is treated like regular(size(), 0, size()) in the conversion. It is // the responsibility of the user to set the labels accordingly when a histogram with a // category axis is plotted. template constexpr bool is_category(const A&) { return false; } template constexpr bool is_category(const bh::axis::category&) { return true; } template decltype(auto) select(Continuous&& c, Discrete&& d, Integer&&, const A& ax) { return bh::detail::static_if>( std::forward(c), std::forward(d), ax); } template decltype(auto) select(Continuous&&, Discrete&&, Integer&& i, const bh::axis::integer& ax) { return std::forward(i)(ax); } template decltype(auto) select(Continuous&&, Discrete&&, Boolean&& i, const boolean& ax) { return std::forward(i)(ax); } /// Return bin center for continuous axis and bin value for discrete axis template double unchecked_center(const A& ax, bh::axis::index_type i) { return select([i](const auto& ax) { return ax.value(i + 0.5); }, [i](const auto&) { return i + 0.5; }, [i](const auto& ax) { return ax.value(i) + 0.5; }, ax); } /// Return bin in a native Python representation template decltype(auto) unchecked_bin(const A& ax, bh::axis::index_type i) { return bh::detail::static_if>( [i](const auto& ax) -> decltype(auto) { return py::make_tuple(ax.value(i), ax.value(i + 1)); }, [i](const auto& ax) -> decltype(auto) { return (!is_category(ax) || i < ax.size()) ? py::cast(ax.bin(i)) : py::none(); }, ax); } /// Convert continuous axis into numpy.histogram compatible edge array template py::array_t edges(const A& ax, bool flow = false, bool numpy_upper = false) { auto continuous = [flow, numpy_upper](const auto& ax) { using AX = std::decay_t; using index_type = bh::axis::index_type; const index_type underflow = flow && (bh::axis::traits::get_options::test(option::underflow)); const index_type overflow = flow && (bh::axis::traits::get_options::test(option::overflow)); py::array_t edges( static_cast(ax.size() + 1 + overflow + underflow)); double* data = edges.mutable_data(); for(index_type i = -underflow; i <= ax.size() + overflow; ++i) data[i + underflow] = ax.value(i); if(numpy_upper && !(std::is_same::value || std::is_same::value)) { data[ax.size() + underflow] = std::nextafter( data[ax.size() + underflow], std::numeric_limits::lowest()); } return edges; }; return select( continuous, [flow](const auto& ax) { using AX = std::decay_t; static_assert(!bh::axis::traits::get_options::test(option::underflow), "discrete axis never has underflow"); const bh::axis::index_type overflow = flow && bh::axis::traits::get_options::test(option::overflow); py::array_t edges( static_cast(ax.size() + 1 + overflow)); double* data = edges.mutable_data(); for(bh::axis::index_type i = 0; i <= ax.size() + overflow; ++i) data[i] = i; return edges; }, continuous, ax); } template py::array_t centers(const A& ax) { py::array_t result(ax.size()); for(bh::axis::index_type i = 0; i < ax.size(); ++i) result.mutable_data()[i] = unchecked_center(ax, i); return result; } template py::array_t widths(const A& ax) { py::array_t result(ax.size()); bh::detail::static_if>( [](py::array_t& result, const auto& ax) { std::transform(ax.begin(), ax.end(), result.mutable_data(), [](const auto& b) { return b.width(); }); }, [](py::array_t& result, const auto& ax) { std::fill(result.mutable_data(), result.mutable_data() + ax.size(), 1.0); }, result, ax); return result; } } // namespace axis // The following list is all types supported using axis_variant = bh::axis::variant; // This saves a little typing using vector_axis_variant = std::vector; namespace pybind11 { namespace detail { /// Register axis_variant as a variant for pybind11 template <> struct type_caster : variant_caster {}; } // namespace detail } // namespace pybind11 scikit-hep-boost-histogram-b4c49b0/include/bh_python/def_eq.hpp000066400000000000000000000031561524660700500246650ustar00rootroot00000000000000// Copyright 2018-2019 Henry Schreiner and Hans Dembinski // // Distributed under the 3-Clause BSD License. See accompanying // file LICENSE or https://github.com/scikit-hep/boost-histogram for details. #pragma once #include /// Compare with the GIL held (the default for def_eq) struct keep_gil {}; /// Add __eq__ and __ne__ to a py::class_, comparing against another Python /// object via A::operator==. Objects that cannot be cast to A compare /// unequal rather than raising. Only operator== is required on A. /// /// Pass py::gil_scoped_release as Gil to compare without the GIL, for a type /// whose operator== is safe that way (a histogram: its axes reacquire the /// GIL themselves for the metadata compare). template py::class_& def_eq(py::class_& cls) { return cls .def("__eq__", [](const A& self, const py::object& other) { try { const A& other_ref = py::cast(other); const Gil gil; (void)gil; return self == other_ref; } catch(const py::cast_error&) { return false; } }) .def("__ne__", [](const A& self, const py::object& other) { try { const A& other_ref = py::cast(other); const Gil gil; (void)gil; return !(self == other_ref); } catch(const py::cast_error&) { return true; } }); } scikit-hep-boost-histogram-b4c49b0/include/bh_python/fill.hpp000066400000000000000000000266521524660700500243760ustar00rootroot00000000000000// Copyright 2018-2019 Hans Dembinski and Henry Schreiner // // Distributed under the 3-Clause BSD License. See accompanying // file LICENSE or https://github.com/scikit-hep/boost-histogram for details. #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace detail { namespace mp11 = boost::mp11; namespace variant = boost::variant2; static_assert( mp11::mp_empty>, mp11::mp_list>>::value, "supported value types are double, int, std::string; " "new axis was added with different value type"); template struct c_array_t : py::array_t { using base_t = py::array_t; using base_t::base_t; using base_t::operator=; std::size_t size() const { return static_cast(base_t::size()); } }; // not actually a numpy array template <> struct c_array_t : std::vector { using base_t = std::vector; using base_t::base_t; using base_t::operator=; // NOLINTNEXTLINE(google-explicit-constructor) c_array_t(base_t&& x) : base_t(std::move(x)) {} c_array_t& operator=(base_t&& x) { base_t::operator=(std::move(x)); return *this; } }; // for int, double template bool is_value(py::handle h) { if(py::isinstance(h) && py::cast(h).ndim() > 0) return false; return PyNumber_Check(h.ptr()); } // for std::string template <> inline bool is_value(py::handle h) { return py::isinstance(h) || (py::isinstance(h) && py::cast(h).ndim() == 0); } template decltype(auto) special_cast(py::handle x) { return py::cast(x); } // allow conversion of dim 0 arrays template <> inline decltype(auto) special_cast(py::handle x) { if(py::isinstance(x)) return py::cast(py::cast(x)); return py::cast(x); } // easier than specializing type_caster for c_array_t template <> inline decltype(auto) special_cast>(py::handle x) { using B = typename c_array_t::base_t; return py::cast(x); } inline bool fits_in_int(std::int64_t v) { return v >= static_cast(std::numeric_limits::min()) && v <= static_cast(std::numeric_limits::max()); } inline bool fits_in_int(std::uint64_t v) { return v <= static_cast(std::numeric_limits::max()); } /// Narrow a wide or unsigned integer array to int, the value type of the integer /// axes. NumPy would wrap out-of-range values silently. template inline c_array_t narrow_to_int(py::handle x) { const py::array_t wide( py::reinterpret_borrow(x)); const std::vector shape(wide.shape(), wide.shape() + wide.ndim()); py::array_t out(shape); const T* in = wide.data(); int* op = out.mutable_data(); for(py::ssize_t i = 0, n = wide.size(); i < n; ++i) { if(!fits_in_int(in[i])) throw py::value_error( "Integer axis values must fit in a 32-bit signed integer"); op[i] = static_cast(in[i]); } return {py::reinterpret_borrow(out)}; } // Make sure float arrays don't get cast to integers (-.5 rounds to 0!) template <> inline decltype(auto) special_cast>(py::handle x) { const auto dtype = py::cast(x).dtype(); const char kind = dtype.kind(); const auto nbytes = dtype.itemsize(); // These always fit in an int, so let NumPy convert them if(kind == 'b' || (kind == 'i' && nbytes <= 4) || (kind == 'u' && nbytes <= 2)) return py::cast>(x); if(kind == 'i') return narrow_to_int(x); if(kind == 'u') return narrow_to_int(x); throw py::type_error("Only integer arrays supported when targeting integer axes"); } // Produce a type error for passing float to int template <> inline decltype(auto) special_cast(py::handle x) { try { return py::cast(x); } catch(std::runtime_error&) { throw py::type_error( "Only integer values supported when targeting integer axes"); } } // The first alternative must stay cheap to default construct; a stack buffer of // these is made for every fill, and only the leading axes.size() are assigned. using arg_t = variant::variant, int, c_array_t, std::string, c_array_t>; using weight_t = variant::variant>; // a sample is either a scalar (broadcast) or a 1D array using sample_t = variant::variant>; inline auto get_vargs(const vector_axis_variant& axes, const py::args& args) { if(args.size() != axes.size()) throw std::invalid_argument("Wrong number of args"); auto vargs = bh::detail::make_stack_buffer(axes); bh::detail::for_each_axis( axes, [args_it = args.begin(), vargs_it = vargs.begin()](const auto& ax) mutable { using T = bh::axis::traits::value_type>; // T is one of: int, double, std::string const auto& x = *args_it++; auto& v = *vargs_it++; if(is_value(x)) { v = special_cast(x); } else { if(py::isinstance(x) && py::cast(x).ndim() != 1) throw std::invalid_argument("All arrays must be 1D"); v = special_cast>(x); } }); return vargs; } inline auto get_weight(py::kwargs& kwargs) { // default constructed as monostate to indicate absence of weight variant::variant> weight; auto w = optional_arg(kwargs, "weight"); if(!w.is_none()) { if(is_value(w)) weight = py::cast(w); else weight = py::cast>(w); } return weight; } inline auto get_sample(const py::handle& s) { sample_t sample; // a scalar sample is broadcast to match the other arguments, like a scalar // axis value or weight if(is_value(s)) { sample = py::cast(s); } else { auto sarray = py::cast>(s); if(sarray.ndim() != 1) throw std::invalid_argument("Sample array must be 1D"); sample = std::move(sarray); } return sample; } // Boost.Histogram < 1.91 reports sample types as const T&, newer versions as T template struct decayed_traits; template struct decayed_traits> { using type = bh::detail::accumulator_traits_holder...>; }; // for accumulators that accept a weight template void fill_impl(bh::detail::accumulator_traits_holder, Histogram& h, const VArgs& vargs, const weight_t& weight, py::kwargs& kwargs) { none_only_arg(kwargs, "sample"); finalize_args(kwargs); // releasing gil here is safe, we don't manipulate refcounts const py::gil_scoped_release lock; variant::visit( overload([&h, &vargs](const variant::monostate&) { h.fill(vargs); }, [&h, &vargs](const auto& w) { h.fill(vargs, bh::weight(w)); }), weight); } // for accumulators that accept a weight and a double template void fill_impl(bh::detail::accumulator_traits_holder, Histogram& h, const VArgs& vargs, const weight_t& weight, py::kwargs& kwargs) { auto s = required_arg(kwargs, "sample"); finalize_args(kwargs); auto sample = get_sample(s); // releasing gil here is safe, we don't manipulate refcounts const py::gil_scoped_release lock; variant::visit( [&h, &vargs, &weight](const auto& sval) { variant::visit(overload( [&h, &vargs, &sval](const variant::monostate&) { h.fill(vargs, bh::sample(sval)); }, [&h, &vargs, &sval](const auto& w) { h.fill(vargs, bh::sample(sval), bh::weight(w)); }), weight); }, sample); } // for multi_cell template void fill_impl(bh::detail::accumulator_traits_holder>, Histogram& h, const VArgs& vargs, const weight_t& weight, py::kwargs& kwargs) { boost::ignore_unused(weight); auto s = required_arg(kwargs, "sample"); finalize_args(kwargs); auto sarray = py::cast>(s); if(sarray.ndim() != 2) throw std::invalid_argument("Sample or weight array for MultiCell must be 2D"); auto buf = sarray.request(); const auto buf_shape0 = static_cast(buf.shape[0]); const auto buf_shape1 = static_cast(buf.shape[1]); // A row that is not nelem wide would throw deep inside the fill loop const auto nelem = bh::unsafe_access::storage(h).nelem(); if(buf_shape1 != nelem) throw std::invalid_argument("Sample or weight array for MultiCell must have " + std::to_string(nelem) + " entries per row, got " + std::to_string(buf_shape1)); // releasing gil here is safe, we don't manipulate refcounts const py::gil_scoped_release lock; auto* src = static_cast(buf.ptr); std::vector> vec_s; vec_s.reserve(buf_shape0); for(std::size_t i = 0; i < buf_shape0; i++) { vec_s.emplace_back(src + (i * buf_shape1), buf_shape1); } h.fill(vargs, bh::sample(vec_s)); } } // namespace detail template Histogram& fill(Histogram& self, const py::args& args, py::kwargs kwargs) { using value_type = typename Histogram::value_type; using traits = typename detail::decayed_traits< bh::detail::accumulator_traits>::type; detail::fill_impl(traits{}, self, detail::get_vargs(bh::unsafe_access::axes(self), args), detail::get_weight(kwargs), kwargs); return self; } scikit-hep-boost-histogram-b4c49b0/include/bh_python/guarded_object.hpp000066400000000000000000000041501524660700500263760ustar00rootroot00000000000000// Copyright 2026 Henry Schreiner and Hans Dembinski // // Distributed under the 3-Clause BSD License. See accompanying // file LICENSE or https://github.com/scikit-hep/boost-histogram for details. #pragma once #include #include #include /// A py::object member for C++ values that are copied and destroyed with the /// GIL released (axes during reduce, project, growing fill): every special /// member that touches reference counts attaches to the interpreter first. /// Moves and the adopting constructor are refcount-neutral, so GIL-free. class guarded_object { py::object obj_; public: guarded_object() noexcept = default; /// Adopt an existing reference; rvalue-only so no copy (incref) can happen /// here, which is why no GIL is needed explicit guarded_object(py::object&& obj) noexcept : obj_(std::move(obj)) {} guarded_object(const guarded_object& other) { const py::gil_scoped_acquire gil; obj_ = other.obj_; } guarded_object(guarded_object&&) noexcept = default; guarded_object& operator=(const guarded_object& other) { if(this != &other) { const py::gil_scoped_acquire gil; obj_ = other.obj_; } return *this; } // Swap defers the decref of the old value to other's guarded destructor guarded_object& operator=(guarded_object&& other) noexcept { std::swap(obj_, other.obj_); return *this; } // Acquiring the GIL can throw if no thread state can be made; leaking the // reference is the only safe option then ~guarded_object() { if(!obj_) return; try { const py::gil_scoped_acquire gil; obj_ = py::object(); } catch(...) { // NOLINT(bugprone-empty-catch) } } /// Access the held object; the caller must hold the GIL to use or copy it const py::object& unguarded_get() const noexcept { return obj_; } /// Mutable access; the caller must hold the GIL to assign through this py::object& unguarded_ref() noexcept { return obj_; } }; scikit-hep-boost-histogram-b4c49b0/include/bh_python/histogram.hpp000066400000000000000000000120231524660700500254300ustar00rootroot00000000000000// Copyright 2018-2019 Henry Schreiner and Hans Dembinski // // Distributed under the 3-Clause BSD License. See accompanying // file LICENSE or https://github.com/scikit-hep/boost-histogram for details. #pragma once #include #include #include #include #include #include #include #include #include namespace pybind11 { /// The descriptor for atomic_* is the same as the descriptor for *, as long this uses /// standard layout template struct format_descriptor> : format_descriptor { static_assert(std::is_standard_layout>::value, ""); }; } // namespace pybind11 namespace detail { template py::buffer_info make_buffer_impl(const Axes& axes, bool flow, T* ptr) { // strides are in bytes auto shape = bh::detail::make_stack_buffer(axes); auto strides = bh::detail::make_stack_buffer(axes); py::ssize_t stride = sizeof(T); unsigned rank = 0; char* start = reinterpret_cast(ptr); bh::detail::for_each_axis(axes, [&](const auto& axis) { const bool underflow = bh::axis::traits::options(axis) & bh::axis::option::underflow; if(!flow && underflow) start += stride; const auto extent = bh::axis::traits::extent(axis); shape[rank] = flow ? extent : axis.size(); strides[rank] = stride; stride *= extent; ++rank; }); return py::buffer_info( start, // Pointer to buffer sizeof(T), // Size of one scalar py::format_descriptor::format(), // Python format descriptor static_cast(rank), // Number of dimensions shape, // Buffer shape strides // Strides (in bytes) for each index ); } struct double_converter { template void operator()(T* tp, Buffer& b) const { b.template make(b.size, tp); } template void operator()(double*, Buffer&) const {} // nothing to do }; } // namespace detail /// Build and return a buffer over the current data. /// Flow controls whether under/over flow bins are present template py::buffer_info make_buffer(bh::histogram>& h, bool flow) { const auto& axes = bh::unsafe_access::axes(h); auto& storage = bh::unsafe_access::storage(h); return detail::make_buffer_impl(axes, flow, &storage[0]); } /// Specialization for unlimited_buffer template py::buffer_info make_buffer(bh::histogram>& h, bool flow) { const auto& axes = bh::unsafe_access::axes(h); auto& storage = bh::unsafe_access::storage(h); // User requested a view into the memory of unlimited storage. We convert // the internal storage to double now to avoid the view becoming invalid // upon changes to the histogram. This is the only way to provide a safe // view, because we cannot automatically update the view when the // underlying memory buffer changes. In practice it is ok, because users // usually want to get the view after filling the histogram, and then the // counts are usually converted to doubles for further processing anyway. auto& buffer = bh::unsafe_access::unlimited_storage_buffer(storage); buffer.visit(detail::double_converter(), buffer); return detail::make_buffer_impl(axes, flow, static_cast(buffer.ptr)); } /// From C++17 template constexpr std::add_const_t& std_as_const(T& t) noexcept { return t; } /// Specialization for multi_cell buffer template py::buffer_info make_buffer(bh::histogram>& h, bool flow) { const auto& axes = bh::unsafe_access::axes(h); auto& storage = bh::unsafe_access::storage(h); using AxesVec = std::decay_t; AxesVec new_axes; // Add the cells as a first pseudo-axis to treat them correctly for the buffer // creation. This will create a buffer in the shape (nelem, axis_1, axis_2, ...) // where nelem is the number of cells per bin This also coincides with how the // cells are stored on the multi cell storage side Having the cells as the // last dimension might feel more natural, but does not work with the current // storage implementation new_axes.emplace_back(axis::integer_none{0, static_cast(storage.nelem())}); new_axes.insert(std::end(new_axes), std::begin(axes), std::end(axes)); return detail::make_buffer_impl( std_as_const(new_axes), flow, static_cast(storage.get_buffer())); } scikit-hep-boost-histogram-b4c49b0/include/bh_python/kwargs.hpp000066400000000000000000000032511524660700500247340ustar00rootroot00000000000000// Copyright 2018-2019 Henry Schreiner and Hans Dembinski // // Distributed under the 3-Clause BSD License. See accompanying // file LICENSE or https://github.com/scikit-hep/boost-histogram for details. #pragma once #include /// Get and remove a value from a keyword argument dict template T required_arg(py::kwargs& kwargs, const char* name) { if(kwargs.contains(name)) { return py::cast(kwargs.attr("pop")(name)); } throw py::key_error(std::string(name) + " is required"); } /// Get and remove a value from a keyword argument dict or return None inline py::object optional_arg(py::kwargs& kwargs, const char* name) { if(kwargs.contains(name)) { return kwargs.attr("pop")(name); } return py::none(); } /// Get and remove a value from a keyword argument dict or return default value template T optional_arg(py::kwargs& kwargs, const char* name, T original_value) { if(kwargs.contains(name)) { return py::cast(kwargs.attr("pop")(name)); } return original_value; } /// Remove a value from a keyword argument dict if None, do not remove if non None /// (triggers final error) inline void none_only_arg(py::kwargs& kwargs, const char* name) { if(kwargs.contains(name)) { if(kwargs[name].is_none()) kwargs.attr("pop")(name); } } /// Run this last; it will provide an error if other keyword are still present inline void finalize_args(const py::kwargs& kwargs) { if(!kwargs.empty()) { auto keys = py::str(", ").attr("join")(kwargs.attr("keys")()); throw py::type_error(py::str("Keyword(s) {0} not expected").format(keys)); } } scikit-hep-boost-histogram-b4c49b0/include/bh_python/make_pickle.hpp000066400000000000000000000311641524660700500257060ustar00rootroot00000000000000// Copyright 2015-2019 Hans Dembinski and Henry Schreiner // // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt // or copy at http://www.boost.org/LICENSE_1_0.txt) // Design notes: This uses the Boost.Serialization mechanism without actually using the // Boost.Serialization library. The Boost mechanism is an algorithm to recursively break // down complex objects into primitives for saving and doing the reverse for loading. // When we save, we are building a tuple of Python primitives with this mechanism and // when we load, we are doing the reserve. Large buffers of primitive types are best // stored by converting them to numpy arrays. This way, we can ride piggy-back on // numpy's pickle code for fast loading and saving. // The Boost approach stores very little meta-data, only a version number. It relies on // the fact that the internal structure of the C++ classes is not changed. When you // actually change this internal structure, you must also increment this version // number and add code to the affected serialize method to load the previous version and // the new version. // // It is very important that there are specializations for all storages with non-trivial // accumulators. A histogram can have very very many cells and the generic serializer // converts each cell individually into Python objects which is extremely time // consuming. The storages should be specialized in the corresponding header file that // includes the storages, by providing a specialization of the save and load functions // // template // void save(Archive& ar, const MyClass& c, unsigned version); // // template // void load(Archive& ar, const MyClass& c, unsigned version); // // or for classes with template arguments, something like // // template // void save(Archive& ar, const MyClass& c, unsigned version); // // template // void load(Archive& ar, MyClass& c, unsigned version); // // in the global namespace. It should not be necessary to touch the code here. #pragma once #include #include #include #include #include #include #include #include // mp_or #include #include #include #include #include #include #include // Available as make_span in boost/core/make_span.hpp in 1.82+ namespace { template constexpr boost::span make_span(T* begin, std::size_t size) noexcept { return boost::span{begin, size}; } } // namespace namespace boost { namespace serialization { // provide default implementation of boost::serialization::version template struct version : std::integral_constant {}; } // namespace serialization } // namespace boost template struct is_string : std::false_type {}; template struct is_string> : std::true_type {}; template struct is_string> : std::true_type {}; template using is_serialization_primitive = typename boost::mp11::mp_or, is_string>::type; template void save(Archive& ar, const T& t, unsigned version) { // default implementation calls serialize method const_cast(t).serialize(ar, version); } template void load(Archive& ar, T& t, unsigned version) { // default implementation calls serialize method static_assert(!static_cast(std::is_const::value), "T must be non-const"); t.serialize(ar, version); } template void split_serialize(std::true_type, Archive& ar, T& t, unsigned version) { load(ar, t, version); } template void split_serialize(std::false_type, Archive& ar, const T& t, unsigned version) { save(ar, t, version); } template void serialize(Archive& ar, T& t, unsigned version) { // default implementation calls serialize method static_assert(!static_cast(std::is_const::value), "T must be non-const"); split_serialize(typename Archive::is_loading{}, ar, t, version); } // builds a tuple of Python primitives from C++ primitives class tuple_oarchive { public: using is_saving = std::true_type; using is_loading = std::false_type; explicit tuple_oarchive(py::list& lst) : lst_(lst) {} template tuple_oarchive& operator&(boost::nvp t) { return operator<<(t.const_value()); } template tuple_oarchive& operator<<(boost::nvp t) { return operator<<(t.const_value()); } template tuple_oarchive& operator&(const T& t) { return operator<<(t); } template std::enable_if_t(is_serialization_primitive::value), tuple_oarchive&> operator<<(const T& t) { // no version number is saved for primitives this->operator<<(py::cast(t)); return *this; } template std::enable_if_t(is_serialization_primitive::value), tuple_oarchive&> operator<<(const T& t) { // save current class version with each non-primitive type const unsigned version = boost::serialization::version::value; this->operator<<(version); serialize(*this, const_cast(t), version); return *this; } tuple_oarchive& operator<<(const py::object& obj) { lst_.append(obj); return *this; } tuple_oarchive& operator<<(py::object& obj) { return operator<<(static_cast(obj)); } tuple_oarchive& operator<<(py::object&& obj) { return operator<<(static_cast(obj)); } // put specializations here that side-step normal serialization // tuple_oarchive& operator<<(py::str& m) { // return operator<<(static_cast(m)); // } tuple_oarchive& operator<<(const py::str& m) { return operator<<(static_cast(m)); } tuple_oarchive& operator<<(const metadata_t& m) { return operator<<(m.unguarded_obj()); } tuple_oarchive& operator<<(const guarded_object& m) { return operator<<(m.unguarded_get()); } template tuple_oarchive& operator<<(const py::array_t& a) { return operator<<(static_cast(a)); } template std::enable_if_t(std::is_arithmetic::value), tuple_oarchive&> operator<<(const std::vector& v) { // fast version for vector of arithmetic types py::array_t const a(static_cast(v.size()), v.data()); this->operator<<(static_cast(a)); return *this; } template std::enable_if_t(std::is_arithmetic::value), tuple_oarchive&> operator<<(const std::vector& v) { // generic version this->operator<<(v.size()); for(auto&& item : v) this->operator<<(item); return *this; } template std::enable_if_t(std::is_arithmetic::value), tuple_oarchive&> operator<<(const bh::detail::array_wrapper& w) { // fast version py::array_t const a(static_cast(w.size), w.ptr); this->operator<<(static_cast(a)); return *this; } template std::enable_if_t(std::is_arithmetic::value), tuple_oarchive&> operator<<(const bh::detail::array_wrapper& w) { // generic version for(auto&& item : ::make_span(w.ptr, w.size)) this->operator<<(item); return *this; } private: py::list& lst_; }; class tuple_iarchive { public: using is_saving = std::false_type; using is_loading = std::true_type; explicit tuple_iarchive(const py::tuple& t) : tup_(t) {} // no object tracking void reset_object_address(const void*, const void*) {}; template tuple_iarchive& operator&(boost::nvp t) { return operator>>(t.value()); } template tuple_iarchive& operator>>(boost::nvp t) { return operator>>(t.value()); } template tuple_iarchive& operator&(T& t) { return operator>>(t); } template std::enable_if_t(is_serialization_primitive::value), tuple_iarchive&> operator>>(T& t) { // no version number is saved for primitives py::object obj; this->operator>>(obj); t = py::cast(obj); return *this; } template std::enable_if_t(is_serialization_primitive::value), tuple_iarchive&> operator>>(T& t) { // load saved class version of each non-primitive type to call legacy code unsigned saved_version; this->operator>>(saved_version); serialize(*this, t, saved_version); return *this; } tuple_iarchive& operator>>(py::object& obj) { BOOST_ASSERT(cur_ < tup_.size()); obj = tup_[cur_++]; return *this; } // put specializations here that side-step normal serialization tuple_iarchive& operator>>(py::str& m) { return operator>>(static_cast(m)); } tuple_iarchive& operator>>(metadata_t& m) { py::object obj; this->operator>>(obj); m = metadata_t{std::move(obj)}; return *this; } tuple_iarchive& operator>>(guarded_object& m) { return operator>>(m.unguarded_ref()); } template tuple_iarchive& operator>>(py::array_t& a) { py::object obj; this->operator>>(obj); // validate that the pickled object really is an array of the right // dtype; make a C-contiguous copy/conversion if needed auto arr = py::array_t::ensure(obj); if(!arr) throw py::value_error("expected a numpy array in pickle data"); a = std::move(arr); return *this; } template std::enable_if_t(std::is_arithmetic::value), tuple_iarchive&> operator>>(std::vector& v) { // fast version for vector of arithmetic types py::array_t a; this->operator>>(a); v.resize(static_cast(a.size())); // sadly we cannot move the memory from the numpy array into the vector std::copy(a.data(), a.data() + a.size(), v.begin()); return *this; } template std::enable_if_t(std::is_arithmetic::value), tuple_iarchive&> operator>>(std::vector& v) { // generic version std::size_t new_size; this->operator>>(new_size); v.resize(new_size); for(auto&& item : v) this->operator>>(item); return *this; } template std::enable_if_t(std::is_arithmetic::value), tuple_iarchive&> operator>>(bh::detail::array_wrapper& w) { // fast version py::array_t a; this->operator>>(a); // buffer wrapped by array_wrapper must already have correct size if(static_cast(a.size()) != w.size) throw std::runtime_error( "array size does not match expected size in pickle data"); // sadly we cannot move the memory from the numpy array into the vector std::copy(a.data(), a.data() + a.size(), w.ptr); return *this; } template std::enable_if_t(std::is_arithmetic::value), tuple_iarchive&> operator>>(bh::detail::array_wrapper& w) { // generic version for(auto&& item : ::make_span(w.ptr, w.size)) this->operator>>(item); return *this; } private: const py::tuple& tup_; std::size_t cur_ = 0; }; /// Make a pickle serializer with a given type template decltype(auto) make_pickle() { return py::pickle( [](const T& obj) { py::list lst; tuple_oarchive oa{lst}; oa << obj; return py::tuple{lst}; }, [](const py::tuple& tup) { tuple_iarchive ia{tup}; T obj; ia >> obj; return obj; }); } scikit-hep-boost-histogram-b4c49b0/include/bh_python/metadata.hpp000066400000000000000000000117561524660700500252270ustar00rootroot00000000000000// Copyright 2018-2019 Henry Schreiner and Hans Dembinski // // Distributed under the 3-Clause BSD License. See accompanying // file LICENSE or https://github.com/scikit-hep/boost-histogram for details. #pragma once #include #include #include #include #include /// Axis metadata: a Python dict, shared with the Python axis wrapper's /// __dict__. The dict is held through guarded_object because axes are copied /// and destroyed with the GIL released (reduce, project, growing fill). class metadata_t { guarded_object data_; // a dict; null only after being moved from public: metadata_t() : data_(make_dict()) {} /// Adopt an existing reference; rvalue-only, so no GIL needed explicit metadata_t(py::object&& data) noexcept : data_(std::move(data)) {} /// Copying an axis must not alias its dict with the source axis, so make /// a fresh shallow copy here rather than sharing guarded_object's /// reference. Code that wants the identical dict (e.g. the Python /// wrapper's raw_metadata getter) must go through unguarded_obj() instead /// of copying a metadata_t. metadata_t(const metadata_t& other) : data_(shallow_copy_dict(other.data_.unguarded_get())) {} metadata_t& operator=(const metadata_t& other) { if(this != &other) data_ = guarded_object(shallow_copy_dict(other.data_.unguarded_get())); return *this; } metadata_t(metadata_t&&) noexcept = default; metadata_t& operator=(metadata_t&&) noexcept = default; ~metadata_t() = default; /// Access the held dict; hold the GIL to use or copy it const py::object& unguarded_obj() const noexcept { return data_.unguarded_get(); } // Boost compares axes in a noexcept context, so these must not throw. bool operator==(const metadata_t& other) const noexcept { try { const py::gil_scoped_acquire gil; try { return unguarded_obj().equal(other.unguarded_obj()); } catch(...) { // pybind11 fetched the Python error into the exception object return item_equal(unguarded_obj(), other.unguarded_obj()); } } catch(...) { return false; } } bool operator!=(const metadata_t& other) const noexcept { return !operator==(other); } private: static py::object make_dict() { const py::gil_scoped_acquire gil; return py::dict(); } /// Compare the dicts value by value, used when dict == dict raised. A /// value whose comparison raises, or gives no truth value (a NumPy array), /// is equal only when it is the same object. static bool item_equal(const py::object& a, const py::object& b) { const auto da = py::reinterpret_borrow(a); const auto db = py::reinterpret_borrow(b); if(da.size() != db.size()) return false; return std::all_of(da.begin(), da.end(), [&db](const auto& item) { if(!db.contains(item.first)) return false; const py::object mine = py::reinterpret_borrow(item.second); const py::object their = db[item.first]; try { return mine.equal(their); } catch(...) { return mine.is(their); } }); } static py::object shallow_copy_dict(const py::object& obj) { const py::gil_scoped_acquire gil; // A moved-from metadata_t holds nothing, and a pickle from 0.10 or // before holds the metadata value itself instead of a dict. There is // no dict to copy in either case, so share the object. if(!obj || !PyDict_Check(obj.ptr())) return obj; return py::reinterpret_steal(PyDict_Copy(obj.ptr())); } }; /// Deepcopy the held dict, for __deepcopy__ implementations inline metadata_t deep_copy_metadata(const metadata_t& m, const py::object& memo) { py::module const copy = py::module::import("copy"); return metadata_t{copy.attr("deepcopy")(m.unguarded_obj(), memo)}; } namespace pybind11 { namespace detail { /// Convert to/from the held dict itself, preserving identity: the same dict /// object is shared between the C++ axis and the Python wrapper's __dict__. template <> struct type_caster { PYBIND11_TYPE_CASTER(metadata_t, const_name("dict")); // start null instead of allocating a dict that load() would discard type_caster() : value{object()} {} bool load(handle src, bool /*convert*/) { if(!isinstance(src)) return false; value = metadata_t{reinterpret_borrow(src)}; return true; } static handle cast(const metadata_t& src, return_value_policy /*policy*/, handle /*parent*/) { return src.unguarded_obj().inc_ref(); } }; } // namespace detail } // namespace pybind11 scikit-hep-boost-histogram-b4c49b0/include/bh_python/multi_cell.hpp000066400000000000000000000245751524660700500256030ustar00rootroot00000000000000#ifndef BOOST_HISTOGRAM_MULTI_CELL_HPP #define BOOST_HISTOGRAM_MULTI_CELL_HPP #include #include #include #include #include #include #include #include #include namespace boost { namespace histogram { template struct multi_cell_base : public BASE { using BASE::BASE; template bool operator==(const S& values) const { if(values.size() != this->size()) return false; return std::equal(this->begin(), this->end(), values.begin()); } template bool operator!=(const S& values) const { return !operator==(values); } }; template struct multi_cell_reference : public multi_cell_base> { // using boost::span::span; using multi_cell_base>::multi_cell_base; void operator()(const boost::span values) { operator+=(values); } void operator+=(const boost::span values) { if(values.size() != this->size()) throw std::range_error("size does not match for += ref"); auto it = this->begin(); for(const T& x : values) *it++ += x; } void operator-=(const boost::span values) { if(values.size() != this->size()) throw std::range_error("size does not match for -= ref"); auto it = this->begin(); for(const T& x : values) *it++ -= x; } template multi_cell_reference& operator=(const S& values) { if(values.size() != this->size()) throw std::range_error("size does not match for = ref"); auto it = this->begin(); for(const T& x : values) *it++ = x; return *this; } }; template struct multi_cell_value : public multi_cell_base> { using multi_cell_base>::multi_cell_base; explicit multi_cell_value(const boost::span values) { this->assign(values.begin(), values.end()); } multi_cell_value() = default; void operator()(const boost::span& values) { operator+=(values); } void operator+=(const boost::span& values) { if(values.size() != this->size()) { if(this->size() > 0) { throw std::range_error("size does not match for += val"); } this->assign(values.begin(), values.end()); return; } auto it = this->begin(); for(const T& x : values) *it++ += x; } void operator-=(const boost::span& values) { if(values.size() != this->size()) { if(this->size() > 0) { throw std::range_error("size does not match for -= val"); } // Subtracting into an uninitialized cell yields the negation. this->resize(values.size()); auto it = this->begin(); for(const T& x : values) *it++ = -x; return; } auto it = this->begin(); for(const T& x : values) *it++ -= x; } template multi_cell_value& operator=(const S values) { this->assign(values.begin(), values.end()); return *this; } }; template class multi_cell { public: using element_type = ElementType; using value_type = multi_cell_value; using reference = multi_cell_reference; using const_reference = const reference; template struct iterator_base : public detail::iterator_adaptor, std::size_t, Reference> { using base_type = detail::iterator_adaptor, std::size_t, Reference>; iterator_base() = default; iterator_base(const iterator_base& other) : iterator_base(other.par_, other.base()) {} iterator_base(MWPtr par, std::size_t idx) : base_type{idx} , par_{par} {} iterator_base& operator=(const iterator_base& other) = default; decltype(auto) operator*() const { return Reference{par_->buffer_.get() + this->base() * par_->nelem_, par_->nelem_}; } MWPtr par_ = nullptr; }; using iterator = iterator_base; using const_iterator = iterator_base; static constexpr bool has_threading_support() { return false; } explicit multi_cell(const std::size_t k = 0) : nelem_{k} {} multi_cell(const multi_cell& other) { *this = other; } multi_cell& operator=(const multi_cell& other) { // Protect against self assignment if(this == &other) { return *this; } nelem_ = other.nelem_; reset(other.size_); std::copy( other.buffer_.get(), other.buffer_.get() + size_ * nelem_, buffer_.get()); return *this; } // Without these the buffer is deep copied on reduce, project and init multi_cell(multi_cell&& other) noexcept : size_{other.size_} , nelem_{other.nelem_} , buffer_{std::move(other.buffer_)} { other.size_ = 0; } multi_cell& operator=(multi_cell&& other) noexcept { if(this != &other) { size_ = other.size_; nelem_ = other.nelem_; buffer_ = std::move(other.buffer_); other.size_ = 0; } return *this; } std::size_t size() const { return size_; } std::size_t nelem() const { return nelem_; } void reset(std::size_t n) { size_ = n; buffer_.reset(new element_type[size_ * nelem_]); default_fill(); } void reset_nelem(std::size_t n) { nelem_ = n; reset(size_); } template ::value, bool> = true> void default_fill() {} template ::value, bool> = true> void default_fill() { std::fill_n(buffer_.get(), size_ * nelem_, 0); } iterator begin() { return {this, 0}; } iterator end() { return {this, size_}; } const_iterator begin() const { return {this, 0}; } const_iterator end() const { return {this, size_}; } reference operator[](std::size_t i) { return reference{buffer_.get() + i * nelem_, nelem_}; } const_reference operator[](std::size_t i) const { return const_reference{buffer_.get() + i * nelem_, nelem_}; } template bool operator==(const multi_cell& other) const { if(size_ * nelem_ != other.size_ * other.nelem_) return false; return std::equal( buffer_.get(), buffer_.get() + size_ * nelem_, other.buffer_.get()); } template bool operator!=(const multi_cell& other) const { return !operator==(other); } template void operator+=(const multi_cell& other) { if(size_ * nelem_ != other.size_ * other.nelem_) { throw std::range_error("size does not match"); } for(std::size_t i = 0; i < size_ * nelem_; i++) { buffer_[i] += other.buffer_[i]; } } template void operator-=(const multi_cell& other) { if(size_ * nelem_ != other.size_ * other.nelem_) { throw std::range_error("size does not match"); } for(std::size_t i = 0; i < size_ * nelem_; i++) { buffer_[i] -= other.buffer_[i]; } } template void serialize(Archive& ar, unsigned /* version */) { ar& make_nvp("size", size_); ar& make_nvp("nelem", nelem_); std::vector w; if(Archive::is_loading::value) { ar& make_nvp("buffer", w); if(w.size() != size_ * nelem_) throw std::runtime_error( "multi_cell: serialized buffer size does not match size * nelem"); reset(size_); std::swap_ranges(buffer_.get(), buffer_.get() + size_ * nelem_, w.data()); } else { w.assign(buffer_.get(), buffer_.get() + size_ * nelem_); ar& make_nvp("buffer", w); } } element_type* get_buffer() { return buffer_.get(); } private: std::size_t size_ = 0; // Number of bins std::size_t nelem_ = 0; // Number of cells per bin std::unique_ptr buffer_; }; template std::ostream& operator<<(std::ostream& os, const multi_cell_value& v) { os << "multi_cell_value("; bool first = true; for(const T& x : v) if(first) { first = false; os << x; } else os << ", " << x; os << ")"; return os; } template std::ostream& operator<<(std::ostream& os, const multi_cell_reference& v) { os << "multi_cell_reference("; bool first = true; for(const T& x : v) if(first) { first = false; os << x; } else os << ", " << x; os << ")"; return os; } template std::ostream& operator<<(std::ostream& os, const multi_cell& v) { os << "multi_cell(\n"; int index = 0; for(const multi_cell_reference& x : v) { os << "Index " << index << ": " << x << "\n"; index++; } os << ")"; return os; } /** * Overload the make_default_impl function to default initialize multi cell storage * with the number of cells per bin from the old storage that is used as a reference. * Otherwise, the number of cells would not be set and subsequent operations with the * new storage would fail as it would be default initialized with 0 cells per bin. */ template multi_cell make_default_impl(const multi_cell& old_storage, float) { using S = multi_cell; return S{old_storage.nelem()}; } } // namespace histogram } // namespace boost #endif scikit-hep-boost-histogram-b4c49b0/include/bh_python/overload.hpp000066400000000000000000000015021524660700500252460ustar00rootroot00000000000000// Copyright 2018-2019 Hans Dembinski and Henry Schreiner // // Distributed under the 3-Clause BSD License. See accompanying // file LICENSE or https://github.com/scikit-hep/boost-histogram for details. #pragma once #include // for std::forward template struct overload_t; template struct overload_t : F { explicit overload_t(F&& f) : F(std::forward(f)) {} using F::operator(); }; template struct overload_t : F, overload_t { explicit overload_t(F&& x, Fs&&... xs) : F(std::forward(x)) , overload_t(std::forward(xs)...) {} using F::operator(); using overload_t::operator(); }; template auto overload(Fs&&... xs) { return overload_t(std::forward(xs)...); } scikit-hep-boost-histogram-b4c49b0/include/bh_python/pybind11.hpp000066400000000000000000000037771524660700500251020ustar00rootroot00000000000000// Copyright 2018-2019 Henry Schreiner and Hans Dembinski // // Distributed under the 3-Clause BSD License. See accompanying // file LICENSE or https://github.com/scikit-hep/boost-histogram for details. // This file must be the first include (that is, it must be before stdlib or boost // headers) This reduces the warning output from Python.h and also lets us define s afew // things #pragma once #include #include #include #include #include #include #include namespace py = pybind11; using namespace pybind11::literals; // For ""_a syntax namespace boost { namespace histogram {} } // namespace boost namespace bh = boost::histogram; /// Static if standin: define a method if expression is true template void def_optionally(T&& module, std::true_type, Args&&... expression) { module.def(std::forward(expression)...); } /// Static if standin: Do nothing if compile time expression is false template void def_optionally(T&&, std::false_type, Args&&...) {} /// Shift to string template std::string shift_to_string(const T& x) { std::ostringstream out; out << x; return out.str(); } template void unchecked_set_impl(std::true_type, py::tuple& tup, py::ssize_t i, Obj&& obj) { // PyTuple_SetItem steals a reference to 'val' if(PyTuple_SetItem(tup.ptr(), i, obj.release().ptr()) != 0) { throw py::error_already_set(); } } template void unchecked_set_impl(std::false_type, py::tuple& tup, py::ssize_t i, T&& t) { unchecked_set_impl(std::true_type{}, tup, i, py::cast(std::forward(t))); } /// Unchecked tuple assign template void unchecked_set(py::tuple& tup, std::size_t i, T&& t) { unchecked_set_impl(std::is_base_of>{}, tup, static_cast(i), std::forward(t)); } scikit-hep-boost-histogram-b4c49b0/include/bh_python/register_accumulator.hpp000066400000000000000000000026141524660700500276630ustar00rootroot00000000000000// Copyright 2018-2019 Henry Schreiner and Hans Dembinski // // Distributed under the 3-Clause BSD License. See accompanying // file LICENSE or https://github.com/scikit-hep/boost-histogram for details. #pragma once #include #include #include #include #include template py::class_ register_accumulator(py::module acc, Args&&... args) { py::class_ cls(std::move(acc), std::forward(args)...); def_eq(cls); return cls .def(py::init<>()) .def(py::self += py::self) .def(py::self *= double()) .def("__add__", [](const A& self, const A& other) { A retval(self); retval += other; return retval; }) // The c++ name is replaced with the Python name here .def("__repr__", [](const py::object& self) { const A& item = py::cast(self); py::str const str = shift_to_string(item); return py::str("{0.__class__.__name__}({1})").format(self, str); }) .def("__copy__", [](const A& self) { return A(self); }) .def("__deepcopy__", [](const A& self, const py::object&) { return A(self); }) .def(make_pickle()); } scikit-hep-boost-histogram-b4c49b0/include/bh_python/register_axis.hpp000066400000000000000000000234541524660700500263150ustar00rootroot00000000000000// Copyright 2018-2019 Henry Schreiner and Hans Dembinski // // Distributed under the 3-Clause BSD License. See accompanying // file LICENSE or https://github.com/scikit-hep/boost-histogram for details. #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef __cpp_noexcept_function_type #define BHP_NOEXCEPT_17 noexcept #else #define BHP_NOEXCEPT_17 #endif template auto vectorize_index(T input) { return py::vectorize(input); } namespace detail { template decltype(auto) axis_cast(py::handle x) { return special_cast(x); } template <> inline decltype(auto) axis_cast(py::handle x) { if(py::isinstance(x)) return py::cast(x); // overflow raises here auto val = py::cast(x); auto ival = static_cast(val); if(static_cast(ival) == val) return ival; throw py::type_error(py::str("cannot cast {} to int").format(val)); } } // namespace detail // we overload vectorize index for the integer axis, whose value type is int template auto vectorize_index(int (bh::axis::integer::*pindex)(int) const BHP_NOEXCEPT_17) { using A = bh::axis::integer; auto indexer = py::vectorize(pindex); return [indexer](const A& self, const py::object& arg) mutable -> py::object { auto array = py::array::ensure(arg); // integer input is range checked; anything else keeps the NumPy cast if(array && (array.dtype().kind() == 'i' || array.dtype().kind() == 'u')) return indexer(&self, detail::special_cast>(array)); return indexer(&self, py::cast>(arg)); }; } // we overload vectorize index for category axis template auto vectorize_index(int (bh::axis::category::*pindex)(const T&) const BHP_NOEXCEPT_17) { return [pindex](const bh::axis::category& self, const py::object& arg) -> py::object { auto index = std::mem_fn(pindex); if(detail::is_value(arg)) { auto index_value = index(self, detail::axis_cast(arg)); if(index_value >= self.size()) throw pybind11::key_error(py::str("{!r} not in axis").format(arg)); return py::cast(index_value); } auto indices = array_like(arg); auto values = detail::special_cast>(arg); auto* ip = indices.mutable_data(); auto vp = values.data(); for(std::size_t i = 0, n = values.size(); i < n; ++i) { ip[i] = index(self, vp[i]); if(ip[i] >= self.size()) throw pybind11::key_error(py::str("{!r} not in axis").format(vp[i])); } return indices; }; } template auto vectorize_value(T input) { return py::vectorize(input); } // we overload vectorize value for category axis template auto vectorize_value(R (bh::axis::category::*pvalue)(int) const) { return [pvalue](const bh::axis::category& self, const py::object& arg) -> py::object { auto value = std::mem_fn(pvalue); if(detail::is_value(arg)) { auto i = py::cast(arg); return i < self.size() ? py::cast(value(self, i)) : py::none(); } auto indices = py::cast>(arg); // this limitation could be removed if we find a way to make object arrays if(indices.ndim() != 1) throw std::invalid_argument("only ndim == 1 supported"); const auto n = static_cast(indices.size()); py::tuple values(n); const auto* pi = indices.data(); for(std::size_t k = 0; k < n; ++k) { const auto i = pi[k]; unchecked_set( values, k, i < self.size() ? py::cast(value(self, i)) : py::none()); } return values; }; } #undef BHP_NOEXCEPT_17 /// Add helpers common to all axis types template py::class_ register_axis(py::module& m, Args&&... args) { py::class_ ax(m, axis::string_name(), std::forward(args)...); def_eq(ax); ax.def("__repr__", &shift_to_string) .def_property_readonly("traits_underflow", [](const A& self) { return static_cast( self.options() & bh::axis::option::underflow); }) .def_property_readonly("traits_overflow", [](const A& self) { return static_cast( self.options() & bh::axis::option::overflow); }) .def_property_readonly("traits_circular", [](const A& self) { return static_cast( self.options() & bh::axis::option::circular); }) .def_property_readonly("traits_growth", [](const A& self) { return static_cast(self.options() & bh::axis::option::growth); }) .def_property_readonly( "traits_continuous", [](const A& self) { return bh::axis::traits::continuous(self); }) .def_property_readonly( "traits_ordered", [](const A& self) { return bh::axis::traits::ordered(self); }) .def_property( "raw_metadata", // Return the actual held dict (not a metadata_t copy, which // would now be an independent dict): callers rely on repeated // .raw_metadata accesses returning the identical object. [](const A& self) -> const py::object& { const py::gil_scoped_acquire gil; return self.metadata().unguarded_obj(); }, [](A& self, metadata_t label) { self.metadata() = std::move(label); }, "Set the metadata") .def_property_readonly( "size", [](const A& ob) { return ob.size(); }, "Returns the number of bins excluding under- and overflow") .def_property_readonly( "extent", &bh::axis::traits::extent, "Returns the number of bins including under- and overflow") .def("__copy__", [](const A& self) { return A(self); }) .def("__deepcopy__", [](const A& self, const py::object& memo) { auto a = std::make_unique(self); a->metadata() = deep_copy_metadata(a->metadata(), memo); return a; }) .def( "bin", [](const A& ax, int i) { const bh::axis::index_type begin = bh::axis::traits::get_options::test( bh::axis::option::underflow) ? -1 : 0; const bh::axis::index_type end = bh::axis::traits::get_options::test(bh::axis::option::overflow) ? ax.size() + 1 : ax.size(); if(begin <= i && i < end) return axis::unchecked_bin(ax, i); throw py::index_error(); }, "i"_a, "Return bin at index (-1 accesses underflow bin, size access overflow)") .def( "__iter__", [](const A& ax) { struct iterator : bh::detail::iterator_adaptor { const A& axis_; iterator(const A& axis, int idx) : iterator::iterator_adaptor_(idx) , axis_(axis) {} auto operator*() const { return axis::unchecked_bin(axis_, this->base()); } }; iterator begin(ax, 0); iterator end(ax, ax.size()); return py::make_iterator(std::move(begin), std::move(end)); }, py::keep_alive<0, 1>()) // This is a property because we hide the flow bins consistently here. // Users should use histogram.to_numpy(True) to get a consistent representation // of edges and the cell matrix, where we can guarantee that both are in sync. .def_property_readonly( "edges", [](const A& ax) { return axis::edges(ax, false); }, "Return bin edges") .def_property_readonly("centers", &axis::centers, "Return bin centers") .def_property_readonly("widths", &axis::widths, "Return bin widths") .def("index", vectorize_index(&A::index), "Index for value (or values) on the axis") .def("value", vectorize_value(&A::value), "Value at index (or indices)") .def(make_pickle()); return ax; } scikit-hep-boost-histogram-b4c49b0/include/bh_python/register_histogram.hpp000066400000000000000000000446401524660700500273460ustar00rootroot00000000000000// Copyright 2018-2019 Henry Schreiner and Hans Dembinski // // Distributed under the 3-Clause BSD License. See accompanying // file LICENSE or https://github.com/scikit-hep/boost-histogram for details. #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /// A growth axis is replaced or reallocated by a growing fill or merge, so a /// reference to it would dangle. Give a copy instead; its bins are a snapshot. /// Other axes stay no-copy, held alive by py::keep_alive on the histogram. template py::object growth_safe_axis_cast(const A& item, std::true_type /*growing*/) { py::object obj = py::cast(item, py::return_value_policy::copy); // The copy stands in for the stored axis, so it must hold the same // metadata dict, not the independent one that copying a metadata_t makes. py::cast(obj).metadata() = metadata_t{py::object(item.metadata().unguarded_obj())}; return obj; } template py::object growth_safe_axis_cast(const A& item, std::false_type /*growing*/) { return py::cast(item, py::return_value_policy::reference); } template using axis_is_growing = std::integral_constant::test( bh::axis::option::growth)>; /// In-place operator functors. The member template is only instantiated by /// the std::true_type def_inplace_op, so a storage without the operator /// still compiles. struct op_iadd { template void operator()(H& a, const H& b) const { a += b; } }; struct op_isub { template void operator()(H& a, const H& b) const { a -= b; } }; struct op_imul { template void operator()(H& a, const H& b) const { a *= b; } }; struct op_itruediv { template void operator()(H& a, const H& b) const { a /= b; } }; /// Define an in-place histogram operator that runs without the GIL and gives /// back the same Python object (a registered instance is returned as is). template void def_inplace_op(py::class_& hist, std::true_type, const char* name, Op) { hist.def( name, [](H& self, const H& other) -> H& { const py::gil_scoped_release release; Op{}(self, other); return self; }, py::is_operator(), py::return_value_policy::reference); } template void def_inplace_op(py::class_&, std::false_type, const char*, Op) {} /// Whole-buffer operations run with the GIL released. The storage is plain /// C++; the axes hold metadata through guarded_object, which takes the GIL /// itself for the reference counting a copy or compare needs. template void def_buffer_ops(py::class_& hist) { def_eq(hist); hist.def("reset", [](H& self) { const py::gil_scoped_release release; self.reset(); }) .def("__copy__", [](const H& self) { const py::gil_scoped_release release; return H(self); }) .def("__deepcopy__", [](const H& self, const py::object& memo) { auto a = [&self] { const py::gil_scoped_release release; return std::make_unique(self); }(); for(unsigned i = 0; i < a->rank(); i++) { bh::unsafe_access::axis(*a, i).metadata() = deep_copy_metadata(a->axis(i).metadata(), memo); } return a; }); def_inplace_op(hist, std::true_type{}, "__iadd__", op_iadd{}); def_inplace_op(hist, bh::detail::has_operator_rsub{}, "__isub__", op_isub{}); def_inplace_op(hist, bh::detail::has_operator_rmul{}, "__imul__", op_imul{}); def_inplace_op( hist, bh::detail::has_operator_rdiv{}, "__itruediv__", op_itruediv{}); } template auto register_histogram(py::module& m, const char* name, const char* desc) { using histogram_t = bh::histogram; using value_type = typename histogram_t::value_type; py::class_ hist(m, name, desc, py::buffer_protocol()); def_buffer_ops(hist); hist.def(py::init(), "axes"_a, "storage"_a = S()) .def_buffer( [](histogram_t& h) -> py::buffer_info { return make_buffer(h, false); }) .def("rank", &histogram_t::rank) .def("size", &histogram_t::size) .def_property_readonly_static( "_storage_type", [](const py::object&) { return py::type::of(); }) .def( "to_numpy", [](histogram_t& h, bool flow) { py::tuple tup(1 + h.rank()); // Add the histogram buffer as the first argument unchecked_set(tup, 0, py::array(make_buffer(h, flow))); // Add the axis edges h.for_each_axis([&tup, flow, i = 0U](const auto& ax) mutable { unchecked_set(tup, ++i, axis::edges(ax, flow, true)); }); return tup; }, "flow"_a = false) .def( "view", [](const py::object& self, bool flow) { auto& h = py::cast(self); return py::array(make_buffer(h, flow), self); }, "flow"_a = false) .def( "axis", [](const histogram_t& self, int i) -> py::object { unsigned const ii = i < 0 ? self.rank() - static_cast(std::abs(i)) : static_cast(i); if(ii < self.rank()) { const axis_variant& var = self.axis(ii); return bh::axis::visit( [](auto&& item) -> py::object { using item_t = std::decay_t; return growth_safe_axis_cast(item, axis_is_growing{}); }, var); } throw std::out_of_range("The axis value must be less than the rank"); }, "i"_a = 0, py::keep_alive<0, 1>()) // Setting metadata through axis() would only reach the copy handed out // for a growth axis, so set it on the stored axis instead. .def("_set_axis_metadata", [](histogram_t& self, unsigned i, metadata_t data) { if(i >= self.rank()) throw std::out_of_range( "The axis value must be less than the rank"); bh::unsafe_access::axis(self, i).metadata() = std::move(data); }) .def("at", [](const histogram_t& self, const py::args& args) -> value_type { auto int_args = py::cast>(args); return self.at(int_args); }) .def("_at_set", [](histogram_t& self, const value_type& input, const py::args& args) { auto int_args = py::cast>(args); self.at(int_args) = input; }) .def("__repr__", &shift_to_string) .def( "sum", [](const histogram_t& self, bool flow) { const py::gil_scoped_release release; // A rank-0 histogram has no flow bins, so inner == all. Use // all to avoid Boost's indexed range, which is UB for rank-0 // (it reads uninitialized per-axis state); the all path uses // plain iteration instead. const auto cov = (flow || self.rank() == 0) ? bh::coverage::all : bh::coverage::inner; return bh::algorithm::sum(self, cov); }, "flow"_a = false) .def( "empty", [](const histogram_t& self, bool flow) { const py::gil_scoped_release release; if(self.rank() == 0) { // algorithm::empty drives the same rank-0-UB indexed range; // check the single cell directly instead. using value_type = typename histogram_t::value_type; return !(*self.begin() != value_type()); } return bh::algorithm::empty( self, flow ? bh::coverage::all : bh::coverage::inner); }, "flow"_a = false) .def("reduce", [](const histogram_t& self, const py::args& args) -> histogram_t { auto commands = py::cast>(args); // reduce drives the same rank-0-UB indexed range; with no // commands there is nothing to do, and any axis index is // rejected before that point. if(self.rank() == 0 && commands.empty()) return self; const py::gil_scoped_release release; return bh::algorithm::reduce(self, commands); }) .def("project", [](const histogram_t& self, const py::args& values) -> histogram_t { auto cpp_values = py::cast>(values); // Same rank-0-UB indexed range; the identity is the only // projection a rank-0 histogram has. if(self.rank() == 0 && cpp_values.empty()) return self; const py::gil_scoped_release release; return bh::algorithm::project(self, cpp_values); }) .def("fill", &fill) .def(make_pickle()) ; return hist; } template <> auto inline register_histogram>(py::module& m, const char* name, const char* desc) { using S = bh::multi_cell; using histogram_t = bh::histogram; using value_type = std::vector; py::class_ hist(m, name, desc, py::buffer_protocol()); def_buffer_ops(hist); hist.def(py::init(), "axes"_a, "storage"_a = S()) .def_buffer( [](histogram_t& h) -> py::buffer_info { return make_buffer(h, false); }) .def("rank", &histogram_t::rank) .def("size", &histogram_t::size) .def("nelem", [](const histogram_t& self) { return bh::unsafe_access::storage(self).nelem(); }) // Reset number of cells per bin after recreation of histogram because number // of cells can (?) not be passed to the creation of the new histogram. Set it // manually afterwards. .def("reset_nelem", [](histogram_t& self, const std::size_t nelem) { bh::unsafe_access::storage(self).reset_nelem(nelem); }) .def_property_readonly_static( "_storage_type", [](const py::object&) { return py::type::of(); }) .def( "to_numpy", [](histogram_t& h, bool flow) { py::tuple tup(1 + h.rank()); // Add the histogram buffer as the first argument unchecked_set(tup, 0, py::array(make_buffer(h, flow))); // Add the axis edges h.for_each_axis([&tup, flow, i = 0U](const auto& ax) mutable { unchecked_set(tup, ++i, axis::edges(ax, flow, true)); }); return tup; }, "flow"_a = false) .def( "view", [](const py::object& self, bool flow) { auto& h = py::cast(self); return py::array(make_buffer(h, flow), self); }, "flow"_a = false) .def( "axis", [](const histogram_t& self, int i) -> py::object { unsigned const ii = i < 0 ? self.rank() - static_cast(std::abs(i)) : static_cast(i); if(ii < self.rank()) { const axis_variant& var = self.axis(ii); return bh::axis::visit( [](auto&& item) -> py::object { using item_t = std::decay_t; return growth_safe_axis_cast(item, axis_is_growing{}); }, var); } throw std::out_of_range("The axis value must be less than the rank"); }, "i"_a = 0, py::keep_alive<0, 1>()) // Setting metadata through axis() would only reach the copy handed out // for a growth axis, so set it on the stored axis instead. .def("_set_axis_metadata", [](histogram_t& self, unsigned i, metadata_t data) { if(i >= self.rank()) throw std::out_of_range( "The axis value must be less than the rank"); bh::unsafe_access::axis(self, i).metadata() = std::move(data); }) .def("at", [](const histogram_t& self, const py::args& args) -> value_type { auto int_args = py::cast>(args); auto at_value = self.at(int_args); return {at_value.begin(), at_value.end()}; }) .def("_at_set", [](histogram_t& self, const value_type& input, const py::args& args) { auto int_args = py::cast>(args); self.at(int_args) = input; }) .def("__repr__", &shift_to_string) .def( "sum", [](const histogram_t& self, bool flow) -> value_type { const py::gil_scoped_release release; // rank-0 inner coverage drives Boost's indexed range, which is // UB for rank-0; all coverage is equivalent (no flow bins) and // uses plain iteration. const auto cov = (flow || self.rank() == 0) ? bh::coverage::all : bh::coverage::inner; value_type result = bh::algorithm::sum(self, cov); // A histogram with zero bins has no cells to accumulate, so the // default-constructed accumulator stays empty. Return a // zero-filled vector of length nelem so the result shape is // consistent with the non-empty case. if(result.empty()) { result.assign(bh::unsafe_access::storage(self).nelem(), 0.0); } return result; }, "flow"_a = false) .def( "empty", [](const histogram_t& self, bool flow) { const py::gil_scoped_release release; // A cell is empty when every element is zero; algorithm::empty // compares against a default-constructed (zero length) cell, // which never matches. rank 0 also drives the indexed range, // which is UB for rank-0 histograms. const auto all_zero = [](const auto& cell) { return std::all_of( cell.begin(), cell.end(), [](double x) { return x == 0; }); }; if(flow || self.rank() == 0) return std::all_of(self.begin(), self.end(), all_zero); auto range = bh::indexed(self); return std::all_of(range.begin(), range.end(), [&all_zero](const auto& x) { return all_zero(*x); }); }, "flow"_a = false) .def("reduce", [](const histogram_t& self, const py::args& args) -> histogram_t { auto commands = py::cast>(args); // reduce drives the same rank-0-UB indexed range; with no // commands there is nothing to do, and any axis index is // rejected before that point. if(self.rank() == 0 && commands.empty()) return self; const py::gil_scoped_release release; return bh::algorithm::reduce(self, commands); }) .def("project", [](const histogram_t& self, const py::args& values) -> histogram_t { auto cpp_values = py::cast>(values); // Same rank-0-UB indexed range; the identity is the only // projection a rank-0 histogram has. if(self.rank() == 0 && cpp_values.empty()) return self; const py::gil_scoped_release release; return bh::algorithm::project(self, cpp_values); }) .def("fill", &fill) .def(make_pickle()) ; return hist; } scikit-hep-boost-histogram-b4c49b0/include/bh_python/register_storage.hpp000066400000000000000000000046101524660700500270060ustar00rootroot00000000000000// Copyright 2018-2019 Henry Schreiner and Hans Dembinski // // Distributed under the 3-Clause BSD License. See accompanying // file LICENSE or https://github.com/scikit-hep/boost-histogram for details. #pragma once #include #include #include #include #include #include /// Add helpers common to all storage types template py::class_ register_storage(py::module& m, const char* name, const char* desc) { py::class_ storage(m, name, desc); def_eq(storage); storage.def(py::init<>()) .def(make_pickle()) .def("__copy__", [](const A& self) { return A(self); }) .def("__deepcopy__", [](const A& self, const py::object&) { return A(self); }); return storage; } /// Add helpers to the unlimited storage type template <> py::class_ inline register_storage(py::module& m, const char* name, const char* desc) { using A = storage::unlimited; // match code above py::class_ storage(m, name, desc); def_eq(storage); storage.def(py::init<>()) .def(make_pickle()) .def("__copy__", [](const A& self) { return A(self); }) .def("__deepcopy__", [](const A& self, const py::object&) { return A(self); }); return storage; } /// Add helpers to the multi_cell storage type template <> py::class_ inline register_storage(py::module& m, const char* name, const char* desc) { using A = storage::multi_cell; // match code above py::class_ storage(m, name, desc); def_eq(storage); storage .def(py::init([](int k) { if(k < 1) throw std::invalid_argument("MultiCell nelem must be 1 or larger"); return A{static_cast(k)}; }), py::arg("k")) .def(make_pickle()) .def("__copy__", [](const A& self) { return A(self); }) .def("__deepcopy__", [](const A& self, const py::object&) { return A(self); }) .def_property_readonly("nelem", [](const A& self) { return self.nelem(); }) ; return storage; } scikit-hep-boost-histogram-b4c49b0/include/bh_python/regular_numpy.hpp000066400000000000000000000025211524660700500263260ustar00rootroot00000000000000// Copyright 2018-2019 Henry Schreiner and Hans Dembinski // // Distributed under the 3-Clause BSD License. See accompanying // file LICENSE or https://github.com/scikit-hep/boost-histogram for details. #pragma once #include #include #include #include namespace bh = boost::histogram; namespace axis { using index_type = boost::histogram::axis::index_type; /// Mimics the numpy behavior for the upper bin; required for auto range. class regular_numpy : public bh::axis::regular { using value_type = double; double stop_{0}; public: regular_numpy(unsigned n, value_type start, value_type stop, metadata_t meta = {}) : regular(n, start, stop, std::move(meta)) , stop_(stop) {} regular_numpy(const regular_numpy& a, index_type i, index_type j, unsigned n) : regular(a, i, j, n) , stop_(a.stop_) {} regular_numpy() = default; bh::axis::index_type index(value_type v) const { return v <= stop_ ? std::min(regular::index(v), size() - 1) : regular::index(v); } template void serialize(Archive& ar, unsigned version) { regular::serialize(ar, version); ar& boost::make_nvp("stop", stop_); } }; } // namespace axis scikit-hep-boost-histogram-b4c49b0/include/bh_python/storage.hpp000066400000000000000000000164331524660700500251100ustar00rootroot00000000000000// Copyright 2018-2019 Henry Schreiner and Hans Dembinski // // Distributed under the 3-Clause BSD License. See accompanying // file LICENSE or https://github.com/scikit-hep/boost-histogram for details. #pragma once #include #include #include #include #include #include #include #include #include #include #include #include namespace storage { // Names match Python names using int64 = bh::dense_storage; using atomic_int64 = bh::dense_storage>; using double_ = bh::dense_storage; using unlimited = bh::unlimited_storage<>; using weight = bh::dense_storage>; using multi_cell = bh::multi_cell; using mean = bh::dense_storage>; using weighted_mean = bh::dense_storage>; // Allow repr to show python name template inline const char* name() { return "unknown"; } template <> inline const char* name() { return "int64"; } template <> inline const char* name() { return "atomic_int64"; } template <> inline const char* name() { return "double"; } template <> inline const char* name() { return "unlimited"; } template <> inline const char* name() { return "weight"; } template <> inline const char* name() { return "multi_cell"; } template <> inline const char* name() { return "mean"; } template <> inline const char* name() { return "weighted_mean"; } } // namespace storage // It is very important that storages with accumulators have specialized serialization. // No specializations needed for dense_storage or unlimited_storage. template void save(Archive& ar, const storage::atomic_int64& s, unsigned /* version */) { // We cannot view the memory as a numpy array, because the internal layout of // std::atomic is undefined. So no reinterpret_casts are allowed. py::array_t a(static_cast(s.size())); auto in_ptr = s.begin(); auto* out_ptr = a.mutable_data(); for(; in_ptr != s.end(); ++in_ptr, ++out_ptr) *out_ptr = in_ptr->value(); ar << a; } template void load(Archive& ar, storage::atomic_int64& s, unsigned /* version */) { // data is stored as flat numpy array py::array_t a; ar >> a; s.resize(static_cast(a.size())); // sadly we cannot move the memory from the numpy array into the vector std::copy(a.data(), a.data() + a.size(), s.data()); } template void save(Archive& ar, const bh::dense_storage>& s, unsigned /* version */) { using T = accumulators::weighted_sum; static_assert(std::is_standard_layout::value && std::is_trivially_copyable::value && sizeof(T) == 2 * sizeof(double), "weighted_sum cannot be fast serialized"); // view storage buffer as flat numpy array py::array_t const a(static_cast(s.size()) * 2, reinterpret_cast(s.data())); ar << a; } template void load(Archive& ar, bh::dense_storage>& s, unsigned /* version */) { // data is stored as flat numpy array py::array_t a; ar >> a; if(a.size() % 2 != 0) throw std::runtime_error( "weighted_sum: serialized buffer size is not a multiple of 2"); s.resize(static_cast(a.size() / 2)); // sadly we cannot move the memory from the numpy array into the vector std::copy(a.data(), a.data() + a.size(), reinterpret_cast(s.data())); } template void save(Archive& ar, const bh::dense_storage>& s, unsigned /* version */) { using T = accumulators::mean; static_assert(std::is_standard_layout::value && std::is_trivially_copyable::value && sizeof(T) == 3 * sizeof(double), "mean cannot be fast serialized"); // view storage buffer as flat numpy array py::array_t const a(static_cast(s.size()) * 3, reinterpret_cast(s.data())); ar << a; } template void load(Archive& ar, bh::dense_storage>& s, unsigned /* version */) { // data is stored as flat numpy array py::array_t a; ar >> a; if(a.size() % 3 != 0) throw std::runtime_error("mean: serialized buffer size is not a multiple of 3"); s.resize(static_cast(a.size() / 3)); // sadly we cannot move the memory from the numpy array into the vector std::copy(a.data(), a.data() + a.size(), reinterpret_cast(s.data())); } template void save(Archive& ar, const bh::dense_storage>& s, unsigned /* version */) { using T = accumulators::weighted_mean; static_assert(std::is_standard_layout::value && std::is_trivially_copyable::value && sizeof(T) == 4 * sizeof(double), "weighted_mean cannot be fast serialized"); // view storage buffer as flat numpy array py::array_t const a(static_cast(s.size()) * 4, reinterpret_cast(s.data())); ar << a; } template void load(Archive& ar, bh::dense_storage>& s, unsigned /* version */) { // data is stored as flat numpy array py::array_t a; ar >> a; if(a.size() % 4 != 0) throw std::runtime_error( "weighted_mean: serialized buffer size is not a multiple of 4"); s.resize(static_cast(a.size() / 4)); // sadly we cannot move the memory from the numpy array into the vector std::copy(a.data(), a.data() + a.size(), reinterpret_cast(s.data())); } namespace pybind11 { namespace detail { /// Allow a Python int to implicitly convert to an atomic int in C++ template <> struct type_caster { PYBIND11_TYPE_CASTER(storage::atomic_int64::value_type, _("atomic_int64")); bool load(handle src, bool) { auto* ptr = PyNumber_Long(src.ptr()); if(ptr == nullptr) return false; value = PyLong_AsLongLong(ptr); Py_DECREF(ptr); return PyErr_Occurred() == nullptr; } static handle cast(const storage::atomic_int64::value_type& src, return_value_policy /* policy */, handle /* parent */) { return PyLong_FromLongLong(src.value()); } }; } // namespace detail } // namespace pybind11 scikit-hep-boost-histogram-b4c49b0/include/bh_python/transform.hpp000066400000000000000000000156101524660700500254530ustar00rootroot00000000000000// Copyright 2018-2019 Henry Schreiner and Hans Dembinski // // Distributed under the 3-Clause BSD License. See accompanying // file LICENSE or https://github.com/scikit-hep/boost-histogram for details. #pragma once #include #include #include #include #include #include #include namespace bh = boost::histogram; struct func_transform { using raw_t = double(double); // guarded_object because axes are copied and destroyed with the GIL // released (reduce, project) raw_t* _forward = nullptr; raw_t* _inverse = nullptr; guarded_object _forward_ob; // Held for reference counting, repr, and pickling guarded_object _inverse_ob; guarded_object _forward_converted; // Held for reference counting if conversion // makes a new object (ctypes does not bump // the refcount) guarded_object _inverse_converted; guarded_object _convert_ob; // Called before computing transform if not None guarded_object _name; // Optional name str (uses repr from objects otherwise) /// Convert an object into a std::function. Can handle ctypes /// function pointers and pybind11 C++ functions, or anything /// else with a defined convert function std::tuple compute(const py::object& input) const { // Run the conversion function on the input (unless conversion is None) py::object const tmp_src = _convert_ob.unguarded_get().is_none() ? input : _convert_ob.unguarded_get()(input); // If a CTypes object is present, just use that (numba, for example) py::object const src = py::getattr(tmp_src, "ctypes", tmp_src); // import ctypes py::module const ctypes = py::module::import("ctypes"); // Get the type: double(double) // function_type = ctypes.CFUNCTYPE(ctypes.c_double, ctypes.c_double) py::handle const CFUNCTYPE = ctypes.attr("CFUNCTYPE"); py::handle const c_double = ctypes.attr("c_double"); py::object const function_type = CFUNCTYPE(c_double, c_double); if(py::isinstance(src, function_type)) { py::handle const cast = ctypes.attr("cast"); py::handle const c_void_p = ctypes.attr("c_void_p"); // ctypes.cast(in, ctypes.c_void_p).value py::object const addr_obj = cast(src, c_void_p); auto addr = py::cast(addr_obj.attr("value")); auto ptr = reinterpret_cast(addr); // NOLINT(performance-no-int-to-ptr) return std::make_tuple(ptr, src); } // If we made it to this point, we probably have a C++ pybind object or an // invalid object. The following is based on the std::function conversion in // pybind11/functional.hpp if(!py::isinstance(src)) throw py::type_error("Only ctypes double(double) and C++ functions allowed " "(must be function)"); py::detail::make_caster> func_caster; if(!func_caster.load(src, /*convert*/ false)) { // Note that each error is slightly different just to help with debugging throw py::type_error("Only ctypes double(double) and C++ functions allowed " "(must be stateless)"); } auto func = static_cast&>(func_caster); auto* cfunc = func.target(); if(cfunc == nullptr) { throw py::type_error( "Retrieving double(double) function failed (must be stateless)"); } return std::make_tuple(*cfunc, src); } func_transform(py::object f, py::object i, py::object c, py::str n) : _forward_ob{std::move(f)} , _inverse_ob{std::move(i)} , _convert_ob{std::move(c)} , _name{std::move(n)} { std::tie(_forward, _forward_converted.unguarded_ref()) = compute(_forward_ob.unguarded_get()); std::tie(_inverse, _inverse_converted.unguarded_ref()) = compute(_inverse_ob.unguarded_get()); } func_transform() = default; double forward(double x) const { return _forward(x); } double inverse(double x) const { return _inverse(x); } bool operator==(const func_transform& other) const noexcept { try { const py::gil_scoped_acquire gil; return _forward_ob.unguarded_get().equal(other._forward_ob.unguarded_get()) && _inverse_ob.unguarded_get().equal( other._inverse_ob.unguarded_get()); } catch(const std::exception&) { return false; } } bool operator!=(const func_transform& other) const noexcept { return !(*this == other); } template void serialize(Archive& ar, unsigned /* version */) { ar& boost::make_nvp("forward", _forward_ob); ar& boost::make_nvp("inverse", _inverse_ob); ar& boost::make_nvp("convert", _convert_ob); ar& boost::make_nvp("name", _name); if(Archive::is_loading::value) { std::tie(_forward, _forward_converted.unguarded_ref()) = compute(_forward_ob.unguarded_ref()); std::tie(_inverse, _inverse_converted.unguarded_ref()) = compute(_inverse_ob.unguarded_ref()); } } }; namespace boost { namespace histogram { namespace detail { inline const char* axis_suffix(const ::func_transform&) { return "_trans"; } } // namespace detail } // namespace histogram } // namespace boost /// Simple deep copy for any class *without* a python component template T deep_copy(const T& input, py::object&) { return T(input); } /// Specialization for the case where Python components are present /// (Function transform in this case) template <> inline func_transform deep_copy(const func_transform& input, py::object& memo) { py::module const copy = py::module::import("copy"); py::object const forward = copy.attr("deepcopy")(input._forward_ob.unguarded_get(), memo); py::object const inverse = copy.attr("deepcopy")(input._inverse_ob.unguarded_get(), memo); py::object const convert = copy.attr("deepcopy")(input._convert_ob.unguarded_get(), memo); py::str const name = copy.attr("deepcopy")(input._name.unguarded_get(), memo); return {forward, inverse, convert, name}; } // Print in repr template std::basic_ostream& operator<<(std::basic_ostream& os, const func_transform&) { return os << "func_transform"; } scikit-hep-boost-histogram-b4c49b0/include/bh_python/vector_string_caster.hpp000066400000000000000000000071311524660700500276700ustar00rootroot00000000000000// Copyright 2018-2019 Hans Dembinski and Henry Schreiner // // Distributed under the 3-Clause BSD License. See accompanying // file LICENSE or https://github.com/scikit-hep/boost-histogram for details. #pragma once #include #include #include #include namespace pybind11 { namespace detail { template <> struct type_caster> : list_caster, std::string> { using base_t = list_caster, std::string>; bool load(handle src, bool convert) { if(isinstance(src)) { auto arr = reinterpret_borrow(src); const auto kind = arr.dtype().kind(); if(kind == 'S' || kind == 'U') { // the loaders below assume tightly packed (C-contiguous) data, // so make a contiguous copy if needed (e.g. sliced arrays) if((arr.flags() & array::c_style) == 0) { arr = array::ensure(arr, array::c_style); if(!arr) return false; } return kind == 'S' ? load_from_array_s(arr) : load_from_array_u(arr); } } return base_t::load(src, convert); } template static std::size_t strlen(const T* p, std::size_t nmax) { std::size_t n = 0; for(; n < nmax && p[n] != 0; ++n) ; return n; } bool load_from_array_s(const array& src) { const auto step = static_cast(src.itemsize()); const auto size = static_cast(src.size()); const auto* p = static_cast(src.data()); value.clear(); value.reserve(size); for(std::size_t i = 0; i < size; p += step, ++i) value.emplace_back(p, strlen(p, step)); return true; } // encode one UCS-4 code point as UTF-8 and append it to s static void append_utf8(std::string& s, std::uint32_t cp) { if(cp < 0x80) { s.push_back(static_cast(cp)); } else if(cp < 0x800) { s.push_back(static_cast(0xC0 | (cp >> 6))); s.push_back(static_cast(0x80 | (cp & 0x3F))); } else if(cp < 0x10000) { s.push_back(static_cast(0xE0 | (cp >> 12))); s.push_back(static_cast(0x80 | ((cp >> 6) & 0x3F))); s.push_back(static_cast(0x80 | (cp & 0x3F))); } else { s.push_back(static_cast(0xF0 | (cp >> 18))); s.push_back(static_cast(0x80 | ((cp >> 12) & 0x3F))); s.push_back(static_cast(0x80 | ((cp >> 6) & 0x3F))); s.push_back(static_cast(0x80 | (cp & 0x3F))); } } bool load_from_array_u(const array& src) { const auto step = static_cast(src.itemsize()) / sizeof(std::uint32_t); const auto size = static_cast(src.size()); const auto* p = static_cast(src.data()); value.clear(); value.reserve(size); for(std::size_t i = 0; i < size; p += step, ++i) { // numpy 'U' dtype stores each character as a UCS-4 code point; // encode to UTF-8, keeping the fast path for pure ASCII const auto n = strlen(p, step); std::string s; s.reserve(n); for(std::size_t j = 0; j < n; ++j) append_utf8(s, p[j]); value.emplace_back(std::move(s)); } return true; } }; } // namespace detail } // namespace pybind11 scikit-hep-boost-histogram-b4c49b0/notebooks/000077500000000000000000000000001524660700500213125ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/notebooks/BoostHistogramHandsOn.ipynb000066400000000000000000015473351524660700500266160ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Using boost-histogram\n" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import functools\n", "import operator\n", "\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", "\n", "import boost_histogram as bh" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1: Basic 1D histogram\n", "\n", "Let's start with the basics. We will create a histogram using boost-histogram and fill it." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 1.1: Data\n", "\n", "Let's make a 1d dataset to run on." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "data1 = np.random.normal(3.5, 2.5, size=1_000_000)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, let's make a histogram" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "hist1 = bh.Histogram(bh.axis.Regular(40, -2, 10))" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Histogram(Regular(40, -2, 10), storage=Double()) # Sum: 981542.0 (1000000.0 with flow)" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "hist1.fill(data1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can see that the histogram has been filled. Let's explicitly check to see how many entries are in the histogram:" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "981542.0" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "hist1.sum()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "What happened to the missing items? They are in the underflow and overflow bins:" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "1000000.0" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "hist1.sum(flow=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Like ROOT, we have overflow bins by default. We can turn them off, but they enable some powerful things like projections.\n", "\n", "Let's plot this (Hist should make this easier):" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "image/png": { "height": 250, "width": 388 }, "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "plt.bar(hist1.axes[0].centers, hist1.values(), width=hist1.axes[0].widths);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note: you can select the axes before or after calling `.centers`; this is very useful for ND histograms." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "From now on, let's be lazy" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "plothist = lambda h: plt.bar(*h.axes.centers, h.values(), width=h.axes.widths[0]);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Aside: here's step. The edges are quite ugly for us, just like it is for numpy. Or anyone." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "image/png": { "height": 251, "width": 388 }, "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "plt.step(hist1.axes[0].edges[:-1], hist1.values(), where=\"post\");" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Recent versions of matplotlib support `.stairs`, which was designed to work well with histograms:" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "image/png": { "height": 250, "width": 388 }, "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "plt.stairs(hist1.values(), hist1.axes[0].edges);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "No plotting is built in, but the data is easy to access." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 2: Drop-in replacement for NumPy\n", "\n", "To start using this yourself, you don't even need to change your code. Let's try the numpy adapters." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "bins2, edges2 = bh.numpy.histogram(data1, bins=10)" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "b2, e2 = np.histogram(data1, bins=10)" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0], dtype=int64)" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "bins2 - b2" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([ 0.00000000e+00, 8.88178420e-16, 8.88178420e-16, 0.00000000e+00,\n", " 0.00000000e+00, 0.00000000e+00, 1.77635684e-15, 3.55271368e-15,\n", " 0.00000000e+00, 0.00000000e+00, -1.77635684e-15])" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "e2 - edges2" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Not bad! Let's start moving to the boost-histogram API, so we can use our little plotting function:" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "image/png": { "height": 248, "width": 388 }, "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "hist2 = bh.numpy.histogram(data1, bins=\"auto\", histogram=bh.Histogram)\n", "plothist(hist2);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we can move over to boost-histogram one step at a time! Just to be complete, we can also go back to a NumPy tuple from a Histogram object:" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([ True, True, True, True, True, True, True, True, True,\n", " True])" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "b2p, e2p = bh.numpy.histogram(data1, bins=10, histogram=bh.Histogram).to_numpy()\n", "b2p == b2" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 3: More dimensions" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The same API works for multiple dimensions." ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [], "source": [ "hist3 = bh.Histogram(bh.axis.Regular(150, -1.5, 1.5), bh.axis.Regular(100, -1, 1))" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [], "source": [ "def make_2D_data(*, mean=(0, 0), widths=(1, 1), size=1_000_000):\n", " cov = np.asarray(widths) * np.eye(2)\n", " return np.random.multivariate_normal(mean, cov, size=size).T" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [], "source": [ "data3x = make_2D_data(mean=[-0.75, 0.5], widths=[0.2, 0.02])\n", "data3y = make_2D_data(mean=[0.75, 0.5], widths=[0.2, 0.02])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "From here on out, I will be using `.reset()` before a `.fill()`, just to make sure each cell in the notebook can be rerun." ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Histogram(\n", " Regular(150, -1.5, 1.5),\n", " Regular(100, -1, 1),\n", " storage=Double()) # Sum: 1905785.0 (2000000.0 with flow)" ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" } ], "source": [ "hist3.reset()\n", "hist3.fill(*data3x)\n", "hist3.fill(*data3y)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Again, let's make plotting a little function:" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [], "source": [ "def plothist2d(h):\n", " return plt.pcolormesh(*h.axes.edges.T, h.values().T)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This is transposed because `pcolormesh` [expects it](https://matplotlib.org/3.2.2/api/_as_gen/matplotlib.pyplot.pcolormesh.html#axes-pcolormesh-grid-orientation)." ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "image/png": { "height": 252, "width": 394 }, "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "plothist2d(hist3);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's try a 3D histogram" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Histogram(\n", " Regular(150, -5, 5),\n", " Regular(100, -5, 5),\n", " Regular(100, -5, 5),\n", " storage=Double()) # Sum: 1000000.0" ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "data3d = [np.random.normal(size=1_000_000) for _ in range(3)]\n", "\n", "hist3d = bh.Histogram(\n", " bh.axis.Regular(150, -5, 5),\n", " bh.axis.Regular(100, -5, 5),\n", " bh.axis.Regular(100, -5, 5),\n", ")\n", "\n", "hist3d.fill(*data3d)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's project to the first two axes:" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "image/png": { "height": 248, "width": 370 }, "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "plothist2d(hist3d.project(0, 1));" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 4: UHI\n", "\n", "Let's explore the boost-histogram UHI syntax. We will reuse the previous 2D histogram from part 3:" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "image/png": { "height": 252, "width": 394 }, "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "plothist2d(hist3);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "I can see that I want y from 0.25 to 0.75, in data coordinates:" ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "image/png": { "height": 248, "width": 380 }, "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "plothist2d(hist3[:, bh.loc(0.25) : bh.loc(0.75)]);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "What's the contents of a bin?" ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "181.0" ] }, "execution_count": 27, "metadata": {}, "output_type": "execute_result" } ], "source": [ "hist3[100, 87]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "How about in data coordinates?" ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "181.0" ] }, "execution_count": 28, "metadata": {}, "output_type": "execute_result" } ], "source": [ "hist3[bh.loc(0.5), bh.loc(0.75)]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "> Note: to get the coordinates manually:\n", "> \n", "> ```python\n", "hist3.axes[0].index(.5) == 100\n", "hist3.axes[1].index(.75) == 87\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "How about a 1d histogram?" ] }, { "cell_type": "code", "execution_count": 29, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "image/png": { "height": 248, "width": 394 }, "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "plothist(hist3[:, :: bh.sum])\n", "plothist(hist3[:: bh.sum, :]);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's look at one part and rebin:" ] }, { "cell_type": "code", "execution_count": 30, "metadata": {}, "outputs": [ { "data": { "image/png": "iVBORw0KGgoAAAANSUhEUgAAAugAAAH4CAYAAAAYZBiWAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/YYfK9AAAACXBIWXMAABYlAAAWJQFJUiTwAAApsklEQVR4nO3dfbRlZ10n+O/vvlRVqpJUAIk6wPCSzosz9qiAvMU3gp1GtBVsnHFmiZppnKFhTRB0tSxReXHobteaFgiCdo8C0uq0omOzRpHOICBKRNs4ONPyFoOF2ISXJBIrSaVe7nnmj3NKLkXdVNW9u5773JvPZ627dursfZ7fs89+zj7fs7P3PtVaCwAAMIal7e4AAADweQI6AAAMREAHAICBCOgAADAQAR0AAAYioAMAwEAEdAAAGIiADgAAAxHQAQBgIAI6AAAMREAHAICBCOgAADAQAR0AAAYioAMAwEAmCehV9eyqel1V/X5V/W1Vtar6pU229fCqemNVfbKqjlbVoap6TVU9aIq+AgDAyFYmaufHknxVkruT/HWSqzbTSFVdluSmJJcmeVuSDyd5QpIXJnl6VV3dWrtjkh4DAMCApjrF5UVJrkhycZJ/uoV23pB5OL++tfbM1tpLWmvXJHl1kiuTvGrLPQUAgIFVa23aBqu+Kcm7k/xya+17zuF5j0lya5JDSS5rrc3WzbsoyW1JKsmlrbV7JuwyAAAMY6SLRK9ZTG9cH86TpLV2OMn7kuxP8qTeHQMAgF5GCuhXLqYf3WD+LYvpFR36AgAA22Kqi0SncHAxvWuD+Scfv+RMDVXVzRvM+srML2Q9dC4dAwCAc/SoJH/bWnv0uT5xpIB+JrWYbuWk+eWlLD/4QC568BQdAgCA07knhzPL2qaeO1JAP3mE/OAG8y8+ZbkNtdYed7rHq+rmA7nosU+sb95E9wAA4Oz8UXtnDudzhzbz3JHOQf/IYrrROeaXL6YbnaMOAAA73kgB/d2L6bVV9QX9Wtxm8eokR5K8v3fHAACgl+4BvapWq+qqxa+G/p3W2q1Jbsz8hPoXnPK0VyQ5kOQt7oEOAMBuNsk56FX1zCTPXPzzyxbTJ1fVmxf/fXtr7YcX//2wJB9K8vHMw/h6z09yU5Ibquppi+WemOSpmZ/a8tIp+gsAAKOa6iLRr07yfac89pjFXzIP4z+cM2it3VpVj0/yyiRPT/KMzH9B9IYkr2it3TlRfwEAYEiTBPTW2suTvPwslz2Uz98y8XTzP5Hkuin6BQAAO81IF4kCAMADnoAOAAADEdABAGAgAjoAAAxEQAcAgIEI6AAAMBABHQAABiKgAwDAQAR0AAAYyCS/JAo7Xu3S76pt1qfObn39dqla2vDHnCfVZq1Lnd1sN26rXuvU2258DXfjOnV1YvPr5FMVAAAGIqADAMBABHQAABiIgA4AAAMR0AEAYCACOgAADERABwCAgQjoAAAwEAEdAAAGIqADAMBABHQAABiIgA4AAAMR0AEAYCACOgAADERABwCAgQjoAAAwEAEdAAAGsrLdHeivkur0vaTN+tRJ+q1TR7VU292Fna9Wt7sH0/O+2jFquWOtnvuLnuOi03jvua12rWr9SnUa77t1XLRZv221WT59AABgIAI6AAAMREAHAICBCOgAADAQAR0AAAYioAMAwEAEdAAAGIiADgAAAxHQAQBgIAI6AAAMREAHAICBCOgAADAQAR0AAAYioAMAwEAEdAAAGIiADgAAA1nZ7g7sZrWyut1d2NFqqXoW61erzfrV6rRetdpvV9JmHV+/jmp5uV+x1vrU6blOPcdFr9evp+q4v13quL/tOC7KuNg51tY61alkk0PQEXQAABiIgA4AAAMR0AEAYCACOgAADERABwCAgQjoAAAwEAEdAAAGIqADAMBABHQAABiIgA4AAAMR0AEAYCACOgAADERABwCAgQjoAAAwEAEdAAAGIqADAMBAVra7A91VsrTaabWr4/efpepXq/rUquXlLnWSJEsdt9Vyx1qz1qVMz21VPV+/1uf1667XeO/5Ht6tem2r3TrWe+r5GvYaF7NZnzpJsrbWrVSvxFRHl5NNvoSOoAMAwEAEdAAAGIiADgAAAxHQAQBgIAI6AAAMREAHAICBCOgAADAQAR0AAAYioAMAwEAEdAAAGMhkAb2qHl5Vb6yqT1bV0ao6VFWvqaoHnWM731pVN1bVX1fVkar6WFW9taqePFVfAQBgVJME9Kq6LMnNSa5L8sdJXp3kY0lemOQPq+ohZ9nOTyX5rSSPTfKOJK9N8qdJviPJ+6rqe6boLwAAjGplonbekOTSJNe31l538sGq+ukkL0ryqiTPu78GqurLkvxwkk8n+W9aa59ZN++pSd6V5JVJfmmiPgMAwHC2fAS9qh6T5Nokh5K8/pTZL0tyT5LnVNWBMzT1yEV//mh9OE+S1tq7kxxO8tCt9hcAAEY2xSku1yymN7bWZutntNYOJ3lfkv1JnnSGdm5JcizJE6rqS9bPqKpvSHJRkndO0F8AABjWFAH9ysX0oxvMv2UxveL+Gmmt3ZnkR5J8aZIPVtW/qap/UVW/luTGJP93kv95gv4CAMCwpjgH/eBietcG808+fsmZGmqtvaaqDiV5Y5IfWDfrL5K8+dRTXzZSVTdvMOuqs3k+AABsl6kuEr0/tZi2My5Y9c+S/PMkNyT5mSSfyjxU/4skv1xVX91a+2db600lKz1WO6k9e7rUSZJa7bNOXS11vE3/8nK/Wj3Xq9NYzxee3XZ+9VqnpOu2anv6jcE60Wd7tT0dt9Ws3xhsXcdgnzK9xkTSd1zU8bVutdpynXmhidTaGSPVjtOq4+vXOr1+dy0nJzb31CneJSePkB/cYP7Fpyx3WlX1TUl+KslvttZevG7Wn1bVszI/heaHqurnWmsfu7+2WmuP26DGzZnfwhEAAIY0xXfzjyymG51jfvliutE56id922L67lNntNbuzfz+6ktJvuZcOwgAADvFFAH9ZKC+tqq+oL2quijJ1UmOJHn/GdrZu5hudCvFk48f20wnAQBgJ9hyQG+t3Zr5XVYeleQFp8x+RZIDSd7SWrsnSapqtaquWvz66Hq/v5j+T1X1sPUzqupbMg/69yW5aat9BgCAUU11pcbzMw/ON1TV05J8KMkTkzw181NbXrpu2Yct5n8881B/0q9nfp/zb07yoar6zcwvEv2KzE9/qSQvaa3dMVGfAQBgOJME9NbarVX1+CSvTPL0JM9Iclvmd2N5xeIe52dqY1ZVz8j8KPx3J3lW5j9wdGeStye5obV24xT9BQCAUU12r6PW2ieSXHcWyx3K52+9eOq840les/gDAIAHnI43ZAYAAM5EQAcAgIEI6AAAMBABHQAABiKgAwDAQAR0AAAYiIAOAAADEdABAGAgAjoAAAxksl8S3SlqaTlLF13Yp1hrfeokyZ49HWut9qlTp/3B2fNjqd931XZBv23VVvqsV+v5+u1Z7lZrttJvDM46rtfSiVmXOm253+vXlnpuq37jvY732VazvR2P13X8aKyOtXruL5aOdnoP91ynY33WKUm3fDH7y+XkyOae6wg6AAAMREAHAICBCOgAADAQAR0AAAYioAMAwEAEdAAAGIiADgAAAxHQAQBgIAI6AAAMREAHAICBCOgAADAQAR0AAAYioAMAwEAEdAAAGIiADgAAAxHQAQBgICvb3YHulpeSCw/0qbWy3KdOkrbar1aW+nyv67lOaxf0eyuc2N+vVs361Dl+Ub9tNVupbrVax0MYa3v7rdes0xBc29NvndKxVE8n9vap0/Plax2LLR/rV6vn/mLpRJ99bs9t1dPSWp86a3+y+RfQEXQAABiIgA4AAAMR0AEAYCACOgAADERABwCAgQjoAAAwEAEdAAAGIqADAMBABHQAABiIgA4AAAMR0AEAYCACOgAADERABwCAgQjoAAAwEAEdAAAGIqADAMBABHQAABjIynZ3oLulStu/t0uptqffy3viwj3daq3t7fO9rq32+/44W6lutY4f6Ldexw50Wq+OX/VPXNCv1lq/t1VmHWv1Wq+e69SWWrdaaxf0q1WzPnVaz8N1HbdVrfXbt6ffaqXt7TMwlu5d7lInSepEt1LdrG0hbjqCDgAAAxHQAQBgIAI6AAAMREAHAICBCOgAADAQAR0AAAYioAMAwEAEdAAAGIiADgAAAxHQAQBgIAI6AAAMREAHAICBCOgAADAQAR0AAAYioAMAwEAEdAAAGMjKdnegt7a8lOMP3t+l1treft9/Znv61Tp6cLlLndbx6+OJC6pfrT7DL0ly7OI+ddb29qmTJLM9rVutnmNwdmCtW609D7mvS529K/3WqfUbFrlw37F+xTrZt3p8u7twXsxav31761irqs+AP7HW5/M+Se45uqdbrSP3rfYptGe26ac6gg4AAAMR0AEAYCACOgAADERABwCAgQjoAAAwEAEdAAAGIqADAMBABHQAABiIgA4AAAMR0AEAYCCTBfSqenhVvbGqPllVR6vqUFW9pqoetIm2vr6qfqOqblu0dVtV3VhVz5iqvwAAMKKVKRqpqsuS3JTk0iRvS/LhJE9I8sIkT6+qq1trd5xlWz+W5CeT3J7kt5LcluRLknxNkm9K8vYp+gwAACOaJKAneUPm4fz61trrTj5YVT+d5EVJXpXkeWdqpKq+K/Nw/s4k39laO3zK/NWJ+gsAAEPa8ikuVfWYJNcmOZTk9afMflmSe5I8p6oOnKGdpSQ/leTeJP/DqeE8SVprx7faXwAAGNkU56Bfs5je2FqbrZ+xCNnvS7I/yZPO0M5Tkjw681NY/qaqvrWqfqSqXlhVT56gnwAAMLwpTnG5cjH96Abzb8n8CPsVSX73ftr52sX000n+NMnfXz+zqt6b5Nmttc9uvqsAADC2KQL6wcX0rg3mn3z8kjO0c+li+rwkf5nkm5P8UZJHJvlXSf5hkrdmfqHo/aqqmzeYddWZngsAANtpqotE708tpu0Myy2vW/7ZrbU/W/z7z6vqWZkfof/Gqnpya+0PN9uZtlQ5sX/5zAtOYG1fv9vMH9/fsdaBOvNCU9TZ36VMkuTE/V4hMa21ff1qHb94duaFJtAuXOtSJ0mW9p3oVuuC/ce61XrIhfd0q/XQC/rUeujeL7qU6PzV2nN3t1r7l492q3Vw+UiXOvfO9nSpkySz1u/z6tF7P9Ot1mdPXNyt1m3HLulSZ9b6fN4nyepSv8+Rzxy7qEudt+69L7dv8rlTBPSTR8gPbjD/4lOW28jfLKYfWxfOkySttSNV9R+S/JPMb994vwG9tfa40z2+OLL+2DP0AwAAts0UX2M/sphescH8yxfTjc5RP7Wdz20w/2SAv+DsugUAADvPFAH93YvptYtbJf6dqrooydVJjiR5/xnaeW+SE0kur6rT/b+2r1xMD22+qwAAMLYtB/TW2q1JbkzyqCQvOGX2K5IcSPKW1to9yfzHhqrqqsWvj65v5/Ykv5r5qTI/sX5eVf2DzC8SvSvJO7baZwAAGNVUF4k+P8lNSW6oqqcl+VCSJyZ5auantrx03bIPW8z/eOahfr0XL5730qr6hiR/nPldXJ6VZC3JD7TWPjdRnwEAYDiTXEq9OIr++CRvzjxg/1CSy5LckOTJrbU7zrKdzyye/+okj0hyfeY/hPTbSb6+tfbWKfoLAACjmuw2i621TyS57iyWO5TP33rxdPPvzPxI+oun6hsAAOwU/W5GCgAAnJGADgAAAxHQAQBgIAI6AAAMREAHAICBCOgAADAQAR0AAAYioAMAwEAEdAAAGMhkvyS6U7Sl5MQFfb6XrO3r9/3n+P4Nf5x1+loXdqpzoE+dJDl+sHWrNVvtV6tdcrxLnYsOHulSJ0lWV9a61XrExZ/rVutL9x3uVuuKA5/qUme1+m2r/2rvf+5Wa7lm3Wo9YqXPuLhkqd9nyJ1r/faBe6tfrX37butW64PHL+pSZzn9Xr9jbblbrVtWv6xLnd9aOrbp5zqCDgAAAxHQAQBgIAI6AAAMREAHAICBCOgAADAQAR0AAAYioAMAwEAEdAAAGIiADgAAAxHQAQBgIAI6AAAMREAHAICBCOgAADAQAR0AAAYioAMAwEAEdAAAGMjKdndgNzuxr1+tttyv1tqePnVaz9E561eqXXK8W63l1T4rdvxEvwF48QX3dav10H13d6t1yeq93WrNWp9jMw9euatLnSR51Gq/Wvuqdat16dIFXeosV7/jdRfWWrdae2u1W60j7Wi3Wl+1554udT691m+s//mxL+tW66Erf9ulzsoWxroj6AAAMBABHQAABiKgAwDAQAR0AAAYiIAOAAADEdABAGAgAjoAAAxEQAcAgIEI6AAAMBABHQAABiKgAwDAQAR0AAAYiIAOAAADEdABAGAgAjoAAAxEQAcAgIEI6AAAMJCV7e5Ady1ZWutTavlonzpJsranX63l+zoVqk51ksxW+xWbfW61W621i050qbNn7/EudZLk+Npyt1qfve/CbrWW0rrVWq0+O8EHr9zdpU6SHDp+sFut/3Llrm61Ds367HDXeu5wO3ro8rFute5rs261PrvW53NkreNx3Ptm/T4be2lbeF85gg4AAAMR0AEAYCACOgAADERABwCAgQjoAAAwEAEdAAAGIqADAMBABHQAABiIgA4AAAMR0AEAYCACOgAADERABwCAgQjoAAAwEAEdAAAGIqADAMBABHQAABjIynZ3oLdqyfJ9sy61Vpa7lEmStKV+37VqVl3qtI6v39LxfrX2/E2/bXV8bbVLnXtPdFyni/sNjLvv29Ot1m37Lu5W6/IHfbZLnb868uAudZJk7/Jl3Wo9+oLbu9W6cPm+LnX2Vb+d4Gqtdat1vOMHyYNX7u5W65PHH9Slzr2zfvvAT97XZ52S5Hjr85l199otSe7d1HMdQQcAgIEI6AAAMBABHQAABiKgAwDAQAR0AAAYiIAOAAADEdABAGAgAjoAAAxEQAcAgIEI6AAAMJDJAnpVPbyq3lhVn6yqo1V1qKpeU1Wb/u3WqnpOVbXF33On6isAAIxqZYpGquqyJDcluTTJ25J8OMkTkrwwydOr6urW2h3n2OYjkrwuyd1JLpyinwAAMLqpjqC/IfNwfn1r7ZmttZe01q5J8uokVyZ51bk0VlWV5E1J7kjycxP1EQAAhrflgF5Vj0lybZJDSV5/yuyXJbknyXOq6sA5NHt9kmuSXLd4PgAAPCBMcQT9msX0xtbabP2M1trhJO9Lsj/Jk86msar6iiT/MslrW2vvnaB/AACwY0wR0K9cTD+6wfxbFtMrztRQVa0k+bdJ/irJj269awAAsLNMcZHowcX0rg3mn3z8krNo6yeSfE2Sr2utHdlsh6rq5g1mXbXZNgEAoIdJ7uJyBrWYtvtdqOoJmR81/1ettT88b52ZtSwfOXG+mv8CSyf63WZ++fhyt1pHZ31qLR/rUiZJsnpPnXmhiRw/l6sxtmjlSJ/1Onq0x65kbna431i/d//szAtNVWvfWrdat9/Z58ZYtXy/u/1JLa/021Z/esHDu9V6yP4+l2EdPrqvS50kObDnaLdaa7N+n8N7V/pkiyS55/ieLnVa6/fZePi+vd1qHTve5zPr8LHNr9MUPTx5hPzgBvMvPmW5L7Lu1JaPJvnxrXaotfa4DercnOSxW20fAADOlym+Wn5kMd3oHPPLF9ONzlFP5vc5vyLJVyS5b92PE7XM7wSTJP/74rHXbLXDAAAwqimOoL97Mb22qpbW38mlqi5KcnWSI0nefz9tHE3yCxvMe2zm56X/QeZfBs7b6S8AALDdthzQW2u3VtWNmd8L/QWZ//rnSa9IciDJv26t3ZMkVbWa5LIkx1trty7aOJLkuadrv6pennlA/8XW2s9vtb8AADCyqc6Sf36Sm5LcUFVPS/KhJE9M8tTMT2156bplH7aY//Ekj5qoPgAA7AqTXN68OBL++CRvzjyY/1DmR8lvSPLk1todU9QBAIDdbrL7zLTWPpHkurNY7lA+f+vFs2n35Ulevtl+AQDATtLvBqEAAMAZCegAADAQAR0AAAYioAMAwEAEdAAAGIiADgAAAxHQAQBgIAI6AAAMREAHAICBTPZLojvGWsvK4aNdSs32rXapkyTLR9a61Vq5d7lLnRP7+9RJkrU9/b6rnjh81j+ku2WzTu/w1bv7rdPaar9aWeo3Bo9f1K/WrNOuqdf4S5JZtW61Prd3f7daf1MP6lKn9Txc13FbddU67pt6vYY9d7fHOxZb61OrHdv8ft0RdAAAGIiADgAAAxHQAQBgIAI6AAAMREAHAICBCOgAADAQAR0AAAYioAMAwEAEdAAAGIiADgAAAxHQAQBgIAI6AAAMREAHAICBCOgAADAQAR0AAAYioAMAwEBWtrsDvdVslqXD93WptXTkeJc6SdL29NuUS0eqS53Vz/X7/njioj3daqX1K3X84j7jYv/tXcokSY5dtDuPK8yW+7yvkmRtb586rfqtU1vq+Prt61drN2rL/WrVrF+tWcddU68RuHy0U6Ekax0/hlfu7VPnU1uIm7vzkw4AAHYoAR0AAAYioAMAwEAEdAAAGIiADgAAAxHQAQBgIAI6AAAMREAHAICBCOgAADAQAR0AAAYioAMAwEAEdAAAGIiADgAAAxHQAQBgIAI6AAAMREAHAICBCOgAADCQle3uQHdra8nfHu5Tq6pPnSR1wQX9aq0sd6vVy+q9x/oV6/i1eOXwapc6sz39diV77uxWKm2l38aadaxVrXWpM1vtt05tqd/+tvU8tNVpvWYrHT+vZn3GX2+z5X6vYS8rR2bb3YUdbfnY5se6I+gAADAQAR0AAAYioAMAwEAEdAAAGIiADgAAAxHQAQBgIAI6AAAMREAHAICBCOgAADAQAR0AAAYioAMAwEAEdAAAGIiADgAAAxHQAQBgIAI6AAAMREAHAICBrGx3B7prLe3ose3uxfQ6rlPtWe1Wq5da6fhWqOpXarnPd/Cl1d03JpJ03VbptK2SdFuvtrrcpU6Svtuqo7bcab1mfcokSbXWrVa316+3TturZh0HRk8n+qzX0rG1zT93wn4AAABbJKADAMBABHQAABiIgA4AAAMR0AEAYCACOgAADERABwCAgQjoAAAwEAEdAAAGIqADAMBAJgvoVfXwqnpjVX2yqo5W1aGqek1VPegsn/+QqnpuVf1mVf1FVR2pqruq6g+q6p9UlS8TAADseitTNFJVlyW5KcmlSd6W5MNJnpDkhUmeXlVXt9buOEMz35XkZ5PcluTdSf4qyZcm+c4kP5/kW6rqu1prbYo+AwDAiCYJ6EnekHk4v7619rqTD1bVTyd5UZJXJXneGdr4aJJvT/LbrbXZujZ+NMkfJ/nHmYf135iozwAAMJwtnzZSVY9Jcm2SQ0lef8rslyW5J8lzqurA/bXTWntXa+3/Wh/OF49/KsnPLf75TVvtLwAAjGyK87qvWUxvPE24PpzkfUn2J3nSFmocX0xPbKENAAAY3hQB/crF9KMbzL9lMb1iM41X1UqS71388x2baQMAAHaKKc5BP7iY3rXB/JOPX7LJ9v9lkq9M8vbW2n84mydU1c0bzLpqk30AAIAuprpI9P7UYnrOd1+pquuT/FDmd4V5zhSdabOWdt/RKZo6s6U68zI7UDva5/Wr5eUudebFOm6rnuvVS8exvmvvuLrccb06jfda6rhOPWt13Fa1NjvzQpMU6rgPnHVap/Qegx1fw17joueN83bjTfrW1jb91CkC+skj5Ac3mH/xKcudlap6QZLXJvlgkqe11u482+e21h63QZs3J3nsufQDAAB6muKr5UcW043OMb98Md3oHPUvUlU/mORnkvynJE9d3MkFAAB2vSkC+rsX02tP/bXPqrooydVJjiR5/9k0VlU/kuTVST6QeTj/zAR9BACAHWHLAb21dmuSG5M8KskLTpn9iiQHkryltXZPklTValVdtfj10S9QVT+e+UWhN2d+WsvtW+0fAADsJFNdJPr8JDcluaGqnpbkQ0memOSpmZ/a8tJ1yz5sMf/jmYf6JElVfV+SVyZZS/L7Sa6vL75o5VBr7c0T9RkAAIYzSUBvrd1aVY/PPGA/PckzktyW5IYkrzjLCzwfvZguJ/nBDZb5vSRv3lJnAQBgYJPdZrG19okk153Fcofy+Vsvrn/85UlePlV/AABgJ9qlNxQGAICdSUAHAICBCOgAADAQAR0AAAYioAMAwEAEdAAAGIiADgAAAxHQAQBgIAI6AAAMZLJfEt0xWks7cXy7ezG92oXftdbW+tXq+vrtwvG39EU/DnzetG6VOpv1W7Na7bTrr37jIq3jyOi5Xku7cN++W8fFbNavVi+79X3Vy9rmx8QufOcDAMDOJaADAMBABHQAABiIgA4AAAMR0AEAYCACOgAADERABwCAgQjoAAAwEAEdAAAGIqADAMBABHQAABiIgA4AAAMR0AEAYCACOgAADERABwCAgQjoAAAwkJXt7sB2aLO23V04D9a2uwOTax1XqZaqXzG2phxX2LI22+4e7Gg9P0NqeblbrW6MP06j6/uq02d+28JY90kHAAADEdABAGAgAjoAAAxEQAcAgIEI6AAAMBABHQAABiKgAwDAQAR0AAAYiIAOAAADEdABAGAgAjoAAAxEQAcAgIEI6AAAMBABHQAABiKgAwDAQAR0AAAYiIAOAAADWdnuDvTXkjbb7k7sbLX7vte1tbXt7sLO1nVM2FZb1mq7ezC5Nmvb3YXzwr6J06mlPu/hnu+rXuuUJLPjndZrC2V2X9ICAIAdTEAHAICBCOgAADAQAR0AAAYioAMAwEAEdAAAGIiADgAAAxHQAQBgIAI6AAAMREAHAICBCOgAADAQAR0AAAYioAMAwEAEdAAAGIiADgAAAxHQAQBgICvb3QF2oDbb7h4wGmNiR2lr290D6KQ6HofsuB/cje/h3bhOSdv0Mx1BBwCAgQjoAAAwEAEdAAAGIqADAMBABHQAABiIgA4AAAMR0AEAYCACOgAADERABwCAgQjoAAAwkMkCelU9vKreWFWfrKqjVXWoql5TVQ/ajnYAAGAnWpmikaq6LMlNSS5N8rYkH07yhCQvTPL0qrq6tXZHr3YAAGCnmuoI+hsyD9XXt9ae2Vp7SWvtmiSvTnJlkld1bgcAAHakaq1trYGqxyS5NcmhJJe11mbr5l2U5LYkleTS1to957udM/T15otyyWOfWN+8macDADtJdbzU7vOxBZIkf9TemcP53J+21h53rs+dYuRes5jeuD5UJ0lr7XCS9yXZn+RJndoBAIAda4qAfuVi+tEN5t+ymF7RqR0AANixprhI9OBietcG808+fkmndlJVN28w66vuyeH8UXvnmZoAAHa6Vj2LdazFTnBPDifJozbz3Enu4nIGJ98dWx25U7SzNsvaXYfzuUNb7MtucNVi+uFt7QWjMS44HeOC0zEuOB3j4vMeleRvN/PEKQL6ySPbBzeYf/Epy53vdrKZk/EfaE7+XwavFesZF5yOccHpGBecjnExjSnOQf/IYrrRueGXL6YbnVs+dTsAALBjTRHQ372YXlv1hfczWtwe8eokR5K8v1M7AACwY205oLfWbk1yY+bn2bzglNmvSHIgyVtO3ru8qlar6qrFr4Zuuh0AANiNprpI9PlJbkpyQ1U9LcmHkjwxyVMzPyXlpeuWfdhi/sfzxVe2nks7AACw60zyE1uLo9+PT/LmzAP1DyW5LMkNSZ7cWrujZzsAALBTVWvu2wkAAKOY5Ag6AAAwDQEdAAAGIqADAMBABHQAABiIgA4AAAMR0AEAYCACOgAADERA3+WqarWqXlhVb6qqD1TVsapqVfXcidr/hUV7rar+3hRtcv5NOS6q6vKq+pGqeldVfWLR1qer6m1V9dTz0X/Oj/Oxv6iqp1TV26vqzqq6t6r+36r6wapanrLvnH9Tbcuq2ltVL6iqP66q26vq7qr6UFXdUFWPPF/95/yY8j1ec99XVe9ZtHekqv6yqn6tqq44H/0f1cp2d4Dz7kCS1yz++9NJPpXkEVM0XFX/KMn/mOTuJBdO0SbdTDkufjLJf5fkg0nenuTOJFcm+fYk315VL2yt3bCl3tLLpPuLqvqOJL+R5L4kv5r52PhHSV6d5Ook37WFvtLRVNuyqlaS/O7iOR9O8n8kOZrka5P8L0m+t6qe0lr74NTrwPSmfI9X1b4kb03ybUk+kuRXkhxO8l8k+fokVyT56ITdH1trzd8u/kuyJ8m3JPnyxb9fnqQlee4W231o5h/e/y7JexZt/r3tXl9//cdFku9P8jWnefwbkxzL/MP3y7d7nf11HxcXJ/nMYvs/ft3j+5LctGj3u7d7nf313ZaZB7aW5J1Jlk6Z94rFvDdu9zr76zsuFs97/eI5//zUsbGYv7rd69zzzykuu1xr7Vhr7Xdaa7dN3PS/WUxfMHG7dDDluGitvbm19v+c5vHfy/zL254kT9lqHc6/ifcXz878i/y/a639yboa9yX5scU//+kEdTj/ptyWj1lMf7u1Njtl3tsW04dutqN0Ndm4qKrLkjwvyX9M8tLTjI201o5vucc7iFNcOGdV9f1JnpnkWa21O6pqezvEyE7uUE9say/YDtcspu84zbz3Jrk3yVOqam9r7Wi/brEJU27LP19Mv6WqXntKEPu2xfSdm+8qHU05Lv77zK+L/MUkFy9OoX1EkjuSvKu19hcT9XnHENA5J4sLeF6b5Jdaa/9+m7vDwBZj5WmZ76Tfu83dob8rF9MvOme0tXaiqv4yyX+d+RHVD/XsGOdsym3520n+zyTfmeT/q6p3Zn4q3OOSfF2S1yX5mYn6zfk15bj42sX0YJJbkzxkfXNV9bNJrm+trW2tyzuHU1w4a1V18tvt3Umu3+buMLCq2pvkl5PsTfLy1trfbHOX6O/gYnrXBvNPPn7J+e8KWzTZtmzzk4mfnfn1DVdm/lnyw0memvkX+V95IIWwHW7K9/ili+krk/xJkr+f5KLMD/LcmuT5SX58U73coQT0HaCqDq27leHZ/P3SeerKizK/8O8HBK7tN9C4OLVfy0n+beZX8P9qkv+tR13mRh0Xp+vqYtq2qf4DynkeF2e9LRd36vjVzEP5C5J8eeZB7xlJHpnkvYs7g9DBKOMiyclbMt6W+emz/6m1dndr7V2Zf6GbJXlxVe05h/o7mlNcdoZbM7+F0dn65NQdqKrLk7wqyZtaa2+fun02ZdvHxakW4fyXMr9Tw68l+Z7FETP6GWVcnDx6dnCD+Refshzn11bGxZTb8iWZ7x9e2Fr71+se/52qenaSD2R+GuXbTvNcpjfKuDh50O8drbUj62e01v5scbrMZUm+IsmfnWVfdzQBfQdorT1tu/uQ+Xlke5NcV1XXbbDMLYsLRp/l/PTzb5Bx8XcW9zf+lcw/fH8lyff6X9X9DTQuPpLk8Znfu/jm9TMWY+XRmV88/LH+XXvg2eK4mHJbnrwQ9N2n6eOfVdWdSR5ZVQ9prd2xhT5zFgYaFx9Jcm2Sz20w/2SAv2AzHd2JnOLC2TqU5Bc2+PvUYpm3Lv59qH/32E6L/+3465mH87ckeY5w/oD3rsX06aeZ9w1J9ie5yR1cdoQpt+XexfSLbqW4uHbl5FHXY+faSbqbclz87mL6lafOWIyLyxf/PHSOfdyxBHS+SFUdrKqrqurLTz7WWvtAa+25p/vL/Jtvkvzo4rEPbEvHOa9ONy4Wj+9N8ptJviPzL2jXne4etuxOG42LzL+w3Z7ku6vq8euW35fkf13882c7dZOtOedteT/j4vcX0x9d7DvWe3nm/2f/P7bWDk/Vec6bKcfF72R+pP0fVtU/OGXej2d+Gs3vtdY+lQeIcnro7ldVL0ly1eKfX53kqzL/la9bFo/9QWvt59ct//1J3pTkF1tr338W7b8n84tHL38g3qt0p5pqXFTVmzL/NdHbk7whp78g6D2ttfdM2X/Ojyn3F1X1zMw/xO/L/FeH70zy7ZnfvePXk/y3rlHYGc51W97P/uJhSd6f5OGZHw19R5IjmV9U/oTFfz+ttfaH53mVmMBU42Ix7+uS3Jj5j9v9ZpKPZ377xW9I8tkkX9da+6JbOu5WzkF/YHh65gF6vafkC3/d8efDA81U4+LRi+mXJPmJ+1nuPWfdM7bTZPuL1tq/r6pvTPLSJP84858A/4skL05yg3C+c0y1LVtr/7mqHpvkR5J8a5LrMv+/+bcleXOSn2qtfXj6NeB8mPI93lr7g8WR+JdlftvNS5J8OvNfLv/J1tpfT9z9oTmCDgAAA3EOOgAADERABwCAgQjoAAAwEAEdAAAGIqADAMBABHQAABiIgA4AAAMR0AEAYCACOgAADERABwCAgQjoAAAwEAEdAAAGIqADAMBABHQAABiIgA4AAAMR0AEAYCACOgAADOT/B84fi3NkFPQMAAAAAElFTkSuQmCC\n", "text/plain": [ "
" ] }, "metadata": { "image/png": { "height": 252, "width": 372 }, "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "plothist2d(hist3[: 50 : bh.rebin(2), 50 :: bh.rebin(2)]);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "What is the value at `(-.75, .5)`?" ] }, { "cell_type": "code", "execution_count": 31, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "1005.0" ] }, "execution_count": 31, "metadata": {}, "output_type": "execute_result" } ], "source": [ "hist3[bh.loc(-0.75), bh.loc(0.5)]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 5: Understanding accumulators\n", "\n", "Boost-histogram has several different storages; storages store accumulators. Let's try making a profile." ] }, { "cell_type": "code", "execution_count": 32, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Mean(count=3, value=0.4, variance=0.01)" ] }, "execution_count": 32, "metadata": {}, "output_type": "execute_result" } ], "source": [ "mean = bh.accumulators.Mean()\n", "mean.fill([0.3, 0.4, 0.5])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here's a quick example accessing the values:" ] }, { "cell_type": "code", "execution_count": 33, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "mean.count=3.0 mean.value=0.4 mean.variance=0.01\n" ] } ], "source": [ "print(f\"{mean.count=} {mean.value=} {mean.variance=}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 6: Changing the storage" ] }, { "cell_type": "code", "execution_count": 34, "metadata": {}, "outputs": [], "source": [ "hist6 = bh.Histogram(bh.axis.Regular(10, 0, 10), storage=bh.storage.Mean())" ] }, { "cell_type": "code", "execution_count": 35, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Histogram(Regular(10, 0, 10), storage=Mean()) # Sum: Mean(count=3, value=0.4, variance=0.01)" ] }, "execution_count": 35, "metadata": {}, "output_type": "execute_result" } ], "source": [ "hist6.fill([0.5] * 3, sample=[0.3, 0.4, 0.5])" ] }, { "cell_type": "code", "execution_count": 36, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Mean(count=3, value=0.4, variance=0.01)" ] }, "execution_count": 36, "metadata": {}, "output_type": "execute_result" } ], "source": [ "hist6[0]" ] }, { "cell_type": "code", "execution_count": 37, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "MeanView(\n", " [(3., 0.4, 0.02), (0., 0. , 0. ), (0., 0. , 0. ), (0., 0. , 0. ),\n", " (0., 0. , 0. ), (0., 0. , 0. ), (0., 0. , 0. ), (0., 0. , 0. ),\n", " (0., 0. , 0. ), (0., 0. , 0. )],\n", " dtype=[('count', '" ] }, "metadata": { "image/png": { "height": 248, "width": 378 }, "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "bins = [\n", " -10,\n", " -7,\n", " -4,\n", " -3,\n", " -2,\n", " -1,\n", " -0.75,\n", " -0.5,\n", " -0.25,\n", " 0,\n", " 0.25,\n", " 0.5,\n", " 0.75,\n", " 1,\n", " 2,\n", " 3,\n", " 4,\n", " 7,\n", " 10,\n", "]\n", "d7, e7 = np.histogram(data1 - 3.5, bins=bins, density=True)\n", "plt.hist(data1 - 3.5, bins=bins, density=True);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Yes, it's ugly. Don't judge.\n", "\n", "We don't have a `.density`! What do we do? (note: `density=True` is supported if you do not return a bh object)" ] }, { "cell_type": "code", "execution_count": 41, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([3. , 3. , 1. , 1. , 1. , 0.25, 0.25, 0.25, 0.25, 0.25, 0.25,\n", " 0.25, 0.25, 1. , 1. , 1. , 3. , 3. ])" ] }, "execution_count": 41, "metadata": {}, "output_type": "execute_result" } ], "source": [ "hist7 = bh.numpy.histogram(data1 - 3.5, bins=bins, histogram=bh.Histogram)\n", "\n", "widths = hist7.axes.widths\n", "area = functools.reduce(operator.mul, hist7.axes.widths)\n", "\n", "area" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Yes, that does not need to be so complicated for 1D, but it's general." ] }, { "cell_type": "code", "execution_count": 42, "metadata": {}, "outputs": [], "source": [ "factor = np.sum(hist7.values())\n", "view = hist7.values() / (factor * area)" ] }, { "cell_type": "code", "execution_count": 43, "metadata": {}, "outputs": [ { "data": { "image/png": "iVBORw0KGgoAAAANSUhEUgAAAvQAAAHwCAYAAADJpfudAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/YYfK9AAAACXBIWXMAABYlAAAWJQFJUiTwAAAud0lEQVR4nO3de5RldX3n/fdHQVSGbtDIg8EsWwgNjpnRRxCQniiXlQ6KF5zB0ZklQRLjGHgCITITHlEBE2ecNYmSJniNiIR4zygrAzgkQKuh40yC0Uwem4tIERQ0AoZL04LC9/lj7wpl2afrVNWuqvPrfr/WOuvXdX57f/f+bTZ1PmfX7+yTqkKSJElSmx630jsgSZIkaeEM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSw3ZZ6R2YdEluBVYBUyu8K5IkSdqxrQHuq6pnzWclA/3cVj3pSU96yrOf/eynrPSOSJIkace1efNmtm7dOu/1DPRzm3r2s5/9lOuvv36l90OSJEk7sIMPPpivfOUrU/Ndzzn0kiRJUsMM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSwwYL9EmekeSiJHckeSjJVJLzk+w1jxonJLkgyZeS3Jekklw6xnpJclKSjUnuSbI1ya1JPpVk7eJGJkmSJE2uXYYokmR/YBOwN3AZcANwKHA6cGySdVV19xil3go8F3gA+BZw0BjbfiLwaeBlwI3Ax4D7gZ8Gfh5YC9w0zyFJkiRJTRgk0APvpQvzp1XVBdNPJnk3cAbwTuBNY9Q5gy7IfwN4MXDtGOv8Hl2Y/y/AW6vq0ZmdSXYdZwCSJElSixY95SbJfsB6YAq4cFb3OcAW4MQku89Vq6quraqbq6rG3Pb+dG8U/go4e3aY72v+cJxakiRJUouGuEJ/dN9eNTtQV9X9Sa6jC/yHA1cPsL2Z/h3dm5KPAquSvBz4GeBu4Jqq+sa4hZJcP6Jrzmk/kiRJ0koZItAf2Lej5qnfTBfo1zJ8oH9B364GbgGeOqOvkryPbhrQIwNvV5IkSZoIQwT61X1774j+6ef3HGBbs+3dt+8A/hw4k27qz6HAB4BTgO8B585VqKoO3tbz/ZX75y9+VyVpx7LmrMu32z/1ruOWaU8kaee2HPehT9+ONS9+nh7ft3cCr6qqv6uqB6rqGuAE4FHgN5M8YQm2LUmSJK24Ia7QT1+BXz2if9Ws5Yb0/b79fFVtndlRVV9LciuwP/Bs4GtLsH1J0gjbu4Lv1XtJGs4QV+hv7NtRX+B0QN8uxb3gp7f9jyP6pwP/k5Zg25IkSdKKG+IK/fS94tcnedzMO90k2QNYB2wFvjzAtma7Gvh14OdmdyTZjcfeTEwtwbYlaYc21xx5SdJkWPQV+qq6BbgKWAOcOqv7PGB34JKq2gLdFz0lOai/h/xiXQl8E/jFJL8wq+9tdNOAvlBV3xlgW5IkSdLEGeqbYk8BNgEbkhwDbAYOA46im2pz9oxl9+37b6N7E/BPkhwPHN//uE/fvjDJxf2/76qqM6eXr6qHk5xE94biyiSf7eu+AHgR3R1u3jjEACVJkqRJNEigr6pbkhxCd/vIY4GX0t15ZgNwXlXdM2ap5wEnzXpuv/4BXVg/c2ZnVf1Fv+1z6N5A7Al8F/gg8NtV9a35jkeStLS85aUkDWeoK/RU1e3AyWMsN8Vjt7Kc3XcuY9wzfhvrfR14zXzXkyRJklq3HPehlyRJkrREDPSSJElSwwabciNJ0lCcYy9J4/MKvSRJktQwA70kSZLUMAO9JEmS1DDn0EvSTmyuueqSpMnnFXpJkiSpYQZ6SZIkqWEGekmSJKlhBnpJkiSpYQZ6SZIkqWEGekmSJKlhBnpJkiSpYQZ6SZIkqWEGekmSJKlhBnpJkiSpYQZ6SZIkqWG7rPQOSJKWzpqzLl/pXVgS2xvX1LuOW8Y9kaSV5xV6SZIkqWEGekmSJKlhBnpJkiSpYQZ6SZIkqWEGekmSJKlhBnpJkiSpYQZ6SZIkqWEGekmSJKlhBnpJkiSpYQZ6SZIkqWEGekmSJKlhBnpJkiSpYQZ6SZIkqWEGekmSJKlhBnpJkiSpYQZ6SZIkqWEGekmSJKlhBnpJkiSpYYMF+iTPSHJRkjuSPJRkKsn5SfaaR40TklyQ5EtJ7ktSSS6d5358uF+vkvzs/EciSZIktWOXIYok2R/YBOwNXAbcABwKnA4cm2RdVd09Rqm3As8FHgC+BRw0z/14OfDL/fr/bD7rSpIkSS0a6gr9e+nC/GlVdXxVnVVVRwPvAQ4E3jlmnTOAtcAq4NfmswNJngZ8CPgkcP181pUkSZJatehAn2Q/YD0wBVw4q/scYAtwYpLd56pVVddW1c1VVQvYlQ/27akLWFeSJElq0hBTbo7u26uq6tGZHVV1f5Lr6AL/4cDVA2zvJyR5PXA88KqqujvJQmqMuqo/r2k/kiRJ0nIaYsrNgX1704j+m/t27QDb+glJngn8PnBpVX1uKbYhSZIkTaohrtCv7tt7R/RPP7/nANv6MUkeB3yU7kOwpy2mVlUdPGIb1wPPX0xtSZIkaakMcpebOUzPf1nIvPi5nAG8GDiuqr6/BPUlSZKkiTbElJvpK/CrR/SvmrXcIJIcQHf3nI9U1RVD1pYkSZJaMUSgv7FvR82RP6BvR82xX6jnALsBJ8/4IqlKUnRX7QFu7p87fuBtS5IkSRNhiCk31/bt+iSPm3mnmyR7AOuArcCXB9jWTFPAh0f0HQfsA3wauK9fVpIkSdrhLDrQV9UtSa6iuzXlqcAFM7rPA3YHPlBVWwCS7ArsD/ywqm5ZxHa/CrxhW31JNtIF+rdU1TcWug1JkiRp0g31odhTgE3AhiTHAJuBw4Cj6KbanD1j2X37/tuANTOL9FNjju9/3KdvX5jk4v7fd1XVmQPtsyRJktS8QQJ9f5X+EOAdwLHAS4E7gQ3AeVV1z5ilngecNOu5/foHdG8CDPSSJElSb7DbVlbV7cDJYyw3xWO3spzddy5w7gD7cuRia0iSJEktGOIuN5IkSZJWiIFekiRJapiBXpIkSWqYgV6SJElqmIFekiRJapiBXpIkSWqYgV6SJElq2GD3oZckjbbmrMtXehckSTsoA70kaYeykm+ept513IptW9LOyyk3kiRJUsMM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSwwYL9EmekeSiJHckeSjJVJLzk+w1jxonJLkgyZeS3Jekkly6neUPSPJbSa5JcnuSh5N8N8llSY4aZmSSJEnS5NpliCJJ9gc2AXsDlwE3AIcCpwPHJllXVXePUeqtwHOBB4BvAQfNsfxvA68Bvg5cAdwDHAi8AnhFktOrasP8RyRJkiS1YZBAD7yXLsyfVlUXTD+Z5N3AGcA7gTeNUecMuiD/DeDFwLVzLP954L9W1d/MfDLJi4E/A/5bkk9X1Z3jDkSSJElqyaKn3CTZD1gPTAEXzuo+B9gCnJhk97lqVdW1VXVzVdU4266qi2eH+f75LwAbgScAR4xTS5IkSWrREHPoj+7bq6rq0ZkdVXU/cB3wZODwAbY1Hz/s2x8t83YlSZKkZTPElJsD+/amEf03013BXwtcPcD25pTkmcAxwIPAF8dc5/oRXXPN45ckSZJWzBCBfnXf3juif/r5PQfY1pyS7Ab8MbAb8J+q6vvLsV1JkiRpJQz1odjtSd+ONS9+URtKHg/8EbAO+CTwu+OuW1UHj6h5PfD8QXZQkiRJGtgQc+inr8CvHtG/atZyS6IP85cCrwY+Bbxu3A/XSpIkSa0aItDf2LdrR/Qf0Lej5tgvWpJdgI8DrwU+Bvz7qvLDsJIkSdrhDRHop+8Vvz7Jj9VLsgfd9JetwJcH2NZPSPIE4DN0V+YvAU6sqkeWYluSJEnSpFl0oK+qW4CrgDXAqbO6zwN2By6pqi0ASXZNclD/7bKL0n8A9rPAK4EPAyfPvnWmJEmStCMb6kOxpwCbgA1JjgE2A4cBR9FNtTl7xrL79v230b0J+CdJjgeO73/cp29fmOTi/t93VdWZM1Z5P/BS4C7g28DbkzDLxqrauKBRSZIkSRNukEBfVbckOQR4B3AsXci+E9gAnFdV94xZ6nnASbOe269/QPcmYGagf1bf/hTw9u3U3Tjm9iVJkqSmDHbbyqq6HTh5jOWmeOxWlrP7zgXOncc2jxx3WUmSJGlHNMSHYiVJkiStEAO9JEmS1DADvSRJktQwA70kSZLUMAO9JEmS1DADvSRJktQwA70kSZLUMAO9JEmS1DADvSRJktQwA70kSZLUMAO9JEmS1DADvSRJktQwA70kSZLUMAO9JEmS1DADvSRJktQwA70kSZLUMAO9JEmS1DADvSRJktQwA70kSZLUMAO9JEmS1DADvSRJktQwA70kSZLUMAO9JEmS1DADvSRJktQwA70kSZLUMAO9JEmS1DADvSRJktQwA70kSZLUMAO9JEmS1DADvSRJktQwA70kSZLUMAO9JEmS1DADvSRJktQwA70kSZLUMAO9JEmS1DADvSRJktQwA70kSZLUsMECfZJnJLkoyR1JHkoyleT8JHvNo8YJSS5I8qUk9yWpJJeOsd4RSa5Ick+SB5P8bZLfSPL4xY1KkiRJmmy7DFEkyf7AJmBv4DLgBuBQ4HTg2CTrquruMUq9FXgu8ADwLeCgMbb9SuBPgB8AnwTuAV4OvAdYB7x6vuORJEmSWjHUFfr30oX506rq+Ko6q6qOpgvVBwLvHLPOGcBaYBXwa3MtnGQV8CHgEeDIqvqVqvqPwPOAvwROSPLa+Q5GkiRJasWiA32S/YD1wBRw4azuc4AtwIlJdp+rVlVdW1U3V1WNufkTgKcBn6iqv55R5wd0V/thjDcGkiRJUquGuEJ/dN9eVVWPzuyoqvuB64AnA4cPsK1R2/78Nvq+CDwIHJFktyXYtiRJkrTihphDf2Df3jSi/2a6K/hrgasH2N5Y266qHyW5FXgOsB+weXuFklw/omvOefySJEnSShniCv3qvr13RP/083sOsK1J2rYkSZK04ga5y80c0rfjzotfkW1X1cHbLNBduX/+kDslSZIkDWWIK/TTV8FXj+hfNWu5Ia3ktiVJkqQVN0Sgv7Fv147oP6BvR82xX5JtJ9kFeBbwI+CbS7BtSZIkacUNEeiv7dv1SX6sXpI96L7caSvw5QG2Nds1fXvsNvpeRHd3nU1V9dASbFuSJElacYsO9FV1C3AVsAY4dVb3ecDuwCVVtQUgya5JDuq/XXaxPgPcBbw2ySHTTyZ5IvA7/Y/vG2A7kiRJ0kQa6kOxpwCbgA1JjqG7ReRhwFF0U23OnrHsvn3/bXRvAv5JkuOB4/sf9+nbFya5uP/3XVV15vTyVXVfkl+lC/Ybk3wCuAd4Bd0tLT8DfHKIAUqSJEmTaJBAX1W39FfI30E3/eWlwJ3ABuC8qrpnzFLPA06a9dx+/QO6NwFnzuysqs8leTHdm4Z/AzwR+Abwm8CGeXzrrCRJktScwW5bWVW3AyePsdwUj91OcnbfucC5C9j2dXRvIiRJkqSdynLch16SJsKasy5f6V3QDm6lzrGpdx23ItuVNBmGuMuNJEmSpBVioJckSZIaZqCXJEmSGmaglyRJkhpmoJckSZIaZqCXJEmSGmaglyRJkhpmoJckSZIaZqCXJEmSGmaglyRJkhpmoJckSZIaZqCXJEmSGmaglyRJkhpmoJckSZIaZqCXJEmSGmaglyRJkhpmoJckSZIaZqCXJEmSGmaglyRJkhpmoJckSZIaZqCXJEmSGmaglyRJkhpmoJckSZIaZqCXJEmSGmaglyRJkhpmoJckSZIaZqCXJEmSGmaglyRJkhpmoJckSZIaZqCXJEmSGmaglyRJkhpmoJckSZIaZqCXJEmSGmaglyRJkhpmoJckSZIaZqCXJEmSGjZYoE/yjCQXJbkjyUNJppKcn2SvpayTZLckpyb530nuSvJAks1JNiR55jCjkyRJkibTLkMUSbI/sAnYG7gMuAE4FDgdODbJuqq6e+g6SXYBrgbW9ct+HHgIeAHw68AvJTmiqr4+xDglSZKkSTNIoAfeSxfCT6uqC6afTPJu4AzgncCblqDOq+jC/NXA+qp6dMY65wFvB84Efnlhw5IkSZIm26Kn3CTZD1gPTAEXzuo+B9gCnJhk9yWos1/fXj4zzPcu69unzT0KSZIkqU1DzKE/um+vmh2qq+p+4DrgycDhS1Dn/+vblySZPZaX9e2fzzkCSZIkqVFDTLk5sG9vGtF/M92V97V0U2OGrHM58N+Bfw38nyR/DjwMHAz8K+AC4A/mHgIkuX5E10HjrC9JkiSthCEC/eq+vXdE//Tzew5dp6oqyQl0c+XfBvzzGctfDXysqh6ZY7uSJElSs4b6UOz2pG9r6DpJnghcArwEOJVu3vyDdB+U3QB8Mcmrq+oy5lBVB29zo92V++cvbtclSZKkpTHEHPrpK+erR/SvmrXckHXOAl4NnF1VH6iq71TVfVV1JXACsCvw+3NsV5IkSWrWEIH+xr5dO6L/gL4dNTd+MXWmP/h67eyFq+prwD3AM5M8dY5tS5IkSU0aItBPh+n1s+80k2QPuukvW4EvL0Gd3fr2J25NmWQ3Hruq//Ac25YkSZKatOhAX1W3AFcBa+jmsc90HrA7cElVbQFIsmuSg/pvhV1wnd6X+vYtfYCf6Vy6zwj8VX/bS0mSJGmHM9SHYk8BNgEbkhwDbAYOA46imyJz9oxl9+37b6ML7wutA903x74cOAa4Icnn6a7irwMO7f99+iAjlCRJkibQEFNupq+uHwJcTBfA3wzsT3enmRdW1d1LUaeqvk13B5rfA34AnAz8P8A+fY3nV9VfLmpwkiRJ0gQb7LaVVXU7XaCea7kpHrsF5YLrzFj+e8CZ/UOSJEnaqQxyhV6SJEnSyjDQS5IkSQ0z0EuSJEkNM9BLkiRJDTPQS5IkSQ0z0EuSJEkNM9BLkiRJDTPQS5IkSQ0z0EuSJEkNM9BLkiRJDTPQS5IkSQ0z0EuSJEkNM9BLkiRJDTPQS5IkSQ0z0EuSJEkNM9BLkiRJDTPQS5IkSQ0z0EuSJEkNM9BLkiRJDTPQS5IkSQ0z0EuSJEkNM9BLkiRJDTPQS5IkSQ0z0EuSJEkNM9BLkiRJDTPQS5IkSQ0z0EuSJEkNM9BLkiRJDTPQS5IkSQ0z0EuSJEkNM9BLkiRJDTPQS5IkSQ0z0EuSJEkNM9BLkiRJDTPQS5IkSQ0z0EuSJEkNM9BLkiRJDRss0Cd5RpKLktyR5KEkU0nOT7LXUtdJ56QkG5Pck2RrkluTfCrJ2sWPTpIkSZpMuwxRJMn+wCZgb+Ay4AbgUOB04Ngk66rq7qWok+SJwKeBlwE3Ah8D7gd+Gvh5YC1w0wDDlCRJkibOIIEeeC9dCD+tqi6YfjLJu4EzgHcCb1qiOr9HF+b/C/DWqnp0ZmeSXec9GkmSJKkRi55yk2Q/YD0wBVw4q/scYAtwYpLdh67TX9F/E/BXwNmzwzxAVf1wHsORJEmSmjLEHPqj+/aq2YG6qu4HrgOeDBy+BHX+Hd0YPgqsSvK6JP9vkjcm+dkFjUaSJElqyBBTbg7s21Hz1G+mu/K+Frh64Dov6NvVwC3AU2csX0neRzd955HtbBeAJNeP6DpornUlzc+asy5f6V2QJGmHMUSgX923947on35+zyWos3ffvgP4c+BMuik7hwIfAE4BvgecO8e2JUlq1kq9SZ5613Ersl1JP26oD8VuT/q2lqDO4/v2TuBVVbW1//maJCcAXwF+M8l/rqqHt1e8qg7e5ka7K/fPX/huS5IkSUtniDn001fOV4/oXzVruSHrfL9vPz8jzANQVV8DbgX2AJ49x7YlSZKkJg0R6G/s21Ff4HRA3851L/iF1Jle5x9HrDMd+J80x7YlSZKkJg0R6K/t2/VJfqxekj2AdcBW4MtLUGf6w7E/N7tYkt147E3A1BzbliRJkpq06EBfVbcAVwFrgFNndZ8H7A5cUlVboPuipyQH9feQX3Cd3pXAN4FfTPILs9Z5G930nS9U1XcWNjpJkiRpsg31odhTgE3AhiTHAJuBw4Cj6KbInD1j2X37/tvowvtC61BVDyc5ie6NwJVJPtvXfQHwIro73LxxoDFKkiRJE2eIKTfTV9cPAS6mC+BvBvYHNgAvrKq7l6pOVf1Fv86fAC8GTgP2Az4IPL+q5pq7L0mSJDVrsNtWVtXtwMljLDfFY7egXHCdWet8HXjNfNaRJEmSdgSDXKGXJEmStDIM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSwwz0kiRJUsMGC/RJnpHkoiR3JHkoyVSS85PstZx1knw4SfWPn13YaCRJkqQ27DJEkST7A5uAvYHLgBuAQ4HTgWOTrKuqu5e6TpKXA78MPAD8s0UNSpIkSWrAUFfo30sXwk+rquOr6qyqOhp4D3Ag8M6lrpPkacCHgE8C1y98KJIkSVI7Fh3ok+wHrAemgAtndZ8DbAFOTLL7Etf5YN+eOu6+S5IkSa0b4gr90X17VVU9OrOjqu4HrgOeDBy+VHWSvB44HnjTOFN7JEmSpB3FEIH+wL69aUT/zX27dinqJHkm8PvApVX1uTm2MVKS67f1AA5aaE1JkiRpqQ0R6Ff37b0j+qef33PoOkkeB3yU7kOwp81RX5IkSdrhDHKXmzmkb2sJ6pwBvBg4rqq+v5jiVXXwNjfaXaV//mJqS5IkSUtliCv001fOV4/oXzVruUHqJDmA7q43H6mqK8bYT0mSJGmHM0Sgv7FvR82RP6BvR82NX2id5wC7ASfP+CKpSlJ0V+0Bbu6fO36ObUuSJElNGmLKzbV9uz7J42beoSbJHsA6YCvw5YHrTAEfHlHrOGAf4NPAff2ykiRJ0g5n0YG+qm5JchXdPeRPBS6Y0X0esDvwgaraApBkV2B/4IdVdctC61TVV4E3bGufkmykC/RvqapvLHaMkiRJ0qQa6kOxpwCbgA1JjgE2A4cBR9FNkTl7xrL79v23AWsWUUeSJEna6Q0xh57+SvshwMV0AfzNdFfhNwAvHPfLnoaqI0mSJO0sBrttZVXdDpw8xnJTPHYLygXXmWMbRy5mfUmSJKkVg1yhlyRJkrQyDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDDPSSJElSwwz0kiRJUsMM9JIkSVLDBgv0SZ6R5KIkdyR5KMlUkvOT7LVUdZIckOS3klyT5PYkDyf5bpLLkhw11NgkSZKkSbXLEEWS7A9sAvYGLgNuAA4FTgeOTbKuqu5egjq/DbwG+DpwBXAPcCDwCuAVSU6vqg1DjFGSJEmaRIMEeuC9dCH8tKq6YPrJJO8GzgDeCbxpCep8HvivVfU3M4skeTHwZ8B/S/LpqrpzQaOSJEmSJtyip9wk2Q9YD0wBF87qPgfYApyYZPeh61TVxbPDfP/8F4CNwBOAI8YfjSRJktSWIebQH923V1XVozM7qup+4DrgycDhy1Rn2g/79kdjLi9JkiQ1Z4hAf2Df3jSi/+a+XbtMdUjyTOAY4EHgi3Mt369z/bYewEHjrC9JkiSthCHm0K/u23tH9E8/v+dy1EmyG/DHwG7Af6qq78+xXUmSJKlZQ30odnvSt7XUdZI8HvgjYB3wSeB3xy1eVQePqHk98Pzxd1OSJElaPkNMuZm+cr56RP+qWcstSZ0+zF8KvBr4FPC6qlrsmwhJkiRpog0R6G/s21Fz2w/o21Fz4xddJ8kuwMeB1wIfA/59VflhWEmSJO3whgj01/bt+iQ/Vi/JHnTTX7YCX16KOkmeAHyG7sr8JcCJVfXIAsYhSZIkNWfRgb6qbgGuAtYAp87qPg/YHbikqrYAJNk1yUH9t8IuuE5fazfgs8ArgQ8DJ8++5aUkSZK0IxvqQ7GnAJuADUmOATYDhwFH0U2ROXvGsvv2/bfRhfeF1gF4P/BS4C7g28Dbk8xahI1VtXHhQ5MkSduy5qzLV3oXdipT7zpupXdBE2qQQF9VtyQ5BHgHcCxdyL4T2ACcV1X3LFGdZ/XtTwFv307pjWMORTsxX5gkSVKLBrttZVXdDpw8xnJTPHYLygXX6Zc9cszdkyRJknZIQ3woVpIkSdIKMdBLkiRJDTPQS5IkSQ0z0EuSJEkNM9BLkiRJDTPQS5IkSQ0z0EuSJEkNM9BLkiRJDTPQS5IkSQ0z0EuSJEkNM9BLkiRJDTPQS5IkSQ0z0EuSJEkNM9BLkiRJDTPQS5IkSQ0z0EuSJEkNM9BLkiRJDTPQS5IkSQ0z0EuSJEkNM9BLkiRJDTPQS5IkSQ0z0EuSJEkNM9BLkiRJDTPQS5IkSQ0z0EuSJEkNM9BLkiRJDTPQS5IkSQ0z0EuSJEkNM9BLkiRJDTPQS5IkSQ0z0EuSJEkNM9BLkiRJDTPQS5IkSQ0z0EuSJEkNM9BLkiRJDTPQS5IkSQ0z0EuSJEkNGyzQJ3lGkouS3JHkoSRTSc5PstdS10lyRJIrktyT5MEkf5vkN5I8fvEjkyRJkibXLkMUSbI/sAnYG7gMuAE4FDgdODbJuqq6eynqJHkl8CfAD4BPAvcALwfeA6wDXj3EGCVJkqRJNNQV+vfShfDTqur4qjqrqo6mC9UHAu9cijpJVgEfAh4BjqyqX6mq/wg8D/hL4IQkr1388CRJkqTJtOgr9En2A9YDU8CFs7rPAd4InJjkzVW1ZeA6JwBPAy6pqr+eXriqfpDkrcDVwK8Bn1jY6FbWmrMuX+ldkCRJ0oQbYsrN0X17VVU9OrOjqu5Pch1dUD+cLmAPWWd6nc9vo94XgQeBI5LsVlUPjTsgSZKkSeOFvuUz9a7jVnoX5mWIQH9g3940ov9muiC+lu0H+oXUGblOVf0oya3Ac4D9gM3b2TZJrh/R9dzNmzdz8MEHb2/1JXHnt+9d9m1KkiTt7A7+s7evyHY3b94MsGa+6w0R6Ff37aj0Of38nktQZ6htb88jW7duvfcrX/nK1CJqLMRBfXvDMm+3VR6v+fOYzY/Ha348XvPj8Zofj9f8eLzm56CvfBdYmeO1BrhvvisNcpebOaRvawXqjL1OVS3/JfjtmP6LwaTt16TyeM2fx2x+PF7z4/GaH4/X/Hi85sfjNT8tHq8h7nIzfRV89Yj+VbOWG7LOUNuWJEmSmjREoL+xb9eO6D+gb0fNjV9MnZHrJNkFeBbwI+Cbc2xbkiRJatIQgf7avl2f5MfqJdmD7sudtgJfXoI61/Ttsduo9yLgycAm73AjSZKkHdWiA31V3QJcRTeJ/9RZ3ecBu9PdJ34LQJJdkxzUfyvsguv0PgPcBbw2ySHTTyZ5IvA7/Y/vW/DgJEmSpAk31IdiTwE2ARuSHEN3i8jDgKPopsicPWPZffv+2/jJ2/LMpw5VdV+SX6UL9huTfAK4B3gF3S0tPwN8cqAxSpIkSRMnVYu9+UxfKPkZ4B1001+eCtwJfA44r6rumbHcGuBW4LaqWrPQOrPWWUcX9l8IPBH4BnARsKGqHhlifJIkSdIkGizQS5IkSVp+Q3woVpIkSdIKMdBLkiRJDTPQS5IkSQ0z0EuSJEkNM9BLkiRJDTPQS5IkSQ0z0C+T/htyT0/ykSRfTfJwkkryhjHWPSnJ/07yQJJ7k2xM8rIF7sdgtVZCkov747a9x9Vj1lozR51PLPV4ltpSjDHJEUmuSHJPkgeT/G2S30jy+KUYw3JKckCS30pyTZLb+/9Pv5vksiRHzbPWDnV+JXlGkouS3JHkoSRTSc5PstdK1JlUSZ6a5A1JPpvkG0m29r9r/yLJryQZ+3W3Pzajzp/vLOU4ltuQY90JzrHXj/E6ONZ38OxI51iSE5JckORLSe7rx3DpHOsM9nq20q+NQ31TrOa2O3B+/+/vAt8BfmaulZL8LvBm4FvAh4AnAK8F/jTJr1fVH4y7A0PWWkGfA6ZG9J0I7AdcOc+aX+vrzvZ386wzyQYZY5JXAn8C/IDuW5jvAV4OvAdYB7x6UXu58n4beA3wdeAKuvEdSPft069IcnpVbZhnzebPryT7032L997AZcANwKHA6cCxSdZV1d3LVWfCvRp4H92XIl4L/D3wfwH/GvhD4CVJXl3jfwnMvTz22jHTA4vf1Ymz6LHuJOfYV4HzRvT9PHA083sd3FHOsbcCz6Xb728BB21v4SFfzybitbGqfCzDgy48vwR4ev/zuUABb9jOOkf0y3wD2GvG82uAu/sTZ82Y2x+s1iQ+gD2BB4GHgJ8ac501/TG5eKX3fwmPy2BjBFYB/9Af40NmPP9EuhfQAl670mNe5BhfD/zf23j+xcDD/difvtzHfqUfwP/sx/Lrs55/d//8+5ezziQ/6MLUy4HHzXp+H7pwX8C/GbPWFDC10mNapuM2yFh3hnNsjvH/ZT/OVyzncZ+EB3AUcAAQ4Mj+OFw6YtnBXs8m5bXRKTfLpKoerqorq+rOeaz2pr59Z1V9f0atKeBCYDfg5BWoNYlOBJ4E/Pequmuld2YHdQLwNOATVfXX009W1Q/orowA/NpK7NhQquriqvqbbTz/BWAj3RvzI5Z7v1ZSkv2A9XQv/BfO6j4H2AKcmGT35agz6arqmqr606p6dNbz3wHe3/945LLv2E5gZznHRknyc8DhwLeBy1d4d5ZdVV1bVTdXn6bnMOTr2US8NhroJ9vRffv5bfRdOWuZ5aw1iX61bz+4gHV/Osl/SPKWvv2XQ+7YhBhijNs7h75I9xeSI5LstuC9nGw/7NsfzXO91s+v6f/uV20jpN4PXAc8mS5ILEedli3kHNotyev68+f0JEct15zcFbDYse7s59h/6NsPV9VYc+h7O9M5Nm3I17OJeG10Dv2E6q8g7As8MOKq/s19u3Y5a02iJC8E/gVwU1Vdu4ASv9A/ZtbcCJxUVX+/+D2cCEOM8cC+vWl2R1X9KMmtwHPoPseweeG7OnmSPBM4hu4X8xfnuXrr59fI/+69m+muiq4FtveB9KHqNCnJLsAv9T9u64V/lH2AP5r13K1JTu7/crQjWexYd9pzLMmTgNcBj9J9VmM+dqZzbNqQr2cT8droFfrJtbpv7x3RP/38nstcaxK9sW8/NM/1HqT7EOTBwF7948V0H2Q7Erh6B/jT7JBj3NHPo23qr6r8Md20tHNnTlmbw45yfg31332nPH9meBfwc8AVVfU/x1znI3RvJPehu7HCvwA+QPf5jCuTPHcJ9nOlDDHWnfkc+7d047qyqm6fx3o70zk20w6XsQz08zDH7Z229dju7ZIGMu6dEpa71khDHsckq+l+kT0MXDyf/aiqf6iqt1fVV6rqH/vHF+mu4Pwv4GeBOW8rutQWc7yWeYyZ3uxA9Ra2E8OeX4+nu3K1ju7OBb877n60cn4NYKj/7hNx/iyFJKfR3WHsBrrP+4ylqs7r5+R/t6oerKq/q6o30X3A80l0N1fYISzTWHfYc4zHLmx9YD4r7Uzn2DwNea4sy3nnlJv5uYXubjDjumMR25p+R7d6RP9c7wiXqtYQhjyOr6ObE/mJoT4M2/+J7A+Bw4AXAb8/RN1FGPy8W+AY5zqPVs1abqUMcrz6MH8p3e3GPgW8bswPW23XBJ5fcxnqv3sr58+gkpxK99/468AxVXXPAGXfT/cG4UUD1Jp08xnrznqO/XO6D+t/i+52u0PY0c+xIc+ViTjvDPTzUFXHLOO2tiT5NrBvkqdvY+77AX07aq7gktQawsDHcfrDsPO6KjGG7/Xtik+JWMLzbr5jvBE4hG7+6fUzO/r5wc+i+7DfN4fawYUY4nj14/kYXZj/GPBL8/yQ2Vwm5vwaw419O+ozNuP+/hiqTjOS/Abdfaj/ji7M/8NApafrtHD+LNZ8xrrTnWO9hX4Ydnt29HNsyNeziXhtdMrNZLumb4/dRt9LZi2znLUmQpLD6L5E4qaq2jhw+em7IKxoOF1i8x3j9s6hF9H9pWRTVT202B1bSUmeAHyGLsxfApw4cJiHts6v6Q+ar8+sbzlNsgfddKStwJeXqU4TkvwWXZj/KnDUgGEe4IV928L5s1jzGetOdY4BJHki3TSuR4EPD1h6Rz/Hhnw9m4zXxiFuZu9j/g+W6IulgKfTfTva6sXWmvQH3S+vAt48x3Kr+2Py9FnPHwY8YRvLH90fjwKOWOlxLvIYzXuM2zleq+iuLO/IXyy1G939m4vuThGPG2OdHf78Yh5f1gPs2h+P/RdTp+UH8LZ+PH8NPGWOZbd5vOjuivET6wLPpLtbSwFvWemxDnS85jVWz7EfG9eJ/bj+1HPsx8ZwJHN/sdS8Xs8m/bUx/Ua1DJKcxWNfRfw8uqvLm3jstpF/UVV/OGud3wN+k25u3GfovtjmNcBT6X5h/cGs5S8GTgJOrqqLF1NrkiVZRTf3eVdg39rO/Pkkr6f7JP9Hq+r1M57fSPcLbSPdMQH4lzx2T9m3VdXvDLzry2ohYxx1vPq+4+nOnR8An6D7eutX0N226zPAv62Gf6kk+Qjdt8XeBbyXbX+IaWPN+IvQznB+Jdmf7nfV3sBldLdeO4zumxlvontjcne/7BrgVuC2qlqz0DqtSnIS3Qf0HwEuYNvzZqemfz+POl5JzgXOorvqfCtwP7A/cBxdULgCeFVVPbwkA1lG8x3rzn6OzZTkS8C/ovtm2D8dscwadoJzrH99Or7/cR/gF+n+wvCl/rm7qurMWcuP/Xo28a+NK/0uamd60L2w13YeF49Y7yTgr+i+5e5+4AvAy0Yse3Ff6/WLrTXJD7pvXSvg42Ms+/ptHV/gV4D/Qfetgg/Qvbv+e7q7mfz8So9xoOM07zGOOl4z+tfR/aL/Pt2fr/8PcAbw+JUe7wDHa67/R4vu1pU73fkF/Azdi9mddHeVuo3uw55PmbXcmv54TC2mTqsPHvvr6/YeG+c6XnS3OP043Z1x/pHuS6m+B/wZ3f3ss9JjHfCYzWusO/s5NmOcz+6Pw+3b+/27s5xjY/y/9xPnC/N4PRv1u34htZbi4RV6SZIkqWF+KFaSJElqmIFekiRJapiBXpIkSWqYgV6SJElqmIFekiRJapiBXpIkSWqYgV6SJElqmIFekiRJapiBXpIkSWqYgV6SJElqmIFekiRJapiBXpIkSWqYgV6SJElqmIFekiRJapiBXpIkSWqYgV6SJElqmIFekiRJatj/D6VSrTl6ap2QAAAAAElFTkSuQmCC\n", "text/plain": [ "
" ] }, "metadata": { "image/png": { "height": 248, "width": 378 }, "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "plt.bar(hist7.axes[0].centers, view, width=hist7.axes[0].widths);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 8: Axis types\n", "\n", "There are more axes types, and they all provide the same API in histograms, so they all just work without changes:" ] }, { "cell_type": "code", "execution_count": 44, "metadata": {}, "outputs": [], "source": [ "hist8 = bh.Histogram(\n", " bh.axis.Regular(30, 1, 10, transform=bh.axis.transform.log),\n", " bh.axis.Regular(30, 1, 10, transform=bh.axis.transform.sqrt),\n", ")" ] }, { "cell_type": "code", "execution_count": 45, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Histogram(\n", " Regular(30, 1, 10, transform=log),\n", " Regular(30, 1, 10, transform=sqrt),\n", " storage=Double()) # Sum: 903807.0 (1000000.0 with flow)" ] }, "execution_count": 45, "metadata": {}, "output_type": "execute_result" } ], "source": [ "hist8.reset()\n", "hist8.fill(*make_2D_data(mean=(5, 5), widths=(5, 5)))" ] }, { "cell_type": "code", "execution_count": 46, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "image/png": { "height": 252, "width": 375 }, "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "plothist2d(hist8);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 9: And, circular, too!" ] }, { "cell_type": "code", "execution_count": 47, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Histogram(Regular(30, 0, 6.28319, circular=True), storage=Double()) # Sum: 300.0" ] }, "execution_count": 47, "metadata": {}, "output_type": "execute_result" } ], "source": [ "hist9 = bh.Histogram(bh.axis.Regular(30, 0, 2 * np.pi, circular=True))\n", "hist9.fill(np.random.uniform(0, np.pi * 4, size=300))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, the really complicated part, making a circular histogram:\n" ] }, { "cell_type": "code", "execution_count": 48, "metadata": {}, "outputs": [ { "data": { "image/png": "iVBORw0KGgoAAAANSUhEUgAAAisAAAIaCAYAAADhiA0gAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/YYfK9AAAACXBIWXMAABYlAAAWJQFJUiTwAAEAAElEQVR4nOydd3Qc1dmHnzur3nuXLdmSJVtyk2XLvYFpAUKA0CGkQKhJgJAEQksgIaTARyCFGpIQQggQQgvdvUqyXGWrWFbvvWu1O/f7Y7VCtiVL2qKV7fucM2dXszN37q52Z37zViGlRKFQKBQKhWKyorl6AgqFQqFQKBQnQ4kVhUKhUCgUkxolVhQKhUKhUExqlFhRKBQKhUIxqVFiRaFQKBQKxaRGiRWFQqFQKBSTGiVWFAqFQqFQTGqUWFEoFAqFQjGpUWJFoVAoFArFpEaJFYVCoVAoFJMaJVYUCoVCoVBMapRYUSgUCoVCMalRYkWhUCgUCsWkRokVhUKhUCgUkxolVhQKxZgRFr4lhNghhOgQQnQLIfKEEN8TQhhG2GepEOJDIUTzwPb7hBA/OMn2ZwkhcgbG3yGEWOHcd6VQKCY7Qkrp6jkoFIpTBCHE34DrgXrgPaALOBuYBbwFfF0OOakIIb46sL4X+BfQDFwEpABvSim/ftz46UAusBvYDKwA5gEZUspDznxvCoVi8qLEikKhGBNCiEuA/wBHgUVSysaB9e7AG8AlwDellK8MrA8AioFAYJmUMmdgvRfwBbAEuFpK+fqQY/wSWAdkSSl1IYQG7AQ+llI+MAFvU6FQTEKUG0ihUIyVSwcef2cVKgBSyn7gwYE/7xyy/eVAOPC6VagMbN8LWIXHrccdwwCYAetdlBz4e1iXkUKhODNQYkWhUIyVqIHHkmFes67LEEIEDTxfO/D40TDbbwK6gaVCCM8h698AFgCbhBBPABuB+cA/7Zi3QqE4xVFiRaFQjBWrNSVxmNemDXmeOvCYMvBYePzGUkoTFneS29B9pZS5WNxJ/sDtgA9wgZRynz0TVygUpzZKrCgUirHy/sDj3UKIEOtKIYQb8LMh2wUPPAYOPLaNMJ51fdDQlVLKD6SU86SUflLKTCnl5/ZNW6FQnOq4uXoCCoXilOF14DrgfCBfCPEuFlfO2cB0oAhIxhJjMhbEwKOK8lcoFCdFWVYUCsWYkFLqwMXAD4FaLCnM3wIqgeVA08Cm9QOPVstJIMMTcNx2CoVCMSwqdVmhUNiNEMIbSw0VCQRKKfuFEK8C1wLXSCn/edz2blhEigfgJ6Xsm+g5KxSKUwdlWVEoFI7gesALeGMglRkstVQAzhtm+5VYgme3KaGiUChGQ1lWFArFmBFCBEgp249btxD4GHAH5kopS6zbAkewuHvGVBROoVAohkOJFYVCMWaEEDuBHuAA0AGkARcAfcClUsqPj9v+EuBNLOX2X8fiKrqYgXL7wBVSnYQUCsUoKLGiUCjGjBDiXuAqLNk/3kA1FqvKr6SUpSPsswz4KRZLiheWEvwvA7+XUo41c0ihUJzBKLGiUCgUCoViUqMCbBUKhUKhUExqlFhRKBQKhUIxqVFiRaFQKBQKxaRGiRWFQqFQKBSTGiVWFAqFQqFQTGqUWFEoFAqFQjGpUWJFoVAoFArFpEaJFYVCoVAoFJMaJVYUCoVCoVBMapRYUSgUCoVCMalRYkWhUCgUCsWkRokVhUKhUCgUkxolVhQKhUKhUExqlFhRKBQKhUIxqVFiRaFQKBQKxaRGiRWFQqFQKBSTGiVWFAqFQqFQTGqUWFEoFAqFQjGpUWJFoVAoFArFpEaJFYVCoVAoFJMaN1dPQKFQnBoIIQTgA/gB/kMe/YdZ5wcYsNwQaUOeS0A/bjEBnQNLx8DSedyj9XmPlFI6/c0qFIpJhVC/e4XizEYIoQFhQDQQM/AYPfRvTdNipZSRUkoP180UhBB9QohaXdergBqgeuCx5ri/m5SoUShOH5RYUSjOAAYESQwwY+gihEiRUiYwgpU1MDBQj4mJETExMSI6OprQ0FD8/f3x9/fHz8/vmMehz319fXF3d0fTtMFl8+bNAKxYsQJd1weX/v5+Ojs76ejoOOHx+OdNTU1UV1dTXV0tq6urZUdHx7CubCFEP1AipSwACo9bapWQUShOLZQbSKE4jRBCGIAUIANIxSJIZgghZkgpvYduGxoaqqempmrTp08nNjaWmJgYoqOjj1m8vLwcFtfm7u4OgJeX1wmvhYaGjnc4AYju7m5qa2uprq6mpqZmcKmsrHQ/cuRIyqFDh5JbW1uPeQ9CiC4hxFARcwjYDRRLKfXxvzOFQuFslFhRKE5RBoRJKrDAuggh5kspfazb+Pj4yJSUFDFjxgyGLsnJyQQHB5/yAfY+Pj5MmzaNadOmjbSJ1tTURGFh4dDFt6CgYH5RUdH83t5eMbihpnUIIXYDuQPLbqBQCRiFwvUoN5BCcQowENw6HVgGZAIZA8Jk0FoydepUPTMzU1uwYAELFiwgPT2d6OhoLLu6ng0bNgCwevVql87Diq7rVFVVsX//fnJzc8nNzSUnJ0evqqoaFHFCiC4ppVXA5ABbpJRlLpu0QnGGosSKQjEJGSJOVgOrNU1bo+t6jPX1xMREmZmZKRYsWEBGRgYZGRm2uFImlMkmVkaivr6e3bt3HyNgKioqBgWMpmlluq6vBzYAG5R4USicjxIrCsUkYECcTONYcRIL4ObmJhcvXixWr17NypUryczMJDg42JXTtYlTRawMR0NDAzk5OWzatIkNGzaQnZ2N2WwGlHhRKCYCJVYUChchhPADzgEu0jRt3XDiZPXq1SxZsgQfH5+TD3YKcCqLlePp6Ohg69atbNiwgQ0bNpCTkzNUvJTquv4p8C7wuZSyx6WTVShOA5RYUSgmECFEHHARcLEQ4iwppbvBYGDx4sWsWbPmtBInx3M6iZXjGSpe1q9fT3Z2NlJKhBC9UspPsAiXD6SUta6eq0JxKqLEikLhRAbcO/OxiJOLpJQZAP7+/vKCCy4QF198Meeddx4hISGunegEcDqLleOpr6/nww8/5N133+Wjjz6SPT091ijnnViEy3vAAVXvRaEYG0qsKBQOZkCgLAWu0TTtEmtg7NSpU+VXv/pVcdFFF7Fy5Uo8PFxaDHbCOZPEylB6e3tZv3497777Lv/973/1mpoaDQZjXf4DvAbkKOGiUIyMEisKhYMQQswErtU07Vpd1xMAFi1axFe/+lUuuugi0tPTJ00asSs4U8XKUKSU5OXlWYWL3LNnjwAQQhRJKf8O/ENKWeLiaSoUkw4lVhQKOxBCRANXCSGus7p4Zs6cKW+44QZx9dVXM3XqVBfPcPKgxMqJFBUV8dprr/H3v/9dHjlyxKpktwP/AN6QUja4cHoKxaRBiRWFYpwIIfyBS4FrgbMALSoqSr/uuuu0a6+9lrlz557RFpSRUGJlZKSUZGdn8+qrr/LPf/5Tb2xs1IQQZinlR8CrwLtSym5Xz1OhcBVKrCgUY2AgDiUT+K4Q4hoppbe/v7+8/PLLxXXXXceqVaswGAyunuakRomVsdHf389nn33Gq6++yn/+8x/Z09MjhBCdUsq/Ac9JKfe5eo4KxUSjxIpCcRIGaqFcI4T4rpQyQwjBueeey7e+9S0uvPBCvL29Rx3jdETXdUwm0wmL2WwefK7rOlJKrOeYI0eOADB9+vRBy5MQAk3TcHNzO2YxGAy4u7tjMBhwc3ND0075NkY20dnZyTvvvMPLL7/M+vXrrau3A38G/q1quCjOFJRYUSiGQQiRAtwuhPimlNIvPDxc/853vqPddNNNJCYmunp6TkHXdfr6+ujt7R18tC5D/zYajej6xPb20zQNDw8PvLy8jlk8PT1P+Pt0dcEVFBTw/PPP8/LLL+utra2apmmtuq6/APxJSnnU1fNTKJyJEisKxQADXYwvAO7AUlmW1atXc+utt3LJJZecFqnGUkq6u7vp6uqis7NzcOnq6qK7e+SQiKFCwcPD4wRLyFCLiPW5pmkIIQaXnTt3ApCVlTVocZFSDmulGWqhsS5DhZTRaBx2nkIIfHx88PX1xc/PDz8/v8Hn3t7ep4WQ6e3t5d///jd/+MMf5M6dOwUgsdRteRb4TKVAK05HlFhRnPEIIbyAb2ia9iNd16d5e3vLG264Qdxxxx2kp6e7eno2IaWkq6uLtrY22trajhElx1tFPD09By/qx1surNYKR7hhHBmzYjabh7X+9PT00N3dTWdnJ319fcfso2naMeIlKCiIwMBAfHx8TlkRk5OTw7PPPss///lPaTQahRDikJTyV8A/pZT9rp6fQuEolFhRnLEMZPXcomnaPbquR8bHx+t33XWX9s1vfpOgoCBXT2/MWIVJa2srra2ttLW10draislkGtzGzc1t0NIw9ILt6+uLu7v7hMxzogNs+/v7jxFpQy1J1j4+AO7u7oPCJSgoiKCgoFNOwDQ0NPDiiy/yf//3f3p9fb2maVq5ruu/AV5ScS2K0wElVhRnHEKIMOD7mqbdqet6YEpKirzvvvvENddcM2EXbnvo7++nubmZxsZGWlpaaGtrO0aYeHl5DV50AwMDCQwMxMvLy+UX38mSDSSlpLe39wRxN9QS4+7uTmBgIMHBwYSFhRESEoKbm5tdx+0zmWnsNBIb5Lyg7N7eXl555RWeeOIJWVpaKjRNa9R1/Sngj1LKVqcdWKFwMkqsKM4YhBDxwD1CiJullN6ZmZny/vvvF1/96lcndbbJUHHS2NhIa2vr4Gve3t7HWASswmQyMlnEynBYBYxVuFgXq4ARQhAcHExoaKjN4uXT/Dpu+lsO86cE8ZXZ0VwwO5oYJwkXk8nEG2+8wS9/+Ut58OBBoWlah67rfwD+T0pZ55SDKhRORIkVxWmPECIJuF8Icb2U0m3t2rXcd999nHXWWS63NgyHyWSiqalpWHHi6+s7eMEMCws7pVKnJ7NYGYnu7m4aGhoG/x89PRaPyvHiJTQ0dNQ6O3e/sYe3d1cdsy5jShCXzI/lq3NjCfRxvFVP13U++OADfvnLX8odO3YIIUSflPIl4AkpZbnDD6hQOAklVhSnLQOl8B8SQtwkpTRccskl3HfffSxatMjVUzuBnp4e6urqqK2tpaGhYTAI1sfHZ1CYnGri5HhORbEyFGsmVWNj4wnixWAwEBERQWRkJFFRUXh6eh6zr9Gkk/nYp7T3moYbGg83jXPTorgyM56l00PRNMeKaCklmzZt4vHHH+fjjz9GCGGUUj4L/FJK2eTQgykUTkCJFcVphxAiEPiREOIuKaX3xRdfzGOPPcbs2bNdPbVBpJS0t7dTW1tLbW3toPXEYDAQGRlJZGQkYWFh+Pj4uHaiDuRUFyvHYxUvDQ0N1NXV0dDQMBi4GxwcTFRUFFFRUfj7+7OpqJFvvLxrTOPGBnlz+YI4vp4ZR1yw4///2dnZ3H///Xz22WcMuIeewOIe6nL4wRQKB6HEiuK0YSAF+XZN0x7QdT1o+fLl/OpXv2LZsmWunhpgMck3NjYOChTrXbm3t/fghW0s7oRTldNNrByP2WymoaFh8P9rjXfx8fHhSKcb/87voqQDJGOzmggBy6aH8fXMOM5Ni8LL3bHfi88++4wf//jHcvfu3ULTtHpd1x8BXlQpz4rJiBIrilMeIYQbcIOmaY/quh6TlpYmf/WrX4mvfOUrLo9JkVLS1tZGRUUFVVVVgxewoKCgQYESEBDg8nlOBKe7WBmKlJLW1tZB4dLe3g5ASx/kNgpyGjVqusf+Pw/wcuPSjDhuXJpAQpivw+ap6zpvvvkm999/vzxy5IjQNO2Irus/xVLKf2LLFCsUJ0GJFcUpy0BzwYuFEL+SUqZOmTJFf+yxx7RrrrnG5daJrq4uKisrqayspLOzE7AIlLi4OGJiYk7p2BNbOZPEylC2HWnk9r/uYF6oJDNcJ25Aa1R1QU6jRm6joM04dmvLWakRfHNZIkunhzpM5Pb39/PSSy/x8MMP6/X19ZoQIk9K+SMp5WcOOYBCYSdKrChOSYQQM4BngHNCQkL0hx56SLvllltOCGycSPr6+qiurqayspLm5mbAkr0TFxdHXFwcfn5+LpvbZOBMFSsP/fcAf9teNvh3tLdkQbhOZpgk2BN0CcXtgtxGwd4mQY95bAIkJdKfby1P4JL5sXi6OUacd3V18fvf/57HH39c7+jo0IC3gbtU5pDC1SixojilEEL4Aj8VQtyraZrhBz/4gXjooYcICAhwyXyklDQ2NlJaWkpNTQ1SSjw8PIiNjSUuLo7g4OAzwsUzFs5EsaLrksWPf059R98Jrwkk0/whM1xnXqjExw36dchrEmyt1SjttGw1GuH+nty4NIHrsqY6LP25qamJBx54gOeeew6gV0r5KPA7KeWJb0ShmACUWFGcEgy4fC7VNO3/dF2PW7NmDc8++yyzZs1yyXyMRiPl5eWUlpbS1dWFEILo6GimTJlCeHj4pC4y5yrORLGSW9bMZX/aPup2BiFJC5YsCrc8agKqu2FrrUZOo6B3DNYWHw8DVy6M56YV0xxWbC4nJ4fbbrtNZmdnC03TinVdv1NK+ZFDBlcoxoESK4pJz1CXT1RUlP7UU09pV155pUssFq2trRw9epTKykp0XcfHx4epU6cyZcqUSVs5drJwJoqVx97P58UtR8e1T5CHZHGEzpIISZAn9Jkhu0GwuVajtmf077yHQeOqRfHcviaJyAD7v5O6rvOXv/yFe++9V29padGA/2BxDZWNtq9C4SiUWFFMWo53+dx1113ioYcewt/ff0Lnoes61dXVHD16dDAWJSoqioSEBCIiIpSbZ4ycaWJFSsnyJ9ZT1WpbH0ENyaxgyYooSWqQ5Txd2CbYXCvY3yxGTYH2dNO4Nmsqt66eTri//bFczc3NPPDAA/z5z38Gi2voMeC3yjWkmAiUWFFMSoQQ52ma9oIrXT4mk4ny8nKKi4vp6enB3d2dqVOnkpCQgK+v49JHzxTONLGyr7KVi5/d6pCxIrwkK6J0FkVIvAxQ3wOfV2tkNwjM8uSixctd4xtLEvjuqumE+HrYPZfc3Fxuu+02uWvXLqtr6FtSys12D6xQnAQlVhSTioHqs08C3woPD9d///vfT7jLp7+/n5KSEkpKSjAajfj4+JCUlER8fLzdnXfPZM40sfLER4f504YjDh3T0yBZHC5ZG6MT5AmtRlhfrbGtTmDUT/4b8fUwcOOyBG5aMY0gH/tEi9U1dM899+htbW0C+D1wv5Sy266BFYoRUGJFMWkQQpyradrLuq7HXH311TzzzDOEhoZO2PF7e3s5cuQIpaWlmEwmAgICSE5OJiYmRgXMOoAzSaxIKVn7u40cbXROBXuDkGSGSc6K1Yn0hq5+2FQr2FSr0W06uWjx93TjW8sTuWnlNPw87RPfNTU1fPe73+W9995joKDcjVLKLXYNqlAMgzoDOxkhxOVCiGeEEJuFEO1CCCmEeHWEbeOFEH8UQuwUQtQKIfqEENUD+35TCHFCXqIQ4saBMUdabhnhWHcKIY4IIVqFEP8RQkxx9HsfK0KIACHEC8BHISEhUW+//TavvfbahAmVrq4u9u7dy6effkpxcTEBAQFkZWWxevVq4uLilFBRjJuCug6nCRUAsxTsbNB4fI+Blws0mvvg/HjJIxlmLplqJtBj5JvQjj4TT39exJrfbuBf2eWYddtvWKOjo/nvf//LX//6V/z8/BKBTUKIp4QQk76plRDi+iHnye8c91rCKOfV10cY8ywhRI4QokMIsUMIsWJi3s3pj7KsOBkhxB5gLtAJVAKpwD+klNcNs+1q4L/ATqAEaAZCgfOBeGADsE5KaRqyz43AXwb22zPMFN6XUuYcd5ybgT9jKfhUClwO9ADzpZS9Nr1RGxFCnDNgTYm96qqreOaZZwgLC5uQY/f09HD48GEqKiqQUhIREcGMGTMm1JpzJnEmWVae+rSQpz8vmsAjSlICJetidZIDwaTD1jrBJ5UanaNYWmZFB/DAhTNZOt2+3111dTU333wzH3zwAQOxLDdKKR0TtONghBDxwH7AAPgBN0kpXxzyegJwFNgLvDPMEAeklG8eN2Y6kAvsBjYDK4B5QIaU8pDD38QZhnLAO5+7sIiUYmAVsP4k224Dgo/vyTFgUfkEWA1cCrwxzL7vSClfGeOcbgSeklLeMzD+r4FyYDkwIeW1hRABwG+Bm0JCQvTnnnuOSy+9dCIOjdFopKioiJKSEnRdJzo6mhkzZhAUFDQhx1ec/vzvQM0EH1FQ0CYoaNNI8JOcF6+zKlqSFWFmfbVgfbVG3wgxLfk17Vzzwk7+8s2FrEmJsHkGMTExvPfee/z973/nzjvvnNbe3r5ZCPF/wAOTKZZloGbTX4AmLDdsPzzJ5nuklI+McehrgH3AMimlLoTQsNx4Xgs8YPuMFaDcQE5HSrleSlkkx2DCklIah2seNtAF9Z2BP5MdMC0DYB7ytxmQA+udjhBiiaZpB4CbrrzySg4dOqRNhFAxmUwUFhYOuntCQkJYsWIFixYtUkJF4TCONHRSWNfpsuOXdgr+fMjAMwc1arst7qEHM8ysjNIxiOFPQ6lR/qxMDrf72EIIbrjhBvLz87WvfOUrArhL07Q8IcQ8uwd3HN8D1gLfBBzpq7OeV60fshz4+/Rsoz7BKMvKKYAQwgBcMPDnvhE2myeE+AHgBVQB66WUlSNs+w/gKSHEVCwWla8B1YBTA+MG7jTuBX7h7+8vXnzxRS6//HJnHhKwZC6Ul5dTUFBAb28vgYGBzJw5U9VIUTiFjw7UunoKABS3azx1QDAnRHLhFJ3LEnVWR8OHFZbmiUPrtDx8URoGzXG/hdjYWN577z1eeeUV7rjjjuSenp6dA+enP4/lxs1ZCCFmAr8CnpZSbhJCrB1llxghxHexuOObgO1SypHOwW8Ad2OJ29kGLAHmAzc7ZvZnNkqsTEKEEGHAHVgag4QD64Ak4DXg/RF2+/5xf5uFEC8CPxgmDuUZLKLmVuA8LP7V70spnRYRKISIAP4GnLtkyRL5+uuviylTnBvTK6WkpqaG/Px8urq68PHxYcGCBcTGxiqRonAaE+8COhmCfc2CA82CRRGS8+N0rk/WWRsD75dr5LdqnJ8exZLpjo/TEkLwzW9+k2XLlomvf/3r7vv27fsjsFYIcZOUstXhBxx9Pm7A37HcoN0/xt3WDSxDx9kAfOP45o5SylwhxCXAL4DbgcPABScRN4pxoMTK5CQMeHjI3xJLfMf9w9yVHAXuxBLTUgkEYok9eRz4LhCAxZf65WCWMX49sDgdIcRqTdNe13U98ic/+Qk///nPhbu7YxqujUR7ezv79++nsbERT09PZs+eTUJCgsrsUTiViuZuDlS1u3oaJ6Aj2FFv6ey8IkpydqzOd2fq5LdKvr5mqlOPPWPGDHbu3Cnuuece/vjHP16uaVqmEOIKKWW2Uw98Ig9hsXQsl1KOVla4G3gUi/u9ZGDdHOARYA3wuRBi3vE3eFLKD4APHDhnxQDqzD0JkVIellIKLGJyKpYg3ZuxmBdDjtt2o5TyWSlloZSyW0pZI6X8N5YfVAtwtRBi7kS/B7C4r4QQDwOfBwcHh3/00Uc8/vjjOFOo9Pf3c/DgQTZs2EBTUxPJycmcffbZTJs2TQkVhdOZLC6gkejXBV9Uazy628DGGsHMIMmh3ds5fPgwZrN59AFsxMvLiz/84Q/8+9//xtfXd6oQYpsQ4m4xQSZOIcQiLNaU30kpR+0sKaWsl1I+JKXcLaVsHVg2AedgCZpNAr5z8lEUjkRZViYxUkozFpPl00KIOuCfwM+xuIhG27dCCPEhlkj0lVhS8CYMIUQMltiY1WvWrOHVV18lJibGaceTUlJVVcXBgwfp7e0lPDyc2bNnT3gfodMdKSV9fX309vZiNBoxmUwjLmazGV3XB/fr6rLchO7YsWPQDadpGm5ubri5uWEwGAafD108PT3x8vLCw8Nj0rvvPpxULqCR6TELtjb78KPL5lN0OJ+CggIqKiqYPXs2UVFRTjvu5ZdfTkZGhrjyyisNOTk5vwPWCCFulFI2OeuYQ9w/hcCD9owlpTQNuNezsJxXn7Z/hoqxoMTKqcP/Bh5Xj2OfhoHHCW1kI4RYp2na60DIww8/zE9/+lMMBucFxA91+Xh7e7Nw4UKio6Mn/YVtsmEVIp2dnXR1ddHT00Nvb+8xi9FoZCzxkUIIDAYDmqYN/h+sd+6tra2Dx9N1HZPJNNIwJ4xpFS7WxdPTE29vb/z8/PDz83OpoKlp6yGvvNUlx7aFn5yfSlR4KJFhy6moqCA/P5+dO3cSGRnJ7Nmzndb/atq0aWzdulXcd999PPnkkxdqmrZPCHH5WCweNuIHzBh43jvC9+OFgcKUT0spfzDKeC45r57pKLFy6hA78Di2M7uFrIHHkpNu5SAGTLrfB34XHh7O66+/7tQCYCaTiYKCAo4csfRfSU5OZsaMGap/zyiYzWba29vp6uqis7NzcOnq6hpWOHh6euLp6UlgYOAxIsHT03NYS4ibm9sxIsXKSEXhhoqW4RarJae3t3fweXNzM0aj8YS5uru74+vrOyherM/9/f2dKpgBPp7kLqChzIsP4qtzLacUIQRTpkwhOjqaw4cPU1JSQkNDA8nJySQnJzvlc/Pw8OB3v/sda9as4frrr49qa2vbKIT4rpTyLw4/GPQBL43wWgaWOJYtQAEwFsG0eOBxQs6rCgvqrD6JEEJkAfuPL6AkhPDjS3PjB8e9tuL4jqcDouEnWFLnGoGPnDbpL4/pCfwJ+ObChQvlO++8I5zp9mloaCAvL4+enh7l8jkJJpOJ9vZ2WltbaWtro7W1lY6OjmOsI5qm4efnR3h4+OBF3s/PD29vbzw9PZ0e62O1whgMBjw9Pce8n67r9PX10d3dfYzg6uzspLq6etAFZT1GQEAAQUFBBAYGEhQUREBAgEMvxP87hcTKwxfNQjsuVdnd3Z3Zs2czZcoU9u3bR0FBAVVVVcyfP5+QkJARRrKPCy+8kNzcXO3iiy8WBw8efFkIMRv40dAq3fYyEEw7bHyJEOIRLGLlr8dVsM0C8qSUxuO2X4slhhBg2LYpCuegxIqTGUhlu2TgT6szeIkQ4pWB541SSmsFxfuA1UKIjVhiVbqxlNk/HwjCUuH28eMOsUkIUQhkY6mvEggsA9IH9r9WSunU9AQhRJQQ4m0p5ZLrr7+e559/Xnh5eTnlWP39/eTn51NaWoqHhweZmZnExMQolw9fxoU0NjbS3NxMa2srnZ2dg8JECIG/vz/x8fEEBgYeI0pOxc9P0zS8vb3x9vY+oUWClJLu7m66urro6OgYFGtlZWWD21gFTGBgICEhIYSFheHj42PTZ9HQ0Ud2abPd72kiuDQjlvlTgkd8PTAwkOXLl1NeXs6BAwfYvHkzSUlJpKamOsXKMm3aNLZv3y6uu+463n333buANCHEVVLKFocfbOw8MTCPDViyLMGSDWSty/KglHKbKyZ2pqLEivOZB3zjuHXTBhaAMr4s9/wCloqKC7HEpvhgyejJxVJw6OVh7jh+CyzC8iMKAXQsQucPwJNSSqeaKoUQGZqmvSeljPnNb37DPffc47QLX0NDA3v27KG7u5vo6Gjmzp07rjvx042h4sS69PX1AV9eiOPj4wetCYGBgU53hUwWhBD4+vri6+tLRMSXJeRNJhNtbW2DVqbW1lbKy8spL7eUzPD29iYsLIzQ0NBxiZdP8muxox/ghOHjYeDH56WOup0QgqlTpxIREcGePXsoLi6mtrbWaVYWf39//vOf//DQQw/xi1/84hxN03YJIS6SUh52+MHGxt+xFMtciOVm0R2ow3IefvZ4a7bC+ahGhgqbEUJcKYR4xc/Pz+P111/XLrjggtF3sgGTycTBgwcHrSlz5sw5Y60pvb291NXVDYqT3l5LvT9N0wgODiYsLIywsDCCg4MnnTCZrI0MTSYTzc3NNDY20tTUREtLy6A1yipewsPDiYyMxMPDY9gxrn9pJ5uLGidy2jZx77kp3L4maVz7SCkHrSwmk8mpVhaAf/3rX9x4443SaDR26rp+lZTyQ6ccSHFKocSKYtwMlM1/FLg/KSlJf++997TU1NHv1myhsbGRvLy8QWvKnDlzcJaLaTIipaSjo4Pa2lpqa2tpabFYxk8FcXI8k1WsHM/JxEtoaChRUVFERUXh5+cHQGu3kczHPsM0yU0r8SHefHrXKrzcbfuedHd3s2fPHhoaGvDz8yMjI4Pg4JHdSfaQm5vLxRdfrFdXVwvgx8BvXVmmX+F6lFhRjAshhDeWsv+XrFu3jn/9619OOWGZzWby8/MpKSnB3d2dOXPmnDFl8nVdp6mpaVCgdHdb4q29vLwGL5RhYWGTXpwcz6kiVo7HZDLR0NAw+P+wZiH5+fkRFRXFoVaNH713BJ3J/d3883UZnJcebdcYUkrKyso4ePAgJpOJGTNmkJKS4pQg7NraWr72ta/JHTt2COCvwE0DTV0VZyBKrCjGjBAiGHgXWH7nnXfy5JNPOiVNuLOzk5ycHNra2oiKimLu3LmnvTVFSklzczOVlZVUVVXR3285JwcGBg4KlMDAwFNarJ2qYmUoUkpaWloGhUtHRwcAHUbY3STIadQo7wQmmXBZMi2U127Kctj3p7u7m7y8PBobGwkJCSEzMxNvb2+HjD2Uvr4+br75Zv72t7+BpdbU153Zw0wxeVFiRTEmhBCxQohPpJSznnjiCe69916nXDgrKyvZs2cPUkrS0tJITEw8pS/Qo9He3k5lZSWVlZX09FjalYSFhRETE0NUVJRTLgCu4nQQK8dT39zGT17dzJwQM4kDmfP1PZDbqJHTKGjsdf13VxPwwfdWMDM6wKHjSikpKiri8OHDuLm5kZGR4ZTqt1JK7r//fn71q18hhMiWUl4gpZz8AUIKh6KygRSjIoRI1TTtUyFE3EsvvcQ3vnF8cpP9mEwm9u/fT3l5Ob6+vmRmZhIUFOTw40wGenp6qKqqorKykra2NgACAgJITEwkLi7utBIopzvbyzv5olrwRbUboZ6SBWGSzHCd8+N1zo+H0g7IadTY3SjoMrlGuFyTNcXhQgUsGUMzZswgNDSUnJwcdu7cyfTp05k1a5ZD3UJCCB5//HEiIyO56667FmqatlUIcY6Usmz0vRWnC8qyojgpQogsTdM+8vDwCHzzzTfFV77yFYcfo729nZycHDo6OoiNjWXu3LlObXboCqSU1NfXU1paSl1dHVJKvLy8iIuLIy4ujsDAQFdP0emcjpaVW/6ey0cHjy8GJ4n3hcxwnQWhEn8PMOmwr1mwtU5Q3C6YKDdRgJcbG+5dQ4jv8FlMjsJoNLJ7927q6uoICgoiMzPTKeX6//nPf3LDDTdIXdfrdF1fJ6U84PCDKCYlSqwoRkQIcb4Q4q3AwEDPDz/8UFuyZIlDx7emRO7fvx9gsHrm6eT26evro7y8nNLSUrq7u9E0jZiYGKZMmUJYWNikfa9mXVLX3ktNWw9Vrb3UtPZw+YI4Qv1sr2tzuomVbqOJjEc/pbdfH3EbDcmMIMniCMmcYIlBg7oe2FqrsatB0GN27v//oQtn8a3liU49hhUpJSUlJRw8eBCDwcD8+fOd0rz0008/5ZJLLpG9vb0duq5fqGqenBkoN5BiWIQQNwAvx8TEiE8//VSbOXOmQ8c3m83s3buXiooK/P39yczMJCDA8abqkegxmvnzxiNcuTCemCDHu11aW1spKSmhqqoKXdfx9fUlLS2N+Pj4SVXIrsdopqSxk+L6To40dHGk3vL8aFMXRtOxF+GMqcF2iZXTjY0FDScVKgA6gsOtgsOtEOAuyYqQLInQuTRR5ytTILtBsLlWo7bH8aJlergv1y+Z6vBxR0IIwfTp0wkJCSEnJ4fs7GwSExNJT093qFto3bp1bNy4UZx33nl+zc3NnwohrpRS/tdhB1BMSpRYUZyAEOJu4HczZ86Un3zyiYiLi3Po+D09PezatYvW1lbi4+OZM2fOhDUflFLyvwO1/OKDQ1S19nC0sYvfXz3fYWPX19dTVFREU5Ol431UVBSJiYmEh4e73IpS397Lvso29lW1caCqjcK6DqpaexircbW2rde5EzzFGG8voPZ+wadVgs+qBGnBkhVRkuVRkuVRZg63Wl5zpIvooYvScDc4t6/TcAQHB7N69Wry8vI4evQoHR0dLFy4cMSCeraQmZnJ9u3btXPOOce9tLT0bSHETVLKlx12AMWkQ4kVxTEIIe4HfrF48WL5wQcfCEeX1m5paWHXrl309vaSnp7OtGnTJuwiXlzfwUP/Pci2I02D697dW831S6ayMMH296nrOtXV1RQVFdHe3o7BYGD69OkkJiY6xW8/Fnr7zRyoamN3eQt55a3klbdS226f2Kizc//Tid5+M58fqrNpX4ngQIvgQAtEeElWRutkhUvuTJOUdsCnVRoHWwTSDtFyVmoEq2aE27y/vbi7u7Nw4UIKCgooKChg06ZNLFq0yKHW0+TkZLZv366de+65ct++fS8JITyklH922AEUkwolVhSDCCEeAn62cuVKPvjgA2Gt0OkoKioq2LNnDwaDgSVLlhzTs8WZ9Jt1/rzhCM98UYzRfKLZ/ufv5fPf25ed0IV2NMxmMxUVFRQXF9PV1YWHhwepqakkJiY69C5yTHPRJfur2tha3MjmogZ2l7UO+17tQVlWvmRLUSNdRrPd49T3Ct48auCjCotoWREluSlVp6YbPqvS2N0k0OX4vpfuBsFPv+JYt60tCCFITU0lICCA3bt3s3nzZhYsWODQ9OaoqCjWr18vzj77bJmXl/cnIYSblPJZhx1AMWlQYkWBsJg2fgY8uGbNGt577z2HWgSklOTn51NcXIyfnx9ZWVk4WgiNxP7KNu59cy+HaztG3qaqjTd3V3JFZvyYxjSZTJSWllJcXExfXx/e3t6kp6czderUCXVnlTV1s6W4kS1FjWw70kh77/E9Lh2LvZaZ04nxuoBGo9Mk+LDCwOfVkuWRktXROtcn61wQD19Ua+yoF5jGKFpuXJrAtPCJ+X2NhZiYGHx9fdm5cyc7d+5k5syZJCcnO8yiGhISwueffy7OOeccmZOT88yAYPk/hwyumDSobKAznAGh8ijw07Vr1/Lee+/h4+PjsPH7+/vJzc2lrq6OiIgIMjMzJyQtubffzFOfFfLCppIxdcMN8/Nk/Q9X4e818tx0XaesrIyCggL6+vrw8/MjOTmZuLg4p5QbPx6TWWdXaTOfHKzjs0N1VLb0OP2YQ1mYEMy/b1lq8/6nSzZQv1kn87HPaOtxXuV3N2EJxl0boxPmBS198GGFRnbDyd1Dob4erL93NQEn+R67ir6+Pnbt2kVzczOxsbHMnz/foS0j2traWLdunczOzhbAPVLKJx02uMLlKMuK4mHgp/PmzeOee+5x6EW3q6uLHTt20NnZSVJSErNmzZqQ+JSdJU385O39HG0ce1Xuxs4+nl1fzH3nn2g+l1JSVVXF4cOH6erqwtfXl9mzZ09o5+dnPi/ipa1Hae12XWsUZVmxsP1Ik1OFCoBJWmqybK8TZIZLzo/XuTZJZ20MvF+ucaBl+EDce89NmZRCBcDT05Nly5axb98+ysrK6OzsZPHixQ5rpeHm5sZ9990n7r//fg4fPvw7IYRJSvl7hwyucDkTHyqumDQIIR4AHj7rrLN444036O/vZ+vWrfT22n9RamtrY/PmzXR3dzN//nzS0tKcfmHv6O3ngXf2c+XzO8YlVKy8vOXoMftZs3s2btxIbm4uJpOJuXPnsnbt2glvqigELhUqAHXtfShLrONdQCdDR7CrQeMXeQb+U6rh7w43pep8P83MNP9j/xdpMQF8fYyuTFehaRpz585l9uzZg+eIzs5Ou8ft6upi69atuLm58e6777Jw4UIJPC2EuM3+WSsmA0qsnKEIIX4CPLpmzRreffddkpOTWbBgAZ2dnXYLlsbGRrZs2YLZbGbx4sVMmTLFcRMfgZzSZs5/ejOv7ii3eYx+s+QXHxwCLFlL27ZtY/v27XR1dTFz5kzOPvtsEhISJsTlczz2dst1BEaTTouLBZOrMeuST06oWOt8TFKwoUbj0TwDH1cKYn3h++lmbk41E+1tES0PX5SGYZxB4q5ACMG0adNYtGgRvb29bN68mZaWFpvHswqVvr4+Fi1aRHJyMp988olYsGCBBP4ghLjZcbNXuAolVs5AhBB3Ao+vXLnymBiVuLg4uwVLdXU127dvx2AwsGzZMsLDnZs+aTLrPPVpIVc8t90hMRzbi2r5aON2Nm3aRHNzM0lJSaxbt44ZM2ZMWPDscCRF+DEj0vVBk2d6RtCuo800dRlddvxesyUQ99E8A5trBamBknvnmrk704v5cf4um5ctREdHs3TpUqSUbN26lfr6+nGPcbxQiYyMBCAoKIhPP/1UzJs3TwLPCSGuc/D0FROMEitnGEKIq4CnFy5cKD/44IMTsn7sESxHjx4lOzsbb29vVqxY4fRGhBXN3Vzx3Hae/rxoTEG0J0MgWRap89P5Zvpa64mOjuass84iLS1twtOQR2IyWFfO9ForHx2ocfUUAOjot6Q8P77XQGGbYKp7J59//jnl5eWnlKsuNDSU5cuX4+7uzo4dO6isrBzzviMJFSvBwcF89tlnYubMmVII8YoQ4jxHz18xcSixcgYhhDhbCPH3GTNmyA8//HDEOirjFSxSSg4fPsy+ffsIDAxk+fLlTi+G9uH+Gi74/WZ2l7faPdZUP8k9s81cMU2nwwh/yNc4ZIpwaFaUIzg/3XH1KWzlTBYrui6HaVroWhp6BR7RKSxatAiDwUBeXh5btmwZ7OZ9KhAQEMCKFSvw9fUlNzeXI0eOjLrPaELFSmhoKB9//LGIjo4WQoi3hRBZjp6/YmJQYuUMQQixQAjxTkREhPbJJ59oYWFhJ91+rIJFSsm+ffsoKCggLCyMZcuWOSy6fzh6+8088M5+bvvHbjrsrCvi6ya5apqZu2ebifCG/5Zp/HqfgcI2jSc/LaTFheb+4UiN8ich1LUC6kzOCMqraKWuvc/V0ziG6EAvblk9nejoaNauXUtKSgqtra1s2LCB/fv3099/asQY+fj4sHz5coKDgzlw4AAHDx4c0UI0VqFiJT4+nk8//VQLCAjw1DTtf0KIFGe8B4VzUWLlDEAIkaRp2kd+fn7en3zyiTZ16tiam40mWHRdJycnh9LSUmJjY1m8eLFTa6gcbezikj9stSuI1oIkM8zi8lkSKdndKPjFHgNfVGuYBwpvtfX089RnhfZP2oEIIVzuCjqTLSuTxQU0lJ+cn4qPhyWWymAwkJqaytq1a4mMjKSkpITPP/+c6upqF89ybHh6erJ06VIiIiIoLi5m7969JwiW8QoVK7NmzeLDDz/UPDw8gjRN+1QI4fh20AqnosTKaY4QIkrTtE/c3NxC33//fW3OnDnj2n8kwWIVKtXV1SQkJLBgwQKHFng6ng0F9Xz12S0nrUQ7FgLcJd9JsVQH7TbBHw5q/LXIQJvxxCyKf+wsp8DO4zkaV7uCztQAWyklH+6fXC6gBVODuXjuiddcX19fFi9eTFZWFkIIsrOzyc3NxWicXJbC4XBzcyMrK4vY2FjKysrIy8sbFCy2ChUrS5cu5Y033hBCiHghxMdCiCAnvAWFk1AVbE9jhBABQohNQoi5b731FpdcconNY1VWVpKbm4ufnx9Llixh//791NbWMm3aNNLT051Wc0RKyXObSvj1R4ftDKKVZIZJLk3U8TbAplrB++Ua/frJ570sKZRXv53l8o7JVqSULH9iPVWtE1u91kpatD9v3ryQ3t7eY5a+vr7B52azGV3XkVIOLrquD7ok3N3d0TQNIQRCiMHnbm5ueHl54enpiZeX1+Bi/dvT09MlaeNgadtw0bNbXHLskXj3jmXMiQs66Tb9/f0cOHCA8vJyPD09mTt3LtHRrg/UHg0pJbt376ayspK4uDhSUlLYtm2bzUJlKH/5y1/41re+BbAZOFdK6Zofk2JcKLFymiKE8AT+B6x57rnnuPlm+0sNWAWLwWDAbDYzffp0pxZ76zGa+fFb+3h3r31m7AB3yRXTdGaHSBp74bViA0c6xj7n569fwDlprg9utfLo+/m8tOWoU8Y2CEm0D8T7SkI8JYEeEOBh+QwDPMD/JF4+q6hwc3M7QYgIIWhsbAQgLCzsGBFjfd7f309vb+9JLQBDhYtVzPj4+BAUFIS/v7/TxMyvPzrMHzeMHvg5UXx9QRy/+frcMW9fV1fHnj176O3tJS4ujtmzZ0+aLLeRkFKyZ88eysvLMRgM6LpOVlaWXULFyq9+9Svuu+8+gHeAy6WU9nelVDgVVW7/NGSg389LwJqf/exnDhEqYKmLEBgYSFtbG+7u7kyfPt1pQqWqtYeb/5bDwep2O0aRLAiTXJao4+sGG2ss1hTjKNaU43nsg0OsSgnH0815bq7xcH56lEPEipuQxPhAvJ8kzlcS72sRKm5DrvdmCR390G6E0g5Bez/csDwJPx+fcVs9rL2BFi1adNLtdF0/xlIznPWmra3thLocmqYREBBAUFAQQUFBBAYGEhAQYLeAkVLy0QRWrR0NXw8D9543vhjRyMhI1q5dO2hlaWhomPRWFiEEycnJVFVVYTabCQsLc1in9h//+MfU1NTw+9///hLgKeB7DhlY4TSUZeU0RAjxI+CJb37zm7z00ksOERS6rpOdnU1tbS2RkZHU1dXh5+fnlOyfnSVN3PaP3XYV3/LULNaUzPABa8oRA0fabf8cfnxeKreunm7z/o5E1yWLH/+c+o6xZ6a4CUmMr8ViEu8rifeTRHuDYeA6rkuo64GKLkFlp6CiS9DQC539nNA4b/OP1hAfMv6sJEc3MpRS0tfXR2dnJ62trYNLV9eXLROGCpjAwECCgoLGLWAKajs49/82OWTOjsDe7+JQK0tCQgLp6elOjTezFWuMSm9vL2FhYTQ0NBAfH8/8+fMddk677LLLeOeddwBuklK+aPegCqehxMpphhDiK8B7S5YsYf369cLT09PuMa3BtDU1NYOun6qqqsEYFkcKlld3lPHIuwcx2RGgEusjuXGGJR15R73graPjt6Ycj6+HgfX3ribC33lp2ePhwXcO8PcdZSO+bhCS5ABJWohkmv8IwmRAlFR0Caq6GPNn9O9blrAwIWTcc56orsv9/f20tbXR2to6+Di0/4wQgoCAAEJDQ4mKiiI0NPSk4uWpTwt5+vMip855rEwN9eGTu1babeXr7+9nz549VFdXExAQQGZmJv7+k6cC7vHBtBEREeTl5VFRUcGUKVOYN2+eQwRLZ2cnS5YskQcPHjRLKddIKSdXYJJiECVWTiOEEDM1TdsVFRXls3v3bs0Rvl0pJbm5uVRVVZGYmMjs2bMHTxJDg27tFSy6Lnni48M8t7HEntmyPFJySYKOWcIbJRq5jY6LYRhvnIAz2XakkWte2HnMOl83yawgSXqIJDVI4jVwPavphnKrMOkUVHePXZgMxzNXz+eiYbJQRmOixMpw9Pf3097efowFxipg3N3diYiIICoqisjIyBPS7899ahMFdZMjK8yR8VNSSkpLSzlw4ABCCObOnUt8vOsbIY6U9TM06DYhIYE5c+Y4RLAcPXqUBQsW6G1tbS26rmdIKe2tjaBwAipm5TRBCBGiadp7Hh4evu+9955wlFDZv38/VVVVTJ069RihApa0ZoDc3Fy2bt1qs2DpM5n50Zv7+O8e2wNpvQ2Sq6frzA2VVHbBK4UGGnodG0/z79xKrls8lbnxQQ4d1xYWJYQQ4uuBm7mP9BBJerBOoj9oAow6FLYKDrQIDrYI2vsd+zmcirVW3N3dCQ0NJTQ0dHBdd3c3tbW11NbWUl1dTVVVFUKIQYtLVFQUdd1y0giV5UlhrJtl/+/aihCCxMREQkJCyM7OZvfu3TQ0NDBnzhyX9cE6WXqyEIKMjAx0Xae0tBQvLy9SUuyv75aYmMjbb7+tnX322SFCiHeFEMuklONv265wKsqychoghHDDkvlz9r/+9S+uuOIKh4xbWFjIoUOHiI6OZuHChSPexdhjYek2mvj2KzlsL2myeZ5T/STfSDYT6mVJSf5vqYZJOifwN2NKEG/dutRlqcy6rtPc3ExtbS0HisvwFpYqvh1GONBiESiFbcJut9fJuGlFIj/9yqxx72ePZeX1XeVUtHQTE+RNTKA3U0J9mBrig5vBMZaz/v5+GhoaqKmpoa6ubjDNul/zZEO5kQMtGmWdJ8bvTBQGTfDh91aQEuUcV43JZGLv3r1UVlbi5+fHwoULCQgIcMqxRmKsdVTMZjM7duygsbGRuXPnkpCQ4JDj//nPf+bWW28FeBO4QqqL46RCWVZOD34LnP3ggw86TKiUlZVx6NAhQkNDWbBgwUkvzvZYWLzcDEQH2u4+Whyh8/VEHaMOLxVo7Gt2bh2O3eWtvLu3mq/Oi3XqcYYipaShoYGKiopjLqS+Xt58dMTMgRaN8gm8kNa6oOT8+/tq2FLceMw6d4NgaqgvSeF+TI/wJSnCj+nhfiRF+A1WdR0r7u7uxMTEEBMTg67rtLS0UFtbS86hEtbFSdbFmQcF4a4GjZIOYAKFy7VZU5wmVMBSjG3BggWEh4ezb98+Nm3axPz584mNnZjv+XgKvhkMBhYtWsTWrVvZu3cvHh4exMTYX5D2lltuYf/+/fzxj3+8HHgAeNTuQRUOQ1lWTnGEEN8GXrzkkkt46623HFJnoqamhl27dhEQEDDYEXUs2Gph6TfrfPuvOWwqbBjzHDUssSmroiXVXfBigYGmvom5eEQHevH5PavGfUEcL729vZSXl1NWVkZ3d/cJLgp3T28WPPap3T2SxsuihBDeuGXJuPezx7Jy9pMbKa7vHH1DLK6w5Ah/ZscFMjcukHnxwaRG++M+TitMRXM3K369nnAvSXqwJD1EZ9qAq622G3bUa+xqEHSZnPu9C/R2Z8MPVxPsOzF1Udrb29m1axddXV2kpKSQkpLiVEuirZVpe3t72bx5M729vSxZsoTR+p2Nhf7+fs455xzrd/VSKeV/7B5U4RCUWDmFEUJkCSG2pKWlGbZv3z5iF+Xx0NTUxLZt2/D09GTFihV4e3uPa39bBUtXn4lrXtjB3srRu8X6uEluTNZJCZLsbRK8Wmx/ts94+d5Zydy9bobDx5VSUldXR1lZGXV1dUgp8fX1ZerUqUyZMoXjs7vu/tce3s6rcvg8TsaUEB82/WjNuPezR6zMfuRju0SZl7vGnLggFiYEszwpnIypQaNm1LywqYRffHjomHWBHpKscMniCJ1QLzDpsK9ZsL1OUNQunGLd+tnFaXxjaYLDxz0ZRqOR7OxsGhsbiY6OJiMjwylxLPaW0O/s7GTLli2YzWaWL19OYGCg3XNqbGxk4cKFellZWa+UcqGUMt/uQRV2o8TKKYoQIljTtL1+fn5xeXl5Ytq0aXaP2d7ezpYtWxBCsGLFCmwVP7YKlqbOPi770zZKm7pH3CbSW3JTqplwL/ioQvBRpeaSOAJPN43P71lFXLBjuiAbjUbKyso4evQoPT09aJpGTEwMU6dOJTQ0dMQ720/z67jpbzkOmcNY8XDTKHj0vHHfbdsqVrqNJmY99PG49hkNb3cDixJDWJEcxrKkMFKj/E94P5f+cSu7y1uH3V8gSQmULImUzA6WGDSo74FNtRq76gV9DhLPMyL9+PB7KxwWmzMedF3n4MGDlJSUEBAQQFZWFj4+juv6ba9QsdLa2sqWLVtwc3NjxYoV+Pr62j23/fv3s3DhQmk0GvOllIuklCOflBQTghIrpyADFWrfBC596623uPTSS+0es6enh02bNtHf38+yZcsIDg62azxbBUt5UzeX/mkbjZ0nxkWkBenckKyjCfhHscYeJ8enjMZX5kTzh2sy7Bqjvb2do0ePUlFRgdlsxtfXl8TEROLj48dUDr2338yCRz+lyzix1cLzHlw3breErWKlpKGTtb/bOK59xkuYnycrZ4RxbloUK5PDaevpZ/Hjn49pX393yaJwybJIi7WlxwQ76wWbajW7XZOvfjuL5cn2uzfsoaysjL179+Lu7s7ChQsd4m5xlFCx0tDQwPbt2/Hx8WHFihUnWCBt4cUXX+Smm24CeEFK6Zgy4AqbUWLlFEQIcTvw7B133MEzzzxj93gmk4ktW7bQ3t7O4sWLHVbS2lbBcqCqjaue30Fn35dm/9XROl+dqtNmhBcOG6jqnhyNBf9182KypoWOvuEQpJTU19dz5MgRGhoscToRERFMmzaNiIiIcVss7nhtN+/vqxnXPvbyv++vYGb0+LJFbBUrw9WUcSZe7hrrZkby3jg/Uw1LjZtV0TpJAZbiewdbBBtrLC6i8QbkrpsVyQs3ZI5rH2fR1NTErl276O/vZ+7cuUydOtXmsRwtVKxUVVWRk5NDWFgYS5YscUibhWuvvZZ//vOfANdIKf/pkIkqbMK1t6aKcSOEmC+EeGrevHnyN7/5jd3jWZuFtbW1kZ6e7jChApYsoQULFtDZ2TlYNnsspMcG8vz1C3A3CEBy8RQzX0vQKeuE3+2fPEIF4Ofv52MeR7XdhoYGNm3axI4dO2hubiYhIYG1a9eyZMkSIiMjbQpkPD994vu71E5grZWJruvS269T3DC2YN6h6Aj2NWs8c9CNX+81kN0gmBkkuSNN5840Mwl+Y/+euBsEP71g5rjn4CxCQ0NZtWoV/v7+7Nmzh8LCQmy50XWWUAGIjY0lNTWVxsZG9u/fb/d4Qgj+/Oc/M336dF0I8YIQItkB01TYiBIrpxBCCH9N0/7t4+Pj9sYbbwhHlLgvKioaLPqWmJjogFkei62CZWlSGL/7+hyuma5zVqzkYIvgD/kGOhxc4MxeDla38++cilG3a21tZdu2bWzbto3Ozk5SU1M555xzmDt3rt1lzlenhOPpNrE/5bq2iRMQtW0Tmyod6O3O4Rr7CsFVdQteO2Lg4VwDn1YKpvjCXbPNfDvFTKT36Bf5by1PJCHM/tgLR+Lj48Py5csJDQ3l0KFDHDhwYFyCxZlCxcqMGTOIiYmhtLSUo0ftb/YZEBDAG2+8obm5ufkIId4Y6GavcAFKrJwiDMSpPKfr+vQXXnhBJCfbL/JramoGa6k4qnT1cNgiWEwmExG9lWRFSHbWC14s0Oif4IyfsfKbjwto7+0f9rXOzk6ys7PZuHEjTU1NTJ8+nbPPPpuUlJQxxaSMBV9PN1anhDtkrLFyOltWkiP8cJRzvNMkeL/CwKN5BrbWCtKCJT+Za+aa6WaCPYY/SpifJ3esSXLQDByLu7s7S5YsISoqipKSEnJzc9F1fdT9JkKogMUaMn/+fAICAti/fz+NjY2j7zQKGRkZPPnkk0JKOQ+w35ytsAklVk4dvgVc/Z3vfIerr77a7sHa29vJzc3F29ubhQsXOqQ+y8kYj2AxGo1s27aNuro6kpKSCIhNRndSRVpH0NRl5JnjGt319vayd+9evvjiC6qrq4mPj+ess84iPT3dIcF/xzPRrqCJFBC1E2jFAejpd3ywcnu/4I2jBh7fY2BvkyArQvLT+WYumWrGx+1Y0fKj81Lw9xpbbSNXYC3KNnXqVKqqqtixY8dgocLhmCihYsXNzY2srCzc3d3Jzs4+pgu3rdx+++187WtfA7hTCPE1uwdUjBslVk4BhBBpQohnZ82aJZ9++mm7xzMajezcaQlYzMrKcsrFczjGIli6u7vZvHkzLS0tpKenk5aWxo/PT+XSjImrGGsLf9laSklDJ/39/eTn5/PZZ59RWlpKZGQka9asISMjw6Fpn8ezdmbEQIzPxFA3gVVsJ9KK4+/pxuGadqeN39AreKXIwG/3GShpF6yJkTw038y6WB0PTTI7NpDLM+KcdnxHYW18mJKSQkNDw6AYOZ6JFipWfHx8WLRoEf39/ezatQuTyb7CiUIIXnrpJeLj43VN014RQiQ4ZqaKsaLEyiRHCOEhhPinp6en57///W9h7wVP13Wys7Pp7u4mIyPDIUWUxsPJBIu1wFNXVxcLFixg+vTpgOVE8cRlc1g1Y2JdHeNC6rz+WTaffvopRUVFBAYGsnz5crKysiakx0qAlzvLkyYuxXUirR0TacWZEeWPeQISJCu6BH88ZOAP+RoNvXDhFJ0H5pu5Z0kQOMwJ5VyEEKSmpjJnzhza2trYvHkzPT09g6+7SqhYCQ0NZe7cubS3t7N7926bAoKHEhwczBtvvKEJIfyBvwoh1PVzAlEf9uTnp1LK2b/+9a/FrFnjbx53PAcPHqSxsZHU1FSH9NOwheEEi/V5X18fWVlZg/2GrLgbNP54bQZz4yZWXI2OZEGY5UKT5tOOWbOYoK2BiBPJRLqCJkpA6LqkvmPirDj95tHjLxxJYZvGk/sN/KVQw93NjbaqI3zxxRfU1ExsKro9JCYmkpmZSXd3N1u3bqWnp8flQsXK1KlTmTZtGjU1NRQWFto93uLFi3nggQcEsBK4w+4BFWNG1VmZxAykKWevXLnS8MUXX9gdV2Lt+RMVFcWiRYtc1jnYirUOi4+PDyaTCZPJRFZW1knTp5s6+7j8z9s52uj6Du4B7pKrpuukBUua++DDco0W4cf/frBq3H1oHEFLl5HMX3w2rlRqeyh47LxRS9YPxZY6K/UdvSz6xdiKs9mLt7sBk1mnf4I+v6F4uWt8dtdK+tvqKSgooK+vj9jYWGbPnj1hblp7qa6uJicnBy8vL3Rdp7+/36VCxYqu62zbto2mpiaWLVtmd1E7o9HIwoUL5f79+/uklLOllMUOmqriJCjLyiRlwP3zVy8vL+3ll1+2W6h0d3eTl5eHt7c38+fPd7lQAYuFJS0tje7uboxGIxkZGaPWeQn18+Rv31pEuL8rT+CShWE6980zMzNIsqFa8Ms9BrIbNYobuvn79jKXzCrY14Ml4yxQZw/1ExC3UjeBacupUf4uESoAt65KIi7EUr34rLPOGgxeXb9+PdXV1S6Z03iJiYlh9uzZ9PT00NfXx9y5c10uVAA0TWPBggV4eHiQm5s7bGzNePDw8OCvf/2r0DTNE3hZuYMmBvUhT14esLp/7O37o+s6ubm5mEwmMjMzHZYyay+dnZ0UFxcPCqfDhw+PKa05PsSHV765ED9P53Y9Ho4Ad8lNKTrXJet09cMzBw38p8xwTFr1/31WSHOXccLnBnBeetSEHWsiXEETGVzrKhtzbJA3N6/88jfu7u7OvHnzWLJkCUIIsrOzycnJwWh0zXdqrHR1dVFUVIQQAiEEhYWFx8SwuBJvb28yMjLo7e11SPzKvHnzePDBBwWwArjTIZNUnBQlViYhQogM4P7Vq1dz22232T3e4cOHaW5uZubMmYSEhNg/QQdg9Wn39/ezZMmScddhSYuxVLn1mDB3iyRzwJoyK1iyoUbwxD4DJR0nWqjae008+WnBBM3rWM5Ji2SijGYTISQmSqx4umlOzQI6GT85PxVvjxPdaREREaxdu5YpU6ZQVVU1qWNZhsaoZGVlnRDDMhmIjIwkKSlpsNWFvdx///3MnTtXCiF+parbOh8lViYZVvePj4+P9tJLL9nt/qmvr6eoqIiIiAiSkiZHoamenh62bt2K0WgkKyuL8PBwmwrHLU0K48kr5zr94hzgLvlOis71yTpdpgFrSqnhpEXqXttZziEXXPwi/L1YOHViBOlEZARNVKXc1Gh/ek0TG1wLsDAhmAvnjBwY7e7uzvz581m8eDFCCHbt2kVubu6ksrIMF0wbExPDggUL6O7uZvv27ZNmvjNnziQ4OJj8/Hyam5vtGsvd3V25gyYQ9eFOPh6UUqY7wv3T29tLbm4unp6eZGRkTIo4lf7+fnbs2EFvby8LFy48JkbFFsFy4ZwYHrrQ/iyp4bFk+vxknpnZIZKNNYJf7x3emnI8uoSfv5dvt7nZFibKFXQ6uYEMLvhtCAEPX5Q2pt9lZGTkoJWlsrJy0lhZTpb1Exsby/z58+no6GDnzp121zpxBJqmkZmZiZubm0Nca3PnzuWhhx4SwHLgew6ZpGJYlFiZRAy4f+5bvXo1t956q11jSSkH78AWLFgwKTIKzGYzO3fupL29nXnz5hEVdeJF1RbB8s1lidy6erpD5+rvLvl2is4NyTo9Jvj9AQNvlxowjqPk//aSJj4+WOvQeY2FiRIrtRMRYDsBYsXdICiota8XkC1cmRlPeuzYU/Enm5VlLOnJ8fHxpKWl0dzcTE5OzphK8zsbHx8f5s2bR09PD3v27LH7huK+++5j3rx5UgjxuHIHOQ8lViYJQgiDEOJ5b29vh2T/FBUV0djYSEpKCuHhri+mJqUkJyeHpqYmZs2axZQpU0bc1hbB8qNzU7jMQZU/04N17ptrZs6ANeWJvQaOjMGaMhyPfXCIXieUbz8ZMUHezIsPcvpxRhISZrOZ3t5euru76erqorOzk/b2dsxmM2azmfb2djo7O+nq6qK7u5ve3t4RL2IT4WpKjQqgyzix/yN/TzfuOSfFpn2tVpb4+PhBK0tDQ4ODZ3hyxlNHJSkpiaSkJOrq6ti7d69LrI3HExMTQ2JiIjU1NZSV2Ze9d5w76A9iMpiwT0MmPp1CMRLfkVIuePjhh+3uftze3s7hw4cJDg4mJcW2E6IjkVKyd+9eamtrmTZt2phiZ6xF4XJzc9m6dSvLli3jZF2mhRD86rLZNHX1saHA1hO35JxYyVem6DT1wsuFGsXt9onGypYeXtpylNsnuDHd+elR7KlodeiY7pokwB0CPSxxPDFaGwcPHqSvr4/e3t7B5WR9YgDWr18/7HoPDw+8vLzw8vLC09MTLy8vEjw68QnRaTMK2vuh3QgmB/eJ8pjgjtUAd56VZFf6vbu7OxkZGcTExLBnzx62b99Oeno6iYmJTnf32lLwbdasWfT19VFeXo6npyeOKHBpL2lpaTQ2NnLgwAEiIiLsaocxZ84c7r77bvGb3/xmHXAZ8KbDJqoAVFG4SYEQIkzTtKKkpKTA/fv3C3tSi3VdZ9OmTXR0dLB69Wr8/f0dOFPbOHToEIWFhcTGxrJgwYJxnUytheP8/PxGFSwA3UYTV7+wk73jvFB7aJJrpuvMD5MUtcFfCg10mRxz0vfxMLD+h6uJDDj53B1JeVM3K38zvCgYCwHuknhfSbwfxPlK4v0kQSN8LYUQxwgMLy8vPDw80DQNIcTgY3GxpXZWUlISUkp0XR98NBqNxwievr6+Ee/Am/ugolNQ0SWo6LI8t/V/ZdAEPh4GOnonLp4iMcyXj3+w0mEiqaenh127dtHa2srUqVOZM2eO0xqT2lOZVtd1du7cSX19Penp6YPtNFxJS0sLmzdvJjQ0lKVLl9ol9Do6OkhJSdHr6upqdV2fIaV0feXK0wglViYBQojngZs+/fRTzj77bLvGKigo4PDhw6SlpU2K7J/S0lL27t1LeHg4ixcvtukkOl7B0txl5PI/baNkjFVugz0k304xE+8HW2oFb5VqDu/yfGlGLE9eMc+hY47GBU9vJn8MGUkB7hYxEu8rifeFeD9J4BBh0m2Cyi5BbQ+0GwVtRouFo71f8M73VhEe6Demk/x4KthKKSmuaeXGF7cS4GGx6AR4QJCHJMpHEucLXkOyfVv6LP12LCLGImA6xyBg0mICOFg9sVlbL30jk7NmOrZYmtlsZs+ePVRWVhISEsKiRYscHqfmiBL6JpOJbdu20dLSQmZmJrGxrm9Qmp+fT1FREXPmzLHbqv2vf/2Lq666CuBXUsr7HDJBBaDcQC5HCLEI+M7Xv/51u4VKe3s7BQUFhISETIq7lsbGRvbt20dgYCALFy60+W5vvC6hEF8P/vqtRVz6p200jNJXJtFf8u0ZZnzc4I0Sja11zrkjfXt3Fdcvnsr8KcFOGX84zk+PGlas+LpJZgVJ0kMkif4nCpOKTkF2g+XiX9klaOwFGP7C39wriQhyvNtBCEFjj05Vt6Cq+8TxBZJwLwZFVpyvZEaAZE7IlzdfLX1Q0iE40Cw41CroMZ84jrf72NsFOIKVM8JZm3ryKs22YDAYyMjIICAggPz8fDZu3EhWVpbDGpU6qtePm5sbixcvZtOmTeTl5eHr60tQUJBD5mgrKSkp1NbWcvDgQSIiIvD19bV5rCuuuILnn3+e9evX/1AI8YqU0jUFl05DlGXFhQwE1e708vLKKCgoEPHx8TaPNdT9s2bNGvz8/Bw40/HT1dXFpk2bEEKwcuVKu/zBVsZrYcmvbufK57bT0Te8iT8rXOeKaTq9ZviLA+JTRmNefBBv37oUTZuY+Lvi+k7OfnIjIInwgvQQSXqwTqI/aAKMOhxt/9ISUdElaOqDkYTJcLzyzYWsThnbxXe8vYHeyaviB//aM+a5CCRhXgy4rywiJtEf3DQw63BkQLgcaBE09QmEgGBvd5q7Tx5j4ygMmuDjH6wgKcK5rtna2lpyc3ORUg7GtdiDM5oSdnR0sGnTJtzc3Fi1atWov2Vn09rayqZNmxziDjp06BCzZ8+WZrP5M+BcqS6yDkFZVlzLt6WUCx566CHsESpgyf5pa2sjPT3d5UKlv7+fnTt30t/fz7JlyxwiVGD8FpZZMQE8d8MCbnw5G+OQbroakq8m6KyOllR3wwuHDTT3OV9A7Klo5Z09VVzqoKylk6HrOkFaLzfONBDr2UeEt2V9uxF21lsu2AVt4qSF7caCM1OLx1tjRSJo6IWGXsHuJss6T4MkNVCSHiyZFSyZESi5NBFquqHG6MXGCiMtA/s6m+sXT3W6UAGIiopixYoV7Nq1i+zsbFJSUkhJSbHpAuys7sn+/v5kZmayY8cOsrOzWbp0KQbDxFq5hhIUFERycjKFhYWUlpba5Q6aOXMmd911l/jtb3+7DrgUeMthEz2DUZYVFyGECNU0rdgRQbVtbW1s3LiR4OBgli9f7tLib1JKdu3aRW1tLfPmzWPq1KkOP8Z4LSzv76vmzn/mISX4uEluTNZJCZLsaxa8WqTRZ+cFezxEBnjyxT2r8XVCXyMpJXV1dVRVVVFXVzeYlVPdBQdaBAdaNMo7HXthvuvsGXz/7LGVlhivZeWRdw/yyrZS2yY2DBqSBH9Lanp6iCRyQMB1GOFgqyC3UVDUJpwiXIJ93NnwwzUE+rg7fOyRMBqNZGdn09jYSHR0NBkZGbi5jf175yyhMpSioiLy8/OZMmUK8+bNc+m5y2w2s3HjRrq7u1mzZo1d7qCOjg5mzJih19fXV+u6nqqCbe1H1VlxHb/UdT3o2WeftTv7Jy8vD03TJkU35cOHDw+mKDtDqMD467BcOCeGhy+cRaS35O50MylBko8qBS8XTKxQAahr7+NPG+zvSzKU7u5uDh06xCeffMLOnTupqqoiKCiI9PR0EuYs5ol9bnxQYaCs0/EXYmdWmHW01UZHUNIheLfcwC/3uPHHAm/eKdWo64VF4ZLbZ+k8MN/MulidAHfH3sTdfU7KhAoVsKSCL1myZLCeyObNm+nu7h7TvhMhVMCSGRYXF0d5eTklJSVOOcZYscb96Lpud7E4f39/nnrqKU3X9Tjgp46b5ZmLcgO5ACHEHOCmyy67jHXr1tk1VmlpKW1tbaSlpbnc/VNVVUVhYSHh4eGkpaU59VjjdQl9ZYYfAY0S80B8yp4m1+n05zeXcOXCeOJDbHePSSmpra2ltLSU+vp6wHKCTEpKIj4+frCztpSSqaE+lDWN7SI1XiaTG2g8JEf4UVDfSQEa62s0/NwkiyIkSyJ0Lpyic348HGwRbKsTHG61T+SlRvlz9UL73Ly2omkac+bMISAggH379g0G3p6soelECRWwBFLPmzePzs5ODhw4gL+//zEtOCaaoKAgkpKSKCoqoqqqavA8YwtXXnklzz//PBs2bPihEOJ5KWWp42Z65qEsK67hlwaDgSeeeMKuQXp7ezl06BD+/v7Y20fIXjo6Ogaj+zMzM51W52EoY7Ww1NbWsmPHDny9PDigR7tUqAAYTTq//PCQTfv29/dTXFzMZ599xq5du2hqamLKlCmsWLGCNWvWMH36dIZa6oQQnJ8+cqM8e3FmhVlnNjEMPs7K0WkSfFGt8Ys9Bn5/0EBeo2BmkOSWmZZO28sjdTw02+60H7pwFm4T1h18eBISEli2bBlSSrZt20ZTU9Ow202kULFiMBhYtGgRXl5e5OTkuLxL84wZM/D29ubAgQOjFjg8GUIIfve73yGldAd+5rgZnpkosTLBCCFWAF+55ZZbhL3pxfn5+ZhMJqcWgRoLZrOZnJwcpJQsXLgQe9xa42U0wVJdXc2uXbvw9PRk+fLlPPC1TNakuL79wP8O1LL9yPAXjOHo6Ohg7969fPzxxxw8eBAhBOnp6Zx77rnMnz+fkJCQEV2A5zuxV1B9h3MEha5L6kdJO7eH8uaRLE2CI+2CvxcbeCjXwH/LNDw0+Po0nZ8tMHPJVDMhnmMXLeemRbI0Kcwxk7aT0NBQli9fjsFgYPv27SeU6HeFULHi7e1NZmYmJpPJ5T2E3NzcSE9Pp6+vj4IC+zKP58+fb627cr0QYrZDJniGosTKBCIsPOHj4yMffPBBu8Zqbm6moqKC2NhYwsJcezI8cOAA7e3tzJ4922F1HcbDSIKlsrKSnJwcfHx8WL58Ob6+vrgbNP5wbcaE9M4ZjZ+9dxCzfvILX3d3N7t37+aLL76gtLSU4OBgsrKyOOuss5g+fTru7qPHQcyJCyQ2yNtR0z6Gxk4jRpPjLyyNXX2YRvlsbGVamO+YmjB2D1hbfr7bwF8KNGq7YU2M5IF5Zi5PNOM/SlyLh0Hjpxe4vqz8UAICAli+fDlubm7s2LGDuro6wLVCxUpoaCipqak0Nzdz+PDhCT/+UKKjowkPD6ekpIT2dvuKBj766KPWTKdfOmRyZyhKrEwsF0spl9xzzz3CnpOBruvs27cPg8Hg9NiQ0aiqqqK0tJSYmBinBdSOheMFy5EjR8jNzcXX1/eE9GkfDzdevnEh08Jsj/Z3BIdrO3g9u3zY1/r6+jhw4ACff/45FRUVxMTEsGbNGpYtW0ZUVNS4AqmFEJybdmpZV+qd2NF5vD15dAR7mjWePujGb/cZONQqWBEleXC+mQvizXgZhhct31mRyJRQx6TtOxJ/f3+WL1+Op6cnO3fu5OjRoy4XKlaSk5MJDw+nqKhoMBbLFQghmDNnDgD79++3K9g2KSmJ7373uwK4UAix3EFTPONQYmWCGCgA93hISIj+wx/+0K6xrEG1qampeHs75455LHR1dbFnz57BluuuzkQaKlgOHDgwmNo83GdkrXIbYUczOUfw248LaBtSlMxkMlFQUMBnn33GkSNHCA0NZdWqVSxcuJCAgACbj3P+bOeJFWcE2TozFqaqxfaYiIouwQsFBp4+YKCqC86Ns4iW1dE6buLLC1qEvye3TXDzyvHg5+fH8uXL8fLyYt++ffT29rpcqIBFJGRkZODp6cnu3bvH1HHdWfj5+ZGUlERjYyNVVVV2jfXggw/i7e0tgSdUV2bbUGJl4rhBSjnzwQcf1Oy56PT19U2KoFqz2Ux2djZms5nMzMwxuSMmgqF3QKPdDcWH+PDKNxfh74SaJ2Olpbufpz8vQtd1SkpK+Oyzzzh8+DB+fn4sXbqUpUuXOqQc+YIpwXZ1+T0ZtW2Ot4I4KxNoSogPla32B3CWdAiePmjg+cMaHf3wtQRL2nNWuI5A8uPzUvFz4fdqLEgpx/V7mSi8vLxYsGABfX19g5V4XYWjgm2joqK45557BLAUuMhhEzyDUGJlAhBCeGma9vP4+Hj9lltusWusgwcPToqg2vz8/MGU6eDgiet3czKqqqrYvXs3/v7+zJkzZ9APf7K7s1kxATx/QyYeLsrWEEj2F5bw0SefsX//ftzc3MjMzGTlypWEhzsuEFjTBOc5yRXkDGHhrJToqEBHlnUXHGzReGKvgVeLNSRwTZLOw5mwJFqbNBf/4bD+NoxGI/Pnz8fb25vs7GyXul6GEh4ezowZM2hsbKSwsNBl83BksO0Pf/hDgoODdSHE40II15XrPUVRYmViuF3X9bhHH31Us6cHRmtr66QIqm1oaKCkpISoqCiXp0xbqampGYxRWbp0KYmJiWMuHLdkeihPXTmPiTbOxvhI7p5t5rokM+09fcydO5e1a9cSGxvrFJeas7KCTiU3UL0T5ioRZDdo/CLPwH9KNUK9BNnZ2WzdupXOzk6HH89ejg+mnTJlCkuXLsXDw4OdO3fS2Njo6ikClgaDoaGhFBQU0Nra6rJ5DA227eqyvRBtYGAgDzzwgCalnAVc77gZnhkoseJkhBD+mqY9kJaWJq+77jq7xsrPz0fTNGbNcl2GQX9/P3l5eXh4eDB37lyXx6kA1NXVkZ2djbe39zHF4cZT6fYrc6J55KKJCVbWhOTcOJ0fzjYT5Q3vl2s8mC042uvjVGvZosSQE+qLOAJnCAtnWGtigrwodVJxPACTFARHxXPeuecwY8YMmpubWb9+PUeOHJk0VpaRsn6sgeju7u7s2LFjxDosE4m1KremaezevRuz2eySeQghSEtLQ0rJoUO21UeycttttxEXF6drmvaoEGLiajycBiix4nxu0XU96Oc//7mwp1FXQ0MDDQ0NJCQkOKwxoC3k5+fT09PDnDlzXN4pFaCxsZFdu3bh5eU1bDDteATLN5YmcPsa+2rfjEaMj6Xk/wXxOqWd8MQ+A59WafTrgkffz3dKGrAVN4PGObMcb105VdxAccHO/d14uxv48XmpuLu7M3PmTFavXo2/vz8HDhxgy5YtLreyjJaebI2TMhgM7Nixw6XWDCu+vr6kpaXR0dHhUndQYGAgcXFxVFVV2fW5eHl58fDDD1vL8Nt393qGocSKExmIVbknNTVVXnLJJTaPI6UkPz8fNzc3ZsyY4bgJjpOGhobBNGV72847gq6uLrKzs3F3d2fp0qUjirjxCJYfnpPC1xc4viuyJiTnxOrcM9tMpDe8fVTjmYMGGnu/tEyVNHbxt+2lDj/2UM5zQlbQqeIGau5yXjo0wO1rph8TExMQEMDKlStJTU2lpaWFDRs2uMzKMtY6KgEBASxduhSAnTt3ujQbx0pCQgJhYWEUFRXR0tLisnmkpqYihLDbunL99dcTFRWlCyF+omJXxo4SK87lG7quR/7kJz8R9pj3a2pqaG1tJSkpCU9P16TaDnX/zJkzx+Xun/7+fnbu3El/fz8LFy4ctS/SWAWLEILHL53N2lTH9SeJHrCmfGWKTnkn/HqfgY212rD9Zp7+rIjGTuddVJdND8Pfy7FZKrVtvQ69APcYzbT3mhw2HlhSiYvrndf4NjbIm++sODF+S9M0UlJSWLVqFX5+fi6xsoy34FtgYCCZmZn09vYOZvy5EiHEoDsoLy/PZfPx9fUlMTGR+vr6E6r/jgdPT0/uvfdeTUqZDHzNcTM8vVFixUkIIdw0TftxfHy8fs0119g8jq7r5Ofn4+npib3l+e1hqPvHVYLJipSS3bt309HRwdy5cwkNDR3TfmMVLG4GjWevmW93lVurNeWHQ6wpvz9ooKF3ZKHX0Wfid584z9zt4aaxbqZja2n0mXTaexwnLpzhVkpwcgHAn35lJl7uI98kBwYGusTKYmtl2sjISGbNmkVzczP79u1zecyNj4/PoDvI3qwce5gxYwYGg4H8/PwxfSZvvvkmd955JytWrCAgIAAhBNdddx0333wzQUFBuhDifmvdFSHEK0IIOcry+VjmKYRIGGWc1+38KCacyV0I4NTm67quJ95777121SApLy+nq6uL2bNn4+bmmn9XfX39oPsnNjbWJXMYyuHDh6mtrWXatGnjrpo71m7N1iq3l/95GyUN478jD/eSfCPZTLwflLTDa0dOLlKG8np2OdctnkJajHNaF5yXHsXbefYVuTqe2vZeAh0UvOsMF9DQwnuOJisxZEyZVlYrS1RUFHl5eRw4cICamhoWLFjglOKO9pbQT0pKor29nfLycgICAlx6swQWd1B1dTXFxcVER0e7pGSCp6cnSUlJFBQUUFNTM6o7/LHHHmPv3r34+fkRFxc32EbAz8+P73//+9rPfvaz+cA64BPgHaB0hKGuB6YB/xvnlPcOjHs8B8Y5jstRlhUnMNAD6L7Q0FD929/+ts3jWKuZ+vj4kJCQ4LgJjgOz2cy+ffsG3T+upqqqisLCQsLDw21uNTBWC0uIrwd/s6HKbUqgzt2zzUT5wH9KR7emHI+U8LP3xnbnZgsrZ4Tj4+FYV7kjrSGOLt8f6utBQV2HQ8e0ogl46KJZ43KLDrWyNDc3s3HjRpqbmx06L0f0+hFCMG/ePIKCgjhw4IDLa7AMdQft3bvXZdYea2fzQ4cOjdpw8amnnqKwsJD29nb+9Kc/HfPanXfeiY+PjwTuA5BSviOlfOT4Bfg/IBowAq+Mc7p7hhtTSvnmOMdxOUqsOIcLpJSz7777bs2ezJ2jR4/S29tLamqqywrAFRcX09XVxaxZs1zu/mltbSUvLw9fX18yMzPt+kzGKljign3467fGWuVWsjpa55aZOkYdfn/AwIaa4WNTRmPX0WY+3F877v3Ggpe7gTUOjMkBqHOgNcTRlpVp4c5zAV21yDYLmNXKsmTJEqSUbN26lbKyMofMyZFNCQ0GA4sWLcLT05OcnByXZzT5+PiQmppKW1sbpaWlLpmDu7s7M2bMoLOzk4qKipNuu2bNGpKTk4cVs6Ghodxyyy0CWC2EWHKSYa4HvIG3pZSTowiOC1BixcEM+B/v9/Pzk7fddpvN45hMJoqLiwkICBh0XUw03d3dFBYWEhwczJQpU1wyByu9vb3s2rULIQRZWVl4eNhfomCsgmVm9OhVbg1Ccs10na8lWIJof7fPQHmXfUHIv/zwEL39zgkmdHSBOEdaVhwds9LV55zP0N/LjXvW2ZedFx4ezsqVK/H19WXPnj3s379/1Lv1k+GM7sne3t5kZWVhNpsHg9pdybRp0/Dz8+PQoUP09Tk3w2skEhIS8Pb2prCw0K7/1913342bm9ugdWUEbhp4fN6GQ8QIIb47EBvzXSGE683jNqLEiuNZASy9/fbbhT09XcrLyzEajSOq8onAeuJ0dfaPtQ9RT08PCxYswN/f32Fjj1WwLJkeyv9dNXyVW393yZ1pZrIiJLsaBM8cNNDeb//nVdXawwubSuweZzjWpETg6ea4n78jBYYjU6EDvd05VNPusPGG8v2zkgn1s9/a6Ovry4oVK4iKiqKkpIQdO3ZgNBrHPY4zhIqV4OBg5s2bR2dnp8v79Wiaxpw5c+jv7yc/P98lczAYDCQlJdHd3U11dbXN48TGxnLjjTcK4CIhRPrxrw9YXGYDhVLK9TYcYh3wZ+AXA497hRDrhRCuvfu0ASVWHM+9Hh4e8gc/+IHNA+i6TnFxMT4+Pi6rZ1JXV0dtbS0JCQkOaaRnK1JK9u3bR3NzM7NmzSIqyvF1QsYqWC6YHc3PLj42TibOV3LPbDNT/eCdUo1/FGuYpOOE3R83HKGmzf7Ge8fj6+nGqhmO6z00Wd1AyRF+OOOyOi3clxuWJDhsPHd3dxYtWsSMGTNoaGhg06ZNdHSMPc7GmULFSnx8PElJSdTV1blMJFgJDw8nNjaW8vJyh8f7jJUpU6bg4eFBYWGhXeLtRz/6kfVm8IfDvHzzwOML4xy2G3gUWAAEDyyrgPXAauBzIYRzU+QcjBIrDkQIMQ34yrXXXivsuahWVlbS09NDcnKyS2JVzGYz+/fvx8PDg5kzZ0748YdSUlJCeXk5cXFxJCUlOe04YxUsNyxJ4I41lnnMD9X5froZLwM8f1hjfY0GNsSnnIyefjNP/O+ww8aTUnKopp1ff3QYkx3m6+OxWlZMJhOdnZ00NjZSVVVFeXk5ZWVllJaW0tfXR19fH6WlpZSVlVFeXk5VVRVNTU10dXVhMlnSn+vaHWfa7zU5xwX04IWz8HCgZQosAaQzZ84crHGyadMm6urqRt1vIoSKlVmzZhEZGUlxcfGo8RrOJi0tDYPB4LLUajc3N6ZPn05HR8eY/k8jkZyczEUXXYQQ4mohxOAdhBAiELgCGwJrpZT1UsqHpJS7pZStA8sm4BxgJ5AEfMfmSbsAlbrsWG4DxB133GHzAFJKioqK8PT0JD4+3nEzGwfWoNp58+Y5JDbEVhobGzl48CBBQUHMmzfP6a6osaY1370uGa+uGuINbdT3wAuHDdSPI9tnvLyzp5rrl0xlwdQQm8eobOnmrdwq3ttXTXG9JUjSz9MNNw3GU+FfE5Job4j3k0R6SwI9IMBdEuLVwgcffDAoOE7G3r17R3zNzc2NbySYaDMK2o3Q1g813YLKTkFdD+jjEIP+nm4cqna8C2hNSjhrUhwboDyU2NhYfH192blzJzt27GDWrFkkJSUN+/2fSKECFkG1YMECNm3axJ49e/D393eZ5dXb25uUlBTy8/MpLS0lMTFxwueQmJhIUVERhYWFREZG2nyOuuOOO3j33Xc9sAiIxwdWXwf4AK87KrBWSmkSQrwIZAErgacdMe5EoMSKgxBC+Gia9p3FixeTkZFh8zg1NTV0dnYya9Ys7OklZCs9PT0UFRW5PKjWZDKRl5c3aB6fqM9iNMFidUvFG9qo7/fkyf0meszOj+f52Xv5vHPbMjRtPCnQkm1HmnhlWymfH6pDP+7ms7PPxOzYAPZXDX9BNwhJ1IAwifeVxPtJYnzAfYhBoc8M7f3Q2idJSwjHx9sbLy8vPD098fLywmAwIIRA0zRyc3MBWLBgAbquI6XEbDbT29s7uLR3dlPYXEeIpyTBHzw0YMCRYzRDVTdUdAoquwQVXYLa7pEFzIxIf3LLHVue3U0TPHCh8xuJBgUFsWrVKrKzs8nPz8doNDJr1rEp0hMtVKy4u7uTlZXFhg0byMvLY9WqVS7LVpw+fTrl5eUcOnSIuLg4u2pa2YK7uzsJCQkUFxfT1NREWFiYTeOcffbZzJgxQxYXF98mhPiNlNLEl4G1zzlswhas5XdPKTeQEiuO41pd1wMdYVWx/gBcQUFBAWazmfT0dJcG1R48eJDu7m6nFcw6GSMJFl3X2bNnDxUVFcTGxnJ2+lw+rN9FXnmr0+e0r7KNt/OquHwMfYt6+828mVvJK9tKB60oI+HpNlQESiK8ID1Ekh6sM9UPrJ4OXUJDL+xtsoiEii5BdRf0mMHq+rryotnEBo38v7IKzpPdiR+oauPpd60dfyW+bhDrI4nzY1AwrYyWDAoYHUo7BAeaBQdaBE19X35n+x3o5rLyjaUJTA8/eWsHR+Hl5cXSpUvJzs6muLgYXdcHf5euEipW/Pz8mDVrFvv376egoMBl7mJN00hLS2Pnzp0UFRW5pCP99OnTKSkpoaioyGaxIoTgzjvvFHfeeWcccLEQogqYiyWwdoMDpwuweODROdH7TkKJFQcwUATujvDwcP2yyy6z+RajoaGB1tZWZsyYMeF3CAAdHR2Ul5cTFRVFSIjtLgd7sTZMjI6OdlnF3OMFy5IlS8jPz6eqqor4+Hjmz5+PEIKXv7GQy2yscjtenvjoMOelR+E3Qs2Xtu5+/r6jlFe2ldLYObZskuK6dpIDJGnBOunBkvABrdFuhD1WYdIpqOyCPv3k4rW2rfekYmUsHBtcK+gyQWG7oHCI8cfbIInzlcQPCJgZgZbl0kSo6YYDLYKiDjcO17TZNZfjCfH14HtnJTt0zNEwGAwsXLiQnJwcSkpK0HWd6dOns23bNpcJFSuJiYnU1NRQVFREdHS0y9xBkZGRhIaGUlJSQmJi4phubt588002btzInj172Lt3Lx0dHVx77bW8+uqrJ2w7movpyiuv5L777qO0tJTW1tYxfQ7btm3jscceY8eOHfT29pKUlMQ111yDr6+v7Orqup0vK9meNF15IK4lGmiTUtYMWZ8F5Ekpjcdtvxa4a+DPE9/sJEaJFcewSEo55+abb7YrxqOoqAiDwcC0aSc2RJsIDh8+jJTSJXcnVoY2TJw7d65LrTtDBcsXX3yB2WwmISHhmFTu4IEqt5f9aZtDA0OHo6Gjjz+sL+bH56Ues76ps4/nNpXwjx1ldBlHDyg1CElasGRuiGRmsAnfgbNAdRd8XCk42KJR3sm4i9k5IuW4bgzVa3vMgqJ2QdGAgNGwuIzSg3XSQyTrYiXr6KejH/JbBHuaBIdbxbjiXYbjh+ekEOg98TcRVsGye/duSktLqaioQNd1srKyXCZU4MsKt+vXr2f37t2sWrXKJa5rIQSzZs1i8+bNFBYWMnfu3FH3GakM/smYO3cul1xyyQnr09PTSU5OpqysjKKiIhYuXDj42jvvvMM777wDQG2tpcjjZ599xmuvvYbBYCA1NZXzzjuP9957j/vuu49p06aJkpKStViyeYzAX0eZ1teAvwxsd+OQ9U8AaUKIDUDlwLo5wNqB5w9KKbeN+qYnEUqsOIZbhBB85zu2B1e3t7fT2NhIYmKiSyrFtrS0UF1dzZQpUxxax2S8WBsmZmZmnvA56LpkV2kzi6eNrXGhI4iJieHIkSO0trYOVq48XkBZq9x+/c/b6XBwt+DjeWnzUa5aGM/UUF96+838ZWspf1xfTEff6McN95IsjtDJCpf4e1gCa4vbBR3Clw+P9NDcZ9/F3BEpx7akQOsISjqgpMPAu+WW93luoidBopvMcElWhKS1D3Y0CHbUabQYx/8+Z0YHcOVC1wS8g8XdkZqaSm1tLWazmfDwcCIinBfkO1Z8fX1JS0tj3759FBYWuswdFBISQlRUFGVlZUyfPn3ULuxPPfXUYIbhxo0bWbNmzajHmDdvHo888siIr8fGxlJZWUl3dzfWyuV79uzhr389Vm9YM4dMJhMdHR385je/4dFHH2Xt2rVs377dupm9gbV/xyJkFgLnA+5AHfAG8KyUcrON47oMlbpsJ0KIYCHEVRdccMG4m+oN5ejRowAusapIKcnPzx88IbqK0RomvrKtlKue38F9b++nZwwWBHuxdndubW0lPDyc/v5+tm3bNmxac2pUAC/ckOnwdNbjMZp1fvHBId7Jq+Ks323kiY8On1SoCCzxJ7fPMvPAfDNnx0qajfD6EY2f5hj40yEDH5aZ7RYq4BjLiiOKy7WbDLxZZOLZfDceyDHw5lGNbhOcFyd5KMPMzalmUgN1GEcFlocvmoVhHMHNjqarq4tt27ah6zphYWE0NDRMim7IYKnmGhYWRlFRES0tjg1oHg8zZ860pOYfOjTqticrg28rVlfR0DYAjzzyCFLKweWll14C4IYbbkBKObitl5cXjz32GAD+/v5omtYMfHO0Y0opX5FSCinljcetf0lKeaGUMkFK6Sel9JRSTpFSXnkqChVQlhVHcL2U0uu73/2uzQMYjUYqKiqIiIgY9Y7AGTQ0NNDY2EhSUtKEB7Na6e/vZ8+ePSM2TDxU086vBuqN/HNXOdmlzTxz9XxmRgc4ZT5SSvLy8qiqqmLq1KnMnTuXqqqqk6Y1L54WytNXzuO213bjzGvIJ/l1VLb0UNU6crE4L4NkcYRkRZROmBf0mmFzrWB7nUZV97En6OauflKi/Ciota/viyOERq0DXGmpUf7srbTEq3SbBJtrLctUP1gaqZMRZnGD1XbDplqN7AaB8STxOBfMjppQa97xDA2mzcrKIjw8nNzcXEpLSzEYDKSlpbnUXWptMPjFF18MZge5wh0UEBDAlClTKC8vp6WlxeFdmaurq3nuuedoamoiNDSUJUuWHHOuCg4OJigoiLKyMlJSUob9DL744gsAzjvvvBNeW7lyJT4+PnR1daHreghwGfAPh76JUxhlWbETIcR3YmNj9QsuuMDmMSoqKjCbzS6pE2C1qri5uZGcPLHBg0M5ePAgPT09zJkz5wT3T5/JzA9e34PR/GV2R3F9J1/9w1b+tr3U4XeX1vTkiooK4uLiBmNnxlI47vzZ0fz8Ytu6QY+HbqNp2AgMNyFZE63zUIaZryVYPq+3j2o8nGvgzaOGE4SKlUAv++vpuMoNdDzDW0AEZZ2Cfx4x8EiugffKNLwMcMU0nQfnm1keqaOJE79HHm4a953vusKIw2X9aJrGggULiIqK4siRI2OKt3A2Pj4+pKWl0dHRQUFBgcvmkZKSgqZpY7KujJdPP/2UW265hZ/+9KfccsstzJ07lzVr1lBeXg5YRFtiYiJGo5Gqqqphx7B+NjNmnNhTys3NjcTERHRdx9fXV3KKFW1zNkqs2IEQYraUcvb111+v2XonIaXk6NGj+Pj4uCRYrq6ujra2NpKSklxWAK6+vp6ysrIR3T9/+KKYgroTS48bTToP/fcgN/0tl5au8fdSGYni4uJBd5Q168fKWATL9UsSuHOt86rtApQ2dZOZ8OWdo4YkK1zngflmLknQaTPCSwUaj+UZ2Fir0TtKLZijTfZnM00GN5C7QVBYe/Iy9V0mwWfVGj/LM/C3Io0+M3x9ms7988zMD9URQ9xD3105jfgQ2zun28PJ0pM1TSMzM5OIiAgKCwsd1rHZHiaDO8jHx4eEhAQaGhocVobfx8eHBx98kNzcXFpaWmhpaRmMc9mwYQNnnXUWXV2W309sbCweHh6Dbv3jaWuzWPwCA4fv1G1df9ZZZwlglRDCNV1sJyFKrNjHtQDXXnutzQM0NDTQ1dVFQkKCS0y5RUVFuLm5uSwDaSzunz9uOHLSMT47VMf5T29m+5Gmk243Fmpra8nPzyckJISMjIxhi12NRbDcvW4GV2Y6NyCzsK4Tf0+NOSE6P55r5pokHV3C34s0fr3XwL5mbcwZPQ0dfSRF2OeCrG3vtcvK1dtvpq3Hvo6+qVH+dI4xnkmXgtxGjV/uNfBGiYaHBjfO0LlntpmUQJ2oAE9uXT3drvnYyljqqFizhAICAti7dy9NTfZ//+3B6g4yGAzk5eVhNjs/rmw4rNV+i4qKHDJeREQEP//5z8nIyCAoKIigoCBWrlzJJ598QlZWFsXFxbz44ouA5X8yZcoUWltbaW1tHfexrL+fc889FywFjK52yJs4DVBixUaEEJqmadfOmTNHpqef0CxzzJSWliKEcEm12KamJpqbm0lISHBJXRf40v0zd+7cE9w/JrPOj9/ah+n40qvDUNveyzUv7uB3nxRgMttWDKyjo4Pc3Fy8vb1ZuHDhSf3uowkWIQS/+Fo6Z6U6L2MjzM3I99N1vp2i4+ducff8Yo+BnMaxi5ShhPjaZ1nr7ddp77E9G8oRbqRji9yNDV0KttZZrFDvl2uEecFts3R+kqHR1zX2ZoKOYjwF39zc3MjKysLd3Z1du3bR3d09gTM9ER8fH9LT013qDvL29iY+Pp7a2lra253TcRssn701A3TTpk2D660FPYcG2lqxWk6sFpbjsc531apVxMXFSSHE9Q6c8imNEiu2s0LX9bjrr7/eZnNIb28vtbW1xMTEuCRdubCwEE3TmD7dNXePTU1Ng+6f4bpLv7z1KPsqx17YS0p45otirnhuOxXN4ztpG41Gdu7ciZSSRYsWDdsT6HhGEyxuBo1nr8kgY0rQuOYyGt4GyTXTzXwv3UyAwcS2Rg9+PuDuMdvR8Xm8n9lw2OPGsdcFZNAEhfW2iwujLvi0SuPneQYOdHhj6O9i06ZN7Nu3b0w9jxyBLZVpfXx8WLhwIf39/ezatWvC5joSU6dOJTw8nKKiIqeKhZNhjb9zlHVlJMLDLX0HrW4gsKRzh4eHU1lZSX//sZbClJQUwHLuPR6TycTRo0dxc3MjKSmJ6667TkgpZwshZjvxLZwyKLFiO9cJIbj6atutdGVlZUgpXVJav62tjfr6eqZMmTKmC7OjkVJy8OBBDAYDs2ef+Fs82tjF7z458Qc9FnaXt3LB7zfzwb6a0TcGdF0nJyeHrq4u5s+fP65KnKMJFm8PAy99YyHTwx3ThmNmkM5P5prJipDkNAgezTNwoMuPPgf0J6pp6yUxzL552hO3Ym/MS0qkv12WHSvdJsFXVi3i7LPPJioqiqNHj7J+/XoaGx3SS25E7CmhHxYWxpw5c2hrayMvL8+lKc1CiMHCifn5+S6Zg5+fHzExMVRVVR0jJBzNjh07gBNLTiQkJGA2m6msrDxm/dq1lppsH3300Qljbdq0ie7ubpYuXYqnp+fQ8ALb4wxOI5RYsQEhhJcQ4oo1a9bYXA5eSklFRQW+vr6Ehk58WqT1jiMpybmBoCNRW1tLS0sL06dPP0EsSSm57+199I2nHfBxdPSauP213fzkrX10G09+ATt48CANDQ3MmDHDpv/naIIl2NeDv307i8gA261n3gbJ1dPN3DJTRxPwwmGNvxcb6OgXHKxuZ07s8AF74yXC3z4Ln12WFTvdQL6ejkmXvSwjjnnxQXh7e7No0SIyMjLo7+9n69at7N+/3ymWC0f0+klISCAhIYHq6uph79wnEj8/P6ZOnUpdXd2YY2nefPNN7rzzTlasWEFAQABCCK677rphty0qKuKJJ55g7dq1xMfH4+HhQWRkJF/96ldZv349YLGuSCkpLi4+6XGtlWX/8Y9/IIQ4YVm3bh1G44kB/F988QVPPfUUwAnz9Pb2xmAwcOTIsfF2l19+OWFhYbz++uvk5OQMru/t7eWBBx4A4NZbbwUslXHnzJkjNU27Vghxxl+rVZ0V27hAShlgT2BtS0sLXV1dpKamTnhgbWdnJ1VVVcTFxeHrO/GNN62Fmzw8PIYVS+/tq2FHiWMi+V/PrhioyZLBrJgTa7KUlZVRUlJCVFSUXQXxRuvWHBvkbXOV29Qgnaun6QR5Qk6D4K1SjW7Tsd+Zpi4jbpqlKq09VJ+kdstYsCf12J52BULAkVGaNo4FXw8DPz4vZci4gvj4eMLDw9m7dy8lJSXU1dUxf/58h91kOLIp4ezZs+ns7OTw4cP4+/sP616dKFJSUqioqCA/P5/ly5ePep4bTwn8Bx98kH/961/MmjWLCy64gJCQEAoKCnj33Xd59913efrpp/ne975HREQE5eXlpKSkHPN7HFoG35q54+HhMViB18fHh3POOQeAf//738TGxrJ69erB3/m+ffsGa6Y8+uijLF269Jj5/fe//2Xz5s1cfPHFdHZ2DtbPCggI4IUXXuDyyy9n9erVXHXVVYSEhPDuu+9SUFDA5ZdfzpVXXjk4znXXXSd+9KMfxQErgI2jfOSnNWe8WrORaz08PORll11m8wBW86D1yz+RWNV+9JSECT82WOrKdHR0DNuwsavPxC8/cGyNhCMNXVzyh628svXoMebxpqYm9u7dS0BAAAsWLLBbNI5mYUmNCuDFcVS59dAkV00zc+uANeXFAWvK8UIFoKq1h4yp9hfBqmjpIT7E9sKA9lhW7HEDpUT609xtXyYRwO1rk4gIONEt6uXlNWhlMRqNbNmyhf3799ud8eLo7snWlGYfHx927949YiDnRODl5cX06dNpbm4eLDF/Mp566ikKCwtpb2/nT3/600m3Pe+889i9ezcHDx7kueee4/HHH+ftt9/m888/x93dnXvvvZeamhqSk5PRdZ2SkmMbDFvL4P/1r38dDI41Go3s3buXvXv3Ul1dzSOPPMIjjzzC3XffTVZWFtnZ2bzwwgv88Y9/pKioiCuuuIJNmzYNWkSOZ+NGi7Y43hV0ySWXsHHjRlauXMlbb73FM888g7u7O08++SSvv/76Meehq6++2vr3Ge8KUmJlnAyU17/o4osvFiPlyo+GrutUVVUREhIy4ZYNo9FIWXk5h1oF657J5ufv5TsksHKsmM1mDh8+jLe397CxOn/cUOyQSqjHYzTrPPJePjf9LYfmLiM9PT1kZ2fj7u7OokWLcHNzjJFxNMGSNS2U3181j9F0UbCH5PvpZpZESnIbBY/vNbC/5eQ/14NV7QT72J/VFRNou1ixR3DY83/397L//xcf4s23lo1cmNFqZVm7di1RUVGUlJSM2H5hLDhaqFjx9PQkKysLgJ07dw7rwpgorPWb8vPzR42jGU8J/BtvvJH58+efsH7VqlWsXr0ao9HItm3bCA0NHawqO1RYDi2Db7WsfOMb3xhcNzST59vf/jbvv/8+paWldHZ20tfXR3l5Of/6179YsWLFiPMrKirCz8+PysrKE977smXL+PDDD2lpaaGnp4f9+/dz1113nZCBGBcXx+rVq9E07QohxMRnYUwilFgZP1+VUrrbW1vFaDS6xKpSXl6O1HU21wi6jGZe3nqUVb9Zz23/yCW3zPmFnI4ePUpPTw+pqakn/DArmrt5YfPwxZQcxWeH6jnv/zayftsu+vr6yMzMdLhgHE2wnJcezc+/OnK6+zR/yT1zzER7W3r4/K1oeGvK8XQZzUwPt79dQ/0YOh+PhKtiVsqa7BfcP71gFl7uo8e9WK0ss2bNorm5mY0bN45YU0PXJc9+UURH77FWH2cJFSsBAQFkZGQMXghdhbu7O8nJyXR0dFBRUTFhxwRLavFYqspasZbT/+Uvf8lzzz3Hvn377JqHtep1V1eXXUXyrr32WnRdDwTOtWtCpzhKrIyfi728vORA0R6bqKioQAgx4f5kKSVHSo7S1Af5rV9e/HQJH+6v5bI/beNrf9zKB/tqbK5VcjL6+/spLCzE39+f+PgTC6Y98dFhjPYGXYyBRK9e+jtbaTYEExTinODm0QTL9Yun8r1hqtwuibA0HRTAH/INbK8f3080t7yFaXZm9Bxt7CYmyLYMsdo22+JOdF3aLJKSI/yo77Cvp9DS6aGcmzZ2wSCEIDk5mcWLF2MymdiyZcsJF0MpJT9/P5/fflLI1S/soKnTMkdnCxUrIeGRNOFPZWUlNTVjy4xzBomJiXh7e3P48GGnF4orKyvj888/x8fHh5UrVwJfVpUtKSk5qXVntHL6tmC9IT3eFTQeLr74YmtxyotsHuQ0QImVcTCQBXTeOeecI2xt+Nff309tbS2RkZETXlulvr6e3p5uNteMXDQsr7yV21/bzarfbODFzSUn3BHaQ3FxMf39/cyaNesEU29uWQvvjzHV2B6CPCRfS9Cp64Ffbmu3qSbLWBlNsNy1bgZXLbSINk1ILkswc9V0y9x+t9/AkY7xx9BICe4O6PwcF2xbifmmrj76bRC6zd1G+s22pdsG21nMThPw0EUnfifHQmRkJCtXrsTLy4ucnBwOHTo0eEH808YjvLKtFIADVZbv2tHapgkRKkV1HVzyh638Zlc3bUbYnbfHZe4gg8FAamoqPT09wxZKcxR9fX1ce+219PX18cgjjww2MjQYDEydOpW2trZhLRzjKac/Xnx9fQkJCaGqqgpdt+1GLDw8nCVLlqBp2sVnclbQGfvGbWSNlNL74osvtnmA2tpazGazS1xAR48exaTDzobRT8pVrT089sEhljz+BY+9n09li30X9N7eXo4cOUJISMiwJ+jffDwRzdgkV03X8TTAa8UG+nVBXnkrFzy9mff2VjvliCcTLEIIHrsknfNnhXHrTJ2V0ZK9TYL/O2Cguc/2YN+C2g7m21mIrtnGXktSYpOVwx4XUEWzfXU0rs2aSmqU7d27/f39WblyJeHh4RQWFrJr1y7+vauUX390bAXX1vZONm+xfAecJVSklLy2s5yLnt3C4doOesyCfx7RMPUb2WunW8Me4uPj8ff3p7Cw8IRCaY7AbDZz/fXXs3XrVq688kp++MMfHvP6yarKjqecvi3ExcVhNBppaGiweYyLL74YXdcjgEybBznFUWJlfFwshODCCy+0eYDKykrc3NwmvGlhT08PdXV17G4SY4p/sNLZZ+LFLUdZ+ev13P7abvLKbfO9FhQUYDabh7WqbDvS6LBU5ZOxOEIyM0iyvkZQ2vnlHDr6TNz5zzx+9ObeUWuy2MLJBEu/sY9LYzqYESj5X4XgL4UaRt3+VPaqlh48DLaPU1zfaXPNFVuEh62BuYlhvtTY6HoCCPBy4651J3bAHS8eHh4sXryYadOmUVtbS33xXnzdvrQUhXpK7kgz42uQvFXhhfB2TF2cobR193P7a7u5/z/76e3/8i7+UKvGjnpBdVWVy9xBQghmzpyJ0Wgcte7JeDGbzVx33XX8+9//5oorruDVV1894RxjbRRbXV09ZgvTSOX0x0tMTAxCCLtdQdanNg9yiqPEyhgRlmZAF9tzR9TX10d9fT3R0dEOyz4ZK9aurFtrbfuX6xI+2FfD1/64jcv+tI3/7a/BPIaePWCp61JWVkZUVNQJtSmklDxpY6Xa8RDkIblkqsXF8r+K4T+DN3IqufCZLRyocny653CCpbu7my1bttDT3c3M9LkUGwNt6ukzHPUdfcyfYl8qc0Koba4gW4SHrYG59haxu2vdDLt7IlnRNA1TYBz/KXMjzhfumGXG310OCpUAd0sn7I0V/Vz/0i5aux3nlskpbeaC32/mw/21w77+TqlGqxFyd+e5zB0UFRVFSEgIR44csTmD6nhMJhNXX301r7/+Otdccw2vvfbaiOdWa1XZ8QT6DldOf7x4enoSERFBTU2NzTE7KSkpTJ8+XQohvmrzRE5xlFgZO/N1XY+xxwVkrTUQHR3tqDmNCSklpWVl1PYISu2vm0VuWQu3/mM3q3+7npe3HKWz7+TWiMOHDyOlHCy4NJTNRY3kOD0L6Uv3zz8G3D8jUdLQxaV/3MbLW446vGT5UMGyefNmNm/eTE9Pz/+z995hslzV1ffvVOfp3JPzzM26+eomhIQyQuRgmYwMxoAxYBtjv9iv7dcYY3/OxhgbbIMJNsFkkZOQRJJ0c85hcp7OubvqfH/UdE/qmek0PQp3PY+euepQp7q6+px19l57bfbv38+m9T185tcP0FLA46NcnBwKVrSYh8q0ri+HrJRrJjdcgYndhiYHb3xOd9nvX4irk1F+/dOHeXQE/ueKQksd/M52lffMISrngvqUe3E8wps/dZjYCr+dlaBqkn/+8WVe/e+PL3stEqrgi1cV1GxmzdJBQgi2bt2KqqpVaXKYTqd54IEH+PKXv8yDDz7If//3fy/bfLS5uRmbzZbfuBWDpez0S0VrayuqqpadChJC8PKXv1xIKbcLIXoqOpmnKW6QleLxMpgXjisZY2NjKIqSZ+u1wtTUFKlkkifGBVRp5w4w6E/wwW+f45a/epi/+u75gpNlLBbLu+W6XPN1AVJK/uFHqx9Vyad/RgT90ZU/f1rV+OC3z/HWzxzJV3BUCx0dHWzfvp14PE4ymWTPnj158ppzua2GZwjoXZA7vOV7plwcj5QVdSgnSlKOe22Xr46hQPlk5U9fshWToTpT4Hg4yYOfPERgxpju8KTC1/oEDRbwmOELV0WeqORwYjDIOz93rCxBMsBoKMHr/vMJ/unHlygmyDk3HTQyUpxGqxQL/Bx++ctf5l1l6+rq2LlzJx/+8IdRVZX6+vq8q2wqtfJ3Pjk5WfBY8XicV77ylTz00EO89a1v5VOf+lSuYmZJhMNhbDYbkUhknlneUl40y9npl4qWlhZg1tq/HMxZe56VVUE37PaLhBDiZV1dXXLbtm1lrfaqqjIxMUFjY2PNU0C6KREcnVodW/9IKst//PQan/z5dV60o5W33tbL7k4PMGtlXaiz8yMXJzg5GFyVc8rBa5a8sltjLA7fXSL9sxR+cmGC+//5Z3z4Nbu5dUNDVc4nHo9z9epVhBBIKbl06RKNjY15K/DNLU7+4017efC/DpVdHTMXxwaCbG52cHG8vJDa+kZ7yWLbcqIk5RCcVreVgTIrue69qYk7NlVn0xBOZnjzpw7PI+v1FsndbRJN6q0Ant8uuRCURBfoxX56aZL/99AZ/uqVO0qqRvrB2THe/9VTBEt07f16n8JNHpUTJ05SX1+/YkViKRb4oNvM/8qv/ApWq5XXvOY1+Hw+vvWtb/He976XX/ziF3z5y19m/fr1TExM0N/fz6ZNs3qhuRb4uUX9hz/8IUajkZ6eHjweD5FIhPe+9718+MMfpr+/n4aGBtrb2/ngBz+46FzuvPNO7rzzztnP/vWv86d/+qd89KMfZWhoiJyp5/vf/37Onj1bkp1+qbBYLHi9XsbGxpBSllV5duutt+LxeLRgMPgy4F8qOqGnIW6QlSIghOgA9rz85S8v25J9enoaVVXzDLtWUFWVoeFhLoUE4czq9iBSNcm3To7wrZMj7Ov28tZbu9AG+/H5fIs6GUsp+eeHqyu0K4SX92iYDfD5qwaysvTPPxlJ8cZPPslv3rGe33v+pop24olEIu94euDAAbLZ7KJeQhlV44uHB9na6uLUUIhqJKKyRWqLCiGWKj3HXl5kpXaiXJNB8Mcv3lrWexcilVV5+2ePcH40nH9srkblExcVbEZ44waN39qq8tFziw3+vnBokE5fHb9158pNRZMZlQ995xz/80R53h9JVfD5Kwrv3Jrm/Pnz7N69e9nX/9M//RMdHR1s2LAhX867FMLhMG9729swGAw8+uij7NunF678xV/8BXfffTdf+cpX+OIXv8hrXvManE4n169fZ8OGDfmISM4CfyGy2SxXrlyhu7ubCxcucPfdd/P4448DetS4EFHJYS5ZAX3jFgwGGRoayov93/SmN/H1r3+dw4cP873vfY9MJkNzczOvfvWrefe7372kS22paGlp4fz58wSDwXxZdSkwGo28+MUvVj7/+c/fKYRwSynXrpfCGuBGGqg4vBjgpS8tP/qWU+HXmqyMj4+jqSpHVimqshSO9Af43MPHyWaz9Gcci3Qth677Vz2q0mWX7KmXHJosLv2zFKSEjz16lQc+/jgDZTqlZjIZHn/8ceLxOPv27aOlpWWR6DYYjvGO/z7KQydGODkUYm9P5b1+QO+NtK/MvkEXxsK4bcXtaWwGSYtNYs7EGBwc5PLly5w+fZpjx45x9OhR4vE48Xico0ePcuzYMc6cOcPly5cZHBzEpsVotkkshuKIVZvbSl+Z38Vbbu2lt0LjPNDJ+e/978l5lWwLxbTnggpHpxS+eFWh3Q5v36JiFIs/499+/+KK5fMXxyK8/KO/KJuo5HAhpHDKL+jv7ycSiSz72lIs8L/yla8wOTnJa1/72jxRAd3x90Mf+hAAH/vYxxBCsG7dOpLJ5Lx01FwL/E9+8pMAPPjgg/Ms8Oce6/bbb88/V+i/D3zgA/PO781vfjNSSm699VaSySRTU1NAeXb65aBaqSAppRG4r0qn9bTBjchKcbjbYrHI2267rawVT0rJ+Pg4Ho9nXufPWmBwcJCMBif9tSUrAsntrRr+FPzF48P8zU8neP2BLn7tuT20eWz8x0+vrXyQiiB5abdGRlu6+qdUnBwM8qKP/Iy/fOV2Xr67vfgzkZJjx44RiUTYvXv3POfiud2av/mjRzhyFXK6oiN9AQ72+njyeuVl3demotSZFOKZ0vQRmoSNTc5FImiHUdLpkHTaodMh6bBLfPmMQoxjx47lXyuEQFGUfCXE6OgomqbNEzC/eU6WcCoJgzHBUFQwGNP/vTAa0eGrY6SMdFODw8y7CzgHlwopJR/81lm+c3q2FLgQUcnhyUmFOiO8okfj1es0Pn9VYaF+7P985RRbWpxsbHYuGutzTw7wF98+R6pKDs/fHlDY7lU5f/48Bw4cqMoxc2mT+++/f9Fzt99+O3V1dfzyl78klUrR0dHBuXPnuHbtWkHPqVKOVaq5Znt7O2fPnmVoaKim+kGn00ldXR3j4+MFiw2KwT333JP7513Al6t1bk8H3CArK2CmZPmu5z73uaJcohEOh0kkEnR3V6/yoBik02nGxsc57Rek1NqSlS0eSbMNHupX0BBEkln+/afX+MTPr3P/thb6KzTyWgmb3ZJNbsnDI4JgunqfPZrK8jtfPMHPLk/x5y/bht2y8k/owoULjI2NsX79+oL3QENzK78MunmOO8S7t8FHzxryKbsnr/vZ3+PlcF9lFVP+WKZs4pPMqihI1rkk272SbV5Jk23u8zAUh1N+QSAlCKXhw68/QL3bjtVqzWu0Hn30UUAPzUspyWazJJNJ+ieC/NnXT+Aygc+iE5+tHj0qlsNoHM4GBKf9Cv1R8MfKEz7/wQs247JW3uzx3x69ymcen60qWY6o5PDIqKCtTnCwSTISlzw6Ov++TGRUfutzx3jo3bdSZ9avWTCe5g+/eprvny1/N14I4wnBoUnBc8QogUCgrLTEQuQqfObqUHIwGo309vZy9uxZrl27xk033UR3dzdXrlwpOH6pxyoFNpuNhoYGRkZG2Llz57IVRNWEECLfADMej1NXV7o1QH19PTt27JBnzpxZOh/3DMUNsrIytmia1rgw91kKcmG/WqeARkZGQEqOTNU+2/e8FklahScm5k/Iqibzu9EtLU7MRoVTQ9VNvQokL+3SSGThx8Or89m/cnSIo/0B/uV1e9jevrTB1/DwcF5Eu3XrYp1EVtV4zxeO86PzMS7VKzy4UePd29R5hOVof4CdHe6Kr9OxgQBtHisjweIiEkahk5OddQHesF9SNzNbDMfgkRHBYEwwGBVMJlnkD5My1uFwLN1UUQiByWTCZDIRm0xzdME9KtAJUadd0uWQbHZL7m2X3NuuEsvCGX8YW0ZwLihQi9QibW938cDexT2pSsWXjwzydz+YLb0thqjkPtX/XlNosqm8fEb0fSE0/7WXJ6L86TfO8g+v3sWT16b53f89wWgF7r7L4fuDCvsaVM6cPcfzbru14uPlKmyW6kafezzX9LG3t5erV69y7do19u7dW9GxSkVnZydTU1OMj4/XtEdbjqyMjY2VXQ591113idOnT28RQjRLKcerfIpPWdzQrKyMO2GxUKsUjI2NYbPZFpXurjaGh4dJZOFCsLZRFa9ZLxVeyS33wliEU0Mh2jxW9vd4sRirc5676yWdDnh4RCnJrbdUXJ+K8cp/+wWf+Nk1tAIi1mAwyPHjx7Hb7ezbt29RaaWUkvd/9TQ/OqfPN8enFT57WaHRyszipx9Tk7peYfOC9ECpyKiSBsfKIfNmm+RVPSof3Kfyls0au3ySYNbEV64r/PkxA397ysg3+g0cnVKYSIqCRnaluNgWEuRKBOMJwZEpha/1Gfj/Thr50HEDX+9TiKom9jVKfmOLxp/frPLSLpV6y8pal//3km0YlMruh59cGOcPvzbbxbh4oqIjKwWfvGggnIFf26TRaF183l8/PsT//dppXvefT6waUQEIpAU/GxP4p6eYmJhYtXFyyKX9cvqXclxllzpWqWhtbUVRlIpcZctBfX09RqMx77lVDuasRXdU45yeLrhBVlbGnRaLRZab102n0wSDQZqamsr+YZU77tTUFKf8xe88q4WDTRqKgMfHi7u9RoJJDvcFMBsNHOj1UV+Bo6giJC/u0gil4bHR1f/cGVXyoe+c59c/c5ipOZ4syWSSJ598EiEEBw8exGxe/Jn+4YeX+Oqx+ZPlUoQlldUYCSXo9JXvmwJwaijEtrbFpFkg2ebR+K2bVP7vbpU7WiVjcd3c7I+PGPj+pIufjSlF9ywqpSKo2KqeyaTg0VGFrw7b+ZMjBr54VcGfhnvbJX+yR+Vtm1U2uTUoUEP1kp2tHOj1FX1OhXB8IMBvfe5Y3rm5VKKSQzgj+MQFA0YF3rZFxTZHVNzotLCp2cnnDw0U5Z1SKX44rJBU4fSZsxWbIOaiHXM9TOYiHA7Pex1Ad3c3mqYtIg3lHKsUmEwmGhsbmZiYWPVO0HOhKApNTU356tBykOsmzcxG+tmCG2RlGVRDrzI9PQ1AQ0N1fDqKRW6ndCZQe2Htc5oko3FKdsuNJLMcuu4nlMhwc5enrIqNW5okjVb4wVB1euwUi0cvTnL/h3/Gzy5Pomkahw4dIplMsm/fPpzOxRGRLx4a4KOPFC7dXoqwRJJZUhmNxiKiI8shkswyG2CQbHFrvG+Hyttv0uhxSn42JvirEwY+ctbI4UmFpCo4PxbBZip+upgogayMldDbx2c3c3E8SjwreHxC4R9PG/nbkwYOTQo2eyTv2qrxO9tU1jlnF16LUeGPXlSeoDGHnDttrudOuUQlh8GY4AtXFJpt8OBGDYFkV4ebRDrLhbHlK3SqiXhW8ONhhWgkXLRR3FLYvHkzAJcuLTZ6zGazXL9+HaPROC/90dTUhNVqpa+vbx5ZKudYpaKlpQVVVfNzdK1QX1+Pqqplp7ByuhUhxLNKt3KDrCyPLZqmNS7nLbAScuVxtSYro6OjZLXap4Bu8ki8Fvjl+OJqh2KR1STHBoJcn4pxU6uTXR3F7Z7MiuQFHRoTCXh8orafG2AqmuJNnzzEf33nFwQCAbZu3Vqwj9RPL03yx984s+yxliIsE5EUNrMBp6V8UeCAP86+bi9dDn1xf+dWjXqrXiHyZ0cNfOW6gfHE/OuXzmpsaS0+jbkakRXQTeoWYjgu+MJVA3921MAPhgTtdt3m/u1bVFrrJL95x3raPeVHpBa601ZKVHI4Nq3w42HBVq/kDVutnBwKES3D16ZSPDYqCKfhzNlzaFr51UZ33303AN///vcXPffTn/6UeDzOc5/73HnVO4qi0N3dTSQSIRAIVHSsUpHTENa6uWNuLcitDeXg7rvvFlLKLUKI2nbEXUPcICvL406oTK8yPT2Nw+GoacmypmmMjU9wKSRqGl0AuKVZktGomq/L+dEIJ4dCtHttK+pa7miVuM3wnUEFrcaprxw67BKf6mc0acTkXdwDatAf512fP1ZUE8ilCMuAP06btw5TmV2VfRbJLus079uh0uOUPDws+IvjBn40rJBYrmqshCxBKdGSUsjKciZ1sazgu4MG/uK4gZ+OCba4Je/fqXKrR6/GKwfhZIZf+69DeXfaahGVHE5HHYwnFXY5YgX1K4UQPf1j+v/mJcv/97fFtQUZ+tivc/n/eymf/6+Pk0zEedGLXoQQIl+5UgoeeOABGhoa+OIXv8iRI0fyjyeTSf7kT/4EgHe+853z3hMKhchkdBI4t2dPOccqFVarFY/Hw/j4eNX7gC0Hp9OJ2WyuiKw8G3UrN6qBlkdOr1LWqpBOpwmFQvT09FT5tJbH9PQ0mprlTKC2XNRl0staj08tL6wtB8OBBMOBBC6rkd2dLi5PROfZwJsVyd1tGoNRODm9NkTFICRvWK+S1eA/zkn+5ezP+ctX7uAVe3RPllRW5V2fP0YkWXzzuuPT+ne4sEro4liEXTMVQsVOswLJc5slL+/WMClwJW7ls+czhIos7b4wFsFsEKSLaANQCgEpNgrjtpnmOcUuhUhG8NXrBh4dkbxhixExMszExDi7du6ko6OjaO1YMqPyts8cyadlqk1U9vd4OTkY5NMXBe/bAa9fr/KRs4YVO2+bm9bhvvV1BZ9LDZ0l2X8K27q9BZ8vBGGxcyjm41XBKL/+1rfynOc8BwCHw1HQAv/xxx/nzW9+M6BHCf7+7/8eAJfLxX/+53/ywAMPcOedd/La174Wn8/HN7/5TS5evMgDDzzAa17zmnljf/3rX+ctb3kLH//4xzEYDGzfvh2TyVTWscpBS0sLFy5cIBwOl61/KRVCCBoaGhgfH0dV1bJKp2+//fZcy447gS9V/SSfgrhBVpZATq9y6623inJDjWulV8lNKmdrrFfZVS8xCHhyFVMw4WSWJ6/7MRkEe7u9TEdT9E3H2d+ol9Z+aURZcbJfLbygQ6PNDl++lhOiqvzu/57gp5cn+eDLt/P3P7hYVvnxUoTl5FCIfT1ejhThweKzSF63XmOTWzIS17tPj8SzdHrrCPmLizokMiq7OtycLOIzFEtApJRMFNnEcGOTo6QO3Q1eFx85GWGd08AbNmQ5duwYw8PD7N69e8VIp6pJfu9LJ/K+NNUkKk6rkd4Ge947ZyQu+MGQwou7NO5oXey/shDm5nWYmwtrNUb/+30AOHYtNlNbCorFjvO5r+cxv8avejTe9ra30d6uE+wPfOADiyzwr127xrVruqljd3d3nqwAvOIVr+Cxxx7jL//yL/nqV79KMplkw4YN/OM//iO//du/vSRR7O/vp6WlhfHx8bxJXLnHKgU5sjI2NlYzsgLkfV6CwSD19fUlv9/n87Fjxw55+vTpu1fh9J6SuEFWlkaPpmmNt95avv9ALsxXzs1YLqSUjI6OMhSjqmZoxWC7VxLLwpVIbapwjs4sXFtbnTy/PUwwVXun3hw67LoPyKWQ4Bfj88/ha8eGOdIXKDttA0sTlpVdbiXPbZK8vEePpvxgSF8YcxVidosJKD5FUmzp71Q0RUbVVuyl5I+lSRfZdTiZKV7PIQRkZtxer0UEf3PSwEu6NO5gnB8//DC7d+0q6JwK+m/oz791lu+e1kl/NYnK5hYngVh6EWn98Yhgpw9e3KVxNiCYTJZ+r6Qn+0iPXMTgqMe2ft/Kb1iAQ5OCl3bD1atX55GVhbb1K+HWW2/lu9/9blGvffOb38yb3/xmMpkM3//+9xkbG5v3vZRyrHLgcrmw2WyMjY3lRb21QG5NmJqaKnt9uO2228SpU6c2P1v6BN3QrCyNvcAis6JSsBZ6lUgkQiKR4EyNF22rQbLBJTkfEDXXi6jxEF6TyqmQmb3dPswVkIJyYBCSN2zQ0z9fuFo4sjPgjzPgj3Ogp/zy2aU0LE9e97OvQB8ho5C8Yb3Ga9ZrBFLwT6cNfHfQMK+U/dxouGgBM8Cl8SjGImYNKeHUUJCj/X5+eHaMb58aIRjPEEpk+PapEX58bpxjAwHODBc3xzosRi6MrZwCymFvt5drU7MuyWlN8LU+Ax85Y2AipnL06FFOnTpVUFD6b49e5bMz7rTVIiqKgIO9Pi6NRZiILI4kaVLwuasGFPR0kCijhWX0hC5Gdey8D6GUkFpQM2QuPEbn4I8IBfwEAoGaV8iYTKZ8aqQSkW+pyGlzgsEgyeTqedoshNPpxGKxVKRbmbM23VyVk3qK40ZkZWlURFbWSq+Su/kXOmOuNrZ4JEal9qXSoAtr0xp8r18lng3gtpnY0+Lk8ngUf7w0s6ly8IIOjbY6+NK15X1IMqrkUJ+f7e0uhgOJfHVJKVgqwnKsPzAvReMySd66WaXHqaflvnRNWbLr9GQkhUkRZIoQ/UZTWXa0uzg9vDJxeP9XT3NlYrZ+/fd36Fqdv3/seP6xHe3FVRhtbnZydKC4FJDdbODaROG6+asRwV+fUHjjBg24Tigc4eCB/XkfnC/NcaetFlFpclrw1JlWbHUwWmI6aC60TIrY2UdAKDh2Fd/jzqgIUuEAIw/9HSPAmR828bGPfYwPf/jD3HvvvdxxR+30my0tLUxMTDA9PV3Tnj2NjY1cv36dqampJaNt1YYQgvr6+op0K3PWpr3AI9U8v6cibkRWlsbehoYGLRcOLRV+vz4x1VqvMj09TUaDgRI9TirFdq9E1eB8jUul6y26qPfI5KyoN5TI8OR1P5FUhr3dXnrqS+/BUSx8Fsk9bZLLIcEvx4v77GeGwwgB2wuYsxWDQhEWTcL5sQibmx102iXv26HS5YCv9yl8/urSRAVgJJRkTwldmS3G4iZWexHl1cUeK1PCbntbuxv/MkQwrQk+dUnhe4O6e+sjjz5GOBzm4fPj/NGMO221iMquTt075dJ4cT/IH48IBqPwwk4Nu7H46Er8ws/RUjFs6/ZidK280O/scPOR1+3h//z2b/Lwww8zNjZGLBbj4YcfZnJykl27dvGa17yGkydPFn0OlaIaXYnLQS4NU+toUkNDA6qqLml8txK2bt2K2WyWzGysn+m4QVYKYEZcu3///v1KuSKunOGPx+Op3omtACklE5OTXAvX1rVWQbLVK7kSFiRr3DDxlmZ9Efv52OJbOadr6ZuOs63NVfQuvhS8qFPDqOikoBRhrz+W4cxImIO9PsrJWhUiLOmshpsYv7NdxWyAfz+v8OhocX43Z4ZD+OqKcw6+PBGlGOmKZQW9CoBSxIFsZkNRVUAAbR4rx/pXbtYoEXx/yMB/XVSIxOI88uhj/NO3jqBqsipExWJUZqp9QkRK8E7RpOChfgWrAe7rKJ6gRU/OpIB2Ly2sFQJesK2ZL//mLTz0rlt52a42PvjnH+Duu++mubmZuro6tm/fzitf+UpMJhO33HJLyXqVSmCz2XC73YyNjdW0lNhsNuN2uytKyZSD3Now11+mFJhMJnbv3i2EEKULlJ6GuEFWCqNb0zRPJXqVYDCIyWQqq7NmuYhEImQzGS6Ha0sYel1gN9Y+BaQIycFGyUBUNwZbDmdHwpweDtPp0/1aKhG75tBWJ9nbIDk6JVYcfyk8ed1PT4OdNk/puqaFhOW2ZpXX9maIZASfuWYrKRUYT6usK2C4VgihRIYtLUX0KSqC6BfqqbQQW5qdZIoolwZodFjIliB5OOlX+Hx/HeGUxps3ZnlOo1YxUempr6PJZSm7U/blsML5oOC2ZomviJ5H6akBUsPnMTgbsK1bvG7VmQ28+bk9PPr7d/Lvb9rH/h7fspU0jY2NmEwmnv/85/PTn/60rM9QLlpaWojH40QitXPxBT3KEY1Ga6pbcblcCCHKjqyAngqSUm4QQtSulGmNcIOsFEbF4tpgMIjH46lpP6DczuBKjcnKdq++OtSarGz3Slzm4nsQAQz6ExzuC1BnNnKw14evzlT2+C/p0pASvjNQ2c/o6mSMYFxPWZWKuYTlgV6JPwn/dFohLk0lu9we6Q8UdIgtBLtlZblbMcZ3pVT4rIRtba6iyqrnot5uZjgm+PAZA7EMvHa9hrsConKgx8twMMFgkeXgS+Fb/QpGRU8HrYToie8B4Nj5/HnC2la3lT964RYe/8N7+MDLttFdX9x3K4Sgo6OD9vZ2urq6yvsAZWKtU0G1jK4YDAZcLlfZtvswb43aU41zeirjBlkpjIrISiKRIJVK1bRuH9ZKryLZ4dW9O4ptclct3NIkSalwtAwTuFldS5Z93V66S2wQuN6pa2V+MSGYrsLnjqdVjvYH2Nvtpa6EHjxLYcAfp81TV3JlVLGlyVcnYyu+JpFemYhEU8sb5JmNSlFVQAZFEE6WJlh2WIw4rUbGC1XnlHQkcFmN7Oxwc6gvUHQUaDkMxwVHpwT7GiRtdUsfT2bTs8Lanbqwdke7m39+7W5++n/u4h13rMddBiEfGRlBVVVe/vKXl/0ZyoHb7cZqtT5rdCsej0ePiGeLN4qciwUi22c0blQDFcbN9fX1WkdHR1mrRi6s92zQqzRZodEGPxyqLVHxmCVbPJJDk4JUBTqZjCrzRmPb2vSw7MrltJKXdqukVPjhUHX5/tH+AG0eK60mQ1GEYKtH400bNSaT8MiI4NXrZL5K6OJ46S63l8aj3Nzl4dhAcNnX+WNptrQ4uDC2NDMuhjwEVqjWuqnFWVS05OYuT0lpF5NB0OG1cWEskteoOEzw+auCF3VK3rpZ42PnBNeK8Aza0uJkKpri1FCIoY/9Omp4ouDrFLuHznf/T9Hn+K3zIXY918492kk+9Pd/gcHuo27jc3Df9noMVgcAsQs/R0tGsa3fz4ues53feN469vd450V0M5kMV69exWQysX79+vzjZ8+epbW1FZ9vfjl9f38/73nPe/jVX/1V9u/fTzqdLtg1fDWQKyXu6+sjmUzWzPZhrXQruQ1tOBxe9D0Ug23btmE2m2U6nX7Gly/fICsL8HQV187qVWobLNvu05fBWlv77/BJFAGHJ6tHks6O6Dv4Ll8dTU4LJ4eCBXfJ272SXid8f0gQyVSfpI0EkxgVsYLZG7TYJL+2SSOUhn89ZyCUFiRVbV5Z88mhEPu6vSU5vw4GEliMglR2eYrjsi6/gE3HliciZqNCKLH8jrKYSI/bZuJCkQJc0KU029rcnBgMFhTTXotIfne7yls3q/zDacOSEUNFwP6exd+RsNhx7Vvcm0eYio/eZQKjnPqfP+CHkVfzohe9iH2veBunD/+CyNFvkrh+jJY3/i0Gm4v4qR8A8JE//wN+4w2FdZbDw8PcdNNNdHd309fXl3/8y1/+Mn/913/NXXfdRW9vL06nk6tXr/Kd73yHZDJJOBxGURQmJiZqVtIL5MnK+Pg43d3dNRu3oaGBq1evrkiSpqMpTg4FOTEY4u23r8NRREp0KeTWiGAwWBZZMZlM7Nq1Sxw5cuQZL7K9QVYWo03TNN+ePeWnANdCXJtTlF+vrS6N7V6NcHptSqVjWYra+ZaKnIGbt87EpmYnl8YjczxRJC/u0ohm4Ccjq0fQsprkyet+trW5GA0mFpXi1hklb9uiIoD/vGDI9/cp5MNypH8ll9v5mIykinp93/TykZ9EWsVhNhBdIh1UbzczGlpa0GgyCC6NrXxDb252cqivuM8GsK/by+G+wJJVP1NJwX9dNPCurSq/sVnlw2cMixqCNrssuK2FvVMUix3PbW8o+nwKwf/Df0OLB/nBmIV7VHjty1/A5IYX43/4P4kceYjk459j34veQP/gWTo6OnjLa19V8hh33XUXFy9e5Pjx4zz++OPEYjE8Hg+33XYbb3rTm3jd615X0FV2tdHQ0IDBYGBsbKymZMXn83H16lUCgQCtrXoT0kRa5exIiBODQU4MBjk5FJynR7p1fT0H15XvUJ4T2VaiW9mzZw+HDx/eKISwSylXDsc+TXGDrCzGZoCbbrqp7AMEg0HcbndNxbW51NNQrHZj2o16hOHJCVHTfjw5t9wT06vrlhuI67oWs1FhX7eX8UiSOjVGWx18e0CpKP1ULM6OhPHWmdje7uLMjBGbIiRv2aTRYIX/uqgwsqASqRBhybncFtNHCODEYJBmp6WgniOHiUiKDY12riyTrvLazUTThcWmLptpWbKypcXF6RVSct31dRwugajkSNhK5cnXIoIvX1d43XqNN27Q+NSl2dL03Z0erkxEGC+yp1GpyATHSPYdx+BuRm55Pr8cl9zVJumwS7pe8XZ+eeqHTJ/4MWdvewt//PVTfOgVO5Y9Xk9PT8FS4DvuuGNF07e5rrKKUpvoqcFgoLGxkcnJybIN08qB06VbG/zy/ABHn5ji5GCQC2ORZYXip4ZCFZEVg8GA0+msiKzMrFUC2ADUzhinxrghsF2MTQCbNm0q683JZJJUKlXTFBDoBMlut/PEH9/HZ379AL/3/E3cvaUJn331cs1bPHoqptZVQDm33NM1Gjed1TjSH2DQn+AlvQaykqIN4KqBQDzDmWHdk8WowCu79YaE3xtUOOkv/BMu5MNydMblthikshpt3pXTFj7H8k0+Xbal90N15uUXIUsRvv52s7FoPc6+bm9RRCWHJyYUHhsV7KqXvKBDw2rSvVNODAaJLuedomaInn2E0ONfInzkIZL9p5Ba8VVPyX59vbH17EEIhZ+PK0gJL+s1MBCRmNq2IDMpUiMX+Nqx4ZKFxaWgpaWFbDZbc+FpS0sLqqquqoZkLJTk+2fG+OvvXeB1//EEt/zdz4ll4eLAOJ9/coCzI+EVK9pOFdkuYjlUKrKds1aVt2g9TXAjsrIYmwA2btxY1pvDYX33W8tKIE3TCIfDtLS04Kkzc8emRu7YpLtYSikZCiT0EOZMKPP0cIhUKWYUS6Dbof+QVyMVsxxybrkXauyW22iVdNmyPDEhaHDbuclh4eRgsCib+mrgyet+7u02c3trnJPTgh+sIGouFGE5PxZZURibf/9AkC0tTi4sk4oZ9MeXPYbVtDQhMS2zUzcogksTy6eAiu0ADXqFzLGBpVM/S+EbfQotNo37OyRBzcATRUSm1FiA6W//w7zHjO5m6l/0u1i7lo+CAGT9wwDYGjvypdCnAzG2etI4TQb8vjaSfcfJBkaIp3fz5SNDvPW23hWPWw5aWlo4deoUY2NjNbXAz1XnBAIBmpubKz5eNJXl1FAwPw+eHAwV7Aw+FBV02Iv/PZ8eClZ8brm1IhKJ4PWWbl8wpwHjDbLyLMMmn8+n+Xy+sqJO0ai+CDgcjqqe1EpjqqpakCAJIej01dHpq+Olu9oAyKgaF8cinByaJTCXJ6KUahrZaZdMJcnb3NcCa+mWe9uMW+5jowoj8Tj903F8dhMbm5xcHI8QLKPXTymoM0pub4gTSsO5tBfJyqLSxYQFhgNJun02+ovwAkmvQGpHQ0l6G+xcnyqcCjKUmTrY3Ozk3DKiWZNBFGwIWAgbmxxcGg/jNZfuTKshOJPy0qn6ubc5wbGxxfqVuXDsuBdL5zbMDV0Is41scIzIsW8TPfEDJr78AVre9HeYm9YtO6ZZ078Xk83OoRly9Jgm2OmT3NIkGbDofilaUp9r/vvxPn791p5VSTvPdZXdvn17zVLbdrsdo9FYlmFabn7LE5Oh4ue3wRhs9ui9tcJFiOf7puPEUtmifIeWQm6tiMViZZGV7u5ujEajzGazN8jKswlCiM1btmwpOz2WIyt2e3EGTNVAqdVHJoPC9nY329vdvOGgLmDL7TxODobyBKbQziMHgaTdDudqHN3odepuubVKAeVgFJL9jZJrYeZpRPyxWV3L/h4vY6Ekg4HKDMGWwq/0aLjN8PHzCueDYW7u8nBhNEw8szyhKERY4hkDTU7Ligv+takY+3u8y5YFNzktS5KV5WzTl+v3s1JfoZu7vEUJhju9NsYjSRyG0p1p3TYT3b46fnotRDys8KaNGi/p0vha39Ln5rnt9fP+39zYQ/0L3o1ishE+/HWCP/88Ta/6k4LvXddgx2s386OZ7zOZ0cg5pFwJC8bi8JwmjS/lrukMceibjnN8MMjNXaUvdMWgpaWFixcvEolEcLmq37KiEIQQeDyeFbUccyPHOXJyZiREcoXfxFIYjAlA0umQnC1yjrk2GWNHCZ3LFyJHVnJrR6kwGo2sX7+eixcv3iArzxYIIUxAb7l6FdDZscViwWQq3xm1VOR+0JWknhwWI89d38Bz1882XhwLJfMK+BMDQU4NBYnNVHY028Bi0MOmtcR2nz4JFTuRVAu76iV2E/yyv/Ail85q+QV9R7sbTcp8KXQ1sMOrsa9R8sSE4PzMQntsIEir20qb2bCsyBUKEZYkbmcdTquRSHL5XPmVieiyVT0joaXJ2XKRmfgShnBCwNWJpT9Pvd3MqSLC7w0OM+mshknLlExUtrY6GQ+n8pqEI1OC3fWCO1olJ/2SqyW6RDv2vJDw4a+TGjy76LmdHW4yqsb50QhMxRAzkROZmnsNBI9PKLyyR2Nbp49fPqFXHeXwrZMjq05WxsbGakZWQN98TU1NzSslDsbTnBwKcWIgmI8Mr1QiXwoGZ+azTrvkbJHV/lcmIxWRFZvNhqIoxGLlF/Js3rxZXL58eUvZB3ga4AZZmY9ewFAJWYlGozVNAYFOVurq6qpu3NTitnK/u4X7t+sW2KomuTYZ5fhgkKHBQWCy7J445WK7VzISWxu33HgWThThlpurYOmpr6PBYeHEYKCkfjULYTNIXr1OI5jSNRRzMRrSPVkO9Po4tEKkYTFhidPqc5LKxEgv47oaiGeWLWUe9Cfo9NoKRpRiy7jYhhKF02abmp1cXEYn09Ng5+gKvjEOixG72Ug0FiuJqBgUwd5ub4FrKfjSNYV1LpXXr1f565MGMsukgxYdt84DgJbRo5VWk8KOdg+jwQSnFuhujD6903vGPzLv8cOTgpd2wR3buvnll8Hobcs/951To/zJi7cW7UBcCua6ylYyN5aKOofef+orj1/kyLjGyaHQkhG8amE6BfEsdJYQGF+OWBcDIQR2u73syAroIltN0zxCiHopZW3V0DXCDbIyHxVVAqmqSjwer6kQTUpJJBKpyZgGRbCx2cnGZienLUGuXZvki+++h8tTMwLeoRAnBgMV90VZCk1WSZONFYWl1YbPItnolvxsTJS0QPVNx+mbjuOzm9nY5ODCWHhFE7RCuLddw2WG/7igkCig08lqkkN5T5Yk/mVcYRcTlghbWj2cXsHl9mh/gHaPjeFg4e+2zZMjKxK3Wd+Zdtol69xhfqVVwyTAO1M49Ce7s2Q08KdU+p2CoZgefs8Z7LmtS09LG5ocKxKVnDvtZCBcElFpcVlx2oxLkr5wRvCNPoU3bNB4XovkJyPF3wup4fMAWH2tHOz1cX4svGTJtbVrJwCJvuNIqSGEft6xrODklMre7ZtwenxY2mY30hORFIeu+7llfflltEuhFq6ymia5Ph2bFzGZ8If4w13ws7MD/KDKTtFLQzAcg9ZlWhwsxJWJyk2mHA4Hk5OTSCnL0gUtqAh6vOITegriBlmZj4rISi6MV8vISiqVIpvNrlk0x+2wsc9hY1/PrPviXIfHnMCtGuLTrV59AjlbY7fc7TPjHp8qb1x/LM2T1/1YZnQto6EkQ0XqWtxmye2tksuhlVNfZ0fCeOpMbG9zcWaZFNRiwhJkb7dvWZfbrCapd5gLkhWTIvHKCL+xWaXboTeXzCGelUwmIKYJHCb9OgbTUGcSbHDJvANy7vH+iOByLIxByIJtI1aax4WAba1uBicDvHubikiG+cD/HuLq6SNkJvtRo9OgGDE39mDfcS+OnfcihKJ7p4xHltVpDX3s1xmITPLcf/xH7m5o4Ev/9zfzv3nF7qH5NX+JweHDYJvfkTobmiDyyH8AULf1znyESqpZssFRUIyYvK2z19PbirVnD8m+40SOfQfX3pfmn/vZY4+y91fv4ZaXvoGL5vmk4TunR1aFrMCsq+zExERVmhtORlJ5bVyOnIQXpCMFkkSWkqpzqoGJpGC9S2IUkmwRPk5XJ6tDVkZHR0mlUmWRwRtk5dmHjQAbNmwo6825iauW4tq1qD6SUhIKhWhqair4fL3Dwt1bmrl7S3P+9f3T8XkukGdHwitWmixEt0OSVmFwjdxyK3UHTs3RtezscJNV5bIVLwD3d2iYFfjWgAGKMN4LxjME4xkO9Hg5NrB0+mkxYfFzsLd+WdHqqaHQPHO6RqvkjlaNfQ0SmzFBSoW+iODQpJ77H4wJplPQ4LAwFU3z+zv0xeij54z01NfRPx2jwaovRp0OSaddJ6S76lPc36qnPR4bVQjMuPPu7fJydGD5qMq+bi/XRv35iMqffeFxzn7jYxgcPixdOzC6GlFjQeKXHif1/Y+Q7jvKfe/5G46u0AspD3MdXzs6yPse6OV17/ljvvyz04BupR+/+HNCT3wFa9dOjJ5mFLMNe2qK8bOPo2XS2Nbtw7H/lflDqdFpRj7xTgyuJjre+V/zhvHd91uM/c8fEPjxv5PsP4mpvoPUyCXGJy6RecXtHLztDi5em39qPz43wV+8vLyd+UrIWcEHAoGSyUo8neXMcJgTgwFODupusEtF6OZCIhiKCTprTFYmEwJFSBqsMFbEnqJvOkZW1TAayt9E5daMWCxWDbLyjMQNsjIf3U1NTZrNZnvalC2vRTQnVypdbPWREIKeBjs9DXZesUfPx6ezGhfGwvzDDy8RTWUYD6dWjDZ0OiTDcb2ctFbIueUemxZVHTenU+htqMNnN3NyMLiIWDRZJQebJCenBf0lCpkP9QVY12AnldWWXBgWE5Zp9vf4lq38CcUztNgkL+rU8v2ZLoUET04IzK56fnlt8Xs9NhNT0fmpKafVhEQwmYTJpOD4TJb91nUe1Kif5zRp3NUmub1V5fiU4OFRE/3+5bUBB3t9XBmZnpf6mbR20Pgrf4pt/f58OgVAvf3XmPzc+4he+AU//dF3sG++ddlj56BY7Ay038WVsMp9+7fypHFHvsQ1OXCajH+YzMQ11LGLZFMJohY75rat2LffhX3b3UUTCZO3ldZf+yeCP/scyetHSVw9gsHhpW7b87kUUthab8BwfX70aSyc5OxImO3t1fd4MplM2O32FatzVE1yeSKST+ccHwhyaTxCuVZEQzHY6AaHURKtkUXCxExwrckmGUusPGZGlQz446xrLH8OnlsRlPOYKQUtLS25hoaVh72eorhBVuajrdxOy7A2ZctPh1LpQjAbFXob7Dx2aTL/mMNipLu+DofFSCKjMuRP5PUXNoOk0Qrna1wqfZNHYlBWz6X3+lSc61Nx6u1mNjQ5ODcazlfnvKhTp0ffGSzvlrw2FcNmMizbyHAhYfnXc352dXgKmq0JJBttMV6yWUNKPfLx6Ois3f8GpXCqr5AHxVLutNemk4yGFJ6YUOiyS+5q07i5QbKrPsPX+7JMRQWFIkz7ur2LiMq5oIKte1fBcW7Zvp6Hd91P6rHPkho4XTRZ0SH4Vr+B9+5Qub9D40vX9VLmtptuZuMddxStTTK6m+l+/7eXft7VSMOLf3fR42fDGtsaNNa7JJdC86/Fj86NrwpZAf33Pjo6mrfel1IymqsYHAxyfDDImeEQ8WVE1aVibilxrX77kzMEpbGEAMeViWjVyEo5mNEVyYGBgbaVX/30xA2yMgeKorS3tZX/XScSiXwZWq0QjUYxmUw1a+EO1SmVBji3QFcRTWUXlfs2OS20e2x01GWBIOOJ2utVsjVwy52OpZm+7p+xdPcRj8XYVR/jyJRgvIjd3VJIZFSO9Ae4ucvDxbFIweqcuYTlXVtV/v1ieJFzbZNV8voNKr1Ovf/U1wdNXAnMDwVdmYgW9G4xFyAmSoGqld6GOq5PzTriDsQEn7ls4ETIyItak7x6nWSXT/CFq7OpIYCd7W76xvxFiWndNhNdPhuH+vxoYsYvRSmh98yMlf7J8CQnnbdxoLOBUxEbDrudk4PBoptFVoIzAcGr0e/NSws45Y/Pj/Pe569OJsBqd6Jpw3ziJ+c4PKKTlMkijfnKxWwpMZwPrupQeUynQJV6ZKVYXF3BOmAlmM1mDAYDiUT5xQnt7e3K4ODgDbLyTIcQwgjU57ptloPVUsovh1ypdK2bJtpsNiyW5fvCrITlRKA5TERSTERSeNs08Om6kVxJMMBUNEX/dLzo/jClQEFyk1f31KiVW24yo3G4z88ru1UUAf0ZB1B5ddWxgSAtbiutHlvB6oW5hOUdmzN86nJ8xuU2zp2teqdpBfhWv8JPRgR7ezwQWLww9zTULSIrhe5NrUBeoMlpnUdWclAsDv72VIYXdmjc0y75w90qX+/Toy+bmh1MBEK8qwiisrXVxVg4yenhMFJTiZ35CQC23psLvr4Q5lrpfzV4gl0f/CCeU1/g0eymoqz0q4FQWjAY1cnK1/okcyNNZ0fCBONpPHWVbV5yaVpdBKtX+YlUhPdsg4dPXufJydpsGiaTkMzqKeBaQZUCf1LXZBWL4eDybSdWghACq9VKMrm0wHsltLW1IYS4QVaeBWgGRKVkpZx8Y7mQUhKLxWhvb6/pmMFgcElxbSk4O1K8lXanXZLWYDQGWkwvCc7BalLoqbfjspnIZFVGQsmqdMTtda2NW65Z0bUqV8Lw8/5E3tn0xECAZexQVsRYKIlBEUt6pswlLG/ZmOILfYJf2yy42acyEIXPXzEwOhPlOdrvp6e+bt73ABRMf6gFTjqVXRzhGSmgrdnS4uT4YBAQfHvQwCm/HuF53XqNdR4jvxiP8Y4t2WWJSiHvlOCjnyYz1Y9t3T5s6/Yuek8hOHbci6NnO3t27sCfNnD9+jUGx6e5787b+Mo7303TG/5mRSv9auFMQPDCTkmrDUYXXLZjA4G8uL0YSKlrLnIC+BODhQXwVsNMlMMheXKy0JGqD4lgKE7NRbYTSUFXCWOOBssnGTlUSlZaW1vRNM0lhLBJKVfHP2INcYOszKINdHZaDlRVJZPJ1DSykkgkkFLWVFwbj8eX7ENUKs4OF+/w2mnXzeAKiVyTGW1Rsz1fnYkOXx02k4FoKkv/VGxJB9alsM2zNm65e+olNiP8fExfeK9NxWAqRoPDzPpGB+dGQkSW6/q7DFRN8uR1fz7K4F/g/jkvwrIxic0IpwIKn74o5ok5NVm4a/Kl8Qi+OvM8r5dEZvG5LnTN7fTZFvnzCMGihpsDMcE/nDbwjq2Sg/VpdnnAKJYmKi1uKw7LfO+U8JFvEj78dYy+Dupf8r5F7ykEn93EwTe9m4vjEU77M4CGubGHJ2Iav7pO4+Zd2zmzjJV+tXEmoPDCTpWtXpknkDkc6VuerPhj6XzZ8IkSrAWSqmAyUXviMBTVy9xtBlnQZ2g1MJmEbV6wKJJUEd5KI6HKyYrFYllRwLwc5my0W4Fry7z0aYkbZGUWrTDvCy8JqZS+k68lWcmx8FoTJNAtoitBMqNypUh/AqtB0miDC2PFT1T+eAZ/fH7kpsNro9lpRVEgEE/TNxVb1lm2yyEJpNbALbdZI5qBU/75405F00xF/dhMhnw33uEyd3TnRsO4bSZ2tLs4vYA0npgW3NYCG1y6m+exsB2DklwUITk3GmF3p4cTg8H8Y1LC+iY7/r5ZshJOLl4IAwuM69rci8nKvu7CPYmsJhM/nzTQVRfDaoDT/sI9qvZ0ebg0FmFszkISOfZtAg//B6b6Lppf+5eLPFEWoru+jsaZ7tqFolFHpwQv75bcd999HP2Hjyx7rGpiOAYpdbbz+VzMFVMnMypnR0IzqRxdCDuwQqfs5TAYE2z3SRRkzarydH2SxGOGCiQdJSE0M6bbPFsdtBxGl2k5USysVmt+01tOu5YbZOXZg4rIyloQh6fzmAP+OGqR9Yw5U6jBCvsQDQUS88qjTQbBxiY9xZJVNcbDyfziL9B9Py6V2AOmUvgskl4nPDoqCpqigR6pONQXQAjY1eEmlV0cWSoGoUSG08MZDvT4OD4QIKNJQPKqHo0NLrgU0gnLC5simI1eDg1EFmmDxsJJTIqYea+O2IKoz8LojckgFqWLFupcHBYjlwtoa0wGwZZGMy9qCmMQcC0MO3y6H833h/RIj81kYFuba1EFVPjwQwR+8p+YGrp1omL3LHlttre7QOq6qv7ppRf3hCo4Mamx9+abqaug826p0D1IFhumdfpsKAL+5BunOTEY5MJohGy5dcMFMBgT3NwgaamDkcpkGkUjNHP7uMyLo0i1GHMiWZy/USKtYisQbSwWuTk1lUpVg6w843CDrMyiFcpPAz2dicNajDkUKH6my/Xp0MsYq4eMKhctiC6rkS5fHS11YDX6mUqVP/mUg5xb7qnplQWMUpIvMV7faMdtM3FyMFiyruVQn5/ehjoyqqRJiXF7q+TEtODTlxR210se3Khxjy+A2eDjZ9fnR2HGQslFGpiL4xHcNmOekMTTKnazAdD/v8FhYXROtKPVbV3U82Vrq4tDC+zohYCDHQ7uqQ/kNSoXQ4J3bNF4YaekL6KRNjlJZbVFRCX0xFcIPvZpTE3raH7NX2CoW5zGNBkEuzo8TESSeeO7YnDk4gAHWnrZf+udNd3OTqaMrHdled46D9MJlYHpOIP+BIP+BE9cW52qpJwhY6ddzus+vpoIz1R+uWpX8Eh4hqy4SxhzIpKku758C4ncnJpMJstK7c9Zu56RZKW2daBPbbQJIWhuLl6YNhe5RbzSCplyxqwlWalWumu4SLt50HePGa04N8lKEU5mOTMSxh8IAnA5qNHitrKny8OBXh+bm52YDas3SZfrlnt1MsaxgSA+h4WDvT4cltJI1vWpOKlkitdukEwl4XNXFCSC49MKn72s0GiF57n93LF+8SJ/ajhEvX12Vlc1ycam+ekV75znXdb5u8ZOX928/2/32jjav3ixvXOdax5RORdUUKXgM5cVIml4cDNMhKKLTPCCv/gCwcc+jbllgx5RWUBUPHWmmWtm5PC1Sa5evkQmMDrvNenJftTE4i8lG5rgl1/4ZzKZDM+5455Fz1cLNpOBLS1ODvb62N3hoclp4XJQz2FOTPs5NxopWZNVDoZmNgztNdSthGeyiO7aNbLPG/25ShhzYXSwVOTWjnJFtnMiK8/IiqAbkZVZtPh8Ps1oXMKtagWsVZRDCFFTj5VkMomiKGWFKediqAi77Ry8Fok/BVoRfTqqhY4ZLYDeYC85T/dgUATrGuz4HGakhMlIkoEqNG+shlvuZCTFZCSV17UMBhLzohhLQ/KyrgwWRfJIxIXZmCI9s/jNFd3eyjRynY+fXpuNPCTSKltbXUzPSfcsFMa65jQnXCjM9UfnT/K+OtMiMnv3BjfPc00XrPoxmc08ETLw/MYoL+kUfPHa7PGjpx8m9PPPgVCwdGwjcvSb+efcNhN2s5FRxUVw273A0hb4haz0s4FREteOILNpzlzpZ+fG9fxvoHBPo1KgCF0r0+CwICVMRVL0++OLUn0iO+tBcrH4wrqKkFAF0cxsU8paYG5KplYIlzFmpZ4zcyMr5aC+vh6j0Ug2m22p6ESeorhBVmbh8nq9Zc8yuYhDrSMrFoulph4rOS+ZSscstpEf6KHYwOp6Ty1Cpx2CKfKdgOdC1STXpmJ6lc4M6swGeurrcFpNJDMqQ4HEvMW7GOTdcv2Vf5/zdC2dHlKZLBfGlhY0722Q7PRJfjoq+F5fnBaXlVavjcvj+nvmEpbb8JPp8vH4wCxhOdofYEOTI+/hcmEsjMNsyO/2baZZAmGasx9oclq4MsdQa0e7m9PD81feO9e5liQquU7T376SxCcEtzRLTvg1Lsy8Jhsa018oNSJHHpp33Nwols7t2GbIylKwdu0k4x8mPX6N1MgFZCaJYrFjadet9C+ZN7LHKAu6yq6EZpeFNo8Ns0EhlEhzfSqWdzZeDhMJXWTbUUMPEtAjHe4aEoe0JkhmS4tyVIqECmmtxDTQMk0wi8FczUo5UBQFh8OhBYNBV0Un8hTFDbIyC6fL5Sp7lcgpuGtJHMrt0FkJcgSpUhSfBpK4TJTcG6cySDrsuhlcsYinVc6Nzt/5NjostHmtWI0GwskM/VMx4pmly4+2eiSqBudLXOyWg5RwcqZaJ6drOTEYnNerxW6U/EqPxlQSvjWgL/Jj4SST0fmeLHMJy90+PynVzbHhWaIx96wzqmRHu5NjMw0CDXObvM0Zu6feng+fGxSxqEro1m4Hd3j8i4iKccY7Za5W5ivXFTa6VF63TuOvTghSmsBz2xvw3PaGvB5lMpKkf5ko2FIW+NauHcuavp0NSkBlm2exq+xcOGdaStgtRhJplaFAnPFwqixfIIlgOFb7UuJwWtBcgrtrNRDK1DayAoJwujSCVOrmZCFy0epMpvwO9U6nk2AwuHyJ29MUN8jKDBRFcTmd5X/H2WwWo7G2lzOZTOa7odZyzEquUw6FDMAKwWoAs2E2LFsLNFihzli5oHcymmJyTopDCOjy1dHktCCEXorcPx3LE4eumUaNqVXykshZgjc5LfQ22Dk7EiKaUrmjVcNugs9cVkjP8ZSY9WRxMhFJMRVNzyMsL2oKk0zbOTep7ygvT0TZ2+3l6Iy4dW61l5Sz/86os4QtnJz9Yhcat+1rt/H8+iDOBUSl1W3FbjYsKiWOZwVf61N48yaN5zRJHhsTuG0mtrQ4uTweWbI/UjUQSgumkvp3mIPJIOiut+OrM6NqkvFwkqFgoijn5mIxGBPc4ZLUGSXxGjX6C6dho0uvmJM1Kl8Op2ubesqNWQpBiqZW7ge1HIQQGAwGstnyjzOz4b5BVp7hcFZirlZrsiKlJJ1OV6wdKQWqqpLNZiuO5miaLHoXktvZhNK1i6w0WPQJarzKpZlS6iXbc30uLEbdfbfebqDZNsXxwOrfQ7kWBjazgVvWebm9fpKBKFxcIqJzbjSC22ZiZ4ebU0OheYTlVzpiJLNWrgX03WD/dAyrUSGZ1Tg/FsFmUkhktHluqImMPhn77OZ8asptM3FujqPx9mYLL22J4DDOJyo3d3m4MBpmNFQ4QnViWu/kfG+HJGH1cHwwVJN+PQBTaSPrHVkO9niYjutGhIVaG1QTet8oSb1F98SpBUIZMCg6oY/VaMxwRsx4ysxvL7CaiGUFjSVEkGIVkhUgpzkp+/1Op1MIIZ6RZOVGNdAMpJSOp1NkRdP0ybqWY1ZLRBxMZIr2WMntbMLlR0ZLRq5EMlxAr1JtpLIaF8cjjE3q+pJLAQ1PnYkd7W4O9vrY3u7GaV2d7ziRVhHRaWxGuJZysLl56VR3KJHh1FCIAz1eTMpslZDPInljb4pWh65JmYqm2dXpAfT+Mlta9GPObaAYSuhf5vrG2TLPTc0OojP+LBu8Rl7dEZ9HVGxmA3u7PRwbCC6fSmtzcyleh8skSYf9ZCrpT7AMPHU6eTvY62N7mwunxcDlgIZJgb7xAFcmovO8Z1YLayM+1X8Xpeg5Kh9Tj7Baa+gkkFLBUsIKWWlkBapCVhBC3NCsPFMhhFCAukojK7XUj+Ru6LUgK5VqVqajxefncxNiLdNAs9Gc2o2Z0x0MxgTBeIbgAvfddo+NZpcFo0EhENPdd6uxGB5s0oik4aHLCTSZZEOTA6fVyMkFupYcDvUF6KmvQ9Ukx6f1VF6u+eFHzylMJeD4YJAWl4WxOTqM4BwtylRU/3duJ9rbUJd3qu1yKvza+hR1BpknKusb7SQzKkf7gwU/g8kg2NnhYTqa4uxImCuKZN8+ONAkOVGFoIrFqNDTYMdjM5FWNUaDCcbCqUXf0eCMfKfTLivqlF0K8h4ktSzrnUOQauW1knOUdZkgufoV2oBOVswGinbrjZbZAmMuKiUrM2tY7fqv1BA3yIqOOkA8nSIra0FWquWxMlkCWVmLNJB7DaI5nQ5JVoPRJVJPurX+fPfdDU0OvHUmNI28HqIUNNskPU54ZETky8JzaYu8rmU4tMi/o286jtWksK/bm9eBPLhR4z3b4B9PCkIZjRa3jbFwigvjEUwGkXexNSiCjCpx20z5UlyLUd8ut9gFb9+SxabMEpWDvT6O9vsLtkWY1aNE8zoZgJQmODEt2N8ocZlkSRGynK6o0WEBAdMzuqKLRTgE5zROnQ7Jkamih6wIoZwHSQ0jK6GZ61lb3xP9b7GOstVAcuaeMxuKI0jRAm0lSoXRaKyomaHT6URKeYOsPIPhBCoSjj4byEquL1ClZGU6WnzIYi2Ig8us5/8zRTQwqxaabZKJBEV7dGRUuUgP4bQY6aqvw2ExksyoDPrj+JdpULdtxi33yNTiWHdO11JnNnCg10f/VIzxOT4SyYzuErun08PliQifvZzhwY0a79tl4O9PSk4MBtnS4uTCWCSvdTEaBMpMtdzGJgdH+gP53kINVsl7tmpYFY1PXlQYTVvY1mYrqDfp8NpodVuX7NcDes+eg02SrV7JExNLX9MGh5kOrw2z0UBkpmKrfzq+rMX+UohnBcE0NFfWNqskRDJ6U8m1cHet5ZihMhxlK0Va1aM5liLJysI2E+WgGmkgKaVJCGGWUtYwNrz6qIpmRQjxgBDiX4QQPxNChIUQUgjxP8u83iKEeJcQ4pAQYkoIERVCnBdCfEQI0b3M+35t5j1RIURICPGoEOIly7z+PUKIq0KIoBDi60KIriVeqsfOykwDaZqGlBKDoXYJ1bUgK+m0fu9XmgaaKiWyYoa0WrvQL6Dvxmv8M3eZIFhh9CiSynJ2JMyT1/2cHArhj2dodlnY0+nhQK+XLS1OrHM8TjrtkrQKI7GljxlPqxy67mcymmJPp4dNzfN/I8cHg9gtRmJGF5+9rOA0qvz+bv0aprIqAjDNlC0bFIGi6J8xlVUxGxXGQgnqLZL37pB5oiLqPAjg7IKqma2tLnZ2uBkKJDjcFyC9jB6lL5ozTJt9TZ3ZwNZWFwd6fezqcNPgMDMVTXNiMMSh637Oj0aW1cMUg1Ba/+y1giYFsRp7kOSjHDX8nDm/I2cNP2duzilWt/JU0KzMWcOqFl0RQnQIIf5LCDEihEgJIfqEEB8WQngLvHadEOLbM+vzJSHEO6p1HtVa6f4E2AVEgSFgy1IvFEIYgYeBW4ELwBeAFLAfeA/woBDiuVLKcwve9/fA+2aO/5+AGXgt8C0hxHuklB9d8Pq3A/8MfA3oAx4AfiCE2COlXBhnqyiyshbEYS3GVFX911spKYsmi/8xuky5ybGWaSCYrmGnZYHEaYbwKriQLvTwUAT0Ntipt5vZ4JlmKq0UVX6qSZ2YgB4VcViMnBgMImfGmIyk2N/TwGcvT/HgRpXf323g70/E2Nfj4/xoBKMCihAIBA6LkfOjYfZ2+7gyMs37dkksQuNTlww4vfXzoiVGRbCr04M/lubcaPElv1mpEEhLtvgU9mleJmZchks5RjkIpwW9zhp7kKRrmwbKaIJ4trZRjhyHNNbQbikXKClW1FstsqKqKlLKsjy75qxhTqBixZYQYj3wS6AJeAh9zT4A/A5wvxDiVinl9Mxr64AfACb0NboH+JgQIiWl/HSl51Ktle696CTiCnAH8Mgyr30lOlF5GLhPSpnfyggh/hz4f8DvA78+5/HnohOVq8B+KWVg5vG/A44Cfy+E+LaUsm/OOG8G/klK+b6Z1/4tMADcBvx4wTnVAdTV1VEOni1kJeeVUanxXSldYK0GSaLyOaAESFzm0nvzVAKHCQyiNiJiTcL1qRhjgShvatM4MSmwmsz0NOjuu+msxkgwsWyfk1zzx2aXhe56XdcSS6s8ed3PlhY33xiM8YrONO/bZeA/LkYAyZYWF0KEEeiVPwP+OIMTAX5vp05Uvj5kJWG0cGaGqLisRm5qdXFlYr4eZSm0uq20uK2YFIVQIsP1qSjXwpKdvizHBvw1a9UQTue+z8pt94sfU9BWY2O4RBZKbD9VEXJThrIGZMViKK5cuhqly7mNoKqqZc3tc9aw8hazxfg3dKLy21LKf8k9KIT4R/R1/y+B35x5+DagA+iSUk7Oed1vAJ+u9ESqstJJKfPkpIiFbN3M3+/MJSozeAidrDQueDx3Mf4yR1Rmxu0TQvwr8KfAW4A/m/MeAzA3eaCiF+kX+okpUH7EILeIK0rtKsGrFeUoBdX6nMWWLYM+OVUYmS8JFgVMiq4FqBVyIfxalErn0DGn+iiRUTm/wH3XZzfT6bVhMRmIpTL0T8UXCW1zURu72cDBXh99UzEujEUYtZnw1jm4oz7K2zZJfhL0Es8q6AoA/fvf1mTlvvokVkXjMb+LoxNJEpkY7V4bbW4rp4aW1qO4rLo2x242Ek9n8/2PFvZAGowK9jZIWmwwUmXPnKWgf4cSpwmCNUolRjPgqLH6UJO1JQ65KcMgamnzr/81FznFZjVZdkQkh9zcOtdEsRTMWQ8qXoyEEOuA+9AzE/+64Ok/A94OvEkI8T4pZYzZtXXujJ2l8JpbMtZCYHt25u8LhRD/vICw5PQnCyMfd8/8/X6B430PnazczXyy8jngn2Y0MAPoEZ0R4OcFjqFA+YtwuTdWJahWlGMtxsxoxbMPRVCwhHa1kHOFz9ZwzLyXTA11Mh0zFidDS7j0+mPpfAVPDl0+G41OK4rQn++biqFK8lEVRcCeLg+RZJZvXI5iNDi51RPhLk+Ab4468vePhQzP90WwKpLHAi6+eTnO1lYnRkXh1HBoXisGk0HQ02DHazOT1TTGwklGgknODK+czplbSly7Elv9r9tcO7KSlaBLkWpnmKZKUGpIHNQ1iKzk5p1ShtSkHiWtFOWuKXPWsGrsnHPr7g8XBhaklBEhxC/Qycxz0DMlPwMmgENCiK8BncCvAu+qwrmsCVn5DrqO5FXAaSHEj4E0sBc9jPQvQF5/IoSwA+1AVEo5uvhwXJ75u2nB4/8CWIF3AvejX8jfmWGAC1EVslJL4pBDLcfMGdFVOqZaglGXQcxOVLVAbqKpJUEyz9x2tRQRO2bEkaUsqAP+xLzu0iaDYF29HW/dLJE4PtMLaHOzgysxI5FklvtbEryyPYaUemnwK1qjWA2SbwzZiBks9DaIfF+lTq+NJpcVgxBMx1L0TcXyzRRLRa7c3V5DUWYudWBSakcc5i6qtbptNaqzKBc93lqQFUofU9UkhgpOMje3PkXIyuaZv5eWeP4yOlnZBDwspYwKIV6ArhV9BzAJvFdK+fEqnEvtyYqUUgohHkBP9/wpsHXO0w8Dn5dSzp223TN/l5If5h73LBwH+NuZ/1ZCRWQlh7WIctQSUsqqpLpK0azokZXaXVclT1bWYsyaDUmuKKiSFFtGlXn9Sg5um4kuXx02s4FEOsvVlJnHQyae6w6jMbug/izgZigJDqtGk9NCndlA/3SMwUCCwRI6ci+HHMl117BqxTRzXWvptGqbmcWNSu1Spm4TZGu4euR+G7WsejLN/C5L0eZoFc7LuSaGWgnR57mIRPLp3GpoVkpee6WUF4AXVGHsRRDVXvSEEHeiC2w/J6V8Y4HnrcBngReiC2kfAuLootuPAN3Ar0opH5p5fRswDAxLKTsKHM+EHplJSSnLMgARQtwH/OADH/gAd9xxR8nvV1WVaDSK1WqtSkfiYpBKpUgmkzgcjprpVuLxOJlMBrfbPe9xKSFcgiFSLJUlkSkujOA1qagSwtnafEYFic+sEcsKElptNEgWRcNplAQzCtkakSS7QcNmkEylFVY7AqAIgVXJYpu5nHENUpqx4ol9JRiQeM36pD+Vrs394zBouihcFcTU2tw/DWb9tzRdZGXXUjAoAkORGy6XQf+9h9VasQeJy5Ct6ZgWoWJRNDJSkNCKY2Yd3joq2bOGQvr6X+68/pu/+ZtcvHgR4J2VRjSEEP8BvA14m5TyEwWe/yvgj4A/klL+dSVjFYO1SAP9IXoe63eklP8+5/HvzURcTqCHkR6aeTzH3uavkLNYif0Vg9qHKZ5BUKVk0L86CkaPETRN72NTCxgEYIasKosmVJVCGAGj3kunCr5SRcFiBgyQSK/OVtxsVDAqCiBRkPO8KqwKpLUZIaKErDa/0WG1YBDgNUMyW7v7x2jSoyqJjCSRrc2Y6ZnO5Im0VtFE1uSy0uQsbrOVW1Q7fdUqOlkeUkrC4TCKotDprs2Y6XSaRCKBw2qmoUatVAwGQ754ohxs3rw5R1YGq3A6K629rgWvW1WsBVnJiWgXlTdLKU8KIfxAtxCiXko5LaWMCSGGgXYhRGsB3crGmb9L5dWKgQZw0003ceedd5b85kgkwk9+8hPWr1/Phg0bKjiN4tHX18fJkye5+eab8XoXefOsCo4fP87g4OCiazQdTfEbH1qoia4O/mR3lmBa8NFztdkZO02SD+1TeXxS8L3B2oy526fxls0aX7mucDFUm934y7pU7mmX/Os5Awm1sshKk9NCm8eGxaiXDfdPx0llVXZ3ehDZFL/SrsvEYpoBkNQpGoqAb47Y0RQjJ4dC1JlMdNfbcVp1992hQKLoztxLod4i+X83q/xwWOHhkdpc1z31Gm/epPH5qwqXw7UZ84Felee1SP7hjKGi9OUfvGA9r76zuPnrkUcewWAwcPvtt5c9XinIZDJ897vfZd26dWzbtq0mY46Pj/PEE0+wc+dO2traajLmuXPnuHz5MrfddltZUfrz58/zzW9+E/RsRKW4OPN3oR40h2qsvUVjLchK7htYWJ6MEMLCLFubO1P9BHgTulD2Uwve9sI5rykXGpSfJ1xLYW0ttStCCKRcXJ5XiaBsJdS66iBfIlmzEWcrj4y1WdsAvX8OSGxGSJSwkXOYDXQ31OGwmEjMkIqcNT+Aw2JgZ4eba1Mxkok4r+tOYDVIfjLt4mZ3HBAcD9m51RvhZS0RPnvNQovLQqfPzunh0LwISKPDQrtXJ0HhZIa+6XhJEZKclqNW0SqY/Q5r5bEC1dM8GUv4HWua9rT0eCoHT6cx56xh1QhV5gIK9wkhlAWeaE506UYCeKIKY62ItSArPwO2A/9XCPELKeVc96kPzJzTYSnlXOOHj6OTlT8WQnxjjilcD3pZVIrFJKYUVERWcqg1cag15hKkueMbDau3ytbazyFfIllD4rAWVuK5hontdRL/Em69BgE9DXbq7RZUKZkIJxkMJDg7stgxr9VtpdNr4/Swbvd/xzond3kC2I3w/QkHIykjN7sBCScDCuGUjRe2JHh9T5KH/R4ev+7HYdH9Wq5PxZiIpJiMphY1veyeKZ8WQu/ePOCPL+nbk/OSGa1RB2SY/Q5r6dNjyJf3V/Y5S9l0VEtsXyyqVYlYCtbSkqJcVJOsSCmvCiF+iF7x8y70Ctsc/hywA/++RIVt1VEVsiKEeAXwipn/bZn5e4sQ4tMz/56SUv7+zL//EngpcA9wQQjxfXR2diu6jW8C3co3DynlL2ec8H4POCWE+Aq63f5rAB/wngXutaVCA8rOFa5FlCM3UVSS3yx3zIWfs860enEItUq+BcUipeoTf10NQyv5xnA1JCuDud45DsnpGZvFNo+VFpcVo6IQiKfpm45xdVL/bylsaXFgMRk5NRRkNJSkzqRwe6+TOz0BXGb4xAWFuEGhwTFzQQX47BYGEgqfvZziwY0ad3kDmAw+nhyI5v1abu7yEEpkFo3d70/QP6d82mIUbGh04LaZyWgao8EkY2HdHC7XF2i4JlOpjpztfaiGnjl1Rqri8mwqYdNRqflZqXi2GW+We23nrAfVEoH9Frrd/keEEPcA54GDwF3o6Z8/rtI4K6JakZXdwK8teGwds261/eiVP0gph4UQNwPvB16M7jyrAKPolrx/M1P+NA9SyvcJIU4B70Z3ztOAY8DfSSm/XeH5p0CvsCkHcy2Sa4VcCLaSplelIvcD0jRt3g9YUQR2s4HYKogYsxJsNSQOEkEkU+MutjO78Fr2d9EMRpKqxo5GEwOag4HpOCNB3XBtJSgCdnd6CCezXBibLV3e0GjHKDPc5Q3gNsMnLirYPfWc7wsQiGegQTdaGfDHGQ0l2dPl4/NXArx+g8pt+Jny2YmpBvqn4xyb8WuZS4YK7QVSWcnFBT4s3joTnd46tvhCBDMaJqOJpFqb34nLpPvlpGvYsdtlklXpSm4r1qoVfa6rJXFYizTQ07H/2pw1rLzFbAFmoiv7gA+iyzBehL5WfwT4cyllxf2HikW17PY/gJ7CKfb1k+jk5fdXeu2C930G+Ewp7ykSEZhXo14S1oI4rCVZKRRBcliNq0JWwmlBk7u24dhwjRvDqVIQW0WCZDYIehscuG0mMprG2Iw1/fUIdNSlOTMcopgUgsNsYFu7m+tTsTyZyOFAr4++MT/vvCmL2wyfvmxkIG7AkIxyU6uLc6NhNKk3bRwNJeltqGMkkCCQMGC8Lnh1b5ZXtMX49wtG9vd4Odynh3tyZGhummmlKq1APEMoEeTBDpVTfkEklaXDa6PJacGoCPzxDP3TMTKr4DboMte+Y7fbDONVSHU1OooTdGqaRiqVwlqjCpncmPDMJyvZbBZFUcomgnPWsKp1N5NSDqIHFdYUa6FZeSoiChCNlueUmWPBz3SyYjbrq2k6nc7/Owe7xUiVyPw8hDNgN4JRyJp5kIQzIp9CqBVCGX2HXA10+mw0z+g6/LE0fdNxLo4vnruGonCTBzwrWMO3uCx0+eycHgkt6tfjqzPT6rFydWSad29TcZngf66ZODUtOdDr5tB1P5ua9W71qiZn9EeCJqeVJ6f8HOz18fh1P0aDmVd1pXnHliwfPetnV4eH61MxwjMdunO9f5wzupark1GmokufdKtNN2jLpbuGAgmG5pjNGRXY2KS776oSxsLJeTb/5cJdw55AOvQ+RJerUDxa7yiOLed277XylAJ9zgEWzTuribUiK5WMN2cNK28xewrjBlnRUVFkRQiB0WhcE7JSy9RTbieVM6ObC4dldW6lcFqvWnGZwV99LlQQobS+iCtItBpZpk8nBetcklL7u9TbzXTMNByMJrO6C6w/waB/5YX3cljwfCQ7fZKfji0ec3OzE5vZwMmhIGPhxRd/e5uLoWCCselQnqh8ZcDCsQmVTq8t3y352ozuJKvJfO+lkZB+ficGAzQ6LfxsJIXFaOXFbUnevU3lo2eDWK1W2jw2LozN/i4jKb0PkWFG1xJMZPLHn4udPn0nfiVc+FpmNbg8EQNm3+u0GOiut2O3GElkVAb9cT19VSSMQuKzwLVI7Xb/dqNOvKqRBmooMrKSIyu1jKwkk8maj5mbW59OZGVmDdPQtZ/PKNwgKzqiUD5ZAWpOVtYimjOXrCzEqpGVmUnYZaodWQmndV2Gw1SdRaAYDMVghw/qLTC9xOe0mRR6Guw4rSbSWZXhYJLJSKpsP5JLIUEwDQebNH46prMIAezu8hBNZgtGY0BfHG/u8nKoL0C9ReaJykMjNh4f1S+Y22ZiMJBgc7OTi+MRnFYjUmbJqvq1HfQn6PTZ9L9eG5ORFD8eyGIy1nFfU3yGsCS5GElxsNfH4T7/vNJcVTJH1+LEYtSbIMqZVNP+RslofLaZYTGIpFTOjMxvkNg84yFjnvGQ6ZuOkVzC076tTm+EObhEc8jVQC51mOuDVAl89uKiFmtBHNZizNzcWkuBbTXIiqIocbWUBmxPE9wgK4CUMqsoSioajZYd11yryEotx8yFfQuRFad1tSIr+l9dQ1K7NBBI3ObakRV9gZN02iXTKYEioLveToPDjJQwGU0x4I9zfrRqqWgkgsOTgue3SzZ5BB6Pl35/LN+MsBDaZ8zfFhKV7084+Omgfl/saHdxeqYrsnvG7MRXZwb05312M1PRNG1unawcGwiyqdnBpfEo37uWxmywc2d9bIawGHjyup/NzU4C8XTez2UucpGXdo+Vdo+NRCRAvRUe6q+8lcB4JMX4nDEVAb0NM98LMBVJ0e+PI6VeWQWzqadaINf3qFKdjNNqxFpkVV/u91/LNNBajJnNZhFC1JysVJLqmkkDVW+SeAqhhm4ST20IIaJPp8jKWpCV3K6mUNVUsSHkUpHbMdayOidXduqqocg2JfTrd6DdyuYWJ2ajwvWpGIf7AhzpD9A/HS9YDVMpLkWtaBJubVY51OdnvEC6J4d93V78sTTXpmLziMpPpp386Lq+mBgE+OekTvqmdUMXp22WzHrq9C9zLvGY65Xy0OUUPw84abQyM4bk4niERDrLnk7Pkuc3HExyqC/AHS0aWQmXY9W/JzXJ7PfSF6BvOo7VqHBTi5ObW8xoEjJK7RbU3O8inKmMILV7bEW/NkccbLbi31MpUqkUQoiak5VapoCqMWYkEkFKGV75lU8/3CArs4iWK7AFnTzUUj+S08nkunTWAiaTCUVRCkZWmpyrE56dTQPVjjgEZkzSGlZpXnRaDGxvc3Gw18fODjfeOhNX/GnCaTBmE1wciyyZaqgWNjU72NPp4Uogw4lpwQ6vhncJcmY3G7i5y8OR/gCJjDqPqDwWcPGdK7Pp8b3dvrxQdUOTPU9IrEbDvOOBvui3uvX75upkjL3ds20jvnoxwS9DrnmEJZJSOT4YZH+PF8sSdr9tdZKNLo3HxwUj4Qx7u72sa7BXcKVWRiKjcX4sgkVLMp6A4XCaeruZXR1uDvb62Nbmyn/maqPeon9ngQpTpB3e4vvtrIXANplMYrFYaloNlMlkMJlqaH6ErpOpJJITjUZZYKj6jMGNNNAMNE0LPZ0iK6BPFoWIw2pBCIHVai1MVlyrM3FFM7o+wV3DyMpYAlRt1gG1EpgUQXeDHV+dCVVKxsMphgKJRdoIEAzGBL1OuWrCXoHujxJNZbk0x5vk4RGF3fUqv9Kr8YmL81MnG5ocRFPZvD5kLlH5WdDNNy/NikK8dSbOjc5+Lp/dQk7AOtcd1TyHaHT66hgN6ffT9ckYdWYD8ZkS+C+fjyO3OLnVE8mnhMIZweG+AJ0+GwYh8pEb/fNJfqVXRdXg0VEFVZIX+d7U6sRkUDg1tDo918yKpLUOjk3rn3M6ll6kJery1dHotKAIgT+Won86TrZCj/xOB8SyEKgwDdThLS2yYjaba+qzkkwma6pXyY1ZS0Impaw4shIOhzWeoWmgG2RlFpFwOFy2MCIX5ails+NSxGEtxmxeJbKyFiZtqhSMJqDDUfpC0uGx0eyyYlBE3gX2ykRxEbuLQcE2r6THCdeqON3YZ/xRBqbjHB8MLnp+KCb42ZjgjlbJvgbJkSn9/j3Y6+NIfyCfopkXUQm6+ObF+erVDU2OvD8KwFBglkioc3JYc38f/jkLuj+e5mCvb1559FcuJNA2OXmebz5hGfQnMBkEB3p9HJp5/e0tkg0u+O6gwlRy/m8wp/Vp99ho89g4PRysavRqs1tiVPTvcCkM+OMMzOlObjIINjU78NSZyKqSsVCSkVBpv+cOu2QoKqhUm1NqGmgtiIPP56vpmKlUCqfTWbPxVFVFSllRNCccDsMNsvKMRyQUCpVNViwWC5qmkc1maxY6tFqtBIPBmhIki8WS+0HMQ6t79fLX4XRt00CgiyQPNklMiiSzhBtpzinVZjYQS2Xp98cZCiYYCpZXNXgmIHhVL2z3alyLVJ42aHZa6Gmwc2Y4lF/Ql8K3BxS2elVe1asxmbHgsNvmkYa5ROXhKSffvxaf9/6eBjtH+meJSk993byoR3oOMVDn9OC6MhGlyWnJp4uO9Qdoc1vnLdrfupIk3WvnnsbYPMKSUSWHrvvZ2eEmEonykq4sg1H40fDSv4XhYILhYAKXzciuDg9XJqIVd3cG2O6TaBLOLUNWFiKjynkRLtAFyV2+OurMRuJplQF/nFCicKrXZdJF4IcnKzp1ANpLjKzUchHXNI10Ol3TKIeqqmQymZqSsmqk16LRqOAZ6LECNzQrczE5NTUlyu3vs1xZ72rBarWiqmrNRbbZbHbRmG0l7MxKRSgt8FpA9yCpDQZjekVO+0wq32IUbGlxcKDHx54uD80uC4F4hlPDulHamZEwkWRl38N0SjAa1xe+SrChycGeLg+T0RRPXvcX5Syc1gSfv2KgzgAvak9xdmQ2XTKXqHxv3M6P+1OLOvxajMo8AXCza/4kH03NXpvkAgfanvpZTUlGkzQ650/WGU3yyGCW74/Z5mlYcjg9FORV3RkMCnz+qgGtCPPAcCLLk9f9hBJp9nZ76G0oXrOxEALJNo/kWgTi2co2DaFENt8M8vRwiFAiQ5vHys1dHg70+NjU7Min0XLGhdUold7UXBz5yLnX1lpcC2tTKv10qniKRCIkk0kBTFTxtJ4yuEFWZjGayWSE319eq4O1ICvLlRKvFpaqCHJZjavmtTIS15u1eWuYCsoa9Mn41i4bvQ11ZFTJhbEoh/r8HB8ILls1UwnOBATNNmi0lkZYcnqUzc0OrkxEOT4QXEQoloNJETTU1/PYmGCLW+PW5sWpn4dGbDw5ppLOzk+f7On0cHFsfuR5dEE6IxCfjV6EF3TdCyfnRzZODoXY2uqa91gsrXJ4UvDNIcsiwnJvu6TLrvH9QYXO5vqSugdnNTjaH+T6VJytrS52driLfm8OXQ5wmuGMf3Wm05FgkmMDQQ71+bk0HkVVNdY32jnYof8WVWNlxKHObKC3SBFyOBxGSonL5Vr5xVXCWnisPB2N70ZHR/P/rM4ZPbVwIw00ixGAkZER6uvrS37zWhOHWoVl55Iyu312ghNC0NNQx5nh6lfNDeU8SBySgL/66a4mp4U2tw2LSTf+6vfHOTqa5DWdIFMxrk/VzmfhjF/h+e0q272SR0ZX/qx1JoXtHW4G/AlOFNCjFIMOrw2TQeFQnx+TorDBpfJAry7xvaddw2WCL/dbuBpTiCTnkzSzQeTdaOceb642w6gwzwnWvyDtcmEsis9uwh+bfU0slUERzCNc/liaSyYbX+nXeKA7w7u3qTw+LnhJl+RqGH48ItCkbu8fSmRKJpQ5cXCH10ar28qpoRCp7Mq6lh0zbrlnArVJxapSr566y6cSz8Lp8SQOs5HuhjocFhPJjMpgILHoOi+Fra2uogleMBgEwO0undSVi2eLCV0VycpIdc7oqYUbkZVZjMK8L7wkrFUa6Kk05oZGx6LHqoGcyVY1+vXYzQa2tjo52OtjV4eberuZiUiKE0NBnrzu58JYhERaJTsjsq11j6D+KARTcKBRY7m0V5PTwsFeH4qicOh6gLEShZk57Ov2Mh1Nc31KF8tmNMHHzxsIpOCBXg23CT53zchI2sJkAUO2PV3eRaRgoViz3j4/rB1Lq4s6/K5fcO/0+xPsm1PKnMNwMMF4to4vXjPQaIWXd0tGYvAfF2bTP5fGo8RSWfZ0eYq7CAswFEhwuC+A1aRwsNeHz760Bk2gi5JH4jCZrA1ZyaHTIWeIvCCaVjk7EuHJ635ODoXwx9I0OS3s7vRwsNfHTa1ObEuYvm1rKz5KEgrp6cG1ICtrkZJ5OpGVkZE8R7kRWXmGoypkpZBh2mrhqZZ6WrjgVAuBtF7C3Fni4Q1CF3767LpZ10QkyaA/wbkiXWCHYoL9jRKLQZJSa7MQSQRPTAru75B0O3TyMhcbmuy4rCZODoUKurkWC4fZwMYW5zxRbA5mRb92AEJAm9PCsQWCWtC79J4cCi56fCGpcdeZ5rnAgt7TaCg9G5GJpxfrfc6PRXBZjflmhjlcmYxx82YLAv2cjIp+zsk5UphoSuX4QJB93V7OjITKqvwJzehaTIpgX7eXqWhqnmgY4CaPxGuBR67Xdt/nNks8Zji6jLh2IpKad48IdOFzg8OCEDAVTdM/HWNbe/HEIxgM4nA4auo/8nQkDuUgZ3xXroPtjTTQswejMI+dlgSj0YjBYKgpcciJ3GKxEhqgVAiHw4EQomBF0Iam1SErOQ8SPcqxdMFWl6+O3Z0ednV6cFkN/PE3znJ1MsbVAo3uisGFoOA5TZItbsnJVUg/LYUnxhXua1e5tVmjP6rvhnd3uklktEXakHKwsdlBJJEtaKuf06g4TPA/VwQv6VG4pylGNq3ww2ExT7za6bNxbGA+CWl1W7g2Nf96FzJEc1lNzO21dn40gttmmlf5EklmOdCj9yDKwSgkL+7SuNUXZypt5EcDKq9ZL+dVCc3Fkf7ATJpLcH1qMeEqBhlN5kndtjYnilA4PaxHGJ7bLMlocHiqtlGVbR496nYxVPy4Et1ReC7hshoV/vfQIJfHI+zq9LC700O7x1awulDTNMLhMG1tbRWffykIh8MYjcaaEodYLIbBYKhpl+dKje9ukJVnDyqKrCxnmLZaMBqN2Gw2KnHeLRUGgwGn05nPXc/F+lUjKzAY1Tshe8wQTIOnzsSuDn1yzRGUuY3YEmmVP/ramYrGvBAUqJpenXOyPN11WQikBWcDgpsbJIPSw5XpJCcGq2NmttA7ZS7mimk/cVHB7q7n7074+fXN8MJOje1evdpmJK77gxwrQHY6fXZGQ/MJjKmA2+zCNJAmYWOTY1Gk50h/gC5fHQP+ON0OyevXq7TUwdmA4DOXJbu6Gvns5Ske3KgtSViGAglMynxPlnJxdkQni51eGxt8JrZ5/RyeFBVXAZWK7T5JMrt0V+likcxqHB0IcHRg9ro3OCzs7nTrv68uDzs7PLhtJiKRCJqm1TQFBHo0x+Px1NS9NhaL5TdmtUKl/jWjo6MIIVJSymD1zuqpgxtkZQZSypjBYIiOjo6WveKuhUmb3W6vaWQF9Hz10NDQImvo7vo6DIoouBBWArNRwWizARH+/P4edm/upctXt+xEYpsxQjtZpugUIKEKrkYEWz2r5ypbCI0OCxOY2KEEsaX8jIYqTzE0OMw0OS3zvFPmYiFRcXobZhZ2wUfPGri9RfKSLo337VD5wZDC9XThtEqggKhTFLhuhQSdqeziEmtNgtdqYFeXyt1tkpQKn7+i8OSkrtU4dN3P/p56Pnt5elnCktFmPFna3fQv411SLAYDCfa5oigCphUPPntynkB4NWFWJJvckjN+gVpEmXapmIqm+PH5CX58frYCdl2Dnft7DGw2wXTaSGdWxWJcfeF5KpUimUzS3t6+6mPlIKUkGo3S3NxcszFB/6yVGN+NjIwghBjTtCpPwE8R3CArcyClHBkZGdlU7vvXwqTN4XAwNTVV06ZbHo+HwcFBwuEwXu+sCNJiNNDlq8uLNcvF+kY7uzu9+u6u08OWFhfZdJIf/ehHbPIa6K4vrsxyX7e3IrICcMYv2OSuvqtsIaxvtOO26XqUb15MsmUn3NGq8dMxQbICzcyuDjd90/EltTqLIiqe+nkRCIngsTHBuaDgdetVXtylMRQL872swtmAQM6QkUaHhcsF3HozagFiU8DP6PxoGIfFmPdkUZDsqpfc3xigpQ7OBwVfvKoQTM+/Fof7Auzp9PHZy/5lCQvAqeEQ9XYzW1udRWuXCsFlktzSLLkcEvzgciSva5mM6jb6q4nNbolJqV31EcC1qRiTTpXNLfDg584gOc9NbS72dHrY1elmd6eXnvrlNxDlIBfB9Xg8VT3uckgmk6iqisOxepHihchms6TT6YoiKyMjI1LTtOEqntZTCjfIyhzMkJUNlFklZbfbUVWVZDJZM9Ok3A8qFovVLDybmziCweA8sgKwtc1VEllpnKlY2N3pYVeHh52d7hk9w3yYDDbMZnPB9NNS2N/j5ZM/v1706wsh7yrrq46rbCHs6nCTympcmKdHEXx3UOFtWzTubtP47mDpY5sUwZ4uzzzNx0IsJComhzffT2chJpOCT14ycXsb3NmU4W1bNMYT8LMxhaNTgt4GO5PRxaLfeAFTulQBApPV9AaLl0cD7GuQPK9Fo94KsQx8b9TCw4MqmSX87U4PB9nU7Oazl0MrEpZc357lUmIr4QUdGhYDfGdQnyrm6lq2t7lAsCpl/FCeW2410GGXjCeYEZtrnBwMztsMuG0mXffS4c7rX+or7Ma+FqXSubR6LclKLjpeyZgjIyOSZ6heBW6QlYUYGh4eFuV2vszdaNFo9BlNVnKGUIWIw852N985Vfj3Umc2sKPdndeY7O700Oq2FrUbE0LgdrsJhUJFR6729VTeSyTvKuuVfLO/4sPlYTUq7OxwMxRMcHKJ5npnAoJrYbizVfKzMUmkwMK7FDq9NgyKKImoaBYXF8YihYIeeWzr8PK9634eGTJwS5PkjlaNB3o1XtEN/YkA5maNwZhgJE6+TUE4uTg9EkvNsg6zImmr00txD/hCvLFNxSBgKglfvqZwaFKQ1tRlNSdZDfqn4zQ6HXz2cnRFwgLw5HU/G5scRJJZxsLFp28brJJbmiSn/YLrkcXHzjWp7PLZaHJaOTkUJKNWJzIvkGzzVscttxQoQtJuh1PLCM1DiQw/vTTJTy/pJUqvurmdf3z17orGDQaDGI3GNSEOc32kVhuVEqRwOEwkElGAG5GVZwkuZzIZMTAwQG9vb8lvnktWGhsbq31uBZH7QdVSZGs0GnE6nXnPhbnYMeMAqgjdwntPlycv1NvQ6MBoKF9/4fF4mJycJB6PFzWR6EJBT9lmaTmcCQie3y5ptMqKvTQaHGbWNzo4NxpelkjoEHxrwMDvbFe5r0Pjq9eLI9D7e7ycHl6+XHchUYkpdvzB5CJ32rlodVs5PiPETKqCR0YFj40Ktngkt7UKNrsyrF+nv1aVMBbXreD9qQQbrBL7zGxzR4uGxxbj4AaVTruk2abfLwBpVeOUX+GXY3A5PJtiAjg7HFpkHjcXsbSKOZElaazjs5fjRRGWyxNRHGYDe7o8BaujCuFFnRpCzEZVlsKAP8GAP4G3zsSmZicXxyME45XpWrod4DTBw8O1LZVuqwOTMut5VAz2dVe+WQiFQrjd7poKXdcispIbs1yCdPny5dw/L1XnjJ56uEFW5uMSwKVLl8oiK7kbrZaC17o6PU9cS7ICOnEoJLLd3enhS++4he3tLurM1b29mpqauHz5MuPj46xbt66o9zx/a3PlZGXGVXanT/LwSHmT5roGO167iZODoSVFroVwLaJXBt3aJHlsVC7qJjwXDouBjU3OeZ2PC2EhUZlWbUhVm9e/pxCanJZFNvoaupbF7vHyqYt+2mciJJ12/b/9jRKDmJ+7eVWvBmhkNRiNwxMTgqGYYDCqR2R2dHi5FA4uGj+WVrmp1YU/tvTnC8TTtHtsXItb+OzlVFGEJZqe9WQ5OxImsVSuCT0VsrdBcmhCMBov7l4IxDO6X4tB17VMRFLzHH5LQc4t92wN9SoA27yll0rv61ls6lcKUqkUiUSi5qXS0WgUs9lc07LlWCyGEIK6uvJ6VF26lOcoN8jKswR5svKCF7yg5DfnbvBaEgdFUXA4HAV9T1YTbre7oMi2zmzkQO/qtHL3+XyYTCbGxsaKJiv33tTM3/3gYkXj9kf1lMTBJm2GrBQ/Ye/qcJNWNc6PRmCqvPG/NaCwZafKq3s1/u28UnD8zc1Ogok0x1cgZguJymDChNeuMOhfvlP0tjbnkukq0HUpGU3QF4W+Obvv9fVWQrEEJgFv2aQigU9fMpDWwGAyMxEtEGlY5vIe6Q+wvtG+rHfOcDDBhkY7F4Ian72cKYqw5I7d4bFhMioFdVeKkLxmnUpag+8NlR7ZyKhzdC3tbpAynzIqBoqQHGiUDEZhosZuudu9GtNJvU9XMXBZjRU7Wq+FXgX0lEotoyqgEyS73Y6ilBcxezaQlRt2+/NxGeZ98SXD4XDUPMrhdrsJh8NoWukuneVirsi2VlAUhebmZqampshkigunb2p20OmrTD8kETwxodBsg/VFOJNbjIL9PV7aPTZODoV0olIBRuOCnwwLNntkvsHgXBzs9XF5IrJiL5yFROVqxECbx7YiURFAJLl0tMFpNS4QB8/CajEzlRSMJgRpDTIajCYE0ymB01Z453phNJzvLFwIxiIm9CuTMdY32jkVMPDZy0rBbs2FMBRMMOSPFyTc97RJuhzw3QEFf6oysnBmOMSZkTDdPhv7ur2YDCsfb7tX4jLD4xO1d8vtcuSiOcV97r3dXpQSGkoWwlpUAqXTaRKJRE3HBJ2sVEKQLl26hBAiDQxW76yeWrhBVuZAShlRFGW8UrISj8drThyklDWNruR2O4V0K6uJlpYWpJRMTBTXBV0IwfNvaql43CcnBKqE5zYtowOxmznY68NiNHC4L8BwcHkSUAq+N6QwGoeXdWv4LPqC2+iwsKXFyZPX/St2WF5IVC4EFba0urg0vjKx3tfjXTZtsbnZuWRFzVL9aIAlu3QnMhpbWpZuzHlxPMLNRfT9OTMSZneHm+PTSkmEJefJsqPdhadOr0xrrZPc36FxLQyPFtFgslj0+xMc6Q/gtBg52OvDbVvaxv6WJt1n5sgaueWWUipdDXF7KBTCYDDUNMqxVgQpk8lUTFaAy1LK2i08NcYNsrIAmqZduHjxYtlfuN1uR0pJPL66XgtzsRZRjpzItpZjgq5bEUIwNjZW9HtevLO14nHDGV07sqteYjfOX+xuanXx+oNdhBK6NmFhL5tqQJWCz10xYFLgdes1dnW4SKsLy50LYyFROR9U2Nvt5dQyaZ0cnFbjihb/6UI+KjNYrpvvctET0wrRk6FAAqtp5enr6ECQA72+kgkLwOnhMIoQbG918ob1Khq6g69cBXNA/4yuJZ7Osq/buyga6LNItngkx6dFzfpU5VCOW241UsFr4Vy7lqXS5YprpZRcvHhRSikry3c/xXGDrCzGpYGBAaVcJ9q5FUG1wlpFOXLpJ1VdOkVQbZhMJurr6xkfHy86enVzl2dRJ+By8PMxgUnR+8EA3LOlic+/7SDf/e3b+K071y/TI7k6GIwJHhlV2OSWeLLBolxYCxGVnL9IMbipxbks+XKYDZwfXTqipy1TB60sswhdHA9jXIboTERS7OrwLPn8XOgut+URFn8sTachSKcDvjeorHpn5ZyuZdCfYHubK98R+XktGorQPW1qiZxb7rlg8W653joTezo9FY2bE9fWWq+Si+Y4nUtH9qqNSquPJiYmiEQigmewXgVukJVCuCSl5OrVq2W9OXeT1zIlk/MhqHWUI5d+WotUUCaTwe8vrqpGCMFLdlUeXbkYEown4L5OwY9+93l88s37ee76BoQQdHjreNmu1a1a6PLZOBezMxCFl/dobHQtT9YKEZX9Pd6iq5G6fDaOrlDOu6nFuayHSGqZUujsMrmraEplS+vyC8bxgQBNzuJMxw73+fXy5BIJy06fxv2dkkshwWDWSau7ds30zoyEOTsSZmODjdta4HpEr5qqJbZ4dLfcUqqP7trSVJFFE/k0VwAAkHJJREFUAUAgoJPpWmtHgsFgzUulc2tFuQTp2SCuhRtkpRDyFUHlwOFwYDAY1oQ41Fpk29TUBMD4+HjNxgSdrJQ67kt3VkYkGp0Wfv++zdy6ZxtmoVKnLk6NvOOO4iqUysH+Hi/j4RRXpxJ88qKBWAbeskmj3lJ4sS1EVHZ1uIuOqAA4LKYV3V3V5RzkYNly6OQyJcIA1hV6z6RVSbu3+IjZ6aEgW1tdRROWtjrJGzdoTCXh05cULk/ECCcyRellqolmQwyzIjkybVpR11JtbPNK1BLdcu/bWnlPndxvu1Z+VaBrR+Lx+JoQJKvVisVSntvvDbLy7MUlgIsXy0v/zXVarSU8Hk++hXut4HA4sNvtJelHqgG73Y7T6SypQ/a2NhfrGkvPCW9pcfL3v7qLn7//Lt5990Y2r+/BZDJx+fJl5IKFekuLixdsq27zM6fFwO5OD4f7AvkoRTAt+ORFAxYDvG2LikWZfx6FiMrmFgfnV3CnnYudHW7OLZPeAd2F98IKlU7B+OLGhjkUcradi8sTEVYqKDk+EGTzMmLcudBdbmP0NthXJCx2o+Q3Nuul1p+4YCA24xYbS6scGwiyt9u7qHP0akARkjvbNAIp+OWoypPX/STSWfb1eOkogaiVg7xbbrh4t1yzQeF5GysjGFJKxsbG8Hg8FfXKKRVroVfJRaYrIUhz1qrLy73u6Y4bZGUxrgoh0idPniz7AB6Ph3g8Tjq99ERdbayFyFYIQUtLC+FwuKaCYtCjK7FYjEikuLJgIQSvP9BV9PHv2tzI537jIN/7nefxwN6OfIdZo9HI+vXrCYVCjIyMLHrf++/fsqzWohRsaXFQZzEWNLXriwr+95pCax28aaOGmFHMFCIq3T4bI4Hl3WnnwqjonXdXwk1trmXTPAbBsp2IA9Hlfx+hRLYoIpLJ6o6yxSCWVgnFM7S4rUsSFoOQvGWTitcC/31ZYTSx+OBH+wP46sysL4MAl4JbmiSNVvjRsJLv+p1WJUf6ApgMgk88uJdb1tWvytg5t9yzgeKXieduqMe+RJVXsQiFQiSTyXwEtVZYi0qgWCxGNputaMwTJ06gKMoUMFm1E3sK4gZZWQApZUZKeeLIkSNl6yXXgjh4PB4URWFqqkznsTKRm1BqHV3JjVuIMCyFX93XuWwprcWo8LoDXfz4927nU285wK0bGgrmrtevX4/FYuH8+fOL0m7rGh288TndRZ9TIQh075RL49FlvVMOTSo8MiLY4ZO8br1GvUVbRFQanRZiaZXICu60c7G328dIcGWB+Ur8YKUmdtG0uuz3AUuXN8/FtakYe7uLd0v1x9MYBHjqTIsIi8es8eZNGhvd8N1BhTPLLNTDwQT90zEOrpIJolmRvKBDYzIBj08svtrvuH09925t4Qtvfw7ffs9tvGpPe9WIMsCeBv3eLqVk+d6bKo8s5uaS1tbKdWalYGpqCrPZXFNxbaUESUrJ4cOHNU3TDsuFod5nGG6QlcI4euXKFVFuKicXRqwlWTEYDHi9XqamphalJ1YTc11lawmv14vD4aC/v7/oz+u2mXjlze2LHm9wmPm952/il394N//fq3awoWn5ycpoNLJ582ZisRj9/Yu7G/72PRtxWsvbXTY5LWwu0jsF4KF+hUMTgoNNkv+zS5tHVJxWI1aTwtQKEYy58NWZOD288n1vMgjOr1DS7K5bWVvhcyxvab6cU+1cXJuIYi8hLTMcTNJgN2MzGeYRlj/apbHTJ3l0VPCj4ZUX6aymN0Tc3ubCW8TnLQV3tErcZr0HkbagEqfJaZl3L29vd/OPr9nNz99/N7915/qKdS1GIdnfoKeAiq2AUgTcv73yaMjY2Bg2m62mpEHTNPx+P/X19TUV1+bWmHJTT/39/QSDQQU4WsXTekriBlkpjKMAx48fL+vNTqcTg8FQc91KQ0MDqVSqpr2JynGVrQaEEHR3d5NIJJicLD76+eAts1GPzc1O/vaBnfz8/Xfz2/dsLKmdfXd3N3a7nYsXL5LNzo9a+Oxm3nvvpqKPlcMLtjVzcJ2vKO+UHCSCHwwJUipYDXApBBeCArNB0Oq2ruhOuxDrGh3E0yuXot/U6iKxwuvsRfSGcq9A6vyxNJubV160/PGMbmFfAq5MxljfZMegCE76Bf1RsBohnoWHh0trq3BmJIxA5EuNK0WdUXJPm8ZgFE5MLz6Pt97Wm09NzkWL28r/uX8Lj//R3fzFK7bT21BemmpXvcRuKs0t99YNDTSU8BsqhEQiQSgUoqWlpeb+Kqqq0tDQULMxc+NardaytTlHj+Y5yg2y8izFUZh3I5SEnMi21hVBuR9arVNBzc3NJbnKVgudnZ0oikJfX1/R79nS4uKdd67nv996gO//7vN49b5OrCukIgpBURS2bNlCKpXi2rVri55/8JbuZV1Y58JiVPjQK7bz8Tfu5UOv2FFSe4B6i+Rd2zQMAi6HYKsX3rBB46ZWZ1HutHOxvtFedLWQqYjS1GIs5Iu59sVGCY72+0v20zkzHObmDhe/vkmj1wkXgjrpe/c2rSgflrnwx9OcHQlzsNdXcTrmvnYNm1HvC7XQhM5TZ+L1B5fXX9WZjbzpOd08/Ht38IkH9/GcdaWlqm5pkiSyhYnSUnhpFUr3cxHaWutVcnNmLcmKlDJvfFcubpCVGzgrhEiXS1ZAD+vVWmTr9XrXRLfS3NxcsqtsNWCxWGhtbWVsbIxSTPzef/8WnrexseKdW3t7O263m8uXLy/6no0GhQ++fPuKx9jU7OCb776NNz6nWye5NhP/+vqbMRdBBhaKaf/1nIEnJgT7GyXPcwUWOe2uhGJ67oAunL1URPSnmOtbjB9H33RxpCur6Sm9UuA2S253T7PDJ3lkRPCx86X1EiqEJ6/76Wmoo61MTxavWfK8FsnFoOBiaPH1+e27N+K0FkfgFEVw79Zmvvj2W/j2e27jlUXoWhqtko1uydEpQVor7jdiMghesK06KSCj0VjzCMda6FXi8TjZbLai6qOjR4+iKMo0MFS9M3tq4gZZKYAZke3Jp5vI1mAw4PP5aq5bKcdVtlro7u5GSsnAwEBNxwV9Md66dSvZbLagL8+BXh+/cnPHku9/w8EuHnrXbYsqXnZ2ePjQK5cnOoWqfiSCa1oD3x5Q2OiWvG+HSmtdcffBzV0eLo4Xl366qc1dlGBXXcYsrhRMRNJsaCrO3fPkUKjoVEy3Q79G7Xb4ynWFYVEPiLKcbhfiykSMYDxdlifLizo1jIoeVVl0zvV1ZQu4t7e7+acZXcs771yPa4kU3C3N+m/4l+PFLw93bGqqWCeTzWaZmpqiqamp7O7D5WCt9Co3xLWl4QZZWRpHL1++LMr1LfH59LDr9PR0Nc9pRdTX15NKpWre+blUV9lqoaGhAYfDQV9fX82JEuimVU1NTVy9ejXvujkX/++lWxelJtw2Ex9/417+8pU7lvTqePW+Tn777g0FnytEVIAZd9oAPxpW+MQFBYcJ3rtdZYd3+etiMQqGAsVrW6zL9PSZi3hmZUKTzhT3nfnsxUdMwsnMsj2JAPY3aLxnm4pRgY+fV/jZmMLhvmDeJr4ahCWe0XRPlq7iPVk2uzUONOlRjcECbrV/eP+WZXsqFYMWt5X337+Fx//oHj748m1019flnzMrkluaJNcjMBwvJQVUeeXOxMQEmqatScnyWuhVchHw3FpRKgYGBggEAs8KcS3cICvL4SjoNezlwG63Y7Vaa05W1kq3Uo6rbDUghGDdunUkEomSTOKqOf6uXbswGo0cO3ZsUZ8kt83EP7x6V94H5ECPj+/9zvOKqpp47/M38ao986uXliIqC91pTwcU/umMgWgGfmOLxsu6VIyi8IK7q9PLRGRlXxXQJaeXJ4ojwuHEymQlli6upHooULyPz6A/sWQps1mR/Gqvyhs3akwn4R9PG7g0J9Vyelh3uYXqEBaAowMBfHWmFT1ZrAa9BD2Sga9eXzw17+v2VqXaJge7xciDt/Twk/fdyX+8aS8Hen3sb5TUGeGx0eKXBrfNxH1bKz+v8fFxhBA0N1fXWHEl5AT6tSYr09PTuFwuzObSUpc5HDt2LPfPG2TlWY6KRbYNDQ0EAoFF1SKrCZ/Ph9ForDlpKMdVtlro7OzEaDQWFLrWAnV1dWzbto1oNMqFCxcWPf+cdfW88471/O69G/nC259DW5EiUCEEf/PATu7arDuCLkVUtrQ4OTcaXuROOxoX/MNpA+eDgnvaJX+wU6XLMf9FjU4LJwuYzi2FLa3OohooAkzHViZAxR5rJJikZ04EYCWcHwktSktscGn84S6V21okJ6YF/3jGwNSCstysBn1T0XwVTbUIy3AwuaIny8u7NbwW+PI1Je+Ym4Mi4AMv27YqaQqDIrhvWwv/+/bn8IatVpLSwJlg8UvDa/d3Vuzmm3Ot9fl8ZS/e5WJ8fByr1VpTvUoqlSISiVREkJ5N4lq4QVaWw1khROrxxx8v+wANDQ1omlYwPbBaUBSFpqYmJicna0qSoHRX2WrBaDTS3d2N3++v6bWei+7ubhobG7ly5UrBVNj/uX8Lv3vvphXTEwthMij82xv2cvd6d0Gi0u2rYyiQWLKZYCwr+Ph5hS9cVfCY9bTQS+dEWTq9tmVdaBfCWaQ7qcNiIFlEimcqmirafbbZVbxgNZJS8yXPZkXyQK/Ke7ZpWAx6n59PXVJIqYUHjmc0QvE0LTMC2WoRlpwny7Y2Fz77fCK1xa3x3GbJsSnBSf/iafmNz+kuuTS7VExOTpJJxtmzdROP/cHdvOOOdSv6BQlBxSaIoDcuTKfTNU8BJZNJAoFAzUulc5Hv+vry3YefeOKJnHPtYJVO6ymNG2RlCUgp01LKXz766KNaudql3I24FikZTdNqXkrc1qaXLg4N1V6Yvm6d3kSw3G7ZlUIIwe7duzEajRw/fnxROqgSaJkkr+6K4TbPJypNTguxdHbZZoEzZ8cTEwp/fdLApZDg3pkoy309Fo4NlEburk0V5+FTrMZEk+CtK+61I6HSPGMO901zb7eJ9+9SeV6L5OS04K9P6iZwK3mo+OMZlBmXW6geYQE4OxIGCdvb9XST1SB57XqNSFoX+i5Evd3M+56/uezxisXVq1dRFIWenh7aPDb+6IU38cQf3cMHXrqVLl/hqNY9W5rpXOK5UjA4qK+3tXatzUWgn26l0qlUil/84hdS07RHnw3iWrhBVlbCo5OTk0qh0H4xyOlW1qqUuNapILfbjcvlYmBgoOZi17q6Otrb2xkeHq5pM8eF57B9+/Yl00HlIBaL8Ytf/IJMOs3N+/bjrddTQk6rEYuxNHfaQFrwsfMKX7yq4DTDi1tjvGebSo+juLluc7Oz6PFcRZbWAkU7vw76E3QW2bxvk0vj93aovLQtgUWBz1xS+K9LCpFM8bvnkWCS+hmXW6guYfHHM5wZ1j1ZXtWjp3++dH1x+gfgD1+4pSg34EowNTXFxMQE3d3d89IwdouRN9/ayyO/fycff+Ne9vfM1wL92nMrj6pks1mGhoZoaGjAbl/dXksLMTY2hsFgWBNxbSV6lcOHD5NMJgXwaFVP7CmMG2RleTwK8Oijj5b15rXSrZjNZnw+H2NjYzUtYc65yiaTyZoTJYAtW7YghODcuXM1HzuHrq4umpqauHLlSsXi6hxRSaVSHDhwgJ6ONv7rzft50fYWWlxWBkuo4JmF4PEJhW9N1vOjYUGXHd67Q+Wtm1VabMvfK6UsmJYSjPZKaXy3kt6nwy55500q79qm0WSF7wwofH2ynmNFRFMK4epkjHWN9rw3STUJC0AkMMXBJsm5kJFTBdI/d29p4oG9S5e/VwNSSs6dO4fBYGDTpsLOywZFcP/2Fr78m8/lG++6lZfuamNzs5PbNlS+yA8PD5PNZunurpz4lAJVVZmcnKSpqQmDYfU7aOeQTCaJRqMVEaQ5a9KjS7/qmYUbZGV5PCmESJVLVkAP80kpa66laGlpIZ1O13zczs5ODAZDwZ45qw2Hw0F3dzfj4+M1r8LKIZcOMplMHD58mESiHEKxmKjkKiQMiiCZ1YquyCkEu9nAhYk43x4w8BfHDfxiTLDNK3n/LpW3bVbZ5NaAxYtw/3TxbRxKcXC1lFCKW6hqSSDZ5tH4rZtU/mCnygaXbvD2weMGfjisMBJKFuWmuxTOjoTZ2TGrF6kWYWm0St60USOYhq9cE4sqmHx2M3/9KztWXUsxNjZGIBBg/fr1Rdm+7+708C+v28M333NrVc6tv78fs9lc8xTQ5OQkqqrWPAWUm5sqJSszepW125nVGDfIyjKQUqaklL945JFHytatrHUpca1dZU0mE21tbYyPj5e9UFeCzZs3YzAYOHfuXE2jSnNhs9nYv38/6XSaJ598smT9ylJERUrJ//3aaX5yoTIt0vZ2N4G4XoUTzgi+dN3A/3fCwKFJwWaP5F1bNf5ol8qdrVreBXd9o33ZDtALoZVw7ZUSFrzrU7G88NVlktzbpvEne1TefpNGj1PyszHBh44b+Ea/IZ9SGQ2luLmr+K7MhXBsIMiBOZU8lRIWm0Hyti26z8snLxiYTkqO9ge4uctD3UxlzV+/agdNzvJccIuFlJLz589jNpvZsKGwr89SKNSbqFSEw2ECgUB+k1NL5ObGWpdKVyqunaNXeeTZoleBG2SlGFSkW6mrq8Nms9Vc7OpwOHA4HIyOjtZ80c6Fc9fCVdZqtbJ+/Xr8fn/NidpcNDY2sn37dkKhEMePHy/6O1iKqAD8/Q8v8uWjlYmX2z3WgqLayaTgC1cN/NlRA98eUDAp8MoejQ/uVXnHFpXnNKq4zcXfR6VUGGWLaS89g3qL5Pkd8K6tKh/Yq/LSbg1Nwjf6FP7sqIGvXDcQSC8mP6eGgtSXYCxXCIeu+zkwR7NRLmERSB7cqNFsgy9eVRiYY/52bCCI22bi127p5r4q2NevhIGBASKRCJs2bcJkWl1dTCHkIrC1TgHNLZW2WCprvljquJOTkzf0KmWgvD72zy48CnrY7aabbir5zTmTo76+PpLJZNndNctBW1sbly5dIhwOV9R/olT4fD4cDgf9/f1s3LixptbZABs2bKCvr4/z58/XvCRxLnp7ewmHw/T39+NyuZbUA+SwHFH5zC/7+NdHKq90qndYGA4u3UcplhX8aFjw42HBZrfkQJNkq0diM0Y5sBcGo3AmILgUUhiKsWTvmFgRdvw5pDJLR54sBkmnXbLFLdnuk7TWAUSJZeDopODJScGV8ModkhMZja1tdqZjlfXqOtQXYE+Xh+MDQYCZyiJ4cKPGu7epfPSsgfAKIt6Xdmts9Up+NCw4OrX4tzEaSvK5Jwdo9dh4+/PWoVTYFHEpqKrKxf+/vfMOj+sq8//n3FHvVu/NkizLluUiW+6OkwAJ8AstECCwsGEpoS0ElhrKhiXAEkoghLCUQAgtJJQkkIpL3GXJsmVZktVlVauXkTSacs/vj9FMZFm2pZmRZiSfz/PMM7I0c+6RPHPvd97yfc+fJzg4mMzMzAU5xtWwWq20tbURHR29qB4nYI9uTE5Okpubu6jHNRqNjI2NXfNccDWux3oVUGJlLpRO1a0E3n333S4tkJiYSEtLCxcvXlzUTxCpqanU1dXR3t6+qGJFCEFWVhZnz56lu7vb2dK8WPj7+5Obm8u5c+doa2sjPf3qE2oXCiEE69atw2g0UlNTQ0RExBXz41cTKv+o7OLrz5xzez9rkiOobB+e02MlgtphQe0wZMcE42cZY220ZO0Kya1pklvTbOgSeiagbUzQZhS0jwkGzTBsxplmmgsjJgv+miTCH1YEStJCIS1MkhoqSZhWT3txAv7VKaga0DASSM/o/IRHeesgOfFhNLhR7wNwtn2INckR9hZk5idYNsfp3JQsqRoQ/GOW2T8OrLrk28/VcqShj++9vYj4eXjMzJXm5mYmJibYsGHDoqdgwG5xYLFYyMrK8sqxwT6MdDHxxFRpR72Krus1ntrXUkCJlWsgpZwUQhzZt2/fDVJKzZVP6bGxsRgMBrq7uxdVrISHhxMZGUl7ezsFBQWLGmFIS0ujpqaGpqamRRcrYI9qNDU1UVtbS0pKildOxmA36du8eTMHDx6krKyM3bt3ExFx6aC9qwmVY439fPpPpy9zp533PoRdFLhCXEQwJ5onqBuBv7RIkoIhK1ySFmaPemyIkWyOu3SDY5ZxRiwwYhaMWe1+KtFT0fb35tgwCAj1t9edRAaMEjzjTGTRoWMMDnfbZ+Q0jQh6prnNbskMo2d0/nOoPBGksOrQ3GskOzbU6TszF8GSESZ5Z7ZO1zg81mAfPHktDtX3ceuDh3jgHUXsXRXv/uansFgs1NXVER4eTlpamsfWnStSSpqamggMDFz084PNZqOzs5O4uLhFjXSDXawEBga6PLzweq1XASVW5sqBvr6+G8+dO8fatVefhjsbBoOB+Ph4enp6sFqt+Pkt3p89LS2Nqqoq+vr6iIuLW7Tj+vv7k56eTlNTE8PDw4sa2QH73zw/P5+KigpaWlpYuXLloh5/OoGBgZSUlHDo0CGOHz/Ozp07CQmxG2ldTahUd47wocfKMNvc96wpzoymtNm1IZNdw9PTRoKuCeiaEDBVhmUQ9vRMSogkMgASQjX8sde4xAVLMgx2p9OAqUBCYbRElzBuhRELXBwWGK2CwUkYmrQP0OueAF1e+WLuqvCqu2hkU/oKyudphjeTcYvO0LiZpMgg59/naoIlIdheUDupw89rDVd0z52N/jEz//7oST6wM4vP3bLKI4WtDQ0NWCwWNmzY4JU0aV9fH6Ojo+Tn5y96mri7uxur1Upq6sK2hM9kcnKSgYEBMjIyXP6bHz9+3FGvctCzu/N9lFiZG88B9z377LMuiRWwh/26urro6+tb1Fa5lJQUqqqqaG9vX1SxAnZX2aamJhobG9m4ceOiHhvsQq2hoYG6ujpSU1MXtZBuJpGRkWzevJkTJ05w9OhRduzYga7rVxQqbQPjvP/RUkbnUftxJSKC/ajpcs0oL3VFMBcGrj5E0CYF7WPQPlUomhsfNmtr9WcL7b/LA2cvP+2kRAXTMTT37rHa7lGiQ/0ZGJu/aGkdGCPY38DEVWpl5sLAuIXkKANRIf4MTaW9ZhMsAQZ7QXCABo/UGOifdO1C9cvDzRxv6ufH79pAdlyYy/seHx+nsbGR6OjoRW/bddDU1OR0y11s2tvbMRgMS9It99lnn3V8+Zz7O1paqG6guXFK07Tup59+2uUFHBeixe5QCQoKIi4ujs7OTo9awM+F0NBQEhMT6ejoYHJy7m2vnkIIwdq1azGbzZw9e3bRjz+ThIQEiouLGR8f5/Dhwxw+fHhWoTIwZuZ9vyqd8yTka7EqIZxRk2uiJ2WOQxenExow/89AETPzQHPA1Qt2n9F8iW+KO3QOmYgODbhkkN/0LqFPrrXxiQIbwX7w81qNplH3ohjnOkd4448P8+eyNpe6/KSUnD59GpvNxtq1a70SVRkbG6O7u9srHyDMZjMXL14kMTFx0bufPOGW+/e//10KIaqllN6Z2upFlFiZA1JKXdf1p48fP+5yC3JgYKBXXGXBXmhrtVq90sqbnZ2Nruu0tLQs+rEB4uPjycjIoKOjg87OTq/sYTrJycmsWbOG8fFxTCYT69evv0SojJut3PXrk3OewXMtMqKDKW91PeXRZ5y/YAqYh8mbg+B5ON46mDC7HnU6dWGQhAjPXCibesfIjg29xAivol/jL82C2ECIDIA/NGjUj3jmdDtutvFfT1byn388zeg802Gtra309vaSk5PDihXuec+4imM6umOe12LS0dGBlHLRU0A2m42enh7i4uJcLgM4f/489fX1Qkr5dw9vb0mgxMrceVpKyT/+8Q+XF0hMTGRycpKhoSHP7WoOJCUlYTAYnMPCFpPY2FgiIyNpbGzEYnGtzsBd1qxZQ3BwMGfOnPFKhGc6Y2NjNDY2Oj/Rnj9/3mmeZ7HpfOx3pzjdNuSx44UG+jMPG5NLSIwIpLHXBdHkwod1P8P8T0U1XaNEXGMq8JWw2OQ1rfvnw0yX27ggyU0p9tocCdySprttzT+Tp8908vofHaJijvU34+PjVFVVER4eTn5+vkf3MlcmJiZobW11nhcWm7a2NgICAoiP91yx8lzo6+tz2y33mWeecXzpeoh/CaPEytzZJ4QwuZMKcrxQu7q6PLWnOeHv709SUpJXXGWFEOTn52OxWKivr1/UYzvw9/dn/fr1mM1mKisrvbIHuLSYtqSk5JKU0NjYGF/8y1n2n+/12PGKUiOpdrFWBSA9xrVpulZXCoJduI7rEvISXPfnqLgwRH6i5/w97C63K0gIlnxijY1Qf/i/Ws/OEppJ28AEb3/kGA8faEC/iiqdnv7xVqsy2MW5zWZzybPKXRxuuampqV4p6gX33HKffvppNE3rBUo9tK0lhRIrc0RKOSGlfOGFF16QJtOVTbWuRlhYGOHh4c5Q5GLi8DLwxsyehIQEYmJiaGpq8ooFP7yaDurs7PRKOmi2rp+UlBQ2b96MyWTi+Zf3c/K85yJf/ppwu+ZlPl4p03GlcNU8D8fb6Uxa3avDmrTqeLJsY6i/n8+s0wkywM9qNGqHNY8PP5yJVZf87/Pnee+vTnBxZPZzkyP9k5ub67X0z+joKBcuXCAxMZHo6OhrP8HDOFLRi13Uq+s6nZ2dREdHu9wq3d/fz5EjR9B1/Rkp5eKOtPcRlFiZH09PTEyIffv2ufRkIQSpqamMj48v+oDBFStWEB4eTmtrK7q+uK91IQQFBQXYbDbq6uoW9djT8VY66GrtyUlJSQyHZTBptfGJNTaKYz3zf7MhY8WMluP5ERcWSP1F18zTRibmX0sy5mL9SU3XCKEBrkcJmvvGKM7wxMVbsitR5yMFOiar5KX+SBqm1agstGABONLQz60PHmJf7aUTz6enf1atWuXx486V2tpapJQUFBQs+rEdbrkxMTGL7pbb09OD2Wx2y8/mn//8p+O8fV2mgECJlfnyD0C6kwpyFHYtdv2IEILMzExMJpNXCm0dbZKtra0Yje45iLqKN9JBVxMqAM9WdvKVF9v4wVkD/SZ4b67Obek2hCt5kSmiQwM42zE3p9orkRUX6vJzB1ywtB+emH8UJyMmhPdvz2JnruvdFQD1PUbCAl13cTAIyR3ZOrdn6XSMwffOGtjXNEZB0qXmf4shWAbGzNz16zL++5lzTFptSCmpqKhA13Wvpn8GBwfp7OwkPT190cUC2AtrrVar11qlhRBumd89/fTTCCEmgZc9t7OlhRIr80BKeRE48fTTT7s8hTkkJITo6Gg6OzsXPcLhmGza3Ny8qMd1sHr1aueUV28RHx9PZmYmnZ2ddHR0LOixriVUjjb2cc+fziClfZDgD6oMnBsU3JQi+VC+TpDBtddYVmwoE2b30iPz7TJxEBbomn9J/xy6jmLDArllTSL3vmE1L316Nwc+ewP3vrGAt2xwzzJ9aNxCQXLEtR84C2F+ko8V2NieICnvE/zonIFhs8AmobnPSPYM0bcYggXg0SMtvOUnRzl59jx9fX1e7f6RUlJdXY2maV4p7J3ulrvY3ioWi4Wuri4SEhJcHlw4OTnJ888/L6WUL0spPdMmuARRpnDz5+murq6tZWVlbN682aUF0tLSOHPmDD09PYtqyuTv709aWhotLS2MjIxcZvu+0ERERJCens6FCxcYHBz02smzoKCAixcvUllZSUxMzIJYbl9LqFR3jvDhx8ovcac12QQ/r9V4Y7rOzSmSewpt/LzWQK9p7kUVOfGhbrUqA0SH+FPbPerac0MDME7Ovy7JJrnE5C0uPJD8xHDWJEeyNiWCwpRI0qNDZvUF2ZMX77bJW3nLAKkrgmkfnPveU0Ik/5FvIyoAnmnVeLnz0oGKExadQeOlLrfg2vBDV+geGKalYQD/wCC3Bue5S29vr1MwBQd7rgNrrgwMDDAyMkJeXt6iR5a6urrQdd2tFNCBAwcwGo2C6zgFBCqy4gpPAfzxj390eYHk5GSEEM5hWouJw9vAW9EVh712dXW1V44PdtG2YcMGzGYzJ0+e9HiE61pCpW1gnPddwZ1WInjmgoHH6jWiA+GeQhvro+e+P+FK3/AMsuPDXJ5FFB7kutHWF29dzb7P7KH2G7dw8ss389sPlPCFW/N547pkMmJCr2hgFhxg4IZV7rkz2ySsCJnr3iUlcTr/udZGiB/84rzGy50as/VsD06lt2auvdARlgBN8h+r7DOYvn/KwqeeqHR5RIE7OKIqfn5+iz7h2EFTU5MzDb7YtLe34+fn51YX0B/+8AcAG3Bd+qs4UGJlnkgp64QQZb/73e90Vx1hAwICSEhIoKura9G9R8LDw4mLi6OtrQ2zef61Be4SHBxMVlYWfX19LhvseYK4uDgKCgoYGBigsrLSY91Z1xIq/cZJ3verUnqv0alT3qfxoyoDJhv8+yqd9+faCPW7+h5fuybBjUqXV3EnhRTkgrmbg6iQALLjwlxa45a17kcoz3aMUJhyde+PyADJh/N13p2jM2SGH5w1cG7w6qfRruHLXW5h4QSLQHJnjk5KKPy5WaNtTPBsZRevf/CQ21G3+dLR0cHw8DC5ubkup0HcYXx8nK6uLpKSkhY9qjMxMUFvby/JyckuR3TGx8d58sknJfDSVBnCdYsSKy4gpXz84sWL2v79+11eIzU1FV3XF91zBWDlypXYbDavRVfy8vLw8/Ojurp60Vu4p5OTk0Nqaiqtra0e+VtcS6iMm63c9ZuyObvTXhgTfPuMgSMXBRtiJV9cb6PoClGWYH8D/33bGp7/z1188y1riQ1z7cIQHuTncgoIuMTFdb50X6Htdi7cmB9PgAvGcjMZHDdf4XewR1O+UGQjP0qyr1Pw3UoD3RNz+30be8fIjgm9bO2FECyvS5Wsj5Ec7BIc73n1b9I+OME7fnaMn+xvwOaqU+A80HWd2tpaAgMDveJWC9DY2IiUkpycnEU/tqMmzh233GeeeYaxsTEB/M5D21qyKLHiGn8EbI8//rjLCyQmJuLn5+eVVFB8fDyRkZE0NTVhtbo/KG++BAQEkJuby/DwsFct8IUQrF+/nqioKKqqqujtdd2Q7VpCxWLT+ejvTnFmnu60kzbBE00GHq7WsOpw1yqd980SZbn7hpUkRQbjZ9C4sySDA/+1l0/dnEvUnFMbdlYlhLt1IdPdEJ8X3Wi1Dg/yZ5ebXUFgv6BvnNHKHBlgL3h+d47OqAUerDLw91YDFn1+wuxc1wiFs8wk8qRgKYrWuTVN5/yQ4G8tl5/ebbrkuy+c5z2/OEG3G3/vudDa2srY2Bj5+fmLOmneweTkpNMt1xv1ce3t7QQFBbk1C+jxxx9HCDEB/M1jG1uiKLHiAlPhuJeffPJJOT5+9Ym0V8JgMJCSkkJvby9jY4tb4C2EIDc3F7PZ7BWTOLDXzgQGBlJTU7PoXVHTMRgMbNmyhYCAAE6ePOnS/8W1hIqUki88dZYDbrjTnh/W+NYZA0cvCjbOiLKkRAXzod2XfnINC/TjUzfnceTzN/LVNxaQHDm3IuJJF83ZPPF8dyIrALcWeqbTo7pzeErkSTZPRVNWR0n2T0VTWoyuR48qLgyxJfNyQzRPCJbkEHv6p9cEv67X0K9Sv3SsqZ9bH3yFl6sXJrNgtVo5f/48oaGhpKenL8gxrkVTUxM2m80rxcVDQ0MMDw+Tmprq8rDIvr4+nnvuOSml/KuU0jt+Dz6EEiuu8/jY2JiYNq9h3mRkZADecZVNTk4mNDSUhoYGr4gFPz8/Vq1axdjYmHOwmbcIDg5my5Yt2Gw2Tpw4Ma86omsJFYD/feE8T51yP4I2aRP8aUaU5SOrbXzxpvQr1nmEBvpx184sDn5uLw++cz0b0qOuuH5ogIHabtft+QHGZikanitXcl+dK69ZneBWGsqBcdLGtpRAPrnGxntydIwW+NE5A39zIZoyG6UtA2zJvPyTvjuCJcxP8sF8G1LCL2oNjFuvvc/BcQv/8VgZX3/6HCY3Oqlmo66ujsnJSVavXr3o1vZgbxlubm4mKirKrciGqzjcch3neFd44oknsNlsAnA9hL+MUGLFdf4mhJhwJxUUFRVFZGQkFy5c8IqrbG5uLiaTySsDDsH+Ro6MjKSmpobRUdfrJDxBdHQ0RUVFjI6OUl5ePqdamrkIlUePNPPTA40e3asjynKgU5AXKbF1nqOsrOyqZnv+Bo03rU/hrx/dwdMf38FbN6ZcNh15VWI4Fpt7tQyuWvQDbqclIkP82bYyxq01EoPtXTS3xA6RHgYvtgv+t9JA86hnW4tLWwbZOItwdEWwGITk31fZW6gfa9DmXEfj4NnKTkZcMOW7EoODgzQ0NBAbG+uWEZo7tLS0YLFYyM3NdTmy4SoWi4X29nbi4uIICwtzeZ3HH38cTdP6gJc8t7ulixIrLiKlNEop//rcc8/Jvr4+l9ZwtNNNTk56xVU2NTWVoKAg6uvrvVLoqmkaGzdudLpserPYFiA9PZ2VK1dy8eLFaxrXzUWoPFvZyX3PLkyL9qRN8LcLBrLWlZCamkpHRwf79u3jzJkzXGt21brUKL7/jvWU33sz339HEXtXxeGnCbfqTQA0YS9QdRV3IysAt651LRW0IkDy7pU2Pl9ko2CF5Ei34PftkfyjzTPRlNk40zbEmlnM6OYnWCS3Z+nkRMA/LmjX7EyaiRDw4Ds3EB/hGa8hm81GRUUFmqaxYcOGRRcKjj00NjYSFha26CZwYC+stdlsbrVKNzU1cezYMXRd/4OUcvELC30QJVbc43c2m0088cQTLi+QmpqKn5+fVzpzDAYDK1euZGxszGuFrhEREaxatYrBwUEaGz0bgXCFNWvWEB8fT319PRcuXJj1MXMRKt3DJj7zxBmX/Urmwjs3p7EhO5FNmzZxww03EB8fT0tLCy+//DI1NTXXTGeFB/nz1o2pPPrvW3jlv/a61QUEdodZd37fEZPVbefd165JYD6ZoFA/yZszbNy7wUZJvOR0v+Bbpw080WzgVMcYG9Ki3NrP1bBJuNA/xuqky+3n5ypYbkiSTvdcuynd/PjkjbnsyPFcmuT8+fOMjo6yZs0aQkJcm9rtLm1tbUxOTnolqiKlpLm5mcDAQLcMP3/3O2fzz3XfBeRAiRX3eEnTtD53UkF+fn6kpaXR19fHyIh79QKukJmZSUBAAOfPn/daZCM3N9dn0kFCCIqLiwkPD6eiouIyS/65CBWAxMggfnDH+stSLZ4iPNCPz7z21aF0kZGRbN26lZ07dxIZGUldXR0vvvgiZ8+enVPR8On2IUwW91KRkcGuG8I5cLfINjYskC1Z157omxAseXuWja9ttLE3WdI4Inig0sBv6i91DO4cmliw/8OwQD9+cucmHrurhPToyy/s1xIsOxJ03pKp0zwKf2ic3ZTuauzIieGTN3nOqG1wcJD6+nri4uK8YsAGOIelBgcHu9Uy7CoOt9zMzEyXa3WklPz2t7+VmqY1AqWe3eHSRYkVN5BSWnRdf/zYsWNUVVW5vE5WVhbgHVdZh7Pk6Oio12pXHOkgwCfSQf7+/mzfvp2wsDDKy8udUae5ChUHry9M4g8fLCE61PNmWJ+8KZfYsMDLvh8TE8POnTspKSlxtqe//PLLnDhxgp6eniv+bf951n2/n1A3hgE68EQ77ZVSQQLJmih7UfKX1tvYmShpHhX8pFrj4RoDbWOXX+wvjk4uSHQlMSKIJz68jd15ccSFB/LYXVuImeV1ciXBUhKn845snTYj/Kxm/qmquPBAfnjHBgweKEiGV9M/fn5+rF+/3ivpH7CfQycmJpxO2YuNwy3XncLaQ4cOUV9fL3Rd/7X09snQh1BixX3+D+BnP/uZywtMd5VdbEdbsIul4OBgamtrcdWV1118LR0UFBTE9u3bCQ4OpqysjNbW1nkJFQebMqL5y93byYzxXEg8KzaU923PvOLPhRAkJiayc+dO9uzZQ1paGj09PRw7doyXX36Zurq6S+paTBYb+2vddxMOMLh/gfJE3crr1lwafl8RILk11R5F+dBqnZXh9pqU+08b+GmNgbrhq58Gz7QNuWyyNxurkyL428d2XDI8MTM2lEf/fTMhAZd3dc0ULDsTbLxzpU7nGDxcY2DCNr+/u79B8NC7NhAXfrnYdRVfSP9YLBbq6uoIDw93axaPq0xMTNDV1UVycrJbbrmPPPII2O31f+WpvS0HlFhxEyllDXDwN7/5jXTHLyUrKwubzXbFOomFxGAwkJ+fz8TEhNdcbcHuKBsVFeUT6SCwtzTv2LGDwMBATp8+jclkmpdQcZAZG8pfP7qDvW7Or3HwlTeunnNqIioqio0bN/La176WgoIChBDU1NTw4osvcuzYMZqbmzlY3cGYm7UigL1a003cTQOBPQW3OyucPYk6Hyuw8dWNNm5Jk0zY4Klmja+dstekXJxj14zJqpMRE3rtB86BN6xL4s8f2UbiLL4361KjeOQ9m2Ztv54uWG7PkvSb4CfVc2tRnsl3by+iJNu9rqnpDAwMONM/7kQU3KW+vh6LxeJ8nS82LS0tSCmdkXJX6O3t5c9//rME/i6l9J5jpg+ixIpneGR0dFT86U9/cnmBxMREQkJCaGpq8orvSVpaGuHh4dTV1XklugM4OwjAN9JBYM8fT9+Hq3taERrAL9+3mc/dssqt0PuevDj2roqf9/MCAwPJzc3lpptuYseOHaSkpDjnIplaKvhsoZVbU22khkpwccKQ1eb+69bVNJCUkoGBAaqrq9m3bx9vSxzkrVk6KSGS0l7B988a+M4ZA690ay5d4MtbB8mNd70NNcCg8Y03r+Whd20g7Crpst15cTzw9qJZfzYzcOXKy+izr83jzRtS5v/EK+Ar6R+TyURTUxPR0dFuDQ10FavVSktLC5GRkURHX7tm6kr8+te/xmq1CuARz+1ueaDEimf4q6Zp/Q8//LDLV1chBCtXrmR8fPyyos7FQAjB6tWrsVgsNDQ0LPrxHUxPB3lzH/BqjYrFYmH9+vUEBwdTWlrqcueUpgk+ekMOf/jgVhIi5h+C99MEX3njarcuCEIIYmNj2bRpE7feeiubS7ZyrMdAsB/ckib5r3U2/nuTjXdk2R1yowPnLl48YSw21zSQlBKj0Uh7ezsVFRW88MILjlw/UkriUzJ4sMrAvWUG/tBooNUomG8B6mzHdIX06BD+8tHtvHdrxpz+7968IYUvv371Jd/bGq9zZ47OxQl4okkQ44Jx3B3FaXxsr2dn5NTW1mI0Gr2a/gF7Gspms3ktqtLa2orZbCYnJ8fl4+u6ziOPPCI1TWsG/uXZHS59Fn9gwzJESjkphPh5eXn5F0pLS9myZYtL62RkZFBXV0d9fb1bNs2ukpiYSHR0NI2NjWRlZREU5BnvhfmSk5NDV1cXtbW1JCYmEh5+eWvnQjNbMW1cXBxHjx6lrKyMjRs3utxtsCUrmn9+chef/fMZ9s/Dgv+92zLIiffc30LTNOpHNP7YKAADScGwZoVkbbTOtgTJjkT7hXDMAm1jgrYxaDMK2sYEA5Mw8+I/bHLfDmK2NJCUkrGxMaeF+dDQEENDQ5fMtYqJiSEnJ4fExESnEVfoMSP66LDbe3LQ0DvGpowV85pc/MZ1Sdz/1kIigubXKfXB3dn0jJr4+aFmdibovD1bp30MHq42MGYVTNh0/i1X5+NrbDx0zsCI5ernil25sfzPW9Z69JwyMDBAQ0OD19M/RqOR1tZWEhMTiYnxXHprrui6TkNDA6GhoW6Z4L3wwgs0NTUJ4KdSSu/NIPFRlFjxHI8An3vooYe0xx57zKUFDAYD2dnZ1NTU0N3dveiGRkIICgoKOHz4MLW1taxfv35Rj+/AkQ46ePAgp06dYteuXYta2X+lrp+QkBB27NjB0aNHKS8vx2w2k5WV5dIFICYskF+9fzO/O3GB//lH9TXbhleE+POpmzw/4+Q5ZxeQoGsCuiYEL3dqhPtLMsMkqWGStFBIC5XkR4EjyjJmgc5xGDYLhi0wYhaY9AmCIiQjZhgxw+ScOlQkQQaI8IeIAEm0HKWhoQGTyeS8DQ8PXyJMHMPhIiMjiYqKIjo6moCAywtgb1mbyNkOz4kVgJa+MYIDDNf0gwkP8uMbb1rLm9YnuywQvnBLPiETPWT7D3PBCD+tebVGpaLf/n6Yi2BZkxzBw3duxN8DU6kd+Er6B3BOb1+9evW1H7wAtLW1YTKZKCoqcus89dBDDyGEmJRS/tKD21s2CF+oC1guCCH+6u/v/6b29nYRHz//ugKwV7S/+OKLhIeHs2vXLq+cBEpLS+nq6mL37t1emVbqoL6+nurqarKzsyksLFyUY86lPXlycpJjx44xPDxMZmYmhYWFbp2kGnuNfPpPp6lsv/KF9X/evJb3bPXsp1erTWfL/f9iYGwurrOSyAC7aEkLlaSF2b1KIgIg4Aq/+qQNzDro0m6AFjEVXBi12OstNGF/buDsY40wGAwEBQURHh5OVFSUczzFXCN+Tb1GbvzewTk9dj6UZEVzonngqj///h3rSYlyvSPEarVy+vRpOjo66LX480CFjmmWrp8NMfYIS6+JWQVLdmwoT3xk26xt7q4ipeTUqVO0t7ezfv16r0ZVHF1uGRkZXvlwJaVk3759WK1Wbr75ZgyGK7yYr0FDQwN5eXlIKX8ppfwPD29zWaAiK57lxxaL5c0///nP+fKXv+zSAv7+/mRlZVFfX09/f79XhnCtXbuWnp4eKisr2b17t9c+NeXk5DAwMEBTUxMRERELflKcq49KYGAgO3fu5PTp07S0tDA6OsrmzZsJDHTtgrAyLoyn7t7OT/Y38PD+RswzClXzE8N51xbPT64tbRmYo1ABEAyb7ZGUqkuyIJJgA6yKC2J8fIKIAIgIgEh/u5Dx0+yFoZqAEIP9g1H3hHAKGItuj8IMmwUjFhg2wy/v2k5yTAR+fn5uvfay48JYlRDO+Yue7Sw71TpIYmTQZcXAwf4G7nlNHnftzHKriHpiYoITJ04wPDxMRkYGe1cV8Peu0lmjRFeLsCRHBvHb/yjxqFABaGxspL29nfT0dK9NVAZ7+uXs2bP4+/t7LarS1dXlrNlxVagAPPzww46aqIc8trllhoqseBBh51xiYuKqlpYWbbbQ9FyYnJzkxRdfJCYmhu3bt3t4l3Ojrq6Ompoar39yslgsHDp0CKPRyI4dOxYsJz1fwzewf6qqr6+npqaG4OBgpxGbOzT3jfH1p89xsO7VWpbff7CE7Ss9L1q/+vcqHjvmmYnfa5IjONd5dQfmzxbaUzkPnL36Z6RnP7GTtSnu/R0d/OClOh78V71H1prO+rRITre9Kh5eX5jIvW8oINmNaArY60BKS0sxm80UFhaSmZmJEILe0Uluf+Qorf3jsz5vZoTFPyCQJz6yjZVxrncwzcbFixc5fvw4K1asYMeOHW5doN3FEXktKiryimOulJKDBw8yPj7Oa17zGvz9XXNwHh0dJTU1VY6MjByRUu7y8DaXDaobyINIO9/v6urSps12mDeBgYFkZGTQ29vL0NCQ5zY4D1auXEloaCjV1dWYza4Pp3MXf39/SkpK8Pf35+TJk4yPz36ydgdXhArYa3zy8vLYsmULZrOZQ4cOuT1jKSs2lF//+2Z+9t5NpEQFc8uaxAURKroueb7Kc8Mzg/w9d9HyhIutg1sLXZ/PcjVOtw1TkBROdmwoj921hYfv3OS2ULlw4QJHjhxB13W2bdt2ST2Uw+X2SuZ0031YPrlW55fvKfK4UBkdHaWsrIygoCC2bNniVaEyMTHB+fPniYyM9NqHqd7eXoaHh8nOznZZqIDdUHRkZEQAP/Dc7pYfSqx4nt9qmtb9rW99S7rjButogaurq/Pg1uaOwWCgsLAQs9l8zQnEC01oaCibN2/GbDZTWlp6SbGlu7gqVKaTlJTE7t27CQwM5OTJk27PWRJC8Lo1ibx8zx7+5y1rXV7napy6MEjP6KTH1pvNyMxVPGEM52BVgl1QLASTVp1nPrGD3Xnumf3puk5VVRUVFRWEhoayZ88e4uIuXzMjJpRH37+F0FlcbsEuWJ5s8ScuSNLfdPaa07fng9ls5sSJE0gpKSkp8VqnoIOqqipsNhtFRUVeS1PX19c7myJcxWQy8cADD+hCiPPA3zy2uWWIEiseRko5qev6d+vr68Vf//pXl9cJCQkhNTWVrq4ur0VXEhISSEpKoqWlhcHBubdrLgSxsbEUFhYyPDzsMcM4TwgVBxEREezZs4fY2Fhqa2spKytzW1QFBxg8Xm/g4DkPRlUAdA+mkz1hue9ACMEtaxcmutLYO8ZfKtyLpJnNZo4fP05jYyOJiYns2rWL0NAri6vC1Egeee8m/GcZbRAbFsi9d+xk06ZNGI1Gjhw54hHBous6ZWVljI2NsWHDBqKiotxe0x16enro7OwkIyPDaw0Avb299PX1OQfBuspvfvMbLl68qEkpv6Xala+OEisLw/9pmjZ0//33uzWHatWqVU57dG+xdu1aDAYDlZWVXneUzcrKIjMzk87OTrcjTp4UKg4CAgKc4fvOzk4OHTrkE2MDZiKlZ1NAAGar586znkwDwZUHG3qC7794nqFx19KkQ0NDvPLKK/T29jrTiXNJJ+zKvdzlNnVFME9+ZBurkyJITU31qGCprq527jElxXPut65gs9m8XlQrpaS6uto5BNZVrFYr3/nOd6SmaW3A7z23w+WJEisLgJTSqOv6gxUVFeKll15yeZ3Q0FCysrLo6emht3fu5mGeJCQkhLy8PIaGhnxiwGBhYSExMTHU1tbS1eXapOCFECoONE1j3bp1FBUVYTQaOXDggNNV1Vc42zFMx9CER9c0TnouNefJNBDA2pQIUle4V09yJQbHLfzw5fkV8Oq6Tk1NDa+88gomk4lNmzaxevX8nInftD6Fe99gv1jnxIfx5Ee2kzkt3eUpwdLa2uqM+uTn57u0hic5f/48RqORgoICl7vv3MUR7c7JyXFrD3/+859pbm4Wuq5/V0rpnRknSwglVhaOHwshxr/1rW+5tUheXh4Gg8FpfOQNfGnAoKZpbN68mZCQEMrLyxkZuXoHykwWUqhMJzMzkz179hAREUF1dbVPRVk8nQICGBr33LnWk2kgmEoFrVmYVBDAb4+3Uj/H9uihoSEOHjxIXV0dMTEx7N2712Un5P/Ylc19b1rDEx+efTCiu4LFMTsqPDycjRs3etX4DWBwcNDrAxMdQjMgIMCtWhUpJd/61rekpmn9gDKBmwNKrCwQUsp+KeXPDhw4wPHjx11eJzAwkJycHIaGhlyOJLiLrw0YDAwMdI40OHHiBJOTcysUXSyh4iAiIoJdu3axevVqhoeHfSLKIqWc5lrrGTQBAy6mQmbD02kggFsLFy4VZNMl9z179Q8Tuq5TW1vLK6+8wtjYGIWFhWzfvv2q9Slz4d+2ZRIdeuWaCVcFy8TEBKWlpRgMBmc3njex2WycOnXK6465Fy5cwGg0smrVKrf+Jv/85z85e/as0HX9B1JKz7c4LkOUWFlYvi+EsLobXVm5ciUBAQHU1NR4ZSIz+NaAQYDIyEg2btzI+Pg4ZWVlXKvzarGFigNN08jLy/OZKEtt9ygtV/DqcJWY0EA8qb9GTNZr2tnPlw1pUS4Nj5wrh+r72FfbM+vPhoeHOXjwIOfPnyc6Opq9e/eSnZ29aBfc+QoWq9XKiRMnMJvNbN682W1B5Ql8YWCi1Wrl/PnzhISEuOXrIqXk/vvvRwhhBH7isQ0uc5RYWUCklO1Syl8//fTTVFVVubyOv78/eXl5GI1G2traPLjD+eFIB9XW1vpESiM5OZn8/Hz6+vooLy+/opDzllCZzmxRloaGhkWPsixECigyxPOfuj2dCtK0hU0FAXzj2epLCo0d0ZSDBw86oyk7duzwysV/roLFZrM53XMLCwtnbaFebHxlYGJzczMmk4n8/Hy3xmscOnSIo0ePIqX8iZRyyHM7XN4osbLw/C+g33fffW4tkpmZSUhICLW1tdeMIiwUvpYOAntNT3Z2Nl1dXZw6deqyPfmCUHEwPcoSHh7OuXPnOHz48Lzrbtzh+SrPpxLDrjTcxw08XWQLcMsCdgUBtPSP8+ujzYC9vsKb0ZTZuJZgsdlslJaW0tfXR35+PllZWV7a6aV78oWBiWazmfr6eiIiIlyuMQJ7VOW///u/EUKYgR96bIPXAUqsLDBSynrgN3/+858pKytzeR2DwUB+fj4mk8mrXTnT00H19Z63MZ8vQgjWrl1LZmYmHR0dl4goXxIq04mIiGD37t2sXr2awcFB9u/fz6lTpxbEnXc6Uko+f0s+79qSRly451IiAR6c5uvA05EVgC1Z0cRcpb7DE/z+cB2Hj524pDbFW9GU2biSYHF4qfT09JCbm0tenucnfLuCL6R/wG4AZ7FYKCgocEswvfTSS+zbtw8p5Y+klJ4Pcy5j1CDDxeHrQog7v/CFLwS8/PLLLi+SmppKY2MjdXV1pKWlERy8MO2Y1yInJ4fu7m5qa2uJjY0lOjraK/twIIRg3bp12Gw22tra0DSNnJwcjh496nNCxYEjypKcnExNTQ1tbW10dHSQlZVFXl6eW0ZTV0IIwU2rE7hpdQLf1CWVHcO8VN3NwbpearpGsemuRcoW4tPuQhTZGjTBa9ck8IdSz6dSI/wlr0vV2ZZgpb+nm7S0NPLz8716gb0SjshAeXk5R44cYdu2bZw7d47u7m5Wrlw57zbqhaKnp4eGhgbi4+O9mv4ZHR2lsbGR2NhY4uPjXV5H13U+//nPS03TjLquu1fIeB2iBhkuEkKI7wH3vPjii7zmNa9xeZ2BgQEOHTpESkoKxcXFntvgPBkfH+fAgQP4+flxww03LMjFdb5IKSkvL6ejowODwYCu65SUlPicUJmNwcFBampq6O3txc/Pj5ycHFauXImf3+J8nhibtHK6bYiylkHKWgeouDA0Z++UTRkrKG+9tsPxXAcZAvz7jky+9v/WzOn48+GVul7+7VelHlsv2CC5MVnnhmRJgGZ3fS4oKCAiIsJjx1go2tvbKS8vx8/PD6vVSmZmJuvWrfMJoTIxMcGBAwcQQrB3716veapIKTl27Bh9fX3s3buX8PBwl9f64x//yLve9S6AL0sp7/fYJq8TVGRl8bhf07QPfuELXwi76aabhKsFWtHR0aSnp3PhwgUyMjK8VgAXEhLC+vXrOXnyJKdPn2bz5s1eP8kJIVi1ahVdXV3YbDYSEhLc+iS0mKxYsYLt27fT09NDdXU1tbW1NDc3s2rVKjIyMtwq6JsLoYF+7MiJZUeOfWiirkta+sc41zlCVecw5zrs97P5qXi6cwcWJg0EsG1lDJHB/gxPuOcL4yckuxIlr0nVCfWDqBUrWLtmzYJNBV8IkpOTaW5uZmBgAD8/P/Ly8rz+Hga7QDh16hRms5nt27d7TaiA3QCut7eXnJwct4SKxWLhy1/+stQ0rVfX9Qc9uMXrBiVWFgkpZb8Q4junTp36nyeffJJ3vOMdLq9VUFBAV1cXlZWV7N27d8EvZFciOTmZrKwsmpubaW5udsskyROMjY1x7NgxpJRER0dz8eJFTp8+7dXCvPkSHx9PXFwcnZ2d1NTUUFlZSWNjI/n5+aSkpCza76Fpguy4MLLjwvh/RcmA/SIyMmGlfWic9sEJOgYnaB+c4OWaix4//kKkgQD8DRo3r07gqVPtLj1fQ7I5TvL6dJ2oAAgODWPd2jUkJCQsmdcYvFqjMjAwQGxsLH19fRw9epQdO3Z4fUhhXV0dfX19rFq1yqvdSFarlbNnzxIUFOR2Dc8vfvELmpqaBPA1KeWYZ3Z4faHSQIuIECJU07SmrKysuJqaGuGOqVBzczOVlZWsWbOGnJwcD+5yfthsNqdvyK5du7w25GxmMW1cXBwVFRW0t7eTmprKhg0bvCbqXEXXdVpbWzl//jyTk5OEhYWRlZVFenr6oqWHroWUkjVfe4HxOURX5pMGSokK5sgXbnR7f7PxcvVF/uOx+RW7BxskW+Mle5IlKwIkgUHBFKzOJy0tbUmJFLC/Z0+ePMnFixfJysqisLCQjo4OysvLCQsL86pg6evr48iRI8TGxrJ9+3av/m2rq6upr6+nuLjYrZlIY2NjZGdn6319fU26rhcoa33XWFpn7yWOlHJM1/WvNzY2il/+0j2H5czMTCIjI6mtrWViwrNzXuaDwWCguLgYIQRlZWVYLIv/Ppyt60fTNDZu3Eh6erozN+8tQz1X0TSNrKwsbr75ZgoKCpxD3F544QWqqqoYG/P+B7TRSeuchMp8uThiQnex4Pda7MyNJTRgbu3W8UGSt2XZuK/YxpszdZKjgiksLOQ1N99Eenr6khMqDsO3ixcvkpOTQ2FhIUIIjw8/dIXJyUnKysoICAjwur3/6OgoDQ0NxMbGkpyc7NZaP/zhD+np6dF0Xf+yEiquoyIri4wQwl/TtJrY2NispqYmzZ2WRl8ptoVXi/WSk5Od4mUxuFZ7spSSyspKWlpaSExMZNOmTT4TlZgvuq7T1dVFU1MTAwMDACQmJpKRkUF8fLxXIkf1F0d5zQ9emdNj5xNZATj55Zs92mI9nU/8oYJnznTO+jNNSApXSLYlSFZH2c+PsbGxrFy5csmle6ZjsVicPip5eXnk5+df9rs43seLHWHRdZ3jx4/T29vL1q1bvVoU78mi2v7+frKysqTRaKyQUm6WUi6tT0w+xNI8ay9hpJQWIcSXe3p6/viDH/yAe++91+W1fKXYFuztkP39/bS0tFBXV8eqVasW/Jhz8VFxtDUbDAYaGxs5fPgwJSUlXmv7dgdN00hJSSElJYWhoSGampro6Oigu7uboKAg0tPTycjIWNR22YUwb3NwccS0YGLl9WsTLxMrcUGSrfE6W+MlYf6gaQbS0lLJzs5eEt09V8NoNHLixAmMRiP5+flXfH/ObGteLMFSXV1Nb28vubm5Xu/e81RRLcD//M//MDo6KoDPK6HiHiqy4gWEEJoQ4lhgYODm8+fPi/T0dJfXmpyc5F//+hcBAQHccMMNXo0a6LrO0aNH6e/vZ8uWLSQlLZxj6HwN36SUNDc3U1VVRUBAAFu2bPG6P4wnMJvNtLe309ra6nTCjYuLIykpicTExAUXZU+Wt/PZP5+Z02PnG1n55fuKuWn1wly4xs1WNn7jJUI1G2tXSIpiJCsj7OfC8IhIsrMySUlJ8foAP0/Q09PjnJ+1fv160tLSrvmcxYywXLhwgYqKChITE9myZYtXI1dms5l9+/YhhODGG2906/+/qqqK9evXY7PZnpdS3urBbV6XKLHiJYQQxUDpW9/6VvHUU0+5tZbjzZ6dnU1hYaFnNugik5OTHDx4ELPZzO7duxfkE6k7zrS9vb2cPHkSm81GUVER7ghFX0JKyeDgIK2trXR2dmK12oVBZGQkiYmJJCQkEBUV5fELwU/2N/DdF87P6bHzFSvffMta7izxrBmY4+/U3d1NRW0zYQb7nvz8/UlNSXHWgi0HpJQ0NTVx7tw5AgICKCkpYcWKFXN+/mIIloGBAY4cOUJoaCi7du3yujgsLy+nvb2dkpISEhNdnyUlpWTv3r288sorFinlmiknc4UbqDSQl5BSlgkhfvaXv/zlIy+88AKve93rXF4rLS2Nzs5OmpqaSE5O9qrXQ2BgICUlJRw6dIgTJ06we/duj/okuGuhHxcXx549ezhx4gQVFRWMjIxQUFCw5DqFZiKEIDo6mujoaIqKiujv76e7u5vu7m7Onz/P+fPnCQoKIjExkcTERGJjYzEY3J/ps1AtxgAXPbS21Wqlt7fX+fcwm80AJEaGOf8e0dHRS7YWZTZsNhuVlZVcuHCBqKgotmzZMu8o20KnhCYmJigtLcVgMFBSUuJ1odLd3U17eztpaWluCRWwG8AdPHgQ4H+VUPEMKrLiRYQQ0ZqmNWRnZ0dVVVUJdy7qExMT7N+/3yfSQQCdnZ2cPHmS2NhYtm3b5hEx4MlZPxaLhVOnTtHd3U18fDzFxcVeP1kuBFJKRkdHnRfqwUG706zBYCAuLo74+HgiIyOJjIx0Sbx88LEyXqqem8/KfCMr7yhO5X9vL5r3nqxWK8PDwwwNDdHb20tvby+6rjsFnUOghIWFzXvtpYDJZOLkyZMMDAyQmprK+vXr3RKmCxFhsVqtziGe27Zt8/p0Z0+mf0ZGRsjLy9N7e3s7dV1fJaVc2KFf1wkqsuJFpJQDQojPNTQ0/Pz73/8+X/ziF11eKzjY3lJ56tQpampqvJ4OSk5OJj8/n9raWs6ePeu2jbenhxL6+/uzZcsWampqqK+v5+DBg5SUlLhdUOdrCCGIiIggIiKCvLw8JicnncLFEW1wPC48PJyoqCgiIyOJiooiIiLimqJ3oZxmAbpHJq/5mOnCZGhoiOHhYUZHR50/9/Pzc4qThIQEnxgLsZAMDQ1RWlrKxMQEBQUF5OTkuB0x8nSERUrJ6dOnGR4eZt26dV4XKgBnz55lcnKSrVu3uv2h5b777uPixYsa8EklVDyHiqx4mali2+OBgYHF7hbbSikpLS2lu7ubHTt2EBsb68GdurafsrIyOjs7KSgoIDc316V1Fnp6cnt7OxUVFWiaRnFxsde7ERYLm83mvNA77kdHR51Tq4UQhIWFERUVRVRUFOHh4QQFBREUFISfnx9CCLZ882V6Rq8tKmD+kZVVCeG88OndSCmxWCyYTCZMJhOjo6NOcWI0Gp2Pdwgzx34dEaOlnuKbK46p40IINm3a5HYqYyaeirCcO3eOhoYGMjIyKCoq8nr6rauri9LSUtLS0ti4caNba507d46ioiJsNtsLwK1SXWA9hhIrPoCj2PZtb3ubePLJJ91ay2QysW/fPvz9/dm7d6/X00E2m42jR48yMDDAhg0b5l3QutBCxcHg4CClpaWYTCbWrFnDypUrvX4S9QYOATM9WjFdwDgwGAwEBgZS1T3OsFkwYoERs2DYAiNmMFoENgm6xHn/wXwbAvi/WgOaAIPAeR/uL4kIgMgA+wTjiACIDhTkxwdhMpkuM/SbKUwcYsoTdThLDSkltbW11NXVERoauqARQncFS2NjI1VVVSQkJLBlyxavC0lPpn+klNx4440cPHjQOlVUW+fBrV73KLHiIwghHgE+/MILL/Da177WrbXa2to4deoUWVlZrFu3zjMbdAOz2czhw4cxGo1s2bJlzp/4FkuoODCZTJSWljI4OEhqairr1q1blnUs88VmszEyMoLRaGRyctIZ4RgxjtPQPUikPwR5WBOPW+2ipyAthpDgYIKCgggMDCQoKIiwsDAiIiK8fqHzBcxmMxUVFXR3dxMXF0dxcfGCp7pcFSyO85JjaKe3P0jBq90/njCi+8Mf/sC73/1ugPullF/2yAYVTpRY8RGEEDGapjWsXLkysrKyUribE3akg7Zt2+YTk4cnJiY4dOiQc5LqtTxOFluoOLDZbJw5c4a2tjaCg4PZsGGDT+TUfZGKC4O85eGjAARoU5ERf4gIkIT6vRo5cdx2Jeog4dBFDX0q2uKIvBgtMGIRDJthxAIW3R7VOvDZG8iMdd3leTnT1dXFmTNnmJycJDs7mzVr1iyagJuvYOnp6eH48ePOFmVfqB3q6OigrKyM9PR0NmzY4NZaw8PD5Ofn6z09PZ26ruerYYWeR3008RGklP26rn+mvr5efO1rX3NrLSEE69evJzAwkFOnTnllzsdMgoODnV1Bx48fv6QIcibeEipgT29s3LiR4uJiZwrrzJkzTt8SxatML64164I+k6BxVFDRr3H4osbBbo39XRr/6tR4qUNjZEqIvNRh/97+LvtjDl/UOD2g0TQq6J8UTqECC+uQu1Qxm82Ul5dTWlqKEIKtW7dSWFi4qJGm+cwScqRYAwMD2bZtm08IlbGxMU6fPk1oaChr1651e73PfOYzdHd3a7quf0IJlYVBiRXf4lHghQceeIDjx4+7tVBgYCCbNm1icnKS8vLyy2oOvEF4eDhbt25F13WOHTs26wBGbwqV6aSkpHDjjTeSlJRES0sL+/fvp7e31yt78VUW0mPFwUJ2Gy1Furu72bdvn9MPZO/evV57j8xFsBiNRo4fP46maWzbtm1RR0FcCcfUaZvN5hHLgueff56pwbR/kFL+zRN7VFyOEis+xFTl+AeB0fe///3S3YhIXFwcq1atoq+vj7o636j1io6OZvPmzZhMJo4ePXrJCc5XhIqDwMBANm/ezKZNm7BarSrKMoOLc+wCcusYSqwA9mjKqVOnOHHiBAAlJSVs3LjR61GKqwmW8fFxjh49itVqpaSkxGfmK1VXVzM8PMyaNWuIiopya63h4WE+8IEP6Jqm9QKf8MgGFbOixIqPIaVs03X90+fPnxdf/epX3V4vLy+PmJgYamtr6evr88AO3SchIYGNGzdiNBo5evQok5OTPidUHAghSE1NZe/evZdEWXzlb+lNPOUwezW6hxdeEPk6jmhKW1sbaWlp3HjjjR5vS3aH2QTL+Pi48+stW7Z41VV7Oo6p5YmJiWRnZ7u93j333ENnZ6em6/qHpJT9Htii4gqoAlsfRNh7Zv+padotR44cYevWrW6tNzExwYEDB9A0jRtuuMGj9vfu4OgOCAsLw2q1YjabfUqozERKSUdHB5WVlVgsFrKysigoKPCJrgZv8O6fH+do49zPz/P1WQF4Q2ESP7nTPe+LpYrZbKaqqoq2tjYCAwNZv369T4mUmTiKbkNCQpBSOoWKr+x5fHycAwcO4Ofnxw033OB2VOq5557j9a9/PcDvpZR3emSTiiuiIis+yPR00Pve9z45W23HfAgODmbjxo2YTCZOnTrlE/UrYJ9pVFBQgNFoxGQysWHDBp8VKvBqlMXxyba5ufm6jrIsRvHr9Vpg293dzf79+2lra7vkNefLpKamUlhYyPj4OBMTExQVFfnMnnVdp7y8HKvV6pH27qGhoenpn096ZpeKq6HEio8ipWzXdf1TdXV1HkkHJSQkkJOTQ09PD42NjR7YofuMjY3R3NzsNF+rr69nctL3w/5BQUFs2bKFTZs2YbFYOHLkCKdPn/aJrqvFZHHSQNfX33R8fJzy8nJOnDiBlNL5OvN2bcpcGB8fv+Tc0tDQ4DPvidraWgYGBli9evU1bRPmwj333ENXV5em6/oHVfpncVBixbd5FHjue9/7HseOHXN7sdWrV7NixQqqq6u93tkyvUalpKSEDRs2MDIycs02SF9hepQlKSmJ1tZWXn75ZWpqarBYLN7e3oIzarIwZrYt+HF6Rk3oum9EAheSyclJqqqq+Ne//kV7e/slr62lwNjYGIcPH2ZiYsIpsObS1rwYdHV1UV9fT3x8PDk5OW6v989//pNHH30U4HdSyr+7vaBiTqiaFR9HCJGqaVp1dnZ2WEVFhXB3UuzExAQHDx5ESsnu3bsJDV18w60rFdM6alhCQ0PZtm2bV/bmKv39/VRXVzMwMEBAQAC5ublkZWUtW/v3hp5Rbv7+K/N6jis1KwBl995MbJhv1Fl5GqvVSmNjIw0NDVitVuLi4igoKHC7S2UxGRkZ4dixY5jNZjZv3uxM/SzEtGZX9vbKK68QGBjI7t273a7X6+/vZ+3atXpPT0+fruurpZQDHtqq4hqoyIqPM5UO+mhDQ4P42Mc+5vZ6wcHBbNmyBYvFwokTJxY9CnC1rp+0tDQ2b97sdLsdHh5e1L25Q0xMDDt37qSkpITAwEDOnTvHv/71Ly5cuOAzNUKeZDG7dJZjKkjXdZqbm3n55Zepra0lLCyM7du3s3379iUlVAYGBjh8+DAWi4WSkpJLalTmYxy3EExOTl7S6u2uUJFS8v73v99h/naXEiqLixIrSwAp5ePArx977DF+85vfuL1edHQ0RUVFjI6OLmrB7Vzak5OTk9m6dSs2m43Dhw8vqeJVIQSJiYns3bvXad9dUVHB/v376erqWlaiZTELX5eT14qUkvb2dvbt20dlZSV+fn4UFxeze/fuJTfWobu7m6NHjyKEYMeOHbOO9fCWYNF1nZMnTzI+Ps6mTZs84vHywx/+kGeffRbge1LKf7i9oGJeKLGydPi4pmnn7777bllbW+v2YhkZGWRnZ9Pd3Y0n1rsW8/FRiYuLY8eOHRgMBo4dO0ZnZ+eC78+TCCFIT0/npptuYu3atUxOTlJaWsrhw4fp718etXiLKSCWQ0eQlJKenh4OHjzo7EopKirixhtvJCUlZclN+G5tbaW0tJSAgAB27tzJihUrrvhYbwiWs2fP0t/fT35+vkfqfkpLS/nc5z4nhRAngS+5v0PFfFFiZYkgpRzTdf3tJpPJ/Pa3v93tdmaANWvWEBcXR11dHR0dHR7Y5ey4YvgWFRXFrl27CA4O5uTJkzQ3Ny/Y/hYKg8HAypUrufnmm8nLy2N4eJjDhw9z/PhxRkZGvL09t1hMsXJxxPc7xK7G4OAgR48e5dixY4yNjbF69WpuvvlmMjMzl9zkaCkldXV1nD59mvDwcHbt2kV4ePg1n7eYgqW5uZmWlhaSk5PJy8tze72hoSHuuOMOXdd1o5TyDiml2QPbVMyTpfVOuc6RUp6VUn6yqqpKfPrTn3Z7PU3TKC4uJjQ0lIqKCoaGhtzf5AzccaYNDQ1l586dREZGUllZSW1t7ZJMpfj7+zsvUFlZWfT09LB//36OHz9OT0/PkvydFrOOZDFapD2NlJKuri6OHDnCK6+8wsDAADk5ObzmNa8hLy9vSRoJSimpqqqipqbGWaMVHBw85+cvhmDp7e3l7NmzREZGsmHDBrcjVlJKPvjBD9LS0uKoU1l6n5qWCaobaIkx5W77B+COP/7xj9xxxx1urzk6Osorr7yCn58fe/bs8VjVvqcs9C0WC6WlpfT19ZGSksKGDRuWdJfN2NgYdXV1tLe3o+s6YWFhZGdnk5aWtmQuYm966DBn2udXAO1qN9CevDh+c9eWeT3HW1gsFi5cuEBTUxPj4+MYDAbS09PJzc2d14Xd17BYLJSXl3Px4kUSExMpLi52+T24UF1CY2NjvPLKKwgh2L17t0eGJv70pz/lox/9KMDDUkr3OxwULqPEyhJECBGhaVpFSEhI1unTp8XKlSvdXvPixYscP36ciIgIdu7c6fYkUk/P+tF1ncrKSlpbW4mMjKSkpGRJn/zB3q3Q2tpKc3MzJpMJf39/MjIyyMrK8onptFej5P6X552ecVWs5CeG8/ynds/rOYuN0WikqamJCxcuYLPZCAkJITs7m/T0dLffS97GaDRy4sQJjEYjOTk5FBQUuB2x8LRgMZlMTp+X7du3e2QW0enTpykpKZEWi+WslLJESrn0QnzLCCVWlihCiE1CiOMbNmwwHD16VHhi3k9rayunT58mJiaGbdu2ufzJaaGGEkopaW5upqqqioCAALZs2eIRN0pvo+s6XV1dNDY2Mjg4CEB8fDwZGRkkJib6XF2D1aaTd+9zzNerzVWxEhXiz+mvvnZ+B1sEbDYbnZ2dtLa2OgunY2Njyc7OJjExcckVzc5GT08PZWVl2Gw21q9fT1pamsfW9pRgcbhIDw8Ps3nzZpKTk93em9FoZOPGjbKhoWFCSrlBSukbY+uvY5ZGzFlxGVLKciHEZ06dOvXgxz72MX7+85+7fXLMyMjAZDJRW1tLeXk5mzdvnveaCzk9WQhBdnY24eHhnDx5kiNHjlBUVER6errHjuENNE0jJSWFlJQUBgcHaW1tpb29nZ6eHgIDA0lLSyMjIwN3DQE9RZ/RPG+h4g5D4xZMFhtB/r6R+hsZGaG1tZW2tjYsFgt+fn5kZWWRkZFBZGSkt7fnEaSUNDU1ce7cOQICAti6davHPxikpqYCUF5ezpEjR1wSLDabjdLSUoaHh1m3bp1HhIqu6/zbv/0b9fX1AviwEiq+gRIrS5sfAyW//OUv311UVMQnPvEJtxfMy8tjcnKS5uZmKisrWbdu3ZwFy0IKlenExcWxZ88eTpw4QUVFBSMjIxQUFPhcBMIVVqxYwYoVK1izZg0dHR20trbS0NBAQ0MDsbGxJCUlkZiY6NU0kTdaiS+OmMiI8Z6j8djYGN3d3XR0dDijX9HR0WRkZJCcnLxkao3mgs1mo7KykgsXLhAVFcWWLVsWLOXqjmCRUlJRUUFfXx+rVq0iKyvLI3u67777+Otf/wrw4ymPK4UPoNJASxwhRLAQ4pCmaZuef/55br75ZrfXlFJSVlZGZ2cn+fn5rFq16prPWSyhMp3pRX+xsbFs2rTJK5beC83w8DAtLS10dHQ4HYcjIyNJSEggMTGRqKioRU05PF/VzUceL5/381xNAwH86UNbKcl2vw5hrkgpGRwcpLu7m+7ubkZHRwEICAggLS2N9PR0jxiN+Rrj4+OUlZUxODhIamoq69evX5Ri9vmmhKSUnD17lubmZjIzM+f1oepqPPnkk7z97W8H+Bdwi5TS6vaiCo+gxMoyQAiRomnaqfDw8NiysjLNE8O6bDYbx48fp6+vj6KiIjIzM6/4WG8IFQdSSmpra6mrqyMwMJCNGzfO6qS5HNB1nf7+fucFdHx8HLBPgXYIl7i4uAW/uPzmaAtfe/rcvJ/njlh58J3redP6lHk/bz5YrVZ6enq4ePEi3d3dmM12O42wsDASExNJTEwkOjp6WdSizEZnZycVFRXYbDYKCgpYuXLlov6u8xEsdXV11NTUkJSU5FK6ejZOnz7N9u3b5eTkZLOu61vUNGXfQomVZYIQokQI8cqqVav8T5w4ITzxqW8uhWveFCrT6e3tpby8nMnJSXJzc8nPz18WaaErIaVkdHTUKVwcqQmDwUBcXByJiYkkJCQsSKTpO8/X8tMDjfN+njti5Uuvz+dDu93vepvJxMSE82/Y19eHruuAfdaTQ6D4Sq3QQmGz2Th37hzNzc0EBwdTXFzstcL1uQgWTzUCTKenp4dNmzbpnZ2d47qub5VSzl+NKxaU5ZNovc6RUp4QQnywtrb2N+9+97v5+9//7vab2N/fn61bt3L48GHKy8sxGAyXiBFfESpgr2O54YYbOHXqFPX19fT391NcXLzk25uvhBCCiIgIIiIinHVGjotub28v3d3dgL0GJi4ujqioKKKioggKCnL7U6g3TNo8MThRSsnExARDQ0MMDQ3R09PjHJbp5+fnFCcJCQkEBAS4fbylgNFopKysjOHhYZKSkli/fr1Xf/dr1bC0t7dz+vRpIiIiKCkp8YhQMZvNvO1tb6O9vV0A71ZCxTdRkZVlhhDiu8BnP//5z/Ptb3/bI2uOj49z+PBhJicnKSkpIT4+3qeEynSklNTX11NbW4ufnx8bN268ZBLs9YDNZqOvr88pXqY7hQYGBhIZGekUL64ImDt/cZwjDfOPkLsTWXnDuiR+8u6Nc368lJLx8XGGh4ed4mR4eNiZ2gH7BHKHQImNjV3WkbjZcFz4pZSsWbOGrKwsn0lxzRZh6ejooLy8nNDQUI+ZyUkp+dCHPsQvfvELgC9KKT1z0lR4HCVWlhlCCAPwLHDL448/zp133umRdcfGxjh8+DBms5n169dTU1Pjc0JlOv39/ZSVlWEymcjMzKSgoGDJm3O5wmwX7aGhIWehLtiLRh3CxSFkgoODr3jhuul7B2jsHZv3XtwRK8UZK3jy7u2z/szxO04XJe7+jssZs9nM2bNnaW9vJzQ0lOLiYqKiory9rcuYLlhycnI4c+YMISEh7Nixw2MR0x//+Md88pOfBLsr+J1SXRB9FiVWliFCiChN0074+fnlvvzyy2LXrl0eWddoNHLo0CHMZjNCCEpKSnxSqDgwm82cPn2arq4uQkJCWL9+PXFxcd7elteZmQ6ZLeoQEBBAaGgoQUFBzltgYCBBQUHc+egpuow2xq0gmfvF3hWxIpCE+UNuTCA/fdc6TCbTJbfJyUmMRuMlwiQwMPASUeKp9NdyoLu7m9OnTzM5OUlaWhqFhYU+LeIdggXskTDHcFNP8I9//IM3velN6Lp+Skq5U0rp/nRYxYKhxMoyRQiRq2nasbCwsBWHDx/WCgsL3V5zbGyMQ4cOMTk5iRCCrVu3+nznjZSSzs5OKisrMZvNZGZmsmbNmmXli+EJZgqY4eFhxsfHMZlMWK2zd2/adBix2G/DZsGIGUYsApMVdECXYJP2e13CrWn24tXn2jQ0AZoAw7T7IIMkIgAi/CEywP51uL/9Z7Ph7+9PUFAQISEhl4gTJUwuZ3o0JSgoiKKioiWRHu3s7OTkyZMAhISEsGvXLo+kf44dO8aNN94ozWZzx1RB7cKNnVd4BCVWljFCiGIhxMGEhISg48ePaxkZGS6vNb1GpbCwkPPnz2M2m302DTQTk8lEZWWlirK4gM1mc0YyWnqGePCFaiICJBH+EBEwJSz8IcxDH9BHLTBinhJAU1+PWAT33raOuKhwZ5RnKQ+zXExmRlPWrl27JAqIHTUqjjlLZ8+e9cgsoerqarZv366Pjo6O6Lq+TUpZ68FtKxYIJVaWOUKI1wgh/pmbm2s4cuSIiI2NnfcasxXTOsa8m81mNm/evCQ+pUkp6ejooLKyEovFcl3XsrjKkYY+7vzFiVl/ZhB20RJgeDViognQsP/7nSttAPyx0WCPuHBp9GXSZo/S6HL2qMg/PrmTNcnLw85+MTCbzVRVVdHW1kZgYCDr169fEu9TeDX94yimDQ4O9sgsoba2NrZu3ap3dXVNSin3SilnfzErfA4lVq4DhBDvAn6/efNmuX//fhEaOnfb8qt1/Th+ZjKZ2Lhxo7Pt0NeZHmUJCgpizZo1pKSkqNTBHHiqvJ3P/PmMS891p8AW4FfvL+bGfN+P4nkbKSUXLlyguroas9lMamoqhYWFSyKaAjhHfcwmStwRLAMDA+zcuVPW1NTowP+TUj63ANtXLBDXV6/edYqU8g/Ap06ePCluv/32S4oRr8a12pNDQ0PZuXMnoaGhlJeX09g4f6MwbxAUFMTmzZvZvHkzYPd0OHr0KCMjI17eme/jjblAzmN7wGtluTM0NMShQ4c4ffq00ydp06ZNS0KoONyoKysriYyMnFWMpKamsmnTJmdkd3pb/tUYHx/njW98o6ypqRHAXUqoLD2UWLlOkFI+CHz7+eef56677nI6dV6JufqoOIreVqxYQVVVFefOnWMpROuEECQnJ3PTTTeRm5tLf38/Bw4coKqqas5i7nrkojfFiheP7euYzWbOnDnDwYMHGRkZIT8/n7179y6JejKwC5UzZ85w/vx5YmNjrxo1ma9gsVqt3HHHHRw7dkwA/yWlfGwBfgXFAqPSQNcRwp7n+CXw75/97Gf57ne/O+vjXDF8s1qtlJWVcfHiRdLS0li/fv2SMtkaHR3l7Nmz9Pb2qtTQVdh/vofylkF6RyfpGTXRP2bGOGllbNKK0WRlzGy74nOvlQYSAkID/AgL9CM00EBYkD+xoQHERwQSFxbItpWxbFu5eMMMlwIzUz5JSUmsXbvWq1O554vNZqO8vJyuri5SUlLYsGHDnIqn55ISklLygQ98gEcffRTge1LKz3r+N1AsBkqsXGcIIfyAvwJv/MY3vsG99957yc/dcabVdZ3Tp0/T1tZGQkICxcXFS6pFWEpJV1cXVVVVTExMEB0dzZo1a7w2J2UpouuSMbOVsUkbxkkrJosNKUGXko4au19GyupNaEIgBAQHGKbEiR8h/gY0TYnDudLT00N1dTXDw8OEhoZSWFi4ZCIpDiwWCydOnKC/v5/s7GzWrl07rw8IVxMsUkruuecefvjDHwI8DrxPSnn1kLLCZ1Fi5TpECBGC3eV277e+9S2+8IUvAJ6Z9SOlpLq6moaGBlasWMHWrVuXRL58Olarlfr6ehobG7HZbCQmJrJ69Wo8MRzyeubAgQMA3HDDDV7dx1JnaGiI6upqent78fPzIzc3l5UrVy65Vu6JiQmOHz/OyMgIq1evJjc316VI5myCRUrJ5z73OR544AGAvwNvl1Kq/O4SRomV6xQhRCjwHLDru9/9LnfffbdHZ/00NDRw7tw5wsLC2Lp1K/PpQPIVJiYmqKuro7W1FSklaWlp5OfnL6kQuy+hxIp7GI1Gampq6OzsRNM0srKyyM3NJTAw0NtbmzcjIyMcP34ck8lEUVER7nhAwaWCZfv27dx3332O2WjPAm+TUpqvsYTCx1Fi5TpGCBEuhHhOSrnjwx/+MG94wxs8avLW1tZGRUUF/v7+bN68GVc8XnyB2S4SeXl5Sy5i5G2UWHENk8nE+fPnl41o7u7upry8HCklxcXFHvN+aW9vp6ysjKeeeorHH38c7B/G3iKlVG1kywAlVq5zhBARQogXpZQl//3f/81Xv/pVj67f399PaWkpFouFdevWkZmZ6dH1F5PBwUFqamqc4ffMzExWrlzpEfvv6wElVubHxMQEDQ0NtLa2Lot0pJSShoYGqqurCQ4OZsuWLR4doCil5FOf+hQ/+tGPAF4CbpNSqhayZYISKwqEEJFTEZZt3/nOd/jc5z7n0fXHx8cpLS1leHiYzMxMCgsLl1Sn0Ex6eno4f/48AwMDaJpGeno6OTk5SzLVtZgosTI3RkdHqa+vp729HSklsbGx5OfnExOzdDuhbDYbp0+fpr29nejoaDZv3uxRkS+l5Etf+pIj9fMi8GY1mHB5ocSKAnCmhP4ppdz5zW9+ky996UseXd9qtVJRUUFnZyexsbEUFxcvyVy7Aykl/f391NfX09PTgxCClJQUcnNzl+wn34VGiZWrMzQ0RF1dHV1dXQAkJSWRm5vLihUrvLwz95iYmKC0tJShoSHS09NZt26dR4uBZxTTPge8VUVUlh9KrCicCCHCgH8Au7/2ta/xta99zaM+I1JK6urqqK2tJSQkhJKSkmVxYR8aGqK+vp7Ozk4AEhMTycnJITo6Wvm0TEOJlcuRUtLX10d9fT29vb0IIUhNTSU3N5fw8HBvb89tBgYGKC0txWw2s3btWrKysjz6ntB1nXvuuYcHH3wQ7MW0t6saleWJEiuKS5jqEnoauPFjH/sYDz74oMdbIjs7Ozl16hQAmzZtIikpyaPrewuj0UhDQwMXLlxASklkZCRZWVmkpqYuubbShUCJlVexWq20tbXR3NzM6OgoBoPBmU5cqoWzM2lra+P06dMYDAaKi4uJj4/36Ppms5m77rqL3/3udwB/A+5QXT/LFyVWFJchhAgCfgvcfvvtt/P44497PGUzPDzMiRMnmJiYYOXKlRQUFCzpOpbpTExM0NLSQktLC2azmYCAANLT08nIyCAsLMzb2/MaSqzYW3ZbW1u5cOECVquVoKAgMjMzyczMXNJp0elYrVaqqqpobW0lLCyMkpISj7/ujUYjt99+Oy+88ALYXbk/IqW0evQgCp9CiRXFrAghDMCPgbv37t3L3/72N4+nbMxmM6dOneLixYtERUVRXFy8rIpUbTYbnZ2dNDU1MTQ0BEBcXByZmZkkJiYuG3E2V65XsWKz2ejq6qKlpYX+/n4AYmJiyMrKIikpaVm9DkZGRigrK2N0dJSUlBSKiorw9/f36DF6e3t5/etfL8vKygTwTeArUl3Ilj1KrCiuyNQsoXuB+4qKiuTzzz8vPOWJ4EBKSVNTE+fOncNgMLBhwwaSk5M9egxfYGhoiJaWFtrb27HZbAQEBJCSkkJqaiorVqy4Lmpbriex4ijAbm9vp7OzE4vFgp+fnzPCthxqtabjmFF09uxZAAoLC0lPT/f467qlpYXXvva1sr6+HuA/pZQ/9ugBFD6LEiuKayKE+BDw08zMTF566SUtJyfH48cYHBykrKyM8fFxMjMzWbt27bKs87BYLHR0dNDW1sbAwAAAoaGhpKamkpqauqzTRNeDWBkZGaGtrY2Ojg4mJuyds7GxsaSlpZGcnLykZmXNFYvFQmVlJe3t7YSHh1NcXLwgYqyyspLXvva1ek9Pjy6lfI+U8k8eP4jCZ1FiRTEnhBBvEUL8MSYmxu+FF17QNm7c6PFjWCwWTp8+TWdnJxERERQXFy+LjogrMTY2Rnt7O+3t7RiNRgCioqJITU0lOTmZ4OBgL+/QsyxXsTI+Pk5HRwft7e2MjIwAEBER4RSgy+3/cTpDQ0OUlZUxNjZGeno6hYWFCyLIDh48yP/7f/9PNxqNE1LKN0spX/b4QRQ+jRIrijkjhNitadqzwcHBYX/961/Fa17zGo8fQ0pJS0sLVVVVCCFYt24daWlpyzpNIqVkeHjYKVwmJ+2dl1FRUSQmJpKYmEhERMSS/xssF7EipWRoaIju7m66u7udAiU4ONgpUJZbmmcmUkqam5s5d+4cQgiKiopIS0tbkGP95S9/4V3vepe0Wq39uq6/Tkp5akEOpPBplFhRzAshxDpN014UQiQ8+OCDfPSjH12Qi+jw8DAnT55kbGyMxMREioqKrgtbeyklvb29zguhI5UQHBzsFC4xMTFLMkW2lMWKzWa75P/FIShDQ0Mv+X9Z6oJyLoyPj1NRUUFfXx+RkZEUFxcvSPpSSsn999/Pvffei6Zpzbquv1ZK2eDxAymWBEqsLDBCiBjgLcAbgEIgBTADZ4FHgUellPq0x+cCbwVeB+QCCcAgcBz4oZRy/yzHeP/UWlfibinlI7M87xPAp4AYYD/2grULc/id0oQQf5dSbvjQhz7Ej3/84wUZ6me1WqmpqaGpqQl/f38KCwtJTU29Li4IYD9Zj4yMOC+Qjo4ig8FAXFwcsbGxxMbGLpmoy1ISK7quMzw8TH9/P319ffT19WGz2QCIjo4mMTGRhIQEwsPDl8Tf3hM4op7nzp1D13Vyc3PJy8tbEOE8Pj7OXXfdxZ/+9CeAV7CbvfVe63lCiO8AxUAeEAtMAK3YfVgeklL2T3usP/BRYD2wASgA/IEPSil/cYX1388inmsVr6LEygIjhPgI8FOgC/uL9AJ2AfJWIBJ4Cni7o/VOCPFH4A6gGjgMDACrgNsAA/YX+Y9mHOP92N9AfwdOz7KNZ6WUZTOe8yHgEeAvQAtwO/Y39oa5WFULIUKw+xu8c9euXTz11FPExcVd62ku0dfXR0VFBePj49dVlGUmExMTXLx4ke7u7ksunv7+/sTExPi8ePFlseJI7TjESX9/P1ar3bbDz8+PuLg4p0BZLn4o82F6NCU8PJwNGzYs2BiAtrY23vSmN8mKigoB/Az45FzN3oQQZuAU9vNnDxAKbMUuYDqBrVLKtqnHRmH/IAhwEfuHyDTmJlYW7VyrsKPEygIjhLgR+xvmHzMiKIlAKfY3x+1Syqemvv9+4IyUsmLGOnuwTxKVQKaUsmvaz96P/Q3071LKX89xX0eBY1LKz0z9Ox67kHrjXIvXplqbvwB8My0tTT7zzDNaUVHRXJ46b6xWK9XV1TQ3N1+XUZaZ6Lp+2cV1pniJjIwkKiqKqKgonxB3viJWpJSYTCaGhoact4GBgUvESWxsrFMARkZGXrevMyklra2tVFVVoes6OTk5rFq1asHSkEePHuXNb36z3tvbK4FPSCl/Op/nCyGCZhMAQohvAl8Cfiql/OjU9wKAm4DTUsouIcTXga8xN7GyqOdaBSy/PjofQ0q57wrf7xZCPILd1OgG7BEWrvQGkFIeFEIcAF4DbHc83g0MgG3av23YhdCcz0JT0aBvCSHOtbe3/37btm0hv/3tb8Xb3vY2N7d2OX5+fqxbt47k5GQqKio4deoUnZ2d122URdM0oqOjiY6OJjc39zLx0tfXR3d3t/PxgYGBTuESFRVFZGQkQUFBy/4iLKVkYmKCoaEhhoeHneLEbH71g7q/v78SJ7OwmNEUgEcffZQPf/jD0mazjWAfRnhZyvtaXCVS8QR2sZI77bFm7IMPFxq3z7UKJVa8jWXqfq420dd6/HohxKeAIKAD2C+lbL/CY38H/EAIkYFd5b8Fe5j08Bz34kRK+bQQomRycvKZ22+/PevrX/86X/nKVxbEmTM2Npa9e/c6oyz9/f2sXr2azMzM6/oCM1O8SCkZGxu75AI9MDDAxYsXnc8xGAyEhYU5b6Ghoc6vPe06utCYzWaMRiNjY2MYjUbnbWxszBlxAggICLgk4hQVFUVwcPB1/dqZia7rNDU1UVtbi81mIzc3d0GjKVarlc997nP84Ac/QAhRLaW8TUrZ5OHD/L+p+0oPreeVc+31jBIrXkII4Qf829Q/n5/D4zOwhyzHsReczcZ/zvi3TQjxC+BTs3zi+DH2N9rdwC3AIez1MGNz+w0uRUp5TgixGfjz17/+9b2VlZX86le/IjIy0pXlrsr0KMuZM2eorKyktbWVdevWER0d7fHjLUWEEE7hkZKSAtijDOPj484og+PC3t3dfckFHeyRmODgYIKCgggKCiIwMND5tePfgYGBC24Vr+s6k5OTmEwmTCbTJV87bhMTE5dESuBVIZaQkEBYWJhToChhcnV6e3s5e/Yso6OjREZGUlRUtKDRlN7eXu68805eeuklgKenzN5G3V1XCPFZIAx7XWAxsBO7UPm2u2tP4bVz7fWKqlnxEkKIB4DPAP+UUr7hGo8NBP4F7AA+J6X87oyf78HeafQi0I79DboT+BawEviDlPLdHv8lZt+rP/B94ONZWVn6E088oRUXFy/Y8XRdp7GxkfPnz2Oz2cjIyGD16tXXZRGkqzhqOGZGJCYmJjCZTJcJgelomoafn98Vb0IIpzjo7OwEcI5TkFKi6zo2mw2r1TrrTdf1Kx7bIZiCg4MvixBdDykuTzIxMcG5c+fo6OjAz8+P1atXk5WVtaB/w4MHD/LOd75T7+7u1rCnw786va7PHYQQ3dgbGRw8D7xfSnnxCk9hjjUrPnOuvd5QYsULCCE+CTwI1AI7pJQDV3msAfgD8HbgT8C75jq0SwiRBpwBVgDrpZRn3N37XBFCvEPTtF9qmhb6wAMPiE9+8pMLeuKbmJigqqqKzs5O/P39VWrIgziiGxMTE5dENsxm8xVFhtVqxWazca2XqhDCKWwMBsOsgscR1Zke3VmMqM71wMyUT3p6OgUFBQsq9m02G/fffz9f//rXAQZ0XX+PlHJBakeEEAnYa/y+DYRjL2qd1VRuLmLlKsfx2rn2ekGJlUVGCPEx4CHsrXU3SSm7r/JYA/A48E7sBWJ3zncMuhDiceBO7O1/izr0SwixUgjxhJRy42233cajjz664Gma3t5eKisrMRqNREZGqtSQl3GcX6SUHDx4EIA9e/Y4RaQSk95jZspnMd4rXV1dvOc972Hfvn1gT2e/W0rZsaAHxZlGrwPqpZRrr/CYr+OiWJl6vtfOtdcD6qPJIjJVkPUQUAXsvYZQ8cMeUXkn8Hvsb+p5CZUpHEZKoS481y2klI1Syu3AD59++mnWrVunHzlyZEGPGRcXx969eykoKMBoNHLo0CHKy8sZG1PpYW/gSANpmjbr14rFZ3R0lNLSUo4ePYrJZGLdunXs2bNnwYXKiy++SGFhob5v3z4J3If9w9qCCxUAKWUr9g+Ia4QQsQt0GK+da68HlFhZJIQQnwd+gN1IaK+Usucqjw0AnsSe+nkMeK+U0nalx1+Dkql7T1fXzwkp5aSU8tPAm7q6ukb37NnDt7/97avWIriLpmnk5uZy0003kZqaSnt7O//61784e/as0yZdobjemJiY4PTp0+zbt4+uri4yMjK46aabFrw2xWq18qUvfYnXve51DA4O9gE3Sym/5uKHL3dInrp39Vx6Lbx6rl3uqG6gRUAI8RXsnyTKgddeo0YlELvT4euxO8R+6FpFZ0KIXVLKQzO+5zBs2wb0MYeOo4Vkqr15nRDiD1/84he379+/n8cee4yEhIRrP9lFgoOD2bRpEytXrnTa9re2tpKTk8PKlSuXXHuuQuEKZrOZ+vp6mpqa0HWdpKQkVq9evSgTzS9cuMC73vUuefToUQG8pOv6e69W5OoOQoh8YGhmxFoIoQHfAOKBo1LKwdmeP8dj+Py5drmialYWGCHE+4BfY1fzPwaGZ3lYi8MMTgjxKPB+7C/6h7GbB83kgJTywLRjSOz52JPYe/4jsXcOrcXe6vwWKeWLnvh93GWqW+i/gS+uWLFCf+SRR7R3vOMdi3Ls3t5eqqurGRoaIiAggLy8PDIzM5fkUMCliK842F4vWK1WmpqaaGhowGKxEBsby+rVqxelhktKya9+9Ss+9alP6UajUQL3Av/rqW6f2ZhKs38Xey1MI9CPvSNoD5ANdGNPPVVPe84XgPypf64HioCjQP3U9w5Pr19ZSufa5YYSKwvMtKKtq3FQSnnD1OMPYH9zXY3/llJ+fdoxvgtswe7OGA3o2M2HXga+vwAGS24jhNiradqvdV1Pv/322/nJT35CfHz8gh9XSklXVxc1NTUYjUZCQkLIz8+/rq37FwslVhYHXde5cOEC58+fx2QyERERQUFBAfHx8YvyGm9ra+NDH/oQzz//PEKI81LK90spjy/0cYUQa7F7mewAUoEoYAy7uPgH8KOZUe05nG9/I6V8/7THL7lz7XJBiRWF1xBChAPfAe6Ojo7Wf/rTny5alEXXddra2qitrcVkMhEaGkpOTg5paWkq0rJAKLGysFitVlpbW2lsbGRiYoKQkBBWr15NSkrKoogURzTl05/+tD46OgrwPeBrUsqJBT+4YtmjxIrC6wghbtI07VFd19Nuv/12Hn744QWb4DwTm81GS0sLDQ0NmEwmAgMDycnJISMjQ9W0eBglVhYGs9lMc3MzTU1NmM1mp/BOT09fNC+a9vZ2PvjBD/L888+jaVqdruvvW4xoiuL6QYkVhU8wFWX5LvDh6Oho/ZFHHtHe/va3L9rxHZGWhoYGjEYj/v7+ZGdnk52dTUBAwKLtYzmjxIpnMZlMNDY20tzcjM1mIyIigtzcXJKTkxdNpEgp+fWvf81//ud/6qOjowJ7NOWrKpqi8DRKrCh8CiHEzVNRltR3vOMd/OhHP1rQjqGZOGpa6urqGB4exmAwkJGRQU5ODsHBwYu2j+WIEiueYWxsjIaGBi5cuICu60RHR5OXl7doNSkO2tra+MhHPsI///lPNE2rn4qmHFu0DSiuK5RYUfgcQogI7FGWD4WHh+vf/OY3tbvvvhs/v8XrtJdS0tvbS319PX19fQghSE5OJisri+joaFWM6wJKrLiOlJK+vj6ampro7rZ35iYkJJCbm0tMTMyi7sVsNvP973+f++67T05MTIB9FthXVDRFsZAosaLwWYQQe4QQD0spC9atWycffvhhsWPHjkXfx8DAAI2NjXR1dSGlJDIykqysLFJTU1Ux7jxQYmX+WK1W2traaG5uZnR0FE3TSE5OJicnZ0Emml+LF198kY9//OOyvr5eCCHKpZR3SylPLvpGFNcdSqwofJopX5aPa5p2n67rYe973/v4zne+s6ipIQcTExO0tLTQ2trK5OQk/v7+pKWlkZmZuSgGW0sdJVbmzvDwMC0tLbS3t2O1WgkKCiIzM5PMzEyvTBS/cOEC99xzD0899RSapg3puv554JduOGsrFPNCiRXFkkAIkYQ9NXSnt1JDDmw2G52dnbS0tDAwYLdtiImJITMzk6SkJBVtuQJKrFwdq9XqfF0NDtpNVmNjY8nKyiIxMdErU6YnJyf5/ve/zze+8Q1HyufnwJeklP2LvhnFdY0SK4olxVRq6CdSyjWFhYXy4YcfFjt37vTafkZGRmhpaaGtrQ2r1Yqfnx/JycmkpqYSGxuralumocTK5ei6Tm9vL+3t7XR1dWGz2QgICCA9PZ2MjAzCwsK8trcZKZ8yKeVHVcpH4S2UWFEsOaZSQx8TQnxDShn2zne+k2984xvk5OR4bU+OT8Xt7e309tqHrwYFBZGamkpqaioRERHXvXBRYsWOlJKhoSHa2tro7OxkcnISIQTx8fGkpqZ6PTp37tw5vvSlL/H000+jadqgruufA361kFb5CsW1UGJFsWSZSg19C/g3g8HAhz/8YfGVr3yFxMREr+7LZDLR0dFBW1sbw8P2UVDh4eFO4RISEuLV/XmL612sGI1G2tvbaW9vZ2xsDIAVK1aQmppKSkqKV2pRptPa2srXvvY1HnvsMaZqUf4Pe5ePSvkovI4SK4oljxCiELgfeGNwcLC85557xH/91395pVtiJqOjo84L1Pj4OGC/QCUmJpKYmEh4ePh1E3G53sSKlJLh4WG6u7vp7u52CtfQ0FDS0tJITU0lNDTUy7uEvr4+7r//fh566CFpsVgE8ARwr5Sy/lrPVSgWCyVWFMsGIcRO7LOGtq9YsUK/9957tY9+9KMEBQV5e2tIKRkYGKC9vZ3u7m5MJhMAISEhTuESExPjlSLKxeJ6ECs2m42+vj6nQHH8PwcHB5OUlERqaipRUVE+IVDHxsb4wQ9+wP/+7//qo6OjGvAS8EUpZbm396ZQzESJFcWyQtivAm8UQnxbSlmQmpqqf+Mb39De+973+kyXzpU+cfv5+ZGQkEBiYiLx8fHLzuZ/uYqVyclJLl68SHd3Nz09Pdhs9m7eqKgoEhMTSUpK8qkImsVi4ec//zlf//rX9d7eXm3KL+XzUsp/eXtvCsWVUGJFsSwRQhiA92ia9j+6rqfm5+fLL37xi+Jd73qXzw0oHB8fd17s+vr60HV7HWNkZCSxsbHExsYSExPjc/ueL8tFrJjNZvr7++nr66Ovr4+RkREADAYDcXFxJCYmkpCQ4BMRvemYTCZ+85vf8J3vfEc2NzcLTdMadF3/EvCkVBcChY+jxIpiWSOECALu1jTtC7qux6enp8vPfe5z4q677vLJWT8Wi4Xe3l56enro6+tzFmKC/ZO6Q7gsRfGyVMXKlcQJQFhYGLGxsSQkJBAbG+sV359rMTo6ys9+9jMeeOAB/eLFi5qmaW26rn8Te4ePxdv7UyjmghIriuuCKdHyfk3TPq/remZsbKx+zz33aB/96Ed9ohD3SkxMTNDX1+e8WM4UL9HR0URGRhIVFUVYWJhP17wsBbGi6zqjo6MMDQ0xNDTE4OCgM00Hr4oTh2j0tejJdPr6+vjRj37Ej370I314eFgTQpyXUn4L+L0SKYqlhhIriusKIYQf8A4hxJeklGvCw8Plxz72MfGpT33KKxb+8+Vq4sVgMBAREUFUVJTz5ksCxtfEiq7rjIyMMDw87BQnIyMjzjQcLC1x4qCtrY3vfe97/N///Z+cmJhwGLrdD/xdeaUolipKrCiuS4QQGvAGIcQXpZTbAgMD5Qc+8AHxmc98huzsbG9vb86YzWaGhoYuueA6WqQBNE0jMjKSsLCwS26hoaGLXnDsLbFitVoZGxvDaDRecpspTEJDQy8RepGRkUsq1VZTU8MDDzzAY489Jq1WqwD2Yfch+peqSVEsdZRYUVzXTHUP7Qa+CLxOCMEb3vAGPv7xj/Oa17zGZ6IS88FsNjvFi+N+fHycme/14OBgp3Bx3AcFBREUFERgYKDHu1cWSqzous7k5CSTk5OYTKbLhImjfdiBEMIpTBwptKUmTBzYbDaeffZZHnroIV5++WXHt/8GfEtKWeq9nSkUnkWJFYViCiHEeuATQog7pZSBubm58uMf/7h43/ve59N1LXNB13XGx8cxGo3XvJiD/YIeGBhIYGCgU8A4bgEBAfj5+c16u5q4u5ZYkVKi6zpWqxWbzYbVar3k5hAjjnvHbXJyctb1Zooxxy04OHhJitDp9Pf388tf/pKf/OQn8sKFC0IIMS6l/DXwkJSyxtv7Uyg8jRIrCsUMhBAxwAc0TfuYruvpwcHB8t3vfrf4yEc+QnFxsbe353EcaZKxsbErioErCYKZCCEuES5CCOfNaDQCOF1bpZSXCBSr1XpZ9OdKTBdRMwVVaGioV9JcC42UkiNHjvDII4/wxBNPSIvFIoQQDVLKHwO/kVIOX3MRhWKJosSKQnEFprxa3gh8BHgdIDZs2CDvvvtucccddxAREeHdDS4ijlSLyWTCbDZfFvVw3KZHRHRddwoSwNnyO32ooxACTdMui9AYDIbLvhcQEOAUJ0s9MjIfBgcHefzxx3nkkUdkdXW1AGzAM8AjwEuqaFZxPaDEikIxB4QQWcAHNU37D13X4wIDA+Wb3/xmceedd/K6171u2bnNLgS+1g3ky5hMJv7xj3/w+OOP8+yzz0qr1So0TevQdf3/gF9KKTu8vUeFYjFRYkWhmAdCiADgNuA9Qog3SCn9oqOj9Xe+853anXfeybZt23zGVt3XUGLl6ui6zsGDB/nd737Hn//8ZzkyMiKEEJNSyr8DjwPPSSmt3t6nQuENlFhRKFxECBENvB24E9gFkJWVJe+8805x5513kp+f79X9+RpKrMxOZWUljz/+OL///e/1jo4ODZDY244fB/4ipRy5+goKxfJHiRWFwgMIITKBdwkh3iulXA2wceNG+Y53vEPcdttt5OfnX/cRFyVW7Egpqays5JlnnuFPf/qTrKqqEgBCiNNSyseBP6o0j0JxKUqsKBQeZMq3pQj7EMU7dV1PBFi5cqW87bbbxG233cbOnTt9cobMQnM9i5XJyUkOHjzIM888w9///nfZ1tYmADRNu6Dr+u+A30kpz3l5mwqFz6LEikKxQEy55G4CbhNCvElKWQgQFRWlv/71r9duu+02brnlliXv4TJXrjex0t/fzz//+U+efvppnn/+eWk0Gh0RlHIp5dPA08AZ5S6rUFwbJVYUikVCCJEB/D/s4mWvlNLPYDDIG264Qdx6663s3buXoqKiZecP4mC5ixWr1Up5eTn79+/nueee4/Dhw+i6jhDCLKV8CXu78bMqxaNQzB8lVhQKLyCEiMDu3XKbpmlv1HU9CiAiIkLu2bNH3HDDDdxwww3LSrwsN7HiECcHDhzgwIEDHDp0SI6NjTnSO726rj+DPXryspRy7OqrKRSKq6HEikLhZaYmQa8HbgBuEELskVKGwfISL0tdrFitVk6dOuUUJ6+88opTnAghhqWUBwDHrVKZtSkUnkOJFYXCx5gSLxt4VbzsdoiX8PBwuWnTJrFp0yYct5ycnCXh6LqUxIrNZuP8+fOUl5c7b6dOnZLj4+OOyMmwrusHeFWcnJVS2ry3Y4VieaPEikLh48wQLzs1Tdus63qS4+dhYWFy48aNlwiYvLw8nxMwvipWrFYrtbW1DkFCeXk5FRUVTmECzq6dMuAQSpwoFIuOEisKxRJECJGAvdNoI7BJ07RiXddTHT8PDQ2Va9asEXl5eUy/5ebmEhYW5pU9e1usDA8PU19fT11d3SW36upqOTExMV2YtE4Jk/Kp2ykpZZ9XNq1QKAAlVhSKZYMQIp4p8QJsFEKsBnKklP7TH5eYmKjn5+drDvGSk5NDSkoKSUlJJCQk4O/vP9vybrPQYsVsNtPd3U1XVxcdHR00NDQ4BUltba3e29t7SahJCDEJ1Espa7hUmAwsyAYVCoXLKLGiUCxjpiZHpwN502+apuXrup4GiBmPJyYmRk9JSdGSkpJITk4mKSkJx9cxMTGEhYURHh7uvA8NDZ2TO+98xIqu64yNjTE6OorRaHTe9/X10dnZSVdXF11dXc6vOzo69IGBgdnyXrqmaS26rp8H6mbc2lURrEKxNFBiRaG4ThFCBAErp25JU7dkx9eapqXouh4HXLX9SAhBSEiIDAsLk+Hh4SIiIkKEhYXh5+eHpmnO29DQEFJKVqxYga7rzpvFYsFoNDIyMqJPiRIxvV7kKlg1TevRdb0D6Jq6dU77ugFoklKaXf0bKRQK30CJFYVCcUWmIjNxvCpmooEwIHzaffjM7wkhwqcKg7Wpm2Hq33JqcrDuuEkprVPD+ozA6NTNOOPe8fUArwqSPhUZUSiuD5RYUSgUCoVC4dP4Vm+jQqFQKBQKxQyUWFEoFAqFQuHTKLGiUCgUCoXCp1FiRaFQKBQKhU+jxIpCoVAoFAqfRokVhUKhUCgUPo0SKwqFQqFQKHwaJVYUCoVCoVD4NEqsKBQKhUKh8GmUWFEoFAqFQuHTKLGiUCgUCoXCp1FiRaFQKBQKhU+jxIpCoVAoFAqfRokVhUKhUCgUPo0SKwqFQqFQKHwaJVYUCoVCoVD4NEqsKBQKhUKh8GmUWFEoFAqFQuHTKLGiUCguQQgRI4T4DyHEX4UQDUKICSHEsBDisBDiA0IIbcbjfy2EkNe4/esKx3qfEKJUCGGcOsYBIcQbr7K3TwghGoUQQ1P7S/f0769QKHwPIaX09h4UCoUPIYT4CPBToAvYD1wAEoC3ApHAU8Db5dTJQwjxZmD9FZZ7L5AN/JeU8oEZx3kA+AzQDjwJBADvBKKBT0gpH5rx+A8BjwB/AVqA24EJYIOU0uTGr6xQKHwcJVYUCsUlCCFuBEKBf0gp9WnfTwRKgTTgdinlU9dYJwroBAxAipSyb9rPtgNHgEZgs5RycOr7mUD51PHzpZQt055zFDgmpfzM1L/jsQupN0opX3bvt1YoFL6MSgMpFIpLkFLuk1I+M12oTH2/G3tkA+CGOSz1XiAY+Mt0oTLFR6buv+kQKlPHaAF+AgQC/z7jOQbANu3fNkBOfV+hUCxjlFhRKBTzwTJ1b53DYz84df9/s/zsxqn752f52XMzHuPgd8BnhBB/EkJ8FziBPXJzeA57USgUSxg/b29AoVAsDYQQfsC/Tf1zNpEx/bHbgEKgTkq5f8bPQoEUwCil7Jrl6fVT93kzvv9jIAi4G7gFOAT8p5RybD6/h0KhWHoosaJQKObKt4G1wD+llC9c47Efmrr/+Sw/i5y6H77Ccx3fj5r+zamC3v+duikUiusIlQZSKBTXRAjxSeydO7XYa1Gu9thI4B2AGfi1G4dV1f8KhQJQYkWhUFwDIcTHgAeBamCvlHLgGk95DxDC7IW18GrkJHKWn03//pUiLwqF4jpDiRWFQnFFhBCfAh4CqrALle45PM1RWPuz2X44VWPSAYQJIZJmeUju1H3d/HarUCiWK0qsKBSKWRFCfB74AXAau1DpmcNzSoAi7IW1B67y0H1T97fM8rNbZzxGoVBc5yixolAoLkMI8RXsBbXlwE1XSOfMhqOwdrZ25ek4/Fq+LIRYMe24mcDHgEng0TlvWKFQLGuUg61CobgEIcT7sBfG2rC3C89WO9Iipfz1jOdFYPc98WeGY+0VjvM94B4utdu/A4hhFrt9hUJx/aJalxUKxUyypu4NwKeu8JiDXN7pcyd2m/w/ziUSI6X8jBCiEvg49oiMDpwCviulfHb+21YoFMsVFVlRKBQKhULh06iaFYVCoVAoFD6NEisKhUKhUCh8GiVWFAqFQqFQ+DRKrCgUCoVCofBplFhRKBQKhULh0yixolAoFAqFwqdRYkWhUCgUCoVPo8SKQqFQKBQKn0aJFYVCoVAoFD6NEisKhUKhUCh8GiVWFAqFQqFQ+DRKrCgUCoVCofBplFhRKBQKhULh0yixolAoFAqFwqdRYkWhUCgUCoVPo8SKQqFQKBQKn0aJFYVCoVAoFD7N/wewvwICpJvibAAAAABJRU5ErkJggg==\n", "text/plain": [ "
" ] }, "metadata": { "image/png": { "height": 269, "width": 277 }, "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "ax = plt.subplot(111, polar=True)\n", "plothist(hist9);" ] } ], "metadata": { "kernelspec": { "display_name": "boost-hist", "language": "python", "name": "boost-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.10" } }, "nbformat": 4, "nbformat_minor": 4 } scikit-hep-boost-histogram-b4c49b0/notebooks/MultiCellExample.ipynb000066400000000000000000000256071524660700500255750ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "id": "440ab15f-a5d9-46e8-966f-ae6ab9839d63", "metadata": {}, "source": [ "# MultiCell histograms" ] }, { "cell_type": "markdown", "id": "0ccfc0b8-2e1e-41c3-9d37-b84c7e89d13b", "metadata": {}, "source": [ "Usually, one has to deal with `n` events to histogram by sorting each event into bins based on some variable `x` and by adding the value of a `weight` to the value stored in that bin.\n", "\n", "However, there are also cases where one has to create multiple histograms for multiple independent weights that are all sorted into the same bin per event. Each event has one variable `x` and a list `weights` with a fixed number of `k` weights. A usage example could be to study a nominal weight and some systematic effects that manifest in variations of this weight without changing the the variable for selecting the bin (without changing `x`).\n", "\n", "In such a case, one option would be to create `k` histograms (one for each weight) and sort each event into these `k` histograms (each time providing a different weight). The downsides of this approach is, that one has to do the sorting of the events into the bins `k` times whereas one time would be sufficient. One also has to create `k` histogram objects.\n", "\n", "A more efficient approach would be to have only one histogram that stores a list of `k` weights per bin. This is achieved by using the `boost_histogram.storage.MultiCell` storage." ] }, { "cell_type": "code", "execution_count": 1, "id": "bb508fb8-440d-4349-941a-281216db2381", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "\n", "import boost_histogram as bh" ] }, { "cell_type": "markdown", "id": "375102e8-14b6-4300-8ff8-516e05de0074", "metadata": {}, "source": [ "A `MultiCell` histogram `h` has to be provided with `k`, the fixed number of cells (in this example the weights) per bin, at construction time:" ] }, { "cell_type": "code", "execution_count": 2, "id": "3222b5d6-daec-4d83-8d1f-633fa5bae72f", "metadata": {}, "outputs": [], "source": [ "k = 10\n", "h = bh.Histogram(bh.axis.Integer(0, 7), storage=bh.storage.MultiCell(k))" ] }, { "cell_type": "markdown", "id": "420f519f-5821-4c0d-b673-f73ad3cd7a9c", "metadata": {}, "source": [ "To fill the `MultiCell` histogram with `n` events, one has to provide a 2-dimensional array of weights with the shape `(n, k)` via the `weight` keyword of the `fill()` method:" ] }, { "cell_type": "code", "execution_count": null, "id": "bb4f10fb-7c10-41ef-8c22-908fc751633f", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Histogram(Integer(0, 7), storage=MultiCell(0)) # Sum: [210.0, 217.0, 224.0, 231.0, 238.0, 245.0, 252.0, 259.0, 266.0, 273.0] ([12250.0, 12300.0, 12350.0, 12400.0, 12450.0, 12500.0, 12550.0, 12600.0, 12650.0, 12700.0] with flow)" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "n = 50\n", "x = np.arange(n)\n", "weights = np.arange(n * k).reshape(n, k)\n", "h.fill(x, weight=weights)" ] }, { "cell_type": "markdown", "id": "d6e8b3a4-460c-4d85-8ecf-87f3dc0c849a", "metadata": {}, "source": [ "Now, each bin of `h` stores a list of `k` weights:" ] }, { "cell_type": "code", "execution_count": 31, "id": "e0b97769-cfe2-4323-86ae-33bf37ae6b07", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0]" ] }, "execution_count": 31, "metadata": {}, "output_type": "execute_result" } ], "source": [ "h[1]" ] }, { "cell_type": "markdown", "id": "0310baa6-a38b-4c29-a4b0-d9506b2817a6", "metadata": {}, "source": [ "One can interact with a `MultiCell` identical to any other histogram with a different storage type.\n", "\n", "However, 2 important keypoints to remember:\n", "1. A `view` of a `MultiCell` histogram has the cells (weights) as first dimension. In our case, `h.view()` will have the shape `(k, 7)` with `7` being the number of bins from the `bh.axis.Integer(0, 7)` axis.:" ] }, { "cell_type": "code", "execution_count": 32, "id": "ee5b984f-5549-43ff-b4f6-bd828d1c3255", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[ 0., 10., 20., 30., 40., 50., 60.],\n", " [ 1., 11., 21., 31., 41., 51., 61.],\n", " [ 2., 12., 22., 32., 42., 52., 62.],\n", " [ 3., 13., 23., 33., 43., 53., 63.],\n", " [ 4., 14., 24., 34., 44., 54., 64.],\n", " [ 5., 15., 25., 35., 45., 55., 65.],\n", " [ 6., 16., 26., 36., 46., 56., 66.],\n", " [ 7., 17., 27., 37., 47., 57., 67.],\n", " [ 8., 18., 28., 38., 48., 58., 68.],\n", " [ 9., 19., 29., 39., 49., 59., 69.]])" ] }, "execution_count": 32, "metadata": {}, "output_type": "execute_result" } ], "source": [ "h.view()" ] }, { "cell_type": "markdown", "id": "9dc4980d-591c-4561-be64-e94b426734d9", "metadata": {}, "source": [ "To get the histogram corresponding to the weight with index 3 one can use `h.view()[3]`" ] }, { "cell_type": "markdown", "id": "b7d09f9d-d567-416e-9d21-0f8ae1e65db5", "metadata": {}, "source": [ "2. `MultiCell` histograms do NOT track the variances of their cell contents (weights) (in contrast to histograms with the `Weight` storage). If required, one can track the variances by adding them as another weight to the histogram: " ] }, { "cell_type": "code", "execution_count": null, "id": "4d592b6b-766d-436a-ae45-f43aa19c97e1", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Histogram(Integer(0, 5), storage=MultiCell(0)) # Sum: [100.0, 105.0, 110.0, 115.0, 120.0, 125.0, 130.0, 135.0, 140.0, 145.0, 3000.0] ([12250.0, 12300.0, 12350.0, 12400.0, 12450.0, 12500.0, 12550.0, 12600.0, 12650.0, 12700.0, 4042500.0] with flow)" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "h = bh.Histogram(bh.axis.Integer(0, 5), storage=bh.storage.MultiCell(k + 1))\n", "weights = np.zeros((n, k + 1))\n", "weights[:, :-1] = np.arange(n * k).reshape(n, k)\n", "weights[:, -1] = weights[:, 0] ** 2\n", "h.fill(x, weight=weights)" ] }, { "cell_type": "markdown", "id": "6e2ab208-de48-4ec2-987e-ef11a5863ce8", "metadata": {}, "source": [ "Now, the weight with index `k + 1` is the square of the weight with index `0` and therefore tracks the sum-of-weights-squared of the first weight." ] }, { "cell_type": "markdown", "id": "1eb9fcbf-36b0-41be-b78e-1b44ae17a4f2", "metadata": {}, "source": [ "## Benchmarks" ] }, { "cell_type": "markdown", "id": "69cadcb6-11b6-4fd0-97c9-ec3a2a8871ef", "metadata": {}, "source": [ "Let's estimate how much time one can save with `MultiCell` histograms in a 'real world' example in contrast to using multiple histograms with `Weight` and `Double` storage.\n", "\n", "For a precicsion analysis we have to process 150 million events. We can fill 10000 events per iteration. Each event has 2000 different weights due to systematic variations. We also have some reweighting that increases the number of weights by a factor 10 to 20000 weights. The events are sorted based on two variables into a `8 x 8` histogram that uses `variable` axis.\n", "\n", "\n", "We are only interested in the sum-of-weights-squared for the nominal weight (which would be 10 out of these 20000 in this case), therefore we will use histograms with a `Double` storage type as comparison to the `MultiCell` histogram." ] }, { "cell_type": "code", "execution_count": null, "id": "b8defdfc-bf77-4c54-9123-b8c283730162", "metadata": {}, "outputs": [], "source": [ "n_total = 150_000_000\n", "n_batch = 10_000\n", "k = 20_000\n", "axes = [\n", " bh.axis.Variable(np.linspace(-1, 1, 9)),\n", " bh.axis.Variable(np.linspace(-1, 1, 9)),\n", "]\n", "h_MultiCell = bh.Histogram(*axes, storage=bh.storage.MultiCell(k))\n", "h_doubles = [bh.Histogram(*axes, storage=bh.storage.Double()) for i in range(k)]" ] }, { "cell_type": "code", "execution_count": 51, "id": "ea43b439-c4e3-437c-bcbe-fba1d010d717", "metadata": {}, "outputs": [], "source": [ "rng = np.random.default_rng()\n", "variables = rng.normal(size=(2, n_batch))\n", "weights = rng.normal(size=(n_batch, k))" ] }, { "cell_type": "markdown", "id": "7c39a95f-6050-4edb-96e3-155bd3360341", "metadata": {}, "source": [ "Filling the `Double` type histograms:" ] }, { "cell_type": "code", "execution_count": 53, "id": "9ebdb94b-a39c-4748-bc5b-2ba7fe7bb51d", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "26.7 s ± 1.58 s per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" ] } ], "source": [ "%%timeit\n", "for index, hist in enumerate(h_doubles):\n", " hist.fill(variables[0], variables[1], weight=weights[:, index])" ] }, { "cell_type": "markdown", "id": "c9d5f4e8-2242-4760-92b2-f5f2070c982c", "metadata": {}, "source": [ "Filling the `MultiCell` histogram:" ] }, { "cell_type": "code", "execution_count": null, "id": "a79cec31-a319-4c0e-86ff-4f73276059c3", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "325 ms ± 60.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" ] } ], "source": [ "%%timeit\n", "h_MultiCell.fill(variables[0], variables[1], weight=weights)" ] }, { "cell_type": "markdown", "id": "a989034f-be9e-4f04-b7e7-ba3eaa112688", "metadata": {}, "source": [ "On my test laptop, filling the `k` `Double` type histograms took `26.7 +- 1.6 s` whereas filling the `MultiCell` histogram only took `325 +- 60 ms` which is `82` times faster.\n", "\n", "Therefore, using `MultiCell` histograms instead of multiple `Double` type histograms for filling all 150 million events would save us `110` hours (`1.4 h` for the `MultiCell` filling vs. `111 h` for iterated `Double` type filling)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.12" } }, "nbformat": 4, "nbformat_minor": 5 } scikit-hep-boost-histogram-b4c49b0/notebooks/PerformanceComparison.ipynb000066400000000000000000000206401524660700500266530ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Performance Comparison\n", "\n", "We will compare boost-histogram to numpy." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "from numpy.testing import assert_allclose\n", "\n", "import boost_histogram as bh" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "threads: 8\n" ] } ], "source": [ "import os\n", "\n", "threads = os.cpu_count() // 2\n", "print(f\"threads: {threads}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Testing setup\n", "\n", "This is just a simple 1D and 2D dataset to use for performance runs. The testing setup is the same as \"MBP\" in [this post](https://iscinumpy.gitlab.io/post/histogram-speeds-in-python/), a dual-core MacBook Pro 2015." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "bins = (100, 100)\n", "ranges = ((-3, 3), (-3, 3))\n", "bins = np.asarray(bins).astype(np.int64)\n", "ranges = np.asarray(ranges).astype(np.float64)\n", "\n", "edges = (\n", " np.linspace(*ranges[0, :], bins[0] + 1),\n", " np.linspace(*ranges[1, :], bins[1] + 1),\n", ")" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "np.random.seed(42)\n", "vals = np.random.normal(size=[2, 10_000_000]).astype(np.float32)\n", "vals1d = np.random.normal(size=[10_000_000]).astype(np.float32)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Traditional 1D NumPy Histogram\n", "\n", "This is reasonably optimized; it should provide good performance." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "answer, e = np.histogram(vals1d, bins=bins[0], range=ranges[0])" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "74.5 ms ± 2.37 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" ] } ], "source": [ "%%timeit\n", "h, _ = np.histogram(vals1d, bins=bins[0], range=ranges[0])\n", "assert_allclose(h, answer, atol=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Boost histogram 1D\n" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "41.6 ms ± 712 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" ] } ], "source": [ "%%timeit\n", "hist = bh.Histogram(bh.axis.Regular(bins[0], *ranges[0]), storage=bh.storage.Int64())\n", "hist.fill(vals1d)\n", "assert_allclose(hist, answer, atol=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Boost histogram 1D NumPy clone" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "43.1 ms ± 769 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" ] } ], "source": [ "%%timeit\n", "h, _ = bh.numpy.histogram(vals1d, bins=bins[0], range=ranges[0])\n", "assert_allclose(h, answer, atol=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Boost histogram in 1D, threaded" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "13.3 ms ± 153 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" ] } ], "source": [ "%%timeit\n", "hist = bh.Histogram(bh.axis.Regular(bins[0], *ranges[0]), storage=bh.storage.Int64())\n", "\n", "hist.fill(vals1d, threads=threads)\n", "assert_allclose(hist, answer, atol=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Boost histogram 1D NumPy clone, threaded" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "13.8 ms ± 238 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" ] } ], "source": [ "%%timeit\n", "h, _ = bh.numpy.histogram(vals1d, bins=bins[0], range=ranges[0], threads=threads)\n", "assert_allclose(h, answer, atol=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Traditional 2D NumPy histogram\n", "\n", "Not as well optimized for regular filling." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "answer2, *ledges = np.histogram2d(*vals, bins=bins, range=ranges)" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "874 ms ± 22.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" ] } ], "source": [ "%%timeit\n", "H, *ledges = np.histogram2d(*vals, bins=bins, range=ranges)\n", "assert_allclose(H, answer2, atol=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Boost histogram in 2D" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "77.6 ms ± 615 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" ] } ], "source": [ "%%timeit\n", "hist = bh.Histogram(\n", " bh.axis.Regular(bins[0], *ranges[0]), bh.axis.Regular(bins[1], *ranges[1])\n", ")\n", "hist.fill(*vals)\n", "assert_allclose(hist, answer2, atol=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Boost histogram 2D NumPy clone" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "84.7 ms ± 2.78 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" ] } ], "source": [ "%%timeit\n", "H, *ledges = bh.numpy.histogram2d(*vals, bins=bins, range=ranges)\n", "assert_allclose(H, answer2, atol=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Boost histogram in 2D, threaded" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "28.7 ms ± 708 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" ] } ], "source": [ "%%timeit\n", "hist = bh.Histogram(\n", " bh.axis.Regular(bins[0], *ranges[0]), bh.axis.Regular(bins[1], *ranges[1])\n", ")\n", "\n", "hist.fill(*vals, threads=threads)\n", "assert_allclose(hist, answer2, atol=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Boost histogram 2D NumPy clone, threaded" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "29.6 ms ± 503 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" ] } ], "source": [ "%%timeit\n", "H, *ledges = bh.numpy.histogram2d(*vals, bins=bins, range=ranges, threads=threads)\n", "assert_allclose(H, answer2, atol=1)" ] } ], "metadata": { "kernelspec": { "display_name": "boost-hist", "language": "python", "name": "boost-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.12.8" } }, "nbformat": 4, "nbformat_minor": 4 } scikit-hep-boost-histogram-b4c49b0/notebooks/SimpleExample.ipynb000066400000000000000000000065001524660700500251230ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Simple Example\n", "\n", "Let's try some basic functionality of boost-histogram:" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "\n", "import boost_histogram as bh" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "vals = np.random.normal(size=(2, 1_000_000))" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Histogram(\n", " Regular(10, 0, 10, metadata='x', transform=sqrt),\n", " Regular(10, 0, 1, circular=True, metadata='y'),\n", " storage=Int64())" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "hist = bh.Histogram(\n", " bh.axis.Regular(10, 0, 10, metadata=\"x\", transform=bh.axis.transform.sqrt),\n", " bh.axis.Regular(10, 0, 1, circular=True, metadata=\"y\"),\n", " storage=bh.storage.Int64(),\n", ")\n", "hist" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This fills the histogram" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Histogram(\n", " Regular(10, 0, 10, metadata='x', transform=sqrt),\n", " Regular(10, 0, 1, circular=True, metadata='y'),\n", " storage=Int64()) # Sum: 499682.0 (1000000.0 with flow)" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "hist.fill(*vals)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's just take a quick look at the bin contents:" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[[ 3955 3992 3972 3948 3930 3987 4073 3982 3878 4073]\n", " [11519 11675 11560 11528 11612 11575 11476 11643 11573 11603]\n", " [16197 16087 16261 16043 15775 15912 16067 16010 15973 15998]\n", " [12843 12956 12855 12912 12870 12940 12962 12891 12752 13073]\n", " [ 4807 4952 4844 4859 4869 4788 4657 4931 4775 4874]\n", " [ 636 618 598 608 633 644 603 635 611 645]\n", " [ 14 18 20 13 12 21 10 18 15 22]\n", " [ 0 0 0 0 1 0 0 0 0 0]\n", " [ 0 0 0 0 0 0 0 0 0 0]\n", " [ 0 0 0 0 0 0 0 0 0 0]]\n" ] } ], "source": [ "print(hist.view())" ] } ], "metadata": { "kernelspec": { "display_name": "boost-hist", "language": "python", "name": "boost-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.12.8" } }, "nbformat": 4, "nbformat_minor": 4 } scikit-hep-boost-histogram-b4c49b0/notebooks/ThreadedFills.ipynb000066400000000000000000000110741524660700500250720ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Threaded Fills" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "from concurrent.futures import ThreadPoolExecutor\n", "from functools import reduce\n", "from operator import add\n", "\n", "import numpy as np\n", "from numpy.testing import assert_array_equal\n", "\n", "import boost_histogram as bh" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This notebook explores parallel filling by hand (not using the `threads=` argument)." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "hist_linear = bh.Histogram(bh.axis.Regular(100, 0, 1))\n", "hist_atomic = bh.Histogram(bh.axis.Regular(100, 0, 1), storage=bh.storage.AtomicInt64())\n", "\n", "vals = np.random.rand(10_000_000)\n", "hist_answer = hist_linear.fill(vals).copy()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This is a traditional fill." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "25.5 ms ± 774 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" ] } ], "source": [ "%%timeit\n", "hist_linear.reset()\n", "hist_linear.fill(vals)\n", "assert_array_equal(hist_answer, hist_linear)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This is a single threaded atomic fill." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "59.9 ms ± 2.19 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" ] } ], "source": [ "%%timeit\n", "hist_atomic.reset()\n", "hist_atomic.fill(vals)\n", "assert_array_equal(hist_answer, hist_atomic)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This is a threaded fill (storage not threadsafe, so will get the wrong result; just for comparison)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This is a threaded fill, this time with atomics. It may not be faster, but is useful in situations where you are filling from multiple places in your code." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "61.2 ms ± 1.57 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" ] } ], "source": [ "%%timeit\n", "hist_atomic.reset()\n", "threads = 4\n", "with ThreadPoolExecutor(threads) as pool:\n", " for chunk in np.array_split(vals, threads):\n", " pool.submit(hist_atomic.fill, chunk)\n", "assert_array_equal(hist_answer, hist_atomic)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This makes four separate histograms, then fills them and adds at the end." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "def fun(x):\n", " hist = bh.Histogram(bh.axis.Regular(100, 0, 1))\n", " return hist.fill(x)" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "8.12 ms ± 90 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" ] } ], "source": [ "%%timeit\n", "threads = 4\n", "with ThreadPoolExecutor(threads) as pool:\n", " results = pool.map(fun, np.array_split(vals, threads))\n", "hist_quad = reduce(add, results)\n", "assert_array_equal(hist_answer, hist_quad)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The expense of creating the histogram and summing them must be significantly less than the cost of filling for this to be faster." ] } ], "metadata": { "kernelspec": { "display_name": "boost-hist", "language": "python", "name": "boost-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.12.8" } }, "nbformat": 4, "nbformat_minor": 4 } scikit-hep-boost-histogram-b4c49b0/notebooks/xarray.ipynb000066400000000000000000006274551524660700500237060ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# XArray Example\n", "\n", "To run this example, an environment.yml similar to this one could be used:\n", "\n", "```yaml\n", "name: bh_xhistogram\n", "channels:\n", " - conda-forge\n", "dependencies:\n", " - python==3.9\n", " - boost-histogram\n", " - xhistogram\n", " - matplotlib\n", " - netcdf4\n", "```" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "import numpy as np\n", "import xarray as xr\n", "from xhistogram.xarray import histogram as xhistogram\n", "\n", "import boost_histogram as bh" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's look at using boost-histogram to imitate the xhistogram package by reading and producing xarrays. As a reminder, xarray is a sort of generalized Pandas library, supporting ND labeled and indexed data." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Simple 1D example\n", "\n", "We will start with the first example from the xhistogram docs:" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "da = xr.DataArray(np.random.randn(100, 30), dims=[\"time\", \"x\"], name=\"foo\")\n", "bins = np.linspace(-4, 4, 20)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### xhistogram\n", "\n", "And, this is what historamming and plotting looks like:" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
<xarray.DataArray 'histogram_foo' (foo_bin: 19)>\n",
       "array([  2,   1,   2,  22,  56, 135, 234, 375, 453, 496, 456, 346, 226,\n",
       "        96,  65,  25,   7,   3,   0])\n",
       "Coordinates:\n",
       "  * foo_bin  (foo_bin) float64 -3.789 -3.368 -2.947 -2.526 ... 2.947 3.368 3.789
" ], "text/plain": [ "\n", "array([ 2, 1, 2, 22, 56, 135, 234, 375, 453, 496, 456, 346, 226,\n", " 96, 65, 25, 7, 3, 0])\n", "Coordinates:\n", " * foo_bin (foo_bin) float64 -3.789 -3.368 -2.947 -2.526 ... 2.947 3.368 3.789" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "image/png": "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", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "h = xhistogram(da, bins=[bins])\n", "display(h)\n", "h.plot()\n", "# h is an xarray" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### boost-histogram (direct usage)\n", "\n", "Let's first just try this by hand, to see how it works. This will not return an xarray, etc." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "image/png": "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", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "bh_bins = bh.axis.Regular(19, -4, 4)\n", "bh_hist = bh.Histogram(bh_bins).fill(np.asarray(da).flatten())\n", "plt.plot(bh_hist.axes[0].centers, bh_hist.values());" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### boost-histogram (adapter function)\n", "\n", "Now, let's make an adaptor for boost-histogram." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "def bh_xhistogram(*args, bins):\n", " # Convert bins to boost-histogram axes first\n", " prepare_bins = (bh.axis.Variable(b) for b in bins)\n", " h = bh.Histogram(*prepare_bins)\n", "\n", " # We need flat NP arrays for filling\n", " prepare_fill = (np.asarray(a).flatten() for a in args)\n", " h.fill(*prepare_fill)\n", "\n", " # Now compute the xarray output.\n", " return xr.DataArray(\n", " h.values(),\n", " name=\"_\".join(a.name for a in args) + \"_histogram\",\n", " coords=[\n", " (f\"{a.name}_bin\", arr.flatten(), a.attrs)\n", " for a, arr in zip(args, h.axes.centers, strict=False)\n", " ],\n", " )" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
<xarray.DataArray 'foo_histogram' (foo_bin: 19)>\n",
       "array([  2.,   1.,   2.,  22.,  56., 135., 234., 375., 453., 496., 456.,\n",
       "       346., 226.,  96.,  65.,  25.,   7.,   3.,   0.])\n",
       "Coordinates:\n",
       "  * foo_bin  (foo_bin) float64 -3.789 -3.368 -2.947 -2.526 ... 2.947 3.368 3.789
" ], "text/plain": [ "\n", "array([ 2., 1., 2., 22., 56., 135., 234., 375., 453., 496., 456.,\n", " 346., 226., 96., 65., 25., 7., 3., 0.])\n", "Coordinates:\n", " * foo_bin (foo_bin) float64 -3.789 -3.368 -2.947 -2.526 ... 2.947 3.368 3.789" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "image/png": "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", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "h = bh_xhistogram(da, bins=[bins])\n", "display(h)\n", "h.plot();" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### More features\n", "\n", "Let's add a few more features to our function defined above.\n", "* Let's allow bins to be a list of axes or even a completely prepared histogram; this will allow us to take advantage of boost-histogram features later.\n", "* Let's add a weights keyword so we can do weighted histograms as well." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "def bh_xhistogram(*args, bins, weights=None):\n", " \"\"\"\n", " bins is either a histogram, a list of axes, or a list of bins\n", " \"\"\"\n", "\n", " if isinstance(bins, bh.Histogram):\n", " h = bins\n", " else:\n", " prepare_bins = (\n", " b if isinstance(b, bh.axis.Axis) else bh.axis.Variable(b) for b in bins\n", " )\n", " h = bh.Histogram(*prepare_bins)\n", "\n", " prepare_fill = (np.asarray(a).flatten() for a in args)\n", "\n", " if weights is None:\n", " h.fill(*prepare_fill)\n", " else:\n", " prepared_weights, *_ = xr.broadcast(weights, *args)\n", " h.fill(*prepare_fill, weight=np.asarray(prepared_weights).flatten())\n", "\n", " return xr.DataArray(\n", " h.values(),\n", " name=\"_\".join(a.name for a in args) + \"_histogram\",\n", " coords=[\n", " (f\"{a.name}_bin\", arr.flatten(), a.attrs)\n", " for a, arr in zip(args, h.axes.centers, strict=False)\n", " ],\n", " )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 2D example\n", "\n", "This mirrors the xhistogram docs, but uses a small synthetic ocean-like dataset so the notebook stays self-contained and reliable in CI." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "lev = xr.DataArray(np.linspace(0, 5000, 16), dims=[\"lev\"], attrs={\"units\": \"m\"})\n", "lat = xr.DataArray(\n", " np.linspace(-87.5, 87.5, 36), dims=[\"lat\"], attrs={\"units\": \"degrees_north\"}\n", ")\n", "lon = xr.DataArray(\n", " np.linspace(2.5, 357.5, 72), dims=[\"lon\"], attrs={\"units\": \"degrees_east\"}\n", ")\n", "\n", "lev3, lat3, lon3 = xr.broadcast(lev, lat, lon)\n", "lat_radians = np.deg2rad(lat3)\n", "lon_radians = np.deg2rad(lon3)\n", "\n", "ds = xr.Dataset(\n", " data_vars={\n", " \"tmn\": 25 - 0.0045 * lev3 - 20 * np.sin(lat_radians) ** 2 + np.cos(lon_radians),\n", " \"smn\": 34.5\n", " + 1.6 * np.cos(lat_radians) ** 2\n", " + 0.3 * np.sin(lon_radians)\n", " - 0.00015 * lev3,\n", " \"omn\": 240\n", " - 0.03 * lev3\n", " + 35 * np.cos(lat_radians)\n", " + 5 * np.sin(2 * lon_radians),\n", " }\n", ")" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "sbins = np.arange(31, 38, 0.025)\n", "tbins = np.arange(-2, 32, 0.1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### xhistogram" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/usr/local/Caskroom/miniconda/base/envs/xtest/lib/python3.8/site-packages/xarray/core/computation.py:601: RuntimeWarning: divide by zero encountered in log10\n", " result_data = func(*input_data)\n" ] }, { "data": { "text/plain": [ "" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" }, { "data": { "image/png": "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", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "hTS = xhistogram(ds.smn, ds.tmn, bins=[sbins, tbins])\n", "np.log10(hTS.T).plot(levels=31)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### boost-histogram\n", "\n", "We could hand in the same bin definitions, but let's use boost-histogram axes instead:" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" }, { "data": { "image/png": "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", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "sax = bh.axis.Regular(250, 31, 38)\n", "tax = bh.axis.Regular(340, -2, 32)\n", "\n", "hTS = bh_xhistogram(ds.smn, ds.tmn, bins=[sax, tax])\n", "np.log10(hTS.T).plot(levels=31)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Speed comparson" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "17.6 ms ± 508 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" ] } ], "source": [ "%%timeit\n", "hTS = xhistogram(ds.smn, ds.tmn, bins=[sbins, tbins])" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "4.7 ms ± 245 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" ] } ], "source": [ "%%timeit\n", "hTS = bh_xhistogram(ds.smn, ds.tmn, bins=[sax, tax])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Weighted histogram" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's try a more complex example from the docs; the dVol weights one:" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" }, { "data": { "image/png": "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", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "dz = np.diff(ds.lev)\n", "dz = np.insert(dz, 0, dz[0])\n", "dz = xr.DataArray(dz, coords={\"lev\": ds.lev}, dims=\"lev\")\n", "\n", "dVol = dz * (5 * 110e3) * (5 * 110e3 * np.cos(ds.lat * np.pi / 180))\n", "\n", "hTSw = bh_xhistogram(ds.smn, ds.tmn, bins=[sax, tax], weights=dVol)\n", "np.log10(hTSw.T).plot(levels=31, vmin=11.5, vmax=16, cmap=\"brg\")" ] } ], "metadata": { "kernelspec": { "display_name": "boost-hist", "language": "python", "name": "boost-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.10" } }, "nbformat": 4, "nbformat_minor": 4 } scikit-hep-boost-histogram-b4c49b0/noxfile.py000066400000000000000000000103761524660700500213340ustar00rootroot00000000000000# /// script # dependencies = ["nox>=2025.2.9"] # /// from __future__ import annotations import argparse import shutil from pathlib import Path import nox nox.needs_version = ">=2025.2.9" nox.options.default_venv_backend = "uv|virtualenv" DIR = Path(__file__).parent.resolve() @nox.session def tests(session: nox.Session) -> None: """ Run the unit and regular tests. """ opts = ( ["--reinstall-package=boost-histogram"] if session.venv_backend == "uv" else [] ) args = session.posargs or ["-n", "auto", "--benchmark-disable"] pyproject = nox.project.load_toml("pyproject.toml") session.install(*nox.project.dependency_groups(pyproject, "test")) session.install("-v", "-e.", *opts, silent=False) session.run("pytest", *args) @nox.session(default=False) def hist(session: nox.Session) -> None: """ Run Hist's test suite """ session.install(".") tmpdir = session.create_tmp() session.chdir(tmpdir) shutil.rmtree("hist", ignore_errors=True) session.run("git", "clone", "https://github.com/scikit-hep/hist", external=True) session.chdir("hist") session.install( ".", "--group=test", "--group=plot", "mypy", "pandas-stubs", "numpy<2.5" ) session.run("pytest", *session.posargs) session.run( "mypy", "--disable-error-code=override", "--disable-error-code=unused-ignore", ) @nox.session(reuse_venv=True, default=False) def docs(session: nox.Session) -> None: """ Build the docs. Pass --non-interactive to avoid serving. Pass "-b linkcheck" to check links. """ pyproject = nox.project.load_toml("pyproject.toml") parser = argparse.ArgumentParser() parser.add_argument( "-b", dest="builder", default="html", help="Build target (default: html)" ) args, posargs = parser.parse_known_args(session.posargs) serve = args.builder == "html" and session.interactive extra_installs = ["sphinx-autobuild"] if serve else [] session.install(*nox.project.dependency_groups(pyproject, "docs"), *extra_installs) shared_args = ( "-n", # nitpicky mode "-T", # full tracebacks f"-b={args.builder}", "docs", *(posargs or [f"docs/_build/{args.builder}"]), ) if serve: session.run("sphinx-autobuild", "--open-browser", *shared_args) else: session.run("sphinx-build", "--keep-going", *shared_args) @nox.session(default=False) def build_api_docs(session: nox.Session) -> None: """ Build (regenerate) API docs. """ pyproject = nox.project.load_toml("pyproject.toml") session.install(*nox.project.dependency_groups(pyproject, "docs")) session.run( "sphinx-apidoc", "-o", "docs/api/", "--no-toc", "--template", "docs/template/", "--force", "--module-first", "src/boost_histogram", ) @nox.session def lint(session: nox.Session) -> None: """ Run the linter. """ session.install("prek") session.run("prek", "run", "--all-files", *session.posargs) @nox.session def pylint(session: nox.Session) -> None: """ Run pylint. """ session.install("pylint==4.0.*") session.install(".") session.run("pylint", "boost_histogram", *session.posargs) @nox.session(default=False) def make_pickle(session: nox.Session) -> None: """ Make a pickle file for this version """ pyproject = nox.project.load_toml("pyproject.toml") session.install(".", *nox.project.dependency_groups(pyproject, "dev")) session.run("python", "tests/pickles/make_pickle.py", *session.posargs) @nox.session(default=False, venv_backend=None) def bump_boost(session: nox.Session) -> None: """ Bump boost. Requires a version number. """ parser = argparse.ArgumentParser() parser.add_argument("version", help="Version to checkout") args = parser.parse_args(session.posargs) extern = DIR / "extern" for path in extern.iterdir(): session.chdir(path) session.run("git", "fetch", external=True) session.run( "git", "switch", "--detach", f"boost-{args.version}", external=True, success_codes=[0, 128], ) if __name__ == "__main__": nox.main() scikit-hep-boost-histogram-b4c49b0/pyproject.toml000066400000000000000000000224221524660700500222250ustar00rootroot00000000000000[build-system] requires = ["scikit-build-core>=0.11", "pybind11>=3"] build-backend = "scikit_build_core.build" [project] name = "boost-histogram" dynamic = ["version"] description = "The Boost::Histogram Python wrapper." readme = "README.md" requires-python = ">=3.10" authors = [ { name = "Hans Dembinski", email = "hans.dembinski@gmail.com" }, { name = "Henry Schreiner", email = "hschrein@cern.ch" }, ] keywords = [ "boost-histogram", "histogram", ] license = "BSD-3-Clause AND BSL-1.0" license-files = [ "LICENSE", "extern/*/LICENSE" ] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Operating System :: Unix", "Programming Language :: C++", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.15", "Programming Language :: Python :: Free Threading :: 3 - Stable", "Programming Language :: Python :: Free Threading", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python", "Topic :: Scientific/Engineering :: Information Analysis", "Topic :: Scientific/Engineering :: Mathematics", "Topic :: Scientific/Engineering :: Physics", "Topic :: Scientific/Engineering", "Topic :: Software Development", "Topic :: Utilities", "Typing :: Typed", ] dependencies = [ "numpy>=1.21.3", ] [project.entry-points.pytest11] boost_histogram = "boost_histogram._pytest" [project.urls] "Bug Tracker" = "https://github.com/scikit-hep/boost-histogram/issues" Changelog = "https://boost-histogram.readthedocs.io/en/latest/CHANGELOG.html" Chat = "https://gitter.im/HSF/PyHEP-histogramming" Discussions = "https://github.com/scikit-hep/boost-histogram/discussions" Documentation = "https://boost-histogram.readthedocs.io/" Homepage = "https://github.com/scikit-hep/boost-histogram" [dependency-groups] dev = [ { include-group = "test" }, "ipykernel", "nbconvert", "setuptools_scm", "typer", ] docs = [ "ipython", "myst_parser>=0.13", "nbsphinx", "numpy>=1.21.3", "sphinx-book-theme>=0.0.33", "sphinx>=4.0", "sphinx_copybutton", ] examples = [ "matplotlib", "netCDF4", "numba", "uproot", "xarray", "xhistogram", ] test = [ "cloudpickle", "pytest>=6.0", "pytest-benchmark", "hypothesis>=6.0; sys_platform != 'ios'", # slow on iOS, can fail health check "pytest-xdist; sys_platform != 'ios'", # No processes on iOS "jsonschema", # validate UHI serialization against the uhi schema "uhi>=1.1", # ships the histogram JSON schema with metadata-only storage support ] github = [ { include-group = "test" }, "setuptools_scm", "pytest-github-actions-annotate-failures" ] [tool.scikit-build] minimum-version = "build-system.requires" metadata.version.provider = "scikit_build_core.metadata.setuptools_scm" sdist.exclude = [ "extern/**/*.py", "extern/**/*.md", "extern/**/*.rst", "extern/**/*.toml", "extern/*/checks/**", "extern/*/test/**", "extern/*/benchmark/**", "extern/*/doc/**", "extern/*/examples/**", "extern/*/tools/**", "extern/*/README.md", ] logging.level = "INFO" [[tool.scikit-build.generate]] path = "boost_histogram/version.py" template = ''' __version__ = version = "$version" ''' [tool.setuptools_scm] [tool.uv] pip.only-binary = ["numpy"] cache-keys = [ { file = "pyproject.toml" }, { file = "CMakeLists.txt" }, { file = "include/**" }, { file = "src/**/*.cpp" }, ] [tool.pytest.ini_options] minversion = "6.0" addopts = [ "-ra", "--strict-markers", "--strict-config", "--import-mode=importlib", ] filterwarnings = [ "error", "default:could not create cache path:pytest.PytestCacheWarning", # Editable installs import the _boost_histogram_editable shim at interpreter # startup (via .pth), so pytest cannot assertion-rewrite it once our pytest11 # plugin is registered. We do not want our own library rewritten anyway. # pytest's fix for this (PR #10231, v7.2.0) only skips setuptools-style # "__editable__*" finders, not scikit-build-core's "__editable" name. # Fixed in pyest 7.2.0 "ignore:Module already imported so cannot be rewritten:pytest.PytestAssertRewriteWarning", ] xfail_strict = true testpaths = ["tests"] required_plugins = ["pytest-benchmark"] log_level = "DEBUG" [tool.mypy] python_version = "3.10" files = ["src", "examples"] strict = true enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] warn_unreachable = true [[tool.mypy.overrides]] module = "tests.*" disallow_untyped_defs = false disallow_untyped_calls = false [[tool.mypy.overrides]] module = ["matplotlib.*", "typer"] ignore_missing_imports = true [tool.cibuildwheel] build-frontend = "build[uv]" test-groups = ["test"] test-command = "python -m pytest -n auto --benchmark-disable tests" test-sources = ["pyproject.toml", "tests"] test-environment.CI = "1" # Hypothesis needs this on GraalPy test-skip = [ "cp310-win_arm64", "cp31*-musllinux_*", # Threading test crashes "cp315t-win32", # pytest crashes with an access violation on 3.15.0rc1 "gp312_*", # No numpy wheels yet "*-ios_*", # Fails on CI with no device found ] enable = ["pypy", "graalpy"] environment-pass = ["SETUPTOOLS_SCM_PRETEND_VERSION"] environment.PIP_ONLY_BINARY = "numpy" environment.PIP_PREFER_BINARY = "1" environment.UV_ONLY_BINARY = "numpy" environment.UV_PREFER_BINARY = "1" [tool.cibuildwheel.pyodide] build-frontend = {name = "pyodide-build", args = ["--exports", "whole_archive"]} test-command = "pytest --benchmark-disable tests" [tool.cibuildwheel.ios] build-frontend = "build" xbuild-tools = ["cmake", "ninja"] test-command = "python -m pytest --benchmark-disable tests" environment.PIP_EXTRA_INDEX_URL = "https://pypi.anaconda.org/beeware/simple/" # [tool.cibuildwheel.android] # build-frontend = "build" # xbuild-tools = ["cmake", "ninja"] # test-command = "pytest --benchmark-disable tests" # environment.PIP_EXTRA_INDEX_URL = "https://chaquo.com/pypi-13.1" [[tool.cibuildwheel.overrides]] select = ["gp*"] inherit.environment = "append" environment.UV_INDEX = "https://www.graalvm.org/python/wheels/" environment.UV_INDEX_STRATEGY = "unsafe-best-match" test-command = "pytest --benchmark-disable tests" [tool.pylint] py-version = "3.10" ignore-patterns = ['.*\.pyi'] ignore = "version.py" extension-pkg-allow-list = ["boost_histogram._core"] reports.output-format = "colorized" similarities.ignore-imports = "yes" messages_control.disable = [ "cyclic-import", # TODO: move files out of _internal "fixme", "invalid-name", "line-too-long", "missing-class-docstring", "missing-function-docstring", "missing-module-docstring", "c-extension-no-member", # C extensions mess with this "protected-access", "too-few-public-methods", "too-many-arguments", "too-many-branches", "too-many-lines", "too-many-locals", "too-many-return-statements", "too-many-statements", "too-many-positional-arguments", "wrong-import-position", "unused-argument", # Covered by ruff "unsubscriptable-object", # Wrongly triggered ] [tool.ruff.lint] extend-select = [ "ARG", # flake8-unused-arguments "B", # flake8-bugbear "C4", # flake8-comprehensions "EXE", # flake8-executable "FURB", # refurb "G", # flake8-logging-format "I", # isort "ICN", # flake8-import-conventions "ISC", # flake8-implicit-str-concat "PD", # pandas-vet "PGH", # pygrep-hooks "PIE", # flake8-pie "PL", # pylint "PT", # flake8-pytest-style "PTH", # flake8-use-pathlib "PYI", # flake8-pyi "TID251", # flake8-tidy-imports.banned-api "RET", # flake8-return "RUF", # Ruff-specific "SIM", # flake8-simplify "T20", # flake8-print "UP", # pyupgrade "YTT", # flake8-2020 ] ignore = [ "PLR09", # Too many X "PLR2004", # Magic value in comparison "PT013", # from pytest import approx is fine "PT011", # pytest.raises too broad "PYI034", # We are returning Self, just generic ] typing-modules = ["boost_histogram._compat.typing"] isort.required-imports = ["from __future__ import annotations"] exclude = ["extern/*"] [tool.ruff.lint.flake8-tidy-imports.banned-api] "typing.Self".msg = "Use boost_histogram._compat.typing.Self instead." "typing_extensions.Self".msg = "Use boost_histogram._compat.typing.Self instead." [tool.ruff.lint.mccabe] max-complexity = 13 [tool.ruff.lint.per-file-ignores] "docs/*" = ["T20"] "examples/*" = ["T20"] "scripts/*" = ["T20"] "tests/*" = ["T20"] "notebooks/*" = ["T20"] "*.pyi" = ["F401", "TID251"] "docs/conf.py" = ["ARG001"] "src/boost_histogram/test.py" = ["PT"] "**.ipynb" = ["I002", "E731"] "src/boost_histogram/_compat/**.py" = ["TID251"] scikit-hep-boost-histogram-b4c49b0/scripts/000077500000000000000000000000001524660700500207765ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/scripts/performance_report.py000077500000000000000000000110531524660700500252470ustar00rootroot00000000000000#!/usr/bin/env python from __future__ import annotations import math import multiprocessing from timeit import timeit print("Welcome to boost-histogram's performance report") setup_1d = """ import numpy as np import boost_histogram as bh bins=100 ranges=(-1,1) bins = np.asarray(bins).astype(np.int64) ranges = np.asarray(ranges).astype(np.float64) edges = np.linspace(*ranges, bins+1) np.random.seed(42) vals = np.random.normal(size=[50_000_000]).astype(np.float32) """ setup_2d = """ import numpy as np import boost_histogram as bh bins=(100, 100) ranges=((-1,1),(-1,1)) bins = np.asarray(bins).astype(np.int64) ranges = np.asarray(ranges).astype(np.float64) edges = (np.linspace(*ranges[0,:], bins[0]+1), np.linspace(*ranges[1,:], bins[1]+1)) np.random.seed(42) vals = np.random.normal(size=[2, 5_000_000]).astype(np.float64) """ def timer(setup, statement, n=10): t = timeit(statement, setup, number=n) return (t * 1000) / n def print_timer(setup, statement, name, storage, fill, flow, base=None, n=10): time = timer( setup, statement.format(name=name, storage=storage, fill=fill, flow=flow), n ) speedup = 1 if base is None else base / time print( table_line.format( name=name, storage=storage, fill=fill, flow=str(flow), time=time, speedup=speedup, ) ) return time c = multiprocessing.cpu_count() counts = [c // 2**x for x in reversed(range(int(math.log2(c) + 1)))] table_header = "| Type | Storage | Threads | Flow | Time | Speedup |" table_spacer = "|-------|---------------|----------|------|-----------|---------|" table_line = ( "|{name:^7}|{storage:^15}|{fill:^9}|{flow:<6}|{time:>7.4g} ms |{speedup:>7.2g}x |" ) print() print("### 1D, 10 runs each") print() print(table_header) print(table_spacer) base = print_timer( setup_1d, "h, _ = np.histogram(vals, bins=bins, range=ranges)", name="NumPy", storage="uint64", fill="", flow=False, ) print_timer( setup_1d, "hist = bh.Histogram(bh.axis.{name}(bins, *ranges, underflow={flow}, overflow={flow}), storage=bh.storage.{storage}()); hist.fill(vals)", name="Regular", storage="Int64", fill="", flow=False, base=base, ) print_timer( setup_1d, "hist = bh.Histogram(bh.axis.{name}(bins, *ranges, underflow={flow}, overflow={flow}), storage=bh.storage.{storage}()); hist.fill(vals)", name="Regular", storage="Int64", fill="", flow=True, base=base, ) for fill in counts: print_timer( setup_1d, "hist = bh.Histogram(bh.axis.{name}(bins, *ranges, underflow={flow}, overflow={flow}), storage=bh.storage.{storage}()); hist.fill(vals, threads={fill})", name="Regular", storage="Int64", fill=fill, flow=True, base=base, ) for fill in counts: print_timer( setup_1d, "hist = bh.Histogram(bh.axis.{name}(bins, *ranges, underflow={flow}, overflow={flow}), storage=bh.storage.{storage}()); hist.fill(vals, threads={fill})", name="Regular", storage="AtomicInt64", fill=fill, flow=True, base=base, ) print() print("### 2D, 10 runs each") print() print(table_header) print(table_spacer) base = print_timer( setup_2d, "H, *ledges = np.histogram2d(*vals, bins=bins, range=ranges)", name="NumPy", storage="uint64", fill="", flow=False, ) print_timer( setup_2d, "hist = bh.Histogram(bh.axis.{name}(bins[1], *ranges[0], underflow={flow}, overflow={flow}), bh.axis.{name}(bins[1], *ranges[1], underflow={flow}, overflow={flow}), storage=bh.storage.{storage}()); hist.fill(*vals)", name="Regular", storage="Int64", fill="", flow=False, base=base, ) for fill in counts: print_timer( setup_2d, "hist = bh.Histogram(bh.axis.{name}(bins[1], *ranges[0], underflow={flow}, overflow={flow}), bh.axis.{name}(bins[1], *ranges[1], underflow={flow}, overflow={flow}), storage=bh.storage.{storage}()); hist.fill(*vals, threads={fill})", name="Regular", storage="Int64", fill=fill, flow=False, base=base, ) for fill in counts: print_timer( setup_2d, "hist = bh.Histogram(bh.axis.{name}(bins[1], *ranges[0], underflow={flow}, overflow={flow}), bh.axis.{name}(bins[1], *ranges[1], underflow={flow}, overflow={flow}), storage=bh.storage.{storage}()); hist.fill(*vals, threads={fill})", name="Regular", storage="AtomicInt64", fill=fill, flow=False, base=base, ) scikit-hep-boost-histogram-b4c49b0/src/000077500000000000000000000000001524660700500200765ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/src/boost_histogram/000077500000000000000000000000001524660700500233015ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/src/boost_histogram/__init__.py000066400000000000000000000020471524660700500254150ustar00rootroot00000000000000from __future__ import annotations import sys try: from . import _core # noqa: F401 except ImportError as err: if "_core" not in str(err): raise new_msg = "Did you forget to compile boost-histogram? Use CMake or scikit-build-core to build, see the readme." if sys.version_info >= (3, 11): err.add_note(new_msg) raise total_msg = f"{err}\n{new_msg}" new_exception = type(err)(total_msg, name=err.name, path=err.path) raise new_exception from err from . import accumulators, axis, numpy, storage from .histogram import Histogram, IndexingExpr, Kind from .tag import ( # pylint: disable=redefined-builtin loc, overflow, rebin, sum, underflow, ) # pylint: disable-next=import-error from .version import version as __version__ __all__ = [ "Histogram", "IndexingExpr", "Kind", "__version__", "accumulators", "axis", "loc", "numpy", "overflow", "rebin", "storage", "sum", "underflow", ] def __dir__() -> list[str]: return __all__ scikit-hep-boost-histogram-b4c49b0/src/boost_histogram/_compat/000077500000000000000000000000001524660700500247235ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/src/boost_histogram/_compat/__init__.py000066400000000000000000000000001524660700500270220ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/src/boost_histogram/_compat/typing.py000066400000000000000000000004351524660700500266110ustar00rootroot00000000000000from __future__ import annotations import sys import typing if sys.version_info >= (3, 11): from typing import Self elif typing.TYPE_CHECKING: from typing_extensions import Self else: Self = typing.Any __all__ = ["Self"] def __dir__() -> list[str]: return __all__ scikit-hep-boost-histogram-b4c49b0/src/boost_histogram/_core/000077500000000000000000000000001524660700500243705ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/src/boost_histogram/_core/__init__.pyi000066400000000000000000000000731524660700500266520ustar00rootroot00000000000000from . import accumulators, algorithm, axis, hist, storage scikit-hep-boost-histogram-b4c49b0/src/boost_histogram/_core/accumulators.pyi000066400000000000000000000075751524660700500276330ustar00rootroot00000000000000from typing import Any, overload from numpy.typing import ArrayLike from typing_extensions import Self class _BaseAccumulator: def __eq__(self, other: object) -> bool: ... def __ne__(self, other: object) -> bool: ... def __imul__(self, other: float) -> Self: ... def __iadd__(self, other: Self) -> Self: ... def __add__(self, other: Self) -> Self: ... def __copy__(self) -> Self: ... def __deepcopy__(self, memo: Any) -> Self: ... class WeightedSum(_BaseAccumulator): @overload def __init__(self) -> None: ... @overload def __init__(self, value: float) -> None: ... @overload def __init__(self, value: float, variance: float) -> None: ... @property def value(self) -> float: ... @property def variance(self) -> float: ... @overload # type: ignore[misc] def __iadd__(self, other: Self) -> Self: ... @overload def __iadd__(self, other: float) -> Self: ... def fill(self, value: ArrayLike, *, variance: ArrayLike | None = None) -> Self: ... @staticmethod def _make(a: ArrayLike, b: ArrayLike) -> WeightedSum: ... @staticmethod def _array(a: ArrayLike, b: ArrayLike) -> WeightedSum: ... def __getitem__(self, key: str) -> float: ... def __setitem__(self, key: str, value: float) -> None: ... def _ipython_key_completions_(self) -> tuple[str, ...]: ... class Sum(_BaseAccumulator): @overload def __init__(self) -> None: ... @overload def __init__(self, value: float) -> None: ... @property def value(self) -> float: ... @overload # type: ignore[misc] def __iadd__(self, other: Self) -> Self: ... @overload def __iadd__(self, other: float) -> Self: ... def fill(self, value: ArrayLike) -> Self: ... @property def _small(self) -> float: ... @property def _large(self) -> float: ... class WeightedMean(_BaseAccumulator): @overload def __init__(self) -> None: ... @overload def __init__( self, sum_of_weights: float, sum_of_weights_squared: float, value: float, variance: float, ) -> None: ... @property def sum_of_weights(self) -> float: ... @property def sum_of_weights_squared(self) -> float: ... @property def value(self) -> float: ... @property def _sum_of_weighted_deltas_squared(self) -> float: ... @property def variance(self) -> float: ... def __call__(self, value: float, *, weight: float | None = None) -> Self: ... def fill(self, value: ArrayLike, *, weight: ArrayLike | None = None) -> Self: ... @staticmethod def _make( arg0: ArrayLike, arg1: ArrayLike, arg2: ArrayLike, arg3: ArrayLike ) -> WeightedMean: ... @staticmethod def _array( arg0: ArrayLike, arg1: ArrayLike, arg2: ArrayLike, arg3: ArrayLike ) -> WeightedMean: ... def __getitem__(self, key: str) -> float: ... def __setitem__(self, key: str, value: float) -> None: ... def _ipython_key_completions_(self) -> tuple[str, ...]: ... class Mean(_BaseAccumulator): @overload def __init__(self) -> None: ... @overload def __init__(self, count: float, value: float, variance: float) -> None: ... @property def count(self) -> float: ... @property def value(self) -> float: ... @property def _sum_of_deltas_squared(self) -> float: ... @property def variance(self) -> float: ... def __call__(self, value: float, *, weight: float | None = None) -> Self: ... def fill(self, value: ArrayLike, *, weight: ArrayLike | None = None) -> Self: ... @staticmethod def _make(arg0: ArrayLike, arg1: ArrayLike, arg2: ArrayLike) -> Mean: ... @staticmethod def _array(arg0: ArrayLike, arg1: ArrayLike, arg2: ArrayLike) -> Mean: ... def __getitem__(self, key: str) -> float: ... def __setitem__(self, key: str, value: float) -> None: ... def _ipython_key_completions_(self) -> tuple[str, ...]: ... scikit-hep-boost-histogram-b4c49b0/src/boost_histogram/_core/algorithm.pyi000066400000000000000000000027371524660700500271120ustar00rootroot00000000000000import enum import typing class reduce_command: iaxis: int class slice_mode(enum.Enum): shrink = enum.auto() crop = enum.auto() @typing.overload def shrink_and_rebin( iaxis: int, lower: float, upper: float, merge: int ) -> reduce_command: ... @typing.overload def shrink_and_rebin(lower: float, upper: float, merge: int) -> reduce_command: ... @typing.overload def crop_and_rebin( iaxis: int, lower: float, upper: float, merge: int ) -> reduce_command: ... @typing.overload def crop_and_rebin(lower: float, upper: float, merge: int) -> reduce_command: ... @typing.overload def slice_and_rebin( iaxis: int, begin: int, end: int, merge: int, mode: slice_mode = ... ) -> reduce_command: ... @typing.overload def slice_and_rebin( begin: int, end: int, merge: int, mode: slice_mode = ... ) -> reduce_command: ... @typing.overload def rebin(iaxis: int, merge: int) -> reduce_command: ... @typing.overload def rebin(merge: int) -> reduce_command: ... @typing.overload def shrink(iaxis: int, lower: float, upper: float) -> reduce_command: ... @typing.overload def shrink(lower: float, upper: float) -> reduce_command: ... @typing.overload def crop(iaxis: int, lower: float, upper: float) -> reduce_command: ... @typing.overload def crop(lower: float, upper: float) -> reduce_command: ... @typing.overload def slice(iaxis: int, begin: int, end: int, mode: slice_mode) -> reduce_command: ... @typing.overload def slice(begin: int, end: int, mode: slice_mode) -> reduce_command: ... scikit-hep-boost-histogram-b4c49b0/src/boost_histogram/_core/axis/000077500000000000000000000000001524660700500253345ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/src/boost_histogram/_core/axis/__init__.pyi000066400000000000000000000073151524660700500276240ustar00rootroot00000000000000from collections.abc import Iterable, Iterator from typing import Any import numpy as np from numpy.typing import ArrayLike from typing_extensions import Self from . import transform class _BaseAxis: def __eq__(self, other: object) -> bool: ... def __ne__(self, other: object) -> bool: ... def __copy__(self) -> Self: ... def __deepcopy__(self, memo: Any) -> Self: ... @property def traits_underflow(self) -> bool: ... @property def traits_overflow(self) -> bool: ... @property def traits_circular(self) -> bool: ... @property def traits_growth(self) -> bool: ... @property def traits_continuous(self) -> bool: ... @property def traits_ordered(self) -> bool: ... @property def raw_metadata(self) -> Any: ... @raw_metadata.setter def raw_metadata(self, item: Any) -> None: ... @property def size(self) -> int: ... @property def extent(self) -> int: ... @property def edges(self) -> np.typing.NDArray[Any]: ... @property def centers(self) -> np.typing.NDArray[Any]: ... @property def widths(self) -> np.typing.NDArray[Any]: ... def index(self, arg0: ArrayLike) -> int | np.typing.NDArray[Any]: ... def value(self, arg0: ArrayLike) -> float | np.typing.NDArray[Any]: ... class _BaseRegular(_BaseAxis): def __init__(self, bins: int, start: float, stop: float) -> None: ... def __iter__(self) -> Iterator[tuple[float, float]]: ... def bin(self, arg0: int) -> tuple[float, float]: ... class regular_none(_BaseRegular): ... class regular_uflow(_BaseRegular): ... class regular_oflow(_BaseRegular): ... class regular_uoflow(_BaseRegular): ... class regular_uoflow_growth(_BaseRegular): ... class regular_circular(_BaseRegular): ... class regular_pow(_BaseRegular): def __init__(self, bins: int, start: float, stop: float, power: float) -> None: ... @property def transform(self) -> transform.pow: ... class regular_trans(_BaseRegular): def __init__( self, bins: int, start: float, stop: float, transform: transform._BaseTransform ) -> None: ... @property def transform(self) -> transform._BaseTransform: ... class _BaseVariable(_BaseAxis): def __init__(self, edges: Iterable[float]) -> None: ... def __iter__(self) -> Iterator[tuple[float, float]]: ... def bin(self, arg0: int) -> tuple[float, float]: ... class variable_none(_BaseVariable): ... class variable_uflow(_BaseVariable): ... class variable_oflow(_BaseVariable): ... class variable_uoflow(_BaseVariable): ... class variable_uoflow_growth(_BaseVariable): ... class variable_circular(_BaseVariable): ... class _BaseInteger(_BaseAxis): def __init__(self, start: int, stop: int) -> None: ... def __iter__(self) -> Iterator[int]: ... def bin(self, arg0: int) -> int: ... class integer_none(_BaseInteger): ... class integer_uflow(_BaseInteger): ... class integer_oflow(_BaseInteger): ... class integer_uoflow(_BaseInteger): ... class integer_growth(_BaseInteger): ... class integer_circular(_BaseInteger): ... class _BaseCatInt(_BaseAxis): def __init__(self, categories: Iterable[int]) -> None: ... def __iter__(self) -> Iterator[int]: ... def bin(self, arg0: int) -> int: ... class category_int_none(_BaseCatInt): ... class category_int(_BaseCatInt): ... class category_int_growth(_BaseCatInt): ... class _BaseCatStr(_BaseAxis): def __init__(self, categories: Iterable[str]) -> None: ... def __iter__(self) -> Iterator[str]: ... def bin(self, arg0: int) -> str: ... class category_str_none(_BaseCatStr): ... class category_str(_BaseCatStr): ... class category_str_growth(_BaseCatStr): ... class boolean(_BaseAxis): def __iter__(self) -> Iterator[int]: ... def bin(self, arg0: int) -> int: ... scikit-hep-boost-histogram-b4c49b0/src/boost_histogram/_core/axis/transform.pyi000066400000000000000000000016271524660700500301000ustar00rootroot00000000000000from collections.abc import Callable from typing import Any from typing_extensions import Self def _log_fn(arg0: float) -> float: ... def _exp_fn(arg0: float) -> float: ... def _sqrt_fn(arg0: float) -> float: ... def _sq_fn(arg0: float) -> float: ... class _BaseTransform: def forward(self, arg0: float) -> float: ... def inverse(self, arg0: float) -> float: ... def __copy__(self) -> Self: ... def __deepcopy__(self, memo: Any) -> Self: ... class id(_BaseTransform): ... class sqrt(_BaseTransform): ... class log(_BaseTransform): ... class pow(_BaseTransform): def __init__(self, power: float) -> None: ... @property def power(self) -> float: ... class func_transform(_BaseTransform): def __init__( self, forward: Callable[[float], float], inverse: Callable[[float], float], convert: Callable[[float], float], name: str, ) -> None: ... scikit-hep-boost-histogram-b4c49b0/src/boost_histogram/_core/hist.pyi000066400000000000000000000076001524660700500260650ustar00rootroot00000000000000from typing import Any, ClassVar import numpy as np from numpy.typing import ArrayLike from typing_extensions import Self from . import accumulators, axis, storage from .axis import transform _axes_limit: int class _BaseHistogram: _storage_type: ClassVar[type[storage._BaseStorage]] # Note that storage has a default simply because subclasses always handle it. def __init__( self, axes: list[axis._BaseAxis], storage: storage._BaseStorage = ... ) -> None: ... def rank(self) -> int: ... def size(self) -> int: ... def reset(self) -> None: ... def __eq__(self, other: object) -> bool: ... def __ne__(self, other: object) -> bool: ... def __copy__(self) -> Self: ... def __deepcopy__(self, memo: Any) -> Self: ... def __iadd__(self, other: _BaseHistogram) -> Self: ... def to_numpy(self, flow: bool = ...) -> tuple[np.typing.NDArray[Any], ...]: ... def view(self, flow: bool = ...) -> np.typing.NDArray[Any]: ... def axis(self, i: int = ...) -> axis._BaseAxis: ... def _set_axis_metadata(self, i: int, metadata: dict[str, Any]) -> None: ... def fill( self, *args: ArrayLike, weight: ArrayLike | None = ..., sample: ArrayLike | None = ..., ) -> Self: ... def empty(self, flow: bool = ...) -> bool: ... def reduce(self, *args: Any) -> Self: ... def project(self, *args: int) -> Self: ... def sum(self, flow: bool = ...) -> Any: ... def at(self, *args: int) -> Any: ... class any_int64(_BaseHistogram): def __isub__(self, other: any_int64) -> Self: ... def __itruediv__(self, other: any_int64) -> Self: ... def __imul__(self, other: any_int64) -> Self: ... def at(self, *args: int) -> int: ... def _at_set(self, value: int, *args: int) -> None: ... def sum(self, flow: bool = ...) -> int: ... class any_unlimited(_BaseHistogram): def __isub__(self, other: any_unlimited) -> Self: ... def __itruediv__(self, other: any_unlimited) -> Self: ... def __imul__(self, other: any_unlimited) -> Self: ... def at(self, *args: int) -> float: ... def _at_set(self, value: float, *args: int) -> None: ... def sum(self, flow: bool = ...) -> float: ... class any_double(_BaseHistogram): def __isub__(self, other: any_double) -> Self: ... def __itruediv__(self, other: any_double) -> Self: ... def __imul__(self, other: any_double) -> Self: ... def at(self, *args: int) -> float: ... def _at_set(self, value: float, *args: int) -> None: ... def sum(self, flow: bool = ...) -> float: ... class any_atomic_int64(_BaseHistogram): def __itruediv__(self, other: any_atomic_int64) -> Self: ... def __imul__(self, other: any_atomic_int64) -> Self: ... def at(self, *args: int) -> int: ... def _at_set(self, value: int, *args: int) -> None: ... def sum(self, flow: bool = ...) -> int: ... class any_weight(_BaseHistogram): def at(self, *args: int) -> accumulators.WeightedSum: ... def _at_set(self, value: accumulators.WeightedSum, *args: int) -> None: ... def sum(self, flow: bool = ...) -> accumulators.WeightedSum: ... class any_mean(_BaseHistogram): def at(self, *args: int) -> accumulators.Mean: ... def _at_set(self, value: accumulators.Mean, *args: int) -> None: ... def sum(self, flow: bool = ...) -> accumulators.Mean: ... class any_weighted_mean(_BaseHistogram): def at(self, *args: int) -> accumulators.WeightedMean: ... def _at_set(self, value: accumulators.WeightedMean, *args: int) -> None: ... def sum(self, flow: bool = ...) -> accumulators.WeightedMean: ... class any_multi_cell(_BaseHistogram): def __isub__(self, other: any_multi_cell) -> Self: ... def at(self, *args: int) -> list[float]: ... def _at_set(self, value: ArrayLike, *args: int) -> None: ... def sum(self, flow: bool = ...) -> list[float]: ... def nelem(self) -> int: ... def reset_nelem(self, nelem: int) -> None: ... scikit-hep-boost-histogram-b4c49b0/src/boost_histogram/_core/storage.pyi000066400000000000000000000012341524660700500265570ustar00rootroot00000000000000from typing import Any from typing_extensions import Self class _BaseStorage: def __init__(self) -> None: ... def __eq__(self, other: object) -> bool: ... def __ne__(self, other: object) -> bool: ... def __copy__(self) -> Self: ... def __deepcopy__(self, memo: Any) -> Self: ... class int64(_BaseStorage): ... class double(_BaseStorage): ... class atomic_int64(_BaseStorage): ... class unlimited(_BaseStorage): ... class weight(_BaseStorage): ... class mean(_BaseStorage): ... class weighted_mean(_BaseStorage): ... class multi_cell(_BaseStorage): def __init__(self, k: int = ...) -> None: ... @property def nelem(self) -> int: ... scikit-hep-boost-histogram-b4c49b0/src/boost_histogram/_pytest.py000066400000000000000000000140371524660700500253470ustar00rootroot00000000000000""" A pytest plugin that produces readable explanations when an ``assert a == b`` between two :class:`~boost_histogram.Histogram` (or axis) objects fails. It is registered as a ``pytest11`` entry point (see ``pyproject.toml``), so it loads automatically for any project that has ``boost-histogram`` installed. The hook is inert for every comparison that is not a ``==`` between two histograms or two axes, so it never interferes with unrelated assertions. """ from __future__ import annotations from typing import TYPE_CHECKING, Any import numpy as np from .axis import Axis from .histogram import Histogram if TYPE_CHECKING: from numpy.typing import NDArray __all__ = ["pytest_assertrepr_compare"] def __dir__() -> list[str]: return __all__ # Maximum number of individual differing bins to list before truncating. _MAX_SAMPLES = 5 def pytest_assertrepr_compare(op: str, left: object, right: object) -> list[str] | None: """ pytest hook: return a custom explanation for a failing ``left op right``. Returning ``None`` falls back to pytest's default representation. """ if op != "==": return None if isinstance(left, Histogram) and isinstance(right, Histogram): return _histogram_explanation(left, right) if isinstance(left, Axis) and isinstance(right, Axis): return ["Axes are not equal:", *_indent(_axis_explanation(left, right))] return None def _indent(lines: list[str]) -> list[str]: return [f" {line}" for line in lines] def _histogram_explanation(left: Histogram[Any], right: Histogram[Any]) -> list[str]: lines = [f"{left!r} == {right!r}", "Histograms are not equal:"] # 1. Rank (number of axes). if left.ndim != right.ndim: lines.append(f" ndim: {left.ndim} != {right.ndim}") return lines # 2. Axes. Value bins are only meaningful once the binning matches, so if any # axis differs we explain the first difference and stop. differing = [ (i, la, ra) for i, (la, ra) in enumerate(zip(left.axes, right.axes, strict=True)) if la != ra ] if differing: lines.append(" axes differ:") for i, la, ra in differing: lines.append(f" axis {i}: {la!r} != {ra!r}") i, la, ra = differing[0] lines.append(f" axis {i} details:") lines.extend(_indent(_indent(_axis_explanation(la, ra)))) return lines # 3. Storage type. left_storage = left.storage_type.__name__ right_storage = right.storage_type.__name__ if left_storage != right_storage: lines.append(f" storage: {left_storage} != {right_storage}") return lines # 4. Bin contents (including flow, matching ``__eq__`` semantics). Note that # histogram-level Python ``metadata`` is not part of ``__eq__`` (only the # underlying C++ histogram is compared), so it is intentionally not reported. lines.extend(_indent(_contents_explanation(left, right))) return lines def _contents_explanation(left: Histogram[Any], right: Histogram[Any]) -> list[str]: lv = np.asarray(left.view(flow=True)) rv = np.asarray(right.view(flow=True)) if lv.shape != rv.shape: # pragma: no cover - guarded by equal axes above return [f"contents shape: {lv.shape} != {rv.shape}"] names = lv.dtype.names if names is None: return _plain_contents(lv, rv) return _structured_contents(lv, rv, names) def _plain_contents(lv: NDArray[Any], rv: NDArray[Any]) -> list[str]: mask = lv != rv count = int(np.count_nonzero(mask)) if count == 0: # pragma: no cover - only reached on a real difference return ["contents differ"] lines = [f"contents: {count} of {lv.size} bins differ (counts include flow)"] indices = np.argwhere(mask) for idx in indices[:_MAX_SAMPLES]: key = tuple(int(i) for i in idx) lines.append(f" bin {key}: {lv[tuple(idx)]} != {rv[tuple(idx)]}") if count > _MAX_SAMPLES: lines.append(f" ... and {count - _MAX_SAMPLES} more") lines.append(f"sum (with flow): {lv.sum()} vs {rv.sum()}") return lines def _structured_contents( lv: NDArray[Any], rv: NDArray[Any], names: tuple[str, ...] ) -> list[str]: lines = ["contents differ (counts include flow):"] for name in names: field_mask = lv[name] != rv[name] count = int(np.count_nonzero(field_mask)) if count: lines.append(f" field {name!r}: {count} of {lv.size} bins differ") if len(lines) == 1: # pragma: no cover - only reached on a real difference lines.append(" contents differ") return lines def _axis_explanation(left: Axis, right: Axis) -> list[str]: # 1. Different axis classes (e.g. Regular vs Variable). if type(left) is not type(right): return [f"type: {type(left).__name__} != {type(right).__name__}"] # 2. Constructor arguments, in the same vocabulary as the repr. This surfaces # differing size/start/stop/edges/categories/metadata. left_args = left._repr_args_() right_args = right._repr_args_() if left_args != right_args: lines = ["arguments differ:"] for la, ra in zip(left_args, right_args, strict=False): if la != ra: lines.append(f" {la} != {ra}") # Trailing args present on only one side (different arity). for extra in left_args[len(right_args) :]: lines.append(f" {extra} != ") for extra in right_args[len(left_args) :]: lines.append(f" != {extra}") return lines # 3. Fallback: reprs match but the objects still differ. Compare the edges and # metadata directly. lines = [] left_edges = getattr(left, "edges", None) right_edges = getattr(right, "edges", None) if ( left_edges is not None and right_edges is not None and not np.array_equal(left_edges, right_edges) ): lines.append("edges differ") if left.metadata != right.metadata: lines.append(f"metadata: {left.metadata!r} != {right.metadata!r}") if not lines: lines.append(f"{left!r} != {right!r}") return lines scikit-hep-boost-histogram-b4c49b0/src/boost_histogram/_utils.py000066400000000000000000000125111524660700500251520ustar00rootroot00000000000000from __future__ import annotations import typing from collections.abc import Callable, Iterator from typing import ClassVar, Protocol, TypeVar import boost_histogram class Registerable(Protocol): _types: ClassVar[set[type[object]]] class HasFamily(Protocol): _family: ClassVar[object] T = TypeVar("T") def register( cpp_types: set[type[object]] | None = None, ) -> Callable[[type[T]], type[T]]: """ Decorator to register a C++ type to a Python class. Each class given will be added to a lookup list "_types" that cast knows about. It should also part of a "family", and any class in a family will cast to the same family. You do not need to register a class if it inherits from the C++ class. For example, internally this call: ax = hist._axis(0) which will get a raw C++ object and need to cast it to a Python wrapped object. There is currently one candidate (users could add more): boost_histogram. Cast will use the parent class's family to return the correct family. If the requested family is not found, then the regular family is the fallback. This decorator, like other decorators in boost-histogram, is safe for pickling since it does not replace the original class. If nothing or an empty set is passed, this will ensure that this class is not selected during the cast process. This can be used for simple renamed classes that inject warnings, etc. """ def add_registration(cls: type[T]) -> type[T]: if cpp_types is None or len(cpp_types) == 0: cls._types = set() # type: ignore[attr-defined] return cls if "_types" not in cls.__dict__: cls._types = set() # type: ignore[attr-defined] for cpp_type in cpp_types: if cpp_type in cls._types: # type: ignore[attr-defined] raise TypeError(f"You are trying to register {cpp_type} again") cls._types.add(cpp_type) # type: ignore[attr-defined] return cls return add_registration # TODO: Split into is_class/not is class versions (better typing) def _cast_make_object(canidate_class: T, cpp_object: object, is_class: bool) -> T: "Make an object for cast" if is_class: return canidate_class if hasattr(canidate_class, "_convert_cpp"): return canidate_class._convert_cpp(cpp_object) # type: ignore[no-any-return] # Casting down does not work in pybind11, # see https://github.com/pybind/pybind11/issues/1640 # so for now, all non-copy classes must have a # _convert_cpp method. return canidate_class(cpp_object) # type: ignore[operator, no-any-return] def cast(self: object, cpp_object: object, parent_class: type[T]) -> T: """ This converts a C++ object into a Python object. This takes the parent object, the C++ object, the Python class. If a class is passed in instead of an object, this will return a class instead. The parent object (self) can be either a registered class or an instance of a registered class. Instances simply have their class replaced. If a class does not support direction conversion in the constructor, it should have _convert_cpp class method instead. Example: cast(self, hist.cpp_axis(), Axis) # -> returns Regular(...) if regular axis, etc. If self is None, just use the boost_histogram family. """ if self is None: family: object = boost_histogram else: family = self._family # type: ignore[attr-defined] # Convert objects to classes, and remember if we did so if isinstance(cpp_object, type): is_class = True cpp_class = cpp_object else: is_class = False cpp_class = cpp_object.__class__ # Remember the fallback class if a class in the same family does not exist fallback_class = None for canidate_class in _walk_subclasses(parent_class): # If a class was registered with this c++ type if hasattr(canidate_class, "_types"): ccr = typing.cast(Registerable, canidate_class) is_valid_type = cpp_class in ccr._types else: is_valid_type = cpp_class in _walk_bases(canidate_class) if is_valid_type and hasattr(canidate_class, "_family"): ccf = typing.cast(HasFamily, canidate_class) # Return immediately if the family is right if ccf._family is family: return _cast_make_object(ccf, cpp_object, is_class) # type: ignore[return-value] # Or remember the class if it was from the main family if ccf._family is boost_histogram: fallback_class = ccf # If no perfect match was registered, return the main family if fallback_class is not None: return _cast_make_object(fallback_class, cpp_object, is_class) # type: ignore[return-value] raise TypeError( f"No conversion to {parent_class.__name__} from {cpp_object} found." ) def _walk_bases(cls: type[object]) -> Iterator[type[object]]: for base in cls.__bases__: yield from _walk_bases(base) yield base def _walk_subclasses(cls: type[object]) -> Iterator[type[object]]: for base in cls.__subclasses__(): # Find the furthest child to allow # user subclasses to work yield from _walk_subclasses(base) yield base scikit-hep-boost-histogram-b4c49b0/src/boost_histogram/accumulators.py000066400000000000000000000006111524660700500263530ustar00rootroot00000000000000from __future__ import annotations from ._core.accumulators import ( # pylint: disable=import-error,no-name-in-module Mean, Sum, WeightedMean, WeightedSum, ) from .typing import Accumulator __all__ = ("Accumulator", "Mean", "Sum", "WeightedMean", "WeightedSum") for cls in (Sum, Mean, WeightedSum, WeightedMean): cls.__module__ = "boost_histogram.accumulators" del cls scikit-hep-boost-histogram-b4c49b0/src/boost_histogram/axis/000077500000000000000000000000001524660700500242455ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/src/boost_histogram/axis/__init__.py000066400000000000000000000707661524660700500263760ustar00rootroot00000000000000from __future__ import annotations import copy from collections.abc import Callable, Iterable, Iterator from dataclasses import dataclass from functools import partial from typing import ( Any, ClassVar, Literal, TypedDict, ) import numpy as np # pylint: disable=unused-import import boost_histogram from .._compat.typing import Self from .._core import axis as ca from .._utils import cast, register from . import transform from .transform import AxisTransform __all__ = [ "ArrayTuple", "AxesTuple", "Axis", "Boolean", "IntCategory", "Integer", "Regular", "StrCategory", "Traits", "Variable", "transform", ] def __dir__() -> list[str]: return __all__ def _isstr(value: Any) -> bool: """ Check to see if this is a stringlike or a non-empty (nested) iterable of stringlikes. Empty iterables are not considered stringlike. Iterables without a length (one-shot iterators) are not inspected, since checking them would consume them. """ if isinstance(value, (str, bytes)): return True if isinstance(value, np.ndarray): if value.dtype.kind in {"S", "U"}: return True if value.dtype.kind == "O": return value.size > 0 and all(_isstr(v) for v in value.flat) return False if hasattr(value, "__iter__") and hasattr(value, "__len__"): return len(value) > 0 and all(_isstr(v) for v in value) return False def _is_empty_sized(value: Any) -> bool: """ Check for a sized, empty iterable (without consuming one-shot iterators). """ try: return len(value) == 0 except TypeError: return False def _opts(**kwargs: bool) -> set[str]: return {k for k, v in kwargs.items() if v} AxCallOrInt = int | Callable[["Axis"], int] @dataclass(order=True, frozen=True) class Traits: underflow: bool = False overflow: bool = False circular: bool = False growth: bool = False continuous: bool = False ordered: bool = False @property def discrete(self) -> bool: "True if axis is not continuous" return not self.continuous # Contains common methods and properties to all axes class Axis: __slots__ = ("__dict__", "_ax") _family: object def __init_subclass__(cls, *, family: object) -> None: super().__init_subclass__() cls._family = family def __setattr__(self, attr: str, value: Any) -> None: if attr == "__dict__": self._ax.raw_metadata = value object.__setattr__(self, attr, value) def __getattr__(self, attr: str) -> Any: if attr == "metadata": return None raise AttributeError( f"object {self.__class__.__name__} has no attribute {attr}" ) def __init__( self, ax: Any, metadata: dict[str, Any] | None, __dict__: dict[str, Any] | None, ) -> None: """ ax: the C++ object metadata: the metadata keyword contents __dict__: the __dict__ keyword contents """ self._ax = ax if __dict__ is not None and metadata is not None: raise KeyError( "Cannot provide metadata by keyword and __dict__, use __dict__ only" ) if __dict__ is not None: self._ax.raw_metadata = __dict__ elif metadata is not None: self._ax.raw_metadata["metadata"] = metadata self.__dict__ = self._ax.raw_metadata def __setstate__(self, state: dict[str, Any]) -> None: self._ax = state["_ax"] self.__dict__ = self._ax.raw_metadata def __getstate__(self) -> dict[str, Any]: return {"_ax": self._ax} def __copy__(self) -> Self: other: Self = self.__class__.__new__(self.__class__) other._ax = copy.copy(self._ax) other.__dict__ = other._ax.raw_metadata return other def index(self, value: float | str) -> int: """ Return the fractional index(es) given a value (or values) on the axis. """ if _isstr(value): msg = f"index({value}) cannot be a string for a numerical axis" raise TypeError(msg) return self._ax.index(value) # type: ignore[no-any-return] def value(self, index: float) -> float: """ Return the value(s) given an (fractional) index (or indices). """ return self._ax.value(index) # type: ignore[no-any-return] def bin(self, index: float) -> int | str | tuple[float, float]: """ Return the edges of the bins as a tuple for a continuous axis or the bin value for a non-continuous axis, when given an index. """ return self._ax.bin(index) # type: ignore[no-any-return] __hash__ = None # type: ignore[assignment] def __eq__(self, other: object) -> bool: return hasattr(other, "_ax") and self._ax == other._ax @classmethod def _convert_cpp(cls, cpp_object: Any) -> Self: nice_ax: Self = cls.__new__(cls) nice_ax._ax = cpp_object nice_ax.__dict__ = cpp_object.raw_metadata return nice_ax def __len__(self) -> int: return self._ax.size # type: ignore[no-any-return] def __iter__( self, ) -> Iterator[float] | Iterator[str] | Iterator[tuple[float, float]]: return self._ax.__iter__() # type: ignore[no-any-return] def _process_callable(self, value: AxCallOrInt | None, *, default: int) -> int: """ This processes a callable in start or stop. None gets replaced by default. """ if value is None: return default if callable(value): return value(self) # Normalize plain integer bounds like NumPy slicing: negative values # count from the end, and out-of-range values clamp to [0, len]. # Locators (callables) are handled above and keep flow access (e.g. # tag.at(-1) for underflow). if value < 0: value += len(self) return min(max(int(value), 0), len(self)) def _process_loc( self, start: AxCallOrInt | None, stop: AxCallOrInt | None ) -> tuple[int, int]: """ Compute start and stop into actual start and stop values in Boost.Histogram. None -> -1 or 0 for start, -> len or len+1 for stop. If start or stop are callable, then call them with the axes. For a non-ordered axes, flow is all or nothing, so this will ensure overflow is turned off if underflow is not None. """ underflow = -1 if self._ax.traits_underflow else 0 overflow = 1 if self._ax.traits_overflow else 0 # Non-ordered axes only use flow if integrating from None to None if not self._ax.traits_ordered and not (start is None and stop is None): overflow = 0 begin = self._process_callable(start, default=underflow) end = self._process_callable(stop, default=len(self) + overflow) return begin, end def __repr__(self) -> str: arg_str = ", ".join(self._repr_args_()) return f"{self.__class__.__name__}({arg_str})" def _repr_args_(self) -> list[str]: """ Return arg options for use in the repr as strings. """ ret = [] if self.metadata is not None: if isinstance(self.metadata, str): ret.append(f"metadata={self.metadata!r}") else: ret.append("metadata=...") return ret @property def traits(self) -> Traits: """ Get traits for the axis - read only properties of a specific axis. """ return Traits( self._ax.traits_underflow, self._ax.traits_overflow, self._ax.traits_circular, self._ax.traits_growth, self._ax.traits_continuous, self._ax.traits_ordered, ) @property def size(self) -> int: """ Return number of bins excluding under- and overflow. """ return self._ax.size # type: ignore[no-any-return] @property def extent(self) -> int: """ Return number of bins including under- and overflow. """ return self._ax.extent # type: ignore[no-any-return] def __getitem__(self, i: AxCallOrInt) -> int | str | tuple[float, float]: """ Access a bin, using normal Python syntax for wraparound. """ # UHI support if callable(i): i = i(self) else: size: int = self._ax.size requested = i if i < 0: i += size if i < 0 or i >= size: raise IndexError( f"Out of range access, {requested} is out of range for axis with size {size}" ) assert not callable(i) return self.bin(i) @property def edges(self) -> np.typing.NDArray[Any]: return self._ax.edges # type: ignore[no-any-return] @property def centers(self) -> np.typing.NDArray[Any]: """ An array of bin centers. """ return self._ax.centers # type: ignore[no-any-return] @property def widths(self) -> np.typing.NDArray[Any]: """ An array of bin widths. """ return self._ax.widths # type: ignore[no-any-return] # Contains all common methods and properties for Regular axes @register( { ca.regular_uoflow, ca.regular_uoflow_growth, ca.regular_uflow, ca.regular_oflow, ca.regular_none, ca.regular_pow, ca.regular_trans, ca.regular_circular, } ) class Regular(Axis, family=boost_histogram): __slots__ = () def __init__( self, bins: int, start: float, stop: float, *, metadata: Any = None, underflow: bool = True, overflow: bool = True, growth: bool = False, circular: bool = False, transform: AxisTransform | None = None, # pylint: disable=redefined-outer-name __dict__: dict[str, Any] | None = None, ): """ Make a regular axis with nice keyword arguments for underflow, overflow, and growth. Parameters ---------- bins : int The number of bins between start and stop start : float The beginning value for the axis stop : float The ending value for the axis metadata : Any Fills .metadata on the axis. underflow : bool = True Enable the underflow bin overflow : bool = True Enable the overflow bin growth : bool = False Allow the axis to grow if a value is encountered out of range. Be careful, the axis will grow as large as needed. circular : bool = False Filling wraps around. transform : Optional[AxisTransform] = None Transform the regular bins (Log, Sqrt, and Pow(v)) __dict__: Optional[dict[str, Any]] = None The full metadata dictionary """ options = _opts( underflow=underflow, overflow=overflow, growth=growth, circular=circular ) ax: ca._BaseRegular if transform is not None: if options != {"underflow", "overflow"}: raise KeyError("Transform supplied, cannot change other options") if not isinstance(transform, AxisTransform): if isinstance(transform, type) and issubclass( # type: ignore[unreachable] transform, AxisTransform ): msg = f"You must pass an instance, use {transform.__name__}()" raise TypeError(msg) msg = f"transform must be an AxisTransform instance, got {transform!r}" raise TypeError(msg) ax = transform._produce(bins, start, stop) elif options == {"growth", "underflow", "overflow"}: ax = ca.regular_uoflow_growth(bins, start, stop) elif options == {"underflow", "overflow"}: ax = ca.regular_uoflow(bins, start, stop) elif options == {"underflow"}: ax = ca.regular_uflow(bins, start, stop) elif options == {"overflow"}: ax = ca.regular_oflow(bins, start, stop) elif options in ( {"circular", "underflow", "overflow"}, {"circular", "overflow"}, ): # growth=True, underflow=False is also correct ax = ca.regular_circular(bins, start, stop) elif options == set(): ax = ca.regular_none(bins, start, stop) else: raise KeyError("Unsupported collection of options") super().__init__(ax, metadata, __dict__) def _repr_args_(self) -> list[str]: "Return inner part of signature for use in repr" ret = [f"{self.size}", f"{self.edges[0]:g}", f"{self.edges[-1]:g}"] if self.traits.growth: ret.append("growth=True") elif self.traits.circular: ret.append("circular=True") else: if not self.traits.underflow: ret.append("underflow=False") if not self.traits.overflow: ret.append("overflow=False") if self.transform is not None: ret.append(f"transform={self.transform}") ret += super()._repr_args_() return ret @property def transform(self) -> AxisTransform | None: if hasattr(self._ax, "transform"): return cast(self, self._ax.transform, AxisTransform) return None @register( { ca.variable_none, ca.variable_uflow, ca.variable_oflow, ca.variable_uoflow, ca.variable_uoflow_growth, ca.variable_circular, } ) class Variable(Axis, family=boost_histogram): __slots__ = () def __init__( self, edges: Iterable[float], *, metadata: Any = None, underflow: bool = True, overflow: bool = True, growth: bool = False, circular: bool = False, __dict__: dict[str, Any] | None = None, ): """ Make an axis with irregularly spaced bins. Provide a list or array of bin edges, and len(edges)-1 bins will be made. Parameters ---------- edges : Array[float] The edges for the bins. There will be one less bin than edges. metadata : object Any Python object to attach to the axis, like a label. underflow : bool = True Enable the underflow bin overflow : bool = True Enable the overflow bin circular : bool = False Enable wraparound growth : bool = False Allow the axis to grow if a value is encountered out of range. Be careful, the axis will grow as large as needed. __dict__: Optional[dict[str, Any]] = None The full metadata dictionary """ options = _opts( underflow=underflow, overflow=overflow, growth=growth, circular=circular ) ax: ca._BaseVariable if options == {"growth", "underflow", "overflow"}: ax = ca.variable_uoflow_growth(edges) elif options == {"underflow", "overflow"}: ax = ca.variable_uoflow(edges) elif options == {"underflow"}: ax = ca.variable_uflow(edges) elif options == {"overflow"}: ax = ca.variable_oflow(edges) elif options in ( {"circular", "underflow", "overflow"}, {"circular", "overflow"}, ): # growth=True, underflow=False is also correct ax = ca.variable_circular(edges) elif options == set(): ax = ca.variable_none(edges) else: raise KeyError("Unsupported collection of options") super().__init__(ax, metadata, __dict__) def _repr_args_(self) -> list[str]: "Return inner part of signature for use in repr" if len(self) > 20: ret = [repr(self.edges)] else: args = ", ".join(format(v, "g") for v in self.edges) ret = [f"[{args}]"] if self.traits.growth: ret.append("growth=True") elif self.traits.circular: ret.append("circular=True") else: if not self.traits.underflow: ret.append("underflow=False") if not self.traits.overflow: ret.append("overflow=False") ret += super()._repr_args_() return ret @register( { ca.integer_none, ca.integer_uflow, ca.integer_oflow, ca.integer_uoflow, ca.integer_growth, ca.integer_circular, } ) class Integer(Axis, family=boost_histogram): __slots__ = () def __init__( self, start: int, stop: int, *, metadata: Any = None, underflow: bool = True, overflow: bool = True, growth: bool = False, circular: bool = False, __dict__: dict[str, Any] | None = None, ): """ Make an integer axis, with a collection of consecutive integers. Parameters ---------- start : int The beginning value for the axis stop : int The ending value for the axis. (start-stop) bins will be created. metadata : object Any Python object to attach to the axis, like a label. underflow : bool = True Enable the underflow bin overflow : bool = True Enable the overflow bin circular : bool = False Enable wraparound growth : bool = False Allow the axis to grow if a value is encountered out of range. Be careful, the axis will grow as large as needed. __dict__: Optional[dict[str, Any]] = None The full metadata dictionary """ options = _opts( underflow=underflow, overflow=overflow, growth=growth, circular=circular ) ax: ca._BaseInteger # underflow and overflow settings are ignored, integers are always # finite and thus cannot end up in a flow bin when growth is on if "growth" in options and "circular" not in options: ax = ca.integer_growth(start, stop) elif options == {"underflow", "overflow"}: ax = ca.integer_uoflow(start, stop) elif options == {"underflow"}: ax = ca.integer_uflow(start, stop) elif options == {"overflow"}: ax = ca.integer_oflow(start, stop) elif "circular" in options and "growth" not in options: ax = ca.integer_circular(start, stop) elif options == set(): ax = ca.integer_none(start, stop) else: raise KeyError("Unsupported collection of options") super().__init__(ax, metadata, __dict__) def _repr_args_(self) -> list[str]: "Return inner part of signature for use in repr" ret = [f"{self.edges[0]:g}", f"{self.edges[-1]:g}"] if self.traits.growth: ret.append("growth=True") elif self.traits.circular: ret.append("circular=True") else: if not self.traits.underflow: ret.append("underflow=False") if not self.traits.overflow: ret.append("overflow=False") ret += super()._repr_args_() return ret class BaseCategory(Axis, family=boost_histogram): __slots__ = () def _repr_args_(self) -> list[str]: "Return inner part of signature for use in repr" ret = [] if self.traits.growth: ret.append("growth=True") elif not self.traits.overflow: ret.append("overflow=False") ret += super()._repr_args_() return ret @register({ca.category_str_growth, ca.category_str, ca.category_str_none}) class StrCategory(BaseCategory, family=boost_histogram): __slots__ = () def __init__( self, categories: Iterable[str], *, metadata: Any = None, growth: bool = False, overflow: bool = True, __dict__: dict[str, Any] | None = None, ): """ Make a category axis with strings; items will be added to a predefined list of bins or a growing (with growth=True) list of bins. Parameters ---------- categories : Iterator[str] The bin values in strings. May be empty if growth is enabled. metadata : object Any Python object to attach to the axis, like a label. growth : bool = False Allow the axis to grow if a value is encountered out of range. Be careful, the axis will grow as large as needed. overflow : bool = True Include an overflow bin for "missed" hits. Ignored if growth=True. __dict__: Optional[dict[str, Any]] = None The full metadata dictionary """ options = _opts(growth=growth, overflow=overflow) ax: ca._BaseCatStr # henryiii: We currently expand "abc" to "a", "b", "c" - some # Python interfaces protect against that if "growth" in options: ax = ca.category_str_growth(tuple(categories)) elif options == {"overflow"}: ax = ca.category_str(tuple(categories)) elif not options: ax = ca.category_str_none(tuple(categories)) else: raise KeyError("Unsupported collection of options") super().__init__(ax, metadata, __dict__) def index(self, value: float | str) -> int: """ Return the fractional index(es) given a value (or values) on the axis. """ if _isstr(value) or _is_empty_sized(value): return self._ax.index(value) # type: ignore[no-any-return] msg = f"index({value}) must be a string or iterable of strings for a StrCategory axis" raise TypeError(msg) def _repr_args_(self) -> list[str]: "Return inner part of signature for use in repr" args = ", ".join(repr(c) for c in self) ret = [f"[{args}]"] ret += super()._repr_args_() return ret @register({ca.category_int, ca.category_int_growth, ca.category_int_none}) class IntCategory(BaseCategory, family=boost_histogram): __slots__ = () def __init__( self, categories: Iterable[int], *, metadata: Any = None, growth: bool = False, overflow: bool = True, __dict__: dict[str, Any] | None = None, ): """ Make a category axis with ints; items will be added to a predefined list of bins or a growing (with growth=True) list of bins. An empty list is allowed if growth=True. Parameters ---------- categories : Iterable[int] The bin values, either ints or strings. metadata : object Any Python object to attach to the axis, like a label. growth : bool = False Allow the axis to grow if a value is encountered out of range. Be careful, the axis will grow as large as needed. overflow : bool = True Include an overflow bin for "missed" hits. Ignored if growth=True. __dict__: Optional[dict[str, Any]] = None The full metadata dictionary """ options = _opts(growth=growth, overflow=overflow) ax: ca._BaseCatInt if "growth" in options: ax = ca.category_int_growth(tuple(categories)) elif options == {"overflow"}: ax = ca.category_int(tuple(categories)) elif not options: ax = ca.category_int_none(tuple(categories)) else: raise KeyError("Unsupported collection of options") super().__init__(ax, metadata, __dict__) def _repr_args_(self) -> list[str]: "Return inner part of signature for use in repr" args = ", ".join(format(c, "g") for c in self) ret = [f"[{args}]"] ret += super()._repr_args_() return ret # Contains all common methods and properties for the boolean axis @register({ca.boolean}) class Boolean(Axis, family=boost_histogram): __slots__ = () def __init__(self, *, metadata: Any = None, __dict__: dict[str, Any] | None = None): """ Make an axis for boolean values. Parameters ---------- metadata : object Any Python object to attach to the axis, like a label. __dict__: Optional[dict[str, Any]] = None The full metadata dictionary """ ax = ca.boolean() super().__init__(ax, metadata, __dict__) def _repr_args_(self) -> list[str]: "Return inner part of signature for use in repr" ret = [] if self.size == 0: ret.append("") elif self.size == 1 and self.centers[0] < 0.75: ret.append("") elif self.size == 1: ret.append("") ret += super()._repr_args_() return ret class MGridOpts(TypedDict): sparse: bool indexing: Literal["ij", "xy"] class ArrayTuple(tuple): # type: ignore[type-arg] __slots__ = () # This is an exhaustive list as of NumPy 1.19 _REDUCTIONS = frozenset(("sum", "any", "all", "min", "max", "prod")) def __getattr__(self, name: str) -> Any: if name in self._REDUCTIONS: return partial(getattr(np, name), np.broadcast_arrays(*self)) return self.__class__(getattr(a, name) for a in self) def __dir__(self) -> list[str]: names = set(dir(self.__class__)) | set(dir(np.ndarray)) | self._REDUCTIONS return sorted(n for n in names if not n.startswith("_")) def __call__(self, *args: Any, **kwargs: Any) -> Any: return self.__class__(a(*args, **kwargs) for a in self) def broadcast(self) -> Self: """ The arrays in this tuple will be compressed if possible to save memory. Use this method to broadcast them out into their full memory representation. """ return self.__class__(np.broadcast_arrays(*self)) class AxesTuple(tuple): # type: ignore[type-arg] __slots__ = () _MGRIDOPTS: ClassVar[MGridOpts] = {"sparse": True, "indexing": "ij"} def __init__(self, /, _iterable: Iterable[Axis]) -> None: for item in self: if not isinstance(item, Axis): raise TypeError( f"Only an iterable of Axis supported in AxesTuple, got {item}" ) super().__init__() @property def size(self) -> tuple[int, ...]: return tuple(s.size for s in self) @property def extent(self) -> tuple[int, ...]: return tuple(s.extent for s in self) @property def centers(self) -> ArrayTuple: gen = (s.centers for s in self) return ArrayTuple(np.meshgrid(*gen, **self._MGRIDOPTS)) @property def edges(self) -> ArrayTuple: gen = (s.edges for s in self) return ArrayTuple(np.meshgrid(*gen, **self._MGRIDOPTS)) @property def widths(self) -> ArrayTuple: gen = (s.widths for s in self) return ArrayTuple(np.meshgrid(*gen, **self._MGRIDOPTS)) def value(self, *indexes: float) -> tuple[float, ...]: if len(indexes) != len(self): raise IndexError( "Must have the same number of arguments as the number of axes" ) return tuple(self[i].value(indexes[i]) for i in range(len(indexes))) def bin(self, *indexes: float) -> tuple[float, ...]: if len(indexes) != len(self): raise IndexError( "Must have the same number of arguments as the number of axes" ) return tuple(self[i].bin(indexes[i]) for i in range(len(indexes))) def index(self, *values: float) -> tuple[int, ...]: # type: ignore[override] if len(values) != len(self): raise IndexError( "Must have the same number of arguments as the number of axes" ) return tuple(self[i].index(values[i]) for i in range(len(values))) def __getitem__(self, item: Any) -> Any: result = super().__getitem__(item) return self.__class__(result) if isinstance(result, tuple) else result def __getattr__(self, attr: str) -> tuple[Any, ...]: return tuple(getattr(s, attr) for s in self) def __setattr__(self, attr: str, values: Any) -> None: try: super().__setattr__(attr, values) except AttributeError: for s, v in zip(self, values, strict=True): s.__setattr__(attr, v) value.__doc__ = Axis.value.__doc__ index.__doc__ = Axis.index.__doc__ bin.__doc__ = Axis.bin.__doc__ scikit-hep-boost-histogram-b4c49b0/src/boost_histogram/axis/transform.py000066400000000000000000000135451524660700500266420ustar00rootroot00000000000000from __future__ import annotations import copy from typing import Any, ClassVar import boost_histogram from .._compat.typing import Self from .._core import axis as ca from .._utils import register __all__ = ["AxisTransform", "Function", "Pow", "log", "sqrt"] def __dir__() -> list[str]: return __all__ class AxisTransform: __slots__ = ("_this",) _family: ClassVar[object] # pylint: disable=declare-non-slot _this: ca.transform._BaseTransform def __init_subclass__(cls, *, family: object) -> None: super().__init_subclass__() cls._family = family def __copy__(self) -> Self: other: Self = self.__class__.__new__(self.__class__) other._this = copy.copy(self._this) return other @classmethod def _convert_cpp(cls, this: Any) -> Self: self: Self = cls.__new__(cls) self._this = this return self def __repr__(self) -> str: if hasattr(self, "_this"): return repr(self._this) return f"{self.__class__.__name__}() # Missing _this, broken class" def _produce(self, bins: int, start: float, stop: float) -> Any: raise NotImplementedError() def __init__(self) -> None: "Create a new transform instance" raise NotImplementedError() def forward(self, value: float) -> float: "Compute the forward transform" return self._this.forward(value) def inverse(self, value: float) -> float: "Compute the inverse transform" return self._this.inverse(value) def __eq__(self, other: object) -> bool: # Compare by type and parameters, not by repr - two different # Function transforms can have the same name (and so the same repr). # Subclasses override this and __hash__ with real comparisons. return NotImplemented def __hash__(self) -> int: return hash(type(self)) @register({ca.transform.pow}) class Pow(AxisTransform, family=boost_histogram): __slots__ = () _type = ca.regular_pow _this: ca.transform.pow # Note: this comes from family _types: ClassVar[set[type[ca.transform.pow]]] def __init__(self, power: float): # pylint: disable=super-init-not-called "Create a new transform instance" (cpp_class,) = self._types self._this = cpp_class(power) @property def power(self) -> float: "The power of the transform" return self._this.power # This one does need to be a normal method def _produce(self, bins: int, start: float, stop: float) -> Any: return self.__class__._type(bins, start, stop, self.power) def __eq__(self, other: object) -> bool: if not isinstance(other, Pow): return NotImplemented return self.power == other.power def __hash__(self) -> int: return hash((Pow, self.power)) @register({ca.transform.func_transform}) class Function(AxisTransform, family=boost_histogram): __slots__ = () _type = ca.regular_trans _this: ca.transform.func_transform # Note: this comes from family _types: ClassVar[set[type[ca.transform.func_transform]]] def __init__( # pylint: disable=super-init-not-called self, forward: Any, inverse: Any, *, convert: Any = None, name: str = "" ): """ Create a functional transform from a ctypes double(double) function pointer or any object that provides such an interface through a ``.ctypes`` attribute (such as numba.cfunc). A pure python function *can* be adapted to a ctypes pointer, but please use a Variable axis instead or use something like numba to produce a compiled function pointer. You can manually specify the repr name with ``name=``. Example of Numba use: --------------------- @numba.cfunc(numba.float64(numba.float64,)) def exp(x): return math.exp(x) @numba.cfunc(numba.float64(numba.float64,)) def log(x): return math.log(x) Example of slow CTypes use: --------------------------- ftype = ctypes.CFUNCTYPE(ctypes.c_double, ctypes.c_double) log = ftype(math.log) exp = ftype(math.exp) Now you can supply these functions, and you will get a high performance transformation axis. You can also supply an optional conversion function; this will take the input forward and inverse and call them before producing a transform. This enables pickling, as well, since ctypes pointers are not picklable. A few common utilities have been supplied: * ``convert.numba``: Compile using numba (required) * ``convert.python``: Just call the Python function (15-90x slower than compiled) See also -------- * ``Numbify(forward, inverse, *, name='')``: Uses convert=convert.numba * ``PythonFunction(forward, inverse, *, name='')``: Uses convert=convert.python """ (cpp_class,) = self._types self._this = cpp_class(forward, inverse, convert, name) # This one does need to be a normal method def _produce(self, bins: int, start: float, stop: float) -> Any: return self.__class__._type(bins, start, stop, self._this) def __eq__(self, other: object) -> bool: # Compares the underlying forward/inverse functions, not the name - # two Function transforms with the same name are not necessarily equal. if not isinstance(other, Function): return NotImplemented return bool(self._this == other._this) def __hash__(self) -> int: return hash(Function) def _internal_conversion(name: str) -> Any: return getattr(ca.transform, name) sqrt: Function = Function( "_sqrt_fn", "_sq_fn", convert=_internal_conversion, name="sqrt" ) log: Function = Function("_log_fn", "_exp_fn", convert=_internal_conversion, name="log") scikit-hep-boost-histogram-b4c49b0/src/boost_histogram/histogram.py000066400000000000000000002417101524660700500256550ustar00rootroot00000000000000from __future__ import annotations import collections.abc import copy import enum import logging import threading import typing import warnings from collections.abc import Callable, Iterable, Mapping from os import cpu_count from typing import ( TYPE_CHECKING, Any, ClassVar, Literal, NewType, SupportsIndex, TypeAlias, TypeVar, ) import numpy as np import boost_histogram from boost_histogram import _core from . import serialization from . import storage as bhs from ._compat.typing import Self from ._utils import cast, register from .axis import AxesTuple, Axis, Variable from .storage import Double, Storage from .view import MeanView, WeightedMeanView, WeightedSumView, _to_view if TYPE_CHECKING: from types import EllipsisType from .typing import ( Accumulator, ArrayLike, CppHistogram, Mean, RebinProtocol, WeightedMean, WeightedSum, ) # This is a StrEnum as defined in Python 3.11 class Kind(str, enum.Enum): COUNT = "COUNT" MEAN = "MEAN" __str__ = str.__str__ __all__ = [ "Histogram", "IndexingExpr", "Kind", ] def __dir__() -> list[str]: return __all__ NOTHING = object() _histograms: set[type[CppHistogram]] = { _core.hist.any_double, _core.hist.any_int64, _core.hist.any_atomic_int64, _core.hist.any_unlimited, _core.hist.any_weight, _core.hist.any_mean, _core.hist.any_weighted_mean, _core.hist.any_multi_cell, } # Tuple form of ``_histograms`` for fast isinstance checks. The set above is # fixed at import time (only ``@register`` reads it), so this never goes stale. _histogram_types: tuple[type[CppHistogram], ...] = tuple(_histograms) logger = logging.getLogger(__name__) # User-facing operator symbols for the in-place dunders dispatched in # _compute_inplace_op, used to build clear errors when a storage's underlying # C++ histogram does not support an operation between two histograms. _INPLACE_OP_SYMBOLS: dict[str, str] = { "__iadd__": "+", "__isub__": "-", "__imul__": "*", "__itruediv__": "/", } CppAxis = NewType("CppAxis", object) SimpleIndexing: TypeAlias = ( "SupportsIndex | slice | RebinProtocol | np.typing.NDArray[Any]" ) InnerIndexing: TypeAlias = "SimpleIndexing | Callable[[Axis], int]" FullInnerIndexing: TypeAlias = "InnerIndexing | list[InnerIndexing]" IndexingWithMapping: TypeAlias = "FullInnerIndexing | Mapping[int, FullInnerIndexing]" IndexingExpr: TypeAlias = ( "IndexingWithMapping | tuple[IndexingWithMapping, ...] | EllipsisType" ) T = TypeVar("T") IntHists = TypeVar( "IntHists", bound="Histogram[bhs.AtomicInt64] | Histogram[bhs.Int64]" ) FloatHists = TypeVar( "FloatHists", bound="Histogram[bhs.Double] | Histogram[bhs.Unlimited]" ) ListHists = TypeVar("ListHists", bound="Histogram[bhs.MultiCell]") WeightHists = TypeVar("WeightHists", bound="Histogram[bhs.Weight]") MeanHists = TypeVar("MeanHists", bound="Histogram[bhs.Mean]") WeightedMeanHists = TypeVar("WeightedMeanHists", bound="Histogram[bhs.WeightedMean]") @typing.overload def _fill_cast( value: tuple[T, ...] | list[T], *, inner: Literal[False] = False ) -> tuple[T | np.typing.NDArray[Any], ...]: ... @typing.overload def _fill_cast(value: T, *, inner: bool = False) -> T | np.typing.NDArray[Any]: ... def _fill_cast( value: Any, *, inner: bool = False ) -> Any | np.typing.NDArray[Any] | tuple[Any | np.typing.NDArray[Any], ...]: """ Convert to NumPy arrays. Some buffer objects do not get converted by forcecast. If not called by itself (inner=False), then will work through one level of tuple/list. """ if value is None or isinstance(value, (str, bytes)): return value if not inner and isinstance(value, (tuple, list)): return tuple(_fill_cast(a, inner=True) for a in value) if hasattr(value, "__iter__") or hasattr(value, "__array__"): return np.asarray(value) return value def mean_storage_sample_check(sample: ArrayLike | None) -> None: if sample is None: raise TypeError("Sample key-argument (sample=) needs to be provided.") msg1 = f"Sample key-argument needs to be a number or a sequence, {sample.__class__.__name__} given." if isinstance(sample, str): raise TypeError(msg1) sample_dim = np.ndim(sample) msg2 = f"Sample key-argument needs to be a scalar or 1 dimensional, {sample_dim} given." if sample_dim > 1: raise ValueError(msg2) def _arg_shortcut(item: tuple[int, float, float] | Axis | CppAxis) -> CppAxis: if isinstance(item, tuple) and len(item) == 3: msg = "Using () directly in constructor is a developer shortcut and will be removed in a future version" warnings.warn(msg, FutureWarning, stacklevel=4) return _core.axis.regular_uoflow(item[0], item[1], item[2]) # type: ignore[return-value] if isinstance(item, Axis): return item._ax # type: ignore[no-any-return] raise TypeError("Only axes supported in histogram constructor") # Discrete C++ axes that ``axis::edges`` represents as 0..size (the category # axes); everything else gets its true (continuous-like) edge values. _category_cpp_axes = ( _core.axis.category_int, _core.axis.category_int_growth, _core.axis.category_int_none, _core.axis.category_str, _core.axis.category_str_growth, _core.axis.category_str_none, ) # Axes that the C++ ``axis::edges`` helper does not nudge when producing # NumPy-convention (upper-edge inclusive) edges. _no_nudge_cpp_axes = ( _core.axis.regular_none, _core.axis.regular_uflow, ) def _numpy_compatible_edges(cpp_ax: Any, flow: bool) -> np.typing.NDArray[np.float64]: """ Edges for one C++ axis following the NumPy convention (upper edge inclusive); this replicates exactly what the C++ ``axis::edges(ax, flow, true)`` helper produces, without requiring a copy of the bin contents. """ if isinstance(cpp_ax, _category_cpp_axes): overflow = int(flow and cpp_ax.traits_overflow) return np.arange(cpp_ax.size + 1 + overflow, dtype=np.float64) underflow = int(flow and cpp_ax.traits_underflow) overflow = int(flow and cpp_ax.traits_overflow) edges: np.typing.NDArray[np.float64] = cpp_ax.edges if underflow or overflow: full = np.empty(len(edges) + underflow + overflow, dtype=np.float64) full[underflow : underflow + len(edges)] = edges if underflow: full[0] = cpp_ax.value(-1) if overflow: full[-1] = cpp_ax.value(cpp_ax.size + 1) edges = full if not isinstance(cpp_ax, _no_nudge_cpp_axes): last = cpp_ax.size + underflow edges[last] = np.nextafter(edges[last], -np.inf) return edges def _expand_ellipsis(indexes: Iterable[Any], rank: int) -> list[Any]: indexes = list(indexes) # Compare by identity: ``==`` is ambiguous when indexes contain NumPy arrays. ellipsis_positions = [i for i, ind in enumerate(indexes) if ind is Ellipsis] number_ellipses = len(ellipsis_positions) if number_ellipses == 0: return indexes if number_ellipses == 1: index = ellipsis_positions[0] additional = rank + 1 - len(indexes) if additional < 0: raise IndexError("too many indices for histogram") # Fill out the ellipsis with empty slices return indexes[:index] + [slice(None)] * additional + indexes[index + 1 :] raise IndexError("an index can only have a single ellipsis ('...')") def _combine_group_contents( new_view: np.typing.NDArray[Any], reduced_view: np.typing.NDArray[Any], i: int, j: int, jj: int, ) -> None: """ Add bin ``j`` (along view dimension ``i``) of ``reduced_view`` into bin ``jj`` of ``new_view``, in-place. Used for rebinning with groups. """ pos = [slice(None)] * (i) if new_view.dtype.names: for field in new_view.dtype.names: new_view[(*pos, jj, ...)][field] += reduced_view[(*pos, j, ...)][field] # type: ignore[arg-type] else: new_view[(*pos, jj, ...)] += reduced_view[(*pos, j, ...)] # type: ignore[arg-type] H = TypeVar("H", bound="Histogram[Any]") S = TypeVar("S", bound="Storage") NO_METADATA = object() # We currently do not cast *to* a histogram, but this is consistent # and could be used later. @register(_histograms) # type: ignore[arg-type] class Histogram(typing.Generic[S]): # Note this is a __slots__ __dict__ class! __slots__ = ( "__dict__", "_hist", "axes", ) # .metadata and ._variance_known are part of the dict. # .metadata will not be placed in the dict if not passed. _family: ClassVar[object] = boost_histogram axes: AxesTuple _hist: CppHistogram _variance_known: bool def __init_subclass__(cls, *, family: object | None = None) -> None: """ Sets the family for the histogram. This should be a unique object (such as the main module of your package) that is consistently set across all subclasses. When converting back from C++, casting will try to always pick the best matching family from the loaded subclasses for Axis and such. """ super().__init_subclass__() cls._family = family if family is not None else object() @typing.overload def __init__( self, arg: Histogram[S], /, *, metadata: Any = ..., __dict__: Any = ... ) -> None: ... @typing.overload def __init__( self, arg: dict[str, Any], /, *, metadata: Any = ..., __dict__: Any = ... ) -> None: ... @typing.overload def __init__( self, arg: CppHistogram, /, *, metadata: Any = ..., __dict__: Any = ... ) -> None: ... @typing.overload def __init__( self, *axes: Axis | CppAxis, storage: S, metadata: Any = ..., __dict__: Any = ..., ) -> None: ... @typing.overload def __init__( self: Histogram[Double], *axes: Axis | CppAxis, storage: None = ..., metadata: Any = ..., __dict__: Any = ..., ) -> None: ... def __init__( self, *axes: Axis | CppAxis | Histogram[Any] | CppHistogram | dict[str, Any], storage: S | None = None, metadata: Any = NO_METADATA, __dict__: Any = None, ) -> None: """ Construct a new histogram. If you pass in a single argument, this will be treated as a histogram and this will convert the histogram to this type of histogram. Parameters ---------- *args : Axis Provide 1 or more axis instances. storage : Storage = bh.storage.Double() Select a storage to use in the histogram metadata : Any = None Data that is passed along if a new histogram is created. No not use in new code; use ``__dict__`` instead. __dict__ : Any = None Better way to set metadata. """ self._variance_known = True storage_err_msg = "storage= is not allowed with conversion constructor" if metadata is not NO_METADATA and __dict__: msg = ( "Can't set both metadata and __dict__. Set the 'metadata' key instead." ) raise TypeError(msg) if metadata is not NO_METADATA: __dict__ = {"metadata": metadata} if __dict__ is None: __dict__ = {} # Allow construction from a raw histogram object (internal) if len(axes) == 1 and isinstance(axes[0], _histogram_types): if storage is not None: raise TypeError(storage_err_msg) cpp_hist: CppHistogram = axes[0] self._from_histogram_cpp(cpp_hist, __dict__=__dict__) return # If we construct with another Histogram as the only positional argument, # support that too if len(axes) == 1 and isinstance(axes[0], Histogram): if storage is not None: raise TypeError(storage_err_msg) self._from_histogram_object(axes[0], __dict__=__dict__) return # Support objects that provide a to_boost method, like Uproot if len(axes) == 1 and hasattr(axes[0], "_to_boost_histogram_"): if storage is not None: raise TypeError(storage_err_msg) self._from_histogram_object( axes[0]._to_boost_histogram_(), __dict__=__dict__ ) return # Support UHI if len(axes) == 1 and isinstance(axes[0], dict) and "uhi_schema" in axes[0]: if storage is not None: raise TypeError(storage_err_msg) self._from_histogram_object( serialization.from_uhi(axes[0]), __dict__=__dict__ ) return resolved_storage = Double() if storage is None else storage self.__dict__.update(__dict__) # Check for missed parenthesis or incorrect types if not isinstance(resolved_storage, Storage): if isinstance(resolved_storage, type) and issubclass( # type: ignore[unreachable] resolved_storage, Storage ): msg = f"Storages need to be initialized; use {resolved_storage.__name__}() instead. Please add ()." raise TypeError(msg) msg = f"Only storages allowed in storage argument, got {resolved_storage!r}" raise TypeError(msg) # Allow a tuple to represent a regular axis axes = tuple(_arg_shortcut(arg) for arg in axes) # type: ignore[arg-type] if len(axes) > _core.hist._axes_limit: msg = f"Too many axes, must be less than {_core.hist._axes_limit}" raise IndexError(msg) # Check all available histograms, and if the storage matches, return that one for h in _histograms: if isinstance(resolved_storage, h._storage_type): self._hist = h(axes, resolved_storage) # type: ignore[arg-type] self.axes = self._generate_axes_() return raise TypeError("Unsupported storage") @classmethod def _clone( cls, _hist: Histogram[Any] | CppHistogram, *, other: Histogram[Any] | None = None, memo: Any = NOTHING, ) -> Self: """ Clone a histogram (possibly of a different base). Does not trigger __init__. This will copy data from `other=` if non-None, otherwise metadata gets copied from the input. """ self = cls.__new__(cls) if isinstance(_hist, _histogram_types): self._from_histogram_cpp(_hist, __dict__={}) if other is not None: return cls._clone(self, other=other, memo=memo) return self assert isinstance(_hist, Histogram) if other is None: other = _hist if memo is NOTHING: dict_copy = copy.copy(other.__dict__) else: dict_copy = copy.deepcopy(other.__dict__, memo) self._from_histogram_object(_hist, __dict__=dict_copy) for i, ax in enumerate(self.axes): if memo is NOTHING: new_metadata = copy.copy(ax._ax.raw_metadata) else: new_metadata = copy.deepcopy(ax._ax.raw_metadata, memo) # A growth axis wrapper holds a copy, so set the stored axis too. self._hist._set_axis_metadata(i, new_metadata) ax._ax.raw_metadata = new_metadata ax.__dict__ = new_metadata return self def _new_hist(self, _hist: CppHistogram, memo: Any = NOTHING) -> Self: """ Return a new histogram given a new _hist, copying current metadata. """ return self.__class__._clone(_hist, other=self, memo=memo) def _from_histogram_cpp( self, other: CppHistogram, *, __dict__: dict[str, Any] ) -> None: """ Import a Cpp histogram. """ self._variance_known = True self._hist = other self.__dict__.update(__dict__) self.axes = self._generate_axes_() def _from_histogram_object( self, other: Histogram[S], *, __dict__: dict[str, Any] ) -> None: """ Convert self into a new histogram object based on another, possibly converting from a different subclass. """ self._hist = other._hist self.__dict__ = copy.copy(other.__dict__) self.axes = self._generate_axes_() for ax in self.axes: # Give each axis its own metadata dict, so mutating it here does # not alias with the axes of `other` (or of each other). ax._ax.raw_metadata = copy.copy(ax._ax.raw_metadata) ax.__dict__ = ax._ax.raw_metadata self.__dict__.update(__dict__) # Allow custom behavior on either "from" or "to" other._export_bh_(self) self._import_bh_() def _import_bh_(self) -> None: """ If any post-processing is needed to pass a histogram between libraries, a subclass can implement it here. self is the new instance in the current (converted-to) class. """ @classmethod def _export_bh_(cls, self: Histogram[Any]) -> None: """ If any preparation is needed to pass a histogram between libraries, a subclass can implement it here. cls is the current class being converted from, and self is the instance in the class being converted to. """ def _generate_axes_(self) -> AxesTuple: """ This is called to fill in the axes. Subclasses can override it if they need to change the axes tuple. """ return AxesTuple(self._axis(i) for i in range(self.ndim)) @property def _has_growth(self) -> bool: """ True if any axis can grow. Growing invalidates views and axis objects. """ return any(ax._ax.traits_growth for ax in self.axes) # Backward compat for metadata default def __getattr__(self, name: str) -> Any: if name == "metadata": msg = ".metadata was not set, returning None instead of Attribute error, boost-histogram 1.9+ will error." warnings.warn(msg, FutureWarning, stacklevel=2) return None return super().__getattribute__(name) def _to_uhi_(self) -> dict[str, Any]: """ Convert to a UHI histogram. """ return serialization.to_uhi(self) @classmethod def _from_uhi_(cls, inp: dict[str, Any], /) -> Self: """ Convert from a UHI histogram. """ return cls(serialization.from_uhi(inp)) @property def ndim(self) -> int: """ Number of axes (dimensions) of the histogram. """ return self._hist.rank() @typing.overload def view( self: Histogram[bhs.Double] | Histogram[bhs.MultiCell] | Histogram[bhs.Unlimited], flow: bool = False, ) -> np.typing.NDArray[np.float64]: ... @typing.overload def view( self: Histogram[bhs.Int64] | Histogram[bhs.AtomicInt64], flow: bool = False, ) -> np.typing.NDArray[np.int64]: ... @typing.overload def view(self: Histogram[bhs.Weight], flow: bool = False) -> WeightedSumView: ... @typing.overload def view(self: Histogram[bhs.Mean], flow: bool = False) -> MeanView: ... @typing.overload def view( self: Histogram[bhs.WeightedMean], flow: bool = False ) -> WeightedMeanView: ... @typing.overload def view( self: Histogram[Any], flow: bool = False ) -> ( np.typing.NDArray[np.float64] | np.typing.NDArray[np.int64] | WeightedSumView | WeightedMeanView | MeanView ): ... def view( self, flow: bool = False ) -> ( np.typing.NDArray[np.float64] | np.typing.NDArray[np.int64] | WeightedSumView | WeightedMeanView | MeanView ): """ Return a view into the data, optionally with overflow turned on. The view shares memory with the histogram. If the histogram has a growth axis, a fill that grows an axis moves the data, and the view then points at freed memory. Take a copy before such a fill. """ return _to_view(self._hist.view(flow)) def __array__( self, dtype: np.typing.DTypeLike | None = None, *, # pylint: disable-next=redefined-outer-name copy: bool | None = None, ) -> np.typing.NDArray[Any]: # The copy kw is new in NumPy 2.0 kwargs = {} if copy is not None: kwargs["copy"] = copy return np.asarray(self.view(False), dtype=dtype, **kwargs) # type: ignore[call-overload, no-any-return] __hash__ = None # type: ignore[assignment] def allclose( self, other: object, *, rtol: float = 1e-05, atol: float = 1e-08, equal_nan: bool = False, flow: bool = True, metadata: bool = False, ) -> bool: """ Check whether two histograms are close to each other. Parameters ---------- other : Histogram The histogram to compare against. rtol : float = 1e-05 Relative tolerance for comparing edges and bins. atol : float = 1e-08 Absolute tolerance for comparing edges and bins. equal_nan : bool = False Whether to compare NaNs as equal. flow : bool = True Whether to include underflow and overflow bins in the comparison. metadata : bool = False Whether to compare histogram and axis metadata. Returns ------- bool True if the histograms are close, False otherwise. """ if not isinstance(other, Histogram): return False if self.ndim != other.ndim: return False if self.storage_type != other.storage_type: return False if metadata and self.__dict__ != other.__dict__: return False for i in range(self.ndim): ax1 = self.axes[i] ax2 = other.axes[i] if ax1.size != ax2.size: return False if metadata and ax1.__dict__ != ax2.__dict__: return False if ax1.traits.continuous != ax2.traits.continuous: return False if ax1.traits.continuous: if not np.allclose( ax1.edges, ax2.edges, rtol=rtol, atol=atol, equal_nan=equal_nan, ): return False else: if ax1.traits.ordered != ax2.traits.ordered: return False if list(ax1) != list(ax2): return False v1 = self.view(flow=flow) v2 = other.view(flow=flow) if v1.shape != v2.shape: return False if v1.dtype.names: for name in v1.dtype.names: if not np.allclose( v1[name], # type: ignore[index] v2[name], # type: ignore[index] rtol=rtol, atol=atol, equal_nan=equal_nan, ): return False elif not np.allclose( v1, v2, rtol=rtol, atol=atol, equal_nan=equal_nan, ): return False return True def __eq__(self, other: object) -> bool: return hasattr(other, "_hist") and self._hist == other._hist def __add__(self, other: Histogram[S] | np.typing.NDArray[Any] | float) -> Self: result = self.copy(deep=False) return result.__iadd__(other) def __iadd__(self, other: Histogram[S] | np.typing.NDArray[Any] | float) -> Self: if isinstance(other, (int, float)) and other == 0: return self self._compute_inplace_op("__iadd__", other) # Addition may change the axes if they can grow self.axes = self._generate_axes_() return self def __radd__(self, other: np.typing.NDArray[Any] | float) -> Self: return self + other def __sub__(self, other: Histogram[S] | np.typing.NDArray[Any] | float) -> Self: result = self.copy(deep=False) return result.__isub__(other) def __isub__(self, other: Histogram[S] | np.typing.NDArray[Any] | float) -> Self: if isinstance(other, (int, float)) and other == 0: return self self._compute_inplace_op("__isub__", other) self.axes = self._generate_axes_() return self def __rsub__(self, other: np.typing.NDArray[Any] | float) -> Self: # Subtraction is not commutative, so unlike __radd__ this cannot defer # to the forward operator: other - self == -(self - other). return (self - other) * -1 # If these fail, the underlying object throws the correct error def __mul__(self, other: Histogram[S] | np.typing.NDArray[Any] | float) -> Self: result = self.copy(deep=False) return result.__imul__(other) def __rmul__(self, other: np.typing.NDArray[Any] | float) -> Self: return self * other def __truediv__(self, other: Histogram[S] | np.typing.NDArray[Any] | float) -> Self: result = self.copy(deep=False) return result.__itruediv__(other) def __itruediv__( self, other: Histogram[S] | np.typing.NDArray[Any] | float ) -> Self: # Division should produce floating-point results, so promote integer # storages to Double. Histogram/histogram division in C++ requires both # operands to share a storage, so the divisor is promoted to match. self._convert_int_storage_to_double() if isinstance(other, Histogram): other = self._as_double_cpp(other._hist) # type: ignore[assignment] elif isinstance(other, _histogram_types): other = self._as_double_cpp(other) # type: ignore[assignment] return self._compute_inplace_op("__itruediv__", other) def __rtruediv__(self, other: np.typing.NDArray[Any] | float) -> Self: # Division is not commutative, so unlike __rmul__ this cannot defer to # the forward operator: divide other by each cell. Promote integer # storages to Double first, matching __itruediv__. result = self.copy(deep=False) result._convert_int_storage_to_double() view = result.view(flow=True) # Empty bins divide to inf/nan; suppress the warnings as elsewhere. with np.errstate(divide="ignore", invalid="ignore"): np.true_divide(other, view, out=view) result._variance_known = False return result def __imul__(self, other: Histogram[S] | np.typing.NDArray[Any] | float) -> Self: # Multiplying by a non-integer scalar/array should promote an integer # storage to Double, matching __itruediv__, instead of leaking a # numpy UFuncTypeError from the in-place view multiply. if not isinstance(other, (Histogram, *_histogram_types)) and not np.issubdtype( np.asarray(other).dtype, np.integer ): self._convert_int_storage_to_double() return self._compute_inplace_op("__imul__", other) @staticmethod def _as_double_cpp(cpp_hist: CppHistogram) -> CppHistogram: """ Return a Double-storage copy of an integer-storage (Int64/AtomicInt64) C++ histogram, so that division produces floating-point results instead of truncating. Returns the input unchanged for storages that are already floating point or richer. """ if cpp_hist._storage_type not in { _core.storage.int64, _core.storage.atomic_int64, }: return cpp_hist cpp_axes = [cpp_hist.axis(i) for i in range(cpp_hist.rank())] new_hist = _core.hist.any_double(cpp_axes, _core.storage.double()) new_hist.view(flow=True)[...] = cpp_hist.view(flow=True) return new_hist def _convert_int_storage_to_double(self) -> None: """ Convert an integer storage to Double in place (see _as_double_cpp). """ new_hist = self._as_double_cpp(self._hist) if new_hist is not self._hist: self._hist = new_hist # The axes belong to the old histogram; point them at the new one. self.axes = self._generate_axes_() def _hist_inplace_op(self, name: str, other: CppHistogram) -> None: if self._hist._storage_type is not other._storage_type: symbol = _INPLACE_OP_SYMBOLS.get(name, name) other_storage = typing.cast( type[Storage], cast(self, other._storage_type, Storage) ) msg = ( f"Cannot {symbol} histograms with different storage types: " f"{self.storage_type.__name__} and {other_storage.__name__}" ) raise TypeError(msg) # The underlying C++ histogram only exposes the in-place dunders its # storage supports (e.g. weight/mean/multi_cell storages have no # __isub__). Calling a missing one raises a confusing AttributeError # leaking the dunder name, so surface a clear error instead. op = getattr(self._hist, name, None) if op is None: symbol = _INPLACE_OP_SYMBOLS.get(name, name) msg = ( f"The {self.storage_type.__name__} storage does not support the " f"{symbol!r} operation between two histograms" ) raise TypeError(msg) op(other) def _compute_inplace_op( self, name: str, other: Histogram[S] | np.typing.NDArray[Any] | float ) -> Self: # Also takes CppHistogram, but that confuses mypy because it's hard to pick out if isinstance(other, Histogram): self._hist_inplace_op(name, other._hist) elif isinstance(other, _histogram_types): self._hist_inplace_op(name, other) elif hasattr(other, "shape") and other.shape: assert not isinstance(other, float) if len(other.shape) != self.ndim: msg = f"Number of dimensions {len(other.shape)} must match histogram {self.ndim}" raise ValueError(msg) if all(a in {b, 1} for a, b in zip(other.shape, self.shape, strict=False)): view = self.view(flow=False) getattr(view, name)(other) elif all( a in {b, 1} for a, b in zip(other.shape, self.axes.extent, strict=False) ): view = self.view(flow=True) getattr(view, name)(other) else: msg = f"Wrong shape {other.shape}, expected {self.shape} or {self.axes.extent}" raise ValueError(msg) else: view = self.view(flow=True) getattr(view, name)(other) self._variance_known = False return self # TODO: Marked as too complex by flake8. Should be factored out a bit. def fill( self, *args: ArrayLike | str, weight: ArrayLike | None = None, sample: ArrayLike | None = None, threads: int | None = None, ) -> Self: """ Insert data into the histogram. Parameters ---------- *args : Union[Array[float], Array[int], Array[str], float, int, str] Provide one value or array per dimension. weight : list[Union[Array[float], Array[int], float, int, str]]] Provide weights (only if the histogram storage supports it) sample : list[Union[Array[float], Array[int], Array[str], float, int, str]]] Provide samples (only if the histogram storage supports it) threads : Optional[int] Fill with threads. Defaults to None, which does not activate threaded filling. Using 0 will automatically pick the number of available threads (usually two per core). A continuous growth axis (such as a growing Regular axis) cannot be merged across threads yet, so such a fill raises instead. """ if self._hist._storage_type is _core.storage.mean: mean_storage_sample_check(sample) if ( self._hist._storage_type not in { _core.storage.weight, _core.storage.mean, _core.storage.weighted_mean, } and weight is not None ): self._variance_known = False if self._hist._storage_type is _core.storage.multi_cell: # Use weight keyword for MultiCell filling even though it uses sample on the C++ backend sample = weight weight = None # Convert to NumPy arrays args_ars = _fill_cast(args) weight_ars = _fill_cast(weight) sample_ars = _fill_cast(sample) # Broadcast scalar positional args to match sample length when sample is an array. # This allows e.g. h.fill(0, sample=[1, 2, 3]) to work for Mean/WeightedMean storage. if sample_ars is not None: sample_arr = np.asarray(sample_ars) if sample_arr.ndim > 0: sample_len = len(sample_arr) if sample_len > 1: args_ars = tuple( np.full(sample_len, a) if np.ndim(a) == 0 else a for a in args_ars ) if threads == 0: threads = cpu_count() growth = self._has_growth if threads is None or threads == 1: self._hist.fill(*args_ars, weight=weight_ars, sample=sample_ars) if growth: # A growing fill replaces the axes. self.axes = self._generate_axes_() return self if self._hist._storage_type in { _core.storage.mean, _core.storage.weighted_mean, }: raise RuntimeError("Mean histograms do not support threaded filling") # If everything is scalar, there is only a single fill; threading would # incorrectly repeat it, so fill directly instead. if ( all(isinstance(a, str) or np.ndim(a) == 0 for a in args_ars) and (weight_ars is None or np.ndim(weight_ars) == 0) and (sample_ars is None or np.ndim(sample_ars) == 0) ): self._hist.fill(*args_ars, weight=weight_ars, sample=sample_ars) if growth: self.axes = self._generate_axes_() return self data: list[list[Any]] = [] for a in args_ars: if isinstance(a, str) or np.ndim(a) == 0: # Scalars broadcast against each thread's chunk data.append([a] * threads) else: data.append(list(np.array_split(np.asarray(a), threads))) weights: list[Any] if weight_ars is None or np.ndim(weight_ars) == 0: assert threads is not None weights = [weight_ars] * threads else: weights = np.array_split(np.asarray(weight_ars), threads) samples: list[Any] if sample_ars is None or np.ndim(sample_ars) == 0: assert threads is not None samples = [sample_ars] * threads else: samples = np.array_split(np.asarray(sample_ars), threads) if self._hist._storage_type is _core.storage.atomic_int64: def work( weight: ArrayLike | None, sample: ArrayLike | None, *args: np.typing.NDArray[Any], ) -> None: self._hist.fill(*args, weight=weight, sample=sample) else: sum_lock = threading.Lock() def work( weight: ArrayLike | None, sample: ArrayLike | None, *args: np.typing.NDArray[Any], ) -> None: # The copy and the merge both release the GIL, so a copy made # while another worker merges would read a histogram that is # changing. The lock keeps every access to self._hist ordered. with sum_lock: local_hist = copy.copy(self._hist) local_hist.reset() local_hist.fill(*args, weight=weight, sample=sample) with sum_lock: self._hist += local_hist # A worker exception must not be lost; the histogram would silently # hold too few entries. errors: list[Exception] = [] error_lock = threading.Lock() def fun( weight: ArrayLike | None, sample: ArrayLike | None, *args: np.typing.NDArray[Any], ) -> None: try: work(weight, sample, *args) # Any worker error must reach the caller, so catch them all except Exception as err: # noqa: BLE001 # pylint: disable=broad-exception-caught with error_lock: errors.append(err) thread_list = [ threading.Thread(target=fun, args=arrays) for arrays in zip(weights, samples, *data, strict=False) ] for thread in thread_list: thread.start() for thread in thread_list: thread.join() if errors: raise errors[0] if growth: # The merge of the per-thread copies replaces the axes. self.axes = self._generate_axes_() return self def __str__(self) -> str: """ A rendering of the histogram is made using ASCII or unicode characters (whatever is supported by the terminal). What exactly is displayed is still experimental. Do not rely on any particular rendering. """ # TODO check the terminal width and adjust the presentation # only use for 1D, fall back to repr for ND if self._hist.rank() != 1: return repr(self) s = str(self._hist) # get rid of first line and last character return s[s.index("\n") + 1 : -1] def _axis(self, i: int = 0) -> Axis: """ Get N-th axis. """ return cast(self, self._hist.axis(i), Axis) @property def storage_type(self) -> type[S]: return cast(self, self._hist._storage_type, Storage) # type: ignore[return-value] @property def _storage_type(self) -> type[S]: warnings.warn( "Accessing storage type has changed from _storage_type to storage_type, and will be removed in future.", FutureWarning, stacklevel=2, ) return cast(self, self._hist._storage_type, Storage) # type: ignore[return-value] @property def storage(self) -> S: """ New storage matching the one the histogram was constructed with. """ if issubclass(self.storage_type, bhs.MultiCell): return self.storage_type(self._hist.nelem()) # type: ignore[attr-defined] return self.storage_type() def _reduce(self, *args: Any) -> Self: return self._new_hist(self._hist.reduce(*args)) def __copy__(self) -> Self: return self._new_hist(copy.copy(self._hist)) def __deepcopy__(self, memo: Any) -> Self: return self._new_hist(copy.deepcopy(self._hist), memo=memo) def __getstate__(self) -> tuple[int, dict[str, Any]]: """ Version 0.8: metadata added Version 0.11: version added and set to 0. metadata/_hist replaced with dict. Version 0.12: _variance_known is now in the dict (no format change) ``dict`` contains __dict__ with added "_hist" """ local_dict = copy.copy(self.__dict__) local_dict["_hist"] = self._hist # Version 0 of boost-histogram pickle state return (0, local_dict) def __setstate__(self, state: Any) -> None: if isinstance(state, tuple): if state[0] == 0: for key, value in state[1].items(): setattr(self, key, value) # Added in 0.12 if "_variance_known" not in state[1]: self._variance_known = True else: msg = f"Cannot open boost-histogram pickle v{state[0]}" raise RuntimeError(msg) else: # Classic (0.10 and before) state self._hist = state["_hist"] self._variance_known = True self.metadata = state.get("metadata", None) for i in range(self._hist.rank()): self._hist._set_axis_metadata( i, {"metadata": self._hist.axis(i).raw_metadata} ) self.axes = self._generate_axes_() def __repr__(self) -> str: newline = "\n " first_newline = newline if len(self.axes) > 1 else "" storage_newline = ( newline if len(self.axes) > 1 else " " if len(self.axes) > 0 else "" ) sep = "," if len(self.axes) > 0 else "" ret = f"{self.__class__.__name__}({first_newline}" ret += f",{newline}".join(repr(ax) for ax in self.axes) ret += f"{sep}{storage_newline}storage={self.storage}" ret += ")" outer = self.sum(flow=True) # Accumulators (Mean, WeightedSum, ...) have no __bool__, so they are # always truthy; compare against a fresh instance to detect "empty". if isinstance(outer, (int, float)): non_empty = bool(outer) else: non_empty = outer != type(outer)() if non_empty: inner = self.sum(flow=False) ret += f" # Sum: {inner}" if inner != outer: ret += f" ({outer} with flow)" return ret def _compute_uhi_index(self, index: InnerIndexing, axis: int) -> SimpleIndexing: """ Converts an expression that contains UHI locators to one that does not. """ # Support sum and rebin directly if index is sum or hasattr(index, "factor"): # type: ignore[comparison-overlap,redundant-expr] return slice(None, None, index) # General locators # Note that MyPy doesn't like these very much - the fix # will be to properly set input types if callable(index): return index(self.axes[axis]) # NumPy integer arrays pass through untouched; they trigger vectorized # gather/scatter in __getitem__/__setitem__ rather than per-element access. if isinstance(index, np.ndarray): return index if isinstance(index, float): raise TypeError(f"Index {index} must be an integer, not float") if isinstance(index, SupportsIndex): idx = int(index) size: int = self._hist.axis(axis).size if not -size <= idx < size: raise IndexError("histogram index is out of range") return idx % size return index def _compute_commonindex( self, index: IndexingExpr ) -> list[SupportsIndex | slice | Mapping[int, SupportsIndex | slice]]: """ Takes indices and returns two iterables; one is a tuple or dict of the original, Ellipsis expanded index, and the other returns index, operation value pairs. """ indexes: list[Any] # Shorten the computations with direct access to raw object hist = self._hist # Support dict access if hasattr(index, "items"): indexes = [slice(None)] * hist.rank() for k, v in index.items(): indexes[k] = v # Normalize -> h[i] == h[i,] else: tuple_index = (index,) if not isinstance(index, tuple) else index # Now a list indexes = _expand_ellipsis(tuple_index, hist.rank()) if len(indexes) != hist.rank(): raise IndexError("Wrong number of indices for histogram") # Allow [bh.loc(...)] to work # TODO: could be nicer making a new list via a comprehension for i in range(len(indexes)): # pylint: disable=consider-using-enumerate # Support list of UHI indexers if isinstance(indexes[i], list): indexes[i] = [self._compute_uhi_index(ind, i) for ind in indexes[i]] else: indexes[i] = self._compute_uhi_index(indexes[i], i) return indexes def _flow_pick_index(self, axis: int, idx: int) -> int: """ Map a UHI-resolved scalar index for ``axis`` to an offset into the ``flow=True`` view. Regular indices are 0..size-1; the sentinels -1 and size (produced by ``bh.underflow``/``bh.overflow``/``bh.loc`` on an out-of-range value) select the flow bins, raising IndexError if the axis does not have the requested flow bin. """ traits = self.axes[axis].traits size = self._hist.axis(axis).size offset = 1 if traits.underflow else 0 if idx == -1: if not traits.underflow: raise IndexError(f"Axis {axis} has no underflow bin") return 0 if idx == size: if not traits.overflow: raise IndexError(f"Axis {axis} has no overflow bin") return size + offset return idx + offset @staticmethod def _flow_slice_bound(v: int | None, default: int, size: int, offset: int) -> int: """ Resolve one bound of a plain integer slice used in vectorized indexing: None and negative values are resolved the same way NumPy would on the flow=False view, then shifted onto the flow=True view. """ if v is None: v = default elif v < 0: v += size return v + offset def _compute_vectorized_index(self, indexes: list[Any]) -> tuple[Any, ...]: """ Build a NumPy fancy-index tuple from already-normalized indexes for vectorized cell access (gather/scatter) into the ``flow=True`` view. Each axis may be an integer array, an integer (including the ``bh.underflow``/``bh.overflow`` sentinels), or a plain integer slice. Rebin/sum/locator slices and categorical lists are rejected with a pointer to ``.view()``. """ view_index: list[Any] = [] for i, ind in enumerate(indexes): offset = 1 if self.axes[i].traits.underflow else 0 if isinstance(ind, np.ndarray): view_index.append(ind + offset) elif isinstance(ind, slice): if ind.step is not None or not all( s is None or isinstance(s, int) for s in (ind.start, ind.stop) ): msg = ( f"Vectorized (array) indexing on axis {i} only supports plain " "integer slices; use .view() for rebin, sum, or locator slices" ) raise IndexError(msg) # Bounds are plain non-flow indices (matching scalar/array # indexing semantics: no flow bins unless asked for), with # None and negative values resolved the same way as NumPy # would on the flow=False view, then shifted onto the # flow=True view. size = self._hist.axis(i).size start = self._flow_slice_bound(ind.start, 0, size, offset) stop = self._flow_slice_bound(ind.stop, size, size, offset) view_index.append(slice(start, stop)) elif isinstance(ind, SupportsIndex): view_index.append(self._flow_pick_index(i, ind.__index__())) else: msg = ( "Vectorized (array) indexing only supports integer arrays, " f"integers, and integer slices; got {type(ind).__name__} on axis {i}" ) raise IndexError(msg) if isinstance(self._hist, _core.hist.any_multi_cell): # The buffer of a MultiCell histogram has the cell index as its first # dimension, which is not part of the user-facing axis indexing. view_index.insert(0, slice(None, None, None)) return tuple(view_index) @typing.overload def to_numpy( self, flow: bool = ..., *, dd: Literal[False] = ..., view: bool = ... ) -> tuple[np.typing.NDArray[Any], ...]: ... @typing.overload def to_numpy( self, flow: bool = ..., *, dd: Literal[True], view: bool = ... ) -> tuple[np.typing.NDArray[Any], tuple[np.typing.NDArray[np.float64], ...]]: ... @typing.overload def to_numpy( self, flow: bool = ..., *, dd: bool, view: bool = ... ) -> ( tuple[np.typing.NDArray[Any], ...] | tuple[np.typing.NDArray[Any], tuple[np.typing.NDArray[np.float64], ...]] ): ... def to_numpy( self, flow: bool = False, *, dd: bool = False, view: bool = False ) -> ( tuple[np.typing.NDArray[Any], ...] | tuple[np.typing.NDArray[Any], tuple[np.typing.NDArray[np.float64], ...]] ): """ Convert to a NumPy style tuple of return arrays. Edges are converted to match NumPy standards, with upper edge inclusive, unlike boost-histogram, where upper edge is exclusive. Parameters ---------- flow : bool = False Include the flow bins. dd : bool = False Use the histogramdd return syntax, where the edges are in a tuple. Otherwise, this is the histogram/histogram2d return style. view : bool = False The behavior for the return value. By default, this will return array of the values only regardless of the storage (which is all NumPy's histogram function can do). view=True will return the boost-histogram view of the storage. Return ------ contents : Array[Any] The bin contents *edges : Array[float] The edges for each dimension """ hist = self.view(flow=flow) if view else self.values(flow=flow) # Compute the edges directly; this avoids the deep copy of the bin # contents that the C++ ``to_numpy`` helper would make. edges = [ _numpy_compatible_edges(self._hist.axis(i), flow) for i in range(self.ndim) ] return (hist, edges) if dd else (hist, *edges) # type: ignore[return-value] def copy(self, *, deep: bool = True) -> Self: """ Make a copy of the histogram. Defaults to making a deep copy (axis metadata copied); use deep=False to avoid making a copy of axis metadata. """ return copy.deepcopy(self) if deep else copy.copy(self) def reset(self) -> Self: """ Clear the bin counters. """ self._hist.reset() return self def empty(self, flow: bool = False) -> bool: """ Check to see if the histogram has any non-default values. You can use flow=True to check flow bins too. """ return self._hist.empty(flow) @typing.overload def sum( self: Histogram[bhs.Double] | Histogram[bhs.Int64] | Histogram[bhs.AtomicInt64] | Histogram[bhs.Unlimited], flow: bool = False, ) -> float: ... @typing.overload def sum(self: Histogram[bhs.MultiCell], flow: bool = False) -> list[float]: ... @typing.overload def sum(self: Histogram[bhs.Weight], flow: bool = False) -> WeightedSum: ... @typing.overload def sum(self: Histogram[bhs.Mean], flow: bool = False) -> Mean: ... @typing.overload def sum(self: Histogram[bhs.WeightedMean], flow: bool = False) -> WeightedMean: ... @typing.overload def sum(self, flow: bool = False) -> float | Accumulator | list[float]: ... def sum(self, flow: bool = False) -> float | Accumulator | list[float]: """ Compute the sum over the histogram bins (optionally including the flow bins). """ return self._hist.sum(flow) # type: ignore[no-any-return] @property def size(self) -> int: """ Total number of bins in the histogram (including underflow/overflow). """ return self._hist.size() @property def shape(self) -> tuple[int, ...]: """ Tuple of axis sizes (not including underflow/overflow). """ return self.axes.size @typing.overload def __getitem__(self: FloatHists, index: IndexingExpr) -> FloatHists | float: ... @typing.overload def __getitem__(self: IntHists, index: IndexingExpr) -> IntHists | int: ... @typing.overload def __getitem__( self: ListHists, index: IndexingExpr ) -> ListHists | list[float]: ... @typing.overload def __getitem__( self: WeightHists, index: IndexingExpr ) -> WeightHists | WeightedSum: ... @typing.overload def __getitem__(self: MeanHists, index: IndexingExpr) -> MeanHists | Mean: ... @typing.overload def __getitem__( self: WeightedMeanHists, index: IndexingExpr ) -> WeightedMeanHists | WeightedMean: ... @typing.overload def __getitem__( self, index: IndexingExpr ) -> Self | float | list[float] | int | Accumulator: ... def __getitem__( self, index: IndexingExpr ) -> Self | float | Accumulator | list[float] | int | np.typing.NDArray[Any]: indexes = self._compute_commonindex(index) # Vectorized (NumPy array) indexing gathers scattered cells through the # buffer instead of building a new histogram. Only ndarray indices # trigger this; lists keep their categorical pick semantics. if any(isinstance(a, np.ndarray) for a in indexes): return self.view(flow=True)[self._compute_vectorized_index(indexes)] # Early return for all-integer case if all(isinstance(a, SupportsIndex) for a in indexes): return self._hist.at(*indexes) # type: ignore[no-any-return, arg-type] integrations = set[int]() slices = list[_core.algorithm.reduce_command]() pick_each = dict[int, int]() pick_set = dict[int, list[int]]() reduced: CppHistogram | None = None for i, ind in enumerate(indexes): match ind: case SupportsIndex(): pick_each[i] = self._flow_pick_index(i, ind.__index__()) # str/bytes are Sequences but not valid indices; they fall # through to the IndexError below. case collections.abc.Sequence() if not isinstance(ind, (str, bytes)): # type: ignore[unreachable] pick_set[i] = list(ind) case slice(start=start, stop=stop, step=step): reduced, new_slices, new_integrations = self._handle_slice( i, start, stop, step, reduced ) slices.extend(new_slices) integrations.update(new_integrations) case _: raise IndexError( "Must be a slice, an integer, or follow the locator protocol." ) if (slices or pick_set or pick_each or integrations) and not reduced: reduced = self._hist elif not reduced: reduced = copy.copy(self._hist) if pick_each: tuple_slice = tuple( pick_each.get(i, slice(None)) for i in range(reduced.rank()) ) if isinstance(self._hist, _core.hist.any_multi_cell): # View of multi cell histograms has as first (index 0) dimension the cell index # Add a full slice to the beginning of the slicing expression to adept for this cell index # e.g. a slice like [0, :, 3] is converted to [:, 0, :, 3] tuple_slice = (slice(None, None, None), *tuple_slice) logger.debug("Slices for pick each: %s", tuple_slice) axes = [ reduced.axis(i) for i in range(reduced.rank()) if i not in pick_each ] logger.debug("Axes: %s", axes) new_reduced: _core.hist._BaseHistogram | _core.hist.any_multi_cell = ( reduced.__class__(axes) ) if isinstance(reduced, _core.hist.any_multi_cell) and isinstance( new_reduced, _core.hist.any_multi_cell ): # The constructor in reduced.__class__(axes) does not take care of the number of cells. # If reduced is a multi cell histogram, we have to set the number of cells per bin manually for new_reduced new_reduced.reset_nelem(reduced.nelem()) new_reduced.view(flow=True)[...] = reduced.view(flow=True)[tuple_slice] reduced = new_reduced integrations = {i - sum(j <= i for j in pick_each) for i in integrations} pick_set = { i - sum(j <= i for j in pick_each): v for i, v in pick_set.items() } for slice_ in slices: slice_.iaxis -= sum(j <= slice_.iaxis for j in pick_each) if slices: logger.debug("Reduce with %s", slices) reduced = reduced.reduce(*slices) if pick_set: warnings.warn( "List indexing selection is experimental. Removed bins are not placed in overflow.", stacklevel=2, ) logger.debug("Slices for picking sets: %s", pick_set) axes = [reduced.axis(i) for i in range(reduced.rank())] reduced_view = reduced.view(flow=True) for i in pick_set: selection = copy.copy(pick_set[i]) ax = reduced.axis(i) if ax.traits_ordered: msg = f"Axis {i} is not a categorical axis, cannot pick with list: {ax}" raise RuntimeError(msg) if ax.traits_overflow and ax.size not in pick_set[i]: selection.append(ax.size) new_axis = axes[i].__class__([axes[i].value(j) for j in pick_set[i]]) # type: ignore[call-arg] new_axis.raw_metadata = axes[i].raw_metadata axes[i] = new_axis reduced_view = np.take(reduced_view, selection, axis=i) logger.debug("Axes: %s", axes) new_reduced = reduced.__class__(axes) new_reduced.view(flow=True)[...] = reduced_view reduced = new_reduced if integrations: projections = [i for i in range(reduced.rank()) if i not in integrations] reduced = reduced.project(*projections) return self._new_hist(reduced) if reduced.rank() > 0 else reduced.sum(flow=True) @staticmethod def _empty_slice_msg(i: int, start: Any, stop: Any) -> str: return ( f"Slice [{start}:{stop}] on axis {i} selects no bins; boost-histogram " "axes cannot have zero bins (NumPy would return an empty array here)" ) def _handle_slice( self, i: int, start: int | None, stop: int | None, step: int | slice | Mapping[int, SupportsIndex | slice] | Callable[[Any], int] | None, reduced: CppHistogram | None, ) -> tuple[CppHistogram | None, list[_core.algorithm.reduce_command], set[int]]: if any(isinstance(v, slice) for v in (start, stop, step)): msg = ( "You have put a slice in a slice. Did you forget curly braces [{...}]?" ) raise TypeError(msg) slices = list[_core.algorithm.reduce_command]() integrations = set[int]() if start is None and stop is None and step is None: return reduced, slices, integrations start_int, stop_int = self.axes[i]._process_loc(start, stop) groups = [] new_axis = None merge = 1 has_bounds = start is not None or stop is not None match step: case x if x is sum: # https://github.com/oracle/graalpython/issues/620 integrations.add(i) if has_bounds: if start_int >= stop_int: raise IndexError(self._empty_slice_msg(i, start, stop)) slices.append( _core.algorithm.slice( i, start_int, stop_int, _core.algorithm.slice_mode.crop ) ) return reduced, slices, integrations case None: pass case object(factor=x) if x is not None: merge = x case object(axis_mapping=x) if x is not None: # Groups are applied against the sliced (start:stop) range, # not the full axis, so crop to that range first. axis_for_groups = self.axes[i] if has_bounds: if start_int >= stop_int: raise IndexError(self._empty_slice_msg(i, start, stop)) reduced = (reduced or self._hist).reduce( _core.algorithm.slice( i, start_int, stop_int, _core.algorithm.slice_mode.crop ) ) axis_for_groups = cast(self, reduced.axis(i), Axis) tmp_both = x(axis_for_groups) if tmp_both is None: msg = "The third argument to a slice must be rebin or projection" raise IndexError(msg) groups, new_axis = tmp_both if new_axis is None and not axis_for_groups.traits.ordered: msg = ( f"Group rebin on categorical axis {i} needs an explicit " "axis= (the merged category axis); it cannot be inferred " "automatically" ) raise ValueError(msg) case x if callable(x): raise NotImplementedError case _: msg = "The third argument to a slice must be rebin or projection" raise IndexError(msg) assert isinstance(start_int, int) assert isinstance(stop_int, int) # rebinning with factor if len(groups) == 0: # NumPy returns an empty array for slices like [1:-15] or [5:2]; # a Boost.Histogram axis cannot have zero bins, so refuse with a # clear message instead of the low-level "begin < end required". if min(stop_int, self.axes[i].size) <= max(start_int, 0): raise IndexError(self._empty_slice_msg(i, start, stop)) slices.append( _core.algorithm.slice_and_rebin(i, start_int, stop_int, merge) ) # rebinning with groups else: reduced = self._rebin_with_groups( reduced or self._hist, i, groups, new_axis ) return reduced, slices, integrations def _rebin_with_groups( self, reduced: CppHistogram, i: int, groups: list[int], new_axis: Any ) -> CppHistogram: """Handle rebinning with groups.""" axes = [reduced.axis(x) for x in range(reduced.rank())] reduced_view = reduced.view(flow=True) new_axes_indices = [axes[i].edges[0]] j = 0 for group in groups: new_axes_indices += [axes[i].edges[j + group]] j += group if new_axis is None: new_axis = Variable( new_axes_indices, __dict__=axes[i].raw_metadata, underflow=axes[i].traits_underflow, overflow=axes[i].traits_overflow, ) old_axis = axes[i] axes[i] = new_axis._ax logger.debug("Axes: %s", axes) new_reduced: _core.hist._BaseHistogram | _core.hist.any_multi_cell new_reduced = reduced.__class__(axes) if isinstance(reduced, _core.hist.any_multi_cell) and isinstance( new_reduced, _core.hist.any_multi_cell ): # The constructor in reduced.__class__(axes) does not take care of the number of cells. # If reduced is a multi cell histogram, we have to set the number of cells per bin manually for new_reduced new_reduced.reset_nelem(reduced.nelem()) new_view = new_reduced.view(flow=True) # Views of multi cell histograms have the cell index as the first # (index 0) dimension, so the axis position within the view is # shifted by one. view_i = i + 1 if isinstance(reduced, _core.hist.any_multi_cell) else i groups = list(groups) # do not modify the caller's list j = 0 new_j_base = 0 if old_axis.traits_underflow and axes[i].traits_underflow: groups.insert(0, 1) elif axes[i].traits_underflow: new_j_base = 1 elif old_axis.traits_underflow: # The new axis has no underflow bin: skip the old underflow bin # here. For unordered (categorical) axes its contents are folded # into the new overflow bin below; otherwise they are dropped. j = 1 if old_axis.traits_overflow and axes[i].traits_overflow: groups.append(1) # If the old axis has an overflow bin but the new one does not, the # old overflow contents are dropped (the bin is simply not consumed). for new_j, group in enumerate(groups): for _ in range(group): _combine_group_contents( new_view, reduced_view, view_i, j, new_j + new_j_base ) j += 1 if ( old_axis.traits_underflow and not axes[i].traits_underflow and not axes[i].traits_ordered and axes[i].traits_overflow ): # On an unordered (categorical) axis every out-of-range entry # lands in the overflow bin, so the old underflow contents are # added to the new overflow bin -- exactly once. _combine_group_contents(new_view, reduced_view, view_i, 0, -1) return new_reduced def __setitem__(self, index: IndexingExpr, value: ArrayLike | Accumulator) -> None: """ There are several supported possibilities: h[slice] = array # same size If an array is given to a compatible slice, it is set. h[a:] = array # One larger If an array is given that does not match, if it does match the with-overflow size, it fills that. h[a:] = h2 If another histogram is given, that must either match with or without overflow, where the overflow bins must be overflow bins (that is, you cannot set a histogram's flow bins from another histogram that is 2 larger). If you don't want this level of type safety, just use ``h[...] = h2.view()``. """ indexes = self._compute_commonindex(index) # Vectorized (NumPy array) indexing scatters values through the buffer. # The View handles accumulator (n+1 dim raw array) assignment itself. if any(isinstance(a, np.ndarray) for a in indexes): self.view(flow=True)[self._compute_vectorized_index(indexes)] = np.asarray( value ) return # A Histogram value must keep its flow bins; np.asarray() would call # __array__, which drops them (returns view(flow=False)). in_array = ( value.view(flow=True) if isinstance(value, Histogram) else np.asarray(value) ) view: Any = self.view(flow=True) value_shape: tuple[int, ...] # Support raw arrays for accumulators, the final dimension is the constructor values if ( in_array.ndim > 0 and len(view.dtype) > 0 and len(in_array.dtype) == 0 # type: ignore[arg-type] and len(view.dtype) == in_array.shape[-1] ): value_shape = in_array.shape[:-1] value_ndim = in_array.ndim - 1 else: value_shape = in_array.shape value_ndim = in_array.ndim value_n_slice = sum(isinstance(i, slice) for i in indexes) if isinstance(self._hist, _core.hist.any_multi_cell): # MultiCell histograms have to provide the cell index as first dimension, but the cell index is not included in the histogram indexing. # Slicing over the cell index is not possible for __setitem__ and is always represented as a full slice (as slice(None, None, None)). # Therefore, the number of slices is always one large than the indexed number of slices in the MultiCell case. value_n_slice += 1 # NumPy does not broadcast partial slices, but we would need # to allow it (because we do allow broadcasting up dimensions) # Instead, we simply require matching dimensions. if value_ndim > 0 and value_ndim != value_n_slice: if isinstance(self._hist, _core.hist.any_multi_cell): msg = f"Setting a {len(indexes)}D MultiCell histogram with a {value_ndim}D array must have a one higher dimension of array than histogram" else: msg = f"Setting a {len(indexes)}D histogram with a {value_ndim}D array must have a matching number of dimensions" raise ValueError(msg) # Here, value_n does not increment with n if this is not a slice value_n = 0 if isinstance(self._hist, _core.hist.any_multi_cell): # Ignore first dimension for MultiCell arrays, the first dimension is for the cells, the normal histogram axis indexing starts with dimension 2 in this case value_n = 1 # value_hist_axis tracks the axis index in the value histogram (always 0-based, # independent of the MultiCell offset in value_n) value_hist_axis = 0 for n, request in enumerate(indexes): has_underflow = self.axes[n].traits.underflow has_overflow = self.axes[n].traits.overflow if isinstance(request, slice): # This ensures that callable start/stop are handled start, stop = self.axes[n]._process_loc(request.start, request.stop) # Only consider underflow/overflow if the endpoints are not given use_underflow = has_underflow and start < 0 use_overflow = has_overflow and stop > len(self.axes[n]) # If the input is a histogram, we need to exactly match underflow/overflow if isinstance(value, Histogram): in_underflow = value.axes[value_hist_axis].traits.underflow in_overflow = value.axes[value_hist_axis].traits.overflow if use_underflow != in_underflow or use_overflow != in_overflow: msg = ( f"Cannot set histogram with underflow={in_underflow} and overflow={in_overflow} " f"to a histogram slice with underflow={use_underflow} and overflow={use_overflow}" ) raise ValueError(msg) # Convert to non-flow coordinates start_real = start + 1 if has_underflow else start stop_real = stop + 1 if has_underflow else stop # This is the total requested length without flow bins request_len = min(stop, len(self.axes[n])) - max(start, 0) # If set to a scalar, then treat it like broadcasting without flow bins # Normal requests here too # Also single element broadcasting if ( value_ndim == 0 or request_len == value_shape[value_n] or value_shape[value_n] == 1 ): start_real += 1 if start < 0 else 0 stop_real -= 1 if stop > len(self.axes[n]) else 0 # Expanded setting elif request_len + use_underflow + use_overflow == value_shape[value_n]: pass else: msg = f"Mismatched shapes {value_shape} in dimension {n}" msg += f", {value_shape[value_n]} != {request_len}" if use_underflow or use_overflow: msg += f" or {request_len + use_underflow + use_overflow}" raise ValueError(msg) logger.debug( "__setitem__: axis %i, start: %i (actual %i), stop: %i (actual %i)", n, start, start_real, stop, stop_real, ) indexes[n] = slice(start_real, stop_real, request.step) value_n += 1 value_hist_axis += 1 else: indexes[n] = self._flow_pick_index(n, int(request)) # type: ignore[arg-type] if isinstance(self._hist, _core.hist.any_multi_cell): # View of multi cell histograms has as first (index 0) dimension the cell index # Add a full slice to the beginning of the slicing expression to adept for this cell index # e.g. a slice like [0, :, 3] is converted to [:, 0, :, 3] indexes.insert(0, slice(None, None, None)) view[tuple(indexes)] = in_array def project(self, *args: int, flow: bool = True) -> Self: """ Project to a single axis or several axes on a multidimensional histogram. Provided a list of axis numbers, this will produce the histogram over those axes only. Flow bins are used if available. If flow is False, flow bins on the integrated-out axes are excluded. """ for arg in args: if arg < 0 or arg >= self.ndim: raise ValueError( f"Projection axis must be a valid axis number 0 to {self.ndim - 1}, not {arg}" ) if flow: return self._new_hist(self._hist.project(*args)) keep_axes = set(args) drop_axes = [i for i in range(self.ndim) if i not in keep_axes] slices = [ _core.algorithm.slice( i, 0, self.axes[i].size, _core.algorithm.slice_mode.crop ) for i in drop_axes ] reduced_hist = self._hist.reduce(*slices) if slices else self._hist return self._new_hist(reduced_hist.project(*args)) # Implementation of PlottableHistogram @property def kind(self) -> Kind: """ Returns Kind.COUNT if this is a normal summing histogram, and Kind.MEAN if this is a mean histogram. :return: Kind """ mean = self._hist._storage_type in { _core.storage.mean, _core.storage.weighted_mean, } return Kind.MEAN if mean else Kind.COUNT @typing.overload def values( self: Histogram[bhs.Int64] | Histogram[bhs.AtomicInt64], flow: bool = ... ) -> np.typing.NDArray[np.int64]: ... @typing.overload def values( self: Histogram[bhs.Double] | Histogram[bhs.Unlimited] | Histogram[bhs.Weight] | Histogram[bhs.Mean] | Histogram[bhs.WeightedMean] | Histogram[bhs.MultiCell], flow: bool = ..., ) -> np.typing.NDArray[np.float64]: ... @typing.overload def values( self, flow: bool = ... ) -> np.typing.NDArray[np.float64] | np.typing.NDArray[np.int64]: ... def values( self, flow: bool = False ) -> np.typing.NDArray[np.float64] | np.typing.NDArray[np.int64]: """ Returns the accumulated values. The counts for simple histograms, the sum of weights for weighted histograms, the mean for profiles, etc. If counts is equal to 0, the value in that cell is undefined if kind == "MEAN". :param flow: Enable flow bins. Not part of PlottableHistogram, but included for consistency with other methods and flexibility. :return: "np.typing.NDArray[Any]"[np.float64] """ view: Any = self.view(flow) # TODO: Might be a NumPy typing bug if len(view.dtype) == 0: return view # type: ignore[no-any-return] return view.value # type: ignore[no-any-return] @typing.overload def variances( self: Histogram[bhs.AtomicInt64] | Histogram[bhs.Int64], flow: bool = ... ) -> np.typing.NDArray[np.int64] | None: ... @typing.overload def variances( self: Histogram[bhs.Double] | Histogram[bhs.Unlimited] | Histogram[bhs.MultiCell], flow: bool = ..., ) -> np.typing.NDArray[np.float64] | None: ... @typing.overload def variances( self: Histogram[bhs.Weight] | Histogram[bhs.Mean] | Histogram[bhs.WeightedMean], flow: bool = ..., ) -> np.typing.NDArray[np.float64]: ... @typing.overload def variances( self, flow: bool = False ) -> np.typing.NDArray[np.int64] | np.typing.NDArray[np.float64] | None: ... def variances( self, flow: bool = False ) -> np.typing.NDArray[np.int64] | np.typing.NDArray[np.float64] | None: """ Returns the estimated variance of the accumulated values. The sum of squared weights for weighted histograms, the variance of samples for profiles, etc. For an unweighed histogram where kind == "COUNT", this should return the same as values if the histogram was not filled with weights, and None otherwise. If counts is equal to 1 or less, the variance in that cell is undefined if kind == "MEAN". This must be written <= 1, and not < 2; when this effective counts (weighed mean), then counts could be less than 2 but more than 1. If kind == "MEAN", the counts can be used to compute the error on the mean as sqrt(variances / counts), this works whether or not the entries are weighted if the weight variance was tracked by the implementation. Currently, this always returns - but in the future, it will return None if a weighted fill is made on a unweighed storage. :param flow: Enable flow bins. Not part of PlottableHistogram, but included for consistency with other methods and flexibility. :return: "np.typing.NDArray[Any]"[np.float64] """ view: Any = self.view(flow) if len(view.dtype) == 0: return view if self._variance_known else None if hasattr(view, "sum_of_weights"): valid = view.sum_of_weights**2 > view.sum_of_weights_squared return np.divide( # type: ignore[no-any-return] view.variance, view.sum_of_weights, out=np.full(view.sum_of_weights.shape, np.nan), where=valid, ) if hasattr(view, "count"): return np.divide( # type: ignore[no-any-return] view.variance, view.count, out=np.full(view.count.shape, np.nan), where=view.count > 1, ) return view.variance # type: ignore[no-any-return] @typing.overload def counts( self: Histogram[bhs.Int64] | Histogram[bhs.AtomicInt64], flow: bool = ... ) -> np.typing.NDArray[np.int64]: ... @typing.overload def counts( self: Histogram[bhs.Double] | Histogram[bhs.Unlimited] | Histogram[bhs.Weight] | Histogram[bhs.Mean] | Histogram[bhs.WeightedMean] | Histogram[bhs.MultiCell], flow: bool = ..., ) -> np.typing.NDArray[np.float64]: ... @typing.overload def counts( self, flow: bool = ... ) -> np.typing.NDArray[np.float64] | np.typing.NDArray[np.int64]: ... def counts( self, flow: bool = False ) -> np.typing.NDArray[np.float64] | np.typing.NDArray[np.int64]: """ Returns the number of entries in each bin for an unweighted histogram or profile and an effective number of entries (defined below) for a weighted histogram or profile. An exotic generalized histogram could have no sensible .counts, so this is Optional and should be checked by Consumers. If kind == "MEAN", counts (effective or not) can and should be used to determine whether the mean value and its variance should be displayed (see documentation of values and variances, respectively). The counts should also be used to compute the error on the mean (see documentation of variances). For a weighted histogram, counts is defined as sum_of_weights ** 2 / sum_of_weights_squared. It is equal or less than the number of times the bin was filled, the equality holds when all filled weights are equal. The larger the spread in weights, the smaller it is, but it is always 0 if filled 0 times, and 1 if filled once, and more than 1 otherwise. :return: "np.typing.NDArray[Any]"[np.float64] """ view: Any = self.view(flow) if len(view.dtype) == 0: return view # type: ignore[no-any-return] if hasattr(view, "sum_of_weights"): return np.divide( # type: ignore[no-any-return] view.sum_of_weights**2, view.sum_of_weights_squared, out=np.zeros_like(view.sum_of_weights, dtype=np.float64), where=view.sum_of_weights_squared != 0, ) if hasattr(view, "count"): return view.count # type: ignore[no-any-return] return view.value # type: ignore[no-any-return] if TYPE_CHECKING: from uhi.typing.plottable import PlottableHistogram _: PlottableHistogram = typing.cast(Histogram[Any], None) scikit-hep-boost-histogram-b4c49b0/src/boost_histogram/numpy.py000066400000000000000000000172731524660700500250350ustar00rootroot00000000000000from __future__ import annotations import contextlib import typing from collections.abc import Sequence from functools import reduce from operator import mul from typing import Any if typing.TYPE_CHECKING: from numpy.typing import ArrayLike else: ArrayLike = object import numpy as np from . import axis as _axis from . import storage as _storage from .histogram import Histogram __all__ = ("histogram", "histogram2d", "histogramdd") # pylint: disable=redefined-builtin # pylint: disable=redefined-outer-name def __dir__() -> list[str]: return list(__all__) def _outer_edges(a: ArrayLike, rng: tuple[float, float] | None) -> tuple[float, float]: """ Compute the outer bin edges, following ``numpy.lib._histograms_impl._get_outer_edges``. """ if rng is not None: first_edge, last_edge = rng if first_edge > last_edge: raise ValueError("max must be larger than min in range parameter.") if not (np.isfinite(first_edge) and np.isfinite(last_edge)): raise ValueError( f"supplied range of [{first_edge}, {last_edge}] is not finite" ) elif np.size(a) == 0: # No data, so no range can be found; NumPy uses 0-1 here. first_edge, last_edge = 0.0, 1.0 else: first_edge, last_edge = np.amin(a), np.amax(a) if not (np.isfinite(first_edge) and np.isfinite(last_edge)): raise ValueError( f"autodetected range of [{first_edge}, {last_edge}] is not finite" ) # Expand an empty range to avoid a division by zero if first_edge == last_edge: first_edge -= 0.5 last_edge += 0.5 return first_edge, last_edge def histogramdd( a: tuple[ArrayLike, ...], bins: int | tuple[int, ...] | tuple[np.typing.NDArray[Any], ...] = 10, range: Sequence[tuple[float, float] | None] | None = None, normed: None = None, weights: ArrayLike | None = None, density: bool = False, *, histogram: type[Histogram[Any]] | None = None, storage: _storage.Storage | None = None, threads: int | None = None, ) -> Any: if storage is None: storage = _storage.Double() cls: type[Histogram[Any]] = Histogram if histogram is None else histogram if normed is not None: raise KeyError( "normed=True is not recommended for use in NumPy, and is not supported in boost-histogram; use density=True instead" ) if density and histogram is not None: raise KeyError( "boost-histogram does not support the density keyword when returning a boost-histogram object" ) # Odd NumPy design here. Oh well. A 1D array is N samples of one dimension, # while an ND array has one sample per row. if isinstance(a, np.ndarray): # type: ignore[unreachable] a = np.atleast_2d(a) if a.ndim == 1 else a.T # type: ignore[unreachable] elif len(a) > 0 and np.ndim(a[0]) == 0: a = (np.asarray(a),) rank = len(a) # Integer bins: all the same with contextlib.suppress(TypeError): bins = (int(bins),) * rank # type: ignore[arg-type] assert not isinstance(bins, int) if len(bins) != rank: raise ValueError( "The dimension of bins must be equal to the dimension of the sample x." ) # Single None -> list of Nones if range is None: range = (None,) * rank elif len(range) != rank: raise ValueError("range argument must have one entry per dimension") axs: list[_axis.Axis] = [] for n, (b, r) in enumerate(zip(bins, range, strict=True)): if np.issubdtype(type(b), np.integer): start, stop = _outer_edges(a[n], r) new_ax = _axis.Regular( typing.cast(int, b), start, stop, underflow=False, overflow=False ) axs.append(new_ax) else: # Always copy; the input edges must not be modified in-place below barr: np.typing.NDArray[Any] = np.array(b, dtype=np.double) # This does have a .max member, not sure why pylint doesn't like it # pylint: disable-next=no-member barr[-1] = np.nextafter(barr[-1], np.finfo("d").max) axs.append(_axis.Variable(barr)) hist = cls(*axs, storage=storage).fill(*a, weight=weights, threads=threads) if density: areas = reduce(mul, hist.axes.widths) density_val = hist.values() / np.sum(hist.values()) / areas return (density_val, hist.to_numpy()[1:]) # Note: this is view=True since users have to ask explicitly for special # storages, so view=False would throw away part of what they are asking # for. Users can use a histogram return type if they need view=False. return hist if histogram is not None else hist.to_numpy(view=True, dd=True) def histogram2d( x: ArrayLike, y: ArrayLike, bins: int | tuple[int, int] = 10, range: Sequence[tuple[float, float] | None] | None = None, normed: None = None, weights: ArrayLike | None = None, density: bool = False, *, histogram: type[Histogram[Any]] | None = None, storage: _storage.Storage | None = None, threads: int | None = None, ) -> Any: if storage is None: storage = _storage.Double() # NumPy shares a single edge array between both axes, but only if it is not # one or two entries long (those are read as per-axis bin counts). try: n_bins = len(bins) # type: ignore[arg-type] except TypeError: n_bins = 1 if n_bins not in {1, 2}: edges = np.asarray(bins) bins = (edges, edges) # type: ignore[assignment] result = histogramdd( (x, y), bins, range, normed, weights, density, histogram=histogram, storage=storage, threads=threads, ) if not isinstance(result, tuple): return result data, (edgesx, edgesy) = result return data, edgesx, edgesy def histogram( a: ArrayLike, bins: int | str | np.typing.NDArray[Any] = 10, range: tuple[float, float] | None = None, normed: None = None, weights: ArrayLike | None = None, density: bool = False, *, histogram: type[Histogram[Any]] | None = None, storage: _storage.Storage | None = None, threads: int | None = None, ) -> Any: # numpy 1d histogram returns integers in some cases if storage is None: storage = ( _storage.Double() if weights is not None or normed or density # type: ignore[redundant-expr] else _storage.Int64() ) if isinstance(bins, str): bins = np.histogram_bin_edges(a, bins, range, weights) # TODO: make sure all types work at runtime (type ignore below) # I think it's safe and the union is in the wrong place result = histogramdd( (a,), (bins,), # type: ignore[arg-type] (range,), normed, weights, density, histogram=histogram, storage=storage, threads=threads, ) if not isinstance(result, tuple): return result data, (edges,) = result return data, edges # Process docstrings # TODO: make this a decorator for f, np_f in zip( (histogram, histogram2d, histogramdd), (np.histogram, np.histogram2d, np.histogramdd), strict=False, ): H = """\ Return a boost-histogram object using the same arguments as numpy's {}. This does not support the deprecated normed=True argument. Three extra arguments are added: histogram=bh.Histogram will enable object based output, storage=bh.storage.* lets you set the storage used, and threads=int lets you set the number of threads to fill with (0 for auto, None for 1). """ f.__doc__ = H.format(np_f.__name__) + (np_f.__doc__ or "") scikit-hep-boost-histogram-b4c49b0/src/boost_histogram/py.typed000066400000000000000000000000001524660700500247660ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/src/boost_histogram/serialization/000077500000000000000000000000001524660700500261565ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/src/boost_histogram/serialization/__init__.py000066400000000000000000000123571524660700500302770ustar00rootroot00000000000000from __future__ import annotations import copy from typing import Any, TypeVar import numpy as np # pylint: disable-next=import-error from .. import histogram, storage, version from ._axis import _axis_from_dict, _axis_to_dict from ._common import serialize_metadata from ._storage import ( _data_from_dict, _storage_from_dict, _storage_to_dict, _storage_type_to_str, ) __all__ = ["from_uhi", "remove_writer_info", "to_uhi"] def __dir__() -> list[str]: return __all__ def _storage_has_data_keys(storage_data: dict[str, Any], storage_type: str) -> bool: """ Check if storage data dict has the required keys for the given storage type. Returns True if all required data keys are present, False if it's structure-only. Raises ValueError if required keys are missing (malformed/partial data). """ match storage_type: case "int" | "double": required_keys = {"values"} case "weighted": required_keys = {"values", "variances"} case "mean": required_keys = {"counts", "values", "variances"} case "weighted_mean": required_keys = { "sum_of_weights", "sum_of_weights_squared", "values", "variances", } case _: msg = f"Unknown storage type: {storage_type}" raise ValueError(msg) present_keys = required_keys & set(storage_data.keys()) if not present_keys: return False if present_keys != required_keys: missing = required_keys - present_keys msg = f"{storage_type.capitalize()} storage missing required keys: {missing}" raise ValueError(msg) return True def to_uhi( h: histogram.Histogram[Any], /, *, keep_storage: bool = True ) -> dict[str, Any]: """Convert an Histogram to a dictionary.""" # Convert the histogram to a dictionary writer_info = {"boost-histogram": {"version": version.version}} # The storage_type property does a subclass-walking cast on every access, # so look it up once; a single instance (which for MultiCell carries the # real nelem) serves for dispatch and error messages. storage_type = h.storage_type storage_obj = h.storage # Store storage type info for AtomicInt64 and Unlimited (they serialize as int/double) storage_type_str = _storage_type_to_str(storage_obj) if issubclass(storage_type, (storage.AtomicInt64, storage.Unlimited)): writer_info["boost-histogram"]["storage_type"] = storage_type.__name__ data = { "uhi_schema": 1, "writer_info": writer_info, "axes": [_axis_to_dict(axis) for axis in h.axes], } if keep_storage: data["storage"] = _storage_to_dict(storage_obj, h.view(flow=True)) else: data["storage"] = {"type": storage_type_str} # _variance_known is internal Histogram state, not user metadata public_dict = {k: v for k, v in h.__dict__.items() if k != "_variance_known"} data["metadata"] = serialize_metadata(public_dict) return data def from_uhi(data: dict[str, Any], /) -> histogram.Histogram[Any]: """Convert a dictionary to an Histogram.""" # One time use axis = (_axis_from_dict(ax) for ax in data["axes"]) storage_data = data["storage"] storage_ = _storage_from_dict(storage_data, data.get("writer_info", {})) h = histogram.Histogram[Any](*axis, storage=storage_) h.__dict__ = dict(data.get("metadata", {})) # Not part of the UHI schema; assume known unless the dict says otherwise h.__dict__.setdefault("_variance_known", True) # Check if storage has data (if not, it's a structure-only histogram) # Validate required keys per storage type before deciding to skip data loading storage_type = storage_data["type"] has_data_keys = _storage_has_data_keys(storage_data, storage_type) if not has_data_keys: return h raw_data = _data_from_dict(storage_data) view_shape = h.view(flow=True).shape # Reshape raw_data to the expected shape. This is necessary because JSON # serialization can collapse empty dimensions (e.g. (5, 0, 0) -> (5, 0)), # so we must restore the correct number of dimensions. if storage_type in {"weighted", "mean", "weighted_mean"}: raw_data = np.asarray(raw_data) raw_data = raw_data.reshape(view_shape + raw_data.shape[-1:]) else: raw_data = np.reshape(raw_data, view_shape) h[...] = raw_data return h T = TypeVar("T", bound="dict[str, Any]") def remove_writer_info(obj: T, /, *, library: str | None = "boost-histogram") -> T: """ Removes all ``writer_info`` for a library from a histogram dict, axes dict, or storage dict. Makes copies where required, and the outer dictionary is always copied. Specify a library name, or ``None`` to remove all. """ obj = copy.copy(obj) if library is None: obj.pop("writer_info", None) elif library in obj.get("writer_info", {}): obj["writer_info"] = copy.copy(obj["writer_info"]) del obj["writer_info"][library] if "axes" in obj: obj["axes"] = [remove_writer_info(ax, library=library) for ax in obj["axes"]] if "storage" in obj: obj["storage"] = remove_writer_info(obj["storage"], library=library) return obj scikit-hep-boost-histogram-b4c49b0/src/boost_histogram/serialization/_axis.py000066400000000000000000000177401524660700500276440ustar00rootroot00000000000000from __future__ import annotations import functools from typing import Any from .. import axis from ._common import serialize_metadata __all__ = ["_axis_from_dict", "_axis_to_dict"] # Transformed Regular axes have no portable UHI representation, so they are # written as a ``variable`` axis (with the transformed edges) for interoperability # and additionally tagged with the original Regular parameters + transform identity # under boost-histogram's writer_info, letting us restore the exact axis on read. # Only transforms we can reconstruct from these names are recorded here. Keyed # by name, but matched against the actual transform object (via __eq__), not # its repr - a custom Function transform can share a name with a built-in. _KNOWN_FUNCTION_TRANSFORMS = {"log": axis.transform.log, "sqrt": axis.transform.sqrt} def __dir__() -> list[str]: return __all__ def _transform_writer_info(ax: axis.Regular, /) -> dict[str, Any]: """Capture enough to restore a transformed Regular axis, or {} if it can't be round-tripped (e.g. a custom function-pointer transform).""" tr = ax.transform info: dict[str, Any] if isinstance(tr, axis.transform.Pow): info = {"transform": "pow", "power": tr.power} else: name = next( (n for n, known in _KNOWN_FUNCTION_TRANSFORMS.items() if tr == known), None, ) if name is None: return {} info = {"transform": name} info["bins"] = ax.size info["lower"] = float(ax.edges[0]) info["upper"] = float(ax.edges[-1]) return info def _transform_from_writer_info(writer_info: dict[str, Any], /) -> Any: """Rebuild a transform from writer_info, or None if the name is unknown.""" name = writer_info["transform"] if name == "pow": return axis.transform.Pow(writer_info["power"]) if name == "log": return axis.transform.log if name == "sqrt": return axis.transform.sqrt return None @functools.singledispatch def _axis_to_dict(ax: Any, /) -> dict[str, Any]: """Convert an axis to a dictionary.""" raise TypeError(f"Unsupported axis type: {type(ax)}") @_axis_to_dict.register(axis.Regular) @_axis_to_dict.register(axis.Integer) def _(ax: axis.Regular | axis.Integer, /) -> dict[str, Any]: """Convert a Regular axis to a dictionary.""" shared = { "underflow": ax.traits.underflow, "overflow": ax.traits.overflow, "circular": ax.traits.circular, } transform_info: dict[str, Any] = {} # Special handling if the axis has a transform if isinstance(ax, axis.Regular) and ax.transform is not None: data = { "type": "variable", "edges": ax.edges, **shared, } transform_info = _transform_writer_info(ax) elif isinstance(ax, axis.Integer): data = { "type": "regular", "lower": int(ax.edges[0]), "upper": int(ax.edges[-1]), "bins": ax.size, **shared, } else: data = { "type": "regular", "lower": float(ax.edges[0]), "upper": float(ax.edges[-1]), "bins": ax.size, **shared, } writer_info: dict[str, Any] = {} if isinstance(ax, axis.Integer): writer_info["orig_type"] = "Integer" if ax.traits.growth: writer_info["growth"] = True writer_info.update(transform_info) if writer_info: data["writer_info"] = {"boost-histogram": writer_info} metadata = serialize_metadata(ax.__dict__) if metadata: data["metadata"] = metadata return data @_axis_to_dict.register def _(ax: axis.Variable, /) -> dict[str, Any]: """Convert a Variable or Integer axis to a dictionary.""" data = { "type": "variable", "edges": ax.edges, "underflow": ax.traits.underflow, "overflow": ax.traits.overflow, "circular": ax.traits.circular, } if ax.traits.growth: data["writer_info"] = {"boost-histogram": {"growth": True}} metadata = serialize_metadata(ax.__dict__) if metadata: data["metadata"] = metadata return data @_axis_to_dict.register def _(ax: axis.IntCategory, /) -> dict[str, Any]: """Convert an IntCategory axis to a dictionary.""" data = { "type": "category_int", "categories": list(ax), "flow": ax.traits.overflow, } if ax.traits.growth: data["writer_info"] = {"boost-histogram": {"growth": True}} metadata = serialize_metadata(ax.__dict__) if metadata: data["metadata"] = metadata return data @_axis_to_dict.register def _(ax: axis.StrCategory, /) -> dict[str, Any]: """Convert a StrCategory axis to a dictionary.""" data = { "type": "category_str", "categories": list(ax), "flow": ax.traits.overflow, } if ax.traits.growth: data["writer_info"] = {"boost-histogram": {"growth": True}} metadata = serialize_metadata(ax.__dict__) if metadata: data["metadata"] = metadata return data @_axis_to_dict.register def _(ax: axis.Boolean, /) -> dict[str, Any]: """Convert a Boolean axis to a dictionary.""" data: dict[str, Any] = { "type": "boolean", } metadata = serialize_metadata(ax.__dict__) if metadata: data["metadata"] = metadata return data def _axis_from_dict(data: dict[str, Any], /) -> axis.Axis: writer_info = data.get("writer_info", {}).get("boost-histogram", {}) orig_type = writer_info.get("orig_type", "") if orig_type == "Integer": if data["upper"] - data["lower"] != data["bins"]: msg = ( "Invalid Integer axis: bins " f"({data['bins']}) must equal upper - lower " f"({data['upper']} - {data['lower']} = {data['upper'] - data['lower']})" ) raise ValueError(msg) return axis.Integer( data["lower"], data["upper"], underflow=data["underflow"], overflow=data["overflow"], circular=data["circular"], growth=writer_info.get("growth", False), __dict__=data.get("metadata"), ) # A transformed Regular axis is stored as a variable axis plus the original # Regular parameters; restore it if we recognize the transform. if writer_info.get("transform"): transform_obj = _transform_from_writer_info(writer_info) if transform_obj is not None: return axis.Regular( writer_info["bins"], writer_info["lower"], writer_info["upper"], transform=transform_obj, __dict__=data.get("metadata"), ) hist_type = data["type"] if hist_type == "regular": return axis.Regular( data["bins"], data["lower"], data["upper"], underflow=data["underflow"], overflow=data["overflow"], circular=data["circular"], growth=writer_info.get("growth", False), __dict__=data.get("metadata"), ) if hist_type == "variable": return axis.Variable( data["edges"], underflow=data["underflow"], overflow=data["overflow"], circular=data["circular"], growth=writer_info.get("growth", False), __dict__=data.get("metadata"), ) if hist_type == "category_int": return axis.IntCategory( data["categories"], overflow=data["flow"], growth=writer_info.get("growth", False), __dict__=data.get("metadata"), ) if hist_type == "category_str": return axis.StrCategory( data["categories"], overflow=data["flow"], growth=writer_info.get("growth", False), __dict__=data.get("metadata"), ) if hist_type == "boolean": return axis.Boolean(__dict__=data.get("metadata")) raise TypeError(f"Unsupported axis type: {hist_type}") scikit-hep-boost-histogram-b4c49b0/src/boost_histogram/serialization/_common.py000066400000000000000000000003301524660700500301530ustar00rootroot00000000000000from __future__ import annotations from typing import Any def serialize_metadata(value: dict[str, Any], /) -> dict[str, Any]: return {k: v for k, v in value.items() if not k.startswith("@") and v is not None} scikit-hep-boost-histogram-b4c49b0/src/boost_histogram/serialization/_storage.py000066400000000000000000000112311524660700500303310ustar00rootroot00000000000000from __future__ import annotations import functools from typing import Any import numpy as np from .. import storage __all__ = [ "_data_from_dict", "_storage_from_dict", "_storage_to_dict", "_storage_type_to_str", ] def __dir__() -> list[str]: return __all__ def _storage_type_to_str(_storage: storage.Storage, /) -> str: """Return the canonical storage type string for a storage object.""" match _storage: case storage.Int64(): return "int" case storage.Double(): return "double" case storage.AtomicInt64(): return "int" case storage.Unlimited(): return "double" case storage.Weight(): return "weighted" case storage.Mean(): return "mean" case storage.WeightedMean(): return "weighted_mean" case _: raise TypeError(f"Unsupported storage type: {_storage}") @functools.singledispatch def _storage_to_dict(_storage: storage.Storage, /, data: Any) -> dict[str, Any]: # noqa: ARG001 """Convert a storage to a dictionary.""" msg = f"Unsupported storage type: {_storage}" raise TypeError(msg) @_storage_to_dict.register(storage.Int64) @_storage_to_dict.register(storage.Double) def _(_storage: storage.Int64 | storage.Double, /, data: Any) -> dict[str, Any]: return {"type": _storage_type_to_str(_storage), "values": data} @_storage_to_dict.register(storage.AtomicInt64) @_storage_to_dict.register(storage.Unlimited) def _( _storage: storage.AtomicInt64 | storage.Unlimited, /, data: Any, ) -> dict[str, Any]: # AtomicInt64's view is always int64 and Unlimited's view is always # converted to double (see make_buffer in histogram.hpp), so the type # follows directly from the storage type. return {"type": _storage_type_to_str(_storage), "values": data} @_storage_to_dict.register(storage.Weight) def _(_storage: storage.Weight, /, data: Any) -> dict[str, Any]: return { "type": _storage_type_to_str(_storage), "values": data.value, "variances": data.variance, } @_storage_to_dict.register(storage.Mean) def _(_storage: storage.Mean, /, data: Any) -> dict[str, Any]: return { "type": _storage_type_to_str(_storage), "counts": data.count, "values": data.value, "variances": data.variance, } @_storage_to_dict.register(storage.WeightedMean) def _(_storage: storage.WeightedMean, /, data: Any) -> dict[str, Any]: return { "type": _storage_type_to_str(_storage), "sum_of_weights": data.sum_of_weights, "sum_of_weights_squared": data.sum_of_weights_squared, "values": data.value, "variances": data.variance, } def _storage_from_dict( data: dict[str, Any], writer_info: dict[str, Any] | None = None, / ) -> storage.Storage: """Convert a dictionary to a storage object.""" # If loading a boost-histogram, we can load the exact original type # Check both the main writer_info (new location) and storage writer_info (old location) orig_type = "" if writer_info: orig_type = writer_info.get("boost-histogram", {}).get("storage_type", "") if not orig_type: orig_type = ( data.get("writer_info", {}).get("boost-histogram", {}).get("orig_type", "") ) if orig_type == "AtomicInt64": return storage.AtomicInt64() if orig_type == "Unlimited": return storage.Unlimited() storage_type = data["type"] if storage_type == "int": return storage.Int64() if storage_type == "double": return storage.Double() if storage_type == "weighted": return storage.Weight() if storage_type == "mean": return storage.Mean() if storage_type == "weighted_mean": return storage.WeightedMean() raise TypeError(f"Unsupported storage type: {storage_type}") def _data_from_dict(data: dict[str, Any], /) -> np.typing.NDArray[Any]: """Convert a dictionary to data.""" storage_type = data["type"] if storage_type in {"int", "double"}: return data["values"] # type: ignore[no-any-return] if storage_type == "weighted": return np.stack([data["values"], data["variances"]], axis=-1) if storage_type == "mean": return np.stack([data["counts"], data["values"], data["variances"]], axis=-1) if storage_type == "weighted_mean": return np.stack( [ data["sum_of_weights"], data["sum_of_weights_squared"], data["values"], data["variances"], ], axis=-1, ) raise TypeError(f"Unsupported storage type: {storage_type}") scikit-hep-boost-histogram-b4c49b0/src/boost_histogram/storage.py000066400000000000000000000035321524660700500253220ustar00rootroot00000000000000from __future__ import annotations from typing import ClassVar import boost_histogram from ._core import accumulators # pylint: disable=no-name-in-module from ._core import storage as store # pylint: disable=no-name-in-module __all__ = [ "AtomicInt64", "Double", "Int64", "Mean", "MultiCell", "Storage", "Unlimited", "Weight", "WeightedMean", ] def __dir__() -> list[str]: return __all__ # Simple mixin to provide a common base class for types class Storage: _family: object def __init_subclass__(cls, *, family: object) -> None: super().__init_subclass__() cls._family = family def __repr__(self) -> str: return f"{self.__class__.__name__}()" accumulator: ClassVar[ ( type[ int | float | accumulators.WeightedMean | accumulators.WeightedSum | accumulators.Mean ] ) ] class Int64(store.int64, Storage, family=boost_histogram): accumulator = int class Double(store.double, Storage, family=boost_histogram): accumulator = float class AtomicInt64(store.atomic_int64, Storage, family=boost_histogram): accumulator = int class Unlimited(store.unlimited, Storage, family=boost_histogram): accumulator = float class Weight(store.weight, Storage, family=boost_histogram): accumulator = accumulators.WeightedSum class Mean(store.mean, Storage, family=boost_histogram): accumulator = accumulators.Mean class WeightedMean(store.weighted_mean, Storage, family=boost_histogram): accumulator = accumulators.WeightedMean class MultiCell(store.multi_cell, Storage, family=boost_histogram): accumulator = float __match_args__ = ("nelem",) def __repr__(self) -> str: return f"{self.__class__.__name__}({self.nelem})" scikit-hep-boost-histogram-b4c49b0/src/boost_histogram/tag.py000066400000000000000000000160651524660700500244360ustar00rootroot00000000000000# bh.sum is just the Python sum, so from boost_histogram import * is safe (but # not recommended) from __future__ import annotations import copy import operator from builtins import sum from collections.abc import Sequence from typing import TYPE_CHECKING, TypeVar import numpy as np if TYPE_CHECKING: from uhi.typing.plottable import PlottableAxis from ._compat.typing import Self from .typing import AxisLike __all__ = ("Locator", "Slicer", "at", "loc", "overflow", "rebin", "sum", "underflow") def Slicer() -> np.lib._index_tricks_impl.IndexExpression: """ It is encouraged to use ``np.s_`` directly instead of this function: h[{0: np.s_[::bh.rebin(2)]}] # rebin axis 0 by two This is provided for backward compatibility. """ return np.s_ T = TypeVar("T", bound="Locator") class Locator: __slots__ = ("offset",) NAME = "" def __init__(self, offset: int = 0) -> None: if not isinstance(offset, int): raise TypeError("The offset must be an integer") self.offset = offset def __add__(self, offset: int) -> Self: other = copy.copy(self) other.offset += offset return other def __sub__(self, offset: int) -> Self: other = copy.copy(self) other.offset -= offset return other def _print_self_(self) -> str: return "" def __repr__(self) -> str: s = self.NAME or self.__class__.__name__ s += self._print_self_() if self.offset != 0: s += " + " if self.offset > 0 else " - " s += str(abs(self.offset)) return s class loc(Locator): __slots__ = ("value",) def __init__(self, value: str | float, offset: int = 0) -> None: super().__init__(offset) self.value = value def _print_self_(self) -> str: return f"({self.value})" def __call__(self, axis: AxisLike) -> int: return axis.index(self.value) + self.offset class Underflow(Locator): __slots__ = () NAME = "underflow" def __call__(self, axis: AxisLike) -> int: # noqa: ARG002 return -1 + self.offset underflow = Underflow() class Overflow(Locator): __slots__ = () NAME = "overflow" def __call__(self, axis: AxisLike) -> int: return len(axis) + self.offset overflow = Overflow() class at: __slots__ = ("value",) def __init__(self, value: int) -> None: self.value = value def __call__(self, axis: AxisLike) -> int: # noqa: ARG002 return self.value class rebin: __slots__ = ( "axis", "edges", "factor", "groups", ) def __init__( self, factor_or_axis: int | PlottableAxis | None = None, /, *, factor: int | None = None, groups: Sequence[int] | None = None, edges: Sequence[int | float] | None = None, axis: PlottableAxis | None = None, ) -> None: # bool is a subclass of int, but is not a sensible rebin factor if isinstance(factor_or_axis, bool): raise TypeError("The factor must be an integer, not a bool") if isinstance(factor_or_axis, int): factor = factor_or_axis elif factor_or_axis is not None: try: # Accept any other integer-like type (e.g. NumPy integers). factor = operator.index(factor_or_axis) # type: ignore[arg-type] except TypeError: axis = factor_or_axis total_args = sum(i is not None for i in [factor, groups, edges]) if total_args != 1 and axis is None: raise ValueError("Exactly one argument should be provided") if factor is not None and axis is not None: msg = "A factor cannot be combined with an axis; use groups= or edges= with axis= instead" raise ValueError(msg) self.groups = groups self.edges = edges self.axis = axis self.factor = factor def __repr__(self) -> str: repr_str = f"{self.__class__.__name__}" args: dict[str, int | Sequence[int | float] | PlottableAxis | None] = { "factor": self.factor, "groups": self.groups, "edges": self.edges, "axis": self.axis, } for k, v in args.items(): if v is not None: return_str = f"{repr_str}({k}={v})" break return return_str # Note: this preserves the input type of `self.axis`, so is safe within a # single UHI library, but not cross-library. Returns None for the axis if # an axis is not provided, the caller should make an axis if that's the # case. def axis_mapping( self, axis: PlottableAxis ) -> tuple[Sequence[int], PlottableAxis | None]: return (self.group_mapping(axis), self.axis) def group_mapping(self, axis: PlottableAxis) -> Sequence[int]: """ Return the list of group sizes (numbers of adjacent bins to merge) for ``axis``. For explicit groups, the sum of the group sizes must equal the number of bins in the axis. For a factor, this returns ``len(axis) // factor`` groups of size ``factor``; if the factor does not divide the number of bins evenly, the leftover bins are not part of any group (a consumer should merge them into the overflow bin, matching the C++ factor-based rebinning). """ if self.groups is not None: if sum(self.groups) != len(axis): msg = f"The sum of the groups ({sum(self.groups)}) must be equal to the number of bins in the axis ({len(axis)})" raise ValueError(msg) return self.groups if self.factor is not None: return [self.factor] * (len(axis) // self.factor) if self.edges is not None or self.axis is not None: newedges = None if self.edges is not None: newedges = self.edges elif self.axis is not None and hasattr(self.axis, "edges"): newedges = self.axis.edges if newedges is not None and hasattr(axis, "edges"): if newedges[0] != axis.edges[0]: msg = "Edges must start at first bin" raise ValueError(msg) if newedges[-1] != axis.edges[-1]: msg = "Edges must end at last bin" raise ValueError(msg) matched_ixes = [np.abs(axis.edges - edge).argmin() for edge in newedges] missing_edges = [ edge for ix, edge in zip(matched_ixes, newedges, strict=False) if not np.isclose(axis.edges[ix], edge) ] if missing_edges: missing_edges_repr = ", ".join(map(str, missing_edges)) msg = f"Edge(s) {missing_edges_repr} not found in axis" raise ValueError(msg) return [ int(ix - matched_ixes[i]) for i, ix in enumerate(matched_ixes[1:]) ] raise ValueError("No rebinning factor or groups provided") scikit-hep-boost-histogram-b4c49b0/src/boost_histogram/test.py000066400000000000000000000054171524660700500246410ustar00rootroot00000000000000""" This test module does simple smoke checks to make sure boost_histogram compiled correctly. Run with `python -m boost_histogram.test`. """ from __future__ import annotations import pickle import unittest import boost_histogram as bh class TestBoostHistogram(unittest.TestCase): def test_1d_histogram(self) -> None: h = bh.Histogram(bh.axis.Regular(10, 0, 1)) h.fill([0.15, 0.25, 0.25, 0.35]) self.assertEqual(h[0], 0) self.assertEqual(h[1], 1) self.assertEqual(h[2], 2) self.assertEqual(h[3], 1) self.assertEqual(h[4], 0) def test_2d_histogram(self) -> None: h = bh.Histogram( bh.axis.Integer(4, 10), bh.axis.StrCategory(["a", "b", "c"], overflow=False), storage=bh.storage.Weight(), ) h.fill([5, 5, 5, 7], ["a", "b", "b", "c"]) val = h[bh.loc(5), bh.loc("a")] assert isinstance(val, bh.accumulators.WeightedSum) self.assertEqual(val.value, 1) val = h[bh.loc(5), bh.loc("b")] assert isinstance(val, bh.accumulators.WeightedSum) self.assertEqual(val.value, 2) val = h[bh.loc(7), bh.loc("c")] assert isinstance(val, bh.accumulators.WeightedSum) self.assertEqual(val.value, 1) val = h[bh.loc(8), bh.loc("c")] assert isinstance(val, bh.accumulators.WeightedSum) self.assertEqual(val.value, 0) def test_pickle(self) -> None: h = bh.Histogram( bh.axis.Variable([0, 0.3, 0.4, 0.5, 1.0]), storage=bh.storage.Mean() ) h.fill([0.15, 0.25, 0.25, 0.35], sample=[1, 2, 3, 4]) pickled = pickle.dumps(h) unpickled = pickle.loads(pickled) self.assertEqual(h, unpickled) def test_sqrt_transform(self) -> None: a = bh.axis.Regular(10, 0, 10, transform=bh.axis.transform.sqrt) # Edges: 0. , 0.1, 0.4, 0.9, 1.6, 2.5, 3.6, 4.9, 6.4, 8.1, 10. self.assertEqual(a.index(-100), 10) # Always in overflow bin self.assertEqual(a.index(-1), 10) # When transform is invalid self.assertEqual(a.index(0), 0) self.assertEqual(a.index(0.15), 1) self.assertEqual(a.index(0.5), 2) self.assertEqual(a.index(1), 3) self.assertEqual(a.index(1.7), 4) self.assertEqual(a.index(9), 9) self.assertEqual(a.index(11), 10) self.assertEqual(a.index(1000), 10) val = a.bin(0) assert isinstance(val, tuple) self.assertAlmostEqual(val[0], 0.0) val = a.bin(1) assert isinstance(val, tuple) self.assertAlmostEqual(val[0], 0.1) val = a.bin(2) assert isinstance(val, tuple) self.assertAlmostEqual(val[0], 0.4) if __name__ == "__main__": unittest.main("boost_histogram.test", warnings="error") scikit-hep-boost-histogram-b4c49b0/src/boost_histogram/typing.py000066400000000000000000000024611524660700500251700ustar00rootroot00000000000000from __future__ import annotations from types import EllipsisType from typing import TYPE_CHECKING, Any, Protocol, TypeAlias if TYPE_CHECKING: from collections.abc import Sequence from numpy import ufunc as Ufunc from numpy.typing import ArrayLike from uhi.typing.plottable import PlottableAxis from boost_histogram._core.accumulators import Mean, WeightedMean, WeightedSum from boost_histogram._core.hist import _BaseHistogram as CppHistogram Accumulator = WeightedSum | Mean | WeightedMean else: WeightedSum = Any Mean = Any WeightedMean = Any ArrayLike = Any Ufunc = Any CppHistogram = Any Accumulator = Any __all__ = ( "ArrayLike", "AxisLike", "CppHistogram", "Mean", "RebinProtocol", "StdIndex", "StrIndex", "Ufunc", "WeightedMean", "WeightedSum", ) class AxisLike(Protocol): def index(self, value: float | str) -> int: ... def __len__(self) -> int: ... StdIndex: TypeAlias = ( int | slice | EllipsisType | tuple[slice | int | EllipsisType, ...] ) StrIndex: TypeAlias = ( int | slice | str | EllipsisType | tuple[slice | int | str | EllipsisType, ...] ) class RebinProtocol(Protocol): def axis_mapping( self, axis: PlottableAxis ) -> tuple[Sequence[int], PlottableAxis | None]: ... scikit-hep-boost-histogram-b4c49b0/src/boost_histogram/version.pyi000066400000000000000000000000151524660700500255050ustar00rootroot00000000000000version: str scikit-hep-boost-histogram-b4c49b0/src/boost_histogram/view.py000066400000000000000000000471161524660700500246360ustar00rootroot00000000000000from __future__ import annotations from collections.abc import Callable, Mapping, MutableMapping from typing import TYPE_CHECKING, Any, ClassVar, Literal import numpy as np from .accumulators import Mean, WeightedMean, WeightedSum from .typing import ArrayLike, StrIndex, Ufunc UFMethod = Literal["__call__", "reduce", "reduceat", "accumulate", "outer", "at"] class View(np.ndarray[Any, Any]): __slots__ = () _FIELDS: ClassVar[tuple[str, ...]] _PARENT: type[WeightedSum | WeightedMean | Mean] # Whether ``a - b`` (and unary ``-``) and multiplying/dividing two views are # meaningful for this view. Overridden per subclass. _SUPPORTS_SUB: ClassVar[bool] = False _SUPPORTS_VIEW_PRODUCT: ClassVar[bool] = False def __getitem__(self, ind: StrIndex) -> np.typing.NDArray[Any]: # type: ignore[override] sliced = super().__getitem__(ind) # type: ignore[index] # If the dtype has changed, return a normal array (no longer a record). # This must be checked before the shape check so that 0-d field access # (e.g. ``view["value"]`` on a 0-d MeanView) returns a plain scalar # array instead of trying to iterate over a 0-d array. if sliced.dtype != self.dtype: return np.asarray(sliced) # If the shape is empty, return the parent type if not sliced.shape: return self._PARENT._make(*sliced) # type: ignore[return-value] # Otherwise, no change, return the same View type return sliced def __repr__(self) -> str: # NumPy starts the ndarray class name with "array", so we replace it # with our class name return f"{self.__class__.__name__}(\n " + repr(self.view(np.ndarray))[6:] def __str__(self) -> str: my_fields = ", ".join(self._FIELDS) return f"{self.__class__.__name__}: ({my_fields})\n{self.view(np.ndarray)}" def __setitem__(self, ind: StrIndex, value: ArrayLike) -> None: # type: ignore[override] # `.value` really is ["value"] for an record array if isinstance(ind, str): super().__setitem__(ind, value) return current_ndim = super().__getitem__(ind).ndim # type: ignore[index] array: np.typing.NDArray[Any] = np.asarray(value) msg = "Needs matching ndarray or n+1 dim array" if array.ndim == current_ndim + 1: if len(self._FIELDS) == array.shape[-1]: self.__setitem__(ind, self._PARENT._array(*np.moveaxis(array, -1, 0))) # type: ignore[assignment] return msg += f", final dimension should be {len(self._FIELDS)} for this storage, got {array.shape[-1]} instead" raise ValueError(msg) if self.dtype == array.dtype: super().__setitem__(ind, array) # type: ignore[index] return msg += f", {current_ndim}D {self.dtype} or {current_ndim + 1}D required, got {array.ndim}D {array.dtype}" raise ValueError(msg) # ------------------------------------------------------------------ # Arithmetic: shared ufunc scaffolding that dispatches the field math # to per-view hooks (``_combine``/``_add_scalar``/``_scale``/...). # ------------------------------------------------------------------ def __array_ufunc__( self, ufunc: Ufunc, method: UFMethod, *inputs: Any, **kwargs: Any ) -> np.typing.NDArray[Any]: # Avoid infinite recursion by working with plain ndarrays raw_inputs = [np.asarray(x) for x in inputs] # Reductions (``.sum()`` / ``np.add.reduce``) don't get a pre-allocated # ``out`` here; the hook builds the reduced accumulator(s) itself. A # caller-supplied ``out`` cannot be forwarded to the per-field math (it # would be reused for every field), so it is filled in afterwards. match method, ufunc, raw_inputs: case "reduce", np.add, [raw_input]: out_tuple = kwargs.pop("out", None) reduced = self._reduce(raw_input, **kwargs) if out_tuple is None: return reduced out_array: np.typing.NDArray[Any] = out_tuple[0] if out_array.dtype != self.dtype: raise TypeError( f"out= with dtype {out_array.dtype} is not supported for " f"{self.__class__.__name__} reductions; the dtype must " f"match the view ({self.dtype})" ) out_array[...] = reduced return out_array.view(self.__class__) # Everything else fills a pre-allocated result of this view's dtype (result,) = ( kwargs.pop("out") if "out" in kwargs else [ np.empty( np.broadcast_shapes(*(np.shape(r) for r in raw_inputs)), self.dtype, ) ] ) match method, ufunc, raw_inputs: # Unary + and - case "__call__", (np.negative | np.positive), [raw_input]: self._unary(raw_input, result, ufunc, **kwargs) return result.view(self.__class__) # Addition and subtraction case "__call__", (np.add | np.subtract), [raw1, raw2]: if ufunc is np.subtract and not self._SUPPORTS_SUB: raise TypeError( f"{self.__class__.__name__} does not support subtraction" ) if raw1.dtype == raw2.dtype: self._combine(raw1, raw2, result, ufunc, **kwargs) else: self._add_scalar(raw1, raw2, result, ufunc, **kwargs) return result.view(self.__class__) # Multiplication and division case ( "__call__", (np.multiply | np.divide | np.floor_divide), [raw1, raw2], ): if raw1.dtype == raw2.dtype: if not self._SUPPORTS_VIEW_PRODUCT: raise TypeError( f"{self.__class__.__name__} does not support multiplying " "or dividing two views" ) self._product(raw1, raw2, result, ufunc, **kwargs) else: self._scale(raw1, raw2, result, ufunc, **kwargs) return result.view(self.__class__) # Cumulative sums (``np.cumsum`` / ``np.add.accumulate``) case "accumulate", np.add, [raw_input]: self._accumulate(raw_input, result, **kwargs) return result.view(self.__class__) # If unsupported, just pass through (will return not implemented) # pylint: disable-next=no-member return super().__array_ufunc__(ufunc, method, *raw_inputs, **kwargs) # type: ignore[no-any-return] # --- Arithmetic hooks; the defaults are the field-wise (linear) behavior # correct for additive storages like WeightedSum. Non-linear views (means) # override the ones they need and reject the rest. --- def _unary(self, raw: Any, out: Any, ufunc: Ufunc, **kwargs: Any) -> None: del raw, out, kwargs raise TypeError( f"{self.__class__.__name__} does not support unary {ufunc.__name__}" ) def _combine( self, raw1: Any, raw2: Any, out: Any, ufunc: Ufunc, **kwargs: Any ) -> None: del raw1, raw2, out, ufunc, kwargs raise TypeError(f"{self.__class__.__name__} does not support this operation") def _add_scalar( self, raw1: Any, raw2: Any, out: Any, ufunc: Ufunc, **kwargs: Any ) -> None: del raw1, raw2, out, kwargs raise TypeError( f"{self.__class__.__name__} does not support {ufunc.__name__} " "with a scalar or array" ) def _scale( self, raw1: Any, raw2: Any, out: Any, ufunc: Ufunc, **kwargs: Any ) -> None: del raw1, raw2, out, kwargs raise TypeError(f"{self.__class__.__name__} does not support {ufunc.__name__}") def _product( self, raw1: Any, raw2: Any, out: Any, ufunc: Ufunc, **kwargs: Any ) -> None: del raw1, raw2, out, ufunc, kwargs raise TypeError( f"{self.__class__.__name__} does not support multiplying two views" ) def _reduce(self, raw: Any, **kwargs: Any) -> np.typing.NDArray[Any]: results = (np.add.reduce(raw[field], **kwargs) for field in self._FIELDS) return self._PARENT._make(*results) # type: ignore[return-value] def _accumulate(self, raw: Any, out: Any, **kwargs: Any) -> None: for field in self._FIELDS: np.add.accumulate(raw[field], out=out[field], **kwargs) def make_getitem_property(name: str) -> property: def fget(self: Mapping[str, Any]) -> Any: return self[name] def fset(self: MutableMapping[str, Any], value: Any) -> None: self[name] = value return property(fget, fset) def fields(*names: str) -> Callable[[type[object]], type[object]]: """ This decorator adds the name to the _FIELDS class property (for printing in reprs), and adds a property that looks like this: @property def name(self): return self["name"] @name.setter def name(self, value): self["name"] = value """ def injector(cls: type[object]) -> type[object]: if hasattr(cls, "_FIELDS"): raise RuntimeError( f"{cls.__name__} already has had a fields decorator applied" ) my_fields = [] for name in names: my_fields.append(name) setattr(cls, name, make_getitem_property(name)) cls._FIELDS = tuple(my_fields) # type: ignore[attr-defined] return cls return injector @fields("value", "variance") class WeightedSumView(View): __slots__ = () _PARENT = WeightedSum _SUPPORTS_SUB = True value: np.typing.NDArray[Any] variance: np.typing.NDArray[Any] def _unary(self, raw: Any, out: Any, ufunc: Ufunc, **kwargs: Any) -> None: ufunc(raw["value"], out=out["value"], **kwargs) out["variance"] = raw["variance"] def _combine( self, raw1: Any, raw2: Any, out: Any, ufunc: Ufunc, **kwargs: Any ) -> None: # Values add/subtract; independent variances always add. ufunc(raw1["value"], raw2["value"], out=out["value"], **kwargs) np.add(raw1["variance"], raw2["variance"], out=out["variance"], **kwargs) def _add_scalar( self, raw1: Any, raw2: Any, out: Any, ufunc: Ufunc, **kwargs: Any ) -> None: # The scalar/array operand is treated as a constant with variance s**2. if raw1.dtype == self.dtype: ufunc(raw1["value"], raw2, out=out["value"], **kwargs) np.add(raw1["variance"], raw2**2, out=out["variance"], **kwargs) else: ufunc(raw1, raw2["value"], out=out["value"], **kwargs) np.add(raw1**2, raw2["variance"], out=out["variance"], **kwargs) def _scale( self, raw1: Any, raw2: Any, out: Any, ufunc: Ufunc, **kwargs: Any ) -> None: # Multiplying/dividing by a scalar scales the variance by s**2. if raw1.dtype == self.dtype: ufunc(raw1["value"], raw2, out=out["value"], **kwargs) ufunc(raw1["variance"], raw2**2, out=out["variance"], **kwargs) elif ufunc in {np.divide, np.floor_divide}: # ``scalar / view``: the reciprocal of a weighted sum is not a # linear scaling, so no meaningful variance can be propagated. raise TypeError( f"Cannot divide a scalar or array by a {self.__class__.__name__}" ) else: ufunc(raw1, raw2["value"], out=out["value"], **kwargs) ufunc(raw1**2, raw2["variance"], out=out["variance"], **kwargs) class _MeanArithmetic: """Combine + scalar-scaling arithmetic shared by the mean-like views. Addition combines two samples (the Welford/West parallel merge, mirroring the C++ ``mean``/``weighted_mean`` ``operator+=``); multiplication/division by a scalar scales the mean and ``_sum_of_*deltas_squared`` (by ``s`` and ``s**2`` respectively) while leaving the weights untouched (mirroring ``operator*=``). Subtraction, scalar addition, unary minus, and cumulative sums are undefined for means and fall through to the rejecting :class:`View` defaults. """ __slots__ = () # Field names supplied by the concrete view subclass. _WEIGHT_FIELD: ClassVar[str] _DELTAS_FIELD: ClassVar[str] _WEIGHT_SQ_FIELD: ClassVar[str | None] = None if TYPE_CHECKING: # Provided by the View base that the concrete subclass also inherits. _FIELDS: ClassVar[tuple[str, ...]] _PARENT: type[Mean | WeightedMean] dtype: np.dtype[Any] def _combine( self, raw1: Any, raw2: Any, out: Any, ufunc: Ufunc, **kwargs: Any ) -> None: # Means only ever reach here for addition (subtraction is rejected). del ufunc, kwargs weight, deltas, weight_sq = ( self._WEIGHT_FIELD, self._DELTAS_FIELD, self._WEIGHT_SQ_FIELD, ) n1, n2 = raw1[weight], raw2[weight] mu1, mu2 = raw1["value"], raw2["value"] total = n1 + n2 # New mean = weight-weighted average; 0 (not NaN) where both are empty. new_value = np.zeros_like(total) np.divide(n1 * mu1 + n2 * mu2, total, out=new_value, where=total != 0) # ``deltas`` uses the *new* value; empty operands contribute nothing. # Compute every field into a fresh array *before* writing, so an # in-place ``v += other`` (where ``out`` aliases ``raw1``) is correct. new_deltas = ( raw1[deltas] + raw2[deltas] + n1 * (new_value - mu1) ** 2 + n2 * (new_value - mu2) ** 2 ) new_weight_sq = None if weight_sq is None else raw1[weight_sq] + raw2[weight_sq] out[weight] = total out["value"] = new_value out[deltas] = new_deltas if weight_sq is not None: out[weight_sq] = new_weight_sq def _scale( self, raw1: Any, raw2: Any, out: Any, ufunc: Ufunc, **kwargs: Any ) -> None: del kwargs # Floor division would truncate the (floating-point) mean and deltas, so # it is not a meaningful scaling of a mean accumulator. if ufunc is np.floor_divide: raise TypeError( f"{self.__class__.__name__} does not support floor division" ) weight, deltas, weight_sq = ( self._WEIGHT_FIELD, self._DELTAS_FIELD, self._WEIGHT_SQ_FIELD, ) if raw1.dtype == self.dtype: view, scalar = raw1, raw2 elif ufunc is np.divide: # ``scalar / view`` is not a meaningful scaling of a mean. raise TypeError( f"Cannot divide a scalar or array by a {self.__class__.__name__}" ) else: view, scalar = raw2, raw1 out[weight] = view[weight] out["value"] = ufunc(view["value"], scalar) # The mean scales by ``s``; its variance (hence ``deltas``) scales by # ``s**2``. Applying ``ufunc`` twice gives exactly that: for multiply # ``(x*s)*s == x*s**2`` and for divide ``(x/s)/s == x/s**2``. out[deltas] = ufunc(ufunc(view[deltas], scalar), scalar) if weight_sq is not None: out[weight_sq] = view[weight_sq] def _reduce(self, raw: Any, **kwargs: Any) -> np.typing.NDArray[Any]: axis = kwargs.pop("axis", 0) keepdims = kwargs.pop("keepdims", False) # ``np.sum`` always forwards ``dtype=None`` and ``where=True``; any # other reduction argument (a real dtype, a where mask, initial, ...) # cannot be honored by the mean-merging math below, so reject it # instead of silently ignoring it. if ( kwargs.pop("dtype", None) is not None or kwargs.pop("where", True) is not True or kwargs ): raise TypeError( f"{self.__class__.__name__} reductions only support the " "axis and keepdims arguments" ) weight, deltas, weight_sq = ( self._WEIGHT_FIELD, self._DELTAS_FIELD, self._WEIGHT_SQ_FIELD, ) counts = raw[weight] values = raw["value"] def reduce(array: Any, *, keep: bool = keepdims) -> Any: return np.add.reduce(array, axis=axis, keepdims=keep) # type: ignore[call-overload] with np.errstate(divide="ignore", invalid="ignore"): total = reduce(counts) combined = np.where(total != 0, reduce(counts * values) / total, 0.0) # The spread term needs the combined mean broadcast back against the # un-reduced ``values``; ``keepdims=True`` keeps the reduced axis as # size 1 so this works for any ``axis`` (int, tuple, or None) and is # independent of the caller's ``keepdims``. total_b = reduce(counts, keep=True) mean_b = np.where( total_b != 0, reduce(counts * values, keep=True) / total_b, 0.0 ) new_deltas = reduce(raw[deltas]) + reduce(counts * (values - mean_b) ** 2) out_fields: dict[str, Any] = { weight: total, "value": combined, deltas: new_deltas, } if weight_sq is not None: out_fields[weight_sq] = reduce(raw[weight_sq]) return self._PARENT._make(*(out_fields[name] for name in self._FIELDS)) # type: ignore[return-value] def _accumulate(self, raw: Any, out: Any, **kwargs: Any) -> None: del raw, out, kwargs raise TypeError(f"{self.__class__.__name__} does not support cumulative sums") @fields( "sum_of_weights", "sum_of_weights_squared", "value", "_sum_of_weighted_deltas_squared", ) class WeightedMeanView(_MeanArithmetic, View): __slots__ = () _PARENT = WeightedMean _WEIGHT_FIELD = "sum_of_weights" _DELTAS_FIELD = "_sum_of_weighted_deltas_squared" _WEIGHT_SQ_FIELD = "sum_of_weights_squared" sum_of_weights: np.typing.NDArray[Any] sum_of_weights_squared: np.typing.NDArray[Any] value: np.typing.NDArray[Any] _sum_of_weighted_deltas_squared: np.typing.NDArray[Any] @property def variance(self) -> np.typing.NDArray[Any]: with np.errstate(divide="ignore", invalid="ignore"): return self["_sum_of_weighted_deltas_squared"] / ( # type: ignore[no-any-return] self["sum_of_weights"] - self["sum_of_weights_squared"] / self["sum_of_weights"] ) @fields("count", "value", "_sum_of_deltas_squared") class MeanView(_MeanArithmetic, View): __slots__ = () _PARENT = Mean _WEIGHT_FIELD = "count" _DELTAS_FIELD = "_sum_of_deltas_squared" _WEIGHT_SQ_FIELD = None count: np.typing.NDArray[Any] value: np.typing.NDArray[Any] _sum_of_deltas_squared: np.typing.NDArray[Any] # Variance is a computation @property def variance(self) -> np.typing.NDArray[Any]: with np.errstate(divide="ignore", invalid="ignore"): return self["_sum_of_deltas_squared"] / (self["count"] - 1) # type: ignore[no-any-return] def _to_view( item: np.typing.NDArray[Any], value: bool = False ) -> np.typing.NDArray[Any] | WeightedSumView | WeightedMeanView | MeanView: for cls in View.__subclasses__(): if item.dtype.names == cls._FIELDS: ret = item.view(cls) if value and ret.shape: return ret.value # type: ignore[no-any-return, attr-defined] return ret return item scikit-hep-boost-histogram-b4c49b0/src/module.cpp000066400000000000000000000020221524660700500220630ustar00rootroot00000000000000// Copyright 2018-2019 Henry Schreiner and Hans Dembinski // // Distributed under the 3-Clause BSD License. See accompanying // file LICENSE or https://github.com/scikit-hep/boost-histogram for details. #include void register_algorithms(py::module&); void register_storages(py::module&); void register_axes(py::module&); void register_histograms(py::module&); void register_accumulators(py::module&); void register_transforms(py::module&); PYBIND11_MODULE(_core, m, py::mod_gil_not_used()) { py::module storage = m.def_submodule("storage"); register_storages(storage); py::module ax = m.def_submodule("axis"); register_axes(ax); py::module trans = ax.def_submodule("transform"); register_transforms(trans); py::module hist = m.def_submodule("hist"); register_histograms(hist); py::module accumulators = m.def_submodule("accumulators"); register_accumulators(accumulators); py::module algorithm = m.def_submodule("algorithm"); register_algorithms(algorithm); } scikit-hep-boost-histogram-b4c49b0/src/register_accumulators.cpp000066400000000000000000000313241524660700500252130ustar00rootroot00000000000000// Copyright 2018-2019 Henry Schreiner and Hans Dembinski // // Distributed under the 3-Clause BSD License. See accompanying // file LICENSE or https://github.com/scikit-hep/boost-histogram for details. #include #include #include #include #include #include #include #include #include namespace { /// The mean fill can be implemented once. (sum fill varies slightly) template decltype(auto) make_mean_fill() { return [](T& self, const py::object& value, const py::object& weight) { if(weight.is_none()) { py::vectorize([](T& self, double val) { self(val); })(self, value); } else { py::vectorize([](T& self, double wei, double val) { self(bh::weight(wei), val); })(self, weight, value); } return self; }; } /// The mean call can be implemented once. (sum uses +=) template decltype(auto) make_mean_call() { return [](T& self, double value, const py::object& weight) { if(weight.is_none()) self(value); else self(bh::weight(py::cast(weight)), value); return self; }; } template decltype(auto) make_buffer() { return [](T& self) -> py::buffer_info { return py::buffer_info( &self, // Pointer to buffer sizeof(T), // Size of one scalar py::format_descriptor::format(), // Format registered with // PYBIND11_NUMPY_DTYPE 0, // Number of dimensions {}, // Buffer dimensions {} // Stride ); }; } } // namespace // NOLINTNEXTLINE(misc-use-internal-linkage) void register_accumulators(py::module& accumulators) { // Naming convention: // If a value is publicly available in Boost.Histogram accumulators // as a method, it has the same name in the numpy record array. // If it is not available except through a computation, it has // the same name as the private property without the trailing _. using weighted_sum = accumulators::weighted_sum; PYBIND11_NUMPY_DTYPE(weighted_sum, value, variance); register_accumulator( accumulators, "WeightedSum", py::buffer_protocol()) .def_buffer(make_buffer()) .def(py::init(), "value"_a) .def(py::init(), "value"_a, "variance"_a) .def_readonly("value", &weighted_sum::value) .def_readonly("variance", &weighted_sum::variance) .def("__iadd__", [](weighted_sum& self, double value) { self += bh::weight(value); return self; }) .def( "fill", [](weighted_sum& self, const py::object& value, const py::object& variance) { if(variance.is_none()) { py::vectorize([](weighted_sum& self, double val) { self += bh::weight(val); })(self, value); } else { py::vectorize([](weighted_sum& self, double val, double var) { self += weighted_sum(val, var); })(self, value, variance); } return self; }, "value"_a, py::kw_only(), "variance"_a = py::none(), "Fill the accumulator with values. Optional variance parameter.") // This adapts existing memory to an accumulator .def_static("_make", py::vectorize([](const double& a, const double& b) { return weighted_sum(a, b); })) // This creates a array of accumulators using the normal constructor arguments .def_static("_array", py::vectorize([](const double& a, const double& b) { return weighted_sum(a, b); })) .def("__getitem__", [](const weighted_sum& self, const py::str& key) { if(key.equal(py::str("value"))) return self.value; if(key.equal(py::str("variance"))) return self.variance; throw py::key_error( py::str("{0} not one of value, variance").format(key)); }) .def("__setitem__", [](weighted_sum& self, const py::str& key, double value) { if(key.equal(py::str("value"))) self.value = value; else if(key.equal(py::str("variance"))) self.variance = value; else throw py::key_error( py::str("{0} not one of value, variance").format(key)); }) .def("_ipython_key_completions_", [](const py::object& /* self */) { return py::make_tuple("value", "variance"); }) ; using sum = bh::accumulators::sum; register_accumulator(accumulators, "Sum") .def(py::init(), "value"_a) .def_property_readonly("value", &sum::value) .def(py::self += double()) .def( "fill", [](sum& self, const py::object& value) { py::vectorize([](sum& self, double v) { self += v; })(self, value); return self; }, "value"_a, "Run over an array with the accumulator") .def_property_readonly("_small", &sum::small_part) .def_property_readonly("_large", &sum::large_part) ; using weighted_mean = accumulators::weighted_mean; PYBIND11_NUMPY_DTYPE(weighted_mean, sum_of_weights, sum_of_weights_squared, value, _sum_of_weighted_deltas_squared); register_accumulator( accumulators, "WeightedMean", py::buffer_protocol()) .def_buffer(make_buffer()) .def(py::init(), "sum_of_weights"_a, "sum_of_weights_squared"_a, "value"_a, "variance"_a) .def_readonly("sum_of_weights", &weighted_mean::sum_of_weights) .def_readonly("sum_of_weights_squared", &weighted_mean::sum_of_weights_squared) .def_readonly("value", &weighted_mean::value) .def_readonly("_sum_of_weighted_deltas_squared", &weighted_mean::_sum_of_weighted_deltas_squared) .def_property_readonly("variance", &weighted_mean::variance) .def("__call__", make_mean_call(), "value"_a, py::kw_only(), "weight"_a = py::none(), "Fill with value and optional keyword-only weight") .def("fill", make_mean_fill(), "value"_a, py::kw_only(), "weight"_a = py::none(), "Fill the accumulator with values. Optional weight parameter.") .def_static( "_make", py::vectorize( [](const double& a, const double& b, const double& c, const double& d) { return weighted_mean(a, b, c, d, true); })) .def_static( "_array", py::vectorize( [](const double& a, const double& b, const double& c, const double& d) { return weighted_mean(a, b, c, d); })) .def("__getitem__", [](const weighted_mean& self, const py::str& key) { if(key.equal(py::str("value"))) return self.value; if(key.equal(py::str("sum_of_weights"))) return self.sum_of_weights; if(key.equal(py::str("sum_of_weights_squared"))) return self.sum_of_weights_squared; if(key.equal(py::str("_sum_of_weighted_deltas_squared"))) return self._sum_of_weighted_deltas_squared; throw py::key_error( py::str("{0} not one of value, sum_of_weights, " "sum_of_weights_squared, _sum_of_weighted_deltas_squared") .format(key)); }) .def("__setitem__", [](weighted_mean& self, const py::str& key, double value) { if(key.equal(py::str("value"))) self.value = value; else if(key.equal(py::str("sum_of_weights"))) self.sum_of_weights = value; else if(key.equal(py::str("sum_of_weights_squared"))) self.sum_of_weights_squared = value; else if(key.equal(py::str("_sum_of_weighted_deltas_squared"))) self._sum_of_weighted_deltas_squared = value; else throw py::key_error( py::str( "{0} not one of value, sum_of_weights, " "sum_of_weights_squared, _sum_of_weighted_deltas_squared") .format(key)); }) .def("_ipython_key_completions_", [](const py::object& /* self */) { return py::make_tuple("value", "sum_of_weights", "sum_of_weights_squared", "_sum_of_weighted_deltas_squared"); }) ; using mean = accumulators::mean; PYBIND11_NUMPY_DTYPE(mean, count, value, _sum_of_deltas_squared); register_accumulator(accumulators, "Mean", py::buffer_protocol()) .def_buffer(make_buffer()) .def(py::init(), "count"_a, "value"_a, "variance"_a) .def_readonly("count", &mean::count) .def_readonly("value", &mean::value) .def_readonly("_sum_of_deltas_squared", &mean::_sum_of_deltas_squared) .def_property_readonly("variance", &mean::variance) .def("__call__", make_mean_call(), "value"_a, py::kw_only(), "weight"_a = py::none(), "Fill with value and optional keyword-only weight") .def("fill", make_mean_fill(), "value"_a, py::kw_only(), "weight"_a = py::none(), "Fill the accumulator with values. Optional weight parameter.") .def_static( "_make", py::vectorize([](const double& a, const double& b, const double& c) { return mean(a, b, c, true); })) .def_static( "_array", py::vectorize([](const double& a, const double& b, const double& c) { return mean(a, b, c); })) .def("__getitem__", [](const mean& self, const py::str& key) { if(key.equal(py::str("count"))) return self.count; if(key.equal(py::str("value"))) return self.value; if(key.equal(py::str("_sum_of_deltas_squared"))) return self._sum_of_deltas_squared; throw py::key_error( py::str("{0} not one of count, value, _sum_of_deltas_squared") .format(key)); }) .def("__setitem__", [](mean& self, const py::str& key, double value) { if(key.equal(py::str("count"))) self.count = value; else if(key.equal(py::str("value"))) self.value = value; else if(key.equal(py::str("_sum_of_deltas_squared"))) self._sum_of_deltas_squared = value; else throw py::key_error( py::str("{0} not one of count, value, _sum_of_deltas_squared") .format(key)); }) .def("_ipython_key_completions_", [](const py::object& /* self */) { return py::make_tuple("count", "value", "_sum_of_deltas_squared"); }) ; } scikit-hep-boost-histogram-b4c49b0/src/register_algorithm.cpp000066400000000000000000000224401524660700500244760ustar00rootroot00000000000000// Copyright 2018-2019 Henry Schreiner and Hans Dembinski // // Distributed under the 3-Clause BSD License. See accompanying // file LICENSE or https://github.com/scikit-hep/boost-histogram for details. #include #include // NOLINTNEXTLINE(misc-use-internal-linkage) void register_algorithms(py::module& algorithm) { py::class_(algorithm, "reduce_command") .def(py::init()) .def_readwrite("iaxis", &bh::algorithm::reduce_command::iaxis) .def("__repr__", [](const bh::algorithm::reduce_command& self) { using range_t = bh::algorithm::reduce_command::range_t; if(self.range != range_t::none) { // merge is always >= 1 (1 means "no extra rebin"), so only // show the "_and_rebin" suffix and merge= value when a real // merge factor was requested. const char* suffix = self.merge > 1 ? "_and_rebin" : ""; const char* c_start = self.iaxis == bh::algorithm::reduce_command::unset ? "" : "iaxis={0}, "; const char* c_merge = self.merge > 1 ? ", merge={0}" : ""; py::str const start = py::str(c_start).format(self.iaxis); py::str const merge = py::str(c_merge).format(self.merge); if(self.range == range_t::indices) { return py::str("reduce_command(slice{0}({1}begin={2}, " "end={3}{4}, mode={5}))") .format(suffix, start, self.begin.index, self.end.index, merge, self.crop ? "slice_mode.crop" : "slice_mode.shrink"); } const char* name = self.crop ? "crop" : "shrink"; return py::str("reduce_command({0}{1}({2}lower={3}, upper={4}{5}))") .format( name, suffix, start, self.begin.value, self.end.value, merge); } // self.range == range_t::none return py::str("reduce_command(merge({0}))").format(self.merge); }); using slice_mode = bh::algorithm::slice_mode; py::enum_(algorithm, "slice_mode") .value("shrink", slice_mode::shrink) .value("crop", slice_mode::crop); algorithm .def("shrink_and_rebin", py::overload_cast( &bh::algorithm::shrink_and_rebin), "iaxis"_a, "lower"_a, "upper"_a, "merge"_a, "Shrink and rebin option to be used in reduce().\n" "\n" "To shrink and rebin in one command. Equivalent to passing both the " "shrink() and the\n" "rebin() option for the same axis to reduce.\n" "\n" ":param iaxis: which axis to operate on.\n" ":param lower: lowest bound that should be kept.\n" ":param upper: highest bound that should be kept. If upper is inside " "bin interval, the whole " "interval is removed.\n" ":param merge: how many adjacent bins to merge into one.") .def("shrink_and_rebin", py::overload_cast( &bh::algorithm::shrink_and_rebin), "lower"_a, "upper"_a, "merge"_a, "Positional shrink and rebin option to be used in reduce().\n" "\n" "To shrink and rebin in one command. Equivalent to passing both the " "shrink() and the\n" "rebin() option for the same axis to reduce.\n" "\n" ":param iaxis: which axis to operate on.\n" ":param lower: lowest bound that should be kept.\n" ":param upper: highest bound that should be kept. If upper is inside " "bin interval, the whole " "interval is removed.\n" ":param merge: how many adjacent bins to merge into one.") .def("crop_and_rebin", py::overload_cast( &bh::algorithm::crop_and_rebin), "iaxis"_a, "lower"_a, "upper"_a, "merge"_a, "Crop and rebin option to be used in reduce().\n" "\n" "To crop and rebin in one command. Equivalent to passing both the " "crop and the\n" "rebin option for the same axis to reduce.\n" "\n" ":param iaxis: which axis to operate on.\n" ":param lower: lowest bound that should be kept.\n" ":param upper: highest bound that should be kept. If upper is inside " "bin interval, the whole " "interval is removed.\n" ":param merge: how many adjacent bins to merge into one.") .def( "crop_and_rebin", py::overload_cast(&bh::algorithm::crop_and_rebin), "lower"_a, "upper"_a, "merge"_a, "Positional crop and rebin option to be used in reduce().\n" "\n" "To crop and rebin in one command. Equivalent to passing both the " "crop and the\n" "rebin option for the same axis to reduce.\n" "\n" ":param iaxis: which axis to operate on.\n" ":param lower: lowest bound that should be kept.\n" ":param upper: highest bound that should be kept. If upper is inside " "bin interval, the whole " "interval is removed.\n" ":param merge: how many adjacent bins to merge into one.") .def("slice_and_rebin", py::overload_cast(&bh::algorithm::slice_and_rebin), "iaxis"_a, "begin"_a, "end"_a, "merge"_a, "mode"_a = slice_mode::shrink, "Slice and rebin option to be used in reduce().\n" "\n" "To slice and rebin in one command. Equivalent to passing both the " "slice() and the\n" "rebin() option for the same axis to reduce.\n" "\n" ":param iaxis: which axis to operate on.\n" ":param begin: first index that should be kept.\n" ":param end: one past the last index that should be kept.\n" ":param merge: how many adjacent bins to merge into one.\n" ":param mode: see slice_mode") .def("slice_and_rebin", py::overload_cast(&bh::algorithm::slice_and_rebin), "begin"_a, "end"_a, "merge"_a, "mode"_a = slice_mode::shrink, "Positional slice and rebin option to be used in reduce().\n" "\n" "To slice and rebin in one command. Equivalent to passing both the " "slice() and the\n" "rebin() option for the same axis to reduce.\n" "\n" ":param iaxis: which axis to operate on.\n" ":param begin: first index that should be kept.\n" ":param end: one past the last index that should be kept.\n" ":param merge: how many adjacent bins to merge into one.\n" ":param mode: see slice_mode") .def("rebin", py::overload_cast(&bh::algorithm::rebin), "iaxis"_a, "merge"_a) .def("rebin", py::overload_cast(&bh::algorithm::rebin), "merge"_a) .def("shrink", py::overload_cast(&bh::algorithm::shrink), "iaxis"_a, "lower"_a, "upper"_a) .def("shrink", py::overload_cast(&bh::algorithm::shrink), "lower"_a, "upper"_a) .def("crop", py::overload_cast(&bh::algorithm::crop), "iaxis"_a, "lower"_a, "upper"_a) .def("crop", py::overload_cast(&bh::algorithm::crop), "lower"_a, "upper"_a) .def("slice", py::overload_cast(&bh::algorithm::slice), "iaxis"_a, "begin"_a, "end"_a, "mode"_a = slice_mode::shrink) .def("slice", py::overload_cast( &bh::algorithm::slice), "begin"_a, "end"_a, "mode"_a = slice_mode::shrink) ; } scikit-hep-boost-histogram-b4c49b0/src/register_axis.cpp000066400000000000000000000065151524660700500234610ustar00rootroot00000000000000// Copyright 2018-2019 Henry Schreiner and Hans Dembinski // // Distributed under the 3-Clause BSD License. See accompanying // file LICENSE or https://github.com/scikit-hep/boost-histogram for details. #include #include #include #include #include #include namespace { template void register_axis_each(py::module& mod, Func&& function) { using namespace boost::mp11; using types = mp_list; mp_for_each>([&](auto I) { using T = mp_at_c; auto ax = register_axis(mod); function(ax); }); } } // namespace // NOLINTNEXTLINE(misc-use-internal-linkage) void register_axes(py::module& mod) { register_axis_each(mod, [](auto ax) { ax.def(py::init(), "bins"_a, "start"_a, "stop"_a); }); register_axis(mod) .def(py::init([](unsigned n, double start, double stop, double pow) { return new axis::regular_pow( bh::axis::transform::pow{pow}, n, start, stop); }), "bins"_a, "start"_a, "stop"_a, "power"_a) .def_property_readonly("transform", [](const axis::regular_pow& self) { return self.transform(); }); register_axis(mod) .def(py::init([](unsigned n, double start, double stop, func_transform& trans) { return new axis::regular_trans(trans, n, start, stop); }), "bins"_a, "start"_a, "stop"_a, "transform"_a) .def_property_readonly("transform", [](const axis::regular_trans& self) { return self.transform(); }); register_axis_each( mod, [](auto ax) { ax.def(py::init>(), "edges"_a); }); register_axis_each( mod, [](auto ax) { ax.def(py::init(), "start"_a, "stop"_a); }); register_axis_each( mod, [](auto ax) { ax.def(py::init>(), "categories"_a); }); register_axis_each(mod, [](auto ax) { ax.def(py::init>(), "categories"_a); }); register_axis(mod, "boolean").def(py::init<>()); ; } scikit-hep-boost-histogram-b4c49b0/src/register_histograms.cpp000066400000000000000000000036171524660700500246750ustar00rootroot00000000000000// Copyright 2018-2019 Henry Schreiner and Hans Dembinski // // Distributed under the 3-Clause BSD License. See accompanying // file LICENSE or https://github.com/scikit-hep/boost-histogram for details. #include #include #include #include #include #include // NOLINTNEXTLINE(misc-use-internal-linkage) void register_histograms(py::module& hist) { hist.attr("_axes_limit") = BOOST_HISTOGRAM_DETAIL_AXES_LIMIT; register_histogram( hist, "any_int64", "N-dimensional histogram for unlimited size data with any axis types."); register_histogram( hist, "any_unlimited", "N-dimensional histogram for unlimited size data with any axis types."); register_histogram( hist, "any_double", "N-dimensional histogram for real-valued data with weights with any axis " "types."); register_histogram( hist, "any_atomic_int64", "N-dimensional histogram for threadsafe integer data with any axis types."); register_histogram( hist, "any_weight", "N-dimensional histogram for weighted data with any axis types."); register_histogram( hist, "any_mean", "N-dimensional histogram for sampled data with any axis types."); register_histogram( hist, "any_weighted_mean", "N-dimensional histogram for weighted and sampled data with any axis types."); register_histogram( hist, "any_multi_cell", "N-dimensional histogram for storing multiple cells at once with any axis " "types."); } scikit-hep-boost-histogram-b4c49b0/src/register_storage.cpp000066400000000000000000000030231524660700500241500ustar00rootroot00000000000000// Copyright 2018-2019 Henry Schreiner and Hans Dembinski // // Distributed under the 3-Clause BSD License. See accompanying // file LICENSE or https://github.com/scikit-hep/boost-histogram for details. #include #include #include #include // NOLINTNEXTLINE(misc-use-internal-linkage) void register_storages(py::module& storage) { register_storage( storage, "int64", "Integers in vectors storage type"); register_storage( storage, "double", "Weighted storage without variance type (fast but simple)"); register_storage( storage, "atomic_int64", "Threadsafe (not growing axis) integer storage"); register_storage( storage, "unlimited", "Optimized for unweighted histograms, adaptive"); register_storage( storage, "weight", "Dense storage which tracks sums of weights and a variance estimate"); register_storage( storage, "mean", "Dense storage which tracks means of samples in each cell"); register_storage( storage, "weighted_mean", "Dense storage which tracks means of weighted samples in each cell"); register_storage( storage, "multi_cell", "Dense storage which tracks sums of cell entries for multiple cells per entry"); } scikit-hep-boost-histogram-b4c49b0/src/register_transforms.cpp000066400000000000000000000070311524660700500247050ustar00rootroot00000000000000// Copyright 2018-2019 Henry Schreiner and Hans Dembinski // // Distributed under the 3-Clause BSD License. See accompanying // file LICENSE or https://github.com/scikit-hep/boost-histogram for details. #include #include #include #include #include #include #include namespace { template py::class_ register_transform(py::module& mod, Args&&... args) { py::class_ transform(mod, std::forward(args)...); transform .def(py::init()) .def("forward", [](const T& self, double v) { return self.forward(v); }) .def("inverse", [](const T& self, double v) { return self.inverse(v); }) .def(make_pickle()) .def("__copy__", [](const T& self) { return T(self); }) .def("__deepcopy__", &deep_copy) ; return transform; } } // namespace extern "C" { double _log_fn(double v) { return std::log(v); } double _exp_fn(double v) { return std::exp(v); } double _sqrt_fn(double v) { return std::sqrt(v); } double _sq_fn(double v) { return v * v; } } // NOLINTNEXTLINE(misc-use-internal-linkage) void register_transforms(py::module& mod) { mod.def("_log_fn", &_log_fn); mod.def("_exp_fn", &_exp_fn); mod.def("_sqrt_fn", &_sqrt_fn); mod.def("_sq_fn", &_sq_fn); register_transform(mod, "id") .def(py::init<>()) .def("__repr__", [](const py::object& self) { return py::str("{}()").format(self.attr("__class__").attr("__name__")); }) ; register_transform(mod, "sqrt") .def(py::init<>()) .def("__repr__", [](const py::object& self) { return py::str("{}()").format(self.attr("__class__").attr("__name__")); }) ; register_transform(mod, "log") .def(py::init<>()) .def("__repr__", [](const py::object& self) { return py::str("{}()").format(self.attr("__class__").attr("__name__")); }) ; register_transform(mod, "pow") .def(py::init(), "power"_a) .def_readonly("power", &bh::axis::transform::pow::power) .def("__repr__", [](const py::object& self) { double const power = py::cast(self).power; return py::str("{}({:g})") .format(self.attr("__class__").attr("__name__"), power); }) ; register_transform(mod, "func_transform") .def(py::init(), "forward"_a, "inverse"_a, "convert"_a, "name"_a) .def(py::self == py::self) .def(py::self != py::self) .def("__repr__", [](const py::object& self) -> py::object { auto& s = py::cast(self); if(s._name.unguarded_get().equal(py::str(""))) { return py::str("{}({}, {})") .format(self.attr("__class__").attr("__name__"), s._forward_ob.unguarded_get(), s._inverse_ob.unguarded_get()); } return s._name.unguarded_get(); }) ; } scikit-hep-boost-histogram-b4c49b0/tests/000077500000000000000000000000001524660700500204515ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/tests/CMakeLists.txt000066400000000000000000000023401524660700500232100ustar00rootroot00000000000000pybind11_find_import(numpy) pybind11_find_import(pytest) pybind11_find_import(pytest-benchmark) # Much better test target on CMake 3.29+ set(CMAKE_SKIP_TEST_ALL_DEPENDENCY FALSE) # Support for running from build directory file(WRITE "${PROJECT_BINARY_DIR}/pytest.ini" "[pytest]\n" "addopts = --benchmark-disable\n" "testpaths = ${CMAKE_CURRENT_SOURCE_DIR}\n") # Support plain "pytest" in addition to "python -m pytest" file(WRITE "${PROJECT_BINARY_DIR}/conftest.py" "import sys\n" "sys.path.insert(0, '${PROJECT_BINARY_DIR}')\n") # Look for all the tests file(GLOB BOOST_HIST_PY_TESTS CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/test_*.py") # Add each test foreach(TEST_FILE IN LISTS BOOST_HIST_PY_TESTS) get_filename_component(TEST_NAME "${TEST_FILE}" NAME_WE) add_test( NAME ${TEST_NAME} COMMAND ${Python_EXECUTABLE} -m pytest "${TEST_FILE}" --rootdir=. --showlocals WORKING_DIRECTORY "${PROJECT_BINARY_DIR}") set_tests_properties(${TEST_NAME} PROPERTIES SKIP_RETURN_CODE 5) endforeach() add_test( NAME boost_histogram_test COMMAND ${Python_EXECUTABLE} -m boost_histogram.test WORKING_DIRECTORY "${PROJECT_BINARY_DIR}") scikit-hep-boost-histogram-b4c49b0/tests/conftest.py000066400000000000000000000047251524660700500226600ustar00rootroot00000000000000from __future__ import annotations import contextlib import importlib.metadata import sys import sysconfig from pathlib import Path if sys.version_info < (3, 11): import tomli as tomllib else: import tomllib import pytest from packaging.requirements import Requirement import boost_histogram as bh DIR = Path(__file__).parent BASE = DIR.parent @pytest.fixture(params=(False, True), ids=("no_growth", "growth")) def growth(request): return request.param @pytest.fixture(params=(False, True), ids=("no_overflow", "overflow")) def overflow(request): return request.param @pytest.fixture(params=(False, True), ids=("no_underflow", "underflow")) def underflow(request): return request.param @pytest.fixture(params=(False, True), ids=("no_flow", "flow")) def flow(request): return request.param @pytest.fixture( params=(None, "str", 1, {"a": 1}), ids=("no_metadata", "str_metadata", "int_metadata", "dict_metadata"), ) def metadata(request): return request.param @pytest.fixture( params=( bh.storage.Double, bh.storage.Int64, bh.storage.AtomicInt64, bh.storage.Weight, bh.storage.Unlimited, ), ids=("Double", "Int64", "AtomicInt64", "Weight", "Unlimited"), ) def count_storage(request): return request.param @pytest.fixture( params=( bh.storage.Double, bh.storage.Int64, bh.storage.AtomicInt64, bh.storage.Unlimited, ), ids=("Double", "Int64", "AtomicInt64", "Unlimited"), ) def count_single_storage(request): return request.param def pytest_report_header() -> str: with BASE.joinpath("pyproject.toml").open("rb") as f: pyproject = tomllib.load(f) project = pyproject.get("project", {}) pkgs = project.get("dependencies", []) pkgs += [p for ps in project.get("optional-dependencies", {}).values() for p in ps] if "name" in project: pkgs.append(project["name"]) interesting_packages = {Requirement(p).name for p in pkgs} interesting_packages.add("pip") interesting_packages.add("numpy") valid = [] for package in sorted(interesting_packages): with contextlib.suppress(ModuleNotFoundError): valid.append(f"{package}=={importlib.metadata.version(package)}") reqs = " ".join(valid) lines = [ f"installed packages of interest: {reqs}", ] if sysconfig.get_config_var("Py_GIL_DISABLED"): lines.append("free-threaded Python build") return "\n".join(lines) scikit-hep-boost-histogram-b4c49b0/tests/pickles/000077500000000000000000000000001524660700500221035ustar00rootroot00000000000000scikit-hep-boost-histogram-b4c49b0/tests/pickles/bh_0.10.2.pkl000066400000000000000000000023561524660700500240100ustar00rootroot00000000000000€}q(Xh1qcboost_histogram Histogram q)�q}q(X_histqcboost_histogram._core.hist any_double q)�q(KKKKKKKKNGÀ.G@>KKcnumpy.core.multiarray _reconstruct qcnumpy ndarray q K…q c_codecs encode q Xbq Xlatin1q †qRq‡qRq(KK!…qcnumpy dtype qXf8q‰ˆ‡qRq(KXbNKKhh K…q?h‡q@RqA(KK …qBh‰h XSð?ð?ð?qCh †qDRqEtqFbtqGbhNubXversionqHX0.10.2qIu.scikit-hep-boost-histogram-b4c49b0/tests/pickles/bh_0.11.1.pkl000066400000000000000000000024251524660700500240050ustar00rootroot00000000000000€}q(Xh1qcboost_histogram Histogram q)�qK}q(XmetadataqNX_histqcboost_histogram._core.hist any_double q)�q(KKKKKKKK}q hNsGÀ.G@>KKcnumpy.core.multiarray _reconstruct q cnumpy ndarray q K…q c_codecs encode q XbqXlatin1q†qRq‡qRq(KK!…qcnumpy dtype qXf8q‰ˆ‡qRq(KX(KK…q?h‰h X!ð?@@q@h†qARqBtqCb}qDhNsKKh h K…qEh‡qFRqG(KK …qHh‰h XSð?ð?ð?qIh†qJRqKtqLbtqMbu†qNbXversionqOX0.11.1qPu.scikit-hep-boost-histogram-b4c49b0/tests/pickles/bh_0.6.2.pkl000066400000000000000000000023131524660700500237260ustar00rootroot00000000000000€}q(Xh1qcboost_histogram Histogram q)�q}qX_histqcboost_histogram._core.hist any_double q)�q(KKKKKKKKNGÀ.G@>KKcnumpy.core.multiarray _reconstruct qcnumpy ndarray q K…q c_codecs encode q Xbq Xlatin1q †qRq‡qRq(KK!…qcnumpy dtype qXf8q‰ˆ‡qRq(KXRq?(KK …q@h‰h XSð?ð?ð?qAh †qBRqCtqDbtqEbsbXversionqFX0.6.2qGu.scikit-hep-boost-histogram-b4c49b0/tests/pickles/bh_1.1.0.pkl000066400000000000000000000025621524660700500237260ustar00rootroot00000000000000€}q(Xh1qcboost_histogram Histogram q)�qK}q(X_variance_knownqˆXmetadataqNX_histqcboost_histogram._core.hist any_int64 q)�q (KKKKKKKK}q GÀ.G@>KKcnumpy.core.multiarray _reconstruct q cnumpy ndarray q K…q c_codecs encode qXbqXlatin1q†qRq‡qRq(KK!…qcnumpy dtype qXu8q‰ˆ‡qRq(KX(KKKKKKKXoneq?Xtwoq@}qAKKK Kh h K…qBh‡qCRqD(KK…qEh2‰hX!ð?@@qFh†qGRqHtqIb}qJKKh h K…qKh‡qLRqM(KK …qNh2‰hXSð?ð?ð?qOh†qPRqQtqRbtqSbu†qTbXversionqUX1.1.0qVu.scikit-hep-boost-histogram-b4c49b0/tests/pickles/make_pickle.py000077500000000000000000000022601524660700500247240ustar00rootroot00000000000000#!/usr/bin/env python3 from __future__ import annotations import pickle from pathlib import Path import typer import boost_histogram as bh DIR = Path(__file__).parent def make_pickle( output: Path | None = typer.Argument(None, exists=False), # noqa: B008 *, protocol: int = 2, ) -> None: """ Make a pickle file with the current boost-histogram for use in tests. """ VER = tuple(map(int, bh.__version__.split(".")[:3])) if output is None: output = DIR / f"bh_{bh.__version__}.pkl" h1 = bh.Histogram(bh.axis.Regular(31, -15, 15), storage=bh.storage.Int64()) h2 = bh.Histogram( bh.axis.Integer(0, 5, metadata={"hello": "world"}), storage=bh.storage.Weight() ) if VER >= (0, 9, 0): h2.metadata = "foo" h3 = bh.Histogram( bh.axis.StrCategory([], growth=True), bh.axis.Variable([0, 1, 2, 3]) ) h1.fill([-5, 1, 1, 2]) h2.fill([1, 2, 2, 3, 3, 3]) h3.fill(["one", "two", "two"], [3, 2, 1]) with output.open("wb") as f: pickle.dump( {"h1": h1, "h2": h2, "h3": h3, "version": bh.__version__}, f, protocol ) if __name__ == "__main__": typer.run(make_pickle) scikit-hep-boost-histogram-b4c49b0/tests/test_accumulators.py000066400000000000000000000100541524660700500245640ustar00rootroot00000000000000from __future__ import annotations import pytest from pytest import approx import boost_histogram as bh hypothesis = pytest.importorskip("hypothesis") st = pytest.importorskip("hypothesis.strategies") hypothesis.settings.register_profile("ci", deadline=None) hypothesis.settings.load_profile("ci") def test_weighted_sum(): a = bh.accumulators.WeightedSum(1.5, 2.5) assert repr(a) == "WeightedSum(value=1.5, variance=2.5)" assert a == bh.accumulators.WeightedSum(1.5, 2.5) assert a.value == 1.5 assert a.variance == 2.5 a += 1.5 assert a.value == 3.0 assert a.variance == 4.75 vals = [1, 2, 3] vari = [4, 5, 6] a = bh.accumulators.WeightedSum() for val, var in zip(vals, vari, strict=False): a += bh.accumulators.WeightedSum(val, variance=var) assert a.value == 6 assert a.variance == 15 a2 = bh.accumulators.WeightedSum().fill(vals, variance=vari) assert a == a2 assert a == bh.accumulators.WeightedSum(6, 15) def test_sum(): vals = [1, 2, 3] a = bh.accumulators.Sum() for val in vals: a += val assert a.value == 6 a2 = bh.accumulators.Sum().fill(vals) assert a == a2 assert a == bh.accumulators.Sum(6) assert repr(a) == "Sum(6 + 0)" def test_weighted_mean(): vals = [4, 1] weights = [1, 2] a = bh.accumulators.WeightedMean() for v, w in zip(vals, weights, strict=False): a(v, weight=w) assert a.sum_of_weights == 3.0 assert a.variance == 4.5 assert a.value == 2.0 a2 = bh.accumulators.WeightedMean().fill(vals, weight=weights) assert a == a2 assert a == bh.accumulators.WeightedMean(3, 5, 2, 4.5) assert ( repr(a) == "WeightedMean(sum_of_weights=3, sum_of_weights_squared=5, value=2, variance=4.5)" ) def test_mean(): vals = [1, 2, 3] a = bh.accumulators.Mean() for val in vals: a(val) assert a.count == 3 assert a.value == 2 assert a.variance == 1 a2 = bh.accumulators.Mean().fill([1, 2, 3]) assert a == a2 assert a == bh.accumulators.Mean(3, 2, 1) assert repr(a) == "Mean(count=3, value=2, variance=1)" float_st = st.floats( allow_nan=False, allow_infinity=False, min_value=-1e4, max_value=1e4 ) simple_list_st = st.lists(float_st, min_size=1, max_size=10) @hypothesis.given( simple_list_st, simple_list_st, ) def test_sum_mean(list1, list2): a = bh.accumulators.Mean() a.fill(list1) b = bh.accumulators.Mean() b.fill(list2) c = bh.accumulators.Mean() c.fill(list1 + list2) ab = a + b assert ab.value == approx(c.value) assert ab.variance == approx(c.variance, nan_ok=True, abs=1e-7, rel=1e-3) assert ab.count == approx(c.count) a += b assert a.value == approx(c.value) assert a.variance == approx(c.variance, nan_ok=True, abs=1e-7, rel=1e-3) assert a.count == approx(c.count) dual_lists_st = st.integers(min_value=1, max_value=10).flatmap( lambda n: st.tuples( st.lists(float_st, min_size=n, max_size=n), st.lists( st.floats( allow_nan=False, allow_infinity=False, min_value=1e-2, max_value=1e2 ), min_size=n, max_size=n, ), ) ) @hypothesis.given(dual_lists_st, dual_lists_st) def test_sum_weighed_mean(pair1, pair2): a = bh.accumulators.WeightedMean() a.fill(pair1[0], weight=pair1[1]) b = bh.accumulators.WeightedMean() b.fill(pair2[0], weight=pair2[1]) c = bh.accumulators.WeightedMean() c.fill(pair1[0] + pair2[0], weight=pair1[1] + pair2[1]) ab = a + b assert ab.value == approx(c.value) assert ab.variance == approx(c.variance, nan_ok=True, abs=1e-3, rel=1e-2) assert ab.sum_of_weights == approx(c.sum_of_weights) assert ab.sum_of_weights_squared == approx(c.sum_of_weights_squared) a += b assert a.value == approx(c.value) assert a.variance == approx(c.variance, nan_ok=True, abs=1e-3, rel=1e-2) assert a.sum_of_weights == approx(c.sum_of_weights) assert a.sum_of_weights_squared == approx(c.sum_of_weights_squared) scikit-hep-boost-histogram-b4c49b0/tests/test_axes_object.py000066400000000000000000000062121524660700500243510ustar00rootroot00000000000000from __future__ import annotations import numpy as np import pytest from pytest import approx import boost_histogram as bh @pytest.fixture def h(): return bh.Histogram( bh.axis.Regular(10, 0, 10, metadata=2), bh.axis.Integer(0, 5, metadata="hi"), bh.axis.StrCategory(["HI", "HO"]), ) def test_axes_basics(h): assert h.axes.bin(1, 2, 0) == ((1.0, 2.0), 2, "HI") assert h.axes.value(0, 0, 0) == (0.0, 0, "HI") assert h.axes.index(2, 3, "HO") == (2, 3, 1) assert h.axes.size == (10, 5, 2) assert h.axes.extent == (12, 7, 3) assert h.axes.metadata == (2, "hi", None) h.axes.metadata = None, 3, "bye" assert h.axes.metadata == (None, 3, "bye") def test_axes_centers(h): centers = h.axes.centers answers = np.ogrid[0.5:10, 0.5:5, 0.5:2] full_answers = np.mgrid[0.5:10, 0.5:5, 0.5:2] for i in range(3): assert centers.broadcast()[i] == approx(full_answers[i]) assert centers[i] == approx(answers[i]) assert centers.T[i] == approx(answers[i].T) assert centers.flatten()[i] == approx(answers[i].flatten()) assert h.axes[i].centers == approx(answers[i].ravel()) def test_axes_edges(h): edges = h.axes.edges answers = np.ogrid[0:11, 0:6, 0:3] full_answers = np.mgrid[0:11, 0:6, 0:3] for i in range(3): assert edges.broadcast()[i] == approx(full_answers[i]) assert edges[i] == approx(answers[i]) assert edges.T[i] == approx(answers[i].T) assert edges.ravel()[i] == approx(answers[i].ravel()) assert h.axes[i].edges == approx(answers[i].ravel()) def test_axes_widths(h): widths = h.axes.widths answers = np.ogrid[1:1:10j, 1:1:5j, 1:1:2j] full_answers = np.mgrid[1:1:10j, 1:1:5j, 1:1:2j] for i in range(3): assert widths.broadcast()[i] == approx(full_answers[i]) assert widths[i] == approx(answers[i]) assert widths.T[i] == approx(answers[i].T) assert widths.ravel()[i] == approx(answers[i].ravel()) assert h.axes[i].widths == approx(answers[i].ravel()) def test_axis_misconstuct(): inp = [bh.axis.Regular(12, 0, 1)] ok = bh.axis.AxesTuple(inp) assert ok[0] == inp[0] with pytest.raises(TypeError): bh.axis.AxesTuple(inp[0]) def test_histogram_axes_do_not_alias_metadata(): # Passing the same axis instance twice must not let the two histogram # axes (or the original axis) share one metadata dict. a = bh.axis.Regular(3, 0, 1) h = bh.Histogram(a, a) h.axes[0].label = "x" assert a.metadata is None assert h.axes[1].metadata is None # The conversion constructor must give its axes their own metadata too. h1 = bh.Histogram(bh.axis.Regular(2, 0, 1)) h2 = bh.Histogram(h1) h2.axes[0].label = "y" assert h1.axes[0].metadata is None def test_array_tuple_dir(h): # Issue #1143 (B15): __dir__ used dir() of a string literal listing = dir(h.axes.centers) assert "sum" in listing assert "broadcast" in listing # ndarray attributes are forwarded, so they should be listed assert "shape" in listing assert "T" in listing assert "upper" not in listing # no str methods scikit-hep-boost-histogram-b4c49b0/tests/test_axis.py000066400000000000000000001104651524660700500230350ustar00rootroot00000000000000from __future__ import annotations import abc import copy import numpy as np import pytest from pytest import approx import boost_histogram as bh @pytest.mark.parametrize( ("axis", "args", "opt", "kwargs"), [ (bh.axis.Regular, (1, 2, 3), "", {}), (bh.axis.Regular, (1, 2, 3), "u", {}), (bh.axis.Regular, (1, 2, 3), "o", {}), (bh.axis.Regular, (1, 2, 3), "uo", {}), (bh.axis.Regular, (1, 2, 3), "g", {}), (bh.axis.Regular, (1, 2, 3), "", {"circular": True}), (bh.axis.Regular, (1, 2, 3), "", {"transform": bh.axis.transform.log}), (bh.axis.Regular, (1, 2, 3), "", {"transform": bh.axis.transform.sqrt}), (bh.axis.Regular, (1, 2, 3), "", {"transform": bh.axis.transform.Pow(1)}), (bh.axis.Variable, ((1, 2, 3),), "", {}), (bh.axis.Variable, ((1, 2, 3),), "u", {}), (bh.axis.Variable, ((1, 2, 3),), "o", {}), (bh.axis.Variable, ((1, 2, 3),), "uo", {}), (bh.axis.Variable, ((1, 2, 3),), "g", {}), (bh.axis.Variable, ((1, 2, 3),), "", {"circular": True}), (bh.axis.Integer, (1, 2), "", {}), (bh.axis.Integer, (1, 2), "u", {}), (bh.axis.Integer, (1, 2), "o", {}), (bh.axis.Integer, (1, 2), "uo", {}), (bh.axis.Integer, (1, 2), "g", {}), (bh.axis.Integer, (1, 2), "", {"circular": True}), (bh.axis.IntCategory, ((1, 2, 3),), "", {}), (bh.axis.IntCategory, ((1, 2, 3),), "o", {}), (bh.axis.IntCategory, ((1, 2, 3),), "g", {}), (bh.axis.IntCategory, ((1, 2, 3),), "go", {}), (bh.axis.IntCategory, ((),), "g", {}), (bh.axis.StrCategory, (tuple("ABC"),), "", {}), (bh.axis.StrCategory, (tuple("ABC"),), "o", {}), (bh.axis.StrCategory, (tuple("ABC"),), "g", {}), (bh.axis.StrCategory, (tuple("ABC"),), "go", {}), (bh.axis.StrCategory, ((),), "g", {}), ], ) def test_metadata(axis, args, opt, kwargs): for m in ("foo", 64, {"one": 1}): if "u" in opt: kwargs["underflow"] = False if "o" in opt: kwargs["overflow"] = False if "g" in opt: kwargs["growth"] = True kwargs["metadata"] = m assert axis(*args, **kwargs).metadata is m mcopy = copy.deepcopy(m) assert axis(*args, **kwargs).metadata == m assert axis(*args, **kwargs).metadata == mcopy assert axis(*args, **kwargs).metadata != "bar" assert axis(*args, **kwargs) == axis(*args, **kwargs) assert axis(*args, **kwargs) != axis(*args, metadata="bar") del kwargs["metadata"] ax = axis(*args, __dict__={"metadata": 3, "other": 2}) assert ax.metadata == 3 assert ax.other == 2 del ax.__dict__ assert ax.__dict__ == {} assert ax.metadata is None ax.__dict__ = {"metadata": 5} assert ax.__dict__ == {"metadata": 5} assert ax.metadata == 5 # Python 2 did not allow mixing ** and kw new_kwargs = copy.copy(kwargs) new_kwargs["__dict__"] = {"something": 2} new_kwargs["metadata"] = 3 with pytest.raises(KeyError): axis(*args, **new_kwargs) new_kwargs = copy.copy(kwargs) new_kwargs["__dict__"] = {"metadata": 2} new_kwargs["metadata"] = 3 with pytest.raises(KeyError): axis(*args, **new_kwargs) # The point of this ABC is to force all the tests listed here to be implemented # for each axis type. Pytest instantiates these test classes for us, so missing # one really does fail the test. class Axis(abc.ABC): @abc.abstractmethod def test_init(self): pass @abc.abstractmethod def test_traits(self): pass @abc.abstractmethod def test_equal(self): pass @abc.abstractmethod def test_len(self): pass @abc.abstractmethod def test_repr(self): pass @abc.abstractmethod def test_getitem(self): pass @abc.abstractmethod def test_iter(self): pass @abc.abstractmethod def test_index(self): pass @abc.abstractmethod def test_edges_centers_widths(self): pass class TestRegular(Axis): def test_init(self): # Should not throw bh.axis.Regular(1, 1.0, 2.0) bh.axis.Regular(1, 1.0, 2.0, metadata="ra") bh.axis.Regular(1, 1.0, 2.0, underflow=False) bh.axis.Regular(1, 1.0, 2.0, underflow=False, overflow=False, metadata="ra") bh.axis.Regular(1, 1.0, 2.0, metadata=0) bh.axis.Regular(1, 1.0, 2.0, transform=bh.axis.transform.log) bh.axis.Regular(1, 1.0, 2.0, transform=bh.axis.transform.sqrt) bh.axis.Regular(1, 1.0, 2.0, transform=bh.axis.transform.Pow(1.5)) with pytest.raises(TypeError): bh.axis.Regular() with pytest.raises(TypeError): bh.axis.Regular(overflow=False, underflow=False) with pytest.raises(TypeError): bh.axis.Regular(1) with pytest.raises(TypeError): bh.axis.Regular(1, 1.0) with pytest.raises(ValueError): bh.axis.Regular(0, 1.0, 2.0) with pytest.raises(TypeError): bh.axis.Regular("1", 1.0, 2.0) with pytest.raises(TypeError): bh.axis.Regular(-1, 1.0, 2.0) with pytest.raises(ValueError): bh.axis.Regular(1, 1.0, 1.0) with pytest.raises(TypeError): bh.axis.Regular(1, 1.0, 2.0, bad_keyword="ra") with pytest.raises(TypeError): bh.axis.Regular(1, 1.0, 2.0, transform=lambda _: 2) with pytest.raises(TypeError): bh.axis.Regular(1, 1.0, 2.0, transform=bh.axis.transform.Pow) def test_traits(self): STD_TRAITS = {"continuous": True, "ordered": True} ax = bh.axis.Regular(1, 2, 3) assert isinstance(ax, bh.axis.Regular) assert ax.traits == bh.axis.Traits(underflow=True, overflow=True, **STD_TRAITS) ax = bh.axis.Regular(1, 2, 3, overflow=False) assert isinstance(ax, bh.axis.Regular) assert ax.traits == bh.axis.Traits(underflow=True, **STD_TRAITS) ax = bh.axis.Regular(1, 2, 3, underflow=False) assert isinstance(ax, bh.axis.Regular) assert ax.traits == bh.axis.Traits(overflow=True, **STD_TRAITS) ax = bh.axis.Regular(1, 2, 3, underflow=False, overflow=False) assert isinstance(ax, bh.axis.Regular) assert ax.traits == bh.axis.Traits(**STD_TRAITS) ax = bh.axis.Regular(1, 2, 3, growth=True) assert isinstance(ax, bh.axis.Regular) assert ax.traits == bh.axis.Traits( underflow=True, overflow=True, growth=True, **STD_TRAITS ) def test_equal(self): a = bh.axis.Regular(4, 1.0, 2.0) assert a == bh.axis.Regular(4, 1.0, 2.0) assert a != bh.axis.Regular(3, 1.0, 2.0) assert a != bh.axis.Regular(4, 1.1, 2.0) assert a != bh.axis.Regular(4, 1.0, 2.1) assert a != object() assert a != object() # __eq__ and __ne__ are separately implemented # metadata compare assert bh.axis.Regular(1, 2, 3, metadata=1) == bh.axis.Regular( 1, 2, 3, metadata=1 ) assert bh.axis.Regular(1, 2, 3, metadata=1) != bh.axis.Regular( 1, 2, 3, metadata="1" ) assert bh.axis.Regular(1, 2, 3, metadata=1) != bh.axis.Regular( 1, 2, 3, metadata=[1] ) def test_len(self): a = bh.axis.Regular(4, 1.0, 2.0) assert len(a) == 4 assert a.size == 4 assert a.extent == 6 def test_repr(self): ax = bh.axis.Regular(4, 1.1, 2.2) assert repr(ax) == "Regular(4, 1.1, 2.2)" ax = bh.axis.Regular(4, 1.1, 2.2, metadata="ra") assert repr(ax) == "Regular(4, 1.1, 2.2, metadata='ra')" ax = bh.axis.Regular(4, 1.1, 2.2, underflow=False) assert repr(ax) == "Regular(4, 1.1, 2.2, underflow=False)" ax = bh.axis.Regular(4, 1.1, 2.2, metadata="ra", overflow=False) assert repr(ax) == "Regular(4, 1.1, 2.2, overflow=False, metadata='ra')" ax = bh.axis.Regular(4, 1.1, 2.2, metadata="ra", circular=True) assert repr(ax) == "Regular(4, 1.1, 2.2, circular=True, metadata='ra')" ax = bh.axis.Regular(4, 1.1, 2.2, transform=bh.axis.transform.log) assert repr(ax) == "Regular(4, 1.1, 2.2, transform=log)" # TODO: Add caching so that an extracted functional transform actually works ax = bh.axis.Regular(3, 1.1, 2.2, transform=bh.axis.transform.sqrt) assert repr(ax) == "Regular(3, 1.1, 2.2, transform=sqrt)" ax = bh.axis.Regular(4, 1.1, 2.2, transform=bh.axis.transform.Pow(0.5)) assert repr(ax) == "Regular(4, 1.1, 2.2, transform=pow(0.5))" def test_repr_large_bin_count(self): # A large bin count must print as an integer, not "1e+06" ax = bh.axis.Regular(1_000_000, 0, 1) assert repr(ax) == "Regular(1000000, 0, 1)" def test_copy_does_not_alias_metadata(self): a = bh.axis.Regular(3, 0, 1) b = copy.copy(a) b.label = "x" assert a.metadata is None def test_getitem(self): a = bh.axis.Regular(2, 1.0, 2.0) ref = [1.0, 1.5, 2.0] for i in range(2): assert a.bin(i) == approx(ref[i : i + 2]) assert a[i] == approx(ref[i : i + 2]) assert a[-1] == a[1] with pytest.raises(IndexError): a[2] assert a.bin(-1)[0] == -np.inf assert a.bin(2)[1] == np.inf assert a[bh.underflow] == approx(a.bin(-1)) assert a[bh.overflow] == approx(a.bin(2)) with pytest.raises(IndexError): a.bin(-2) with pytest.raises(IndexError): a.bin(3) def test_iter(self): a = bh.axis.Regular(2, 1.0, 2.0) ref = [(1.0, 1.5), (1.5, 2.0)] assert np.asarray(a) == approx(np.array(ref)) def test_index(self): a = bh.axis.Regular(4, 1.0, 2.0) assert a.index(-1) == -1 assert a.index(0.99) == -1 assert a.index(1.0) == 0 assert a.index(1.249) == 0 assert a.index(1.250) == 1 assert a.index(1.499) == 1 assert a.index(1.500) == 2 assert a.index(1.749) == 2 assert a.index(1.750) == 3 assert a.index(1.999) == 3 assert a.index(2.000) == 4 assert a.index(20) == 4 def test_reversed_index(self): a = bh.axis.Regular(4, 2.0, 1.0) assert a.index(-1) == 4 assert a.index(0.99) == 4 assert a.index(1.0) == 4 assert a.index(1.249) == 3 assert a.index(1.250) == 3 assert a.index(1.499) == 2 assert a.index(1.500) == 2 assert a.index(1.749) == 1 assert a.index(1.750) == 1 assert a.index(1.999) == 0 assert a.index(2.000) == 0 assert a.index(20) == -1 def test_sqrt_transform(self): a = bh.axis.Regular(10, 0, 10, transform=bh.axis.transform.sqrt) # Edges: 0. , 0.1, 0.4, 0.9, 1.6, 2.5, 3.6, 4.9, 6.4, 8.1, 10. assert a.index(-100) == 10 # Always in overflow bin assert a.index(-1) == 10 # When transform is invalid assert a.index(0) == 0 assert a.index(0.15) == 1 assert a.index(0.5) == 2 assert a.index(1) == 3 assert a.index(1.7) == 4 assert a.index(9) == 9 assert a.index(11) == 10 assert a.index(1000) == 10 assert a.bin(0)[0] == approx(0) assert a.bin(1)[0] == approx(0.1) assert a.bin(1)[1] == approx(0.4) assert a.bin(2)[0] == approx(0.4) def test_log_transform(self): a = bh.axis.Regular(2, 1e0, 1e2, transform=bh.axis.transform.log) assert a.index(-1) == 2 assert a.index(0.99) == -1 assert a.index(1.001) == 0 assert a.index(9.99) == 0 assert a.index(10.01) == 1 assert a.index(99.9) == 1 assert a.index(100.01) == 2 assert a.index(1000.1) == 2 assert a.bin(0)[0] == approx(1e0) assert a.bin(1)[0] == approx(1e1) assert a.bin(1)[1] == approx(1e2) def test_pow_transform(self): a = bh.axis.Regular(2, 1.0, 9.0, transform=bh.axis.transform.Pow(0.5)) assert a.index(-1) == 2 assert a.index(0.99) == -1 assert a.index(1.0) == 0 assert a.index(3.99) == 0 assert a.index(4.0) == 1 assert a.index(8.99) == 1 assert a.index(9) == 2 assert a.index(1000) == 2 assert a.bin(0)[0] == approx(1.0) assert a.bin(1)[0] == approx(4.0) assert a.bin(1)[1] == approx(9.0) def test_edges_centers_widths(self): a = bh.axis.Regular(2, 0, 1) assert a.edges == approx([0, 0.5, 1]) assert a.centers == approx([0.25, 0.75]) assert a.widths == approx([0.5, 0.5]) class TestCircular(Axis): def test_init(self): # Should not throw bh.axis.Regular(1, 2, 3, circular=True) bh.axis.Regular(1, 2, 3, metadata="pa", circular=True) with pytest.raises(TypeError): bh.axis.Regular(1, 2, 3, "pa", circular=True) with pytest.raises(TypeError): bh.axis.Regular(circular=True) with pytest.raises(TypeError): bh.axis.Regular(1, circular=True) with pytest.raises(TypeError): bh.axis.Regular(1, -1, circular=True) with pytest.raises(TypeError): bh.axis.Regular(-1, circular=True) with pytest.raises(TypeError): bh.axis.Regular(1, 1.0, metadata=1, circular=True) with pytest.raises(TypeError): bh.axis.Regular("1", circular=True) def test_traits(self): ax = bh.axis.Regular(1, 2, 3, circular=True) assert isinstance(ax, bh.axis.Regular) assert ax.traits == bh.axis.Traits( overflow=True, circular=True, continuous=True, ordered=True ) def test_equal(self): a = bh.axis.Regular(4, 0.0, 1.0, circular=True) assert a == bh.axis.Regular(4, 0, 1, circular=True) assert a != bh.axis.Regular(2, 0, 1, circular=True) assert isinstance(a, bh.axis.Regular) def test_len(self): assert len(bh.axis.Regular(4, 0.0, 1.0, circular=True)) == 4 assert bh.axis.Regular(4, 0.0, 1.0, circular=True).size == 4 assert bh.axis.Regular(4, 0.0, 1.0, circular=True).extent == 5 def test_repr(self): ax = bh.axis.Regular(4, 1.1, 2.2, circular=True) assert repr(ax) == "Regular(4, 1.1, 2.2, circular=True)" ax = bh.axis.Regular(4, 1.1, 2.2, metadata="hi", circular=True) assert repr(ax) == "Regular(4, 1.1, 2.2, circular=True, metadata='hi')" def test_getitem(self): a = bh.axis.Regular(2, 1, 1 + np.pi * 2, circular=True) ref = [1.0, 1.0 + np.pi, 1.0 + 2.0 * np.pi] for i in range(2): assert a.bin(i) == approx(ref[i : i + 2]) assert a[i] == approx(ref[i : i + 2]) assert a[-1] == a[1] with pytest.raises(IndexError): a[2] with pytest.raises(IndexError): a[bh.underflow] assert a[bh.overflow] == approx(a.bin(2)) assert a.bin(2)[0] == approx(1 + 2 * np.pi) assert a.bin(2)[1] == approx(1 + 3 * np.pi) with pytest.raises(IndexError): a.bin(-1) # no underflow with pytest.raises(IndexError): a.bin(3) def test_iter(self): a = bh.axis.Regular(2, 1, 2, circular=True) ref = [(1, 1.5), (1.5, 2)] assert np.asarray(a) == approx(np.array(ref)) def test_index(self): a = bh.axis.Regular(4, 1, 1 + np.pi * 2, circular=True) d = 0.5 * np.pi assert a.index(0.99 - 4 * d) == 3 assert a.index(0.99 - 3 * d) == 0 assert a.index(0.99 - 2 * d) == 1 assert a.index(0.99 - d) == 2 assert a.index(0.99) == 3 assert a.index(1.0) == 0 assert a.index(1.01) == 0 assert a.index(0.99 + d) == 0 assert a.index(1.0 + d) == 1 assert a.index(1.0 + 2 * d) == 2 assert a.index(1.0 + 3 * d) == 3 assert a.index(1.0 + 4 * d) == 0 assert a.index(1.0 + 5 * d) == 1 def test_edges_centers_widths(self): a = bh.axis.Regular(2, 0, 1, circular=True) assert a.edges == approx([0, 0.5, 1]) assert a.centers == approx([0.25, 0.75]) assert a.widths == approx([0.5, 0.5]) class TestVariable(Axis): def test_init(self): # should not raise bh.axis.Variable([0, 1]) bh.axis.Variable((0, 1, 2, 3, 4)) bh.axis.Variable([0, 1], metadata="va") with pytest.raises(TypeError): bh.axis.Variable() with pytest.raises(ValueError): bh.axis.Variable([1]) with pytest.raises(TypeError): bh.axis.Variable(1) with pytest.raises(ValueError): bh.axis.Variable([1, -1]) with pytest.raises(ValueError): bh.axis.Variable([1, 1]) with pytest.raises(TypeError): bh.axis.Variable(["1", 2]) with pytest.raises(TypeError): bh.axis.Variable([0.0, 1.0, 2.0], bad_keyword="ra") def test_traits(self): STD_TRAITS = {"continuous": True, "ordered": True} ax = bh.axis.Variable([1, 2, 3]) assert isinstance(ax, bh.axis.Variable) assert ax.traits == bh.axis.Traits(underflow=True, overflow=True, **STD_TRAITS) ax = bh.axis.Variable([1, 2, 3], overflow=False) assert isinstance(ax, bh.axis.Variable) assert ax.traits == bh.axis.Traits(underflow=True, **STD_TRAITS) ax = bh.axis.Variable([1, 2, 3], underflow=False) assert isinstance(ax, bh.axis.Variable) assert ax.traits == bh.axis.Traits(overflow=True, **STD_TRAITS) ax = bh.axis.Variable([1, 2, 3], underflow=False, overflow=False) assert isinstance(ax, bh.axis.Variable) assert ax.traits == bh.axis.Traits(**STD_TRAITS) def test_equal(self): a = bh.axis.Variable([-0.1, 0.2, 0.3]) assert a == bh.axis.Variable((-0.1, 0.2, 0.3)) assert a != bh.axis.Variable([0, 0.2, 0.3]) assert a != bh.axis.Variable([-0.1, 0.1, 0.3]) assert a != bh.axis.Variable([-0.1, 0.1]) def test_len(self): a = bh.axis.Variable([-0.1, 0.2, 0.3]) assert len(a) == 2 assert a.size == 2 assert a.extent == 4 def test_repr(self): a = bh.axis.Variable([-0.1, 0.2]) assert repr(a) == "Variable([-0.1, 0.2])" a = bh.axis.Variable([-0.1, 0.2], metadata="hi") assert repr(a) == "Variable([-0.1, 0.2], metadata='hi')" def test_getitem(self): ref = [-0.1, 0.2, 0.3] a = bh.axis.Variable(ref) for i in range(2): assert a.bin(i) == approx(ref[i : i + 2]) assert a[i] == approx(ref[i : i + 2]) assert a[-1] == a[1] with pytest.raises(IndexError): a[2] assert a[bh.underflow] == approx(a.bin(-1)) assert a[bh.overflow] == approx(a.bin(2)) assert a.bin(-1)[0] == -np.inf assert a.bin(-1)[1] == ref[0] assert a.bin(2)[0] == ref[2] assert a.bin(2)[1] == np.inf with pytest.raises(IndexError): a.bin(-2) with pytest.raises(IndexError): a.bin(3) def test_iter(self): ref = [-0.1, 0.2, 0.3] a = bh.axis.Variable(ref) for i, bin in enumerate(a): assert bin == approx(ref[i : i + 2]) def test_index(self): a = bh.axis.Variable([-0.1, 0.2, 0.3]) assert a.index(-10.0) == -1 assert a.index(-0.11) == -1 assert a.index(-0.1) == 0 assert a.index(0.0) == 0 assert a.index(0.19) == 0 assert a.index(0.2) == 1 assert a.index(0.21) == 1 assert a.index(0.29) == 1 assert a.index(0.3) == 2 assert a.index(0.31) == 2 assert a.index(10) == 2 def test_edges_centers_widths(self): a = bh.axis.Variable([0, 1, 3]) assert a.edges == approx([0, 1, 3]) assert a.centers == approx([0.5, 2]) assert a.widths == approx([1, 2]) class TestInteger: def test_init(self): bh.axis.Integer(-1, 2) bh.axis.Integer(-1, 2, metadata="foo") bh.axis.Integer(-1, 2, underflow=False) bh.axis.Integer(-1, 2, underflow=False, overflow=False) bh.axis.Integer(-1, 2, growth=True) with pytest.raises(TypeError): bh.axis.Integer() with pytest.raises(TypeError): bh.axis.Integer(1) with pytest.raises(TypeError): bh.axis.Integer("1", 2) with pytest.raises(ValueError): bh.axis.Integer(2, -1) with pytest.raises(TypeError): bh.axis.Integer(-1, 2, "foo") with pytest.raises(TypeError): bh.axis.Integer(20, 30, 40) def test_traits(self): STD_TRAITS = {"ordered": True} ax = bh.axis.Integer(1, 3) assert isinstance(ax, bh.axis.Integer) assert ax.traits == bh.axis.Traits(underflow=True, overflow=True, **STD_TRAITS) # See https://github.com/boostorg/histogram/issues/305 ax = bh.axis.Integer(1, 3, overflow=False) assert isinstance(ax, bh.axis.Integer) assert ax.traits == bh.axis.Traits(underflow=True, **STD_TRAITS) # See https://github.com/boostorg/histogram/issues/305 ax = bh.axis.Integer(1, 3, underflow=False) assert isinstance(ax, bh.axis.Integer) assert ax.traits == bh.axis.Traits(overflow=True, **STD_TRAITS) ax = bh.axis.Integer(1, 3, underflow=False, overflow=False) assert isinstance(ax, bh.axis.Integer) assert ax.traits == bh.axis.Traits(**STD_TRAITS) ax = bh.axis.Integer(1, 3, growth=True) assert isinstance(ax, bh.axis.Integer) assert ax.traits == bh.axis.Traits(growth=True, **STD_TRAITS) def test_equal(self): assert bh.axis.Integer(-1, 2) == bh.axis.Integer(-1, 2) assert bh.axis.Integer(-1, 2) != bh.axis.Integer(-1, 2, metadata="Other") assert bh.axis.Integer(-1, 2, underflow=True) != bh.axis.Integer( -1, 2, underflow=False ) def test_len(self, underflow, overflow): a = bh.axis.Integer(-1, 3, underflow=underflow, overflow=overflow) assert len(a) == 4 assert a.size == 4 assert a.extent == 4 + underflow + overflow def test_repr(self): a = bh.axis.Integer(-1, 1) assert repr(a) == "Integer(-1, 1)" a = bh.axis.Integer(-1, 1, metadata="hi") assert repr(a) == "Integer(-1, 1, metadata='hi')" a = bh.axis.Integer(-1, 1, metadata=2) assert repr(a) == "Integer(-1, 1, metadata=...)" a = bh.axis.Integer(-1, 1, underflow=False) assert repr(a) == "Integer(-1, 1, underflow=False)" a = bh.axis.Integer(-1, 1, overflow=False) assert repr(a) == "Integer(-1, 1, overflow=False)" a = bh.axis.Integer(-1, 1, growth=True) assert repr(a) == "Integer(-1, 1, growth=True)" def test_label(self): a = bh.axis.Integer(-1, 2, metadata="foo") assert a.metadata == "foo" a.metadata = "bar" assert a.metadata == "bar" def test_getitem(self): a = bh.axis.Integer(-1, 3) ref = [-1, 0, 1, 2] for i, r in enumerate(ref): assert a.bin(i) == r assert a[i] == r assert a.bin(-1) == -2 assert a.bin(4) == 3 assert a[bh.underflow] == approx(a.bin(-1)) assert a[bh.overflow] == approx(a.bin(4)) def test_iter(self): a = bh.axis.Integer(-1, 3) ref = (-1, 0, 1, 2) assert np.asarray(a) == approx(ref) def test_index(self): a = bh.axis.Integer(-1, 3) assert a.index(-3) == -1 assert a.index(-2) == -1 assert a.index(-1) == 0 assert a.index(0) == 1 assert a.index(1) == 2 assert a.index(2) == 3 assert a.index(3) == 4 assert a.index(4) == 4 def test_edges_centers_widths(self): a = bh.axis.Integer(1, 3) assert a.edges == approx([1, 2, 3]) assert a.centers == approx([1.5, 2.5]) assert a.widths == approx([1, 1]) class TestCategory(Axis): def test_init(self): # should not raise bh.axis.IntCategory([1, 2]) bh.axis.IntCategory({1, 2}) bh.axis.IntCategory((1, 2), metadata="foo") bh.axis.StrCategory(["A", "B"]) bh.axis.StrCategory({"A", "B"}) bh.axis.StrCategory("AB") bh.axis.StrCategory("AB", metadata="foo") with pytest.raises(TypeError): bh.axis.IntCategory(["A", "B"]) with pytest.raises(TypeError): bh.axis.StrCategory([1, 2]) with pytest.raises(TypeError): bh.axis.IntCategory([1, 2], "foo") with pytest.raises(TypeError): bh.axis.StrCategory("AB", "foo") with pytest.raises(TypeError): bh.axis.StrCategory() with pytest.raises(TypeError): bh.axis.IntCategory() with pytest.raises(TypeError): bh.axis.StrCategory([1, "2"]) with pytest.raises(TypeError): bh.axis.IntCategory([1, "2"]) with pytest.raises(TypeError): bh.axis.IntCategory([1, 2, 3], underflow=True) def test_traits(self): ax = bh.axis.IntCategory([1, 2, 3]) assert isinstance(ax, bh.axis.IntCategory) assert ax.traits == bh.axis.Traits(overflow=True) ax = bh.axis.IntCategory([1, 2, 3], growth=True) assert isinstance(ax, bh.axis.IntCategory) assert ax.traits == bh.axis.Traits(growth=True) ax = bh.axis.StrCategory(["1", "2", "3"]) assert isinstance(ax, bh.axis.StrCategory) assert ax.traits == bh.axis.Traits(overflow=True) ax = bh.axis.StrCategory(["1", "2", "3"], growth=True) assert isinstance(ax, bh.axis.StrCategory) assert ax.traits == bh.axis.Traits(growth=True) def test_equal(self): assert bh.axis.IntCategory([1, 2, 3]) == bh.axis.IntCategory([1, 2, 3]) assert bh.axis.IntCategory([1, 2, 3]) != bh.axis.IntCategory([1, 3, 2]) assert bh.axis.StrCategory(["A", "B"]) == bh.axis.StrCategory("AB") assert bh.axis.StrCategory(["A", "B"]) != bh.axis.StrCategory("BA") @pytest.mark.parametrize("ref", [[1, 2, 3], "ABC"]) def test_len(self, ref, growth): Cat = bh.axis.StrCategory if isinstance(ref[0], str) else bh.axis.IntCategory a = Cat(ref, growth=growth) assert len(a) == 3 assert a.size == 3 assert a.extent == 3 if growth else 4 def test_repr(self): ax = bh.axis.IntCategory([1, 2, 3]) assert repr(ax) == "IntCategory([1, 2, 3])" ax = bh.axis.IntCategory([1, 2, 3], metadata="foo") assert repr(ax) == "IntCategory([1, 2, 3], metadata='foo')" ax = bh.axis.StrCategory("ABC", metadata="foo") assert repr(ax) == "StrCategory(['A', 'B', 'C'], metadata='foo')" @pytest.mark.parametrize("ref", [[1, 2, 3], "ABC"]) def test_getitem(self, ref, growth): Cat = bh.axis.StrCategory if isinstance(ref[0], str) else bh.axis.IntCategory a = Cat(ref, growth=growth) for i in range(3): assert a.bin(i) == ref[i] assert a[i] == ref[i] assert a[-1] == a[2] with pytest.raises(IndexError): a[3] with pytest.raises(IndexError): a.bin(-1) if growth: with pytest.raises(IndexError): a.bin(3) else: assert a.bin(3) is None @pytest.mark.parametrize("ref", [[1, 2, 3], ("A", "B", "C")]) def test_iter(self, ref, growth): Cat = bh.axis.StrCategory if isinstance(ref[0], str) else bh.axis.IntCategory a = Cat(ref, growth=growth) assert np.asarray(a) == approx(ref) @pytest.mark.parametrize( "ref", [[1, 2, 3, 4], ("A", "B", "C", "D")], ids=("int", "str") ) def test_index(self, ref, growth): Cat = bh.axis.StrCategory if isinstance(ref[0], str) else bh.axis.IntCategory a = Cat(ref, growth=growth) for i, r in enumerate(ref): assert a.index(r) == i assert a.index(ref) == approx([0, 1, 2, 3]) assert a.index(np.reshape(ref, (2, 2))) == approx(np.array([[0, 1], [2, 3]])) if isinstance(ref[0], str): with pytest.raises(KeyError): a.index("E") else: with pytest.raises(KeyError): a.index(5) with pytest.raises(KeyError): a.index(-2) @pytest.mark.parametrize("ref", [[1, 2, 3], ("A", "B", "C")]) def test_value(self, ref, growth): Cat = bh.axis.StrCategory if isinstance(ref[0], str) else bh.axis.IntCategory a = Cat(ref, growth=growth) for i, r in enumerate(ref): assert a.value(i) == r assert a.value(range(3)) == approx(ref) assert a.value(3) is None assert a.value((0, 3)) == approx([ref[0], None]) assert a.value(np.array((0, 1, 2, 3))) == approx([ref[0], ref[1], ref[2], None]) # may be added in the future with pytest.raises(ValueError): a.value([[2], [2]]) @pytest.mark.parametrize("ref", [[1, 2, 3], "ABC"]) def test_edges_centers_widths(self, ref, growth): Cat = bh.axis.StrCategory if isinstance(ref[0], str) else bh.axis.IntCategory a = Cat(ref, growth=growth) assert a.edges == approx([0, 1, 2, 3]) assert a.centers == approx([0.5, 1.5, 2.5]) assert a.widths == approx([1, 1, 1]) def test_non_ascii(self): # non-ASCII strings must round-trip through construction, indexing, # and fill by list/ndarray, not just scalar fill labels = ["é", "日本", "a"] a = bh.axis.StrCategory(labels) assert list(a) == labels assert a.index("é") == 0 assert a.index("日本") == 1 h = bh.Histogram(a) h.fill(["é", "日本", "a", "é"]) assert h.view() == approx([2, 1, 1]) h2 = bh.Histogram(bh.axis.StrCategory(labels)) h2.fill(np.array(["é", "日本", "a", "é"])) assert h2.view() == approx([2, 1, 1]) h3 = bh.Histogram(bh.axis.StrCategory(labels)) for label in labels: h3.fill(label) assert h3.view() == approx([1, 1, 1]) class TestBoolean(Axis): def test_init(self): bh.axis.Boolean() bh.axis.Boolean(metadata="foo") with pytest.raises(TypeError): bh.axis.Boolean(1) def test_traits(self): ax = bh.axis.Boolean() assert isinstance(ax, bh.axis.Boolean) assert ax.traits == bh.axis.Traits(ordered=True) def test_equal(self): assert bh.axis.Boolean() == bh.axis.Boolean() assert bh.axis.Boolean(metadata="hi") == bh.axis.Boolean(metadata="hi") assert bh.axis.Boolean(metadata="hi") != bh.axis.Boolean() assert bh.axis.Boolean(metadata="hi") != bh.axis.Boolean(metadata="ho") def test_len(self): a = bh.axis.Boolean() assert len(a) == 2 assert a.size == 2 assert a.extent == 2 def test_repr(self): a = bh.axis.Boolean() assert repr(a) == "Boolean()" a = bh.axis.Boolean(metadata="hi") assert repr(a) == "Boolean(metadata='hi')" def test_label(self): a = bh.axis.Boolean(metadata="foo") assert a.metadata == "foo" a.metadata = "bar" assert a.metadata == "bar" def test_getitem(self): a = bh.axis.Boolean() ref = [False, True] for i, r in enumerate(ref): assert a.bin(i) == r assert a[i] == r assert a.bin(0) == 0 assert a.bin(1) == 1 def test_iter(self): a = bh.axis.Boolean() ref = (False, True) # approx() does not support booleans; compare exactly assert np.asarray(a).tolist() == list(ref) def test_index(self): a = bh.axis.Boolean() assert a.index(False) == 0 assert a.index(True) == 1 def test_edges_centers_widths(self): a = bh.axis.Boolean() assert a.edges == approx([0.0, 1.0, 2.0]) assert a.centers == approx([0.5, 1.5]) assert a.widths == approx([1, 1]) def test_value(self): a = bh.axis.Boolean() assert a.value(0) == 0 assert a.value(1) == 1 assert a.value([0, 1]) == approx([0, 1]) def test_metadata_compare_no_bool(): # Metadata that does not give a plain bool from == must not abort array = np.arange(3) assert bh.axis.Regular(3, 0, 1, metadata=array) == bh.axis.Regular( 3, 0, 1, metadata=array ) assert bh.axis.Regular(3, 0, 1, metadata=array) != bh.axis.Regular( 3, 0, 1, metadata=np.arange(3) ) assert not bh.axis.Regular(3, 0, 1, metadata=np.arange(3)) == bh.axis.Regular( # noqa: SIM201 3, 0, 1, metadata=np.arange(3) ) # A copy keeps the same metadata object, so it stays equal ax = bh.axis.Regular(3, 0, 1, metadata=array) assert copy.copy(ax) == ax def test_metadata_compare_raises(): class Bad: def __eq__(self, other): raise RuntimeError("no compare") __hash__ = None bad = Bad() assert bh.axis.Regular(3, 0, 1, metadata=bad) == bh.axis.Regular( 3, 0, 1, metadata=bad ) assert bh.axis.Regular(3, 0, 1, metadata=bad) != bh.axis.Regular( 3, 0, 1, metadata=Bad() ) # Issue #1143 regression tests def test_getitem_negative_wraparound(): # Issue #1143 (B8b): ax[-size-1] used to wrap into the underflow bin ax = bh.axis.Regular(10, 0, 1) assert ax[-10] == ax[0] assert ax[-1] == ax[9] with pytest.raises(IndexError, match="-11"): ax[-11] with pytest.raises(IndexError, match="10"): ax[10] def test_index_empty_sequence(): # Issue #1143 (B9): empty sequences are not strings assert bh.axis.Regular(10, 0, 1).index([]).size == 0 assert bh.axis.Regular(10, 0, 1).index(np.array([])).size == 0 assert bh.axis.Integer(0, 5).index(()).size == 0 # Still rejects actual strings with pytest.raises(TypeError): bh.axis.Regular(10, 0, 1).index("hi") with pytest.raises(TypeError): bh.axis.Regular(10, 0, 1).index(["hi", "ho"]) # StrCategory still accepts empty and string input ax = bh.axis.StrCategory(["a", "b"]) assert ax.index([]).size == 0 assert ax.index(np.array([])).size == 0 assert ax.index("a") == 0 assert ax.index(["b", "a"]) == approx([1, 0]) with pytest.raises(TypeError): ax.index([1, 2]) def test_transform_validation(): # Issue #1143 (B14c): clear TypeErrors for invalid transform arguments with pytest.raises(TypeError, match=r"use Pow\(\)"): bh.axis.Regular(10, 1, 100, transform=bh.axis.transform.Pow) with pytest.raises(TypeError, match="AxisTransform"): bh.axis.Regular(10, 1, 100, transform="log") with pytest.raises(TypeError, match="AxisTransform"): bh.axis.Regular(10, 1, 100, transform=42) class TestNonContiguousIndex: """Regression tests for gh-1143: category axis index() must honor the input array's memory layout (Fortran order, slices, reversed views).""" def test_intcategory_fortran_order(self): ax = bh.axis.IntCategory([1, 2, 3]) arr = np.asfortranarray([[1, 2], [3, 1]]) assert ax.index(arr) == approx(np.array([[0, 1], [2, 0]])) def test_intcategory_negative_stride(self): ax = bh.axis.IntCategory([1, 2, 3]) arr = np.array([1, 2, 3, 1])[::-1] assert ax.index(arr) == approx([0, 2, 1, 0]) def test_intcategory_strided(self): ax = bh.axis.IntCategory([1, 2, 3]) arr = np.array([1, 2, 3, 1])[::2] assert ax.index(arr) == approx([0, 2]) def test_strcategory_strided(self): ax = bh.axis.StrCategory(["a", "b", "c"]) arr = np.array(["a", "b", "c", "a"])[::2] assert ax.index(arr) == approx([0, 2]) def test_strcategory_negative_stride(self): ax = bh.axis.StrCategory(["a", "b", "c"]) arr = np.array(["a", "b", "c"])[::-1] assert ax.index(arr) == approx([2, 1, 0]) def test_strcategory_bytes_strided(self): ax = bh.axis.StrCategory(["a", "b", "c"]) arr = np.array([b"a", b"b", b"c", b"a"])[::2] assert ax.index(arr) == approx([0, 2]) def test_to_numpy_upper_edge_nudge_direction(): # Regression test for gh-1143: the last edge returned by to_numpy() is # nudged one ulp *below* the true edge, which must also hold for # negative and zero upper edges. for start, stop in [(-2, -1), (-1, 0), (1, 2)]: ax = bh.axis.Regular(4, start, stop) h = bh.Histogram(ax) edges = h.to_numpy()[1] assert edges[-1] == np.nextafter(ax.edges[-1], -np.inf) assert edges[-1] < stop scikit-hep-boost-histogram-b4c49b0/tests/test_axis_options.py000066400000000000000000000016371524660700500246100ustar00rootroot00000000000000from __future__ import annotations from boost_histogram.axis import Traits def test_compare(): assert Traits() == Traits() assert Traits(underflow=True) == Traits(underflow=True) assert Traits(overflow=True) == Traits(overflow=True) assert Traits(circular=True) == Traits(circular=True) assert Traits(growth=True) == Traits(growth=True) assert Traits(underflow=True, overflow=True) == Traits( underflow=True, overflow=True ) assert Traits() != Traits(overflow=True) assert Traits(underflow=True) != Traits(overflow=True) assert Traits(underflow=True, overflow=True) != Traits(overflow=True) def test_fields(): o = Traits() assert not o.underflow assert not o.overflow assert not o.circular assert not o.growth o = Traits(underflow=True, overflow=True) assert o.underflow assert o.overflow assert not o.circular assert not o.growth scikit-hep-boost-histogram-b4c49b0/tests/test_benchmark_1d.py000066400000000000000000000030221524660700500243750ustar00rootroot00000000000000from __future__ import annotations import platform import numpy as np import pytest from pytest import approx import boost_histogram as bh STORAGES = (bh.storage.Int64, bh.storage.Double, bh.storage.Unlimited) DTYPES = (np.float64, np.float32, np.int64, np.int32) # Casting is broken for numpy on ppc64le: https://github.com/numpy/numpy/issues/21062 if platform.machine() == "ppc64le": DTYPES = (np.float64, np.int64) bins = 100 ranges = (-1, 1) bins = np.asarray(bins).astype(np.int64) ranges = np.asarray(ranges).astype(np.float64) edges = np.linspace(ranges[0], ranges[1], bins + 1) np.random.seed(42) vals_core = np.random.normal(size=[100000]) vals = {t: vals_core.astype(t) for t in DTYPES} answer = {t: np.histogram(vals[t], bins=bins, range=ranges)[0] for t in DTYPES} @pytest.mark.benchmark(group="1d-fills") @pytest.mark.parametrize("dtype", vals) def test_numpy_1d(benchmark, dtype): result, _ = benchmark(np.histogram, vals[dtype], bins=bins, range=ranges) assert result == approx(answer[dtype]) def make_and_run_hist(flow, storage, vals): histo = bh.Histogram( bh.axis.Regular(bins, *ranges, underflow=flow, overflow=flow), storage=storage() ) histo.fill(vals) return histo.view() @pytest.mark.benchmark(group="1d-fills") @pytest.mark.parametrize("dtype", vals) @pytest.mark.parametrize("storage", STORAGES) def test_boost_1d(benchmark, flow, storage, dtype): result = benchmark(make_and_run_hist, flow, storage, vals[dtype]) assert result[:-1] == approx(answer[dtype][:-1], abs=2) scikit-hep-boost-histogram-b4c49b0/tests/test_benchmark_2d.py000066400000000000000000000033331524660700500244030ustar00rootroot00000000000000from __future__ import annotations import platform import numpy as np import pytest from pytest import approx import boost_histogram as bh STORAGES = (bh.storage.Int64, bh.storage.Double, bh.storage.Unlimited) DTYPES = (np.float64, np.float32, np.int64, np.int32) # Casting is broken for numpy on ppc64le: https://github.com/numpy/numpy/issues/21062 if platform.machine() == "ppc64le": DTYPES = (np.float64, np.int64) bins = (100, 100) ranges = ((-1, 1), (-1, 1)) bins = np.asarray(bins).astype(np.int64) ranges = np.asarray(ranges).astype(np.float64) edges = ( np.linspace(ranges[0, 0], ranges[0, 1], bins[0] + 1), np.linspace(ranges[1, 0], ranges[1, 1], bins[1] + 1), ) np.random.seed(42) vals_core = np.random.normal(size=[2, 100000]) vals = {t: vals_core.astype(t) for t in DTYPES} answer = {t: np.histogram2d(*vals[t], bins=bins, range=ranges)[0] for t in DTYPES} @pytest.mark.benchmark(group="2d-fills") @pytest.mark.parametrize("dtype", vals) def test_numpy_perf_2d(benchmark, dtype): result, _, _ = benchmark(np.histogram2d, *vals[dtype], bins=bins, range=ranges) assert result == approx(answer[dtype]) def make_and_run_hist(flow, storage, vals): histo = bh.Histogram( bh.axis.Regular(bins[0], *ranges[0], underflow=flow, overflow=flow), bh.axis.Regular(bins[1], *ranges[1], underflow=flow, overflow=flow), storage=storage(), ) histo.fill(*vals) return histo.view() @pytest.mark.benchmark(group="2d-fills") @pytest.mark.parametrize("dtype", vals) @pytest.mark.parametrize("storage", STORAGES) def test_2d(benchmark, flow, storage, dtype): result = benchmark(make_and_run_hist, flow, storage, vals[dtype]) assert result[:-1, :-1] == approx(answer[dtype][:-1, :-1]) scikit-hep-boost-histogram-b4c49b0/tests/test_benchmark_category_axis.py000066400000000000000000000036751524660700500267500ustar00rootroot00000000000000from __future__ import annotations import numpy as np import pytest import boost_histogram as bh @pytest.mark.benchmark(group="IntCategory") @pytest.mark.parametrize("dtype", ["i", tuple]) def test_IntCategory(benchmark, growth, dtype): np.random.seed(42) values = np.random.choice(np.arange(5), size=[100000]) h = bh.Histogram( bh.axis.IntCategory([] if growth else np.arange(4), growth=growth), storage=bh.storage.Double(), ) def run(h, data): h.fill(data) benchmark(run, h, tuple(values) if dtype is tuple else values.astype(dtype)) @pytest.mark.benchmark(group="StrCategory") @pytest.mark.parametrize("dtype", ["S", "U", "O", tuple]) def test_StrCategory(benchmark, growth, dtype): np.random.seed(42) values = np.random.choice(["A", "B", "C", "D", "E"], size=[100000]) h = bh.Histogram( bh.axis.StrCategory([] if growth else ["A", "B", "C", "D"], growth=growth), storage=bh.storage.Double(), ) def run(h, data): h.fill(data) benchmark(run, h, tuple(values) if dtype is tuple else values.astype(dtype)) @pytest.mark.benchmark(group="Pick") def test_pick_only(benchmark): h = bh.Histogram( bh.axis.StrCategory([str(i) for i in range(32)]), bh.axis.StrCategory([str(i) for i in range(32)]), bh.axis.StrCategory([str(i) for i in range(32)]), bh.axis.Regular(32, 0, 320), ) h[...] = 1.0 def run(h): return h[bh.loc("13"), bh.loc("13"), bh.loc("13"), :].view() benchmark(run, h) @pytest.mark.benchmark(group="Pick") def test_pick_and_slice(benchmark): h = bh.Histogram( bh.axis.StrCategory([str(i) for i in range(32)]), bh.axis.StrCategory([str(i) for i in range(32)]), bh.axis.StrCategory([str(i) for i in range(32)]), bh.axis.Regular(32, 0, 320), ) h[...] = 1.0 def run(h): return h[3:29, bh.loc("13"), bh.loc("13"), :].view() benchmark(run, h) scikit-hep-boost-histogram-b4c49b0/tests/test_histogram.py000066400000000000000000001663441524660700500240750ustar00rootroot00000000000000from __future__ import annotations import copy import functools import operator import pickle import platform import subprocess import sys import threading from collections import OrderedDict from io import BytesIO import numpy as np import pytest from pytest import approx import boost_histogram as bh def test_init(): bh.Histogram() bh.Histogram(bh.axis.Integer(-1, 1)) with pytest.raises(TypeError): bh.Histogram(1) with pytest.raises(TypeError): bh.Histogram("bla") with pytest.raises(TypeError): bh.Histogram([]) with pytest.raises(TypeError): bh.Histogram(bh.axis.Regular) with pytest.raises(TypeError): bh.Histogram(bh.axis.Regular()) with pytest.raises(TypeError): bh.Histogram([bh.axis.Integer(-1, 1)]) with pytest.raises(TypeError): bh.Histogram([bh.axis.Integer(-1, 1), bh.axis.Integer(-1, 1)]) with pytest.raises(TypeError): bh.Histogram(bh.axis.Integer(-1, 1), unknown_keyword="nh") h = bh.Histogram(bh.axis.Integer(-1, 2)) assert h.ndim == 1 assert h.axes[0] == bh.axis.Integer(-1, 2) assert h.axes[0].extent == 5 assert h.axes[0].size == 3 assert h != bh.Histogram(bh.axis.Regular(1, -1, 1)) assert h != bh.Histogram(bh.axis.Integer(-1, 1, metadata="ia")) def test_copy(): a = bh.Histogram(bh.axis.Integer(-1, 1)) b = copy.copy(a) assert a == b assert id(a) != id(b) c = copy.deepcopy(b) assert b == c assert id(b) != id(c) b = a.copy(deep=False) assert a == b assert id(a) != id(b) c = a.copy() assert b == c assert id(b) != id(c) def test_fill_int_1d(): h = bh.Histogram(bh.axis.Integer(-1, 2)) assert isinstance(h, bh.Histogram) assert h.empty() assert h.empty(flow=True) with pytest.raises(ValueError): h.fill() with pytest.raises(ValueError): h.fill(1, 2) with pytest.raises(TypeError): h.fill(1, fiddlesticks=2) h.fill(-3) assert h.empty() assert not h.empty(flow=True) h.reset() for x in (-10, -1, -1, 0, 1, 1, 1, 10): h.fill(x) assert h.sum() == 6 assert not h.empty() assert not h.empty(flow=True) assert h.sum(flow=True) == 8 assert h.axes[0].extent == 5 with pytest.raises(IndexError): h[0, 1] for get in (lambda h, arg: h[arg], lambda h, arg: h[arg]): # lambda h, arg: h[arg]): assert get(h, 0) == 2 assert get(h, 1) == 1 assert get(h, 2) == 3 # assert get(h, 0).variance == 2 # assert get(h, 1).variance == 1 # assert get(h, 2).variance == 3 assert h[bh.overflow - 1] == 3 assert h[bh.overflow] == 1 assert h[bh.underflow] == 1 assert h[bh.underflow + 1] == 2 assert h[-1] == 3 assert h[-3] == h[0] with pytest.raises(IndexError): h[3] with pytest.raises(IndexError): h[-4] def test_fill_int_with_float_single_1d(): h = bh.Histogram(bh.axis.Integer(-1, 2)) with pytest.raises(TypeError): h.fill(0.3) def test_fill_int_with_float_array_1d(): h = bh.Histogram(bh.axis.Integer(-1, 2)) with pytest.raises(TypeError): h.fill([-0.3, 0.3]) def test_fill_1d(flow): h = bh.Histogram(bh.axis.Regular(3, -1, 2, underflow=flow, overflow=flow)) with pytest.raises(ValueError): h.fill() with pytest.raises(ValueError): h.fill(1, 2) for x in (-10, -1, -1, 0, 1, 1, 1, 10): h.fill(x) assert h.sum() == 6 assert h.sum(flow=True) == 6 + 2 * flow assert h.axes[0].extent == 3 + 2 * flow with pytest.raises(IndexError): h[0, 1] for get in (lambda h, arg: h[arg],): # lambda h, arg: h[arg]): assert get(h, 0) == 2 assert get(h, 1) == 1 assert get(h, 2) == 3 # assert get(h, 0).variance == 2 # assert get(h, 1).variance == 1 # assert get(h, 2).variance == 3 if flow is True: assert get(h, bh.underflow) == 1 assert get(h, bh.overflow) == 1 def test_setting(count_single_storage): h = bh.Histogram(bh.axis.Regular(10, 0, 1), storage=count_single_storage()) h[bh.underflow] = 1 h[0] = 2 h[1] = 3 h[bh.loc(0.55)] = 4 h[-1] = 5 h[bh.overflow] = 6 assert h[bh.underflow] == 1 assert h[0] == 2 assert h[1] == 3 assert h[bh.loc(0.55)] == 4 assert h[5] == 4 assert h[-1] == 5 assert h[9] == 5 assert h[bh.overflow] == 6 assert h.view(flow=True) == approx([1, 2, 3, 0, 0, 0, 4, 0, 0, 0, 5, 6]) def test_growth(): h = bh.Histogram(bh.axis.Integer(-1, 2)) h.fill(-1) h.fill(1) h.fill(1) for _ in range(255): h.fill(0) h.fill(0) for _ in range(1000 - 256): h.fill(0) assert h[bh.underflow] == 0 assert h[0] == 1 assert h[1] == 1000 assert h[2] == 2 assert h[bh.overflow] == 0 def test_growing_cats(): h = bh.Histogram( bh.axis.IntCategory([], growth=True), bh.axis.StrCategory([], growth=True) ) h.fill([1, 2, 1, 1], ["hi", "ho", "hi", "ho"]) assert h.size == 4 def test_noflow_cats(): h = bh.Histogram( bh.axis.IntCategory([1, 2, 3], overflow=False), bh.axis.StrCategory(["hi"], overflow=False), ) h.fill([1, 2, 3, 4], ["hi", "ho", "hi", "ho"]) assert h.sum() == 2 def _has_issue_960_bug(): # Probe for the upstream Boost.Histogram bug where a broadcast scalar argument # invalidates an entire bulk fill when an earlier non-inclusive axis dropped the # first array entry. Returns True while the (vendored) bug is present. h = bh.Histogram(bh.axis.IntCategory([1], overflow=False), bh.axis.IntCategory([1])) h.fill([0, 1], 1) # first entry (0) is out of range on the non-inclusive axis return h.sum() == 0 @pytest.mark.xfail( _has_issue_960_bug(), reason="upstream Boost.Histogram bug, fixed upstream; pending boost bump (#960)", strict=True, ) def test_noflow_cat_scalar_broadcast(): # https://github.com/scikit-hep/boost-histogram/issues/960 # A broadcast scalar on a later axis must not zero the whole fill when an earlier # non-inclusive axis drops the first array entry. h = bh.Histogram( bh.axis.IntCategory([1, 2, 3], overflow=False), bh.axis.StrCategory(["nominal"]), storage=bh.storage.Weight(), ) values = np.array([-1, 1, 2, 3, 1]) # first entry (-1) is out of range h.fill(values, "nominal", weight=np.ones_like(values, dtype=float)) # only the out-of-range -1 is dropped; everything else is kept assert h.sum().value == 4 assert h[bh.loc(1), bh.loc("nominal")].value == 2 assert h[bh.loc(2), bh.loc("nominal")].value == 1 assert h[bh.loc(3), bh.loc("nominal")].value == 1 def test_metadata_add(): h1 = bh.Histogram( bh.axis.IntCategory([1, 2, 3]), bh.axis.StrCategory(["1", "2", "3"]) ) h2 = bh.Histogram( bh.axis.IntCategory([1, 2, 3]), bh.axis.StrCategory(["1", "2", "3"]) ) h1.fill([1, 2, 1, 2], ["1", "1", "2", "2"]) h2.fill([2, 3, 2, 3], ["2", "2", "3", "3"]) h3 = h1 + h2 assert h1.axes[0].size == 3 assert h2.axes[0].size == 3 assert h3.axes[0].size == 3 assert h1.axes[1].size == 3 assert h2.axes[1].size == 3 assert h3.axes[1].size == 3 assert h3[bh.loc(2), bh.loc("2")] == 2.0 def test_grow_and_add(): h1 = bh.Histogram( bh.axis.IntCategory([], growth=True), bh.axis.StrCategory([], growth=True) ) h2 = bh.Histogram( bh.axis.IntCategory([], growth=True), bh.axis.StrCategory([], growth=True) ) h1.fill([1, 2, 1, 2], ["hi", "hi", "ho", "ho"]) h2.fill([2, 3, 4, 5], ["ho", "ho", "hum", "hum"]) h3 = h1 + h2 assert h1.axes[0].size == 2 assert h2.axes[0].size == 4 assert h3.axes[0].size == 5 assert h1.axes[1].size == 2 assert h2.axes[1].size == 2 assert h3.axes[1].size == 3 assert h3[bh.loc(2), bh.loc("ho")] == 2.0 def test_fill_2d(flow): h = bh.Histogram( bh.axis.Integer(-1, 2, underflow=flow, overflow=flow), bh.axis.Regular(4, -2, 2, underflow=flow, overflow=flow), ) h.fill(-1, -2) h.fill(-1, -1) h.fill(0, 0) h.fill(0, 1) h.fill(1, 0) h.fill(3, -1) h.fill(0, -3) # TODO: this really should be a TypeError, but we are throwing ValueError with pytest.raises(ValueError): h.fill(1) with pytest.raises(ValueError): h.fill(1, 2, 3) m = [ [1, 1, 0, 0, 0, 0], [0, 0, 1, 1, 0, 1], [0, 0, 1, 0, 0, 0], [0, 1, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], ] for get in (lambda h, x, y: h[bh.tag.at(x), bh.tag.at(y)],): # lambda h, x, y: h[x, y]): for i in range(-flow, h.axes[0].size + flow): for j in range(-flow, h.axes[1].size + flow): assert get(h, i, j) == m[i][j] h.fill(1, [1, 2]) h.fill(np.float64(1), [1, 2]) def test_add_2d(flow): h0 = bh.Histogram( bh.axis.Integer(-1, 2, underflow=flow, overflow=flow), bh.axis.Regular(4, -2, 2, underflow=flow, overflow=flow), ) assert isinstance(h0, bh.Histogram) h0.fill(-1, -2) h0.fill(-1, -1) h0.fill(0, 0) h0.fill(0, 1) h0.fill(1, 0) h0.fill(3, -1) h0.fill(0, -3) m = [ [1, 1, 0, 0, 0, 0], [0, 0, 1, 1, 0, 1], [0, 0, 1, 0, 0, 0], [0, 1, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], ] h = h0.copy() h += h for i in range(-flow, h.axes[0].size + flow): for j in range(-flow, h.axes[1].size + flow): assert h[bh.tag.at(i), bh.tag.at(j)] == 2 * m[i][j] h = sum([h0, h0]) for i in range(-flow, h.axes[0].size + flow): for j in range(-flow, h.axes[1].size + flow): assert h[bh.tag.at(i), bh.tag.at(j)] == 2 * m[i][j] h = 0 + h0 + h0 for i in range(-flow, h.axes[0].size + flow): for j in range(-flow, h.axes[1].size + flow): assert h[bh.tag.at(i), bh.tag.at(j)] == 2 * m[i][j] @pytest.mark.parametrize("flow", [True, False]) def test_add_2d_fancy(flow): h = bh.Histogram( bh.axis.Integer(-1, 2, underflow=flow, overflow=flow), bh.axis.Regular(4, -2, 2, underflow=flow, overflow=flow), ) assert isinstance(h, bh.Histogram) h.fill(-1, -2) h.fill(-1, -1) h.fill(0, 0) h.fill(0, 1) h.fill(1, 0) h.fill(3, -1) h.fill(0, -3) m = [ [1, 1, 0, 0, 0, 0], [0, 0, 1, 1, 0, 1], [0, 0, 1, 0, 0, 0], [0, 1, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], ] h += h for i in range(-flow, h.axes[0].size + flow): for j in range(-flow, h.axes[1].size + flow): assert h[bh.tag.at(i), bh.tag.at(j)] == 2 * m[i][j] def test_add_2d_bad(): a = bh.Histogram(bh.axis.Integer(-1, 1)) b = bh.Histogram(bh.axis.Regular(3, -1, 1)) with pytest.raises(ValueError): a += b def test_add_2d_w(flow): h = bh.Histogram( bh.axis.Integer(-1, 2, underflow=flow, overflow=flow), bh.axis.Regular(4, -2, 2, underflow=flow, overflow=flow), ) h.fill(-1, -2) h.fill(-1, -1) h.fill(0, 0) h.fill(0, 1) h.fill(1, 0) h.fill(3, -1) h.fill(0, -3) m = [ [1, 1, 0, 0, 0, 0], [0, 0, 1, 1, 0, 1], [0, 0, 1, 0, 0, 0], [0, 1, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], ] h2 = bh.Histogram( bh.axis.Integer(-1, 2, underflow=flow, overflow=flow), bh.axis.Regular(4, -2, 2, underflow=flow, overflow=flow), ) h2.fill(0, 0, weight=0) h2 += h h2 += h h += h assert h == h2 for i in range(-flow, h.axes[0].size + flow): for j in range(-flow, h.axes[1].size + flow): assert h[bh.tag.at(i), bh.tag.at(j)] == 2 * m[i][j] def test_sub_2d(flow, count_storage): h0 = bh.Histogram( bh.axis.Integer(-1, 2, underflow=flow, overflow=flow), bh.axis.Regular(4, -2, 2, underflow=flow, overflow=flow), storage=count_storage(), ) h0.fill(-1, -2) h0.fill(-1, -1) h0.fill(0, 0) h0.fill(0, 1) h0.fill(1, 0) h0.fill(3, -1) h0.fill(0, -3) m = h0.values(flow=True).copy() if count_storage not in {bh.storage.AtomicInt64, bh.storage.Weight}: h = h0.copy() h -= h0 assert h.values(flow=True) == approx(m * 0) h -= h0 assert h.values(flow=True) == approx(-m) h2 = h0 - (h0 + h0 + h0) assert h2.values(flow=True) == approx(-2 * m) h3 = h0 - h0.view(flow=True) * 4 assert h3.values(flow=True) == approx(-3 * m) h4 = h0.copy() h4 -= h0.view(flow=True) * 5 assert h4.values(flow=True) == approx(-4 * m) h5 = h0.copy() h5 -= 2 assert h5.values(flow=True) == approx(m - 2) h6 = h0 - 3 assert h6.values(flow=True) == approx(m - 3) def test_repr(): hrepr = """Histogram( Regular(3, 0, 1), Integer(0, 1), storage=Double())""" h = bh.Histogram(bh.axis.Regular(3, 0, 1), bh.axis.Integer(0, 1)) assert repr(h) == hrepr h.fill([0.3, 0.5], [0, 0]) hrepr += " # Sum: 2.0" assert repr(h) == hrepr h.fill([0.3, 12], [3, 0]) hrepr += " (4.0 with flow)" assert repr(h) == hrepr def test_1d_repr(): hrepr = """Histogram(Regular(4, 1, 2), storage=Double())""" h = bh.Histogram(bh.axis.Regular(4, 1, 2)) assert repr(h) == hrepr def test_empty_repr(): hrepr = """Histogram(storage=Double())""" h = bh.Histogram() assert repr(h) == hrepr def test_empty_mean_repr_has_no_sum_line(): # Mean/WeightedMean accumulators have no __bool__, so an empty sum is # still truthy; the repr must not print a spurious "# Sum:" for it. h = bh.Histogram(bh.axis.Regular(3, 0, 1), storage=bh.storage.Mean()) assert "# Sum:" not in repr(h) h2 = bh.Histogram(bh.axis.Regular(3, 0, 1), storage=bh.storage.WeightedMean()) assert "# Sum:" not in repr(h2) h.fill([0.5], sample=[1.0]) assert "# Sum:" in repr(h) def test_str(): h1 = bh.Histogram(bh.axis.Regular(3, 0, 1)) h1.view(True)[...] = [0, 1, 3, 2, 1] current_repr = repr(str(h1)) assert "[ -inf, 0)" in current_repr assert "[0.3333, 0.6667) 3" in current_repr assert "[ 1, inf)" in current_repr h2 = bh.Histogram(bh.axis.Regular(3, 0, 1), bh.axis.Integer(0, 1)) assert repr(str(h2)) == repr(repr(h2)) def test_axis(): axes = (bh.axis.Regular(10, 0, 1), bh.axis.Integer(0, 1)) h = bh.Histogram(*axes) for i, a in enumerate(axes): assert h.axes[i] == a with pytest.raises(IndexError): h.axes[2] assert h.axes[-1] == axes[-1] assert h.axes[-2] == axes[-2] with pytest.raises(IndexError): h.axes[-3] def test_out_of_limit_axis(): lim = bh._core.hist._axes_limit ax = ( bh.axis.Regular(1, -1, 1, underflow=False, overflow=False) for a in range(lim) ) # Nothrow bh.Histogram(*ax) ax = ( bh.axis.Regular(1, -1, 1, underflow=False, overflow=False) for a in range(lim + 1) ) with pytest.raises(IndexError): bh.Histogram(*ax) def test_out_of_range(): h = bh.Histogram(bh.axis.Regular(3, 0, 1)) h.fill(-1) h.fill(2) assert h[bh.underflow] == 1 assert h[bh.overflow] == 1 with pytest.raises(IndexError): h[4] def test_operators(): h = bh.Histogram(bh.axis.Integer(0, 2)) h.fill(0) h_orig = h h += h assert h is h_orig assert h[0] == 2 assert h[1] == 0 h *= 2 assert h is h_orig assert h[0] == 4 assert h[1] == 0 assert (h + h)[0] == (h * 2)[0] assert (h + h)[0] == (2 * h)[0] h /= 2 assert h is h_orig assert h[0] == 2 assert h[1] == 0 assert (h / 2)[0] == 1 h2 = bh.Histogram(bh.axis.Regular(2, 0, 2)) with pytest.raises(ValueError): h + h2 @pytest.mark.parametrize("operation", [operator.add, operator.iadd]) @pytest.mark.parametrize( ("left_storage", "right_storage"), [ (bh.storage.Int64, bh.storage.Double), (bh.storage.Double, bh.storage.Int64), ], ) def test_add_mismatched_storage_raises(operation, left_storage, right_storage): left = bh.Histogram(bh.axis.Regular(3, -1, 1), storage=left_storage()) right = bh.Histogram(bh.axis.Regular(3, -1, 1), storage=right_storage()) right.fill(0) message = ( f"different storage types: {left_storage.__name__} and {right_storage.__name__}" ) with pytest.raises(TypeError, match=message): operation(left, right) def test_hist_hist_div(): h1 = bh.Histogram(bh.axis.Boolean()) h2 = bh.Histogram(bh.axis.Boolean()) h1[:] = (8, 6) h2[:] = (2, 3) h3 = h1 / h2 assert h3[False] == 4 assert h3[True] == 2 h1 /= h2 assert h1[False] == 4 assert h1[True] == 2 @pytest.mark.parametrize("storage", [bh.storage.Int64, bh.storage.AtomicInt64]) def test_int_storage_division_promotes_to_double(storage): # gh #601: dividing integer-storage histograms must not truncate. a = bh.Histogram(bh.axis.Integer(0, 3), storage=storage()) b = bh.Histogram(bh.axis.Integer(0, 3), storage=storage()) a[:] = (5, 3, 7) b[:] = (2, 4, 3) expected = np.array([5, 3, 7]) / np.array([2, 4, 3]) # hist / hist result = a / b assert result.storage_type is bh.storage.Double assert result.view() == approx(expected) # operands are unchanged assert a.storage_type is storage assert b.storage_type is storage # hist / scalar assert (a / 2).view() == approx(np.array([5, 3, 7]) / 2) assert (a / 2).storage_type is bh.storage.Double # in-place division promotes storage but keeps the same object a_orig = a a /= b assert a is a_orig assert a.storage_type is bh.storage.Double assert a.view() == approx(expected) @pytest.mark.parametrize("storage", [bh.storage.Int64, bh.storage.AtomicInt64]) def test_int_storage_mul_by_float_promotes_to_double(storage): # A non-integer scalar multiply must promote like division, instead of # leaking a numpy UFuncTypeError from the in-place int64 view multiply. a = bh.Histogram(bh.axis.Integer(0, 3), storage=storage()) a[:] = (1, 2, 3) result = a * 0.5 assert result.storage_type is bh.storage.Double assert result.view() == approx(np.array([0.5, 1.0, 1.5])) # operand is unchanged assert a.storage_type is storage # multiplying by an integer scalar keeps the int storage int_result = a * 2 assert int_result.storage_type is storage assert int_result.view() == approx(np.array([2, 4, 6])) # in-place multiply promotes storage but keeps the same object a_orig = a a *= 0.5 assert a is a_orig assert a.storage_type is bh.storage.Double assert a.view() == approx(np.array([0.5, 1.0, 1.5])) def test_mixed_int_double_division(): # gh #601: storage mismatch must still divide correctly. ai = bh.Histogram(bh.axis.Integer(0, 3), storage=bh.storage.Int64()) bd = bh.Histogram(bh.axis.Integer(0, 3), storage=bh.storage.Double()) ai[:] = (5, 3, 7) bd[:] = (2, 4, 3) expected = np.array([5, 3, 7]) / np.array([2, 4, 3]) assert (ai / bd).view() == approx(expected) assert (bd / ai).view() == approx(1 / expected) def test_reflected_scalar_operators(): # gh-1154: scalar - hist and scalar / hist were missing (only the # commutative __radd__/__rmul__ existed). h = bh.Histogram(bh.axis.Regular(4, 0, 4)) h[:] = (1, 2, 4, 5) values = h.view().copy() # scalar - hist == -(hist - scalar) assert (10 - h).view() == approx(10 - values) assert (10 - h).view() == approx(((h - 10) * -1).view()) # scalar / hist, elementwise assert (8 / h).view() == approx(8 / values) # operands left unchanged assert h.view() == approx(values) def test_reflected_division_promotes_int_storage(): # gh-1154: scalar / int-hist must not truncate (mirrors __itruediv__). h = bh.Histogram(bh.axis.Integer(0, 3), storage=bh.storage.Int64()) h[:] = (2, 4, 8) result = 1 / h assert result.storage_type is bh.storage.Double assert result.view() == approx(1 / np.array([2, 4, 8])) assert h.storage_type is bh.storage.Int64 def test_reflected_operators_weight_storage(): # scalar - weighted-hist works (variance is preserved through the negation), # but scalar / weighted-hist is rejected: the reciprocal of a weighted sum # has no meaningful variance (matches test_view_rdiv_rejected). h = bh.Histogram(bh.axis.Regular(3, 0, 3), storage=bh.storage.Weight()) h.fill([0, 1, 1, 2], weight=[1, 2, 3, 4]) values = h.values().copy() assert (10 - h).values() == approx(10 - values) with pytest.raises(TypeError, match="divide a scalar or array"): 12 / h def test_project(): h = bh.Histogram(bh.axis.Integer(0, 2), bh.axis.Integer(1, 4)) h.fill(0, 1) h.fill(0, 2) h.fill(1, 3) h0 = h.project(0) assert h0.ndim == 1 assert h0.axes[0] == bh.axis.Integer(0, 2) assert [h0[i] for i in range(2)] == [2, 1] h1 = h.project(1) assert h1.ndim == 1 assert h1.axes[0] == bh.axis.Integer(1, 4) assert [h1[i] for i in range(3)] == [1, 1, 1] with pytest.raises(ValueError): h.project(*range(10)) with pytest.raises(ValueError): h.project(2, 1) with pytest.raises(ValueError): h.project(9) with pytest.raises(ValueError): h.project(-1) h_noflow = h.project(0, flow=False) assert [h_noflow[i] for i in range(2)] == [2, 1] h.fill(-1, 0) h.fill(0, 0) h_flow_true = h.project(0, flow=True) assert h_flow_true[0] == 3 h_flow_false = h.project(0, flow=False) assert h_flow_false[0] == 2 def test_project_nd(): h = bh.Histogram( bh.axis.Integer(0, 2), bh.axis.Integer(0, 2), bh.axis.Integer(0, 2) ) h.fill(0, 0, 0) h.fill(1, 1, 0) h.fill(0, 0, -1) h.fill(1, 1, 2) h_flow_true = h.project(0, 1, flow=True) h_flow_false = h.project(0, 1, flow=False) assert h_flow_true[0, 0] == 2 assert h_flow_true[1, 1] == 2 assert h_flow_false[0, 0] == 1 assert h_flow_false[1, 1] == 1 def test_shrink_1d(): h = bh.Histogram(bh.axis.Regular(20, 1, 5)) h.fill(1.1) hs = h[{0: slice(bh.loc(1), bh.loc(2))}] assert hs.view() == approx([1, 0, 0, 0, 0]) d = OrderedDict({0: slice(bh.loc(1), bh.loc(2))}) hs = h[d] assert hs.view() == approx([1, 0, 0, 0, 0]) @pytest.mark.parametrize( "metadata", [None, {}, {"a": "1"}], ids=["None", "empty", "dict"] ) def test_rebin_1d(metadata): h = bh.Histogram(bh.axis.Regular(20, 1, 5, metadata=metadata)) h.fill([1.1, 2.2, 3.3, 4.4]) hs = h[{0: slice(None, None, bh.rebin(4))}] assert hs.view() == approx([1, 1, 1, 0, 1]) assert h.axes[0].metadata is hs.axes[0].metadata hs = h[{0: bh.rebin(4)}] assert hs.view() == approx([1, 1, 1, 0, 1]) assert h.axes[0].metadata is hs.axes[0].metadata hs = h[{0: bh.rebin(groups=[1, 2, 3, 14])}] assert hs.view() == approx([1, 0, 0, 3]) assert hs.axes.edges[0] == approx([1.0, 1.2, 1.6, 2.2, 5.0]) assert h.axes[0].metadata is hs.axes[0].metadata exact_edges = [1.0, 1.2, 1.6, 2.2, 5.0] hs = h[bh.rebin(edges=exact_edges)] assert hs.view() == approx([1, 0, 0, 3]) assert hs.axes.edges[0] == approx(exact_edges) assert h.axes[0].metadata is hs.axes[0].metadata fuzzy_edges = [1.0, 1.200000000000001, 1.6, 2.2, 5.0] hs = h[bh.rebin(edges=fuzzy_edges)] assert hs.view() == approx([1, 0, 0, 3]) assert hs.axes.edges[0] == approx(exact_edges) assert h.axes[0].metadata is hs.axes[0].metadata hs = h[bh.rebin(axis=bh.axis.Variable([1.0, 1.2, 1.6, 2.2, 5.0], metadata="hi"))] assert hs.view() == approx([1, 0, 0, 3]) assert hs.axes.edges[0] == approx([1.0, 1.2, 1.6, 2.2, 5.0]) assert hs.axes[0].metadata == "hi" def test_rebin_1d_flow(): h = bh.Histogram(bh.axis.Regular(5, 0, 5, underflow=True, overflow=True)) h.fill([-1, 1.1, 2.2, 3.3, 4.4, 5.5]) hs = h[bh.rebin(edges=[0, 3, 5.0])] assert hs.view() == approx([2, 2]) assert hs.view(flow=True) == approx([1, 2, 2, 1]) assert hs.axes.edges[0] == approx([0.0, 3.0, 5.0]) # Flow bins are kept from the original h = bh.Histogram(bh.axis.Regular(5, 0, 5, underflow=False, overflow=False)) h.fill([-1, 1.1, 2.2, 3.3, 4.4, 5.5]) hs = h[bh.rebin(edges=[0, 3, 5.0])] assert hs.view(flow=True) == approx([2, 2]) h = bh.Histogram(bh.axis.Regular(5, 0, 5, underflow=True, overflow=False)) h.fill([-1, 1.1, 2.2, 3.3, 4.4, 5.5]) hs = h[bh.rebin(edges=[0, 3, 5.0])] assert hs.view(flow=True) == approx([1, 2, 2]) h = bh.Histogram(bh.axis.Regular(5, 0, 5, underflow=True, overflow=True)) h.fill([-1, 1.1, 2.2, 3.3, 4.4, 5.5]) hs = h[ bh.rebin(axis=bh.axis.Variable([0, 3, 5.0], underflow=False, overflow=False)) ] assert hs.view(flow=True) == approx([2, 2]) def test_rebin_change_axis_int(): h = bh.Histogram(bh.axis.Regular(5, 0, 5)) h.fill([-1, 1.1, 2.2, 3.3, 4.4, 5.5]) hs = h[bh.rebin(edges=[0, 3, 5.0], axis=bh.axis.Integer(10, 12))] assert hs.view() == approx([2, 2]) assert hs.view(flow=True) == approx([1, 2, 2, 1]) assert hs.axes.edges[0] == approx([10, 11, 12]) def test_rebin_change_axis_cat(): h = bh.Histogram(bh.axis.Regular(5, 0, 5)) h.fill([-1, 1.1, 2.2, 3.3, 4.4, 5.5]) hs = h[bh.rebin(groups=[2, 3], axis=bh.axis.StrCategory(["a", "b"]))] assert hs.view() == approx([1, 3]) # Old underflow (1) and old overflow (1) both end up in the new overflow, # exactly once each. assert hs.view(flow=True) == approx([1, 3, 2]) assert list(hs.axes[0]) == approx(["a", "b"]) def test_rebin_groups_flow_to_noflow(): # Issue #1143 (B6b): rebinning a flow axis onto a categorical axis # without an underflow bin must not fold the old underflow into the # first group, and must add it to the new overflow exactly once. h = bh.Histogram(bh.axis.Regular(4, 0, 4)) h.view(flow=True)[:] = [100, 1, 2, 3, 4, 200] hs = h[bh.rebin(groups=[2, 2], axis=bh.axis.IntCategory([0, 1]))] assert hs.view(flow=True) == approx([3, 7, 300]) def test_rebin_groups_flow_combinations(): # All combinations of (old flow) x (new flow) for group rebinning. h = bh.Histogram(bh.axis.Regular(4, 0, 4)) h.view(flow=True)[:] = [100, 1, 2, 3, 4, 200] # Old flow -> new flow: flow bins are carried over unchanged assert h[bh.rebin(groups=[2, 2])].view(flow=True) == approx([100, 3, 7, 200]) # Old underflow dropped on an ordered axis without underflow hs = h[bh.rebin(groups=[2, 2], axis=bh.axis.Variable([0, 2, 4], underflow=False))] assert hs.view(flow=True) == approx([3, 7, 200]) # Old overflow dropped on an axis without overflow hs = h[bh.rebin(groups=[2, 2], axis=bh.axis.Variable([0, 2, 4], overflow=False))] assert hs.view(flow=True) == approx([100, 3, 7]) # Both flow bins dropped on a flowless ordered axis hs = h[ bh.rebin( groups=[2, 2], axis=bh.axis.Variable([0, 2, 4], underflow=False, overflow=False), ) ] assert hs.view(flow=True) == approx([3, 7]) # Old axis without underflow: new axis copies the traits h2 = bh.Histogram(bh.axis.Regular(4, 0, 4, underflow=False)) h2.view(flow=True)[:] = [1, 2, 3, 4, 200] assert h2[bh.rebin(groups=[2, 2])].view(flow=True) == approx([3, 7, 200]) # Old axis without underflow onto a categorical axis: only the old # overflow goes to the new overflow hs = h2[bh.rebin(groups=[2, 2], axis=bh.axis.IntCategory([0, 1]))] assert hs.view(flow=True) == approx([3, 7, 200]) # Old flowless axis onto an axis with flow: new flow bins stay empty h3 = bh.Histogram(bh.axis.Regular(4, 0, 4, underflow=False, overflow=False)) h3.view(flow=True)[:] = [1, 2, 3, 4] hs = h3[bh.rebin(groups=[2, 2], axis=bh.axis.Variable([0, 2, 4]))] assert hs.view(flow=True) == approx([0, 3, 7, 0]) def test_shrink_rebin_1d(): h = bh.Histogram(bh.axis.Regular(20, 0, 4)) h.fill(1.1) hs = h[{0: slice(bh.loc(1), bh.loc(3), bh.rebin(2))}] assert hs.view() == approx([1, 0, 0, 0, 0]) def test_rebin_nd(): h = bh.Histogram( bh.axis.Regular(20, 1, 3), bh.axis.Regular(30, 1, 3), bh.axis.Regular(40, 1, 3) ) assert h[{0: np.s_[:: bh.rebin(2)]}].axes.size == (10, 30, 40) assert h[{1: np.s_[:: bh.rebin(2)]}].axes.size == (20, 15, 40) assert h[{2: np.s_[:: bh.rebin(2)]}].axes.size == (20, 30, 20) assert h[{0: np.s_[:: bh.rebin(groups=[1, 2, 17])]}].axes.size == (3, 30, 40) assert h[{1: np.s_[:: bh.rebin(groups=[1, 2, 27])]}].axes.size == (20, 3, 40) assert h[{2: np.s_[:: bh.rebin(groups=[1, 2, 37])]}].axes.size == (20, 30, 3) assert np.all( np.isclose( h[{0: np.s_[:: bh.rebin(groups=[1, 2, 17])]}].axes[0].edges, [1.0, 1.1, 1.3, 3.0], ) ) assert np.all( np.isclose( h[{1: np.s_[:: bh.rebin(groups=[1, 2, 27])]}].axes[1].edges, [1.0, 1.06666667, 1.2, 3.0], ) ) assert np.all( np.isclose( h[{2: np.s_[:: bh.rebin(groups=[1, 2, 37])]}].axes[2].edges, [1.0, 1.05, 1.15, 3.0], ) ) assert h[{0: np.s_[:: bh.rebin(2)], 2: np.s_[:: bh.rebin(2)]}].axes.size == ( 10, 30, 20, ) assert h[ { 0: np.s_[:: bh.rebin(groups=[1, 2, 17])], 2: np.s_[:: bh.rebin(groups=[1, 2, 37])], } ].axes.size == (3, 30, 3) assert np.all( np.isclose( h[ { 0: np.s_[:: bh.rebin(groups=[1, 2, 17])], 2: np.s_[:: bh.rebin(groups=[1, 2, 37])], } ] .axes[0] .edges, [1.0, 1.1, 1.3, 3], ) ) assert np.all( np.isclose( h[ { 0: np.s_[:: bh.rebin(groups=[1, 2, 17])], 2: np.s_[:: bh.rebin(groups=[1, 2, 37])], } ] .axes[2] .edges, [1.0, 1.05, 1.15, 3.0], ) ) assert h[{1: np.s_[:: bh.sum]}].axes.size == (20, 40) assert h[{1: bh.sum}].axes.size == (20, 40) # CLASSIC: This used to have metadata too, but that does not compare equal def test_pickle_0(flow): a = bh.Histogram( bh.axis.IntCategory([0, 1, 2], overflow=flow), bh.axis.Integer(0, 20), bh.axis.Regular(2, 0.0, 20.0, underflow=False, overflow=False), bh.axis.Variable([0.0, 1.0, 2.0]), bh.axis.Regular(4, 0, 2 * np.pi, circular=True), ) for i in range(a.axes[0].extent): a.fill(i, 0, 0, 0, 0) for j in range(a.axes[1].extent): a.fill(i, j, 0, 0, 0) for k in range(a.axes[2].extent): a.fill(i, j, k, 0, 0) for l in range(a.axes[3].extent): a.fill(i, j, k, l, 0) for m in range(a.axes[4].extent): a.fill(i, j, k, l, m * 0.5 * np.pi) io = pickle.dumps(a, -1) b = pickle.loads(io) assert id(a) != id(b) assert a.ndim == b.ndim assert a.axes[0] == b.axes[0] assert a.axes[1] == b.axes[1] assert a.axes[2] == b.axes[2] assert a.axes[3] == b.axes[3] assert a.axes[4] == b.axes[4] assert a.sum() == b.sum() assert repr(a) == repr(b) assert str(a) == str(b) assert a == b def test_pickle_1(): a = bh.Histogram( bh.axis.IntCategory([0, 1, 2]), bh.axis.Integer(0, 3, metadata="ia"), bh.axis.Regular(4, 0.0, 4.0, underflow=False, overflow=False), bh.axis.Variable([0.0, 1.0, 2.0]), ) assert isinstance(a, bh.Histogram) for i in range(a.axes[0].extent): a.fill(i, 0, 0, 0, weight=3) for j in range(a.axes[1].extent): a.fill(i, j, 0, 0, weight=10) for k in range(a.axes[2].extent): a.fill(i, j, k, 0, weight=2) for l in range(a.axes[3].extent): a.fill(i, j, k, l, weight=5) io = BytesIO() pickle.dump(a, io, protocol=-1) io.seek(0) b = pickle.load(io) assert id(a) != id(b) assert a.ndim == b.ndim assert a.axes[0] == b.axes[0] assert a.axes[1] == b.axes[1] assert a.axes[2] == b.axes[2] assert a.axes[3] == b.axes[3] assert a.sum() == b.sum() assert repr(a) == repr(b) assert str(a) == str(b) assert a == b def test_fill_bool_not_bool(): h = bh.Histogram(bh.axis.Boolean()) h.fill([0, 1, 1, 7, -3]) assert h.view() == approx([1, 4]) def test_pick_bool(): h = bh.Histogram(bh.axis.Boolean(), bh.axis.Boolean(metadata={"one": 1})) h.fill([True, True, False, False], [True, False, True, True]) h.fill([True, True, True], True) assert h[True, :].view() == approx([1, 4]) assert h[False, :].view() == approx([0, 2]) assert h[:, False].view() == approx([0, 1]) assert h[:, True].view() == approx([2, 4]) def test_slice_bool(): h = bh.Histogram(bh.axis.Boolean()) h.fill([0, 0, 0, 1, 3, 4, -2]) assert h.view() == approx([3, 4]) assert h[1:].view() == approx([4]) assert h[:1].view() == approx([3]) assert h[:1].axes[0].centers == approx([0.5]) assert h[1:].axes[0].centers == approx([1.5]) def test_pickle_bool(): a = bh.Histogram(bh.axis.Boolean(), bh.axis.Boolean(metadata={"one": 1})) assert isinstance(a, bh.Histogram) a.fill([True, True, False, False], [True, False, True, True]) a.fill([True, True, True], True) assert a[True, True] == 4 assert a[True, False] == 1 assert a[False, True] == 2 assert a[False, False] == 0 io = BytesIO() pickle.dump(a, io, protocol=-1) io.seek(0) b = pickle.load(io) assert id(a) != id(b) assert a.ndim == b.ndim assert a.axes[0] == b.axes[0] assert a.axes[1] == b.axes[1] assert a.sum() == b.sum() assert repr(a) == repr(b) assert str(a) == str(b) assert a == b assert a.view() == approx(b.view()) # NumPy tests def test_numpy_conversion_0(): a = bh.Histogram(bh.axis.Integer(0, 3, underflow=False, overflow=False)) a.fill(0) for _ in range(5): a.fill(1) c = np.array(a) # a copy v = np.asarray(a) # a view for t in (c, v): assert t.dtype == np.double # CLASSIC: np.uint8 assert t == approx((1, 5, 0)) for _ in range(10): a.fill(2) # copy does not change, but view does assert c == approx((1, 5, 0)) assert v == approx((1, 5, 10)) for _ in range(255): a.fill(1) c = np.array(a) assert c.dtype == np.double # CLASSIC: np.uint16 assert c == approx((1, 260, 10)) # view does not follow underlying switch in word size # assert not np.all(c, v) def test_numpy_conversion_1(): # CLASSIC: was weight array h = bh.Histogram(bh.axis.Integer(0, 3)) for _ in range(10): h.fill(1, weight=3) c = np.array(h) # a copy v = np.asarray(h) # a view assert c.dtype == np.double # CLASSIC: np.float64 assert c == approx(np.array((0, 30, 0))) assert v == approx(c) def test_numpy_conversion_2(): a = bh.Histogram( bh.axis.Integer(0, 2, underflow=False, overflow=False), bh.axis.Integer(0, 3, underflow=False, overflow=False), bh.axis.Integer(0, 4, underflow=False, overflow=False), ) r = np.zeros((2, 3, 4), dtype=np.int8) for i in range(a.axes[0].extent): for j in range(a.axes[1].extent): for k in range(a.axes[2].extent): for _ in range(i + j + k): a.fill(i, j, k) r[i, j, k] = i + j + k d = np.zeros((2, 3, 4), dtype=np.int8) for i in range(a.axes[0].extent): for j in range(a.axes[1].extent): for k in range(a.axes[2].extent): d[i, j, k] = a[i, j, k] assert d == approx(r) c = np.array(a) # a copy v = np.asarray(a) # a view assert c == approx(r) assert v == approx(r) def test_numpy_conversion_3(): a = bh.Histogram( bh.axis.Integer(0, 2), bh.axis.Integer(0, 3), bh.axis.Integer(0, 4), storage=bh.storage.Double(), ) r = np.zeros((4, 5, 6)) for i in range(a.axes[0].extent): for j in range(a.axes[1].extent): for k in range(a.axes[2].extent): a.fill(i - 1, j - 1, k - 1, weight=i + j + k) r[i, j, k] = i + j + k c = a.view(flow=True) assert c == approx(r) assert a.sum() == approx(144) assert a.sum(flow=True) == approx(720) assert c.sum() == approx(720) def test_numpy_conversion_4(): a = bh.Histogram( bh.axis.Integer(0, 2, underflow=False, overflow=False), bh.axis.Integer(0, 4, underflow=False, overflow=False), ) a1 = np.asarray(a) assert a1.dtype == np.double assert a1.shape == (2, 4) b = bh.Histogram() b1 = np.asarray(b) assert b1.shape == () assert np.sum(b1) == 0 # Compare sum methods assert b.sum() == np.asarray(b).sum() def test_numpy_conversion_5(): a = bh.Histogram( bh.axis.Integer(0, 3, underflow=False, overflow=False), bh.axis.Integer(0, 2, underflow=False, overflow=False), storage=bh.storage.Unlimited(), ) a.fill(0, 0) for _ in range(80): a = a + a # a now holds a multiprecision type a.fill(1, 0) for _ in range(2): a.fill(2, 0) for _ in range(3): a.fill(0, 1) for _ in range(4): a.fill(1, 1) for _ in range(5): a.fill(2, 1) a1 = a.view() assert a1.shape == (3, 2) assert a1[0, 0] == float(2**80) assert a1[1, 0] == 1 assert a1[2, 0] == 2 assert a1[0, 1] == 3 assert a1[1, 1] == 4 assert a1[2, 1] == 5 def test_rank0_sum_empty(): # A rank-0 histogram has a single cell and no flow bins. inner coverage # used to drive Boost's indexed range, which is UB for rank-0 and crashed # depending on stack layout (e.g. free-threaded Windows). inner == all here. h = bh.Histogram() assert h.ndim == 0 assert h.empty() is True assert h.empty(flow=True) is True assert h.sum() == 0 assert h.sum(flow=True) == 0 h.fill() h.fill() h.fill() assert h.empty() is False assert h.sum() == 3 assert h.sum(flow=True) == 3 def test_rank0_project(): # project() drives the same rank-0-UB indexed range as sum()/empty() above, # and unlike those it cannot be avoided by picking a coverage. The identity # is the only valid projection of a rank-0 histogram. h = bh.Histogram() h.fill() assert h.project().ndim == 0 assert h.project().sum() == 1 def test_rank0_reduce(): # reduce() drives the same rank-0-UB indexed range. Nothing public reaches # it on a rank-0 histogram today (__getitem__ only reduces when it has # slices to apply), so exercise the binding directly. h = bh.Histogram() h.fill() assert h._reduce().ndim == 0 assert h._reduce().sum() == 1 @pytest.mark.parametrize( "dtype", [np.int8, np.int16, np.int32, np.int64, np.uint8, np.uint16, np.uint32, np.uint64], ) def test_fill_dtypes(dtype): a = bh.Histogram(bh.axis.Integer(0, 2), storage=bh.storage.Int64()) a.fill(np.array([0, 0, 0, 1, 1, 2], dtype=dtype)) a.fill(dtype(0)) assert list(a.values()) == [4, 2] def test_fill_with_sequence_0(): def fa(*args): return np.array(args, dtype=float) def ia(*args): return np.array(args, dtype=int) a = bh.Histogram(bh.axis.Integer(0, 2)) a.fill(np.array(1)) # 0-dim arrays work a.fill(ia(-1, 0, 1, 2)) a.fill((2, 1, 0, -1)) assert a.view(True) == approx([2, 2, 3, 2]) with pytest.raises(ValueError): a.fill(np.empty((2, 2))) with pytest.raises(ValueError): a.fill(np.empty(2), 1) with pytest.raises(ValueError): a.fill(np.empty(2), np.empty(3)) with pytest.raises(TypeError): a.fill("abc") with pytest.raises(IndexError): a[1, 2] b = bh.Histogram(bh.axis.Regular(3, 0, 3)) b.fill(fa(0, 0, 1, 2)) b.fill(ia(1, 0, 2, 2)) assert b.view(True) == approx([0, 3, 2, 3, 0]) c = bh.Histogram( bh.axis.Integer(0, 2, underflow=False, overflow=False), bh.axis.Regular(2, 0, 2) ) c.fill(ia(-1, 0, 1), fa(-1.0, 1.5, 0.5)) assert c.view(True) == approx(np.array([[0, 0, 1, 0], [0, 1, 0, 0]])) # we don't support: assert a[[1, 1]].value, 0 with pytest.raises(ValueError): c.fill(1) with pytest.raises(ValueError): c.fill([1, 0, 2], [1, 1]) # this broadcasts c.fill([1, 0], -1) assert c.view(True) == approx(np.array([[1, 0, 1, 0], [1, 1, 0, 0]])) c.fill([1, 0], 0) assert c.view(True) == approx(np.array([[1, 1, 1, 0], [1, 2, 0, 0]])) c.fill(0, [-1, 0.5, 1.5, 2.5]) assert c.view(True) == approx(np.array([[2, 2, 2, 1], [1, 2, 0, 0]])) with pytest.raises(IndexError): c[1] with pytest.raises(IndexError): c[1, 2, 3] @pytest.mark.xfail( platform.machine() == "ppc64le", reason="ppc64le bug (TBD)", strict=False ) def test_fill_with_sequence_1(): a = bh.Histogram(bh.axis.Integer(0, 3), storage=bh.storage.Weight()) v = np.array([-1, 0, 1, 2, 3, 4], dtype=int) w = np.array([2, 3, 4, 5, 6, 7], dtype=float) a.fill(v, weight=w) a.fill((0, 1), weight=(2, 3)) assert a[bh.underflow] == bh.accumulators.WeightedSum(2, 4) assert a[0] == bh.accumulators.WeightedSum(5, 13) assert a[1] == bh.accumulators.WeightedSum(7, 25) assert a[2] == bh.accumulators.WeightedSum(5, 25) assert a[bh.underflow].value == 2 assert a[0].value == 5 assert a[1].value == 7 assert a[2].value == 5 assert a[bh.underflow].variance == 4 assert a[0].variance == 13 assert a[1].variance == 25 assert a[2].variance == 25 a.fill((1, 2), weight=1) a.fill(0, weight=1) a.fill(0, weight=2) assert a[0].value == 8 assert a[1].value == 8 assert a[2].value == 6 with pytest.raises(TypeError): a.fill((1, 2), foo=(1, 1)) with pytest.raises(ValueError): a.fill((1, 2, 3), weight=(1, 2)) with pytest.raises(ValueError): a.fill((1, 2), weight="ab") with pytest.raises(TypeError): a.fill((1, 2), weight=(1, 1), foo=1) with pytest.raises(ValueError): a.fill((1, 2), weight=([1, 1], [2, 2])) a = bh.Histogram(bh.axis.Integer(0, 3)) # this broadcasts a.fill((1, 2), weight=1) assert a[1] == 1.0 assert a[2] == 1.0 a = bh.Histogram( bh.axis.Integer(0, 2, underflow=False, overflow=False), bh.axis.Regular(2, 0, 2, underflow=False, overflow=False), ) a.fill((-1, 0, 1), (-1, 1, 0.1)) assert a[0, 0] == 0 assert a[0, 1] == 1 assert a[1, 0] == 1 assert a[1, 1] == 0 a = bh.Histogram(bh.axis.Integer(0, 3, underflow=False, overflow=False)) a.fill((0, 0, 1, 2)) a.fill((1, 0, 2, 2)) assert a[0] == 3 assert a[1] == 2 assert a[2] == 3 def test_fill_with_sequence_2(): a = bh.Histogram(bh.axis.StrCategory(["A", "B"])) a.fill("A") a.fill(np.array("B")) # 0-dim array is also accepted a.fill(("A", "B", "C")) assert a.view(True) == approx([2, 2, 1]) a.fill(np.array(("D", "B", "A"), dtype="S5")) a.fill(np.array(("D", "B", "A"), dtype="U1")) assert a.view(True) == approx([4, 4, 3]) with pytest.raises(ValueError): a.fill(np.array((("B", "A"), ("C", "A")))) # ndim == 2 not allowed b = bh.Histogram( bh.axis.Integer(0, 2, underflow=False, overflow=False), bh.axis.StrCategory(["A", "B"]), ) b.fill((1, 0, 10), ("C", "B", "A")) assert b.view(True) == approx(np.array([[0, 1, 0], [0, 0, 1]])) def test_fill_with_sequence_3(): h = bh.Histogram(bh.axis.StrCategory([], growth=True)) h.fill("A") assert h.axes[0].size == 1 h.fill(["A"]) assert h.axes[0].size == 1 h.fill(["A", "B"]) assert h.axes[0].size == 2 assert h.view(True) == approx([3, 1]) def test_fill_with_sequence_4(): h = bh.Histogram( bh.axis.StrCategory([], growth=True), bh.axis.Integer(0, 0, growth=True) ) h.fill("1", np.arange(2)) assert h.axes[0].size == 1 assert h.axes[1].size == 2 assert h.view(True) == approx(np.array([[1, 1]])) with pytest.raises(ValueError): h.fill(["1"], np.arange(2)) # lengths do not match def test_axes_reference(): h = bh.Histogram( bh.axis.Regular(10, 0, 1), bh.axis.Regular(20, 2, 4, metadata=12), bh.axis.StrCategory([], growth=True), ) h.axes[0].metadata = "set1" h.axes[1].metadata = None h_copy = h[...] assert h_copy.axes[0].metadata == "set1" assert h_copy.axes[1].metadata is None assert h_copy.axes[2].size == 0 h_copy.fill([0.3], [3.2], ["check"]) assert h_copy.axes[2].size == 1 def test_axes_lifetime(): h = bh.Histogram(bh.axis.Regular(10, 0, 1, metadata=2)) ax = h.axes[0] if platform.python_implementation() == "CPython": # 2 is the minimum refcount, so the *python* object should be deleted # after the del; hopefully the C++ object lives through the axis instance. # On Python 3.14, this can be 1 assert sys.getrefcount(h) <= 2 del h assert ax.metadata == 2 ax.metadata = 3 assert ax.metadata == 3 def test_copy_axes(): h = bh.Histogram(bh.axis.Regular(10, 0, 1)) h2 = h.copy() h.axes[0].metadata = 1 assert h2.axes[0].metadata is None def test_shape(): h = bh.Histogram( bh.axis.Regular(7, 0, 1), bh.axis.Regular(13, 0, 1), bh.axis.Regular(17, 0, 1), bh.axis.Regular(24, 0, 1), ) assert h.shape == (7, 13, 17, 24) def test_empty_shape(): h = bh.Histogram() assert h.shape == () # issue #416 a def test_hist_division(): edges = [0, 0.25, 0.5, 0.75, 1, 2, 3, 4, 7, 10] edges = [-x for x in reversed(edges)] + edges[1:] h = bh.Histogram(bh.axis.Variable(edges)) h[...] = 1 h1 = h.copy() dens = h.view().copy() dens /= h.axes[0].widths * h.sum() h1 /= h.axes[0].widths * h.sum() assert h1.view() == approx(dens) def test_add_hists(): edges = [0, 0.25, 0.5, 0.75, 1, 2, 3, 4, 7, 10] edges = [-x for x in reversed(edges)] + edges[1:] h = bh.Histogram(bh.axis.Variable(edges)) h[...] = 1 h1 = h.copy() h1 += h.view() h2 = h.copy() h2 += h1 h3 = h.copy() h3 += 5 assert np.asarray(h) == approx(1) assert np.asarray(h1) == approx(2) assert np.asarray(h2) == approx(3) assert np.asarray(h3) == approx(6) def test_add_broadcast(): h = bh.Histogram(bh.axis.Regular(10, 0, 1), bh.axis.Regular(20, 0, 1)) h1 = h.copy() h2 = h.copy() h1[...] = 1 assert h1.view().sum() == 10 * 20 assert h1.view(flow=True).sum() == 10 * 20 h2 = h + [[1]] # noqa: RUF005 assert h2.sum() == 10 * 20 assert h2.sum(flow=True) == 12 * 22 h3 = h + np.ones((10, 20)) assert h3.sum() == 10 * 20 assert h3.sum(flow=True) == 10 * 20 h4 = h + np.ones((12, 22)) assert h4.view(flow=True).sum() == 12 * 22 h5 = h + np.ones((10, 1)) assert h5.sum(flow=True) == 10 * 20 h5 = h + np.ones((1, 22)) assert h5.sum(flow=True) == 12 * 22 # Issue #431 def test_mul_shallow(): my_lock = threading.Lock() h = bh.Histogram(bh.axis.Integer(0, 3, metadata=my_lock), metadata=my_lock) h.fill([0, 0, 0, 1]) h2 = h * 2 assert h.metadata is h2.metadata assert h.axes[0].metadata is h2.axes[0].metadata def test_reductions(): h = bh.Histogram(bh.axis.Variable([1, 2, 4, 7, 9, 9.5, 10])) widths_1 = functools.reduce(operator.mul, h.axes.widths) widths_2 = np.prod(h.axes.widths, axis=0) assert widths_1 == approx(widths_2) # Issue 435 def test_np_scalars(): hist = bh.Histogram(bh.axis.Regular(30, 1, 500, transform=bh.axis.transform.log)) hist.fill([7, 7]) hist2 = hist / np.float64(2.0) assert hist2[bh.loc(7)] == 1.0 hist2 = hist / hist.axes.widths.prod(axis=0) assert hist2[bh.loc(7)] == approx(1.3467513416439476) with pytest.raises(ValueError): hist / np.array([1, 2, 3]) hist /= np.float64(2.0) assert hist[bh.loc(7)] == 1.0 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) # Issue #618 def test_negative_fill(count_storage): h = bh.Histogram(bh.axis.Integer(0, 3), storage=count_storage()) h.fill(1, weight=-1) answer = np.array([0, -1, 0]) assert h.values() == approx(answer) # Issue #589 def test_underfill_growth(): h = bh.Histogram(bh.axis.Regular(10, 0, 1, growth=True)) h.fill(2) h.fill(-1) assert h.sum() == 2 # ---- allclose tests --------------------------------------------------------- def test_allclose_same_histogram(): h = bh.Histogram(bh.axis.Regular(5, 0, 1)) h.fill(np.random.default_rng(42).random(100)) assert h.allclose(h) assert h.allclose(h.copy()) def test_allclose_different_bins(): h1 = bh.Histogram(bh.axis.Regular(5, 0, 1)) h2 = bh.Histogram(bh.axis.Regular(5, 0, 1)) rng = np.random.default_rng(42) vals = rng.random(100) h1.fill(vals) h2.fill(vals) h2.fill(0.7) assert not h1.allclose(h2) def test_allclose_edges_within_tol(): h1 = bh.Histogram(bh.axis.Regular(5, 0, 1)) h2 = bh.Histogram(bh.axis.Regular(5, 0, 1 + 1e-7)) assert h1.allclose(h2) def test_allclose_edges_outside_tol(): h1 = bh.Histogram(bh.axis.Regular(5, 0, 1)) h2 = bh.Histogram(bh.axis.Regular(5, 0, 1 + 0.1)) assert not h1.allclose(h2) def test_allclose_different_shape(): h1 = bh.Histogram(bh.axis.Regular(5, 0, 1)) h2 = bh.Histogram(bh.axis.Regular(6, 0, 1)) assert not h1.allclose(h2) def test_allclose_different_ndim(): h1 = bh.Histogram(bh.axis.Regular(5, 0, 1)) h2 = bh.Histogram(bh.axis.Regular(5, 0, 1), bh.axis.Regular(3, 0, 1)) assert not h1.allclose(h2) def test_allclose_different_storage(): h1 = bh.Histogram(bh.axis.Regular(5, 0, 1)) h2 = bh.Histogram(bh.axis.Regular(5, 0, 1), storage=bh.storage.Weight()) assert not h1.allclose(h2) def test_allclose_flow_option(): h1 = bh.Histogram(bh.axis.Regular(3, 0, 1)) h2 = bh.Histogram(bh.axis.Regular(3, 0, 1)) h1.fill([0.5]) # central bin only h2.fill([0.5]) # central bin only # h1 has flow bins empty, h2 also, so both should match with/without flow assert h1.allclose(h2, flow=True) assert h1.allclose(h2, flow=False) # Now put different data in underflow h3 = bh.Histogram(bh.axis.Regular(3, 0, 1)) h4 = bh.Histogram(bh.axis.Regular(3, 0, 1)) h3.fill([-0.5]) h4.fill([-0.5, -0.5]) assert not h3.allclose(h4, flow=True) assert h3.allclose(h4, flow=False) def test_allclose_metadata(): h1 = bh.Histogram(bh.axis.Regular(3, 0, 1), metadata="foo") h2 = bh.Histogram(bh.axis.Regular(3, 0, 1), metadata="foo") h3 = bh.Histogram(bh.axis.Regular(3, 0, 1), metadata="bar") assert h1.allclose(h2) assert h1.allclose(h2, metadata=True) assert not h1.allclose(h3, metadata=True) assert h1.allclose(h3, metadata=False) def test_allclose_non_histogram(): h = bh.Histogram(bh.axis.Regular(3, 0, 1)) assert not h.allclose(42) assert not h.allclose("histogram") def test_allclose_categorical_exact(): h1 = bh.Histogram(bh.axis.StrCategory(["a", "b"])) h2 = bh.Histogram(bh.axis.StrCategory(["a", "b"])) h3 = bh.Histogram(bh.axis.StrCategory(["a", "c"])) h4 = bh.Histogram(bh.axis.StrCategory(["b", "a"])) assert h1.allclose(h2) assert not h1.allclose(h3) assert not h1.allclose(h4) def test_allclose_intcategory(): h1 = bh.Histogram(bh.axis.IntCategory([1, 2, 3])) h2 = bh.Histogram(bh.axis.IntCategory([1, 2, 3])) h3 = bh.Histogram(bh.axis.IntCategory([1, 2, 4])) assert h1.allclose(h2) assert not h1.allclose(h3) def test_allclose_boolean(): h1 = bh.Histogram(bh.axis.Boolean()) h2 = bh.Histogram(bh.axis.Boolean()) assert h1.allclose(h2) def test_allclose_mean_storage(): h1 = bh.Histogram(bh.axis.Regular(3, 0, 1), storage=bh.storage.Mean()) h2 = bh.Histogram(bh.axis.Regular(3, 0, 1), storage=bh.storage.Mean()) rng = np.random.default_rng(42) samples = rng.random(100) h1.fill(samples, sample=samples) h2.fill(samples, sample=samples) assert h1.allclose(h2) def test_allclose_weight_storage(): h1 = bh.Histogram(bh.axis.Regular(3, 0, 1), storage=bh.storage.Weight()) h2 = bh.Histogram(bh.axis.Regular(3, 0, 1), storage=bh.storage.Weight()) rng = np.random.default_rng(42) data = rng.random(100) w = rng.random(100) h1.fill(data, weight=w) h2.fill(data, weight=w) assert h1.allclose(h2) def test_allclose_weighted_mean_storage(): h1 = bh.Histogram(bh.axis.Regular(3, 0, 1), storage=bh.storage.WeightedMean()) h2 = bh.Histogram(bh.axis.Regular(3, 0, 1), storage=bh.storage.WeightedMean()) rng = np.random.default_rng(42) data = rng.random(100) w = rng.random(100) h1.fill(data, sample=data, weight=w) h2.fill(data, sample=data, weight=w) assert h1.allclose(h2) def test_allclose_different_continuous_traits(): h1 = bh.Histogram(bh.axis.Regular(3, 0, 1)) h2 = bh.Histogram(bh.axis.Integer(0, 3)) assert not h1.allclose(h2) @pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="needs subprocess") def test_missing_core_hint(): # Issue #1143 (B10): if the compiled _core module is missing, the import # error must include the "did you forget to compile" hint. This only works # if boost_histogram/__init__.py imports _core before any submodule does. code = """ import sys import importlib.abc class Blocker(importlib.abc.MetaPathFinder): def find_spec(self, name, path=None, target=None): if name == "boost_histogram._core": raise ModuleNotFoundError(f"No module named {name!r}", name=name) return None sys.meta_path.insert(0, Blocker()) import boost_histogram """ result = subprocess.run( [sys.executable, "-c", code], capture_output=True, text=True, check=False ) assert result.returncode != 0 assert "Did you forget to compile boost-histogram?" in result.stderr def test_fill_noncontiguous_int_category(): # Regression test for gh-1143: fill must honor input strides cat = [1, 2, 3] expected = bh.Histogram(bh.axis.IntCategory(cat)) expected.fill([1, 3, 2, 1]) h = bh.Histogram(bh.axis.IntCategory(cat)) h.fill(np.array([1, 2, 3, 1])[::-1]) assert h.view(flow=True) == approx(expected.view(flow=True)) h2 = bh.Histogram(bh.axis.IntCategory(cat)) h2.fill(np.array([1, 7, 3, 7, 2, 7, 1, 7])[::2]) assert h2.view(flow=True) == approx(expected.view(flow=True)) def test_fill_noncontiguous_str_category(): # Regression test for gh-1143: fill must honor input strides cat = ["a", "b", "c"] expected = bh.Histogram(bh.axis.StrCategory(cat)) expected.fill(["a", "c", "a"]) h = bh.Histogram(bh.axis.StrCategory(cat)) h.fill(np.array(["a", "b", "c", "b", "a"])[::2]) assert h.view(flow=True) == approx(expected.view(flow=True)) expected2 = bh.Histogram(bh.axis.StrCategory(cat)) expected2.fill(["c", "b", "a"]) h2 = bh.Histogram(bh.axis.StrCategory(cat)) h2.fill(np.array(["a", "b", "c"])[::-1]) assert h2.view(flow=True) == approx(expected2.view(flow=True)) @pytest.mark.parametrize( ("dtype", "value"), [(np.int64, 2**32 + 3), (np.uint32, 2**31 + 3), (np.uint64, 2**32 + 3)], ) def test_fill_int_axis_out_of_range(dtype, value): # Regression test: wide integers were narrowed to int32 and wrapped silently h = bh.Histogram(bh.axis.Integer(0, 5)) with pytest.raises(ValueError): h.fill(np.array([value], dtype=dtype)) assert h.sum(flow=True) == 0 c = bh.Histogram(bh.axis.IntCategory([1, 2, 3])) with pytest.raises(ValueError): c.fill(np.array([value], dtype=dtype)) assert c.sum(flow=True) == 0 def test_index_int_axis_out_of_range(): with pytest.raises(ValueError): bh.axis.Integer(0, 5).index(np.array([2**32 + 3])) with pytest.raises(ValueError): bh.axis.IntCategory([1, 2, 3]).index(np.array([2**32 + 2])) def test_fill_int_axis_scalar_out_of_range(): h = bh.Histogram(bh.axis.Integer(0, 5)) with pytest.raises((ValueError, TypeError)): h.fill(2**32 + 3) assert h.sum(flow=True) == 0 @pytest.mark.parametrize( "dtype", [np.bool_, np.int8, np.uint8, np.int16, np.uint16, np.int32, np.int64] ) def test_fill_int_axis_dtypes(dtype): h = bh.Histogram(bh.axis.Integer(0, 5)) h.fill(np.array([0, 1, 1], dtype=dtype)) assert h.view() == approx(np.array([1, 2, 0, 0, 0])) b = bh.Histogram(bh.axis.Boolean()) b.fill(np.array([0, 1, 1], dtype=dtype)) assert b.view() == approx(np.array([1, 2])) def test_compare_ndarray_metadata(): # Metadata that gives no plain bool from == must not abort the interpreter array = np.arange(3) h = bh.Histogram(bh.axis.Regular(3, 0, 1, metadata=array)) h_same = bh.Histogram(bh.axis.Regular(3, 0, 1, metadata=array)) h_other = bh.Histogram(bh.axis.Regular(3, 0, 1, metadata=np.arange(3))) assert h == h_same assert h != h_other assert (h + h_same).sum() == 0 assert h.copy(deep=False) == h with pytest.raises(ValueError, match="axes"): h + h_other @pytest.mark.parametrize( "axis_type", [bh.axis.StrCategory, bh.axis.IntCategory], ids=["str", "int"] ) def test_growth_axis_survives_merge(axis_type): # An axis held by the user must not dangle when a merge replaces it values = ["a", "b", "c"] if axis_type is bh.axis.StrCategory else [1, 2, 3] h = bh.Histogram(axis_type([], growth=True)) h.fill(values[:1]) ax = h.axes[0] assert ax.size == 1 h2 = bh.Histogram(axis_type([], growth=True)) h2.fill(values[1:]) h += h2 # The old axis is a snapshot of the merged-from state assert ax.size == 1 assert ax.index(values[0]) == 0 assert h.axes[0].size == 3 assert list(h.axes[0]) == values def test_growth_axis_size_after_fill(): h = bh.Histogram(bh.axis.Regular(2, 0, 1, growth=True)) assert h.axes[0].size == 2 h.fill([0.5, 2.5]) assert h.axes[0].size == 6 assert h.axes[0].size == h.view().size def test_view_after_growth(): # A growing fill moves the storage; the view must follow the new size h = bh.Histogram(bh.axis.Integer(0, 5, growth=True)) h.fill(np.arange(1000)) assert h.view().sum() == 1000 assert h.view(flow=True).size == 1000 def test_setitem_with_growth_axis(): h = bh.Histogram(bh.axis.Regular(4, 0, 1, growth=True)) h[...] = np.arange(4) assert h.view() == approx(np.arange(4)) h[1] = 10 assert h[1] == 10 assert h.view() == approx([0, 10, 2, 3]) h[np.array([0, 2])] = [5, 6] assert h.view() == approx([5, 10, 6, 3]) def test_inplace_op_with_growth_axis(): h = bh.Histogram(bh.axis.Regular(4, 0, 1, growth=True)) h.fill([0.1, 0.6]) h *= 2 assert h.sum() == 4 h /= 2 assert h.sum() == 2 assert h.axes[0].size == 4 @pytest.mark.parametrize("deep", [False, True], ids=["shallow", "deep"]) def test_copy_growth_axis_metadata_is_independent(deep): h = bh.Histogram(bh.axis.Regular(4, 0, 1, growth=True, metadata="original")) h2 = h.copy(deep=deep) h2.axes[0].metadata = "changed" assert h.axes[0].metadata == "original" assert h2.axes[0].metadata == "changed" def test_growth_axis_metadata_write_through(): h = bh.Histogram(bh.axis.StrCategory([], growth=True)) h.axes[0].metadata = "label" assert h.axes[0].metadata == "label" h.fill(["a"]) assert h.axes[0].metadata == "label" # The copy of a growth axis must hold the same dict as the stored axis assert h.axes[0].__dict__ is h.axes[0].__dict__ scikit-hep-boost-histogram-b4c49b0/tests/test_histogram_indexing.py000066400000000000000000000677151524660700500257640ustar00rootroot00000000000000from __future__ import annotations import numpy as np import pytest from pytest import approx import boost_histogram as bh from boost_histogram import _core def test_1D_get_bin(): h = bh.Histogram(bh.axis.Regular(10, 0, 0.99)) h.fill([0.25, 0.25, 0.25, 0.15]) assert h[0] == 0 assert h[1] == 1 assert h[2] == 3 assert h[bh.loc(0)] == 0 assert h[bh.loc(0.1)] == 1 assert h[bh.loc(0.1) + 1] == 3 assert h[bh.loc(0.2)] == 3 assert h.view()[2] == h[2] with pytest.raises(IndexError): h[1, 2] def test_2D_get_bin(): h = bh.Histogram(bh.axis.Regular(10, 0, 0.99), bh.axis.Regular(10, 0, 0.99)) h.fill(0.15, [0.25, 0.25, 0.25, 0.15]) assert h[0, 0] == 0 assert h[0, 1] == 0 assert h[1, 1] == 1 assert h[1, 2] == 3 assert h[bh.loc(0.1), bh.loc(0.2)] == 3 assert h[bh.loc(0) + 1, bh.loc(0.3) - 1] == 3 assert h.view()[1, 2] == h[1, 2] with pytest.raises(IndexError): h[1] def test_get_1D_histogram(): h = bh.Histogram(bh.axis.Regular(10, 0, 1)) h.fill([0.25, 0.25, 0.25, 0.15]) h2 = h[:] assert h == h2 h.fill(0.75) assert h != h2 def test_get_1D_slice(): h1 = bh.Histogram(bh.axis.Regular(10, 0, 1)) h2 = bh.Histogram(bh.axis.Regular(5, 0, 0.5)) h1.metadata = {"that": 3} h1.fill([0.25, 0.25, 0.25, 0.15]) h2.fill([0.25, 0.25, 0.25, 0.15]) assert h1 != h2 assert h1[:5] == h2 assert h1[: bh.loc(0.5)] == h2 assert h1[2:4] == h2[2:4] assert h1[bh.loc(0.2) : bh.loc(0.4)] == h2[bh.loc(0.2) : bh.loc(0.4)] assert len(h1[2:4].view()) == 2 assert len(h1[2 : 4 : bh.rebin(2)].view()) == 1 assert len(h1[:: bh.rebin(2)].view()) == 5 # Shortcut assert len(h1[bh.rebin(2)].view()) == 5 assert h1[2:4].metadata == {"that": 3} def test_ellipsis(): h = bh.Histogram(bh.axis.Regular(10, 0, 1), bh.axis.Regular(10, 0, 1)) assert h == h[...] assert h == h[:, ...] assert h == h[..., :] assert h == h[:, :, ...] assert h == h[:, ..., :] assert h == h[..., :, :] with pytest.raises(IndexError): h[:, :, :, ...] with pytest.raises(IndexError): h[:, :, ..., :] with pytest.raises(IndexError): h[..., :, :, :] with pytest.raises(IndexError): h[..., ...] assert h[2:4, ...] == h[2:4, :] def test_basic_projection(): h2 = bh.Histogram( bh.axis.Regular(10, 0, 10), bh.axis.Regular(10, 0, 10), bh.axis.Regular(10, 0, 10), ) h1 = bh.Histogram(bh.axis.Regular(10, 0, 10)) contents = [[2, 2, 2, 3, 4, 5, 6], [1, 2, 2, 3, 2, 1, 2], [-12, 33, 4, 9, 2, 4, 9]] h1.fill(contents[0]) h2.fill(*contents) assert h1 == h2[:, :: bh.sum, :: bh.sum] assert h1 == h2[..., :: bh.sum, :: bh.sum] assert h2.sum(flow=True) == h2[:: bh.sum, :: bh.sum, :: bh.sum] # Python's builtin sum is identical to bh.sum assert bh.sum is sum assert h1 == h2[:, ::sum, ::sum] assert h1 == h2[..., ::sum, ::sum] assert h2.sum(flow=True) == h2[::sum, ::sum, ::sum] # Shortcut assert h1 == h2[:, sum, sum] assert h1 == h2[..., sum, sum] assert h2.sum(flow=True) == h2[sum, sum, sum] def test_slicing_projection(): h1 = bh.Histogram( bh.axis.Regular(10, 0, 10), bh.axis.Regular(10, 0, 10), bh.axis.Regular(10, 0, 10), ) X, Y, Z = np.mgrid[-0.5:10.5:12j, -0.5:10.5:12j, -0.5:10.5:12j] h1.fill(X.ravel(), Y.ravel(), Z.ravel()) assert h1[:: bh.sum, :: bh.sum, :: bh.sum] == 12**3 assert h1[0 : len : bh.sum, 0 : len : bh.sum, 0 : len : bh.sum] == 10**3 assert h1[0 : bh.overflow : bh.sum, 0 : len : bh.sum, :: bh.sum] == 10 * 10 * 12 assert h1[:: bh.sum, 0 : len : bh.sum, :: bh.sum] == 10 * 12 * 12 # make sure nothing was modified assert h1.sum() == 10**3 assert h1.sum(flow=True) == 12**3 h2 = h1[0 : 3 : bh.sum, ...] assert h2[1, 2] == 3 h3 = h2[:, 5 : 7 : bh.sum] assert h3[1] == 6 # Select one bin assert h1[2, :: bh.sum, :: bh.sum] == 12 * 12 # Select one bin assert h1[2, 7, :: bh.sum] == 12 def test_mix_value_with_slice(): h = bh.Histogram( bh.axis.Regular(10, 0, 10), bh.axis.Regular(10, 0, 10), bh.axis.Integer(0, 2) ) vals = np.arange(100).reshape(10, 10, 1) h[:, :, 1:2] = vals assert h[0, 1, True] == 1 assert h[1, 0, True] == 10 assert h[1, 1, True] == 11 assert h[3, 4, False] == 0 assert h[:, :, True].view() == approx(vals[:, :, 0]) assert h[:, :, False].view() == approx(0) def test_mix_value_with_slice_2(): h = bh.Histogram( bh.axis.Regular(10, 0, 10), bh.axis.Regular(10, 0, 10), bh.axis.Integer(0, 2) ) vals = np.arange(100).reshape(10, 10) h[:, :, True] = vals assert h[0, 1, True] == 1 assert h[1, 0, True] == 10 assert h[1, 1, True] == 11 assert h[3, 4, False] == 0 assert h[:, :, True].view() == approx(vals) assert h[:, :, False].view() == approx(0) h2 = h[bh.rebin(2), bh.rebin(5), :] assert h2.shape == approx((5, 2, 2)) def test_one_sided_slice(): h = bh.Histogram(bh.axis.Regular(4, 1, 5)) h.view(True)[:] = 1 assert h[sum] == 6 # 4 (internal bins) + 2 (flow bins) assert h[bh.tag.at(-1) : bh.tag.at(5) : sum] == 6 # keeps underflow, keeps overflow # check that slicing without bh.sum adds removed counts to flow bins assert h[1:3].view(True) == approx([2, 1, 1, 2]) assert h[0::sum] == 5 # removes underflow, keeps overflow assert h[:4:sum] == 5 # removes overflow, keeps underflow assert h[0:4:sum] == 4 # removes underflow and overflow assert h[bh.loc(1) :: sum] == 5 # remove underflow assert h[: bh.loc(5) : sum] == 5 # remove overflow assert h[bh.loc(1) : bh.loc(5) : sum] == 4 # removes underflow and overflow assert h[bh.loc(0) :: sum] == 6 # keep underflow assert h[: bh.loc(10) + 1 : sum] == 6 # keep overflow assert h[bh.loc(0) : bh.loc(10) + 1 : sum] == 6 def test_negative_index_access(): # Negative plain-int indices and slice bounds are normalized relative to the # number of bins on the axis, like Python/NumPy semantics. h = bh.Histogram(bh.axis.Regular(10, 0, 10)) h.fill(range(10)) # Bare integer index already worked, but check it stays consistent assert h[-1] == h[9] assert h[-2] == h[8] # Slice bounds (positional) assert h[1:-2] == h[1:8] assert h[-3:-1] == h[7:9] assert h[-3:] == h[7:] assert h[:-2] == h[:8] # Dict indexing goes through the same path assert h[{0: -2}] == h[8] assert h[{0: slice(1, -2)}] == h[1:8] # Two-axis case to confirm per-axis normalization uses the right length h2 = bh.Histogram(bh.axis.Regular(10, 0, 10), bh.axis.Regular(4, 0, 4)) assert h2[1:-2, :].axes[0].size == h2[1:8, :].axes[0].size assert h2[:, -1] == h2[:, 3] def test_out_of_range_slice_clamps_like_numpy(): # Out-of-range bounds clamp to [0, len] like NumPy, instead of raising or # accidentally pulling in flow bins. h = bh.Histogram(bh.axis.Regular(10, 0, 10)) h.fill(range(10)) a = np.arange(10) for sl in [ slice(-15, None), slice(-15, 5), slice(5, 100), slice(None, 100), slice(-100, 8), slice(2, 100), ]: assert h[sl].axes[0].size == len(a[sl]) def test_empty_slice_raises(): # NumPy returns an empty array; a Boost.Histogram axis cannot have zero # bins, so these raise a clear IndexError rather than a low-level error. h = bh.Histogram(bh.axis.Regular(10, 0, 10)) h.fill(range(10)) for sl in [ slice(5, 2), slice(3, 3), slice(1, -15), slice(None, -15), slice(100, None), ]: with pytest.raises(IndexError): h[sl] # Reversed bounds are also empty under integration (but flow access like # h[3::sum] on a size-3 axis stays valid; see test_single_flow_bin). with pytest.raises(IndexError): h[5:2:sum] def test_repr(): assert repr(bh.loc(2)) == "loc(2)" assert repr(bh.loc(3) + 1) == "loc(3) + 1" assert repr(bh.loc(1) - 2) == "loc(1) - 2" assert repr(bh.underflow) == "underflow" assert repr(bh.underflow + 1) == "underflow + 1" assert repr(bh.underflow - 1) == "underflow - 1" assert repr(bh.overflow) == "overflow" assert repr(bh.overflow + 1) == "overflow + 1" assert repr(bh.overflow - 1) == "overflow - 1" assert repr(bh.rebin(2)) == "rebin(factor=2)" # Was broken in 0.6.1 def test_noflow_slicing(): noflow = {"underflow": False, "overflow": False} h = bh.Histogram( bh.axis.Regular(10, 0, 10), bh.axis.Regular(10, 0, 10, **noflow), bh.axis.Integer(0, 2, **noflow), ) vals = np.arange(100).reshape(10, 10) h[:, :, True] = vals assert h[0, 1, True] == 1 assert h[1, 0, True] == 10 assert h[1, 1, True] == 11 assert h[3, 4, False] == 0 assert h[{0: 3, 1: 4, 2: False}] == 0 assert h[:, :, True].view() == approx(vals) assert h[:, :, False].view() == approx(0) def test_singleflow_slicing(): h = bh.Histogram( bh.axis.Integer(0, 4, underflow=False), bh.axis.Integer(0, 4, overflow=False) ) vals = np.arange(4 * 4).reshape(4, 4) h[:, :] = vals assert h[0, 0] == 0 assert h[0, 1] == 1 assert h[1, 0] == 4 assert h[1, 1] == 5 assert np.asarray(h[:, 1 : 3 : bh.sum]) == approx(vals[:, 1:3].sum(axis=1)) assert np.asarray(h[{1: slice(1, 3, bh.sum)}]) == approx(vals[:, 1:3].sum(axis=1)) assert np.asarray(h[1 : 3 : bh.sum, :]) == approx(vals[1:3, :].sum(axis=0)) def test_set_range_with_scalar(): h = bh.Histogram(bh.axis.Integer(0, 10)) h[2:5] = 42 assert h[1] == 0 assert h[2] == 42 assert h[3] == 42 assert h[4] == 42 assert h[5] == 0 def test_set_range_with_scalar_callable(): h = bh.Histogram(bh.axis.Integer(0, 10)) h[2:len] = 42 assert h[1] == 0 assert h[2] == 42 assert h[3] == 42 assert h[4] == 42 assert h[5] == 42 assert h[bh.overflow] == 0 def test_set_all_with_scalar(): h = bh.Histogram(bh.axis.Integer(0, 10)) h[:] = 42 assert h[0] == 42 assert h[9] == 42 assert h[::sum] == 42 * 10 def test_pick_str_category(): noflow = {"underflow": False, "overflow": False} h = bh.Histogram( bh.axis.Regular(10, 0, 10), bh.axis.Regular(10, 0, 10, **noflow), bh.axis.StrCategory(["on", "off", "maybe"]), ) vals = np.arange(100).reshape(10, 10) h[:, :, bh.loc("on")] = vals assert h[0, 1, bh.loc("on")] == 1 assert h[1, 0, bh.loc("on")] == 10 assert h[1, 1, bh.loc("on")] == 11 assert h[3, 4, bh.loc("maybe")] == 0 assert h[:, :, bh.loc("on")].view() == approx(vals) assert h[{2: bh.loc("on")}].view() == approx(vals) assert h[:, :, bh.loc("off")].view() == approx(0) def test_string_requirement(): h = bh.Histogram( bh.axis.Integer(0, 10), bh.axis.StrCategory(["1", "a", "hello"]), storage=bh.storage.Int64(), ) with pytest.raises(TypeError): h[bh.loc("1"), bh.loc(1)] with pytest.raises(TypeError): h[bh.loc(1), bh.loc(1)] with pytest.raises(TypeError): h[bh.loc("1"), bh.loc("1")] assert h[bh.loc(1), bh.loc("1")] == 0 def test_pick_int_category(): noflow = {"underflow": False, "overflow": False} h = bh.Histogram( bh.axis.Regular(10, 0, 10), bh.axis.Regular(10, 0, 10, **noflow), bh.axis.IntCategory([3, 5, 7, 12, 13]), ) vals = np.arange(100).reshape(10, 10) h[:, :, bh.loc(3)] = vals h[:, :, bh.loc(5)] = vals + 1 h[:, :, 3] = vals + 100 assert h[0, 1, bh.loc(3)] == 1 assert h[1, 0, bh.loc(5)] == 10 + 1 assert h[1, 1, bh.loc(5)] == 11 + 1 assert h[3, 4, bh.loc(7)] == 0 assert h[3, 4, bh.loc(12)] == 134 assert h[:, :, bh.loc(3)].view() == approx(vals) assert h[{2: bh.loc(3)}].view() == approx(vals) assert h[:, :, bh.loc(5)].view() == approx(vals + 1) assert h[:, :, bh.loc(7)].view() == approx(0) @pytest.mark.parametrize( "ax", [bh.axis.Regular(3, 0, 1), bh.axis.Variable([0, 0.3, 0.6, 1])], ids=["regular", "variable"], ) def test_pick_flowbin(ax): w = 1e-2 # e.g. a cross section for a process x = [-0.1, -0.1, 0.1, 0.1, 0.1] y = [-0.1, 0.1, -0.1, -0.1, 0.1] h = bh.Histogram( ax, ax, storage=bh.storage.Weight(), ) h.fill(x, y, weight=w) uf_slice = h[bh.tag.underflow, ...] assert uf_slice.values(flow=True) == approx(np.array([1, 1, 0, 0, 0]) * w) uf_slice = h[..., bh.tag.underflow] assert uf_slice.values(flow=True) == approx(np.array([1, 2, 0, 0, 0]) * w) def test_axes_tuple(): h = bh.Histogram(bh.axis.Regular(10, 0, 1)) assert isinstance(h.axes[:1], bh.axis.AxesTuple) assert isinstance(h.axes[0], bh.axis.Regular) (before,) = h.axes.centers[:1] (after,) = h.axes[:1].centers assert before == approx(after) def test_axes_tuple_Nd(): h = bh.Histogram( bh.axis.Integer(0, 5), bh.axis.Integer(0, 4), bh.axis.Integer(0, 6) ) assert isinstance(h.axes[:2], bh.axis.AxesTuple) assert isinstance(h.axes[1], bh.axis.Integer) b1, b2 = h.axes.centers[1:3] a1, a2 = h.axes[1:3].centers assert b1.flatten() == approx(a1.flatten()) assert b2.flatten() == approx(a2.flatten()) assert b1.ndim == 3 assert a1.ndim == 2 # issue 556 def test_single_flow_bin(): # Flow is removed for category axes unless full sum is used h = bh.Histogram(bh.axis.IntCategory([0, 1, 2])) h.view(True)[:] = 1 assert h[::sum] == 4 assert h[0::sum] == 3 assert h[1::sum] == 2 assert h[2::sum] == 1 # start past the last bin selects no bins -> clear empty-slice error with pytest.raises(IndexError): h[3::sum] assert h[1:2][sum] == 4 h = bh.Histogram(bh.axis.Integer(0, 3)) h.view(True)[:] = 1 assert h[::sum] == 5 assert h[0::sum] == 4 assert h[1::sum] == 3 assert h[2::sum] == 2 assert h[3::sum] == 1 assert h[1:2][sum] == 5 # issue 579 def test_scale_flowbins(): w = 1e-1 x = np.random.normal(loc=0.4, scale=0.4, size=100) h = bh.Histogram(bh.axis.Variable([0, 0.5, 1]), storage=bh.storage.Weight()) h.fill(x, weight=w) ref_value = h.values(flow=True) * 5 scale_value = (h * 5).values(flow=True) assert scale_value == approx(ref_value) def test_add_flowbins(): w = 1e-1 x = np.random.normal(loc=0.4, scale=0.4, size=100) h = bh.Histogram(bh.axis.Variable([0, 0.5, 1]), storage=bh.storage.Weight()) h.fill(x, weight=w) ref_value = h.values(flow=True) + 5 scale_value = (h + 5).values(flow=True) assert scale_value == approx(ref_value) assert (scale_value != 5).any() # issue 737 def test_large_index(): h = bh.Histogram( bh.axis.IntCategory([4, 8, 15, 16, 23, 42, 99_999_001, 1_000_010_020]) ) assert h.axes[0].value(6) == 99_999_001 assert h.axes[0].index(99_999_001) == 6 def test_scaling_slice(): h = bh.Histogram(bh.axis.Regular(3, 0, 3), bh.axis.StrCategory(["a", "b"])) h.fill([1, 1, 2], "a") h.fill([0], "b") h[:, bh.loc("a")] *= 2 assert h[1, 0] == approx(4) assert h[2, 0] == approx(2) assert h[0, 1] == approx(1) def test_scaling_slice_weight(): h = bh.Histogram( bh.axis.Regular(3, 0, 3), bh.axis.StrCategory(["a", "b"]), storage=bh.storage.Weight(), ) h.fill([1, 1, 2], "a") h.fill([0], "b") h[:, bh.loc("a")] *= 2 assert h[1, 0].value == approx(4) assert h[2, 0].value == approx(2) assert h[0, 1].value == approx(1) def test_setting_histogram_mismatch(): h = bh.Histogram(bh.axis.Regular(10, 0, 10), bh.axis.Integer(0, 20)) h[:, 0] = bh.Histogram(bh.axis.Regular(10, 0, 10)) h[0:, 0] = bh.Histogram(bh.axis.Regular(10, 0, 10, underflow=False)) h[:len, 0] = bh.Histogram(bh.axis.Regular(10, 0, 10, overflow=False)) h[0:len, 0] = bh.Histogram( bh.axis.Regular(10, 0, 10, underflow=False, overflow=False) ) with pytest.raises(ValueError, match="Cannot set histogram with underflow"): h[0:, 0] = bh.Histogram(bh.axis.Regular(10, 0, 10)) with pytest.raises(ValueError, match="Cannot set histogram with underflow"): h[:len, 0] = bh.Histogram(bh.axis.Regular(10, 0, 10)) with pytest.raises(ValueError, match="Cannot set histogram with underflow"): h[:, 0] = bh.Histogram(bh.axis.Regular(10, 0, 10, underflow=False)) with pytest.raises(ValueError, match="Cannot set histogram with underflow"): h[:, 0] = bh.Histogram(bh.axis.Regular(10, 0, 10, overflow=False)) with pytest.raises(ValueError, match="Cannot set histogram with underflow"): h[:, 0] = bh.Histogram( bh.axis.Regular(10, 0, 10, underflow=False, overflow=False) ) with pytest.raises(ValueError, match="Cannot set histogram with underflow"): h[0:, 0] = bh.Histogram( bh.axis.Regular(10, 0, 10, underflow=False, overflow=False) ) with pytest.raises(ValueError, match="Cannot set histogram with underflow"): h[:len, 0] = bh.Histogram( bh.axis.Regular(10, 0, 10, underflow=False, overflow=False) ) def test_setting_histogram_slice_not_at_start(): """Regression test: __setitem__ with slices that are not at the start of the index. Previously, h[2,:,:,2] = other_hist would use the wrong axis indices when checking underflow/overflow traits of the value histogram, causing either a ValueError (wrong axis compared) or IndexError (index out of range). """ axis_wUnderflow0 = bh.axis.Regular(10, 0, 1, underflow=True, overflow=True) axis_wUnderflow1 = bh.axis.Regular(5, 0, 5, underflow=True, overflow=True) axis_wUnderflow2 = bh.axis.Regular(7, 0, 3, underflow=True, overflow=True) axis_noUnderflow = bh.axis.Regular(8, -2, 2, underflow=False, overflow=True) h_2D = bh.Histogram(axis_noUnderflow, axis_wUnderflow0) h_2D.fill([-1.1, -0.2, 0.3, 1.4], [0.2, 0.5, 0.3, 0.8]) # Slices in the middle of the index h_middle = bh.Histogram( axis_wUnderflow1, axis_noUnderflow, axis_wUnderflow0, axis_wUnderflow2 ) h_middle[2, :, :, 2] = h_2D # previously raised ValueError # Slices at the end of the index h_last = bh.Histogram( axis_wUnderflow1, axis_wUnderflow2, axis_noUnderflow, axis_wUnderflow0 ) h_last[2, 2, :, :] = h_2D # previously raised IndexError # Slices at the start (sanity check - this always worked) h_first = bh.Histogram( axis_noUnderflow, axis_wUnderflow0, axis_wUnderflow1, axis_wUnderflow2 ) h_first[:, :, 2, 2] = h_2D # Verify that the data was correctly set (use a non-flow slice comparison) assert h_first.view()[:, :, 2, 2] == approx(h_2D.view()) assert h_middle.view()[2, :, :, 2] == approx(h_2D.view()) assert h_last.view()[2, 2, :, :] == approx(h_2D.view()) def test_rebin_groups_no_inplace_modification(): """ Test that rebinning with a groups list does not mutate the input list in-place. This ensures consecutive rebin operations with the same list do not fail. """ h1 = bh.Histogram(bh.axis.Regular(60, 0, 600)) h2 = bh.Histogram(bh.axis.Regular(60, 0, 600)) rebinner = [5] * 12 h3 = h1[bh.rebin(groups=rebinner)] # The original list should remain unchanged assert rebinner == [5] * 12, "rebinner list was mutated in-place" # Second rebin should not raise h4 = h2[bh.rebin(groups=rebinner)] assert h3 == h4 # Issue #1143 (B7) def test_rebin_group_mapping_factor(): # The factor form of the RebinProtocol must produce groups that sum to # the number of bins when the factor divides evenly ax = bh.axis.Regular(10, 0, 10) groups = bh.rebin(2).group_mapping(ax) assert list(groups) == [2] * 5 assert sum(groups) == len(ax) # With a remainder, the leftover bins are not part of any group (they # are merged into overflow, matching the C++ factor rebinning) ax5 = bh.axis.Regular(5, 0, 5) assert list(bh.rebin(2).group_mapping(ax5)) == [2, 2] # The group form of group_mapping must match the C++ factor rebinning h = bh.Histogram(bh.axis.Regular(5, 0, 5)) h.view(flow=True)[:] = [100, 1, 2, 3, 4, 5, 200] hs = h[:: bh.rebin(2)] assert hs.view(flow=True) == approx([100, 3, 7, 205]) def test_rebin_factor_and_axis_raises(): with pytest.raises(ValueError, match="factor cannot be combined"): bh.rebin(2, axis=bh.axis.Regular(2, 0, 4)) with pytest.raises(ValueError, match="factor cannot be combined"): bh.rebin(factor=2, axis=bh.axis.Regular(2, 0, 4)) def test_vectorized_get_basic(): """NumPy integer-array indices gather scattered cells through the buffer.""" h = bh.Histogram( bh.axis.IntCategory(list(range(5))), bh.axis.IntCategory(list(range(6))), bh.axis.Regular(7, 0, 7), ) h.view()[...] = np.arange(5 * 6 * 7).reshape(5, 6, 7) i0 = np.array([0, 2, 4]) i1 = np.array([1, 3, 5]) i2 = np.array([2, 4, 6]) # Full gather (no slice) -> 1D array of values, like view() fancy indexing assert h[i0, i1, i2] == approx(h.view()[i0, i1, i2]) # Arrays plus a trailing slice keep the axis assert h[i0, i1, :] == approx(h.view()[i0, i1, :]) # Arrays plus an ellipsis expand the remaining axes assert h[i0, i1, ...] == approx(h.view()[i0, i1, :]) def test_vectorized_get_mixed_scalar_and_loc(): h = bh.Histogram( bh.axis.StrCategory([str(i) for i in range(4)]), bh.axis.StrCategory([str(i) for i in range(4)]), bh.axis.Regular(5, 0, 5), ) h.view()[...] = np.arange(4 * 4 * 5).reshape(4, 4, 5) i1 = np.array([0, 2]) # Scalar locator + array + slice mix assert h[bh.loc("1"), i1, :] == approx(h.view()[1, i1, :]) assert h[2, i1, :] == approx(h.view()[2, i1, :]) def test_vectorized_get_flow_offset(): """Array indices are non-flow, matching scalar __getitem__ semantics.""" h = bh.Histogram(bh.axis.Regular(5, 0, 5)) h.view(flow=True)[...] = np.arange(7.0) idx = np.array([0, 4]) assert h[idx] == approx(np.array([h[0], h[4]])) def test_vectorized_get_accumulator_storage(): h = bh.Histogram(bh.axis.Regular(5, 0, 5), storage=bh.storage.Weight()) h.fill([0, 0, 1, 2, 3], weight=[1, 2, 3, 4, 5]) idx = np.array([0, 1, 3]) got = h[idx] assert isinstance(got, bh.view.WeightedSumView) assert got.value == approx(h.view()[idx].value) assert got.variance == approx(h.view()[idx].variance) def test_vectorized_get_multicell_storage(): h = bh.Histogram( bh.axis.Regular(4, 0, 4), bh.axis.Regular(4, 0, 4), storage=bh.storage.MultiCell(3), ) h.view()[...] = np.arange(np.prod(h.view().shape)).reshape(h.view().shape) i0 = np.array([1, 2]) i1 = np.array([0, 3]) # The cell index stays the leading dimension of the result assert h[i0, i1] == approx(h.view()[:, i0, i1]) def test_vectorized_get_rejects_unsupported(): h = bh.Histogram(bh.axis.IntCategory([1, 2, 3]), bh.axis.Regular(5, 0, 5)) arr = np.array([0, 1]) with pytest.raises(IndexError, match="rebin, sum, or locator slices"): h[arr, :: bh.rebin(2)] with pytest.raises(IndexError, match="rebin, sum, or locator slices"): h[arr, ::sum] # A categorical pick list cannot be combined with array indexing with pytest.raises(IndexError, match="integer arrays"): h[[0, 1], arr] def test_negative_size_index(): # Issue #1143 (B8a): -size is a valid Python index for bin 0 h = bh.Histogram(bh.axis.Regular(10, 0, 1)) h[0] = 7 assert h[-10] == 7 assert h[-10] == h[0] with pytest.raises(IndexError): h[-11] with pytest.raises(IndexError): h[10] def test_string_index_raises(): # Issue #1143 (B14a): a plain string is not a valid index (use bh.loc) h = bh.Histogram(bh.axis.StrCategory(["a", "b"])) h.fill(["a", "a", "b"]) with pytest.raises(IndexError, match="locator protocol"): h["a"] assert h[bh.loc("a")] == 2 def test_vectorized_get_set_respects_flow_locator(): # A flow locator (bh.underflow/bh.overflow) mixed with an array index on # another axis must select the flow bin, not wrap into the regular range. h = bh.Histogram(bh.axis.Regular(4, 0, 1), bh.axis.Regular(3, 0, 1)) h.view(flow=True)[...] = np.arange(6 * 5).reshape(6, 5) assert h[np.array([0]), bh.underflow] == approx([h.view(flow=True)[1, 0]]) assert h[np.array([0, 1]), bh.overflow] == approx( [h.view(flow=True)[1, 4], h.view(flow=True)[2, 4]] ) h[np.array([0]), bh.underflow] = 999 assert h.view(flow=True)[1, 0] == 999 def test_vectorized_get_flow_locator_out_of_bounds_raises(): h = bh.Histogram( bh.axis.Regular(4, 0, 1, underflow=False), bh.axis.Regular(3, 0, 1) ) with pytest.raises(IndexError, match="no underflow bin"): h[bh.underflow, np.array([0])] def test_underflow_locator_raises_without_underflow_bin(): # Issue: bh.underflow on an axis without underflow silently wrapped to # the last (overflow) bin instead of raising. h = bh.Histogram( bh.axis.Regular(4, 0, 1, underflow=False), bh.axis.Regular(3, 0, 1) ) h.view(flow=True)[...] = np.arange(5 * 5).reshape(5, 5) with pytest.raises(IndexError, match="no underflow bin"): h[bh.underflow, :] def test_loc_below_range_raises_without_underflow_bin_setitem(): # Issue: bh.loc(value_below_range) on an axis without underflow silently # wrote to the overflow bin instead of raising. h = bh.Histogram(bh.axis.Regular(4, 0, 1, underflow=False)) with pytest.raises(IndexError, match="no underflow bin"): h[bh.loc(-5)] = 7 assert h.view(flow=True) == approx([0, 0, 0, 0, 0]) def test_group_rebin_respects_slice_bounds(): # Issue: h[start:stop:bh.rebin(groups=...)] ignored start/stop and # grouped the full axis instead of just the sliced range. h = bh.Histogram(bh.axis.Regular(6, 0, 6)) h.view()[:] = [1, 2, 3, 4, 5, 6] hs = h[1 : 5 : bh.rebin(groups=[2, 2])] assert hs.view() == approx([2 + 3, 4 + 5]) assert hs.axes[0].edges == approx([1, 3, 5]) def test_group_rebin_mismatched_sum_raises(): h = bh.Histogram(bh.axis.Regular(6, 0, 6)) h.view()[:] = [1, 2, 3, 4, 5, 6] with pytest.raises(ValueError, match="sum of the groups"): h[1 : 5 : bh.rebin(groups=[2, 3])] def test_group_rebin_categorical_without_axis_raises(): # Issue: group rebin on a category axis without axis= silently produced # a nonsensical Variable axis instead of a category axis. h = bh.Histogram(bh.axis.IntCategory([1, 2, 3, 4])) h.view()[:] = [10, 20, 30, 40] with pytest.raises(ValueError, match="categorical axis"): h[:: bh.rebin(groups=[2, 2])] def test_group_rebin_categorical_with_axis(): h = bh.Histogram(bh.axis.IntCategory([1, 2, 3, 4])) h.view()[:] = [10, 20, 30, 40] new_axis = bh.axis.IntCategory([1, 3]) hs = h[:: bh.rebin(groups=[2, 2], axis=new_axis)] assert hs.view() == approx([30, 70]) assert hs.axes[0] == new_axis def test_rebin_numpy_integer_factor(): # Issue: bh.rebin(np.int64(2)) was treated as an axis (isinstance(x, int) # is False for NumPy integer scalars), not a factor. r = bh.rebin(np.int64(2)) assert r.factor == 2 assert r.axis is None h = bh.Histogram(bh.axis.Regular(6, 0, 6)) h.view()[:] = [1, 2, 3, 4, 5, 6] hs = h[:: bh.rebin(np.int64(2))] assert hs.view() == approx([3, 7, 11]) def test_rebin_bool_is_not_a_factor(): with pytest.raises(TypeError, match="not a bool"): bh.rebin(True) def test_reduce_command_repr(): assert ( repr(_core.algorithm.slice(0, 1, 3, _core.algorithm.slice_mode.crop)) == "reduce_command(slice(iaxis=0, begin=1, end=3, mode=slice_mode.crop))" ) assert repr(_core.algorithm.crop(0, 1.0, 3.0)) == ( "reduce_command(crop(iaxis=0, lower=1.0, upper=3.0))" ) assert repr(_core.algorithm.shrink(0, 1.0, 3.0)) == ( "reduce_command(shrink(iaxis=0, lower=1.0, upper=3.0))" ) assert repr(_core.algorithm.shrink_and_rebin(0, 1.0, 3.0, 2)) == ( "reduce_command(shrink_and_rebin(iaxis=0, lower=1.0, upper=3.0, merge=2))" ) scikit-hep-boost-histogram-b4c49b0/tests/test_histogram_init.py000066400000000000000000000104311524660700500251010ustar00rootroot00000000000000from __future__ import annotations import pytest from pytest import approx import boost_histogram as bh @pytest.mark.parametrize(("opt", "extent"), [("uo", 2), ("", 0)]) def test_make_regular_1D(opt, extent): hist = bh.Histogram( bh.axis.Regular(3, 2, 5, underflow="u" in opt, overflow="o" in opt) ) assert hist.ndim == 1 assert hist.axes[0].size == 3 assert hist.axes[0].extent == 3 + extent assert hist.axes[0].bin(1) == (3, 4) @pytest.mark.filterwarnings("ignore") def test_shortcuts(): hist = bh.Histogram((1, 2, 3), (10, 0, 1)) assert hist.ndim == 2 for i in range(2): assert isinstance(hist.axes[i], bh.axis.Regular) assert not isinstance(hist.axes[i], bh.axis.Variable) @pytest.mark.filterwarnings("ignore") def test_shortcuts_with_metadata(): with pytest.raises(TypeError): bh.Histogram((1, 2, 3, 4)) with pytest.raises(TypeError): bh.Histogram((1, 2)) with pytest.raises(TypeError): bh.Histogram((1, 2, 3, 4, 5)) with pytest.raises(TypeError): bh.Histogram((1, 2, 3, "this")) @pytest.mark.parametrize(("opt", "extent"), [("uo", 2), ("", 0)]) def test_make_regular_2D(opt, extent): hist = bh.Histogram( bh.axis.Regular(3, 2, 5, underflow="u" in opt, overflow="o" in opt), bh.axis.Regular(5, 1, 6, underflow="u" in opt, overflow="o" in opt), ) assert hist.ndim == 2 assert hist.axes[0].size == 3 assert hist.axes[0].extent == 3 + extent assert hist.axes[0].bin(1) == approx((3, 4)) assert hist.axes[1].size == 5 assert hist.axes[1].extent == 5 + extent assert hist.axes[1].bin(1) == approx((2, 3)) @pytest.mark.parametrize( "storage", [ bh.storage.Int64(), bh.storage.Double(), bh.storage.Unlimited(), bh.storage.Weight(), ], ) def test_make_any_hist(storage): hist = bh.Histogram( bh.axis.Regular(3, 1, 4), bh.axis.Regular(2, 2, 4, underflow=False, overflow=False), bh.axis.Regular(4, 1, 5, circular=True), storage=storage, ) assert hist.ndim == 3 assert hist.axes[0].size == 3 assert hist.axes[0].extent == 5 assert hist.axes[0].bin(1) == approx((2, 3)) assert hist.axes[1].size == 2 assert hist.axes[1].extent == 2 assert hist.axes[1].bin(1) == approx((3, 4)) assert hist.axes[2].size == 4 assert hist.axes[2].extent == 5 assert hist.axes[2].bin(1) == approx((2, 3)) def test_make_any_hist_storage(): assert not isinstance( bh.Histogram(bh.axis.Regular(5, 1, 2), storage=bh.storage.Int64())[0], float ) assert isinstance( bh.Histogram(bh.axis.Regular(5, 1, 2), storage=bh.storage.Double())[0], float ) assert isinstance(bh.Histogram(bh.axis.Regular(5, 1, 2), storage=None)[0], float) def test_issue_axis_bin_swan(): hist = bh.Histogram( bh.axis.Regular(10, 0, 10, metadata="x", transform=bh.axis.transform.sqrt), bh.axis.Regular(10, 0, 1, metadata="y", circular=True), ) b = hist.axes[1].bin(1) assert repr(b) == "(0.1, 0.2)" assert b[0] == approx(0.1) assert b[1] == approx(0.2) assert hist.axes[0].bin(0)[0] == 0 assert hist.axes[0].bin(1)[0] == approx(0.1) assert hist.axes[0].bin(2)[0] == approx(0.4) hist_ax = ( bh.axis.Regular(5, 1, 2, metadata=None), bh.axis.Regular(5, 1, 2, metadata=None, overflow=False, underflow=False), bh.axis.Integer(0, 5, metadata=None), ) hist_storage = ( bh.storage.Double, bh.storage.Unlimited, bh.storage.Int64, bh.storage.AtomicInt64, bh.storage.Weight, ) @pytest.mark.parametrize("ax", hist_ax) @pytest.mark.parametrize("storage", hist_storage) def test_make_selection(ax, storage): histogram = bh.Histogram(ax, storage=storage()) assert isinstance(histogram, bh.Histogram) histogram = bh.Histogram(ax, ax, storage=storage()) assert isinstance(histogram, bh.Histogram) # TODO: Make this test do something useful def test_init_bad_storage(): # Issue #1143 (B14b): clear TypeErrors for invalid storage arguments with pytest.raises(TypeError, match="Only storages allowed"): bh.Histogram(bh.axis.Regular(10, 0, 1), storage=42) with pytest.raises(TypeError, match=r"use Double\(\)"): bh.Histogram(bh.axis.Regular(10, 0, 1), storage=bh.storage.Double) scikit-hep-boost-histogram-b4c49b0/tests/test_histogram_set.py000066400000000000000000000145531524660700500247420ustar00rootroot00000000000000from __future__ import annotations import numpy as np import pytest from pytest import approx import boost_histogram as bh def test_1D_set_bin(): h = bh.Histogram(bh.axis.Regular(10, 0, 1)) h[2] = 2 assert h[2] == 2.0 h[bh.underflow] = 3 assert h[bh.underflow] == 3.0 h[bh.overflow] = 4 assert h[bh.overflow] == 4.0 def test_2d_set_bin(): h = bh.Histogram(bh.axis.Regular(10, 0, 1), bh.axis.Regular(10, 0, 1)) h[2, 4] = 2 assert h[2, 4] == 2.0 h[bh.underflow, 5] = 3 assert h[bh.underflow, 5] == 3.0 h[bh.overflow, bh.overflow] = 4 assert h[bh.overflow, bh.overflow] == 4.0 def test_1d_set_array(): h = bh.Histogram(bh.axis.Regular(10, 0, 1)) h[...] = np.arange(10) assert h.view() == approx(np.arange(10)) h[...] = np.arange(12) assert h.view(flow=True) == approx(np.arange(12)) with pytest.raises(ValueError): h[...] = np.arange(9) with pytest.raises(ValueError): h[...] = np.arange(11) with pytest.raises(ValueError): h[...] = np.arange(13) h[...] = 1 assert h.view() == approx(np.ones(10)) def test_2d_set_array(): h = bh.Histogram(bh.axis.Regular(10, 0, 1), bh.axis.Regular(10, 0, 1)) h[...] = np.arange(10).reshape(-1, 1) assert h.view()[:, 2] == approx(np.arange(10)) h[...] = np.arange(12).reshape(-1, 1) assert h.view(flow=True)[:, 3] == approx(np.arange(12)) with pytest.raises(ValueError): h[...] = np.arange(9).reshape(-1, 1) with pytest.raises(ValueError): h[...] = np.arange(11).reshape(-1, 1) with pytest.raises(ValueError): h[...] = np.arange(13).reshape(-1, 1) h[...] = 1 assert h.view() == approx(np.ones((10, 10))) def test_weighted_set_shortcut(): h = bh.Histogram(bh.axis.Regular(10, 0, 1), storage=bh.storage.Weight()) h[0] = (1, 2) assert h[0].value == 1 assert h[0].variance == 2 h[3:6] = ((3, 4), (5, 6), (7, 8)) assert h[3].value == 3 assert h[3].variance == 4 assert h[4].value == 5 assert h[4].variance == 6 assert h[5].value == 7 assert h[5].variance == 8 @pytest.mark.parametrize( ("storage", "default"), [ (bh.storage.Mean, bh.accumulators.Mean(1.0, 2.0, 3.0)), (bh.storage.WeightedMean, bh.accumulators.WeightedMean(1.0, 2.0, 3.0, 4.0)), (bh.storage.Weight, bh.accumulators.WeightedSum(1.0, 2)), ], ) def test_set_special_dtype(storage, default): h = bh.Histogram( bh.axis.Regular(10, 0, 1), bh.axis.Regular(10, 0, 1), storage=storage() ) arr = np.full((10, 1), default) h[...] = arr assert h.view()[:, 1:2] == approx(arr) arr = np.full((12, 1), default) h[...] = arr assert h.view(flow=True)[:, 2:3] == approx(arr) arr = np.full((10, 10), default) h[...] = arr assert h.view() == approx(arr) arr = np.full((10, 12), default) h[...] = arr assert h.view(flow=True)[1:11, :] == approx(arr) arr = np.full((12, 10), default) h[...] = arr assert h.view(flow=True)[:, 1:11] == approx(arr) arr = np.full((12, 12), default) h[...] = arr assert h.view(flow=True) == approx(arr) arr = np.full((9, 1), default) with pytest.raises(ValueError): h[...] = arr arr = np.full((11, 1), default) with pytest.raises(ValueError): h[...] = arr arr = np.full((13, 1), default) with pytest.raises(ValueError): h[...] = arr with pytest.raises(ValueError): h[...] = 1 with pytest.raises(ValueError): h[1, 1] = 1 def test_set_array_slice_mismatch_not_at_start(): h = bh.Histogram( bh.axis.Regular(3, 0, 3), bh.axis.Regular(4, 0, 4), bh.axis.Regular(5, 0, 5) ) with pytest.raises( ValueError, match=r"Mismatched shapes \(4, 6\) in dimension 2, 6 != 5" ): h[0, :, :] = np.ones((4, 6)) def test_vectorized_set_basic(): """NumPy integer-array indices scatter values through the buffer.""" h = bh.Histogram( bh.axis.IntCategory(list(range(5))), bh.axis.IntCategory(list(range(6))), bh.axis.Regular(7, 0, 7), ) i0 = np.array([0, 2, 4]) i1 = np.array([1, 3, 5]) h[i0, i1, :] = 9.0 assert h.view()[i0, i1, :] == approx(9.0) # Untouched cells remain zero assert h.view()[1, 0, 0] == 0.0 # Per-cell values, broadcasting over the trailing slice vals = np.arange(3 * 7).reshape(3, 7).astype(float) h[i0, i1, :] = vals assert h.view()[i0, i1, :] == approx(vals) def test_vectorized_set_accumulator(): h = bh.Histogram(bh.axis.Regular(5, 0, 5), storage=bh.storage.Weight()) idx = np.array([1, 3]) # The View accepts a trailing (value, variance) dimension h[idx] = np.array([[10.0, 1.0], [20.0, 2.0]]) assert h.view()[idx].value == approx([10.0, 20.0]) assert h.view()[idx].variance == approx([1.0, 2.0]) def test_vectorized_set_multicell(): h = bh.Histogram( bh.axis.Regular(4, 0, 4), bh.axis.Regular(4, 0, 4), storage=bh.storage.MultiCell(3), ) i0 = np.array([1, 2]) i1 = np.array([0, 3]) h[i0, i1] = 7.0 assert h.view()[:, i0, i1] == approx(7.0) def test_set_histogram_with_flow(): # Issue #1143 (B1): np.asarray(value) dropped a Histogram's flow bins h = bh.Histogram(bh.axis.Regular(3, 0, 1)) h2 = bh.Histogram(bh.axis.Regular(3, 0, 1)) h2.view(flow=True)[:] = [100, 1, 2, 3, 200] h[:] = h2 assert h.view(flow=True) == approx([100, 1, 2, 3, 200]) def test_set_histogram_with_flow_2d(): h = bh.Histogram(bh.axis.Regular(2, 0, 1), bh.axis.Regular(2, 0, 1)) h2 = bh.Histogram(bh.axis.Regular(2, 0, 1), bh.axis.Regular(2, 0, 1)) h2.view(flow=True)[...] = np.arange(16).reshape(4, 4) h[:, :] = h2 assert h.view(flow=True) == approx(np.arange(16).reshape(4, 4)) def test_set_histogram_without_flow(): # A flow-less histogram value still sets the inner bins only h = bh.Histogram(bh.axis.Regular(3, 0, 1)) h3 = bh.Histogram(bh.axis.Regular(3, 0, 1, underflow=False, overflow=False)) h3[:] = [1, 2, 3] h[0:3] = h3 assert h.view(flow=True) == approx([0, 1, 2, 3, 0]) def test_set_histogram_flow_mismatch(): # Cannot set flow bins of a slice that does not include them h = bh.Histogram(bh.axis.Regular(3, 0, 1)) h2 = bh.Histogram(bh.axis.Regular(3, 0, 1)) h2.view(flow=True)[:] = [100, 1, 2, 3, 200] with pytest.raises(ValueError): h[0:3] = h2 scikit-hep-boost-histogram-b4c49b0/tests/test_internal_histogram.py000066400000000000000000000313151524660700500257560ustar00rootroot00000000000000from __future__ import annotations import numpy as np import pytest from pytest import approx import boost_histogram as bh methods = (bh.storage.Double, bh.storage.Int64, bh.storage.Unlimited) @pytest.mark.parametrize("dtype", [np.double, np.int_, np.float32]) def test_noncontig_fill(dtype): a = np.array([[0, 0], [1, 1]], dtype=dtype, order="C") b = np.array([[0, 0], [1, 1]], dtype=dtype, order="F") h1 = bh.Histogram(bh.axis.Regular(10, 0, 2)).fill(a[0]) h2 = bh.Histogram(bh.axis.Regular(10, 0, 2)).fill(b[0]) assert h1 == h2 @pytest.mark.parametrize("storage", methods) def test_1D_fill_int(storage): bins = 10 ranges = (0, 1) vals = (0.15, 0.25, 0.25) hist = bh.Histogram(bh.axis.Regular(bins, *ranges), storage=storage()) assert hist.storage_type == storage hist.fill(vals) H = np.array([0, 1, 2, 0, 0, 0, 0, 0, 0, 0]) assert np.asarray(hist) == approx(H) assert hist.view(flow=False) == approx(H) assert hist.view(flow=True)[1:-1] == approx(H) assert hist.axes[0].size == bins assert hist.axes[0].extent == bins + 2 @pytest.mark.parametrize("storage", methods) def test_2D_fill_int(storage): bins = (10, 15) ranges = ((0, 3), (0, 2)) vals = ((0.15, 0.25, 0.25), (0.35, 0.45, 0.45)) hist = bh.Histogram( bh.axis.Regular(bins[0], *ranges[0]), bh.axis.Regular(bins[1], *ranges[1]), storage=storage(), ) assert hist.storage_type == storage hist.fill(*vals) H = np.histogram2d(*vals, bins=bins, range=ranges)[0] assert np.asarray(hist) == approx(H) assert hist.view(flow=True)[1:-1, 1:-1] == approx(H) assert hist.view(flow=False) == approx(H) assert hist.axes[0].size == bins[0] assert hist.axes[0].extent == bins[0] + 2 assert hist.axes[1].size == bins[1] assert hist.axes[1].extent == bins[1] + 2 def test_edges_histogram(): edges = (1, 12, 22, 79) hist = bh.Histogram(bh.axis.Variable(edges), storage=bh.storage.Int64()) vals = (13, 15, 24, 29) hist.fill(vals) bins = np.asarray(hist) assert bins == approx([0, 2, 2]) assert hist.view(flow=True) == approx([0, 0, 2, 2, 0]) assert hist.view(flow=False) == approx([0, 2, 2]) def test_int_histogram(): hist = bh.Histogram(bh.axis.Integer(3, 7), storage=bh.storage.Int64()) vals = (1, 2, 3, 4, 5, 6, 7, 8, 9) hist.fill(vals) bins = np.asarray(hist) assert bins == approx([1, 1, 1, 1]) assert hist.view(flow=False) == approx([1, 1, 1, 1]) assert hist.view(flow=True) == approx([2, 1, 1, 1, 1, 3]) def test_str_categories_histogram(): hist = bh.Histogram( bh.axis.StrCategory(["a", "b", "c"]), storage=bh.storage.Int64() ) vals = ["a", "b", "b", "c"] hist.fill(vals) assert hist[bh.loc("a")] == 1 assert hist[bh.loc("b")] == 2 assert hist[bh.loc("c")] == 1 # Issue 715 def test_select_many(): hist = bh.Histogram( bh.axis.StrCategory(["a", "b"]), bh.axis.StrCategory(["x", "y", "z"]), bh.axis.Regular(10, 0, 1), ) pick_a = hist[bh.loc("a"), ...] with pytest.warns(UserWarning, match="List indexing selection is experimental"): pick_b = pick_a[[bh.loc("x"), bh.loc("y")], ...] with pytest.warns(UserWarning, match="List indexing selection is experimental"): pick = hist[bh.loc("a"), [bh.loc("x"), bh.loc("y")], ...] assert pick_b.axes[0] == pick.axes[0] assert len(pick_b.axes) == len(pick.axes) def test_growing_histogram(): hist = bh.Histogram( bh.axis.Regular(10, 0, 1, growth=True), storage=bh.storage.Int64() ) hist.fill(1.45) assert hist.size == 17 def test_numpy_dd(): h = bh.Histogram( bh.axis.Regular(10, 0, 1), bh.axis.Regular(5, 0, 1), storage=bh.storage.Int64() ) for i in range(10): for j in range(5): x, y = h.axes[0].centers[i], h.axes[1].centers[j] v = i + j * 10 + 1 h.fill([x] * v, [y] * v) h2, x2, y2 = h.to_numpy() h1, (x1, y1) = h.to_numpy(dd=True) assert h1 == approx(h2) assert x1 == approx(x2) assert y1 == approx(y2) def test_numpy_weights(): h = bh.Histogram( bh.axis.Regular(10, 0, 1), bh.axis.Regular(5, 0, 1), storage=bh.storage.Weight() ) for i in range(10): for j in range(5): x, y = h.axes[0].centers[i], h.axes[1].centers[j] v = i + j * 10 + 1 h.fill([x] * v, [y] * v) h2, x2, y2 = h.to_numpy(view=False) h1, (x1, y1) = h.to_numpy(dd=True, view=False) assert h1 == approx(h2) assert x1 == approx(x2) assert y1 == approx(y2) h1, (x1, y1) = h.to_numpy(dd=True, view=False) h2, x2, y2 = h.to_numpy(view=True) assert h1 == approx(h2.value) assert x1 == approx(x2) assert y1 == approx(y2) def test_numpy_flow(): h = bh.Histogram( bh.axis.Regular(10, 0, 1), bh.axis.Regular(5, 0, 1), storage=bh.storage.Int64() ) for i in range(10): for j in range(5): x, y = h.axes[0].centers[i], h.axes[1].centers[j] v = i + j * 10 + 1 h.fill([x] * v, [y] * v) flow_true = h.to_numpy(True)[0][1:-1, 1:-1] flow_false = h.to_numpy(False)[0] assert flow_true == approx(flow_false) view_flow_true = h.view(flow=True) view_flow_false = h.view(flow=False) view_flow_default = h.view() assert view_flow_true[1:-1, 1:-1] == approx(view_flow_false) assert view_flow_default == approx(view_flow_false) def test_numpy_compare(): h = bh.Histogram( bh.axis.Regular(10, 0, 1), bh.axis.Regular(5, 0, 1), storage=bh.storage.Int64() ) xs = [] ys = [] for i in range(10): for j in range(5): x, y = h.axes[0].centers[i], h.axes[1].centers[j] v = i + j * 10 + 1 xs += [x] * v ys += [y] * v h.fill(xs, ys) H, E1, E2 = h.to_numpy() nH, nE1, nE2 = np.histogram2d(xs, ys, bins=(10, 5), range=((0, 1), (0, 1))) assert approx(nH) == H assert approx(nE1) == E1 assert approx(nE2) == E2 def test_project(): h = bh.Histogram( bh.axis.Regular(10, 0, 1), bh.axis.Regular(5, 0, 1), storage=bh.storage.Int64() ) h0 = bh.Histogram(bh.axis.Regular(10, 0, 1), storage=bh.storage.Int64()) h1 = bh.Histogram(bh.axis.Regular(5, 0, 1), storage=bh.storage.Int64()) for x, y in ( (0.3, 0.3), (0.7, 0.7), (0.5, 0.6), (0.23, 0.92), (0.15, 0.32), (0.43, 0.54), ): h.fill(x, y) h0.fill(x) h1.fill(y) assert h.project(0, 1) == h assert h.project(0) == h0 assert h.project(1) == h1 assert np.asarray(h.project(0, 1)) == approx(np.asarray(h)) assert np.asarray(h.project(0)) == approx(np.asarray(h0)) assert np.asarray(h.project(1)) == approx(np.asarray(h1)) def test_sums(): h = bh.Histogram(bh.axis.Regular(4, 0, 1)) h.fill([0.1, 0.2, 0.3, 10]) assert h.sum() == 3 assert h.sum(flow=True) == 4 def test_int_cat_hist(): h = bh.Histogram(bh.axis.IntCategory([1, 2, 3]), storage=bh.storage.Int64()) h.fill(1) h.fill(2) h.fill(3) assert h.view() == approx([1, 1, 1]) assert h.sum() == 3 with pytest.raises(TypeError): h.fill(0.5) @pytest.mark.filterwarnings("ignore:List indexing selection is experimental") def test_int_cat_hist_pick_several(): h = bh.Histogram( bh.axis.IntCategory([1, 2, 7], __dict__={"xval": 5}), storage=bh.storage.Int64() ) h.fill(1, weight=8) h.fill(2, weight=7) h.fill(7, weight=6) assert h.view() == approx(np.array([8, 7, 6])) assert h.sum() == 21 assert h[[0, 2]].values() == approx(np.array([8, 6])) assert h[[2, 0]].values() == approx(np.array([6, 8])) assert h[[1]].values() == approx(np.array([7])) assert h[[bh.loc(1), bh.loc(7)]].values() == approx(np.array([8, 6])) assert h[[bh.loc(7), bh.loc(1)]].values() == approx(np.array([6, 8])) assert h[[bh.loc(2)]].values() == approx(np.array([7])) assert tuple(h[[0, 2]].axes[0]) == (1, 7) assert tuple(h[[2, 0]].axes[0]) == (7, 1) assert tuple(h[[1]].axes[0]) == (2,) assert h.axes[0].xval == 5 assert h[[0]].axes[0].xval == 5 assert h[[0, 1, 2]].axes[0].xval == 5 @pytest.mark.filterwarnings("ignore:List indexing selection is experimental") def test_str_cat_pick_several(): h = bh.Histogram(bh.axis.StrCategory(["a", "b", "c"])) h.fill(["a", "a", "a", "b", "b", "c"], weight=0.25) assert h[[0, 1, 2]].values() == approx(np.array([0.75, 0.5, 0.25])) assert h[[2, 1, 0]].values() == approx(np.array([0.25, 0.5, 0.75])) assert h[[1, 0]].values() == approx(np.array([0.5, 0.75])) assert h[[bh.loc("a"), bh.loc("b"), bh.loc("c")]].values() == approx( np.array([0.75, 0.5, 0.25]) ) assert h[[bh.loc("c"), bh.loc("b"), bh.loc("a")]].values() == approx( np.array([0.25, 0.5, 0.75]) ) assert h[[bh.loc("b"), bh.loc("a")]].values() == approx(np.array([0.5, 0.75])) assert tuple(h[[1, 0]].axes[0]) == ("b", "a") @pytest.mark.filterwarnings("ignore:List indexing selection is experimental") def test_pick_invalid(): h = bh.Histogram(bh.axis.Regular(10, 0, 1)) with pytest.raises(RuntimeError): h[[0, 1]] h = bh.Histogram(bh.axis.Integer(0, 10)) with pytest.raises(RuntimeError): h[[0, 1]] @pytest.mark.filterwarnings("ignore:List indexing selection is experimental") def test_str_cat_pick_dual(): h = bh.Histogram( bh.axis.StrCategory(["a", "b", "c"]), bh.axis.StrCategory(["d", "e", "f"]) ) vals = np.arange(9).reshape(3, 3) h.values()[...] = vals assert h[[0], [0]].values() == approx(vals[[0]][:, [0]]) assert h[[1], [2]].values() == approx(vals[[1]][:, [2]]) assert h[[1], [0, 1]].values() == approx(vals[[1]][:, [0, 1]]) assert h[[0, 1], [1]].values() == approx(vals[[0, 1]][:, [1]]) assert h[[0, 1], [0, 1]].values() == approx(vals[[0, 1]][:, [0, 1]]) assert h[[0, 1], [2, 1]].values() == approx(vals[[0, 1]][:, [2, 1]]) @pytest.mark.filterwarnings("ignore:List indexing selection is experimental") def test_pick_multiaxis(): h = bh.Histogram( bh.axis.StrCategory(["a", "b", "c"]), bh.axis.IntCategory([-5, 0, 10]), bh.axis.Regular(5, 0, 1), bh.axis.StrCategory(["d", "e", "f"]), storage=bh.storage.Int64(), ) h.fill("b", -5, 0.65, "f") h.fill("b", -5, 0.65, "e") mini = h[[bh.loc("b"), 2], [1, bh.loc(-5)], sum, bh.loc("f")] assert mini.ndim == 2 assert tuple(mini.axes[0]) == ("b", "c") assert tuple(mini.axes[1]) == (0, -5) assert h[[1, 2], [0, 1], sum, bh.loc("f")].sum() == 1 assert h[[1, 2], [1, 0], sum, bh.loc("f")].sum() == 1 assert mini.values() == approx(np.array(((0, 1), (0, 0)))) # to_numpy computes its edges in Python now; the C++ helper (which copies the # bin contents too) remains the reference implementation for the NumPy edge # convention, including the nextafter nudge of the upper edge and flow bins. edge_convention_axes = ( bh.axis.Regular(5, 1, 2), bh.axis.Regular(5, 1, 2, underflow=False), bh.axis.Regular(5, 1, 2, overflow=False), bh.axis.Regular(5, 1, 2, underflow=False, overflow=False), bh.axis.Regular(5, 1, 2, growth=True), bh.axis.Regular(5, 1, 2, circular=True), bh.axis.Regular(5, 1, 2, transform=bh.axis.transform.log), bh.axis.Regular(5, 1, 2, transform=bh.axis.transform.sqrt), bh.axis.Regular(5, 1, 2, transform=bh.axis.transform.Pow(0.5)), bh.axis.Variable([1, 2, 5, 8]), bh.axis.Variable([1, 2, 5, 8], underflow=False), bh.axis.Variable([1, 2, 5, 8], overflow=False), bh.axis.Variable([1, 2, 5, 8], underflow=False, overflow=False), bh.axis.Variable([1, 2, 5, 8], growth=True), bh.axis.Variable([1, 2, 5, 8], circular=True), bh.axis.Integer(-2, 3), bh.axis.Integer(-2, 3, underflow=False), bh.axis.Integer(-2, 3, overflow=False), bh.axis.Integer(-2, 3, underflow=False, overflow=False), bh.axis.Integer(-2, 3, growth=True), bh.axis.Integer(-2, 3, circular=True), bh.axis.IntCategory([1, 3, 5]), bh.axis.IntCategory([1, 3, 5], overflow=False), bh.axis.IntCategory([1, 3, 5], growth=True), bh.axis.StrCategory(["a", "b"]), bh.axis.StrCategory(["a", "b"], overflow=False), bh.axis.StrCategory(["a", "b"], growth=True), bh.axis.Boolean(), ) @pytest.mark.parametrize("flow", [False, True]) @pytest.mark.parametrize("axis", edge_convention_axes, ids=repr) def test_to_numpy_edges_match_cpp_helper(axis, flow): h = bh.Histogram(axis, bh.axis.Regular(3, 0, 1)) _, *cpp_edges = h._hist.to_numpy(flow) _, *py_edges = h.to_numpy(flow) assert len(cpp_edges) == len(py_edges) == 2 for cpp_e, py_e in zip(cpp_edges, py_edges, strict=True): assert cpp_e.dtype == py_e.dtype assert cpp_e == approx(py_e) scikit-hep-boost-histogram-b4c49b0/tests/test_minihist_title.py000066400000000000000000000121741524660700500251140ustar00rootroot00000000000000# The point of this test is to make sure that the infrastructure for supporting # custom attributes, like title in Hist, is working. from __future__ import annotations import pytest import boost_histogram as bh # First, make a new family to identify your library CUSTOM_FAMILY = object() # Add named axes class NamedAxesTuple(bh.axis.AxesTuple): __slots__ = () def _get_index_by_name(self, name): if not isinstance(name, str): return name for i, ax in enumerate(self): if ax.name == name: return i raise KeyError(f"{name} not found in axes") def __getitem__(self, item): if isinstance(item, slice): item = slice( self._get_index_by_name(item.start), self._get_index_by_name(item.stop), self._get_index_by_name(item.step), ) else: item = self._get_index_by_name(item) return super().__getitem__(item) @property def name(self): """ The names of the axes. May be empty strings. """ return tuple(ax.name for ax in self) @name.setter def name(self, values): for ax, val in zip(self, values, strict=False): ax._ax.raw_metadata["name"] = f"test: {val}" # When you subclass Histogram or an Axes, you should register your family so # boost-histogram will know what to convert C++ objects into. class AxesMixin: __slots__ = () # Only required for placing the Mixin first def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) @property def name(self): """ Get the name for the Regular axis """ return self._ax.raw_metadata.get("name", "") # The order of the mixin is important here - it must be first # if it needs to override bh.axis.Regular, otherwise, last is simpler, # as it doesn't need to forward __init_subclass__ kwargs then. class Regular(bh.axis.Regular, AxesMixin, family=CUSTOM_FAMILY): __slots__ = () def __init__(self, bins, start, stop, name): super().__init__(bins, start, stop) self._ax.raw_metadata["name"] = name class Integer(AxesMixin, bh.axis.Integer, family=CUSTOM_FAMILY): __slots__ = () def __init__(self, start, stop, name): super().__init__(start, stop) self._ax.raw_metadata["name"] = name class CustomHist(bh.Histogram, family=CUSTOM_FAMILY): def _generate_axes_(self): return NamedAxesTuple(self._axis(i) for i in range(self.ndim)) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) valid_names = [ax.name for ax in self.axes if ax.name] if len(valid_names) != len(set(valid_names)): msg = f"{self.__class__.__name__} instance cannot contain axes with duplicated names" raise KeyError(msg) def test_hist_creation(): hist_1 = CustomHist(Regular(10, 0, 1, name="a"), Integer(0, 4, name="b")) assert hist_1.axes[0].name == "a" assert hist_1.axes[1].name == "b" hist_2 = CustomHist(Regular(10, 0, 1, name=""), Regular(20, 0, 4, name="")) assert not hist_2.axes[0].name assert not hist_2.axes[1].name with pytest.raises(KeyError): CustomHist(Regular(10, 0, 1, name="a"), Regular(20, 0, 4, name="a")) def test_hist_index(): hist_1 = CustomHist(Regular(10, 0, 1, name="a"), Regular(20, 0, 4, name="b")) assert hist_1.axes[0].name == "a" assert hist_1.axes[1].name == "b" def test_hist_convert(): hist_1 = CustomHist(Regular(10, 0, 1, name="a"), Integer(0, 4, name="b")) hist_bh = bh.Histogram(hist_1) assert type(hist_bh.axes[0]) is bh.axis.Regular assert type(hist_bh.axes[1]) is bh.axis.Integer assert hist_bh.axes[0].name == "a" assert hist_bh.axes[1].name == "b" hist_2 = CustomHist(hist_bh) assert type(hist_2.axes[0]) is Regular assert type(hist_2.axes[1]) is Integer assert hist_2.axes[0].name == "a" assert hist_2.axes[1].name == "b" # Just verify no-op status hist_3 = CustomHist(hist_1) assert type(hist_3.axes[0]) is Regular assert type(hist_3.axes[1]) is Integer assert hist_3.axes[0].name == "a" assert hist_3.axes[1].name == "b" def test_access(): hist = CustomHist(Regular(10, 0, 1, name="a"), Regular(20, 0, 4, name="b")) assert hist.axes["a"] == hist.axes[0] assert hist.axes["b"] == hist.axes[1] from_bh = bh.Histogram(bh.axis.Regular(10, 0, 1), bh.axis.Regular(20, 0, 4)) from_bh.axes.name = "a", "b" hist_conv = CustomHist(from_bh) assert hist_conv.axes["a"] == hist_conv.axes[0] assert hist_conv.axes["b"] == hist_conv.axes[1] def test_hist_name_set(): hist_1 = CustomHist(Regular(10, 0, 1, name="a"), Regular(20, 0, 4, name="b")) hist_1.axes.name = ("c", "d") assert hist_1.axes.name == ("test: c", "test: d") with pytest.raises(AttributeError): hist_1.axes[0].name = "a" hist_1.axes.label = ("one", "two") assert hist_1.axes.label == ("one", "two") with pytest.raises(ValueError): hist_1.axes.label = ("one",) with pytest.raises(ValueError): hist_1.axes.label = ("one", "two", "three") scikit-hep-boost-histogram-b4c49b0/tests/test_numpy_interface.py000066400000000000000000000252351524660700500252610ustar00rootroot00000000000000from __future__ import annotations import copy import numpy as np import pytest from pytest import approx import boost_histogram as bh inputs_1d = ( [1, 2, 3, 4, 3, 4, 5, 10, 9, 11, 21, -2], [ 0.27237556, 0.72020987, 0.75204098, -0.29265003, -2.67332888, 0.68420365, -0.60629843, -0.6375687, -1.00017927, -0.07707552, ], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], ) opts = ( {}, {"bins": 10}, {"bins": "auto"}, {"range": (0, 5), "bins": 30}, {"range": np.array((0, 5), dtype=float), "bins": np.int32(30)}, {"range": np.array((0, 3), dtype=np.double), "bins": np.uint32(10)}, {"range": np.array((0, 10), dtype=int), "bins": np.int8(30)}, {"bins": [0, 1, 1.2, 1.3, 4, 21]}, ) @pytest.mark.parametrize("a", inputs_1d) @pytest.mark.parametrize("opt", opts) def test_histogram1d(a, opt): v = np.array(a) h1, e1 = np.histogram(v, **opt) h2, e2 = bh.numpy.histogram(v, **opt) assert e1 == approx(e2) assert h1 == approx(h2) opt = copy.deepcopy(opt) opt["density"] = True h1, e1 = np.histogram(v, **opt) h2, e2 = bh.numpy.histogram(v, **opt) assert e1 == approx(e2) assert h1 == approx(h2) @pytest.mark.parametrize("a", inputs_1d) @pytest.mark.parametrize("opt", opts) def test_histogram1d_object(a, opt): bh_opt = copy.deepcopy(opt) bh_opt["histogram"] = bh.Histogram v = np.array(a) h1, e1 = np.histogram(v, **opt) bh_h2 = bh.numpy.histogram(v, **bh_opt) h2, e2 = bh_h2.to_numpy() assert e1 == approx(e2) assert h1 == approx(h2) # Ensure reducible assert bh_h2[:5].values() == approx(h1[:5]) opt = copy.deepcopy(opt) opt["density"] = True bh_opt = copy.deepcopy(bh_opt) bh_opt["density"] = True with pytest.raises(KeyError): bh_h2 = bh.numpy.histogram(v, **bh_opt) def test_histogram2d(): x = np.array([0.3, 0.3, 0.1, 0.8, 0.34, 0.03, 0.32, 0.65]) y = np.array([0.4, 0.5, 0.22, 0.65, 0.32, 0.01, 0.23, 1.98]) h1, e1x, e1y = np.histogram2d(x, y) h2, e2x, e2y = bh.numpy.histogram2d(x, y) assert e1x == approx(e2x) assert e1y == approx(e2y) assert h1 == approx(h2) h1, e1x, e1y = np.histogram2d(x, y, density=True) h2, e2x, e2y = bh.numpy.histogram2d(x, y, density=True) assert e1x == approx(e2x) assert e1y == approx(e2y) assert h1 == approx(h2) def test_histogram2d_object(): x = np.array([0.3, 0.3, 0.1, 0.8, 0.34, 0.03, 0.32, 0.65]) y = np.array([0.4, 0.5, 0.22, 0.65, 0.32, 0.01, 0.23, 1.98]) h1, e1x, e1y = np.histogram2d(x, y) bh_h2 = bh.numpy.histogram2d(x, y, histogram=bh.Histogram) h2, e2x, e2y = bh_h2.to_numpy() assert e1x == approx(e2x) assert e1y == approx(e2y) assert h1 == approx(h2) with pytest.raises(KeyError): bh.numpy.histogram2d(x, y, density=True, histogram=bh.Histogram) def test_histogramdd(): x = np.array([0.3, 0.3, 0.1, 0.8, 0.34, 0.03, 0.32, 0.65]) y = np.array([0.4, 0.5, 0.22, 0.65, 0.32, 0.01, 0.23, 1.98]) z = np.array([0.5, 0.7, 0.0, 0.65, 0.72, 0.01, 0.3, 1.4]) h1, (e1x, e1y, e1z) = np.histogramdd([x, y, z]) h2, (e2x, e2y, e2z) = bh.numpy.histogramdd([x, y, z]) assert e1x == approx(e2x) assert e1y == approx(e2y) assert e1z == approx(e2z) assert h1 == approx(h2) h1, (e1x, e1y, e1z) = np.histogramdd([x, y, z], density=True) h2, (e2x, e2y, e2z) = bh.numpy.histogramdd([x, y, z], density=True) assert e1x == approx(e2x) assert e1y == approx(e2y) assert e1z == approx(e2z) assert h1 == approx(h2) def test_histogramdd_object(): x = np.array([0.3, 0.3, 0.1, 0.8, 0.34, 0.03, 0.32, 0.65]) y = np.array([0.4, 0.5, 0.22, 0.65, 0.32, 0.01, 0.23, 1.98]) z = np.array([0.5, 0.7, 0.0, 0.65, 0.72, 0.01, 0.3, 1.4]) h1, (e1x, e1y, e1z) = np.histogramdd([x, y, z]) bh_h2 = bh.numpy.histogramdd([x, y, z], histogram=bh.Histogram) h2, (e2x, e2y, e2z) = bh_h2.to_numpy(dd=True) assert e1x == approx(e2x) assert e1y == approx(e2y) assert e1z == approx(e2z) assert h1 == approx(h2) with pytest.raises(KeyError): bh.numpy.histogramdd([x, y, z], density=True, histogram=bh.Histogram) def test_histogram_weights(): x = np.array([0.3, 0.3, 0.1, 0.8, 0.34, 0.03, 0.32, 0.65]) weights = np.array([0.4, 0.5, 0.22, 0.65, 0.32, 0.01, 0.23, 1.98]) h1, edges = np.histogram(x, weights=weights) bh_h1, bh_edges = bh.numpy.histogram(x, weights=weights) assert bh_h1 == approx(h1) assert bh_edges == approx(edges) def test_histogram_nans(): x = np.array([0, 1, 2, 3, np.nan]) with pytest.raises(ValueError): np.histogram(x) with pytest.raises(ValueError): bh.numpy.histogram(x) def test_histogram_all_zeros(): x = np.array([0, 0, 0, 0, 0, 0]) h1, edges = np.histogram(x) bh_h1, bh_edges = bh.numpy.histogram(x) assert bh_h1 == approx(h1) assert bh_edges == approx(edges) def test_histogram_all_ones(): x = np.array([1, 1, 1, 1, 1, 1]) h1, edges = np.histogram(x) bh_h1, bh_edges = bh.numpy.histogram(x) assert bh_h1 == approx(h1) assert bh_edges == approx(edges) def test_histogram_does_not_mutate_input_edges(): edges = np.array([0.0, 1.0, 2.0]) original = edges.copy() bh.numpy.histogram([0.5, 1.5], bins=edges) assert edges == approx(original) def test_histogramdd_does_not_mutate_input_edges(): x = np.array([0.3, 0.3, 0.1, 0.8]) y = np.array([0.4, 0.5, 0.22, 0.65]) edges_x = np.array([0.0, 0.5, 1.0]) edges_y = np.array([0.0, 0.25, 0.5, 1.0]) original_x = edges_x.copy() original_y = edges_y.copy() bh.numpy.histogramdd((x, y), bins=(edges_x, edges_y)) assert edges_x == approx(original_x) assert edges_y == approx(original_y) @pytest.mark.parametrize("bad_bins", [(2,), (2, 2, 2), (2, 2, 2, 2)]) def test_histogramdd_mismatched_bins(bad_bins): x = np.array([0.3, 0.3, 0.1, 0.8]) y = np.array([0.4, 0.5, 0.22, 0.65]) with pytest.raises(ValueError, match="dimension of bins"): np.histogramdd((x, y), bins=bad_bins) with pytest.raises(ValueError, match="dimension of bins"): bh.numpy.histogramdd((x, y), bins=bad_bins) @pytest.mark.parametrize( "bad_range", [((0, 1),), ((0, 1), (0, 1), (0, 1)), ((0, 1), (0, 1), (0, 1), (0, 1))] ) def test_histogramdd_mismatched_range(bad_range): x = np.array([0.3, 0.3, 0.1, 0.8]) y = np.array([0.4, 0.5, 0.22, 0.65]) with pytest.raises(ValueError, match="range argument"): np.histogramdd((x, y), bins=2, range=bad_range) with pytest.raises(ValueError, match="range argument"): bh.numpy.histogramdd((x, y), bins=2, range=bad_range) @pytest.mark.parametrize("empty", [[], np.array([]), np.array([], dtype=np.int64)]) @pytest.mark.parametrize("opt", [{}, {"bins": 5}, {"range": (0, 4)}]) def test_histogram_empty(empty, opt): h1, e1 = np.histogram(empty, **opt) h2, e2 = bh.numpy.histogram(empty, **opt) assert e1 == approx(e2) assert h1 == approx(h2) def test_histogramdd_empty(): h1, (e1,) = np.histogramdd((np.array([]),), bins=(4,)) h2, (e2,) = bh.numpy.histogramdd((np.array([]),), bins=(4,)) assert e1 == approx(e2) assert h1 == approx(h2) @pytest.mark.parametrize("dtype", [np.float64, np.int64]) def test_histogramdd_1d_array(dtype): v = np.array([1, 2, 3, 4, 3, 4, 5, 10, 9, 11, 21, -2], dtype=dtype) h1, (e1,) = np.histogramdd(v) h2, (e2,) = bh.numpy.histogramdd(v) assert e1 == approx(e2) assert h1 == approx(h2) def test_histogramdd_1d_list(): v = [1.0, 2.0, 3.0, 4.0] h1, (e1,) = np.histogramdd(v, bins=3) h2, (e2,) = bh.numpy.histogramdd(v, bins=3) assert e1 == approx(e2) assert h1 == approx(h2) @pytest.mark.parametrize("bins", [np.array([0.0, 0.5, 1.0, 2.0]), [0.0, 0.5, 1.0, 2.0]]) def test_histogram2d_shared_edges(bins): x = np.array([0.3, 0.3, 0.1, 0.8, 0.34, 0.03, 0.32, 0.65]) y = np.array([0.4, 0.5, 0.22, 0.65, 0.32, 0.01, 0.23, 1.98]) h1, e1x, e1y = np.histogram2d(x, y, bins=bins) h2, e2x, e2y = bh.numpy.histogram2d(x, y, bins=bins) assert e1x == approx(e2x) assert e1y == approx(e2y) assert h1 == approx(h2) def test_histogram_reversed_range(): x = np.array([0.3, 0.3, 0.1, 0.8]) with pytest.raises(ValueError, match="max must be larger than min"): np.histogram(x, bins=4, range=(1, 0)) with pytest.raises(ValueError, match="max must be larger than min"): bh.numpy.histogram(x, bins=4, range=(1, 0)) def test_histogramdd_reversed_range(): x = np.array([0.3, 0.3, 0.1, 0.8]) y = np.array([0.4, 0.5, 0.22, 0.65]) with pytest.raises(ValueError, match="max must be larger than min"): np.histogramdd((x, y), bins=4, range=((0, 1), (1, 0))) with pytest.raises(ValueError, match="max must be larger than min"): bh.numpy.histogramdd((x, y), bins=4, range=((0, 1), (1, 0))) def test_histogram_nonfinite_range(): x = np.array([0.3, 0.3, 0.1, 0.8]) with pytest.raises(ValueError, match="is not finite"): np.histogram(x, bins=4, range=(0, np.inf)) with pytest.raises(ValueError, match="is not finite"): bh.numpy.histogram(x, bins=4, range=(0, np.inf)) def test_histogram_empty_range(): x = np.array([0.3, 0.3, 0.1, 0.8]) h1, e1 = np.histogram(x, bins=4, range=(1, 1)) h2, e2 = bh.numpy.histogram(x, bins=4, range=(1, 1)) assert e1 == approx(e2) assert h1 == approx(h2) @pytest.mark.parametrize("opt", [{}, {"density": True}]) def test_histogram_weights_and_density(opt): x = np.array([0.3, 0.3, 0.1, 0.8, 0.34, 0.03, 0.32, 0.65]) weights = np.array([0.4, 0.5, 0.22, 0.65, 0.32, 0.01, 0.23, 1.98]) h1, e1 = np.histogram(x, bins=7, weights=weights, **opt) h2, e2 = bh.numpy.histogram(x, bins=7, weights=weights, **opt) assert e1 == approx(e2) assert h1 == approx(h2) h1, e1x, e1y = np.histogram2d(x, x, bins=3, weights=weights, **opt) h2, e2x, e2y = bh.numpy.histogram2d(x, x, bins=3, weights=weights, **opt) assert e1x == approx(e2x) assert e1y == approx(e2y) assert h1 == approx(h2) @pytest.mark.xfail( reason="A Regular axis finds the bin from the range, so a value exactly on " "an interior edge can land one bin lower than in NumPy, which compares the " "value against the linspace edges." ) @pytest.mark.parametrize(("lo", "hi", "n"), [(-3, 7, 13), (0, 1, 7), (0, 3, 11)]) def test_histogram_values_on_edges(lo, hi, n): v = np.linspace(lo, hi, n + 1) h1, e1 = np.histogram(v, bins=n, range=(lo, hi)) h2, e2 = bh.numpy.histogram(v, bins=n, range=(lo, hi)) assert e1.tolist() == e2.tolist() assert h1.tolist() == h2.tolist() scikit-hep-boost-histogram-b4c49b0/tests/test_pandaslike.py000066400000000000000000000021231524660700500241730ustar00rootroot00000000000000from __future__ import annotations import numpy as np import boost_histogram as bh # This is not a reasonable approximation of Pandas, but rather a test for an # arbitrary convertible object (but if this works, so does Pandas) class Seriesish: def __init__(self, array): self.array = np.asarray(array) def __array__(self, dtype=None, *, copy=None): # Copy is new in NumPy 2.0 kw = {} if copy is not None: kw["copy"] = copy return np.asarray(self.array, dtype=dtype, **kw) def test_setting_weighted_profile_convertable(): h = bh.Histogram(bh.axis.Regular(10, 0, 10), storage=bh.storage.WeightedMean()) data = Seriesish([0.3, 0.3, 0.4, 1.2, 1.6]) samples = Seriesish([1, 2, 3, 4, 4]) weights = Seriesish([1, 1, 1, 1, 2]) h.fill(data, sample=samples, weight=weights) assert h[0] == bh.accumulators.WeightedMean( sum_of_weights=3, sum_of_weights_squared=3, value=2, variance=1 ) assert h[1] == bh.accumulators.WeightedMean( sum_of_weights=3, sum_of_weights_squared=5, value=4, variance=0 ) scikit-hep-boost-histogram-b4c49b0/tests/test_pbt.py000066400000000000000000000022311524660700500226450ustar00rootroot00000000000000from __future__ import annotations import numpy as np import pytest from pytest import approx import boost_histogram as bh hypothesis = pytest.importorskip("hypothesis") nst = pytest.importorskip("hypothesis.extra.numpy") hypothesis.settings.register_profile("ci", deadline=None) hypothesis.settings.load_profile("ci") @hypothesis.given( nst.arrays( float, (4,), elements={ "min_value": 1, "max_value": 100, "exclude_min": True, "allow_nan": False, }, ), nst.arrays( float, (4,), elements={"min_value": -100, "max_value": 100, "allow_nan": False} ), nst.arrays( float, (4,), elements={ "min_value": 0, "max_value": 100, "allow_nan": False, "exclude_min": True, }, ), ) def test_variance_setting(cnt, val, var): h = bh.Histogram(bh.axis.Regular(4, 0, 1), storage=bh.storage.Mean()) h[...] = np.stack([cnt, val, var * cnt], axis=-1) assert h.counts() == approx(cnt) assert h.values() == approx(val) assert h.variances() == approx(var) # , abs=1e-3, rel=1e-3) scikit-hep-boost-histogram-b4c49b0/tests/test_pickle.py000066400000000000000000000220511524660700500233310ustar00rootroot00000000000000from __future__ import annotations import copy import ctypes import math import sys from pickle import dumps, loads import numpy as np import pytest from pytest import approx import boost_histogram as bh ftype = ctypes.CFUNCTYPE(ctypes.c_double, ctypes.c_double) def python_convert(x): return ftype(x) def pickle_roundtrip_protocol_2(x): return loads(dumps(x, 2)) def pickle_roundtrip_protocol_highest(x): return loads(dumps(x, -1)) @pytest.fixture( params=( pickle_roundtrip_protocol_2, pickle_roundtrip_protocol_highest, copy.copy, copy.deepcopy, ) ) def copy_fn(request): return request.param @pytest.mark.parametrize( "opts", [{}, {"growth": True}, {"underflow": True, "overflow": True}] ) def test_options(copy_fn, opts): orig = bh.axis.Traits(**opts) new = copy_fn(orig) assert new == orig @pytest.mark.parametrize( ("accum", "args"), [ (bh.accumulators.Sum, (12,)), (bh.accumulators.WeightedSum, (1.5, 2.5)), (bh.accumulators.Mean, (5, 1.5, 2.5)), (bh.accumulators.WeightedMean, (1.5, 2.5, 3.5, 4.5)), ], ) def test_accumulators(accum, args, copy_fn): orig = accum(*args) new = copy_fn(orig) assert new == orig axes_creations = [ (bh.axis.Regular, (4, 2, 4), {"underflow": False, "overflow": False}), (bh.axis.Regular, (4, 2, 4), {}), (bh.axis.Regular, (4, 2, 4), {"growth": True}), (bh.axis.Regular, (4, 2, 4), {"transform": bh.axis.transform.log}), (bh.axis.Regular, (4, 2, 4), {"transform": bh.axis.transform.sqrt}), (bh.axis.Regular, (4, 2, 4), {"transform": bh.axis.transform.Pow(0.5)}), (bh.axis.Regular, (4, 2, 4), {"circular": True}), (bh.axis.Variable, ([1, 2, 3, 4],), {}), (bh.axis.Variable, ([1, 2, 3, 4],), {"circular": True}), (bh.axis.Integer, (1, 4), {}), (bh.axis.Integer, (1, 4), {"circular": True}), (bh.axis.IntCategory, ([1, 2, 3],), {}), (bh.axis.IntCategory, ([1, 2, 3],), {"growth": True}), (bh.axis.StrCategory, (["1", "2", "3"],), {}), (bh.axis.StrCategory, (["1", "2", "3"],), {"growth": True}), ] @pytest.mark.parametrize(("axis", "args", "opts"), axes_creations) def test_axes(axis, args, opts, copy_fn): orig = axis(*args, **opts) new = copy_fn(orig) assert new == orig assert new.centers == approx(orig.centers) @pytest.mark.parametrize(("axis", "args", "opts"), axes_creations) def test_metadata_str(axis, args, opts, copy_fn): orig = axis(*args, **opts) orig.metadata = "foo" new = copy_fn(orig) assert new.metadata == orig.metadata new.metadata = orig.metadata assert new == orig assert new.centers == approx(orig.centers) # Special test: Deepcopy should change metadata id, copy should not def test_compare_copy_axis(metadata): orig = bh.axis.Regular(4, 0, 2, metadata=metadata) new = copy.copy(orig) dnew = copy.deepcopy(orig) assert orig.metadata is new.metadata assert orig.metadata == dnew.metadata if metadata is not copy.copy(metadata): assert orig.metadata is not dnew.metadata # Special test: Deepcopy should change metadata id, copy should not def test_compare_copy_hist(metadata): orig = bh.Histogram(bh.axis.Regular(4, 0, 2, metadata=metadata)) new = copy.copy(orig) dnew = copy.deepcopy(orig) assert orig.axes[0].metadata is new.axes[0].metadata assert orig.axes[0].metadata == dnew.axes[0].metadata if metadata is not copy.copy(metadata): assert orig.axes[0].metadata is not dnew.axes[0].metadata @pytest.mark.parametrize(("axis", "args", "opts"), axes_creations) def test_metadata_any(axis, args, opts, copy_fn): orig = axis(*args, **opts) orig.metadata = (1, 2, 3) new = copy_fn(orig) assert new.metadata == orig.metadata new.metadata = orig.metadata assert new == orig @pytest.mark.benchmark(group="histogram-pickling") @pytest.mark.parametrize( ("storage", "extra"), [ (bh.storage.AtomicInt64, {}), (bh.storage.Int64, {}), (bh.storage.Unlimited, {}), (bh.storage.Unlimited, {"weight"}), (bh.storage.Double, {"weight"}), (bh.storage.Weight, {"weight"}), (bh.storage.Mean, {"sample"}), (bh.storage.WeightedMean, {"weight", "sample"}), (bh.storage.MultiCell, {"MultiCell"}), ], ) def test_storage(benchmark, copy_fn, storage, extra): n = 1000 init_list = [] if extra == {"MultiCell"}: init_list.append(3) hist = bh.Histogram(bh.axis.Integer(0, n), storage=storage(*init_list)) x = np.arange(2 * (n + 2)) % (n + 2) - 1 if extra == {}: hist.fill(x) elif extra == {"weight"}: hist.fill(x, weight=np.arange(2 * n + 4) + 1) elif extra == {"sample"}: hist.fill(x, sample=np.arange(2 * n + 4) + 1) elif extra == {"MultiCell"}: hist.fill(x, weight=np.reshape(np.arange(3 * (2 * n + 4)) + 1, (2 * n + 4, 3))) else: hist.fill(x, weight=np.arange(2 * n + 4) + 1, sample=np.arange(2 * n + 4) + 1) new = benchmark(copy_fn, hist) assert np.asarray(hist.view(True)) == approx(np.asarray(new.view(True))) assert new == hist def test_histogram_regular(copy_fn): hist = bh.Histogram(bh.axis.Regular(4, 1, 2), bh.axis.Regular(8, 3, 6)) new = copy_fn(hist) assert hist == new def test_histogram_fancy(copy_fn): hist = bh.Histogram(bh.axis.Regular(4, 1, 2), bh.axis.Integer(0, 6)) new = copy_fn(hist) assert hist == new def test_histogram_metadata(copy_fn, metadata): hist = bh.Histogram(bh.axis.Regular(4, 1, 2, metadata=metadata)) new = copy_fn(hist) assert hist == new @pytest.mark.skipif( sys.implementation.name == "pypy", reason="Not remotely supported on PyPY, hangs forever", ) @pytest.mark.skipif( sys.implementation.name == "graalpy", reason="Not supported, throws exception", ) @pytest.mark.parametrize("mod", [np, math]) def test_pickle_transforms(mod, copy_fn): ax1 = bh.axis.Regular( 100, 1, 100, transform=bh.axis.transform.Function(mod.log, mod.exp, convert=python_convert), ) ax2 = copy_fn(ax1) ax3 = bh.axis.Regular(100, 1, 100, transform=bh.axis.transform.log) assert ax1 == ax2 assert ax1.centers == approx(ax2.centers) assert ax2.centers == approx(ax3.centers) def test_hist_axes_reference(copy_fn): h = bh.Histogram(bh.axis.Regular(10, 0, 1, metadata=1)) h.axes[0].metadata = 2 h2 = copy_fn(h) assert h2._hist is not h._hist assert h2.axes[0] is not h.axes[0] assert h2.axes[0].metadata == 2 h.axes[0].metadata = 3 assert h2._axis(0).metadata == 2 assert h2.axes[0].metadata == 2 def test_hist_axes_reference_arbitrary(copy_fn): h = bh.Histogram(bh.axis.Regular(10, 0, 1)) h.other = 2 h2 = copy_fn(h) assert h2._hist is not h._hist assert h2.other == 2 h.other = 3 assert h2.other == 2 def test_hist_reference_arbitrary(copy_fn): h = bh.Histogram(bh.axis.Regular(10, 0, 1)) h.axes[0].other = 2 h2 = copy_fn(h) assert h2._hist is not h._hist assert h2.axes[0] is not h.axes[0] assert h2.axes[0].other == 2 h.axes[0].other = 3 assert h2._axis(0).other == 2 assert h2.axes[0].other == 2 def test_axis_wrapped(copy_fn): ax = bh.axis.Regular(10, 0, 2) ax2 = copy_fn(ax) assert ax._ax is not ax2._ax def test_trans_wrapped(copy_fn): tr = bh.axis.transform.Pow(2) tr2 = copy_fn(tr) assert tr._this is not tr2._this # Testing #342 # https://github.com/cloudpipe/cloudpickle/issues/592 @pytest.mark.skipif( sys.implementation.name == "pypy", reason="cloudpickle does not work with the latest PyPy", ) def test_cloudpickle(): cloudpickle = pytest.importorskip("cloudpickle") h = bh.Histogram(bh.axis.Regular(50, 0, 20)) h.fill([1, 2, 3, 4, 5]) h2 = loads(cloudpickle.dumps(h)) assert h == h2 assert h is not h2 # Regression tests for gh-1143: a corrupted pickle state must raise instead # of writing/reading out of bounds. def _corrupt_state(h, corrupt): ch = h._hist state = list(ch.__getstate__()) # the (last) flat numpy buffer in the state is the storage data (idx,) = [i for i, x in enumerate(state) if isinstance(x, np.ndarray)][-1:] state[idx] = corrupt(state[idx]) new = ch.__class__.__new__(ch.__class__) new.__setstate__(tuple(state)) def test_corrupted_pickle_truncated_buffer(): h = bh.Histogram(bh.axis.Regular(4, 0, 1), storage=bh.storage.Weight()) h.fill([0.1, 0.2]) with pytest.raises(RuntimeError, match="not a multiple of 2"): _corrupt_state(h, lambda a: a[:-1]) def test_corrupted_pickle_non_array(): h = bh.Histogram(bh.axis.Regular(4, 0, 1), storage=bh.storage.Weight()) h.fill([0.1, 0.2]) with pytest.raises(ValueError, match="expected a numpy array"): _corrupt_state(h, lambda _: "not an array") def test_corrupted_pickle_mean_storage(): h = bh.Histogram(bh.axis.Regular(4, 0, 1), storage=bh.storage.Mean()) h.fill([0.1, 0.2], sample=[1.0, 2.0]) with pytest.raises(RuntimeError, match="not a multiple of 3"): _corrupt_state(h, lambda a: a[:-1]) scikit-hep-boost-histogram-b4c49b0/tests/test_pickles.py000066400000000000000000000032231524660700500235140ustar00rootroot00000000000000from __future__ import annotations import pickle from pathlib import Path import pytest import boost_histogram as bh DIR = Path(__file__).parent @pytest.mark.parametrize("version", ["0.10.2", "0.6.2", "0.11.1", "1.1.0"]) def test_read_pickle(version): filename = DIR / "pickles" / f"bh_{version}.pkl" with filename.open("rb") as f: d = pickle.load(f) assert d["version"] == version h1 = d["h1"] h2 = d["h2"] h3 = d["h3"] assert h1.storage_type == ( bh.storage.Double if version[0] == "0" else bh.storage.Int64 ) assert h2.storage_type == bh.storage.Weight assert h3.storage_type == bh.storage.Double assert h1[bh.loc(-5)] == 1 assert h1[bh.loc(1)] == 2 assert h1[bh.loc(2)] == 1 assert h2[0].value == 0 assert h2[1].value == 1 assert h2[2].value == 2 assert h2[3].value == 3 assert h2[4].value == 0 assert h3[bh.loc("one"), bh.loc(3)] == 1 assert h3[bh.loc("two"), bh.loc(2)] == 1 assert h3[bh.loc("two"), bh.loc(1)] == 1 assert h3[bh.loc("two"), bh.loc(3)] == 0 assert h3[bh.loc("two"), sum] == 2 assert isinstance(h1.axes[0], bh.axis.Regular) assert isinstance(h2.axes[0], bh.axis.Integer) assert isinstance(h3.axes[0], bh.axis.StrCategory) assert isinstance(h3.axes[1], bh.axis.Variable) assert h3.axes[0].traits.growth assert not h3.axes[1].traits.growth assert h1.axes[0].metadata is None assert h2.axes[0].metadata == {"hello": "world"} ver = tuple(map(int, version.split("."))) assert h1.metadata is None if ver < (0, 9, 0): assert h2.metadata is None else: assert h2.metadata == "foo" scikit-hep-boost-histogram-b4c49b0/tests/test_plottable_protocol.py000066400000000000000000000062251524660700500257760ustar00rootroot00000000000000from __future__ import annotations import numpy as np from pytest import approx import boost_histogram as bh COUNTS = np.array([3, 4, 5, 6]) VALUES = np.array([1, 2, 3, 4]) VARIANCES = np.array([0.1, 0.1, 0.2, 0.2]) SUM2 = np.array([9, 8, 7, 6]) # not in protocol, so not checked def test_plottable_histogram_mean_int(): h = bh.Histogram(bh.axis.Integer(0, 4), storage=bh.storage.Mean()) h[...] = np.stack([COUNTS, VALUES, VARIANCES]).T assert approx(h.counts()) == COUNTS assert approx(h.values()) == VALUES assert approx(h.variances()) == VARIANCES / COUNTS assert h.kind == bh.Kind.MEAN assert h.kind == "MEAN" assert len(h.axes) == 1 assert h.axes[0] == bh.axis.Integer(0, 4) assert h.axes[0][0] == 0 assert h.axes[0][1] == 1 assert h.axes[0][2] == 2 assert h.axes[0][3] == 3 assert h.axes[0].traits.discrete assert not h.axes[0].traits.circular def test_plottible_histogram_weight_reg(): h = bh.Histogram(bh.axis.Regular(4, 0, 4), storage=bh.storage.Weight()) h[...] = np.stack([VALUES, VARIANCES]).T assert approx(h.counts()) == VALUES assert approx(h.values()) == VALUES assert approx(h.variances()) == VARIANCES assert h.kind == bh.Kind.COUNT assert h.kind == "COUNT" assert len(h.axes) == 1 assert h.axes[0] == bh.axis.Regular(4, 0, 4) assert h.axes[0][0] == approx((0, 1)) assert h.axes[0][1] == approx((1, 2)) assert h.axes[0][2] == approx((2, 3)) assert h.axes[0][3] == approx((3, 4)) assert not h.axes[0].traits.discrete assert not h.axes[0].traits.circular def test_plottible_histogram_simple_var(): h = bh.Histogram(bh.axis.Variable([0, 1, 2, 3, 4])) # Setting directly or filling with weight should cause variances to # be None. Manipulations directly on the view, however, are up to the user # - once you've asked for a view of the memory, you should know what your # are doing. At least if you change it inplace. h.view()[...] = VALUES assert approx(h.counts()) == VALUES assert approx(h.values()) == VALUES assert approx(h.variances()) == VALUES assert h.kind == bh.Kind.COUNT assert h.kind == "COUNT" assert len(h.axes) == 1 assert h.axes[0] == bh.axis.Variable([0, 1, 2, 3, 4]) assert h.axes[0][0] == approx((0, 1)) assert h.axes[0][1] == approx((1, 2)) assert h.axes[0][2] == approx((2, 3)) assert h.axes[0][3] == approx((3, 4)) assert not h.axes[0].traits.discrete assert not h.axes[0].traits.circular h.fill([1], weight=0) assert approx(h.counts()) == VALUES assert approx(h.values()) == VALUES assert h.variances() is None def test_plottible_histogram_simple_var_invalidate_inplace(): h = bh.Histogram(bh.axis.Variable([0, 1, 2, 3, 4])) h.view()[...] = VALUES h2 = h * 1 assert approx(h.counts()) == VALUES assert approx(h.values()) == VALUES assert approx(h.variances()) == VALUES assert approx(h2.counts()) == VALUES assert approx(h2.values()) == VALUES assert h2.variances() is None h *= 1 assert approx(h.counts()) == VALUES assert approx(h.values()) == VALUES assert h.variances() is None scikit-hep-boost-histogram-b4c49b0/tests/test_profiles.py000066400000000000000000000071361524660700500237140ustar00rootroot00000000000000from __future__ import annotations import numpy as np from pytest import approx import boost_histogram as bh def test_mean_hist(): h = bh.Histogram(bh.axis.Regular(3, 0, 1), storage=bh.storage.Mean()) h.fill(0.10, sample=[2.5]) h.fill(0.25, sample=[3.5]) h.fill(0.45, sample=[1.2]) h.fill(0.51, sample=[3.4]) h.fill(0.81, sample=[1.3]) h.fill(0.86, sample=[1.9]) results = ( {"count": 2, "value": 3.0, "variance": 0.5}, {"count": 2, "value": 2.3, "variance": 2.42}, {"count": 2, "value": 1.6, "variance": 0.18}, ) for i in range(len(h.axes[0])): assert results[i]["count"] == h[i].count assert results[i]["value"] == approx(h[i].value) assert results[i]["variance"] == approx(h[i].variance) def test_mean_hist_scalar_axis_broadcast(): """Test that a scalar axis value is broadcast to match a sample array (issue #727).""" data = np.array([[1, 2, 3, 4, 5], [10, 20, 30, 40, 50], [100, 200, 300, 400, 500]]) h = bh.Histogram(bh.axis.Integer(0, 3), storage=bh.storage.Mean()) h.fill(0, sample=data[0]) h.fill(1, sample=data[1]) h.fill(2, sample=data[2]) assert h[0].count == 5 assert h[0].value == approx(3.0) assert h[1].count == 5 assert h[1].value == approx(30.0) assert h[2].count == 5 assert h[2].value == approx(300.0) # Verify equivalence with the explicit workaround h2 = bh.Histogram(bh.axis.Integer(0, 3), storage=bh.storage.Mean()) h2.fill([0] * len(data[0]), sample=data[0]) h2.fill([1] * len(data[1]), sample=data[1]) h2.fill([2] * len(data[2]), sample=data[2]) for i in range(3): assert h[i].count == h2[i].count assert h[i].value == approx(h2[i].value) assert h[i].variance == approx(h2[i].variance) def test_weighted_mean_hist_scalar_axis_broadcast(): """Test scalar axis broadcast for WeightedMean storage (issue #727).""" data = np.array([[1, 2, 3, 4, 5], [10, 20, 30, 40, 50], [100, 200, 300, 400, 500]]) h = bh.Histogram(bh.axis.Integer(0, 3), storage=bh.storage.WeightedMean()) h.fill(0, sample=data[0]) h.fill(1, sample=data[1]) h.fill(2, sample=data[2]) assert h[0].sum_of_weights == approx(5.0) assert h[0].value == approx(3.0) assert h[1].sum_of_weights == approx(5.0) assert h[1].value == approx(30.0) assert h[2].sum_of_weights == approx(5.0) assert h[2].value == approx(300.0) def test_mean_hist_scalar_sample(): """A scalar sample is accepted like a scalar axis value (issue #646).""" h = bh.Histogram(bh.axis.Regular(10, 0, 1), storage=bh.storage.Mean()) h.fill(0.3, sample=4) ref = bh.Histogram(bh.axis.Regular(10, 0, 1), storage=bh.storage.Mean()) ref.fill(0.3, sample=[4]) assert h[3] == ref[3] def test_weighted_mean_hist_scalar_sample(): """A scalar sample is accepted like a scalar axis value (issue #646).""" h = bh.Histogram(bh.axis.Regular(10, 0, 1), storage=bh.storage.WeightedMean()) h.fill(0.3, sample=4, weight=1.0) ref = bh.Histogram(bh.axis.Regular(10, 0, 1), storage=bh.storage.WeightedMean()) ref.fill(0.3, sample=np.array([4]), weight=1.0) assert h[3] == ref[3] def test_mean_hist_scalar_sample_broadcast(): """A scalar sample is broadcast across an array of axis values (issue #646).""" h = bh.Histogram(bh.axis.Regular(10, 0, 1), storage=bh.storage.Mean()) h.fill([0.3, 0.31, 0.32], sample=4) ref = bh.Histogram(bh.axis.Regular(10, 0, 1), storage=bh.storage.Mean()) ref.fill([0.3, 0.31, 0.32], sample=[4, 4, 4]) assert h[3] == ref[3] assert h[3].count == 3 assert h[3].value == approx(4.0) scikit-hep-boost-histogram-b4c49b0/tests/test_pytest_plugin.py000066400000000000000000000124031524660700500247700ustar00rootroot00000000000000from __future__ import annotations import importlib.metadata import sys import pytest import boost_histogram as bh from boost_histogram._pytest import pytest_assertrepr_compare pytest_plugins = ["pytester"] def _plugin_autoloaded() -> bool: """True when boost-histogram is installed with its pytest11 entry point. In a CMake-only / in-place build the package is importable but not pip-installed, so there is no entry point and the auto-load test is skipped. """ eps = importlib.metadata.entry_points(group="pytest11") return any(ep.value == "boost_histogram._pytest" for ep in eps) def text(lines: list[str] | None) -> str: assert lines is not None return "\n".join(lines) # --- top-level hook gating ------------------------------------------------ def test_returns_none_for_other_ops() -> None: h1 = bh.Histogram(bh.axis.Regular(2, 0, 1)) h2 = bh.Histogram(bh.axis.Regular(2, 0, 1)) assert pytest_assertrepr_compare("!=", h1, h2) is None assert pytest_assertrepr_compare("<", h1, h2) is None def test_returns_none_for_non_histogram_axis() -> None: assert pytest_assertrepr_compare("==", 1, 2) is None assert ( pytest_assertrepr_compare("==", bh.Histogram(bh.axis.Regular(2, 0, 1)), 2) is None ) # --- histogram explanations ----------------------------------------------- def test_histogram_ndim_differs() -> None: h1 = bh.Histogram(bh.axis.Regular(2, 0, 1)) h2 = bh.Histogram(bh.axis.Regular(2, 0, 1), bh.axis.Regular(2, 0, 1)) out = text(pytest_assertrepr_compare("==", h1, h2)) assert "ndim: 1 != 2" in out def test_histogram_axis_differs() -> None: h1 = bh.Histogram(bh.axis.Regular(10, 0, 1)) h2 = bh.Histogram(bh.axis.Regular(20, 0, 1)) out = text(pytest_assertrepr_compare("==", h1, h2)) assert "axes differ" in out assert "axis 0:" in out # The detailed per-axis explanation is included for the first differing axis. assert "arguments differ" in out def test_histogram_storage_differs() -> None: h1 = bh.Histogram(bh.axis.Regular(2, 0, 1), storage=bh.storage.Double()) h2 = bh.Histogram(bh.axis.Regular(2, 0, 1), storage=bh.storage.Int64()) out = text(pytest_assertrepr_compare("==", h1, h2)) assert "storage: Double != Int64" in out def test_histogram_contents_differ_plain() -> None: h1 = bh.Histogram(bh.axis.Regular(10, 0, 1)) h1.fill([0.05]) h2 = bh.Histogram(bh.axis.Regular(10, 0, 1)) h2.fill([0.05, 0.05]) out = text(pytest_assertrepr_compare("==", h1, h2)) assert "1 of 12 bins differ" in out # The summed values include flow bins (there are none filled here, but the # label must say so, since lv/rv passed to _plain_contents include flow). assert "sum (with flow): 1.0 vs 2.0" in out def test_histogram_contents_truncated() -> None: h1 = bh.Histogram(bh.axis.Regular(20, 0, 1)) h2 = bh.Histogram(bh.axis.Regular(20, 0, 1)) h2.fill([(i + 0.5) / 20 for i in range(20)]) out = text(pytest_assertrepr_compare("==", h1, h2)) assert "20 of 22 bins differ" in out assert "and 15 more" in out def test_histogram_contents_differ_weight() -> None: h1 = bh.Histogram(bh.axis.Regular(5, 0, 1), storage=bh.storage.Weight()) h1.fill([0.5], weight=[2.0]) h2 = bh.Histogram(bh.axis.Regular(5, 0, 1), storage=bh.storage.Weight()) h2.fill([0.5], weight=[3.0]) out = text(pytest_assertrepr_compare("==", h1, h2)) assert "contents differ" in out assert "field 'value'" in out # --- axis explanations ---------------------------------------------------- def test_axis_type_differs() -> None: a1 = bh.axis.Regular(10, 0, 1) a2 = bh.axis.Variable([0, 0.5, 1]) out = text(pytest_assertrepr_compare("==", a1, a2)) assert "type: Regular != Variable" in out def test_axis_size_differs() -> None: a1 = bh.axis.Regular(10, 0, 1) a2 = bh.axis.Regular(20, 0, 1) out = text(pytest_assertrepr_compare("==", a1, a2)) assert "arguments differ" in out assert "10 != 20" in out def test_axis_edges_differ() -> None: a1 = bh.axis.Variable([0, 0.5, 1]) a2 = bh.axis.Variable([0, 0.3, 1]) out = text(pytest_assertrepr_compare("==", a1, a2)) assert "arguments differ" in out def test_axis_metadata_differs() -> None: a1 = bh.axis.Regular(10, 0, 1, metadata="a") a2 = bh.axis.Regular(10, 0, 1, metadata="b") out = text(pytest_assertrepr_compare("==", a1, a2)) assert "metadata='a'" in out assert "metadata='b'" in out # --- end-to-end through pytest (proves the entry point is wired up) ------- @pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="needs subprocess") @pytest.mark.skipif( not _plugin_autoloaded(), reason="boost-histogram not installed with its pytest11 entry point (CMake-only build)", ) def test_plugin_loaded_end_to_end(pytester: pytest.Pytester) -> None: pytester.makepyfile( """ import boost_histogram as bh def test_demo(): h1 = bh.Histogram(bh.axis.Regular(10, 0, 1)); h1.fill([0.05]) h2 = bh.Histogram(bh.axis.Regular(10, 0, 1)); h2.fill([0.05, 0.05]) assert h1 == h2 """ ) result = pytester.runpytest_subprocess("-vv", "-p", "no:cacheprovider") result.assert_outcomes(failed=1) result.stdout.fnmatch_lines(["*bins differ*"]) scikit-hep-boost-histogram-b4c49b0/tests/test_serialization_uhi.py000066400000000000000000000610751524660700500256150ustar00rootroot00000000000000from __future__ import annotations import ctypes import importlib.metadata import importlib.resources import json import math import sys import numpy as np import pytest import boost_histogram as bh from boost_histogram.serialization import from_uhi, remove_writer_info, to_uhi @pytest.mark.parametrize( ("storage_type", "expected_type"), [ pytest.param(bh.storage.AtomicInt64(), "int", id="atomic_int"), pytest.param(bh.storage.Int64(), "int", id="int"), pytest.param( bh.storage.Unlimited(), "double", id="unlimited" ), # This always renders as double pytest.param(bh.storage.Double(), "double", id="double"), ], ) def test_simple_to_dict(storage_type: bh.storage.Storage, expected_type: str) -> None: h = bh.Histogram( bh.axis.Regular(10, 0, 1), storage=storage_type, ) data = to_uhi(h) assert data["axes"][0]["type"] == "regular" assert data["axes"][0]["lower"] == 0 assert data["axes"][0]["upper"] == 1 assert data["axes"][0]["bins"] == 10 assert data["axes"][0]["underflow"] assert data["axes"][0]["overflow"] assert not data["axes"][0]["circular"] assert data["storage"]["type"] == expected_type assert data["storage"]["values"] == pytest.approx(np.zeros(12)) assert data["uhi_schema"] == 1 def test_weighed_to_dict() -> None: h = bh.Histogram( bh.axis.Integer(3, 15), storage=bh.storage.Weight(), ) data = to_uhi(h) assert data["axes"][0]["type"] == "regular" assert data["axes"][0]["lower"] == 3 assert data["axes"][0]["upper"] == 15 assert data["axes"][0]["bins"] == 12 assert data["axes"][0]["underflow"] assert data["axes"][0]["overflow"] assert not data["axes"][0]["circular"] assert data["storage"]["type"] == "weighted" assert data["storage"]["values"] == pytest.approx(np.zeros(14)) assert data["storage"]["variances"] == pytest.approx(np.zeros(14)) def test_mean_to_dict() -> None: h = bh.Histogram( bh.axis.StrCategory(["one", "two", "three"]), storage=bh.storage.Mean(), ) h.name = "hi" data = to_uhi(h) assert data["metadata"]["name"] == "hi" assert data["axes"][0]["type"] == "category_str" assert data["axes"][0]["categories"] == ["one", "two", "three"] assert data["axes"][0]["flow"] assert data["storage"]["type"] == "mean" assert data["storage"]["counts"] == pytest.approx(np.zeros(4)) assert data["storage"]["values"] == pytest.approx(np.zeros(4)) assert data["storage"]["variances"] == pytest.approx(np.zeros(4)) def test_weighted_mean_to_dict() -> None: h = bh.Histogram( bh.axis.IntCategory([1, 2, 3]), storage=bh.storage.WeightedMean(), ) h.fill([1, 2, 3, 50], weight=[10, 20, 30, 5], sample=[100, 200, 300, 1]) h.fill([1, 2, 3, -3], weight=[10, 20, 30, 5], sample=[100, 200, 300, 1]) data = to_uhi(h) assert data["axes"][0]["type"] == "category_int" assert data["axes"][0]["categories"] == pytest.approx([1, 2, 3]) assert data["axes"][0]["flow"] assert data["storage"]["type"] == "weighted_mean" assert data["storage"]["sum_of_weights"] == pytest.approx( np.array([20, 40, 60, 10]) ) assert data["storage"]["sum_of_weights_squared"] == pytest.approx( np.array([200, 800, 1800, 50]) ) assert data["storage"]["values"] == pytest.approx(np.array([100, 200, 300, 1])) assert data["storage"]["variances"] == pytest.approx(np.zeros(4)) def test_transform_log_axis_to_dict() -> None: h = bh.Histogram(bh.axis.Regular(10, 1, 10, transform=bh.axis.transform.log)) data = to_uhi(h) assert data["axes"][0]["type"] == "variable" assert data["axes"][0]["edges"] == pytest.approx( np.exp(np.linspace(0, np.log(10), 11)) ) writer_info = data["axes"][0]["writer_info"]["boost-histogram"] assert writer_info["transform"] == "log" assert writer_info["bins"] == 10 assert writer_info["lower"] == pytest.approx(1) assert writer_info["upper"] == pytest.approx(10) def test_transform_sqrt_axis_to_dict() -> None: h = bh.Histogram(bh.axis.Regular(10, 0, 10, transform=bh.axis.transform.sqrt)) data = to_uhi(h) assert data["axes"][0]["type"] == "variable" assert data["axes"][0]["edges"] == pytest.approx( (np.linspace(0, np.sqrt(10), 11)) ** 2 ) writer_info = data["axes"][0]["writer_info"]["boost-histogram"] assert writer_info["transform"] == "sqrt" assert writer_info["bins"] == 10 assert writer_info["lower"] == pytest.approx(0) assert writer_info["upper"] == pytest.approx(10) @pytest.mark.parametrize( "transform", [ pytest.param(bh.axis.transform.log, id="log"), pytest.param(bh.axis.transform.sqrt, id="sqrt"), pytest.param(bh.axis.transform.Pow(2), id="pow2"), pytest.param(bh.axis.transform.Pow(0.5), id="pow_half"), ], ) def test_round_trip_transform(transform: bh.axis.transform.AxisTransform) -> None: h = bh.Histogram(bh.axis.Regular(5, 1, 10, transform=transform)) h.fill([0.5, 1.5, 3.0, 8.0, 50.0]) data = _json_round_trip(to_uhi(h)) h2 = from_uhi(data) assert isinstance(h2.axes[0], bh.axis.Regular) assert h2.axes[0].transform is not None assert repr(h2.axes[0].transform) == repr(transform) assert h == h2 @pytest.mark.skipif( sys.implementation.name == "pypy", reason="ctypes function-pointer transforms hang forever on PyPy", ) @pytest.mark.skipif( sys.implementation.name == "graalpy", reason="ctypes function-pointer transforms are not supported on GraalPy", ) def test_round_trip_transform_custom_falls_back_to_variable() -> None: """A transform we can't reconstruct (a raw function pointer) degrades to a Variable axis rather than failing.""" ftype = ctypes.CFUNCTYPE(ctypes.c_double, ctypes.c_double) transform = bh.axis.transform.Function( ftype(math.log), ftype(math.exp), name="custom" ) h = bh.Histogram(bh.axis.Regular(5, 1, 10, transform=transform)) data = to_uhi(h) assert "writer_info" not in data["axes"][0] h2 = from_uhi(data) assert isinstance(h2.axes[0], bh.axis.Variable) assert np.asarray(h2.axes[0].edges) == pytest.approx(np.asarray(h.axes[0].edges)) @pytest.mark.skipif( sys.implementation.name == "pypy", reason="ctypes function-pointer transforms hang forever on PyPy", ) @pytest.mark.skipif( sys.implementation.name == "graalpy", reason="ctypes function-pointer transforms are not supported on GraalPy", ) def test_transform_same_name_as_builtin_is_not_confused() -> None: """A custom Function transform named 'log' must not round-trip as the built-in log transform - the two have different forward/inverse math.""" ftype = ctypes.CFUNCTYPE(ctypes.c_double, ctypes.c_double) transform = bh.axis.transform.Function(ftype(math.exp), ftype(math.log), name="log") assert transform != bh.axis.transform.log h = bh.Histogram(bh.axis.Regular(5, 1, 10, transform=transform)) data = to_uhi(h) # Not reconstructable, so it must fall back to a Variable axis, not "log" assert "writer_info" not in data["axes"][0] h2 = from_uhi(data) assert isinstance(h2.axes[0], bh.axis.Variable) def test_from_uhi_unknown_transform_falls_back_to_variable() -> None: """An unrecognized transform name in writer_info reads back as a Variable axis instead of raising.""" data = { "uhi_schema": 1, "axes": [ { "type": "variable", "edges": [1.0, 2.0, 4.0, 10.0], "underflow": True, "overflow": True, "circular": False, "writer_info": { "boost-histogram": { "transform": "mystery", "bins": 3, "lower": 1.0, "upper": 10.0, } }, } ], "storage": {"type": "double"}, "metadata": {}, } h = from_uhi(data) assert isinstance(h.axes[0], bh.axis.Variable) assert np.asarray(h.axes[0].edges) == pytest.approx([1.0, 2.0, 4.0, 10.0]) @pytest.mark.parametrize( "storage_type", [ pytest.param(bh.storage.AtomicInt64(), id="atomic_int"), pytest.param(bh.storage.Int64(), id="int"), pytest.param(bh.storage.Double(), id="double"), pytest.param(bh.storage.Unlimited(), id="unlimited"), ], ) def test_round_trip_simple(storage_type: bh.storage.Storage) -> None: h = bh.Histogram( bh.axis.Regular(10, 0, 10), storage=storage_type, ) h.fill([-1, 0, 0, 1, 20, 20, 20]) data = to_uhi(h) h2 = from_uhi(data) if isinstance(storage_type, (bh.storage.Int64, bh.storage.Double)): assert h == h2 assert h.view() == pytest.approx(h2.view()) def test_round_trip_weighted() -> None: h = bh.Histogram( bh.axis.Variable([1, 2, 4, 5], circular=True), storage=bh.storage.Weight(), ) h.fill(["1", "2", "3"], weight=[10, 20, 30]) h.fill(["1", "2", "3"], weight=[10, 20, 30]) data = to_uhi(h) h2 = from_uhi(data) assert pytest.approx(np.array(h.axes[0])) == np.array(h2.axes[0]) assert np.asarray(h) == pytest.approx(h2) def test_round_trip_mean() -> None: h = bh.Histogram( bh.axis.StrCategory(["1", "2", "3"]), storage=bh.storage.Mean(), ) h.fill(["1", "2", "3"], weight=[10, 20, 30], sample=[100, 200, 300]) h.fill(["1", "2", "3"], weight=[10, 20, 30], sample=[100, 200, 300]) data = to_uhi(h) h2 = from_uhi(data) assert pytest.approx(np.array(h.axes[0])) == np.array(h2.axes[0]) assert np.asarray(h) == pytest.approx(h2) def test_round_trip_weighted_mean() -> None: h = bh.Histogram( bh.axis.IntCategory([1, 2, 3]), storage=bh.storage.WeightedMean(), ) h.fill([1, 2, 3], weight=[10, 20, 30], sample=[100, 200, 300]) h.fill([1, 2, 3], weight=[10, 20, 30], sample=[100, 200, 300]) data = to_uhi(h) h2 = from_uhi(data) assert pytest.approx(np.array(h.axes[0])) == np.array(h2.axes[0]) assert np.asarray(h) == pytest.approx(h2) def test_uhi_wrapper(): h = bh.Histogram( bh.axis.IntCategory([1, 2, 3]), storage=bh.storage.WeightedMean(), ) expected_keys = {"uhi_schema", "writer_info", "axes", "storage", "metadata"} assert to_uhi(h).keys() == expected_keys assert h._to_uhi_().keys() == expected_keys data = h._to_uhi_() assert repr(from_uhi(data)) == repr(bh.Histogram._from_uhi_(data)) == repr(h) def test_uhi_direct_conversion(): h = bh.Histogram( bh.axis.IntCategory([1, 2, 3]), storage=bh.storage.Int64(), ) uhi_dict = h._to_uhi_() h2 = bh.Histogram(uhi_dict) assert h == h2 def test_to_uhi_does_not_leak_variance_known() -> None: h = bh.Histogram(bh.axis.Regular(3, 0, 1)) data = to_uhi(h) assert "_variance_known" not in data["metadata"] def test_from_uhi_sets_variance_known_by_default() -> None: # A dict without boost-histogram's private "_variance_known" flag (e.g. # built by hand, or written by another UHI library) must still produce a # histogram whose .variances() works. data = { "uhi_schema": 1, "axes": [ { "type": "regular", "lower": 0.0, "upper": 1.0, "bins": 3, "underflow": True, "overflow": True, "circular": False, } ], "storage": {"type": "double"}, "metadata": {}, } h = from_uhi(data) assert h.variances() is not None def test_round_trip_native() -> None: h = bh.Histogram( bh.axis.Integer(0, 10), storage=bh.storage.AtomicInt64(), ) h.fill([-1, 0, 0, 1, 20, 20, 20]) data = to_uhi(h) h2 = from_uhi(data) assert h == h2 @pytest.mark.parametrize( ("storage_type", "expected_type"), [ pytest.param(bh.storage.Int64(), "int", id="int64"), pytest.param(bh.storage.AtomicInt64(), "int", id="atomic_int64"), pytest.param(bh.storage.Double(), "double", id="double"), pytest.param(bh.storage.Weight(), "weighted", id="weight"), pytest.param(bh.storage.Mean(), "mean", id="mean"), ], ) def test_to_uhi_keep_storage_option( storage_type: bh.storage.Storage, expected_type: str ) -> None: h = bh.Histogram( bh.axis.Regular(3, 0, 1), storage=storage_type, ) data_with = to_uhi(h) data_without = to_uhi(h, keep_storage=False) assert "storage" in data_with assert "storage" in data_without # Storage with data has "values" key assert "values" in data_with["storage"] # Storage without data has only type information assert "values" not in data_without["storage"] assert data_without["storage"]["type"] == expected_type @pytest.mark.parametrize( "storage_type", [ pytest.param(bh.storage.Int64(), id="int64"), pytest.param(bh.storage.AtomicInt64(), id="atomic_int64"), pytest.param(bh.storage.Double(), id="double"), pytest.param(bh.storage.Weight(), id="weight"), pytest.param(bh.storage.Mean(), id="mean"), ], ) def test_from_uhi_missing_storage_data(storage_type: bh.storage.Storage) -> None: h = bh.Histogram( bh.axis.Regular(4, 0.0, 1.0), storage=storage_type, ) # produce a UHI dict with storage type but no data data = to_uhi(h, keep_storage=False) h2 = from_uhi(data) # axes and storage type should round-trip, data should be zeros assert pytest.approx(np.array(h.axes[0])) == np.array(h2.axes[0]) assert h2.storage_type is type(storage_type) assert np.asarray(h2) == pytest.approx(np.zeros_like(np.asarray(h2))) @pytest.mark.parametrize( ("storage_type", "expected_type"), [ pytest.param(bh.storage.AtomicInt64(), "int", id="atomic_int64"), pytest.param(bh.storage.Unlimited(), "double", id="unlimited"), ], ) def test_from_uhi_old_style_writer_info( storage_type: bh.storage.Storage, expected_type: str ) -> None: h = bh.Histogram( bh.axis.Regular(4, 0.0, 1.0), storage=storage_type, ) h.fill([0, 0, 1, 2]) data = to_uhi(h) old_style_data = { "uhi_schema": 1, "axes": data["axes"], "storage": { "type": expected_type, "values": data["storage"]["values"], "writer_info": { "boost-histogram": {"orig_type": type(storage_type).__name__} }, }, "metadata": data.get("metadata", {}), } h2 = from_uhi(old_style_data) assert h == h2 assert h2.storage_type is type(storage_type) @pytest.mark.parametrize( ("axis_type", "axis_args", "fill_values"), [ pytest.param(bh.axis.Integer, (0, 10), [-1, 0, 0, 1, 20, 20, 20], id="integer"), pytest.param( bh.axis.Regular, (10, 0.0, 10.0), [-1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 20.0], id="regular", ), pytest.param( bh.axis.Variable, ([0.0, 2.0, 5.0, 10.0],), [-1.0, 0.0, 1.0, 6.0, 20.0, 20.0], id="variable", ), pytest.param( bh.axis.IntCategory, ([1, 2, 3],), [0, 1, 1, 2, 10, 10, 10], id="intcategory", ), pytest.param( bh.axis.StrCategory, (["A", "B", "C"],), ["Z", "A", "A", "B", "X", "X", "X"], id="strcategory", ), ], ) def test_round_trip_growth( axis_type: type, axis_args: tuple, fill_values: list ) -> None: h = bh.Histogram( axis_type(*axis_args, growth=True), ) h.fill(fill_values) data = to_uhi(h) h2 = from_uhi(data) assert h == h2 assert isinstance(h2.axes[0], axis_type) assert h2.axes[0].traits.growth == h.axes[0].traits.growth @pytest.mark.parametrize("remove", ["boost-histogram", None]) def test_round_trip_clean(remove: str | None) -> None: h = bh.Histogram( bh.axis.Integer(0, 10), storage=bh.storage.AtomicInt64(), ) h.fill([-1, 0, 0, 1, 20, 20, 20]) data = to_uhi(h) data = remove_writer_info(data, library=remove) h2 = from_uhi(data) assert isinstance(h2.axes[0], bh.axis.Regular) assert h2.storage_type is bh.storage.Int64 def test_unserializable_metadata() -> None: h = bh.Histogram( bh.axis.Integer(0, 10, __dict__={"c": 3, "@d": 4}), ) h.__dict__["a"] = 1 h.__dict__["@b"] = 2 data = to_uhi(h) # _variance_known is internal Histogram state, not user metadata assert data["metadata"] == {"a": 1} assert data["axes"][0]["metadata"] == {"c": 3} def test_histogram_metadata() -> None: h = bh.Histogram(bh.axis.Integer(0, 10)) h.name = "Hi" h.label = "hi" h.other = 3 data = to_uhi(h) assert data["metadata"] == { "name": "Hi", "label": "hi", "other": 3, } def test_remove_writer_info() -> None: d = { "uhi_schema": 1, "writer_info": {"boost-histogram": {"foo": "bar"}, "hist": {"FOO": "BAR"}}, } assert remove_writer_info(d, library=None) == {"uhi_schema": 1} assert remove_writer_info(d) == { "uhi_schema": 1, "writer_info": {"hist": {"FOO": "BAR"}}, } assert remove_writer_info(d, library="boost-histogram") == { "uhi_schema": 1, "writer_info": {"hist": {"FOO": "BAR"}}, } assert remove_writer_info(d, library="hist") == { "uhi_schema": 1, "writer_info": {"boost-histogram": {"foo": "bar"}}, } assert remove_writer_info(d, library="c") == d def test_convert_weight() -> None: h = bh.Histogram( bh.axis.Regular(3, 13, 10, __dict__={"name": "x"}), bh.axis.StrCategory(["one", "two"]), storage=bh.storage.Weight(), ) data = h._to_uhi_() h2 = bh.Histogram(data) assert h == h2 def test_convert_weightmean() -> None: h = bh.Histogram( bh.axis.Regular(12, 0, 1), bh.axis.StrCategory(["a", "b", "c", "d", "e", "f", "g"]), bh.axis.Boolean(), bh.axis.Integer(1, 18), storage=bh.storage.WeightedMean(), ) data = h._to_uhi_() h2 = bh.Histogram(data) assert h.axes == h2.axes def _json_round_trip(data: dict) -> dict: """Simulate a JSON round-trip, which can collapse empty array dimensions.""" return json.loads( json.dumps( data, default=lambda x: x.tolist() if isinstance(x, np.ndarray) else x, ) ) @pytest.mark.parametrize( "storage_type", [ pytest.param(bh.storage.Double(), id="double"), pytest.param(bh.storage.Int64(), id="int64"), pytest.param(bh.storage.Weight(), id="weight"), pytest.param(bh.storage.Mean(), id="mean"), pytest.param(bh.storage.WeightedMean(), id="weighted_mean"), ], ) def test_round_trip_3d_histogram_json(storage_type: bh.storage.Storage) -> None: """Regression test: 3D histograms with empty StrCategory axes round-trip correctly through JSON serialization. JSON serialization collapses empty array dimensions (e.g. shape (5, 0, 0) becomes (5, 0)), which must be handled correctly during deserialization. """ h = bh.Histogram( bh.axis.Variable([0, 1, 2, 3]), bh.axis.StrCategory([], growth=True), bh.axis.StrCategory([], growth=True), storage=storage_type, ) data = to_uhi(h) data = _json_round_trip(data) h2 = from_uhi(data) assert h.ndim == h2.ndim assert h == h2 def test_round_trip_3d_histogram_json_constructor() -> None: """Regression test: bh.Histogram(uhi_dict) works for 3D histograms after JSON round-trip (reproduces the exact example from the bug report).""" h = bh.Histogram( bh.axis.Variable([0, 1, 2, 3]), bh.axis.StrCategory([], growth=True), bh.axis.StrCategory([], growth=True), storage=bh.storage.Weight(), ) data = _json_round_trip(to_uhi(h)) h2 = bh.Histogram(data) assert h.ndim == h2.ndim assert h == h2 def test_from_uhi_malformed_weight_storage() -> None: """Test that malformed Weight storage ( missing required keys) raises ValueError.""" data = { "uhi_schema": 1, "axes": [ { "type": "regular", "lower": 0, "upper": 10, "bins": 5, "underflow": True, "overflow": True, "circular": False, } ], "storage": { "type": "weighted", "values": [1, 2, 3, 4, 5], }, "metadata": {}, } with pytest.raises(ValueError, match="Weighted storage missing required keys"): from_uhi(data) def test_from_uhi_malformed_mean_storage() -> None: """Test that malformed Mean storage (missing required keys) raises ValueError.""" data = { "uhi_schema": 1, "axes": [ { "type": "regular", "lower": 0, "upper": 10, "bins": 5, "underflow": True, "overflow": True, "circular": False, } ], "storage": { "type": "mean", "counts": [1, 2, 3, 4, 5], "values": [1, 2, 3, 4, 5], }, "metadata": {}, } with pytest.raises(ValueError, match="Mean storage missing required keys"): from_uhi(data) def test_from_uhi_malformed_weighted_mean_storage() -> None: """Test that malformed WeightedMean storage (missing required keys) raises ValueError.""" data = { "uhi_schema": 1, "axes": [ { "type": "regular", "lower": 0, "upper": 10, "bins": 5, "underflow": True, "overflow": True, "circular": False, } ], "storage": { "type": "weighted_mean", "sum_of_weights": [1, 2, 3, 4, 5], "values": [1, 2, 3, 4, 5], }, "metadata": {}, } with pytest.raises(ValueError, match="Weighted_mean storage missing required keys"): from_uhi(data) def test_from_uhi_structure_only_no_error() -> None: """Test that structure-only (no data keys) histograms load correctly.""" data = { "uhi_schema": 1, "axes": [ { "type": "regular", "lower": 0, "upper": 10, "bins": 5, "underflow": True, "overflow": True, "circular": False, } ], "storage": { "type": "double", }, "metadata": {}, } h = from_uhi(data) assert h.storage_type is bh.storage.Double assert np.asarray(h) == pytest.approx(np.zeros(5)) @pytest.mark.parametrize( "storage_type", [ pytest.param(bh.storage.Int64(), id="int64"), pytest.param(bh.storage.AtomicInt64(), id="atomic_int64"), pytest.param(bh.storage.Double(), id="double"), pytest.param(bh.storage.Unlimited(), id="unlimited"), pytest.param(bh.storage.Weight(), id="weight"), pytest.param(bh.storage.Mean(), id="mean"), pytest.param(bh.storage.WeightedMean(), id="weighted_mean"), ], ) @pytest.mark.parametrize( "keep_storage", [True, False], ids=["with_data", "metadata_only"] ) def test_to_uhi_matches_uhi_schema( storage_type: bh.storage.Storage, keep_storage: bool ) -> None: """``to_uhi`` output conforms to the UHI histogram JSON schema, including the metadata-only (``keep_storage=False``) form used for type-only storage.""" jsonschema = pytest.importorskip("jsonschema") # Older jsonschema (pulled in where rpds-py has no wheel, e.g. Pyodide 3.15) # cannot resolve the UHI schema's $ref entries jsonschema_version = importlib.metadata.version("jsonschema").split(".") if tuple(int(v) for v in jsonschema_version[:2]) < (4, 18): pytest.skip("jsonschema>=4.18 required") schema = json.loads( importlib.resources.files("uhi.resources") .joinpath("histogram.schema.json") .read_text() ) h = bh.Histogram( bh.axis.Regular(3, 0, 1), bh.axis.IntCategory([1, 2]), storage=storage_type, ) if isinstance(storage_type, (bh.storage.Mean, bh.storage.WeightedMean)): h.fill([0.1, 0.5, 0.5], [1, 2, 2], sample=[1.0, 2.0, 3.0]) else: h.fill([0.1, 0.5, 0.5], [1, 2, 2]) data = _json_round_trip(to_uhi(h, keep_storage=keep_storage)) # The schema describes a mapping of named histograms. jsonschema.validate({"hist": data}, schema) scikit-hep-boost-histogram-b4c49b0/tests/test_storage.py000066400000000000000000000524471524660700500235420ustar00rootroot00000000000000from __future__ import annotations import math import operator import numpy as np import pytest from pytest import approx import boost_histogram as bh @pytest.mark.parametrize( "storage", [bh.storage.Int64, bh.storage.Double, bh.storage.AtomicInt64, bh.storage.Unlimited], ) def test_setting(storage): h = bh.Histogram(bh.axis.Regular(10, 0, 1), storage=storage()) h[0] = 2 h[1] = 3 h[-1] = 5 assert h[0] == 2 assert h[1] == 3 assert h[9] == 5 assert h.view() == approx([2, 3, 0, 0, 0, 0, 0, 0, 0, 5]) def test_setting_weight(): h = bh.Histogram(bh.axis.Regular(10, 0, 10), storage=bh.storage.Weight()) h.fill([0.3, 0.3, 0.4, 1.2]) assert h[0] == bh.accumulators.WeightedSum(3, 3) assert h[1] == bh.accumulators.WeightedSum(1, 1) h[0] = bh.accumulators.WeightedSum(value=2, variance=2) assert h[0] == bh.accumulators.WeightedSum(2, 2) a = h.view() assert a[0] == h[0] b = np.asarray(h) assert b["value"][0] == h[0].value assert b["variance"][0] == h[0].variance h[0] = bh.accumulators.WeightedSum(value=3, variance=1) assert h[0].value == 3 assert h[0].variance == 1 assert a[0] == h[0] assert b["value"][0] == h[0].value assert b["variance"][0] == h[0].variance assert b[0]["value"] == a[0]["value"] assert b[0]["variance"] == a[0]["variance"] assert b["value"][0] == a["value"][0] assert b["variance"][0] == a["variance"][0] assert a.view().value == approx(b.view()["value"]) assert a.view().variance == approx(b.view()["variance"]) def test_sum_weight(): h = bh.Histogram(bh.axis.Integer(0, 10), storage=bh.storage.Weight()) h.fill([1, 2, 3, 3, 3, 4, 5]) v = h.view().copy() res = np.sum(v) hres = h.sum() assert res.value == hres.value == 7 assert res.variance == hres.variance == 7 v2 = v + v h2 = h + h assert h2.view().value == approx(v2.value) assert h2.view().variance == approx(v2.variance) def test_setting_profile(): h = bh.Histogram(bh.axis.Regular(10, 0, 10), storage=bh.storage.Mean()) h.fill([0.3, 0.3, 0.4, 1.2, 1.6], sample=[1, 2, 3, 4, 4]) assert h[0] == bh.accumulators.Mean(count=3, value=2, variance=1) assert h[1] == bh.accumulators.Mean(count=2, value=4, variance=0) h[0] = bh.accumulators.Mean(count=12, value=11, variance=10) assert h[0] == bh.accumulators.Mean(count=12, value=11, variance=10) a = h.view() assert a[0] == h[0] b = np.asarray(h) assert b["value"][0] == h[0].value assert b["count"][0] == h[0].count assert b["_sum_of_deltas_squared"][0] == h[0]._sum_of_deltas_squared h[0] = bh.accumulators.Mean(count=6, value=3, variance=2) assert h[0].count == 6 assert h[0].value == 3 assert h[0].variance == 2 assert a[0] == h[0] assert b["value"][0] == h[0].value assert b["count"][0] == h[0].count assert b["_sum_of_deltas_squared"][0] == h[0]._sum_of_deltas_squared assert b[0]["value"] == a[0]["value"] assert b[0]["count"] == a[0]["count"] assert b[0]["_sum_of_deltas_squared"] == a[0]["_sum_of_deltas_squared"] assert b[0]["value"] == a["value"][0] assert b[0]["count"] == a["count"][0] assert b[0]["_sum_of_deltas_squared"] == a["_sum_of_deltas_squared"][0] assert a.view().value == approx(b.view()["value"]) assert a.view().count == approx(b.view()["count"]) assert a.view()._sum_of_deltas_squared == approx(b.view()["_sum_of_deltas_squared"]) def test_mean_storage_rejects_string_sample(): h = bh.Histogram(bh.axis.Regular(10, 0, 10), storage=bh.storage.Mean()) with pytest.raises( TypeError, match=r"Sample key-argument needs to be a number or a sequence, str given\.", ): h.fill([0.3], sample="abc") def test_setting_weighted_profile(): h = bh.Histogram(bh.axis.Regular(10, 0, 10), storage=bh.storage.WeightedMean()) h.fill([0.3, 0.3, 0.4, 1.2, 1.6], sample=[1, 2, 3, 4, 4], weight=[1, 1, 1, 1, 2]) assert h[0] == bh.accumulators.WeightedMean( sum_of_weights=3, sum_of_weights_squared=3, value=2, variance=1 ) assert h[1] == bh.accumulators.WeightedMean( sum_of_weights=3, sum_of_weights_squared=5, value=4, variance=0 ) h[0] = bh.accumulators.WeightedMean( sum_of_weights=12, sum_of_weights_squared=15, value=11, variance=10 ) assert h[0] == bh.accumulators.WeightedMean( sum_of_weights=12, sum_of_weights_squared=15, value=11, variance=10 ) a = h.view() assert a[0] == h[0] b = np.asarray(h) assert b["value"][0] == h[0].value assert b["sum_of_weights"][0] == h[0].sum_of_weights assert b["sum_of_weights_squared"][0] == h[0].sum_of_weights_squared assert ( b["_sum_of_weighted_deltas_squared"][0] == h[0]._sum_of_weighted_deltas_squared ) h[0] = bh.accumulators.WeightedMean( sum_of_weights=6, sum_of_weights_squared=12, value=3, variance=2 ) assert a[0] == h[0] assert h[0].value == 3 assert h[0].variance == 2 assert h[0].sum_of_weights == 6 assert h[0].sum_of_weights_squared == 12 assert h[0]._sum_of_weighted_deltas_squared == 8 assert b["value"][0] == h[0].value assert b["sum_of_weights"][0] == h[0].sum_of_weights assert b["sum_of_weights_squared"][0] == h[0].sum_of_weights_squared assert ( b["_sum_of_weighted_deltas_squared"][0] == h[0]._sum_of_weighted_deltas_squared ) assert b[0]["value"] == a[0]["value"] assert b[0]["sum_of_weights"] == a[0]["sum_of_weights"] assert b[0]["sum_of_weights_squared"] == a[0]["sum_of_weights_squared"] assert ( b[0]["_sum_of_weighted_deltas_squared"] == a[0]["_sum_of_weighted_deltas_squared"] ) assert b[0]["value"] == a["value"][0] assert b[0]["sum_of_weights"] == a["sum_of_weights"][0] assert b[0]["sum_of_weights_squared"] == a["sum_of_weights_squared"][0] assert ( b[0]["_sum_of_weighted_deltas_squared"] == a["_sum_of_weighted_deltas_squared"][0] ) assert a.view().value == approx(b.view()["value"]) assert a.view().sum_of_weights == approx(b.view()["sum_of_weights"]) assert a.view().sum_of_weights_squared == approx(b.view()["sum_of_weights_squared"]) assert a.view()._sum_of_weighted_deltas_squared == approx( b.view()["_sum_of_weighted_deltas_squared"] ) # Issue #486 def test_modify_weights_by_view(): bins = [0, 1, 2] hist = bh.Histogram(bh.axis.Variable(bins), storage=bh.storage.Weight()) yields = [3, 4] var = [0.1, 0.2] hist[...] = np.stack([yields, var], axis=-1) hist.view().value /= 2 assert hist.view().value[0] == pytest.approx(1.5) assert hist.view().value[1] == pytest.approx(2) # Issue #531 def test_summing_mean_storage(): np.random.seed(42) values = np.random.normal(loc=1.3, scale=0.1, size=1000) samples = np.random.normal(loc=1.3, scale=0.1, size=1000) h1 = bh.Histogram(bh.axis.Regular(20, -1, 3), storage=bh.storage.Mean()) h1.fill(values, sample=samples) h2 = bh.Histogram(bh.axis.Regular(1, -1, 3), storage=bh.storage.Mean()) h2.fill(values, sample=samples) s1 = h1.sum() s2 = h2.sum() assert s1.value == approx(s2.value) assert s1.count == approx(s2.count) assert s1.variance == approx(s2.variance) # Issue #531 def test_summing_weighted_mean_storage(): np.random.seed(42) values = np.random.normal(loc=1.3, scale=0.1, size=1000) samples = np.random.normal(loc=1.3, scale=0.1, size=1000) weights = np.random.uniform(0.1, 5, size=1000) h1 = bh.Histogram(bh.axis.Regular(20, -1, 3), storage=bh.storage.WeightedMean()) h1.fill(values, sample=samples, weight=weights) h2 = bh.Histogram(bh.axis.Regular(1, -1, 3), storage=bh.storage.WeightedMean()) h2.fill(values, sample=samples, weight=weights) s1 = h1.sum() s2 = h2.sum() assert s1.value == approx(s2.value) assert s1.sum_of_weights == approx(s2.sum_of_weights) assert s1.sum_of_weights_squared == approx(s2.sum_of_weights_squared) assert s1.variance == approx(s2.variance) # Raised on Gitter def test_UHI_variance_counts(): h = bh.Histogram( bh.axis.Regular(bins=1, start=0, stop=1), storage=bh.storage.WeightedMean() ) h.fill(0.5, sample=[1], weight=[0.5]) h.fill(0.5, sample=[2], weight=[0.4]) assert not math.isnan(h.variances()[0]) h = bh.Histogram( bh.axis.Regular(bins=1, start=0, stop=1), storage=bh.storage.WeightedMean() ) h.fill(0.5, sample=[1], weight=[0.5]) h.fill(0.5, sample=[1], weight=[0.5]) assert not math.isnan(h.variances()[0]) @pytest.mark.parametrize( "storage", [ bh.storage.Int64, bh.storage.Double, bh.storage.AtomicInt64, bh.storage.Unlimited, bh.storage.Weight, bh.storage.Mean, bh.storage.WeightedMean, ], ) def test_empty_axis_histogram(storage): h2d = bh.Histogram( bh.axis.Regular(10, 0, 10), bh.axis.StrCategory([], growth=True), storage=storage(), ) assert h2d.sum() == h2d.storage_type.accumulator() assert h2d.sum(flow=True) == h2d.storage_type.accumulator() # Issue #971 def test_non_uniform_rebin_with_weights(): # 1D h = bh.Histogram(bh.axis.Regular(20, 1, 5), storage=bh.storage.Weight()) h.fill([1.1, 2.2, 3.3, 4.4]) rslt = np.array( [(1.0, 1.0), (1.0, 1.0), (1.0, 1.0), (0.0, 0.0), (1.0, 1.0)], dtype=[("value", " all-zero result, no NaN and (under filterwarnings=error) no # divide/invalid warning. empty = bh.Histogram(bh.axis.Integer(0, 3), storage=bh.storage.Mean()).view() result = empty + empty assert result.count == approx([0, 0, 0]) assert result.value == approx([0, 0, 0]) assert result._sum_of_deltas_squared == approx([0, 0, 0]) assert not np.any(np.isnan(result.value)) # Empty in one operand only -> result equals the other operand bin-for-bin. h = bh.Histogram(bh.axis.Integer(0, 3), storage=bh.storage.Mean()) h.fill([0, 0, 2], sample=[3.0, 5.0, 7.0]) left = empty + h.view() right = h.view() + empty for field in h.view().dtype.names: assert left[field] == approx(h.view()[field]) assert right[field] == approx(h.view()[field]) def test_mean_view_add_accumulator(mean_pair): h1, _ = mean_pair v = h1.view() acc = bh.accumulators.Mean(3, 5.0, 2.0) # count, value, variance result = v + acc # Equivalent to merging every bin with a view holding that accumulator. other = bh.Histogram(bh.axis.Integer(0, 4), storage=bh.storage.Mean()) other[...] = [[3.0, 5.0, 2.0]] * 4 # count, value, variance (matches acc) expected = v + other.view() for field in v.dtype.names: assert result[field] == approx(expected[field]) def test_weighted_mean_view_add_accumulator(weighted_mean_pair): h1, _ = weighted_mean_pair v = h1.view() acc = bh.accumulators.WeightedMean(2.0, 1.5, 5.0, 2.0) result = v + acc # Equivalent to merging every bin with a view holding that accumulator. other = bh.Histogram(bh.axis.Integer(0, 4), storage=bh.storage.WeightedMean()) # sum_of_weights, sum_of_weights_squared, value, variance (matches acc) other[...] = [[2.0, 1.5, 5.0, 2.0]] * 4 expected = v + other.view() for field in v.dtype.names: assert result[field] == approx(expected[field]) def test_weighted_mean_view_combine_empty_bin(): # A genuinely empty bin (sum_of_weights == 0) must not introduce NaN. h = bh.Histogram(bh.axis.Integer(0, 3), storage=bh.storage.WeightedMean()) h.fill([0, 2], sample=[3.0, 7.0], weight=[1.0, 1.0]) # bin 1 empty empty = bh.Histogram( bh.axis.Integer(0, 3), storage=bh.storage.WeightedMean() ).view() result = h.view() + empty for field in h.view().dtype.names: assert result[field] == approx(h.view()[field]) assert not np.any(np.isnan(result.value)) def test_mean_view_scalar_scale(): h = bh.Histogram(bh.axis.Integer(0, 4), storage=bh.storage.Mean()) h.fill([0, 0, 1], sample=[2.0, 4.0, 6.0]) v = h.view() for scaled in (v * 3, 3 * v): assert scaled.count == approx(v.count) # weights unchanged assert scaled.value == approx(v.value * 3) assert scaled._sum_of_deltas_squared == approx(v._sum_of_deltas_squared * 9) divided = v / 2 assert divided.count == approx(v.count) assert divided.value == approx(v.value / 2) assert divided._sum_of_deltas_squared == approx(v._sum_of_deltas_squared / 4) v2 = v.copy() v2 *= 3 assert v2.value == approx(v.value * 3) v2 = v.copy() v2 /= 2 assert v2.value == approx(v.value / 2) def test_weighted_mean_view_scalar_scale(weighted_mean_pair): h1, _ = weighted_mean_pair v = h1.view() scaled = v * 2 assert scaled.sum_of_weights == approx(v.sum_of_weights) assert scaled.sum_of_weights_squared == approx(v.sum_of_weights_squared) assert scaled.value == approx(v.value * 2) assert scaled._sum_of_weighted_deltas_squared == approx( v._sum_of_weighted_deltas_squared * 4 ) def test_mean_view_reduce_sum(): h = bh.Histogram(bh.axis.Integer(0, 4), storage=bh.storage.Mean()) h.fill([0, 0, 1, 3], sample=[1.0, 2.0, 3.0, 9.0]) v = h.view() reduced = v.sum() assert isinstance(reduced, bh.accumulators.Mean) projected = h.project().view() assert reduced.count == approx(projected.count) assert reduced.value == approx(projected.value) assert reduced._sum_of_deltas_squared == approx(projected._sum_of_deltas_squared) def test_weighted_mean_view_reduce_sum(weighted_mean_pair): h1, _ = weighted_mean_pair reduced = h1.view().sum() assert isinstance(reduced, bh.accumulators.WeightedMean) projected = h1.project().view() assert reduced.sum_of_weights == approx(projected.sum_of_weights) assert reduced.value == approx(projected.value) assert reduced._sum_of_weighted_deltas_squared == approx( projected._sum_of_weighted_deltas_squared ) def test_mean_view_reduce_keepdims(): h = bh.Histogram( bh.axis.Integer(0, 3), bh.axis.Integer(0, 2), storage=bh.storage.Mean() ) h.fill([0, 0, 1, 2], [0, 1, 1, 0], sample=[1.0, 2.0, 3.0, 4.0]) v = h.view() plain = np.add.reduce(v, axis=0) kept = np.add.reduce(v, axis=0, keepdims=True) assert plain.shape == (2,) assert kept.shape == (1, 2) for field in v.dtype.names: assert kept[field][0] == approx(plain[field]) # Issue #1143 (B2): mean reductions used to silently ignore reduction # arguments (out, where, initial, dtype) they cannot honor. def test_mean_view_reduce_rejects_unsupported_kwargs(mean_pair): h1, _ = mean_pair v = h1.view() with pytest.raises(TypeError, match="axis and keepdims"): np.sum(v, dtype=np.float32) with pytest.raises(TypeError, match="axis and keepdims"): np.sum(v, initial=2.0) with pytest.raises(TypeError, match="axis and keepdims"): np.sum(v, where=np.zeros(v.shape, dtype=bool)) with pytest.raises(TypeError, match="out="): np.sum(v, out=np.empty(())) def test_mean_view_reduce_out_matching_dtype(mean_pair): h1, _ = mean_pair v = h1.view() out = np.zeros((), dtype=v.dtype) result = np.sum(v, out=out) assert np.shares_memory(result, out) expected = np.sum(v) assert out["count"] == approx(expected.count) assert out["value"] == approx(expected.value) assert out["_sum_of_deltas_squared"] == approx(expected._sum_of_deltas_squared) def test_mean_view_rejects_floor_division(mean_pair): h1, _ = mean_pair v = h1.view() with pytest.raises(TypeError): v // 2 with pytest.raises(TypeError): 2 // v def test_mean_view_rejects_subtraction(mean_pair): h1, h2 = mean_pair v1, v2 = h1.view(), h2.view() with pytest.raises(TypeError): v1 - v2 with pytest.raises(TypeError): v1 - 5 def test_mean_view_rejects_scalar_add(mean_pair): h1, _ = mean_pair with pytest.raises(TypeError): h1.view() + 5 def test_mean_view_rejects_view_product(mean_pair): h1, h2 = mean_pair v1, v2 = h1.view(), h2.view() with pytest.raises(TypeError): v1 * v2 with pytest.raises(TypeError): v1 / v2 # Issue #696 def test_view_cumsum(): h = bh.Histogram( bh.axis.Integer(1, 10, underflow=True, overflow=False), storage=bh.storage.Weight(), ) h.fill([2, 3], weight=[1.5, 2.5]) view = h.view() c_view = np.cumsum(view) assert c_view.value == approx(np.cumsum(view.value)) assert c_view.variance == approx(np.cumsum(view.variance))