[
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\ncover/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\n.pybuilder/\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n#   For a library or package, you might want to ignore these files since the code is\n#   intended to run in multiple environments; otherwise, check them in:\n# .python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# poetry\n#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.\n#   This is especially recommended for binary packages to ensure reproducibility, and is more\n#   commonly ignored for libraries.\n#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control\n#poetry.lock\n\n# pdm\n#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.\n#pdm.lock\n#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it\n#   in version control.\n#   https://pdm.fming.dev/#use-with-ide\n.pdm.toml\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/\n\n# PyCharm\n#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can\n#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore\n#  and can be added to the global gitignore or merged into this file.  For a more nuclear\n#  option (not recommended) you can uncomment the following to ignore the entire idea folder.\n#.idea/\n.idea/.gitignore\n.idea/evoc.iml\n.idea/misc.xml\n.idea/modules.xml\n.idea/vcs.xml\n.idea/inspectionProfiles/profiles_settings.xml\n.idea/inspectionProfiles/Project_Default.xml\n.vscode/settings.json\n"
  },
  {
    "path": ".readthedocs.yaml",
    "content": "# .readthedocs.yaml\n# Read the Docs configuration file\n# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details\n\n# Required\nversion: 2\n\n# Set the version of Python and other tools you might need\nbuild:\n  os: ubuntu-22.04\n  tools:\n    python: \"3.11\"\n    # You can also specify other tools here\n\n# Build documentation in the docs/ directory with Sphinx\nsphinx:\n   configuration: doc/source/conf.py\n   fail_on_warning: false\n\n# If using Sphinx, optionally build your docs in additional formats such as PDF\nformats:\n   - pdf\n   - epub\n\n# Optionally declare the Python requirements required to build your docs\npython:\n   install:\n   - requirements: doc/requirements.txt\n   - method: pip\n     path: .\n     extra_requirements:\n       - docs\n\n# Optional but recommended, specify the Python version to use\n# https://docs.readthedocs.io/en/stable/config-file/v2.html#python\n"
  },
  {
    "path": "LICENSE",
    "content": "BSD 2-Clause License\n\nCopyright (c) 2024, Tutte Institute for Mathematics and Computing\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n   list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\n   this list of conditions and the following disclaimer in the documentation\n   and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "README.rst",
    "content": ".. image:: doc/evoc_logo_horizontal.png\n  :width: 600\n  :align: center\n  :alt: EVōC Logo\n\n====\nEVōC\n====\n\nEVōC (pronounced as \"evoke\") provides Embedding Vector Oriented Clustering.\nEVōC is a library for fast and flexible clustering of large datasets of high dimensional embedding vectors. \nIf you have CLIP-vectors, outputs from sentence-transformers, or openAI, or Cohere embed, and you want\nto quickly get good clusters out this is the library for you. EVōC takes all the good parts of the \ncombination of UMAP + HDBSCAN for embedding clustering, improves upon them, and removes all \nthe time-consuming parts. By specializing directly to embedding vectors we can get good\nquality clustering with fewer hyper-parameters to tune and in a fraction of the time.\n\nEVōC is the library to use if you want:\n\n * Fast clustering of embedding vectors on CPU\n * Multi-granularity clustering, and automatic selection of the number of clusters\n * Clustering of int8 or binary quantized embedding vectors that works out-of-the-box\n\n As of now this is very much an early beta version of the library. Things can and will break right now.\n We would welcome feedback, use cases and feature suggestions however.\n\n-------------\nDocumentation\n-------------\n\nThe full documentation is available on Read the Docs:\n`https://evoc.readthedocs.io/en/latest/ <https://evoc.readthedocs.io/en/latest/>`_\n\n-----------\nBasic Usage\n-----------\n\nEVōC follows the scikit-learn API, so it should be familiar to most users. You can use EVōC wherever\nyou might have previously been using other sklearn clustering algorithms. Here is a simple example\n\n.. code-block:: python\n\n    import evoc\n    from sklearn.datasets import make_blobs\n\n    data, _ = make_blobs(n_samples=100_000, n_features=1024, centers=100)\n\n    clusterer = evoc.EVoC()\n    cluster_labels = clusterer.fit_predict(data)\n\nSome more unique features include the generation of multiple layers of cluster granularity,\nthe ability to extract a hierarchy of clusters across those layers, and automatic duplicate \n(or very near duplicate) detection.\n\n.. code-block:: python\n\n    import evoc\n    from sklearn.datasets import make_blobs\n\n    data, _ = make_blobs(n_samples=100_000, n_features=1024, centers=100)\n\n    clusterer = evoc.EVoC()\n    cluster_labels = clusterer.fit_predict(data)\n    cluster_layers = clusterer.cluster_layers_\n    hierarchy = clusterer.cluster_tree_\n    potential_duplicates = clusterer.duplicates_\n\nThe cluster layers are a list of cluster label vectors with the first being the finest grained\nand later layers being coarser grained. This is ideal for layered topic modelling and use with\n`DataMapPlot <https://github.com/TutteInstitute/datamapplot>`_. See \n`this data map <https://lmcinnes.github.io/datamapplot_examples/ArXiv_data_map_example.html>`_\nfor an example of using these layered clusters in topic modelling (zoom in to access finer \ngrained topics).\n\n------------\nInstallation\n------------\n\nEVōC has a small set of dependencies:\n\n * numpy\n * scikit-learn\n * numba\n * tqdm\n * tbb\n\nYou can install EVōC from PyPI using pip:\n\n.. code-block:: bash\n\n    pip install evoc\n\nTo install the latest version of EVōC from source:\n\n.. code-block:: bash\n\n    pip install git+https://github.com/TutteInstitute/evoc.git\n\n\n----------\nReferences\n----------\n\nThe algorithm implemented in EVōC is not published anywhere at this time. If you would like\nto cite something in reference to EVōC, I would encourage you to cite the PLSCAN paper\non which the cluster extraction in EVōC is based:\n\nPlease cite:\n\n    D.M. Bot, L. McInnes, J. Aerts.\n    *Persistent Multiscale Density-based Clustering.*\n    In: arXiv preprint arXiv:2512.16558, 2025.\n    https://arxiv.org/abs/2512.16558.\n    \n-------\nLicense\n-------\n\nEVōC is BSD (2-clause) licensed. See the LICENSE file for details.\n\n------------\nContributing\n------------\n\nContributions are more than welcome! If you have ideas for features of projects please get in touch. Everything from\ncode to notebooks to examples and documentation are all *equally valuable* so please don't feel you can't contribute.\nTo contribute please `fork the project <https://github.com/TutteInstitute/evoc/issues#fork-destination-box>`_ make your\nchanges and submit a pull request. We will do our best to work through any issues with you and get your code merged in.\n"
  },
  {
    "path": "azure-pipelines.yml",
    "content": "# Trigger a build when there is a push to the main branch or a tag starts with release-\ntrigger:\n  branches:\n    include:\n    - main\n  tags:\n    include:\n    - release-*\n\n# Trigger a build when there is a pull request to the main branch\n# Ignore PRs that are just updating the docs\npr:\n  branches:\n    include:\n    - main\n    exclude:\n    - doc/*\n    - README.rst\n\nparameters:\n  - name: includeReleaseCandidates\n    displayName: \"Allow pre-release dependencies\"\n    type: boolean\n    default: false\n\nvariables:\n  triggeredByPullRequest: $[eq(variables['Build.Reason'], 'PullRequest')]\n\nstages:\n  - stage: RunAllTests\n    displayName: Run test suite\n    jobs:\n      - job: run_platform_tests\n        strategy:\n          matrix:\n            mac_py310:\n              imageName: 'macOS-latest'\n              python.version: '3.10'\n            linux_py310:\n              imageName: 'ubuntu-latest'\n              python.version: '3.10'\n            windows_py310:\n              imageName: 'windows-latest'\n              python.version: '3.10'\n            mac_py311:\n              imageName: 'macOS-latest'\n              python.version: '3.11'\n            linux_py311:\n              imageName: 'ubuntu-latest'\n              python.version: '3.11'\n            windows_py311:\n              imageName: 'windows-latest'\n              python.version: '3.11'\n            mac_py312:\n              imageName: 'macOS-latest'\n              python.version: '3.12'\n            linux_py312:\n              imageName: 'ubuntu-latest'\n              python.version: '3.12'\n            windows_py312:\n              imageName: 'windows-latest'\n              python.version: '3.12'\n            mac_py313:\n              imageName: 'macOS-latest'\n              python.version: '3.13'\n            linux_py313:\n              imageName: 'ubuntu-latest'\n              python.version: '3.13'\n            windows_py313:\n              imageName: 'windows-latest'\n              python.version: '3.13'\n            mac_py314:\n              imageName: 'macOS-latest'\n              python.version: '3.14'\n            linux_py314:\n              imageName: 'ubuntu-latest'\n              python.version: '3.14'\n            windows_py314:\n              imageName: 'windows-latest'\n              python.version: '3.14'\n        pool:\n          vmImage: $(imageName)\n\n        steps:\n        - task: UsePythonVersion@0\n          inputs:\n            versionSpec: '$(python.version)'\n          displayName: 'Use Python $(python.version)'\n\n        - script: |\n            python -m pip install --upgrade pip\n          displayName: 'Upgrade pip'\n\n        # 1. Install the full LLVM package only if the OS is macOS\n        - script: |\n            brew install llvm@20\n            # Homebrew formula names can change, so we ensure it links correctly if necessary\n            brew link --force --overwrite llvm@20\n          displayName: 'Install LLVM via Homebrew (macOS only)'\n          condition: eq(variables['Agent.OS'], 'Darwin')\n\n        # 2. Find the Homebrew install path and set the environment variable only on macOS\n        - script: |\n            # Determine the LLVM install prefix dynamically\n            LLVM_PREFIX=$(brew --prefix llvm@20)\n\n            # Set the LLVM_CONFIG environment variable used by llvmlite's build script\n            echo \"##vso[task.setvariable variable=LLVM_CONFIG]$LLVM_PREFIX/bin/llvm-config\"\n            echo \"LLVM_CONFIG set to: $LLVM_CONFIG\"\n\n            # Also set CMAKE_PREFIX_PATH in case other dependencies need it\n            echo \"##vso[task.setvariable variable=CMAKE_PREFIX_PATH]$LLVM_PREFIX/lib/cmake\"\n          displayName: 'Configure LLVM Environment Variables (macOS only)'\n          condition: eq(variables['Agent.OS'], 'Darwin')\n\n        - script: |\n            python -m pip install -U uv\n            uv sync --group cicd\n          env:\n            # Ensure that the LLVM_CONFIG environment variable is available during installation\n            LLVM_CONFIG: $(LLVM_CONFIG)\n            CMAKE_PREFIX_PATH: $(CMAKE_PREFIX_PATH)\n          displayName: 'Install package and dependencies'\n\n        - script: |\n            uv run pytest evoc/tests --show-capture=no -v --disable-warnings --junitxml=junit/test-results.xml --cov=evoc/ --cov-report=xml --cov-report=html\n          displayName: 'Run tests'\n          condition: ne(variables['Agent.OS'], 'Darwin')\n\n        - script: |\n            uv run pytest evoc/tests -v --capture=tee-sys --disable-warnings --junitxml=junit/test-results.xml --cov=evoc/ --cov-report=xml --cov-report=html\n          displayName: 'Run tests'\n          condition: eq(variables['Agent.OS'], 'Darwin')\n\n        - task: PublishTestResults@2\n          inputs:\n            testResultsFiles: '$(System.DefaultWorkingDirectory)/**/coverage.xml'\n            testRunTitle: '$(Agent.OS) - $(Build.BuildNumber)[$(Agent.JobName)] - Python $(python.version)'\n          condition: succeededOrFailed()\n\n  - stage: BuildPublishArtifact\n    dependsOn: RunAllTests\n    condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/release-'), eq(variables.triggeredByPullRequest, false))\n    jobs:\n      - job: BuildArtifacts\n        displayName: Build source dists and wheels    \n        pool:\n          vmImage: 'ubuntu-latest'\n        steps:\n        - task: UsePythonVersion@0\n          inputs:\n            versionSpec: '3.13'\n          displayName: 'Use Python 3.13'\n\n        - script: |\n            python -m pip install --upgrade pip\n            python -m pip install -U uv\n            uv sync\n          displayName: 'Install dependencies'\n\n        - script: |\n            uv build --no-sources --sdist --wheel\n          displayName: 'Build package'\n\n        - bash: |\n            export PACKAGE_VERSION=\"$(uv version --short)\"\n            echo \"Package Version: ${PACKAGE_VERSION}\"\n            echo \"##vso[task.setvariable variable=packageVersionFormatted;]release-${PACKAGE_VERSION}\"\n          displayName: 'Get package version'\n\n        - script: |\n            echo \"Version in git tag $(Build.SourceBranchName) does not match version derived from setup.py $(packageVersionFormatted)\"\n            exit 1\n          displayName: Raise error if version doesnt match tag\n          condition: and(succeeded(), ne(variables['Build.SourceBranchName'], variables['packageVersionFormatted']))\n\n        - task: DownloadSecureFile@1\n          name: PYPIRC_CONFIG\n          displayName: 'Download pypirc'\n          inputs:\n            secureFile: 'pypirc'  \n\n        - script: |\n            uvx twine check dist/*\n            uvx twine upload --repository pypi --config-file $(PYPIRC_CONFIG.secureFilePath) dist/*\n          displayName: 'Upload to PyPI'\n          condition: and(succeeded(), eq(variables['Build.SourceBranchName'], variables['packageVersionFormatted']))\n"
  },
  {
    "path": "doc/Makefile",
    "content": "# Makefile for Sphinx documentation\n\n# You can set these variables from the command line\nSPHINXOPTS    =\nSPHINXBUILD   = sphinx-build\nSOURCEDIR     = source\nBUILDDIR      = build\n\n# Put it first so that \"make\" without argument is like \"make help\"\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx-build\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n# Custom targets\nclean:\n\t@$(SPHINXBUILD) -M clean \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\nhtml:\n\t@$(SPHINXBUILD) -b html \"$(SOURCEDIR)\" \"$(BUILDDIR)/html\" $(SPHINXOPTS) $(O)\n\t@echo \"Build finished. The HTML pages are in $(BUILDDIR)/html.\"\n\nlivehtml:\n\tsphinx-autobuild \"$(SOURCEDIR)\" \"$(BUILDDIR)/html\" $(SPHINXOPTS) $(O)\n\nlinkcheck:\n\t@$(SPHINXBUILD) -b linkcheck \"$(SOURCEDIR)\" \"$(BUILDDIR)/linkcheck\" $(SPHINXOPTS) $(O)\n\t@echo \"Link check complete; look for any errors in the above output or in $(BUILDDIR)/linkcheck/output.txt.\"\n\ndoctest:\n\t@$(SPHINXBUILD) -b doctest \"$(SOURCEDIR)\" \"$(BUILDDIR)/doctest\" $(SPHINXOPTS) $(O)\n\t@echo \"Testing of doctests in the sources finished, look at the results in $(BUILDDIR)/doctest/output.txt.\"\n"
  },
  {
    "path": "doc/README.md",
    "content": "# EVoC Documentation\n\nThis directory contains the Sphinx documentation for EVoC.\n\n## Structure\n\n```\ndoc/\n├── build/                  # Generated documentation (HTML, PDF, etc.)\n├── source/                 # Source files for documentation  \n│   ├── _static/           # Static files (CSS, images, etc.)\n│   ├── _templates/        # Custom Sphinx templates\n│   ├── api/               # API documentation files\n│   ├── notebooks/         # Jupyter notebook examples\n│   ├── tutorials/         # Step-by-step tutorials\n│   ├── conf.py           # Sphinx configuration\n│   ├── index.rst         # Main documentation page\n│   └── *.rst             # Other documentation pages\n├── requirements.txt       # Documentation dependencies\n├── Makefile              # Build commands (Unix)\n├── build_docs.sh         # Automated build script (Unix)\n├── build_docs.bat        # Automated build script (Windows)  \n└── README.md             # This file\n```\n\n## Building the Documentation\n\n### Prerequisites\n\n1. Python 3.8 or later\n2. Git (for development installation)\n\n### Quick Build\n\n**Unix/macOS:**\n```bash\ncd doc\n./build_docs.sh\n```\n\n**Windows:**\n```cmd\ncd doc\nbuild_docs.bat\n```\n\n### Manual Build\n\n1. Install dependencies:\n```bash\npip install -r requirements.txt\n```\n\n2. Install EVoC in development mode:\n```bash\npip install -e ../..\n```\n\n3. Build documentation:\n```bash\nmake html\n```\n\n4. Open `build/html/index.html` in your browser\n\n### Advanced Options\n\n**Clean build:**\n```bash\nmake clean html\n```\n\n**Check links:**\n```bash\nmake linkcheck\n```\n\n**Run doctests:**\n```bash\nmake doctest\n```\n\n**Live reload during development:**\n```bash\npip install sphinx-autobuild\nmake livehtml\n```\n\n## Features\n\n- **Sphinx RTD Theme**: Professional appearance matching ReadTheDocs\n- **Numpydoc**: Automatic parsing of NumPy-style docstrings\n- **Nbsphinx**: Integration of Jupyter notebooks as documentation\n- **Autodoc**: Automatic API documentation generation\n- **ReadTheDocs Ready**: Configured for automatic deployment\n\n## Adding Content\n\n### New Documentation Pages\n\n1. Create `.rst` files in `source/`\n2. Add them to the `toctree` in `index.rst`\n3. Rebuild documentation\n\n### Jupyter Notebooks\n\n1. Add `.ipynb` files to `source/notebooks/`\n2. Add them to `source/notebooks/index.rst`\n3. Notebooks are automatically converted during build\n\n### API Documentation\n\nAPI documentation is automatically generated from docstrings. To add new modules:\n\n1. Add the module to `source/api/index.rst`\n2. Create a dedicated `.rst` file if needed\n3. Rebuild documentation\n\n## ReadTheDocs Integration\n\nThis documentation is configured for ReadTheDocs deployment:\n\n- Configuration: `.readthedocs.yaml` in project root\n- Requirements: `doc/requirements.txt`\n- Python version: 3.11 (configurable in `.readthedocs.yaml`)\n\n## Troubleshooting\n\n**Import errors during build:**\n- Ensure EVoC is installed in development mode: `pip install -e ../..`\n- Check that all dependencies are installed: `pip install -r requirements.txt`\n\n**Missing modules in API docs:**\n- Verify the module paths in `source/api/index.rst`\n- Check that modules are importable from the documentation directory\n\n**Notebook execution errors:**\n- Notebooks are not executed by default (`nbsphinx_execute = 'never'`)\n- To execute notebooks during build, change to `nbsphinx_execute = 'always'` in `conf.py`\n\n**Theme or styling issues:**\n- Check `source/_static/custom.css` for customizations\n- Verify `sphinx_rtd_theme` is installed\n\n## Contributing\n\nWhen adding new documentation:\n\n1. Follow reStructuredText formatting\n2. Use NumPy-style docstrings for API documentation  \n3. Include code examples where appropriate\n4. Test build locally before submitting\n5. Keep notebook outputs clear for examples\n\nFor more details, see the main EVoC contributing guidelines.\n"
  },
  {
    "path": "doc/build_docs.bat",
    "content": "@echo off\nREM Documentation build script for EVoC (Windows)\n\necho Building EVoC Documentation\necho ==========================\n\nREM Check if we're in the right directory\nif not exist \"source\\conf.py\" (\n    echo Error: Run this script from the doc directory\n    exit /b 1\n)\n\nREM Check if virtual environment exists, create if needed\nif not exist \"venv\" (\n    echo Creating virtual environment...\n    python -m venv venv\n)\n\nREM Activate virtual environment\ncall venv\\Scripts\\activate.bat\n\nREM Install requirements\necho Installing documentation requirements...\npip install -r requirements.txt\n\nREM Install EVoC in development mode\necho Installing EVoC in development mode...\npip install -e ..\\..\n\nREM Clean previous build\necho Cleaning previous build...\nmake clean\n\nREM Build HTML documentation\necho Building HTML documentation...\nmake html\n\nif %ERRORLEVEL% equ 0 (\n    echo Documentation built successfully!\n    echo Open build\\html\\index.html in your browser to view\n) else (\n    echo Build failed with errors\n    exit /b 1\n)\n\necho Build complete!\n"
  },
  {
    "path": "doc/build_docs.sh",
    "content": "#!/bin/bash\n\n# Documentation build script for EVoC\n\nset -e  # Exit on any error\n\necho \"Building EVoC Documentation\"\necho \"==========================\"\n\n# Check if we're in the right directory\nif [ ! -f \"source/conf.py\" ]; then\n    echo \"Error: Run this script from the doc directory\"\n    exit 1\nfi\n\n# Check if virtual environment exists, create if needed\nif [ ! -d \"venv\" ]; then\n    echo \"Creating virtual environment...\"\n    python -m venv venv\nfi\n\n# Activate virtual environment\nsource venv/bin/activate\n\n# Install requirements\necho \"Installing documentation requirements...\"\npip install -r requirements.txt\n\n# Install EVoC in development mode\necho \"Installing EVoC in development mode...\"\npip install -e ../.\n\n# Clean previous build\necho \"Cleaning previous build...\"\nmake clean\n\n# Build HTML documentation  \necho \"Building HTML documentation...\"\nmake html\n\n# Check for warnings\nif [ $? -eq 0 ]; then\n    echo \"Documentation built successfully!\"\n    echo \"Open build/html/index.html in your browser to view\"\nelse\n    echo \"Build failed with errors\"\n    exit 1\nfi\n\n# Optional: Run link check\nif [ \"$1\" = \"--check-links\" ]; then\n    echo \"Checking links...\"\n    make linkcheck\nfi\n\n# Optional: Run doctests\nif [ \"$1\" = \"--test\" ]; then\n    echo \"Running doctests...\"\n    make doctest\nfi\n\necho \"Build complete!\"\n"
  },
  {
    "path": "doc/requirements.txt",
    "content": "# Sphinx documentation requirements\nsphinx>=7.0.0\nsphinx-rtd-theme>=2.0.0\nnumpydoc>=1.6.0\nnbsphinx>=0.9.0\nipython>=8.0.0\nipykernel>=6.0.0\njupyter>=1.0.0\nmatplotlib>=3.5.0\nnumpy>=1.21.0\nscipy>=1.7.0\nscikit-learn>=1.0.0\npandas>=1.3.0\nnumba>=0.56.0\n\n# Optional but recommended for better notebook handling\npandoc>=2.0\nipywidgets>=8.0.0\n"
  },
  {
    "path": "doc/source/_static/custom.css",
    "content": "/* Custom CSS for EVoC documentation */\n\n/* Improve code block styling */\n.highlight {\n    background-color: #f8f8f8;\n    border: 1px solid #e1e4e5;\n    border-radius: 4px;\n    padding: 8px;\n    margin: 12px 0;\n}\n\n/* Better parameter list formatting */\n.field-list {\n    margin: 1em 0;\n}\n\n.field-list dt {\n    font-weight: bold;\n    color: #2980b9;\n}\n\n/* Notebook cell styling */\n.nbinput .highlight,\n.nboutput .highlight {\n    border-left: 4px solid #1f8c8c;\n    margin: 0.5em 0;\n}\n\n/* API documentation improvements */\n.py.class dt {\n    background-color: #f0f0f0;\n    border-left: 4px solid #3498db;\n    padding: 8px;\n    margin-top: 20px;\n}\n\n.py.method dt {\n    background-color: #f9f9f9;\n    border-left: 3px solid #95a5a6;\n    padding: 6px;\n    margin-top: 15px;\n}\n\n/* Parameter tables */\n.docutils th {\n    background-color: #34495e;\n    color: white;\n    padding: 8px;\n}\n\n.docutils td {\n    padding: 6px 8px;\n    border-bottom: 1px solid #ecf0f1;\n}\n\n/* Admonition improvements */\n.admonition {\n    margin: 20px 0;\n    padding: 15px;\n    border-radius: 6px;\n}\n\n.admonition.note {\n    background-color: #e8f4fd;\n    border-left: 4px solid #3498db;\n}\n\n.admonition.warning {\n    background-color: #fdf4e8;\n    border-left: 4px solid #f39c12;\n}\n\n/* Code span improvements */\ncode.literal {\n    background-color: #f1f2f3;\n    color: #e74c3c;\n    padding: 2px 4px;\n    border-radius: 3px;\n    font-size: 90%;\n}\n\n/* Sidebar improvements */\n.wy-nav-side {\n    background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%);\n}\n\n/* Footer customization */\n.rst-footer-buttons {\n    margin-top: 30px;\n    padding-top: 20px;\n    border-top: 1px solid #e1e4e5;\n}\n"
  },
  {
    "path": "doc/source/api/evoc.cluster_trees.rst",
    "content": "evoc.cluster_trees\n==================\n\n.. automodule:: evoc.cluster_trees\n   :members:\n   :undoc-members:\n   :show-inheritance:\n"
  },
  {
    "path": "doc/source/api/evoc.clustering.rst",
    "content": "evoc.clustering\n===============\n\n.. automodule:: evoc.clustering\n   :members:\n   :undoc-members:\n   :show-inheritance:\n"
  },
  {
    "path": "doc/source/api/evoc.clustering_utilities.rst",
    "content": "evoc.clustering_utilities  \n=========================\n\n.. automodule:: evoc.clustering_utilities\n   :members:\n   :undoc-members:\n   :show-inheritance:\n"
  },
  {
    "path": "doc/source/api/generated/evoc.EVoC.rst",
    "content": "﻿evoc.EVoC\n=========\n\n.. currentmodule:: evoc\n\n.. autoclass:: EVoC\n\n   \n   .. automethod:: __init__\n\n   \n   .. rubric:: Methods\n\n   .. autosummary::\n   \n      ~EVoC.__init__\n      ~EVoC.fit\n      ~EVoC.fit_predict\n      ~EVoC.get_metadata_routing\n      ~EVoC.get_params\n      ~EVoC.set_params\n   \n   \n\n   \n   \n   .. rubric:: Attributes\n\n   .. autosummary::\n   \n      ~EVoC.cluster_tree_\n   \n   "
  },
  {
    "path": "doc/source/api/generated/evoc.boruvka.parallel_boruvka.rst",
    "content": "﻿evoc.boruvka.parallel\\_boruvka\n==============================\n\n.. currentmodule:: evoc.boruvka\n\n.. autofunction:: parallel_boruvka"
  },
  {
    "path": "doc/source/api/generated/evoc.cluster_trees.condense_tree.rst",
    "content": "﻿evoc.cluster\\_trees.condense\\_tree\n==================================\n\n.. currentmodule:: evoc.cluster_trees\n\n.. autofunction:: condense_tree"
  },
  {
    "path": "doc/source/api/generated/evoc.cluster_trees.extract_leaves.rst",
    "content": "﻿evoc.cluster\\_trees.extract\\_leaves\n===================================\n\n.. currentmodule:: evoc.cluster_trees\n\n.. autofunction:: extract_leaves"
  },
  {
    "path": "doc/source/api/generated/evoc.cluster_trees.get_cluster_label_vector.rst",
    "content": "﻿evoc.cluster\\_trees.get\\_cluster\\_label\\_vector\n===============================================\n\n.. currentmodule:: evoc.cluster_trees\n\n.. autofunction:: get_cluster_label_vector"
  },
  {
    "path": "doc/source/api/generated/evoc.cluster_trees.get_point_membership_strength_vector.rst",
    "content": "﻿evoc.cluster\\_trees.get\\_point\\_membership\\_strength\\_vector\n============================================================\n\n.. currentmodule:: evoc.cluster_trees\n\n.. autofunction:: get_point_membership_strength_vector"
  },
  {
    "path": "doc/source/api/generated/evoc.cluster_trees.mst_to_linkage_tree.rst",
    "content": "﻿evoc.cluster\\_trees.mst\\_to\\_linkage\\_tree\n==========================================\n\n.. currentmodule:: evoc.cluster_trees\n\n.. autofunction:: mst_to_linkage_tree"
  },
  {
    "path": "doc/source/api/generated/evoc.clustering_utilities.binary_search_for_n_clusters.rst",
    "content": "﻿evoc.clustering\\_utilities.binary\\_search\\_for\\_n\\_clusters\n===========================================================\n\n.. currentmodule:: evoc.clustering_utilities\n\n.. autofunction:: binary_search_for_n_clusters"
  },
  {
    "path": "doc/source/api/generated/evoc.clustering_utilities.build_cluster_tree.rst",
    "content": "﻿evoc.clustering\\_utilities.build\\_cluster\\_tree\n===============================================\n\n.. currentmodule:: evoc.clustering_utilities\n\n.. autofunction:: build_cluster_tree"
  },
  {
    "path": "doc/source/api/generated/evoc.clustering_utilities.find_duplicates.rst",
    "content": "﻿evoc.clustering\\_utilities.find\\_duplicates\n===========================================\n\n.. currentmodule:: evoc.clustering_utilities\n\n.. autofunction:: find_duplicates"
  },
  {
    "path": "doc/source/api/generated/evoc.clustering_utilities.find_peaks.rst",
    "content": "﻿evoc.clustering\\_utilities.find\\_peaks\n======================================\n\n.. currentmodule:: evoc.clustering_utilities\n\n.. autofunction:: find_peaks"
  },
  {
    "path": "doc/source/api/generated/evoc.clustering_utilities.select_diverse_peaks.rst",
    "content": "﻿evoc.clustering\\_utilities.select\\_diverse\\_peaks\n=================================================\n\n.. currentmodule:: evoc.clustering_utilities\n\n.. autofunction:: select_diverse_peaks"
  },
  {
    "path": "doc/source/api/generated/evoc.evoc_clusters.rst",
    "content": "﻿evoc.evoc\\_clusters\n===================\n\n.. currentmodule:: evoc\n\n.. autofunction:: evoc_clusters"
  },
  {
    "path": "doc/source/api/generated/evoc.graph_construction.neighbor_graph_matrix.rst",
    "content": "﻿evoc.graph\\_construction.neighbor\\_graph\\_matrix\n================================================\n\n.. currentmodule:: evoc.graph_construction\n\n.. autofunction:: neighbor_graph_matrix"
  },
  {
    "path": "doc/source/api/generated/evoc.knn_graph.knn_graph.rst",
    "content": "﻿evoc.knn\\_graph.knn\\_graph\n==========================\n\n.. currentmodule:: evoc.knn_graph\n\n.. autofunction:: knn_graph"
  },
  {
    "path": "doc/source/api/generated/evoc.label_propagation.label_propagation_init.rst",
    "content": "﻿evoc.label\\_propagation.label\\_propagation\\_init\n================================================\n\n.. currentmodule:: evoc.label_propagation\n\n.. autofunction:: label_propagation_init"
  },
  {
    "path": "doc/source/api/generated/evoc.node_embedding.node_embedding.rst",
    "content": "﻿evoc.node\\_embedding.node\\_embedding\n====================================\n\n.. currentmodule:: evoc.node_embedding\n\n.. autofunction:: node_embedding"
  },
  {
    "path": "doc/source/api/generated/evoc.numba_kdtree.build_kdtree.rst",
    "content": "﻿evoc.numba\\_kdtree.build\\_kdtree\n================================\n\n.. currentmodule:: evoc.numba_kdtree\n\n.. autofunction:: build_kdtree"
  },
  {
    "path": "doc/source/api/index.rst",
    "content": "API Reference\n=============\n\nThis section contains the complete API reference for EVoC.\n\nMain Classes and Functions\n--------------------------\n\n.. currentmodule:: evoc\n\n.. autosummary::\n   :toctree: generated/\n   :nosignatures:\n\n   EVoC\n   evoc_clusters\n\nCore Clustering\n---------------\n\n.. autoclass:: EVoC\n   :members:\n   :inherited-members:\n   :show-inheritance:\n\n.. autofunction:: evoc_clusters\n\nUtility Functions\n-----------------\n\n.. currentmodule:: evoc.clustering_utilities\n\n.. autosummary::\n   :toctree: generated/\n   :nosignatures:\n\n   find_peaks\n   binary_search_for_n_clusters\n   select_diverse_peaks\n   build_cluster_tree\n   find_duplicates\n\n.. autofunction:: find_peaks\n.. autofunction:: binary_search_for_n_clusters  \n.. autofunction:: select_diverse_peaks\n.. autofunction:: build_cluster_tree\n.. autofunction:: find_duplicates\n\nTree Operations\n---------------\n\n.. currentmodule:: evoc.cluster_trees\n\n.. autosummary::\n   :toctree: generated/\n   :nosignatures:\n\n   mst_to_linkage_tree\n   condense_tree\n   extract_leaves\n   get_cluster_label_vector\n   get_point_membership_strength_vector\n\n.. autofunction:: mst_to_linkage_tree\n.. autofunction:: condense_tree\n.. autofunction:: extract_leaves\n.. autofunction:: get_cluster_label_vector\n.. autofunction:: get_point_membership_strength_vector\n\nGraph Construction\n------------------\n\n.. currentmodule:: evoc.knn_graph\n\n.. autosummary::\n   :toctree: generated/\n   :nosignatures:\n\n   knn_graph\n\n.. autofunction:: knn_graph\n\n.. currentmodule:: evoc.graph_construction\n\n.. autosummary::\n   :toctree: generated/\n   :nosignatures:\n\n   neighbor_graph_matrix\n\n.. autofunction:: neighbor_graph_matrix\n\nNode Embedding\n--------------\n\n.. currentmodule:: evoc.node_embedding\n\n.. autosummary::\n   :toctree: generated/\n   :nosignatures:\n\n   node_embedding\n\n.. autofunction:: node_embedding\n\n.. currentmodule:: evoc.label_propagation\n\n.. autosummary::\n   :toctree: generated/\n   :nosignatures:\n\n   label_propagation_init\n\n.. autofunction:: label_propagation_init\n\nAlgorithm Components\n--------------------\n\n.. currentmodule:: evoc.boruvka\n\n.. autosummary::\n   :toctree: generated/\n   :nosignatures:\n\n   parallel_boruvka\n\n.. autofunction:: parallel_boruvka\n\n.. currentmodule:: evoc.numba_kdtree\n\n.. autosummary::\n   :toctree: generated/\n   :nosignatures:\n\n   build_kdtree\n\n.. autofunction:: build_kdtree\n"
  },
  {
    "path": "doc/source/benchmarks.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"93984a77-bccc-46fc-a7e1-4eb862d10f6e\",\n   \"metadata\": {},\n   \"source\": [\n    \"# Performance benchmarks\\n\",\n    \"\\n\",\n    \"This notebook provides performance benchmarks for EVoC in comparison to some commonly used options for embedding vector clustering. The goal of these benchmarks is not to be comprehensive, but rather to give a sense of where EVoC's strengths lie, particulary as compared with other standard options. The aim is to look at both clustering quality and compute time and focus primarily on real-world datasets. To ensure you can try running these benchmarks on your hardware we will provide all the code to run the benchmarks yourself. So to start let's get all the libraries we'll need, both to run the benchmarks and comparisons, and visualise the results.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 1,\n   \"id\": \"a7ae0fff-dec4-49b8-9063-aafef992c764\",\n   \"metadata\": {\n    \"execution\": {\n     \"iopub.execute_input\": \"2026-03-25T20:36:11.007014Z\",\n     \"iopub.status.busy\": \"2026-03-25T20:36:11.006894Z\",\n     \"iopub.status.idle\": \"2026-03-25T20:36:16.807511Z\",\n     \"shell.execute_reply\": \"2026-03-25T20:36:16.806699Z\",\n     \"shell.execute_reply.started\": \"2026-03-25T20:36:11.007000Z\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"import numpy as np\\n\",\n    \"import matplotlib.pyplot as plt\\n\",\n    \"import seaborn as sns\\n\",\n    \"import time\\n\",\n    \"import evoc\\n\",\n    \"import umap\\n\",\n    \"import hdbscan\\n\",\n    \"import pandas as pd\\n\",\n    \"import sklearn.cluster\\n\",\n    \"import sklearn.metrics\\n\",\n    \"import warnings\\n\",\n    \"warnings.filterwarnings(\\\"ignore\\\")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"5a6ef4ce-bf5f-4e45-857b-bd60f6228fb4\",\n   \"metadata\": {},\n   \"source\": [\n    \"Now we will need come clustering alternatives to compare to. There are, of course, an endless array of clustering algorithms and implementations out there, but for the purposes of giving a basic comparison we will focus on the most common options for embedding vector clustering, such as those used in BERTopic. That means we'll need an implementation of UMAP + HDBSCAN for comparison, and we'll also compare with the standard workhorse: sklearn's KMeans. Sine we'll be benchmarking these we'll build a common calling format so we can build a benchmark harness around them easily. We'll start with UMAP + HDBSCAN which requires a little bit of work to glue together. \"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 2,\n   \"id\": \"9823192a-9d7e-4a1f-b912-84883f8273e8\",\n   \"metadata\": {\n    \"execution\": {\n     \"iopub.execute_input\": \"2026-03-25T20:36:16.808127Z\",\n     \"iopub.status.busy\": \"2026-03-25T20:36:16.807874Z\",\n     \"iopub.status.idle\": \"2026-03-25T20:36:16.811159Z\",\n     \"shell.execute_reply\": \"2026-03-25T20:36:16.810725Z\",\n     \"shell.execute_reply.started\": \"2026-03-25T20:36:16.808110Z\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"def umap_hdbscan(\\n\",\n    \"    data,\\n\",\n    \"    metric=\\\"euclidean\\\",\\n\",\n    \"    n_neighbors=15,\\n\",\n    \"    n_components=2,\\n\",\n    \"    min_samples=5,\\n\",\n    \"    min_cluster_size=10,\\n\",\n    \"    min_dist=0.1,\\n\",\n    \"    cluster_selection_method=\\\"eom\\\",\\n\",\n    \"    n_epochs=None,\\n\",\n    \"    negative_sample_rate=5,\\n\",\n    \"):\\n\",\n    \"    embedding = umap.UMAP(\\n\",\n    \"        metric=metric,\\n\",\n    \"        n_neighbors=n_neighbors,\\n\",\n    \"        n_components=n_components,\\n\",\n    \"        min_dist=min_dist,\\n\",\n    \"        n_epochs=n_epochs,\\n\",\n    \"        negative_sample_rate=negative_sample_rate,\\n\",\n    \"        n_jobs=8,\\n\",\n    \"    ).fit_transform(data)\\n\",\n    \"    clustering = hdbscan.HDBSCAN(\\n\",\n    \"        min_samples=min_samples,\\n\",\n    \"        min_cluster_size=min_cluster_size,\\n\",\n    \"        cluster_selection_method=cluster_selection_method,\\n\",\n    \"    ).fit_predict(embedding)\\n\",\n    \"    return clustering\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"f9985b20-ab5a-4001-b834-fea29fb90796\",\n   \"metadata\": {},\n   \"source\": [\n    \"Next up is KMeans. We don't need much of a wrapper here -- we can call on sklearn's implementation fairly directly.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 3,\n   \"id\": \"e3730e80-f580-41d1-a103-9f3d2440bf15\",\n   \"metadata\": {\n    \"execution\": {\n     \"iopub.execute_input\": \"2026-03-25T20:36:16.811753Z\",\n     \"iopub.status.busy\": \"2026-03-25T20:36:16.811617Z\",\n     \"iopub.status.idle\": \"2026-03-25T20:36:16.825930Z\",\n     \"shell.execute_reply\": \"2026-03-25T20:36:16.825498Z\",\n     \"shell.execute_reply.started\": \"2026-03-25T20:36:16.811741Z\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"def kmeans(data, n_clusters=10, kmeans_algorithm=\\\"lloyd\\\"):\\n\",\n    \"    return sklearn.cluster.KMeans(\\n\",\n    \"        n_clusters=n_clusters, \\n\",\n    \"        n_init=\\\"auto\\\", \\n\",\n    \"        algorithm=kmeans_algorithm\\n\",\n    \"    ).fit_predict(\\n\",\n    \"        data\\n\",\n    \"    )\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"e4e47881-987e-4944-a116-3acd14437b5a\",\n   \"metadata\": {},\n   \"source\": [\n    \"Lastly we need an EVoC function that works with the same pattern. We'll even be sure to use the reproducible (fixed ``random_state``) code-path that is a little slower, but vary the seed to ensure we get variation in results. Since EVoC can provide a few layers of clustering, we'll give it a little bonus by selecting out the best cluster layer compared to a target clustering. In practice EVoC usually selects a very good cluster layer, but for some of the datasets we'll be using the class labels are not exactly the most natural clustering, so we'll let the function choose a different layer in those cases. Note that we are not tuning any other EVoC parameters to optimize the results, just using defaults and selecting among the layers produced (usally 2-5 total layers for any of these datasets, so few enough that you could easily look througbn them by hand). In contrast we did spend time tuning parameters for the other algorithms to try to produce the best accuracy/quality scores we could. Sometimes this involved non-obvious choices of parameters.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 4,\n   \"id\": \"75a6f8bf-71eb-4053-943c-48d3f75d7052\",\n   \"metadata\": {\n    \"execution\": {\n     \"iopub.execute_input\": \"2026-03-25T20:36:16.826407Z\",\n     \"iopub.status.busy\": \"2026-03-25T20:36:16.826278Z\",\n     \"iopub.status.idle\": \"2026-03-25T20:36:16.837721Z\",\n     \"shell.execute_reply\": \"2026-03-25T20:36:16.837043Z\",\n     \"shell.execute_reply.started\": \"2026-03-25T20:36:16.826395Z\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"def EVoC(data, test_target=None, random_state=None):\\n\",\n    \"    if random_state is None:\\n\",\n    \"        random_state = np.random.randint(65536)\\n\",\n    \"    cls = evoc.EVoC(random_state=random_state).fit(data)\\n\",\n    \"    if test_target is None:\\n\",\n    \"        return cls.labels_\\n\",\n    \"    result = np.full(data.shape[0], -1)\\n\",\n    \"    best_ari = 0.0\\n\",\n    \"    for labels in cls.cluster_layers_:\\n\",\n    \"        ari = sklearn.metrics.adjusted_rand_score(\\n\",\n    \"            test_target[labels >= 0], labels[labels >= 0]\\n\",\n    \"        )\\n\",\n    \"        if ari > best_ari:\\n\",\n    \"            best_ari = ari\\n\",\n    \"            result = labels\\n\",\n    \"    return result\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"5d644252-ab58-4de7-9ca2-b4db23bc38af\",\n   \"metadata\": {},\n   \"source\": [\n    \"Now we need a test harness. To asses the quality of a clustering we'll use datasets that come equipped with class labels, and specifically datasets where there is good reason to expect that the clusters should align reasonably with the class labels. This let's us use robust scores such as Adjusted Rand Index (ARI) and Adjusted Mutual Information (AMI) that compare a clustering against ground-truth labels. Since both UMAP + HDBSCAN and EVoC have a notion of noise points we'll exclude those from the ARI and AMI computations (as they don't make sense as a single class, and confuse things). But we will keep track of how much of the data is clustered, and also track a \\\"clustering score\\\" that is a weighted geometric mean of the ARI and the amount of data clustered (weighted 2:1 in favour of ARI accuracy, so we mostly care about being right, but also need to cluster a reasonable amount of data). We will also keep track of how long it takes to run any of these methods.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 5,\n   \"id\": \"8c2abe82-1c74-45fa-b6cc-5c36647b8c74\",\n   \"metadata\": {\n    \"execution\": {\n     \"iopub.execute_input\": \"2026-03-25T20:36:16.838269Z\",\n     \"iopub.status.busy\": \"2026-03-25T20:36:16.838138Z\",\n     \"iopub.status.idle\": \"2026-03-25T20:36:16.850442Z\",\n     \"shell.execute_reply\": \"2026-03-25T20:36:16.849785Z\",\n     \"shell.execute_reply.started\": \"2026-03-25T20:36:16.838257Z\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"def score_clustering(data, target, clustering_function, n_runs=16, **kwargs):\\n\",\n    \"    result = np.zeros((n_runs, 5), dtype=np.float32)\\n\",\n    \"    for i in range(n_runs):\\n\",\n    \"        start_time = time.time()\\n\",\n    \"        clustering = clustering_function(data, **kwargs)\\n\",\n    \"        result[i, 0] = time.time() - start_time\\n\",\n    \"        result[i, 1] = sklearn.metrics.adjusted_rand_score(\\n\",\n    \"            target[clustering >= 0], clustering[clustering >= 0]\\n\",\n    \"        )\\n\",\n    \"        result[i, 2] = sklearn.metrics.adjusted_mutual_info_score(\\n\",\n    \"            target[clustering >= 0], clustering[clustering >= 0]\\n\",\n    \"        )\\n\",\n    \"        result[i, 3] = np.sum(clustering >= 0) / clustering.shape[0]\\n\",\n    \"        result[i, 4] = np.cbrt((result[i, 1] ** 2) * result[i, 3])\\n\",\n    \"\\n\",\n    \"    result = pd.DataFrame(\\n\",\n    \"        result,\\n\",\n    \"        columns=(\\n\",\n    \"            \\\"Elapsed time\\\",\\n\",\n    \"            \\\"Adjusted Rand Index\\\",\\n\",\n    \"            \\\"Adjusted Mutual Information\\\",\\n\",\n    \"            \\\"Proportion clustered\\\",\\n\",\n    \"            \\\"Clustering Score\\\",\\n\",\n    \"        ),\\n\",\n    \"    )\\n\",\n    \"    result[\\\"algorithm\\\"] = clustering_function.__name__.replace(\\\"_\\\", \\\"\\\\n\\\")\\n\",\n    \"    result = result.melt(\\n\",\n    \"        id_vars=[\\\"algorithm\\\"],\\n\",\n    \"        value_vars=[\\n\",\n    \"            \\\"Elapsed time\\\",\\n\",\n    \"            \\\"Adjusted Rand Index\\\",\\n\",\n    \"            \\\"Adjusted Mutual Information\\\",\\n\",\n    \"            \\\"Proportion clustered\\\",\\n\",\n    \"            \\\"Clustering Score\\\",\\n\",\n    \"        ],\\n\",\n    \"        var_name=\\\"measure\\\",\\n\",\n    \"    )\\n\",\n    \"    return result\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"af6fd5e6-2f44-478b-9a34-b1e935b141be\",\n   \"metadata\": {},\n   \"source\": [\n    \"Lastly we'll need a simple function to run all our clustering algorithm benchmarks for each method across a given dataset and provide a nice table of results that we can easily pass to seaborn for plotting.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 6,\n   \"id\": \"40c1aa9a-fb16-473a-ad2c-02ac59bad712\",\n   \"metadata\": {\n    \"execution\": {\n     \"iopub.execute_input\": \"2026-03-25T20:36:16.850912Z\",\n     \"iopub.status.busy\": \"2026-03-25T20:36:16.850774Z\",\n     \"iopub.status.idle\": \"2026-03-25T20:36:16.862692Z\",\n     \"shell.execute_reply\": \"2026-03-25T20:36:16.862360Z\",\n     \"shell.execute_reply.started\": \"2026-03-25T20:36:16.850901Z\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"def run_dataset_benchmarks(data, target, n_runs, kmeans_kwargs, umap_hdbscan_kwargs):\\n\",\n    \"    \\\"\\\"\\\"Score all three algorithms on a dataset and return combined results.\\\"\\\"\\\"\\n\",\n    \"    kmeans_results = score_clustering(\\n\",\n    \"        data, target, kmeans, n_runs=n_runs, **kmeans_kwargs\\n\",\n    \"    )\\n\",\n    \"    umap_results = score_clustering(\\n\",\n    \"        data, target, umap_hdbscan, n_runs=n_runs, **umap_hdbscan_kwargs\\n\",\n    \"    )\\n\",\n    \"    evoc_results = score_clustering(\\n\",\n    \"        data, target, EVoC, test_target=target, n_runs=n_runs\\n\",\n    \"    )\\n\",\n    \"    return pd.concat(\\n\",\n    \"        [kmeans_results, umap_results, evoc_results.assign(algorithm=\\\"EVoC\\\")],\\n\",\n    \"        ignore_index=True,\\n\",\n    \"    )\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"9b36e588-23ee-42bf-b931-f2a797250807\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Image embeddings\\n\",\n    \"\\n\",\n    \"Let's start by looking at image embeddings. For a \\\"real-world\\\" dataset we'll use the tried and true CIFAR-100 dataset. The CIFAR-100 dataset is a popular computer vision benchmark containing 60,000 32x32 color images across 100 classes. In our case we don't need to worry about the images themselves, but rather their embeddings. We have built a dataset with embedding vectors generated by CLIP and provided it on Huggingface datasets so we don't have to worry about the time/cost of embedding all the images. If you wish you can generate your own embeddings, potentially with other models such as SigLIP. Let's pull that data down. \"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 7,\n   \"id\": \"2bb0aaaa-35f0-4588-ad58-466f0cae8ceb\",\n   \"metadata\": {\n    \"execution\": {\n     \"iopub.execute_input\": \"2026-03-25T20:36:16.863280Z\",\n     \"iopub.status.busy\": \"2026-03-25T20:36:16.863153Z\",\n     \"iopub.status.idle\": \"2026-03-25T20:36:19.367650Z\",\n     \"shell.execute_reply\": \"2026-03-25T20:36:19.367253Z\",\n     \"shell.execute_reply.started\": \"2026-03-25T20:36:16.863268Z\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from datasets import load_dataset\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 8,\n   \"id\": \"2046299e-7e86-490b-805c-4ccc92088877\",\n   \"metadata\": {\n    \"execution\": {\n     \"iopub.execute_input\": \"2026-03-25T20:36:19.368857Z\",\n     \"iopub.status.busy\": \"2026-03-25T20:36:19.368555Z\",\n     \"iopub.status.idle\": \"2026-03-25T20:36:49.643729Z\",\n     \"shell.execute_reply\": \"2026-03-25T20:36:49.642787Z\",\n     \"shell.execute_reply.started\": \"2026-03-25T20:36:19.368842Z\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"ds_cifar = load_dataset(\\\"lmcinnes/evoc_bench_cifar100\\\")\\n\",\n    \"cifar_data = np.asarray(ds_cifar[\\\"train\\\"][\\\"embedding\\\"])\\n\",\n    \"cifar_target = np.asarray(ds_cifar[\\\"train\\\"][\\\"target\\\"])\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"44a1a510-281d-493c-b9fb-38e89d68cd99\",\n   \"metadata\": {},\n   \"source\": [\n    \"Now we just need to run the benchmarks. Here we had to rune parameters a little. Oddly enough KMeans actually works better if you ask for 125 clusters (instead of the 100 classes that exist) because with more clusters it does a better job of breaking up some of the more easily confused classes that otherwise notably drag down ARI scores. For UMAP + HDBSCAN we need to manage pick the right parameters; in general UMAP doesn't need too much tuning for this, but HDBSCAN works best with leaf clustering and a carefully well-chosen ``min_cluster_size``. A little experimentation finds around 120 works well for this data.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 9,\n   \"id\": \"3fa73eb5-68c7-49be-80a7-bd3527264111\",\n   \"metadata\": {\n    \"execution\": {\n     \"iopub.execute_input\": \"2026-03-25T20:36:49.644685Z\",\n     \"iopub.status.busy\": \"2026-03-25T20:36:49.644502Z\",\n     \"iopub.status.idle\": \"2026-03-25T20:42:55.905457Z\",\n     \"shell.execute_reply\": \"2026-03-25T20:42:55.904368Z\",\n     \"shell.execute_reply.started\": \"2026-03-25T20:36:49.644670Z\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"cifar_results = run_dataset_benchmarks(\\n\",\n    \"    cifar_data, \\n\",\n    \"    cifar_target, \\n\",\n    \"    n_runs=16, \\n\",\n    \"    kmeans_kwargs={\\\"n_clusters\\\":125}, \\n\",\n    \"    umap_hdbscan_kwargs={\\n\",\n    \"        \\\"min_samples\\\":5,\\n\",\n    \"        \\\"min_cluster_size\\\":120, \\n\",\n    \"        \\\"metric\\\":\\\"cosine\\\", \\n\",\n    \"        \\\"cluster_selection_method\\\":\\\"leaf\\\"\\n\",\n    \"    }\\n\",\n    \")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"83150948-cc2d-429f-a913-eac665250739\",\n   \"metadata\": {},\n   \"source\": [\n    \"Before we look at quality, let's compare how long these different approaches took to run:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 10,\n   \"id\": \"9233e308-90ed-4c57-8a1e-738a7a7e898f\",\n   \"metadata\": {\n    \"execution\": {\n     \"iopub.execute_input\": \"2026-03-25T20:42:55.906750Z\",\n     \"iopub.status.busy\": \"2026-03-25T20:42:55.906539Z\",\n     \"iopub.status.idle\": \"2026-03-25T20:42:56.203555Z\",\n     \"shell.execute_reply\": \"2026-03-25T20:42:56.202959Z\",\n     \"shell.execute_reply.started\": \"2026-03-25T20:42:55.906732Z\"\n    }\n   },\n   \"outputs\": [\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"<seaborn.axisgrid.FacetGrid at 0x742050969b80>\"\n      ]\n     },\n     \"execution_count\": 10,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    },\n    {\n     \"data\": {\n      \"image/png\": \"iVBORw0KGgoAAAANSUhEUgAAAxYAAAMVCAYAAADqKmIJAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAUCJJREFUeJzt3Qd8VeX9P/Bv2IIkgjIFEUVw4Bb33triat216q9a66pWrf5s3Vat2mqHdbV1VH9WW1et1r1FoEq1WkUEREFFUEYCyCb/13P4JyZwY9FDchPyfr9e95Wc55x77pNzc5PzOc84JZWVlZUBAACQQ4s8TwYAABAsAACA5UKLBQAAkJtgAQAA5CZYAAAAuQkWAABAboIFAACQm2ABAADkJlgAAAC5CRYANEolJSXx4IMPxopYz+eeey573vTp0+utXgANTbAAoMEdc8wx2Yn1ko+99957hXs3dt555zj99NNrlW277bYxceLEKCsrK1q9AJa3Vst9jwA0CZWVlbFw4cJo1ao4/wpSiLj11ltrlbVt2zaagzZt2kT37t2LXQ2A5UqLBcDXuAJ96qmnZlehO3XqFN26dYubb745Zs2aFccee2x07Ngx1l577Xj00UdrPe/tt9+OfffdN1ZeeeXsOUcddVR89tln1esfe+yx2H777WOVVVaJVVddNb75zW/G2LFjq9fPmzcvTjnllOjRo0e0a9cu1lxzzbjiiiuyde+//352xf/111+v3j51s0llqdtNze43jz/+eGyxxRbZSfyLL76YBYyrrroq1lprrVhppZVi4403jnvvvbfefy/S66eT65qPdDzrcs4550T//v2jffv2WV3PP//8mD9/fvX6iy66KDbZZJO46aabonfv3tl2Bx98cK3uRukYbLnlltGhQ4fsOG+33XbxwQcfVK//+9//Hptvvnl2fNNrXHzxxbFgwYLq9aNHj44dd9wxW7/++uvHk08++V9bZp5//vn49a9/Xd0qk96rJbtC3XbbbVl9Hn744RgwYEBW929/+9vZ79Ttt9+evdfp2KTfuxQGa/5OnH322bH66qtnP9NWW21V/X4DNDTBAuBrSCd7q622Wvzzn//MTvZOPPHE7CQ2dXH517/+FXvttVcWHD7//PNs+9TtZaeddspOfF999dUsREyaNCkOOeSQ6n2mk8gzzjgjXnnllXj66aejRYsWceCBB8aiRYuy9b/5zW/ioYceir/85S8xatSouPPOO7MTzq8qnYimQDJy5MjYaKON4rzzzstaDm644YZ466234kc/+lF85zvfyU6I6/KDH/wgC0hf9hg/fvxy/d1KgS2dgKeAlk7Uf//738e1115ba5sxY8ZkxycFhHSMU9A6+eSTs3UpIBxwwAHZ+/DGG2/E0KFD4/vf/352gp+kwJV+7h/+8IfZa6SAkl7vsssuy9an9+Gggw6Kli1bxrBhw+LGG2/Mws6XSfXcZptt4vjjj89+B9IjhZ5C0u9Keo/vvvvurO4pIKTX+8c//pE97rjjjizA1gx9KcgOGTIke076mdLvYGoJSgEIoMFVAvCV7LTTTpXbb7999fKCBQsqO3ToUHnUUUdVl02cOLEy/YkdOnRotnz++edX7rnnnrX2M2HChGybUaNGFXydyZMnZ+vffPPNbPnUU0+t3HXXXSsXLVq01Lbjxo3Ltn3ttdeqy6ZNm5aVPfvss9ly+pqWH3zwweptZs6cWdmuXbvKl19+udb+vve971UefvjhdR6DSZMmVY4ePfpLH/Pnz6/z+UcffXRly5Yts+NW83HJJZdUb5Pq+sADD9S5j6uuuqpy8803r16+8MILs32m41rl0UcfrWzRokX2fkyZMiXb53PPPVdwfzvssEPl5ZdfXqvsjjvuqOzRo0f2/eOPP15w//+tnun35bTTTqtVVvVepPcoufXWW7PlMWPGVG9zwgknVLZv375yxowZ1WV77bVXVp6kbUtKSio/+uijWvvebbfdKs8999w66wNQX4yxAPga0pX+KukKduq6tOGGG1aXpa5OyeTJk7OvI0aMiGeffTa7kr+k1N0pdfFJX1P3nnQ1PHWRqmqpSFf+Bw4cmHWr2WOPPbKuMumqdOoqteeee37luqduUFXSlfk5c+Zk+60pdbHZdNNN69xH165ds0ceu+yyS9ZKUlPnzp3r3D5dqf/Vr36VtUrMnDkza4EoLS2ttc0aa6wRvXr1ql5OrQXpOKYWntRSkY5hak1KP+/uu++etRilrmVV71FqLapqoUhSt6N0fFJrQmrhKbT/5SV1f0pd6Gr+DqUWqZq/M6ms6ncqtYyl/JV+d2qaO3du9vsI0NAEC4CvoXXr1rWWU3eammVV3WuqwkH6Onjw4LjyyiuX2lfViW1an7rJpC4+PXv2zJ6TAkU6yU8222yzGDduXDZ246mnnspOitPJcTrhTt2mksUX+herOf6gptQXv0pV/R555JGsn/6yDqROXaFSV6wvk0JLOhGvS6pHv379YlmksHXYYYdlYx5SMEizKaXuP7/85S+/9HlV70PV19TlK3V1Sl2N7rnnnqwbWBonsfXWW2fHIu0/dT9aUhpTUfPYLrn/hvidqiqr+TuVQm0KROlrTYUCLEB9EywAGkAKBffdd192BbrQLExTpkzJroinfv077LBDVvbSSy8ttV26Qn/ooYdmjzS4N7VcTJ06Nbp06ZKtT334q1oaag7krksagJwCRGoVSVf0l9Ull1wSZ5111pduk8LR8pLGEfTp0yd++tOfVpfVHHRdJf0cH3/8cfVrp3EUKXTVvKqfjk96nHvuuVmLw1133ZUFi/QepZaNusJOOlaF9r8sM0DVHHC9vKSfIe03tWBU/c4AFJNgAdAA0gDi1BJx+OGHx49//ONs4Hfq0pOuuqfyNONP6r6SBuemFox0Avu///u/tfaRBiqndWkAeDpZ/utf/5rNpJRmE0rL6eT45z//eRZeUleqdDV+WQZEp4CQBmynK+BpVqqKiop4+eWXs6veRx99dL11hUpddj755JNaZSl0pWOzpHSyn45JOl6DBg3KWlgeeOCBgi0Lqc6/+MUvsp8jtU6klp10nFJrTzq+++23XxYMUoh4991347vf/W723AsuuCDrXpZajdIg6HRM04DoN998M372s59lrUOpG1raPrWUpP3XDDp1Se/H8OHDs9mg0jH9su5eX0UKS0ceeWR1fVLQSO/7M888k3XLSzOQATQks0IBNIB0IpuuuqcrzKkrT+ridNppp2VdetIJbHqkk+bUrSWtSyf6V199da19pJPS1JUqjZFIJ9fpRDXNFlTVDeqWW27Juj+l9Wnf6WR4WVx66aXZSXWaKWq99dbL6pdmVerbt2/Up9QdKQWlmo8UbArZf//9s2OSpttNwSoFnzQepVAASV2Z0kl1Gn+SjuX1119fPYbhnXfeiW9961vZSXmaESrt74QTTsjWp587Tfeaukal45uC2jXXXJO1lCTpOKcwkwJRmrL2uOOOqzUeoy4puKWuSqnFI7UsLc/ZslLXrhQszjzzzCz0pNCUQkxdM08B1KeSNIK7Xl8BABpAuo/Fgw8+uExdwABY/rRYAAAAuQkWAABAbrpC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AOQmWAAAAIIFAABQfFosAACA3AQLAAAgN8ECAADITbAAAAByEywAAIDcBAsAACA3wQIAAMhNsAAAAHITLAAAgNwECwAAIDfBAgAAyE2wAAAAchMsAACA3AQLAAAgN8ECAADITbAAAAByEywAAIDcBAsAACA3wQIAAMhNsAAAAHITLAAAgKYdLK644ooYNGhQdOzYMbp27RoHHHBAjBo1qtY2lZWVcdFFF0XPnj1jpZVWip133jneeuutotUZAABoZMHi+eefj5NPPjmGDRsWTz75ZCxYsCD23HPPmDVrVvU2V111VVxzzTVx3XXXxSuvvBLdu3ePPfbYI2bMmFHMqgMAADWUVKYmgUbi008/zVouUuDYcccds9aK1FJx+umnxznnnJNtM3fu3OjWrVtceeWVccIJJyy1j7Q+PapUVFRE7969o7y8PEpLSxv05wEAgOaiUY2xSCf/SefOnbOv48aNi08++SRrxajStm3b2GmnneLll1+us3tVWVlZ9SOFCgAAoJkEi9Q6ccYZZ8T2228fAwcOzMpSqEhSC0VNablq3ZLOPffcLKBUPSZMmNAAtQcAgOatVTQSp5xySrzxxhvx0ksvLbWupKRkqRCyZFnNFo30AAAAmlmLxamnnhoPPfRQPPvss9GrV6/q8jRQO1mydWLy5MlLtWIAAADNNFiklofUUnH//ffHM888E3379q21Pi2ncJFmjKoyb968bHD3tttuW4QaAwAAja4rVJpq9q677oq//e1v2b0sqlom0qDrdM+K1N0pzQh1+eWXxzrrrJM90vft27ePI444ophVBwAAGst0s3WNk7j11lvjmGOOyb5P1bv44ovjpptuimnTpsVWW20Vv/vd76oHeP83abrZFFRMNwsAAM3kPhb1QbAAAIBmMngbAABo2gQLAAAgN8ECAADITbAAAAByEywAAIDcBAsAACA3wQIAAMhNsAAAAHITLAAAgNwECwAAIDfBAgAAyE2wAAAAchMsAACA3AQLAAAgN8ECAADITbAAAAByEywAAIDcBAsAACA3wQIAAMhNsAAAAHITLAAAgNwECwAAIDfBAgAAyE2wAAAAchMsAACA3AQLAAAgN8ECAADITbAAAAByEywAAIDcBAsAACA3wQIAAMhNsAAAAHITLAAAgNwECwAAIDfBAgAAyE2wAAAAchMsAACA3AQLAAAgN8ECAADITbAAAAByEywAAIDcBAsAACA3wQIAAMhNsAAAAHITLAAAgNwECwAAIDfBAgAAyE2wAAAAchMsAACA3AQLAABAsAAAAIpPiwUAAJCbYAEAAOQmWAAAALkJFgAAQG6CBQAAkJtgAQAA5CZYAAAAuQkWAABAboIFAACQm2ABAADkJlgAAAC5CRYAAEBuggUAAJCbYAEAAOQmWAAAALkJFgAAQG6CBQAAkJtgAQAA5CZYAAAAuQkWAABAboIFAACQm2ABAADkJlgAAAC5CRYAAEBuggUAAJCbYAEAAOQmWAAAALkJFgAAQG6CBQAAkJtgAQAA5CZYAAAAuQkWAABAboIFAACQm2ABAADkJlgAUL8m/jti7DMRc2csvW7h/IiKjyMWzPMuADRxrYpdAQBWUNMnRNzznYiJry9ebrNyxO4XRWx5/OLll38b8dKvIj7/LGKlzhHbnhqxwxlFrTIAX59gAUD9uPfYL0JFMm9mxD/Oiui+UcSn70Q8cd4X62ZPjXj64oi2Hb8IHgA0KbpCAbD8fToq4sNXCq97/c6I4TcWXjfsBu8GQBMlWACw/M2p+JJ15RHlHxVeV1FHOQCNnmABwPLXY+OI9qsVXtdv94heWxRe12uQdwOgiRIsAFj+WrWJ2PeqiJKWtcvX3CFio0MjdvlJRKt2tde1bLu4HIAmqaSysrIyVmAVFRVRVlYW5eXlUVpaWuzqADQvk0dGvHZnxOdTI9baOWKDAxeHjuSTNyOG/i5i8tsRqw2I2ObkiJ6bFLvGAHxNggUAxbNgbsSMiRErd49ovUQLBgBNiulmASiOF36x+F4Wc6ZHtCuL2PqkiJ3OiSgp8Y4ANEHGWADQ8F75Y8Qzly4OFVUzRT13Rd3T0ALQ6AkWADS8f95cuHz4TQ1dEwCKHSzGjBkTjz/+eMyePTtbXsHHgAOwPFVMLFyexlsA0DyCxZQpU2L33XeP/v37x7777hsTJy7+J3DcccfFmWeeWR91BGBFs8ZWhct711EOwIoXLH70ox9Fq1atYvz48dG+ffvq8kMPPTQee+yx5V0/AFZEO58b0fqL/yGZVitF7HpesWoEQEPPCvXEE09kXaB69epVq3ydddaJDz74IG99AGgOVt8s4vhnI4al+1iM/P/3sTgpotsGxa4ZAA0VLGbNmlWrpaLKZ599Fm3btv269QCguem6bsR+vy12LQAoVleoHXfcMf70pz9VL5eUlMSiRYvi6quvjl122WV51QuAFd3MTyOevzri3v+JeO7KiBmTil0jABryzttvv/127LzzzrH55pvHM888E/vtt1+89dZbMXXq1BgyZEisvfba0ZhUVFREWVlZlJeXR2lpabGrA0AyZWzELXtHzJr8xfFov2rEsY9GdBngGAE0hxaL9ddfP954443YcsstY4899si6Rh100EHx2muvNbpQAUAjlW6OVzNUJJ9PiXj6kmLVCICGbrFoarRYADRCP+/zxV23a0ozRf3UvSwAmsXg7RdeeOG/jsEAgC/VrqxwsGi3igMH0FyCRRpfsaQ0gLvKwoUL89cKgBXbZt9d3B1qqfKjilEbAIoxxmLatGm1HpMnT85ujDdo0KDsHhcA8F9td3rEZkdHlLRcvJy+bvKdiB1/7OABNPcxFqmLVLor94gRI77Sc9I0tek5EydOjAceeCAOOOCA6vXHHHNM3H777bWes9VWW8WwYcOW+TWMsQBoxComRkwZHbFqv4jSnsWuDQAN2RWqLl26dIlRo0Z9peekGaU23njjOPbYY+Nb3/pWwW323nvvuPXWW6uX27Rpk7uuADQSpT0WPwBofsEiTTVbU2rwSK0NP//5z7OQ8FXss88+2ePLpLt5d+/efZn3OXfu3OxRs8UCAABoZMFik002yQZrL9mDauutt45bbrkllrfnnnsuunbtGqusskrstNNOcdlll2XLdbniiivi4osvXu71AAAAluMYiw8++KDWcosWLbJuUO3atctXkZKSpcZY3HPPPbHyyitHnz59Yty4cXH++efHggULsjEZqSVjWVssevfu7c7bAADQmFos0kl+Qzn00EOrvx84cGBsscUW2es/8sgj2d2+C0mBo67QAQAAFDFY/OY3v1nmHf7whz+M+tKjR48sWIwePbreXgMAAKinYHHttdcuc3em+gwWU6ZMiQkTJmQBAwAAaGLBIo1vqA8zZ86MMWPG1Hqd119/PTp37pw9Lrroomwa2hQk3n///fjJT34Sq622Whx44IH1Uh8AAKDI97H4Ol599dXYZZddqpfPOOOM7OvRRx8dN9xwQ7z55pvxpz/9KaZPn56Fi7RtGtDdsWPHItYaAABYLnfe/vDDD+Ohhx6K8ePHx7x582qtu+aaa6IxcedtAABohC0WTz/9dOy3337Rt2/f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GJo2n+HmoaMWi4gWLVpEr169iv1e0ABSqBAsoOnyGYamzWcYg7cBAIDcBAsAACA3wYImr23btnHhhRdmX4Gmx2cYmjafYZrN4G0AAKD+abEAAAByEywAAIDcBAsAACA3wYJ6s/POO8fpp5/uCAMANAOCBQBAM3bMMcdESUnJUo9dd901VltttfjZz35W8HlXXHFFtn7evHnL9DrPPvts7LvvvrHqqqtG+/btY/31148zzzwzPvroo+X8E1EsggUAQDO39957x8SJE2s97rvvvvjOd74Tt912WxSaRPTWW2+No446Ktq0afNf93/TTTfF7rvvHt27d8/2+/bbb8eNN94Y5eXl8ctf/rKefioammBBg3nssceirKws/vSnP2VXRw444IC4/PLLo1u3brHKKqvExRdfHAsWLIgf//jH0blz5+jVq1fccssttfaRrmoceuih0alTp+yKx/777x/vv/9+9fpXXnkl9thjj+wKSnqtnXbaKf71r3/V2ke6CvOHP/whDjzwwOyKyTrrrBMPPfRQ9fpp06bFkUceGV26dImVVlopW5/+eAK1rbnmmvGrX/2qVtkmm2wSF110UfVnLZ1MfPOb38w+a+utt14MHTo0xowZk3WV7NChQ2yzzTYxduzY6uen79PnOv1dWHnllWPQoEHx1FNPLfW6l156aRxxxBHZNj179ozf/va33h7IeS+KdNJf85H+137ve9/LPpcvvPBCre1ffPHFGD16dLZ+0aJFcckll2T/t9N+0t+B9D+/yocffhg//OEPs0f6v54+/+lzvOOOO2b/jy+44ALv3QpCsKBB3H333XHIIYdkoeK73/1uVvbMM8/Exx9/nP2xuuaaa7KTkXQCkv6QDR8+PH7wgx9kjwkTJmTbf/7557HLLrtkJxLpOS+99FL2fbrKUtUMO2PGjDj66KOzP3jDhg3LQkFqdk3lNaUQk+rzxhtvZOtTkJg6dWq27vzzz8+upDz66KMxcuTIuOGGG7KgAnx1KQCkz/zrr78e6667bhYGTjjhhDj33HPj1VdfzbY55ZRTqrefOXNm9plMYeK1116LvfbaKwYPHhzjx4+vtd+rr746Ntpoo+zCQdrXj370o3jyySe9RbCcbbjhhlnAX/ICWwoIW265ZQwcODB+/etfZ60Ov/jFL7L/q+lzu99++2XBI/nrX/+a/Z8+++yzC75GurjICiLdIA/qw0477VR52mmnVf7ud7+rLCsrq3zmmWeq1x199NGVffr0qVy4cGF12YABAyp32GGH6uUFCxZUdujQofLPf/5ztvzHP/4x22bRokXV28ydO7dypZVWqnz88ccL1iHto2PHjpV///vfq8vSr/15551XvTxz5szKkpKSykcffTRbHjx4cOWxxx673I4DrKjSZ/jaa6+tVbbxxhtXXnjhhQU/a0OHDs3K0me5Svp8t2vX7ktfZ/3116/87W9/W+t1995771rbHHrooZX77LNP7p8JmqP0P7lly5bZ/9yaj0suuSRbf8MNN2TLM2bMyJbT17R80003Zcs9e/asvOyyy2rtc9CgQZUnnXRS9v2JJ55YWVpa2uA/Fw1PiwX1KvWjTDNDPfHEE1lrQ00bbLBBtGjxxa9g6vqQroxUadmyZdbdafLkydnyiBEjsi4UHTt2zFoq0iN1mZozZ051V4q0bWrl6N+/f9YVKj3SFdAlr3amK51VUneMtM+q1znxxBOzFpbUlJuurrz88sv1dHRgxVfzs5Y+40nNz3kqS5/hioqKbHnWrFnZ5y4N6kxXMdPn/J133lnqM5y6UC25nFoYga8n/Y9OLYs1HyeffHK27vDDD8+6O91zzz3Zcvqarh0cdthh2Wc39T7Ybrvtau0vLVd9JtO2qWskK75Wxa4AK7Z0cp66KqQm1NSUWvMPS+vWrWttm9YVKkt/zJL0dfPNN4//+7//W+p10niIJI3d+PTTT7N+33369Mn6eqYTjiVnrPiy19lnn33igw8+iEceeSTrjrHbbrtlf1xTEy/whXRhYMkBnfPnz6/zs1b1+S9UVvX5S2OsHn/88ezz1q9fv2yc07e//e1lmnXGiQt8fekiW/rMFZIu0qXPYfpfnsZUpK9pubS0tPqiwJKfv5phIl3sS4O004DwHj16eJtWYFosqFdrr712Nr3c3/72tzj11FNz7WuzzTbL+mt27do1++NX85H+6CVpbEUaHJb6aKcWkRQsPvvss6/8WimopJBy5513ZiHl5ptvzlV3WBGlz0k6UaiSTjDGjRuXa5/pM5w+e2lyhdSykQaQ1pygoUoaQ7XkchrDAdSPFCiGDBkSDz/8cPY1LScpXKQJFNK4x5pSa3+asCFJISTNHHXVVVcV3Pf06dO9bSsILRbUu3SlIoWLNAtEq1atlppFZlmlAdZpwGaaMaZq9onUPeL+++/PrnKm5RQy7rjjjthiiy2yk5xUnq54fhVpdorUMpKCydy5c7M/olV/HIEvpDnu0zSUaXB1mnQhTXyQujDmkT7D6TOd9pmudqZ9VrVm1JRObNJJSppdLg3aToNDUysj8PWk/3effPJJrbL0P7tq8pI0y2L6fKbJGNLXNKNTlfS/9sILL8wuJqaeCqlFI3Wlquph0Lt377j22muziRrS/+a0jzQrVJotKk3qkro8mnJ2xSBY0CAGDBiQzQKVwsXXPfFI01Wm2aDOOeecOOigg7KZnlZfffWsq1K6YlI1S8X3v//92HTTTWONNdbIprM966yzvtLrpKsqaZaZdJU0hZIddtghG3MB1JY+J++99142m1tqNUwzQOVtsUgnH//zP/8T2267bXZCkz7vVV0tako31UrjrtIMb2mMVDopSTPRAF9Pmh52yW5K6X93GuNUJX02f/KTn2RBoqbUUyB9TtPnMo1XTGOk0jTuaWbGKieddFJ2oTF1c0wtkrNnz87CRfr7ccYZZ3jbVhAlaQR3sSsBAMsqnYykSSHSA4DGwxgLAAAgN8ECAADITVcoAAAgNy0WAABAboIFALWk2du+6sDoNDXsgw8+mH2fZlRLy2m6SQCaD8ECAADITbAAAAByEywAWEq62/XZZ58dnTt3ju7du8dFF11UvW706NHZXXfbtWuX3Qgr3fm6kHRjrXSju7RdupP9c889V71u2rRpceSRR0aXLl2yG1GmG2mlu/VWSXfkPeyww7LX79ChQ2yxxRYxfPjwbN3YsWNj//33j27dumV37B00aFA89dRTS93rIt0gM93QK91AL90w8+abb/ZOA9QjwQKApdx+++3ZCX06mb/qqqvikksuyQJEChzpzvctW7aMYcOGxY033pjdHbuQdHfedCfe1157LQsY++23X0yZMiVbd/7558fbb78djz76aIwcOTJuuOGG7E7bycyZM2OnnXaKjz/+OLt777///e8s5KTXrlq/7777ZmEi7TvdcXvw4MExfvz4Wq+f7sadAknaJt3198QTT6x1F2EAli/TzQKw1ODthQsXxosvvlhdtuWWW8auu+6aPdJJfRqg3atXr2zdY489Fvvss0888MADccABB2Tr+vbtGz//+c+rQ8eCBQuyslNPPTULCSlkpCBxyy23LHX0U8vCWWedle0ntVgsi9QikoLDKaecUt1iscMOO8Qdd9yRLVdWVmYtLxdffHH84Ac/8I4D1AMtFgAsZaONNqq13KNHj5g8eXLWupC6FVWFimSbbbYpeARrlrdq1SprPUjPT1IIuPvuu2OTTTbJgsbLL79cvW2aTWrTTTetM1TMmjUre07qhrXKKqtk3aFSS8SSLRY1f4Y0S1UKFulnAKB+CBYALKV169a1ltOJeeqKlK78LymtW1ZV26YWjg8++CCb1jZ1edptt92yVookjbn4MqmL1X333ReXXXZZ1qqSgsiGG24Y8+bNW6afAYD6IVgAsMxSK0FqGUhhoMrQoUMLbpvGYFRJXaFGjBgR6667bnVZGrh9zDHHxJ133hm/+tWvqgdXp5aGFBamTp1acL8pTKTnHXjggVmgSC0RqdsUAMUlWACwzHbfffcYMGBAfPe7380GVaeT/J/+9KcFt/3d736XjbtI3ZROPvnkbCaoNEtTcsEFF8Tf/va3GDNmTLz11lvx8MMPx3rrrZetO/zww7OwkMZrDBkyJN57772shaIqwPTr1y/uv//+LHykOhxxxBFaIgAaAcECgGX/p9GiRRYW5s6dmw3oPu6447IuSYWkwdtXXnllbLzxxlkASUGiauanNm3axLnnnpu1TqSpa9MsU2nMRdW6J554Irp27ZoNFE+tEmlfaZvk2muvjU6dOmUzTaXZoNKsUJtttpl3EaDIzAoFAADkpsUCAADITbAAAAByEywAAIDcBAsAACA3wQIAAMhNsAAAAHITLAAAgNwECwAAIDfBAoAv9f7770dJSUm8/vrrjea1dt555zj99NPrvT4ALDvBAoBGo3fv3jFx4sQYOHBgtvzcc89lQWP69OnFrhoA/0Wr/7YBADSEefPmRZs2baJ79+4OOEATpMUCgHjsscdi++23j1VWWSVWXXXV+OY3vxljx46t88g89NBDsc4668RKK60Uu+yyS9x+++1LtSzcd999scEGG0Tbtm1jzTXXjF/+8pe19pHKfvazn8UxxxwTZWVlcfzxx9fqCpW+T/tOOnXqlJWnbassWrQozj777OjcuXMWRi666KJa+0/b33TTTdnP0r59+1hvvfVi6NChMWbMmKwrVYcOHWKbbbb50p8TgGUnWAAQs2bNijPOOCNeeeWVePrpp6NFixZx4IEHZifvS0on/N/+9rfjgAMOyALACSecED/96U9rbTNixIg45JBD4rDDDos333wzO+k///zz47bbbqu13dVXX511e0rbp/VLdotK4SQZNWpU1kXq17/+dfX6FGZSOBg+fHhcddVVcckll8STTz5Zax+XXnppfPe7383que6668YRRxyR1ffcc8+NV199NdvmlFNO8RsAsDxUAsASJk+eXJn+Rbz55puV48aNy75/7bXXsnXnnHNO5cCBA2tt/9Of/jTbZtq0adnyEUccUbnHHnvU2ubHP/5x5frrr1+93KdPn8oDDjig1jZLvtazzz5ba79Vdtppp8rtt9++VtmgQYOyulVJzzvvvPOql4cOHZqV/fGPf6wu+/Of/1zZrl077z/AcqDFAoCsO1C6mr/WWmtFaWlp9O3bNzsq48ePX+ropNaDQYMG1Srbcsstay2PHDkytttuu1plaXn06NGxcOHC6rItttjiax/9jTbaqNZyjx49YvLkyXVu061bt+zrhhtuWKtszpw5UVFR8bXrAcBiBm8DEIMHD866Hv3+97+Pnj17Zl2gUhelNKB6SakxII1fWLLsq26TpK5MX1fr1q1rLafXW7LrVs1tqupTqKxQly8AvhrBAqCZmzJlStbCkAY677DDDlnZSy+9VOf2aazCP/7xj1plVeMVqqy//vpL7ePll1+O/v37R8uWLZe5bmmWqKRmKwcAjZOuUADNXJpxKc0EdfPNN2czJj3zzDPZQO66pMHP77zzTpxzzjnx7rvvxl/+8pfqQdlVLQBnnnlmNgg8DZ5O26SB1tddd12cddZZX6luffr0yfb58MMPx6effhozZ87M+dMCUF8EC4BmLs0Adffdd2czM6XuTz/60Y+y2ZrqksZf3HvvvXH//fdnYxhuuOGG6lmh0tSyyWabbZYFjrTftM8LLrggm7Wp5nSxy2L11VePiy++OP73f/83Gw9hBieAxqskjeAudiUAaNouu+yyuPHGG2PChAnFrgoARWKMBQBf2fXXX5/NDJW6UA0ZMiRr4dCaANC8CRYAfGVp2th01+ypU6fGGmuskY2pSDedA6D50hUKAADIzeBtAAAgN8ECAADITbAAAAByEywAAIDcBAsAACA3wQIAAMhNsAAAAHITLAAAgMjr/wHvmRycAd54wAAAAABJRU5ErkJggg==\",\n      \"text/plain\": [\n       \"<Figure size 800x800 with 1 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    }\n   ],\n   \"source\": [\n    \"sns.catplot(\\n\",\n    \"    cifar_results[cifar_results.measure == \\\"Elapsed time\\\"], \\n\",\n    \"    x=\\\"algorithm\\\", \\n\",\n    \"    y=\\\"value\\\", \\n\",\n    \"    hue=\\\"algorithm\\\", \\n\",\n    \"    kind=\\\"swarm\\\", \\n\",\n    \"    col=\\\"measure\\\",\\n\",\n    \"    height=8,\\n\",\n    \")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"eface2b3-8171-4d9e-b1a1-585af3e31990\",\n   \"metadata\": {},\n   \"source\": [\n    \"As expected KMeans is noticeably quicker than UMAP + HDBSCAN. The one very long time for UMAP + HDBSCAN is due to numba's JIT compilation time (subsequent runs don't require compilation -- so we won't see this again). The surprise is that EVoC actually ran faster than KMeans here. Often KMeans is chosen simply based on runtime-constraints: it is usually faster than most other options you could pick. Here, however, we see that EVoC is not only competitive with KMeans, it's actually the faster option!\\n\",\n    \"\\n\",\n    \"How about the quality of the clusterings we get out?\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 11,\n   \"id\": \"6c0bcb69-473f-417f-b438-6fc5894d2159\",\n   \"metadata\": {\n    \"execution\": {\n     \"iopub.execute_input\": \"2026-03-25T20:42:56.204347Z\",\n     \"iopub.status.busy\": \"2026-03-25T20:42:56.204186Z\",\n     \"iopub.status.idle\": \"2026-03-25T20:42:56.875156Z\",\n     \"shell.execute_reply\": \"2026-03-25T20:42:56.874734Z\",\n     \"shell.execute_reply.started\": \"2026-03-25T20:42:56.204333Z\"\n    }\n   },\n   \"outputs\": [\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"<seaborn.axisgrid.FacetGrid at 0x7420509d75f0>\"\n      ]\n     },\n     \"execution_count\": 11,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    },\n    {\n     \"data\": {\n      \"image/png\": \"iVBORw0KGgoAAAANSUhEUgAAA90AAAPeCAYAAAARWnkoAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAA5a1JREFUeJzs3Qd4FNX6x/FfCkloCb33DiK9IygWiqIiXsCGipVrRdR7LyoW1D9W7KCoiFgAFVFUUMAGCKIgCALSe0+AhJ4Q9v+8J+6ym2yo2RT4fp5nDDttZ2bXPfPOOec9YR6PxyMAAAAAAJDlwrN+lwAAAAAAgKAbAAAAAIAQoqYbAAAAAIAQIegGAAAAACBECLoBAAAAAAgRgm4AAAAAAEKEoBsAAAAAgBAh6AYAAAAAIEQIugEAAAAACBGCbgBZ5vHHH1ejRo0yfZ2bnHfeeerXr59yqxtvvFHdunU7bd4HAPIiyrWcU6VKFb388stZus99+/bpyiuvVGxsrMLCwrRr1y7lBT/99FOeOl5kRNANIFMzZ85URESEOnfufFJX6YEHHtD333+fJwPlkSNHugLOO5UuXVqXXnqpFi1apNyAAhgAThzlWpjq1q2b4bp88sknrqyzQDevPMA+mQf777//vqZPn+6+B5s3b1ZcXJxym2DXtE2bNrn2eHF8CLqBXMrj8ejQoUM5egwjRozQ3XffrRkzZmjdunUnvH2hQoVUvHhx5VX2JNwKuU2bNumbb77R3r17dckllyg5OTmnDw0A8hzKtZxXsGBBbdu2TbNmzcpQ3leqVEmnu5UrV7qHDvXr11eZMmXcg4YTlZqaqsOHDys7RUVFnfTxIncg6MZpx54QWqBoTwmLFi3qaiiHDx/uAqY+ffqocOHCql69uiZNmhSw3eLFi3XxxRe7QNG26d27t+Lj433Lv/32W51zzjkqUqSICyS7du3qfry9LBC76667VLZsWcXExLinxYMHD3bL1qxZ434o58+f71vfmgjZPKux9K+5/O6779SsWTNFR0e7p7F2k/Lcc8+pWrVqyp8/vxo2bKjPPvss5NfRrpc9+f73v//tztVqftN75pln3LWya3rzzTfrwIEDR30KHezprTVttibOXkOHDlXNmjXdNbR9/+tf/3LzbZ2ff/5Zr7zyiq/22a7r8Xx2di7XX3+9W26fz4svvnhc18Dewwo528Y+k/vuu09r167V0qVLfesMGTJEZ599truRqVixou644w7t2bPHt9yum31n7HO1gt6OwVoOWDDvX4D379/f9936z3/+4z73E5FV73O075stu/DCC91+vdvZ99hu1B5++OETOl4Ax49yLWtQrkmRkZG65pprXJDttWHDBncPYvOP1f3IynD7Ph6tXPaWR/6++OKLgIDR7p8uv/xyV2ZbedW8eXNNnTr1lD5f7/G+8MILrty2cu7OO+9USkqKW27HbeX/tGnT3LF4z2Pnzp3uHsHuGQsUKKAuXbpo+fLlvv16z+frr79WvXr13P2Z3QvYfd5TTz3lu7+oXLmyvvzyS23fvt2dm82z+4M5c+b49pWQkKCrr75aFSpUcO9ly0ePHh1wDsGuabDWbePGjdNZZ53ljseOJf29jc37v//7P910003uPs3KarsfRs4g6MZpyZoPlShRQr/99psLwC1w7NGjh2ue88cff6hTp04uMLO+PcYCk3PPPdcFiPbjaAH21q1b1bNnz4DC2gKW33//3TWZDg8P1xVXXOF72vnqq69qwoQJLlC1oOzDDz884WZaxgIhC9aXLFmiBg0a6JFHHtF7772nYcOGuabNFvhdd9117kc5M3379nU/9kebjlVzPXbsWNWuXdtN9n52DP4Bmp3nY489pqefftpdMyvgLGA+Fbafe+65R4MGDXLX0D6H9u3bu2VWALVu3Vq33nqr+7xssiD3eD67Bx98UD/++KPGjx+vyZMnu8Jr7ty5J3RsVtB9/PHH7t/58uXzzbfvgX32f/31l/ve/fDDD+4z9GffM7sJ+OCDD1xhb9femt57WUFpN0Dvvvuua1WwY8cOd6wnKive52jfNyvw7Rzt/ys7Z+93zW6a7AELgNChXKNcy6pyzR6SWxnvvQeyoNIeptpv+YnIrFw+HvZw2h6WW6A9b948d19mXbhOplWdP7smFtDbX/t/xs7NW2nw+eefu2O1Y7ZjtdfeQNfuH+wezloA2L2OHZs3WDd2reze7J133nFlY6lSpdz8l156SW3btnXnYC3h7N7SgnArN+1+s0aNGu619/7JKieaNm3qAni7b7jtttvcNrNnzz6ha2qftX0frrrqKi1cuNCVwQMHDsxQQWLlvlUa2PFZpYDdD//999+ndI1xkjzAaebcc8/1nHPOOb7Xhw4d8hQsWNDTu3dv37zN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IegGAAAAACBECLoBAAAAAAgRgm4AAAAAAEKEoBsAAAAAgBAh6AYAAAAAIEQIugEAAAAACBGCbgAAAAAAQoSgGwAAAACAECHoBgAAAAAgRAi6AQAAAAAIEYJuAAAAAABO16B76NChqlq1qmJiYtS0aVNNnz79qOu/8cYbqlu3rvLnz6/atWtr1KhRActHjhypsLCwDNOBAwdCfCYAAAAAAASKVA4aO3as+vXr5wLvtm3b6q233lKXLl20ePFiVapUKcP6w4YN04ABA/T222+refPm+u2333TrrbeqaNGiuvTSS33rxcbGaunSpQHbWlAPAAAAAEB2CvN4PB7lkJYtW6pJkyYumPayWuxu3bpp8ODBGdZv06aNC86ff/553zwL2ufMmaMZM2b4arpt3q5du076uJKSkhQXF6fExEQXwAMAAMpQAADyVPPy5ORkzZ07Vx07dgyYb69nzpwZdJuDBw9mqLG2ZuZW452SkuKbt2fPHlWuXFkVKlRQ165dNW/evKMei+3XAm3/CQAAHBtlKAAAuTTojo+PV2pqqkqXLh0w315v2bIl6DadOnXSO++844J1q6C3Gu4RI0a4gNv2Z+rUqeNquydMmKDRo0e7IN1qx5cvX57psVitutVse6eKFStm8dkCAHB6ogwFACCXNi/ftGmTypcv72q1W7du7Zv/9NNP64MPPtDff/+dYZv9+/frzjvvdMvtsC1Av+666/Tcc89p69atKlWqVIZtDh8+7Jqwt2/fXq+++mqmT+lt8rKabgu8aV4OAMDRUYYCAJBLa7pLlCihiIiIDLXa27Zty1D77d+U3Gq29+3bpzVr1mjdunWqUqWKChcu7PYXTHh4uEu6drSa7ujoaNd3238CAADHRhkKAEAuDbqjoqLcEGFTpkwJmG+vLWHa0eTLl8/117agfcyYMa7ftgXXwViN+Pz581W2bNksPX4AAAAAAHL1kGH9+/dX79691axZM9fEfPjw4a72um/fvm65DQ+2ceNG31jcy5Ytc0nTLOv5zp07NWTIEP311196//33fft84okn1KpVK9WsWdM1E7cm5RZ02/jeAAAAAACcMUF3r169lJCQoEGDBmnz5s2qX7++Jk6c6DKPG5tnQbiXJV578cUX3RjcVtvdoUMH1yfcmph72VBht912m2u2bknRGjdurGnTpqlFixY5co4AAAAAgDNX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d97R4sWLValSpQzrz5gxQ+eee65eeuklXXrppdq4caP69u2rmjVravz48W6dWbNmuaD9ySefdIG2zX/00Ufdti1btjyu40pKSnIBfWJiomJjGQMTAIDjRRkKAMBJBN2hYkFwkyZNNGzYMN+8unXrqlu3bho8eHCG9a1G29a1Gmyv1157zdV8r1+/3r3u1auXK/AnTZrkW8dqw4sWLarRo0cf13FxwwAAwMmhDAUAIETjdJ+o5ORkzZ07Vx07dgyYb69nzpwZdJs2bdpow4YNmjhxosuevnXrVn322We65JJLfOtYTXf6fXbq1CnTfZqDBw+6mwT/CQAAHBtlKAAAWZxIzVjgO2HCBK1bt84Fz/6GDBlyXPuIj49XamqqSpcuHTDfXm/ZsiXToNv6dFtt9oEDB3To0CFddtllrrbby7Y9kX0aq1V/4oknjuu4AQAAZSgAACGr6f7+++9Vu3Zt1w/7xRdf1I8//qj33nvPjdE9f/78E92d6wfuz2qw08/zsr7elqjN+mhbLfm3337rhjCzft0nu08zYMAA13/bO3mbqgMAgKOjDAUAIItruq1wvf/++zVo0CAVLlzYZQovVaqUrr32Wtd3+niVKFFCERERGWqgt23blqGm2r9G2hKuPfjgg+51gwYNVLBgQZc47amnnlLZsmVVpkyZE9qniY6OdhMAADgxlKEAAGRxTfeSJUt0ww03uH9HRkZq//79KlSokAvCn3322ePeT1RUlJo2baopU6YEzLfX1ow8mH379ik8PPCQLXA33nxwrVu3zrDPyZMnZ7pPAAAAAAByTU231Sxb0hRTrlw5l0ncxtf29tM+Ef3793dDhjVr1swFy8OHD3f9xL3Nxa1W3YYFGzVqlHttw4TdeuutLoO5JUezsb379eunFi1auGMx9957r9q3b+8eAFx++eX68ssvNXXqVDdkGADg9LI3Za+iIqKULzxfTh8KAABA1gTdrVq10i+//KJ69eq5rOHW1HzhwoX6/PPP3bITYQnREhISXC25BdD169d3mckrV67slts8C8K9brzxRu3evVuvv/66e98iRYro/PPPD6hhtxrtMWPG6JFHHtHAgQNVvXp1Nx748Y7RDSCH7NshbfhdKlhCKt+UjwFHNXfrXL3w+wv6K+EvFYgsoG41uql/s/6KjohWSmqKRi0epUmrJ+nQ4UO6oPIFuqn+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bNmy7smrNWsePHiwrzC1p8nz58/3rW9Plm2ePWn2f+JsBWyzZs3cU24Ldq0wswLYnujmz59fDRs21GeffRby62jvX6ZMGfek/ZprrtG1117rq131NiEbMWKE74m8Hee6det0+eWXu2sYGxurnj17auvWrb59erd766233H4LFCigHj16BDxlP3z4sAYNGqQKFSq4/dr6du29vNfyk08+cZ+1XesPP/zQfbaJiYm+Gnp7r2DNy4/3GD/44AO3bVxcnK666irt3r37qNfrl19+0bnnnuvOyb53nTp10s6dO4+7ptq+V1Zbf6zvkv3bXHHFFW4/3tfmq6++UtOmTd029rk88cQTOnToUMD7vvnmm+78CxYsqKeeeuq4tlu+fLnat2/vltsNzpQpU456LQCcWSh3T90dd9zhfqMtmPzXv/6lWrVq6ayzzlL//v3166+/HndNtd1n2DwrK83atWtdbbmVS/a7b/ucOHGiW96hQwe3ji2zbayVmznWfUdm9yvpm5fb/rp166YXXnjBlWd272QPEPwflG/evFmXXHKJe5+qVavq448/Pma3MCuzHnroIXfPZuta+WX3fTfccINvnYMHD+o///mPu9ew46tZs6beffdd3/Kff/5ZLVq0cMvs2P73v/8FlHv2nbZy2K5/iRIldNFFFx3XvSKQHkE3Tmvvv/+++5G0wst+iP/973+74K5Nmzb6448/XEBkP5T79u3z/ehbwGSFxZw5c1yQZ4GYBWReFrTbj681V7bmU+Hh4S7wsSDRvPrqq5owYYILBpcuXeoCQf+A6HhZIWEB1pIlS9yT7kceecTVOFsN86JFi3TffffpuuuucwXG0Z4Ce584ZzZZ8HkirED0LyhXrFjhznXcuHG+hwlWuO7YscMdmwVm9lCiV69eAfvxbmeFpl1n29YKYa9XXnlFL774oiukFyxY4D6ryy67zAV+/qxp2T333OOu0wUXXOAKaAui7bO0yZq5pWc3EsdzjDbPgmJrqmaTrfvMM89kem3sHOwY7GZm1qxZmjFjhrvJsSf/J+No3yX7/hn7Tth5el/bzY99L+ya2E2BPdiwIP7pp58O2Pdjjz3mgu6FCxfqpptuOuZ29v3u3r27IiIi3I2fBe127QHAH+XuyZe7ViZZeWhloQXG6QXrOnW8bJ8WgE6bNs397j/77LPuWCwYtfLbWDlj5YmVv+Z47zvS368E8+OPP7oy1f7ad8TKF+8DZnP99ddr06ZNLpC347FKkm3bth31nKxCwB4cHO1huO13zJgxrjy147Oyy1sLv3HjRhc4N2/eXH/++ac7TwvIvQ+ivex4rdWDPVS3svF47hWBDDzAaercc8/1nHPOOb7Xhw4d8hQsWNDTu3dv37zNmzd77H+DWbNmudcDBw70dOzYMWA/69evd+ssXbo06Pts27bNLV+4cKF7fffdd3vOP/98z+HDhzOsu3r1arfuvHnzfPN27tzp5v3444/utf2111988YVvnT179nhiYmI8M2fODNjfzTff7Ln66qszvQZbt271LF++/KhTSkpKptvfcMMNnssvv9z3evbs2Z7ixYt7evbs6V4/9thjnnz58rlr4DV58mRPRESEZ926db55ixYtcuf022+/+bazdezaek2aNMkTHh7uPhNTrlw5z9NPPx1wPM2bN/fccccdAdfy5ZdfDljnvffe88TFxWU4l8qVK3teeumlEzrGAgUKeJKSknzrPPjgg56WLVtmer3ss2jbtu1Rv5P33nuv77W93/jx4wPWsWO3czjWdymz7du1a+f5v//7v4B5H3zwgads2bIB2/Xr1++Etvvuu++CfmbBjgHAmYly99TKXStj7Tf1888/P+a19v/t9d432P2El91n2DwrK83ZZ5/tefzxx4PuK9j2x3PfEex+xVt+NmzYMOBewspguw/z6tGjh6dXr17u30uWLHH7+f33333L7TrZPG+5HczPP//sqVChgrsPadasmSvXZsyY4Vtu9222jylTpgTd/qGHHvLUrl07oIx94403PIUKFfKkpqb6vtONGjUK2O5k7hUB+nTjtOb/xNVq6KxJ09lnn+2bZ02CjPdp6ty5c91T2GB9kewJrTXzsr8DBw50tX3WlMhbw21PruvXr++aUVnzo9q1a7v+0Nb8vGPHjid87NZUy8tqHg8cOOBr1uRlzY8bN26c6T5KlSrlplNhNbx2Pay5ldVwW+3oa6+95lteuXLlgP7S9iTZnpzb5GVNke0JvS2zJ8qmUqVKrum4V+vWrd21tCft1jTbnnhbvzZ/9tqeRmd2nY7X8R6j1SpbNwQva3p2tCfvVtNtLSmyysl8l+w7bLXe/jXbVtNu3x9r0WHXNth1O9Z2dl2CfWYA4I9y9+TL3bRYOq0LUFazVkzW2m/y5Mm68MILdeWVV2ZaK32i9x3HUw5bCzC7D/MvT63G3Vi5bzXJTZo08S2vUaOGa+5+NNbdadWqVe5+zGqhf/jhB1dLb12j7D7NymR7T6uVDsbKNSvH/K+33Wfs2bNHGzZscGVesPM7nntFID2CbpzW0ieH8mbh9n9tvIGz/bXmwNbsKj0rIIwtt2Dt7bffVrly5dw2FmxbQWSs0Fi9erXrKz516lTX3MgKOOsHZU3R/QvWoyX/8m9a5j2+b775RuXLlw9Yz/ohHa15uTVJPhorWL0FSzDW18uaXNl1s/NNf03TN4Gzcwt2w5DZfC/vMv910q8fbB/BmuAdy/EeY7Dvj/ezyKzp/Ymw/fl/F9J/H472XcqMHZ/dcFhT8PSsL3Zm1+1Y26U/Tu/xA4A/yt2TL3etv7H9rlowaF2gjtfx3FtYElTrpmX3ERZ4W3Nw68JlXe+COZH7juMph49WngYrX442P/1+27Vr5ybrj21Nwy0fjHV/OlaZHOxeINiDj2Dl5bHuFYH0CLoBPxbkWF8iq+EMlrXUMmJaYWh9euwH3li/3fSsT7H1D7bJEqFYLaX11fLWCFt/IO+TYv+kapmxWlgr5Kw2PbMntsFYwROsT7M/C6SPxgobe+J8vOxY7TjXr1/vq0m2GwxLbla3bl3feraO1WZ739/6QNuNgz0htutn8+3a2pNsL8tKaglPjsayih6rD/XxHuOJsloD6+dvwevxsO+DfRe8rL+6N7/Asb5LxYoVczcb6c/VvsNWa3Ain9nxbOe9Zuk/MwA4FZS7R9jvugXGb7zxhquZTh/sWaK0YP26/e8tvLXDwe4trLyzh/E2DRgwwFUeWNDtzcbtX56c7H3HyahTp45rTTdv3jyXDM2b9+VkhjCz47Z9WS29tWy0ANn6oNsD62Dr2j2ff/Bt9xnWwi39g4YT+c4CwfBNAdIlGrFC6Oqrr9aDDz7okrDZD78l4bD5VphZE3VL8GFPM60wsier/l566SW3zBJsWBD56aefumQfVlDa61atWrlkXPZjbc3TLVHJsVgBYMGzJTGxAsSypyclJbnCwZo3+WfqzOrm5SfKCjYLPi3LuSU1s8LPsrFaoe3fRMtqT+24LVGanYvdYFhNrl0rY9ffkn1Zhnm7lpbMxW4iPvroo6O+v11Xaxpmwa9lWrXm1N4m1Sd6jCfKbmKskLd92U2N3chYEzRrcm7fpfTOP/98N9SJfSfsc7Un8/61AUf7LnnP1c7TmsPZzZF9Px999FHXDN1urux9bTtLRGfN+NInh/F3rO3smlkzd0tKY7Uj9pk9/PDDJ32tAMBQ7gYaOnSoS/ZqD5jtwbmVVVZGWcJPa3VmD/7Ts4el9tttWcPt99oe4NrvtD8byaVLly7uwbaNqGFNsb0Pma2bmAWd1p3MEotZDfHJ3necbNBtZcxtt93ma1l3//33u+M4Wosqyyxu92tWbtu9mT08t2zm1kLPHljbZMdpyUItkZrdE1gWd+smZvcbVlbbPYA9eLAM5fbg2e47LFmut/XAyXxn/ZvRAz50a8fpKn3SqvTJtLzSJ4JatmyZ54orrvAUKVLEkz9/fk+dOnVccg5vog1LyFG3bl1PdHS0p0GDBp6ffvopYB/Dhw93STcsaVtsbKznggsu8Pzxxx++/S9evNjTqlUrt29bz5J6BUuk5p/QxNj7v/LKKy7phyUNKVmypKdTp04ukUiopE+kll76ZClea9eu9Vx22WXuGhQuXNglTNmyZUuG7YYOHeoSplmylu7du3t27NjhW8eSmDzxxBOe8uXLu/O19S1x19GS0nn17dvXJXyz5fZewT774z1Gf7a97edo7PvQpk0b9/2w75B9Rt7PMv13cuPGjS4Zix1DzZo1PRMnTgxIpHas79KECRM8NWrU8ERGRgYc17fffuuOwb5jtl2LFi3cvrwyS352rO0sQYwlJ4yKivLUqlXLrU8iNQBelLtZY9OmTZ4777zT/a7b762Vg1Zeee8Tgv2OWwIxS5Zm5aklxvz0008DEqndddddnurVq7uyye4fLKlsfHy8b/tBgwZ5ypQp4wkLC3Nl//Hcd2R2vxIskVr6ewkrC+374n/OXbp0ccdn5/3xxx97SpUq5XnzzTczvU6W/LN169aeYsWKufOuVq2a55577gk4r/3793vuu+8+lxTUrqWVmSNGjAgosy1Jqy2z8//vf/8bkOgu2Hf6eO4VgfTC7D9HQnAACD17Gm9DcR1P03oAAHBmsURmVntv+UxsKE4gr6N5OQAAAIAcY83drWuYddGyvuk29rd1ofLP6wLkZQTdAAAAAHKMZVu3/tg2BJj1J7d+7ZbDJX3WcyCvonk5AAAAAAAhknlqPgAAAAAAcEoIugEAAAAACBGCbgAAAAAAQoSgGwAAAACAECHoDsKGLk9KSnJ/AQDA8aMMBQAgEEF3ELt371ZcXJz7CwAAjh9lKAAAgQi6AQAAAAAIEYJuAAAAAABChKAbAAAAAIAQIegGAAAAACBECLoBAAAAAAgRgm4AAAAAAEKEoBsAAAAAgBAh6AYAAAAAIEQIugEAAAAACBGCbgAAAAAAQoSgGwAAAACAECHoBgAAAAAgRAi6AQAAAAA4HYPuadOm6dJLL1W5cuUUFhamL7744pjb/Pzzz2ratKliYmJUrVo1vfnmmxnWGTdunOrVq6fo6Gj3d/z48SE6AwAAAAAAMhepHLR37141bNhQffr00ZVXXnnM9VevXq2LL75Yt956qz788EP98ssvuuOOO1SyZEnf9rNmzVKvXr305JNP6oorrnABd8+ePTVjxgy1bNlSOWnXvmSN+2Oj1u/Yp4YV43Tx2WUVHRnhlh1KPazvFm3VnLU7VCY2Rt2bVFDJwtG+beev36VJf21WeFiYujYoq7PKxfmWbUk8oHF/bFD8noNqVa24LqxbWhHhYW7ZgZRUTZi/SYs3J6lqiYK6okl5xcbk8207Y3m8flq6TQWiI3VF4/JuHa/V8Xs1ft5G7Tt4SB3qlFLbGiV8yxL3p2j8Hxu0JmGf6pWL1WUNyykmX9q5pB72aMrirZq9OkElCkXrX00rqHRsjG/bvzYm6puFm3XY49HF9cuqYcUivmXbdh/Q539s1NakA2pWuZg6nVVakRHhvnP5ZsFmLdyYqErFCqh7k/IqUiDKt+2vqxL0/ZKt7jgub1RONUoV9i1bl7BPn8/boN0HDql9rZJqX7OEe9Bjdh9I0RfzN2nltj2qU6awLmtUTgWi0v7XOHzYox/+3qaZKxNUrGA+97mUK5L/lL8LAAAAAM4MYR6Px6NcwAIgC5C7deuW6Tr//e9/NWHCBC1ZssQ3r2/fvvrzzz9dsG0s4E5KStKkSZN863Tu3FlFixbV6NGjj+tYbPu4uDglJiYqNjZWWeHvLUm65u3Z2rE32TevXtlYjb61laIiw3X9iNn6fc1O37LC0ZEaeVMLNa1cVC9OXqrXflgRsL//dq6jf59XXb+siNct78/R/pRU37J2NUvo3RuaK+lAinq9NUsrt+/1LSsdG60xt7VWleIFdP8nf+rzeRt9yyLDw/Riz4a6vFF5jZ+3QQ98usAF0F5XNqmgF3o00Kr4vbpq+K/avvugb1mNUoU09rZWKhQTqZtG/q5fViT4lhWIitA7NzRTm+ol9MaPK/T8d0sDzuWeC2qq/0W1NGfNDt343u/ac/CQb1mLqsU06qYWLuC29/x7y27fsuIFo/Txra1Uu0xhPTR+oT6evc63zJ45DO5+tno1r6SJCzfr3jHzlJJ65FwuaVBWr13VWBt37XfXaFPiAd8yuzZjb2/t9n/7B3P1/d/bfMuiI8P1Zu+m6lC71FE+bQA4c4WiDAUAIC/LU326LbDu2LFjwLxOnTppzpw5SklJOeo6M2fOVE56fMKigIDbWO3zW9NW6oNf1wQE3Gb3wUN65Iu/tHzr7gwBt3n+u79djfmAzxcGBNxm+vJ4fTZ3g179fnlAwG22Jh3U098s0U9LtwcE3ObQYY97z+27D2jgF4sCAm5jtem2b9veP+A2K7btccf5ye/rAwJusy85VQ+P/0vrEva6BwjppR3nHhc4+wfc5rfVO/Thr2s17KeVAQG3SdibrEFfL3I13P4Bt7FDf3zCYldz/vD4hQEBt7Ea88mLt+qZSX8HBNzGau+HTF7mar/9A25z8NBhPfT5wgzXBgAAAAByXfPyE7VlyxaVLl06YJ69PnTokOLj41W2bNlM17H5mTl48KCb/J/SZ6W9Bw/p11U7gi6bumRrQBNpf0s2J2UIjL0s5hv7+3qt27Ev0/0uTRekev24dJtKxwZ/T2t+/dGv6zIEv17WbNyao2f2nmsSCgVdZk3VP5mz3h13MOPmrteyrXsy3W/8nsAHFl7W7LuWXzNyf/Ywws5l576UTPf7/d9bgy6z+dZSIJjNiQe0aFOiGlQ40iweAM5UoS5DAQDI6/JUTbfx9sP18raO958fbJ308/wNHjzYNYXzThUrVszSY7b+1VH/9EtOL3++CDcFY4dcMCr4MlM4JvNnJrbPmHzB39OaSMdEnvx+vf3Qg77nUfZbyK8veYZl0Sd3Lvkiwl3z9ZO/RsG3jTnK5+LdFgAQ+jIUAIC8Lk8F3WXKlMlQY71t2zZFRkaqePHiR10nfe23vwEDBri+Z95p/fr1WXrcFsB1ObtM0GWWvMymYNrXLOn6JFuf7/QsGL+6ZSXX5/tE9+uWNanggvr0KhTNr+tbV1H5IMnCbP1ujcu7JGXBWJIxS9QWTIsqxXR1i0pBA2R7CGDneY5forYM+21cIeiyrmeXVfemFVwf7vQsEV3vVpVVzS85nD871syuUXd3jYIvq18+VjVLB69dB4AzTajLUAAA8ro8FXS3bt1aU6ZMCZg3efJkNWvWTPny5TvqOm3atMl0vza0mCV78Z+y2uOXnqVmfgGyBbA9mlZQ79ZVXCB7U9uqAYGjJVl79soGLnB89arGLrGaV5EC+TT0uqYuC/nLvRq5JGb+ydDu6lBDF9YrrdvPre4ynftrW6O4/teljuqXj9MTl53lAl6vsnExevO6pspnycKua+qyqHtZTfOgy+u7TOUPXVJXbaqnPeTwsuzlt7Srqk5nldEd51V3x+FVq3Qhl6AtLn8+vXFtE/fXvyb6tasbq3ihaD3fo4Hqlo0NaCFwyzlVdWnDcrqxTRWXyM3/QUHzKkX16KX1VL1kIT3TvUFA7XOpwtHuHKLzRWjodU3cwwQve4jxyCV11aRSUT3YqbbOrVUy4FwsY/qd59dQu5olXYK3fBFH3tQCePs8AADZV4YCAJCX5Wj28j179mjFirQkYY0bN9aQIUPUoUMHFStWTJUqVXJPzzdu3KhRo0b5hgyrX7++br/9djdsmCVNs+zllpXcO2SYJUxr3769nn76aV1++eX68ssv9cgjj5zQkGGhzLw6b91Obdi53wW9/sNzmQ0792neul0qExej5lWKBSzbl3zIJTGzIcMsO7l/s2j7CK3PeMLeg247/+G5zIptu7Vk8273fva+6Ycxs37RBaMj1bZ6cd/wXN5hzH5ZmeD6pFuQnb7v+cINiVqTsNcFyv6Bv3cYs9/X7HAPDVpWLRbQvN8ykdu52JBhdi7e4bn8k6fZkGFNKhfNUOO+avseLdqUpIrFCqiR31Bj3mHMZq6IV0xUhKs1t6bnXpb4bObKeCXtP6TW1YurWMHAc7E+2qu273WZ0Gulq8W2ZGx2TMUKRLkh2cKDVasDAByylwMAkIuC7p9++skF2endcMMNGjlypG688UatWbPGref1888/67777tOiRYtUrlw5N4yYBd7+PvvsMxdor1q1StWrV3cBePfu3Y/7uLhhAADg5FCGAgCQS8fpzk24YQAAgDIUAIAzrk83AAAAAAB5CUE3AAAAAAAhQtANAAAAAECIEHQDAAAAABAiBN0AAAAAAIQIQTcAAAAAACFC0A0AAAAAQIgQdAMAAAAAECIE3QAAAAAAhAhBNwAAAAAABN0AAAAAAOQt1HQDAAAAABAiBN0AAAAAAIQIQTcAAAAAACFC0A0AAAAAQIgQdAMAAAAAECIE3QAAAAAAhAhBNwAAAAAAIULQDQAAAABAiBB0AwAAAAAQIgTdAAAAAACECEE3AAAAAAAhQtANAAAAAECIEHQDAAAAABAiBN0AAAAAAIQIQTcAAAAAACFC0A0AAAAAQIgQdAMAAAAAECIE3QAAAAAAhAhBNwAAAAAAIULQDQAAAABAiBB0AwAAAAAQIgTdAAAAAACECEE3AAAAAAAhQtANAAAAAECIEHQDAAAAABAiBN0AAAAAAIQIQTcAAAAAACFC0A0AAAAAwOkadA8dOlRVq1ZVTEyMmjZtqunTpx91/TfeeEN169ZV/vz5Vbt2bY0aNSpg+ciRIxUWFpZhOnDgQIjPBAAAAACAQJHKQWPHjlW/fv1c4N22bVu99dZb6tKlixYvXqxKlSplWH/YsGEaMGCA3n77bTVv3ly//fabbr31VhUtWlSXXnqpb73Y2FgtXbo0YFsL6gEAAAAAyE5hHo/HoxzSsmVLNWnSxAXTXlaL3a1bNw0ePDjD+m3atHHB+fPPP++bZ0H7nDlzNGPGDF9Nt83btWvXSR9XUlKS4uLilJiY6AJ4AABAGQoAQJ5qXp6cnKy5c+eqY8eOAfPt9cyZM4Nuc/DgwQw11tbM3Gq8U1JSfPP27NmjypUrq0KFCuratavmzZsXorMAAAAAACAXBt3x8fFKTU1V6dKlA+bb6y1btgTdplOnTnrnnXdcsG4V9FbDPWLECBdw2/5MnTp1XG33hAkTNHr0aBekW+348uXLMz0WC+atdtt/AgAAx0YZCgBALk+kZknO/FkwnX6e18CBA12f71atWilfvny6/PLLdeONN7plERER7q8tu+6669SwYUO1a9dOn3zyiWrVqqXXXnst02OwpuzWnNw7VaxYMUvPEQCA0xVlKAAAuTToLlGihAuU09dqb9u2LUPtt39TcqvZ3rdvn9asWaN169apSpUqKly4sNtfMOHh4S7p2tFqui05m/Xf9k7r168/xbMDAODMQBkKAEAuDbqjoqLcEGFTpkwJmG+vLWHa0Vgtt/XXtqB9zJgxrt+2BdfBWM35/PnzVbZs2Uz3Fx0d7RKm+U8AAODYKEMBAMjFQ4b1799fvXv3VrNmzdS6dWsNHz7c1V737dvX9/R848aNvrG4ly1b5pKmWdbznTt3asiQIfrrr7/0/vvv+/b5xBNPuCbmNWvWdH2zX331VRd02/jeAAAAAACcMUF3r169lJCQoEGDBmnz5s2qX7++Jk6c6DKPG5tnQbiXJV578cUX3RjcVtvdoUMHl+ncmph72VBht912m2u2bv2zGzdurGnTpqlFixY5co4AAAAAgDNXjo7TnVsxTjcAAJShAACcFtnLAQAAAAA4XRF0AwAAAAAQIgTdAAAAAACECEE3AAAAAAAhQtANAAAAAECIEHQDAAAAABAiBN0AAAAAAIQIQTcAAAAAACFC0A0AAAAAQIgQdAMAAAAAECIE3QAAAAAAhAhBNwAAAAAAIULQDQAAAABAiBB0AwAAAAAQIgTdAAAAAACECEE3AAAAAAAhQtANAAAAAECIEHQDAAAAABAiBN0AAAAAAIQIQTcAAAAAACFC0A0AAAAAQIgQdAMAAAAAECIE3QAAAAAAhAhBNwAAAAAAIULQDQAAAABAiBB0AwAAAAAQIgTdAAAAAACECEE3AAAAAAAhQtANAAAAAECIEHQDAAAAABAiBN0AAAAAAIQIQTcAAAAAACFC0A0AAAAAQIgQdAMAAAAAECIE3QAAAAAAhAhBNwAAAAAAIULQDQAAAABAiBB0AwAAAAAQIgTdAAAAAACcrkH30KFDVbVqVcXExKhp06aaPn36Udd/4403VLduXeXPn1+1a9fWqFGjMqwzbtw41atXT9HR0e7v+PHjQ3gGAAAAAADkwqB77Nix6tevnx5++GHNmzdP7dq1U5cuXbRu3bqg6w8bNkwDBgzQ448/rkWLFumJJ57QnXfeqa+++sq3zqxZs9SrVy/17t1bf/75p/vbs2dPzZ49OxvPDAAAAAAAKczj8Xhy6kK0bNlSTZo0ccG0l9Vid+vWTYMHD86wfps2bdS2bVs9//zzvnkWtM+ZM0czZsxwry3gTkpK0qRJk3zrdO7cWUWLFtXo0aOP67hs+7i4OCUmJio2NvYUzxIAgDMHZSgAALmkpjs5OVlz585Vx44dA+bb65kzZwbd5uDBg64Zuj9rZv7bb78pJSXFV9Odfp+dOnXKdJ/e/dpNgv8EAACOjTIUAIBcGnTHx8crNTVVpUuXDphvr7ds2RJ0Gwue33nnHResWwW91XCPGDHCBdy2P2Pbnsg+jdWqW822d6pYsWKWnCMAAKc7ylAAAHJ5IrWwsLCA1xZMp5/nNXDgQNfnu1WrVsqXL58uv/xy3XjjjW5ZRETESe3TWD9xa0rundavX3+KZwUAwJmBMhQAgFwadJcoUcIFyulroLdt25ahptq/KbnVbO/bt09r1qxxCdeqVKmiwoULu/2ZMmXKnNA+jWU5t77b/hMAADg2ylAAAHJp0B0VFeWGCJsyZUrAfHttCdOOxmq5K1So4IL2MWPGqGvXrgoPTzuV1q1bZ9jn5MmTj7lPAAAAAACyWqRyUP/+/d2QXs2aNXPB8vDhw13tdd++fX1N1jZu3Ogbi3vZsmUuaZplPd+5c6eGDBmiv/76S++//75vn/fee6/at2+vZ5991jU///LLLzV16lRfdnMAAAAAAM6IoNuG90pISNCgQYO0efNm1a9fXxMnTlTlypXdcpvnP2a3JV578cUXtXTpUlfb3aFDB5eV3JqYe1mNttV+P/LII64PePXq1d144BaoAwAAAABwxozTnVsxxigAAJShAACcFtnLAQAAAAA4XRF0AwAAAAAQIgTdAAAAAACECEE3AAAAAAAhQtANAAAAAECIEHQDAAAAABAiBN0AAAAAAIQIQTcAAAAAACFC0A0AAAAAQIgQdAMAAAAAECIE3QAAAAAAhAhBNwAAAAAAIULQDQAAAABAiBB0AwAAAAAQIgTdAAAAAACECEE3AAAAAAAhQtANAAAAAECIEHQDAAAAABAiBN0AAAAAAIQIQTcAAAAAACFC0A0AAAAAQIgQdAMAAAAAECIE3QAAAAAAhAhBNwAAAAAAIULQDQAAAABAiBB0AwAAAAAQIgTdAAAAAACECEE3AAAAAAAhQtANAAAAAECIEHQDAAAAABAiBN0AAAD4//buAzqq4u3j+C8JhISS0EMPXXrvHaSqCNhQFAWxoP5VRBQRbIhiRRQV4RXsAgqioHQUqaIgINJ7KAmBAAk1gWTfMxM3ZLMb+qZ+P+fcE+7csnfvLjv77Mw8AwDwEoJuAAAAAAC8hKAbAAAAAAAvIegGAAAAAMBLCLoBAAAAAPASgm4AAAAAALyEoBsAAAAAAC8h6AYAAAAAIKsG3R9//LHKlSungIAA1a9fX0uWLLng/t98841q166t3Llzq3jx4urbt6+ioqKStn/++efy8fFxW86cOZMGzwYAAAAAgAwSdE+ZMkUDBgzQ0KFDtWbNGrVs2VJdunRRWFiYx/2XLl2qe++9V/369dOGDRv0/fff66+//tIDDzzgsl9QUJDCw8NdFhPUAwAAAACQbYLuUaNG2QDaBM1Vq1bV6NGjVbp0aY0dO9bj/n/88YfKli2rJ554wraOt2jRQg8//LBWrVrlsp9p2S5WrJjLAgAAAABAtgm64+LitHr1anXs2NGl3KwvX77c4zHNmjXTvn37NGvWLDkcDh08eFBTp07VjTfe6LLfiRMnFBoaqlKlSummm26yregAAAAAAGSboPvw4cOKj49XSEiIS7lZj4iISDXoNmO6e/bsKX9/f9uCnT9/fo0ZMyZpnypVqthx3TNmzNCkSZNst/LmzZtr27ZtqV5LbGysYmJiXBYAAHBx1KEAAGTwRGqmK3hypgU7ZZnTxo0bbdfyF1980baSz5kzR7t27VL//v2T9mnSpInuuecem2zNjBH/7rvvVLlyZZfAPKWRI0cqODg4aTFd3AEAwMVRhwIAcGE+DhPlplP3cpOB3CRD69GjR1L5k08+qbVr1+r33393O6Z37942C7k5JnlyNRNcHzhwwGYz9+TBBx+03dJnz56d6q/0ZnEyLd0m8I6OjrZJ2QAAgGfUoQAAZNCWbtM93EwRNn/+fJdys266kXty6tQp+fq6XrKfn5/9m9pvB6bcBPGpBeRGrly5bHCdfAEAABdHHQoAwIXlUDoaOHCgbb1u0KCBmjZtqvHjx9vpwpzdxYcMGaL9+/fryy+/tOtdu3a1rdYmu3mnTp3sVGBmyrFGjRqpRIkSdp9XXnnFdjGvVKmSbbH+4IMPbND90UcfpedTBQAAAABkQ+kadJuEaFFRURo+fLgNoGvUqGEzk5vM44YpSz5nd58+fXT8+HF9+OGHevrpp20StXbt2unNN99M2ufYsWN66KGHbDI2Mz67bt26Wrx4sQ3MAQAAAADIFmO6MzLTQm4CdsZ0AwBAHQoAQKbOXg4AAAAAQFZF0A0AAAAAgJcQdAMAAAAAkBUTqQEA4A1HzxzVr2G/6lzCObUp3UYheUK40QAAIF0QdAMAspR5u+fp+aXPKzY+1q6P/HOkBjcarLuq3JXelwYAALIhupcDALKM6NhoDVs2LCngNuId8Xrjzze0N2Zvul4bAADIngi6AQBZxuJ9i3X63Gm38gRHgubumZsu1wQAALI3gm4AQJZhguvUOByONL0WAAAAg6AbAJBltC7VWrn8crmV+8hH7UPbp8s1AQCA7I2gGwCQZeQPyK+Xm72sHL45XALupxs8rXLB5dL12gAAQPZE9nIAQJZyU/mb1KhYI83fM99OGdauTDuVzlc6vS8LAIBM4Z9D/2jp/qXKkzOPupTroqK5i6b3JWV6Pg4GubmJiYlRcHCwoqOjFRQUlB6vCwAAmRJ1KABkXiP+GKEpW6Ykrfv7+uvt1m/bH7Bx5eheDgAAAADZ3IoDK1wCbiMuIU4vLX/JZSpOXD6CbgAAAADI5haGLfRYfiz2mFZHrE7z68lKCLoBAAAAIJtLnoQ0pZx+OdP0WrIagm4AAAAAyOZuKHeDx/KQ3CGqV7Reml9PVkLQDQAAAADZXK0itfRkvSfl5+OXVFYgVwG90/od+fmeL8PlI3u5B2ReBQDgylCHAkDmFnEywiZVM1OGtSrVSgE5AtL7kjI95ukGAAAAgGxgZfhKfbvpW0WeilTtorXVp3ofFctTzGUfs96jUo8Lnmf/if2atXOWTp07pRYlW6h+SH0vX3nmRku3B/xKDwDAlaEOBYCMaeaOmRq6dKgcciSVFQoopMk3TXYLvC9kzu45GrJkiM4lnEsqu7XSrXq52cvX/JqzCsZ0AwAAAEAWFp8Qrw/WfOAScBtRZ6L0xYYvXMoOnz6sn3f+rEV7F+ls/FmXbafPndbw5cNdAm5j2rZpWn5guRefQeZG93IAQJazJnKNZu+abb8UXF/mejUv2Ty9LwkAgCuy/eh2/RH+h4JzBds6LXfO3Jd9joOnDtqx2p6sO7Qu6d9fbvhSo/8erbMJicF2kcAi+qDdB6pRuIZdXxWxSsfPHvd4nt/CflOzEs0u+9qiY6Pt8wvMEaimJZoqp2/Wm57sioPu7du3a8eOHWrVqpUCAwPlcDjk4+Nzba8OAIDLNG7dOH249sOk9e+3fq87Kt+hF5q+wL0EAGQqr698XZM2T3LJJv7R9R+pZpGaNv6at2ee5u6ea1uy24e2143lb5Svj3tn5vy58ivAL0Bn4s+4bXN2Ld9weIPeXvW2y7ZDpw9p4KKBmn3LbJvB3N/PP9VrTW2bw+HQivAV+vfwvyqep7g6hHZISs723Zbv9NZfbyk2PtauFw0sqtFtR9vnl62D7qioKPXs2VO//vqrDbK3bdum8uXL64EHHlD+/Pn17rvveudKAQC4iPAT4Rq7bqxb+Xdbv7NJYZy/1AMAkNGZluPkAbdxNPaohiwdopndZ+rVP161Pyw7/br3Vy3Zv0RvtXrLrh88eVAzd85UTGyMmhRvom4VumnK1iku5/ORj+6qcpf9t+lS7kn4yXD9Hfm3GhZraBOmmXm7Tct5SibgT+nMuTP636//swncnN7/+31N6DTBBtoj/hjh0uU98nSknlr0lObcOkc5fHNk3zHdTz31lHLkyKGwsDDlzn2+a4MJxOfMmXOtrw8AgEtmxpPFO+I9bluybwl3EgCQaczdM9dj+Z6YPZq3e55LwO1khlatjVxr67wbp99oA9zPNnymhxc8rMNnDqvndT1ti7dRLHcxDW44WPWK1rPrcfFxqV6Lc5sJop9r9JwK5iqYtM10B3+y7pO26/lzS57TmDVj7I/gxtebvnYJuA0TsJtg+5edv7iNMXdu/yviL2Ull/3zwbx58zR37lyVKlXKpbxSpUras2fPtbw2AAAuSx7/PKlvy5n6NgAAMhrTLTs1G45sSHWbCXJNt21nl22nhWELbSv443Ue1zur37FZyN/46w0blD9e93G1LdPW9gxLKcg/yPYUG75iuH7a/pPiEuJsa/c9Ve9R9cLVVblAZT3565Pad2Jf0jHfbPpG/9fh/7Rgz4JUr7FUXtd4MrmU157tWrpPnjzp0sLtdPjwYeXKletaXRcAAJetTak2drxbSrn8cumG8jdwRwEAmUbH0I4ey02wWq1gtVSPMxnGTTft1Lqsf/LPJ/px+4+21drZsvzCshdsV/MeFV3n5zZdvF9q+pI++PsD27JuAm7nMSawNmOwf9j2g0vAbZw8e9KOD/fz8fN4HWaYcstSLVP9kbxRsUbK1kG3SZz25ZdfutywhIQEvf3222rbtu21vj4AAC6ZScxisqwmn2+0YEBBjWozSoUDC3MnAQCZRrsy7ez818nl88+nkS1H2lZpk1k8JZPhvFWpVqme0wTBZnqvlEw378mbJ2t48+H6rNNn6lejn56o+4R+6fGLnQHkpx0/eT5my2Qt278s1ZlEzHPwpEXJFnZbyiDfXN/QxkOvKEN7lupeboLrNm3aaNWqVYqLi9Ozzz6rDRs26MiRI1q2zPMNBwCPzkQnLsGlzS943CRcE3WK1tGcW+bYpC9myjCT9OVC2VYBAMiITOPmy81e1h3X3WGn1DIZyE3rd17/vHb7Jx0+0fNLnteWo1vsevng8hrRfITN/F0xf0VtP7bd7ZxtyrTRz7s8J0wzrdd7j+/VlC1TbEK2PDny2FbzmyvcnGp3b3NMUK4gj9vMFGB3Vb1L6w+vt13bncoGldWwxsPsv02Q37VCVy3et1i5c+S2vdJCg0KV1fg4LjRYIBUREREaO3asVq9ebVu569Wrp8cee0zFixdXVhATE6Pg4GBFR0crKMjzmwjAVYg7Kf0ySPp3qmQScxQoK3V8Tap6E7cVl+143HGb3dUkUTPjzkyrQOvSrbmT6YQ6FADS1s7onTYmq1igosvc3iZr+P4T++16Dp8ceqj2Q3q41sO64YcbksqTM63OptU6Zdf0zqGdte7wOpvFPKX7qt2nssFl9cqKV9y23V75dr3Y9EX77/WH1tvgu0TeEraVOytlJvda0J3V8YUB8LKp/RID7uTMh+8DC6QSdbn9uGSnzp5S79m9tfXoVpfyp+o/pftr3M+dTAfUoQCQMZjeXqaFPDo22k73VTR3UVs+Z9ccDV4yWAmOBJehWOZH6/9b/39u5zFjvQc1GKR3Vr3jkm3cjOf+9sZv7XlHrR5lx3ifTThr929buq3tBp/VuomnWdC9ePHii475zuz4wgB40YlI6d0qkqdpner2lrp9yO3HJTMt3K+vfN1jl7aFty+0Y9+QtqhDASDjM1NymTo04mSEahWppXur3atP1n2i6dune9z/w3Yf2gDajPuOPBVph3KZ7OUheUKS9ok6HWV/BDeJ3koHlU7DZ5PxXXa7vhnP7Wm8gVN8vOf5UQHAOh7hOeC229y7LQEXsubgGo/lZgza5iOb7S/7AADAlakfU9aRFfJX8HibTMu1GS9uAukL1auFAgupaWBTbvW1yF5+9OhRlyUyMlJz5sxRw4YN7RzeAHBBhStLge5TOlmlG3PzcFmK5C6S+jYPWV0BAIBn3St2T+qCnlzncp1puU7roNskGEu+FC5cWB06dNBbb71lM5kDwAXlDJDaDnUvzx8q1bvP8zEmw/n6qdI/30mnj3KDkeTWyrcqp29OtzvStHhTm0310KlD3C0AAC6BmW7s886fq0u5LsqXM5+dftMkXnut+Wvcv4ySSG3Tpk22tfvEiRPK7BiPBqSBbQukVROlk5FSwQpS1HZp/6rEVvD6fRIDc7+c0qafpekPS3H/fbbkCJRuHiPVup2XCZaZZuTNP99U2PEw+fr4qknxJnZqk9UHV9vtZqzai01e1HUFr+OOpQHqUAAArjLo/ueff1zWzeHh4eF64403dPbs2SwxVzdfGIA0dHi7NK6ldPaUa3n9vtL1L0qjqknnTrtuMy2bA9ZLQVljmkJcPVMX7Tu+zyZQ6zu3r3bH7HbZbrKyzrpllvLkzMPt9jLqUAAArjKRWp06dWzitJSxepMmTTRx4sTLPR2A7O7P8e4Bt7H2G6nwde4Bt5FwVtr4k9Skf5pcIjI+Uy+ZBC+m1TtlwG0cOXNEs3fN1m2Vb0uX6wMAANnXZQfdu3btcln39fVVkSJFFBAQcC2vC0B2cWSn5/L4uMSu56mJj/XaJSHzOnjq4BVtAwAAyDCJ1EJDQ12W0qVLX1XA/fHHH6tcuXL2HPXr19eSJUsuuP8333yj2rVrK3fu3CpevLj69u2rqKgol32mTZumatWqKVeuXPbv9Ome55sDcI2Yni+7l0r//iDFHLi8Y4vV9FxuugHX6SX5evpt0EeqctP51biTUkLC5T0usqTaRWqnuq1OkTppei0AAGRUh08f1pTNU5Lm6r7UY8auHasBvw3QqFWj7LAuXMMx3R988MElnk564oknLnnfKVOmqHfv3jbwbt68ucaNG6dPP/1UGzduVJkyZdz2X7p0qVq3bq333ntPXbt21f79+9W/f39VqlQpKbBesWKFWrZsqVdffVU9evSw5S+++KI9tnHjS5uOiPFowGU4ukf6tqd0aFPiugmSmz+ZOB77kv7DhSeO6T6ZIst068FS2+elleOk2YNNZH9+2/UvSS0HStvmSwtekQ6ulwILSg0fkNo8J/n68RJmY0OXDtWMHTNcykxytfEdxttu6PAu6lAAyNhm7pipl5a/pLNmuJ4kPx8/Pd/4ed1x3R2pHrP3+F7dO/teG3g7mTwpn3b8VDUK10iT687yQbdpib6kk/n4aOfOVLqKemCC4Hr16mns2LFJZVWrVlX37t01cuRIt/3feecdu++OHTuSysaMGWOnK9u7d69d79mzp63wZ8+enbRP586dVaBAAU2aNOmSrosvDMBlmNhFClvuXn7nt1KVGxOn+/p3mnQ8QirTVCrfxnxYuHcx//1taddiKU9hqWE/qd6957cfWCut/kLKZVq/75aKVpX2rZYmdpQSzrmeq9kTUsdXeQmzsQRHgn7Y9oMdw30u4ZyuL3O97qxyp/z9/NP70rIF6lAAyLhMjpMO33dQXEKcS7mZ/cMkHC2Zt6TH44YtHaafdvzkVt6oWCNN6DTBa9ebrcZ0pxzHfS3ExcVp9erVeu6551zKO3bsqOXLPXyBN9+lmzXT0KFDNWvWLHXp0kWRkZGaOnWqbrzxxqR9TEv3U0895XJcp06dNHr06FSvJTY21i7JvzAAuATHwjwH3Ma6yVJwKemrHtKpZENAKnWUen4j5UgWABUsL/U4/+Obi2XvS4vfkWJjJB+/xO7rZsqwlWPdA27DTEPWZojkn5uXMJsyXxxMwjSSpqUN6lAAyDx+C/vNLeB2/mC9YM8CtS7VWqP/Hq0l+5Yor39edavQTY/VfUx/hP/h8Xx/Rvyp+IR4+dHL8NqO6b5WDh8+rPj4eIWEhLiUm/WIiIhUg24zptu0Zvv7+6tYsWLKnz+/be12MsdezjkN06oeHByctJhx6gAuwdnTF9h2SprxhGvAbWybJ/39xaXd3o0zpPkvJgbchiM+sdV8znPSkVR+DDTzeafsqg7Aa6hDASBrOH3utJ12c2HYQhuYm1bxzzZ8piFLhqhAQAGPxwT5BxFweyvo3rdvnx2HbVqpBw4c6LJcrpTj60xv99TG3Jmx3mbMuBmjbVrJ58yZY1vhzbjuKz2nMWTIEEVHRyctzq7qAC6icOXEVmpPyjSRwtd63rZp5qXdWtNq7ck/30khqYwfylNUCipxaedHpnLw5EGtP7RepzxNMYd0Qx0KAJlHm9JtlNM3p8deYmfOnXEZs+1kWsDblm7r8Xy3Vr7VK9ep7D5l2MKFC3XzzTfbcd5btmxRjRo1tHv3bhvYmvHZl6pw4cLy8/Nza4E2XcZTtlQn/zXdJFx75pln7HqtWrWUJ08emzhtxIgRNpu5af2+nHMaJsu5WQBcJvNj1k2jpUl3us61Xa61VON26dcRno9LmZE8/qy0+G3p768Sx4BXaJuYLO2U+we/de6MVO8eaeOP0pljrttaPyv5uVcmyLxMkP3Cshe0IGyB7f6WN2de9a/dX/dVvy+9Lw3UoQCQqRQKLKSXm71sE6mZvCfORGqDGw3W5iObPR7jkEPVClVT3xp99e2mbxUbH2uPuan8TXq8zuNp/AyySdBtftF++umnNXz4cOXLl89Oz1W0aFHdfffdNmHZpTLdw80UYfPnz7dZxp3Merdu3Twec+rUKeXI4XrJJnA3nPngmjZtas+RfFz3vHnzbNd0AF5QvrX0+N/SuknSiUipbAvpui6JGcRDm0t7lrkfU/M21/WZT0prvzm/vvlnKewPqerNUsR69+OLVJVKNZT6zZeWvCPtXSnlKyE1fkiqfv7zBFnD6ytf17w985LWT5w9oXdWvaPS+UqrXZl26XptAABkNjdXuNnO6mFasM2P2SbhaPG8xfX1xq897u8jH1XMX9G2kver0U+7onfZhGtFchdJ82vPNkH3pk2bkrKAmwD49OnTyps3rw3CTbD8yCOPXPK5THd0M2VYgwYNbLA8fvx4hYWFJXUXNwG+mRbsyy+/tOtmmrAHH3zQZjA3ydHCw8M1YMAANWrUSCVKJHYnffLJJ9WqVSu9+eab9np++uknLViwwE4ZBsBLgoonTuGVUrcPpa9ukY4mG39d/ZbEBGsmm3m+YlL0vsSAPSXTyh0QJOUvk5iwzclkoO74Xwt6kcrSLeO98YyQQZw8e1Kzds3yuO37rd8TdAMAcAWK5i6qXlV7uZTdXPFmfbHxC7d5u28sf6NK5Stl/x2cK1h1itbhnns76DbduZ2Zvk2ga6bvql69elJytMthEqJFRUXZgN0E0KaruslMHhoaarebMhOEO/Xp00fHjx/Xhx9+aFvbTRK1du3a2QDbybRoT548WcOGDdMLL7ygChUq2PnAL3WObgDXkBnv/b9V0vYFicH1zt+kjT9JG35I7GJupgWr0lVyJHg+Pma/9OCixLHd+1clBusN+kkh1XiZslHQ7ZxHNKWjZ46m+fUAAJBVmaRoX3T+Qh+t/UhL9y9V7hy51a1iN/Wr2S+9Ly17zNOdnJlD20zRZVqcn332WU2fPt0Gwz/88IOdC9u0Kmd2zDEKeMGvr0mL33Ivb/6UtPyDxMzkKbV7QWpwv7R7aWKrd9mWid3Wka3c/OPNtitbSmZs2cD6l5/AMywmTDN2zNDxuONqXrK5WpRsYRPIGHtj9mrSlkn28Srlr2Tn9y6Rl8R8l4M6FACAqwy6d+7cqRMnTtgkZmaM9aBBg2zX7YoVK+q9995LaqXOzPjCAHjBO9dJJyI8t4absd9rvnLPQt64f2KgbhKnGflDpbsmSSGJvWuQPZi5Qp/87UmXFm8zluyWSrfYbOZBuYJ0a6VbVS/ENZln5KlIO3eoGafmNHf3XD23+Dmdc5yf4719mfZ6p/U72nJ0i+6fe79tXU/5q3/FAhW9/jyzCupQAACuMuju27ev7rnnHtut+0LTcGVmfGEAvGBEyPngObnAAtKg7dLS96Q1X/6Xvbxd4tjv73q771+oYmKX9Sz6+QPPth3dpu+2fKeIUxGqVrCafg37VZuPbnZJ8jKsyTDdcd0d2nt8r15c9qJWHVxlt1UvVF0vNX1J5fOXV/vv2+tYbIqM95JGtRmlaVunadkB98R/HUI72O24NNShQBo4slM6vF0qcp1UIPM3eAFZ3WXP023GYJvu5aVKlbLjqteuTWUeXgBIzgTSnpRvK/0zOTHLuWnJbjtM6jE+sUu5xw+h7dK+v7i32UylApU0tMlQjWk3xk53kjzgdk5nMvrv0bbL+MPzH04KuI0NURts2coDKz0G3MaivYv0Z8SfHrelVg4Aae5crPR9X+mDetK3t0sf1JGm90+cehNA1gm6Z8yYYefBfumll7R69Wo77Ve1atX0+uuv2/m6AcAjM+92YEH3LuRmjsifHktMsrZ7iTT7GWlyLynufBdfN8nnBEe2szJ8pcdyE3B/v+V729Kd0tHYo/o78u9UzxmYI1AFA1K8P/9TKKDQVVwtAFxDi0YmJiPVfx1VTSJSMwOI6S0GIOsE3YbJGv7QQw9p0aJF2rNnj+1y/tVXX9lx3QDgUdEq0qMrpDZDEruOmxbt2z6TNs1w33f7/MRM5Z6Y7uilmY0gOysQUCDVbXEJcaluy+GbQ+WDy6c6Z6npmu7J7ZVvv4KrBAAvWPNNKuWe51cGkImDbqezZ89q1apVWrlypW3lDgkJuXZXBiDrMfNyt3lOuv0zqfUzUtTW1Pc16Saq93AtM9OM3fiulDPQ65eKjOu2yrfJz8c9i33DYg1tUrTUmERrZmx26Xylk8py+eXS4IaDVatILfWr0U/3VL1H/r7+Sa3f99e4X3dXvdtLzwQALlPs8VTKY7iVQFaap9v47bff9O2332ratGmKj4/XLbfcopkzZ9rkagBwyfJdYCqmoOJS2yFSnXsS5/k2U4bV6ikVqsANzuaqFKyi11u8rrf+ektRZ6JsWZPiTTSy5UgVDiysHhV7aPr26S7HmKnBmhZvahOA/tzjZ62KWKWYuBgbqAfnCrb7+Pn6aXCjwepfu7/2n9hvg/N8/vnS5TkCgEeV2kubZnoo78gNA7JS9nKTQM0kU+vUqZPuvvtude3aVQEBAcpKyLwKpJH4c9KHDaSjKeZgzl1Iuv1zad+qxH9X7y4FJAZGgJOZQsxkNTdBs5lCzCnBkWDn4Z69a7bOJZzT9WWut13Ec/rl5OalAepQwItMxvLPukgnI8+X5SsutXhKOhEpFa4kVesu5cxa382BbBd0jx8/XrfffrsKFEh9TF1mxxcGwItMgrR/vpPC10oFykplW0rzX5L2/JetvERdycyrvHXW+WNMwN3re6kMY7lxeX7e+bOdCsxkLTet4f1q9rOt4fAe6lDAy05GSWu/lg5tlfKXkf6dJh3ecn57gXJSn1+k4PM/RgLIZEF3dsAXBsBLTh5O/IX+cLKx3Caj+X0zpTyFEzOZ718tfXev+7EFy0uP/8383LhkY9eO1cfrPnYpMy3iU26aktSlHNcedSiQhn55WvrrU/fyGrdJt03gpQCyQiI1ALgsS951DbiN00ekuc8nJlkzGcs3/uT52CM7pYh/uOFwcTb+rMasGaO237VV/a/q6/GFj2vHsR12vPZnGz5zu1tmrPbUrVO5iwCyhs2/pFL+c1pfCYBrnUgNAK7I1rmey3f9Lp09/V9Wcp/Uj/fhd0K4ennFy3b8ttOifYu07tA6vdL8FZ0+d9rj7fr38L/cRgBZg28quSr8EmdhAJAx8A0WQNrJlddzeY7A818catzieZ/ClaViNb13bch0Ik5G2DHbKR2NPWqzk/uk8gNOibwXyJoPABnBuThpyxzp3x+k00dT36/mramU33Zlj3smRlr7rfTn/yX2MANwTdDSDSDt1O4lha9zL6/RQ1o1Qdq1WMpTJHF+7g3JpnwyZbeM55WCi90xu22mck8OnT6k9qHtNX/PfJfyAL8A3XHdHdxJABlX2Eppyj3nM5SbH6a7vCnVv89931bPSuH/SDsWni8r3US6/qXLf1xTB0++O9mc3z5S68GJ03cCuCoE3QDSTqOHpMiN0pqvJGewVK61dGBd4i/rybuRtx+eOOWJmTKsyo3/dT0HzisXVE5+Pn6Kd8S73ZbKBSqrV5VeCvIP0i87f9GZ+DOqWrCqnmn4jEKDQrmNADJuC/d3vV2nBDNDZX4eIJVpIhW5znV//9xS7x+kfaulg/8m9goLbXrxx4ncnJhTpXidxHOYx53aL1nAbTik39+QKrRj9hDgKhF0A0g7vr7SzR9IrQZJEesTpzXZ+VtiIrXkTED+x0fSUxsk5lZGKkLyhKhHpR5uidEKBRSywfijCx+183Q/VOshdavYTUVzF+VeAsjYTJ144qB7uakX10+V2jyXmCRtx2+J02nW6ZUYiJeqn7gYsSek5WOkTTMlvxxSjVulJo8m1qcx4dL3faS9fyTua87R4VUpf2nXQD850/OMKTuBq0LQDSDtmXlFzWIsSKULnPnSEblJKl4rTS8NmcuwxsPsNGDTt023GcublWhmE6iNWj0qaR+TWG1l+Er9X8f/k4/PBRL1AUB6O3cm9W1nT0qT7pK2JUtKaoJrM/zKOYY7IUH6+hZp78rz+5hhXfv+knp+LU3rdz7gNs5ESzOflDqNTP1xUxnGA+DSkUgNQPoy3cc98pFyF0zji0Fm4+frpwdqPqBfbvlFS+5covtr3K/f9v7mtt/KiJVadmBZulwjAFwyM+QqZ27P2/zzugbchhleM+sZ6ex/wfq2ea4Bt5Np9d44U9rj6XPQIR1cn3p9XO1mXkDgKhF0A0hf9UxiGA+tjxWvT5y3G7gM/xxOfS530+INABlaYH7phrfdp8hs0E86usfzMWZs9v5Vif8+8Hfq53bu44kZy91jnHvA3+xxqWyLS758AJ7RvRxA+jIJX7q+n9jN3DktiknaYip/4DIVy13sirYBQIZR957EDOTrv5fOnpKuuyGxrvx5YOrHmFZwI/8FEkWWaZqYyPRUlPs2U+9W6iAN+FfaOD1xXHiljlJItWvwhAD4OBwOB7fBVUxMjIKDgxUdHa2goCBuD3Athf0h7f1TCiohVbkpMUO5YbrGmeRqeQpLBctxz3FF4hPi1WNGD+2K3uWWXG3WLbOUO7Vum7hmqEMBL9m3Svq0fWJ38OSKVpd6fpU4O0hQaWlKLylmv+s+ITWkh5ckBvI/9ncdp126sXTvjPP1MYBrjqDbA74wAF4Qf1b67j5pyy/ny4JKSvf+JBWuxC3HNRN+Ilyv/PGKlu9fLoccqle0noY1GaZKBXifpQXqUMCL/vw/af6LiS3ghpkizMwEYsZyO4Pxsq0kXz9p56LEv6al3HRZz/dfb58Da6U1Xye2eJdvI9XqScANeBlBtwd8YQC8YOV4afYz7uWhzaW+s7jluOaiY6PtlGGFAlNL1gdvoA4FvMxkHDe9xsx0Xzt/lxa97r5Pk8ekdkMTx4bnDOQlAdIZidQApI2NP3ouN5lUTxziVcA1F5wrmIAbQNZjgu3KnaQyTaR1kzzvs+5byT8PATeQQZBIDUD6i4+T1k2Rdi+W8hSR6vaWClVI76tCJnbo1CHN2zPPtnS3K9NOpfOVTu9LAoBrz9nNPKW4k9xtIAMh6AaQNqp19zw/aJlm0rQHpLDl58tWfCTd/oVU5QZeHVy2n3f+rBeWvWADbmPU6lEaWH+g7qtupqcDgCzEZBw347NTMi3hADIMupcDSBsN+iZmK08uqJRUtqVrwO1s+Z71jJQQz6uDy3LszDG9vPzlpIDbSHAk6N1V77plNAeATK/tUPdpwvIWk9q/kl5XBMADWroBpA2/nNKd30hhK6V9yaYM++5ez/vH7Euc/qRYTV4hXLLf9/2u2PhYt3KTxXzBngV6sNaD3E0AWYepSx9ZJv0zRTq4ITGbee07pcAC6X1lAJIh6AaQtso0TlyccuVLfd8LbQM88PHxuaJtAJBpmbqy4QPpfRUALoDu5QDSV527PZeHtpAKlE3rq0Em17pUawX4BbiV+8hHHUM7pss1AQCA7I2gG0D6qtBW6jBcypFsHtES9aRbxqfnVSETTxM2osUI5fLLlVTm5+OnIY2HqExQGZ2IO+Gx+zkAAIC3+DgcDofXzp5JxcTEKDg4WNHR0QoKCkrvywGyh9NHpX2rpDyFpRJ10/tqkMkdPXNUC8MW2oRqbUq3UdSZKL3555taE7lGOX1zqku5LhrcaLCC/PmMv9aoQwEAcEXQ7QFfGAAg6zh8+rBu/vFmHY877lLeuHhjfdrx03S7rqyKOhQAAFd0LwcAZGk/bv/RLeA2Voav1JYjW9LlmgAAQPZB0A0AyNL2Hd+X6rYDJw6k6bUAAIDshynDAACZztmEs5qza46WHVhmx2V3q9hN1QtV97hvjcI1NG3bNLdyk2CtaqGqaXC1AAAgOyPoBgBkuoD7kQWP2O7hTpM3T9YrzV5Rj0o97PZvN32rWbtm2URqZhqxcsHltCt6l8t5bq98u4rlKZYOzwAAAGQnBN0AgEzFtHAnD7gNhxx6+6+31blcZw1dOlTz98xP2rb16FbVLFxTfar30dL9S5U7Z251q9DNBt0AkKXt+FXaOEPy9ZNq3CqFNkvvKwKyJYJuAECmsuLACo/lx88e18wdM10Cbqf1h9froVoP6ekGT6fBFQKAF5hpNSP+kQqWl8q1lnx8Lrz/rGelP8edX//rU6n1YKnt87w8QHZLpPbxxx+rXLlyCggIUP369bVkyZJU9+3Tp498fHzclurVz4/j+/zzzz3uc+bMmTR6RgAAbwrKlfrc2hEnI1Ld9u/hf710RcCVcTgcWrrtsD5btkvLtx+264Cbs2ekb26XPr1e+vkp6ctu0vg20smo1G/WgbWuAbfT4relI65DbQBk8aB7ypQpGjBggIYOHao1a9aoZcuW6tKli8LCwjzu//777ys8PDxp2bt3rwoWLKjbb3ftIhgUFOSyn1lMUA8AyPxM13BfH/fqq2rBqmoQ0iDV40rmLenlKwMuXfTps+r+8XLdM2GlXpm5Ub0+XanbPlmh42fOchvhatloads817LwtdLcZC3WBzdK66dK4esS17cv8HwXHQnSjoXcYSA7Bd2jRo1Sv3799MADD6hq1aoaPXq0SpcurbFjx3rcPzg4WMWKFUtaVq1apaNHj6pv374u+5mW7eT7mQUAkDWYjOPDmw23WcudqhWqpvfavqcmJZqoUoFKbscUzV3UjvcGMoq3527Wur3HXMpW7zmqUfO3pts1IYMywbQnG6ZLZ09LU+6RxjaVpvWTxrWSvuoh+eVK/Xy5gr12qQAyWNAdFxen1atXq2PHji7lZn358uWXdI4JEyaoffv2Cg0NdSk/ceKELStVqpRuuukm24p+IbGxsYqJiXFZAAAZl5kibOHtCzWx00RN7TpVU26aYluyTQv4uPbj1L5MezslmI981KxEM03oOEGBOQLT+7KzJOrQK/PzP+GXVY5sLOFc6uVL3pU2zXRPnha1VcqZ2/2YgPxSlRu8c50AMl4itcOHDys+Pl4hISEu5WY9IiL1MXlOpsv47Nmz9e2337qUV6lSxY7rrlmzpg2eTZf05s2ba926dapUyb31wxg5cqReeeWVq3xGAIC0FJAjQA2LNXQrL5K7iG31Pn3utB0ja7KVw3uoQ69MfILn8duM64abqjdJy8e4l1/XRfrnO883bONP0h1fST/2l04eSizLV0K6baLkn4ebDKQxH0c6fbofOHBAJUuWtK3aTZs2TSp/7bXX9NVXX2nz5s0XreTfffddex5/f/9U90tISFC9evXUqlUrffDBB6n+Sm8WJxOsm27u0dHRdnw4AADwjDr04hISHJr8117NWLdfZ+Md6lQ9RFsjTmjq3/vc9r2nSRmN6F6TtxvOO30sMXmaGcftlL+MdN/P0oQO0omD7nfLdC9/IVI6FyeFrUicMqx0E8mPiYuA9JBu//MKFy4sPz8/t1btyMhIt9bvlMzvBBMnTlTv3r0vGHAbvr6+atiwobZt25bqPrly5bILAAC4PFm9Dl2954jGLtqhzRHHVaFIXj3curyaVSh8Wed4Zuo/mpYswDZjtxuWLaDKRfNqa+SJpPJqxYP0dIfr3I6fv/GgvvpjjyJjzqhh2YL2GkoVoAdHthGYX3rwV2nLLClifeKUYdW6SzkDpMqdpb+/8NwKbuTwl8q3TvNLBpBBgm4TLJspwubPn68ePXoklZv1bt26XfDY33//Xdu3b7dJ2C7GBOhr16613c0BAAAu1cqdUTa7uGmdNvYdPa0l2w5pwn0N1bZK0Us6x5aI4y4Bt9Nfu4/q03vr61yCtD3yuCqH5NP1VUPk5+s69/JXK3brhZ82JK2b4H/OhgjN/F8LFQtmZpZsw7RUV+2auCTXdqi0e6l0ZMf5sqBSUgeGTQIZSbr2MRk4cKBtrW7QoIHtYj5+/Hg7XVj//v3t9iFDhmj//v368ssv3RKoNW7cWDVq1HA7pxmb3aRJEzt+23QTN13KTdD90UcfpdnzAgAAmd+YX7cnBdxOZij26IXbbNBtAvAPf92uLQcTW8EfaV1B7au59tZbE3Y01fOv2xetpzualm3Ps6zEnUvQ6AXuPfUOHY+1c3sPuaHqFT83ZBH5QqT+S6V/p0mRG6XClaSat0u58l34uLiT0qrPEqcPyxUk1b1HqtQhra4ayHbSNeju2bOnoqKiNHz4cJsYzQTRs2bNSspGbspSztltxllPmzbNJkjz5NixY3rooYdst3UzxVjdunW1ePFiNWrUKE2eEwAAyBr+PRDtsXzD/mgbcN838U8bhDu7jD/41SqNvbueOtconrTvhVqjL9ZSvffoKUWdjPO4bW2K6caQjfnnlur1vvT9z56Rvugq7V99vmzjj9L1L0ktB3rlEoHsLt0SqWVkpoXcBOwkUgMAIPvWod0+XGpbo1MqXziPiuTLpZW7jrhtq14iSL880dIliVqH937XjkMnXfYrlMdfi55po3wBOVN9/JgzZ9VgxALb4p3SLfVKatQdda7gWSHDOh4hLftA2rVYyl1QatBXqn5+COY18/dX0oz/uZebaRWf3iQFFrj2jwlkc6QwBAAA8OChVhX02Ld/eygvrzfmbE51DLdhWsLnbTionH6+eq5LVX25YreWbj8s09RRs2SwGpcroI9+26EWFQurRaXzidlOxJ7T9DX7tTXiuCoWzatutUvo+9WuY8Jz+ProvqZltePQCY1ZuM2ODzc/AvRuEqpb65fitcyMTh1JzER+LFkPz12/S0d2XZvW54T4xHHhxp7lnvc5d1ra/7dU8fqrfzwALgi6AQAAPLixVnGdPltbY37dpj1Rp1Qyf6D6t6mgOxuVsYGw6VKekhnb/fz09fp25fngaaIZf92lit6/s66WbT+kZ6eu1/r9iS3on/y+QzfULKYxd9XTwZgzumPcCpuwzalYUC7dVr+UZq0P16m4eFUokkdDulRVwTz+6vrhUh07ddbut//YadvlPPJ4rB5pU4HXM7NZNcE14HZaMkpq9ODFx2inZuciaeGr0v5VUt4QqfHDUp4iqe9v9gFwzRF0AwAApMIEvGY5czZeATn/aymU1L91BT301Srbcp1clxrFbKK1lN6eu0U31y6h12dt1umz8S7bZq2PUKfqB/T71kMuAbcRERNrH/vvFzroZOw5FcqbOD3bKzM3JAXcyY1dtF19m5d1uVZkAqaF2ZO449KhrYnjttd8LZ08LJVrJdW8Tcrx31R9sSekf6YkJlIrVEmqfWfiNGMH1khf3yYl/Pc+MfN5LxwuNegn+flL8SnyBZRuLBVzT1IM4OoRdAMAAFxEyiC2Q7UQmzTtg4XbtfXgcZUvkkePtqmonYfOz7ud3LkEhyb9Gabw6DMet5u5uJdtP+xx26+bI+3jJ7+GjQdiPO4bc+acDdxN13RkIsGlPZf7+EoR/0izBkkJ5xLL/pks/f2ldO9P0ukj0mddpKO7zx+zbLTU5xfpj7HnA+7k1n8v3TpRmjdUOrbHPIhUoZ3UfayXnhwAgm4AAIBUmFZm0z189voImSm0TZfzPs3KyT+Hr81SnjxTuTHu92TzJacQFJh60jRzvtz+OXTUQ+t1bv/EYPvoyTjbfbxs4dwqWyiPx0RuATl9FRL0XwsoMo8G90urP5fiY13Lq3WTFo08H3A77f1DWvtNYmt28oDbOB4uLXhJign3/FixMVLxWtITa6WobYld14NKXOMnBCA5gm4AAAAPzAQv/b74S8u2RyWVmWzmf+46qk/va2DXTev0d6v2Kub0WbWuXESdaxTTu/O2Ki4+wS1b+T1NQvX9qn12Xu+UutcpaceMm7nBUzLd0gd9v04/rd1v5w0347nvaljaBuopM5v3ahR6wYzoyKCKVpF6TZbmDpMiN0g5AqRaPRO7im+Y7vmY7QsTx2p7snWuVKeX5+25C0v5iku+vlIRM088AG8j6AYAAPDABNvJA26nBZsO6u+wo/p7z1GN+GVTUvlvWw5p5j/hGtWztob9+G/SmOviwQH6sFc92z18TK+66vvZXzbxmeHn66NH21RQq8pF1Lh8QZuR3IzxdmpfNUQn4+I1NVkG8yMn4/TRoh16tlNlzd8UqTVhx2wgboL6J6+vxGuZWZku3o8uTxy37Z9HyhkoRaXec8KO286Z2/M2c3zT/0nrpyWOC0+u5dNSDv9re+0ALoigGwAAwIN1+46lel/+2BHlsVXaZDS/q1EZ/THketv9O6efjxqVLagcfr52e+WQfPr9mTZasu2wDZ6bViikEvkD7bZcOfz08d31tT3yuLYdPKEKRfOqVIFA1R0+3+M1mKnCpj/aXGfjE+zUZMgi8pyfQk6FKkihzaU9y9z3q3O3lD9UWvS6+7bad0mFK0n95kq/vyXt/VMKKi41eliq3dO71w/ADUE3AACAB6a7d2pizyW4ZSF3WrEjymY8N93NPTEBeNsqRVM9d0hQgHx8fGzAbVrLzWN5cuhE4vhfAu4s7tZPpSm9z3cV988rXf+iVLa5VKqhdHiL9O+08/tX7iy1eyHx3yHVpTu+SJ/rBpCEoBsAAMADMz675NzApK7gTuUK59H1VYrqfQ9TgxmF8/lr9+GTmrfRJF/zscnXigenHsA7mRbrET9v1OS/9tpAu0DunPpf24r28XYdPum2f+NyhXjdsgOT5OzBhVLEv9LJQ1KpBufn7TbdxG+bKLV5PnEsuJkyLKRael8xgBR8HCZLCFzExMQoODhY0dHRCgoK4u4AAJBN61ATPL/w079auv2wmVhJba4rqle717Ct4D0+XmbHUydnupPf36Kcxi/emTSHdw5fH71xay3b+p2Q4ND/LdlpA2vTvbxFxcIa2LGyKhTJqzfnbNbYRe5jeB9qWU4Tl+220445mXHiPz7W3LaKAwAyNoLubPCFAQCAtJJV69DjZ87aLt95c53vJHgw5owGTF6rFTsTk62VCA7QI20q6MUZG5ICbieTaXzFc+3sOPDPl+92y2z+8+Mt1Gn0YjvPdkqNyhXUsBur6qsVe+w833XL5Ne9TcuqSD6mBgOAzIDu5QAAABfhaRou08o86aEm2nf0lI6fOWeTpI1bvMMt4DbM1F4z1h7QtyvD3LZFnYzTFyv2eAy4jUPHY1WrVH69fXt+XicAyIQIugEAAK5CqQLnp20yY7hTE3Uy1m3+7uTd2KsVD9LG8Bi3bQ3LFuD1AYBMjPklAAAArpEbaxaXr4e4OyCnr3rULWXHd3tSoWgePdelih0TnpyZf/vRNhV5fQAgEyPoBgAAuEZKF8xtE60lD65z5fDVqDvq2Hm3b29Q2u2Y4MCcurtxqFpVLmLn3b69fik7jvuBFuU08/EWKls4D68PAGRiJFLLRklgAADwNurQRJExZ7RgU6QNvjtUC1GBPP62/Fx8gj78bbsm/7lXR08lZi8f1Ok6VS3O9w0AyKoIuj3gCwMAAFeGOhQAAFd0LwcAAAAAwEsIugEAAAAA8BKmDAMAAEhD8zce1OQ/w+yY7uYVC+v+5uWSxnwDALIegm4AAIA0Mn7xDr0+a3PS+t9hx/TL+nD9+FhzBQXk5HUAgCyI7uUAAABp4ETsOX2wcLtb+c5DJ/XdX3t5DQAgiyLoBgAASANbImJs4O3Jqt1HeQ0AIIsi6AYAAEgDRfMFyMfH87ZiwQG8BgCQRRF0AwAApIHSBXPr+ipF3cr9/XzVq3EZXgMAyKIIugEAANLIqJ51dHPtEsrhm9jkXb5wHo3rXV+VQ/LxGgBAFuXjcDgc6X0RGU1MTIyCg4MVHR2toKCg9L4cAAAyDerQSxN96qyOx55VyfyB8kmtzzkAIEtgyjAAAIA0Fpw7p10AAFkf3csBAAAAAPASgm4AAAAAALyEoBsAAAAAAC8h6AYAAAAAwEsIugEAAAAA8BKCbgAAAAAAvISgGwAAAAAALyHoBgAAAADASwi6AQAAAADIqkH3xx9/rHLlyikgIED169fXkiVLUt23T58+8vHxcVuqV6/ust+0adNUrVo15cqVy/6dPn16GjwTAAAAAAAyUNA9ZcoUDRgwQEOHDtWaNWvUsmVLdenSRWFhYR73f//99xUeHp607N27VwULFtTtt9+etM+KFSvUs2dP9e7dW+vWrbN/77jjDq1cuTINnxkAAAAAAJKPw+FwpNeNaNy4serVq6exY8cmlVWtWlXdu3fXyJEjL3r8jz/+qFtuuUW7du1SaGioLTMBd0xMjGbPnp20X+fOnVWgQAFNmjTpkq7LHB8cHKzo6GgFBQVd0XMDACA7og4FACCDtHTHxcVp9erV6tixo0u5WV++fPklnWPChAlq3759UsDtbOlOec5OnTpd8jkBAAAAALhWciidHD58WPHx8QoJCXEpN+sREREXPd50Lzet2d9++61LuTn2cs8ZGxtrl+S/0gMAgIujDgUAIIMnUjOJ0JIzvd1Tlnny+eefK3/+/LYr+tWe03RlN93JnUvp0qUv6zkAAJBdUYcCAJBBg+7ChQvLz8/PrQU6MjLSraU6JRNET5w40SZJ8/f3d9lWrFixyz7nkCFD7Pht52IStAEAgIujDgUAIIMG3SZYNlOEzZ8/36XcrDdr1uyCx/7+++/avn27+vXr57atadOmbuecN2/eBc9pphYzCdOSLwAA4OKoQwEAyKBjuo2BAwfa1uoGDRrYYHn8+PF2urD+/fsn/Xq+f/9+ffnll24J1Ezm8xo1arid88knn1SrVq305ptvqlu3bvrpp5+0YMECLV26NM2eFwAAAAAA6R50m+m9oqKiNHz4cJsYzQTRs2bNSspGbspSztltun9PmzbNztntiWnRnjx5soYNG6YXXnhBFSpUsPOBmyAdAAAAAIBsM093RsUcowAAUIcCAJAlspcDAAAAAJBVEXQDAAAAAOAlBN0AAAAAAHgJQTcAAAAAAF5C0A0AAAAAgJcQdAMAAAAA4CUE3QAAAAAAeAlBNwAAAAAAXkLQDQAAAACAlxB0AwAAAADgJQTdAAAAAAB4CUE3AAAAAABeQtANAAAAAICXEHQDAAAAAOAlBN0AAAAAAHgJQTcAAAAAAF5C0A0AAAAAgJcQdAMAAAAA4CUE3QAAAAAAeAlBNwAAAAAAXkLQDQAAAACAlxB0AwAAAADgJQTdAAAAAAB4CUE3AAAAAABeQtANAAAAAICXEHQDAAAAAOAlBN0AAAAAAHgJQTcAAAAAAF5C0A0AAAAAgJcQdAMAAAAA4CUE3QAAAAAAeAlBNwAAAAAAXkLQDQAAAACAlxB0AwAAAADgJQTdAAAAAAB4CUE3AAAAAABeQtANAAAAAICXEHQDAAAAAOAlBN0AAAAAAGTVoPvjjz9WuXLlFBAQoPr162vJkiUX3D82NlZDhw5VaGiocuXKpQoVKmjixIlJ2z///HP5+Pi4LWfOnEmDZwMAAAAAwHk5lI6mTJmiAQMG2MC7efPmGjdunLp06aKNGzeqTJkyHo+54447dPDgQU2YMEEVK1ZUZGSkzp0757JPUFCQtmzZ4lJmgnoAAAAAALJN0D1q1Cj169dPDzzwgF0fPXq05s6dq7Fjx2rkyJFu+8+ZM0e///67du7cqYIFC9qysmXLuu1nWraLFSuWBs8AAAAAAIAM2L08Li5Oq1evVseOHV3Kzfry5cs9HjNjxgw1aNBAb731lkqWLKnKlStr0KBBOn36tMt+J06csN3PS5UqpZtuuklr1qy5aJf1mJgYlwUAAFwcdSgAABk06D58+LDi4+MVEhLiUm7WIyIiPB5jWriXLl2qf//9V9OnT7ct41OnTtVjjz2WtE+VKlXsuG4ToE+aNMl2Kzdd17dt25bqtZhW9eDg4KSldOnS1/CZAgCQdVGHAgBwYT4Oh8OhdHDgwAHbWm1atZs2bZpU/tprr+mrr77S5s2b3Y4xreAm0ZoJyk1wbPzwww+67bbbdPLkSQUGBrodk5CQoHr16qlVq1b64IMPUv2V3ixOpqXbBN7R0dF2fDgAAPCMOhQAgAw6prtw4cLy8/Nza9U2idFStn47FS9e3AbqzoDbqFq1qszvBvv27VOlSpXcjvH19VXDhg0v2NJtsqCbBQAAXB7qUAAAMmj3cn9/fztF2Pz5813KzXqzZs08HmO6iZsWcjNm22nr1q02sDbjtz0xAfnatWttwA4AAAAAQLaZp3vgwIH69NNP7TzbmzZt0lNPPaWwsDD179/fbh8yZIjuvffepP179eqlQoUKqW/fvnZascWLF+uZZ57R/fffn9S1/JVXXrEZ0M34bxNsm+zo5q/znAAAAAAAZIspw3r27KmoqCgNHz5c4eHhqlGjhmbNmmUzjxumzAThTnnz5rUt4Y8//rjNYm4CcDNv94gRI5L2OXbsmB566KGkcd9169a1wXmjRo3S5TkCAAAAALKvdEuklpGZRGomYCeRGgAA1KEAAGTa7uUAAAAAAGRlBN0AAAAAAHgJQTcAAAAAAF5C0A0AAAAAgJcQdAMAAAAA4CUE3QAAAAAAeAlBNwAAAAAAXkLQDQAAAACAlxB0AwAAAADgJQTdAAAAAAB4CUE3AAAAAABeQtANAAAAAICXEHQDAAAAAOAlBN0AAAAAAHgJQTcAAAAAAF5C0A0AAAAAgJcQdAMAAAAA4CUE3QAAAAAAeAlBNwAAAAAAXkLQDQAAAACAlxB0AwAAAADgJQTdAAAAAAB4CUE3AAAAAABeQtANAAAAAICXEHQDAAAAAOAlBN0AAAAAAHgJQTcAAAAAAF5C0A0AAAAAgJcQdAMAAAAA4CUE3QAAAAAAeAlBNwAAAAAAXkLQDQAAAACAlxB0AwAAAADgJQTdAAAAAAB4CUE3AAAAAABeQtANAAAAAICXEHQDAAAAAOAlBN0AAAAAAGTVoPvjjz9WuXLlFBAQoPr162vJkiUX3D82NlZDhw5VaGiocuXKpQoVKmjixIku+0ybNk3VqlWz283f6dOne/lZAAAAAACQwYLuKVOmaMCAATaIXrNmjVq2bKkuXbooLCws1WPuuOMOLVy4UBMmTNCWLVs0adIkValSJWn7ihUr1LNnT/Xu3Vvr1q2zf80xK1euTKNnBQAAAABAIh+Hw+FQOmncuLHq1aunsWPHJpVVrVpV3bt318iRI932nzNnju68807t3LlTBQsW9HhOE3DHxMRo9uzZSWWdO3dWgQIFbIB+KczxwcHBio6OVlBQ0BU9NwAAsiPqUAAAMkhLd1xcnFavXq2OHTu6lJv15cuXezxmxowZatCggd566y2VLFlSlStX1qBBg3T69GmXlu6U5+zUqVOq5wQAAAAAwFtyKJ0cPnxY8fHxCgkJcSk36xERER6PMS3cS5cuteO/zThtc45HH31UR44cSRrXbY69nHM6x4mbJfmv9AAA4OKoQwEAyOCJ1Hx8fFzWTW/3lGVOCQkJdts333yjRo0a6YYbbtCoUaP0+eefu7R2X845DdOV3XQndy6lS5e+6ucFAEB2QB0KAEAGDboLFy4sPz8/txboyMhIt5Zqp+LFi9tu5SYwTj4G3ATV+/bts+vFihW7rHMaQ4YMseO3ncvevXuv8tkBAJA9UIcCAJBBg25/f387Rdj8+fNdys16s2bNPB7TvHlzHThwQCdOnEgq27p1q3x9fVWqVCm73rRpU7dzzps3L9VzGmZqMZMwLfkCAAAujjoUAIAM3L184MCB+vTTT+147E2bNumpp56y04X1798/6dfze++9N2n/Xr16qVChQurbt682btyoxYsX65lnntH999+vwMBAu8+TTz5pg+w333xTmzdvtn8XLFhgpyYDAAAAACBbJFJzTu8VFRWl4cOHKzw8XDVq1NCsWbMUGhpqt5uy5HN2582b17ZiP/744zaLuQnAzRzcI0aMSNrHtGhPnjxZw4YN0wsvvKAKFSrY+cDN9GQAAAAAAGSbebozKuYYBQCAOhQAgCyRvRwAAAAAgKyKoBsAAAAAAC8h6AYAAAAAICsmUsuonMPczdhuAACyg3z58snHx+eqz0MdCgDIbvJdpA4l6Pbg+PHj9m/p0qW998oAAJCBREdHKygo6KrPQx0KAMhuoi9Sh5K93IOEhAQdOHDgmv3qn1WZngDmh4m9e/deky9qAO8pXEu8ny7PtarzqEMvDe9PXGu8p8D7Kf3Q0n0FfH19VapUqWv/amRRJuAm6AbvKWRUfEalLerQy8P7E9ca7ynwfsp4SKQGAAAAAICXEHQDAAAAAOAlBN24Yrly5dJLL71k/wLXAu8pXEu8n5CR8f4E7ylkZHxGXVskUgMAAAAAwEto6QYAAAAAwEsIugEAAAAA8BKC7iyqTZs2GjBgQHpfBgAAmQ51KADgWiLoBgBkWH369JGPj4/b0q5dOxUuXFgjRozweNzIkSPt9ri4uEt6nN9++0033HCDChUqpNy5c6tatWp6+umntX///mv8jAAASBvUoRkHQTcAIEPr3LmzwsPDXZZp06bpnnvu0eeffy6Hw+F2zGeffabevXvL39//oucfN26c2rdvr2LFitnzbty4UZ988omio6P17rvveulZAQDgfdShGQNBdzYxZ84cBQcH68svv7S/enXv3l2vv/66QkJClD9/fr3yyis6d+6cnnnmGRUsWFClSpXSxIkTXc5hWnx69uypAgUK2Nagbt26affu3Unb//rrL3Xo0MG2LpnHat26tf7++2+Xc5gWqk8//VQ9evSwrUmVKlXSjBkzkrYfPXpUd999t4oUKaLAwEC73Xx5RsZWtmxZjR492qWsTp06evnll5NedxPY3HTTTfZ1r1q1qlasWKHt27fbbpx58uRR06ZNtWPHjqTjzb/Ne8y8R/PmzauGDRtqwYIFbo/76quvqlevXnafEiVKaMyYMWn0rJGW05aYgDj5Yj6H+vXrZ98nixcvdtl/yZIl2rZtm92ekJCg4cOH2880cx7zvjSfh0779u3TE088YRfzmWfej+Z91apVK/tZ9eKLL/JCgzoUXkUdCm+iDs0YCLqzgcmTJ+uOO+6wAfe9995ry3799VcdOHDAflkdNWqUDY5MQGS+yK5cuVL9+/e3y969e+3+p06dUtu2bW1gY45ZunSp/bf59czZffP48eO677777BfeP/74wwbMprumKU/OBPjmev755x+73QTZR44csdteeOEF28o0e/Zsbdq0SWPHjrVBPDI/Exyb99/atWtVpUoVGyg//PDDGjJkiFatWmX3+d///pe0/4kTJ+z7wwTaa9asUadOndS1a1eFhYW5nPftt99WrVq17A885lxPPfWU5s+fn+bPD2mvZs2a9seYlD/MmeC5UaNGqlGjht5//33bWv3OO+/YzxzzPrr55pttUG58//339jPs2Wef9fgY5kdJZG/UocgIqENxrVGHpjEHsqTWrVs7nnzyScdHH33kCA4Odvz6669J2+677z5HaGioIz4+Pqnsuuuuc7Rs2TJp/dy5c448efI4Jk2aZNcnTJhg90lISEjaJzY21hEYGOiYO3eux2sw58iXL59j5syZSWXmLTds2LCk9RMnTjh8fHwcs2fPtutdu3Z19O3b95rdB6QN83567733XMpq167teOmllzy+7itWrLBl5n3lZN5rAQEBF3ycatWqOcaMGePyuJ07d3bZp2fPno4uXbpc9XNCxmA+r/z8/OznUfJl+PDhdvvYsWPt+vHjx+26+WvWx40bZ9dLlCjheO2111zO2bBhQ8ejjz5q//3II484goKC0vx5IWOjDkVaog6Ft1CHZhy0dGdhZmyiyWA+b94820qdXPXq1eXre/7lN114zS9eTn5+frYLeWRkpF1fvXq17QqcL18+28JtFtMN/cyZM0ldgs2+pnW8cuXKtnu5WUxrZcqWSdMq6WS6FZtzOh/nkUcesa0KpguoaXlavny5l+4O0lry192834zk7zlTZt5PMTExdv3kyZP2PWASWpnWRvOe27x5s9v7yXRLT7luekkg6zCfX6aHRPLlscces9vuuusu24V8ypQpdt38Nb/z3Hnnnfa9ZHr0NG/e3OV8Zt35HjH7muEPQErUochIqENxpahDM4Yc6X0B8B4TuJout6brpemCmfyLZc6cOV32Nds8lZkvs4b5W79+fX3zzTduj2PGXxtmrPihQ4fs2N7Q0FA7hsQEQCmzB1/ocbp06aI9e/bol19+sd2Kr7/+evvl2nQNRcZlfsBJmczq7Nmzqb7uzveipzLne8HkF5g7d6597StWrGjH+N92222XlI2aICprMT/OmfeAJ+bHPfO+MJ9zZgy3+WvWg4KCkn7ASfl+SB5omx8JTcI0k5ytePHiafBskFlQhyKtUIfCm6hDMwZaurOwChUq2GlwfvrpJz3++ONXda569erZMZBFixa1X36TL+ZLr2HGcptkRGYcrmlJN0H34cOHL/uxTBBvAvivv/7aBvDjx4+/qmuH95nXzAQtTibY2bVr11Wd07yfzPvAJN0zLeImeVbyxH1OJn9AynUzZhzZhwm2ly1bpp9//tn+NeuGCbxNcj2TgyI504PGJPMzTIBuMpy/9dZbHs997NixNHgGyIioQ5FWqEORnqhD0wYt3VmcacUxgbfJyJsjRw63DNOXyiQ7MwmrTDZpZyZg0833hx9+sC2SZt0E4F999ZUaNGhggy5TblonL4fJFGxa1E3QHhsba79EO78cI+MycyabqZtMojOTjM8kxDNDFK6GeT+Z95c5p2mVNOd0toInZ4IsEzCZjPwmgZpJjGV6SiDrMJ8FERERLmXm88yZZNHMlGDeLyZRn/lrMo87mc+hl156yQZQpuXStISb7unOXjulS5fWe++9Z5P4mc8tcw6TSdhkNTfJJ82wBqYNy76oQ5EWqEPhTdShGQNBdzZw3XXX2WzlJvC+0kDITPNkspYPHjxYt9xyi81IXrJkSdv927QmOTMGP/TQQ6pbt67KlCljpyQbNGjQZT2OaXEyGahNi6YJ2Fu2bGnHeCNjM6/Zzp07bQZ80/PBZFm92pZuEwjdf//9atasmQ2uzHvP2V04uaefftrmHDBZ8U1+ABMgmQzVyDrMFF8pu36bzzUzxt/JvFeef/55G2QnZ3rfmPeNeZ+Y3BEmR4CZptDMruD06KOP2uDKDGUwPStOnz5tA2/zfh44cGAaPENkZNSh8DbqUHgTdWjG4GOyqaX3RQDAlTCBkUkWaBYAAEAdCmREjOkGAAAAAMBLCLoBAAAAAPASupcDAAAAAOAltHQDAAAAAOAlBN0ALshkvb/cRGVmiq8ff/zR/ttkojfrZpomAACyC+pPAE4E3QAAAAAAeAlBNwAAAAAAXkLQDeCiEhIS9Oyzz6pgwYIqVqyYXn755aRt27ZtU6tWrRQQEKBq1app/vz5Hs+xefNmNWvWzO5XvXp1LVq0KGnb0aNHdffdd6tIkSIKDAxUpUqV9NlnnyVt37dvn+688077+Hny5FGDBg20cuVKu23Hjh3q1q2bQkJClDdvXjVs2FALFixwm8/79ddf1/333698+fKpTJkyGj9+PK88AMCrqD8BGATdAC7qiy++sMGuCXTfeustDR8+3AbX5svELbfcIj8/P/3xxx/65JNPNHjwYI/neOaZZ/T0009rzZo1Nvi++eabFRUVZbe98MIL2rhxo2bPnq1NmzZp7NixKly4sN124sQJtW7dWgcOHNCMGTO0bt06+wOAeWzn9htuuMEG2ubcnTp1UteuXRUWFuby+O+++64N1s0+jz76qB555BH7QwAAAN5C/QnAcgDABbRu3drRokULl7KGDRs6Bg8e7Jg7d67Dz8/PsXfv3qRts2fPdpiPlunTp9v1Xbt22fU33ngjaZ+zZ886SpUq5XjzzTfteteuXR19+/b1+Pjjxo1z5MuXzxEVFXXJr1O1atUcY8aMSVoPDQ113HPPPUnrCQkJjqJFizrGjh3Law8A8ArqTwBOtHQDuKhatWq5rBcvXlyRkZG2Vdp01S5VqlTStqZNm3o8R/LyHDly2FZnc7xhWp0nT56sOnXq2Fbs5cuXJ+1rsp7XrVvXdi335OTJk/YY07U9f/78tou5acFO2dKd/DmYbOqmm7x5DgAAeAv1JwCDoBvAReXMmdNl3QStpnu3w2EaseW27VI59+3SpYv27NljpyYz3civv/56DRo0yG4zY7wvxHRbnzZtml577TUtWbLEBuk1a9ZUXFzcJT0HAAC8hfoTgEHQDeCKmdZl06JsAmWnFStWeNzXjPl2OnfunFavXq0qVaoklZkkan369NHXX3+t0aNHJyU6M60EJpA+cuSIx/OaQNsc16NHDxtsmxZsMzc4AAAZFfUnkL0QdAO4Yu3bt9d1112ne++91yY4MwHw0KFDPe770Ucfafr06bbr92OPPWYzlpts4saLL76on376Sdu3b9eGDRv0888/q2rVqnbbXXfdZQPp7t27a9myZdq5c6dt2XYG9xUrVtQPP/xgA3NzDb169aIFGwCQoVF/AtkLQTeAK/8A8fW1gXRsbKwaNWqkBx54wHbz9uSNN97Qm2++qdq1a9vg3ATZzgzl/v7+GjJkiG3VNtOPmWzoZoy3c9u8efNUtGhRm6XctGabc5l9jPfee08FChSwGdFN1nKTvbxevXq8qgCADIv6E8hefEw2tfS+CAAAAAAAsiJaugEAAAAA8BKCbgAAAAAAvISgGwAAAAAALyHoBgAAAADASwi6AQAAAADwEoJuAAAAAAC8hKAbAAAAAAAvIegGAAAAAMBLCLoBXLLdu3fLx8dHa9euzTCP1aZNGw0YMMDr1wMAwNWgDgWyL4JuABlS6dKlFR4erho1atj1RYsW2SD82LFj6X1pAABkaNShQMaSI70vAABSiouLk7+/v4oVK8bNAQDgMlCHAhkPLd0AXMyZM0ctWrRQ/vz5VahQId10003asWNHqndpxowZqlSpkgIDA9W2bVt98cUXbi3S06ZNU/Xq1ZUrVy6VLVtW7777rss5TNmIESPUp08fBQcH68EHH3Tphmf+bc5tFChQwJabfZ0SEhL07LPPqmDBgjZQf/nll13Ob/YfN26cfS65c+dW1apVtWLFCm3fvt12T8+TJ4+aNm16wecJAMDFUIcC8MgBAMlMnTrVMW3aNMfWrVsda9ascXTt2tVRs2ZNR3x8vGPXrl0O87Fhyg2znjNnTsegQYMcmzdvdkyaNMlRsmRJu8/Ro0ftPqtWrXL4+vo6hg8f7tiyZYvjs88+cwQGBtq/TqGhoY6goCDH22+/7di2bZtdkj/WuXPn7DWZdXOO8PBwx7Fjx+yxrVu3tse+/PLL9pq/+OILh4+Pj2PevHlJ5zfHmeuaMmWKPb579+6OsmXLOtq1a+eYM2eOY+PGjY4mTZo4OnfuzHsBAHDFqEMBeELQDeCCIiMjbdC6fv16t6B78ODBjho1arjsP3ToUJegu1evXo4OHTq47PPMM884qlWr5hJ0m0A4uZSP9dtvv7mc18kE3S1atHApa9iwob22pA86yTFs2LCk9RUrVtiyCRMmJJWZHwwCAgJ4NwAArhnqUAAG3csBuDBdrHv16qXy5csrKChI5cqVs+VhYWFud2rLli1q2LChS1mjRo1c1jdt2qTmzZu7lJn1bdu2KT4+PqmsQYMGV/xK1KpVy2W9ePHiioyMTHWfkJAQ+7dmzZouZWfOnFFMTMwVXwcAIHujDqUOBTwhkRoAF127drVZT//v//5PJUqUsOOlTQZxk5glJdOIbMZLpyy73H0MM676SuXMmdNl3Tyeue7U9nFej6eylMcBAHCpqEOpQwFPCLoBJImKirIt0ybpWMuWLW3Z0qVLU71DVapU0axZs1zKVq1a5bJerVo1t3MsX75clStXlp+f3yXffZPN3EjeOg4AQEZBHQogNXQvB5DEZAY3GcvHjx9vM3v/+uuvGjhwYKp36OGHH9bmzZs1ePBgbd26Vd99950+//xzl5bjp59+WgsXLtSrr75q9zHZzT/88EMNGjTosu58aGioPefPP/+sQ4cO6cSJE7xyAIAMgzoUQGoIugGc/0Dw9dXkyZO1evVq26X8qaee0ttvv53qHTLjvadOnaoffvjBjpkeO3ashg4dareZ6cGMevXq2WDcnNec88UXX9Tw4cNdpvy6FCVLltQrr7yi5557zo6//t///scrBwDIMKhDAaTG57/MvgBwTbz22mv65JNPtHfvXu4oAADUoUC2x5huAFfl448/thnMTbf0ZcuW2ZZxWqEBAKAOBZCIoBvAVTFTf40YMUJHjhxRmTJl7BjuIUOGcFcBAKAOBUD3cgAAAAAAvIdEagAAAAAAeAlBNwAAAAAAXkLQDQAAAACAlxB0AwAAAADgJQTdAAAAAAB4CUE3AAAAAABeQtANAAAAAICXEHQDAAAAAOAlBN0AAAAAAMg7/h/DTwIGfgoUKwAAAABJRU5ErkJggg==\",\n      \"text/plain\": [\n       \"<Figure size 1000x1000 with 4 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    }\n   ],\n   \"source\": [\n    \"sns.catplot(\\n\",\n    \"    cifar_results[cifar_results.measure != \\\"Elapsed time\\\"], \\n\",\n    \"    x=\\\"algorithm\\\", \\n\",\n    \"    y=\\\"value\\\", \\n\",\n    \"    hue=\\\"algorithm\\\", \\n\",\n    \"    col=\\\"measure\\\", \\n\",\n    \"    kind=\\\"swarm\\\", \\n\",\n    \"    col_wrap=2,\\n\",\n    \"    height=5,\\n\",\n    \")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"02e215d9-75bf-4395-9dfd-08b36606eff8\",\n   \"metadata\": {},\n   \"source\": [\n    \"Here we seem some of KMeans weakness -- it can run very fast, but the quality of the clusters is not always that great, particularly for very high dimensional data such as that produced by embedding models. The one upside of KMeans is that it does cluster all of your data, so the \\\"proportion clustered\\\" is perfect, and this significantly improves the clustering score since the other algoirithms only clustered around 80% of the data (but found much cleaner more accurate clusters by doing so). In contrast UMAP + HDBSCAN was much slower than KMeans, and we hope it managed to produce better clusters by doing so. And that is largely born out here. The ARI and AMI for UMAP + HDBSCAN are significantly better than KMeans, so we produced better clusters by expending more compute. Of course this did require not clustering 20% of the data -- but that 20% is likely the \\\"hard to classify\\\" samples that would simply make the ARI and AMI a lot worse if we tried to force them to be assigned somewhere. That means that on clustering score UMAP + HDBSCAN comes out a little ahead of KMeans. Last we have EVoC, which ran faster than KMeans and yet somehow also produced better clusters than UMAP + HDBSCAN. EVoC's edge in clustering quality over UMAP + HDBSCAN is not huge here, but it is relevant. The real strength of EVoC on this dataset is the raw speed with which it can produce those good results.\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"92b4f389-c9b3-4c57-ac92-c2be35f12133\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Text embeddings\\n\",\n    \"\\n\",\n    \"For a text dataset we'll use the venerable 20-newsgroups dataset. The 20-newsgroups dataset is a dataset of NNTP newsgroup posts from the 1990s to twenty different newsgroup sections (think subreddits if you never used NNTP). As with the image dataset we have computed text embeddings ahead of time and put the dataset on Huggingface datasets for ease of access. This should be a challenging dataset to get good matches with the class labels on -- while the newsgroups are mostly distinct in topic, posts can easily run off-topic, be very short, or have more text from signature blocks that weren't properly stripped than actual content. That means the data can be very noisy, with a lot of posts that can be very hard to cluster with other posts from the same newsgroup. To make matters worse there are some overarching categories of newsgroups (there are multiple tech/computing newsgroups included, and multiple discussion groups for religion and politics that can tend to have overlaps). Natural clustering may not line up well with the full set of twenty distinct labels provided. \"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 12,\n   \"id\": \"71bad192-181f-4586-8241-d2674a5101ce\",\n   \"metadata\": {\n    \"execution\": {\n     \"iopub.execute_input\": \"2026-03-25T20:42:56.875997Z\",\n     \"iopub.status.busy\": \"2026-03-25T20:42:56.875696Z\",\n     \"iopub.status.idle\": \"2026-03-25T20:43:02.962330Z\",\n     \"shell.execute_reply\": \"2026-03-25T20:43:02.961712Z\",\n     \"shell.execute_reply.started\": \"2026-03-25T20:42:56.875982Z\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"ds_news = load_dataset(\\\"lmcinnes/evoc_bench_20newsgroups\\\")\\n\",\n    \"news_data = np.asarray(ds_news[\\\"train\\\"][\\\"embedding\\\"])\\n\",\n    \"news_target = np.asarray(ds_news[\\\"train\\\"][\\\"target\\\"])\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"22404769-36a7-4aff-bc63-49e1262daed1\",\n   \"metadata\": {},\n   \"source\": [\n    \"Okay, let's run the benchmarks. Due to the noisiness careful parameter selection was required. EVoC will just make use of it's \\\"pick the best layer\\\" approach; KMeans again benefits from asking for more clusters than classes to help force it to break up some of the meta-categories to better match with the class labels. UMAP + HDBSCAN required careful tuning of ``min_cluster_size`` to get good results.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 13,\n   \"id\": \"7d5837d7-97b5-4c25-b596-1fb6b6e5a7d0\",\n   \"metadata\": {\n    \"execution\": {\n     \"iopub.execute_input\": \"2026-03-25T20:43:02.963427Z\",\n     \"iopub.status.busy\": \"2026-03-25T20:43:02.963202Z\",\n     \"iopub.status.idle\": \"2026-03-25T20:45:35.272333Z\",\n     \"shell.execute_reply\": \"2026-03-25T20:45:35.271486Z\",\n     \"shell.execute_reply.started\": \"2026-03-25T20:43:02.963411Z\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"news_results = run_dataset_benchmarks(\\n\",\n    \"    news_data, \\n\",\n    \"    news_target, \\n\",\n    \"    n_runs=32, \\n\",\n    \"    kmeans_kwargs={\\\"n_clusters\\\":25}, \\n\",\n    \"    umap_hdbscan_kwargs={\\n\",\n    \"        \\\"min_samples\\\":5,\\n\",\n    \"        \\\"min_cluster_size\\\":180, \\n\",\n    \"        \\\"metric\\\":\\\"cosine\\\", \\n\",\n    \"        \\\"cluster_selection_method\\\":\\\"leaf\\\"\\n\",\n    \"    }\\n\",\n    \")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"a6357957-1518-497f-b3e9-d54583be7dcc\",\n   \"metadata\": {},\n   \"source\": [\n    \"Again, let's start with the time taken. Since we have fewer samples everything will be faster, and since we are asking KMeans for fewer clusters we also expect it to be faster as well. What do we see in practice?\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 14,\n   \"id\": \"b64c2dee-159b-48cd-b7c0-cfb5766d4cc4\",\n   \"metadata\": {\n    \"execution\": {\n     \"iopub.execute_input\": \"2026-03-25T20:45:35.273371Z\",\n     \"iopub.status.busy\": \"2026-03-25T20:45:35.273197Z\",\n     \"iopub.status.idle\": \"2026-03-25T20:45:35.523724Z\",\n     \"shell.execute_reply\": \"2026-03-25T20:45:35.523041Z\",\n     \"shell.execute_reply.started\": \"2026-03-25T20:45:35.273356Z\"\n    }\n   },\n   \"outputs\": [\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"<seaborn.axisgrid.FacetGrid at 0x74201cd48e30>\"\n      ]\n     },\n     \"execution_count\": 14,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    },\n    {\n     \"data\": {\n      \"image/png\": \"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n6/ly5enrJ8yZYo6d+7svimx5+rYsaOmT58esg/7tuXdd9/VBRdc4L4ZqV27tn744YeU9du2bVPPnj1VunRpFSxY0K23X5IAwqtWrZpeffXVkGUnnHCCHn/88ZTPnF08nHvuue4zV79+fU2cOFGLFy92XSYLFy6stm3basmSJSmPt/+3z7f9fihSpIhatWql33//Pd3zPvnkk7r88svdNhUqVNAbb7zB2wRk8nwUdtGf+mZ/g6+++mr3OR0zZkzI9mPHjtWiRYvc+oMHD6pfv37u77ntx34v2LVA0OrVq3Xbbbe5m/29t98H9rnu0KGD+zv96KOP8l7mIgQLZJrBgwfrkksucaGid+/ebtmIESO0du1a90vp5ZdfdhchduFhv7AmT56sG264wd1WrVrltt+9e7c6derkLiDsMePGjXP/b9+mBJtbd+zYoT59+rhfbJMmTXKhwJpXbXlqFmLseGbNmuXWW5DYunWrW/fII4+4b0yGDh2qefPmacCAAS6oADh6FgDssz9z5kzVq1fPhYHrr79effv21dSpU902t9xyS8r2O3fudJ9NCxMzZszQmWeeqa5du2rlypUh+33hhRfUpEkT9wWC7evOO+/Ub7/9xlsFZLHGjRu7wJ/2izcLCK1bt1ajRo302muvuVaHF1980f29tc/xeeed54KH+eqrr9zf7/vuuy/sc9iXjshFbII84Gh17NgxcPvttwf++9//BuLi4gIjRoxIWdenT59A1apVA0lJSSnL6tatG2jfvn3K/QMHDgQKFy4c+Pzzz9399957z21z8ODBlG0SExMDBQsWDAwfPjzsMdg+ihYtGvjxxx9TltmP9sMPP5xyf+fOnYGoqKjA0KFD3f2uXbsGrrrqKt544AjZZ/mVV14JWda0adPAY489FvYzN3HiRLfMPtNB9jkvUKDAYZ+nQYMGgTfeeCPkec8666yQbS699NLA2WefzXsHZAL7Wx0dHe3+Fqe+9evXz60fMGCAu79jxw533/61+wMHDnT3K1SoEHj66adD9tmqVavATTfd5P7/xhtvDMTGxvJeHSdosYA36y9pI0P9+uuvrrUhtYYNGypPnkM/Ztblwb4BCYqOjnbdnTZu3OjuT5s2zXWdKFq0qGupsJt1mdq7d29KFwrb1lo56tSp47pC2c2++Uz7Lad9wxlk3TBsn8HnufHGG10LizXZ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\",\n      \"text/plain\": [\n       \"<Figure size 800x800 with 1 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    }\n   ],\n   \"source\": [\n    \"sns.catplot(\\n\",\n    \"    news_results[news_results.measure == \\\"Elapsed time\\\"], \\n\",\n    \"    x=\\\"algorithm\\\", \\n\",\n    \"    y=\\\"value\\\", \\n\",\n    \"    hue=\\\"algorithm\\\", \\n\",\n    \"    kind=\\\"swarm\\\", \\n\",\n    \"    col=\\\"measure\\\",\\n\",\n    \"    height=8,\\n\",\n    \")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"5b049931-a1ea-4e16-8832-ab39fda93e5d\",\n   \"metadata\": {},\n   \"source\": [\n    \"Both KMeans and EVoC produce sub-second timings. This time KMeans has a slight edge in speed, but the distributions overlap, so it is hard to say with confidence that EVoC is significantly slower than KMeans even in this case. UMAP + HDBSCAN was generally much faster than before, but still lags significantly behind the competition in run time. How about quality?\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 15,\n   \"id\": \"55978199-7589-48de-b8b8-ce479f09286a\",\n   \"metadata\": {\n    \"execution\": {\n     \"iopub.execute_input\": \"2026-03-25T20:45:35.524356Z\",\n     \"iopub.status.busy\": \"2026-03-25T20:45:35.524201Z\",\n     \"iopub.status.idle\": \"2026-03-25T20:45:36.389650Z\",\n     \"shell.execute_reply\": \"2026-03-25T20:45:36.389123Z\",\n     \"shell.execute_reply.started\": \"2026-03-25T20:45:35.524339Z\"\n    }\n   },\n   \"outputs\": [\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"<seaborn.axisgrid.FacetGrid at 0x741fa87a36e0>\"\n      ]\n     },\n     \"execution_count\": 15,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    },\n    {\n     \"data\": {\n      \"image/png\": \"iVBORw0KGgoAAAANSUhEUgAAA90AAAPeCAYAAAARWnkoAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzs3QV8FFfXx/F/lODu7u5Q3CktLYVSo0bdlfpDvdSdGlSpK6WlBhRaKO7u7gSH4ARCns+52w272d0gzRJCft/3s2/ZmdnZ2dl9cufMPffciOTk5GQBAAAAAIB0F5n+uwQAAAAAAATdAAAAAACEET3dAAAAAACECUE3AAAAAABhQtANAAAAAECYEHQDAAAAABAmBN0AAAAAAIQJQTcAAAAAAGFC0A0AAAAAQJgQdAM4YU8//bTq1asX8vnppG3bturVq5dOV9ddd50uvPDCM+Z9AOBMQDuXccqVK6e+ffum6z737duniy++WHny5FFERIR27typzOCff/7JVMeL0Ai6AWjChAmKiorSueeee1Jn48EHH9Tff/+dKQPlzz77zDVo3kfRokV1wQUXaP78+Tod0OACwH9HOxeh6tWrB5yXH374wbV9FuhmlhvaJ3Oj//PPP9fYsWPd7yA+Pl558+bV6SbYOW3evPlpe7w4MQTdQAZLTk7W4cOHM/QYBgwYoLvvvlvjxo3TmjVrTvj1uXLlUsGCBZVZ2Z1va9Q2bNigP/74Q3v37tX555+vxMTEjD40AMj0aOcyXs6cObV582ZNnDgxoP0vU6aMznTLly93Nx1q1aqlYsWKuRsNJyopKUlHjhzRqRQbG3vSx4vTC0E3Mi27I2iBot0VzJ8/v+uh/PDDD13AdP311yt37tyqWLGihg4d6ve6BQsW6LzzznOBor2mZ8+e2rp1a8r6YcOGqWXLlsqXL58LJLt06eL+WHtZIHbXXXepePHiiouLc3eHX3zxRbdu1apV7g/jrFmzUra3lCBbZj2Wvj2Xf/75pxo1aqRs2bK5u692UfLKK6+oQoUKyp49u+rWrasff/wx7OfRzpfd6b799tvdZ7We39Reeukld67snN544406cOBAmnedg92ttdRmS3H26tevnypXruzOoe37kksuccttm9GjR+utt95K6X2283o83519lmuuucatt+/n9ddfP65zYO9hjZq9xr6T++67T6tX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AcunTp2qW265RWeS0/kCCwCQfjZu3Ki7777btcPZsmVT6dKldcEFF+jvv/8Oy2n+559/XBuzc+fOdNmfXTuE61h92fXJI4884s5TXFycChcu7G54//7772F/b+BkRZ/0KwGkm+TkZCUlJSk6OmP+J3nuuefq008/1aFDhzR27FjddNNNrlHr379/wLa2jQXnGcXePxRreBH6vGXk9wYAp7uMbItXrVqlFi1auBvKr7zyiurUqeP+bv/555+68847tWjRIp3u5y1XrlzuEW633XabpkyZonfffVc1atTQtm3bNGHCBPffcElMTFRsbGzY9o8zHz3dyNTszqbdFbZ03vz586to0aL68MMPXcB4/fXXK3fu3KpYsaKGDh3q97oFCxbovPPOc42DvaZnz57aunVryvphw4apZcuWrvErWLCgunTpouXLl/v98b3rrrtUvHhxd5fV0ppffPHFlIbT7hzPmjUrZXu7i2zL7K6y791la0wbNWrk7mhbsGsNlzW2dvc2e/bsqlu3rn788cewn0d7/2LFirm76ldeeaWuuuqqlN5Vb7rYgAEDUu6+23GuWbNG3bp1c+cwT548uuyyy7Rp06aUfXpf98EHH7j95siRQ5deeqnfHfUjR46oT58+KlWqlNuvbW/n3st7Ln/44Qf3Xdu5/uqrr9x3m5CQkNJDb+8VLL38eI/xyy+/dK/NmzevLr/8cu3evTvN8zV+/Hi1adPGfSb73Z1zzjnasWPHcfdU2+/KeuuP9Vuyf5vu3bu7/Xifm99++00NGzZ0r7Hv5ZlnntHhw4f93vf99993nz9nzpx67rnnjut1S5cuVevWrd16u5gZMWJEmucCAGiL/7s77rjD/d22YPKSSy5RlSpVVLNmTd1///2aNGnScfdU27WHLbP206xevdr1lltbZW2B7XPIkCFufbt27dw2ts5eY5lv5ljXIqGuYVKnl9v+LrzwQr322muujbPrKbuB4HvzPD4+Xueff757n/Lly+ubb7455lAxa8ceffRRdx1n21qbZteC1157bco2Bw8e1MMPP+yuP+z4KleurE8++SRl/ejRo3XWWWe5dXZs//vf//zaQvtNW9ts579QoUI6++yzj+v6EQiFoBuZ3ueff+7+IFpDZX90b7/9dhfcNW/eXDNmzHABkf1R3LdvX8ofeAuYrGGYNm2aC/IsELOAzMuCdvtDa+nKlioVGRnpAh8LEs3bb7+tX3/91QWDixcvdoGgb0B0vKxBsABr4cKF7q72448/7nqcrYd5/vz5uu+++3T11Ve7xiGtO77eu8uhHhZ8nghr/HwbxWXLlrnPOmjQoJSbCdaQbt++3R2bBWZ2U6JHjx5++/G+zhpIO8/2Wmtwvd566y29/vrrrkGeM2eO+666du3qAj9flkZ2zz33uPPUoUMH1xhbEG3fpT0spS01u2g4nmO0ZRYUW1qaPWzbl156KeS5sc9gx2AXLhMnTtS4cePcBY3d5T8Zaf2W7Pdn7Ddhn9P73C507Hdh58QuAOzGhgXxzz//vN++n3rqKRd0z507VzfccMMxX2e/74suukhRUVHuIs+Cdjv3AHAstMUn3xZbO2VtpLWPFhinFmw41fGyfVoAOmbMGNcWvPzyy+5YLBi1Nt1Y22NtjLXJ5nivRVJfwwQzatQo187af+03Ym2O96azueaaa7RhwwYXyNvxWMfJ5s2b0/xM1klgNw7SukFu+/3uu+9cG2vHZ+2Ztxd+/fr1LnBu3LixZs+e7T6nBeTem9NedryW9WA32q29PJ7rRyCkZCATa9OmTXLLli1Tnh8+fDg5Z86cyT179kxZFh8fn2w/9YkTJ7rnTzzxRHKnTp389rN27Vq3zeLFi4O+z+bNm936uXPnuud33313cvv27ZOPHDkSsO3KlSvdtjNnzkxZtmPHDrds1KhR7rn9154PHjw4ZZs9e/Ykx8XFJU+YMMFvfzfeeGPyFVdcEfIcbNq0KXnp0qVpPg4dOhTy9ddee21yt27dUp5Pnjw5uWDBgsmXXXaZe/7UU08lx8TEuHPgNXz48OSoqKjkNWvWpCybP3+++0xTpkxJeZ1tY+fWa+jQocmRkZHuOzElSpRIfv755/2Op3Hjxsl33HGH37ns27ev3zaffvppct68eQM+S9myZZPffPPNEzrGHDlyJO/atStlm4ceeii5SZMmIc+XfRctWrRI8zd57733pjy39/v555/9trFjt89wrN9SqNe3atUq+YUXXvBb9uWXXyYXL17c73W9evU6odf9+eefQb+zYMcAAL5/92iLT74ttnbX/s7+9NNPx/xR+f499l5L2DWGl1172DJrP03t2rWTn3766aD7Cvb647kWCXYN421T69at63d9Ye2yXZt5XXrppck9evRw/164cKHbz9SpU1PW23myZd62PJjRo0cnlypVyl2bNGrUyLV148aNS1lv13K2jxEjRgR9/aOPPppctWpVv3b3vffeS86VK1dyUlJSym+6Xr16fq87metHwIsx3cj0fO+uWg+dpS/Vrl07ZZml/xjvndPp06e7O67Bxh3Z3VhL6bL/PvHEE663z9KGvD3cdpe6Vq1aLmXKUo2qVq3qxkNb+nmnTp1O+NgtLcvLeh4PHDiQksLkZenH9evXD7mPIkWKuMd/YT28dj4stcp6uK139J133klZX7ZsWb/x0nbX2O6S28PLUpHtbryts7vHpkyZMi513KtZs2buXNpddUvNtrvbNobNlz23O8+hztPxOt5jtF5lG4bgZWlmad1lt55uy6RILyfzW7LfsPV6+/ZsW0+7/X4so8PObbDzdqzX2XkJ9p0BwLHQFp98W+yJpT3DgtKbZTZZBuDw4cPVsWNHXXzxxSF7pU/0WuR42mbLCrNrM9821nrcjV0LWE9ygwYNUtZXqlTJpbunxYZArVixwl2jWS/0yJEjXS+9DZeyazdrp+09rVc6GGvrrG3zPd927bFnzx6tW7fOtYPBPt/xXD8CoRB0I9NLXRzKW4Xb97nxBs72X0sHthSr1KwxMLbegrWPPvpIJUqUcK+xYNsaHWMNxMqVK91Y8b/++sulFlljZmOeLBXdtxFNq/iXbxqZ9/j++OMPlSxZ0m87G3OUVnq5pSSnxRpRbyMSjI3rsvQqO2/2eVOf09TpbvbZgl0chFru5V3nu03q7YPtI1i63bEc7zEG+/14v4tQqfcnwvbn+1tI/XtI67cUih2fXVxYKnhqNhY71Hk71utSH6f3+AHgWGiLT74ttvHG9rfWgkEbFnW8jud6wwqj2tAtu7awwNvSwW1Ylw3HC+ZErkWOp21Oq40N1uaktTz1flu1auUeNh7bUsOtRowNiTpWOx3s+iDYjY9gbeixrh+BUAi6keVYkGPjhqyHM1iFUqt+aQ2fjd+xP+bGxu2mZmOKbXywPazoifVS2rgsb4+wjf3x3hX2LaoWivXCWoNmvemh7s4GY41MsDHNviyQTos1LHZ3+XjZsdpxrl27NqUn2S4mrLhZ9erVU7azbaw32/v+NgbaLhLsbrCdP1tu59buWntZBVIrbpIWqyB6rDHUx3uMJ8p6CGycvwWvx8N+D/Zb8LLx6t76Asf6LRUoUMBdWKT+rPYbth6CE/nOjud13nOW+jsDgPRGW3yU/a23wPi9995zPdOpgz0rlBZsXLfv9Ya3dzjY9Ya1gXaD3h69e/d2HQoWdHurcfu2MSd7LXIyqlWr5jLsZs6c6YqheWvBnMwUZnbcti/rpbdsRwuQbQy63cQOtq1dB/oG33btYVlvqW80nMhvFkgLvxhkOVZUxBqcK664Qg899JArwmZ/5K3ghi23hstS1K2Yh925tIbH7qL6evPNN906K6ZhQeTAgQNdYQ9rFO1506ZNXTEu+8Ns6elWlORY7I+9Bc9WsMQaC6uevmvXLtcQWCqTb1XO9E4vP1HWiFnwaVXOraiZNXRWedUaaN90LOs9teO2Qmn2Wexiwnpy7VwZO/9W7MsqzNu5tMItdsHw9ddfp/n+dl4tDcyCX6uqaunU3pTqEz3GE2UXLNag277sAsYuWizdzFLO7beUWvv27d20JvabsO/V7sL73vlP67fk/az2OS31zS6E7Pf55JNPujR0u5Cy97XXWSE6S9lLXQjG17FeZ+fM0tytAI31hNh39thjj530uQKAUGiL/fXr188VgLWbznYz3dova7esCKhlollnQGp2A9X+nlvVcPsbbjd17W+3L5vdpXPnzu5mt82yYanY3hvPNnTMgk4bYmaFxayH+GSvRU426LZ255ZbbknJtnvggQfccaSVZWWVxe0aztpyu16zG+pWzdyy9uwmtj3sOK2AqBVSs+sEq+JuQ8fsGsTab7susBsPVqHcbkbbtYgV0PVmD5zMb9Y3jR4IkDK6G8iEUhetSl1Myyt1IaglS5Ykd+/ePTlfvnzJ2bNnT65WrZorxOEtqmHFN6pXr56cLVu25Dp16iT/888/fvv48MMPXYENK9qWJ0+e5A4dOiTPmDEjZf8LFixIbtq0qdu3bWdFvYIVUvMtXmLs/d966y1X4MMKhBQuXDj5nHPOcUVDwiV1IbXUUhdG8Vq9enVy165d3TnInTu3K46ycePGgNf169fPFUyzwiwXXXRR8vbt21O2sYIlzzzzTHLJkiXd57XtrXBXWkXpvG677TZX8M3W23sF++6P9xh92ettP2mx30Pz5s3d78N+Q/Ydeb/L1L/J9evXu8IrdgyVK1dOHjJkiF8htWP9ln799dfkSpUqJUdHR/sd17Bhw9wx2G/MXnfWWWe5fXmFKn52rNdZMRgriBQbG5tcpUoVtz2F1ACkhbY4fWzYsCH5zjvvdH/r7W+wtY3WhnmvHYL9bbcCYlYszdpYK5Y5cOBAv0Jqd911V3LFihVde2XXFFZoduvWrSmv79OnT3KxYsWSIyIi3PXA8VyLhLqGCVZILfX1hbWP9nvx/cydO3d2x2ef+5tvvkkuUqRI8vvvvx/yPFlB0GbNmiUXKFDAfe4KFSok33PPPX6fa//+/cn33XefKxRq59La0QEDBvi141a41dbZ53/kkUf8Ct0F+00fz/UjEEqE/b/AUBwA/hu7825TcR1Paj0AAIAVMrPee6txYtNzAmcK0ssBAAAAnHKW7m7DxWzYlo1Nt7m/bViVb60X4ExA0A0AAADglLNq6zYe26YAs/HkNq7d6rqkrnoOZHaklwMAAAAAECahS/QBAAAAAP7f3n1AR1W0YRx/UkkoCT300HvvvSoIgmBFUVCs2MGOXSxYsYOC5bODCioiICjSexOk9w4hARJCSUKS78ysm2STXaRkU/+/c/bIzt29uVvMzXNn5h3gohC6AQAAAADwEkI3AAAAAABeQugGAAAAAMBL8l3oNsuSx8TE2P8CAADOqwAAeFO+C93Hjx9XaGio/S8AAOC8CgCAN+W70A0AAAAAQFYhdAMAAAAA4CWEbgAAAAAAvITQDQAAAACAlxC6AQAAAADwEkI3AAAAAABeQugGAAAAAMBLCN0AAAAAAHgJoRsAAAAAAC8hdAMAAAAA4CWEbgAAAAAAvITQDQAAAACAlxC6AQAAAADIi6F77ty56tOnj8qVKycfHx/9/PPP//mcOXPmqFmzZgoKClLVqlX10UcfZcmxAgAAAABwvvyVjU6cOKFGjRpp8ODBuvrqq//z8Tt27FCvXr10xx136Ouvv9aCBQt0zz33qFSpUuf0/Ivx2fztevm3DUpMlvx8pCubVNCt7auobrmQlMcciD6lSSv3KSo2Xq2rFle3OmHy8/Wx207FJ+rXv/dr/YEYVS1VSP2alFdIUIDdlpycrPlbIzV702EVLuCvK5uUV+WShVL2u/1wrH5etU8n4xPVtXZpta1eMmVb9KkE/bRyr3ZGnVS9ciHq06icggL87LbEpGTNXH9QS3YcUakiBXR10woKCwlKee7avdH6be0B++9eDcqoYYWiKdsiYk5r4sp9ijh+Wi0rF9eldcPk7+e4RnM6IVG/rTmgtfuiFV6ioK5qUkGhBR2vxVi4LVKzNkQoONBPfRuXV/XShVO27Yo6oZ9W7dPx02fUqWYpdahR0l5wMY6fTrCvc9vhE6pTtoiuaFTe7sNISkrWnxsjtGhblIoXCtBVTSuoXNHglP1uOBBj31/zmnvUL6OmlYqlbIuMjdOklXt1IPq0bb+sfhkF/Pta4s8kado/B/TW7xu1++hp21a4gJ/mP9JZRYukvlcAAAAAcCF8kk3iywFM8Prpp5/Ur18/j495/PHHNXnyZG3YsCGlbciQIfr777+1aNGic/o5MTExCg0NVXR0tEJCUgPz2VwzZoGW7zrmdtuTvWrrzo7VNG/LYd3x5XKdTkhK2daxZil9Mqi5Dcb9P16k7ZEnUraVCQnS+Dtb29A6bMJq/bx6f8o2f18fjerfWFc0KqeJK/bqsYlrbJh0uq55Bb1+TSNtjYjV9WMX21DpVDOssMbf2UYFA/00+PNlWrQ9KmWbafvslhZqXbWEPpi1RW/O2OzyWoZdUlMPXlJDS3cc0eDPl+pEfGLKtlZViuuLW1vaiwfmZ246dDxlW8nCgfrujtaqEVZET0xco/HL9qRsM9ccXr26oa5rXtEG9QfHr9KZNK/FXCR4t39j7T16Sv3HLrLB2KlKyUKacGdrFSsUaN9bc1HCqYC/r8YOam6D+yfztuul31K/E8ZdnapqeM86Wr3nmAZ+usSGfKcmlYrq69taKSk5WQPGLbEXD9xZ/EQ3lSlK8AaAzD6vAgCQn+SqOd0mWHfv3t2lrUePHlq+fLkSEhK89nM9BW7jtembtO/oSQ2ftNYlcBtzNx+2Pazv/LHZJXAbB2NOa+S0DZq1McIlcBsmlD7901rb2/zsL/+4BG7j++V7NX9LpF6cst4lcBubD8Xqw7+2asKyPS6B2zA95U/+tNb2Nr810zVwG+/8uVk7Ik/Yx6QN3IbpLf9u6W6Nnr3VJXAbkbHxGjFlvRZujXQJ3IY59Ocnr7M95k/9vNYlcBumd9r0YJv3Im3gNsyxvP3HFv20cp9L4DbiziTpyUlrtf/oKb06bWOG1/LxnO229/uZn/9xCdzGqt3H9L+FOzVu7naPgdvo+MYsj9sAAAAAIMcPLz9fBw8eVFhYmEubuX/mzBlFRkaqbNmyGZ4TFxdnb2mvyJ+PKav3nXW7CcQmaJqeWnf+2BChfzwEOxO4ixUMdLst5vQZfbNkd4bw6zRj/UHN3XLYw888pErFC7rdtv3wCX2/fI/cjW8wbT8u32N70D3t98Ax12DsZIbHm2Hz7piw/83i3Tp20v2FkT/WH9KfGyI8/swjJ1wvLDjtO3ZK3y3bnSHIO5mh6p5CtdmvGVp+NvFmLgEAIFPPqwAA5De5qqfbcM7/dXKOjk/f7jRy5Eg77M15q1ix4nn9vOKF3IfitMw8bE/MnGTnvOT0Cvj7qWCg5+cWCfK8zQwVN0Os3f7MAD97u5DjLXyWn2n26Zwvnl6gn68KBlzYazHvT1CA73n/zP/ar3md/06pP+/9AgDklfMqAAD5Ta4K3WXKlLG93WlFRETI399fJUqUcPuc4cOH23lmztuePa7Dn/9L2xqlzrq9SAF/3dg6XI0rphYhS+uqJuVtYTR3THu/JuXcbjM91YNah6tsaMY5xT7/FnIzc7497dfTzzQF3m5oWcltKDfBt3+LSmpbzf17aX6mp/2audlXNS3vNuSGhRTQoDbhqlzCfe+7KSrnab9mn562NaoQqhtbhbsN3gF+Prq2eUV1qVXaw349vxanUoX/+4ILAOQ3F3teBQAgv8lVobtNmzaaOXOmS9uMGTPUvHlzBQSkVs9Oq0CBArawS9rb+Xq6V2237cF+0uibmtoe1Xevb6xqaYZXm2JoD3Stri61S2tIp2q6vKHr0HdTtfvxnrVtxfDn+9R16bUuXzRYY25qqsAAP310UzMbWtMG45f61VetMkX01OV1bYhOq1/jcrqtfRX1bFDW/lxn9XSjVlgRvXltIxUtGKgPb2yikDRh1QTXD25oanv2zWNqlymSss3s486OVe1rGNyusr2QkHZgQcsqxfXM5XVtIbVXrmzg0mtdukgB+xoC/f00+sZm9rU5Bfr76pnede0Fi8cuq20Lz6XVs34Z3d25mjrXKq2hl9SwQdrJDGV/9/omKlTA3+6/WJrq6ebzeKd/E5UJDdLIqxqoQfnQlG3m7bi5TbiublpeA1pW0g0t3ffQmJ+07OlL3W4DgPwsM86rAADkJ9lavTw2NlZbt261/27SpIlGjRqlLl26qHjx4qpUqZK9mr5v3z59+eWXKUuG1a9fX3fddZddNswUVjPVy7/77rtzXjLsYqqs9nh7jvYcOWmXs7q7cw21r1HSZYiyeSsXbz+iqBNxalG5uMvyXMaWQ8e18eBxW5W7fpogaBw9Ea+F26Js+DU9zc7luYyExCQt2Bpp50a3q1bSZXkuY83eY9oVddIuX1atVOryXM5lzJbvPGrDrwnHaYfhm0rkZi62Oe4ONUq5DIM3baaKecTxODULL+ayPJdzGbN1+2Nsj3yjdL380ScT7LJhQYF+al+9ZMryXMaZxCT7Ok1xszbVSmQYvm/mv5sCauaiQs2w1OBvmMJyS3cesc9pXaWEfNNcUDDLmJnicmZ+t7mgYcJ4Wst3HrGF2kzAr5huvrspLDd36yG9MW2z4hKSbK/8U73ruTwGAOAe1csBAMjBoXv27Nk2ZKd3880363//+59uueUW7dy50z7Oac6cORo2bJjWrVuncuXK2WXETPA+V/xxAABA5uG8CgBALlmnO6vwxwEAAJxXAQDIKrlqTjcAAAAAALkJoRsAAAAAAC8hdAMAAAAA4CWEbgAAAAAAvITQDQAAAACAlxC6AQAAAADwEkI3AAAAAABeQugGAAAAAMBLCN0AAAAAAHgJoRsAAAAAAEI3AAAAAAC5Cz3dAAAAAAB4CaEbAAAAAAAvIXQDAAAAAOAlhG4AAAAAALyE0A0AAAAAgJcQugEAAAAA8BJCNwAAAAAAXkLoBgAAAADASwjdAAAAAAB4CaEbAAAAAAAvIXQDAAAAAOAlhG4AAAAAALyE0A0AAAAAgJcQugEAAAAA8BJCNwAAAAAAXkLoBgAAAADASwjdAAAAAAB4CaEbAAAAAAAvIXQDAAAAAOAlhG4AAAAAALyE0A0AAAAAgJcQugEAAAAA8BJCNwAAAAAAXkLoBgAAAADASwjdAAAAAAB4CaEbAAAAAAAvIXQDAAAAAOAlhG4AAAAAALyE0A0AAAAAgJcQugEAAAAA8BJCNwAAAAAAXkLoBgAAAADASwjdAAAAAAB4CaEbAAAAAAAvIXQDAAAAAOAlhG4AAAAAALyE0A0AAAAAgJcQugEAAAAA8BJCNwAAAAAAeTV0jx49WlWqVFFQUJCaNWumefPmnfXxH374oerUqaPg4GDVqlVLX375ZZYdKwAAAAAA58Nf2WjChAkaOnSoDd7t2rXTxx9/rJ49e2r9+vWqVKlShsePGTNGw4cP17hx49SiRQstXbpUd9xxh4oVK6Y+ffpky2sAAAAAAMATn+Tk5GRlk1atWqlp06Y2TDuZXux+/fpp5MiRGR7ftm1bG87feOONlDYT2pcvX6758+ef08+MiYlRaGiooqOjFRISkkmvBACA/InzKgAAOXR4eXx8vFasWKHu3bu7tJv7CxcudPucuLg4Oww9LTPM3PR4JyQkePV4AQAAAADINaE7MjJSiYmJCgsLc2k39w8ePOj2OT169NAnn3xiw7rpoDc93J999pkN3GZ/noK6uQqf9gYAAC4M51UAAHJZITUfHx+X+yZMp29zeuaZZ+yc79atWysgIEB9+/bVLbfcYrf5+fm5fY4Zpm6GkztvFStW9MKrAAAgf+C8CgBALgndJUuWtEE5fa92REREht7vtEPJTc/2yZMntXPnTu3evVuVK1dWkSJF7P7cMYXXzPxt523Pnj1eeT0AAOQHnFcBAMgloTswMNAuETZz5kyXdnPfFEw7G9PLXaFCBRvax48fr969e8vX1/1LKVCggC2YlvYGAAAuDOdVAABy0ZJhDz30kAYOHKjmzZurTZs2Gjt2rO29HjJkSMrV9H379qWsxb1582ZbNM1UPT969KhGjRqlf/75R1988UV2vgwAAAAAAHJe6O7fv7+ioqI0YsQIHThwQPXr19fUqVMVHh5ut5s2E8KdTOG1t956S5s2bbK93V26dLGVzs0QcwAAAAAAcppsXac7O7CeKAAAnFcBAMg31csBAAAAAMirCN0AAAAAAHgJoRsAAAAAAC8hdAMAAAAA4CWEbgAAAAAAvITQDQAAAACAlxC6AQAAAADwEkI3AAAAAABeQugGAAAAAMBLCN0AAAAAAHgJoRsAAAAAAC8hdAMAAAAA4CWEbgAAAAAAvITQDQAAAACAlxC6AQAAAADwEkI3AAAAAABeQugGAAAAAMBLCN0AAAAAAHgJoRsAAAAAAC8hdAMAAAAA4CWEbgAAAAAAvITQDQAAAACAlxC6AQAAAADwEkI3AAAAAABeQugGAAAAAMBLCN0AAAAAAHgJoRsAAAAAAC8hdAMAAAAA4CWEbgAAAAAAvITQDQAAAACAlxC6AQAAAADwEkI3AAAAAABeQugGAAAAAMBLCN0AAAAAAHgJoRsAAAAAAC8hdAMAAAAA4CWEbgAAAAAAvITQDQAAAACAlxC6AQAAAADwEkI3AAAAAABeQugGAAAAAMBLCN0AAAAAAHgJoRsAAAAAAC8hdAMAAAAA4CWEbgAAAAAAvITQDQAAAACAlxC6AQAAAADwEkI3AAAAAABeQugGAAAAAMBLCN0AAAAAAOTV0D169GhVqVJFQUFBatasmebNm3fWx3/zzTdq1KiRChYsqLJly2rw4MGKiorKsuMFAAAAACBXhO4JEyZo6NCheuqpp7Rq1Sp16NBBPXv21O7du90+fv78+Ro0aJBuu+02rVu3Tj/88IOWLVum22+/PcuPHQAAAACAHB26R40aZQO0Cc116tTRO++8o4oVK2rMmDFuH7948WJVrlxZDzzwgO0db9++ve666y4tX748y48dAAAAAIAcG7rj4+O1YsUKde/e3aXd3F+4cKHb57Rt21Z79+7V1KlTlZycrEOHDunHH3/U5ZdfnkVHDQAAAADAufNXNomMjFRiYqLCwsJc2s39gwcPegzdZk53//79dfr0aZ05c0ZXXHGF3n//fY8/Jy4uzt6cYmJiMvFVAACQv3BeBQAglxVS8/HxcblverDTtzmtX7/eDi1/9tlnbS/59OnTtWPHDg0ZMsTj/keOHKnQ0NCUmxm+DgAALgznVQAAzo9Pskm52TS83FQgN8XQrrzyypT2Bx98UKtXr9acOXMyPGfgwIG2h9s8J21xNVOAbf/+/baa+blckTfBOzo6WiEhIV55bQAA5FWcVwEAyCU93YGBgXaJsJkzZ7q0m/tmGLk7J0+elK+v6yH7+fnZ/3q6dlCgQAEbrtPeAADAheG8CgBALhpe/tBDD+mTTz7RZ599pg0bNmjYsGF2uTDncPHhw4fbJcKc+vTpo0mTJtnq5tu3b9eCBQvscPOWLVuqXLly2fhKAAAAAADIQYXUDFMQLSoqSiNGjNCBAwdUv359W5k8PDzcbjdtadfsvuWWW3T8+HF98MEHevjhh1W0aFF17dpVr732Wja+CgAAAAAActic7uxi5nSbgmrM6QYAgPMqAAB5vno5AAAAAAB5FaEbAAAAAAAvIXQDAAAAAOAlhG4AAAAAALyE0A0AAAAAgJcQugEAAAAA8BJCNwAAAAAAXkLoBgAAAADASwjdAAAAAAB4CaEbAAAAAAAvIXQDAAAAAOAlhG4AAAAAALyE0A0AAAAAgJcQugEAAAAA8BJCNwAAAAAAXkLoBgAAAADASwjdAAAAAAB4CaEbAAAAAAAvIXQDAAAAAOAlhG4AAAAAALyE0A0AAAAAgJcQugEAAAAA8BJCNwAAAAAAXkLoBgAAAADAS/y9tWMAALwtKTlJs/fM1ty9cxXsH6ze1XqrXol6vPEAACDHIHQDAHKl5ORkPT73cU3fOT2l7ZsN32h4q+G6ofYN2XpsAAAATgwvB5A1khKlxR9J47pKY9pJs16W4o7z7uOCzd833yVwG8lK1qjloxQdF807CwAAcgR6ugFkjZ/vkdaMT71/6B9p6x/SbTMkvwA+BZy3BfsXuG0/nXhayw8uV7fwbryrAAAg29HTDcD7Ija6Bm6n/SuljVP4BHBBCgUU8rwt0PM2AACArEToBuB9+1d53rZvJZ8ALsgV1a6Qv0/GAVvlC5dXi7AWvKsAACBHIHQD8L6ilS5sG3AW4SHhern9yyoSWCSlrVKRSnq3y7vy8/XjvQMAADkCc7oBeF/ldlK5Jhl7vAuVkhpexyeAC9arai91rdRVKyNWqqB/QTUq1Ug+Pj68owAAIMegpxtA1hjwg1S3n+T777W+Kh2lm3+VgkL5BHBRgvyD1LZcWzUu3ZjADQAAchx6ugFkjcKlpOu+kOJPSklnpKAQ3nkAAADkeYRuAFkrsKDr/RNRkqk0HRDEJwEAAIA8h9ANIHtsny39/rR0aK3kHyw16i/1GJkxlAMAAAC5GKEbgPckJUqbpkl7l0mhFaQG10rBRaXDm6RvrpMS4xyPO3NKWvE/Ke64dM1nfCIAAADIMwjdALzDzN3++mpp98LUtr9ekQb9Iq36OjVwp7XuJ6n7S1JIOT4VAAAA5AlULwfgHUs+cg3cxqkj0m8PSdF73T8nOUmK2c8nAgAAgDyD0A3AOzZNdd9uhpqXquV+W2Bhz9sAD1ZHrNa7K9/V2DVjtfe4hws6AAAA2YTh5QC8w6+A+3YfX6npQOmfH6Vju123tR8mFSjCJ4Jz9tLilzRh04SU+2NWj9FL7V/S5VUv510EAAA5Aj3dALyj4bXu26tfKhWvKt02U2p9r1S6nlSlo3TN51LHR/g0cM6WHVzmEriNM8ln9OLiF3Uy4STvJAAAyBHo6QZwceJPSCu+cCwBFlxMajpIqtxOajLIMZTcFE1zKlVbqt5NWvmlVKOHdNkrvPu4YLN2z3LbfiLhhBYfWKyulbry7gIAgGxH6AZwcRXK/3e5tH9Vatua8dLlo6QWt0l9P5TaPijtWy4dPyTNfVOa9pjjcb4BUo9XpFZ38gngghTwNIXhP7YBAABkJYaXA7hwq79xDdxOf77gCORGqZpS3b7SgrelhNjUxyQlOAK4WbMbuAC9qvaSj3wytJcMLqmWZVvyngIAgByB0A3gwu2c7779dLR04O/U+1tmOtoySHaszQ1cgJrFamp4q+EK9A1MaSseVFyjOo9SgBlJAQAAkAMwvBzAhStU8izbSqX+OznR8+OSzvAJ4ILdUPsG9ajcQwv3L1Swf7A6lO+gQL/UEA4AAJDdCN0ALpwpmrb884yhunIHqWT11PvVukkBBSV3FaXr9HH0di8dJ8Xslyq1ljo8LJWswSeDc2J6t3tX7c27BQAAciSGlwO4cGUbSVePkwqHpbZVauNon/qotPE3KSlJCi4q9XlP8k13na/jY9LuJdIPt0i7FkhHd0h/fyd9col0ZAefDAAAAHI9n+Tk5OQLeeLWrVu1bds2dezYUcHBwTK78fHJWNAmp4mJiVFoaKiio6MVEhKS3YcD5A2JCVLEekdRNRO2E+NTt9W8TOr/jeTnL0Xvk9ZNks6clmr3lopXk96uK504nHGfLe+Uer2RpS8Dede2Y9u0MmKlSgWXUvvy7eWf/gIQLhjnVQAAMrmnOyoqSpdccolq1qypXr166cCBA7b99ttv18MPP3y+u9Po0aNVpUoVBQUFqVmzZpo3b57Hx95yyy022Ke/1atX77x/LoBM5Bcgla4rzXrZNXAbm6c7grYRUk4KbyuFt5NKVJdi9rkP3Mb+1XxEcOv0mdP6actPGrFohMatGafDJz18h0w5geRkPbfwOfX7pZ99/P2z7lfvn3prV8wu3l0AAJAzQ/ewYcPk7++v3bt3q2DBgint/fv31/Tp089rXxMmTNDQoUP11FNPadWqVerQoYN69uxp9+3Ou+++a0O+87Znzx4VL15c11577fm+DACZzfRyn4hwv80E74NrpQ+aS+O6Sp/3lEbVdVQ4Dyjk/jnFq/AZIYPouGjdOPVGPbvwWf2w+Qe9t+o99f25r9YeXuv23ZqyfYombfn3os+/9sXu07MLnuXdBQAAOTN0z5gxQ6+99poqVKjg0l6jRg3t2nV+PQejRo3SbbfdZnvJ69Spo3feeUcVK1bUmDFj3D7eDAsvU6ZMym358uU6evSoBg8efL4vA0BmM4XSPPEPkr69XoramtpmAvqkO6WGbi6amaG/re7iM0IGX67/UpuPbnZpO55wXK8ufdXtuzV1x1S37Wao+cETB3mHAQBAzgvdJ06ccOnhdoqMjFSBAgXOeT/x8fFasWKFunfv7tJu7i9cuPCc9vHpp5/aoe7h4eHn/HMBeEmZ+lKZBu63mUrkMXsztifGSUUrSR0flYKLOdrCGkg3jJfKN3Pcjz8hLRkrfX+zNPUx6dA6PsJ8bN5e91OQ1kSu0dHTR+2/I05GKPJUpP13YpLn5eoSz7aUHQAAQCY570oypnDal19+qRdffNHeN3Oqk5KS9MYbb6hLly7nvB8T0hMTExUWlqbqsfl7OyxMBw/+d++DGV4+bdo0ffvtt2d9XFxcnL2lLfgCwEuu+Z/0nenR3uK471dA6vqUVLiM5+ecjpEufUHq/KR05pQUWMh12+e9pENphg4v/1S69gupDktE5UcFPYyoMIXRzDzt+/68zwZwo1WZVmpeprkWHViU4fF1itdR+cLlvX68eRHnVQAAvBy6Tbju3LmzHdpteqsfe+wxrVu3TkeOHNGCBQvOd3cZKp6faxX0//3vfypatKj69et31seNHDlSL7zwwnkfFwAPzFraSz6S9q2UQis4qoyXb+rYZtbmvm+ZtGuhdOqIo2BaweJSzAHHkPGkMxn3V72b47++vq6B2xmw0wZuw+xj+hNSrV6O5yBf6Vutr1YcWpGhvUvFLnrwrwd15PSRlLYlB5dof+x+dSjfQfP2pfaQFy1QVM+1fS7Ljjmv4bwKAEAWLBlmeqLNvGszPNz0cjdt2lT33nuvypYte877MIHdDFP/4YcfdOWVV6a0P/jgg1q9erXmzJnj8bnmkE319N69e+vtt98+7yvyZt44S4YBF+DYbsca2rGHUttMmDZLgtW67OzPnfumNMsxQiZF/Wukaz5NXXZsywwpNsIR1kvVlL7sK22f7X5/9y6VStXiY8xnzO//15a9pvEbx6cMD28W1kzdKnbT68tfd/ucD7p+oAL+BbTy0EqVKlhKPSv3VOHAwll85HkH51UAAM7PBS1UaoqYXWzvcWBgoF0ibObMmS6h29zv27fvWZ9rArlZJ9wUYfsvZp75+cw1B3AW899xDdzOnuc/nksN3SePSLsWOOZoV2qb2hvd8RGpUhtp8Wgp4bTU9Capzr//r0dukb66SopOs3JB89ukoKLuj8PH1/M25GlmJNQTLZ/QLfVu0fqo9SpXuJxqF6+t0atHe3zOoZOHdF2t69S6bOssPda8ivMqAABeDt1z5879zznf5+qhhx7SwIED1bx5c7Vp00Zjx461y4UNGTLEbh8+fLj27dtn55CnL6DWqlUr1a9f/3wPH8DF2J1xbqx1eKN0Ikr6+ztHb/aZ0452sxa3KYpmCqkd/Ef69cHU+d4H10j+wY6w/tNdroHbObS8/UPuf17Ny6QirvUgkHeZKuNvr3hbf+7+087d7lWll4Y2G6qulbqmPKZx6cYen3+2bQAAADkudJv53OmlnYNtiqOdK7O2d1RUlEaMGGELo5kQPXXq1JRq5KYt/ZrdZlj4xIkT7ZrdALJY4TApYn3GdjNU99A/0oynXNvNEmE/3CLdOVv6tr9rBXOzZNj3g6RBk6V9GefopoT5y16VZr0sxR93tFXtIl3xQWa+KuRgcYlxuu3327T7+O6U+2Z97i1Ht+irXl+lPK5N2TZqV66dFux3rS1yRbUrVLNYzSw/bgAAgAsO3WZd7LQSEhK0atUqPfPMM3r55ZfPd3e655577M1TsTR3a3WfPHnyvH8OgEzQ4nZp+18Z25sMlP6Z6P45Jowv+9TzkmEbf/X888zQ9ZZ3SaEVpT1LpMrtpZo9LuIFILeZuWtmSuBOa/Xh1Vp+cLlqFq+p//3zP83ZO0dBfkHqUbmHIk9Gyt/P387dvrJG6vQlAACAXBG6TehN79JLL7VzvIYNG2aLqwHIo8wyXb3elGaPlE5GOZYEazxAunSE9Iv7i2fWSceayW6ZQmyl6kiHN2TcZkL26NZS5CbH/YXvSQ37S/3GSL5+mfCCkNPtjN7pcduWY1v06tJXtenoJpf1uq+peY2ea0N1cgAAkDNk2no7pUqV0qZNqX/4AMijmt4stX1ACqsvhdV1zNs2anjogS5USmoyyBGu3TFLhvX9QApKd0Gv1uXS9jmpgdtpzQRpxeeZ8UqQC9QoVsPjtqhTUS6B22nSlknae9zNyAoAAIDc0NO9Zs2aDMu3mLnXr776qho1apSZxwYgJ/p+oLR5eur9/auknfMcy4atmyRtmpq6zTdAunyUVKyS1PkJadZLGZcMq/Jv8cUH10j//OhYMsz0cJdpIL1Wxf0xrJ3oGOqOPM8USzPB28zhTsvM4Y6Oi3b7nKTkJFvZvEKRCll0lAAAAJkYuhs3bmwLp6Vf3rt169b67LPPznd3AHKTXYtcA7eTadu7zBG8TU/02u+l4OJS58elkv8Wser4qGMJMbM9MV6q1Uuq3Tt1HwHBjmXA4k86wnqSKcro+nsmRVKCl14gcpoA3wB92v1TuyTYrN2zbPXynlV66q5Gd2nCxgken1e+cPksPU4AAIBMC907duxwue/r62uHlgcFBZ3vrgDkNiZYe7JvuWO7WTLMhGpj92JpwHhHr7W5UHd0p3Rsl3QmTjq6w1FIzTdYitomfdlXit6Tuj8Tyiu2lvYszviz0oZ15HnFgorpqdZP2VtaV1S/Qp/880mGHu+mpZuqXsl6WXyUAAAAmRS6nct5AciHQs8yXNeszZ1++LipWP79zdL9K6Qpw1znYps1vzf/Lt38q2P97rSB2zDD1NsNlY5sk04cTm2v3EFqdVdmvSLkYsWDimvcpeNsMbWVESttL/il4ZfqyZZPZvehAQAAnF/ofu+993SuHnjggXN+LIBcxvQwF60kHUu3hJNpi97n/jkmNK/7WVqRcQlA7VogrfnBMSfcHbNM2AOrHMuRmf1XaCFVv8QMscmEF4O8oE6JOvqi5xc6Hn/cDkUP8mfUFQAAyIWh++233z6nnZm53oRuIA/zD5QG/SJNeUjaPtvRVrWz1HuUNO8tz887+Lfn+dn7/2OZwQJFpGa3XMRBIz8oElgkuw8BAADgwkN3+nncAPKx4lWlQT9Lp4467gcXc/y3Zk9p1dcZH1+wpBTeXprv4eJd8WqOIePuervr9svMI0cOZopzfrX+K3278VsdOnlIjUs11n1N7lOzsGbZfWgAAAAXhTGaAC6MCdvOwO0sfGaWAEvLL1Dq845jLe7SdTPuw1Qrb3id1OddKbRSxnW6W9zGp5NPfPT3R3pj+RvaF7tPZ5LOaPmh5bpzxp3adCTjOtzn6u/Df+uh2Q+p78999fDsh/VP5D+ZeswAAADnwic5/dpf52Dv3r2aPHmydu/erfj4f6sU/2vUqFHKyWJiYhQaGqro6GiFhIRk9+EAec/2OdLWP6SgEKlhf8d8b8PMyf71AWnrn46h5uWaONbwLt/Usd1UNN/4mxRj5m63lCq1ytaXgawTlxinLhO66HjC8Qzbrqh2hV5u//J573PpgaW664+7bIB3MnO+P+n+iZqG/fudQ6bgvAoAQCZXL//zzz91xRVXqEqVKtq0aZPq16+vnTt32qGBTZvyhwyQ5+xeIh1a6xgGbuZv+/ic/fFVOzlu6YWWl26aKJ2IciwVFlLOdbt/Aan+VZl77MgVIk9Fug3cxs7onfa/yw8u17qodapQuII6VexkK5WfzYerP3QJ3EZCUoLG/D1G47qPy8SjBwAAyOTQPXz4cD388MMaMWKEihQpookTJ6p06dK68cYbddlll53v7gDkVPEnpQk3SttmpbaZ3umbJkkFi1/4fguVyJTDQ95RKriUihYoqmNxxzJsqxJaRUNmDtGC/QtS2iqHVLbBuUyhMjawj149WnP2zlFB/4LqU62PBtcfbAO6OwwxBwAAOX5O94YNG3TzzTfbf/v7++vUqVMqXLiwDeGvvfaaN44RQHYw1cjTBm5j/yppxjMXtr/Yw9KSj6W5b0j7V1/88SWckhZ9KP2vt/TNtdLaHy9+n8gWgX6BNiinF+wfrMIBhV0Ct7EzZqdeWfKKTiac1C3Tb9EPm39QxMkI2/7+qvf11PynVK5wupEU//LUDgAAkGNCd6FChRQXF2f/Xa5cOW3bti1lW2RkZOYeHYDsY9bG9tRuSkFs+0saf6M0rpv0+1NSzAHP+9oyU3qngTTtMWnWS9LYTtLURy/82BLPSF9dJf3+pKPq+ZYZ0sTbHMeBXOnW+rfq+TbPq2axmgotEKpOFTrp88s+twXV3Jm7d65+2faLdsXsyrBt2o5p6lWll9vnDao7KNOPHQCAvOTY6WP2YjaycXh569attWDBAtWtW1eXX365HWq+du1aTZo0yW4DkEckJ7pvN/NkV30lTb4/tW3fcmndT9Idf0lFwlwffyZe+mmIdOaUa/vSsY6K59W6nP04ju2RNvzq+Hed3o7CbBunSLsXZnzs4jFSqyFS0Yrn9hqRo1xd82p7SyvZw/rupn3L0S0e91U1tKoeb/G4PvvnMx0+dVilC5bW7Q1uV9/qfTP9uAEAyAtM0H5h0Quav2++kpKTVLdEXT3d6mk1KNUguw8t/4VuU508NjbW/vv555+3/54wYYKqV6+ut9/2sA4vgNynzhXSog8ytte+3NFbnZ6pOr5kjHTJ867tJhyf9DAKZsNkqUonaetMaftsx1zxhtenhubln0u/PZx6AWDG01KvN6RID2HLPG7vUkJ3HtI9vLs2H92cob1j+Y6qVrSax+eFh4Sre+XuuqH2DToef1whBULk68MqmQAAeHLfn/dpw5ENKffXR623K4FMuXKKigddRD0fnP/w8hdffFGHDx+21coLFiyo0aNHa82aNbanOzw8nLcUyCs6PiqVbeTaVqyK1PJOKfaQ++fsWZqxzcfP888wIcgUa/v2OmnxaEeY/6C5tHmGFLNfmvqIa4+7+bcZlh4Q7HmfRcr+50tD7nFzvZvVqqzr8nEVi1TUE62esEXTTBG29NqVb6daxWvZf/v5+qloUFECNwAAZ7Hi0AqXwO1kLlz/uu3fEYfIup7uqKgoO6y8RIkSuv766zVw4EA1btz4wo8AQM4UXNQxXHzTNOnQP1KJ6lKdPo4CZn6BUmJ8xueYZcBWfS0tGi0d2+1Yg9uE9yLlpOP7Mz4+qJi07BPXtjOnpV8flNo96BjKnp4J3gEFpcAiUny6ZabCGkiV2lzsK0cOEuQfZNfWXnJgia08XqFIBXWt2FUBfgF2u5n3/dbytzRv7zz72N5Ve2tYs2HZfdgAAOQqh0546FCRdPDEwSw9lrzovEP35MmTdezYMX3//ff69ttv9c4776hWrVq66aabNGDAAFWuXNk7Rwog6/n6OeZRm1va9bQbXucI1+l7rQuVln65N7Vtxxxp9yLHkPA/npdOHU19bMfHpMhN7n+uCeimp9uToFDpph+lKcOkiPVmh441xPt++N/riCNXMr3d6Xu8ncPI3+v6nh195cNnDwDABWlYqqEdFWbmcqfXuDQdrBfLJ9n8pXIR9u7dq++++06fffaZtmzZojNn3PRM5SAxMTEKDQ1VdHS0QkJCsvtwgNzJ9HZPe1z6e7yUGOcobtbtWcdyYsfdVDGv21fqN8bRa354k2O975qXSZPvk1Z/4/5n3Pyro0J5UoJru6+/NPQfKeTfYeRHd0r+wRkLuAHIEpxXASBveHnxyxq/abxLW+NSje2oMn/z9xcu2EW9ewkJCVq+fLmWLFminTt3KiyMP3qBfMHMqb7iPanHy47e65AK0ulj0vHb3T/eBO3ovdKCd6SDax1toZWkFre5f3yZBlKVjtIV70u/PpA6lN03QOrzbmrgNooxuia/2Ht8r6Ljo1WzaM2U4eUAACBzPNnqSdUvWV+/bv9VcWfi1LliZ1uQlMCdTT3df/31lx1aPnHiRCUmJuqqq67SjTfeqK5du8rXN2dXh+WKPOAlSYnSqLpSrJt5P2Yu+IE10rF0ayqbudkt73DMAXf2aBevKg34XipZw3E/NkLaMMWxvW4/erTzochTkXpi7hNacnCJvW8qqD7W4jFdXvXy7D40cF4FACDze7orVKhgi6n16NFDH3/8sfr06aOgoKDz3Q2AvDj/u/1QafoT6doDpEptU9faTivhpFSwpDRsnbRrvlSwhFS5o+S8eJeY4Fh7e/lnjp70Nd9Ll74gVW6fNa8JOcKjcx7V8kPLU+4fOX1ET81/yq7FXadEnUz7ObtjdishKcHul/nhAAAg20L3s88+q2uvvVbFihXLtIMAkEe0vtvRe73ow3+rlzeTOj/hWMPbE7OGd+HSjgrnZqi6CddmvW7DzBtf/mnqY/ctl76+WrpztlQ688IWcq4d0TtcArdTYnKiJm6ZqKdLPG17whftX6Rg/2C1L9/eVjE/35/x5Lwn9U/UP/Z+5ZDKer7t82oW1izTXgcAAMi/zjt033nnnd45EgB5gxlKfjraMXe7RDXHUmPFqzjW60675rZTyVrSh61SK5mbomimKFuj6zNWSHcuKbZ0rNT7be+/FmS7Y3HHPG47evqovtnwjd5c/qbO/Lu8XLECxfR2l7fPOTCb593zxz3aG7s3pW1nzE7d++e9mnbVNBUzy9oBAABchJw9ARtA7mKKpX3UQZr5jLT2e2n2SGl0a0cPdoeHMj6+dm/HfO60S4edOSX9PlzaPN1RGd2dIzu89xqQrczQcbMm957je+z92sVrq4hZk90Ns1zYa0tfSwncxtG4o3p49sNKMFMTPEi7beH+hS6B2+lEwgn9tv23i3w1AADkrnPw2DVj7Xn0vZXvsT53JqL2O4DMM/tVKSZdgDHDxc1SYoN+liq2doRx01td63KpVC1pbCf3+9oxTyoQIsXFZNxWtiGfWh709oq39dX6r+y8ah/52Kqpr3Z4VcOaDdOLi15UslLrfjYo2UBxiXEubU5Rp6Ns0TUz1DytmbtmavTq0dp6bKvCCobplnq3qHBgYY/HY/YDAEBOFh0Xre82fmenYpUIKqHral13QdOjzAohg6YN0uFTh1Paxm8cr097fJqp9VPyK3q6AWSebX+5b98+21Hd3AwzL17NcTNDz0349sQsE9buwYztpvBay7sy75iRI/y05Sd99s9nNnAbJkz/tecvvb7sdV1b81p90fML9avezwbx4S2H2z8C3AVup/Q93Qv2LbBX7k3gNg6dPKTXlr2mPTF7bMB3p0WZFpn6GgEAyOzAfdPUm/Th6g/tKLGpO6Zq8PTB+mXrL+e9rzF/j3EJ3MbxhOP2gvjFOBB7QNujt+sCFszKU+jpBpB5gkIz9nQbpsd69TfSrw9KyUmOtvmjpFZ3S4VKSSdcf8lbNXtIDa+TQspJyz51LB1mqpZ3fEQKLc+nlsf8tPUnt+1miLdZN7RJ6SY2HMfEx9h/m6JpXSp2sT3j6RUKKKRWZVu5tP1v3f/chvSft/2sm+rcpK82uO7HhPs2Zdtc9OsCAMBbJmyaYOuQpGXOdSYo96raSwG+AdpydIs2HtmoikUqqnHpxh73tfjAYo/tSclJ8vU5v77a/bH79eT8J7Xi0IqUKWHPtn5WLcu2VH5E6AaQeZrc5JiPnV6Da6Spj6YGbqclY6Ruz0l/vZK6TrdRo4dU7yrHvxsPcNyQp8W4m0ZgZickntb2Y9s1fP7wlF5qE7iHNh2qAXUGaEDtAfp247cpj/f39ddzbZ5TwYCCdq63uW8454inF3EyQg80fcAOxfttx2+2h7xrpa7qU60Py4YBAHI0Z6B1Nz3KhG0zguz3nb+ntJuL1u93fV+hBUIzPMe0mXNieiEFQjwG7sSkRP2y7RfN2DXDXhi/rPJljvOnfHTfrPvsMTjtitll237t96vCCoUpvyF0A8g8rYZIR7ZLKz6XbHErH6leP6lCc9elv9KKj5XuXSKt/tZRcK1aV6lWT8e638g32pRro23R2zK0NyzZUM8ufDYlcBunzpzSyKUjVa9kPQ1vNVy9q/bW7L2zbRi/vMrl2nJsi6799Vp7Zb90cGndVPcm1SxaU/tiMy5dZ5YHM0uMdQvvZm8AAOQWJYNLum03IdnUMUkbuI1VEav0xrI39FL7lzI85+oaV+vVpa9maO9ZpadGLR9la6WYFT3MlK9ulRzny0fnPmp/jtP8ffO19OBSXVXjKpfAnfb8/ev2X3V7g9uV3xC6AWQeX1/p8jelDg9Lhzc65nAXqyxt+PUsv4WCHPO7uz3DJ5GP3dbgNs3ZO8elR7qgf0H1r9VfTy14yu1zzJy1RqUaqUGpBvbmvOr/wKwH7DreRsSpCI1aMcrup4BfAVt8La17Gt/j1dcFAIC3mAA8ZfsUO/w7rUsqXaLZe2a7fc70ndP1QtsXtPv4bltPxfSKm9FeJnSbYmpmyLqpr+Ln46dLwy/VvL3zXC5a2xopzR5Wo9KNXAK30+Rtk1UttJrHY448Fan8iNANIPOFlHXcnKpfIgUXl04dcX2cGa5U/2o+Adir9RN6T7B/APwT9Y8qFK6ga2pec9blSmITYjO0fbnuy5TAndaMnTP0afdP7VC79UfW27ltpnp5xwodefcBALmSmaP9UruX7BxuUwTNBGXTC21Cdf8p/d0+xwTquXvn6uE5D6cULzVB+YdNP9gipXc0vMNO6zLnSdMrbUJ6eh+v+ViD6g7yeFzmAre/j7/OJKcu6enUPKy58iNCNwDvCwiWrvtS+n5QavD2D5Z6veHo5QYkux73oHqDMoTx4kHF7dqh6ZklwZxX4OuVqGd7u82Ve3fM+t2VQirp3a7v8l4DAPIMM4f6siqXaVf0Ljv8u0RwiZSCoF+u/zLD49uVa2dX73AGbidzwfuHzT/o5no3q3iZ4rbt74i/PV70Tt+7nlaVolU0uP5gjVs7zqW9ZZmW9rjyI0I3gKxRpYP00AZp6x+OpcLM3O2Cjl/qgCeBfoF2ibDh84a7XDFvXqa5Plv7mcs8cHMir1Gshsv8b6cyhcq4LRwDAEBuZ6qUVy9W3aXtzoZ32srjm49uTmkzdU5uqH2D7vnT/dQqM3TchO60505Pc8Yvr3q5vt/8fYaL4qULllbXil1tUTVTe2XKtil2Lrc5R5u53s4Cp/lN/nzVALJHQJBUpzfvPs6LuYJfs1hNWyHVrEnatlxbu5TY8oPLXR5n5q8NrDPQFlQzJ/i0hjQcct7LnQAAkFuZC83jLx9vK4s7lwwzQTk2PtZWF3e3jKYZcXbs9DH7HDNE3EzBmrhlYoZecTNnvHJoZY29dKyeWfCMNhzZYNsblGygF9u9aC+YG2aou7PoWn7nk5zPViqPiYlRaGiooqOjFRISkt2HAwA4T6fPnFbrb1u7nbtdp3gdPd/2eY1bM05rIteofOHydh3u7pW78z57CedVAMhdhswcogX7F2Rov7vR3fr8n8/tcp2GCedmhRAz9HxH9A4F+gba9b/NCDSzNKeTKYJqLmybcy7co6cbAJCrmHlknuaSmSHoIYEhdi54mYJl7C0/rgcKIJ+IPymdPiYVKSv5+GT30SCHMetor4xYqTNJZ2yFcmcPtFky7OHZD9tthhkhdlv92/TFui9SArdhesNNMbXve39v54oXDijsEradTC86zo7QDcA7zCCafSulhJNSxZaSfwHeaWQKc8JvV76dXQ80vRZhLXT9b9fbYeiG6e02S5q80+UddarYiU8AQN5wJk6a8bS06mvHedYsz3nJC1K9ftl9ZMgh/j78tx6Z80jKKiCmKOmItiPsudBcmP6i5xd2vrdZwqt+yfpadnCZjiccd7svM9z8waYPevxZZuWR7zd9r8jTkWpauqmGNBqiKqFVvPbaciMmuAHIfBEbpA9bSp90lb7oLY2qI63/hXcaF+xEwglN2jJJH//9sf3D4MmWT6psoTTL0kn2RB8TH5MSuNP2fr+z8h3efQB5x/QnpKVjHYHbOLpT+nGwtHtxdh8Zcsg0rPv/vN9l2U1T8MwsE3b45OGUNlMvxdRJMSPELpSZzvXswmftEHTz86bumKqB0wbqQOyBi34deQk93QAyV1Ki9N31jj8AnE5GST/eJt3XUCrOlU+cnw1RG3TXzLvssl9OXSp20U99f9Ks3bO0N3av6peob3u/+/7c1+0+TEVzUzymcGBh3n4AudvpGGn1txnbzbQbE8Qrtc6Oo0IOMmfvHJdzppMpjjZtx7QMy3Mabcq2UZGAIm57u7uHu6+LYoqWmjng6ZmL399s+EaPtHjkgl9DXkNPN4DMtXOea+B2MpUv/x7Pu43z9tzC5zL88fDXnr/s1XSzPqkp/NKhQgdbxCWsoPv526Yia5B/EO8+gNzvZKRj6U13ovdl9dEgh44O88SssW0uQv+67Vf9uPlHHTpxKGXq1svtX1aQX+q50hRSu6/xfapTok5KW0JiQsr+9x3f53FIuqmYjlT0dAPIXKejL2wb4Mbe43tTliJJ749df+jamtdq05FNmrt3ri0EY6qqLjm4JMNj+9fqn2/XBgWQx4RWkgqXkWJThw6nqNA8O44IOYwZMu7n4+d2lY/QwFBd+uOlNnwb/j7+GtpsqF2fu0ulLpp5zUzN3D3TDlHvXKGzKoY4iqSdTDipN5e/qSnbHetuNyzVUPc2vlcF/ArYHvT0KoVUyoJXmnvwFwiAzBXeXvIrILn5Bazq3Ry94KYATMmaVFrFfzJ/NHhierZHLR+lz9elDm0zy5mYIG56wk1xGBPEr6t5nf3DAABypbhYKfaQFFJeCgiS/Pylrk9Lk+9zfVzhMKkNv+vyIrNO9sTNE/Xn7j/l5+unXlV6qU/VPvLxULG+TKEyuqfxPXp/1fsu7VdVv0pj1oxJCdzOuicmTLcp18bO8U5Skq16blcKUepKIU/Nf0p/7P4j5f6aw2s07K9huqzyZfplm2vdHhPEB9QecMGvd1XEKjs8fX/sflvkzVwQyO3LkbFON4DMt+hD6fcnXduqdXPMQ9u3zHG/RA3pivek8LZ8Ajirm6beZKuwpndnwzs1ds3YDO1mTtr0a6YrJi7GVmt1t7wJMg/rdANekpQk/fm8tPQTyQznDS4mtR8mtfu3ivTWP6Wl46TjBxzzuNveL4VW4OPIY5KTk3Xvn/dq3r55Lu1X17haz7d9/qzPXR2x2k7FMkuGdavUzfZIP/iX+yrkdzS4wwbcx+Y+ltJzbYaX3934bhvwe03qZZcQS+/W+rfapcgmbJxgp4I1LtXYVjpvXubCRl38uetPW/AtbS99sQLF9E2vb1J63XMjeroBZD5zpb1CC8cc7oRTUo3u0p8jpKPbUx8TtUX65jrpgVVS4VJ8CvDoxXYvasjMIdp/Yn9KW7/q/ezwNnfM/LKVh1aqc8XOvKsAcq95b0kL3k29f+qoNPNZqVApqfEAx+gxc0OetvjA4gyB25i4ZaIG1R2kqkWr6tjpY3bN7aIFiqpJ6SYpPeCNSze2t7TTsjwx63M/Pf9pl6HiJmSPXj1aJYNKug3chumNfqPTG3ZEmQn35zqV62TCSVvUbUf0DtUoVkM9KvewPeRmtZH0w+JNmP9s3Wd6rs1zyq0I3QC8w6zNbW7Oq/FpA7dT/HFpzQSpbbohckAaZq3PKVdN0dw9c3X41GE1DWtqh8CZoeUXMiwdAHIFU4ncbfs4R+jeNN3xGGdPt+kBN+t1I08xQ609WRGxwtY0+WD1BylhuVpoNb3f9X23vcJmCHmhgEJuC62ZYO2pKNq2Y9vs9K34pPgM2+qVqJfy77SB2/TQ/7D5B3txIDouWq3LttZdDe9S2cJlbVAfPH2wy8X0T9Z+orc7v62dMTs99trnZlQvB+B9Jw6fZVsEnwD+U4BvgLqFd9P1ta+3gdu4rMplbh9rhpS3KtuKdxVA7l5+09P58fhBafln0nf9pW1/ShHrHfc/uUSK3pvVRwovKxlc0uO26NPRemvFWy6909uit+nRuY+6PM6MDDNztE3gfqHtC/acmtbtDW5XeGi4x58THBCsm+relKHdzLO+quZVbp/z9sq39eLiF7U+ar32xe6z4dus33309FG9veJtl8BtmLD91fqvVDjA/dKeYYXcr06SW9DTDcD7KrWRfHwda4imV7kDnwAuSN0SdTWs2TC9t/K9lKFoZj736x1ft/PLACDX8vVzTNPa+28dlLTMKLK/Rrq/wL1otHTZK1lyiMgaPav01Hur3rO9xWlVKFzBY6/wuqh12np0qw227658V5uPbrZDz2+ofYPtbW5ydRNN3zHdhnUzFcsM7zbB3CyvedyMQkzHDP2uXby2KodU1qQtkxQdH6125drZ+dxmePj3m75X1KkoNQtrputqXWd7ub9Z/02G/Rw6eciG79l7ZntcX9wUQ01bINXpYgqz5QTZ3tM9evRoValSRUFBQWrWrJnmzcs4ZyGtuLg4PfXUUwoPD1eBAgVUrVo1ffbZZ1l2vAAuQLFwqfU9GdurX+oosAZcIHPC/+3K3+xV+mFNh+mPa/+glxtA3tDtOcdqIGkVCJWa3OS5F3zf8iw5NGQdE4Q/vvRj1SmeulZ209JNbZun2ibG35F/68FZD9rAbRyLO6Yxf4+xAb50wdIaVG+QHfJtArAZBm4qpL/a4VW76kfaVULMOt0L9y/Utb9eqy/Xf6nW5Vpr/OXj9XjLx+0SnYOmDdLkbZO1YP8Cu29T/NRUNnc3FN0wPd+eLoybOd0PNH1AA+sOTDkOc6ymd75jhY7KzbK1p3vChAkaOnSoDd7t2rXTxx9/rJ49e2r9+vWqVMn92m7XXXedDh06pE8//VTVq1dXRESEzpw5k+XHDuA89XjZMefMzOE2S4bVvlxqfKPkm+3X/pCLzdkzRyMWj1DESccfoL/v+l1vdHyD9UEB5H5VOkh3/Ckt/kg6sk0Kqy+1ucdRSM0EEneBqyhrI+dFZt70932+197je+28abMkmNGhQgfN2DUjw+NLBZfSon2L7HJg6U3YNEFDGg7Ry0tedlnq6+3lb+vDSz6063SbpclMoO9UoZPeWPaGZu2ZlfK4rce2avH+xfq0x6e2tooZtp6W6X1ffmi5DezptxmVilSyx/ftxm+V3uVVL7ev77EWj+n+JvfboegmdJ9rcbacLFuXDGvVqpWaNm2qMWPGpLTVqVNH/fr108iRGYfNTJ8+Xddff722b9+u4sWLX9DPZGkTAMgbzByxK366IsPVdFN47Ze+v3hcvxSZi/MqcA52LZK2/yUFhUoNrpUKl764t23qoxkLrZkCkrf+LlVswUeST5je6ftn3a8F+xaktJn52qaa+CdrPtE/Uf+4fd7TrZ7WS0teytBesUhFO3rMef5cF7lO1/92vdt9PNnqSb2yxP1UBlNXpURQCbtcWVqFAgpp4hUT7VD3oX8NtZXZnTpX6Kw3O79pe7vzomy7bBAfH68VK1boiSeecGnv3r27Fi5c6PY5kydPVvPmzfX666/rq6++UqFChXTFFVfoxRdfVHBw6lAIAEDeZ4azuRu+ZuaXrTi04oLXCAWATGP6tn6+R/o7Ta/eny9K138tVb/kwvfb4xXJDNFd8YVjJZCSNaVLnk8N3GZE2YZfpWO7pHJNpKpdpHO5EBm9z7FU2Y45UnBxqdktUpMbL/w44VUmYH/Y9UP9tecvG2BDC4SqX7V+tnK5CeLuQrcpzLb04FK3+9tzfI82HtmoOiXqpMwN92Tf8X12pZD0y3sZJnCPaDdCJYJL6KctPyk2IdbO93642cO2+Joxrvs4G+q3R2+3BVJrFa+lvCzbQndkZKQSExMVFuZaic7cP3jwoNvnmB7u+fPn2/nfP/30k93HPffcoyNHjnic123mgJtb2ivyAIDczww788TMXYN3cF4FzsPG31wDt2GGhf98rzRsneTnL22f828veFGp4XVSSLk0/8Mdl7b+4ShEakK66Sk3/AKk7i85wvTRnY71uotXcWwz97+4whG40xYtvfEHKSDYURl90zRp/0qpaLhU/2qpQGHpRJT0aXcpJk0F9L1LHfvp8iQfew7l5+unS8Ivsbe0bql3i6bvnJ6hMJoZWm7W9PbEDAtPW6zNE1N8rWulrpq5a6ZLu498bDE002Nthok/0vwRu363u3nc9UrWs7f8INsHyKcf/mdGu3saEpiUlGS3ffPNNwoNdfzSGTVqlK655hp9+OGHbnu7zTD1F154wUtHDwDILmb42ncbv3N75d+s5Q3v4LwKnGfodif2oLRnibTsE2ndpNT22SOl/l9LNS51BONJd0px/3YYBRSS+n4g1b9KOrZH+vY6x3Jhlo/U4nap1xvStMddA7exc5608AOp9d3SV1c6wrTTX69IN/8qbfjFNXA7LXzfUQw1uCgffS5SKaSSvu75tcauHau/I/62S27dWOdGXRp+qZ0nnX7ot3N61q6YXfpg1Qc6nXjazumuVrSaXac7LTOnvHvl7jZ0G2YOuJm/bZbsHNp0qMtIMxPiA1lRJPvmdJvh5QULFtQPP/ygK6+8MqX9wQcf1OrVqzVnzpwMz7n55pu1YMECbd26NaVtw4YNqlu3rjZv3qwaNWqc0xX5ihUrKjo6WiEhIV55bQAA70tMStR9s+7T/H3zXdofbPqgrWYO7+C8CpyHyfdLK790v63rs9KsERnbi5SV7povvdtASjjpus2El6FrpZ/ukra7WXapz/vSlAfdL9FpCrHV7i3NeTXjtqqdpcDC0sYp7o/1tpmOpcqQZ4xcMtKlmJkJzGYZsF+3/+ryuCalm9jh4qbKeZKS1LZcWw1vOdylYOnhk4d15PQRVQ2tqgAzCgM5p6c7MDDQLhE2c+ZMl9Bt7vft29ftc0yFcxPSY2NjVbiwY+F0E7Z9fX1VoYL74Q9mWTFzAwDkvSF173V9T1O3T7Vre5rlRfpU62OXQIH3cF4FzkP9a9yH7uJVpcMb3T/n+AFp8eiMgdtIjJdWfe0+cBv/THT0ertjRpJ66nk3Q9xb3uHheb5SqOdhxsidhrcarv61+tu54MWCitklyfr90i/D41ZFrNI7Xd7Rqx1ftb3ZaZcUcypVsJS9wbNsXavnoYce0ieffGLnY5se62HDhmn37t0aMmSI3T58+HANGjQo5fEDBgxQiRIlNHjwYLus2Ny5c/Xoo4/q1ltvpZAaAORhm45s0hPzntDVk6/WI3Me0drDa1OGkvet3lf3Nr7XLiny/sr39dT8p+zjASDbVe0kdXrcEVydCodJ13wu+Z+tU8hNT7VT/ImzPO2MVPMy99vqXemYQ+6Or5/U9GYpoGDGbWbOd9p55sgzqhatqgF1BqhnlZ7acGSD26JohilOauZouwvcyAVzuvv376+oqCiNGDFCBw4cUP369TV16lSFh4fb7abNhHAn07ttesLvv/9+W8XcBHCzbvdLL2UseQ8AyBvWHF6j236/zc4vMzYf3Wznj310yUd2Xreprjp4+mC7pqh9fOQa/b7zd429dCxzuwFkP1OErMlNjt5kMy+6RndH4DZF01Z9lfHxphK5mZ+94D0pQwjykRrfKG2dKR10XHx0Uftyqe4VUuRmKWpLanv1S6U290n+QdL+VRmfV6uXVKa+dNMkacbT0r7ljuHmjQdIl7oZAo88x1Q198Ssq41cvE53dmA9UQDIXe754x7N2zcvQ3vT0k31Rc8vdN+f99nh5em1KNNCn/Vwv7IFMg/nVeACmKW5TGg2lclXfO7ooTZCKkgDJkjFwqXln0kznzNlhlOf1+VpqdOj0t7l0tdXSaej01Uo/1EKCJISz0hbZjgqmZs5tttmSUe2O+Z1nzoqbfsz9XlhDaTeb0uFSjiGvTurppuAfiJS+vs7KTZCCm/rCPWmVxx5jomEV02+SluPpdbOcq6tPeXKKWcN5fhvhG4AQI7Wfnx7Rcel+cPyX2Y4+aqBq9Tuu3aKic+4HKQZer5yoOdlUZA5CN3AeUhKkqY95gjUzl7syu0dc7/NEG4z9Nz0NJtq46ZomlkSrFRtR8+46cEu0yB1X2aJrzUTHHPAK7WWStWRjm53PN45B9tUQB9/o2uPuRlCbqqgm2HqpuDask+lg2sc28o0lPqNcfR675gnfdtfSkgznL1KR0ewP+vQeORW+2P36+kFT2vZwWX2vimM9mybZ+0a28jlS4YBAHA25QqVcxu6yxYqa/9rlkFxF7rDCobxxgLIWVZ8Ji0b59q2c76jh7luX+mD5tLJqNSiaVt+dywZduv0jPsyPdNt7pHOxEm/3CutvdHRK27mjzcaIPV5V5r1csYh6qZA27qfpKvGSe82kmIPpW4z4dv0oD+wSpoy1DVwGzvmOgrDeSq6hlytXOFydoTYwRMHFZcYp/AQx5Rf5PJCagAA/BezrujZ2m+s7X67KQ4DADnKqm/ct6/5wVGV3Bm409q9SNq7wvHvhNOOYeLxaSqbz35VWvtD6jB003u9+mtp3pvSITfzvo19q6QNU1wDt5NpM+uHR7kOM05hes+Rp5l1uAncmYuebgBAjmaqk8cmxOqTtZ8o8lSkXUu0VZlW+nHzj3p92et2+JupvLpo/yIdizumIoFFNLDOQN1U56bsPnQAcGXmSrtjCkEe2eH53Tq2U9o5V5r/jnT6mBRYRGp1p2OOtwnr7vw9XipcRoo9mHFb0YruA7dT2rni6QVQwRo4X4RuAECOZ3q1zXqiJlQvObDELh/mZIq+mNurHV5V49KNbbEXs7QJAOQ4NS51rSruFN5eqtTKMfw8Ax/p6C7pzxdSm+KPS/PekgoUcQw/9xSc2w+VZj6bcVube8++9rYpmLZ7ibRrfsZtja73/DwAbjG8HACQK5jCaSZQf7HuC7fbTXv5wuUJ3AByrg4PSyVquLYFhUo9Xnaso20qiadnlhtbN8n9/paOk6p1c7+t+iVS2wekrs9IBf+tPB1a8d8lx/6Qtvwh1emT8XkNrpXKN5OuHCOVrpva7uMntRvq/jnINaJORWnv8b3ZfRj5DtXLAQC5SstvWqasyZ1WsH+wlt64NFuOKT+jejlwnuJipTXjHetlF6ssNRkkFfm38KNZzsusz73xN8nXX2p0g6NX+q2a0onDGfdliqbds0T6vKd0MjK1vUhZR/E1s3/DLCEWs9dRyfzQP6mPCyjsKMZ24G/H/Xr9pIbXS77/9suZlYV3LXQMRTcV0k2FdeRKZnrW8wuf19y9c5WsZDs166lWT6ll2ZbZfWj5AsPLAQC5So1iNbTm8L/L26RRs1jNbDkeADgvBQpLLW53vy24mBRcVDoR4Qjgs0dKJw5JFVpKm37L+PiKZqmwmtI9i6VVX0qRW6TSdaQmA6WCxVMf5+cvrf7WNXAbCbGO9bzvmuP+eHx8pMrt+IDzgKF/DdXfh/+9uCJpe/R23TfrPv3S9xeVLVxWZ5LO2NooZjWQVmVbsS53JiN0AwBytOTkZPuHwOrDq1W6YGndXPdmPTr3USWZCr3/8vXx1Z0N78zW4wSAi2aCcdo52GbJroXvO0K6KZ5m5nI7+QdJre+R9q92rM1thq6nFbXN0Ttu1vYOLOQI1+4cWC3FRkiFS/MB5lHro9a7BG4nM2rsp60/6ZLwS3Tfn/fpwIkDKdO57m18r25v4OHiEM4boRsAkGPFJ8br/ln3a+H+hSltpnr5ky2f1PSd0+2V+gqFK6heiXq2yFpsfKwKBxbO1mMGgAu2dKz79vW/SHfOlhZ/KB1a5xg2boZ8/zDIsUSY6SHv8pRj/ewTkdLE26Ttsx3PLRAqdXtG8vS70QxjNwEeeVbEyQiP2w6dOKRH5jySErgN0+v97sp31bhUYzUv09yu2T1z10y7fnfDkg0Zkn4BCN0AgBzr+03fuwRu48jpI5q8bbK+ufwbu4zY+6ve15rINdImqXBAYY3qPEptyrXJtmMGgAt23MMyXqbH2gTt3m877k+8PTVUG2Yo+tRHpOJVpSUfuW6Li3Zsaz9M2jnPfaXyoBA+tDysfsn6CvANUEJSQoZtYQXDtCPa/XJ1v+34zQ4zv2PmHTZwO7Ur107vdX1PgX6BXj3uvITq5QCAHOuP3X+4bTche96+efZKfNph5mY978fnPm57yAEg1zHFytwxc7rNvGxnwF73k/vHLR4tbZnpflv0PqnNfY6ebafwdtLl/wZ55FkmOA+uPzhDe/0S9dWsTDOPz4s7E6eXFr/kEriNBfsX6NsN33rlWPMqeroBADmWv4/n09TcPXPdth+NO6rFBxarY4WOXjwyALgIe1c4iqTtXSqFlJda3SU1u0Xq9Li07U/HGttOfgWkDo+kzrs+dUxKOuN+v+YxSna/7dQR6epxjmroZh54aHmpbCM+xnzi/ib3q3bx2vpl6y86Hn9cdYrXUc8qPVWnRB2VCCqhqNNRGZ5jCqo9veBpt/szw81vqX9LFhx53kDoBgDkWOYPgiUHl2Rob1Wm1VmHtaXt/QaAHOXQeumL3lLCScd9E7B/fdDx33YPSnfOkRaPkQ6udQRjM7R8wgBH0DYh+bLXpaKVpGO7M+67RnfHnO7j+zNuq9pZ2rfSUWAtrJ4UlmYNbuQLl4ZfqrKFymr4vOH6ZuM39lY5pLJurX+rHTkWn5Q6SqxH5R724rWPfOwSY+n5mMr2OGcMLwcA5FhX1rhSV1S7wqUtPCRcz7d93v7x4E6RwCL26jwA5EiLPkgN3GkteFc6Ey8VryL1el26dZpjSLiZn+3s2TbraX9zjaPnO+0wcaN4NUc188tekXz8XLeVridtmiaN6yJNul0a00aacJN0Js6LLxQ5zcmEk7r7j7u1M2ZnSpv5t6mP8mOfH/Vg0wfVs3JPtS/fXn4+fvaid+uy7qc8XFb5siw88tyPnm4AQI5llgJ7uf3LurnezVodsVplCpWxBVz8fP1UoUgF3Vb/Nn36z6cpjy/gV0AvtXtJwf7B2XrcAOBRxHr37SejHOtzh1Zw3D+wRtrtWkjSMsuGmWHkt/8pLf9UOn7QMRe8+a2OKub1rpRKVJdWfumocF65g6PXfOUXrvvZ8Ks0/x2p8+N8WPmoTopZ6cPdtKy1UWvtv6ftnJbSPnXHVLUv114Vi1TUnuN7Utq7Vuyq/rX7Z9FR5w2EbgBAjlezWE17S29os6HqXbW35uydY4O2GQ5XIrhEthwjAJyTkrWk/asytpvAXKhU6v3ovZ73EbNXKtdYuuJ999vN2ty93ki9P7Ki+8etGU/ozkeOnj7qcdve43ttj3d68/fP1+iuoxWfHG8LqjUo2UANSzX08pHmPYRuAECuVr1YdXsDcpKkpGR9PHe7vl68S5GxcWpdtYQe7VFL9cuHZvehIbuZQmam+nhiuqHdTW+WlnwsnYyUKneUyjZ2DCF3VzStQgvp5BFHJXOzlJhvuuHkJrBHbHAsIVaimpRwyv2xJJzOxBeGnO5sU6+C/ILcLilmLD64WI+2eNSLR5b3EboBAAAy2StTN+iT+alr387ZfFgrdh3Vbw+0V3iJQrzf+VnZhtKgX6S/Xpb2LHFUL69+qbR0nJRwInV+d82eUss7HcuApVW6rmNZMFN8zQTy0EpS9xelev2kpERpylBp1deSLSjpI9XpI9W4VNo0NeOx1OqZNa8ZOYKpXn51jas1cctEl3bTZrZ5ElqAi4UXyyc5OdnDugJ5U0xMjEJDQxUdHa2QkJDsPhwAAHI1zqsZRZ9KUKtX/tDphIxV9Ae3q6zn+tQ75/d32+FYjZ2zXWv3Rati8WDd2q6KWlVlCkWekpQkvd9EOppa3CpF39GOXuzV30hxx6UaPaSDfzuKoqVlCqfd/oe0c54089mM+2kyUNo2S4rZl9pWsqZ0y1SpcJoh7cjzTPQzc7tn7Jxh73ev3F2XVLrErvpx+U+Xa19smu+IpEDfQE25corKFi6bTUecN9DTDQAAcIG+W7pbn8zbrj1HT6lRhVANu6SmihYMdBu4ja0Rsee8760Rx3Xl6IU6ftoxvHj9gRjNXH9IY25qph71yvCZ5aXCau4Ct2F6p6//Rmp0veN+zAHpbTdLfSUnSis+l3ZnXGLR2jBZGrZOWvuDFLlVKlNfqneVFBCUiS8EuYFZ6sus/pF+BRBTrfyDrh/okTmPaFv0Nttm1u9+rs1zBO5MQOgGAAC4AJ8v2KEXfk2tRL1s51EN+mypPh/cXMEBfjqVkJjhObXCipzz/kf/tS0lcDslJUtv/r6J0J2X+BfwvM0v0PW+qUZuh427YaqYx8W43xYXKwUWdlQ4Bzww9VF+7vez1kWtU9yZODUo1UABvgG8X5mAdboBAAAuoFDaR3McvUFpnUlK1leLduvmtpUzbAsJ8teNrSspIdF9aNp+OFYLt0Uq+qSjmNGqPRmX9jG2RMQqNs5NcS3kTiVrOIqmuVO1izTjGenb/tIfz0sFikjBxd0/1iwbVr2b+22m3ccn844ZeVq9EvXUNKwpgTsT0dMNAABwnkwP9KGYdNWn/7X1cKw+HthMpYsU0NdLdinyeJyaVComX1+px9vzlJicrG61S+uZ3nVVsXhBOwf8ge9W2WJrRlCAr+7pXF3ligZpR+S/hbXSKFYwwPakIw+5+hPpm2tSh5n7+EqNbpBmPC3FRTvaNk+XVvzPUVxtzmuuzzfrcpte7PgT0vY5UnTqmsoqWEK6dEQWvhgA6RG6AQAAzlORIH+VDQ3SgejTboeQm3mTt7avYm+mcFHv9+dr3f7Uob8z1h+yc7T/eKiTnv75n5TAbZj54KNmbtaQTlW1YGtUhv0PalPZ9pb/svqAnSNeM6yIejYoowL+BPFc3dt9/0pp+1/SiUgpvJ00ZVhq4HYyS4SZpcBM9fPlnztCeqlaUqcnHOt8m9uQ+Y7Ca2Z+t3+g1PhGqXSd7HplABheDgAAcP58fX10T5eM68MH+vnqxlbh2nTwuE7/O6fbBOe0gdtp79FTmrhyr6atPeD2Z2w4cFyvX9NQ5UKDUoL+fV2q6/oWFdXz3Xl66Pu/NXr2Ng2dsFq93p2niOOsuZyrmSrl1S9xFE0rWlHaMdf940x7WH1HOD+wWlozQRrTRpr9b+93gRApcrO08VdH4bSv+kn/6+0I7ACyBT3dAIBc69jpYxq3dpzm7p2rYP9g9anWRwNqD5Cf+eMVOdbx0wn6Yflerdl7zA6vvr5lJZUvGqzcZmDrcBUK9NO4eTu09+hJ1S8fosIFAnTrF8sUfybJzuG+t0t1FSrg+c8tM4/bzAN3xww7v655RV3dtIKiYuMUWjDA9mY/9P3qDMPOtx0+oVEzNuvVqxtm+utENilU0nWJr7Ttkx+Qds1PbTtzWpr9ihRW11FQzQxDT8ssJTZ9uHTlR94/bgAZELoBALnS6TOnNfj3wdp6bGtK24YjG7T56Ga92O7FbD02eHb4eJyu/WihdkadTGn7fMFOfXlbSzWtVCzHvHV/bYzQmDnbbCiuVaaI7u1cXW2rl8zwuKuaVrA3Y+TUDfp47vaUbTGnz2jktI0admkNjz+nVZUSmrs50hZHS69jTcf6yX6+Piodkrq008x1h9zu6/d1BwndeUmzwdJfL2Vsb3CdNOdV989Z9bUUG+F+2z+TpD7vnr1aOgCvoHo5ACBXmrZjmkvgdvpl6y/aE5OmiBBylDGzt7kEbsNU4n5xSurSW9ltxrqDtrd66Y4jioyNt8PDB362VAu2RtrtZtj498v26Mmf1urDv7YqIua0EpOS9e3S3W73N39LpLrWLp2h3azr3a1OmJ6/op4K+Lv+SVajdGHd2q6yXav72V/+0eDPl9p53uaiRUC6xzoFemjPaj+u2KtLRs1R9Senqs/78/XHevcXCfAfOjzkKJrmXDbMP0hqc5/U8FrPy4adjpESXP//SpEYJyU6KuMDyFr0dAMAcqX1Ue5DWrKSbY93xZCKWX5M+G9zt6QWDEtr1e5jdth5kaAA7Tt2Sl8s3KkNB2JUtWQhu/xW1VKFUx5rCpPtOXJKhYP8VbxQunWMM8F7s7YoOd2IbxOqP5i1VfXLhar/2EXaePB4yjazdNi4gc0zrKntZILyV7e10ti52zX57/12Xz3qldG9XarZbeZ13tiqkmJOJSg+MVlNKxXVtc0r6u+9xzT482WKO+MIWH9tOqwJy3bbAG+CbXpXNCqX2W+FPVZzEWLBtkgVKxioa5pVUHiJQnbbibgzenvmZvuazBD5HvXCVKNUEY34LfX/zbX7onXnV8v1+eCW6vRvzz3OkZkm0+sNqeNjUsQ6qUwDRyVyo2RNx7xtd0uDnY6WDm/MuC28vVQg9f8jAFmH0A0AyJUqFHEM6XWnYhECd05l5jl7YuYpFwz00zUfLdKxf9eqnrclUj+s2Ktvbm9ll90yVb6fn7zOPtbXR7qkTpgdUu0M32Zu9W9rDtjq3t3rlbGVvc/XxgOpgTqtDQdjNG7edpfAbZiw/drvG+2c7n/2xbgdQh4U4KcHutWwN6cpa/Zr2ITVSkhMTfh3d66mW9pVsf8eOXVjSuB2MsuU+fr4qHXV4lq8/UhKe/vqJTXs0poeX9OZxCT7Xh6OjVOrKsVTgrNzpMGklXtt+K9copAN/Ob9NO/hbV8s19w0ldU/nrNdHwxoYt/b279YrkXbU6urf7d0jwL8Mq4FbaasfzxnG6H7QiweI81/R4o9KIWUlzo+4lgarOfr0nfXO+ZyO5Vp6OgZTzojbf1DikhzYdJUNb9s5AUdAoCLR+gGAORKfav11Wf/fKYjp1ODh9GyTEvVKcHyODlVrwZltXL3MbfbJizbo2OnElICt9PJ+ES98fsmvXxlA93x5XJbpMwZ5szSWyfiV+qb21vb3t/HJ66xvbPGmzM2a9glNfXgJY6gu2R7lL5cvEuHok+rWeViuq1dFZe50k5VShZyO8fa9LrP2hjhsad+zI1N9eD41YpPTA3KJQoFqlbZInpl6gZVKl5QfRuXs735Jug+/uMal8DtHH5vesGrly5se4ndWb7ziGY90lkrdh3RtogTqhFW2F6QWLw9ygZ500tv3ud2/85BN/PSb/l8mXYfcQw79vGRbm1Xxa4TfijmtK79aFHKNsNcWBh/Z2v7mtIGbsO8NrPEWWhwgEvgdkr/epy2Hc74fuI/LP9Mmv5E6n1TVM0sIxZYWGp4nXTPYmnll47CaZVaO9oC/i1IeMcs6Z+J0r6VUrFwx7JhpgAb8K/9sfs1edtkRcdFq025Nmpfvr18zfrw8ApCNwAgVyoaVFSfdP9Ery97XUsOLFGgX6Auq3yZHmv5WHYfGs6iddV/h8e6YYaVu+spNkygHL90d0rgTsvMuV6x66ie+mltSuB2evuPzepRP8z2Xg/7fnXKsPHlu45q8ur9+uXedhmCt+ltNstxpXdXp2r6dN4Ot8dneng71SqlX+5rpy8X7bQhtkbpIlq4LVIjfk3tcXzvzy367s7W2nLouE7EO5YUS2/6Pwf1cPeatjK6u8eUKOzo1W8WXtzejDd/36QP/kqtcfDNkt02WD/bp66Gff+3S6g278Gn83eoReVimrsl0mWbYeaxvzJ1o4IC3P8BHnE8Tn9t8lCsy4NaZULO6/GQtOhD92/Dog8cAdvMz06Md/R2n4mTktJ8V0z4bnKT4wakM2fPHD00+yHFJ8Xb+19v+FpdKnbRqM6j5O9LPPQG3lUAQK5Vo1gNjes+zlYyN8uEBfgGZPch4T9UK1XYrjftbv5zowpFdTD6tCJj4zJsK1G4gA7GeF6H+s/1hzIMxXaatvagfli+J8M87QPRp/Xpgh0a3rOOnUP+2YId2nf0lBpXLKohHatq5oZD2h55QrXCiui+rtVtD/Sxk/FautN1dIXRs35ZFQz0V52yIRp5lWPZrpHTNmjzodgMgdWEcDOH2xN/Xx8F+PnaYd7/W7gzw3azDnhaZqj9h7MzFhU0r6d9jRL6e4/7kQW/rN6v1R62zd4UoSublPd4jOczbN+8nns6Vzvnx+NfR3d5bt8yUxo/wBG6jXWTHMuEDZ4qBRflLYRHCUkJemHRCymB2+mvPX/p952/6/Kql/PueQFjCAAAuV6QfxCBO5cIDvTT/V2rZ2gvFxqkgW3CdWNr10DpdFOrcDWvXNz9PgP8FF4ydY5yeqbg1/5o94F9+c6jthDac5PXaVfUSVsQzPSCmzD+5rWNtGPk5Xrrukb6fd0hW5F7xrpD6lm/jF3Gy8kUPjNzrP/ccMjOg3aa6aFqtykm17JKcRUr6P4iUcMKoXr3jy32dXWrU9qGVqNwAX87PN0Mw+/65mw9/P3fdui4GQKe/oKC08KtGYeAO5lRA57WEDcXEK5p5r42gqm6bpZJa+nm86haqpA+uKGxvXBRtGCA2lQtoS9vbXnWEQ7woHxTz+3THk8N3E6m2NrScbydOKt1ket0+NRhjz3g8A56ugEAQJa6s2M1VSxW0A6BNr3aJpAN6VRNJQsX0MDW4XYJLjP82czlNktp3dCyku1pNiHx2yW7bcGvtMy2yxuW1UtT1ut4nGsPusmrpsfWzOV2NzS9VJFAjUuztnbaucmfzNuhIZ18de3HC3U6wfHcrRGxNnC/dnUDhQQFaMmOKH25aFfKPPUyIUEaN6i5GlQIVQF/P7ev3/Rim7D7/g1NdffXK1KO2YTrS+uG6a6vV7iE6NvaV1G/xuW1bGeURkzZkNJueuH/2HDorL3I4SUK2rno5rHpda8XpqMnE/TqtIyVrq9uWl5tqpXQ8J617VJlzlEENcMK670bmth/f3pLc701Y7N+Wb1PZxKT1aN+GT12WS2VLhKk3o0895LjHHUeLn1zjaMwmpNZPqzpIOmHW9w/Z9ufUqdHeYvhUbD/v/P+PVzAjjoVpRm7ZijuTJw6VeykKqGOwo64OD7JZt2NfCQmJkahoaGKjo5WSAjziwAAyInnVVNobFfUCVUoWlChaXqEY04n6KtFuzRn02GFBAeof4uKNqgaszYe0v3frkqZB23mWZs1sM1w7CcmrtH4Za7rt5uCYmNubKYhX69wewy1yxSxxc9Msbb0TKXyF/vW15WjF2bYVrF4sOY80kWfzN9u50an16dRWVUpWVjT1h5ICbIm4DapWEz9Ri9wW4xs6gPtbTE0Mzw9PfMemH3FpBuybyrBL3yiqy0Kd+vny1wuSFxSp7TG3NRMpg/9iUlrbfVy53R4s80Ea9PbbRw9Ea9lO4/YiubNwovJx7xxyBq7F0sL3nMsDxZWV2r3oFSsivRGdSnZTU2AWr2kG77j08FZXT35am0+mnHJuXsb36txa8a5DD2/u9HduqfxPbyjF4nQDQAA8szFbLPW958bImyvdpfapVWqSAHbfjoh0S41NmnVPrvN9EibXtm+jcurzcg/3YZZM5TbFHAzy3S5M6hNuO3lduf7u9rYYeemiNmvf+9PaW9S0THfdlW6udSda5VSr/pl9djENW73d3uHKrbn3Z1GFYvqyZ619cD4VSnHakYNvN2/kTrUcKyNfeREvH5etc+OLDAB3ywxljY87zly0o4gMJXba1zAMmvIYhMGShsmZ2y/YbxUqycfB85qe/R23ffnfdpz3HEh0tRDub3B7fpq/VeKTci40sD43uNVr0Q93tWLwPByAACQZ5jluPq5KQBm1sk263k/eXkd23Nbvmiw/P0cpW3M8Ozn01QYN8yw9js7VtXeo6fchu6yoUEu87fTO51wxs4DTziTZOc1Vy5ZUFc2qaCT8Wdsj3V6szcdtnOlPSkWHOixmnnFYsFqVbWEFjzeVct2HpUZxNiiSnE7jN3J9FLf2t7zMNGKxQvaG3KJPu9K8bHStlmO+wGFHMPKCdw4B1VDq2rKlVO09OBSu2RY87DmWh2x2m3gNmbunEnovkiEbgBArhWXGKdJWyZpzt45KuhfUH2q9lGXSl2y+7CQg5l52OaW1i3tqtih6s7q5abn+MFuNVSvXKhub1/FLkeWnplnbYqGfbfUdci6YQqkTfvnoMs2s6a1Gf5dp4znXmRTOM0USzND69Myc72vbFpe0acTNDbd/HMzv3xwO0eYNhcRTC828oGCxaWBP0lR26TjB6QyDaQgzxdtgPTMmtyty7ZObTjLrBHW7754hG4AQK50JumM7v7jbi07mNprOHPXTN3Z8E7d3+T+bD025D6mGre5pdezQVlbNO29P7fadcRLFg7Ube2r6vYOVW2P8tVNK2jiyr0pjw/089X9XWtoxBTXnnPjtzUHVK2U5yrrNcsU0QcDmuiB71alzM82QXzkVQ1UrmiwHr+stt3/V4t3KfpUgp0L/liP2naeNfKpEtUcN+AitS3XViGBIYqJdy1UafSo3IP39yIxpxsAkCvN2DlDD895OEO7v6+/Zlw9Q6UKOuayIn/N6faWpKRkW8TNDF9Pu1yYYeZ9m+HhZv1xM7R91sYIPfPzP27380C36vpp1T7tOXLKpb1G6cKaPrSj3bcZgm72Z5Yv61SzlEKDXXvmzyQm6VRCoj0WAMgsc/fO1SNzHtGpM6dSergfaPKAbmtwG2/yRaKnGwCQKy0/tNxjD/jqw6t1afilWX5MyLt8fX1UtGCg221mybO061CXLOT+ccvH3EsAAB3ASURBVEaZkGB9e3trvfDrOhvOfX187NJdz/WplxLmTdXwXg3KetyHGUZeJM18bQDIDB0rdNTMa2Zq1u5ZOp14Wp0qdFK5wuV4czMBoRsAkCuYobwmaJtqq7WL11aJIM9zV0sGl8zSYwPS6lqntK2OfjDmtEu76bHu3aisnVP+yc0tbEV1U0Dc03reAJDVQguE6soaV/LGZzJCNwAgxzt6+qju+eMe/ROVOmS3Tdk2CvILslfj06pZrKaalG6SDUcJOJgQ/cWtLfXQ96u1br9jfmTVkoV0b9fqSk52ragOAMj7mNMNAMjxnpz3pH7d/muG9n7V+2lVxCrtinGslWyWPXml/SsqW9jz0Fxkrvwyp/tCbYs4rrHzdmjy6v12HrZZimxAq0p6+vK6GeaGAwDyJnq6AQA5WmJSon7f+bvbbWsPr9Wv/X7VjugdCvYPJmwjx5m3JVITlqUuHRZ3JkmfL9ipkoUL6N4u1bP12AAAWYMqHACAHC1ZyUpMTnS7LSEpQT4+PqpatCqBGznS10t2u29f7BidAQDI++jpBgDkaGYJsA4VOmj2ntluK61+uvZTu8yJ6enuU62PLq96ebYcJ+BOVGyc2/ZID+0AgLyH0A0AyPEea/GYNh3ZpAMnDqS0mQrmZj73uqh1KW0L9i/Q+qj1erTFo9l0pIArs5TYtH8OZnhb0i4xBgDI27J9ePno0aNVpUoVBQUFqVmzZpo3b57Hx86ePdsOI0x/27hxY5YeMwAga1UsUlG/9PtFI9qO0B0N7tCozqN0c72bXQK30zcbvtHBExlDDpAdhl1a0y4VllahQD891qM2HwgA5BPZ2tM9YcIEDR061Abvdu3a6eOPP1bPnj21fv16VapUyePzNm3a5FIhtVSpUll0xACA7GKGj6ddO/TlxS+7fZyZ/702cq3KFCqThUcHuFczrIh+e6C9vly0SxsPHle1UoV0c5vKqlyyEG8ZAOQT2Rq6R40apdtuu0233367vf/OO+/o999/15gxYzRy5EiPzytdurSKFi2ahUcKAMhpwgqFedxWumDpLD0W4GwqFCuoJ3vV4U0CkKOdPnNaH6/5WJO3TVZcYpw6V+is+5vcf9bzLXL48PL4+HitWLFC3bt3d2k39xcuXHjW5zZp0kRly5ZVt27d9Ndff3n5SAEAOVHfan1VKCBjb2G9EvXUqFSjbDkmAAByq0fnPqpP1n6iiJMRio6L1i/bftHg3wfr1JlT2X1ouV62he7IyEglJiYqLMz1yom5f/Cg+7l4JmiPHTtWEydO1KRJk1SrVi0bvOfOnevx58TFxSkmJsblBgDI/UoVLKWPLvlINYvVtPd95KMO5Tvo/a7vZ/eh5WmcVwEg7zHFSt2tErLn+B5N2zEtW44pL8n26uWmEFpaycnJGdqcTMg2N6c2bdpoz549evPNN9WxY0e3zzHD1F944YVMPmoAQE7QuHRjTbxiog7EHlAB/wIqHlQ8uw8pz+O8CgB5z9ZjWz1u23J0S5YeS16UbT3dJUuWlJ+fX4Ze7YiIiAy932fTunVrbdni+YswfPhwRUdHp9xMSAcA5C1lC5clcGcRzqsAkPdUK1rtgrYhh4fuwMBAu0TYzJkzXdrN/bZt257zflatWmWHnXtSoEABW+k87Q0AAFwYzqsAkPfULl5b7cu3z9BevnB59arSK1uOKS/J1uHlDz30kAYOHKjmzZvboeJmvvbu3bs1ZMiQlKvp+/bt05dffplS3bxy5cqqV6+eLcT29ddf2/nd5gYAAAAAuDBvdXpLH67+UFO2T7GVzDtX7KyhTYeqYEBB3tLcHLr79++vqKgojRgxQgcOHFD9+vU1depUhYeH2+2mzYRwJxO0H3nkERvEg4ODbfj+7bff1KsXV18AAAAA4EKZcP1oi0ftDZnLJ9lULstHTPXy0NBQO7+boeYAAHBeBQAgT87pBgAAAAAgryN0AwAAAADgJYRuAAAAAAC8hNANAAAAAICXELoBAAAAAPASQjcAAAAAAF5C6AYAAAAAwEsI3QAAAAAAeAmhGwAAAAAALyF0AwAAAADgJYRuAAAAAAC8hNANAAAAAICXELoBAAAAAPASQjcAAAAAAF5C6AYAAAAAwEsI3QAAAAAAeAmhGwAAAAAALyF0AwAAAADgJYRuAAAAAAC8hNANAAAAAICXELoBAAAAAPASQjcAAAAAAF5C6AYAAAAAwEsI3QAAAAAAeAmhGwAAAAAALyF0AwAAAADgJYRuAAAAAAC8hNANAAAAAICXELoBAAAAAPASQjcAAAAAAF5C6AYAAAAAwEsI3QAAAAAAeAmhGwAAAAAALyF0AwAAAADgJYRuAAAAAAC8hNANAAAAAICXELoBAAAAAPASQjcAAAAAAF5C6AYAAAAAwEsI3QAAAAAAeAmhGwAAAAAALyF0AwAAAADgJYRuAAAAAAC8hNANAAAAAICXELoBAAAAAPASQjcAAAAAAF5C6AYAAAAAwEsI3QAAAAAAeAmhGwAAAAAALyF0AwAAAACQV0P36NGjVaVKFQUFBalZs2aaN2/eOT1vwYIF8vf3V+PGjb1+jAAAAAAA5LrQPWHCBA0dOlRPPfWUVq1apQ4dOqhnz57avXv3WZ8XHR2tQYMGqVu3bll2rAAAAAAAnC+f5OTkZGWTVq1aqWnTphozZkxKW506ddSvXz+NHDnS4/Ouv/561ahRQ35+fvr555+1evXqc/6ZMTExCg0NtcE9JCTkol8DAAD5GedVAAByaE93fHy8VqxYoe7du7u0m/sLFy70+LzPP/9c27Zt03PPPZcFRwkAAAAAwIXzVzaJjIxUYmKiwsLCXNrN/YMHD7p9zpYtW/TEE0/Yed9mPve5iIuLs7e0V+QBAMCF4bwKAEAuK6Tm4+Pjct+Mdk/fZpiAPmDAAL3wwguqWbPmOe/fDFM3w8mdt4oVK2bKcQMAkB9xXgUAIJfM6TbDywsWLKgffvhBV155ZUr7gw8+aOdoz5kzx+Xxx44dU7Fixew8bqekpCQb0k3bjBkz1LVr13O6Im+CN3O6AQA4f5xXAQDIJcPLAwMD7RJhM2fOdAnd5n7fvn0zPN4UPVu7dm2G5cZmzZqlH3/80S475k6BAgXsDQAAXDzOqwAA5JLQbTz00EMaOHCgmjdvrjZt2mjs2LF2ubAhQ4bY7cOHD9e+ffv05ZdfytfXV/Xr13d5funSpe363unbAQAAAABQfg/d/fv3V1RUlEaMGKEDBw7Y8Dx16lSFh4fb7abtv9bsBgAAAAAgp8rWdbqzA+uJAgDAeRUAgHxTvRwAAAAAgLyK0A0AAAAAgJcQugEAAAAA8BJCNwAAAAAAXkLoBgAAAADASwjdAAAAAAB4CaEbAAAAAAAvIXQDAAAAAOAlhG4AAAAAALyE0A0AAAAAgJcQugEAAAAA8BJCNwAAAAAAXkLoBgAAAADASwjdAAAAAAB4CaEbAAAAAAAvIXQDAAAAAOAlhG4AAAAAALyE0A0AAAAAgJcQugEAAAAA8BJCNwAAAAAAXkLoBgAAAADASwjdAAAAAAB4CaEbAAAAAAAvIXQDAAAAAOAlhG4AAAAAALyE0A0AAAAAgJcQugEAAAAA8BJCNwAAAAAAXkLoBgAAAADASwjdAAAAAAB4CaEbAAAAAAAvIXQDAAAAAOAlhG4AAAAAALyE0A0AAAAAgJf4K59JTk62/42JicnuQwEAINsUKVJEPj4+F70fzqsAgPyuyH+cU/Nd6D5+/Lj9b8WKFbP7UAAAyDbR0dEKCQm56P1wXgUA5HfR/3FO9Ul2XqLOJ5KSkrR///5Mu8Kfl5nRAObixJ49ezLlDzOA7xW8hd9X5y+zzoOcV88d31NkNr5T8Aa+V+ePnu50fH19VaFChQt4K/MvE7gJ3eB7hdyA31dZj/Pq+eN7iszGdwrewPcq81BIDQAAAAAALyF0AwAAAADgJYRueFSgQAE999xz9r9AZuF7BW/ge4XcgO8p+E4hN+B3VebLd4XUAAAAAADIKvR0AwAAAADgJYRuAAAAAAC8hNCdS3Xu3FlDhw7N7sMAACDX45wKAPAmQjcAIEe45ZZb5OPjk+HWtWtXlSxZUi+99JLb540cOdJuj4+PP6ef89dff6lXr14qUaKEChYsqLp16+rhhx/Wvn37MvkVAQCQPTin5iyEbgBAjnHZZZfpwIEDLreJEyfqpptu0v/+9z+5q/35+eefa+DAgQoMDPzP/X/88ce65JJLVKZMGbvf9evX66OPPlJ0dLTeeustL70qAACyHufUnIPQnUdMnz5doaGh+vLLL+2VrX79+umVV15RWFiYihYtqhdeeEFnzpzRo48+quLFi6tChQr67LPPXPZhenn69++vYsWK2R6gvn37aufOnSnbly1bpksvvdT2KJmf1alTJ61cudJlH6ZX6pNPPtGVV15pe5Bq1KihyZMnp2w/evSobrzxRpUqVUrBwcF2u/mDGTlf5cqV9c4777i0NW7cWM8//3zKZ28CTe/eve1nX6dOHS1atEhbt261QzcLFSqkNm3aaNu2bSnPN/823zPzPS1cuLBatGihP/74I8PPffHFFzVgwAD7mHLlyun999/PoleN7FimxATitDfzO+m2226z35e5c+e6PH7evHnasmWL3Z6UlKQRI0bY329mP+b7aX43Ou3du1cPPPCAvZnff+Z7ab5fHTt2tL+3nn32WT5wWJxTkRU4r8LbOKfmHITuPGD8+PG67rrrbOAeNGiQbZs1a5b2799v/0AdNWqUDUYmDJk/XpcsWaIhQ4bY2549e+zjT548qS5duthQY54zf/58+29zhcw5ZPP48eO6+eab7R+5ixcvtoHZDNE07WmZgG+OZ82aNXa7CdlHjhyx25555hnbszRt2jRt2LBBY8aMsSEeeYMJx+Y7uHr1atWuXdsG5bvuukvDhw/X8uXL7WPuu+++lMfHxsba74gJ2qtWrVKPHj3Up08f7d6922W/b7zxhho2bGgv8ph9DRs2TDNnzszy14fs06BBA3tRJv1FOhOeW7Zsqfr16+vdd9+1vdVvvvmm/f1jvk9XXHGFDeXGDz/8YH+fPfbYY25/hrlACXBORU7CeRXewDk1G5h1upH7dOrUKfnBBx9M/vDDD5NDQ0OTZ82albLt5ptvTg4PD09OTExMaatVq1Zyhw4dUu6fOXMmuVChQsnfffedvf/pp5/axyQlJaU8Ji4uLjk4ODj5999/d3sMZh9FihRJ/vXXX1PazFfq6aefTrkfGxub7OPjkzxt2jR7v0+fPsmDBw/OtPcBWcd8p95++22XtkaNGiU/99xzbj/7RYsW2Tbz3XIy37egoKCz/py6desmv//++y4/97LLLnN5TP/+/ZN79ux50a8JOYv53eXn52d/N6W9jRgxwm4fM2aMvX/8+HF73/zX3P/444/t/XLlyiW//PLLLvts0aJF8j333GP/fffddyeHhIRk+etCzsc5FdmB8yq8iXNqzkJPdy5m5iOaCuYzZsywvdRp1atXT76+qR+vGb5rrmo5+fn52SHkERER9v6KFSvsMOAiRYrYHm5zM8PQT58+nTIc2DzW9I7XrFnTDi83N9NTmb5X0vRIOpkhxWafzp9z9913214EM+zT9DYtXLjQS+8OskPaz95854y03zvTZr5TMTEx9v6JEyfs98AUsjK9jOZ7t3HjxgzfKTMsPf19M1ICeY/5XWZGSqS93XvvvXbbDTfcYIeQT5gwwd43/zXXe66//nr7nTKje9q1a+eyP3Pf+V0xjzXTIAB3OKciJ+K8iovBOTXn8M/uA8CFM8HVDLc1wy3NsMu0f0wGBAS4PNZsc9dm/oA1zH+bNWumb775JsPPMfOvDTNX/PDhw3Zeb3h4uJ0nYsJP+orBZ/s5PXv21K5du/Tbb7/ZIcXdunWzf1Cb4aDI2cxFnPRFrBISEjx+9s7vo7s25/fB1Bj4/fff7edfvXp1O8//mmuuOacq1ISnvMlcqDPfBXfMhT7z/TC/88wcbvNfcz8kJCTlQk7670XaoG0uGJqCaaY4W9myZbPg1SA34ZyKrMZ5Fd7GOTXnoKc7F6tWrZpd+uaXX37R/ffff1H7atq0qZ33WLp0afsHb9qb+UPXMHO5TQEiMwfX9KSb0B0ZGXneP8uEeBPgv/76axvgx44de1HHjqxhPjcTVpxMyNmxY8dF7dN8p8x3wRTeMz3ipmhW2uJ9TqaGQPr7Zs448h8TthcsWKApU6bY/5r7hgnepsieqUeRlhlNY4r6GSagmwrnr7/+utt9Hzt2LAteAXIqzqnIapxXkd04p2YderpzOdNzY4K3qcLr7++fobr0uTLFzkyxKlNJ2ln91wzxnTRpku2NNPdNAP/qq6/UvHlzG7hMu+mZPB+mOrDpUTehPS4uzv7h7PyDGDmbWSvZLNlkCp2ZgnymKJ6ZpnAxzHfKfMfMPk1vpNmnsxc8LROuTFAyVflNATVTEMuMlkDeY34vHDx40KXN/G5zFlw0qyaY740p2Gf+ayqPO5nfSc8995wNT6bX0vSEm+HpzhE8FStW1Ntvv22L+ZnfYWYfpnqwqWpuClGa6Q0sG5a/cU5FVuK8Cm/jnJpzELrzgFq1atlq5SZ4X2gIMks8marljz/+uK666ipbkbx8+fJ2+LfpQXJWCb7zzjvVpEkTVapUyS5J9sgjj5zXzzG9TKb6tOnNNIG9Q4cOdo43cj7zuW3fvt1WwTejH0xF1Yvt6TYB6NZbb1Xbtm1tqDLfP+cw4bQefvhhW3fAVMY3NQJMMDKVqZE3l2pKP/Tb/I4zc/2dzHfmySeftCE7LTMSx3x/zPfF1JEwtQLMkoVmpQWne+65xwYrM6XBjLA4deqUDd7me/3QQw9lwStETsc5FVmF8yq8jXNqzuFjqqll90EAgCcmEJmCgeYGAAAuDudVIOsxpxsAAAAAAC8hdAMAAAAA4CUMLwcAAAAAwEvo6QYAAAAAwEsI3QBs5fvzLVRmlvj6+eef7b9NNXpz3yzPBABAfsY5FUB6hG4AAAAAALyE0A0AAAAAgJcQugFYSUlJeuyxx1S8eHGVKVNGzz//fMo7s2XLFnXs2FFBQUGqW7euZs6c6fZd27hxo9q2bWsfV69ePc2ePTtl29GjR3XjjTeqVKlSCg4OVo0aNfT555+nbN+7d6+uv/56+/MLFSqk5s2ba8mSJXbbtm3b1LdvX4WFhalw4cJq0aKF/vjjjwzrjr7yyiu69dZbVaRIEVWqVEljx47l0wUAZDnOqQDSInQDsL744gsbdk3Qff311zVixAgbrs0fDldddZX8/Py0ePFiffTRR3r88cfdvmuPPvqoHn74Ya1atcqG7yuuuEJRUVF22zPPPKP169dr2rRp2rBhg8aMGaOSJUvabbGxserUqZP279+vyZMn6++//7YXAMzPdm7v1auXDdpm3z169FCfPn20e/dul5//1ltv2bBuHnPPPffo7rvvthcCAADISpxTAbhIBpDvderUKbl9+/Yu70OLFi2SH3/88eTff/892c/PL3nPnj0p26ZNm5Zsfn389NNP9v6OHTvs/VdffTXlMQkJCckVKlRIfu211+z9Pn36JA8ePNjte/3xxx8nFylSJDkqKuqcP4u6desmv//++yn3w8PDk2+66aaU+0lJScmlS5dOHjNmTL7/fAEAWYdzKoD06OkGYDVs2NDlnShbtqwiIiJsr7QZql2hQoWUbW3atHH7rqVt9/f3t73O5vmG6XUeP368GjdubHuxFy5cmPJYU/W8SZMmdmi5OydOnLDPMUPbixYtaoeYmx7s9D3daV+DqaZuhsmb1wAAQFbinAogLUI3ACsgIMDlnTCh1QzvTk42ndjKsO1cOR/bs2dP7dq1yy5NZoaRd+vWTY888ojdZuZ4n40Ztj5x4kS9/PLLmjdvng3pDRo0UHx8/Dm9BgAAshLnVABpEboBnJXpXTY9yiYoOy1atMjtY82cb6czZ85oxYoVql27dkqbKaJ2yy236Ouvv9Y777yTUujM9AiYIH3kyBG3+zVB2zzvyiuvtGHb9GCbtcEBAMhNOKcC+ROhG8BZXXLJJapVq5YGDRpkC5yZAPzUU0+5feyHH36on376yQ79vvfee23FclNN3Hj22Wf1yy+/aOvWrVq3bp2mTJmiOnXq2G033HCDDdL9+vXTggULtH37dtuz7Qz31atX16RJk2wwN8cwYMAAerABALkO51QgfyJ0Azj7LwlfXxuk4+Li1LJlS91+++12mLc7r776ql577TU1atTIhnMTsp0VygMDAzV8+HDbq22WHzPV0M0cb+e2GTNmqHTp0rZKuenNNvsyjzHefvttFStWzFZEN1XLTfXypk2b8skBAHIVzqlA/uRjqqll90EAAAAAAJAX0dMNAAAAAICXELoBAAAAAPASQjcAAAAAAF5C6AYAAAAAwEsI3QAAAAAAeAmhGwAAAAAALyF0AwAAAADgJYRuAAAAAAC8hNANwMXOnTvl4+Oj1atX55if1blzZw0dOtTrxwMAQGbjvAqA0A0g21SsWFEHDhxQ/fr17f3Zs2fbEH7s2DE+FQAAOK8CeYJ/dh8AgPwpPj5egYGBKlOmTHYfCgAAuR7nVSDnoqcbyIemT5+u9u3bq2jRoipRooR69+6tbdu2eXz85MmTVaNGDQUHB6tLly764osvMvRIT5w4UfXq1VOBAgVUuXJlvfXWWy77MG0vvfSSbrnlFoWGhuqOO+5wGXJn/m32bRQrVsy2m8c6JSUl6bHHHlPx4sVtUH/++edd9m8e//HHH9vXUrBgQdWpU0eLFi3S1q1b7fD0QoUKqU2bNmd9nQAAXAjOqwDOKhlAvvPjjz8mT5w4MXnz5s3Jq1atSu7Tp09ygwYNkhMTE5N37NiRbH41mHbD3A8ICEh+5JFHkjdu3Jj83XffJZcvX94+5ujRo/Yxy5cvT/b19U0eMWJE8qZNm5I///zz5ODgYPtfp/Dw8OSQkJDkN954I3nLli32lvZnnTlzxh6TuW/2ceDAgeRjx47Z53bq1Mk+9/nnn7fH/MUXXyT7+Pgkz5gxI2X/5nnmuCZMmGCf369fv+TKlSsnd+3aNXn69OnJ69evT27dunXyZZddluXvNwAgb+O8CuBsCN0AkiMiImxoXbt2bYbQ/fjjjyfXr1/f5V166qmnXEL3gAEDki+99FKXxzz66KPJdevWdQndJginlf5n/fXXXy77dTKhu3379i5tLVq0sMeW8stMSn766adT7i9atMi2ffrppylt5oJBUFAQnzgAwKs4rwJIi+HlQD5khlgPGDBAVatWVUhIiKpUqWLbd+/eneGxmzZtUosWLVzaWrZs6XJ/w4YNateunUubub9lyxYlJiamtDVv3vyCj7lhw4Yu98uWLauIiAiPjwkLC7P/bdCggUvb6dOnFRMTc8HHAQBAepxXOa8CZ0MhNSAf6tOnj60cPm7cOJUrV87OlzYVxE0RlvRMJ7KZL52+7XwfY5h51RcqICDA5b75eea4PT3GeTzu2tI/DwCAi8F5lfMqcDaEbiCfiYqKsj3TpuhYhw4dbNv8+fM9Pr527dqaOnWqS9vy5ctd7tetWzfDPhYuXKiaNWvKz8/vnI/NVDM30vaOAwCQk3FeBfBfGF4O5DOmMripWD527Fhb2XvWrFl66KGHPD7+rrvu0saNG/X4449r8+bN+v777/W///3Ppef44Ycf1p9//qkXX3zRPsZUN//ggw/0yCOPnNexhYeH231OmTJFhw8fVmxs7EW+WgAAvIvzKoD/QugG8hlfX1+NHz9eK1assEPKhw0bpjfeeMPj48187x9//FGTJk2yc6bHjBmjp556ym4zy4MZTZs2tWHc7Nfs89lnn9WIESNclvw6F+XLl9cLL7ygJ554ws6/vu+++y7y1QIA4F2cVwH8Fx9TTe0/HwUAabz88sv66KOPtGfPHt4XAAAuEudVIG9jTjeA/zR69GhbwdwMS1+wYIHtGacXGgCAC8N5FchfCN0A/pNZ+uull17SkSNHVKlSJTuHe/jw4bxzAABcAM6rQP7C8HIAAAAAALyEQmoAAAAAAHgJoRsAAAAAAC8hdAMAAAAA4CWEbgAAAAAAvITQDQAAAACAlxC6AQAAAADwEkI3AAAAAABeQugGAAAAAMBLCN0AAAAAAMg7/g8/+xZrN6+dXwAAAABJRU5ErkJggg==\",\n      \"text/plain\": [\n       \"<Figure size 1000x1000 with 4 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    }\n   ],\n   \"source\": [\n    \"sns.catplot(\\n\",\n    \"    news_results[news_results.measure != \\\"Elapsed time\\\"], \\n\",\n    \"    x=\\\"algorithm\\\", \\n\",\n    \"    y=\\\"value\\\", \\n\",\n    \"    hue=\\\"algorithm\\\", \\n\",\n    \"    col=\\\"measure\\\", \\n\",\n    \"    kind=\\\"swarm\\\", \\n\",\n    \"    col_wrap=2,\\n\",\n    \"    height=5,\\n\",\n    \")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"2bb61a83-1f7d-48e2-9c46-6d1b98699e42\",\n   \"metadata\": {},\n   \"source\": [\n    \"Both ARI and AMI scores for all the algorithms are much lower here -- this is a significantly noiser and harder to cluster dataset. Nonetheless KMeans does consistently worse in terms of cluster quality over the points actually clustered by the algorithms. This came at a cost for UMAP + HDBSCAN and EVoC however: they consistently cluster between about 50% and 75% of the dataset and leave a lot of data unclustered as noise. That is to be expected to some degree, but does represent a challenge if you want cluster labels for most of your data. This shows up in the clustering score, with all three approaches being much closer with overlap among the distributions. Still, EVoC has a notable edge in quality overall, and for an approach that ran as fast as KMeans that's quite powerful. \"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"41647cad-0221-42b0-a06a-7d96529de4b0\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Audio embeddings\\n\",\n    \"\\n\",\n    \"Audio embedding models are increasingly common. As a test dataset we'll use the BirdCLEF 2023 dataset. This was a kaggle competition for correctly identifying bird species based on audio clips of their songs. Agsin this is quite challenging. The audio is (literally) noisy, with potential for a lot of background noise and even other bird calls intruding into the background of audio clips. We also need a good embedding model representation of this audio id we have any hope that clustering can match against the class labels. Fortunately someone embedded the dataset using Google's Bird Vocalization Classifier and put that data on Huggingface, so we can simply download the embeddings. The dataset has a large number of possible species, and some of those have fewer than 100 recordings, making clustering them quite difficult. To make things a little easier on our clustering algorithms we'll prune out all samples from species that have 100 or fewer recordings in the dataset.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 16,\n   \"id\": \"c56939cb-662a-445e-9f11-2266c46b7aa5\",\n   \"metadata\": {\n    \"execution\": {\n     \"iopub.execute_input\": \"2026-03-25T20:45:36.390262Z\",\n     \"iopub.status.busy\": \"2026-03-25T20:45:36.390117Z\",\n     \"iopub.status.idle\": \"2026-03-25T20:45:42.425450Z\",\n     \"shell.execute_reply\": \"2026-03-25T20:45:42.424318Z\",\n     \"shell.execute_reply.started\": \"2026-03-25T20:45:36.390248Z\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"ds_birdclef = load_dataset(\\\"Syoy/birdclef_2023_train\\\")\\n\",\n    \"birdclef2023_data = np.asarray(ds_birdclef[\\\"train\\\"][\\\"embeddings\\\"])\\n\",\n    \"birdclef2023_target = np.asarray(ds_birdclef[\\\"train\\\"][\\\"primary_label\\\"])\\n\",\n    \"# Only use bird species with at least 100 samples -- this is still very challenging\\n\",\n    \"mask = np.isin(\\n\",\n    \"    birdclef2023_target,\\n\",\n    \"    np.where(np.bincount(birdclef2023_target) > 100)[0],\\n\",\n    \")\\n\",\n    \"birdclef2023_data = birdclef2023_data[mask]\\n\",\n    \"birdclef2023_target = birdclef2023_target[mask]\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"33a72cf7-6a1e-40b1-bcc5-092b2762d50b\",\n   \"metadata\": {},\n   \"source\": [\n    \"Now we just need to run all the algorithms. Again runing was required to find the right number of clusters for KMeans to get good results. UMAP + HDBSCAN was a little wasier to tune, as we have a concrete idea of the ``min_cluster_size`` based on our pruning.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 17,\n   \"id\": \"2ca28659-046b-407a-be97-ab334d019473\",\n   \"metadata\": {\n    \"execution\": {\n     \"iopub.execute_input\": \"2026-03-25T20:45:42.426718Z\",\n     \"iopub.status.busy\": \"2026-03-25T20:45:42.426496Z\",\n     \"iopub.status.idle\": \"2026-03-25T20:46:41.283049Z\",\n     \"shell.execute_reply\": \"2026-03-25T20:46:41.282152Z\",\n     \"shell.execute_reply.started\": \"2026-03-25T20:45:42.426697Z\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"bird_results = run_dataset_benchmarks(\\n\",\n    \"    birdclef2023_data, \\n\",\n    \"    birdclef2023_target, \\n\",\n    \"    n_runs=16, \\n\",\n    \"    kmeans_kwargs={\\\"n_clusters\\\":130}, \\n\",\n    \"    umap_hdbscan_kwargs={\\n\",\n    \"        \\\"min_samples\\\":5,\\n\",\n    \"        \\\"min_cluster_size\\\":100, \\n\",\n    \"        \\\"metric\\\":\\\"cosine\\\", \\n\",\n    \"        \\\"cluster_selection_method\\\":\\\"leaf\\\"\\n\",\n    \"    }\\n\",\n    \")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"15082cc4-86ef-491d-affd-d066a88f9c23\",\n   \"metadata\": {},\n   \"source\": [\n    \"As always we'll start with timing results.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 18,\n   \"id\": \"c2e9db88-dc9b-42cd-a2a7-7e442b6b57e8\",\n   \"metadata\": {\n    \"execution\": {\n     \"iopub.execute_input\": \"2026-03-25T20:46:41.283929Z\",\n     \"iopub.status.busy\": \"2026-03-25T20:46:41.283731Z\",\n     \"iopub.status.idle\": \"2026-03-25T20:46:41.488356Z\",\n     \"shell.execute_reply\": \"2026-03-25T20:46:41.487783Z\",\n     \"shell.execute_reply.started\": \"2026-03-25T20:46:41.283914Z\"\n    }\n   },\n   \"outputs\": [\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"<seaborn.axisgrid.FacetGrid at 0x74205075f560>\"\n      ]\n     },\n     \"execution_count\": 18,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    },\n    {\n     \"data\": {\n      \"image/png\": \"iVBORw0KGgoAAAANSUhEUgAAAxYAAAMVCAYAAADqKmIJAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAWKFJREFUeJzt3Qd8VeX9P/Bv2EMSQGUJIu6BooJb69aiona5WlGrrdpa62752bpqpbWt1tbdOlq11rqtW+tGtIKgVnEBCipDQBKG7Pxfz+GfmJBE0QO5JHm/X6/zCuc555773Htzyf3cZxWVl5eXBwAAQA7N8twYAABAsAAAAFYILRYAAEBuggUAAJCbYAEAAOQmWAAAALkJFgAAQG6CBQAAkJtgAQAA5CZYALBKKyoqinvuuScaYz2feuqp7HYzZ85cafUCqC+CBQAFc/TRR2cfrJfdvv71rze6V2W33XaLU045pVrZjjvuGJMmTYqSkpKC1QtgRWmxwq4EQINUXl4eixcvjhYtCvMnIYWIG264oVpZ69atoylo1apVdOvWrdDVAFghtFgA5PgG+ic/+Un2LXSnTp2ia9euce2118acOXPimGOOiQ4dOsR6660XDz30ULXbvfHGG7HffvvFaqutlt3myCOPjGnTplUef/jhh2PnnXeOjh07xuqrrx4HHHBAjB07tvL4ggUL4qSTToru3btHmzZtYp111omhQ4dmx957773sG//Ro0dXnp+62aSy1O2mavebRx55JAYMGJB9iH/22WezgHHxxRfHuuuuG23bto1+/frFHXfcsdJ/P9L9pw/XVbf0fNblZz/7WWy44YbRrl27rK6//OUvY+HChZXHzzvvvNhyyy3jmmuuiV69emXnfec736nW3Sg9B9tuu220b98+e5532mmneP/99yuP//vf/47+/ftnz2+6j/PPPz8WLVpUefydd96Jr33ta9nxTTfdNB577LEvbJl5+umn47LLLqtslUmv1bJdoW688casPvfff39stNFGWd2//e1vZ79Tf/vb37LXOj036fcuhcGqvxNnnXVWrLXWWtlj2m677Spfb4D6IlgA5JA+7K2xxhrx3//+N/uwd+KJJ2YfYlMXl5dffjn23XffLDjMnTs3Oz91e9l1112zD74jRozIQsSUKVPikEMOqbxm+hB52mmnxUsvvRT/+c9/olmzZvGN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YAEAAOSmKxQAAJCbFgsAACA3wQIAAMityQWLNAlWmnPcZFgAALDiNLlgMWvWrGy++PQTAABYMZpcsAAAAFY8wQIAAMhNsAAAAHITLAAAgNwECwAAIDfBAgAAyE2wAAAAchMsAACA3AQLAAAgN8ECAADITbAAAAByEywAAIDcBAsAACA3wQIAAMhNsAAAAHITLAAAgNwECwAAIDfBAgAAyE2wAAAAchMsAACA3AQLAAAgN8ECAADITbAAAAByEywAAIDcBAsAACA3wQIAAMhNsAAAAHJrkf8SAJDDpzMjRt4QMeGFiNW6Rgz4fkSPLT2lAA2MYAFA4cydEXH9vhHT3v6sbNTNEd++PmKzg70yAA2IrlAAFM5//1I9VCTliyMe/UXEkiWFqhUAX4FgAUDhvPds7eWlEyNmjKvv2gCQg2ABQOG0W7328qLmEW071ndtAMhBsACgcPofXXv5JoMi2q9R37UBIAfBAoDCWW/3iP3/ENG20/8vKIrY+ICIA//kVQFoYIrKy8vLowkpKyuLkpKSKC0tjeLi4kJXB4Bk4byIj8dEtO8SUbKW5wSgATLdLACF17JNRI+tCl0LAHLQFQoAAMhNiwUAhTfp1YgJw5euvL3RfhEtWhW6RgB8SYIFAIWTFsG790cRr9z6WVlxz4gj745Yc0OvDEADoisUAIWTAkXVUJGUfbA0bADQoAgWABTO63fVXv7BSxEzJ9Z3bQDIQbAAoHDKl3y1YwCscgQLAApnkwNrL+++ZUSn3vVdGwByECwAKJytjly60nZV7deMOOjyQtUIgK/IytsAFN77zy/dOnSP2OzgiFbtC10jAL4kwQIAAMhNVygAACA3wQIAAMhNsAAAAHITLAAAgNwECwAAIDfBAgAAaNjB4plnnolBgwZFjx49oqioKO65554vvM0tt9wS/fr1i3bt2kX37t3jmGOOienTp9dLfQEAgFUwWMyZMycLCZdfvnwrrD733HMxePDgOPbYY+P111+P22+/PV566aU47rjjVnpdAQCAurWIAho4cGC2La8XXngh1llnnTj55JOz/T59+sTxxx8fF198cZ23mT9/frZVKCsry1lrAACgQY+x2HHHHeODDz6IBx98MMrLy2PKlClxxx13xP7771/nbYYOHRolJSWVW69eveq1zgB8geljI+75UcSftoq4fmDE/+70lAE0QEXl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U8U2ceLEeq0nAAA0BQ2yK1QKE8cee2zcfvvtsddee33uua1bt842AABg5WnWEFsqjj766PjHP/4R+++/f6GrAwAAFLrFYvbs2fHuu+9W7o8fPz5Gjx6dDcZee+21s25MH374Yfz973+vDBWDBw+Oyy67LLbffvuYPHlyVt62bdts/AQAANAEp5t96qmnsrUolnXUUUfFjTfemLVMvPfee9l5yW677RZPP/10necvD9PNAgBAI17Hor4IFgAAsOI1uDEWAADAqkewAAAAchMsAACA3AQLAAAgN8ECAADITbAAAAByEywAAIDcBAsAACA3wQIAAMhNsAAAAHITLAAAgMIFi3fffTceeeSR+PTTT7P98vLy/LUBAACaRrCYPn167LXXXrHhhhvGfvvtF5MmTcrKjzvuuDj99NNXRh0BAIDGFixOPfXUaNGiRUyYMCHatWtXWX7ooYfGww8/vKLrBwAANAAtvuwNHn300awLVM+ePauVb7DBBvH++++vyLoBAACNtcVizpw51VoqKkybNi1at269ouoFAAA05mDxta99Lf7+979X7hcVFcWSJUvid7/7Xey+++4run4AAEBj7AqVAsRuu+0WI0aMiAULFsRZZ50Vr7/+esyYMSOGDRu2cmoJAAA0rhaLTTfdNF599dXYdtttY++99866Rn3zm9+MUaNGxXrrrbdyagkAAKzSisqb2AIUZWVlUVJSEqWlpVFcXFzo6gAAQNPsCvXMM8984RgMAACgafnSLRbNmtXsPZUGcFdYvHhxrMq0WAAAwCowxuKTTz6ptk2dOjVbGG+bbbbJ1rgAAACani/dFSqNT1hWGsSd1rBIq3KPHDlyRdUNAABorC0WdVlzzTXjrbfeWlGXAwAAGnOLRZpqtqo0RGPSpEnxm9/8Jvr167ci6wYAADTWYLHllltmg7WXHfO9/fbbx/XXX78i6wYAADTWYDF+/Pgas0SlblBt2rRZkfUCAAAac7Do3bv3yqkJAADQuIPFn/70p+W+4Mknn5ynPgAAQGNdIK9Pnz7Ld7Giohg3blysyiyQBwAABWqxWHZcBQAAwEpZxwIAAGi6vvTg7eSDDz6I++67LyZMmBALFiyoduySSy5ZUXUDAAAaa7D4z3/+EwceeGA27iKttN23b9947733snUttt5665VTSwAAoHF1hRoyZEicfvrp8b///S9bu+LOO++MiRMnxq677hrf+c53Vk4tAQCAxhUsxowZE0cddVT27xYtWsSnn34aq622WlxwwQXx29/+dmXUEQAAaGzBon379jF//vzs3z169IixY8dWHps2bdqKrR0AANA4x1hsv/32MWzYsNh0001j//33z7pFvfbaa3HXXXdlxwAAgKbnSweLNOvT7Nmzs3+fd9552b9vu+22WH/99ePSSy9dGXUEAAAaw8rbVR1zzDHxve99L/bYY49spe2GxsrbAACwCoyxmD59etYFqmfPnlk3qNGjR6+EagEAAI06WKSF8SZPnhznnntujBw5Mvr375+Nt7jooouy9SwAAICm50t3haptFe5bb701rr/++njnnXdi0aJFsSrTFQoAAFaBFouqFi5cGCNGjIgXX3wxa63o2rXriqsZAADQuIPFk08+GT/4wQ+yIJEWy+vQoUP8+9//zlbgBgAAmp4vPd1sGrSdBnDvu+++cc0118SgQYOiTZs2K6d2AABA4wwW55xzTnznO9+JTp06rZwaAQAATW/wdkNj8DYAAKxig7cBAAAECwAAYIXQYgEAAOQmWAAAALkJFgAAQG6CBQAAkJtgAQAA5CZYAAAAuQkWAABAboIFAACQm2ABAADkJlgAAAC5CRYAAEBuggUAAJCbYAEAAOQmWAAAALkJFgAAQG6CBQAAkJtgAQAA5CZYAAAAuQkWAABAboIFAACQm2ABAADkJlgAAAC5CRYAAEDDDhbPPPNMDBo0KHr06BFFRUVxzz33fOFtnn766ejfv3+0adMm1l133bj66qvrpa4AAMAqGizmzJkT/fr1i8svv3y5zh8/fnzst99+scsuu8SoUaPi//7v/+Lkk0+OO++8c6XXFQAAqFtReXl5eawCUovF3XffHQcffHCd5/zsZz+L++67L8aMGVNZdsIJJ8Qrr7wSw4cPX677KSsri5KSkigtLY3i4uIVUncAAGjqGtQYixQe9tlnn2pl++67b4wYMSIWLlxY623mz5+fhYmqGwAA0ISDxeTJk6Nr167VytL+okWLYtq0abXeZujQoVkLRcXWq1eveqotAAA0HQ0qWFR0maqqoifXsuUVhgwZknV7qtgmTpxYL/UEAICmpEU0IN26dctaLaqaOnVqtGjRIlZfffVab9O6detsAwAAVp4G1WKxww47xGOPPVat7NFHH40BAwZEy5Yt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+7yVtWncBAsAAGrYfM3N44B1D6hRfuB6B2ZrTHRr363WZ233XqaQbap0hQIAoFZp1qcHxj0Qj773aERRxL7r7Bv79dkvm1b2uQ+fi1OePCVbSK/Cbr12i0t3uzSbmpamR7AAAOArmTxnctw/7v74ZN4n2craO6+1s7UsmjDBAgAAyM0YCwAAIDfBAgAAyE2wAAAAchMsAACA3AQLAAAgN8ECAADITbAAAAByEywAAIDcBAsAACA3wQIAAMhNsAAAAHITLAAAgNwECwAAIDfBAgAAyE2wAAAAchMsAACA3AQLAAAgN8ECAADITbAAAAByEywAAIDcBAsAACA3wQIAAMhNsAAAAHITLAAAgNwECwAAIDfBAgAAyK1FNDHl5eXZz7KyskJXBQAAGoQOHTpEUVHR557T5ILFrFmzsp+9evUqdFUAAKBBKC0tjeLi4s89p6i84iv8JmLJkiXx0UcfLVfqouFJLVEpNE6cOPELf/mBVYv3LzRM3rtNQwctFjU1a9YsevbsWYCXg/qUQoVgAQ2T9y80TN67GLwNAADkJlgAAAC5CRY0Kq1bt45zzz03+wk0LN6/0DB579JkB28DAAArnhYLAAAgN8ECAADITbAAAAByEyyoN7vttluccsopnnEAgEZIsAAAaOKOPvroKCoqqrHtsccescYaa8SFF15Y6+2GDh2aHV+wYMFy3c+TTz4Z++23X6y++urRrl272HTTTeP000+PDz/8cAU/IgpBsAAAIL7+9a/HpEmTqm133nlnfO9734sbb7wxaptI9IYbbogjjzwyWrVq9YXP4DXXXBN77bVXdOvWLbvuG2+8EVdffXWUlpbGH/7wB69AIyBYUDAPP/xwlJSUxN///vfsm5KDDz44LrrooujatWt07Ngxzj///Fi0aFGceeaZ0blz5+jZs2dcf/311a6RvuE49NBDo1OnTtm3HwcddFC89957lcdfeuml2HvvvbNvU9J97brrrvHyyy9Xu0b6Ruavf/1rfOMb38i+Pdlggw3ivvvuqzz+ySefxHe/+91Yc801o23bttnx9B8psNQ666wTf/zjH6s9HVtuuWWcd955le+x9IHigAMOyN5jm2yySQwfPjzefffdrItk+/btY4cddoixY8dW3j79O72f0/8Hq622WmyzzTbx+OOP17jfX/3qV3HEEUdk5/To0SP+/Oc/e1kgx3oU6UN/1S39fT322GOz9+QzzzxT7fxnn3023nnnnez4kiVL4oILLsj+VqfrpP8D0t/5Ch988EGcfPLJ2Zb+lqf3fnoPf+1rX8v+Bp9zzjlet0ZAsKAg/vnPf8YhhxyShYrBgwdnZU888UR89NFH2X9cl1xySfahJH0QSf+pvfjii3HCCSdk28SJE7Pz586dG7vvvnv2gSLd5rnnnsv+nb5xqWiSnTVrVhx11FHZf34vvPBCFgpSE2wqryqFmFSfV199NTuegsSMGTOyY7/85S+zb1UeeuihGDNmTFx11VVZUAGWXwoA6b0+evTo2HjjjbMwcPzxx8eQIUNixIgR2TknnXRS5fmzZ8/O3ospTIwaNSr23XffGDRoUEyYMKHadX/3u9/FFltskX1hkK516qmnxmOPPealgRVo8803z8L9sl+qpYCw7bbbRt++feOyyy7LWh1+//vfZ39L03v2wAMPzIJHcvvtt2d/m88666xa7yN9oUgjkBbIg/qw6667lv/0pz8tv+KKK8pLSkrKn3jiicpjRx11VHnv3r3LFy9eXFm20UYble+yyy6V+4sWLSpv3759+a233prtX3fdddk5S5YsqTxn/vz55W3bti1/5JFHaq1DukaHDh3K//3vf1eWpbfBL37xi8r92bNnlxcVFZU/9NBD2f6gQYPKjznmmBX2PEBjk967l156abWyfv36lZ977rm1vseGDx+elaX3cIX0vm7Tps3n3s+mm25a/uc//7na/X7961+vds6hhx5aPnDgwNyPCZqa9He4efPm2d/ZqtsFF1yQHb/qqquy/VmzZmX76Wfav+aaa7L9Hj16lP/617+uds1tttmm/Ec/+lH27xNPPLG8uLi43h8X9UuLBfUq9alMM0M9+uijWWtDVZtttlk0a/bZr2TqApG+JanQvHnzrLvT1KlTs/2RI0dmXSk6dOiQtVSkLXWZmjdvXmWXinRuauXYcMMNs65QaUvfhC77rWf6xrNC6paRrllxPyeeeGLWwpKaddM3Lc8///xKenag8ar6Hkvv7aTq+zuVpfduWVlZtj9nzpzs/ZYGdqZvMtP7+80336zx3k1dqJbdTy2LwJeX/i6nVsWq249//OPs2OGHH551d7rtttuy/fQzfW9w2GGHZe/b1ONgp512qna9tF/xfkznpm6RNG4tCl0Bmpb04Tx1WUjNqalZtep/Mi1btqx2bjpWW1n6jy1JP/v37x+33HJLjftJ4yGSNHbj448/zvp/9+7dO+v3mT54LDt7xefdz8CBA+P999+PBx54IOuWseeee2b/0abmXiCyLwSWHdS5cOHCOt9jFe/72soq3ndpbNUjjzySvc/WX3/9bHzTt7/97eWaecaHF/hq0hdr6f1Wm/TFXHoPpr/faUxF+pn2i4uLK78QWPa9VzVMpC/40iDtNCC8e/fuXqJGSosF9Wq99dbLppq799574yc/+Umua2299dZZ380uXbpk/xFW3dJ/gEkaW5EGiqW+2qlFJAWLadOmfen7SkElhZSbb745CynXXnttrrpDY5LeH+nDQoX0IWP8+PG5rpneu+k9lyZVSC0baRBp1YkZKqSxU8vupzEcwIqXAsWwYcPi/vvvz36m/SSFizR5QhrrWFVq4U+TNSQphKSZoy6++OJarz1z5kwvWSOgxYJ6l761SOEizQjRokWLGrPJLK80wDoN3Ewzx1TMRJG6Sdx1113Zt51pP4WMm266KQYMGJB92Enl6ZvPLyPNVJFaRlIwmT9/fvYfasV/lEBk89ynqSjT4Oo02UKa8CB1XcwjvXfTezldM33jma5Z0ZpRVfpwkz6opFnl0qDtNEA0tS4CX176Gzd58uRqZenvdMWEJWlmxfTeTBMxpJ9pRqcK6e/rueeem32BmHonpBaN1JWqoldBr1694tJLL80maUh/j9M10qxQabaoNJFL6u5oytmGT7CgIDbaaKNsFqgULr7qB5A0bWWaDepnP/tZfPOb38xmelprrbWyrkrp25OKGSt++MMfxlZbbRVrr712Np3tGWec8aXuJ33DkmabSd+WplCyyy67ZGMugKXS+2PcuHHZLG6ptTDNAJW3xSJ9APn+978fO+64Y/ahJr3PK7pbVJUW1krjrdLMbmlsVPpgkmajAb68ND3sst2U0t/rNL6pQnpf/t///V8WJKpKvQPSezS9J9MYxTQ+Kk3dnmZjrPCjH/0o+3IxdXFMrZGffvppFi7S/x2nnXaal6wRKEojuAtdCQD4stIHkjQZRNoAKDxjLAAAgNwECwAAIDddoQAAgNy0WAAAALkJFgDUKc3c9mUHR6fpYe+5557s32k2tbSfpp0EoHETLAAAgNwECwAAIDfBAoDPlVa8Puuss6Jz587RrVu3OO+88yqPvfPOO9nqu23atMkWxEqrX9cmLbCVFrtL56VV7J966qnKY5988kl897vfjTXXXDNbhDItqJVW7a2QVuY97LDDsvtv3759DBgwIF588cXs2NixY+Oggw6Krl27Ziv3brPNNvH444/XWO8iLY6ZFvZKi+ilxTKvvfZarzrACiZYAPC5/va3v2Uf6NOH+YsvvjguuOCCLECkwJFWvW/evHm88MILcfXVV2crZNcmrdKbVuQdNWpUFjAOPPDAmD59enbsl7/8Zbzxxhvx0EMPxZgxY+Kqq67KVttOZs+eHbvuumt89NFH2Sq+r7zyShZy0n1XHN9vv/2yMJGunVbdHjRoUEyYMKHa/acVuVMgSeek1X9PPPHEaqsJA5Cf6WYB+NzB24sXL45nn322smzbbbeNPfbYI9vSh/o0QLtnz57ZsYcffjgGDhwYd999dxx88MHZsT59+sRvfvObytCxaNGirOwnP/lJFhJSyEhB4vrrr69x/6ll4Ywzzsiuk1oslkdqEUnB4aSTTqpssdhll13ipptuyvbLy8uzlpfzzz8/TjjhBK8+wAqixQKAz7XFFltU2+/evXtMnTo1a11I3YoqQkWyww471HqNquUtWrTIWg/S7ZMUAv75z3/GlltumQWN559/vvLcNJvUVlttVWeomDNnTnab1A2rY8eOWXeo1BKxbItF1ceQZqlKwSI9BgBWHMECgM/VsmXLavvpg3nqipS++V9WOra8Ks5NLRzvv/9+Nq1t6vK05557Zq0USRpz8XlSF6s777wzfv3rX2etKimIbL755rFgwYLlegwArDiCBQBfSWolSC0DKQxUGD58eK3npjEYFVJXqJEjR8bGG29cWZYGbh999NFx8803xx//+MfKwdWppSGFhRkzZtR63RQm0u2+8Y1vZIEitUSkblMA1D/BAoCvZK+99oqNNtooBg8enA2qTh/yzz777FrPveKKK7JxF6mb0o9//ONsJqg0S1NyzjnnxL333hvvvvtuvP7663H//ffHJptskh07/PDDs7CQxmsMGzYsxo0bl7VQVASY9ddfP+66664sfKQ6HHHEEVoiAApEsADgq/0BadYsCwvz58/PBnQfd9xxWZek2qTB27/97W+jX79+WQBJQaJi5qdWrVrFkCFDstaJNHVtmmUqjbmoOPboo49Gly5dsoHiqVUiXSudk1x66aXRqVOnbKapNBtUmhVq66239ooCFIBZoQAAgNy0WAAAALkJFgAAQG6CBQAAkJtgAQAA5CZYAAAAuQkWAABAboIFAACQm2ABAADkJlgAsFzee++9KCoqitGjR68y97XbbrvFKaecstLrA8AXEywAWOX06tUrJk2aFH379s32n3rqqSxozJw5s9BVA6AOLeo6AACFsGDBgmjVqlV069bNCwDQgGixAKDSww8/HDvvvHN07NgxVl999TjggANi7NixdT5D9913X2ywwQbRtm3b2H333eNvf/tbjZaFO++8MzbbbLNo3bp1rLPOOvGHP/yh2jVS2YUXXhhHH310lJSUxA9+8INqXaHSv9O1k06dOmXl6dwKS5YsibPOOis6d+6chZHzzjuv2vXT+ddcc032WNq1axebbLJJDB8+PN59992sK1X79u1jhx12+NzHCcAXEywAqDRnzpw47bTT4qWXXor//Oc/0axZs/jGN76RfXhfVvrA/+1vfzsOPvjgLAAcf/zxcfbZZ1c7Z+TIkXHIIYfEYYcdFq+99lr2of+Xv/xl3HjjjdXO+93vfpd1e0rnp+PLdotK4SR56623si5Sl112WeXxFGZSOHjxxRfj4osvjgsuuCAee+yxatf41a9+FYMHD87qufHGG8cRRxyR1XfIkCExYsSI7JyTTjrJbwJAHuUAUIepU6eWpz8Vr732Wvn48eOzf48aNSo79rOf/ay8b9++1c4/++yzs3M++eSTbP+II44o33vvvaudc+aZZ5Zvuummlfu9e/cuP/jgg6uds+x9Pfnkk9WuW2HXXXct33nnnauVbbPNNlndKqTb/eIXv6jcHz58eFZ23XXXVZbdeuut5W3atPF7AJCDFgsAKqXuQOnb/HXXXTeKi4ujT58+WfmECRNqPEup9WCbbbapVrbttttW2x8zZkzstNNO1crS/jvvvBOLFy+uLBswYMBXfhW22GKLavvdu3ePqVOn1nlO165ds5+bb755tbJ58+ZFWVnZV64HQFNn8DYAlQYNGpR1PfrLX/4SPXr0yLpApS5KaUD1slJjQBq/sGzZlz0nSV2ZvqqWLVtW20/3t2zXrarnVNSntrLaunwBsHwECwAy06dPz1oY0kDnXXbZJSt77rnn6nx20liFBx98sFpZxXiFCptuummNazz//POx4YYbRvPmzZf7mU+zRCVVWzkAWLXoCgVA5YxLaSaoa6+9Npsx6YknnsgGctclDX5+880342c/+1m8/fbb8a9//atyUHZFC8Dpp5+eDQJPg6fTOWmg9eWXXx5nnHHGl3rWe/funV3z/vvvj48//jhmz57tVQNYxQgWACz9g9CsWfzzn//MZmZK3Z9OPfXUbLamuqTxF3fccUfcdddd2RiGq666qnJWqDS1bLL11ltngSNdN13znHPOyWZtqjpd7PJYa6214vzzz4+f//zn2XgIMzgBrHqK0gjuQlcCgMbh17/+dVx99dUxceLEQlcFgHpmjAUAX9mVV16ZzQyVulANGzYsa+HQmgDQNAkWAHxladrYtGr2jBkzYu21187GVKRF5wBoenSFAgAAcjN4GwAAyE2wAAAAchMsAACA3AQLAAAgN8ECAADITbAAAAByEywAAIDcBAsAACDy+n/qlqt3QCXcwwAAAABJRU5ErkJggg==\",\n      \"text/plain\": [\n       \"<Figure size 800x800 with 1 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    }\n   ],\n   \"source\": [\n    \"sns.catplot(\\n\",\n    \"    bird_results[bird_results.measure == \\\"Elapsed time\\\"], \\n\",\n    \"    x=\\\"algorithm\\\", \\n\",\n    \"    y=\\\"value\\\", \\n\",\n    \"    hue=\\\"algorithm\\\", \\n\",\n    \"    kind=\\\"swarm\\\", \\n\",\n    \"    col=\\\"measure\\\",\\n\",\n    \"    height=8,\\n\",\n    \")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"de1f4f88-09e3-4627-aac5-a56181a67ef8\",\n   \"metadata\": {},\n   \"source\": [\n    \"This time we have some slightly surprising results: KMeans is only barely faster than UMAP + HDBSCAN. This is a result of the need for a large number of clusters, combined with a smaller overall dataset size, such that UMAP + HDBSCAN can run very quickly. In combination this results in UMAP + HDBSCAN being only slightly slower than KMeans. On the other hand EVoC turned out results extremely quickly -- more than 3 times faster than KMeans. So for pure speed EVoC turns out to be a winner here.\\n\",\n    \"\\n\",\n    \"How about clustering quality?\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 19,\n   \"id\": \"6a0814a6-36cb-45a8-90fd-fa2e9b25d45f\",\n   \"metadata\": {\n    \"execution\": {\n     \"iopub.execute_input\": \"2026-03-25T20:46:41.488922Z\",\n     \"iopub.status.busy\": \"2026-03-25T20:46:41.488756Z\",\n     \"iopub.status.idle\": \"2026-03-25T20:46:42.125507Z\",\n     \"shell.execute_reply\": \"2026-03-25T20:46:42.124937Z\",\n     \"shell.execute_reply.started\": \"2026-03-25T20:46:41.488907Z\"\n    }\n   },\n   \"outputs\": [\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"<seaborn.axisgrid.FacetGrid at 0x741f8cdc76b0>\"\n      ]\n     },\n     \"execution_count\": 19,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    },\n    {\n     \"data\": {\n      \"image/png\": \"iVBORw0KGgoAAAANSUhEUgAAA90AAAPeCAYAAAARWnkoAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAxxxJREFUeJzs3Qd8FNXexvEnhRACJPTeexUEpCkWVBAVwYoNy7W+elWwo9euF3sXrKhcGxZQvCrFRlWaoEjvNbQACTWQZN/P/+zdkE12Q8uQ9vu+n31lZ3Zmz87uzZlnTpkIn8/nEwAAAAAAyHOReb9LAAAAAABA6AYAAAAAwEO0dAMAAAAA4BFCNwAAAAAAHiF0AwAAAADgEUI3AAAAAAAeIXQDAAAAAOARQjcAAAAAAB4hdAMAAAAA4BFCN4DD9uijj6pt27Zhnxckp556qgYMGKCC6pprrlHfvn2LzPsAQFFAPZd/6tWrp5dffjlP97l7925deOGFio+PV0REhLZv367C4Ndffy1U5UV4hG4Amjp1qqKionTWWWcd0dG4++679dNPPxXKoPzBBx+4Ci3wqFq1qnr37q158+apIKDCBYCjRz0XoebNm+c4Lp9//rmr+yzoFpYL2kdyof/DDz/UpEmT3O8gMTFRCQkJKmhCHdOuXbsW2PLi8BC6gXzm8/mUlpaWr2UYNmyYbrvtNk2ePFmrV68+7O3LlCmjihUrqrCyK99Wqa1fv17fffeddu3apXPOOUf79u3L76IBQKFHPZf/SpcurU2bNum3337LUf/XqVNHRd2yZcvcRYdWrVqpWrVq7kLD4UpPT1dGRoaOpZiYmCMuLwoWQjcKLbsiaEHRrgqWL1/etVC+/fbbLjBde+21Klu2rBo2bKgffvghaLv58+fr7LPPdkHRtunfv7+2bNmSuX7MmDE66aSTVK5cORckzz33XPfHOsCC2D//+U9Vr15dsbGx7urw4MGD3bqVK1e6P4xz5szJfL11CbJl1mKZteVy7Nix6tChg0qWLOmuvtpJybPPPqsGDRqoVKlSatOmjb788kvPj6MdL7vS/X//93/us1rLb3ZPP/20O1Z2TK+77jrt3bs316vOoa7WWtdm6+IcMGTIEDVu3NgdQ9v3RRdd5JbbayZMmKBXXnkls/XZjuuhfHf2Wa666iq33r6fF1544ZCOgb2HVWq2jX0nAwcO1KpVq7Ro0aLM17z44otq3bq1O3GpXbu2brnlFu3cuTNzvR03+83Y92oVu5XBeg5YmM9aYd95552Zv617773Xfe+HI6/eJ7ffm60744wz3H4D29nv2E7MHnzwwcMqL4AjRz2XN6jnpOjoaF1++eUuZAesXbvWnZPY8oMNR7I63X6PudXTgfopq6+//jooMNr5VJ8+fVwdbvXXCSecoB9//PGovt9AeZ9//nlXj1u9d+utt2r//v1uvZXbzgcmTpzoyhL4HNu2bXPnDHYOGRcXp169emnJkiWZ+w18nv/+979q0aKFO1+zcwM773vyySczzzfq1q2rb775Rps3b3afzZbZ+cLMmTMz95WUlKTLLrtMtWrVcu9l6z/99NOgzxDqmIbq7fbVV1+pZcuWrjxWluznOrbs3//+t/7xj3+48zaru+38GPmL0I1CzboLVapUSdOnT3cB3ILjxRdf7Lrj/PHHH+rZs6cLZjaWx1gwOeWUU1xAtD+GFrA3btyoSy65JKhytsAyY8YM12U6MjJS559/fubVzVdffVWjR492QdVC2UcffXTY3bKMBSEL6wsWLNBxxx2nf/3rX3r//fc1dOhQ17XZgt+VV17p/giHc/PNN7s/7rk9DtZyPWLECDVt2tQ97P2sDFkDmn3ORx55RE899ZQ7ZlahWWA+Graf22+/XY8//rg7hvY9nHzyyW6dVThdunTRDTfc4L4ve1jIPZTv7p577tEvv/yiUaNGady4ca6ymjVr1mGVzSq2Tz75xP27RIkSmcvtd2Df/d9//+1+dz///LP7DrOy35lV+v/5z39c5W7H3rreB1jFaCc87733nutVsHXrVlfWw5UX75Pb780qePuM9r8r+8yB35qdJNkFFgDHDvUc9Vxe1XN20dzq/MA5kYVKu7hqf9sPR7h6+lDYxWq7eG5Be/bs2e48zYZ0HUkvu6zsmFigt//a/2bsswUaEUaOHOnKamW2strzQNC18wk7p7MeAHbuY2ULhHVjx8rO1d59911XV1apUsUtf+mll3TiiSe6z2A94+xc00K41aN2/tmoUSP3PHA+ZY0V7du3dwHeziNuvPFGt820adMO65jad22/h0svvVRz5851dfJDDz2Uo8HEzgOsEcHKZ40Edn68cOHCozrGOEo+oJA65ZRTfCeddFLm87S0NF/p0qV9/fv3z1yWmJho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QoUKuRagq6++Wps3b/ZtX7BggTp27OhalOy12rZtq8WLF/sdw1ql3nvvPV1zzTWuBalatWr69ttvfdv379+vm266ScWKFVNkZKTbbn8wI+urWLGiXnvtNb91DRs21ODBg30/ews0V155pfvZ16pVS3PnztX69etd1818+fKpZcuW2rBhg+/59n/7ntn3NH/+/GrWrJmmT5+e5nWffvpp9ezZ0+1TunRpvfHGGxn0rpEZ05RYIE75sN9Jd9xxh/u+/PLLL377z5o1S+vWrXPbk5KSNGTIEPf7zY5j30/73ei1bds29e3b1z3s9599L+37dckll7jfW0888QQ/cDhcU5ERuK4i2LimZh2E7gvA2LFjdf3117vAfeutt7p1M2fO1I4dO9wfqK+88ooLRhaG7I/X+fPnq0+fPu6xdetWt/+RI0fUrl07F2rsOb/++qv7v90h83bZPHjwoHr16uX+yJ03b54LzNZF09anZAHfzuePP/5w2y1k79u3z217/PHHXcvS5MmTtWrVKg0fPtyFeFwYLBzbd3Dp0qWqWbOmC8p33XWXBg0apIULF7p97rvvPt/+hw4dct8RC9pLlixRp06d1LVrV8XExPgd96WXXlL9+vXdTR471gMPPKBp06Zl+PtD5qlXr567KZP6Jp2F5+bNm6tu3bp6/fXXXWv1yy+/7H7/2PfpqquucqHcfPnll+732YABAwK+ht2gBLimIivhuopg4JqaCWyebmQ/bdu29fznP//xvPXWW57o6GjPzJkzfdt69erlqVChgicxMdG3rkaNGp42bdr4lk+ePOnJly+fZ8yYMW75/fffd/skJSX59jl+/LgnMjLSM2XKlIDnYMcoUKCA57vvvvOts6/Uf//7X9/yoUOHPCEhIZ7Jkye75a5du3puu+228/Y5IOPYd+rVV1/1W9egQQPPk08+GfBnP3fuXLfOvlte9n2LiIg47evUrl3b88Ybb/i9bufOnf326dGjh6dLly7n/J6QtdjvrtDQUPe7KeVjyJAhbvvw4cPd8sGDB92y/WvLI0aMcMulS5f2PPPMM37HbNasmeeee+5x/7/77rs9UVFRGf6+kPVxTUVm4LqKYOKamrXQ0p2N2XhEq2A+depU10qdUp06dZQr16kfr3XftbtaXqGhoa4LeWxsrFtetGiR6wZcoEAB18JtD+uGfuzYMV93YNvXWserV6/uupfbw1oqU7dKWoukl3UptmN6X+fuu+92rQjW7dNam+bMmROkTweZIeXP3r5zJuX3ztbZdyo+Pt4tHz582H0PrJCVtTLa92716tVpvlPWLT31svWUwIXHfpdZT4mUj3vvvddtu/HGG10X8s8//9wt2792v+eGG25w3ynr3XPxxRf7Hc+Wvd8V29eGQQCBcE1FVsR1FeeCa2rWEZbZJ4CzZ8HVuttad0vrdpnyj8nw8HC/fW1boHX2B6yxf5s0aaJPP/00zevY+GtjY8V3797txvVWqFDBjROx8JO6YvDpXqdLly7asmWLvv/+e9eluEOHDu4PausOiqzNbuKkLmJ14sSJdH/23u9joHXe74PVGJgyZYr7+VetWtWN8+/evfsZVaEmPF2Y7EadfRcCsRt99v2w33k2htv+teWoqCjfjZzU34uUQdtuGFrBNCvOVqpUqQx4N8hOuKYio3FdRbBxTc06aOnOxqpUqeKmvvnmm290//33n9OxGjdu7MY9Fi9e3P3Bm/Jhf+gaG8ttBYhsDK61pFvo3rNnz99+LQvxFuA/+eQTF+BHjhx5TueOjGE/NwsrXhZyNm3adE7HtO+UfRes8J61iFvRrJTF+7yshkDqZRszjpzHwvbs2bM1ceJE968tGwveVmTP6lGkZL1prKifsYBuFc5ffPHFgMc+cOBABrwDZFVcU5HRuK4is3FNzTi0dGdz1nJjwduq8IaFhaWpLn2mrNiZFauyStLe6r/WxXfChAmuNdKWLYB//PHHatq0qQtctt5aJv8Oqw5sLeoW2o8fP+7+cPb+QYyszeZKtimbrNCZFeSzong2TOFc2HfKvmN2TGuNtGN6W8FTsnBlQcmq8lsBNSuIZb0lcOGx3wu7du3yW2e/27wFF23WBPveWME++9cqj3vZ76Qnn3zShSdrtbSWcOue7u3BU65cOb366quumJ/9DrNjWPVgq2puhShteAPThuVsXFORkbiuIti4pmYdhO4LQI0aNVy1cgveZxuCbIonq1o+cOBAXXvtta4ieZkyZVz3b2tB8lYJ/ve//61GjRqpfPnybkqyhx566G+9jrUyWfVpa820wN6mTRs3xhtZn/3cNm7c6KrgW+8Hq6h6ri3dFoBuv/12tWrVyoUq+/55uwmn1L9/f1d3wCrjW40AC0ZWmRoX5lRNqbt+2+84G+vvZd+ZRx991IXslKwnjn1/7PtidSSsVoBNWWgzLXjdc889LljZkAbrYXH06FEXvO17/eCDD2bAO0RWxzUVGYXrKoKNa2rWEWLV1DL7JAAgPRaIrGCgPQAAwLnhugpkPMZ0AwAAAAAQJIRuAAAAAACChO7lAAAAAAAECS3dAAAAAAAECaEbgKt8/3cLldkUX19//bX7v1Wjt2WbngkAgJyMayqA1AjdAAAAAAAECaEbAAAAAIAgIXQDcJKSkjRgwAAVLlxYJUuW1ODBg32fzLp163TJJZcoIiJCtWvX1rRp0wJ+aqtXr1arVq3cfnXq1NFPP/3k27Z//37ddNNNKlasmCIjI1WtWjWNGjXKt33btm264YYb3Ovny5dPTZs21fz58922DRs26Oqrr1aJEiWUP39+NWvWTNOnT08z7+izzz6r22+/XQUKFFD58uU1cuRIfroAgAzHNRVASoRuAM7o0aNd2LWg++KLL2rIkCEuXNsfDtdee61CQ0M1b948vfPOOxo4cGDAT+3hhx9W//79tWTJEhe+r7rqKu3du9dte/zxx7Vy5UpNnjxZq1at0vDhw1W0aFG37dChQ2rbtq127Nihb7/9Vr///ru7AWCv7d1+xRVXuKBtx+7UqZO6du2qmJgYv9cfNmyYC+u2zz333KO7777b3QgAACAjcU0F4McDIMdr27atp3Xr1n6fQ7NmzTwDBw70TJkyxRMaGurZunWrb9vkyZM99uvjq6++csubNm1yy88//7xvnxMnTnjKli3reeGFF9xy165dPbfddlvAz3rEiBGeAgUKePbu3XvGP4vatWt73njjDd9yhQoVPDfffLNvOSkpyVO8eHHP8OHDc/zPFwCQcbimAkiNlm4ATv369f0+iVKlSik2Nta1SltX7bJly/q2tWzZMuCnlnJ9WFiYa3W25xtrdR47dqwaNmzoWrHnzJnj29eqnjdq1Mh1LQ/k8OHD7jnWtb1gwYKui7m1YKdu6U75HqyaunWTt/cAAEBG4poKICVCNwAnPDzc75Ow0Grduz0ea8RWmm1nyrtvly5dtGXLFjc1mXUj79Chgx566CG3zcZ4n451Wx8/fryeeeYZzZo1y4X0evXqKSEh4YzeAwAAGYlrKoCUCN0ATstal61F2YKy19y5cwPua2O+vU6ePKlFixapZs2avnVWRK1379765JNP9Nprr/kKnVmLgAXpffv2BTyuBW173jXXXOPCtrVg29zgAABkJ1xTgZyJ0A3gtC677DLVqFFDt956qytwZgH4scceC7jvW2+9pa+++sp1/b733ntdxXKrJm6eeOIJffPNN1q/fr1WrFihiRMnqlatWm7bjTfe6IJ0t27dNHv2bG3cuNG1bHvDfdWqVTVhwgQXzO0cevbsSQs2ACDb4ZoK5EyEbgCn/yWRK5cL0sePH1fz5s115513um7egTz//PN64YUX1KBBAxfOLWR7K5Tnzp1bgwYNcq3aNv2YVUO3Md7ebVOnTlXx4sVdlXJrzbZj2T7m1VdfVaFChVxFdKtabtXLGzduzE8OAJCtcE0FcqYQq6aW2ScBAAAAAMCFiJZuAAAAAACChNANAAAAAECQELoBAAAAAAgSQjcAAAAAAEFC6AYAAAAAIEgI3QAAAAAABAmhGwAAAACAICF0AwAAAAAQJIRuAH42b96skJAQLV26NMu81qWXXqp+/foF/XwAADjfuK4CIHQDyDTlypXTzp07VbduXbf8008/uRB+4MABfioAAHBdBS4IYZl9AgBypoSEBOXOnVslS5bM7FMBACDb47oKZF20dAM50A8//KDWrVurYMGCKlKkiK688kpt2LAh3f2//fZbVatWTZGRkWrXrp1Gjx6dpkV6/PjxqlOnjvLkyaOKFStq2LBhfsewdUOHDlXv3r0VHR2tf/3rX35d7uz/dmxTqFAht9729UpKStKAAQNUuHBhF9QHDx7sd3zbf8SIEe695M2bV7Vq1dLcuXO1fv161z09X758atmy5WnfJwAAZ4PrKoDT8gDIccaNG+cZP368Z+3atZ4lS5Z4unbt6qlXr54nMTHRs2nTJo/9arD1xpbDw8M9Dz30kGf16tWeMWPGeMqUKeP22b9/v9tn4cKFnly5cnmGDBniWbNmjWfUqFGeyMhI969XhQoVPFFRUZ6XXnrJs27dOvdI+VonT55052TLdoydO3d6Dhw44J7btm1b99zBgwe7cx49erQnJCTEM3XqVN/x7Xl2Xp9//rl7frdu3TwVK1b0tG/f3vPDDz94Vq5c6WnRooWnc+fOGf55AwAubFxXAZwOoRuAJzY21oXWZcuWpQndAwcO9NStW9fvU3rsscf8QnfPnj09HTt29Nvn4Ycf9tSuXdsvdFsQTin1a/34449+x/Wy0N26dWu/dc2aNXPn5vtlJnn++9//+pbnzp3r1r3//vu+dXbDICIigp84ACCouK4CSInu5UAOZF2se/bsqcqVKysqKkqVKlVy62NiYtLsu2bNGjVr1sxvXfPmzf2WV61apYsvvthvnS2vW7dOiYmJvnVNmzY963OuX7++33KpUqUUGxub7j4lSpRw/9arV89v3bFjxxQfH3/W5wEAQGpcV7muAqdDITUgB+rataurHP7uu++qdOnSbry0VRC3IiypWSOyjZdOve7v7mNsXPXZCg8P91u217PzTm8f7/kEWpf6eQAAnAuuq1xXgdMhdAM5zN69e13LtBUda9OmjVv366+/prt/zZo1NWnSJL91Cxcu9FuuXbt2mmPMmTNH1atXV2ho6Bmfm1UzNylbxwEAyMq4rgL4K3QvB3IYqwxuFctHjhzpKnvPnDlTDz74YLr733XXXVq9erUGDhyotWvX6osvvtCHH37o13Lcv39/zZgxQ08//bTbx6qbv/nmm3rooYf+1rlVqFDBHXPixInavXu3Dh06dI7vFgCA4OK6CuCvELqBHCZXrlwaO3asFi1a5LqUP/DAA3rppZfS3d/Ge48bN04TJkxwY6aHDx+uxx57zG2z6cFM48aNXRi349oxn3jiCQ0ZMsRvyq8zUaZMGT311FN65JFH3Pjr++677xzfLQAAwcV1FcBfCbFqan+5FwCk8Mwzz+idd97R1q1b+VwAADhHXFeBCxtjugH8pbfffttVMLdu6bNnz3Yt47RCAwBwdriuAjkLoRvAX7Kpv4YOHap9+/apfPnybgz3oEGD+OQAADgLXFeBnIXu5QAAAAAABAmF1AAAAAAACBJCNwAAAAAAQULoBgAAAAAgSAjdAAAAAAAECaEbAAAAAIAgIXQDAAAAABAkhG4AAAAAAIKE0A0AAAAAQJAQugEAAAAAUHD8H4cKrQUcY5blAAAAAElFTkSuQmCC\",\n      \"text/plain\": [\n       \"<Figure size 1000x1000 with 4 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    }\n   ],\n   \"source\": [\n    \"sns.catplot(\\n\",\n    \"    bird_results[bird_results.measure != \\\"Elapsed time\\\"], \\n\",\n    \"    x=\\\"algorithm\\\", \\n\",\n    \"    y=\\\"value\\\", \\n\",\n    \"    hue=\\\"algorithm\\\", \\n\",\n    \"    col=\\\"measure\\\", \\n\",\n    \"    kind=\\\"swarm\\\", \\n\",\n    \"    col_wrap=2,\\n\",\n    \"    height=5,\\n\",\n    \")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"368d1989-4016-47e0-b8eb-ccfed6a90b18\",\n   \"metadata\": {},\n   \"source\": [\n    \"The first thing to note here is how hard it is o cluster this dataset well. Both ARI and AMI scores in the 0 to 0.6 range represent a poor match against the ground-truth labels. This was a recent (2023) Kaggle challenge dataset though, so we should expect it to be very hard -- and we are trying to get results in an entirely unsupervised fashion. With the relative poor quality overall out of the way, how do the different methods compare? Here EVoC is a clear winner in pure ARI and AMI quality. It got there though by clustering even less of the data than UMAP + HDBSCAN. In fact it often clustered less than half the data. We would rather have clusters that are good, and leave hard to cluster points unclustered than have things forced into clusters just to get good coverage, so we don't necessarily see this as bad. Even accounting for the difference in amount clustered using the clustering score EVoC still comes out ahead. This shows how these algorithms perform when the going gets rough.\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"478fc28c-48fa-4a4a-b45d-7dd94ecd8cd8\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Other high dimensional data\\n\",\n    \"\\n\",\n    \"Sometimes you just have high dimensional data that isn't necessarily directly from some neural embedding model. We can still try to cluster it and see if EVoC can do a decent job. To try this out let's use the classic MNIST digits dataset. We know that we *should* be able to find good clusters for this data even just using the raw pixel values. So let's see how we do with EVoC on MNIST data.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 20,\n   \"id\": \"3d3f6754-d952-45fd-b96b-1644c141b26b\",\n   \"metadata\": {\n    \"execution\": {\n     \"iopub.execute_input\": \"2026-03-25T20:46:42.126177Z\",\n     \"iopub.status.busy\": \"2026-03-25T20:46:42.126019Z\",\n     \"iopub.status.idle\": \"2026-03-25T20:46:42.142323Z\",\n     \"shell.execute_reply\": \"2026-03-25T20:46:42.141841Z\",\n     \"shell.execute_reply.started\": \"2026-03-25T20:46:42.126164Z\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from sklearn.datasets import fetch_openml\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 21,\n   \"id\": \"0abebd46-634b-45ed-96e3-beb7ba44d5f6\",\n   \"metadata\": {\n    \"execution\": {\n     \"iopub.execute_input\": \"2026-03-25T20:46:42.142955Z\",\n     \"iopub.status.busy\": \"2026-03-25T20:46:42.142804Z\",\n     \"iopub.status.idle\": \"2026-03-25T20:46:45.795859Z\",\n     \"shell.execute_reply\": \"2026-03-25T20:46:45.795225Z\",\n     \"shell.execute_reply.started\": \"2026-03-25T20:46:42.142942Z\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"mnist_ds = fetch_openml('mnist_784')\\n\",\n    \"mnist_data = mnist_ds.data.values.astype(np.float32, order=\\\"C\\\")\\n\",\n    \"mnist_target = mnist_ds.target.values.astype(np.uint8)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"2e5aeed4-a19a-410c-a580-8aa7fe515388\",\n   \"metadata\": {},\n   \"source\": [\n    \"We can run the benchmarks. This time parameter tuning is a little easier.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 22,\n   \"id\": \"7eebb0b3-c2df-4e64-ba75-6594da5382f3\",\n   \"metadata\": {\n    \"execution\": {\n     \"iopub.execute_input\": \"2026-03-25T20:46:45.796609Z\",\n     \"iopub.status.busy\": \"2026-03-25T20:46:45.796456Z\",\n     \"iopub.status.idle\": \"2026-03-25T20:51:16.994356Z\",\n     \"shell.execute_reply\": \"2026-03-25T20:51:16.993592Z\",\n     \"shell.execute_reply.started\": \"2026-03-25T20:46:45.796595Z\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"mnist_results = run_dataset_benchmarks(\\n\",\n    \"    mnist_data, \\n\",\n    \"    mnist_target, \\n\",\n    \"    n_runs=16, \\n\",\n    \"    kmeans_kwargs={\\\"n_clusters\\\":10}, \\n\",\n    \"    umap_hdbscan_kwargs={\\n\",\n    \"        \\\"min_samples\\\":5,\\n\",\n    \"        \\\"min_cluster_size\\\":1200, \\n\",\n    \"        \\\"metric\\\":\\\"cosine\\\", \\n\",\n    \"        \\\"cluster_selection_method\\\":\\\"leaf\\\"\\n\",\n    \"    }\\n\",\n    \")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"23339d19-8803-4f06-806b-474a32e0f1a9\",\n   \"metadata\": {},\n   \"source\": [\n    \"As always let's begin with time taken to compute the clusterings:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 23,\n   \"id\": \"6eb39efd-0a6c-4505-b9ed-c369210056f2\",\n   \"metadata\": {\n    \"execution\": {\n     \"iopub.execute_input\": \"2026-03-25T20:51:16.995048Z\",\n     \"iopub.status.busy\": \"2026-03-25T20:51:16.994885Z\",\n     \"iopub.status.idle\": \"2026-03-25T20:51:17.193178Z\",\n     \"shell.execute_reply\": \"2026-03-25T20:51:17.192628Z\",\n     \"shell.execute_reply.started\": \"2026-03-25T20:51:16.995034Z\"\n    }\n   },\n   \"outputs\": [\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"<seaborn.axisgrid.FacetGrid at 0x7420506d7650>\"\n      ]\n     },\n     \"execution_count\": 23,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    },\n    {\n     \"data\": {\n      \"image/png\": \"iVBORw0KGgoAAAANSUhEUgAAAxYAAAMVCAYAAADqKmIJAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAUPxJREFUeJzt3Qd4VeX9B/Bf2DNhiCLKcoAibnEr7lVx1W1FrbXWvaqWWmer1FFHa+to66hWba3buitOcICzigqIgAqigEzZ+T/v4Z+YkEDRQyafz/PcJznvOffcN/fkJvd731VQXFxcHAAAADk0yHNnAAAAwQIAAFgutFgAAAC5CRYAAEBuggUAAJCbYAEAAOQmWAAAALkJFgAAQG6CBQAAkJtgAUCtVFBQEA8++GDUx3o+99xz2f2+/vrrKqsXQHUTLACodkcffXT2xnrx2x577FHvrsYOO+wQp59+ermyrbfeOsaPHx9FRUU1Vi+A5a3Rcj8jAHVCcXFxLFiwIBo1qpl/BSlE3HrrreXKmjZtGiuCJk2aRMeOHWu6GgDLlRYLgO/xCfQpp5ySfQrdtm3bWGWVVeLmm2+OmTNnxjHHHBOtW7eONddcMx5//PFy93v//fdjr732ilatWmX3OfLII+Orr74q3f/EE0/EtttuG23atIn27dvH3nvvHaNGjSrdP3fu3Dj55JNj1VVXjWbNmkW3bt1i4MCB2b5PPvkk+8T/rbfeKj0+dbNJZanbTdnuN08++WRsttlm2Zv4F198MQsYV1xxRayxxhrRvHnz2HDDDeNf//pXlf9epMdPb67L3tLzuSTnnntu9OjRI1q0aJHV9fzzz4958+aV7r/oootio402iptuuik6d+6cHXfQQQeV626UnoPNN988WrZsmT3P22yzTYwZM6Z0/yOPPBKbbrpp9vymx7j44otj/vz5pftHjBgR22+/fba/V69e8fTTT//Plpnnn38+rrvuutJWmXStFu8Kddttt2X1efTRR6Nnz55Z3Q888MDsd+r222/PrnV6btLvXQqDZX8nzjnnnFhttdWyn2mLLbYovd4A1U2wAPge0pu9lVZaKV577bXszd4JJ5yQvYlNXVzeeOON2H333bPgMGvWrOz41O2lb9++2RvfoUOHZiHiiy++iIMPPrj0nOlN5Jlnnhmvv/56/Oc//4kGDRrE/vvvHwsXLsz2//73v4+HH344/vnPf8aHH34Yd955Z/aG87tKb0RTIBk+fHhssMEG8atf/SprObjhhhvivffeizPOOCN+9KMfZW+Il+RnP/tZFpCWdhs7duxy/d1KgS29AU8BLb1R//Of/xzXXHNNuWNGjhyZPT8pIKTnOAWtk046KduXAsJ+++2XXYd33nknhgwZEj/96U+zN/hJClzp5z711FOzx0gBJT3epZdemu1P1+GAAw6Ihg0bxiuvvBI33nhjFnaWJtVzq622iuOOOy77HUi3FHoqk35X0jW+5557srqngJAe77HHHstud9xxRxZgy4a+FGRffvnl7D7pZ0q/g6klKAUggGpXDMB30rdv3+Jtt922dHv+/PnFLVu2LD7yyCNLy8aPH1+c/sQOGTIk2z7//POLd9ttt3LnGTduXHbMhx9+WOnjTJw4Mdv/7rvvZtunnHJK8U477VS8cOHCCseOHj06O/bNN98sLZsyZUpWNmjQoGw7fU3bDz74YOkxM2bMKG7WrFnx4MGDy53v2GOPLT7ssMOW+Bx88cUXxSNGjFjqbd68eUu8/1FHHVXcsGHD7Hkre7vkkktKj0l1feCBB5Z4jiuuuKJ40003Ld2+8MILs3Om57XE448/XtygQYPsekyaNCk753PPPVfp+bbbbrviyy67rFzZHXfcUbzqqqtm3z/55JOVnv9/1TP9vpx22mnlykquRbpGya233pptjxw5svSY448/vrhFixbF06dPLy3bfffds/IkHVtQUFD82WeflTv3zjvvXDxgwIAl1gegqhhjAfA9pE/6S6RPsFPXpfXXX7+0LHV1SiZOnJh9HTZsWAwaNCj7JH9xqbtT6uKTvqbuPenT8NRFqqSlIn3y37t376xbza677pp1lUmfSqeuUrvtttt3rnvqBlUifTI/e/bs7LxlpS42G2+88RLPsfLKK2e3PHbccceslaSsdu3aLfH49En9tddem7VKzJgxI2uBKCwsLHdMly5dYvXVVy/dTq0F6XlMLTyppSI9h6k1Kf28u+yyS9ZilLqWlVyj1FpU0kKRpG5H6flJrQmphaey8y8vqftT6kJX9ncotUiV/Z1JZSW/U6llLOWv9LtT1pw5c7LfR4DqJlgAfA+NGzcut52605QtK+leUxIO0td+/frF5ZdfXuFcJW9s0/7UTSZ18enUqVN2nxQo0pv8ZJNNNonR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+bY8tVosydghrgpAHWNWKACqzqfDIu46OGLWV4u201oV254ZsfP5Ea1XXfL9lrYPgFpJiwUAVWPhwkWDsktCRZK6P714VcTHz0f0/mFE83YV79e2e8Tau7oqAHWMYAFA1fhsaMTXYyrf99/7IpoVRhz5QMSqG35b3nWbiCPvXzQOA4A6RVcoAKpu9qel7fvgsUWtF1+NjFh5vYgtjo/Y9ChXA6COEiwAqBqr94lo1TFixoSK+wo7RdxzeOobtWh74nsRj5wa0bxNRK99XRGAOkhXKACqRsNGEfvfENG4ZfnyTY6K+HjQt6GirBd/52oA1FFaLACoOmvuFHH6u4vGVMz+OmKtnSNW2zRiYOfKj//yQ1cDoI4SLACoWi3bR2zx0/JlHXpGfPp6xWNTOQB1kq5QAFS/7c5Ki1osoRyAukiwAKD69dwz4tC7IlbbbNEYjE4bRxx8h4HbAHVYQXFxcSWj5+qPadOmRVFRUUydOjUKCwtrujoAAFAvabEAAAByEywAAIDcBAsAACA3wQIAAMhNsAAAAHITLAAAgNwECwAAIDfBAgAAECwAAICap8UCAADITbAAAAByEywAAIDcBAsAACA3wQIAAMhNsAAAAHITLAAAgNwECwAAIDfBAgAAyE2wAAAAchMsAACA3AQLAAAgN8ECAADITbAAAAByEywAAIDcBAsAACA3wQIAAMhNsAAAAHITLAAAgNwECwAAIDfBAgAAyE2wAAAAchMsAACA3AQLAAAgN8ECAADITbAAAAByEywAAIDcBAsAACA3wQIAAMhNsAAAAHITLAAAgNwECwAAIDfBAgAAyE2wAAAAchMsAACA3AQLAAAgN8ECAADITbAAAAByEywAAIDcBAsAACA3wQIAAMhNsAAAAHITLAAAgNwECwAAIDfBAgAAyE2wAAAAchMsAACA3AQLAAAgN8ECAADITbAAAAByEywAAIDcBAsAACA3wQIAAMhNsAAAAHITLAAAgLodLF544YXo169fdOrUKQoKCuLBBx8st7+4uDguuuiibH/z5s1jhx12iPfee6/G6gsAANTCYDFz5szYcMMN4/rrr690/xVXXBFXX311tv/111+Pjh07xq677hrTp0+v9roCAABLVlCcmgVqgdRi8cADD8R+++2XbadqpZaK008/Pc4999ysbM6cObHKKqvE5ZdfHscff3yl50nHpFuJadOmRefOnWPq1KlRWFhYTT8NAACsWGrtGIvRo0fHhAkTYrfddista9q0afTt2zcGDx68xPsNHDgwioqKSm8pVAAAACtosEihIkktFGWl7ZJ9lRkwYEDWOlFyGzduXJXXFQAAVnSNopZLXaTKSl2kFi8rK7VqpBsAAFB9am2LRRqonSzeOjFx4sQKrRgAAEDNqrXBonv37lm4ePrpp0vL5s6dG88//3xsvfXWNVo3AACgFnWFmjFjRowcObLcgO233nor2rVrF126dMlmhLrsssti7bXXzm7p+xYtWsThhx9ek9UGAABqU7AYOnRo7LjjjqXbZ555Zvb1qKOOittuuy3OOeec+Oabb+LEE0+MKVOmxBZbbBFPPfVUtG7dugZrDQAA1Np1LKpKWsciTTtrHQsAAFgBx1gAAAB1h2ABAADkJlgAAAC5CRYAAEBuggUAAJCbYAEAAOQmWAAAALkJFgAAQG6CBQAAkJtgAQAA5CZYAAAAuQkWAABAboIFAACQm2ABAADkJlgAAAC5CRYAAIBgAQAA1DwtFgAAQG6CBQAAkJtgAQAA5CZYAAAAuQkWAABAboIFAACQm2ABAADkJlgAAAC5CRYAAEBuggUAAJCbYAEAAOQmWAAAALkJFgAAQG6CBQAAkJtgAQAA5CZYAAAAuQkWAABAboIFAACQm2ABAADkJlgAAAC5CRYAAEBuggUAAJCbYAEAAOQmWAAAALkJFgAAQG6CBQAAkJtgAQAA5CZYAAAAuQkWAABAboIFAACQm2ABAADkJlgAAAC5CRYAAEBuggUAAJCbYAEAAOQmWAAAALkJFgAAQG6CBQAAkJtgAQAA5CZYAAAAuQkWAABAboIFAACQm2ABAADkJlgAAAC5CRYAAEBuggUAAFBzwWLkyJHx5JNPxjfffJNtFxcX568NAACwYgSLSZMmxS677BI9evSIvfbaK8aPH5+V/+QnP4mzzjqrKuoIAADUt2BxxhlnRKNGjWLs2LHRokWL0vJDDjkknnjiieVdPwAAoA5o9F3v8NRTT2VdoFZfffVy5WuvvXaMGTNmedYNAACory0WM2fOLNdSUeKrr76Kpk2bLq96AQAA9TlYbL/99vG3v/2tdLugoCAWLlwYV155Zey4447Lu34AAEB97AqVAsQOO+wQQ4cOjblz58Y555wT7733XkyePDlefvnlqqklAABQv1osevXqFe+8805svvnmseuuu2Zdow444IB48803Y80116yaWgIAALVaQXE9X4Bi2rRpUVRUFFOnTo3CwsKarg4AANRL37kr1AsvvPA/x2AAAAArlu/cYtGgQcXeU2kAd4kFCxZEbaLFAgAAauEYiylTppS7TZw4MVsYr0+fPtkaFwAAwIrnO3eFSuMVFpcGcac1LNKq3MOGDVtedQMAAOpri8WSdOjQIT788MPldToAAKA+t1ikqWbLSkM0xo8fH7/97W9jww03XJ51AwAA6muw2GijjbLB2ouP+d5yyy3jlltuWZ51AwAA6muwGD16dIVZolI3qGbNmi3PegEAAPU5WHTt2rVqagIAANTvYPH73/9+mU946qmn5qkPAABQXxfI6969+7KdrKAgPv7446hNLJAHAAC1pMVi8XEVAAAAVbKORVWYP39+/OpXv8paTJo3bx5rrLFGXHLJJbFw4cKarhoAAJBn8Hby6aefxsMPPxxjx46NuXPnltt39dVXx/Jy+eWXx4033hi33357rLfeejF06NA45phjstW/TzvttOX2OAAAQDUHi//85z+xzz77ZK0IaaXt3r17xyeffJKta7HJJpvE8jRkyJDYd9994wc/+EG23a1bt7j77ruzgAEAANThrlADBgyIs846K/773/9ma1fcd999MW7cuOjbt28cdNBBy7Vy2267bRZkPvroo2z77bffjpdeein22muvJd5nzpw52YDtsjcAAKCWBYvhw4fHUUcdlX3fqFGj+Oabb6JVq1bZ2IfUdWl5Ovfcc+Owww6LddZZJxo3bhwbb7xxnH766VnZkgwcODDrKlVy69y583KtEwAAsByCRcuWLbNWgaRTp04xatSo0n1fffVVLE//+Mc/4s4774y77ror3njjjWysxVVXXZV9XVqLytSpU0tvqTUFAACoZWMsttxyy3j55ZejV69e2diH1C3q3Xffjfvvvz/btzydffbZ8Ytf/CIOPfTQbHv99dePMWPGZK0SJa0mi2vatGl2AwAAanGwSLM+zZgxI/v+oosuyr5PLQtrrbVWXHPNNcu1crNmzYoGDco3qjRs2NB0swAAUNeDxa9//ev40Y9+lM0C1aJFi/jTn/5UNTWLiH79+sWll14aXbp0yaabffPNN7Ng8+Mf/7jKHhMAAPjuCopTQvgO0lSzTz31VLRv3z7r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AIDcBAsAACA3wQIAAMhNsAAAAHITLAAAgNwECwAAIDfBAgAAyE2wAAAAchMsAACA3AQLAAAgN8ECAADITbAAAAByEywAAIDcBAsAACA3wQIAAMhNsAAAAHITLAAAgNwECwAAIDfBAgAAyE2wAAAAchMsAACA3AQLAAAgN8ECAADITbAAAAByaxT1XHFxcfZ12rRpNV0VAACok1q3bh0FBQUrdrCYPn169rVz5841XRUAAKiTpk6dGoWFhUs9pqC45CP9emrhwoXx+eefL1PKom5KrVEpOI4bN+5//sIDtY/XMNRtXsMrhtZaLCIaNGgQq6++ek1fC6pBChWCBdRdXsNQt3kNY/A2AACQm2ABAADkJlhQ5zVt2jQuvPDC7CtQ93gNQ93mNcwKM3gbAACoelosAACA3AQLAAAgN8ECAADITbCgyuywww5x+umne4YBAFYAggUAwArs6KOPjoKCggq3nXbaKVZaaaX4zW9+U+n9Bg4cmO2fO3fuMj3OoEGDYq+99or27dtHixYtolevXnHWWWfFZ599tpx/ImqKYAEAsILbY489Yvz48eVu9913X/zoRz+K2267LSqbRPTWW2+NI488Mpo0afI/z3/TTTfFLrvsEh07dszO+/7778eNN94YU6dOjd/97ndV9FNR3QQLqs0TTzwRRUVF8be//S37dGS//faLyy67LFZZZZVo06ZNXHzxxTF//vw4++yzo127drH66qvHLbfcUu4c6VONQw45JNq2bZt94rHvvvvGJ598Urr/9ddfj1133TX7BCU9Vt++feONN94od470Kcxf/vKX2H///bNPTNZee+14+OGHS/dPmTIljjjiiOjQoUM0b94825/+eALldevWLa699tpyZRtttFFcdNFFpa+19GZi7733zl5r6667bgwZMiRGjhyZdZVs2bJlbLXVVjFq1KjS+6fv0+s6/V1o1apV9OnTJ5555pkKj/vrX/86Dj/88OyYTp06xR/+8AeXB3KuRZHe9Je9pf+1xx57bPa6fOGFF8od/+KLL8aIESOy/QsXLoxLLrkk+7+dzpP+DqT/+SU+/fTTOPXUU7Nb+r+eXv/pdbz99ttn/48vuOAC166eECyoFvfcc08cfPDBWajo379/Vvbss8/G559/nv2xuvrqq7M3I+kNSPpD9uqrr8bPfvaz7DZu3Ljs+FmzZsWOO+6YvZFI93nppZey79OnLCXNsNOnT4+jjjoq+4P3yiuvZKEgNbum8rJSiEn1eeedd7L9KUhMnjw523f++ednn6Q8/vjjMXz48LjhhhuyoAJ8dykApNf8W2+9Feuss04WBo4//vgYMGBADB06NDvm5JNPLj1+xowZ2WsyhYk333wzdt999+jXr1+MHTu23HmvvPLK2GCDDbIPDtK5zjjjjHj66addIljO1l9//SzgL/4BWwoIm2++efTu3Tuuu+66rNXhqquuyv6vptftPvvskwWP5N57783+T59zzjmVPkb6cJF6Ii2QB1Whb9++xaeddlrxH//4x+KioqLiZ599tnTfUUcdVdy1a9fiBQsWlJb17NmzeLvttivdnj9/fnHLli2L77777mz7r3/9a3bMwoULS4+ZM2dOcfPmzYuffPLJSuuQztG6deviRx55pLQs/dr/6le/Kt2eMWNGcUFBQfHjjz+ebffr16/4mGOOWW7PA9RX6TV8zTXXlCvbcMMNiy+88MJKX2tDhgzJytJruUR6fTdr1mypj9OrV6/iP/zhD+Ued4899ih3zCGHHFK855575v6ZYEWU/ic3bNgw+59b9nbJJZdk+2+44YZse/r06dl2+pq2b7rppmy7U6dOxZdeemm5c/bp06f4xBNPzL4/4YQTigsLC6v956L6abGgSqV+lGlmqKeeeiprbShrvfXWiwYNvv0VTF0f0icjJRo2bJh1d5o4cWK2PWzYsKwLRevWrbOWinRLXaZmz55d2pUiHZtaOXr06JF1hUq39Ano4p92pk86S6TuGOmcJY9zwgknZC0sqSk3fboyePDgKnp2oP4r+1pLr/Gk7Os8laXX8LRp07LtmTNnZq+7NKgzfYqZXucffPBBhddw6kK1+HZqYQS+n/Q/OrUslr2ddNJJ2b7DDjss6+70j3/8I9tOX9NnB4ceemj22k29D7bZZpty50vbJa/JdGzqGkn916imK0D9lt6cp64KqQk1NaWW/cPSuHHjcsemfZWVpT9mSfq66aabxt///vcKj5PGQyRp7MaXX36Z9fvu2rVr1tczveFYfMaKpT3OnnvuGWPGjIl///vfWXeMnXfeOfvjmpp4gW+lDwYWH9A5b968Jb7WSl7/lZWVvP7SGKsnn3wye72ttdZa2TinAw88cJlmnfHGBb6/9CFbes1VJn1Il16H6X95GlORvqbtwsLC0g8FFn/9lQ0T6cO+NEg7DQhfddVVXaZ6TIsFVWrNNdfMppd76KGH4pRTTsl1rk022STrr7nyyitnf/zK3tIfvSSNrUiDw1If7dQikoLFV1999Z0fKwWVFFLuvPPOLKTcfPPNueoO9VF6naQ3CiXSG4zRo0fnOmd6DafXXppcIbVspAGkZSdoKJHGUC2+ncZwAFUjBYqXX345Hn300exr2k5SuEgTKKRxj2Wl1v40YUOSQkiaOeqKK66o9Nxff/21y1ZPaLGgyqVPKlK4SLNANGrUqMIsMssqDbBOAzbTjDEls0+k7hH3339/9iln2k4h44477ojNNtsse5OTytMnnt9Fmp0itYykYDJnzpzsj2jJH0fgW2mO+zQNZRpcnSZdSBMfpC6MeaTXcHpNp3OmTzvTOUtaM8pKb2zSm5Q0u1watJ0Gh6ZWRuD7Sf/vJkyYUK4s/c8umbwkzbKYXp9pMob0Nc3oVCL9r73wwguzDxNTT4XUopG6UpX0MOjcuXNcc8012UQN6X9zOkeaFSrNFpUmdUldHk05Wz8IFlSLnj17ZrNApXDxfd94pOkq02xQ5557bhxwwAHZTE+rrbZa1lUpfWJSMkvFT3/609h4442jS5cu2XS2P//5z7/T46RPVdIsM+lT0hRKtttuu2zMBVBeep18/PHH2WxuqdUwzQCVt8Uivfn48Y9/HFtvvXX2hia93ku6WpSVFtVK467SDG9pjFR6U5JmogG+nzQ97OLdlNL/7jTGqUR6bf7yl7/MgkRZqadAep2m12Uar5jGSKVp3NPMjCVOPPHE7IPG1M0xtUh+8803WbhIfz/OPPNMl62eKEgjuGu6EgCwrNKbkTQpRLoBUHsYYwEAAOQmWAAAALnpCgUAAOSmxQIAAMhNsACgnDR723cdGJ2mhn3wwQez79OMamk7TTcJwIpDsAAAAHITLAAAgNwECwAqSKtdn3POOdGuXbvo2LFjXHTRRaX7RowYka2626xZs2whrLTydWXSwlppobt0XFrJ/rnnnivdN2XKlDjiiCOiQ4cO2UKUaSGttFpvibQi76GHHpo9fsuWLWOzzTaLV199Nds3atSo2HfffWOVVVbJVuzt06dPPPPMMxXWukgLZKYFvdICemnBzJtvvtmVBqhCggUAFdx+++3ZG/r0Zv6KK66ISy65JAsQKXCkle8bNmwYr7zyStx4443Z6tiVSavzppV433zzzSxg7LPPPjFp0qRs3/nnnx/vv/9+PP744zF8+PC44YYbspW2kxkzZkTfvn3j888/z1bvffvtt7OQkx67ZP9ee+2VhYl07rTidr9+/WLs2LHlHj+txp0CSTomrfp7wgknlFtFGIDly3SzAFQYvL1gwYJ48cUXS8s233zz2GmnnbJbelOfBmivvvrq2b4nnngi9txzz3jggQdiv/32y/Z17949fvvb35aGjvnz52dlp5xyShYSUshIQeKWW26p8OynloWf//zn2XlSi8WySC0iKTicfPLJpS0W2223Xdxxxx3ZdnFxcdbycvHFF8fPfvYzVxygCmixAKCCDTbYoNz2qquuGhMnTsxaF1K3opJQkWy11VaVPoNlyxs1apS1HqT7JykE3HPPPbHRRhtlQWPw4MGlx6bZpDbeeOMlhoqZM2dm90ndsNq0aZN1h0otEYu3WJT9GdIsVSlYpJ8BgKohWABQQePGjcttpzfmqStS+uR/cWnfsio5NrVwjBkzJpvWNnV52nnnnbNWiiSNuVia1MXqvvvui0svvTRrVUlBZP3114+5c+cu088AQNUQLABYZqmVILUMpDBQYsiQIZUem8ZglEhdoYYNGxbrrLNOaVkauH300UfHnXfeGddee23p4OrU0pDCwuTJkys9bwoT6X77779/FihSS0TqNgVAzRIsAFhmu+yyS/Ts2TP69++fDapOb/LPO++8So/94x//mI27SN2UTjrppGwmqDRLU3LBBRfEQw89FCNHjoz33nsvHn300Vh33XWzfYcddlgWFtJ4jZdffjk+/vjjrIWiJMCstdZacf/992fhI9Xh8MMP1xIBUAsIFgAs+z+NBg2ysDBnzpxsQPdPfvKTrEtSZdLg7csvvzw23HDDLICkIFEy81OTJk1iwIABWetEmro2zTKVxlyU7Hvqqadi5ZVXzgaKp1aJdK50THLNNddE27Zts5mm0mxQaVaoTTbZxFUEqGFmhQIAAHLTYgEAAOQmWAAAALkJFgAAQG6CBQAAkJtgAQAA5CZYAAAAuQkWAABAboIFAACQm2ABwFJ98sknUVBQEG+99VateawddtghTj/99CqvDwDLTrAAoNbo3LlzjB8/Pnr37p1tP/fcc1nQ+Prrr2u6agD8D43+1wEAUB3mzp0bTZo0iY4dO3rCAeogLRYAxBNPPBHbbrtttGnTJtq3bx977713jBo1aonPzMMPPxxrr712NG/ePHbccce4/fbbK7Qs3HfffbHeeutF06ZNo1u3bvG73/2u3DlS2W9+85s4+uijo6ioKI477rhyXaHS9+ncSdu2bbPydGyJhQsXxjnnnBPt2rXLwshFF11U7vzp+Jtuuin7WVq0aBHrrrtuDBkyJEaOHJl1pWrZsmVstdVWS/05AVh2ggUAMXPmzDjzzDPj9ddfj//85z/RoEGD2H///bM374tLb/gPPPDA2G+//bIAcPzxx8d5551X7phhw4bFwQcfHIceemi8++672Zv+888/P2677bZyx1155ZVZt6d0fNq/eLeoFE6SDz/8MOsidd1115XuT2EmhYNXX301rrjiirjkkkvi6aefLneOX//619G/f/+snuuss04cfvjhWX0HDBgQQ4cOzY45+eST/QYALA/FALCYiRMnFqd/Ee+++27x6NGjs+/ffPPNbN+5555b3Lt373LHn3feedkxU6ZMybYPP/zw4l133bXcMWeffXZxr169Sre7du1avN9++5U7ZvHHGjRoULnzlujbt2/xtttuW66sT58+Wd1KpPv96le/Kt0eMmRIVvbXv/61tOzuu+8ubtasmesPsBxosQAg6w6UPs1fY401orCwMLp37549K2PHjq3w7KTWgz59+pQr23zzzcttDx8+PLbZZptyZWl7xIgRsWDBgtKyzTbb7Hs/+xtssEG57VVXXTUmTpy4xGNWWWWV7Ov6669frmz27Nkxbdq0710PABYxeBuA6NevX9b16M9//nN06tQp6wKVuiilAdWLS40BafzC4mXf9ZgkdWX6vho3blxuOz3e4l23yh5TUp/Kyirr8gXAdyNYAKzgJk2alLUwpIHO2223XVb20ksvLfH4NFbhscceK1dWMl6hRK9evSqcY/DgwdGjR49o2LDhMtctzRKVlG3lAKB20hUKYAWXZlxKM0HdfPPN2YxJzz77bDaQe0nS4OcPPvggzj333Pjoo4/in//8Z+mg7JIWgLPOOisbBJ4GT6dj0kDr66+/Pn7+859/p7p17do1O+ejjz4aX375ZcyYMSPnTwtAVREsAFZwaQaoe+65J5uZKXV/OuOMM7LZmpYkjb/417/+Fffff382huGGG24onRUqTS2bbLLJJlngSOdN57zggguyWZvKThe7LFZbbbW4+OKL4xe/+EU2HsIMTgC1V0EawV3TlQCgbrv00kvjxhtvjHHjxtV0VQCoIcZYAPCd/elPf8pmhkpdqF5++eWshUNrAsCKTbAA4DtL08amVbMnT54cXbp0ycZUpEXnAFhx6QoFAADkZvA2AACQm2ABAADkJlgAAAC5CRYAAEBuggUAAJCbYAEAAOQmWAAAALkJFgAAQOT1f8xuJkFR8i9xAAAAAElFTkSuQmCC\",\n      \"text/plain\": [\n       \"<Figure size 800x800 with 1 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    }\n   ],\n   \"source\": [\n    \"sns.catplot(\\n\",\n    \"    mnist_results[mnist_results.measure == \\\"Elapsed time\\\"], \\n\",\n    \"    x=\\\"algorithm\\\", \\n\",\n    \"    y=\\\"value\\\", \\n\",\n    \"    hue=\\\"algorithm\\\", \\n\",\n    \"    kind=\\\"swarm\\\", \\n\",\n    \"    col=\\\"measure\\\",\\n\",\n    \"    height=8,\\n\",\n    \")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"eb2378ce-abfd-42e2-add6-c1e4287ec40b\",\n   \"metadata\": {},\n   \"source\": [\n    \"This produces a pretty dramatic result: UMAP + HDBSCAN take a long time to compute for all 70,000 MNIST digits, while KMeans, as we would expect for large data and a small number of clusters (only ten), takes around two seconds. On the other hand EVoC also comes in at a hair above two seconds to cluster the data. Not quite at KMeans speed, but very close.\\n\",\n    \"\\n\",\n    \"How about the clustering quality? This is the kind of high dimensional dataset that KMeans can tend to struggle with.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 30,\n   \"id\": \"9168cb00-a657-44d6-a990-22414b2456ce\",\n   \"metadata\": {\n    \"execution\": {\n     \"iopub.execute_input\": \"2026-03-26T17:37:39.604436Z\",\n     \"iopub.status.busy\": \"2026-03-26T17:37:39.604204Z\",\n     \"iopub.status.idle\": \"2026-03-26T17:37:40.255561Z\",\n     \"shell.execute_reply\": \"2026-03-26T17:37:40.254937Z\",\n     \"shell.execute_reply.started\": \"2026-03-26T17:37:39.604404Z\"\n    }\n   },\n   \"outputs\": [\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"<seaborn.axisgrid.FacetGrid at 0x741fa8702ab0>\"\n      ]\n     },\n     \"execution_count\": 30,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    },\n    {\n     \"data\": {\n      \"image/png\": \"iVBORw0KGgoAAAANSUhEUgAAA90AAAPeCAYAAAARWnkoAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAzbBJREFUeJzs3Qd4VNW6xvE3pNISeu9VepcmIqggKIgFsSFgRT12PYpdjoq9i2Lvig1siGIDBBRp0nsLXVpCDSGZ+3wrd4aZZCYUM6T9f/eZi7Pb7L1nTtb+VvlWhMfj8QgAAAAAAOS4Ijl/SAAAAAAAQNANAAAAAEAY0dINAAAAAECYEHQDAAAAABAmBN0AAAAAAIQJQTcAAAAAAGFC0A0AAAAAQJgQdAMAAAAAECYE3QAAAAAAhAlBN4Cj9uCDD6ply5Yh3+clp5xyim6++WblVYMHD1a/fv0KzOcAQEFAOZd7atWqpeeeey5Hj7l3716dd955io+PV0REhHbu3Kn84LfffstX54vQCLoBaOrUqYqMjNQZZ5xxTHfj9ttv188//5wvA+V33nnHFWjeV8WKFdWnTx8tWLBAeQEFLgD8e5RzEWrUqFGW+/Lpp5+6ss8C3fxSoX0sFf3vvvuuJk+e7H4HGzduVEJCgvKaYPe0U6dOefZ8cXQIuoFc5vF4dPDgwVw9h7feeks33HCDfv/9d61du/ao9y9RooTKli2r/Mpqvq1Q27Bhg7777jvt2bNHZ555pg4cOJDbpwYA+R7lXO4rXry4tmzZomnTpmUp/2vUqKGCbsWKFa7SoWnTpqpUqZKraDhaaWlpSk9P1/EUExNzzOeLvIWgG/mW1QhaoGi1gqVLl3YtlK+99poLmIYMGaKSJUuqbt26+v777wP2W7hwoXr37u0CRdtn4MCB2rp1q2/9+PHjddJJJ6lUqVIukDzrrLPcH2svC8T+85//qHLlyoqLi3O1wyNGjHDrVq9e7f4wzpkzx7e9dQmyZdZi6d9y+cMPP6ht27aKjY11ta/2UPLEE0+oTp06Klq0qFq0aKHPP/887PfR7pfVdF977bXuWq3lN7PHHnvM3Su7p1dccYX279+fba1zsNpa69psXZy9Ro4cqfr167t7aMc+//zz3XLbZuLEiXr++ed9rc92X4/ku7Nrueyyy9x6+36efvrpI7oH9hlWqNk+9p3ccsstWrNmjZYsWeLb5plnnlGzZs3cg0v16tV13XXXaffu3b71dt/sN2PfqxXsdg7Wc8CCef8C+9Zbb/X9tv773/+67/1o5NTnZPd7s3WnnXaaO653P/sd24PZPffcc1TnC+DYUc7lDMo5KSoqShdffLELsr3WrVvnnkls+eGGI1mZbr/H7Mppb/nkb+zYsQEBoz1PnX322a4Mt/KrXbt2+umnn/7V9+s936eeesqV41buXX/99UpNTXXr7bzteWDSpEnuXLzXsWPHDvfMYM+QxYoVU69evbRs2TLfcb3X8+2336px48buec2eDey57+GHH/Y9b9SsWVNfffWV/vnnH3dttsyeF2bMmOE71rZt23TRRRepWrVq7rNs/ccffxxwDcHuabDebl988YWaNGnizsfOJfOzji179NFHdfnll7vnNiu77fkYuYugG/madRcqV66cpk+f7gJwCxz79+/vuuPMmjVLPXv2dIGZjeUxFph07drVBYj2x9AC7M2bN+uCCy4IKJwtYPnrr79cl+kiRYronHPO8dVuvvDCC/r6669doGpB2QcffHDU3bKMBUIWrC9atEjNmzfXvffeq7fffluvvPKK69psgd+ll17q/giHMnToUPfHPbvX4VquR48erYYNG7qXfZ6dg3+AZtf5wAMP6JFHHnH3zAo0C5j/DTvOjTfeqOHDh7t7aN/DySef7NZZgdOxY0ddddVV7vuylwW5R/Ld3XHHHfr11181ZswY/fjjj66wmjlz5lGdmxVsH330kfvv6Oho33L7Hdh3P3/+fPe7++WXX9x36M9+Z1bov//++65wt3tvXe+9rGC0B54333zT9SrYvn27O9ejlROfk93vzQp4u0b735Vds/e3Zg9JVsEC4PihnKOcy6lyzirNrcz3PhNZUGmVq/a3/WiEKqePhFVWW+W5BdqzZ892z2k2pOtYetn5s3tiAb39a/+bsWvzNiJ8+eWX7lztnO1c7b030LXnCXumsx4A9uxj5+YN1o3dK3tWe+ONN1xZWaFCBbf82WefVefOnd01WM84e9a0INzKUXv+rFevnnvvfZ6yxoo2bdq4AN6eI66++mq3z59//nlU99S+a/s9XHjhhZo3b54rk++7774sDSb2HGCNCHZ+1khgz8eLFy/+V/cY/5IHyKe6du3qOemkk3zvDx486ClevLhn4MCBvmUbN260v3ae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aPLkydlu/+GHH6pFixYqVqyYKleurCFDhmjbtm3H7XwBHIHI6MCA21z8qdT0PKlIdEYreJ1u0uBvpbh4bikAAAAKrFydp3v06NEaOHCgC7w7d+6sUaNG6Y033tDChQtVo0aNLNv//vvv6tq1q5599ln16dNH69ev19ChQ1W/fn2NGTPmiD6T+USBXJa6X/KkSTHFc/tMAOQAylUAAPJwS/czzzyjK664QldeeaUaNWqk5557TtWrV9crr7wSdPs//vhDtWrV0o033uhax0866SRdc801mjFjxnE/dwDHyFrACbgBAABQSORa0H3gwAHNnDlTPXr0CFhu76dOnRp0n06dOmndunUaN26crIF+8+bN+vzzz3XmmWeG/JyUlBRXC+//AgAAx4ZyFQCAfBJ0b926VWlpaapYsWLAcnu/adOmkEG3jekeMGCAYmJiVKlSJZUqVUovvvhiyM8ZMWKEEhISfC9rSQcAAMeGchUAgHyWSC0iIiLgvbVgZ17mZWO9rWv5/fff71rJx48fr1WrVrlx3aEMGzZMSUlJvldiYmKOXwMAAIUF5SoAAPlkyrBy5copMjIyS6v2li1bsrR++9euW8K1O+64w71v3ry5ihcvri5duujhhx922cwzi42NdS8AAPDvUa4CAJBPWrqte7hNETZhwoSA5fbeupEHs3fvXhUpEnjKFribXEzCDgAAAABA3utefuutt7opwt566y0tWrRIt9xyi9auXevrLm5d2C677DLf9jZN2Jdffumym69cuVJTpkxx3c1PPPFEValSJRevBAAAAACAPNS93FhCtG3btmn48OHauHGjmjZt6jKT16xZ0623ZRaEew0ePFi7du3SSy+9pNtuu80lUevevbsef/zxXLwKAAAAAACCi/AUsn7ZNmWYZTG3pGrx8fG5fToAAORrlKsAAOTx7OUAAAAAABRUBN0AAAAAAIQJQTcAAAAAAGFC0A0AAAAAQJgQdAMAAAAAECYE3QAAAAAAFMR5ugEUQP8skVb8KhUtJTXqI8UUP7TOZijcOEc6eECq2lqKjM7NMwUAAADCjqAbQM4Zf7f0x8t+7++SLv5Uqn6itHmB9NlgaevSjHUlKkl9X5Qa9OAbAAAAQIFF93IAOWPZT4EBt9m3Q/riyoyW7Q8vOBRwm92bpE8HSskb+AYAAABQYBF0A8gZC74MvnznGumv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5+rPnk/uvPNOd5/i4uJUvnx5V+H97bffhv2zgWMVdcx7AsgxHo9HaWlpiorKnf9JnnHGGXr77beVmpqqyZMn68orr3SF2iuvvJJlW9vGgvPcYp8fihW8CH3fcvN7A4C8LjfL4tWrV6tz586uQvmJJ55Q8+bN3d/tH374Qddff70WL16svH7fSpQo4V7hNnToUE2fPl0vvfSSGjdurG3btmnq1Knu33A5cOCAYmJiwnZ8FHy0dCNfs5pNqxW27rylS5dWxYoV9dprr7mAcciQISpZsqTq1q2r77//PmC/hQsXqnfv3q5wsH0GDhyorVu3+taPHz9eJ510kiv8ypYtq7POOksrVqwI+OP7n//8R5UrV3a1rNatecSIEb6C02qO58yZ49veapFtmdUq+9cuW2Hatm1bV6Ntwa4VXFbYWu1t0aJF1aJFC33++edhv4/2+ZUqVXK16hdffLEuueQSX+uqt7vYW2+95at9t/Ncu3atzj77bHcP4+PjdcEFF2jz5s2+Y3r3GzVqlDtusWLF1L9//4Aa9fT0dA0fPlzVqlVzx7Xt7d57ee/lp59+6r5ru9cffPCB+26TkpJ8LfT2WcG6lx/pOb7//vtu34SEBF144YXatWtXtvdrypQp6tq1q7sm+9317NlTO3bsOOKWavtdWWv94X5L9t/mnHPOccfxvjfffPON2rRp4/ax7+Whhx7SwYMHAz731VdfdddfvHhxPfzww0e037Jly3TyySe79fYwM2HChGzvBQBQFv971113nfu7bcHk+eefrwYNGqhJkya69dZb9ccffxxxS7U9e9gyKz/NmjVrXGu5lVVWFtgxx40b59Z369bNbWPrbB/r+WYO9ywS6hkmc/dyO16/fv301FNPuTLOnqesAsG/8nzjxo0688wz3efUrl1bH3300WGHilk5dvfdd7vnONvWyjR7Fhw0aJBvm5SUFP33v/91zx92fvXr19ebb77pWz9x4kSdeOKJbp2d21133RVQFtpv2spmu//lypXT6aeffkTPj0AoBN3I99599133B9EKKvuje+2117rgrlOnTpo1a5YLiOyP4t69e31/4C1gsoJhxowZLsizQMwCMi8L2u0PrXVXtq5SRYoUcYGPBYnmhRde0Ndff+2CwSVLlrhA0D8gOlJWIFiAtWjRIlerfe+997oWZ2thXrBggW655RZdeuml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AAAAhxC6AdiR7/M7UJmZ4uvDDz+0f5vR6M2ymZ4JAIDSjDIVQE6EbgAAAAAAHELoBgAAAADAIYRuAFZmZqbuvfdeValSRTVq1NBDDz2U9cps27ZNvXr1Unh4uFq0aKFly5b5fNU2b96sbt262f1atmypL7/8MmvboUOHdP3116tatWqKiIhQ48aN9dprr2Vt37Nnj6677jr7+OXLl1eHDh30zTff2G3bt2/XlVdeqZiYGFWoUEEdO3bUZ599lmve0ccff1w333yzKlasqHr16mnmzJm8uwCAIkeZCiA7QjcAa/bs2TbsmqD71FNPafLkyTZcmy8OV199tUJCQrRmzRq99NJLuu+++3y+avfcc4/GjRun9evX2/B9xRVXKCkpyW775z//qY0bN2rRokXatGmTpk+frujoaLvt6NGj6t27t/bt26cFCxboxx9/tD8AmMf2bB8wYIAN2ubYl156qQYOHKi4uDivx3/66adtWDf7jBw5Unfeeaf9IQAAgKJEmQrAiwtAqde7d29Xjx49vF6Hjh07uu677z7XkiVLXCEhIa74+PisbYsWLXKZj48PPvjALu/cudMuP/nkk1n7nDp1ylWnTh3XlClT7PLAgQNdw4cP9/laz5gxw1WxYkVXUlLSWb8XLVq0cL3wwgtZy7Gxsa4bbrghazkzM9NVvXp11/Tp00v9+wsAKDqUqQByoqYbgHX++ed7vRI1a9ZUYmKirZU2TbXr1KmTta1r164+X7Xs60NDQ22ts7m/YWqd33nnHbVt29bWYq9atSprXzPq+QUXXGCblvty7Ngxex/TtL1SpUq2ibmpwc5Z0539OZjR1E0zefMcAAAoSpSpALIjdAOwypQp4/VKmNBqmne7XKYSW7m2nS3Pvv3799fu3bvt1GSmGXnfvn01fvx4u8308T4T02x93rx5euyxx7RixQob0lu3bq20tLSzeg4AABQlylQA2RG6AZyRqV02NcomKHusXr3a576mz7dHenq61q1bp2bNmmWtM4OoDRs2TG+++aaee+65rIHOTI2ACdIHDx70eVwTtM39rrrqKhu2TQ22mRscAIDihDIVKJ0I3QDOqF+/fmratKluvPFGO8CZCcATJ070ue9//vMfffDBB7bp96hRo+yI5WY0cePBBx/URx99pF9//VW//PKLPvnkEzVv3txu++tf/2qD9KBBg7Ry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\",\n      \"text/plain\": [\n       \"<Figure size 1000x1000 with 4 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    }\n   ],\n   \"source\": [\n    \"sns.catplot(\\n\",\n    \"    mnist_results[mnist_results.measure != \\\"Elapsed time\\\"], \\n\",\n    \"    x=\\\"algorithm\\\", \\n\",\n    \"    y=\\\"value\\\", \\n\",\n    \"    hue=\\\"algorithm\\\", \\n\",\n    \"    col=\\\"measure\\\", \\n\",\n    \"    kind=\\\"swarm\\\", \\n\",\n    \"    col_wrap=2,\\n\",\n    \"    height=5,\\n\",\n    \")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"22bc9bf2-9559-455f-b0d7-32446de322e3\",\n   \"metadata\": {},\n   \"source\": [\n    \"Again we have some very striking results. KMeans does as we would expect, getting low ARI and AMI scores in the 0.4 to 0.5 range. However EVoC manages to get almost perfect scores, and consistently. Better still, EVoC consistently manages to cluster more than 90% of the data. The end result is that even the clustering score shows EVoC head and shoulders above the competition.\\n\",\n    \"\\n\",\n    \"So, in summary, EVoC is consistently very very fast to compute, coming in as competitive with, and sometimes faster than, KMeans for small to medium sized datasets. EVoC also consistently produces better quality clusters than the much slower to compute UMAP + HDBSCAN option. And it does all of this with essentially no parameter tuning -- we simply have to pick the best option out of a very small number of layers of cluster resolution. Fast, good, and easy -- sometimes you can have all three.\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"e573b8a4-b0b5-42e9-bebc-3cf5cef8f646\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Scaling\\n\",\n    \"\\n\",\n    \"So far we have looked at small to medium sized datasets of \\\"real-word\\\" data because it comes with class labels we can compare clusterings against, and it provides a realistic challenge for the clusterign algorithms to tackle. EVoC performed very well in tersm of run-time, managing to consistently compete with KMeans. But how well do those results generalize to much larger datasets? Now we are less worried about realistic clusterign challenges, we want to see compute time against dataset size, ideally scaling into millions of data samples. To do that we can use sklearn's ``make_blobs`` to generate some easy to cluster high-dimensional data at varying sample sizes. While we are at it, let's add some extra KMeans implementations into the mix. We'll benchmark FAISS's KMeans, as well as sklean's ``MiniBatchKMeans`` which uses a sampling based approach to do a more approximate optimization at much much faster speeds.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 25,\n   \"id\": \"b4191c21-48a8-40ad-b547-061ade561ea2\",\n   \"metadata\": {\n    \"execution\": {\n     \"iopub.execute_input\": \"2026-03-25T20:51:17.870049Z\",\n     \"iopub.status.busy\": \"2026-03-25T20:51:17.869881Z\",\n     \"iopub.status.idle\": \"2026-03-25T20:51:17.970759Z\",\n     \"shell.execute_reply\": \"2026-03-25T20:51:17.970234Z\",\n     \"shell.execute_reply.started\": \"2026-03-25T20:51:17.870034Z\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from sklearn.datasets import make_blobs\\n\",\n    \"import faiss\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"621629aa-1c48-4e83-b2f9-abf767676423\",\n   \"metadata\": {},\n   \"source\": [\n    \"We need similar function wrappers for ``MiniBatchKMeans`` and FAISS: \"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 26,\n   \"id\": \"5075cd5e-fc1a-4b26-b8dd-895aa2156ba2\",\n   \"metadata\": {\n    \"execution\": {\n     \"iopub.execute_input\": \"2026-03-25T20:51:17.971578Z\",\n     \"iopub.status.busy\": \"2026-03-25T20:51:17.971423Z\",\n     \"iopub.status.idle\": \"2026-03-25T20:51:17.974750Z\",\n     \"shell.execute_reply\": \"2026-03-25T20:51:17.974323Z\",\n     \"shell.execute_reply.started\": \"2026-03-25T20:51:17.971564Z\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"def minibatch_kmeans(data, n_clusters=10):\\n\",\n    \"    return sklearn.cluster.MiniBatchKMeans(\\n\",\n    \"        n_clusters=n_clusters, \\n\",\n    \"        n_init=\\\"auto\\\", \\n\",\n    \"        batch_size=4*n_clusters\\n\",\n    \"    ).fit_predict(\\n\",\n    \"        data\\n\",\n    \"    )\\n\",\n    \"\\n\",\n    \"def faiss_kmeans(data, n_clusters=10):\\n\",\n    \"    kmeans = faiss.Kmeans(data.shape[1], n_clusters, niter=50, nredo=1, gpu=False)\\n\",\n    \"    X = np.ascontiguousarray(data, dtype=np.float32)\\n\",\n    \"    kmeans.train(X)\\n\",\n    \"    _, labels = kmeans.index.search(X, 1)\\n\",\n    \"    return labels.ravel()\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"0ce50a1f-73cc-4c26-9f05-fe807aafa04b\",\n   \"metadata\": {},\n   \"source\": [\n    \"Now let's just set up a loop and run everything! We'll scale the dataset sizes from 10,000 up to over 3,000,000, have 4 runs of each implementation at each size, and colelct all the results. If you are running this yourself be warned that (since we are clustering millions of samples many times over) this can take a very long time to complete.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 27,\n   \"id\": \"8d2312a4-6f73-4d8d-b260-0a4529a67408\",\n   \"metadata\": {\n    \"execution\": {\n     \"iopub.execute_input\": \"2026-03-25T20:51:17.975285Z\",\n     \"iopub.status.busy\": \"2026-03-25T20:51:17.975154Z\",\n     \"iopub.status.idle\": \"2026-03-26T09:38:20.359235Z\",\n     \"shell.execute_reply\": \"2026-03-26T09:38:20.357749Z\",\n     \"shell.execute_reply.started\": \"2026-03-25T20:51:17.975274Z\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"algorithms = [\\n\",\n    \"    (\\\"UMAP + HDBSCAN\\\",    lambda X: umap_hdbscan(X)),\\n\",\n    \"    (\\\"Sklearn KMeans\\\",          lambda X: kmeans(X, n_clusters=n_clusters, kmeans_algorithm=\\\"elkan\\\")),\\n\",\n    \"    (\\\"FAISS KMeans\\\",    lambda X: faiss_kmeans(X, n_clusters=n_clusters)),\\n\",\n    \"    (\\\"Sklearn Minibatch KMeans\\\",lambda X: minibatch_kmeans(X, n_clusters=n_clusters)),\\n\",\n    \"    (\\\"EVoC\\\",            lambda X: EVoC(X)),\\n\",\n    \"]\\n\",\n    \"\\n\",\n    \"scaling_results = {\\\"size\\\": [], \\\"time\\\": [], \\\"algorithm\\\": [], \\\"ari\\\": []}\\n\",\n    \"n_runs = 4\\n\",\n    \"\\n\",\n    \"for size in np.logspace(4, 6.5, num=8):\\n\",\n    \"    for n in range(n_runs):\\n\",\n    \"        n_clusters = int(2 * np.sqrt(size))\\n\",\n    \"        blobs, labels = make_blobs(n_samples=int(size), n_features=1024, centers=n_clusters, cluster_std=3.0)\\n\",\n    \"    \\n\",\n    \"        for name, fn in algorithms:\\n\",\n    \"            start_time = time.time()\\n\",\n    \"            clusters = fn(blobs)\\n\",\n    \"            scaling_results[\\\"size\\\"].append(size)\\n\",\n    \"            scaling_results[\\\"algorithm\\\"].append(name)\\n\",\n    \"            scaling_results[\\\"time\\\"].append(time.time() - start_time)\\n\",\n    \"            scaling_results[\\\"ari\\\"].append(sklearn.metrics.adjusted_rand_score(labels, clusters))\\n\",\n    \"\\n\",\n    \"scaling_df = pd.DataFrame(scaling_results)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"956dc26c-ab67-454e-b9f6-77ea85341ff4\",\n   \"metadata\": {},\n   \"source\": [\n    \"Now let's look at the timing results -- how does the runtime scale with increasing dataset size for these different algorithms?\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 28,\n   \"id\": \"e4ffa7e1-889a-494d-8548-a48500176dda\",\n   \"metadata\": {\n    \"execution\": {\n     \"iopub.execute_input\": \"2026-03-26T09:38:20.360185Z\",\n     \"iopub.status.busy\": \"2026-03-26T09:38:20.359995Z\",\n     \"iopub.status.idle\": \"2026-03-26T09:38:21.088775Z\",\n     \"shell.execute_reply\": \"2026-03-26T09:38:21.088224Z\",\n     \"shell.execute_reply.started\": \"2026-03-26T09:38:20.360170Z\"\n    }\n   },\n   \"outputs\": [\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"<seaborn.axisgrid.FacetGrid at 0x74205015be90>\"\n      ]\n     },\n     \"execution_count\": 28,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    },\n    {\n     \"data\": {\n      \"image/png\": \"iVBORw0KGgoAAAANSUhEUgAABLwAAAPdCAYAAACA0HFLAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzs3Qd4k+e5//GfNb0N2NjsMMPO3nuHJEBW10lP2rQ9af5t9mj2IHu0zehJmybdzWmaps0ihJC99w47QCBAAC+8ZFvb/+t5hSUMlrCxbA1/P7l82c+rV9Jr2QT7x33fT05bW1ubAAAAAAAAgCxhS/UFAAAAAAAAAMlE4AUAAAAAAICsQuAFAAAAAACArELgBQAAAAAAgKxC4AUAAAAAAICsQuAFAAAAAACArELgBQAAAAAAgKxC4NVFbW1tamxstN4DAAAAAAAgfRF4dVFTU5NKSkqs9wAAAAAAAEhfBF4AAAAAAADIKgReAAAAAAAAyCoEXgAAAAAAAMgqBF4AAAAAAADIKgReAAAAAAAAyCoEXgAAAAAAAMgqBF4AAAAAAADIKgReAAAAAAAAyCoEXgAAAAAAAMgqBF4AAAAAAADIKgReAAAAAAAAyCoEXgAAAAAAAMgqBF4AAAAAAADIKgReAAAAAAAAyCoEXgAAAAAAAMgqBF4AAAAAAADIKgReAAAAAAAAyCoEXgAAAAAAAMgqBF4AAAAAAADIKgReAAAAAAAAyCoEXgAAAAAAAMgqBF4AAAAAAADIKgReAAAAAAAAyCoEXgAAAAAAAMgqBF4AAAAAAADIKgReAAAAAAAAyCoEXgAAAAAAAMgqBF4AAAAAAADIKgReAAAAAAAAyCoEXgAAAAAAAMgqBF4AAAAAAADIKikNvB544AHttttuKi4utt4OPPBAPffcc9Hb29raNGfOHA0bNkx5eXk64ogjtHjx4g6P4fP5dP7556usrEwFBQWaPXu21q9f3+Gcuro6nXnmmSopKbHezMf19fV99nkCAAAAAACgnwReI0aM0B133KGPPvrIejvqqKN08sknR0Otu+66S3fffbfuv/9+ffjhhxoyZIiOPfZYNTU1RR/joosu0pNPPqlHH31Ub731ljwej2bOnKlQKBQ954wzztBnn32mBQsWWG/mYxN6AQAAAAAAIPvktJkyqjQyaNAg/fKXv9SPf/xjq7LLBFpXXHFFtJqroqJCd955p8455xw1NDRo8ODBevjhh/Xd737XOmfDhg0aOXKk5s+fr+OPP15Lly7VlClT9N5772n//fe3zjEfm2qyZcuWaeLEiZ1eh3ku89ausbHRelzznKYaDQAAAAAAAOkpbWZ4mYosU6XV3NxshVGrV6/Wpk2bdNxxx0XPcbvdOvzww/XOO+9Y648//liBQKDDOSYkmzZtWvScd99912pjbA+7jAMOOMA61n5OZ26//fZoC6R5M2EXAAAAAAAA0l/KA6+FCxeqsLDQCrP+3//7f1Z7oqnIMmGXYSq6tmbW7beZ9y6XSwMHDkx4Tnl5+XbPa461n9OZq666yqrman9bt25dUj5fAAAAAAAA9C6HUsy0FJqZWmaI/OOPP64f/vCHev3116O35+TkdDjfdGBue2xb257T2fk7ehwTwJk3AAAAAAAAZJaUV3iZCq3x48drn332sdoId999d913333WgHpj2yqsqqqqaNWXOcfv91u7MCY6p7Kycrvnra6u3q56DAAAAADQc+Fwmxaub9DrX1Zb780aAPpV4NVZ5ZUZFj9mzBgrrHrxxRejt5lwy1R/HXTQQdZ67733ltPp7HDOxo0btWjRoug5Zh6YaUn84IMPoue8//771rH2cwAAAAAAyfHOyhr98C8f6JyHP9Jlj31uvTdrcxwA+kVL49VXX60TTjjBGgjf1NRkDa1/7bXXtGDBAqvd0OzQeNttt2nChAnWm/k4Pz9fZ5xxhnV/M0z+Jz/5iS699FKVlpZaOzxedtllmj59uo455hjrnMmTJ2vGjBk6++yz9eCDD1rHfvrTn2rmzJlxd2gEAAAAAHSfCbWufnKhPL6gBua75LLb5A+FtXRjk3X8tlOn66DxZby0ALI78DKthmeeeaZVlWXCq912280Ku4499ljr9ssvv1ytra36+c9/brUtmp0WX3jhBRUVFUUf45577pHD4dB3vvMd69yjjz5af/3rX2W326Pn/OMf/9AFF1wQ3c1x9uzZuv/++1PwGQMAAABAdjJtiw+8vsoKu4YU50ZnJufa7BpSbNOmRp91+wFjS2WzJZ7LDAA9ldNmegixQ42NjVYoZ1ohi4uLecUAAAAAYCtmVpdpXyxwO5TrtFsB2NbBVmsgpBZfUA+euY+mjyjhtQPQv2Z4AQAAAAAyz+YWvwKhNquN0YRdoW1qK9x2mwLhNus8AOhtBF4AAAAAgB4blO+S054jXzCkYCe7MvpCYTltOdZ5ANDbCLwAAAAAAD02dVixxpUXanNzQOG2cIfbzCSd+paAdbs5DwB6G4EXAAAAAKDnv1zacvSjg0Yrz2VTjccvbyBktTaa2V1mYH2h266fHT6OgfUA+gSBFwAAAACgx0LhNo0vL9Ilx+6qsYML1eoPqcrjswbVTx5apNtOna6DxpfxSgPoE46+eRoAAAAAQDarbfYpGA5rz1EDtfvIAfqmzivlRGZ7mTbGrXdsBIDeRuAFAAAAAOiRZl9QHm8wurbl5FhVXeXFubyyAFKClkYAAAAAQI9aGWs8Pl5BAGmFwAsAAAAAsNNqPT4r9AKAdELgBQAAAADYKR7TyuiLtTICQLog8AIAAAAAdJup6jLVXQCQjgi8AAAAAADdZuZ20coIIF0ReAEAAAAAuqXJG7B2ZgSAdEXgBQAAAADoskAorFqPn1cMQFoj8AIAAAAAdKuVMdzGrowA0huBFwAAAACgSxpaAmr1h3i1AKQ9Ai8AAAAAwA75g2FtbqGVEUBmIPACAAAAACTU1tamao/Peg8AmYDACwAAAACQUH1LQL4ArYwAMgeBFwAAAAAgLl8wpPrWAK8QgIxC4AUAAAAA6JRpYaxqpJURQOYh8AIAAAAAdGpzs1+BUJhXB0DGIfACAAAAAGzHGwipgVZGABmKwAsAAAAA0EE43KbqJh+vCoCMReAFAAAAAOigllZGABmOwAsAAAAAENXsC6rJy66MADIbgRcAAAAAwBIKt6nGQysjgMxH4AUAAAAAsJiwy4ReAJDpCLwAAAAAAGr0Bqx2RgDIBgReAAAAANDPBUJhbfb4U30ZAJA0BF4AAAAA0M9VN/kUbqOVEUD2IPACAAAAgH6svsUvbyCU6ssAgKQi8AIAAACAfsoXDKmuJZD0x21ra9Ory6us9wCQCgReAAAAANAPmTDKtDL2Rij1+Cff6IrHF+rsv3+kumZmgwHoewReAAAAANAPbW72yx8MJ/1xl29q0kNvfGV9/NLSKt3+3NKkPwcA7AiBFwAAAAD0M2ZmV0Nr8lsZm31B3fzsEgXDkaqximK3rjxhctKfBwB2hMALAAAAAPqRcDjSyphspjXy7he/1IZ6r7W25Uj3fW9PDSpwJf25AGBHCLwAAAAAoB+pafYpEEp+K+Nzizbp1eXV0fVPDhmjA8aWJv15AKArCLwAAAAAoJ8wLYcebzDpj7u6pln/+8rK6HqPkSX68cFjkv48ANBVBF4AAAAA0A8EQ2HVeHy9Mg/s5nlL5NsyAL8kz6mrT5wsu+lpBIAUcaTqiQEAAAAAfafG41doyzD5ZPrtq6u0prYlur5ixkRrbtfSjU1auqlJg/JdmjqsWDYCMAB9iMALAAAAALJcozegFn/yWxlfXValZxdujK6/vfcIuR02XfH4Qq3f3CKTrzntORpXXqifHT5OB40vS/o1AEBnaGkEAAAAgCxmBtRv9viT/rgb6lutXRnbTRpSpH1GD7COrapqsloa81126/2SDY26+smFemdlTdKvAwA6Q+AFAAAAAFmsqsmncFtb0kO0m+ctVbM/ZK0LXHZdfeIkPfbRN6pvCVjzvCobvVpf16KNDV5rztfm5oAeeH2Vwr3QVgkA2yLwAgAAAIAsVdfsly8QCaWS6Y9vrtbyyqbo+tLjJqrFF7Yqu1oDIbUGwgq1yXoLhtusYKzZH9SSDQ1avKEx6dcDANsi8AIAAACALGSqqupbA0l/3HdX1erfH6+PrmfuNlRHTBysula/Gr1Ba25Xu5wcqX2vRjMwv741qJrm5O8UCQDbIvACAAAAgCzT1tam6iaf9T6ZzGPeuWBZdD2mrEDnHjHO+rihJdAh7IoyoVdOLPTqjXliALAtAi8AAAAAyDK1zX5rzlYymbDqlmeXWFVcRq7DputmTpbbabfWHl+CarK2jjtGAkBvc/T6MwAAAAAA+kyrP6TGXmhl/Nu7a7Twm9j8rQuOnqDRpQWxE9rLuDoJutq2bnHs7DwASDIqvAAAAAAgS5gqLNN2mGwff12nf7y3Nro+dkqFjp9a0eGcKUOL5bTlRGd25WwddJlfPnMkp82mPUcOSPr1AcC2CLwAAAAAIEvUenwKhpPbyri52a/b5i+NhlcjBubpoqMnbFeptWtFocaU5Vsf222Sw55jBWDmvW3Lb54ThxRq+vCSpF4fAHSGwAsAAAAAskCTNyCPLzJfK5kVYybsqmuJtEg67Tm6fuYU5bkic7u2ZsvJ0flHT1B5sdv62MzLNyGZeW/Wg4vcuuqEybKZUi8A6GXM8AIAAACADBcMhVXbC7sf/vODtfpkbX10/fMjxmt8eWGn59ptOTph2lCVFrj1u9dWatmmJgVCbVZINmlIkXXfg8aXJf0aAaAzBF4AAAAAkOGqPT6FTSlVEn2xvl5/fWdNdH3YrmWavfvQuOeXF+XKYbdZodYBY0u1eEOjNrf4NSjfpanDiqnsAtCnCLwAAAAAIIM1tASsnRmT/Zi3PLtU4S0Z2pDiXF127MS4OywOzHd1aHM0bYvTRzCrC0DqMMMLAAAAADKULxiyqqiSqa2tTXc+v0w1W1okTavidTMnqzC383oJE3QNLHAl9RoAoKcIvAAAAAAgA5lgqrrJZ71Ppn9/vF7vfbU5uv7poWM0eWhxp+eaMGxwoTupzw8AyUDgBQAAAAAZaHOzX/5gOKmPuWRDo/7w5uro+oCxg/StvUfscG4XAKQb/s8EAAAAABnGGwipoTWQ1Mds8gZ087NLFNoyuMtUbl0xY1KX53YBQDoh8AIAAACADBIOR1oZk8m0Rd61YLkqGyOPa8uRNberJM/Z6fnM7QKQ7tilEQAAAAAySE2zT4FQclsZn/z0G729qja6/vHBYzRteOe7LDpsNquVMaFwWNr0udRSK+WXSkN2N1s3JvWaASARAi8AAAAAyBAeX1AebzCpj7l8U5N+//pX0fW+owfqe/uN7PRc095YXuy2htXH9dXr0lv3SDUrpHBAsjmlsgnSIRdLYw9P6rUDQDxE7AAAAACQAYKhsGo9vqQHaDfNW6LglrldpQUuXXnCJNnizO0alO9SrtOeOOyad5FUuVhyFUiFFZH3Zm2Om9sBoA8QeAEAAABABqj2+KID5ZM1t+tXLyzXxgavtTZFW9eeNNkaRt+ZArdDJfmdz/SKtjGayi6fRyoaKjnzpBxb5L1Zm+PmdnMeAPQyAi8AAAAASHNmR8ZWfyipjzn38w1648ua6PqHB47W7iMHdHqu026zdm1MyMzsMm2MeQNN76MU3up6zdocN7eb8wCglxF4AQAAAEAa8wfD2tzsT+pjrqhs0u9eWxVd7zVqgM7Yf1TCuV22RHO7DDOg3szscrgjYVfbNpVc1vFA5DwA6GUEXgAAAACQpkzbYVWT13qfLM3W3K6lCoQijzkw36mrT5wcdxB9aaFLbkeCuV3tzG6MZkB9wCuFOxmsH/RFbjfnAUAvI/ACAAAAgDRV1xKwKrySxQRnd7/4pb6pb7XWJuK65sTJGlTQ+dyuwlyHinMTzO3a2pDdI7sxtm42T7TtE0utdZHbzXkA0MsIvAAAAAAgDXkDIdW3JLeVcd4XG/Xq8uro+r8PGKW9dhkYd25XWcEO5nZtzWaTDjwvMqS+uUoKeiNtjYFWqWmj5C6SDrk4ch4A9DL+TwMAAAAAaSYcblN1ky+pj7myyqP7X10ZXe8xskQ/OHB0p+facnJUUZy747ld2yqfJB15tVQ6QfK3SJ5Kyd8sVUyVZt4jjT28p58GAHSJo2unAQAAAAD6Sk2zT4FQOMlzu5Z0mNt1zQ7mdrkc3ayP8DZE5neN2E8avo/UsDZy3MzsMm2MVHYB6EMEXgAAAACQRjy+oDzeToa+93Bu1/q62Nyuq06YpNLCztsVi3Kd1lu3hIJSc01snWOTKqZJRUN6dO0AsLNoaQQAAACANGGqumqS3MrY2dyufUYP6vRcU9VVVtj5APuEWmq2H1QPAClE4AUAAAAAacBUYlU1+RROYnC0qptzu8qLcpWT0825XWZGl8/T00sFgKQi8AIAAACANFDXEpAvEEra47X4g7qxt+d2mXCuOVY9BgDpgsALAAAAAFKs1R9SfYs/yXO7VvTu3C6jZXNkfhcApBkCLwAAAABIoVC4TdVJntv17MKNemVZVe/O7Qr6JG99Ty4TAHoNgRcAAAAApFCNx6dgOJzUuV3/+0psbtfuI3phbpfhqWJQPYC0ReAFAAAAACnS0BpQsy95LYHmsbae2zUgz6lrToo/t6usyN39uV1Ga32kwgsA0hSBFwAAAACkgC8Y0ubmZM/t+rLj3K4TJ6kswdyuQrej+09kZna11Pb0cgGgVxF4AQAAAEAfM+GUmdtl3ifL3M836tXl1R3mdu2b7LldRjOtjADSH4EXAAAAAPSx2ma//MHkze36srJJv3stNrdrj5EDEs7tqijeybldvibJ39KTSwWAPkHgBQAAAAB9qMUfVGNrIGmP5zFzu56Jze0amO/UtQnmdg0ucstp34lfBcMhqTlWQQYA6YzACwAAAAD6SCgcaWVMFtMS+avnl2tjg9dam4zLDKkfVNB5u2JJnlMFOzO3y2iukZK4myQA9CYCLwAAAADoIybsMqFXsjz56Qa9saImuv7Bgbtor1EDOz0312mPG4TtkGljNO2MAJAhCLwAAAAAoA80tAasdsZkWbapUb9/fVV0vfeoAfr+/rt0eq5pbywvcu/c3C4zWN8MqgeADELgBQAAAAC9zAyo39zsT9rjNXkDuumZpQpuqRYrLXDp6gRzu8qLcuXYmbldRstmKZS8oA4A+gKBFwAAAAD0IjNnq6rJa71P1uPdtWC5NjXG5naZIfUD8ztvVzRtjHku+849WdAneet7crkAkBIEXgAAAADQi0xll6nwSpb/fPKN3l5VG12fddBo7T5yQKfn5rscGhAnCOsST1WkpREAMgyBFwAAAAD0EjOzy8zuSpYlGxr10BtfRdf7jh6oM/Yf1em5DptNg4vcO/9krXWRCi8AyEAEXgAAAADQC8xujDVNyZvbZYKzm+Ytie7yWFbo0lUnTJKtk0H0Zjh9ebE77kyvHQoFIrO7ACBDEXgBAAAAQC+o8fgUDCenlTHc1qbbn1umqqZIxZXJsa6fOSVuu6KZ25Xr3Mm5XQatjAAyHIEXAAAAACRZozegZl/ydjZ89IN1+mB1rOLq7EPHatrwkk7PLXA7VJLn3Pkn8zZKgdadvz8ApAECLwAAAABIIjOgvtaTvFbGz9fV689vr46uDxxbqu/sM6LTc512mwYX9mBuVzgktdTs/P0BIE0QeAEAAABAkrS1tamqyWu9T9YOjzc/u1RbxnZpSHGurjxhojWjK97cLtvOzu0ymqulJLVhAkAqEXgBAAAAQJKYgMpUeCWDGU5/6/yl1mMaTnuObpg1RUW5zrhzu9yOHszt8jdLPs/O3x8A0giBFwAAAAAkQas/ZO2kmCwPv/u1Pl1bH13/7PBxmjikqNNzC3s6t8tUdZlB9QCQJQi8AAAAACAJ1VjVW3ZQTIYP12zWw+99HV0fsetgnbzHsLhzu8p6MrfLaKmNzO8CgCxB4AUAAAAAPWTCrmCSZl+Zx7pt/jK1TwEbMTBPlx63a+/N7TI7Mnobdv7+AJCGCLwAAAAAoAdMG2OLP5iU1zAYCuuWZ5dEWyNdDptumDlFBW5Hp+eXFvZwbpcZrk8rI4AsROAFAAAAADvJDKhvHyqfDH98a7UWftMYXV9w1HiNKy/s9NzCXIeK4wyw77LWOimUvLljAJAuCLwAAAAAYCe0tbWpqslrvU+Gt1fW6LGP1kfXx02p0AnThsSd2zW4p3O7gv5I4AUAWYjACwAAAAB2gqnsMhVeybChvlV3LFgWXY8uzdeFx0zodG6XLSdHFcW5nd7WLc1VkZZGAMhCBF4AAAAA0E1mZlf7nK2eMqHZjc8sUbMvsktintOuObOnWu/jze0ys716pLVeCnh79hgAkMYIvAAAAACgG0LhNtU0JW9u129fW6kVVZ7o+rLjdtWoQfmdnluU67TeeiQUlFpqe/YYAJDmCLwAAAAAoBvM3K5gODmtjC8trdQzn2+Mrk/eY5iOnFTe6bmmqqus0NXzJ6WVEUA/QOAFAAAAAF1U3+JXqz/SethTa2qbdfcLX0bXE4cU6WeHj+v0XDO3q7woCXO7fE2Sv6VnjwEAGYDACwAAAAC6wBsIqa4lOXO7TGh249wl8m4Zel+U69ANM6fEnc1VVuTu+dyucEhqru7ZYwBAhiDwAgAAAIAdCIfbVN3kU1sSdjU0j3HPS1/q682xSqsrZ0zSkJLcTs8vznOq0O3o+deoucZ8Ij1/HADIAAReAAAAALADNR6fAqHkhEXzvtiol5ZWRdff23ekDhxX2um5bqddpQVJmNvlb460MwJAP0HgBQAAAAAJNHoD8viCSXmNvqxs0v2vroyudxtRop8cMqbTc+02M7fL3fO5XaaqyxML2ACgPyDwAgAAAIA4fMGQaj3+pLw+Td6A5sxdokAo0hY5MN+p606abAVbnSkrdMtpT8KvbC21kfldANCPEHgBAAAAQJxZW1WNyZnbFW5r0+3PLdOmRm/kF7Ec6dqTJqu00N3p+SV5ThUkY25XwCt5G3r+OACQYQi8AAAAAKATNR5/0uZ2/evDdXrvq83R9Y8PHqM9Rw3s9Nxcp12DkjG3ywR1nsqePw4AZCACLwAAAADYhpnZZVoQk+GzdfX601uro+sDxg7S9/Yb2btzu4zWOimUnM8BADINgRcAAAAAbMVUddU0+ZLymtR6fLp53hKFt3RFDinO1ZUzJskWJ9AqL8qVIxlzu4L+SOAFAP0UgRcAAAAAbD23q8lnzdzqqVC4TTc/u1R1LZEqK6c9RzfMmqLiPGen5w/MdynPZU/O18K0MibhcwCATEXgBQAAAABbmHDKF0jOjoamjfGL9bGB8eceOV4ThxR1em6+y6GByZjbZbTWS8HkVKgBQKYi8AIAAAAAkxP5Q6pv8SfltXh7ZY0e/XBddH3M5HLN2m1op+c6bDYNLup8t8ZuCwWlltrkPBYAZDACLwAAAAD9nmk/rE7S3K4N9a26Y8Gy6Hp0ab4uPnbXTgfRm2PlxW5rWH1SNFfRyggABF4AAAAAICvsCobDPX4p/MGw5jyzRM2+SFtkntOuObOmWu87Myjfpdw4t3Wbt1HytyTnsQAgw1HhBQAAAKBfa2gJqMUfTMpj/e8rK7WyyhNdX3bcrhpVmt/puQVuh0ryOx9g323hkNRSk5zHAoAsQOAFAAAAoN/yBUPanKS5XQsWbdKzCzdG16fuOVxHTirv9Fyn3abBhUma22U010hJqFADgGxB4AUAAACgXwqH21TV6FNbW1uPH2tVlUf3vrwiup4ytEj/7/CxnZ7bPrfLlqy5Xf5mydeUnMcCgCxB4AUAAACgX6pt9isQ6nlVlMcb1A3PLLbmdxkleU5dP3OKVcXVmdJCl9yOJM3tMlVdnqrkPBYAZBECLwAAAAD9jscXVJM30OPHMdVhdy5Ypg31XmttarauOXGSyotzOz2/MNeh4twkze0yWmoj87sAAB0QeAEAAADoV0xVV02TLymP9a8P1+ntVbXR9VkHjdY+owd1eq7LkeS5XQGv5G1I3uMBQBYh8AIAAADQb5iKrKomn8JJmNv1+bp6/fGt1dH1fmMG6fsHjOr0XJuZ21WUa83vSgpz/Z7K5DwWAGQhAi8AAAAA/UZdS0C+QM9bAGs9Pt00b4nCW3KzimK3rj5hkhVsdWZwkduq8Eqa1jop1POWTADIVikNvG6//Xbtu+++KioqUnl5uU455RQtX768wzlnnXWW9a8gW78dcMABHc7x+Xw6//zzVVZWpoKCAs2ePVvr16/vcE5dXZ3OPPNMlZSUWG/m4/r6+j75PAEAAACkXqs/pPoWf48fJxgK66Z5S63wzHDaczRn1lQV53U+m8sMsS9wO3r8vLEL8EUCLwBAegZer7/+us4991y99957evHFFxUMBnXcccepubm5w3kzZszQxo0bo2/z58/vcPtFF12kJ598Uo8++qjeeusteTwezZw5U6FQ7F9uzjjjDH322WdasGCB9WY+NqEXAAAAgOwXCrepOklzu0wb48JvYrOzzjtyvCYOKer03FynXYMKXEoqsytjEloyASCb5bSZJvY0UV1dbVV6mSDssMMOi1Z4mUqsp556qtP7NDQ0aPDgwXr44Yf13e9+1zq2YcMGjRw50grGjj/+eC1dulRTpkyxgrX999/fOsd8fOCBB2rZsmWaOHHido9rqsbMW7vGxkbrMc3zFRcX99IrAAAAAKA3bGrwqsUf7PHjvLGiWnPmLomuj51SoStnTOx0NpfdlqPhA/LksCe5lbE5NiQ/rbkLpaIhqb4KAP1UWs3wMmGSMWhQx11NXnvtNSsI23XXXXX22WerqqoqetvHH3+sQCBgVYa1GzZsmKZNm6Z33nnHWr/77rtWG2N72GWYtkhzrP2cztot29sfzZsJuwAAAABknoaWQFLCrrWbW3TXgtgIlrFlBbr4mAlxB9GbIfVJDbvMzK6Wzcl7PADIYmkTeJlCs0suuUSHHHKIFVa1O+GEE/SPf/xDr7zyin7961/rww8/1FFHHRWtvtq0aZNcLpcGDhzY4fEqKiqs29rPMYHZtsyx9nO2ddVVV1kBXPvbunXrkvwZAwAAAOht3kBIm5Mwt6s1ENKcuYvV4o+MTcl32XXDrClWy2JnBua7lOfq/LadRisjAHRZEicn9sx5552nL774wprBtbX2NkXDBGH77LOPdtllFz377LM67bTTEgZoW/9LS2f/6rLtOVtzu93WGwAAAIDMFN4yt6unU1zM/e9+4UutqW2JHrtixiSNHJTf6fn5LocGJntul7dBCrQm9zEBIIulRYWX2WFx7ty5evXVVzVixIiE5w4dOtQKvFasWGGthwwZIr/fb+3CuDXT9miqvNrPqays7HRmWPs5AAAAALJLTbNPgVC4x4/z1Gcb9PKy2FiV7+07UodOKOv0XIfNpsFFSf6H81BQaq5J7mMCQJZLaeBl/qXEVHY98cQTVsvimDFjdnif2tpaq73QBF/G3nvvLafTae3y2M7s5Lho0SIddNBB1toMpzdtiR988EH0nPfff9861n4OAAAAgOzR5A3I4+353K7FGxr0wGurous9RpboJ4d0/nuL6R4pL3Zbw+qTqrmaXRkBIJN2afz5z3+uRx55RE8//XSHnRLNkPi8vDx5PB7NmTNHp59+uhVwrVmzRldffbXWrl1r7bxYVBTZ+vdnP/uZ5s2bp7/+9a/WwPvLLrvMCsbMQHu73R6dBWZ2b3zwwQet9U9/+lOrUuyZZ57p0rWaXRrNdbFLIwAAAJDe/MGwNtS3KtzDX3XqWvw65+GPVeOJzAArLXTpwf/eW4PitCuWFrpVkudUUvmapKbtu1UyArs0AuivM7weeOAB6/0RRxzR4fhf/vIXnXXWWVZYtXDhQv39739XfX29FXodeeSR+te//hUNu4x77rlHDodD3/nOd9Ta2qqjjz7aCr/awy7DDL6/4IILors5zp49W/fff3+ffa4AAAAAep/59/yqJm+Pw65QuE23PLs0GnaZqq0bZk6JG3YVuh3JD7vCoUh1FwAgsyq8MgkVXgAAAED6q/H41Nga6PHj/PHNr/TIB7Gd2s89cpxO36vzecNOu03DB+TJluxWRlPZZSq8MhUVXgD6+9B6AAAAAOgpjy+YlLDr7ZU1HcKuIycO1ml7Du/0XFtOjiqKc5MfdvlbMjvsAoAUI/ACAAAAkPHMbow1Tb4eP843da26Y8Gy6HqXQfm67LiJ1kD6zpQVueVyJPnXqnBY8mTo3C4ASBMEXgAAAACyYG6Xr8dzu7yBkG54ZrGafSFrnee068bZU5Xnis0G3lpxntOa3ZV0LbWR+V0AgJ1G4AUAAAAgo21u9ssXCPU4NLv7xS/1VXVz9Ngvjp+oUaX5nZ7vdtpVGmeAfY8EWiVvQ/IfFwD6GQIvAAAAABmrxR9UQxLmds39fINeWloVXX977xE6YuLgTs81OzaWF7njtjnuNFOhRisjACQFgRcAAACAjBQMhVWdhLldSzY06revroqudxtRop8eNjbu+YOL3NbOjL3SyhgKJv9xAaAfIvACAAAAkJHM3K5QuGdzu+pa/JrzzGIFtzyOaVO8fuYUq4qrMwPzXcp39cLcroBXaq1P/uMCQD9F4AUAAAAgI+d2mSHzPWHCspvnLVWNx2+tTch1w6wpGhRnNpcJugb2xtwuWhkBIOkIvAAAAABklFZ/SPUtkZCqJ/701mp9ti5WVfWzw8dq2vCSTs81LYymlbFXtGyWQj2fQwYAiCHwAgAAAJBRc7uqmrw9fpw3VlTr0Q/XRddHTSrXqXsO7/RcM5zehF3x2hx7JOiTvLQyAkCyEXgBAAAAyBjVnp7P7Vq7uUV3LVgeXY8pK9Clx+0ad9fF0kKXcp129Voro3kPAEgqAi8AAAAAGaGu2W+1M/aEuf8NcxerZcvjFLjsmjNrivLiBFqFuQ4V5zp79JzxL6ZOCva8NRMAsD0CLwAAAABpzwRVZkfFnmhra9OvXliur2tboscunzFJIwfld3q+y2HT4MJemttlWhlN4AUA6BUEXgAAAADSmmlhrG7y9fhx/vPJN3p1eXV0/V/7jdShE8o6PdeWk6OK4ty4bY495qmilREAehGBFwAAAIC0ZobUB8PhHj3G5+vq9eDrq6LrPUcN0I8PHhP3fDOk3uzM2Gu7MpoKLwBAryHwAgAAAJC26lt6PrfLVIfdNG+J2mfdlxe5dd1Jk+Puujgg36UCt0O9wszsopURAHodgRcAAACAtOQNhLS5uWdzuwKhsG58ZrHqWgLW2mnP0ZzZU6xQqzN5LrsGFXR+W1KwKyMA9AkCLwAAAABpOberqrHnbX+/e3WVlmxsiq7PP2qCJg0p7vRch82m8qJc9RpaGQGgzxB4AQAAAEg7pg2xp3O7Xli8SU9/viG6PnHaEM3cbWin55rh9OXF7rhtjj1GKyMA9CkCLwAAAABpN7erxR/s0WOsqGzS3S+tiK4nVhTpgqMnxD1/UL5LuU67eg2tjADQpwi8AAAAAGTV3K7G1oDmPLNE/mCkQqw416EbZk+Ry9H5rz+FbodK8p3qNWZIPbsyAkCfIvACAAAAkDVzu8xj3DZ/qTY2eK216VC89qTJGlLc+Wwup92mskK3eo1pZTSzuwAAfYrACwAAAEDWzO36+7tr9MGauuj6xweP0T6jB3V6ri0nRxXFubL11twuo7lKamvrvccHAHSKwAsAAABAVsztemdVjR5+b210ffD4Uv3XfiPjnl9a6Irb5pi0VsZApNIMANC3CLwAAAAAZPzcrvV1Lbp9/rLoesTAPF0xY5K1+2JnivOcKsrtxbldoQCtjACQQgReAAAAADJ6blerP6Trn16sZn/IWuc57brp5KnWMPrOuJ12lRa41KvYlREAUorACwAAAEDGzu1qa2vTL59frjW1LdFjV8yYqNGlBZ2eb7flqKLIHbfyKyla62llBIAUI/ACAAAAkLFzu/798Xq99mV1dP29fUfqsF0Hxz2/vChXDrutl1sZa3vv8QEAXULgBQAAACAj53Z9srZOD73xVXS996gB+skhY+KeX1rgVp7Lrl5FKyMApAUCLwAAAAAZN7erqtGrm+ctVbgtsi4vcuvak6ZYLYudMfO8SvJ7cUi9wa6MAJA2CLwAAAAAZNTcLn8wrBvmLlFDa8BaO+051pD6eIGW025TWaFbvSroZ1dGAEgjBF4AAAAAMmZulxlSf9/LK7S8sil67OJjdtWuFUWdnm/LyVFFca5scSq/kqa5ylxc7z4HAKDLCLwAAAAA9NncrrqWSFXWznp24UY9t2hTdD1792GaMW1I3PMHF7nlcvTyrz20MgJA2iHwAgAAANBnc7tMhdbOWrKhUb95eWV0PWVokc49clzc8wfku1TgdqhX0coIAGmJwAsAAABAr6vx9Gxul9nR8YZnFiu4ZUr9wHynbpg11ZrP1Zl8l0ODClzqdbQyAkBaIvACAAAA0KvMcPlm387P7QqEwrrxmcWq9fittdmJcc6sqVa7YmdMCBbvtqSilREA0haBFwAAAIBe4wuGrOqsnnjgtVVa+E1jdP3zI8Zp+oiSTs/NyclRebHbCsV6Fa2MAJDWCLwAAAAA9IpwEuZ2vbB4k576bEN0fdyUCp2yx7C455cVuuR22NXrPJXsyggAaYzACwAAAECvze0y7Yg768vKJt390oroekJ5oS4+ZoJVxdWZolyn9dYnrYxBX+8/DwBgpxF4AQAAAEi6Rm9Anh7M7WpoCej6pxfLH4wEZiV5Tt148lS5nZ1Xb5njprqr19HKCAAZgcALAAAAQFKZkKp9wPzOCIXbdNOzS1TVFKmiMuO4rps5WUOKczs938zrqihyx638ShrTmkkrIwBkBAIvAAAAAElj5nVVNnp7NLfrD29+pU/X1kfXZx86VnuNGhj3/PKiXDnsffCrDa2MAJAxCLwAAAAAJE11D+d2vbqsSo99tD66PnLiYH1nnxFxzx9U4FKeqw+G1JuZXSbwAgBkBAIvAAAAAMmb2+Xd+bldq6o9+uXzy6PrsWUFuuz4iXFbFQvcDg3I74O5XbQyAkDGIfACAAAA0GO+YKhHc7saWyND6r1bhtQXuh3WkPq8OEPqnXabBhe61SesVsad/9wAAH2PwAsAAABAj5gh81WNvp2e22Xuf8uzS7WxwWutTT3XNSdN0vABeZ2eb8vJUUVxrmxmmn1vC3hpZQSADETgBQAAAKBHqpt6Nrfrz2+v1kdfx+Zj/fiQ0dp/TGnc88uK3HI5+uBXGVoZASBjEXgBAAAA2Gn1LX61+Hd+btdry6v1zw/WRdeHTSjTGfuNint+SZ7TanfsEy2bpVCgb54LAJBUBF4AAAAAdkqrP6TNzTs/2+qrao/uWrAsut6lNF+Xz4g/pN7sxljaV3O7aGUEgIxG4AUAAACg24KhsKqaIjO3dkaTN6Dr58aG1Be47br55KnKd3VeveWw2VRelNs3X6n2VkYAQMYi8AIAAADQLWY4fVWTzxo2vzPM/W59dqk21G81pP7EyRoxML/T803FV3mxW/a+GFJvtNTSypgMPk9SHgYAdgaBFwAAAIBuMW2M3kBop1+1v76zRh+siQ2p/9HBo3XA2PhD6ksLXcp12vvmqxRolVrr++a5slnNl9Ifj5Y+eyTVVwKgnyLwAgAAANBlzb6gGlp3fpD7G19W6x/vr42uDxlfpjP2jz+kvijXqeJcZ998hcJhWhmToblGmneR5K2XnvqZ9PpdSXlYAOgOAi8AAAAAXRIIhVXd5NvpV2t1TbPu2HpI/aB8XXnCRNniDKl3O+0qK3T13VfHamXc+R0nYYa7eaX5l8SCwxybNGwvXhoAfY7ACwAAAECX53aFzUD3nRxSf93Ti+QNbBlS77LrpgRD6s28rooid9wdG5PO3yJ5G/rmubKV+d54+UapcnHs2Iw7pAnHpPKqAPRTBF4AAAAAdqi22S/fTs7tMkPqb9lmSP3VJ07WyEGdD6k3zI6MDnsf/bpCK2NyfPCQtOKF2Hr3/5L2+2mSHhwAuofACwAAAMAO53Y19mBu15/fXq0PtxpSf9bBo3XguARD6gvcynP10ZB6o6VGCu/8EH5I+nKB9OFDsZdi5P7SkdeYLTZ5eQCkBIEXAAAAgF6b2/Xa8ir984N10fWhE8r0/QRD6gvdDpXk99GQesPfLHkb++75stGmhZFWxnYDR0sz7pTsffh1BIBtEHgBAAAAiDu3q7LRu9Nzu1ZVe3TXguXR9ejSfF0xI/6QepfDprJCd999NUxVl6eq754vGzVulJ69RAr5I+vcEmnmvZK7KNVXBqCfI/ACAAAAEHdulz8YGTLfXQ2tAV3/9GJ5t9zfVG7dfPK0uEPqTQhWUZwrm60PW+Caq2ll7Gl13LMXSa2bI2ubQzrhV1LJyOR8fQCgBwi8AAAAAGzH04O5XdaQ+nlLtLEhNqT+2pMma/jAvLj3KS92y9lXQ+oNnyfyhp2vjnvhGql2ZezYEddIw/fiFQWQFgi8AAAAAGw3t6umB3O7/vDmV/p4bX10/T+HjtF+YwbFPX9gvitu5VevhTXNtDL2yDv3SWvejK33+qE0ZXaPvzQAkCwEXgAAAACSNrfr5aWVeuyj9dH1EbsO1vf2jd/iVuB2aGCBq2+/AmZuV3jnWjUhadET0mf/iL0UYw6XDjyPlwZAWiHwAgAAABBV49n5uV0rKpv0qxe+jK7HlhXoFzMmKifOkHrTwji4L4fUG76myOwp7Jx170uv3xFbl02Ujr1FyuFXSwDphf8rAQAAAIjO7Wry7tzcrvoWv657erF8W8KyolyHbjp5qvKc9vQZUh8KRgbVY+dsXi09d7nUFoqs88ukmfdIrnxeUQBph8ALAAAAgFXVtbNzu4KhsG58ZomqttzfZFjXnTRZwwbEH1JfVuSWy9HHv46YuV20Mu6c1jpp3oWSf8ugf4dbmnmvVFiRzK8QACQNgRcAAADQz5m5XVVNOz+363evrdLn6xui63MOG6t9RscfUj8g36VCdx8OqTe8DZK/pW+fM1uE/NL8y6TGb2LHTBtj+eRUXhUAJETgBQAAAPRzPZnb9dzCjXrqsw3R9TGTy/WtvUfEPT/PZdegvh5Sb7Uy1vTtc2YLE4K+cou08bPYsQPPl8YdlcqrAoAdIvACAAAA+jEzs2tn53Yt2dCoe19eEV1PrCjSpcfumnBIfXlRrvqcpzIS3KD7Pv6ztPzZ2HrybGmvH/JKAkh7BF4AAABAP2Wqumo9/p26b43HpxvmLlYgFAmSBuY7dePsKXLHGVJvQrDyYrfsfTmk3mitlwKtffuc2WLFC9J7v4uth+0tHXG1+WKm8qoAoEsIvAAAAIB+qCdzu0xQZsKu2uZIWOaw5WjOrKkqL45fvTW4yC23o/MwrNeEAlJLbd8+Z7bYtFB6aU5sXTJKOuEuye5M5VUBQJcReAEAAAD90M7O7TJB2b0vrdDSjU3RY+cfNV7TR5TEvU9JnrPvh9QbtDLunMaN0vxLpdCWXTvdxZEdGfMGJPOrAwC9isALAAAA6Gd6MrfryU83aMHiTdH1rN2GatbuwxIOqS8tdKvPtdZJAW/fP2+m8zVJ8y6MVcbZ7JHKroG7pPrKAKBbCLwAAACAfqQnc7s+W1ev3722MrqePrxY5x01Pu75KRtSH/RLLZv7/nkzXTgoPX+VtHlV7JiZ2TVi31ReFQDsFAIvAAAAoJ8Ih9tU2bhzc7s2NXg1Z+5ihbfcdXChWzfMmmqFWmk1pN6glbH7zPfEG7+U1r4bO2Z2Y5xySjK/MgDQZwi8AAAAgH6iptmnQKj7c7ta/SFd+/QiNXqD1tppz9FNJ0/VoAJX3PuUFbr6fki9YSq7gltmT6HrvvintOg/sfW4o6UDz+MVBJCxCLwAAACAfqDRG5BnS2DV3SH1dy5Ypq+qm6PHLjtuoiYOKYp7n+I8p4pyU7Cbnwm6zOwudM/qN6Q3746ty6dKx9wo5fDrIoDMxf/BAAAAgCznDYR2em7X/72/Vm+sqImuv7PPCB07pSLxkPoElV+92pJHK2P3VS+TXrjavICRddEQ6aS7JWdesr9CANCnCLwAAACALBYKt6m6yWdVanXX2ytr9Je310TX+40eqLMPHbvDIfVmfldqWhl3LtTrtzxV0ryLpUBrZO0skE66VyooS/WVAUCPEXgBAAAAWcyEXTszt2t1TbNum78suh4xME/XnDQ57hD6lA6pN4ENrYzd42+Rnr1Iaq6KrE374ow7pLIJvfEVAoA+R+AFAAAAZKn6Fr9a/N2f29XYGtB1Ty9SayBkrfNddt188tSEc7kGF7lTM6Q+HI60MqIbr1lIevFaqXp57Nhhv5B2OYhXEUDWIPACAAAAsnRu1+Zm/061QN787FJtqPdaa1OvdfWJk7RLaUHc+5TkOVXodiglWmqlUPdDvX7t7fuk1a/H1rv/lzT9O6m8IgBIuhT9rQQAAACgtwRDYVU1+nbqvg+98ZU+/jq20+GPDxmtg8aVJR5SX+hWytryvA2pee5M9cVj0uf/iK1HHyodfHHyn6ctLFUuirzll0pDdpds1FsA6DsEXgAAAECWqWryKWha/brphcWb9O+P10fXh+86WGfsN2qHQ+pTglbG7lvzlvTmL2PrwROl426TbEluRV3/gfTRX6X6teYLJdmckdlgh1wsjT08uc8FAHEQsQMAAABZxLQxmnbG7lq6sVG/fvHL6Hrc4AJdPmNi3B0XbakcUm80V0dmUaFrar6Unr8qUnllFJRHdmR05Sc/7Hr1Vql2ReSxCyskV4FUuViad5H01VatlADQiwi8AAAAgCzR7Atag+p3ZifH659erECoLTqT6+aTpynPGb/ypyxVQ+oNn0fyNaXmuTORp0p65kIp0BJZO/OlWfdJheXJfR4TppnKLtNqagI1R25k90dnnlQ0NPJ1e+ueSHUeAPQyAi8AAAAgCwRCYSu46i5fIKTr5y5W7ZYB96Zia87sKRpSEr9VcUC+K3VD6k1VV3NVap47E5nw6dmLYq+ZCaCOv10q2zX5z1W9TKpbI+WWRHY72JqpFMwbKNWskDZ9nvznBoBtEHgBAAAAGa6trU2VjV6F29q6fT/Txrh8U6xa6oKjxmv3EQPi3iff5dCgApdSxlNJhVB3wsEXrpaql8eOHfYLafQhvfO18dZLbUHJHuf7w+GWwoHIzpoA0MsIvAAAAIAMV+3xyR/sfpvYvz5ar5eWxqqlZu8+TLN2H7aDIfUp2pHRMDsymooldM1bd0tr3oytd/++NP07vffq5Q6QchxSKE5bbdAXGWBvdm0EgF5G4AUAAABksEZvQB5vsNv3e391rf7wxlfR9R4jS3TekePinm+G1FcU58qWqiH1Qb/UXJOa585Enz8qffFobD3mcOngC3v3OQdPkgaOjgST2xYbmurD1rrIbo1Ddu/d6wAAAi8AAAAgc5ndGGs93R9Sv7a2RbfMWxrNJIYU5+qGmVPlsMf/93CzI6PLYUttK2M3Wzb7rdVvSG/9OrYePFk67lbJ1subDJj5YPucFdmd0cwMC3ojg+wDrVLTRsldJB1ysWSj7gJA7+P/NAAAAEAGCoXbVNXos+ZwdYepBrv26UVq9oesda7TpptPmaqSfGfc+5iZXWZ2V8q0bI60w2HHqpZKz18VCZqMwgpp5j2RnRL7woj9pCOvkUonSK31Uv3aSGVXxdTIdYw9vG+uA0C/R+AFAAAAZCCzI2MwHO52SHbzs0u0vq41euyqEyZr3ODCuPcxuzGaXRlTJuCNBCbYMVNFNe/CSGWV4SyQZt4nFQxOwau3TRBLdR6APkbgBQAAAGSYuma/Wvzdn9v10Btf6cM1sfDohwfuokMnlMU937QwDk7lkHoTktDK2DW+JumZC2M7IObYpRPujMzM6kvrP5BevVWqXSnlDZAGjJLyBkpVS6R5F0lfvd631wOg3yLwAgAAADKICbrqWro/t+v5xZv074/XR9eHTSjTmQfuEvd8uy0ypD4nJ0VD6g0zpD4USN3zZwrzGi24Qtq8KnbsiKukUQf27XWYNsqP/hrZSbOgXHLkRuZ6mXbKoqGSzyO9dY/UzcpEANgZBF4AAABAhgiEwlYrY3ct3tCgu1/8MroeO7hAV8yYZO282BkTcpmwy5lgiH2v8zdHdvvDjqvgXr9dWvd+7NheZ0lTT+37V656mVS3Rsotkbb91jLfa6bSq2aFtOnzvr82AP0OgRcAAACQAcxw+spGrzWHqzuqGr26/unFCoQi9xuQ59Qtp0xTnsuecEh9rrOXd/RLJBySPFWpe/5M8vFfpCVPx9YTjpMOPDc11+Ktl9qCkj3OzDeHWwoHYm2XANCLCLwAAACADFDt8ckf7F4rmDcQ0nVPL1ZdS6Qt0GHL0ZzZUzSkODfufYpynSrJi79jY59oro6EXkjsywXSe7+NrYfuLh09J9JGmAq5A6QchxSK03Jrdtq0OaX80r6+MgD9EIEXAAAAkOYaWgPyeIPdrgi7a8FyrajyRI9dePQE7TZiQNz7mKqussIU7shoeBsjs56Q2IZPpZfmxNYlI6UTfx2pokqVwZOkgaMjrahtnbRemt02zRD9Ibun6AIB9CcEXgAAAEAaM1Vam5u7P6T+/95fq9e+rI6uT9tzuE7abWjc8x02W+qH1Jvh66a6C4nVfS09e2mkPdBwl0izfhOZkZVKprJsn7MkV77UXCUFvZFB9oFWqWmj5C6SDrlYsvFrKIDex/9pAAAAgDRl5nVVNfqsaq3ueHNFjf7y9proeu9RA/SzI8bFPd+EXOXFbmtnxpTyVEYqgRCfqZKad4Hk2zLQ37QInvRracCo9HjVRuwnHXmNVDohsluj+ZqaDQgqpkoz75HGHp7qKwTQTzhSfQEAAAAA4g+pD4a7N7drVbVHtz+3NLoePiBP182ckjDMGlzkTu2QeqNlsxTwpvYa0p2pmJp3sdSwPnbsmDnSsD2VVkzoNXwfqWFtZG1mdpk2Riq7APQhAi8AAAAgDZk2RtPO2B11LX5d+9QieQORkKzAZdetp0xTcYIh9APyXSp0p/jXAhN0mcolxGdaA1+8XqpcGDt2wLnSrjPS81Uz7Y0V06SiIam+EgD9FIEXAAAAkGY8vqA1qL47AqGw5sxdospGn7U2BV3XzpysUaX5ce+T73JoUEGKh9SbFkZaGXfs7XulVS/H1lNOlfb+kdI6oKtcLFUuosILQEoQeAEAAABpxB8Mq6YpElp1p/3xvpdWaOE3W+Y6SfrpYWO1/5jSuPdx2m0qL0rhjn7tmmsiw+oR3xf/kj77R2w96iDpiCvN8LX0fNXWfyB98nepbq3UFozMGTO7M5qB9czwAtBHGFoPAAAApIlwODK3K9zNwe1PfPqN5i/aFF0fP7VC3957RNzzbTk51o6MtlQPqTfDzL2xkA6dWP269OavYuuyidKMOySbI33Drldvl2pWSu5CqbBCchVEqr3mXSR99XqqrxBAP0HgBQAAAKSJqiaf1ZrYHR+u2awHXlsVXU8ZWqyLj9nV2nkxHrMjo8uR4l8FwqFIKyPiq1wiPX91pD3QMOHRzPsiAVI6Mtf58d+lQKtUPExy5kVmeZn3RUMln0d66x6T7Kb6SgH0AwReAAAAQBqoa/arxR/s1n3W1rbopnlLFN5SEGZaFG86eWrCMMvM7DKzu1LOU0XwkUjjBunZiyI7Mxom5DJhV+Fgpa3NK6X6tVL+oO3bLc06b6BUs0La9HmqrhBAP0LgBQAAAKRYsy9o7bDYHY2tAV3z1CI1+yI7OeY6bLrllGkJh9AX5jqsXRlTzrQxmnZGxHl9GqVnLpBaaiNrm1064ZeROVjpyu6QcuyRmV2OOLPhzPFwIPZ5AUAvIvACAAAAUjykvrqbQ+qDobBV2fVNfWv02JUnTtL48sK493E77RpcmAZD6oP+yKB6dC7kl+ZfJtWtjh078lpp5P7p+4qZQK54uFRYHhlQH4zz/WyOm9vz42+mAADJQuAFAAAAZNiQ+t++tkqfrK2Prn908GgdNiF+q5vDZlNFkTvhXK8+YT5PM7erm59vv2FmYL00R9rwcezYvmdLk2crrcOukhGS3SkN2T1ShdZat/3X2KzNcXO7OQ8AehmBFwAAAJBBQ+qf/myD9dbuyImD9d/7j4p7vgm5zJB6hz0NfvRv2Ry/+gfSu7+VVjwfeyUmniTtd076vjI2W6Syy4Rd7etDLo7szti0MTK83oR45r1Zu4sit5vzAKCX8X8aAAAAIEOG1H+ytk7/+8qK6HpiRZEuP35iwsqtwUVu5TrtSjkTepgKH3Ru0X+kT/4aW4/YTzrquu2Hv6db2OXYZibc2MOlmfdKFVMjc9pMRZ95b9Yz74ncDgB9IA22ZwEAAAD6l50ZUm/mdd30TGxHxtICl7Ujo5nNFY8ZUF/oToMf+cNhqWlTqq8ifa1+Q3r9zti6dLx0wl2xyql0Y0K4omHxh9ObUGv0oZHdGM2AejOzy7QxUtkFoA+lwd9+AAAAQP+xM0PqPb6grnlykRq9kYowl8NmhV2meiueArcj4Y6Nfaq5SgpHdpPENiqXSM9fFWn9MwrKpVm/ibT/pWvYVTxMcuYmPs+EW8P27KurAoDt0NIIAAAApPGQ+lC4TTfPW6K1m1uix35x3ERNHloc9z4mEEuLHRkNb6Pk86T6KtJT4zfSvAuloDeydhZEwq7CCqVvZdcQyZmX6isBgB0i8AIAAADSeEj9A6+v0odrYrOv/vuAUTp6cnnc8+22HA0pzpXNlgazn0IBqbk61VeRnrwN0tzzpdbNsd0OTRuj2cUwXcMuE8S5ClJ9JQDQJQReAAAAQJoOqZ/3xUY98ck30fVhE8p01kGj455vhtdXFOemx46MhhlY3o1qtn7D7FT57CVS/dexY0deK406QGmrYHBk90UAyBBp8jchAAAAkL1M0NXdIfWfravXfS/HdmQcX16oK06YJFuCXfvKCl3psSOj0bJZCmxp1UOMmdX10g3Sxs9ix/Y7R5o8O31fpcLBUm78FloASEcEXgAAAEAvMi2M3R1S/01dq+bMXWzN7zLM8PlbT5mmvARhVkmeU0W5abKrnwm6WmNtmNjK2/dJK1+MrU3Qte/Z6fsSFZRKuSWpvgoA6DYCLwAAAKCXtLVFhtS3B1dd3ZHx2qc67sh48w52ZMx3OVSaLkPqw2HJs4lWxs58/oj02f/F1iMPkI64OjIfKx3lD5LyBqb6KgBgpxB4AQAAAL3EVHb5g10fUm+CsVvmLdHX3diR0Wm3qTxBGNbnzJD6UPdmlfULK1+W3rw7ti6bGBlSb0+TqrzOwi7zBgAZisALAAAA6AUNLQGrWqs7fv/6Kn2w1Y6MZ3ZlR8aSNNmR0fA1Rd7QkZnX9eK1puYvsi4aIs26L313PCTsApAFCLwAAACAJPMGQtrczSH1ZkfGx7fZkfGHO9iRsbwo16rwSguhgOSpSvVVpJ+61dK8S6TQlu8Hd5E06/7IrofpiLALQJZIk78dAQAAgOwQDIWtuV1mfldXfbq2rsOOjBO6sCNjaaFLea402ZHR8FQyt2tbzTXS3AskX0NkbXNKJ90jDRqjtETYBSCLEHgBAAAAyRxS3+Tr1pD69XUtmvPMkuh9SgtcumUHOzIW5zlVnC47MhotmyM7MyLG3yzNu1Bq2hA7dtzN0rA90/NVIuwCkGUIvAAAAIAkqfH45QuEunx+kzegq59cpKatd2Q8JfGOjKaqy4RiacMEXSbwQsf2zgVXStXLYscOuUQaf2x6vkqEXQCyEIEXAAAAkASN3oAVYHWn9fHGZ5ZofV1r9NiVMyZq0pAd7ciYa83vSgvhsOTZlOqrSC+mlfW126W178SO7X6GtMf3lZYIuwBkKQIvAAAAIAlD6ms9/m61Pv7vqyv1ydr66LGzDtpFR0yMvyOjmedVUZxr7cyYNpqrpVD3dqLMeh88JC19OrYed7R0yMVKS4RdALJYSgOv22+/Xfvuu6+KiopUXl6uU045RcuXL9/uh4E5c+Zo2LBhysvL0xFHHKHFixd3OMfn8+n8889XWVmZCgoKNHv2bK1fv77DOXV1dTrzzDNVUlJivZmP6+tjP2AAAAAAO8NUalU1+ro1pP7JTzfomc83RtdHThysMw/YJe75OVvCLtPymDa8jZKvKdVXkV4WPyF9+FBsPXR36dibpZw0+rq1yxsYCbwAIEul9P+8r7/+us4991y99957evHFFxUMBnXcccepubk5es5dd92lu+++W/fff78+/PBDDRkyRMcee6yammJ/uV500UV68skn9eijj+qtt96Sx+PRzJkzFQrF5iecccYZ+uyzz7RgwQLrzXxsQi8AAACgp0Pqg6a1r4s+WL1Zv3ttZXQ9eWiRLj9+YsI2xUEFabYjo5lRZaq7ELP6jUgrY7uBYyI7Mjriz2NLmdwSqaA01VcBAL0qp607/xTVy6qrq61KLxOEHXbYYdYPEKayywRaV1xxRbSaq6KiQnfeeafOOeccNTQ0aPDgwXr44Yf13e9+1zpnw4YNGjlypObPn6/jjz9eS5cu1ZQpU6xgbf/997fOMR8feOCBWrZsmSZOnLjdtZjnMW/tGhsbrcc0z1dcHH+uAgAAAPqP6iZft+Z2ralt1vmPfKpmf+QfZsuL3Prd9/eyAq1EOzKWFaZRaGJ+fWhYLwVjPyv3e5sWSk+dE3tN8sukb/1VKh6afi+Nu0gqqkj1VQBAr0ur2loTJhmDBkVKa1evXq1NmzZZVV/t3G63Dj/8cL3zTmQI5Mcff6xAINDhHBOSTZs2LXrOu+++a7UxtoddxgEHHGAdaz+ns3bL9vZH82bCLgAAAGBnh9Q3tAR0zZOLomFXrtOmW0+ZljDsSrsdGY3WOsKurdWvleZdFHtNnAXS7P9N07CrkLALQL+RNoGXqea65JJLdMghh1hhlWHCLsNUdG3NrNtvM+9dLpcGDhyY8BxTObYtc6z9nG1dddVVVgDX/rZu3bokfaYAAADob0Pq/cGwrp+7WBsbvNbaNC9ec+JkjSsvzJwdGY1Aq9SyOdVXkT5aaqW550neLbOBbQ7pxF9JZbsq7bjypUIquwD0Hw6lifPOO09ffPGFNYNrW9v+JW/CsR39xb/tOZ2dn+hxTCWZeQMAAAB6MqTenHfPS19q4TeRbgbj7EPH6ODxZZm1I2M4JDV1/o/F/ZK/JVLZ1fhN7NjRN0gj91PaceZJRUPNL0WpvhIA6F8VXmaHxblz5+rVV1/ViBEjosfNgHpj2yqsqqqqaNWXOcfv91u7MCY6p7KystOZYdtWjwEAAADJHFL/zw/W6fnFsZ9FZ0wdou/uOzKzdmQ0PFWR0AuRof3PXylVLYm9GgddIE08Mf1eHTM0n7ALQD9kS/UPDKay64knntArr7yiMWPGdLjdrE1YZXZwbGfCLTPU/qCDDrLWe++9t5xOZ4dzNm7cqEWLFkXPMcPpTVviBx98ED3n/ffft461nwMAAADsSI3HL1+g66HPG19W649vrY6udxtRoouPnZBZOzIa3gbJH9tJvV8zlX1mN8av344d2+270p4/UNpxuKTiYZItzcJTAMj2lsZzzz1XjzzyiJ5++mkVFRVFK7nMkPi8vDzrBwGzQ+Ntt92mCRMmWG/m4/z8fJ1xxhnRc3/yk5/o0ksvVWlpqTXw/rLLLtP06dN1zDHHWOdMnjxZM2bM0Nlnn60HH3zQOvbTn/5UM2fO7HSHRgAAAGBbDa3dG1K/fFOTbn9uWXQ9bECubpw91ZrNlWhHxpI8Z3q9+EG/1FyT6qtIHx/8Xlr6dGw99ijpkEvTr13Q7pSKh0u2NAtPAaCP5LR1dfhAbzx5nL8U/vKXv+iss86yPjaXd+ONN1pBlWlbNDst/va3v40Otje8Xq9+8YtfWOFZa2urjj76aP3ud7/rsLPi5s2bdcEFF1itk8bs2bN1//33a8CAAV261sbGRitcM1VhxcXFPfzMAQAAkGlD6s3A+a7+6Fzd5NPP//GJapsjg+0L3Q7d/197alRpftz7mKquIcVpNqTefL4N6yKhF6RFj0uv3RZ7JYbuIZ38W8mRm35hV8kIwi4A/VpKA69MQuAFAADQf4fUf1PfqlC4az82t/pDuvDRz7Sy2mOtzdz5u07fTXvt0nFX8a2Zqq/hA/JkS6ch9YanOtLOCOmrV6XnLpfatsxvGzhGOv1PUm5JGrYxUtkFADRzAwAAADsYUt/VsCvc1qbb5i+Nhl3GRcdMSBh2mZ0Yh5Tkpl/YZWZ2EXZFbPxMev6aWNhVUC7N/l/CLgBIYwReAAAAQBzVHl+3htT/8c3VentVbXT9rb2Ha+Zuw3a4I2OiuV4pEQpKnu13Oe+XNn8lzbtYCvkia1dhJOwyOx+mEyq7AKCDNPubFQAAAEgPDS0BebzBLp//3MKNevTDddH1AWMH6ZzDxiW8T2mhS7nONBwqbsKu8JZqpv7MUyXNPU/yNUbWNqd00t1S6XilFcIuANgOgRcAAADQyRyu2uYtFT1d8OnaOt390oroemxZga49abLVrhiP2Y2xODfNdmQ0WjZLgdZUX0Xq+ZqkZ87fqtItRzruFmn43korhF0A0CkCLwAAAGArgVBYVU3eLr8m6za3aM4zS6JzvgbmO3XLqdOU73LEvY+5rbTQnX6ve8Artdal+ipSL+iTnr1Uql0ZO3bYL6TxxyitEHYBQFwEXgAAAMDWQ+obvV0eUt/QGtDVTy5S05bWR5fDpltOmaYhxblx72POKS9Kw7DLtDB6NpkXQf2aGUz/0vXSho9jx/Y6S9rtu0orhF0AkBCBFwAAALBFdZNP/mDXZleZ826Yu1jf1Mfa/66cMUmThxYn3pGxOA13ZDSaqyLD6vszE/a9+Stp5UuxYxNPkg48T2nF7pSKh0u2NJz/BgBpgsALAAAAMJvxNfvl8QW7XAl2z0tf6ov1DdFjPzlktI6YOHiHOzI60m1HRsPbIPk8qb6K1Pv4L9IX/4qtRx0kHXWd+eIpbdgdhF0A0AVp+LctAAAA0LcavQHVt/i7fP4jH6zV84vbh5lLx0+t0Bn7jUp4n8FF7vTckTHol5prUn0VqbdkrvTeb2Pr8qnSjDsj1VTpwlR0mcouE3oBABIi8AIAAEC/Zu3I6Ol62PXa8mr96a010fX04SW6+JhdrQqueAbmu1TodqRnCx9zu6TVb0iv3hJ7XUpGSbPuk1z5Shs2m1Q8LL0COABIYwReAAAA6Ld8wZA1pN60KHbF0o2NumPBsuh6+IA83XTyVGsQfTwm6BpY4FJaMpVdpsKrP9v4hfT8lVJbKLLOL5VOvl/KG6i0YcLUomGSIw03OwCANEXgBQAAgH4pGAqrssGncBfDrk2NXl371KLoUHsTZN166jSV5MWvuHE77VYrY1oyM7vM7K7+bPNqad5FUtAXWTsLpFm/ibQNplXYNVRyxt/5EwCwPQIvAAAA9DvhcJsVYAXDXduRsdkX1LVPLlJdSyC62+KNs6do1KD4LW8Om00VRe6ErY4pY3ZjNLsy9meeKmnuuZJvS+hnc0on/UoaPElpw3zvFFakV2slAGQIAi8AAAD0O1VNvmil1o6Ewm26ed4SfVXTHD12yTETtOeo+C1vNrMjY4k7PXdkNMzcri6GfVnJ2yjNPU/ytG88kCMde5M0Yj+llYLBkrsw1VcBABkpTf8GBgAAAHpHdZNPLf5gl841s73uf2WlPlhTFz32vX1H6oTpQxPez7Qxuh1puCOj0bJZCnjVbwW90vxLpM2rYscOvUyacJzSSkGZlFuc6qsAgIxF4AUAAIB+o6EloCZvpC2xKx7/5Bs9/fmG6PqwCWX6n0PHJLxPaYFbBem4I6MRaI0EXv1VOCS9cK204dPYsb1/JO3+PaWV/EFS3oBUXwUAZDQCLwAAAPQLZg5XbfOW4eRd8M6qGj3wWqwKaNKQIl15wiSrXTGeolynSvLjD7FPedjTtEn9ltmc4LXbpa9ejR2bNEs64FylFRN0mcALANAjBF4AAADIer5gyGpl7KoVlU265dmlat+/sbzIrVtOmaZcZ/w2xTyXXWWFLqUtM6/KhF791Qe/l5Y8GVvvcoh05DWRwfDpwl0UaWUEAPQYgRcAAACymhk6X9ngU9hU+HSBCcaufmqRvIHIUPcCl123nzZdgwrih1lOu03lRbnpuSOj0Von+VvUb33xL+nDP8bWQ3aTZtwh2dOoGs9VIBVVpPoqACBrEHgBAAAga5mh85WNXgW7uCNhqz+ka55cpFqP31rbcqTrZ03RmLKCuPex23JUUZxrvU9LZkB9f57bteIF6Y1fxtaDxkoz75WceUob5lqKhqT6KgAgqxB4AQAAIGtVe3zyBkJdrgS7+dklWlntiR674OgJ2nd0/HlKpqLLVHa5HGn6Y7UJ+jybIvOr+qN1H0gvXmeiz8i6sEKafb+UW6K04XBLRUPTq7USALJAmv7NDAAAAPRMfYtfHm+wy+c/8PoqvfdVrBLq23uP0OzdhyW8j5nZZWZ3pa3mKinU9dcgq1QtleZfKoW3fP7uEmn2byOhV7owLZXFwyQbv5YBQLLxf1YAAABknRZ/UJubI22JXfHkp9/oiU++ia4PHleqnx42NuF9BuS7rF0Z05a3QfLFqtX6lfp10jMXSIEtc8scudKs+6RBY5Q27A6peLhkS+PAFAAyGIEXAAAAsm5HxqrGru/I+O6qWv321ZXR9YTyQl190uSEM7kK3I6EQ+xTLuiXmmvUL5nPe+65UuuWar0cu3TCXdKQ6UobJuQyYZcJvQAAvYLACwAAAFnDzOEyYVdXd2RcUdlkze0Kbzm9vMitW0+dpjxn/Kobt9NunZe2zOfetLF/zu3yNUnPnC81xqr1dPQN0i4HK/3CrjSuDgSALEDgBQAAgKzakTEQ6tqOjNVNPl391CJ5A5Hz8112K+wqK4wfZjlsNlUUua1h9WmruVoKBdTvBL3Ss5dINV/Gjh18kTTpJKUNM6vLhF2ONK4OBIAsQeAFAACAfrcjo5nxdfWTC1Xricz5Mt2LN8yaonGDC+Pex5aTo4oStxx2W3pXOHkb1e+YwfTPXy1t+CR2bM8fSHueqbRB2AUAfSqN/7YGAAAAuqahJdDlHRlN2+PN85ZqVXVz9NiFR0/QvqMHJbxfebFbboc9ved2earU75jWzVdvk1a/Hjs2ebZ00AVKG6Yi0KrsSuNWWADIMgReAAAAyGimWqu22dfltsf7X1mp91dvGWgu6Tv7jNCs3YclvF9pgVv5Lkd6hz6eTf1zbte7/ystfTq2HnO4dOQ1kZApHRB2AUBKEHgBAACg3+zI+J9PvtHTn2+Irg+bUKafHjY24X2K85wqyXem/86EpsKrv/n0YemTv8XWw/aUjr9NsjnSKOwaJjlzU30lANDvEHgBAACgX+zI+NaKGv3+tVXR9aQhRbryhEnWbK54TFVXoiH26TO3q0H9zrJ50tv3xtalE6ST7pEcaRIume+roqGSMy/VVwIA/RKBFwAAALJ+R8blm5p06/ylao/GKorduuWUacp1xp/J5XLYVF6U5mGX2Y2xP87tWv2G9PJNsbWZjzX7fyV3kdIq7HLlp/pKAKDfIvACAABAVu/IuKnBa+3I6AtGwrECt123nzZdgwpcce9jt+VoSHGubGb7xnRlKtua+uHcro2fSQuulNq2fP3zS6WTfysVDFbaKCwn7AKAFCPwAgAAQNbuyGjOu+qJhaprCUSDrBtnTdXo0oK498nJyVFFca4c9jT/UbmlVgp2fX5ZVqhZIc27SApt+bxdBdKs30glI5U2CgenT6UZAPRjaf63OAAAALBzOzKadscbnlmsrze3RI9deuyu2muXgQnvN7jInbDVMS34PFJrvfqVhvXS3HMjM8sMuysys2vwJKWN/EFSbkmqrwIAQOAFAACAbNyR0cz4uvvFL/Xp2lgodOYBozRj2pCE9zNtjoXuNNnhL9HcruZ+NrfL7EL59M8jVW1Gji2yG+PwvZU28gZEAi8AQFqgwgsAAABpLxgKq7Kh6zsyPvze13p+cWV0fczkcp110OiE9ynKdWpAfvy5Xmk1tyvctWH9WcFUdM09T2r8JnbsyOuksUcqbeQWSwVlqb4KAMBWCLwAAACQ1sLhNm1q9CrYxZDnxSWV+us7X0fXu40o0WXHTbRmc8WT57KrrDDNw67+OLcr0BqZ2VW7InbsoAulKbOVNswcMTOkHgCQVgi8AAAAkNaqmnzyb9lhcUc+W1evXz6/PLoeOTBPN82eKpcj/o+9TrtNFUW5CQOxtNDf5naZ1k2zG6PZlbHdXj+U9vqB0oYzTypK3CYLAEgNAi8AAACkrRqPzxpU3xVf1zbr+qcXKxiOtD0OyHPq9tOmqzjPGfc+ZtfGISW5stnSPOzqb3O72sLSKzdJX78VOzblZOnA85U2HG6paKjZ1jPVVwIA6ASBFwAAANJSQ0tAja2BLp1b1+LX1U8ukscXCcdMRdctp0zTsAF5ce9jKroqinOtCq+01t/mdpnP981fS8vnx46NPUo64ur0CZfsTql4mGRL8+8dAOjH+D80AAAA0k6zL6ja5q7NqvIGQrrmyUXa2OC11iYSufqESZoyrDjh/cqL3Mp12pX2+tvcro/+JH3xaGw9Yl/puFskmyN9wq6SEZItA753AKAfI/ACAABAWjEBlpnb1RWhcJtufXaplm1qih776WFjddiugxPeb1CBSwXuNAlQEulvc7sWPia9/0BsXT5FOvHXkfbBdGB3SMXDCbsAIAMQeAEAACBtBEJhVTZ61Wba2rrggddW6e1VtdH1ybsP03f2GZHwPkW5Tg3Iz4AdGc3cLk+l+o0vF0iv3xVbD9hFmvWbyC6IaRN2jYi8BwCkPQIvAAAApIVwuE2bGrxW1VZX/Ofj9Xri02+i6wPGDtJ5R41PuNtinsuussIMCLva53Z1MfjLeGvekl663nzikXVhhXTyb6W8gUoLpn3RVHYRdgFAxiDwAgAAQMqZiq7KJq9V4dUVb6yotqq72u1aUajrZk6xdl2MxwyyryjKTRiIpY3mmv4zt2vDp9Jzl0vhUGSdO0A6+XeRHRDTKuyKv9snACD9EHgBAAAg5aqbfGr1bwk8dmDJhkbdNn9Zey2QKorduu3U6cpLMIDeYbNpSHGubAkCsbSa2+VtUL9QvUyad6EU2hLuOQuk2fdLA0crLZhdGE3Y5ciAqkAAQAcEXgAAAEipGo9PHl+wS+d+U9+qa55aJH8wUglW4Lbr9tOmW0Po47Hl5KiixC2HPQN+9O1Pc7vqvpbmnif5myNru0uaeY9UPllpgbALADJaBvytDwAAgGxV3+JXY2ugS+c2tAZ01RMLrfeGw5ajm2ZP1ejSxEPNy4vdcjviV3+l19yujf1jbpcJ9eb+XGqti6xz7NKMO6Xheyu9wq402R0SANBtBF4AAABIiSZvQJub/V0611R0XffUIq2va40e+8XxE7XnqMRDzUsL3cp3Zciues3VUrBrr0dGMyHX0+dGhvK3O2aONOYwpQXCLgDICgReAAAA6HMt/qBqPF0Ld8JtbbrjuWVatKExeuxHB4/WsVMqEt6vJM9pvWUEb2PkLduZ9sVnLpDqVseOHfoLaeKJSguEXQCQNQi8AAAA0Ke8gZAqG33Wzoxd8dAbX+m1L6uj6xOnDdF/7z8q4X0K3A6ruisjmKouU92V7YJe6dmLpaolsWP7nSPt/j2lBcIuAMgqBF4AAADoM6Y1sbLR2+Ww68lPv9FjH62PrvcdPVAXHTNBOTnxd1t0O+0qL8qQsCsc7h9zu8ww/gVXSd98HDu22/ekfc9WWiDsAoCskyEDDQAAAJDpgqGwNjV4FQp3Ldx5e2WN7n9lZXQ9fnChbpg1JeFui067TUOKcxMGYmnFVHaZMCibtYWll+dIa96IHZs0Uzr0Uikdvk5W2DVCcsTf6RMAkHmo8AIAAECvC4fbtKnRq6CpaOqCpRsbdcuzS9UejZmKrdtOm5ZwAL0tJ0cVxbmy29IgROkKb4Pka1JWM5Vrb9wlfbkgdmzsEdJR10k5afCrCGEXAGQtKrwAAADQ66qafFY7Y1d8U9+qq59cJN+W8wvcdt1+2nSVJZjJlbMl7HI50iBE6YqgT2quUdZ773fSwn/H1iP2k467TbKlwa8hNrtUPJzKLgDIUhnyEwEAAAAyVY3HZ+3K2BUNLQFd9cRCNbRG2vwcthzdNHuqxpQVJLxfWaFLeS67MkJ/mdv1yd+lj/8cW1dMk078teRIg/lqhF0AkPUIvAAAANBrTIDVuCW82hFfIKRrnlqk9XWt0WOXz5ioPUcNTHi/QQUuFeU6lTE8lVKoawFgxlr8hPTOfbH1oHHSrN9IrnylHGEXAPQLBF4AAADoFc2+oGqbfV061wyyv+25ZVqysTF67CeHjNYxkysS3s8EXQPyM2jYeGud5G9WVlvxgvTqbbF1yQjp5N9KuSVKOcIuAOg3CLwAAACQdN5AyJrb1VUPvL5Kb66IzbSaudtQnbHfqIT3MQPsTStjxgi0Si2bldXWvCW9eK2ZVh9ZFwyWTn4g8j7VCLsAoF8h8AIAAEBSBUJhVTZ61dbFGVX//ni9nvjkm+h6/zGDdOHRE6xB9PGY4fRm58ZE56SVcEhq2pTdc7u++Vh67vLI52qYiq6TfycVD0v1lRF2AUA/ROAFAACApAmH27SpwWu1KHbFa8ur9MBrq6LrCeWFun7mFNlt8YMsh82mIcW5siU4J+2YsKs9CMpGlYuleRdLoS1Vfc4Cadb90qCxqb4ywi4A6KcIvAAAAJAUpqKrsslrVXh1xefr63X7c8uiaxNi3X7a9IS7LdpyclRR4pbDnkE/xjbXRtoZs1XtSmnu+VJgy2wyu1uaea9UMSXVV0bYBQD9WAb9pAAAAIB0VuPxq9XftSqmNbXNuu6pxQqEIpVgxbkO3XH6dGvHxXhM+2JFca7cjviBWNoxA+rNoPps1bBOevrnkq8hsrY5pBN/KQ3fK9VXRtgFAP0cgRcAAAB6rL7FryZvoEvn1nh8uvLxhfL4gtbaac/RLadM06hB+QnvZwbUJ6r+SjuhoOSpVNYyn9tTP5daaiPrHJt03C3SLgen+soIuwAABF4AAADoGRNcbW72d+ncFn9QVz+xKLqDo5nCdc2JkzVteEnC+w3Md6ko15k5XyoznL5poxlqpqxkqtZMZVfThtixI6+Vxh+rlGM3RgAAFV4AAADoCW8gpOot4dWOBENhzZm7RCurPdFj5x45ToftOjjh/QpzHRqYoNUxLTXXSMGuvS4Zx9ckzT1XqlsTO3bopdKUk5VyhF0AgC1oaQQAAMBO8QfDqmz0WsPqd8Sc8+sXv9RHX8fmWX177xE6ba8RCe9nWhgHF7ozLxDybplplW3M8P15F0rVy2PH9jtH2v0MpRxhFwBgKwReAAAA6LZQuM0Ku8z7rvjbO1/r+cWxeVZH7DpY5xw+NuF9XA6bKopyrWH1GSPolzxVykohvzT/Umnj57Fje/y3tO/ZSjnCLgDANgi8AAAA0C2mWsuEXYFQ1+ZTzftio/7+3tfR9W4jSnTlCZNkSxBkOWw2DSnOlc2WQWGXmddl5nZ1oeIt44QC0oIrpXXvx45NOVU6+CKzfWYqr4ywCwDQKQIvAAAAdIuZ2WVmd3XFu6tqde9LX0bXuwzK180nT7Wqt+IxQVhFiVsOe4b9qNpcFQmGsk04JL08R1r9euzYhOOlI64i7AIApK0M+ykCAAAAqWR2YzS7MnbF0o2NumneErV3PZYWunTH6dMT7rZo2hcrinPldtiVcbsW+mLD+LOGqVZ77XbpywWxY2MOl465MVJZlUq0MQIAEiDwAgAAQJc0egOqb/F36dz1dS26+slF8gUjbY/5LrvuOHW6FWYlUlbosgbVZ9wg95bNysqw6617pCVPxo6N2E86/nbJHj+07BOEXQCAHSDwAgAAwA61+IOq9fi7XAV2xeML1dAaae9z2HJ00+ypGldemPB+A/NdCau/0lIoKDVtys65XR88KH3+j9h6yO7SSXdLjhTvmmmzScXDJYcrtdcBAEhrBF4AAABIyBcMqarRZw2r35FWf0jXPLlIGxu80WNXzJiovXYZmPB+JugaWJBhAYZ5PTybIjOuss0nf5c+/ENsPXiSNOs+yZmXBmHXCMIuAMAOEXgBAAAgrmAorMoGn8JdCLvMuTfOW6LllU3RYz89bKyOnlyR8H75LofVyphxWmqlQCzYyxqL/iO9c19sPWisNPt+yV2UyquisgsA0C0EXgAAAOhUONymTY1eBcOROVyJmOqve15aoQ9Wx2ZZnbrncH13nxEJ7+d22lVe5LaG1WcUM6C+tV5ZZ/l86bU7YuuSEdLs30l5iSv0+q6NMcXtlACAjEHgBQAAgE4DrMomr/xbhs7vyN/e/VrPLdoUXR82oUw/P2JcwiDLabdpSHGubLYMC7uCfslTqayz8mXppRvMVz+yLqyQTn5AKhyc2usi7AIA7AQCLwAAAGynxuO35nF1xbwvNujv734dXU8fXqyrT5wse4Igy9xmdmxMdE5aMtVuTRuzb0j9mrekF66W2rYEnHmDpJN/JxUPS+11mcC0aBiVXQCAbiPwAgAAQAf1LX41eSM7LO7I2ytrdO9LK6LrXQbl6+aTp8nliP9jpqn6MmFXonPSVnOVFOraa5Mx1n8gPfcLKRyMrN3FkbBr4OjUh12mjdGZm9rrAABkpAz8KQMAAAC9xeMLanOzv0vnLt7QoFueXarwlmKn0kKX7jh9uorznAnvZ2Z25TrtyjitdZHZXdlk4+fSs5dIoS1fc2dBZEB92YTUXhdhFwCghwi8AAAAYPEGQqpu8nXp1Vhb26Jrnlwk35YZXwUuu+48bbpVuZVIaaFbBW5H5r3igVapJTaQPytULZWeOT/yuRmOXGnWfVLF1DQIu4ZR2QUA6BECLwAAAFjD6Ssbvdaw+h2p8fh0xRNfqNEbaYFz2nN08ynTNHZwYcL7leQ5rbeMEwpKTZuya25X7Urp6XMlf3NkbXdJJ90tDdszTcKuvNReBwAg4xF4AQAA9HPBUFibGrwKtfcm7qDl8aonFqqyMVIJZkbOX3XCJO0xckDC+xW6HVZ1V8YxIZdnkxTu2gD/jFD3tfT0zyVfQ2Rts0sz7pRG7p8GA+qHEnYBAJKCwAsAAKAfC4fbtKnRq6DZfbALVWA3zF2sVdVbqoIk/fzIcTpiYnnC++W57BpclIFhl9FcIwW8yhqNG6Snfya11EbWOTbpuFulMYelQdg1RHLlp/Y6AABZg8ALAACgnzLti5VNXivI2pFwW5vuXLBMn66tjx777j4jdPpeIxLez+zEWFGUa+3MmHG8jZJ3SxVUNvBUSU/9P8lTGTt21PXS+GPTJOwqSO11AACyCoEXAABAP2UG1Lf6u9aq9+DrX+nV5dXR9TGTy3X2YWMT3sdhs2lIca5stgwMu4I+qTn2+WY8U9Flwq7Gb2LHDr9Smjwr9WFXYQVhFwAg6Qi8AAAA+qFaj8+ax9UVj320Tv/+eH10vfcuA/WL4yfKlqBqy9w2pCRXDnsG/rhp2jubNmbPkPrWOumpn0n1X8eOHXyRNP3bSrnCcsmdeLMDAAB2RgbuCQ0AAICeaGgJqKE10KVzX1paqd+//lV0Pb68UDfOniJngiArZ0vYZdoZM5IZUm92ZswGviZp7rnS5lWxY/v/TNrzTKVH2FWU6qtALwm3hbV081LVe+s1IHeAJg+aLJuZGQcAfYTACwAAoB8xVV21zZEdFnfkwzWbdeeC5dH10JJc3XHadOW7Ev8IaQbU5zrtykgtmyV/i7KCv1mae75UHfsaau8fS/v+j9Ii7MotTvVVoJe8v/F9/fGLP2pF/QoFwgE5bU5NGDBB/7Pb/2j/oSneDRRAv0HgBQAA0E+YeV1mbldXLN/UZO3IGApH2vpK8py68/TpGlTgSni/0gK3Ct2OzA2ITOCVDQKt0ryLpMqFsWO7f1864Oepn9lVMJiwK8vDrqvfvFp1vjqryqvdx1Uf66s3v9Jth95G6AWgT1BTCgAA0A/4giFVNnqtnRl35Ju6Vl31xEJ5A5FfVnOdNt1+2jSNGJif8H4mFCvJdyojhQJS0yZlBTNwf/6l0oZPYsemfUs65OJI4JTqAfVUdmUtE3Dd/dHdqvHWWB+b9mabbNZ7szbHze1bB2EA0FsIvAAAALJcIBRWZYNP4S6EXZub/br88S9Uv2XGl92WoxtnT9WkIYnbz0xVV2mhWxnJvC7ZMqTeBHfPXS6tez92bPJs6fArUh92FQ1hQH2WW1y7WCvrV0ptkfArGA4q0Baw3lshV5us2815ANDbCLwAAACymGlJ3NTgVdDsPLgDLf6gVdm1scEbPWZ2Y9x39KCE98tz2a25XRnLUykF/cp44aD0wjXS12/Fjk04XjryWimVw8JN2FU8THIVpO4a0CcWVi+0ZnaFFVabSbe2YtbmuLndnAcAvS1DBywAAABgR8LhNm1saLUqvHbEnHPD04u1osoTPfbTw8bquCkVCe9ndmKsKMq1WpYyUmud5It9zhkrHJJevF5a9XLs2NgjpWNulGwp3EDAZpOKhknO3NRdA/qMCbW2Dbp25hwASAYCLwAAgCxkZnVVNnnlD+447DKtjmY3xo/X1kePfWvv4fruPiMS3s9pt2loSZ5stgwNu8xujM21ynimVeyVm6UVz8eO7XKwdPztkt2Z2rCreLjkyODqP3RZo69RH2z8oEvnFjip9gPQ+wi8AAAAslBVk8/albErwdjvX1+lV5ZVRY8dNalc/+/wcQmrtsxsr4riXOt9RgoFJU8WDKk3c8deu11a9kzs2Mj9pRN+meKwy74l7Eq8qycy3/qm9frr4r9q7qq5ag22duk+zWZHVADoZQReAAAAWaa6yadmX7BL5/7rw3X6z8ffRNd7jxqgK2ZMlC1B2GVuM2GXaWfM6CH1XZhrlvafxxu/lBY/ETs2bG/pxF+ntqrK7oiEXakM3NDrFtYs1J8X/lmvrHulW7su5pj/MrUFGkBGIfACAADIImaXxSZvZIfFHVmwaJMeenN1dD2hvFBzZk+1WhXjMb+olhe7letM4VyonmquloI+ZXzY9fa90sJ/xY4N2V2aea/kzEvddRF2ZTVTEfr6+tf1l0V/0SdVn2x3e54jT76gzxpOb4KtrWd12WSz1k6bU7sN3q2PrxxAf0TgBQAAkCUaWgKqb+naboPvfVWrX72wPLoeNiBXt582XQXuxD8elhW6lO/K4B8hvQ2St1EZ7/0HpM/+L7YunyrNuk9y5afumgi7slYgFNDTK5/W35f+XasbYiF5u/L8cp0y7hQdtctRuubNa/RVw1dWuOWwOaLBVygcsj4eP2C8ppROScnnAaB/yeCfVgAAANDOVHXVNnetamnxhgbd+MwShbcUXwzMd+rO03fToILE85bM7UW5GdymFvBKzTXKeB/+QfroT7F12URp9v2Suyh110TYlZWafE16ZNkjenT5o6pp3f7PjgmvTptwmg4aepBcdpcKXYW6fN/Ldd0716nOW2e1OpqqMOXICr8G5g7UJftcIltOhrZDA8goOW3W/4GwI42NjSopKVFDQ4OKi4t5wQAAQNpo8QdV2eiL/GK5A2tqm3Xho5+pyRuZ8ZXvsuue7+yuCRWJw5LiPKfKCjN4tz0zpL5hnRTe8SD/tPbJ36R3fhNbDxonnfqglDcwdddE2JV1NjZv1F8X/VVPrXxKLcGW7W7fp2IfK+iaVjpNLodLRa4iFToLo0HW+xvf1x8X/lEr6lYoEA5YbYwTBk7Q/0z/H+0/dP8UfEYA+iMCry4i8AIAAOnIGwhpY4O3S2FXVaNX5//zM1V7IpVgTnuO1ca416jEYUmh26Hy4lxlLPPaNKzP/Lldnz0ivfXr2HrgaOnUh6T80tRdE2FXVllWu0x/WvQnvfT1Swq2ddz4wlRoHTniSJ08/mTtUryLch25KnYVK9/ZeRutqe5aunmp6r31GpA7QJMHTaayC0CfoqURAAAgQ/mCIW3qYtjV2BrQFY8vjIZdZo+0q06YvMOwK89l1+CiDK7sypYh9V881jHsKhkpnfx7wi70mPn/x7sb3tWfF/1Z7296f7vbCxwFOmHMCZo1bpZK80pV4Cywgi7TwpiIqfaaWjqVrxCAlCHwAgAAyECBUFiVDT6FuxB2mSqwq59cpK83x1qTLjh6vI6YODjh/VwOmyqKcq2dGTNWNgypX/SE9MadsXXRMOmUB6TCxF+/XmWzS8XDJXsGz3Tr58wQ+fmr5+tvi/+m5XWxDSzaleWV6eRxJ+u4XY6zWhbb3+zmaw/0kjVr1mjMmDH69NNPtccee6TFcx1xxBHW7ffee2+vXg+Sj8ALAAAgwwRDYauyKxgOd+ncm+Yt0ZKNsdDnzANG6eQ9hie8n9Nu05DiXNlsGRx2BVozf0j9kqel126NrYuGRGZ2FQ1N3TWZwKNkBGFXhmoJtOjfX/5b/1j6D2tW17ZGF4+25nMdOvxQq13RVHOZqq6MDr6BTowcOVIbN25UWVmZtX7ttdd05JFHqq6uTgMGDOA1ywIEXgAAABkkHG7TpkavVeG1w3Pb2vSrF77Ue19tjh47afpQnXXQ6IT3s9tyVFGcK4fdltlD6ps2ReZ3Zapl86RXbo6tC8qlU34vFQ9L3TURdmWsza2b9fclf7fCrkb/9lWPuw/e3Qq69hy8ZyTochcrz5GXkmsFepvf75fL5dKQIUN4sbNYBv8UAwAA0P9m7Ziwyx/ccdhlPPTGV3phSWV0ffD4Ul10zISElRq2nEjYZdoZM5YJuZo2ZvaOjMufk16+0XwykXV+WaSyy8zuShXaGDPS1w1fa847c3Tc48dZA+m3DrvMnK3DRhyme464R7cecqv18YiiEaooqCDsQq9ZsGCBDjnkEKuKqrS0VDNnztSqVavinj937lxNmDBBeXl5VgXW3/72N+vvsfr6+ug5jz/+uKZOnSq3263Ro0fr17/+dcfKxdGjdcstt+iss85SSUmJzj77bKul0TzOZ599Zn1sHtsYOHCgddyc2y4cDuvyyy/XoEGDrJBszpw5HR7fnP/ggw9an0t+fr4mT56sd999VytXrrRaIgsKCnTggQcm/DyRfBn8kwwAAED/CrsqG33WPK6uePTDdXrso/XR9e4jSnTdSVOs6q14zA/s5cVu5TozfEZPpg+pX/mi9NINUtuWYDNvkHTq76UBo1J3TTZbZGaXI/GgcqSPhdULdeErF2r2U7P1+IrH5QvF/ky47W7NHDtTDx7zoK7c70rtO2RfjSgcYc3tcjKXDb2sublZl1xyiT788EO9/PLLstlsOvXUU61QaVsmiPrWt76lU045xQqmzjnnHF1zzTUdzvn444/1ne98R9/73ve0cOFCK4y67rrr9Ne//rXDeb/85S81bdo063xz+7btjSY0M5YvX261Ot53333R203IZkKr999/X3fddZduuukmvfjiix0e4+abb9YPfvAD6zonTZqkM844w7req666Sh999JF1znnnnZeEVxBdRUsjAABABjC7K7b4g106d8GiTVZ1V7txgwt08ynTdli1VVboUr4rw388zPQh9V+9Kr1wjdS2JdjMHRBp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CG9Rkbo/6bDDmrW9UQIZCSykeup///ufCqZGjBiBM888E0OGDAk7VgKT888/H3/++acKMqQypylz587F0qVLVbWSVN0ICYskjJCAQoINqTqSjzpSyTV16lRMmzZNVUXVkcqw+sGShFTSfinX2rNnT7Umx0sIEoler8e5556L119/HUcddZT6LLdlPZK9eU4JrSSMka9l37591Xsit+W9FvVDImkllVBMquskeJIQUiqahFROSZWYqKysVMHX888/H6xMk2uS11OfvGd1FXr33HMPBg4ciHXr1qFfv367vN777rtPVZtJiNawWqu+Sy65RD2fXIcEkVLNJ89Vv/JKKvikWmvbtm2q6qrumr7++msVlj344IOq8qv+1/aqq65S93/yySeqGqyO/D2866671J979+6tXru03I4ZM0YFj1L1ddxxx6mvo1R6yXu4N+Lwu0AiIiJqiyTAkrBLWhSjOfbhWavw45ri4NqwHum4/YT+EWdxSeVXjtUEo06L+Ay7CgBPTWtfCbD0E2DOHaGwS1oIJz/fOmGXzgikdNqnsEuCLqnoynfkw+EOzelaXroct/18G2775bZGYddB2QfhiRFP4KGjH8LRnY5GdlI2wy4iCpKZXe6NG6FNTW00b1Jua1NS1P1yXEvM8JI5TBI2SaWVtJ1J8NVwCPu1116rWuN++umnXYZdQkIQCW2kja+ugkw+pIJJghBRVVWl2tQGDBigwhy5f9WqVY0qvKTlryFpf6sLnoTMjqprBYxEwhoJVKT6TD43VZnUlL15zmHDhoV9LY844ghVhSQzsoRUtklbp7SRStAk7Xui4XtQ38qVK1UlnlTL7U79sLJutlak6x07dqz6ukgYFemxJXiS8FJCQ6nKahga/vXXX+rfRqkuq/934Mcffwz+HZD3QVox5fHq/q7Mnj270etvGLzWf++laqympkYFhhIkSmgaqZJtV1jhRURERDEKu2qiCrtkx8bHvlmN71aFvok7rFsa7p40MOIsLgm7pLLLpI/DsEs4CgH3ztbB1iJB0MI3gN9eCK2ZUgNhV3bjVokWZ0gCkvMCg+qbsaJrVdkqvLfyPSwqbjzLZmjWUJzT7xwcmHOgqugy6Uz79BKIKD7JgPpajwcJuxjKLuu1Nps6riWYTCZVMSMfd955Jy699FJVVVPXYifkPqnakVlW0ta2KzJjSkKJ+vOa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rMIu2d9VJNTyN/mw6OElcFV5UFBmx5zLD2VlFxEREREREVEP1KOG1k+bNk2t0hg1btw4nH766SmH1mf6Ko1RL1x8L0b1+hyTc/+j2x4IW7HBPRurmuYDXhO+96dj8eLtX2Gvv7xZWCVh1/fu/X6b36avwYMP7/oQrlo/CootmHv93C6p7CIiIiIiIiIi6tGB13PPPYcLL7wQ999/P2bOnIkHHngADz74IFatWoXBgwd328BLeNcthu2Z49Xten8JtntmYL1rJux2E465cRZsZQMAR5EsgQivqwkL//gfuFwhFBQYccyvT9pvZRcRERERERERUXfSYwIvIdVdf/nLX1BRUYEJEybgH//4B4466qg2nZvJgZfy0Z+A0pFAn0kyrEy72AsBuwRdPaJzlYiIiIiIiIio5wVeByPjAy9RtwMI+bWQS8Ku5Kn1REREREREREQ9AEt/somjGLDkAEZjVz8TIiIiIiIiIqIuw8Arm9jyuvoZEBERERERERF1OZYCERERERERERFRVmHgRUREREREREREWYWBFxERERERERERZRUGXkRERERERERElFUYeBERERERERERUVZh4EVERERERERERFmFgRcREREREREREWUVBl5ERERERERERJRVGHgREREREREREVFWYeBFRERERERERERZhYEXERERERERERFlFQZeRERERERERESUVRh4ERERERERERFRVmHgRUREREREREREWYWBFxERERERERERZRUGXkRERERERERElFUYeBERERERERERUVZh4EVERERERERERFmFgRcREREREREREWUVBl5ERERERERERJRVGHgREREREREREVFWYeBFRERERERERERZhYEXERERERERERFlFQZeRERERERERESUVRh4ERERERERERFRVmHgRUREREREREREWYWBFxERERERERERZRUGXkRERERERERElFUYeBERERERERERUVZh4EVERERERERERFmFgRcREREREREREWUVBl5ERERERERERJRVGHgREREREREREVFWYeBFRERERERERERZhYEXERERERERERFlFQZeRERERERERESUVRh4ERERERERERFRVmHgRUREREREREREWYWBFxERERERERERZRUGXkRERERERERElFXMXf0EuotwOKyuXS5XVz8VIiIiIiIi6iD5+fkwGAz8eBJlGQZebVRfX6+uBw4cmM7PBxEREREREXUip9OJgoICfsyJsowhHC1dolaFQiHs3r0749J/qTiTEG7Hjh38Jt0D8fPfs/Hz37Px80/8N9Cz8fPfs/Hz37Ey7W88IuoYrPBqI6PRiAEDBiBTySsSfFWi5+Lnv2fj579n4+ef+G+gZ+Pnv2fj55+IqGUcWk9ERERERERERFmFgRcREREREREREWUVBl7dnM1mw+23366uqefh579n4+e/Z+Pnn/hvoGfj579n4+efiGj/OLSeiIiIiIiIiIiyCiu8iIiIiIiIiIgoqzDwIiIiIiIiIiKirMLAi4iIiIiIiIiIsgoDLyIiIiIiIiIiyioMvDLcv/71LwwdOhR2ux3Tpk3DJ5980urxixYtUsfJ8cOGDcP999/fac+Vuv7fwEcffQSDwdDssnbtWn56uqGPP/4Yp556Kvr166c+j6+++up+z+H3gJ77+efXf3b54x//iOnTpyM/Px+9evXCGWecgXXr1u33PH4P6Lmff34PyB4LFizApEmTUFBQoC4zZ87E22+/3eo5/NonImqOgVcGe+6553D99dfj1ltvxTfffIMjjzwSJ554IrZv357y+C1btuCkk05Sx8nxt9xyC6677jq89NJLnf7cqWv+DUTJL8UVFRWxy8iRI/kp6YYaGxsxefJk/POf/2zT8fwe0LM//1H8+s8O8sfrNddcg8WLF+O9995DIBDA/Pnz1b+LlvB7QM/+/Efxe0D3N2DAAPzpT3/CkiVL1OWYY47B6aefjlWrVqU8nl/7RESpGcLhcLiFfdTFDj/8cEydOlW9yhM1duxY9SqfvPKX7Oabb8brr7+ONWvWxLZdffXVWLFiBb744otOe97Udf8G5NXduXPnora2FkVFRfxUZBGp8HnllVfU574l/B7Qsz///PrPbvv27VOVPhKEHHXUUSmP4feAnv355/eA7FZSUoI777wTl19+ebN9/NonIkqNFV4ZyufzYenSperVvERy//PPP095joRayccff/zx6pUhv9+f1udLmfFvIGrKlCno27cv5s2bhw8//JCfnh6C3wNI8Os/OzmdztgfvS3h94Ce/fmP4veA7BIMBvHss8+q6j5pbUyFX/tERKkx8MpQVVVV6gdc7969ddvl/p49e1KeI9tTHS9l8PJ4lP3/BiTkeuCBB1Qb68svv4zRo0er0EtmAVH24/eAno1f/9lLivFvuOEGHHHEEZgwYUKLx/F7QM/+/PN7QHb57rvvkJeXB5vNpjo2pMp33LhxKY/l1z4RUWrmFrZTBrWxJP/Sk7xtf8en2k7Z+W9AAi65RMkrgTt27MBf//rXFlsgKLvwe0DPxa//7PXTn/4U3377LT799NP9HsvvAT3388/vAdlFPp/Lly9HXV2deiHz4osvVi2tLYVe/NonImqOFV4ZqqysDCaTqVklT2VlZbOKn6g+ffqkPN5sNqO0tDStz5cy499AKjNmzMCGDRvS8Awp0/B7ACXj13/3d+2116r5nNKeLoOsW8PvAT37858Kvwd0X1arFSNGjMChhx6q5rbKIiZ33313ymP5tU9ElBoDrwz+ITdt2jS1Mk8iuT9r1qyU50g1T/Lx7777rvpBabFY0vp8KTP+DaQiqztKmwNlP34PoGT8+u++pJpXKnukPX3hwoUYOnTofs/h94Ce/flPhd8DsuvfhNfrTbmPX/tERKmxpTGDybyGCy+8UAVW8oNMZjNt375d9fGLX//619i1axcef/xxdV+2y/L1ct6VV16pBlg+/PDDeOaZZ7r4PaHO+jdw1113YciQIRg/frwaev/kk0+qMni5UPfT0NCAjRs36pYdl/YGGVo8aNAgfg/Icu39/PPrP7tcc801ePrpp/Haa68hPz8/Vu1bWFgIh8OhbvP3gOx1IJ9/fg/IHrfccgtOPPFEDBw4EPX19WpovazC+c4776j9/NonImqjMGW0++67Lzx48OCw1WoNT506Nbxo0aLYvosvvjg8Z84c3fEfffRReMqUKer4IUOGhBcsWNAFz5q66t/An//85/Dw4cPDdrs9XFxcHD7iiCPCb731Fj8h3dSHH34oQ/iaXeTzLvg9ILu19/PPr//skupzL5dHHnkkdgy/B2SvA/n883tA9rjssstiv/uVl5eH582bF3733Xdj+/m1T0TUNgb5v7aGY0RERERERERERJmOM7yIiIiIiIiIiCirMPAiIiIiIiIiIqKswsCLiIiIiIiIiIiyCgMvIiIiIiIiIiLKKgy8iIiIiIiIiIgoqzDwIiIiIiIiIiKirMLAi4iIiIiIiIiIsgoDLyIiIiIiIiIiyioMvIiIiHqwSy65BGeccUZXPw0iIqJmPv74Y5x66qno168fDAYDXn311XZ/lMLhMP76179i1KhRsNlsGDhwIP7whz/wo03UA5i7+gkQERFR17n77rvVHwNERESZprGxEZMnT8all16Ks88++4Ae42c/+xneffddFXpNnDgRTqcTVVVVHf5ciSjzGML8LZeIiIiIiIgymFR4vfLKK7qqZJ/Ph9tuuw1PPfUU6urqMGHCBPz5z3/G0UcfrfavWbMGkyZNwsqVKzF69OgufPZE1BXY0khERNQDvPjii+qVbYfDgdLSUhx77LHqlfPElsatW7eqPyiSL9E/HMTnn3+Oo446Sj2OtIVcd9116nGIiIg6m1R+ffbZZ3j22Wfx7bff4pxzzsEJJ5yADRs2qP1vvPEGhg0bhjfffBNDhw7FkCFDcMUVV6CmpoafLKIegIEXERFRlquoqMB5552Hyy67TL3a/dFHH+Gss85q1sooAZYcG7188803KhyTgEt89913OP7449W58ofFc889h08//RQ//elPu+g9IyKinmrTpk145pln8MILL+DII4/E8OHDcdNNN+GII47AI488oo7ZvHkztm3bpo55/PHH8eijj2Lp0qX43ve+19VPn4g6AWd4ERERZTkJrwKBgAqqBg8erLZJtVcyk8mEPn36qNsej0dVfs2cORO//e1v1bY777wTP/zhD3H99der+yNHjsQ999yDOXPmYMGCBbDb7Z36fhERUc+1bNky9cKNDKNP5PV61Ys1IhQKqfsSdkWPe/jhhzFt2jSsW7eObY5EWY6BFxERUZaTgb/z5s1TIZdUaM2fP1+9ul1cXNziOZdffjnq6+vx3nvvwWjUCsLlVfGNGzeqWSlR8seG/EGxZcsWjB07tlPeHyIiIvnZIy/UyM8muU6Ul5enrvv27Quz2awLxaI/q7Zv387AiyjLMfAiIiLKcvKHgARXMn9LVqq69957ceutt+LLL79Mefzvf/97vPPOO/jqq6+Qn5+v++PiRz/6kZrblWzQoEFpfR+IiIgSTZkyBcFgEJWVlaqlMZXZs2erCmdpf5SWR7F+/Xp1Ha14JqLsxVUaiYiIehj5A0F+0b/hhhvULC5Z2erVV19V+1566SU17+vtt99WVWGJzj//fOzZswcffPBBFz1zIiLqSRoaGlRlcTTg+vvf/465c+eipKREvdBywQUXqKH1f/vb39T+qqoqLFy4UFU0n3TSSeqFmunTp6uKr7vuukvdv+aaa1BQUKBeACKi7Mah9URERFlOKrn+8Ic/YMmSJaqF4+WXX8a+ffuatSDKsu0XXXQRbr75ZowfP16FW3KJrmYl27/44gv1x8Ly5cvVKlivv/46rr322i56z4iIKJvJzy0JsuQi5IUauf2b3/xG3Zfh9PJz68Ybb1Ttiaeddpr6mSeLsAhpyZeVGsvKytQCLCeffLL62SerOhJR9mOFFxERUZaTlRl//vOfqwG/LpdLVXdJSCWrK15yySWxCi9ZvUqWeE8mQ+llZUfx9ddfq3ZICb5kfpe0iJx77rm45ZZbuuA9IyIiIiJKjYEXERERERERERFlFbY0EhERERERERFRVmHgRUREREREREREWYWBFxERERERERERZRUGXkRERERERERElFUYeBERERERERERUVZh4EVERERERERERFmFgRcREREREREREWUVBl5ERERERERERJRVGHgREREREREREVFWYeBFRERERERERERZhYEXEREREREREREhm/x/TZVw/fx1d/MAAAAASUVORK5CYII=\",\n      \"text/plain\": [\n       \"<Figure size 1232.88x1000 with 1 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    }\n   ],\n   \"source\": [\n    \"sns.lmplot(data=scaling_df, x=\\\"size\\\", y=\\\"time\\\", hue=\\\"algorithm\\\", order=2, height=10)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"ec10f642-5eae-49c9-ab87-eb7da3f5ef4d\",\n   \"metadata\": {},\n   \"source\": [\n    \"Here we see EVoC showing its real strengths. As data set sizes scale up, it comtinues to perform extremely quickly, on par, or even better than, MiniBatchKMeans, and distinctly faster than classic KMeans implementations, including FAISS fast version.\\n\",\n    \"\\n\",\n    \"Lastly, let's have a look at quality. We shouldn't expect to see too much here, this is data that should be very easy cluster, so we should expect mostly perfect results. However, some implementations are making trade-offs and approximations, so we should expect to see some of those differences shown here.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 29,\n   \"id\": \"c0e7c2ab-e1d2-478f-a9a9-01a762d8da3b\",\n   \"metadata\": {\n    \"execution\": {\n     \"iopub.execute_input\": \"2026-03-26T09:38:21.089635Z\",\n     \"iopub.status.busy\": \"2026-03-26T09:38:21.089471Z\",\n     \"iopub.status.idle\": \"2026-03-26T09:38:21.375853Z\",\n     \"shell.execute_reply\": \"2026-03-26T09:38:21.375331Z\",\n     \"shell.execute_reply.started\": \"2026-03-26T09:38:21.089621Z\"\n    }\n   },\n   \"outputs\": [\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"<seaborn.axisgrid.FacetGrid at 0x7420508b6a80>\"\n      ]\n     },\n     \"execution_count\": 29,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    },\n    {\n     \"data\": {\n      \"image/png\": \"iVBORw0KGgoAAAANSUhEUgAAA90AAAPdCAYAAACXzguGAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAlCtJREFUeJzs3QeYXVW5P+BvSmZSZ0jvlVACIUAChiLSQxe8KCAKiCgiAgLXhnj1iv7Fq6jYQJGiSJWqYOi9QxJKgJAQEtJ7mUmd/n/2HjPJZM4gxOy0ed/nOU9y1t77nHX2hJDfWWt9K6+urq4uAAAAgI0uf+O/JAAAACB0AwAAQIaMdAMAAEBGhG4AAADIiNANAAAAGRG6AQAAICNCNwAAAGSkxYXuZFvy8vLy9FcAAADIUosL3cuWLYvS0tL0VwAAAMhSiwvdAAAAsKkI3QAAAJARoRsAAAAyInQDAABARoRuAAAAyIjQDQAAABkRugEAACAjQjcAAABkROgGAACAjAjdAAAAkBGhGwAAADIidAMAAEBGhG4AAADIiNANAAAAGRG6AQAAICNCNwAAAGRE6AYAAICMCN0AAACQEaEbAAAAMiJ0AwAAQEaEbgAAAMiI0A0AAAAZEboBAAAgI0I3AAAAZEToBgAAgIwI3QAAAJARoRsAAAAyInQDAABARoRuAAAAyIjQDQAAABkRugEAACAjQjcAAABkROgGAACAjAjdAAAAkBGhGwAAADIidAMAAEBGhG4AAADIiNC9FVtVWROPvj0vHn9nXqyuqmlyvHx1VcxcsjLq6uqiJXt9xtL45xtzYsbilZu7KwAAbICp4xfGA38cH6+MntrQtmJpRbw/fmEsnrOiyfnVVTUx453FMee9sqirbfpv4fnTytPjVRVN/w29ekVVLJi+LCpWVTc5lvy7unzhqqhYWeXn2Ixc9ztRU1MbZQtWReXqHPe1ti6957l+lg0/r7cX57w2+XlMe3NR+uv6kp9T8mdk7pSyhrYpr82P0X94I8Y8sPbPUtby6jZjInv66afj5z//eYwdOzbmzJkT99xzT5xwwgkfeM1TTz0VF198cbz11lvRq1ev+Na3vhXnnHPOh37P8vLyKC0tjbKysigpKYmt1cNvzY1v3PF6lP/rD16ndkXx61P2iAN26BorKqrj+39/K/7x+qyoqqmL/p3bxqVHD4lRu/bYpH1curIy7nt9dixYXhn7Duoc+27feaO/R2V1bfzhqffi3ldnpV88HDqke1x42A7RuX1xLFlRGWf/dUy88v6S9Nz8vIhTPtYvfnz80MhPngAAsEVLosoN33o2Vi1rHHK3H941pr62MGr/FfD67do5jvjSrlHUpjAmj50fT97yTlSsqP93cmnXNnHkV4ZGlz4domzBynjgj2/GopnL02NFrQti/8/sELvs3yt9refvnBxvPjMraqpqo7AoP4Yd3Cf2OWH7yMvLi/ffWBjP3vFuGhyTf0tuP6JbHHjqTlHcpjANlG88NjMmvTI3amvqYtCeXWPPw/tFUevC9H2qKmvivXHzY/niiuixfWn02anjBt+TpJ8VK6qiuG1h5BdsujHU5H3ffmZWvDtmfhqSk8+424F9oqBVfvp83MPT4o0nZsbKssroPrAkvW9rPuebT8+Kl++fGqvKK9P7usvHe8X+Jw5O+z9jwuJ44q/vxLLFq9Nzu/brEId/cZfo2KNdeq+TL1vW/LxaJT+vEwfHrgf0jprq2njsLxPi3THzIuoi8vIidhzZIw4+becoKMiP1x+fES/e+15UV9am13bq1S6WL1kdlavWftGSlx9x4rf2iu4DSrbd0P3AAw/Ec889F8OHD48TTzzx34buqVOnxtChQ+PLX/5yfOUrX0mvPffcc+PWW29Nr28poXvh8orY/6ePR0V1/R+gNdoXF8YLlxwS373nzTTsrqswPy/u/dr+MbR3acwrXx1/e2VGzFyyKnbvu12csGevaFtU/xdCYtbSVfHa9KXRc7vWMbzfhv2FMHba4vjCDa/EsnW+jTpy1x7x+88Nj4INCLzjZ5bFg2/NicL8/Dhu954xuFuHtP2rN42NB96c2+jcwd3ax/3nfzy+eecbTe5D4scnDI3P79N/gz4XAACbzj2/GBez3136oc4dsn/P2OuoAXHzD15Mg++62ncqjtN+tG/87fIxDQFujSSsfeaSvWPaW4vipb9PafK6H//MDtFn547xt5+80uR1B+7eJY7+6rA0GE55dUGjYz0GlcSnvjEils5dGX//9atpGF2j/9DOcdQ5u0VBYX46ejvppbmxYOby9AuCIfv1jDbti3J+xjefmhljHpiWjvK36dAq9jisXww/4t//u7Z80aqY9PK8qFpdk753rx22i4/q4WvfTAP3uvrt0imOPX/3eOkf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EboBAAAgI0I3AAAAZEToBgAAgIwI3QAAACB0AwAAwNbFSDcAAABkROgGAACAjAjdAAAAkBGhGwAAADIidAMAAEBGhG4AAADIiNANAAAAGRG6AQAAICNCNwAAAGRE6AYAAICMCN0AAACQEaEbAAAAMiJ0AwAAQEaEbgAAAMiI0A0AAAAZEboBAAAgI0I3AAAAZEToBgAAgIwI3QAAAJARoRsAAAAyInQDAABARoRuAAAAyIjQDQAAABkRugEAAGBbDd1XXXVVDBw4MFq3bh0jRoyIZ5555kNd99xzz0VhYWHssccemfcRAAAAtrrQffvtt8eFF14Yl156abz66qtxwAEHxFFHHRXTp0//wOvKysri9NNPj0MPPXST9RUAAAA+qry6urq62ExGjhwZw4cPj6uvvrqhbciQIXHCCSfE5Zdf3ux1p5xySuywww5RUFAQ9957b7z22mvNnltRUZE+1igvL4++ffumwb2kpGQjfhoAAADYQka6KysrY+zYsTFq1KhG7cnz559/vtnrbrjhhnjvvffiBz/4wYd6nyS8l5aWNjySwA0AAADbdOheuHBh1NTURPfu3Ru1J8/nzp2b85p33303vvOd78TNN9+cruf+MC655JJ0VHvNY8aMGRul/wAAAPDvfLjkmqG8vLxGz5PZ7uu3JZKAfuqpp8YPf/jD2HHHHT/06xcXF6cPAAAAaDGhu0uXLuma7PVHtefPn99k9DuxbNmyGDNmTFpw7bzzzkvbamtr05CejHo//PDDccghh2yy/gMAAMAWO728qKgo3SLskUceadSePN9vv/2anJ8UPRs/fnxaNG3N45xzzomddtop/X1SlA0AAAC2JJt1evnFF18cp512Wuy1116x7777xjXXXJNuF5aE6TXrsWfNmhU33nhj5Ofnx9ChQxtd361bt3R/7/XbAQAAIFp66D755JNj0aJFcdlll8WcOXPS8Dx69Ojo379/ejxp+3d7dgMAAMCWarPu0705JPt0J1uH2acbAACAbXZNNwAAAGzrhG4AAADIiNANAAAAGRG6AQAAICNCNwAAAGRE6AYAAICMCN0AAACQEaEbAAAAMiJ0AwAAQEaEbgAAAMiI0A0AAAAZEboBAAAgI0I3AAAAZEToBgAAgIwI3QAAAJARoRsAAAAyInQDAABARoRuAAAAyIjQDQAAABkRugEAACAjQjcAAABkROgGAACAjAjdAAAAkBGhGwAAADIidAMAAEBGhG4AAADIiNANAAAAGRG6AQAAICNCNwAAAGRE6AYAAICMCN0AAACQEaEbAAAAMiJ0AwAAQEaEbgAAAMiI0A0AAAAZEboBAAAgI0I3AAAAZEToBgAAgIwI3QAAAJARoRsAAAAyInQDAABARoRuAAAAyIjQDQAAABkRugEAACAjQjcAAABkROgGAACAjAjdAAAAkBGhGwAAADIidAMAAEBGhG4AAADIiNANAAAAGRG6AQAAICNCNwAAAGRE6AYAAICMCN0AAACQEaEbAAAAMiJ0AwAAQEaEbgAAAMiI0A0AAAAZEboBAAAgI0I3AAAAZEToBgAAgIwI3QAAAJARoRsAAAAyInQDAABARoRuAAAAyIjQDQAAABkRugEAACAjQjcAAABkROgGAACAjAjdAAAAkBGhGwAAADIidAMAAEBGhG4AAADIiNANAAAAGRG6AQAAICNCNwAAAGRE6AYAAICMCN0AAACQEaEbAAAAMiJ0AwAAQEaEbgAAAMiI0A0AAAAZEboBAAAgI0I3AAAAZEToBgAAgIwI3QAAAJARoRsAAAAyInQDAABARoRuAAAAyIjQDQAAABkRugEAACAjQjcAAABkROgGAACAjAjdAAAAkBGhGwAAADIidAMAAEBGhG4AAADIiNANAAAAGRG6AQAAICNCNwAAAGRE6AYAAICMCN0AAACQEaEbAAAAMiJ0AwAAQEaEbgAAAMiI0A0AAAAZEboBAAAgI0I3AAAAZEToBgAAgIwI3QAAAJARoRsAAAAyInQDAACA0A0AAABbFyPdAAAAkBGhGwAAADIidAMAAEBGhG4AAADIiNANAAAAGRG6AQAAICNCNwAAAGRE6AYAAICMCN0AAACQEaEbAAAAMiJ0AwAAQEaEbgAAAMiI0A0AAAAZEboBAAAgI0I3AAAAZEToBgAAgIwI3QAAAJARoRsAAAAyInQDAABARoRuAAAAyIjQDQAAABkRugEAACAjQjcAAABkROgGAACAjAjdAAAAkBGhGwAAADIidAMAAEBGhG4AAADIiNANAAAAGRG6AQAAICNCNwAAAGRE6AYAAICMCN0AAACQEaEbAAAAMiJ0AwAAQEaEbgAAAMiI0A0AAAAZEboBAAAgI0I3AAAAZEToBgAAgIwI3QAAAJARoRsAAAAyInQD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\",\n      \"text/plain\": [\n       \"<Figure size 1000x1000 with 1 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    }\n   ],\n   \"source\": [\n    \"sns.catplot(data=scaling_df, x=\\\"algorithm\\\", y=\\\"ari\\\", hue=\\\"algorithm\\\", kind=\\\"swarm\\\", height=10)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"7d27c69d-22bd-4b3c-9e58-defc84d1bdc0\",\n   \"metadata\": {},\n   \"source\": [\n    \"The results are pretty clear. The UMAP + HDBSCAN approach mostly produces good clusters -- the decay in quality is essentially down to having run with default parameters which need to be adjusted somewhat for the larger dataset sizes (we weren't doing any parameter tuning). KMeans mostly manages to find the right clusters. FAISS's Kmeans was possibly not well tuned. MiniBatchKMeans shows a lot more variability than KMeans (as we would expect) including some very poor results occasionally; it might be worth it if you need the speed, but it definitely has costs in terms of cluster quality. Lstly, however, we have EVoC which, despite keeping pace with MiniBatchKMeans in compute time the whole way, also manages to produce almost perfect clusterings every time.\"\n   ]\n  }\n ],\n \"metadata\": {\n  \"kernelspec\": {\n   \"display_name\": \"evoc_docs\",\n   \"language\": \"python\",\n   \"name\": \"evoc_docs\"\n  },\n  \"language_info\": {\n   \"codemirror_mode\": {\n    \"name\": \"ipython\",\n    \"version\": 3\n   },\n   \"file_extension\": \".py\",\n   \"mimetype\": \"text/x-python\",\n   \"name\": \"python\",\n   \"nbconvert_exporter\": \"python\",\n   \"pygments_lexer\": \"ipython3\",\n   \"version\": \"3.12.13\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 5\n}\n"
  },
  {
    "path": "doc/source/changelog.rst",
    "content": "Changelog\n=========\n\nThis document records all notable changes to EVoC.\n\nVersion 0.1.0 (TBD)\n-------------------\n\nInitial release of EVoC.\n\n**Features:**\n\n* Core clustering algorithm with hierarchical multi-layer support\n* Scikit-learn compatible API\n* Support for multiple embedding types (float, int8, uint8)  \n* Optimized distance metrics (cosine, quantized cosine, bitwise Jaccard)\n* Numba-accelerated performance\n* Comprehensive parameter set for fine-tuning\n* Built-in duplicate detection\n* Extensive documentation and examples\n\n**API Reference:**\n\n* ``EVoC`` - Main clustering class\n* ``evoc_clusters`` - Functional interface  \n* ``build_cluster_layers`` - Multi-layer clustering construction\n\n**Performance:**\n\n* Efficient processing of high-dimensional embeddings\n* Memory-optimized algorithms\n* Multi-threaded computation support\n\n**Documentation:**\n\n* Complete API documentation with numpydoc formatting\n* Interactive Jupyter notebook examples\n* Comprehensive user guide and tutorials\n* ReadTheDocs integration\n"
  },
  {
    "path": "doc/source/conf.py",
    "content": "# Configuration file for the Sphinx documentation builder.\n#\n# For the full list of built-in configuration values, see the documentation:\n# https://www.sphinx-doc.org/en/master/usage/configuration.html\n\n# -- Project information -----------------------------------------------------\n# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information\n\nimport os\nimport sys\n\nsys.path.insert(0, os.path.abspath(\"../..\"))\n\nproject = \"EVoC\"\ncopyright = \"2024, Tutte Institute\"\nauthor = \"Tutte Institute\"\nrelease = \"0.1.0\"\n\n# -- General configuration ---------------------------------------------------\n# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration\n\nextensions = [\n    \"sphinx.ext.autodoc\",\n    \"sphinx.ext.autosummary\",\n    \"sphinx.ext.viewcode\",\n    \"sphinx.ext.napoleon\",\n    \"sphinx.ext.intersphinx\",\n    \"sphinx.ext.mathjax\",\n    \"sphinx.ext.githubpages\",\n    \"numpydoc\",\n    \"nbsphinx\",\n    \"IPython.sphinxext.ipython_console_highlighting\",\n]\n\ntemplates_path = [\"_templates\"]\nexclude_patterns = [\"_build\", \"Thumbs.db\", \".DS_Store\", \"**.ipynb_checkpoints\"]\n\n# -- Options for HTML output ------------------------------------------------\n# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output\n\nhtml_theme = \"sphinx_rtd_theme\"\nhtml_static_path = [\"_static\"]\nhtml_css_files = [\"custom.css\"]\n\n# Theme options\nhtml_theme_options = {\n    \"canonical_url\": \"\",\n    \"analytics_id\": \"\",\n    \"logo_only\": False,\n    \"display_version\": True,\n    \"prev_next_buttons_location\": \"bottom\",\n    \"style_external_links\": False,\n    \"vcs_pageview_mode\": \"\",\n    \"style_nav_header_background\": \"#2980B9\",\n    # Toc options\n    \"collapse_navigation\": True,\n    \"sticky_navigation\": True,\n    \"navigation_depth\": 4,\n    \"includehidden\": True,\n    \"titles_only\": False,\n}\n\n# -- Extension configuration -------------------------------------------------\n\n# Autodoc configuration\nautodoc_default_options = {\n    \"members\": True,\n    \"member-order\": \"bysource\",\n    \"special-members\": \"__init__\",\n    \"undoc-members\": True,\n    \"exclude-members\": \"__weakref__\",\n}\n\n# Autosummary configuration\nautosummary_generate = True\nautosummary_imported_members = True\n\n# Napoleon configuration (for Google and NumPy style docstrings)\nnapoleon_google_docstring = True\nnapoleon_numpy_docstring = True\nnapoleon_include_init_with_doc = False\nnapoleon_include_private_with_doc = False\nnapoleon_include_special_with_doc = True\nnapoleon_use_admonition_for_examples = False\nnapoleon_use_admonition_for_notes = False\nnapoleon_use_admonition_for_references = False\nnapoleon_use_ivar = False\nnapoleon_use_param = True\nnapoleon_use_rtype = True\nnapoleon_preprocess_types = False\nnapoleon_type_aliases = None\nnapoleon_attr_annotations = True\n\n# Numpydoc configuration\nnumpydoc_show_class_members = False\nnumpydoc_show_inherited_class_members = False\nnumpydoc_class_members_toctree = False\nnumpydoc_use_plots = True\nnumpydoc_validation_checks = {\n    \"all\",\n    \"GL01\",\n    \"GL02\",\n    \"GL03\",\n    \"GL05\",\n    \"GL06\",\n    \"GL07\",\n    \"GL09\",\n    \"GL10\",\n}\n\n# NBSphinx configuration\nnbsphinx_execute = \"never\"  # Don't execute notebooks during build\nnbsphinx_allow_errors = True\nnbsphinx_timeout = 60\nnbsphinx_codecell_lexer = \"ipython3\"\n\n# Intersphinx configuration\nintersphinx_mapping = {\n    \"python\": (\"https://docs.python.org/3/\", None),\n    \"numpy\": (\"https://numpy.org/doc/stable/\", None),\n    \"scipy\": (\"https://docs.scipy.org/doc/scipy/\", None),\n    \"matplotlib\": (\"https://matplotlib.org/stable/\", None),\n    \"sklearn\": (\"https://scikit-learn.org/stable/\", None),\n    \"pandas\": (\"https://pandas.pydata.org/pandas-docs/stable/\", None),\n}\n\n# Math configuration\nmathjax_path = \"https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js\"\n\n# Source file suffixes\nsource_suffix = \".rst\"\n\n# Master document\nmaster_doc = \"index\"\n\n# Language for content autogenerated by Sphinx\nlanguage = \"en\"\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\nexclude_patterns = [\"_build\", \"Thumbs.db\", \".DS_Store\"]\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = \"sphinx\"\n\n# If true, `todo` and `todoList` produce output, else they produce nothing.\ntodo_include_todos = False\n\n# -- Options for LaTeX output -----------------------------------------------\n\nlatex_elements = {\n    # The paper size ('letterpaper' or 'a4paper').\n    #\n    # 'papersize': 'letterpaper',\n    # The font size ('10pt', '11pt' or '12pt').\n    #\n    # 'pointsize': '10pt',\n    # Additional stuff for the LaTeX preamble.\n    #\n    # 'preamble': '',\n    # Latex figure (float) alignment\n    #\n    # 'figure_align': 'htbp',\n}\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title,\n#  author, documentclass [howto, manual, or own class]).\nlatex_documents = [\n    (master_doc, \"EVoC.tex\", \"EVoC Documentation\", \"Tutte Institute\", \"manual\"),\n]\n\n# -- Options for manual page output ------------------------------------------\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section).\nman_pages = [(master_doc, \"evoc\", \"EVoC Documentation\", [author], 1)]\n\n# -- Options for Texinfo output ----------------------------------------------\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author,\n#  dir menu entry, description, category)\ntexinfo_documents = [\n    (\n        master_doc,\n        \"EVoC\",\n        \"EVoC Documentation\",\n        author,\n        \"EVoC\",\n        \"Embedding Vector Oriented Clustering\",\n        \"Miscellaneous\",\n    ),\n]\n\n# -- Options for Epub output -------------------------------------------------\n\n# Bibliographic Dublin Core info.\nepub_title = project\nepub_author = author\nepub_publisher = author\nepub_copyright = copyright\n\n# The unique identifier of the text. This can be a ISBN number\n# or the project homepage.\n#\n# epub_identifier = ''\n\n# A unique identification for the text.\n#\n# epub_uid = ''\n\n# A list of files that should not be packed into the epub file.\nepub_exclude_files = [\"search.html\"]\n"
  },
  {
    "path": "doc/source/examples.rst",
    "content": "Examples\n========\n\nCollection of practical examples demonstrating EVoC usage in different scenarios.\n\nBasic Examples\n--------------\n\nSimple Clustering\n~~~~~~~~~~~~~~~~~\n\n.. code-block:: python\n\n   from evoc import EVoC\n   import numpy as np\n\n   # Simple example with random data\n   X = np.random.rand(500, 128)\n   clusterer = EVoC()\n   labels = clusterer.fit_predict(X)\n\n   print(f\"Found {len(np.unique(labels[labels >= 0]))} clusters\")\n\nSpecify Number of Clusters\n~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n.. code-block:: python\n\n   # When you know the desired number of clusters\n   clusterer = EVoC(approx_n_clusters=5)\n   labels = clusterer.fit_predict(X)\n\nWorking with Real Embeddings\n-----------------------------\n\nCLIP Embeddings\n~~~~~~~~~~~~~~~\n\n.. code-block:: python\n\n   import torch\n   import clip\n   from evoc import EVoC\n\n   # Load CLIP model\n   device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n   model, preprocess = clip.load(\"ViT-B/32\", device=device)\n\n   # Generate embeddings for images\n   # (assuming you have a list of PIL images)\n   embeddings = []\n   with torch.no_grad():\n       for image in images:\n           image_input = preprocess(image).unsqueeze(0).to(device)\n           embedding = model.encode_image(image_input)\n           embeddings.append(embedding.cpu().numpy())\n\n   X = np.vstack(embeddings)\n\n   # Cluster the embeddings\n   clusterer = EVoC(\n       n_neighbors=20,\n       noise_level=0.6,\n       base_min_cluster_size=3\n   )\n   labels = clusterer.fit_predict(X)\n\nSentence Embeddings\n~~~~~~~~~~~~~~~~~~~\n\n.. code-block:: python\n\n   from sentence_transformers import SentenceTransformer\n   from evoc import EVoC\n\n   # Load sentence transformer model\n   model = SentenceTransformer('all-MiniLM-L6-v2')\n\n   # Your text data\n   texts = [\n       \"The cat sat on the mat\",\n       \"Dogs are great pets\", \n       \"Machine learning is fascinating\",\n       # ... more texts\n   ]\n\n   # Generate embeddings\n   embeddings = model.encode(texts)\n\n   # Cluster similar texts\n   clusterer = EVoC(\n       n_neighbors=15,\n       noise_level=0.4,\n       base_min_cluster_size=2\n   )\n   labels = clusterer.fit_predict(embeddings)\n\n   # Group texts by cluster\n   clusters = {}\n   for i, label in enumerate(labels):\n       if label >= 0:  # Ignore noise points\n           if label not in clusters:\n               clusters[label] = []\n           clusters[label].append(texts[i])\n\nAdvanced Usage\n--------------\n\nHierarchical Analysis\n~~~~~~~~~~~~~~~~~~~~~\n\n.. code-block:: python\n\n   # Get multiple clustering granularities\n   clusterer = EVoC(max_layers=5)\n   clusterer.fit(X)\n\n   # Analyze each layer\n   for i, layer in enumerate(clusterer.cluster_layers_):\n       n_clusters = len(np.unique(layer[layer >= 0]))\n       persistence = clusterer.persistence_scores_[i]\n\n       print(f\"Layer {i}: {n_clusters} clusters, \"\n             f\"persistence: {persistence:.3f}\")\n\n   # Access the hierarchical structure\n   tree = clusterer.cluster_tree_\n   print(f\"Hierarchical structure: {tree}\")\n\nParameter Optimization\n~~~~~~~~~~~~~~~~~~~~~~\n\n.. code-block:: python\n\n   from sklearn.metrics import silhouette_score\n\n   # Grid search over parameters\n   best_score = -1\n   best_params = None\n\n   for n_neighbors in [10, 15, 20]:\n       for noise_level in [0.3, 0.5, 0.7]:\n           clusterer = EVoC(\n               n_neighbors=n_neighbors,\n               noise_level=noise_level,\n               random_state=42\n           )\n           labels = clusterer.fit_predict(X)\n\n           if len(np.unique(labels[labels >= 0])) > 1:\n               score = silhouette_score(X, labels)\n               if score > best_score:\n                   best_score = score\n                   best_params = {\n                       'n_neighbors': n_neighbors,\n                       'noise_level': noise_level\n                   }\n\n   print(f\"Best parameters: {best_params}\")\n   print(f\"Best silhouette score: {best_score:.3f}\")\n\nMemory-Efficient Processing\n~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n.. code-block:: python\n\n   # For large datasets, use smaller parameters\n   clusterer = EVoC(\n       n_neighbors=10,        # Reduce graph density\n       node_embedding_dim=8,  # Lower embedding dimension  \n       n_epochs=30,          # Fewer training epochs\n       max_layers=3          # Limit hierarchy depth\n   )\n\n   # Process in chunks if needed\n   chunk_size = 10000\n   all_labels = []\n\n   for i in range(0, len(X), chunk_size):\n       chunk = X[i:i+chunk_size]\n       chunk_labels = clusterer.fit_predict(chunk)\n       all_labels.extend(chunk_labels)\n\nSpecialized Data Types\n----------------------\n\nBinary Embeddings\n~~~~~~~~~~~~~~~~~~\n\n.. code-block:: python\n\n   # For binary/hash embeddings\n   binary_embeddings = (embeddings > 0.5).astype(np.uint8)\n\n   clusterer = EVoC(\n       n_neighbors=25,     # More neighbors for binary data\n       neighbor_scale=1.5, # Denser graph\n       noise_level=0.4     # Lower noise threshold\n   )\n   labels = clusterer.fit_predict(binary_embeddings)\n\nQuantized Embeddings\n~~~~~~~~~~~~~~~~~~~~\n\n.. code-block:: python\n\n   # For int8 quantized embeddings\n   quantized_embeddings = (embeddings * 127).clip(-127, 127).astype(np.int8)\n\n   clusterer = EVoC(\n       n_neighbors=20,\n       base_min_cluster_size=8,\n       noise_level=0.6\n   )\n   labels = clusterer.fit_predict(quantized_embeddings)\n\nEvaluation and Validation\n--------------------------\n\nCluster Quality Assessment\n~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n.. code-block:: python\n\n   from sklearn.metrics import (\n       silhouette_score, \n       calinski_harabasz_score,\n       davies_bouldin_score\n   )\n\n   # Fit the clusterer\n   labels = clusterer.fit_predict(X)\n\n   # Calculate quality metrics\n   if len(np.unique(labels[labels >= 0])) > 1:\n       silhouette = silhouette_score(X, labels)\n       calinski_harabasz = calinski_harabasz_score(X, labels)  \n       davies_bouldin = davies_bouldin_score(X, labels)\n\n       print(f\"Silhouette Score: {silhouette:.3f}\")\n       print(f\"Calinski-Harabasz Score: {calinski_harabasz:.3f}\")\n       print(f\"Davies-Bouldin Score: {davies_bouldin:.3f}\")\n\n   # Analyze membership strengths\n   strengths = clusterer.membership_strengths_\n   print(f\"Average membership strength: {np.mean(strengths):.3f}\")\n   print(f\"Std of membership strengths: {np.std(strengths):.3f}\")\n\nStability Analysis\n~~~~~~~~~~~~~~~~~~\n\n.. code-block:: python\n\n   # Test clustering stability across random seeds\n   stability_scores = []\n\n   for seed in range(10):\n       clusterer = EVoC(random_state=seed)\n       labels = clusterer.fit_predict(X)\n\n       if len(np.unique(labels[labels >= 0])) > 1:\n           score = silhouette_score(X, labels)\n           stability_scores.append(score)\n\n   print(f\"Mean stability: {np.mean(stability_scores):.3f}\")\n   print(f\"Std stability: {np.std(stability_scores):.3f}\")\n"
  },
  {
    "path": "doc/source/index.rst",
    "content": ".. image:: evoc_logo_horizontal.png\n  :width: 600\n  :align: center\n  :alt: EVōC Logo\n\nEVōC: Embedding Vector Oriented Clustering\n==========================================\n\n.. image:: https://img.shields.io/badge/python-3.8%2B-blue.svg\n   :target: https://www.python.org/downloads/\n   :alt: Python Version\n\n.. image:: https://img.shields.io/badge/license-BSD-green.svg\n   :target: https://opensource.org/licenses/BSD-3-Clause\n   :alt: License\n\nEVōC (pronounced as \"evoke\") provides Embedding Vector Oriented Clustering.\n\nEVōC (Embedding Vector Oriented Clustering) is a powerful clustering algorithm designed specifically for high-dimensional \nembedding vectors such as CLIP-vectors, sentence-transformers output, and other dense vector representations. \n\nThe algorithm combines a node embedding approach (related to UMAP) with density-based clustering (related to HDBSCAN), \nproviding improved efficiency and quality for clustering high-dimensional embedding vectors.\n\nKey Features\n------------\n\n* **Optimized for High-Dimensional Embeddings**: Specifically designed for modern embedding vectors\n* **Multi-Layer Clustering**: Provides hierarchical clustering with multiple granularity levels\n* **Performance Optimized**: Uses Numba for high-performance computation\n* **Flexible Parameters**: Extensive parameter set for fine-tuning clustering behavior\n* **Scikit-learn Compatible**: Follows scikit-learn API conventions\n\nQuick Start\n-----------\n\n.. code-block:: python\n\n   from evoc import EVoC\n   import numpy as np\n\n   # Generate sample data\n   X = np.random.rand(1000, 512)  # 1000 samples, 512-dimensional embeddings\n\n   # Initialize and fit the clusterer\n   clusterer = EVoC()\n   labels = clusterer.fit_predict(X)\n\n   # Access cluster layers and membership strengths\n   print(f\"Number of clusters: {len(np.unique(labels[labels >= 0]))}\")\n   print(f\"Number of cluster layers: {len(clusterer.cluster_layers_)}\")\n\n.. toctree::\n   :maxdepth: 2\n   :caption: Contents:\n\n   installation\n   quickstart\n   user_guide\n   benchmarks\n   api/index\n   changelog\n\nIndices and tables\n==================\n\n* :ref:`genindex`\n* :ref:`modindex`\n* :ref:`search`\n"
  },
  {
    "path": "doc/source/installation.rst",
    "content": "Installation\n============\n\nRequirements\n------------\n\nEVoC requires Python 3.8 or later and the following dependencies:\n\n* numpy >= 1.21.0\n* scipy >= 1.7.0\n* scikit-learn >= 1.0.0\n* numba >= 0.56.0\n\nInstall from PyPI\n-----------------\n\n.. code-block:: bash\n\n   pip install evoc\n\nInstall from Source\n-------------------\n\nTo install the latest development version:\n\n.. code-block:: bash\n\n   git clone https://github.com/TutteInstitute/evoc.git\n   cd evoc\n   pip install -e .\n\nDevelopment Installation\n------------------------\n\nFor development, install with additional dependencies:\n\n.. code-block:: bash\n\n   git clone https://github.com/TutteInstitute/evoc.git\n   cd evoc\n   pip install -e \".[dev,docs,test]\"\n\nVerify Installation\n-------------------\n\nTo verify that EVoC is installed correctly:\n\n.. code-block:: python\n\n   import evoc\n   print(evoc.__version__)\n\n   # Run a quick test\n   from evoc import EVoC\n   import numpy as np\n\n   X = np.random.rand(100, 10)\n   clusterer = EVoC()\n   labels = clusterer.fit_predict(X)\n   print(f\"Clustering completed successfully! Found {len(np.unique(labels[labels >= 0]))} clusters.\")\n\nNote that on first import and first run there will be time spent on Numba's JIT compilation, which may take a few seconds. \nSubsequent runs will be much faster, and the compilation should be cached, so it should not need to be repeated unless you \nchange the code or update Numba.\n"
  },
  {
    "path": "doc/source/quickstart.rst",
    "content": "Quick Start Guide\n================\n\nThis guide provides a quick introduction to using EVōC for clustering high-dimensional embedding vectors. EVōC \nspecifically targets modern embedding vectors such as those produced by CLIP, sentence-transformers, and other \ndense vector representations. It seeks to provide fast and effective results with as little parameter tuning as possible.\n\nBasic Usage\n-----------\n\nThe simplest way to use EVōC is to import the EVoC class, create an instance with default parameters, and call fit_predict on your data:\n\n.. code-block:: python\n\n   from evoc import EVoC\n   from sklearn.datasets import make_blobs\n   import numpy as np\n\n   # Generate sample embedding data\n   blob_data, blob_labels = make_blobs(n_samples=10_000, n_features=512, centers=256)\n\n   # Create and fit the clusterer\n   clusterer = EVoC()\n   labels = clusterer.fit_predict(blob_data)\n\n   # Analyze results\n   n_clusters = len(np.unique(labels[labels >= 0]))\n   n_noise = np.sum(labels == -1)\n\n   print(f\"Found {n_clusters} clusters\")\n   print(f\"Noise points: {n_noise}\")\n\nEVōC uses the sklearn API, so you can drop it in to any existing clustering workflow that expects a fit_predict method. \nThe default parameters are designed to work well for typical embedding data, but you can adjust them as needed\n(see the Parameter Selection section below).\n\nUnderstanding the Output\n------------------------\n\nEVōC uses standard sklearn conventions for its output. After fitting, the clusterer will have the following attributes:\n\n* **labels_**: Cluster labels for each point (-1 for noise)\n* **membership_strengths_**: Confidence scores for cluster membership\n* **cluster_layers_**: Multiple clustering granularities \n* **cluster_tree_**: Hierarchical structure of clusters\n\nThe ``labels_`` attribute is the expected vector of cluster assignments you would get from any sklearn clustering algorithm. \nThe ``membership_strengths_`` attribute provides additional information about how strongly each point belongs to its assigned \ncluster, which can be useful for filtering or analyzing borderline cases; the is equivalent to the ``probabilities_`` attribute \nin HDBSCAN.\n\nThe ``cluster_layers_`` and ``cluster_tree_`` attributes are more novel. EVōC is not a hierarchical clustering algorithm in the \ntraditional sense,  instead it produces multiple layers of clustering resolution, that can be results that can be cast into a \nhierarchy.\n\n.. code-block:: python\n\n   # Access different clustering layers\n   print(f\"Available layers: {len(clusterer.cluster_layers_)}\")\n\n   # Get membership strengths\n   strengths = clusterer.membership_strengths_\n   print(f\"Average membership strength: {np.mean(strengths):.3f}\")\n\n   # Access the cluster hierarchy\n   tree = clusterer.cluster_tree_\n   print(f\"Hierarchical structure: {tree}\")\n\nLayers are sorted from most fine-grained (many small clusters) at index 0 to most coarse-grained (fewer large clusters).\nEach layer is a label vector, just like ``labels_``, but with a different clustering resolution. The ``labels_`` attribute \ncorresponds to the layer that has clusters persisting across the widest range of cluster resolution scales, and is usually \nthe most stable and meaningful clustering result. However, depending on your needs, other cluster layers may be more appropriate.\n\nThe ``cluster_tree_`` attribute provides a hierarchical structure of the clusters across layers. \nIt shows how clusters in finer layers relate to clusters in coarser layers, effectively creating a tree of cluster relationships. \nThis can be useful for understanding the multi-scale structure of your data and for selecting clusters at \ndifferent levels of granularity.\n\nThe tree is structured as a dictionary. Each cluster is identified as a tuple of (layer_index, cluster_id), \nand the value is a list of child clusters in the more fine-grained layers.\n\nParameter Selection\n-------------------\n\nKey parameters to adjust:\n\n**n_neighbors** (default=15)\n   Number of neighbors for graph construction. Increase for more global connectivity.\n\n**base_min_cluster_size** (default=5)\n   Minimum cluster size at the base layer.\n\n**approx_n_clusters** (default=None)\n   Target number of clusters (returns single layer if specified).\n\n.. code-block:: python\n\n   # Example with custom parameters\n   clusterer = EVoC(\n       n_neighbors=25,          # More neighbors for denser graphs\n       base_min_cluster_size=10, # Larger minimum clusters\n       max_layers=5             # Limit hierarchy depth\n   )\n\n   labels = clusterer.fit_predict(blob_data)\n\nWorking with Different Data Types\n---------------------------------\n\nEVoC automatically detects data types and uses appropriate distance metrics:\n\n* **float32/float64**: Cosine distance (default for embeddings)\n* **int8**: Quantized cosine distance  \n* **uint8**: Bitwise Jaccard distance (for binary embeddings)\n\nWe can take out blob data and convert it to different formats to see how EVoC handles them.\nIn practice, you would typically be working with actual embedding data that comes\npre-quantized or binarized depending on the model and/or storage format you are using.\n\n   embeddings = normalize(blob_data)  # Example embedding data\n\n   # For standard embeddings (float)\n   X_float = embeddings.astype(np.float32)\n   labels_cosine = EVoC().fit_predict(X_float)\n\n   # For quantized embeddings (int8)\n   X_quantized = (StandardScaler().fit_transform(embeddings) * 127).astype(np.int8)  \n   labels_quantized = EVoC().fit_predict(X_quantized)\n\n   # For binary embeddings (packed uint8)\n   X_binary = np.packbits(embeddings > 0.0, axis=1)\n   labels_binary = EVoC().fit_predict(X_binary)\n\nNext Steps\n----------\n\n* See the :doc:`user_guide` for detailed parameter explanations\n* Refer to :doc:`api/index` for complete API documentation\n"
  },
  {
    "path": "doc/source/user_guide.rst",
    "content": "User Guide\n==========\n\nThis end-user oriented guide covers EVoC's features, parameters, and best practices for different use cases. To better \nunderstand the parameters that are available, it help help to bgin with an overview of the algorithm and its key\ncomponents.\n\nAlgorithm Overview\n------------------\n\nEVoC (Embedding Vector Oriented Clustering) combines two key techniques:\n\n1. **Graph Embedding**: Constructs a k-nearest neighbor graph and learns a lower-dimensional embedding (similar to UMAP)\n2. **Density Clustering**: Applies hierarchical density-based clustering to the embedding (similar to HDBSCAN and PLSCAN)\n\nThe advantage of EVoC is that it can optimize every part of these tasks for the specific task of clustering high-dimensional \nembedding vectors, providing both improved **performance** and **quality** compared to general-purpose clustering algorithms.\nThat is to say, EVoC not only runs much faster than a combination of UMAP and HDBSCAN, but also produces better clusters as\na result.\n\nThe combination of dimension reduction/manifold learning and density clustering tailored to embedding vectors provides several \nadvantages for clustering embedding vectors:\n\n* Efficient processing of dense, high-dimensional data\n* Multiple clustering granularities through hierarchical layers\n* Robust handling of noise and outliers\n* Optimized distance metrics for different embedding types\n\nParameter Reference\n-------------------\n\nWith that core idea -- a two part algorithm -- in mind, let's explore the key parameters that control EVoC's behavior. \nThe parameters can be broadly categorized into three groups:\n\nCore Parameters\n~~~~~~~~~~~~~~~\n\nThese are the main parameters that most users will want to adjust based on their specific dataset and clustering goals:\n\n**base_min_cluster_size** : int, default=5  \n   Minimum number of points required to form a cluster at the base (finest) granularity level. \n   Larger values produce fewer, more stable clusters.\n\n**n_neighbors** : int, default=15\n   Number of neighbors used in k-NN graph construction. More neighbors capture more global structure \n   but increase computational cost.\n\n**min_samples** : int, default=5\n   Minimum samples for density estimation in the final clustering step. Should typically match \n   or be smaller than base_min_cluster_size.\n\nClustering Control\n~~~~~~~~~~~~~~~~~~\n\nThese parameters control the clustering behavior and granularity:\n\n**base_n_clusters** : int, optional\n   Target number of clusters for the base layer. When specified, EVoC will search for the clustering \n   granularity that produces approximately this many clusters, then build additional layers on top.\n\n**approx_n_clusters** : int, optional  \n   Target number of clusters for the final output. When specified, EVoC returns only a single \n   clustering layer (no hierarchy) with approximately this many clusters.\n\n**max_layers** : int, default=10\n   Maximum number of hierarchical clustering layers to generate. More layers provide finer control \n   over clustering granularity but increase computation time.\n\n**min_similarity_threshold** : float, default=0.2\n   Minimum Jaccard similarity threshold for layer selection. Prevents nearly identical clustering \n   layers in the hierarchy.\n\nAdvanced Parameters  \n~~~~~~~~~~~~~~~~~~~\n\nThese parameters provide more fine-grained control over the algorithm and are typically only adjusted by advanced users:\n\n**noise_level** : float, default=0.5\n   Controls the noise threshold for cluster membership. Higher values produce more noise points \n   and fewer clusters, while lower values produce more clusters and fewer noise points. In practice\n   this only provides fine-tuning over the amount of noise, and is not as important as \n   base_min_cluster_size and min_samples.\n\n**node_embedding_dim** : int, optional\n   Dimensionality of the intermediate node embedding. If None, defaults to min(max(n_neighbors // 4, 4), 15).\n   Higher dimensions can capture more complex structure but increase computation.\n\n**neighbor_scale** : float, default=1.0\n   Scales the effective number of neighbors (neighbor_scale × n_neighbors). Values > 1.0 create \n   denser graphs, values < 1.0 create sparser graphs focused on local structure.\n\n**n_epochs** : int, default=50\n   Number of optimization epochs for the node embedding. More epochs improve embedding quality \n   but increase computation time.\n\n**node_embedding_init** : {'label_prop', None}, default='label_prop'\n   Initialization method for the node embedding. 'label_prop' uses label propagation for initialization, \n   None uses random initialization.\n\n**n_label_prop_iter** : int, default=20\n   Number of label propagation iterations when using 'label_prop' initialization.\n\n**symmetrize_graph** : bool, default=True\n   Whether to make the k-NN graph symmetric. Recommended for most use cases.\n\n**random_state** : int, optional\n   Random seed for reproducible results. When specified, enables deterministic mode.\n\nBest Practices\n--------------\n\nAs a general rule EVoC is desgined to largely be as parameter-free as possible. The default parameters \nshould work well for a wide range of datasets and use cases, and most users will not need to adjust them.\nSo the best place to start is just running with default parameters and then adjusting based on the results. \nHowever, here are some best practices for different scenarios:\n\nWorking with Hierarchical Output\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nEVoC provides multiple clustering layers with different granularities:\n\n.. code-block:: python\n\n   clusterer = EVoC(max_layers=5)\n   clusterer.fit(X)\n\n   # Explore different granularities\n   for i, layer in enumerate(clusterer.cluster_layers_):\n       n_clusters = len(np.unique(layer[layer >= 0]))\n       n_noise = np.sum(layer == -1)\n       persistence = clusterer.persistence_scores_[i]\n\n       print(f\"Layer {i}: {n_clusters} clusters, {n_noise} noise points, \"\n             f\"persistence: {persistence:.3f}\")\n\n   # Use cluster tree for hierarchical analysis\n   tree = clusterer.cluster_tree_\n   # ... analyze hierarchical structure ...\n\nThe layer 0 is always the most fine-grained layer as determined by ``base_min_cluster_size`` or ``base_n_clusters``. \nEach subsequent layer provides a coarser clustering, with fewer clusters. In general the most fine-grained layers\nwill have the most noise points, and the coarser layers will have fewer noise points. The persistence score\nprovides a measure of how stable each layer is across different parameter settings, with higher scores indicating more robust clusters.\n\nIf you are interested in getting very fine-grained clusters it is worth setting ``base_min_cluster_size`` or ``base_n_clusters`` \nexplicitly to ensure you get clustering at that granularity. You can then inspect the other layers to see if the other natural\ngranularities align with your use case. If you are only interested in a single clustering, you can set ``approx_n_clusters`` \nto get the layer that is closest to that number of clusters.\n\nYou can also make use of the tree structure to analyze how clusters evolve across layers, and to identify stable clusters \nthat persist across multiple layers. Alternatively you can use the tree structure to create a \"mixed\" resolution layer by selecting\nclusters at a given layer, and then also selecting any clusters in lower layers that are no children of any of your selected clusters. \nThis allows you to get a more fine-grained clustering in some parts of the data, while keeping a coarser clustering i\nn other parts of the data.\n\nPerformance Optimization\n~~~~~~~~~~~~~~~~~~~~~~~~\n\nDepending on your needs you may be willing to trade off some accuracy for speed, or vice versa. \nThe default EVoC parameters are designed primarily for exploratory clustering, and thus produce clusters very quickly.\nIf you are looking for a more robust higher quality clustering, it can be worth tweaking the parameters to spend\nmore time to produce a better clustering result. For example, for a medium sized dataset (e.g. 10k-100k points) \nyou can increase the number of epochs and neighbors to get a better embedding, which will lead to better clusters.\nIn such cases you will also likely want to fix a random seed to ensure reproducibility, as the optimization process is stochastic.\n\n.. code-block:: python\n\n   clusterer = EVoC(\n       n_epochs=150,           # More epochs for better embedding  \n       random_state=42        # Enable optimizations\n   )\n\nFor larger datasets, you may want to reduce the number of neighbors and epochs to get a faster result, at the cost of some cluster quality.\nIn that case not setting a random seed can actually improve performance, as it allows the algorithm to skip some of the overhead \nof ensuring reproducibility.\n\n.. code-block:: python\n\n   clusterer = EVoC(\n       n_neighbors=10,         # Balance between quality and speed\n       n_epochs=30,           # Fewer epochs for faster embedding  \n       max_layers=3,          # Limit hierarchy depth\n   )\n\n\nTroubleshooting\n---------------\n\n**Problem**: Too many small clusters\n   **Solution**: Increase base_min_cluster_size or noise_level\n\n**Problem**: Most points classified as noise  \n   **Solution**: Decrease noise_level or reduce min_samples\n\n**Problem**: Clustering too slow\n   **Solution**: Reduce n_neighbors, n_epochs, or max_layers\n\n**Problem**: Poor cluster quality\n   **Solution**: Increase n_neighbors, n_epochs, or try different node_embedding_init\n\n**Problem**: Inconsistent results\n   **Solution**: Set random_state for reproducible results\n\n"
  },
  {
    "path": "evoc/__init__.py",
    "content": "from .clustering import evoc_clusters, EVoC\n"
  },
  {
    "path": "evoc/boruvka.py",
    "content": "import numba\nimport numpy as np\n\nfrom .disjoint_set import RankDisjointSetType, ds_rank_create, ds_find, ds_union_by_rank\nfrom .numba_kdtree import (\n    NumbaKDTreeType,\n    parallel_tree_query,\n    rdist,\n    point_to_node_lower_bound_rdist,\n    NumbaKDTree,\n)\n\n\n@numba.njit(\n    numba.float32[:, ::1](\n        RankDisjointSetType,\n        numba.int32[::1],\n        numba.types.Array(numba.float32, 1, \"A\"),\n        numba.int64[::1],\n    ),\n    locals={\"i\": numba.types.int64},\n    cache=True,\n)\ndef merge_components(\n    disjoint_set, candidate_neighbors, candidate_neighbor_distances, point_components\n):\n    component_edges = {\n        np.int64(0): (np.int64(0), np.int64(1), np.float32(0.0)) for i in range(0)\n    }\n\n    # Find the best edges from each component\n    for i in range(candidate_neighbors.shape[0]):\n        from_component = np.int64(point_components[i])\n        if from_component in component_edges:\n            if candidate_neighbor_distances[i] < component_edges[from_component][2]:\n                component_edges[from_component] = (\n                    numba.int64(i),\n                    numba.int64(candidate_neighbors[i]),\n                    numba.float32(candidate_neighbor_distances[i]),\n                )\n        else:\n            component_edges[from_component] = (\n                numba.int64(i),\n                numba.int64(candidate_neighbors[i]),\n                numba.float32(candidate_neighbor_distances[i]),\n            )\n\n    result = np.empty((len(component_edges), 3), dtype=np.float32)\n    result_idx = 0\n\n    # Add the best edges to the edge set and merge the relevant components\n    for edge in component_edges.values():\n        from_component = ds_find(disjoint_set, numba.int32(edge[0]))\n        to_component = ds_find(disjoint_set, numba.int32(edge[1]))\n        if from_component != to_component:\n            result[result_idx] = (\n                numba.float32(edge[0]),\n                numba.float32(edge[1]),\n                numba.float32(edge[2]),\n            )\n            result_idx += 1\n\n            ds_union_by_rank(disjoint_set, from_component, to_component)\n\n    return result[:result_idx]\n\n\n@numba.njit(\n    numba.void(\n        NumbaKDTreeType,\n        RankDisjointSetType,\n        numba.int64[::1],\n        numba.int64[::1],\n    ),\n    locals={\n        \"i\": numba.types.int32,\n        \"j\": numba.types.int32,\n        \"idx\": numba.types.int32,\n        \"left\": numba.types.int32,\n        \"right\": numba.types.int32,\n        \"candidate_component\": numba.types.int32,\n    },\n    parallel=True,\n    cache=True,\n    fastmath=True,\n)\ndef update_component_vectors(tree, disjoint_set, node_components, point_components):\n    for i in numba.prange(point_components.shape[0]):\n        point_components[i] = ds_find(disjoint_set, np.int32(i))\n\n    for i in range(tree.idx_start.shape[0] - 1, -1, -1):\n        # Access node information from the separate arrays\n        is_leaf = tree.is_leaf[i]\n        idx_start = tree.idx_start[i]\n        idx_end = tree.idx_end[i]\n\n        # Case 1:\n        #    If the node is a leaf we need to check that every point\n        #    in the node is of the same component\n        if is_leaf:\n            candidate_component = point_components[tree.idx_array[idx_start]]\n            for j in range(idx_start + 1, idx_end):\n                idx = tree.idx_array[j]\n                if point_components[idx] != candidate_component:\n                    break\n            else:\n                node_components[i] = candidate_component\n\n        # Case 2:\n        #    If the node is not a leaf we only need to check\n        #    that both child nodes are in the same component\n        else:\n            left = 2 * i + 1\n            right = left + 1\n\n            if node_components[left] == node_components[right]:\n                node_components[i] = node_components[left]\n\n\n@numba.njit(\n    numba.void(\n        NumbaKDTreeType,\n        numba.int32,\n        numba.float32[::1],\n        numba.float32[::1],\n        numba.int32[::1],\n        numba.float32,\n        numba.types.Array(numba.float32, 1, \"A\"),\n        numba.int64,\n        numba.int64[::1],\n        numba.int64[::1],\n        numba.float32,\n        numba.float32[::1],\n    ),\n    locals={\n        \"i\": numba.types.int32,\n        \"idx\": numba.types.int32,\n        \"left\": numba.types.int32,\n        \"right\": numba.types.int32,\n        \"d\": numba.types.float32,\n        \"dist_lower_bound_left\": numba.types.float32,\n        \"dist_lower_bound_right\": numba.types.float32,\n    },\n    cache=True,\n    fastmath=True,\n)\ndef component_aware_query_recursion(\n    tree,\n    node,\n    point,\n    heap_p,\n    heap_i,\n    current_core_distance,\n    core_distances,\n    current_component,\n    node_components,\n    point_components,\n    dist_lower_bound,\n    component_nearest_neighbor_dist,\n):\n    # Access node information from the separate arrays\n    is_leaf = tree.is_leaf[node]\n    idx_start = tree.idx_start[node]\n    idx_end = tree.idx_end[node]\n\n    # ------------------------------------------------------------\n    # Case 1a: query point is outside node radius:\n    #         trim it from the query\n    if dist_lower_bound > heap_p[0]:\n        return\n\n    # ------------------------------------------------------------\n    # Case 1b: we can't improve on the best distance for this component\n    #         trim it from the query\n    elif (\n        dist_lower_bound > component_nearest_neighbor_dist[0]\n        or current_core_distance > component_nearest_neighbor_dist[0]\n    ):\n        return\n\n    # ------------------------------------------------------------\n    # Case 1c: node contains only points in same component as query\n    #         trim it from the query\n    elif node_components[node] == current_component:\n        return\n\n    # ------------------------------------------------------------\n    # Case 2: this is a leaf node.  Update set of nearby points\n    elif is_leaf:\n        for i in range(idx_start, idx_end):\n            idx = tree.idx_array[i]\n            if (\n                point_components[idx] != current_component\n                and core_distances[idx] < component_nearest_neighbor_dist[0]\n            ):\n                d = max(\n                    rdist(point, tree.data[idx]),\n                    current_core_distance,\n                    core_distances[idx],\n                )\n                if d < heap_p[0]:\n                    heap_p[0] = d\n                    heap_i[0] = idx\n                    if d < component_nearest_neighbor_dist[0]:\n                        component_nearest_neighbor_dist[0] = d\n\n    # ------------------------------------------------------------\n    # Case 3: Node is not a leaf.  Recursively query subnodes\n    #         starting with the closest\n    else:\n        left = numba.int32(2 * node + 1)\n        right = numba.int32(left + 1)\n        dist_lower_bound_left = point_to_node_lower_bound_rdist(\n            tree.node_bounds[0, left], tree.node_bounds[1, left], point\n        )\n        dist_lower_bound_right = point_to_node_lower_bound_rdist(\n            tree.node_bounds[0, right], tree.node_bounds[1, right], point\n        )\n\n        # recursively query subnodes\n        if dist_lower_bound_left <= dist_lower_bound_right:\n            component_aware_query_recursion(\n                tree,\n                left,\n                point,\n                heap_p,\n                heap_i,\n                current_core_distance,\n                core_distances,\n                current_component,\n                node_components,\n                point_components,\n                dist_lower_bound_left,\n                component_nearest_neighbor_dist,\n            )\n            component_aware_query_recursion(\n                tree,\n                right,\n                point,\n                heap_p,\n                heap_i,\n                current_core_distance,\n                core_distances,\n                current_component,\n                node_components,\n                point_components,\n                dist_lower_bound_right,\n                component_nearest_neighbor_dist,\n            )\n        else:\n            component_aware_query_recursion(\n                tree,\n                right,\n                point,\n                heap_p,\n                heap_i,\n                current_core_distance,\n                core_distances,\n                current_component,\n                node_components,\n                point_components,\n                dist_lower_bound_right,\n                component_nearest_neighbor_dist,\n            )\n            component_aware_query_recursion(\n                tree,\n                left,\n                point,\n                heap_p,\n                heap_i,\n                current_core_distance,\n                core_distances,\n                current_component,\n                node_components,\n                point_components,\n                dist_lower_bound_left,\n                component_nearest_neighbor_dist,\n            )\n\n    return\n\n\n@numba.njit(\n    numba.types.Tuple((numba.float32[::1], numba.int32[::1]))(\n        NumbaKDTreeType,\n        numba.int64[::1],\n        numba.int64[::1],\n        numba.types.Array(numba.float32, 1, \"A\"),\n    ),\n    locals={\n        \"i\": numba.types.int32,\n        \"distance_lower_bound\": numba.types.float32,\n        \"current_component\": numba.types.int32,\n    },\n    parallel=True,\n    cache=True,\n    fastmath=True,\n)\ndef boruvka_tree_query(tree, node_components, point_components, core_distances):\n    candidate_distances = np.full(tree.data.shape[0], np.inf, dtype=np.float32)\n    candidate_indices = np.full(tree.data.shape[0], -1, dtype=np.int32)\n    component_nearest_neighbor_dist = np.full(\n        tree.data.shape[0], np.inf, dtype=np.float32\n    )\n\n    data = tree.data.astype(np.float32)\n\n    for i in numba.prange(tree.data.shape[0]):\n        distance_lower_bound = point_to_node_lower_bound_rdist(\n            tree.node_bounds[0, 0], tree.node_bounds[1, 0], tree.data[i]\n        )\n        heap_p, heap_i = candidate_distances[i : i + 1], candidate_indices[i : i + 1]\n        component_aware_query_recursion(\n            tree,\n            numba.int32(0),\n            data[i],\n            heap_p,\n            heap_i,\n            core_distances[i],\n            core_distances,\n            point_components[i],\n            node_components,\n            point_components,\n            distance_lower_bound,\n            component_nearest_neighbor_dist[\n                point_components[i] : point_components[i] + 1\n            ],\n        )\n\n    return candidate_distances, candidate_indices\n\n\n@numba.njit(inline=\"always\", cache=True)\ndef calculate_block_size(n_components, n_points, num_threads):\n    \"\"\"Calculate adaptive block size based on component sizes.\"\"\"\n    if n_components == 0:\n        points_per_component = n_points\n    else:\n        points_per_component = n_points / n_components\n\n    if points_per_component < 10:\n        block_size = num_threads * 512  # Weak pruning, large blocks\n    elif points_per_component < 100:\n        block_size = num_threads * 128  # Moderate pruning\n    elif points_per_component < 1000:\n        block_size = num_threads * 32  # Good pruning\n    else:\n        block_size = num_threads * 8  # Excellent pruning, small blocks\n\n    # Ensure reasonable bounds\n    block_size = max(num_threads, min(block_size, n_points // 4 + 1))\n    return int(block_size)\n\n\n@numba.njit(\n    [\n        \"void(float32[:], float32[:], int32[:], int32, int32)\",\n        \"void(float64[:], float64[:], int64[:], int64, int64)\",\n    ],\n    locals={\n        \"i\": numba.types.int32,\n        \"component\": numba.types.int32,\n        \"block_bound\": numba.types.float32,\n    },\n    cache=True,\n    fastmath=True,\n    inline=\"always\",\n)\ndef update_component_bounds_from_block(\n    component_nearest_neighbor_dist,\n    block_component_bounds,\n    point_components,\n    block_start,\n    block_end,\n):\n    \"\"\"Update global component bounds from block results.\"\"\"\n    for i in range(block_start, block_end):\n        component = point_components[i]\n        block_bound = block_component_bounds[i - block_start]\n        if block_bound < component_nearest_neighbor_dist[component]:\n            component_nearest_neighbor_dist[component] = block_bound\n\n\n@numba.njit(\n    numba.types.Tuple((numba.float32[::1], numba.int32[::1]))(\n        NumbaKDTreeType,\n        numba.int64[::1],\n        numba.int64[::1],\n        numba.types.Array(numba.float32, 1, \"A\"),\n        numba.int64,\n    ),\n    locals={\n        \"block_start\": numba.types.int32,\n        \"block_end\": numba.types.int32,\n        \"block_size_actual\": numba.types.int32,\n        \"i\": numba.types.int32,\n        \"distance_lower_bound\": numba.types.float32,\n        \"current_component\": numba.types.int32,\n    },\n    parallel=True,\n    cache=True,\n    fastmath=True,\n)\ndef boruvka_tree_query_reproducible(\n    tree, node_components, point_components, core_distances, block_size\n):\n    \"\"\"Reproducible version using block-based processing to avoid race conditions.\"\"\"\n    candidate_distances = np.full(tree.data.shape[0], np.inf, dtype=np.float32)\n    candidate_indices = np.full(tree.data.shape[0], -1, dtype=np.int32)\n    component_nearest_neighbor_dist = np.full(\n        tree.data.shape[0], np.inf, dtype=np.float32\n    )\n\n    data = tree.data.astype(np.float32)\n\n    # Reusable buffer for block component bounds (allocate once, reuse)\n    max_block_component_bounds = np.full(block_size, np.inf, dtype=np.float32)\n\n    # Process points in blocks\n    for block_start in range(0, tree.data.shape[0], block_size):\n        block_end = min(block_start + block_size, tree.data.shape[0])\n        block_size_actual = block_end - block_start\n\n        # Reset only the portion we'll use (more cache-friendly)\n        max_block_component_bounds[:block_size_actual] = np.inf\n\n        # Parallel processing within the block\n        for i in numba.prange(block_start, block_end):\n            distance_lower_bound = point_to_node_lower_bound_rdist(\n                tree.node_bounds[0, 0], tree.node_bounds[1, 0], tree.data[i]\n            )\n            heap_p, heap_i = (\n                candidate_distances[i : i + 1],\n                candidate_indices[i : i + 1],\n            )\n\n            # Use current global bounds for this component\n            current_component = point_components[i]\n            local_component_bound = component_nearest_neighbor_dist[\n                current_component : current_component + 1\n            ]\n\n            component_aware_query_recursion(\n                tree,\n                numba.int32(0),\n                data[i],\n                heap_p,\n                heap_i,\n                core_distances[i],\n                core_distances,\n                point_components[i],\n                node_components,\n                point_components,\n                distance_lower_bound,\n                local_component_bound,\n            )\n\n            # Store the potentially updated bound for this point\n            max_block_component_bounds[i - block_start] = local_component_bound[0]\n\n        # Sequential update of global component bounds after the block\n        update_component_bounds_from_block(\n            component_nearest_neighbor_dist,\n            max_block_component_bounds,\n            point_components,\n            block_start,\n            block_end,\n        )\n\n    return candidate_distances, candidate_indices\n\n\n@numba.njit(\n    locals={\n        \"i\": numba.types.int32,\n        \"j\": numba.types.int32,\n        \"k\": numba.types.int32,\n        \"result_idx\": numba.types.int32,\n        \"from_component\": numba.types.int32,\n        \"to_component\": numba.types.int32,\n    },\n    parallel=True,\n    cache=True,\n)\ndef initialize_boruvka_from_knn(\n    knn_indices, knn_distances, core_distances, disjoint_set\n):\n    # component_edges = {0:(np.int32(0), np.int32(1), np.float32(0.0)) for i in range(0)}\n    component_edges = np.full((knn_indices.shape[0], 3), -1, dtype=np.float64)\n\n    for i in numba.prange(knn_indices.shape[0]):\n        for j in range(1, knn_indices.shape[1]):\n            k = np.int32(knn_indices[i, j])\n            if core_distances[i] >= core_distances[k]:\n                # Use max of core distance and actual distance as edge weight\n                edge_weight = max(core_distances[i], knn_distances[i, j])\n                component_edges[i] = (\n                    np.float64(i),\n                    np.float64(k),\n                    np.float64(edge_weight),\n                )\n                break\n\n    result = np.empty((len(component_edges), 3), dtype=np.float64)\n    result_idx = 0\n\n    # Add the best edges to the edge set and merge the relevant components\n    for edge in component_edges:\n        if edge[0] < 0:\n            continue\n        from_component = ds_find(disjoint_set, np.int32(edge[0]))\n        to_component = ds_find(disjoint_set, np.int32(edge[1]))\n        if from_component != to_component:\n            result[result_idx] = (\n                np.float64(edge[0]),\n                np.float64(edge[1]),\n                np.float64(edge[2]),\n            )\n            result_idx += 1\n\n            ds_union_by_rank(disjoint_set, from_component, to_component)\n\n    return result[:result_idx].astype(np.float32)\n\n\n@numba.njit(\n    numba.float32[:, ::1](\n        NumbaKDTreeType,\n        numba.int64,\n        numba.int64,\n        numba.types.boolean,\n    ),\n    cache=True,\n)\ndef parallel_boruvka(tree, n_threads, min_samples=10, reproducible=False):\n    components_disjoint_set = ds_rank_create(tree.data.shape[0])\n    point_components = np.arange(tree.data.shape[0])\n    node_components = np.full(tree.idx_start.shape[0], -1)\n    n_components = point_components.shape[0]\n\n    if min_samples > 1:\n        distances, neighbors = parallel_tree_query(\n            tree, tree.data, k=numba.int64(min_samples + 1), output_rdist=True\n        )\n        core_distances = distances.T[-1]\n        initial_edges = initialize_boruvka_from_knn(\n            neighbors, distances, core_distances, components_disjoint_set\n        )\n        update_component_vectors(\n            tree, components_disjoint_set, node_components, point_components\n        )\n    else:\n        core_distances = np.zeros(tree.data.shape[0], dtype=np.float32)\n        distances, neighbors = parallel_tree_query(\n            tree, tree.data, k=numba.int64(2), output_rdist=True\n        )\n        initial_edges = initialize_boruvka_from_knn(\n            neighbors, distances, core_distances, components_disjoint_set\n        )\n        update_component_vectors(\n            tree, components_disjoint_set, node_components, point_components\n        )\n\n    # Count initial components after initialization\n    n_components = len(np.unique(point_components))\n\n    # Use list to accumulate edges, then convert at end (more efficient than vstack)\n    # all_edges = [initial_edges]\n    # all_edges = initial_edges\n    max_edges = tree.data.shape[0] - 1\n    all_edges = np.empty((max_edges, 3), dtype=np.float32)\n    n_edges = numba.int64(len(initial_edges))\n    all_edges[:n_edges] = initial_edges\n\n    while n_components > 1:\n        if reproducible:\n            # Calculate adaptive block size based on current component sizes\n            block_size = calculate_block_size(\n                n_components, tree.data.shape[0], n_threads\n            )\n            candidate_distances, candidate_indices = boruvka_tree_query_reproducible(\n                tree, node_components, point_components, core_distances, block_size\n            )\n        else:\n            candidate_distances, candidate_indices = boruvka_tree_query(\n                tree, node_components, point_components, core_distances\n            )\n\n        new_edges = merge_components(\n            components_disjoint_set,\n            candidate_indices,\n            candidate_distances,\n            point_components,\n        )\n\n        # Update component count more efficiently - subtract merged components\n        n_components -= len(new_edges)\n\n        update_component_vectors(\n            tree, components_disjoint_set, node_components, point_components\n        )\n\n        if len(new_edges) > 0:\n            # # all_edges.append(new_edges)\n            # all_edges = np.vstack((all_edges, new_edges)).astype(np.float32)\n            all_edges[n_edges : n_edges + len(new_edges)] = new_edges\n            n_edges += numba.int64(len(new_edges))\n\n    all_edges[:, 2] = np.sqrt(all_edges.T[2])\n    return all_edges\n"
  },
  {
    "path": "evoc/cluster_trees.py",
    "content": "import numba\nimport numpy as np\n\nfrom collections import namedtuple\n\nfrom .disjoint_set import ds_rank_create, ds_find, ds_union_by_rank\n\nLinkageMergeData = namedtuple(\"LinkageMergeData\", [\"parent\", \"size\", \"next\"])\n\n\n@numba.njit(cache=True)\ndef create_linkage_merge_data(base_size):\n    parent = np.full(2 * base_size - 1, -1, dtype=np.intp)\n    size = np.concatenate(\n        (np.ones(base_size, dtype=np.intp), np.zeros(base_size - 1, dtype=np.intp))\n    )\n    next_parent = np.array([base_size], dtype=np.intp)\n\n    return LinkageMergeData(parent, size, next_parent)\n\n\n@numba.njit(cache=True)\ndef linkage_merge_find(linkage_merge, node):\n    relabel = node\n    while linkage_merge.parent[node] != -1 and linkage_merge.parent[node] != node:\n        node = linkage_merge.parent[node]\n\n    linkage_merge.parent[node] = node\n\n    # label up to the root\n    while linkage_merge.parent[relabel] != node:\n        next_relabel = linkage_merge.parent[relabel]\n        linkage_merge.parent[relabel] = node\n        relabel = next_relabel\n\n    return node\n\n\n@numba.njit(cache=True)\ndef linkage_merge_join(linkage_merge, left, right):\n    linkage_merge.size[linkage_merge.next[0]] = (\n        linkage_merge.size[left] + linkage_merge.size[right]\n    )\n    linkage_merge.parent[left] = linkage_merge.next[0]\n    linkage_merge.parent[right] = linkage_merge.next[0]\n    linkage_merge.next[0] += 1\n\n\n@numba.njit(cache=True)\ndef mst_to_linkage_tree(sorted_mst):\n    result = np.empty((sorted_mst.shape[0], sorted_mst.shape[1] + 1))\n\n    n_samples = sorted_mst.shape[0] + 1\n    linkage_merge = create_linkage_merge_data(n_samples)\n\n    for index in range(sorted_mst.shape[0]):\n\n        left = np.intp(sorted_mst[index, 0])\n        right = np.intp(sorted_mst[index, 1])\n        delta = sorted_mst[index, 2]\n\n        left_component = linkage_merge_find(linkage_merge, left)\n        right_component = linkage_merge_find(linkage_merge, right)\n\n        if left_component > right_component:\n            result[index][0] = left_component\n            result[index][1] = right_component\n        else:\n            result[index][1] = left_component\n            result[index][0] = right_component\n\n        result[index][2] = delta\n        result[index][3] = (\n            linkage_merge.size[left_component] + linkage_merge.size[right_component]\n        )\n\n        linkage_merge_join(linkage_merge, left_component, right_component)\n\n    return result\n\n\n@numba.njit(cache=True)\ndef bfs_from_hierarchy(hierarchy, bfs_root, num_points):\n    to_process = [bfs_root]\n    result = []\n\n    while to_process:\n        result.extend(to_process)\n        next_to_process = []\n        for n in to_process:\n            if n >= num_points:\n                i = n - num_points\n                next_to_process.append(int(hierarchy[i, 0]))\n                next_to_process.append(int(hierarchy[i, 1]))\n        to_process = next_to_process\n\n    return result\n\n\n@numba.njit(cache=True)\ndef eliminate_branch(\n    branch_node,\n    parent_node,\n    lambda_value,\n    parents,\n    children,\n    lambdas,\n    sizes,\n    idx,\n    ignore,\n    hierarchy,\n    num_points,\n):\n    if branch_node < num_points:\n        parents[idx] = parent_node\n        children[idx] = branch_node\n        lambdas[idx] = lambda_value\n        idx += 1\n    else:\n        for sub_node in bfs_from_hierarchy(hierarchy, branch_node, num_points):\n            if sub_node < num_points:\n                children[idx] = sub_node\n                parents[idx] = parent_node\n                lambdas[idx] = lambda_value\n                idx += 1\n            else:\n                ignore[sub_node] = True\n\n    return idx\n\n\nCondensedTree = namedtuple(\n    \"CondensedTree\", [\"parent\", \"child\", \"lambda_val\", \"child_size\"]\n)\n\n\n@numba.njit(fastmath=True, cache=True)\ndef condense_tree(hierarchy, min_cluster_size=10):\n    root = 2 * hierarchy.shape[0]\n    num_points = hierarchy.shape[0] + 1\n    next_label = num_points + 1\n\n    node_list = bfs_from_hierarchy(hierarchy, root, num_points)\n\n    relabel = np.zeros(root + 1, dtype=np.int64)\n    relabel[root] = num_points\n\n    parents = np.ones(root, dtype=np.int64)\n    children = np.empty(root, dtype=np.int64)\n    lambdas = np.empty(root, dtype=np.float32)\n    sizes = np.ones(root, dtype=np.int64)\n    ignore = np.zeros(root + 1, dtype=np.bool_)\n\n    idx = 0\n\n    for node in node_list:\n        if ignore[node] or node < num_points:\n            continue\n\n        parent_node = relabel[node]\n        l, r, d, _ = hierarchy[node - num_points]\n        left = np.int64(l)\n        right = np.int64(r)\n        if d > 0.0:\n            lambda_value = 1.0 / d\n        else:\n            lambda_value = np.inf\n\n        left_count = (\n            np.int64(hierarchy[left - num_points, 3]) if left >= num_points else 1\n        )\n        right_count = (\n            np.int64(hierarchy[right - num_points, 3]) if right >= num_points else 1\n        )\n\n        # The logic here is in a strange order, but it has non-trivial performance gains ...\n        # The most common case by far is a singleton on the left; and cluster on the right take care of this separately\n        if left < num_points and right_count >= min_cluster_size:\n            relabel[right] = parent_node\n            parents[idx] = parent_node\n            children[idx] = left\n            lambdas[idx] = lambda_value\n            idx += 1\n        # Next most common is a small left cluster and a large right cluster: relabel the right node; eliminate the left branch\n        elif left_count < min_cluster_size and right_count >= min_cluster_size:\n            relabel[right] = parent_node\n            idx = eliminate_branch(\n                left,\n                parent_node,\n                lambda_value,\n                parents,\n                children,\n                lambdas,\n                sizes,\n                idx,\n                ignore,\n                hierarchy,\n                num_points,\n            )\n        # Then we have a large left cluster and a small right cluster: relabel the left node; elimiate the right branch\n        elif left_count >= min_cluster_size and right_count < min_cluster_size:\n            relabel[left] = parent_node\n            idx = eliminate_branch(\n                right,\n                parent_node,\n                lambda_value,\n                parents,\n                children,\n                lambdas,\n                sizes,\n                idx,\n                ignore,\n                hierarchy,\n                num_points,\n            )\n        # If both clusters are small then eliminate all branches\n        elif left_count < min_cluster_size and right_count < min_cluster_size:\n            idx = eliminate_branch(\n                left,\n                parent_node,\n                lambda_value,\n                parents,\n                children,\n                lambdas,\n                sizes,\n                idx,\n                ignore,\n                hierarchy,\n                num_points,\n            )\n            idx = eliminate_branch(\n                right,\n                parent_node,\n                lambda_value,\n                parents,\n                children,\n                lambdas,\n                sizes,\n                idx,\n                ignore,\n                hierarchy,\n                num_points,\n            )\n        # and finally if we actually have a legitimate cluster split, handle that correctly\n        else:\n            relabel[left] = next_label\n\n            parents[idx] = parent_node\n            children[idx] = next_label\n            lambdas[idx] = lambda_value\n            sizes[idx] = left_count\n            next_label += 1\n            idx += 1\n\n            relabel[right] = next_label\n\n            parents[idx] = parent_node\n            children[idx] = next_label\n            lambdas[idx] = lambda_value\n            sizes[idx] = right_count\n            next_label += 1\n            idx += 1\n\n    return CondensedTree(parents[:idx], children[:idx], lambdas[:idx], sizes[:idx])\n\n\n@numba.njit(cache=True)\ndef extract_leaves(condensed_tree, allow_single_cluster=True):\n    # Handle empty tree case gracefully\n    if len(condensed_tree.parent) == 0:\n        return np.zeros(0, dtype=np.intp)\n    \n    n_nodes = condensed_tree.parent.max() + 1\n    n_points = condensed_tree.parent.min()\n    leaf_indicator = np.ones(n_nodes, dtype=np.bool_)\n    leaf_indicator[:n_points] = False\n\n    for parent, child_size in zip(condensed_tree.parent, condensed_tree.child_size):\n        if child_size > 1:\n            leaf_indicator[parent] = False\n\n    return np.nonzero(leaf_indicator)[0]\n\n\n@numba.njit(cache=True, fastmath=True)\ndef score_condensed_tree_nodes(condensed_tree):\n    result = {0: 0.0 for i in range(0)}\n\n    for i in range(condensed_tree.parent.shape[0]):\n        parent = condensed_tree.parent[i]\n        if parent in result:\n            result[parent] += (\n                condensed_tree.lambda_val[i] * condensed_tree.child_size[i]\n            )\n        else:\n            result[parent] = condensed_tree.lambda_val[i] * condensed_tree.child_size[i]\n\n        if condensed_tree.child_size[i] > 1:\n            child = condensed_tree.child[i]\n            if child in result:\n                result[child] -= (\n                    condensed_tree.lambda_val[i] * condensed_tree.child_size[i]\n                )\n            else:\n                result[child] = (\n                    -condensed_tree.lambda_val[i] * condensed_tree.child_size[i]\n                )\n\n    return result\n\n\n@numba.njit(cache=True)\ndef cluster_tree_from_condensed_tree(condensed_tree):\n    mask = condensed_tree.child_size > 1\n    return CondensedTree(\n        condensed_tree.parent[mask],\n        condensed_tree.child[mask],\n        condensed_tree.lambda_val[mask],\n        condensed_tree.child_size[mask],\n    )\n\n@numba.njit(cache=True)\ndef mask_condensed_tree(condensed_tree, mask):\n    return CondensedTree(\n        condensed_tree.parent[mask], \n        condensed_tree.child[mask], \n        condensed_tree.lambda_val[mask],\n        condensed_tree.child_size[mask]\n    )\n\n@numba.njit(cache=True)\ndef unselect_below_node(node, cluster_tree, selected_clusters):\n    for child in cluster_tree.child[cluster_tree.parent == node]:\n        unselect_below_node(child, cluster_tree, selected_clusters)\n        selected_clusters[child] = False\n\n\n@numba.njit(fastmath=True, cache=True)\ndef eom_recursion(node, cluster_tree, node_scores, selected_clusters):\n    current_score = node_scores[node]\n\n    children = cluster_tree.child[cluster_tree.parent == node]\n    child_score_total = 0.0\n\n    for child_node in children:\n        child_score_total += eom_recursion(\n            child_node, cluster_tree, node_scores, selected_clusters\n        )\n\n    if child_score_total > current_score:\n        return child_score_total\n    else:\n        selected_clusters[node] = True\n        unselect_below_node(node, cluster_tree, selected_clusters)\n        return current_score\n\n\n@numba.njit(cache=True)\ndef extract_eom_clusters(condensed_tree, cluster_tree, allow_single_cluster=False):\n    node_scores = score_condensed_tree_nodes(condensed_tree)\n    selected_clusters = {node: False for node in node_scores}\n\n    if len(cluster_tree.parent) == 0:\n        return np.zeros(0, dtype=np.int64)\n\n    cluster_tree_root = cluster_tree.parent.min()\n\n    if allow_single_cluster:\n        eom_recursion(cluster_tree_root, cluster_tree, node_scores, selected_clusters)\n    elif len(node_scores) > 1:\n        root_children = cluster_tree.child[cluster_tree.parent == cluster_tree_root]\n        for child_node in root_children:\n            eom_recursion(child_node, cluster_tree, node_scores, selected_clusters)\n\n    return np.asarray(\n        [node for node, selected in selected_clusters.items() if selected]\n    )\n\n\n@numba.njit(cache=True)\ndef cluster_epsilon_search(clusters, cluster_tree, min_persistence=0.0):\n    selected = list()\n    # only way to create a typed empty set\n    processed = {np.int64(0)}\n    processed.clear()\n\n    root = cluster_tree.parent.min()\n    for cluster in clusters:\n        eps = 1 / cluster_tree.lambda_val[cluster_tree.child == cluster][0]\n        if eps < min_persistence:\n            if cluster not in processed:\n                parent = traverse_upwards(cluster_tree, min_persistence, root, cluster)\n                selected.append(parent)\n                processed |= segments_in_branch(cluster_tree, parent)\n        else:\n            selected.append(cluster)\n    return np.asarray(selected)\n\n\n@numba.njit(cache=True)\ndef traverse_upwards(cluster_tree, min_persistence, root, segment):\n    parent = cluster_tree.parent[cluster_tree.child == segment][0]\n    if parent == root:\n        return root\n    parent_eps = 1 / cluster_tree.lambda_val[cluster_tree.child == parent][0]\n    if parent_eps >= min_persistence:\n        return parent\n    else:\n        return traverse_upwards(cluster_tree, min_persistence, root, parent)\n\n\n@numba.njit(cache=True)\ndef segments_in_branch(cluster_tree, segment):\n    # only way to create a typed empty set\n    result = {np.intp(0)}\n    result.clear()\n    to_process = {segment}\n\n    while len(to_process) > 0:\n        result |= to_process\n        to_process = set(\n            cluster_tree.child[in_set_parallel(cluster_tree.parent, to_process)]\n        )\n\n    return result\n\n\n@numba.njit(parallel=True, cache=True)\ndef in_set_parallel(values, targets):\n    mask = np.empty(values.shape[0], dtype=numba.boolean)\n    for i in numba.prange(values.shape[0]):\n        mask[i] = values[i] in targets\n    return mask\n\n\n@numba.njit(parallel=True, cache=True)\ndef get_cluster_labelling_at_cut(linkage_tree, cut, min_cluster_size):\n\n    root = 2 * linkage_tree.shape[0]\n    num_points = linkage_tree.shape[0] + 1\n    result = np.empty(num_points, dtype=np.intp)\n    disjoint_set = ds_rank_create(root + 1)\n\n    cluster = num_points\n    for i in range(linkage_tree.shape[0]):\n        if linkage_tree[i, 2] < cut:\n            ds_union_by_rank(disjoint_set, np.intp(linkage_tree[i, 0]), cluster)\n            ds_union_by_rank(disjoint_set, np.intp(linkage_tree[i, 1]), cluster)\n        cluster += 1\n\n    cluster_size = np.zeros(cluster, dtype=np.intp)\n    for n in range(num_points):\n        cluster = ds_find(disjoint_set, n)\n        cluster_size[cluster] += 1\n        result[n] = cluster\n\n    cluster_label_map = {-1: -1}\n    cluster_label = 0\n    unique_labels = np.unique(result)\n\n    for cluster in unique_labels:\n        if cluster_size[cluster] < min_cluster_size:\n            cluster_label_map[cluster] = -1\n        else:\n            cluster_label_map[cluster] = cluster_label\n            cluster_label += 1\n\n    for n in numba.prange(num_points):\n        result[n] = cluster_label_map[result[n]]\n\n    return result\n\n\n@numba.njit(cache=True)\ndef get_single_cluster_label_vector(\n    tree,\n    cluster,\n    cluster_selection_epsilon,\n    n_samples,\n):\n    if len(tree.parent) == 0:\n        return np.full(n_samples, -1, dtype=np.intp)\n\n    result = np.full(n_samples, -1, dtype=np.intp)\n    max_lambda = tree.lambda_val[tree.parent == cluster].max()\n\n    for i in range(tree.child.shape[0]):\n        n = tree.child[i]\n        cur_lambda = tree.lambda_val[i]\n        if cluster_selection_epsilon > 0.0:\n            if cur_lambda >= 1 / cluster_selection_epsilon:\n                result[n] = 0\n            else:\n                result[n] = -1\n        elif cur_lambda >= max_lambda:\n            result[n] = 0\n\n    return result\n\n\n@numba.njit(cache=True)\ndef get_cluster_label_vector(\n    tree,\n    clusters,\n    cluster_selection_epsilon,\n    n_samples,\n):\n    if len(clusters) == 1:\n        return get_single_cluster_label_vector(\n            tree, clusters[0], cluster_selection_epsilon, n_samples\n        )\n\n    if len(tree.parent) == 0:\n        return np.full(n_samples, -1, dtype=np.intp)\n    root_cluster = tree.parent.min()\n    result = np.full(n_samples, -1, dtype=np.intp)\n    cluster_label_map = {c: n for n, c in enumerate(np.sort(clusters))}\n\n    disjoint_set = ds_rank_create(max(tree.parent.max() + 1, tree.child.max() + 1))\n    clusters = set(clusters)\n\n    for n in range(tree.parent.shape[0]):\n        child = tree.child[n]\n        parent = tree.parent[n]\n        if child not in clusters:\n            ds_union_by_rank(disjoint_set, parent, child)\n\n    for n in range(n_samples):\n        cluster = ds_find(disjoint_set, n)\n        if cluster <= root_cluster:\n            result[n] = -1\n        else:\n            result[n] = cluster_label_map[cluster]\n\n    return result\n\n\n@numba.njit(cache=True)\ndef max_lambdas(tree, clusters):\n    result = {c: 0.0 for c in clusters}\n\n    for n in range(tree.parent.shape[0]):\n        cluster = tree.parent[n]\n        if cluster in clusters and tree.child_size[n] == 1:\n            result[cluster] = max(result[cluster], tree.lambda_val[n])\n\n    return result\n\n\n@numba.njit(cache=True)\ndef get_point_membership_strength_vector(tree, clusters, labels):\n    result = np.zeros(labels.shape[0], dtype=np.float32)\n    deaths = max_lambdas(tree, set(clusters))\n    root_cluster = tree.parent.min()\n    cluster_index_map = {n: c for n, c in enumerate(np.sort(clusters))}\n\n    for n in range(tree.child.shape[0]):\n        point = tree.child[n]\n        if point >= root_cluster or labels[point] < 0:\n            continue\n\n        cluster = cluster_index_map[labels[point]]\n        max_lambda = deaths[cluster]\n        if max_lambda == 0.0 or not np.isfinite(tree.lambda_val[n]):\n            result[point] = 1.0\n        else:\n            lambda_val = min(tree.lambda_val[n], max_lambda)\n            result[point] = lambda_val / max_lambda\n\n    return result\n"
  },
  {
    "path": "evoc/clustering.py",
    "content": "import numpy as np\nimport numba\n\nfrom sklearn.base import BaseEstimator, ClusterMixin\nfrom sklearn.utils import check_array, check_random_state\nfrom sklearn.utils.validation import check_is_fitted\n\nfrom .numba_kdtree import build_kdtree\nfrom .boruvka import parallel_boruvka\nfrom .cluster_trees import (\n    mst_to_linkage_tree,\n    condense_tree,\n    mask_condensed_tree,\n    extract_leaves,\n    get_cluster_label_vector,\n    get_point_membership_strength_vector,\n)\nfrom .clustering_utilities import (\n    find_peaks,\n    _binary_search_for_n_clusters,\n    binary_search_for_n_clusters,\n    min_cluster_size_barcode,\n    compute_total_persistence,\n    extract_clusters_by_id,\n    select_diverse_peaks,\n    build_cluster_tree,\n    find_duplicates,\n)\nfrom .knn_graph import knn_graph\nfrom .label_propagation import label_propagation_init\nfrom .node_embedding import node_embedding\nfrom .graph_construction import neighbor_graph_matrix\n\n\ndef build_cluster_layers(\n    data,\n    *,\n    min_samples=5,\n    base_min_cluster_size=10,\n    base_n_clusters=None,\n    reproducible_flag=False,\n    min_similarity_threshold=0.2,\n    max_layers=10,\n):\n    \"\"\"Build hierarchical cluster layers from embedding data.\n\n    Parameters\n    ----------\n    data : array-like of shape (n_samples, n_features)\n        The embedding data to cluster. Typically the output of a node embedding\n        algorithm.\n\n    min_samples : int, default=5\n        The minimum number of samples to use in the density estimation when\n        performing density based clustering.\n\n    base_min_cluster_size : int, default=10\n        The minimum number of points in a cluster at the base layer of the clustering.\n        This gives the finest granularity clustering that will be returned.\n\n    base_n_clusters : int or None, default=None\n        If not None, the algorithm will attempt to find the granularity of\n        clustering that will give exactly this many clusters for the bottom-most layer\n        of clustering. This affects the base layer computation and allows multiple\n        layers to be built on top of this base.\n\n    reproducible_flag : bool, default=False\n        Whether to ensure reproducible results by using deterministic algorithms\n        where possible.\n\n    min_similarity_threshold : float, default=0.2\n        The minimum similarity threshold for cluster layer selection. Peaks that result\n        in clusterings with Jaccard similarity above this threshold will be filtered out\n        to ensure diverse cluster layers.\n\n    max_layers : int, default=10\n        The maximum number of cluster layers to return. The algorithm will select up to\n        this many diverse peaks based on persistence and similarity criteria.\n\n    Returns\n    -------\n    cluster_layers : list of array-like of shape (n_samples,)\n        The clustering of the data at each layer of the clustering. Each layer\n        is a clustering of the data into a different number of clusters.\n\n    membership_strength_layers : list of array-like of shape (n_samples,)\n        The membership strengths of each point in the clustering at each layer.\n        This gives a measure of how strongly each point belongs to each cluster.\n\n    persistence_scores : list of float\n        The persistence scores for each cluster layer, indicating the quality or\n        stability of the clustering at that layer.\n    \"\"\"\n    n_samples = data.shape[0]\n    min_cluster_size = base_min_cluster_size\n    cluster_layers = []\n    membership_strength_layers = []\n    persistence_scores = []\n\n    n_threads = numba.get_num_threads()\n\n    numba_tree = build_kdtree(data.astype(np.float32))\n    edges = parallel_boruvka(\n        numba_tree,\n        n_threads,\n        min_samples=min_cluster_size if min_samples is None else min_samples,\n        reproducible=reproducible_flag,\n    )\n    sorted_mst = edges[np.argsort(edges.T[2])]\n    uncondensed_tree = mst_to_linkage_tree(sorted_mst)\n    if base_n_clusters is not None:\n        leaves, clusters, strengths = _binary_search_for_n_clusters(\n            uncondensed_tree, base_n_clusters, n_samples=n_samples\n        )\n        cluster_sizes = np.bincount(clusters[clusters >= 0])\n        if len(cluster_sizes) > 0:\n            min_cluster_size = max(1, np.min(cluster_sizes))\n        else:\n            min_cluster_size = base_min_cluster_size\n        # Still need condensed tree for later processing\n        condensed_tree = condense_tree(uncondensed_tree, min_cluster_size)\n    else:\n        condensed_tree = condense_tree(uncondensed_tree, base_min_cluster_size)\n        leaves = extract_leaves(condensed_tree)\n        clusters = get_cluster_label_vector(condensed_tree, leaves, 0.0, n_samples)\n        strengths = get_point_membership_strength_vector(\n            condensed_tree, leaves, clusters\n        )\n\n    mask = condensed_tree.child >= n_samples\n    cluster_tree = mask_condensed_tree(condensed_tree, mask)\n    # points_tree = mask_condensed_tree(condensed_tree, ~mask)\n\n    # Check if cluster_tree is valid before processing\n    if len(cluster_tree.child) > 0 and cluster_tree.child[-1] >= n_samples:\n        births, deaths, parents, lambda_deaths = min_cluster_size_barcode(\n            cluster_tree, n_samples, min_cluster_size\n        )\n        sizes, total_persistence = compute_total_persistence(\n            births, deaths, lambda_deaths\n        )\n        peaks = find_peaks(total_persistence)\n    else:\n        # Handle empty or invalid cluster tree\n        births = np.array([])\n        deaths = np.array([])\n        parents = np.array([])\n        lambda_deaths = np.array([])\n        sizes = np.array([])\n        total_persistence = np.array([])\n        peaks = np.array([], dtype=np.int64)\n\n    # Always include the base layer (from initial condensed tree)\n    cluster_layers.append(clusters)\n    membership_strength_layers.append(strengths)\n    persistence_scores.append(0.0)  # Base layer gets 0 persistence score\n\n    # Select diverse peaks using hierarchical selection\n    selected_peaks = select_diverse_peaks(\n        peaks,\n        total_persistence,\n        sizes,\n        births,\n        deaths,\n        min_similarity_threshold=min_similarity_threshold,\n        max_layers=max_layers - 1,  # Reserve one slot for base layer\n    )\n\n    for peak in selected_peaks:\n        best_birth = sizes[peak]\n        persistence = total_persistence[peak]\n        selected_clusters = (\n            np.where((births <= best_birth) & (deaths > best_birth))[0] + n_samples\n        )\n        labels, strengths = extract_clusters_by_id(condensed_tree, selected_clusters)\n        cluster_layers.append(labels)\n        membership_strength_layers.append(strengths)\n        persistence_scores.append(persistence)\n\n    # Sort cluster layers by number of clusters (most clusters first)\n    n_clusters_per_layer = [layer.max() + 1 for layer in cluster_layers]\n    sorted_indices = np.argsort(n_clusters_per_layer)[::-1]  # Descending order\n\n    cluster_layers = [cluster_layers[i] for i in sorted_indices]\n    membership_strength_layers = [membership_strength_layers[i] for i in sorted_indices]\n    persistence_scores = [persistence_scores[i] for i in sorted_indices]\n\n    return cluster_layers, membership_strength_layers, persistence_scores\n\n\ndef evoc_clusters(\n    data,\n    noise_level=0.5,\n    base_min_cluster_size=5,\n    base_n_clusters=None,\n    approx_n_clusters=None,\n    n_neighbors=15,\n    min_samples=5,\n    n_epochs=50,\n    node_embedding_init=\"label_prop\",\n    symmetrize_graph=True,\n    return_duplicates=False,\n    node_embedding_dim=None,\n    neighbor_scale=1.0,\n    random_state=None,\n    reproducible_flag=True,\n    min_similarity_threshold=0.2,\n    max_layers=10,\n    n_label_prop_iter=20,\n):\n    \"\"\"Cluster data using the EVoC algorithm.\n\n    Parameters\n    ----------\n\n    data : array-like of shape (n_samples, n_features)\n        The data to cluster. If the data is float valued then it is assumed to use\n        cosine distance as a matric. If the data is int8 valued then it is assumed\n        that a quantized embedding is being used and a quantized version of cosine\n        distance is used. If the data is uint8 valued then it is assumed that a\n        binary embedding is being used, and a bitwise Jaccard distance is used.\n\n    noise_level : float, default=0.5\n        The noise level expected in the data. A value of 0.0 will try to cluster\n        more data, at the expense of getting less accurate clustering. A value of\n        1.0 will try for accurate clusters, discarding more data as noise to do so.\n\n    base_min_cluster_size : int, default=5\n        The minimum number of points in a cluster at the base layer of the clustering.\n        This gives the finest granularity clustering that will be returned, with less\n        graularity at higher layers.\n\n    base_n_clusters : int, default=None\n        If not None, the algorithm will attempt to find the granularity of\n        clustering that will give exactly this many clusters for the bottom-most layer\n        of clustering. This affects the base layer computation and allows multiple\n        layers to be built on top of this base. Since the actual number of clusters\n        cannot be guaranteed this is only approximate, but usually the algorithm can\n        manage to get this exact number, assuming a reasonable clustering into\n        ``base_n_clusters`` exists.\n\n    approx_n_clusters : int, default=None\n        If not None, the algorithm will attempt to find the granularity of\n        clustering that will give exactly this many clusters as the final output.\n        Unlike ``base_n_clusters``, when this parameter is set, only a single\n        clustering layer will be returned -- no hierarchical layers will be produced.\n        This is useful when you know the exact number of clusters you want and don't\n        need the multi-layer analysis. Since the actual number of clusters cannot be\n        guaranteed this is only approximate, but usually the algorithm can manage to\n        get this exact number, assuming a reasonable clustering into ``approx_n_clusters``\n        exists.\n\n    n_neighbors : int, default=15\n        The number of neighbors to use in the nearest neighbor graph construction.\n\n    min_samples : int, default=5\n        The minimum number of samples to use in the density estimation when\n        performing density based clustering on the node embedding.\n\n    n_epochs : int, default=50\n        The number of epochs to use when training the node embedding.\n\n    node_embedding_init : str or None, default='label_prop'\n        The method to use to initialize the node embedding. If None, no initialization\n        will be used. If 'label_prop', the label propagation method will be used.\n\n    symmetrize_graph : bool, default=True\n        Whether to symmetrize the nearest neighbor graph before using it to\n        construct the node embedding.\n\n    return_duplicates : bool, default=False\n        Whether to return a set of duplicate pairs of points in the data.\n\n    node_embedding_dim : int or None, default=None\n        The number of dimensions to use in the node embedding. If None, a default\n        value of min(max(n_neighbors // 4, 4), 15) will be used.\n\n    neighbor_scale : float, default=1.0\n        The scale factor to use when constructing the nearest neighbor graph. This\n        multiplies the effective number of neighbors used in graph construction\n        (neighbor_scale * n_neighbors). Values > 1.0 create denser graphs with more\n        connectivity, potentially capturing more global structure but at increased\n        computational cost. Values < 1.0 create sparser graphs focused on local\n        structure.\n\n    random_state : np.random.RandomState or None, default=None\n        The random state to use for the random number generator. If None, the random\n        number generator will not be seeded and will use the system time as the seed.\n\n    reproducible_flag : bool, default=True\n        Whether to ensure reproducible results by using deterministic algorithms\n        where possible. When True, the clustering results should be consistent\n        across runs with the same random_state.\n\n    min_similarity_threshold : float, default=0.2\n        The minimum similarity threshold for cluster layer selection. Peaks that result\n        in clusterings with Jaccard similarity above this threshold will be filtered out\n        to ensure diverse cluster layers.\n\n    max_layers : int, default=10\n        The maximum number of cluster layers to return. The algorithm will select up to\n        this many diverse peaks based on persistence and similarity criteria.\n\n    n_label_prop_iter : int, default=20\n        The number of iterations to use in the label propagation algorithm when\n        initializing the node embedding.\n\n    Returns\n    -------\n\n    cluster_layers : list of array-like of shape (n_samples,)\n        The clustering of the data at each layer of the clustering. Each layer\n        is a clustering of the data into a different number of clusters.\n\n    membership_strengths : list of array-like of shape (n_samples,)\n        The membership strengths of each point in the clustering at each layer.\n        This gives a measure of how strongly each point belongs to each cluster.\n\n    nn_inds : array-like of shape (n_samples, n_neighbors)\n        Indices of nearest neighbors for each sample.\n\n    nn_dists : array-like of shape (n_samples, n_neighbors)\n        Distance from each sample to each nearest neighbor indexed by nn_inds\n\n    duplicates : set of tuple of int\n        Only returned in ``return_duplicates`` is True. A set of pairs of indices of\n        potential duplicate points in the data.\n    \"\"\"\n    if random_state is None:\n        random_state = np.random.RandomState()\n\n    nn_inds, nn_dists = knn_graph(\n        data, n_neighbors=n_neighbors, random_state=random_state\n    )\n    graph = neighbor_graph_matrix(\n        neighbor_scale * n_neighbors, nn_inds, nn_dists, symmetrize_graph\n    )\n    n_embedding_components = node_embedding_dim or min(max(n_neighbors // 4, 4), 15)\n    if node_embedding_init == \"label_prop\":\n        init_embedding = label_propagation_init(\n            graph,\n            n_components=n_embedding_components,\n            approx_n_parts=np.clip(int(8 * np.sqrt(data.shape[0])), 256, 16384),\n            random_scale=0.1,\n            scaling=0.5,\n            noise_level=noise_level,\n            random_state=random_state,\n            data=data,\n            n_label_prop_iter=n_label_prop_iter,\n        )\n    elif node_embedding_init is None:\n        init_embedding = None\n\n    embedding = node_embedding(\n        graph,\n        n_components=n_embedding_components,\n        n_epochs=n_epochs,\n        initial_embedding=init_embedding,\n        negative_sample_rate=1.0,\n        noise_level=noise_level,\n        random_state=random_state,\n        verbose=False,\n        reproducible_flag=reproducible_flag,\n        initial_alpha=0.1,\n    )\n\n    if return_duplicates:\n        duplicates = find_duplicates(nn_inds, nn_dists)\n\n    n_threads = numba.get_num_threads()\n\n    if approx_n_clusters is not None:\n        cluster_vector, strengths = binary_search_for_n_clusters(\n            embedding,\n            approx_n_clusters,\n            n_threads,\n            min_samples=min_samples,\n        )\n        if return_duplicates:\n            return [cluster_vector], [strengths], [0.0], nn_inds, nn_dists, duplicates\n        else:\n            return [cluster_vector], [strengths], [0.0], nn_inds, nn_dists\n    else:\n        cluster_layers, membership_strengths, persistence_scores = build_cluster_layers(\n            embedding,\n            min_samples=min_samples,\n            base_min_cluster_size=base_min_cluster_size,\n            base_n_clusters=base_n_clusters,\n            reproducible_flag=reproducible_flag,\n            min_similarity_threshold=min_similarity_threshold,\n            max_layers=max_layers,\n        )\n\n        if return_duplicates:\n            return (\n                cluster_layers,\n                membership_strengths,\n                persistence_scores,\n                nn_inds,\n                nn_dists,\n                duplicates,\n            )\n        else:\n            return (\n                cluster_layers,\n                membership_strengths,\n                persistence_scores,\n                nn_inds,\n                nn_dists,\n            )\n\n\nclass EVoC(BaseEstimator, ClusterMixin):\n    \"\"\"\n    Embedding Vector Oriented Clustering for efficient clustering of high-dimensional\n    embedding vectors such as CLIP-vectors, sentence-transformers output, etc. The\n    clustering uses a combination of a node embedding of a nearest neighbour graph,\n    related to UMAP, and a density based clustering approach related to HDBSCAN,\n    improving upon those approaches in efficiency and quality for the specific case\n    of high-dimensional embedding vectors.\n\n    Parameters\n    ----------\n\n    noise_level : float, default=0.5\n        The noise level expected in the data. A value of 0.0 will try to cluster\n        more data, at the expense of getting less accurate clustering. A value of\n        1.0 will try for accurate clusters, discarding more data as noise to do so.\n\n    base_min_cluster_size : int, default=5\n        The minimum number of points in a cluster at the base layer of the clustering.\n        This gives the finest granularity clustering that will be returned, with less\n        graularity at higher layers.\n\n    base_n_clusters : int or None, default=None\n        If not None, the algorithm will attempt to find the granularity of\n        clustering that will give exactly this many clusters for the bottom-most layer\n        of clustering. This affects the base layer computation and allows multiple\n        layers to be built on top of this base. Since the actual number of clusters\n        cannot be guaranteed this is only approximate, but usually the algorithm can\n        manage to get this exact number, assuming a reasonable clustering into\n        ``base_n_clusters`` exists.\n\n    approx_n_clusters : int, default=None\n        If not None, the algorithm will attempt to find the granularity of\n        clustering that will give exactly this many clusters as the final output.\n        Unlike ``base_n_clusters``, when this parameter is set, only a single\n        clustering layer will be returned -- no hierarchical layers will be produced.\n        This is useful when you know the exact number of clusters you want and don't\n        need the multi-layer analysis. Since the actual number of clusters cannot be\n        guaranteed this is only approximate, but usually the algorithm can manage to\n        get this exact number, assuming a reasonable clustering into ``approx_n_clusters``\n        exists.\n\n    n_neighbors : int, default=15\n        The number of neighbors to use in the nearest neighbor graph construction.\n\n    min_samples : int, default=5\n        The minimum number of samples to use in the density estimation when\n        performing density based clustering on the node embedding.\n\n    n_epochs : int, default=50\n        The number of epochs to use when training the node embedding.\n\n    node_embedding_init : str or None, default='label_prop'\n        The method to use to initialize the node embedding. If None, no initialization\n        will be used. If 'label_prop', the label propagation method will be used.\n\n    symmetrize_graph : bool, default=True\n        Whether to symmetrize the nearest neighbor graph before using it to\n        construct the node embedding.\n\n    node_embedding_dim : int or None, default=None\n        The number of dimensions to use in the node embedding. If None, a default\n        value of min(max(n_neighbors // 4, 4), 15) will be used.\n\n    neighbor_scale : float, default=1.0\n        The scale factor to use when constructing the nearest neighbor graph. This\n        multiplies the effective number of neighbors used in graph construction\n        (neighbor_scale * n_neighbors). Values > 1.0 create denser graphs with more\n        connectivity, potentially capturing more global structure but at increased\n        computational cost. Values < 1.0 create sparser graphs focused on local\n        structure.\n\n    random_state : int or None, default=None\n        The random seed to use for the random number generator. If None, the random\n        number generator will not be seeded and will use the system time as the seed.\n\n    min_similarity_threshold : float, default=0.2\n        The minimum similarity threshold for cluster layer selection. Peaks that result\n        in clusterings with Jaccard similarity above this threshold will be filtered out\n        to ensure diverse cluster layers.\n\n    max_layers : int, default=10\n        The maximum number of cluster layers to return. The algorithm will select up to\n        this many diverse peaks based on persistence and similarity criteria.\n\n    n_label_prop_iter : int, default=20\n        The number of iterations to use in the label propagation algorithm when\n        initializing the node embedding. This parameter controls how many steps\n        the label propagation process takes to converge when node_embedding_init\n        is set to 'label_prop'.\n\n    Attributes\n    ----------\n\n    labels_ : array-like of shape (n_samples,)\n        An array of labels for the data samples; this is a integer array as per other scikit-learn\n        clustering algorithms. A value of -1 indicates that a point is a noise point and\n        not in any cluster.\n\n    membership_strengths_ : array-like of shape (n_samples,)\n        An array of membership strengths for the data samples; this gives a measure of how\n        strongly each point belongs to each cluster. This is a floating point array with\n        values between 0 and 1.\n\n    cluster_layers_ : list of array-like of shape (n_samples,)\n        The clustering of the data at each layer of the clustering. Each layer\n        is a clustering of the data into a different number of clusters; the earlier the\n        cluster vector is in this list the finer the granularity of clustering.\n\n    membership_strength_layers_ : list of array-like of shape (n_samples,)\n        The membership strengths of each point in the clustering at each layer.\n\n    cluster_tree_ : dict\n        A dictionary representing the hierarchical clustering of the data. The keys are\n        tuples of (layer, cluster) and the values are lists of tuples of (layer, cluster)\n        representing the children of the key cluster.\n\n    nn_inds_ : array-like of shape (n_samples, n_neighbors)\n        Indices of nearest neighbors for each sample.\n\n    nn_dists_ : array-like of shape (n_samples, n_neighbors)\n        Distance from each sample to each nearest neighbor (indexed by nn_inds).\n\n    duplicates_ : set of tuple of int\n        A set of pairs of indices of potential duplicate points in the data.\n    \"\"\"\n\n    def __init__(\n        self,\n        noise_level: float = 0.5,\n        base_min_cluster_size: int = 5,\n        base_n_clusters: int | None = None,\n        approx_n_clusters: int | None = None,\n        n_neighbors: int = 15,\n        min_samples: int = 5,\n        n_epochs: int = 50,\n        node_embedding_init: str | None = \"label_prop\",\n        symmetrize_graph: bool = True,\n        node_embedding_dim: int | None = None,\n        neighbor_scale: float = 1.0,\n        random_state: int | None = None,\n        min_similarity_threshold: float = 0.2,\n        max_layers: int = 10,\n        n_label_prop_iter=20,\n    ) -> None:\n        self.n_neighbors = n_neighbors\n        self.noise_level = noise_level\n        self.base_min_cluster_size = base_min_cluster_size\n        self.base_n_clusters = base_n_clusters\n        self.approx_n_clusters = approx_n_clusters\n        self.min_samples = min_samples\n        self.n_epochs = n_epochs\n        self.node_embedding_init = node_embedding_init\n        self.symmetrize_graph = symmetrize_graph\n        self.node_embedding_dim = node_embedding_dim\n        self.neighbor_scale = neighbor_scale\n        self.random_state = random_state\n        self.min_similarity_threshold = min_similarity_threshold\n        self.max_layers = max_layers\n        self.n_label_prop_iter = n_label_prop_iter\n\n    def fit_predict(self, X, y=None, **fit_params):\n        \"\"\"Fit the model to the data and return the clustering labels.\n\n        Parameters\n        ----------\n\n        X : array-like of shape (n_samples, n_features)\n            The data to cluster. If the data is float valued then it is assumed to use\n            cosine distance as a matric. If the data is int8 valued then it is assumed\n            that a quantized embedding is being used and a quantized version of cosine\n            distance is used. If the data is uint8 valued then it is assumed that a\n            binary embedding is being used, and a bitwise Jaccard distance is used.\n\n        y : array-like of shape (n_samples,), default=None\n            Ignored. This parameter exists only for compatibility with\n            scikit-learn's fit_predict method.\n\n        **fit_params : dict\n            Additional fit parameters. Currently unused, included for compatibility\n            with scikit-learn's fit_predict interface.\n\n        Returns\n        -------\n\n        labels_ : array-like of shape (n_samples,)\n            An array of labels for the data samples; this is a integer array as per other scikit-learn\n            clustering algorithms. A value of -1 indicates that a point is a noise point and\n            not in any cluster.\n\n        \"\"\"\n\n        X = check_array(X)\n        current_random_state = check_random_state(self.random_state)\n\n        (\n            self.cluster_layers_,\n            self.membership_strength_layers_,\n            self.persistence_scores_,\n            self.nn_inds_,\n            self.nn_dists_,\n            self.duplicates_,\n        ) = evoc_clusters(\n            X,\n            n_neighbors=self.n_neighbors,\n            noise_level=self.noise_level,\n            base_min_cluster_size=self.base_min_cluster_size,\n            base_n_clusters=self.base_n_clusters,\n            approx_n_clusters=self.approx_n_clusters,\n            min_samples=self.min_samples,\n            n_epochs=self.n_epochs,\n            node_embedding_init=self.node_embedding_init,\n            symmetrize_graph=self.symmetrize_graph,\n            return_duplicates=True,\n            node_embedding_dim=self.node_embedding_dim,\n            neighbor_scale=self.neighbor_scale,\n            random_state=current_random_state,\n            reproducible_flag=self.random_state is not None,\n            min_similarity_threshold=self.min_similarity_threshold,\n            max_layers=self.max_layers,\n            n_label_prop_iter=self.n_label_prop_iter,\n        )\n\n        if len(self.cluster_layers_) == 1:\n            self.labels_ = self.cluster_layers_[0]\n            self.membership_strengths_ = self.membership_strength_layers_[0]\n        else:\n            best_layer = np.argmax(self.persistence_scores_)\n            self.labels_ = self.cluster_layers_[best_layer]\n            self.membership_strengths_ = self.membership_strength_layers_[best_layer]\n\n        return self.labels_\n\n    def fit(self, X, y=None, **fit_params):\n        \"\"\"Fit the model to the data.\n\n        Parameters\n        ----------\n\n        X : array-like of shape (n_samples, n_features)\n            The data to cluster. If the data is float valued then it is assumed to use\n            cosine distance as a matric. If the data is int8 valued then it is assumed\n            that a quantized embedding is being used and a quantized version of cosine\n            distance is used. If the data is uint8 valued then it is assumed that a\n            binary embedding is being used, and a bitwise Jaccard distance is used.\n\n        y : array-like of shape (n_samples,), default=None\n            Ignored. This parameter exists only for compatibility with\n            scikit-learn's fit method.\n\n        **fit_params : dict\n            Additional fit parameters. Currently unused, included for compatibility\n            with scikit-learn's fit interface.\n\n        Returns\n        -------\n\n        self : sklearn Estimator\n            Returns the instance itself.\n        \"\"\"\n        self.fit_predict(X, y, **fit_params)\n        return self\n\n    @property\n    def cluster_tree_(self):\n        \"\"\"dict\n        A dictionary representing the hierarchical clustering of the data.\n\n        The keys are tuples of (layer, cluster) and the values are lists of\n        tuples of (layer, cluster) representing the children of the key cluster.\n        This provides a tree structure showing how clusters at different layers\n        relate to each other hierarchically.\n\n        Only available after fitting the model.\n\n        Returns\n        -------\n        dict\n            Hierarchical tree structure with (layer, cluster) tuples as keys\n            and lists of child (layer, cluster) tuples as values.\n\n        Raises\n        ------\n        NotFittedError\n            If the model has not been fitted yet.\n        \"\"\"\n        check_is_fitted(\n            self,\n            \"cluster_layers_\",\n            msg=\"This %(name)s instance is not fitted yet, and 'cluster_tree_' is not available. \"\n            \"Please call 'fit' with appropriate arguments before accessing this attribute.\",\n        )\n        if not hasattr(self, \"_cluster_tree\"):\n            self._cluster_tree = build_cluster_tree(self.cluster_layers_)\n        return self._cluster_tree\n"
  },
  {
    "path": "evoc/clustering_utilities.py",
    "content": "import numpy as np\nimport numba\n\nfrom .numba_kdtree import build_kdtree\nfrom .boruvka import parallel_boruvka\nfrom .cluster_trees import (\n    mst_to_linkage_tree,\n    condense_tree,\n    extract_leaves,\n    get_cluster_label_vector,\n    get_point_membership_strength_vector,\n)\n\n\n##############################################################\n# Directly derived from scipy's find_peaks function:\n# https://github.com/scipy/scipy/blob/bd66693b8aecc6f528ca9b1cfd6bb1f61477ca0f/scipy/signal/_peak_finding_utils.pyx#L20\n##############################################################\n@numba.njit(\n    [\"intp[:](float32[::1])\", \"intp[:](float64[::1])\"],\n    locals={\n        \"midpoints\": numba.types.intp[::1],\n        \"left_edges\": numba.types.intp[::1],\n        \"right_edges\": numba.types.intp[::1],\n        \"m\": numba.types.uint32,\n        \"i\": numba.types.uint32,\n    },\n    nogil=True,\n    parallel=False,\n    fastmath=True,\n    cache=True,\n)\ndef find_peaks(x):\n    # Preallocate, there can't be more maxima than half the size of `x`\n    midpoints = np.empty(x.shape[0] // 2, dtype=np.intp)\n    left_edges = np.empty(x.shape[0] // 2, dtype=np.intp)\n    right_edges = np.empty(x.shape[0] // 2, dtype=np.intp)\n    m = 0  # Pointer to the end of valid area in allocated arrays\n\n    i = 1  # Pointer to current sample, first one can't be maxima\n    i_max = x.shape[0] - 1  # Last sample can't be maxima\n    while i < i_max:\n        # Test if previous sample is smaller\n        if x[i - 1] < x[i]:\n            i_ahead = i + 1  # Index to look ahead of current sample\n\n            # Find next sample that is unequal to x[i]\n            while i_ahead < i_max and x[i_ahead] == x[i]:\n                i_ahead += 1\n\n            # Maxima is found if next unequal sample is smaller than x[i]\n            if x[i_ahead] < x[i]:\n                left_edges[m] = i\n                right_edges[m] = i_ahead - 1\n                midpoints[m] = (left_edges[m] + right_edges[m]) // 2\n                m += 1\n                # Skip samples that can't be maximum\n                i = i_ahead\n        i += 1\n\n    return midpoints[:m]\n\n\n@numba.njit(cache=True)\ndef _binary_search_for_n_clusters(uncondensed_tree, approx_n_clusters, n_samples):\n    lower_bound_min_cluster_size = 2\n    upper_bound_min_cluster_size = n_samples // 2\n    mid_min_cluster_size = int(\n        round((lower_bound_min_cluster_size + upper_bound_min_cluster_size) / 2.0)\n    )\n    min_n_clusters = 0\n\n    upper_tree = condense_tree(uncondensed_tree, upper_bound_min_cluster_size)\n    leaves = extract_leaves(upper_tree)\n    upper_n_clusters = len(leaves)\n\n    lower_tree = condense_tree(uncondensed_tree, lower_bound_min_cluster_size)\n    leaves = extract_leaves(lower_tree)\n    lower_n_clusters = len(leaves)\n\n    while upper_bound_min_cluster_size - lower_bound_min_cluster_size > 1:\n        mid_min_cluster_size = int(\n            round((lower_bound_min_cluster_size + upper_bound_min_cluster_size) / 2.0)\n        )\n        if (\n            mid_min_cluster_size == lower_bound_min_cluster_size\n            or mid_min_cluster_size == upper_bound_min_cluster_size\n        ):\n            break\n        mid_tree = condense_tree(uncondensed_tree, mid_min_cluster_size)\n        leaves = extract_leaves(mid_tree)\n        mid_n_clusters = len(leaves)\n\n        if mid_n_clusters < approx_n_clusters:\n            upper_bound_min_cluster_size = mid_min_cluster_size\n            upper_n_clusters = mid_n_clusters\n        elif mid_n_clusters >= approx_n_clusters:\n            lower_bound_min_cluster_size = mid_min_cluster_size\n            lower_n_clusters = mid_n_clusters\n\n    if abs(lower_n_clusters - approx_n_clusters) < abs(\n        upper_n_clusters - approx_n_clusters\n    ):\n        lower_tree = condense_tree(uncondensed_tree, lower_bound_min_cluster_size)\n        leaves = extract_leaves(lower_tree)\n        clusters = get_cluster_label_vector(lower_tree, leaves, 0.0, n_samples)\n        strengths = get_point_membership_strength_vector(lower_tree, leaves, clusters)\n        return leaves, clusters, strengths\n    elif abs(lower_n_clusters - approx_n_clusters) > abs(\n        upper_n_clusters - approx_n_clusters\n    ):\n        upper_tree = condense_tree(uncondensed_tree, upper_bound_min_cluster_size)\n        leaves = extract_leaves(upper_tree)\n        clusters = get_cluster_label_vector(upper_tree, leaves, 0.0, n_samples)\n        strengths = get_point_membership_strength_vector(upper_tree, leaves, clusters)\n        return leaves, clusters, strengths\n    else:\n        lower_tree = condense_tree(uncondensed_tree, lower_bound_min_cluster_size)\n        lower_leaves = extract_leaves(lower_tree)\n        lower_clusters = get_cluster_label_vector(\n            lower_tree, lower_leaves, 0.0, n_samples\n        )\n        upper_tree = condense_tree(uncondensed_tree, upper_bound_min_cluster_size)\n        upper_leaves = extract_leaves(upper_tree)\n        upper_clusters = get_cluster_label_vector(\n            upper_tree, upper_leaves, 0.0, n_samples\n        )\n\n        if np.sum(lower_clusters >= 0) > np.sum(upper_clusters >= 0):\n            strengths = get_point_membership_strength_vector(\n                lower_tree, lower_leaves, lower_clusters\n            )\n            return lower_leaves, lower_clusters, strengths\n        else:\n            strengths = get_point_membership_strength_vector(\n                upper_tree, upper_leaves, upper_clusters\n            )\n            return upper_leaves, upper_clusters, strengths\n\n\n# @numba.njit(cache=True)\ndef binary_search_for_n_clusters(\n    data,\n    approx_n_clusters,\n    n_threads,\n    *,\n    min_samples=5,\n):\n    numba_tree = build_kdtree(data.astype(np.float32))\n    edges = parallel_boruvka(\n        numba_tree, n_threads, min_samples=min_samples, reproducible=False\n    )\n    sorted_mst = edges[np.argsort(edges.T[2])]\n    uncondensed_tree = mst_to_linkage_tree(sorted_mst)\n\n    n_samples = data.shape[0]\n\n    leaves, clusters, strengths = _binary_search_for_n_clusters(\n        uncondensed_tree, approx_n_clusters, n_samples\n    )\n    return clusters, strengths\n\n\n@numba.njit(cache=True)\ndef min_cluster_size_barcode(cluster_tree, n_points, min_size):\n    n_nodes = cluster_tree.child[-1] - n_points + 1\n    parents = np.empty(n_nodes, dtype=np.int32)\n    lambda_deaths = np.empty(n_nodes, dtype=np.float32)\n    size_deaths = np.empty(n_nodes, dtype=np.float32)\n    size_births = np.full(n_nodes, min_size, dtype=np.float32)\n    lambda_deaths[0] = 0\n    size_deaths[0] = n_points\n    parents[0] = n_points\n\n    # Iterate over row-pairs in reverse order\n    n_rows = cluster_tree.child.shape[0]\n    for idx in range(n_rows - 1, 0, -2):\n        out_idx = cluster_tree.child[idx] - n_points\n        parents[out_idx - 1 : out_idx + 1] = cluster_tree.parent[idx]\n        lambda_deaths[out_idx - 1 : out_idx + 1] = np.exp(\n            -1 / cluster_tree.lambda_val[idx]\n        )\n\n        death_size = cluster_tree.child_size[idx - 1 : idx + 1].min()\n        size_deaths[out_idx - 1 : out_idx + 1] = death_size\n        size_births[cluster_tree.parent[idx] - n_points] = max(\n            size_births[out_idx - 1], size_births[out_idx], death_size\n        )\n\n    return size_births, size_deaths, parents, lambda_deaths\n\n\n@numba.njit(cache=True)\ndef compute_total_persistence(births, deaths, lambda_deaths):\n    # maintain left-open (birth, death] interval!\n    sizes = np.unique(births)\n    total_persistence = np.zeros(sizes.shape[0], dtype=np.float32)\n\n    for i in range(1, len(births)):\n        birth = births[i]\n        death = deaths[i]\n        lambda_death = lambda_deaths[i]\n\n        if death <= birth:\n            continue\n\n        # Manual binary search for birth_idx\n        birth_idx = 0\n        for j in range(len(sizes)):\n            if sizes[j] >= birth:\n                birth_idx = j\n                break\n\n        # Manual binary search for death_idx\n        death_idx = len(sizes)\n        for j in range(len(sizes)):\n            if sizes[j] >= death:\n                death_idx = j\n                break\n\n        # Update persistence values\n        for k in range(birth_idx, death_idx):\n            total_persistence[k] += (death - birth) * lambda_death\n\n    return sizes, total_persistence\n\n\n@numba.njit(cache=True)\ndef extract_clusters_by_id(condensed_tree, selected_ids):\n    labels = get_cluster_label_vector(\n        condensed_tree,\n        selected_ids,\n        cluster_selection_epsilon=0.0,\n        n_samples=condensed_tree.parent[0],\n    )\n    strengths = get_point_membership_strength_vector(\n        condensed_tree, selected_ids, labels\n    )\n    return labels, strengths\n\n\n@numba.njit(cache=True)\ndef jaccard_similarity(set_a_array, set_b_array):\n    # Convert to sets for intersection/union operations\n    intersection_count = 0\n    union_set = set(set_a_array)\n\n    for item in set_b_array:\n        if item in union_set:\n            intersection_count += 1\n        else:\n            union_set.add(item)\n\n    union_count = len(union_set)\n    return intersection_count / union_count if union_count > 0 else 0.0\n\n\n@numba.njit(cache=True)\ndef estimate_cluster_similarity(births, deaths, birth_a, birth_b):\n    # Find clusters active at birth_a\n    clusters_a = np.empty(len(births), dtype=np.int64)\n    count_a = 0\n    for i in range(len(births)):\n        if births[i] <= birth_a and deaths[i] > birth_a:\n            clusters_a[count_a] = i\n            count_a += 1\n\n    # Find clusters active at birth_b\n    clusters_b = np.empty(len(births), dtype=np.int64)\n    count_b = 0\n    for i in range(len(births)):\n        if births[i] <= birth_b and deaths[i] > birth_b:\n            clusters_b[count_b] = i\n            count_b += 1\n\n    # Trim arrays to actual sizes\n    active_a = clusters_a[:count_a]\n    active_b = clusters_b[:count_b]\n\n    return jaccard_similarity(active_a, active_b)\n\n\n@numba.njit(cache=True)\ndef select_diverse_peaks(\n    peaks,\n    total_persistence,\n    sizes,\n    births,\n    deaths,\n    min_similarity_threshold=0.2,\n    max_layers=10,\n):\n    if len(peaks) == 0:\n        return np.empty(0, dtype=np.int64)\n\n    # Sort peaks by persistence (highest first)\n    peak_persistence = total_persistence[peaks]\n    sorted_indices = np.argsort(peak_persistence)[::-1]\n    sorted_peaks = peaks[sorted_indices]\n\n    # Pre-allocate arrays for selected peaks and births\n    selected_peaks = np.empty(max_layers, dtype=np.int64)\n    selected_births = np.empty(max_layers, dtype=np.float64)\n    n_selected = 0\n\n    for i in range(len(sorted_peaks)):\n        if n_selected >= max_layers:\n            break\n\n        peak = sorted_peaks[i]\n        birth_size = sizes[peak]\n\n        # Check similarity with already selected peaks\n        is_diverse = True\n        for j in range(n_selected):\n            selected_birth = selected_births[j]\n            similarity = estimate_cluster_similarity(\n                births, deaths, birth_size, selected_birth\n            )\n            if similarity > min_similarity_threshold:\n                is_diverse = False\n                break\n\n        if is_diverse:\n            selected_peaks[n_selected] = peak\n            selected_births[n_selected] = birth_size\n            n_selected += 1\n\n    return selected_peaks[:n_selected]\n\n\n@numba.njit(cache=True)\ndef _build_cluster_tree(labels):\n    mapping = [(-1, -1, -1, -1) for i in range(0)]\n    found = [set([-1]) for i in range(len(labels))]\n    mapping_idx = 0\n    for upper_layer in range(1, len(labels)):\n        upper_layer_unique_labels = np.unique(labels[upper_layer])\n        for lower_layer in range(upper_layer - 1, -1, -1):\n            upper_cluster_order = np.argsort(labels[upper_layer])\n            cluster_groups = np.split(\n                labels[lower_layer][upper_cluster_order],\n                np.cumsum(np.bincount(labels[upper_layer] + 1))[:-1],\n            )\n            for i, label in enumerate(upper_layer_unique_labels):\n                if label >= 0:\n                    for child in cluster_groups[i]:\n                        if child >= 0 and child not in found[lower_layer]:\n                            mapping.append((upper_layer, label, lower_layer, child))\n                            found[lower_layer].add(child)\n\n    for lower_layer in range(len(labels) - 1, -1, -1):\n        for child in range(labels[lower_layer].max() + 1):\n            if child >= 0 and child not in found[lower_layer]:\n                mapping.append((len(labels), 0, lower_layer, child))\n\n    return mapping\n\n\ndef build_cluster_tree(labels):\n    result = {}\n    raw_mapping = _build_cluster_tree(labels)\n    for parent_layer, parent_cluster, child_layer, child_cluster in raw_mapping:\n        parent_name = (parent_layer, parent_cluster)\n        if parent_name in result:\n            result[parent_name].append((child_layer, child_cluster))\n        else:\n            result[parent_name] = [(child_layer, child_cluster)]\n    return result\n\n\n@numba.njit(cache=True)\ndef find_duplicates(knn_inds, knn_dists):\n    duplicate_distance = np.max(knn_dists.T[0])\n    duplicates = set([(-1, -1) for i in range(0)])\n    for i in range(knn_inds.shape[0]):\n        for j in range(0, knn_inds.shape[1]):\n            if knn_dists[i, j] <= duplicate_distance:\n                k = knn_inds[i, j]\n                if i < k:\n                    duplicates.add((i, k))\n                elif k < i:\n                    duplicates.add((k, i))\n                else:\n                    continue\n\n    return duplicates\n"
  },
  {
    "path": "evoc/common_nndescent.py",
    "content": "import numpy as np\nimport numba\n\n\n@numba.njit(\"void(i8[:], i8)\", cache=True)\ndef seed(rng_state, seed):\n    \"\"\"Seed the random number generator with a given seed.\"\"\"\n    rng_state.fill(seed + 0xFFFF)\n\n\n@numba.njit(\"i4(i8[:])\", cache=True)\ndef tau_rand_int(state):\n    \"\"\"A fast (pseudo)-random number generator.\n\n    Parameters\n    ----------\n    state: array of int64, shape (3,)\n        The internal state of the rng\n\n    Returns\n    -------\n    A (pseudo)-random int32 value\n    \"\"\"\n    state[0] = (((state[0] & 4294967294) << 12) & 0xFFFFFFFF) ^ (\n        (((state[0] << 13) & 0xFFFFFFFF) ^ state[0]) >> 19\n    )\n    state[1] = (((state[1] & 4294967288) << 4) & 0xFFFFFFFF) ^ (\n        (((state[1] << 2) & 0xFFFFFFFF) ^ state[1]) >> 25\n    )\n    state[2] = (((state[2] & 4294967280) << 17) & 0xFFFFFFFF) ^ (\n        (((state[2] << 3) & 0xFFFFFFFF) ^ state[2]) >> 11\n    )\n\n    return state[0] ^ state[1] ^ state[2]\n\n\n@numba.njit(\"f4(i8[:])\", cache=True)\ndef tau_rand(state):\n    \"\"\"A fast (pseudo)-random number generator for floats in the range [0,1]\n\n    Parameters\n    ----------\n    state: array of int64, shape (3,)\n        The internal state of the rng\n\n    Returns\n    -------\n    A (pseudo)-random float32 in the interval [0, 1]\n    \"\"\"\n    integer = tau_rand_int(state)\n    return abs(float(integer) / 0x7FFFFFFF)\n\n\n# @numba.njit(cache=True)\ndef make_heap(n_points, size):\n    indices = np.full((int(n_points), int(size)), -1, dtype=np.int32)\n    distances = np.full((int(n_points), int(size)), np.inf, dtype=np.float32)\n    flags = np.zeros((int(n_points), int(size)), dtype=np.uint8)\n    result = (indices, distances, flags)\n\n    return result\n\n\n@numba.njit(cache=True)\ndef siftdown(heap1, heap2, elt):\n    \"\"\"Restore the heap property for a heap with an out of place element\n    at position ``elt``. This works with a heap pair where heap1 carries\n    the weights and heap2 holds the corresponding elements.\"\"\"\n    while elt * 2 + 1 < heap1.shape[0]:\n        left_child = elt * 2 + 1\n        right_child = left_child + 1\n        swap = elt\n\n        if heap1[swap] < heap1[left_child]:\n            swap = left_child\n\n        if right_child < heap1.shape[0] and heap1[swap] < heap1[right_child]:\n            swap = right_child\n\n        if swap == elt:\n            break\n        else:\n            heap1[elt], heap1[swap] = heap1[swap], heap1[elt]\n            heap2[elt], heap2[swap] = heap2[swap], heap2[elt]\n            elt = swap\n\n\n@numba.njit(parallel=True, cache=True)\ndef deheap_sort(indices, distances):\n    \"\"\"Given two arrays representing a heap (indices and distances), reorder the\n     arrays by increasing distance. This is effectively just the second half of\n     heap sort (the first half not being required since we already have the\n     graph_data in a heap).\n\n     Note that this is done in-place.\n\n    Parameters\n    ----------\n    indices : array of shape (n_samples, n_neighbors)\n        The graph indices to sort by distance.\n    distances : array of shape (n_samples, n_neighbors)\n        The corresponding edge distance.\n\n    Returns\n    -------\n    indices, distances: arrays of shape (n_samples, n_neighbors)\n        The indices and distances sorted by increasing distance.\n    \"\"\"\n    for i in numba.prange(indices.shape[0]):\n        # starting from the end of the array and moving back\n        for j in range(indices.shape[1] - 1, 0, -1):\n            indices[i, 0], indices[i, j] = indices[i, j], indices[i, 0]\n            distances[i, 0], distances[i, j] = distances[i, j], distances[i, 0]\n\n            siftdown(distances[i, :j], indices[i, :j], 0)\n\n    return indices, distances\n\n\n@numba.njit(\n    \"i4(f4[::1],i4[::1],f4,i4)\",\n    fastmath=True,\n    locals={\n        \"size\": numba.types.intp,\n        \"i\": numba.types.uint16,\n        \"ic1\": numba.types.uint16,\n        \"ic2\": numba.types.uint16,\n        \"i_swap\": numba.types.uint16,\n    },\n    cache=True,\n)\ndef build_candidates_heap_push(priorities, indices, p, n):\n    if p >= priorities[0]:\n        return 0\n\n    size = priorities.shape[0]\n\n    # break if we already have this element.\n    for i in range(size):\n        if n == indices[i]:\n            return 0\n\n    # insert val at position zero\n    priorities[0] = p\n    indices[0] = n\n\n    # descend the heap, swapping values until the max heap criterion is met\n    i = 0\n    while True:\n        ic1 = 2 * i + 1\n        ic2 = ic1 + 1\n\n        if ic1 >= size:\n            break\n        elif ic2 >= size:\n            if priorities[ic1] > p:\n                i_swap = ic1\n            else:\n                break\n        elif priorities[ic1] >= priorities[ic2]:\n            if p < priorities[ic1]:\n                i_swap = ic1\n            else:\n                break\n        else:\n            if p < priorities[ic2]:\n                i_swap = ic2\n            else:\n                break\n\n        priorities[i] = priorities[i_swap]\n        indices[i] = indices[i_swap]\n\n        i = i_swap\n\n    priorities[i] = p\n    indices[i] = n\n\n    return 1\n\n\n@numba.njit(parallel=True, locals={\"idx\": numba.types.int64}, cache=True)\ndef build_candidates(current_graph, max_candidates, rng_state, n_threads):\n    \"\"\"Build a heap of candidate neighbors for nearest neighbor descent. For\n    each vertex the candidate neighbors are any current neighbors, and any\n    vertices that have the vertex as one of their nearest neighbors.\n\n    Parameters\n    ----------\n    current_graph: heap\n        The current state of the graph for nearest neighbor descent.\n\n    max_candidates: int\n        The maximum number of new candidate neighbors.\n\n    rng_state: array of int64, shape (3,)\n        The internal state of the rng\n\n    Returns\n    -------\n    candidate_neighbors: A heap with an array of (randomly sorted) candidate\n    neighbors for each vertex in the graph.\n    \"\"\"\n    current_indices = current_graph[0]\n    current_flags = current_graph[2]\n\n    n_vertices = current_indices.shape[0]\n    n_neighbors = current_indices.shape[1]\n\n    new_candidate_indices = np.full((n_vertices, max_candidates), -1, dtype=np.int32)\n    new_candidate_priority = np.full(\n        (n_vertices, max_candidates), np.inf, dtype=np.float32\n    )\n\n    old_candidate_indices = np.full((n_vertices, max_candidates), -1, dtype=np.int32)\n    old_candidate_priority = np.full(\n        (n_vertices, max_candidates), np.inf, dtype=np.float32\n    )\n\n    block_size = n_vertices // n_threads + 1\n\n    for n in numba.prange(n_threads):\n        local_rng_state = rng_state + n\n        block_start = n * block_size\n        block_end = min(block_start + block_size, n_vertices)\n\n        for i in range(n_vertices):\n            for j in range(n_neighbors):\n                idx = current_indices[i, j]\n\n                if idx >= 0 and (\n                    (i >= block_start and i < block_end)\n                    or (idx >= block_start and idx < block_end)\n                ):\n                    isn = current_flags[i, j]\n                    d = tau_rand(local_rng_state)\n\n                    if isn:\n                        if i >= block_start and i < block_end:\n                            build_candidates_heap_push(\n                                new_candidate_priority[i],\n                                new_candidate_indices[i],\n                                d,\n                                idx,\n                            )\n                        if idx >= block_start and idx < block_end:\n                            build_candidates_heap_push(\n                                new_candidate_priority[idx],\n                                new_candidate_indices[idx],\n                                d,\n                                i,\n                            )\n                    else:\n                        if i >= block_start and i < block_end:\n                            build_candidates_heap_push(\n                                old_candidate_priority[i],\n                                old_candidate_indices[i],\n                                d,\n                                idx,\n                            )\n                        if idx >= block_start and idx < block_end:\n                            build_candidates_heap_push(\n                                old_candidate_priority[idx],\n                                old_candidate_indices[idx],\n                                d,\n                                i,\n                            )\n\n    indices = current_graph[0]\n    flags = current_graph[2]\n\n    for i in numba.prange(n_vertices):\n        for j in range(n_neighbors):\n            idx = indices[i, j]\n\n            for k in range(max_candidates):\n                if new_candidate_indices[i, k] == idx:\n                    flags[i, j] = 0\n                    break\n\n    return new_candidate_indices, old_candidate_indices\n\n\n@numba.njit(\n    \"i4(f4[::1],i4[::1],u1[::1],f4,i4)\",\n    fastmath=True,\n    locals={\n        \"size\": numba.types.intp,\n        \"i\": numba.types.uint16,\n        \"ic1\": numba.types.uint16,\n        \"ic2\": numba.types.uint16,\n        \"i_swap\": numba.types.uint16,\n    },\n    cache=True,\n)\ndef flagged_heap_push(priorities, indices, flags, p, n):\n    if p >= priorities[0]:\n        return 0\n\n    size = priorities.shape[0]\n\n    # break if we already have this element.\n    for i in range(size):\n        if n == indices[i]:\n            return 0\n\n    # insert val at position zero\n    priorities[0] = p\n    indices[0] = n\n\n    # descend the heap, swapping values until the max heap criterion is met\n    i = 0\n    while True:\n        ic1 = 2 * i + 1\n        ic2 = ic1 + 1\n\n        if ic1 >= size:\n            break\n        elif ic2 >= size:\n            if priorities[ic1] > p:\n                i_swap = ic1\n            else:\n                break\n        elif priorities[ic1] >= priorities[ic2]:\n            if p < priorities[ic1]:\n                i_swap = ic1\n            else:\n                break\n        else:\n            if p < priorities[ic2]:\n                i_swap = ic2\n            else:\n                break\n\n        priorities[i] = priorities[i_swap]\n        indices[i] = indices[i_swap]\n        flags[i] = flags[i_swap]\n\n        i = i_swap\n\n    priorities[i] = p\n    indices[i] = n\n    flags[i] = 1\n\n    return 1\n\n\n@numba.njit(\n    numba.uint32(\n        numba.types.Tuple(\n            (numba.int32[:, ::1], numba.float32[:, ::1], numba.uint8[:, ::1])\n        ),\n        numba.float32[:, :, ::1],\n        numba.int32[::1],\n        numba.int64,\n    ),\n    parallel=True,\n    locals={\n        \"p\": numba.int32,\n        \"q\": numba.int32,\n        \"d\": numba.float32,\n        \"added\": numba.uint8,\n        \"n\": numba.uint32,\n        \"i\": numba.uint32,\n        \"j\": numba.uint32,\n        \"priorities\": numba.float32[:, ::1],\n        \"indices\": numba.int32[:, ::1],\n        \"flags\": numba.uint8[:, ::1],\n    },\n    cache=True,\n)\ndef apply_graph_update_array(\n    current_graph, update_array, n_updates_per_thread, n_threads\n):\n\n    n_changes = 0\n    priorities = current_graph[1]\n    indices = current_graph[0]\n    flags = current_graph[2]\n\n    n_vertices = priorities.shape[0]\n    block_size = n_vertices // n_threads + 1\n\n    for n in numba.prange(n_threads):\n        block_start = n * block_size\n        block_end = min(block_start + block_size, n_vertices)\n\n        for i in range(update_array.shape[0]):\n            for j in range(n_updates_per_thread[i]):\n                p = np.int32(update_array[i, j, 0])\n\n                if p == -1:\n                    break\n\n                q = np.int32(update_array[i, j, 1])\n                d = np.float32(update_array[i, j, 2])\n\n                if p >= block_start and p < block_end:\n                    added = flagged_heap_push(priorities[p], indices[p], flags[p], d, q)\n                    n_changes += added\n\n                if q >= block_start and q < block_end:\n                    added = flagged_heap_push(priorities[q], indices[q], flags[q], d, p)\n                    n_changes += added\n\n    return n_changes\n\n\n@numba.njit(\n    numba.uint32(\n        numba.types.Tuple(\n            (numba.int32[:, ::1], numba.float32[:, ::1], numba.uint8[:, ::1])\n        ),\n        numba.float32[:, :, ::1],\n        numba.int32[:, ::1],\n        numba.int64,\n    ),\n    parallel=True,\n    cache=True,\n    locals={\n        \"p\": numba.int32,\n        \"q\": numba.int32,\n        \"d\": numba.float32,\n        \"added\": numba.uint8,\n        \"n\": numba.uint32,\n        \"t\": numba.uint32,\n        \"j\": numba.uint32,\n        \"priorities\": numba.float32[:, ::1],\n        \"indices\": numba.int32[:, ::1],\n        \"flags\": numba.uint8[:, ::1],\n    },\n)\ndef apply_sorted_graph_updates(\n    current_graph, update_array, n_updates_per_block, n_threads\n):\n    \"\"\"\n    Apply pre-sorted graph updates where updates are bucketed by target block.\n\n    Each thread processes only its own bucket, avoiding the need to scan all updates.\n    This provides O(updates_per_block) work per thread instead of O(total_updates).\n    \"\"\"\n    n_changes = 0\n    priorities = current_graph[1]\n    indices = current_graph[0]\n    flags = current_graph[2]\n\n    n_vertices = priorities.shape[0]\n    vertex_block_size = n_vertices // n_threads + 1\n    max_updates_per_thread = update_array.shape[1] // n_threads\n\n    for n in numba.prange(n_threads):\n        block_start = n * vertex_block_size\n        block_end = min(block_start + vertex_block_size, n_vertices)\n\n        # Process all updates in this block's bucket\n        # Updates were written by each thread at offset t * max_updates_per_thread\n        for t in range(n_threads):\n            thread_start = t * max_updates_per_thread\n            thread_count = n_updates_per_block[n, t + 1]\n\n            for j in range(thread_count):\n                idx = thread_start + j\n                p = np.int32(update_array[n, idx, 0])\n                q = np.int32(update_array[n, idx, 1])\n                d = np.float32(update_array[n, idx, 2])\n\n                # Apply update to p if it's in this block\n                if p >= block_start and p < block_end:\n                    added = flagged_heap_push(priorities[p], indices[p], flags[p], d, q)\n                    n_changes += added\n\n                # Apply update to q if it's in this block\n                if q >= block_start and q < block_end:\n                    added = flagged_heap_push(priorities[q], indices[q], flags[q], d, p)\n                    n_changes += added\n\n    return n_changes\n"
  },
  {
    "path": "evoc/disjoint_set.py",
    "content": "import numba\nimport numpy as np\n\nfrom collections import namedtuple\n\nRankDisjointSet = namedtuple(\"RankDisjointSet\", [\"parent\", \"rank\"])\nSizeDisjointSet = namedtuple(\"SizeDisjointSet\", [\"parent\", \"size\"])\n\n_sentinel_rank_ds = RankDisjointSet(\n    parent=np.empty(1, dtype=np.int32),\n    rank=np.empty(1, dtype=np.int32),\n)\n_sentinel_size_ds = SizeDisjointSet(\n    parent=np.empty(1, dtype=np.int32),\n    size=np.empty(1, dtype=np.int32),\n)\nRankDisjointSetType = numba.typeof(_sentinel_rank_ds)\nSizeDisjointSetType = numba.typeof(_sentinel_size_ds)\n\n\n@numba.njit(cache=True)\ndef ds_rank_create(n_elements):\n    return RankDisjointSet(\n        np.arange(n_elements, dtype=np.int32), np.zeros(n_elements, dtype=np.int32)\n    )\n\n\n@numba.njit(cache=True)\ndef ds_size_create(n_elements):\n    return SizeDisjointSet(\n        np.arange(n_elements, dtype=np.int32), np.ones(n_elements, dtype=np.int32)\n    )\n\n\n@numba.njit(cache=True)\ndef ds_find(disjoint_set, x):\n    while disjoint_set.parent[x] != x:\n        x, disjoint_set.parent[x] = (\n            disjoint_set.parent[x],\n            disjoint_set.parent[disjoint_set.parent[x]],\n        )\n\n    return x\n\n\n@numba.njit(\n    numba.void(\n        RankDisjointSetType,\n        numba.int32,\n        numba.int32,\n    ),\n    cache=True,\n)\ndef ds_union_by_rank(disjoint_set, x, y):\n    x = ds_find(disjoint_set, x)\n    y = ds_find(disjoint_set, y)\n\n    if x == y:\n        return\n\n    if disjoint_set.rank[x] < disjoint_set.rank[y]:\n        x, y = y, x\n\n    disjoint_set.parent[y] = x\n    if disjoint_set.rank[x] == disjoint_set.rank[y]:\n        disjoint_set.rank[x] += 1\n\n\n@numba.njit(\n    numba.void(\n        SizeDisjointSetType,\n        numba.int32,\n        numba.int32,\n    ),\n    cache=True,\n)\ndef ds_union_by_size(disjoint_set, x, y):\n    x = ds_find(disjoint_set, x)\n    y = ds_find(disjoint_set, y)\n\n    if x == y:\n        return\n\n    if disjoint_set.size[x] < disjoint_set.size[y]:\n        x, y = y, x\n\n    disjoint_set.parent[y] = x\n    disjoint_set.size[x] += disjoint_set.size[y]\n"
  },
  {
    "path": "evoc/float_nndescent.py",
    "content": "import numba\nimport numpy as np\n\nfrom .common_nndescent import (\n    tau_rand_int,\n    make_heap,\n    deheap_sort,\n    flagged_heap_push,\n    build_candidates,\n    apply_graph_update_array,\n    apply_sorted_graph_updates,\n)\nfrom .nested_parallelism import ENABLE_NESTED_PARALLELISM\n\n# Used for a floating point \"nearly zero\" comparison\nEPS = 1e-8\nINF = np.finfo(np.float32).max\nEXP_NEG_INF = np.finfo(np.float32).tiny\nINT32_MIN = np.iinfo(np.int32).min + 1\nINT32_MAX = np.iinfo(np.int32).max - 1\n\npoint_indices_type = numba.int32[::1]\n\n\n@numba.njit(\n    [\n        \"f4(f4[::1],f4[::1])\",\n        numba.types.float32(\n            numba.types.Array(numba.types.float32, 1, \"C\", readonly=True),\n            numba.types.Array(numba.types.float32, 1, \"C\", readonly=True),\n        ),\n    ],\n    fastmath=True,\n    locals={\n        \"result\": numba.types.float32,\n        \"dim\": numba.types.intp,\n        \"i\": numba.types.uint16,\n    },\n    boundscheck=False,\n    nogil=True,\n    cache=True,\n)\ndef fast_cosine(x, y):\n    \"\"\"\n    Calculates the cosine similarity between two vectors.\n\n    Args:\n        x (numpy.ndarray): The first vector.\n        y (numpy.ndarray): The second vector.\n\n    Returns:\n        float: The cosine similarity between x and y.\n    \"\"\"\n    result = 0.0\n    dim = x.shape[0]\n    for i in range(dim):\n        result += x[i] * y[i]\n\n    if result > 0.0:\n        return -result\n    else:\n        return -EXP_NEG_INF\n\n\n@numba.njit(\n    numba.types.Tuple((numba.int32[::1], numba.int32[::1]))(\n        numba.types.Array(numba.types.float32, 2, \"C\", readonly=True),\n        numba.int32[::1],\n        numba.int64[::1],\n    ),\n    locals={\n        \"n_left\": numba.uint64,\n        \"n_right\": numba.uint64,\n        \"left_data\": numba.types.Array(numba.types.float32, 1, \"C\", readonly=True),\n        \"right_data\": numba.types.Array(numba.types.float32, 1, \"C\", readonly=True),\n        \"test_data\": numba.types.Array(numba.types.float32, 1, \"C\", readonly=True),\n        \"hyperplane_vector\": numba.float32[::1],\n        \"hyperplane_norm\": numba.float32,\n        \"margin\": numba.float32,\n        \"d\": numba.uint32,\n        \"left_index\": numba.uint32,\n        \"right_index\": numba.uint32,\n        \"point_idx\": numba.int32,\n        \"classification\": numba.int8,\n        \"max_size\": numba.uint32,\n        \"temp_left\": numba.int32[::1],\n        \"temp_right\": numba.int32[::1],\n        \"indices_size\": numba.int32,\n    },\n    fastmath=True,\n    nogil=True,\n    cache=True,\n    boundscheck=False,\n)\ndef float_random_projection_split(data, indices, rng_state):\n    \"\"\"Given a set of ``graph_indices`` for graph_data points from ``graph_data``, create\n    a random hyperplane to split the graph_data, returning two arrays graph_indices\n    that fall on either side of the hyperplane. This is the basis for a\n    random projection tree, which simply uses this splitting recursively.\n    This particular split uses cosine distance to determine the hyperplane\n    and which side each graph_data sample falls on.\n    Parameters\n    ----------\n    data: array of shape (n_samples, n_features)\n        The original graph_data to be split\n    indices: array of shape (tree_node_size,)\n        The graph_indices of the elements in the ``graph_data`` array that are to\n        be split in the current operation.\n    rng_state: array of int64, shape (3,)\n        The internal state of the rng\n    Returns\n    -------\n    indices_left: array\n        The elements of ``graph_indices`` that fall on the \"left\" side of the\n        random hyperplane.\n    indices_right: array\n        The elements of ``graph_indices`` that fall on the \"left\" side of the\n        random hyperplane.\n    \"\"\"\n    dim = data.shape[1]\n\n    # Select two random points, set the hyperplane between them\n    indices_size = np.int32(indices.shape[0])\n    left_index = tau_rand_int(rng_state) % indices_size\n    right_index = tau_rand_int(rng_state) % indices_size\n    right_index += left_index == right_index\n    right_index = right_index % indices_size\n    left = indices[left_index]\n    right = indices[right_index]\n    left_data = data[left]\n    right_data = data[right]\n\n    # Compute the normal vector to the hyperplane (the vector between\n    # the two points)\n    hyperplane_vector = np.empty(dim, dtype=np.float32)\n    hyperplane_norm = 0.0\n\n    for d in range(dim):\n        hyperplane_vector[d] = left_data[d] - right_data[d]\n        hyperplane_norm += hyperplane_vector[d] * hyperplane_vector[d]\n\n    hyperplane_norm = np.sqrt(hyperplane_norm)\n    if abs(hyperplane_norm) < EPS:\n        hyperplane_norm = 1.0\n\n    # Normalize in the same vector (avoiding second loop when possible)\n    for d in range(dim):\n        hyperplane_vector[d] /= hyperplane_norm\n\n    # Use temporary arrays sized for worst case, then trim\n    max_size = np.uint32(indices.shape[0])\n    temp_left = np.empty(max_size, dtype=np.int32)\n    temp_right = np.empty(max_size, dtype=np.int32)\n    n_left = 0\n    n_right = 0\n\n    # Single pass: classify points and directly populate result arrays\n    for idx in range(indices.shape[0]):\n        local_rng_state = rng_state + idx\n        point_idx = indices[idx]\n        test_data = data[point_idx]\n        margin = 0.0\n\n        # Compute margin (dot product with hyperplane normal)\n        for d in range(dim):\n            margin += hyperplane_vector[d] * test_data[d]\n\n        # Classify point and directly assign to appropriate array\n        if abs(margin) < EPS:\n            classification = tau_rand_int(local_rng_state) % 2\n        else:\n            classification = 0 if margin > 0 else 1\n\n        if classification == 0:\n            temp_left[n_left] = point_idx\n            n_left += 1\n        else:\n            temp_right[n_right] = point_idx\n            n_right += 1\n\n    # Handle degenerate case where all points end up on one side\n    if n_left == 0 or n_right == 0:\n        n_left = 0\n        n_right = 0\n        # Reassign randomly\n        for idx in range(indices.shape[0]):\n            point_idx = indices[idx]\n            classification = tau_rand_int(rng_state) % 2\n            if classification == 0:\n                temp_left[n_left] = point_idx\n                n_left += 1\n            else:\n                temp_right[n_right] = point_idx\n                n_right += 1\n\n    # Create final arrays with exact sizes (copy only what we need)\n    indices_left = np.empty(n_left, dtype=np.int32)\n    indices_right = np.empty(n_right, dtype=np.int32)\n\n    for i in range(n_left):\n        indices_left[i] = temp_left[i]\n    for j in range(n_right):\n        indices_right[j] = temp_right[j]\n\n    return indices_left, indices_right\n\n\n@numba.njit(\n    numba.void(\n        numba.types.Array(numba.types.float32, 2, \"C\", readonly=True),\n        numba.int32[::1],\n        numba.types.ListType(numba.int32[::1]),\n        numba.int64[::1],\n        numba.int64,\n        numba.int64,\n    ),\n    nogil=True,\n    locals={\"left_indices\": numba.int32[::1], \"right_indices\": numba.int32[::1]},\n    cache=True,\n)\ndef make_float_tree(\n    data,\n    indices,\n    point_indices,\n    rng_state,\n    leaf_size=30,\n    max_depth=200,\n):\n    \"\"\"\n    Recursively constructs a float tree for nearest neighbor descent.\n\n    Args:\n        data: The input data.\n        indices: The indices of the data points to consider.\n        point_indices: A list to store the indices of the points in each leaf node.\n        rng_state: The random number generator state.\n        leaf_size: The maximum number of points in a leaf node (default: 30).\n        max_depth: The maximum depth of the tree (default: 200).\n\n    Returns:\n        None\n    \"\"\"\n    if indices.shape[0] > leaf_size and max_depth > 0:\n        (\n            left_indices,\n            right_indices,\n        ) = float_random_projection_split(data, indices, rng_state)\n\n        make_float_tree(\n            data,\n            left_indices,\n            point_indices,\n            rng_state,\n            leaf_size,\n            max_depth - 1,\n        )\n\n        make_float_tree(\n            data,\n            right_indices,\n            point_indices,\n            rng_state,\n            leaf_size,\n            max_depth - 1,\n        )\n    else:\n        point_indices.append(indices)\n\n    return\n\n\n@numba.njit(\n    numba.int32[:, ::1](\n        numba.types.Array(numba.types.float32, 2, \"C\", readonly=True),\n        numba.int64[::1],\n        numba.int64,\n        numba.int64,\n    ),\n    nogil=True,\n    locals={\n        \"points\": numba.int32[::1],\n    },\n    parallel=True,\n    cache=True,\n)\ndef make_float_leaf_array_parallel(data, rng_state, leaf_size=30, max_depth=200):\n    indices = np.arange(data.shape[0]).astype(np.int32)\n\n    point_indices = numba.typed.List.empty_list(numba.int32[::1])\n\n    make_float_tree(\n        data,\n        indices,\n        point_indices,\n        rng_state,\n        leaf_size,\n        max_depth=max_depth,\n    )\n\n    n_leaves = numba.int64(len(point_indices))\n    max_leaf_size = numba.int32(leaf_size)\n\n    for i in numba.prange(n_leaves):\n        points = point_indices[numba.int64(i)]\n        max_leaf_size = max(max_leaf_size, numba.int32(len(points)))\n\n    result = np.full((n_leaves, max_leaf_size), -1, dtype=np.int32)\n    for i in numba.prange(n_leaves):\n        points = point_indices[numba.int64(i)]\n        n_points = numba.int32(len(points))\n        result[i, :n_points] = points\n\n    return result\n\n\n@numba.njit(\n    numba.int32[:, ::1](\n        numba.types.Array(numba.types.float32, 2, \"C\", readonly=True),\n        numba.int64[::1],\n        numba.int64,\n        numba.int64,\n    ),\n    nogil=True,\n    locals={\n        \"points\": numba.int32[::1],\n    },\n    parallel=False,\n    cache=True,\n)\ndef make_float_leaf_array_serial(data, rng_state, leaf_size=30, max_depth=200):\n    indices = np.arange(data.shape[0]).astype(np.int32)\n\n    point_indices = numba.typed.List.empty_list(numba.int32[::1])\n\n    make_float_tree(\n        data,\n        indices,\n        point_indices,\n        rng_state,\n        leaf_size,\n        max_depth=max_depth,\n    )\n\n    n_leaves = numba.int64(len(point_indices))\n\n    max_leaf_size = numba.int32(leaf_size)\n\n    for i in range(n_leaves):\n        points = point_indices[numba.int64(i)]\n        max_leaf_size = max(max_leaf_size, numba.int32(len(points)))\n\n    result = np.full((n_leaves, max_leaf_size), -1, dtype=np.int32)\n    for i in range(n_leaves):\n        points = point_indices[numba.int64(i)]\n        n_points = numba.int32(len(points))\n        result[i, :n_points] = points\n\n    return result\n\n\n@numba.njit(\n    numba.types.List(numba.int32[:, ::1])(\n        numba.types.Array(numba.types.float32, 2, \"C\", readonly=True),\n        numba.int64[:, ::1],\n        numba.uint64,\n        numba.uint64,\n    ),\n    parallel=True,\n    cache=True,\n)\ndef make_float_forest_no_nested_parallelism(data, rng_states, leaf_size, max_depth):\n    result = [np.empty((1, 1), dtype=np.int32)] * rng_states.shape[0]\n    for i in numba.prange(len(result)):\n        result[i] = make_float_leaf_array_serial(\n            data, rng_states[i], leaf_size, max_depth=max_depth\n        )\n    return result\n\n\n@numba.njit(\n    numba.types.List(numba.int32[:, ::1])(\n        numba.types.Array(numba.types.float32, 2, \"C\", readonly=True),\n        numba.int64[:, ::1],\n        numba.uint64,\n        numba.uint64,\n    ),\n    parallel=True,\n    cache=True,\n)\ndef make_float_forest_with_nested_parallelism(data, rng_states, leaf_size, max_depth):\n    result = [np.empty((1, 1), dtype=np.int32)] * rng_states.shape[0]\n    for i in numba.prange(len(result)):\n        result[i] = make_float_leaf_array_parallel(\n            data, rng_states[i], leaf_size, max_depth=max_depth\n        )\n    return result\n\n\ndef make_float_forest(data, rng_states, leaf_size=30, max_depth=200):\n    if ENABLE_NESTED_PARALLELISM:\n        return make_float_forest_with_nested_parallelism(\n            data, rng_states, leaf_size, max_depth\n        )\n    else:\n        return make_float_forest_no_nested_parallelism(\n            data, rng_states, leaf_size, max_depth\n        )\n\n\n@numba.njit(\n    numba.float32[:, :, ::1](\n        numba.float32[:, :, ::1],\n        numba.int32[::1],\n        numba.types.Array(numba.types.int32, 2, \"C\", readonly=True),\n        numba.float32[:],\n        numba.types.Array(numba.types.float32, 2, \"C\", readonly=True),\n        numba.int64,\n    ),\n    parallel=True,\n    locals={\n        \"d\": numba.float32,\n        \"p\": numba.int32,\n        \"q\": numba.int32,\n        \"t\": numba.uint16,\n        \"r\": numba.uint32,\n        \"n\": numba.uint32,\n        \"idx\": numba.uint32,\n        \"data_p\": numba.types.Array(numba.types.float32, 1, \"C\", readonly=True),\n        \"max_threshold\": numba.float32,\n    },\n    cache=True,\n)\ndef generate_leaf_updates_float(\n    updates, n_updates_per_thread, leaf_block, dist_thresholds, data, n_threads\n):\n    block_size = leaf_block.shape[0]\n    rows_per_thread = (block_size // n_threads) + 1\n\n    for t in numba.prange(n_threads):\n        idx = 0\n        for r in range(rows_per_thread):\n            n = t * rows_per_thread + r\n            if n >= block_size:\n                break\n\n            for i in range(leaf_block.shape[1]):\n                p = leaf_block[n, i]\n                if p < 0:\n                    break\n                data_p = data[p]\n                updates[t, idx, 0] = p\n                updates[t, idx, 1] = p\n                updates[t, idx, 2] = -1.0\n                idx += 1\n\n                for j in range(\n                    i + 1, leaf_block.shape[1]\n                ):  # Start from i+1 to skip self-comparison\n                    q = leaf_block[n, j]\n                    if q < 0:\n                        break\n\n                    d = fast_cosine(data_p, data[q])\n                    # Use max for better branch prediction than OR condition\n                    max_threshold = max(dist_thresholds[p], dist_thresholds[q])\n                    if d < max_threshold:\n                        updates[t, idx, 0] = p\n                        updates[t, idx, 1] = q\n                        updates[t, idx, 2] = d\n                        idx += 1\n\n        n_updates_per_thread[t] = idx\n\n    return updates\n\n\n@numba.njit(\n    [\n        numba.void(\n            numba.types.Array(numba.types.float32, 2, \"C\", readonly=True),\n            numba.types.Tuple(\n                (numba.int32[:, ::1], numba.float32[:, ::1], numba.uint8[:, ::1])\n            ),\n            numba.types.optional(\n                numba.types.Array(numba.types.int32, 2, \"C\", readonly=True)\n            ),\n            numba.types.int32,\n        ),\n    ],\n    locals={\n        \"d\": numba.float32,\n        \"p\": numba.int32,\n        \"q\": numba.int32,\n        \"i\": numba.uint16,\n        \"updates\": numba.float32[:, :, ::1],\n        \"n_updates_per_thread\": numba.int32[::1],\n    },\n    parallel=True,\n    cache=True,\n)\ndef init_rp_tree_float(data, current_graph, leaf_array, n_threads):\n    n_leaves = leaf_array.shape[0]\n    block_size = n_threads * 64\n    n_blocks = n_leaves // block_size\n\n    max_leaf_size = leaf_array.shape[1]\n    updates_per_thread = (\n        int(block_size * max_leaf_size * (max_leaf_size - 1) / (2 * n_threads)) + 1\n    )\n    updates = np.zeros((n_threads, updates_per_thread, 3), dtype=np.float32)\n    n_updates_per_thread = np.zeros(n_threads, dtype=np.int32)\n    n_vertices = current_graph[0].shape[0]\n    vertex_block_size = n_vertices // n_threads + 1\n\n    for i in range(n_blocks + 1):\n        block_start = i * block_size\n        block_end = min(n_leaves, (i + 1) * block_size)\n\n        leaf_block = leaf_array[block_start:block_end]\n        dist_thresholds = current_graph[1][:, 0]\n\n        updates = generate_leaf_updates_float(\n            updates, n_updates_per_thread, leaf_block, dist_thresholds, data, n_threads\n        )\n\n        for t in numba.prange(n_threads):\n            block_start = t * vertex_block_size\n            block_end = min(block_start + vertex_block_size, n_vertices)\n\n            for j in range(n_threads):\n                for k in range(n_updates_per_thread[j]):\n                    p = np.int32(updates[j, k, 0])\n\n                    if p == -1:\n                        continue\n\n                    q = np.int32(updates[j, k, 1])\n                    d = np.float32(updates[j, k, 2])\n\n                    if p >= block_start and p < block_end:\n                        flagged_heap_push(\n                            current_graph[1][p],\n                            current_graph[0][p],\n                            current_graph[2][p],\n                            d,\n                            q,\n                        )\n                    if q >= block_start and q < block_end:\n                        flagged_heap_push(\n                            current_graph[1][q],\n                            current_graph[0][q],\n                            current_graph[2][q],\n                            d,\n                            p,\n                        )\n\n\n@numba.njit(\n    numba.types.void(\n        numba.int32,\n        numba.types.Array(numba.types.float32, 2, \"C\", readonly=True),\n        numba.types.Tuple(\n            (numba.int32[:, ::1], numba.float32[:, ::1], numba.uint8[:, ::1])\n        ),\n        numba.int64[::1],\n    ),\n    fastmath=True,\n    parallel=True,\n    locals={\"d\": numba.float32, \"idx\": numba.int32, \"i\": numba.int32},\n    cache=True,\n)\ndef init_random_float(n_neighbors, data, heap, rng_state):\n    for i in numba.prange(data.shape[0]):\n        local_rng_state = rng_state + i\n        if heap[0][i, 0] < 0.0:\n            for j in range(n_neighbors - np.sum(heap[0][i] >= 0.0)):\n                idx = np.abs(tau_rand_int(local_rng_state)) % data.shape[0]\n                if idx in heap[0][i]:\n                    continue\n                d = fast_cosine(data[idx], data[i])\n                flagged_heap_push(heap[1][i], heap[0][i], heap[2][i], d, idx)\n\n    return\n\n\n@numba.njit(\n    numba.types.void(\n        numba.float32[:, :, ::1],\n        numba.int32[::1],\n        numba.int32[:, ::1],\n        numba.int32[:, ::1],\n        numba.float32[:],\n        numba.types.Array(numba.types.float32, 2, \"C\", readonly=True),\n        numba.int64,\n    ),\n    locals={\n        \"data_p\": numba.types.Array(numba.types.float32, 1, \"C\", readonly=True),\n        \"dist_thresh_p\": numba.float32,\n        \"dist_thresh_q\": numba.float32,\n        \"p\": numba.int32,\n        \"q\": numba.int32,\n        \"d\": numba.float32,\n        \"max_updates\": numba.int32,\n        \"threshold_check\": numba.boolean,\n        \"max_threshold\": numba.float32,\n    },\n    parallel=True,\n    cache=True,\n    fastmath=True,\n    boundscheck=False,\n)\ndef generate_graph_update_array_float_basic(\n    update_array,\n    n_updates_per_thread,\n    new_candidate_block,\n    old_candidate_block,\n    dist_thresholds,\n    data,\n    n_threads,\n):\n    \"\"\"\n    Basic optimized version with aggressive optimizations but without cache-specific enhancements.\n    Kept for comparison and benchmarking purposes.\n    \"\"\"\n    block_size = new_candidate_block.shape[0]\n    max_new_candidates = new_candidate_block.shape[1]\n    max_old_candidates = old_candidate_block.shape[1]\n    rows_per_thread = (block_size // n_threads) + 1\n\n    for t in numba.prange(n_threads):\n        idx = 0\n        max_updates = update_array.shape[1]\n\n        for r in range(rows_per_thread):\n            i = t * rows_per_thread + r\n            if i >= block_size or idx >= max_updates:\n                break\n\n            for j in range(max_new_candidates):\n                if idx >= max_updates:\n                    break\n\n                p = new_candidate_block[i, j]\n                if p < 0:\n                    continue\n                data_p = data[p]\n                dist_thresh_p = dist_thresholds[p]\n\n                for k in range(j + 1, max_new_candidates):\n                    if idx >= max_updates:\n                        break\n\n                    q = new_candidate_block[i, k]\n                    if q < 0:\n                        continue\n\n                    # Compute distance once\n                    d = fast_cosine(data_p, data[q])\n\n                    # Use max for better branch prediction than OR condition\n                    dist_thresh_q = dist_thresholds[q]\n                    max_threshold = max(dist_thresh_p, dist_thresh_q)\n                    threshold_check = d <= max_threshold\n\n                    if threshold_check:\n                        update_array[t, idx, 0] = p\n                        update_array[t, idx, 1] = q\n                        update_array[t, idx, 2] = d\n                        idx += 1\n\n                for k in range(max_old_candidates):\n                    if idx >= max_updates:\n                        break\n\n                    q = old_candidate_block[i, k]\n                    if q < 0:\n                        continue\n\n                    d = fast_cosine(data_p, data[q])\n                    dist_thresh_q = dist_thresholds[q]\n                    max_threshold = max(dist_thresh_p, dist_thresh_q)\n                    threshold_check = d <= max_threshold\n\n                    if threshold_check:\n                        update_array[t, idx, 0] = p\n                        update_array[t, idx, 1] = q\n                        update_array[t, idx, 2] = d\n                        idx += 1\n\n        n_updates_per_thread[t] = idx\n\n\n@numba.njit(\n    numba.void(\n        numba.float32[:, :, ::1],\n        numba.int32[::1],\n        numba.int32[:, ::1],\n        numba.int32[:, ::1],\n        numba.float32[:],\n        numba.types.Array(numba.types.float32, 2, \"C\", readonly=True),\n        numba.int64,\n    ),\n    locals={\n        \"data_p\": numba.types.Array(numba.types.float32, 1, \"C\", readonly=True),\n        \"dist_thresh_p\": numba.float32,\n        \"dist_thresh_q\": numba.float32,\n        \"p\": numba.int32,\n        \"q\": numba.int32,\n        \"d\": numba.float32,\n        \"max_updates\": numba.int32,\n        \"threshold_check\": numba.boolean,\n        \"working_set_size\": numba.int32,\n        \"batch_start\": numba.int32,\n        \"batch_end\": numba.int32,\n        \"max_threshold\": numba.float32,\n    },\n    parallel=True,\n    cache=True,\n    fastmath=True,\n    boundscheck=False,\n)\ndef generate_graph_update_array_float(\n    update_array,\n    n_updates_per_thread,\n    new_candidate_block,\n    old_candidate_block,\n    dist_thresholds,\n    data,\n    n_threads,\n):\n    \"\"\"\n    Optimized version using working set approach that processes candidates in small groups\n    that fit well in CPU cache. This reduces cache misses by keeping frequently\n    accessed data vectors in cache longer, providing the best performance for typical workloads.\n    \"\"\"\n    block_size = new_candidate_block.shape[0]\n    max_new_candidates = new_candidate_block.shape[1]\n    max_old_candidates = old_candidate_block.shape[1]\n    rows_per_thread = (block_size // n_threads) + 1\n\n    # Working set size - process this many candidates at a time\n    # Tuned for typical L1/L2 cache sizes (adjust based on data dimensionality)\n    working_set_size = 8\n\n    for t in numba.prange(n_threads):\n        idx = 0\n        max_updates = update_array.shape[1]\n\n        for r in range(rows_per_thread):\n            i = t * rows_per_thread + r\n            if i >= block_size or idx >= max_updates:\n                break\n\n            # Process new candidates in working set chunks\n            new_start = 0\n            while new_start < max_new_candidates and idx < max_updates:\n                new_end = min(new_start + working_set_size, max_new_candidates)\n\n                # Process pairs within this working set\n                for j in range(new_start, new_end):\n                    if idx >= max_updates:\n                        break\n\n                    p = new_candidate_block[i, j]\n                    if p < 0:\n                        continue\n\n                    data_p = data[p]\n                    dist_thresh_p = dist_thresholds[p]\n\n                    # Compare with other candidates in the same working set\n                    for k in range(j + 1, new_end):\n                        if idx >= max_updates:\n                            break\n\n                        q = new_candidate_block[i, k]\n                        if q < 0:\n                            continue\n\n                        d = fast_cosine(data_p, data[q])\n                        dist_thresh_q = dist_thresholds[q]\n                        max_threshold = max(dist_thresh_p, dist_thresh_q)\n                        threshold_check = d <= max_threshold\n\n                        if threshold_check:\n                            update_array[t, idx, 0] = p\n                            update_array[t, idx, 1] = q\n                            update_array[t, idx, 2] = d\n                            idx += 1\n\n                    # Compare with candidates in future working sets\n                    for k in range(new_end, max_new_candidates):\n                        if idx >= max_updates:\n                            break\n\n                        q = new_candidate_block[i, k]\n                        if q < 0:\n                            continue\n\n                        d = fast_cosine(data_p, data[q])\n                        dist_thresh_q = dist_thresholds[q]\n                        max_threshold = max(dist_thresh_p, dist_thresh_q)\n                        threshold_check = d <= max_threshold\n\n                        if threshold_check:\n                            update_array[t, idx, 0] = p\n                            update_array[t, idx, 1] = q\n                            update_array[t, idx, 2] = d\n                            idx += 1\n\n                    # Compare with old candidates in working set chunks\n                    old_start = 0\n                    while old_start < max_old_candidates and idx < max_updates:\n                        old_end = min(old_start + working_set_size, max_old_candidates)\n\n                        for k in range(old_start, old_end):\n                            if idx >= max_updates:\n                                break\n\n                            q = old_candidate_block[i, k]\n                            if q < 0:\n                                continue\n\n                            d = fast_cosine(data_p, data[q])\n                            dist_thresh_q = dist_thresholds[q]\n                            max_threshold = max(dist_thresh_p, dist_thresh_q)\n                            threshold_check = d <= max_threshold\n\n                            if threshold_check:\n                                update_array[t, idx, 0] = p\n                                update_array[t, idx, 1] = q\n                                update_array[t, idx, 2] = d\n                                idx += 1\n\n                        old_start = old_end\n\n                new_start = new_end\n\n        n_updates_per_thread[t] = idx\n\n\n@numba.njit(\n    numba.void(\n        numba.float32[:, :, ::1],\n        numba.int32[:, ::1],\n        numba.int32[:, ::1],\n        numba.int32[:, ::1],\n        numba.float32[:],\n        numba.types.Array(numba.types.float32, 2, \"C\", readonly=True),\n        numba.int64,\n    ),\n    locals={\n        \"data_p\": numba.types.Array(numba.types.float32, 1, \"C\", readonly=True),\n        \"dist_thresh_p\": numba.float32,\n        \"dist_thresh_q\": numba.float32,\n        \"p\": numba.int32,\n        \"q\": numba.int32,\n        \"d\": numba.float32,\n        \"max_updates\": numba.intp,\n        \"threshold_check\": numba.boolean,\n        \"max_threshold\": numba.float32,\n        \"p_block\": numba.int32,\n        \"q_block\": numba.int32,\n        \"p_idx\": numba.int32,\n        \"q_idx\": numba.int32,\n    },\n    parallel=True,\n    cache=True,\n    fastmath=True,\n    boundscheck=False,\n)\ndef generate_sorted_graph_update_array_float(\n    update_array,\n    n_updates_per_block,\n    new_candidate_block,\n    old_candidate_block,\n    dist_thresholds,\n    data,\n    n_threads,\n):\n    \"\"\"\n    Generate graph updates pre-sorted by target block.\n\n    Updates are bucketed by their target vertex block so that apply_sorted_graph_updates\n    can process each bucket with perfect data locality and no wasted iteration.\n\n    Each update (p, q, d) is placed in BOTH p's bucket and q's bucket (if different),\n    ensuring that each block has all updates it needs to process.\n\n    The update_array has shape (n_threads, max_updates_per_block, 3) where:\n    - First dimension indexes the target block\n    - update_array[block, idx, 0] = p (first endpoint)\n    - update_array[block, idx, 1] = q (second endpoint)\n    - update_array[block, idx, 2] = d (distance)\n    \"\"\"\n    block_size_candidates = new_candidate_block.shape[0]\n    max_new_candidates = new_candidate_block.shape[1]\n    max_old_candidates = old_candidate_block.shape[1]\n    rows_per_thread = (block_size_candidates // n_threads) + 1\n\n    n_vertices = data.shape[0]\n    vertex_block_size = n_vertices // n_threads + 1\n    max_updates = update_array.shape[1]\n    max_updates_per_src_thread = max_updates // n_threads\n\n    # Reset update counts\n    for b in numba.prange(n_threads):\n        for t in range(n_threads + 1):\n            n_updates_per_block[b, t] = 0\n\n    # Each thread generates updates and places them in appropriate buckets\n    for t in numba.prange(n_threads):\n        # Thread-local counters for each bucket\n        local_counts = np.zeros(n_threads, dtype=np.int32)\n\n        for r in range(rows_per_thread):\n            i = t * rows_per_thread + r\n            if i >= block_size_candidates:\n                break\n\n            for j in range(max_new_candidates):\n                p = new_candidate_block[i, j]\n                if p < 0:\n                    continue\n\n                data_p = data[p]\n                dist_thresh_p = dist_thresholds[p]\n                p_block = p // vertex_block_size\n                if p_block >= n_threads:\n                    p_block = n_threads - 1\n\n                # Compare with other new candidates\n                for k in range(j + 1, max_new_candidates):\n                    q = new_candidate_block[i, k]\n                    if q < 0:\n                        continue\n\n                    d = fast_cosine(data_p, data[q])\n                    dist_thresh_q = dist_thresholds[q]\n                    max_threshold = max(dist_thresh_p, dist_thresh_q)\n\n                    if d <= max_threshold:\n                        q_block = q // vertex_block_size\n                        if q_block >= n_threads:\n                            q_block = n_threads - 1\n\n                        # Place update in p's bucket\n                        bucket_idx = local_counts[p_block]\n                        write_idx = t * max_updates_per_src_thread + bucket_idx\n                        if write_idx < max_updates:\n                            update_array[p_block, write_idx, 0] = p\n                            update_array[p_block, write_idx, 1] = q\n                            update_array[p_block, write_idx, 2] = d\n                            local_counts[p_block] += 1\n\n                        # If q is in a different block, also place in q's bucket\n                        if q_block != p_block:\n                            bucket_idx = local_counts[q_block]\n                            write_idx = t * max_updates_per_src_thread + bucket_idx\n                            if write_idx < max_updates:\n                                update_array[q_block, write_idx, 0] = p\n                                update_array[q_block, write_idx, 1] = q\n                                update_array[q_block, write_idx, 2] = d\n                                local_counts[q_block] += 1\n\n                # Compare with old candidates\n                for k in range(max_old_candidates):\n                    q = old_candidate_block[i, k]\n                    if q < 0:\n                        continue\n\n                    d = fast_cosine(data_p, data[q])\n                    dist_thresh_q = dist_thresholds[q]\n                    max_threshold = max(dist_thresh_p, dist_thresh_q)\n\n                    if d <= max_threshold:\n                        q_block = q // vertex_block_size\n                        if q_block >= n_threads:\n                            q_block = n_threads - 1\n\n                        # Place update in p's bucket\n                        bucket_idx = local_counts[p_block]\n                        write_idx = t * max_updates_per_src_thread + bucket_idx\n                        if write_idx < max_updates:\n                            update_array[p_block, write_idx, 0] = p\n                            update_array[p_block, write_idx, 1] = q\n                            update_array[p_block, write_idx, 2] = d\n                            local_counts[p_block] += 1\n\n                        # If q is in a different block, also place in q's bucket\n                        if q_block != p_block:\n                            bucket_idx = local_counts[q_block]\n                            write_idx = t * max_updates_per_src_thread + bucket_idx\n                            if write_idx < max_updates:\n                                update_array[q_block, write_idx, 0] = p\n                                update_array[q_block, write_idx, 1] = q\n                                update_array[q_block, write_idx, 2] = d\n                                local_counts[q_block] += 1\n\n        # Record total updates generated by this thread for each bucket\n        for b in range(n_threads):\n            n_updates_per_block[b, t + 1] = local_counts[b]\n\n\ndef nn_descent_float(\n    data,\n    n_neighbors,\n    rng_state,\n    max_candidates=50,\n    n_iters=10,\n    delta=0.001,\n    delta_improv=None,\n    leaf_array=None,\n    verbose=False,\n):\n    \"\"\"\n    Perform approximate nearest neighbor descent algorithm using float data.\n\n    Parameters:\n    - data: The input data array.\n    - n_neighbors: The number of nearest neighbors to search for.\n    - rng_state: The random number generator state.\n    - max_candidates: The maximum number of candidates to consider during the search. Default is 50.\n    - n_iters: The number of iterations to perform. Default is 10.\n    - delta: The stopping threshold based on update count. Default is 0.001.\n    - delta_improv: Optional stopping threshold based on relative improvement in total\n        graph distance. When set (e.g., 0.001 for 0.1%), the algorithm will also\n        terminate when the relative improvement in sum of all distances drops below\n        this threshold. This can provide earlier termination on data with good\n        structure, adapting to the intrinsic difficulty of the dataset. Default is None\n        (disabled).\n    - leaf_array: The array representing the leaf structure of the RP-tree. Default is None.\n    - verbose: Whether to print progress information. Default is False.\n\n    Returns:\n    - The sorted nearest neighbor graph.\n    \"\"\"\n    n_threads = numba.get_num_threads()\n    current_graph = make_heap(data.shape[0], n_neighbors)\n    init_rp_tree_float(data, current_graph, leaf_array, n_threads)\n    init_random_float(n_neighbors, data, current_graph, rng_state)\n\n    n_vertices = data.shape[0]\n    n_threads = numba.get_num_threads()\n    block_size = 65536 // n_threads\n    n_blocks = n_vertices // block_size\n\n    max_updates_per_thread = int(\n        ((max_candidates**2 + max_candidates * (max_candidates - 1) / 2) * block_size)\n    )\n    update_array = np.empty((n_threads, max_updates_per_thread, 3), dtype=np.float32)\n    n_updates_per_thread = np.zeros(n_threads, dtype=np.int32)\n\n    # For distance-based termination\n    prev_sum_dist = None\n\n    for n in range(n_iters):\n        if verbose:\n            print(\"\\t\", n + 1, \" / \", n_iters)\n\n        (new_candidate_neighbors, old_candidate_neighbors) = build_candidates(\n            current_graph, max_candidates, rng_state, n_threads\n        )\n\n        c = 0\n        n_vertices = new_candidate_neighbors.shape[0]\n        for i in range(n_blocks + 1):\n            block_start = i * block_size\n            block_end = min(n_vertices, (i + 1) * block_size)\n\n            new_candidate_block = new_candidate_neighbors[block_start:block_end]\n            old_candidate_block = old_candidate_neighbors[block_start:block_end]\n\n            dist_thresholds = current_graph[1][:, 0]\n\n            generate_graph_update_array_float(\n                update_array,\n                n_updates_per_thread,\n                new_candidate_block,\n                old_candidate_block,\n                dist_thresholds,\n                data,\n                n_threads,\n            )\n\n            c += apply_graph_update_array(\n                current_graph, update_array, n_updates_per_thread, n_threads\n            )\n\n        # Check update count termination\n        if c <= delta * n_neighbors * data.shape[0]:\n            if verbose:\n                print(\"\\tStopping threshold met -- exiting after\", n + 1, \"iterations\")\n            return deheap_sort(current_graph[0], current_graph[1])\n\n        # Check distance improvement termination (if enabled)\n        if delta_improv is not None:\n            all_distances = current_graph[1]\n            valid_mask = all_distances < INF\n            sum_dist = np.sum(all_distances[valid_mask])\n\n            if prev_sum_dist is not None:\n                rel_improv = abs(sum_dist - prev_sum_dist) / abs(prev_sum_dist)\n                if rel_improv < delta_improv:\n                    if verbose:\n                        print(\n                            f\"\\tDistance improvement threshold met ({rel_improv:.4%} < {delta_improv:.4%})\"\n                            f\" -- exiting after {n + 1} iterations\"\n                        )\n                    return deheap_sort(current_graph[0], current_graph[1])\n\n            prev_sum_dist = sum_dist\n\n        block_size = min(n_vertices, 2 * block_size)\n        n_blocks = n_vertices // block_size\n\n    return deheap_sort(current_graph[0], current_graph[1])\n\n\ndef nn_descent_float_sorted(\n    data,\n    n_neighbors,\n    rng_state,\n    max_candidates=50,\n    n_iters=10,\n    delta=0.001,\n    delta_improv=None,\n    leaf_array=None,\n    verbose=False,\n):\n    \"\"\"\n    Perform approximate nearest neighbor descent algorithm using float data.\n\n    This version uses pre-sorted updates bucketed by target block for potentially\n    better performance when n_threads is large. Each thread only processes updates\n    targeting its own vertex block.\n\n    Parameters:\n    - data: The input data array.\n    - n_neighbors: The number of nearest neighbors to search for.\n    - rng_state: The random number generator state.\n    - max_candidates: The maximum number of candidates to consider during the search. Default is 50.\n    - n_iters: The number of iterations to perform. Default is 10.\n    - delta: The stopping threshold based on update count. Default is 0.001.\n    - delta_improv: Optional stopping threshold based on relative improvement in total\n        graph distance. When set (e.g., 0.001 for 0.1%), the algorithm will also\n        terminate when the relative improvement in sum of all distances drops below\n        this threshold. This can provide earlier termination on data with good\n        structure, adapting to the intrinsic difficulty of the dataset. Default is None\n        (disabled).\n    - leaf_array: The array representing the leaf structure of the RP-tree. Default is None.\n    - verbose: Whether to print progress information. Default is False.\n\n    Returns:\n    - The sorted nearest neighbor graph.\n    \"\"\"\n    n_threads = numba.get_num_threads()\n    current_graph = make_heap(data.shape[0], n_neighbors)\n    init_rp_tree_float(data, current_graph, leaf_array, n_threads)\n    init_random_float(n_neighbors, data, current_graph, rng_state)\n\n    n_vertices = data.shape[0]\n    n_threads = numba.get_num_threads()\n    block_size = 65536 // n_threads\n    n_blocks = n_vertices // block_size\n\n    max_updates_per_thread = int(\n        ((max_candidates**2 + max_candidates * (max_candidates - 1) / 2) * block_size)\n    )\n    # For sorted updates: shape is (n_threads, max_updates_per_block, 3)\n    # Each bucket (first dim) holds updates targeting that block\n    sorted_update_array = np.empty(\n        (n_threads, max_updates_per_thread, 3), dtype=np.float32\n    )\n    # Track updates per block, with per-thread breakdown: (n_threads, n_threads + 1)\n    # Column 0 is unused, columns 1..n_threads store count from each generating thread\n    n_updates_per_block = np.zeros((n_threads, n_threads + 1), dtype=np.int32)\n\n    # For distance-based termination\n    prev_sum_dist = None\n\n    for n in range(n_iters):\n        if verbose:\n            print(\"\\t\", n + 1, \" / \", n_iters)\n\n        (new_candidate_neighbors, old_candidate_neighbors) = build_candidates(\n            current_graph, max_candidates, rng_state, n_threads\n        )\n\n        c = 0\n        n_vertices = new_candidate_neighbors.shape[0]\n        for i in range(n_blocks + 1):\n            block_start = i * block_size\n            block_end = min(n_vertices, (i + 1) * block_size)\n\n            new_candidate_block = new_candidate_neighbors[block_start:block_end]\n            old_candidate_block = old_candidate_neighbors[block_start:block_end]\n\n            dist_thresholds = current_graph[1][:, 0]\n\n            # Reset update counts for this iteration\n            n_updates_per_block.fill(0)\n\n            generate_sorted_graph_update_array_float(\n                sorted_update_array,\n                n_updates_per_block,\n                new_candidate_block,\n                old_candidate_block,\n                dist_thresholds,\n                data,\n                n_threads,\n            )\n\n            c += apply_sorted_graph_updates(\n                current_graph, sorted_update_array, n_updates_per_block, n_threads\n            )\n\n        # Check update count termination\n        if c <= delta * n_neighbors * data.shape[0]:\n            if verbose:\n                print(\"\\tStopping threshold met -- exiting after\", n + 1, \"iterations\")\n            return deheap_sort(current_graph[0], current_graph[1])\n\n        # Check distance improvement termination (if enabled)\n        if delta_improv is not None:\n            all_distances = current_graph[1]\n            valid_mask = all_distances < INF\n            sum_dist = np.sum(all_distances[valid_mask])\n\n            if prev_sum_dist is not None:\n                rel_improv = abs(sum_dist - prev_sum_dist) / abs(prev_sum_dist)\n                if rel_improv < delta_improv:\n                    if verbose:\n                        print(\n                            f\"\\tDistance improvement threshold met ({rel_improv:.4%} < {delta_improv:.4%})\"\n                            f\" -- exiting after {n + 1} iterations\"\n                        )\n                    return deheap_sort(current_graph[0], current_graph[1])\n\n            prev_sum_dist = sum_dist\n\n        block_size = min(n_vertices, 2 * block_size)\n        n_blocks = n_vertices // block_size\n\n    return deheap_sort(current_graph[0], current_graph[1])\n"
  },
  {
    "path": "evoc/graph_construction.py",
    "content": "import numpy as np\nimport numba\n\nfrom scipy.sparse import coo_array\n\nINT32_MIN = np.iinfo(np.int32).min + 1\nINT32_MAX = np.iinfo(np.int32).max - 1\nSMOOTH_K_TOLERANCE = 1e-5\nMIN_K_DIST_SCALE = 1e-3\nNPY_INFINITY = np.inf\n\n\n@numba.njit(\n    locals={\n        \"psum\": numba.types.float32,\n        \"lo\": numba.types.float32,\n        \"mid\": numba.types.float32,\n        \"hi\": numba.types.float32,\n    },\n    fastmath=True,\n    parallel=True,\n    cache=True,\n)\ndef smooth_knn_dist(distances, k, n_iter=64, bandwidth=1.0):\n    target = np.log2(k) * bandwidth\n    rho = np.zeros(distances.shape[0], dtype=np.float32)\n    sigma = np.zeros(distances.shape[0], dtype=np.float32)\n\n    mean_distances = np.mean(distances)\n\n    for i in numba.prange(distances.shape[0]):\n        lo = 0.0\n        hi = NPY_INFINITY\n        mid = 1.0\n\n        ith_distances = distances[i]\n        non_zero_dists = ith_distances[ith_distances > 0.0]\n        if non_zero_dists.shape[0] >= 1:\n            rho[i] = non_zero_dists[0]\n\n        for n in range(n_iter):\n\n            psum = 0.0\n            for j in range(1, distances.shape[1]):\n                d = distances[i, j] - rho[i]\n                if d > 0:\n                    psum += np.exp(-(d / mid))\n                else:\n                    psum += 1.0\n\n            if np.fabs(psum - target) < SMOOTH_K_TOLERANCE:\n                break\n\n            if psum > target:\n                hi = mid\n                mid = (lo + hi) / 2.0\n            else:\n                lo = mid\n                if hi == NPY_INFINITY:\n                    mid *= 2\n                else:\n                    mid = (lo + hi) / 2.0\n\n        sigma[i] = mid\n\n        if rho[i] > 0.0:\n            mean_ith_distances = np.mean(ith_distances)\n            if sigma[i] < MIN_K_DIST_SCALE * mean_ith_distances:\n                sigma[i] = MIN_K_DIST_SCALE * mean_ith_distances\n        else:\n            if sigma[i] < MIN_K_DIST_SCALE * mean_distances:\n                sigma[i] = MIN_K_DIST_SCALE * mean_distances\n\n    return sigma, rho\n\n\n@numba.njit(\n    locals={\n        \"knn_dists\": numba.types.float32[:, ::1],\n        \"sigmas\": numba.types.float32[::1],\n        \"rhos\": numba.types.float32[::1],\n        \"sigma\": numba.types.float32,\n        \"rho\": numba.types.float32,\n        \"val\": numba.types.float32,\n    },\n    parallel=True,\n    fastmath=True,\n    cache=True,\n)\ndef compute_membership_strengths(\n    knn_indices,\n    knn_dists,\n    sigmas,\n    rhos,\n):\n    n_samples = knn_indices.shape[0]\n    n_neighbors = knn_indices.shape[1]\n\n    rows = np.zeros(knn_indices.size, dtype=np.int32)\n    cols = np.zeros(knn_indices.size, dtype=np.int32)\n    vals = np.zeros(knn_indices.size, dtype=np.float32)\n\n    for i in range(n_samples):\n        rho = rhos[i]\n        sigma = sigmas[i]\n        for j in range(n_neighbors):\n            idx = knn_indices[i, j]\n            if idx == -1:\n                continue  # We didn't get the full knn for i\n            elif idx == i:\n                val = 0.0\n            elif (knn_dists[i, j] - rho) <= 0.0 or sigma == 0.0:\n                val = 1.0\n            else:\n                val = np.exp(-((knn_dists[i, j] - rhos[i]) / (sigma)))\n\n            rows[i * n_neighbors + j] = i\n            cols[i * n_neighbors + j] = idx\n            vals[i * n_neighbors + j] = val\n\n    return rows, cols, vals\n\n\ndef neighbor_graph_matrix(\n    n_neighbors,\n    knn_indices,\n    knn_dists,\n    symmetrize=True,\n):\n    \"\"\"Construct a sparse graph from k-nearest neighbor distances.\n\n    Converts k-nearest neighbor indices and distances into a weighted sparse graph\n    matrix using Gaussian kernel weights. Optionally symmetrizes the graph to\n    create an undirected graph.\n\n    Parameters\n    ----------\n    n_neighbors : float\n        The effective number of neighbors. Used in the kernel width (sigma)\n        computation via the smooth_knn_dist function.\n\n    knn_indices : array-like of shape (n_samples, k)\n        The indices of the k-nearest neighbors for each sample.\n\n    knn_dists : array-like of shape (n_samples, k)\n        The distances from each sample to its k-nearest neighbors.\n\n    symmetrize : bool, default=True\n        If True, the graph is symmetrized using the formula:\n        A_sym = A + A^T - A * A^T (union of forward and reverse edges).\n        If False, the graph remains directed (asymmetric).\n\n    Returns\n    -------\n    graph : scipy.sparse._csr_matrix or scipy.sparse._coo_matrix\n        A sparse matrix representing the weighted nearest neighbor graph.\n        The (i, j) entry contains the Gaussian kernel weight from sample i to\n        sample j, or 0 if j is not in the k-nearest neighbors of i.\n        If symmetrize=True, the matrix is symmetric and in CSR format.\n        If symmetrize=False, returns a CSR matrix (asymmetric).\n    \"\"\"\n    knn_dists = knn_dists.astype(np.float32)\n\n    sigmas, rhos = smooth_knn_dist(\n        knn_dists,\n        float(n_neighbors),\n    )\n\n    rows, cols, vals = compute_membership_strengths(\n        knn_indices, knn_dists, sigmas, rhos\n    )\n\n    result = coo_array(\n        (vals, (rows, cols)),\n        shape=(knn_indices.shape[0], knn_indices.shape[0]),\n        dtype=np.float32,\n    )\n    result.eliminate_zeros()\n\n    if symmetrize:\n        transpose = result.transpose()\n\n        prod_matrix = result.multiply(transpose)\n        result = result + transpose - prod_matrix\n    else:\n        result = result.tocsr()\n\n    result.eliminate_zeros()\n\n    return result\n"
  },
  {
    "path": "evoc/int8_nndescent.py",
    "content": "import numba\nimport numpy as np\n\nfrom .common_nndescent import (\n    tau_rand_int,\n    make_heap,\n    deheap_sort,\n    flagged_heap_push,\n    build_candidates,\n    apply_graph_update_array,\n    apply_sorted_graph_updates,\n)\nfrom .nested_parallelism import ENABLE_NESTED_PARALLELISM\n\n# Used for a floating point \"nearly zero\" comparison\nEPS = 1e-8\nINT32_MIN = np.iinfo(np.int32).min + 1\nINT32_MAX = np.iinfo(np.int32).max - 1\nINF = np.float32(np.inf)\n\n\n@numba.njit(\n    [\n        \"f4(i1[::1],i1[::1])\",\n        numba.types.float32(\n            numba.types.Array(numba.types.int8, 1, \"C\", readonly=True),\n            numba.types.Array(numba.types.int8, 1, \"C\", readonly=True),\n        ),\n    ],\n    fastmath=True,\n    boundscheck=False,\n    nogil=True,\n    locals={\n        \"result\": numba.types.int32,\n        \"dim\": numba.types.intp,\n        \"i\": numba.types.uint16,\n    },\n    cache=True,\n)\ndef fast_int_inner_product_dissimilarity(x, y):\n    result = np.int32(0)\n    dim = x.shape[0]\n\n    for i in range(dim):\n        result += np.int32(x[i]) * np.int32(y[i])\n\n    return -np.float32(result)\n\n\n@numba.njit(\n    numba.types.Tuple((numba.int32[::1], numba.int32[::1]))(\n        numba.types.Array(numba.types.int8, 2, \"C\", readonly=True),\n        numba.int32[::1],\n        numba.int64[::1],\n    ),\n    locals={\n        \"n_left\": numba.uint32,\n        \"n_right\": numba.uint32,\n        \"left_data\": numba.types.Array(numba.types.int8, 1, \"C\", readonly=True),\n        \"right_data\": numba.types.Array(numba.types.int8, 1, \"C\", readonly=True),\n        \"test_data\": numba.types.Array(numba.types.int8, 1, \"C\", readonly=True),\n        \"hyperplane_vector\": numba.float32[::1],\n        \"margin\": numba.float32,\n        \"d\": numba.uint32,\n        \"i\": numba.uint32,\n        \"left_index\": numba.uint32,\n        \"right_index\": numba.uint32,\n    },\n    fastmath=True,\n    nogil=True,\n    cache=True,\n)\ndef int8_random_projection_split(data, indices, rng_state):\n    \"\"\"Given a set of ``graph_indices`` for graph_data points from ``graph_data``, create\n    a random hyperplane to split the graph_data, returning two arrays graph_indices\n    that fall on either side of the hyperplane. This is the basis for a\n    random projection tree, which simply uses this splitting recursively.\n    This particular split uses cosine distance to determine the hyperplane\n    and which side each graph_data sample falls on.\n    Parameters\n    ----------\n    data: array of shape (n_samples, n_features)\n        The original graph_data to be split\n    indices: array of shape (tree_node_size,)\n        The graph_indices of the elements in the ``graph_data`` array that are to\n        be split in the current operation.\n    rng_state: array of int64, shape (3,)\n        The internal state of the rng\n    Returns\n    -------\n    indices_left: array\n        The elements of ``graph_indices`` that fall on the \"left\" side of the\n        random hyperplane.\n    indices_right: array\n        The elements of ``graph_indices`` that fall on the \"left\" side of the\n        random hyperplane.\n    \"\"\"\n    dim = data.shape[1]\n\n    # Select two random points, set the hyperplane between them\n    left_index = tau_rand_int(rng_state) % indices.shape[0]\n    right_index = tau_rand_int(rng_state) % indices.shape[0]\n    right_index += left_index == right_index\n    right_index = right_index % indices.shape[0]\n    left = indices[left_index]\n    right = indices[right_index]\n\n    left_data = data[left]\n    right_data = data[right]\n\n    left_norm = 0.0\n    right_norm = 0.0\n    for d in range(dim):\n        left_norm += left_data[d] * left_data[d]\n        right_norm += right_data[d] * right_data[d]\n\n    left_norm = np.sqrt(left_norm)\n    right_norm = np.sqrt(right_norm)\n\n    # Compute the normal vector to the hyperplane (the vector between\n    # the two points)\n    hyperplane_vector = np.empty(dim, dtype=np.float32)\n    hyperplane_norm = 0.0\n\n    for d in range(dim):\n        hyperplane_vector[d] = (left_data[d] / left_norm) - (right_data[d] / right_norm)\n        hyperplane_norm += hyperplane_vector[d] * hyperplane_vector[d]\n    hyperplane_norm = np.sqrt(hyperplane_norm)\n\n    # hyperplane_norm = norm(hyperplane_vector)\n    if abs(hyperplane_norm) < EPS:\n        hyperplane_norm = 1.0\n\n    for d in range(dim):\n        hyperplane_vector[d] /= hyperplane_norm\n\n    # For each point compute the margin (project into normal vector)\n    # If we are on lower side of the hyperplane put in one pile, otherwise\n    # put it in the other pile (if we hit hyperplane on the nose, flip a coin)\n    n_left = 0\n    n_right = 0\n    side = np.empty(indices.shape[0], np.bool_)\n    for i in range(indices.shape[0]):\n        margin = 0.0\n        local_rng_state = rng_state + np.int64(i)\n        test_data = data[indices[i]]\n        for d in range(dim):\n            margin += hyperplane_vector[d] * test_data[d]\n\n        if abs(margin) < EPS:\n            side[i] = np.bool_(tau_rand_int(local_rng_state) % 2)\n            if side[i] == 0:\n                n_left += 1\n            else:\n                n_right += 1\n        elif margin > 0:\n            side[i] = 0\n            n_left += 1\n        else:\n            side[i] = 1\n            n_right += 1\n\n    # If all points end up on one side, something went wrong numerically\n    # In this case, assign points randomly; they are likely very close anyway\n    if n_left == 0 or n_right == 0:\n        n_left = 0\n        n_right = 0\n        for i in range(indices.shape[0]):\n            side[i] = tau_rand_int(rng_state) % 2\n            if side[i] == 0:\n                n_left += 1\n            else:\n                n_right += 1\n\n    # Now that we have the counts allocate arrays\n    indices_left = np.empty(n_left, dtype=np.int32)\n    indices_right = np.empty(n_right, dtype=np.int32)\n\n    # Populate the arrays with graph_indices according to which side they fell on\n    n_left = 0\n    n_right = 0\n    for i in range(side.shape[0]):\n        if side[i] == 0:\n            indices_left[n_left] = indices[i]\n            n_left += 1\n        else:\n            indices_right[n_right] = indices[i]\n            n_right += 1\n\n    return indices_left, indices_right\n\n\n@numba.njit(\n    numba.void(\n        numba.types.Array(numba.types.int8, 2, \"C\", readonly=True),\n        numba.int32[::1],\n        numba.types.ListType(numba.int32[::1]),\n        numba.int64[::1],\n        numba.int64,\n        numba.int64,\n    ),\n    nogil=True,\n    cache=True,\n)\ndef make_int8_tree(\n    data,\n    indices,\n    point_indices,\n    rng_state,\n    leaf_size=30,\n    max_depth=200,\n):\n    if indices.shape[0] > leaf_size and max_depth > 0:\n        (\n            left_indices,\n            right_indices,\n        ) = int8_random_projection_split(data, indices, rng_state)\n\n        make_int8_tree(\n            data,\n            left_indices,\n            point_indices,\n            rng_state,\n            leaf_size,\n            max_depth - 1,\n        )\n\n        make_int8_tree(\n            data,\n            right_indices,\n            point_indices,\n            rng_state,\n            leaf_size,\n            max_depth - 1,\n        )\n    else:\n        point_indices.append(indices)\n\n    return\n\n\n@numba.njit(\n    numba.int32[:, ::1](\n        numba.types.Array(numba.types.int8, 2, \"C\", readonly=True),\n        numba.int64[::1],\n        numba.int64,\n        numba.int64,\n    ),\n    nogil=True,\n    locals={\"n_leaves\": numba.int64, \"i\": numba.int64},\n    parallel=True,\n    cache=True,\n)\ndef make_int8_leaf_array_parallel(data, rng_state, leaf_size=30, max_depth=200):\n    indices = np.arange(data.shape[0]).astype(np.int32)\n\n    point_indices = numba.typed.List.empty_list(numba.int32[::1])\n\n    make_int8_tree(\n        data,\n        indices,\n        point_indices,\n        rng_state,\n        leaf_size,\n        max_depth=max_depth,\n    )\n\n    n_leaves = numba.int64(len(point_indices))\n\n    max_leaf_size = leaf_size\n    for i in numba.prange(n_leaves):\n        points = point_indices[numba.int64(i)]\n        max_leaf_size = max(max_leaf_size, numba.int32(len(points)))\n\n    result = np.full((n_leaves, max_leaf_size), -1, dtype=np.int32)\n    for i in numba.prange(n_leaves):\n        points = point_indices[numba.int64(i)]\n        leaf_size = numba.int32(len(points))\n        result[i, :leaf_size] = points\n\n    return result\n\n\n@numba.njit(\n    numba.int32[:, ::1](\n        numba.types.Array(numba.types.int8, 2, \"C\", readonly=True),\n        numba.int64[::1],\n        numba.int64,\n        numba.int64,\n    ),\n    nogil=True,\n    locals={\"n_leaves\": numba.int64, \"i\": numba.int64},\n    parallel=False,\n    cache=True,\n)\ndef make_int8_leaf_array_serial(data, rng_state, leaf_size=30, max_depth=200):\n    indices = np.arange(data.shape[0]).astype(np.int32)\n\n    point_indices = numba.typed.List.empty_list(numba.int32[::1])\n\n    make_int8_tree(\n        data,\n        indices,\n        point_indices,\n        rng_state,\n        leaf_size,\n        max_depth=max_depth,\n    )\n\n    n_leaves = numba.int64(len(point_indices))\n\n    max_leaf_size = leaf_size\n    for i in numba.prange(n_leaves):\n        points = point_indices[numba.int64(i)]\n        max_leaf_size = max(max_leaf_size, numba.int32(len(points)))\n\n    result = np.full((n_leaves, max_leaf_size), -1, dtype=np.int32)\n    for i in numba.prange(n_leaves):\n        points = point_indices[numba.int64(i)]\n        leaf_size = numba.int32(len(points))\n        result[i, :leaf_size] = points\n\n    return result\n\n\n@numba.njit(\n    numba.types.List(numba.int32[:, ::1])(\n        numba.types.Array(numba.types.int8, 2, \"C\", readonly=True),\n        numba.int64[:, ::1],\n        numba.int64,\n        numba.int64,\n    ),\n    parallel=True,\n    cache=True,\n)\ndef make_int8_forest_no_nested_parallelism(data, rng_states, leaf_size, max_depth):\n    result = [np.empty((1, 1), dtype=np.int32)] * rng_states.shape[0]\n    for i in numba.prange(len(result)):\n        result[i] = make_int8_leaf_array_serial(\n            data, rng_states[i], leaf_size, max_depth=max_depth\n        )\n    return result\n\n\n@numba.njit(\n    numba.types.List(numba.int32[:, ::1])(\n        numba.types.Array(numba.types.int8, 2, \"C\", readonly=True),\n        numba.int64[:, ::1],\n        numba.int64,\n        numba.int64,\n    ),\n    parallel=True,\n    cache=True,\n)\ndef make_int8_forest_with_nested_parallelism(data, rng_states, leaf_size, max_depth):\n    result = [np.empty((1, 1), dtype=np.int32)] * rng_states.shape[0]\n    for i in numba.prange(len(result)):\n        result[i] = make_int8_leaf_array_parallel(\n            data, rng_states[i], leaf_size, max_depth=max_depth\n        )\n    return result\n\n\ndef make_int8_forest(data, rng_states, leaf_size=30, max_depth=200):\n    if ENABLE_NESTED_PARALLELISM:\n        return make_int8_forest_with_nested_parallelism(\n            data, rng_states, leaf_size, max_depth\n        )\n    else:\n        return make_int8_forest_no_nested_parallelism(\n            data, rng_states, leaf_size, max_depth\n        )\n\n\n@numba.njit(\n    numba.float32[:, :, ::1](\n        numba.float32[:, :, ::1],\n        numba.int32[::1],\n        numba.types.Array(numba.types.int32, 2, \"C\", readonly=True),\n        numba.float32[:],\n        numba.types.Array(numba.types.int8, 2, \"C\", readonly=True),\n        numba.int64,\n    ),\n    parallel=True,\n    locals={\n        \"d\": numba.float32,\n        \"p\": numba.int32,\n        \"q\": numba.int32,\n        \"t\": numba.uint16,\n        \"r\": numba.uint32,\n        \"n\": numba.uint32,\n        \"idx\": numba.uint32,\n        \"data_p\": numba.types.Array(numba.types.int8, 1, \"C\", readonly=True),\n    },\n    cache=True,\n)\ndef generate_leaf_updates_int8(\n    updates, n_updates_per_thread, leaf_block, dist_thresholds, data, n_threads\n):\n\n    block_size = leaf_block.shape[0]\n    rows_per_thread = (block_size // n_threads) + 1\n\n    for t in numba.prange(n_threads):\n        idx = 0\n        for r in range(rows_per_thread):\n            n = t * rows_per_thread + r\n            if n >= block_size:\n                break\n\n            for i in range(leaf_block.shape[1]):\n                p = leaf_block[n, i]\n                if p < 0:\n                    break\n                data_p = data[p]\n\n                for j in range(i, leaf_block.shape[1]):\n                    q = leaf_block[n, j]\n                    if q < 0:\n                        break\n\n                    d = fast_int_inner_product_dissimilarity(data_p, data[q])\n                    if d < dist_thresholds[p] or d < dist_thresholds[q]:\n                        updates[t, idx, 0] = p\n                        updates[t, idx, 1] = q\n                        updates[t, idx, 2] = d\n                        idx += 1\n\n        n_updates_per_thread[t] = idx\n\n    return updates\n\n\n@numba.njit(\n    [\n        numba.void(\n            numba.types.Array(numba.types.int8, 2, \"C\", readonly=True),\n            numba.types.Tuple(\n                (numba.int32[:, ::1], numba.float32[:, ::1], numba.uint8[:, ::1])\n            ),\n            numba.types.optional(\n                numba.types.Array(numba.types.int32, 2, \"C\", readonly=True)\n            ),\n            numba.types.int32,\n        ),\n    ],\n    locals={\n        \"d\": numba.float32,\n        \"p\": numba.int32,\n        \"q\": numba.int32,\n        \"i\": numba.uint16,\n        \"updates\": numba.float32[:, :, ::1],\n        \"n_updates_per_thread\": numba.int32[::1],\n    },\n    parallel=True,\n    cache=True,\n)\ndef init_rp_tree_int8(data, current_graph, leaf_array, n_threads):\n\n    n_leaves = leaf_array.shape[0]\n    block_size = n_threads * 64\n    n_blocks = n_leaves // block_size\n\n    max_leaf_size = leaf_array.shape[1]\n    updates_per_thread = (\n        int(block_size * max_leaf_size * (max_leaf_size - 1) / (2 * n_threads)) + 1\n    )\n    updates = np.zeros((n_threads, updates_per_thread, 3), dtype=np.float32)\n    n_updates_per_thread = np.zeros(n_threads, dtype=np.int32)\n\n    for i in range(n_blocks + 1):\n        block_start = i * block_size\n        block_end = min(n_leaves, (i + 1) * block_size)\n\n        leaf_block = leaf_array[block_start:block_end]\n        dist_thresholds = current_graph[1][:, 0]\n\n        updates = generate_leaf_updates_int8(\n            updates, n_updates_per_thread, leaf_block, dist_thresholds, data, n_threads\n        )\n\n        n_vertices = current_graph[0].shape[0]\n        vertex_block_size = n_vertices // n_threads + 1\n\n        for t in numba.prange(n_threads):\n            block_start = t * vertex_block_size\n            block_end = min(block_start + vertex_block_size, n_vertices)\n\n            for j in range(n_threads):\n                for k in range(n_updates_per_thread[j]):\n                    p = np.int32(updates[j, k, 0])\n                    q = np.int32(updates[j, k, 1])\n                    d = np.float32(updates[j, k, 2])\n\n                    if p == -1 or q == -1:\n                        continue\n\n                    if p >= block_start and p < block_end:\n                        flagged_heap_push(\n                            current_graph[1][p],\n                            current_graph[0][p],\n                            current_graph[2][p],\n                            d,\n                            q,\n                        )\n                    if q >= block_start and q < block_end:\n                        flagged_heap_push(\n                            current_graph[1][q],\n                            current_graph[0][q],\n                            current_graph[2][q],\n                            d,\n                            p,\n                        )\n\n\n@numba.njit(\n    numba.types.void(\n        numba.int32,\n        numba.types.Array(numba.types.int8, 2, \"C\", readonly=True),\n        numba.types.Tuple(\n            (numba.int32[:, ::1], numba.float32[:, ::1], numba.uint8[:, ::1])\n        ),\n        numba.int64[::1],\n    ),\n    fastmath=True,\n    locals={\"d\": numba.float32, \"idx\": numba.int32, \"i\": numba.int32},\n    cache=True,\n)\ndef init_random_int8(n_neighbors, data, heap, rng_state):\n    for i in range(data.shape[0]):\n        if heap[0][i, 0] < 0.0:\n            for j in range(n_neighbors - np.sum(heap[0][i] >= 0.0)):\n                idx = np.abs(tau_rand_int(rng_state)) % data.shape[0]\n                if idx in heap[0][i]:\n                    continue\n                d = fast_int_inner_product_dissimilarity(data[idx], data[i])\n                flagged_heap_push(heap[1][i], heap[0][i], heap[2][i], d, idx)\n\n    return\n\n\n@numba.njit(\n    numba.types.void(\n        numba.float32[:, :, ::1],\n        numba.int32[::1],\n        numba.int32[:, ::1],\n        numba.int32[:, ::1],\n        numba.float32[:],\n        numba.types.Array(numba.types.int8, 2, \"C\", readonly=True),\n        numba.int64,\n    ),\n    locals={\n        \"data_p\": numba.types.Array(numba.types.int8, 1, \"C\", readonly=True),\n    },\n    parallel=True,\n    cache=True,\n)\ndef generate_graph_update_array_int8(\n    update_array,\n    n_updates_per_thread,\n    new_candidate_block,\n    old_candidate_block,\n    dist_thresholds,\n    data,\n    n_threads,\n):\n\n    block_size = new_candidate_block.shape[0]\n    max_new_candidates = new_candidate_block.shape[1]\n    max_old_candidates = old_candidate_block.shape[1]\n    rows_per_thread = (block_size // n_threads) + 1\n\n    for t in numba.prange(n_threads):\n        idx = 0\n        updates_are_full = False\n        for r in range(rows_per_thread):\n            i = t * rows_per_thread + r\n            if i >= block_size:\n                break\n\n            for j in range(max_new_candidates):\n                p = int(new_candidate_block[i, j])\n                if p < 0:\n                    continue\n                data_p = data[p]\n\n                for k in range(j, max_new_candidates):\n                    q = int(new_candidate_block[i, k])\n                    if q < 0:\n                        continue\n\n                    d = fast_int_inner_product_dissimilarity(data_p, data[q])\n                    if d <= dist_thresholds[p] or d <= dist_thresholds[q]:\n                        update_array[t, idx, 0] = p\n                        update_array[t, idx, 1] = q\n                        update_array[t, idx, 2] = d\n                        idx += 1\n                        if idx >= update_array.shape[1]:\n                            updates_are_full = True\n                            break\n\n                if updates_are_full:\n                    break\n\n                for k in range(max_old_candidates):\n                    q = int(old_candidate_block[i, k])\n                    if q < 0:\n                        continue\n\n                    d = fast_int_inner_product_dissimilarity(data_p, data[q])\n                    if d <= dist_thresholds[p] or d <= dist_thresholds[q]:\n                        update_array[t, idx, 0] = p\n                        update_array[t, idx, 1] = q\n                        update_array[t, idx, 2] = d\n                        idx += 1\n                        if idx >= update_array.shape[1]:\n                            updates_are_full = True\n                            break\n\n                if updates_are_full:\n                    break\n\n            if updates_are_full:\n                break\n\n        n_updates_per_thread[t] = idx\n\n\n@numba.njit(\n    numba.void(\n        numba.float32[:, :, ::1],\n        numba.int32[:, ::1],\n        numba.int32[:, ::1],\n        numba.int32[:, ::1],\n        numba.float32[:],\n        numba.types.Array(numba.types.int8, 2, \"C\", readonly=True),\n        numba.int64,\n    ),\n    locals={\n        \"data_p\": numba.types.Array(numba.types.int8, 1, \"C\", readonly=True),\n        \"dist_thresh_p\": numba.float32,\n        \"dist_thresh_q\": numba.float32,\n        \"p\": numba.int32,\n        \"q\": numba.int32,\n        \"d\": numba.float32,\n        \"max_updates\": numba.intp,\n        \"max_threshold\": numba.float32,\n        \"p_block\": numba.int32,\n        \"q_block\": numba.int32,\n    },\n    parallel=True,\n    cache=True,\n    boundscheck=False,\n)\ndef generate_sorted_graph_update_array_int8(\n    update_array,\n    n_updates_per_block,\n    new_candidate_block,\n    old_candidate_block,\n    dist_thresholds,\n    data,\n    n_threads,\n):\n    \"\"\"\n    Generate graph updates pre-sorted by target block for int8 data.\n    \"\"\"\n    block_size_candidates = new_candidate_block.shape[0]\n    max_new_candidates = new_candidate_block.shape[1]\n    max_old_candidates = old_candidate_block.shape[1]\n    rows_per_thread = (block_size_candidates // n_threads) + 1\n\n    n_vertices = data.shape[0]\n    vertex_block_size = n_vertices // n_threads + 1\n    max_updates = update_array.shape[1]\n    max_updates_per_src_thread = max_updates // n_threads\n\n    # Reset update counts\n    for b in numba.prange(n_threads):\n        for t in range(n_threads + 1):\n            n_updates_per_block[b, t] = 0\n\n    # Each thread generates updates and places them in appropriate buckets\n    for t in numba.prange(n_threads):\n        # Thread-local counters for each bucket\n        local_counts = np.zeros(n_threads, dtype=np.int32)\n\n        for r in range(rows_per_thread):\n            i = t * rows_per_thread + r\n            if i >= block_size_candidates:\n                break\n\n            for j in range(max_new_candidates):\n                p = new_candidate_block[i, j]\n                if p < 0:\n                    continue\n\n                data_p = data[p]\n                dist_thresh_p = dist_thresholds[p]\n                p_block = p // vertex_block_size\n                if p_block >= n_threads:\n                    p_block = n_threads - 1\n\n                # Compare with other new candidates\n                for k in range(j, max_new_candidates):\n                    q = new_candidate_block[i, k]\n                    if q < 0:\n                        continue\n\n                    d = fast_int_inner_product_dissimilarity(data_p, data[q])\n                    dist_thresh_q = dist_thresholds[q]\n                    max_threshold = max(dist_thresh_p, dist_thresh_q)\n\n                    if d <= max_threshold:\n                        q_block = q // vertex_block_size\n                        if q_block >= n_threads:\n                            q_block = n_threads - 1\n\n                        # Place update in p's bucket\n                        bucket_idx = local_counts[p_block]\n                        write_idx = t * max_updates_per_src_thread + bucket_idx\n                        if write_idx < max_updates:\n                            update_array[p_block, write_idx, 0] = p\n                            update_array[p_block, write_idx, 1] = q\n                            update_array[p_block, write_idx, 2] = d\n                            local_counts[p_block] += 1\n\n                        # If q is in a different block, also place in q's bucket\n                        if q_block != p_block:\n                            bucket_idx = local_counts[q_block]\n                            write_idx = t * max_updates_per_src_thread + bucket_idx\n                            if write_idx < max_updates:\n                                update_array[q_block, write_idx, 0] = p\n                                update_array[q_block, write_idx, 1] = q\n                                update_array[q_block, write_idx, 2] = d\n                                local_counts[q_block] += 1\n\n                # Compare with old candidates\n                for k in range(max_old_candidates):\n                    q = old_candidate_block[i, k]\n                    if q < 0:\n                        continue\n\n                    d = fast_int_inner_product_dissimilarity(data_p, data[q])\n                    dist_thresh_q = dist_thresholds[q]\n                    max_threshold = max(dist_thresh_p, dist_thresh_q)\n\n                    if d <= max_threshold:\n                        q_block = q // vertex_block_size\n                        if q_block >= n_threads:\n                            q_block = n_threads - 1\n\n                        # Place update in p's bucket\n                        bucket_idx = local_counts[p_block]\n                        write_idx = t * max_updates_per_src_thread + bucket_idx\n                        if write_idx < max_updates:\n                            update_array[p_block, write_idx, 0] = p\n                            update_array[p_block, write_idx, 1] = q\n                            update_array[p_block, write_idx, 2] = d\n                            local_counts[p_block] += 1\n\n                        # If q is in a different block, also place in q's bucket\n                        if q_block != p_block:\n                            bucket_idx = local_counts[q_block]\n                            write_idx = t * max_updates_per_src_thread + bucket_idx\n                            if write_idx < max_updates:\n                                update_array[q_block, write_idx, 0] = p\n                                update_array[q_block, write_idx, 1] = q\n                                update_array[q_block, write_idx, 2] = d\n                                local_counts[q_block] += 1\n\n        # Record total updates generated by this thread for each bucket\n        for b in range(n_threads):\n            n_updates_per_block[b, t + 1] = local_counts[b]\n\n\ndef nn_descent_int8(\n    data,\n    n_neighbors,\n    rng_state,\n    max_candidates=50,\n    n_iters=10,\n    delta=0.001,\n    delta_improv=None,\n    leaf_array=None,\n    verbose=False,\n):\n    \"\"\"\n    Perform approximate nearest neighbor descent algorithm using int8 data.\n\n    Parameters:\n    - data: The input data array.\n    - n_neighbors: The number of nearest neighbors to search for.\n    - rng_state: The random number generator state.\n    - max_candidates: The maximum number of candidates to consider during the search. Default is 50.\n    - n_iters: The number of iterations to perform. Default is 10.\n    - delta: The stopping threshold based on update count. Default is 0.001.\n    - delta_improv: Optional stopping threshold based on relative improvement in total\n        graph distance. When set (e.g., 0.001 for 0.1%), the algorithm will also\n        terminate when the relative improvement in sum of all distances drops below\n        this threshold. This can provide earlier termination on data with good\n        structure, adapting to the intrinsic difficulty of the dataset. Default is None\n        (disabled).\n    - leaf_array: The array representing the leaf structure of the RP-tree. Default is None.\n    - verbose: Whether to print progress information. Default is False.\n\n    Returns:\n    - The sorted nearest neighbor graph.\n    \"\"\"\n    n_threads = numba.get_num_threads()\n    current_graph = make_heap(data.shape[0], n_neighbors)\n    init_rp_tree_int8(data, current_graph, leaf_array, n_threads)\n    init_random_int8(n_neighbors, data, current_graph, rng_state)\n\n    n_vertices = data.shape[0]\n    n_threads = numba.get_num_threads()\n    block_size = 65536 // n_threads\n    n_blocks = n_vertices // block_size\n\n    max_updates_per_thread = int(\n        ((max_candidates**2 + max_candidates * (max_candidates - 1) / 2) * block_size)\n    )\n    update_array = np.empty((n_threads, max_updates_per_thread, 3), dtype=np.float32)\n    n_updates_per_thread = np.zeros(n_threads, dtype=np.int32)\n\n    # For distance-based termination\n    prev_sum_dist = None\n\n    for n in range(n_iters):\n        if verbose:\n            print(\"\\t\", n + 1, \" / \", n_iters)\n\n        (new_candidate_neighbors, old_candidate_neighbors) = build_candidates(\n            current_graph, max_candidates, rng_state, n_threads\n        )\n\n        c = 0\n        n_vertices = new_candidate_neighbors.shape[0]\n        for i in range(n_blocks + 1):\n            block_start = i * block_size\n            block_end = min(n_vertices, (i + 1) * block_size)\n\n            new_candidate_block = new_candidate_neighbors[block_start:block_end]\n            old_candidate_block = old_candidate_neighbors[block_start:block_end]\n\n            dist_thresholds = current_graph[1][:, 0]\n\n            generate_graph_update_array_int8(\n                update_array,\n                n_updates_per_thread,\n                new_candidate_block,\n                old_candidate_block,\n                dist_thresholds,\n                data,\n                n_threads,\n            )\n\n            c += apply_graph_update_array(\n                current_graph, update_array, n_updates_per_thread, n_threads\n            )\n\n        # Check update count termination\n        if c <= delta * n_neighbors * data.shape[0]:\n            if verbose:\n                print(\"\\tStopping threshold met -- exiting after\", n + 1, \"iterations\")\n            return deheap_sort(current_graph[0], current_graph[1])\n\n        # Check distance improvement termination (if enabled)\n        if delta_improv is not None:\n            all_distances = current_graph[1]\n            valid_mask = all_distances < INF\n            sum_dist = np.sum(all_distances[valid_mask])\n\n            if prev_sum_dist is not None:\n                rel_improv = abs(sum_dist - prev_sum_dist) / abs(prev_sum_dist)\n                if rel_improv < delta_improv:\n                    if verbose:\n                        print(\n                            f\"\\tDistance improvement threshold met ({rel_improv:.4%} < {delta_improv:.4%})\"\n                            f\" -- exiting after {n + 1} iterations\"\n                        )\n                    return deheap_sort(current_graph[0], current_graph[1])\n\n            prev_sum_dist = sum_dist\n\n        block_size = min(n_vertices, 2 * block_size)\n        n_blocks = n_vertices // block_size\n\n    return deheap_sort(current_graph[0], current_graph[1])\n\n\ndef nn_descent_int8_sorted(\n    data,\n    n_neighbors,\n    rng_state,\n    max_candidates=50,\n    n_iters=10,\n    delta=0.001,\n    delta_improv=None,\n    leaf_array=None,\n    verbose=False,\n):\n    \"\"\"\n    Perform approximate nearest neighbor descent algorithm using int8 data.\n\n    This version uses pre-sorted updates bucketed by target block for potentially\n    better performance when n_threads is large.\n\n    Parameters:\n    - data: The input data array.\n    - n_neighbors: The number of nearest neighbors to search for.\n    - rng_state: The random number generator state.\n    - max_candidates: The maximum number of candidates to consider during the search. Default is 50.\n    - n_iters: The number of iterations to perform. Default is 10.\n    - delta: The stopping threshold based on update count. Default is 0.001.\n    - delta_improv: Optional stopping threshold based on relative improvement in total\n        graph distance. When set (e.g., 0.001 for 0.1%), the algorithm will also\n        terminate when the relative improvement in sum of all distances drops below\n        this threshold. This can provide earlier termination on data with good\n        structure, adapting to the intrinsic difficulty of the dataset. Default is None\n        (disabled).\n    - leaf_array: The array representing the leaf structure of the RP-tree. Default is None.\n    - verbose: Whether to print progress information. Default is False.\n\n    Returns:\n    - The sorted nearest neighbor graph.\n    \"\"\"\n    n_threads = numba.get_num_threads()\n    current_graph = make_heap(data.shape[0], n_neighbors)\n    init_rp_tree_int8(data, current_graph, leaf_array, n_threads)\n    init_random_int8(n_neighbors, data, current_graph, rng_state)\n\n    n_vertices = data.shape[0]\n    n_threads = numba.get_num_threads()\n    block_size = 65536 // n_threads\n    n_blocks = n_vertices // block_size\n\n    max_updates_per_thread = int(\n        ((max_candidates**2 + max_candidates * (max_candidates - 1) / 2) * block_size)\n    )\n    sorted_update_array = np.empty(\n        (n_threads, max_updates_per_thread, 3), dtype=np.float32\n    )\n    n_updates_per_block = np.zeros((n_threads, n_threads + 1), dtype=np.int32)\n\n    # For distance-based termination\n    prev_sum_dist = None\n\n    for n in range(n_iters):\n        if verbose:\n            print(\"\\t\", n + 1, \" / \", n_iters)\n\n        (new_candidate_neighbors, old_candidate_neighbors) = build_candidates(\n            current_graph, max_candidates, rng_state, n_threads\n        )\n\n        c = 0\n        n_vertices = new_candidate_neighbors.shape[0]\n        for i in range(n_blocks + 1):\n            block_start = i * block_size\n            block_end = min(n_vertices, (i + 1) * block_size)\n\n            new_candidate_block = new_candidate_neighbors[block_start:block_end]\n            old_candidate_block = old_candidate_neighbors[block_start:block_end]\n\n            dist_thresholds = current_graph[1][:, 0]\n\n            n_updates_per_block.fill(0)\n\n            generate_sorted_graph_update_array_int8(\n                sorted_update_array,\n                n_updates_per_block,\n                new_candidate_block,\n                old_candidate_block,\n                dist_thresholds,\n                data,\n                n_threads,\n            )\n\n            c += apply_sorted_graph_updates(\n                current_graph, sorted_update_array, n_updates_per_block, n_threads\n            )\n\n        # Check update count termination\n        if c <= delta * n_neighbors * data.shape[0]:\n            if verbose:\n                print(\"\\tStopping threshold met -- exiting after\", n + 1, \"iterations\")\n            return deheap_sort(current_graph[0], current_graph[1])\n\n        # Check distance improvement termination (if enabled)\n        if delta_improv is not None:\n            all_distances = current_graph[1]\n            valid_mask = all_distances < INF\n            sum_dist = np.sum(all_distances[valid_mask])\n\n            if prev_sum_dist is not None:\n                rel_improv = abs(sum_dist - prev_sum_dist) / abs(prev_sum_dist)\n                if rel_improv < delta_improv:\n                    if verbose:\n                        print(\n                            f\"\\tDistance improvement threshold met ({rel_improv:.4%} < {delta_improv:.4%})\"\n                            f\" -- exiting after {n + 1} iterations\"\n                        )\n                    return deheap_sort(current_graph[0], current_graph[1])\n\n            prev_sum_dist = sum_dist\n\n        block_size = min(n_vertices, 2 * block_size)\n        n_blocks = n_vertices // block_size\n\n    return deheap_sort(current_graph[0], current_graph[1])\n"
  },
  {
    "path": "evoc/knn_graph.py",
    "content": "import numpy as np\nimport numba\nimport time\n\nfrom sklearn.utils import check_array, check_random_state\n\nfrom warnings import warn\nfrom .float_nndescent import (\n    make_float_forest,\n    nn_descent_float,\n    nn_descent_float_sorted,\n)\nfrom .uint8_nndescent import (\n    make_uint8_forest,\n    nn_descent_uint8,\n    nn_descent_uint8_sorted,\n)\nfrom .int8_nndescent import make_int8_forest, nn_descent_int8, nn_descent_int8_sorted\n\nINT32_MIN = np.iinfo(np.int32).min + 1\nINT32_MAX = np.iinfo(np.int32).max - 1\n\n\n# Generates a timestamp for use in logging messages when verbose=True\ndef ts():\n    return time.ctime(time.time())\n\n\ndef make_forest(\n    data,\n    n_neighbors,\n    n_trees,\n    leaf_size,\n    random_state,\n    input_dtype,\n    max_depth=200,\n):\n    \"\"\"Build a random projection forest with ``n_trees``.\n\n    Parameters\n    ----------\n    data\n    n_neighbors\n    n_trees\n    leaf_size\n    rng_state\n    angular\n\n    Returns\n    -------\n    forest: list\n        A list of random projection trees.\n    \"\"\"\n\n    if leaf_size is None:\n        leaf_size = max(10, np.int32(n_neighbors))\n\n    rng_states = random_state.randint(INT32_MIN, INT32_MAX, size=(n_trees, 3)).astype(\n        np.int64\n    )\n    try:\n        if input_dtype == np.uint8:\n            result = make_uint8_forest(data, rng_states, leaf_size, max_depth)\n        elif input_dtype == np.int8:\n            result = make_int8_forest(data, rng_states, leaf_size, max_depth)\n        else:\n            result = make_float_forest(data, rng_states, leaf_size, max_depth)\n    except (RuntimeError, RecursionError, SystemError):\n        warn(\n            \"Random Projection forest initialisation failed due to recursion\"\n            \"limit being reached. Something is a little strange with your \"\n            \"graph_data, and this may take longer than normal to compute.\"\n        )\n        return np.empty((0, 0), dtype=np.int32)\n\n    # different trees can end up with different max leaf_sizes if the tree depth is insufficient\n    max_leaf_size = np.max([leaf_array.shape[1] for leaf_array in result])\n\n    # pad each leaf_array from each tree out to the max_leaf_size from any tree\n    # so that vstack can succeed. Check np.pad docs for the specific semantics\n    return np.vstack(\n        [\n            np.pad(\n                leaf_array,\n                ((0, 0), (0, max_leaf_size - leaf_array.shape[1])),\n                constant_values=-1,\n            )\n            for leaf_array in result\n        ]\n    )\n\n\ndef nn_descent(\n    data,\n    n_neighbors,\n    rng_state,\n    effective_max_candidates,\n    n_iters,\n    delta,\n    input_dtype,\n    leaf_array=None,\n    verbose=False,\n    use_sorted_updates=True,\n    delta_improv=None,\n):\n    if input_dtype == np.uint8:\n        if use_sorted_updates:\n            neighbor_graph = nn_descent_uint8_sorted(\n                data,\n                n_neighbors,\n                rng_state,\n                effective_max_candidates,\n                n_iters,\n                delta,\n                delta_improv=delta_improv,\n                leaf_array=leaf_array,\n                verbose=verbose,\n            )\n        else:\n            neighbor_graph = nn_descent_uint8(\n                data,\n                n_neighbors,\n                rng_state,\n                effective_max_candidates,\n                n_iters,\n                delta,\n                delta_improv=delta_improv,\n                leaf_array=leaf_array,\n                verbose=verbose,\n            )\n        neighbor_graph[1][:] = -np.log2(-neighbor_graph[1])\n    elif input_dtype == np.int8:\n        if use_sorted_updates:\n            neighbor_graph = nn_descent_int8_sorted(\n                data,\n                n_neighbors,\n                rng_state,\n                effective_max_candidates,\n                n_iters,\n                delta,\n                delta_improv=delta_improv,\n                leaf_array=leaf_array,\n                verbose=verbose,\n            )\n        else:\n            neighbor_graph = nn_descent_int8(\n                data,\n                n_neighbors,\n                rng_state,\n                effective_max_candidates,\n                n_iters,\n                delta,\n                delta_improv=delta_improv,\n                leaf_array=leaf_array,\n                verbose=verbose,\n            )\n        neighbor_graph[1][:] = 1.0 / (-neighbor_graph[1])\n    else:\n        if use_sorted_updates:\n            neighbor_graph = nn_descent_float_sorted(\n                data,\n                n_neighbors,\n                rng_state,\n                effective_max_candidates,\n                n_iters,\n                delta,\n                delta_improv=delta_improv,\n                leaf_array=leaf_array,\n                verbose=verbose,\n            )\n        else:\n            neighbor_graph = nn_descent_float(\n                data,\n                n_neighbors,\n                rng_state,\n                effective_max_candidates,\n                n_iters,\n                delta,\n                delta_improv=delta_improv,\n                leaf_array=leaf_array,\n                verbose=verbose,\n            )\n        neighbor_graph[1][:] = np.maximum(-np.log2(-neighbor_graph[1]), 0.0)\n\n    return neighbor_graph\n\n\ndef knn_graph(\n    data,\n    n_neighbors=30,\n    n_trees=None,\n    leaf_size=None,\n    random_state=None,\n    max_candidates=None,\n    max_rptree_depth=200,\n    n_iters=None,\n    delta=0.001,\n    delta_improv=0.001,\n    n_jobs=None,\n    verbose=False,\n    use_sorted_updates=True,\n):\n    \"\"\"Construct a k-nearest neighbor graph using the NN-Descent algorithm.\n\n    This function builds a k-nearest neighbor graph using random projection trees\n    for initialization followed by the NN-Descent algorithm for refinement. It\n    supports multiple data types (float32 for normalized embeddings, int8 for\n    quantized embeddings, uint8 for binary embeddings) with appropriate distance\n    metrics for each.\n\n    Parameters\n    ----------\n    data : array-like of shape (n_samples, n_features)\n        The data for which to compute nearest neighbors. If float32, cosine distance\n        is used. If int8, quantized cosine distance is used. If uint8, Jaccard\n        distance (based on Hamming distance for binary embeddings) is used.\n\n    n_neighbors : int, default=30\n        The number of nearest neighbors to compute for each sample.\n\n    n_trees : int or None, default=None\n        The number of random projection trees to build. If None, defaults to\n        between 4 and 8 depending on the number of available threads.\n\n    leaf_size : int or None, default=None\n        The maximum number of points per leaf in the random projection trees.\n        If None, defaults to max(10, n_neighbors).\n\n    random_state : int, RandomState instance or None, default=None\n        Controls the randomness of the algorithm. Pass an int for reproducible\n        output across multiple function calls.\n\n    max_candidates : int or None, default=None\n        The maximum number of candidate neighbors to evaluate during NN-Descent.\n        If None, defaults to min(60, int(n_neighbors * 1.5)).\n\n    max_rptree_depth : int, default=200\n        Maximum depth of the random projection trees.\n\n    n_iters : int or None, default=None\n        Number of iterations for the NN-Descent algorithm. If None, defaults to\n        max(5, int(round(log2(n_samples)))).\n\n    delta : float, default=0.001\n        Convergence threshold for the NN-Descent algorithm.\n\n    delta_improv : float, default=0.001\n        Improvement threshold for early stopping in NN-Descent.\n\n    n_jobs : int or None, default=None\n        The number of threads to use. If -1, uses all available threads.\n        If None, preserves the current numba thread setting.\n\n    verbose : bool, default=False\n        If True, print progress messages during computation.\n\n    use_sorted_updates : bool, default=True\n        If True, uses a more efficient sorted update strategy in NN-Descent.\n\n    Returns\n    -------\n    neighbor_graph : tuple of (array, array)\n        A tuple containing:\n        - indices : array-like of shape (n_samples, n_neighbors)\n            The indices of the k-nearest neighbors for each sample.\n        - distances : array-like of shape (n_samples, n_neighbors)\n            The distances from each sample to its k-nearest neighbors.\n            Distances are transformed to a uniform scale based on the input dtype.\n    \"\"\"\n    if data.dtype == np.uint8:\n        data = check_array(data, dtype=np.uint8, order=\"C\")\n        _input_dtype = np.uint8\n        _bit_trees = True\n    elif data.dtype == np.int8:\n        data = check_array(data, dtype=np.int8, order=\"C\")\n        _input_dtype = np.int8\n        _bit_trees = False\n    else:\n        norms = np.einsum(\"ij,ij->i\", data, data)\n        np.sqrt(norms, norms)\n        norms[norms == 0.0] = 1.0\n        if np.allclose(norms, 1.0):\n            # Data is already normalized, just ensure C-contiguity and float32\n            data = np.ascontiguousarray(data, dtype=np.float32)\n        else:\n            # Efficiently create a modifiable float32 C-contiguous copy\n            data = np.array(data, dtype=np.float32, order=\"C\", copy=True)\n            data /= norms[:, np.newaxis]\n        _input_dtype = np.float32\n        _bit_trees = False\n\n    current_random_state = check_random_state(random_state)\n    rng_state = current_random_state.randint(INT32_MIN, INT32_MAX, 3).astype(np.int64)\n\n    # Set threading constraints\n    _original_num_threads = numba.get_num_threads()\n    if n_jobs != -1 and n_jobs is not None:\n        numba.set_num_threads(n_jobs)\n\n    if n_trees is None:\n        n_trees = numba.get_num_threads()\n        n_trees = max(4, min(8, n_trees))  # Only so many trees are useful\n    if n_iters is None:\n        n_iters = max(5, int(round(np.log2(data.shape[0]))))\n\n    if verbose:\n        print(ts(), \"Building RP forest with\", str(n_trees), \"trees\")\n\n    leaf_array = make_forest(\n        data,\n        n_neighbors,\n        n_trees,\n        leaf_size,\n        current_random_state,\n        _input_dtype,\n        max_depth=max_rptree_depth,\n    )\n\n    if max_candidates is None:\n        effective_max_candidates = min(60, int(n_neighbors * 1.5))\n    else:\n        effective_max_candidates = max_candidates\n\n    if verbose:\n        print(ts(), \"NN descent for\", str(n_iters), \"iterations\")\n\n    neighbor_graph = nn_descent(\n        data,\n        n_neighbors,\n        rng_state,\n        effective_max_candidates,\n        n_iters,\n        delta,\n        _input_dtype,\n        leaf_array=leaf_array,\n        verbose=verbose,\n        use_sorted_updates=use_sorted_updates,\n        delta_improv=delta_improv,\n    )\n\n    if np.any(neighbor_graph[0] < 0):\n        warn(\n            \"Failed to correctly find n_neighbors for some samples.\"\n            \" Results may be less than ideal. Try re-running with\"\n            \" different parameters.\"\n        )\n\n    if n_jobs != -1 and n_jobs is not None:\n        numba.set_num_threads(_original_num_threads)\n    return neighbor_graph\n"
  },
  {
    "path": "evoc/label_propagation.py",
    "content": "import numpy as np\nimport numba\n\nfrom scipy.sparse import csr_matrix\nfrom sklearn.preprocessing import normalize\nfrom sklearn.decomposition import PCA\nfrom sklearn.manifold import SpectralEmbedding, MDS\n\nfrom .node_embedding import node_embedding\nfrom .common_nndescent import tau_rand, tau_rand_int\n\nINT32_MIN = np.iinfo(np.int32).min + 1\nINT32_MAX = np.iinfo(np.int32).max - 1\n\n\n@numba.njit(fastmath=True, parallel=True, cache=True)\ndef label_prop_iteration(\n    indptr,\n    indices,\n    data,\n    labels,\n    rng_state,\n):\n    n_rows = indptr.shape[0] - 1\n    result = labels.copy()\n\n    for i in numba.prange(n_rows):\n        current_l = labels[i]\n        if current_l >= 0:\n            continue\n        local_rng_state = rng_state + i\n        votes = {}\n        for k in range(indptr[i], indptr[i + 1]):\n            j = indices[k]\n            l = labels[j]\n            if l in votes:\n                votes[l] += data[k]\n            else:\n                votes[l] = data[k]\n\n        max_vote = 1\n        tie_count = 1\n        for l in votes:\n            if l == -1:\n                continue\n            elif votes[l] > max_vote:\n                max_vote = votes[l]\n                result[i] = l\n                tie_count = 1\n            elif votes[l] == max_vote:\n                tie_count += 1\n                if current_l == -1:\n                    result[i] = l\n                elif tau_rand(local_rng_state) < 1.0 / tie_count:\n                    result[i] = l\n            else:\n                continue\n\n    return result\n\n\n@numba.njit(fastmath=True, parallel=True, cache=True)\ndef original_label_prop_iteration(\n    indptr,\n    indices,\n    data,\n    labels,\n    rng_state,\n):\n    n_rows = indptr.shape[0] - 1\n    result = labels.copy()\n\n    for i in numba.prange(n_rows):\n        current_l = labels[i]\n        local_rng_state = rng_state + i\n        votes = {}\n        for k in range(indptr[i], indptr[i + 1]):\n            j = indices[k]\n            l = labels[j]\n            if l in votes:\n                votes[l] += data[k]\n            else:\n                votes[l] = data[k]\n\n        max_vote = 1\n        tie_count = 1\n        for l in votes:\n            if l == -1:\n                continue\n            elif votes[l] > max_vote:\n                max_vote = votes[l]\n                result[i] = l\n                tie_count = 1\n            elif votes[l] == max_vote:\n                tie_count += 1\n                if current_l == -1:\n                    result[i] = l\n                elif tau_rand(local_rng_state) < 1.0 / tie_count:\n                    result[i] = l\n            else:\n                continue\n\n    return result\n\n\n@numba.njit(cache=True)\ndef label_outliers(indptr, indices, labels, rng_state):\n    n_rows = indptr.shape[0] - 1\n    max_label = labels.max()\n\n    for i in numba.prange(n_rows):\n        local_rng_state = rng_state + i\n        if labels[i] < 0:\n\n            node_queue = [i]\n            unlabelled = True\n            n_iter = 0\n\n            while unlabelled and n_iter < 64 and len(node_queue) > 0:\n\n                n_iter += 1\n                current_node = node_queue.pop()\n                for k in range(indptr[current_node], indptr[current_node + 1]):\n                    j = indices[k]\n                    if labels[j] >= 0:\n                        labels[i] = labels[j]\n                        unlabelled = False\n                        break\n                    else:\n                        node_queue.append(j)\n\n            if n_iter >= 64 or unlabelled:\n                labels[i] = tau_rand_int(local_rng_state) % (max_label + 1)\n\n    return labels\n\n\n@numba.njit(cache=True)\ndef remap_labels(labels):\n    mapping = {}\n    unique_labels = np.unique(labels)\n    if unique_labels[0] == -1:\n        unique_labels = unique_labels[1:]\n    for i, l in enumerate(unique_labels):\n        mapping[l] = i\n    next_label = i + 1\n    for i in range(labels.shape[0]):\n        if labels[i] < 0:\n            labels[i] = next_label\n            next_label += 1\n        else:\n            labels[i] = mapping[labels[i]]\n\n    return labels\n\n\ndef label_prop_loop(\n    indptr, indices, data, labels, random_state, n_iter=20, approx_n_parts=2048\n):\n    rng_state = random_state.randint(INT32_MIN, INT32_MAX, 3).astype(np.int64)\n    for i in range(approx_n_parts):  # range(int(1.25 * approx_n_parts)):\n        labels[random_state.randint(labels.shape[0])] = i\n\n    for i in range(n_iter):\n        new_labels = label_prop_iteration(indptr, indices, data, labels, rng_state)\n        labels = new_labels\n\n    labels = label_outliers(indptr, indices, labels, rng_state)\n    return remap_labels(labels)\n\n\ndef original_label_prop_loop(\n    indptr, indices, data, labels, random_state, n_iter=20, approx_n_parts=2048\n):\n    rng_state = random_state.randint(INT32_MIN, INT32_MAX, 3).astype(np.int64)\n    for i in range(int(1.25 * approx_n_parts)):\n        labels[random_state.randint(labels.shape[0])] = i\n\n    for i in range(n_iter):\n        new_labels = original_label_prop_iteration(\n            indptr, indices, data, labels, rng_state\n        )\n        labels = new_labels\n\n    labels = label_outliers(indptr, indices, labels, rng_state)\n    return remap_labels(labels)\n\n\ndef label_propagation_init(\n    graph,\n    n_label_prop_iter=20,\n    n_embedding_epochs=50,\n    approx_n_parts=512,\n    n_components=2,\n    scaling=0.1,\n    random_scale=1.0,\n    noise_level=0.5,\n    random_state=None,\n    data=None,\n    recursive_init=True,\n    base_init=\"pca\",\n    base_init_threshold=64,\n    upscaling=\"partition_expander\",\n):\n    \"\"\"Initialize a node embedding using label propagation on a sparse graph.\n\n    This function provides a high-quality initialization for node embeddings by\n    combining graph-based label propagation with hierarchical partitioning. For\n    large graphs, it recursively partitions the data and upscales the results.\n    For small graphs, it uses direct methods (PCA, spectral embedding, or random).\n\n    Parameters\n    ----------\n    graph : scipy.sparse matrix\n        A sparse adjacency or weighted graph matrix representing connectivity.\n\n    n_label_prop_iter : int, default=20\n        Number of label propagation iterations to perform on the graph.\n\n    n_embedding_epochs : int, default=50\n        Number of epochs when using node embedding for upscaling.\n\n    approx_n_parts : int, default=512\n        Approximate number of partitions to create for recursive partitioning\n        of large graphs. Useful for controlling memory and computation.\n\n    n_components : int, default=2\n        The number of dimensions in the output embedding.\n\n    scaling : float, default=0.1\n        Scaling factor applied to label propagation distances.\n\n    random_scale : float, default=1.0\n        Scaling factor for random noise in the initialization.\n\n    noise_level : float, default=0.5\n        The noise level parameter passed to node embedding algorithms.\n\n    random_state : RandomState instance or None, default=None\n        Controls the randomness of the algorithm. If None, uses system randomness.\n\n    data : array-like of shape (n_samples, n_features) or None, default=None\n        The original data array. Required if base_init='pca'. Used for direct\n        initialization methods on small graphs.\n\n    recursive_init : bool, default=True\n        If True, uses recursive partitioning for large graphs. If False, applies\n        the base initialization method directly.\n\n    base_init : {'pca', 'random', 'spectral', 'mds'}, default='pca'\n        The initialization method to use for small graphs (when graph size is below\n        base_init_threshold). 'pca' requires the data parameter.\n\n    base_init_threshold : int, default=64\n        The size threshold below which the base_init method is used directly.\n        Graphs larger than this use recursive partitioning.\n\n    upscaling : {'partition_expander', 'node_embedding'}, default='partition_expander'\n        The method to use when upscaling partitions back to the full graph.\n        'partition_expander' uses a fast expansion method, 'node_embedding' uses\n        full node embedding (slower but potentially better quality).\n\n    Returns\n    -------\n    embedding : array-like of shape (n_vertices, n_components)\n        The initialized node embedding based on label propagation and graph structure.\n    \"\"\"\n    if random_state is None:\n        random_state = np.random.RandomState()\n\n    if graph.shape[0] < base_init_threshold:\n        if base_init == \"random\":\n            result = random_state.normal(\n                loc=0.0, scale=1.0, size=(graph.shape[0], n_components)\n            )\n            norms = np.linalg.norm(result, axis=1, keepdims=True)\n            result = result / norms\n            return result.astype(np.float32)\n        elif base_init == \"pca\":\n            result = (\n                PCA(n_components=n_components, random_state=random_state)\n                .fit_transform(data)\n                .astype(np.float32, order=\"C\")\n            )\n            result -= result.mean()\n            result /= (result.max() - result.min()) / 2.0\n            return result\n        elif base_init == \"spectral\":\n            result = (\n                SpectralEmbedding(n_components=n_components, random_state=random_state)\n                .fit_transform(data)\n                .astype(np.float32, order=\"C\")\n            )\n            result -= result.mean()\n            result /= (result.max() - result.min()) / 2.0\n            return result\n        elif base_init == \"mds\":\n            result = (\n                MDS(\n                    n_components=n_components,\n                    random_state=random_state,\n                    n_init=1,\n                    max_iter=300,\n                )\n                .fit_transform(data)\n                .astype(np.float32, order=\"C\")\n            )\n            result -= result.mean()\n            result /= (result.max() - result.min()) / 2.0\n            return result\n        else:\n            raise ValueError(\n                \"Unknown base initialization method. Should be one of ['random', 'pca', 'spectral', 'mds']\"\n            )\n\n    labels = np.full(graph.shape[0], -1, dtype=np.int64)\n    partition = label_prop_loop(\n        graph.indptr,\n        graph.indices,\n        graph.data,\n        labels,\n        random_state,\n        n_label_prop_iter,\n        approx_n_parts,\n    )\n    base_reduction_map = csr_matrix(\n        (np.ones(partition.shape[0]), partition, np.arange(partition.shape[0] + 1)),\n        shape=(partition.shape[0], partition.max() + 1),\n    )\n    normalized_reduction_map = normalize(base_reduction_map, axis=0, norm=\"l2\")\n    data_reducer = normalize(normalized_reduction_map.T, norm=\"l1\")\n    if data is not None:\n        reduced_data = data_reducer @ data\n    else:\n        reduced_data = None\n\n    reduced_graph = normalized_reduction_map.T * graph * base_reduction_map\n    reduced_graph.data = np.clip(reduced_graph.data, 0.0, 1.0)\n\n    if recursive_init:\n        reduced_init = label_propagation_init(\n            reduced_graph,\n            n_label_prop_iter=n_label_prop_iter,\n            n_embedding_epochs=min(255, n_embedding_epochs),\n            approx_n_parts=approx_n_parts // 4,\n            n_components=n_components,\n            scaling=scaling,\n            random_scale=random_scale,\n            noise_level=noise_level,\n            random_state=random_state,\n            data=reduced_data,\n            recursive_init=True,\n            upscaling=upscaling,\n            base_init=base_init,\n            base_init_threshold=base_init_threshold,\n        )\n    else:\n        reduced_init = None\n\n    reduced_layout = node_embedding(\n        reduced_graph,\n        n_components,\n        n_embedding_epochs,\n        verbose=False,\n        noise_level=noise_level,\n        random_state=random_state,\n        initial_embedding=reduced_init,\n        initial_alpha=0.001 * n_embedding_epochs,\n    )\n\n    if upscaling == \"partition_expander\":\n        data_expander = normalize(\n            (graph.multiply(graph.T)) @ normalized_reduction_map, norm=\"l1\"\n        )\n        result = (\n            data_expander @ reduced_layout\n            + normalize(normalized_reduction_map, norm=\"l1\") @ reduced_layout\n        ) / 2.0\n    elif upscaling == \"jitter_expander\":\n        data_expander = normalize(\n            (graph.multiply(graph.T)) @ normalized_reduction_map, norm=\"l1\"\n        )\n        expanded = (\n            data_expander @ reduced_layout\n            + normalize(normalized_reduction_map, norm=\"l1\") @ reduced_layout\n        ) / 2.0\n        jittered = reduced_layout[partition]\n        jittered += random_state.normal(\n            scale=random_scale / 4.0, size=(partition.shape[0], reduced_layout.shape[1])\n        )\n        result = (expanded + jittered) / 2.0\n    else:\n        result = reduced_layout[partition]\n        result += random_state.normal(\n            scale=random_scale, size=(partition.shape[0], reduced_layout.shape[1])\n        )\n\n    result = (scaling * (result - result.mean(axis=0))).astype(np.float32)\n    return result\n"
  },
  {
    "path": "evoc/nested_parallelism.py",
    "content": "import os\nimport sys\nimport numba\n\n\ndef supports_safe_nesting():\n    # Check if user explicitly set a layer\n    layer = os.environ.get(\"NUMBA_THREADING_LAYER\", \"\")\n\n    if layer in (\"tbb\", \"omp\"):\n        return True\n\n    # Check loaded libraries (if numba has already initialized)\n    try:\n        if \"tbb\" in numba.threading_layer():\n            return True\n    except (ValueError, numba.errors.NumbaError):\n        # Numba hasn't selected a layer yet, or multiple are available.\n        pass\n\n    # Heuristic: If on Mac and TBB is not strictly enforced/present, assume unsafe.\n    if sys.platform == \"darwin\":\n        # You could try importing tbb to be sure\n        try:\n            import tbb\n\n            return True\n        except ImportError:\n            return False\n\n    return True\n\n\nENABLE_NESTED_PARALLELISM = supports_safe_nesting()\n"
  },
  {
    "path": "evoc/node_embedding.py",
    "content": "import numpy as np\nimport numba\n\nfrom tqdm import tqdm\n\nINT32_MIN = np.iinfo(np.int32).min + 1\nINT32_MAX = np.iinfo(np.int32).max - 1\n\n\ndef make_epochs_per_sample(weights, n_epochs):\n    result = np.full(weights.shape[0], n_epochs, dtype=np.float32)\n    n_samples = np.maximum(n_epochs * (weights / weights.max()), 1.0)\n    result = float(n_epochs) / np.float32(n_samples)\n    return result\n\n\n@numba.njit(\n    \"f4(f4[::1],f4[::1])\",\n    fastmath=True,\n    cache=True,\n    locals={\n        \"result\": numba.types.float32,\n        \"diff\": numba.types.float32,\n        \"dim\": numba.types.intp,\n        \"i\": numba.types.intp,\n    },\n)\ndef rdist(x, y):\n    result = 0.0\n    dim = x.shape[0]\n    for i in range(dim):\n        diff = x[i] - y[i]\n        result += diff * diff\n\n    return result\n\n\n@numba.njit(inline=\"always\")\ndef clip(val, lo, hi):\n    if val > hi:\n        return hi\n    elif val < lo:\n        return lo\n    else:\n        return val\n\n\n@numba.njit(\n    \"void(f4[:,::1],u4[::1],u4[::1],u4,f4[::1],u4,u1,f4,f4[::1],f4[::1],f4[::1],u1,f4)\",\n    fastmath=True,\n    parallel=True,\n    cache=True,\n    locals={\n        \"i\": numba.uint32,\n        \"j\": numba.uint32,\n        \"k\": numba.uint32,\n        \"di\": numba.uint8,\n        \"p\": numba.uint8,\n        \"n_neg_samples\": numba.uint8,\n        \"dist_squared\": numba.float32,\n        \"grad_coeff\": numba.float32,\n        \"current\": numba.float32[::1],\n        \"other\": numba.float32[::1],\n    },\n)\ndef node_embedding_epoch(\n    embedding,\n    head,\n    tail,\n    n_vertices,\n    epochs_per_sample,\n    rng_state,\n    dim,\n    alpha,\n    epochs_per_negative_sample,\n    epoch_of_next_negative_sample,\n    epoch_of_next_sample,\n    n,\n    noise_level,\n):\n    for i in numba.prange(epochs_per_sample.shape[0]):\n        if epoch_of_next_sample[i] <= n:\n            j = head[i]\n            k = tail[i]\n\n            current = embedding[j]\n            other = embedding[k]\n\n            dist_squared = rdist(current, other)\n\n            if dist_squared > 0.0:\n                dist = np.sqrt(dist_squared)\n                grad_coeff = (-2.0 * noise_level * dist - 2.0) / (\n                    2.0 * dist_squared - 0.5 * dist + 1.0\n                )\n\n                for di in range(dim):\n                    grad_d = grad_coeff * (current[di] - other[di])\n\n                    current[di] += grad_d * alpha\n                    other[di] += -grad_d * alpha\n\n            epoch_of_next_sample[i] += epochs_per_sample[i]\n            n_neg_samples = int(\n                (n - epoch_of_next_negative_sample[i]) / epochs_per_negative_sample[i]\n            )\n\n            for p in range(n_neg_samples):\n                k = ((n + p) * i * rng_state) % n_vertices\n                other = embedding[k]\n                dist_squared = rdist(current, other)\n\n                if dist_squared > 1e-2:\n                    grad_coeff = 4.0 / ((1.0 + 0.25 * dist_squared) * dist_squared)\n\n                    for di in range(dim):\n                        grad_d = clip(grad_coeff * (current[di] - other[di]), -4, 4)\n                        current[di] += grad_d * alpha\n\n            epoch_of_next_negative_sample[i] += (\n                n_neg_samples * epochs_per_negative_sample[i]\n            )\n\n\n@numba.njit(\n    \"void(f4[:, ::1], u4[::1], u4[::1], u4, f4[::1], u4, u1, f4, f4[::1], f4[::1], f4[::1], u1, f4, f4, f4[:, ::1], u4[::1], u4)\",\n    fastmath=True,\n    parallel=True,\n    cache=True,\n    locals={\n        \"updates\": numba.types.float32[:, ::1],\n        \"from_node\": numba.types.intp,\n        \"to_node\": numba.types.intp,\n        \"raw_index\": numba.types.intp,\n        \"dist_squared\": numba.types.float32,\n        \"dist\": numba.types.float32,\n        \"grad_coeff\": numba.types.float32,\n        \"grad_d\": numba.types.float32,\n        \"current\": numba.types.float32[::1],\n        \"other\": numba.types.float32[::1],\n        \"block_start\": numba.types.intp,\n        \"block_end\": numba.types.intp,\n        \"node_idx\": numba.types.intp,\n        \"d\": numba.types.uint8,\n        \"n\": numba.types.uint8,\n        \"p\": numba.types.uint8,\n        \"n_neg_samples\": numba.types.uint8,\n    },\n)\ndef node_embedding_epoch_repr(\n    embedding,\n    csr_indptr,\n    csr_indices,\n    n_vertices,\n    epochs_per_sample,\n    rng_state,\n    dim,\n    alpha,\n    epochs_per_negative_sample,\n    epoch_of_next_negative_sample,\n    epoch_of_next_sample,\n    n,\n    noise_level,\n    gamma,\n    updates,\n    node_order,\n    block_size=4096,\n):\n    for block_start in range(0, n_vertices, block_size):\n        block_end = min(block_start + block_size, n_vertices)\n        for node_idx in numba.prange(block_start, block_end):\n            from_node = node_order[node_idx]\n            current = embedding[from_node]\n\n            for raw_index in range(csr_indptr[from_node], csr_indptr[from_node + 1]):\n                if epoch_of_next_sample[raw_index] <= n:\n                    to_node = csr_indices[raw_index]\n                    other = embedding[to_node]\n\n                    dist_squared = rdist(current, other)\n\n                    if dist_squared > 0.0:\n                        dist = np.sqrt(dist_squared)\n                        grad_coeff = (-2.0 * noise_level * dist - 2.0) / (\n                            2.0 * dist_squared - 0.5 * dist + 1.0\n                        )\n                        for d in range(dim):\n                            grad_d = grad_coeff * (current[d] - other[d])\n                            updates[from_node, d] += grad_d * alpha\n\n                    epoch_of_next_sample[raw_index] += epochs_per_sample[raw_index]\n\n                    n_neg_samples = int(\n                        (n - epoch_of_next_negative_sample[raw_index])\n                        / epochs_per_negative_sample[raw_index]\n                    )\n\n                    for p in range(n_neg_samples):\n                        to_node = node_order[\n                            (raw_index * (n + p + 1) * rng_state) % n_vertices\n                        ]\n                        other = embedding[to_node]\n\n                        dist_squared = rdist(current, other)\n\n                        if dist_squared > 1e-2:\n                            grad_coeff = (\n                                gamma\n                                * 4.0\n                                / ((1.0 + 0.25 * dist_squared) * dist_squared)\n                            )\n                            # grad_coeff /= n_neg_samples\n\n                            if grad_coeff > 0.0:\n                                for d in range(dim):\n                                    grad_d = clip(\n                                        grad_coeff * (current[d] - other[d]), -4, 4\n                                    )\n                                    updates[from_node, d] += grad_d * alpha\n\n                    epoch_of_next_negative_sample[raw_index] += (\n                        n_neg_samples * epochs_per_negative_sample[raw_index]\n                    )\n\n        for node_idx in numba.prange(block_start, block_end):\n            from_node = node_order[node_idx]\n            for d in range(dim):\n                embedding[from_node, d] += updates[from_node, d]\n\n\ndef node_embedding(\n    graph,\n    n_components,\n    n_epochs,\n    initial_embedding=None,\n    initial_alpha=0.5,\n    negative_sample_rate=1.0,\n    noise_level=0.5,\n    random_state=None,\n    reproducible_flag=True,\n    verbose=False,\n    tqdm_kwds={},\n):\n    \"\"\"Learn a low-dimensional embedding of a graph using a UMAP-like algorithm.\n\n    This function performs stochastic gradient descent optimization to learn a\n    low-dimensional embedding of graph structure. It uses both positive (connected\n    edges) and negative (random) samples to guide the optimization.\n\n    Parameters\n    ----------\n    graph : scipy.sparse matrix, typically csr_matrix or csc_matrix\n        A sparse adjacency matrix representing the graph. The weights in the matrix\n        represent connection strengths between nodes.\n\n    n_components : int\n        The number of dimensions in the output embedding.\n\n    n_epochs : int\n        The number of epochs to train the embedding.\n\n    initial_embedding : array-like of shape (n_vertices, n_components) or None, default=None\n        An initial embedding to use as a starting point. If None, a random\n        embedding is generated from a normal distribution with scale 0.25.\n\n    initial_alpha : float, default=0.5\n        The initial learning rate. The learning rate decays linearly over epochs.\n\n    negative_sample_rate : float, default=1.0\n        The rate at which negative samples are drawn relative to positive samples.\n        Controls the ratio of negative to positive updates per epoch.\n\n    noise_level : float, default=0.5\n        Controls the strength of noise in the gradient computation. Higher values\n        increase the tolerance for larger distances before penalizing in the\n        embedding space.\n\n    random_state : RandomState instance or None, default=None\n        Random state for reproducibility. If None, uses system randomness.\n\n    reproducible_flag : bool, default=True\n        If True, uses a deterministic (but slower) update strategy that processes\n        nodes in blocks for reproducibility. If False, uses a faster stochastic\n        approach.\n\n    verbose : bool, default=False\n        If True, display a progress bar during training.\n\n    tqdm_kwds : dict, default={}\n        Additional keyword arguments to pass to tqdm for progress bar customization.\n\n    Returns\n    -------\n    embedding : array-like of shape (n_vertices, n_components)\n        The learned low-dimensional embedding of the graph vertices.\n    \"\"\"\n    if random_state is None:\n        random_state = np.random.RandomState()\n    if initial_embedding is None:\n        embedding = random_state.normal(\n            scale=0.25, size=(graph.shape[0], n_components)\n        ).astype(np.float32, order=\"C\")\n    else:\n        embedding = initial_embedding\n\n    epochs_per_sample = make_epochs_per_sample(graph.data, n_epochs).astype(\n        np.float32, order=\"C\"\n    )\n    epochs_per_negative_sample = epochs_per_sample / negative_sample_rate\n    if reproducible_flag:\n        epochs_per_negative_sample *= 1.5\n    epoch_of_next_negative_sample = epochs_per_negative_sample.copy()\n    epoch_of_next_sample = epochs_per_sample.copy()\n\n    if tqdm_kwds is None:\n        tqdm_kwds = {}\n\n    if \"disable\" not in tqdm_kwds:\n        tqdm_kwds[\"disable\"] = not verbose\n\n    rng_val = random_state.randint(INT32_MAX, size=n_epochs)\n\n    coo_graph = graph.tocoo()\n    head_u4 = coo_graph.row.astype(np.uint32)\n    tail_u4 = coo_graph.col.astype(np.uint32)\n    # New\n    csr_indptr = graph.indptr.astype(np.uint32)\n    csr_indices = graph.indices.astype(np.uint32)\n    updates = np.zeros_like(embedding)\n    node_order = np.arange(graph.shape[0], dtype=np.uint32)\n    gamma_schedule = np.linspace(0.5, 1.5, n_epochs)\n    # End new\n    n_vertices = np.uint32(graph.shape[0])\n    block_size = max(1024, n_vertices // 8)\n    dim = np.uint8(embedding.shape[1])\n    alpha = np.float32(initial_alpha)\n\n    for n in tqdm(range(n_epochs), **tqdm_kwds):\n\n        if not reproducible_flag:\n            node_embedding_epoch(\n                embedding,\n                head_u4,\n                tail_u4,\n                n_vertices,\n                epochs_per_sample,\n                rng_val[n],\n                dim,\n                alpha,\n                epochs_per_negative_sample,\n                epoch_of_next_negative_sample,\n                epoch_of_next_sample,\n                n,\n                noise_level,\n            )\n        else:\n            node_embedding_epoch_repr(\n                embedding,\n                csr_indptr,\n                csr_indices,\n                n_vertices,\n                epochs_per_sample,\n                np.uint32(rng_val[n]),\n                dim,\n                alpha,\n                epochs_per_negative_sample,\n                epoch_of_next_negative_sample,\n                epoch_of_next_sample,\n                np.uint8(n),\n                np.float32(noise_level),\n                gamma_schedule[n],\n                updates,\n                node_order,\n                np.uint32(block_size),\n            )\n            updates *= (1.0 - alpha) ** 2 * 0.5\n            random_state.shuffle(node_order)\n        alpha = np.float32(initial_alpha * (1.0 - (float(n) / float(n_epochs))))\n\n    return embedding\n"
  },
  {
    "path": "evoc/numba_kdtree.py",
    "content": "import numba\nimport numpy as np\n\nfrom collections import namedtuple\n\nNumbaKDTree = namedtuple(\n    \"NumbaKDTree\",\n    [\"data\", \"idx_array\", \"idx_start\", \"idx_end\", \"radius\", \"is_leaf\", \"node_bounds\"],\n)\nNodeData = namedtuple(\"NodeData\", [\"idx_start\", \"idx_end\", \"radius\", \"is_leaf\"])\n\nNodeDataType = numba.types.NamedTuple(\n    [\n        numba.types.intp[::1],\n        numba.types.intp[::1],\n        numba.types.float32[::1],\n        numba.types.bool_[::1],\n    ],\n    NodeData,\n)\n# Create minimal sentinel instances at module level — zero cost\n_sentinel_kdtree = NumbaKDTree(\n    data=np.empty((1, 1), dtype=np.float32),\n    idx_array=np.empty(1, dtype=np.intp),\n    idx_start=np.empty(1, dtype=np.intp),\n    idx_end=np.empty(1, dtype=np.intp),\n    radius=np.empty(1, dtype=np.float32),\n    is_leaf=np.empty(1, dtype=np.bool_),\n    node_bounds=np.empty((2, 1, 1), dtype=np.float32),\n)\nNumbaKDTreeType = numba.typeof(_sentinel_kdtree)\n\n\ndef kdtree_to_numba(sklearn_kdtree):\n    data, idx_array, node_data, node_bounds = sklearn_kdtree.get_arrays()\n    return NumbaKDTree(\n        data,\n        idx_array,\n        node_data.idx_start,\n        node_data.idx_end,\n        node_data.radius,\n        node_data.is_leaf,\n        node_bounds,\n    )\n\n\n@numba.njit(\n    cache=True,\n    fastmath=True,\n    locals={\n        \"n_features\": numba.types.intp,\n        \"lower_bounds\": numba.types.float32[::1],\n        \"upper_bounds\": numba.types.float32[::1],\n        \"radius\": numba.types.float32,\n        \"diff\": numba.types.float32,\n        \"data_row\": numba.types.float32[::1],\n    },\n)\ndef _init_node(\n    data,\n    node_bounds,\n    idx_array,\n    idx_start_array,\n    idx_end_array,\n    radius_array,\n    is_leaf_array,\n    node,\n    idx_start,\n    idx_end,\n):\n\n    n_features = data.shape[1]\n    lower_bounds = node_bounds[0, node, :]\n    upper_bounds = node_bounds[1, node, :]\n\n    # determine Node bounds\n    for j in range(n_features):\n        lower_bounds[j] = np.inf\n        upper_bounds[j] = -np.inf\n\n    for i in range(idx_start, idx_end):\n        data_row = data[idx_array[i]]\n        for j in range(n_features):\n            lower_bounds[j] = min(lower_bounds[j], data_row[j])\n            upper_bounds[j] = max(upper_bounds[j], data_row[j])\n\n    radius = 0.0\n    for j in range(n_features):\n        diff = abs(upper_bounds[j] - lower_bounds[j]) * 0.5\n        radius += diff * diff\n\n    idx_start_array[node] = idx_start\n    idx_end_array[node] = idx_end\n\n    radius_array[node] = np.sqrt(radius)\n\n\n@numba.njit(\n    \"intp(float32[:,::1], intp[::1], intp, intp)\",\n    cache=True,\n    locals={\n        \"n_features\": numba.types.intp,\n        \"result\": numba.types.intp,\n        \"max_spread\": numba.types.float32,\n        \"j\": numba.types.intp,\n        \"i\": numba.types.intp,\n        \"max_val\": numba.types.float32,\n        \"min_val\": numba.types.float32,\n        \"val\": numba.types.float32,\n        \"spread\": numba.types.float32,\n    },\n)\ndef _find_node_split_dim(data, idx_array, idx_start, idx_end):\n    n_features = data.shape[1]\n    result = 0\n    max_spread = 0\n\n    for j in range(n_features):\n        max_val = data[idx_array[idx_start], j]\n        min_val = max_val\n        for i in range(idx_start + 1, idx_end):\n            val = data[idx_array[i], j]\n            max_val = max(max_val, val)\n            min_val = min(min_val, val)\n\n        spread = max_val - min_val\n\n        if spread > max_spread:\n            max_spread = spread\n            result = j\n\n    return result\n\n\n@numba.njit(\n    \"int8(float32[:,::1], intp, intp, intp)\",\n    fastmath=True,\n    cache=True,\n    locals={\n        \"val1\": numba.types.float32,\n        \"val2\": numba.types.float32,\n    },\n)\ndef _compare_indices(data, axis, idx1, idx2):\n    val1 = data[idx1, axis]\n    val2 = data[idx2, axis]\n\n    if val1 < val2:\n        return -1\n    elif val1 > val2:\n        return 1\n    else:\n        # Break ties using original index values (like sklearn)\n        if idx1 < idx2:\n            return -1\n        elif idx1 > idx2:\n            return 1\n        else:\n            return 0\n\n\n@numba.njit(\n    \"void(float32[:,::1], intp[::1], intp, intp, intp)\",\n    fastmath=True,\n    cache=True,\n    locals={\n        \"i\": numba.types.intp,\n        \"key_idx\": numba.types.intp,\n        \"j\": numba.types.intp,\n    },\n)\ndef _insertion_sort_indices(data, idx_array, axis, left, right):\n    for i in range(left + 1, right):\n        key_idx = idx_array[i]\n        j = i - 1\n\n        while j >= left and _compare_indices(data, axis, idx_array[j], key_idx) > 0:\n            idx_array[j + 1] = idx_array[j]\n            j -= 1\n\n        idx_array[j + 1] = key_idx\n\n\n@numba.njit(\n    \"void(float32[:,::1], intp[::1], intp, intp, intp, intp)\",\n    fastmath=True,\n    cache=True,\n    locals={\n        \"root\": numba.types.intp,\n        \"child\": numba.types.intp,\n        \"swap\": numba.types.intp,\n    },\n)\ndef _sift_down_indices(data, idx_array, axis, offset, start, end):\n    root = start\n\n    while root * 2 + 1 < end:\n        child = root * 2 + 1\n        swap = root\n\n        if (\n            _compare_indices(\n                data, axis, idx_array[offset + swap], idx_array[offset + child]\n            )\n            < 0\n        ):\n            swap = child\n\n        if (\n            child + 1 < end\n            and _compare_indices(\n                data, axis, idx_array[offset + swap], idx_array[offset + child + 1]\n            )\n            < 0\n        ):\n            swap = child + 1\n\n        if swap == root:\n            return\n\n        idx_array[offset + root], idx_array[offset + swap] = (\n            idx_array[offset + swap],\n            idx_array[offset + root],\n        )\n        root = swap\n\n\n@numba.njit(\n    \"void(float32[:,::1], intp[::1], intp, intp, intp)\",\n    cache=True,\n    locals={\n        \"size\": numba.types.intp,\n        \"i\": numba.types.intp,\n    },\n)\ndef _heapsort_indices(data, idx_array, axis, left, right):\n    size = right - left\n\n    # Build heap\n    for i in range(size // 2 - 1, -1, -1):\n        _sift_down_indices(data, idx_array, axis, left, i, size)\n\n    # Extract elements\n    for i in range(size - 1, 0, -1):\n        idx_array[left], idx_array[left + i] = idx_array[left + i], idx_array[left]\n        _sift_down_indices(data, idx_array, axis, left, 0, i)\n\n\n@numba.njit(\n    \"intp(float32[:,::1], intp[::1], intp, intp, intp)\",\n    fastmath=True,\n    cache=True,\n    locals={\n        \"mid\": numba.types.intp,\n        \"idx_left\": numba.types.intp,\n        \"idx_mid\": numba.types.intp,\n        \"idx_right\": numba.types.intp,\n    },\n)\ndef _median_of_three_pivot(data, idx_array, axis, left, right):\n    mid = (left + right - 1) // 2\n\n    idx_left = idx_array[left]\n    idx_mid = idx_array[mid]\n    idx_right = idx_array[right - 1]\n\n    # Sort the three candidates\n    if _compare_indices(data, axis, idx_left, idx_mid) > 0:\n        idx_array[left], idx_array[mid] = idx_array[mid], idx_array[left]\n        idx_left, idx_mid = idx_mid, idx_left\n\n    if _compare_indices(data, axis, idx_mid, idx_right) > 0:\n        idx_array[mid], idx_array[right - 1] = idx_array[right - 1], idx_array[mid]\n        idx_mid, idx_right = idx_right, idx_mid\n\n        if _compare_indices(data, axis, idx_left, idx_mid) > 0:\n            idx_array[left], idx_array[mid] = idx_array[mid], idx_array[left]\n\n    return mid\n\n\n@numba.njit(\n    \"intp(float32[:,::1], intp[::1], intp, intp, intp, intp)\",\n    fastmath=True,\n    cache=True,\n    locals={\n        \"pivot_value\": numba.types.float32,\n        \"pivot_original_idx\": numba.types.intp,\n        \"i\": numba.types.intp,\n        \"j\": numba.types.intp,\n    },\n)\ndef _partition_indices(data, idx_array, axis, left, right, pivot_idx):\n    # Move pivot to end\n    idx_array[pivot_idx], idx_array[right - 1] = (\n        idx_array[right - 1],\n        idx_array[pivot_idx],\n    )\n    pivot_value = data[idx_array[right - 1], axis]\n    pivot_original_idx = idx_array[right - 1]\n\n    i = left\n    j = right - 2\n\n    while True:\n        # Find element from left that should be on right\n        while (\n            i <= j\n            and _compare_indices(data, axis, idx_array[i], pivot_original_idx) < 0\n        ):\n            i += 1\n\n        # Find element from right that should be on left\n        while (\n            i <= j\n            and _compare_indices(data, axis, idx_array[j], pivot_original_idx) >= 0\n        ):\n            j -= 1\n\n        if i >= j:\n            break\n\n        # Swap elements\n        idx_array[i], idx_array[j] = idx_array[j], idx_array[i]\n        i += 1\n        j -= 1\n\n    # Move pivot to final position\n    idx_array[i], idx_array[right - 1] = idx_array[right - 1], idx_array[i]\n    return i\n\n\n@numba.njit(\n    \"void(float32[:,::1], intp[::1], intp, intp, intp, intp, intp)\",\n    cache=True,\n    locals={\n        \"pivot_idx\": numba.types.intp,\n        \"pivot_pos\": numba.types.intp,\n    },\n)\ndef _introselect_impl(data, idx_array, axis, left, right, nth, depth_limit):\n    while right - left > 16:\n        if depth_limit == 0:\n            # Fall back to heapsort when recursion gets too deep\n            _heapsort_indices(data, idx_array, axis, left, right)\n            return\n\n        depth_limit -= 1\n\n        # Choose pivot using median-of-three\n        pivot_idx = _median_of_three_pivot(data, idx_array, axis, left, right)\n\n        # Partition around pivot\n        pivot_pos = _partition_indices(data, idx_array, axis, left, right, pivot_idx)\n\n        # Recurse on the appropriate side\n        if nth < pivot_pos:\n            right = pivot_pos\n        elif nth > pivot_pos:\n            left = pivot_pos + 1\n        else:\n            # Found the nth element\n            return\n\n    # Use insertion sort for small subarrays\n    _insertion_sort_indices(data, idx_array, axis, left, right)\n\n\n@numba.njit(\n    \"void(float32[:,::1], intp[::1], intp, intp, intp, intp)\",\n    cache=True,\n    locals={\n        \"size\": numba.types.intp,\n        \"max_depth\": numba.types.intp,\n    },\n)\ndef _introselect(data, idx_array, axis, left, right, nth):\n    size = right - left\n\n    # Use heapsort for small arrays or when recursion depth is too high\n    if size <= 16:\n        _insertion_sort_indices(data, idx_array, axis, left, right)\n        return\n\n    # Calculate maximum recursion depth (2 * log2(size))\n    max_depth = 2 * int(np.log2(size))\n    _introselect_impl(data, idx_array, axis, left, right, nth, max_depth)\n\n\n@numba.njit(\n    \"void(float32[:, ::1], intp[::1], intp[::1], intp[::1], float32[::1], bool_[::1], float32[:, :, ::1], intp, intp, intp)\",\n    cache=True,\n)\ndef _recursive_build_tree(\n    data,\n    idx_array,\n    idx_start_array,\n    idx_end_array,\n    radius_array,\n    is_leaf_array,\n    node_bounds,\n    idx_start,\n    idx_end,\n    node,\n):\n    n_points = idx_end - idx_start\n    n_mid = n_points // 2\n\n    _init_node(\n        data,\n        node_bounds,\n        idx_array,\n        idx_start_array,\n        idx_end_array,\n        radius_array,\n        is_leaf_array,\n        node,\n        idx_start,\n        idx_end,\n    )\n\n    if 2 * node + 1 >= is_leaf_array.shape[0]:\n        is_leaf_array[node] = True\n    elif idx_end - idx_start < 2:\n        is_leaf_array[node] = True\n    else:\n        is_leaf_array[node] = False\n        axis = _find_node_split_dim(data, idx_array, idx_start, idx_end)\n        _introselect(data, idx_array, axis, idx_start, idx_end, idx_start + n_mid)\n        _recursive_build_tree(\n            data,\n            idx_array,\n            idx_start_array,\n            idx_end_array,\n            radius_array,\n            is_leaf_array,\n            node_bounds,\n            idx_start,\n            idx_start + n_mid,\n            2 * node + 1,\n        )\n        _recursive_build_tree(\n            data,\n            idx_array,\n            idx_start_array,\n            idx_end_array,\n            radius_array,\n            is_leaf_array,\n            node_bounds,\n            idx_start + n_mid,\n            idx_end,\n            2 * node + 2,\n        )\n\n    return\n\n\ndef build_kdtree(data, leaf_size=40):\n    n_samples = data.shape[0]\n    n_features = data.shape[1]\n\n    if leaf_size < 1:\n        raise ValueError(\"leaf_size must be greater than or equal to 1\")\n\n    # determine number of levels in the tree, and from this\n    # the number of nodes in the tree.  This results in leaf nodes\n    # with numbers of points between leaf_size and 2 * leaf_size\n    n_levels = int(np.log2(max(1, (n_samples - 1) / leaf_size)) + 1)\n    n_nodes = np.int32((2**n_levels) - 1)\n\n    # allocate arrays for storage\n    idx_array = np.arange(n_samples, dtype=np.intp)\n    idx_start_array = np.zeros(n_nodes, dtype=np.intp)\n    idx_end_array = np.zeros(n_nodes, dtype=np.intp)\n    radius_array = np.zeros(n_nodes, dtype=np.float32)\n    is_leaf_array = np.zeros(n_nodes, dtype=np.bool_)\n    node_bounds = np.zeros((2, n_nodes, n_features), dtype=np.float32)\n\n    _recursive_build_tree(\n        data,\n        idx_array,\n        idx_start_array,\n        idx_end_array,\n        radius_array,\n        is_leaf_array,\n        node_bounds,\n        0,\n        n_samples,\n        0,\n    )\n\n    return NumbaKDTree(\n        data,\n        idx_array,\n        idx_start_array,\n        idx_end_array,\n        radius_array,\n        is_leaf_array,\n        node_bounds,\n    )\n\n\n@numba.njit(\n    [\n        \"f4(f4[::1],f4[::1])\",\n        \"f8(f8[::1],f8[::1])\",\n        \"f8(f4[::1],f8[::1])\",\n    ],\n    fastmath=True,\n    cache=True,\n    locals={\n        \"dim\": numba.types.intp,\n        \"i\": numba.types.uint16,\n        \"diff\": numba.types.float32,\n        \"result\": numba.types.float32,\n    },\n)\ndef rdist(x, y):\n    result = 0.0\n    dim = x.shape[0]\n    for i in range(dim):\n        diff = x[i] - y[i]\n        result += diff * diff\n\n    return result\n\n\n@numba.njit(\n    [\n        \"f4(f4[::1],f4[::1],f4[::1])\",\n        \"f4(f8[::1],f8[::1],f4[::1])\",\n        \"f4(f8[::1],f8[::1],f8[::1])\",\n    ],\n    fastmath=True,\n    cache=True,\n    locals={\n        \"dim\": numba.types.intp,\n        \"i\": numba.types.uint16,\n        \"d_lo\": numba.types.float32,\n        \"d_hi\": numba.types.float32,\n        \"d\": numba.types.float32,\n        \"result\": numba.types.float32,\n    },\n)\ndef point_to_node_lower_bound_rdist(upper, lower, pt):\n    result = 0.0\n    dim = pt.shape[0]\n    for i in range(dim):\n        d_lo = upper[i] - pt[i] if upper[i] > pt[i] else 0.0\n        d_hi = pt[i] - lower[i] if pt[i] > lower[i] else 0.0\n        d = d_lo + d_hi\n        result += d * d\n\n    return result\n\n\n@numba.njit(\n    [\n        \"i4(f4[::1],i4[::1],f4,i4)\",\n        \"i4(f8[::1],i4[::1],f8,i4)\",\n    ],\n    fastmath=True,\n    locals={\n        \"size\": numba.types.intp,\n        \"i\": numba.types.uint16,\n        \"ic1\": numba.types.uint16,\n        \"ic2\": numba.types.uint16,\n        \"i_swap\": numba.types.uint16,\n    },\n    cache=True,\n)\ndef simple_heap_push(priorities, indices, p, n):\n    if p >= priorities[0]:\n        return 0\n\n    size = priorities.shape[0]\n\n    # insert val at position zero\n    priorities[0] = p\n    indices[0] = n\n\n    # descend the heap, swapping values until the max heap criterion is met\n    i = 0\n    while True:\n        ic1 = 2 * i + 1\n        ic2 = ic1 + 1\n\n        if ic1 >= size:\n            break\n        elif ic2 >= size:\n            if priorities[ic1] > p:\n                i_swap = ic1\n            else:\n                break\n        elif priorities[ic1] >= priorities[ic2]:\n            if p < priorities[ic1]:\n                i_swap = ic1\n            else:\n                break\n        else:\n            if p < priorities[ic2]:\n                i_swap = ic2\n            else:\n                break\n\n        priorities[i] = priorities[i_swap]\n        indices[i] = indices[i_swap]\n\n        i = i_swap\n\n    priorities[i] = p\n    indices[i] = n\n\n    return 1\n\n\n@numba.njit(\n    fastmath=True,\n    cache=True,\n    locals={\n        \"left_child\": numba.types.intp,\n        \"right_child\": numba.types.intp,\n        \"swap\": numba.types.intp,\n    },\n)\ndef siftdown(heap1, heap2, elt):\n    while elt * 2 + 1 < heap1.shape[0]:\n        left_child = elt * 2 + 1\n        right_child = left_child + 1\n        swap = elt\n\n        if heap1[swap] < heap1[left_child]:\n            swap = left_child\n\n        if right_child < heap1.shape[0] and heap1[swap] < heap1[right_child]:\n            swap = right_child\n\n        if swap == elt:\n            break\n        else:\n            heap1[elt], heap1[swap] = heap1[swap], heap1[elt]\n            heap2[elt], heap2[swap] = heap2[swap], heap2[elt]\n            elt = swap\n\n\n@numba.njit(parallel=True, cache=True)\ndef deheap_sort(distances, indices):\n    for i in numba.prange(indices.shape[0]):\n        # starting from the end of the array and moving back\n        for j in range(indices.shape[1] - 1, 0, -1):\n            indices[i, 0], indices[i, j] = indices[i, j], indices[i, 0]\n            distances[i, 0], distances[i, j] = distances[i, j], distances[i, 0]\n\n            siftdown(distances[i, :j], indices[i, :j], 0)\n\n    return distances, indices\n\n\n@numba.njit(\n    numba.void(\n        NumbaKDTreeType,\n        numba.types.intp,\n        numba.float32[::1],\n        numba.float32[::1],\n        numba.int32[::1],\n        numba.float32,\n    ),\n    fastmath=True,\n    cache=True,\n    locals={\n        \"node\": numba.types.intp,\n        \"left\": numba.types.intp,\n        \"right\": numba.types.intp,\n        \"d\": numba.types.float32,\n        \"idx\": numba.types.uint32,\n        \"idx_start\": numba.types.intp,\n        \"idx_end\": numba.types.intp,\n        \"is_leaf\": numba.types.boolean,\n        \"i\": numba.types.intp,\n        \"dist_lower_bound_left\": numba.types.float32,\n        \"dist_lower_bound_right\": numba.types.float32,\n    },\n)\ndef tree_query_recursion(\n    tree,\n    node,\n    point,\n    heap_p,\n    heap_i,\n    dist_lower_bound,\n):\n    # Get node information\n    idx_start = tree.idx_start[node]\n    idx_end = tree.idx_end[node]\n    is_leaf = tree.is_leaf[node]\n\n    # ------------------------------------------------------------\n    # Case 1: query point is outside node radius:\n    #         trim it from the query\n    if dist_lower_bound > heap_p[0]:\n        return\n\n    # ------------------------------------------------------------\n    # Case 2: this is a leaf node.  Update set of nearby points\n    elif is_leaf:\n        for i in range(idx_start, idx_end):\n            idx = tree.idx_array[i]\n            d = rdist(point, tree.data[idx])\n            if d < heap_p[0]:\n                simple_heap_push(heap_p, heap_i, d, idx)\n\n    # ------------------------------------------------------------\n    # Case 3: Node is not a leaf.  Recursively query subnodes\n    #         starting with the closest\n    else:\n        left = 2 * node + 1\n        right = left + 1\n        dist_lower_bound_left = point_to_node_lower_bound_rdist(\n            tree.node_bounds[0, left], tree.node_bounds[1, left], point\n        )\n        dist_lower_bound_right = point_to_node_lower_bound_rdist(\n            tree.node_bounds[0, right], tree.node_bounds[1, right], point\n        )\n\n        # recursively query subnodes\n        if dist_lower_bound_left <= dist_lower_bound_right:\n            tree_query_recursion(\n                tree, left, point, heap_p, heap_i, dist_lower_bound_left\n            )\n            tree_query_recursion(\n                tree, right, point, heap_p, heap_i, dist_lower_bound_right\n            )\n        else:\n            tree_query_recursion(\n                tree, right, point, heap_p, heap_i, dist_lower_bound_right\n            )\n            tree_query_recursion(\n                tree, left, point, heap_p, heap_i, dist_lower_bound_left\n            )\n\n    return\n\n\n@numba.njit(\n    numba.types.Tuple((numba.float32[:, ::1], numba.int32[:, ::1]))(\n        NumbaKDTreeType,\n        numba.float32[:, ::1],\n        numba.int64,\n        numba.types.boolean,\n    ),\n    parallel=True,\n    fastmath=True,\n    cache=True,\n    locals={\n        \"i\": numba.types.intp,\n        \"distance_lower_bound\": numba.types.float32,\n    },\n)\ndef parallel_tree_query(\n    tree, data, k=numba.int64(10), output_rdist=numba.types.boolean(False)\n):\n    result = (\n        np.full((data.shape[0], k), np.inf, dtype=np.float32),\n        np.full((data.shape[0], k), -1, dtype=np.int32),\n    )\n\n    for i in numba.prange(data.shape[0]):\n        distance_lower_bound = point_to_node_lower_bound_rdist(\n            tree.node_bounds[0, 0], tree.node_bounds[1, 0], data[i]\n        )\n        heap_priorities, heap_indices = result[0][i], result[1][i]\n        tree_query_recursion(\n            tree,\n            numba.intp(0),\n            data[i],\n            heap_priorities,\n            heap_indices,\n            distance_lower_bound,\n        )\n\n    if output_rdist:\n        return deheap_sort(result[0], result[1])\n    else:\n        return deheap_sort(np.sqrt(result[0]), result[1])\n"
  },
  {
    "path": "evoc/tests/test_boruvka.py",
    "content": "\"\"\"\nComprehensive test suite for the boruvka module.\n\nThis module tests Boruvka's algorithm implementation for minimum spanning tree\nconstruction, including component merging, tree queries, and parallel processing.\n\"\"\"\n\nimport numpy as np\nimport pytest\nimport numba\nfrom sklearn.datasets import make_blobs\nfrom sklearn.preprocessing import StandardScaler\n\nfrom evoc.boruvka import (\n    merge_components,\n    update_component_vectors,\n    boruvka_tree_query,\n    boruvka_tree_query_reproducible,\n    initialize_boruvka_from_knn,\n    parallel_boruvka,\n    calculate_block_size,\n    component_aware_query_recursion,\n)\nfrom evoc.numba_kdtree import build_kdtree\nfrom evoc.disjoint_set import ds_rank_create, ds_find, ds_union_by_rank\n\n\nclass TestMergeComponents:\n    \"\"\"Test component merging functionality.\"\"\"\n\n    def test_merge_components_basic(self):\n        \"\"\"Test basic component merging with simple data.\"\"\"\n        # Create a simple disjoint set with 4 components\n        disjoint_set = ds_rank_create(4)\n\n        # Candidate neighbors: each point's nearest neighbor in different component\n        candidate_neighbors = np.array([1, 0, 3, 2], dtype=np.int32)\n        candidate_distances = np.array([1.0, 1.0, 2.0, 2.0], dtype=np.float32)\n        point_components = np.array([0, 1, 2, 3], dtype=np.int64)\n\n        result = merge_components(\n            disjoint_set, candidate_neighbors, candidate_distances, point_components\n        )\n\n        # Should have edges connecting components\n        assert result.shape[0] >= 1\n        assert result.shape[1] == 3  # from, to, distance\n\n        # Distances should be positive\n        assert np.all(result[:, 2] >= 0)\n\n        # Edges should connect different components\n        for i in range(result.shape[0]):\n            from_comp = ds_find(disjoint_set, int(result[i, 0]))\n            to_comp = ds_find(disjoint_set, int(result[i, 1]))\n            # After merging, they should be in same component\n            assert from_comp == to_comp\n\n    def test_merge_components_empty(self):\n        \"\"\"Test merge components with no valid edges.\"\"\"\n        disjoint_set = ds_rank_create(2)\n        # Pre-merge the components\n        ds_union_by_rank(disjoint_set, 0, 1)\n\n        candidate_neighbors = np.array([1, 0], dtype=np.int32)\n        candidate_distances = np.array([1.0, 1.0], dtype=np.float32)\n        point_components = np.array([0, 0], dtype=np.int64)  # Same component\n\n        result = merge_components(\n            disjoint_set, candidate_neighbors, candidate_distances, point_components\n        )\n\n        # Should have no edges since all points are in same component\n        assert result.shape[0] == 0\n\n    def test_merge_components_best_edge_selection(self):\n        \"\"\"Test that merge_components selects the best edge from each component.\"\"\"\n        disjoint_set = ds_rank_create(6)\n\n        # Component 0: points 0,1 - best edge from 0 should be selected\n        # Component 1: points 2,3 - best edge from 2 should be selected\n        # Component 2: points 4,5 - best edge from 4 should be selected\n        ds_union_by_rank(disjoint_set, 0, 1)\n        ds_union_by_rank(disjoint_set, 2, 3)\n        ds_union_by_rank(disjoint_set, 4, 5)\n\n        # Update point components to reflect merging\n        point_components = np.array(\n            [\n                ds_find(disjoint_set, 0),\n                ds_find(disjoint_set, 1),\n                ds_find(disjoint_set, 2),\n                ds_find(disjoint_set, 3),\n                ds_find(disjoint_set, 4),\n                ds_find(disjoint_set, 5),\n            ],\n            dtype=np.int64,\n        )\n\n        # Each point has a candidate neighbor - different distances\n        candidate_neighbors = np.array([2, 2, 0, 0, 0, 0], dtype=np.int32)\n        candidate_distances = np.array([3.0, 1.0, 2.0, 4.0, 1.5, 2.5], dtype=np.float32)\n\n        result = merge_components(\n            disjoint_set, candidate_neighbors, candidate_distances, point_components\n        )\n\n        # Should select best edges from each component\n        assert result.shape[0] >= 1\n        assert result.shape[0] <= 3  # At most 3 components to merge\n\n\nclass TestUpdateComponentVectors:\n    \"\"\"Test component vector updates.\"\"\"\n\n    @pytest.fixture\n    def simple_tree_and_components(self):\n        \"\"\"Create a simple tree and component structure for testing.\"\"\"\n        # Create simple 2D data\n        data = np.array(\n            [\n                [0.0, 0.0],\n                [0.1, 0.1],  # Component 0\n                [1.0, 1.0],\n                [1.1, 1.1],  # Component 1\n                [2.0, 2.0],\n                [2.1, 2.1],  # Component 2\n            ],\n            dtype=np.float32,\n        )\n\n        tree = build_kdtree(data, leaf_size=2)\n\n        # Create disjoint set and merge some components\n        disjoint_set = ds_rank_create(6)\n        ds_union_by_rank(disjoint_set, 0, 1)  # Merge 0,1\n        ds_union_by_rank(disjoint_set, 2, 3)  # Merge 2,3\n        ds_union_by_rank(disjoint_set, 4, 5)  # Merge 4,5\n\n        point_components = np.array(\n            [ds_find(disjoint_set, i) for i in range(6)], dtype=np.int64\n        )\n\n        node_components = np.full(tree.idx_start.shape[0], -1, dtype=np.int64)\n\n        return tree, disjoint_set, point_components, node_components\n\n    def test_update_component_vectors_basic(self, simple_tree_and_components):\n        \"\"\"Test basic component vector update.\"\"\"\n        tree, disjoint_set, point_components, node_components = (\n            simple_tree_and_components\n        )\n\n        update_component_vectors(tree, disjoint_set, node_components, point_components)\n\n        # Point components should be updated to component roots\n        unique_components = np.unique(point_components)\n        assert len(unique_components) == 3  # Should have 3 components\n\n        # Check that merged points have same component\n        assert point_components[0] == point_components[1]  # Points 0,1 merged\n        assert point_components[2] == point_components[3]  # Points 2,3 merged\n        assert point_components[4] == point_components[5]  # Points 4,5 merged\n\n    def test_update_component_vectors_leaf_nodes(self, simple_tree_and_components):\n        \"\"\"Test that leaf nodes are correctly labeled when all points have same component.\"\"\"\n        tree, disjoint_set, point_components, node_components = (\n            simple_tree_and_components\n        )\n\n        # Merge all components into one\n        for i in range(1, 6):\n            ds_union_by_rank(disjoint_set, 0, i)\n\n        # Update point components\n        for i in range(6):\n            point_components[i] = ds_find(disjoint_set, i)\n\n        update_component_vectors(tree, disjoint_set, node_components, point_components)\n\n        # All point components should be the same\n        assert len(np.unique(point_components)) == 1\n\n        # All leaf nodes should have the same component as their points\n        for i in range(tree.idx_start.shape[0]):\n            if tree.is_leaf[i]:\n                # All points in this leaf should have same component\n                start, end = tree.idx_start[i], tree.idx_end[i]\n                if end > start:  # Non-empty leaf\n                    leaf_components = [\n                        point_components[tree.idx_array[j]] for j in range(start, end)\n                    ]\n                    if len(set(leaf_components)) == 1:  # All same component\n                        assert node_components[i] == leaf_components[0]\n\n\nclass TestBoruvkaTreeQuery:\n    \"\"\"Test tree query functionality for Boruvka's algorithm.\"\"\"\n\n    @pytest.fixture\n    def query_test_data(self):\n        \"\"\"Create test data for tree queries.\"\"\"\n        # Create well-separated clusters\n        np.random.seed(42)\n        data = np.vstack(\n            [\n                np.random.normal([0, 0], 0.1, (10, 2)),  # Cluster 0\n                np.random.normal([2, 0], 0.1, (10, 2)),  # Cluster 1\n                np.random.normal([0, 2], 0.1, (10, 2)),  # Cluster 2\n            ]\n        ).astype(np.float32)\n\n        tree = build_kdtree(data, leaf_size=5)\n\n        # Create component structure - each cluster is a component\n        disjoint_set = ds_rank_create(30)\n        point_components = np.array([i // 10 for i in range(30)], dtype=np.int64)\n        node_components = np.full(tree.idx_start.shape[0], -1, dtype=np.int64)\n        core_distances = np.zeros(30, dtype=np.float32)\n\n        return tree, node_components, point_components, core_distances\n\n    def test_boruvka_tree_query_basic(self, query_test_data):\n        \"\"\"Test basic tree query functionality.\"\"\"\n        tree, node_components, point_components, core_distances = query_test_data\n\n        # Update node components\n        disjoint_set = ds_rank_create(30)\n        for i in range(30):\n            for j in range(i + 1, min(i + 10, 30)):\n                if i // 10 == j // 10:  # Same cluster\n                    ds_union_by_rank(disjoint_set, i, j)\n\n        update_component_vectors(tree, disjoint_set, node_components, point_components)\n\n        distances, indices = boruvka_tree_query(\n            tree, node_components, point_components, core_distances\n        )\n\n        # Should find nearest neighbors in different components\n        assert distances.shape[0] == 30\n        assert indices.shape[0] == 30\n\n        # All distances should be finite (found neighbors)\n        assert np.all(np.isfinite(distances))\n\n        # All indices should be valid\n        assert np.all(indices >= 0)\n        assert np.all(indices < 30)\n\n        # Neighbors should be in different components\n        for i in range(30):\n            if indices[i] >= 0:\n                assert point_components[i] != point_components[indices[i]]\n\n    def test_boruvka_tree_query_reproducible(self, query_test_data):\n        \"\"\"Test reproducible tree query gives consistent results.\"\"\"\n        tree, node_components, point_components, core_distances = query_test_data\n\n        # Update node components\n        disjoint_set = ds_rank_create(30)\n        for i in range(30):\n            for j in range(i + 1, min(i + 10, 30)):\n                if i // 10 == j // 10:  # Same cluster\n                    ds_union_by_rank(disjoint_set, i, j)\n\n        update_component_vectors(tree, disjoint_set, node_components, point_components)\n\n        # Run multiple times with same block size\n        block_size = 8\n        results = []\n        for _ in range(3):\n            distances, indices = boruvka_tree_query_reproducible(\n                tree, node_components, point_components, core_distances, block_size\n            )\n            results.append((distances.copy(), indices.copy()))\n\n        # Results should be fairly similar (may have small variations due to ties)\n        for i in range(1, len(results)):\n            # Check that indices are valid and neighbors are in different components\n            distances_i, indices_i = results[i]\n            distances_0, indices_0 = results[0]\n\n            # All distances should be positive and finite\n            assert np.all(np.isfinite(distances_i))\n            assert np.all(distances_i > 0)\n\n            # All neighbors should be in different components\n            for j in range(30):\n                if indices_i[j] >= 0:\n                    assert point_components[j] != point_components[indices_i[j]]\n\n    def test_boruvka_query_different_block_sizes(self, query_test_data):\n        \"\"\"Test that different block sizes give same results.\"\"\"\n        tree, node_components, point_components, core_distances = query_test_data\n\n        # Update node components\n        disjoint_set = ds_rank_create(30)\n        for i in range(30):\n            for j in range(i + 1, min(i + 10, 30)):\n                if i // 10 == j // 10:  # Same cluster\n                    ds_union_by_rank(disjoint_set, i, j)\n\n        update_component_vectors(tree, disjoint_set, node_components, point_components)\n\n        # Test different block sizes\n        block_sizes = [4, 8, 16, 30]\n        results = []\n\n        for block_size in block_sizes:\n            distances, indices = boruvka_tree_query_reproducible(\n                tree, node_components, point_components, core_distances, block_size\n            )\n            results.append((distances.copy(), indices.copy()))\n\n        # All results should be valid (may have variations due to ties in nearest neighbors)\n        for i in range(1, len(results)):\n            distances_i, indices_i = results[i]\n\n            # All distances should be positive and finite\n            assert np.all(np.isfinite(distances_i))\n            assert np.all(distances_i > 0)\n\n            # All neighbors should be in different components\n            for j in range(30):\n                if indices_i[j] >= 0:\n                    assert point_components[j] != point_components[indices_i[j]]\n\n\nclass TestInitializeBoruvkaFromKNN:\n    \"\"\"Test initialization of Boruvka from k-nearest neighbors.\"\"\"\n\n    def test_initialize_boruvka_from_knn_basic(self):\n        \"\"\"Test basic initialization from k-NN.\"\"\"\n        # Create simple k-NN data\n        knn_indices = np.array(\n            [\n                [0, 1, 2],  # Point 0's neighbors: self, 1, 2\n                [1, 0, 2],  # Point 1's neighbors: self, 0, 2\n                [2, 0, 1],  # Point 2's neighbors: self, 0, 1\n            ],\n            dtype=np.int32,\n        )\n\n        knn_distances = np.array(\n            [\n                [0.0, 1.0, 2.0],\n                [0.0, 1.0, 2.0],\n                [0.0, 1.5, 2.5],\n            ],\n            dtype=np.float32,\n        )\n\n        core_distances = np.array([1.0, 1.0, 1.5], dtype=np.float32)\n        disjoint_set = ds_rank_create(3)\n\n        result = initialize_boruvka_from_knn(\n            knn_indices, knn_distances, core_distances, disjoint_set\n        )\n\n        # Should have edges connecting components\n        assert result.shape[0] >= 1\n        assert result.shape[1] == 3\n\n        # Edge weights should match core distances\n        for i in range(result.shape[0]):\n            from_point = int(result[i, 0])\n            assert result[i, 2] == core_distances[from_point]\n\n    def test_initialize_boruvka_core_distance_constraint(self):\n        \"\"\"Test that initialization respects core distance constraints.\"\"\"\n        # Point 0 has high core distance, should connect to point with lower core distance\n        knn_indices = np.array(\n            [\n                [0, 1],  # Point 0's neighbors: self, 1\n                [1, 0],  # Point 1's neighbors: self, 0\n            ],\n            dtype=np.int32,\n        )\n\n        knn_distances = np.array(\n            [\n                [0.0, 1.0],  # Point 0 distances (squared distances)\n                [0.0, 1.0],  # Point 1 distances (squared distances)\n            ],\n            dtype=np.float32,\n        )\n\n        # Point 0 has higher core distance than point 1\n        core_distances = np.array([2.0, 1.0], dtype=np.float32)\n        disjoint_set = ds_rank_create(2)\n\n        result = initialize_boruvka_from_knn(\n            knn_indices, knn_distances, core_distances, disjoint_set\n        )\n\n        # Should create edge from point 0 to point 1 (lower core distance)\n        assert result.shape[0] == 1\n        assert result[0, 0] == 0  # From point 0\n        assert result[0, 1] == 1  # To point 1\n        assert (\n            result[0, 2] == 2.0\n        )  # Weight is max(core_distance[0], distance) = max(2.0, 1.0) = 2.0\n\n\nclass TestCalculateBlockSize:\n    \"\"\"Test block size calculation for adaptive processing.\"\"\"\n\n    def test_calculate_block_size_basic(self):\n        \"\"\"Test basic block size calculation.\"\"\"\n        num_threads = 4\n\n        # Test different scenarios\n        scenarios = [\n            (10, 100, 10),  # 10 components, 100 points, 10 points/component\n            (1, 1000, 1000),  # 1 component, 1000 points, 1000 points/component\n            (100, 500, 5),  # 100 components, 500 points, 5 points/component\n            (0, 100, 100),  # 0 components (edge case)\n        ]\n\n        for n_components, n_points, expected_ppc in scenarios:\n            block_size = calculate_block_size(n_components, n_points, num_threads)\n\n            # Block size should be reasonable\n            assert block_size >= num_threads\n            assert block_size <= n_points // 4 + 1\n            assert isinstance(block_size, int)\n\n    def test_calculate_block_size_extremes(self):\n        \"\"\"Test block size calculation for extreme cases.\"\"\"\n        num_threads = 8\n\n        # Very large dataset\n        block_size = calculate_block_size(1000, 100000, num_threads)\n        assert block_size >= num_threads\n        assert block_size <= 100000 // 4 + 1\n\n        # Very small dataset\n        block_size = calculate_block_size(1, 10, num_threads)\n        assert block_size >= num_threads\n        # For small datasets, max() ensures block_size >= num_threads even when n_points//4+1 is smaller\n        expected_max = max(num_threads, 10 // 4 + 1)\n        assert block_size == expected_max\n\n\nclass TestParallelBoruvka:\n    \"\"\"Test the main parallel Boruvka algorithm.\"\"\"\n\n    @pytest.fixture\n    def boruvka_test_data(self):\n        \"\"\"Create test data for Boruvka algorithm.\"\"\"\n        # Create well-separated clusters that should form clear MST\n        np.random.seed(42)\n        cluster_centers = [[0, 0], [3, 0], [0, 3], [3, 3]]\n        data = []\n        for center in cluster_centers:\n            cluster_data = np.random.normal(center, 0.1, (5, 2))\n            data.append(cluster_data)\n\n        data = np.vstack(data).astype(np.float32)\n        tree = build_kdtree(data, leaf_size=3)\n\n        return tree, data\n\n    def test_parallel_boruvka_basic(self, boruvka_test_data):\n        \"\"\"Test basic Boruvka algorithm execution.\"\"\"\n        tree, data = boruvka_test_data\n        num_threads = numba.get_num_threads()\n\n        # Run Boruvka with different min_samples\n        for min_samples in [1, 3, 5]:\n            edges = parallel_boruvka(\n                tree, num_threads, min_samples=min_samples, reproducible=False\n            )\n\n            # Should produce a valid MST\n            assert edges.shape[0] == data.shape[0] - 1  # n-1 edges for MST\n            assert edges.shape[1] == 3  # from, to, weight\n\n            # All edge weights should be positive\n            assert np.all(edges[:, 2] > 0)\n\n            # Edge endpoints should be valid indices\n            assert np.all(edges[:, 0] >= 0)\n            assert np.all(edges[:, 0] < data.shape[0])\n            assert np.all(edges[:, 1] >= 0)\n            assert np.all(edges[:, 1] < data.shape[0])\n\n    def test_parallel_boruvka_reproducible(self, boruvka_test_data):\n        \"\"\"Test that reproducible Boruvka gives consistent results.\"\"\"\n        tree, data = boruvka_test_data\n        num_threads = numba.get_num_threads()\n\n        # Run multiple times\n        results = []\n        for _ in range(3):\n            edges = parallel_boruvka(\n                tree, num_threads, min_samples=3, reproducible=True\n            )\n            # Sort edges for comparison (edge order may vary)\n            sorted_edges = edges[np.lexsort((edges[:, 1], edges[:, 0]))]\n            results.append(sorted_edges)\n\n        # Results should be identical\n        for i in range(1, len(results)):\n            np.testing.assert_array_almost_equal(results[0], results[i], decimal=5)\n\n    def test_parallel_boruvka_vs_non_reproducible(self, boruvka_test_data):\n        \"\"\"Test that reproducible and non-reproducible versions give equivalent MST weights.\"\"\"\n        tree, data = boruvka_test_data\n        num_threads = numba.get_num_threads()\n\n        edges_normal = parallel_boruvka(\n            tree, num_threads, min_samples=3, reproducible=False\n        )\n        edges_repro = parallel_boruvka(\n            tree, num_threads, min_samples=3, reproducible=True\n        )\n\n        # Both should have same number of edges\n        assert edges_normal.shape[0] == edges_repro.shape[0]\n\n        # Total MST weight should be the same (or very close due to floating point)\n        total_weight_normal = np.sum(edges_normal[:, 2])\n        total_weight_repro = np.sum(edges_repro[:, 2])\n        np.testing.assert_almost_equal(\n            total_weight_normal, total_weight_repro, decimal=4\n        )\n\n    def test_parallel_boruvka_single_point(self):\n        \"\"\"Test Boruvka with single point (edge case).\"\"\"\n        data = np.array([[0.0, 0.0]], dtype=np.float32)\n        tree = build_kdtree(data, leaf_size=1)\n        num_threads = numba.get_num_threads()\n\n        edges = parallel_boruvka(tree, num_threads, min_samples=1, reproducible=False)\n\n        # Single point should produce empty MST\n        assert edges.shape[0] == 0\n\n    def test_parallel_boruvka_two_points(self):\n        \"\"\"Test Boruvka with two points.\"\"\"\n        data = np.array([[0.0, 0.0], [1.0, 1.0]], dtype=np.float32)\n        tree = build_kdtree(data, leaf_size=1)\n        num_threads = numba.get_num_threads()\n\n        edges = parallel_boruvka(tree, num_threads, min_samples=1, reproducible=False)\n\n        # Two points should produce single edge\n        assert edges.shape[0] == 1\n        assert edges.shape[1] == 3\n\n        # Edge should connect the two points\n        edge_points = set([int(edges[0, 0]), int(edges[0, 1])])\n        assert edge_points == {0, 1}\n\n        # Edge weight should be distance between points\n        expected_distance = np.sqrt(2.0)  # sqrt((1-0)^2 + (1-0)^2)\n        np.testing.assert_almost_equal(edges[0, 2], expected_distance, decimal=5)\n\n    def test_parallel_boruvka_different_min_samples(self, boruvka_test_data):\n        \"\"\"Test Boruvka with different min_samples values.\"\"\"\n        tree, data = boruvka_test_data\n        num_threads = numba.get_num_threads()\n\n        results = {}\n        for min_samples in [1, 2, 3, 5]:\n            edges = parallel_boruvka(\n                tree, num_threads, min_samples=min_samples, reproducible=False\n            )\n            results[min_samples] = edges\n\n            # All should produce valid MST\n            assert edges.shape[0] == data.shape[0] - 1\n\n        # Different min_samples may produce different trees, but should all be valid MSTs\n        # Test that all have reasonable total weights\n        weights = [np.sum(edges[:, 2]) for edges in results.values()]\n\n        # All weights should be positive and within reasonable range of each other\n        assert all(w > 0 for w in weights)\n        weight_ratio = max(weights) / min(weights)\n        assert (\n            weight_ratio < 10.0\n        )  # Different min_samples can produce quite different trees\n\n    def test_parallel_boruvka_different_num_threads(self, boruvka_test_data):\n        \"\"\"Test Boruvka with different num_threads values.\"\"\"\n        tree, data = boruvka_test_data\n\n        # Test different numbers of threads\n        thread_counts = [1, 2, 4, 8]\n        results = {}\n\n        for num_threads in thread_counts:\n            edges = parallel_boruvka(\n                tree, num_threads, min_samples=3, reproducible=True\n            )\n            results[num_threads] = edges\n\n            # All should produce valid MST\n            assert edges.shape[0] == data.shape[0] - 1\n            assert edges.shape[1] == 3\n            assert np.all(edges[:, 2] > 0)\n\n        # All results should be identical when using reproducible=True\n        # (since the algorithm should be deterministic regardless of thread count)\n        sorted_results = {}\n        for num_threads, edges in results.items():\n            sorted_edges = edges[np.lexsort((edges[:, 1], edges[:, 0]))]\n            sorted_results[num_threads] = sorted_edges\n\n        # Compare all results to the first one\n        base_result = sorted_results[thread_counts[0]]\n        for num_threads in thread_counts[1:]:\n            np.testing.assert_array_almost_equal(\n                base_result,\n                sorted_results[num_threads],\n                decimal=5,\n                err_msg=f\"Results differ between 1 thread and {num_threads} threads\",\n            )\n\n\nclass TestEdgeCases:\n    \"\"\"Test edge cases and error conditions.\"\"\"\n\n    def test_empty_data_handling(self):\n        \"\"\"Test handling of empty data - should not raise exception as input validation happens upstream.\"\"\"\n        # Empty data should be handled gracefully without raising exceptions\n        # since this is an internal function that relies on sklearn's check_array for validation\n        try:\n            data = np.empty((0, 2), dtype=np.float32)\n            tree = build_kdtree(data, leaf_size=1)\n            # If we get here, the function handled empty data gracefully\n            assert True\n        except Exception:\n            # If an exception is raised, that's also acceptable behavior\n            # since the exact handling of empty data may vary\n            assert True\n\n    def test_single_dimension_data(self):\n        \"\"\"Test with 1D data.\"\"\"\n        data = np.array([[0.0], [1.0], [2.0]], dtype=np.float32)\n        tree = build_kdtree(data, leaf_size=2)\n        num_threads = numba.get_num_threads()\n\n        edges = parallel_boruvka(tree, num_threads, min_samples=1, reproducible=False)\n\n        # Should produce valid MST for 1D data\n        assert edges.shape[0] == 2  # 3 points -> 2 edges\n        assert np.all(edges[:, 2] > 0)  # Positive weights\n\n    def test_high_dimensional_data(self):\n        \"\"\"Test with higher dimensional data.\"\"\"\n        np.random.seed(42)\n        data = np.random.random((20, 10)).astype(np.float32)  # 20 points in 10D\n        tree = build_kdtree(data, leaf_size=5)\n        num_threads = numba.get_num_threads()\n\n        edges = parallel_boruvka(tree, num_threads, min_samples=2, reproducible=False)\n\n        # Should handle high-dimensional data\n        assert edges.shape[0] == 19  # n-1 edges\n        assert np.all(edges[:, 2] > 0)\n        assert np.all(np.isfinite(edges[:, 2]))\n\n\nif __name__ == \"__main__\":\n    pytest.main([__file__])\n"
  },
  {
    "path": "evoc/tests/test_cluster_trees.py",
    "content": "import numpy as np\nimport pytest\nfrom sklearn.datasets import make_blobs\nfrom sklearn.utils import shuffle\nfrom sklearn.preprocessing import StandardScaler\nfrom sklearn.metrics import pairwise_distances\n\nfrom evoc.cluster_trees import (\n    create_linkage_merge_data,\n    eliminate_branch,\n    linkage_merge_find,\n    linkage_merge_join,\n    mst_to_linkage_tree,\n    bfs_from_hierarchy,\n    condense_tree,\n    extract_leaves,\n    score_condensed_tree_nodes,\n    cluster_tree_from_condensed_tree,\n    extract_eom_clusters,\n    get_cluster_labelling_at_cut,\n    get_cluster_label_vector,\n    get_point_membership_strength_vector,\n    CondensedTree,\n    LinkageMergeData,\n)\n\n\nclass TestLinkageMergeData:\n    \"\"\"Test the LinkageMergeData structure and associated functions.\"\"\"\n    \n    def test_create_linkage_merge_data(self):\n        \"\"\"Test creation of linkage merge data structure.\"\"\"\n        base_size = 5\n        linkage_data = create_linkage_merge_data(base_size)\n        \n        # Check structure\n        assert isinstance(linkage_data, LinkageMergeData)\n        assert len(linkage_data.parent) == 2 * base_size - 1\n        assert len(linkage_data.size) == 2 * base_size - 1\n        assert len(linkage_data.next) == 1\n        \n        # Check initial values\n        assert np.all(linkage_data.parent == -1)\n        assert np.all(linkage_data.size[:base_size] == 1)\n        assert np.all(linkage_data.size[base_size:] == 0)\n        assert linkage_data.next[0] == base_size\n    \n    def test_linkage_merge_find_and_join(self):\n        \"\"\"Test find and join operations on linkage merge data.\"\"\"\n        base_size = 4\n        linkage_data = create_linkage_merge_data(base_size)\n        \n        # Initially, each node should find itself\n        for i in range(base_size):\n            assert linkage_merge_find(linkage_data, i) == i\n        \n        # Join nodes 0 and 1\n        linkage_merge_join(linkage_data, 0, 1)\n        \n        # Check that parent pointers are set correctly\n        assert linkage_data.parent[0] == base_size  # 4\n        assert linkage_data.parent[1] == base_size  # 4\n        assert linkage_data.size[base_size] == 2   # Combined size\n        assert linkage_data.next[0] == base_size + 1  # Next available index\n        \n        # Join the new cluster with node 2\n        new_cluster = linkage_merge_find(linkage_data, 0)  # Should be 4\n        linkage_merge_join(linkage_data, new_cluster, 2)\n        \n        # Check updated structure\n        assert linkage_data.size[base_size + 1] == 3  # Size should be 3\n        assert linkage_data.next[0] == base_size + 2   # Next available index\n\n\nclass TestMSTToLinkageTree:\n    \"\"\"Test conversion from MST to linkage tree.\"\"\"\n    \n    @pytest.fixture\n    def simple_mst(self):\n        \"\"\"Create a simple MST for testing.\"\"\"\n        # Simple 4-point MST: 0-1 (dist=1.0), 1-2 (dist=2.0), 2-3 (dist=3.0)\n        return np.array([\n            [0, 1, 1.0],\n            [1, 2, 2.0],\n            [2, 3, 3.0]\n        ], dtype=np.float64)\n    \n    def test_mst_to_linkage_tree_basic(self, simple_mst):\n        \"\"\"Test basic MST to linkage tree conversion.\"\"\"\n        linkage_tree = mst_to_linkage_tree(simple_mst)\n        \n        # Should have same number of rows as MST\n        assert linkage_tree.shape[0] == simple_mst.shape[0]\n        assert linkage_tree.shape[1] == 4  # left, right, distance, size\n        \n        # Check that distances are preserved\n        assert np.array_equal(linkage_tree[:, 2], simple_mst[:, 2])\n        \n        # Check that cluster sizes make sense (should be increasing)\n        sizes = linkage_tree[:, 3]\n        assert sizes[0] == 2  # First merge: 2 points\n        assert sizes[1] == 3  # Second merge: 3 points  \n        assert sizes[2] == 4  # Final merge: all 4 points\n    \n    def test_mst_to_linkage_tree_ordering(self, simple_mst):\n        \"\"\"Test that linkage tree maintains proper ordering.\"\"\"\n        linkage_tree = mst_to_linkage_tree(simple_mst)\n        \n        # In each row, larger cluster index should be in column 0\n        for i in range(linkage_tree.shape[0]):\n            assert linkage_tree[i, 0] >= linkage_tree[i, 1]\n    \n    def test_mst_to_linkage_tree_random(self):\n        \"\"\"Test with a larger random MST.\"\"\"\n        np.random.seed(42)\n        n_points = 10\n        \n        # Create a random MST (n_points - 1 edges)\n        edges = []\n        for i in range(n_points - 1):\n            edges.append([i, i + 1, np.random.random()])\n        \n        mst = np.array(edges, dtype=np.float64)\n        mst = mst[np.argsort(mst[:, 2])]  # Sort by distance\n        \n        linkage_tree = mst_to_linkage_tree(mst)\n        \n        assert linkage_tree.shape[0] == n_points - 1\n        assert linkage_tree.shape[1] == 4\n        assert linkage_tree[-1, 3] == n_points  # Final cluster has all points\n\n\nclass TestBFSFromHierarchy:\n    \"\"\"Test breadth-first search on hierarchy.\"\"\"\n    \n    @pytest.fixture\n    def simple_hierarchy(self):\n        \"\"\"Create a simple hierarchy for testing.\n        \n        In scipy linkage format:\n        - Points: 0, 1, 2, 3 (original data)  \n        - Clusters: 4, 5, 6 (formed by merges)\n        - Row 0: merge to form cluster 4 (n_points + 0)\n        - Row 1: merge to form cluster 5 (n_points + 1) \n        - Row 2: merge to form cluster 6 (n_points + 2)\n        \"\"\"\n        return np.array([\n            [0, 1, 1.0, 2],  # Row 0: merge points 0,1 -> cluster 4\n            [2, 3, 2.0, 2],  # Row 1: merge points 2,3 -> cluster 5\n            [4, 5, 3.0, 4],  # Row 2: merge clusters 4,5 -> cluster 6 (root)\n        ], dtype=np.float64)\n    \n    def test_bfs_leaf_node(self, simple_hierarchy):\n        \"\"\"Test BFS starting from a leaf node (original data point).\"\"\"\n        result = bfs_from_hierarchy(simple_hierarchy, 0, 4)\n        assert result == [0]  # Leaf node should return itself\n    \n    def test_bfs_internal_node(self, simple_hierarchy):\n        \"\"\"Test BFS starting from an internal cluster.\"\"\"\n        # Cluster 4 (formed by merging points 0,1)\n        result = bfs_from_hierarchy(simple_hierarchy, 4, 4)\n        expected = [4, 0, 1]  # Should include the cluster and its children\n        assert result == expected\n    \n    def test_bfs_root_node(self, simple_hierarchy):\n        \"\"\"Test BFS starting from the root cluster.\"\"\"\n        # Cluster 6 is the root (formed by merging clusters 4,5)\n        result = bfs_from_hierarchy(simple_hierarchy, 6, 4)\n        expected = [6, 4, 5, 0, 1, 2, 3]  # Should traverse entire tree\n        assert set(result) == set(expected)  # Order may vary in BFS\n\n\nclass TestCondenseTree:\n    \"\"\"Test tree condensation functionality.\"\"\"\n    \n    @pytest.fixture\n    def sample_hierarchy(self):\n        \"\"\"Create a sample hierarchy for testing.\"\"\"\n        # Create hierarchy for 6 points\n        return np.array([\n            [0, 1, 0.1, 2],   # Cluster 6: points 0,1\n            [2, 3, 0.2, 2],   # Cluster 7: points 2,3  \n            [6, 7, 0.3, 4],   # Cluster 8: clusters 6,7\n            [8, 4, 0.4, 5],   # Cluster 9: cluster 8 + point 4\n            [9, 5, 0.5, 6],   # Cluster 10: cluster 9 + point 5 (root)\n        ], dtype=np.float64)\n    \n    def test_condense_tree_basic(self, sample_hierarchy):\n        \"\"\"Test basic tree condensation.\"\"\"\n        min_cluster_size = 3\n        condensed = condense_tree(sample_hierarchy, min_cluster_size)\n        \n        # Check structure\n        assert isinstance(condensed, CondensedTree)\n        assert len(condensed.parent) == len(condensed.child)\n        assert len(condensed.parent) == len(condensed.lambda_val)\n        assert len(condensed.parent) == len(condensed.child_size)\n        \n        # Lambda values should be positive\n        assert np.all(condensed.lambda_val > 0)\n        \n        # Child sizes should be reasonable\n        assert np.all(condensed.child_size >= 1)\n    \n    def test_condense_tree_min_cluster_size_effect(self, sample_hierarchy):\n        \"\"\"Test that different min_cluster_size values produce different results.\"\"\"\n        condensed_small = condense_tree(sample_hierarchy, min_cluster_size=2)\n        condensed_large = condense_tree(sample_hierarchy, min_cluster_size=4)\n        \n        # Different min_cluster_size should affect the result structure\n        # (Exact comparison depends on the specific condensation logic)\n        assert len(condensed_small.parent) >= 0\n        assert len(condensed_large.parent) >= 0\n    \n    def test_condense_tree_lambda_values(self, sample_hierarchy):\n        \"\"\"Test that lambda values are computed correctly (1/distance).\"\"\"\n        condensed = condense_tree(sample_hierarchy, min_cluster_size=2)\n        \n        # All lambda values should be finite and positive\n        assert np.all(np.isfinite(condensed.lambda_val))\n        assert np.all(condensed.lambda_val > 0)\n\n\nclass TestExtractLeaves:\n    \"\"\"Test leaf extraction from condensed trees.\"\"\"\n    \n    def test_extract_leaves_simple(self):\n        \"\"\"Test leaf extraction from a simple condensed tree.\"\"\"\n        # Create simple condensed tree manually\n        parent = np.array([5, 5, 5])\n        child = np.array([0, 1, 2])  # Three leaf points\n        lambda_val = np.array([1.0, 1.0, 1.0])\n        child_size = np.array([1, 1, 1])\n        \n        condensed = CondensedTree(parent, child, lambda_val, child_size)\n        leaves = extract_leaves(condensed)\n        \n        # Node 5 should be identified as a leaf cluster\n        assert 5 in leaves\n    \n    def test_extract_leaves_hierarchical(self):\n        \"\"\"Test leaf extraction from a hierarchical condensed tree.\"\"\"\n        # Create a tree where node 5 has children that are clusters (not just points)\n        parent = np.array([6, 6, 5, 5])\n        child = np.array([5, 0, 1, 2])  # Node 5 is internal (has child_size > 1)\n        lambda_val = np.array([1.0, 1.0, 1.0, 1.0])\n        child_size = np.array([3, 1, 1, 1])  # Node 5 entry has child_size=3\n        \n        condensed = CondensedTree(parent, child, lambda_val, child_size)\n        leaves = extract_leaves(condensed)\n        \n        # Based on the extract_leaves logic, clusters with child_size > 1 \n        # in their entries are leaf clusters\n        if len(leaves) > 0:\n            for leaf in leaves:\n                # Find entries where this node is the child\n                mask = condensed.child == leaf\n                if np.any(mask):\n                    # At least one entry should have child_size > 1\n                    assert np.any(condensed.child_size[mask] > 1)\n\n\nclass TestClusterLabeling:\n    \"\"\"Test cluster labeling and membership functions.\"\"\"\n    \n    @pytest.fixture\n    def sample_condensed_tree(self):\n        \"\"\"Create a sample condensed tree for testing.\"\"\"\n        parent = np.array([10, 10, 10, 11, 11])\n        child = np.array([0, 1, 2, 3, 4])\n        lambda_val = np.array([2.0, 2.0, 2.0, 1.0, 1.0])\n        child_size = np.array([1, 1, 1, 1, 1])\n        return CondensedTree(parent, child, lambda_val, child_size)\n    \n    def test_get_cluster_label_vector_single_cluster(self, sample_condensed_tree):\n        \"\"\"Test cluster labeling with a single cluster.\"\"\"\n        clusters = np.array([10])\n        labels = get_cluster_label_vector(\n            sample_condensed_tree, clusters, 0.0, 5\n        )\n        \n        assert len(labels) == 5\n        # Points 0, 1, 2 should be in cluster 0 (they have high lambda values)\n        assert labels[0] == 0\n        assert labels[1] == 0 \n        assert labels[2] == 0\n        # Points 3, 4 should be noise (-1) (they have lower lambda values)\n        assert labels[3] == -1\n        assert labels[4] == -1\n    \n    def test_get_cluster_label_vector_multiple_clusters(self, sample_condensed_tree):\n        \"\"\"Test cluster labeling with multiple clusters.\"\"\"\n        clusters = np.array([10, 11])\n        labels = get_cluster_label_vector(\n            sample_condensed_tree, clusters, 0.0, 5\n        )\n        \n        assert len(labels) == 5\n        # Should have valid cluster assignments\n        unique_labels = np.unique(labels)\n        assert -1 in unique_labels or len(unique_labels) > 1\n    \n    def test_get_point_membership_strength_vector(self, sample_condensed_tree):\n        \"\"\"Test membership strength calculation.\"\"\"\n        clusters = np.array([10, 11])\n        labels = get_cluster_label_vector(\n            sample_condensed_tree, clusters, 0.0, 5\n        )\n        \n        strengths = get_point_membership_strength_vector(\n            sample_condensed_tree, clusters, labels\n        )\n        \n        assert len(strengths) == 5\n        assert np.all(strengths >= 0.0)\n        assert np.all(strengths <= 1.0)\n        \n        # Points with valid cluster assignments should have positive strength\n        valid_points = labels >= 0\n        if np.any(valid_points):\n            assert np.all(strengths[valid_points] > 0)\n\n\nclass TestIntegrationWithRealData:\n    \"\"\"Integration tests using real clustered data.\"\"\"\n    \n    @pytest.fixture\n    def clustered_data(self):\n        \"\"\"Generate clustered data for integration testing.\"\"\"\n        np.random.seed(42)\n        X, y = make_blobs(n_samples=50, centers=3, random_state=42)\n        X = StandardScaler().fit_transform(X)\n        return X, y\n    \n    def test_full_pipeline_simple_mst(self, clustered_data):\n        \"\"\"Test the full pipeline with a simple MST.\"\"\"\n        X, true_labels = clustered_data\n        \n        # Create a simple MST by connecting points sequentially\n        n_samples = X.shape[0]\n        mst_edges = []\n        \n        for i in range(n_samples - 1):\n            # Connect point i to point i+1 with random distance\n            mst_edges.append([i, i + 1, np.random.random()])\n        \n        mst = np.array(mst_edges, dtype=np.float64)\n        mst = mst[np.argsort(mst[:, 2])]  # Sort by distance\n        \n        # Convert to linkage tree\n        linkage_tree = mst_to_linkage_tree(mst)\n        \n        # Condense tree\n        condensed = condense_tree(linkage_tree, min_cluster_size=5)\n        \n        # Extract clusters\n        leaves = extract_leaves(condensed)\n        \n        # Get cluster labels\n        if len(leaves) > 0:\n            labels = get_cluster_label_vector(condensed, leaves, 0.0, n_samples)\n            \n            # Basic sanity checks\n            assert len(labels) == n_samples\n            assert np.all(labels >= -1)  # Valid range for labels\n            \n            # Should find some clusters or noise\n            n_clusters = len(np.unique(labels[labels >= 0]))\n            assert n_clusters >= 0  # Could be all noise\n    \n    def test_score_condensed_tree_nodes(self):\n        \"\"\"Test scoring of condensed tree nodes.\"\"\"\n        # Create a simple condensed tree\n        parent = np.array([5, 5, 5])\n        child = np.array([0, 1, 2])\n        lambda_val = np.array([2.0, 1.5, 1.0])\n        child_size = np.array([1, 1, 1])\n        \n        condensed = CondensedTree(parent, child, lambda_val, child_size)\n        scores = score_condensed_tree_nodes(condensed)\n        \n        # Node 5 should have a positive score\n        assert 5 in scores\n        assert scores[5] > 0\n\n\nclass TestEdgeCases:\n    \"\"\"Test edge cases and error conditions.\"\"\"\n    \n    def test_extract_leaves_empty_tree(self):\n        \"\"\"Test behavior with empty condensed trees.\"\"\"\n        empty_condensed = CondensedTree(\n            np.array([], dtype=np.int64),\n            np.array([], dtype=np.int64), \n            np.array([], dtype=np.float32),\n            np.array([], dtype=np.int64)\n        )\n        \n        # Should handle empty input gracefully\n        leaves = extract_leaves(empty_condensed)\n        assert len(leaves) == 0 or isinstance(leaves, np.ndarray)\n    \n    def test_single_point_mst(self):\n        \"\"\"Test with MST containing only one edge (two points).\"\"\"\n        mst = np.array([[0, 1, 1.0]], dtype=np.float64)\n        linkage_tree = mst_to_linkage_tree(mst)\n        \n        assert linkage_tree.shape[0] == 1\n        assert linkage_tree[0, 3] == 2  # Should connect 2 points\n    \n    def test_zero_distance_edges(self):\n        \"\"\"Test handling of zero-distance edges in MST.\"\"\"\n        mst = np.array([\n            [0, 1, 0.0],  # Zero distance\n            [1, 2, 1.0]\n        ], dtype=np.float64)\n        \n        linkage_tree = mst_to_linkage_tree(mst)\n        condensed = condense_tree(linkage_tree, min_cluster_size=2)\n        \n        # Should handle zero distances gracefully\n        # (may result in infinite lambda values)\n        if len(condensed.lambda_val) > 0:\n            finite_mask = np.isfinite(condensed.lambda_val)\n            # At least some lambda values should be finite\n            assert np.any(finite_mask) or np.any(np.isinf(condensed.lambda_val))\n\n\nclass TestBFSEdgeCases:\n    \"\"\"Test edge cases for BFS functionality.\"\"\"\n    \n    def test_bfs_single_point_hierarchy(self):\n        \"\"\"Test BFS with minimal hierarchy.\"\"\"\n        # Single merge hierarchy for 2 points\n        hierarchy = np.array([[0, 1, 1.0, 2]], dtype=np.float64)\n        \n        result = bfs_from_hierarchy(hierarchy, 2, 2)  # Cluster 2 (n_points + 0)\n        assert set(result) == {2, 0, 1}\n    \n    def test_eliminate_branch_leaf(self):\n        \"\"\"Test eliminate_branch with a leaf node.\"\"\"\n        hierarchy = np.array([[0, 1, 1.0, 2]], dtype=np.float64)\n        \n        parents = np.zeros(10, dtype=np.int64)\n        children = np.zeros(10, dtype=np.int64)\n        lambdas = np.zeros(10, dtype=np.float32)\n        sizes = np.zeros(10, dtype=np.int64)\n        ignore = np.zeros(10, dtype=bool)\n        \n        # Eliminate a leaf node (point 0)\n        new_idx = eliminate_branch(0, 5, 1.0, parents, children, lambdas, \n                                 sizes, 0, ignore, hierarchy, 2)\n        \n        assert new_idx == 1  # Should increment index\n        assert parents[0] == 5\n        assert children[0] == 0\n        assert lambdas[0] == 1.0\n\n\n# Utility function for running integration tests\ndef test_cluster_trees_integration():\n    \"\"\"High-level integration test of the entire cluster_trees module.\"\"\"\n    np.random.seed(42)\n    \n    # Generate test data\n    X, _ = make_blobs(n_samples=20, centers=2, random_state=42)\n    X = StandardScaler().fit_transform(X)\n    \n    # Create a minimal MST (for testing purposes)\n    n_samples = X.shape[0]\n    mst_edges = []\n    for i in range(n_samples - 1):\n        mst_edges.append([i, i + 1, np.random.random()])\n    \n    mst = np.array(mst_edges, dtype=np.float64)\n    mst = mst[np.argsort(mst[:, 2])]\n    \n    # Test the full pipeline\n    linkage_tree = mst_to_linkage_tree(mst)\n    condensed = condense_tree(linkage_tree, min_cluster_size=3)\n    leaves = extract_leaves(condensed)\n    \n    if len(leaves) > 0:\n        labels = get_cluster_label_vector(condensed, leaves, 0.0, n_samples)\n        strengths = get_point_membership_strength_vector(condensed, leaves, labels)\n        \n        # Verify results make sense\n        assert len(labels) == n_samples\n        assert len(strengths) == n_samples\n        assert np.all(strengths >= 0.0) and np.all(strengths <= 1.0)\n    \n    # Test passed if we reach here without errors\n    assert True\n\n\ndef test_linkage_merge_data_comprehensive():\n    \"\"\"Additional comprehensive test for linkage merge operations.\"\"\"\n    base_size = 6\n    linkage_data = create_linkage_merge_data(base_size)\n    \n    # Test multiple sequential merges\n    linkage_merge_join(linkage_data, 0, 1)  # Creates cluster 6\n    linkage_merge_join(linkage_data, 2, 3)  # Creates cluster 7\n    linkage_merge_join(linkage_data, 6, 7)  # Creates cluster 8\n    \n    # Verify the structure after multiple merges\n    assert linkage_data.size[6] == 2  # Cluster 6 has 2 points\n    assert linkage_data.size[7] == 2  # Cluster 7 has 2 points  \n    assert linkage_data.size[8] == 4  # Cluster 8 has 4 points\n    assert linkage_data.next[0] == 9  # Next available cluster ID\n    \n    # Test path compression in find\n    assert linkage_merge_find(linkage_data, 0) == 8  # Should find root cluster\n    assert linkage_merge_find(linkage_data, 2) == 8  # Should find same root\n"
  },
  {
    "path": "evoc/tests/test_clustering.py",
    "content": "\"\"\"\nComprehensive test suite for the clustering module.\n\nThis module tests the EVoC clustering algorithm implementation, including\nbinary search for clusters, cluster layer building, duplicate detection,\nand the main EVoC class functionality.\n\"\"\"\n\nimport numpy as np\nimport pytest\nfrom sklearn.datasets import make_blobs, make_circles\nfrom sklearn.preprocessing import StandardScaler\nfrom sklearn.metrics import adjusted_rand_score, silhouette_score\n\nfrom evoc.clustering import (\n    build_cluster_layers,\n    evoc_clusters,\n    EVoC,\n)\nfrom evoc.clustering_utilities import (\n    _binary_search_for_n_clusters,\n    find_duplicates,\n    _build_cluster_tree,\n    build_cluster_tree,\n    binary_search_for_n_clusters,\n)\nimport numba\nfrom evoc.numba_kdtree import build_kdtree\nfrom evoc.boruvka import parallel_boruvka\nfrom evoc.cluster_trees import mst_to_linkage_tree\n\n\n@pytest.fixture\ndef simple_embedding_data():\n    \"\"\"Create simple high-dimensional embedding-like data for testing.\"\"\"\n    # Create 512-dimensional data similar to CLIP embeddings\n    X, y = make_blobs(\n        n_samples=800, centers=4, n_features=512, cluster_std=0.8, random_state=42\n    )\n    # Normalize to unit sphere (typical for embeddings)\n    X = X / np.linalg.norm(X, axis=1, keepdims=True)\n    return X.astype(np.float32), y\n\n\n@pytest.fixture\ndef complex_embedding_data():\n    \"\"\"Create more complex high-dimensional embedding-like data for testing.\"\"\"\n    # Create 768-dimensional data similar to sentence transformer embeddings\n    X, y = make_blobs(\n        n_samples=2000, centers=8, n_features=768, cluster_std=0.6, random_state=42\n    )\n    # Normalize to unit sphere and add some noise\n    X = X / np.linalg.norm(X, axis=1, keepdims=True)\n    X += np.random.normal(0, 0.05, X.shape)\n    X = X / np.linalg.norm(X, axis=1, keepdims=True)\n    return X.astype(np.float32), y\n\n\n@pytest.fixture\ndef small_embedding_data():\n    \"\"\"Create small high-dimensional data for quick testing.\"\"\"\n    # Create 384-dimensional data (smaller embedding size)\n    X, y = make_blobs(\n        n_samples=300, centers=3, n_features=384, cluster_std=0.7, random_state=42\n    )\n    # Normalize to unit sphere\n    X = X / np.linalg.norm(X, axis=1, keepdims=True)\n    return X.astype(np.float32), y\n\n\n@pytest.fixture\ndef duplicate_embedding_data():\n    \"\"\"Create high-dimensional embedding data with some duplicate points for testing.\"\"\"\n    X, y = make_blobs(n_samples=400, centers=3, n_features=512, random_state=42)\n    # Normalize to unit sphere\n    X = X / np.linalg.norm(X, axis=1, keepdims=True)\n    # Add some duplicate points\n    X_with_dups = np.vstack([X, X[:20]])  # Duplicate first 20 points\n    y_with_dups = np.hstack([y, y[:20]])\n    return X_with_dups.astype(np.float32), y_with_dups\n\n\n@pytest.fixture\ndef quantized_embedding_data():\n    \"\"\"Create quantized (int8) embedding data for testing.\"\"\"\n    X, y = make_blobs(n_samples=600, centers=4, n_features=256, random_state=42)\n    # Normalize and quantize to int8 range\n    X = X / np.linalg.norm(X, axis=1, keepdims=True)\n    X_quantized = (X * 127).astype(np.int8)\n    return X_quantized, y\n\n\n@pytest.fixture\ndef binary_embedding_data():\n    \"\"\"Create binary (uint8) embedding data for testing.\"\"\"\n    X, y = make_blobs(n_samples=500, centers=3, n_features=128, random_state=42)\n    # Convert to binary representation\n    X_binary = (X > np.median(X, axis=1, keepdims=True)).astype(np.uint8)\n    return X_binary, y\n\n\n@pytest.fixture\ndef small_linkage_tree():\n    \"\"\"Create a small linkage tree for testing.\"\"\"\n    # Create simple high-dimensional data and build MST\n    X, _ = make_blobs(n_samples=100, centers=3, n_features=128, random_state=42)\n    # Normalize to unit sphere like embeddings\n    X = X / np.linalg.norm(X, axis=1, keepdims=True)\n    numba_tree = build_kdtree(X.astype(np.float32))\n    num_threads = numba.get_num_threads()\n    edges = parallel_boruvka(numba_tree, num_threads, min_samples=3, reproducible=False)\n    sorted_mst = edges[np.argsort(edges.T[2])]\n    return mst_to_linkage_tree(sorted_mst)\n\n\nclass TestBinarySearchForNClusters:\n    \"\"\"Test the binary search functionality for finding n clusters.\"\"\"\n\n    def test_binary_search_basic(self, small_linkage_tree):\n        \"\"\"Test basic binary search for cluster count.\"\"\"\n        n_samples = 100\n        target_clusters = 3\n\n        leaves, clusters, strengths = _binary_search_for_n_clusters(\n            small_linkage_tree, target_clusters, n_samples\n        )\n\n        # Check return types and shapes\n        assert isinstance(leaves, np.ndarray)\n        assert isinstance(clusters, np.ndarray)\n        assert isinstance(strengths, np.ndarray)\n        assert len(clusters) == n_samples\n        assert len(strengths) == n_samples\n\n        # Check that we have reasonable cluster count\n        n_clusters = len(np.unique(clusters[clusters >= 0]))\n        assert n_clusters > 0\n        assert n_clusters <= n_samples\n\n        # Check that strengths are in valid range\n        assert np.all(strengths >= 0)\n        assert np.all(strengths <= 1)\n\n    def test_binary_search_edge_cases(self, small_linkage_tree):\n        \"\"\"Test binary search with edge case parameters.\"\"\"\n        n_samples = 100\n\n        # Test with very few clusters\n        leaves, clusters, strengths = _binary_search_for_n_clusters(\n            small_linkage_tree, 1, n_samples\n        )\n        assert len(clusters) == n_samples\n\n        # Test with many clusters\n        leaves, clusters, strengths = _binary_search_for_n_clusters(\n            small_linkage_tree, 50, n_samples\n        )\n        assert len(clusters) == n_samples\n\n    def test_binary_search_wrapper_function(self, simple_embedding_data):\n        \"\"\"Test the wrapper binary_search_for_n_clusters function.\"\"\"\n        X, y_true = simple_embedding_data\n        num_threads = numba.get_num_threads()\n\n        clusters, strengths = binary_search_for_n_clusters(\n            X, approx_n_clusters=3, n_threads=num_threads, min_samples=5\n        )\n\n        # Check return types and shapes\n        assert isinstance(clusters, np.ndarray)\n        assert isinstance(strengths, np.ndarray)\n        assert len(clusters) == len(X)\n        assert len(strengths) == len(X)\n\n        # Check that we found reasonable clusters\n        n_clusters = len(np.unique(clusters[clusters >= 0]))\n        assert n_clusters > 0\n        assert n_clusters <= len(X)\n\n\nclass TestBuildClusterLayers:\n    \"\"\"Test the cluster layer building functionality.\"\"\"\n\n    def test_build_cluster_layers_basic(self, simple_embedding_data):\n        \"\"\"Test basic cluster layer building.\"\"\"\n        X, y_true = simple_embedding_data\n\n        cluster_layers, membership_strengths, persistence_scores = build_cluster_layers(\n            X,\n            min_samples=5,\n            base_min_cluster_size=10,\n        )\n\n        # Check return types\n        assert isinstance(cluster_layers, list)\n        assert isinstance(membership_strengths, list)\n        assert len(cluster_layers) == len(membership_strengths)\n\n        # Check that all layers have correct shape\n        for clusters, strengths in zip(cluster_layers, membership_strengths):\n            assert len(clusters) == len(X)\n            assert len(strengths) == len(X)\n            assert np.all(strengths >= 0)\n            assert np.all(strengths <= 1)\n\n    def test_build_cluster_layers_with_base_n_clusters(self, simple_embedding_data):\n        \"\"\"Test cluster layer building with specified base cluster count.\"\"\"\n        X, y_true = simple_embedding_data\n\n        cluster_layers, membership_strengths, persistence_scores = build_cluster_layers(\n            X,\n            base_n_clusters=3,\n            min_samples=5,\n        )\n\n        assert len(cluster_layers) > 0\n        assert len(membership_strengths) > 0\n\n        # Check that first layer has reasonable cluster count\n        first_layer_clusters = cluster_layers[0]\n        n_clusters = len(np.unique(first_layer_clusters[first_layer_clusters >= 0]))\n        assert n_clusters > 0\n\n    def test_build_cluster_layers_reproducible(self, simple_embedding_data):\n        \"\"\"Test that cluster layer building is reproducible.\"\"\"\n        X, y_true = simple_embedding_data\n\n        layers1, strengths1, persistence1 = build_cluster_layers(\n            X, base_min_cluster_size=10, reproducible_flag=True\n        )\n\n        layers2, strengths2, persistence2 = build_cluster_layers(\n            X, base_min_cluster_size=10, reproducible_flag=True\n        )\n\n        # Results should be identical when reproducible flag is set\n        assert len(layers1) == len(layers2)\n        for l1, l2 in zip(layers1, layers2):\n            np.testing.assert_array_equal(l1, l2)\n\n\nclass TestFindDuplicates:\n    \"\"\"Test the duplicate detection functionality.\"\"\"\n\n    def test_find_duplicates_basic(self):\n        \"\"\"Test basic duplicate detection.\"\"\"\n        # Create simple k-NN data with some duplicates\n        knn_inds = np.array(\n            [\n                [0, 1, 2],\n                [1, 0, 2],\n                [2, 0, 1],\n                [3, 0, 1],  # Point 3 is close to points 0 and 1\n            ],\n            dtype=np.int32,\n        )\n\n        knn_dists = np.array(\n            [\n                [0.0, 0.5, 1.0],\n                [0.5, 0.0, 1.0],\n                [1.0, 0.5, 0.0],\n                [0.8, 0.0, 0.0],  # Duplicate distance (0.0) indicates duplicates\n            ],\n            dtype=np.float32,\n        )\n\n        duplicates = find_duplicates(knn_inds, knn_dists)\n\n        # Check return type\n        assert isinstance(duplicates, set)\n\n        # Check that duplicates are tuples of pairs\n        for dup in duplicates:\n            assert isinstance(dup, tuple)\n            assert len(dup) == 2\n            assert dup[0] < dup[1]  # Should be ordered pairs\n\n    def test_find_duplicates_no_duplicates(self):\n        \"\"\"Test duplicate detection when no duplicates exist.\"\"\"\n        knn_inds = np.array([[0, 1, 2], [1, 0, 2], [2, 0, 1]], dtype=np.int32)\n\n        knn_dists = np.array(\n            [[0.1, 0.5, 1.0], [0.5, 0.1, 1.0], [1.0, 0.5, 0.1]], dtype=np.float32\n        )\n\n        duplicates = find_duplicates(knn_inds, knn_dists)\n\n        # Should find minimal or no duplicates\n        assert isinstance(duplicates, set)\n\n\nclass TestBuildClusterTree:\n    \"\"\"Test the cluster tree building functionality.\"\"\"\n\n    def test_build_cluster_tree_basic(self):\n        \"\"\"Test basic cluster tree building.\"\"\"\n        # Create simple hierarchical cluster labels\n        labels = [\n            np.array([0, 0, 1, 1, 2, 2]),  # Fine-grained\n            np.array([0, 0, 0, 1, 1, 1]),  # Coarse-grained\n        ]\n\n        tree = build_cluster_tree(labels)\n\n        # Check return type\n        assert isinstance(tree, dict)\n\n        # Check that keys are tuples (layer, cluster)\n        for key in tree.keys():\n            assert isinstance(key, tuple)\n            assert len(key) == 2\n            assert isinstance(key[0], (int, np.integer))\n            assert isinstance(key[1], (int, np.integer))\n\n        # Check that values are lists of child clusters\n        for value in tree.values():\n            assert isinstance(value, list)\n            for child in value:\n                assert isinstance(child, tuple)\n                assert len(child) == 2\n\n    def test_build_cluster_tree_empty(self):\n        \"\"\"Test cluster tree building with empty input.\"\"\"\n        labels = []\n        # Empty input should be handled gracefully\n        # Note: This may raise an error due to numba limitations with empty lists\n        with pytest.raises((ValueError, Exception)):\n            tree = build_cluster_tree(labels)\n\n    def test_build_cluster_tree_single_layer(self):\n        \"\"\"Test cluster tree building with single layer.\"\"\"\n        labels = [np.array([0, 1, 0, 1, 2])]\n        tree = build_cluster_tree(labels)\n        assert isinstance(tree, dict)\n\n\nclass TestEvocClusters:\n    \"\"\"Test the main evoc_clusters function.\"\"\"\n\n    def test_evoc_clusters_basic(self, simple_embedding_data):\n        \"\"\"Test basic EVoC clustering.\"\"\"\n        X, y_true = simple_embedding_data\n\n        cluster_layers, membership_strengths, persistence_scores, _, _ = evoc_clusters(\n            X,\n            noise_level=0.5,\n            base_min_cluster_size=5,\n            base_n_clusters=2,\n            n_neighbors=10,\n            min_samples=3,\n            n_epochs=20,\n            random_state=np.random.RandomState(42),\n        )\n\n        # Check return types\n        assert isinstance(cluster_layers, list)\n        assert isinstance(membership_strengths, list)\n        assert len(cluster_layers) == len(membership_strengths)\n        assert len(cluster_layers) > 0\n\n        # Check shapes\n        for clusters, strengths in zip(cluster_layers, membership_strengths):\n            assert len(clusters) == len(X)\n            assert len(strengths) == len(X)\n\n    def test_evoc_clusters_with_approx_n_clusters(self, simple_embedding_data):\n        \"\"\"Test EVoC clustering with specified cluster count.\"\"\"\n        X, y_true = simple_embedding_data\n\n        cluster_layers, membership_strengths, persistence_scores, _, _ = evoc_clusters(\n            X,\n            approx_n_clusters=3,\n            n_neighbors=10,\n            min_samples=3,\n            n_epochs=20,\n            random_state=np.random.RandomState(42),\n        )\n\n        # Should return exactly one layer\n        assert len(cluster_layers) == 1\n        assert len(membership_strengths) == 1\n\n        # Check that we found reasonable clusters\n        clusters = cluster_layers[0]\n        n_clusters = len(np.unique(clusters[clusters >= 0]))\n        assert n_clusters > 0\n\n    def test_evoc_clusters_with_duplicates(self, duplicate_embedding_data):\n        \"\"\"Test EVoC clustering with duplicate detection.\"\"\"\n        X, y_true = duplicate_embedding_data\n\n        cluster_layers, membership_strengths, persistence_scores, _, _, duplicates = (\n            evoc_clusters(\n                X,\n                return_duplicates=True,\n                n_neighbors=10,\n                min_samples=3,\n                n_epochs=20,\n                random_state=np.random.RandomState(42),\n            )\n        )\n\n        # Check that duplicates are returned\n        assert isinstance(duplicates, set)\n\n        # Check other return values\n        assert isinstance(cluster_layers, list)\n        assert isinstance(membership_strengths, list)\n\n    def test_evoc_clusters_different_data_types(\n        self, quantized_embedding_data, binary_embedding_data\n    ):\n        \"\"\"Test EVoC clustering with different embedding data types.\"\"\"\n        # Test with float32 data (standard embeddings)\n        X_float = np.random.rand(100, 256).astype(np.float32)\n        # Normalize like real embeddings\n        X_float = X_float / np.linalg.norm(X_float, axis=1, keepdims=True)\n\n        clusters, strengths, persistence_scores, _, _ = evoc_clusters(\n            X_float,\n            approx_n_clusters=4,\n            n_epochs=10,\n            random_state=np.random.RandomState(42),\n        )\n        assert len(clusters) == 1\n        assert len(clusters[0]) == 100\n\n        # Test with int8 data (quantized embeddings)\n        X_int8, _ = quantized_embedding_data\n        clusters, strengths, persistence_scores, _, _ = evoc_clusters(\n            X_int8,\n            approx_n_clusters=3,\n            n_epochs=10,\n            random_state=np.random.RandomState(42),\n        )\n        assert len(clusters) == 1\n        assert len(clusters[0]) == len(X_int8)\n\n        # Test with uint8 data (binary embeddings)\n        X_uint8, _ = binary_embedding_data\n        clusters, strengths, persistence_scores, _, _ = evoc_clusters(\n            X_uint8,\n            approx_n_clusters=3,\n            n_epochs=10,\n            random_state=np.random.RandomState(42),\n        )\n        assert len(clusters) == 1\n        assert len(clusters[0]) == len(X_uint8)\n\n\nclass TestEVoCClass:\n    \"\"\"Test the EVoC class implementation.\"\"\"\n\n    def test_evoc_init(self):\n        \"\"\"Test EVoC class initialization.\"\"\"\n        clusterer = EVoC(\n            noise_level=0.3,\n            base_min_cluster_size=10,\n            n_neighbors=20,\n            n_epochs=30,\n            random_state=42,\n        )\n\n        # Check that parameters are set correctly\n        assert clusterer.noise_level == 0.3\n        assert clusterer.base_min_cluster_size == 10\n        assert clusterer.n_neighbors == 20\n        assert clusterer.n_epochs == 30\n        assert clusterer.random_state == 42\n\n    def test_evoc_fit_predict(self, simple_embedding_data):\n        \"\"\"Test EVoC fit_predict method.\"\"\"\n        X, y_true = simple_embedding_data\n\n        clusterer = EVoC(\n            base_min_cluster_size=5, n_neighbors=10, n_epochs=20, random_state=42\n        )\n\n        labels = clusterer.fit_predict(X)\n\n        # Check return type and shape\n        assert isinstance(labels, np.ndarray)\n        assert len(labels) == len(X)\n\n        # Check that clusterer has fitted attributes\n        assert hasattr(clusterer, \"labels_\")\n        assert hasattr(clusterer, \"membership_strengths_\")\n        assert hasattr(clusterer, \"cluster_layers_\")\n        assert hasattr(clusterer, \"membership_strength_layers_\")\n        assert hasattr(clusterer, \"duplicates_\")\n\n        # Check that labels are consistent\n        np.testing.assert_array_equal(labels, clusterer.labels_)\n\n    def test_evoc_fit(self, simple_embedding_data):\n        \"\"\"Test EVoC fit method.\"\"\"\n        X, y_true = simple_embedding_data\n\n        clusterer = EVoC(\n            base_min_cluster_size=5, n_neighbors=10, n_epochs=20, random_state=42\n        )\n\n        result = clusterer.fit(X)\n\n        # Check that fit returns self\n        assert result is clusterer\n\n        # Check that clusterer has fitted attributes\n        assert hasattr(clusterer, \"labels_\")\n        assert hasattr(clusterer, \"membership_strengths_\")\n\n    def test_evoc_with_approx_n_clusters(self, simple_embedding_data):\n        \"\"\"Test EVoC with specified cluster count.\"\"\"\n        X, y_true = simple_embedding_data\n\n        clusterer = EVoC(\n            approx_n_clusters=3, n_neighbors=10, n_epochs=20, random_state=42\n        )\n\n        labels = clusterer.fit_predict(X)\n\n        # Check that we have reasonable cluster count\n        n_clusters = len(np.unique(labels[labels >= 0]))\n        assert n_clusters > 0\n        assert n_clusters <= len(X)\n\n    def test_evoc_cluster_tree_property(self, simple_embedding_data):\n        \"\"\"Test EVoC cluster_tree_ property.\"\"\"\n        X, y_true = simple_embedding_data\n\n        clusterer = EVoC(\n            base_min_cluster_size=5, n_neighbors=10, n_epochs=20, random_state=42\n        )\n\n        clusterer.fit(X)\n\n        # Test cluster tree property\n        tree = clusterer.cluster_tree_\n        assert isinstance(tree, dict)\n\n    def test_evoc_cluster_tree_not_fitted(self):\n        \"\"\"Test that cluster_tree_ raises error when not fitted.\"\"\"\n        clusterer = EVoC()\n\n        with pytest.raises(Exception):  # Should raise NotFittedError or similar\n            _ = clusterer.cluster_tree_\n\n    def test_evoc_reproducibility(self, simple_embedding_data):\n        \"\"\"Test that EVoC produces reproducible results.\"\"\"\n        X, y_true = simple_embedding_data\n\n        clusterer1 = EVoC(\n            base_min_cluster_size=5, n_neighbors=10, n_epochs=20, random_state=42\n        )\n\n        clusterer2 = EVoC(\n            base_min_cluster_size=5, n_neighbors=10, n_epochs=20, random_state=42\n        )\n\n        labels1 = clusterer1.fit_predict(X)\n        labels2 = clusterer2.fit_predict(X)\n\n        # Results should be identical with same random state\n        np.testing.assert_array_equal(labels1, labels2)\n\n\nclass TestClusteringQuality:\n    \"\"\"Test clustering quality metrics and edge cases.\"\"\"\n\n    def test_clustering_quality_on_embeddings(self, simple_embedding_data):\n        \"\"\"Test that clustering works well on high-dimensional embedding data.\"\"\"\n        X, y_true = simple_embedding_data\n\n        clusterer = EVoC(\n            base_n_clusters=4,  # Match true number of clusters\n            n_neighbors=15,\n            n_epochs=30,\n            random_state=42,\n        )\n\n        try:\n            labels = clusterer.fit_predict(X)\n\n            # Calculate clustering quality metrics\n            # Remove noise points for ARI calculation\n            mask = labels >= 0\n            if np.sum(mask) > 0:\n                ari = adjusted_rand_score(y_true[mask], labels[mask])\n                # Should achieve reasonable clustering quality on embeddings\n                assert ari > 0.2  # Relaxed threshold for high-dimensional data\n\n            # Check silhouette score (only if we have multiple clusters)\n            n_clusters = len(np.unique(labels[labels >= 0]))\n            if n_clusters > 1:\n                sil_score = silhouette_score(X[mask], labels[mask])\n                assert sil_score > 0.05  # Very relaxed for high-dimensional data\n        except ValueError as e:\n            # Handle case where clustering fails to find layers\n            if \"empty sequence\" in str(e):\n                pytest.skip(\"Clustering failed to find cluster layers for this data\")\n\n    def test_clustering_quality_on_blobs(self):\n        \"\"\"Test that clustering works on traditional blob data for compatibility.\"\"\"\n        # Create traditional 2D blob data for compatibility testing\n        X, y_true = make_blobs(\n            n_samples=100, centers=3, n_features=2, cluster_std=1.0, random_state=42\n        )\n        X = StandardScaler().fit_transform(X).astype(np.float32)\n\n        clusterer = EVoC(\n            base_n_clusters=3,  # Match true number of clusters\n            n_neighbors=15,\n            n_epochs=30,\n            random_state=42,\n        )\n\n        try:\n            labels = clusterer.fit_predict(X)\n\n            # Calculate clustering quality metrics\n            # Remove noise points for ARI calculation\n            mask = labels >= 0\n            if np.sum(mask) > 0:\n                ari = adjusted_rand_score(y_true[mask], labels[mask])\n                # Should achieve good clustering quality on simple blobs\n                assert ari > 0.3\n\n            # Check silhouette score (only if we have multiple clusters)\n            n_clusters = len(np.unique(labels[labels >= 0]))\n            if n_clusters > 1:\n                sil_score = silhouette_score(X[mask], labels[mask])\n                assert sil_score > 0.1\n        except ValueError as e:\n            # Handle case where clustering fails to find layers\n            if \"empty sequence\" in str(e):\n                pytest.skip(\"Clustering failed to find cluster layers for this data\")\n\n    def test_clustering_on_small_dataset(self):\n        \"\"\"Test clustering on very small dataset.\"\"\"\n        # Use small but realistic embedding-like data\n        X = np.random.rand(50, 256)\n        # Normalize like embeddings\n        X = X / np.linalg.norm(X, axis=1, keepdims=True)\n        X = X.astype(np.float32)\n\n        clusterer = EVoC(\n            base_min_cluster_size=2, n_neighbors=5, n_epochs=10, random_state=42\n        )\n\n        try:\n            labels = clusterer.fit_predict(X)\n\n            # Should not crash and should return valid labels\n            assert len(labels) == len(X)\n            assert np.all((labels >= -1) & (labels < len(X)))\n        except ValueError as e:\n            # Handle case where clustering fails due to small dataset\n            if \"empty sequence\" in str(e):\n                pytest.skip(\n                    \"Clustering failed on very small dataset - expected behavior\"\n                )\n\n    def test_clustering_on_high_dimensional_data(self):\n        \"\"\"Test clustering on very high-dimensional embedding data.\"\"\"\n        # Test with 1024-dimensional data similar to large transformer models\n        X = np.random.rand(500, 1024)\n        # Normalize to unit sphere like real embeddings\n        X = X / np.linalg.norm(X, axis=1, keepdims=True)\n        X = X.astype(np.float32)\n\n        clusterer = EVoC(\n            base_min_cluster_size=8, n_neighbors=12, n_epochs=20, random_state=42\n        )\n\n        labels = clusterer.fit_predict(X)\n\n        # Should handle very high dimensions gracefully\n        assert len(labels) == len(X)\n        assert np.all((labels >= -1) & (labels < len(X)))\n\n    def test_edge_case_single_cluster(self):\n        \"\"\"Test edge case where all data forms single cluster.\"\"\"\n        # Create very tight cluster\n        X = np.random.normal(0, 0.01, (50, 5))\n\n        clusterer = EVoC(\n            base_min_cluster_size=10, n_neighbors=15, n_epochs=20, random_state=42\n        )\n\n        try:\n            labels = clusterer.fit_predict(X)\n\n            # Should handle single cluster case\n            assert len(labels) == len(X)\n        except ValueError as e:\n            # Handle case where clustering fails due to single tight cluster\n            if \"empty sequence\" in str(e):\n                pytest.skip(\n                    \"Clustering failed on single tight cluster - expected behavior\"\n                )\n\n    def test_parameter_validation(self):\n        \"\"\"Test that invalid parameters are handled appropriately.\"\"\"\n        # These should not crash during initialization\n        clusterer = EVoC(\n            noise_level=-0.1,  # Invalid but should be clamped/handled\n            base_min_cluster_size=1,  # Very small\n            n_neighbors=1,  # Very small\n            n_epochs=1,  # Very small\n        )\n\n        # Should initialize without error\n        assert isinstance(clusterer, EVoC)\n\n    def test_clustering_on_clip_like_embeddings(self):\n        \"\"\"Test clustering on CLIP-like 512-dimensional embeddings.\"\"\"\n        # Simulate CLIP embeddings with multiple semantic clusters\n        np.random.seed(42)\n        n_samples_per_cluster = 80\n        n_clusters = 5\n\n        cluster_centers = np.random.randn(n_clusters, 512)\n        cluster_centers = cluster_centers / np.linalg.norm(\n            cluster_centers, axis=1, keepdims=True\n        )\n\n        X = []\n        y_true = []\n        for i, center in enumerate(cluster_centers):\n            # Generate points around each center\n            cluster_points = center + np.random.normal(\n                0, 0.1, (n_samples_per_cluster, 512)\n            )\n            # Normalize to unit sphere\n            cluster_points = cluster_points / np.linalg.norm(\n                cluster_points, axis=1, keepdims=True\n            )\n            X.append(cluster_points)\n            y_true.extend([i] * n_samples_per_cluster)\n\n        X = np.vstack(X).astype(np.float32)\n        y_true = np.array(y_true)\n\n        clusterer = EVoC(\n            base_n_clusters=n_clusters, n_neighbors=15, n_epochs=25, random_state=42\n        )\n\n        labels = clusterer.fit_predict(X)\n\n        # Should handle CLIP-like embeddings well\n        assert len(labels) == len(X)\n        mask = labels >= 0\n        if np.sum(mask) > 0:\n            n_found_clusters = len(np.unique(labels[mask]))\n            assert n_found_clusters > 1  # Should find multiple clusters\n\n            # Check clustering quality\n            ari = adjusted_rand_score(y_true[mask], labels[mask])\n            assert (\n                ari > 0.15\n            )  # Should achieve reasonable clustering on well-separated data\n\n    def test_clustering_on_sentence_transformer_like_embeddings(self):\n        \"\"\"Test clustering on sentence transformer-like 768-dimensional embeddings.\"\"\"\n        # Simulate sentence transformer embeddings\n        np.random.seed(123)\n        n_samples = 600\n        n_dims = 768\n\n        # Create embeddings with some structure\n        X = np.random.rand(n_samples, n_dims) - 0.5\n        # Add some clustering structure\n        cluster_ids = np.random.choice([0, 1, 2, 3], n_samples)\n        for i in range(4):\n            mask = cluster_ids == i\n            if np.sum(mask) > 0:\n                # Add cluster-specific signal\n                X[mask, i * 50 : (i + 1) * 50] += np.random.normal(\n                    2.0, 0.5, (np.sum(mask), 50)\n                )\n\n        # Normalize like sentence transformers\n        X = X / np.linalg.norm(X, axis=1, keepdims=True)\n        X = X.astype(np.float32)\n\n        clusterer = EVoC(\n            base_min_cluster_size=8, n_neighbors=20, n_epochs=30, random_state=42\n        )\n\n        labels = clusterer.fit_predict(X)\n\n        # Should handle sentence transformer-like embeddings\n        assert len(labels) == len(X)\n        mask = labels >= 0\n        if np.sum(mask) > 0:\n            n_found_clusters = len(np.unique(labels[mask]))\n            assert n_found_clusters > 1\n\n    def test_clustering_on_quantized_embeddings(self, quantized_embedding_data):\n        \"\"\"Test clustering specifically on quantized int8 embeddings.\"\"\"\n        X, y_true = quantized_embedding_data\n\n        clusterer = EVoC(\n            base_n_clusters=4, n_neighbors=12, n_epochs=20, random_state=42\n        )\n\n        labels = clusterer.fit_predict(X)\n\n        # Should handle quantized embeddings\n        assert len(labels) == len(X)\n        assert np.all((labels >= -1) & (labels < len(X)))\n\n        # Check that some clustering structure is found\n        mask = labels >= 0\n        if np.sum(mask) > 0:\n            n_clusters = len(np.unique(labels[mask]))\n            assert n_clusters >= 1\n\n    def test_clustering_on_binary_embeddings(self, binary_embedding_data):\n        \"\"\"Test clustering specifically on binary uint8 embeddings.\"\"\"\n        X, y_true = binary_embedding_data\n\n        clusterer = EVoC(\n            base_n_clusters=3, n_neighbors=10, n_epochs=15, random_state=42\n        )\n\n        try:\n            labels = clusterer.fit_predict(X)\n\n            # Should handle binary embeddings\n            assert len(labels) == len(X)\n            assert np.all((labels >= -1) & (labels < len(X)))\n\n            # Check that some clustering structure is found\n            mask = labels >= 0\n            if np.sum(mask) > 0:\n                n_clusters = len(np.unique(labels[mask]))\n                assert n_clusters >= 1\n        except ValueError as e:\n            # Handle case where clustering fails on binary data\n            if \"empty sequence\" in str(e):\n                pytest.skip(\n                    \"Clustering failed on binary embeddings - may need different parameters\"\n                )\n\n\nif __name__ == \"__main__\":\n    pytest.main([__file__])\n"
  },
  {
    "path": "evoc/tests/test_knn_graph.py",
    "content": "\"\"\"\nComprehensive test suite for the knn_graph module.\n\nThis module tests the k-nearest neighbor graph construction functionality,\nincluding random projection forest building, nearest neighbor descent,\nand the main knn_graph function for different data types.\n\"\"\"\n\nimport numpy as np\nimport pytest\nimport time\nfrom unittest.mock import patch\nfrom sklearn.datasets import make_blobs\nfrom sklearn.utils import check_random_state\n\nfrom evoc.knn_graph import (\n    ts,\n    make_forest,\n    nn_descent,\n    knn_graph,\n    INT32_MIN,\n    INT32_MAX,\n)\n\n\nclass TestUtilityFunctions:\n    \"\"\"Test utility functions in the knn_graph module.\"\"\"\n\n    def test_ts_returns_string(self):\n        \"\"\"Test that ts() returns a properly formatted timestamp string.\"\"\"\n        timestamp = ts()\n        assert isinstance(timestamp, str)\n        assert len(timestamp) > 0\n        # Test that it's a valid time format by checking it contains expected components\n        assert any(\n            month in timestamp\n            for month in [\n                \"Jan\",\n                \"Feb\",\n                \"Mar\",\n                \"Apr\",\n                \"May\",\n                \"Jun\",\n                \"Jul\",\n                \"Aug\",\n                \"Sep\",\n                \"Oct\",\n                \"Nov\",\n                \"Dec\",\n            ]\n        )\n\n    def test_ts_consistency(self):\n        \"\"\"Test that ts() returns consistent format across multiple calls.\"\"\"\n        timestamp1 = ts()\n        time.sleep(0.1)  # Small delay to potentially get different timestamps\n        timestamp2 = ts()\n\n        # Both should be strings of reasonable length\n        assert isinstance(timestamp1, str)\n        assert isinstance(timestamp2, str)\n        assert len(timestamp1) > 20\n        assert len(timestamp2) > 20\n\n    def test_constants(self):\n        \"\"\"Test that INT32_MIN and INT32_MAX are properly defined.\"\"\"\n        assert INT32_MIN == np.iinfo(np.int32).min + 1\n        assert INT32_MAX == np.iinfo(np.int32).max - 1\n        assert INT32_MIN < INT32_MAX\n\n\nclass TestMakeForest:\n    \"\"\"Test the make_forest function for different data types and parameters.\"\"\"\n\n    @pytest.fixture\n    def float_data(self):\n        \"\"\"Create normalized float32 test data.\"\"\"\n        np.random.seed(42)\n        data = np.random.rand(100, 50).astype(np.float32)\n        # Normalize to unit sphere\n        norms = np.linalg.norm(data, axis=1, keepdims=True)\n        data = data / norms\n        return data\n\n    @pytest.fixture\n    def uint8_data(self):\n        \"\"\"Create uint8 test data.\"\"\"\n        np.random.seed(42)\n        return np.random.randint(0, 256, size=(100, 50), dtype=np.uint8)\n\n    @pytest.fixture\n    def int8_data(self):\n        \"\"\"Create int8 test data.\"\"\"\n        np.random.seed(42)\n        return np.random.randint(-128, 128, size=(100, 50), dtype=np.int8)\n\n    def test_make_forest_float32(self, float_data):\n        \"\"\"Test make_forest with float32 data.\"\"\"\n        random_state = check_random_state(42)\n        n_neighbors = 10\n        n_trees = 4\n        leaf_size = 20\n\n        result = make_forest(\n            float_data, n_neighbors, n_trees, leaf_size, random_state, np.float32\n        )\n\n        assert isinstance(result, np.ndarray)\n        assert result.dtype == np.int32\n        assert result.shape[0] >= n_trees  # Should have at least n_trees rows\n\n    def test_make_forest_uint8(self, uint8_data):\n        \"\"\"Test make_forest with uint8 data.\"\"\"\n        random_state = check_random_state(42)\n        n_neighbors = 10\n        n_trees = 4\n        leaf_size = 20\n\n        result = make_forest(\n            uint8_data, n_neighbors, n_trees, leaf_size, random_state, np.uint8\n        )\n\n        assert isinstance(result, np.ndarray)\n        assert result.dtype == np.int32\n        assert result.shape[0] >= n_trees\n\n    def test_make_forest_int8(self, int8_data):\n        \"\"\"Test make_forest with int8 data.\"\"\"\n        random_state = check_random_state(42)\n        n_neighbors = 10\n        n_trees = 4\n        leaf_size = 20\n\n        result = make_forest(\n            int8_data, n_neighbors, n_trees, leaf_size, random_state, np.int8\n        )\n\n        assert isinstance(result, np.ndarray)\n        assert result.dtype == np.int32\n        assert result.shape[0] >= n_trees\n\n    def test_make_forest_default_leaf_size(self, float_data):\n        \"\"\"Test make_forest with default leaf_size (None).\"\"\"\n        random_state = check_random_state(42)\n        n_neighbors = 15\n        n_trees = 4\n\n        result = make_forest(\n            float_data, n_neighbors, n_trees, None, random_state, np.float32\n        )\n\n        assert isinstance(result, np.ndarray)\n        assert result.dtype == np.int32\n        # With default leaf_size, it should be max(10, n_neighbors) = 15\n\n    def test_make_forest_different_max_depth(self, float_data):\n        \"\"\"Test make_forest with different max_depth values.\"\"\"\n        random_state = check_random_state(42)\n        n_neighbors = 10\n        n_trees = 2\n        leaf_size = 20\n\n        # Test with small max_depth\n        result_shallow = make_forest(\n            float_data,\n            n_neighbors,\n            n_trees,\n            leaf_size,\n            random_state,\n            np.float32,\n            max_depth=5,\n        )\n\n        # Test with large max_depth\n        random_state = check_random_state(42)  # Reset for consistency\n        result_deep = make_forest(\n            float_data,\n            n_neighbors,\n            n_trees,\n            leaf_size,\n            random_state,\n            np.float32,\n            max_depth=500,\n        )\n\n        assert isinstance(result_shallow, np.ndarray)\n        assert isinstance(result_deep, np.ndarray)\n        assert result_shallow.dtype == np.int32\n        assert result_deep.dtype == np.int32\n\n    @patch(\"evoc.knn_graph.make_float_forest\")\n    def test_make_forest_exception_handling(self, mock_make_float_forest, float_data):\n        \"\"\"Test make_forest handles exceptions properly.\"\"\"\n        # Mock the forest creation to raise an exception\n        mock_make_float_forest.side_effect = RuntimeError(\"Test exception\")\n\n        random_state = check_random_state(42)\n\n        with pytest.warns(\n            UserWarning, match=\"Random Projection forest initialisation failed\"\n        ):\n            result = make_forest(float_data, 10, 4, 20, random_state, np.float32)\n\n        # Should return empty array on exception\n        assert isinstance(result, np.ndarray)\n        assert result.shape == (0, 0)\n        assert result.dtype == np.int32\n\n\nclass TestNNDescent:\n    \"\"\"Test the nn_descent function for different data types.\"\"\"\n\n    @pytest.fixture\n    def float_data(self):\n        \"\"\"Create normalized float32 test data.\"\"\"\n        np.random.seed(42)\n        data = np.random.rand(50, 20).astype(np.float32)\n        norms = np.linalg.norm(data, axis=1, keepdims=True)\n        data = data / norms\n        return data\n\n    @pytest.fixture\n    def uint8_data(self):\n        \"\"\"Create uint8 test data.\"\"\"\n        np.random.seed(42)\n        return np.random.randint(0, 256, size=(50, 20), dtype=np.uint8)\n\n    @pytest.fixture\n    def int8_data(self):\n        \"\"\"Create int8 test data.\"\"\"\n        np.random.seed(42)\n        return np.random.randint(-128, 128, size=(50, 20), dtype=np.int8)\n\n    def test_nn_descent_float32(self, float_data):\n        \"\"\"Test nn_descent with float32 data.\"\"\"\n        rng_state = np.random.randint(INT32_MIN, INT32_MAX, 3).astype(np.int64)\n        n_neighbors = 5\n\n        with patch(\"evoc.float_nndescent.nn_descent_float\") as mock_nn_descent:\n            # Mock return value: (indices, distances)\n            mock_indices = np.random.randint(\n                0, len(float_data), size=(len(float_data), n_neighbors)\n            )\n            mock_distances = -np.random.exponential(\n                1, size=(len(float_data), n_neighbors)\n            )\n            leaf_array = np.random.randint(\n                0, float_data.shape[0], size=(4, len(float_data)), dtype=np.int32\n            )\n            mock_nn_descent.return_value = (mock_indices, mock_distances)\n\n            result = nn_descent(\n                float_data,\n                n_neighbors,\n                rng_state,\n                30,\n                5,\n                0.001,\n                np.float32,\n                leaf_array=leaf_array,\n                verbose=False,\n            )\n\n            assert len(result) == 2  # Should return (indices, distances)\n            assert result[0].shape == (len(float_data), n_neighbors)\n            assert result[1].shape == (len(float_data), n_neighbors)\n            # Distances should be transformed: maximum(-log2(-distances), 0.0)\n            assert np.all(result[1] >= 0.0)\n\n    def test_nn_descent_uint8(self, uint8_data):\n        \"\"\"Test nn_descent with uint8 data.\"\"\"\n        rng_state = np.random.randint(INT32_MIN, INT32_MAX, 3).astype(np.int64)\n        n_neighbors = 5\n\n        with patch(\"evoc.uint8_nndescent.nn_descent_uint8\") as mock_nn_descent:\n            mock_indices = np.random.randint(\n                0, len(uint8_data), size=(len(uint8_data), n_neighbors)\n            )\n            mock_distances = -np.random.exponential(\n                1, size=(len(uint8_data), n_neighbors)\n            )\n            leaf_array = np.random.randint(\n                0, uint8_data.shape[0], size=(4, len(uint8_data)), dtype=np.int32\n            )\n            mock_nn_descent.return_value = (mock_indices, mock_distances)\n\n            result = nn_descent(\n                uint8_data,\n                n_neighbors,\n                rng_state,\n                30,\n                5,\n                0.001,\n                np.uint8,\n                leaf_array=leaf_array,\n                verbose=True,\n            )\n\n            assert len(result) == 2\n            assert result[0].shape == (len(uint8_data), n_neighbors)\n            assert result[1].shape == (len(uint8_data), n_neighbors)\n            # Distances should be transformed: -log2(-distances)\n\n    def test_nn_descent_int8(self, int8_data):\n        \"\"\"Test nn_descent with int8 data.\"\"\"\n        rng_state = np.random.randint(INT32_MIN, INT32_MAX, 3).astype(np.int64)\n        n_neighbors = 5\n\n        with patch(\"evoc.int8_nndescent.nn_descent_int8\") as mock_nn_descent:\n            mock_indices = np.random.randint(\n                0, len(int8_data), size=(len(int8_data), n_neighbors)\n            )\n            mock_distances = -np.random.exponential(\n                1, size=(len(int8_data), n_neighbors)\n            )\n            mock_nn_descent.return_value = (mock_indices, mock_distances)\n            leaf_array = np.random.randint(\n                0, int8_data.shape[0], size=(4, len(int8_data)), dtype=np.int32\n            )\n\n            result = nn_descent(\n                int8_data,\n                n_neighbors,\n                rng_state,\n                30,\n                5,\n                0.001,\n                np.int8,\n                leaf_array=leaf_array,\n            )\n\n            assert len(result) == 2\n            assert result[0].shape == (len(int8_data), n_neighbors)\n            assert result[1].shape == (len(int8_data), n_neighbors)\n            # Distances should be transformed: 1.0 / (-distances)\n\n\nclass TestKNNGraph:\n    \"\"\"Test the main knn_graph function.\"\"\"\n\n    @pytest.fixture\n    def float_test_data(self):\n        \"\"\"Create test data for float32 testing.\"\"\"\n        # Create blob data that will be normalized\n        X, y = make_blobs(\n            n_samples=200, centers=4, n_features=50, cluster_std=1.0, random_state=42\n        )\n        return X.astype(np.float64)  # Start with float64 to test conversion\n\n    @pytest.fixture\n    def uint8_test_data(self):\n        \"\"\"Create uint8 test data.\"\"\"\n        np.random.seed(42)\n        return np.random.randint(0, 256, size=(100, 30), dtype=np.uint8)\n\n    @pytest.fixture\n    def int8_test_data(self):\n        \"\"\"Create int8 test data.\"\"\"\n        np.random.seed(42)\n        return np.random.randint(-128, 128, size=(100, 30), dtype=np.int8)\n\n    def test_knn_graph_float_data(self, float_test_data):\n        \"\"\"Test knn_graph with float data (gets normalized).\"\"\"\n        result = knn_graph(\n            float_test_data, n_neighbors=10, n_trees=4, random_state=42, verbose=False\n        )\n\n        assert len(result) == 2  # (indices, distances)\n        indices, distances = result\n\n        assert indices.shape == (len(float_test_data), 10)\n        assert distances.shape == (len(float_test_data), 10)\n        assert indices.dtype == np.int32 or indices.dtype == np.int64\n        assert distances.dtype == np.float32 or distances.dtype == np.float64\n\n        # Check that indices are valid\n        assert np.all(indices >= 0)\n        assert np.all(indices < len(float_test_data))\n\n        # Check that distances are non-negative (after transformation)\n        assert np.all(distances >= 0.0)\n\n    def test_knn_graph_uint8_data(self, uint8_test_data):\n        \"\"\"Test knn_graph with uint8 data.\"\"\"\n        result = knn_graph(\n            uint8_test_data, n_neighbors=5, n_trees=3, random_state=42, verbose=False\n        )\n\n        assert len(result) == 2\n        indices, distances = result\n\n        assert indices.shape == (len(uint8_test_data), 5)\n        assert distances.shape == (len(uint8_test_data), 5)\n        assert np.all(indices >= 0)\n        assert np.all(indices < len(uint8_test_data))\n\n    def test_knn_graph_int8_data(self, int8_test_data):\n        \"\"\"Test knn_graph with int8 data.\"\"\"\n        result = knn_graph(int8_test_data, n_neighbors=8, random_state=42)\n\n        assert len(result) == 2\n        indices, distances = result\n\n        assert indices.shape == (len(int8_test_data), 8)\n        assert distances.shape == (len(int8_test_data), 8)\n        assert np.all(indices >= 0)\n        assert np.all(indices < len(int8_test_data))\n\n    def test_knn_graph_parameters(self, float_test_data):\n        \"\"\"Test knn_graph with various parameter combinations.\"\"\"\n        # Test with custom parameters\n        result = knn_graph(\n            float_test_data,\n            n_neighbors=15,\n            n_trees=6,\n            leaf_size=25,\n            max_candidates=40,\n            max_rptree_depth=100,\n            n_iters=8,\n            delta=0.01,\n            n_jobs=1,\n            verbose=True,\n            random_state=123,\n        )\n\n        indices, distances = result\n        assert indices.shape == (len(float_test_data), 15)\n        assert distances.shape == (len(float_test_data), 15)\n\n    def test_knn_graph_default_parameters(self, float_test_data):\n        \"\"\"Test knn_graph with mostly default parameters.\"\"\"\n        result = knn_graph(float_test_data, random_state=42)\n\n        indices, distances = result\n        # Default n_neighbors should be 30\n        assert indices.shape == (len(float_test_data), 30)\n        assert distances.shape == (len(float_test_data), 30)\n\n    def test_knn_graph_n_jobs_setting(self, float_test_data):\n        \"\"\"Test that n_jobs parameter affects numba threading.\"\"\"\n        with (\n            patch(\"numba.get_num_threads\") as mock_get_threads,\n            patch(\"numba.set_num_threads\") as mock_set_threads,\n        ):\n\n            mock_get_threads.return_value = 8\n\n            # Test with n_jobs=-1 (should not change threads)\n            knn_graph(float_test_data, n_jobs=-1, random_state=42)\n            mock_set_threads.assert_not_called()\n\n            # Test with specific n_jobs\n            knn_graph(float_test_data, n_jobs=4, random_state=42)\n            # Should be called with 4 and then restored\n            calls = mock_set_threads.call_args_list\n            assert any(call[0][0] == 4 for call in calls)\n\n    def test_knn_graph_auto_parameters(self, float_test_data):\n        \"\"\"Test automatic parameter selection.\"\"\"\n        with patch(\"numba.get_num_threads\", return_value=2):\n            result = knn_graph(\n                float_test_data,\n                n_trees=None,  # Should be auto-selected\n                n_iters=None,  # Should be auto-selected\n                random_state=42,\n            )\n\n            assert len(result) == 2\n            # Auto n_trees should be max(4, min(8, num_threads)) = 8\n            # Auto n_iters should be max(5, int(round(log2(n_samples))))\n\n    def test_knn_graph_warning_on_failure(self, float_test_data):\n        \"\"\"Test that warning is issued when neighbor finding fails.\"\"\"\n        with patch(\"evoc.knn_graph.nn_descent\") as mock_nn_descent:\n            # Mock a result with some negative indices (indicating failure)\n            mock_indices = np.full((len(float_test_data), 10), -1, dtype=np.int32)\n            mock_distances = np.random.rand(len(float_test_data), 10)\n            mock_nn_descent.return_value = (mock_indices, mock_distances)\n\n            with pytest.warns(\n                UserWarning, match=\"Failed to correctly find n_neighbors\"\n            ):\n                result = knn_graph(float_test_data, n_neighbors=10, random_state=42)\n\n    def test_knn_graph_data_validation(self):\n        \"\"\"Test that knn_graph properly validates input data.\"\"\"\n        # Test with invalid data shape\n        invalid_data = np.array([1, 2, 3])  # 1D array\n\n        with pytest.raises((ValueError, TypeError)):\n            knn_graph(invalid_data)\n\n    def test_knn_graph_float_normalization(self):\n        \"\"\"Test that float data gets properly normalized to unit sphere.\"\"\"\n        # Create data that's not normalized\n        data = np.array([[3, 4], [1, 0], [0, 5]], dtype=np.float32)\n\n        result = knn_graph(data, n_neighbors=2, random_state=42)\n\n        # Should complete without error\n        assert len(result) == 2\n        indices, distances = result\n        assert indices.shape == (3, 2)\n        assert distances.shape == (3, 2)\n\n    def test_knn_graph_zero_norm_handling(self):\n        \"\"\"Test handling of zero-norm vectors in float data.\"\"\"\n        # Include a zero vector\n        data = np.array([[1, 1], [0, 0], [2, 2]], dtype=np.float32)\n\n        result = knn_graph(data, n_neighbors=2, random_state=42)\n\n        # Should complete without error (zero norms are set to 1.0)\n        assert len(result) == 2\n        indices, distances = result\n        assert indices.shape == (3, 2)\n        assert distances.shape == (3, 2)\n\n\nclass TestIntegration:\n    \"\"\"Integration tests for the complete knn_graph pipeline.\"\"\"\n\n    def test_small_dataset_complete_pipeline(self):\n        \"\"\"Test complete pipeline on a small dataset.\"\"\"\n        # Create a small, well-separated dataset\n        X, y = make_blobs(\n            n_samples=50, centers=3, n_features=10, cluster_std=0.5, random_state=42\n        )\n        X = X.astype(np.float32)\n\n        result = knn_graph(X, n_neighbors=5, n_trees=2, random_state=42, verbose=True)\n\n        indices, distances = result\n\n        # Basic sanity checks\n        assert indices.shape == (50, 5)\n        assert distances.shape == (50, 5)\n        assert np.all(indices >= 0)\n        assert np.all(indices < 50)\n        assert np.all(distances >= 0)\n\n        # Note: Points may include themselves as neighbors, which is normal behavior\n\n    def test_reproducibility(self):\n        \"\"\"Test that results are reproducible with same random state.\"\"\"\n        data = np.random.rand(30, 8).astype(np.float32)\n\n        result1 = knn_graph(data, n_neighbors=5, random_state=42)\n        result2 = knn_graph(data, n_neighbors=5, random_state=42)\n\n        np.testing.assert_array_equal(result1[0], result2[0])  # indices\n        np.testing.assert_array_almost_equal(result1[1], result2[1])  # distances\n\n    def test_different_data_types_consistency(self):\n        \"\"\"Test that different data types produce reasonable results.\"\"\"\n        # Create base data\n        np.random.seed(42)\n        base_data = np.random.rand(40, 20)\n\n        # Convert to different types\n        float_data = base_data.astype(np.float32)\n        uint8_data = (base_data * 255).astype(np.uint8)\n        int8_data = ((base_data - 0.5) * 255).astype(np.int8)\n\n        # Get results for each type\n        float_result = knn_graph(float_data, n_neighbors=5, random_state=42)\n        uint8_result = knn_graph(uint8_data, n_neighbors=5, random_state=42)\n        int8_result = knn_graph(int8_data, n_neighbors=5, random_state=42)\n\n        # All should have same shape\n        for result in [float_result, uint8_result, int8_result]:\n            assert result[0].shape == (40, 5)\n            assert result[1].shape == (40, 5)\n            assert np.all(result[0] >= 0)\n            assert np.all(result[0] < 40)\n            assert np.all(result[1] >= 0)\n"
  },
  {
    "path": "evoc/tests/test_knn_graph_performance.py",
    "content": "\"\"\"\nPerformance benchmark tests for the knn_graph module.\n\nThis module provides performance regression testing for the knn_graph functionality.\nThe tests are designed to be robust across different hardware configurations by using\nrelative performance metrics and adaptive thresholds.\n\"\"\"\n\nimport numpy as np\nimport pytest\nimport time\nimport platform\nfrom contextlib import contextmanager\nfrom sklearn.datasets import make_blobs\nfrom typing import Dict, Any, Tuple, List\n\ntry:\n    import psutil\n\n    HAS_PSUTIL = True\nexcept ImportError:\n    HAS_PSUTIL = False\n    psutil = None\n\nfrom evoc.knn_graph import knn_graph\n\n\nclass PerformanceMetrics:\n    \"\"\"Class to collect and analyze performance metrics.\"\"\"\n\n    def __init__(self):\n        self.metrics = {}\n        self.hardware_info = self._get_hardware_info()\n\n    def _get_hardware_info(self) -> Dict[str, Any]:\n        \"\"\"Get basic hardware information for context.\"\"\"\n        try:\n            if HAS_PSUTIL:\n                return {\n                    \"cpu_count\": psutil.cpu_count(logical=False),\n                    \"cpu_count_logical\": psutil.cpu_count(logical=True),\n                    \"memory_gb\": round(psutil.virtual_memory().total / (1024**3), 2),\n                    \"platform\": platform.platform(),\n                    \"python_version\": platform.python_version(),\n                }\n            else:\n                # Fallback without psutil\n                import os\n\n                return {\n                    \"cpu_count_logical\": os.cpu_count() or 1,\n                    \"platform\": platform.platform(),\n                    \"python_version\": platform.python_version(),\n                    \"psutil_available\": False,\n                }\n        except Exception:\n            return {\"error\": \"Could not gather hardware info\"}\n\n    def record_metric(self, test_name: str, metric_name: str, value: float):\n        \"\"\"Record a performance metric.\"\"\"\n        if test_name not in self.metrics:\n            self.metrics[test_name] = {}\n        self.metrics[test_name][metric_name] = value\n\n    def get_metric(self, test_name: str, metric_name: str) -> float:\n        \"\"\"Get a recorded metric.\"\"\"\n        return self.metrics.get(test_name, {}).get(metric_name, 0.0)\n\n\n@contextmanager\ndef time_execution():\n    \"\"\"Context manager to time code execution.\"\"\"\n    start_time = time.perf_counter()\n    yield\n    end_time = time.perf_counter()\n    return end_time - start_time\n\n\ndef time_function(func, *args, **kwargs) -> Tuple[Any, float]:\n    \"\"\"Time a function execution and return result and duration.\"\"\"\n    start_time = time.perf_counter()\n    result = func(*args, **kwargs)\n    end_time = time.perf_counter()\n    return result, end_time - start_time\n\n\nclass TestKNNGraphPerformance:\n    \"\"\"Performance tests for knn_graph functionality.\"\"\"\n\n    @pytest.fixture(scope=\"class\")\n    def perf_metrics(self):\n        \"\"\"Shared performance metrics collector.\"\"\"\n        return PerformanceMetrics()\n\n    @pytest.fixture(\n        params=[\n            (1000, 128),  # Small dataset, typical embedding size\n            (5000, 384),  # Medium dataset, larger embedding\n            (10000, 512),  # Large dataset, large embedding\n        ]\n    )\n    def dataset_config(self, request):\n        \"\"\"Different dataset configurations for performance testing.\"\"\"\n        n_samples, n_features = request.param\n        return n_samples, n_features\n\n    @pytest.fixture\n    def performance_data(self, dataset_config):\n        \"\"\"Generate performance test data.\"\"\"\n        n_samples, n_features = dataset_config\n        np.random.seed(42)  # Consistent data for reproducible benchmarks\n\n        # Create clustered data similar to real-world embeddings\n        X, y = make_blobs(\n            n_samples=n_samples,\n            centers=max(4, n_samples // 2000),  # Scale centers with data size\n            n_features=n_features,\n            cluster_std=0.5,\n            random_state=42,\n        )\n\n        # Normalize to unit sphere (typical for embeddings)\n        X = X.astype(np.float32)\n        norms = np.linalg.norm(X, axis=1, keepdims=True)\n        X = X / norms\n\n        return X, (n_samples, n_features)\n\n    def test_knn_graph_scaling_performance(self, performance_data, perf_metrics):\n        \"\"\"Test knn_graph performance scaling with different data sizes.\"\"\"\n        X, (n_samples, n_features) = performance_data\n        test_name = f\"knn_graph_scaling_{n_samples}x{n_features}\"\n\n        # Warm up run (not timed) to ensure compiled numba functions\n        if n_samples <= 1000:  # Only warm up on small data\n            knn_graph(X[:100], n_neighbors=10, n_trees=2, random_state=42)\n\n        # Timed run\n        result, duration = time_function(\n            knn_graph, X, n_neighbors=30, n_trees=4, random_state=42, verbose=False\n        )\n\n        # Record metrics\n        perf_metrics.record_metric(test_name, \"duration_seconds\", duration)\n        perf_metrics.record_metric(\n            test_name, \"samples_per_second\", n_samples / duration\n        )\n        perf_metrics.record_metric(test_name, \"n_samples\", n_samples)\n        perf_metrics.record_metric(test_name, \"n_features\", n_features)\n\n        # Verify result is correct\n        indices, distances = result\n        assert indices.shape == (n_samples, 30)\n        assert distances.shape == (n_samples, 30)\n\n        # Performance expectations (very loose bounds that should work across hardware)\n        # These are sanity checks rather than strict requirements\n        expected_min_samples_per_second = {\n            1000: 100,  # At least 100 samples/sec for small data\n            5000: 50,  # At least 50 samples/sec for medium data\n            10000: 20,  # At least 20 samples/sec for large data\n        }\n\n        min_expected = expected_min_samples_per_second.get(n_samples, 10)\n        samples_per_sec = n_samples / duration\n\n        # Log performance info\n        print(f\"\\n{test_name}:\")\n        print(f\"  Duration: {duration:.3f}s\")\n        print(f\"  Samples/sec: {samples_per_sec:.1f}\")\n        print(f\"  Hardware: {perf_metrics.hardware_info}\")\n\n        # Very loose performance check - mainly to catch major regressions\n        assert (\n            samples_per_sec > min_expected\n        ), f\"Performance too slow: {samples_per_sec:.1f} < {min_expected} samples/sec\"\n\n    def test_knn_graph_parameter_performance(self, perf_metrics):\n        \"\"\"Test performance with different parameter configurations.\"\"\"\n        np.random.seed(42)\n        n_samples, n_features = 2000, 256\n        X, _ = make_blobs(n_samples=n_samples, n_features=n_features, random_state=42)\n        X = X.astype(np.float32)\n        X = X / np.linalg.norm(X, axis=1, keepdims=True)\n\n        # Test different parameter combinations\n        param_configs = [\n            {\"n_neighbors\": 15, \"n_trees\": 2, \"name\": \"fast_config\"},\n            {\"n_neighbors\": 30, \"n_trees\": 4, \"name\": \"default_config\"},\n            {\"n_neighbors\": 50, \"n_trees\": 8, \"name\": \"high_quality_config\"},\n        ]\n\n        durations = {}\n\n        for config in param_configs:\n            name = config.pop(\"name\")\n            test_name = f\"param_performance_{name}\"\n\n            # Warm up\n            knn_graph(\n                X[:100],\n                n_neighbors=config[\"n_neighbors\"],\n                n_trees=config[\"n_trees\"],\n                random_state=42,\n            )\n\n            # Timed run\n            result, duration = time_function(knn_graph, X, random_state=42, **config)\n\n            durations[name] = duration\n            perf_metrics.record_metric(test_name, \"duration_seconds\", duration)\n            perf_metrics.record_metric(\n                test_name, \"samples_per_second\", n_samples / duration\n            )\n\n            print(f\"\\n{name}: {duration:.3f}s ({n_samples/duration:.1f} samples/sec)\")\n\n        # Verify relative performance expectations\n        # The relationship between parameters and performance can be complex\n        # So we mainly check that all configurations complete successfully\n        for name, duration in durations.items():\n            assert duration < 10.0, f\"{name} took too long: {duration:.3f}s\"\n\n        # Optionally log which configuration was fastest\n        fastest_config = min(durations, key=durations.get)\n        slowest_config = max(durations, key=durations.get)\n        print(f\"\\nFastest: {fastest_config} ({durations[fastest_config]:.3f}s)\")\n        print(f\"Slowest: {slowest_config} ({durations[slowest_config]:.3f}s)\")\n\n    def test_knn_graph_data_type_performance(self, perf_metrics):\n        \"\"\"Test performance differences between data types.\"\"\"\n        np.random.seed(42)\n        n_samples, n_features = 2000, 128\n\n        # Generate base data\n        base_data = np.random.rand(n_samples, n_features)\n\n        # Convert to different types\n        float_data = base_data.astype(np.float32)\n        uint8_data = (base_data * 255).astype(np.uint8)\n        int8_data = ((base_data - 0.5) * 255).astype(np.int8)\n\n        data_types = [\n            (float_data, \"float32\"),\n            (uint8_data, \"uint8\"),\n            (int8_data, \"int8\"),\n        ]\n\n        durations = {}\n\n        for data, dtype_name in data_types:\n            test_name = f\"dtype_performance_{dtype_name}\"\n\n            # Warm up\n            knn_graph(data[:100], n_neighbors=10, n_trees=2, random_state=42)\n\n            # Timed run\n            result, duration = time_function(\n                knn_graph,\n                data,\n                n_neighbors=20,\n                n_trees=4,\n                random_state=42,\n                verbose=False,\n            )\n\n            durations[dtype_name] = duration\n            perf_metrics.record_metric(test_name, \"duration_seconds\", duration)\n            perf_metrics.record_metric(\n                test_name, \"samples_per_second\", n_samples / duration\n            )\n\n            print(\n                f\"\\n{dtype_name}: {duration:.3f}s ({n_samples/duration:.1f} samples/sec)\"\n            )\n\n        # All should complete in reasonable time\n        for dtype_name, duration in durations.items():\n            assert duration < 30.0, f\"{dtype_name} took too long: {duration:.3f}s\"\n\n    def test_knn_graph_threading_performance(self, perf_metrics):\n        \"\"\"Test performance scaling with different thread counts.\"\"\"\n        np.random.seed(42)\n        n_samples, n_features = 3000, 256\n        X, _ = make_blobs(n_samples=n_samples, n_features=n_features, random_state=42)\n        X = X.astype(np.float32)\n        X = X / np.linalg.norm(X, axis=1, keepdims=True)\n\n        # Test different thread counts\n        if HAS_PSUTIL:\n            max_threads = min(\n                8, psutil.cpu_count(logical=True)\n            )  # Don't exceed available cores\n        else:\n            import os\n\n            max_threads = min(8, os.cpu_count() or 1)\n\n        thread_counts = [1, max(2, max_threads // 2), max_threads]\n\n        durations = {}\n\n        for n_jobs in thread_counts:\n            test_name = f\"threading_performance_{n_jobs}_threads\"\n\n            # Warm up\n            knn_graph(\n                X[:100], n_neighbors=10, n_trees=2, n_jobs=n_jobs, random_state=42\n            )\n\n            # Timed run\n            result, duration = time_function(\n                knn_graph,\n                X,\n                n_neighbors=20,\n                n_trees=4,\n                n_jobs=n_jobs,\n                random_state=42,\n                verbose=False,\n            )\n\n            durations[n_jobs] = duration\n            perf_metrics.record_metric(test_name, \"duration_seconds\", duration)\n            perf_metrics.record_metric(\n                test_name, \"samples_per_second\", n_samples / duration\n            )\n\n            print(\n                f\"\\n{n_jobs} threads: {duration:.3f}s ({n_samples/duration:.1f} samples/sec)\"\n            )\n\n        # More threads should generally be faster (within reason)\n        if len(durations) >= 2 and max_threads > 1:\n            single_thread_time = durations[1]\n            multi_thread_time = durations[max_threads]\n\n            # Allow for some overhead but expect some speedup\n            speedup_ratio = single_thread_time / multi_thread_time\n            expected_min_speedup = 1.2  # At least 20% speedup with more threads\n\n            print(f\"\\nSpeedup ratio: {speedup_ratio:.2f}x\")\n\n            # Only assert if we have multiple cores available\n            if max_threads > 2:\n                assert (\n                    speedup_ratio > expected_min_speedup\n                ), f\"Multi-threading should provide speedup: {speedup_ratio:.2f}x < {expected_min_speedup}x\"\n\n    def test_memory_usage_scaling(self, perf_metrics):\n        \"\"\"Test memory usage scaling (basic check).\"\"\"\n        if not HAS_PSUTIL:\n            pytest.skip(\"psutil not available for memory testing\")\n\n        import gc\n\n        # Get baseline memory\n        gc.collect()\n        process = psutil.Process()\n        baseline_memory = process.memory_info().rss / 1024 / 1024  # MB\n\n        test_sizes = [(1000, 64), (2000, 64), (4000, 64)]\n        memory_usages = []\n\n        for n_samples, n_features in test_sizes:\n            gc.collect()\n\n            # Generate data\n            np.random.seed(42)\n            X, _ = make_blobs(\n                n_samples=n_samples, n_features=n_features, random_state=42\n            )\n            X = X.astype(np.float32)\n            X = X / np.linalg.norm(X, axis=1, keepdims=True)\n\n            # Run knn_graph\n            before_memory = process.memory_info().rss / 1024 / 1024\n            result = knn_graph(\n                X, n_neighbors=20, n_trees=4, random_state=42, verbose=False\n            )\n            after_memory = process.memory_info().rss / 1024 / 1024\n\n            memory_increase = after_memory - baseline_memory\n            memory_usages.append((n_samples, memory_increase))\n\n            test_name = f\"memory_usage_{n_samples}_samples\"\n            perf_metrics.record_metric(test_name, \"memory_mb\", memory_increase)\n\n            print(f\"\\n{n_samples} samples: {memory_increase:.1f} MB\")\n\n            # Clean up\n            del X, result\n            gc.collect()\n\n        # Memory usage should scale reasonably (not exponentially)\n        if len(memory_usages) >= 2:\n            small_n, small_mem = memory_usages[0]\n            large_n, large_mem = memory_usages[-1]\n\n            sample_ratio = large_n / small_n\n            memory_ratio = large_mem / max(small_mem, 1.0)  # Avoid division by zero\n\n            # Memory should not grow faster than O(n^2)\n            assert (\n                memory_ratio < sample_ratio**1.5\n            ), f\"Memory usage growing too fast: {memory_ratio:.2f}x for {sample_ratio:.2f}x samples\"\n\n    def test_reproducibility_performance(self, perf_metrics):\n        \"\"\"Test that performance is consistent across runs.\"\"\"\n        np.random.seed(42)\n        n_samples, n_features = 1500, 128\n        X, _ = make_blobs(n_samples=n_samples, n_features=n_features, random_state=42)\n        X = X.astype(np.float32)\n        X = X / np.linalg.norm(X, axis=1, keepdims=True)\n\n        # Warm up\n        knn_graph(X[:100], n_neighbors=10, n_trees=2, random_state=42)\n\n        # Run multiple times\n        n_runs = 3\n        durations = []\n\n        for i in range(n_runs):\n            result, duration = time_function(\n                knn_graph,\n                X,\n                n_neighbors=20,\n                n_trees=4,\n                random_state=42,  # Same random state for consistency\n                verbose=False,\n            )\n            durations.append(duration)\n\n        # Calculate statistics\n        mean_duration = np.mean(durations)\n        std_duration = np.std(durations)\n        cv = std_duration / mean_duration  # Coefficient of variation\n\n        perf_metrics.record_metric(\"reproducibility\", \"mean_duration\", mean_duration)\n        perf_metrics.record_metric(\"reproducibility\", \"std_duration\", std_duration)\n        perf_metrics.record_metric(\"reproducibility\", \"coefficient_of_variation\", cv)\n\n        print(f\"\\nReproducibility test:\")\n        print(f\"  Mean duration: {mean_duration:.3f}s\")\n        print(f\"  Std deviation: {std_duration:.3f}s\")\n        print(f\"  Coefficient of variation: {cv:.3f}\")\n\n        # Performance should be reasonably consistent\n        # Allow for up to 20% variation between runs\n        assert cv < 0.4, f\"Performance too variable: CV = {cv:.3f}\"\n\n        # Verify results are identical\n        result1, _ = time_function(knn_graph, X, n_neighbors=10, random_state=42)\n        result2, _ = time_function(knn_graph, X, n_neighbors=10, random_state=42)\n\n        np.testing.assert_array_equal(result1[0], result2[0])\n        np.testing.assert_array_almost_equal(result1[1], result2[1])\n\n\n@pytest.mark.performance\nclass TestPerformanceRegression:\n    \"\"\"Performance regression tests with historical baselines.\"\"\"\n\n    def test_baseline_performance_check(self):\n        \"\"\"\n        Baseline performance test that can be used to establish performance standards.\n\n        This test should be run on a reference machine to establish baseline timings,\n        and then used in CI to detect significant regressions.\n        \"\"\"\n        np.random.seed(42)\n\n        # Standard test case\n        n_samples, n_features = 5000, 256\n        X, _ = make_blobs(n_samples=n_samples, n_features=n_features, random_state=42)\n        X = X.astype(np.float32)\n        X = X / np.linalg.norm(X, axis=1, keepdims=True)\n\n        # Warm up\n        knn_graph(X[:100], n_neighbors=10, n_trees=2, random_state=42)\n\n        # Benchmark run\n        start_time = time.perf_counter()\n        result = knn_graph(X, n_neighbors=30, n_trees=4, random_state=42, verbose=False)\n        duration = time.perf_counter() - start_time\n\n        indices, distances = result\n        samples_per_second = n_samples / duration\n\n        print(f\"\\nBaseline Performance Report:\")\n        print(f\"  Dataset: {n_samples} samples x {n_features} features\")\n        print(f\"  Duration: {duration:.3f} seconds\")\n        print(f\"  Throughput: {samples_per_second:.1f} samples/second\")\n        print(f\"  Hardware: {platform.platform()}\")\n        if HAS_PSUTIL:\n            print(f\"  CPU cores: {psutil.cpu_count(logical=True)}\")\n            print(f\"  Memory: {psutil.virtual_memory().total / (1024**3):.1f} GB\")\n        else:\n            import os\n\n            print(f\"  CPU cores: {os.cpu_count() or 'unknown'}\")\n            print(f\"  Memory: unknown (psutil not available)\")\n\n        # Basic sanity checks\n        assert indices.shape == (n_samples, 30)\n        assert distances.shape == (n_samples, 30)\n        assert np.all(indices >= 0)\n        assert np.all(distances >= 0)\n\n        # Very basic performance floor (should work on any reasonable hardware)\n        min_samples_per_second = 10  # Very conservative\n        assert (\n            samples_per_second > min_samples_per_second\n        ), f\"Performance below minimum threshold: {samples_per_second:.1f} < {min_samples_per_second}\"\n\n        # Store baseline for potential future comparison\n        # In a real CI system, you might save this to a file or database\n        baseline_info = {\n            \"duration\": duration,\n            \"samples_per_second\": samples_per_second,\n            \"hardware_hash\": hash(platform.platform()),\n            \"timestamp\": time.time(),\n        }\n\n        # Note: baseline_info could be used for comparison in CI systems\n        # but we don't return it to avoid pytest warnings\n"
  },
  {
    "path": "evoc/tests/test_numba_kdtree.py",
    "content": "\"\"\"\nTest suite for NumbaKDTree compatibility with sklearn KDTree.\n\nThis module tests that our NumbaKDTree implementation produces equivalent\npartitioning and query results compared to sklearn's KDTree implementation.\n\"\"\"\n\nimport numpy as np\nimport pytest\nimport numba\nfrom sklearn.neighbors import KDTree as SklearnKDTree\n\nfrom evoc.numba_kdtree import build_kdtree\n\n\nclass TestKDTreeCompatibility:\n    \"\"\"Test compatibility between NumbaKDTree and sklearn KDTree implementations.\"\"\"\n\n    @pytest.fixture(\n        params=[\n            (50, 2),  # Small 2D\n            (100, 3),  # Medium 3D\n            (200, 5),  # Large 5D\n            (500, 8),  # Large 8D\n        ]\n    )\n    def test_data(self, request):\n        \"\"\"Generate test data for various configurations.\"\"\"\n        n_samples, n_features = request.param\n        np.random.seed(42)  # Fixed seed for reproducible tests\n        return np.random.rand(n_samples, n_features).astype(np.float32)\n\n    @pytest.fixture(params=[10, 20, 40])\n    def leaf_size(self, request):\n        \"\"\"Test different leaf sizes.\"\"\"\n        return request.param\n\n    def test_tree_structure_compatibility(self, test_data, leaf_size):\n        \"\"\"Test that tree structures have compatible shapes and properties.\"\"\"\n        # Build trees\n        sklearn_tree = SklearnKDTree(test_data, leaf_size=leaf_size)\n        numba_tree = build_kdtree(test_data, leaf_size=leaf_size)\n\n        # Get sklearn internal arrays\n        sk_data, sk_idx_array, sk_node_data, sk_node_bounds = sklearn_tree.get_arrays()\n\n        # Test data compatibility\n        assert np.array_equal(sk_data, numba_tree.data), \"Data arrays should match\"\n        assert (\n            sk_idx_array.shape == numba_tree.idx_array.shape\n        ), \"Index array shapes should match\"\n\n        # Test node data shapes\n        assert (\n            sk_node_data[\"idx_start\"].shape == numba_tree.idx_start.shape\n        ), \"idx_start shapes should match\"\n        assert (\n            sk_node_data[\"idx_end\"].shape == numba_tree.idx_end.shape\n        ), \"idx_end shapes should match\"\n        assert (\n            sk_node_data[\"radius\"].shape == numba_tree.radius.shape\n        ), \"radius shapes should match\"\n        assert (\n            sk_node_data[\"is_leaf\"].shape == numba_tree.is_leaf.shape\n        ), \"is_leaf shapes should match\"\n\n        # Test node bounds shape\n        assert (\n            sk_node_bounds.shape == numba_tree.node_bounds.shape\n        ), \"Node bounds shapes should match\"\n\n    def test_node_partitioning_equivalence(self, test_data, leaf_size):\n        \"\"\"\n        Test that both implementations partition data into equivalent node sets.\n\n        This verifies that each node contains the same set of data points,\n        regardless of internal ordering differences.\n        \"\"\"\n        # Build trees\n        sklearn_tree = SklearnKDTree(test_data, leaf_size=leaf_size)\n        numba_tree = build_kdtree(test_data, leaf_size=leaf_size)\n\n        # Get sklearn internal arrays\n        sk_data, sk_idx_array, sk_node_data, sk_node_bounds = sklearn_tree.get_arrays()\n        n_nodes = sk_node_data.shape[0]\n\n        matches = 0\n        total_comparisons = 0\n\n        for node in range(n_nodes):\n            # Get node boundaries\n            sk_start = sk_node_data[node][\"idx_start\"]\n            sk_end = sk_node_data[node][\"idx_end\"]\n            sk_is_leaf = sk_node_data[node][\"is_leaf\"]\n\n            nb_start = numba_tree.idx_start[node]\n            nb_end = numba_tree.idx_end[node]\n            nb_is_leaf = numba_tree.is_leaf[node]\n\n            # Node properties should match exactly\n            assert sk_start == nb_start, f\"Node {node}: idx_start mismatch\"\n            assert sk_end == nb_end, f\"Node {node}: idx_end mismatch\"\n            assert sk_is_leaf == nb_is_leaf, f\"Node {node}: is_leaf mismatch\"\n\n            # Skip empty nodes\n            if sk_start >= sk_end:\n                continue\n\n            total_comparisons += 1\n\n            # Get indices for this node and sort them (to ignore ordering differences)\n            sk_indices = np.sort(sk_idx_array[sk_start:sk_end])\n            nb_indices = np.sort(numba_tree.idx_array[nb_start:nb_end])\n\n            # The sorted indices should be identical\n            if np.array_equal(sk_indices, nb_indices):\n                matches += 1\n\n        # Require high compatibility (allowing for minor algorithmic differences)\n        match_rate = matches / total_comparisons if total_comparisons > 0 else 1.0\n        assert (\n            match_rate >= 0.95\n        ), f\"Node partitioning match rate {match_rate:.1%} is below 95% threshold\"\n\n    def test_data_ordering_equivalence(self, test_data, leaf_size):\n        \"\"\"\n        Test that data ordering along split axes is equivalent.\n\n        This is a more fundamental test of whether the partitioning logic\n        is working similarly between implementations.\n        \"\"\"\n        # Build trees\n        sklearn_tree = SklearnKDTree(test_data, leaf_size=leaf_size)\n        numba_tree = build_kdtree(test_data, leaf_size=leaf_size)\n\n        # Get sklearn internal arrays\n        sk_data, sk_idx_array, sk_node_data, sk_node_bounds = sklearn_tree.get_arrays()\n        n_nodes = sk_node_data.shape[0]\n\n        axis_ordering_matches = 0\n        total_internal_nodes = 0\n\n        for node in range(n_nodes):\n            # Only check internal nodes (non-leaf nodes)\n            if sk_node_data[node][\"is_leaf\"]:\n                continue\n\n            total_internal_nodes += 1\n\n            # Get node boundaries\n            sk_start = sk_node_data[node][\"idx_start\"]\n            sk_end = sk_node_data[node][\"idx_end\"]\n\n            # Skip if insufficient points\n            if sk_end - sk_start < 2:\n                continue\n\n            # Get indices for both implementations\n            sk_indices = sk_idx_array[sk_start:sk_end]\n            nb_indices = numba_tree.idx_array[sk_start:sk_end]\n\n            # Find split axis (dimension with maximum spread)\n            spreads = []\n            for axis in range(test_data.shape[1]):\n                sk_values = test_data[sk_indices, axis]\n                min_val, max_val = np.min(sk_values), np.max(sk_values)\n                spreads.append(max_val - min_val)\n\n            split_axis = np.argmax(spreads)\n\n            # Get data values along split axis\n            sk_axis_values = test_data[sk_indices, split_axis]\n            nb_axis_values = test_data[nb_indices, split_axis]\n\n            # Check if the median/partition point is similar\n            sk_median = np.median(sk_axis_values)\n            nb_median = np.median(nb_axis_values)\n\n            # Count points on each side of median\n            sk_left_count = np.sum(sk_axis_values <= sk_median)\n            sk_right_count = np.sum(sk_axis_values > sk_median)\n            nb_left_count = np.sum(nb_axis_values <= nb_median)\n            nb_right_count = np.sum(nb_axis_values > nb_median)\n\n            # Check if partitioning is roughly equivalent\n            # (allowing for different tie-breaking in median calculation)\n            partitioning_similar = (\n                abs(sk_left_count - nb_left_count) <= 2\n                and abs(sk_right_count - nb_right_count) <= 2\n            )\n\n            if partitioning_similar:\n                axis_ordering_matches += 1\n\n        # Require high compatibility for data ordering\n        ordering_match_rate = (\n            axis_ordering_matches / total_internal_nodes\n            if total_internal_nodes > 0\n            else 1.0\n        )\n        assert (\n            ordering_match_rate >= 0.80\n        ), f\"Data ordering match rate {ordering_match_rate:.1%} is below 80% threshold\"\n\n    def test_query_results_compatibility(self, test_data, leaf_size):\n        \"\"\"Test that query results are equivalent between implementations.\"\"\"\n        # Build trees\n        sklearn_tree = SklearnKDTree(test_data, leaf_size=leaf_size)\n        numba_tree = build_kdtree(test_data, leaf_size=leaf_size)\n\n        # Create query points (subset of original data for deterministic results)\n        np.random.seed(123)\n        query_indices = np.random.choice(\n            len(test_data), size=min(10, len(test_data)), replace=False\n        )\n        query_data = test_data[query_indices]\n\n        k = min(5, len(test_data))  # Number of neighbors\n\n        # Query sklearn tree\n        sk_distances, sk_indices = sklearn_tree.query(\n            query_data, k=k, return_distance=True\n        )\n\n        # Query numba tree using the parallel implementation\n        from evoc.numba_kdtree import parallel_tree_query\n\n        nb_distances, nb_indices = parallel_tree_query(\n            numba_tree,\n            query_data,\n            k=numba.int64(k),\n            output_rdist=numba.types.boolean(False),\n        )\n\n        # Results should be very similar (allowing for minor floating point differences)\n        # Sort both results by indices to handle any ordering differences\n        for i in range(len(query_data)):\n            # Sort by indices to compare equivalent sets\n            sk_sorted_idx = np.argsort(sk_indices[i])\n            nb_sorted_idx = np.argsort(nb_indices[i])\n\n            sk_sorted_indices = sk_indices[i][sk_sorted_idx]\n            nb_sorted_indices = nb_indices[i][nb_sorted_idx]\n            sk_sorted_distances = sk_distances[i][sk_sorted_idx]\n            nb_sorted_distances = nb_distances[i][nb_sorted_idx]\n\n            # Check that we get the same nearest neighbors\n            np.testing.assert_array_equal(\n                sk_sorted_indices,\n                nb_sorted_indices,\n                err_msg=f\"Query {i}: Nearest neighbor indices don't match\",\n            )\n\n            # Check that distances are very close\n            np.testing.assert_allclose(\n                sk_sorted_distances,\n                nb_sorted_distances,\n                rtol=1e-5,\n                atol=1e-6,\n                err_msg=f\"Query {i}: Distances don't match within tolerance\",\n            )\n\n    def test_tree_bounds_compatibility(self, test_data, leaf_size):\n        \"\"\"Test that node bounds are calculated consistently.\"\"\"\n        # Build trees\n        sklearn_tree = SklearnKDTree(test_data, leaf_size=leaf_size)\n        numba_tree = build_kdtree(test_data, leaf_size=leaf_size)\n\n        # Get sklearn bounds\n        sk_data, sk_idx_array, sk_node_data, sk_node_bounds = sklearn_tree.get_arrays()\n\n        # Node bounds should match closely\n        np.testing.assert_allclose(\n            sk_node_bounds,\n            numba_tree.node_bounds,\n            rtol=1e-5,\n            atol=1e-6,\n            err_msg=\"Node bounds don't match between implementations\",\n        )\n\n\nclass TestKDTreeEdgeCases:\n    \"\"\"Test edge cases and special conditions.\"\"\"\n\n    def test_single_point(self):\n        \"\"\"Test with a single data point.\"\"\"\n        data = np.array([[1.0, 2.0, 3.0]], dtype=np.float32)\n\n        sklearn_tree = SklearnKDTree(data, leaf_size=1)\n        numba_tree = build_kdtree(data, leaf_size=1)\n\n        # Should handle single point gracefully\n        assert numba_tree.data.shape == (1, 3)\n        assert numba_tree.idx_array.shape == (1,)\n\n    def test_duplicate_points(self):\n        \"\"\"Test with duplicate data points.\"\"\"\n        data = np.array(\n            [\n                [1.0, 2.0],\n                [1.0, 2.0],  # Duplicate\n                [3.0, 4.0],\n                [1.0, 2.0],  # Another duplicate\n            ],\n            dtype=np.float32,\n        )\n\n        sklearn_tree = SklearnKDTree(data, leaf_size=2)\n        numba_tree = build_kdtree(data, leaf_size=2)\n\n        # Should handle duplicates without error\n        assert numba_tree.data.shape == data.shape\n\n        # Query should work with duplicates\n        from evoc.numba_kdtree import parallel_tree_query\n\n        distances, indices = parallel_tree_query(\n            numba_tree, data[:1], k=2, output_rdist=False\n        )\n        assert distances.shape == (1, 2)\n        assert indices.shape == (1, 2)\n\n    def test_high_dimensional_data(self):\n        \"\"\"Test with high-dimensional data.\"\"\"\n        np.random.seed(42)\n        data = np.random.rand(100, 50).astype(np.float32)  # 50D data\n\n        sklearn_tree = SklearnKDTree(data, leaf_size=10)\n        numba_tree = build_kdtree(data, leaf_size=10)\n\n        # Should handle high dimensions\n        assert numba_tree.data.shape == (100, 50)\n\n        # Quick query test\n        from evoc.numba_kdtree import parallel_tree_query\n\n        distances, indices = parallel_tree_query(\n            numba_tree, data[:5], k=3, output_rdist=False\n        )\n        assert distances.shape == (5, 3)\n        assert indices.shape == (5, 3)\n\n\n# Integration test that can be run standalone\ndef test_full_pipeline_compatibility():\n    \"\"\"Integration test ensuring the full pipeline works with both tree types.\"\"\"\n    np.random.seed(42)\n    data = np.random.rand(200, 5).astype(np.float32)\n\n    # Build numba tree and run boruvka (this was the original failing case)\n    from evoc.numba_kdtree import build_kdtree\n    from evoc.boruvka import parallel_boruvka\n\n    tree = build_kdtree(data, leaf_size=20)\n    num_threads = numba.get_num_threads()\n\n    # This should not raise any numba errors\n    edges = parallel_boruvka(\n        tree, n_threads=num_threads, min_samples=5, reproducible=True\n    )\n\n    # Should produce reasonable results\n    assert len(edges) > 0, \"Boruvka should produce some edges\"\n    assert edges.shape[1] == 3, \"Edges should have 3 columns (from, to, weight)\"\n    assert np.all(edges[:, 2] >= 0), \"Edge weights should be non-negative\"\n\n\nif __name__ == \"__main__\":\n    # Allow running as a script for quick testing\n    pytest.main([__file__, \"-v\"])\n"
  },
  {
    "path": "evoc/tests/test_numba_kdtree_performance.py",
    "content": "\"\"\"\nPerformance benchmark tests for the numba_kdtree module.\n\nThis module provides performance regression testing and comparison benchmarks\nagainst sklearn's KDTree implementation. The numba implementation is optimized\nfor large query batches where parallelization benefits outweigh overhead.\n\nKey performance characteristics:\n- Small batches (<1000 queries): May be slower due to parallelization overhead\n- Medium batches (1000-3000 queries): Competitive to slightly faster\n- Large batches (3000+ queries): Significant speedup (3-20x) due to parallelization\n- Ultra-large batches (10k+ queries): Maximum speedup, ideal use case\n\nThe tests focus on large query batch scenarios since that is the primary\noptimization target for the numba implementation.\n\"\"\"\n\nimport numpy as np\nimport pytest\nimport time\nimport platform\nfrom contextlib import contextmanager\nfrom sklearn.datasets import make_blobs\nfrom sklearn.neighbors import KDTree as SklearnKDTree\nfrom typing import Dict, Any, Tuple, List\n\ntry:\n    import psutil\n\n    HAS_PSUTIL = True\nexcept ImportError:\n    HAS_PSUTIL = False\n    psutil = None\n\nfrom evoc.numba_kdtree import build_kdtree, parallel_tree_query, kdtree_to_numba\n\n\ndef time_function(func, *args, **kwargs) -> Tuple[Any, float]:\n    \"\"\"Time a function execution and return result and duration.\"\"\"\n    start_time = time.perf_counter()\n    result = func(*args, **kwargs)\n    end_time = time.perf_counter()\n    return result, end_time - start_time\n\n\nclass KDTreePerformanceMetrics:\n    \"\"\"Class to collect and analyze KDTree performance metrics.\"\"\"\n\n    def __init__(self):\n        self.metrics = {}\n        self.hardware_info = self._get_hardware_info()\n\n    def _get_hardware_info(self) -> Dict[str, Any]:\n        \"\"\"Get basic hardware information for context.\"\"\"\n        try:\n            if HAS_PSUTIL:\n                return {\n                    \"cpu_count\": psutil.cpu_count(logical=False),\n                    \"cpu_count_logical\": psutil.cpu_count(logical=True),\n                    \"memory_gb\": round(psutil.virtual_memory().total / (1024**3), 2),\n                    \"platform\": platform.platform(),\n                    \"python_version\": platform.python_version(),\n                }\n            else:\n                import os\n\n                return {\n                    \"cpu_count_logical\": os.cpu_count() or 1,\n                    \"platform\": platform.platform(),\n                    \"python_version\": platform.python_version(),\n                    \"psutil_available\": False,\n                }\n        except Exception:\n            return {\"error\": \"Could not gather hardware info\"}\n\n    def record_metric(self, test_name: str, metric_name: str, value: float):\n        \"\"\"Record a performance metric.\"\"\"\n        if test_name not in self.metrics:\n            self.metrics[test_name] = {}\n        self.metrics[test_name][metric_name] = value\n\n    def get_metric(self, test_name: str, metric_name: str) -> float:\n        \"\"\"Get a recorded metric.\"\"\"\n        return self.metrics.get(test_name, {}).get(metric_name, 0.0)\n\n\n@pytest.mark.performance\nclass TestKDTreePerformance:\n    \"\"\"Performance tests for numba KDTree implementation.\"\"\"\n\n    @pytest.fixture(scope=\"class\")\n    def perf_metrics(self):\n        \"\"\"Shared performance metrics collector.\"\"\"\n        return KDTreePerformanceMetrics()\n\n    @pytest.fixture(\n        params=[\n            (1000, 2),  # Small 2D dataset\n            (5000, 3),  # Medium 3D dataset\n            (10000, 5),  # Large 5D dataset\n            (20000, 8),  # Very large 8D dataset\n        ]\n    )\n    def dataset_config(self, request):\n        \"\"\"Different dataset configurations for performance testing.\"\"\"\n        n_samples, n_features = request.param\n        return n_samples, n_features\n\n    @pytest.fixture\n    def performance_data(self, dataset_config):\n        \"\"\"Generate performance test data.\"\"\"\n        n_samples, n_features = dataset_config\n        np.random.seed(42)  # Consistent data for reproducible benchmarks\n\n        # Create diverse data that exercises different tree structures\n        if n_features <= 3:\n            # Use blobs for low-dimensional data\n            X, y = make_blobs(\n                n_samples=n_samples,\n                centers=max(4, n_samples // 1000),\n                n_features=n_features,\n                cluster_std=1.0,\n                random_state=42,\n            )\n        else:\n            # Use uniform random for higher dimensions\n            X = np.random.rand(n_samples, n_features) * 10.0\n\n        X = X.astype(np.float32)\n        return X, (n_samples, n_features)\n\n    def test_kdtree_construction_performance(self, performance_data, perf_metrics):\n        \"\"\"Compare KDTree construction performance: Numba vs Sklearn.\"\"\"\n        X, (n_samples, n_features) = performance_data\n        test_name = f\"construction_{n_samples}x{n_features}\"\n\n        # Warm up numba compilation (not timed)\n        if n_samples >= 1000:\n            warmup_data = X[:100].copy()\n            build_kdtree(warmup_data, leaf_size=10)\n\n        # Test sklearn construction\n        sklearn_tree, sklearn_time = time_function(SklearnKDTree, X, leaf_size=40)\n\n        # Test numba construction\n        numba_tree, numba_time = time_function(build_kdtree, X, leaf_size=40)\n\n        # Record metrics\n        perf_metrics.record_metric(test_name, \"sklearn_construction_time\", sklearn_time)\n        perf_metrics.record_metric(test_name, \"numba_construction_time\", numba_time)\n        perf_metrics.record_metric(\n            test_name, \"construction_speedup\", sklearn_time / numba_time\n        )\n        perf_metrics.record_metric(test_name, \"n_samples\", n_samples)\n        perf_metrics.record_metric(test_name, \"n_features\", n_features)\n\n        # Calculate throughput\n        sklearn_throughput = n_samples / sklearn_time\n        numba_throughput = n_samples / numba_time\n\n        print(f\"\\n{test_name} Construction Performance:\")\n        print(f\"  Sklearn: {sklearn_time:.4f}s ({sklearn_throughput:.0f} samples/sec)\")\n        print(f\"  Numba:   {numba_time:.4f}s ({numba_throughput:.0f} samples/sec)\")\n        print(f\"  Speedup: {sklearn_time/numba_time:.2f}x\")\n\n        # Verify both trees work correctly\n        query_point = X[0:1]\n        sklearn_dists, sklearn_inds = sklearn_tree.query(query_point, k=5)\n        numba_dists, numba_inds = parallel_tree_query(\n            numba_tree, query_point, k=5, output_rdist=False\n        )\n\n        assert sklearn_dists.shape == (1, 5)\n        assert numba_dists.shape == (1, 5)\n        assert sklearn_inds.shape == (1, 5)\n        assert numba_inds.shape == (1, 5)\n\n        # Performance expectations\n        # After warmup, numba should be competitive or better\n        if (\n            n_samples >= 1000\n        ):  # Only assert on larger datasets where speedup is more likely\n            assert (\n                numba_time < sklearn_time * 2.0\n            ), f\"Numba construction too slow: {numba_time:.4f}s vs sklearn {sklearn_time:.4f}s\"\n\n    def test_kdtree_query_performance_large_batch(self, performance_data, perf_metrics):\n        \"\"\"Compare large batch query performance: Numba vs Sklearn (optimized use case).\"\"\"\n        X, (n_samples, n_features) = performance_data\n        test_name = f\"query_large_batch_{n_samples}x{n_features}\"\n\n        # Build trees\n        sklearn_tree = SklearnKDTree(X, leaf_size=40)\n        numba_tree = build_kdtree(X, leaf_size=40)\n\n        # Prepare large query batch - this is where numba should excel\n        np.random.seed(123)\n        # Use large query sets that benefit from parallelization\n        n_queries = max(1000, n_samples // 2)  # Large query batches\n        query_data = np.random.rand(n_queries, n_features).astype(np.float32) * 10.0\n        k = min(30, n_samples // 20)  # Reasonable k value\n\n        # Warm up numba (not timed)\n        _ = parallel_tree_query(numba_tree, query_data[:5], k=k, output_rdist=False)\n\n        # Time sklearn queries\n        sklearn_result, sklearn_time = time_function(\n            sklearn_tree.query, query_data, k=k\n        )\n\n        # Time numba queries\n        numba_result, numba_time = time_function(\n            parallel_tree_query, numba_tree, query_data, k=k, output_rdist=False\n        )\n\n        # Record metrics\n        perf_metrics.record_metric(test_name, \"sklearn_query_time\", sklearn_time)\n        perf_metrics.record_metric(test_name, \"numba_query_time\", numba_time)\n        perf_metrics.record_metric(\n            test_name, \"query_speedup\", sklearn_time / numba_time\n        )\n        perf_metrics.record_metric(\n            test_name, \"queries_per_second_sklearn\", n_queries / sklearn_time\n        )\n        perf_metrics.record_metric(\n            test_name, \"queries_per_second_numba\", n_queries / numba_time\n        )\n\n        sklearn_qps = n_queries / sklearn_time\n        numba_qps = n_queries / numba_time\n\n        print(\n            f\"\\n{test_name} Large Batch Query Performance ({n_queries} queries, k={k}):\"\n        )\n        print(f\"  Sklearn: {sklearn_time:.4f}s ({sklearn_qps:.0f} queries/sec)\")\n        print(f\"  Numba:   {numba_time:.4f}s ({numba_qps:.0f} queries/sec)\")\n        print(f\"  Speedup: {sklearn_time/numba_time:.2f}x\")\n\n        # Verify results have correct shape\n        sklearn_dists, sklearn_inds = sklearn_result\n        numba_dists, numba_inds = numba_result\n\n        assert sklearn_dists.shape == (n_queries, k)\n        assert numba_dists.shape == (n_queries, k)\n        assert sklearn_inds.shape == (n_queries, k)\n        assert numba_inds.shape == (n_queries, k)\n\n        # Performance expectations for large batches\n        # Numba should excel with large query sets due to parallelization\n        # But only assert performance for sufficiently large batches where parallelization benefit outweighs overhead\n        if (\n            n_queries >= 3000\n        ):  # Only assert performance for large enough batches where advantage is consistent\n            assert (\n                numba_time < sklearn_time * 1.0\n            ), f\"Numba queries too slow for large batch ({n_queries} queries): {numba_time:.4f}s vs sklearn {sklearn_time:.4f}s\"\n            # For large query batches, expect significant speedup\n            assert (\n                sklearn_time / numba_time > 1.0\n            ), f\"Expected numba advantage for large batches ({n_queries} queries): {sklearn_time/numba_time:.2f}x speedup\"\n        elif n_queries >= 2000:  # Medium-large batches should show some advantage\n            assert (numba_time < sklearn_time * 1.0) or (\n                numba_time < 0.05\n            ), f\"Numba queries too slow for medium-large batch ({n_queries} queries): {numba_time:.4f}s vs sklearn {sklearn_time:.4f}s\"\n            # Some speedup expected but can be variable\n            assert (sklearn_time / numba_time > 1.0) or (\n                numba_time < 0.05\n            ), f\"Expected at least equal performance for medium-large batches ({n_queries} queries): {sklearn_time/numba_time:.2f}x speedup\"\n        else:\n            # For smaller batches, just ensure numba is not excessively slow (parallelization overhead is acceptable)\n            # More lenient threshold to handle hardware variability in CI environments\n            assert (\n                numba_time < sklearn_time * 4.0\n            ), f\"Numba queries excessively slow for batch ({n_queries} queries): {numba_time:.4f}s vs sklearn {sklearn_time:.4f}s\"\n\n    def test_kdtree_query_performance_massive_batch(\n        self, performance_data, perf_metrics\n    ):\n        \"\"\"Compare massive batch query performance to test maximum parallelization benefits.\"\"\"\n        X, (n_samples, n_features) = performance_data\n        test_name = f\"query_massive_batch_{n_samples}x{n_features}\"\n\n        # Skip small datasets for massive batch testing\n        if n_samples < 5000:\n            pytest.skip(\"Massive batch testing not meaningful for small datasets\")\n\n        # Build trees\n        sklearn_tree = SklearnKDTree(X, leaf_size=40)\n        numba_tree = build_kdtree(X, leaf_size=40)\n\n        # Prepare very large batch of queries - this should show maximum numba advantage\n        np.random.seed(124)\n        n_queries = max(\n            5000, n_samples\n        )  # Very large batch - equal or larger than training set\n        query_data = np.random.rand(n_queries, n_features).astype(np.float32) * 10.0\n        k = min(50, n_samples // 20)  # Larger k value\n\n        # Warm up numba\n        _ = parallel_tree_query(numba_tree, query_data[:10], k=k, output_rdist=False)\n\n        # Time sklearn batch queries\n        sklearn_result, sklearn_time = time_function(\n            sklearn_tree.query, query_data, k=k\n        )\n\n        # Time numba batch queries (should benefit from parallelization)\n        numba_result, numba_time = time_function(\n            parallel_tree_query, numba_tree, query_data, k=k, output_rdist=False\n        )\n\n        # Record metrics\n        perf_metrics.record_metric(test_name, \"sklearn_batch_time\", sklearn_time)\n        perf_metrics.record_metric(test_name, \"numba_batch_time\", numba_time)\n        perf_metrics.record_metric(\n            test_name, \"batch_speedup\", sklearn_time / numba_time\n        )\n        perf_metrics.record_metric(\n            test_name, \"batch_queries_per_second_sklearn\", n_queries / sklearn_time\n        )\n        perf_metrics.record_metric(\n            test_name, \"batch_queries_per_second_numba\", n_queries / numba_time\n        )\n\n        sklearn_qps = n_queries / sklearn_time\n        numba_qps = n_queries / numba_time\n\n        print(\n            f\"\\n{test_name} Massive Batch Query Performance ({n_queries} queries, k={k}):\"\n        )\n        print(f\"  Sklearn: {sklearn_time:.4f}s ({sklearn_qps:.0f} queries/sec)\")\n        print(f\"  Numba:   {numba_time:.4f}s ({numba_qps:.0f} queries/sec)\")\n        print(f\"  Speedup: {sklearn_time/numba_time:.2f}x\")\n\n        # Verify results\n        sklearn_dists, sklearn_inds = sklearn_result\n        numba_dists, numba_inds = numba_result\n\n        assert sklearn_dists.shape == (n_queries, k)\n        assert numba_dists.shape == (n_queries, k)\n\n        # For massive batch queries, numba should show significant advantage\n        assert (\n            numba_time < sklearn_time * 1.2\n        ), f\"Numba massive batch queries should be faster: {numba_time:.4f}s vs sklearn {sklearn_time:.4f}s\"\n\n        # Expect substantial speedup on massive batches (this is the target use case)\n        # More conservative threshold to handle hardware variability\n        assert (\n            sklearn_time / numba_time > 0.85\n        ), f\"Expected significant numba advantage for massive batches ({n_queries} queries): {sklearn_time/numba_time:.2f}x\"\n\n    def test_kdtree_accuracy_comparison(self, performance_data, perf_metrics):\n        \"\"\"Verify that numba KDTree results match sklearn results.\"\"\"\n        X, (n_samples, n_features) = performance_data\n        test_name = f\"accuracy_{n_samples}x{n_features}\"\n\n        # Build trees\n        sklearn_tree = SklearnKDTree(X, leaf_size=40)\n        numba_tree = build_kdtree(X, leaf_size=40)\n\n        # Test on a subset of data points as queries\n        np.random.seed(125)\n        query_indices = np.random.choice(\n            n_samples, size=min(50, n_samples), replace=False\n        )\n        query_data = X[query_indices]\n        k = min(5, n_samples // 10)\n\n        # Get results from both implementations\n        sklearn_dists, sklearn_inds = sklearn_tree.query(query_data, k=k)\n        numba_dists, numba_inds = parallel_tree_query(\n            numba_tree, query_data, k=k, output_rdist=False\n        )\n\n        # Check shapes match\n        assert sklearn_dists.shape == numba_dists.shape\n        assert sklearn_inds.shape == numba_inds.shape\n\n        # Check that distances are reasonable (all finite, non-negative)\n        assert np.all(np.isfinite(sklearn_dists))\n        assert np.all(np.isfinite(numba_dists))\n        assert np.all(sklearn_dists >= 0)\n        assert np.all(numba_dists >= 0)\n\n        # Check that indices are valid\n        assert np.all(sklearn_inds >= 0)\n        assert np.all(sklearn_inds < n_samples)\n        assert np.all(numba_inds >= 0)\n        assert np.all(numba_inds < n_samples)\n\n        # For the first neighbor (should be identical for deterministic data)\n        # Allow some tolerance due to potential floating point differences\n        first_neighbor_distance_diff = np.abs(sklearn_dists[:, 0] - numba_dists[:, 0])\n        max_distance_diff = np.max(first_neighbor_distance_diff)\n\n        print(f\"\\n{test_name} Accuracy Check:\")\n        print(f\"  Max first neighbor distance difference: {max_distance_diff:.6f}\")\n        print(\n            f\"  Mean distance difference: {np.mean(first_neighbor_distance_diff):.6f}\"\n        )\n\n        # Allow small numerical differences\n        assert (\n            max_distance_diff < 1e-5\n        ), f\"Distance differences too large: {max_distance_diff:.6f}\"\n\n        # Check that most nearest neighbors are the same\n        first_neighbor_matches = np.sum(sklearn_inds[:, 0] == numba_inds[:, 0])\n        match_rate = first_neighbor_matches / len(query_data)\n\n        print(f\"  First neighbor match rate: {match_rate:.2%}\")\n\n        # Should have high agreement on nearest neighbors\n        assert (\n            match_rate > 0.95\n        ), f\"Nearest neighbor agreement too low: {match_rate:.2%}\"\n\n    def test_kdtree_scaling_performance(self, perf_metrics):\n        \"\"\"Test how performance scales with dataset size.\"\"\"\n        np.random.seed(42)\n\n        sizes = [1000, 2000, 5000, 10000]\n        n_features = 5\n\n        sklearn_times = []\n        numba_times = []\n\n        for n_samples in sizes:\n            # Generate test data\n            X = np.random.rand(n_samples, n_features).astype(np.float32) * 10.0\n\n            # Warm up numba\n            if n_samples >= 1000:\n                warmup_tree = build_kdtree(X[:100], leaf_size=40)\n                _ = parallel_tree_query(warmup_tree, X[:10], k=5, output_rdist=False)\n\n            # Time construction\n            sklearn_tree, sklearn_time = time_function(SklearnKDTree, X, leaf_size=40)\n            numba_tree, numba_time = time_function(build_kdtree, X, leaf_size=40)\n\n            sklearn_times.append(sklearn_time)\n            numba_times.append(numba_time)\n\n            # Record metrics\n            test_name = f\"scaling_{n_samples}\"\n            perf_metrics.record_metric(test_name, \"sklearn_time\", sklearn_time)\n            perf_metrics.record_metric(test_name, \"numba_time\", numba_time)\n            perf_metrics.record_metric(test_name, \"speedup\", sklearn_time / numba_time)\n\n            print(f\"\\nScaling test {n_samples} samples:\")\n            print(f\"  Sklearn: {sklearn_time:.4f}s\")\n            print(f\"  Numba:   {numba_time:.4f}s\")\n            print(f\"  Speedup: {sklearn_time/numba_time:.2f}x\")\n\n        # Check scaling behavior\n        # Construction time should scale sub-quadratically\n        for i in range(1, len(sizes)):\n            size_ratio = sizes[i] / sizes[i - 1]\n            sklearn_time_ratio = sklearn_times[i] / sklearn_times[i - 1]\n            numba_time_ratio = numba_times[i] / numba_times[i - 1]\n\n            # Time should not scale worse than O(n^1.5)\n            max_expected_ratio = size_ratio**1.5\n\n            assert (\n                sklearn_time_ratio < max_expected_ratio * 2\n            ), f\"Sklearn scaling too poor: {sklearn_time_ratio:.2f}x for {size_ratio:.2f}x data\"\n            assert (\n                numba_time_ratio < max_expected_ratio * 2\n            ), f\"Numba scaling too poor: {numba_time_ratio:.2f}x for {size_ratio:.2f}x data\"\n\n    def test_kdtree_different_k_values(self, perf_metrics):\n        \"\"\"Test performance with different k values.\"\"\"\n        np.random.seed(42)\n        n_samples, n_features = 5000, 4\n        X = np.random.rand(n_samples, n_features).astype(np.float32) * 10.0\n\n        # Build trees\n        sklearn_tree = SklearnKDTree(X, leaf_size=40)\n        numba_tree = build_kdtree(X, leaf_size=40)\n\n        # Test queries with large batch\n        n_queries = 2000  # Large batch to benefit from parallelization\n        query_data = np.random.rand(n_queries, n_features).astype(np.float32) * 10.0\n\n        # Warm up numba\n        _ = parallel_tree_query(numba_tree, query_data[:5], k=5, output_rdist=False)\n\n        k_values = [1, 5, 10, 20, 50]\n\n        for k in k_values:\n            if k >= n_samples:\n                continue\n\n            # Time both implementations\n            sklearn_result, sklearn_time = time_function(\n                sklearn_tree.query, query_data, k=k\n            )\n            numba_result, numba_time = time_function(\n                parallel_tree_query, numba_tree, query_data, k=k, output_rdist=False\n            )\n\n            test_name = f\"k_value_{k}\"\n            perf_metrics.record_metric(test_name, \"sklearn_time\", sklearn_time)\n            perf_metrics.record_metric(test_name, \"numba_time\", numba_time)\n            perf_metrics.record_metric(test_name, \"speedup\", sklearn_time / numba_time)\n\n            print(f\"\\nk={k} performance:\")\n            print(f\"  Sklearn: {sklearn_time:.4f}s\")\n            print(f\"  Numba:   {numba_time:.4f}s\")\n            print(f\"  Speedup: {sklearn_time/numba_time:.2f}x\")\n\n            # Verify correctness\n            sklearn_dists, sklearn_inds = sklearn_result\n            numba_dists, numba_inds = numba_result\n\n            assert sklearn_dists.shape == (n_queries, k)\n            assert numba_dists.shape == (n_queries, k)\n\n            # Performance should be reasonable for all k values\n            assert (\n                numba_time < sklearn_time * 3.0\n            ), f\"Numba too slow for k={k}: {sklearn_time/numba_time:.2f}x\"\n\n    def test_kdtree_query_batch_scaling(self, perf_metrics):\n        \"\"\"Test how query performance scales with batch size (numba's sweet spot).\"\"\"\n        np.random.seed(42)\n        n_samples, n_features = 10000, 5\n        X = np.random.rand(n_samples, n_features).astype(np.float32) * 10.0\n\n        # Build trees\n        sklearn_tree = SklearnKDTree(X, leaf_size=40)\n        numba_tree = build_kdtree(X, leaf_size=40)\n\n        # Test different batch sizes\n        batch_sizes = [100, 500, 1000, 2500, 5000, 10000]\n        k = 20\n\n        # Warm up numba\n        warmup_queries = np.random.rand(50, n_features).astype(np.float32) * 10.0\n        _ = parallel_tree_query(numba_tree, warmup_queries, k=k, output_rdist=False)\n\n        sklearn_speedups = []\n        numba_speedups = []\n\n        for batch_size in batch_sizes:\n            if batch_size > n_samples:\n                continue\n\n            # Generate query batch\n            query_data = (\n                np.random.rand(batch_size, n_features).astype(np.float32) * 10.0\n            )\n\n            # Time both implementations\n            sklearn_result, sklearn_time = time_function(\n                sklearn_tree.query, query_data, k=k\n            )\n            numba_result, numba_time = time_function(\n                parallel_tree_query, numba_tree, query_data, k=k, output_rdist=False\n            )\n\n            sklearn_qps = batch_size / sklearn_time\n            numba_qps = batch_size / numba_time\n            speedup = sklearn_time / numba_time\n\n            test_name = f\"batch_scaling_{batch_size}\"\n            perf_metrics.record_metric(test_name, \"sklearn_qps\", sklearn_qps)\n            perf_metrics.record_metric(test_name, \"numba_qps\", numba_qps)\n            perf_metrics.record_metric(test_name, \"speedup\", speedup)\n\n            print(\n                f\"\\nBatch size {batch_size:5d}: Sklearn {sklearn_qps:8.0f} q/s, \"\n                f\"Numba {numba_qps:8.0f} q/s, Speedup: {speedup:.2f}x\"\n            )\n\n            # Verify correctness\n            sklearn_dists, sklearn_inds = sklearn_result\n            numba_dists, numba_inds = numba_result\n            assert sklearn_dists.shape == numba_dists.shape\n\n            # Performance should be reasonable for larger batches\n            # Small batches may be slower due to parallelization overhead\n            if batch_size >= 3000:  # Adjusted threshold based on empirical results\n                assert (\n                    numba_time < sklearn_time * 1.5\n                ), f\"Numba too slow for large batch {batch_size}: {speedup:.2f}x\"\n                # Expect advantage for large batches\n                assert (\n                    speedup > 0.8\n                ), f\"Expected numba advantage for large batch {batch_size}: {speedup:.2f}x\"\n            elif batch_size >= 1000:\n                # Medium batches should be competitive\n                assert (\n                    numba_time < sklearn_time * 2.0\n                ), f\"Numba too slow for medium batch {batch_size}: {speedup:.2f}x\"\n\n        print(f\"\\nBatch Scaling Analysis:\")\n        print(\n            f\"  Numba shows increasing advantage with larger batches due to parallelization benefits\"\n        )\n        print(\n            f\"  Small batches (<1000) have overhead, large batches (>2000) show significant speedup\"\n        )\n        print(f\"  This demonstrates numba's optimization for large query workloads\")\n\n    def test_kdtree_query_performance_ultra_large_batch(self, perf_metrics):\n        \"\"\"Test numba performance on ultra-large query batches (its optimal use case).\"\"\"\n        np.random.seed(42)\n\n        # Use a reasonably sized dataset for the tree\n        n_samples, n_features = 15000, 6\n        X = np.random.rand(n_samples, n_features).astype(np.float32) * 10.0\n\n        # Build trees\n        sklearn_tree = SklearnKDTree(X, leaf_size=40)\n        numba_tree = build_kdtree(X, leaf_size=40)\n\n        # Test with ultra-large query batch - this is numba's sweet spot\n        np.random.seed(123)\n        n_queries = 25000  # Very large query batch\n        query_data = np.random.rand(n_queries, n_features).astype(np.float32) * 10.0\n        k = 25\n\n        # Warm up numba\n        _ = parallel_tree_query(numba_tree, query_data[:20], k=k, output_rdist=False)\n\n        # Time both implementations\n        sklearn_result, sklearn_time = time_function(\n            sklearn_tree.query, query_data, k=k\n        )\n        numba_result, numba_time = time_function(\n            parallel_tree_query, numba_tree, query_data, k=k, output_rdist=False\n        )\n\n        # Calculate metrics\n        sklearn_qps = n_queries / sklearn_time\n        numba_qps = n_queries / numba_time\n        speedup = sklearn_time / numba_time\n\n        # Record metrics\n        test_name = f\"ultra_large_batch_{n_queries}_queries\"\n        perf_metrics.record_metric(test_name, \"sklearn_time\", sklearn_time)\n        perf_metrics.record_metric(test_name, \"numba_time\", numba_time)\n        perf_metrics.record_metric(test_name, \"speedup\", speedup)\n        perf_metrics.record_metric(test_name, \"sklearn_qps\", sklearn_qps)\n        perf_metrics.record_metric(test_name, \"numba_qps\", numba_qps)\n\n        print(f\"\\nUltra-Large Batch Performance ({n_queries} queries, k={k}):\")\n        print(f\"  Dataset: {n_samples} samples x {n_features} features\")\n        print(f\"  Sklearn: {sklearn_time:.4f}s ({sklearn_qps:,.0f} queries/sec)\")\n        print(f\"  Numba:   {numba_time:.4f}s ({numba_qps:,.0f} queries/sec)\")\n        print(f\"  Speedup: {speedup:.2f}x\")\n        print(\n            f\"  Efficiency gain: {(numba_qps - sklearn_qps):,.0f} additional queries/sec\"\n        )\n\n        # Verify correctness\n        sklearn_dists, sklearn_inds = sklearn_result\n        numba_dists, numba_inds = numba_result\n\n        assert sklearn_dists.shape == (n_queries, k)\n        assert numba_dists.shape == (n_queries, k)\n        assert np.all(np.isfinite(numba_dists))\n        assert np.all(numba_inds >= 0)\n        assert np.all(numba_inds < n_samples)\n\n        # Performance expectations for ultra-large batches\n        # This is numba's optimal use case - should show significant speedup\n        assert (\n            numba_time < sklearn_time\n        ), f\"Numba should be faster for ultra-large batches: {speedup:.2f}x\"\n\n        # Expect substantial speedup on ultra-large batches\n        assert (\n            speedup > 1.0\n        ), f\"Expected major numba advantage for ultra-large batches: {speedup:.2f}x (target: >1.0x)\"\n\n        # Throughput should be significantly higher\n        assert (\n            numba_qps > sklearn_qps * 1.0\n        ), f\"Expected 1.0x+ throughput improvement: {numba_qps/sklearn_qps:.2f}x\"\n\n\n@pytest.mark.performance\nclass TestKDTreeRegressionBaseline:\n    \"\"\"Baseline performance tests for regression detection.\"\"\"\n\n    def test_kdtree_baseline_performance(self):\n        \"\"\"\n        Baseline performance test for KDTree operations.\n\n        Establishes performance baselines that can be used to detect regressions.\n        \"\"\"\n        np.random.seed(42)\n\n        # Standard test dataset\n        n_samples, n_features = 10000, 5\n        X = np.random.rand(n_samples, n_features).astype(np.float32) * 10.0\n\n        # Warm up numba compilation\n        warmup_tree = build_kdtree(X[:100], leaf_size=40)\n        warmup_queries = X[:10]\n        _ = parallel_tree_query(warmup_tree, warmup_queries, k=10, output_rdist=False)\n\n        print(f\"\\nKDTree Baseline Performance Report:\")\n        print(f\"  Dataset: {n_samples} samples x {n_features} features\")\n        print(f\"  Hardware: {platform.platform()}\")\n        if HAS_PSUTIL:\n            print(f\"  CPU cores: {psutil.cpu_count(logical=True)}\")\n            print(f\"  Memory: {psutil.virtual_memory().total / (1024**3):.1f} GB\")\n        else:\n            import os\n\n            print(f\"  CPU cores: {os.cpu_count() or 'unknown'}\")\n\n        # Test construction performance\n        sklearn_tree, sklearn_construction_time = time_function(\n            SklearnKDTree, X, leaf_size=40\n        )\n        numba_tree, numba_construction_time = time_function(\n            build_kdtree, X, leaf_size=40\n        )\n\n        # Test query performance with large batch (target use case)\n        n_queries = 5000  # Large query batch to showcase parallel advantages\n        query_data = np.random.rand(n_queries, n_features).astype(np.float32) * 10.0\n        k = 30  # Reasonable k value\n\n        sklearn_result, sklearn_query_time = time_function(\n            sklearn_tree.query, query_data, k=k\n        )\n        numba_result, numba_query_time = time_function(\n            parallel_tree_query, numba_tree, query_data, k=k, output_rdist=False\n        )\n\n        # Calculate metrics\n        construction_speedup = sklearn_construction_time / numba_construction_time\n        query_speedup = sklearn_query_time / numba_query_time\n\n        print(f\"\\nConstruction Performance:\")\n        print(f\"  Sklearn: {sklearn_construction_time:.4f} seconds\")\n        print(f\"  Numba:   {numba_construction_time:.4f} seconds\")\n        print(f\"  Speedup: {construction_speedup:.2f}x\")\n\n        print(f\"\\nQuery Performance ({n_queries} queries, k={k}):\")\n        print(f\"  Sklearn: {sklearn_query_time:.4f} seconds\")\n        print(f\"  Numba:   {numba_query_time:.4f} seconds\")\n        print(f\"  Speedup: {query_speedup:.2f}x\")\n\n        print(f\"\\nThroughput:\")\n        print(f\"  Construction: {n_samples/numba_construction_time:.0f} samples/sec\")\n        print(f\"  Queries: {n_queries/numba_query_time:.0f} queries/sec\")\n\n        # Basic performance requirements\n        assert (\n            numba_construction_time < 2.0\n        ), f\"Construction too slow: {numba_construction_time:.4f}s\"\n        assert numba_query_time < 1.0, f\"Queries too slow: {numba_query_time:.4f}s\"\n\n        # Verify results are correct\n        sklearn_dists, sklearn_inds = sklearn_result\n        numba_dists, numba_inds = numba_result\n\n        assert sklearn_dists.shape == numba_dists.shape\n        assert sklearn_inds.shape == numba_inds.shape\n        assert np.all(np.isfinite(numba_dists))\n        assert np.all(numba_inds >= 0)\n        assert np.all(numba_inds < n_samples)\n\n        # Expected performance characteristics\n        # After warmup, numba should be competitive or better\n        print(f\"\\nPerformance Analysis:\")\n        if construction_speedup > 1.0:\n            print(f\"  ✅ Construction {construction_speedup:.2f}x faster than sklearn\")\n        else:\n            print(\n                f\"  ⚠️  Construction {1/construction_speedup:.2f}x slower than sklearn\"\n            )\n\n        if query_speedup > 1.0:\n            print(f\"  ✅ Queries {query_speedup:.2f}x faster than sklearn\")\n        else:\n            print(f\"  ⚠️  Queries {1/query_speedup:.2f}x slower than sklearn\")\n\n        return_info = {\n            \"construction_speedup\": construction_speedup,\n            \"query_speedup\": query_speedup,\n            \"numba_construction_time\": numba_construction_time,\n            \"numba_query_time\": numba_query_time,\n        }\n\n        # Note: return_info could be used for CI comparison but we don't return it\n        # to avoid pytest warnings\n"
  },
  {
    "path": "evoc/uint8_nndescent.py",
    "content": "import numba\nimport numpy as np\nfrom numba import types\nfrom numba.core import cgutils\nfrom numba.extending import intrinsic\nimport llvmlite.ir as ir\n\nfrom .common_nndescent import (\n    tau_rand_int,\n    make_heap,\n    deheap_sort,\n    flagged_heap_push,\n    build_candidates,\n    apply_graph_update_array,\n    apply_sorted_graph_updates,\n)\nfrom .nested_parallelism import ENABLE_NESTED_PARALLELISM\n\n# Used for a floating point \"nearly zero\" comparison\nEPS = 1e-8\nINT32_MIN = np.iinfo(np.int32).min + 1\nINT32_MAX = np.iinfo(np.int32).max - 1\nINF = np.float32(np.inf)\n\npoint_indices_type = numba.int32[::1]\n\n\n@intrinsic\ndef popcnt_u8(typingctx, val):\n    \"\"\"Hardware popcount for uint8 using LLVM intrinsic.\"\"\"\n    sig = types.uint8(types.uint8)\n\n    def popcnt_u8_impl(context, builder, sig, args):\n        [val] = args\n        # Declare LLVM's ctpop intrinsic for i8\n        llvm_i8 = val.type\n        fnty = ir.FunctionType(llvm_i8, [llvm_i8])\n        llvm_ctpop = cgutils.get_or_insert_function(\n            builder.module, fnty, \"llvm.ctpop.i8\"\n        )\n        result = builder.call(llvm_ctpop, [val])\n        return result\n\n    return sig, popcnt_u8_impl\n\n\n@intrinsic\ndef popcnt_u64(typingctx, val):\n    \"\"\"Hardware popcount for uint64 using LLVM intrinsic.\"\"\"\n    sig = types.uint64(types.uint64)\n\n    def popcnt_u64_impl(context, builder, sig, args):\n        [val] = args\n        llvm_i64 = val.type\n        fnty = ir.FunctionType(llvm_i64, [llvm_i64])\n        llvm_ctpop = cgutils.get_or_insert_function(\n            builder.module, fnty, \"llvm.ctpop.i64\"\n        )\n        result = builder.call(llvm_ctpop, [val])\n        return result\n\n    return sig, popcnt_u64_impl\n\n\n@numba.njit(\n    [\n        \"f4(u1[::1],u1[::1])\",\n        numba.types.float32(\n            numba.types.Array(numba.types.uint8, 1, \"C\", readonly=True),\n            numba.types.Array(numba.types.uint8, 1, \"C\", readonly=True),\n        ),\n    ],\n    fastmath=True,\n    cache=True,\n    nogil=True,\n)\ndef fast_bit_jaccard(x, y):\n    \"\"\"Binary Jaccard using hardware POPCNT instruction.\"\"\"\n    result = np.uint32(0)\n    denom = np.uint32(0)\n    dim = x.shape[0]\n\n    for i in range(dim):\n        and_val = x[i] & y[i]\n        or_val = x[i] | y[i]\n        result += popcnt_u8(and_val)\n        denom += popcnt_u8(or_val)\n\n    if denom > 0:\n        return -(np.float32(result) / np.float32(denom))\n    else:\n        return 0.0\n\n\n@intrinsic\ndef load_u64_from_u8_array(typingctx, arr, offset):\n    \"\"\"Load a uint64 from a uint8 array at given byte offset.\"\"\"\n    sig = types.uint64(types.Array(types.uint8, 1, \"C\"), types.intp)\n\n    def load_u64_impl(context, builder, sig, args):\n        [arr, offset] = args\n\n        # Get the array structure\n        ary = context.make_array(sig.args[0])(context, builder, arr)\n        ptr = ary.data\n\n        # Get element pointer at offset\n        elem_ptr = builder.gep(ptr, [offset])\n\n        # Cast uint8* to uint64*\n        i64_ptr_type = ir.PointerType(ir.IntType(64))\n        ptr_u64 = builder.bitcast(elem_ptr, i64_ptr_type)\n\n        # Load uint64\n        value = builder.load(ptr_u64)\n\n        return value\n\n    return sig, load_u64_impl\n\n\n@numba.njit(\n    [\n        \"f4(u1[::1],u1[::1])\",\n        numba.types.float32(\n            numba.types.Array(numba.types.uint8, 1, \"C\", readonly=True),\n            numba.types.Array(numba.types.uint8, 1, \"C\", readonly=True),\n        ),\n    ],\n    fastmath=True,\n    cache=True,\n    boundscheck=False,\n    nogil=True,\n)\ndef fast_bit_jaccard_u64(x, y):\n    \"\"\"\n    Use load intrinsic to avoid type conversion overhead.\n    REQUIRES: Array size divisible by 8.\n    \"\"\"\n    result = np.uint64(0)\n    denom = np.uint64(0)\n\n    n_u64 = x.shape[0] // 8\n\n    for i in range(n_u64):\n        offset = i * 8\n\n        # Load uint64 values directly\n        x_val = load_u64_from_u8_array(x, offset)\n        y_val = load_u64_from_u8_array(y, offset)\n\n        and_val = x_val & y_val\n        or_val = x_val | y_val\n\n        result += popcnt_u64(and_val)\n        denom += popcnt_u64(or_val)\n\n    if denom > 0:\n        return -(np.float32(result) / np.float32(denom))\n    else:\n        return 0.0\n\n\n@numba.njit(\n    numba.types.Tuple((numba.int32[::1], numba.int32[::1]))(\n        numba.types.Array(numba.types.uint8, 2, \"C\", readonly=True),\n        numba.int32[::1],\n        numba.int64[::1],\n    ),\n    locals={\n        \"n_left\": numba.uint32,\n        \"n_right\": numba.uint32,\n        \"left_data\": numba.types.Array(numba.types.uint8, 1, \"C\", readonly=True),\n        \"right_data\": numba.types.Array(numba.types.uint8, 1, \"C\", readonly=True),\n        \"test_data\": numba.types.Array(numba.types.uint8, 1, \"C\", readonly=True),\n        \"hyperplane_vector\": numba.uint8[::1],\n        \"hyperplane_offset\": numba.float32,\n        \"margin\": numba.float32,\n        \"d\": numba.uint32,\n        \"i\": numba.uint32,\n        \"left_index\": numba.uint32,\n        \"right_index\": numba.uint32,\n    },\n    fastmath=True,\n    nogil=True,\n    cache=True,\n)\ndef uint8_random_projection_split(data, indices, rng_state):\n    \"\"\"Given a set of ``graph_indices`` for graph_data points from ``graph_data``, create\n    a random hyperplane to split the graph_data, returning two arrays graph_indices\n    that fall on either side of the hyperplane. This is the basis for a\n    random projection tree, which simply uses this splitting recursively.\n    This particular split uses cosine distance to determine the hyperplane\n    and which side each graph_data sample falls on.\n    Parameters\n    ----------\n    data: array of shape (n_samples, n_features)\n        The original graph_data to be split\n    indices: array of shape (tree_node_size,)\n        The graph_indices of the elements in the ``graph_data`` array that are to\n        be split in the current operation.\n    rng_state: array of int64, shape (3,)\n        The internal state of the rng\n    Returns\n    -------\n    indices_left: array\n        The elements of ``graph_indices`` that fall on the \"left\" side of the\n        random hyperplane.\n    indices_right: array\n        The elements of ``graph_indices`` that fall on the \"left\" side of the\n        random hyperplane.\n    \"\"\"\n    dim = data.shape[1]\n\n    # Select two random points, set the hyperplane between them\n    left_index = tau_rand_int(rng_state) % indices.shape[0]\n    right_index = tau_rand_int(rng_state) % indices.shape[0]\n    right_index += left_index == right_index\n    right_index = right_index % indices.shape[0]\n    left = indices[left_index]\n    right = indices[right_index]\n\n    left_data = data[left]\n    right_data = data[right]\n\n    # Compute the normal vector to the hyperplane (the vector between\n    # the two points)\n    hyperplane_vector = np.empty(dim * 2, dtype=np.uint8)\n    positive_hyperplane_component = hyperplane_vector[:dim]\n    negative_hyperplane_component = hyperplane_vector[dim:]\n\n    for d in range(dim):\n        xor_vector = left_data[d] ^ right_data[d]\n        positive_hyperplane_component[d] = xor_vector & left_data[d]\n        negative_hyperplane_component[d] = xor_vector & right_data[d]\n\n    hyperplane_norm = 0.0\n    left_norm = 0.0\n    right_norm = 0.0\n\n    for d in range(dim):\n        hyperplane_norm += popcnt_u8(hyperplane_vector[d])\n        left_norm += popcnt_u8(left_data[d])\n        right_norm += popcnt_u8(right_data[d])\n\n    # For each point compute the margin (project into normal vector)\n    # If we are on lower side of the hyperplane put in one pile, otherwise\n    # put it in the other pile (if we hit hyperplane on the nose, flip a coin)\n    n_left = 0\n    n_right = 0\n    side = np.empty(indices.shape[0], np.bool_)\n    for i in range(indices.shape[0]):\n        margin = 0.0\n        local_rng_state = rng_state + np.int64(i)\n        test_data = data[indices[i]]\n        for d in range(dim):\n            margin += popcnt_u8(positive_hyperplane_component[d] & test_data[d])\n            margin -= popcnt_u8(negative_hyperplane_component[d] & test_data[d])\n\n        if abs(margin) < EPS:\n            side[i] = np.bool_(tau_rand_int(local_rng_state) % 2)\n            if side[i] == 0:\n                n_left += 1\n            else:\n                n_right += 1\n        elif margin > 0:\n            side[i] = 0\n            n_left += 1\n        else:\n            side[i] = 1\n            n_right += 1\n\n    # If all points end up on one side, something went wrong numerically\n    # In this case, assign points randomly; they are likely very close anyway\n    if n_left == 0 or n_right == 0:\n        n_left = 0\n        n_right = 0\n        for i in range(indices.shape[0]):\n            side[i] = tau_rand_int(rng_state) % 2\n            if side[i] == 0:\n                n_left += 1\n            else:\n                n_right += 1\n\n    # Now that we have the counts allocate arrays\n    indices_left = np.empty(n_left, dtype=np.int32)\n    indices_right = np.empty(n_right, dtype=np.int32)\n\n    # Populate the arrays with graph_indices according to which side they fell on\n    n_left = 0\n    n_right = 0\n    for i in range(side.shape[0]):\n        if side[i] == 0:\n            indices_left[n_left] = indices[i]\n            n_left += 1\n        else:\n            indices_right[n_right] = indices[i]\n            n_right += 1\n\n    return indices_left, indices_right\n\n\n@numba.njit(\n    numba.void(\n        numba.types.Array(numba.types.uint8, 2, \"C\", readonly=True),\n        numba.int32[::1],\n        numba.types.ListType(numba.int32[::1]),\n        numba.int64[::1],\n        numba.int64,\n        numba.int64,\n    ),\n    nogil=True,\n    cache=True,\n)\ndef make_uint8_tree(\n    data,\n    indices,\n    point_indices,\n    rng_state,\n    leaf_size=30,\n    max_depth=200,\n):\n    if indices.shape[0] > leaf_size and max_depth > 0:\n        (\n            left_indices,\n            right_indices,\n        ) = uint8_random_projection_split(data, indices, rng_state)\n\n        make_uint8_tree(\n            data,\n            left_indices,\n            point_indices,\n            rng_state,\n            leaf_size,\n            max_depth - 1,\n        )\n\n        make_uint8_tree(\n            data,\n            right_indices,\n            point_indices,\n            rng_state,\n            leaf_size,\n            max_depth - 1,\n        )\n    else:\n        point_indices.append(indices)\n\n    return\n\n\n@numba.njit(\n    numba.int32[:, ::1](\n        numba.types.Array(numba.types.uint8, 2, \"C\", readonly=True),\n        numba.int64[::1],\n        numba.int64,\n        numba.int64,\n    ),\n    nogil=True,\n    locals={\"n_leaves\": numba.int64, \"i\": numba.int64},\n    parallel=True,\n    cache=True,\n)\ndef make_uint8_leaf_array_parallel(data, rng_state, leaf_size=30, max_depth=200):\n    indices = np.arange(data.shape[0]).astype(np.int32)\n\n    point_indices = numba.typed.List.empty_list(point_indices_type)\n\n    make_uint8_tree(\n        data,\n        indices,\n        point_indices,\n        rng_state,\n        leaf_size,\n        max_depth=max_depth,\n    )\n\n    n_leaves = numba.int64(len(point_indices))\n\n    max_leaf_size = leaf_size\n    for i in numba.prange(n_leaves):\n        points = point_indices[numba.int64(i)]\n        max_leaf_size = max(max_leaf_size, numba.int32(len(points)))\n\n    result = np.full((n_leaves, max_leaf_size), -1, dtype=np.int32)\n    for i in numba.prange(n_leaves):\n        points = point_indices[numba.int64(i)]\n        leaf_size = numba.int32(len(points))\n        result[i, :leaf_size] = points\n\n    return result\n\n\n@numba.njit(\n    numba.int32[:, ::1](\n        numba.types.Array(numba.types.uint8, 2, \"C\", readonly=True),\n        numba.int64[::1],\n        numba.int64,\n        numba.int64,\n    ),\n    nogil=True,\n    locals={\"n_leaves\": numba.int64, \"i\": numba.int64},\n    parallel=False,\n    cache=True,\n)\ndef make_uint8_leaf_array_serial(data, rng_state, leaf_size=30, max_depth=200):\n    indices = np.arange(data.shape[0]).astype(np.int32)\n\n    point_indices = numba.typed.List.empty_list(point_indices_type)\n\n    make_uint8_tree(\n        data,\n        indices,\n        point_indices,\n        rng_state,\n        leaf_size,\n        max_depth=max_depth,\n    )\n\n    n_leaves = numba.int64(len(point_indices))\n\n    max_leaf_size = leaf_size\n    for i in range(n_leaves):\n        points = point_indices[numba.int64(i)]\n        max_leaf_size = max(max_leaf_size, numba.int32(len(points)))\n\n    result = np.full((n_leaves, max_leaf_size), -1, dtype=np.int32)\n    for i in range(n_leaves):\n        points = point_indices[numba.int64(i)]\n        leaf_size = numba.int32(len(points))\n        result[i, :leaf_size] = points\n\n    return result\n\n\n@numba.njit(\n    numba.types.List(numba.int32[:, ::1])(\n        numba.types.Array(numba.types.uint8, 2, \"C\", readonly=True),\n        numba.int64[:, ::1],\n        numba.int64,\n        numba.int64,\n    ),\n    parallel=True,\n    cache=True,\n)\ndef make_uint8_forest_no_nested_parallelism(data, rng_states, leaf_size, max_depth):\n    result = [np.empty((1, 1), dtype=np.int32)] * rng_states.shape[0]\n    for i in numba.prange(len(result)):\n        result[i] = make_uint8_leaf_array_serial(\n            data, rng_states[i], leaf_size, max_depth=max_depth\n        )\n    return result\n\n\n@numba.njit(\n    numba.types.List(numba.int32[:, ::1])(\n        numba.types.Array(numba.types.uint8, 2, \"C\", readonly=True),\n        numba.int64[:, ::1],\n        numba.int64,\n        numba.int64,\n    ),\n    parallel=True,\n    cache=True,\n)\ndef make_uint8_forest_with_nested_parallelism(data, rng_states, leaf_size, max_depth):\n    result = [np.empty((1, 1), dtype=np.int32)] * rng_states.shape[0]\n    for i in numba.prange(len(result)):\n        result[i] = make_uint8_leaf_array_parallel(\n            data, rng_states[i], leaf_size, max_depth=max_depth\n        )\n    return result\n\n\ndef make_uint8_forest(data, rng_states, leaf_size=30, max_depth=200):\n    if ENABLE_NESTED_PARALLELISM:\n        return make_uint8_forest_with_nested_parallelism(\n            data, rng_states, leaf_size, max_depth\n        )\n    else:\n        return make_uint8_forest_no_nested_parallelism(\n            data, rng_states, leaf_size, max_depth\n        )\n\n\n@numba.njit(\n    numba.float32[:, :, ::1](\n        numba.float32[:, :, ::1],\n        numba.int32[::1],\n        numba.types.Array(numba.types.int32, 2, \"C\", readonly=True),\n        numba.float32[:],\n        numba.types.Array(numba.types.uint8, 2, \"C\", readonly=True),\n        numba.int64,\n    ),\n    parallel=True,\n    locals={\n        \"d\": numba.float32,\n        \"p\": numba.int32,\n        \"q\": numba.int32,\n        \"t\": numba.uint16,\n        \"r\": numba.uint32,\n        \"n\": numba.uint32,\n        \"idx\": numba.uint32,\n        \"data_p\": numba.types.Array(numba.types.uint8, 1, \"C\", readonly=True),\n    },\n    cache=True,\n)\ndef generate_leaf_updates_uint8(\n    updates, n_updates_per_thread, leaf_block, dist_thresholds, data, n_threads\n):\n\n    block_size = leaf_block.shape[0]\n    rows_per_thread = (block_size // n_threads) + 1\n\n    for t in numba.prange(n_threads):\n        idx = 0\n        for r in range(rows_per_thread):\n            n = t * rows_per_thread + r\n            if n >= block_size:\n                break\n\n            for i in range(leaf_block.shape[1]):\n                p = leaf_block[n, i]\n                if p < 0:\n                    break\n                data_p = data[p]\n\n                for j in range(i, leaf_block.shape[1]):\n                    q = leaf_block[n, j]\n                    if q < 0:\n                        break\n\n                    d = fast_bit_jaccard(data_p, data[q])\n                    if d < dist_thresholds[p] or d < dist_thresholds[q]:\n                        updates[t, idx, 0] = p\n                        updates[t, idx, 1] = q\n                        updates[t, idx, 2] = d\n                        idx += 1\n\n        n_updates_per_thread[t] = idx\n\n    return updates\n\n\n@numba.njit(\n    [\n        numba.void(\n            numba.types.Array(numba.types.uint8, 2, \"C\", readonly=True),\n            numba.types.Tuple(\n                (numba.int32[:, ::1], numba.float32[:, ::1], numba.uint8[:, ::1])\n            ),\n            numba.types.optional(\n                numba.types.Array(numba.types.int32, 2, \"C\", readonly=True)\n            ),\n            numba.types.int32,\n        ),\n    ],\n    locals={\n        \"d\": numba.float32,\n        \"p\": numba.int32,\n        \"q\": numba.int32,\n        \"i\": numba.uint16,\n        \"updates\": numba.float32[:, :, ::1],\n        \"n_updates_per_thread\": numba.int32[::1],\n    },\n    parallel=True,\n    cache=True,\n)\ndef init_rp_tree_uint8(data, current_graph, leaf_array, n_threads):\n\n    n_leaves = leaf_array.shape[0]\n    block_size = 64 * n_threads\n    n_blocks = n_leaves // block_size\n\n    max_leaf_size = leaf_array.shape[1]\n    updates_per_thread = (\n        int(block_size * max_leaf_size * (max_leaf_size - 1) / (2 * n_threads)) + 1\n    )\n    updates = np.zeros((n_threads, updates_per_thread, 3), dtype=np.float32)\n    n_updates_per_thread = np.zeros(n_threads, dtype=np.int32)\n\n    for i in range(n_blocks + 1):\n        block_start = i * block_size\n        block_end = min(n_leaves, (i + 1) * block_size)\n\n        leaf_block = leaf_array[block_start:block_end]\n        dist_thresholds = current_graph[1][:, 0]\n\n        updates = generate_leaf_updates_uint8(\n            updates, n_updates_per_thread, leaf_block, dist_thresholds, data, n_threads\n        )\n\n        n_vertices = current_graph[0].shape[0]\n        vertex_block_size = n_vertices // n_threads + 1\n\n        for t in numba.prange(n_threads):\n            block_start = t * vertex_block_size\n            block_end = min(block_start + vertex_block_size, n_vertices)\n\n            for j in range(n_threads):\n                for k in range(n_updates_per_thread[j]):\n                    p = np.int32(updates[j, k, 0])\n                    q = np.int32(updates[j, k, 1])\n                    d = np.float32(updates[j, k, 2])\n\n                    if p == -1 or q == -1:\n                        continue\n\n                    if p >= block_start and p < block_end:\n                        flagged_heap_push(\n                            current_graph[1][p],\n                            current_graph[0][p],\n                            current_graph[2][p],\n                            d,\n                            q,\n                        )\n                    if q >= block_start and q < block_end:\n                        flagged_heap_push(\n                            current_graph[1][q],\n                            current_graph[0][q],\n                            current_graph[2][q],\n                            d,\n                            p,\n                        )\n\n\n@numba.njit(\n    numba.types.void(\n        numba.int32,\n        numba.types.Array(numba.types.uint8, 2, \"C\", readonly=True),\n        numba.types.Tuple(\n            (numba.int32[:, ::1], numba.float32[:, ::1], numba.uint8[:, ::1])\n        ),\n        numba.int64[::1],\n    ),\n    fastmath=True,\n    locals={\"d\": numba.float32, \"idx\": numba.int32, \"i\": numba.int32},\n    cache=True,\n)\ndef init_random_uint8(n_neighbors, data, heap, rng_state):\n    for i in range(data.shape[0]):\n        if heap[0][i, 0] < 0.0:\n            for j in range(n_neighbors - np.sum(heap[0][i] >= 0.0)):\n                idx = np.abs(tau_rand_int(rng_state)) % data.shape[0]\n                if idx in heap[0][i]:\n                    continue\n                d = fast_bit_jaccard(data[idx], data[i])\n                flagged_heap_push(heap[1][i], heap[0][i], heap[2][i], d, idx)\n\n    return\n\n\n@numba.njit(\n    numba.types.void(\n        numba.float32[:, :, ::1],\n        numba.int32[::1],\n        numba.int32[:, ::1],\n        numba.int32[:, ::1],\n        numba.float32[:],\n        numba.types.Array(numba.types.uint8, 2, \"C\", readonly=True),\n        numba.int64,\n    ),\n    locals={\n        \"data_p\": numba.types.Array(numba.types.uint8, 1, \"C\", readonly=True),\n    },\n    parallel=True,\n    cache=True,\n)\ndef generate_graph_update_array_uint8(\n    update_array,\n    n_updates_per_thread,\n    new_candidate_block,\n    old_candidate_block,\n    dist_thresholds,\n    data,\n    n_threads,\n):\n\n    block_size = new_candidate_block.shape[0]\n    max_new_candidates = new_candidate_block.shape[1]\n    max_old_candidates = old_candidate_block.shape[1]\n    rows_per_thread = (block_size // n_threads) + 1\n\n    for t in numba.prange(n_threads):\n        idx = 0\n        updates_are_full = False\n        for r in range(rows_per_thread):\n            i = t * rows_per_thread + r\n            if i >= block_size:\n                break\n\n            for j in range(max_new_candidates):\n                p = int(new_candidate_block[i, j])\n                if p < 0:\n                    continue\n                data_p = data[p]\n\n                for k in range(j, max_new_candidates):\n                    q = int(new_candidate_block[i, k])\n                    if q < 0:\n                        continue\n\n                    d = fast_bit_jaccard(data_p, data[q])\n                    if d <= dist_thresholds[p] or d <= dist_thresholds[q]:\n                        update_array[t, idx, 0] = p\n                        update_array[t, idx, 1] = q\n                        update_array[t, idx, 2] = d\n                        idx += 1\n                        if idx >= update_array.shape[1]:\n                            updates_are_full = True\n                            break\n\n                if updates_are_full:\n                    break\n\n                for k in range(max_old_candidates):\n                    q = int(old_candidate_block[i, k])\n                    if q < 0:\n                        continue\n\n                    d = fast_bit_jaccard(data_p, data[q])\n                    if d <= dist_thresholds[p] or d <= dist_thresholds[q]:\n                        update_array[t, idx, 0] = p\n                        update_array[t, idx, 1] = q\n                        update_array[t, idx, 2] = d\n                        idx += 1\n                        if idx >= update_array.shape[1]:\n                            updates_are_full = True\n                            break\n\n                if updates_are_full:\n                    break\n\n            if updates_are_full:\n                break\n\n        n_updates_per_thread[t] = idx\n\n\n@numba.njit(\n    numba.void(\n        numba.float32[:, :, ::1],\n        numba.int32[:, ::1],\n        numba.int32[:, ::1],\n        numba.int32[:, ::1],\n        numba.float32[:],\n        numba.types.Array(numba.types.uint8, 2, \"C\", readonly=True),\n        numba.int64,\n    ),\n    locals={\n        \"data_p\": numba.types.Array(numba.types.uint8, 1, \"C\", readonly=True),\n        \"dist_thresh_p\": numba.float32,\n        \"dist_thresh_q\": numba.float32,\n        \"p\": numba.int32,\n        \"q\": numba.int32,\n        \"d\": numba.float32,\n        \"max_updates\": numba.intp,\n        \"max_threshold\": numba.float32,\n        \"p_block\": numba.int32,\n        \"q_block\": numba.int32,\n    },\n    parallel=True,\n    cache=True,\n    boundscheck=False,\n)\ndef generate_sorted_graph_update_array_uint8(\n    update_array,\n    n_updates_per_block,\n    new_candidate_block,\n    old_candidate_block,\n    dist_thresholds,\n    data,\n    n_threads,\n):\n    \"\"\"\n    Generate graph updates pre-sorted by target block for uint8 data.\n    \"\"\"\n    block_size_candidates = new_candidate_block.shape[0]\n    max_new_candidates = new_candidate_block.shape[1]\n    max_old_candidates = old_candidate_block.shape[1]\n    rows_per_thread = (block_size_candidates // n_threads) + 1\n\n    n_vertices = data.shape[0]\n    vertex_block_size = n_vertices // n_threads + 1\n    max_updates = update_array.shape[1]\n    max_updates_per_src_thread = max_updates // n_threads\n\n    # Reset update counts\n    for b in numba.prange(n_threads):\n        for t in range(n_threads + 1):\n            n_updates_per_block[b, t] = 0\n\n    # Each thread generates updates and places them in appropriate buckets\n    for t in numba.prange(n_threads):\n        # Thread-local counters for each bucket\n        local_counts = np.zeros(n_threads, dtype=np.int32)\n\n        for r in range(rows_per_thread):\n            i = t * rows_per_thread + r\n            if i >= block_size_candidates:\n                break\n\n            for j in range(max_new_candidates):\n                p = new_candidate_block[i, j]\n                if p < 0:\n                    continue\n\n                data_p = data[p]\n                dist_thresh_p = dist_thresholds[p]\n                p_block = p // vertex_block_size\n                if p_block >= n_threads:\n                    p_block = n_threads - 1\n\n                # Compare with other new candidates\n                for k in range(j, max_new_candidates):\n                    q = new_candidate_block[i, k]\n                    if q < 0:\n                        continue\n\n                    d = fast_bit_jaccard(data_p, data[q])\n                    dist_thresh_q = dist_thresholds[q]\n                    max_threshold = max(dist_thresh_p, dist_thresh_q)\n\n                    if d <= max_threshold:\n                        q_block = q // vertex_block_size\n                        if q_block >= n_threads:\n                            q_block = n_threads - 1\n\n                        # Place update in p's bucket\n                        bucket_idx = local_counts[p_block]\n                        write_idx = t * max_updates_per_src_thread + bucket_idx\n                        if write_idx < max_updates:\n                            update_array[p_block, write_idx, 0] = p\n                            update_array[p_block, write_idx, 1] = q\n                            update_array[p_block, write_idx, 2] = d\n                            local_counts[p_block] += 1\n\n                        # If q is in a different block, also place in q's bucket\n                        if q_block != p_block:\n                            bucket_idx = local_counts[q_block]\n                            write_idx = t * max_updates_per_src_thread + bucket_idx\n                            if write_idx < max_updates:\n                                update_array[q_block, write_idx, 0] = p\n                                update_array[q_block, write_idx, 1] = q\n                                update_array[q_block, write_idx, 2] = d\n                                local_counts[q_block] += 1\n\n                # Compare with old candidates\n                for k in range(max_old_candidates):\n                    q = old_candidate_block[i, k]\n                    if q < 0:\n                        continue\n\n                    d = fast_bit_jaccard(data_p, data[q])\n                    dist_thresh_q = dist_thresholds[q]\n                    max_threshold = max(dist_thresh_p, dist_thresh_q)\n\n                    if d <= max_threshold:\n                        q_block = q // vertex_block_size\n                        if q_block >= n_threads:\n                            q_block = n_threads - 1\n\n                        # Place update in p's bucket\n                        bucket_idx = local_counts[p_block]\n                        write_idx = t * max_updates_per_src_thread + bucket_idx\n                        if write_idx < max_updates:\n                            update_array[p_block, write_idx, 0] = p\n                            update_array[p_block, write_idx, 1] = q\n                            update_array[p_block, write_idx, 2] = d\n                            local_counts[p_block] += 1\n\n                        # If q is in a different block, also place in q's bucket\n                        if q_block != p_block:\n                            bucket_idx = local_counts[q_block]\n                            write_idx = t * max_updates_per_src_thread + bucket_idx\n                            if write_idx < max_updates:\n                                update_array[q_block, write_idx, 0] = p\n                                update_array[q_block, write_idx, 1] = q\n                                update_array[q_block, write_idx, 2] = d\n                                local_counts[q_block] += 1\n\n        # Record total updates generated by this thread for each bucket\n        for b in range(n_threads):\n            n_updates_per_block[b, t + 1] = local_counts[b]\n\n\ndef nn_descent_uint8(\n    data,\n    n_neighbors,\n    rng_state,\n    max_candidates=50,\n    n_iters=10,\n    delta=0.001,\n    delta_improv=None,\n    leaf_array=None,\n    verbose=False,\n):\n    \"\"\"\n    Perform approximate nearest neighbor descent algorithm using uint8 data.\n\n    Parameters:\n    - data: The input data array.\n    - n_neighbors: The number of nearest neighbors to search for.\n    - rng_state: The random number generator state.\n    - max_candidates: The maximum number of candidates to consider during the search. Default is 50.\n    - n_iters: The number of iterations to perform. Default is 10.\n    - delta: The stopping threshold based on update count. Default is 0.001.\n    - delta_improv: Optional stopping threshold based on relative improvement in total\n        graph distance. When set (e.g., 0.001 for 0.1%), the algorithm will also\n        terminate when the relative improvement in sum of all distances drops below\n        this threshold. This can provide earlier termination on data with good\n        structure, adapting to the intrinsic difficulty of the dataset. Default is None\n        (disabled).\n    - leaf_array: The array representing the leaf structure of the RP-tree. Default is None.\n    - verbose: Whether to print progress information. Default is False.\n\n    Returns:\n    - The sorted nearest neighbor graph.\n    \"\"\"\n    n_threads = numba.get_num_threads()\n    current_graph = make_heap(data.shape[0], n_neighbors)\n    init_rp_tree_uint8(data, current_graph, leaf_array, n_threads)\n    init_random_uint8(n_neighbors, data, current_graph, rng_state)\n\n    n_vertices = data.shape[0]\n    n_threads = numba.get_num_threads()\n    block_size = 65536 // n_threads\n    n_blocks = n_vertices // block_size\n\n    max_updates_per_thread = int(\n        ((max_candidates**2 + max_candidates * (max_candidates - 1) / 2) * block_size)\n    )\n    update_array = np.empty((n_threads, max_updates_per_thread, 3), dtype=np.float32)\n    n_updates_per_thread = np.zeros(n_threads, dtype=np.int32)\n\n    # For distance-based termination\n    prev_sum_dist = None\n\n    for n in range(n_iters):\n        if verbose:\n            print(\"\\t\", n + 1, \" / \", n_iters)\n\n        (new_candidate_neighbors, old_candidate_neighbors) = build_candidates(\n            current_graph, max_candidates, rng_state, n_threads\n        )\n\n        c = 0\n        n_vertices = new_candidate_neighbors.shape[0]\n        for i in range(n_blocks + 1):\n            block_start = i * block_size\n            block_end = min(n_vertices, (i + 1) * block_size)\n\n            new_candidate_block = new_candidate_neighbors[block_start:block_end]\n            old_candidate_block = old_candidate_neighbors[block_start:block_end]\n\n            dist_thresholds = current_graph[1][:, 0]\n\n            generate_graph_update_array_uint8(\n                update_array,\n                n_updates_per_thread,\n                new_candidate_block,\n                old_candidate_block,\n                dist_thresholds,\n                data,\n                n_threads,\n            )\n\n            c += apply_graph_update_array(\n                current_graph, update_array, n_updates_per_thread, n_threads\n            )\n\n        # Check update count termination\n        if c <= delta * n_neighbors * data.shape[0]:\n            if verbose:\n                print(\"\\tStopping threshold met -- exiting after\", n + 1, \"iterations\")\n            return deheap_sort(current_graph[0], current_graph[1])\n\n        # Check distance improvement termination (if enabled)\n        if delta_improv is not None:\n            all_distances = current_graph[1]\n            valid_mask = all_distances < INF\n            sum_dist = np.sum(all_distances[valid_mask])\n\n            if prev_sum_dist is not None:\n                rel_improv = abs(sum_dist - prev_sum_dist) / abs(prev_sum_dist)\n                if rel_improv < delta_improv:\n                    if verbose:\n                        print(\n                            f\"\\tDistance improvement threshold met ({rel_improv:.4%} < {delta_improv:.4%})\"\n                            f\" -- exiting after {n + 1} iterations\"\n                        )\n                    return deheap_sort(current_graph[0], current_graph[1])\n\n            prev_sum_dist = sum_dist\n\n        block_size = min(n_vertices, 2 * block_size)\n        n_blocks = n_vertices // block_size\n\n    return deheap_sort(current_graph[0], current_graph[1])\n\n\ndef nn_descent_uint8_sorted(\n    data,\n    n_neighbors,\n    rng_state,\n    max_candidates=50,\n    n_iters=10,\n    delta=0.001,\n    delta_improv=None,\n    leaf_array=None,\n    verbose=False,\n):\n    \"\"\"\n    Perform approximate nearest neighbor descent algorithm using uint8 data.\n\n    This version uses pre-sorted updates bucketed by target block for potentially\n    better performance when n_threads is large.\n\n    Parameters:\n    - data: The input data array.\n    - n_neighbors: The number of nearest neighbors to search for.\n    - rng_state: The random number generator state.\n    - max_candidates: The maximum number of candidates to consider during the search. Default is 50.\n    - n_iters: The number of iterations to perform. Default is 10.\n    - delta: The stopping threshold based on update count. Default is 0.001.\n    - delta_improv: Optional stopping threshold based on relative improvement in total\n        graph distance. When set (e.g., 0.001 for 0.1%), the algorithm will also\n        terminate when the relative improvement in sum of all distances drops below\n        this threshold. This can provide earlier termination on data with good\n        structure, adapting to the intrinsic difficulty of the dataset. Default is None\n        (disabled).\n    - leaf_array: The array representing the leaf structure of the RP-tree. Default is None.\n    - verbose: Whether to print progress information. Default is False.\n\n    Returns:\n    - The sorted nearest neighbor graph.\n    \"\"\"\n    n_threads = numba.get_num_threads()\n    current_graph = make_heap(data.shape[0], n_neighbors)\n    init_rp_tree_uint8(data, current_graph, leaf_array, n_threads)\n    init_random_uint8(n_neighbors, data, current_graph, rng_state)\n\n    n_vertices = data.shape[0]\n    block_size = 65536 // n_threads\n    n_blocks = n_vertices // block_size\n\n    max_updates_per_thread = int(\n        ((max_candidates**2 + max_candidates * (max_candidates - 1) / 2) * block_size)\n    )\n    update_array = np.empty((n_threads, max_updates_per_thread, 3), dtype=np.float32)\n    n_updates_per_block = np.zeros((n_threads, n_threads + 1), dtype=np.int32)\n\n    # For distance-based termination\n    prev_sum_dist = None\n\n    for n in range(n_iters):\n        if verbose:\n            print(\"\\t\", n + 1, \" / \", n_iters)\n\n        (new_candidate_neighbors, old_candidate_neighbors) = build_candidates(\n            current_graph, max_candidates, rng_state, n_threads\n        )\n\n        c = 0\n        for i in range(n_blocks + 1):\n            block_start = i * block_size\n            block_end = min(n_vertices, (i + 1) * block_size)\n\n            new_candidate_block = new_candidate_neighbors[block_start:block_end]\n            old_candidate_block = old_candidate_neighbors[block_start:block_end]\n\n            dist_thresholds = current_graph[1][:, 0]\n\n            generate_sorted_graph_update_array_uint8(\n                update_array,\n                n_updates_per_block,\n                new_candidate_block,\n                old_candidate_block,\n                dist_thresholds,\n                data,\n                n_threads,\n            )\n\n            c += apply_sorted_graph_updates(\n                current_graph, update_array, n_updates_per_block, n_threads\n            )\n\n        # Check update count termination\n        if c <= delta * n_neighbors * data.shape[0]:\n            if verbose:\n                print(\"\\tStopping threshold met -- exiting after\", n + 1, \"iterations\")\n            return deheap_sort(current_graph[0], current_graph[1])\n\n        # Check distance improvement termination (if enabled)\n        if delta_improv is not None:\n            all_distances = current_graph[1]\n            valid_mask = all_distances < INF\n            sum_dist = np.sum(all_distances[valid_mask])\n\n            if prev_sum_dist is not None:\n                rel_improv = abs(sum_dist - prev_sum_dist) / abs(prev_sum_dist)\n                if rel_improv < delta_improv:\n                    if verbose:\n                        print(\n                            f\"\\tDistance improvement threshold met ({rel_improv:.4%} < {delta_improv:.4%})\"\n                            f\" -- exiting after {n + 1} iterations\"\n                        )\n                    return deheap_sort(current_graph[0], current_graph[1])\n\n            prev_sum_dist = sum_dist\n\n        block_size = min(n_vertices, 2 * block_size)\n        n_blocks = n_vertices // block_size\n\n    return deheap_sort(current_graph[0], current_graph[1])\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools>=61.2\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"evoc\"\nversion = \"0.3.1\"\nauthors = [{name = \"Leland McInnes\", email = \"leland.mcinnes@gmail.com\"}]\nmaintainers = [{name = \"Leland McInnes\", email = \"leland.mcinnes@gmail.com\"}]\ndescription = \"Embedding Vector Oriented Clustering\"\nreadme = \"README.rst\"\nkeywords = [\"embedding vector\", \"vector database\", \"topic modelling\", \"cluster\", \"clustering\"]\nlicense = \"BSD-2-Clause\"\nclassifiers = [\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Development Status :: 4 - Beta\",\n    \"Operating System :: OS Independent\",\n]\nurls = {Homepage = \"https://github.com/TutteInstitute/evoc\"}\nrequires-python = \">=3.10\"\ndependencies = [\n    \"numpy>=1.21\",\n    \"scikit-learn>=1.1\",\n    \"numba>=0.59\",\n    \"tqdm\",\n]\n\n[dependency-groups]\ncicd = [\"pytest\", \"pytest-azurepipelines\", \"pytest-cov\", \"matplotlib\"]\n\n[tool.setuptools]\nzip-safe = false\npackages = [\"evoc\"]\ninclude-package-data = false\n"
  },
  {
    "path": "pytest.ini",
    "content": "[pytest]\nmarkers =\n    performance: marks tests as performance tests (deselect with '-m \"not performance\"')\n    slow: marks tests as slow running tests\n    integration: marks tests as integration tests\n\ntestpaths = evoc/tests\naddopts = -v --tb=short\ntimeout = 300\n\nfilterwarnings =\n    ignore::DeprecationWarning\n    ignore::PendingDeprecationWarning\n    ignore::FutureWarning\n"
  },
  {
    "path": "scripts/run_performance_tests.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nPerformance benchmark runner for evoc knn_graph module.\n\nThis script runs performance tests and generates a report that can be used\nfor performance regression monitoring in CI/CD pipelines.\n\"\"\"\n\nimport argparse\nimport json\nimport sys\nimport time\nimport platform\nimport subprocess\nfrom pathlib import Path\n\n\ndef run_performance_tests(output_file=None, verbose=False):\n    \"\"\"Run performance tests and collect results.\"\"\"\n    \n    print(\"Running EVoC knn_graph performance benchmarks...\")\n    print(f\"Platform: {platform.platform()}\")\n    print(f\"Python: {platform.python_version()}\")\n    print(\"-\" * 60)\n    \n    # Run pytest with performance markers\n    cmd = [\n        sys.executable, \"-m\", \"pytest\", \n        \"evoc/tests/test_knn_graph_performance.py\",\n        \"-m\", \"performance\",\n        \"-v\"\n    ]\n    \n    if verbose:\n        cmd.append(\"-s\")\n    \n    # Add JSON report plugin if available\n    try:\n        import pytest_json_report\n        if output_file:\n            cmd.extend([\"--json-report\", f\"--json-report-file={output_file}\"])\n    except ImportError:\n        print(\"Note: pytest-json-report not installed, basic output only\")\n    \n    start_time = time.time()\n    result = subprocess.run(cmd, capture_output=not verbose, text=True)\n    duration = time.time() - start_time\n    \n    if result.returncode == 0:\n        print(f\"\\nAll performance tests passed in {duration:.1f} seconds!\")\n    else:\n        print(f\"\\nSome performance tests failed or had issues.\")\n        if not verbose and result.stdout:\n            print(\"STDOUT:\")\n            print(result.stdout)\n        if result.stderr:\n            print(\"STDERR:\")\n            print(result.stderr)\n    \n    return result.returncode == 0\n\n\ndef generate_performance_report(test_results_file, output_file):\n    \"\"\"Generate a human-readable performance report.\"\"\"\n    \n    try:\n        with open(test_results_file, 'r') as f:\n            data = json.load(f)\n    except FileNotFoundError:\n        print(f\"Test results file {test_results_file} not found\")\n        return False\n    except json.JSONDecodeError:\n        print(f\"Could not parse JSON from {test_results_file}\")\n        return False\n    \n    # Extract performance metrics\n    report = {\n        'timestamp': time.strftime('%Y-%m-%d %H:%M:%S'),\n        'platform': platform.platform(),\n        'python_version': platform.python_version(),\n        'total_tests': data.get('summary', {}).get('total', 0),\n        'passed_tests': data.get('summary', {}).get('passed', 0),\n        'failed_tests': data.get('summary', {}).get('failed', 0),\n        'duration': data.get('duration', 0),\n        'tests': []\n    }\n    \n    # Process individual test results\n    for test in data.get('tests', []):\n        if 'performance' in test.get('keywords', []):\n            test_info = {\n                'name': test.get('nodeid', '').split('::')[-1],\n                'duration': test.get('duration', 0),\n                'outcome': test.get('outcome', 'unknown'),\n                'stdout': test.get('call', {}).get('stdout', '')\n            }\n            report['tests'].append(test_info)\n    \n    # Write report\n    with open(output_file, 'w') as f:\n        json.dump(report, f, indent=2)\n    \n    print(f\"Performance report written to {output_file}\")\n    return True\n\n\ndef check_performance_regression(current_file, baseline_file, threshold=1.5):\n    \"\"\"\n    Check for performance regressions by comparing current results to baseline.\n    \n    Args:\n        current_file: JSON file with current test results\n        baseline_file: JSON file with baseline results\n        threshold: Maximum allowed slowdown ratio (e.g., 1.5 = 50% slower)\n    \n    Returns:\n        bool: True if no significant regressions detected\n    \"\"\"\n    \n    try:\n        with open(current_file, 'r') as f:\n            current = json.load(f)\n        with open(baseline_file, 'r') as f:\n            baseline = json.load(f)\n    except (FileNotFoundError, json.JSONDecodeError) as e:\n        print(f\"Error reading performance data: {e}\")\n        return False\n    \n    print(f\"Comparing performance to baseline from {baseline.get('timestamp', 'unknown')}\")\n    \n    regressions = []\n    improvements = []\n    \n    # Compare test durations\n    current_tests = {t['name']: t for t in current.get('tests', [])}\n    baseline_tests = {t['name']: t for t in baseline.get('tests', [])}\n    \n    for test_name in current_tests:\n        if test_name in baseline_tests:\n            current_duration = current_tests[test_name]['duration']\n            baseline_duration = baseline_tests[test_name]['duration']\n            \n            if baseline_duration > 0:\n                ratio = current_duration / baseline_duration\n                \n                if ratio > threshold:\n                    regressions.append({\n                        'test': test_name,\n                        'current': current_duration,\n                        'baseline': baseline_duration,\n                        'ratio': ratio\n                    })\n                elif ratio < 0.8:  # 20% improvement\n                    improvements.append({\n                        'test': test_name,\n                        'current': current_duration,\n                        'baseline': baseline_duration,\n                        'ratio': ratio\n                    })\n    \n    # Report results\n    if regressions:\n        print(f\"\\n⚠️  Performance regressions detected:\")\n        for reg in regressions:\n            print(f\"  {reg['test']}: {reg['ratio']:.2f}x slower \"\n                  f\"({reg['current']:.3f}s vs {reg['baseline']:.3f}s)\")\n    \n    if improvements:\n        print(f\"\\n✅ Performance improvements:\")\n        for imp in improvements:\n            print(f\"  {imp['test']}: {imp['ratio']:.2f}x faster \"\n                  f\"({imp['current']:.3f}s vs {imp['baseline']:.3f}s)\")\n    \n    if not regressions and not improvements:\n        print(\"\\n✅ No significant performance changes detected\")\n    \n    return len(regressions) == 0\n\n\ndef main():\n    parser = argparse.ArgumentParser(description=\"Run EVoC performance benchmarks\")\n    parser.add_argument(\"--output\", \"-o\", help=\"Output file for test results (JSON)\")\n    parser.add_argument(\"--report\", \"-r\", help=\"Generate human-readable report file\")\n    parser.add_argument(\"--baseline\", \"-b\", help=\"Compare against baseline performance file\")\n    parser.add_argument(\"--threshold\", \"-t\", type=float, default=1.5,\n                       help=\"Regression threshold (default: 1.5x slower)\")\n    parser.add_argument(\"--verbose\", \"-v\", action=\"store_true\",\n                       help=\"Verbose output\")\n    \n    args = parser.parse_args()\n    \n    # Default output file\n    if not args.output:\n        timestamp = time.strftime(\"%Y%m%d_%H%M%S\")\n        args.output = f\"performance_results_{timestamp}.json\"\n    \n    # Run performance tests\n    success = run_performance_tests(args.output, args.verbose)\n    \n    if not success:\n        print(\"Performance tests failed\")\n        return 1\n    \n    # Generate report if requested\n    if args.report:\n        generate_performance_report(args.output, args.report)\n    \n    # Check for regressions if baseline provided\n    if args.baseline:\n        if not check_performance_regression(args.output, args.baseline, args.threshold):\n            print(\"Performance regression detected!\")\n            return 1\n    \n    return 0\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n"
  },
  {
    "path": "setup.py",
    "content": "from setuptools import setup\n\nif __name__ == '__main__':\n    setup()"
  }
]