diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index b346c17..0000000 --- a/.dockerignore +++ /dev/null @@ -1,15 +0,0 @@ -.git -.github -.pytest_cache -.mypy_cache -.ruff_cache -__pycache__ -*.pyc -*.pyo -*.pyd -.venv -venv -dist -build -*.egg-info -.ipynb_checkpoints diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 328ab57..0000000 --- a/.flake8 +++ /dev/null @@ -1,7 +0,0 @@ - -[flake8] -ignore = E501, W503 -max-line-length = 88 -extend-ignore = E203 -max-complexity = 18 -select = B,C,E,F,W,T4,B9,B950 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 913a5d3..a1e39e7 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,33 +1,95 @@ -name: Publish Python package +name: Build and publish the package to internal PDBe PyPi registry, DockerHub and GitLab Container Registry +permissions: + contents: read on: - release: - types: [published] - workflow_dispatch: + pull_request: + branches: + - main + push: + branches: + - main + tags: + - "*" -permissions: - contents: read - id-token: write +env: + BASE_PYTHON_VERSION: &base_python_version "3.12" jobs: - publish: + build: + name: Lint and Test (Python ${{ matrix.python-version }}) runs-on: ubuntu-latest - environment: pypi + strategy: + matrix: + python-version: [*base_python_version, "3.13", "3.14"] steps: - - name: Check out repository - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 + - uses: actions/checkout@v4 + - name: Install uv + uses: astral-sh/setup-uv@v7 with: - python-version: "3.11" - - - name: Set up UV - uses: astral-sh/setup-uv@v5 + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + uv sync --locked --group test + - name: Run pre-commit checks + uses: pre-commit/action@v3.0.1 + if: matrix.python-version == env.BASE_PYTHON_VERSION + - name: Run Tests + run: | + uv run make test - - name: Build distributions - run: uv build + publish: + name: Publish + if: startsWith(github.ref, 'refs/tags/v') + needs: build + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/project/pdbe-shape-retrieval + permissions: + id-token: write # IMPORTANT: mandatory for trusted publishing - - name: Publish to PyPI + steps: + - uses: actions/checkout@v4 + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + python-version: "${{ env.BASE_PYTHON_VERSION }}" + - name: Create distribution + run: | + uv build + - name: Publish to internal PyPi + run: | + pip install twine + TWINE_PASSWORD=${{ secrets.TWINE_PASSWORD }} TWINE_USERNAME=${{ secrets.TWINE_USERNAME }} python -m twine upload --repository-url ${{ secrets.PIP_INDEX_URL }} dist/* + - name: Publish to public PyPi uses: pypa/gh-action-pypi-publish@release/v1 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Extract Docker tag from GitHub ref + run: echo "DOCKER_TAG=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV + - name: Build and push + uses: docker/build-push-action@v6 + with: + push: true + platforms: linux/amd64,linux/arm64/v8 + tags: | + ${{ secrets.DOCKER_USERNAME }}/pdbe-shape-retrieval:${{ env.DOCKER_TAG }} + ${{ secrets.DOCKER_USERNAME }}/pdbe-shape-retrieval:latest + - name: Login to GitLab Container Registry + run: echo ${{ secrets.REGISTRY_PASSWORD }} | docker login -u ${{ secrets.REGISTRY_USERNAME }} --password-stdin ${{ vars.REGISTRY_URL }} + - name: Build and tag Docker image + run: docker build -t ${{ vars.REGISTRY_URL }}/${{ secrets.REGISTRY_USERNAME }}/packages/pdbe-shape-retrieval:latest -t ${{ vars.REGISTRY_URL }}/${{ secrets.REGISTRY_USERNAME }}/packages/pdbe-shape-retrieval:${{ env.DOCKER_TAG }} . + - name: Push Docker image to GitLab Container Registry + run: | + docker push ${{ vars.REGISTRY_URL }}/${{ secrets.REGISTRY_USERNAME }}/packages/pdbe-shape-retrieval:latest + docker push ${{ vars.REGISTRY_URL }}/${{ secrets.REGISTRY_USERNAME }}/packages/pdbe-shape-retrieval:${{ env.DOCKER_TAG }} diff --git a/.gitignore b/.gitignore index 7caad8b..ced60cc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,6 @@ **/.vscode/ .idea/ Pipfile -# Vagrant -.vagrant/ # Mac/OSX .DS_Store @@ -40,16 +38,6 @@ wheels/ *.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/ @@ -59,72 +47,13 @@ htmlcov/ .cache nosetests.xml coverage.xml +report.xml *.cover .hypothesis/ .pytest_cache/ -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - # Jupyter Notebook .ipynb_checkpoints -# IPython -profile_default/ -ipython_config.py - -# pyenv -.python-version - -# celery beat schedule file -celerybeat-schedule - -# 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 -.envrc - - -TODO diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4a012c8..e9c3120 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,12 @@ +default_install_hook_types: + - pre-commit + - post-checkout + - post-merge + - post-rewrite + repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.1.0 + rev: v6.0.0 hooks: - id: check-yaml - id: check-added-large-files @@ -12,23 +18,13 @@ repos: - id: fix-byte-order-marker - id: end-of-file-fixer - id: check-ast - - id: no-commit-to-branch - - repo: https://gitlab.com/pycqa/flake8 - rev: 4.0.1 + - repo: https://github.com/astral-sh/uv-pre-commit + rev: 0.11.28 hooks: - - id: flake8 - additional_dependencies: [flake8-bugbear==21.4.3] - - repo: https://github.com/asottile/blacken-docs - rev: v1.12.0 + - id: uv-lock + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.20 hooks: - - id: blacken-docs - additional_dependencies: [black==20.8b1] - - repo: https://github.com/pycqa/isort - rev: 5.10.1 - hooks: - - id: isort - name: isort (python) - - repo: https://github.com/psf/black - rev: 21.12b0 - hooks: - - id: black + - id: ruff-check + args: [ --fix ] + - id: ruff-format diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Dockerfile b/Dockerfile index 9722413..8f9d021 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,23 +1,43 @@ -FROM python:3.11-slim +# Inspired from https://github.com/astral-sh/uv-docker-example/blob/5748835918ec293d547bbe0e42df34e140aca1eb/multistage.Dockerfile +FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder -ENV PYTHONUNBUFFERED=1 \ - PIP_NO_CACHE_DIR=1 \ - MAP2ZERNIKE_SETUP_DIR=/usr/local/bin \ - OBJ2GRID_PATH=/usr/local/bin/obj2grid \ - PATH="/usr/local/bin:${PATH}" +ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy + +# Omit development dependencies +ENV UV_NO_DEV=1 + +ENV UV_PYTHON_DOWNLOADS=0 + +ARG PIP_INDEX_URL +ENV UV_DEFAULT_INDEX=$PIP_INDEX_URL +ENV UV_EXTRA_INDEX_URL=https://pypi.org/simple WORKDIR /app +COPY pyproject.toml uv.lock /app/ +RUN uv sync --locked --no-install-project --no-dev + +COPY shape_retrieval /app/shape_retrieval +RUN uv tool install shape_retrieval + +FROM python:3.12-slim-bookworm +LABEL maintainer="Sreenath Sasidharan Nair " -RUN apt-get update \ - && apt-get install -y --no-install-recommends libgl1 libglib2.0-0 libgomp1 \ - && rm -rf /var/lib/apt/lists/* +RUN groupadd --system --gid 101 nonroot \ + && useradd --system --gid 101 --uid 101 --create-home nonroot -RUN python -m pip install --upgrade pip uv +COPY --from=builder --chown=nonroot:nonroot /app /app -COPY . /app +ENV PATH="/app/.venv/bin:$PATH" -RUN install -m 0755 /app/bin/map2zernike /usr/local/bin/map2zernike \ - && install -m 0755 /app/bin/obj2grid /usr/local/bin/obj2grid \ - && uv pip install --system . +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +# Disable core dumps +RUN echo -e "* soft core 0\n* hard core 0" >> /etc/security/limits.conf + +USER nonroot + +# Set the working directory to /app +WORKDIR /app -ENTRYPOINT ["shape_retrieval"] +CMD ["shape_retrieval"] diff --git a/LICENSE b/LICENSE index 646d4b5..d95b5fc 100644 --- a/LICENSE +++ b/LICENSE @@ -10,4 +10,3 @@ software distributed under the License is distributed on an either express or implied. See the License for the specific language governing permissions and limitations under the License. - diff --git a/README.md b/README.md index 21f1ac6..39da7b6 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# Shape retrieval for protein surfaces +# Shape retrieval for protein surfaces ## Basic information -This Python package calculates 3D shape descriptors for triangulated molecular surface meshes to analyse protein structure similarity. +This Python package calculates 3D shape descriptors for triangulated molecular surface meshes to analyse protein structure similarity. The code is based on [pyFM](https://github.com/RobinMagnet/pyFM) modules and [3D-Surfer 2.0](https://kiharalab.org/3d-surfer/) code and will: @@ -10,85 +10,55 @@ The code is based on [pyFM](https://github.com/RobinMagnet/pyFM) modules and [3D - Calculate 3D Shape descriptors for two protein structures: Wave Kernel Signatures (WKS), Heat Kernel Signatures (HKS) and 3D Zernike descriptors (3DZD) - Compute functional maps and refine methods (Zoomout and ICP ) - Compute similarity scores -- Provides analysis tools to compute a score square matrix and perform agglomerative clustering +- Provides analysis tools to compute a score square matrix and perform agglomerative clustering -To install the module ```shape_retrieval``` : -``` -git clone https://github.com/PDBe-KB/pdbe_shape-retrieval +To install the module `shape_retrieval`: -cd pdbe_shape-retrieval +```shell +$ git clone https://github.com/PDBe-KB/pdbe_shape_retrieval -python setup.py install +$ cd pdbe_shape_retrieval -``` -## Dependencies +$ uv sync +``` -This package requires the installation of [pyFM](https://github.com/RobinMagnet/pyFM) module for the calculation of spectral descriptors: +### Development +```shell +$ uv sync --group test +$ uv tool install pre-commit --with pre-commit-uv +$ pre-commit +$ pre-commit install +$ pre-commit run --all-files ``` -pip install pyfmaps -``` -For the calculation of Zernike Descriptors, binaries `obj2grid` and `map2zernike` from 3D-Surfer and obj2grid codes need to be provided. +## Dependencies + +This package requires the installation of [pyFM](https://github.com/RobinMagnet/pyFM) module for the calculation of spectral descriptors. + +For the calculation of Zernike Descriptors, binaries `obj2grid` and `map2zernike` from 3D-Surfer and obj2grid codes need to be provided. The binaries are available [here](https://github.com/PDBe-KB/pdbe_shape-retrieval/blob/main/bin) -To make your life easier when running the process, it is better to set two path environment variables for 3D-Surfer: +To make your life easier when running the process, it is better to set the following environment variables for 3D-Surfer: An environment variable to the `obj2grid` binary: -``` +```shell export OBJ2GRID_PATH="$PATH:your_path_to_obj2grid/obj2grid" ``` -A path to the `map2zernike binary` of 3D-Surfer : +A path to the `map2zernike` binary of 3D-Surfer: -``` +```shell export MAP2ZERNIKE_SETUP_DIR="/your_path_to_3DSurfer/bin/" ``` - -Other dependencies can be installed with: - -``` -pip install -r requirements.txt -``` -See [requirements.txt](https://github.com/PDBe-KB/pdbe_shape-retrieval/blob/main/requirements.txt) - - -For development: - -**pre-commit usage** - -``` -pip install pre-commit -pre-commit -pre-commit install -``` - - ## Usage -Follow the steps below to install the modules **pdbe_shape-retrieval** - -``` -cd pdbe_shape-retrieval/ - -python setup.py install . - -``` - -To run the modules in the command line: - -**pdbe_shape-retrieval**: - -``` -python pdbe_shape-retrieval/shape_utils/run.py [-h] --input_mesh1 INPUT_FILE_MESH_1 --input_mesh2 INPUT_FILE_MESH_2 --entry_ids ENTRY_ID_1 ENTRY_ID_2 -o PATH_TO_OUTPUT_DIR -``` -OR - -``` -shape_retrieval [-h] --input_mesh1 INPUT_FILE_MESH_1 --input_mesh2 INPUT_FILE_MESH_2 --entry_ids ENTRY_ID_1 ENTRY_ID_2 -o PATH_TO_OUTPUT_DIR +To run `shape_retrieval` in the command line: +```shell +$ shape_retrieval [-h] --input_mesh1 INPUT_FILE_MESH_1 --input_mesh2 INPUT_FILE_MESH_2 --entry_ids ENTRY_ID_1 ENTRY_ID_2 -o PATH_TO_OUTPUT_DIR ``` Required arguments are : @@ -96,11 +66,10 @@ Required arguments are : ``` --input_mesh1 : Triangulated mesh for structure 1 (.obj) --input_mesh2 : Triangulated mesh for structure 2 (.obj) ---entry_ids : Entry IDs for protein structures +--entry_ids : Entry IDs for protein structures --output (-o) : Output directory ``` - Other optional arguments: For pre-processing and fixing faulty meshes: @@ -117,7 +86,7 @@ To select the shape descriptor: Options for the calculation of spectral descriptors: ``` ---neigvecs : No. of eigenvalues/eigenvectors to process (>100). A minimum of neigvecs=100 will be used by default (recommended) +--neigvecs : No. of eigenvalues/eigenvectors to process (>100). A minimum of neigvecs=100 will be used by default (recommended) --n_ev : The least number of Laplacian eigenvalues to consider for the functional map. --ndescr : No. of descriptors to process (WKS/HKS). --landmarks : Input indices of landmarks @@ -147,7 +116,7 @@ The process will output CSV files with the WKS/HKS descriptors for the two input *DESCR_TYPE_descr_ENTRY_ID_1.csv* *DESCR_TYPE_descr_ENTRY_ID_2.csv* -where DESCR_TYPE is the selected descriptor (WKS) and ENTRY_ID is the entry id of the input structure. +where DESCR_TYPE is the selected descriptor (WKS) and ENTRY_ID is the entry id of the input structure. A vector of N descriptors is given for each vertex of the mesh, and therefore the No. of rows of the file is the no. of vertices of the mesh: @@ -170,7 +139,7 @@ The process will output a CSV file with the correspondence matrix or functional where ENTRY_ID_* is the entry ID for each input structure. -The csv file contains NxN rows and columns with values of the transformation coefficients c_ij. N is the number of terms or Laplace-Beltrami functions used in the expansion in fuctional space. +The csv file contains NxN rows and columns with values of the transformation coefficients c_ij. N is the number of terms or Laplace-Beltrami functions used in the expansion in fuctional space. For example if N=4 the output file would look like this: ``` @@ -183,7 +152,7 @@ The process will output a CSV file with the point-to-point map from mesh2 to mes *ENTRY_ID_1_ENTRY_ID_2_p2p21.csv* -In this output file the No. of rows N corresponds to the no. of vertices in Mesh 2 and each row displays the corresponding vertex index of Mesh 1. +In this output file the No. of rows N corresponds to the no. of vertices in Mesh 2 and each row displays the corresponding vertex index of Mesh 1. ``` 8 diff --git a/analysis_tools/clustering.py b/analysis_tools/clustering.py index 9bcfdc5..1632471 100644 --- a/analysis_tools/clustering.py +++ b/analysis_tools/clustering.py @@ -1,20 +1,17 @@ +import itertools +from itertools import combinations_with_replacement + import numpy as np import pandas as pd - -from scipy.linalg import norm -from scipy.cluster.hierarchy import dendrogram -from sklearn.cluster import AgglomerativeClustering -from sklearn import cluster -from scipy.cluster.hierarchy import dendrogram, from_mlab_linkage, cut_tree, leaves_list, set_link_color_palette, to_tree, fcluster -from scipy.spatial.distance import squareform, pdist +from scipy.cluster.hierarchy import cut_tree, fcluster +from scipy.spatial.distance import squareform from sklearn.metrics import silhouette_samples -import scipy.stats as st -import itertools -from itertools import combinations_with_replacement + def get_pairs_fast(arr): return list(combinations_with_replacement(arr, 2)) + def get_pairs(arr): pairs = [] for i in range(len(arr)): @@ -22,17 +19,20 @@ def get_pairs(arr): if i <= j: pairs.append((arr[i], arr[j])) return pairs + + def compute_scores_sym_matrix(scores_file, entries_file): - scores_file=open(scores_file) - scores_entries = scores_file.read().splitlines() - entries_file = open(entries_file).read().splitlines() - entry_labels = [s.split(' ')[0] for s in entries_file] - - #Derive dimension of the score matrix from the list of points + with open(scores_file) as scores_f: + scores_entries = scores_f.read().splitlines() + with open(entries_file) as entries_f: + entries_lines = entries_f.read().splitlines() + entry_labels = [s.split(" ")[0] for s in entries_lines] + + # Derive dimension of the score matrix from the list of points dim = len(entry_labels) - #Read list of elements and compute pairs + # Read list of elements and compute pairs pairs_entries = get_pairs_fast(entry_labels) - + axes_labels = [] for label in entry_labels: axes_labels.append(label) @@ -40,22 +40,20 @@ def compute_scores_sym_matrix(scores_file, entries_file): scores = [] for j in pairs_entries: - j_inv = (j[1],j[0]) + j_inv = (j[1], j[0]) for line in scores_entries: p = line.split() - pair_score = (p[0],p[1]) - score = p[2] - if j==pair_score or j_inv==pair_score: + pair_score = (p[0], p[1]) + score = p[2] + if j == pair_score or j_inv == pair_score: scores.append(score) sym_matrix = np.zeros((dim, dim)) row, col = np.triu_indices(dim) # Upper triangular indices sym_matrix[row, col] = scores sym_matrix[col, row] = scores - + return sym_matrix, axes_labels -import numpy as np -import itertools def compute_partial_scores_matrix_combined( scores_file, @@ -63,7 +61,7 @@ def compute_partial_scores_matrix_combined( volumes_file=None, fill_value=0.0, w_vol=0.15, - normalize_spec="max", # "max", "p95", or None + normalize_spec="max", # "max", "p95", or None ): # Read entry labels with open(entries_file) as f: @@ -112,9 +110,7 @@ def compute_partial_scores_matrix_combined( spec_scale = 1.0 else: - raise ValueError( - "normalize_spec must be 'max', 'p95', or None" - ) + raise ValueError("normalize_spec must be 'max', 'p95', or None") if spec_scale == 0: spec_scale = 1.0 @@ -137,7 +133,6 @@ def compute_partial_scores_matrix_combined( # Fill matrix for p1, p2 in itertools.combinations_with_replacement(entry_labels, 2): - if (p1, p2) not in scores_dict: continue @@ -148,7 +143,6 @@ def compute_partial_scores_matrix_combined( # Optional volume distance if volumes_file is not None: - if p1 not in vol_dict or p2 not in vol_dict: raise KeyError(f"Missing volume for {p1} or {p2}") @@ -163,9 +157,7 @@ def compute_partial_scores_matrix_combined( vol_dist = abs(v1 - v2) / denom # Combined dissimilarity - combined_dist = ( - spec_dist*(1+w_vol * vol_dist) - ) + combined_dist = spec_dist * (1 + w_vol * vol_dist) else: combined_dist = spec_dist @@ -175,10 +167,11 @@ def compute_partial_scores_matrix_combined( return sym_matrix, entry_labels + def compute_scores_sym_matrix_fast(scores_file, entries_file): - + entries_file = open(entries_file).read().splitlines() - entry_labels = [s.split(' ')[0] for s in entries_file] + entry_labels = [s.split(" ")[0] for s in entries_file] dim = len(entry_labels) label_to_idx = {label: idx for idx, label in enumerate(entry_labels)} @@ -199,7 +192,7 @@ def compute_scores_sym_matrix_fast(scores_file, entries_file): pairs_entries = get_pairs(entry_labels) # Fill the matrix using pairs - for (p1, p2) in pairs_entries: + for p1, p2 in pairs_entries: if (p1, p2) in scores_dict: i = label_to_idx[p1] j = label_to_idx[p2] @@ -209,13 +202,14 @@ def compute_scores_sym_matrix_fast(scores_file, entries_file): # Optional: Handle missing pairs if needed (e.g., assign zero or NaN) pass - return sym_matrix, entry_labels + return sym_matrix, entry_labels + def compute_partial_scores_matrix_fast(scores_file, entries_file, fill_value=0.0): # Read entry labels (subset allowed) with open(entries_file) as f: entry_labels = [line.split()[0] for line in f] - + dim = len(entry_labels) label_to_idx = {label: idx for idx, label in enumerate(entry_labels)} @@ -245,6 +239,7 @@ def compute_partial_scores_matrix_fast(scores_file, entries_file, fill_value=0.0 return sym_matrix, entry_labels + def linkage_matrix(model): # Create linkage matrix and then plot the dendrogram @@ -266,9 +261,10 @@ def linkage_matrix(model): return linkage_matrix # Plot the corresponding dendrogram - #dendrogram(linkage_matrix, **kwargs) + # dendrogram(linkage_matrix, **kwargs) -def two_gap_diff_stat(model, max_k,dist): + +def two_gap_diff_stat(model, max_k, dist): clusters = linkage_matrix(model) dist = pd.DataFrame(dist) # cluster levels over from 1 to N-1 clusters @@ -287,8 +283,7 @@ def two_gap_diff_stat(model, max_k,dist): for i in range(np.max(level.unique()) + 1): cluster = level.loc[level == i] # Based on correlation distance - cluster_dist = dist.loc[cluster.index, - cluster.index] # get distance + cluster_dist = dist.loc[cluster.index, cluster.index] # get distance cluster_pdist = squareform(cluster_dist, checks=False) if cluster_pdist.shape[0] != 0: D = np.nan_to_num(cluster_pdist.mean()) @@ -310,7 +305,7 @@ def two_gap_diff_stat(model, max_k,dist): return k -def std_silhouette_score(model, max_k,dist): +def std_silhouette_score(model, max_k, dist): clusters = linkage_matrix(model) dist = pd.DataFrame(dist) # cluster levels over from 1 to N-1 clusters @@ -339,39 +334,46 @@ def std_silhouette_score(model, max_k,dist): return k -def find_optimal_num_clusters(model, dist, max_k=10, ktype="d",k=None): +def find_optimal_num_clusters(model, dist, max_k=10, ktype="d", k=None): if k is None: if ktype == "s": - k = std_silhouette_score(model, max_k,dist) + k = std_silhouette_score(model, max_k, dist) else: - k = two_gap_diff_stat(model, max_k,dist) + k = two_gap_diff_stat(model, max_k, dist) return k -def compute_clusters(sym_matrix,axes_labels,cluster,linkage_method="ward", threshold=None,no_clusters=None): + +def compute_clusters( + sym_matrix, + axes_labels, + cluster, + linkage_method="ward", + threshold=None, + no_clusters=None, +): if linkage_method == "average": average_linkage_average = cluster.AgglomerativeClustering( - linkage=linkage_method, - metric = 'precomputed', - compute_distances = True, - compute_full_tree = True, - distance_threshold = threshold, - n_clusters= no_clusters, - ) + linkage=linkage_method, + metric="precomputed", + compute_distances=True, + compute_full_tree=True, + distance_threshold=threshold, + n_clusters=no_clusters, + ) if linkage_method == "ward": average_linkage_average = cluster.AgglomerativeClustering( - linkage='ward', - compute_distances = True, - compute_full_tree = True, - distance_threshold = threshold, - n_clusters= no_clusters, + linkage="ward", + compute_distances=True, + compute_full_tree=True, + distance_threshold=threshold, + n_clusters=no_clusters, ) - clustering_av = average_linkage_average.fit(sym_matrix) k = clustering_av.n_clusters_ - threshold_dist = average_linkage_average.distances_[-(k-1)] - + threshold_dist = average_linkage_average.distances_[-(k - 1)] + k = clustering_av.n_clusters_ link_matrix = linkage_matrix(clustering_av) @@ -381,23 +383,24 @@ def compute_clusters(sym_matrix,axes_labels,cluster,linkage_method="ward", thres clusters[v].append(i) clusters_all = [] - for i in range(1,len(clusters)+1): + for i in range(1, len(clusters) + 1): cluster_entries = [] cluster = clusters[i] for j in cluster: - cluster_entries.append(axes_labels[j]) + cluster_entries.append(axes_labels[j]) clusters_all.append(cluster_entries) - - return clusters_all,k,link_matrix,threshold_dist + + return clusters_all, k, link_matrix, threshold_dist + def compute_volumes_pockets(volumes_file, entries_cluster): volumes_file = open(volumes_file) volumes_pockets = volumes_file.read().splitlines() entry_labels = entries_cluster - - #Derive dimension of the score matrix from the list of points - dim = len(entry_labels) - + + # Derive dimension of the score matrix from the list of points + # dim = len(entry_labels) + axes_labels = [] for label in entry_labels: axes_labels.append(label) @@ -408,8 +411,8 @@ def compute_volumes_pockets(volumes_file, entries_cluster): for line in volumes_pockets: p = line.split() pocket_entry = p[0] - pocket_volume = p[1] - if j==pocket_entry : - vol_pockets.append([pocket_entry,pocket_volume]) + pocket_volume = p[1] + if j == pocket_entry: + vol_pockets.append([pocket_entry, pocket_volume]) - return vol_pockets \ No newline at end of file + return vol_pockets diff --git a/analysis_tools/superposition_shapes.py b/analysis_tools/superposition_shapes.py index 0184cac..cf9ee11 100644 --- a/analysis_tools/superposition_shapes.py +++ b/analysis_tools/superposition_shapes.py @@ -1,31 +1,33 @@ import argparse import logging -import os -import csv import numpy as np from numpy import linalg from typing import Tuple import trimesh -import torch from pyFM import mesh logger = logging.getLogger(__name__) -def read_vertices(file_mesh1,file_mesh2): + +def read_vertices(file_mesh1, file_mesh2): mesh1 = trimesh.load_mesh(file_mesh1) mesh2 = trimesh.load_mesh(file_mesh2) # Extract vertex coordinates vertices_s1 = np.array(mesh1.vertices) vertices_s2 = np.array(mesh2.vertices) return vertices_s1, vertices_s2 -def optimal_rotation_translation(A: np.ndarray, B: np.ndarray, allow_mirror: bool=False, weights: np.ndarray=None) -> Tuple[np.ndarray, np.ndarray]: - ''' A, B - matrices 3*n, weights - vector n, result - (matrix 3*3, matrix 3*1) + + +def optimal_rotation_translation( + A: np.ndarray, B: np.ndarray, allow_mirror: bool = False, weights: np.ndarray = None +) -> Tuple[np.ndarray, np.ndarray]: + """A, B - matrices 3*n, weights - vector n, result - (matrix 3*3, matrix 3*1) Find the optimal rotation matrix R and translation vector t for 3D superimposition of A onto B, where columns of A, B are coordinates of corresponding points. If allow_mirror == True, allow also improper rotation (i.e. mirroring + rotation). A_superimposed = R * A + t - ''' + """ if weights is not None: sumW = weights.sum() cA = (A * weights).sum(axis=1, keepdims=True) / sumW @@ -33,43 +35,47 @@ def optimal_rotation_translation(A: np.ndarray, B: np.ndarray, allow_mirror: boo else: cA = np.mean(A, axis=1, keepdims=True) cB = np.mean(B, axis=1, keepdims=True) - + R = optimal_rotation(A - cA, B - cB, allow_mirror=allow_mirror, weights=weights) t = np.matmul(R, -cA) + cB - return R,t -def optimal_rotation(A: np.ndarray, B: np.ndarray, allow_mirror: bool=False, weights: np.ndarray=None) -> np.ndarray: - ''' A, B - matrices 3*n, weights - vector n, result - matrix 3*3 + return R, t + + +def optimal_rotation( + A: np.ndarray, B: np.ndarray, allow_mirror: bool = False, weights: np.ndarray = None +) -> np.ndarray: + """A, B - matrices 3*n, weights - vector n, result - matrix 3*3 Find the optimal rotation matrix for 3D superimposition of A onto B, where columns of A, B are coordinates of corresponding points. If allow_mirror == True, allow also improper rotation (i.e. mirroring + rotation). - ''' + """ - if weights is not None: A = A * weights.reshape((1, -1)) H = A @ B.transpose() U, S, Vh = linalg.svd(H) R = (U @ Vh).transpose() if not allow_mirror and np.linalg.det(R) < 0: # type: ignore # mypy doesn't know .det - Vh[-1,:] = -Vh[-1,:] + Vh[-1, :] = -Vh[-1, :] R = (U @ Vh).transpose() return R -def calculate_rotation_translation(mesh1,mesh2,map_p2p): - #read point to point correspondances file and save it into a list + +def calculate_rotation_translation(mesh1, mesh2, map_p2p): + # read point to point correspondances file and save it into a list file_p2p21 = map_p2p with open(file_p2p21) as csvfile: - p2p21 = csvfile.read().splitlines() - p2p21 = np.asarray(p2p21, dtype=int) - list_p2p=p2p21 + p2p21 = csvfile.read().splitlines() + p2p21 = np.asarray(p2p21, dtype=int) + list_p2p = p2p21 + + # Get lists of vertices for surface 1 and surface 2 using TriMesh from pyFM + vertices_1, vertices_2 = read_vertices(mesh1, mesh2) - #Get lists of vertices for surface 1 and surface 2 using TriMesh from pyFM - vertices_1, vertices_2 = read_vertices(mesh1,mesh2) - if len(vertices_2) != len(list_p2p): logging.error( - "something went wrong, the number of correspondaces should match the number of vertices in surface 2 " - ) + "something went wrong, the number of correspondaces should match the number of vertices in surface 2 " + ) matrix_A = [] matrix_B = vertices_2 @@ -78,13 +84,14 @@ def calculate_rotation_translation(mesh1,mesh2,map_p2p): point_A = vertices_1[i] matrix_A.append(point_A) matrix_A = np.array(matrix_A) - + matrix_A = np.transpose(matrix_A) matrix_B = np.transpose(matrix_B) - R,t = optimal_rotation_translation(matrix_A,matrix_B) - - return R,t + R, t = optimal_rotation_translation(matrix_A, matrix_B) + + return R, t + def calculate_rotation_translation_fixed(mesh1, mesh2, map_p2p): @@ -94,7 +101,9 @@ def calculate_rotation_translation_fixed(mesh1, mesh2, map_p2p): vertices_1, vertices_2 = read_vertices(mesh1, mesh2) if len(vertices_2) != len(list_p2p): - raise ValueError("Number of correspondences must equal number of vertices in mesh2") + raise ValueError( + "Number of correspondences must equal number of vertices in mesh2" + ) # Build correspondence matrices A = vertices_2 # TARGET points @@ -110,8 +119,9 @@ def calculate_rotation_translation_fixed(mesh1, mesh2, map_p2p): return R, t - -def compute_aligned_meshes(file_mesh1, file_mesh2, R, area_normalize=True, center=False, use_transpose=True): +def compute_aligned_meshes( + file_mesh1, file_mesh2, R, area_normalize=True, center=False, use_transpose=True +): """ Load meshes using mesh.TriMesh, align mesh2 to mesh1 using rotation R, and return aligned TriMesh objects. @@ -165,6 +175,7 @@ def compute_aligned_meshes(file_mesh1, file_mesh2, R, area_normalize=True, cente return mesh1_aligned, mesh2_aligned + def main(): logging.basicConfig(level=logging.INFO, format="%(levelname)s:%(name)s:%(message)s") parser = argparse.ArgumentParser() @@ -186,21 +197,21 @@ def main(): ) args = parser.parse_args() - - #read point to point correspondances file and save it into a list + + # read point to point correspondances file and save it into a list file_p2p21 = args.input_p2p21 with open(file_p2p21) as csvfile: - p2p21 = csvfile.read().splitlines() - p2p21 = np.asarray(p2p21, dtype=int) - list_p2p=p2p21 + p2p21 = csvfile.read().splitlines() + p2p21 = np.asarray(p2p21, dtype=int) + list_p2p = p2p21 + + # Get lists of vertices for surface 1 and surface 2 using TriMesh from pyFM + vertices_1, vertices_2 = read_vertices(args.input_mesh1, args.input_mesh2) - #Get lists of vertices for surface 1 and surface 2 using TriMesh from pyFM - vertices_1, vertices_2 = read_vertices(args.input_mesh1,args.input_mesh2) - if len(vertices_2) != len(list_p2p): logging.error( - "something went wrong, the number of correspondaces should match the number of vertices in surface 2 " - ) + "something went wrong, the number of correspondaces should match the number of vertices in surface 2 " + ) matrix_A = [] matrix_B = vertices_2 @@ -209,14 +220,14 @@ def main(): point_A = vertices_1[i] matrix_A.append(point_A) matrix_A = np.array(matrix_A) - + matrix_A = np.transpose(matrix_A) matrix_B = np.transpose(matrix_B) - R,t = optimal_rotation_translation(matrix_A,matrix_B) + R, t = optimal_rotation_translation(matrix_A, matrix_B) logger.info("Translation vector: %s", t) logger.info("Rotation matrix: %s", R) -if __name__ == "__main__": +if __name__ == "__main__": main() diff --git a/dev-requirements.txt b/dev-requirements.txt deleted file mode 100644 index 4e445d0..0000000 --- a/dev-requirements.txt +++ /dev/null @@ -1,12 +0,0 @@ -attrs==20.3.0 -coverage==5.5 -iniconfig==1.1.1 -packaging==20.9 -pluggy==0.13.1 -py==1.10.0 -pyparsing==2.4.7 -pytest-cov==2.11.1 -pytest-mock==3.5.1 -pytest==9.0.3 -toml==0.10.2 -pre-commit==2.11.1 diff --git a/example_data/clustering_data/complexes/Complexes_shape_clustering.html b/example_data/clustering_data/complexes/Complexes_shape_clustering.html index ae8d810..0dc2e04 100644 --- a/example_data/clustering_data/complexes/Complexes_shape_clustering.html +++ b/example_data/clustering_data/complexes/Complexes_shape_clustering.html @@ -1 +1 @@ -

Benchmarking Results for Shape-Based Similarity Analysis on Complexes

Grisell Díaz Leines & Sameer Velankar

Protein Data Bank in Europe, European Molecular Biology Laboratory, European Bioinformatics Institute (EMBL- EBI), Wellcome Genome Campus, Hinxton, Cambridge CB10 1SD, United Kingdom

SARS-CoV-2 Spike complex

Benchmarking set

