Repository: TutteInstitute/evoc Branch: main Commit: a58b66e402bd Files: 68 Total size: 1.1 MB Directory structure: gitextract_hnrqw2lv/ ├── .gitignore ├── .readthedocs.yaml ├── LICENSE ├── README.rst ├── azure-pipelines.yml ├── doc/ │ ├── Makefile │ ├── README.md │ ├── build_docs.bat │ ├── build_docs.sh │ ├── requirements.txt │ └── source/ │ ├── _static/ │ │ └── custom.css │ ├── api/ │ │ ├── evoc.cluster_trees.rst │ │ ├── evoc.clustering.rst │ │ ├── evoc.clustering_utilities.rst │ │ ├── generated/ │ │ │ ├── evoc.EVoC.rst │ │ │ ├── evoc.boruvka.parallel_boruvka.rst │ │ │ ├── evoc.cluster_trees.condense_tree.rst │ │ │ ├── evoc.cluster_trees.extract_leaves.rst │ │ │ ├── evoc.cluster_trees.get_cluster_label_vector.rst │ │ │ ├── evoc.cluster_trees.get_point_membership_strength_vector.rst │ │ │ ├── evoc.cluster_trees.mst_to_linkage_tree.rst │ │ │ ├── evoc.clustering_utilities.binary_search_for_n_clusters.rst │ │ │ ├── evoc.clustering_utilities.build_cluster_tree.rst │ │ │ ├── evoc.clustering_utilities.find_duplicates.rst │ │ │ ├── evoc.clustering_utilities.find_peaks.rst │ │ │ ├── evoc.clustering_utilities.select_diverse_peaks.rst │ │ │ ├── evoc.evoc_clusters.rst │ │ │ ├── evoc.graph_construction.neighbor_graph_matrix.rst │ │ │ ├── evoc.knn_graph.knn_graph.rst │ │ │ ├── evoc.label_propagation.label_propagation_init.rst │ │ │ ├── evoc.node_embedding.node_embedding.rst │ │ │ └── evoc.numba_kdtree.build_kdtree.rst │ │ └── index.rst │ ├── benchmarks.ipynb │ ├── changelog.rst │ ├── conf.py │ ├── examples.rst │ ├── index.rst │ ├── installation.rst │ ├── quickstart.rst │ └── user_guide.rst ├── evoc/ │ ├── __init__.py │ ├── boruvka.py │ ├── cluster_trees.py │ ├── clustering.py │ ├── clustering_utilities.py │ ├── common_nndescent.py │ ├── disjoint_set.py │ ├── float_nndescent.py │ ├── graph_construction.py │ ├── int8_nndescent.py │ ├── knn_graph.py │ ├── label_propagation.py │ ├── nested_parallelism.py │ ├── node_embedding.py │ ├── numba_kdtree.py │ ├── tests/ │ │ ├── test_boruvka.py │ │ ├── test_cluster_trees.py │ │ ├── test_clustering.py │ │ ├── test_knn_graph.py │ │ ├── test_knn_graph_performance.py │ │ ├── test_numba_kdtree.py │ │ └── test_numba_kdtree_performance.py │ └── uint8_nndescent.py ├── pyproject.toml ├── pytest.ini ├── scripts/ │ └── run_performance_tests.py └── setup.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. #pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. # https://pdm.fming.dev/#use-with-ide .pdm.toml # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ .idea/.gitignore .idea/evoc.iml .idea/misc.xml .idea/modules.xml .idea/vcs.xml .idea/inspectionProfiles/profiles_settings.xml .idea/inspectionProfiles/Project_Default.xml .vscode/settings.json ================================================ FILE: .readthedocs.yaml ================================================ # .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the version of Python and other tools you might need build: os: ubuntu-22.04 tools: python: "3.11" # You can also specify other tools here # Build documentation in the docs/ directory with Sphinx sphinx: configuration: doc/source/conf.py fail_on_warning: false # If using Sphinx, optionally build your docs in additional formats such as PDF formats: - pdf - epub # Optionally declare the Python requirements required to build your docs python: install: - requirements: doc/requirements.txt - method: pip path: . extra_requirements: - docs # Optional but recommended, specify the Python version to use # https://docs.readthedocs.io/en/stable/config-file/v2.html#python ================================================ FILE: LICENSE ================================================ BSD 2-Clause License Copyright (c) 2024, Tutte Institute for Mathematics and Computing Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.rst ================================================ .. image:: doc/evoc_logo_horizontal.png :width: 600 :align: center :alt: EVōC Logo ==== EVōC ==== EVōC (pronounced as "evoke") provides Embedding Vector Oriented Clustering. EVōC is a library for fast and flexible clustering of large datasets of high dimensional embedding vectors. If you have CLIP-vectors, outputs from sentence-transformers, or openAI, or Cohere embed, and you want to quickly get good clusters out this is the library for you. EVōC takes all the good parts of the combination of UMAP + HDBSCAN for embedding clustering, improves upon them, and removes all the time-consuming parts. By specializing directly to embedding vectors we can get good quality clustering with fewer hyper-parameters to tune and in a fraction of the time. EVōC is the library to use if you want: * Fast clustering of embedding vectors on CPU * Multi-granularity clustering, and automatic selection of the number of clusters * Clustering of int8 or binary quantized embedding vectors that works out-of-the-box As of now this is very much an early beta version of the library. Things can and will break right now. We would welcome feedback, use cases and feature suggestions however. ------------- Documentation ------------- The full documentation is available on Read the Docs: `https://evoc.readthedocs.io/en/latest/ `_ ----------- Basic Usage ----------- EVōC follows the scikit-learn API, so it should be familiar to most users. You can use EVōC wherever you might have previously been using other sklearn clustering algorithms. Here is a simple example .. code-block:: python import evoc from sklearn.datasets import make_blobs data, _ = make_blobs(n_samples=100_000, n_features=1024, centers=100) clusterer = evoc.EVoC() cluster_labels = clusterer.fit_predict(data) Some more unique features include the generation of multiple layers of cluster granularity, the ability to extract a hierarchy of clusters across those layers, and automatic duplicate (or very near duplicate) detection. .. code-block:: python import evoc from sklearn.datasets import make_blobs data, _ = make_blobs(n_samples=100_000, n_features=1024, centers=100) clusterer = evoc.EVoC() cluster_labels = clusterer.fit_predict(data) cluster_layers = clusterer.cluster_layers_ hierarchy = clusterer.cluster_tree_ potential_duplicates = clusterer.duplicates_ The cluster layers are a list of cluster label vectors with the first being the finest grained and later layers being coarser grained. This is ideal for layered topic modelling and use with `DataMapPlot `_. See `this data map `_ for an example of using these layered clusters in topic modelling (zoom in to access finer grained topics). ------------ Installation ------------ EVōC has a small set of dependencies: * numpy * scikit-learn * numba * tqdm * tbb You can install EVōC from PyPI using pip: .. code-block:: bash pip install evoc To install the latest version of EVōC from source: .. code-block:: bash pip install git+https://github.com/TutteInstitute/evoc.git ---------- References ---------- The algorithm implemented in EVōC is not published anywhere at this time. If you would like to cite something in reference to EVōC, I would encourage you to cite the PLSCAN paper on which the cluster extraction in EVōC is based: Please cite: D.M. Bot, L. McInnes, J. Aerts. *Persistent Multiscale Density-based Clustering.* In: arXiv preprint arXiv:2512.16558, 2025. https://arxiv.org/abs/2512.16558. ------- License ------- EVōC is BSD (2-clause) licensed. See the LICENSE file for details. ------------ Contributing ------------ Contributions are more than welcome! If you have ideas for features of projects please get in touch. Everything from code to notebooks to examples and documentation are all *equally valuable* so please don't feel you can't contribute. To contribute please `fork the project `_ make your changes and submit a pull request. We will do our best to work through any issues with you and get your code merged in. ================================================ FILE: azure-pipelines.yml ================================================ # Trigger a build when there is a push to the main branch or a tag starts with release- trigger: branches: include: - main tags: include: - release-* # Trigger a build when there is a pull request to the main branch # Ignore PRs that are just updating the docs pr: branches: include: - main exclude: - doc/* - README.rst parameters: - name: includeReleaseCandidates displayName: "Allow pre-release dependencies" type: boolean default: false variables: triggeredByPullRequest: $[eq(variables['Build.Reason'], 'PullRequest')] stages: - stage: RunAllTests displayName: Run test suite jobs: - job: run_platform_tests strategy: matrix: mac_py310: imageName: 'macOS-latest' python.version: '3.10' linux_py310: imageName: 'ubuntu-latest' python.version: '3.10' windows_py310: imageName: 'windows-latest' python.version: '3.10' mac_py311: imageName: 'macOS-latest' python.version: '3.11' linux_py311: imageName: 'ubuntu-latest' python.version: '3.11' windows_py311: imageName: 'windows-latest' python.version: '3.11' mac_py312: imageName: 'macOS-latest' python.version: '3.12' linux_py312: imageName: 'ubuntu-latest' python.version: '3.12' windows_py312: imageName: 'windows-latest' python.version: '3.12' mac_py313: imageName: 'macOS-latest' python.version: '3.13' linux_py313: imageName: 'ubuntu-latest' python.version: '3.13' windows_py313: imageName: 'windows-latest' python.version: '3.13' mac_py314: imageName: 'macOS-latest' python.version: '3.14' linux_py314: imageName: 'ubuntu-latest' python.version: '3.14' windows_py314: imageName: 'windows-latest' python.version: '3.14' pool: vmImage: $(imageName) steps: - task: UsePythonVersion@0 inputs: versionSpec: '$(python.version)' displayName: 'Use Python $(python.version)' - script: | python -m pip install --upgrade pip displayName: 'Upgrade pip' # 1. Install the full LLVM package only if the OS is macOS - script: | brew install llvm@20 # Homebrew formula names can change, so we ensure it links correctly if necessary brew link --force --overwrite llvm@20 displayName: 'Install LLVM via Homebrew (macOS only)' condition: eq(variables['Agent.OS'], 'Darwin') # 2. Find the Homebrew install path and set the environment variable only on macOS - script: | # Determine the LLVM install prefix dynamically LLVM_PREFIX=$(brew --prefix llvm@20) # Set the LLVM_CONFIG environment variable used by llvmlite's build script echo "##vso[task.setvariable variable=LLVM_CONFIG]$LLVM_PREFIX/bin/llvm-config" echo "LLVM_CONFIG set to: $LLVM_CONFIG" # Also set CMAKE_PREFIX_PATH in case other dependencies need it echo "##vso[task.setvariable variable=CMAKE_PREFIX_PATH]$LLVM_PREFIX/lib/cmake" displayName: 'Configure LLVM Environment Variables (macOS only)' condition: eq(variables['Agent.OS'], 'Darwin') - script: | python -m pip install -U uv uv sync --group cicd env: # Ensure that the LLVM_CONFIG environment variable is available during installation LLVM_CONFIG: $(LLVM_CONFIG) CMAKE_PREFIX_PATH: $(CMAKE_PREFIX_PATH) displayName: 'Install package and dependencies' - script: | uv run pytest evoc/tests --show-capture=no -v --disable-warnings --junitxml=junit/test-results.xml --cov=evoc/ --cov-report=xml --cov-report=html displayName: 'Run tests' condition: ne(variables['Agent.OS'], 'Darwin') - script: | uv run pytest evoc/tests -v --capture=tee-sys --disable-warnings --junitxml=junit/test-results.xml --cov=evoc/ --cov-report=xml --cov-report=html displayName: 'Run tests' condition: eq(variables['Agent.OS'], 'Darwin') - task: PublishTestResults@2 inputs: testResultsFiles: '$(System.DefaultWorkingDirectory)/**/coverage.xml' testRunTitle: '$(Agent.OS) - $(Build.BuildNumber)[$(Agent.JobName)] - Python $(python.version)' condition: succeededOrFailed() - stage: BuildPublishArtifact dependsOn: RunAllTests condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/release-'), eq(variables.triggeredByPullRequest, false)) jobs: - job: BuildArtifacts displayName: Build source dists and wheels pool: vmImage: 'ubuntu-latest' steps: - task: UsePythonVersion@0 inputs: versionSpec: '3.13' displayName: 'Use Python 3.13' - script: | python -m pip install --upgrade pip python -m pip install -U uv uv sync displayName: 'Install dependencies' - script: | uv build --no-sources --sdist --wheel displayName: 'Build package' - bash: | export PACKAGE_VERSION="$(uv version --short)" echo "Package Version: ${PACKAGE_VERSION}" echo "##vso[task.setvariable variable=packageVersionFormatted;]release-${PACKAGE_VERSION}" displayName: 'Get package version' - script: | echo "Version in git tag $(Build.SourceBranchName) does not match version derived from setup.py $(packageVersionFormatted)" exit 1 displayName: Raise error if version doesnt match tag condition: and(succeeded(), ne(variables['Build.SourceBranchName'], variables['packageVersionFormatted'])) - task: DownloadSecureFile@1 name: PYPIRC_CONFIG displayName: 'Download pypirc' inputs: secureFile: 'pypirc' - script: | uvx twine check dist/* uvx twine upload --repository pypi --config-file $(PYPIRC_CONFIG.secureFilePath) dist/* displayName: 'Upload to PyPI' condition: and(succeeded(), eq(variables['Build.SourceBranchName'], variables['packageVersionFormatted'])) ================================================ FILE: doc/Makefile ================================================ # Makefile for Sphinx documentation # You can set these variables from the command line SPHINXOPTS = SPHINXBUILD = sphinx-build SOURCEDIR = source BUILDDIR = build # Put it first so that "make" without argument is like "make help" help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx-build %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) # Custom targets clean: @$(SPHINXBUILD) -M clean "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) html: @$(SPHINXBUILD) -b html "$(SOURCEDIR)" "$(BUILDDIR)/html" $(SPHINXOPTS) $(O) @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." livehtml: sphinx-autobuild "$(SOURCEDIR)" "$(BUILDDIR)/html" $(SPHINXOPTS) $(O) linkcheck: @$(SPHINXBUILD) -b linkcheck "$(SOURCEDIR)" "$(BUILDDIR)/linkcheck" $(SPHINXOPTS) $(O) @echo "Link check complete; look for any errors in the above output or in $(BUILDDIR)/linkcheck/output.txt." doctest: @$(SPHINXBUILD) -b doctest "$(SOURCEDIR)" "$(BUILDDIR)/doctest" $(SPHINXOPTS) $(O) @echo "Testing of doctests in the sources finished, look at the results in $(BUILDDIR)/doctest/output.txt." ================================================ FILE: doc/README.md ================================================ # EVoC Documentation This directory contains the Sphinx documentation for EVoC. ## Structure ``` doc/ ├── build/ # Generated documentation (HTML, PDF, etc.) ├── source/ # Source files for documentation │ ├── _static/ # Static files (CSS, images, etc.) │ ├── _templates/ # Custom Sphinx templates │ ├── api/ # API documentation files │ ├── notebooks/ # Jupyter notebook examples │ ├── tutorials/ # Step-by-step tutorials │ ├── conf.py # Sphinx configuration │ ├── index.rst # Main documentation page │ └── *.rst # Other documentation pages ├── requirements.txt # Documentation dependencies ├── Makefile # Build commands (Unix) ├── build_docs.sh # Automated build script (Unix) ├── build_docs.bat # Automated build script (Windows) └── README.md # This file ``` ## Building the Documentation ### Prerequisites 1. Python 3.8 or later 2. Git (for development installation) ### Quick Build **Unix/macOS:** ```bash cd doc ./build_docs.sh ``` **Windows:** ```cmd cd doc build_docs.bat ``` ### Manual Build 1. Install dependencies: ```bash pip install -r requirements.txt ``` 2. Install EVoC in development mode: ```bash pip install -e ../.. ``` 3. Build documentation: ```bash make html ``` 4. Open `build/html/index.html` in your browser ### Advanced Options **Clean build:** ```bash make clean html ``` **Check links:** ```bash make linkcheck ``` **Run doctests:** ```bash make doctest ``` **Live reload during development:** ```bash pip install sphinx-autobuild make livehtml ``` ## Features - **Sphinx RTD Theme**: Professional appearance matching ReadTheDocs - **Numpydoc**: Automatic parsing of NumPy-style docstrings - **Nbsphinx**: Integration of Jupyter notebooks as documentation - **Autodoc**: Automatic API documentation generation - **ReadTheDocs Ready**: Configured for automatic deployment ## Adding Content ### New Documentation Pages 1. Create `.rst` files in `source/` 2. Add them to the `toctree` in `index.rst` 3. Rebuild documentation ### Jupyter Notebooks 1. Add `.ipynb` files to `source/notebooks/` 2. Add them to `source/notebooks/index.rst` 3. Notebooks are automatically converted during build ### API Documentation API documentation is automatically generated from docstrings. To add new modules: 1. Add the module to `source/api/index.rst` 2. Create a dedicated `.rst` file if needed 3. Rebuild documentation ## ReadTheDocs Integration This documentation is configured for ReadTheDocs deployment: - Configuration: `.readthedocs.yaml` in project root - Requirements: `doc/requirements.txt` - Python version: 3.11 (configurable in `.readthedocs.yaml`) ## Troubleshooting **Import errors during build:** - Ensure EVoC is installed in development mode: `pip install -e ../..` - Check that all dependencies are installed: `pip install -r requirements.txt` **Missing modules in API docs:** - Verify the module paths in `source/api/index.rst` - Check that modules are importable from the documentation directory **Notebook execution errors:** - Notebooks are not executed by default (`nbsphinx_execute = 'never'`) - To execute notebooks during build, change to `nbsphinx_execute = 'always'` in `conf.py` **Theme or styling issues:** - Check `source/_static/custom.css` for customizations - Verify `sphinx_rtd_theme` is installed ## Contributing When adding new documentation: 1. Follow reStructuredText formatting 2. Use NumPy-style docstrings for API documentation 3. Include code examples where appropriate 4. Test build locally before submitting 5. Keep notebook outputs clear for examples For more details, see the main EVoC contributing guidelines. ================================================ FILE: doc/build_docs.bat ================================================ @echo off REM Documentation build script for EVoC (Windows) echo Building EVoC Documentation echo ========================== REM Check if we're in the right directory if not exist "source\conf.py" ( echo Error: Run this script from the doc directory exit /b 1 ) REM Check if virtual environment exists, create if needed if not exist "venv" ( echo Creating virtual environment... python -m venv venv ) REM Activate virtual environment call venv\Scripts\activate.bat REM Install requirements echo Installing documentation requirements... pip install -r requirements.txt REM Install EVoC in development mode echo Installing EVoC in development mode... pip install -e ..\.. REM Clean previous build echo Cleaning previous build... make clean REM Build HTML documentation echo Building HTML documentation... make html if %ERRORLEVEL% equ 0 ( echo Documentation built successfully! echo Open build\html\index.html in your browser to view ) else ( echo Build failed with errors exit /b 1 ) echo Build complete! ================================================ FILE: doc/build_docs.sh ================================================ #!/bin/bash # Documentation build script for EVoC set -e # Exit on any error echo "Building EVoC Documentation" echo "==========================" # Check if we're in the right directory if [ ! -f "source/conf.py" ]; then echo "Error: Run this script from the doc directory" exit 1 fi # Check if virtual environment exists, create if needed if [ ! -d "venv" ]; then echo "Creating virtual environment..." python -m venv venv fi # Activate virtual environment source venv/bin/activate # Install requirements echo "Installing documentation requirements..." pip install -r requirements.txt # Install EVoC in development mode echo "Installing EVoC in development mode..." pip install -e ../. # Clean previous build echo "Cleaning previous build..." make clean # Build HTML documentation echo "Building HTML documentation..." make html # Check for warnings if [ $? -eq 0 ]; then echo "Documentation built successfully!" echo "Open build/html/index.html in your browser to view" else echo "Build failed with errors" exit 1 fi # Optional: Run link check if [ "$1" = "--check-links" ]; then echo "Checking links..." make linkcheck fi # Optional: Run doctests if [ "$1" = "--test" ]; then echo "Running doctests..." make doctest fi echo "Build complete!" ================================================ FILE: doc/requirements.txt ================================================ # Sphinx documentation requirements sphinx>=7.0.0 sphinx-rtd-theme>=2.0.0 numpydoc>=1.6.0 nbsphinx>=0.9.0 ipython>=8.0.0 ipykernel>=6.0.0 jupyter>=1.0.0 matplotlib>=3.5.0 numpy>=1.21.0 scipy>=1.7.0 scikit-learn>=1.0.0 pandas>=1.3.0 numba>=0.56.0 # Optional but recommended for better notebook handling pandoc>=2.0 ipywidgets>=8.0.0 ================================================ FILE: doc/source/_static/custom.css ================================================ /* Custom CSS for EVoC documentation */ /* Improve code block styling */ .highlight { background-color: #f8f8f8; border: 1px solid #e1e4e5; border-radius: 4px; padding: 8px; margin: 12px 0; } /* Better parameter list formatting */ .field-list { margin: 1em 0; } .field-list dt { font-weight: bold; color: #2980b9; } /* Notebook cell styling */ .nbinput .highlight, .nboutput .highlight { border-left: 4px solid #1f8c8c; margin: 0.5em 0; } /* API documentation improvements */ .py.class dt { background-color: #f0f0f0; border-left: 4px solid #3498db; padding: 8px; margin-top: 20px; } .py.method dt { background-color: #f9f9f9; border-left: 3px solid #95a5a6; padding: 6px; margin-top: 15px; } /* Parameter tables */ .docutils th { background-color: #34495e; color: white; padding: 8px; } .docutils td { padding: 6px 8px; border-bottom: 1px solid #ecf0f1; } /* Admonition improvements */ .admonition { margin: 20px 0; padding: 15px; border-radius: 6px; } .admonition.note { background-color: #e8f4fd; border-left: 4px solid #3498db; } .admonition.warning { background-color: #fdf4e8; border-left: 4px solid #f39c12; } /* Code span improvements */ code.literal { background-color: #f1f2f3; color: #e74c3c; padding: 2px 4px; border-radius: 3px; font-size: 90%; } /* Sidebar improvements */ .wy-nav-side { background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%); } /* Footer customization */ .rst-footer-buttons { margin-top: 30px; padding-top: 20px; border-top: 1px solid #e1e4e5; } ================================================ FILE: doc/source/api/evoc.cluster_trees.rst ================================================ evoc.cluster_trees ================== .. automodule:: evoc.cluster_trees :members: :undoc-members: :show-inheritance: ================================================ FILE: doc/source/api/evoc.clustering.rst ================================================ evoc.clustering =============== .. automodule:: evoc.clustering :members: :undoc-members: :show-inheritance: ================================================ FILE: doc/source/api/evoc.clustering_utilities.rst ================================================ evoc.clustering_utilities ========================= .. automodule:: evoc.clustering_utilities :members: :undoc-members: :show-inheritance: ================================================ FILE: doc/source/api/generated/evoc.EVoC.rst ================================================ evoc.EVoC ========= .. currentmodule:: evoc .. autoclass:: EVoC .. automethod:: __init__ .. rubric:: Methods .. autosummary:: ~EVoC.__init__ ~EVoC.fit ~EVoC.fit_predict ~EVoC.get_metadata_routing ~EVoC.get_params ~EVoC.set_params .. rubric:: Attributes .. autosummary:: ~EVoC.cluster_tree_ ================================================ FILE: doc/source/api/generated/evoc.boruvka.parallel_boruvka.rst ================================================ evoc.boruvka.parallel\_boruvka ============================== .. currentmodule:: evoc.boruvka .. autofunction:: parallel_boruvka ================================================ FILE: doc/source/api/generated/evoc.cluster_trees.condense_tree.rst ================================================ evoc.cluster\_trees.condense\_tree ================================== .. currentmodule:: evoc.cluster_trees .. autofunction:: condense_tree ================================================ FILE: doc/source/api/generated/evoc.cluster_trees.extract_leaves.rst ================================================ evoc.cluster\_trees.extract\_leaves =================================== .. currentmodule:: evoc.cluster_trees .. autofunction:: extract_leaves ================================================ FILE: doc/source/api/generated/evoc.cluster_trees.get_cluster_label_vector.rst ================================================ evoc.cluster\_trees.get\_cluster\_label\_vector =============================================== .. currentmodule:: evoc.cluster_trees .. autofunction:: get_cluster_label_vector ================================================ FILE: doc/source/api/generated/evoc.cluster_trees.get_point_membership_strength_vector.rst ================================================ evoc.cluster\_trees.get\_point\_membership\_strength\_vector ============================================================ .. currentmodule:: evoc.cluster_trees .. autofunction:: get_point_membership_strength_vector ================================================ FILE: doc/source/api/generated/evoc.cluster_trees.mst_to_linkage_tree.rst ================================================ evoc.cluster\_trees.mst\_to\_linkage\_tree ========================================== .. currentmodule:: evoc.cluster_trees .. autofunction:: mst_to_linkage_tree ================================================ FILE: doc/source/api/generated/evoc.clustering_utilities.binary_search_for_n_clusters.rst ================================================ evoc.clustering\_utilities.binary\_search\_for\_n\_clusters =========================================================== .. currentmodule:: evoc.clustering_utilities .. autofunction:: binary_search_for_n_clusters ================================================ FILE: doc/source/api/generated/evoc.clustering_utilities.build_cluster_tree.rst ================================================ evoc.clustering\_utilities.build\_cluster\_tree =============================================== .. currentmodule:: evoc.clustering_utilities .. autofunction:: build_cluster_tree ================================================ FILE: doc/source/api/generated/evoc.clustering_utilities.find_duplicates.rst ================================================ evoc.clustering\_utilities.find\_duplicates =========================================== .. currentmodule:: evoc.clustering_utilities .. autofunction:: find_duplicates ================================================ FILE: doc/source/api/generated/evoc.clustering_utilities.find_peaks.rst ================================================ evoc.clustering\_utilities.find\_peaks ====================================== .. currentmodule:: evoc.clustering_utilities .. autofunction:: find_peaks ================================================ FILE: doc/source/api/generated/evoc.clustering_utilities.select_diverse_peaks.rst ================================================ evoc.clustering\_utilities.select\_diverse\_peaks ================================================= .. currentmodule:: evoc.clustering_utilities .. autofunction:: select_diverse_peaks ================================================ FILE: doc/source/api/generated/evoc.evoc_clusters.rst ================================================ evoc.evoc\_clusters =================== .. currentmodule:: evoc .. autofunction:: evoc_clusters ================================================ FILE: doc/source/api/generated/evoc.graph_construction.neighbor_graph_matrix.rst ================================================ evoc.graph\_construction.neighbor\_graph\_matrix ================================================ .. currentmodule:: evoc.graph_construction .. autofunction:: neighbor_graph_matrix ================================================ FILE: doc/source/api/generated/evoc.knn_graph.knn_graph.rst ================================================ evoc.knn\_graph.knn\_graph ========================== .. currentmodule:: evoc.knn_graph .. autofunction:: knn_graph ================================================ FILE: doc/source/api/generated/evoc.label_propagation.label_propagation_init.rst ================================================ evoc.label\_propagation.label\_propagation\_init ================================================ .. currentmodule:: evoc.label_propagation .. autofunction:: label_propagation_init ================================================ FILE: doc/source/api/generated/evoc.node_embedding.node_embedding.rst ================================================ evoc.node\_embedding.node\_embedding ==================================== .. currentmodule:: evoc.node_embedding .. autofunction:: node_embedding ================================================ FILE: doc/source/api/generated/evoc.numba_kdtree.build_kdtree.rst ================================================ evoc.numba\_kdtree.build\_kdtree ================================ .. currentmodule:: evoc.numba_kdtree .. autofunction:: build_kdtree ================================================ FILE: doc/source/api/index.rst ================================================ API Reference ============= This section contains the complete API reference for EVoC. Main Classes and Functions -------------------------- .. currentmodule:: evoc .. autosummary:: :toctree: generated/ :nosignatures: EVoC evoc_clusters Core Clustering --------------- .. autoclass:: EVoC :members: :inherited-members: :show-inheritance: .. autofunction:: evoc_clusters Utility Functions ----------------- .. currentmodule:: evoc.clustering_utilities .. autosummary:: :toctree: generated/ :nosignatures: find_peaks binary_search_for_n_clusters select_diverse_peaks build_cluster_tree find_duplicates .. autofunction:: find_peaks .. autofunction:: binary_search_for_n_clusters .. autofunction:: select_diverse_peaks .. autofunction:: build_cluster_tree .. autofunction:: find_duplicates Tree Operations --------------- .. currentmodule:: evoc.cluster_trees .. autosummary:: :toctree: generated/ :nosignatures: mst_to_linkage_tree condense_tree extract_leaves get_cluster_label_vector get_point_membership_strength_vector .. autofunction:: mst_to_linkage_tree .. autofunction:: condense_tree .. autofunction:: extract_leaves .. autofunction:: get_cluster_label_vector .. autofunction:: get_point_membership_strength_vector Graph Construction ------------------ .. currentmodule:: evoc.knn_graph .. autosummary:: :toctree: generated/ :nosignatures: knn_graph .. autofunction:: knn_graph .. currentmodule:: evoc.graph_construction .. autosummary:: :toctree: generated/ :nosignatures: neighbor_graph_matrix .. autofunction:: neighbor_graph_matrix Node Embedding -------------- .. currentmodule:: evoc.node_embedding .. autosummary:: :toctree: generated/ :nosignatures: node_embedding .. autofunction:: node_embedding .. currentmodule:: evoc.label_propagation .. autosummary:: :toctree: generated/ :nosignatures: label_propagation_init .. autofunction:: label_propagation_init Algorithm Components -------------------- .. currentmodule:: evoc.boruvka .. autosummary:: :toctree: generated/ :nosignatures: parallel_boruvka .. autofunction:: parallel_boruvka .. currentmodule:: evoc.numba_kdtree .. autosummary:: :toctree: generated/ :nosignatures: build_kdtree .. autofunction:: build_kdtree ================================================ FILE: doc/source/benchmarks.ipynb ================================================ { "cells": [ { "cell_type": "markdown", "id": "93984a77-bccc-46fc-a7e1-4eb862d10f6e", "metadata": {}, "source": [ "# Performance benchmarks\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." ] }, { "cell_type": "code", "execution_count": 1, "id": "a7ae0fff-dec4-49b8-9063-aafef992c764", "metadata": { "execution": { "iopub.execute_input": "2026-03-25T20:36:11.007014Z", "iopub.status.busy": "2026-03-25T20:36:11.006894Z", "iopub.status.idle": "2026-03-25T20:36:16.807511Z", "shell.execute_reply": "2026-03-25T20:36:16.806699Z", "shell.execute_reply.started": "2026-03-25T20:36:11.007000Z" } }, "outputs": [], "source": [ "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import seaborn as sns\n", "import time\n", "import evoc\n", "import umap\n", "import hdbscan\n", "import pandas as pd\n", "import sklearn.cluster\n", "import sklearn.metrics\n", "import warnings\n", "warnings.filterwarnings(\"ignore\")" ] }, { "cell_type": "markdown", "id": "5a6ef4ce-bf5f-4e45-857b-bd60f6228fb4", "metadata": {}, "source": [ "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. " ] }, { "cell_type": "code", "execution_count": 2, "id": "9823192a-9d7e-4a1f-b912-84883f8273e8", "metadata": { "execution": { "iopub.execute_input": "2026-03-25T20:36:16.808127Z", "iopub.status.busy": "2026-03-25T20:36:16.807874Z", "iopub.status.idle": "2026-03-25T20:36:16.811159Z", "shell.execute_reply": "2026-03-25T20:36:16.810725Z", "shell.execute_reply.started": "2026-03-25T20:36:16.808110Z" } }, "outputs": [], "source": [ "def umap_hdbscan(\n", " data,\n", " metric=\"euclidean\",\n", " n_neighbors=15,\n", " n_components=2,\n", " min_samples=5,\n", " min_cluster_size=10,\n", " min_dist=0.1,\n", " cluster_selection_method=\"eom\",\n", " n_epochs=None,\n", " negative_sample_rate=5,\n", "):\n", " embedding = umap.UMAP(\n", " metric=metric,\n", " n_neighbors=n_neighbors,\n", " n_components=n_components,\n", " min_dist=min_dist,\n", " n_epochs=n_epochs,\n", " negative_sample_rate=negative_sample_rate,\n", " n_jobs=8,\n", " ).fit_transform(data)\n", " clustering = hdbscan.HDBSCAN(\n", " min_samples=min_samples,\n", " min_cluster_size=min_cluster_size,\n", " cluster_selection_method=cluster_selection_method,\n", " ).fit_predict(embedding)\n", " return clustering" ] }, { "cell_type": "markdown", "id": "f9985b20-ab5a-4001-b834-fea29fb90796", "metadata": {}, "source": [ "Next up is KMeans. We don't need much of a wrapper here -- we can call on sklearn's implementation fairly directly." ] }, { "cell_type": "code", "execution_count": 3, "id": "e3730e80-f580-41d1-a103-9f3d2440bf15", "metadata": { "execution": { "iopub.execute_input": "2026-03-25T20:36:16.811753Z", "iopub.status.busy": "2026-03-25T20:36:16.811617Z", "iopub.status.idle": "2026-03-25T20:36:16.825930Z", "shell.execute_reply": "2026-03-25T20:36:16.825498Z", "shell.execute_reply.started": "2026-03-25T20:36:16.811741Z" } }, "outputs": [], "source": [ "def kmeans(data, n_clusters=10, kmeans_algorithm=\"lloyd\"):\n", " return sklearn.cluster.KMeans(\n", " n_clusters=n_clusters, \n", " n_init=\"auto\", \n", " algorithm=kmeans_algorithm\n", " ).fit_predict(\n", " data\n", " )" ] }, { "cell_type": "markdown", "id": "e4e47881-987e-4944-a116-3acd14437b5a", "metadata": {}, "source": [ "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." ] }, { "cell_type": "code", "execution_count": 4, "id": "75a6f8bf-71eb-4053-943c-48d3f75d7052", "metadata": { "execution": { "iopub.execute_input": "2026-03-25T20:36:16.826407Z", "iopub.status.busy": "2026-03-25T20:36:16.826278Z", "iopub.status.idle": "2026-03-25T20:36:16.837721Z", "shell.execute_reply": "2026-03-25T20:36:16.837043Z", "shell.execute_reply.started": "2026-03-25T20:36:16.826395Z" } }, "outputs": [], "source": [ "def EVoC(data, test_target=None, random_state=None):\n", " if random_state is None:\n", " random_state = np.random.randint(65536)\n", " cls = evoc.EVoC(random_state=random_state).fit(data)\n", " if test_target is None:\n", " return cls.labels_\n", " result = np.full(data.shape[0], -1)\n", " best_ari = 0.0\n", " for labels in cls.cluster_layers_:\n", " ari = sklearn.metrics.adjusted_rand_score(\n", " test_target[labels >= 0], labels[labels >= 0]\n", " )\n", " if ari > best_ari:\n", " best_ari = ari\n", " result = labels\n", " return result" ] }, { "cell_type": "markdown", "id": "5d644252-ab58-4de7-9ca2-b4db23bc38af", "metadata": {}, "source": [ "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." ] }, { "cell_type": "code", "execution_count": 5, "id": "8c2abe82-1c74-45fa-b6cc-5c36647b8c74", "metadata": { "execution": { "iopub.execute_input": "2026-03-25T20:36:16.838269Z", "iopub.status.busy": "2026-03-25T20:36:16.838138Z", "iopub.status.idle": "2026-03-25T20:36:16.850442Z", "shell.execute_reply": "2026-03-25T20:36:16.849785Z", "shell.execute_reply.started": "2026-03-25T20:36:16.838257Z" } }, "outputs": [], "source": [ "def score_clustering(data, target, clustering_function, n_runs=16, **kwargs):\n", " result = np.zeros((n_runs, 5), dtype=np.float32)\n", " for i in range(n_runs):\n", " start_time = time.time()\n", " clustering = clustering_function(data, **kwargs)\n", " result[i, 0] = time.time() - start_time\n", " result[i, 1] = sklearn.metrics.adjusted_rand_score(\n", " target[clustering >= 0], clustering[clustering >= 0]\n", " )\n", " result[i, 2] = sklearn.metrics.adjusted_mutual_info_score(\n", " target[clustering >= 0], clustering[clustering >= 0]\n", " )\n", " result[i, 3] = np.sum(clustering >= 0) / clustering.shape[0]\n", " result[i, 4] = np.cbrt((result[i, 1] ** 2) * result[i, 3])\n", "\n", " result = pd.DataFrame(\n", " result,\n", " columns=(\n", " \"Elapsed time\",\n", " \"Adjusted Rand Index\",\n", " \"Adjusted Mutual Information\",\n", " \"Proportion clustered\",\n", " \"Clustering Score\",\n", " ),\n", " )\n", " result[\"algorithm\"] = clustering_function.__name__.replace(\"_\", \"\\n\")\n", " result = result.melt(\n", " id_vars=[\"algorithm\"],\n", " value_vars=[\n", " \"Elapsed time\",\n", " \"Adjusted Rand Index\",\n", " \"Adjusted Mutual Information\",\n", " \"Proportion clustered\",\n", " \"Clustering Score\",\n", " ],\n", " var_name=\"measure\",\n", " )\n", " return result" ] }, { "cell_type": "markdown", "id": "af6fd5e6-2f44-478b-9a34-b1e935b141be", "metadata": {}, "source": [ "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." ] }, { "cell_type": "code", "execution_count": 6, "id": "40c1aa9a-fb16-473a-ad2c-02ac59bad712", "metadata": { "execution": { "iopub.execute_input": "2026-03-25T20:36:16.850912Z", "iopub.status.busy": "2026-03-25T20:36:16.850774Z", "iopub.status.idle": "2026-03-25T20:36:16.862692Z", "shell.execute_reply": "2026-03-25T20:36:16.862360Z", "shell.execute_reply.started": "2026-03-25T20:36:16.850901Z" } }, "outputs": [], "source": [ "def run_dataset_benchmarks(data, target, n_runs, kmeans_kwargs, umap_hdbscan_kwargs):\n", " \"\"\"Score all three algorithms on a dataset and return combined results.\"\"\"\n", " kmeans_results = score_clustering(\n", " data, target, kmeans, n_runs=n_runs, **kmeans_kwargs\n", " )\n", " umap_results = score_clustering(\n", " data, target, umap_hdbscan, n_runs=n_runs, **umap_hdbscan_kwargs\n", " )\n", " evoc_results = score_clustering(\n", " data, target, EVoC, test_target=target, n_runs=n_runs\n", " )\n", " return pd.concat(\n", " [kmeans_results, umap_results, evoc_results.assign(algorithm=\"EVoC\")],\n", " ignore_index=True,\n", " )" ] }, { "cell_type": "markdown", "id": "9b36e588-23ee-42bf-b931-f2a797250807", "metadata": {}, "source": [ "## Image embeddings\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. " ] }, { "cell_type": "code", "execution_count": 7, "id": "2bb0aaaa-35f0-4588-ad58-466f0cae8ceb", "metadata": { "execution": { "iopub.execute_input": "2026-03-25T20:36:16.863280Z", "iopub.status.busy": "2026-03-25T20:36:16.863153Z", "iopub.status.idle": "2026-03-25T20:36:19.367650Z", "shell.execute_reply": "2026-03-25T20:36:19.367253Z", "shell.execute_reply.started": "2026-03-25T20:36:16.863268Z" } }, "outputs": [], "source": [ "from datasets import load_dataset" ] }, { "cell_type": "code", "execution_count": 8, "id": "2046299e-7e86-490b-805c-4ccc92088877", "metadata": { "execution": { "iopub.execute_input": "2026-03-25T20:36:19.368857Z", "iopub.status.busy": "2026-03-25T20:36:19.368555Z", "iopub.status.idle": "2026-03-25T20:36:49.643729Z", "shell.execute_reply": "2026-03-25T20:36:49.642787Z", "shell.execute_reply.started": "2026-03-25T20:36:19.368842Z" } }, "outputs": [], "source": [ "ds_cifar = load_dataset(\"lmcinnes/evoc_bench_cifar100\")\n", "cifar_data = np.asarray(ds_cifar[\"train\"][\"embedding\"])\n", "cifar_target = np.asarray(ds_cifar[\"train\"][\"target\"])" ] }, { "cell_type": "markdown", "id": "44a1a510-281d-493c-b9fb-38e89d68cd99", "metadata": {}, "source": [ "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." ] }, { "cell_type": "code", "execution_count": 9, "id": "3fa73eb5-68c7-49be-80a7-bd3527264111", "metadata": { "execution": { "iopub.execute_input": "2026-03-25T20:36:49.644685Z", "iopub.status.busy": "2026-03-25T20:36:49.644502Z", "iopub.status.idle": "2026-03-25T20:42:55.905457Z", "shell.execute_reply": "2026-03-25T20:42:55.904368Z", "shell.execute_reply.started": "2026-03-25T20:36:49.644670Z" } }, "outputs": [], "source": [ "cifar_results = run_dataset_benchmarks(\n", " cifar_data, \n", " cifar_target, \n", " n_runs=16, \n", " kmeans_kwargs={\"n_clusters\":125}, \n", " umap_hdbscan_kwargs={\n", " \"min_samples\":5,\n", " \"min_cluster_size\":120, \n", " \"metric\":\"cosine\", \n", " \"cluster_selection_method\":\"leaf\"\n", " }\n", ")" ] }, { "cell_type": "markdown", "id": "83150948-cc2d-429f-a913-eac665250739", "metadata": {}, "source": [ "Before we look at quality, let's compare how long these different approaches took to run:" ] }, { "cell_type": "code", "execution_count": 10, "id": "9233e308-90ed-4c57-8a1e-738a7a7e898f", "metadata": { "execution": { "iopub.execute_input": "2026-03-25T20:42:55.906750Z", "iopub.status.busy": "2026-03-25T20:42:55.906539Z", "iopub.status.idle": "2026-03-25T20:42:56.203555Z", "shell.execute_reply": "2026-03-25T20:42:56.202959Z", "shell.execute_reply.started": "2026-03-25T20:42:55.906732Z" } }, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" }, { "data": { "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==", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "sns.catplot(\n", " cifar_results[cifar_results.measure == \"Elapsed time\"], \n", " x=\"algorithm\", \n", " y=\"value\", \n", " hue=\"algorithm\", \n", " kind=\"swarm\", \n", " col=\"measure\",\n", " height=8,\n", ")" ] }, { "cell_type": "markdown", "id": "eface2b3-8171-4d9e-b1a1-585af3e31990", "metadata": {}, "source": [ "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", "How about the quality of the clusterings we get out?" ] }, { "cell_type": "code", "execution_count": 11, "id": "6c0bcb69-473f-417f-b438-6fc5894d2159", "metadata": { "execution": { "iopub.execute_input": "2026-03-25T20:42:56.204347Z", "iopub.status.busy": "2026-03-25T20:42:56.204186Z", "iopub.status.idle": "2026-03-25T20:42:56.875156Z", "shell.execute_reply": "2026-03-25T20:42:56.874734Z", "shell.execute_reply.started": "2026-03-25T20:42:56.204333Z" } }, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" }, { "data": { "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/y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9VcdOHDAflkdNWqUDY5MQGS+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/+evvl2nQNRcZlfsBJmczq7Nmzqb7uzveipzLne8HkF5g7d659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", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "sns.catplot(\n", " cifar_results[cifar_results.measure != \"Elapsed time\"], \n", " x=\"algorithm\", \n", " y=\"value\", \n", " hue=\"algorithm\", \n", " col=\"measure\", \n", " kind=\"swarm\", \n", " col_wrap=2,\n", " height=5,\n", ")" ] }, { "cell_type": "markdown", "id": "02e215d9-75bf-4395-9dfd-08b36606eff8", "metadata": {}, "source": [ "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." ] }, { "cell_type": "markdown", "id": "92b4f389-c9b3-4c57-ac92-c2be35f12133", "metadata": {}, "source": [ "## Text embeddings\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. " ] }, { "cell_type": "code", "execution_count": 12, "id": "71bad192-181f-4586-8241-d2674a5101ce", "metadata": { "execution": { "iopub.execute_input": "2026-03-25T20:42:56.875997Z", "iopub.status.busy": "2026-03-25T20:42:56.875696Z", "iopub.status.idle": "2026-03-25T20:43:02.962330Z", "shell.execute_reply": "2026-03-25T20:43:02.961712Z", "shell.execute_reply.started": "2026-03-25T20:42:56.875982Z" } }, "outputs": [], "source": [ "ds_news = load_dataset(\"lmcinnes/evoc_bench_20newsgroups\")\n", "news_data = np.asarray(ds_news[\"train\"][\"embedding\"])\n", "news_target = np.asarray(ds_news[\"train\"][\"target\"])" ] }, { "cell_type": "markdown", "id": "22404769-36a7-4aff-bc63-49e1262daed1", "metadata": {}, "source": [ "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." ] }, { "cell_type": "code", "execution_count": 13, "id": "7d5837d7-97b5-4c25-b596-1fb6b6e5a7d0", "metadata": { "execution": { "iopub.execute_input": "2026-03-25T20:43:02.963427Z", "iopub.status.busy": "2026-03-25T20:43:02.963202Z", "iopub.status.idle": "2026-03-25T20:45:35.272333Z", "shell.execute_reply": "2026-03-25T20:45:35.271486Z", "shell.execute_reply.started": "2026-03-25T20:43:02.963411Z" } }, "outputs": [], "source": [ "news_results = run_dataset_benchmarks(\n", " news_data, \n", " news_target, \n", " n_runs=32, \n", " kmeans_kwargs={\"n_clusters\":25}, \n", " umap_hdbscan_kwargs={\n", " \"min_samples\":5,\n", " \"min_cluster_size\":180, \n", " \"metric\":\"cosine\", \n", " \"cluster_selection_method\":\"leaf\"\n", " }\n", ")" ] }, { "cell_type": "markdown", "id": "a6357957-1518-497f-b3e9-d54583be7dcc", "metadata": {}, "source": [ "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?" ] }, { "cell_type": "code", "execution_count": 14, "id": "b64c2dee-159b-48cd-b7c0-cfb5766d4cc4", "metadata": { "execution": { "iopub.execute_input": "2026-03-25T20:45:35.273371Z", "iopub.status.busy": "2026-03-25T20:45:35.273197Z", "iopub.status.idle": "2026-03-25T20:45:35.523724Z", "shell.execute_reply": "2026-03-25T20:45:35.523041Z", "shell.execute_reply.started": "2026-03-25T20:45:35.273356Z" } }, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" }, { "data": { "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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", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "sns.catplot(\n", " news_results[news_results.measure == \"Elapsed time\"], \n", " x=\"algorithm\", \n", " y=\"value\", \n", " hue=\"algorithm\", \n", " kind=\"swarm\", \n", " col=\"measure\",\n", " height=8,\n", ")" ] }, { "cell_type": "markdown", "id": "5b049931-a1ea-4e16-8832-ab39fda93e5d", "metadata": {}, "source": [ "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?" ] }, { "cell_type": "code", "execution_count": 15, "id": "55978199-7589-48de-b8b8-ce479f09286a", "metadata": { "execution": { "iopub.execute_input": "2026-03-25T20:45:35.524356Z", "iopub.status.busy": "2026-03-25T20:45:35.524201Z", "iopub.status.idle": "2026-03-25T20:45:36.389650Z", "shell.execute_reply": "2026-03-25T20:45:36.389123Z", "shell.execute_reply.started": "2026-03-25T20:45:35.524339Z" } }, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" }, { "data": { "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==", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "sns.catplot(\n", " news_results[news_results.measure != \"Elapsed time\"], \n", " x=\"algorithm\", \n", " y=\"value\", \n", " hue=\"algorithm\", \n", " col=\"measure\", \n", " kind=\"swarm\", \n", " col_wrap=2,\n", " height=5,\n", ")" ] }, { "cell_type": "markdown", "id": "2bb61a83-1f7d-48e2-9c46-6d1b98699e42", "metadata": {}, "source": [ "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. " ] }, { "cell_type": "markdown", "id": "41647cad-0221-42b0-a06a-7d96529de4b0", "metadata": {}, "source": [ "## Audio embeddings\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." ] }, { "cell_type": "code", "execution_count": 16, "id": "c56939cb-662a-445e-9f11-2266c46b7aa5", "metadata": { "execution": { "iopub.execute_input": "2026-03-25T20:45:36.390262Z", "iopub.status.busy": "2026-03-25T20:45:36.390117Z", "iopub.status.idle": "2026-03-25T20:45:42.425450Z", "shell.execute_reply": "2026-03-25T20:45:42.424318Z", "shell.execute_reply.started": "2026-03-25T20:45:36.390248Z" } }, "outputs": [], "source": [ "ds_birdclef = load_dataset(\"Syoy/birdclef_2023_train\")\n", "birdclef2023_data = np.asarray(ds_birdclef[\"train\"][\"embeddings\"])\n", "birdclef2023_target = np.asarray(ds_birdclef[\"train\"][\"primary_label\"])\n", "# Only use bird species with at least 100 samples -- this is still very challenging\n", "mask = np.isin(\n", " birdclef2023_target,\n", " np.where(np.bincount(birdclef2023_target) > 100)[0],\n", ")\n", "birdclef2023_data = birdclef2023_data[mask]\n", "birdclef2023_target = birdclef2023_target[mask]" ] }, { "cell_type": "markdown", "id": "33a72cf7-6a1e-40b1-bcc5-092b2762d50b", "metadata": {}, "source": [ "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." ] }, { "cell_type": "code", "execution_count": 17, "id": "2ca28659-046b-407a-be97-ab334d019473", "metadata": { "execution": { "iopub.execute_input": "2026-03-25T20:45:42.426718Z", "iopub.status.busy": "2026-03-25T20:45:42.426496Z", "iopub.status.idle": "2026-03-25T20:46:41.283049Z", "shell.execute_reply": "2026-03-25T20:46:41.282152Z", "shell.execute_reply.started": "2026-03-25T20:45:42.426697Z" } }, "outputs": [], "source": [ "bird_results = run_dataset_benchmarks(\n", " birdclef2023_data, \n", " birdclef2023_target, \n", " n_runs=16, \n", " kmeans_kwargs={\"n_clusters\":130}, \n", " umap_hdbscan_kwargs={\n", " \"min_samples\":5,\n", " \"min_cluster_size\":100, \n", " \"metric\":\"cosine\", \n", " \"cluster_selection_method\":\"leaf\"\n", " }\n", ")" ] }, { "cell_type": "markdown", "id": "15082cc4-86ef-491d-affd-d066a88f9c23", "metadata": {}, "source": [ "As always we'll start with timing results." ] }, { "cell_type": "code", "execution_count": 18, "id": "c2e9db88-dc9b-42cd-a2a7-7e442b6b57e8", "metadata": { "execution": { "iopub.execute_input": "2026-03-25T20:46:41.283929Z", "iopub.status.busy": "2026-03-25T20:46:41.283731Z", "iopub.status.idle": "2026-03-25T20:46:41.488356Z", "shell.execute_reply": "2026-03-25T20:46:41.487783Z", "shell.execute_reply.started": "2026-03-25T20:46:41.283914Z" } }, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 18, "metadata": {}, "output_type": "execute_result" }, { "data": { "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==", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "sns.catplot(\n", " bird_results[bird_results.measure == \"Elapsed time\"], \n", " x=\"algorithm\", \n", " y=\"value\", \n", " hue=\"algorithm\", \n", " kind=\"swarm\", \n", " col=\"measure\",\n", " height=8,\n", ")" ] }, { "cell_type": "markdown", "id": "de1f4f88-09e3-4627-aac5-a56181a67ef8", "metadata": {}, "source": [ "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", "How about clustering quality?" ] }, { "cell_type": "code", "execution_count": 19, "id": "6a0814a6-36cb-45a8-90fd-fa2e9b25d45f", "metadata": { "execution": { "iopub.execute_input": "2026-03-25T20:46:41.488922Z", "iopub.status.busy": "2026-03-25T20:46:41.488756Z", "iopub.status.idle": "2026-03-25T20:46:42.125507Z", "shell.execute_reply": "2026-03-25T20:46:42.124937Z", "shell.execute_reply.started": "2026-03-25T20:46:41.488907Z" } }, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" }, { "data": { "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+uGfuzYMV93YNvX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", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "sns.catplot(\n", " bird_results[bird_results.measure != \"Elapsed time\"], \n", " x=\"algorithm\", \n", " y=\"value\", \n", " hue=\"algorithm\", \n", " col=\"measure\", \n", " kind=\"swarm\", \n", " col_wrap=2,\n", " height=5,\n", ")" ] }, { "cell_type": "markdown", "id": "368d1989-4016-47e0-b8eb-ccfed6a90b18", "metadata": {}, "source": [ "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." ] }, { "cell_type": "markdown", "id": "478fc28c-48fa-4a4a-b45d-7dd94ecd8cd8", "metadata": {}, "source": [ "## Other high dimensional data\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." ] }, { "cell_type": "code", "execution_count": 20, "id": "3d3f6754-d952-45fd-b96b-1644c141b26b", "metadata": { "execution": { "iopub.execute_input": "2026-03-25T20:46:42.126177Z", "iopub.status.busy": "2026-03-25T20:46:42.126019Z", "iopub.status.idle": "2026-03-25T20:46:42.142323Z", "shell.execute_reply": "2026-03-25T20:46:42.141841Z", "shell.execute_reply.started": "2026-03-25T20:46:42.126164Z" } }, "outputs": [], "source": [ "from sklearn.datasets import fetch_openml" ] }, { "cell_type": "code", "execution_count": 21, "id": "0abebd46-634b-45ed-96e3-beb7ba44d5f6", "metadata": { "execution": { "iopub.execute_input": "2026-03-25T20:46:42.142955Z", "iopub.status.busy": "2026-03-25T20:46:42.142804Z", "iopub.status.idle": "2026-03-25T20:46:45.795859Z", "shell.execute_reply": "2026-03-25T20:46:45.795225Z", "shell.execute_reply.started": "2026-03-25T20:46:42.142942Z" } }, "outputs": [], "source": [ "mnist_ds = fetch_openml('mnist_784')\n", "mnist_data = mnist_ds.data.values.astype(np.float32, order=\"C\")\n", "mnist_target = mnist_ds.target.values.astype(np.uint8)" ] }, { "cell_type": "markdown", "id": "2e5aeed4-a19a-410c-a580-8aa7fe515388", "metadata": {}, "source": [ "We can run the benchmarks. This time parameter tuning is a little easier." ] }, { "cell_type": "code", "execution_count": 22, "id": "7eebb0b3-c2df-4e64-ba75-6594da5382f3", "metadata": { "execution": { "iopub.execute_input": "2026-03-25T20:46:45.796609Z", "iopub.status.busy": "2026-03-25T20:46:45.796456Z", "iopub.status.idle": "2026-03-25T20:51:16.994356Z", "shell.execute_reply": "2026-03-25T20:51:16.993592Z", "shell.execute_reply.started": "2026-03-25T20:46:45.796595Z" } }, "outputs": [], "source": [ "mnist_results = run_dataset_benchmarks(\n", " mnist_data, \n", " mnist_target, \n", " n_runs=16, \n", " kmeans_kwargs={\"n_clusters\":10}, \n", " umap_hdbscan_kwargs={\n", " \"min_samples\":5,\n", " \"min_cluster_size\":1200, \n", " \"metric\":\"cosine\", \n", " \"cluster_selection_method\":\"leaf\"\n", " }\n", ")" ] }, { "cell_type": "markdown", "id": "23339d19-8803-4f06-806b-474a32e0f1a9", "metadata": {}, "source": [ "As always let's begin with time taken to compute the clusterings:" ] }, { "cell_type": "code", "execution_count": 23, "id": "6eb39efd-0a6c-4505-b9ed-c369210056f2", "metadata": { "execution": { "iopub.execute_input": "2026-03-25T20:51:16.995048Z", "iopub.status.busy": "2026-03-25T20:51:16.994885Z", "iopub.status.idle": "2026-03-25T20:51:17.193178Z", "shell.execute_reply": "2026-03-25T20:51:17.192628Z", "shell.execute_reply.started": "2026-03-25T20:51:16.995034Z" } }, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" }, { "data": { "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", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "sns.catplot(\n", " mnist_results[mnist_results.measure == \"Elapsed time\"], \n", " x=\"algorithm\", \n", " y=\"value\", \n", " hue=\"algorithm\", \n", " kind=\"swarm\", \n", " col=\"measure\",\n", " height=8,\n", ")" ] }, { "cell_type": "markdown", "id": "eb2378ce-abfd-42e2-add6-c1e4287ec40b", "metadata": {}, "source": [ "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", "How about the clustering quality? This is the kind of high dimensional dataset that KMeans can tend to struggle with." ] }, { "cell_type": "code", "execution_count": 30, "id": "9168cb00-a657-44d6-a990-22414b2456ce", "metadata": { "execution": { "iopub.execute_input": "2026-03-26T17:37:39.604436Z", "iopub.status.busy": "2026-03-26T17:37:39.604204Z", "iopub.status.idle": "2026-03-26T17:37:40.255561Z", "shell.execute_reply": "2026-03-26T17:37:40.254937Z", "shell.execute_reply.started": "2026-03-26T17:37:39.604404Z" } }, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 30, "metadata": {}, "output_type": "execute_result" }, { "data": { "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/2qD9KBBg7Ry5Urt2LHD1mx7wn2jRo00f/58G8zNOQwZMoQabABAsUOZCpROhG4AZ/6QCA62QTo1NVWdOnXSrbfeapt5+/Lkk09qypQpatOmjQ3nJmR7RigPCwvThAkTbK22mX7MjIZu+nh7ti1dulTVq1e3o5Sb2mxzLLOP8eyzz6py5cp2RHQzarkZvbxdu3a8cwCAYoUyFSidgsxoav4+CQAAAAAASiJqugEAAAAAcAihGwAAAAAAhxC6AQAAAABwCKEbAAAAAACHELoBAAAAAHAIoRsAAAAAAIcQugEAAAAAcAihGwAAAAAAhxC6AXjZtWuXgoKC9MMPPwTMY/Xp00ejR492/HwAAChslKsACN0A/KZu3bpKSEhQq1at7PKXX35pQ/jhw4d5VwAAoFwFSoRQf58AgNIpLS1NYWFhqlGjhr9PBQCAYo9yFQhc1HQDpdDixYvVo0cPVapUSVWrVtXll1+u7du357n/ggUL1LhxY0VEROjCCy/U7Nmzc9VIz5s3Ty1btlTZsmVVv359Pf30017HMOseffRRDRs2TFFRUbrtttu8mtyZv82xjcqVK9v1Zl+PzMxM3XvvvapSpYoN6g899JDX8c3+M2bMsM+lXLlyat68uVavXq1ff/3VNk8vX768unbtesbnCQBAQVCuAjgjF4BS5/3333fNmzfPtXXrVtf69etdAwcOdLVu3dqVkZHh2rlzp8t8NJj1hlkuU6aMa/z48a7Nmze73n77bVft2rXtPocOHbL7rF271hUcHOyaPHmya8uWLa7XXnvNFRERYf/1iI2NdUVGRrqmTp3q2rZtm71lf6z09HR7TmbZHCMhIcF1+PBhe9/evXvb+z700EP2nGfPnu0KCgpyLV26NOv45n7mvObOnWvvP2jQIFf9+vVdF110kWvx4sWujRs3urp06eK67LLLivz1BgCUbJSrAM6E0A3AlZiYaEPrhg0bcoXu++67z9WqVSuvV2nixIleoXvIkCGuiy++2Gufe+65x9WiRQuv0G2CcHY5H+uLL77wOq6HCd09evTwWtexY0d7blkfZpLrgQceyFpevXq1Xffqq69mrTM/GISHh/OOAwAcRbkKIDualwOlkGliPWTIEJ133nmKjIxUgwYN7Pq4uLhc+27ZskUdO3b0WtepUyev5U2bNql79+5e68zytm3blJGRkbWuQ4cOBT7n888/32u5Zs2aSkxMzHOfmJgY+2/r1q291p08eVIpKSkFPg8AAHKiXKVcBc6EgdSAUmjgwIF25PCXX35ZtWrVsv2lzQjiZhCWnEwlsukvnXNdfvcxTL/qgipTpozXsnk8c9557eM5H1/rct4PAIBzQblKuQqcCaEbKGWSkpJszbQZdKxnz5523ddff53n/s2aNdPChQu91q1du9ZruUWLFrmOsWrVKjVp0kQhISFnfW5mNHMje+04AACBjHIVwB+heTlQypiRwc2I5TNnzrQje3/++ecaO3Zsnvvfcccd2rx5s+677z5t3bpV7777rl5//XWvmuNx48bpv//9rx555BG7jxnd/MUXX9T48ePzdW6xsbH2mJ988ol+//13HT169ByfLQAAzqJcBfBHCN1AKRMcHKx33nlH69ats03Kx4wZo6lTp+a5v+nv/f7772v+/Pm2z/T06dM1ceJEu81MD2a0a9fOhnFzXHPMBx98UJMnT/aa8uts1K5dWw8//LDuv/9+2//6rrvuOsdnCwCAsyhXAfyRIDOa2h/uBQDZPPbYY3rppZcUHx/P6wIAwDmiXAVKNvp0A/hD06ZNsyOYm2bpK1eutDXj1EIDAFAwlKtA6ULoBvCHzNRfjz76qA4ePKh69erZPtwTJkzglQMAoAAoV4HSheblAAAAAAA4hIHUAAAAAABwCKEbAAAAAACHELoBAAAAAHAIoRsAAAAAAIcQugEAAAAAcAihGwAAAAAAhxC6AQAAAABwCKEbAAAAAACHELoBAAAAAJAz/h+jFLvO+R7b1QAAAABJRU5ErkJggg==", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "sns.catplot(\n", " mnist_results[mnist_results.measure != \"Elapsed time\"], \n", " x=\"algorithm\", \n", " y=\"value\", \n", " hue=\"algorithm\", \n", " col=\"measure\", \n", " kind=\"swarm\", \n", " col_wrap=2,\n", " height=5,\n", ")" ] }, { "cell_type": "markdown", "id": "22bc9bf2-9559-455f-b0d7-32446de322e3", "metadata": {}, "source": [ "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", "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." ] }, { "cell_type": "markdown", "id": "e573b8a4-b0b5-42e9-bebc-3cf5cef8f646", "metadata": {}, "source": [ "## Scaling\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." ] }, { "cell_type": "code", "execution_count": 25, "id": "b4191c21-48a8-40ad-b547-061ade561ea2", "metadata": { "execution": { "iopub.execute_input": "2026-03-25T20:51:17.870049Z", "iopub.status.busy": "2026-03-25T20:51:17.869881Z", "iopub.status.idle": "2026-03-25T20:51:17.970759Z", "shell.execute_reply": "2026-03-25T20:51:17.970234Z", "shell.execute_reply.started": "2026-03-25T20:51:17.870034Z" } }, "outputs": [], "source": [ "from sklearn.datasets import make_blobs\n", "import faiss" ] }, { "cell_type": "markdown", "id": "621629aa-1c48-4e83-b2f9-abf767676423", "metadata": {}, "source": [ "We need similar function wrappers for ``MiniBatchKMeans`` and FAISS: " ] }, { "cell_type": "code", "execution_count": 26, "id": "5075cd5e-fc1a-4b26-b8dd-895aa2156ba2", "metadata": { "execution": { "iopub.execute_input": "2026-03-25T20:51:17.971578Z", "iopub.status.busy": "2026-03-25T20:51:17.971423Z", "iopub.status.idle": "2026-03-25T20:51:17.974750Z", "shell.execute_reply": "2026-03-25T20:51:17.974323Z", "shell.execute_reply.started": "2026-03-25T20:51:17.971564Z" } }, "outputs": [], "source": [ "def minibatch_kmeans(data, n_clusters=10):\n", " return sklearn.cluster.MiniBatchKMeans(\n", " n_clusters=n_clusters, \n", " n_init=\"auto\", \n", " batch_size=4*n_clusters\n", " ).fit_predict(\n", " data\n", " )\n", "\n", "def faiss_kmeans(data, n_clusters=10):\n", " kmeans = faiss.Kmeans(data.shape[1], n_clusters, niter=50, nredo=1, gpu=False)\n", " X = np.ascontiguousarray(data, dtype=np.float32)\n", " kmeans.train(X)\n", " _, labels = kmeans.index.search(X, 1)\n", " return labels.ravel()" ] }, { "cell_type": "markdown", "id": "0ce50a1f-73cc-4c26-9f05-fe807aafa04b", "metadata": {}, "source": [ "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." ] }, { "cell_type": "code", "execution_count": 27, "id": "8d2312a4-6f73-4d8d-b260-0a4529a67408", "metadata": { "execution": { "iopub.execute_input": "2026-03-25T20:51:17.975285Z", "iopub.status.busy": "2026-03-25T20:51:17.975154Z", "iopub.status.idle": "2026-03-26T09:38:20.359235Z", "shell.execute_reply": "2026-03-26T09:38:20.357749Z", "shell.execute_reply.started": "2026-03-25T20:51:17.975274Z" } }, "outputs": [], "source": [ "algorithms = [\n", " (\"UMAP + HDBSCAN\", lambda X: umap_hdbscan(X)),\n", " (\"Sklearn KMeans\", lambda X: kmeans(X, n_clusters=n_clusters, kmeans_algorithm=\"elkan\")),\n", " (\"FAISS KMeans\", lambda X: faiss_kmeans(X, n_clusters=n_clusters)),\n", " (\"Sklearn Minibatch KMeans\",lambda X: minibatch_kmeans(X, n_clusters=n_clusters)),\n", " (\"EVoC\", lambda X: EVoC(X)),\n", "]\n", "\n", "scaling_results = {\"size\": [], \"time\": [], \"algorithm\": [], \"ari\": []}\n", "n_runs = 4\n", "\n", "for size in np.logspace(4, 6.5, num=8):\n", " for n in range(n_runs):\n", " n_clusters = int(2 * np.sqrt(size))\n", " blobs, labels = make_blobs(n_samples=int(size), n_features=1024, centers=n_clusters, cluster_std=3.0)\n", " \n", " for name, fn in algorithms:\n", " start_time = time.time()\n", " clusters = fn(blobs)\n", " scaling_results[\"size\"].append(size)\n", " scaling_results[\"algorithm\"].append(name)\n", " scaling_results[\"time\"].append(time.time() - start_time)\n", " scaling_results[\"ari\"].append(sklearn.metrics.adjusted_rand_score(labels, clusters))\n", "\n", "scaling_df = pd.DataFrame(scaling_results)" ] }, { "cell_type": "markdown", "id": "956dc26c-ab67-454e-b9f6-77ea85341ff4", "metadata": {}, "source": [ "Now let's look at the timing results -- how does the runtime scale with increasing dataset size for these different algorithms?" ] }, { "cell_type": "code", "execution_count": 28, "id": "e4ffa7e1-889a-494d-8548-a48500176dda", "metadata": { "execution": { "iopub.execute_input": "2026-03-26T09:38:20.360185Z", "iopub.status.busy": "2026-03-26T09:38:20.359995Z", "iopub.status.idle": "2026-03-26T09:38:21.088775Z", "shell.execute_reply": "2026-03-26T09:38:21.088224Z", "shell.execute_reply.started": "2026-03-26T09:38:20.360170Z" } }, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 28, "metadata": {}, "output_type": "execute_result" }, { "data": { "image/png": "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05DiXNlsGRx2BVozf0j9kqel126NrYuGRGZ2FQ1N3TWZwKNkBGFXhmoJtOjfX/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+9Kr1wjdS2JdjMHRBpYxw4JsVh1wjCrgwJxt9Y/4b+sugv+rjq4+1uN/O4Zo2dpRPHnmiFW8znQiqcfvrpHdZ/+tOfVF5eriVLlqiwsLDDbb///e81ceJEK6wyzMeLFi3SrbfGZhvefffdOvroo6Mh1q677mo9lrnP1lVaRx11lC677LIOYVo7u91uVW8Z5lq2neG122676YYbbrA+NtVm999/vxXWHXvssdFzfvSjH1nBm3HFFVdYFV3mmo4//njr2IUXXmidg76T4T/RAAAAZL8aj08eb9fCrndW1ehXL8R2XBtakqs7T99Nhe7EP/YNKnCpKDfDd9zL9CH1q9+QFlwZa8V0l0R2Yywdl7prorIrIwTDQc37ap614+LK+pXb3T60YKhOGX+Kjh51tAa4BzCfCyll2vpMEPTee++ppqYmWtm1du1aTZkypcO5ptpq33337XBsv/3267BeunSpTj755A7HDj74YGtXxVAoZIVZxj777LPT12wCr60NHTpUVVVVcc+pqKiw3k+fPr3DMa/Xq8bGRhUXF+/0taDrCLwAAADSWH2LX42tgS6du3B9g26at1ThLWOfBuY7dde3drPCrERK8pwakJ/hrWrWkPqNmTukfs1b0nOXS+Etwaa7SDr5t1LZrqm7JjPrzWpjdKfuGrDDHRf/tfxfemTZI9rUvGm728cPGK9vTfiWDhx+oFXNVeIqoWURKTdr1iyrhfAPf/iDhg0bZgVeptXQDJLvrGpx27mT5lh3zzFMS+LOcjo7/oOQeb5tWzC3Pqf9ejo71lnrJnoHgRcAAECaavQGtLl5+18AOvNVtUfXPLUoOtC+wGW3KruGD0i8y5qp/CotzPBAIzqkPkN/ifj6HWn+ZVJ4S7DpKpBm3y+VT07dNRF2ZcSOi//58j9q8Ddsd/te5Xvp9Amna4/Be6jIXWSFXXaz6QCQYrW1tVZFlhnwfuihh1rH3nrrrbjnm1lY8+fP73CsfR5WO1MVtu1jvPPOO1ZrY3t1V1eYXRsNUxWG7EDgBQAAkIY8vqBqmro2eH1Tg1dXPL7Quo/htOfollOmaXx5x1ko28pz2TW4KMPDLsNTlblD6te+J82/NBZ2OU3Y9VupYlrqwy5nbuquAZ1a17hOf170Z6t90RvydrjN2nFx+GE6bcJpmjBwgtW2WOQsSrgrK9DXzA6IZmfGhx56yGoLNG2MV155ZdzzzdB3M6PLzMT6yU9+Yg2Ebx9G3/69femll1ptj2Zo/He/+11rd0QzY+t3v/tdt65tl112sR5z3rx5OvHEE61dIbedKYbMwi6NAAAAaabVH1J1F8Ouuha/Ln/8C9VuqQQzmzBee9IU7T6y48DdbZkB9hVFuZn/y3BrneRrUkZa94H07CVSaEsVnzNfmv2/0pDYzJfUhF3DCLvSzJLaJbrktUs066lZ+s+K/3QIu8yOi2YQ/UPHPKSr9r9K+w3dTyOKRlhVXRn/5xtZx+zI+Oijj1o7JZo2xosvvjg6kL4zY8aM0X/+8x898cQT1oysBx54ILpLo9sd+QebvfbaS4899pj1uOYxr7/+emsXxa0H1nfF8OHDdeONN1oBnJm3xY6KmS+nrbPmVmzHDJYrKSlRQ0MDA+YAAECv8QZCVsVWuAs/ojX7grr4sc+1ssoTPXbpsbvqpN2GJryf027TsAF5spt0LJP5W6TGDcpI6z+S5l0Qq0xz5kmz/lcatmcahF2J22DRd9755h39ZfFf9N7G97a7rchVZAVdJ409SeX55SpxlyjPwdcO2c/s0Gh2b1y3bl2qLwVpjpZGAACANGHmb1U2di3sMude9/SiDmHX/xwyZodhlwm5KopzMz/sCgUkz/ZDujPCN59I8y6MhV2OXGnmbwi7YAmFQ3p+zfP625K/WZVd2yrPK9epE07VMaOOUWleqdW6aKq8gGxlWhNNy6JphXz77betijCqr9AVBF4AAABpIBgKW5VdofYtFhMw59zy7FJ9ti42rPpbew/Xf+03MuH9bDmRsMu0M2Y0M5zeVHZl4pD6DZ9uqeza0pJmdkCceZ80fK/UXROVXWnBG/TqyRVP6v+W/p/WNq3d7vbRxaOtQfSHjjhUA9wDrKDLaeu4cxyQjVasWKFbbrlFmzdv1qhRo6yZXVdddVWqLwsZgJbGLqKlEQAA9BYTYG2ob1UgtOMAx0yj+PULX2r+olh107FTKnTFjIlWoBWPmeVTUexWvisL/r2zcaPkb1bG2fiFNPdcKdASWZuqnJn3SiP3S901me+ZoqGSKz9119DPNfoa9ciyR/TY8sdU3Vq93e3TSqfp9F1P134V+0UG0buK2HERALogC37iAQAAyFzhcJs2NXq7FHYZf3xrdYew64Cxg/SL43ZNGHYZZYWu7Ai7WjZncNh13lZhl0s66deEXf3YJs8m/X3J3/XUyqfUFOi48UKOcnTA0AOsiq6pZVPZcREAdkIW/NQDAACQmUy1VmWTV75AqEvnP/bROv3zg9iQ3unDi3XDzCly2BO3KJYWuFWUmwWtTz5PJPDKNJsWbgm7tgR1pg3txF9Low5M3TVR2ZUyq+pX6W+L/6b5q+fLF+q4G6sjx6EjRx5pzegaP2C8NYg+3+zeCQDoNgIvAACAFKlu8qnV37Ww6/nFm/T717+KrscOLtCtp0yX22lPeL+SPKdK8rMg7DID3j2Vysyw69yOYZep7NrloNRdE2FXnwu3hfVF9RdW0PXqulcVauv4597srnj86ON18riTNbJopFXRxY6LANAzBF4AAAApCrs8vmCXzn1nVY1++fzy6HpoSa7uPG26CnMT/yhnbi8tzILd28IhqWmjKYlTRoZd/q0ru34l7XJw6q6JsKtPBcNBvbfhPWvHxfc2vrfd7SWuEs0aN0snjTlJFQUVVkWXy7S7AgB6jMALAACgj21u9qvJG+jSuZ+vr9dN85aqffPGgflO3fWt3XYYZJl5XYOzIewyIVfTJinUtXAwbVQu6iTs+qU0+pDUXRO7MfYZf9Cvl9a+pIeXPqxFNYu2u708r1ynTThNx+xyjMryythxEQB6AYEXAABAH2poDai+xd+lc1dUNunaJxfJH4wMtC9w2XXn6btp+IC8hPczbY7lRW5rZ8aM11wjBVqVUSoXS09vHXY5pBPukkYfmuKwa7jkzE3dNfQDHr9H876ap38u+6e+aoi1ILfbpXgXfWvCt3T4iMM1IHeAil3F7LgIAL2EwAsAAKCPmKquWk/HIdXxrNvcoiseX6jmLTO+XA6bbj11msaXFya8n9Nu05DiXNlsWRB2eRsib5mkcon09M8lv2ersOuX0pjDUndNNlsk7HJkQcVfmqrz1unxLx/XY18+po3NG7e7ffKgyfr2rt/W/kP3t9oWi1xFsuUk3mwCANAz/F8WAACgD7T4g6rx+Ls83+sX//lC9a2RtkeTXZndGHcbMSDh/Rw2m4aU5MqeDWGXqeoy1V2ZpGqpNHfbsOuuNAi7RhB29dIuqybcuvfje3XK06fovk/v2y7s2qdiH91x6B2658h7dMKYE6yB9CbwIuzqf8LhNi1c36DXv6y23pt1bzriiCN00UUXbXf8qaee6lD9+9e//tVaT548ebtzH3vsMeu20aNHb3dba2urBg4cqEGDBlkfb8vcx9zXvOXn52vatGl68MEH1ZvMc957773bHZ8zZ4722GOPDuv2a3M4HCorK9Nhhx1m3dfn8233Orafa7PZVFFRoW9/+9v6+uuvo+eEQiHdfvvtmjRpkvLy8qzX5IADDtBf/vKXDo+1adMmnX/++Ro7dqzcbrdGjhypWbNm6eWXX97umm+77TbZ7Xbdcccd293W/jWbMWNGh+P19fXW8ddee62br1z2osILAACgl3kDIVU2+qxfkHekoSWgy//zhaqaYj90X3nCJB04rjTh/Ww5OaoocVsVXhnPzOvKtCH1prLLhF2+pljYNeNOaczhqbsmm31LZRdD0JMpFA5pXdM6PbL0ET3z1TPyBDwdX3bZdOiIQ3X6hNM1adAkK+DKd+Yn9RqQWd5ZWaMHXl+lVVUeBUJtctpzNK68UD87fJwOGl+W6stTQUGBqqqq9O677+rAAw+MHv/zn/+sUaNGdXqfxx9/3AqxzN9rTzzxhL7//e9vd85NN92ks88+Wx6Pxwpp/t//+38aMGCAvvvd7+7wmtasWaMxY8Z06e/NnTF16lS99NJLCofDqq2ttUKiW265RQ8//LD1cVFRUfRc8zmYz8Vciwm6TJD43//933rzzTejAdpDDz2k+++/X/vss48aGxv10Ucfqa6ursPnc/DBB1uf/1133aXddttNgUBAzz//vM4991wtW7asw/WZsOzyyy+3vgZXXnnldtdvgjoTlL366qs68sgje+U16reB16pVq6wvgHl/3333qby8XAsWLLASSvONAwAAgFjYtanB26Uf2k0V2JVPLtTXm1uix847cryOmVyR8H7mX3RNZZfbYc+SIfUbTDmEMjfsskfCrrFHpO6a7I5I2GV3pu4askwgFNCK+hX6x9J/6Pk1z8sX6lgJ4rQ5dcyoY6xh9GNKxlhBV66DmWn9nQm7rn5yobUr78B8l1x2m/yhsJZubLKO33bq9JSHXiY8OeOMM6xwpT3wWr9+vRX8XHzxxfrnP/+53X3+9Kc/WaGP+bvNfNxZ4GVCoyFDhlgfmzDJVIyZCrOuBF598Tm3X9uwYcM0ffp0HXvssdp999115513WtfbzlSotZ87dOhQK6Ay4V27Z555Rj//+c+tyq925nG2Zm43f1d/8MEHVsDYzuQnP/7xjzuc+/rrr1tVcyZk+/vf/6433njDqkDbmnmM73znO1YY9v777yftdck23f4nQPPim28G86KaJNektcYXX3yhG264oVuPZb5wpoTPfIOZL7755t/aWWedFS0fbH8zpYFbMyWHpizQlCGaL/rs2bOtP5xbM8nqmWeeqZKSEuvNfGzK/QAAAHqTLxgJu8JdCLvMYPrrn16s5Zu2hCaSfnDgLjptr+E7vK8ZUJ/rzIKwy/BUSsGutX6mz4D6n3UMu46/I8VhlzPSxkjYlRTeoFcfV36sy9+4XN9/9vuau2puh7Arz5FnVXP9+fg/6/L9LtdeFXupoqCCsAtW26Kp7DJhl5mtaP4/beYrmvdDit3y+ELW7b3d3tgVP/nJT/Svf/1LLS2Rf3AxFVmmZc608G3LFL6YajATuJi3d955R199tf0mDdvKzc21qprSlWlJPOGEE6ycI57Nmzfr3//+t/bff//oMROGvfLKK6quro57H1MgZIKyrcOudqbqa2smQPyv//ovOZ1O671Zd8ZUli1cuFD/+c9/uvFZ9i/dDrxMgmjSzhdffFEuV6w82pTRmW/67mhubraST1P6F4/5Q7Zx48bo2/z58zvcbsoJn3zyST366KN66623rABu5syZVh9tO5NWf/bZZ9Y3mXkzH5vQCwAAoLeYAKurYVco3KZb5y/VJ2tj/yB3yh7D9MMDd9nhfcuK3CpwZ8mUipbNkq9je1haq1y0zYD6LZVd445KcdhlKruy5HsihVoCLXpj3Ru66NWL9KMFP9JLa19SsC0Yvb3EVaIzJ5+pvxz/F52/5/maWjZVZXllctlpIUXE4g2NVhujqezadtdcsx6Q77RuN+elmplxNW7cOCs8MVVbJvDatvKonakEM8FQ+wwv8zu7ORZPMBi0Hs+EM0cffXQvfhbSFVdcocLCwg5vZh5Wd0Iv0364td/97nfW45iwqrS0VMuXL+/w+d59991W2GWCL9OqaKq/nnvuuejtK1eutF5T89g7YtohTbuoqZ4zzHvzNTHHt2UKhy688EJdc8011muMJARe5pv01FNP3e744MGDrd7X7jB/SEx4dtppp8U9xwxzM9847W/mD1S7hoYGK+389a9/rWOOOUZ77rmn/u///s+6RtOPayxdutQKuf74xz9a5Znm7Q9/+IPmzZtnfaMCAAAkWyAUCbtMkLUj5ofge178Um+uiA1oP2Zyuc47avx2vyBta1CBS8W5WdKyZoIuE3hlVNh1bscB9TPuksamcJaKmdVVYiq7CLt2lvnz2OBr0Pyv5utnL/1M575yrt7e8LbaFPuzPDhvsM7Z7Rwr6Prpbj+15nSV5pVaLY3A1ja3+K2ZXaaNsTNuu02BcJt1XjowAZcZXWS6ukwhyYknnrjdOaaw5G9/+1s0kDHMx+bY1kUnW4dPZpC7qW76xS9+oXPOOSfu85v2vvaQqn1U0tbBVVfGJ5nnMAUuW79t3X7Ylf8HbPt3r2nXNI/z+eefW0U248eP13HHHaempkhl75QpU7Ro0SK99957+tGPfqTKykqrk+1//ud/oo9p7OjvdOORRx6xhtq3t0SaINKsTYFPZ8xrbMK2RIFjf9btvw1NuZ2ptDID5Lb26aefavjwHZfcd5fpGzYzwszzHn744br11luttfHxxx9bJZHmm23rlNMMzzNllccff7xVdWbaGLcuOTRtkeaYOWfixImdPq9pldx6h4bOElUAAIBtBUNhbaz3KtiFGVTmh+AH3/hK8xdtiv2cMnaQLj9+ojWEPpGSPKcG5GdJJUnQF2llzLjKruaOA+pT2cbozJWKhkV2ZcRODaI3Qder617Vv5b/S0s3L93unBGFI/StXb+lo0YdpUG5g1TsKu7SL7Dovwblu6wB9WZmV66pAN2GLxSW05ZjnZdsxcXFVoHItsxoH3NbZ0ywYwalm1a5H/zgB9acq22ZIevffPPNdnO4TNj1wgsvWEUtW4dPZkyRmYFlZl/t6M+L6eZqb3k0z2F2SDRBUzvT4rcjZtSRCaS2tnXRzI6Ygpltsw6THbQ/pnlvim7M52NaQNtDLbOD47777mu9mblnphDHdJWZ6qsJEyZYn7t57FNOOSXh85vgavHixR1eezNY3zznT3/60+3ONznJVVddpRtvvNHqdEMPAy/THmhSRNO3ar5o5sV/++23ddlll1l/KJLJ/GExg9922WUXrV69Wtddd52OOuooK+gylV9mW0/TVmlKKbdm+ozNbYZ53x6Qbc0caz+nM2ZbUfNNAwAA0K2wq6FrYZfxyAdr9dhHsdmj04cX6/qZU+TYwU6LhW6HSgvd2fGFCYekxg2ZsyPjpoXS3HM7hl0n3JXa3RideVLRUMKunRxEv9m7Wc9//bz+vfzfWtPYsZXJmDBggr6967d1yIhDNMA9QIXOQoIudMnUYcXWboxmQP2QYluH7xvzDx71LQFNHlpknZdspn1u67a6dh9++GHcog8TDJmZ2Ga4/O9///tOzzHBy/e+9z0ryNnaHXfcYd22deDVWfiUiPm9v1174NOd+/eU2SnRdIeZACkRuz0SXprB8vGYqq/2MU4mQDPFOL/97W91wQUXbDfHy4SQJrgynWpmd0dT9LN1SGduN0PrTRWZKe7Zlplp/pvf/MbaUBA9DLxMhZVJaU01l/lDar6QJs01Qdi1116rZNo6NTZfWLPFp/lD8OyzzyZsg9y2DLGzJLmzUsWtmW/ySy65pEOFl9mFEgAAoDOmfdGEXaadsSue/myD/vRW7Jfr8YMLdesp03c4fD7f5dDgoiwJu0zIZcIuE3plbNj1S2lMx92z+pQrPxJ2UWnU7UH01S3Vmr96vh5f8bg2Nm/c7pzdynazgq59h+yrAbkDVODcftg0kIgZUP+zw8dZuzFuavRZM7tMG6Op7DJhV6Hbbt1uzks2syugmZVtWglNZZBpKzRzuE0o9fDDD8e9n5m1ZWZWmVlV2zKtc2ZHwrlz524XvPzwhz/USSedZJ1jxh2lOzPzyhTAmAIeM5rJhExm3JJpITSVaVszg/zbi2VMu6I5zwzgb+80+9a3vqWDDz5YBx10kDWGyRTrmDxh1113jc7tMq+puX2//fazdl80s77MNZivyQMPPGBVf5mvjbl92x0ZDTOaydx+zz33bHebuRZTrGO+1uhh4GXKCP/xj39YXyTTxmi+QczsLFOm19tM2aAJvFasWGGtzTeT3++3dmHcusqrqqrK+mZqP8d8U27L/EHsbMeJdqaCzLwBAADsiNlha2NDa5fDrpeXVuk3L0d+njFGDMzTnd+arsLcxD+auZ12a0fGrGmj8lRF2hkzwcYvpLnnSYE0CrvchVJhBWFXNzQHmlXZXKlnVj2jJ1c+qVrv9jOI9x+yv9W6uEf5HlbbYr4zP5lfNfQzB40v022nTrd2YzQD6hvCbVYbo6nsMmGXub03jB49Wm+++aZViWWCGa/XawUwJtAyXVTxmGDMvHXm73//u1Wd1NngebOJXVFRkRWmbV04kq5M26DJF0y1lmlZNIU8JqT62c9+tl0OYGaAmzfD5A4mrDLtl+2VcqZ665///KfVJWbaSE0GYTrTTGtoe6WaqfL65JNPrAKiSy+91BoTZYLBvffe2wq8TK5h2iBNN11nTj/9dOvx77zzzk5vN4GjmW2+ZMmSJL9SmS2nrX2CWoqZH9zMbouJelpN8moqyx566CGrfdJ8M5lvEvONYbZDNcw3zogRI6xvQPONZ5JS8837/vvvW2mpYT42c7xMyWK8cs5tmQov8wfBPGe8nmcAAND/mB+lTGWXN9C1KqX3vqrVdU8vjg60H1zo1m/+aw9VFOcmvJ/TbtOwAXmy90IlQEq01knN3dvwKGU2fi7NPT/Nwq4iqSj+P96i45/RpkCTNjRt0NOrnrbemvyRYdPtbLLpsBGHWUGXGUJf4i5RriPxn0mgu/8wYnZjNAPqzcwu08bYG5VdAHpQ4WX+wjDbYr766qtWJZWp8NraE0880eXHMjs/mC0625nSPzOUzvSrmjeTiJok0ySvZmvQq6++2uoDbt8l0gRQP/nJT6yE1JRcmvuYWWLTp0+3dm00Jk+ebG2TevbZZ+vBBx+0jpmSTjPQrathFwAAQDxVTb4uh12fr6/XnGeWRMMuM3j+l9/abYdhl8Nm09CS3OwJu0xLYKaEXRs+lZ65QAq0RNZmJ74TfymNPjR115RbIhWmf8tQOgyib/Q3an3Tej2x4gk9u/pZtQY7ztxx2Bw6ZtQxOm3CaRo/YLwVdLnsWbIZBNKKCbemjyhJ9WUA/Uq3A68LL7zQqrAyJYumJbAnJfVmIJt5nHbtpY+mHM+U9ZmhbaZs0gxpM6GXOdfshGBKJduZHlZTJmgqvMzQOFNeaco02wfJGaYF0wyHa++xNYP4TD8zAABAT1Q3+dTsC3bp3C8rm3Ttk4vkD0b+sTDfZdedp0/XqNLE7VIm5BpSkrvDQfYZI+iXmuJvHJRWvvlEmmfCri0hiQlCTvyVtMvBqbumvAFSQe+0QGXTIPoGf4PWNKyx5nO9sOYF+cP+Dufk2nM1Y/QMnTrhVI0qHqUSV4mc9h3vAAcAyOKWRlNFZVoITzzxRPUntDQCAICt1TX7VdfS8ZfoeNbWtujCf32mhtbIdusuh013njZdu48ckPB+tpxI2LWjQfYZwwynb1gnhboWEqbU+o+keRdKQW9kbXdvCbsic2JTIm+gVLD9IGnEBtGbiq6VdSv1ny//o1fXvapgW8fvNTN4ftbYWZo9braGFQ5TsbtYTlO1BwDIOt2u8DJthGPHju2dqwEAAMgAjd5Al8OuykavLn/8i2jYZSq2bpg5ZYdhl6miN62OWRN2mX9jbdqYIWHXB9K8i2ID9U3YddLd0qgDUndN+YMib9hOS6DFCrqW1S7Tv7/8t9765i2F1XHsygD3AJ0y/hSdNOYklReUW8PoTTsjACB7dbvC629/+5sWLFigP//5z3F3b8hGVHgBAADDtDCaEKsrNjf7ddG/PtP6ukhLnBkEcfWJk3T05B0PGy8vzlWhO4t+IW+qlHwdB4WnpXXvS/MulkJbwi7H/2fvPuCbLNc2gF/NbpOme7H3Hm5RVEBkiIC4917nO0c9juPee289HvfeAwUBBRXFgagoe+9RutukaZvd73c/oUnTFhKgTdtw/b9fv5In75u8SfHQXr3v+5Gw62mgc2Dzo1bBsKsR+RHG4XGooGt5yXJ8vPpjLChY0Oi4rMQsnNL7FIzrNg6ZiZlINiRDq4mTEJmIiHZrj7+Lki1MZcvN7OxstdWpXh9eAixbbRIRERHFIxlOL0Pqo1Hp9OCmz5YEwy5x1bG9ogq7MpON8RV2VZe1j7Bry3xgxvX1wi4TMPFpoNOhrXdN0sIorYyk+Gv9aodFm9OGxcWL8fGaj7GoeFGjd6ejpSNO7X0qRncdjXRTugq6NAlxMgePiIiissffSV144YVYuHAhzj333H0eWk9ERETUXri8PhTYnKqyJJIajw+3fL4M64urgmuXHNUNUw7sGPHctCQDrKY4minktAcCr7Zu8y/AzP8Avp2tqvpEYOIzQMeDW++aZDi9DKkneP1eVc1V6arEHwV/qKBrZdnKRu9Md2t3nNb3NBzT6RgVdFn0Fv68QkS0n9rjwGvGjBn45ptvcNRRR7XMFRERERG1MR6fH4U2F/xRhF2yC+OdXy7Hih324NoZh3TC2Yd1iXiuNVGPNLMBcUN2N6wqRpu3cR4w60bAH5izBn0SMOlZoMOBrXdNlizAlIL9Xd2Oi1LVNT9/Pj5a/RE22DY0Oq5vWl+c3vd0DO8wHKmmVDWcnoiI9m97HHh17twZVqu1Za6GiIiIqI3x+WtVZZfX74/q2AdmrsTCzeXBtYlD8nD5MT0iVplIC2OmxYi44fMEhtTv2bjY2Fv/PfDNzYEdJIUEJZOfBfIOaL1rYtgFl88Fm8umKrp+2v6TqujaWrm10Vs1JHOICroOzTlUBV1JElYSERHtTeD1xBNP4MYbb8T//vc/NcOLiIiIKF75JeyyO1WFV8Rja2vx+OzV+GltSXBtVN8s/Ht074hhV5JBh6zkOAq7JDyy58sbiDZt3Rzgm9uA2p1hl8ECTH4eyB3cetdkyQZM++8vl+t2XJSKru+3fI9P13yKguqCRsdJwCVB1wHZByDFmIJE3f6zmRZRc5F/m6ZOnYopU6Y0eb/8vH/NNdeoD6L2aI8nN8rsrrlz56Jnz55ITk5Genp62AcRERFRPJBZXYWVTrg8vqiO/e/c9fhmeWFwbViPdNxyfD9oNbsPu0x6LXKsxviZMyQVXVLZJRVebdmar8PDLqMVOPHF1gu75OufnLPfhl1VnirkO/JVFdcnqz/B5XMux/OLng8LuxKQoFoWnxn5DB4+5mGM6jIKueZchl3UPsgvAPL/BtZ9G/jcwr8QKCoqwhVXXIEuXbrAaDQiNzcX48aNw/z58xEPNm3apP7dXLQotGlFZWUlRo4ciX79+mHr1kBFqBwjH7/99lvY+S6XCxkZGeq+H374IebXT220wuvpp59umSshIiIiakOKK12ocUcOu8Rb8zfj87+3B28P7ZSCuyYOgE67+98tGnQa5FpN8RN2CUcR4HGiTVs1A/jubqB25w+cMivrxP8CWf1a53rk62/JAYwW7E8kKHZ4HIHWRXclZm2chanrpqLCVRF2nOyuOLLTSJzW5zT0y+iHFEMK9No42tiB4t+GH4GfnwJK1gZmBWr0QGZv4KhrgR4jWuQpTznlFHg8Hrz11lvo0aMHCgsL8d1336GsrG1tIuJ2u2Ew7PvsyuLiYhx//PHqzz///DMyMzPDxjK98cYbGDZsWHBNKtssFkubez+olSu8Lrjggt1+EBEREbV3JQ4XHC5vVMd+snAb3p6/OXi7b04y7p8yCEa9drfn6bWBsEsToQKsXZHdGF2VaNNWfAl8e1e9sCsVmPJS64Zdybn7Vdjlr/WrkGubYxs22zbjvZXv4ZJvLsEby98IC7t0Gh3GdxuPl8e8jDuPuBPDOgxDZmImwy5qf2HXV9cAhcsBgzkQbstnuS3rcn8zq6ioUKHPI488glGjRqFr16447LDDcMstt+CEE07Y5Xn33nsvcnJywqqm6rPZbLj88suRnZ2t5nofe+yxWLx4cfD+9evX48QTT1SPIWHSoYceim+//bZRm+T999+PCy+8ECkpKbjsssvw5ptvIjU1VW2O179/f3Xu+PHjsWPHjqher1RzHX300aoDTbrR6oddQnKKDz/8EDU1NcG1119/vcn8Yvv27TjjjDOQlpamKsDk9Ug1WZ0//vgDY8aMUc8h1z9ixAj89ddfYY8hv8R69dVXcdJJJyEpKQm9e/fGtGnTgveXl5fjnHPOQVZWFhITE9X9EshRKwVedrs97M+7+yAiIiJqzyqq3bDXRNeO99WSHXjxh/XB210zkvDwyYNhNu6+iF6n0SA3xRSxAqxdcdoCgVdbtuxz4Pt7pbYocDspAzjp5UClRauFXXmBH373Az6/D+XOcmyr3IZNtk14a9lbuGT2JXh35buo9ISCUoPGgMk9JuO1sa/h1sNvxSE5hyAjMUMFYETtirQtSmWXyxH4b12fCCRoAp/ltqzL/c3c3iiBkXx88cUXqnUvmmrLf//733jttddUUHbAAQc0eYyEZQUFBZg5cyYWLlyIgw46CKNHjw5WSTkcDkyYMEGFXH///bdqoZw0aRK2bNkS9liPPfYYBg0apB7jjjvuUGvV1dV4/PHH8c4772DevHnqnP/85z8Rr3316tUYPny4amP8+uuvVejV0MEHH4zu3bvjs88+CwZk8hznnXde2HFyDRIQynsn98t7URe+SSVaXdukBGU//fSTapOUsEpes6zXd8899+D000/HkiVL1P0ScNW9T/KaV6xYgVmzZmHlypV48cUXG4V01DwSauVvbgRarValq5LkajSaJsvu5WFk3eeLrvS/vZEwTxJcSbW5SyUREVF8sjs9KKmM/MOB+H5VER6YsbIuOkFeiglPn3FAxOHzmoQE5KWaYNTtvgKsXZGqrsrQ/LI2acnHwLxHQreTMoGT/gekdW+d65Hvp60dAj/4xjmP36MqumROV1lNmWpblPZFpy+89VUGz0/oPgEn9z4ZXa1dkWxIVu2MRO2WzOr68NxAqN3Uf+ueGsBdBZz5LtDhwGZ9agl3pHpKqpokmJJKpDPPPBNDhgwJHiM/v3/yySf48ssv8eeff2LOnDno1KlTk0Prv//+e1WxJLPBZCZYnV69eqlN7aTyqykDBw7E//3f/+HKK68MPuaBBx6oWgrrSIXXRRddhHXr1qlZ4eK///2vqjiTgK0pUnUlIZa0Qx555JEqZJPcYleD+Tdv3qxep7wOeVypYpMqL6nkkqowmf0ltx999FEVQtVlHhJ0SfWZhIdjx45t9PiSf8hjvP/++5g4cWLwOW+//Xbcd9996nZVVZUK4iQolPBs8uTJKuCS56OWFdWvSeQvRd1Aeim1kx7Yhn+Z/H5/o+SWiIiIqL2ocnmjDrt+XV+Ch2atCoZdGRYDHj9tSMSwS74JlsquuAq73NWBuV1t2d/vAr88FbptzgZOeglI7dI616PRAMkSdpkQz9w+dzDoKqkpwWdrP8PsTbPh9gcqJeqYdWZM6jkJU3pNQWdrZyTrk+Nrrh3tv6pLAzO7dLv4t0HWnRWB41pghpdUZEklkgyql+onCXOk1U7aCetce+21KsCSaqXdVRlJNZZUcEmbX30SqEkrY12wI5VNX331FfLz8+H1etX9DXOCQw45pNHjS+tfXdgl8vLyVLgWibQcSqAlAZ9UVO1u872bb74ZGzZsUAHbs88+2+RrlNCtYZWY0+kMvka5pjvvvFNlJDIXTQIvqQxr+BrrB4tms1k9Zt3rkQBQvj7SCikhmuySKaEdtVLgJWlwnYsvvjhY7VVfaWkpjjvuOM7xIiIionZHhtMXRRl2/bWlHPdMXwGfPxB3pSTq8dipQ5CXsvtKHfkBXnZjlF0Z44YMp5cdGSM3DLSeP18HfnshdFvmZU35H5DSufXCLmvHXf8AHAecXqcKumq8NSisKsSnaz/Ft1u+hdcfPhdPKrim9JyCKb2nIM+cB4vewqCL4ou0TcuAeq+r6QovWZf75bgWYDKZ1Lwp+ZCQ5tJLL8Vdd90VFnjJfR988IGanyVtd7siBS4SQjW1o6FUQIkbbrhBPY60Jkrll8ynOvXUU4PtgPUDoIb0en2jfzOjaEbDrbfeqsIluXY5XuZvNUWCOqnAuuSSS1SAJQPuG7YhymuU9sf33nuv0fkyb0vIeycD8mUzP5mNJmHhEUcc0eg1NvV65PGFPLdUnM2YMUNVpklb6L/+9S/1vlHz2uNG+LrWxYYk7ZX/oIiIiIjaE6fHh0K7M6pvrFfk23H7F8vg8QWONRu0eOSUweiWEXkGk1R/JRniaAaR1w1U5rfdsEuu6/eXgT9eDq1J0CQD6q15rXNNGu3OsGvfdyRri6o91bC77Srwynfk45M1n2Du1rnw1YaPPEk1puLkXifjxF4nIsecA7N+/5hhRvuh3KGBGYEyoF5nCrQy1//fqJpyIGdg4LgYGDBggGrNq0/a62TO1tlnn626uKTtsSnSFinthTqdTrUlNkWqySQQktbHuoyg/sD3liLtg3JdEnpJqHTWWWc1eZwU78g8rZtuuqnJ9kd5jR999FFwKP+uXqO0W8rj1M0DKykp2eNrlgBN3iv5kIH7EhYy8Gp+UX/Xdd1116nPEnbJkDUpOawjZXwLFixocrgdERERUVsOuwpsTvijCG3WFzlw8+dL4fQEfkNr0mnw0MmD0Sen8YDchjKTjbBEGGTfrvi8gbCrmQctNxv5es5/HvjrzdBaaldgyouBHdJag1YXCLu04b/1jwfSsigVXdLCuMW+BR+v+Rg/bfsJfoT//ZAdFk/pfQom9piI7KRsJOlDP08QxSWp6Dzq2sBujFINm5gWqO6Uyi4Ju4zJgfvluGYk3VennXaaCnik+kna6WRGl7Q0SgtgQxJQybB4GeIuwZFUZTUk3VxSySTtd7L7Y9++fVXbosylkjVpU5Sqrs8//1wFaHW5QV1VU0uTdkUJseQ1yHM2Va0m87OkOmtXYZacIwP15T2SOV8yz0xaFeU1SSAlt+U1ynslr1fmfMu6VLLtCam2k0oymW8mmwpIC6jsTknNL+rvvGSXBSG//Vy6dKkaDldH/jx06NCodlEgIiIiam9h15ayatz42RI4XIGWLL02AfecOBCDOqZEPDfDbITVFEchh98XCLsk9GqL5Ospu54trteSkt4DOPG/gDnQkhJzUtElM7sk9IoT8jOBw+NQQZe0Km60bcRHqz/Cr/m/ojY43S5Awq3T+pymBtJnJWWp4fRE+40eI4CJTwf+d6lkbWBml7QxSmWXhF1yfzOTnQUPP/xwPPXUU2r2lMfjUXO4ZYi9tAA2RUIuCYokMJKN6k4++eSw+yXAknDrtttuU0GaBEe5ubk45phjkJMT+EWCPJ/cJ/OoZB6YVFJJKBQrEj5J6CW7KNa9loavYXdzyqSoR3ZnlOuW1y8tjx07dlQth3UhmQyalwH9Mni/S5cuePDBB/c4B5H85JZbblHVbxKWSYXXhx9+uJevmvZ5l8b6ZPeEZ555Zr/bqZC7NBIREe2fYVeB3Yl/f7AIxY7AjC9NAnDXpIE4unfkLcRTkwxIN8dR+5q8X/btgdldbVGtH5j3GLD049BaRi/gxBeBpMAGTDEn1RyyG6O0M8YBf60fle5K1bro8/uwrmIdPlz1IRYULGh0bAdzB5ze93SM7zYeGYkZMElLF9H+SiqdChYHBtTLzC5pY2zmyi4i2sfAa3/FwIuIiGj/C7tKHS78+6NFyK8IBTw3H98PYwdEbouzJuqRaYmzweT2/MCujG017Jr7ILAitNU9svoBk18AEgMDlWNOhlQn58XFD7USbknIJWGXhF6rylbhw9UfYmHhwkbHdk7ujDP6noFx3cYh3ZQOgzaOQl8iImo34qeumoiIiKgZwy5btQf/+XRJWNj179G9ogq7LCZd/IVdjqK2G3bJ7n/f3QusnhFak3YhCbtkRk5rMCQFwq4mNntqTzx+j2pblDld8nvyZSXLVOviouJFjY7tbu2OM/qdgTFdxyDNmAZ9HM4rIyKi9oOBFxEREe0X9iTsklldMrNrc2ko4Ln86O448YCOEc81G3XITo6z1q3qMsAZuzkse8TnAebcAaybE1rLGwpMehYwWFrnmoyWwHD8dhx2yQD6+kHX0pKl+GDVB1hWuqzRsb1Se+GsfmdhVOdRSDWlQi/ziYiIiFoZAy8iIiKKe3sSdtV4fLj186VYW+QIrp07rAvOPKxLxHOTDBJ2xVlll6syEHi1RT438PXNwMYfQ2sdDwZOeDpQYdUaTFbAko32yul1qqCrxlujgq6/i/9WM7pWlq1sdGy/9H44s++ZGNFphAq6dBr+aEFERG0H/1UiIiKiuOby+lBojy7scnv9uPOLZViWH6pmOuWgjrjoyG4RzzXptcixGtUuUHHDUxNoZWyLvE5g5g3All9Da12OACY8DrTWcPTENMCcgfao2lOtZnRJ4CVBl8zmkhldq8tXNzp2YMZAVdF1VMejkGpMhTZOBvITEVF8YeBFREREcUsCrEKbCz5/5LDL6/PjnukrsHBLRXBtwuBc/HNkz4ghllGvRa7VFF9hl9cNVO4I7MzY1sgssRnXAtv/DK11HwGMfxhorQHpsgtka+0EuQ+kZVEquqSFUYKuPwr+wAerP1C7LzY0JHMIzu5/No7scCSsBiuDLiIiatMYeBEREVFckgBL2hi9shV8BBKIPTRrFeZvKA2uje6XjWuP6xMxxNJrNSrs0mjiKOzy+4DKfCCK965VWiyn/xsoWBxa63UcMOZ+oLWGpFuyAFMK2gsJthweh6ro8vg8atfFBTsWqIquDbYNjY4/KPsgVdF1eN7hDLqIiKjdYOBFREREcUcCrB1Rhl3S6vjUnDWYu7o4uDa8VwZuGt8XWk3ksCsvxRTxuHZFKrrs+YDPizbHaQOmXQkUrQit9T0BGH0n0BrzoyQMlXldrbUT5F4EXZWeSlXR5fP7VNA1P3++Cro22Tc1Ov6QnENU0HVo7qEMuoiIqN1h4EVERERxGHbVwOPzRxUA/PeH9Zi5rCC4dkjXNNxxwgDotJrdnqvTBMKuSMe1O5UFgNeFNkcG53/5T6B0bWhtwEnAqFuBBE3rhF3JuYDBjLZOgq1KdyXsLjt8tT718ev2X1XQtaVyS6PjD889XAVdB+ccjGRDMlsXiYioXYqz79CIiIhof+b316LA7lSzu6Lx2s8b8flf24O3B3dMwb0nDoRBFznsyo3HsKuqBHBXoc2RwflTLwsPu4acAYy6rfXCLmuHNh92SRVXubMc2yq3qc9uvxs/bP0BV31/FR7989FGYdcReUfg2VHP4vERj2Nk55Fq50UOpCdqny688ELVkt/wY9260Hy+Bx98EFqtFg8//HCj8998802kpqYGb/t8Pjz00EPo168fEhMTkZ6ejmHDhuGNN94IHlNUVIQrrrgCXbp0gdFoRG5uLsaNG4f58+fv8jrvvvtuHHDAAWFrP/30k3ruq666Sv1iSq5Frr1///6Nzv/444/Vfd26Rd5chvY/rPAiIiKiuCDfFEvY5fL4ojr+3d824/3ftwZv981JxoMnDVK7Le6OtC/mpBgjhmLtTk1F4KOtkfbKL/4B2EPBJA66ADjiqkDwFGsaDZDcAdC30k6QUfD6vWo+l8PtUNVdEnzN2z4PH63+CNsd28OzOySoIfRn9zsbQ7OHqoouTWuEiERxTv5bXFm2EhXOChUm90/v3+L/rY0fPz4skBJZWVnBP8t9N954I15//XXcfPPNu30sCaZefvllPP/88zjkkENgt9vx559/ory8PHjMKaecAo/Hg7feegs9evRAYWEhvvvuO5SVlUV9zTNmzMBpp52GG264Affcc09w3Ww2q0BNwrMjjjgiuC7XLgEbUVMYeBEREVFchF2FdhecUYZdnyzchtd/Cc0s6pFlxiOnDIbZuPtvjTQJCcixmmDU7T4Ua3ekqkuqu9qa8s3Al/8HOApDa4ddARx6WSuFXVrA2hHQtdJOkBF4/B41n0t2XpT/JiTo+nHbjyroyq/KbxR0Hd3xaNW6ODhrMIMuohYkm0K8tvQ1bLRvVIG0TqNDd2t3XDL4ErUZREupq7Jqyo8//oiamhrce++9ePvttzFv3jwcc8wxu3ys6dOn45///KcKo+oMHTo0+OeKigr8/PPP+OGHHzBixAi11rVrVxx22GFRX+/777+Piy66CI899hiuvvrqsPt0Oh3OPvtsFXDVBV7btm1Tz3fttdfigw8+aHS9EtItX74cHTp0wAUXXIDbbrtNPY548sknVeC3YcMGVa02adIkPProo7BYLOp+qSq75ppr8NFHH6nPW7duxVFHHaXOycvLU8fIc0tgKM+h1+sxcOBA9RrkdVPbwF/fEBERUbtXXOlCtTu6IevTFufjxR/WB293TkvEY6cOgTVx9zv8ScuEtDFGqgBrdzzOwNyutqZ0XaCNsX7YdeS/gcMub52wS6sDUjq1ybBLdlosqSlBviNfVXV5fV58u/lb/N93/4en/noqLOzSQIMRnUbgxeNexINHP4jhHYcjxZjCqi6iFgy77p1/L9aUr0GSLgmZiZnqs9yWdbm/Nbz22ms466yzVFAjn+X27khw9v3336O4OLTBS30SFMnHF198AZdrz+dAvvDCCyrskutoGHbVueSSS1QAVV1dHQylpIotJycn7LhvvvkG5557rnqcFStW4KWXXlLHPvDAA8FjNBoNnn32WSxbtkxVpMlrk/CqPnmexx9/HO+8844KBLds2YL//Oc/6j6v14spU6aocG/JkiWq8uzyyy+PuLMzxRYDLyIiImrXiuxOOFzRhV2zlxfg6W9Dc6Bk6Pzjpw1FWpIhcthljcOwS3ZirNwR2JmxLSlaCUy9HKguDa2NuAk46PzWuR4JuVI6A9rdh6Kx5va5UVxdrNoUJeiS4GvO5jn4x7f/wDN/P4MdVTvCgq6RnUbif2P+p4IuaWOUoIs/nBG1bBujVHZJ1WV2UjZMOpMKl+Wz3JZ1uV+OawlfffVVMIiSj7rqLGlH/Oyzz1QoJOTzp59+qtZ3RSqiJOyS4GvIkCH4xz/+gVmzZgXvl8opCZUkPJL5W8OHD8ett96qwqBIVq5ciSuvvBIvvvhi8JqaIrO+evbsqa61brbXxRdf3Og4CbakRVOquqS1csyYMbjvvvtU8FVHqrZGjRqF7t2749hjj1X3yzyw+qQ983//+59q4TzooIPUNUqLZt17aLPZMHHiRHVNMl9Mno/tlW0LAy8iIiJqt4oqow+7flhdjEe/WR28nWUx4vHThiAr2bjb8yQQyE42ItEQZ2GX3w9U5gP+6NpAY2bHYuCLKwCnLXBbZtyMvhsYfHrrXI/M6rJ2CrQzthFOrxOFVYWqokt+YJYWqdmbZqug69m/n0VBdUFY0HVs52NV0PXA0Q9gWN4wWA1WBl1EMSAzu6SNsalwWW7Lutwvx7UECXQWLVoU/JCKJiFtdxIE1bUkSpAktz/88MNdPtaAAQNUNdRvv/2mKrFkPpe0AV566aVhM7zy8/Mxbdo0NaxeWv4kKJJganc6deqkjpOWwh07QkF9UyTgkrZCacl0OByYMGFCo2MWLlyoWjXrh32XXXaZeuy66rC5c+eqIKxjx45ITk7G+eefj9LSUlRVhTZuSUpKUmFWHWlllDliQtogZWMAeZ3yPjzzzDMRr51ij4EXERERtds2RoczurDr1/UleGDmSvh3FjKlJelV2JWXkhjx3EyLIeJsr3ZHKrqkssvrRpuy7Xdg2r9CO0VKyDT2AaD/pNa5HkNSYGaXDKpvA2q8NSioKlAf8ue6oOuKb6/Ac4ueQ2F1qP1TqkhGdxmNl8e+jHuH38ugi6gVyIB6+e/UoG26iljW5X45riXIoPdevXoFP+pmT8kcLJk7JVVZdR9yO1Jbo7QBHnrooWpm1tSpU1WQJeds3LgxeIzJZFJB0p133olff/1VhUJ33XXXbh9XAqdvv/1WfR45cqQKzXblnHPOUaGbzOeSkKpuJld9fr9fDbyvH/YtXboUa9euVde3efNmFZQNGjRIVbpJQCYtlXVVXXWk3bNhSCmVZXUkeJNWxiOPPFK1Wvbp00ddG7UdcfbdGxEREe0vYVelM/RN6e4s3FyOe6avgG9n2mU16VQbY+f0pIjnZiYbkWxqW21szaKqGPDUoE3Z/Asw8wbAt3P2i0YPHP8o0H3XQ5RblNECWHJaZ15YA9WearXrolR21Q2n/37L9/h4zccoqg5UG4QFXZ1H48y+Z6JvRl9WcxG1ItmNUQbUS/uxtDE2JOtyvxwXKxL8yO6KUn0lVUr1h87L0Hqp4pIgKBpS9SXqV0U1dYzM9YokLS1NhV5SMSWhl1RgSfVVQ3LNkydPVu2H0m7YFKkWW716tQr5miKvX2ZwPfHEEyrEEw3bGaN14IEHqo9bbrlFDdOX6rlhw4bt1WNR82PgRURERO1KiSP6sGvxtgrc/sUyeHyBsMts0OLRU4ege6Y54rkZZiOs8Rh2VZcBzl3PaWkV674FZt8G+HdW7OmMwIQngS6t9EODKQWwZKEtBF2y66JrZwi4u6BLm6DFsV2ODQRd6Qy6iNqC/un91W6MMqDeqDWGtTVKpZD8990nrY86LlakIkt2TmxqR0YJbOT+p556qtF9p556qprLJdVMMsdLqrok5JGqpn79+ql2QJkRJi2HMuNLqrUkWJI2xRNPPDGqa0tJScHs2bPVIPq60EvaHRuSyrL//ve/yMjIaPJxpLpMZmt17txZXZOEWjJLTMK++++/X7UpSuD13HPPqXbEX375ZZfh2a7I63/55ZdV+Ca7QErAtmbNGlV1Rm1H26jPJiIiIopCqcMFe010YdeKfDtu/XwZXN7AMGCTToOHTh6MPjnJEc+VIfYpSXEYdrkqA4FXW7JyOvDNLaGwS28GJr/QemFXYlqrh10yl0vmc0moJWGXtDx9s+kbNaPr+UXPh4VdEnSN7ToWr4x5BXcdcRcOyT2Ew+iJ2gipuLxk8CUw683qv1up0pQB9fJZbsu63C/HxYLb7ca7776rZm01RdblfjmuIam8mj59ugqIJOSSAe0SdElAJW2FMifr8MMPV2GZhGlSJXbHHXeo2VnPP/981NdotVrVLouy86KEXlu3bm10TGJi4i7DrrprlYH9c+bMUS2YUnElQ/e7du0anFkmtx955BF1ne+99x4eeugh7AmZ77Vq1Sr1nsn7ITs0ylD7K664Yo8eh1pWQm39JlTaJdmFQRJn2YlB/iMkIiKi2CqrcqOiOrqZU2sKK3H9J4tR5QoMZDfoNHjwpEE4qEtaxHNTEvXIsOx+kH27JC2M9vy2tSPjko+BeY+EbhtTgMnPATkDW+d6zBmBwKuVyE6LNrdN7bYoJOj6bst3u6zoOq7Lcaqiq3d6byQbkmP2QzMR7ZkFOxao3RhlQL38dy1tjFL5JWHX4XmH8+0kaiEMvKLEwIuIiKj1lFe5UR5l2LWxpArXfrQI9p0D7XWaBNw/ZRAO6x6aVbIrMq8r0q6N7ZIMp7dvC+zM2FYsfBOY/1zodlIGcOJ/gYymZ660KGkzMmcBptj/UlN+9+zwOFRrk/wgLBh0EcUfqeyS3RhlQL3M7JI2RobURC2LM7yIiIioTZOqrmjDri1l1fjPJ4uDYZcmAbhz4oCowi6LURefYZffB1Tmt52wSyrMfvsvsPD10JoMh5/yPyC1S+uEXcm5gCHyXLfmDroqPZWwu+xhQZfM6PpozUe7rOg6o+8Z6J3WG1ajlT8sE7UjEm4NzGil6lWi/RQDLyIiImqzbDUe1coYjfyKGhV2lVd7gmHXbRP646jemRHPNcdt2OUPtDH6ds7Ham21fuCnJ4AlH4bWUjoDU14EkvNaJ+yydgD0iTENumTHRfnwSRi5M+iau3UuPlr9EQqrC8OOZ9BFRES0dxh4ERERUZtkd3rUkPpoFNqdamZXiSMQjsk+WDeO64tR/bIjnptk0CE7OXz3rLjhKAC80b2HLU7CnbkPACu/DK2l9wy0MZojh5LNTqMNhF2yI2SM2pkq3YGKLl9tIOiSwKsu6CqoLmgUdI3uMlpVdMkubqzoIiIi2jMMvIiIiKjNcbi8KKmMLqgpcbjwn0+WoNAeOv7aMb0xdmBuxHMTDVrkWOM17CoG3NVoE2QI+7d3Amtnh9ayBwYG1JtSYn89Wh1g7Qho9a0adMkw+h1VOxq1PUnQJcPoJeiSYfRaCeeIiIhojzDwIiIiojalyuVFcZRhl8z2uuGTJdheURNcu3JUT0wc0iHiuSa9FjnJpvgMu6rLAKcNbYLXCcy6Edj8S2itw0HAxKcAgyX216MzAMkdAqFXCwddEnJJ2FU/6Ppx24/4cPWHTQddnUfjjH47K7oMVgZdRERE+4CBFxEREbUZ1W4viipdas5RNPO9JOzaXBaqYrrs6O44+aBOEc816rXItZqgkUFf8cZVGQi82gK3A/jqWiD/r9BalyOA4x+L6dysIL0pEHZpNC32FBJqyXwuCbok9FJrtT7M2zYPH676EPlV+Y2CrlGdR+GsvmehTzqDLiIioubCwIuIiIjahBq3T7UlRhN2VTo9uOHTJdhQUhVcO/+IrjjrsMi7/Bl0mvgNuzw1gCN8d79WU1MOTL8aKFoRWus5Ghj7QExaCRuRXRhlN8YWqujbVdD18/afVdC1zbEt7HgNNBjZeSTO6ncW+qb3ZUUXERFRM2PgRURERK3O6ZGwyxlV2CUtjzd9thTrihzBtbMO64wLjuga8Vy9VoO8lERo4zHs8rqByh2yDWDbmB827Z9A2YbQWv/JwKjbAE0rfPspc8IsWS0WdNncNjjcjmDQJZ9/2f4LPlj9AbZWbm0UdI3oPAJn9jsT/dL7IcWQwtZFIiKiFtBy9dxEREREUXB5fSiwOeGPIqiRKrBbPl+KVQWVwbVTD+6IS4/qHnEWVyDsMsVn2CVD4e3bAX8gcGlVtm3A55eEh11DzwKOvaN1wq6k9BYJu7x+L8qcZapyS2Z1SchVF3RdPfdqPPrno2FhVwISMLLTSPz3uP/i7iPvxuG5hyPdlM6wi4hajfy7+cUXX+zy/m7duuHpp59GPPjhhx/U662oqIj6nAsvvBBTpkwJ3h45ciSuueYaxNqmTZvUtS9atCjmz93eMfAiIiKidhF2SRXYbV8sxbJ8e3DtxAM64P9G9Iw67NJp4/BbH593Z9gVGIzeqiTk+vzSwPXUOfQy4KjrgYRWeO8l6JLAqwWCru2O7SrokqpE+ZifPx//nvtvPPzHw9hs3xwWdB3T6ZhQ0JXHoIuIWl5RURGuuOIKdOnSBUajEbm5uRg3bhzmz58fF29/XQik0+mwfXu9f3MA7NixQ63L/XKcOPLII9V6Skr0OwM/88wzePPNN1s9eNtb3RoElvJv1fXXX4/k5GR8//33wRBPrufhhx9udP6ECRPUfXfffTfaK7Y0EhERUatwe/0q7PL5a6M69o4vl2PR1tDOgxMG5+KqY3tFDLt0Gg1y4zXskpBLwiUJvVpb0Upg2r/Cd4ccfi1w4Lmxvxb5OyHzumRuVzMGXTaXDQ6PI9h6K5//KPgD7616Dxts9Sradjqq41FqRteAjAFINaZC1xoVbkTUJtT6/XCuWAlfeTm0aWkwDeiPhBbcQOOUU06Bx+PBW2+9hR49eqCwsBDfffcdysrayKYmO7ndbhgMhr0+v0OHDnj77bdxyy23BNfkNXfs2BFbtmwJrslzSOi3J/YkHGvrfD4fLrvsMkyfPl2FXYceemjwvs6dO+ONN97AzTffHFzLz89Xx+Xl5aE9i8Pv/IiIiKit8/iiD7vk2LunL8fCzeXBtbEDcnDdmD7QRBl2SYVX3JH2RRV2eVr7SoDtfwFTr6gXdiUAo25vnbBLfoC0dmi2sEuCrtKaUlXRJQPp6yq6/iz8E9f9eB3uW3Bfo7DryA5H4rlRz+G+4ffhiA5HIDMxk2EX0X6s6rffsOXSy7DtqquQf8st6rPclvWWINVDP//8Mx555BGMGjUKXbt2xWGHHaZCoRNOOGGX5917773IycnZZeuczWbD5ZdfjuzsbFitVhx77LFYvHhx8P7169fjxBNPVI9hsVhUqPLtt982qjq6//77VbugBEoSwkgVVWpqKr755hv0799fnTt+/HhVkRXJBRdcoMKa+uTxZH13lVXRPGfDlkbh9Xpx5ZVXqnMzMjJw++23h80ffffdd3HIIYeoKioJ2M4++2xVbSek2ky+HiItLU1djzyH8Pv96uvVq1cvVZEnlXkPPPBA2HNv2LBBnZ+UlIShQ4dGXa3ncrlw2mmnYc6cOZg3b15Y2CUmTpyI0tJS/PLLL2Hv4dixY9XXumFAeeONN6pA0Ww24/DDD1fvbR15nLPOOgudOnVS1zl48GB88MEHYY8hVWVXX321epz09HT1PjWsIpPbddWJEmrK8XsjDr/7IyIiorbM6/NjR4UT3ijmTcmx9321Er9tCP1GelTfLNwwrm/EsEtmdUnYJbsyxh157yrzA4PqW9umn4BpVwKenTtmarTAuAeBgSfF/lrkua2dAH1iswRdJTUljYKuv4r+wg3zbsA98+/Buop1YecMyxuGZ0c9i/uH34/hHYeroEuvaYUdKYmozZBQa8ddd8G1ejU0SUnQZWWpz641a9R6S4ReEt7Ih8znkrAjEvnftn//+9947bXXVFB2wAEHNHmMhGUFBQWYOXMmFi5ciIMOOgijR48OVo05HA7VBich199//61aKCdNmhRWaSUee+wxDBo0SD3GHXfcodaqq6vx+OOP45133lGhjJzzn//8J+K1T548GeXl5eq6hXyW65HnjWRvnlOqx6RdcsGCBXj22Wfx1FNP4dVXXw0LhO677z4VBMr7v3HjxmCoJZVUn332mfrz6tWrVbgmbZNCwkgJvOT9WLFiBd5//30VHNZ32223qeuTQLJPnz4qWJIAbnccDof6ui1fvlwFWhLuNSTVb+ecc05YcCiB18UXX9zo2Isuukg9zocffoglS5aoIE2CwrVr16r7nU4nDj74YHz11VdYtmyZCkjPO+889X41fB8lMJP1Rx99VIWtEsiJTz/9VL2vL730knpceR8lONsbrKsmIiKi2IZdtujCLqn+emDmKvy8riS4dnTvTNxyfL+Ig+fjOuyS3yTLboweZ2tfCbB6FvDdXaH5YVojcPwjQLejY38tWj1g7Qho9+3bW4/fo1oXqzxVYb+1X1K8RLUurihd0eicQ3MOxdn9z8aQrCGqddGg3fv2HCKKrzbGkpdfgd9RBV1OTrAFP8FkQoLRCG9Rkbo/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+vOa6tRVKt1www3qcaTyS2ZRJSYm4tRTT1VtfvVJK1tDDcMVCZXq/2/y7kh7pFQ5SYuftOzJ7V3NIWuu52yKBEsSMMmHzPLKyspSQY8Ejg3fg/rkfYpG/eutC93k67I7o0ePVvOvJISTMOq5557b5bESFL7wwguqGuz3339vdL88l1arVeGnfK5Pgi3xxBNPqIo32R1S2hDla33NNdc0ev1Nvfd1r0Wq6KTlU1ocpT32n//8p2qDlWAtmsq9+hh4ERERUYvy7wy7ZKfFiMfW1uLJ2Wswe0VhcO3gLqm4Z/LAiO2JCXEfdhUBLkfrXoP8IPDrs8Dfb4fWLDnAiS8Aad1jfz3GZMCSvVdh166CLpnLJUGXDKVvaFDGIJzT/xwcnHMwUk2pSNTt+6wwIopfshtjgl6PWrdbtTE2pNb1enVcLEjllbQgNpyBJfOuZLi6hBjS9tgUqQ6TCippl5TB802RKjEJ0046KTDDUSrCZFB7LEhYI8HIiy++2KLP81uDmWtyWyqj5L2TaraSkhI8/PDDKrQR0ipaX92OlHUVYULOl9BLWvoklGxuY8aMUTO15OssoZK0Dza1w7X8HZB2RGlLlb8rDR144IHquqUS6+ijj97l3wEJ16StVMjzSQVcU7PDdkfeD/m7KR//+te/VKAp7aPy93BPMPAiIiKilg277M6owi4JHZ75bi1mLisIrh3QOQX3TRkEY4QQS4Vd1jgOu6rLAKe9da9BWhd/eBBYUe+HpZQuwIn/BaytsG15Yipgzmy2oGujbaMKuhYUhA/WFf3T+6ug65CcQ5BmSkOSPmmfL5+I4p9pQH8YundXA+plZlf9kEH+98dns8HYp486rjnJTnnSFiYhkLSOSWudBC8yHFzCiIYkoJLB7dLGJoGWVGU1JPOUpH1Pdi2U4eoyv0paJmWAvaxJm6JUdX3++ecqWJHXKgPYI1UgNRdpfZPXXFdt1lK2bt2K6667DldccYVq8ZOKKalqEjJrSgItWfvHP/6hhrbLDK36pNVR3hsJoGTAvwQ7Uh110003qXZQOX/48OEoLi5Wg+alXbM5HHvssZgxY4bakVH+7kklV8PQS3aOlEH6u6qiklZGqQKUmW/ymiUAk4Dv+++/V9Vc8nrk74AM5v/111/V4z355JMqKN2TwEvabiVYk5lf0nYqfzflfZL3bk8x8CIiIqIWDbtcntBvMXdFvvl6fu56TF8c2g58cEcrHpgyOGKIJd+w5ViNSDTEadjltAUCr9bk8wBzbgfW1dtePrNvoI0xKTCAN6bkOffweXcVdG2xb8H7q97HL/mh7djr9E7tjXP7n4vD8g5TQZdZ37gNh4hoV2QQfebll6ndGGVAvczsUm2MbrcKuzRms7q/OQfWCwlQJCyQ1jKZrSTD2aXiSEKhW2+9tclzJOSScEpCL41Gg5NPPjn8tSQkqHBLdgqUIE0CGRksfswxxwR3E5Tnk/uOPPJIZGZmqhDHbo/NL2skqJPnbGkS9sh8KRmiLlVdMpRdBvYLaWGUsEbeYxlWL9VI0t4pVUp1ZKi7DJy/+eab1Y6H8nhyjoSD8hqk9VSCRGkfldCsOY0cOVJ9DWUYvXytm6qGixQYynB62Yzg+uuvx/bt29WcLglCJewS8jpkrpu0cUpYJe+NBKIyBD9acg1SJSfBogRfEqZNnz5dPdeeSqjdlybV/Yj8h5qSkqK+UFartbUvh4iIKK7Crv/9uAGfLNwWXBuQZ8Wjpw5GkmH3v5uTb8Czk40wG+P0d3jSwihD6luTuxqYdQOwtV4bR94BwMSnAy2FsWbJAkwp+xx0yXD6D1Z9gB+3/YhahH873COlh6roOiLvCBV0WQyB2SRERHuj6rff1G6MMqBezfTS61Xll4Rd5mHD+KYStRAGXlFi4EVERNQyYdcrP23Eh39sDa71zU3GY6cOgSWKECvbaorquHZJdmK05wfmZrVmddn0fwOFS0NrXY4Ejn8U0Md4fpW0Xsi8MKNln4KugqoCfLT6I3y/9Xv4a8Nbbbpau+LsfmfjqI5HqRldyfrkJuecEBHtqVq/X+3GKAPqZWaXtDE2d2UXEYWL0+8QiYiIqD2EXa//siks7OqVbcGjpwyOKsTKSjbGb9jldbV+2OUoBqb9CygLbDWu9B4HHHcPoN2zXZL2mfxQmNwB0EfeDdHr96LCVdEo6CqpKcHHqz/G7M2z4asN//vZydJJBV3HdDpGBV1Wg5VBFxE1Kwm3EgcN5LtKFENx+l0iERERtUbYVRBl2CXemr8Z7y3YErzdI8usKruSTZHDlMxkY1THtUs+b+uHXRVbgS//CVTmh9YGnQoccyOgifGsNK0uEHbpAjtb7WnQVe4sx6drPsWsTbNU1Vd9eeY8nNn3TIzqMgrppnQkG5KhSWDFBRERUTxg4EVERETNFnY5owy73pm/GW/P3xy83S0jCY+fOgQpiZFDrAyzEdZ4DbtkJ0T79sDn1lKyBph2JVBdGlo75BLg8P8LtBXGkoRcEnZJ6LWboEtaFx0eR1jQZXfb8fnazzF9w3S4fe6wc7ITs3FG3zNwXNfj1IyuFGMKgy4iIqI4w8CLiIiIYhp2vb9gC974dVPwdtf0JDx+2lCkJu2+gkekmw1ISYrTsEvCmsodgR0RW0v+38BX1wBuR2jtqOuBA86O/bXIjLDkvEA74x4EXQ63A1+s/wLT1k9Djbcm7Byp4jq9z+kY132c+nOKIQXaWFesERERUUww8CIiIqKYhV0f/r4Fr/68MXi7c1oinjh9qAqyIklLMkQVirVbshujx9l6z7/pJ2DWTYDPFbidoAVG3wn0mxj7a5HdHy3ZTVaUSdAl1VuV7sqwoEvCrenrp+PzdZ+rtsb6Uo2pOLX3qTi+x/HIMGWoii6dht8GExERxTP+S09EREQxCbs++XMrXv4pFHZ12oOwS4KutCiOa7ccRYA7PKSJqdUzge/uDrVSag3A+IeB7iNify2JqYA5s9Gyz++DzW1rFHS5fC7M2jgLn6z5RAVh9Vn0FpzS+xRM7DERmUmZKujSa+K0QpCIiIjCMPAiIiKiFg+7PvtrG178cUPwdsfURDxx2lBkWowRz5W5XtGEYu1WVSngDA9qYmrx+8BPT4Ru683ACU8CnQ6J/bWYM4DEtKiCLhlAP3vTbHy85mOUOcvCzknSJWFKrymY3HMyspOyVYWXPtY7SxIREVGrYuBFRERELRp2Tf17O16Yuz54Oy/FhCdPH4qs5MhhlzVRj4woQrF2q6Y88NEaJDxa8CLw52uhNQmbJj0HZPeP7bVI66K0MEorY72gq6510V/rD1ufu3UuPlj1AYpqisIexqg1YlKPSTi598nIMeeooMsg1WpERES032HgRURERC0Wdn25KB/Pfb8ueDvXGn3YlWzSR1UB1m65KgPVXa1BWhd/fARY/lloTQbET34BSOsa+7BLntuQFLi0Wj/sLrsKu+oHXfLnn7b/hPdXvo/8qvywh5A2xeO7H6/mdOVZ8tTOixJ+ERER0f6r6W1viIiIiPYx7Jq2OB/PfLc2eDs72ajCrhyrKeK5FpMuqlCs3XJXB+Z2tQafG/jmlvCwK70ncMrrsQ+7ZIfElE4q7JJAq8JZgW2V21DhqgiGXdLGOD9/Pq6eezUe//PxsLBLm6DF8d2Ox8tjXsZVB16Ffhn9kGvOZdhFRPu9Cy+8EAkJCY0+jj32WGRmZuL+++9v8j166KGH1P1utzuq93Du3LmYMGECMjIykJSUhAEDBuD666/H9u3b9/uvAbW+hNr6wxBol+x2O1JSUmCz2WC1WvlOERHRfmVPw66vluTjyTmNw64OqYlRhV3ZyZFDsXZLdmK0bw+0FMaaDMafeT2w7Y/QWu5QYOJTgCklttciM7WsHeDXaFXbolR1+WpDf7/kW9S/i/7GOyvfwbqKUJWg0ECDUV1G4cy+Z6KLtYtqXUzSByrEiIjaolp/LYq3VsLp8MBk0SOrczISNI13om3OwKuwsBBvvPFG2LrRaMQ999yDr776CmvXrlUhWH19+vTBCSecgKeeeiric7z00kv45z//iQsuuADnn38+unXrhi1btuDtt99WPzM/+eSTzf66iPYEA68oMfAiIqL91Z6HXTvw5Jw1wdtZFiOePGOoGlQficWoQ3YUFWDtltcN2LfJmxr7564uA6ZfDRSvDK11HQ6MfwTQR/7aNCudEbXJebB7qxoFXWJ5yXIVdC0vXd7o1KM6HoWz+52NHqk9VNBlliH7RERt2LZVZfjrm80oL6iG31cLjTYBablJOGhcV3Tql95igVdFRQW++OKLRvctXboUQ4YMwQ8//IARI0K78f7000845phj1P1SqSVVYC+//DKKi4vRv39/PPzwwxg/fnzgNW3bhp49e6rAq6lwTJ47NTW1RV4bUbQ4w4uIiIh2Saps9iTsmtEg7Mq0GFRlVzRhl9kY522MPi9Qmd86YZd9BzDtX0DF5tBan+OB0XcFKq1iqFafCLvRAnv1DjWAvr415Wvw7sp3VWVXQ4fnHo5z+p+DXmm9VNBl0VsaVSYQEbXFsOuH91bD7fTCZNZDq9PA5/WjZHuVWh95Tt8WC712ZfDgwTj00ENV9Vf9wOv111/HYYcdhkGDBqkQ64knnlBVXAceeKC6b/LkyVi+fDl69+6NTz75RLU93njjjU0+B8Muags4w4uIiIh2GXYV2l1Rh12zlu7AE/XCrgyLAU+dfgA6pkUXdknbY9wGGBLsSNgloVeslawFPrsoPOwaehYw5t6Yhl3y98mu0WCbxo9yV3lY2LXZvhkPLHgA1/94faOw64CsA/D4MY/jriPuwkE5B6GTpROSDcnx+3eFiOKqjVEquyTsMqcaoTNoVRujfDanGOB2+dT9clxLkLZFi8US9nHfffep+y6++GJ8+umncDgc6rZ8lhDrkksuUbcff/xx3HTTTTjzzDPRt29fPPLIIzjggAPw9NNPq/ulHVLaFvPy8lrk2omaAyu8iIiIqMlwoqjShWp3dAHNrGUFeHx2vbDLLGHX0KjCriRDnIddMqvLnh9oZ4y1/L+BGdcGdoSsM+xfwMEXBXZHjNHfJYe3GjadDl6tORD+1V2eIx8frPoAP277EbUI/4Gvf3p/nDfgPAzNGgqrwQqr0QpNAn9XS0Tth8zskjZGqexq+G+c3DYl6dT9clx21+afEz1q1Ci8+OKLYWvp6YFqsrPOOgvXXXcdPvroIxVyyWf532sJuGScT35+PoYPHx52rtxevHix+rMcG7f/blPcYOBFREREjRQ7XKhyRRd2fS1h1zerg3GFhF3SxtgpLSmqsCvHGudhV+UOwOuK/XNv/BH4+hbAt/O5JSwaeQsw8OSYXYLDU40KrwNekxUwhGZtFVcX46PVH2HOljnB3Rjr9EzpiXMHnItDcg5RIVeKIQVa2c2RiKidkQH1MrNL2hibIuuuaq86riWYzWb06tWryftkQ7ZTTz1VtTVK4CWf5bZUbUngJRr+21w/5JLh9rKh244dO1jlRW0Wf01GREREYYorXXA4owu7vllegMfqhV3pZgOeOH0oOqcz7FIchYC7OvZ/w1Z8Ccy8IRR2aQ2B4fQxCruqvDXYXl2EEncFvInpgMGi1itcFXhl6Su44tsr8M3mb8LCrs7JnXHLYbfg6VFPY2TnkeiU3AnppnSGXUTUbslujDKgXmZ2NUXW5X45rjVI0PXLL7+o1kf5XNfOKKFXhw4d8PPPP4cd/+uvv6rh9ULCMYPBgEcffbTJx5ah9UStjRVeREREFFTqcKHS6Yk67Hr06/Cw68nThqILw64ARxHgCsxGiWlF2cI3gN9eCK1J2HTCU0DHg1r86au9NahwV8Lt9wBSlZWcq8I2h9uBqeumYtr6aXD6nGHn5Cbl4qx+Z2FE5xGqdTHVlAq9pnV++CMiak5ZnZPVbowyoN6s14RVTEm1lLPai8yOZnVcS3C5XCgoKAhb0+l0yMzMVH+WgfVSAXb++eerz7JDY50bbrgBd911l9qJUWZ3SQXYokWL8N5776n7O3furAbbX3nllaoiTB6jW7duavfGt99+W80Lk6H3RK2JgRcREREp5VVu2Gr2LuxKS9LjidOGoEsGK7uUqlLAGWgJiRmplvr5SWDxB6G1pAxg8gtAZu8WfepqrxM2TyVcvp1zymQYviUHTr8H09d8gs/WfoYqT1XYOVK9dWbfMzGm6xg1hD7NlAaDVKIREcUJGVB/0LiuajfGKptbzeyq26VRwi6DSavul+Nawtdff92o3VAG0K9atSp4W4bX33rrrSrgqu/qq69WQdb111+PoqIiDBgwANOmTVM7NNb55z//qVobZcD9SSedhJqaGhV6TZw4Uc0HI2ptCbUSLVNE8h+79DlLn7KUeBIREcUTW7UHpVWuvQ67ZGZX14zQjKZdSTRokWs1xe/MLlFTHgi8YsnnAb67G1jzdWgtpTNw4guAtWOLPa3T51IVXfI5SG+Ex5SGb7bMUXO6pI2xPqniOq3PaTi++/FIMaYg1ZgKk87UYtdIRNTatq0qU7sxyoB6meklbYxS+SVhV6d+gSHyRNT8WOFFRES0n7M7ow+7ZEB9/ZldDLsacNpiH3a5q4BZNwJbfwutZfUHJj0LJLXMD1JSyVXutocHXZK76UyYW7pU7bxYVFMUdl+SLgkn9ToJk3tOVm2LEnQl6SNXBBIRtXcSanXsk6Z2Y5QB9TKzS9oYW6qyi4gCGHgRERHt52FXSSXDrmbhqgQcxYip6jJg+tVA8crQWqfDgAmPh+2K2FzcPg/KPXbUeMPncMnw+fm2tXh3/VRsc2wLu0/aFCf1mISTe5+s2hgl6LLsHGJPRLS/kHAruys7hYhiiYEXERHRfkrmdcmQ+r2t7Hrq9AOimtm1X7QxSpWVDKmPJdtWYNqVgK1ewNR7LHDcPYFdGZuRx+9RrYuy+2J9Mhnjr/KVeGfzTKy3bwq7T5egw7hu43B639ORlZSl2heT9cnx/feAiIiI2gwGXkRERPuhPZnZxbArAtmJ0VEY2CExVopWBiq7aspCa0PPAo66TsoImu1pPH4vbO5KOLzVje5bYVuPtzdOx3LburB1DTQY2Xmk2nmxg6WDCrpkbheDLiIiIoolBl5ERET7mX0Ju9LNBjx52lBWdoW1MRbFNuzaugCY+R/AUy+EOuIq4KALgGaqnvL6fWrXRQm6Gu5vtMGxDe9snI4/y5Y3Ou/IDkfinH7noGtKVxVyyYdWo22WayIiIiLaEwy8iIiI9iMV1W6UVbmjOnbW0h14fPYahl27C7sqCxFTa74Bvr0T8HsDtxO0wLF3AP0nNcvD+2p9sLkdqPRWNQq68quL8N6mGZhXvLDReQdmH4jz+p+HPul9YNab1ZwunYbfZhIREVHr4XciRERE+4k9CbtmLNmBJ+asCd7ek8quJIMOOVZjfLewOe2xn9m16H3g5ydCt3Um4PhHga7D9/mhZei83eOA3VOl/lxfiascH2yahW8LfoMf4ff1S++H8/ufj8FZgwNBlykVeo1+n6+HiIiIaF8x8CIiItoPlFe5UV4dXdj11ZJ8PDlnbfB2htmAJ04fii7pDLsUpy22uzFKADX/eeCvt0JrphRg4jNA7uB9e+ja2p1BlwO+BkGXzePAp1tmY8b2efDU7qwo26mbtZuq6Do091Ak6ZOQZkpTuzESERERtRUMvIiIiOLcnoRd0xbn4+lv64VdlkBlV2eGXa0Tdvk8wPf3AqtnhtaS84DJzwNp3fYp6Kr0Vqs5XT6/L+y+aq8TX26bi6nbvkONzxl2X545D2f3OxvHdDoGibpEFXSZpNKMiIiIqI1h4EVERBTHpIVRWhmj8eWi7Xjmu9COe5kSdp0+FJ3SWNml1FQAVSWIGXcVMOuGwJD6Ohm9gUnPAZasvX5Yh6caFZ5KeOvmgNU9nd+DWfk/4+Mt36iKr/rSjak4s9/ZGNN1DBL1iUgzpqnKLiIiIqK2ioEXERFRnCp1uGCr8UR17NS/t+O570NhV5bFqMKujmmJEc/dL2Z2xTrskuf66mqgeHVoreMhwITHAWPyXj1ktbcG5e5KePyeRoPqvy/4He9vnqnmddVn0SXhtF4nY0KvE2ExWNQwevlMRERE1NYx8CIiIopDJQ4X7FGGXZ//tQ3Pz10fvJ2dHAi7OqQy7GqVsKt8MzDtSqAyP7TWexxw3N3AXszJkhZFaV10+dyN2hrnlyzGO5umY1t1+G6TJo0BJ3Y5Dif1OwvWxAxYjVZYDdb4DjWJiIgorjDwIiIiijPFlS5UOqMLuz5duA3//SEUdkmlloRdeSkMu1ol7CpYCnz178CssDoHnAMMvwZI0OzRQzl9LlS4K9XnhhaXr8ZbG6dhbeXmsHVdghbjOxyF07tPQkZGH1hNaSrs0uzhcxMRERG1NgZeRERE+2nY9dEfW/HSvA1hYddTpx+A3JTIQ8j3izZGCZ1iGXZtnAd8czPgrRdQDb8WOPDcPXoYt8+Dco8dNd7wgfNijX0z3t44DYsr6rVKAtAgASNzDsPZ3SYg19oFltRuSDWlQavR7v3rISIiImpFDLyIiIjiRFGlEw5n+CDyXXl/wRa8+vPG4O1cqwlPnjFUfY5kvwm7Yrkb4/KpwA8PArX+wG2NDjjuXqDPuKgfwuP3osJtR5W3ptF9W6sL8O7Gr/BryaJG9w3LGIJzu09CV3MeLMl5SEnrDr1Gv2+vh4iIiKiVMfAiIiKKA0V2Jxyu6MKud37bjDd+2RS8nZdiUm2MOQy7Yh921dYCv78E/PFKaE1vBk54HOh0WFQP4fX71K6LVd5qNZervmJnOT7YPBPfFfwGP8LvG5zSG+f3mIx+1u5I1CUiLa07DOa93/2RiIiIqC1h4EVERNSOScAhbYzRhF1y7FvzN+Pt+aG5TZ3SEvHEaUORlWyMeP7+Udllj13Y5fMEqrpWTgutJWUCk58DMvtEPr3WB5vbgUpvVaOgy+5x4JMtszFj+zx4asP/bvS0dMb53SfjwLR+MOmMSDOmwJTaDTAkNd9rIyIiImplDLyIiIjaKQk5iipdqIoy7Hrj101497ctwbXOEnadPhSZFoZdobCrCDHhrga+vgnY8mtoLa07MOk5wJq321P9tX5Ueqpg8zjUn+ur8bkwbdtcfL71W1T7wmd4dUjMwrndJmF41gEwao1IMyQjyZgMJHcAdHu++yMRERFRW8bAi4iIqB2SAKvQ7kK1O7qw65WfNuLDP7YG17qmJ6mwK90cOejYfyq7YhR2ySB82YmxeFVorcOBwIQnAFPKbr+Old5q2Nx2+BoEXTK/a/aOX/Dh5q9Ve2N96YYUnNX1eByXewSMOgPS9FZY9EmA3gQk5wEcTE9ERERxiIEXERFROyPBR4HdiRq3L6pj//fjBnyycFtwrXumGY+fNgRpSZHDLrNRh+zkOA+7XI7YhV3lm4BpVwGV+aG1nqOBMfcBul1X2jk81SrI8vrDA06p8JpXtBDvbvoKhc7SsPvMukSc1nksTug4AmadCVZDMqw6c+BrKZVdlmwgnr+uREREtF9j4EVERNSO+P2BsMvpiS7seuGH9fj8r+3BtR5ZZjx+6hCkMuwKcFcBjkLExI5FwFfXAS5baG3oWcBR1wEJmiZPqfbWoNxdCY/f0+hru7BsBd7eOA0bq0JfX2HQ6DG540ic0nkMkg1mWPUWpOgt0NQ9R1J64IOIiIgojjHwIiIiisOwy19bi2e/W4dpi0OVRL2yLHjstCFISdRHPH+/qOySOVqVBYGdElva+u+B2bcDPldobfi1wIHnNnm40+dCudsOl8/d6L5V9o14a8OXWGZbF7augQZj847AmV2PR4YxFRa9Gan6ZOjqWhblaylVXVLdRURERBTnGHgRERG1A76dYZcryrDryTlrMHNpQXCtT44Fj54yBFaGXQGeGqByR2zCrsUfAj89LnVZgdsaPTDmXqD32EaHun0elHvsqPGGD5wXW6sKVEXXb6VLGt13VNZBOK/bRHRIykaSLlENpNfL89SR0EvmdcncLiIiIqL9AAMvIiKidhB27bDVwO31R3Xs47NX45vloTa9frnJKuyymCL/s79fVHZ5nIA9v+XDLhks/8vTwKL3QmtSXSXD6TseHH5Jfi8q3HZUeWsaPUyxsxwfbJ6J7wp+g78uNNvpgLR+uKD7ZPRK7gKT1ohUQ7L6HEZ2YJSdGLX8to+IiIj2H/zOh4iIKI7CrodnrcJ3q0ID2Ad1sOKhkwerICuS/SLs8roCA+NbOuyS5/n2TmDdt6E1Sw4w6Tkgo2foML9PDaOv8laruVz1VXqq8OmWOZi+/Qd4asOH1UvAdWH3EzE0ra+q5EozWJGka6J6y2AGknM5nJ6IiIj2Owy8iIiI2iivz48dNic8Pn9Ux94/cyXmrSkJrg3tlIIHTxqMRMPOGU7Y38MuN2DfLsPQWvZ5aiqAmdcBOxaH1jL7AhOfASxZwd0VbR4H7B5Ho6DL6XPjq+0/4tOtsxtVfHVMzMZ53SfhyMwDoNPqkKa3wqJPavo6EtMAc0YLvEAiIiKito+BFxERUTsPu6T6676vVuCX9aXBtYO7pOK+KYNg0jPsUnye2IRdtm3A9KuBis2htS5HAOMfUdVWEm7ZvVWwuyvhk5bHeny1Pnxb8Bve3zQTZe56OzkCSDdYcVbXCTgu9wgYtXpYDcmw6sxNB5SyZs4CTNYWe5lEREREbR0DLyIiojZGQq6CPQi77p6+HL9tKAuuHdYtDfdMHggjw64GYVfkgf/7pHA58NU1QE3oa4H+JwIjbwG0etWiKO2LvgbXISHY/JLFeHvjdGyvCc1eE2ZtIk7pchwmdRyFRJ0RVr0FKXoLNAmapq9BownM6+JweiIiItrPMfAiIiJqQyTk2lHhhDeKSiTZsfGOL5fjz83lwbUjemTgrkkDYNDtIhCpx2LUISve2xh93kDYJZ9b0sZ5wDe3APV3Vzz8H8Ahl6La50R5dTk8fk+j05ZVrMObG77A6spNYev6BB0mdhyBU7uMQYohGWZdElL1ydDJbou7oobT56lwjYiIiGh/x8CLiIiojZBqLansiibsqnH7cNsXy7Boa0Vw7ejembj9hP7Qaxl2hcKubS0fdi39BJj3aGBXRiGh1Kg74OwzFuXOErh87kanbHJsx1sbp+HPsuVh6xok4Njcw1X7YrYpHUm6RKQZktVg+t3SJwbCLqnwIiIiIiIGXkRERG0l7JLdGGWnxUiqXF7c8vlSLMu3B9dG9c3CLcf3gy7KsCvb2sSOfvEkFmGXBFy/Pgv8/U5oTW+GZ/xDKMvph5qa0AYCdYqcZXhv01eYW/gHahH+tT48YzDO6z4ZXc15MGmNaudFo9YQ+TpkVpfM7IrnSj0iIiKiPcQKLyIiolbm8vpUZVc0YVel04ObPluKVQWVwbUxA3Jw47i+0GoiBx4Wk+zGyLBrn0nr4pw7gfXfBZdqzVmoGPsAbCm54a2NgNqN8ZMts/HV9nnw1oaHcP2tPXBBjxMxMKUnDBo9Ug1WJOmi/BolpQc+iIiIiCgMAy8iIqJW5PT4UGiPLuyyVXtww2dLsK7IEVybMDgX143pA00U1T37T9jVwjO7asqBGdcDBYuDS970nig47g54G4RPTp8b07f/gM+2zEGVrybsvs5Jubig+2QcljEYeq1eVXSZdYnRXYN8vS3ZgDG5eV4TERERUZxh4EVERNSKYZdUdvlrI4ddZVVu3PDpEmwsqQquTTmgA648thfDrkZhV+Ph8M2mYgsw/WrAtjW4VNPxIBSNuBG1hqTQpdT68G3BAry/aQbK3Lawh8gwpOKcbifg2NzDVEVXisGKZF1S9JsHqJ0Y8wJzu4iIiIioSQy8iIiI2njYVVzpwn8+WYyt5aEKodMO7oR/jOgRVUiSbNKr3RjjWizCrh2LgBnXAc5QgFXZZyxKh/0D0AS+paqtrcWC0qV4e+M0bK0uCDvdrE3EaV3Gqt0XE3UmWPVmWPUWaBL2YNC8VgckdwjsyEhEREREu8TAi4iIKMZkh8UCu1OFI5HIcdd/vBg7bKGZUOcc3gUXD+/GsCuWYde6OYGZXfV2XCw/6HzYBp8SHBa/wrYeb274EivtG8JO1SfoVMglYZfVYIFFl4RUQzK0Cdo9uwadEbB2COwCSURERES7xcCLiIiojYZd+RU1uO7jxSiqdAXXLhreDecN6xrVc1kT9ci0xHtll6dlZ3bJ10l2Yfz1mdCSRoeSo65BVY9j1O2tVQV4a+M0LChdEnZqAhJwbM5hOLvbCcg2pav5XDKQXr+zGmyPGC2AJYc7MRIRERFFiYEXERFRjFS7vSi0u6IKu7aUVuP6Txej1BGqKLrimB4449DOUT1XSqIeGfEednndgbDL72uZx/d54PvxYWhXfBFaMlhQNPo2uHIGotRVgQ82z8ScHfPhR/jX9JD0gWogfTdLR5i0RjWQ3qjdyzZE7sRIREREtMcYeBEREcVAlcurKrWiCbvWFztwwydLUFETatG7clQvnHxQx6ieKzXJgHRznM94auGwy+u0wT/rBhi2LwyueZJzUXjcnbCbM/DZxun4Ytv3cPvD2yj7JHfFhT1OxODUPmogfZoxBYnavQweuRMjERER0V5j4EVERNTCHC6vGjwfTdi1uqASN322BHZnoEVPpkNdO6YPJg7Ji+q50pIMSIv7sMu1M+zyN/tDy+6KlWUbkPT1zTCUbwquO7P6YfuomzCjbCk+Wv487B5H2HkdErNwfvfJODLzAOi1elXRJS2Me03mdKmdGE378nKIiIiI9lsMvIiIiFpQpdOjwq5oLNtuwy2fL0WVO1C1pEkAbhrfD2MG5ER1vlR1SXVXXPM4A2FXFOHhnpAw0u6tQk3+38j89l7oasqC91V2OxJT+47E20ufRaGzNOy8VH0yzux6PMblDYdRq0eKwYpkXVJUGwrsdji9hF2yIyMRERER7RV+J0VERNRC7E4PSqIMu/7aUo7bpy6D0xuoWtJqEnD7Cf0xok9WVOdnmI1ISdIjrnlqAHt+s4ddDk81yj12GDbPR/aPj0EjFWQ7fd9/LJ7TVWHdmnfDzjFpDDip82hM6TQaZn0irHoLUvQWaBI0+3YxHE5PRERE1CwYeBEREbUAW7UHpVXRhV0LNpbirmkr4N4Zdum1Cbh70kAc0TMjqvNlOL0MqY9r7mqgckezhl3VXicq3HY1hyt55VdI//1VJNQGvgarDQY81mMIFjhXhZ2jgQbjOgzHWV2PV22LFr1ZVXnppAVxX3E4PREREVGzYeBFRETUzCqq3SirCu2uuDs/rS3BfV+tgNcfCHKMOg3unzIIB3dNi+r8rGQjkk1xHna5HICjsNnCLpfPjXK3HU6fSw29T//jdVhXTlf3FWi1eD4jA9PMJtS6isLOOyJzqNp5sWNSDpJ0iUgzJEOvaYb3XtofzVmAybrvj0VERERECgMvIiKiZiRBlwRe0fhuZREemrUSO7MuJOq1eOjkQRjSKTXiuTIjSsIuizHO/yl3VQKOomYJuzx+r6roqvLWqNsJnmpk/fg4krb9icqEBLyWasW7KVa4GszfGmDtiYt6nIh+KT1g1BpUZZdpb3debEieS+Z1GZKa5/GIiIiISInz75KJiIhip9Thgq3GE9WxM5fuwBOz16AuxpHg6pFTBqN/njWqsCs72QhzvIddThvgKG6WnRdtbgcqvVXBnTK1VcXI+fZ+JJRvxLvWZLyUakWFNrwtsWNiDi7scSIOzxgMgwq6klVlV7ORNkhrh8CQeiIiIiJqVnH+nTIREVHL8/tr8fO6Euyw1SDFZECvHDM0u9ml77O/tuGFueuDt2X+1qOnDEbvnOSowq5cqwmJhmaYGdWW1ZQDVeE7Iu7VzoseB2weB/w7Z3MJQ8k6ZH13P77TOPFMpzxs04e3JUoF19ldJ2BM3hEwaJpp58WGdAYguQN3YiQiIiJqIQy8iIiI9sEva4vxzHfrsLaoEh5frRo43yPTgrMP74wDuzSew/Xegs147edNwdsZZgMeO20IumWYIz6XhGi5KSaY9HEedlWXBT6aYedFn98Xtp60+TdsWvAMrk+zYJnsiFhPotaIkzodhymdj4VZJzsvmtXui/u882JD0r5oyQU0zfy4RERERBTEwIuIiGgfwq5/f7RIzezy+6HaE6UGaPG2cmwqdeDWCf2DoZdUG73280a8//vW4Pk5ViMeP20oOqZGbpPTahKQY90Pwq6qEqCmoll2XgxTW4uyxe/i8fwf8GNORqOdF8d3GI4zW2LnxYZkML0MqG/OajEiIiIiaqRVf7U4b948TJo0CR06dFBtAl988UXY/fLDwd13363uT0xMxMiRI7F8+fKwY1wuF6666ipkZmbCbDZj8uTJ2LZtW9gx5eXlOO+885CSkqI+5M8VFXv/zTQREZHP58d9M1agxOGG1w9Iw5wEXvJZbpdWefDSj+vhr61VH9LCWD/s6pSWiGfOOCDqsGu/qOySeV17GXbJzosFNSUocpY2CrvKnKV45ac7cFHFb/jRnNho58UXDr0N/9f7DLX7YsekbGQaU1sm7EpKByzZDLuIiIiI4j3wqqqqwtChQ/H88883ef+jjz6KJ598Ut3/xx9/IDc3F2PGjEFlZWXwmGuuuQZTp07Fhx9+iJ9//hkOhwMTJ06EzxdqYTj77LOxaNEifP311+pD/iyhFxER0d7O7Jq7uhjrihy7PW5DSRVW7ajEk7PX4PO/twfXu2ea8fQZByDbaor4XDqNBnkpiTDq4jzsqiwMDKnfi50Xi51l2FFTDKfPFXZfjc+F99dNxRW/3YVptRXw16uq6p/UAY8ccC1uHXgZeiZ3Rm5iJrJN6dBrwud5Nd9OjDmBwIuIiIiIYiKhtm67olYmFV4SXE2ZMkXdlsuSyi4JtG666aZgNVdOTg4eeeQRXHHFFbDZbMjKysI777yDM844Qx2Tn5+Pzp07Y+bMmRg3bhxWrlyJAQMG4LfffsPhhx+ujpE/H3HEEVi1ahX69u0b1fXZ7XZVHSbPabVG3kGLiIjik89fiwK7Ex8s2Izn6w2e35Xe2RasrReM9c1JxsOnDFaD6iPRazWqsks+xy35NqSyAHBX7fPOi/Xvm73jV3yw8SuUe8Mft4s/Aef2ORPDOhwJvVavWhhlXleLkUqx5DxAHzncJCIiIqL9YIbXxo0bUVBQgLFjxwbXjEYjRowYgV9//VUFXgsXLoTH4wk7RkKyQYMGqWMk8Jo/f74KqurCLjFs2DC1JsfsKvCScE0+6gdeRES0f5OwS3ZidHv9KLaHVxPtSv2wa3BHKx44aTAsxsj//ErIlZdigi6ewy4ZfFa5A/DU7PPOi3X3/V66FG9u/BLbqgvD7kv3+XCxJgtHDb8ZBqMFVkMyrDpz8+682BB3YiQiIiJqNW028JKwS0hFV31ye/PmzcFjDAYD0tLSGh1Td758zs7ObvT4slZ3TFMeeugh3HPPPc3yWoiIqP3z+vzYYXPC4wuELJlW4x6df3DXNNx74kAkRjGHa/8Ju/IBj3Ofd14Ua+yb8PqGL7Dcti5s3eT34zx7JU7pPAbeg85XQVdKS+y82BB3YiQiIiJqVW028KrT8Dev8tvbSL+NbXhMU8dHepxbbrkF1113XViFl7RKEhHR/kdCroJ6YZcY2CEF2gRpn4t8/pE9M3DnxAEw6CKHLHKMzOySQfVxSwIr+3bA647qcJnFVe6yNd55UX6xVVOCtzdOw0/Ff4WtJ9TWYoqjCv+0OaAddiUS+k5AjsHaMsPoG0pMBcyZLf88RERERNT+Ai8ZUC+kCisvLy+4XlRUFKz6kmPcbrfahbF+lZccc+SRRwaPKSwMb2sQxcXFjarH6pP2SfkgIqL9m7QvStjllYqkevrkWNAzy4w1RbufPXVsvyzcPL5fVNVaRr0WeVYTNPEcdvm8gbDL1zi8asjt86iKrhpv4yqwSk8VPtr8NWbkz4O3Nrzia3h1Da4rq0BPbRIqxj2M5M5HwqBtgWH0Dckv0iToMqW0/HMRERER0W612V6J7t27q7Bqzpw5wTUJt3788cdgmHXwwQdDr9eHHbNjxw4sW7YseIwMp5dB87///nvwmAULFqi1umOIiIia4vL61MyuhmGX0CQk4IoRPZFh1qtKr6YiqmE90nHL8f2jCrtM+0PYJRVdtq0Rwy6v34cSZznya4oahV0evwdTt36Hy36/G19unxsWdvVzufHyjkL8r7AY3c15cJ/8KjK6jYhN2KXRBIbTM+wiIiIiahNatcLL4XBg3bp1YYPqFy1ahPT0dHTp0kXt0Pjggw+id+/e6kP+nJSUhLPPPlsdL4PnL7nkElx//fXIyMhQ5/3nP//B4MGDcdxxx6lj+vfvj/Hjx+Oyyy7DSy+9pNYuv/xyTJw4MeodGomIaP/j9PhQaHeqQfW7cmCXNNw6oT/e+GUjVuyoVBsO1hnRJ1O1MUYzFD3RoEWu1dSyA9Rbm9cVqOxqIjysI0PoZRi9DKVvuPOi3P6peCHe2jgdRc7SsPtyfX5cXVaGExzV6jd53s6HQz/+EeiNyYgJCdQk7JIh9URERETUJrRq4PXnn39i1KhRwdt1M7MuuOACvPnmm7jxxhtRU1ODf/7zn6ptUXZanD17NpKTQ9/APvXUU9DpdDj99NPVsaNHj1bnarWhGR3vvfcerr766uBujpMnT8bzzz8f09dKRETtK+ySNkZ/g9ClKWlmA3bYXKifi11wRFecf0TXqAKsJIMOOVZjfIddMphewq5dvJ8SZlV6q2Fz2+FrsPOiWFaxDq9vmIq1lYFNa+qYE/S4pLwM51VUwLTzsWsHnw7d0dcDmhh9i6NPBJJzgVjMBiMiIiKiqCXUNvwVKjVJhtZLRZm0QlqtVr5LRERxak/CrjWFlbjps6Ww1YRa9P5vZE+cdnCnqJ7LYtQhKznOwy53FVBZsMuwq9pbg3J3pWpVbGhbdSHe3PAlFpQuCVvXJmhwoiEP/16zAOk7K8ZqEzRIOPo/wJAzEDMmK2DOCszuIiIiIqI2pc0OrSciIoq1GrcPBXZno3a6pizdZsOtU5eiyh2YISWRx7Vj+mDikNBGK7uTbNKrsCuuuSoBR1GTYZfL50a52w6nz9XoPpu7Eh9snoVZ+T/Dj/CKryMyhuCasnIMWPVLaNFgQcL4h4EuRyBmzBlAYmjDHCIiIiJqWxh4ERERSaWR24tCuyuqsOuPTWW488vlcHkDYYxWk4Bbju+HY/tlR/VeWhP1yLTEedjltAGO4kbLHr8XFW47qrw1TYZg07b/gE+3zEa1L3xYfd/kbri00xiM/PN9mIpWhu5I6QSc8DSQ3h0xIdVc0sJoMMfm+YiIiIhorzDwIiKi/Z7D5UVxZXRh149rivHAjJXw7hzapdcm4K5JA3Bkz8yo3sfUJAPSzXE+3Ly6LPBRj6/WB5vbgUpvVaP3WYbV/1j0J97eOB0lrvKw+3JMGbig+4kYpUtH7vcPQOcoDN3Z4WDg+EeBxFTEhFa3czh9nIeVRERERHGAgRcREe3XKp0eFXZFY9ayAjwxe3VwQH2iXov7pwxUuzVGQ4IuCbziWlUJUFMRvCnhlt1bBbu7ssmB9Esr1uC19VOx3rE1bN2iS8IZXcfjhA5HIzV/CTLn3IQET72qsAEnAiNuCeyQGAsSclk7cDg9ERERUTvBwIuIiPZbdqcHJVGGXZ/9tQ0vzF0fvJ1s0uHhkwejf150G5lkWIxISYxRONNaZF6X0x68KW2LMqfL6/c2OnRrdQHe3PAFfi9dFrauS9BiYscROL3LOFj1ZmStnInEBS8hAXVVYQnAUdcCQ8+O3bB4YzJgyeZweiIiIqJ2hIEXERHtl2zVHpRWRQ67pELp7fmb8db8zWGVWo+dOgTdM6Ob4yTD6WVIfdySFkXZiVF2ZNw5i6vMbVOfmxpI//7mmfg6/5dGA+mPyjoIF3SfjNzETJgTtMiY/19oVs0IHaBPAsY9CHQ7GjGTlB74ICIiIqJ2hYEXERHtd8qq3KiobhzGNBV2/feH9fjsr+3BtVyrCY+dNgQdUxMjnp+QkKDCLosxjv+59fuByh2ApyaqgfSfbJmNmgYD6ftbe+DiHlPQL6UHTFoj0rxeGL+5FdixOHRQcgfghCeBzN6xeFWBai6p6pLqLiIiIiJqd+L4O3AiIqLGSh0u2Go8Ed8an78WT85Zo+Z21emSnqQquyTEqs9fW4t1hVWwOd1IMRnQK8cMrUaDHKsRSYY4/qfW7wPs+fB5qnc7kH5e0UK8vXEaihsMpM81ZeLCHifiyMwDoNfqkW6wIqliGzDjmkDFWJ0OBwLHPwYkRjcrbZ9ptIHh9HpTbJ6PiIiIiJpdHH8XTkREFE6G08uQ+kjcXj8enLkS89aWBNd6Z1vwyCmDGw2d/3tLOd7/fSu2llbB46+FXpOALhlm/GtUz6hbHtslnxe1tm2wuyp2OZB+WcU6vLbhc6yr3LLLgfRS0WU1JMOqMyNh4w/AnDtUtVhQ/xOBkbEcTm8IVJPJjoxERERE1G7xuzkiIop7UnUkYZfD1Xh4ekM1bh/unLYcCzeHqpEGd0zBAycNatSaKGGXVIFVu32wmvSwahPg8dViY0kV7pm+AkadFkf2ykTc8bpRXbYOZc7yJgfS51cX4Y0NX+C30iWNBtKf0OEYFXZZDRYk68xIMVighQZY+Abw2wuhgxM0wHAZTn9W7IbFG8yAJQfQaGLzfERERETUYhh4ERFR3IddhXYXqt2Rwy57jQe3Tl2KFTsqg2uHdU/H3ZMGwKTXNmpjlMouCbsyLQYkyP8lJMCoT1A7OBbYXXjxx/UY1iMDGk2MApsYcDltKC9dBacnfA6XsHsc+HDz15iZP69RxdfwzANxQY/JyEvMQpIuEWkGK/QaHeB1At/fD6yZFR48jXsY6HokYiYxFTDHYThJREREtJ9i4EVERHHL769Fgd0Jp8cX1WyvGz9bqqqz6ozqm4Wbj+8HvbZxxY/M7JI2Rqnsqgu79NrAZ5GapMf6IgeW59sxuFMK2jup5Cqv3I6qis2BXRnr8fg9+Gr7PHy05etGA+v7JnfDxT1PwoCUnjBqDSrokjZGxVEEzPwPULQ8dEJKJ+CEp4H07jF5Xap6TIIuU/v/GhERERFRCAMvIiKKS76dYZcrirBrh60G//lkCXbYQlVLk4bm4epje0O7i+osGVAvM7ukjbFh2CWMWg1s/lqURbEbZFsmQ+dtLhvsjh2orSoJC7ukeu7XkkV4c8OXKHCG5p2JbGO6qug6OutgNZBegi6zrt7OloXLgBnXA9X1zut0aKCyS6qtYkG+XjKc3pAUm+cjIiIiophh4EVERHHH6/Or8MrjazxIvSGp6Lrx0yUorQoFU2cf1hmXHNU9LMBqSHZjlAH1Xn8tTPrwsEu4fH51f3qDIfftSaW7EhXOCvic5UB1+A6Lq+2b8Nr6z7HSviFsPUlrwuldxmFSp5GqkitFb4FVbwl/f1bPBL6/D/DVCwMHnwYcdX3shtPLUHoZTi9D6omIiIgo7jDwIiKiuCI7LBbYnPD6I4ddK3fYccvnS2F3huZ7XX5MD5x5aOeI5/bKMaNbphnri6saDbOXyqeKag/65yVjYAcr2ptqTzXKXeXw+DxATQXgtAXvK3KW4a2NX2Je0cKwczTQ4PgOR+GsrscjxZAMi96MNEMytAn1Zp/5fYHB9H+9Ve9ELXDMjcCgUxEzelOgskuem4iIiIjiEgMvIiKKGzKrq9DuVO2Mkfy1uRy3f7kMTk8gGJPOxWuP64MThuRF9VwWox7/Ht0bt32xTA2ol5ld0sYolV0SdlmMWvzfiJ7tamC9BFxlzjLU1M3hqi4FXI7AH701+HTLHHyx7Xt4asM3ADgsYxAu7DEFnZNykagzIV0NpG9QqeV2AN/cBmz+ObQmc7PGPwp0OgQxY7QEdmKM1c6PRERERNQqEmrl19AUkd1uR0pKCmw2G6zW9vfbeiKieCe7MBbZXWr3xEjmrS3GAzNWwuMLHKvTJOC2E/pjRJ+sqJ5LKrqyko2qTe/XdSVqN0YZUC8zvaSNsWe2RYVdR/ZqH7v++fw+VdHlkFBKqQUcJYCnGr5aH2bvmI/3N81AhSe0e6XoYemEi3uchKFpfWHQ6JFmTEFi3UD6+iq2AjOuBco3htbSewITnwKsHREzSemBDyIiIiKKewy8osTAi4io7XK4vCiudKlWwkhmLNmBp75dg7oiMJNOg3tOHIhDu0UXhFgT9ci0GBvtBim7McqAepnZJW2M7aGyS94vu9uuhtLLcPrAoh+oKgI8LvxVtgKvrZ+KLdU7ws6TCq7zuk/CqJzDYdDokGqwIllvbvpJti4Avr4ZcNlDa91HAGPuAwy7OKdFdmLMAkz8hRURERHR/oItjURE1K7ZajwodbiiOvbD37fg5Z9CVUbJJh0ePGkQBnZIier8tCQD0syNh5xLuDW4U3SP0VZUeapQ7iyH1+8Nn7FVVYQtts0q6PqrfEXYOVLFdUrn43BS5+OQpDOpYfQylF6ToGn8BBI+Lv4A+OWpQIhW55BLgMP/ATR1TkvQaALD6WVuFxERERHtNxh4ERFRu1Ve5UZ5db2d/nZTyfTyvA346M9twbUMswGPnjoE3TOjqzLKsBiRkhijHQRbkMvnQllNmfocxudBRfk6vL9hOr7J/wV+hEKqBCRgVM5hOL/7JGQYU2HWJaqqLr1mF99GeF3ADw8Cq74KremMwOi7gd5jETOyA6MMp4/Vzo9ERERE1GYw8CIionapxOGCvcYT8TgZYP/UnDWYuawguNYh1YTHTh2CvJTEiOfLnC6Z19VwJ8b2xuP3oMJZoSq7GnK7KjF99cf4ePPXqPY5w+4blNILl/Q8Bb2SO8OoNSDdkKI+75KjGJh1PVC4PLQmQ+InPAFk90fMGJIAS26gwouIiIiI9jvt+7t3IiLa70i1lszrkrldkbi9fjwwcyV+WlsSXOuZZcYjpwxBehOtiU2FXTlWI5IM7fefS5nNJTO6ZFZXwxlncvuXrXPx5oq3UegsDbsvLzELF/WYgmEZQ6DT6lTQJZVdu1WwFJj5H6A69H4j7wDg+MdiOyw+MRUwt48NA4iIiIioZbTf7+CJiGi/I8PhCyudqHH7otq18Y4vl+PvLRXBtcEdrXhgymBYTJH/+dNqJOwywaTXor2qdFeqqi7ZabGhNeVr8OqSl7GyfHXYuoRaZ3WdgAkdjlaVXDKjS2Z1Sfi3WyunA3MfAPz1qu4GngIcc0PsWgrVcPpMwNS+5qkRERERUfNj4EVERO2CtCYW2J1weSKHXRXVbtzy+TKsLqwMrg3rkY47Jw6IKsDSaTTISTHCqGufYVeNt0YNpHf7Gs83K64uxtsr3sYP234IW9cmaDChwzE4s+t4FXBZ9Gak6pOh00R4D2To/S/PAIvfD63JOcfcCAw6FTGjhtPnAfrIbapEREREFP8YeBERUZvn8flRYHOqz5EU2p248dMl2FpeE1wb3S8bN43vC5028jwnvVaD3BST+tzeeHwelDnLVODVkKx9tvYzTF03tVEQdnjGYFzYYwo6JeXApDWq9kVDNFVZNRXA7FuBrQtCa4lpwPhHgY4HIWY4nJ6IiApeYtsAAKpPSURBVIiIGmDgRUREbZrL60OhzQWvP3LYtam0SoVdJY5QoHPSgR3xr1E9oYnUkidzznUaNche2hnbE5/fhwpXBRweR6M5XdLOOHfLXLyz8h0VhtXXzdwRl/Y8GUPT+kKnkTldViRFmtNVp2QtMPN6wL49tJbZNzCc3pqHmDFaAHM2h9MTERERURgGXkRE1GY5PT5VsSXtjJGsyLfj1qlLYXeGhtlfNLwbzj28S+T5U1KYZNAiJ9kETTsKuyTckmH0MpRehtM3tLRkKV5d+io22DaErUur4nndJ2F07jDoNTqkGJJh1Zmjep+UdXOAb+8GvPV2dOw9Fjj2zti2FMog/FgOwyciIiKidoOBFxERtUkydL7Q7mpUsdSUPzaV4a4vl8PpDYQ+Etv8+7jemDy0Q1TPZTbqkJ1sjD7waQOqPdWqYssrM7Qa2FG1A28sewPzd8wPW9cn6DCl87E4tfNYJOlMSJY5XYZkaBOinFXm9wELXgQWvlFvMQE48irgwPMDQ+NjQZ4nORcwmGPzfERERETU7jDwIiKiNqfS6VFtidGEXd+tLMLDX68KVoHptQm4dUJ/jOiTFdVzJZv0yEo2or2Q+VsSdDnrV1ftVOWpwkerP8L09dPhrQ0Pwo7JPhgXdD8R2aZ0Nacrw5gCvWYPdk90VQKzbwM2/xJaMyYDYx8Euh6JmJHZYtYOsdv5kYiIiIjaJQZeRETUpsgOi2VVjXcXbMrUv7fj+e/XoS4WS9Rrcd+JA3FQ17Sozk9NMiDdbEB7mdNV7iqHw+1o8r5vNn+D91a+p1oc6+ub3A2X9joF/azdVcCVpuZ0mfbsycs2AjOuA2xbQmvpPYAJTwKpnREzUtFlyeG8LiIiIiKKiIEXERG1GaUOF2w1nojHSeXXW79uxtu/bQ6upSTq8fDJg9E3Nzmq58qwGNU57X1O119Ff+G1pa9hS2W9MErmx5sycEG3iRiRfQh0Gi2sezqnq86GH4A5dwKeqtBaj5HAcffGtqWQ87qIiIiIaA8w8CIiojYR6hRXuuBwNZ5H1ZC0Lj773VpMX7IjuCbztx49dQi6pCdFPF8CH2lhtBjb/j+B0qJY7ixvck7X1sqteH3Z6/iz8M+wdaPWiFO7Ho8pHY5Bos4Iiy5pz+Z01ZFw7feXgT9eCV8//B/AIZcACRrEhEYTqOrivC4iIiIi2gNt/7t9IiKKa35/LQornahx+yIe6/b68cDMlfhpbUlwrWtGEh49ZUhUc7g0CQnIsZrUjoztdU5XpbsSH676EDM2zoCvNvw9G915FM7rMh4ZOjMSdSakG6x7Nqer/ryuOXcAm34KrenNwNj7gO4jEDM6A5Ccx3ldRERERLTHGHgREVGrkWqtArsTLk/ksEuqv+74YhkWb7MF1wZ2sOKBKYNgjaI1UafRICfFCKOu7YZdUslV4apock6X3Pf1pq/x/sr3UempDLtvQMYAXNbvPPTSW9VOjHs1p6tO2QZgxvXh87pSuwInPAGkdUfMGC2Byq52tHMmEREREbUdDLyIiKhVeHx+FNic6nM0s71u/nwp1heH5kgN65GOOycOgEkfOcDSazXITTGpz+1xTtfCwoV4bdlrqo2xvuykbFw88GIcmTYAOrcDKQYrknVJez6nq87674Fv7wI81aE1qeg67p7AjoyxYs4AEqPbeICIiIiIqCkMvIiIKOZcXh8KbS54/ZHDru3lNbjxsyXYYQu1940bmIPrx/SBLooAy6jXItdqglbTNiuFqj3Vqn1xV3O6JOiSwKu+RF0iTutzGk7sMQlGVyUsfh9Sk7L3fE5XHb8P+P0l4M/XwtcPuwI49NIYz+vKBQyRZ7EREREREe0OAy8iIoopmdVVaHfCX1sb8dg1hZW45fOlKK8O7dx45qGdcdnR3aOqYkoy6NRAe00bDLsizel6f9X7mLlxZljFVwIScFzX43Be//OQprcg0WlHus6yd3O66jjtwJzbgc2/hNZkQPyY+4HuxyBmdMad87r4rQkRERER7Tt+V0lERDEjc7hkN0Zp4Yvkry3luPPL5aiuN8z+HyN64PRDOkf1XBaTDlkW496397UQn9+Hclf5Lud0zdo4S4VdDk/4/YMyBuGywZehR2oPGHwepHk9SDSk7NvFlKwFZv0HsG0LraV1AybIvK5uiBmTFTBncV4XERERETUbBl5ERBQTthqPmsUVjbmrivDQrFXw+gPBmLQj3jCuL8YOyInq/JREPTIskXdtbA9zunKTcnHRoItwRN4R0Gl1SPXXItmfAGgM+3ZBa74Bvr8XqF9h1mNkYF6XwYKYkDBSgi4JvIiIiIiImhEDLyIianFlVW5UVLujOvbzv7bhhbnrUVcDZtJpcNfkATi8e0ZU56ebDUhN2scwqI3M6Tqj7xmY3GMyDDoDrDoLUjw10HiiCw13yecBfn0OWPxevcUE4PB/AIdcHLt5XVp9oIVR17a+VkREREQUHxh4ERFRi1Y1FTtccDi9UR376s8b8cHvoQonq0mHB08ajAEdIlcASetipsWAZNM+zLNqZh6fB6XO0l3O6fpg1QeYsXFGozldY7qOwbn9z0WaKQ0WgwWp2iToqooDYdW+qC4Fvr4FyK8XrhmtwNj7ga7DETOy46NUdsmQeiIiIiKiFsDAi4iIWoQEWIV2F6rdkcMur8+PJ+aswTfLC4NrMmz+0VOGoEtGUlRhV47VqIbUt5U5XRWuChVqNXXf15u+xnsr30Olp3KXc7pMOpMKvIw+L1C5Q97QfbuogqXArBuBqqLQWmYf4PjHgJROiF0LYyZg2sfZY0REREREEbSNnwyIiCiu+Py1KLA74fKEBs7vSo3Hh3unr8CCjWXBtR6ZZjx8ymBkRjGHS+Z75VhNMOm1aAshn4RYNqcNvtrGr/3vor/x6tJXsaVyS9h6TlIOLh50sZrTJe2LacY0JOmTgOqywMe+WvY5MO9RwF+vQqzPeGDU7YA+ETEhuy+qFsa2NVuNiIiIiOITAy8iImpWHp8fBTan+hyJrdqDW79YipU7QpVOQzql4P4TB6ldFiPRaTTITTHBoGvd1ji/z4tFqz7F1oqNSErKQM9uo6HRhAK4fEe+mtP1e8HvjeZ0nd7ndEzuORmJ+kSkGlORbEgOVHNVFgKuxhVie8TrAuY9Aqz4MrQm1zX8WmDImbHbFdFgBiw5bGEkIiIiophh4EVERM3G5fWh0OaC1x857JIKsJs+XYKt5TXBtaN7Z+K2Cf2jCrD0Wg3yUkzQaVs37Ppl4Yt4Zdnr2OyrgdR0SczVacnLOLXPaejVeyI+Wv0Rpq+fDm+tN2xO13FdjsN5A85DRmIGrAYrrEYrNDIwvq6FUcKqfWHfAcy6ASheGVpLygDGPQx0PAgxIYFaUjqQmBab5yMiIiIi2imhVvovKCK73Y6UlBTYbDZYrdw+nYiooRq3D4V2J/xR/LOyvtiBmz9filJHaOfGyUM74Kpje6kWxUikfVHaGKM5tqXIoPnv/3gOjy57FTWohVF2OpTLqQWc8v8SEuDVG1HlCw+uBmQMUHO6eqX2CgykN6ZCp9n5+yePMxB2+SO3gu7WlvnAN7cBLltoLWcwcPyjgCUbMWthtOQCelNsno+IiIiIqB5WeBER0T5zuLwornSpGVaR/L2lHHd+uRxV7lCoc9Hwbjj38C5q+HwkZqNODbSP5tiW4nA7UFZdgrdWvgN7Qi18CQmoaz6sTZDMK0F9Rr2wKysxCxcNvAhHdTxKtS+qgfTaevOspH3RUbRvw+llt8eFbwC/vRhI3uoMPg046jpAa0DsWhizA+2TREREREStgIEXERHtE5nDVVoVXfvd3FVFePjrVfD4AmGMFGhde1wfnDAkL6rzrYn6qAbZtxSXz4WymjL1ee3G2djod8KZkKCiJWmslEZOf4MczpCgw2l9z8BJvU9Sg+jTTekw683hB1WVAjXl+3hxlcCcO4FN80JrEqiNuhXoNxExwRZGIiIiImojGHgREdFeK3W4YKupt/Pfbny6cBv++8P64G2jToM7JvbHkT0zozo/3WxAalKMKpQa8Pq9qHBVqMquOjZHAaoSoOZ2aVALNaGrQdVZQm0t/tVlHI7rfzZSjClqVldYZZoaTl8AuKv27QJL1gbmddm2htasHYHjHwOy+iImpJpLdmFkCyMRERERtQEMvIiIaI9J66K0MEorYyQy0+vleRvw8Z/bgmtWkw4PnjQYAzpEnokoAVGmxYBkk75VXqfdbYfNZVMzu+qzaxICIRdq4W8i6JKP2oQEmBLT0cHSITSnq05zDadfPQuYe1/443Q9ChhzH2CK0cxJQ9LOXRjZwkhEREREbQMDLyIi2iN+fy0KK51qSH0kHp8fj32zGt+uLAqu5ViNeOSUIeiSnhTxfE1CghpOn2iIfZBS7alGmbNMVXc1tKZ8DT4p+LVRRVfd/C31/xMS1I6NnTsc1jjsknDKnr9vw+l9HuDnp4ClH9VbTAAOvwI45BJAdnxsaWxhJCIiIqI2ioEXERFFzevzo8DuhNsbXu3UlCqXF3dPW46FWyqCa72yLHjo5EHIiGIOl06jQU6KEUZdbMMuj8+DUmcpnF5no/vKneV4a8Vb+G7Ld+F3NDFoXlYS/bVIry4Lv8NdHajs2pfh9NIG+fVNQOGy0JrRCox9AOh6JGKCuzASERERURvGwIuIiKIiIVeBzQmv3x/VbK9bpi7DuqLQzKuDuqTinskD1S6Lkei1GuSmmNTnWJGWRZnTVemubLTbpIRg0zZMw0erP0KNtyb8xF3mVglqgH1fTb1KNqcNqCrZt7Br6wLgm1sBZyhIVHO6ZF6XzO2KBe7CSERERERtHAMvIiKKyOnxodDuhM8fOajZUlqNmz5fgkJ7aKbU6H7ZuHF836gCLJNeq9oYtbKFY4xIyFXhrICvNrzFUIKvBQUL8Pqy17GjakfYfbnGdBQ5S+GXNkL1Efj/9d8hVwKw0ufAYLkhQVdNvZBqT8kMsYVvAL+9GP4sA04CjrkB0Blj1MKYASSmtvxzERERERHtAwZeREQUsTWxqNLVqOqpKcu223D7F8tgd4bmXp12cCdcMaKHmscViVR/ZScbw3cybEHStihzutw+d6P7tti34JWlr2BR8aKw9WR9Ms4dcK6q+npt2avQynB6qeaqF0Npdu7cKMPsl/pdGGzfsW87MTrtwJw7gM0/h9a0RmDEzcCAyYgJtjASERERUTvCwIuIiHbJVuNR7YnRmLe2GA/MWAmPLxD7SGT1fyN74tSDO0V1vjVRj8woZns1BxlEL/O4qjyNQyiH24H3V72PGRtnhO3MqEnQYEL3CTi739lIT0zHnE1zkIAEFW5J6FV/p0ZNbS186nYC4KrYt7CraGVgXpd9e2hNWhelhVFaGWPBaAHM2YAmdi2mRERERET7goEXERE1qazKjYrqxpVPTZn693Y8//26YIWTXpuAW47vj5F9s6I6P91sQGqSocW/ElKlZnfbYXPZwsIsIe2MszfNxjsr31EtjvUdkHUALh18KXqk9ECKKQVWgxWH5B4CnUYPr98DbUICNPJ48gZIzqXRQJojZXfGIdYee3uxwIovgHmPAvUr0LodA4y5FzAmo8WxhZGIiIiI2ikGXkRE1CgUKna44KjXlrgr/tpavPrTRnz4x9bgmsWow/1TBmJIp8hznqR1MdNiQLJJ3+JfhWpPtWpflOquhpaWLMUrS17BRvvGsPXcpFxcMvgSDMsbBqvRilRjqqr0EgMyBqBXai+sKlsFiaN0WoOq9pIYzesPzALrZemMASk99/xiPTXAjw8Dq74KrcnzDvsncNAFgT+3NK0eSM6NzWwwIiIiIqJmxsCLiIjCwi4ZNl/t9ka1a+Nj36zGd6uKgmsyf+vhUwajW4Y54vky00uG0ycatC36FZBZWxJ0NdpdUboFq4vUQPpf8n8JWzdpTTi97+mY0nOKCrrSTenQSwAUdv0aXHfIdbj151tVe6Sv1o+d3ZzQJmiQZrDiuv4XBAOyqJVvBr6+EShdF1pLTAPGPgh0PgwxwRZGIiIiImrnEmqjmUJMsNvtSElJgc1mg9Vq5TtCRHFHdmAssDvh8oTvVNgUh8uLu6Ytx99bQrsO9sgy4+GTB0c1h0un0SAnxQijruXCLmlZrHBVqPbEhv/UybD6qeum4tO1nzYaWD+q8yhcMOAC5FpyVdCVqEvc7fMs2LEAry59FWvL18Ljc0Gv0aF3cldc2vNkHJ45ZM8uet0c4Lv7gPqzxfKGAuMeBizZiEkLozkTMKW0/HMREREREbUgBl5RYuBFRPHM4/OjwOZUnyMprnThls+XYkNJKJQ5qEsq7pk8UO2yGIlBp0Gu1QSdtuXa8mTwfLmrHL6drYV1JPiSai6p6iquKQ67r3dqb1w+5HIMzBio5nTJbozR7hbpd1Vi5bZfUeG2I9VgRX9r9z2r7PJ5gF+fARZ/EL5+wLnAEVcG2gtbGlsYiYiIiCiOsKWRiGg/5/L6UGhzweuPHHatL3aosKvEEaqKOq5/Nm4Y1xf6KAIsaV/MSTZBo4kuSNpTUq0l7YtSwdXQRttGvLL0FTWvqz6ZyyUVXaO7jlbD6OW2VrMHlWfVZdBUl2Hg3szqEpUFwNc3A4X1rstgBkbfDfQ8FjEhA/DNWdyFkYiIiIjiBgMvIqL9WI3bh0K7Uw2fj2Th5nLcPW05qtyhqqmzDuuMS46SaqbIAZbFpEOWxRh11dSekEouqeiSyq6GZFfGd1e+i282fgO/GikfoEvQYXLPyTij7xlIT0xHhimj0Zyu3ZKA0FEAuKv3/sI3/wrMuR1w2kJrmX2A8Y8CqZ3R4tjCSERERERxioEXEdF+SuZwSXtiNKMcZy8vwGOz16g5X0IKtK4e3RuTh3aI6rnSkgxIMxvQEiTQsjlt8NX6GoVgX2/6WoVdDk94EHZIziG4dPCl6GrtquZ0JemT9uxJvW6gckegFXFvSKvl7y8Bf74ujZah9QEnAsfcCOhMaHFsYSQiIiKiOMbAi4hoP2Sr9qC0yhXxOAnD3l2wBW/8sim4ZtJpcPvE/jiyZ2bE86WaK8NigNXU/DOopG1R2hcbDp0XS4uX4uWlL2OTPXTdooO5Ay4bfBkOyzsMKcYU1cK4xxVnLgfgKJQ3Z+8uvKoEmH0bsP3P0JrWCIy8Geg/GTHBFkYiIiIiinMMvIiI9jOlDhdsNZErk6Sa66lv12Dm0oLgWlqSHg+cNAj9ciPvVittjtlWI5IMzftPjdfvRbmzHFX1dzLcqai6CG8sfwM/b/85bF12Wjyz75mY1HMS0kxpSDOm7dmcrjpVpUBNeeP1Wj9QvApwVgCmVCCrH9DU0PptfwTCrurS0FpqV2D8I0Bmb7Q4tjASERER0X6CgRcR0X5CqrWkhVFaGaOZ7XXvVyuwYGNZcK1TWiIePnkwOqQmRjxfq0lAbooJRt1ehEq7uX7VvuiywS8BUz0unwtT107FJ2s/aVTxdWznY3HhwAuRZ8lT7YsG7V60Vu5uXte234E/3wTKNwG1XiBBB6R1Aw65EOh02M6L9wML3wAW/C/w5zq9xwGjbgsMqW9pbGEkIiIiov1IQm00w1sIdrsdKSkpsNlssFojVzYQEbUlfn8tCiudKsiKpgLs1qnLsLYoNPdqYAcr7p8yCCmJkVsTZbdGCbui2bUxWtWeajWU3tNgZpb8E/bbjt/w6rJXVXVXfb1Te+OKIVdgYOZAVdVl1u9lqLS7eV0Sds19IBCEmVIACdMkcJMh9IakQJiV0RuYcyew5dfQeRo9cPT1wKBTA1VXLY0tjERERES0n2GFFxFRnPP6/CiwO+H2hldFNWVjSRVu+XwpiipD872O6Z2JW47vB6M+crWWSa9FjtWkKryag8fvQVlNGWq8NY3u22LfgleWvoJFxYvC1lONqbhgwAU4rutxKujaqzlddVyVgKOo6XldUqkllV0Sdpmzgbqn0BkDt6uKgF+eA2pKAo9Rx9ox0MKY3R+xaWHMAkz8RQ0RERER7V8YeBERxTEJuQpsTnilJS+Cv7eU485py1HlClWBnXJQR/xjRM+oAiyzUYfsZOPeh0v1SMuitC5KC2PDQmSZ3fX+qvfx1YavwlobtQlaTOoxCWf2OxPZSdkq7NJp9vKfOXlOmbNVU7HrY2Rml7QxSmVXo5cs11wLFK8IX+4xEhh9d6DiqqVJ8GbJAXQtszsmEREREVFbxsCLiChOOT0+FNqdavh8JHNWFOKxb1bDu/NYyW/+NaonTj6oU1TPZU3UI9NiRHOodFeiwlkBX214+6WEW99t+Q5vr3gbFa7wIOrA7APV7os9U3uqOV0mnWnvL8DnDczr8jh3f5wMqJeZXQ1ngvl9QGU+4K43VF8G5B95DTD0rNi0MCamAkkZsXkuIiIiIqI2iIEXEVEcksH0MqA+0phGuf/d37bgjV83BdeMOg1um9AfR/XOjOq50s0GpCbtexWR0+tEmbOs0dB5saZ8Df63+H9YW7E2bD0nKQeXDLoEwzsMR1piGpIN+1g55akBKgsCoVUkshujDKiX65VqKnV+NWDPB/z1NgaQ4On4x4G8IWhxEqxJVZfMDyMiIiIi2o8x8CIiijO2ag9Kq0IzuHY32+upb9di1rKC4Fpqoh4PnDQI/fMiz3yS1sVMiwHJpsiD7CPN6ZKKLmlVbKjcWa4qur7d8m3Yuuy0eHqf03Fy75NVRZe0L2oS9nFIfk05UF3W9LyupmT1C+zGWLoWSMoCnGVAVXHjUOysj4DENLQ4Cbkk7JLQi4iIiIhoP8fAi4gojsgOi7aaJnYTbKDK5cU901fgz83lwbVOaYl46OTB6JiaGPF8memVnWxComHvwxWpLpM5XTa3rVElmtfvVTO6Plj1Aaq91WH3Hd3xaFw08CJ0tnZGhikDeu2+BW6Q+WYyYN4V2pUyKhKwHXIh8N29QPkGwN/gfZfZXuMebPmwS9oWk9JjE6oREREREbUTDLyIiOKABEbSwiitjJEU2Z24deoybCgJVVQN6mDFfVMGISUxcnik12rUTowG3d5XVEk1l7Qv+ppoHVxUtAgvL30ZWyu3hq13s3bD5UMuV/O6pKorSd8MbXteN1C5A/BFDgmbJPO7vM7wsEuCsMx+wPCrgE6H7fs17vb5dYAlF9Dvw8wyIiIiIqI4xMCLiKidk6H0MpxehtRHsrawErd+sQyljtCcrJF9snDz8f2iCrCMei1yraaodm1sisfnQamzVM3raqiougivLn0V83fMD1s36804t/+5mNB9ggq6UowpzbITJJz2QAtitC2M9cnukH+9Bfz2IlB/uH7OIGDYv4BOhwSCr5ZkMO9sYWzh5yEiIiIiaocYeBERtWMenx8FNqf6HMlvG0px71cr4PSEjj3z0M649Oju0EQRIFmMOmQlG/cqbJIdFmVnRdmBsWH7osvnwudrP8enaz6F2x8K4hKQgLHdxuK8/uehg6WDmtOl0zTDP1uqhbEYcFXu3fky52vOHcDW30JrMjfriKuAA85t+Z0R2cJIRERERBQRAy8ionbK5fWh0OaCVwKcCL5ctB3Pfb8O/p1ZkxRoXXNcb0wc0iGq55JdGGU3xr3hcDvU8Hlf/UqonW2Yv+34Da8ue1VVd9XXL70frhhyBQZkDFBBV6Iu8lyxqHhdgV0Y97aFcdsfwOzbgOrS0FpyLjD2odjtwpicxxZGIiIiIqIIGHgREbVD1W4viuwu+CO048n9L/24AZ8s3BZcS9RrcdekATise3rE55FqrgyLAda92InR7XOjtKZUVXA1JPO5Xln6Cv4u+jtsPdWYqgbSj+4yGmmJabAaIu8WGTWnDagq2bsWRpk19scrwB+vSlQXWu8xEjj2LsDUjNe5K9yFkYiIiIgoagy8iIjaGdmFUXZjjERmej00axV+WlsSXMu0GPDQSYPRM9sS8Xxpc5Th9Hu6E6O0L0pFl7QvNlTtqcaHqz/EtPXTwiq+tAlaTOo5CWf1PQvZ5mykGdOglWqm5rC3uzDWcRQBs28H8heG1jR6YPg1wJAzWr6FUcgujPJBRERERERRYeBFRNSOSNAlgVckZVVu3P7FMqwqCIVOvbIseOCkQWoOVyQ6jQY5KUYYddpma1/8YdsPeHP5m2p3xvoOyDpA7b7YO623GkpvkJ0Pm4vHCTikhTHy7pVN2vwLMOdOwFkRWkvpDIx7CMjuj9i0MMoujM3U0klEREREtJ9g4EVE1A5IYFRU6UKVK3Jws7m0CrdOXYYdttBOiId3T8cdE/sjyaCLaifGnGQjdFrNHrUvSpDV1O6LGyo24KWlL2FF6Yqw9ezEbFw6+FIM7zgcGYkZajfGZiXD5WvK966FUWZ8/fYC8Pc74eu9xwGjbgUMkSvk9hlbGImIiIiI9hoDLyKiNs7nr0WB3QmXJ7xqqil/bS7HXdOXo8oVOnby0A646the0Mqk+gjMRh2y92Anxt21L8rauyvfxdcbv4YfocH6eo0ep/Y+Faf0OQXZSdlIMaZAkxB9uBaRVHM5CgFPzd6db9sGfHMrULQ8tKY1AsfcAAyYEptdGBPT2MJIRERERLQPGHgREbVhbq8fhXYnPL7IOzHOWroDT367VgVkQmKZK0b0wGkHd4oqwEpJ1CPDErndMax90VUOnwx0r0faGb/d/C3eWvFWoyBsWN4wXDLoEvRI7aF2X5Twq1m5qwJhVxQ7VzZp7Wxg7v2Bx6mT3iPQwpjRCy2OLYxERERERM2CgRcRURslQ+cl7KoLsHa3E+PrP2/E+79vDa4ZdRrcOqE/ju6dGdVzSdAlgVc0PD4PSp2lTbYvri5bjf8t+R/WVawLW+9o6YjLB1+OYR2GqaArUdfMM6mkbbG6FKipN2trT0g12E+PAyu+CF8fcBJw9PWxmaHFFkYiIiIiombDwIuIqA2qdHpQ4nCr2V27I22OD3+9Gj+uKQ6upZsNuH/KQPTLtUa1E2O21RjVbC+5lgpXBexue6PrkvW3l7+NOVvmhK2btCac2e9MnNTrJGQmZcJqiHxNe8zrDgyml897o2Qt8M0tQPnG0JrBDIy6Heg9Fi1Oqu9kB0ZpYyQiIiIiombBwIuIqI0pr3KjvDpyeCPH3PHFMqzYEWob7J5pxoMnDUKO1dSsOzFWe6rVUHqvP3xovrQzzto0S83qqvLUawMEcEynY3DxoIvRzdoNqcZUaKVdr7k57UBV8d4Nppdzln8G/PQk4HOF1nMGBloYrR3R4rQ6wCK7MEb+ehERERERUfQYeBERtRFSNVXscMHh3LudGA/pmoa7Jg1Qg+cjMeg0yLWaIu7EKAGXBF0SeDW0vGS5al/cZN8Utt7V2hVXDLkCh+YeigxTBvTaZp7TVRdWOYoAV+Nh+VFx2oDv7wc2fB++ftAFwOH/B7TENTdkTAbMWYCmGQf2ExERERGRwsCLiKgNkDldMq9L5nZFsnBzOe5usBPjpCF5uHp076h3YsyyGKHZzbESvknros1lUzsx1icB2BvL3sAP234IW0/SJeGc/udgUs9JavdFs96MFiGti5U7AJ9n787f/hcw5/bAcPs6ienAcfcAXY9ETFoYLdmBwIuIiIiIiFoEAy8iona0E+NXS3bg6W/XoG6OfUvsxLir9kW5/dWGr/D+qvdR460Ju290l9G4cOCFqrorxZgCTUILVS3tSwujvJ4/XgP+fBWoH+J1Phw47l7AHN2A/30irYvSwiitjERERERE1GL4HTcRUTvYiVHuf+WnDfj4z20tthOj7L4oQVfDMEssKV6i2he3VoZ2ghQ9UnrgH0P+gYNyDkK6Kb1l2hebo4VRKsJm3w7sWBRak5liw64EDjwXaKmAro6EkTKUXobTExERERFRi2PgRUTUxndirPH48OCMlfhlfWlwLcNswAMnDUKfnOR93olRWhZll8VKd2WjaympKcHry17HT9t/Clu36C04b8B5OKHHCchKzEKSPgktZl9bGNd9B8y9LzwsS+kEjH0wMKC+pXEwPRERERFRzDHwIiJqwzsxFle6cNsXy7CuyBFc65klOzEORlby7lsTo9mJ0eF2oNxVrnZbrM/j92Da+mn4cNWHcPpCg/ETkICxXcfi/IHno0tyF9W+GE0rZau0MHpqgJ+fBJZ/Hr7e9wRgxE2AoYVmjNVnsgJJmRxMT0REREQUYwy8iIja6E6MawsrcesXy1DqCAVjw3qk444TBiDR0HSAVZ9Rr0VOsrHJnRjdPrdqX3R6Q2FWnUVFi/DSkpewzRFqnxS9U3vjH0P/gQOyD0CaKQ16TQvuZOj3B4KuvW1hLF4NzL4NKN8YWpMqtBE3A/1OQIuTdkkZTB+LUI2IiIiIiBph4EVE1AZ3YvxlXQkemLESTm9ouPqpB3fEFcf0jGonRovsxJhsbFR9Je2L5c5y1b7YUHF1MV5b9hp+yf8lbD3ZkIwLBlyACT0mIMOU0bLti8LjBBwFgC9yKNiIDKNf/AHw63OAv14LZPaAQAtjame0OKMFMGcFQi8iIiIiImoVDLyIiGJAdmAssEXeiVEqwGQw/cvzNqCuiU/yratH98bkoR2ieq7UJAPSzYY9al/8Yt0X+Gj1R3D5XGHti+O7jVfti52TO8NqsLZs+6KoKQeqy/auhbGqBPjubmDL/PD1A88Hhv0TaKmB+nU0mkDQZYw8V42IiIiIiFoWAy8iojayE6OEYc98uxYzlxUE18wGLe6cNACHdou8u5+EUVLVJdVd4Y/rQamztMn2xb+K/sLLS17Gdsf2sPW+aX1V++LQrKGqfVGnaeF/LiSEcxQC7uq9O3/Tz4GwSwKzOjI7a8y9QOfD0eJ0JsCxAyjbACRlALlDObeLiIiIiKgVMfAiImpBDpdXDZ6PtBOjrcaDu6ctx+JttuBartWkdmLsnmmOaji97MRo0muj2n2xqLpItS/+mv9r2LpUcV048EKM7z4emYmZSNQlosVJyCVhV4PKs6h4XcCvzwBLPgpf7z4COPYOIDENLUoq3opXAb+/ApSsDbRRymyzzN7AUdcCPUa07PMTEREREVGTEmoj/RRGit1uR0pKCmw2G6xWK98VIoqootqNsqrIOzFuKavGbVOXYXtFTXBtUAcr7j1xoGpPjMSg06hwrP5w+mpPtarqirZ9UQMNju9+PM4bcB46JXeKTfui/PMj7Yv1q7L2ROl6YPatQOm60JrWCBx9HTDwlEAY1ZKkRVJCrlk3AC5HIFzTGQMhnLwmmeU18WmGXkRERERErYAVXkREzUx+j1DicKPSWW9o+i78tbkcd09foSrB6owZkIPrx/RRQVYkZhlObzFCs3OQvQRaZTVlqPGGwrM6fxf9rXZfbNi+2C+9H/4x5B8YkjUkNu2LwucBKgsC4dDeBGVS0SWVXb56gWJGb2Dcg0B6D7Q4mdMlrYvT/x0Iu5LzQgGbPjHQ4li5A/j5KaDb0WxvJCIiIiKKMQZeRETNyC87MVY6UeOO3J43fXE+nvluLeqP9rrkqG44+7AuUVVX1R9OLyGbzWWDzW1r1L64q90XUwwpqn1xXPdxsWtfFE47UFW8D4Pp7wG2hLdiYujZwBFXBiqsWpJ8XcyZgCkFyP87UOEllV0Nv15yW9bl/oLFQIcDW/a6iIiIiIgoDAMvIqIY78Qow+v/9+N6fPZXqNLKqNPg5uP7YUSfrIjPI2FYpsWAZJM+2L5Y5iyD1+8Nvx6/B1+u+xIfrv6wUfvihO4TcO6Ac2PXvij8/kDQ5arcu/M3zguEXc6K0JpUWY2+C+g6HC1OWhilkku3s820ujQws2tXIZusy7XKcUREREREFFMMvIiIYrgTY5XLi/tnrMSCjWXBtQyzAfdPGYS+ucl7NJxeAi4JuiTwamhx8WK8uPjFttG+KDxOwFEA+Lx7cW4N8MtTwLLPwte7HRMYTJ8UeQfLZmlhtGSHV3JJ2CYD6qUtU9oYG5J1uV+OIyIiIiKimGLgRUS0jyTEKopiJ8b8ihrc9sUybC4NBVS9si14YMogZCVHbsUz6rXISTZCq0kItC+6bGonxvpKa0rx6rJX8fP2n8PWpYrrooEXxb59UdQNpt+bFsailcDs24CKzeGVU0fFaDC9amHMAkxNbFaSOzSwG2Ph8sDMrvrXIq9VXnPOwMBxREREREQUUwy8iIhisBPjkm0VuGvaCthqQoPsj+qViVsm9EOiXhvxfIsMp082qtbEoqoyuOsPa5diIr8X09ZPU+2L9QfWS/vi+O7jcf6A82PbviikmstRGKjQ2lOyu+Sid4Hf/gvUb9XM6guMfQBI644WpzcBlpxAK2NTNBrgqGuBr64JDKhvtEtjcuB+OY6IiIiIiGIqoTZSSQIpdrsdKSkpsNlssFqb+E0/Ee1X9mQnxllLd+Cpb9fCW6/d8ZzDu+Ci4d3+v737gG+zvNoGfmlL3jN77x2SEMgAQgiEvUspZc/SUigFWlrgLS1f3y46GKWhjJe99yqUEQgzQBISyN7bieMleWhL3+/cj9Yjy46dWLYsX3+qSnqGLNvxunTOuWFsQ/gkg+nz7SbUemvR4Gtotv+7fd9hwbcLsKN+h2776OLRuHry1ZhcPrlz2xeFrFzYWKnN7WovVwXw/m+A3csSNhqAqRcBh/+45QCqo0QHzre1VXLzIm01RhlQLzO9pI1RKr8k7Bo2J73PlYiIiIiIUmLg1UYMvIiovSsxyjyvBz/ZjOeX7Ixts5gMuGn+aBw3rvd+P6AShklVV8jgRp2nDsGw/u3J/K5HVj6Cj3Z+pNueb83HJeMuwUnDTlLti3Zpt+ss8hqKDKaXlRgP5Nz1bwOL/gT4GuPbc3sBx90BDJiOzhlM36f9qz1KsCerMcqAepnZJW2MrOwiIiIiIuoybGkkIkrDSowy1+t//7MGizfHh9MX51hwx+njMb5f4X7fjsVkRFGuAS7/Pt0KiyIYCuKtLW/hqTVPoSkQnwdmgAHzh8xXYdfAgoGd274opJWvXgbT77/qrRkJyBb9Edjwrn77iOOAo38N2Pf/MTtojiItrDqQj5mEW/2mpONZERERERHRAWDgRUTUwSsxSiAmw+m3VMWrlIaV56qVGPsU7L/aymYxwGp1o9pT32zfmuo1avXFLa4tuu0jikbgx5N/jKm9p6LIVtS57YtCZlbJcPoD6ZLf+RXw/m+1eV9R1lxgzq+AUSemfzC90aTN6rLmpPftEBERERFRp2HgRUTUBi6PH9UNvv2uxLhiZx1+mzScfvbwUtxy0lg4rPsfTm82+xAyNqLRr29flBUZH131KN7f/r5ue54lTw2kP2X4KSh3lHdu++LBDqaXirDF9wHLn9Jv7zcVOPYOoKAv0s6Wp63CKKEXERERERFlDQZeRET7Ud3g1QVYLfnPdxW4K2k4/XmHDcTlRwzd73B6mc9lNDcgZPQDYf32d7e+i8dXP44Gv35g/XGDjsOlEy7FoIJBnd++eLCD6WXA+3u3AdUb49ukKm3GT4BDLkh/ACUtiBJ0yUqKRERERESUdRh4ERG1Mpx+X4NXzeNqjbQ4Lli0CS8v26UbTn/jcaMwf3yfVs+VijF3sEG1MJpMRt2+9bXrVfvixrqEUAjA0IKhqn1xet/pXdO+KAGXDKb31h/AuUFg+ZPA4n8BoYSPa/FQYP7vgfIxSDtpXZRB+Cb+CCQiIiIiylb8bZ+IKIWADKd3eeALtF691OAJ4I43V2PJttp2D6f3Bj1oCjlR5DDAlLCiX72vHk+sfgLvbH0H4YRyL4fZgQvGXoAzR5yJ8pwuaF8UviathVGCq/Zy7QLevx3Y/Y1++6RzgVnXAel+f6QCLrescwbgExERERFRl2LgRUSUYjh9pcuLwH5a9XbUNOG2V1diR218ftWIXnn4/enj0auV4fTBUABOXx0MJi9Kcq1qdUURCoewcPtCPLLqEbh8Lt05cwbMwRUTr8DQwqFd074os8uaqgF33YGdu+YN4JM7AX98VUnVUjjvt8CgGUg7i10bTG+ypP9tERERERFRl2PgRUSUQNoXK+u9+x1Ov2RrDe54cw0aEtodjxpVhptPGAOHJfX8KXnMxkA96n1OFDgsyLfbYvu2OrdiwbcLsLp6te6cgfkDcfWkqzGr/6yuaV8Ufo9W1RXc/xyzZmTlxg9/D2xZpN8+8nhgzs3pr7aSYNBRDOSUpPftEBERERFRRmHgRUQUUdfkQ02jr9WPh4RWr3yzG//6aCMSZtPj4pmDceHMwS0Op5f2RaevFqFwAKV5NtgtWgtjk78JT699Gm9sfkNVeEXZTDacN+Y8fG/U99A7p3fXtC9K6Oeu1S77CQBT2vIxsPD/Ae6a+DYZEj/nV8CoE5B2Us2V3wcwx4NFIiIiIiLqGRh4EVGPJyGWDKeXeVyt8QdDuPuDDfjPd3ti22xmo6rqOnp0ecpzZJVFl68OTf5GWMxG9Mq3wWw0qrf56e5P8dB3D6HGkxAIAZjZdyaumnQVRhaP7Jr2RRHwaVVdAW/7z/U1AJ/+HVj9mn77gMOAY3+rtRamm1SOybyurvjYERERERFRl2PgRUQ9mqywuNflUXO7WlPb5MPtr63Cyt3x2VpleVb8/owJGNU7P+U5DX6tfVEqt3KsJhTnWlUF2K6GXbh/xf1Yvm+57vi+uX1V0CXzuortxV3TviikoktaEQ+kqmvXUm0wfX1FfJvJBsy6VhtOb9CvRNnhZPi/BGrW3PS+HSIiIiIiymgMvIiox5IVGCXsksqt1mysbFDD6WW2V9S4vvm44/QJauh8Mn/QhzpfDXxBrT2yMMeCArsF3qAXL6x7AS9tfAmBULyazGK04JxR5+AHY36APrl91GqMXUJmdDVUAv74EP42C3iAL/4FrHhaeiHj28vHAsfdAZQMQ9pZc7Swy5h6hhoREREREfUcDLyIqEdy+4KorPeoCq/WfLRuH/7yzlp4AvFQ7PjxvfHzY0fBatZXK0mbYr3fqSq75LZUc5XmWWG3mPD1nq/x72//jb1Ne3XnTO01FT+Z/BOMKR3Tde2LwuMEGqsOrKpr72rg/d8AtVvi2wwmYPoVwLRL078yonzMpH0x3QPwiYiIiIio22DgRUQ9jsvjR3WDr9WVGEPhMB7/fBseX7wtts1oAK6eMxxnT+3fLJiSofR13ppY5ZbZZFQtj7XeKvx92UP4ouIL3fFljjJcMeEKHDf4OJQ4SrqufTEU1Kq6fI0HVhG29BHg64eAcEJLaPFQ4Ng7gN7jkHZmK5Ang+mbV9oREREREVHPxcCLiNokFApj1W4Xapp8KMmxYny/AhglAepmqhq8cLn9+63++uPba/HpxqrYtjybGf9zylhMH1LSbCi901sLd6Aptk3mdeU5DHh98yt4Zu0zqpUxymQw4fThp+PC8ReiX24/5Fhy0GW8DUBjpXxy239uzRatqqtydcJGAzD5h8DMnwCdsaqkoxjIKeFgeiIiIiIiaoaBFxHt1+cbq7Bg0SZsqmyAPxiGxWTA8F55+PGc4Zg1oqzbBHYyg6vJ1/pKjBVON/7n1VXYXBWveBpY7FDD6QeW6MOpRn+DWoFRhtJHybyubQ1rseDLBdhRv0N3/PjS8fjxIT/G5LLJKLQVdl37olR1Ne7TAq8DOXf5U8CXC4DIjDIlv5+2AmP/aUg7mdGlBtN3YVhIREREREQZzRBuraeHYlwuFwoLC+F0OlFQUMCPDPWosOuWV75DgzeA4hwrrCYjfMEQapv8yLOZ8IczJ2Z86CVD6fc49z+cftn2Wtzxxmq4PPFQ7PChJbj15LGqwiv2eCG/quqSNsYok9EAk7kJT697HAt3LNQ9bqG1EJdOuBQnDz0ZpTmlakh9l5HWRWlhlOCqvep2AB/cDlSs0G8fdwZwxA2dszKiLQ/ILedgeiIiIiIiahUrvIio1aooqeySsKtPgT1WkWQ3mtCnwIg9Lq/aP2NYaca2N3r8QbUSY2vD6SX3f+Wb3fjXRxuReNi5hw7AFUcOU2FW9Lh6vwsNfpdu/pfJBCyrXogn1zyBRn+8MswAA04YcgIumXAJhhQM6dr2RWlbbKoCPK72nysVbN+9CHx+t7YaY1ROKXDM/wBDjkTaqcH05YCdLzgQEREREdH+MfAiohbJzC5pY5TKruT2O7lflGNR++W4iQMKu+Vwel8ghLs/2IC3V+6JbZPVF2+aPwrHju2dNJS+FoGQfv7XPs82PLn+IWyoW6/bPqxwGK455Boc2udQFNmKYDToV3TsVH430LAXCLbezpmSqwJY+Dtg59f67aNOAI76ZeesjGi2Afl90r/aIxERERERZQ0GXkTUIhlQLzO7pI0xFZvJCGcorI7LNNUNXjj3M5xejrn99VVYXVEf21aeZ8Mdp4/H6D75saH0MqerKaFyS8iQ+nd3vYj3tr2NEOKtkjnmHFww9gKcNeoslDvKYTV14eqBEvQ1VQPuugM7d83rwCd/AxLfd3sRcPSvgRHHIu0kZJXB9HLpqnlnRERERETULTHwIqIWyWqMMqBeZnZJG2MybzAEi9Ggjutuw+nXVLjwm9dXqQqwKFl58nenjUdJrrXFofRSLfZN9WK8tOkx1HprdY951ICjcNXEqzC8aDjyrHnoUgEvUL8HCLYe+qUkM74+/D2w7TP99mFHA0fforUypptUc8lgeksnrPZIRERERERZh4EXEbVIAiBZjXFNRb2a2ZXY1ijBT12TH2P75qvjutNw+ndX78Xf3l2nqteiTp7YF9ceM0K1M/qDPtT5auELenXnVbor8MKmR7CqRj+0vX9ef1w96WocMeAIFNuKYUoRDnaqphrAXatVabWHHL/2TeCTvwK+hBUcJbybczMw6sTOqbSSOV0yr4tVXUREREREdIAYeBFRi2QQ/Y/nDFerNMqAepnZJW2MUtlVF1mlUfZnwsD6tgynl333L9qEl5btim2TgfQ/nTsCp03uizDCavXFxkCDbu6XP+TDuzteUxdZoTFKVlv8/qjv4wdjfoC+eX1hM9nQpaSaS2Z1+RMGy7dVw75IVden+u2DZgHH3KZVW6WbBIV5vTpntUciIiIiIspqXThFef9++9vfqoqSxEufPn1i++UPUjmmX79+cDgcOProo7Fq1SrdY3i9Xlx77bUoKytDbm4uTjvtNOzcubML3hui7mnWiDL84cyJqpKryRtAZYNXXct92S77M2E4fYWz9bDL2eTHL1/6Vhd2FTos+Ov3JuH0Q/rBE3SrCq4Gf70u7FpT+y1+v/QXeGvbi7qwa2qvqVgwbwF+MuUnGFI4pOvDLo8TqNve/rBLVXW9BTxzjj7sktDpmN8Ap97TOWGXvL2iQQy7iIiIiIioZ1R4jR8/Hu+//37svskUbxX6y1/+gr///e949NFHMWrUKPz+97/Hcccdh3Xr1iE/Xxs4ff311+ONN97As88+i9LSUtx444045ZRTsHTpUt1jEVHLJNSaMaxUrcYoA+plZpe0MWZCZVdbhtNvrGzA/7y2Entd8RbFEeV5ajh9Wb4Z1Z5KeAL6oEgqvV7a/DiW7Ptct73EXqLmdM0fMl/d7vL2xVBQm7nl0w/Vb5PGfcBHfwS2LNJvHzgDOOZ/tJUR003aFqV9UdoYiYiIiIiIekrgZTabdVVdUVKBcdddd+HWW2/FWWedpbY99thj6N27N55++mn86Ec/gtPpxMMPP4wnnngCxx6rrSj25JNPYuDAgSpEO/7441t8u1IZJpcol8uVlvePqLuQcGvigEJ0t+H0C9dW4s7/roM3EJ/rdcyYXrjxuJEIoBGV7n26ii4ZUP/x7nfx+tZnVdVXlBFGnDL8FFw8/mIMzB8Ih9mBLichl7QwhlqfWdaMvL/r3wY+vhPwJnxvs+QCR/wcGHdG58zPsji0FkYZUE9ERERERNSTAq8NGzaolkWbzYbDDz8cf/jDHzBs2DBs2bIFe/bswfz582PHyjFz5szB559/rgIvqeLy+/26Y+SxJkyYoI5pLfD64x//iN/97ndpf/+IKD3D6aW98eFPt+DZr3fEtklB2pVHDsPpU8rg9O1DIKFFUWyr34RnNjyE7Q2bddtHF4/GNYdcg6m9p6LAWqAb3t8lJOCS6ixv/YHN6vroD8DWj/XbBx4eqerqi7STj5+s9OgoSv/bIiIiIiKiHimjAy8JuB5//HHVrrh3717Vsjhr1iw1p0vCLiEVXYnk/rZt29RtOcZqtaK4uLjZMdHzW/LrX/8aN9xwg67CSyrDiKhruX1BVNa3Pq+r3uPH799ag6+31sa25dvNuPWkMRjVz4Bqzz79YwaaVEWXVHbJ4PqoXEsuLh53Mc4ceSbKHGUwG80ZUtVVqbUyHsgKjJ/+TR+UWXKA2T8Hxp/Jqi4iIiIiIsoaGfDXW8tOPPHE2O2JEydi5syZGD58uGpdnDFjhtqeXGkhrUn7q75oyzFSLSYXIsocMqurptGna0FMtqWqUc3r2l0Xn8k1tCwXt548DDk5bjT640GRPM7SfV/gxc2PweWr0z3O3IFz1ayuYUXDkCOhUFeTqq6mKsBzAO3V0vb44f8C2z5rXtU19zagoB86Z1ZXGWDPnLZYIiIiIiLKXhkdeCWTVRYl+JI2xzPOOENtk0qtvn3jLTiVlZWxqi+Z/eXz+VBbW6ur8pJjpFKMiLoHCaaqGnyqcqs1H63bh7/8dy08/nir4xEjSvGjueWAsQGJHZCV7j14buPDahXGRP3z+qv2xSP6H4EiW1HXty8KX1NkVtcBVHWteV2r6kocas9ZXURERERElOWM6EZkiPyaNWtUwDV06FAVaL333nux/RJuLVq0KBZmTZs2DRaLRXdMRUUFVq5cycCLqJuQ1sUKp6fVsEuOefCTzbjjzdWxsEtiqgtm9MPV8woAY3wBCn/Ij/9sexG/X3KTLuyyGq24YOwFeOC4B3DCkBNQbC/u+rBLqrqkfdG1u/1hV30F8Ma1wMI79GHXoJnAD5/rnBZGefy8cqCwPwfTExERERFRp8roCq+bbroJp556KgYNGqSqsmSGl8zSuvjii9Ufotdff70aYj9y5Eh1kds5OTn44Q9/qM4vLCzE5ZdfjhtvvBGlpaUoKSlRjylVYtFVG4koc3kDQex1ehFoZRVCl1ub17VkW3xeV67NhGuP7YfxA8wJE7mAdbUr8czGh1DprtA9xtReU1VV1/iy8WpuV0Y44KquELDqZeCzuwF/U3y7NQ844kZg7KmdM6vLlgfklAGmjP4xQ0REREREWSqj/xLZuXMnzjvvPFRVVaG8vFzN7Vq8eDEGDx6s9v/yl7+E2+3GT37yE9W2KEPu3333XeTn58ce4x//+AfMZjO+//3vq2PnzZuHRx99FCaTqQvfMyLan0ZvAPvqvQi1Mq9r074G/Oa1VaoCLGpgiR0/m1+O3oXxb2/1Pide2vwEvqr8RHd+sa0EV026UlV0lThKYDQYM2RWVzXgcbb/3LodwML/B+xeqt8++Ahg7q1AXi+knQRcueWANUOCQyIiIiIi6pEM4damP1OMVJZJxZjT6URBQQE/MkRpVNvoQ22Tr9VjFq6txF//uw6eQLz667BhebhiTikcVi24CoVD+HzPQryy5Wm4A/G2PgOMOGXYybhswmUYWDAQNlOGLFDhd2tVXcFA+86TKrAVTwNfLgAC8fZN2AqAI28ERp/cOe2LMpA+p7RzKsiIiIiIiIi6a4UXEfUskr9LVVeDN7DfeV3PL9kZ2ybxyjmHFeOUQwpjc7d2NmxT7YtbXOt15w8vHIFrp/wU0/tOR4E1Q8Jred1Bqrrc+pUi26R6kzana+9K/fZhxwBzbtZWRkw3s02rHpNrIiIiIiKiDMDAi4gyQiAYwh6XB76Eiq1kdU0+/L+31uCb7fFgKMdqxDXzyjFpUI667w168Na2F7Fw51sIIf5YdpMDF467EN8ffQ7KHGUwGzPk25/fE6nqan0Fymbk+GWPAV8/BIQSznWUaEHXiE6YUyjholR0OYrS/7aIiIiIiIjaIUP+4iOinszjD2Kvy6Oqt1qybk89bn99FSrr4y17A0ss+Nn83uhdaFH3v61eiuc3/h9qvFW6cw/rPQvXTvkxRpeOhsPsQMZUdblrtUt7O8sr12hVXVX66jWMPkkbTN8ZAZQ1B8jtxaH0RERERESUkRh4EVGXqvf4UdXgU+2MLXl75R7c9f56+IPxYw4fnosr5pTBbjGi1luN5zc+ghXVX+vOK7P3xpUTf4SThh2HQlu83TEjqroaK4GAr53nuYGvHgCWP6mtxhglwdPcW4AhRyLtjEZt9UV7hrSDEhERERERpcDAi4i6THWDF053y618/mAI//xwI95YURHbZjQAP5hRghMmFqiWRWldfGPb86qVMcpkMGH+oNNw5eRLMaRgACwmrQKsW6/AuPNr4MPfA8747DJl3JnA7J8BtvjqtGljy9NWYDRylVsiIiIiIspsDLyIqNOFQmHVmtjka3k4vQyv/90bq7C6oj62Ld9uxE+P7YVx/R3YVr8JT294EDsatujOG1EwBleM/wmOGjwd+RLQZApfI9C4r/0rMHrrgc/uBla/ot9eOACYexswYDrSTgIuCboy6eNJRERERETUCgZeRNSpZCi9zOuS6q2WfLuzDr97YzVqm+LVX8PKrbhufm/k2H2qfXHR7v8ijHiLY645D2cNuwBnjz4TQ4v7wGgwIiOEgkBjlRZctdfmD4GP/gQ0Jcwkk/frkAuAw64CLJ0wj0xaF6WFUVoZiYiIiIiIugkGXkTUaRq9AVW5FWphXpfM8Xrlm11YsGizboD90WPycMGsEqyuW4LnVz4Cp69Wd96M3nNw3sjLMK3/GBTatdUaM4LHpYVV0srYHhKQfXInsPF9/fayUcAxvwF6jUXaSRtoXq/OCdWIiIiIiIg6GAMvIuoUtY0+1Da1PKTd7Q/i7++uxwdrK2PbzEbgoiNKMWmoBw+v+ytW1izTndPb0Q8/HHUljux/FIaWlMFsypAqJGlblKH0vqb2nSdB4JrXgM/u0leEmazA9CuBKRdqQVQ6yWD/nBLAXqTdJiIiIiIi6oYYeBFR2ud17WvwququluysbcJvXluFrdXxgKg414Rrji3Bdv9C/L8lL8IX8sb2mQ0WnDDoTJw69PsYVToAZXm5mfNZdNdpg+lbWXUypbrt2lD6XUv12/seAhxzG1A8FGlnzdVmdZn4o4GIiIiIiLo3/lVDRGkjc7r2OFuf1/XZxir88e01aPLFjxnbz45TZzjx4vZ7satxu+740UUTcP6oH2FsyXgMLi6Bw5ohKwYGfFpVlz++WmSbBP3AN08AXz8IBBMq4Cy5wKxrgQlna3O70kkCLgm6JPAiIiIiIiLKAgy8iCgtZAVGmdeVOIsrkWz/v8824Zmvdum2z59kgan0Ldy/5n3dUPo8SwG+N+wiHN3/BPTOK0OfAkfmtDA21QDu2vZXde1dCSz8PVC9Qb996Bxgzq+0GVrpJC2LjmLtwvZFIiIiIiLKIgy8iKjDOZv8qG6MtyAmq2vy4Y63vsPy7Q2xbXYLcOxhW/FNw7Nw7anTHT+7zzycM+Ii9M8djF75+SjJtWbGZy3gBRoqtev2kNleXy4Avn0WCCdUv+WUAkfdDAw/Jv0BlAyjl0At3TPBiIiIiIiIugADLyLqMLLKolR1NbQyr2vl7mrc8eZaVNXHj+lTWo8+w9/EopoVumP75gzA+aOuwpTyw1FoK0R5vg051gz4tiWVXFLV5alrf1XXlo+Bj/8M1O/Rbx9/FjDrOsCWj7QymoDcsvS/HSIiIiIioi6UAX85ElE2kDlde10e+AKp53UFQ0G8vHwLHly0C/FDghg24ivU2d7Gpvr4/CqL0YITB52NU4acgzJ7L+TYbOidb8uMFkapzmrcp83eag855+M7gU0f6LcXDQbm3gb0n4q0sxdqVWTGDPg4EhERERERpREDLyI6aG5fEJX1nhbndTk9jfjH++vw8br62DaTYzt6D3sd+0I7gYSMbEzRJFww+iqMKBwLhzkHhQ6LamE0dPWMqVAQaKwCvPH3oU2kZXHVy8Dn9wC+xvh2oxmYdgkw7TLAbENama1Abi/AYk/v2yEiIiIiIsoQDLyIKG3zuoLhINZV7sVf3t6G7dWRCi6jB/l9/wsULEZ9QkCWbynE94ZfhKP7nYACWxHMRpNqYcy1ZcC3KY8LaKoCQi2vNplS9Ubgwz8Ae/Stmug7WavqKhmGtJKQMKdEG0pPRERERETUg2TAX5JE1G3ndTV40eBJPa+rKdCID9ftxIKFlWjySVAUhjl/JXL7vYGQ0ZViKP3F6J87CFaTDTaLCb3ybbB0dQujtC1KK6K0MbZHwAMs+T9g2aNaZViUNQ+Y9TNg/BmAIc3vmy0PyCkDTPw2T0REREREPQ//EiKidgvIvK56L7z+hDAnIhgKoNpTg6cX78br3zjVNoO5DvY+r8Kcvzaxe1E3lD7PUqC2FTgsKO3qFkYZRC8D6WUwfXuH0m//Alj0J8C5U7995HzgiBu1gfHpJKsu5pYD1pz0vh0iIiIiIqIMxsCLiNrF4w+q4fSp5nU1+Ouxy1mNf76/F6t2edRQekvJ57CVvweDMT6U3myQofRn4tSh56qh9CajGUaDITNaGP0eoLESCMSfb5vIfK9P/w5s+K9+e35fYM6vgCFHoFPaF+1F2m0iIiIiIqIejIEXEbWZ0+1HTaNPtTMm8of8qPPWYOUuJ/75fiVqG4Mw2nfC3ucVmBy7dMeOLpqA80f9CKOKxqmh9CIjWhhlPpe7BnDXHeBQ+nsBX0N8u8EETD4POPxqwOJAWtnytdUX2b5IRERERESkMPAiov2SgKuqwYd6j7/Z9ga/Cy6fE+9868SzX9YgCC9svd+FpfhzGAzxYCzPko+zh12EY/qfpIbSGyMzrDKihVFWT5RZXcHU88haVLUB+PB/gb3f6bf3ngDMvRUoG4X0r75Ynv5AjYiIiIiIqJth4EVEBzSvyxf0os5XA5fbiwcX7cPXm5tgyluN3D6vwWjRZndFzeg9B98fcSkG5g1RQ+lFRrQwykB5Cbq8CZVZbeF3A189ACx/CggnDqXPBWb+FBh/NmA0IW2MRsAhqy8Wpe9tEBERERERdWMMvIio1XldlS4vAtLul1DV5fLVqXldO6p9uOe9SuxtrIa9/+uwFKzUnd/L0Rfnj7wSh/aeHRtKnzEtjB4n0FSttTK2x+aPgE/uBOr3pBhKf4NWcZVO9kJtVlc6AzUiIiIiIqJujoEXEaXk8vhR3aCf1+UNetSsrkAogE/X1+ORT/YhnPclcoe9DYPJGzvOZDDh+IFn4PSh56Hc0VsNpY8qdFhQ0pUtjDKMXobSy3D69nBVaEHXlkX67QX9taH0g2chrSx2LUwzaxVyRERERERE1DIGXkSkIwFXdaMPLnd8XlcwHESNpwqfVLyPfY1VWLl+BNZUNMDe/2WYcrbrzh9RMAYXjL4aY4onxobSC5NRa2HMsXbRtx0J7ty12iVp6H6rgn6tdfHrB4FAQkgmFVZTLgIOvTy9M7Tk7eSWaYPpiYiIiIiIqE0YeBGRbl5XZb1XtTJGNQUa8eLGx/D2tpfR4LbBvfv7MOd9ipyhH8NgiLcDOsy5OHPo+Thu4KkospXEhtILe6SF0dxVLYxSzdWwVwuv2mPXMmDRH4Gazfrt/aYBR/8KKBmGtJEKOEexdunKgf5ERERERETdEAMvIko5rysYCqDOV4s3tz6HVzY/DV/9WPjrpsHR7wUYrdW6j9q08pk4b+QVGJw/PDaUPqoox6paGLuEvC8yp0vmdbWHVIF9djew9g39dgmfZl8PjD45vSGUNUdrXzRZ0vc2iIiIiIiIshgDLyJqNq+r0d8Al68W/pAfb255Be7KeTBZa5Az8AndR0sOtxgtuHbibSiyFevmcpmNRtXC6LB20XB1WXlRVmCUlRjbSo5d/SrwxT8BrythhwEYfxYw8xptaHy6mMxAjrQv5qXvbRAREREREfUADLyIejAJuKoafKj3aK1+MoxehtLLcHqxcOtiuCpnwFryBYzmxoTztAInuQTCfnxT9SWOGXBibL+EXL3y7WpuV6cLBrSgyxd/vm2ydzWw6E9A5Sr99rLRwNG/BvpMRNrIB9JepK2+yPZFIiIiIiKig8bAi6gHz+vaW++FNzKvq8HvgsvnjFV5fbp5K15a/x/YyzfpzguHzDAYA7pt6+pWqsBLKrxKcqwozOmiVjx3ndbC2J6h9B4XsPg+YOVL8t7Ft1tygcOvBiZ9H0hYZbLDycB7tfpiF7V9EhERERERZSEGXkQ9dF7XXpcHwVBYtS3WeavhC/rUPn/Aj3u+egkbfW/AmBMPtsIhaU0MNwu7hN3kgMWktTDKgPpOF/ACDZXadVuFQ8DaN4HP79FmdiUaeTww++dAXjnShqsvEhERERERpQ0DL6IextnkR02TD6FQSFV11ftdsaqulfvW48GVC+A37UbCIosI+QtgtCTOtNI7dtCx6F/kgLGzWxjleTfVAJ669lV1VW3QVl+sWKHfXjwEmHMzMOAwpA1XXyQiIiIiIko7Bl5EPUQoJPO6vGjwBuAP+lDrq4Y/qM3u8gTceGzVU1hR9z5gigdHhmA+wiELjJaaFh/XZrJh9qBDOj/s8jUBjZXazK628tYDX94PfPcCEE4YZm+2A9OvBA45P70rI9rygZxSbTg9ERERERERpQ3/6iLqAfwyr8vlUfO66v1ONPjrY1Vd31YvxaOrH4QnXKsWI4wqCs7EBRNPwOMb/oF6vxFhhJo9rhFG5Fpysa52HcaXju+cd0YCrqYqbRXGdrUvvhVpX0wK74YdAxx5A5DfF2ljsWurL8o1ERERERERpR0DL6Is1+QLoNLlhSfgQa23BoGQVtXl9NXh6XWP4LvaxbrjQ74yHF12KX502HFY71wFk9GEYktv1PpqEYbM+ZKgzAAjrCi1lyBs8KFOWgozdSj9vnXAoj8De5LaFwsHAEf9Ehg8G2kjlVxS0SWVXURERERERNRpGHgRZbGaRh9qG71w+epUVZeQyq7P93yIFzc9AW+oKXZsOGyCpX4ubppxCQ4bPAQmgwkF1iIEQ0bUN4UQDpUCBj8MBqn0MiEUsqA64EdRrhFF9qL0viN+j9a+GNAG67e5fXHxAmCltC8mVKeZbcChlwOHXKDdTgfO6SIiIiIiIupSDLyIspCsvriv3otadwPqvLWxqq69Tbvx1IYHsNG5Rn9802CMNF+A206dg9LceDXSkPyRCHjKEDLsQChUIL15CWeFEDY1wu8ZhNFFY9LzjoRCWvuip+WB+c1IuLXmTeCLFKsvSvviETcABWlsX+ScLiIiIiIioi7HwIsoy3gDQexxulHtrkGjX5tzFQgF8N6O1/D29lcQCGvhlwgHbfBXnYgLxp2BH0wfodoXE22udKNh75EIl70Eg8WFcCAHCJtkmj0M5iaEQjbU7zsSayoaMHFAYce+IxJySdgloVdb7V0FfPwXYO9K/fbCQcBRvwAGz0LaSLVYbjnndBEREREREWUABl5EWaTe48cupwu1nmoVcoktrvV4av0D2N20Q3es3zUeeQ1n4w8nHo4J/UtSPp7T40OjcxhM/jNhLV0Eo61SVXbJuPqQpy981XMQahqKygaPpEod805I22LjPsDvbvs5TTXA4n8Cq1+PzBhLWH1R2henXACYrEgLCQllTpddKuCIiIiIiIgoEzDwIsoCMperst6DXa59saouT8CN17Y+g493v4twQggU8hfAu/c0HFo+G78+Yzzy7YltinGyfU+dJ9LyOAIe9zAYbbtVZZdUeoW8/RAOG9X+VTtdmDem98G+E1pwJQPw2zqUXkK9714Avrwf8CWt2jjiWGD29elbfVHmdMnsMkcxYNQ+DkRERERERJQZGHgRdXOBYAjbap2obNwXq+r6tnopnt3wMOp81bpjfbUzEKg6AVfOHotzpg2EQUKbJCajAWV5NuTazDAaE/cbEfIOALzNn0OKh2kfX6NW1RXUnn+b7FyitS/WbNJvLxkOHHUTMOAwpHdOVwlgSh0WEhERERERUddi4EXUjTV6/dhQXYF6r1bd5PTV4YWNj2BZ1WLdcUFvObwVZ6PMMgq3nzseY/qkbr9zWE0oz7PBbNIqlgYU50Ayr1BYK7pSuZb8n9yPnCP75bgDIgGXBF0SeLVV/R7gs7uAje/pt1tzgcOvBiack74gypqjtS+ma3VHIiIiIiIi6hAMvIi6qT0uJ7bU7lVVXdLS+PmeD/HylifhDsTDo3DYBF/V0fBVz8WRI3rjF/PHIM/e/MteKr1KcqwozNEHRadO6ovfvbkKzqaEQfeR4Cta1FXgsKjj2kUeRFZQlEtb2xcDHmDZ48CyR4FAUpnZ2NOBmT/Vqq7SQQIuCbok8CIiIiIiIqKMx8CLqJsJBIPYULUHVU1Odb/SXYGn1z+I9c5VuuOCTYPhqTgLxmAfXDd3OE4/pF/KFkaLyYheBTbYzPoVGoXZbMQ1Rw/Hn99Zh2AoDCn8ihR4IRjS2h9lvxzXZjKMXqq6ZDh9W0ggtukDraqrvkK/r9d4bfXFPhORFiazFnRJCyMRERERERF1Gwy8iLoRp6cB66v2wOP3IRgK4P2db+I/21+EP5RQgRW0wVt5Ivx1h6FfYQ5+c+o4jOqdOrCR6qzSXGvKICzqyqOGq+v7PtqEerdfrdEoR0s1mIRd0f37FQoCTdWAx9X2d7hqA/DJncCupfrtjhKtomvsqYAhDQPjZQi9vA17YQcMKCMiIiIiIqLOZghLLxTtl8vlQmFhIZxOJwoKUs8/IkqXUDiEHc5K7KirVe2LW+s34un1D2Bn4zbdcf76cfDuOR3hQCHmji7HDceNUsPnk0llVnm+DTnWtmfegUAIb3xbgV11TehflKPaGNtc2SUhV1MVEJK4rA3cdcBX9wMrXwLCCecYTcCk84DpV6Sn6krCLVl1UVZf5MqLRERERERE3RYrvIgyXKOvERtr9sDl9sIb9ODNrc9j4a7/IBwbGw+EAvnw7jkNgfoJsJpN+Olxw3HyxL4pK7ck5JKwS0Kv9pBw68yp/dv35KVtsbES8HvadrysMikh15f/Brxay2bM4NnAETcAxUPQ4eTjJNVcEnZJqEZERERERETdGgMvogwVDAVR2VSN7XU18PlDWF2zAs9seBDV3n2643y1h8FbeQIQysHgkhz8zyljMaw8r9njGWUwfZ4VBfY0rWCYSApHm2oAT13bh9Jv/wL49O9AzWb99sJBwJE3AEOOTMtTVZViMuw+XSs7EhERERERUadj4EWUgZr8Tdjp2ouqBg+cXhde2vQ4vqz8WHdMyFsGz56zEGwapu6fOKEPfnrMCDgszSuUbBYTyvNssLZnuPyB8jZo7YvBQNuOr90GfPYPYOsn+u2WHGD6lcDk89ITRsmKizKQXlZgJCIiIiIioqzCwIsow6q6ajw12O2qg7PJhyX7PscLmx5Fgz9h0HvYCG/1UfBVzQPCFhVw3XDcSMwb2zvlYxbnWFGUY2l1MH2HUO2L+7RVGNvCWw98/SDw7XNaK2OMARh7GjDjJ0BuWcc/T4tdC7osjo5/bCIiIiIiIsoIDLyIMkSDrwFV7hrsa2jCblclntn4EFbVfKM7JuQZAPfusxDy9lP3R/bKUy2MA4pzmj2exWRUs7rsKSq+OpQMondL+6Kzbe2Lslrj6leBxf/SWh4T9T0EOPImoNfYjn+eUiUmQZetebsnERERERERZRcGXkRdLBAKoNpdjTpPI6rq3Vi48794beszakB9lCFshbvyOPhrZss0LrXtrCn9cdVRw1K2KebbLSjNtcLYzsH07dbe1Rd3fKnN6areqN+e3xeY/TNg+LHaAPmOJEPoJeiyc3VVIiIiIiKinoKBF1EXqvfVo9ZTC6fbh7VVW/DE+vuxxbVed4zRMwqunWcg7C9R9wvsZvzi+NGYPaJ5u5+svFiWZ0OuLc1f2rLqorQvBrxtO752K/DZXSnmdDmAaZcCh5wPmO0d+xyNRm3VRXtRx4doRERERERElNEYeBF1AX/Ir6q6mvxu7KtvxCubXsI7219BMByMHWMx5KJh98nw1U3R5loBmDSgELeeNFa1KiaTkEvCLgm90kbaEZuqtcqutnDXAV8/AKx8UTs30eiTgZnXAnnlHfscJdySkEvCLgm9iIiIiIiIqMdh4EXUyVw+F+o8dfAEAvh610o8tnYBKpp26o7J9U/F3i0nIRzU5k1JhnXRzME4//DBzQIto8GA0jyramNMKwmvZFZXW9oXg37guxe0ofReV/M5XUfcCPQel4agqzASdKV5bhkRERERERFlNAZeRJ1c1eUJeFDdVI/HVj2Bj3a9gzDig97zTCVo3H0G9tSMim0rz7Ph1pPHYNKAomaP6bCa1H6zyZjm9sVKbRXG/ZGh9VsWAZ/dDTi36/cV9AdmXQcMn9fxLYYyn8tRApj4LY2IiIiIiIgYeBF1CqfXiTpvHULhEBZt+wqPrL4fNd4q3TF9jXOwfvUxQCjerjh7RCl+MX80Chz66i2DwYCSXCsKk7Z3KGlBbKwCvPVtO75yDfDZP4BdS/XbLbnA9MuBST8AzM1bMQ+KrLgoQZfZ2rGPS0RERERERN0ayyGI0sgf9KPKXQVv0Isadx0WrHgQi/d8rDumzNYP/r1nY31F/9g2i8mAnxw9HKdN7qfCrUQ2i1bVlWp1xg5tX5RZXVKxFRUOAfvWAp46bUZW+RjAYATq9wCL/wWse0v/GLJv3JnA4VcDOdrA/Q5jzdFWXuzoAI2IiIiIOkQ4FIJn9RoEa2thKi6GfdxYGDhflYg6EQMvonRXdYVC+GDbIvzfqgdR74/PszIaTBibcyKWfzsTHl985tTgkhzcdspYDC/X5ndFSfBVnGNBUU4aq5n87sjqi0ntizu/ApY8qq22GA4ABjNQOBDIKwM2fQgEk1ZrHDQTmPUzoGxkxwddUtFl6eAVHYmIiIiowzQuXoyqBx6Eb8sWhP1+GCwWWIcORdlVVyJ3xgx+pImoUxjC4cQSDmqJy+VCYWEhnE4nCgoK+IGiFvmCPjWrS6q69rn34Z6l92F5lb7Nb2DucNjrzsWyjfp/S6dO6osfHz0cdot+6LpUc8nKjDZzmoaxBwNAk7QvNjTfJ2HXh/8LeBsBi0NbMdLXEBlGn/Tto2Q4MPt6YPCsjn1+DLqIiIiIuk3YVXH77Qg1NMJUVASD1Yqwz4eg0wljbg76/u53DL2IqFOwwouog0h2LFVdTp8TwVAQb295G4+uegyeoDt2jNVow6yys7H4m6nY5wrEtufbzbhx/igcNbK8WVWXzOmSyq7k1sYOetKAu1a7pMq+pY1RKrvcTiAUADxOGe7V/DipuprxY2Ds6R27QqIEbNK6yIouIiIiom7RxiiVXRJ2mXv3jv3+arDbYbDZEKisVPtzDjuM7Y1ElHYMvChryED4NTVrUOepQ5G9CGNLxsIoc6Q6qapLZnXJ9a6GXbh72b1YU7NKd8zowgnoGzgfb31sQEjaAiMmDSjELSeOQa8CfZuexaRVdSVXe3Xck27UhtIH/S0fIzO7qtYDviYpA0t9jMkOnPAnoP+0Dg66SiIVZURERETUHcjMLmljVJVdSS/Wyn1TYaHaL8c5JozvsudJRD0DAy/KCl9WfImHvn0IG+o2wB/yw2K0YGTRSFwx6Qoc3vfwtFZ1yZwul8+lBtS/svEVPLP2GfUcohzmXJzQ/3ws+XYi3tjdGNtuNAAXzxqCHx42CCa5k0CqumQVxrRUdUnAJXO6VIi1H9UbtSH1ya2LMQYgHAQC8Sq2gyKVXKqii0EXERERUaYLB4PqAr9fXfu2b0fI64UxNxchn0/9Livzu6JUe6PTqQbZExGlGwMvyoqw65ZPbkGtt1ZVeUUtrVyKzZ9sxh+O/ENaQi+Z0SVVXRJ0barbhLu/uRtbnFt0x0wpOxzjbBfiyQ/daJAZWBG9C2y49aSxmNC/sPOqukIhwF2jtSXub3SfxwUs/T9g+dOthF0irLU6SsvjwQZd0hYps7qIiIiIKCOEA4HIRV7g9Gu3JeTyB9QMWGlhTGQwm2EwmRD2elULI5JevJVZXhKAyaqNRETpxsCLujUJuP6+5O+o8lSp7EVaGA0wIIywmqMl22X/M6c802HtjdGqLpnXJaHXs2ufxcsbX9aFbQXWInxv2OVYvW4s7l9dpTv/mDG9cP28kciz67/8ChwWlKarqktCrqZqLfRqTdAHfPcC8PXDgLetIVYYsOuDuzYz27TWRWvugZ1PRERERAdErV0WqcyKBluIBlxStSXX7VzfzDZiOCwDBsC3dStMVqv0Aujengyut40aBfu4sfysEVHaMfCibm1V9SpsrNuoMhcJuQIJs7Ek+DKEDWq/HDexbGKHVnWtqlqFe5ffq2Z2JZrVZy5mFl+EBxc6sasuHnY5LCb87NiROG5sL12oldaqLr9bm9MV8LZ+nIR1698BFi8A6ne3840YIsPs24FBFxEREVFaRYOsxBArVpkVDbU6msGA4nO/j3333INgTQ1QUACjVHzFVmnMRdlVV3JgPRF1CgZe1K19t+87NS9Lwq5k4ch/sl+OO5jAS16RkpZJl9eFJn8THlv9GP6z5T+6Y0rtvXDhyB9jx86R+H+v7kIwFH9OY/vm45aTxqJ/kaNZVVdJjhXGpBleBy0YAJqqAG/D/o/d/gXw+b1A1Tr9dksuMOxoYP1/tUAs1eqMMGqrMjraWOHFoIuIiIgofdVZ6n6k/bCd1VkdxTF5Msqvuw61zz0P/65dCDU2qjZGqeySsCt3xowueV5E1PMw8KJuTX6Qpwq7dMfIfwfxAz+xqmvJ3iW4b/l96n5iJdkxA07GsX0uxIMf1mDFzp2xfZJj/fDwQbhoxmCYTUZdVVdZng0OawdXdUnLogyZd9fuf05X5Rrgi3uBHV/qt0uANf5sYPqVQMMeYNvngLdeq+SSyrSwXIcjj28ArHna/K3WMOgiIiIiarOUFVnRICs6KD6DSejlmDQJvl27VVujzOySNkaDsXNWUCciEgy8qFvLt+V36HEtzeqSy0PfPYSPdn6kO6ZvzgBcOuZauGpG4FfPb0GDN95S2SvfhltOGoNJA4r0z8Wuzerq8KouGTSv5nTt5xcg1y5g8b+0FsZkI44DZlwDFA3U7juKgLJRQOXqyGqMPq3SS0IvCbEMJm1/+ZjUb4vD6ImIiIh0ZNC7viLr4GdnZSSDAfbRo2Dp1aurnwkR9VAMvKhbK7IVxYbUt0T2y3EHUtXlC/jw6e5P8e8V/4bTF59TZTKYcMKgs3DywPPx+Ge1eG/1Bt35R48qxw3HjdINpk9bVZevSWtfVGFUK5pqgCUPAytf1FZWTNT/UGDWdUDv8frtMuj/0EuAD/8XkFUm7UXaNmlxlPlgtjxtf/KCABYH4CjmqotERETUc1sNVYWWH4jdTr2yIRERpQcDL+rWpPKqI49LrOqqdldjwYoF+HKPvuVvSP4IVdUF73D88vmNqHB69IPp543AceN66wbTp2VWlwRcUtHla2z9ONn/zZPA8icBf5N+X+kILegaNKvZstExAw4D5t4KLHkUqN0KhL2y5rRW2SVhl+xPDLpk1UW5JiIiIsriMKtZVVY3aDUkIupJGHhRtyaVW2ajGcFQMDakXrdKIwwwGU1tqvBKrOp6b9t7+L+V/4fGQDxMshitOH3oD3DKoPPw6rJaPPXlSiTMpce4vgWqhbFfwmD6tFR1ScuizOiSlRFbK3cP+rRqrq8f1uZ6JcrrDcz4CTDqRG1m1/5IqCVVYPvWao8llV7Sxhit7LLmaHO8pIWRiIiIqDuGWckthsm3GWYREXUrDLyoWytxlCDPmod6X736RUWqqgxhA8KGcOy+7Jfj2rIC457GPfjn8n9ixb4VumNGFY7HZWOvRS6G4bZXNmBNhQxx10jR1kUzB+P8wwfDZExjVZf8IhYdSN9aKbwEYuvfBr68H6iv0O+zFwKHXg5M+J42g6s9JNzqNU6/jUEXERERdcch8Mm3JdgiIqKswsCLurWxJWMxpngMVlWtQiAcgC/ki1V52Uw2mA1mtV+OS8UdcKvWRanuenPTm3hizRPqdpTd5MDZwy/CSYPOxsfrGnDfhyvg9sdL1fsW2nHrSWMxrl+BrqqrPN8Gu6UDq7q8DdqcrmCg9UBs6yfA4vuA6o36fWY7cMj5wJQLgQMY4N+MNVdrXWxvaEZERETUwWLVVy3MzsqaIfBERNQuDLyoWzMajLh84uW444s70OBrQLG9ODbE3u13q+ou2S/HJQqFQ6jx1KhzdtTvwD3f3IO1NWt1x0wsmaaqugrNg/Hntzfis43Vuv0njO+Dnx4zHDlWc/qquvweLeiS69bsXKIFXXu+1W+XdsXxZ2tVXbllB/98GHQRERFRJ9LNy2rWbijb/QyziIgoJUOYL3e0icvlQmFhIZxOJwoK4tU8lBm+rPgSD3/3MLa4tiAQCqi5XkMLhqqw6/C+h+uObfI3odpTDW/Ai5c3vIxn1j2jzonKs+Tj3BGX47gBp2LFDjfu/O861Db5Y/vz7Wa1AuOcUeXpq+qSSi4JuqSyqzWVa4Av/gnsWNx838jjgRk/BgoHHvzzYdBFREREnR1mcUXDbs+YmwtLr15d/TSIqIdi4NVGDLwyn1RtralZgzpPHYrsRaqNMbGyK7Gqa1PdJlXVtdm5WfcYh5bPxsVjrkGZbQAe/HgrXluxW7d/2uBi/PL40SrciiqUqq5cq25VxgN/J0KRgfR1rQ+kr9kCfLkA2PRB832DZwMzrgHKRx/880mc0SXPbc8KbWXInFKgz2TAqK+cIyIiImqpzbBZsMU2w6zHwIuIuhJbGilrSLg1vnR8yn3Rqi5pc3x23bN4acNLKgCLKrQW44JRV2NO//nYus+PH7/wDXbUumP7LSYDfnTUMJwxpT+MkWCrw6u63DKQvqb1gfSuCuDrB4C1bwIJz1/pewgw86dAvykH/1ySh9FvXgR8+g+gagMQ8gNGC1A2Ejji58CwOQf/9oiIiKjbDYBXoVViiMWZWURElEEYeFFWS6zqkhlddy+7GzsbduqOmd3nGFw4+scot/fDs1/vwGNfbEMwFK+uGlGeh1+fNAZDy3LVfankkqqu4hxLx1R1+RqBRhlIH2+bbKZxH7DkEWDVS0BC+6VSNhqYeQ0waJY8uQN/HnKuNQ9wFOmH0UvY9eb1Wnulo1jbF/ACe1dp20+5i6EXERFRFlAVV8kthSrUitzmaoZERNSNMPCirBWt6mr0NarVF9/Y9EZsBUdRZu+Fi0dfg5l9jkalK4jrX1uB1RWu2H6Jjs6dPhCXzBoCq1lr3ZNrqeqymTugqktCIwm6/PFKspRVX8seA757Tjs+UeEgbUbXiGOBpKH87Q667EWAvRAwJX1LkGozqeySsCu/bzxQszi0lR/rK7T9Q45keyMREVEGC8vP9NYqsqK3iYiIsgQDL8rqqq5v932Le7+5F3ua9sT2yyqOR/c/EReM/hFKbeV467sK/OujTfD44y2CvfJt+PWJYzB5YJF2jsGAIocFRR1R1SUD6aV10RMP15rx1gPLnwaWPwX4G/X78noD068AxpwKmCwH/jxkBUep5rIVthxWycwuaWOUyq7k91vuy3bZL8d1RCslERERHVyLYbN5WRz+TkREPRMDL8rKqq56bz0eWfUI3tn6jm5/n5z+uHTMtTi012y43CHc+upKLN5cozvm2LG9cN28kcizaV8eNosJ5Xm2WJXXAZM2ARlIL5eWhrRKtde3zwHLHge8Tv0+mal16GXA+LP0LYftZbZqFV22/P23QMqAepnZ1dLbk+0yYF+OIyIiovRUZbUUYrHFkIiIqEUMvCjrqrqW7F2C+5bfhyp3lW6g/fyBp+MHI65Asb0EH2/Yh7+/ux4uT3weVoHdjJ8fNwpzRpWr+1LJVZJjRWHOQVRRRUk1lwqPWmgVCHiAlS8Dyx5tHh7ZCoCpFwOTztVaCQ+UhFM5JYBVm0XWJrIaowyol3bKVG9btst+OY6IiIjajFVZRERE6cXAi7KmqqvOU4eHvnsIC3cs1O0fmDcYl439GSaXTofbD/z5nbX476q9umMOG1qCX8wfhdI8rZJJVl4s64iqLl8T0FQFBHyp9wd9wKpXgaX/pw2mT2TJBQ45Hzjkh1o11oGSoEpaD2XlxfbqM1lbjVEG1MvMrsSKsGjFWu/x2nFERETUrBpLzclSt0OsyiIiIupEDLwoK6q6vtj9Bf614l+o89bF9psNZpwy5Hs4e/jFKLAWYcWOOvzpnbXY64oPf7ebjbj66OE4dVJfVdFlNBhQnGtVqzAeFAm4JOiSwCsVWZFxzRvAkoeAhr3NK7EmngtMvUgLqg6UVHLJ+Rb7gT+GzPY64ufaaowyoD5xlUYJuySIk/0tzQAjIiLK5vbCUKj58PeWxhYQ9TDytbBvtxuBfdWw51lQPjAfBmMHrHBORNRGhjB/KreJy+VCYWEhnE4nCgoK2vrxpTRXdVW7q/Hvb/+NT3d9qts/rHAELhtzPcaVHCK/e+Lhz7bgpaW7EtZoBMb2zVeD6QcUa5VPDqtW1WUxHUR4Iy2LTTWA15V6TlcoAKz9D/D1g0D9bv0+kxWYcDYw9RIgt+zA3r5UYEkIJTO6ZFZXR9m8SFuNUQbUy0wvaWOUyi8Ju4bN6bi3Q0RE1MnUr8ItzcZSgRaHvhO11+5tbiz/ohY11UGEQ7JWkgGl/XMx7YQhGDCmhB9QIuoUDLzaiIFXZlV1yVD6j3d9rMKuel99bL/VaMWZw3+A0wdfiFxrHtbtqcef3l6LbTXxSiuT0YCLZg7GDw8bpG5LVVdJnhUFdkv6BtJL0LX+HeDrhwDnDv0+oxkYfyYw7TIgr9eBr7hoL9Qucjsd5JVtWY1RZozJzC5pY2RlFxERdacgK7FKi0EWUdrCro/erIS7MRT/tdigvS6bk2/FsZeOY+hFRJ2CLY3U7aq6KhsrsWDFAny550vd/jHF43D52OsxonC8ymYe/Xwrnly8DaGE/GlwaY6q6hrVW5uJlWM1oyzPCvPBVHW1NpBeBV3/jQRd2/X7JJgacxow/XIgv++BvW1pL3QUAda8/a+4eLAk3Oo3Jb1vg4iIaD8YZBFl9tfnVx9Wo6khlLRDe0240enD5y9vxDm/ms72RiJKOwZe1K2quj7Y/gEeWvkQGv2Nsf12kx0/GHURThh4LhzmHGyrbsQf316L9XsbYsdIFPS9aQNw+RFD1SB6qeoqzbMi/2CqulobSC/h14ZI0FW3Tb/PYARGnwRMvxIoHND+tyvBlsznkrbFg5nPRURElEFi1VeJbYTRoe+syCLqFmFXxXY3qvb6Wz2uamcDKre70HtIYac9NyLqmRh4Ubeo6qpoqMB9y+/Dsspluv2TyibhinE3YlDuSDWf64WlO/HQJ5vhD8bLuvoU2HHzCaMxeWBRx1R1tTaQfn9B16gTgelXAEWDDqzCyhZpWzTxS5eIiLoHXWiVcth7ZPVCCbyIqOu+VsNh+Lwh7eIJwRu5Tr7vlW2eoNqutnlD8Kvrti3YIDO9KjbVMfAiorTjX82U0VVdLq8L/936Xzyy6hG4A+7Y/hxzDi4edznm9T0bFpMNFU43/vLOOqzY6dQ9zkkT++AnRw9XIZfM6yrJPYiqrmAAcNdoLYzNnnAA2PAusORhoHZr6qDr0MuB4sHtf7syfF6quWQYfbrbFomIiNpIAit9BVaKIe9ctZCo04RC+sAqVXjl9Qa1fdFtCcf6fZ23wmjV9ngnBhFRujDwooyt6tpVvwv3fnMvvq36Vrf/0N6H4qpxN6CXfYi6/+a3u7Hgo81w++MztIpzLLhp/mjMHF6q7ufZzSjNtanQq93kF3YZRu+paz6QPjaM/uHmM7pU0HVCJOjSnmv72xYLAYuj/c+ZiIjoAKiQKrGFMKSvyooNfJdtRNShgoF4YKWqqGKBVLyaSiqptAqrpAosbwgBf+cFVgfLbDuI+blERG3EwIsysqrrrc1v4bHVj8Eb9Mb251nycOXEqzGn92kwGCzYV+/FX99dh6+31uoe56hRZfj5vFEozLHAYjKqWV1S4dVuEm55nFpVV3KbRdAPrHsbWCpB186kEw3AqOO11sXioe17m2xbJCKidFVjJczI0gVXbCskOvivsXBYVUjFQqrkKqtIUKUCq4QQSx0j53lCCCaM5OhqFqsBVpsRFpsRNpsRVrsRNrsRVptJbVf35Vq3z4idW5rw+Xs1+338PsM4v4uI0o+BF2VUVdcO1w7c/c3dWF29Wrd/Zt+Z+MnEm1Bg7qd+oXh31R7c++FGNHrjrzDn28342byRmDu6HAaDAYUOC4pzrDAeSFWXt15beVHaGJODrrVvAksfAVy7Wqjouqz9QZestijVXGxbJCKiAx3ynqoaKzori4jaXF2VHFjFKqtis6okvJLAKpzQDhhq1gjQlSw2LbCy6gKrSFgVuR/bb2++7YB+f5ZV0wvz8fWi2lbbIy02E0Yd2ucg3jsiorZh4EVdKhgKotZbC6fXidc3vo4n1zwJXyi+6mGBtQA/nnwNjuh9MkIhE2oaffjHe+vx2aZq3ePMGFaC6+eNVDO8nv16B0b2ysdZU/q3/4e1WnmxGgjEK8sUub/mdWDZY0B9hX6fwQSMjszoas8weq62SEREya2EsetwPMCSv6IT90WrsjLpr2uiLp5dJYFTckVV6kqrsDbHKqE90O8NZ1R1lbyGGgumEgOppFAqVVgloZZUZ8mLv13BaDRiyuwifP1RbcoAUN636acMgdHMlkYiSj9DmL8ttYnL5UJhYSGcTicKCgrS/XnpUVVdW51bcfeyu7Gudp1u/1H9j8JPD7kJDvRCKBzGovX7VNjl8sRfqc61mvCTuSPgavLh6a92oMEbUKs1Ss5VYLfgmrnDceVRw/f/ZCTQkqAreeVFvxtY9QrwzeNA4z79PqMEXacAh14KFA5s+zsu56lqrgKutkiUJSSI8Kxeg2BtLUzFxbCPGwuDtChTj9asjTAxrErezvCKeqiUrYAJl9jMqoQqq+QwK9NmV5nM8eqqliqoYgFViiDLbOm6wKqjfPtVHZZ/Uaet3BiOvM7rMGPaiYMx5bgDWMSJiOgAMPBqIwZeHVvVFZ3V9crGV/D02qfhD/lj+4tsRbjukOswu++J8PgAZ5Mf9yzcgA/X6QOnaYOKcNPxo7Fo3T488PFmpFrM3GQAfnXimJZDL2lRbKrRWhgT+RqB714Alj+pDaxPDqzGnKYFXQX929e26CgCrHlcbZEoizQuXoyqBx6Eb8sWhP1+GCwWWIcORdlVVyJ3xoyufnqUriqsxLAqOcziUHfqYWFVrM0voeWv1Uu04sqnVVdlEhXMRGZXJbYDJgZYqaqsorOu5Nokv4ASQqEQNm8Owu23IL/EjpGH9mZlFxF1KgZebcTAqwOrutzV2OzcrKq6NtRt0O2fO3AufnbIjbCiHP5gCJ9sqMJd769HbVM8ELObjfjRnOE4bXJf9YrRafd9ikZfqrhLk2czYfn/zIc5sXQ6FIysvOjUr7wowde3zwLLnwG8Tv0DmazAuDOAqRcB+X3b8VtTXmS1RXvbziGibhV2Vdx+O0INjTAVFcFgtSLs8yHodMKYm4O+v/sdQ68MpSqqJKCSGVdyW65bqr5iCyH10Mqq1vdpbYSZVpwo1VGpKqhUIBXZlhhmxYayR87JhuqqTGLMzYWlV6+ufhpE1ENxhhd1alWXzOp6acNLeHbtswiE462JJfYS/Hzqz3FE3/mo94RR1eTFPxduxAdrK3WPM7F/IX55wmj0L3Ig327BR+sqWw27RIM3iNdW7MbZ0wZo4ZYEXXJJ/A1NqrxWPA1897xW3ZXIbAcmnA0cciGQV96+tkW5yG0iyjoShkhll4Rd5t69Y38gGex2GGw2BCor1f6cww5je2MnfC5SVlolV2RFgq1YyEXUjWdWtRxOafvUTKtWQqzWhop3FZMJsFoBi4RWFrmGurZawuq2xRyCLLwt1xZTGFZ1HYLFHNauTSEYw2GEwyHt9zz5epf7MhMvui0YRrghDNTL9wL5GGj7ZbVwj7obOUedL+dpx8ht+U973Mi26HHqAt2x6nGTt0e/76irhP1o+Zj4voRfXeM34h+8xE9nR39/Sw4Am+WBkQ0GCQsTzzHAYLWo0Kv4nHNg6d+Ozggiog7AwIs6raprY91G3LXsLlXdlejYQcfihqk3whQuhcsdxGcbq/CP9zeoAfVRNrMRVxw5FGdO6Q+7xYSyPJu6Xr6jrk3PYdm2Gpw9Lk8LuqS6K6p+D/DNE8DqV5oPqrfkApO+DxxyPuAobts7a3FE5nPlte14Iuq2ZGaXtDGqyq6kPwbkvqmwUO2X4xwTxnfZ8+yu9O2B8odpYsugBFkJ87EYXlEG0P59SqVgILJapoStkVUzo/+WAwEE1HD1+NB0aetT1VL+MHzSHugD/AHAFzDAn3gJGrVLyIhgOPNeTDMGfTAHPTAH3DAH3TDJtV+7bQ5Etqt92m3Zb4lca8e4YZSwqR2CkYsnbe8VdZS8o45i4EVEnY6BF6W9qqvOW4cX17+I59Y9h2A4HjaVOcpww7QbcFS/+ahtDKDG7cE/P9yE91bv1T3O+H4FuPmE0RhYkouSHCsKHObYH5dNvoTwqgW5cMPeuAtozIlvrNsOLHsUWPsWEEpart2WD0w6D5j8Ay282h95LjKAXo41W/d/PBFlBRlQr2Z2STlCCqq90elUx/V0upbA5MqraCthcpUWQ6weJRoGqXDILwFRIH4/YZskQeGAXzteUqFgC8cH5N9c9HbifXk7fi2MityPBVJyfvQYeVvqWAlWtfAqGAgiAAsCYTP8MCMAK/wGCwIGKwJGGwImG4ImBwJmOwJmBwImB4Kx23JtV/vDqvI7s34FNwa9kUDKowIpFUIF2hNWeWBM+B2PiIgoE2TWT1vKuqquDbUbVFXXFtcW3f7jBx+PG6fdBFO4CNUNfnyxqRp/f389qhviVV0WkwFXHDEUZ00doNoXS/OssJj0q55N7FeAV77ZlfI5OOBBCephNQQwtleptrFqA7D0EWDje1rJeqKcUq2aS9oXZe7W/ki4FV1tkbMeiHocWY1RBtTLzC5pY0ymtlss6rhsEhvInlx5lRhcye3EAIu6nBYQ+WMXLVjyN7/EtmvXCKTYHkjYl3S8FjolHKcufi2oij5O9JyEgEr9u0nX+w4DgiabFkS1EEJp15Htar8dQXvkdmR/yGRDJjLFgioJoDwtBlWxECshuJJjTMH2V1alnfxeJRejUWsJT3Fbtc/Jdey2nJNwrLw2KuGium+Q5rrYsfH7kXPkWrZErrXj1BPRP67aHXlsuRN5rNhtdRU515C0PXo7cmry/ugO9aJuUltg/HZiO2GkjT7xd9DEYuOO+t00+cWH5LuJ7ZXJ7ZnyNExmGBx2mEtKOub5EBG1AwMv6vCqrlpvLWo9tXh+3fN4Yf0Luqquckc5bjr0JszpfyxqGgPY2+DGfR81r+oa2zcfNx8/BsPK81CSZ0WeLfU/1WlDStRKjMGEH742+FTQZTdo4Zn8HnKobTvw5t+ArZ80f5D8PsCUi4Fxp2nzulrDIfREFGEfN1atxuhdv14bOuP1qlDBIENobDY1uN42apQ6LtM0m2UVDa/kj15dBZYclxRsUbs+zirY8fn2cy0BUaptCfej+xP3SegU25awP8UlnYFSusiPdgmqtCAqIayKBFOJt+U6McjS9kWDLJsWWGQYQzioBU8hL8whH8xh7doi9WNh7doEv9SUwWKIXIwBmI1BmA1BWE1BWIwhGGwmGHJMagCWCoDke5AEPnLfZAGMNm1fwvZYeBQ9xyj7tetm+0yR+7rjo9uixxv02+ScaDCltstxicckBVbJwRZfSMwaHFpPRF2JgRd1bFWXpxrratapFRi3urbq9p809CTcOO1GmFGEvS5fylldUtV1yawh+P6hA1GUY0VprhVG9cpZajLEfly/Any3yyWNBShCA3IN0UkOYRxuXIvL7R9hyEcbm59cNBiYdgkw6kRAfiFsjdmmtTpKNZf8QkZEPZ78cVZ21ZXY9cub4du4MTK0OByrTJDKLtmv/ohLg1g7YORa1yKYdF/bpoVXPW1geyx0kkBSQqHEi9eHkE+2S2AUvZbgSH9cSLc94bY3ckw0cEpxuyfSKqqszYMqXRgVra5KtT0eZmViUCXMJm2IutVigMUKWG0GWK3R1f9MkRUCTbA6zLDaTbDYTbDJtTWygqDNCJOZqwESERGlEwMv6rhZXZ46PLvuWby44UW10k1UL0cv/PKwX2JO/3mobvRjl6sR9324Ee+vqWxW1fWL40djZK98lOdrQ+n3R8KwW44fgTte+BS+Rqd6OdiAII42rsB5poUYbqzQppkmKhsNHHopMOyY1ldQlD9SrfmAvUALvIiIUmipEKG1AoVmYVU0kAonbI9UYcVuJ1RcdefQSrXWRcMmryd2O+z1IBS9LeGTBFTRQCq6LTGkioZNkSArFlylCLaoBVJJY7HELpLgBG25CFhzEbTkImjNQVC1+Dni4ZXMqlIXKwIyv0q93CTXJm22VciMQFhCqg5qp+pgJpMBFls8dLJYjfHbcq3uG3T7tBArerwEXMZWX4wjIiKizMDAizpkVtfamrW4+5u7sc21Tbf/5GEnq6ouq6EIFU4vPlm/D/94fz1qm/y6qq7LZg/FOYcORFmeFYUOS9tK2WWgrLsWM0ub8L8nDsHzizdh2L4PcFr4A/RBdfPj+00Fpl0KDJrZ+l+i1hytmkvmeLGknoiS2wAj16FQCPsW3I+QPwDrsGFa0BIIaG1ENhsCVVXY98/7YBk8GAapMsrwsErNVfJ6EZL3wyOBkke7HbnEtvu8+u1qWySwioRUWugUPS4hlPJ41MymHs1s1gImqzV+bY2HTgaLNb5P7U/cF9kf22cFzBaEzFYEjZHh6SqEssAfluHq0RBKW9nPHzBGVvxTnZDw+2RVQFkpUFYGDCEQCLej1xCdSl6fUhVUyQGVTQugEgOsWEAl4VTstrZdAi8iIiLqGRh40UFXdT2z7hm8tOElXVVX75zeuPmwmzGn/1xV1bWzrgH3frgRC9fqq7rG9S3AL48fjdF981Gaa4PV3IbWBfmD0V0HeOq0Pzw9LkytfBlT6p+FIVzT/PghR2mti30nt/yY0tIYbVk08cuCKBvoqqcSwqr9VlLFzlEPEt+XxLthI3ybN8OUm6uqr1SQEz0uFFLbfdu2wfPdSthGjji490WedyRwUlVRcu3xRLZ54tsi1yp8SrrW9nsTzokGVtrjqPehh5BAUgVGiddWC4xW7VqFSrZo8BS5rQKmyH6rFUZrwv7INt0xsTArIdiSsEvmJ6l/IlrI5POGVPDk94Yi4ZN2P35bOyYxmPI1RbbLeT7597m/91gO6JoZXrGgKqFCSrX6JQRVukqqaEil26a1/xERERG1B/+yp3Zr9Deixl2jqrpkBcbt9dt1+08ddipumHYDbMZC7Kr14MN1lbjng42oc8eruiTYunz2EFXVJe2LsgrjfskfkhJyuWu1P0rr9wDLnwZWvwz43frmCYMJGHU8MPVioHRE6y2LEnRZ9jOsnoi6LqRq7bYuvEoKstJMhtJHV5yTai7VOhed4WU2w5Sfr0KlpmXLENhXqQVPsXBKu8i2kNudEEjFt2vXkWMlkMpWZjOMKnCSYEm7VvdV4JRwrbYnbbNFg6ekbbHAKrpdu0igBYv5gAdiy789XTilLknBVVP0dvTaB5/Xqx0bOy+MYFurqbqI2awFT9GZU83DqYSZVbHrpIoqBlVEPVs4DM+69fCuW6/mWsoiLumaa0lElIohnIl9FRnI5XKhsLAQTqcTBQUF6KlVXTKU3ulxpqzq6pPTR6vqGjAXtY1+bKtuxN0fbMSnG6t0jzOhX4Ga1TW2X4Gq6jLtbw6GquKKBF3yR2z1RmDZ48CGd7Rqr0RGC1A+GhhxHDD5B4AxKdOVP3Is0ZbFXLYsEnWAWAAls5kyPKRSz1feljcSNEmoJNduCZfcWsAUua1dS+DkRsgdDaIi1243gi4XAnv2NF+yPRvI6mkqdLJFwqeWbkuwZNfCJLs9cj+yT8Ime+RYFWZF9qvgKn47WvGULs1DKi2gil583uT7+m2xIEta/vyZ/blWP+JU0GRICqZM8YAqOZxKqr6KnscZVUR0MNwrVqD2uedVpbNUQMv3e1m5WBZzyZ0xgx9cIuoUDLzaqKcHXtGqrjU1a1RV1476Hbr9pw0/TVV1OUyF2Ofy4u2VFbjvo02o98RntdjNRlx2xFC1AmPvAjscVlMbgi6nFnTJvK7dy4BljwHbPmt+rNmu/dEcjFaRGQBbnjaza+pFCass5rc+qJ6oh1FBlLSyJQxQbzZrKtWA9eQwK43PLxZKyXWThFJuhCPXWhDlVtujwVR8f8J5SdfdnQqKVMBkiwRNcm1LsU271m2T+4m3I+FT4rHq8btwhmG03S8aVCW276UKqqIVVaoVMOF2dwipkqup9JVU+vupqq0St5ktXPWPiDIj7Kq8804Ena747Eup7DIaYS4pRr8//5mhFxF1CgZebdRTA6/Eqq6n1z6Nlze8jFDCHJBCayFun3k75g48BrVNAWysrMc/3luPr7bW6h7nkIFFWlVX3wIU5+xnKL38UPS6gKYaIOAFNi0EvnkSqFzV/Nj8vkDZKGDLJ9p8ElXRJaXS8od4QGttPOY24MgbOvTjQpQJUq/0lxRKqZX9kmdTpS+sUtVTkWBJhUtNTbHbsVBKwihVVRXd54ncjlZbJdyWcKobVlBpz1jCB8BUUqLaGyVkMjrs2rWETPbI7UhQpQKnhH2x7dGAKnpMJ1RFtZf8O5IWPX31VKTVLym4St7WvOoq89v9hFFW+4uGTTaTFkLJxR6tmIpss5vi9yPHWmP3tW3yWLGfi4k/H5O3pfjZqft52tq5Kc83pN6VfFx777f0XJsf1fL5bdnXnmO62v6+j7W0v4Xt4bYem7gteX8r9/W7WjkveruFt5PyZ0xr50eudeepmy2c0+y8hJNSPXaqx6eOFQ5jx3U/g3/jxtT7DQbYxo3D0BeeZ3sjEaUdZ3jRfldgbKmqSzh9Ttz++W+xcsRWGF1z8O+PN6PJF28zzLGacPWcYThr6gD0yre3PpReBV31gLsGcLuANa8DK54GXLuaHysh15SLgOFzgUdOjIRdVkC1R8rFCBjMQNALfH4PMOs6DqOn7hdYRa+DEljpWwbDHThgXB5LF07JtYRSTU1a+BTbLmFUZL8Kqpq0SqpYoNXU/QIqad1TQZRDC6Ns2m2DQ8IlRzx4cuRErh1qu9z27tyFHW9+im0DjoPHIi3SFgRNNoSMZgRMNlhCPoze9DzG/uw85E4/FJlm/1VUybeDSe1/Cef72zI4vetJsKQFUNrF6jBr9+3atdVuhsUe3W5W14nHqW2RY0xtWWSFKE26QcSX8WKhVysBmlroJH5C68clBnPRY3T79x/aqWOSQ7ukY5uFegmXTAjyvBs2wL91a+pQOvI8vevXw7NqFRwTJ3bJcySinoOBF7W6AmOqqq5kzkYL/vmOB8GmDbrthw0twS/mj8KYvgWtD6VPDLpce4BvnwNWvqhVeSUbcBgw5UJg0Ezth+fatwBvg1bZFR2CGXtVO/JP3OPSHk9mehF1MBVKRYOoZq2BIX2Flbqd1C54kKv2aeFUE0KNEj5JEBUPq6IX3XZ3wrZoqNVdBqIbjVoYFQuo4rcNOZEgSlVQOWCM3I/tlwBLhVX2WGgl2w6mdS/0/kJsGnIanAVDEJLA3ZAQgIRDCIV82NZ/HkZJS0cHBVTSnpdYFRXwx4OqQCR40oKoyCp/CSv9Jc+u6g5VVMJkiVZJ6YOpxKCqWWAVu064bTOrxyIiEimrKrMgXIz9zpEYgiUHY/K7SEuBWcptkVWLddsTtiX8TuNZs0ZbtTgV+VjLcX4/3MuXM/AiorRj4EXtruqKCoeN8NfMhnfffCAcD7TybGb8dO5wVdVVmtfKUPrEoKtyHbDiKWDtf4BQfDVHRdoSR84HplwAlI+JbzdZAK9T+4Et1VzyK0lYgoTIKmlyHuQSBJwtvx9Euta/pAArXmUVbBZaHUiVVYtBVVNjwrZGfWil7kePi1/Uc81kJpMWMuVIuJQTr6KK3pZQSoVRcj8aTGnbYrcj4ZXc7uq5UsmcxmK48i0ImVKs8mowqu3O/EHYCwPCjSZ96BQJrtSMqWiLn1cqqCKBlCcY2R+59gZVuNUtqMHppljgZLFpgZPNEQ+ekkMpOUaFUwkVVeraboLRxJCKiKjN34KTVkHszJ+a8jtOyOtr07H+PXvT/nyIiBh4Uayqq9Zbi1p3bcoVGAtthXCqcClyvKcPPBVnI+QZqPsIju4fwJ/PmIUxfQpgt5haD7qaqoGtnwHLnwK2f978OFlNcfyZwOTztFldQn6IWyPD5y12rbVRqiokJJOwK5kKvQxAof55UnbSVVRFWwGj1VSxfdGwKqlNsI2PHw2gVPjUGAmqJJBSt1Pcj4RVibdVJViGUoPMVcVUNJTKiVdQxYKqnMj96CVyP3pO5L7Bsp95fel6H1TgbdC+X6i3r83SSt6m/hfZJv8C5NOiKqjkEq2gkja+xMooCaIiVVNVFWUImv2tPRGELLlYtBTA0hZmmWQIqXyKBkzJlVSJIVUsqIoeG62wSgipJOwy7G/1XSIiyjryM9XSr1/yxvjthN+3zH36dOIzI6KeioEXaVVdnmqsq1mnqrq2ubY1W4FR/hp8ffPrCAdN8FYfA3/10ZHqKY3BVA9bn9cwffIYHDLw9NaDrvo9wJo3geVPAjWbmh+XW66FXOPP0oIt+UFpzY2EXDn6H5wTvge8cT0QcLfwNoOA2aEdR90+uNJXWSUHWMH9V1bJnCkVRiVfmhBsbNBCqsTtKryKb5PzM7aSSsKmhEsssMrNjQdX0aAqelw0vErcn+Zh6OqV52jwJEGUBCMt3Zf5WknhVTSgkk+7BFOBxJY9aedT1VCBSIWUXALwRW97tO0+uVbVVNHt2rXW4tHed8jQxbOoIkFUJICKBlQqiFLBVbx6qqXgSs2jYhUVERF1AMfkybL0bLytMdWLimazdhwRUZox8OrBEqu6nl33LF7c8KKuqqtPTh/cfNjNOGbQMfjlolsQbBoEd8XZCPt66x7HXLAUtt5vwmhyI2QY2vKqizWbgRXPAd89r1V3JSsdCRxyPjDqBMBsBSwOLeSy5nWPFZiynARMntVrEKytham4GPZxY/e7uk5sNUBdQNVSmJVQjZXilyM1WF2CpwYJnxq066ZGBKP3VTAV364dlxheZWALoMykkkBKLomBVeS+Cq1y9UGWLtSKBFod3eqnD5mMWggVDaokdIosLd7sduJ5RqPWRiHVUxJOBSLhVDR8cicEUSp08iXcTg6ktPtqW+ScYHdp74swmQ2xyilddVR0pb6k9r74MQmBldrGgelERJS5HBPGwzZ6NLyrV6cOu2SVxtGj1XFEROnGwKuH2l9V16nDTsVN029CnrkIm/Y1YsO6KWjaNltb/TDCYK6Frc/LMOdpw+rlR9rIgtHxB5Efch4nsHOJ1rYoA+Zl1cRkg2cDh1wADJgOWHMAW54WchnbUGkiw+iDvsjzSvUHsFHbz6H1B6Vx8WLs+/cD8G3ZgrDfr1rVrIMHo/Tii5AzdWrqMCt6nRhYSVtffUMsmFJVVQ2RkEqFVvFASwuy5L62Xa3+l0HUMPTEoCp6WwKq2HYJs6LBVW7sfiywstk6JKiKh06J4VTCbQmfEtv5kvfJg0T2hWBAMDJPKnbRhVOJ1VMJ2xLCqMSASs2e8nWvcEqYrVqLX0uVUrJv7xaXuigt/FIvJs8bgCPOGdXJ7wEREVHnk99Jev/iJuz65c0I1tTEB91HXhQzlZSo/ft70ZSIqCMw8OqpVV2eWjy/7nk8v/55XVVX75ze+NVhv1JVXfXuIF75Zif+9u56VNbnJTxKCJbiL2At+y8MpsTBlEZ8va4Al04MaYPo1/0H+OYpYMfi5k/EZAPGnAxM/iFQPkqr5LIVSBlE+94hNYw+DJis0fcw/kM12nIZ8nFofQopw6lAoNn2pqVLUfmPfyDQ0Kja3WS7QVbXWbkSu2+7DfnzjoW5pFiFUkEVZkVCqkhQFYzczqR2QDWnKrGySq7zooFUNJRKuB0Nq+SYaPXVQbT+NQuoElv2UlZPpQ6vpChODTxPaMuLhk/NLokhlS915VSgOw1GT2rvU+FUs4Cq5W2J7XzJ981WE4xtmEG1d7MTL965VEv7WwouDcDIafqqWCIiomyWO2MG+v/lz9qLpRs2IOzzqWp068iRKP/RVWo/EVFnYODVA6u6NtRsUFVdW1xbdPtPGXYKbpx2IwptJVhbUY+/v7ce76+p1B1jsFbC3uclmHL0FWEi5OkDo9sAfHInsPxpoHZr8yfhKAEmngNM+j5QNEgLuWT4/IFSw+jlD80gYDQ3/ycdCvSYofVqhlULVVZhmaMQuVYBVJ0Twfr6SKWVVFM1aJVXDQn3ZV99A3xbt6pVBRPrV6K3JdByvvBC576jBkM8rFJBVV4klMpL2KYFVKa8pKAqcjHIbIl2vckUlVKtBlTxWVOqpS8UaemTmVNqdb6kAEqFVAktfanCKp/+HBmmHgoewMypLiYflnjwlNC6l9jaF7utDUCPtv8lBlSxkMpqUgPXu0KvIQUoLHfAWdlymCv75TgiIqKeREKtnMMOa/c4DCKijmQIt3V5sh7O5XKhsLAQTqcTBQUF3bKqq85Th2fWPoMX1r+AcEJ80cvRS83qOnbwsXC5A3h+yU7c9+FGON3x1cdU7YK5BvZBD8JkccJgSGhVCxvRx2vBxXsLcY5hDeyhxhbmc/1QW3Uxt6zj5nIFA8BfRwDuOq1qLLEqQwZQSwulowi4aWP7q8e6WCywSgiyJLBSrYESSrlcWnDlrFO3VYAVCa3U7YakACty6fI5VtHASgVV0TBKu22K3c6BMS8/HlTl5cGkwqo8GBz2Nv+y1GKFVAtVVNFZUzIMXbX1RVbs0wajx4OmFoOp6DFJrX7BQPf8NisLoEZDKW3geUIwFbvEh6bHj9NXSyW2AZrMkdAwS+xcW4P/PrgSnsbIcN4E9lwzjr9yAgaMKemS50ZERERE1JN1rwSADriqa2PtRvzqk1+hKdDU7JhhBcMwd+CxWL6jDn/6z1p8tbVWt39s33zcMG8UrnxyKY6tHID60r3YajHCbwCmery4yNWAqV6v1kCo+7veAAw9CphyATDsGC146ujQSR7viBuAD34XmQ8mjy/PREKigDYHTPZnSNgVTgiwQl4vQk4nAk4nQjW12rWEV04Xgs5IBVaDhFj1ugos2R5b+aaLBI0WBMw58JtzYA40oWBIb1gH9I+EVHkqqDLFAq34tWyTQeutBVb7m0cVNhgiFVPaKn3BSCgVC6ZkpT6fVFKFtHBKhqH7EoIoFV5JEBVqFk5112BKSJVT8zAqoVIqGkIlXmROVarKqcixRnOkUo1aJGGWhFpL39mGfTvqEQqE1cetfGA+pp0wmGEXEREREVEXYYVXllZ4yVyuGk8NnB6nWn3xqTVP6aq6onV98qdsGAb08Z+PndsnwZMwv8duMeKKI4fh0llDUJJrxfP3/BLfq3lQnbHHbEJhKIS8VAWClhxg7GnA1IuAPhO1+Vzp/qP5s3sQ/uTv8FQ0Ieg1wGQLw943B4YjbwBmX5e2NysFkjJMPVBbqwZzBuvqEKyp1aquVPWVE0GXBFkurQpLBVgSXkk7YYpKuE4kwZMpPy8hpNICqWhgJddNy7/F4rpx8NsKEDA7VMAl12GjJf44QR/OGPwNyi65UBdMhUIGBIJhlc0FIu18WlufNndKBVQqnApp++RatvsSQqiEaqpoWCXXIane66bk4yOVUuaUoVQ0iNKuzQntflIpFW//M+uGqktwZTSxRaArhUNhFXh5Gvyw51lU4KW+FoiIiIiIqEtkRtkLHRT543/VbhdqmnzIsYbw+s77sNW1BbmWXFQ0VmCzc7Pu+MSMKuDpC++es7DRIzOu4mHXYUOK8auTxmBCvyJYzUZISnGu81GEDbLSCtBfqpRSmfUzYNrFQOEAwGzrtM9so+kwVK49Gr6VKxD2B2CwmGGdMBm9jj4MuW18DFl9UEKpgARX0QCrtg4BCbHqalWAJRVZKriSaiypvKqv79Jh7GqlQAmq8iOhVX5+Qmgl9yXEylfX+kArD0aLRa3IFwukVMWUFMaF4Y1sW7ejN6otZa0+h7DJiv9WToXt4V2xkCrgDSEY6H7DzxNJWJFcBaWFTNFKqea31f5oNVXisZFZVKyayl7y76XX4Mx/MYSIiIiIqKdg4NXNfb6xCv9ctAbrmt6Az/EpYK6P70wqLtBVdYUs8FXNg6/myPhqhgAK7GZce8xIfH/6ABQ6rFKKA6xbCCy8Q612aEjxuO4asyrgshcHgLIRQOlwdKbGxYux8/qfw1fXgA0jzoLbXobcxt0YvvhNeFdei7KrroK5d28Ea2tU9ZVUY2mtg06tCktmYcmlqyqujMZ4aBUJrkwFcp0Pg7QA5uYjlFuAsCMfYXseQvZcBO15CJvtCMCsqqbcseqpSNVUpL1P3XeF4K9KqJ7yNcLvc7UxlCpv9vlOpclvRdOe5u2ynUFmQpklcIoEStGLOUW1VOy+ff/b2c5HRERERETUfbGlsRu3NErYdd17t8Hr+Fy1GUqeZQiHYQsAXquhWdglWwzhEEp3jkRF41nwoFR3TEnpFjx74UUYVp4Hs6daW2nxmyeBqvXN3nYgaMYW5xRs3T0GfVZ+BJvdh/4za5F79InAuY932Puoqq5UMCVVVnVa62DkdrR90PXOO6g3FKr3z+JvgNnfBGNCtVq6hQxGBE02hKwOhAtKEM4rAvIKEcopQEhCKkcuQrY8hGw5CFnsCFkc6niZgxU0WBAIGbV5U4ktfiqckm0yrB7dngSiicGSOSlgUoFVpCoqti8aXCUFUfH2Pq2lj618RERERERElIwVXt24jfHXH/8WHsdnMHuAixafiY399uCLEV/Ca40nJHlNQIkrhO29jRi1xYHg7lOxvHSq7rFshmoYB7yCsrwBGBXaDLz1MLDqVcDravZ2G32FWOE5DaubjoU3nA/JzNbPOQMlVd/BvPyfGHqUTARr4Tn7fKpFULUHRq8ltEoIsWKBVmQOlsy7aos8pG4rlBAwbDAhaLIiZLSqay1ssiKkblsRNNoQNFkQimwPWuxaJZUtV4VYcgma7drxsh9mBGBCUM2oMrS+6KF0fsq70OzdkJO8kUuGhVIJgZSvoQn1dS20ryYYPdGOEXNG686VEEuGn8t1tq3MR0RERERERJmNgVc39c2OKlQbPsY1Cy9HTXFvvD3tGewp2KI7ZlTldMzecgbK923Gm32X4jvbOWgqjbcvyjiu6V4TTthdjb22b3G6qxJ44InkpRaVoGMAPltzBFbazkYYJhhCQVgDLlVRJRdTOIiN9mNg/7QJpq1/1IIrCbXUtXYJNTVveZN4TBdGxYIoB4KWQoTKIyGTLrDS35bAKh5eRc5P2A9D/H0+YJJPZUClldFk0A0wjw0ujwRN6n5iBVVCNVT0nJYqrFKFUkFfAA9cuxChhLbXZs8JQcy96nCYrPx2QkRERERERJmhR7U0/utf/8Kdd96JiooKjB8/HnfddReOPFJmWHW/lsab3rsTw5+wIOSYiDfG/Qu7iuJthw5fPuZsOhdDaiegxhjCezkBbLfo05o+AQPOcHtwpO1DjHf8F/mhSgS8RgS9JgTVtRG+gB3+3DHwWQahYUctqpxWGEN+GMJBGBGOB1EJAVOz4MoolVPRqqrI7WhFVeSYbKKqpGKBkjEpnNK2xSqgkoIobX+kvS85tIqEWaYuWInvy4c+wZKvpRItVYVWGIdOt+HwK9r2dURERERERETUGXpM4PXcc8/hwgsvVKHX7Nmz8e9//xsPPfQQVq9ejUGDBnW7wOvEB36AU5ZeoUIIp20fPhj1BAwwYlDNeEzaPQtWXxh7DR7UwoucYACOUAD2YAiOcAjFcht+hFVtjhUBQ6RCKhZIaUFUyGhBtlEr70VCp1jAlBBMJVdIJe5LPi9aYZVYbZWtrXsSeq34ugF+WKLLHsACPyZPz2PYRURERERERBmnxwRehx9+OKZOnYoFCxbEto0dOxZnnHEG/vjHPzY73uv1qkti4DVw4MCMCbwevPgO+Oyz1e28+u1oKBiMbGCyRAOnyHVSCGW2JFZIGbFtyU5UVQb2+7jDR9tw3HWzVCBFB0baG1e/vhz1lQ3I75WHcacdwjZGIiIiIiIiykg9YuiOz+fD0qVL8atf/Uq3ff78+fj8c1nhsDkJwX73u98hU9lDZfBFbpuCnTT4PBxSqzxqDAirSiZ9NVNBXghDDh8cr4iKtu4l3o608ZktScGWxagqsNpj2nED8cDPP9nvcfOuPoxh10GSGV0Tv3fowT4MERERERERUdr1iMCrqqoKwWAQvXv31m2X+3v27El5zq9//WvccMMNzSq8MkWxpRHRNRSl9dAUcMMU8sMY9MEU8mnXYR/MYS/M8MFs9MJs8MFi9MBi9CJosMAV7oO6cD8EjA41Y6tXkQczrz4m1qYXq6yymhDy+vDQLz5TKx62LIxzf3ckrHn2TvooAJYcKwYMNGHnjpZXEpT9chwRERERERER9Qw9IvCKSp6tJN2cLc1bstls6pKpjv3zpXj4tuWqwuqt4lw4nMtgtftxZP5KzHcsQbndCWNSNuUL2bHOPQcrm05ATSC5BTKMU24/GraCnNRv0OZA3yI3Kupa2A+o/Z0ZdkWdftvReO33H6UMvSTskv1ERERERERE1HP0iMCrrKwMJpOpWTVXZWVls6qv7sJeXoZenjWotI/DkfZtmFj6LuaYvk15bJV/CNbUH401vuPgDztSHtPbWtNy2BVx1p9Pxcs3v4GKOnmMxKAwrMIu2d9VJNTyN/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=", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "sns.lmplot(data=scaling_df, x=\"size\", y=\"time\", hue=\"algorithm\", order=2, height=10)" ] }, { "cell_type": "markdown", "id": "ec10f642-5eae-49c9-ab87-eb7da3f5ef4d", "metadata": {}, "source": [ "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", "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." ] }, { "cell_type": "code", "execution_count": 29, "id": "c0e7c2ab-e1d2-478f-a9a9-01a762d8da3b", "metadata": { "execution": { "iopub.execute_input": "2026-03-26T09:38:21.089635Z", "iopub.status.busy": "2026-03-26T09:38:21.089471Z", "iopub.status.idle": "2026-03-26T09:38:21.375853Z", "shell.execute_reply": "2026-03-26T09:38:21.375331Z", "shell.execute_reply.started": "2026-03-26T09:38:21.089621Z" } }, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 29, "metadata": {}, "output_type": "execute_result" }, { "data": { "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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", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "sns.catplot(data=scaling_df, x=\"algorithm\", y=\"ari\", hue=\"algorithm\", kind=\"swarm\", height=10)" ] }, { "cell_type": "markdown", "id": "7d27c69d-22bd-4b3c-9e58-defc84d1bdc0", "metadata": {}, "source": [ "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." ] } ], "metadata": { "kernelspec": { "display_name": "evoc_docs", "language": "python", "name": "evoc_docs" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.13" } }, "nbformat": 4, "nbformat_minor": 5 } ================================================ FILE: doc/source/changelog.rst ================================================ Changelog ========= This document records all notable changes to EVoC. Version 0.1.0 (TBD) ------------------- Initial release of EVoC. **Features:** * Core clustering algorithm with hierarchical multi-layer support * Scikit-learn compatible API * Support for multiple embedding types (float, int8, uint8) * Optimized distance metrics (cosine, quantized cosine, bitwise Jaccard) * Numba-accelerated performance * Comprehensive parameter set for fine-tuning * Built-in duplicate detection * Extensive documentation and examples **API Reference:** * ``EVoC`` - Main clustering class * ``evoc_clusters`` - Functional interface * ``build_cluster_layers`` - Multi-layer clustering construction **Performance:** * Efficient processing of high-dimensional embeddings * Memory-optimized algorithms * Multi-threaded computation support **Documentation:** * Complete API documentation with numpydoc formatting * Interactive Jupyter notebook examples * Comprehensive user guide and tutorials * ReadTheDocs integration ================================================ FILE: doc/source/conf.py ================================================ # Configuration file for the Sphinx documentation builder. # # For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information import os import sys sys.path.insert(0, os.path.abspath("../..")) project = "EVoC" copyright = "2024, Tutte Institute" author = "Tutte Institute" release = "0.1.0" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = [ "sphinx.ext.autodoc", "sphinx.ext.autosummary", "sphinx.ext.viewcode", "sphinx.ext.napoleon", "sphinx.ext.intersphinx", "sphinx.ext.mathjax", "sphinx.ext.githubpages", "numpydoc", "nbsphinx", "IPython.sphinxext.ipython_console_highlighting", ] templates_path = ["_templates"] exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "**.ipynb_checkpoints"] # -- Options for HTML output ------------------------------------------------ # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output html_theme = "sphinx_rtd_theme" html_static_path = ["_static"] html_css_files = ["custom.css"] # Theme options html_theme_options = { "canonical_url": "", "analytics_id": "", "logo_only": False, "display_version": True, "prev_next_buttons_location": "bottom", "style_external_links": False, "vcs_pageview_mode": "", "style_nav_header_background": "#2980B9", # Toc options "collapse_navigation": True, "sticky_navigation": True, "navigation_depth": 4, "includehidden": True, "titles_only": False, } # -- Extension configuration ------------------------------------------------- # Autodoc configuration autodoc_default_options = { "members": True, "member-order": "bysource", "special-members": "__init__", "undoc-members": True, "exclude-members": "__weakref__", } # Autosummary configuration autosummary_generate = True autosummary_imported_members = True # Napoleon configuration (for Google and NumPy style docstrings) napoleon_google_docstring = True napoleon_numpy_docstring = True napoleon_include_init_with_doc = False napoleon_include_private_with_doc = False napoleon_include_special_with_doc = True napoleon_use_admonition_for_examples = False napoleon_use_admonition_for_notes = False napoleon_use_admonition_for_references = False napoleon_use_ivar = False napoleon_use_param = True napoleon_use_rtype = True napoleon_preprocess_types = False napoleon_type_aliases = None napoleon_attr_annotations = True # Numpydoc configuration numpydoc_show_class_members = False numpydoc_show_inherited_class_members = False numpydoc_class_members_toctree = False numpydoc_use_plots = True numpydoc_validation_checks = { "all", "GL01", "GL02", "GL03", "GL05", "GL06", "GL07", "GL09", "GL10", } # NBSphinx configuration nbsphinx_execute = "never" # Don't execute notebooks during build nbsphinx_allow_errors = True nbsphinx_timeout = 60 nbsphinx_codecell_lexer = "ipython3" # Intersphinx configuration intersphinx_mapping = { "python": ("https://docs.python.org/3/", None), "numpy": ("https://numpy.org/doc/stable/", None), "scipy": ("https://docs.scipy.org/doc/scipy/", None), "matplotlib": ("https://matplotlib.org/stable/", None), "sklearn": ("https://scikit-learn.org/stable/", None), "pandas": ("https://pandas.pydata.org/pandas-docs/stable/", None), } # Math configuration mathjax_path = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" # Source file suffixes source_suffix = ".rst" # Master document master_doc = "index" # Language for content autogenerated by Sphinx language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for LaTeX output ----------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, "EVoC.tex", "EVoC Documentation", "Tutte Institute", "manual"), ] # -- Options for manual page output ------------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [(master_doc, "evoc", "EVoC Documentation", [author], 1)] # -- Options for Texinfo output ---------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( master_doc, "EVoC", "EVoC Documentation", author, "EVoC", "Embedding Vector Oriented Clustering", "Miscellaneous", ), ] # -- Options for Epub output ------------------------------------------------- # Bibliographic Dublin Core info. epub_title = project epub_author = author epub_publisher = author epub_copyright = copyright # The unique identifier of the text. This can be a ISBN number # or the project homepage. # # epub_identifier = '' # A unique identification for the text. # # epub_uid = '' # A list of files that should not be packed into the epub file. epub_exclude_files = ["search.html"] ================================================ FILE: doc/source/examples.rst ================================================ Examples ======== Collection of practical examples demonstrating EVoC usage in different scenarios. Basic Examples -------------- Simple Clustering ~~~~~~~~~~~~~~~~~ .. code-block:: python from evoc import EVoC import numpy as np # Simple example with random data X = np.random.rand(500, 128) clusterer = EVoC() labels = clusterer.fit_predict(X) print(f"Found {len(np.unique(labels[labels >= 0]))} clusters") Specify Number of Clusters ~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python # When you know the desired number of clusters clusterer = EVoC(approx_n_clusters=5) labels = clusterer.fit_predict(X) Working with Real Embeddings ----------------------------- CLIP Embeddings ~~~~~~~~~~~~~~~ .. code-block:: python import torch import clip from evoc import EVoC # Load CLIP model device = "cuda" if torch.cuda.is_available() else "cpu" model, preprocess = clip.load("ViT-B/32", device=device) # Generate embeddings for images # (assuming you have a list of PIL images) embeddings = [] with torch.no_grad(): for image in images: image_input = preprocess(image).unsqueeze(0).to(device) embedding = model.encode_image(image_input) embeddings.append(embedding.cpu().numpy()) X = np.vstack(embeddings) # Cluster the embeddings clusterer = EVoC( n_neighbors=20, noise_level=0.6, base_min_cluster_size=3 ) labels = clusterer.fit_predict(X) Sentence Embeddings ~~~~~~~~~~~~~~~~~~~ .. code-block:: python from sentence_transformers import SentenceTransformer from evoc import EVoC # Load sentence transformer model model = SentenceTransformer('all-MiniLM-L6-v2') # Your text data texts = [ "The cat sat on the mat", "Dogs are great pets", "Machine learning is fascinating", # ... more texts ] # Generate embeddings embeddings = model.encode(texts) # Cluster similar texts clusterer = EVoC( n_neighbors=15, noise_level=0.4, base_min_cluster_size=2 ) labels = clusterer.fit_predict(embeddings) # Group texts by cluster clusters = {} for i, label in enumerate(labels): if label >= 0: # Ignore noise points if label not in clusters: clusters[label] = [] clusters[label].append(texts[i]) Advanced Usage -------------- Hierarchical Analysis ~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python # Get multiple clustering granularities clusterer = EVoC(max_layers=5) clusterer.fit(X) # Analyze each layer for i, layer in enumerate(clusterer.cluster_layers_): n_clusters = len(np.unique(layer[layer >= 0])) persistence = clusterer.persistence_scores_[i] print(f"Layer {i}: {n_clusters} clusters, " f"persistence: {persistence:.3f}") # Access the hierarchical structure tree = clusterer.cluster_tree_ print(f"Hierarchical structure: {tree}") Parameter Optimization ~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python from sklearn.metrics import silhouette_score # Grid search over parameters best_score = -1 best_params = None for n_neighbors in [10, 15, 20]: for noise_level in [0.3, 0.5, 0.7]: clusterer = EVoC( n_neighbors=n_neighbors, noise_level=noise_level, random_state=42 ) labels = clusterer.fit_predict(X) if len(np.unique(labels[labels >= 0])) > 1: score = silhouette_score(X, labels) if score > best_score: best_score = score best_params = { 'n_neighbors': n_neighbors, 'noise_level': noise_level } print(f"Best parameters: {best_params}") print(f"Best silhouette score: {best_score:.3f}") Memory-Efficient Processing ~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python # For large datasets, use smaller parameters clusterer = EVoC( n_neighbors=10, # Reduce graph density node_embedding_dim=8, # Lower embedding dimension n_epochs=30, # Fewer training epochs max_layers=3 # Limit hierarchy depth ) # Process in chunks if needed chunk_size = 10000 all_labels = [] for i in range(0, len(X), chunk_size): chunk = X[i:i+chunk_size] chunk_labels = clusterer.fit_predict(chunk) all_labels.extend(chunk_labels) Specialized Data Types ---------------------- Binary Embeddings ~~~~~~~~~~~~~~~~~~ .. code-block:: python # For binary/hash embeddings binary_embeddings = (embeddings > 0.5).astype(np.uint8) clusterer = EVoC( n_neighbors=25, # More neighbors for binary data neighbor_scale=1.5, # Denser graph noise_level=0.4 # Lower noise threshold ) labels = clusterer.fit_predict(binary_embeddings) Quantized Embeddings ~~~~~~~~~~~~~~~~~~~~ .. code-block:: python # For int8 quantized embeddings quantized_embeddings = (embeddings * 127).clip(-127, 127).astype(np.int8) clusterer = EVoC( n_neighbors=20, base_min_cluster_size=8, noise_level=0.6 ) labels = clusterer.fit_predict(quantized_embeddings) Evaluation and Validation -------------------------- Cluster Quality Assessment ~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python from sklearn.metrics import ( silhouette_score, calinski_harabasz_score, davies_bouldin_score ) # Fit the clusterer labels = clusterer.fit_predict(X) # Calculate quality metrics if len(np.unique(labels[labels >= 0])) > 1: silhouette = silhouette_score(X, labels) calinski_harabasz = calinski_harabasz_score(X, labels) davies_bouldin = davies_bouldin_score(X, labels) print(f"Silhouette Score: {silhouette:.3f}") print(f"Calinski-Harabasz Score: {calinski_harabasz:.3f}") print(f"Davies-Bouldin Score: {davies_bouldin:.3f}") # Analyze membership strengths strengths = clusterer.membership_strengths_ print(f"Average membership strength: {np.mean(strengths):.3f}") print(f"Std of membership strengths: {np.std(strengths):.3f}") Stability Analysis ~~~~~~~~~~~~~~~~~~ .. code-block:: python # Test clustering stability across random seeds stability_scores = [] for seed in range(10): clusterer = EVoC(random_state=seed) labels = clusterer.fit_predict(X) if len(np.unique(labels[labels >= 0])) > 1: score = silhouette_score(X, labels) stability_scores.append(score) print(f"Mean stability: {np.mean(stability_scores):.3f}") print(f"Std stability: {np.std(stability_scores):.3f}") ================================================ FILE: doc/source/index.rst ================================================ .. image:: evoc_logo_horizontal.png :width: 600 :align: center :alt: EVōC Logo EVōC: Embedding Vector Oriented Clustering ========================================== .. image:: https://img.shields.io/badge/python-3.8%2B-blue.svg :target: https://www.python.org/downloads/ :alt: Python Version .. image:: https://img.shields.io/badge/license-BSD-green.svg :target: https://opensource.org/licenses/BSD-3-Clause :alt: License EVōC (pronounced as "evoke") provides Embedding Vector Oriented Clustering. EVōC (Embedding Vector Oriented Clustering) is a powerful clustering algorithm designed specifically for high-dimensional embedding vectors such as CLIP-vectors, sentence-transformers output, and other dense vector representations. The algorithm combines a node embedding approach (related to UMAP) with density-based clustering (related to HDBSCAN), providing improved efficiency and quality for clustering high-dimensional embedding vectors. Key Features ------------ * **Optimized for High-Dimensional Embeddings**: Specifically designed for modern embedding vectors * **Multi-Layer Clustering**: Provides hierarchical clustering with multiple granularity levels * **Performance Optimized**: Uses Numba for high-performance computation * **Flexible Parameters**: Extensive parameter set for fine-tuning clustering behavior * **Scikit-learn Compatible**: Follows scikit-learn API conventions Quick Start ----------- .. code-block:: python from evoc import EVoC import numpy as np # Generate sample data X = np.random.rand(1000, 512) # 1000 samples, 512-dimensional embeddings # Initialize and fit the clusterer clusterer = EVoC() labels = clusterer.fit_predict(X) # Access cluster layers and membership strengths print(f"Number of clusters: {len(np.unique(labels[labels >= 0]))}") print(f"Number of cluster layers: {len(clusterer.cluster_layers_)}") .. toctree:: :maxdepth: 2 :caption: Contents: installation quickstart user_guide benchmarks api/index changelog Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` ================================================ FILE: doc/source/installation.rst ================================================ Installation ============ Requirements ------------ EVoC requires Python 3.8 or later and the following dependencies: * numpy >= 1.21.0 * scipy >= 1.7.0 * scikit-learn >= 1.0.0 * numba >= 0.56.0 Install from PyPI ----------------- .. code-block:: bash pip install evoc Install from Source ------------------- To install the latest development version: .. code-block:: bash git clone https://github.com/TutteInstitute/evoc.git cd evoc pip install -e . Development Installation ------------------------ For development, install with additional dependencies: .. code-block:: bash git clone https://github.com/TutteInstitute/evoc.git cd evoc pip install -e ".[dev,docs,test]" Verify Installation ------------------- To verify that EVoC is installed correctly: .. code-block:: python import evoc print(evoc.__version__) # Run a quick test from evoc import EVoC import numpy as np X = np.random.rand(100, 10) clusterer = EVoC() labels = clusterer.fit_predict(X) print(f"Clustering completed successfully! Found {len(np.unique(labels[labels >= 0]))} clusters.") Note that on first import and first run there will be time spent on Numba's JIT compilation, which may take a few seconds. Subsequent runs will be much faster, and the compilation should be cached, so it should not need to be repeated unless you change the code or update Numba. ================================================ FILE: doc/source/quickstart.rst ================================================ Quick Start Guide ================ This guide provides a quick introduction to using EVōC for clustering high-dimensional embedding vectors. EVōC specifically targets modern embedding vectors such as those produced by CLIP, sentence-transformers, and other dense vector representations. It seeks to provide fast and effective results with as little parameter tuning as possible. Basic Usage ----------- The 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: .. code-block:: python from evoc import EVoC from sklearn.datasets import make_blobs import numpy as np # Generate sample embedding data blob_data, blob_labels = make_blobs(n_samples=10_000, n_features=512, centers=256) # Create and fit the clusterer clusterer = EVoC() labels = clusterer.fit_predict(blob_data) # Analyze results n_clusters = len(np.unique(labels[labels >= 0])) n_noise = np.sum(labels == -1) print(f"Found {n_clusters} clusters") print(f"Noise points: {n_noise}") EVōC uses the sklearn API, so you can drop it in to any existing clustering workflow that expects a fit_predict method. The default parameters are designed to work well for typical embedding data, but you can adjust them as needed (see the Parameter Selection section below). Understanding the Output ------------------------ EVōC uses standard sklearn conventions for its output. After fitting, the clusterer will have the following attributes: * **labels_**: Cluster labels for each point (-1 for noise) * **membership_strengths_**: Confidence scores for cluster membership * **cluster_layers_**: Multiple clustering granularities * **cluster_tree_**: Hierarchical structure of clusters The ``labels_`` attribute is the expected vector of cluster assignments you would get from any sklearn clustering algorithm. The ``membership_strengths_`` attribute provides additional information about how strongly each point belongs to its assigned cluster, which can be useful for filtering or analyzing borderline cases; the is equivalent to the ``probabilities_`` attribute in HDBSCAN. The ``cluster_layers_`` and ``cluster_tree_`` attributes are more novel. EVōC is not a hierarchical clustering algorithm in the traditional sense, instead it produces multiple layers of clustering resolution, that can be results that can be cast into a hierarchy. .. code-block:: python # Access different clustering layers print(f"Available layers: {len(clusterer.cluster_layers_)}") # Get membership strengths strengths = clusterer.membership_strengths_ print(f"Average membership strength: {np.mean(strengths):.3f}") # Access the cluster hierarchy tree = clusterer.cluster_tree_ print(f"Hierarchical structure: {tree}") Layers are sorted from most fine-grained (many small clusters) at index 0 to most coarse-grained (fewer large clusters). Each layer is a label vector, just like ``labels_``, but with a different clustering resolution. The ``labels_`` attribute corresponds to the layer that has clusters persisting across the widest range of cluster resolution scales, and is usually the most stable and meaningful clustering result. However, depending on your needs, other cluster layers may be more appropriate. The ``cluster_tree_`` attribute provides a hierarchical structure of the clusters across layers. It shows how clusters in finer layers relate to clusters in coarser layers, effectively creating a tree of cluster relationships. This can be useful for understanding the multi-scale structure of your data and for selecting clusters at different levels of granularity. The tree is structured as a dictionary. Each cluster is identified as a tuple of (layer_index, cluster_id), and the value is a list of child clusters in the more fine-grained layers. Parameter Selection ------------------- Key parameters to adjust: **n_neighbors** (default=15) Number of neighbors for graph construction. Increase for more global connectivity. **base_min_cluster_size** (default=5) Minimum cluster size at the base layer. **approx_n_clusters** (default=None) Target number of clusters (returns single layer if specified). .. code-block:: python # Example with custom parameters clusterer = EVoC( n_neighbors=25, # More neighbors for denser graphs base_min_cluster_size=10, # Larger minimum clusters max_layers=5 # Limit hierarchy depth ) labels = clusterer.fit_predict(blob_data) Working with Different Data Types --------------------------------- EVoC automatically detects data types and uses appropriate distance metrics: * **float32/float64**: Cosine distance (default for embeddings) * **int8**: Quantized cosine distance * **uint8**: Bitwise Jaccard distance (for binary embeddings) We can take out blob data and convert it to different formats to see how EVoC handles them. In practice, you would typically be working with actual embedding data that comes pre-quantized or binarized depending on the model and/or storage format you are using. embeddings = normalize(blob_data) # Example embedding data # For standard embeddings (float) X_float = embeddings.astype(np.float32) labels_cosine = EVoC().fit_predict(X_float) # For quantized embeddings (int8) X_quantized = (StandardScaler().fit_transform(embeddings) * 127).astype(np.int8) labels_quantized = EVoC().fit_predict(X_quantized) # For binary embeddings (packed uint8) X_binary = np.packbits(embeddings > 0.0, axis=1) labels_binary = EVoC().fit_predict(X_binary) Next Steps ---------- * See the :doc:`user_guide` for detailed parameter explanations * Refer to :doc:`api/index` for complete API documentation ================================================ FILE: doc/source/user_guide.rst ================================================ User Guide ========== This end-user oriented guide covers EVoC's features, parameters, and best practices for different use cases. To better understand the parameters that are available, it help help to bgin with an overview of the algorithm and its key components. Algorithm Overview ------------------ EVoC (Embedding Vector Oriented Clustering) combines two key techniques: 1. **Graph Embedding**: Constructs a k-nearest neighbor graph and learns a lower-dimensional embedding (similar to UMAP) 2. **Density Clustering**: Applies hierarchical density-based clustering to the embedding (similar to HDBSCAN and PLSCAN) The advantage of EVoC is that it can optimize every part of these tasks for the specific task of clustering high-dimensional embedding vectors, providing both improved **performance** and **quality** compared to general-purpose clustering algorithms. That is to say, EVoC not only runs much faster than a combination of UMAP and HDBSCAN, but also produces better clusters as a result. The combination of dimension reduction/manifold learning and density clustering tailored to embedding vectors provides several advantages for clustering embedding vectors: * Efficient processing of dense, high-dimensional data * Multiple clustering granularities through hierarchical layers * Robust handling of noise and outliers * Optimized distance metrics for different embedding types Parameter Reference ------------------- With that core idea -- a two part algorithm -- in mind, let's explore the key parameters that control EVoC's behavior. The parameters can be broadly categorized into three groups: Core Parameters ~~~~~~~~~~~~~~~ These are the main parameters that most users will want to adjust based on their specific dataset and clustering goals: **base_min_cluster_size** : int, default=5 Minimum number of points required to form a cluster at the base (finest) granularity level. Larger values produce fewer, more stable clusters. **n_neighbors** : int, default=15 Number of neighbors used in k-NN graph construction. More neighbors capture more global structure but increase computational cost. **min_samples** : int, default=5 Minimum samples for density estimation in the final clustering step. Should typically match or be smaller than base_min_cluster_size. Clustering Control ~~~~~~~~~~~~~~~~~~ These parameters control the clustering behavior and granularity: **base_n_clusters** : int, optional Target number of clusters for the base layer. When specified, EVoC will search for the clustering granularity that produces approximately this many clusters, then build additional layers on top. **approx_n_clusters** : int, optional Target number of clusters for the final output. When specified, EVoC returns only a single clustering layer (no hierarchy) with approximately this many clusters. **max_layers** : int, default=10 Maximum number of hierarchical clustering layers to generate. More layers provide finer control over clustering granularity but increase computation time. **min_similarity_threshold** : float, default=0.2 Minimum Jaccard similarity threshold for layer selection. Prevents nearly identical clustering layers in the hierarchy. Advanced Parameters ~~~~~~~~~~~~~~~~~~~ These parameters provide more fine-grained control over the algorithm and are typically only adjusted by advanced users: **noise_level** : float, default=0.5 Controls the noise threshold for cluster membership. Higher values produce more noise points and fewer clusters, while lower values produce more clusters and fewer noise points. In practice this only provides fine-tuning over the amount of noise, and is not as important as base_min_cluster_size and min_samples. **node_embedding_dim** : int, optional Dimensionality of the intermediate node embedding. If None, defaults to min(max(n_neighbors // 4, 4), 15). Higher dimensions can capture more complex structure but increase computation. **neighbor_scale** : float, default=1.0 Scales the effective number of neighbors (neighbor_scale × n_neighbors). Values > 1.0 create denser graphs, values < 1.0 create sparser graphs focused on local structure. **n_epochs** : int, default=50 Number of optimization epochs for the node embedding. More epochs improve embedding quality but increase computation time. **node_embedding_init** : {'label_prop', None}, default='label_prop' Initialization method for the node embedding. 'label_prop' uses label propagation for initialization, None uses random initialization. **n_label_prop_iter** : int, default=20 Number of label propagation iterations when using 'label_prop' initialization. **symmetrize_graph** : bool, default=True Whether to make the k-NN graph symmetric. Recommended for most use cases. **random_state** : int, optional Random seed for reproducible results. When specified, enables deterministic mode. Best Practices -------------- As a general rule EVoC is desgined to largely be as parameter-free as possible. The default parameters should work well for a wide range of datasets and use cases, and most users will not need to adjust them. So the best place to start is just running with default parameters and then adjusting based on the results. However, here are some best practices for different scenarios: Working with Hierarchical Output ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ EVoC provides multiple clustering layers with different granularities: .. code-block:: python clusterer = EVoC(max_layers=5) clusterer.fit(X) # Explore different granularities for i, layer in enumerate(clusterer.cluster_layers_): n_clusters = len(np.unique(layer[layer >= 0])) n_noise = np.sum(layer == -1) persistence = clusterer.persistence_scores_[i] print(f"Layer {i}: {n_clusters} clusters, {n_noise} noise points, " f"persistence: {persistence:.3f}") # Use cluster tree for hierarchical analysis tree = clusterer.cluster_tree_ # ... analyze hierarchical structure ... The layer 0 is always the most fine-grained layer as determined by ``base_min_cluster_size`` or ``base_n_clusters``. Each subsequent layer provides a coarser clustering, with fewer clusters. In general the most fine-grained layers will have the most noise points, and the coarser layers will have fewer noise points. The persistence score provides a measure of how stable each layer is across different parameter settings, with higher scores indicating more robust clusters. If you are interested in getting very fine-grained clusters it is worth setting ``base_min_cluster_size`` or ``base_n_clusters`` explicitly to ensure you get clustering at that granularity. You can then inspect the other layers to see if the other natural granularities align with your use case. If you are only interested in a single clustering, you can set ``approx_n_clusters`` to get the layer that is closest to that number of clusters. You can also make use of the tree structure to analyze how clusters evolve across layers, and to identify stable clusters that persist across multiple layers. Alternatively you can use the tree structure to create a "mixed" resolution layer by selecting clusters at a given layer, and then also selecting any clusters in lower layers that are no children of any of your selected clusters. This allows you to get a more fine-grained clustering in some parts of the data, while keeping a coarser clustering i n other parts of the data. Performance Optimization ~~~~~~~~~~~~~~~~~~~~~~~~ Depending on your needs you may be willing to trade off some accuracy for speed, or vice versa. The default EVoC parameters are designed primarily for exploratory clustering, and thus produce clusters very quickly. If you are looking for a more robust higher quality clustering, it can be worth tweaking the parameters to spend more time to produce a better clustering result. For example, for a medium sized dataset (e.g. 10k-100k points) you can increase the number of epochs and neighbors to get a better embedding, which will lead to better clusters. In such cases you will also likely want to fix a random seed to ensure reproducibility, as the optimization process is stochastic. .. code-block:: python clusterer = EVoC( n_epochs=150, # More epochs for better embedding random_state=42 # Enable optimizations ) For 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. In that case not setting a random seed can actually improve performance, as it allows the algorithm to skip some of the overhead of ensuring reproducibility. .. code-block:: python clusterer = EVoC( n_neighbors=10, # Balance between quality and speed n_epochs=30, # Fewer epochs for faster embedding max_layers=3, # Limit hierarchy depth ) Troubleshooting --------------- **Problem**: Too many small clusters **Solution**: Increase base_min_cluster_size or noise_level **Problem**: Most points classified as noise **Solution**: Decrease noise_level or reduce min_samples **Problem**: Clustering too slow **Solution**: Reduce n_neighbors, n_epochs, or max_layers **Problem**: Poor cluster quality **Solution**: Increase n_neighbors, n_epochs, or try different node_embedding_init **Problem**: Inconsistent results **Solution**: Set random_state for reproducible results ================================================ FILE: evoc/__init__.py ================================================ from .clustering import evoc_clusters, EVoC ================================================ FILE: evoc/boruvka.py ================================================ import numba import numpy as np from .disjoint_set import RankDisjointSetType, ds_rank_create, ds_find, ds_union_by_rank from .numba_kdtree import ( NumbaKDTreeType, parallel_tree_query, rdist, point_to_node_lower_bound_rdist, NumbaKDTree, ) @numba.njit( numba.float32[:, ::1]( RankDisjointSetType, numba.int32[::1], numba.types.Array(numba.float32, 1, "A"), numba.int64[::1], ), locals={"i": numba.types.int64}, cache=True, ) def merge_components( disjoint_set, candidate_neighbors, candidate_neighbor_distances, point_components ): component_edges = { np.int64(0): (np.int64(0), np.int64(1), np.float32(0.0)) for i in range(0) } # Find the best edges from each component for i in range(candidate_neighbors.shape[0]): from_component = np.int64(point_components[i]) if from_component in component_edges: if candidate_neighbor_distances[i] < component_edges[from_component][2]: component_edges[from_component] = ( numba.int64(i), numba.int64(candidate_neighbors[i]), numba.float32(candidate_neighbor_distances[i]), ) else: component_edges[from_component] = ( numba.int64(i), numba.int64(candidate_neighbors[i]), numba.float32(candidate_neighbor_distances[i]), ) result = np.empty((len(component_edges), 3), dtype=np.float32) result_idx = 0 # Add the best edges to the edge set and merge the relevant components for edge in component_edges.values(): from_component = ds_find(disjoint_set, numba.int32(edge[0])) to_component = ds_find(disjoint_set, numba.int32(edge[1])) if from_component != to_component: result[result_idx] = ( numba.float32(edge[0]), numba.float32(edge[1]), numba.float32(edge[2]), ) result_idx += 1 ds_union_by_rank(disjoint_set, from_component, to_component) return result[:result_idx] @numba.njit( numba.void( NumbaKDTreeType, RankDisjointSetType, numba.int64[::1], numba.int64[::1], ), locals={ "i": numba.types.int32, "j": numba.types.int32, "idx": numba.types.int32, "left": numba.types.int32, "right": numba.types.int32, "candidate_component": numba.types.int32, }, parallel=True, cache=True, fastmath=True, ) def update_component_vectors(tree, disjoint_set, node_components, point_components): for i in numba.prange(point_components.shape[0]): point_components[i] = ds_find(disjoint_set, np.int32(i)) for i in range(tree.idx_start.shape[0] - 1, -1, -1): # Access node information from the separate arrays is_leaf = tree.is_leaf[i] idx_start = tree.idx_start[i] idx_end = tree.idx_end[i] # Case 1: # If the node is a leaf we need to check that every point # in the node is of the same component if is_leaf: candidate_component = point_components[tree.idx_array[idx_start]] for j in range(idx_start + 1, idx_end): idx = tree.idx_array[j] if point_components[idx] != candidate_component: break else: node_components[i] = candidate_component # Case 2: # If the node is not a leaf we only need to check # that both child nodes are in the same component else: left = 2 * i + 1 right = left + 1 if node_components[left] == node_components[right]: node_components[i] = node_components[left] @numba.njit( numba.void( NumbaKDTreeType, numba.int32, numba.float32[::1], numba.float32[::1], numba.int32[::1], numba.float32, numba.types.Array(numba.float32, 1, "A"), numba.int64, numba.int64[::1], numba.int64[::1], numba.float32, numba.float32[::1], ), locals={ "i": numba.types.int32, "idx": numba.types.int32, "left": numba.types.int32, "right": numba.types.int32, "d": numba.types.float32, "dist_lower_bound_left": numba.types.float32, "dist_lower_bound_right": numba.types.float32, }, cache=True, fastmath=True, ) def component_aware_query_recursion( tree, node, point, heap_p, heap_i, current_core_distance, core_distances, current_component, node_components, point_components, dist_lower_bound, component_nearest_neighbor_dist, ): # Access node information from the separate arrays is_leaf = tree.is_leaf[node] idx_start = tree.idx_start[node] idx_end = tree.idx_end[node] # ------------------------------------------------------------ # Case 1a: query point is outside node radius: # trim it from the query if dist_lower_bound > heap_p[0]: return # ------------------------------------------------------------ # Case 1b: we can't improve on the best distance for this component # trim it from the query elif ( dist_lower_bound > component_nearest_neighbor_dist[0] or current_core_distance > component_nearest_neighbor_dist[0] ): return # ------------------------------------------------------------ # Case 1c: node contains only points in same component as query # trim it from the query elif node_components[node] == current_component: return # ------------------------------------------------------------ # Case 2: this is a leaf node. Update set of nearby points elif is_leaf: for i in range(idx_start, idx_end): idx = tree.idx_array[i] if ( point_components[idx] != current_component and core_distances[idx] < component_nearest_neighbor_dist[0] ): d = max( rdist(point, tree.data[idx]), current_core_distance, core_distances[idx], ) if d < heap_p[0]: heap_p[0] = d heap_i[0] = idx if d < component_nearest_neighbor_dist[0]: component_nearest_neighbor_dist[0] = d # ------------------------------------------------------------ # Case 3: Node is not a leaf. Recursively query subnodes # starting with the closest else: left = numba.int32(2 * node + 1) right = numba.int32(left + 1) dist_lower_bound_left = point_to_node_lower_bound_rdist( tree.node_bounds[0, left], tree.node_bounds[1, left], point ) dist_lower_bound_right = point_to_node_lower_bound_rdist( tree.node_bounds[0, right], tree.node_bounds[1, right], point ) # recursively query subnodes if dist_lower_bound_left <= dist_lower_bound_right: component_aware_query_recursion( tree, left, point, heap_p, heap_i, current_core_distance, core_distances, current_component, node_components, point_components, dist_lower_bound_left, component_nearest_neighbor_dist, ) component_aware_query_recursion( tree, right, point, heap_p, heap_i, current_core_distance, core_distances, current_component, node_components, point_components, dist_lower_bound_right, component_nearest_neighbor_dist, ) else: component_aware_query_recursion( tree, right, point, heap_p, heap_i, current_core_distance, core_distances, current_component, node_components, point_components, dist_lower_bound_right, component_nearest_neighbor_dist, ) component_aware_query_recursion( tree, left, point, heap_p, heap_i, current_core_distance, core_distances, current_component, node_components, point_components, dist_lower_bound_left, component_nearest_neighbor_dist, ) return @numba.njit( numba.types.Tuple((numba.float32[::1], numba.int32[::1]))( NumbaKDTreeType, numba.int64[::1], numba.int64[::1], numba.types.Array(numba.float32, 1, "A"), ), locals={ "i": numba.types.int32, "distance_lower_bound": numba.types.float32, "current_component": numba.types.int32, }, parallel=True, cache=True, fastmath=True, ) def boruvka_tree_query(tree, node_components, point_components, core_distances): candidate_distances = np.full(tree.data.shape[0], np.inf, dtype=np.float32) candidate_indices = np.full(tree.data.shape[0], -1, dtype=np.int32) component_nearest_neighbor_dist = np.full( tree.data.shape[0], np.inf, dtype=np.float32 ) data = tree.data.astype(np.float32) for i in numba.prange(tree.data.shape[0]): distance_lower_bound = point_to_node_lower_bound_rdist( tree.node_bounds[0, 0], tree.node_bounds[1, 0], tree.data[i] ) heap_p, heap_i = candidate_distances[i : i + 1], candidate_indices[i : i + 1] component_aware_query_recursion( tree, numba.int32(0), data[i], heap_p, heap_i, core_distances[i], core_distances, point_components[i], node_components, point_components, distance_lower_bound, component_nearest_neighbor_dist[ point_components[i] : point_components[i] + 1 ], ) return candidate_distances, candidate_indices @numba.njit(inline="always", cache=True) def calculate_block_size(n_components, n_points, num_threads): """Calculate adaptive block size based on component sizes.""" if n_components == 0: points_per_component = n_points else: points_per_component = n_points / n_components if points_per_component < 10: block_size = num_threads * 512 # Weak pruning, large blocks elif points_per_component < 100: block_size = num_threads * 128 # Moderate pruning elif points_per_component < 1000: block_size = num_threads * 32 # Good pruning else: block_size = num_threads * 8 # Excellent pruning, small blocks # Ensure reasonable bounds block_size = max(num_threads, min(block_size, n_points // 4 + 1)) return int(block_size) @numba.njit( [ "void(float32[:], float32[:], int32[:], int32, int32)", "void(float64[:], float64[:], int64[:], int64, int64)", ], locals={ "i": numba.types.int32, "component": numba.types.int32, "block_bound": numba.types.float32, }, cache=True, fastmath=True, inline="always", ) def update_component_bounds_from_block( component_nearest_neighbor_dist, block_component_bounds, point_components, block_start, block_end, ): """Update global component bounds from block results.""" for i in range(block_start, block_end): component = point_components[i] block_bound = block_component_bounds[i - block_start] if block_bound < component_nearest_neighbor_dist[component]: component_nearest_neighbor_dist[component] = block_bound @numba.njit( numba.types.Tuple((numba.float32[::1], numba.int32[::1]))( NumbaKDTreeType, numba.int64[::1], numba.int64[::1], numba.types.Array(numba.float32, 1, "A"), numba.int64, ), locals={ "block_start": numba.types.int32, "block_end": numba.types.int32, "block_size_actual": numba.types.int32, "i": numba.types.int32, "distance_lower_bound": numba.types.float32, "current_component": numba.types.int32, }, parallel=True, cache=True, fastmath=True, ) def boruvka_tree_query_reproducible( tree, node_components, point_components, core_distances, block_size ): """Reproducible version using block-based processing to avoid race conditions.""" candidate_distances = np.full(tree.data.shape[0], np.inf, dtype=np.float32) candidate_indices = np.full(tree.data.shape[0], -1, dtype=np.int32) component_nearest_neighbor_dist = np.full( tree.data.shape[0], np.inf, dtype=np.float32 ) data = tree.data.astype(np.float32) # Reusable buffer for block component bounds (allocate once, reuse) max_block_component_bounds = np.full(block_size, np.inf, dtype=np.float32) # Process points in blocks for block_start in range(0, tree.data.shape[0], block_size): block_end = min(block_start + block_size, tree.data.shape[0]) block_size_actual = block_end - block_start # Reset only the portion we'll use (more cache-friendly) max_block_component_bounds[:block_size_actual] = np.inf # Parallel processing within the block for i in numba.prange(block_start, block_end): distance_lower_bound = point_to_node_lower_bound_rdist( tree.node_bounds[0, 0], tree.node_bounds[1, 0], tree.data[i] ) heap_p, heap_i = ( candidate_distances[i : i + 1], candidate_indices[i : i + 1], ) # Use current global bounds for this component current_component = point_components[i] local_component_bound = component_nearest_neighbor_dist[ current_component : current_component + 1 ] component_aware_query_recursion( tree, numba.int32(0), data[i], heap_p, heap_i, core_distances[i], core_distances, point_components[i], node_components, point_components, distance_lower_bound, local_component_bound, ) # Store the potentially updated bound for this point max_block_component_bounds[i - block_start] = local_component_bound[0] # Sequential update of global component bounds after the block update_component_bounds_from_block( component_nearest_neighbor_dist, max_block_component_bounds, point_components, block_start, block_end, ) return candidate_distances, candidate_indices @numba.njit( locals={ "i": numba.types.int32, "j": numba.types.int32, "k": numba.types.int32, "result_idx": numba.types.int32, "from_component": numba.types.int32, "to_component": numba.types.int32, }, parallel=True, cache=True, ) def initialize_boruvka_from_knn( knn_indices, knn_distances, core_distances, disjoint_set ): # component_edges = {0:(np.int32(0), np.int32(1), np.float32(0.0)) for i in range(0)} component_edges = np.full((knn_indices.shape[0], 3), -1, dtype=np.float64) for i in numba.prange(knn_indices.shape[0]): for j in range(1, knn_indices.shape[1]): k = np.int32(knn_indices[i, j]) if core_distances[i] >= core_distances[k]: # Use max of core distance and actual distance as edge weight edge_weight = max(core_distances[i], knn_distances[i, j]) component_edges[i] = ( np.float64(i), np.float64(k), np.float64(edge_weight), ) break result = np.empty((len(component_edges), 3), dtype=np.float64) result_idx = 0 # Add the best edges to the edge set and merge the relevant components for edge in component_edges: if edge[0] < 0: continue from_component = ds_find(disjoint_set, np.int32(edge[0])) to_component = ds_find(disjoint_set, np.int32(edge[1])) if from_component != to_component: result[result_idx] = ( np.float64(edge[0]), np.float64(edge[1]), np.float64(edge[2]), ) result_idx += 1 ds_union_by_rank(disjoint_set, from_component, to_component) return result[:result_idx].astype(np.float32) @numba.njit( numba.float32[:, ::1]( NumbaKDTreeType, numba.int64, numba.int64, numba.types.boolean, ), cache=True, ) def parallel_boruvka(tree, n_threads, min_samples=10, reproducible=False): components_disjoint_set = ds_rank_create(tree.data.shape[0]) point_components = np.arange(tree.data.shape[0]) node_components = np.full(tree.idx_start.shape[0], -1) n_components = point_components.shape[0] if min_samples > 1: distances, neighbors = parallel_tree_query( tree, tree.data, k=numba.int64(min_samples + 1), output_rdist=True ) core_distances = distances.T[-1] initial_edges = initialize_boruvka_from_knn( neighbors, distances, core_distances, components_disjoint_set ) update_component_vectors( tree, components_disjoint_set, node_components, point_components ) else: core_distances = np.zeros(tree.data.shape[0], dtype=np.float32) distances, neighbors = parallel_tree_query( tree, tree.data, k=numba.int64(2), output_rdist=True ) initial_edges = initialize_boruvka_from_knn( neighbors, distances, core_distances, components_disjoint_set ) update_component_vectors( tree, components_disjoint_set, node_components, point_components ) # Count initial components after initialization n_components = len(np.unique(point_components)) # Use list to accumulate edges, then convert at end (more efficient than vstack) # all_edges = [initial_edges] # all_edges = initial_edges max_edges = tree.data.shape[0] - 1 all_edges = np.empty((max_edges, 3), dtype=np.float32) n_edges = numba.int64(len(initial_edges)) all_edges[:n_edges] = initial_edges while n_components > 1: if reproducible: # Calculate adaptive block size based on current component sizes block_size = calculate_block_size( n_components, tree.data.shape[0], n_threads ) candidate_distances, candidate_indices = boruvka_tree_query_reproducible( tree, node_components, point_components, core_distances, block_size ) else: candidate_distances, candidate_indices = boruvka_tree_query( tree, node_components, point_components, core_distances ) new_edges = merge_components( components_disjoint_set, candidate_indices, candidate_distances, point_components, ) # Update component count more efficiently - subtract merged components n_components -= len(new_edges) update_component_vectors( tree, components_disjoint_set, node_components, point_components ) if len(new_edges) > 0: # # all_edges.append(new_edges) # all_edges = np.vstack((all_edges, new_edges)).astype(np.float32) all_edges[n_edges : n_edges + len(new_edges)] = new_edges n_edges += numba.int64(len(new_edges)) all_edges[:, 2] = np.sqrt(all_edges.T[2]) return all_edges ================================================ FILE: evoc/cluster_trees.py ================================================ import numba import numpy as np from collections import namedtuple from .disjoint_set import ds_rank_create, ds_find, ds_union_by_rank LinkageMergeData = namedtuple("LinkageMergeData", ["parent", "size", "next"]) @numba.njit(cache=True) def create_linkage_merge_data(base_size): parent = np.full(2 * base_size - 1, -1, dtype=np.intp) size = np.concatenate( (np.ones(base_size, dtype=np.intp), np.zeros(base_size - 1, dtype=np.intp)) ) next_parent = np.array([base_size], dtype=np.intp) return LinkageMergeData(parent, size, next_parent) @numba.njit(cache=True) def linkage_merge_find(linkage_merge, node): relabel = node while linkage_merge.parent[node] != -1 and linkage_merge.parent[node] != node: node = linkage_merge.parent[node] linkage_merge.parent[node] = node # label up to the root while linkage_merge.parent[relabel] != node: next_relabel = linkage_merge.parent[relabel] linkage_merge.parent[relabel] = node relabel = next_relabel return node @numba.njit(cache=True) def linkage_merge_join(linkage_merge, left, right): linkage_merge.size[linkage_merge.next[0]] = ( linkage_merge.size[left] + linkage_merge.size[right] ) linkage_merge.parent[left] = linkage_merge.next[0] linkage_merge.parent[right] = linkage_merge.next[0] linkage_merge.next[0] += 1 @numba.njit(cache=True) def mst_to_linkage_tree(sorted_mst): result = np.empty((sorted_mst.shape[0], sorted_mst.shape[1] + 1)) n_samples = sorted_mst.shape[0] + 1 linkage_merge = create_linkage_merge_data(n_samples) for index in range(sorted_mst.shape[0]): left = np.intp(sorted_mst[index, 0]) right = np.intp(sorted_mst[index, 1]) delta = sorted_mst[index, 2] left_component = linkage_merge_find(linkage_merge, left) right_component = linkage_merge_find(linkage_merge, right) if left_component > right_component: result[index][0] = left_component result[index][1] = right_component else: result[index][1] = left_component result[index][0] = right_component result[index][2] = delta result[index][3] = ( linkage_merge.size[left_component] + linkage_merge.size[right_component] ) linkage_merge_join(linkage_merge, left_component, right_component) return result @numba.njit(cache=True) def bfs_from_hierarchy(hierarchy, bfs_root, num_points): to_process = [bfs_root] result = [] while to_process: result.extend(to_process) next_to_process = [] for n in to_process: if n >= num_points: i = n - num_points next_to_process.append(int(hierarchy[i, 0])) next_to_process.append(int(hierarchy[i, 1])) to_process = next_to_process return result @numba.njit(cache=True) def eliminate_branch( branch_node, parent_node, lambda_value, parents, children, lambdas, sizes, idx, ignore, hierarchy, num_points, ): if branch_node < num_points: parents[idx] = parent_node children[idx] = branch_node lambdas[idx] = lambda_value idx += 1 else: for sub_node in bfs_from_hierarchy(hierarchy, branch_node, num_points): if sub_node < num_points: children[idx] = sub_node parents[idx] = parent_node lambdas[idx] = lambda_value idx += 1 else: ignore[sub_node] = True return idx CondensedTree = namedtuple( "CondensedTree", ["parent", "child", "lambda_val", "child_size"] ) @numba.njit(fastmath=True, cache=True) def condense_tree(hierarchy, min_cluster_size=10): root = 2 * hierarchy.shape[0] num_points = hierarchy.shape[0] + 1 next_label = num_points + 1 node_list = bfs_from_hierarchy(hierarchy, root, num_points) relabel = np.zeros(root + 1, dtype=np.int64) relabel[root] = num_points parents = np.ones(root, dtype=np.int64) children = np.empty(root, dtype=np.int64) lambdas = np.empty(root, dtype=np.float32) sizes = np.ones(root, dtype=np.int64) ignore = np.zeros(root + 1, dtype=np.bool_) idx = 0 for node in node_list: if ignore[node] or node < num_points: continue parent_node = relabel[node] l, r, d, _ = hierarchy[node - num_points] left = np.int64(l) right = np.int64(r) if d > 0.0: lambda_value = 1.0 / d else: lambda_value = np.inf left_count = ( np.int64(hierarchy[left - num_points, 3]) if left >= num_points else 1 ) right_count = ( np.int64(hierarchy[right - num_points, 3]) if right >= num_points else 1 ) # The logic here is in a strange order, but it has non-trivial performance gains ... # The most common case by far is a singleton on the left; and cluster on the right take care of this separately if left < num_points and right_count >= min_cluster_size: relabel[right] = parent_node parents[idx] = parent_node children[idx] = left lambdas[idx] = lambda_value idx += 1 # Next most common is a small left cluster and a large right cluster: relabel the right node; eliminate the left branch elif left_count < min_cluster_size and right_count >= min_cluster_size: relabel[right] = parent_node idx = eliminate_branch( left, parent_node, lambda_value, parents, children, lambdas, sizes, idx, ignore, hierarchy, num_points, ) # Then we have a large left cluster and a small right cluster: relabel the left node; elimiate the right branch elif left_count >= min_cluster_size and right_count < min_cluster_size: relabel[left] = parent_node idx = eliminate_branch( right, parent_node, lambda_value, parents, children, lambdas, sizes, idx, ignore, hierarchy, num_points, ) # If both clusters are small then eliminate all branches elif left_count < min_cluster_size and right_count < min_cluster_size: idx = eliminate_branch( left, parent_node, lambda_value, parents, children, lambdas, sizes, idx, ignore, hierarchy, num_points, ) idx = eliminate_branch( right, parent_node, lambda_value, parents, children, lambdas, sizes, idx, ignore, hierarchy, num_points, ) # and finally if we actually have a legitimate cluster split, handle that correctly else: relabel[left] = next_label parents[idx] = parent_node children[idx] = next_label lambdas[idx] = lambda_value sizes[idx] = left_count next_label += 1 idx += 1 relabel[right] = next_label parents[idx] = parent_node children[idx] = next_label lambdas[idx] = lambda_value sizes[idx] = right_count next_label += 1 idx += 1 return CondensedTree(parents[:idx], children[:idx], lambdas[:idx], sizes[:idx]) @numba.njit(cache=True) def extract_leaves(condensed_tree, allow_single_cluster=True): # Handle empty tree case gracefully if len(condensed_tree.parent) == 0: return np.zeros(0, dtype=np.intp) n_nodes = condensed_tree.parent.max() + 1 n_points = condensed_tree.parent.min() leaf_indicator = np.ones(n_nodes, dtype=np.bool_) leaf_indicator[:n_points] = False for parent, child_size in zip(condensed_tree.parent, condensed_tree.child_size): if child_size > 1: leaf_indicator[parent] = False return np.nonzero(leaf_indicator)[0] @numba.njit(cache=True, fastmath=True) def score_condensed_tree_nodes(condensed_tree): result = {0: 0.0 for i in range(0)} for i in range(condensed_tree.parent.shape[0]): parent = condensed_tree.parent[i] if parent in result: result[parent] += ( condensed_tree.lambda_val[i] * condensed_tree.child_size[i] ) else: result[parent] = condensed_tree.lambda_val[i] * condensed_tree.child_size[i] if condensed_tree.child_size[i] > 1: child = condensed_tree.child[i] if child in result: result[child] -= ( condensed_tree.lambda_val[i] * condensed_tree.child_size[i] ) else: result[child] = ( -condensed_tree.lambda_val[i] * condensed_tree.child_size[i] ) return result @numba.njit(cache=True) def cluster_tree_from_condensed_tree(condensed_tree): mask = condensed_tree.child_size > 1 return CondensedTree( condensed_tree.parent[mask], condensed_tree.child[mask], condensed_tree.lambda_val[mask], condensed_tree.child_size[mask], ) @numba.njit(cache=True) def mask_condensed_tree(condensed_tree, mask): return CondensedTree( condensed_tree.parent[mask], condensed_tree.child[mask], condensed_tree.lambda_val[mask], condensed_tree.child_size[mask] ) @numba.njit(cache=True) def unselect_below_node(node, cluster_tree, selected_clusters): for child in cluster_tree.child[cluster_tree.parent == node]: unselect_below_node(child, cluster_tree, selected_clusters) selected_clusters[child] = False @numba.njit(fastmath=True, cache=True) def eom_recursion(node, cluster_tree, node_scores, selected_clusters): current_score = node_scores[node] children = cluster_tree.child[cluster_tree.parent == node] child_score_total = 0.0 for child_node in children: child_score_total += eom_recursion( child_node, cluster_tree, node_scores, selected_clusters ) if child_score_total > current_score: return child_score_total else: selected_clusters[node] = True unselect_below_node(node, cluster_tree, selected_clusters) return current_score @numba.njit(cache=True) def extract_eom_clusters(condensed_tree, cluster_tree, allow_single_cluster=False): node_scores = score_condensed_tree_nodes(condensed_tree) selected_clusters = {node: False for node in node_scores} if len(cluster_tree.parent) == 0: return np.zeros(0, dtype=np.int64) cluster_tree_root = cluster_tree.parent.min() if allow_single_cluster: eom_recursion(cluster_tree_root, cluster_tree, node_scores, selected_clusters) elif len(node_scores) > 1: root_children = cluster_tree.child[cluster_tree.parent == cluster_tree_root] for child_node in root_children: eom_recursion(child_node, cluster_tree, node_scores, selected_clusters) return np.asarray( [node for node, selected in selected_clusters.items() if selected] ) @numba.njit(cache=True) def cluster_epsilon_search(clusters, cluster_tree, min_persistence=0.0): selected = list() # only way to create a typed empty set processed = {np.int64(0)} processed.clear() root = cluster_tree.parent.min() for cluster in clusters: eps = 1 / cluster_tree.lambda_val[cluster_tree.child == cluster][0] if eps < min_persistence: if cluster not in processed: parent = traverse_upwards(cluster_tree, min_persistence, root, cluster) selected.append(parent) processed |= segments_in_branch(cluster_tree, parent) else: selected.append(cluster) return np.asarray(selected) @numba.njit(cache=True) def traverse_upwards(cluster_tree, min_persistence, root, segment): parent = cluster_tree.parent[cluster_tree.child == segment][0] if parent == root: return root parent_eps = 1 / cluster_tree.lambda_val[cluster_tree.child == parent][0] if parent_eps >= min_persistence: return parent else: return traverse_upwards(cluster_tree, min_persistence, root, parent) @numba.njit(cache=True) def segments_in_branch(cluster_tree, segment): # only way to create a typed empty set result = {np.intp(0)} result.clear() to_process = {segment} while len(to_process) > 0: result |= to_process to_process = set( cluster_tree.child[in_set_parallel(cluster_tree.parent, to_process)] ) return result @numba.njit(parallel=True, cache=True) def in_set_parallel(values, targets): mask = np.empty(values.shape[0], dtype=numba.boolean) for i in numba.prange(values.shape[0]): mask[i] = values[i] in targets return mask @numba.njit(parallel=True, cache=True) def get_cluster_labelling_at_cut(linkage_tree, cut, min_cluster_size): root = 2 * linkage_tree.shape[0] num_points = linkage_tree.shape[0] + 1 result = np.empty(num_points, dtype=np.intp) disjoint_set = ds_rank_create(root + 1) cluster = num_points for i in range(linkage_tree.shape[0]): if linkage_tree[i, 2] < cut: ds_union_by_rank(disjoint_set, np.intp(linkage_tree[i, 0]), cluster) ds_union_by_rank(disjoint_set, np.intp(linkage_tree[i, 1]), cluster) cluster += 1 cluster_size = np.zeros(cluster, dtype=np.intp) for n in range(num_points): cluster = ds_find(disjoint_set, n) cluster_size[cluster] += 1 result[n] = cluster cluster_label_map = {-1: -1} cluster_label = 0 unique_labels = np.unique(result) for cluster in unique_labels: if cluster_size[cluster] < min_cluster_size: cluster_label_map[cluster] = -1 else: cluster_label_map[cluster] = cluster_label cluster_label += 1 for n in numba.prange(num_points): result[n] = cluster_label_map[result[n]] return result @numba.njit(cache=True) def get_single_cluster_label_vector( tree, cluster, cluster_selection_epsilon, n_samples, ): if len(tree.parent) == 0: return np.full(n_samples, -1, dtype=np.intp) result = np.full(n_samples, -1, dtype=np.intp) max_lambda = tree.lambda_val[tree.parent == cluster].max() for i in range(tree.child.shape[0]): n = tree.child[i] cur_lambda = tree.lambda_val[i] if cluster_selection_epsilon > 0.0: if cur_lambda >= 1 / cluster_selection_epsilon: result[n] = 0 else: result[n] = -1 elif cur_lambda >= max_lambda: result[n] = 0 return result @numba.njit(cache=True) def get_cluster_label_vector( tree, clusters, cluster_selection_epsilon, n_samples, ): if len(clusters) == 1: return get_single_cluster_label_vector( tree, clusters[0], cluster_selection_epsilon, n_samples ) if len(tree.parent) == 0: return np.full(n_samples, -1, dtype=np.intp) root_cluster = tree.parent.min() result = np.full(n_samples, -1, dtype=np.intp) cluster_label_map = {c: n for n, c in enumerate(np.sort(clusters))} disjoint_set = ds_rank_create(max(tree.parent.max() + 1, tree.child.max() + 1)) clusters = set(clusters) for n in range(tree.parent.shape[0]): child = tree.child[n] parent = tree.parent[n] if child not in clusters: ds_union_by_rank(disjoint_set, parent, child) for n in range(n_samples): cluster = ds_find(disjoint_set, n) if cluster <= root_cluster: result[n] = -1 else: result[n] = cluster_label_map[cluster] return result @numba.njit(cache=True) def max_lambdas(tree, clusters): result = {c: 0.0 for c in clusters} for n in range(tree.parent.shape[0]): cluster = tree.parent[n] if cluster in clusters and tree.child_size[n] == 1: result[cluster] = max(result[cluster], tree.lambda_val[n]) return result @numba.njit(cache=True) def get_point_membership_strength_vector(tree, clusters, labels): result = np.zeros(labels.shape[0], dtype=np.float32) deaths = max_lambdas(tree, set(clusters)) root_cluster = tree.parent.min() cluster_index_map = {n: c for n, c in enumerate(np.sort(clusters))} for n in range(tree.child.shape[0]): point = tree.child[n] if point >= root_cluster or labels[point] < 0: continue cluster = cluster_index_map[labels[point]] max_lambda = deaths[cluster] if max_lambda == 0.0 or not np.isfinite(tree.lambda_val[n]): result[point] = 1.0 else: lambda_val = min(tree.lambda_val[n], max_lambda) result[point] = lambda_val / max_lambda return result ================================================ FILE: evoc/clustering.py ================================================ import numpy as np import numba from sklearn.base import BaseEstimator, ClusterMixin from sklearn.utils import check_array, check_random_state from sklearn.utils.validation import check_is_fitted from .numba_kdtree import build_kdtree from .boruvka import parallel_boruvka from .cluster_trees import ( mst_to_linkage_tree, condense_tree, mask_condensed_tree, extract_leaves, get_cluster_label_vector, get_point_membership_strength_vector, ) from .clustering_utilities import ( find_peaks, _binary_search_for_n_clusters, binary_search_for_n_clusters, min_cluster_size_barcode, compute_total_persistence, extract_clusters_by_id, select_diverse_peaks, build_cluster_tree, find_duplicates, ) from .knn_graph import knn_graph from .label_propagation import label_propagation_init from .node_embedding import node_embedding from .graph_construction import neighbor_graph_matrix def build_cluster_layers( data, *, min_samples=5, base_min_cluster_size=10, base_n_clusters=None, reproducible_flag=False, min_similarity_threshold=0.2, max_layers=10, ): """Build hierarchical cluster layers from embedding data. Parameters ---------- data : array-like of shape (n_samples, n_features) The embedding data to cluster. Typically the output of a node embedding algorithm. min_samples : int, default=5 The minimum number of samples to use in the density estimation when performing density based clustering. base_min_cluster_size : int, default=10 The minimum number of points in a cluster at the base layer of the clustering. This gives the finest granularity clustering that will be returned. base_n_clusters : int or None, default=None If not None, the algorithm will attempt to find the granularity of clustering that will give exactly this many clusters for the bottom-most layer of clustering. This affects the base layer computation and allows multiple layers to be built on top of this base. reproducible_flag : bool, default=False Whether to ensure reproducible results by using deterministic algorithms where possible. min_similarity_threshold : float, default=0.2 The minimum similarity threshold for cluster layer selection. Peaks that result in clusterings with Jaccard similarity above this threshold will be filtered out to ensure diverse cluster layers. max_layers : int, default=10 The maximum number of cluster layers to return. The algorithm will select up to this many diverse peaks based on persistence and similarity criteria. Returns ------- cluster_layers : list of array-like of shape (n_samples,) The clustering of the data at each layer of the clustering. Each layer is a clustering of the data into a different number of clusters. membership_strength_layers : list of array-like of shape (n_samples,) The membership strengths of each point in the clustering at each layer. This gives a measure of how strongly each point belongs to each cluster. persistence_scores : list of float The persistence scores for each cluster layer, indicating the quality or stability of the clustering at that layer. """ n_samples = data.shape[0] min_cluster_size = base_min_cluster_size cluster_layers = [] membership_strength_layers = [] persistence_scores = [] n_threads = numba.get_num_threads() numba_tree = build_kdtree(data.astype(np.float32)) edges = parallel_boruvka( numba_tree, n_threads, min_samples=min_cluster_size if min_samples is None else min_samples, reproducible=reproducible_flag, ) sorted_mst = edges[np.argsort(edges.T[2])] uncondensed_tree = mst_to_linkage_tree(sorted_mst) if base_n_clusters is not None: leaves, clusters, strengths = _binary_search_for_n_clusters( uncondensed_tree, base_n_clusters, n_samples=n_samples ) cluster_sizes = np.bincount(clusters[clusters >= 0]) if len(cluster_sizes) > 0: min_cluster_size = max(1, np.min(cluster_sizes)) else: min_cluster_size = base_min_cluster_size # Still need condensed tree for later processing condensed_tree = condense_tree(uncondensed_tree, min_cluster_size) else: condensed_tree = condense_tree(uncondensed_tree, base_min_cluster_size) leaves = extract_leaves(condensed_tree) clusters = get_cluster_label_vector(condensed_tree, leaves, 0.0, n_samples) strengths = get_point_membership_strength_vector( condensed_tree, leaves, clusters ) mask = condensed_tree.child >= n_samples cluster_tree = mask_condensed_tree(condensed_tree, mask) # points_tree = mask_condensed_tree(condensed_tree, ~mask) # Check if cluster_tree is valid before processing if len(cluster_tree.child) > 0 and cluster_tree.child[-1] >= n_samples: births, deaths, parents, lambda_deaths = min_cluster_size_barcode( cluster_tree, n_samples, min_cluster_size ) sizes, total_persistence = compute_total_persistence( births, deaths, lambda_deaths ) peaks = find_peaks(total_persistence) else: # Handle empty or invalid cluster tree births = np.array([]) deaths = np.array([]) parents = np.array([]) lambda_deaths = np.array([]) sizes = np.array([]) total_persistence = np.array([]) peaks = np.array([], dtype=np.int64) # Always include the base layer (from initial condensed tree) cluster_layers.append(clusters) membership_strength_layers.append(strengths) persistence_scores.append(0.0) # Base layer gets 0 persistence score # Select diverse peaks using hierarchical selection selected_peaks = select_diverse_peaks( peaks, total_persistence, sizes, births, deaths, min_similarity_threshold=min_similarity_threshold, max_layers=max_layers - 1, # Reserve one slot for base layer ) for peak in selected_peaks: best_birth = sizes[peak] persistence = total_persistence[peak] selected_clusters = ( np.where((births <= best_birth) & (deaths > best_birth))[0] + n_samples ) labels, strengths = extract_clusters_by_id(condensed_tree, selected_clusters) cluster_layers.append(labels) membership_strength_layers.append(strengths) persistence_scores.append(persistence) # Sort cluster layers by number of clusters (most clusters first) n_clusters_per_layer = [layer.max() + 1 for layer in cluster_layers] sorted_indices = np.argsort(n_clusters_per_layer)[::-1] # Descending order cluster_layers = [cluster_layers[i] for i in sorted_indices] membership_strength_layers = [membership_strength_layers[i] for i in sorted_indices] persistence_scores = [persistence_scores[i] for i in sorted_indices] return cluster_layers, membership_strength_layers, persistence_scores def evoc_clusters( data, noise_level=0.5, base_min_cluster_size=5, base_n_clusters=None, approx_n_clusters=None, n_neighbors=15, min_samples=5, n_epochs=50, node_embedding_init="label_prop", symmetrize_graph=True, return_duplicates=False, node_embedding_dim=None, neighbor_scale=1.0, random_state=None, reproducible_flag=True, min_similarity_threshold=0.2, max_layers=10, n_label_prop_iter=20, ): """Cluster data using the EVoC algorithm. Parameters ---------- data : array-like of shape (n_samples, n_features) The data to cluster. If the data is float valued then it is assumed to use cosine distance as a matric. If the data is int8 valued then it is assumed that a quantized embedding is being used and a quantized version of cosine distance is used. If the data is uint8 valued then it is assumed that a binary embedding is being used, and a bitwise Jaccard distance is used. noise_level : float, default=0.5 The noise level expected in the data. A value of 0.0 will try to cluster more data, at the expense of getting less accurate clustering. A value of 1.0 will try for accurate clusters, discarding more data as noise to do so. base_min_cluster_size : int, default=5 The minimum number of points in a cluster at the base layer of the clustering. This gives the finest granularity clustering that will be returned, with less graularity at higher layers. base_n_clusters : int, default=None If not None, the algorithm will attempt to find the granularity of clustering that will give exactly this many clusters for the bottom-most layer of clustering. This affects the base layer computation and allows multiple layers to be built on top of this base. Since the actual number of clusters cannot be guaranteed this is only approximate, but usually the algorithm can manage to get this exact number, assuming a reasonable clustering into ``base_n_clusters`` exists. approx_n_clusters : int, default=None If not None, the algorithm will attempt to find the granularity of clustering that will give exactly this many clusters as the final output. Unlike ``base_n_clusters``, when this parameter is set, only a single clustering layer will be returned -- no hierarchical layers will be produced. This is useful when you know the exact number of clusters you want and don't need the multi-layer analysis. Since the actual number of clusters cannot be guaranteed this is only approximate, but usually the algorithm can manage to get this exact number, assuming a reasonable clustering into ``approx_n_clusters`` exists. n_neighbors : int, default=15 The number of neighbors to use in the nearest neighbor graph construction. min_samples : int, default=5 The minimum number of samples to use in the density estimation when performing density based clustering on the node embedding. n_epochs : int, default=50 The number of epochs to use when training the node embedding. node_embedding_init : str or None, default='label_prop' The method to use to initialize the node embedding. If None, no initialization will be used. If 'label_prop', the label propagation method will be used. symmetrize_graph : bool, default=True Whether to symmetrize the nearest neighbor graph before using it to construct the node embedding. return_duplicates : bool, default=False Whether to return a set of duplicate pairs of points in the data. node_embedding_dim : int or None, default=None The number of dimensions to use in the node embedding. If None, a default value of min(max(n_neighbors // 4, 4), 15) will be used. neighbor_scale : float, default=1.0 The scale factor to use when constructing the nearest neighbor graph. This multiplies the effective number of neighbors used in graph construction (neighbor_scale * n_neighbors). Values > 1.0 create denser graphs with more connectivity, potentially capturing more global structure but at increased computational cost. Values < 1.0 create sparser graphs focused on local structure. random_state : np.random.RandomState or None, default=None The random state to use for the random number generator. If None, the random number generator will not be seeded and will use the system time as the seed. reproducible_flag : bool, default=True Whether to ensure reproducible results by using deterministic algorithms where possible. When True, the clustering results should be consistent across runs with the same random_state. min_similarity_threshold : float, default=0.2 The minimum similarity threshold for cluster layer selection. Peaks that result in clusterings with Jaccard similarity above this threshold will be filtered out to ensure diverse cluster layers. max_layers : int, default=10 The maximum number of cluster layers to return. The algorithm will select up to this many diverse peaks based on persistence and similarity criteria. n_label_prop_iter : int, default=20 The number of iterations to use in the label propagation algorithm when initializing the node embedding. Returns ------- cluster_layers : list of array-like of shape (n_samples,) The clustering of the data at each layer of the clustering. Each layer is a clustering of the data into a different number of clusters. membership_strengths : list of array-like of shape (n_samples,) The membership strengths of each point in the clustering at each layer. This gives a measure of how strongly each point belongs to each cluster. nn_inds : array-like of shape (n_samples, n_neighbors) Indices of nearest neighbors for each sample. nn_dists : array-like of shape (n_samples, n_neighbors) Distance from each sample to each nearest neighbor indexed by nn_inds duplicates : set of tuple of int Only returned in ``return_duplicates`` is True. A set of pairs of indices of potential duplicate points in the data. """ if random_state is None: random_state = np.random.RandomState() nn_inds, nn_dists = knn_graph( data, n_neighbors=n_neighbors, random_state=random_state ) graph = neighbor_graph_matrix( neighbor_scale * n_neighbors, nn_inds, nn_dists, symmetrize_graph ) n_embedding_components = node_embedding_dim or min(max(n_neighbors // 4, 4), 15) if node_embedding_init == "label_prop": init_embedding = label_propagation_init( graph, n_components=n_embedding_components, approx_n_parts=np.clip(int(8 * np.sqrt(data.shape[0])), 256, 16384), random_scale=0.1, scaling=0.5, noise_level=noise_level, random_state=random_state, data=data, n_label_prop_iter=n_label_prop_iter, ) elif node_embedding_init is None: init_embedding = None embedding = node_embedding( graph, n_components=n_embedding_components, n_epochs=n_epochs, initial_embedding=init_embedding, negative_sample_rate=1.0, noise_level=noise_level, random_state=random_state, verbose=False, reproducible_flag=reproducible_flag, initial_alpha=0.1, ) if return_duplicates: duplicates = find_duplicates(nn_inds, nn_dists) n_threads = numba.get_num_threads() if approx_n_clusters is not None: cluster_vector, strengths = binary_search_for_n_clusters( embedding, approx_n_clusters, n_threads, min_samples=min_samples, ) if return_duplicates: return [cluster_vector], [strengths], [0.0], nn_inds, nn_dists, duplicates else: return [cluster_vector], [strengths], [0.0], nn_inds, nn_dists else: cluster_layers, membership_strengths, persistence_scores = build_cluster_layers( embedding, min_samples=min_samples, base_min_cluster_size=base_min_cluster_size, base_n_clusters=base_n_clusters, reproducible_flag=reproducible_flag, min_similarity_threshold=min_similarity_threshold, max_layers=max_layers, ) if return_duplicates: return ( cluster_layers, membership_strengths, persistence_scores, nn_inds, nn_dists, duplicates, ) else: return ( cluster_layers, membership_strengths, persistence_scores, nn_inds, nn_dists, ) class EVoC(BaseEstimator, ClusterMixin): """ Embedding Vector Oriented Clustering for efficient clustering of high-dimensional embedding vectors such as CLIP-vectors, sentence-transformers output, etc. The clustering uses a combination of a node embedding of a nearest neighbour graph, related to UMAP, and a density based clustering approach related to HDBSCAN, improving upon those approaches in efficiency and quality for the specific case of high-dimensional embedding vectors. Parameters ---------- noise_level : float, default=0.5 The noise level expected in the data. A value of 0.0 will try to cluster more data, at the expense of getting less accurate clustering. A value of 1.0 will try for accurate clusters, discarding more data as noise to do so. base_min_cluster_size : int, default=5 The minimum number of points in a cluster at the base layer of the clustering. This gives the finest granularity clustering that will be returned, with less graularity at higher layers. base_n_clusters : int or None, default=None If not None, the algorithm will attempt to find the granularity of clustering that will give exactly this many clusters for the bottom-most layer of clustering. This affects the base layer computation and allows multiple layers to be built on top of this base. Since the actual number of clusters cannot be guaranteed this is only approximate, but usually the algorithm can manage to get this exact number, assuming a reasonable clustering into ``base_n_clusters`` exists. approx_n_clusters : int, default=None If not None, the algorithm will attempt to find the granularity of clustering that will give exactly this many clusters as the final output. Unlike ``base_n_clusters``, when this parameter is set, only a single clustering layer will be returned -- no hierarchical layers will be produced. This is useful when you know the exact number of clusters you want and don't need the multi-layer analysis. Since the actual number of clusters cannot be guaranteed this is only approximate, but usually the algorithm can manage to get this exact number, assuming a reasonable clustering into ``approx_n_clusters`` exists. n_neighbors : int, default=15 The number of neighbors to use in the nearest neighbor graph construction. min_samples : int, default=5 The minimum number of samples to use in the density estimation when performing density based clustering on the node embedding. n_epochs : int, default=50 The number of epochs to use when training the node embedding. node_embedding_init : str or None, default='label_prop' The method to use to initialize the node embedding. If None, no initialization will be used. If 'label_prop', the label propagation method will be used. symmetrize_graph : bool, default=True Whether to symmetrize the nearest neighbor graph before using it to construct the node embedding. node_embedding_dim : int or None, default=None The number of dimensions to use in the node embedding. If None, a default value of min(max(n_neighbors // 4, 4), 15) will be used. neighbor_scale : float, default=1.0 The scale factor to use when constructing the nearest neighbor graph. This multiplies the effective number of neighbors used in graph construction (neighbor_scale * n_neighbors). Values > 1.0 create denser graphs with more connectivity, potentially capturing more global structure but at increased computational cost. Values < 1.0 create sparser graphs focused on local structure. random_state : int or None, default=None The random seed to use for the random number generator. If None, the random number generator will not be seeded and will use the system time as the seed. min_similarity_threshold : float, default=0.2 The minimum similarity threshold for cluster layer selection. Peaks that result in clusterings with Jaccard similarity above this threshold will be filtered out to ensure diverse cluster layers. max_layers : int, default=10 The maximum number of cluster layers to return. The algorithm will select up to this many diverse peaks based on persistence and similarity criteria. n_label_prop_iter : int, default=20 The number of iterations to use in the label propagation algorithm when initializing the node embedding. This parameter controls how many steps the label propagation process takes to converge when node_embedding_init is set to 'label_prop'. Attributes ---------- labels_ : array-like of shape (n_samples,) An array of labels for the data samples; this is a integer array as per other scikit-learn clustering algorithms. A value of -1 indicates that a point is a noise point and not in any cluster. membership_strengths_ : array-like of shape (n_samples,) An array of membership strengths for the data samples; this gives a measure of how strongly each point belongs to each cluster. This is a floating point array with values between 0 and 1. cluster_layers_ : list of array-like of shape (n_samples,) The clustering of the data at each layer of the clustering. Each layer is a clustering of the data into a different number of clusters; the earlier the cluster vector is in this list the finer the granularity of clustering. membership_strength_layers_ : list of array-like of shape (n_samples,) The membership strengths of each point in the clustering at each layer. cluster_tree_ : dict A dictionary representing the hierarchical clustering of the data. The keys are tuples of (layer, cluster) and the values are lists of tuples of (layer, cluster) representing the children of the key cluster. nn_inds_ : array-like of shape (n_samples, n_neighbors) Indices of nearest neighbors for each sample. nn_dists_ : array-like of shape (n_samples, n_neighbors) Distance from each sample to each nearest neighbor (indexed by nn_inds). duplicates_ : set of tuple of int A set of pairs of indices of potential duplicate points in the data. """ def __init__( self, noise_level: float = 0.5, base_min_cluster_size: int = 5, base_n_clusters: int | None = None, approx_n_clusters: int | None = None, n_neighbors: int = 15, min_samples: int = 5, n_epochs: int = 50, node_embedding_init: str | None = "label_prop", symmetrize_graph: bool = True, node_embedding_dim: int | None = None, neighbor_scale: float = 1.0, random_state: int | None = None, min_similarity_threshold: float = 0.2, max_layers: int = 10, n_label_prop_iter=20, ) -> None: self.n_neighbors = n_neighbors self.noise_level = noise_level self.base_min_cluster_size = base_min_cluster_size self.base_n_clusters = base_n_clusters self.approx_n_clusters = approx_n_clusters self.min_samples = min_samples self.n_epochs = n_epochs self.node_embedding_init = node_embedding_init self.symmetrize_graph = symmetrize_graph self.node_embedding_dim = node_embedding_dim self.neighbor_scale = neighbor_scale self.random_state = random_state self.min_similarity_threshold = min_similarity_threshold self.max_layers = max_layers self.n_label_prop_iter = n_label_prop_iter def fit_predict(self, X, y=None, **fit_params): """Fit the model to the data and return the clustering labels. Parameters ---------- X : array-like of shape (n_samples, n_features) The data to cluster. If the data is float valued then it is assumed to use cosine distance as a matric. If the data is int8 valued then it is assumed that a quantized embedding is being used and a quantized version of cosine distance is used. If the data is uint8 valued then it is assumed that a binary embedding is being used, and a bitwise Jaccard distance is used. y : array-like of shape (n_samples,), default=None Ignored. This parameter exists only for compatibility with scikit-learn's fit_predict method. **fit_params : dict Additional fit parameters. Currently unused, included for compatibility with scikit-learn's fit_predict interface. Returns ------- labels_ : array-like of shape (n_samples,) An array of labels for the data samples; this is a integer array as per other scikit-learn clustering algorithms. A value of -1 indicates that a point is a noise point and not in any cluster. """ X = check_array(X) current_random_state = check_random_state(self.random_state) ( self.cluster_layers_, self.membership_strength_layers_, self.persistence_scores_, self.nn_inds_, self.nn_dists_, self.duplicates_, ) = evoc_clusters( X, n_neighbors=self.n_neighbors, noise_level=self.noise_level, base_min_cluster_size=self.base_min_cluster_size, base_n_clusters=self.base_n_clusters, approx_n_clusters=self.approx_n_clusters, min_samples=self.min_samples, n_epochs=self.n_epochs, node_embedding_init=self.node_embedding_init, symmetrize_graph=self.symmetrize_graph, return_duplicates=True, node_embedding_dim=self.node_embedding_dim, neighbor_scale=self.neighbor_scale, random_state=current_random_state, reproducible_flag=self.random_state is not None, min_similarity_threshold=self.min_similarity_threshold, max_layers=self.max_layers, n_label_prop_iter=self.n_label_prop_iter, ) if len(self.cluster_layers_) == 1: self.labels_ = self.cluster_layers_[0] self.membership_strengths_ = self.membership_strength_layers_[0] else: best_layer = np.argmax(self.persistence_scores_) self.labels_ = self.cluster_layers_[best_layer] self.membership_strengths_ = self.membership_strength_layers_[best_layer] return self.labels_ def fit(self, X, y=None, **fit_params): """Fit the model to the data. Parameters ---------- X : array-like of shape (n_samples, n_features) The data to cluster. If the data is float valued then it is assumed to use cosine distance as a matric. If the data is int8 valued then it is assumed that a quantized embedding is being used and a quantized version of cosine distance is used. If the data is uint8 valued then it is assumed that a binary embedding is being used, and a bitwise Jaccard distance is used. y : array-like of shape (n_samples,), default=None Ignored. This parameter exists only for compatibility with scikit-learn's fit method. **fit_params : dict Additional fit parameters. Currently unused, included for compatibility with scikit-learn's fit interface. Returns ------- self : sklearn Estimator Returns the instance itself. """ self.fit_predict(X, y, **fit_params) return self @property def cluster_tree_(self): """dict A dictionary representing the hierarchical clustering of the data. The keys are tuples of (layer, cluster) and the values are lists of tuples of (layer, cluster) representing the children of the key cluster. This provides a tree structure showing how clusters at different layers relate to each other hierarchically. Only available after fitting the model. Returns ------- dict Hierarchical tree structure with (layer, cluster) tuples as keys and lists of child (layer, cluster) tuples as values. Raises ------ NotFittedError If the model has not been fitted yet. """ check_is_fitted( self, "cluster_layers_", msg="This %(name)s instance is not fitted yet, and 'cluster_tree_' is not available. " "Please call 'fit' with appropriate arguments before accessing this attribute.", ) if not hasattr(self, "_cluster_tree"): self._cluster_tree = build_cluster_tree(self.cluster_layers_) return self._cluster_tree ================================================ FILE: evoc/clustering_utilities.py ================================================ import numpy as np import numba from .numba_kdtree import build_kdtree from .boruvka import parallel_boruvka from .cluster_trees import ( mst_to_linkage_tree, condense_tree, extract_leaves, get_cluster_label_vector, get_point_membership_strength_vector, ) ############################################################## # Directly derived from scipy's find_peaks function: # https://github.com/scipy/scipy/blob/bd66693b8aecc6f528ca9b1cfd6bb1f61477ca0f/scipy/signal/_peak_finding_utils.pyx#L20 ############################################################## @numba.njit( ["intp[:](float32[::1])", "intp[:](float64[::1])"], locals={ "midpoints": numba.types.intp[::1], "left_edges": numba.types.intp[::1], "right_edges": numba.types.intp[::1], "m": numba.types.uint32, "i": numba.types.uint32, }, nogil=True, parallel=False, fastmath=True, cache=True, ) def find_peaks(x): # Preallocate, there can't be more maxima than half the size of `x` midpoints = np.empty(x.shape[0] // 2, dtype=np.intp) left_edges = np.empty(x.shape[0] // 2, dtype=np.intp) right_edges = np.empty(x.shape[0] // 2, dtype=np.intp) m = 0 # Pointer to the end of valid area in allocated arrays i = 1 # Pointer to current sample, first one can't be maxima i_max = x.shape[0] - 1 # Last sample can't be maxima while i < i_max: # Test if previous sample is smaller if x[i - 1] < x[i]: i_ahead = i + 1 # Index to look ahead of current sample # Find next sample that is unequal to x[i] while i_ahead < i_max and x[i_ahead] == x[i]: i_ahead += 1 # Maxima is found if next unequal sample is smaller than x[i] if x[i_ahead] < x[i]: left_edges[m] = i right_edges[m] = i_ahead - 1 midpoints[m] = (left_edges[m] + right_edges[m]) // 2 m += 1 # Skip samples that can't be maximum i = i_ahead i += 1 return midpoints[:m] @numba.njit(cache=True) def _binary_search_for_n_clusters(uncondensed_tree, approx_n_clusters, n_samples): lower_bound_min_cluster_size = 2 upper_bound_min_cluster_size = n_samples // 2 mid_min_cluster_size = int( round((lower_bound_min_cluster_size + upper_bound_min_cluster_size) / 2.0) ) min_n_clusters = 0 upper_tree = condense_tree(uncondensed_tree, upper_bound_min_cluster_size) leaves = extract_leaves(upper_tree) upper_n_clusters = len(leaves) lower_tree = condense_tree(uncondensed_tree, lower_bound_min_cluster_size) leaves = extract_leaves(lower_tree) lower_n_clusters = len(leaves) while upper_bound_min_cluster_size - lower_bound_min_cluster_size > 1: mid_min_cluster_size = int( round((lower_bound_min_cluster_size + upper_bound_min_cluster_size) / 2.0) ) if ( mid_min_cluster_size == lower_bound_min_cluster_size or mid_min_cluster_size == upper_bound_min_cluster_size ): break mid_tree = condense_tree(uncondensed_tree, mid_min_cluster_size) leaves = extract_leaves(mid_tree) mid_n_clusters = len(leaves) if mid_n_clusters < approx_n_clusters: upper_bound_min_cluster_size = mid_min_cluster_size upper_n_clusters = mid_n_clusters elif mid_n_clusters >= approx_n_clusters: lower_bound_min_cluster_size = mid_min_cluster_size lower_n_clusters = mid_n_clusters if abs(lower_n_clusters - approx_n_clusters) < abs( upper_n_clusters - approx_n_clusters ): lower_tree = condense_tree(uncondensed_tree, lower_bound_min_cluster_size) leaves = extract_leaves(lower_tree) clusters = get_cluster_label_vector(lower_tree, leaves, 0.0, n_samples) strengths = get_point_membership_strength_vector(lower_tree, leaves, clusters) return leaves, clusters, strengths elif abs(lower_n_clusters - approx_n_clusters) > abs( upper_n_clusters - approx_n_clusters ): upper_tree = condense_tree(uncondensed_tree, upper_bound_min_cluster_size) leaves = extract_leaves(upper_tree) clusters = get_cluster_label_vector(upper_tree, leaves, 0.0, n_samples) strengths = get_point_membership_strength_vector(upper_tree, leaves, clusters) return leaves, clusters, strengths else: lower_tree = condense_tree(uncondensed_tree, lower_bound_min_cluster_size) lower_leaves = extract_leaves(lower_tree) lower_clusters = get_cluster_label_vector( lower_tree, lower_leaves, 0.0, n_samples ) upper_tree = condense_tree(uncondensed_tree, upper_bound_min_cluster_size) upper_leaves = extract_leaves(upper_tree) upper_clusters = get_cluster_label_vector( upper_tree, upper_leaves, 0.0, n_samples ) if np.sum(lower_clusters >= 0) > np.sum(upper_clusters >= 0): strengths = get_point_membership_strength_vector( lower_tree, lower_leaves, lower_clusters ) return lower_leaves, lower_clusters, strengths else: strengths = get_point_membership_strength_vector( upper_tree, upper_leaves, upper_clusters ) return upper_leaves, upper_clusters, strengths # @numba.njit(cache=True) def binary_search_for_n_clusters( data, approx_n_clusters, n_threads, *, min_samples=5, ): numba_tree = build_kdtree(data.astype(np.float32)) edges = parallel_boruvka( numba_tree, n_threads, min_samples=min_samples, reproducible=False ) sorted_mst = edges[np.argsort(edges.T[2])] uncondensed_tree = mst_to_linkage_tree(sorted_mst) n_samples = data.shape[0] leaves, clusters, strengths = _binary_search_for_n_clusters( uncondensed_tree, approx_n_clusters, n_samples ) return clusters, strengths @numba.njit(cache=True) def min_cluster_size_barcode(cluster_tree, n_points, min_size): n_nodes = cluster_tree.child[-1] - n_points + 1 parents = np.empty(n_nodes, dtype=np.int32) lambda_deaths = np.empty(n_nodes, dtype=np.float32) size_deaths = np.empty(n_nodes, dtype=np.float32) size_births = np.full(n_nodes, min_size, dtype=np.float32) lambda_deaths[0] = 0 size_deaths[0] = n_points parents[0] = n_points # Iterate over row-pairs in reverse order n_rows = cluster_tree.child.shape[0] for idx in range(n_rows - 1, 0, -2): out_idx = cluster_tree.child[idx] - n_points parents[out_idx - 1 : out_idx + 1] = cluster_tree.parent[idx] lambda_deaths[out_idx - 1 : out_idx + 1] = np.exp( -1 / cluster_tree.lambda_val[idx] ) death_size = cluster_tree.child_size[idx - 1 : idx + 1].min() size_deaths[out_idx - 1 : out_idx + 1] = death_size size_births[cluster_tree.parent[idx] - n_points] = max( size_births[out_idx - 1], size_births[out_idx], death_size ) return size_births, size_deaths, parents, lambda_deaths @numba.njit(cache=True) def compute_total_persistence(births, deaths, lambda_deaths): # maintain left-open (birth, death] interval! sizes = np.unique(births) total_persistence = np.zeros(sizes.shape[0], dtype=np.float32) for i in range(1, len(births)): birth = births[i] death = deaths[i] lambda_death = lambda_deaths[i] if death <= birth: continue # Manual binary search for birth_idx birth_idx = 0 for j in range(len(sizes)): if sizes[j] >= birth: birth_idx = j break # Manual binary search for death_idx death_idx = len(sizes) for j in range(len(sizes)): if sizes[j] >= death: death_idx = j break # Update persistence values for k in range(birth_idx, death_idx): total_persistence[k] += (death - birth) * lambda_death return sizes, total_persistence @numba.njit(cache=True) def extract_clusters_by_id(condensed_tree, selected_ids): labels = get_cluster_label_vector( condensed_tree, selected_ids, cluster_selection_epsilon=0.0, n_samples=condensed_tree.parent[0], ) strengths = get_point_membership_strength_vector( condensed_tree, selected_ids, labels ) return labels, strengths @numba.njit(cache=True) def jaccard_similarity(set_a_array, set_b_array): # Convert to sets for intersection/union operations intersection_count = 0 union_set = set(set_a_array) for item in set_b_array: if item in union_set: intersection_count += 1 else: union_set.add(item) union_count = len(union_set) return intersection_count / union_count if union_count > 0 else 0.0 @numba.njit(cache=True) def estimate_cluster_similarity(births, deaths, birth_a, birth_b): # Find clusters active at birth_a clusters_a = np.empty(len(births), dtype=np.int64) count_a = 0 for i in range(len(births)): if births[i] <= birth_a and deaths[i] > birth_a: clusters_a[count_a] = i count_a += 1 # Find clusters active at birth_b clusters_b = np.empty(len(births), dtype=np.int64) count_b = 0 for i in range(len(births)): if births[i] <= birth_b and deaths[i] > birth_b: clusters_b[count_b] = i count_b += 1 # Trim arrays to actual sizes active_a = clusters_a[:count_a] active_b = clusters_b[:count_b] return jaccard_similarity(active_a, active_b) @numba.njit(cache=True) def select_diverse_peaks( peaks, total_persistence, sizes, births, deaths, min_similarity_threshold=0.2, max_layers=10, ): if len(peaks) == 0: return np.empty(0, dtype=np.int64) # Sort peaks by persistence (highest first) peak_persistence = total_persistence[peaks] sorted_indices = np.argsort(peak_persistence)[::-1] sorted_peaks = peaks[sorted_indices] # Pre-allocate arrays for selected peaks and births selected_peaks = np.empty(max_layers, dtype=np.int64) selected_births = np.empty(max_layers, dtype=np.float64) n_selected = 0 for i in range(len(sorted_peaks)): if n_selected >= max_layers: break peak = sorted_peaks[i] birth_size = sizes[peak] # Check similarity with already selected peaks is_diverse = True for j in range(n_selected): selected_birth = selected_births[j] similarity = estimate_cluster_similarity( births, deaths, birth_size, selected_birth ) if similarity > min_similarity_threshold: is_diverse = False break if is_diverse: selected_peaks[n_selected] = peak selected_births[n_selected] = birth_size n_selected += 1 return selected_peaks[:n_selected] @numba.njit(cache=True) def _build_cluster_tree(labels): mapping = [(-1, -1, -1, -1) for i in range(0)] found = [set([-1]) for i in range(len(labels))] mapping_idx = 0 for upper_layer in range(1, len(labels)): upper_layer_unique_labels = np.unique(labels[upper_layer]) for lower_layer in range(upper_layer - 1, -1, -1): upper_cluster_order = np.argsort(labels[upper_layer]) cluster_groups = np.split( labels[lower_layer][upper_cluster_order], np.cumsum(np.bincount(labels[upper_layer] + 1))[:-1], ) for i, label in enumerate(upper_layer_unique_labels): if label >= 0: for child in cluster_groups[i]: if child >= 0 and child not in found[lower_layer]: mapping.append((upper_layer, label, lower_layer, child)) found[lower_layer].add(child) for lower_layer in range(len(labels) - 1, -1, -1): for child in range(labels[lower_layer].max() + 1): if child >= 0 and child not in found[lower_layer]: mapping.append((len(labels), 0, lower_layer, child)) return mapping def build_cluster_tree(labels): result = {} raw_mapping = _build_cluster_tree(labels) for parent_layer, parent_cluster, child_layer, child_cluster in raw_mapping: parent_name = (parent_layer, parent_cluster) if parent_name in result: result[parent_name].append((child_layer, child_cluster)) else: result[parent_name] = [(child_layer, child_cluster)] return result @numba.njit(cache=True) def find_duplicates(knn_inds, knn_dists): duplicate_distance = np.max(knn_dists.T[0]) duplicates = set([(-1, -1) for i in range(0)]) for i in range(knn_inds.shape[0]): for j in range(0, knn_inds.shape[1]): if knn_dists[i, j] <= duplicate_distance: k = knn_inds[i, j] if i < k: duplicates.add((i, k)) elif k < i: duplicates.add((k, i)) else: continue return duplicates ================================================ FILE: evoc/common_nndescent.py ================================================ import numpy as np import numba @numba.njit("void(i8[:], i8)", cache=True) def seed(rng_state, seed): """Seed the random number generator with a given seed.""" rng_state.fill(seed + 0xFFFF) @numba.njit("i4(i8[:])", cache=True) def tau_rand_int(state): """A fast (pseudo)-random number generator. Parameters ---------- state: array of int64, shape (3,) The internal state of the rng Returns ------- A (pseudo)-random int32 value """ state[0] = (((state[0] & 4294967294) << 12) & 0xFFFFFFFF) ^ ( (((state[0] << 13) & 0xFFFFFFFF) ^ state[0]) >> 19 ) state[1] = (((state[1] & 4294967288) << 4) & 0xFFFFFFFF) ^ ( (((state[1] << 2) & 0xFFFFFFFF) ^ state[1]) >> 25 ) state[2] = (((state[2] & 4294967280) << 17) & 0xFFFFFFFF) ^ ( (((state[2] << 3) & 0xFFFFFFFF) ^ state[2]) >> 11 ) return state[0] ^ state[1] ^ state[2] @numba.njit("f4(i8[:])", cache=True) def tau_rand(state): """A fast (pseudo)-random number generator for floats in the range [0,1] Parameters ---------- state: array of int64, shape (3,) The internal state of the rng Returns ------- A (pseudo)-random float32 in the interval [0, 1] """ integer = tau_rand_int(state) return abs(float(integer) / 0x7FFFFFFF) # @numba.njit(cache=True) def make_heap(n_points, size): indices = np.full((int(n_points), int(size)), -1, dtype=np.int32) distances = np.full((int(n_points), int(size)), np.inf, dtype=np.float32) flags = np.zeros((int(n_points), int(size)), dtype=np.uint8) result = (indices, distances, flags) return result @numba.njit(cache=True) def siftdown(heap1, heap2, elt): """Restore the heap property for a heap with an out of place element at position ``elt``. This works with a heap pair where heap1 carries the weights and heap2 holds the corresponding elements.""" while elt * 2 + 1 < heap1.shape[0]: left_child = elt * 2 + 1 right_child = left_child + 1 swap = elt if heap1[swap] < heap1[left_child]: swap = left_child if right_child < heap1.shape[0] and heap1[swap] < heap1[right_child]: swap = right_child if swap == elt: break else: heap1[elt], heap1[swap] = heap1[swap], heap1[elt] heap2[elt], heap2[swap] = heap2[swap], heap2[elt] elt = swap @numba.njit(parallel=True, cache=True) def deheap_sort(indices, distances): """Given two arrays representing a heap (indices and distances), reorder the arrays by increasing distance. This is effectively just the second half of heap sort (the first half not being required since we already have the graph_data in a heap). Note that this is done in-place. Parameters ---------- indices : array of shape (n_samples, n_neighbors) The graph indices to sort by distance. distances : array of shape (n_samples, n_neighbors) The corresponding edge distance. Returns ------- indices, distances: arrays of shape (n_samples, n_neighbors) The indices and distances sorted by increasing distance. """ for i in numba.prange(indices.shape[0]): # starting from the end of the array and moving back for j in range(indices.shape[1] - 1, 0, -1): indices[i, 0], indices[i, j] = indices[i, j], indices[i, 0] distances[i, 0], distances[i, j] = distances[i, j], distances[i, 0] siftdown(distances[i, :j], indices[i, :j], 0) return indices, distances @numba.njit( "i4(f4[::1],i4[::1],f4,i4)", fastmath=True, locals={ "size": numba.types.intp, "i": numba.types.uint16, "ic1": numba.types.uint16, "ic2": numba.types.uint16, "i_swap": numba.types.uint16, }, cache=True, ) def build_candidates_heap_push(priorities, indices, p, n): if p >= priorities[0]: return 0 size = priorities.shape[0] # break if we already have this element. for i in range(size): if n == indices[i]: return 0 # insert val at position zero priorities[0] = p indices[0] = n # descend the heap, swapping values until the max heap criterion is met i = 0 while True: ic1 = 2 * i + 1 ic2 = ic1 + 1 if ic1 >= size: break elif ic2 >= size: if priorities[ic1] > p: i_swap = ic1 else: break elif priorities[ic1] >= priorities[ic2]: if p < priorities[ic1]: i_swap = ic1 else: break else: if p < priorities[ic2]: i_swap = ic2 else: break priorities[i] = priorities[i_swap] indices[i] = indices[i_swap] i = i_swap priorities[i] = p indices[i] = n return 1 @numba.njit(parallel=True, locals={"idx": numba.types.int64}, cache=True) def build_candidates(current_graph, max_candidates, rng_state, n_threads): """Build a heap of candidate neighbors for nearest neighbor descent. For each vertex the candidate neighbors are any current neighbors, and any vertices that have the vertex as one of their nearest neighbors. Parameters ---------- current_graph: heap The current state of the graph for nearest neighbor descent. max_candidates: int The maximum number of new candidate neighbors. rng_state: array of int64, shape (3,) The internal state of the rng Returns ------- candidate_neighbors: A heap with an array of (randomly sorted) candidate neighbors for each vertex in the graph. """ current_indices = current_graph[0] current_flags = current_graph[2] n_vertices = current_indices.shape[0] n_neighbors = current_indices.shape[1] new_candidate_indices = np.full((n_vertices, max_candidates), -1, dtype=np.int32) new_candidate_priority = np.full( (n_vertices, max_candidates), np.inf, dtype=np.float32 ) old_candidate_indices = np.full((n_vertices, max_candidates), -1, dtype=np.int32) old_candidate_priority = np.full( (n_vertices, max_candidates), np.inf, dtype=np.float32 ) block_size = n_vertices // n_threads + 1 for n in numba.prange(n_threads): local_rng_state = rng_state + n block_start = n * block_size block_end = min(block_start + block_size, n_vertices) for i in range(n_vertices): for j in range(n_neighbors): idx = current_indices[i, j] if idx >= 0 and ( (i >= block_start and i < block_end) or (idx >= block_start and idx < block_end) ): isn = current_flags[i, j] d = tau_rand(local_rng_state) if isn: if i >= block_start and i < block_end: build_candidates_heap_push( new_candidate_priority[i], new_candidate_indices[i], d, idx, ) if idx >= block_start and idx < block_end: build_candidates_heap_push( new_candidate_priority[idx], new_candidate_indices[idx], d, i, ) else: if i >= block_start and i < block_end: build_candidates_heap_push( old_candidate_priority[i], old_candidate_indices[i], d, idx, ) if idx >= block_start and idx < block_end: build_candidates_heap_push( old_candidate_priority[idx], old_candidate_indices[idx], d, i, ) indices = current_graph[0] flags = current_graph[2] for i in numba.prange(n_vertices): for j in range(n_neighbors): idx = indices[i, j] for k in range(max_candidates): if new_candidate_indices[i, k] == idx: flags[i, j] = 0 break return new_candidate_indices, old_candidate_indices @numba.njit( "i4(f4[::1],i4[::1],u1[::1],f4,i4)", fastmath=True, locals={ "size": numba.types.intp, "i": numba.types.uint16, "ic1": numba.types.uint16, "ic2": numba.types.uint16, "i_swap": numba.types.uint16, }, cache=True, ) def flagged_heap_push(priorities, indices, flags, p, n): if p >= priorities[0]: return 0 size = priorities.shape[0] # break if we already have this element. for i in range(size): if n == indices[i]: return 0 # insert val at position zero priorities[0] = p indices[0] = n # descend the heap, swapping values until the max heap criterion is met i = 0 while True: ic1 = 2 * i + 1 ic2 = ic1 + 1 if ic1 >= size: break elif ic2 >= size: if priorities[ic1] > p: i_swap = ic1 else: break elif priorities[ic1] >= priorities[ic2]: if p < priorities[ic1]: i_swap = ic1 else: break else: if p < priorities[ic2]: i_swap = ic2 else: break priorities[i] = priorities[i_swap] indices[i] = indices[i_swap] flags[i] = flags[i_swap] i = i_swap priorities[i] = p indices[i] = n flags[i] = 1 return 1 @numba.njit( numba.uint32( numba.types.Tuple( (numba.int32[:, ::1], numba.float32[:, ::1], numba.uint8[:, ::1]) ), numba.float32[:, :, ::1], numba.int32[::1], numba.int64, ), parallel=True, locals={ "p": numba.int32, "q": numba.int32, "d": numba.float32, "added": numba.uint8, "n": numba.uint32, "i": numba.uint32, "j": numba.uint32, "priorities": numba.float32[:, ::1], "indices": numba.int32[:, ::1], "flags": numba.uint8[:, ::1], }, cache=True, ) def apply_graph_update_array( current_graph, update_array, n_updates_per_thread, n_threads ): n_changes = 0 priorities = current_graph[1] indices = current_graph[0] flags = current_graph[2] n_vertices = priorities.shape[0] block_size = n_vertices // n_threads + 1 for n in numba.prange(n_threads): block_start = n * block_size block_end = min(block_start + block_size, n_vertices) for i in range(update_array.shape[0]): for j in range(n_updates_per_thread[i]): p = np.int32(update_array[i, j, 0]) if p == -1: break q = np.int32(update_array[i, j, 1]) d = np.float32(update_array[i, j, 2]) if p >= block_start and p < block_end: added = flagged_heap_push(priorities[p], indices[p], flags[p], d, q) n_changes += added if q >= block_start and q < block_end: added = flagged_heap_push(priorities[q], indices[q], flags[q], d, p) n_changes += added return n_changes @numba.njit( numba.uint32( numba.types.Tuple( (numba.int32[:, ::1], numba.float32[:, ::1], numba.uint8[:, ::1]) ), numba.float32[:, :, ::1], numba.int32[:, ::1], numba.int64, ), parallel=True, cache=True, locals={ "p": numba.int32, "q": numba.int32, "d": numba.float32, "added": numba.uint8, "n": numba.uint32, "t": numba.uint32, "j": numba.uint32, "priorities": numba.float32[:, ::1], "indices": numba.int32[:, ::1], "flags": numba.uint8[:, ::1], }, ) def apply_sorted_graph_updates( current_graph, update_array, n_updates_per_block, n_threads ): """ Apply pre-sorted graph updates where updates are bucketed by target block. Each thread processes only its own bucket, avoiding the need to scan all updates. This provides O(updates_per_block) work per thread instead of O(total_updates). """ n_changes = 0 priorities = current_graph[1] indices = current_graph[0] flags = current_graph[2] n_vertices = priorities.shape[0] vertex_block_size = n_vertices // n_threads + 1 max_updates_per_thread = update_array.shape[1] // n_threads for n in numba.prange(n_threads): block_start = n * vertex_block_size block_end = min(block_start + vertex_block_size, n_vertices) # Process all updates in this block's bucket # Updates were written by each thread at offset t * max_updates_per_thread for t in range(n_threads): thread_start = t * max_updates_per_thread thread_count = n_updates_per_block[n, t + 1] for j in range(thread_count): idx = thread_start + j p = np.int32(update_array[n, idx, 0]) q = np.int32(update_array[n, idx, 1]) d = np.float32(update_array[n, idx, 2]) # Apply update to p if it's in this block if p >= block_start and p < block_end: added = flagged_heap_push(priorities[p], indices[p], flags[p], d, q) n_changes += added # Apply update to q if it's in this block if q >= block_start and q < block_end: added = flagged_heap_push(priorities[q], indices[q], flags[q], d, p) n_changes += added return n_changes ================================================ FILE: evoc/disjoint_set.py ================================================ import numba import numpy as np from collections import namedtuple RankDisjointSet = namedtuple("RankDisjointSet", ["parent", "rank"]) SizeDisjointSet = namedtuple("SizeDisjointSet", ["parent", "size"]) _sentinel_rank_ds = RankDisjointSet( parent=np.empty(1, dtype=np.int32), rank=np.empty(1, dtype=np.int32), ) _sentinel_size_ds = SizeDisjointSet( parent=np.empty(1, dtype=np.int32), size=np.empty(1, dtype=np.int32), ) RankDisjointSetType = numba.typeof(_sentinel_rank_ds) SizeDisjointSetType = numba.typeof(_sentinel_size_ds) @numba.njit(cache=True) def ds_rank_create(n_elements): return RankDisjointSet( np.arange(n_elements, dtype=np.int32), np.zeros(n_elements, dtype=np.int32) ) @numba.njit(cache=True) def ds_size_create(n_elements): return SizeDisjointSet( np.arange(n_elements, dtype=np.int32), np.ones(n_elements, dtype=np.int32) ) @numba.njit(cache=True) def ds_find(disjoint_set, x): while disjoint_set.parent[x] != x: x, disjoint_set.parent[x] = ( disjoint_set.parent[x], disjoint_set.parent[disjoint_set.parent[x]], ) return x @numba.njit( numba.void( RankDisjointSetType, numba.int32, numba.int32, ), cache=True, ) def ds_union_by_rank(disjoint_set, x, y): x = ds_find(disjoint_set, x) y = ds_find(disjoint_set, y) if x == y: return if disjoint_set.rank[x] < disjoint_set.rank[y]: x, y = y, x disjoint_set.parent[y] = x if disjoint_set.rank[x] == disjoint_set.rank[y]: disjoint_set.rank[x] += 1 @numba.njit( numba.void( SizeDisjointSetType, numba.int32, numba.int32, ), cache=True, ) def ds_union_by_size(disjoint_set, x, y): x = ds_find(disjoint_set, x) y = ds_find(disjoint_set, y) if x == y: return if disjoint_set.size[x] < disjoint_set.size[y]: x, y = y, x disjoint_set.parent[y] = x disjoint_set.size[x] += disjoint_set.size[y] ================================================ FILE: evoc/float_nndescent.py ================================================ import numba import numpy as np from .common_nndescent import ( tau_rand_int, make_heap, deheap_sort, flagged_heap_push, build_candidates, apply_graph_update_array, apply_sorted_graph_updates, ) from .nested_parallelism import ENABLE_NESTED_PARALLELISM # Used for a floating point "nearly zero" comparison EPS = 1e-8 INF = np.finfo(np.float32).max EXP_NEG_INF = np.finfo(np.float32).tiny INT32_MIN = np.iinfo(np.int32).min + 1 INT32_MAX = np.iinfo(np.int32).max - 1 point_indices_type = numba.int32[::1] @numba.njit( [ "f4(f4[::1],f4[::1])", numba.types.float32( numba.types.Array(numba.types.float32, 1, "C", readonly=True), numba.types.Array(numba.types.float32, 1, "C", readonly=True), ), ], fastmath=True, locals={ "result": numba.types.float32, "dim": numba.types.intp, "i": numba.types.uint16, }, boundscheck=False, nogil=True, cache=True, ) def fast_cosine(x, y): """ Calculates the cosine similarity between two vectors. Args: x (numpy.ndarray): The first vector. y (numpy.ndarray): The second vector. Returns: float: The cosine similarity between x and y. """ result = 0.0 dim = x.shape[0] for i in range(dim): result += x[i] * y[i] if result > 0.0: return -result else: return -EXP_NEG_INF @numba.njit( numba.types.Tuple((numba.int32[::1], numba.int32[::1]))( numba.types.Array(numba.types.float32, 2, "C", readonly=True), numba.int32[::1], numba.int64[::1], ), locals={ "n_left": numba.uint64, "n_right": numba.uint64, "left_data": numba.types.Array(numba.types.float32, 1, "C", readonly=True), "right_data": numba.types.Array(numba.types.float32, 1, "C", readonly=True), "test_data": numba.types.Array(numba.types.float32, 1, "C", readonly=True), "hyperplane_vector": numba.float32[::1], "hyperplane_norm": numba.float32, "margin": numba.float32, "d": numba.uint32, "left_index": numba.uint32, "right_index": numba.uint32, "point_idx": numba.int32, "classification": numba.int8, "max_size": numba.uint32, "temp_left": numba.int32[::1], "temp_right": numba.int32[::1], "indices_size": numba.int32, }, fastmath=True, nogil=True, cache=True, boundscheck=False, ) def float_random_projection_split(data, indices, rng_state): """Given a set of ``graph_indices`` for graph_data points from ``graph_data``, create a random hyperplane to split the graph_data, returning two arrays graph_indices that fall on either side of the hyperplane. This is the basis for a random projection tree, which simply uses this splitting recursively. This particular split uses cosine distance to determine the hyperplane and which side each graph_data sample falls on. Parameters ---------- data: array of shape (n_samples, n_features) The original graph_data to be split indices: array of shape (tree_node_size,) The graph_indices of the elements in the ``graph_data`` array that are to be split in the current operation. rng_state: array of int64, shape (3,) The internal state of the rng Returns ------- indices_left: array The elements of ``graph_indices`` that fall on the "left" side of the random hyperplane. indices_right: array The elements of ``graph_indices`` that fall on the "left" side of the random hyperplane. """ dim = data.shape[1] # Select two random points, set the hyperplane between them indices_size = np.int32(indices.shape[0]) left_index = tau_rand_int(rng_state) % indices_size right_index = tau_rand_int(rng_state) % indices_size right_index += left_index == right_index right_index = right_index % indices_size left = indices[left_index] right = indices[right_index] left_data = data[left] right_data = data[right] # Compute the normal vector to the hyperplane (the vector between # the two points) hyperplane_vector = np.empty(dim, dtype=np.float32) hyperplane_norm = 0.0 for d in range(dim): hyperplane_vector[d] = left_data[d] - right_data[d] hyperplane_norm += hyperplane_vector[d] * hyperplane_vector[d] hyperplane_norm = np.sqrt(hyperplane_norm) if abs(hyperplane_norm) < EPS: hyperplane_norm = 1.0 # Normalize in the same vector (avoiding second loop when possible) for d in range(dim): hyperplane_vector[d] /= hyperplane_norm # Use temporary arrays sized for worst case, then trim max_size = np.uint32(indices.shape[0]) temp_left = np.empty(max_size, dtype=np.int32) temp_right = np.empty(max_size, dtype=np.int32) n_left = 0 n_right = 0 # Single pass: classify points and directly populate result arrays for idx in range(indices.shape[0]): local_rng_state = rng_state + idx point_idx = indices[idx] test_data = data[point_idx] margin = 0.0 # Compute margin (dot product with hyperplane normal) for d in range(dim): margin += hyperplane_vector[d] * test_data[d] # Classify point and directly assign to appropriate array if abs(margin) < EPS: classification = tau_rand_int(local_rng_state) % 2 else: classification = 0 if margin > 0 else 1 if classification == 0: temp_left[n_left] = point_idx n_left += 1 else: temp_right[n_right] = point_idx n_right += 1 # Handle degenerate case where all points end up on one side if n_left == 0 or n_right == 0: n_left = 0 n_right = 0 # Reassign randomly for idx in range(indices.shape[0]): point_idx = indices[idx] classification = tau_rand_int(rng_state) % 2 if classification == 0: temp_left[n_left] = point_idx n_left += 1 else: temp_right[n_right] = point_idx n_right += 1 # Create final arrays with exact sizes (copy only what we need) indices_left = np.empty(n_left, dtype=np.int32) indices_right = np.empty(n_right, dtype=np.int32) for i in range(n_left): indices_left[i] = temp_left[i] for j in range(n_right): indices_right[j] = temp_right[j] return indices_left, indices_right @numba.njit( numba.void( numba.types.Array(numba.types.float32, 2, "C", readonly=True), numba.int32[::1], numba.types.ListType(numba.int32[::1]), numba.int64[::1], numba.int64, numba.int64, ), nogil=True, locals={"left_indices": numba.int32[::1], "right_indices": numba.int32[::1]}, cache=True, ) def make_float_tree( data, indices, point_indices, rng_state, leaf_size=30, max_depth=200, ): """ Recursively constructs a float tree for nearest neighbor descent. Args: data: The input data. indices: The indices of the data points to consider. point_indices: A list to store the indices of the points in each leaf node. rng_state: The random number generator state. leaf_size: The maximum number of points in a leaf node (default: 30). max_depth: The maximum depth of the tree (default: 200). Returns: None """ if indices.shape[0] > leaf_size and max_depth > 0: ( left_indices, right_indices, ) = float_random_projection_split(data, indices, rng_state) make_float_tree( data, left_indices, point_indices, rng_state, leaf_size, max_depth - 1, ) make_float_tree( data, right_indices, point_indices, rng_state, leaf_size, max_depth - 1, ) else: point_indices.append(indices) return @numba.njit( numba.int32[:, ::1]( numba.types.Array(numba.types.float32, 2, "C", readonly=True), numba.int64[::1], numba.int64, numba.int64, ), nogil=True, locals={ "points": numba.int32[::1], }, parallel=True, cache=True, ) def make_float_leaf_array_parallel(data, rng_state, leaf_size=30, max_depth=200): indices = np.arange(data.shape[0]).astype(np.int32) point_indices = numba.typed.List.empty_list(numba.int32[::1]) make_float_tree( data, indices, point_indices, rng_state, leaf_size, max_depth=max_depth, ) n_leaves = numba.int64(len(point_indices)) max_leaf_size = numba.int32(leaf_size) for i in numba.prange(n_leaves): points = point_indices[numba.int64(i)] max_leaf_size = max(max_leaf_size, numba.int32(len(points))) result = np.full((n_leaves, max_leaf_size), -1, dtype=np.int32) for i in numba.prange(n_leaves): points = point_indices[numba.int64(i)] n_points = numba.int32(len(points)) result[i, :n_points] = points return result @numba.njit( numba.int32[:, ::1]( numba.types.Array(numba.types.float32, 2, "C", readonly=True), numba.int64[::1], numba.int64, numba.int64, ), nogil=True, locals={ "points": numba.int32[::1], }, parallel=False, cache=True, ) def make_float_leaf_array_serial(data, rng_state, leaf_size=30, max_depth=200): indices = np.arange(data.shape[0]).astype(np.int32) point_indices = numba.typed.List.empty_list(numba.int32[::1]) make_float_tree( data, indices, point_indices, rng_state, leaf_size, max_depth=max_depth, ) n_leaves = numba.int64(len(point_indices)) max_leaf_size = numba.int32(leaf_size) for i in range(n_leaves): points = point_indices[numba.int64(i)] max_leaf_size = max(max_leaf_size, numba.int32(len(points))) result = np.full((n_leaves, max_leaf_size), -1, dtype=np.int32) for i in range(n_leaves): points = point_indices[numba.int64(i)] n_points = numba.int32(len(points)) result[i, :n_points] = points return result @numba.njit( numba.types.List(numba.int32[:, ::1])( numba.types.Array(numba.types.float32, 2, "C", readonly=True), numba.int64[:, ::1], numba.uint64, numba.uint64, ), parallel=True, cache=True, ) def make_float_forest_no_nested_parallelism(data, rng_states, leaf_size, max_depth): result = [np.empty((1, 1), dtype=np.int32)] * rng_states.shape[0] for i in numba.prange(len(result)): result[i] = make_float_leaf_array_serial( data, rng_states[i], leaf_size, max_depth=max_depth ) return result @numba.njit( numba.types.List(numba.int32[:, ::1])( numba.types.Array(numba.types.float32, 2, "C", readonly=True), numba.int64[:, ::1], numba.uint64, numba.uint64, ), parallel=True, cache=True, ) def make_float_forest_with_nested_parallelism(data, rng_states, leaf_size, max_depth): result = [np.empty((1, 1), dtype=np.int32)] * rng_states.shape[0] for i in numba.prange(len(result)): result[i] = make_float_leaf_array_parallel( data, rng_states[i], leaf_size, max_depth=max_depth ) return result def make_float_forest(data, rng_states, leaf_size=30, max_depth=200): if ENABLE_NESTED_PARALLELISM: return make_float_forest_with_nested_parallelism( data, rng_states, leaf_size, max_depth ) else: return make_float_forest_no_nested_parallelism( data, rng_states, leaf_size, max_depth ) @numba.njit( numba.float32[:, :, ::1]( numba.float32[:, :, ::1], numba.int32[::1], numba.types.Array(numba.types.int32, 2, "C", readonly=True), numba.float32[:], numba.types.Array(numba.types.float32, 2, "C", readonly=True), numba.int64, ), parallel=True, locals={ "d": numba.float32, "p": numba.int32, "q": numba.int32, "t": numba.uint16, "r": numba.uint32, "n": numba.uint32, "idx": numba.uint32, "data_p": numba.types.Array(numba.types.float32, 1, "C", readonly=True), "max_threshold": numba.float32, }, cache=True, ) def generate_leaf_updates_float( updates, n_updates_per_thread, leaf_block, dist_thresholds, data, n_threads ): block_size = leaf_block.shape[0] rows_per_thread = (block_size // n_threads) + 1 for t in numba.prange(n_threads): idx = 0 for r in range(rows_per_thread): n = t * rows_per_thread + r if n >= block_size: break for i in range(leaf_block.shape[1]): p = leaf_block[n, i] if p < 0: break data_p = data[p] updates[t, idx, 0] = p updates[t, idx, 1] = p updates[t, idx, 2] = -1.0 idx += 1 for j in range( i + 1, leaf_block.shape[1] ): # Start from i+1 to skip self-comparison q = leaf_block[n, j] if q < 0: break d = fast_cosine(data_p, data[q]) # Use max for better branch prediction than OR condition max_threshold = max(dist_thresholds[p], dist_thresholds[q]) if d < max_threshold: updates[t, idx, 0] = p updates[t, idx, 1] = q updates[t, idx, 2] = d idx += 1 n_updates_per_thread[t] = idx return updates @numba.njit( [ numba.void( numba.types.Array(numba.types.float32, 2, "C", readonly=True), numba.types.Tuple( (numba.int32[:, ::1], numba.float32[:, ::1], numba.uint8[:, ::1]) ), numba.types.optional( numba.types.Array(numba.types.int32, 2, "C", readonly=True) ), numba.types.int32, ), ], locals={ "d": numba.float32, "p": numba.int32, "q": numba.int32, "i": numba.uint16, "updates": numba.float32[:, :, ::1], "n_updates_per_thread": numba.int32[::1], }, parallel=True, cache=True, ) def init_rp_tree_float(data, current_graph, leaf_array, n_threads): n_leaves = leaf_array.shape[0] block_size = n_threads * 64 n_blocks = n_leaves // block_size max_leaf_size = leaf_array.shape[1] updates_per_thread = ( int(block_size * max_leaf_size * (max_leaf_size - 1) / (2 * n_threads)) + 1 ) updates = np.zeros((n_threads, updates_per_thread, 3), dtype=np.float32) n_updates_per_thread = np.zeros(n_threads, dtype=np.int32) n_vertices = current_graph[0].shape[0] vertex_block_size = n_vertices // n_threads + 1 for i in range(n_blocks + 1): block_start = i * block_size block_end = min(n_leaves, (i + 1) * block_size) leaf_block = leaf_array[block_start:block_end] dist_thresholds = current_graph[1][:, 0] updates = generate_leaf_updates_float( updates, n_updates_per_thread, leaf_block, dist_thresholds, data, n_threads ) for t in numba.prange(n_threads): block_start = t * vertex_block_size block_end = min(block_start + vertex_block_size, n_vertices) for j in range(n_threads): for k in range(n_updates_per_thread[j]): p = np.int32(updates[j, k, 0]) if p == -1: continue q = np.int32(updates[j, k, 1]) d = np.float32(updates[j, k, 2]) if p >= block_start and p < block_end: flagged_heap_push( current_graph[1][p], current_graph[0][p], current_graph[2][p], d, q, ) if q >= block_start and q < block_end: flagged_heap_push( current_graph[1][q], current_graph[0][q], current_graph[2][q], d, p, ) @numba.njit( numba.types.void( numba.int32, numba.types.Array(numba.types.float32, 2, "C", readonly=True), numba.types.Tuple( (numba.int32[:, ::1], numba.float32[:, ::1], numba.uint8[:, ::1]) ), numba.int64[::1], ), fastmath=True, parallel=True, locals={"d": numba.float32, "idx": numba.int32, "i": numba.int32}, cache=True, ) def init_random_float(n_neighbors, data, heap, rng_state): for i in numba.prange(data.shape[0]): local_rng_state = rng_state + i if heap[0][i, 0] < 0.0: for j in range(n_neighbors - np.sum(heap[0][i] >= 0.0)): idx = np.abs(tau_rand_int(local_rng_state)) % data.shape[0] if idx in heap[0][i]: continue d = fast_cosine(data[idx], data[i]) flagged_heap_push(heap[1][i], heap[0][i], heap[2][i], d, idx) return @numba.njit( numba.types.void( numba.float32[:, :, ::1], numba.int32[::1], numba.int32[:, ::1], numba.int32[:, ::1], numba.float32[:], numba.types.Array(numba.types.float32, 2, "C", readonly=True), numba.int64, ), locals={ "data_p": numba.types.Array(numba.types.float32, 1, "C", readonly=True), "dist_thresh_p": numba.float32, "dist_thresh_q": numba.float32, "p": numba.int32, "q": numba.int32, "d": numba.float32, "max_updates": numba.int32, "threshold_check": numba.boolean, "max_threshold": numba.float32, }, parallel=True, cache=True, fastmath=True, boundscheck=False, ) def generate_graph_update_array_float_basic( update_array, n_updates_per_thread, new_candidate_block, old_candidate_block, dist_thresholds, data, n_threads, ): """ Basic optimized version with aggressive optimizations but without cache-specific enhancements. Kept for comparison and benchmarking purposes. """ block_size = new_candidate_block.shape[0] max_new_candidates = new_candidate_block.shape[1] max_old_candidates = old_candidate_block.shape[1] rows_per_thread = (block_size // n_threads) + 1 for t in numba.prange(n_threads): idx = 0 max_updates = update_array.shape[1] for r in range(rows_per_thread): i = t * rows_per_thread + r if i >= block_size or idx >= max_updates: break for j in range(max_new_candidates): if idx >= max_updates: break p = new_candidate_block[i, j] if p < 0: continue data_p = data[p] dist_thresh_p = dist_thresholds[p] for k in range(j + 1, max_new_candidates): if idx >= max_updates: break q = new_candidate_block[i, k] if q < 0: continue # Compute distance once d = fast_cosine(data_p, data[q]) # Use max for better branch prediction than OR condition dist_thresh_q = dist_thresholds[q] max_threshold = max(dist_thresh_p, dist_thresh_q) threshold_check = d <= max_threshold if threshold_check: update_array[t, idx, 0] = p update_array[t, idx, 1] = q update_array[t, idx, 2] = d idx += 1 for k in range(max_old_candidates): if idx >= max_updates: break q = old_candidate_block[i, k] if q < 0: continue d = fast_cosine(data_p, data[q]) dist_thresh_q = dist_thresholds[q] max_threshold = max(dist_thresh_p, dist_thresh_q) threshold_check = d <= max_threshold if threshold_check: update_array[t, idx, 0] = p update_array[t, idx, 1] = q update_array[t, idx, 2] = d idx += 1 n_updates_per_thread[t] = idx @numba.njit( numba.void( numba.float32[:, :, ::1], numba.int32[::1], numba.int32[:, ::1], numba.int32[:, ::1], numba.float32[:], numba.types.Array(numba.types.float32, 2, "C", readonly=True), numba.int64, ), locals={ "data_p": numba.types.Array(numba.types.float32, 1, "C", readonly=True), "dist_thresh_p": numba.float32, "dist_thresh_q": numba.float32, "p": numba.int32, "q": numba.int32, "d": numba.float32, "max_updates": numba.int32, "threshold_check": numba.boolean, "working_set_size": numba.int32, "batch_start": numba.int32, "batch_end": numba.int32, "max_threshold": numba.float32, }, parallel=True, cache=True, fastmath=True, boundscheck=False, ) def generate_graph_update_array_float( update_array, n_updates_per_thread, new_candidate_block, old_candidate_block, dist_thresholds, data, n_threads, ): """ Optimized version using working set approach that processes candidates in small groups that fit well in CPU cache. This reduces cache misses by keeping frequently accessed data vectors in cache longer, providing the best performance for typical workloads. """ block_size = new_candidate_block.shape[0] max_new_candidates = new_candidate_block.shape[1] max_old_candidates = old_candidate_block.shape[1] rows_per_thread = (block_size // n_threads) + 1 # Working set size - process this many candidates at a time # Tuned for typical L1/L2 cache sizes (adjust based on data dimensionality) working_set_size = 8 for t in numba.prange(n_threads): idx = 0 max_updates = update_array.shape[1] for r in range(rows_per_thread): i = t * rows_per_thread + r if i >= block_size or idx >= max_updates: break # Process new candidates in working set chunks new_start = 0 while new_start < max_new_candidates and idx < max_updates: new_end = min(new_start + working_set_size, max_new_candidates) # Process pairs within this working set for j in range(new_start, new_end): if idx >= max_updates: break p = new_candidate_block[i, j] if p < 0: continue data_p = data[p] dist_thresh_p = dist_thresholds[p] # Compare with other candidates in the same working set for k in range(j + 1, new_end): if idx >= max_updates: break q = new_candidate_block[i, k] if q < 0: continue d = fast_cosine(data_p, data[q]) dist_thresh_q = dist_thresholds[q] max_threshold = max(dist_thresh_p, dist_thresh_q) threshold_check = d <= max_threshold if threshold_check: update_array[t, idx, 0] = p update_array[t, idx, 1] = q update_array[t, idx, 2] = d idx += 1 # Compare with candidates in future working sets for k in range(new_end, max_new_candidates): if idx >= max_updates: break q = new_candidate_block[i, k] if q < 0: continue d = fast_cosine(data_p, data[q]) dist_thresh_q = dist_thresholds[q] max_threshold = max(dist_thresh_p, dist_thresh_q) threshold_check = d <= max_threshold if threshold_check: update_array[t, idx, 0] = p update_array[t, idx, 1] = q update_array[t, idx, 2] = d idx += 1 # Compare with old candidates in working set chunks old_start = 0 while old_start < max_old_candidates and idx < max_updates: old_end = min(old_start + working_set_size, max_old_candidates) for k in range(old_start, old_end): if idx >= max_updates: break q = old_candidate_block[i, k] if q < 0: continue d = fast_cosine(data_p, data[q]) dist_thresh_q = dist_thresholds[q] max_threshold = max(dist_thresh_p, dist_thresh_q) threshold_check = d <= max_threshold if threshold_check: update_array[t, idx, 0] = p update_array[t, idx, 1] = q update_array[t, idx, 2] = d idx += 1 old_start = old_end new_start = new_end n_updates_per_thread[t] = idx @numba.njit( numba.void( numba.float32[:, :, ::1], numba.int32[:, ::1], numba.int32[:, ::1], numba.int32[:, ::1], numba.float32[:], numba.types.Array(numba.types.float32, 2, "C", readonly=True), numba.int64, ), locals={ "data_p": numba.types.Array(numba.types.float32, 1, "C", readonly=True), "dist_thresh_p": numba.float32, "dist_thresh_q": numba.float32, "p": numba.int32, "q": numba.int32, "d": numba.float32, "max_updates": numba.intp, "threshold_check": numba.boolean, "max_threshold": numba.float32, "p_block": numba.int32, "q_block": numba.int32, "p_idx": numba.int32, "q_idx": numba.int32, }, parallel=True, cache=True, fastmath=True, boundscheck=False, ) def generate_sorted_graph_update_array_float( update_array, n_updates_per_block, new_candidate_block, old_candidate_block, dist_thresholds, data, n_threads, ): """ Generate graph updates pre-sorted by target block. Updates are bucketed by their target vertex block so that apply_sorted_graph_updates can process each bucket with perfect data locality and no wasted iteration. Each update (p, q, d) is placed in BOTH p's bucket and q's bucket (if different), ensuring that each block has all updates it needs to process. The update_array has shape (n_threads, max_updates_per_block, 3) where: - First dimension indexes the target block - update_array[block, idx, 0] = p (first endpoint) - update_array[block, idx, 1] = q (second endpoint) - update_array[block, idx, 2] = d (distance) """ block_size_candidates = new_candidate_block.shape[0] max_new_candidates = new_candidate_block.shape[1] max_old_candidates = old_candidate_block.shape[1] rows_per_thread = (block_size_candidates // n_threads) + 1 n_vertices = data.shape[0] vertex_block_size = n_vertices // n_threads + 1 max_updates = update_array.shape[1] max_updates_per_src_thread = max_updates // n_threads # Reset update counts for b in numba.prange(n_threads): for t in range(n_threads + 1): n_updates_per_block[b, t] = 0 # Each thread generates updates and places them in appropriate buckets for t in numba.prange(n_threads): # Thread-local counters for each bucket local_counts = np.zeros(n_threads, dtype=np.int32) for r in range(rows_per_thread): i = t * rows_per_thread + r if i >= block_size_candidates: break for j in range(max_new_candidates): p = new_candidate_block[i, j] if p < 0: continue data_p = data[p] dist_thresh_p = dist_thresholds[p] p_block = p // vertex_block_size if p_block >= n_threads: p_block = n_threads - 1 # Compare with other new candidates for k in range(j + 1, max_new_candidates): q = new_candidate_block[i, k] if q < 0: continue d = fast_cosine(data_p, data[q]) dist_thresh_q = dist_thresholds[q] max_threshold = max(dist_thresh_p, dist_thresh_q) if d <= max_threshold: q_block = q // vertex_block_size if q_block >= n_threads: q_block = n_threads - 1 # Place update in p's bucket bucket_idx = local_counts[p_block] write_idx = t * max_updates_per_src_thread + bucket_idx if write_idx < max_updates: update_array[p_block, write_idx, 0] = p update_array[p_block, write_idx, 1] = q update_array[p_block, write_idx, 2] = d local_counts[p_block] += 1 # If q is in a different block, also place in q's bucket if q_block != p_block: bucket_idx = local_counts[q_block] write_idx = t * max_updates_per_src_thread + bucket_idx if write_idx < max_updates: update_array[q_block, write_idx, 0] = p update_array[q_block, write_idx, 1] = q update_array[q_block, write_idx, 2] = d local_counts[q_block] += 1 # Compare with old candidates for k in range(max_old_candidates): q = old_candidate_block[i, k] if q < 0: continue d = fast_cosine(data_p, data[q]) dist_thresh_q = dist_thresholds[q] max_threshold = max(dist_thresh_p, dist_thresh_q) if d <= max_threshold: q_block = q // vertex_block_size if q_block >= n_threads: q_block = n_threads - 1 # Place update in p's bucket bucket_idx = local_counts[p_block] write_idx = t * max_updates_per_src_thread + bucket_idx if write_idx < max_updates: update_array[p_block, write_idx, 0] = p update_array[p_block, write_idx, 1] = q update_array[p_block, write_idx, 2] = d local_counts[p_block] += 1 # If q is in a different block, also place in q's bucket if q_block != p_block: bucket_idx = local_counts[q_block] write_idx = t * max_updates_per_src_thread + bucket_idx if write_idx < max_updates: update_array[q_block, write_idx, 0] = p update_array[q_block, write_idx, 1] = q update_array[q_block, write_idx, 2] = d local_counts[q_block] += 1 # Record total updates generated by this thread for each bucket for b in range(n_threads): n_updates_per_block[b, t + 1] = local_counts[b] def nn_descent_float( data, n_neighbors, rng_state, max_candidates=50, n_iters=10, delta=0.001, delta_improv=None, leaf_array=None, verbose=False, ): """ Perform approximate nearest neighbor descent algorithm using float data. Parameters: - data: The input data array. - n_neighbors: The number of nearest neighbors to search for. - rng_state: The random number generator state. - max_candidates: The maximum number of candidates to consider during the search. Default is 50. - n_iters: The number of iterations to perform. Default is 10. - delta: The stopping threshold based on update count. Default is 0.001. - delta_improv: Optional stopping threshold based on relative improvement in total graph distance. When set (e.g., 0.001 for 0.1%), the algorithm will also terminate when the relative improvement in sum of all distances drops below this threshold. This can provide earlier termination on data with good structure, adapting to the intrinsic difficulty of the dataset. Default is None (disabled). - leaf_array: The array representing the leaf structure of the RP-tree. Default is None. - verbose: Whether to print progress information. Default is False. Returns: - The sorted nearest neighbor graph. """ n_threads = numba.get_num_threads() current_graph = make_heap(data.shape[0], n_neighbors) init_rp_tree_float(data, current_graph, leaf_array, n_threads) init_random_float(n_neighbors, data, current_graph, rng_state) n_vertices = data.shape[0] n_threads = numba.get_num_threads() block_size = 65536 // n_threads n_blocks = n_vertices // block_size max_updates_per_thread = int( ((max_candidates**2 + max_candidates * (max_candidates - 1) / 2) * block_size) ) update_array = np.empty((n_threads, max_updates_per_thread, 3), dtype=np.float32) n_updates_per_thread = np.zeros(n_threads, dtype=np.int32) # For distance-based termination prev_sum_dist = None for n in range(n_iters): if verbose: print("\t", n + 1, " / ", n_iters) (new_candidate_neighbors, old_candidate_neighbors) = build_candidates( current_graph, max_candidates, rng_state, n_threads ) c = 0 n_vertices = new_candidate_neighbors.shape[0] for i in range(n_blocks + 1): block_start = i * block_size block_end = min(n_vertices, (i + 1) * block_size) new_candidate_block = new_candidate_neighbors[block_start:block_end] old_candidate_block = old_candidate_neighbors[block_start:block_end] dist_thresholds = current_graph[1][:, 0] generate_graph_update_array_float( update_array, n_updates_per_thread, new_candidate_block, old_candidate_block, dist_thresholds, data, n_threads, ) c += apply_graph_update_array( current_graph, update_array, n_updates_per_thread, n_threads ) # Check update count termination if c <= delta * n_neighbors * data.shape[0]: if verbose: print("\tStopping threshold met -- exiting after", n + 1, "iterations") return deheap_sort(current_graph[0], current_graph[1]) # Check distance improvement termination (if enabled) if delta_improv is not None: all_distances = current_graph[1] valid_mask = all_distances < INF sum_dist = np.sum(all_distances[valid_mask]) if prev_sum_dist is not None: rel_improv = abs(sum_dist - prev_sum_dist) / abs(prev_sum_dist) if rel_improv < delta_improv: if verbose: print( f"\tDistance improvement threshold met ({rel_improv:.4%} < {delta_improv:.4%})" f" -- exiting after {n + 1} iterations" ) return deheap_sort(current_graph[0], current_graph[1]) prev_sum_dist = sum_dist block_size = min(n_vertices, 2 * block_size) n_blocks = n_vertices // block_size return deheap_sort(current_graph[0], current_graph[1]) def nn_descent_float_sorted( data, n_neighbors, rng_state, max_candidates=50, n_iters=10, delta=0.001, delta_improv=None, leaf_array=None, verbose=False, ): """ Perform approximate nearest neighbor descent algorithm using float data. This version uses pre-sorted updates bucketed by target block for potentially better performance when n_threads is large. Each thread only processes updates targeting its own vertex block. Parameters: - data: The input data array. - n_neighbors: The number of nearest neighbors to search for. - rng_state: The random number generator state. - max_candidates: The maximum number of candidates to consider during the search. Default is 50. - n_iters: The number of iterations to perform. Default is 10. - delta: The stopping threshold based on update count. Default is 0.001. - delta_improv: Optional stopping threshold based on relative improvement in total graph distance. When set (e.g., 0.001 for 0.1%), the algorithm will also terminate when the relative improvement in sum of all distances drops below this threshold. This can provide earlier termination on data with good structure, adapting to the intrinsic difficulty of the dataset. Default is None (disabled). - leaf_array: The array representing the leaf structure of the RP-tree. Default is None. - verbose: Whether to print progress information. Default is False. Returns: - The sorted nearest neighbor graph. """ n_threads = numba.get_num_threads() current_graph = make_heap(data.shape[0], n_neighbors) init_rp_tree_float(data, current_graph, leaf_array, n_threads) init_random_float(n_neighbors, data, current_graph, rng_state) n_vertices = data.shape[0] n_threads = numba.get_num_threads() block_size = 65536 // n_threads n_blocks = n_vertices // block_size max_updates_per_thread = int( ((max_candidates**2 + max_candidates * (max_candidates - 1) / 2) * block_size) ) # For sorted updates: shape is (n_threads, max_updates_per_block, 3) # Each bucket (first dim) holds updates targeting that block sorted_update_array = np.empty( (n_threads, max_updates_per_thread, 3), dtype=np.float32 ) # Track updates per block, with per-thread breakdown: (n_threads, n_threads + 1) # Column 0 is unused, columns 1..n_threads store count from each generating thread n_updates_per_block = np.zeros((n_threads, n_threads + 1), dtype=np.int32) # For distance-based termination prev_sum_dist = None for n in range(n_iters): if verbose: print("\t", n + 1, " / ", n_iters) (new_candidate_neighbors, old_candidate_neighbors) = build_candidates( current_graph, max_candidates, rng_state, n_threads ) c = 0 n_vertices = new_candidate_neighbors.shape[0] for i in range(n_blocks + 1): block_start = i * block_size block_end = min(n_vertices, (i + 1) * block_size) new_candidate_block = new_candidate_neighbors[block_start:block_end] old_candidate_block = old_candidate_neighbors[block_start:block_end] dist_thresholds = current_graph[1][:, 0] # Reset update counts for this iteration n_updates_per_block.fill(0) generate_sorted_graph_update_array_float( sorted_update_array, n_updates_per_block, new_candidate_block, old_candidate_block, dist_thresholds, data, n_threads, ) c += apply_sorted_graph_updates( current_graph, sorted_update_array, n_updates_per_block, n_threads ) # Check update count termination if c <= delta * n_neighbors * data.shape[0]: if verbose: print("\tStopping threshold met -- exiting after", n + 1, "iterations") return deheap_sort(current_graph[0], current_graph[1]) # Check distance improvement termination (if enabled) if delta_improv is not None: all_distances = current_graph[1] valid_mask = all_distances < INF sum_dist = np.sum(all_distances[valid_mask]) if prev_sum_dist is not None: rel_improv = abs(sum_dist - prev_sum_dist) / abs(prev_sum_dist) if rel_improv < delta_improv: if verbose: print( f"\tDistance improvement threshold met ({rel_improv:.4%} < {delta_improv:.4%})" f" -- exiting after {n + 1} iterations" ) return deheap_sort(current_graph[0], current_graph[1]) prev_sum_dist = sum_dist block_size = min(n_vertices, 2 * block_size) n_blocks = n_vertices // block_size return deheap_sort(current_graph[0], current_graph[1]) ================================================ FILE: evoc/graph_construction.py ================================================ import numpy as np import numba from scipy.sparse import coo_array INT32_MIN = np.iinfo(np.int32).min + 1 INT32_MAX = np.iinfo(np.int32).max - 1 SMOOTH_K_TOLERANCE = 1e-5 MIN_K_DIST_SCALE = 1e-3 NPY_INFINITY = np.inf @numba.njit( locals={ "psum": numba.types.float32, "lo": numba.types.float32, "mid": numba.types.float32, "hi": numba.types.float32, }, fastmath=True, parallel=True, cache=True, ) def smooth_knn_dist(distances, k, n_iter=64, bandwidth=1.0): target = np.log2(k) * bandwidth rho = np.zeros(distances.shape[0], dtype=np.float32) sigma = np.zeros(distances.shape[0], dtype=np.float32) mean_distances = np.mean(distances) for i in numba.prange(distances.shape[0]): lo = 0.0 hi = NPY_INFINITY mid = 1.0 ith_distances = distances[i] non_zero_dists = ith_distances[ith_distances > 0.0] if non_zero_dists.shape[0] >= 1: rho[i] = non_zero_dists[0] for n in range(n_iter): psum = 0.0 for j in range(1, distances.shape[1]): d = distances[i, j] - rho[i] if d > 0: psum += np.exp(-(d / mid)) else: psum += 1.0 if np.fabs(psum - target) < SMOOTH_K_TOLERANCE: break if psum > target: hi = mid mid = (lo + hi) / 2.0 else: lo = mid if hi == NPY_INFINITY: mid *= 2 else: mid = (lo + hi) / 2.0 sigma[i] = mid if rho[i] > 0.0: mean_ith_distances = np.mean(ith_distances) if sigma[i] < MIN_K_DIST_SCALE * mean_ith_distances: sigma[i] = MIN_K_DIST_SCALE * mean_ith_distances else: if sigma[i] < MIN_K_DIST_SCALE * mean_distances: sigma[i] = MIN_K_DIST_SCALE * mean_distances return sigma, rho @numba.njit( locals={ "knn_dists": numba.types.float32[:, ::1], "sigmas": numba.types.float32[::1], "rhos": numba.types.float32[::1], "sigma": numba.types.float32, "rho": numba.types.float32, "val": numba.types.float32, }, parallel=True, fastmath=True, cache=True, ) def compute_membership_strengths( knn_indices, knn_dists, sigmas, rhos, ): n_samples = knn_indices.shape[0] n_neighbors = knn_indices.shape[1] rows = np.zeros(knn_indices.size, dtype=np.int32) cols = np.zeros(knn_indices.size, dtype=np.int32) vals = np.zeros(knn_indices.size, dtype=np.float32) for i in range(n_samples): rho = rhos[i] sigma = sigmas[i] for j in range(n_neighbors): idx = knn_indices[i, j] if idx == -1: continue # We didn't get the full knn for i elif idx == i: val = 0.0 elif (knn_dists[i, j] - rho) <= 0.0 or sigma == 0.0: val = 1.0 else: val = np.exp(-((knn_dists[i, j] - rhos[i]) / (sigma))) rows[i * n_neighbors + j] = i cols[i * n_neighbors + j] = idx vals[i * n_neighbors + j] = val return rows, cols, vals def neighbor_graph_matrix( n_neighbors, knn_indices, knn_dists, symmetrize=True, ): """Construct a sparse graph from k-nearest neighbor distances. Converts k-nearest neighbor indices and distances into a weighted sparse graph matrix using Gaussian kernel weights. Optionally symmetrizes the graph to create an undirected graph. Parameters ---------- n_neighbors : float The effective number of neighbors. Used in the kernel width (sigma) computation via the smooth_knn_dist function. knn_indices : array-like of shape (n_samples, k) The indices of the k-nearest neighbors for each sample. knn_dists : array-like of shape (n_samples, k) The distances from each sample to its k-nearest neighbors. symmetrize : bool, default=True If True, the graph is symmetrized using the formula: A_sym = A + A^T - A * A^T (union of forward and reverse edges). If False, the graph remains directed (asymmetric). Returns ------- graph : scipy.sparse._csr_matrix or scipy.sparse._coo_matrix A sparse matrix representing the weighted nearest neighbor graph. The (i, j) entry contains the Gaussian kernel weight from sample i to sample j, or 0 if j is not in the k-nearest neighbors of i. If symmetrize=True, the matrix is symmetric and in CSR format. If symmetrize=False, returns a CSR matrix (asymmetric). """ knn_dists = knn_dists.astype(np.float32) sigmas, rhos = smooth_knn_dist( knn_dists, float(n_neighbors), ) rows, cols, vals = compute_membership_strengths( knn_indices, knn_dists, sigmas, rhos ) result = coo_array( (vals, (rows, cols)), shape=(knn_indices.shape[0], knn_indices.shape[0]), dtype=np.float32, ) result.eliminate_zeros() if symmetrize: transpose = result.transpose() prod_matrix = result.multiply(transpose) result = result + transpose - prod_matrix else: result = result.tocsr() result.eliminate_zeros() return result ================================================ FILE: evoc/int8_nndescent.py ================================================ import numba import numpy as np from .common_nndescent import ( tau_rand_int, make_heap, deheap_sort, flagged_heap_push, build_candidates, apply_graph_update_array, apply_sorted_graph_updates, ) from .nested_parallelism import ENABLE_NESTED_PARALLELISM # Used for a floating point "nearly zero" comparison EPS = 1e-8 INT32_MIN = np.iinfo(np.int32).min + 1 INT32_MAX = np.iinfo(np.int32).max - 1 INF = np.float32(np.inf) @numba.njit( [ "f4(i1[::1],i1[::1])", numba.types.float32( numba.types.Array(numba.types.int8, 1, "C", readonly=True), numba.types.Array(numba.types.int8, 1, "C", readonly=True), ), ], fastmath=True, boundscheck=False, nogil=True, locals={ "result": numba.types.int32, "dim": numba.types.intp, "i": numba.types.uint16, }, cache=True, ) def fast_int_inner_product_dissimilarity(x, y): result = np.int32(0) dim = x.shape[0] for i in range(dim): result += np.int32(x[i]) * np.int32(y[i]) return -np.float32(result) @numba.njit( numba.types.Tuple((numba.int32[::1], numba.int32[::1]))( numba.types.Array(numba.types.int8, 2, "C", readonly=True), numba.int32[::1], numba.int64[::1], ), locals={ "n_left": numba.uint32, "n_right": numba.uint32, "left_data": numba.types.Array(numba.types.int8, 1, "C", readonly=True), "right_data": numba.types.Array(numba.types.int8, 1, "C", readonly=True), "test_data": numba.types.Array(numba.types.int8, 1, "C", readonly=True), "hyperplane_vector": numba.float32[::1], "margin": numba.float32, "d": numba.uint32, "i": numba.uint32, "left_index": numba.uint32, "right_index": numba.uint32, }, fastmath=True, nogil=True, cache=True, ) def int8_random_projection_split(data, indices, rng_state): """Given a set of ``graph_indices`` for graph_data points from ``graph_data``, create a random hyperplane to split the graph_data, returning two arrays graph_indices that fall on either side of the hyperplane. This is the basis for a random projection tree, which simply uses this splitting recursively. This particular split uses cosine distance to determine the hyperplane and which side each graph_data sample falls on. Parameters ---------- data: array of shape (n_samples, n_features) The original graph_data to be split indices: array of shape (tree_node_size,) The graph_indices of the elements in the ``graph_data`` array that are to be split in the current operation. rng_state: array of int64, shape (3,) The internal state of the rng Returns ------- indices_left: array The elements of ``graph_indices`` that fall on the "left" side of the random hyperplane. indices_right: array The elements of ``graph_indices`` that fall on the "left" side of the random hyperplane. """ dim = data.shape[1] # Select two random points, set the hyperplane between them left_index = tau_rand_int(rng_state) % indices.shape[0] right_index = tau_rand_int(rng_state) % indices.shape[0] right_index += left_index == right_index right_index = right_index % indices.shape[0] left = indices[left_index] right = indices[right_index] left_data = data[left] right_data = data[right] left_norm = 0.0 right_norm = 0.0 for d in range(dim): left_norm += left_data[d] * left_data[d] right_norm += right_data[d] * right_data[d] left_norm = np.sqrt(left_norm) right_norm = np.sqrt(right_norm) # Compute the normal vector to the hyperplane (the vector between # the two points) hyperplane_vector = np.empty(dim, dtype=np.float32) hyperplane_norm = 0.0 for d in range(dim): hyperplane_vector[d] = (left_data[d] / left_norm) - (right_data[d] / right_norm) hyperplane_norm += hyperplane_vector[d] * hyperplane_vector[d] hyperplane_norm = np.sqrt(hyperplane_norm) # hyperplane_norm = norm(hyperplane_vector) if abs(hyperplane_norm) < EPS: hyperplane_norm = 1.0 for d in range(dim): hyperplane_vector[d] /= hyperplane_norm # For each point compute the margin (project into normal vector) # If we are on lower side of the hyperplane put in one pile, otherwise # put it in the other pile (if we hit hyperplane on the nose, flip a coin) n_left = 0 n_right = 0 side = np.empty(indices.shape[0], np.bool_) for i in range(indices.shape[0]): margin = 0.0 local_rng_state = rng_state + np.int64(i) test_data = data[indices[i]] for d in range(dim): margin += hyperplane_vector[d] * test_data[d] if abs(margin) < EPS: side[i] = np.bool_(tau_rand_int(local_rng_state) % 2) if side[i] == 0: n_left += 1 else: n_right += 1 elif margin > 0: side[i] = 0 n_left += 1 else: side[i] = 1 n_right += 1 # If all points end up on one side, something went wrong numerically # In this case, assign points randomly; they are likely very close anyway if n_left == 0 or n_right == 0: n_left = 0 n_right = 0 for i in range(indices.shape[0]): side[i] = tau_rand_int(rng_state) % 2 if side[i] == 0: n_left += 1 else: n_right += 1 # Now that we have the counts allocate arrays indices_left = np.empty(n_left, dtype=np.int32) indices_right = np.empty(n_right, dtype=np.int32) # Populate the arrays with graph_indices according to which side they fell on n_left = 0 n_right = 0 for i in range(side.shape[0]): if side[i] == 0: indices_left[n_left] = indices[i] n_left += 1 else: indices_right[n_right] = indices[i] n_right += 1 return indices_left, indices_right @numba.njit( numba.void( numba.types.Array(numba.types.int8, 2, "C", readonly=True), numba.int32[::1], numba.types.ListType(numba.int32[::1]), numba.int64[::1], numba.int64, numba.int64, ), nogil=True, cache=True, ) def make_int8_tree( data, indices, point_indices, rng_state, leaf_size=30, max_depth=200, ): if indices.shape[0] > leaf_size and max_depth > 0: ( left_indices, right_indices, ) = int8_random_projection_split(data, indices, rng_state) make_int8_tree( data, left_indices, point_indices, rng_state, leaf_size, max_depth - 1, ) make_int8_tree( data, right_indices, point_indices, rng_state, leaf_size, max_depth - 1, ) else: point_indices.append(indices) return @numba.njit( numba.int32[:, ::1]( numba.types.Array(numba.types.int8, 2, "C", readonly=True), numba.int64[::1], numba.int64, numba.int64, ), nogil=True, locals={"n_leaves": numba.int64, "i": numba.int64}, parallel=True, cache=True, ) def make_int8_leaf_array_parallel(data, rng_state, leaf_size=30, max_depth=200): indices = np.arange(data.shape[0]).astype(np.int32) point_indices = numba.typed.List.empty_list(numba.int32[::1]) make_int8_tree( data, indices, point_indices, rng_state, leaf_size, max_depth=max_depth, ) n_leaves = numba.int64(len(point_indices)) max_leaf_size = leaf_size for i in numba.prange(n_leaves): points = point_indices[numba.int64(i)] max_leaf_size = max(max_leaf_size, numba.int32(len(points))) result = np.full((n_leaves, max_leaf_size), -1, dtype=np.int32) for i in numba.prange(n_leaves): points = point_indices[numba.int64(i)] leaf_size = numba.int32(len(points)) result[i, :leaf_size] = points return result @numba.njit( numba.int32[:, ::1]( numba.types.Array(numba.types.int8, 2, "C", readonly=True), numba.int64[::1], numba.int64, numba.int64, ), nogil=True, locals={"n_leaves": numba.int64, "i": numba.int64}, parallel=False, cache=True, ) def make_int8_leaf_array_serial(data, rng_state, leaf_size=30, max_depth=200): indices = np.arange(data.shape[0]).astype(np.int32) point_indices = numba.typed.List.empty_list(numba.int32[::1]) make_int8_tree( data, indices, point_indices, rng_state, leaf_size, max_depth=max_depth, ) n_leaves = numba.int64(len(point_indices)) max_leaf_size = leaf_size for i in numba.prange(n_leaves): points = point_indices[numba.int64(i)] max_leaf_size = max(max_leaf_size, numba.int32(len(points))) result = np.full((n_leaves, max_leaf_size), -1, dtype=np.int32) for i in numba.prange(n_leaves): points = point_indices[numba.int64(i)] leaf_size = numba.int32(len(points)) result[i, :leaf_size] = points return result @numba.njit( numba.types.List(numba.int32[:, ::1])( numba.types.Array(numba.types.int8, 2, "C", readonly=True), numba.int64[:, ::1], numba.int64, numba.int64, ), parallel=True, cache=True, ) def make_int8_forest_no_nested_parallelism(data, rng_states, leaf_size, max_depth): result = [np.empty((1, 1), dtype=np.int32)] * rng_states.shape[0] for i in numba.prange(len(result)): result[i] = make_int8_leaf_array_serial( data, rng_states[i], leaf_size, max_depth=max_depth ) return result @numba.njit( numba.types.List(numba.int32[:, ::1])( numba.types.Array(numba.types.int8, 2, "C", readonly=True), numba.int64[:, ::1], numba.int64, numba.int64, ), parallel=True, cache=True, ) def make_int8_forest_with_nested_parallelism(data, rng_states, leaf_size, max_depth): result = [np.empty((1, 1), dtype=np.int32)] * rng_states.shape[0] for i in numba.prange(len(result)): result[i] = make_int8_leaf_array_parallel( data, rng_states[i], leaf_size, max_depth=max_depth ) return result def make_int8_forest(data, rng_states, leaf_size=30, max_depth=200): if ENABLE_NESTED_PARALLELISM: return make_int8_forest_with_nested_parallelism( data, rng_states, leaf_size, max_depth ) else: return make_int8_forest_no_nested_parallelism( data, rng_states, leaf_size, max_depth ) @numba.njit( numba.float32[:, :, ::1]( numba.float32[:, :, ::1], numba.int32[::1], numba.types.Array(numba.types.int32, 2, "C", readonly=True), numba.float32[:], numba.types.Array(numba.types.int8, 2, "C", readonly=True), numba.int64, ), parallel=True, locals={ "d": numba.float32, "p": numba.int32, "q": numba.int32, "t": numba.uint16, "r": numba.uint32, "n": numba.uint32, "idx": numba.uint32, "data_p": numba.types.Array(numba.types.int8, 1, "C", readonly=True), }, cache=True, ) def generate_leaf_updates_int8( updates, n_updates_per_thread, leaf_block, dist_thresholds, data, n_threads ): block_size = leaf_block.shape[0] rows_per_thread = (block_size // n_threads) + 1 for t in numba.prange(n_threads): idx = 0 for r in range(rows_per_thread): n = t * rows_per_thread + r if n >= block_size: break for i in range(leaf_block.shape[1]): p = leaf_block[n, i] if p < 0: break data_p = data[p] for j in range(i, leaf_block.shape[1]): q = leaf_block[n, j] if q < 0: break d = fast_int_inner_product_dissimilarity(data_p, data[q]) if d < dist_thresholds[p] or d < dist_thresholds[q]: updates[t, idx, 0] = p updates[t, idx, 1] = q updates[t, idx, 2] = d idx += 1 n_updates_per_thread[t] = idx return updates @numba.njit( [ numba.void( numba.types.Array(numba.types.int8, 2, "C", readonly=True), numba.types.Tuple( (numba.int32[:, ::1], numba.float32[:, ::1], numba.uint8[:, ::1]) ), numba.types.optional( numba.types.Array(numba.types.int32, 2, "C", readonly=True) ), numba.types.int32, ), ], locals={ "d": numba.float32, "p": numba.int32, "q": numba.int32, "i": numba.uint16, "updates": numba.float32[:, :, ::1], "n_updates_per_thread": numba.int32[::1], }, parallel=True, cache=True, ) def init_rp_tree_int8(data, current_graph, leaf_array, n_threads): n_leaves = leaf_array.shape[0] block_size = n_threads * 64 n_blocks = n_leaves // block_size max_leaf_size = leaf_array.shape[1] updates_per_thread = ( int(block_size * max_leaf_size * (max_leaf_size - 1) / (2 * n_threads)) + 1 ) updates = np.zeros((n_threads, updates_per_thread, 3), dtype=np.float32) n_updates_per_thread = np.zeros(n_threads, dtype=np.int32) for i in range(n_blocks + 1): block_start = i * block_size block_end = min(n_leaves, (i + 1) * block_size) leaf_block = leaf_array[block_start:block_end] dist_thresholds = current_graph[1][:, 0] updates = generate_leaf_updates_int8( updates, n_updates_per_thread, leaf_block, dist_thresholds, data, n_threads ) n_vertices = current_graph[0].shape[0] vertex_block_size = n_vertices // n_threads + 1 for t in numba.prange(n_threads): block_start = t * vertex_block_size block_end = min(block_start + vertex_block_size, n_vertices) for j in range(n_threads): for k in range(n_updates_per_thread[j]): p = np.int32(updates[j, k, 0]) q = np.int32(updates[j, k, 1]) d = np.float32(updates[j, k, 2]) if p == -1 or q == -1: continue if p >= block_start and p < block_end: flagged_heap_push( current_graph[1][p], current_graph[0][p], current_graph[2][p], d, q, ) if q >= block_start and q < block_end: flagged_heap_push( current_graph[1][q], current_graph[0][q], current_graph[2][q], d, p, ) @numba.njit( numba.types.void( numba.int32, numba.types.Array(numba.types.int8, 2, "C", readonly=True), numba.types.Tuple( (numba.int32[:, ::1], numba.float32[:, ::1], numba.uint8[:, ::1]) ), numba.int64[::1], ), fastmath=True, locals={"d": numba.float32, "idx": numba.int32, "i": numba.int32}, cache=True, ) def init_random_int8(n_neighbors, data, heap, rng_state): for i in range(data.shape[0]): if heap[0][i, 0] < 0.0: for j in range(n_neighbors - np.sum(heap[0][i] >= 0.0)): idx = np.abs(tau_rand_int(rng_state)) % data.shape[0] if idx in heap[0][i]: continue d = fast_int_inner_product_dissimilarity(data[idx], data[i]) flagged_heap_push(heap[1][i], heap[0][i], heap[2][i], d, idx) return @numba.njit( numba.types.void( numba.float32[:, :, ::1], numba.int32[::1], numba.int32[:, ::1], numba.int32[:, ::1], numba.float32[:], numba.types.Array(numba.types.int8, 2, "C", readonly=True), numba.int64, ), locals={ "data_p": numba.types.Array(numba.types.int8, 1, "C", readonly=True), }, parallel=True, cache=True, ) def generate_graph_update_array_int8( update_array, n_updates_per_thread, new_candidate_block, old_candidate_block, dist_thresholds, data, n_threads, ): block_size = new_candidate_block.shape[0] max_new_candidates = new_candidate_block.shape[1] max_old_candidates = old_candidate_block.shape[1] rows_per_thread = (block_size // n_threads) + 1 for t in numba.prange(n_threads): idx = 0 updates_are_full = False for r in range(rows_per_thread): i = t * rows_per_thread + r if i >= block_size: break for j in range(max_new_candidates): p = int(new_candidate_block[i, j]) if p < 0: continue data_p = data[p] for k in range(j, max_new_candidates): q = int(new_candidate_block[i, k]) if q < 0: continue d = fast_int_inner_product_dissimilarity(data_p, data[q]) if d <= dist_thresholds[p] or d <= dist_thresholds[q]: update_array[t, idx, 0] = p update_array[t, idx, 1] = q update_array[t, idx, 2] = d idx += 1 if idx >= update_array.shape[1]: updates_are_full = True break if updates_are_full: break for k in range(max_old_candidates): q = int(old_candidate_block[i, k]) if q < 0: continue d = fast_int_inner_product_dissimilarity(data_p, data[q]) if d <= dist_thresholds[p] or d <= dist_thresholds[q]: update_array[t, idx, 0] = p update_array[t, idx, 1] = q update_array[t, idx, 2] = d idx += 1 if idx >= update_array.shape[1]: updates_are_full = True break if updates_are_full: break if updates_are_full: break n_updates_per_thread[t] = idx @numba.njit( numba.void( numba.float32[:, :, ::1], numba.int32[:, ::1], numba.int32[:, ::1], numba.int32[:, ::1], numba.float32[:], numba.types.Array(numba.types.int8, 2, "C", readonly=True), numba.int64, ), locals={ "data_p": numba.types.Array(numba.types.int8, 1, "C", readonly=True), "dist_thresh_p": numba.float32, "dist_thresh_q": numba.float32, "p": numba.int32, "q": numba.int32, "d": numba.float32, "max_updates": numba.intp, "max_threshold": numba.float32, "p_block": numba.int32, "q_block": numba.int32, }, parallel=True, cache=True, boundscheck=False, ) def generate_sorted_graph_update_array_int8( update_array, n_updates_per_block, new_candidate_block, old_candidate_block, dist_thresholds, data, n_threads, ): """ Generate graph updates pre-sorted by target block for int8 data. """ block_size_candidates = new_candidate_block.shape[0] max_new_candidates = new_candidate_block.shape[1] max_old_candidates = old_candidate_block.shape[1] rows_per_thread = (block_size_candidates // n_threads) + 1 n_vertices = data.shape[0] vertex_block_size = n_vertices // n_threads + 1 max_updates = update_array.shape[1] max_updates_per_src_thread = max_updates // n_threads # Reset update counts for b in numba.prange(n_threads): for t in range(n_threads + 1): n_updates_per_block[b, t] = 0 # Each thread generates updates and places them in appropriate buckets for t in numba.prange(n_threads): # Thread-local counters for each bucket local_counts = np.zeros(n_threads, dtype=np.int32) for r in range(rows_per_thread): i = t * rows_per_thread + r if i >= block_size_candidates: break for j in range(max_new_candidates): p = new_candidate_block[i, j] if p < 0: continue data_p = data[p] dist_thresh_p = dist_thresholds[p] p_block = p // vertex_block_size if p_block >= n_threads: p_block = n_threads - 1 # Compare with other new candidates for k in range(j, max_new_candidates): q = new_candidate_block[i, k] if q < 0: continue d = fast_int_inner_product_dissimilarity(data_p, data[q]) dist_thresh_q = dist_thresholds[q] max_threshold = max(dist_thresh_p, dist_thresh_q) if d <= max_threshold: q_block = q // vertex_block_size if q_block >= n_threads: q_block = n_threads - 1 # Place update in p's bucket bucket_idx = local_counts[p_block] write_idx = t * max_updates_per_src_thread + bucket_idx if write_idx < max_updates: update_array[p_block, write_idx, 0] = p update_array[p_block, write_idx, 1] = q update_array[p_block, write_idx, 2] = d local_counts[p_block] += 1 # If q is in a different block, also place in q's bucket if q_block != p_block: bucket_idx = local_counts[q_block] write_idx = t * max_updates_per_src_thread + bucket_idx if write_idx < max_updates: update_array[q_block, write_idx, 0] = p update_array[q_block, write_idx, 1] = q update_array[q_block, write_idx, 2] = d local_counts[q_block] += 1 # Compare with old candidates for k in range(max_old_candidates): q = old_candidate_block[i, k] if q < 0: continue d = fast_int_inner_product_dissimilarity(data_p, data[q]) dist_thresh_q = dist_thresholds[q] max_threshold = max(dist_thresh_p, dist_thresh_q) if d <= max_threshold: q_block = q // vertex_block_size if q_block >= n_threads: q_block = n_threads - 1 # Place update in p's bucket bucket_idx = local_counts[p_block] write_idx = t * max_updates_per_src_thread + bucket_idx if write_idx < max_updates: update_array[p_block, write_idx, 0] = p update_array[p_block, write_idx, 1] = q update_array[p_block, write_idx, 2] = d local_counts[p_block] += 1 # If q is in a different block, also place in q's bucket if q_block != p_block: bucket_idx = local_counts[q_block] write_idx = t * max_updates_per_src_thread + bucket_idx if write_idx < max_updates: update_array[q_block, write_idx, 0] = p update_array[q_block, write_idx, 1] = q update_array[q_block, write_idx, 2] = d local_counts[q_block] += 1 # Record total updates generated by this thread for each bucket for b in range(n_threads): n_updates_per_block[b, t + 1] = local_counts[b] def nn_descent_int8( data, n_neighbors, rng_state, max_candidates=50, n_iters=10, delta=0.001, delta_improv=None, leaf_array=None, verbose=False, ): """ Perform approximate nearest neighbor descent algorithm using int8 data. Parameters: - data: The input data array. - n_neighbors: The number of nearest neighbors to search for. - rng_state: The random number generator state. - max_candidates: The maximum number of candidates to consider during the search. Default is 50. - n_iters: The number of iterations to perform. Default is 10. - delta: The stopping threshold based on update count. Default is 0.001. - delta_improv: Optional stopping threshold based on relative improvement in total graph distance. When set (e.g., 0.001 for 0.1%), the algorithm will also terminate when the relative improvement in sum of all distances drops below this threshold. This can provide earlier termination on data with good structure, adapting to the intrinsic difficulty of the dataset. Default is None (disabled). - leaf_array: The array representing the leaf structure of the RP-tree. Default is None. - verbose: Whether to print progress information. Default is False. Returns: - The sorted nearest neighbor graph. """ n_threads = numba.get_num_threads() current_graph = make_heap(data.shape[0], n_neighbors) init_rp_tree_int8(data, current_graph, leaf_array, n_threads) init_random_int8(n_neighbors, data, current_graph, rng_state) n_vertices = data.shape[0] n_threads = numba.get_num_threads() block_size = 65536 // n_threads n_blocks = n_vertices // block_size max_updates_per_thread = int( ((max_candidates**2 + max_candidates * (max_candidates - 1) / 2) * block_size) ) update_array = np.empty((n_threads, max_updates_per_thread, 3), dtype=np.float32) n_updates_per_thread = np.zeros(n_threads, dtype=np.int32) # For distance-based termination prev_sum_dist = None for n in range(n_iters): if verbose: print("\t", n + 1, " / ", n_iters) (new_candidate_neighbors, old_candidate_neighbors) = build_candidates( current_graph, max_candidates, rng_state, n_threads ) c = 0 n_vertices = new_candidate_neighbors.shape[0] for i in range(n_blocks + 1): block_start = i * block_size block_end = min(n_vertices, (i + 1) * block_size) new_candidate_block = new_candidate_neighbors[block_start:block_end] old_candidate_block = old_candidate_neighbors[block_start:block_end] dist_thresholds = current_graph[1][:, 0] generate_graph_update_array_int8( update_array, n_updates_per_thread, new_candidate_block, old_candidate_block, dist_thresholds, data, n_threads, ) c += apply_graph_update_array( current_graph, update_array, n_updates_per_thread, n_threads ) # Check update count termination if c <= delta * n_neighbors * data.shape[0]: if verbose: print("\tStopping threshold met -- exiting after", n + 1, "iterations") return deheap_sort(current_graph[0], current_graph[1]) # Check distance improvement termination (if enabled) if delta_improv is not None: all_distances = current_graph[1] valid_mask = all_distances < INF sum_dist = np.sum(all_distances[valid_mask]) if prev_sum_dist is not None: rel_improv = abs(sum_dist - prev_sum_dist) / abs(prev_sum_dist) if rel_improv < delta_improv: if verbose: print( f"\tDistance improvement threshold met ({rel_improv:.4%} < {delta_improv:.4%})" f" -- exiting after {n + 1} iterations" ) return deheap_sort(current_graph[0], current_graph[1]) prev_sum_dist = sum_dist block_size = min(n_vertices, 2 * block_size) n_blocks = n_vertices // block_size return deheap_sort(current_graph[0], current_graph[1]) def nn_descent_int8_sorted( data, n_neighbors, rng_state, max_candidates=50, n_iters=10, delta=0.001, delta_improv=None, leaf_array=None, verbose=False, ): """ Perform approximate nearest neighbor descent algorithm using int8 data. This version uses pre-sorted updates bucketed by target block for potentially better performance when n_threads is large. Parameters: - data: The input data array. - n_neighbors: The number of nearest neighbors to search for. - rng_state: The random number generator state. - max_candidates: The maximum number of candidates to consider during the search. Default is 50. - n_iters: The number of iterations to perform. Default is 10. - delta: The stopping threshold based on update count. Default is 0.001. - delta_improv: Optional stopping threshold based on relative improvement in total graph distance. When set (e.g., 0.001 for 0.1%), the algorithm will also terminate when the relative improvement in sum of all distances drops below this threshold. This can provide earlier termination on data with good structure, adapting to the intrinsic difficulty of the dataset. Default is None (disabled). - leaf_array: The array representing the leaf structure of the RP-tree. Default is None. - verbose: Whether to print progress information. Default is False. Returns: - The sorted nearest neighbor graph. """ n_threads = numba.get_num_threads() current_graph = make_heap(data.shape[0], n_neighbors) init_rp_tree_int8(data, current_graph, leaf_array, n_threads) init_random_int8(n_neighbors, data, current_graph, rng_state) n_vertices = data.shape[0] n_threads = numba.get_num_threads() block_size = 65536 // n_threads n_blocks = n_vertices // block_size max_updates_per_thread = int( ((max_candidates**2 + max_candidates * (max_candidates - 1) / 2) * block_size) ) sorted_update_array = np.empty( (n_threads, max_updates_per_thread, 3), dtype=np.float32 ) n_updates_per_block = np.zeros((n_threads, n_threads + 1), dtype=np.int32) # For distance-based termination prev_sum_dist = None for n in range(n_iters): if verbose: print("\t", n + 1, " / ", n_iters) (new_candidate_neighbors, old_candidate_neighbors) = build_candidates( current_graph, max_candidates, rng_state, n_threads ) c = 0 n_vertices = new_candidate_neighbors.shape[0] for i in range(n_blocks + 1): block_start = i * block_size block_end = min(n_vertices, (i + 1) * block_size) new_candidate_block = new_candidate_neighbors[block_start:block_end] old_candidate_block = old_candidate_neighbors[block_start:block_end] dist_thresholds = current_graph[1][:, 0] n_updates_per_block.fill(0) generate_sorted_graph_update_array_int8( sorted_update_array, n_updates_per_block, new_candidate_block, old_candidate_block, dist_thresholds, data, n_threads, ) c += apply_sorted_graph_updates( current_graph, sorted_update_array, n_updates_per_block, n_threads ) # Check update count termination if c <= delta * n_neighbors * data.shape[0]: if verbose: print("\tStopping threshold met -- exiting after", n + 1, "iterations") return deheap_sort(current_graph[0], current_graph[1]) # Check distance improvement termination (if enabled) if delta_improv is not None: all_distances = current_graph[1] valid_mask = all_distances < INF sum_dist = np.sum(all_distances[valid_mask]) if prev_sum_dist is not None: rel_improv = abs(sum_dist - prev_sum_dist) / abs(prev_sum_dist) if rel_improv < delta_improv: if verbose: print( f"\tDistance improvement threshold met ({rel_improv:.4%} < {delta_improv:.4%})" f" -- exiting after {n + 1} iterations" ) return deheap_sort(current_graph[0], current_graph[1]) prev_sum_dist = sum_dist block_size = min(n_vertices, 2 * block_size) n_blocks = n_vertices // block_size return deheap_sort(current_graph[0], current_graph[1]) ================================================ FILE: evoc/knn_graph.py ================================================ import numpy as np import numba import time from sklearn.utils import check_array, check_random_state from warnings import warn from .float_nndescent import ( make_float_forest, nn_descent_float, nn_descent_float_sorted, ) from .uint8_nndescent import ( make_uint8_forest, nn_descent_uint8, nn_descent_uint8_sorted, ) from .int8_nndescent import make_int8_forest, nn_descent_int8, nn_descent_int8_sorted INT32_MIN = np.iinfo(np.int32).min + 1 INT32_MAX = np.iinfo(np.int32).max - 1 # Generates a timestamp for use in logging messages when verbose=True def ts(): return time.ctime(time.time()) def make_forest( data, n_neighbors, n_trees, leaf_size, random_state, input_dtype, max_depth=200, ): """Build a random projection forest with ``n_trees``. Parameters ---------- data n_neighbors n_trees leaf_size rng_state angular Returns ------- forest: list A list of random projection trees. """ if leaf_size is None: leaf_size = max(10, np.int32(n_neighbors)) rng_states = random_state.randint(INT32_MIN, INT32_MAX, size=(n_trees, 3)).astype( np.int64 ) try: if input_dtype == np.uint8: result = make_uint8_forest(data, rng_states, leaf_size, max_depth) elif input_dtype == np.int8: result = make_int8_forest(data, rng_states, leaf_size, max_depth) else: result = make_float_forest(data, rng_states, leaf_size, max_depth) except (RuntimeError, RecursionError, SystemError): warn( "Random Projection forest initialisation failed due to recursion" "limit being reached. Something is a little strange with your " "graph_data, and this may take longer than normal to compute." ) return np.empty((0, 0), dtype=np.int32) # different trees can end up with different max leaf_sizes if the tree depth is insufficient max_leaf_size = np.max([leaf_array.shape[1] for leaf_array in result]) # pad each leaf_array from each tree out to the max_leaf_size from any tree # so that vstack can succeed. Check np.pad docs for the specific semantics return np.vstack( [ np.pad( leaf_array, ((0, 0), (0, max_leaf_size - leaf_array.shape[1])), constant_values=-1, ) for leaf_array in result ] ) def nn_descent( data, n_neighbors, rng_state, effective_max_candidates, n_iters, delta, input_dtype, leaf_array=None, verbose=False, use_sorted_updates=True, delta_improv=None, ): if input_dtype == np.uint8: if use_sorted_updates: neighbor_graph = nn_descent_uint8_sorted( data, n_neighbors, rng_state, effective_max_candidates, n_iters, delta, delta_improv=delta_improv, leaf_array=leaf_array, verbose=verbose, ) else: neighbor_graph = nn_descent_uint8( data, n_neighbors, rng_state, effective_max_candidates, n_iters, delta, delta_improv=delta_improv, leaf_array=leaf_array, verbose=verbose, ) neighbor_graph[1][:] = -np.log2(-neighbor_graph[1]) elif input_dtype == np.int8: if use_sorted_updates: neighbor_graph = nn_descent_int8_sorted( data, n_neighbors, rng_state, effective_max_candidates, n_iters, delta, delta_improv=delta_improv, leaf_array=leaf_array, verbose=verbose, ) else: neighbor_graph = nn_descent_int8( data, n_neighbors, rng_state, effective_max_candidates, n_iters, delta, delta_improv=delta_improv, leaf_array=leaf_array, verbose=verbose, ) neighbor_graph[1][:] = 1.0 / (-neighbor_graph[1]) else: if use_sorted_updates: neighbor_graph = nn_descent_float_sorted( data, n_neighbors, rng_state, effective_max_candidates, n_iters, delta, delta_improv=delta_improv, leaf_array=leaf_array, verbose=verbose, ) else: neighbor_graph = nn_descent_float( data, n_neighbors, rng_state, effective_max_candidates, n_iters, delta, delta_improv=delta_improv, leaf_array=leaf_array, verbose=verbose, ) neighbor_graph[1][:] = np.maximum(-np.log2(-neighbor_graph[1]), 0.0) return neighbor_graph def knn_graph( data, n_neighbors=30, n_trees=None, leaf_size=None, random_state=None, max_candidates=None, max_rptree_depth=200, n_iters=None, delta=0.001, delta_improv=0.001, n_jobs=None, verbose=False, use_sorted_updates=True, ): """Construct a k-nearest neighbor graph using the NN-Descent algorithm. This function builds a k-nearest neighbor graph using random projection trees for initialization followed by the NN-Descent algorithm for refinement. It supports multiple data types (float32 for normalized embeddings, int8 for quantized embeddings, uint8 for binary embeddings) with appropriate distance metrics for each. Parameters ---------- data : array-like of shape (n_samples, n_features) The data for which to compute nearest neighbors. If float32, cosine distance is used. If int8, quantized cosine distance is used. If uint8, Jaccard distance (based on Hamming distance for binary embeddings) is used. n_neighbors : int, default=30 The number of nearest neighbors to compute for each sample. n_trees : int or None, default=None The number of random projection trees to build. If None, defaults to between 4 and 8 depending on the number of available threads. leaf_size : int or None, default=None The maximum number of points per leaf in the random projection trees. If None, defaults to max(10, n_neighbors). random_state : int, RandomState instance or None, default=None Controls the randomness of the algorithm. Pass an int for reproducible output across multiple function calls. max_candidates : int or None, default=None The maximum number of candidate neighbors to evaluate during NN-Descent. If None, defaults to min(60, int(n_neighbors * 1.5)). max_rptree_depth : int, default=200 Maximum depth of the random projection trees. n_iters : int or None, default=None Number of iterations for the NN-Descent algorithm. If None, defaults to max(5, int(round(log2(n_samples)))). delta : float, default=0.001 Convergence threshold for the NN-Descent algorithm. delta_improv : float, default=0.001 Improvement threshold for early stopping in NN-Descent. n_jobs : int or None, default=None The number of threads to use. If -1, uses all available threads. If None, preserves the current numba thread setting. verbose : bool, default=False If True, print progress messages during computation. use_sorted_updates : bool, default=True If True, uses a more efficient sorted update strategy in NN-Descent. Returns ------- neighbor_graph : tuple of (array, array) A tuple containing: - indices : array-like of shape (n_samples, n_neighbors) The indices of the k-nearest neighbors for each sample. - distances : array-like of shape (n_samples, n_neighbors) The distances from each sample to its k-nearest neighbors. Distances are transformed to a uniform scale based on the input dtype. """ if data.dtype == np.uint8: data = check_array(data, dtype=np.uint8, order="C") _input_dtype = np.uint8 _bit_trees = True elif data.dtype == np.int8: data = check_array(data, dtype=np.int8, order="C") _input_dtype = np.int8 _bit_trees = False else: norms = np.einsum("ij,ij->i", data, data) np.sqrt(norms, norms) norms[norms == 0.0] = 1.0 if np.allclose(norms, 1.0): # Data is already normalized, just ensure C-contiguity and float32 data = np.ascontiguousarray(data, dtype=np.float32) else: # Efficiently create a modifiable float32 C-contiguous copy data = np.array(data, dtype=np.float32, order="C", copy=True) data /= norms[:, np.newaxis] _input_dtype = np.float32 _bit_trees = False current_random_state = check_random_state(random_state) rng_state = current_random_state.randint(INT32_MIN, INT32_MAX, 3).astype(np.int64) # Set threading constraints _original_num_threads = numba.get_num_threads() if n_jobs != -1 and n_jobs is not None: numba.set_num_threads(n_jobs) if n_trees is None: n_trees = numba.get_num_threads() n_trees = max(4, min(8, n_trees)) # Only so many trees are useful if n_iters is None: n_iters = max(5, int(round(np.log2(data.shape[0])))) if verbose: print(ts(), "Building RP forest with", str(n_trees), "trees") leaf_array = make_forest( data, n_neighbors, n_trees, leaf_size, current_random_state, _input_dtype, max_depth=max_rptree_depth, ) if max_candidates is None: effective_max_candidates = min(60, int(n_neighbors * 1.5)) else: effective_max_candidates = max_candidates if verbose: print(ts(), "NN descent for", str(n_iters), "iterations") neighbor_graph = nn_descent( data, n_neighbors, rng_state, effective_max_candidates, n_iters, delta, _input_dtype, leaf_array=leaf_array, verbose=verbose, use_sorted_updates=use_sorted_updates, delta_improv=delta_improv, ) if np.any(neighbor_graph[0] < 0): warn( "Failed to correctly find n_neighbors for some samples." " Results may be less than ideal. Try re-running with" " different parameters." ) if n_jobs != -1 and n_jobs is not None: numba.set_num_threads(_original_num_threads) return neighbor_graph ================================================ FILE: evoc/label_propagation.py ================================================ import numpy as np import numba from scipy.sparse import csr_matrix from sklearn.preprocessing import normalize from sklearn.decomposition import PCA from sklearn.manifold import SpectralEmbedding, MDS from .node_embedding import node_embedding from .common_nndescent import tau_rand, tau_rand_int INT32_MIN = np.iinfo(np.int32).min + 1 INT32_MAX = np.iinfo(np.int32).max - 1 @numba.njit(fastmath=True, parallel=True, cache=True) def label_prop_iteration( indptr, indices, data, labels, rng_state, ): n_rows = indptr.shape[0] - 1 result = labels.copy() for i in numba.prange(n_rows): current_l = labels[i] if current_l >= 0: continue local_rng_state = rng_state + i votes = {} for k in range(indptr[i], indptr[i + 1]): j = indices[k] l = labels[j] if l in votes: votes[l] += data[k] else: votes[l] = data[k] max_vote = 1 tie_count = 1 for l in votes: if l == -1: continue elif votes[l] > max_vote: max_vote = votes[l] result[i] = l tie_count = 1 elif votes[l] == max_vote: tie_count += 1 if current_l == -1: result[i] = l elif tau_rand(local_rng_state) < 1.0 / tie_count: result[i] = l else: continue return result @numba.njit(fastmath=True, parallel=True, cache=True) def original_label_prop_iteration( indptr, indices, data, labels, rng_state, ): n_rows = indptr.shape[0] - 1 result = labels.copy() for i in numba.prange(n_rows): current_l = labels[i] local_rng_state = rng_state + i votes = {} for k in range(indptr[i], indptr[i + 1]): j = indices[k] l = labels[j] if l in votes: votes[l] += data[k] else: votes[l] = data[k] max_vote = 1 tie_count = 1 for l in votes: if l == -1: continue elif votes[l] > max_vote: max_vote = votes[l] result[i] = l tie_count = 1 elif votes[l] == max_vote: tie_count += 1 if current_l == -1: result[i] = l elif tau_rand(local_rng_state) < 1.0 / tie_count: result[i] = l else: continue return result @numba.njit(cache=True) def label_outliers(indptr, indices, labels, rng_state): n_rows = indptr.shape[0] - 1 max_label = labels.max() for i in numba.prange(n_rows): local_rng_state = rng_state + i if labels[i] < 0: node_queue = [i] unlabelled = True n_iter = 0 while unlabelled and n_iter < 64 and len(node_queue) > 0: n_iter += 1 current_node = node_queue.pop() for k in range(indptr[current_node], indptr[current_node + 1]): j = indices[k] if labels[j] >= 0: labels[i] = labels[j] unlabelled = False break else: node_queue.append(j) if n_iter >= 64 or unlabelled: labels[i] = tau_rand_int(local_rng_state) % (max_label + 1) return labels @numba.njit(cache=True) def remap_labels(labels): mapping = {} unique_labels = np.unique(labels) if unique_labels[0] == -1: unique_labels = unique_labels[1:] for i, l in enumerate(unique_labels): mapping[l] = i next_label = i + 1 for i in range(labels.shape[0]): if labels[i] < 0: labels[i] = next_label next_label += 1 else: labels[i] = mapping[labels[i]] return labels def label_prop_loop( indptr, indices, data, labels, random_state, n_iter=20, approx_n_parts=2048 ): rng_state = random_state.randint(INT32_MIN, INT32_MAX, 3).astype(np.int64) for i in range(approx_n_parts): # range(int(1.25 * approx_n_parts)): labels[random_state.randint(labels.shape[0])] = i for i in range(n_iter): new_labels = label_prop_iteration(indptr, indices, data, labels, rng_state) labels = new_labels labels = label_outliers(indptr, indices, labels, rng_state) return remap_labels(labels) def original_label_prop_loop( indptr, indices, data, labels, random_state, n_iter=20, approx_n_parts=2048 ): rng_state = random_state.randint(INT32_MIN, INT32_MAX, 3).astype(np.int64) for i in range(int(1.25 * approx_n_parts)): labels[random_state.randint(labels.shape[0])] = i for i in range(n_iter): new_labels = original_label_prop_iteration( indptr, indices, data, labels, rng_state ) labels = new_labels labels = label_outliers(indptr, indices, labels, rng_state) return remap_labels(labels) def label_propagation_init( graph, n_label_prop_iter=20, n_embedding_epochs=50, approx_n_parts=512, n_components=2, scaling=0.1, random_scale=1.0, noise_level=0.5, random_state=None, data=None, recursive_init=True, base_init="pca", base_init_threshold=64, upscaling="partition_expander", ): """Initialize a node embedding using label propagation on a sparse graph. This function provides a high-quality initialization for node embeddings by combining graph-based label propagation with hierarchical partitioning. For large graphs, it recursively partitions the data and upscales the results. For small graphs, it uses direct methods (PCA, spectral embedding, or random). Parameters ---------- graph : scipy.sparse matrix A sparse adjacency or weighted graph matrix representing connectivity. n_label_prop_iter : int, default=20 Number of label propagation iterations to perform on the graph. n_embedding_epochs : int, default=50 Number of epochs when using node embedding for upscaling. approx_n_parts : int, default=512 Approximate number of partitions to create for recursive partitioning of large graphs. Useful for controlling memory and computation. n_components : int, default=2 The number of dimensions in the output embedding. scaling : float, default=0.1 Scaling factor applied to label propagation distances. random_scale : float, default=1.0 Scaling factor for random noise in the initialization. noise_level : float, default=0.5 The noise level parameter passed to node embedding algorithms. random_state : RandomState instance or None, default=None Controls the randomness of the algorithm. If None, uses system randomness. data : array-like of shape (n_samples, n_features) or None, default=None The original data array. Required if base_init='pca'. Used for direct initialization methods on small graphs. recursive_init : bool, default=True If True, uses recursive partitioning for large graphs. If False, applies the base initialization method directly. base_init : {'pca', 'random', 'spectral', 'mds'}, default='pca' The initialization method to use for small graphs (when graph size is below base_init_threshold). 'pca' requires the data parameter. base_init_threshold : int, default=64 The size threshold below which the base_init method is used directly. Graphs larger than this use recursive partitioning. upscaling : {'partition_expander', 'node_embedding'}, default='partition_expander' The method to use when upscaling partitions back to the full graph. 'partition_expander' uses a fast expansion method, 'node_embedding' uses full node embedding (slower but potentially better quality). Returns ------- embedding : array-like of shape (n_vertices, n_components) The initialized node embedding based on label propagation and graph structure. """ if random_state is None: random_state = np.random.RandomState() if graph.shape[0] < base_init_threshold: if base_init == "random": result = random_state.normal( loc=0.0, scale=1.0, size=(graph.shape[0], n_components) ) norms = np.linalg.norm(result, axis=1, keepdims=True) result = result / norms return result.astype(np.float32) elif base_init == "pca": result = ( PCA(n_components=n_components, random_state=random_state) .fit_transform(data) .astype(np.float32, order="C") ) result -= result.mean() result /= (result.max() - result.min()) / 2.0 return result elif base_init == "spectral": result = ( SpectralEmbedding(n_components=n_components, random_state=random_state) .fit_transform(data) .astype(np.float32, order="C") ) result -= result.mean() result /= (result.max() - result.min()) / 2.0 return result elif base_init == "mds": result = ( MDS( n_components=n_components, random_state=random_state, n_init=1, max_iter=300, ) .fit_transform(data) .astype(np.float32, order="C") ) result -= result.mean() result /= (result.max() - result.min()) / 2.0 return result else: raise ValueError( "Unknown base initialization method. Should be one of ['random', 'pca', 'spectral', 'mds']" ) labels = np.full(graph.shape[0], -1, dtype=np.int64) partition = label_prop_loop( graph.indptr, graph.indices, graph.data, labels, random_state, n_label_prop_iter, approx_n_parts, ) base_reduction_map = csr_matrix( (np.ones(partition.shape[0]), partition, np.arange(partition.shape[0] + 1)), shape=(partition.shape[0], partition.max() + 1), ) normalized_reduction_map = normalize(base_reduction_map, axis=0, norm="l2") data_reducer = normalize(normalized_reduction_map.T, norm="l1") if data is not None: reduced_data = data_reducer @ data else: reduced_data = None reduced_graph = normalized_reduction_map.T * graph * base_reduction_map reduced_graph.data = np.clip(reduced_graph.data, 0.0, 1.0) if recursive_init: reduced_init = label_propagation_init( reduced_graph, n_label_prop_iter=n_label_prop_iter, n_embedding_epochs=min(255, n_embedding_epochs), approx_n_parts=approx_n_parts // 4, n_components=n_components, scaling=scaling, random_scale=random_scale, noise_level=noise_level, random_state=random_state, data=reduced_data, recursive_init=True, upscaling=upscaling, base_init=base_init, base_init_threshold=base_init_threshold, ) else: reduced_init = None reduced_layout = node_embedding( reduced_graph, n_components, n_embedding_epochs, verbose=False, noise_level=noise_level, random_state=random_state, initial_embedding=reduced_init, initial_alpha=0.001 * n_embedding_epochs, ) if upscaling == "partition_expander": data_expander = normalize( (graph.multiply(graph.T)) @ normalized_reduction_map, norm="l1" ) result = ( data_expander @ reduced_layout + normalize(normalized_reduction_map, norm="l1") @ reduced_layout ) / 2.0 elif upscaling == "jitter_expander": data_expander = normalize( (graph.multiply(graph.T)) @ normalized_reduction_map, norm="l1" ) expanded = ( data_expander @ reduced_layout + normalize(normalized_reduction_map, norm="l1") @ reduced_layout ) / 2.0 jittered = reduced_layout[partition] jittered += random_state.normal( scale=random_scale / 4.0, size=(partition.shape[0], reduced_layout.shape[1]) ) result = (expanded + jittered) / 2.0 else: result = reduced_layout[partition] result += random_state.normal( scale=random_scale, size=(partition.shape[0], reduced_layout.shape[1]) ) result = (scaling * (result - result.mean(axis=0))).astype(np.float32) return result ================================================ FILE: evoc/nested_parallelism.py ================================================ import os import sys import numba def supports_safe_nesting(): # Check if user explicitly set a layer layer = os.environ.get("NUMBA_THREADING_LAYER", "") if layer in ("tbb", "omp"): return True # Check loaded libraries (if numba has already initialized) try: if "tbb" in numba.threading_layer(): return True except (ValueError, numba.errors.NumbaError): # Numba hasn't selected a layer yet, or multiple are available. pass # Heuristic: If on Mac and TBB is not strictly enforced/present, assume unsafe. if sys.platform == "darwin": # You could try importing tbb to be sure try: import tbb return True except ImportError: return False return True ENABLE_NESTED_PARALLELISM = supports_safe_nesting() ================================================ FILE: evoc/node_embedding.py ================================================ import numpy as np import numba from tqdm import tqdm INT32_MIN = np.iinfo(np.int32).min + 1 INT32_MAX = np.iinfo(np.int32).max - 1 def make_epochs_per_sample(weights, n_epochs): result = np.full(weights.shape[0], n_epochs, dtype=np.float32) n_samples = np.maximum(n_epochs * (weights / weights.max()), 1.0) result = float(n_epochs) / np.float32(n_samples) return result @numba.njit( "f4(f4[::1],f4[::1])", fastmath=True, cache=True, locals={ "result": numba.types.float32, "diff": numba.types.float32, "dim": numba.types.intp, "i": numba.types.intp, }, ) def rdist(x, y): result = 0.0 dim = x.shape[0] for i in range(dim): diff = x[i] - y[i] result += diff * diff return result @numba.njit(inline="always") def clip(val, lo, hi): if val > hi: return hi elif val < lo: return lo else: return val @numba.njit( "void(f4[:,::1],u4[::1],u4[::1],u4,f4[::1],u4,u1,f4,f4[::1],f4[::1],f4[::1],u1,f4)", fastmath=True, parallel=True, cache=True, locals={ "i": numba.uint32, "j": numba.uint32, "k": numba.uint32, "di": numba.uint8, "p": numba.uint8, "n_neg_samples": numba.uint8, "dist_squared": numba.float32, "grad_coeff": numba.float32, "current": numba.float32[::1], "other": numba.float32[::1], }, ) def node_embedding_epoch( embedding, head, tail, n_vertices, epochs_per_sample, rng_state, dim, alpha, epochs_per_negative_sample, epoch_of_next_negative_sample, epoch_of_next_sample, n, noise_level, ): for i in numba.prange(epochs_per_sample.shape[0]): if epoch_of_next_sample[i] <= n: j = head[i] k = tail[i] current = embedding[j] other = embedding[k] dist_squared = rdist(current, other) if dist_squared > 0.0: dist = np.sqrt(dist_squared) grad_coeff = (-2.0 * noise_level * dist - 2.0) / ( 2.0 * dist_squared - 0.5 * dist + 1.0 ) for di in range(dim): grad_d = grad_coeff * (current[di] - other[di]) current[di] += grad_d * alpha other[di] += -grad_d * alpha epoch_of_next_sample[i] += epochs_per_sample[i] n_neg_samples = int( (n - epoch_of_next_negative_sample[i]) / epochs_per_negative_sample[i] ) for p in range(n_neg_samples): k = ((n + p) * i * rng_state) % n_vertices other = embedding[k] dist_squared = rdist(current, other) if dist_squared > 1e-2: grad_coeff = 4.0 / ((1.0 + 0.25 * dist_squared) * dist_squared) for di in range(dim): grad_d = clip(grad_coeff * (current[di] - other[di]), -4, 4) current[di] += grad_d * alpha epoch_of_next_negative_sample[i] += ( n_neg_samples * epochs_per_negative_sample[i] ) @numba.njit( "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)", fastmath=True, parallel=True, cache=True, locals={ "updates": numba.types.float32[:, ::1], "from_node": numba.types.intp, "to_node": numba.types.intp, "raw_index": numba.types.intp, "dist_squared": numba.types.float32, "dist": numba.types.float32, "grad_coeff": numba.types.float32, "grad_d": numba.types.float32, "current": numba.types.float32[::1], "other": numba.types.float32[::1], "block_start": numba.types.intp, "block_end": numba.types.intp, "node_idx": numba.types.intp, "d": numba.types.uint8, "n": numba.types.uint8, "p": numba.types.uint8, "n_neg_samples": numba.types.uint8, }, ) def node_embedding_epoch_repr( embedding, csr_indptr, csr_indices, n_vertices, epochs_per_sample, rng_state, dim, alpha, epochs_per_negative_sample, epoch_of_next_negative_sample, epoch_of_next_sample, n, noise_level, gamma, updates, node_order, block_size=4096, ): for block_start in range(0, n_vertices, block_size): block_end = min(block_start + block_size, n_vertices) for node_idx in numba.prange(block_start, block_end): from_node = node_order[node_idx] current = embedding[from_node] for raw_index in range(csr_indptr[from_node], csr_indptr[from_node + 1]): if epoch_of_next_sample[raw_index] <= n: to_node = csr_indices[raw_index] other = embedding[to_node] dist_squared = rdist(current, other) if dist_squared > 0.0: dist = np.sqrt(dist_squared) grad_coeff = (-2.0 * noise_level * dist - 2.0) / ( 2.0 * dist_squared - 0.5 * dist + 1.0 ) for d in range(dim): grad_d = grad_coeff * (current[d] - other[d]) updates[from_node, d] += grad_d * alpha epoch_of_next_sample[raw_index] += epochs_per_sample[raw_index] n_neg_samples = int( (n - epoch_of_next_negative_sample[raw_index]) / epochs_per_negative_sample[raw_index] ) for p in range(n_neg_samples): to_node = node_order[ (raw_index * (n + p + 1) * rng_state) % n_vertices ] other = embedding[to_node] dist_squared = rdist(current, other) if dist_squared > 1e-2: grad_coeff = ( gamma * 4.0 / ((1.0 + 0.25 * dist_squared) * dist_squared) ) # grad_coeff /= n_neg_samples if grad_coeff > 0.0: for d in range(dim): grad_d = clip( grad_coeff * (current[d] - other[d]), -4, 4 ) updates[from_node, d] += grad_d * alpha epoch_of_next_negative_sample[raw_index] += ( n_neg_samples * epochs_per_negative_sample[raw_index] ) for node_idx in numba.prange(block_start, block_end): from_node = node_order[node_idx] for d in range(dim): embedding[from_node, d] += updates[from_node, d] def node_embedding( graph, n_components, n_epochs, initial_embedding=None, initial_alpha=0.5, negative_sample_rate=1.0, noise_level=0.5, random_state=None, reproducible_flag=True, verbose=False, tqdm_kwds={}, ): """Learn a low-dimensional embedding of a graph using a UMAP-like algorithm. This function performs stochastic gradient descent optimization to learn a low-dimensional embedding of graph structure. It uses both positive (connected edges) and negative (random) samples to guide the optimization. Parameters ---------- graph : scipy.sparse matrix, typically csr_matrix or csc_matrix A sparse adjacency matrix representing the graph. The weights in the matrix represent connection strengths between nodes. n_components : int The number of dimensions in the output embedding. n_epochs : int The number of epochs to train the embedding. initial_embedding : array-like of shape (n_vertices, n_components) or None, default=None An initial embedding to use as a starting point. If None, a random embedding is generated from a normal distribution with scale 0.25. initial_alpha : float, default=0.5 The initial learning rate. The learning rate decays linearly over epochs. negative_sample_rate : float, default=1.0 The rate at which negative samples are drawn relative to positive samples. Controls the ratio of negative to positive updates per epoch. noise_level : float, default=0.5 Controls the strength of noise in the gradient computation. Higher values increase the tolerance for larger distances before penalizing in the embedding space. random_state : RandomState instance or None, default=None Random state for reproducibility. If None, uses system randomness. reproducible_flag : bool, default=True If True, uses a deterministic (but slower) update strategy that processes nodes in blocks for reproducibility. If False, uses a faster stochastic approach. verbose : bool, default=False If True, display a progress bar during training. tqdm_kwds : dict, default={} Additional keyword arguments to pass to tqdm for progress bar customization. Returns ------- embedding : array-like of shape (n_vertices, n_components) The learned low-dimensional embedding of the graph vertices. """ if random_state is None: random_state = np.random.RandomState() if initial_embedding is None: embedding = random_state.normal( scale=0.25, size=(graph.shape[0], n_components) ).astype(np.float32, order="C") else: embedding = initial_embedding epochs_per_sample = make_epochs_per_sample(graph.data, n_epochs).astype( np.float32, order="C" ) epochs_per_negative_sample = epochs_per_sample / negative_sample_rate if reproducible_flag: epochs_per_negative_sample *= 1.5 epoch_of_next_negative_sample = epochs_per_negative_sample.copy() epoch_of_next_sample = epochs_per_sample.copy() if tqdm_kwds is None: tqdm_kwds = {} if "disable" not in tqdm_kwds: tqdm_kwds["disable"] = not verbose rng_val = random_state.randint(INT32_MAX, size=n_epochs) coo_graph = graph.tocoo() head_u4 = coo_graph.row.astype(np.uint32) tail_u4 = coo_graph.col.astype(np.uint32) # New csr_indptr = graph.indptr.astype(np.uint32) csr_indices = graph.indices.astype(np.uint32) updates = np.zeros_like(embedding) node_order = np.arange(graph.shape[0], dtype=np.uint32) gamma_schedule = np.linspace(0.5, 1.5, n_epochs) # End new n_vertices = np.uint32(graph.shape[0]) block_size = max(1024, n_vertices // 8) dim = np.uint8(embedding.shape[1]) alpha = np.float32(initial_alpha) for n in tqdm(range(n_epochs), **tqdm_kwds): if not reproducible_flag: node_embedding_epoch( embedding, head_u4, tail_u4, n_vertices, epochs_per_sample, rng_val[n], dim, alpha, epochs_per_negative_sample, epoch_of_next_negative_sample, epoch_of_next_sample, n, noise_level, ) else: node_embedding_epoch_repr( embedding, csr_indptr, csr_indices, n_vertices, epochs_per_sample, np.uint32(rng_val[n]), dim, alpha, epochs_per_negative_sample, epoch_of_next_negative_sample, epoch_of_next_sample, np.uint8(n), np.float32(noise_level), gamma_schedule[n], updates, node_order, np.uint32(block_size), ) updates *= (1.0 - alpha) ** 2 * 0.5 random_state.shuffle(node_order) alpha = np.float32(initial_alpha * (1.0 - (float(n) / float(n_epochs)))) return embedding ================================================ FILE: evoc/numba_kdtree.py ================================================ import numba import numpy as np from collections import namedtuple NumbaKDTree = namedtuple( "NumbaKDTree", ["data", "idx_array", "idx_start", "idx_end", "radius", "is_leaf", "node_bounds"], ) NodeData = namedtuple("NodeData", ["idx_start", "idx_end", "radius", "is_leaf"]) NodeDataType = numba.types.NamedTuple( [ numba.types.intp[::1], numba.types.intp[::1], numba.types.float32[::1], numba.types.bool_[::1], ], NodeData, ) # Create minimal sentinel instances at module level — zero cost _sentinel_kdtree = NumbaKDTree( data=np.empty((1, 1), dtype=np.float32), idx_array=np.empty(1, dtype=np.intp), idx_start=np.empty(1, dtype=np.intp), idx_end=np.empty(1, dtype=np.intp), radius=np.empty(1, dtype=np.float32), is_leaf=np.empty(1, dtype=np.bool_), node_bounds=np.empty((2, 1, 1), dtype=np.float32), ) NumbaKDTreeType = numba.typeof(_sentinel_kdtree) def kdtree_to_numba(sklearn_kdtree): data, idx_array, node_data, node_bounds = sklearn_kdtree.get_arrays() return NumbaKDTree( data, idx_array, node_data.idx_start, node_data.idx_end, node_data.radius, node_data.is_leaf, node_bounds, ) @numba.njit( cache=True, fastmath=True, locals={ "n_features": numba.types.intp, "lower_bounds": numba.types.float32[::1], "upper_bounds": numba.types.float32[::1], "radius": numba.types.float32, "diff": numba.types.float32, "data_row": numba.types.float32[::1], }, ) def _init_node( data, node_bounds, idx_array, idx_start_array, idx_end_array, radius_array, is_leaf_array, node, idx_start, idx_end, ): n_features = data.shape[1] lower_bounds = node_bounds[0, node, :] upper_bounds = node_bounds[1, node, :] # determine Node bounds for j in range(n_features): lower_bounds[j] = np.inf upper_bounds[j] = -np.inf for i in range(idx_start, idx_end): data_row = data[idx_array[i]] for j in range(n_features): lower_bounds[j] = min(lower_bounds[j], data_row[j]) upper_bounds[j] = max(upper_bounds[j], data_row[j]) radius = 0.0 for j in range(n_features): diff = abs(upper_bounds[j] - lower_bounds[j]) * 0.5 radius += diff * diff idx_start_array[node] = idx_start idx_end_array[node] = idx_end radius_array[node] = np.sqrt(radius) @numba.njit( "intp(float32[:,::1], intp[::1], intp, intp)", cache=True, locals={ "n_features": numba.types.intp, "result": numba.types.intp, "max_spread": numba.types.float32, "j": numba.types.intp, "i": numba.types.intp, "max_val": numba.types.float32, "min_val": numba.types.float32, "val": numba.types.float32, "spread": numba.types.float32, }, ) def _find_node_split_dim(data, idx_array, idx_start, idx_end): n_features = data.shape[1] result = 0 max_spread = 0 for j in range(n_features): max_val = data[idx_array[idx_start], j] min_val = max_val for i in range(idx_start + 1, idx_end): val = data[idx_array[i], j] max_val = max(max_val, val) min_val = min(min_val, val) spread = max_val - min_val if spread > max_spread: max_spread = spread result = j return result @numba.njit( "int8(float32[:,::1], intp, intp, intp)", fastmath=True, cache=True, locals={ "val1": numba.types.float32, "val2": numba.types.float32, }, ) def _compare_indices(data, axis, idx1, idx2): val1 = data[idx1, axis] val2 = data[idx2, axis] if val1 < val2: return -1 elif val1 > val2: return 1 else: # Break ties using original index values (like sklearn) if idx1 < idx2: return -1 elif idx1 > idx2: return 1 else: return 0 @numba.njit( "void(float32[:,::1], intp[::1], intp, intp, intp)", fastmath=True, cache=True, locals={ "i": numba.types.intp, "key_idx": numba.types.intp, "j": numba.types.intp, }, ) def _insertion_sort_indices(data, idx_array, axis, left, right): for i in range(left + 1, right): key_idx = idx_array[i] j = i - 1 while j >= left and _compare_indices(data, axis, idx_array[j], key_idx) > 0: idx_array[j + 1] = idx_array[j] j -= 1 idx_array[j + 1] = key_idx @numba.njit( "void(float32[:,::1], intp[::1], intp, intp, intp, intp)", fastmath=True, cache=True, locals={ "root": numba.types.intp, "child": numba.types.intp, "swap": numba.types.intp, }, ) def _sift_down_indices(data, idx_array, axis, offset, start, end): root = start while root * 2 + 1 < end: child = root * 2 + 1 swap = root if ( _compare_indices( data, axis, idx_array[offset + swap], idx_array[offset + child] ) < 0 ): swap = child if ( child + 1 < end and _compare_indices( data, axis, idx_array[offset + swap], idx_array[offset + child + 1] ) < 0 ): swap = child + 1 if swap == root: return idx_array[offset + root], idx_array[offset + swap] = ( idx_array[offset + swap], idx_array[offset + root], ) root = swap @numba.njit( "void(float32[:,::1], intp[::1], intp, intp, intp)", cache=True, locals={ "size": numba.types.intp, "i": numba.types.intp, }, ) def _heapsort_indices(data, idx_array, axis, left, right): size = right - left # Build heap for i in range(size // 2 - 1, -1, -1): _sift_down_indices(data, idx_array, axis, left, i, size) # Extract elements for i in range(size - 1, 0, -1): idx_array[left], idx_array[left + i] = idx_array[left + i], idx_array[left] _sift_down_indices(data, idx_array, axis, left, 0, i) @numba.njit( "intp(float32[:,::1], intp[::1], intp, intp, intp)", fastmath=True, cache=True, locals={ "mid": numba.types.intp, "idx_left": numba.types.intp, "idx_mid": numba.types.intp, "idx_right": numba.types.intp, }, ) def _median_of_three_pivot(data, idx_array, axis, left, right): mid = (left + right - 1) // 2 idx_left = idx_array[left] idx_mid = idx_array[mid] idx_right = idx_array[right - 1] # Sort the three candidates if _compare_indices(data, axis, idx_left, idx_mid) > 0: idx_array[left], idx_array[mid] = idx_array[mid], idx_array[left] idx_left, idx_mid = idx_mid, idx_left if _compare_indices(data, axis, idx_mid, idx_right) > 0: idx_array[mid], idx_array[right - 1] = idx_array[right - 1], idx_array[mid] idx_mid, idx_right = idx_right, idx_mid if _compare_indices(data, axis, idx_left, idx_mid) > 0: idx_array[left], idx_array[mid] = idx_array[mid], idx_array[left] return mid @numba.njit( "intp(float32[:,::1], intp[::1], intp, intp, intp, intp)", fastmath=True, cache=True, locals={ "pivot_value": numba.types.float32, "pivot_original_idx": numba.types.intp, "i": numba.types.intp, "j": numba.types.intp, }, ) def _partition_indices(data, idx_array, axis, left, right, pivot_idx): # Move pivot to end idx_array[pivot_idx], idx_array[right - 1] = ( idx_array[right - 1], idx_array[pivot_idx], ) pivot_value = data[idx_array[right - 1], axis] pivot_original_idx = idx_array[right - 1] i = left j = right - 2 while True: # Find element from left that should be on right while ( i <= j and _compare_indices(data, axis, idx_array[i], pivot_original_idx) < 0 ): i += 1 # Find element from right that should be on left while ( i <= j and _compare_indices(data, axis, idx_array[j], pivot_original_idx) >= 0 ): j -= 1 if i >= j: break # Swap elements idx_array[i], idx_array[j] = idx_array[j], idx_array[i] i += 1 j -= 1 # Move pivot to final position idx_array[i], idx_array[right - 1] = idx_array[right - 1], idx_array[i] return i @numba.njit( "void(float32[:,::1], intp[::1], intp, intp, intp, intp, intp)", cache=True, locals={ "pivot_idx": numba.types.intp, "pivot_pos": numba.types.intp, }, ) def _introselect_impl(data, idx_array, axis, left, right, nth, depth_limit): while right - left > 16: if depth_limit == 0: # Fall back to heapsort when recursion gets too deep _heapsort_indices(data, idx_array, axis, left, right) return depth_limit -= 1 # Choose pivot using median-of-three pivot_idx = _median_of_three_pivot(data, idx_array, axis, left, right) # Partition around pivot pivot_pos = _partition_indices(data, idx_array, axis, left, right, pivot_idx) # Recurse on the appropriate side if nth < pivot_pos: right = pivot_pos elif nth > pivot_pos: left = pivot_pos + 1 else: # Found the nth element return # Use insertion sort for small subarrays _insertion_sort_indices(data, idx_array, axis, left, right) @numba.njit( "void(float32[:,::1], intp[::1], intp, intp, intp, intp)", cache=True, locals={ "size": numba.types.intp, "max_depth": numba.types.intp, }, ) def _introselect(data, idx_array, axis, left, right, nth): size = right - left # Use heapsort for small arrays or when recursion depth is too high if size <= 16: _insertion_sort_indices(data, idx_array, axis, left, right) return # Calculate maximum recursion depth (2 * log2(size)) max_depth = 2 * int(np.log2(size)) _introselect_impl(data, idx_array, axis, left, right, nth, max_depth) @numba.njit( "void(float32[:, ::1], intp[::1], intp[::1], intp[::1], float32[::1], bool_[::1], float32[:, :, ::1], intp, intp, intp)", cache=True, ) def _recursive_build_tree( data, idx_array, idx_start_array, idx_end_array, radius_array, is_leaf_array, node_bounds, idx_start, idx_end, node, ): n_points = idx_end - idx_start n_mid = n_points // 2 _init_node( data, node_bounds, idx_array, idx_start_array, idx_end_array, radius_array, is_leaf_array, node, idx_start, idx_end, ) if 2 * node + 1 >= is_leaf_array.shape[0]: is_leaf_array[node] = True elif idx_end - idx_start < 2: is_leaf_array[node] = True else: is_leaf_array[node] = False axis = _find_node_split_dim(data, idx_array, idx_start, idx_end) _introselect(data, idx_array, axis, idx_start, idx_end, idx_start + n_mid) _recursive_build_tree( data, idx_array, idx_start_array, idx_end_array, radius_array, is_leaf_array, node_bounds, idx_start, idx_start + n_mid, 2 * node + 1, ) _recursive_build_tree( data, idx_array, idx_start_array, idx_end_array, radius_array, is_leaf_array, node_bounds, idx_start + n_mid, idx_end, 2 * node + 2, ) return def build_kdtree(data, leaf_size=40): n_samples = data.shape[0] n_features = data.shape[1] if leaf_size < 1: raise ValueError("leaf_size must be greater than or equal to 1") # determine number of levels in the tree, and from this # the number of nodes in the tree. This results in leaf nodes # with numbers of points between leaf_size and 2 * leaf_size n_levels = int(np.log2(max(1, (n_samples - 1) / leaf_size)) + 1) n_nodes = np.int32((2**n_levels) - 1) # allocate arrays for storage idx_array = np.arange(n_samples, dtype=np.intp) idx_start_array = np.zeros(n_nodes, dtype=np.intp) idx_end_array = np.zeros(n_nodes, dtype=np.intp) radius_array = np.zeros(n_nodes, dtype=np.float32) is_leaf_array = np.zeros(n_nodes, dtype=np.bool_) node_bounds = np.zeros((2, n_nodes, n_features), dtype=np.float32) _recursive_build_tree( data, idx_array, idx_start_array, idx_end_array, radius_array, is_leaf_array, node_bounds, 0, n_samples, 0, ) return NumbaKDTree( data, idx_array, idx_start_array, idx_end_array, radius_array, is_leaf_array, node_bounds, ) @numba.njit( [ "f4(f4[::1],f4[::1])", "f8(f8[::1],f8[::1])", "f8(f4[::1],f8[::1])", ], fastmath=True, cache=True, locals={ "dim": numba.types.intp, "i": numba.types.uint16, "diff": numba.types.float32, "result": numba.types.float32, }, ) def rdist(x, y): result = 0.0 dim = x.shape[0] for i in range(dim): diff = x[i] - y[i] result += diff * diff return result @numba.njit( [ "f4(f4[::1],f4[::1],f4[::1])", "f4(f8[::1],f8[::1],f4[::1])", "f4(f8[::1],f8[::1],f8[::1])", ], fastmath=True, cache=True, locals={ "dim": numba.types.intp, "i": numba.types.uint16, "d_lo": numba.types.float32, "d_hi": numba.types.float32, "d": numba.types.float32, "result": numba.types.float32, }, ) def point_to_node_lower_bound_rdist(upper, lower, pt): result = 0.0 dim = pt.shape[0] for i in range(dim): d_lo = upper[i] - pt[i] if upper[i] > pt[i] else 0.0 d_hi = pt[i] - lower[i] if pt[i] > lower[i] else 0.0 d = d_lo + d_hi result += d * d return result @numba.njit( [ "i4(f4[::1],i4[::1],f4,i4)", "i4(f8[::1],i4[::1],f8,i4)", ], fastmath=True, locals={ "size": numba.types.intp, "i": numba.types.uint16, "ic1": numba.types.uint16, "ic2": numba.types.uint16, "i_swap": numba.types.uint16, }, cache=True, ) def simple_heap_push(priorities, indices, p, n): if p >= priorities[0]: return 0 size = priorities.shape[0] # insert val at position zero priorities[0] = p indices[0] = n # descend the heap, swapping values until the max heap criterion is met i = 0 while True: ic1 = 2 * i + 1 ic2 = ic1 + 1 if ic1 >= size: break elif ic2 >= size: if priorities[ic1] > p: i_swap = ic1 else: break elif priorities[ic1] >= priorities[ic2]: if p < priorities[ic1]: i_swap = ic1 else: break else: if p < priorities[ic2]: i_swap = ic2 else: break priorities[i] = priorities[i_swap] indices[i] = indices[i_swap] i = i_swap priorities[i] = p indices[i] = n return 1 @numba.njit( fastmath=True, cache=True, locals={ "left_child": numba.types.intp, "right_child": numba.types.intp, "swap": numba.types.intp, }, ) def siftdown(heap1, heap2, elt): while elt * 2 + 1 < heap1.shape[0]: left_child = elt * 2 + 1 right_child = left_child + 1 swap = elt if heap1[swap] < heap1[left_child]: swap = left_child if right_child < heap1.shape[0] and heap1[swap] < heap1[right_child]: swap = right_child if swap == elt: break else: heap1[elt], heap1[swap] = heap1[swap], heap1[elt] heap2[elt], heap2[swap] = heap2[swap], heap2[elt] elt = swap @numba.njit(parallel=True, cache=True) def deheap_sort(distances, indices): for i in numba.prange(indices.shape[0]): # starting from the end of the array and moving back for j in range(indices.shape[1] - 1, 0, -1): indices[i, 0], indices[i, j] = indices[i, j], indices[i, 0] distances[i, 0], distances[i, j] = distances[i, j], distances[i, 0] siftdown(distances[i, :j], indices[i, :j], 0) return distances, indices @numba.njit( numba.void( NumbaKDTreeType, numba.types.intp, numba.float32[::1], numba.float32[::1], numba.int32[::1], numba.float32, ), fastmath=True, cache=True, locals={ "node": numba.types.intp, "left": numba.types.intp, "right": numba.types.intp, "d": numba.types.float32, "idx": numba.types.uint32, "idx_start": numba.types.intp, "idx_end": numba.types.intp, "is_leaf": numba.types.boolean, "i": numba.types.intp, "dist_lower_bound_left": numba.types.float32, "dist_lower_bound_right": numba.types.float32, }, ) def tree_query_recursion( tree, node, point, heap_p, heap_i, dist_lower_bound, ): # Get node information idx_start = tree.idx_start[node] idx_end = tree.idx_end[node] is_leaf = tree.is_leaf[node] # ------------------------------------------------------------ # Case 1: query point is outside node radius: # trim it from the query if dist_lower_bound > heap_p[0]: return # ------------------------------------------------------------ # Case 2: this is a leaf node. Update set of nearby points elif is_leaf: for i in range(idx_start, idx_end): idx = tree.idx_array[i] d = rdist(point, tree.data[idx]) if d < heap_p[0]: simple_heap_push(heap_p, heap_i, d, idx) # ------------------------------------------------------------ # Case 3: Node is not a leaf. Recursively query subnodes # starting with the closest else: left = 2 * node + 1 right = left + 1 dist_lower_bound_left = point_to_node_lower_bound_rdist( tree.node_bounds[0, left], tree.node_bounds[1, left], point ) dist_lower_bound_right = point_to_node_lower_bound_rdist( tree.node_bounds[0, right], tree.node_bounds[1, right], point ) # recursively query subnodes if dist_lower_bound_left <= dist_lower_bound_right: tree_query_recursion( tree, left, point, heap_p, heap_i, dist_lower_bound_left ) tree_query_recursion( tree, right, point, heap_p, heap_i, dist_lower_bound_right ) else: tree_query_recursion( tree, right, point, heap_p, heap_i, dist_lower_bound_right ) tree_query_recursion( tree, left, point, heap_p, heap_i, dist_lower_bound_left ) return @numba.njit( numba.types.Tuple((numba.float32[:, ::1], numba.int32[:, ::1]))( NumbaKDTreeType, numba.float32[:, ::1], numba.int64, numba.types.boolean, ), parallel=True, fastmath=True, cache=True, locals={ "i": numba.types.intp, "distance_lower_bound": numba.types.float32, }, ) def parallel_tree_query( tree, data, k=numba.int64(10), output_rdist=numba.types.boolean(False) ): result = ( np.full((data.shape[0], k), np.inf, dtype=np.float32), np.full((data.shape[0], k), -1, dtype=np.int32), ) for i in numba.prange(data.shape[0]): distance_lower_bound = point_to_node_lower_bound_rdist( tree.node_bounds[0, 0], tree.node_bounds[1, 0], data[i] ) heap_priorities, heap_indices = result[0][i], result[1][i] tree_query_recursion( tree, numba.intp(0), data[i], heap_priorities, heap_indices, distance_lower_bound, ) if output_rdist: return deheap_sort(result[0], result[1]) else: return deheap_sort(np.sqrt(result[0]), result[1]) ================================================ FILE: evoc/tests/test_boruvka.py ================================================ """ Comprehensive test suite for the boruvka module. This module tests Boruvka's algorithm implementation for minimum spanning tree construction, including component merging, tree queries, and parallel processing. """ import numpy as np import pytest import numba from sklearn.datasets import make_blobs from sklearn.preprocessing import StandardScaler from evoc.boruvka import ( merge_components, update_component_vectors, boruvka_tree_query, boruvka_tree_query_reproducible, initialize_boruvka_from_knn, parallel_boruvka, calculate_block_size, component_aware_query_recursion, ) from evoc.numba_kdtree import build_kdtree from evoc.disjoint_set import ds_rank_create, ds_find, ds_union_by_rank class TestMergeComponents: """Test component merging functionality.""" def test_merge_components_basic(self): """Test basic component merging with simple data.""" # Create a simple disjoint set with 4 components disjoint_set = ds_rank_create(4) # Candidate neighbors: each point's nearest neighbor in different component candidate_neighbors = np.array([1, 0, 3, 2], dtype=np.int32) candidate_distances = np.array([1.0, 1.0, 2.0, 2.0], dtype=np.float32) point_components = np.array([0, 1, 2, 3], dtype=np.int64) result = merge_components( disjoint_set, candidate_neighbors, candidate_distances, point_components ) # Should have edges connecting components assert result.shape[0] >= 1 assert result.shape[1] == 3 # from, to, distance # Distances should be positive assert np.all(result[:, 2] >= 0) # Edges should connect different components for i in range(result.shape[0]): from_comp = ds_find(disjoint_set, int(result[i, 0])) to_comp = ds_find(disjoint_set, int(result[i, 1])) # After merging, they should be in same component assert from_comp == to_comp def test_merge_components_empty(self): """Test merge components with no valid edges.""" disjoint_set = ds_rank_create(2) # Pre-merge the components ds_union_by_rank(disjoint_set, 0, 1) candidate_neighbors = np.array([1, 0], dtype=np.int32) candidate_distances = np.array([1.0, 1.0], dtype=np.float32) point_components = np.array([0, 0], dtype=np.int64) # Same component result = merge_components( disjoint_set, candidate_neighbors, candidate_distances, point_components ) # Should have no edges since all points are in same component assert result.shape[0] == 0 def test_merge_components_best_edge_selection(self): """Test that merge_components selects the best edge from each component.""" disjoint_set = ds_rank_create(6) # Component 0: points 0,1 - best edge from 0 should be selected # Component 1: points 2,3 - best edge from 2 should be selected # Component 2: points 4,5 - best edge from 4 should be selected ds_union_by_rank(disjoint_set, 0, 1) ds_union_by_rank(disjoint_set, 2, 3) ds_union_by_rank(disjoint_set, 4, 5) # Update point components to reflect merging point_components = np.array( [ ds_find(disjoint_set, 0), ds_find(disjoint_set, 1), ds_find(disjoint_set, 2), ds_find(disjoint_set, 3), ds_find(disjoint_set, 4), ds_find(disjoint_set, 5), ], dtype=np.int64, ) # Each point has a candidate neighbor - different distances candidate_neighbors = np.array([2, 2, 0, 0, 0, 0], dtype=np.int32) candidate_distances = np.array([3.0, 1.0, 2.0, 4.0, 1.5, 2.5], dtype=np.float32) result = merge_components( disjoint_set, candidate_neighbors, candidate_distances, point_components ) # Should select best edges from each component assert result.shape[0] >= 1 assert result.shape[0] <= 3 # At most 3 components to merge class TestUpdateComponentVectors: """Test component vector updates.""" @pytest.fixture def simple_tree_and_components(self): """Create a simple tree and component structure for testing.""" # Create simple 2D data data = np.array( [ [0.0, 0.0], [0.1, 0.1], # Component 0 [1.0, 1.0], [1.1, 1.1], # Component 1 [2.0, 2.0], [2.1, 2.1], # Component 2 ], dtype=np.float32, ) tree = build_kdtree(data, leaf_size=2) # Create disjoint set and merge some components disjoint_set = ds_rank_create(6) ds_union_by_rank(disjoint_set, 0, 1) # Merge 0,1 ds_union_by_rank(disjoint_set, 2, 3) # Merge 2,3 ds_union_by_rank(disjoint_set, 4, 5) # Merge 4,5 point_components = np.array( [ds_find(disjoint_set, i) for i in range(6)], dtype=np.int64 ) node_components = np.full(tree.idx_start.shape[0], -1, dtype=np.int64) return tree, disjoint_set, point_components, node_components def test_update_component_vectors_basic(self, simple_tree_and_components): """Test basic component vector update.""" tree, disjoint_set, point_components, node_components = ( simple_tree_and_components ) update_component_vectors(tree, disjoint_set, node_components, point_components) # Point components should be updated to component roots unique_components = np.unique(point_components) assert len(unique_components) == 3 # Should have 3 components # Check that merged points have same component assert point_components[0] == point_components[1] # Points 0,1 merged assert point_components[2] == point_components[3] # Points 2,3 merged assert point_components[4] == point_components[5] # Points 4,5 merged def test_update_component_vectors_leaf_nodes(self, simple_tree_and_components): """Test that leaf nodes are correctly labeled when all points have same component.""" tree, disjoint_set, point_components, node_components = ( simple_tree_and_components ) # Merge all components into one for i in range(1, 6): ds_union_by_rank(disjoint_set, 0, i) # Update point components for i in range(6): point_components[i] = ds_find(disjoint_set, i) update_component_vectors(tree, disjoint_set, node_components, point_components) # All point components should be the same assert len(np.unique(point_components)) == 1 # All leaf nodes should have the same component as their points for i in range(tree.idx_start.shape[0]): if tree.is_leaf[i]: # All points in this leaf should have same component start, end = tree.idx_start[i], tree.idx_end[i] if end > start: # Non-empty leaf leaf_components = [ point_components[tree.idx_array[j]] for j in range(start, end) ] if len(set(leaf_components)) == 1: # All same component assert node_components[i] == leaf_components[0] class TestBoruvkaTreeQuery: """Test tree query functionality for Boruvka's algorithm.""" @pytest.fixture def query_test_data(self): """Create test data for tree queries.""" # Create well-separated clusters np.random.seed(42) data = np.vstack( [ np.random.normal([0, 0], 0.1, (10, 2)), # Cluster 0 np.random.normal([2, 0], 0.1, (10, 2)), # Cluster 1 np.random.normal([0, 2], 0.1, (10, 2)), # Cluster 2 ] ).astype(np.float32) tree = build_kdtree(data, leaf_size=5) # Create component structure - each cluster is a component disjoint_set = ds_rank_create(30) point_components = np.array([i // 10 for i in range(30)], dtype=np.int64) node_components = np.full(tree.idx_start.shape[0], -1, dtype=np.int64) core_distances = np.zeros(30, dtype=np.float32) return tree, node_components, point_components, core_distances def test_boruvka_tree_query_basic(self, query_test_data): """Test basic tree query functionality.""" tree, node_components, point_components, core_distances = query_test_data # Update node components disjoint_set = ds_rank_create(30) for i in range(30): for j in range(i + 1, min(i + 10, 30)): if i // 10 == j // 10: # Same cluster ds_union_by_rank(disjoint_set, i, j) update_component_vectors(tree, disjoint_set, node_components, point_components) distances, indices = boruvka_tree_query( tree, node_components, point_components, core_distances ) # Should find nearest neighbors in different components assert distances.shape[0] == 30 assert indices.shape[0] == 30 # All distances should be finite (found neighbors) assert np.all(np.isfinite(distances)) # All indices should be valid assert np.all(indices >= 0) assert np.all(indices < 30) # Neighbors should be in different components for i in range(30): if indices[i] >= 0: assert point_components[i] != point_components[indices[i]] def test_boruvka_tree_query_reproducible(self, query_test_data): """Test reproducible tree query gives consistent results.""" tree, node_components, point_components, core_distances = query_test_data # Update node components disjoint_set = ds_rank_create(30) for i in range(30): for j in range(i + 1, min(i + 10, 30)): if i // 10 == j // 10: # Same cluster ds_union_by_rank(disjoint_set, i, j) update_component_vectors(tree, disjoint_set, node_components, point_components) # Run multiple times with same block size block_size = 8 results = [] for _ in range(3): distances, indices = boruvka_tree_query_reproducible( tree, node_components, point_components, core_distances, block_size ) results.append((distances.copy(), indices.copy())) # Results should be fairly similar (may have small variations due to ties) for i in range(1, len(results)): # Check that indices are valid and neighbors are in different components distances_i, indices_i = results[i] distances_0, indices_0 = results[0] # All distances should be positive and finite assert np.all(np.isfinite(distances_i)) assert np.all(distances_i > 0) # All neighbors should be in different components for j in range(30): if indices_i[j] >= 0: assert point_components[j] != point_components[indices_i[j]] def test_boruvka_query_different_block_sizes(self, query_test_data): """Test that different block sizes give same results.""" tree, node_components, point_components, core_distances = query_test_data # Update node components disjoint_set = ds_rank_create(30) for i in range(30): for j in range(i + 1, min(i + 10, 30)): if i // 10 == j // 10: # Same cluster ds_union_by_rank(disjoint_set, i, j) update_component_vectors(tree, disjoint_set, node_components, point_components) # Test different block sizes block_sizes = [4, 8, 16, 30] results = [] for block_size in block_sizes: distances, indices = boruvka_tree_query_reproducible( tree, node_components, point_components, core_distances, block_size ) results.append((distances.copy(), indices.copy())) # All results should be valid (may have variations due to ties in nearest neighbors) for i in range(1, len(results)): distances_i, indices_i = results[i] # All distances should be positive and finite assert np.all(np.isfinite(distances_i)) assert np.all(distances_i > 0) # All neighbors should be in different components for j in range(30): if indices_i[j] >= 0: assert point_components[j] != point_components[indices_i[j]] class TestInitializeBoruvkaFromKNN: """Test initialization of Boruvka from k-nearest neighbors.""" def test_initialize_boruvka_from_knn_basic(self): """Test basic initialization from k-NN.""" # Create simple k-NN data knn_indices = np.array( [ [0, 1, 2], # Point 0's neighbors: self, 1, 2 [1, 0, 2], # Point 1's neighbors: self, 0, 2 [2, 0, 1], # Point 2's neighbors: self, 0, 1 ], dtype=np.int32, ) knn_distances = np.array( [ [0.0, 1.0, 2.0], [0.0, 1.0, 2.0], [0.0, 1.5, 2.5], ], dtype=np.float32, ) core_distances = np.array([1.0, 1.0, 1.5], dtype=np.float32) disjoint_set = ds_rank_create(3) result = initialize_boruvka_from_knn( knn_indices, knn_distances, core_distances, disjoint_set ) # Should have edges connecting components assert result.shape[0] >= 1 assert result.shape[1] == 3 # Edge weights should match core distances for i in range(result.shape[0]): from_point = int(result[i, 0]) assert result[i, 2] == core_distances[from_point] def test_initialize_boruvka_core_distance_constraint(self): """Test that initialization respects core distance constraints.""" # Point 0 has high core distance, should connect to point with lower core distance knn_indices = np.array( [ [0, 1], # Point 0's neighbors: self, 1 [1, 0], # Point 1's neighbors: self, 0 ], dtype=np.int32, ) knn_distances = np.array( [ [0.0, 1.0], # Point 0 distances (squared distances) [0.0, 1.0], # Point 1 distances (squared distances) ], dtype=np.float32, ) # Point 0 has higher core distance than point 1 core_distances = np.array([2.0, 1.0], dtype=np.float32) disjoint_set = ds_rank_create(2) result = initialize_boruvka_from_knn( knn_indices, knn_distances, core_distances, disjoint_set ) # Should create edge from point 0 to point 1 (lower core distance) assert result.shape[0] == 1 assert result[0, 0] == 0 # From point 0 assert result[0, 1] == 1 # To point 1 assert ( result[0, 2] == 2.0 ) # Weight is max(core_distance[0], distance) = max(2.0, 1.0) = 2.0 class TestCalculateBlockSize: """Test block size calculation for adaptive processing.""" def test_calculate_block_size_basic(self): """Test basic block size calculation.""" num_threads = 4 # Test different scenarios scenarios = [ (10, 100, 10), # 10 components, 100 points, 10 points/component (1, 1000, 1000), # 1 component, 1000 points, 1000 points/component (100, 500, 5), # 100 components, 500 points, 5 points/component (0, 100, 100), # 0 components (edge case) ] for n_components, n_points, expected_ppc in scenarios: block_size = calculate_block_size(n_components, n_points, num_threads) # Block size should be reasonable assert block_size >= num_threads assert block_size <= n_points // 4 + 1 assert isinstance(block_size, int) def test_calculate_block_size_extremes(self): """Test block size calculation for extreme cases.""" num_threads = 8 # Very large dataset block_size = calculate_block_size(1000, 100000, num_threads) assert block_size >= num_threads assert block_size <= 100000 // 4 + 1 # Very small dataset block_size = calculate_block_size(1, 10, num_threads) assert block_size >= num_threads # For small datasets, max() ensures block_size >= num_threads even when n_points//4+1 is smaller expected_max = max(num_threads, 10 // 4 + 1) assert block_size == expected_max class TestParallelBoruvka: """Test the main parallel Boruvka algorithm.""" @pytest.fixture def boruvka_test_data(self): """Create test data for Boruvka algorithm.""" # Create well-separated clusters that should form clear MST np.random.seed(42) cluster_centers = [[0, 0], [3, 0], [0, 3], [3, 3]] data = [] for center in cluster_centers: cluster_data = np.random.normal(center, 0.1, (5, 2)) data.append(cluster_data) data = np.vstack(data).astype(np.float32) tree = build_kdtree(data, leaf_size=3) return tree, data def test_parallel_boruvka_basic(self, boruvka_test_data): """Test basic Boruvka algorithm execution.""" tree, data = boruvka_test_data num_threads = numba.get_num_threads() # Run Boruvka with different min_samples for min_samples in [1, 3, 5]: edges = parallel_boruvka( tree, num_threads, min_samples=min_samples, reproducible=False ) # Should produce a valid MST assert edges.shape[0] == data.shape[0] - 1 # n-1 edges for MST assert edges.shape[1] == 3 # from, to, weight # All edge weights should be positive assert np.all(edges[:, 2] > 0) # Edge endpoints should be valid indices assert np.all(edges[:, 0] >= 0) assert np.all(edges[:, 0] < data.shape[0]) assert np.all(edges[:, 1] >= 0) assert np.all(edges[:, 1] < data.shape[0]) def test_parallel_boruvka_reproducible(self, boruvka_test_data): """Test that reproducible Boruvka gives consistent results.""" tree, data = boruvka_test_data num_threads = numba.get_num_threads() # Run multiple times results = [] for _ in range(3): edges = parallel_boruvka( tree, num_threads, min_samples=3, reproducible=True ) # Sort edges for comparison (edge order may vary) sorted_edges = edges[np.lexsort((edges[:, 1], edges[:, 0]))] results.append(sorted_edges) # Results should be identical for i in range(1, len(results)): np.testing.assert_array_almost_equal(results[0], results[i], decimal=5) def test_parallel_boruvka_vs_non_reproducible(self, boruvka_test_data): """Test that reproducible and non-reproducible versions give equivalent MST weights.""" tree, data = boruvka_test_data num_threads = numba.get_num_threads() edges_normal = parallel_boruvka( tree, num_threads, min_samples=3, reproducible=False ) edges_repro = parallel_boruvka( tree, num_threads, min_samples=3, reproducible=True ) # Both should have same number of edges assert edges_normal.shape[0] == edges_repro.shape[0] # Total MST weight should be the same (or very close due to floating point) total_weight_normal = np.sum(edges_normal[:, 2]) total_weight_repro = np.sum(edges_repro[:, 2]) np.testing.assert_almost_equal( total_weight_normal, total_weight_repro, decimal=4 ) def test_parallel_boruvka_single_point(self): """Test Boruvka with single point (edge case).""" data = np.array([[0.0, 0.0]], dtype=np.float32) tree = build_kdtree(data, leaf_size=1) num_threads = numba.get_num_threads() edges = parallel_boruvka(tree, num_threads, min_samples=1, reproducible=False) # Single point should produce empty MST assert edges.shape[0] == 0 def test_parallel_boruvka_two_points(self): """Test Boruvka with two points.""" data = np.array([[0.0, 0.0], [1.0, 1.0]], dtype=np.float32) tree = build_kdtree(data, leaf_size=1) num_threads = numba.get_num_threads() edges = parallel_boruvka(tree, num_threads, min_samples=1, reproducible=False) # Two points should produce single edge assert edges.shape[0] == 1 assert edges.shape[1] == 3 # Edge should connect the two points edge_points = set([int(edges[0, 0]), int(edges[0, 1])]) assert edge_points == {0, 1} # Edge weight should be distance between points expected_distance = np.sqrt(2.0) # sqrt((1-0)^2 + (1-0)^2) np.testing.assert_almost_equal(edges[0, 2], expected_distance, decimal=5) def test_parallel_boruvka_different_min_samples(self, boruvka_test_data): """Test Boruvka with different min_samples values.""" tree, data = boruvka_test_data num_threads = numba.get_num_threads() results = {} for min_samples in [1, 2, 3, 5]: edges = parallel_boruvka( tree, num_threads, min_samples=min_samples, reproducible=False ) results[min_samples] = edges # All should produce valid MST assert edges.shape[0] == data.shape[0] - 1 # Different min_samples may produce different trees, but should all be valid MSTs # Test that all have reasonable total weights weights = [np.sum(edges[:, 2]) for edges in results.values()] # All weights should be positive and within reasonable range of each other assert all(w > 0 for w in weights) weight_ratio = max(weights) / min(weights) assert ( weight_ratio < 10.0 ) # Different min_samples can produce quite different trees def test_parallel_boruvka_different_num_threads(self, boruvka_test_data): """Test Boruvka with different num_threads values.""" tree, data = boruvka_test_data # Test different numbers of threads thread_counts = [1, 2, 4, 8] results = {} for num_threads in thread_counts: edges = parallel_boruvka( tree, num_threads, min_samples=3, reproducible=True ) results[num_threads] = edges # All should produce valid MST assert edges.shape[0] == data.shape[0] - 1 assert edges.shape[1] == 3 assert np.all(edges[:, 2] > 0) # All results should be identical when using reproducible=True # (since the algorithm should be deterministic regardless of thread count) sorted_results = {} for num_threads, edges in results.items(): sorted_edges = edges[np.lexsort((edges[:, 1], edges[:, 0]))] sorted_results[num_threads] = sorted_edges # Compare all results to the first one base_result = sorted_results[thread_counts[0]] for num_threads in thread_counts[1:]: np.testing.assert_array_almost_equal( base_result, sorted_results[num_threads], decimal=5, err_msg=f"Results differ between 1 thread and {num_threads} threads", ) class TestEdgeCases: """Test edge cases and error conditions.""" def test_empty_data_handling(self): """Test handling of empty data - should not raise exception as input validation happens upstream.""" # Empty data should be handled gracefully without raising exceptions # since this is an internal function that relies on sklearn's check_array for validation try: data = np.empty((0, 2), dtype=np.float32) tree = build_kdtree(data, leaf_size=1) # If we get here, the function handled empty data gracefully assert True except Exception: # If an exception is raised, that's also acceptable behavior # since the exact handling of empty data may vary assert True def test_single_dimension_data(self): """Test with 1D data.""" data = np.array([[0.0], [1.0], [2.0]], dtype=np.float32) tree = build_kdtree(data, leaf_size=2) num_threads = numba.get_num_threads() edges = parallel_boruvka(tree, num_threads, min_samples=1, reproducible=False) # Should produce valid MST for 1D data assert edges.shape[0] == 2 # 3 points -> 2 edges assert np.all(edges[:, 2] > 0) # Positive weights def test_high_dimensional_data(self): """Test with higher dimensional data.""" np.random.seed(42) data = np.random.random((20, 10)).astype(np.float32) # 20 points in 10D tree = build_kdtree(data, leaf_size=5) num_threads = numba.get_num_threads() edges = parallel_boruvka(tree, num_threads, min_samples=2, reproducible=False) # Should handle high-dimensional data assert edges.shape[0] == 19 # n-1 edges assert np.all(edges[:, 2] > 0) assert np.all(np.isfinite(edges[:, 2])) if __name__ == "__main__": pytest.main([__file__]) ================================================ FILE: evoc/tests/test_cluster_trees.py ================================================ import numpy as np import pytest from sklearn.datasets import make_blobs from sklearn.utils import shuffle from sklearn.preprocessing import StandardScaler from sklearn.metrics import pairwise_distances from evoc.cluster_trees import ( create_linkage_merge_data, eliminate_branch, linkage_merge_find, linkage_merge_join, mst_to_linkage_tree, bfs_from_hierarchy, condense_tree, extract_leaves, score_condensed_tree_nodes, cluster_tree_from_condensed_tree, extract_eom_clusters, get_cluster_labelling_at_cut, get_cluster_label_vector, get_point_membership_strength_vector, CondensedTree, LinkageMergeData, ) class TestLinkageMergeData: """Test the LinkageMergeData structure and associated functions.""" def test_create_linkage_merge_data(self): """Test creation of linkage merge data structure.""" base_size = 5 linkage_data = create_linkage_merge_data(base_size) # Check structure assert isinstance(linkage_data, LinkageMergeData) assert len(linkage_data.parent) == 2 * base_size - 1 assert len(linkage_data.size) == 2 * base_size - 1 assert len(linkage_data.next) == 1 # Check initial values assert np.all(linkage_data.parent == -1) assert np.all(linkage_data.size[:base_size] == 1) assert np.all(linkage_data.size[base_size:] == 0) assert linkage_data.next[0] == base_size def test_linkage_merge_find_and_join(self): """Test find and join operations on linkage merge data.""" base_size = 4 linkage_data = create_linkage_merge_data(base_size) # Initially, each node should find itself for i in range(base_size): assert linkage_merge_find(linkage_data, i) == i # Join nodes 0 and 1 linkage_merge_join(linkage_data, 0, 1) # Check that parent pointers are set correctly assert linkage_data.parent[0] == base_size # 4 assert linkage_data.parent[1] == base_size # 4 assert linkage_data.size[base_size] == 2 # Combined size assert linkage_data.next[0] == base_size + 1 # Next available index # Join the new cluster with node 2 new_cluster = linkage_merge_find(linkage_data, 0) # Should be 4 linkage_merge_join(linkage_data, new_cluster, 2) # Check updated structure assert linkage_data.size[base_size + 1] == 3 # Size should be 3 assert linkage_data.next[0] == base_size + 2 # Next available index class TestMSTToLinkageTree: """Test conversion from MST to linkage tree.""" @pytest.fixture def simple_mst(self): """Create a simple MST for testing.""" # Simple 4-point MST: 0-1 (dist=1.0), 1-2 (dist=2.0), 2-3 (dist=3.0) return np.array([ [0, 1, 1.0], [1, 2, 2.0], [2, 3, 3.0] ], dtype=np.float64) def test_mst_to_linkage_tree_basic(self, simple_mst): """Test basic MST to linkage tree conversion.""" linkage_tree = mst_to_linkage_tree(simple_mst) # Should have same number of rows as MST assert linkage_tree.shape[0] == simple_mst.shape[0] assert linkage_tree.shape[1] == 4 # left, right, distance, size # Check that distances are preserved assert np.array_equal(linkage_tree[:, 2], simple_mst[:, 2]) # Check that cluster sizes make sense (should be increasing) sizes = linkage_tree[:, 3] assert sizes[0] == 2 # First merge: 2 points assert sizes[1] == 3 # Second merge: 3 points assert sizes[2] == 4 # Final merge: all 4 points def test_mst_to_linkage_tree_ordering(self, simple_mst): """Test that linkage tree maintains proper ordering.""" linkage_tree = mst_to_linkage_tree(simple_mst) # In each row, larger cluster index should be in column 0 for i in range(linkage_tree.shape[0]): assert linkage_tree[i, 0] >= linkage_tree[i, 1] def test_mst_to_linkage_tree_random(self): """Test with a larger random MST.""" np.random.seed(42) n_points = 10 # Create a random MST (n_points - 1 edges) edges = [] for i in range(n_points - 1): edges.append([i, i + 1, np.random.random()]) mst = np.array(edges, dtype=np.float64) mst = mst[np.argsort(mst[:, 2])] # Sort by distance linkage_tree = mst_to_linkage_tree(mst) assert linkage_tree.shape[0] == n_points - 1 assert linkage_tree.shape[1] == 4 assert linkage_tree[-1, 3] == n_points # Final cluster has all points class TestBFSFromHierarchy: """Test breadth-first search on hierarchy.""" @pytest.fixture def simple_hierarchy(self): """Create a simple hierarchy for testing. In scipy linkage format: - Points: 0, 1, 2, 3 (original data) - Clusters: 4, 5, 6 (formed by merges) - Row 0: merge to form cluster 4 (n_points + 0) - Row 1: merge to form cluster 5 (n_points + 1) - Row 2: merge to form cluster 6 (n_points + 2) """ return np.array([ [0, 1, 1.0, 2], # Row 0: merge points 0,1 -> cluster 4 [2, 3, 2.0, 2], # Row 1: merge points 2,3 -> cluster 5 [4, 5, 3.0, 4], # Row 2: merge clusters 4,5 -> cluster 6 (root) ], dtype=np.float64) def test_bfs_leaf_node(self, simple_hierarchy): """Test BFS starting from a leaf node (original data point).""" result = bfs_from_hierarchy(simple_hierarchy, 0, 4) assert result == [0] # Leaf node should return itself def test_bfs_internal_node(self, simple_hierarchy): """Test BFS starting from an internal cluster.""" # Cluster 4 (formed by merging points 0,1) result = bfs_from_hierarchy(simple_hierarchy, 4, 4) expected = [4, 0, 1] # Should include the cluster and its children assert result == expected def test_bfs_root_node(self, simple_hierarchy): """Test BFS starting from the root cluster.""" # Cluster 6 is the root (formed by merging clusters 4,5) result = bfs_from_hierarchy(simple_hierarchy, 6, 4) expected = [6, 4, 5, 0, 1, 2, 3] # Should traverse entire tree assert set(result) == set(expected) # Order may vary in BFS class TestCondenseTree: """Test tree condensation functionality.""" @pytest.fixture def sample_hierarchy(self): """Create a sample hierarchy for testing.""" # Create hierarchy for 6 points return np.array([ [0, 1, 0.1, 2], # Cluster 6: points 0,1 [2, 3, 0.2, 2], # Cluster 7: points 2,3 [6, 7, 0.3, 4], # Cluster 8: clusters 6,7 [8, 4, 0.4, 5], # Cluster 9: cluster 8 + point 4 [9, 5, 0.5, 6], # Cluster 10: cluster 9 + point 5 (root) ], dtype=np.float64) def test_condense_tree_basic(self, sample_hierarchy): """Test basic tree condensation.""" min_cluster_size = 3 condensed = condense_tree(sample_hierarchy, min_cluster_size) # Check structure assert isinstance(condensed, CondensedTree) assert len(condensed.parent) == len(condensed.child) assert len(condensed.parent) == len(condensed.lambda_val) assert len(condensed.parent) == len(condensed.child_size) # Lambda values should be positive assert np.all(condensed.lambda_val > 0) # Child sizes should be reasonable assert np.all(condensed.child_size >= 1) def test_condense_tree_min_cluster_size_effect(self, sample_hierarchy): """Test that different min_cluster_size values produce different results.""" condensed_small = condense_tree(sample_hierarchy, min_cluster_size=2) condensed_large = condense_tree(sample_hierarchy, min_cluster_size=4) # Different min_cluster_size should affect the result structure # (Exact comparison depends on the specific condensation logic) assert len(condensed_small.parent) >= 0 assert len(condensed_large.parent) >= 0 def test_condense_tree_lambda_values(self, sample_hierarchy): """Test that lambda values are computed correctly (1/distance).""" condensed = condense_tree(sample_hierarchy, min_cluster_size=2) # All lambda values should be finite and positive assert np.all(np.isfinite(condensed.lambda_val)) assert np.all(condensed.lambda_val > 0) class TestExtractLeaves: """Test leaf extraction from condensed trees.""" def test_extract_leaves_simple(self): """Test leaf extraction from a simple condensed tree.""" # Create simple condensed tree manually parent = np.array([5, 5, 5]) child = np.array([0, 1, 2]) # Three leaf points lambda_val = np.array([1.0, 1.0, 1.0]) child_size = np.array([1, 1, 1]) condensed = CondensedTree(parent, child, lambda_val, child_size) leaves = extract_leaves(condensed) # Node 5 should be identified as a leaf cluster assert 5 in leaves def test_extract_leaves_hierarchical(self): """Test leaf extraction from a hierarchical condensed tree.""" # Create a tree where node 5 has children that are clusters (not just points) parent = np.array([6, 6, 5, 5]) child = np.array([5, 0, 1, 2]) # Node 5 is internal (has child_size > 1) lambda_val = np.array([1.0, 1.0, 1.0, 1.0]) child_size = np.array([3, 1, 1, 1]) # Node 5 entry has child_size=3 condensed = CondensedTree(parent, child, lambda_val, child_size) leaves = extract_leaves(condensed) # Based on the extract_leaves logic, clusters with child_size > 1 # in their entries are leaf clusters if len(leaves) > 0: for leaf in leaves: # Find entries where this node is the child mask = condensed.child == leaf if np.any(mask): # At least one entry should have child_size > 1 assert np.any(condensed.child_size[mask] > 1) class TestClusterLabeling: """Test cluster labeling and membership functions.""" @pytest.fixture def sample_condensed_tree(self): """Create a sample condensed tree for testing.""" parent = np.array([10, 10, 10, 11, 11]) child = np.array([0, 1, 2, 3, 4]) lambda_val = np.array([2.0, 2.0, 2.0, 1.0, 1.0]) child_size = np.array([1, 1, 1, 1, 1]) return CondensedTree(parent, child, lambda_val, child_size) def test_get_cluster_label_vector_single_cluster(self, sample_condensed_tree): """Test cluster labeling with a single cluster.""" clusters = np.array([10]) labels = get_cluster_label_vector( sample_condensed_tree, clusters, 0.0, 5 ) assert len(labels) == 5 # Points 0, 1, 2 should be in cluster 0 (they have high lambda values) assert labels[0] == 0 assert labels[1] == 0 assert labels[2] == 0 # Points 3, 4 should be noise (-1) (they have lower lambda values) assert labels[3] == -1 assert labels[4] == -1 def test_get_cluster_label_vector_multiple_clusters(self, sample_condensed_tree): """Test cluster labeling with multiple clusters.""" clusters = np.array([10, 11]) labels = get_cluster_label_vector( sample_condensed_tree, clusters, 0.0, 5 ) assert len(labels) == 5 # Should have valid cluster assignments unique_labels = np.unique(labels) assert -1 in unique_labels or len(unique_labels) > 1 def test_get_point_membership_strength_vector(self, sample_condensed_tree): """Test membership strength calculation.""" clusters = np.array([10, 11]) labels = get_cluster_label_vector( sample_condensed_tree, clusters, 0.0, 5 ) strengths = get_point_membership_strength_vector( sample_condensed_tree, clusters, labels ) assert len(strengths) == 5 assert np.all(strengths >= 0.0) assert np.all(strengths <= 1.0) # Points with valid cluster assignments should have positive strength valid_points = labels >= 0 if np.any(valid_points): assert np.all(strengths[valid_points] > 0) class TestIntegrationWithRealData: """Integration tests using real clustered data.""" @pytest.fixture def clustered_data(self): """Generate clustered data for integration testing.""" np.random.seed(42) X, y = make_blobs(n_samples=50, centers=3, random_state=42) X = StandardScaler().fit_transform(X) return X, y def test_full_pipeline_simple_mst(self, clustered_data): """Test the full pipeline with a simple MST.""" X, true_labels = clustered_data # Create a simple MST by connecting points sequentially n_samples = X.shape[0] mst_edges = [] for i in range(n_samples - 1): # Connect point i to point i+1 with random distance mst_edges.append([i, i + 1, np.random.random()]) mst = np.array(mst_edges, dtype=np.float64) mst = mst[np.argsort(mst[:, 2])] # Sort by distance # Convert to linkage tree linkage_tree = mst_to_linkage_tree(mst) # Condense tree condensed = condense_tree(linkage_tree, min_cluster_size=5) # Extract clusters leaves = extract_leaves(condensed) # Get cluster labels if len(leaves) > 0: labels = get_cluster_label_vector(condensed, leaves, 0.0, n_samples) # Basic sanity checks assert len(labels) == n_samples assert np.all(labels >= -1) # Valid range for labels # Should find some clusters or noise n_clusters = len(np.unique(labels[labels >= 0])) assert n_clusters >= 0 # Could be all noise def test_score_condensed_tree_nodes(self): """Test scoring of condensed tree nodes.""" # Create a simple condensed tree parent = np.array([5, 5, 5]) child = np.array([0, 1, 2]) lambda_val = np.array([2.0, 1.5, 1.0]) child_size = np.array([1, 1, 1]) condensed = CondensedTree(parent, child, lambda_val, child_size) scores = score_condensed_tree_nodes(condensed) # Node 5 should have a positive score assert 5 in scores assert scores[5] > 0 class TestEdgeCases: """Test edge cases and error conditions.""" def test_extract_leaves_empty_tree(self): """Test behavior with empty condensed trees.""" empty_condensed = CondensedTree( np.array([], dtype=np.int64), np.array([], dtype=np.int64), np.array([], dtype=np.float32), np.array([], dtype=np.int64) ) # Should handle empty input gracefully leaves = extract_leaves(empty_condensed) assert len(leaves) == 0 or isinstance(leaves, np.ndarray) def test_single_point_mst(self): """Test with MST containing only one edge (two points).""" mst = np.array([[0, 1, 1.0]], dtype=np.float64) linkage_tree = mst_to_linkage_tree(mst) assert linkage_tree.shape[0] == 1 assert linkage_tree[0, 3] == 2 # Should connect 2 points def test_zero_distance_edges(self): """Test handling of zero-distance edges in MST.""" mst = np.array([ [0, 1, 0.0], # Zero distance [1, 2, 1.0] ], dtype=np.float64) linkage_tree = mst_to_linkage_tree(mst) condensed = condense_tree(linkage_tree, min_cluster_size=2) # Should handle zero distances gracefully # (may result in infinite lambda values) if len(condensed.lambda_val) > 0: finite_mask = np.isfinite(condensed.lambda_val) # At least some lambda values should be finite assert np.any(finite_mask) or np.any(np.isinf(condensed.lambda_val)) class TestBFSEdgeCases: """Test edge cases for BFS functionality.""" def test_bfs_single_point_hierarchy(self): """Test BFS with minimal hierarchy.""" # Single merge hierarchy for 2 points hierarchy = np.array([[0, 1, 1.0, 2]], dtype=np.float64) result = bfs_from_hierarchy(hierarchy, 2, 2) # Cluster 2 (n_points + 0) assert set(result) == {2, 0, 1} def test_eliminate_branch_leaf(self): """Test eliminate_branch with a leaf node.""" hierarchy = np.array([[0, 1, 1.0, 2]], dtype=np.float64) parents = np.zeros(10, dtype=np.int64) children = np.zeros(10, dtype=np.int64) lambdas = np.zeros(10, dtype=np.float32) sizes = np.zeros(10, dtype=np.int64) ignore = np.zeros(10, dtype=bool) # Eliminate a leaf node (point 0) new_idx = eliminate_branch(0, 5, 1.0, parents, children, lambdas, sizes, 0, ignore, hierarchy, 2) assert new_idx == 1 # Should increment index assert parents[0] == 5 assert children[0] == 0 assert lambdas[0] == 1.0 # Utility function for running integration tests def test_cluster_trees_integration(): """High-level integration test of the entire cluster_trees module.""" np.random.seed(42) # Generate test data X, _ = make_blobs(n_samples=20, centers=2, random_state=42) X = StandardScaler().fit_transform(X) # Create a minimal MST (for testing purposes) n_samples = X.shape[0] mst_edges = [] for i in range(n_samples - 1): mst_edges.append([i, i + 1, np.random.random()]) mst = np.array(mst_edges, dtype=np.float64) mst = mst[np.argsort(mst[:, 2])] # Test the full pipeline linkage_tree = mst_to_linkage_tree(mst) condensed = condense_tree(linkage_tree, min_cluster_size=3) leaves = extract_leaves(condensed) if len(leaves) > 0: labels = get_cluster_label_vector(condensed, leaves, 0.0, n_samples) strengths = get_point_membership_strength_vector(condensed, leaves, labels) # Verify results make sense assert len(labels) == n_samples assert len(strengths) == n_samples assert np.all(strengths >= 0.0) and np.all(strengths <= 1.0) # Test passed if we reach here without errors assert True def test_linkage_merge_data_comprehensive(): """Additional comprehensive test for linkage merge operations.""" base_size = 6 linkage_data = create_linkage_merge_data(base_size) # Test multiple sequential merges linkage_merge_join(linkage_data, 0, 1) # Creates cluster 6 linkage_merge_join(linkage_data, 2, 3) # Creates cluster 7 linkage_merge_join(linkage_data, 6, 7) # Creates cluster 8 # Verify the structure after multiple merges assert linkage_data.size[6] == 2 # Cluster 6 has 2 points assert linkage_data.size[7] == 2 # Cluster 7 has 2 points assert linkage_data.size[8] == 4 # Cluster 8 has 4 points assert linkage_data.next[0] == 9 # Next available cluster ID # Test path compression in find assert linkage_merge_find(linkage_data, 0) == 8 # Should find root cluster assert linkage_merge_find(linkage_data, 2) == 8 # Should find same root ================================================ FILE: evoc/tests/test_clustering.py ================================================ """ Comprehensive test suite for the clustering module. This module tests the EVoC clustering algorithm implementation, including binary search for clusters, cluster layer building, duplicate detection, and the main EVoC class functionality. """ import numpy as np import pytest from sklearn.datasets import make_blobs, make_circles from sklearn.preprocessing import StandardScaler from sklearn.metrics import adjusted_rand_score, silhouette_score from evoc.clustering import ( build_cluster_layers, evoc_clusters, EVoC, ) from evoc.clustering_utilities import ( _binary_search_for_n_clusters, find_duplicates, _build_cluster_tree, build_cluster_tree, binary_search_for_n_clusters, ) import numba from evoc.numba_kdtree import build_kdtree from evoc.boruvka import parallel_boruvka from evoc.cluster_trees import mst_to_linkage_tree @pytest.fixture def simple_embedding_data(): """Create simple high-dimensional embedding-like data for testing.""" # Create 512-dimensional data similar to CLIP embeddings X, y = make_blobs( n_samples=800, centers=4, n_features=512, cluster_std=0.8, random_state=42 ) # Normalize to unit sphere (typical for embeddings) X = X / np.linalg.norm(X, axis=1, keepdims=True) return X.astype(np.float32), y @pytest.fixture def complex_embedding_data(): """Create more complex high-dimensional embedding-like data for testing.""" # Create 768-dimensional data similar to sentence transformer embeddings X, y = make_blobs( n_samples=2000, centers=8, n_features=768, cluster_std=0.6, random_state=42 ) # Normalize to unit sphere and add some noise X = X / np.linalg.norm(X, axis=1, keepdims=True) X += np.random.normal(0, 0.05, X.shape) X = X / np.linalg.norm(X, axis=1, keepdims=True) return X.astype(np.float32), y @pytest.fixture def small_embedding_data(): """Create small high-dimensional data for quick testing.""" # Create 384-dimensional data (smaller embedding size) X, y = make_blobs( n_samples=300, centers=3, n_features=384, cluster_std=0.7, random_state=42 ) # Normalize to unit sphere X = X / np.linalg.norm(X, axis=1, keepdims=True) return X.astype(np.float32), y @pytest.fixture def duplicate_embedding_data(): """Create high-dimensional embedding data with some duplicate points for testing.""" X, y = make_blobs(n_samples=400, centers=3, n_features=512, random_state=42) # Normalize to unit sphere X = X / np.linalg.norm(X, axis=1, keepdims=True) # Add some duplicate points X_with_dups = np.vstack([X, X[:20]]) # Duplicate first 20 points y_with_dups = np.hstack([y, y[:20]]) return X_with_dups.astype(np.float32), y_with_dups @pytest.fixture def quantized_embedding_data(): """Create quantized (int8) embedding data for testing.""" X, y = make_blobs(n_samples=600, centers=4, n_features=256, random_state=42) # Normalize and quantize to int8 range X = X / np.linalg.norm(X, axis=1, keepdims=True) X_quantized = (X * 127).astype(np.int8) return X_quantized, y @pytest.fixture def binary_embedding_data(): """Create binary (uint8) embedding data for testing.""" X, y = make_blobs(n_samples=500, centers=3, n_features=128, random_state=42) # Convert to binary representation X_binary = (X > np.median(X, axis=1, keepdims=True)).astype(np.uint8) return X_binary, y @pytest.fixture def small_linkage_tree(): """Create a small linkage tree for testing.""" # Create simple high-dimensional data and build MST X, _ = make_blobs(n_samples=100, centers=3, n_features=128, random_state=42) # Normalize to unit sphere like embeddings X = X / np.linalg.norm(X, axis=1, keepdims=True) numba_tree = build_kdtree(X.astype(np.float32)) num_threads = numba.get_num_threads() edges = parallel_boruvka(numba_tree, num_threads, min_samples=3, reproducible=False) sorted_mst = edges[np.argsort(edges.T[2])] return mst_to_linkage_tree(sorted_mst) class TestBinarySearchForNClusters: """Test the binary search functionality for finding n clusters.""" def test_binary_search_basic(self, small_linkage_tree): """Test basic binary search for cluster count.""" n_samples = 100 target_clusters = 3 leaves, clusters, strengths = _binary_search_for_n_clusters( small_linkage_tree, target_clusters, n_samples ) # Check return types and shapes assert isinstance(leaves, np.ndarray) assert isinstance(clusters, np.ndarray) assert isinstance(strengths, np.ndarray) assert len(clusters) == n_samples assert len(strengths) == n_samples # Check that we have reasonable cluster count n_clusters = len(np.unique(clusters[clusters >= 0])) assert n_clusters > 0 assert n_clusters <= n_samples # Check that strengths are in valid range assert np.all(strengths >= 0) assert np.all(strengths <= 1) def test_binary_search_edge_cases(self, small_linkage_tree): """Test binary search with edge case parameters.""" n_samples = 100 # Test with very few clusters leaves, clusters, strengths = _binary_search_for_n_clusters( small_linkage_tree, 1, n_samples ) assert len(clusters) == n_samples # Test with many clusters leaves, clusters, strengths = _binary_search_for_n_clusters( small_linkage_tree, 50, n_samples ) assert len(clusters) == n_samples def test_binary_search_wrapper_function(self, simple_embedding_data): """Test the wrapper binary_search_for_n_clusters function.""" X, y_true = simple_embedding_data num_threads = numba.get_num_threads() clusters, strengths = binary_search_for_n_clusters( X, approx_n_clusters=3, n_threads=num_threads, min_samples=5 ) # Check return types and shapes assert isinstance(clusters, np.ndarray) assert isinstance(strengths, np.ndarray) assert len(clusters) == len(X) assert len(strengths) == len(X) # Check that we found reasonable clusters n_clusters = len(np.unique(clusters[clusters >= 0])) assert n_clusters > 0 assert n_clusters <= len(X) class TestBuildClusterLayers: """Test the cluster layer building functionality.""" def test_build_cluster_layers_basic(self, simple_embedding_data): """Test basic cluster layer building.""" X, y_true = simple_embedding_data cluster_layers, membership_strengths, persistence_scores = build_cluster_layers( X, min_samples=5, base_min_cluster_size=10, ) # Check return types assert isinstance(cluster_layers, list) assert isinstance(membership_strengths, list) assert len(cluster_layers) == len(membership_strengths) # Check that all layers have correct shape for clusters, strengths in zip(cluster_layers, membership_strengths): assert len(clusters) == len(X) assert len(strengths) == len(X) assert np.all(strengths >= 0) assert np.all(strengths <= 1) def test_build_cluster_layers_with_base_n_clusters(self, simple_embedding_data): """Test cluster layer building with specified base cluster count.""" X, y_true = simple_embedding_data cluster_layers, membership_strengths, persistence_scores = build_cluster_layers( X, base_n_clusters=3, min_samples=5, ) assert len(cluster_layers) > 0 assert len(membership_strengths) > 0 # Check that first layer has reasonable cluster count first_layer_clusters = cluster_layers[0] n_clusters = len(np.unique(first_layer_clusters[first_layer_clusters >= 0])) assert n_clusters > 0 def test_build_cluster_layers_reproducible(self, simple_embedding_data): """Test that cluster layer building is reproducible.""" X, y_true = simple_embedding_data layers1, strengths1, persistence1 = build_cluster_layers( X, base_min_cluster_size=10, reproducible_flag=True ) layers2, strengths2, persistence2 = build_cluster_layers( X, base_min_cluster_size=10, reproducible_flag=True ) # Results should be identical when reproducible flag is set assert len(layers1) == len(layers2) for l1, l2 in zip(layers1, layers2): np.testing.assert_array_equal(l1, l2) class TestFindDuplicates: """Test the duplicate detection functionality.""" def test_find_duplicates_basic(self): """Test basic duplicate detection.""" # Create simple k-NN data with some duplicates knn_inds = np.array( [ [0, 1, 2], [1, 0, 2], [2, 0, 1], [3, 0, 1], # Point 3 is close to points 0 and 1 ], dtype=np.int32, ) knn_dists = np.array( [ [0.0, 0.5, 1.0], [0.5, 0.0, 1.0], [1.0, 0.5, 0.0], [0.8, 0.0, 0.0], # Duplicate distance (0.0) indicates duplicates ], dtype=np.float32, ) duplicates = find_duplicates(knn_inds, knn_dists) # Check return type assert isinstance(duplicates, set) # Check that duplicates are tuples of pairs for dup in duplicates: assert isinstance(dup, tuple) assert len(dup) == 2 assert dup[0] < dup[1] # Should be ordered pairs def test_find_duplicates_no_duplicates(self): """Test duplicate detection when no duplicates exist.""" knn_inds = np.array([[0, 1, 2], [1, 0, 2], [2, 0, 1]], dtype=np.int32) knn_dists = np.array( [[0.1, 0.5, 1.0], [0.5, 0.1, 1.0], [1.0, 0.5, 0.1]], dtype=np.float32 ) duplicates = find_duplicates(knn_inds, knn_dists) # Should find minimal or no duplicates assert isinstance(duplicates, set) class TestBuildClusterTree: """Test the cluster tree building functionality.""" def test_build_cluster_tree_basic(self): """Test basic cluster tree building.""" # Create simple hierarchical cluster labels labels = [ np.array([0, 0, 1, 1, 2, 2]), # Fine-grained np.array([0, 0, 0, 1, 1, 1]), # Coarse-grained ] tree = build_cluster_tree(labels) # Check return type assert isinstance(tree, dict) # Check that keys are tuples (layer, cluster) for key in tree.keys(): assert isinstance(key, tuple) assert len(key) == 2 assert isinstance(key[0], (int, np.integer)) assert isinstance(key[1], (int, np.integer)) # Check that values are lists of child clusters for value in tree.values(): assert isinstance(value, list) for child in value: assert isinstance(child, tuple) assert len(child) == 2 def test_build_cluster_tree_empty(self): """Test cluster tree building with empty input.""" labels = [] # Empty input should be handled gracefully # Note: This may raise an error due to numba limitations with empty lists with pytest.raises((ValueError, Exception)): tree = build_cluster_tree(labels) def test_build_cluster_tree_single_layer(self): """Test cluster tree building with single layer.""" labels = [np.array([0, 1, 0, 1, 2])] tree = build_cluster_tree(labels) assert isinstance(tree, dict) class TestEvocClusters: """Test the main evoc_clusters function.""" def test_evoc_clusters_basic(self, simple_embedding_data): """Test basic EVoC clustering.""" X, y_true = simple_embedding_data cluster_layers, membership_strengths, persistence_scores, _, _ = evoc_clusters( X, noise_level=0.5, base_min_cluster_size=5, base_n_clusters=2, n_neighbors=10, min_samples=3, n_epochs=20, random_state=np.random.RandomState(42), ) # Check return types assert isinstance(cluster_layers, list) assert isinstance(membership_strengths, list) assert len(cluster_layers) == len(membership_strengths) assert len(cluster_layers) > 0 # Check shapes for clusters, strengths in zip(cluster_layers, membership_strengths): assert len(clusters) == len(X) assert len(strengths) == len(X) def test_evoc_clusters_with_approx_n_clusters(self, simple_embedding_data): """Test EVoC clustering with specified cluster count.""" X, y_true = simple_embedding_data cluster_layers, membership_strengths, persistence_scores, _, _ = evoc_clusters( X, approx_n_clusters=3, n_neighbors=10, min_samples=3, n_epochs=20, random_state=np.random.RandomState(42), ) # Should return exactly one layer assert len(cluster_layers) == 1 assert len(membership_strengths) == 1 # Check that we found reasonable clusters clusters = cluster_layers[0] n_clusters = len(np.unique(clusters[clusters >= 0])) assert n_clusters > 0 def test_evoc_clusters_with_duplicates(self, duplicate_embedding_data): """Test EVoC clustering with duplicate detection.""" X, y_true = duplicate_embedding_data cluster_layers, membership_strengths, persistence_scores, _, _, duplicates = ( evoc_clusters( X, return_duplicates=True, n_neighbors=10, min_samples=3, n_epochs=20, random_state=np.random.RandomState(42), ) ) # Check that duplicates are returned assert isinstance(duplicates, set) # Check other return values assert isinstance(cluster_layers, list) assert isinstance(membership_strengths, list) def test_evoc_clusters_different_data_types( self, quantized_embedding_data, binary_embedding_data ): """Test EVoC clustering with different embedding data types.""" # Test with float32 data (standard embeddings) X_float = np.random.rand(100, 256).astype(np.float32) # Normalize like real embeddings X_float = X_float / np.linalg.norm(X_float, axis=1, keepdims=True) clusters, strengths, persistence_scores, _, _ = evoc_clusters( X_float, approx_n_clusters=4, n_epochs=10, random_state=np.random.RandomState(42), ) assert len(clusters) == 1 assert len(clusters[0]) == 100 # Test with int8 data (quantized embeddings) X_int8, _ = quantized_embedding_data clusters, strengths, persistence_scores, _, _ = evoc_clusters( X_int8, approx_n_clusters=3, n_epochs=10, random_state=np.random.RandomState(42), ) assert len(clusters) == 1 assert len(clusters[0]) == len(X_int8) # Test with uint8 data (binary embeddings) X_uint8, _ = binary_embedding_data clusters, strengths, persistence_scores, _, _ = evoc_clusters( X_uint8, approx_n_clusters=3, n_epochs=10, random_state=np.random.RandomState(42), ) assert len(clusters) == 1 assert len(clusters[0]) == len(X_uint8) class TestEVoCClass: """Test the EVoC class implementation.""" def test_evoc_init(self): """Test EVoC class initialization.""" clusterer = EVoC( noise_level=0.3, base_min_cluster_size=10, n_neighbors=20, n_epochs=30, random_state=42, ) # Check that parameters are set correctly assert clusterer.noise_level == 0.3 assert clusterer.base_min_cluster_size == 10 assert clusterer.n_neighbors == 20 assert clusterer.n_epochs == 30 assert clusterer.random_state == 42 def test_evoc_fit_predict(self, simple_embedding_data): """Test EVoC fit_predict method.""" X, y_true = simple_embedding_data clusterer = EVoC( base_min_cluster_size=5, n_neighbors=10, n_epochs=20, random_state=42 ) labels = clusterer.fit_predict(X) # Check return type and shape assert isinstance(labels, np.ndarray) assert len(labels) == len(X) # Check that clusterer has fitted attributes assert hasattr(clusterer, "labels_") assert hasattr(clusterer, "membership_strengths_") assert hasattr(clusterer, "cluster_layers_") assert hasattr(clusterer, "membership_strength_layers_") assert hasattr(clusterer, "duplicates_") # Check that labels are consistent np.testing.assert_array_equal(labels, clusterer.labels_) def test_evoc_fit(self, simple_embedding_data): """Test EVoC fit method.""" X, y_true = simple_embedding_data clusterer = EVoC( base_min_cluster_size=5, n_neighbors=10, n_epochs=20, random_state=42 ) result = clusterer.fit(X) # Check that fit returns self assert result is clusterer # Check that clusterer has fitted attributes assert hasattr(clusterer, "labels_") assert hasattr(clusterer, "membership_strengths_") def test_evoc_with_approx_n_clusters(self, simple_embedding_data): """Test EVoC with specified cluster count.""" X, y_true = simple_embedding_data clusterer = EVoC( approx_n_clusters=3, n_neighbors=10, n_epochs=20, random_state=42 ) labels = clusterer.fit_predict(X) # Check that we have reasonable cluster count n_clusters = len(np.unique(labels[labels >= 0])) assert n_clusters > 0 assert n_clusters <= len(X) def test_evoc_cluster_tree_property(self, simple_embedding_data): """Test EVoC cluster_tree_ property.""" X, y_true = simple_embedding_data clusterer = EVoC( base_min_cluster_size=5, n_neighbors=10, n_epochs=20, random_state=42 ) clusterer.fit(X) # Test cluster tree property tree = clusterer.cluster_tree_ assert isinstance(tree, dict) def test_evoc_cluster_tree_not_fitted(self): """Test that cluster_tree_ raises error when not fitted.""" clusterer = EVoC() with pytest.raises(Exception): # Should raise NotFittedError or similar _ = clusterer.cluster_tree_ def test_evoc_reproducibility(self, simple_embedding_data): """Test that EVoC produces reproducible results.""" X, y_true = simple_embedding_data clusterer1 = EVoC( base_min_cluster_size=5, n_neighbors=10, n_epochs=20, random_state=42 ) clusterer2 = EVoC( base_min_cluster_size=5, n_neighbors=10, n_epochs=20, random_state=42 ) labels1 = clusterer1.fit_predict(X) labels2 = clusterer2.fit_predict(X) # Results should be identical with same random state np.testing.assert_array_equal(labels1, labels2) class TestClusteringQuality: """Test clustering quality metrics and edge cases.""" def test_clustering_quality_on_embeddings(self, simple_embedding_data): """Test that clustering works well on high-dimensional embedding data.""" X, y_true = simple_embedding_data clusterer = EVoC( base_n_clusters=4, # Match true number of clusters n_neighbors=15, n_epochs=30, random_state=42, ) try: labels = clusterer.fit_predict(X) # Calculate clustering quality metrics # Remove noise points for ARI calculation mask = labels >= 0 if np.sum(mask) > 0: ari = adjusted_rand_score(y_true[mask], labels[mask]) # Should achieve reasonable clustering quality on embeddings assert ari > 0.2 # Relaxed threshold for high-dimensional data # Check silhouette score (only if we have multiple clusters) n_clusters = len(np.unique(labels[labels >= 0])) if n_clusters > 1: sil_score = silhouette_score(X[mask], labels[mask]) assert sil_score > 0.05 # Very relaxed for high-dimensional data except ValueError as e: # Handle case where clustering fails to find layers if "empty sequence" in str(e): pytest.skip("Clustering failed to find cluster layers for this data") def test_clustering_quality_on_blobs(self): """Test that clustering works on traditional blob data for compatibility.""" # Create traditional 2D blob data for compatibility testing X, y_true = make_blobs( n_samples=100, centers=3, n_features=2, cluster_std=1.0, random_state=42 ) X = StandardScaler().fit_transform(X).astype(np.float32) clusterer = EVoC( base_n_clusters=3, # Match true number of clusters n_neighbors=15, n_epochs=30, random_state=42, ) try: labels = clusterer.fit_predict(X) # Calculate clustering quality metrics # Remove noise points for ARI calculation mask = labels >= 0 if np.sum(mask) > 0: ari = adjusted_rand_score(y_true[mask], labels[mask]) # Should achieve good clustering quality on simple blobs assert ari > 0.3 # Check silhouette score (only if we have multiple clusters) n_clusters = len(np.unique(labels[labels >= 0])) if n_clusters > 1: sil_score = silhouette_score(X[mask], labels[mask]) assert sil_score > 0.1 except ValueError as e: # Handle case where clustering fails to find layers if "empty sequence" in str(e): pytest.skip("Clustering failed to find cluster layers for this data") def test_clustering_on_small_dataset(self): """Test clustering on very small dataset.""" # Use small but realistic embedding-like data X = np.random.rand(50, 256) # Normalize like embeddings X = X / np.linalg.norm(X, axis=1, keepdims=True) X = X.astype(np.float32) clusterer = EVoC( base_min_cluster_size=2, n_neighbors=5, n_epochs=10, random_state=42 ) try: labels = clusterer.fit_predict(X) # Should not crash and should return valid labels assert len(labels) == len(X) assert np.all((labels >= -1) & (labels < len(X))) except ValueError as e: # Handle case where clustering fails due to small dataset if "empty sequence" in str(e): pytest.skip( "Clustering failed on very small dataset - expected behavior" ) def test_clustering_on_high_dimensional_data(self): """Test clustering on very high-dimensional embedding data.""" # Test with 1024-dimensional data similar to large transformer models X = np.random.rand(500, 1024) # Normalize to unit sphere like real embeddings X = X / np.linalg.norm(X, axis=1, keepdims=True) X = X.astype(np.float32) clusterer = EVoC( base_min_cluster_size=8, n_neighbors=12, n_epochs=20, random_state=42 ) labels = clusterer.fit_predict(X) # Should handle very high dimensions gracefully assert len(labels) == len(X) assert np.all((labels >= -1) & (labels < len(X))) def test_edge_case_single_cluster(self): """Test edge case where all data forms single cluster.""" # Create very tight cluster X = np.random.normal(0, 0.01, (50, 5)) clusterer = EVoC( base_min_cluster_size=10, n_neighbors=15, n_epochs=20, random_state=42 ) try: labels = clusterer.fit_predict(X) # Should handle single cluster case assert len(labels) == len(X) except ValueError as e: # Handle case where clustering fails due to single tight cluster if "empty sequence" in str(e): pytest.skip( "Clustering failed on single tight cluster - expected behavior" ) def test_parameter_validation(self): """Test that invalid parameters are handled appropriately.""" # These should not crash during initialization clusterer = EVoC( noise_level=-0.1, # Invalid but should be clamped/handled base_min_cluster_size=1, # Very small n_neighbors=1, # Very small n_epochs=1, # Very small ) # Should initialize without error assert isinstance(clusterer, EVoC) def test_clustering_on_clip_like_embeddings(self): """Test clustering on CLIP-like 512-dimensional embeddings.""" # Simulate CLIP embeddings with multiple semantic clusters np.random.seed(42) n_samples_per_cluster = 80 n_clusters = 5 cluster_centers = np.random.randn(n_clusters, 512) cluster_centers = cluster_centers / np.linalg.norm( cluster_centers, axis=1, keepdims=True ) X = [] y_true = [] for i, center in enumerate(cluster_centers): # Generate points around each center cluster_points = center + np.random.normal( 0, 0.1, (n_samples_per_cluster, 512) ) # Normalize to unit sphere cluster_points = cluster_points / np.linalg.norm( cluster_points, axis=1, keepdims=True ) X.append(cluster_points) y_true.extend([i] * n_samples_per_cluster) X = np.vstack(X).astype(np.float32) y_true = np.array(y_true) clusterer = EVoC( base_n_clusters=n_clusters, n_neighbors=15, n_epochs=25, random_state=42 ) labels = clusterer.fit_predict(X) # Should handle CLIP-like embeddings well assert len(labels) == len(X) mask = labels >= 0 if np.sum(mask) > 0: n_found_clusters = len(np.unique(labels[mask])) assert n_found_clusters > 1 # Should find multiple clusters # Check clustering quality ari = adjusted_rand_score(y_true[mask], labels[mask]) assert ( ari > 0.15 ) # Should achieve reasonable clustering on well-separated data def test_clustering_on_sentence_transformer_like_embeddings(self): """Test clustering on sentence transformer-like 768-dimensional embeddings.""" # Simulate sentence transformer embeddings np.random.seed(123) n_samples = 600 n_dims = 768 # Create embeddings with some structure X = np.random.rand(n_samples, n_dims) - 0.5 # Add some clustering structure cluster_ids = np.random.choice([0, 1, 2, 3], n_samples) for i in range(4): mask = cluster_ids == i if np.sum(mask) > 0: # Add cluster-specific signal X[mask, i * 50 : (i + 1) * 50] += np.random.normal( 2.0, 0.5, (np.sum(mask), 50) ) # Normalize like sentence transformers X = X / np.linalg.norm(X, axis=1, keepdims=True) X = X.astype(np.float32) clusterer = EVoC( base_min_cluster_size=8, n_neighbors=20, n_epochs=30, random_state=42 ) labels = clusterer.fit_predict(X) # Should handle sentence transformer-like embeddings assert len(labels) == len(X) mask = labels >= 0 if np.sum(mask) > 0: n_found_clusters = len(np.unique(labels[mask])) assert n_found_clusters > 1 def test_clustering_on_quantized_embeddings(self, quantized_embedding_data): """Test clustering specifically on quantized int8 embeddings.""" X, y_true = quantized_embedding_data clusterer = EVoC( base_n_clusters=4, n_neighbors=12, n_epochs=20, random_state=42 ) labels = clusterer.fit_predict(X) # Should handle quantized embeddings assert len(labels) == len(X) assert np.all((labels >= -1) & (labels < len(X))) # Check that some clustering structure is found mask = labels >= 0 if np.sum(mask) > 0: n_clusters = len(np.unique(labels[mask])) assert n_clusters >= 1 def test_clustering_on_binary_embeddings(self, binary_embedding_data): """Test clustering specifically on binary uint8 embeddings.""" X, y_true = binary_embedding_data clusterer = EVoC( base_n_clusters=3, n_neighbors=10, n_epochs=15, random_state=42 ) try: labels = clusterer.fit_predict(X) # Should handle binary embeddings assert len(labels) == len(X) assert np.all((labels >= -1) & (labels < len(X))) # Check that some clustering structure is found mask = labels >= 0 if np.sum(mask) > 0: n_clusters = len(np.unique(labels[mask])) assert n_clusters >= 1 except ValueError as e: # Handle case where clustering fails on binary data if "empty sequence" in str(e): pytest.skip( "Clustering failed on binary embeddings - may need different parameters" ) if __name__ == "__main__": pytest.main([__file__]) ================================================ FILE: evoc/tests/test_knn_graph.py ================================================ """ Comprehensive test suite for the knn_graph module. This module tests the k-nearest neighbor graph construction functionality, including random projection forest building, nearest neighbor descent, and the main knn_graph function for different data types. """ import numpy as np import pytest import time from unittest.mock import patch from sklearn.datasets import make_blobs from sklearn.utils import check_random_state from evoc.knn_graph import ( ts, make_forest, nn_descent, knn_graph, INT32_MIN, INT32_MAX, ) class TestUtilityFunctions: """Test utility functions in the knn_graph module.""" def test_ts_returns_string(self): """Test that ts() returns a properly formatted timestamp string.""" timestamp = ts() assert isinstance(timestamp, str) assert len(timestamp) > 0 # Test that it's a valid time format by checking it contains expected components assert any( month in timestamp for month in [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", ] ) def test_ts_consistency(self): """Test that ts() returns consistent format across multiple calls.""" timestamp1 = ts() time.sleep(0.1) # Small delay to potentially get different timestamps timestamp2 = ts() # Both should be strings of reasonable length assert isinstance(timestamp1, str) assert isinstance(timestamp2, str) assert len(timestamp1) > 20 assert len(timestamp2) > 20 def test_constants(self): """Test that INT32_MIN and INT32_MAX are properly defined.""" assert INT32_MIN == np.iinfo(np.int32).min + 1 assert INT32_MAX == np.iinfo(np.int32).max - 1 assert INT32_MIN < INT32_MAX class TestMakeForest: """Test the make_forest function for different data types and parameters.""" @pytest.fixture def float_data(self): """Create normalized float32 test data.""" np.random.seed(42) data = np.random.rand(100, 50).astype(np.float32) # Normalize to unit sphere norms = np.linalg.norm(data, axis=1, keepdims=True) data = data / norms return data @pytest.fixture def uint8_data(self): """Create uint8 test data.""" np.random.seed(42) return np.random.randint(0, 256, size=(100, 50), dtype=np.uint8) @pytest.fixture def int8_data(self): """Create int8 test data.""" np.random.seed(42) return np.random.randint(-128, 128, size=(100, 50), dtype=np.int8) def test_make_forest_float32(self, float_data): """Test make_forest with float32 data.""" random_state = check_random_state(42) n_neighbors = 10 n_trees = 4 leaf_size = 20 result = make_forest( float_data, n_neighbors, n_trees, leaf_size, random_state, np.float32 ) assert isinstance(result, np.ndarray) assert result.dtype == np.int32 assert result.shape[0] >= n_trees # Should have at least n_trees rows def test_make_forest_uint8(self, uint8_data): """Test make_forest with uint8 data.""" random_state = check_random_state(42) n_neighbors = 10 n_trees = 4 leaf_size = 20 result = make_forest( uint8_data, n_neighbors, n_trees, leaf_size, random_state, np.uint8 ) assert isinstance(result, np.ndarray) assert result.dtype == np.int32 assert result.shape[0] >= n_trees def test_make_forest_int8(self, int8_data): """Test make_forest with int8 data.""" random_state = check_random_state(42) n_neighbors = 10 n_trees = 4 leaf_size = 20 result = make_forest( int8_data, n_neighbors, n_trees, leaf_size, random_state, np.int8 ) assert isinstance(result, np.ndarray) assert result.dtype == np.int32 assert result.shape[0] >= n_trees def test_make_forest_default_leaf_size(self, float_data): """Test make_forest with default leaf_size (None).""" random_state = check_random_state(42) n_neighbors = 15 n_trees = 4 result = make_forest( float_data, n_neighbors, n_trees, None, random_state, np.float32 ) assert isinstance(result, np.ndarray) assert result.dtype == np.int32 # With default leaf_size, it should be max(10, n_neighbors) = 15 def test_make_forest_different_max_depth(self, float_data): """Test make_forest with different max_depth values.""" random_state = check_random_state(42) n_neighbors = 10 n_trees = 2 leaf_size = 20 # Test with small max_depth result_shallow = make_forest( float_data, n_neighbors, n_trees, leaf_size, random_state, np.float32, max_depth=5, ) # Test with large max_depth random_state = check_random_state(42) # Reset for consistency result_deep = make_forest( float_data, n_neighbors, n_trees, leaf_size, random_state, np.float32, max_depth=500, ) assert isinstance(result_shallow, np.ndarray) assert isinstance(result_deep, np.ndarray) assert result_shallow.dtype == np.int32 assert result_deep.dtype == np.int32 @patch("evoc.knn_graph.make_float_forest") def test_make_forest_exception_handling(self, mock_make_float_forest, float_data): """Test make_forest handles exceptions properly.""" # Mock the forest creation to raise an exception mock_make_float_forest.side_effect = RuntimeError("Test exception") random_state = check_random_state(42) with pytest.warns( UserWarning, match="Random Projection forest initialisation failed" ): result = make_forest(float_data, 10, 4, 20, random_state, np.float32) # Should return empty array on exception assert isinstance(result, np.ndarray) assert result.shape == (0, 0) assert result.dtype == np.int32 class TestNNDescent: """Test the nn_descent function for different data types.""" @pytest.fixture def float_data(self): """Create normalized float32 test data.""" np.random.seed(42) data = np.random.rand(50, 20).astype(np.float32) norms = np.linalg.norm(data, axis=1, keepdims=True) data = data / norms return data @pytest.fixture def uint8_data(self): """Create uint8 test data.""" np.random.seed(42) return np.random.randint(0, 256, size=(50, 20), dtype=np.uint8) @pytest.fixture def int8_data(self): """Create int8 test data.""" np.random.seed(42) return np.random.randint(-128, 128, size=(50, 20), dtype=np.int8) def test_nn_descent_float32(self, float_data): """Test nn_descent with float32 data.""" rng_state = np.random.randint(INT32_MIN, INT32_MAX, 3).astype(np.int64) n_neighbors = 5 with patch("evoc.float_nndescent.nn_descent_float") as mock_nn_descent: # Mock return value: (indices, distances) mock_indices = np.random.randint( 0, len(float_data), size=(len(float_data), n_neighbors) ) mock_distances = -np.random.exponential( 1, size=(len(float_data), n_neighbors) ) leaf_array = np.random.randint( 0, float_data.shape[0], size=(4, len(float_data)), dtype=np.int32 ) mock_nn_descent.return_value = (mock_indices, mock_distances) result = nn_descent( float_data, n_neighbors, rng_state, 30, 5, 0.001, np.float32, leaf_array=leaf_array, verbose=False, ) assert len(result) == 2 # Should return (indices, distances) assert result[0].shape == (len(float_data), n_neighbors) assert result[1].shape == (len(float_data), n_neighbors) # Distances should be transformed: maximum(-log2(-distances), 0.0) assert np.all(result[1] >= 0.0) def test_nn_descent_uint8(self, uint8_data): """Test nn_descent with uint8 data.""" rng_state = np.random.randint(INT32_MIN, INT32_MAX, 3).astype(np.int64) n_neighbors = 5 with patch("evoc.uint8_nndescent.nn_descent_uint8") as mock_nn_descent: mock_indices = np.random.randint( 0, len(uint8_data), size=(len(uint8_data), n_neighbors) ) mock_distances = -np.random.exponential( 1, size=(len(uint8_data), n_neighbors) ) leaf_array = np.random.randint( 0, uint8_data.shape[0], size=(4, len(uint8_data)), dtype=np.int32 ) mock_nn_descent.return_value = (mock_indices, mock_distances) result = nn_descent( uint8_data, n_neighbors, rng_state, 30, 5, 0.001, np.uint8, leaf_array=leaf_array, verbose=True, ) assert len(result) == 2 assert result[0].shape == (len(uint8_data), n_neighbors) assert result[1].shape == (len(uint8_data), n_neighbors) # Distances should be transformed: -log2(-distances) def test_nn_descent_int8(self, int8_data): """Test nn_descent with int8 data.""" rng_state = np.random.randint(INT32_MIN, INT32_MAX, 3).astype(np.int64) n_neighbors = 5 with patch("evoc.int8_nndescent.nn_descent_int8") as mock_nn_descent: mock_indices = np.random.randint( 0, len(int8_data), size=(len(int8_data), n_neighbors) ) mock_distances = -np.random.exponential( 1, size=(len(int8_data), n_neighbors) ) mock_nn_descent.return_value = (mock_indices, mock_distances) leaf_array = np.random.randint( 0, int8_data.shape[0], size=(4, len(int8_data)), dtype=np.int32 ) result = nn_descent( int8_data, n_neighbors, rng_state, 30, 5, 0.001, np.int8, leaf_array=leaf_array, ) assert len(result) == 2 assert result[0].shape == (len(int8_data), n_neighbors) assert result[1].shape == (len(int8_data), n_neighbors) # Distances should be transformed: 1.0 / (-distances) class TestKNNGraph: """Test the main knn_graph function.""" @pytest.fixture def float_test_data(self): """Create test data for float32 testing.""" # Create blob data that will be normalized X, y = make_blobs( n_samples=200, centers=4, n_features=50, cluster_std=1.0, random_state=42 ) return X.astype(np.float64) # Start with float64 to test conversion @pytest.fixture def uint8_test_data(self): """Create uint8 test data.""" np.random.seed(42) return np.random.randint(0, 256, size=(100, 30), dtype=np.uint8) @pytest.fixture def int8_test_data(self): """Create int8 test data.""" np.random.seed(42) return np.random.randint(-128, 128, size=(100, 30), dtype=np.int8) def test_knn_graph_float_data(self, float_test_data): """Test knn_graph with float data (gets normalized).""" result = knn_graph( float_test_data, n_neighbors=10, n_trees=4, random_state=42, verbose=False ) assert len(result) == 2 # (indices, distances) indices, distances = result assert indices.shape == (len(float_test_data), 10) assert distances.shape == (len(float_test_data), 10) assert indices.dtype == np.int32 or indices.dtype == np.int64 assert distances.dtype == np.float32 or distances.dtype == np.float64 # Check that indices are valid assert np.all(indices >= 0) assert np.all(indices < len(float_test_data)) # Check that distances are non-negative (after transformation) assert np.all(distances >= 0.0) def test_knn_graph_uint8_data(self, uint8_test_data): """Test knn_graph with uint8 data.""" result = knn_graph( uint8_test_data, n_neighbors=5, n_trees=3, random_state=42, verbose=False ) assert len(result) == 2 indices, distances = result assert indices.shape == (len(uint8_test_data), 5) assert distances.shape == (len(uint8_test_data), 5) assert np.all(indices >= 0) assert np.all(indices < len(uint8_test_data)) def test_knn_graph_int8_data(self, int8_test_data): """Test knn_graph with int8 data.""" result = knn_graph(int8_test_data, n_neighbors=8, random_state=42) assert len(result) == 2 indices, distances = result assert indices.shape == (len(int8_test_data), 8) assert distances.shape == (len(int8_test_data), 8) assert np.all(indices >= 0) assert np.all(indices < len(int8_test_data)) def test_knn_graph_parameters(self, float_test_data): """Test knn_graph with various parameter combinations.""" # Test with custom parameters result = knn_graph( float_test_data, n_neighbors=15, n_trees=6, leaf_size=25, max_candidates=40, max_rptree_depth=100, n_iters=8, delta=0.01, n_jobs=1, verbose=True, random_state=123, ) indices, distances = result assert indices.shape == (len(float_test_data), 15) assert distances.shape == (len(float_test_data), 15) def test_knn_graph_default_parameters(self, float_test_data): """Test knn_graph with mostly default parameters.""" result = knn_graph(float_test_data, random_state=42) indices, distances = result # Default n_neighbors should be 30 assert indices.shape == (len(float_test_data), 30) assert distances.shape == (len(float_test_data), 30) def test_knn_graph_n_jobs_setting(self, float_test_data): """Test that n_jobs parameter affects numba threading.""" with ( patch("numba.get_num_threads") as mock_get_threads, patch("numba.set_num_threads") as mock_set_threads, ): mock_get_threads.return_value = 8 # Test with n_jobs=-1 (should not change threads) knn_graph(float_test_data, n_jobs=-1, random_state=42) mock_set_threads.assert_not_called() # Test with specific n_jobs knn_graph(float_test_data, n_jobs=4, random_state=42) # Should be called with 4 and then restored calls = mock_set_threads.call_args_list assert any(call[0][0] == 4 for call in calls) def test_knn_graph_auto_parameters(self, float_test_data): """Test automatic parameter selection.""" with patch("numba.get_num_threads", return_value=2): result = knn_graph( float_test_data, n_trees=None, # Should be auto-selected n_iters=None, # Should be auto-selected random_state=42, ) assert len(result) == 2 # Auto n_trees should be max(4, min(8, num_threads)) = 8 # Auto n_iters should be max(5, int(round(log2(n_samples)))) def test_knn_graph_warning_on_failure(self, float_test_data): """Test that warning is issued when neighbor finding fails.""" with patch("evoc.knn_graph.nn_descent") as mock_nn_descent: # Mock a result with some negative indices (indicating failure) mock_indices = np.full((len(float_test_data), 10), -1, dtype=np.int32) mock_distances = np.random.rand(len(float_test_data), 10) mock_nn_descent.return_value = (mock_indices, mock_distances) with pytest.warns( UserWarning, match="Failed to correctly find n_neighbors" ): result = knn_graph(float_test_data, n_neighbors=10, random_state=42) def test_knn_graph_data_validation(self): """Test that knn_graph properly validates input data.""" # Test with invalid data shape invalid_data = np.array([1, 2, 3]) # 1D array with pytest.raises((ValueError, TypeError)): knn_graph(invalid_data) def test_knn_graph_float_normalization(self): """Test that float data gets properly normalized to unit sphere.""" # Create data that's not normalized data = np.array([[3, 4], [1, 0], [0, 5]], dtype=np.float32) result = knn_graph(data, n_neighbors=2, random_state=42) # Should complete without error assert len(result) == 2 indices, distances = result assert indices.shape == (3, 2) assert distances.shape == (3, 2) def test_knn_graph_zero_norm_handling(self): """Test handling of zero-norm vectors in float data.""" # Include a zero vector data = np.array([[1, 1], [0, 0], [2, 2]], dtype=np.float32) result = knn_graph(data, n_neighbors=2, random_state=42) # Should complete without error (zero norms are set to 1.0) assert len(result) == 2 indices, distances = result assert indices.shape == (3, 2) assert distances.shape == (3, 2) class TestIntegration: """Integration tests for the complete knn_graph pipeline.""" def test_small_dataset_complete_pipeline(self): """Test complete pipeline on a small dataset.""" # Create a small, well-separated dataset X, y = make_blobs( n_samples=50, centers=3, n_features=10, cluster_std=0.5, random_state=42 ) X = X.astype(np.float32) result = knn_graph(X, n_neighbors=5, n_trees=2, random_state=42, verbose=True) indices, distances = result # Basic sanity checks assert indices.shape == (50, 5) assert distances.shape == (50, 5) assert np.all(indices >= 0) assert np.all(indices < 50) assert np.all(distances >= 0) # Note: Points may include themselves as neighbors, which is normal behavior def test_reproducibility(self): """Test that results are reproducible with same random state.""" data = np.random.rand(30, 8).astype(np.float32) result1 = knn_graph(data, n_neighbors=5, random_state=42) result2 = knn_graph(data, n_neighbors=5, random_state=42) np.testing.assert_array_equal(result1[0], result2[0]) # indices np.testing.assert_array_almost_equal(result1[1], result2[1]) # distances def test_different_data_types_consistency(self): """Test that different data types produce reasonable results.""" # Create base data np.random.seed(42) base_data = np.random.rand(40, 20) # Convert to different types float_data = base_data.astype(np.float32) uint8_data = (base_data * 255).astype(np.uint8) int8_data = ((base_data - 0.5) * 255).astype(np.int8) # Get results for each type float_result = knn_graph(float_data, n_neighbors=5, random_state=42) uint8_result = knn_graph(uint8_data, n_neighbors=5, random_state=42) int8_result = knn_graph(int8_data, n_neighbors=5, random_state=42) # All should have same shape for result in [float_result, uint8_result, int8_result]: assert result[0].shape == (40, 5) assert result[1].shape == (40, 5) assert np.all(result[0] >= 0) assert np.all(result[0] < 40) assert np.all(result[1] >= 0) ================================================ FILE: evoc/tests/test_knn_graph_performance.py ================================================ """ Performance benchmark tests for the knn_graph module. This module provides performance regression testing for the knn_graph functionality. The tests are designed to be robust across different hardware configurations by using relative performance metrics and adaptive thresholds. """ import numpy as np import pytest import time import platform from contextlib import contextmanager from sklearn.datasets import make_blobs from typing import Dict, Any, Tuple, List try: import psutil HAS_PSUTIL = True except ImportError: HAS_PSUTIL = False psutil = None from evoc.knn_graph import knn_graph class PerformanceMetrics: """Class to collect and analyze performance metrics.""" def __init__(self): self.metrics = {} self.hardware_info = self._get_hardware_info() def _get_hardware_info(self) -> Dict[str, Any]: """Get basic hardware information for context.""" try: if HAS_PSUTIL: return { "cpu_count": psutil.cpu_count(logical=False), "cpu_count_logical": psutil.cpu_count(logical=True), "memory_gb": round(psutil.virtual_memory().total / (1024**3), 2), "platform": platform.platform(), "python_version": platform.python_version(), } else: # Fallback without psutil import os return { "cpu_count_logical": os.cpu_count() or 1, "platform": platform.platform(), "python_version": platform.python_version(), "psutil_available": False, } except Exception: return {"error": "Could not gather hardware info"} def record_metric(self, test_name: str, metric_name: str, value: float): """Record a performance metric.""" if test_name not in self.metrics: self.metrics[test_name] = {} self.metrics[test_name][metric_name] = value def get_metric(self, test_name: str, metric_name: str) -> float: """Get a recorded metric.""" return self.metrics.get(test_name, {}).get(metric_name, 0.0) @contextmanager def time_execution(): """Context manager to time code execution.""" start_time = time.perf_counter() yield end_time = time.perf_counter() return end_time - start_time def time_function(func, *args, **kwargs) -> Tuple[Any, float]: """Time a function execution and return result and duration.""" start_time = time.perf_counter() result = func(*args, **kwargs) end_time = time.perf_counter() return result, end_time - start_time class TestKNNGraphPerformance: """Performance tests for knn_graph functionality.""" @pytest.fixture(scope="class") def perf_metrics(self): """Shared performance metrics collector.""" return PerformanceMetrics() @pytest.fixture( params=[ (1000, 128), # Small dataset, typical embedding size (5000, 384), # Medium dataset, larger embedding (10000, 512), # Large dataset, large embedding ] ) def dataset_config(self, request): """Different dataset configurations for performance testing.""" n_samples, n_features = request.param return n_samples, n_features @pytest.fixture def performance_data(self, dataset_config): """Generate performance test data.""" n_samples, n_features = dataset_config np.random.seed(42) # Consistent data for reproducible benchmarks # Create clustered data similar to real-world embeddings X, y = make_blobs( n_samples=n_samples, centers=max(4, n_samples // 2000), # Scale centers with data size n_features=n_features, cluster_std=0.5, random_state=42, ) # Normalize to unit sphere (typical for embeddings) X = X.astype(np.float32) norms = np.linalg.norm(X, axis=1, keepdims=True) X = X / norms return X, (n_samples, n_features) def test_knn_graph_scaling_performance(self, performance_data, perf_metrics): """Test knn_graph performance scaling with different data sizes.""" X, (n_samples, n_features) = performance_data test_name = f"knn_graph_scaling_{n_samples}x{n_features}" # Warm up run (not timed) to ensure compiled numba functions if n_samples <= 1000: # Only warm up on small data knn_graph(X[:100], n_neighbors=10, n_trees=2, random_state=42) # Timed run result, duration = time_function( knn_graph, X, n_neighbors=30, n_trees=4, random_state=42, verbose=False ) # Record metrics perf_metrics.record_metric(test_name, "duration_seconds", duration) perf_metrics.record_metric( test_name, "samples_per_second", n_samples / duration ) perf_metrics.record_metric(test_name, "n_samples", n_samples) perf_metrics.record_metric(test_name, "n_features", n_features) # Verify result is correct indices, distances = result assert indices.shape == (n_samples, 30) assert distances.shape == (n_samples, 30) # Performance expectations (very loose bounds that should work across hardware) # These are sanity checks rather than strict requirements expected_min_samples_per_second = { 1000: 100, # At least 100 samples/sec for small data 5000: 50, # At least 50 samples/sec for medium data 10000: 20, # At least 20 samples/sec for large data } min_expected = expected_min_samples_per_second.get(n_samples, 10) samples_per_sec = n_samples / duration # Log performance info print(f"\n{test_name}:") print(f" Duration: {duration:.3f}s") print(f" Samples/sec: {samples_per_sec:.1f}") print(f" Hardware: {perf_metrics.hardware_info}") # Very loose performance check - mainly to catch major regressions assert ( samples_per_sec > min_expected ), f"Performance too slow: {samples_per_sec:.1f} < {min_expected} samples/sec" def test_knn_graph_parameter_performance(self, perf_metrics): """Test performance with different parameter configurations.""" np.random.seed(42) n_samples, n_features = 2000, 256 X, _ = make_blobs(n_samples=n_samples, n_features=n_features, random_state=42) X = X.astype(np.float32) X = X / np.linalg.norm(X, axis=1, keepdims=True) # Test different parameter combinations param_configs = [ {"n_neighbors": 15, "n_trees": 2, "name": "fast_config"}, {"n_neighbors": 30, "n_trees": 4, "name": "default_config"}, {"n_neighbors": 50, "n_trees": 8, "name": "high_quality_config"}, ] durations = {} for config in param_configs: name = config.pop("name") test_name = f"param_performance_{name}" # Warm up knn_graph( X[:100], n_neighbors=config["n_neighbors"], n_trees=config["n_trees"], random_state=42, ) # Timed run result, duration = time_function(knn_graph, X, random_state=42, **config) durations[name] = duration perf_metrics.record_metric(test_name, "duration_seconds", duration) perf_metrics.record_metric( test_name, "samples_per_second", n_samples / duration ) print(f"\n{name}: {duration:.3f}s ({n_samples/duration:.1f} samples/sec)") # Verify relative performance expectations # The relationship between parameters and performance can be complex # So we mainly check that all configurations complete successfully for name, duration in durations.items(): assert duration < 10.0, f"{name} took too long: {duration:.3f}s" # Optionally log which configuration was fastest fastest_config = min(durations, key=durations.get) slowest_config = max(durations, key=durations.get) print(f"\nFastest: {fastest_config} ({durations[fastest_config]:.3f}s)") print(f"Slowest: {slowest_config} ({durations[slowest_config]:.3f}s)") def test_knn_graph_data_type_performance(self, perf_metrics): """Test performance differences between data types.""" np.random.seed(42) n_samples, n_features = 2000, 128 # Generate base data base_data = np.random.rand(n_samples, n_features) # Convert to different types float_data = base_data.astype(np.float32) uint8_data = (base_data * 255).astype(np.uint8) int8_data = ((base_data - 0.5) * 255).astype(np.int8) data_types = [ (float_data, "float32"), (uint8_data, "uint8"), (int8_data, "int8"), ] durations = {} for data, dtype_name in data_types: test_name = f"dtype_performance_{dtype_name}" # Warm up knn_graph(data[:100], n_neighbors=10, n_trees=2, random_state=42) # Timed run result, duration = time_function( knn_graph, data, n_neighbors=20, n_trees=4, random_state=42, verbose=False, ) durations[dtype_name] = duration perf_metrics.record_metric(test_name, "duration_seconds", duration) perf_metrics.record_metric( test_name, "samples_per_second", n_samples / duration ) print( f"\n{dtype_name}: {duration:.3f}s ({n_samples/duration:.1f} samples/sec)" ) # All should complete in reasonable time for dtype_name, duration in durations.items(): assert duration < 30.0, f"{dtype_name} took too long: {duration:.3f}s" def test_knn_graph_threading_performance(self, perf_metrics): """Test performance scaling with different thread counts.""" np.random.seed(42) n_samples, n_features = 3000, 256 X, _ = make_blobs(n_samples=n_samples, n_features=n_features, random_state=42) X = X.astype(np.float32) X = X / np.linalg.norm(X, axis=1, keepdims=True) # Test different thread counts if HAS_PSUTIL: max_threads = min( 8, psutil.cpu_count(logical=True) ) # Don't exceed available cores else: import os max_threads = min(8, os.cpu_count() or 1) thread_counts = [1, max(2, max_threads // 2), max_threads] durations = {} for n_jobs in thread_counts: test_name = f"threading_performance_{n_jobs}_threads" # Warm up knn_graph( X[:100], n_neighbors=10, n_trees=2, n_jobs=n_jobs, random_state=42 ) # Timed run result, duration = time_function( knn_graph, X, n_neighbors=20, n_trees=4, n_jobs=n_jobs, random_state=42, verbose=False, ) durations[n_jobs] = duration perf_metrics.record_metric(test_name, "duration_seconds", duration) perf_metrics.record_metric( test_name, "samples_per_second", n_samples / duration ) print( f"\n{n_jobs} threads: {duration:.3f}s ({n_samples/duration:.1f} samples/sec)" ) # More threads should generally be faster (within reason) if len(durations) >= 2 and max_threads > 1: single_thread_time = durations[1] multi_thread_time = durations[max_threads] # Allow for some overhead but expect some speedup speedup_ratio = single_thread_time / multi_thread_time expected_min_speedup = 1.2 # At least 20% speedup with more threads print(f"\nSpeedup ratio: {speedup_ratio:.2f}x") # Only assert if we have multiple cores available if max_threads > 2: assert ( speedup_ratio > expected_min_speedup ), f"Multi-threading should provide speedup: {speedup_ratio:.2f}x < {expected_min_speedup}x" def test_memory_usage_scaling(self, perf_metrics): """Test memory usage scaling (basic check).""" if not HAS_PSUTIL: pytest.skip("psutil not available for memory testing") import gc # Get baseline memory gc.collect() process = psutil.Process() baseline_memory = process.memory_info().rss / 1024 / 1024 # MB test_sizes = [(1000, 64), (2000, 64), (4000, 64)] memory_usages = [] for n_samples, n_features in test_sizes: gc.collect() # Generate data np.random.seed(42) X, _ = make_blobs( n_samples=n_samples, n_features=n_features, random_state=42 ) X = X.astype(np.float32) X = X / np.linalg.norm(X, axis=1, keepdims=True) # Run knn_graph before_memory = process.memory_info().rss / 1024 / 1024 result = knn_graph( X, n_neighbors=20, n_trees=4, random_state=42, verbose=False ) after_memory = process.memory_info().rss / 1024 / 1024 memory_increase = after_memory - baseline_memory memory_usages.append((n_samples, memory_increase)) test_name = f"memory_usage_{n_samples}_samples" perf_metrics.record_metric(test_name, "memory_mb", memory_increase) print(f"\n{n_samples} samples: {memory_increase:.1f} MB") # Clean up del X, result gc.collect() # Memory usage should scale reasonably (not exponentially) if len(memory_usages) >= 2: small_n, small_mem = memory_usages[0] large_n, large_mem = memory_usages[-1] sample_ratio = large_n / small_n memory_ratio = large_mem / max(small_mem, 1.0) # Avoid division by zero # Memory should not grow faster than O(n^2) assert ( memory_ratio < sample_ratio**1.5 ), f"Memory usage growing too fast: {memory_ratio:.2f}x for {sample_ratio:.2f}x samples" def test_reproducibility_performance(self, perf_metrics): """Test that performance is consistent across runs.""" np.random.seed(42) n_samples, n_features = 1500, 128 X, _ = make_blobs(n_samples=n_samples, n_features=n_features, random_state=42) X = X.astype(np.float32) X = X / np.linalg.norm(X, axis=1, keepdims=True) # Warm up knn_graph(X[:100], n_neighbors=10, n_trees=2, random_state=42) # Run multiple times n_runs = 3 durations = [] for i in range(n_runs): result, duration = time_function( knn_graph, X, n_neighbors=20, n_trees=4, random_state=42, # Same random state for consistency verbose=False, ) durations.append(duration) # Calculate statistics mean_duration = np.mean(durations) std_duration = np.std(durations) cv = std_duration / mean_duration # Coefficient of variation perf_metrics.record_metric("reproducibility", "mean_duration", mean_duration) perf_metrics.record_metric("reproducibility", "std_duration", std_duration) perf_metrics.record_metric("reproducibility", "coefficient_of_variation", cv) print(f"\nReproducibility test:") print(f" Mean duration: {mean_duration:.3f}s") print(f" Std deviation: {std_duration:.3f}s") print(f" Coefficient of variation: {cv:.3f}") # Performance should be reasonably consistent # Allow for up to 20% variation between runs assert cv < 0.4, f"Performance too variable: CV = {cv:.3f}" # Verify results are identical result1, _ = time_function(knn_graph, X, n_neighbors=10, random_state=42) result2, _ = time_function(knn_graph, X, n_neighbors=10, random_state=42) np.testing.assert_array_equal(result1[0], result2[0]) np.testing.assert_array_almost_equal(result1[1], result2[1]) @pytest.mark.performance class TestPerformanceRegression: """Performance regression tests with historical baselines.""" def test_baseline_performance_check(self): """ Baseline performance test that can be used to establish performance standards. This test should be run on a reference machine to establish baseline timings, and then used in CI to detect significant regressions. """ np.random.seed(42) # Standard test case n_samples, n_features = 5000, 256 X, _ = make_blobs(n_samples=n_samples, n_features=n_features, random_state=42) X = X.astype(np.float32) X = X / np.linalg.norm(X, axis=1, keepdims=True) # Warm up knn_graph(X[:100], n_neighbors=10, n_trees=2, random_state=42) # Benchmark run start_time = time.perf_counter() result = knn_graph(X, n_neighbors=30, n_trees=4, random_state=42, verbose=False) duration = time.perf_counter() - start_time indices, distances = result samples_per_second = n_samples / duration print(f"\nBaseline Performance Report:") print(f" Dataset: {n_samples} samples x {n_features} features") print(f" Duration: {duration:.3f} seconds") print(f" Throughput: {samples_per_second:.1f} samples/second") print(f" Hardware: {platform.platform()}") if HAS_PSUTIL: print(f" CPU cores: {psutil.cpu_count(logical=True)}") print(f" Memory: {psutil.virtual_memory().total / (1024**3):.1f} GB") else: import os print(f" CPU cores: {os.cpu_count() or 'unknown'}") print(f" Memory: unknown (psutil not available)") # Basic sanity checks assert indices.shape == (n_samples, 30) assert distances.shape == (n_samples, 30) assert np.all(indices >= 0) assert np.all(distances >= 0) # Very basic performance floor (should work on any reasonable hardware) min_samples_per_second = 10 # Very conservative assert ( samples_per_second > min_samples_per_second ), f"Performance below minimum threshold: {samples_per_second:.1f} < {min_samples_per_second}" # Store baseline for potential future comparison # In a real CI system, you might save this to a file or database baseline_info = { "duration": duration, "samples_per_second": samples_per_second, "hardware_hash": hash(platform.platform()), "timestamp": time.time(), } # Note: baseline_info could be used for comparison in CI systems # but we don't return it to avoid pytest warnings ================================================ FILE: evoc/tests/test_numba_kdtree.py ================================================ """ Test suite for NumbaKDTree compatibility with sklearn KDTree. This module tests that our NumbaKDTree implementation produces equivalent partitioning and query results compared to sklearn's KDTree implementation. """ import numpy as np import pytest import numba from sklearn.neighbors import KDTree as SklearnKDTree from evoc.numba_kdtree import build_kdtree class TestKDTreeCompatibility: """Test compatibility between NumbaKDTree and sklearn KDTree implementations.""" @pytest.fixture( params=[ (50, 2), # Small 2D (100, 3), # Medium 3D (200, 5), # Large 5D (500, 8), # Large 8D ] ) def test_data(self, request): """Generate test data for various configurations.""" n_samples, n_features = request.param np.random.seed(42) # Fixed seed for reproducible tests return np.random.rand(n_samples, n_features).astype(np.float32) @pytest.fixture(params=[10, 20, 40]) def leaf_size(self, request): """Test different leaf sizes.""" return request.param def test_tree_structure_compatibility(self, test_data, leaf_size): """Test that tree structures have compatible shapes and properties.""" # Build trees sklearn_tree = SklearnKDTree(test_data, leaf_size=leaf_size) numba_tree = build_kdtree(test_data, leaf_size=leaf_size) # Get sklearn internal arrays sk_data, sk_idx_array, sk_node_data, sk_node_bounds = sklearn_tree.get_arrays() # Test data compatibility assert np.array_equal(sk_data, numba_tree.data), "Data arrays should match" assert ( sk_idx_array.shape == numba_tree.idx_array.shape ), "Index array shapes should match" # Test node data shapes assert ( sk_node_data["idx_start"].shape == numba_tree.idx_start.shape ), "idx_start shapes should match" assert ( sk_node_data["idx_end"].shape == numba_tree.idx_end.shape ), "idx_end shapes should match" assert ( sk_node_data["radius"].shape == numba_tree.radius.shape ), "radius shapes should match" assert ( sk_node_data["is_leaf"].shape == numba_tree.is_leaf.shape ), "is_leaf shapes should match" # Test node bounds shape assert ( sk_node_bounds.shape == numba_tree.node_bounds.shape ), "Node bounds shapes should match" def test_node_partitioning_equivalence(self, test_data, leaf_size): """ Test that both implementations partition data into equivalent node sets. This verifies that each node contains the same set of data points, regardless of internal ordering differences. """ # Build trees sklearn_tree = SklearnKDTree(test_data, leaf_size=leaf_size) numba_tree = build_kdtree(test_data, leaf_size=leaf_size) # Get sklearn internal arrays sk_data, sk_idx_array, sk_node_data, sk_node_bounds = sklearn_tree.get_arrays() n_nodes = sk_node_data.shape[0] matches = 0 total_comparisons = 0 for node in range(n_nodes): # Get node boundaries sk_start = sk_node_data[node]["idx_start"] sk_end = sk_node_data[node]["idx_end"] sk_is_leaf = sk_node_data[node]["is_leaf"] nb_start = numba_tree.idx_start[node] nb_end = numba_tree.idx_end[node] nb_is_leaf = numba_tree.is_leaf[node] # Node properties should match exactly assert sk_start == nb_start, f"Node {node}: idx_start mismatch" assert sk_end == nb_end, f"Node {node}: idx_end mismatch" assert sk_is_leaf == nb_is_leaf, f"Node {node}: is_leaf mismatch" # Skip empty nodes if sk_start >= sk_end: continue total_comparisons += 1 # Get indices for this node and sort them (to ignore ordering differences) sk_indices = np.sort(sk_idx_array[sk_start:sk_end]) nb_indices = np.sort(numba_tree.idx_array[nb_start:nb_end]) # The sorted indices should be identical if np.array_equal(sk_indices, nb_indices): matches += 1 # Require high compatibility (allowing for minor algorithmic differences) match_rate = matches / total_comparisons if total_comparisons > 0 else 1.0 assert ( match_rate >= 0.95 ), f"Node partitioning match rate {match_rate:.1%} is below 95% threshold" def test_data_ordering_equivalence(self, test_data, leaf_size): """ Test that data ordering along split axes is equivalent. This is a more fundamental test of whether the partitioning logic is working similarly between implementations. """ # Build trees sklearn_tree = SklearnKDTree(test_data, leaf_size=leaf_size) numba_tree = build_kdtree(test_data, leaf_size=leaf_size) # Get sklearn internal arrays sk_data, sk_idx_array, sk_node_data, sk_node_bounds = sklearn_tree.get_arrays() n_nodes = sk_node_data.shape[0] axis_ordering_matches = 0 total_internal_nodes = 0 for node in range(n_nodes): # Only check internal nodes (non-leaf nodes) if sk_node_data[node]["is_leaf"]: continue total_internal_nodes += 1 # Get node boundaries sk_start = sk_node_data[node]["idx_start"] sk_end = sk_node_data[node]["idx_end"] # Skip if insufficient points if sk_end - sk_start < 2: continue # Get indices for both implementations sk_indices = sk_idx_array[sk_start:sk_end] nb_indices = numba_tree.idx_array[sk_start:sk_end] # Find split axis (dimension with maximum spread) spreads = [] for axis in range(test_data.shape[1]): sk_values = test_data[sk_indices, axis] min_val, max_val = np.min(sk_values), np.max(sk_values) spreads.append(max_val - min_val) split_axis = np.argmax(spreads) # Get data values along split axis sk_axis_values = test_data[sk_indices, split_axis] nb_axis_values = test_data[nb_indices, split_axis] # Check if the median/partition point is similar sk_median = np.median(sk_axis_values) nb_median = np.median(nb_axis_values) # Count points on each side of median sk_left_count = np.sum(sk_axis_values <= sk_median) sk_right_count = np.sum(sk_axis_values > sk_median) nb_left_count = np.sum(nb_axis_values <= nb_median) nb_right_count = np.sum(nb_axis_values > nb_median) # Check if partitioning is roughly equivalent # (allowing for different tie-breaking in median calculation) partitioning_similar = ( abs(sk_left_count - nb_left_count) <= 2 and abs(sk_right_count - nb_right_count) <= 2 ) if partitioning_similar: axis_ordering_matches += 1 # Require high compatibility for data ordering ordering_match_rate = ( axis_ordering_matches / total_internal_nodes if total_internal_nodes > 0 else 1.0 ) assert ( ordering_match_rate >= 0.80 ), f"Data ordering match rate {ordering_match_rate:.1%} is below 80% threshold" def test_query_results_compatibility(self, test_data, leaf_size): """Test that query results are equivalent between implementations.""" # Build trees sklearn_tree = SklearnKDTree(test_data, leaf_size=leaf_size) numba_tree = build_kdtree(test_data, leaf_size=leaf_size) # Create query points (subset of original data for deterministic results) np.random.seed(123) query_indices = np.random.choice( len(test_data), size=min(10, len(test_data)), replace=False ) query_data = test_data[query_indices] k = min(5, len(test_data)) # Number of neighbors # Query sklearn tree sk_distances, sk_indices = sklearn_tree.query( query_data, k=k, return_distance=True ) # Query numba tree using the parallel implementation from evoc.numba_kdtree import parallel_tree_query nb_distances, nb_indices = parallel_tree_query( numba_tree, query_data, k=numba.int64(k), output_rdist=numba.types.boolean(False), ) # Results should be very similar (allowing for minor floating point differences) # Sort both results by indices to handle any ordering differences for i in range(len(query_data)): # Sort by indices to compare equivalent sets sk_sorted_idx = np.argsort(sk_indices[i]) nb_sorted_idx = np.argsort(nb_indices[i]) sk_sorted_indices = sk_indices[i][sk_sorted_idx] nb_sorted_indices = nb_indices[i][nb_sorted_idx] sk_sorted_distances = sk_distances[i][sk_sorted_idx] nb_sorted_distances = nb_distances[i][nb_sorted_idx] # Check that we get the same nearest neighbors np.testing.assert_array_equal( sk_sorted_indices, nb_sorted_indices, err_msg=f"Query {i}: Nearest neighbor indices don't match", ) # Check that distances are very close np.testing.assert_allclose( sk_sorted_distances, nb_sorted_distances, rtol=1e-5, atol=1e-6, err_msg=f"Query {i}: Distances don't match within tolerance", ) def test_tree_bounds_compatibility(self, test_data, leaf_size): """Test that node bounds are calculated consistently.""" # Build trees sklearn_tree = SklearnKDTree(test_data, leaf_size=leaf_size) numba_tree = build_kdtree(test_data, leaf_size=leaf_size) # Get sklearn bounds sk_data, sk_idx_array, sk_node_data, sk_node_bounds = sklearn_tree.get_arrays() # Node bounds should match closely np.testing.assert_allclose( sk_node_bounds, numba_tree.node_bounds, rtol=1e-5, atol=1e-6, err_msg="Node bounds don't match between implementations", ) class TestKDTreeEdgeCases: """Test edge cases and special conditions.""" def test_single_point(self): """Test with a single data point.""" data = np.array([[1.0, 2.0, 3.0]], dtype=np.float32) sklearn_tree = SklearnKDTree(data, leaf_size=1) numba_tree = build_kdtree(data, leaf_size=1) # Should handle single point gracefully assert numba_tree.data.shape == (1, 3) assert numba_tree.idx_array.shape == (1,) def test_duplicate_points(self): """Test with duplicate data points.""" data = np.array( [ [1.0, 2.0], [1.0, 2.0], # Duplicate [3.0, 4.0], [1.0, 2.0], # Another duplicate ], dtype=np.float32, ) sklearn_tree = SklearnKDTree(data, leaf_size=2) numba_tree = build_kdtree(data, leaf_size=2) # Should handle duplicates without error assert numba_tree.data.shape == data.shape # Query should work with duplicates from evoc.numba_kdtree import parallel_tree_query distances, indices = parallel_tree_query( numba_tree, data[:1], k=2, output_rdist=False ) assert distances.shape == (1, 2) assert indices.shape == (1, 2) def test_high_dimensional_data(self): """Test with high-dimensional data.""" np.random.seed(42) data = np.random.rand(100, 50).astype(np.float32) # 50D data sklearn_tree = SklearnKDTree(data, leaf_size=10) numba_tree = build_kdtree(data, leaf_size=10) # Should handle high dimensions assert numba_tree.data.shape == (100, 50) # Quick query test from evoc.numba_kdtree import parallel_tree_query distances, indices = parallel_tree_query( numba_tree, data[:5], k=3, output_rdist=False ) assert distances.shape == (5, 3) assert indices.shape == (5, 3) # Integration test that can be run standalone def test_full_pipeline_compatibility(): """Integration test ensuring the full pipeline works with both tree types.""" np.random.seed(42) data = np.random.rand(200, 5).astype(np.float32) # Build numba tree and run boruvka (this was the original failing case) from evoc.numba_kdtree import build_kdtree from evoc.boruvka import parallel_boruvka tree = build_kdtree(data, leaf_size=20) num_threads = numba.get_num_threads() # This should not raise any numba errors edges = parallel_boruvka( tree, n_threads=num_threads, min_samples=5, reproducible=True ) # Should produce reasonable results assert len(edges) > 0, "Boruvka should produce some edges" assert edges.shape[1] == 3, "Edges should have 3 columns (from, to, weight)" assert np.all(edges[:, 2] >= 0), "Edge weights should be non-negative" if __name__ == "__main__": # Allow running as a script for quick testing pytest.main([__file__, "-v"]) ================================================ FILE: evoc/tests/test_numba_kdtree_performance.py ================================================ """ Performance benchmark tests for the numba_kdtree module. This module provides performance regression testing and comparison benchmarks against sklearn's KDTree implementation. The numba implementation is optimized for large query batches where parallelization benefits outweigh overhead. Key performance characteristics: - Small batches (<1000 queries): May be slower due to parallelization overhead - Medium batches (1000-3000 queries): Competitive to slightly faster - Large batches (3000+ queries): Significant speedup (3-20x) due to parallelization - Ultra-large batches (10k+ queries): Maximum speedup, ideal use case The tests focus on large query batch scenarios since that is the primary optimization target for the numba implementation. """ import numpy as np import pytest import time import platform from contextlib import contextmanager from sklearn.datasets import make_blobs from sklearn.neighbors import KDTree as SklearnKDTree from typing import Dict, Any, Tuple, List try: import psutil HAS_PSUTIL = True except ImportError: HAS_PSUTIL = False psutil = None from evoc.numba_kdtree import build_kdtree, parallel_tree_query, kdtree_to_numba def time_function(func, *args, **kwargs) -> Tuple[Any, float]: """Time a function execution and return result and duration.""" start_time = time.perf_counter() result = func(*args, **kwargs) end_time = time.perf_counter() return result, end_time - start_time class KDTreePerformanceMetrics: """Class to collect and analyze KDTree performance metrics.""" def __init__(self): self.metrics = {} self.hardware_info = self._get_hardware_info() def _get_hardware_info(self) -> Dict[str, Any]: """Get basic hardware information for context.""" try: if HAS_PSUTIL: return { "cpu_count": psutil.cpu_count(logical=False), "cpu_count_logical": psutil.cpu_count(logical=True), "memory_gb": round(psutil.virtual_memory().total / (1024**3), 2), "platform": platform.platform(), "python_version": platform.python_version(), } else: import os return { "cpu_count_logical": os.cpu_count() or 1, "platform": platform.platform(), "python_version": platform.python_version(), "psutil_available": False, } except Exception: return {"error": "Could not gather hardware info"} def record_metric(self, test_name: str, metric_name: str, value: float): """Record a performance metric.""" if test_name not in self.metrics: self.metrics[test_name] = {} self.metrics[test_name][metric_name] = value def get_metric(self, test_name: str, metric_name: str) -> float: """Get a recorded metric.""" return self.metrics.get(test_name, {}).get(metric_name, 0.0) @pytest.mark.performance class TestKDTreePerformance: """Performance tests for numba KDTree implementation.""" @pytest.fixture(scope="class") def perf_metrics(self): """Shared performance metrics collector.""" return KDTreePerformanceMetrics() @pytest.fixture( params=[ (1000, 2), # Small 2D dataset (5000, 3), # Medium 3D dataset (10000, 5), # Large 5D dataset (20000, 8), # Very large 8D dataset ] ) def dataset_config(self, request): """Different dataset configurations for performance testing.""" n_samples, n_features = request.param return n_samples, n_features @pytest.fixture def performance_data(self, dataset_config): """Generate performance test data.""" n_samples, n_features = dataset_config np.random.seed(42) # Consistent data for reproducible benchmarks # Create diverse data that exercises different tree structures if n_features <= 3: # Use blobs for low-dimensional data X, y = make_blobs( n_samples=n_samples, centers=max(4, n_samples // 1000), n_features=n_features, cluster_std=1.0, random_state=42, ) else: # Use uniform random for higher dimensions X = np.random.rand(n_samples, n_features) * 10.0 X = X.astype(np.float32) return X, (n_samples, n_features) def test_kdtree_construction_performance(self, performance_data, perf_metrics): """Compare KDTree construction performance: Numba vs Sklearn.""" X, (n_samples, n_features) = performance_data test_name = f"construction_{n_samples}x{n_features}" # Warm up numba compilation (not timed) if n_samples >= 1000: warmup_data = X[:100].copy() build_kdtree(warmup_data, leaf_size=10) # Test sklearn construction sklearn_tree, sklearn_time = time_function(SklearnKDTree, X, leaf_size=40) # Test numba construction numba_tree, numba_time = time_function(build_kdtree, X, leaf_size=40) # Record metrics perf_metrics.record_metric(test_name, "sklearn_construction_time", sklearn_time) perf_metrics.record_metric(test_name, "numba_construction_time", numba_time) perf_metrics.record_metric( test_name, "construction_speedup", sklearn_time / numba_time ) perf_metrics.record_metric(test_name, "n_samples", n_samples) perf_metrics.record_metric(test_name, "n_features", n_features) # Calculate throughput sklearn_throughput = n_samples / sklearn_time numba_throughput = n_samples / numba_time print(f"\n{test_name} Construction Performance:") print(f" Sklearn: {sklearn_time:.4f}s ({sklearn_throughput:.0f} samples/sec)") print(f" Numba: {numba_time:.4f}s ({numba_throughput:.0f} samples/sec)") print(f" Speedup: {sklearn_time/numba_time:.2f}x") # Verify both trees work correctly query_point = X[0:1] sklearn_dists, sklearn_inds = sklearn_tree.query(query_point, k=5) numba_dists, numba_inds = parallel_tree_query( numba_tree, query_point, k=5, output_rdist=False ) assert sklearn_dists.shape == (1, 5) assert numba_dists.shape == (1, 5) assert sklearn_inds.shape == (1, 5) assert numba_inds.shape == (1, 5) # Performance expectations # After warmup, numba should be competitive or better if ( n_samples >= 1000 ): # Only assert on larger datasets where speedup is more likely assert ( numba_time < sklearn_time * 2.0 ), f"Numba construction too slow: {numba_time:.4f}s vs sklearn {sklearn_time:.4f}s" def test_kdtree_query_performance_large_batch(self, performance_data, perf_metrics): """Compare large batch query performance: Numba vs Sklearn (optimized use case).""" X, (n_samples, n_features) = performance_data test_name = f"query_large_batch_{n_samples}x{n_features}" # Build trees sklearn_tree = SklearnKDTree(X, leaf_size=40) numba_tree = build_kdtree(X, leaf_size=40) # Prepare large query batch - this is where numba should excel np.random.seed(123) # Use large query sets that benefit from parallelization n_queries = max(1000, n_samples // 2) # Large query batches query_data = np.random.rand(n_queries, n_features).astype(np.float32) * 10.0 k = min(30, n_samples // 20) # Reasonable k value # Warm up numba (not timed) _ = parallel_tree_query(numba_tree, query_data[:5], k=k, output_rdist=False) # Time sklearn queries sklearn_result, sklearn_time = time_function( sklearn_tree.query, query_data, k=k ) # Time numba queries numba_result, numba_time = time_function( parallel_tree_query, numba_tree, query_data, k=k, output_rdist=False ) # Record metrics perf_metrics.record_metric(test_name, "sklearn_query_time", sklearn_time) perf_metrics.record_metric(test_name, "numba_query_time", numba_time) perf_metrics.record_metric( test_name, "query_speedup", sklearn_time / numba_time ) perf_metrics.record_metric( test_name, "queries_per_second_sklearn", n_queries / sklearn_time ) perf_metrics.record_metric( test_name, "queries_per_second_numba", n_queries / numba_time ) sklearn_qps = n_queries / sklearn_time numba_qps = n_queries / numba_time print( f"\n{test_name} Large Batch Query Performance ({n_queries} queries, k={k}):" ) print(f" Sklearn: {sklearn_time:.4f}s ({sklearn_qps:.0f} queries/sec)") print(f" Numba: {numba_time:.4f}s ({numba_qps:.0f} queries/sec)") print(f" Speedup: {sklearn_time/numba_time:.2f}x") # Verify results have correct shape sklearn_dists, sklearn_inds = sklearn_result numba_dists, numba_inds = numba_result assert sklearn_dists.shape == (n_queries, k) assert numba_dists.shape == (n_queries, k) assert sklearn_inds.shape == (n_queries, k) assert numba_inds.shape == (n_queries, k) # Performance expectations for large batches # Numba should excel with large query sets due to parallelization # But only assert performance for sufficiently large batches where parallelization benefit outweighs overhead if ( n_queries >= 3000 ): # Only assert performance for large enough batches where advantage is consistent assert ( numba_time < sklearn_time * 1.0 ), f"Numba queries too slow for large batch ({n_queries} queries): {numba_time:.4f}s vs sklearn {sklearn_time:.4f}s" # For large query batches, expect significant speedup assert ( sklearn_time / numba_time > 1.0 ), f"Expected numba advantage for large batches ({n_queries} queries): {sklearn_time/numba_time:.2f}x speedup" elif n_queries >= 2000: # Medium-large batches should show some advantage assert (numba_time < sklearn_time * 1.0) or ( numba_time < 0.05 ), f"Numba queries too slow for medium-large batch ({n_queries} queries): {numba_time:.4f}s vs sklearn {sklearn_time:.4f}s" # Some speedup expected but can be variable assert (sklearn_time / numba_time > 1.0) or ( numba_time < 0.05 ), f"Expected at least equal performance for medium-large batches ({n_queries} queries): {sklearn_time/numba_time:.2f}x speedup" else: # For smaller batches, just ensure numba is not excessively slow (parallelization overhead is acceptable) # More lenient threshold to handle hardware variability in CI environments assert ( numba_time < sklearn_time * 4.0 ), f"Numba queries excessively slow for batch ({n_queries} queries): {numba_time:.4f}s vs sklearn {sklearn_time:.4f}s" def test_kdtree_query_performance_massive_batch( self, performance_data, perf_metrics ): """Compare massive batch query performance to test maximum parallelization benefits.""" X, (n_samples, n_features) = performance_data test_name = f"query_massive_batch_{n_samples}x{n_features}" # Skip small datasets for massive batch testing if n_samples < 5000: pytest.skip("Massive batch testing not meaningful for small datasets") # Build trees sklearn_tree = SklearnKDTree(X, leaf_size=40) numba_tree = build_kdtree(X, leaf_size=40) # Prepare very large batch of queries - this should show maximum numba advantage np.random.seed(124) n_queries = max( 5000, n_samples ) # Very large batch - equal or larger than training set query_data = np.random.rand(n_queries, n_features).astype(np.float32) * 10.0 k = min(50, n_samples // 20) # Larger k value # Warm up numba _ = parallel_tree_query(numba_tree, query_data[:10], k=k, output_rdist=False) # Time sklearn batch queries sklearn_result, sklearn_time = time_function( sklearn_tree.query, query_data, k=k ) # Time numba batch queries (should benefit from parallelization) numba_result, numba_time = time_function( parallel_tree_query, numba_tree, query_data, k=k, output_rdist=False ) # Record metrics perf_metrics.record_metric(test_name, "sklearn_batch_time", sklearn_time) perf_metrics.record_metric(test_name, "numba_batch_time", numba_time) perf_metrics.record_metric( test_name, "batch_speedup", sklearn_time / numba_time ) perf_metrics.record_metric( test_name, "batch_queries_per_second_sklearn", n_queries / sklearn_time ) perf_metrics.record_metric( test_name, "batch_queries_per_second_numba", n_queries / numba_time ) sklearn_qps = n_queries / sklearn_time numba_qps = n_queries / numba_time print( f"\n{test_name} Massive Batch Query Performance ({n_queries} queries, k={k}):" ) print(f" Sklearn: {sklearn_time:.4f}s ({sklearn_qps:.0f} queries/sec)") print(f" Numba: {numba_time:.4f}s ({numba_qps:.0f} queries/sec)") print(f" Speedup: {sklearn_time/numba_time:.2f}x") # Verify results sklearn_dists, sklearn_inds = sklearn_result numba_dists, numba_inds = numba_result assert sklearn_dists.shape == (n_queries, k) assert numba_dists.shape == (n_queries, k) # For massive batch queries, numba should show significant advantage assert ( numba_time < sklearn_time * 1.2 ), f"Numba massive batch queries should be faster: {numba_time:.4f}s vs sklearn {sklearn_time:.4f}s" # Expect substantial speedup on massive batches (this is the target use case) # More conservative threshold to handle hardware variability assert ( sklearn_time / numba_time > 0.85 ), f"Expected significant numba advantage for massive batches ({n_queries} queries): {sklearn_time/numba_time:.2f}x" def test_kdtree_accuracy_comparison(self, performance_data, perf_metrics): """Verify that numba KDTree results match sklearn results.""" X, (n_samples, n_features) = performance_data test_name = f"accuracy_{n_samples}x{n_features}" # Build trees sklearn_tree = SklearnKDTree(X, leaf_size=40) numba_tree = build_kdtree(X, leaf_size=40) # Test on a subset of data points as queries np.random.seed(125) query_indices = np.random.choice( n_samples, size=min(50, n_samples), replace=False ) query_data = X[query_indices] k = min(5, n_samples // 10) # Get results from both implementations sklearn_dists, sklearn_inds = sklearn_tree.query(query_data, k=k) numba_dists, numba_inds = parallel_tree_query( numba_tree, query_data, k=k, output_rdist=False ) # Check shapes match assert sklearn_dists.shape == numba_dists.shape assert sklearn_inds.shape == numba_inds.shape # Check that distances are reasonable (all finite, non-negative) assert np.all(np.isfinite(sklearn_dists)) assert np.all(np.isfinite(numba_dists)) assert np.all(sklearn_dists >= 0) assert np.all(numba_dists >= 0) # Check that indices are valid assert np.all(sklearn_inds >= 0) assert np.all(sklearn_inds < n_samples) assert np.all(numba_inds >= 0) assert np.all(numba_inds < n_samples) # For the first neighbor (should be identical for deterministic data) # Allow some tolerance due to potential floating point differences first_neighbor_distance_diff = np.abs(sklearn_dists[:, 0] - numba_dists[:, 0]) max_distance_diff = np.max(first_neighbor_distance_diff) print(f"\n{test_name} Accuracy Check:") print(f" Max first neighbor distance difference: {max_distance_diff:.6f}") print( f" Mean distance difference: {np.mean(first_neighbor_distance_diff):.6f}" ) # Allow small numerical differences assert ( max_distance_diff < 1e-5 ), f"Distance differences too large: {max_distance_diff:.6f}" # Check that most nearest neighbors are the same first_neighbor_matches = np.sum(sklearn_inds[:, 0] == numba_inds[:, 0]) match_rate = first_neighbor_matches / len(query_data) print(f" First neighbor match rate: {match_rate:.2%}") # Should have high agreement on nearest neighbors assert ( match_rate > 0.95 ), f"Nearest neighbor agreement too low: {match_rate:.2%}" def test_kdtree_scaling_performance(self, perf_metrics): """Test how performance scales with dataset size.""" np.random.seed(42) sizes = [1000, 2000, 5000, 10000] n_features = 5 sklearn_times = [] numba_times = [] for n_samples in sizes: # Generate test data X = np.random.rand(n_samples, n_features).astype(np.float32) * 10.0 # Warm up numba if n_samples >= 1000: warmup_tree = build_kdtree(X[:100], leaf_size=40) _ = parallel_tree_query(warmup_tree, X[:10], k=5, output_rdist=False) # Time construction sklearn_tree, sklearn_time = time_function(SklearnKDTree, X, leaf_size=40) numba_tree, numba_time = time_function(build_kdtree, X, leaf_size=40) sklearn_times.append(sklearn_time) numba_times.append(numba_time) # Record metrics test_name = f"scaling_{n_samples}" perf_metrics.record_metric(test_name, "sklearn_time", sklearn_time) perf_metrics.record_metric(test_name, "numba_time", numba_time) perf_metrics.record_metric(test_name, "speedup", sklearn_time / numba_time) print(f"\nScaling test {n_samples} samples:") print(f" Sklearn: {sklearn_time:.4f}s") print(f" Numba: {numba_time:.4f}s") print(f" Speedup: {sklearn_time/numba_time:.2f}x") # Check scaling behavior # Construction time should scale sub-quadratically for i in range(1, len(sizes)): size_ratio = sizes[i] / sizes[i - 1] sklearn_time_ratio = sklearn_times[i] / sklearn_times[i - 1] numba_time_ratio = numba_times[i] / numba_times[i - 1] # Time should not scale worse than O(n^1.5) max_expected_ratio = size_ratio**1.5 assert ( sklearn_time_ratio < max_expected_ratio * 2 ), f"Sklearn scaling too poor: {sklearn_time_ratio:.2f}x for {size_ratio:.2f}x data" assert ( numba_time_ratio < max_expected_ratio * 2 ), f"Numba scaling too poor: {numba_time_ratio:.2f}x for {size_ratio:.2f}x data" def test_kdtree_different_k_values(self, perf_metrics): """Test performance with different k values.""" np.random.seed(42) n_samples, n_features = 5000, 4 X = np.random.rand(n_samples, n_features).astype(np.float32) * 10.0 # Build trees sklearn_tree = SklearnKDTree(X, leaf_size=40) numba_tree = build_kdtree(X, leaf_size=40) # Test queries with large batch n_queries = 2000 # Large batch to benefit from parallelization query_data = np.random.rand(n_queries, n_features).astype(np.float32) * 10.0 # Warm up numba _ = parallel_tree_query(numba_tree, query_data[:5], k=5, output_rdist=False) k_values = [1, 5, 10, 20, 50] for k in k_values: if k >= n_samples: continue # Time both implementations sklearn_result, sklearn_time = time_function( sklearn_tree.query, query_data, k=k ) numba_result, numba_time = time_function( parallel_tree_query, numba_tree, query_data, k=k, output_rdist=False ) test_name = f"k_value_{k}" perf_metrics.record_metric(test_name, "sklearn_time", sklearn_time) perf_metrics.record_metric(test_name, "numba_time", numba_time) perf_metrics.record_metric(test_name, "speedup", sklearn_time / numba_time) print(f"\nk={k} performance:") print(f" Sklearn: {sklearn_time:.4f}s") print(f" Numba: {numba_time:.4f}s") print(f" Speedup: {sklearn_time/numba_time:.2f}x") # Verify correctness sklearn_dists, sklearn_inds = sklearn_result numba_dists, numba_inds = numba_result assert sklearn_dists.shape == (n_queries, k) assert numba_dists.shape == (n_queries, k) # Performance should be reasonable for all k values assert ( numba_time < sklearn_time * 3.0 ), f"Numba too slow for k={k}: {sklearn_time/numba_time:.2f}x" def test_kdtree_query_batch_scaling(self, perf_metrics): """Test how query performance scales with batch size (numba's sweet spot).""" np.random.seed(42) n_samples, n_features = 10000, 5 X = np.random.rand(n_samples, n_features).astype(np.float32) * 10.0 # Build trees sklearn_tree = SklearnKDTree(X, leaf_size=40) numba_tree = build_kdtree(X, leaf_size=40) # Test different batch sizes batch_sizes = [100, 500, 1000, 2500, 5000, 10000] k = 20 # Warm up numba warmup_queries = np.random.rand(50, n_features).astype(np.float32) * 10.0 _ = parallel_tree_query(numba_tree, warmup_queries, k=k, output_rdist=False) sklearn_speedups = [] numba_speedups = [] for batch_size in batch_sizes: if batch_size > n_samples: continue # Generate query batch query_data = ( np.random.rand(batch_size, n_features).astype(np.float32) * 10.0 ) # Time both implementations sklearn_result, sklearn_time = time_function( sklearn_tree.query, query_data, k=k ) numba_result, numba_time = time_function( parallel_tree_query, numba_tree, query_data, k=k, output_rdist=False ) sklearn_qps = batch_size / sklearn_time numba_qps = batch_size / numba_time speedup = sklearn_time / numba_time test_name = f"batch_scaling_{batch_size}" perf_metrics.record_metric(test_name, "sklearn_qps", sklearn_qps) perf_metrics.record_metric(test_name, "numba_qps", numba_qps) perf_metrics.record_metric(test_name, "speedup", speedup) print( f"\nBatch size {batch_size:5d}: Sklearn {sklearn_qps:8.0f} q/s, " f"Numba {numba_qps:8.0f} q/s, Speedup: {speedup:.2f}x" ) # Verify correctness sklearn_dists, sklearn_inds = sklearn_result numba_dists, numba_inds = numba_result assert sklearn_dists.shape == numba_dists.shape # Performance should be reasonable for larger batches # Small batches may be slower due to parallelization overhead if batch_size >= 3000: # Adjusted threshold based on empirical results assert ( numba_time < sklearn_time * 1.5 ), f"Numba too slow for large batch {batch_size}: {speedup:.2f}x" # Expect advantage for large batches assert ( speedup > 0.8 ), f"Expected numba advantage for large batch {batch_size}: {speedup:.2f}x" elif batch_size >= 1000: # Medium batches should be competitive assert ( numba_time < sklearn_time * 2.0 ), f"Numba too slow for medium batch {batch_size}: {speedup:.2f}x" print(f"\nBatch Scaling Analysis:") print( f" Numba shows increasing advantage with larger batches due to parallelization benefits" ) print( f" Small batches (<1000) have overhead, large batches (>2000) show significant speedup" ) print(f" This demonstrates numba's optimization for large query workloads") def test_kdtree_query_performance_ultra_large_batch(self, perf_metrics): """Test numba performance on ultra-large query batches (its optimal use case).""" np.random.seed(42) # Use a reasonably sized dataset for the tree n_samples, n_features = 15000, 6 X = np.random.rand(n_samples, n_features).astype(np.float32) * 10.0 # Build trees sklearn_tree = SklearnKDTree(X, leaf_size=40) numba_tree = build_kdtree(X, leaf_size=40) # Test with ultra-large query batch - this is numba's sweet spot np.random.seed(123) n_queries = 25000 # Very large query batch query_data = np.random.rand(n_queries, n_features).astype(np.float32) * 10.0 k = 25 # Warm up numba _ = parallel_tree_query(numba_tree, query_data[:20], k=k, output_rdist=False) # Time both implementations sklearn_result, sklearn_time = time_function( sklearn_tree.query, query_data, k=k ) numba_result, numba_time = time_function( parallel_tree_query, numba_tree, query_data, k=k, output_rdist=False ) # Calculate metrics sklearn_qps = n_queries / sklearn_time numba_qps = n_queries / numba_time speedup = sklearn_time / numba_time # Record metrics test_name = f"ultra_large_batch_{n_queries}_queries" perf_metrics.record_metric(test_name, "sklearn_time", sklearn_time) perf_metrics.record_metric(test_name, "numba_time", numba_time) perf_metrics.record_metric(test_name, "speedup", speedup) perf_metrics.record_metric(test_name, "sklearn_qps", sklearn_qps) perf_metrics.record_metric(test_name, "numba_qps", numba_qps) print(f"\nUltra-Large Batch Performance ({n_queries} queries, k={k}):") print(f" Dataset: {n_samples} samples x {n_features} features") print(f" Sklearn: {sklearn_time:.4f}s ({sklearn_qps:,.0f} queries/sec)") print(f" Numba: {numba_time:.4f}s ({numba_qps:,.0f} queries/sec)") print(f" Speedup: {speedup:.2f}x") print( f" Efficiency gain: {(numba_qps - sklearn_qps):,.0f} additional queries/sec" ) # Verify correctness sklearn_dists, sklearn_inds = sklearn_result numba_dists, numba_inds = numba_result assert sklearn_dists.shape == (n_queries, k) assert numba_dists.shape == (n_queries, k) assert np.all(np.isfinite(numba_dists)) assert np.all(numba_inds >= 0) assert np.all(numba_inds < n_samples) # Performance expectations for ultra-large batches # This is numba's optimal use case - should show significant speedup assert ( numba_time < sklearn_time ), f"Numba should be faster for ultra-large batches: {speedup:.2f}x" # Expect substantial speedup on ultra-large batches assert ( speedup > 1.0 ), f"Expected major numba advantage for ultra-large batches: {speedup:.2f}x (target: >1.0x)" # Throughput should be significantly higher assert ( numba_qps > sklearn_qps * 1.0 ), f"Expected 1.0x+ throughput improvement: {numba_qps/sklearn_qps:.2f}x" @pytest.mark.performance class TestKDTreeRegressionBaseline: """Baseline performance tests for regression detection.""" def test_kdtree_baseline_performance(self): """ Baseline performance test for KDTree operations. Establishes performance baselines that can be used to detect regressions. """ np.random.seed(42) # Standard test dataset n_samples, n_features = 10000, 5 X = np.random.rand(n_samples, n_features).astype(np.float32) * 10.0 # Warm up numba compilation warmup_tree = build_kdtree(X[:100], leaf_size=40) warmup_queries = X[:10] _ = parallel_tree_query(warmup_tree, warmup_queries, k=10, output_rdist=False) print(f"\nKDTree Baseline Performance Report:") print(f" Dataset: {n_samples} samples x {n_features} features") print(f" Hardware: {platform.platform()}") if HAS_PSUTIL: print(f" CPU cores: {psutil.cpu_count(logical=True)}") print(f" Memory: {psutil.virtual_memory().total / (1024**3):.1f} GB") else: import os print(f" CPU cores: {os.cpu_count() or 'unknown'}") # Test construction performance sklearn_tree, sklearn_construction_time = time_function( SklearnKDTree, X, leaf_size=40 ) numba_tree, numba_construction_time = time_function( build_kdtree, X, leaf_size=40 ) # Test query performance with large batch (target use case) n_queries = 5000 # Large query batch to showcase parallel advantages query_data = np.random.rand(n_queries, n_features).astype(np.float32) * 10.0 k = 30 # Reasonable k value sklearn_result, sklearn_query_time = time_function( sklearn_tree.query, query_data, k=k ) numba_result, numba_query_time = time_function( parallel_tree_query, numba_tree, query_data, k=k, output_rdist=False ) # Calculate metrics construction_speedup = sklearn_construction_time / numba_construction_time query_speedup = sklearn_query_time / numba_query_time print(f"\nConstruction Performance:") print(f" Sklearn: {sklearn_construction_time:.4f} seconds") print(f" Numba: {numba_construction_time:.4f} seconds") print(f" Speedup: {construction_speedup:.2f}x") print(f"\nQuery Performance ({n_queries} queries, k={k}):") print(f" Sklearn: {sklearn_query_time:.4f} seconds") print(f" Numba: {numba_query_time:.4f} seconds") print(f" Speedup: {query_speedup:.2f}x") print(f"\nThroughput:") print(f" Construction: {n_samples/numba_construction_time:.0f} samples/sec") print(f" Queries: {n_queries/numba_query_time:.0f} queries/sec") # Basic performance requirements assert ( numba_construction_time < 2.0 ), f"Construction too slow: {numba_construction_time:.4f}s" assert numba_query_time < 1.0, f"Queries too slow: {numba_query_time:.4f}s" # Verify results are correct sklearn_dists, sklearn_inds = sklearn_result numba_dists, numba_inds = numba_result assert sklearn_dists.shape == numba_dists.shape assert sklearn_inds.shape == numba_inds.shape assert np.all(np.isfinite(numba_dists)) assert np.all(numba_inds >= 0) assert np.all(numba_inds < n_samples) # Expected performance characteristics # After warmup, numba should be competitive or better print(f"\nPerformance Analysis:") if construction_speedup > 1.0: print(f" ✅ Construction {construction_speedup:.2f}x faster than sklearn") else: print( f" ⚠️ Construction {1/construction_speedup:.2f}x slower than sklearn" ) if query_speedup > 1.0: print(f" ✅ Queries {query_speedup:.2f}x faster than sklearn") else: print(f" ⚠️ Queries {1/query_speedup:.2f}x slower than sklearn") return_info = { "construction_speedup": construction_speedup, "query_speedup": query_speedup, "numba_construction_time": numba_construction_time, "numba_query_time": numba_query_time, } # Note: return_info could be used for CI comparison but we don't return it # to avoid pytest warnings ================================================ FILE: evoc/uint8_nndescent.py ================================================ import numba import numpy as np from numba import types from numba.core import cgutils from numba.extending import intrinsic import llvmlite.ir as ir from .common_nndescent import ( tau_rand_int, make_heap, deheap_sort, flagged_heap_push, build_candidates, apply_graph_update_array, apply_sorted_graph_updates, ) from .nested_parallelism import ENABLE_NESTED_PARALLELISM # Used for a floating point "nearly zero" comparison EPS = 1e-8 INT32_MIN = np.iinfo(np.int32).min + 1 INT32_MAX = np.iinfo(np.int32).max - 1 INF = np.float32(np.inf) point_indices_type = numba.int32[::1] @intrinsic def popcnt_u8(typingctx, val): """Hardware popcount for uint8 using LLVM intrinsic.""" sig = types.uint8(types.uint8) def popcnt_u8_impl(context, builder, sig, args): [val] = args # Declare LLVM's ctpop intrinsic for i8 llvm_i8 = val.type fnty = ir.FunctionType(llvm_i8, [llvm_i8]) llvm_ctpop = cgutils.get_or_insert_function( builder.module, fnty, "llvm.ctpop.i8" ) result = builder.call(llvm_ctpop, [val]) return result return sig, popcnt_u8_impl @intrinsic def popcnt_u64(typingctx, val): """Hardware popcount for uint64 using LLVM intrinsic.""" sig = types.uint64(types.uint64) def popcnt_u64_impl(context, builder, sig, args): [val] = args llvm_i64 = val.type fnty = ir.FunctionType(llvm_i64, [llvm_i64]) llvm_ctpop = cgutils.get_or_insert_function( builder.module, fnty, "llvm.ctpop.i64" ) result = builder.call(llvm_ctpop, [val]) return result return sig, popcnt_u64_impl @numba.njit( [ "f4(u1[::1],u1[::1])", numba.types.float32( numba.types.Array(numba.types.uint8, 1, "C", readonly=True), numba.types.Array(numba.types.uint8, 1, "C", readonly=True), ), ], fastmath=True, cache=True, nogil=True, ) def fast_bit_jaccard(x, y): """Binary Jaccard using hardware POPCNT instruction.""" result = np.uint32(0) denom = np.uint32(0) dim = x.shape[0] for i in range(dim): and_val = x[i] & y[i] or_val = x[i] | y[i] result += popcnt_u8(and_val) denom += popcnt_u8(or_val) if denom > 0: return -(np.float32(result) / np.float32(denom)) else: return 0.0 @intrinsic def load_u64_from_u8_array(typingctx, arr, offset): """Load a uint64 from a uint8 array at given byte offset.""" sig = types.uint64(types.Array(types.uint8, 1, "C"), types.intp) def load_u64_impl(context, builder, sig, args): [arr, offset] = args # Get the array structure ary = context.make_array(sig.args[0])(context, builder, arr) ptr = ary.data # Get element pointer at offset elem_ptr = builder.gep(ptr, [offset]) # Cast uint8* to uint64* i64_ptr_type = ir.PointerType(ir.IntType(64)) ptr_u64 = builder.bitcast(elem_ptr, i64_ptr_type) # Load uint64 value = builder.load(ptr_u64) return value return sig, load_u64_impl @numba.njit( [ "f4(u1[::1],u1[::1])", numba.types.float32( numba.types.Array(numba.types.uint8, 1, "C", readonly=True), numba.types.Array(numba.types.uint8, 1, "C", readonly=True), ), ], fastmath=True, cache=True, boundscheck=False, nogil=True, ) def fast_bit_jaccard_u64(x, y): """ Use load intrinsic to avoid type conversion overhead. REQUIRES: Array size divisible by 8. """ result = np.uint64(0) denom = np.uint64(0) n_u64 = x.shape[0] // 8 for i in range(n_u64): offset = i * 8 # Load uint64 values directly x_val = load_u64_from_u8_array(x, offset) y_val = load_u64_from_u8_array(y, offset) and_val = x_val & y_val or_val = x_val | y_val result += popcnt_u64(and_val) denom += popcnt_u64(or_val) if denom > 0: return -(np.float32(result) / np.float32(denom)) else: return 0.0 @numba.njit( numba.types.Tuple((numba.int32[::1], numba.int32[::1]))( numba.types.Array(numba.types.uint8, 2, "C", readonly=True), numba.int32[::1], numba.int64[::1], ), locals={ "n_left": numba.uint32, "n_right": numba.uint32, "left_data": numba.types.Array(numba.types.uint8, 1, "C", readonly=True), "right_data": numba.types.Array(numba.types.uint8, 1, "C", readonly=True), "test_data": numba.types.Array(numba.types.uint8, 1, "C", readonly=True), "hyperplane_vector": numba.uint8[::1], "hyperplane_offset": numba.float32, "margin": numba.float32, "d": numba.uint32, "i": numba.uint32, "left_index": numba.uint32, "right_index": numba.uint32, }, fastmath=True, nogil=True, cache=True, ) def uint8_random_projection_split(data, indices, rng_state): """Given a set of ``graph_indices`` for graph_data points from ``graph_data``, create a random hyperplane to split the graph_data, returning two arrays graph_indices that fall on either side of the hyperplane. This is the basis for a random projection tree, which simply uses this splitting recursively. This particular split uses cosine distance to determine the hyperplane and which side each graph_data sample falls on. Parameters ---------- data: array of shape (n_samples, n_features) The original graph_data to be split indices: array of shape (tree_node_size,) The graph_indices of the elements in the ``graph_data`` array that are to be split in the current operation. rng_state: array of int64, shape (3,) The internal state of the rng Returns ------- indices_left: array The elements of ``graph_indices`` that fall on the "left" side of the random hyperplane. indices_right: array The elements of ``graph_indices`` that fall on the "left" side of the random hyperplane. """ dim = data.shape[1] # Select two random points, set the hyperplane between them left_index = tau_rand_int(rng_state) % indices.shape[0] right_index = tau_rand_int(rng_state) % indices.shape[0] right_index += left_index == right_index right_index = right_index % indices.shape[0] left = indices[left_index] right = indices[right_index] left_data = data[left] right_data = data[right] # Compute the normal vector to the hyperplane (the vector between # the two points) hyperplane_vector = np.empty(dim * 2, dtype=np.uint8) positive_hyperplane_component = hyperplane_vector[:dim] negative_hyperplane_component = hyperplane_vector[dim:] for d in range(dim): xor_vector = left_data[d] ^ right_data[d] positive_hyperplane_component[d] = xor_vector & left_data[d] negative_hyperplane_component[d] = xor_vector & right_data[d] hyperplane_norm = 0.0 left_norm = 0.0 right_norm = 0.0 for d in range(dim): hyperplane_norm += popcnt_u8(hyperplane_vector[d]) left_norm += popcnt_u8(left_data[d]) right_norm += popcnt_u8(right_data[d]) # For each point compute the margin (project into normal vector) # If we are on lower side of the hyperplane put in one pile, otherwise # put it in the other pile (if we hit hyperplane on the nose, flip a coin) n_left = 0 n_right = 0 side = np.empty(indices.shape[0], np.bool_) for i in range(indices.shape[0]): margin = 0.0 local_rng_state = rng_state + np.int64(i) test_data = data[indices[i]] for d in range(dim): margin += popcnt_u8(positive_hyperplane_component[d] & test_data[d]) margin -= popcnt_u8(negative_hyperplane_component[d] & test_data[d]) if abs(margin) < EPS: side[i] = np.bool_(tau_rand_int(local_rng_state) % 2) if side[i] == 0: n_left += 1 else: n_right += 1 elif margin > 0: side[i] = 0 n_left += 1 else: side[i] = 1 n_right += 1 # If all points end up on one side, something went wrong numerically # In this case, assign points randomly; they are likely very close anyway if n_left == 0 or n_right == 0: n_left = 0 n_right = 0 for i in range(indices.shape[0]): side[i] = tau_rand_int(rng_state) % 2 if side[i] == 0: n_left += 1 else: n_right += 1 # Now that we have the counts allocate arrays indices_left = np.empty(n_left, dtype=np.int32) indices_right = np.empty(n_right, dtype=np.int32) # Populate the arrays with graph_indices according to which side they fell on n_left = 0 n_right = 0 for i in range(side.shape[0]): if side[i] == 0: indices_left[n_left] = indices[i] n_left += 1 else: indices_right[n_right] = indices[i] n_right += 1 return indices_left, indices_right @numba.njit( numba.void( numba.types.Array(numba.types.uint8, 2, "C", readonly=True), numba.int32[::1], numba.types.ListType(numba.int32[::1]), numba.int64[::1], numba.int64, numba.int64, ), nogil=True, cache=True, ) def make_uint8_tree( data, indices, point_indices, rng_state, leaf_size=30, max_depth=200, ): if indices.shape[0] > leaf_size and max_depth > 0: ( left_indices, right_indices, ) = uint8_random_projection_split(data, indices, rng_state) make_uint8_tree( data, left_indices, point_indices, rng_state, leaf_size, max_depth - 1, ) make_uint8_tree( data, right_indices, point_indices, rng_state, leaf_size, max_depth - 1, ) else: point_indices.append(indices) return @numba.njit( numba.int32[:, ::1]( numba.types.Array(numba.types.uint8, 2, "C", readonly=True), numba.int64[::1], numba.int64, numba.int64, ), nogil=True, locals={"n_leaves": numba.int64, "i": numba.int64}, parallel=True, cache=True, ) def make_uint8_leaf_array_parallel(data, rng_state, leaf_size=30, max_depth=200): indices = np.arange(data.shape[0]).astype(np.int32) point_indices = numba.typed.List.empty_list(point_indices_type) make_uint8_tree( data, indices, point_indices, rng_state, leaf_size, max_depth=max_depth, ) n_leaves = numba.int64(len(point_indices)) max_leaf_size = leaf_size for i in numba.prange(n_leaves): points = point_indices[numba.int64(i)] max_leaf_size = max(max_leaf_size, numba.int32(len(points))) result = np.full((n_leaves, max_leaf_size), -1, dtype=np.int32) for i in numba.prange(n_leaves): points = point_indices[numba.int64(i)] leaf_size = numba.int32(len(points)) result[i, :leaf_size] = points return result @numba.njit( numba.int32[:, ::1]( numba.types.Array(numba.types.uint8, 2, "C", readonly=True), numba.int64[::1], numba.int64, numba.int64, ), nogil=True, locals={"n_leaves": numba.int64, "i": numba.int64}, parallel=False, cache=True, ) def make_uint8_leaf_array_serial(data, rng_state, leaf_size=30, max_depth=200): indices = np.arange(data.shape[0]).astype(np.int32) point_indices = numba.typed.List.empty_list(point_indices_type) make_uint8_tree( data, indices, point_indices, rng_state, leaf_size, max_depth=max_depth, ) n_leaves = numba.int64(len(point_indices)) max_leaf_size = leaf_size for i in range(n_leaves): points = point_indices[numba.int64(i)] max_leaf_size = max(max_leaf_size, numba.int32(len(points))) result = np.full((n_leaves, max_leaf_size), -1, dtype=np.int32) for i in range(n_leaves): points = point_indices[numba.int64(i)] leaf_size = numba.int32(len(points)) result[i, :leaf_size] = points return result @numba.njit( numba.types.List(numba.int32[:, ::1])( numba.types.Array(numba.types.uint8, 2, "C", readonly=True), numba.int64[:, ::1], numba.int64, numba.int64, ), parallel=True, cache=True, ) def make_uint8_forest_no_nested_parallelism(data, rng_states, leaf_size, max_depth): result = [np.empty((1, 1), dtype=np.int32)] * rng_states.shape[0] for i in numba.prange(len(result)): result[i] = make_uint8_leaf_array_serial( data, rng_states[i], leaf_size, max_depth=max_depth ) return result @numba.njit( numba.types.List(numba.int32[:, ::1])( numba.types.Array(numba.types.uint8, 2, "C", readonly=True), numba.int64[:, ::1], numba.int64, numba.int64, ), parallel=True, cache=True, ) def make_uint8_forest_with_nested_parallelism(data, rng_states, leaf_size, max_depth): result = [np.empty((1, 1), dtype=np.int32)] * rng_states.shape[0] for i in numba.prange(len(result)): result[i] = make_uint8_leaf_array_parallel( data, rng_states[i], leaf_size, max_depth=max_depth ) return result def make_uint8_forest(data, rng_states, leaf_size=30, max_depth=200): if ENABLE_NESTED_PARALLELISM: return make_uint8_forest_with_nested_parallelism( data, rng_states, leaf_size, max_depth ) else: return make_uint8_forest_no_nested_parallelism( data, rng_states, leaf_size, max_depth ) @numba.njit( numba.float32[:, :, ::1]( numba.float32[:, :, ::1], numba.int32[::1], numba.types.Array(numba.types.int32, 2, "C", readonly=True), numba.float32[:], numba.types.Array(numba.types.uint8, 2, "C", readonly=True), numba.int64, ), parallel=True, locals={ "d": numba.float32, "p": numba.int32, "q": numba.int32, "t": numba.uint16, "r": numba.uint32, "n": numba.uint32, "idx": numba.uint32, "data_p": numba.types.Array(numba.types.uint8, 1, "C", readonly=True), }, cache=True, ) def generate_leaf_updates_uint8( updates, n_updates_per_thread, leaf_block, dist_thresholds, data, n_threads ): block_size = leaf_block.shape[0] rows_per_thread = (block_size // n_threads) + 1 for t in numba.prange(n_threads): idx = 0 for r in range(rows_per_thread): n = t * rows_per_thread + r if n >= block_size: break for i in range(leaf_block.shape[1]): p = leaf_block[n, i] if p < 0: break data_p = data[p] for j in range(i, leaf_block.shape[1]): q = leaf_block[n, j] if q < 0: break d = fast_bit_jaccard(data_p, data[q]) if d < dist_thresholds[p] or d < dist_thresholds[q]: updates[t, idx, 0] = p updates[t, idx, 1] = q updates[t, idx, 2] = d idx += 1 n_updates_per_thread[t] = idx return updates @numba.njit( [ numba.void( numba.types.Array(numba.types.uint8, 2, "C", readonly=True), numba.types.Tuple( (numba.int32[:, ::1], numba.float32[:, ::1], numba.uint8[:, ::1]) ), numba.types.optional( numba.types.Array(numba.types.int32, 2, "C", readonly=True) ), numba.types.int32, ), ], locals={ "d": numba.float32, "p": numba.int32, "q": numba.int32, "i": numba.uint16, "updates": numba.float32[:, :, ::1], "n_updates_per_thread": numba.int32[::1], }, parallel=True, cache=True, ) def init_rp_tree_uint8(data, current_graph, leaf_array, n_threads): n_leaves = leaf_array.shape[0] block_size = 64 * n_threads n_blocks = n_leaves // block_size max_leaf_size = leaf_array.shape[1] updates_per_thread = ( int(block_size * max_leaf_size * (max_leaf_size - 1) / (2 * n_threads)) + 1 ) updates = np.zeros((n_threads, updates_per_thread, 3), dtype=np.float32) n_updates_per_thread = np.zeros(n_threads, dtype=np.int32) for i in range(n_blocks + 1): block_start = i * block_size block_end = min(n_leaves, (i + 1) * block_size) leaf_block = leaf_array[block_start:block_end] dist_thresholds = current_graph[1][:, 0] updates = generate_leaf_updates_uint8( updates, n_updates_per_thread, leaf_block, dist_thresholds, data, n_threads ) n_vertices = current_graph[0].shape[0] vertex_block_size = n_vertices // n_threads + 1 for t in numba.prange(n_threads): block_start = t * vertex_block_size block_end = min(block_start + vertex_block_size, n_vertices) for j in range(n_threads): for k in range(n_updates_per_thread[j]): p = np.int32(updates[j, k, 0]) q = np.int32(updates[j, k, 1]) d = np.float32(updates[j, k, 2]) if p == -1 or q == -1: continue if p >= block_start and p < block_end: flagged_heap_push( current_graph[1][p], current_graph[0][p], current_graph[2][p], d, q, ) if q >= block_start and q < block_end: flagged_heap_push( current_graph[1][q], current_graph[0][q], current_graph[2][q], d, p, ) @numba.njit( numba.types.void( numba.int32, numba.types.Array(numba.types.uint8, 2, "C", readonly=True), numba.types.Tuple( (numba.int32[:, ::1], numba.float32[:, ::1], numba.uint8[:, ::1]) ), numba.int64[::1], ), fastmath=True, locals={"d": numba.float32, "idx": numba.int32, "i": numba.int32}, cache=True, ) def init_random_uint8(n_neighbors, data, heap, rng_state): for i in range(data.shape[0]): if heap[0][i, 0] < 0.0: for j in range(n_neighbors - np.sum(heap[0][i] >= 0.0)): idx = np.abs(tau_rand_int(rng_state)) % data.shape[0] if idx in heap[0][i]: continue d = fast_bit_jaccard(data[idx], data[i]) flagged_heap_push(heap[1][i], heap[0][i], heap[2][i], d, idx) return @numba.njit( numba.types.void( numba.float32[:, :, ::1], numba.int32[::1], numba.int32[:, ::1], numba.int32[:, ::1], numba.float32[:], numba.types.Array(numba.types.uint8, 2, "C", readonly=True), numba.int64, ), locals={ "data_p": numba.types.Array(numba.types.uint8, 1, "C", readonly=True), }, parallel=True, cache=True, ) def generate_graph_update_array_uint8( update_array, n_updates_per_thread, new_candidate_block, old_candidate_block, dist_thresholds, data, n_threads, ): block_size = new_candidate_block.shape[0] max_new_candidates = new_candidate_block.shape[1] max_old_candidates = old_candidate_block.shape[1] rows_per_thread = (block_size // n_threads) + 1 for t in numba.prange(n_threads): idx = 0 updates_are_full = False for r in range(rows_per_thread): i = t * rows_per_thread + r if i >= block_size: break for j in range(max_new_candidates): p = int(new_candidate_block[i, j]) if p < 0: continue data_p = data[p] for k in range(j, max_new_candidates): q = int(new_candidate_block[i, k]) if q < 0: continue d = fast_bit_jaccard(data_p, data[q]) if d <= dist_thresholds[p] or d <= dist_thresholds[q]: update_array[t, idx, 0] = p update_array[t, idx, 1] = q update_array[t, idx, 2] = d idx += 1 if idx >= update_array.shape[1]: updates_are_full = True break if updates_are_full: break for k in range(max_old_candidates): q = int(old_candidate_block[i, k]) if q < 0: continue d = fast_bit_jaccard(data_p, data[q]) if d <= dist_thresholds[p] or d <= dist_thresholds[q]: update_array[t, idx, 0] = p update_array[t, idx, 1] = q update_array[t, idx, 2] = d idx += 1 if idx >= update_array.shape[1]: updates_are_full = True break if updates_are_full: break if updates_are_full: break n_updates_per_thread[t] = idx @numba.njit( numba.void( numba.float32[:, :, ::1], numba.int32[:, ::1], numba.int32[:, ::1], numba.int32[:, ::1], numba.float32[:], numba.types.Array(numba.types.uint8, 2, "C", readonly=True), numba.int64, ), locals={ "data_p": numba.types.Array(numba.types.uint8, 1, "C", readonly=True), "dist_thresh_p": numba.float32, "dist_thresh_q": numba.float32, "p": numba.int32, "q": numba.int32, "d": numba.float32, "max_updates": numba.intp, "max_threshold": numba.float32, "p_block": numba.int32, "q_block": numba.int32, }, parallel=True, cache=True, boundscheck=False, ) def generate_sorted_graph_update_array_uint8( update_array, n_updates_per_block, new_candidate_block, old_candidate_block, dist_thresholds, data, n_threads, ): """ Generate graph updates pre-sorted by target block for uint8 data. """ block_size_candidates = new_candidate_block.shape[0] max_new_candidates = new_candidate_block.shape[1] max_old_candidates = old_candidate_block.shape[1] rows_per_thread = (block_size_candidates // n_threads) + 1 n_vertices = data.shape[0] vertex_block_size = n_vertices // n_threads + 1 max_updates = update_array.shape[1] max_updates_per_src_thread = max_updates // n_threads # Reset update counts for b in numba.prange(n_threads): for t in range(n_threads + 1): n_updates_per_block[b, t] = 0 # Each thread generates updates and places them in appropriate buckets for t in numba.prange(n_threads): # Thread-local counters for each bucket local_counts = np.zeros(n_threads, dtype=np.int32) for r in range(rows_per_thread): i = t * rows_per_thread + r if i >= block_size_candidates: break for j in range(max_new_candidates): p = new_candidate_block[i, j] if p < 0: continue data_p = data[p] dist_thresh_p = dist_thresholds[p] p_block = p // vertex_block_size if p_block >= n_threads: p_block = n_threads - 1 # Compare with other new candidates for k in range(j, max_new_candidates): q = new_candidate_block[i, k] if q < 0: continue d = fast_bit_jaccard(data_p, data[q]) dist_thresh_q = dist_thresholds[q] max_threshold = max(dist_thresh_p, dist_thresh_q) if d <= max_threshold: q_block = q // vertex_block_size if q_block >= n_threads: q_block = n_threads - 1 # Place update in p's bucket bucket_idx = local_counts[p_block] write_idx = t * max_updates_per_src_thread + bucket_idx if write_idx < max_updates: update_array[p_block, write_idx, 0] = p update_array[p_block, write_idx, 1] = q update_array[p_block, write_idx, 2] = d local_counts[p_block] += 1 # If q is in a different block, also place in q's bucket if q_block != p_block: bucket_idx = local_counts[q_block] write_idx = t * max_updates_per_src_thread + bucket_idx if write_idx < max_updates: update_array[q_block, write_idx, 0] = p update_array[q_block, write_idx, 1] = q update_array[q_block, write_idx, 2] = d local_counts[q_block] += 1 # Compare with old candidates for k in range(max_old_candidates): q = old_candidate_block[i, k] if q < 0: continue d = fast_bit_jaccard(data_p, data[q]) dist_thresh_q = dist_thresholds[q] max_threshold = max(dist_thresh_p, dist_thresh_q) if d <= max_threshold: q_block = q // vertex_block_size if q_block >= n_threads: q_block = n_threads - 1 # Place update in p's bucket bucket_idx = local_counts[p_block] write_idx = t * max_updates_per_src_thread + bucket_idx if write_idx < max_updates: update_array[p_block, write_idx, 0] = p update_array[p_block, write_idx, 1] = q update_array[p_block, write_idx, 2] = d local_counts[p_block] += 1 # If q is in a different block, also place in q's bucket if q_block != p_block: bucket_idx = local_counts[q_block] write_idx = t * max_updates_per_src_thread + bucket_idx if write_idx < max_updates: update_array[q_block, write_idx, 0] = p update_array[q_block, write_idx, 1] = q update_array[q_block, write_idx, 2] = d local_counts[q_block] += 1 # Record total updates generated by this thread for each bucket for b in range(n_threads): n_updates_per_block[b, t + 1] = local_counts[b] def nn_descent_uint8( data, n_neighbors, rng_state, max_candidates=50, n_iters=10, delta=0.001, delta_improv=None, leaf_array=None, verbose=False, ): """ Perform approximate nearest neighbor descent algorithm using uint8 data. Parameters: - data: The input data array. - n_neighbors: The number of nearest neighbors to search for. - rng_state: The random number generator state. - max_candidates: The maximum number of candidates to consider during the search. Default is 50. - n_iters: The number of iterations to perform. Default is 10. - delta: The stopping threshold based on update count. Default is 0.001. - delta_improv: Optional stopping threshold based on relative improvement in total graph distance. When set (e.g., 0.001 for 0.1%), the algorithm will also terminate when the relative improvement in sum of all distances drops below this threshold. This can provide earlier termination on data with good structure, adapting to the intrinsic difficulty of the dataset. Default is None (disabled). - leaf_array: The array representing the leaf structure of the RP-tree. Default is None. - verbose: Whether to print progress information. Default is False. Returns: - The sorted nearest neighbor graph. """ n_threads = numba.get_num_threads() current_graph = make_heap(data.shape[0], n_neighbors) init_rp_tree_uint8(data, current_graph, leaf_array, n_threads) init_random_uint8(n_neighbors, data, current_graph, rng_state) n_vertices = data.shape[0] n_threads = numba.get_num_threads() block_size = 65536 // n_threads n_blocks = n_vertices // block_size max_updates_per_thread = int( ((max_candidates**2 + max_candidates * (max_candidates - 1) / 2) * block_size) ) update_array = np.empty((n_threads, max_updates_per_thread, 3), dtype=np.float32) n_updates_per_thread = np.zeros(n_threads, dtype=np.int32) # For distance-based termination prev_sum_dist = None for n in range(n_iters): if verbose: print("\t", n + 1, " / ", n_iters) (new_candidate_neighbors, old_candidate_neighbors) = build_candidates( current_graph, max_candidates, rng_state, n_threads ) c = 0 n_vertices = new_candidate_neighbors.shape[0] for i in range(n_blocks + 1): block_start = i * block_size block_end = min(n_vertices, (i + 1) * block_size) new_candidate_block = new_candidate_neighbors[block_start:block_end] old_candidate_block = old_candidate_neighbors[block_start:block_end] dist_thresholds = current_graph[1][:, 0] generate_graph_update_array_uint8( update_array, n_updates_per_thread, new_candidate_block, old_candidate_block, dist_thresholds, data, n_threads, ) c += apply_graph_update_array( current_graph, update_array, n_updates_per_thread, n_threads ) # Check update count termination if c <= delta * n_neighbors * data.shape[0]: if verbose: print("\tStopping threshold met -- exiting after", n + 1, "iterations") return deheap_sort(current_graph[0], current_graph[1]) # Check distance improvement termination (if enabled) if delta_improv is not None: all_distances = current_graph[1] valid_mask = all_distances < INF sum_dist = np.sum(all_distances[valid_mask]) if prev_sum_dist is not None: rel_improv = abs(sum_dist - prev_sum_dist) / abs(prev_sum_dist) if rel_improv < delta_improv: if verbose: print( f"\tDistance improvement threshold met ({rel_improv:.4%} < {delta_improv:.4%})" f" -- exiting after {n + 1} iterations" ) return deheap_sort(current_graph[0], current_graph[1]) prev_sum_dist = sum_dist block_size = min(n_vertices, 2 * block_size) n_blocks = n_vertices // block_size return deheap_sort(current_graph[0], current_graph[1]) def nn_descent_uint8_sorted( data, n_neighbors, rng_state, max_candidates=50, n_iters=10, delta=0.001, delta_improv=None, leaf_array=None, verbose=False, ): """ Perform approximate nearest neighbor descent algorithm using uint8 data. This version uses pre-sorted updates bucketed by target block for potentially better performance when n_threads is large. Parameters: - data: The input data array. - n_neighbors: The number of nearest neighbors to search for. - rng_state: The random number generator state. - max_candidates: The maximum number of candidates to consider during the search. Default is 50. - n_iters: The number of iterations to perform. Default is 10. - delta: The stopping threshold based on update count. Default is 0.001. - delta_improv: Optional stopping threshold based on relative improvement in total graph distance. When set (e.g., 0.001 for 0.1%), the algorithm will also terminate when the relative improvement in sum of all distances drops below this threshold. This can provide earlier termination on data with good structure, adapting to the intrinsic difficulty of the dataset. Default is None (disabled). - leaf_array: The array representing the leaf structure of the RP-tree. Default is None. - verbose: Whether to print progress information. Default is False. Returns: - The sorted nearest neighbor graph. """ n_threads = numba.get_num_threads() current_graph = make_heap(data.shape[0], n_neighbors) init_rp_tree_uint8(data, current_graph, leaf_array, n_threads) init_random_uint8(n_neighbors, data, current_graph, rng_state) n_vertices = data.shape[0] block_size = 65536 // n_threads n_blocks = n_vertices // block_size max_updates_per_thread = int( ((max_candidates**2 + max_candidates * (max_candidates - 1) / 2) * block_size) ) update_array = np.empty((n_threads, max_updates_per_thread, 3), dtype=np.float32) n_updates_per_block = np.zeros((n_threads, n_threads + 1), dtype=np.int32) # For distance-based termination prev_sum_dist = None for n in range(n_iters): if verbose: print("\t", n + 1, " / ", n_iters) (new_candidate_neighbors, old_candidate_neighbors) = build_candidates( current_graph, max_candidates, rng_state, n_threads ) c = 0 for i in range(n_blocks + 1): block_start = i * block_size block_end = min(n_vertices, (i + 1) * block_size) new_candidate_block = new_candidate_neighbors[block_start:block_end] old_candidate_block = old_candidate_neighbors[block_start:block_end] dist_thresholds = current_graph[1][:, 0] generate_sorted_graph_update_array_uint8( update_array, n_updates_per_block, new_candidate_block, old_candidate_block, dist_thresholds, data, n_threads, ) c += apply_sorted_graph_updates( current_graph, update_array, n_updates_per_block, n_threads ) # Check update count termination if c <= delta * n_neighbors * data.shape[0]: if verbose: print("\tStopping threshold met -- exiting after", n + 1, "iterations") return deheap_sort(current_graph[0], current_graph[1]) # Check distance improvement termination (if enabled) if delta_improv is not None: all_distances = current_graph[1] valid_mask = all_distances < INF sum_dist = np.sum(all_distances[valid_mask]) if prev_sum_dist is not None: rel_improv = abs(sum_dist - prev_sum_dist) / abs(prev_sum_dist) if rel_improv < delta_improv: if verbose: print( f"\tDistance improvement threshold met ({rel_improv:.4%} < {delta_improv:.4%})" f" -- exiting after {n + 1} iterations" ) return deheap_sort(current_graph[0], current_graph[1]) prev_sum_dist = sum_dist block_size = min(n_vertices, 2 * block_size) n_blocks = n_vertices // block_size return deheap_sort(current_graph[0], current_graph[1]) ================================================ FILE: pyproject.toml ================================================ [build-system] requires = ["setuptools>=61.2"] build-backend = "setuptools.build_meta" [project] name = "evoc" version = "0.3.1" authors = [{name = "Leland McInnes", email = "leland.mcinnes@gmail.com"}] maintainers = [{name = "Leland McInnes", email = "leland.mcinnes@gmail.com"}] description = "Embedding Vector Oriented Clustering" readme = "README.rst" keywords = ["embedding vector", "vector database", "topic modelling", "cluster", "clustering"] license = "BSD-2-Clause" classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Development Status :: 4 - Beta", "Operating System :: OS Independent", ] urls = {Homepage = "https://github.com/TutteInstitute/evoc"} requires-python = ">=3.10" dependencies = [ "numpy>=1.21", "scikit-learn>=1.1", "numba>=0.59", "tqdm", ] [dependency-groups] cicd = ["pytest", "pytest-azurepipelines", "pytest-cov", "matplotlib"] [tool.setuptools] zip-safe = false packages = ["evoc"] include-package-data = false ================================================ FILE: pytest.ini ================================================ [pytest] markers = performance: marks tests as performance tests (deselect with '-m "not performance"') slow: marks tests as slow running tests integration: marks tests as integration tests testpaths = evoc/tests addopts = -v --tb=short timeout = 300 filterwarnings = ignore::DeprecationWarning ignore::PendingDeprecationWarning ignore::FutureWarning ================================================ FILE: scripts/run_performance_tests.py ================================================ #!/usr/bin/env python3 """ Performance benchmark runner for evoc knn_graph module. This script runs performance tests and generates a report that can be used for performance regression monitoring in CI/CD pipelines. """ import argparse import json import sys import time import platform import subprocess from pathlib import Path def run_performance_tests(output_file=None, verbose=False): """Run performance tests and collect results.""" print("Running EVoC knn_graph performance benchmarks...") print(f"Platform: {platform.platform()}") print(f"Python: {platform.python_version()}") print("-" * 60) # Run pytest with performance markers cmd = [ sys.executable, "-m", "pytest", "evoc/tests/test_knn_graph_performance.py", "-m", "performance", "-v" ] if verbose: cmd.append("-s") # Add JSON report plugin if available try: import pytest_json_report if output_file: cmd.extend(["--json-report", f"--json-report-file={output_file}"]) except ImportError: print("Note: pytest-json-report not installed, basic output only") start_time = time.time() result = subprocess.run(cmd, capture_output=not verbose, text=True) duration = time.time() - start_time if result.returncode == 0: print(f"\nAll performance tests passed in {duration:.1f} seconds!") else: print(f"\nSome performance tests failed or had issues.") if not verbose and result.stdout: print("STDOUT:") print(result.stdout) if result.stderr: print("STDERR:") print(result.stderr) return result.returncode == 0 def generate_performance_report(test_results_file, output_file): """Generate a human-readable performance report.""" try: with open(test_results_file, 'r') as f: data = json.load(f) except FileNotFoundError: print(f"Test results file {test_results_file} not found") return False except json.JSONDecodeError: print(f"Could not parse JSON from {test_results_file}") return False # Extract performance metrics report = { 'timestamp': time.strftime('%Y-%m-%d %H:%M:%S'), 'platform': platform.platform(), 'python_version': platform.python_version(), 'total_tests': data.get('summary', {}).get('total', 0), 'passed_tests': data.get('summary', {}).get('passed', 0), 'failed_tests': data.get('summary', {}).get('failed', 0), 'duration': data.get('duration', 0), 'tests': [] } # Process individual test results for test in data.get('tests', []): if 'performance' in test.get('keywords', []): test_info = { 'name': test.get('nodeid', '').split('::')[-1], 'duration': test.get('duration', 0), 'outcome': test.get('outcome', 'unknown'), 'stdout': test.get('call', {}).get('stdout', '') } report['tests'].append(test_info) # Write report with open(output_file, 'w') as f: json.dump(report, f, indent=2) print(f"Performance report written to {output_file}") return True def check_performance_regression(current_file, baseline_file, threshold=1.5): """ Check for performance regressions by comparing current results to baseline. Args: current_file: JSON file with current test results baseline_file: JSON file with baseline results threshold: Maximum allowed slowdown ratio (e.g., 1.5 = 50% slower) Returns: bool: True if no significant regressions detected """ try: with open(current_file, 'r') as f: current = json.load(f) with open(baseline_file, 'r') as f: baseline = json.load(f) except (FileNotFoundError, json.JSONDecodeError) as e: print(f"Error reading performance data: {e}") return False print(f"Comparing performance to baseline from {baseline.get('timestamp', 'unknown')}") regressions = [] improvements = [] # Compare test durations current_tests = {t['name']: t for t in current.get('tests', [])} baseline_tests = {t['name']: t for t in baseline.get('tests', [])} for test_name in current_tests: if test_name in baseline_tests: current_duration = current_tests[test_name]['duration'] baseline_duration = baseline_tests[test_name]['duration'] if baseline_duration > 0: ratio = current_duration / baseline_duration if ratio > threshold: regressions.append({ 'test': test_name, 'current': current_duration, 'baseline': baseline_duration, 'ratio': ratio }) elif ratio < 0.8: # 20% improvement improvements.append({ 'test': test_name, 'current': current_duration, 'baseline': baseline_duration, 'ratio': ratio }) # Report results if regressions: print(f"\n⚠️ Performance regressions detected:") for reg in regressions: print(f" {reg['test']}: {reg['ratio']:.2f}x slower " f"({reg['current']:.3f}s vs {reg['baseline']:.3f}s)") if improvements: print(f"\n✅ Performance improvements:") for imp in improvements: print(f" {imp['test']}: {imp['ratio']:.2f}x faster " f"({imp['current']:.3f}s vs {imp['baseline']:.3f}s)") if not regressions and not improvements: print("\n✅ No significant performance changes detected") return len(regressions) == 0 def main(): parser = argparse.ArgumentParser(description="Run EVoC performance benchmarks") parser.add_argument("--output", "-o", help="Output file for test results (JSON)") parser.add_argument("--report", "-r", help="Generate human-readable report file") parser.add_argument("--baseline", "-b", help="Compare against baseline performance file") parser.add_argument("--threshold", "-t", type=float, default=1.5, help="Regression threshold (default: 1.5x slower)") parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output") args = parser.parse_args() # Default output file if not args.output: timestamp = time.strftime("%Y%m%d_%H%M%S") args.output = f"performance_results_{timestamp}.json" # Run performance tests success = run_performance_tests(args.output, args.verbose) if not success: print("Performance tests failed") return 1 # Generate report if requested if args.report: generate_performance_report(args.output, args.report) # Check for regressions if baseline provided if args.baseline: if not check_performance_regression(args.output, args.baseline, args.threshold): print("Performance regression detected!") return 1 return 0 if __name__ == "__main__": sys.exit(main()) ================================================ FILE: setup.py ================================================ from setuptools import setup if __name__ == '__main__': setup()