From 3aae4588840e237f4fdc321398162ef94dee772e Mon Sep 17 00:00:00 2001 From: EliasLMann Date: Mon, 23 Mar 2026 17:30:05 -0400 Subject: [PATCH] add SMILES-guided topology assignment with Hungarian coordinate matching --- pyproject.toml | 3 +- steamroll/__init__.py | 2 + steamroll/steamroll.py | 235 +++++++++++++++++++++++++++++++++++++--- tests/test_steamroll.py | 138 ++++++++++++++++++----- uv.lock | 75 ++++++++++++- 5 files changed, 407 insertions(+), 46 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9465bfa..eec738d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "steamroll" description = "Package to convert 3D molecules to RDKit" license = {file = "LICENSE"} -version = "0.0.4" +version = "0.0.5" readme = "README.md" keywords = [] authors = [ @@ -14,6 +14,7 @@ dependencies = [ "rdkit", "numpy", "networkx", + "scipy", ] [dependency-groups] diff --git a/steamroll/__init__.py b/steamroll/__init__.py index 138aab6..5030e29 100644 --- a/steamroll/__init__.py +++ b/steamroll/__init__.py @@ -1 +1,3 @@ """steamroll package.""" + +from .steamroll import SteamrollConversionError, SteamrollTopologyMismatchError, to_rdkit diff --git a/steamroll/steamroll.py b/steamroll/steamroll.py index e818681..e3e9258 100644 --- a/steamroll/steamroll.py +++ b/steamroll/steamroll.py @@ -3,11 +3,15 @@ import logging import os import tempfile +from collections import Counter, defaultdict from typing import Iterable import numpy as np from numpy.typing import ArrayLike from rdkit import Chem +from rdkit.Chem import AllChem +from rdkit.Geometry import Point3D +from scipy.optimize import linear_sum_assignment from .xyz2mol.xyz2mol import xyz2mol from .xyz2mol_tmc.xyz2mol_local import xyz2AC_obabel as xyz2ac_obabel @@ -24,6 +28,10 @@ class SteamrollConversionError(Exception): """Raised when a conversion error occurs.""" +class SteamrollTopologyMismatchError(SteamrollConversionError): + """Raised when conversion succeeds but the result doesn't match the provided SMILES.""" + + def remove_hydrogens(molecule: Chem.rdchem.Mol) -> Chem.rdchem.Mol: """Remove hydrogens from an RDKit molecule. @@ -41,14 +49,14 @@ def remove_hydrogens(molecule: Chem.rdchem.Mol) -> Chem.rdchem.Mol: # Delete hydrogen add an explicit H to its first neighbor if atom.GetAtomicNum() == 1: - if neighbors := atom.GetNeighbors(): # type: ignore [call-arg, unused-ignore] + if neighbors := atom.GetNeighbors(): neighbor = neighbors[0] rwmol.RemoveAtom(idx) neighbor.SetNumExplicitHs(neighbor.GetNumExplicitHs() + 1) else: logger.warning("Hydrogen atom has no neighbors, skipping") - return rwmol.GetMol() # type: ignore [call-arg, no-any-return, unused-ignore] + return rwmol.GetMol() def fragment(molecule: Chem.rdchem.Mol) -> list[Chem.rdchem.Mol]: @@ -84,25 +92,176 @@ def _write_temp_xyz(atomic_numbers: list[int], coordinates: list[list[float]]) - return f.name +def _from_smiles_and_coords( + smiles: str, + atomic_numbers: list[int], + coordinates: list[list[float]], +) -> Chem.rdchem.Mol: + """Build an RDKit mol using SMILES for topology and XYZ for 3D coordinates. + + Uses topology-driven BFS propagation with per-element optimal assignment to + map template atoms to XYZ positions without relying on bond perception. + Unique-element atoms serve as unambiguous anchors; the molecular graph then + constrains candidates, and minimum-weight bipartite matching resolves + ambiguity globally rather than greedily. + + Args: + smiles: SMILES string encoding the molecular topology. + atomic_numbers: atomic numbers for each atom. + coordinates: Cartesian coordinates for each atom, in Å. + + Returns: + RDKit molecule with SMILES topology and XYZ coordinates. + + Raises: + ValueError: if SMILES is invalid, atom counts don't match, or elements differ. + """ + template = Chem.MolFromSmiles(smiles) + if template is None: + raise ValueError(f"Invalid SMILES: {smiles}") + template = Chem.AddHs(template) + + n = template.GetNumAtoms() + if n != len(atomic_numbers): + raise ValueError(f"Atom count mismatch: SMILES has {n}, XYZ has {len(atomic_numbers)}") + + xyz_pos = np.array(coordinates) + t_elems = [template.GetAtomWithIdx(i).GetAtomicNum() for i in range(n)] + x_elems = list(atomic_numbers) + + if Counter(t_elems) != Counter(x_elems): + raise ValueError("Element mismatch between SMILES and XYZ") + + t_by_elem: dict[int, list[int]] = defaultdict(list) + x_by_elem: dict[int, list[int]] = defaultdict(list) + for i in range(n): + t_by_elem[t_elems[i]].append(i) + x_by_elem[x_elems[i]].append(i) + + # Unique-element atoms are unambiguous anchors + atom_map: dict[int, int] = {} + for elem, t_indices_for_elem in t_by_elem.items(): + if len(t_indices_for_elem) == 1: + atom_map[t_indices_for_elem[0]] = x_by_elem[elem][0] + + # BFS: expand frontier along bonds, assign each layer with min-weight bipartite matching + changed = True + while changed: + changed = False + + frontier: dict[int, list[tuple[int, np.ndarray]]] = defaultdict(list) + for t_idx in range(n): + if t_idx in atom_map: + continue + elem = t_elems[t_idx] + mapped_nbr_positions = [ + xyz_pos[atom_map[nbr.GetIdx()]] + for nbr in template.GetAtomWithIdx(t_idx).GetNeighbors() + if nbr.GetIdx() in atom_map + ] + if mapped_nbr_positions: + frontier[elem].append((t_idx, np.mean(mapped_nbr_positions, axis=0))) + + for elem, candidates in frontier.items(): + unmapped_x = [x for x in x_by_elem[elem] if x not in atom_map.values()] + if not unmapped_x: + continue + + cost = np.array( + [ + [float(np.linalg.norm(xyz_pos[x_idx] - ref)) for x_idx in unmapped_x] + for _, ref in candidates + ] + ) + row_ind, col_ind = linear_sum_assignment(cost) + for r, c in zip(row_ind, col_ind, strict=True): + atom_map[candidates[r][0]] = unmapped_x[c] + changed = True + + # Fallback for atoms disconnected from all anchors (rare): embed and match + unmapped_t = [i for i in range(n) if i not in atom_map] + if unmapped_t: + coord_map = {t_idx: Point3D(*xyz_pos[x_idx].tolist()) for t_idx, x_idx in atom_map.items()} + AllChem.EmbedMolecule(template, randomSeed=42, coordMap=coord_map) # type: ignore [attr-defined] + t_pos = np.array([list(template.GetConformer().GetAtomPosition(i)) for i in range(n)]) + + by_elem: dict[int, tuple[list[int], list[int]]] = {} + for t_idx in unmapped_t: + elem = t_elems[t_idx] + if elem not in by_elem: + by_elem[elem] = ([], [x for x in x_by_elem[elem] if x not in atom_map.values()]) + by_elem[elem][0].append(t_idx) + + for _elem, (t_indices, x_indices) in by_elem.items(): + cost = np.array( + [ + [float(np.linalg.norm(t_pos[t_idx] - xyz_pos[x_idx])) for x_idx in x_indices] + for t_idx in t_indices + ] + ) + row_ind, col_ind = linear_sum_assignment(cost) + for r, c in zip(row_ind, col_ind, strict=True): + atom_map[t_indices[r]] = x_indices[c] + + conf = Chem.Conformer(n) + for t_idx in range(n): + conf.SetAtomPosition(t_idx, Point3D(*xyz_pos[atom_map[t_idx]].tolist())) + template.RemoveAllConformers() + template.AddConformer(conf, assignId=True) + return template + + +def _smiles_matches(mol: Chem.rdchem.Mol, smiles: str) -> bool: + """Check whether an RDKit molecule's topology matches a SMILES string. + + Args: + mol: RDKit molecule to validate. + smiles: reference SMILES string. + + Returns: + True if canonical SMILES match after stripping hydrogens. + """ + try: + ref = Chem.MolFromSmiles(smiles) + if ref is None: + return False + Chem.SanitizeMol(mol) + # isomericSmiles=False strips stereo/isotopes — we only care about connectivity + got = Chem.MolToSmiles(Chem.RemoveHs(mol), isomericSmiles=False) + return got == Chem.MolToSmiles(ref, isomericSmiles=False) + except Exception: + return False + + def to_rdkit( atomic_numbers: Iterable[int], coordinates: ArrayLike, charge: int = 0, remove_Hs: bool = True, fail_without_bond_order: bool = False, + smiles: str | None = None, ) -> Chem.rdchem.Mol: """Convert a given molecular geometry to an RDKit molecule. + When ``smiles`` is provided, topology-driven coordinate assignment is attempted + first and all subsequent methods are validated against the SMILES; any method + that produces a non-matching topology is skipped, and ``SteamrollTopologyMismatchError`` + is raised if no method matches. + Args: atomic_numbers: atomic numbers coordinates: coordinates, in Å charge: charge remove_Hs: whether or not to strip hydrogens from the output molecule fail_without_bond_order: if bond order cannot be detected, raise SteamrollConversionError + smiles: optional SMILES string; used as the primary conversion method + and as a topology validator for all fallback methods Raises: ValueError: if input dimensions aren't correct SteamrollConversionError: if conversion fails + SteamrollTopologyMismatchError: if smiles is provided but no method produces a + matching topology Returns: RDKit molecule @@ -123,17 +282,17 @@ def to_rdkit( has_tm = any(n in TRANSITION_METALS_NUM for n in atomic_numbers) has_exotic = any(n in _SKIP_XYZ2MOL for n in atomic_numbers) - rdkm: Chem.rdchem.Mol | None = None - - if not has_tm and not has_exotic: + # SMILES-based method: topology from SMILES, positions from XYZ + if smiles is not None: try: - try: - rdkm = xyz2mol(atomic_numbers, coords, charge=charge)[0] - except (Exception, ValueError, IndexError): - rdkm = xyz2mol(atomic_numbers, coords, charge=charge, use_huckel=True)[0] + rdkm = _from_smiles_and_coords(smiles, atomic_numbers, coords) + if _smiles_matches(rdkm, smiles): + return remove_hydrogens(rdkm) if remove_Hs else rdkm + logger.debug("SMILES-based conversion produced wrong topology, falling back") except Exception as e: - if fail_without_bond_order: - raise SteamrollConversionError from e + logger.debug(f"SMILES-based conversion failed, falling back: {e}") + + rdkm: Chem.rdchem.Mol | None = None if has_tm: # Use the specialized TMC converter; Hs come back implicit → make explicit. @@ -146,16 +305,60 @@ def to_rdkit( os.unlink(xyz_file) if rdkm is None: raise SteamrollConversionError("xyz2mol_tm returned no molecule") - return Chem.AddHs(rdkm) # type: ignore [return-value] + return Chem.AddHs(rdkm) + + def _topology_ok(mol: Chem.rdchem.Mol) -> bool: + return smiles is None or _smiles_matches(mol, smiles) + + if not has_exotic: + # xyz2mol (standard) + try: + candidate = xyz2mol(atomic_numbers, coords, charge=charge)[0] + if _topology_ok(candidate): + rdkm = candidate + else: + logger.debug("xyz2mol produced wrong topology, trying Hückel") + except Exception: + logger.debug("xyz2mol failed, trying Hückel") + + # xyz2mol (Hückel) — if standard failed or gave wrong topology + if rdkm is None: + try: + candidate = xyz2mol(atomic_numbers, coords, charge=charge, use_huckel=True)[0] + if _topology_ok(candidate): + rdkm = candidate + else: + logger.debug("xyz2mol Hückel produced wrong topology, trying obabel") + except Exception: + logger.debug("xyz2mol Hückel failed, trying obabel") + + if rdkm is None and fail_without_bond_order: + raise SteamrollConversionError( + f"xyz2mol failed for {len(atomic_numbers)}-atom molecule (charge={charge}); " + "provide a SMILES string or fix the geometry" + ) if rdkm is None: - # xyz2mol failed for a non-TM molecule (e.g. wrong charge, unsupported - # element). Fall back to a geometry-only mol via obabel connectivity. + # Geometry-only fallback via obabel — no bond orders, last resort. try: _, rdkm = xyz2ac_obabel(atomic_numbers, coords) except Exception as e: - raise SteamrollConversionError("xyz2mol conversion failed") from e - return rdkm # type: ignore [return-value] + raise SteamrollConversionError( + f"all conversion methods failed for {len(atomic_numbers)}-atom molecule " + f"(charge={charge}); provide a SMILES string or fix the geometry" + ) from e + if not _topology_ok(rdkm): + try: + got = Chem.MolToSmiles(Chem.RemoveHs(rdkm), isomericSmiles=False) + except Exception: + got = "" + expected = Chem.MolToSmiles(Chem.MolFromSmiles(smiles), isomericSmiles=False) + raise SteamrollTopologyMismatchError( + f"no conversion method matched the provided SMILES for " + f"{len(atomic_numbers)}-atom molecule (charge={charge})\n" + f" expected: {expected}\n" + f" got: {got}" + ) return remove_hydrogens(rdkm) if remove_Hs else rdkm diff --git a/tests/test_steamroll.py b/tests/test_steamroll.py index 8a51292..15f353f 100644 --- a/tests/test_steamroll.py +++ b/tests/test_steamroll.py @@ -3,10 +3,33 @@ from pathlib import Path from typing import Any +import numpy as np import pytest from rdkit import Chem +from rdkit.Chem import AllChem + +from steamroll.steamroll import ATOMIC_NUMBERS, SteamrollTopologyMismatchError, fragment, to_rdkit + +_BROMOBENZENE_SMILES = "Brc1ccccc1" + +# Bromobenzene with C-Br shrunk to 1.3 Å (normal ~1.9 Å); pulls Br into +# bonding range of the ortho carbons, causing DetermineConnectivity to mis-bond. +_BROMOBENZENE_DISTORTED_ATOMIC_NUMBERS = [35, 6, 6, 6, 6, 6, 6, 1, 1, 1, 1, 1] +_BROMOBENZENE_DISTORTED_COORDS = [ + [0.000, 2.700, 0.000], # Br — 1.3 Å from C1 (distorted) + [0.000, 1.400, 0.000], # C1 + [1.212, 0.700, 0.000], # C2 + [1.212, -0.700, 0.000], # C3 + [0.000, -1.400, 0.000], # C4 + [-1.212, -0.700, 0.000], # C5 + [-1.212, 0.700, 0.000], # C6 + [2.147, 1.240, 0.000], # H on C2 + [2.147, -1.240, 0.000], # H on C3 + [0.000, -2.480, 0.000], # H on C4 + [-2.147, -1.240, 0.000], # H on C5 + [-2.147, 1.240, 0.000], # H on C6 +] -from steamroll.steamroll import ATOMIC_NUMBERS, fragment, to_rdkit HERE = Path(__file__).parent DATA_DIR = HERE / "data" @@ -21,7 +44,6 @@ def parse_comment_line(comment: str) -> dict[str, Any]: data[key.strip()] = value.strip() except ValueError: continue - return data @@ -33,46 +55,31 @@ def read_xyz(file: Path | str) -> tuple[list[int], list[list[float]], int]: next(f) data = parse_comment_line(next(f)) charge = int(data.get("charge", 0)) - for line in f: atom, x, y, z = line.split() - if atom.isdigit(): - atomic_numbers.append(int(atom)) - else: - atomic_numbers.append(ATOMIC_NUMBERS[atom]) + atomic_numbers.append(int(atom) if atom.isdigit() else ATOMIC_NUMBERS[atom]) coordinates.append([float(x), float(y), float(z)]) - return atomic_numbers, coordinates, charge def test_steamroll() -> None: """Basic test to make sure the package is working.""" - atomic_numbers = [1, 8, 1] - coordinates = [[0, 0, 0], [0, 0, 1], [0, 1, 1]] - - rdkm = to_rdkit(atomic_numbers, coordinates) - + rdkm = to_rdkit([1, 8, 1], [[0, 0, 0], [0, 0, 1], [0, 1, 1]]) assert rdkm.GetNumAtoms() == 1 def test_no_remove_hydrogens() -> None: - """Test to make sure hydrogens are removed.""" - atomic_numbers = [1, 8, 1] - coordinates = [[0, 0, 0], [0, 0, 1], [0, 1, 1]] - - rdkm = to_rdkit(atomic_numbers, coordinates, remove_Hs=False) - - assert isinstance(rdkm, Chem.rdchem.Mol) + """Hydrogens are retained when remove_Hs=False.""" + rdkm = to_rdkit([1, 8, 1], [[0, 0, 0], [0, 0, 1], [0, 1, 1]], remove_Hs=False) assert rdkm.GetNumAtoms() == 3 def test_fragment() -> None: - """Test to make sure multiple molecules are produced.""" - atomic_numbers = [1, 8, 1, 1, 8, 1] - coordinates = [[0, 0, 0], [0, 0, 1], [0, 1, 1], [50, 0, 0], [50, 0, 1], [50, 1, 1]] - - rdkm = to_rdkit(atomic_numbers, coordinates) - + """Multiple molecules are correctly fragmented.""" + rdkm = to_rdkit( + [1, 8, 1, 1, 8, 1], + [[0, 0, 0], [0, 0, 1], [0, 1, 1], [50, 0, 0], [50, 0, 1], [50, 1, 1]], + ) rdkm1, rdkm2 = fragment(rdkm) assert rdkm1.GetNumAtoms() == 1 assert rdkm2.GetNumAtoms() == 1 @@ -80,8 +87,83 @@ def test_fragment() -> None: @pytest.mark.parametrize("file", DATA_DIR.glob("*.xyz")) def test_all_data(file: str) -> None: - """Test to make sure all data files can be processed.""" + """All data files can be processed.""" atomic_numbers, coordinates, charge = read_xyz(file) rdkm = to_rdkit(atomic_numbers, coordinates, charge=charge, remove_Hs=False) - assert rdkm.GetNumAtoms() == len(atomic_numbers) + + +def test_smiles_distorted_halogen() -> None: + """SMILES-based conversion fixes Br bonding when geometry is distorted. + + Without SMILES, geometry-only methods return wrong topology (Br disconnected). + With SMILES, the correct topology is recovered: Br has exactly 1 bond and + all heavy-atom coordinates are preserved. + """ + atomic_numbers = _BROMOBENZENE_DISTORTED_ATOMIC_NUMBERS + coordinates = _BROMOBENZENE_DISTORTED_COORDS + ref_smiles = Chem.MolToSmiles(Chem.MolFromSmiles(_BROMOBENZENE_SMILES), isomericSmiles=False) + + # Without SMILES: wrong topology + got_no_smiles = Chem.MolToSmiles( + Chem.RemoveHs(to_rdkit(atomic_numbers, coordinates, remove_Hs=False)), + isomericSmiles=False, + ) + assert got_no_smiles != ref_smiles + + # With SMILES: correct topology, Br valence, and coordinate fidelity + rdkm = to_rdkit(atomic_numbers, coordinates, smiles=_BROMOBENZENE_SMILES, remove_Hs=False) + assert Chem.MolToSmiles(Chem.RemoveHs(rdkm), isomericSmiles=False) == ref_smiles + br = next(a for a in rdkm.GetAtoms() if a.GetAtomicNum() == 35) + assert br.GetDegree() == 1 + + input_heavy = np.array(sorted([coordinates[i] for i, n in enumerate(atomic_numbers) if n > 1])) + output_heavy = np.array( + sorted( + [ + (p.x, p.y, p.z) + for a in rdkm.GetAtoms() + if a.GetAtomicNum() > 1 + for p in [rdkm.GetConformer().GetAtomPosition(a.GetIdx())] + ] + ) + ) + np.testing.assert_allclose(input_heavy, output_heavy, atol=1e-3) + + +def test_smiles_mismatch_raises() -> None: + """Raises SteamrollTopologyMismatchError when no method can match the provided SMILES.""" + with pytest.raises(SteamrollTopologyMismatchError): + to_rdkit([1, 8, 1], [[0, 0, 0], [0, 0, 1], [0, 1, 1]], smiles="CC") + + +@pytest.mark.parametrize( + "smiles", + [ + "C12C3C4C1C5C4C3C25", # cubane + "C1C2CC3CC1CC(C2)C3", # adamantane + "c1cc2ccc3ccc4ccc5ccc1c1c2c3c4c51", # corannulene + "CNC1(C2)CC2(C)C1", # BCP derivative + ], +) +def test_smiles_high_symmetry(smiles: str) -> None: + """SMILES-guided assignment works on highly symmetric molecules. + + Generates RDKit 3D coords, re-converts with the SMILES, and verifies the + canonical SMILES is preserved. + """ + ref = Chem.MolFromSmiles(smiles) + mol_h = Chem.AddHs(ref) + AllChem.EmbedMolecule(mol_h, randomSeed=42) # type: ignore [attr-defined] + + atomic_numbers = [a.GetAtomicNum() for a in mol_h.GetAtoms()] + conf = mol_h.GetConformer() + coordinates = [ + [conf.GetAtomPosition(i).x, conf.GetAtomPosition(i).y, conf.GetAtomPosition(i).z] + for i in range(mol_h.GetNumAtoms()) + ] + + rdkm = to_rdkit(atomic_numbers, coordinates, smiles=smiles, remove_Hs=False) + assert Chem.MolToSmiles(Chem.RemoveHs(rdkm), isomericSmiles=False) == Chem.MolToSmiles( + ref, isomericSmiles=False + ) diff --git a/uv.lock b/uv.lock index 10bca45..f0c8d18 100644 --- a/uv.lock +++ b/uv.lock @@ -441,14 +441,86 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6d/78/097c0798b1dab9f8affe73da9642bb4500e098cb27fd8dc9724816ac747b/ruff-0.15.2-py3-none-win_arm64.whl", hash = "sha256:cabddc5822acdc8f7b5527b36ceac55cc51eec7b1946e60181de8fe83ca8876e", size = 10941649, upload-time = "2026-02-19T22:32:18.108Z" }, ] +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" }, + { url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" }, + { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" }, + { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" }, + { url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512, upload-time = "2026-02-23T00:17:23.424Z" }, + { url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248, upload-time = "2026-02-23T00:17:34.561Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +] + [[package]] name = "steamroll" -version = "0.0.3" +version = "0.0.5" source = { editable = "." } dependencies = [ { name = "networkx" }, { name = "numpy" }, { name = "rdkit" }, + { name = "scipy" }, ] [package.dev-dependencies] @@ -466,6 +538,7 @@ requires-dist = [ { name = "networkx" }, { name = "numpy" }, { name = "rdkit" }, + { name = "scipy" }, ] [package.metadata.requires-dev]