The benchmarking set for the SARS-CoV-2 post-fusion S2 Spike complex (PDB-CPX-140202) focuses on the trimeric spike glycoprotein of severe acute respiratory syndrome coronavirus 2. The full PDBe complex entry (https://www.ebi.ac.uk/pdbe/pdbe-kb/complexes/PDB-CPX-140202) includes over 400 structural assemblies, representing a wide range of conformational states and binding contexts. For this preliminary analysis, we filtered assemblies to retain only those with titles containing “RBD” and a resolution of 3 Å or better. This selection is intended to capture high-quality structures that reflect the biologically relevant open (RBD-Up) and closed (RBD-Down) conformations of the spike complex. The resulting benchmark set consists of approximately 50 assemblies, providing a manageable yet representative dataset for early-stage comparison and evaluation.

Clustering

Figure 1. Hierarchical clustering of SARS-CoV-2 Spike protein conformations.

(Left) Global and local similarity scores were computed between pairs of entries using 3D Zernike and spectral descriptors. Scores range from 0 (highly similar) to 1 (highly dissimilar).

(Right) Dendrogram with representative molecular surfaces illustrating the two major conformational clusters: 3-RBD-Down (red) and 3-RBD-Up (blue).

The two main clusters were successfully identified using a similarity score defined as a combination of global (Zernike) and local (spectral) descriptors. There are two false positives for 3-RBD-Up  (9by0 and 9cxe) identified with the global/local similarity score.

Local similarity for different conformations

Spectral descriptors enable the detection of local structural similarity between different conformations of the Spike protein. Despite large global rearrangements, these descriptors identify conserved local regions correspondences. The figure illustrates a point-to-point correspondence between two distinct conformations, revealing partial structural similarity despite flexibility.

Figure 2. Functional map and local point-to-point correspondence between the 3RBD-up and 3RBD-down conformations of the SARS-CoV-2 spike complex.

(Left) Functional map (correspondence matrix) between PDB entries 7V7F (3RBD-up) and 7V7D (3RBD-down). The strong diagonal signal indicates regions of high local similarity despite the conformational transition. (Right) Point-to-point correspondence between the molecular surfaces of 7V7F and 7V7D. For clarity, only 80 correspondence lines are shown. Circles highlight regions where local similarity is preserved across conformations, illustrating that spectral descriptors can detect partial structural correspondence even under large conformational changes.

Bacterial ribosome

Benchmarking Set

The bacterial ribosome from Thermus thermophilus is included as a benchmarking case study to evaluate the performance of our method on large ribonucleoprotein (RNP) complexes. The ribosome forms a ~50-subunit oligomeric assembly, representing a substantially more complex and heterogeneous system than protein-only complexes. The benchmarking dataset is based on a manually curated collection of ribosome structures spanning distinct functional states. These structures capture four primary functional states, classical pre-translocation, hybrid, chimeric hybrid, and classical post-translocation, which are characterized by different tRNA configurations and pronounced large-scale conformational changes, including intersubunit rotation, small subunit head rotation, and L1 stalk movement. This dataset provides a stringent test of the ability of shape-based and spectral methods to identify global and local similarity in the presence of extensive flexibility and compositional complexity.

The annotated functional states in the benchmarking dataset are shown in Table 1.

Classical

Hybrid

Chimeric hybrid

4V51_1

4V8O_1

4V9K_1

4V6F_2

4V90_1

4V9L_1

4V7L_2

4V9H_1

4V9M_1

4V8U_1

4V9J_1

4V63_1

4V5F_1

4V67_2

Table 1. PDB entries and assembly identifiers corresponding to the annotated functional states in the benchmarking dataset.

Clustering

Figure 4. Hierarchical clustering of Bacterial ribosome conformations. (Left) Global and local similarity scores were computed between pairs of entries using 3D Zernike and spectral descriptors. Scores range from 0 (highly similar) to 1 (highly dissimilar).(Right) Dendrogram with representative molecular surfaces (colored by entity identifier) illustrating 4 major conformational clusters: Classical Pre- and post-translocation, Chimeric and Hybrid groups.

We compute a similarity score that integrates global shape information (3D Zernike descriptors) with local geometric features (spectral descriptors). Hierarchical clustering using the Ward linkage in Fig. 2 reveals a clear separation between chimeric hybrid and classical pre- and post-translocation conformations based on both global and local features. In contrast, the classical post-translocation state exhibits local similarity to hybrid structures and therefore clusters appear in proximity to the hybrid conformations in the dendrogram.

A cutoff was chosen to allow a maximum of four clusters, however, defining an a priori threshold to clearly distinguish between classical post-translocation and hybrid state remains challenging due to their shared local structural features.

Clustering analysis using OPTICS algorithm:

To further validate the clustering obtained from surface descriptors, we applied the OPTICS algorithm, which does not require a predefined cutoff. This analysis consistently identified five clusters and an isolated structure 4V8O, supporting the robustness of the observed conformational clustering using surface descriptors.

Figure 5. OPTICS clustering of the score matrix shown in a 2D MDS embedding. Noise points are shown in gray. Clusters reflect the density structure in the original distance space. The embedding is for visualization only.

List of clusters obtained with OPTICS:

Cluster 0 : 4V51, 4V7L (Classical pre-translocation)

Cluster 1 : 4V63, 4V67, 4V6F (Classical pre-translocation)

Cluster 2: 4V5F,4V8U (Classical post-translocation )

Cluster 3: 4V90,4V9H (Hybrid)

Cluster 4: 4V9J, 4V9K, 4V9L, 4V9M (Chimeric)

Cluster X: 4V8O  (Hybrid, considered noise as it cannot be classified in a cluster of at least 2 elements)

Hemoglobin HbA complex

The benchmarking set for human hemoglobin focuses on its tetrameric assembly (α₂β₂) derived from Homo sapiens. The PDBe complex entry (PDB-CPX-154652) includes over 300 structural assemblies, representing various functional and intermediate states. Hemoglobin is known to undergo cooperative conformational changes between two primary states—the relaxed (R) state and the tense (T) state, as well as several intermediate conformations.

To obtain a manageable and functionally relevant subset for benchmarking, assemblies were filtered using the following criteria:

This filtering approach is expected to yield approximately 30 assemblies, providing a representative dataset for assessing local and global similarity across functional states.

Reference: PDBe complex entry: https://www.ebi.ac.uk/pdbe/pdbe-kb/complexes/PDB-CPX-154652

Clustering

Figure 6. Hierarchical clustering of protein conformations for Hemoglobin HbA complex.

(Left) Global and local similarity scores computed between pairs of entries using 3D Zernike and spectral descriptors. Scores range from 0 (highly similar) to 1 (highly dissimilar).

(Right) Dendrogram with representative molecular surfaces illustrating the two major conformational clusters: T-state (red) and R-State (blue).

We successfully distinguish the T-state and R-state conformations using a combination of spectral and 3D Zernike descriptors. The resulting similarity scores show no false-positive similarities between structures annotated as different functional states within the benchmarking dataset.

Protein interfaces

Ubiquitin serves as an illustrative example of a protein chain that participates in multiple, distinct protein–protein complexes while maintaining a conserved interaction surface. In the three PDB entries considered here (2JY6, 2DEN), ubiquitin adopts a highly similar global fold and engages binding partners through the canonical hydrophobic interaction patch centered on Ile44.

In PDB entries 2JY6 and 2DEN, ubiquitin binds to UBA (ubiquitin-associated) domains from different proteins. Although these UBA domains originate from distinct proteins, they share a common fold and recognize ubiquitin in a similar binding mode, resulting in comparable local interface geometries. These interactions exemplify conserved ubiquitin recognition by structurally related binding domains.

ross structurally diverse proteins.

Fig. 1 | Local similarity between two ubiquitin-binding protein partners. (Top) Protein complexes showing ubiquitin bound to chain A in complex 2DEN and to chain B in complex 2JY6. (Bottom) Local shape correspondence between the ubiquitin-binding partners (chains 2DEN-A and 2JY6-B), highlighting a high degree of partial structural similarity in the interface regions that engage ubiquitin. For clarity,  80 correspondence lines are shown.

Local spectral descriptors enable the identification of  interaction patterns between ubiquitin-binding proteins even when their global folds differ. In the example shown, ubiquitin interacts with two partner proteins (2DEN-A and 2JY6-B)  that have distinct overall structures, yet the regions contacting ubiquitin exhibit high local geometric similarity. By focusing on local surface patches rather than full-chain alignments, the descriptors capture shared shape and interface features in the ubiquitin-binding regions that are not apparent from global structural comparisons. This demonstrates how local spectral descriptors can reveal interface-level and binding-site similarity across structurally different proteins, highlighting similarities that are functionally relevant but globally masked.

[1]


[1] These results are preliminary and are not intended for redistribution. For questions or reuse requests, please get in touch with Grisell Díaz Leines (gdiazleines@ebi.ac.uk) and Sameer Velankar (sameer@ebi.ac.uk).

\ No newline at end of file +

Benchmarking Results for Shape-Based Similarity Analysis on Complexes

Grisell Díaz Leines & Sameer Velankar

Protein Data Bank in Europe, European Molecular Biology Laboratory, European Bioinformatics Institute (EMBL- EBI), Wellcome Genome Campus, Hinxton, Cambridge CB10 1SD, United Kingdom

SARS-CoV-2 Spike complex

Benchmarking set

The benchmarking set for the SARS-CoV-2 post-fusion S2 Spike complex (PDB-CPX-140202) focuses on the trimeric spike glycoprotein of severe acute respiratory syndrome coronavirus 2. The full PDBe complex entry (https://www.ebi.ac.uk/pdbe/pdbe-kb/complexes/PDB-CPX-140202) includes over 400 structural assemblies, representing a wide range of conformational states and binding contexts. For this preliminary analysis, we filtered assemblies to retain only those with titles containing “RBD” and a resolution of 3 Å or better. This selection is intended to capture high-quality structures that reflect the biologically relevant open (RBD-Up) and closed (RBD-Down) conformations of the spike complex. The resulting benchmark set consists of approximately 50 assemblies, providing a manageable yet representative dataset for early-stage comparison and evaluation.

Clustering

Figure 1. Hierarchical clustering of SARS-CoV-2 Spike protein conformations.

(Left) Global and local similarity scores were computed between pairs of entries using 3D Zernike and spectral descriptors. Scores range from 0 (highly similar) to 1 (highly dissimilar).

(Right) Dendrogram with representative molecular surfaces illustrating the two major conformational clusters: 3-RBD-Down (red) and 3-RBD-Up (blue).

The two main clusters were successfully identified using a similarity score defined as a combination of global (Zernike) and local (spectral) descriptors. There are two false positives for 3-RBD-Up  (9by0 and 9cxe) identified with the global/local similarity score.

Local similarity for different conformations

Spectral descriptors enable the detection of local structural similarity between different conformations of the Spike protein. Despite large global rearrangements, these descriptors identify conserved local regions correspondences. The figure illustrates a point-to-point correspondence between two distinct conformations, revealing partial structural similarity despite flexibility.

Figure 2. Functional map and local point-to-point correspondence between the 3RBD-up and 3RBD-down conformations of the SARS-CoV-2 spike complex.

(Left) Functional map (correspondence matrix) between PDB entries 7V7F (3RBD-up) and 7V7D (3RBD-down). The strong diagonal signal indicates regions of high local similarity despite the conformational transition. (Right) Point-to-point correspondence between the molecular surfaces of 7V7F and 7V7D. For clarity, only 80 correspondence lines are shown. Circles highlight regions where local similarity is preserved across conformations, illustrating that spectral descriptors can detect partial structural correspondence even under large conformational changes.

Bacterial ribosome

Benchmarking Set

The bacterial ribosome from Thermus thermophilus is included as a benchmarking case study to evaluate the performance of our method on large ribonucleoprotein (RNP) complexes. The ribosome forms a ~50-subunit oligomeric assembly, representing a substantially more complex and heterogeneous system than protein-only complexes. The benchmarking dataset is based on a manually curated collection of ribosome structures spanning distinct functional states. These structures capture four primary functional states, classical pre-translocation, hybrid, chimeric hybrid, and classical post-translocation, which are characterized by different tRNA configurations and pronounced large-scale conformational changes, including intersubunit rotation, small subunit head rotation, and L1 stalk movement. This dataset provides a stringent test of the ability of shape-based and spectral methods to identify global and local similarity in the presence of extensive flexibility and compositional complexity.

The annotated functional states in the benchmarking dataset are shown in Table 1.

Classical

Hybrid

Chimeric hybrid

4V51_1

4V8O_1

4V9K_1

4V6F_2

4V90_1

4V9L_1

4V7L_2

4V9H_1

4V9M_1

4V8U_1

4V9J_1

4V63_1

4V5F_1

4V67_2

Table 1. PDB entries and assembly identifiers corresponding to the annotated functional states in the benchmarking dataset.

Clustering

Figure 4. Hierarchical clustering of Bacterial ribosome conformations. (Left) Global and local similarity scores were computed between pairs of entries using 3D Zernike and spectral descriptors. Scores range from 0 (highly similar) to 1 (highly dissimilar).(Right) Dendrogram with representative molecular surfaces (colored by entity identifier) illustrating 4 major conformational clusters: Classical Pre- and post-translocation, Chimeric and Hybrid groups.

We compute a similarity score that integrates global shape information (3D Zernike descriptors) with local geometric features (spectral descriptors). Hierarchical clustering using the Ward linkage in Fig. 2 reveals a clear separation between chimeric hybrid and classical pre- and post-translocation conformations based on both global and local features. In contrast, the classical post-translocation state exhibits local similarity to hybrid structures and therefore clusters appear in proximity to the hybrid conformations in the dendrogram.

A cutoff was chosen to allow a maximum of four clusters, however, defining an a priori threshold to clearly distinguish between classical post-translocation and hybrid state remains challenging due to their shared local structural features.

Clustering analysis using OPTICS algorithm:

To further validate the clustering obtained from surface descriptors, we applied the OPTICS algorithm, which does not require a predefined cutoff. This analysis consistently identified five clusters and an isolated structure 4V8O, supporting the robustness of the observed conformational clustering using surface descriptors.

Figure 5. OPTICS clustering of the score matrix shown in a 2D MDS embedding. Noise points are shown in gray. Clusters reflect the density structure in the original distance space. The embedding is for visualization only.

List of clusters obtained with OPTICS:

Cluster 0 : 4V51, 4V7L (Classical pre-translocation)

Cluster 1 : 4V63, 4V67, 4V6F (Classical pre-translocation)

Cluster 2: 4V5F,4V8U (Classical post-translocation )

Cluster 3: 4V90,4V9H (Hybrid)

Cluster 4: 4V9J, 4V9K, 4V9L, 4V9M (Chimeric)

Cluster X: 4V8O  (Hybrid, considered noise as it cannot be classified in a cluster of at least 2 elements)

Hemoglobin HbA complex

The benchmarking set for human hemoglobin focuses on its tetrameric assembly (α₂β₂) derived from Homo sapiens. The PDBe complex entry (PDB-CPX-154652) includes over 300 structural assemblies, representing various functional and intermediate states. Hemoglobin is known to undergo cooperative conformational changes between two primary states—the relaxed (R) state and the tense (T) state, as well as several intermediate conformations.

To obtain a manageable and functionally relevant subset for benchmarking, assemblies were filtered using the following criteria:

This filtering approach is expected to yield approximately 30 assemblies, providing a representative dataset for assessing local and global similarity across functional states.

Reference: PDBe complex entry: https://www.ebi.ac.uk/pdbe/pdbe-kb/complexes/PDB-CPX-154652

Clustering

Figure 6. Hierarchical clustering of protein conformations for Hemoglobin HbA complex.

(Left) Global and local similarity scores computed between pairs of entries using 3D Zernike and spectral descriptors. Scores range from 0 (highly similar) to 1 (highly dissimilar).

(Right) Dendrogram with representative molecular surfaces illustrating the two major conformational clusters: T-state (red) and R-State (blue).

We successfully distinguish the T-state and R-state conformations using a combination of spectral and 3D Zernike descriptors. The resulting similarity scores show no false-positive similarities between structures annotated as different functional states within the benchmarking dataset.

Protein interfaces

Ubiquitin serves as an illustrative example of a protein chain that participates in multiple, distinct protein–protein complexes while maintaining a conserved interaction surface. In the three PDB entries considered here (2JY6, 2DEN), ubiquitin adopts a highly similar global fold and engages binding partners through the canonical hydrophobic interaction patch centered on Ile44.

In PDB entries 2JY6 and 2DEN, ubiquitin binds to UBA (ubiquitin-associated) domains from different proteins. Although these UBA domains originate from distinct proteins, they share a common fold and recognize ubiquitin in a similar binding mode, resulting in comparable local interface geometries. These interactions exemplify conserved ubiquitin recognition by structurally related binding domains.

ross structurally diverse proteins.

Fig. 1 | Local similarity between two ubiquitin-binding protein partners. (Top) Protein complexes showing ubiquitin bound to chain A in complex 2DEN and to chain B in complex 2JY6. (Bottom) Local shape correspondence between the ubiquitin-binding partners (chains 2DEN-A and 2JY6-B), highlighting a high degree of partial structural similarity in the interface regions that engage ubiquitin. For clarity,  80 correspondence lines are shown.

Local spectral descriptors enable the identification of  interaction patterns between ubiquitin-binding proteins even when their global folds differ. In the example shown, ubiquitin interacts with two partner proteins (2DEN-A and 2JY6-B)  that have distinct overall structures, yet the regions contacting ubiquitin exhibit high local geometric similarity. By focusing on local surface patches rather than full-chain alignments, the descriptors capture shared shape and interface features in the ubiquitin-binding regions that are not apparent from global structural comparisons. This demonstrates how local spectral descriptors can reveal interface-level and binding-site similarity across structurally different proteins, highlighting similarities that are functionally relevant but globally masked.

[1]


[1] These results are preliminary and are not intended for redistribution. For questions or reuse requests, please get in touch with Grisell Díaz Leines (gdiazleines@ebi.ac.uk) and Sameer Velankar (sameer@ebi.ac.uk).

diff --git a/example_data/test.txt b/example_data/test.txt index 2bb98dc..c427dbf 100644 --- a/example_data/test.txt +++ b/example_data/test.txt @@ -240,5 +240,3 @@ 5 0.000 0.000 0.000 0.000 {method 'items' of 'dict' objects} 2 0.000 0.000 0.000 0.000 {method 'fileno' of '_io.BufferedReader' objects} 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.10/3.10.19_1/Frameworks/Python.framework/Versions/3.10/lib/python3.10/argparse.py:1302(__init__) - - diff --git a/example_data/well_conditioned_meshes/6mka-A.obj b/example_data/well_conditioned_meshes/6mka-A.obj index e841e59..cc2a3c0 100644 --- a/example_data/well_conditioned_meshes/6mka-A.obj +++ b/example_data/well_conditioned_meshes/6mka-A.obj @@ -271547,4 +271547,3 @@ f 5 6 4 f 3 4 1 f 5 4 3 f 1 2 3 - diff --git a/pyproject.toml b/pyproject.toml index d4985e3..49c7ae8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,14 +1,38 @@ +[project] +name = "pdbe-shape-retrieval" +description = "This python package calculates surface shape descriptors for macromolecular assemblies and interfaces, and computes shape similarity scores." +authors = [ + {name = "Grisell Diaz Leines", email = "gdiazleines@ebi.ac.uk"}, +] +keywords = ["interfaces", "structural biology", "shape descriptors", "macromolecular assemblies", "interfaces", "similarity scores"] +version = "2.0.0" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "numpy>=2.2.6", + "pandas>=2.3.3", + "pydantic>=2.13.4", + "pyfmaps>=1.0.1", + "pymeshfix>=0.18.1", + "pymeshlab>=2025.7.post1", + "scikit-learn>=1.7.2", + "scipy>=1.15.3", + "torch>=2.12.0", + "trimesh>=4.12.2", +] + +[project.urls] +Repository = "https://github.com/PDBe-KB/pdbe_shape_retrieval.git" + [build-system] -requires = ["setuptools>=68", "wheel"] +requires = ["setuptools"] build-backend = "setuptools.build_meta" -[tool.uv] -package = true +[tool.setuptools.packages.find] +include = ["shape_retrieval*"] -[tool.pytest.ini_options] -testpaths = ["tests"] +[project.scripts] +shape_retrieval = "shape_retrieval.cli:main" -[tool.coverage.run] -omit = [ - "analysis_tools/visualisation_correspondence.py", -] +[dependency-groups] +test = ["pytest", "coverage"] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 94734f0..0000000 --- a/requirements.txt +++ /dev/null @@ -1,20 +0,0 @@ -imageio -pythreejs -ipywidgets -pyfmaps -setuptools -numpy -pymeshlab -trimesh -scipy -pandas -matplotlib -seaborn -tqdm -scikit-learn -potpourri3d -robust_laplacian -pymeshfix -pyglet -torch -pydantic diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index cb2b1d1..0000000 --- a/setup.cfg +++ /dev/null @@ -1,65 +0,0 @@ -[metadata] -name = shape_retrieval -author = Grisell Diaz Leines -author_email = gdiazleines@ebi.ac.uk -description = This python package calculates surface shape descriptors for macromolecular assemblies and interfaces, and computes shape similarity scores. -version = 2.0.0 -url = https://github.com/PDBe-KB/pdbe_shape-retrieval -keywords = - interfaces assemblies pisa json -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Life Science/Structural Biology - Intended Audience :: Developers - Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.11 - Programming Language :: Python :: 3.12 -long_description = file: README.md -long_description_content_type = text/markdown -license_files = - LICENSE - -[options] -python_requires = >= 3.10 -packages = find_namespace: -zip_safe: False -include_package_data = True -install_requires = - imageio - ipywidgets - lxml>=4.6.3,<5.0.0 - matplotlib - numpy - pandas - potpourri3d - pydantic - pyfmaps - pyglet - pymeshlab - pymeshfix - pythreejs - requests - robust_laplacian - scikit-learn - scipy - seaborn - torch - tqdm - trimesh - -[options.extras_require] -test = - pytest - pytest-cov - pytest-mock -dev = - build - pre-commit - pytest - pytest-cov - pytest-mock - twine - -[options.entry_points] -console_scripts = - shape_retrieval = shape_utils.cli:main diff --git a/setup.py b/setup.py deleted file mode 100644 index 6068493..0000000 --- a/setup.py +++ /dev/null @@ -1,3 +0,0 @@ -from setuptools import setup - -setup() diff --git a/shape_clustering.ipynb b/shape_clustering.ipynb index 3dc8cb4..1ae503f 100644 --- a/shape_clustering.ipynb +++ b/shape_clustering.ipynb @@ -7,18 +7,13 @@ "metadata": {}, "outputs": [], "source": [ - "import numpy as np\n", - "\n", "from analysis_tools.clustering import compute_partial_scores_matrix_fast\n", "from analysis_tools.clustering import compute_clusters\n", "from sklearn import cluster\n", "import seaborn as sns\n", "import matplotlib.pyplot as plt\n", - "from scipy.cluster.hierarchy import dendrogram, fcluster,linkage\n", - "from scipy.cluster.hierarchy import set_link_color_palette\n", - "\n", - "\n", - "\n" + "from scipy.cluster.hierarchy import dendrogram\n", + "from scipy.cluster.hierarchy import set_link_color_palette" ] }, { @@ -44,21 +39,21 @@ "metadata": {}, "outputs": [], "source": [ - "#Find example data of scores and entries in /pdb_shape_retrieval/example_data\n", - "#scores file should have three columns, first two colums #1 #2- entries IDs, third column #3 score\n", - "# entries_file - list of entry ids \n", + "# Find example data of scores and entries in /pdb_shape_retrieval/example_data\n", + "# scores file should have three columns, first two colums #1 #2- entries IDs, third column #3 score\n", + "# entries_file - list of entry ids\n", "\n", - "scores_file = '/Users/gdiazleines/results/shape-retrieval/benchmarking/monomer_paper/shape_analysis/pairs_scores.txt'\n", - "entries_file ='/Users/gdiazleines/results/shape-retrieval/benchmarking/monomer_paper/shape_analysis/list_entries.txt'\n", + "scores_file = \"/Users/gdiazleines/results/shape-retrieval/benchmarking/monomer_paper/shape_analysis/pairs_scores.txt\"\n", + "entries_file = \"/Users/gdiazleines/results/shape-retrieval/benchmarking/monomer_paper/shape_analysis/list_entries.txt\"\n", "\n", - "#compute scores matrix\n", + "# compute scores matrix\n", "scores_matrix, labels = compute_partial_scores_matrix_fast(scores_file, entries_file)\n", - "scores_matrix_single=scores_matrix/scores_matrix.max()\n", + "scores_matrix_single = scores_matrix / scores_matrix.max()\n", "\n", "\n", - "#plot scores matrix\n", + "# plot scores matrix\n", "fig, (ax1) = plt.subplots(1, figsize=(4, 4))\n", - "sns.heatmap(scores_matrix_single,ax=ax1,vmin=0.0,cmap ='seismic')\n" + "sns.heatmap(scores_matrix_single, ax=ax1, vmin=0.0, cmap=\"seismic\")" ] }, { @@ -76,7 +71,7 @@ "metadata": {}, "outputs": [], "source": [ - "#Define scores matrix to use: \n", + "# Define scores matrix to use:\n", "\n", "scores_matrix = scores_matrix_single" ] @@ -96,27 +91,36 @@ "metadata": {}, "outputs": [], "source": [ - "#Define a threshold or number of clusters\n", + "# Define a threshold or number of clusters\n", "threshold = None\n", - "n_clusters=2\n", + "n_clusters = 2\n", "\n", - "#define clustering method ('ward' or 'average') and compute clusters\n", - "linkage_method=\"ward\"\n", + "# define clustering method ('ward' or 'average') and compute clusters\n", + "linkage_method = \"ward\"\n", "\n", - "#Compute clusters\n", - "clusters,n_clusters,link_matrix,threshold_dist = compute_clusters(scores_matrix,labels,cluster,linkage_method, threshold, n_clusters)\n", + "# Compute clusters\n", + "clusters, n_clusters, link_matrix, threshold_dist = compute_clusters(\n", + " scores_matrix, labels, cluster, linkage_method, threshold, n_clusters\n", + ")\n", "\n", - "print('optimal number of clusters',n_clusters)\n", - "print('threshold distance',threshold_dist)\n", + "print(\"optimal number of clusters\", n_clusters)\n", + "print(\"threshold distance\", threshold_dist)\n", "\n", - "#set clustering colour palette\n", - "set_link_color_palette(['red', 'blue', 'green', 'orange', 'purple','black'])\n", + "# set clustering colour palette\n", + "set_link_color_palette([\"red\", \"blue\", \"green\", \"orange\", \"purple\", \"black\"])\n", "\n", - "#plot dendogram\n", - "dendrogram(link_matrix,truncate_mode = \"level\", p=50,color_threshold=threshold_dist, labels=labels, above_threshold_color='lightgrey')\n", - "plt.axhline(threshold_dist, color='k', linestyle='--')\n", + "# plot dendogram\n", + "dendrogram(\n", + " link_matrix,\n", + " truncate_mode=\"level\",\n", + " p=50,\n", + " color_threshold=threshold_dist,\n", + " labels=labels,\n", + " above_threshold_color=\"lightgrey\",\n", + ")\n", + "plt.axhline(threshold_dist, color=\"k\", linestyle=\"--\")\n", "plt.xticks(rotation=90) # Rotate labels vertically\n", - "plt.tight_layout() \n", + "plt.tight_layout()\n", "plt.show()" ] } diff --git a/shape_descriptors_FM.ipynb b/shape_descriptors_FM.ipynb index 209cfc1..d6eb6d4 100644 --- a/shape_descriptors_FM.ipynb +++ b/shape_descriptors_FM.ipynb @@ -9,31 +9,30 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "import pyFM\n", "from pyFM import mesh\n", - "from pyFM import functional\n", - "from pyFM import eval\n", "\n", "\n", - "from shape_utils.spectral_descr import calculate_spectral_descriptors\n", - "from shape_utils.functional_maps import calculate_functional_maps, set_FM_model_parameters, calculate_p2p_map\n", + "from shape_retrieval.spectral_descr import calculate_spectral_descriptors\n", + "from shape_retrieval.functional_maps import (\n", + " calculate_functional_maps,\n", + " set_FM_model_parameters,\n", + " calculate_p2p_map,\n", + ")\n", "\n", "\n", "import os\n", "import seaborn as sns\n", "import numpy as np\n", "import meshplot as mp\n", - "from scipy.linalg import norm\n", "import matplotlib.pyplot as plt\n", - "import csv\n", "\n", - "from shape_utils.zernike_descr import get_inv\n", - "from shape_utils.meshes import remove_until_vertex\n", - "from shape_utils.similarity_scores import predict_similarity_zernike\n", + "from shape_retrieval.zernike_descr import get_inv\n", + "from shape_retrieval.meshes import remove_until_vertex\n", + "from shape_retrieval.similarity_scores import predict_similarity_zernike\n", "import trimesh" ] }, @@ -46,20 +45,25 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "def plot_mesh(myMesh,cmap=None):\n", - " mp.plot(myMesh.vertlist, myMesh.facelist,c=cmap)\n", - " \n", - "def double_plot(myMesh1,myMesh2,cmap1=None,cmap2=None):\n", + "def plot_mesh(myMesh, cmap=None):\n", + " mp.plot(myMesh.vertlist, myMesh.facelist, c=cmap)\n", + "\n", + "\n", + "def double_plot(myMesh1, myMesh2, cmap1=None, cmap2=None):\n", " d = mp.subplot(myMesh1.vertlist, myMesh1.facelist, c=cmap1, s=[2, 2, 0])\n", " mp.subplot(myMesh2.vertlist, myMesh2.facelist, c=cmap2, s=[2, 2, 1], data=d)\n", "\n", + "\n", "def visu(vertices):\n", - " min_coord,max_coord = np.min(vertices,axis=0,keepdims=True),np.max(vertices,axis=0,keepdims=True)\n", - " cmap = (vertices-min_coord)/(max_coord-min_coord)\n", + " min_coord, max_coord = (\n", + " np.min(vertices, axis=0, keepdims=True),\n", + " np.max(vertices, axis=0, keepdims=True),\n", + " )\n", + " cmap = (vertices - min_coord) / (max_coord - min_coord)\n", " return cmap" ] }, @@ -76,28 +80,29 @@ "metadata": {}, "outputs": [], "source": [ - "#use this section only if you have the last version of pyFM at PDBe\n", - "file_mesh = './example_data/well_conditioned_meshes/6mka-A.obj'\n", + "# use this section only if you have the last version of pyFM at PDBe\n", + "file_mesh = \"./example_data/well_conditioned_meshes/6mka-A.obj\"\n", "\n", "\n", - "#load mesh with TriMesh and normalized area \n", + "# load mesh with TriMesh and normalized area\n", "mesh = mesh.TriMesh(file_mesh, area_normalize=True, center=False)\n", "\n", "\n", - "#Define crucial parameters for Spectral descriptors and FM computation\n", - "neigvecs = 200 #No. of eigenvectors\n", - "ndescr = 100 #No. of descriptors\n", - "step = 1 #Step for including descriptors\n", - "n_ev = 50 #No. of eigen vectors to define functional maps\n", - "output = 'example_data/spectral_output_data/' #output path for results \n", - "descr = 'WKS' #Type of shape descriptor (WKS- Wavelet Kernel Signatures)\n", - "landmarks = None #Add landmarks if available\n", + "# Define crucial parameters for Spectral descriptors and FM computation\n", + "neigvecs = 200 # No. of eigenvectors\n", + "ndescr = 100 # No. of descriptors\n", + "step = 1 # Step for including descriptors\n", + "n_ev = 50 # No. of eigen vectors to define functional maps\n", + "output = \"example_data/spectral_output_data/\" # output path for results\n", + "descr = \"WKS\" # Type of shape descriptor (WKS- Wavelet Kernel Signatures)\n", + "landmarks = None # Add landmarks if available\n", "\n", "\n", - "#Computation of WKS descriptors \n", - "wksdescr = calculate_spectral_descriptors(mesh,neigvecs,n_ev,ndescr,step,landmarks,descr)\n", - "wksdescr.shape\n", - "\n" + "# Computation of WKS descriptors\n", + "wksdescr = calculate_spectral_descriptors(\n", + " mesh, neigvecs, n_ev, ndescr, step, landmarks, descr\n", + ")\n", + "wksdescr.shape" ] }, { @@ -120,30 +125,30 @@ "metadata": {}, "outputs": [], "source": [ + "file_mesh1 = \"./example_data/well_conditioned_meshes/6mka-A.obj\"\n", + "file_mesh2 = \"./example_data/well_conditioned_meshes/6mkg-A.obj\"\n", "\n", - "file_mesh1 = './example_data/well_conditioned_meshes/6mka-A.obj'\n", - "file_mesh2 = './example_data/well_conditioned_meshes/6mkg-A.obj'\n", "\n", + "# load mesh with TriMesh and normalized area\n", "\n", - "#load mesh with TriMesh and normalized area\n", - " \n", "mesh1 = mesh.TriMesh(file_mesh1, area_normalize=True, center=False)\n", "mesh2 = mesh.TriMesh(file_mesh2, area_normalize=True, center=False)\n", "\n", - "#Define crucial parameters for Spectral descriptors and FM computation\n", - "neigvecs = 200 #No. of eigenvectors\n", - "ndescr = 100 #No. of descriptors\n", - "step = 1 #Step for including descriptors\n", - "n_ev = 50 #No. of eigen vectors to define functional maps\n", - "output = 'example_data/spectral_output_data/' #output path for results \n", - "descr = 'WKS' #Type of shape descriptor (WKS- Wavelet Kernel Signatures)\n", - "landmarks = None #Add landmarks if available\n", + "# Define crucial parameters for Spectral descriptors and FM computation\n", + "neigvecs = 200 # No. of eigenvectors\n", + "ndescr = 100 # No. of descriptors\n", + "step = 1 # Step for including descriptors\n", + "n_ev = 50 # No. of eigen vectors to define functional maps\n", + "output = \"example_data/spectral_output_data/\" # output path for results\n", + "descr = \"WKS\" # Type of shape descriptor (WKS- Wavelet Kernel Signatures)\n", + "landmarks = None # Add landmarks if available\n", "\n", "\n", - "#Computation of point to point maps\n", - "model = set_FM_model_parameters(mesh1,mesh2,neigvecs,n_ev,ndescr,step,landmarks,descr)\n", - "model_FM, FM = calculate_functional_maps(model,n_cpus=8,refine=None)\n", - "\n" + "# Computation of point to point maps\n", + "model = set_FM_model_parameters(\n", + " mesh1, mesh2, neigvecs, n_ev, ndescr, step, landmarks, descr\n", + ")\n", + "model_FM, FM = calculate_functional_maps(model, n_cpus=8, refine=None)" ] }, { @@ -175,11 +180,12 @@ "metadata": {}, "outputs": [], "source": [ - "print(mesh1.vertlist.shape,mesh2.vertlist.shape)\n", - "cmap1 = visu(mesh1.vertlist); cmap2 = cmap1[p2p_21]\n", - "double_plot(mesh1,mesh2,cmap1,cmap2)\n", + "print(mesh1.vertlist.shape, mesh2.vertlist.shape)\n", + "cmap1 = visu(mesh1.vertlist)\n", + "cmap2 = cmap1[p2p_21]\n", + "double_plot(mesh1, mesh2, cmap1, cmap2)\n", "fig, (ax1) = plt.subplots(1, figsize=(3, 3))\n", - "sns.heatmap(FM,ax=ax1,vmax=1.0,vmin=-1.0,cmap='seismic')" + "sns.heatmap(FM, ax=ax1, vmax=1.0, vmin=-1.0, cmap=\"seismic\")" ] }, { @@ -202,24 +208,24 @@ "metadata": {}, "outputs": [], "source": [ - "#input surface meshes\n", - "file_mesh1 = './example_data/well_conditioned_meshes/6mka-A.obj'\n", - "file_mesh2 = './example_data/well_conditioned_meshes/6mkg-A.obj'\n", + "# input surface meshes\n", + "file_mesh1 = \"./example_data/well_conditioned_meshes/6mka-A.obj\"\n", + "file_mesh2 = \"./example_data/well_conditioned_meshes/6mkg-A.obj\"\n", "\n", - "#ouput directory to save the Zernike moments \n", - "output_dir = './example_data/zernike_output_data'\n", + "# ouput directory to save the Zernike moments\n", + "output_dir = \"./example_data/zernike_output_data\"\n", "\n", - "#Entry IDs\n", + "# Entry IDs\n", "entry_id_1 = \"6mka-A\"\n", "entry_id_2 = \"6mkg-A\"\n", "\n", - "#removing extra lines in .obj file (obj2grid has trouble to read extra header lines in .obj files)\n", + "# removing extra lines in .obj file (obj2grid has trouble to read extra header lines in .obj files)\n", "remove_until_vertex(file_mesh1)\n", "remove_until_vertex(file_mesh2)\n", "\n", - "#Computation of Zernike Moments\n", - "get_inv(file_mesh1,entry_id_1,'map2zernike', 'obj2grid',output_dir)\n", - "get_inv(file_mesh1,entry_id_2,'map2zernike', 'obj2grid',output_dir)" + "# Computation of Zernike Moments\n", + "get_inv(file_mesh1, entry_id_1, \"map2zernike\", \"obj2grid\", output_dir)\n", + "get_inv(file_mesh1, entry_id_2, \"map2zernike\", \"obj2grid\", output_dir)" ] }, { @@ -235,31 +241,34 @@ "metadata": {}, "outputs": [], "source": [ + "meshes_dir = (\n", + " \"./example_data/well_conditioned_meshes/\" # directory containing the meshes\n", + ")\n", + "output_moments_dir = \"./example_data/zernike_output_data\" # output directory to save Zernike moments files\n", + "output_scores_dir = (\n", + " \"./example_data/zernike_output_data\" # directory to save scores file\n", + ")\n", + "file_entries = \"./example_data/clustering_data/monomers/list_entries.txt\" # list of entry ids to read files (mesh file named after entry id)\n", "\n", - "meshes_dir = './example_data/well_conditioned_meshes/' #directory containing the meshes\n", - "output_moments_dir = './example_data/zernike_output_data' #output directory to save Zernike moments files\n", - "output_scores_dir = './example_data/zernike_output_data' #directory to save scores file\n", - "file_entries='./example_data/clustering_data/monomers/list_entries.txt' #list of entry ids to read files (mesh file named after entry id)\n", - "\n", - "#read list of entries\n", - "with open(file_entries, 'r') as file:\n", + "# read list of entries\n", + "with open(file_entries, \"r\") as file:\n", " # Read all lines, stripping the newline character\n", " entry_list = [line.strip() for line in file]\n", "print(np.shape(entry_list))\n", "\n", - "#read mesh files \n", + "# read mesh files\n", "for entry in entry_list:\n", - " mesh_file=os.path.join(meshes_dir,\"{}.obj\".format(entry))\n", + " mesh_file = os.path.join(meshes_dir, \"{}.obj\".format(entry))\n", " print(mesh_file)\n", " mesh = trimesh.load(mesh_file, file_type=\"obj\")\n", - " \n", - " #removing extra lines in .obj file (obj2grid has trouble to read extra header lines in .obj files)\n", + "\n", + " # removing extra lines in .obj file (obj2grid has trouble to read extra header lines in .obj files)\n", " remove_until_vertex(mesh_file)\n", - " #compute Zernike moments for each mesh\n", - " get_inv(mesh_file,entry,'map2zernike', 'obj2grid',output_dir)\n", - " \n", - "#compute scores for the list of files and entries and save scores file in output_scores_dir\n", - "predict_similarity_zernike(output_moments_dir,output_scores_dir)" + " # compute Zernike moments for each mesh\n", + " get_inv(mesh_file, entry, \"map2zernike\", \"obj2grid\", output_dir)\n", + "\n", + "# compute scores for the list of files and entries and save scores file in output_scores_dir\n", + "predict_similarity_zernike(output_moments_dir, output_scores_dir)" ] } ], diff --git a/shape_utils/__init__.py b/shape_retrieval/__init__.py similarity index 100% rename from shape_utils/__init__.py rename to shape_retrieval/__init__.py diff --git a/shape_utils/cli.py b/shape_retrieval/cli.py similarity index 93% rename from shape_utils/cli.py rename to shape_retrieval/cli.py index ea6ca6f..da95396 100644 --- a/shape_utils/cli.py +++ b/shape_retrieval/cli.py @@ -10,7 +10,7 @@ from pathlib import Path from typing import Sequence -from shape_utils.pipeline import PipelineConfig, run_pipeline +from shape_retrieval.pipeline import PipelineConfig, run_pipeline logger = logging.getLogger(__name__) @@ -20,7 +20,9 @@ def build_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser(description="Calculate shape retrieval descriptors for protein surfaces.") + parser = argparse.ArgumentParser( + description="Calculate shape retrieval descriptors for protein surfaces." + ) default_cpu_count = multiprocessing.cpu_count() parser.add_argument( @@ -109,7 +111,11 @@ def build_parser() -> argparse.ArgumentParser: default=1, help="Subsample step to avoid using too many spectral descriptors.", ) - parser.add_argument("--landmarks", default=None, help="Input indices of landmarks for spectral descriptors.") + parser.add_argument( + "--landmarks", + default=None, + help="Input indices of landmarks for spectral descriptors.", + ) parser.add_argument( "--n-cpus", type=int, @@ -135,6 +141,7 @@ def build_parser() -> argparse.ArgumentParser: return parser + def config_from_args(args: argparse.Namespace) -> PipelineConfig: return PipelineConfig( mesh1=Path(args.mesh1).expanduser(), @@ -221,7 +228,9 @@ def run_with_profiling(config: PipelineConfig) -> None: stats = pstats.Stats(profile, stream=stream).sort_stats("tottime") stats.print_stats() - output_path = Path(os.environ.get(PROFILE_OUTPUT_ENV_VAR, "shape_retrieval_profile.txt")).expanduser() + output_path = Path( + os.environ.get(PROFILE_OUTPUT_ENV_VAR, "shape_retrieval_profile.txt") + ).expanduser() output_path.write_text(stream.getvalue()) logger.info("Wrote profile output to %s", output_path) diff --git a/shape_retrieval/config.py b/shape_retrieval/config.py new file mode 100644 index 0000000..6db1f56 --- /dev/null +++ b/shape_retrieval/config.py @@ -0,0 +1,104 @@ +from __future__ import annotations + +from typing import Any, Literal, Optional + +from pydantic import BaseModel, Field + + +class FunctionalMapConfig(BaseModel): + w_descr: float = Field( + default=1e0, ge=0, description="Descriptor preservation weight." + ) + w_lap: float = Field( + default=1e-2, ge=0, description="Laplacian commutativity weight." + ) + w_dcomm: float = Field( + default=1e-1, ge=0, description="Descriptor commutativity weight." + ) + w_orient: float = Field( + default=0.0, ge=0, description="Orientation preservation weight." + ) + + n_cpus: int = Field(default=1, ge=1, description="Number of CPU workers to use.") + refine: Optional[Literal["icp", "zoomout"]] = Field( + default=None, description="Optional refinement method." + ) + verbose: bool = Field( + default=True, description="Pass verbose output to pyFM routines." + ) + + zoomout_nit: int = Field( + default=11, ge=1, description="Number of ZoomOut refinement iterations." + ) + zoomout_step: int = Field( + default=1, ge=1, description="ZoomOut refinement step size." + ) + + def fit_params(self) -> dict[str, float]: + return { + "w_descr": self.w_descr, + "w_lap": self.w_lap, + "w_dcomm": self.w_dcomm, + "w_orient": self.w_orient, + } + + @classmethod + def from_value( + cls, + value: "FunctionalMapConfig | dict[str, Any] | int | None" = None, + *, + n_cpus: int | None = None, + refine: str | None = None, + ) -> "FunctionalMapConfig": + if isinstance(value, cls): + data = value.dict() + elif isinstance(value, dict): + data = dict(value) + elif isinstance(value, int): + data = {"n_cpus": value} + elif value is None: + data = {} + else: + raise TypeError("config must be FunctionalMapConfig, dict, int, or None") + + if n_cpus is not None: + data["n_cpus"] = n_cpus + if refine is not None: + data["refine"] = refine + + return cls(**data) + + +class DenseMeshConfig(BaseModel): + dist_ratio: float = Field( + default=3.0, gt=0, description="Distance radius multiplier." + ) + self_weight_limit: float = Field( + default=0.25, ge=0, description="Minimum self weight." + ) + correct_dist: bool = Field( + default=False, description="Correct distance matrix values." + ) + interpolation: str = Field( + default="poly", description="Dense mesh interpolation method." + ) + return_dist: bool = Field( + default=True, description="Return distance matrix from dense mesh processing." + ) + adapt_radius: bool = Field( + default=True, description="Adapt geodesic radius during dense mesh processing." + ) + update_sample: bool = Field( + default=True, description="Update sampling during dense mesh processing." + ) + force_n_samples: bool = Field( + default=False, description="Force exactly the requested number of samples." + ) + verbose: bool = Field( + default=True, description="Pass verbose output to dense mesh routines." + ) + + def process_params(self, n_cpus: int) -> dict[str, Any]: + data = self.dict() + data["n_jobs"] = n_cpus + return data diff --git a/shape_utils/functional_maps.py b/shape_retrieval/functional_maps.py similarity index 51% rename from shape_utils/functional_maps.py rename to shape_retrieval/functional_maps.py index b3e7630..b3491b9 100644 --- a/shape_utils/functional_maps.py +++ b/shape_retrieval/functional_maps.py @@ -3,17 +3,17 @@ import logging from typing import Any -import numpy as np -from pyFM import functional import dense_mesh as dm +import numpy as np import pyFM.spectral as spectral +from pyFM import functional from pyFM.spectral.nn_utils import knn_query -from shape_utils.config import DenseMeshConfig, FunctionalMapConfig - +from shape_retrieval.config import DenseMeshConfig, FunctionalMapConfig logger = logging.getLogger(__name__) + def set_FM_model_parameters( mesh1: Any, mesh2: Any, @@ -22,46 +22,50 @@ def set_FM_model_parameters( ndescr: int, step: int, landmarks: Any, - descr_type: str = 'WKS', + descr_type: str = "WKS", ) -> Any: - """ - Set model parameters for FM and Wave Kernel Signatures and Heat Kernel Signatures on triangulated meshes using pyFM (https://github.com/RobinMagnet/pyFM) - - Returns FM model with list of WKS descriptors for each mesh + """ + Set model parameters for FM and Wave Kernel Signatures and Heat Kernel Signatures on triangulated meshes using pyFM (https://github.com/RobinMagnet/pyFM) + + Returns FM model with list of WKS descriptors for each mesh - Args: + Args: mesh1 (list) : array with vertices and faces mesh2 (list) : second array with vertices and faces - kprocess (int) : No. of eigenvalues to use - n_ev : the least number of Laplacian eigenvalues to consider + kprocess (int) : No. of eigenvalues to use + n_ev : the least number of Laplacian eigenvalues to consider ndescr (int) : No. of descriptors to include step (int) : sub-sample step, in order to not use too many descriptors landmarks : (p,1|2) array of indices of landmarks to match. If (p,1) uses the same indices for both. - descr_type : Descriptor type : WKS (default), HKS or Zernike + descr_type : Descriptor type : WKS (default), HKS or Zernike outputs: model : functional map model with set parameters and computed descriptors - """ - + """ + process_params = { - 'n_ev': (n_ev,n_ev), # n_ev: (k1, k2) tuple - with the least number of Laplacian eigenvalues to consider. - 'subsample_step': int(step), # In order not to use too many descriptors - 'descr_type': descr_type, # WKS or HKS - 'k_process' : int(kprocess), # No. of eigenvalues/eigenvectors to compute - 'n_descr': int(ndescr), # - 'landmarks': landmarks + "n_ev": ( + n_ev, + n_ev, + ), # n_ev: (k1, k2) tuple - with the least number of Laplacian eigenvalues to consider. + "subsample_step": int(step), # In order not to use too many descriptors + "descr_type": descr_type, # WKS or HKS + "k_process": int(kprocess), # No. of eigenvalues/eigenvectors to compute + "n_descr": int(ndescr), # + "landmarks": landmarks, } - model = functional.FunctionalMapping(mesh1,mesh2) + model = functional.FunctionalMapping(mesh1, mesh2) + + # preprocess functional mapping and compute descriptors + + model.preprocess(**process_params, verbose=True) - #preprocess functional mapping and compute descriptors - - model.preprocess(**process_params,verbose=True) - - #enlist = model.energylist + # enlist = model.energylist return model + def calculate_functional_maps( model: Any, config: FunctionalMapConfig | dict[str, Any] | int | None = None, @@ -69,20 +73,20 @@ def calculate_functional_maps( n_cpus: int | None = None, refine: str | None = None, ) -> tuple[Any, np.ndarray]: - """ - Calculate functional maps and point to point maps with pyFM code (https://github.com/RobinMagnet/pyFM) - - Returns functional maps and fitted model - - Args: - mesh1 (list) : array with vertices and faces - mesh2 (list) : second array with vertices and faces + """ + Calculate functional maps and point to point maps with pyFM code (https://github.com/RobinMagnet/pyFM) + + Returns functional maps and fitted model + + Args: + mesh1 (list) : array with vertices and faces + mesh2 (list) : second array with vertices and faces model (int) : functional maps model pyFM - refine (str) : Selected method to refine functional map + refine (str) : Selected method to refine functional map Returns: FM : Functional map (correspondance matrix) - p2p21 : Point to point map - """ + p2p21 : Point to point map + """ fm_config = FunctionalMapConfig.from_value(config, n_cpus=n_cpus, refine=refine) logger.info("cpus used: %s", fm_config.n_cpus) @@ -92,12 +96,14 @@ def calculate_functional_maps( if fm_config.refine is None: logger.info("Computing point to point map using correspondence matrix") FM = model.FM - elif fm_config.refine == 'icp': - model.change_FM_type('classic') + elif fm_config.refine == "icp": + model.change_FM_type("classic") model.icp_refine(n_jobs=fm_config.n_cpus, verbose=fm_config.verbose) FM = model.FM - elif fm_config.refine == 'zoomout': - model.change_FM_type('classic') # We refine the first computed map, not the icp-refined one + elif fm_config.refine == "zoomout": + model.change_FM_type( + "classic" + ) # We refine the first computed map, not the icp-refined one model.zoomout_refine( nit=fm_config.zoomout_nit, step=fm_config.zoomout_step, @@ -106,15 +112,17 @@ def calculate_functional_maps( FM = model.FM else: raise ValueError("refine must be one of: icp, zoomout, or None") - + model_FM = model return model_FM, FM + def calculate_p2p_map(model_FM: Any, n_cpus: int = 8) -> np.ndarray: p2p_21 = model_FM.get_p2p(n_jobs=n_cpus) return p2p_21 + def calculate_scalable_functional_maps( mesh1: Any, mesh2: Any, @@ -125,40 +133,40 @@ def calculate_scalable_functional_maps( ) -> np.ndarray: process_params = (dense_mesh_config or DenseMeshConfig()).process_params(n_cpus) - U1, Ab1, Wb1, sub1, distmat1 = dm.process_mesh(mesh1, n_samples, **process_params) evals1, evects1 = dm.get_approx_spectrum(Wb1, Ab1, k=neigvecs, verbose=True) U2, Ab2, Wb2, sub2, distmat2 = dm.process_mesh(mesh2, n_samples, **process_params) evals2, evects2 = dm.get_approx_spectrum(Wb2, Ab2, k=neigvecs, verbose=True) - # Compute an initial approximate functional map - p2p_21_sub_init = knn_query(mesh1.vertices[sub1], mesh2.vertices[sub2], k=1, n_jobs=n_cpus) + p2p_21_sub_init = knn_query( + mesh1.vertices[sub1], mesh2.vertices[sub2], k=1, n_jobs=n_cpus + ) # We compute the initial functional map using the approximate spectrum here (same method that will be used inside ZoomOut) FM_12_init = spectral.p2p_to_FM( - p2p_21_sub_init, evects1[:, :20], evects2[:, :20], A2=Ab2 + p2p_21_sub_init, evects1[:, :20], evects2[:, :20], A2=Ab2 ) - #FM_12_zo, p2p_21_sub_zo = zoomout_refine( - #FM_12_init, - #evects1, - #evects2, - #nit=16, - #step=5, - #A2=Ab2, - #return_p2p=True, - #n_jobs=n_cpus, - #verbose=True, - #) + # FM_12_zo, p2p_21_sub_zo = zoomout_refine( + # FM_12_init, + # evects1, + # evects2, + # nit=16, + # step=5, + # A2=Ab2, + # return_p2p=True, + # n_jobs=n_cpus, + # verbose=True, + # ) return FM_12_init def compute_shape_difference(model: Any) -> tuple[np.ndarray, np.ndarray]: """ - Computes shape difference operators, area-based and conformal - + Computes shape difference operators, area-based and conformal + Args: model : functional map fitting model computed with pyFM @@ -170,7 +178,8 @@ def compute_shape_difference(model: Any) -> tuple[np.ndarray, np.ndarray]: D_area = model.D_a D_conformal = model.D_c - return D_area, D_conformal + return D_area, D_conformal + def calculate_functional_maps_chem( model: Any, @@ -181,23 +190,25 @@ def calculate_functional_maps_chem( n_cpus: int | None = None, refine: str | None = None, ) -> np.ndarray: - """ - Calculate functional maps and point to point maps with pyFM code (https://github.com/RobinMagnet/pyFM) - - Returns functional maps and fitted model - - Args: - mesh1 (list) : array with vertices and faces - mesh2 (list) : second array with vertices and faces + """ + Calculate functional maps and point to point maps with pyFM code (https://github.com/RobinMagnet/pyFM) + + Returns functional maps and fitted model + + Args: + mesh1 (list) : array with vertices and faces + mesh2 (list) : second array with vertices and faces model (int) : functional maps model pyFM - refine (str) : Selected method to refine functional map + refine (str) : Selected method to refine functional map Returns: FM : Functional map (correspondance matrix) - p2p21 : Point to point map - """ + p2p21 : Point to point map + """ fm_config = FunctionalMapConfig.from_value(config, n_cpus=n_cpus, refine=refine) logger.info("cpus used: %s", fm_config.n_cpus) - model.fit_othdescr(descr1, descr2, **fm_config.fit_params(), verbose=fm_config.verbose) + model.fit_othdescr( + descr1, descr2, **fm_config.fit_params(), verbose=fm_config.verbose + ) return model.FM diff --git a/shape_utils/meshes.py b/shape_retrieval/meshes.py similarity index 88% rename from shape_utils/meshes.py rename to shape_retrieval/meshes.py index 97dbf79..916d59d 100644 --- a/shape_utils/meshes.py +++ b/shape_retrieval/meshes.py @@ -9,6 +9,7 @@ import trimesh from scipy.spatial import KDTree + def fix_mesh( mesh_file: str, resolution: float = 0.5, @@ -29,7 +30,7 @@ def fix_mesh( resolution (int or float): Target fraction reduction (or simplification parameter) passed to - `reduce_resolution_mesh()` to collapse vertices. + `reduce_resolution_mesh()` to collapse vertices. collapse_vertices (bool, optional): If True, the function collapses/simplifies the mesh after repairing it. @@ -51,12 +52,12 @@ def fix_mesh( - MeshLab (pymeshlab) is used for optional reconstruction and resolution reduction. - The function does not write output to disk; it returns the processed arrays. """ - + ms = ml.MeshSet() ms.load_new_mesh(mesh_file) if reconstruct: - new_ms,v,f = reconstruct_mesh(ms) - ms=new_ms + new_ms, v, f = reconstruct_mesh(ms) + ms = new_ms m = ms.current_mesh() v = m.vertex_matrix() f = m.face_matrix() @@ -64,13 +65,14 @@ def fix_mesh( meshfix.repair() if collapse_vertices: ms.add_mesh(ml.Mesh(meshfix.points, meshfix.faces)) - v,f = reduce_resolution_mesh(ms, resolution) + v, f = reduce_resolution_mesh(ms, resolution) else: v = meshfix.points f = meshfix.faces return v, f + def reduce_resolution_mesh(ms: Any, resolution: float) -> tuple[np.ndarray, np.ndarray]: """ Reduce the resolution of the current mesh in a MeshSet using quadratic edge collapse of vertices. @@ -88,7 +90,7 @@ def reduce_resolution_mesh(ms: Any, resolution: float) -> tuple[np.ndarray, np.n Fraction of the current vertex count used to determine the target number of faces. Typically: 0 < resolution < 1 → reduce to a fraction of the original size - + Returns: Tuple[np.ndarray, np.ndarray]: `(v, f)` where: @@ -100,14 +102,19 @@ def reduce_resolution_mesh(ms: Any, resolution: float) -> tuple[np.ndarray, np.n - Normals, boundaries, and topology are preserved during simplification. """ m = ms.current_mesh() - TARGET = int(resolution*m.vertex_number()) - ms.apply_filter('meshing_decimation_quadric_edge_collapse', - targetfacenum=TARGET, preservenormal=True, - preserveboundary=True,preservetopology=True) + TARGET = int(resolution * m.vertex_number()) + ms.apply_filter( + "meshing_decimation_quadric_edge_collapse", + targetfacenum=TARGET, + preservenormal=True, + preserveboundary=True, + preservetopology=True, + ) m = ms.current_mesh() v = m.vertex_matrix() f = m.face_matrix() - return v,f + return v, f + def reconstruct_mesh(ms: Any) -> tuple[Any, np.ndarray, np.ndarray]: """ @@ -131,38 +138,41 @@ def reconstruct_mesh(ms: Any) -> tuple[Any, np.ndarray, np.ndarray]: ValueError: If the MeshSet has no current mesh to reconstruct. """ - ms.apply_filter('generate_surface_reconstruction_vcg') + ms.apply_filter("generate_surface_reconstruction_vcg") m = ms.current_mesh() - return ms, m.vertex_matrix(),m.face_matrix() + return ms, m.vertex_matrix(), m.face_matrix() def compute_center_of_mass(mesh: Any) -> np.ndarray: center = mesh.center_mass return center + + def compute_min_dist(mesh1_file: str, mesh2_file: str) -> float: """ Compute minimum distance between two meshes - + Args: mesh1_file: Path to the first mesh file to be processed. Supported formats are typically `.obj` and `.off`. mesh1_file: Path to the second mesh file to be processed. Supported formats are typically - `.obj` and `.off`. + `.obj` and `.off`. """ - mesh1=trimesh.load_mesh(mesh1_file) + mesh1 = trimesh.load_mesh(mesh1_file) mesh2 = trimesh.load_mesh(mesh2_file) # Get the vertices of each mesh vertices1 = mesh1.vertices vertices2 = mesh2.vertices - + # Use a KDTree for efficient nearest neighbor search tree = KDTree(vertices2) distances, _ = tree.query(vertices1) # Return the minimum distance return float(np.min(distances)) + def compute_centers_dist(mesh1_file: str, mesh2_file: str) -> float: """ Compute the Euclidean distance between the centers of mass of two meshes. @@ -193,8 +203,8 @@ def compute_centers_dist(mesh1_file: str, mesh2_file: str) -> float: mesh2 = trimesh.load_mesh(mesh2_file) # Compute center of mass (volume-based if possible) - center1 = mesh1.center_mass - center2 = mesh2.center_mass + center1 = mesh1.center_mass + center2 = mesh2.center_mass # Calculate Euclidean distance between centers distance = np.linalg.norm(center1 - center2) @@ -206,8 +216,8 @@ def remove_until_vertex(file_path: str) -> None: Remove all lines in a mesh file until the first vertex declaration. This function scans the file and discards all content before the first line - that begins with `'v''. It is primarily useful for cleaning corrupted or non-standard OBJ files - that cannot be read for Zernike descriptors. + that begins with `'v''. It is primarily useful for cleaning corrupted or non-standard OBJ files + that cannot be read for Zernike descriptors. Args: file_path (str): Path to the mesh file to be cleaned. The file is modified in place. @@ -223,14 +233,16 @@ def remove_until_vertex(file_path: str) -> None: If the file contains no vertex lines starting with `'v'`. """ path = Path(file_path) - with path.open('r') as file: + with path.open("r") as file: lines = file.readlines() # Find the index of the first line that starts with 'v' (ignoring leading whitespace) - start_index = next((i for i, line in enumerate(lines) if line.lstrip().startswith('v')), len(lines)) + start_index = next( + (i for i, line in enumerate(lines) if line.lstrip().startswith("v")), len(lines) + ) # Keep only lines starting from the first 'v' line lines = lines[start_index:] - with path.open('w') as file: + with path.open("w") as file: file.writelines(lines) diff --git a/shape_utils/models.py b/shape_retrieval/models.py similarity index 88% rename from shape_utils/models.py rename to shape_retrieval/models.py index 8c425d8..fd7a029 100644 --- a/shape_utils/models.py +++ b/shape_retrieval/models.py @@ -15,21 +15,21 @@ import torch import torch.nn as nn -import torch.nn.functional as F -import numpy as np -#from utils import pearsonr +# from utils import pearsonr + class SimpleEuclideanModel(nn.Module): def __init__(self): super(SimpleEuclideanModel, self).__init__() - def forward(self, inputs_1, inputs_2, output_dist = False): + def forward(self, inputs_1, inputs_2, output_dist=False): euclidean_dist = torch.norm(inputs_1 - inputs_2, 2, dim=-1).squeeze() if output_dist: return euclidean_dist return 1.0 / (1.0 + euclidean_dist) + class NeuralNetworkModel(nn.Module): def __init__(self, input_dim, hidden_dims, fc_dims, extra_feature_dim): super(NeuralNetworkModel, self).__init__() @@ -45,10 +45,12 @@ def __init__(self, input_dim, hidden_dims, fc_dims, extra_feature_dim): self.nb_encoder_layers = len(hidden_dims) self.fc_layers = [] - prev_dim = 2 * (input_dim + sum(hidden_dims)) + \ - 2 * (1 + self.nb_encoder_layers) + \ - extra_feature_dim - #prev_dim = 4744 # 1444# 4744 #1444 + prev_dim = ( + 2 * (input_dim + sum(hidden_dims)) + + 2 * (1 + self.nb_encoder_layers) + + extra_feature_dim + ) + # prev_dim = 4744 # 1444# 4744 #1444 for fc_dim in fc_dims: self.fc_layers.append(nn.Linear(prev_dim, fc_dim)) self.fc_layers.append(nn.BatchNorm1d(fc_dim)) @@ -60,14 +62,14 @@ def __init__(self, input_dim, hidden_dims, fc_dims, extra_feature_dim): self.last_fc = nn.Linear(prev_dim, 1) self.sigmoid = nn.Sigmoid() - def forward(self, inputs_1, inputs_2, extra_features, output_dist = False): - + def forward(self, inputs_1, inputs_2, extra_features, output_dist=False): + outs_1 = [] out = inputs_1 outs_1.append(out) for layer in self.encoder: out = layer(out) - if 'ReLU' in str(layer): + if "ReLU" in str(layer): outs_1.append(out) # Encoding inputs_2 @@ -76,14 +78,16 @@ def forward(self, inputs_1, inputs_2, extra_features, output_dist = False): outs_2.append(out) for layer in self.encoder: out = layer(out) - if 'ReLU' in str(layer): + if "ReLU" in str(layer): outs_2.append(out) # Creating feature_vectors feature_vectors = [] for i in range(self.nb_encoder_layers + 1): euclidean_dist = torch.norm(outs_1[i] - outs_2[i], 2, dim=-1, keepdim=True) - cosine_similarity = nn.CosineSimilarity(dim=-1, eps=1e-6)(outs_1[i], outs_2[i]).unsqueeze(1) + cosine_similarity = nn.CosineSimilarity(dim=-1, eps=1e-6)( + outs_1[i], outs_2[i] + ).unsqueeze(1) subtraction = torch.abs(outs_1[i] - outs_2[i]) multiplication = outs_1[i] * outs_2[i] feature_vectors.append(euclidean_dist) diff --git a/shape_utils/pipeline.py b/shape_retrieval/pipeline.py similarity index 77% rename from shape_utils/pipeline.py rename to shape_retrieval/pipeline.py index 13d1787..3834e13 100644 --- a/shape_utils/pipeline.py +++ b/shape_retrieval/pipeline.py @@ -12,16 +12,20 @@ import trimesh from pyFM import mesh -from shape_utils.config import FunctionalMapConfig -from shape_utils.functional_maps import ( +from shape_retrieval.config import FunctionalMapConfig +from shape_retrieval.functional_maps import ( calculate_functional_maps, calculate_p2p_map, set_FM_model_parameters, ) -from shape_utils.meshes import fix_mesh -from shape_utils.similarity_scores import calculate_geodesic_norm_score -from shape_utils.utils import find_minimum_distance_meshes, save_data_to_csv, save_list_to_csv -from shape_utils.zernike_descr import get_inv +from shape_retrieval.meshes import fix_mesh +from shape_retrieval.similarity_scores import calculate_geodesic_norm_score +from shape_retrieval.utils import ( + find_minimum_distance_meshes, + save_data_to_csv, + save_list_to_csv, +) +from shape_retrieval.zernike_descr import get_inv logger = logging.getLogger(__name__) @@ -90,7 +94,9 @@ def validate_config(config: PipelineConfig) -> None: validate_input_file(config.mesh2, "mesh2") if len(config.entry_ids) != 2: - raise ValueError(f"--entry-ids must contain exactly two values, got {len(config.entry_ids)}.") + raise ValueError( + f"--entry-ids must contain exactly two values, got {len(config.entry_ids)}." + ) descriptor = config.descriptor.upper() if descriptor not in SUPPORTED_DESCRIPTORS: @@ -105,13 +111,17 @@ def validate_config(config: PipelineConfig) -> None: raise ValueError("--collapse-vertices must be used together with --fix-meshes.") if config.refine is not None and config.refine not in SUPPORTED_REFINEMENT: - raise ValueError(f"--refine must be one of {', '.join(sorted(SUPPORTED_REFINEMENT))}.") + raise ValueError( + f"--refine must be one of {', '.join(sorted(SUPPORTED_REFINEMENT))}." + ) if config.n_cpus < 1: raise ValueError("--n-cpus must be greater than or equal to 1.") if config.neigvecs < 1 or config.n_ev < 1 or config.n_descr < 1 or config.step < 1: - raise ValueError("--neigvecs, --n-ev, --n-descr, and --step must all be positive.") + raise ValueError( + "--neigvecs, --n-ev, --n-descr, and --step must all be positive." + ) config.output.mkdir(parents=True, exist_ok=True) @@ -157,7 +167,9 @@ def _fix_meshes(config: PipelineConfig) -> FixedMeshes: return FixedMeshes(vertices_1, faces_1, vertices_2, faces_2) -def _calculate_minimum_distance(config: PipelineConfig, fixed_meshes: FixedMeshes | None) -> None: +def _calculate_minimum_distance( + config: PipelineConfig, fixed_meshes: FixedMeshes | None +) -> None: if fixed_meshes is None: mesh1 = mesh.TriMesh(str(config.mesh1), area_normalize=False, center=False) mesh2 = mesh.TriMesh(str(config.mesh2), area_normalize=False, center=False) @@ -178,8 +190,12 @@ def _build_area_normalized_meshes( mesh2 = mesh.TriMesh(str(config.mesh2), area_normalize=True, center=False) return mesh1, mesh2 - mesh1 = mesh.TriMesh(fixed_meshes.vertices_1, fixed_meshes.faces_1, area_normalize=True, center=False) - mesh2 = mesh.TriMesh(fixed_meshes.vertices_2, fixed_meshes.faces_2, area_normalize=True, center=False) + mesh1 = mesh.TriMesh( + fixed_meshes.vertices_1, fixed_meshes.faces_1, area_normalize=True, center=False + ) + mesh2 = mesh.TriMesh( + fixed_meshes.vertices_2, fixed_meshes.faces_2, area_normalize=True, center=False + ) return mesh1, mesh2 @@ -204,7 +220,12 @@ def _run_spectral_descriptors( _log_similarity_score(fm) return - logger.info("Calculating %s descriptors for structures %s and %s", descriptor, entry_id_1, entry_id_2) + logger.info( + "Calculating %s descriptors for structures %s and %s", + descriptor, + entry_id_1, + entry_id_2, + ) model = set_FM_model_parameters( mesh1, mesh2, @@ -239,26 +260,56 @@ def _log_similarity_score(fm: np.ndarray) -> None: logger.info("Shape dissimilarity score is: %s", score) -def _run_zernike_descriptors(config: PipelineConfig, fixed_meshes: FixedMeshes | None) -> None: +def _run_zernike_descriptors( + config: PipelineConfig, fixed_meshes: FixedMeshes | None +) -> None: map2zernike_binary = resolve_executable(config.map2zernike_binary, "map2zernike") obj2grid_binary = resolve_executable(config.obj2grid_binary, "obj2grid") entry_id_1, entry_id_2 = config.entry_ids if fixed_meshes is not None: - mesh_1 = trimesh.Trimesh(vertices=fixed_meshes.vertices_1, faces=fixed_meshes.faces_1) - mesh_2 = trimesh.Trimesh(vertices=fixed_meshes.vertices_2, faces=fixed_meshes.faces_2) + mesh_1 = trimesh.Trimesh( + vertices=fixed_meshes.vertices_1, faces=fixed_meshes.faces_1 + ) + mesh_2 = trimesh.Trimesh( + vertices=fixed_meshes.vertices_2, faces=fixed_meshes.faces_2 + ) output1_obj = config.output / f"{entry_id_1}_fixed.obj" output2_obj = config.output / f"{entry_id_2}_fixed.obj" mesh_1.export(output1_obj) mesh_2.export(output2_obj) - get_inv(str(output1_obj), entry_id_1, map2zernike_binary, obj2grid_binary, str(config.output)) - get_inv(str(output2_obj), entry_id_2, map2zernike_binary, obj2grid_binary, str(config.output)) + get_inv( + str(output1_obj), + entry_id_1, + map2zernike_binary, + obj2grid_binary, + str(config.output), + ) + get_inv( + str(output2_obj), + entry_id_2, + map2zernike_binary, + obj2grid_binary, + str(config.output), + ) return _validate_zernike_mesh(config.mesh1) _validate_zernike_mesh(config.mesh2) - get_inv(str(config.mesh1), entry_id_1, map2zernike_binary, obj2grid_binary, str(config.output)) - get_inv(str(config.mesh2), entry_id_2, map2zernike_binary, obj2grid_binary, str(config.output)) + get_inv( + str(config.mesh1), + entry_id_1, + map2zernike_binary, + obj2grid_binary, + str(config.output), + ) + get_inv( + str(config.mesh2), + entry_id_2, + map2zernike_binary, + obj2grid_binary, + str(config.output), + ) def _validate_zernike_mesh(path: Path) -> None: diff --git a/shape_utils/run.py b/shape_retrieval/run.py similarity index 71% rename from shape_utils/run.py rename to shape_retrieval/run.py index 3d9ee94..87093d5 100644 --- a/shape_utils/run.py +++ b/shape_retrieval/run.py @@ -1,7 +1,6 @@ from __future__ import annotations -from shape_utils.cli import main - +from shape_retrieval.cli import main if __name__ == "__main__": raise SystemExit(main()) diff --git a/shape_retrieval/similarity_scores.py b/shape_retrieval/similarity_scores.py new file mode 100644 index 0000000..ef592c0 --- /dev/null +++ b/shape_retrieval/similarity_scores.py @@ -0,0 +1,186 @@ +import logging +import os +from itertools import combinations_with_replacement + +import numpy as np +import torch +from torch import FloatTensor + +from shape_retrieval.models import SimpleEuclideanModel + + +def calculate_geodesic_norm_score(FM): + """ + Calculates norm of correspondance matrix based on geodesic distance of eigenvalues spectrum + + Returns norm as a similarity score + + Args: + FM : Correspondance matrix of functional map + Returns: + """ + + eigenvalues_FM = np.linalg.eigvals(FM) + eps = 1e-12 # avoid log(0) + score = np.sqrt(np.sum(np.log(np.absolute(np.real(eigenvalues_FM)) + eps) ** 2)) + + return score + + +def get_pairs_fast(arr): + """ + Generate all unique pairs (including self-pairs) from a list or array. + + Args: + arr (list or array-like): Input list or array of elements. + + Returns: + list of tuples: A list containing tuples of all unique pairs. Each tuple + is of the form `(a, b)` where `a` and `b` are elements + from `arr`. + + Example: + get_pairs_fast([1, 2, 3]) + [(1, 1), (1, 2), (1, 3), (2, 2), (2, 3), (3, 3)] + """ + + return list(combinations_with_replacement(arr, 2)) + + +def get_pairs(arr): + pairs = [] + for i in range(len(arr)): + for j in range(i, len(arr)): + if i <= j: + pairs.append((arr[i], arr[j])) + return pairs + + +def read_inv(fn): + vectors = [] + f = open(fn, "r") + for line in f: + vectors.append(float(line.strip())) + f.close() + return vectors[1::] + + +def read_dataset(input_dir, db_structures, atom_type): + dataset = {} + + for struct in db_structures: + if atom_type == "mainchain": + descriptors_file = os.path.join(input_dir, struct + "_cacn.inv") + _3dzd = read_inv(descriptors_file) + # _3dzd = read_inv(input_dir + struct + '_cacn.inv') + else: + descriptors_file = os.path.join(input_dir, struct + ".inv") + _3dzd = read_inv(descriptors_file) + # _3dzd = read_inv(input_dir + struct + '.inv') + + # _vertex = read_ply(input_dir + struct + '.ply') + data = {} + data["_3dzd"] = _3dzd + # data['vertex_face'] = _vertex + + dataset[struct] = data + return dataset + + +def pairs_to_features(pairs, alpha_data, scope_data): + + _3DZD_vectors_1, _3DZD_vectors_2 = [], [] + # element_vertices_1, element_vertices_2 = [], [] + # element_faces_1, element_faces_2 = [], [] + for _pair in pairs: + id_0, id_1 = str(_pair[0]), str(_pair[1]) + + _3dzd_1 = list(alpha_data[id_0]["_3dzd"]) + _3dzd_2 = list(scope_data[id_1]["_3dzd"]) + + _3DZD_vector_1 = np.asarray(_3dzd_1) + _3DZD_vector_1 = np.expand_dims(_3DZD_vector_1.squeeze(), axis=0) + _3DZD_vector_2 = np.asarray(_3dzd_2) + _3DZD_vector_2 = np.expand_dims(_3DZD_vector_2.squeeze(), axis=0) + + # element_vertex_1, element_face_1 = tuple([int(x) for x in alpha_data[id_0]['vertex_face']]) + # element_vertex_2, element_face_2 = tuple([int(x) for x in scope_data[id_1]['vertex_face']]) + + # Update + _3DZD_vectors_1.append(_3DZD_vector_1) + _3DZD_vectors_2.append(_3DZD_vector_2) + # element_vertices_1.append(element_vertex_1) + # element_faces_1.append(element_face_1) + # element_vertices_2.append(element_vertex_2) + # element_faces_2.append(element_face_2) + _3DZD_vectors_1 = np.array(_3DZD_vectors_1) + _3DZD_vectors_2 = np.array(_3DZD_vectors_2) + _3DZD_vectors_1 = FloatTensor(_3DZD_vectors_1).squeeze() + # element_vertices_1 = FloatTensor(element_vertices_1) + # element_faces_1 = FloatTensor(element_faces_1) + + _3DZD_vectors_2 = FloatTensor(_3DZD_vectors_2).squeeze() + # element_vertices_2 = FloatTensor(element_vertices_2) + # element_faces_2 = FloatTensor(element_faces_2) + + # vertices_diff = torch.abs(element_vertices_1 - element_vertices_2).unsqueeze(1) + # faces_diff = torch.abs(element_faces_1 - element_faces_2).unsqueeze(1) + # extra_features = torch.cat([vertices_diff, faces_diff], dim = 1) + + return _3DZD_vectors_1, _3DZD_vectors_2 + + +def predict_similarity_zernike( + input_dir, + output_dir, + model_type="simple_euclidean_model", + atom_type="fullatom", + cuda="true", + device_id="0", +): + + if cuda == "true" and torch.cuda.is_available(): + cuda = True + else: + cuda = False + device_id = torch.device("cpu") + + # model_type = 'neural_network' + if model_type == "neural_network": + logging.info("neural network of Kihara not yet implemented") + + elif model_type == "simple_euclidean_model": + logging.info("Simple Euclidean Model") + model = SimpleEuclideanModel() + model.eval() + db_structures = [ + x for x in os.listdir(input_dir) if ".inv" in x and "_cacn" not in x + ] + db_structures = [x.split(".")[0] for x in db_structures] + logging.info("pdb to compare : {} ".format(len(db_structures))) + + if len(db_structures) == 0: + logging.warning("There are no structures to compare.") + return + + database_dataset = read_dataset(input_dir, db_structures, atom_type) + # query_pdb = db_structures[0] + my_pairs = get_pairs_fast(db_structures) + # my_pairs = [(query_pdb, j) for j in db_structures] + inputs_1, inputs_2 = pairs_to_features(my_pairs, database_dataset, database_dataset) + if cuda: + inputs_1 = inputs_1.cuda(device_id) + inputs_2 = inputs_2.cuda(device_id) + # extra_features = extra_features.cuda(device_id) + + if model_type == "simple_euclidean_model": + outputs = model(inputs_1, inputs_2, True) + outputs = outputs.squeeze().cpu().data.numpy().tolist() + + output_scores_file = os.path.join(output_dir, atom_type + "_prediction.txt") + with open(output_scores_file, "w") as fh: + fh.write("Query\tTarget\tDis-similarity Probability\n") + # for i in range(0,len(outputs)): + for pair, score in zip(my_pairs, outputs): + fh.write(pair[0] + "\t" + pair[1] + "\t" + str(round(score, 3)) + "\n") + # fh.write(db_structures[0] + '\t' + db_structures[i] + '\t' +str(round(outputs[i],3)) + '\n') diff --git a/shape_retrieval/spectral_descr.py b/shape_retrieval/spectral_descr.py new file mode 100644 index 0000000..6809959 --- /dev/null +++ b/shape_retrieval/spectral_descr.py @@ -0,0 +1,72 @@ +import logging +from pyFM import functional + + +logger = logging.getLogger() + + +def calculate_spectral_descriptors( + mesh, kprocess, n_ev, ndescr, step, landmarks, descr_type="WKS" +): + """ + Calculate Wave Kernel Signatures and Heat Kernel Signatures on triangulated meshes using pyFM (https://github.com/RobinMagnet/pyFM) + + Returns list of WKS descriptors for each mesh + + Args: + mesh : Trimesh mesh object + kprocess (int) : No. of eigenvalues to use + n_ev : the least number of Laplacian eigenvalues to consider + ndescr (int) : No. of descriptors to include + step (int) : sub-sample step, in order to not use too many descriptors + landmarks : (p,1|2) array of indices of landmarks to match. + If (p,1) uses the same indices for both. + descr_type : Descriptor type : WKS (default), HKS or Zernike + + """ + + process_params = { + "n_ev": ( + n_ev, + n_ev, + ), # n_ev: (k1, k2) tuple - with the least number of Laplacian eigenvalues to consider. + "subsample_step": int(step), # In order not to use too many descriptors + "descr_type": descr_type, # WKS or HKS + "k_process": int(kprocess), # No. of eigenvalues/eigenvectors to compute + "n_descr": int(ndescr), # + "landmarks": landmarks, + } + + descr_model = functional.SpectralDescriptors(mesh) + + # preprocess functional mapping and compute descriptors + descr_model.preprocess_descriptors_mesh(**process_params, verbose=True) + + # wks descriptors for surface mesh (mesh1 and mesh) + + descr = descr_model.descr + + return descr + + +def distance_WKS(wks1, wks2): + """ + Compute distance between two descriptors maps + Returns file with distance + + wks1 : list of wks descriptors for mesh1 + wks2 : list of wks descriptors for mesh2 + + """ + distance_wks = [] + for i, j in zip(wks1, wks2): + coef_sum = 0.0 + for wks_e1, wks_e2 in zip(i, j): + denom = wks_e1 + wks_e2 + if denom != 0: + if abs((wks_e1 - wks_e2) / (wks_e1 + wks_e2)) > 1.0: + logging.info("not equal {} and {}".format(wks_e1, wks_e2)) + coef = abs(wks_e1 - wks_e2) + coef_sum += coef + distance_wks.append(coef_sum) + return distance_wks diff --git a/shape_retrieval/utils.py b/shape_retrieval/utils.py new file mode 100644 index 0000000..1944b5d --- /dev/null +++ b/shape_retrieval/utils.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +import logging +from typing import Any + +import pandas as pd +import numpy as np +from scipy.spatial import KDTree + + +logger = logging.getLogger(__name__) + + +def save_data_to_csv(data: np.ndarray, output_file: str) -> None: + """Dump matrix data into a output csv file. + Args: + data (dict): The data (for spectral descriptors or functional maps) to be dumped. + output_file (str): The path to the output file. + + Raises: + ValueError: If `data` is empty. + """ + if len(data) != 0: + try: + descr_data = tuple(map(tuple, data)) + df = pd.DataFrame(descr_data) + df.to_csv(output_file, index=False, header=None) + + # return df + + except Exception as e: + logger.error( + "Invalid data frame for wks descriptors: probably wrong fields in the data" + ) + logger.error(e) + else: + logger.info("No data found to save") + + return None + + +def save_list_to_csv(data: Any, output_file: str) -> None: + """Dump list into a csv output file. + Args: + data (dict): The data for list of parameters to be dumped. + output_file (str): The path to the output file. + + Raises: + ValueError: If `data` is empty. + """ + if len(data) != 0: + try: + df = pd.DataFrame(data) + df.to_csv(output_file, index=False, header=None) + + except Exception as e: + logger.error( + "Invalid data frame list of parameters: probably wrong fields in the data" + ) + logger.error(e) + else: + logger.info("No data found to save") + + return None + + +def find_minimum_distance_meshes(mesh1: Any, mesh2: Any) -> float: + """ + Compute the minimum Euclidean distance between two meshes. + + Args: + mesh1: A mesh object with a `vertlist` attribute containing vertex coordinates + as an (N, 3) array. + mesh2: A mesh object with a `vertlist` attribute containing vertex coordinates + as an (M, 3) array. + + Returns: + float: The minimum Euclidean distance between any vertex of `mesh1` and any + vertex of `mesh2`. + """ + # Get the vertices of each mesh + vertices1 = mesh1.vertlist + vertices2 = mesh2.vertlist + + # Use a KDTree for efficient nearest neighbor search + tree = KDTree(vertices2) + distances, _ = tree.query(vertices1) + + # Return the minimum distance + return float(np.min(distances)) diff --git a/shape_utils/zernike_descr.py b/shape_retrieval/zernike_descr.py similarity index 95% rename from shape_utils/zernike_descr.py rename to shape_retrieval/zernike_descr.py index 237a3d2..e001b97 100644 --- a/shape_utils/zernike_descr.py +++ b/shape_retrieval/zernike_descr.py @@ -12,7 +12,7 @@ import subprocess from pathlib import Path -from shape_utils.meshes import remove_until_vertex +from shape_retrieval.meshes import remove_until_vertex logger = logging.getLogger(__name__) @@ -57,7 +57,9 @@ def get_inv( _remove_if_exists(grid_file) -def run_binary(command: list[str], binary_name: str) -> subprocess.CompletedProcess[str]: +def run_binary( + command: list[str], binary_name: str +) -> subprocess.CompletedProcess[str]: logger.info("Running %s: %s", binary_name, " ".join(command)) try: return subprocess.run( diff --git a/shape_utils/config.py b/shape_utils/config.py deleted file mode 100644 index 4b7349f..0000000 --- a/shape_utils/config.py +++ /dev/null @@ -1,70 +0,0 @@ -from __future__ import annotations - -from typing import Any, Literal, Optional - -from pydantic import BaseModel, Field - - -class FunctionalMapConfig(BaseModel): - w_descr: float = Field(default=1e0, ge=0, description="Descriptor preservation weight.") - w_lap: float = Field(default=1e-2, ge=0, description="Laplacian commutativity weight.") - w_dcomm: float = Field(default=1e-1, ge=0, description="Descriptor commutativity weight.") - w_orient: float = Field(default=0.0, ge=0, description="Orientation preservation weight.") - - n_cpus: int = Field(default=1, ge=1, description="Number of CPU workers to use.") - refine: Optional[Literal["icp", "zoomout"]] = Field(default=None, description="Optional refinement method.") - verbose: bool = Field(default=True, description="Pass verbose output to pyFM routines.") - - zoomout_nit: int = Field(default=11, ge=1, description="Number of ZoomOut refinement iterations.") - zoomout_step: int = Field(default=1, ge=1, description="ZoomOut refinement step size.") - - def fit_params(self) -> dict[str, float]: - return { - "w_descr": self.w_descr, - "w_lap": self.w_lap, - "w_dcomm": self.w_dcomm, - "w_orient": self.w_orient, - } - - @classmethod - def from_value( - cls, - value: "FunctionalMapConfig | dict[str, Any] | int | None" = None, - *, - n_cpus: int | None = None, - refine: str | None = None, - ) -> "FunctionalMapConfig": - if isinstance(value, cls): - data = value.dict() - elif isinstance(value, dict): - data = dict(value) - elif isinstance(value, int): - data = {"n_cpus": value} - elif value is None: - data = {} - else: - raise TypeError("config must be FunctionalMapConfig, dict, int, or None") - - if n_cpus is not None: - data["n_cpus"] = n_cpus - if refine is not None: - data["refine"] = refine - - return cls(**data) - - -class DenseMeshConfig(BaseModel): - dist_ratio: float = Field(default=3.0, gt=0, description="Distance radius multiplier.") - self_weight_limit: float = Field(default=0.25, ge=0, description="Minimum self weight.") - correct_dist: bool = Field(default=False, description="Correct distance matrix values.") - interpolation: str = Field(default="poly", description="Dense mesh interpolation method.") - return_dist: bool = Field(default=True, description="Return distance matrix from dense mesh processing.") - adapt_radius: bool = Field(default=True, description="Adapt geodesic radius during dense mesh processing.") - update_sample: bool = Field(default=True, description="Update sampling during dense mesh processing.") - force_n_samples: bool = Field(default=False, description="Force exactly the requested number of samples.") - verbose: bool = Field(default=True, description="Pass verbose output to dense mesh routines.") - - def process_params(self, n_cpus: int) -> dict[str, Any]: - data = self.dict() - data["n_jobs"] = n_cpus - return data diff --git a/shape_utils/similarity_scores.py b/shape_utils/similarity_scores.py deleted file mode 100644 index 02d51d1..0000000 --- a/shape_utils/similarity_scores.py +++ /dev/null @@ -1,180 +0,0 @@ -import numpy as np -from numpy.linalg import inv -from torch import FloatTensor, LongTensor -from shape_utils.models import SimpleEuclideanModel, NeuralNetworkModel -import os -import logging -import math -import torch -import torch.nn as nn -import torch.nn.functional as F -import torch.optim as optim -from itertools import combinations_with_replacement - -def calculate_geodesic_norm_score(FM): - """ - Calculates norm of correspondance matrix based on geodesic distance of eigenvalues spectrum - - Returns norm as a similarity score - - Args: - FM : Correspondance matrix of functional map - Returns: - """ - - eigenvalues_FM = np.linalg.eigvals(FM) - eps = 1e-12 # avoid log(0) - score = np.sqrt(np.sum(np.log(np.absolute(np.real(eigenvalues_FM))+ eps) ** 2)) - - return score - -def get_pairs_fast(arr): - """ - Generate all unique pairs (including self-pairs) from a list or array. - - Args: - arr (list or array-like): Input list or array of elements. - - Returns: - list of tuples: A list containing tuples of all unique pairs. Each tuple - is of the form `(a, b)` where `a` and `b` are elements - from `arr`. - - Example: - get_pairs_fast([1, 2, 3]) - [(1, 1), (1, 2), (1, 3), (2, 2), (2, 3), (3, 3)] - """ - - return list(combinations_with_replacement(arr, 2)) - -def get_pairs(arr): - pairs = [] - for i in range(len(arr)): - for j in range(i, len(arr)): - if i <= j: - pairs.append((arr[i], arr[j])) - return pairs - -def read_inv(fn): - vectors = [] - f = open(fn, 'r') - for line in f: - vectors.append(float(line.strip())) - f.close() - return vectors[1::] - -def read_dataset(input_dir,db_structures, atom_type): - dataset = {} - - for struct in db_structures: - - if atom_type == 'mainchain': - descriptors_file = os.path.join(input_dir,struct+ '_cacn.inv') - _3dzd = read_inv(descriptors_file) - #_3dzd = read_inv(input_dir + struct + '_cacn.inv') - else: - descriptors_file = os.path.join(input_dir,struct+ '.inv') - _3dzd = read_inv(descriptors_file) - #_3dzd = read_inv(input_dir + struct + '.inv') - - #_vertex = read_ply(input_dir + struct + '.ply') - data = {} - data['_3dzd'] = _3dzd - #data['vertex_face'] = _vertex - - dataset[struct] = data - return dataset - -def pairs_to_features(pairs,alpha_data,scope_data): - - _3DZD_vectors_1, _3DZD_vectors_2 = [], [] - #element_vertices_1, element_vertices_2 = [], [] - #element_faces_1, element_faces_2 = [], [] - for _pair in pairs: - id_0, id_1 = str(_pair[0]), str(_pair[1]) - - _3dzd_1 = list(alpha_data[id_0]['_3dzd']) - _3dzd_2 = list(scope_data[id_1]['_3dzd']) - - - _3DZD_vector_1 = np.asarray(_3dzd_1) - _3DZD_vector_1 = np.expand_dims(_3DZD_vector_1.squeeze(), axis = 0) - _3DZD_vector_2 = np.asarray(_3dzd_2) - _3DZD_vector_2 = np.expand_dims(_3DZD_vector_2.squeeze(), axis = 0) - - #element_vertex_1, element_face_1 = tuple([int(x) for x in alpha_data[id_0]['vertex_face']]) - #element_vertex_2, element_face_2 = tuple([int(x) for x in scope_data[id_1]['vertex_face']]) - - # Update - _3DZD_vectors_1.append(_3DZD_vector_1) - _3DZD_vectors_2.append(_3DZD_vector_2) - #element_vertices_1.append(element_vertex_1) - #element_faces_1.append(element_face_1) - #element_vertices_2.append(element_vertex_2) - #element_faces_2.append(element_face_2) - _3DZD_vectors_1 = np.array(_3DZD_vectors_1) - _3DZD_vectors_2 = np.array(_3DZD_vectors_2) - _3DZD_vectors_1 = FloatTensor(_3DZD_vectors_1).squeeze() - #element_vertices_1 = FloatTensor(element_vertices_1) - #element_faces_1 = FloatTensor(element_faces_1) - - _3DZD_vectors_2 = FloatTensor(_3DZD_vectors_2).squeeze() - #element_vertices_2 = FloatTensor(element_vertices_2) - #element_faces_2 = FloatTensor(element_faces_2) - - #vertices_diff = torch.abs(element_vertices_1 - element_vertices_2).unsqueeze(1) - #faces_diff = torch.abs(element_faces_1 - element_faces_2).unsqueeze(1) - #extra_features = torch.cat([vertices_diff, faces_diff], dim = 1) - - return _3DZD_vectors_1, _3DZD_vectors_2 - -def predict_similarity_zernike(input_dir,output_dir,model_type='simple_euclidean_model', atom_type='fullatom',cuda='true',device_id='0'): - - if cuda == 'true' and torch.cuda.is_available(): - cuda = True - device_id = device_id - #device_id = args.device_id - else: - cuda = False - device_id = torch.device("cpu") - - #model_type = 'neural_network' - atom_type = atom_type - if model_type == 'neural_network': - logging.info("neural network of Kihara not yet implemented") - - elif model_type == 'simple_euclidean_model': - logging.info("Simple Euclidean Model") - model = SimpleEuclideanModel() - model.eval() - db_structures = [x for x in os.listdir(input_dir) if '.inv' in x and '_cacn' not in x] - db_structures = [x.split('.')[0] for x in db_structures] - logging.info('pdb to compare : {} '.format(len(db_structures))) - - if len(db_structures) == 0: - logging.info("There are no structures to compare.") - exit() - - database_dataset = read_dataset(input_dir,db_structures,atom_type) - #query_pdb = db_structures[0] - my_pairs = get_pairs_fast(db_structures) - #my_pairs = [(query_pdb, j) for j in db_structures] - inputs_1, inputs_2 = pairs_to_features(my_pairs,database_dataset,database_dataset) - if cuda: - inputs_1 = inputs_1.cuda(device_id) - inputs_2 = inputs_2.cuda(device_id) - #extra_features = extra_features.cuda(device_id) - - if model_type == 'simple_euclidean_model': - outputs = model(inputs_1, inputs_2, True) - outputs = outputs.squeeze().cpu().data.numpy().tolist() - - output_scores_file = os.path.join(output_dir,atom_type+'_prediction.txt') - with open(output_scores_file,'w') as fh: - fh.write('Query\tTarget\tDis-similarity Probability\n') - #for i in range(0,len(outputs)): - for pair,score in zip(my_pairs,outputs): - fh.write(pair[0] + '\t' + pair[1] + '\t' +str(round(score,3)) + '\n') - #fh.write(db_structures[0] + '\t' + db_structures[i] + '\t' +str(round(outputs[i],3)) + '\n') - - diff --git a/shape_utils/spectral_descr.py b/shape_utils/spectral_descr.py deleted file mode 100644 index fb72fb9..0000000 --- a/shape_utils/spectral_descr.py +++ /dev/null @@ -1,69 +0,0 @@ -import numpy as np -import logging -import os -from pyFM import functional - - -logger = logging.getLogger() - -def calculate_spectral_descriptors(mesh,kprocess,n_ev,ndescr,step,landmarks,descr_type='WKS'): - """ - Calculate Wave Kernel Signatures and Heat Kernel Signatures on triangulated meshes using pyFM (https://github.com/RobinMagnet/pyFM) - - Returns list of WKS descriptors for each mesh - - Args: - mesh : Trimesh mesh object - kprocess (int) : No. of eigenvalues to use - n_ev : the least number of Laplacian eigenvalues to consider - ndescr (int) : No. of descriptors to include - step (int) : sub-sample step, in order to not use too many descriptors - landmarks : (p,1|2) array of indices of landmarks to match. - If (p,1) uses the same indices for both. - descr_type : Descriptor type : WKS (default), HKS or Zernike - - """ - - process_params = { - 'n_ev': (n_ev,n_ev), # n_ev: (k1, k2) tuple - with the least number of Laplacian eigenvalues to consider. - 'subsample_step': int(step), # In order not to use too many descriptors - 'descr_type': descr_type, # WKS or HKS - 'k_process' : int(kprocess), # No. of eigenvalues/eigenvectors to compute - 'n_descr': int(ndescr), # - 'landmarks': landmarks - } - - descr_model = functional.SpectralDescriptors(mesh) - - #preprocess functional mapping and compute descriptors - descr_model.preprocess_descriptors_mesh(**process_params,verbose=True) - - #wks descriptors for surface mesh (mesh1 and mesh) - - descr = descr_model.descr - - return descr - - -def distance_WKS(wks1,wks2): - """ - Compute distance between two descriptors maps - Returns file with distance - - wks1 : list of wks descriptors for mesh1 - wks2 : list of wks descriptors for mesh2 - - """ - distance_wks = [] - for i,j in zip(wks1,wks2): - coef_sum = 0.0 - for wks_e1,wks_e2 in zip(i,j): - denom = wks_e1 + wks_e2 - if denom != 0: - if abs((wks_e1-wks_e2)/(wks_e1+wks_e2)) > 1.0: - logging.info('not equal {} and {}'.format(wks_e1, wks_e2)) - coef = abs(wks_e1-wks_e2) - coef_sum += coef - distance_wks.append(coef_sum) - return distance_wks - diff --git a/shape_utils/utils.py b/shape_utils/utils.py deleted file mode 100644 index abe8d99..0000000 --- a/shape_utils/utils.py +++ /dev/null @@ -1,88 +0,0 @@ -from __future__ import annotations - -import logging -from typing import Any - -import pandas as pd -import numpy as np -from scipy.spatial import KDTree - - -logger = logging.getLogger(__name__) - -def save_data_to_csv(data: np.ndarray, output_file: str) -> None: - """Dump matrix data into a output csv file. - Args: - data (dict): The data (for spectral descriptors or functional maps) to be dumped. - output_file (str): The path to the output file. - - Raises: - ValueError: If `data` is empty. - """ - if len(data) != 0 : - - try: - - descr_data=tuple(map(tuple, data)) - df = pd.DataFrame(descr_data) - df.to_csv(output_file, index=False,header=None) - - #return df - - - except Exception as e: - logger.error("Invalid data frame for wks descriptors: probably wrong fields in the data") - logger.error(e) - else: - logger.info("No data found to save") - - return None - -def save_list_to_csv(data: Any, output_file: str) -> None: - """Dump list into a csv output file. - Args: - data (dict): The data for list of parameters to be dumped. - output_file (str): The path to the output file. - - Raises: - ValueError: If `data` is empty. - """ - if len(data) != 0 : - - try: - df = pd.DataFrame(data) - df.to_csv(output_file, index=False,header=None) - - except Exception as e: - logger.error("Invalid data frame list of parameters: probably wrong fields in the data") - logger.error(e) - else: - logger.info("No data found to save") - - return None - - -def find_minimum_distance_meshes(mesh1: Any, mesh2: Any) -> float: - """ - Compute the minimum Euclidean distance between two meshes. - - Args: - mesh1: A mesh object with a `vertlist` attribute containing vertex coordinates - as an (N, 3) array. - mesh2: A mesh object with a `vertlist` attribute containing vertex coordinates - as an (M, 3) array. - - Returns: - float: The minimum Euclidean distance between any vertex of `mesh1` and any - vertex of `mesh2`. - """ - # Get the vertices of each mesh - vertices1 = mesh1.vertlist - vertices2 = mesh2.vertlist - - # Use a KDTree for efficient nearest neighbor search - tree = KDTree(vertices2) - distances, _ = tree.query(vertices1) - - # Return the minimum distance - return float(np.min(distances)) diff --git a/tests/test_analysis_clustering.py b/tests/test_analysis_clustering.py index f6a391e..949b2b5 100644 --- a/tests/test_analysis_clustering.py +++ b/tests/test_analysis_clustering.py @@ -25,7 +25,14 @@ class TestClusteringHelpers(unittest.TestCase): def test_pair_helpers(self): entries = ["a", "b", "c"] - expected = [("a", "a"), ("a", "b"), ("a", "c"), ("b", "b"), ("b", "c"), ("c", "c")] + expected = [ + ("a", "a"), + ("a", "b"), + ("a", "c"), + ("b", "b"), + ("b", "c"), + ("c", "c"), + ] self.assertEqual(get_pairs(entries), expected) self.assertEqual(get_pairs_fast(entries), expected) @@ -55,7 +62,9 @@ def test_partial_score_matrix_filters_missing_pairs(self): entries.write_text("a\nb\nc\n") scores.write_text("a b 2\na outside 99\n") - matrix, labels = compute_partial_scores_matrix_fast(scores, entries, fill_value=-1.0) + matrix, labels = compute_partial_scores_matrix_fast( + scores, entries, fill_value=-1.0 + ) expected = np.array([[-1.0, 2.0, -1.0], [2.0, -1.0, -1.0], [-1.0, -1.0, -1.0]]) npt.assert_array_equal(matrix, expected) @@ -91,7 +100,9 @@ def test_combined_score_matrix_rejects_bad_normalization(self): scores.write_text("a a 0\n") with self.assertRaises(ValueError): - compute_partial_scores_matrix_combined(scores, entries, normalize_spec="bad") + compute_partial_scores_matrix_combined( + scores, entries, normalize_spec="bad" + ) def test_linkage_and_cluster_helpers(self): data = np.array([[0.0], [0.1], [5.0], [5.2]]) @@ -113,7 +124,9 @@ def test_linkage_and_cluster_helpers(self): self.assertEqual(sorted(sum(clusters, [])), ["a", "b", "c", "d"]) def test_find_optimal_num_clusters_accepts_explicit_k(self): - model = AgglomerativeClustering(n_clusters=2, compute_distances=True).fit(np.array([[0.0], [1.0], [5.0]])) + model = AgglomerativeClustering(n_clusters=2, compute_distances=True).fit( + np.array([[0.0], [1.0], [5.0]]) + ) self.assertEqual(find_optimal_num_clusters(model, np.eye(3), k=2), 2) diff --git a/tests/test_analysis_geometry.py b/tests/test_analysis_geometry.py index 4c64a63..f673243 100644 --- a/tests/test_analysis_geometry.py +++ b/tests/test_analysis_geometry.py @@ -14,7 +14,9 @@ def test_optimal_rotation_translation_recovers_translation(self): points = np.array([[0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, 0.0]]) translated = points + np.array([[2.0], [3.0], [4.0]]) - rotation, translation = superposition.optimal_rotation_translation(points, translated) + rotation, translation = superposition.optimal_rotation_translation( + points, translated + ) npt.assert_allclose(rotation, np.eye(3), atol=1e-7) npt.assert_allclose(translation, np.array([[2.0], [3.0], [4.0]]), atol=1e-7) @@ -27,8 +29,15 @@ def test_calculate_rotation_translation_fixed_uses_one_based_map(self): vertices_1 = np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]]) vertices_2 = vertices_1.copy() - with patch("analysis_tools.superposition_shapes.read_vertices", return_value=(vertices_1, vertices_2)): - rotation, translation = superposition.calculate_rotation_translation_fixed("a.obj", "b.obj", map_file) + with patch( + "analysis_tools.superposition_shapes.read_vertices", + return_value=(vertices_1, vertices_2), + ): + rotation, translation = ( + superposition.calculate_rotation_translation_fixed( + "a.obj", "b.obj", map_file + ) + ) npt.assert_allclose(rotation, np.eye(3), atol=1e-7) npt.assert_allclose(translation, np.zeros((3, 1)), atol=1e-7) @@ -43,7 +52,9 @@ def test_calculate_rotation_translation_fixed_rejects_wrong_map_length(self): return_value=(np.zeros((3, 3)), np.zeros((3, 3))), ): with self.assertRaises(ValueError): - superposition.calculate_rotation_translation_fixed("a.obj", "b.obj", map_file) + superposition.calculate_rotation_translation_fixed( + "a.obj", "b.obj", map_file + ) def test_compute_aligned_meshes_builds_aligned_objects(self): class FakeTriMesh: @@ -57,8 +68,12 @@ def __init__(self, *args, **kwargs): self.vertlist, self.facelist = self.queue.pop(0) with patch("analysis_tools.superposition_shapes.mesh.TriMesh", FakeTriMesh): - aligned_1, aligned_2 = superposition.compute_aligned_meshes("a.obj", "b.obj", np.eye(3)) + aligned_1, aligned_2 = superposition.compute_aligned_meshes( + "a.obj", "b.obj", np.eye(3) + ) - npt.assert_allclose(aligned_1.vertlist.mean(axis=0), aligned_2.vertlist.mean(axis=0)) + npt.assert_allclose( + aligned_1.vertlist.mean(axis=0), aligned_2.vertlist.mean(axis=0) + ) npt.assert_array_equal(aligned_1.facelist, np.array([[0, 1, 1]])) npt.assert_array_equal(aligned_2.facelist, np.array([[0, 1, 1]])) diff --git a/tests/test_cli.py b/tests/test_cli.py index 144c89a..255973b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2,7 +2,7 @@ import unittest from unittest.mock import patch -from shape_utils.cli import ( +from shape_retrieval.cli import ( default_map2zernike_binary, default_obj2grid_binary, env_flag, @@ -11,11 +11,15 @@ class TestCliEnvironmentDefaults(unittest.TestCase): def test_map2zernike_binary_env_has_priority(self): - with patch.dict(os.environ, {"MAP2ZERNIKE_BINARY": "/tools/map2zernike"}, clear=True): + with patch.dict( + os.environ, {"MAP2ZERNIKE_BINARY": "/tools/map2zernike"}, clear=True + ): self.assertEqual(default_map2zernike_binary(), "/tools/map2zernike") def test_map2zernike_setup_dir_appends_binary_name(self): - with patch.dict(os.environ, {"MAP2ZERNIKE_SETUP_DIR": "/tools/bin"}, clear=True): + with patch.dict( + os.environ, {"MAP2ZERNIKE_SETUP_DIR": "/tools/bin"}, clear=True + ): self.assertEqual(default_map2zernike_binary(), "/tools/bin/map2zernike") def test_obj2grid_binary_env_has_priority(self): @@ -23,7 +27,9 @@ def test_obj2grid_binary_env_has_priority(self): self.assertEqual(default_obj2grid_binary(), "/tools/obj2grid") def test_obj2grid_path_accepts_direct_binary_path(self): - with patch.dict(os.environ, {"OBJ2GRID_PATH": "/tools/bin/obj2grid"}, clear=True): + with patch.dict( + os.environ, {"OBJ2GRID_PATH": "/tools/bin/obj2grid"}, clear=True + ): self.assertEqual(default_obj2grid_binary(), "/tools/bin/obj2grid") def test_obj2grid_path_uses_last_path_entry(self): @@ -39,7 +45,9 @@ def test_defaults_fall_back_to_command_names(self): def test_env_flag_truthy_values(self): for value in ("1", "true", "yes", "on", "TRUE"): with self.subTest(value=value): - with patch.dict(os.environ, {"SHAPE_RETRIEVAL_PROFILE": value}, clear=True): + with patch.dict( + os.environ, {"SHAPE_RETRIEVAL_PROFILE": value}, clear=True + ): self.assertTrue(env_flag("SHAPE_RETRIEVAL_PROFILE")) def test_env_flag_false_when_unset(self): diff --git a/tests/test_config.py b/tests/test_config.py index 224410f..988df58 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -2,7 +2,7 @@ from pydantic import ValidationError -from shape_utils.config import DenseMeshConfig, FunctionalMapConfig +from shape_retrieval.config import DenseMeshConfig, FunctionalMapConfig class TestFunctionalMapConfig(unittest.TestCase): diff --git a/tests/test_fuctional_maps.py b/tests/test_fuctional_maps.py index 8059d7e..a3f3ea3 100644 --- a/tests/test_fuctional_maps.py +++ b/tests/test_fuctional_maps.py @@ -4,8 +4,8 @@ import numpy as np import numpy.testing as npt -from shape_utils.config import FunctionalMapConfig -from shape_utils.functional_maps import ( +from shape_retrieval.config import FunctionalMapConfig +from shape_retrieval.functional_maps import ( calculate_functional_maps, calculate_functional_maps_chem, calculate_p2p_map, @@ -53,12 +53,16 @@ def test_calculate_functional_maps_preserves_old_n_cpus_refine_call_style(self): self.mock_model.icp_refine.assert_called_once_with(n_jobs=3, verbose=True) def test_calculate_functional_maps_zoomout_uses_config(self): - config = FunctionalMapConfig(refine="zoomout", zoomout_nit=5, zoomout_step=2, verbose=False) + config = FunctionalMapConfig( + refine="zoomout", zoomout_nit=5, zoomout_step=2, verbose=False + ) calculate_functional_maps(self.mock_model, config) self.mock_model.change_FM_type.assert_called_once_with("classic") - self.mock_model.zoomout_refine.assert_called_once_with(nit=5, step=2, verbose=False) + self.mock_model.zoomout_refine.assert_called_once_with( + nit=5, step=2, verbose=False + ) def test_calculate_p2p_map(self): result = calculate_p2p_map(self.mock_model, n_cpus=7) diff --git a/tests/test_meshes.py b/tests/test_meshes.py index a5f3fca..ca6e9b8 100644 --- a/tests/test_meshes.py +++ b/tests/test_meshes.py @@ -1,11 +1,19 @@ import os -import numpy as np import unittest -from unittest.mock import MagicMock -from shape_utils.meshes import fix_mesh, reduce_resolution_mesh, reconstruct_mesh, compute_center_of_mass, compute_min_dist, compute_centers_dist, remove_until_vertex -import pytest -from unittest.mock import MagicMock, patch from tempfile import NamedTemporaryFile +from unittest.mock import MagicMock, patch + +import numpy as np + +from shape_retrieval.meshes import ( + compute_center_of_mass, + compute_centers_dist, + compute_min_dist, + fix_mesh, + reduce_resolution_mesh, + remove_until_vertex, +) + class MockMesh: def __init__(self): @@ -22,6 +30,7 @@ def face_matrix(self): def vertex_number(self): return len(self._v) + class MockMeshSet: def __init__(self): self._mesh = MockMesh() @@ -55,12 +64,12 @@ def apply_filter(self, *args, **kwargs): class TestMeshes(unittest.TestCase): - @patch("shape_utils.meshes.ml.MeshSet", return_value=MockMeshSet()) - @patch("shape_utils.meshes.pymeshfix.MeshFix") - def test_fix_mesh_basic(self,mock_meshfix, mock_ms): + @patch("shape_retrieval.meshes.ml.MeshSet", return_value=MockMeshSet()) + @patch("shape_retrieval.meshes.pymeshfix.MeshFix") + def test_fix_mesh_basic(self, mock_meshfix, mock_ms): mock_obj = MagicMock() - mock_obj.points = np.array([[0,0,0],[1,1,1]]) - mock_obj.faces = np.array([[0,1,1]]) + mock_obj.points = np.array([[0, 0, 0], [1, 1, 1]]) + mock_obj.faces = np.array([[0, 1, 1]]) mock_meshfix.return_value = mock_obj v, f = fix_mesh("fake.obj", resolution=0.5) @@ -80,12 +89,9 @@ def test_compute_center_of_mass(self): center = compute_center_of_mass(mesh) assert np.allclose(center, np.array([0.5, 0.5, 0.5])) - - - - @patch("shape_utils.meshes.trimesh.load_mesh") - @patch("shape_utils.meshes.KDTree") - def test_compute_min_dist(self,mock_kdtree, mock_load): + @patch("shape_retrieval.meshes.trimesh.load_mesh") + @patch("shape_retrieval.meshes.KDTree") + def test_compute_min_dist(self, mock_kdtree, mock_load): mesh1 = MagicMock() mesh1.vertices = np.array([[0, 0, 0]]) mesh2 = MagicMock() @@ -100,11 +106,8 @@ def test_compute_min_dist(self,mock_kdtree, mock_load): d = compute_min_dist("a.obj", "b.obj") assert d == 1.0 - - - - @patch("shape_utils.meshes.trimesh.load_mesh") - def test_compute_centers_dist(self,mock_load): + @patch("shape_retrieval.meshes.trimesh.load_mesh") + def test_compute_centers_dist(self, mock_load): mesh1 = MagicMock() mesh1.center_mass = np.array([0, 0, 0]) mesh2 = MagicMock() @@ -115,15 +118,8 @@ def test_compute_centers_dist(self,mock_load): d = compute_centers_dist("a.obj", "b.obj") assert d == 5.0 # distance between (0,0,0) and (3,4,0) - - def test_remove_until_vertex(self): - content = [ - "junk line\n", - "! more junk\n", - "v 1 2 3\n", - "v 4 5 6\n" - ] + content = ["junk line\n", "! more junk\n", "v 1 2 3\n", "v 4 5 6\n"] with NamedTemporaryFile("w+", delete=False) as tmp: tmp.write("".join(content)) @@ -136,4 +132,4 @@ def test_remove_until_vertex(self): os.remove(tmp_path) - assert result == "v 1 2 3\nv 4 5 6\n" \ No newline at end of file + assert result == "v 1 2 3\nv 4 5 6\n" diff --git a/tests/test_models_similarity.py b/tests/test_models_similarity.py index e058c1d..0802370 100644 --- a/tests/test_models_similarity.py +++ b/tests/test_models_similarity.py @@ -6,8 +6,8 @@ import numpy.testing as npt import torch -from shape_utils.models import NeuralNetworkModel, SimpleEuclideanModel -from shape_utils.similarity_scores import ( +from shape_retrieval.models import NeuralNetworkModel, SimpleEuclideanModel +from shape_retrieval.similarity_scores import ( get_pairs, get_pairs_fast, pairs_to_features, @@ -27,10 +27,14 @@ def test_simple_euclidean_model_returns_distance_or_similarity(self): similarities = model(inputs_1, inputs_2, output_dist=False) npt.assert_allclose(distances.detach().numpy(), np.array([5.0, 0.0])) - npt.assert_allclose(similarities.detach().numpy(), np.array([1 / 6, 1.0]), rtol=1e-6) + npt.assert_allclose( + similarities.detach().numpy(), np.array([1 / 6, 1.0]), rtol=1e-6 + ) def test_neural_network_model_forward_shapes(self): - model = NeuralNetworkModel(input_dim=2, hidden_dims=[3], fc_dims=[4], extra_feature_dim=1) + model = NeuralNetworkModel( + input_dim=2, hidden_dims=[3], fc_dims=[4], extra_feature_dim=1 + ) model.eval() inputs_1 = torch.ones((2, 2)) inputs_2 = torch.zeros((2, 2)) @@ -48,7 +52,9 @@ def test_neural_network_model_forward_shapes(self): class TestSimilarityScoresIO(unittest.TestCase): def test_pair_helpers_and_read_inv(self): self.assertEqual(get_pairs(["a", "b"]), [("a", "a"), ("a", "b"), ("b", "b")]) - self.assertEqual(get_pairs_fast(["a", "b"]), [("a", "a"), ("a", "b"), ("b", "b")]) + self.assertEqual( + get_pairs_fast(["a", "b"]), [("a", "a"), ("a", "b"), ("b", "b")] + ) with tempfile.TemporaryDirectory() as tmpdir: inv_file = Path(tmpdir) / "a.inv" diff --git a/tests/test_pipeline.py b/tests/test_pipeline.py index 9f8bc75..8c00337 100644 --- a/tests/test_pipeline.py +++ b/tests/test_pipeline.py @@ -3,7 +3,12 @@ from pathlib import Path from unittest.mock import patch -from shape_utils.pipeline import PipelineConfig, resolve_executable, run_pipeline, validate_config +from shape_retrieval.pipeline import ( + PipelineConfig, + resolve_executable, + run_pipeline, + validate_config, +) class TestPipelineValidation(unittest.TestCase): @@ -15,7 +20,11 @@ def test_validate_config_creates_output_directory(self): mesh1.write_text("v 0 0 0\n") mesh2.write_text("OFF\n") - validate_config(PipelineConfig(mesh1=mesh1, mesh2=mesh2, entry_ids=("a", "b"), output=output)) + validate_config( + PipelineConfig( + mesh1=mesh1, mesh2=mesh2, entry_ids=("a", "b"), output=output + ) + ) self.assertTrue(output.is_dir()) @@ -26,7 +35,14 @@ def test_validate_config_rejects_missing_mesh(self): mesh2.write_text("v 0 0 0\n") with self.assertRaises(FileNotFoundError): - validate_config(PipelineConfig(mesh1=mesh1, mesh2=mesh2, entry_ids=("a", "b"), output=Path(tmpdir))) + validate_config( + PipelineConfig( + mesh1=mesh1, + mesh2=mesh2, + entry_ids=("a", "b"), + output=Path(tmpdir), + ) + ) def test_validate_config_rejects_invalid_descriptor(self): with tempfile.TemporaryDirectory() as tmpdir: @@ -37,7 +53,13 @@ def test_validate_config_rejects_invalid_descriptor(self): with self.assertRaises(ValueError): validate_config( - PipelineConfig(mesh1=mesh1, mesh2=mesh2, entry_ids=("a", "b"), output=Path(tmpdir), descriptor="BAD") + PipelineConfig( + mesh1=mesh1, + mesh2=mesh2, + entry_ids=("a", "b"), + output=Path(tmpdir), + descriptor="BAD", + ) ) def test_validate_config_rejects_fix_mesh_dependent_flags(self): @@ -69,11 +91,13 @@ def test_resolve_executable_accepts_executable_path(self): self.assertEqual(resolve_executable(str(binary), "tool"), str(binary)) def test_resolve_executable_uses_path_lookup(self): - with patch("shape_utils.pipeline.shutil.which", return_value="/usr/local/bin/tool"): + with patch( + "shape_retrieval.pipeline.shutil.which", return_value="/usr/local/bin/tool" + ): self.assertEqual(resolve_executable("tool", "tool"), "/usr/local/bin/tool") def test_resolve_executable_raises_when_missing(self): - with patch("shape_utils.pipeline.shutil.which", return_value=None): + with patch("shape_retrieval.pipeline.shutil.which", return_value=None): with self.assertRaises(FileNotFoundError): resolve_executable("missing-tool", "missing-tool") @@ -99,7 +123,9 @@ def test_run_pipeline_dispatches_spectral_descriptor(self): with tempfile.TemporaryDirectory() as tmpdir: config = self._config(tmpdir, descriptor="WKS") - with patch("shape_utils.pipeline._run_spectral_descriptors") as mock_spectral: + with patch( + "shape_retrieval.pipeline._run_spectral_descriptors" + ) as mock_spectral: run_pipeline(config) mock_spectral.assert_called_once_with(config, "WKS", None) @@ -108,7 +134,9 @@ def test_run_pipeline_dispatches_zernike_descriptor(self): with tempfile.TemporaryDirectory() as tmpdir: config = self._config(tmpdir, descriptor="3DZD") - with patch("shape_utils.pipeline._run_zernike_descriptors") as mock_zernike: + with patch( + "shape_retrieval.pipeline._run_zernike_descriptors" + ) as mock_zernike: run_pipeline(config) mock_zernike.assert_called_once_with(config, None) @@ -117,7 +145,9 @@ def test_run_pipeline_returns_early_when_shape_retrieval_disabled(self): with tempfile.TemporaryDirectory() as tmpdir: config = self._config(tmpdir, no_shape_retrieval=True) - with patch("shape_utils.pipeline._run_spectral_descriptors") as mock_spectral: + with patch( + "shape_retrieval.pipeline._run_spectral_descriptors" + ) as mock_spectral: run_pipeline(config) mock_spectral.assert_not_called() @@ -126,7 +156,9 @@ def test_run_pipeline_calculates_min_distance_before_early_return(self): with tempfile.TemporaryDirectory() as tmpdir: config = self._config(tmpdir, min_dist_mesh=True, no_shape_retrieval=True) - with patch("shape_utils.pipeline._calculate_minimum_distance") as mock_min_distance: + with patch( + "shape_retrieval.pipeline._calculate_minimum_distance" + ) as mock_min_distance: run_pipeline(config) mock_min_distance.assert_called_once_with(config, None) @@ -136,8 +168,12 @@ def test_run_pipeline_passes_fixed_meshes_to_spectral_branch(self): config = self._config(tmpdir, fix_meshes=True) fixed_meshes = object() - with patch("shape_utils.pipeline._fix_meshes", return_value=fixed_meshes) as mock_fix_meshes: - with patch("shape_utils.pipeline._run_spectral_descriptors") as mock_spectral: + with patch( + "shape_retrieval.pipeline._fix_meshes", return_value=fixed_meshes + ) as mock_fix_meshes: + with patch( + "shape_retrieval.pipeline._run_spectral_descriptors" + ) as mock_spectral: run_pipeline(config) mock_fix_meshes.assert_called_once_with(config) diff --git a/tests/test_scores.py b/tests/test_scores.py index 74199d3..441a3fa 100644 --- a/tests/test_scores.py +++ b/tests/test_scores.py @@ -1,12 +1,11 @@ import unittest + import numpy as np -import numpy.testing as npt -from shape_utils.similarity_scores import calculate_geodesic_norm_score +from shape_retrieval.similarity_scores import calculate_geodesic_norm_score class TestGeodesicNormScore(unittest.TestCase): - def test_identity_matrix(self): """ The identity matrix has eigenvalues = [1, 1, ..., 1] @@ -22,8 +21,8 @@ def test_diagonal_matrix(self): eigenvalues = [a, b] score = sqrt((log|a|)^2 + (log|b|)^2) """ - FM = np.diag([2.0, 0.5]) # eigenvalues = [2, 0.5] - expected = np.sqrt(np.log(2.0)**2 + np.log(0.5)**2) + FM = np.diag([2.0, 0.5]) # eigenvalues = [2, 0.5] + expected = np.sqrt(np.log(2.0) ** 2 + np.log(0.5) ** 2) score = calculate_geodesic_norm_score(FM) self.assertAlmostEqual(score, expected, places=7) @@ -32,8 +31,7 @@ def test_arbitrary_matrix(self): """ Check against a NumPy direct calculation for correctness. """ - FM = np.array([[3.0, 1.0], - [0.0, 2.0]]) # eigenvalues = [3, 2] + FM = np.array([[3.0, 1.0], [0.0, 2.0]]) # eigenvalues = [3, 2] eigenvals = np.linalg.eigvals(FM) expected = np.sqrt(np.sum(np.log(np.abs(np.real(eigenvals))) ** 2)) @@ -45,13 +43,11 @@ def test_complex_eigenvalues(self): """ Matrix with complex eigenvalues should use real part in absolute(). """ - FM = np.array([[0, -1], - [1, 0]]) # eigenvalues = ±i + FM = np.array([[0, -1], [1, 0]]) # eigenvalues = ±i eigenvals = np.linalg.eigvals(FM) - eps= 1e-12 # avoid log(0) - expected = np.sqrt(np.sum(np.log(np.abs(np.real(eigenvals))+ eps)**2)) + eps = 1e-12 # avoid log(0) + expected = np.sqrt(np.sum(np.log(np.abs(np.real(eigenvals)) + eps) ** 2)) score = calculate_geodesic_norm_score(FM) self.assertAlmostEqual(score, expected, places=7) - diff --git a/tests/test_utils.py b/tests/test_utils.py index c660962..35a347f 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,19 +1,19 @@ -import unittest -import tempfile import os +import tempfile +import unittest +from unittest.mock import MagicMock + import numpy as np import pandas as pd -from unittest.mock import MagicMock -from shape_utils.utils import ( +from shape_retrieval.utils import ( + find_minimum_distance_meshes, save_data_to_csv, save_list_to_csv, - find_minimum_distance_meshes, ) class TestUtils(unittest.TestCase): - # ------------------------------------------------------------ # save_data_to_csv # ------------------------------------------------------------ @@ -88,5 +88,3 @@ def test_find_minimum_distance_meshes(self): result = find_minimum_distance_meshes(mesh1, mesh2) self.assertAlmostEqual(result, expected, places=6) - - diff --git a/tests/test_wks_descriptors.py b/tests/test_wks_descriptors.py index 9bea76b..ae7e61c 100644 --- a/tests/test_wks_descriptors.py +++ b/tests/test_wks_descriptors.py @@ -1,10 +1,10 @@ import unittest from unittest.mock import MagicMock, patch -from shape_utils.spectral_descr import calculate_spectral_descriptors, distance_WKS +from shape_retrieval.spectral_descr import calculate_spectral_descriptors, distance_WKS -class TestDescriptorFunctions(unittest.TestCase): +class TestDescriptorFunctions(unittest.TestCase): def setUp(self): # Mock model self.mock_model = MagicMock() @@ -12,7 +12,7 @@ def setUp(self): self.mock_model.descr2 = [[1.5, 2.5, 3.5]] self.landmarks = [0, 1, 2] - @patch("shape_utils.spectral_descr.functional.SpectralDescriptors") + @patch("shape_retrieval.spectral_descr.functional.SpectralDescriptors") def test_calculate_spectral_descriptors_calls_preprocess(self, mock_descriptors): descr_model = MagicMock() descr_model.descr = [[1.0, 2.0, 3.0]] diff --git a/tests/test_zernike_descr.py b/tests/test_zernike_descr.py index 881ecde..bc3ee5e 100644 --- a/tests/test_zernike_descr.py +++ b/tests/test_zernike_descr.py @@ -1,10 +1,10 @@ +import subprocess import tempfile import unittest -import subprocess from pathlib import Path from unittest.mock import call, patch -from shape_utils.zernike_descr import get_inv, run_binary +from shape_retrieval.zernike_descr import get_inv, run_binary class Test3DZernike(unittest.TestCase): @@ -19,8 +19,12 @@ def fake_run(command, check, capture_output, text): Path(f"{command[1]}.inv").write_text("0\n1\n") return subprocess.CompletedProcess(command, 0, stdout="", stderr="") - with patch("shape_utils.zernike_descr.subprocess.run", side_effect=fake_run) as mock_run: - inv_file = get_inv(source_obj, "entry", "/bin/map2zernike", "/bin/obj2grid", tmp_path) + with patch( + "shape_retrieval.zernike_descr.subprocess.run", side_effect=fake_run + ) as mock_run: + inv_file = get_inv( + source_obj, "entry", "/bin/map2zernike", "/bin/obj2grid", tmp_path + ) self.assertEqual(inv_file, tmp_path / "entry.inv") self.assertEqual(inv_file.read_text(), "0\n1\n") @@ -36,7 +40,12 @@ def fake_run(command, check, capture_output, text): text=True, ), call( - ["/bin/map2zernike", str(tmp_path / "entry.obj.grid"), "-c", "0.5"], + [ + "/bin/map2zernike", + str(tmp_path / "entry.obj.grid"), + "-c", + "0.5", + ], check=True, capture_output=True, text=True, @@ -55,8 +64,12 @@ def fake_run(command, check, capture_output, text): Path(f"{command[1]}.inv").write_text("0\n1\n") return subprocess.CompletedProcess(command, 0, stdout="", stderr="") - with patch("shape_utils.zernike_descr.subprocess.run", side_effect=fake_run) as mock_run: - get_inv(source_obj, "entry", "/bin/map2zernike", "/bin/obj2grid", tmp_path) + with patch( + "shape_retrieval.zernike_descr.subprocess.run", side_effect=fake_run + ) as mock_run: + get_inv( + source_obj, "entry", "/bin/map2zernike", "/bin/obj2grid", tmp_path + ) temporary_obj = tmp_path / "entry_zernike_input.obj" mock_run.assert_any_call( @@ -76,11 +89,14 @@ def test_run_binary_includes_stderr_on_failure(self): stderr="specific failure", ) - with patch("shape_utils.zernike_descr.subprocess.run", side_effect=error): + with patch("shape_retrieval.zernike_descr.subprocess.run", side_effect=error): with self.assertRaisesRegex(RuntimeError, "specific failure"): run_binary(["/bin/tool"], "tool") def test_run_binary_wraps_os_error(self): - with patch("shape_utils.zernike_descr.subprocess.run", side_effect=OSError("missing executable")): + with patch( + "shape_retrieval.zernike_descr.subprocess.run", + side_effect=OSError("missing executable"), + ): with self.assertRaisesRegex(RuntimeError, "missing executable"): run_binary(["/bin/tool"], "tool") diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..a26e1f1 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1114 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/8b/adeb62ea8951f13c4c7fef2e7a85e1a06b499c8d8237ea589d496029e53f/coverage-7.15.0.tar.gz", hash = "sha256:9ac3fe7a1435986463eaa8ee253ae2f2a268709ba4ae5c7dd1f52a05391ad78f", size = 925362, upload-time = "2026-07-02T13:10:50.535Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/74/fd4c0901137c4f8d81a76ada99e43c65163b4c94a02ece107a4ec0c6b615/coverage-7.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b75ee5e8cb7575636ac598719b4307ac529ec8fcd79608a35c3cd4d4dada812d", size = 220838, upload-time = "2026-07-02T13:09:02.084Z" }, + { url = "https://files.pythonhosted.org/packages/0f/2e/2347583467bd7f0402635101a916961915cc68fce652cd0db5f173ea04fc/coverage-7.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffb31267816b93b075302248cc1737506081b4f163df4401e9df1a6424aafabe", size = 221197, upload-time = "2026-07-02T13:09:03.617Z" }, + { url = "https://files.pythonhosted.org/packages/f0/17/99fa688541ae1d6e84543a0e544f83de0c944815b63e9e7b1ed411d15036/coverage-7.15.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e4d0bb73455bf97ab243a8f12c37c686ccf1c13bb614b7b85f1d062f06f42b2c", size = 252705, upload-time = "2026-07-02T13:09:05.059Z" }, + { url = "https://files.pythonhosted.org/packages/fb/02/6a95a5cd83b74839017ef9cf48d2d8c9ae60af919e17a3f336e6f9f1b7bd/coverage-7.15.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:20d9ccc4ebd0edc434d86dfd2a1dd2a8efa6b6b3073d0485a394fee86459ebb4", size = 255441, upload-time = "2026-07-02T13:09:06.559Z" }, + { url = "https://files.pythonhosted.org/packages/67/f2/406f6c57d600f68185942422c4c00f1a3255d60aee6e5fd961425cd9987e/coverage-7.15.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20c8a976c365c8cb12f0cbd099508772ea41fb5fa80657a8506df0e11bd278c5", size = 256556, upload-time = "2026-07-02T13:09:08.197Z" }, + { url = "https://files.pythonhosted.org/packages/74/8e/d3fa48489c15ecdec1ba48fd61f68798555dddd2f6716f9ad42adeb1a2a9/coverage-7.15.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f948fd5ba1b9cbca91f0ae08b4c1ce2b139509149a435e2585d056d57d70bf01", size = 258815, upload-time = "2026-07-02T13:09:09.691Z" }, + { url = "https://files.pythonhosted.org/packages/47/2e/2d40ddd110462c6a2769677cf7f1c119a52b45f568978fc6c98e4cc0dd0f/coverage-7.15.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f58185f06edf6ad68ec9fb155d63ef650c82f3fbd7e1770e2867751fb13158f4", size = 253117, upload-time = "2026-07-02T13:09:11.212Z" }, + { url = "https://files.pythonhosted.org/packages/51/c0/310782f0d7c3cb2b5ac05ba8d205fe91f24a36f6bf3256098f1782181c38/coverage-7.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02adc79a920c73c647c5d117f55747df7f2de94571884758ce8bc58e04f0a796", size = 254475, upload-time = "2026-07-02T13:09:13.029Z" }, + { url = "https://files.pythonhosted.org/packages/86/f7/702da6c275f8ae6ade423d2877243122932c9b27f5403003b9ef8c927d12/coverage-7.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6eb7c300fbed667fd6e3588eba71c1904cdb06110ca6fdf908c26bdd88b8e382", size = 252619, upload-time = "2026-07-02T13:09:14.699Z" }, + { url = "https://files.pythonhosted.org/packages/fb/84/c5b15a7e5ecba4e56218d772d99fe80a63e63f8d11f12783723a6005ab45/coverage-7.15.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b5fb23fa2de9dce1f5c36c09066d8fcda16cd96e8e26686caa2d7cb9b567d65c", size = 256689, upload-time = "2026-07-02T13:09:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/95/2f/c8b07559b57701230c61b23a953858c052890c12ef568d81780c6c46e92e/coverage-7.15.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cec79341dbe6281484024979976d0c7f22beae08b4a254655decd25d42cbe766", size = 252189, upload-time = "2026-07-02T13:09:17.828Z" }, + { url = "https://files.pythonhosted.org/packages/6b/80/6d2f049dd3fd3dbfd60b62ba6b2162a04009e2c002ce70b24cf3878dec7a/coverage-7.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c664c5444b1d970b1b2a450e21fb19ee5c9cfdf151ded2dda37260031cca0da", size = 254059, upload-time = "2026-07-02T13:09:19.304Z" }, + { url = "https://files.pythonhosted.org/packages/ce/92/b0287a2c42031d25c628f815f89a3cd9f8268ee78bb1252c9356cda1c689/coverage-7.15.0-cp312-cp312-win32.whl", hash = "sha256:5f764a3fa339bde6b3aa97657f5a6a3a9451e4a5b4ea98a2892c773a43525f77", size = 222893, upload-time = "2026-07-02T13:09:20.812Z" }, + { url = "https://files.pythonhosted.org/packages/a9/69/e34c481915fecb499b3146975061dac528752e37706edc1804f32c822469/coverage-7.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:52f9a4d2c4c56c8848bc2f524916698354b0211488b38c49ad9ae54f6cafbff6", size = 223429, upload-time = "2026-07-02T13:09:22.315Z" }, + { url = "https://files.pythonhosted.org/packages/fe/98/6e878f0b571d32684ef3f38d7c03db241ca5b82a5da8a5391596a8f209c4/coverage-7.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:31e5c3e70c85307ea35a12964e2e40f56ca2ee4b1c8c721ccf4609d17071080b", size = 222810, upload-time = "2026-07-02T13:09:23.812Z" }, + { url = "https://files.pythonhosted.org/packages/76/04/145a3748098bcc86b631a85408d2c3dc5c104e0bd86d605468239b25b6c4/coverage-7.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5be4caf3b28836f078abe700f8944dac4a65d78f16d6c600c89cb624e5535782", size = 220863, upload-time = "2026-07-02T13:09:25.371Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5c/4ed55708fed2c64b63c9bc5715daef670872202101938869b7fe5d5fbb8f/coverage-7.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dd58ad1404704303ca8d4f4b8a1095e7cbc7040ef17a66df1e6619aa10176430", size = 221230, upload-time = "2026-07-02T13:09:26.897Z" }, + { url = "https://files.pythonhosted.org/packages/7b/19/3a80b97d3b2a5c77a01ae359c6bed20c13738fe3d9380f08616d4fec0281/coverage-7.15.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bbcbb317c2e5ded5b21104af81c29f391be2af98d065693ffbe8d23949b948e5", size = 252227, upload-time = "2026-07-02T13:09:28.543Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/b70062750686bd7da454da27927622f48bbac6990ac7a4c4a4653e7b0036/coverage-7.15.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:27f31ecb458da3f859aab3f15ada871eb7a7768807d88df4a9f186bb17737970", size = 254823, upload-time = "2026-07-02T13:09:30.177Z" }, + { url = "https://files.pythonhosted.org/packages/a9/09/dad6a75a2e561b9dc5086a8c5257a7591d584246f67e23e70d2995b89ab6/coverage-7.15.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fb759be317fdc62e0f56bffdf61cfcb45c7761ad6b71e3e583e71a67ae753c", size = 256059, upload-time = "2026-07-02T13:09:31.979Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e7/b5d2941fa9564573d44b693a871ff3156f0c42cbefe977a09fa7fdc59971/coverage-7.15.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5cf007add5ab4bb8fa9f4c77e3732127c9e6cad501d7db43355fbfafca0be84", size = 258190, upload-time = "2026-07-02T13:09:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/7c/1d/8e895bcde3c57ccd46d896dda5f2b3d5df761a1b0c6c9d450d175dedc632/coverage-7.15.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc78d9843bd576fbe2118248258d485e968dc535f95ed504a7b0867ba9b51389", size = 252456, upload-time = "2026-07-02T13:09:35.765Z" }, + { url = "https://files.pythonhosted.org/packages/14/4c/f6997da343ddeb959be82c3b05322793f92c071ad45f7cb8a96336e2dd5f/coverage-7.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a263060f1de0b4b74b4e089c2a70b8003b3781c733329a9c8fd54995328f9950", size = 254192, upload-time = "2026-07-02T13:09:37.445Z" }, + { url = "https://files.pythonhosted.org/packages/17/27/a0bc09d032267b9da89d95a2d874cfbef2a5aebbf0e87cf7aba221d79a99/coverage-7.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c48decf16e0dfd5b049c7d5e82200c23c08126719142998d4f172444e3d0529e", size = 252153, upload-time = "2026-07-02T13:09:39.422Z" }, + { url = "https://files.pythonhosted.org/packages/54/c0/77fc233d9fba07b244c40948c53fe27308b8f21732fb3417f87fbd6fd992/coverage-7.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:08fb028000ed0aaa0a4cbdfbb98be7cb42f370db973fbbb469733505ab20e13e", size = 256310, upload-time = "2026-07-02T13:09:41.006Z" }, + { url = "https://files.pythonhosted.org/packages/d5/24/601cecfb5825becacb8d45219a018a3b55b9dbaec624efdb0ea249d08be2/coverage-7.15.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb7dc0c3b7d8a1077abea0b8546ebc5e26d6ef6ecefc2f0f5ad2b8a53bdad837", size = 251974, upload-time = "2026-07-02T13:09:42.733Z" }, + { url = "https://files.pythonhosted.org/packages/47/1e/6f45e5a5b3d5484318d368702af6716b5ab8913b0428bec981a562fcf296/coverage-7.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cb3602054ccbe9f0d8c2dc04bbeba90d5719236e2cd06e042ddd6d3fc7b6e37", size = 253745, upload-time = "2026-07-02T13:09:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/8e/db/4df027a77bd11d0e527f44c53557c76e54ad027413d0304252ea3a78d67e/coverage-7.15.0-cp313-cp313-win32.whl", hash = "sha256:0bf781da64326b677be344df505171435b6f58716108606621d5d27d964fff8b", size = 222902, upload-time = "2026-07-02T13:09:46.122Z" }, + { url = "https://files.pythonhosted.org/packages/a0/10/0355894d34e231f2c5449e71287e81a50793a325df2e2b027b7bcd9dfd19/coverage-7.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:2c57a275078ee3fa185f83e400f765bc764a549de66d99b47881645cbd4ea629", size = 223444, upload-time = "2026-07-02T13:09:47.687Z" }, + { url = "https://files.pythonhosted.org/packages/06/ef/bb725f263befaaff851203ab338e68af15e195d7f7b5f323162532d9b6a8/coverage-7.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:3812c61afc6685c7999b39320779ab8f43b7a3081fdb0def39976e56fbdb9a21", size = 222839, upload-time = "2026-07-02T13:09:49.717Z" }, + { url = "https://files.pythonhosted.org/packages/4f/9c/1e3ca54f72a3185ece06c58d871099898c48f0ed6430d17b6ab75f0d180a/coverage-7.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:41cb79af843222e11da87127ad0ecbfa878abadd0f770a4a99391a27d3887324", size = 220906, upload-time = "2026-07-02T13:09:51.339Z" }, + { url = "https://files.pythonhosted.org/packages/09/37/f718613d83b274880382f6b67e78f3802549ae39b0b3e65ae5b5974df56e/coverage-7.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7d2008989ef8fe54188d3f3bfa2e3099b025af11e90a6a1b9e7dc433d04263d8", size = 221239, upload-time = "2026-07-02T13:09:53.138Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ce/22bae91e0b75445f68d365c7643ed0aa4880bbf77450ee74ca65bdae53a7/coverage-7.15.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:769e8ece11a596315ebf5aa7ec383aeeed016c091d2bf6363ffb996d41529092", size = 252286, upload-time = "2026-07-02T13:09:54.996Z" }, + { url = "https://files.pythonhosted.org/packages/dd/1e/bec5e32aa508615d9d7a2790effb25fb4dc28606e995816afe400b25ece3/coverage-7.15.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:65a6b6164ee5c39e2f3803f314292d6c61a607ba7fee253d1e03c42dc3903502", size = 254789, upload-time = "2026-07-02T13:09:56.678Z" }, + { url = "https://files.pythonhosted.org/packages/17/29/0e865435b4354e4a7c03b1b7920046d31d0a273d55decefea27e011cb9bf/coverage-7.15.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75128817f95a5c45bb01d65fd2d8b9cb54bbe03d81608fb70e3e14b437ad56c2", size = 256135, upload-time = "2026-07-02T13:09:58.343Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/33a870b58a13325d62fc0a6c8f01fa0ff667cef60c7498e2382a147dfa18/coverage-7.15.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9887bb428fe2d4cd4bee89bac1a6c9932f484afd5b36fbd4ff6ea5f825bb1f5e", size = 258449, upload-time = "2026-07-02T13:10:00.057Z" }, + { url = "https://files.pythonhosted.org/packages/18/7b/6fffe596bf3ddba8462758d02c5dad730fd91055a6634aa2e4226229181a/coverage-7.15.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0bfc0be1f702042207a93a00523b1065ee1fe951e96edf311581c0bbc2e34888", size = 252313, upload-time = "2026-07-02T13:10:01.946Z" }, + { url = "https://files.pythonhosted.org/packages/58/1b/11468dd6c1676ab831a70cb9a8d4e198e8607fa0b7220ab918b73fe9bfbd/coverage-7.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f64627d55def5a43282d70e08396672692f77e4da610a5bb8bb4060b432b6859", size = 254142, upload-time = "2026-07-02T13:10:04.065Z" }, + { url = "https://files.pythonhosted.org/packages/79/41/29328e21d16b1b95092c30dd700e08cf915bd3734f836df8f3bdb0e8fa9f/coverage-7.15.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2c6f0fa473003905c6d5bac328ee4eba9fbea654f15bc24b8a3274b23363fa99", size = 252108, upload-time = "2026-07-02T13:10:06.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/de/05ccfb990439655b35afbfd8e0d13fe66677565a7d4eb38c3f5ef2635e1c/coverage-7.15.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2bcf9afaf064172c6ec3c58a325a9957ad1178c05dd934e25f253321776e0676", size = 256385, upload-time = "2026-07-02T13:10:08.141Z" }, + { url = "https://files.pythonhosted.org/packages/51/0e/486828a3d2695ea7a2609f17ff572f6b01905e608379440a11da4b8dffbe/coverage-7.15.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:baf06bc987115d6fb938d403f7eab684a057766c490367999a2b71a6883110c6", size = 251923, upload-time = "2026-07-02T13:10:10.179Z" }, + { url = "https://files.pythonhosted.org/packages/18/c7/03582b6715f078e5e558354c87616d945b9894cda2dace8e4009b17035e4/coverage-7.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f0405f2ff97b1c4c0e782cb32e02f32369bcf2e6b618b591d67e1ea754575dfe", size = 253580, upload-time = "2026-07-02T13:10:12.052Z" }, + { url = "https://files.pythonhosted.org/packages/db/dc/9e578bbaf2ecb4959a81b7e7601ad8cca772cba2892e8d144cb749b4a71a/coverage-7.15.0-cp314-cp314-win32.whl", hash = "sha256:ab282853ed5fbd64bbb162f19cb8fcb7087187508a6374b4f9c34ec1577c4e8f", size = 223107, upload-time = "2026-07-02T13:10:13.994Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3e/c8c3b75d8dbe0e35f7b0cc3ff5e949fc59500f70b21d0398813f66740664/coverage-7.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bb3040e9f4bbe26fcb0cd7cc85ac63e630d3f3a9c74f027abf4caa27e706663", size = 223597, upload-time = "2026-07-02T13:10:15.906Z" }, + { url = "https://files.pythonhosted.org/packages/cd/bc/3cbc9fb036eb388519bccd521f783499c39b64256013fbc362782f196fe1/coverage-7.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:346771144d34f7fa84ec28386f78e0f31653f33cf35e19d253d5b35f9e8201da", size = 223020, upload-time = "2026-07-02T13:10:17.844Z" }, + { url = "https://files.pythonhosted.org/packages/28/00/199c4a8d656dff63102577a056c0fce2ff6a79e40adac092fc986c49cbf1/coverage-7.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d34a010905fb6401324ba016b5da03d574967f7b21ce48ea41e66f0f1f95f641", size = 221638, upload-time = "2026-07-02T13:10:19.703Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8e/9d0092c96a3d3a26951ecc7020826aa57bcb1b119ca81acbba996884ab13/coverage-7.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bb25d825d885ca8036795dacfc3924d33091fc76d71ebc99420c6b79e77d96fa", size = 221903, upload-time = "2026-07-02T13:10:21.514Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b4/c0ca3028f42c9a08e51feb4561ef1192e5de99797cd1db5b04590c215bda/coverage-7.15.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:94c9686bfe8a9a6810297aecbd99beaa3445f9e8dc2f80b1382cca0d86b64461", size = 263267, upload-time = "2026-07-02T13:10:23.261Z" }, + { url = "https://files.pythonhosted.org/packages/5f/aa/a375e3846e5d3c013dc600b2a3231089055c73d77f5393dd2192a8d64da6/coverage-7.15.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9bd671c25f9d85f09d7ec481d0e43d5139f486c06a37139847a7ce569788af72", size = 265390, upload-time = "2026-07-02T13:10:25.152Z" }, + { url = "https://files.pythonhosted.org/packages/92/e1/5783cdabb797305e1c9e4809fea496d31834c51fa772514f73dc148bcfc9/coverage-7.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:110cbdf8d2e216577312cf06ccf85539c0e5a5420ef747e4a4719b5e483c88cd", size = 267811, upload-time = "2026-07-02T13:10:27.249Z" }, + { url = "https://files.pythonhosted.org/packages/85/31/96d8bbf58b8e9193bc8389574a91a0db48355ee98feb66aa6bf8d1b32eea/coverage-7.15.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2c5d4619214f1d9993e7b00a8600d14614b7e9d84e89507460b126aa5e6559e5", size = 268928, upload-time = "2026-07-02T13:10:29.242Z" }, + { url = "https://files.pythonhosted.org/packages/5e/7a/5294567e811a1cb7eda93140c628fa050d66189da28da320f93d1d815c73/coverage-7.15.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:781a704516e2d8346fbbd5be6c6f3412dd824785146528b3a01816f26c081007", size = 262378, upload-time = "2026-07-02T13:10:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/69/3f/3f48538421f899f28946f90a3d272136a4686e1abf461cc9249a783ee0f3/coverage-7.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd4a1b44bcb65ee29e947ac92bbee04956df3a6bfc6143641bb6cae7ede00fc9", size = 265263, upload-time = "2026-07-02T13:10:32.942Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d3/092df15efcab8a9c1467ee960eb8019bbad3f9300d115d89ea6195f369ff/coverage-7.15.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0e4950c9d6d3e39c64c991814ff315e2d0b9cb8152363594212c9e55208c0a8f", size = 262866, upload-time = "2026-07-02T13:10:35.104Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ab/0254d2b88665efb2c57ad368cc77ab5de3435bd8d5add4729c1b0e79431e/coverage-7.15.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:fe9c87ff42e5472d80d21704972e1f96e104a0a599d77c5e35db5a3c562e2571", size = 266599, upload-time = "2026-07-02T13:10:37.05Z" }, + { url = "https://files.pythonhosted.org/packages/a8/79/1cfa4023e489ce6fbc7be4a5d442dbc375edb4f4fda39a352cedb53263c2/coverage-7.15.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f00d5ae1dd2fe13fb8186e3e7d37bcbd8b25c0d764ff7d1b32cef9be058510a8", size = 261714, upload-time = "2026-07-02T13:10:38.966Z" }, + { url = "https://files.pythonhosted.org/packages/b7/eb/fee5c8665656be63f497418d410484637c438172568688e8ac92e06574e7/coverage-7.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:363ab38cc78b615f11c9cac3cf1d7eef950c18b9fdedfb9066f59461dcf84d68", size = 264025, upload-time = "2026-07-02T13:10:40.789Z" }, + { url = "https://files.pythonhosted.org/packages/ab/99/63005db722f91edc81abc16302f9cc2f6228c1679e46e15be9ae144b14d0/coverage-7.15.0-cp314-cp314t-win32.whl", hash = "sha256:54fd9c53a5fafff509195f1b6a3f9be615d8e8362a3629ff1de23d270c03c86b", size = 223413, upload-time = "2026-07-02T13:10:42.597Z" }, + { url = "https://files.pythonhosted.org/packages/c1/e8/2bc6181c4fb06f1a6b981eb85330cc57bfad7e3f710fc9c9d350013ba228/coverage-7.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:87b47553097ba185ed964866078e7e63adea9f5f51b5f39691c34f30afd21080", size = 224245, upload-time = "2026-07-02T13:10:44.47Z" }, + { url = "https://files.pythonhosted.org/packages/79/b8/4d959bf9cc45d0cfed2f4d35cafcab978cdb6ea02eb5100009cd740632a3/coverage-7.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aeefb2dd178fe7eee79f0ad25d75855cb35ee9ed472db2c5ea06f5b4fd00cec5", size = 223558, upload-time = "2026-07-02T13:10:46.368Z" }, + { url = "https://files.pythonhosted.org/packages/52/30/21b2ad45959cd50e909e02ebac1e30b4ceb7162e91c11d4c570223a458b7/coverage-7.15.0-py3-none-any.whl", hash = "sha256:56da6a4cbe8f7e9e80bd072ca9cefe67d7106a440a7ec06519ec6507ac94ad19", size = 212632, upload-time = "2026-07-02T13:10:48.641Z" }, +] + +[[package]] +name = "cuda-bindings" +version = "13.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/67/5e7dba1ba576dd73da5dee894ca076ca5e959450dfff66d6d510a255d1f7/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7855c4868aabc0cfae28abbe83d56734bdfbd08f08fc234ac1912a12858bf49", size = 6025351, upload-time = "2026-05-29T23:11:49.685Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/6d2e9047d1fb243dbaa364b01e0297534b9ed7fd27dba1c9f361519cf69b/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e32d08f71ebcdf00f0f41eab2eb37e8da94c8ed411cc9f7f7a019ce6b34abe3a", size = 6657965, upload-time = "2026-05-29T23:11:52.227Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6e/2394f8163360f8391f8f1b7e72d300a82724edb81a7b7084c799fbd4c91f/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9efb21c1ee64981e184b9e0ba5eb3179e5ba3d4b51665a6cb52b8ef3d01a7cbf", size = 5920504, upload-time = "2026-05-29T23:11:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/34/c2/ef9b6a63f7dc432712a462c816662e662e00d38caa9b861c8c2588195d03/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2732904099e0a4d4db774a5fc6d91ee95fae065b4d2ecabb4968c5fe2406c9d7", size = 6476660, upload-time = "2026-05-29T23:11:59.188Z" }, + { url = "https://files.pythonhosted.org/packages/b1/81/bff68ce829999c1e4209c761bbf903b1c06ec570416ddb25020864ad5907/cuda_bindings-13.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ab2f74ed65bfef4163ba07a8db16f1085e0729291db12a2423aff84ee8278b8", size = 6013639, upload-time = "2026-05-29T23:12:03.509Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e0/c8a1f0c8f9ffdea4f5fe6dbab89b326cef4d85caf489dad39e209da89416/cuda_bindings-13.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd4c814d311ec08c981f6dded1dbe7d4b371067ee4f6c14cccec4bde9590f80", size = 6534419, upload-time = "2026-05-29T23:12:05.633Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/83b1f563925b290f2d11a01a77a84013ba56052fe3653a5bef3ccfbb43d6/cuda_bindings-13.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3c772dfff49681541d59630c90f858e173ac926b9c593a2b7123f2a1043cc76", size = 5809771, upload-time = "2026-05-29T23:12:10.422Z" }, + { url = "https://files.pythonhosted.org/packages/12/20/e79b4bfe98f075195afb6343d41c498f9dbd2d161d7021d4d28bceb83581/cuda_bindings-13.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36febb7c1079d68a981dbbd8d5a67235b399802b82075c9388624719607e52b9", size = 6358584, upload-time = "2026-05-29T23:12:12.767Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.5.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/53/8fc9b0cdc5b7f62746e6a01b85b6461e5ae27f871010a5fcf8fa6950766d/cuda_pathfinder-1.5.6-py3-none-any.whl", hash = "sha256:7e4c07c117b78ba1fb35dac4c444d21f3677b1b1ff56175c53a8e3025c5b43c0", size = 52972, upload-time = "2026-06-30T00:58:04.34Z" }, +] + +[[package]] +name = "cuda-toolkit" +version = "13.0.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl", hash = "sha256:b198824cf2f54003f50d64ada3a0f184b42ca0846c1c94192fa269ecd97a66eb", size = 2364, upload-time = "2025-12-19T23:24:07.328Z" }, +] + +[package.optional-dependencies] +cudart = [ + { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux'" }, +] +cufft = [ + { name = "nvidia-cufft", marker = "sys_platform == 'linux'" }, +] +cufile = [ + { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, +] +cupti = [ + { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux'" }, +] +curand = [ + { name = "nvidia-curand", marker = "sys_platform == 'linux'" }, +] +cusolver = [ + { name = "nvidia-cusolver", marker = "sys_platform == 'linux'" }, +] +cusparse = [ + { name = "nvidia-cusparse", marker = "sys_platform == 'linux'" }, +] +nvjitlink = [ + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux'" }, +] +nvtx = [ + { name = "nvidia-nvtx", marker = "sys_platform == 'linux'" }, +] + +[[package]] +name = "filelock" +version = "3.29.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/dc/be6cbe99670cd6e4ad387123647cb08e0c32975e223f82551e914c5568a6/filelock-3.29.4.tar.gz", hash = "sha256:10cdb3656fc44541cdf30652a93fb10ec6b05325620eb316bd26893e4201538a", size = 63028, upload-time = "2026-06-13T16:12:00.744Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl", hash = "sha256:dac1648087d5115554850d113e7dd8c83ab2d38e3435dde2d4f163847e57b767", size = 42757, upload-time = "2026-06-13T16:11:59.582Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/10/a1/ae4e3e5003468d6391d2c77b6fa1cd73bd5d13511d81c642d7b28ac90ed4/fsspec-2026.6.0.tar.gz", hash = "sha256:f5bac145310fe30e16e1471bd6840b2d990d609e872251d7e674241822abf01a", size = 313646, upload-time = "2026-06-16T01:57:28.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl", hash = "sha256:02e0b71817df9b2169dc30a16832045764def1191b43dcff5bb85bdee212d2a1", size = 203949, upload-time = "2026-06-16T01:57:26.358Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "numpy" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/05/3d27272d30698dc0ecb7fdfaa41ad70303b444f81722bb99bce1d818638a/numpy-2.5.0.tar.gz", hash = "sha256:5a129578019311b6e56bdd714250f19b518f7dceeeb8d1af5490f4942d3f891c", size = 20652461, upload-time = "2026-06-21T20:57:51.95Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/0a/11486d02add7b1384dff7374d124b1cfbb0ee864dcc9f6a2c0380638cf84/numpy-2.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:489780423903667933b4ed6197b6ec3b75ea5dd17d1d8f0f38d798feb6921561", size = 16789987, upload-time = "2026-06-21T20:56:16.657Z" }, + { url = "https://files.pythonhosted.org/packages/55/b2/285f48640a181947b4587a3766d21ec1eaa7fea833d4b49957e09da467a2/numpy-2.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ece55976ced6bca95a03ae2839e2e5ccffe8eb6a3e7022415645eb154a81e4e6", size = 11760322, upload-time = "2026-06-21T20:56:19.813Z" }, + { url = "https://files.pythonhosted.org/packages/dd/67/b032db1eb03ca30d16eda3b0c22aaa615338b9263c2fd559d0f29451aca4/numpy-2.5.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:c83b664b0e6eee9594fa920cf0639d8af796606d3fad6cc70180c87e4b97c7be", size = 5319605, upload-time = "2026-06-21T20:56:22.173Z" }, + { url = "https://files.pythonhosted.org/packages/b9/83/03fc7300c7c6b6c84c487b1dc80d322817b95fbd1f4dd57a85e23b7198de/numpy-2.5.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:bf80333980bf37f523341ddd72c783f39d6829ec7736b9eb99086388a2d52cc2", size = 6653628, upload-time = "2026-06-21T20:56:23.914Z" }, + { url = "https://files.pythonhosted.org/packages/82/49/2ec21730bc63ccfda829323f7040a8ed4715b3852ce658689cf74ee96a8c/numpy-2.5.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1a4874217b36d5ac8fc876f52e39df56f8182c88463e9e2dceabf7ca8b7efb8", size = 15153691, upload-time = "2026-06-21T20:56:25.631Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6b/f4a3d0637692c49da8ef99d72d52526f92e0a8d6ac4f0ca9f31441b9d9ea/numpy-2.5.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aaa760137137e8d3c920d27927748215b56014f92667dc9b6c27dfc61249255a", size = 16660066, upload-time = "2026-06-21T20:56:28.009Z" }, + { url = "https://files.pythonhosted.org/packages/3a/2f/c354ec86d1f3f5c19649463b0d39652e160736e5b0a4cd18dff0576715c4/numpy-2.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7174ce8265fc7f7417d171c9ea8fe905220748893ea67a2a7abe726ec331c4b0", size = 16514638, upload-time = "2026-06-21T20:56:30.26Z" }, + { url = "https://files.pythonhosted.org/packages/06/34/43efdcb319988648580f93c11f1ae82cf7e2faa74925e98e454ae3aa95f8/numpy-2.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b8c3daaf99de52415d20b42f8e8155c78642cb04207d02f9d317a0dcf1b3fb54", size = 18419647, upload-time = "2026-06-21T20:56:32.41Z" }, + { url = "https://files.pythonhosted.org/packages/71/e2/f5d1676b1d7fb682eb5e9a1641e7ebd2414b3216c370661d1029778908b4/numpy-2.5.0-cp312-cp312-win32.whl", hash = "sha256:6206db0af545d73d068add6d992279145f158428d1da6cc49adc4b630c5d6ee5", size = 6056688, upload-time = "2026-06-21T20:56:34.657Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7c/48f115d1c58a34032facebcd51fdf2d02df2c51d4a46a81dd1197bb2ea6b/numpy-2.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:6f2d6873e2940c860a309d21e25b1e69af6aaffdd80aa056b04c16380db1c4f2", size = 12419237, upload-time = "2026-06-21T20:56:36.24Z" }, + { url = "https://files.pythonhosted.org/packages/86/26/2e0882f4044d1b1a1b63e875151fb2393389032022a8b7f5657a7996d3b2/numpy-2.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:a55e1eb2bca2cfd17a16b213c99dfc8502d47b0d494224d2122277d0400935ca", size = 10339912, upload-time = "2026-06-21T20:56:38.733Z" }, + { url = "https://files.pythonhosted.org/packages/8a/33/07675aaad7f26ea013d5e884d9a0d784b79c6bd7566c333f5a52fa3c610b/numpy-2.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:520e6b8be0a4b65840ac8090d4f51cef4bed66e2b0894d5a520f099adc24a9b2", size = 16784890, upload-time = "2026-06-21T20:56:40.799Z" }, + { url = "https://files.pythonhosted.org/packages/85/4b/953118a730ee3b35e28645e0eb4cf9beec5bdbb954e1ac2f5fcefba6bbc3/numpy-2.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:146b81cdd3967fdb6beca8ba25f00c58741d8f3cbd797f55af0fbe0bfec3469c", size = 11754584, upload-time = "2026-06-21T20:56:43.094Z" }, + { url = "https://files.pythonhosted.org/packages/44/9b/56dd530c367c74ae17411027cea4135ca57e1e0583bf5594cee18bd83217/numpy-2.5.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:126b88d95e8ff9b00c9e717aa540469f21d6180162f84c0caec51b16215d49cd", size = 5313904, upload-time = "2026-06-21T20:56:45.503Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b0/bcd672edad27ecca7da1f7bb0ce72cd1706a4f2d79ae94990afc97c13e1c/numpy-2.5.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d4313cef1594c5ce46c31b6e54e918338f63f16ee9322304e8c9114d6d81c8bd", size = 6648504, upload-time = "2026-06-21T20:56:47.567Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/15cdfcbd30a1544a46c9e487a00df331c4672450216538705a9e51fa6710/numpy-2.5.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:750fb097caf26fa878746d9d119f6f9da12dedcbff1eea966c3e3447647c4a9e", size = 15150086, upload-time = "2026-06-21T20:56:49.352Z" }, + { url = "https://files.pythonhosted.org/packages/32/4e/8d7656ccaab3e81e97258b8a9bc5f0c8502513a92fb4ceb0a2cbfebc17bf/numpy-2.5.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3893adc2dc7c0412ba76777db55a049215d99c9aa3113003be8f49f4f1290ab9", size = 16647250, upload-time = "2026-06-21T20:56:51.542Z" }, + { url = "https://files.pythonhosted.org/packages/3c/81/97060281b602ed07f21b12f4ec409eac1f75a2f91fbc829ed8b2becf3ad4/numpy-2.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:835e454dd99b238cdc5a3f63bce2371296f5ebc53ca1e0f8e6ddbb6d92a29aab", size = 16512864, upload-time = "2026-06-21T20:56:55.401Z" }, + { url = "https://files.pythonhosted.org/packages/33/ab/4496208146911f8d8ddb54f68a972aafa6c8d44babcb2ea03b0e5cc87c9d/numpy-2.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f9836778081a0a3c02a6a21493f3e9f5b311f8d2541934f31f05583dc999ea4", size = 18408407, upload-time = "2026-06-21T20:56:57.75Z" }, + { url = "https://files.pythonhosted.org/packages/d4/9f/a4df67c181e4ee8b467aa3332dc2db10fd5c515136831302f3ca48bc0a01/numpy-2.5.0-cp313-cp313-win32.whl", hash = "sha256:0b525be4744b60bb0557ac872d53ef07d085b5f39622bc579c98d3809d05b988", size = 6054431, upload-time = "2026-06-21T20:57:00.016Z" }, + { url = "https://files.pythonhosted.org/packages/30/53/491e1c47c55b62ccc6a63c1c5b8635c73fc2258dddeb9bda27cae4a0ae96/numpy-2.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:44353e2878930039db472b99dc353d749826e4010bd4d2a7f835e94a97a5c748", size = 12414420, upload-time = "2026-06-21T20:57:01.815Z" }, + { url = "https://files.pythonhosted.org/packages/eb/4a/25c2906f541e9d9f4c5769764db732e6627be91a13f4724fa10634d77db4/numpy-2.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:48f54b00711f83a5f796b70c518e8c2b3c5848dda03a54911f23eb68519b9b60", size = 10339533, upload-time = "2026-06-21T20:57:03.961Z" }, + { url = "https://files.pythonhosted.org/packages/86/ad/abc44aaceaf7b17ee1edde2bbb4458da591bc79574cffff50c4bb35f00d1/numpy-2.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f27582c55ba4c750b7c58c8faf021d2cd9324a662b466229db8a417b41368af9", size = 16783807, upload-time = "2026-06-21T20:57:06.253Z" }, + { url = "https://files.pythonhosted.org/packages/5d/39/b72e168daf9c00fb20c9fc996d00437ccecdef3102387775d29d7a62576d/numpy-2.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:28e7137057d551e4a83c4ae414e3451f50568409db7569aacc7f9811ee06a446", size = 11765215, upload-time = "2026-06-21T20:57:08.547Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a0/8400a9c0e3625182347593f5e1f57da9a617a534794805c8df5518154ddc/numpy-2.5.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e1da54b53e75cd9fcfc23efcc7edab2c6aecf97b6037566d8a0fe804af8ec57c", size = 5324493, upload-time = "2026-06-21T20:57:11.012Z" }, + { url = "https://files.pythonhosted.org/packages/f6/8c/0d104deaa0401c93395a629ec902891618a2eff76d19229139cb5a887bfc/numpy-2.5.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:694d8f74e156f7fd01179f1aa8faa2f648ab6ae0f70b6c3fe57a03249aea2303", size = 6645211, upload-time = "2026-06-21T20:57:12.919Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d9/4a4a628c812750363786afc3d33492709a5cd64b215469c16b0f6c7bb811/numpy-2.5.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a7569a7b53c77716f036bb28cb1c91f166a26ec7d9502cd1e4bdfe502fdec22", size = 15166004, upload-time = "2026-06-21T20:57:14.717Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5e/2a902317d7fc4aa93236e80c932662dadfc459b323d758329e01775125e1/numpy-2.5.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39a0433bd4086ebd462960cf375e19195bb07b53dc1d87dd5fcf47ad78576f03", size = 16650797, upload-time = "2026-06-21T20:57:16.906Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a0/a0090e6329f4ca5992c07847bb579c5259a19953dc57255bb08793142ffb/numpy-2.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:929f0c79ac38bcbd7154fe631dc907abfeddbcc5027a896bd1f7767323271e7a", size = 16524647, upload-time = "2026-06-21T20:57:19.165Z" }, + { url = "https://files.pythonhosted.org/packages/5e/7d/6caf27734c42b65837e7461ed0dbbd6b6fc835060c9714ec59d673bb383a/numpy-2.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cc4f247a47bbf070bfd70be53ccdcf47b800af563535e7bbe172322197c30e21", size = 18411841, upload-time = "2026-06-21T20:57:21.638Z" }, + { url = "https://files.pythonhosted.org/packages/13/dc/26edadbd812536769a82c2e9e002234e33feb5da43061d47a044f6d309b7/numpy-2.5.0-cp314-cp314-win32.whl", hash = "sha256:5dc71423499fab3f46f7a7201155ade1669ea101f2f429d332df9e72f8161731", size = 6106361, upload-time = "2026-06-21T20:57:23.844Z" }, + { url = "https://files.pythonhosted.org/packages/f2/9e/4dd1459282229a72d92dece2ae9138e5cac94a72263a7ceb48f37434c925/numpy-2.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:ebb81d9d5443e0309d6c54894c3fbed74ad7da0714352a67b6d773cd189eae73", size = 12551749, upload-time = "2026-06-21T20:57:25.945Z" }, + { url = "https://files.pythonhosted.org/packages/05/a7/6bc6384c080b86c7f6c85c5bc5b540b24f4f679cd144791d99574e90d462/numpy-2.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:3b94d0d0deceebfad3e67ae5c0e5eb87371e8f7a0581cd04a779928c2450cf1e", size = 10617072, upload-time = "2026-06-21T20:57:28.175Z" }, + { url = "https://files.pythonhosted.org/packages/86/6b/4a2b71d66ada5608ae02b63f150dfad520f6940721cb7f029ad270befc0e/numpy-2.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:22f3d43e362d650bc39db1f17851302874a148ca95ba6981c1dfb5fa6862f35b", size = 11881067, upload-time = "2026-06-21T20:57:30.104Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b2/d365eb40a20efb49d67e9feb90494ed8511282ee1f5fa16006675c65397d/numpy-2.5.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:243563efb4cd7528a264567e9fd206c87826457322521d06206a00bfa316c927", size = 5440290, upload-time = "2026-06-21T20:57:32.193Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5e/e9c03188de5f9b767e46a8fe988bcfd3efad066a4a3fda8b9cb11a93f895/numpy-2.5.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:84881d825ca75249b189bbee875fcfe3238aa5c479e6100893cda566e8e86826", size = 6748371, upload-time = "2026-06-21T20:57:33.933Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1d/68c186a38a5027bae2c4ddd5ea681fdaf8b4d30fb7301def6d8ad270390f/numpy-2.5.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cda12aa4779d42b8771180aba759c96f527d43446d8f380ab59e2b35e8489efd", size = 15214643, upload-time = "2026-06-21T20:57:35.677Z" }, + { url = "https://files.pythonhosted.org/packages/8c/67/73f67b7c7e20635baae9c4c3ead4ae7326a005900297a6110971abd62eb5/numpy-2.5.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c0121101093d2bd74981b10f8837d78e794a8ff57834eb27179f49e1ba11ac6", size = 16690128, upload-time = "2026-06-21T20:57:38.159Z" }, + { url = "https://files.pythonhosted.org/packages/eb/05/d4c1fb0c46d02a27d6b2b8b319a78c90937acec8631c1641874670b31e6f/numpy-2.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d371c92cfa09da00022f501ab67fafaea813d752eb30ac44336d45b1e5b0268a", size = 16577902, upload-time = "2026-06-21T20:57:40.447Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1d/771c797d50fa26e4888989cccf1d50ee51f530d4e455ad2692dcb64fa711/numpy-2.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9990713e9c38154c6861e7547f1e3fc7a87e75ff09bab24ef1cc81d81c2835e9", size = 18452814, upload-time = "2026-06-21T20:57:42.875Z" }, + { url = "https://files.pythonhosted.org/packages/e8/46/52fc0d2a68d7643f0f149eeea5a5d8ea2a3507056ac8afa83c9212606e8b/numpy-2.5.0-cp314-cp314t-win32.whl", hash = "sha256:edadfbd4794b1086c0d822f81863e8a68fc129d132fd0bb9e31e955d7fbbbdb7", size = 6253168, upload-time = "2026-06-21T20:57:45.101Z" }, + { url = "https://files.pythonhosted.org/packages/2a/be/6c8d1118b5f13b2881dc095d5b345de19c6638b8959c17409b6eff84c8aa/numpy-2.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f7e5fa4382967ae6548bd2f174219afb908e294b0d5f625af01166edd5f7d9aa", size = 12736286, upload-time = "2026-06-21T20:57:46.935Z" }, + { url = "https://files.pythonhosted.org/packages/fd/6a/d3a169aaf8536cf228d56a09e04bcb713a2fe4410d4e2105b9419b5a9c89/numpy-2.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:016623417bb330d719d579daf2d6b9a01ddc52e41a9ed61a47f39fde46dcd865", size = 10686451, upload-time = "2026-06-21T20:57:49.313Z" }, +] + +[[package]] +name = "nvidia-cublas" +version = "13.1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cuda-nvrtc", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cd/154ca20c38269e05eff77c1464e6c1da89f50a6390b565e9d82e06bc11e1/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:37936a16db8fe4ac1f065c2139360608a543a09275cb1a1af612e08cfa065436", size = 423138758, upload-time = "2026-04-08T18:46:58.655Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime" +version = "13.0.96" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" }, + { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu13" +version = "9.20.0.48" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5e/edb9c0ae051602c3ccaffe424256463636d639e27d7f302dde9975ef9e7a/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0c45dd8eeb50b603f07995b1b300c62ffe6a1980482b82b3bcf94a4ca9d49304", size = 366173588, upload-time = "2026-03-09T19:29:34.474Z" }, +] + +[[package]] +name = "nvidia-cufft" +version = "12.0.0.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" }, +] + +[[package]] +name = "nvidia-cufile" +version = "1.15.1.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" }, +] + +[[package]] +name = "nvidia-curand" +version = "10.4.0.35" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" }, +] + +[[package]] +name = "nvidia-cusolver" +version = "12.0.4.66" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-cusparse", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" }, +] + +[[package]] +name = "nvidia-cusparse" +version = "12.6.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu13" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/e1/cdc1797eadf82d3a9a575a19b33fdc871a97edbec42c00b5b5e914f4aff4/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4dca476c50bf4780d46cd0bfbd82e2bc10a08e4fef7950917ce8d7578d22a23f", size = 221051344, upload-time = "2025-09-05T18:49:51.289Z" }, + { url = "https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:786ce87568c303fadb5afcc7102d454cd3040d75f6f8626f5db460d1871f4dd0", size = 170148586, upload-time = "2025-09-05T18:50:50.248Z" }, +] + +[[package]] +name = "nvidia-nccl-cu13" +version = "2.29.7" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/0d/daf50d44177ee0cbc7ff0a0c91eb5ff676c82be42f9a970bc7597f440c3a/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:674a12383e3c38a1bcccae7d4f3633b37852230b6047883cb2f4c2d1b36d9bf5", size = 206014712, upload-time = "2026-03-03T05:34:20.843Z" }, + { url = "https://files.pythonhosted.org/packages/67/f4/58e4e91b6919367c7aafb8e36fce9aad1a3047e536bf7e2fd560927d3a4c/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:edd81538446786ec3b73972543e53bb43bcaf0bfc8ef76cb679fcc390ffe136d", size = 205976000, upload-time = "2026-03-03T05:36:24.472Z" }, +] + +[[package]] +name = "nvidia-nvjitlink" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:13a74f429e23b921c1109976abefacc69835f2f433ebd323d3946e11d804e47b", size = 40713933, upload-time = "2025-09-04T08:35:43.553Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2c/93c5250e64df4f894f1cbb397c6fd71f79813f9fd79d7cd61de3f97b3c2d/nvidia_nvjitlink-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e931536ccc7d467a98ba1d8b89ff7fa7f1fa3b13f2b0069118cd7f47bff07d0c", size = 38768748, upload-time = "2025-09-04T08:35:20.008Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu13" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" }, +] + +[[package]] +name = "nvidia-nvtx" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" }, + { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pandas" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/f1/392f8c5bfc16f66a0d2d41561c01627c228fe7ed2a0d056ef11315042570/pandas-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fed2ff7fd9779120e388e285fc029bd5cf9490cdd2e4166a9ee22c0e49a9ab09", size = 10357846, upload-time = "2026-05-11T18:52:36.143Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3d/b16412745651e855f357e5e66930248688378853a6e2698a214e331fba1f/pandas-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b168fc218fd80a6cbdbdbc1a97ddc7889ed057d7eb45f50d866ceab5f39904c4", size = 9899550, upload-time = "2026-05-11T18:52:38.976Z" }, + { url = "https://files.pythonhosted.org/packages/31/a8/fa2535168fffcedf67f4f6de28d2dd903a747ca7c8ea6989451aaeb3a92f/pandas-3.0.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0383c72c75cdcca61a9e116e611143902dbfd08bff356829c2f6d1cf40a9ca8c", size = 10412965, upload-time = "2026-05-11T18:52:41.915Z" }, + { url = "https://files.pythonhosted.org/packages/65/b6/09b01cdbc15224e2850365192d17b7bdebb8bdbd8780ed221fcdf0d9a515/pandas-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6dc0b3fd2169c9157deed50b4d519553a3655c8c6a96027136d654592be973a9", size = 10894600, upload-time = "2026-05-11T18:52:45.02Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a4/2eb28f2fccb4ced4a2c79ab2a5dee9ade1ebf44922ebad6fea158c9f95d4/pandas-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e65d5407dc0b394f509699650e4a2ec01c0514f21850f453fa60f3be79a5dbf", size = 11422824, upload-time = "2026-05-11T18:52:48.058Z" }, + { url = "https://files.pythonhosted.org/packages/f8/45/830bb57f533a4604b355e07edcb8ea18cf88b5f94e5fca92f27052d7c597/pandas-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8894dc474d648fe7b6ff0ca9b0bd73950d19952bc1a6534540762c5d79d305c", size = 11950889, upload-time = "2026-05-11T18:52:50.905Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c5/fc1b368f303087d20e8c9bf3d6ceb186263cfac0ade735cd938538bea839/pandas-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:c7be265b62cef88e253a941e4698604973736dcfe242fdb5198f0f7bc473cdcc", size = 9755463, upload-time = "2026-05-11T18:52:53.386Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/fda8f9705b1b09c6ebe14bfc0fa0e4ec8584d54ea673628f157ff55131af/pandas-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:557409bc4178e70ee8d9ddb494798e51ebf6ea59330f6be22c51bab2a7db6c49", size = 9066158, upload-time = "2026-05-11T18:52:56.038Z" }, + { url = "https://files.pythonhosted.org/packages/c5/90/62d8302883c44308c477e222c3daf7c813a34c8e96985882fbd53d964352/pandas-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:67b3b64c11910cfa29f4e94a14d3bff9ee693b6fc76055e7cad549cee0aec5fa", size = 10331071, upload-time = "2026-05-11T18:52:58.838Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ae/6a6493c783a101f165e4356953ba3c74d6f77f0042fa7d753da9dfbb640c/pandas-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39436b377d56d2a2e52d0395bdbee171f01068e99af5250509aceeb929f765c7", size = 9875690, upload-time = "2026-05-11T18:53:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/62/7c/5df8e9f56c69a2769fbe9382a5ef8f2658c007e376434e1e2cbb57ad895f/pandas-3.0.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4be06d68f9ddcfc645b87534911da79a8fbffc7573c80e0edcf42a5020624d8", size = 10381634, upload-time = "2026-05-11T18:53:04.393Z" }, + { url = "https://files.pythonhosted.org/packages/99/68/1237369725aa617bb358263d535803e3053fdbc593513ec5ed9c9896b5b6/pandas-3.0.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4eeb6830daf35a71cc09649bd823e2b542dac246cdee9614c6e4bd65028cd6a", size = 10891243, upload-time = "2026-05-11T18:53:07.643Z" }, + { url = "https://files.pythonhosted.org/packages/25/93/77d108e8af7222b4a503ebde0e30215b1c2e4f8e53a526431890f22d5586/pandas-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1928e07221f82db493cd4af1e23c1bfca524a19a4699887975bff68f49a72bfb", size = 11388659, upload-time = "2026-05-11T18:53:10.634Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bd/eff5b4399f332ac386c853f6cd2bd3fa2ca0061b9f36ecd9c4d7c4265649/pandas-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51b1fe551acb77dac643c6fda86084d8d446c10fe64b06a9cc29c4cc8540e7f2", size = 11942880, upload-time = "2026-05-11T18:53:13.536Z" }, + { url = "https://files.pythonhosted.org/packages/2c/20/559ace4200982c3887d0b86bfd0d856a2143ef8ddab63cc07934951a964c/pandas-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:a82d532a3351d435432cd913edbccaf8b8e01d4dd0e5ced5a8d2e8ecd94c7e44", size = 9757091, upload-time = "2026-05-11T18:53:16.306Z" }, + { url = "https://files.pythonhosted.org/packages/3a/66/69055a09fe200f29f922a3eeec4804611900b95f52d932ece3393c3c0c19/pandas-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:275c14e0fce14a2ec20eee474aecd305478ea3c1e6f6a9d8fe219a165542717e", size = 9057282, upload-time = "2026-05-11T18:53:18.768Z" }, + { url = "https://files.pythonhosted.org/packages/57/0e/efe801b0e6811e8e650cd21b7f2608e30f08a7067e2bf6e8752b0d56ee3c/pandas-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:46997386d528eb40376ecd6b033cf4a8a1e5282580f68f43de875b78cba2199d", size = 10767016, upload-time = "2026-05-11T18:53:21.227Z" }, + { url = "https://files.pythonhosted.org/packages/ea/dc/eb55135a1d5f0f0519f28da1f609a206d2cad1f9c35c32d51e38dd7261ae/pandas-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261e308dfb22448384b7580cf719d2f998fe2966c92893c3e77d14008af1f066", size = 10420210, upload-time = "2026-05-11T18:53:23.982Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3e/b1d5d955ce33ffecb407465a60bc32769d74fcf68224b7ae67ae11d4dea4/pandas-3.0.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd1a5d1def6a46002e964510bdc67c368aa0951df5d1d9f8365336f5a1f490cd", size = 10336126, upload-time = "2026-05-11T18:53:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/a01261711ab60a22d71b862f0de20e4c504bf80457270ad8cb42110f6abc/pandas-3.0.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d72828c20c6d6e83e1e22a6a3b47b326b71664112fa9705dcbccfd7a39b62085", size = 10728051, upload-time = "2026-05-11T18:53:29.125Z" }, + { url = "https://files.pythonhosted.org/packages/e9/21/ea191195e587b18cf682e97f433f81b2d0fbe341380e80a3e0d6e4403c8e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d26cbe1fcfc12e8fd900e2454163e466b2d3af84f7c75481df7683ffc073d870", size = 11350796, upload-time = "2026-05-11T18:53:32.056Z" }, + { url = "https://files.pythonhosted.org/packages/64/69/f0eaaf54939f0e8c6768fd06be9af2cef9b36048b96dfb9e1b2c685a807e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e91cec1879ada0624fc3dc9953c5cbd60208e59c0db28f540c5d6d47502422f", size = 11799741, upload-time = "2026-05-11T18:53:34.985Z" }, + { url = "https://files.pythonhosted.org/packages/45/a4/865e0e510cae5fc2194de4db28be638952de942571ba9125934fd9c01d47/pandas-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:08d789b41f87e0905880e293cedf6197ce71fe67cc081358b1e148a491b9bd13", size = 10499958, upload-time = "2026-05-11T18:53:37.857Z" }, + { url = "https://files.pythonhosted.org/packages/86/54/effdcc3c0ff7a08037889200e148ebe94c16c4f653be078c7b3675955df1/pandas-3.0.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3650109c0f22879df8bd6179ab9ee3d7f1d1d4e7e0094a3f0032d9f51e2e64ac", size = 10336065, upload-time = "2026-05-11T18:53:41.099Z" }, + { url = "https://files.pythonhosted.org/packages/68/10/bf2d6738d72748b961a3751ab89522d58c54efc36a8e1a12161216cd45cf/pandas-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bab900348131a7db1f69a7309ef141fd5680f1487094193bcbbb61791573bf8f", size = 9926101, upload-time = "2026-05-11T18:53:43.515Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e9/e35cf11c8a136e757b956f5f0efdcaa50aecde85ea055f1898dfc68262f3/pandas-3.0.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba7e08b9ac1d54569cd1e256e3668975ed624d6826f7b68df0342b012007bddb", size = 10457553, upload-time = "2026-05-11T18:53:46.394Z" }, + { url = "https://files.pythonhosted.org/packages/58/3b/1cdec6772bdbaf7b25dab360c59f03cadf05492dd724c6540af905389b07/pandas-3.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d71c63ae4ebdbf70209742096f1fc46a83a0613c99d4b23766cced9ff8cd62a", size = 10914065, upload-time = "2026-05-11T18:53:49.134Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/1ef644445fcd72e3627bceec77e3560636f87ddce4ed841afe76b83b5bf9/pandas-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3a2ec42c98ffa2565a67e08e218d06d72576d758d90facb7c00805194d8f360", size = 11459188, upload-time = "2026-05-11T18:53:52.527Z" }, + { url = "https://files.pythonhosted.org/packages/7e/49/4d8d4f42cbc9c4adc7a1870f269c02cbd6cd40d059622c06fb298addcbad/pandas-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:335f62418ed562cfc3c49e9e196375c28b729dcef8543abf4f9438e381bf3c76", size = 11982966, upload-time = "2026-05-11T18:53:55.043Z" }, + { url = "https://files.pythonhosted.org/packages/38/55/792619469bab9882d8bbd5865d45a72f6478762d04a9af4bf0d08c503e95/pandas-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:3c20a521bbb85902f79f7270c80a59e1b5452d96d170c034f207181870f97ac5", size = 9876755, upload-time = "2026-05-11T18:53:58.067Z" }, + { url = "https://files.pythonhosted.org/packages/2a/af/33c469653b0ba03b50c3a98192d4c07f0c75c66b263ceb097fce0ee97d31/pandas-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:a2d2dff8a04f3917b55ab3910c32990f8ddf7eceba114947838cefa976a68977", size = 9198658, upload-time = "2026-05-11T18:54:00.733Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fa/b8c257bd76b8bd060c3a9151c1fca05e9b9c5e3af5d0f549c0356f6d143d/pandas-3.0.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0d589105b3c14645af1738ff279b2995102d8f7a03b0a66dc8d95550eb513e04", size = 10787242, upload-time = "2026-05-11T18:54:03.564Z" }, + { url = "https://files.pythonhosted.org/packages/54/eb/f19206ffb0bf1919002969aa448b4702c6594845156a6f8050674855aac3/pandas-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:13fc1e853d9e04743d11ba75a985ccbc2a317fe07d8af61e445a6fd24dacd6a6", size = 10436369, upload-time = "2026-05-11T18:54:06.311Z" }, + { url = "https://files.pythonhosted.org/packages/fd/24/c7c39fb4fe22b71a0c2d78bf0c585c600092d85f94f086d2b3b2f6ca27e2/pandas-3.0.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:819959dab7bbd0049c15623fbac4e29a191b9528160a61fb1032242d8ced2d9c", size = 10358306, upload-time = "2026-05-11T18:54:09.085Z" }, + { url = "https://files.pythonhosted.org/packages/16/ec/dd2a9eb7fa1204df88c0864164e35b228ac581062ac612ba0a67fd812e4c/pandas-3.0.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:60ae316d3fd75d1858d450d0db0103ea2be3e7d4a95ec2f064f7e2ae63f7b028", size = 10758394, upload-time = "2026-05-11T18:54:11.956Z" }, + { url = "https://files.pythonhosted.org/packages/95/6e/00c61ea8e85b4f6d8d35e11852a1a4998fc7fafc91c6a602d1cc9c972d64/pandas-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd3a518890b400d32f9023722dc9a9a5c969f00b415419a3c06c043f09bb5d7d", size = 11375717, upload-time = "2026-05-11T18:54:14.539Z" }, + { url = "https://files.pythonhosted.org/packages/31/89/8fc1c268969fac43688d65fd92e67df24bd128d53cb4d2eee534cd307399/pandas-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c39be2d709d01fa972a0cabc522389fceca4f3969332ba25a7d6c5802cf976a", size = 11828897, upload-time = "2026-05-11T18:54:17.146Z" }, + { url = "https://files.pythonhosted.org/packages/56/3b/e7d20dea247a3e6dc0bd8a6953854afbedc03951def4e7371e05e7263e25/pandas-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4db8c527972a821cf5286b40ccc57642a39bc62e62022b42f99f8a67fca8c3a1", size = 10900855, upload-time = "2026-05-11T18:54:19.72Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/68a0978d1ef8502b8492099beaa6e7a0c1b32e3b5d4f677f5810cb08711c/pandas-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b2c95f8bfc1ee412bf482605d7bfd30c12d1d26bd59fdd91efeef1d4718decb1", size = 9466464, upload-time = "2026-05-11T18:54:22.754Z" }, +] + +[[package]] +name = "pdbe-shape-retrieval" +version = "2.0.0" +source = { editable = "." } +dependencies = [ + { name = "numpy" }, + { name = "pandas" }, + { name = "pydantic" }, + { name = "pyfmaps" }, + { name = "pymeshfix" }, + { name = "pymeshlab" }, + { name = "scikit-learn" }, + { name = "scipy" }, + { name = "torch" }, + { name = "trimesh" }, +] + +[package.dev-dependencies] +test = [ + { name = "coverage" }, + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "numpy", specifier = ">=2.2.6" }, + { name = "pandas", specifier = ">=2.3.3" }, + { name = "pydantic", specifier = ">=2.13.4" }, + { name = "pyfmaps", specifier = ">=1.0.1" }, + { name = "pymeshfix", specifier = ">=0.18.1" }, + { name = "pymeshlab", specifier = ">=2025.7.post1" }, + { name = "scikit-learn", specifier = ">=1.7.2" }, + { name = "scipy", specifier = ">=1.15.3" }, + { name = "torch", specifier = ">=2.12.0" }, + { name = "trimesh", specifier = ">=4.12.2" }, +] + +[package.metadata.requires-dev] +test = [ + { name = "coverage" }, + { name = "pytest" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "potpourri3d" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/ab/1385878462411b548a909b66f8e925dbefa6faab1ca704dd43a098905ed3/potpourri3d-1.4.0.tar.gz", hash = "sha256:83c31ac4ee69b90cc0f3af1139f91ec952077defcc9166056cc4434712679292", size = 30618500, upload-time = "2026-03-25T05:52:51.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/2e/487d59d1be46ea9724534325c96ef172ed16aff610ace62d427758084404/potpourri3d-1.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a448a72232ca43ffc392fd13408ad4f77a69b6c43578f82a4b3cef7bd012516d", size = 3542611, upload-time = "2026-03-25T05:51:44.16Z" }, + { url = "https://files.pythonhosted.org/packages/35/b3/f53e111fa7fb194fe928d4dbdae97f67c301282863ca3e3fda93609e3c40/potpourri3d-1.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f401f0da41b075a5206af276c3c24fe03114b142aaea8608be08ecfecd806dfc", size = 3147602, upload-time = "2026-03-25T05:51:45.919Z" }, + { url = "https://files.pythonhosted.org/packages/10/fe/46ff1b577bdb1c3b536c406256a155e5f628b5caee1f99669dd54b1ebcd2/potpourri3d-1.4.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e84d78a1758a821b42fdf54304d1d14aa2609579600c1b26a03134dbc367a4bb", size = 3387591, upload-time = "2026-03-25T05:51:48.032Z" }, + { url = "https://files.pythonhosted.org/packages/2c/58/5500f7e6a5ec66b8df50d84cf908041c8fa0344b15a1ad705e2e56c8b8fd/potpourri3d-1.4.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:33310bd58527ff1e19d975d9ba53150b258a3886f32b4ab7fb250188acaba175", size = 3585869, upload-time = "2026-03-25T05:51:49.687Z" }, + { url = "https://files.pythonhosted.org/packages/cb/64/b7ac23e7a3c194df9e9eff3792fceaf9b289e2d9dfbcc691c631f25b3505/potpourri3d-1.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15120a5b948a23b67613658db9dcbdaef71ce002c6b82dddfea313f0bef71180", size = 4639939, upload-time = "2026-03-25T05:51:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/60/d0/6d896897cd8a64949c8511ef37eedc9e593e1c339a6466a330619fd43c6d/potpourri3d-1.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:948bc691012d689871056dffed52c18b100654eac3fada092c37cf457278c0f9", size = 4787902, upload-time = "2026-03-25T05:51:52.816Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f1/e207cebd6ceb6cfdeee811076a6b9eeea4d10cb29025e79375196ad64982/potpourri3d-1.4.0-cp312-cp312-win32.whl", hash = "sha256:664334a2ad80db717adb338f766ea2cbbf1ae8933b34402edc94d74a7c41b1b5", size = 4473626, upload-time = "2026-03-25T05:51:54.187Z" }, + { url = "https://files.pythonhosted.org/packages/01/a9/19d538c2d58b5834086e28b9f1962fbdb9412998d1c7ff51ad9b45ccf7b1/potpourri3d-1.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:771c1795f2542c92789fadbd699d23d470948997cf3202846ec8a55d9a44f7a5", size = 6137810, upload-time = "2026-03-25T05:51:55.796Z" }, + { url = "https://files.pythonhosted.org/packages/58/e6/b5299cc75fc5335b1e2cb61943bdb5bdb322be86b4703f967495ce0fa052/potpourri3d-1.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84a2996dd78de8e9a58a572e8649fcc1b1d068989c987b721d8ce20657db5f2e", size = 3542705, upload-time = "2026-03-25T05:51:57.195Z" }, + { url = "https://files.pythonhosted.org/packages/63/90/62691ec8ac8ba32eafd1e003170a68bb4bb436d96a14e1cea287e4a80c86/potpourri3d-1.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c575f50fe3b146ac4954ce7dd5793a8f28acb94561dfca62952092a982ba5949", size = 3147726, upload-time = "2026-03-25T05:51:58.659Z" }, + { url = "https://files.pythonhosted.org/packages/c1/60/21a94518a95686bee922c5113d230be653238e9ee6985ef0eba9b251ccd9/potpourri3d-1.4.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c24c4203a475a255d0731f8b45f530a49d9c577e865479ef4c55dbcb44f7ee4", size = 3387611, upload-time = "2026-03-25T05:52:00.039Z" }, + { url = "https://files.pythonhosted.org/packages/28/fb/12afb909150af89dd199c9360fa759701cf97a682cb84c7c60a9fb7d8cf8/potpourri3d-1.4.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c09d55f65f92010ce8d745bbba7d18c034d539afb209de6f8e129f2c40424978", size = 3585538, upload-time = "2026-03-25T05:52:02.087Z" }, + { url = "https://files.pythonhosted.org/packages/5d/61/50d601a2e7c4c9e768f2617dfbe54c6d320c3e5f9cf91fa0f5799e15abd8/potpourri3d-1.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:25defb1c9d205852e061c260999f35e8d86f9fc99022cf8d86070515685cb0e0", size = 4639773, upload-time = "2026-03-25T05:52:03.95Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ec/cf8cd2b32a4bf030852a4b534192233500bf31860041f64be26e39997db1/potpourri3d-1.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a245ff2212768a8764799078664ebf2f7e5f80c7b384650d2af2996d3fabb1e2", size = 4787781, upload-time = "2026-03-25T05:52:05.742Z" }, + { url = "https://files.pythonhosted.org/packages/fb/56/98bc09f6aa3cc2637046b09f3a9f92bc0f4f89a6b60786f05959e71a8783/potpourri3d-1.4.0-cp313-cp313-win32.whl", hash = "sha256:99ffee489c34d77af103e895241d2a34a4fc36c03fabe7462256885b71ff29fb", size = 4473719, upload-time = "2026-03-25T05:52:07.692Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6e/3ca6cf54f907e2b9a73efa35b29f5fb5fbc5963f958fff8642b6413cd10c/potpourri3d-1.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:a17ade63286b3a54f11458c504f48e97059af1a6c128afae88c87a0fa3de2d7e", size = 6137822, upload-time = "2026-03-25T05:52:09.653Z" }, + { url = "https://files.pythonhosted.org/packages/d2/75/e482ad0cc361c6ba4cca005558ca6cc231bcfbb72127b46f090ed368773c/potpourri3d-1.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a812d6dac19b6759ecec86f9f50d8fb81f9235d9ab481519e0fa681cd1715c0a", size = 3545267, upload-time = "2026-03-25T05:52:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ab/eece1e1847af56541d991ea9afa9d56a080247756bbb0e638d6f342c3b31/potpourri3d-1.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:44aab06ca9dc0a7c40204351ee130d00d89c200e52946caf58e32a3f517c7abd", size = 3148040, upload-time = "2026-03-25T05:52:12.752Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b9/e3cdce27d3dfc43e146bd2ac984749e8ed2c41180ebb01218ee78010a599/potpourri3d-1.4.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0844aac607ac9d17b1a949392a67b0901524d01af330d201428a42e2c48ee022", size = 3388360, upload-time = "2026-03-25T05:52:14.246Z" }, + { url = "https://files.pythonhosted.org/packages/55/1b/b99e7c3b1289a39c9f9e8c6d659fa2ebadd5935153b6c612f64af231967d/potpourri3d-1.4.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:79805619ca9aaf0c2faa999b6fab9809a8ec50488bd02b38d8855563d9714171", size = 3585784, upload-time = "2026-03-25T05:52:15.676Z" }, + { url = "https://files.pythonhosted.org/packages/1b/99/1959528d4a48a7773adaad9e2d7557ec1c737d3ec1c276eed60406ea2e8f/potpourri3d-1.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d6391ee9fe405e60322599df567930e8062e1a861667642f19060e1baf63586e", size = 4640603, upload-time = "2026-03-25T05:52:17.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/77/48b406d37d0b6810fb06903305b3f77c5983b85076173cb1b868b1ed7e0e/potpourri3d-1.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0c466061ac34d9b6dd1e6b26290b99a26c18d4a76b1803e34ecf7146301522e1", size = 4788065, upload-time = "2026-03-25T05:52:19.064Z" }, + { url = "https://files.pythonhosted.org/packages/a1/21/9b379ad0fa86e9fbe4c208fcd228ed01770df3b4825e942e3d56500bd476/potpourri3d-1.4.0-cp314-cp314-win32.whl", hash = "sha256:eaf9feb59feac3d79f4ba78a97e64d10f1975fd9267cda29ae4ca5e6526208e9", size = 4546780, upload-time = "2026-03-25T05:52:20.6Z" }, + { url = "https://files.pythonhosted.org/packages/4b/12/7fa0b11826304bc14edd84adf4ca7d62b8225b20d0d69e53c39b37c24e17/potpourri3d-1.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:175ac38372d857f88b64f211834de9eb19b7c7258823c82568473dd0c7e7119d", size = 6259790, upload-time = "2026-03-25T05:52:22.48Z" }, + { url = "https://files.pythonhosted.org/packages/1d/38/8c0b9233887c4b8fdd8b5a88ece711f95250532dfffe70819957c7017cea/potpourri3d-1.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:37c416d5b903ebf9fe41c7a8b19e1051bf244faabfffeeee5e78b094e3e86ce3", size = 3553756, upload-time = "2026-03-25T05:52:24.37Z" }, + { url = "https://files.pythonhosted.org/packages/e7/61/f3da1db6e2a502a5577fb8cb14d32bea02d0a0ebd441a3094550b0f7c866/potpourri3d-1.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:040202b00acaddc474dbfbed04a7786f3366f2c94233ae31dc80381e8b23cebd", size = 3161572, upload-time = "2026-03-25T05:52:25.956Z" }, + { url = "https://files.pythonhosted.org/packages/c0/29/4ca84d40842ec1eb2a03944628cc66fb010b5f1fdc8a702377773e39062e/potpourri3d-1.4.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:493b0566e19e51a16c20189a6643d55525b7da85a3a02104e3a1860ebe539b4e", size = 3391295, upload-time = "2026-03-25T05:52:27.509Z" }, + { url = "https://files.pythonhosted.org/packages/35/bb/78b6c615dc6655365077e43cd30673f28dbc064bc4c32437b545a03fbed8/potpourri3d-1.4.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5244afc1f66191c0048d4822586de007c8acb2616caf1bf71e488188a794024b", size = 3589125, upload-time = "2026-03-25T05:52:29.114Z" }, + { url = "https://files.pythonhosted.org/packages/9c/04/669769db8de168c94b5a0b7a3b6d209c88768df4d01e885fbfc4d656e191/potpourri3d-1.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:48394a731d87cab21b007146cf14df9ee1fa050185eeaf04246586f58c7526c7", size = 4642350, upload-time = "2026-03-25T05:52:30.953Z" }, + { url = "https://files.pythonhosted.org/packages/09/a6/8c379d3e47f68462d16dc4112378855f3324ab962eed9a907ad10de8b630/potpourri3d-1.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:51c18c6faf7587119eb589d10395b7d4132ceb0ca3ded7d0b27d3df4251e5268", size = 4791000, upload-time = "2026-03-25T05:52:32.523Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/e895305f05d87129ce0ee3f6124f58d64270d59a4f42115a3045a47f2fe8/potpourri3d-1.4.0-cp314-cp314t-win32.whl", hash = "sha256:aaa02ed42a4295e9e32bdc1a043203b629337603111b9916c63e072405bc4d3b", size = 4558065, upload-time = "2026-03-25T05:52:34.071Z" }, + { url = "https://files.pythonhosted.org/packages/84/57/b6f8f5aba4d2bd80588a094f65bd5f1ec7542d1ffd81fb6f9587cff9aa57/potpourri3d-1.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:792bbbd67af0d251345bb94ed436946e6344fb45db508e54034061060dcf628b", size = 6275645, upload-time = "2026-03-25T05:52:35.493Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, +] + +[[package]] +name = "pyfmaps" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "potpourri3d" }, + { name = "robust-laplacian" }, + { name = "scikit-learn" }, + { name = "scipy" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/1d/bf8b0d15059f89cf9f7a32adea72c7382be0b90a67bcad1ddae98ca5f164/pyfmaps-1.1.1.tar.gz", hash = "sha256:b05ce563f09f05276ad1844cd369ed7d6cd4f4e4d921b55bfa8758f7bb200637", size = 55755, upload-time = "2026-06-04T14:35:33.572Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/4b/488b23b13e1404f6a3844e4ec7f887be4bbda9628f1b0b74d05781f17c2d/pyfmaps-1.1.1-py3-none-any.whl", hash = "sha256:1d978350525adebe331ba745dcb60ec0f4caeef9f0385f8bcaf8c4d5cda31c26", size = 59660, upload-time = "2026-06-04T14:35:32.518Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pymeshfix" +version = "0.18.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/d8/c5124ee26c10bd2d77333eb5a8cddc89cb45ced9f1b173235e13ecfa6ebd/pymeshfix-0.18.1.tar.gz", hash = "sha256:c3386a123e3ecfcff59f229a4fc620ae0e03fa9c7fec025fbd2885349fa8fa3f", size = 1409525, upload-time = "2026-04-23T22:06:26.622Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/3f/c0acb23cb004270b6bd27116306bd2b775ce2c865f452b6451f168ee78d6/pymeshfix-0.18.1-cp312-abi3-macosx_10_13_x86_64.whl", hash = "sha256:365a937ce9ff3888ed994d158bdf9b9069db8d0b9684c32d3dd1056526f04980", size = 1327223, upload-time = "2026-04-23T22:06:16.984Z" }, + { url = "https://files.pythonhosted.org/packages/5e/11/49d95279878c6f2f9297229db4663e94af1b6c2dc6c2140764739b023e57/pymeshfix-0.18.1-cp312-abi3-macosx_11_0_arm64.whl", hash = "sha256:e451cade1c29974467a73eb5c1a216dce07114a27783eb83f681d572316dcbde", size = 1318217, upload-time = "2026-04-23T22:06:18.803Z" }, + { url = "https://files.pythonhosted.org/packages/9d/61/110d7a502290bcc317ebd6acfe3f2fc1064cd23175989022189ef8d5df68/pymeshfix-0.18.1-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:829fbb193dbcca117ab45fda72dfe95f18f27c09f85a21a42f33f88ace40f52a", size = 1340315, upload-time = "2026-04-23T22:06:20.718Z" }, + { url = "https://files.pythonhosted.org/packages/ef/2d/31c8fcd0dea754f4b9272be0984d6669a7d60b43db5ca33c4757b2adb262/pymeshfix-0.18.1-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:109956a0a3e0ac81ae7c76322c0224ac5a641e914a83a1a9904c75b40c916f74", size = 1351825, upload-time = "2026-04-23T22:06:22.778Z" }, + { url = "https://files.pythonhosted.org/packages/11/79/07d46f90f6244edd872d95f94ae550c6af3870733fe6fd9c2a9808b02f2c/pymeshfix-0.18.1-cp312-abi3-win_amd64.whl", hash = "sha256:e926773ff90131884b26b9f925fd6f6f6d2f35c53828b8abc49512dcef00bd47", size = 1508118, upload-time = "2026-04-23T22:06:24.866Z" }, +] + +[[package]] +name = "pymeshlab" +version = "2025.7.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/c1/a8db8ba66b5c51e700ba0eccea2c81f68a3223b775b0afe7b30a3da9a2ec/pymeshlab-2025.7.post1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:693101c1ade7aadbad114878626307bf434fe607de873449fbe6b6b95a30cd01", size = 64995577, upload-time = "2026-01-30T08:40:55.627Z" }, + { url = "https://files.pythonhosted.org/packages/f8/0d/6692ef6d96fa27763899665562c11ee6411eba55b2bddcda6799213dadac/pymeshlab-2025.7.post1-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:265503a06968420c8eb6934e3db78ff0767518000d8afd46ef3c976a506cfcf7", size = 54506925, upload-time = "2026-01-30T08:40:58.707Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8d/6082aa85f8c918901221d1a7e1e810cb5b56eecbed59a5e688feea92c148/pymeshlab-2025.7.post1-cp312-cp312-manylinux_2_35_aarch64.whl", hash = "sha256:d39fbaac1274adbbdf75cd999710d2ba48bd4d9087f34815afbdedb99dd3ded6", size = 65341068, upload-time = "2026-01-30T08:41:02.003Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4c/b0b1b5ad425acd80abcff4632f0241ca490c52ef9c48013d71f573680a2a/pymeshlab-2025.7.post1-cp312-cp312-manylinux_2_35_x86_64.whl", hash = "sha256:bc453d89b114671affc747991a939b257d2320b71885b31213190c64081f5c35", size = 106530409, upload-time = "2026-01-30T08:41:05.155Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/66b91d2d77fbb2bf729dfe58692957bad3cea706ecb3fe51fca4b07c0c95/pymeshlab-2025.7.post1-cp312-cp312-win_amd64.whl", hash = "sha256:fc04cb2206e8d0194b2c8f61ea6ef54505bbe3d217e7f1960ee9608464f9bfad", size = 55211790, upload-time = "2026-01-30T08:41:08.339Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/ef68bf08d23f44118ab846b93dd30eaf87ad85cc44b4f3e5ac8764e9bc9f/pymeshlab-2025.7.post1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:841b1fa15fc76ab73e594f4bbfcbe339782b36be67d9387ff5aa5f4ca422e180", size = 64995970, upload-time = "2026-01-30T08:41:11.153Z" }, + { url = "https://files.pythonhosted.org/packages/27/52/a11aa52eb3b250f78a891b9e4ffcdf4af4da13a462d079cc6cf3d17d34ac/pymeshlab-2025.7.post1-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:06c7171f70ff86753535d1c0d952ead34854923061af37197e9cc22dea515e9d", size = 54507011, upload-time = "2026-01-30T08:41:14.507Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4e/31b08dddba8c77676c9cf7ed90810b7dfcbd99cba845bb9b20b7bbaa4d7c/pymeshlab-2025.7.post1-cp313-cp313-manylinux_2_35_aarch64.whl", hash = "sha256:2432dffe99e58e1b5e77dbfeb7690502b6d0ebccb1ed75356fca4676ec56c9a2", size = 65242911, upload-time = "2026-01-30T08:41:17.622Z" }, + { url = "https://files.pythonhosted.org/packages/7f/af/3f7597d43813e8918bc2798aeed9b3be123a62e8a5be7de032cce03db8ed/pymeshlab-2025.7.post1-cp313-cp313-manylinux_2_35_x86_64.whl", hash = "sha256:99bb0f83ad20c61bdc63beb8d7f0c9ed3af6912a4770da170ce8d4bd02a7ecb0", size = 106407568, upload-time = "2026-01-30T08:41:25.759Z" }, + { url = "https://files.pythonhosted.org/packages/a2/69/1732c3222ce63758c32746841099ac82059907cc3ff6bd77499da78abcbc/pymeshlab-2025.7.post1-cp313-cp313-win_amd64.whl", hash = "sha256:25eb2578dd6c4d1b2e0253fb3b4f8f7895e8bb4f3f1236832db0ab58e6a44998", size = 55211755, upload-time = "2026-01-30T08:41:29.593Z" }, + { url = "https://files.pythonhosted.org/packages/29/e6/68bb19ec11de2f60bc3319e0ce93aba4082ec82a6bbc03dd142f0d319585/pymeshlab-2025.7.post1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c9990cd5e4620fc598795baf91f1b7b37ebb65a7b87eb01beb54d37339207d46", size = 64996542, upload-time = "2026-01-30T08:41:33.604Z" }, + { url = "https://files.pythonhosted.org/packages/65/0d/58258e56400f3a5ff9df565b4ff48616568af25aa5d8a70eb2afee467709/pymeshlab-2025.7.post1-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:472b7247e4ae9b9937d9a13bfb78b9e6e777b805ae20a114101f9dcc15bea8fc", size = 54507346, upload-time = "2026-01-30T08:41:36.517Z" }, + { url = "https://files.pythonhosted.org/packages/ac/01/b26a8c0d70a76fc333a620ce1ee7b11f8ab4b3791306350e28a578dcb47f/pymeshlab-2025.7.post1-cp314-cp314-manylinux_2_35_aarch64.whl", hash = "sha256:de6468bf43d5eeb23cdac78fe30286694ff5ad6d349a9bfa44584ac7e434e054", size = 65417867, upload-time = "2026-01-30T08:41:39.434Z" }, + { url = "https://files.pythonhosted.org/packages/79/c0/9b769a1acd7ce4405059820793c2f14129d9faba99b595ea35c8c9d8c57d/pymeshlab-2025.7.post1-cp314-cp314-manylinux_2_35_x86_64.whl", hash = "sha256:951fe5aeb3e92632f3db3a1a6525279b41e5d7191c6b9011429dbb0b67f2f0d3", size = 106572030, upload-time = "2026-01-30T08:41:43.909Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f0/65e3b96a14e9c80e05c085f8b32b923ccd5a70acf63eb66528e3fa94cf54/pymeshlab-2025.7.post1-cp314-cp314-win_amd64.whl", hash = "sha256:881389fb62ac832c81555fe22a3990a81624deb78e78ca3862836075d5c62c2b", size = 56595688, upload-time = "2026-01-30T08:41:47.145Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "robust-laplacian" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/70/c33cbba6c85563318719d589af115754e82c5fbdd1df518e1ca42dc7edac/robust_laplacian-1.1.0.tar.gz", hash = "sha256:1db9eeb1a7aa340c3da902b125d2f58c7138feda206a72c2402dd25e702290d2", size = 29564480, upload-time = "2026-03-25T05:18:58.55Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/a4/b3c0ce59a1594dbcb1163a9b95a8f800154763b5e4995b63011408a94bc6/robust_laplacian-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6dc9bd17bef1fd2790552d839765c75fa38a26e78a0d9ef98252feacc5f2e6af", size = 2681923, upload-time = "2026-03-25T05:17:33.424Z" }, + { url = "https://files.pythonhosted.org/packages/52/a6/f68bfaed37776e77a61d77530ae74a780e299bbba8fed174485717cec73f/robust_laplacian-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87c62374c7bb4f5089cfba63ffb1ff2b74f191d952c2d5ea29b1e7033c2a81af", size = 2421557, upload-time = "2026-03-25T05:17:35.47Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/104e7443bce304f335fdcd01f30d1b387abfdbcf17b4c90cc8c0f9f71a14/robust_laplacian-1.1.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6273ffaa716c69bdfa3ef57250ec89691f0f051dbedd53aa929a8c9851f9cbbf", size = 2506936, upload-time = "2026-03-25T05:17:37.314Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ed/1c6c677225a7a65d108eff0e2642a55c3f8f805c93fd98b3013f55afc0f6/robust_laplacian-1.1.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c68daef7b3fe77e02e7eae4ae111699f24bd1457a2cffd9330a87393e8dacb3", size = 2626192, upload-time = "2026-03-25T05:17:39.194Z" }, + { url = "https://files.pythonhosted.org/packages/ba/40/89e21e80c76193aa7a0e8c93da40d90e82d5ec001146b288d2fb33c22639/robust_laplacian-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:feae0f5891ac3b4a3cb68d78c498a80440a245e12ec1ec14aad9392db62f84d5", size = 3691495, upload-time = "2026-03-25T05:17:40.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/b0/01333e1e65d40a87a816812c52ce83db5fb9c0d42200e40e69e8d5cbed35/robust_laplacian-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9abd4ee205c4a9f82f50824813e9f59d6a49d9238a651a94d8615c4115397b1c", size = 3780444, upload-time = "2026-03-25T05:17:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/24/98/b0b0012a9581845d3c765a7599ddf150b53b07a5f55bed1d8eda89acfe7a/robust_laplacian-1.1.0-cp312-cp312-win32.whl", hash = "sha256:7332c63984b1bb89e4a51564f3dd2db4dc84a5d7caa64a0222f4dcbd3a02fb2e", size = 3768903, upload-time = "2026-03-25T05:17:44.991Z" }, + { url = "https://files.pythonhosted.org/packages/2e/29/97def3f32a819a192b69a62e13502d98ed7fdfd237dedf343f9030ce9585/robust_laplacian-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:72a0e813901d8cfeca3509b7feb05cd7ecabdafb84745b1485bb12ce8a1cc0b9", size = 5245580, upload-time = "2026-03-25T05:17:47.359Z" }, + { url = "https://files.pythonhosted.org/packages/41/55/3397ed8321b94f3067bf6f35822ad883729a6b254f6e87b803d9f8762c99/robust_laplacian-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:85a7fdc4e8779ad1aa66320a096e9a0f7d524e09ed825bd2ecce42537fceded1", size = 2681910, upload-time = "2026-03-25T05:17:49.176Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9f/0f9f2c53275f41449573548ac7d2d02f2a4430cd877872a21a120a65be4e/robust_laplacian-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c7ef879194434f717394070344940de1b1eebd86c678a6f61314789fcd0cb87", size = 2421614, upload-time = "2026-03-25T05:17:51.772Z" }, + { url = "https://files.pythonhosted.org/packages/73/40/ea98f442943013605d61617782c82831b632ed07799d3b5e9ca58cee11dd/robust_laplacian-1.1.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3e8769cbf7df61e17a0cc19d1496bb46181e8ab982e99709d69985d028a6e5e9", size = 2506927, upload-time = "2026-03-25T05:17:53.663Z" }, + { url = "https://files.pythonhosted.org/packages/65/c7/dde609e5faa829fe8d874dfa27d3cfbecda97066377bb9eca1dc1f77bf7d/robust_laplacian-1.1.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0394bfa2decf06e3e47e37e05c11fb0580fc8ea2cbd0621a8dd0df55e1cf034b", size = 2626040, upload-time = "2026-03-25T05:17:55.449Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f1/21bb73fd7d248cb34896d6bb00ecb4fafddf570dff6fb8bdeb739060b068/robust_laplacian-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:93665feffdddc4c7e81169b72708547a0c413b4d13285dfa5eba75d5948aff2c", size = 3691462, upload-time = "2026-03-25T05:17:57.599Z" }, + { url = "https://files.pythonhosted.org/packages/71/6d/aed120863c93197363568d7bdf28e14176d7af67d93fcc86cc2fd4007c61/robust_laplacian-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9c0b47e8da61670241ec6ec1f4f9c16e213d8805171be19db9414ad94d28d44d", size = 3780421, upload-time = "2026-03-25T05:17:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7a/bd3609fac573ec8fc5a19f625008c22f5dcc957c8db0dec9b9ce559209e7/robust_laplacian-1.1.0-cp313-cp313-win32.whl", hash = "sha256:cbd417d28b9874e85bfc9fefb6bc062d1f8ce257fad9121e8f3ed98278ed26b1", size = 3768859, upload-time = "2026-03-25T05:18:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/13/92/a9c432ed0144512a7ac31092b849cb27aa11be686ef49ec4fc96eebab208/robust_laplacian-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:3735212b371e2bac1b111188a0981626f4628b428d834133f4ecb2897adc4b11", size = 5245559, upload-time = "2026-03-25T05:18:03.622Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4d/2e120e487f845443f0eccfcc493d0c0ed214d28559c31f49581265dbac8c/robust_laplacian-1.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:46eb8f26b280fd52fd63e011d1716c6d32a5a47c480d3892d15dc02ae743f36e", size = 2683713, upload-time = "2026-03-25T05:18:05.563Z" }, + { url = "https://files.pythonhosted.org/packages/9f/36/31caf10da7daa0352023a554f24713411a8436db677065f2c5b85d5d53a0/robust_laplacian-1.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8199985cdcd2f1b539854ce393a017d306bc0d008287a2754ef1b520e33e6693", size = 2421813, upload-time = "2026-03-25T05:18:07.567Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e1/b87589339c05bbaf1865e4b07267c024a6fb0faa77f78c88a3204a9cc8d9/robust_laplacian-1.1.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0499999e03bce72d2c53e9392d6e135c4a2247ef0a3b1c20b113069d197ec034", size = 2507446, upload-time = "2026-03-25T05:18:09.681Z" }, + { url = "https://files.pythonhosted.org/packages/96/c2/9f6bbdd0b1cc46b962b358dd2b344aa0d050bade12833ae2de5f9aef3785/robust_laplacian-1.1.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfa504d41b49a3743e255e3441b58609a804dc066ea115fb1a7cc755123d0fae", size = 2626774, upload-time = "2026-03-25T05:18:11.816Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f0/02e74e2531b7679447f106fdca9966e6d0d9620d4bf8656ae62a34127a2f/robust_laplacian-1.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a558e6b5e91d700fec3ce14411de13a040dd0aa195b711132e3743377a74360e", size = 3691998, upload-time = "2026-03-25T05:18:13.941Z" }, + { url = "https://files.pythonhosted.org/packages/52/f3/13d099211dc10bcbfe314bcdb413d98adcd9fc05e2dac12b6d1f4be72ad1/robust_laplacian-1.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c7b12ecc54e71a14b57960a41b90448756b837206e5d442e01c09a76ba4146b3", size = 3780556, upload-time = "2026-03-25T05:18:17.452Z" }, + { url = "https://files.pythonhosted.org/packages/fa/58/8a95760c52ff394438b78a61c9b1dd40a887d59ad4bd7b7c972efbcac272/robust_laplacian-1.1.0-cp314-cp314-win32.whl", hash = "sha256:a804749cffbd33405d4c3031c51194fad47f72e8330f3f5ffdca451686d62d94", size = 3825561, upload-time = "2026-03-25T05:18:19.367Z" }, + { url = "https://files.pythonhosted.org/packages/21/c9/b37350d1bb9dd8e64acc68eda79ab801aa6a7153b9fbb8e8806bd65bd87c/robust_laplacian-1.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:218a4c3d2b8a679b2472396db659a37e68c89b324b6f1a078beb6b439a31c84f", size = 5342647, upload-time = "2026-03-25T05:18:21.57Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f7/6cf33cdd13febd6176aac439b1ec9a7d808caa4934d890c83074ee003e93/robust_laplacian-1.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:e9f29af96f927b2f43e8ea9323d12239c8f5fbabef0fc72595e85d50cab00d8d", size = 2687317, upload-time = "2026-03-25T05:18:23.801Z" }, + { url = "https://files.pythonhosted.org/packages/86/01/bc818b33ce15fe3f33173bc4e8b2980939707258e63416a10d40648d0436/robust_laplacian-1.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:926719b1988826fedb6848f6bd9225a164d21236764125a3f528c7f2f12e51b9", size = 2426112, upload-time = "2026-03-25T05:18:25.851Z" }, + { url = "https://files.pythonhosted.org/packages/a5/01/24016b72e1c4c4e8c4b0f1346c94d4a00fac9fea9b7f01fb4ab385f362ef/robust_laplacian-1.1.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d57e8e09fe4291c744a3a539073e0e91a78b73dfc3898cee8437c6fcdebc373a", size = 2508720, upload-time = "2026-03-25T05:18:27.683Z" }, + { url = "https://files.pythonhosted.org/packages/23/26/db785ae09346130b3b7278465dcb8b29f2918f7df243def37e66ad8b6ae2/robust_laplacian-1.1.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:06dbf4dd2e26d0e465c7fcd7fa26705f95fa4640bde6919297461b6d298709b5", size = 2630057, upload-time = "2026-03-25T05:18:29.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/f3/4feb2c7f483051f86c132f776cfb18089644672655ca580b0db4a5e4d86a/robust_laplacian-1.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5ca98457920b80711d5bf7471d1c11262c7905d1b3742565ff6b3425850b9b88", size = 3693578, upload-time = "2026-03-25T05:18:31.66Z" }, + { url = "https://files.pythonhosted.org/packages/2b/50/606c287fc14e5b6948caf9580bf86c7cedefc0dbea66fe17b169b0e46c72/robust_laplacian-1.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:363bd3ceaf765b3de87b92db8133de3efbc0baf3edf1251e7a39551932bc52da", size = 3781425, upload-time = "2026-03-25T05:18:33.771Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ef/385299067273bec3363e6ef1f5a6e157e36049d86ef3b66845ac16a1887d/robust_laplacian-1.1.0-cp314-cp314t-win32.whl", hash = "sha256:8e9e84307b6090d8a51f5f4738d1cd07f3dcaba8ffcc0672f634a20f0808ea2a", size = 3830581, upload-time = "2026-03-25T05:18:36.106Z" }, + { url = "https://files.pythonhosted.org/packages/ee/93/e6eb3973feaeee248487e6eda11b3f0ffd1a7772a558a5e126056331e874/robust_laplacian-1.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d3d501116875d4cf4d5a9e2f594ac58e0c003468228cb9961b4223d3974ebeb0", size = 5349007, upload-time = "2026-03-25T05:18:38.477Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/aa/3996e2196075689afb9fce0410ebdb4a09099d7964d061d7213700204409/scikit_learn-1.7.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8d91a97fa2b706943822398ab943cde71858a50245e31bc71dba62aab1d60a96", size = 9259818, upload-time = "2025-09-09T08:20:43.19Z" }, + { url = "https://files.pythonhosted.org/packages/43/5d/779320063e88af9c4a7c2cf463ff11c21ac9c8bd730c4a294b0000b666c9/scikit_learn-1.7.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:acbc0f5fd2edd3432a22c69bed78e837c70cf896cd7993d71d51ba6708507476", size = 8636997, upload-time = "2025-09-09T08:20:45.468Z" }, + { url = "https://files.pythonhosted.org/packages/5c/d0/0c577d9325b05594fdd33aa970bf53fb673f051a45496842caee13cfd7fe/scikit_learn-1.7.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e5bf3d930aee75a65478df91ac1225ff89cd28e9ac7bd1196853a9229b6adb0b", size = 9478381, upload-time = "2025-09-09T08:20:47.982Z" }, + { url = "https://files.pythonhosted.org/packages/82/70/8bf44b933837ba8494ca0fc9a9ab60f1c13b062ad0197f60a56e2fc4c43e/scikit_learn-1.7.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d6e9deed1a47aca9fe2f267ab8e8fe82ee20b4526b2c0cd9e135cea10feb44", size = 9300296, upload-time = "2025-09-09T08:20:50.366Z" }, + { url = "https://files.pythonhosted.org/packages/c6/99/ed35197a158f1fdc2fe7c3680e9c70d0128f662e1fee4ed495f4b5e13db0/scikit_learn-1.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:6088aa475f0785e01bcf8529f55280a3d7d298679f50c0bb70a2364a82d0b290", size = 8731256, upload-time = "2025-09-09T08:20:52.627Z" }, + { url = "https://files.pythonhosted.org/packages/ae/93/a3038cb0293037fd335f77f31fe053b89c72f17b1c8908c576c29d953e84/scikit_learn-1.7.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b7dacaa05e5d76759fb071558a8b5130f4845166d88654a0f9bdf3eb57851b7", size = 9212382, upload-time = "2025-09-09T08:20:54.731Z" }, + { url = "https://files.pythonhosted.org/packages/40/dd/9a88879b0c1104259136146e4742026b52df8540c39fec21a6383f8292c7/scikit_learn-1.7.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:abebbd61ad9e1deed54cca45caea8ad5f79e1b93173dece40bb8e0c658dbe6fe", size = 8592042, upload-time = "2025-09-09T08:20:57.313Z" }, + { url = "https://files.pythonhosted.org/packages/46/af/c5e286471b7d10871b811b72ae794ac5fe2989c0a2df07f0ec723030f5f5/scikit_learn-1.7.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:502c18e39849c0ea1a5d681af1dbcf15f6cce601aebb657aabbfe84133c1907f", size = 9434180, upload-time = "2025-09-09T08:20:59.671Z" }, + { url = "https://files.pythonhosted.org/packages/f1/fd/df59faa53312d585023b2da27e866524ffb8faf87a68516c23896c718320/scikit_learn-1.7.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a4c328a71785382fe3fe676a9ecf2c86189249beff90bf85e22bdb7efaf9ae0", size = 9283660, upload-time = "2025-09-09T08:21:01.71Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c7/03000262759d7b6f38c836ff9d512f438a70d8a8ddae68ee80de72dcfb63/scikit_learn-1.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:63a9afd6f7b229aad94618c01c252ce9e6fa97918c5ca19c9a17a087d819440c", size = 8702057, upload-time = "2025-09-09T08:21:04.234Z" }, + { url = "https://files.pythonhosted.org/packages/55/87/ef5eb1f267084532c8e4aef98a28b6ffe7425acbfd64b5e2f2e066bc29b3/scikit_learn-1.7.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9acb6c5e867447b4e1390930e3944a005e2cb115922e693c08a323421a6966e8", size = 9558731, upload-time = "2025-09-09T08:21:06.381Z" }, + { url = "https://files.pythonhosted.org/packages/93/f8/6c1e3fc14b10118068d7938878a9f3f4e6d7b74a8ddb1e5bed65159ccda8/scikit_learn-1.7.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:2a41e2a0ef45063e654152ec9d8bcfc39f7afce35b08902bfe290c2498a67a6a", size = 9038852, upload-time = "2025-09-09T08:21:08.628Z" }, + { url = "https://files.pythonhosted.org/packages/83/87/066cafc896ee540c34becf95d30375fe5cbe93c3b75a0ee9aa852cd60021/scikit_learn-1.7.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98335fb98509b73385b3ab2bd0639b1f610541d3988ee675c670371d6a87aa7c", size = 9527094, upload-time = "2025-09-09T08:21:11.486Z" }, + { url = "https://files.pythonhosted.org/packages/9c/2b/4903e1ccafa1f6453b1ab78413938c8800633988c838aa0be386cbb33072/scikit_learn-1.7.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191e5550980d45449126e23ed1d5e9e24b2c68329ee1f691a3987476e115e09c", size = 9367436, upload-time = "2025-09-09T08:21:13.602Z" }, + { url = "https://files.pythonhosted.org/packages/b5/aa/8444be3cfb10451617ff9d177b3c190288f4563e6c50ff02728be67ad094/scikit_learn-1.7.2-cp313-cp313t-win_amd64.whl", hash = "sha256:57dc4deb1d3762c75d685507fbd0bc17160144b2f2ba4ccea5dc285ab0d0e973", size = 9275749, upload-time = "2025-09-09T08:21:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/dee5acf66837852e8e68df6d8d3a6cb22d3df997b733b032f513d95205b7/scikit_learn-1.7.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fa8f63940e29c82d1e67a45d5297bdebbcb585f5a5a50c4914cc2e852ab77f33", size = 9208906, upload-time = "2025-09-09T08:21:18.557Z" }, + { url = "https://files.pythonhosted.org/packages/3c/30/9029e54e17b87cb7d50d51a5926429c683d5b4c1732f0507a6c3bed9bf65/scikit_learn-1.7.2-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:f95dc55b7902b91331fa4e5845dd5bde0580c9cd9612b1b2791b7e80c3d32615", size = 8627836, upload-time = "2025-09-09T08:21:20.695Z" }, + { url = "https://files.pythonhosted.org/packages/60/18/4a52c635c71b536879f4b971c2cedf32c35ee78f48367885ed8025d1f7ee/scikit_learn-1.7.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9656e4a53e54578ad10a434dc1f993330568cfee176dff07112b8785fb413106", size = 9426236, upload-time = "2025-09-09T08:21:22.645Z" }, + { url = "https://files.pythonhosted.org/packages/99/7e/290362f6ab582128c53445458a5befd471ed1ea37953d5bcf80604619250/scikit_learn-1.7.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96dc05a854add0e50d3f47a1ef21a10a595016da5b007c7d9cd9d0bffd1fcc61", size = 9312593, upload-time = "2025-09-09T08:21:24.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/87/24f541b6d62b1794939ae6422f8023703bbf6900378b2b34e0b4384dfefd/scikit_learn-1.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:bb24510ed3f9f61476181e4db51ce801e2ba37541def12dc9333b946fc7a9cf8", size = 8820007, upload-time = "2025-09-09T08:21:26.713Z" }, +] + +[[package]] +name = "scipy" +version = "1.18.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/19/ca10ead60b0acc80b2b833c2c4a4f2ff753d0f58b811f70d911c7e94a25c/scipy-1.18.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7bd21faaf5a1a3b2eff922d02db5f191b99a6518db9078a8fb23169f6d22259a", size = 31056519, upload-time = "2026-06-19T14:59:45.203Z" }, + { url = "https://files.pythonhosted.org/packages/96/72/1e6442a00cd2924d361aa1b642ab6373ec35c6fabf311a760be9f76e0f13/scipy-1.18.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:265915e79107de9f946b855e50d7470d5893ec3f54b342e1aa6201cbdcd8bb6b", size = 28681889, upload-time = "2026-06-19T14:59:48.103Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2d/11dd93d21e147a73ba22bd75c0b9208d3a2e0ec76d53170ce7d9029b1015/scipy-1.18.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9ab7b758be6940954a713ee466e2043e9f6e2ed965c1fce5c91039f4be3d90a9", size = 20423580, upload-time = "2026-06-19T14:59:50.665Z" }, + { url = "https://files.pythonhosted.org/packages/9c/01/93552f75e0d2a7dd115a45e59209c51e8d514daff02fc887d2623be06fe1/scipy-1.18.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:97b6cddaaee0a779ef6b5ca83c9604b27cc16b2b8fc22c142652df8793319fb8", size = 23054441, upload-time = "2026-06-19T14:59:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/3c/23/21f5e703643d66f21faa6b4c73195bfcad70c55efcb4f1ab327cd7c4101a/scipy-1.18.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:52a96e21517c7292375c0e27dd796a811f03fcea5fd4d108fdfea8145dcf17ab", size = 33968720, upload-time = "2026-06-19T14:59:56.415Z" }, + { url = "https://files.pythonhosted.org/packages/dd/aa/1b939f6c67ed68635bb538e6752d3dacc02f66535182e939a89581a44e9c/scipy-1.18.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f55797419e16e7f30cf88ffb3113ce0467f00cfe3f70d5c281730b21769bfc2", size = 35287115, upload-time = "2026-06-19T14:59:59.411Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ff/eec46be7e9234208f801062b53e1983085eddebd693f6c9bfb03b459830d/scipy-1.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ad033410e2e0672ffdc1042110cef20e1c46f8fd0616cee1d44d8d58fad8fc11", size = 35577989, upload-time = "2026-06-19T15:00:02.235Z" }, + { url = "https://files.pythonhosted.org/packages/84/ca/210d4759c7210bb7d269437421959b39a33434e2776b60c5cb8a763bb30a/scipy-1.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a55985d54c769c872e64b7f4c8a81cc30ef700cc04296abbbf3705439c126de", size = 37421717, upload-time = "2026-06-19T15:00:05.102Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/9a9edb45345bd6744da5ddfb6628e5d5185920494c6a67ec45b6381004cb/scipy-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:71ccc8faa2dd16ac310233203474a8b5cb67f10dedd54a3116d34943f4b19132", size = 36597428, upload-time = "2026-06-19T15:00:08.112Z" }, + { url = "https://files.pythonhosted.org/packages/99/0e/33f32a2a58987e26aec0f7df252cbbad1e90ae77bdbc76f40dd4ed0cf0ea/scipy-1.18.0-cp312-cp312-win_arm64.whl", hash = "sha256:d88363fd9d8fbd3511bd273f1a49efb2a540773ddf92a91d57498ce7dd7f3e76", size = 24351481, upload-time = "2026-06-19T15:00:11.103Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/9c0136c2de7ae0779b7b366447766cec6d9f0702c56bb8ffeb04c8fd3af4/scipy-1.18.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:09143f676d157d9f546d663504ef9c1becb819824f1afc018814176411942446", size = 31036107, upload-time = "2026-06-19T15:00:14.03Z" }, + { url = "https://files.pythonhosted.org/packages/02/73/0291a64843270f4efb86cdcf2ee0f2048631b65ec6b405398b2b4dbf11bf/scipy-1.18.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5efe260f69417b97ddae455bfb5a95e8359f7f66ad7fa9522a60feb66f169520", size = 28663303, upload-time = "2026-06-19T15:00:16.819Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0f/10ffa0b697a572f4e0d48b92a88895d366422f019f723e7e14a84c050dac/scipy-1.18.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:68363b7eaacd8b5dd426df56d782cc156468ac79a127a1b87ca597d6e2e82197", size = 20404960, upload-time = "2026-06-19T15:00:19.635Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d2/e896cea21ba8edd6c81d4c55b1ffcc717e79698dcbebf9641b4cfb4c6622/scipy-1.18.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:c5557d8be5da8e41353fcd4d21491fdbab83b062fc579e94dc09a7c8ab4f669b", size = 23034074, upload-time = "2026-06-19T15:00:22.107Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b2/e83ea34279a52c03374477c74006256ec78df65fc877baa4617d6de1d202/scipy-1.18.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d13bca67c096d89fb95ced0d8921807300fce0275643aef9533cc63a0773468", size = 33942038, upload-time = "2026-06-19T15:00:24.964Z" }, + { url = "https://files.pythonhosted.org/packages/f6/af/e8fe5fb136f51e2b01678b92cb4106d10d8cd68ec147ead2e7cb0ac75398/scipy-1.18.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a46f9273dbd0eb1cefba61c9b8648b4dfe3cbc14a080176f9a73e44b8336dc7f", size = 35266390, upload-time = "2026-06-19T15:00:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/3a/49/2c5cbb907b56695fc67517811d1db234dfd83381a84814ec220aded2794d/scipy-1.18.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5aba46108853ddfc77906b6557aac839d2b52e900c1d72a1180adaaab58d265f", size = 35551324, upload-time = "2026-06-19T15:00:31.014Z" }, + { url = "https://files.pythonhosted.org/packages/bb/73/eda39f7a2d306ff0ffc574afd13c0bbb6d10a603d9a413998ee269487a80/scipy-1.18.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b6f758e35f12757b5d95c00bc6de2438e229c2664b7a92e96f205959d9f2dfa4", size = 37404785, upload-time = "2026-06-19T15:00:34.072Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d2/ae881ee28d014f38e0ccbfd974a06a919ba9af34f1f74bf42b5301891d63/scipy-1.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:1afac4a847207c7ff8efd321734a50b06d0280b3b2a2c0fc2f413101747ad7c7", size = 36554943, upload-time = "2026-06-19T15:00:36.903Z" }, + { url = "https://files.pythonhosted.org/packages/70/3a/21154e2d54eb3639c6bf4dbae2e531c68356bfe95990daa30df33b30d556/scipy-1.18.0-cp313-cp313-win_arm64.whl", hash = "sha256:c5dbddf60e58c2312316d097271a8e73d40eaf2eabfa4d95ed7d3695bbf2ce7b", size = 24350911, upload-time = "2026-06-19T15:00:40.062Z" }, + { url = "https://files.pythonhosted.org/packages/78/b5/915a19b3de2f7430062b509653563db1633ddbb6f021b06731521115d4e2/scipy-1.18.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4c256ee70c0d1a8a2ace807e199ccd4e3f57037433842abb3fb36bc17eaa9578", size = 31036253, upload-time = "2026-06-19T15:00:43.216Z" }, + { url = "https://files.pythonhosted.org/packages/d7/88/b72def7262e150d16be13fca37a96481138d624e700340bc3362a7588929/scipy-1.18.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:2ef3abc54a4ffc53765374b0d5728532dfdd2585ed23f6b11c206a1f0b1b9af8", size = 28673758, upload-time = "2026-06-19T15:00:46.663Z" }, + { url = "https://files.pythonhosted.org/packages/91/02/2e636a61a525632c373cf6a9c24442a3ffb79e364d38e98b32042964ac32/scipy-1.18.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2a6af57bd9e4a75d70e4117e78a1bbee84f79ae3fbb6d0111005d6ebcc4cb8d", size = 20415514, upload-time = "2026-06-19T15:00:49.399Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b6/2135974442f6aba159d9d39d774a1c8cb19947016725d69fecc685df45bf/scipy-1.18.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:3f1ac564d3bf6c03d861d2cd87a1bea0da2887136f7fb1bf519c05a8971452d6", size = 23034398, upload-time = "2026-06-19T15:00:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e6/ba89ec5abf6ee9257c0d1ec985573f3ae32742c24bc03e016388a40b1b15/scipy-1.18.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40395a5fcd1abee49a5c7aaa98c29db393eedc835138560a588c47ec16156690", size = 33998032, upload-time = "2026-06-19T15:00:54.838Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c4/bc41eb19b0fd0db868f4132920879019318d80cc522ad8f2bca4611af808/scipy-1.18.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ca01e8ae69f1b18e9a58d91afead31be3cef0dd905a10249dac559ee15460a0", size = 35283333, upload-time = "2026-06-19T15:00:58.152Z" }, + { url = "https://files.pythonhosted.org/packages/53/a4/cbdeef6eb3830a8462a9d4ada814de5fc984345cc9ecf17cbec51a036f1e/scipy-1.18.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7a7f3b01647384dbc3a711e8c6778e0aabbe93959249fef5c7393396bcac0867", size = 35610216, upload-time = "2026-06-19T15:01:01.155Z" }, + { url = "https://files.pythonhosted.org/packages/80/4d/b2b82502b65f661d1b789c1665dcdf315d5f12194e06fc0b37946294ebae/scipy-1.18.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6aa94e78ec192a30063a5e72e561c28af769dc311190b24fe91774eff1969709", size = 37418960, upload-time = "2026-06-19T15:01:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/902d836831474b0ab5a37d16404f7bc5fafd9efba632890e271ba952635f/scipy-1.18.0-cp314-cp314-win_amd64.whl", hash = "sha256:2d8bbdc6c817f5b4006a54d799d4f5bab6f910193cbb9a1ff310833d4d270f61", size = 37288845, upload-time = "2026-06-19T15:01:07.822Z" }, + { url = "https://files.pythonhosted.org/packages/b6/43/8d73b337a3bdb14daa0314f0434210747c02d79d729ce1777574a817dcf6/scipy-1.18.0-cp314-cp314-win_arm64.whl", hash = "sha256:18e9575f1569b2c54174e6159d32942e03731177f63dce7975f0a0c88d102f5b", size = 24988971, upload-time = "2026-06-19T15:01:11.076Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b4/f11918b0508a2787031a0499a03fbe3546f3bb5ca05d01038c45b278c09a/scipy-1.18.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f351e0dd702687d12a402b867a1b4146a256923e1c38317cbc472f6372b94707", size = 31399325, upload-time = "2026-06-19T15:01:13.723Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d1/1f287b57c0ff0ee5185dff3946d92c8017d39b0e431f0ae79a3ff1859512/scipy-1.18.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7c7a51b33ce387193c97f228320cf8e87361daa1bba750638677729598b3e677", size = 29092110, upload-time = "2026-06-19T15:01:16.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1a/7b74eb6c392fdcb27d414c0e7558a6d0231eb3b6d73571f479bb81ea8794/scipy-1.18.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:84031d7b052a54fae2f8632e0ec802073d385476eb9a63079bce6e23ef9283d4", size = 20833811, upload-time = "2026-06-19T15:01:20.488Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ad/f3941716320a7b9cb4d68734a903b45fe16eff5fb7da7e16f2e619304979/scipy-1.18.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:56abf29a7c067dde59be8b9a22d606a4ea1b2f2a4b756d9d903c62818f5dacce", size = 23396644, upload-time = "2026-06-19T15:01:23.364Z" }, + { url = "https://files.pythonhosted.org/packages/22/22/1446b62ffe07f9719b7d9b1b6a4e05a772833ae8f441fe4c22c34c9b250f/scipy-1.18.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ad44305cfa24b1ba5803cbbebf033590ccbac1aa5d612d727b785325ab408b0", size = 34079318, upload-time = "2026-06-19T15:01:26.002Z" }, + { url = "https://files.pythonhosted.org/packages/56/3b/b87da667098bb470fa30c7011b0ba351ee976dd395c78798c66e941665a3/scipy-1.18.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:945c1761b93f38d7f99ae81ae80c63e621471608c7eeead563f6df025585cd58", size = 35324320, upload-time = "2026-06-19T15:01:28.881Z" }, + { url = "https://files.pythonhosted.org/packages/f8/a1/c7932f91909759b0267f75fdea34e91309f96b895757534b76a90b6b4344/scipy-1.18.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1a4441f15d620578772a49e5ab48c0ee1f7a0220e387110283062729136b2553", size = 35699541, upload-time = "2026-06-19T15:01:31.968Z" }, + { url = "https://files.pythonhosted.org/packages/f7/86/5185061a1fcc41d18c5dc2463969b3a3964b31d9ac67b2fb05d4c7ff7670/scipy-1.18.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9aac6192fac56bf2ca534389d24623f07b39ff83317d58287285e7fbd622ff76", size = 37472480, upload-time = "2026-06-19T15:01:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/31/8e/f04c68e39919a010d34f2ee1367fd705b0a25a02f609d755f0bfbc0a15fc/scipy-1.18.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e40baea28ae7f5475c779741e2d90b1247c78531207b49c7030e698ff81cee3f", size = 37365390, upload-time = "2026-06-19T15:01:38.091Z" }, + { url = "https://files.pythonhosted.org/packages/d5/19/969dc072906c84dd0a3b05dcf57ea750936087d7873549e408b35cfc3f97/scipy-1.18.0-cp314-cp314t-win_arm64.whl", hash = "sha256:368e0a705903c466aa5f08eefb39e6b1b6b2d659e7352a31fd9e2438365be0f8", size = 25279661, upload-time = "2026-06-19T15:01:40.817Z" }, +] + +[[package]] +name = "setuptools" +version = "81.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/1c/73e719955c59b8e424d015ab450f51c0af856ae46ea2da83eba51cc88de1/setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a", size = 1198299, upload-time = "2026-02-06T21:10:39.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021, upload-time = "2026-02-06T21:10:37.175Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "torch" +version = "2.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings", marker = "sys_platform == 'linux'" }, + { name = "cuda-toolkit", extra = ["cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "nvidia-cublas", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" }, + { name = "setuptools" }, + { name = "sympy" }, + { name = "triton", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/54/efb7ebca77970012b0cc21687a55d70eb2ba514b2c2b8e18d9fb1222f3be/torch-2.12.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:d2dd0f2c5f7ccbddaf34cade0deaf476808368f902b9cdb7f36a2ab42301bc0e", size = 87991951, upload-time = "2026-06-17T21:07:49.309Z" }, + { url = "https://files.pythonhosted.org/packages/1e/00/4210d76ca7424981f04033ebe7e48816ab83287a62538747a58825db770c/torch-2.12.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:2de4e19b88a481482c6c75291f2d6a52eda3ce51f311b29aa9b68499c830c07c", size = 426382721, upload-time = "2026-06-17T21:06:41.842Z" }, + { url = "https://files.pythonhosted.org/packages/76/1f/bc9f5a5aa569307076365f25afcebacb22e9c754b1bcfbaaa146627c7fda/torch-2.12.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:649e4ced014ba646f76f8cb9c9726735a6323eb321b7919f942790a923f90921", size = 532261322, upload-time = "2026-06-17T21:06:06.673Z" }, + { url = "https://files.pythonhosted.org/packages/9e/49/c549461daa008159d006a76a991fbc2f26fa8bac27a4030c858463dcb20f/torch-2.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:e86550597877fb272ddc52db2f85b82cb601ea7bd932576a0340152cae2200b3", size = 122988095, upload-time = "2026-06-17T21:07:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4a/0300261818e1560d72cc160ac826005507e8b7ca0a35788b591436d05b4a/torch-2.12.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:c75e93173c700bccd6bfcc4a9d19ce242ab6dacd1f1781483027a16239b9e650", size = 87992358, upload-time = "2026-06-17T21:07:40.299Z" }, + { url = "https://files.pythonhosted.org/packages/30/a7/874a5ca05e8f159211dca7921060f7057acc1adb26431e119fd150623efc/torch-2.12.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:fcb61ccd20784b62bdd78ec84238a5cfb383b4994902e03bac95505ab360884c", size = 426386134, upload-time = "2026-06-17T21:07:31.481Z" }, + { url = "https://files.pythonhosted.org/packages/e1/75/20bb8fe9c1ad6538cce8cd0391b51927ae5af0b17ed1eab44b8824465dc1/torch-2.12.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:f4afc8083dff08719edbea346644476e3cec0cf40ebe256be0ee5d5b7c7e8c0d", size = 532268019, upload-time = "2026-06-17T21:05:37.925Z" }, + { url = "https://files.pythonhosted.org/packages/d1/fa/824ddb662af55b2eabc0dbb7b57c7c0b1bcd93693754a2b8509ec4d16490/torch-2.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:f92609e3b3ce72f25e2eb780d043ced2480c1a86c47c852604fc7a9108648386", size = 122987777, upload-time = "2026-06-17T21:07:09.49Z" }, + { url = "https://files.pythonhosted.org/packages/63/b7/1b49fe7086ea36839cc80abc43174c43d0ab6f676c0891c871c162f44fe3/torch-2.12.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e9b6f7d2dd66ea87a3ae620069d31335d594c06effb1a383bdd21cfe61e44ece", size = 88010025, upload-time = "2026-06-17T21:07:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/d7/06/5b44063a6545036dcc680d2d303b137d9176cfb2cc1e1863e3ef94abeb52/torch-2.12.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:7973ccd3d2cd35c74449213f7bded199bec6c6247e705cbeda7407af79703d91", size = 426392891, upload-time = "2026-06-17T21:05:52.261Z" }, + { url = "https://files.pythonhosted.org/packages/f8/dd/c9ce9a4b0eb3c5bb92d9ea56766e2c22559f0b45171149188494edcce80f/torch-2.12.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:c64ac4aac16be5e296dcd912305605804b203333c690bf98c55bc09494ee92ad", size = 532272494, upload-time = "2026-06-17T21:06:22.72Z" }, + { url = "https://files.pythonhosted.org/packages/21/7c/f3a601fc1b1f663ff269bfe553654e638651939aa6563e8daa7167c33098/torch-2.12.1-cp314-cp314-win_amd64.whl", hash = "sha256:f6dc4caf7eb4adb38a2d9f536b51db56310fdd1254e69a2d96767e1367c892b3", size = 122987254, upload-time = "2026-06-17T21:06:33.199Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/b8087556cf81ddd808dbeb34afb8396d7ae7a1694ab489f08b1a0004e7d0/torch-2.12.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:2afbb2bdaa8a95040e733f05492ddf133c3967c9b7ce0abd218d704b6cab437d", size = 88303173, upload-time = "2026-06-17T21:05:06.603Z" }, + { url = "https://files.pythonhosted.org/packages/4a/07/fe09d1699fbed2afa10ebc692ff2b99d113f2605b6748cea633989e2789a/torch-2.12.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:97eba061fcb042fed191400b15568990073d67eaacaa6ee9b7ca01dd8b790fe9", size = 426404009, upload-time = "2026-06-17T21:04:57.557Z" }, + { url = "https://files.pythonhosted.org/packages/2e/f7/0ce4f6c1962c60ded7270e0a9eb560fb615c92b89d332cf9e3dff36d5ecc/torch-2.12.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:3867b861391701012adb2df93360efb88494dca245a185e3bb7624495cfe3f33", size = 532184292, upload-time = "2026-06-17T21:05:17.526Z" }, + { url = "https://files.pythonhosted.org/packages/70/db/e384c12aba30320ca92aaaf557456cbcb26f04b4df307728bb8f019f5000/torch-2.12.1-cp314-cp314t-win_amd64.whl", hash = "sha256:dd15595f8fc764cffde8c6361a3beb6ef69a028c851b1b3e70e077f615980d4e", size = 123231142, upload-time = "2026-06-17T21:05:27.061Z" }, +] + +[[package]] +name = "tqdm" +version = "4.68.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/d7/0535a28b1f5f24f6612fb3ff1e89fb1a8d160fee0f976e0aa6803862134b/tqdm-4.68.3.tar.gz", hash = "sha256:00dfa48452b6b6cfae3dd9885636c23d3422d1ec97c66d96818cbd5e0821d482", size = 170596, upload-time = "2026-06-17T07:36:52.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl", hash = "sha256:39832cc2def2789a6f29df83f172db7416cea70052c0907a57801c5f2fdccb03", size = 78337, upload-time = "2026-06-17T07:36:50.132Z" }, +] + +[[package]] +name = "trimesh" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/37/5cb90f04990260d2caceb6093560c6cefafca1ec522c1e43be01ca658244/trimesh-4.12.2.tar.gz", hash = "sha256:c8ca31571ac00b112e4e160e66a2d4c3491df321f056bd33806be0485d1af9d9", size = 842220, upload-time = "2026-05-01T00:57:43.333Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/98/716a473cfb24750858ddd5d14e6527539dd206583a46408d08eeb2844a75/trimesh-4.12.2-py3-none-any.whl", hash = "sha256:b5b5afa63c5272345f2858f7676bc8c217dc8a89f4fadf6193fe10a81b5ff2aa", size = 741043, upload-time = "2026-05-01T00:57:40.763Z" }, +] + +[[package]] +name = "triton" +version = "3.7.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/fa/f856e24deb462d5f18bd4b5a746957862ab9b6ee5834bda60605ec348366/triton-3.7.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9497f2e696ee368862a181a90b2dcc03ca978cc4f602abd67c7d81022a6988e1", size = 184692359, upload-time = "2026-06-17T20:03:48.288Z" }, + { url = "https://files.pythonhosted.org/packages/c4/6f/fb96d15db6f36d6eae4cafb998c2e0353bf59d7c4ea1662d7497f269134a/triton-3.7.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e40869937a68206ec70d7f25bb7ec6433cb083f9135e1f36dbd318dc449a728", size = 197719725, upload-time = "2026-06-17T19:53:20.419Z" }, + { url = "https://files.pythonhosted.org/packages/00/42/c5089d4d9327fcd1e862c599cc2927f39418f84dd11a84cb2ccff9d4787a/triton-3.7.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdbfc09d9ec58bc5e68321525653220de7515c199e7a8097a97c85e62b52cd0a", size = 184694629, upload-time = "2026-06-17T20:03:53.444Z" }, + { url = "https://files.pythonhosted.org/packages/07/42/2c3ac59253ae8892b6f307875263dd23dc875cdf732d3aea40d6d41fb7cb/triton-3.7.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58c0e131da05134a2a4788ccbcc0c1105cf0f54c8e98f19e34cd465396dc15eb", size = 197729241, upload-time = "2026-06-17T19:53:27.801Z" }, + { url = "https://files.pythonhosted.org/packages/40/71/e01aa7ad573883ed9456f130226babdec70b005e098c4d6226a6238e761b/triton-3.7.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe4ea396a06171f1f1f58cbd39c70b09294398f7dd7c620939bab54ad6f934fa", size = 184705764, upload-time = "2026-06-17T20:03:59.064Z" }, + { url = "https://files.pythonhosted.org/packages/a4/09/5683146fda6a2b569deb78ccfd8fbfea8bfe55f726b081c0a6bb18dd6f28/triton-3.7.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2020153b08280415ec0da6607834e79166442147e78e144df06b508c75b186d2", size = 197729537, upload-time = "2026-06-17T19:53:35.516Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f8/448220c3092019f9fdfab39ec47985968181d67da34b44f6a7f6280a5cbb/triton-3.7.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c58e4c61f0c73b5dba3b5d19b4a7093c32f90dc18b2a7f121a7c16ccd31107b7", size = 184814760, upload-time = "2026-06-17T20:04:04.984Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ac/229b7d4589d2e5937310e72c6d46e89599d16a4a12b479ffa1499fee8eb8/triton-3.7.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10ba85fa2cca4a2fbdeb36bf1cb082f2c252bda55bf9fccd74f65ec5bc647e68", size = 197824404, upload-time = "2026-06-17T19:53:42.772Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, +]