diff --git a/CHANGELOG.md b/CHANGELOG.md index d0a04195..6587923b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,52 @@ # Changelog +## 0.7.0 - 2026-10-15 + +### Summary + +nvMolKit 0.7.0 adds a single-precision mode for force field minimization (MMFF, UFF, BFGS, FIRE) and ETKDG conformer generation, which is roughly an order of magnitude faster on consumer GPUs. It also adds a fused 3D descriptor API, diversity selection (Leader and MaxMin), DISE clustering, an Atom-Atom Path (AAP) similarity metric, a persistent GPU `SubstructLibrary`, and various performance improvements to existing fingerprinting, substructure search, and 3D minimization code. + +### Contributors +- Eva Xue (@evasnow1992) +- Kevin Boyd (@scal444) +- Matthew Neba (@Matthew-Neba) +- Ohad Mosafi (@ohadmo) + +### Features +- Single-precision mode (`precision=PrecisionMode.SINGLE`) for MMFF and UFF force fields, BFGS and FIRE minimizers, and ETKDG distance geometry and ETK stages, available through the MMFF, UFF, `BatchedForcefield`, and `EmbedMolecules` APIs. On consumer GPUs, single precision is roughly an order of magnitude faster than double precision. Validation against RDKit is documented in the 3D minimizers guide ([#266](https://github.com/NVIDIA-BioNeMo/nvMolKit/issues/266)) +- GPU-accelerated 3D descriptors through the new `nvmolkit.descriptors3d.Calc3DProperties`, which computes a user-selected set of properties in one fused pass over a batch of conformers: PMI, NPR, radius of gyration, shape factors, PBF, WHIM, RDF, MORSE, AUTOCORR3D, USR, USRCAT, and GETAWAY, matching RDKit's `rdMolDescriptors` ([#341](https://github.com/NVIDIA-BioNeMo/nvMolKit/issues/341)) +- Atom-Atom Path (AAP) molecule similarity via `aap_similarity`, also usable as `metric="aap"` in `fused_leader`, `fused_maxmin`, and `fused_dise` ([#303](https://github.com/NVIDIA-BioNeMo/nvMolKit/pull/303)) +- Diversity selection with `leader` and `maxmin` for distance matrices, and `fused_leader` and `fused_maxmin` for packed fingerprints or molecules, following RDKit's `LeaderPicker` and `MaxMinPicker` ([#339](https://github.com/NVIDIA-BioNeMo/nvMolKit/issues/339)) +- Directed sphere exclusion (DISE) clustering with `dise` and `fused_dise` ([#339](https://github.com/NVIDIA-BioNeMo/nvMolKit/issues/339)) +- Standardized clustering and diversity selection APIs. Butina, DISE, Leader, and MaxMin each come in a matrix form, which takes a precomputed distance matrix, and a fused form, which computes distances on the fly so memory grows linearly instead of quadratically. Every fused form accepts any similarity metric (`"tanimoto"`, `"cosine"`, or `"aap"`) and either output mode (`OutputMode.DEVICE` or `OutputMode.RDKIT`), so algorithms, forms, metrics, and output modes combine freely, except that `fused_butina` does not yet support AAP ([#339](https://github.com/NVIDIA-BioNeMo/nvMolKit/issues/339)) +- Persistent GPU `SubstructLibrary` (`nvmolkit.substruct_library.SubstructLibrary`), the GPU counterpart of RDKit's `SubstructLibrary`. Target molecules are packed and uploaded once, then searched by any number of queries, with multi-GPU storage, asynchronous concurrent queries within a GPU-memory budget, and pattern-fingerprint prescreening ([#350](https://github.com/NVIDIA-BioNeMo/nvMolKit/issues/350)) +- ETKDG now supports eigenvalue-based initial coordinate generation (`useRandomCoords=False`), matching RDKit's initialization and retry behavior ([#288](https://github.com/NVIDIA-BioNeMo/nvMolKit/issues/288)) +- Device-side conformer pruning for ETKDG, so `pruneRmsThresh` can now be combined with GPU-resident output, by @Matthew-Neba ([#160](https://github.com/NVIDIA-BioNeMo/nvMolKit/issues/160)) +- Conformer RMSD supports `alignment_mode="pairwise"` (the default, aligning every pair independently) and `alignment_mode="first-conformer"` (matching RDKit's `GetConformerRMSMatrix`) ([#281](https://github.com/NVIDIA-BioNeMo/nvMolKit/pull/281)) + +### Performance Improvements +- Per-molecule BFGS minimizer is 10-40% faster, applying to ETKDG, MMFF, and UFF on the per-molecule backend ([#342](https://github.com/NVIDIA-BioNeMo/nvMolKit/pull/342), [#355](https://github.com/NVIDIA-BioNeMo/nvMolKit/pull/355)) +- Morgan fingerprint generation is up to 2x faster, through reduced CPU preprocessing and Python transfer overhead ([#267](https://github.com/NVIDIA-BioNeMo/nvMolKit/issues/267)) +- Tanimoto similarity for queries with few molecules is up to 8x faster ([#284](https://github.com/NVIDIA-BioNeMo/nvMolKit/pull/284)) +- Substructure search performance has been improved for queries with repeated recursive SMARTS, since each repeated pattern is now solved only once ([#313](https://github.com/NVIDIA-BioNeMo/nvMolKit/pull/313)) +- ETKDG conformer generation is up to 2x faster for batches with a few hard-to-embed molecules, because retries now keep GPU batches full. The default retry budget is also now bounded per molecule instead of scaling with the largest molecule in the input ([#353](https://github.com/NVIDIA-BioNeMo/nvMolKit/pull/353)) + +### Breaking Changes +- `butina` and `fused_butina` now return a `ClusterDeviceResult` (cluster IDs, centroids, and cluster sizes) by default, or RDKit-format cluster tuples with `output=OutputMode.RDKIT`. The `return_centroids` argument is removed ([#269](https://github.com/NVIDIA-BioNeMo/nvMolKit/issues/269)) + +### Bug Fixes +- Fix `countSubstructMatches` ignoring `uniquify=True`, which returned raw embedding counts that disagreed with RDKit for symmetric queries ([#275](https://github.com/NVIDIA-BioNeMo/nvMolKit/issues/275)) +- Fix SMARTS hydrogen counts excluding explicit hydrogen neighbors, so queries such as `[CH3]` now match deuterated and explicit-H targets as in RDKit ([#276](https://github.com/NVIDIA-BioNeMo/nvMolKit/issues/276)) +- Fix plain query atoms (for example from `MolFromSmiles`) being packed as wildcards in substructure search, and route targets with isotopes above 255 or unsupported bond types to the RDKit fallback instead of failing ([#349](https://github.com/NVIDIA-BioNeMo/nvMolKit/pull/349)) +- Fix ETKDG exiting early on molecules that fail to embed, which previously could skip scheduled retries and fail more often than RDKit ([#300](https://github.com/NVIDIA-BioNeMo/nvMolKit/issues/300)) +- Fix float32 division in the Tanimoto similarity kernels using approximate rounding, which could order equal distances differently than RDKit in downstream clustering ([#338](https://github.com/NVIDIA-BioNeMo/nvMolKit/pull/338)) + +### Miscellaneous +- Improved shared benchmark tooling, including common CLI, timing, result-reporting, and dataset-loading utilities, and RDKit-side deadlines for long-running baselines +- Drastically improved CI coverage, and added back linters and static analysis +- Support for CUDA 13.4 and Rubin GPUs ([#304](https://github.com/NVIDIA-BioNeMo/nvMolKit/pull/304)) + + ## 0.6.0 - 2026-08-13 ### Summary diff --git a/CMakeLists.txt b/CMakeLists.txt index 79f9a3dc..9eff1598 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,7 +17,7 @@ cmake_policy(SET CMP0167 NEW) project( nvmolkit - VERSION 0.6.0 + VERSION 0.7.0 LANGUAGES CXX CUDA) set(CMAKE_CUDA_STANDARD 20) set(CMAKE_CXX_STANDARD 20) diff --git a/README.md b/README.md index e2448122..f1687644 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Conda is the recommended way to install nvMolKit, matching the recommended distr you have a variant of conda installed and activated, such as [Miniconda](https://docs.conda.io/en/latest/miniconda.html) or [Miniforge](https://conda-forge.org/download/). -nvMolKit v0.6.0 supports RDKit 2025.09.1 through 2026.03.5. To install: +nvMolKit v0.7.0 supports RDKit 2025.09.1 through 2026.03.5. To install: ```bash conda install -c conda-forge nvmolkit @@ -59,7 +59,7 @@ Replace `cu128` with another CUDA 12 backend from the PyTorch install page if th compatible with your driver. The wheel published to PyPI is built against a single RDKit release per -nvMolKit version (RDKit 2026.03.5 for nvMolKit v0.6.0), due to versioning +nvMolKit version (RDKit 2026.03.5 for nvMolKit v0.7.0), due to versioning limitations in PyPI's dependency management system. Use this path if you do not have a constraint on which RDKit version to use; @@ -73,12 +73,12 @@ To install nvMolKit pinned to a particular RDKit: ```bash RDKIT_VERSION=2025.9.6 -NVMOLKIT_VERSION=0.6.0 +NVMOLKIT_VERSION=0.7.0 pip install nvmolkit==${NVMOLKIT_VERSION}+rdkit${RDKIT_VERSION} \ --extra-index-url https://nvidia-bionemo.github.io/nvMolKit/wheels/rdkit${RDKIT_VERSION}/simple/ ``` -Replace `2025.9.6` with the RDKit version you want. Variants published for nvMolKit v0.6.0 include every version between 2025.09.1 and 2026.03.5. +Replace `2025.9.6` with the RDKit version you want. Variants published for nvMolKit v0.7.0 include every version between 2025.09.1 and 2026.03.5. ### Installation from Source diff --git a/VERSION b/VERSION index a918a2aa..faef31a4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.6.0 +0.7.0 diff --git a/admin/distribute/test_release_tools.py b/admin/distribute/test_release_tools.py index df954a46..fa778215 100644 --- a/admin/distribute/test_release_tools.py +++ b/admin/distribute/test_release_tools.py @@ -30,11 +30,11 @@ def load_module(name: str): def make_wheel(path: Path) -> None: - dist_info = "nvmolkit-0.6.0.dist-info" - purl = f"pkg:pypi/nvmolkit@0.6.0?file_name={path.name}" + dist_info = "nvmolkit-0.7.0.dist-info" + purl = f"pkg:pypi/nvmolkit@0.7.0?file_name={path.name}" entries = { - "nvmolkit/__init__.py": b"__version__ = '0.6.0'\n", - f"{dist_info}/METADATA": (b"Metadata-Version: 2.1\nName: nvmolkit\nVersion: 0.6.0\nRequires-Dist: rdkit\n"), + "nvmolkit/__init__.py": b"__version__ = '0.7.0'\n", + f"{dist_info}/METADATA": (b"Metadata-Version: 2.1\nName: nvmolkit\nVersion: 0.7.0\nRequires-Dist: rdkit\n"), f"{dist_info}/WHEEL": b"Wheel-Version: 1.0\nTag: cp311-cp311-manylinux_2_28_x86_64\n", f"{dist_info}/sboms/auditwheel.cdx.json": json.dumps( { @@ -42,7 +42,7 @@ def make_wheel(path: Path) -> None: "component": { "bom-ref": purl, "purl": purl, - "version": "0.6.0", + "version": "0.7.0", } }, "dependencies": [{"ref": purl}], @@ -70,7 +70,7 @@ def assert_valid_record(path: Path) -> None: def test_pin_and_retag_wheel_refresh_metadata_and_record(tmp_path: Path) -> None: - source = tmp_path / "nvmolkit-0.6.0-cp311-cp311-manylinux_2_28_x86_64.whl" + source = tmp_path / "nvmolkit-0.7.0-cp311-cp311-manylinux_2_28_x86_64.whl" make_wheel(source) assert pin_wheel_rdkit.rewrite_wheel(source, "2026.3.5", check=False) @@ -78,16 +78,16 @@ def test_pin_and_retag_wheel_refresh_metadata_and_record(tmp_path: Path) -> None assert_valid_record(source) output = Path(retag_wheel.retag(str(source), str(tmp_path / "variants"), "rdkit2026.3.5")) - assert output.name.startswith("nvmolkit-0.6.0+rdkit2026.3.5-") + assert output.name.startswith("nvmolkit-0.7.0+rdkit2026.3.5-") with zipfile.ZipFile(output) as wheel: metadata_name = next(name for name in wheel.namelist() if name.endswith(".dist-info/METADATA")) metadata = wheel.read(metadata_name).decode() - assert "Version: 0.6.0+rdkit2026.3.5\n" in metadata + assert "Version: 0.7.0+rdkit2026.3.5\n" in metadata assert "Requires-Dist: rdkit==2026.3.5\n" in metadata sbom_name = next(name for name in wheel.namelist() if name.endswith("auditwheel.cdx.json")) sbom = json.loads(wheel.read(sbom_name)) component = sbom["metadata"]["component"] - assert component["version"] == "0.6.0+rdkit2026.3.5" + assert component["version"] == "0.7.0+rdkit2026.3.5" assert output.name in component["purl"] assert sbom["dependencies"][0]["ref"] == component["purl"] assert_valid_record(output) @@ -97,22 +97,22 @@ def test_merge_index_preserves_history_and_replaces_matching_filename(tmp_path: generated = tmp_path / "generated/index.html" destination = tmp_path / "destination/index.html" old_name = "nvmolkit-0.5.1-cp311.whl" - replaced_name = "nvmolkit-0.6.0-cp311.whl" + replaced_name = "nvmolkit-0.7.0-cp311.whl" merge_simple_index.write_index( destination, [ merge_simple_index.Link("https://old.example/0.5.1", old_name), - merge_simple_index.Link("https://old.example/0.6.0", replaced_name), + merge_simple_index.Link("https://old.example/0.7.0", replaced_name), ], ) merge_simple_index.write_index( generated, - [merge_simple_index.Link("https://new.example/0.6.0", replaced_name)], + [merge_simple_index.Link("https://new.example/0.7.0", replaced_name)], ) assert merge_simple_index.merge_index(generated, destination) assert merge_simple_index.read_links(destination) == [ merge_simple_index.Link("https://old.example/0.5.1", old_name), - merge_simple_index.Link("https://new.example/0.6.0", replaced_name), + merge_simple_index.Link("https://new.example/0.7.0", replaced_name), ] assert not merge_simple_index.merge_index(generated, destination) diff --git a/docs/changelog.md b/docs/changelog.md index 0b9e1db5..0a71e963 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,51 @@ # Changelog +## 0.7.0 - 2026-10-15 + +### Summary + +nvMolKit 0.7.0 adds a single-precision mode for force field minimization (MMFF, UFF, BFGS, FIRE) and ETKDG conformer generation, which is roughly an order of magnitude faster on consumer GPUs. It also adds a fused 3D descriptor API, diversity selection (Leader and MaxMin), DISE clustering, an Atom-Atom Path (AAP) similarity metric, a persistent GPU `SubstructLibrary`, and various performance improvements to existing fingerprinting, substructure search, and 3D minimization code. + +### Contributors +- Eva Xue (@evasnow1992) +- Kevin Boyd (@scal444) +- Matthew Neba (@Matthew-Neba) +- Ohad Mosafi (@ohadmo) + +### Features +- Single-precision mode (`precision=PrecisionMode.SINGLE`) for MMFF and UFF force fields, BFGS and FIRE minimizers, and ETKDG distance geometry and ETK stages, available through the MMFF, UFF, `BatchedForcefield`, and `EmbedMolecules` APIs. On consumer GPUs, single precision is roughly an order of magnitude faster than double precision. Validation against RDKit is documented in the 3D minimizers guide ([#266](https://github.com/NVIDIA-BioNeMo/nvMolKit/issues/266)) +- GPU-accelerated 3D descriptors through the new `nvmolkit.descriptors3d.Calc3DProperties`, which computes a user-selected set of properties in one fused pass over a batch of conformers: PMI, NPR, radius of gyration, shape factors, PBF, WHIM, RDF, MORSE, AUTOCORR3D, USR, USRCAT, and GETAWAY, matching RDKit's `rdMolDescriptors` ([#341](https://github.com/NVIDIA-BioNeMo/nvMolKit/issues/341)) +- Atom-Atom Path (AAP) molecule similarity via `aap_similarity`, also usable as `metric="aap"` in `fused_leader`, `fused_maxmin`, and `fused_dise` ([#303](https://github.com/NVIDIA-BioNeMo/nvMolKit/pull/303)) +- Diversity selection with `leader` and `maxmin` for distance matrices, and `fused_leader` and `fused_maxmin` for packed fingerprints or molecules, following RDKit's `LeaderPicker` and `MaxMinPicker` ([#339](https://github.com/NVIDIA-BioNeMo/nvMolKit/issues/339)) +- Directed sphere exclusion (DISE) clustering with `dise` and `fused_dise` ([#339](https://github.com/NVIDIA-BioNeMo/nvMolKit/issues/339)) +- Standardized clustering and diversity selection APIs. Butina, DISE, Leader, and MaxMin each come in a matrix form, which takes a precomputed distance matrix, and a fused form, which computes distances on the fly so memory grows linearly instead of quadratically. Every fused form accepts any similarity metric (`"tanimoto"`, `"cosine"`, or `"aap"`) and either output mode (`OutputMode.DEVICE` or `OutputMode.RDKIT`), so algorithms, forms, metrics, and output modes combine freely, except that `fused_butina` does not yet support AAP ([#339](https://github.com/NVIDIA-BioNeMo/nvMolKit/issues/339)) +- Persistent GPU `SubstructLibrary` (`nvmolkit.substruct_library.SubstructLibrary`), the GPU counterpart of RDKit's `SubstructLibrary`. Target molecules are packed and uploaded once, then searched by any number of queries, with multi-GPU storage, asynchronous concurrent queries within a GPU-memory budget, and pattern-fingerprint prescreening ([#350](https://github.com/NVIDIA-BioNeMo/nvMolKit/issues/350)) +- ETKDG now supports eigenvalue-based initial coordinate generation (`useRandomCoords=False`), matching RDKit's initialization and retry behavior ([#288](https://github.com/NVIDIA-BioNeMo/nvMolKit/issues/288)) +- Device-side conformer pruning for ETKDG, so `pruneRmsThresh` can now be combined with GPU-resident output, by @Matthew-Neba ([#160](https://github.com/NVIDIA-BioNeMo/nvMolKit/issues/160)) +- Conformer RMSD supports `alignment_mode="pairwise"` (the default, aligning every pair independently) and `alignment_mode="first-conformer"` (matching RDKit's `GetConformerRMSMatrix`) ([#281](https://github.com/NVIDIA-BioNeMo/nvMolKit/pull/281)) + +### Performance Improvements +- Per-molecule BFGS minimizer is 10-40% faster, applying to ETKDG, MMFF, and UFF on the per-molecule backend ([#342](https://github.com/NVIDIA-BioNeMo/nvMolKit/pull/342), [#355](https://github.com/NVIDIA-BioNeMo/nvMolKit/pull/355)) +- Morgan fingerprint generation is up to 2x faster, through reduced CPU preprocessing and Python transfer overhead ([#267](https://github.com/NVIDIA-BioNeMo/nvMolKit/issues/267)) +- Tanimoto similarity for queries with few molecules is up to 8x faster ([#284](https://github.com/NVIDIA-BioNeMo/nvMolKit/pull/284)) +- Substructure search performance has been improved for queries with repeated recursive SMARTS, since each repeated pattern is now solved only once ([#313](https://github.com/NVIDIA-BioNeMo/nvMolKit/pull/313)) +- ETKDG conformer generation is up to 2x faster for batches with a few hard-to-embed molecules, because retries now keep GPU batches full. The default retry budget is also now bounded per molecule instead of scaling with the largest molecule in the input ([#353](https://github.com/NVIDIA-BioNeMo/nvMolKit/pull/353)) + +### Breaking Changes +- `butina` and `fused_butina` now return a `ClusterDeviceResult` (cluster IDs, centroids, and cluster sizes) by default, or RDKit-format cluster tuples with `output=OutputMode.RDKIT`. The `return_centroids` argument is removed ([#269](https://github.com/NVIDIA-BioNeMo/nvMolKit/issues/269)) + +### Bug Fixes +- Fix `countSubstructMatches` ignoring `uniquify=True`, which returned raw embedding counts that disagreed with RDKit for symmetric queries ([#275](https://github.com/NVIDIA-BioNeMo/nvMolKit/issues/275)) +- Fix SMARTS hydrogen counts excluding explicit hydrogen neighbors, so queries such as `[CH3]` now match deuterated and explicit-H targets as in RDKit ([#276](https://github.com/NVIDIA-BioNeMo/nvMolKit/issues/276)) +- Fix plain query atoms (for example from `MolFromSmiles`) being packed as wildcards in substructure search, and route targets with isotopes above 255 or unsupported bond types to the RDKit fallback instead of failing ([#349](https://github.com/NVIDIA-BioNeMo/nvMolKit/pull/349)) +- Fix ETKDG exiting early on molecules that fail to embed, which previously could skip scheduled retries and fail more often than RDKit ([#300](https://github.com/NVIDIA-BioNeMo/nvMolKit/issues/300)) +- Fix float32 division in the Tanimoto similarity kernels using approximate rounding, which could order equal distances differently than RDKit in downstream clustering ([#338](https://github.com/NVIDIA-BioNeMo/nvMolKit/pull/338)) + +### Miscellaneous +- Improved shared benchmark tooling, including common CLI, timing, result-reporting, and dataset-loading utilities, and RDKit-side deadlines for long-running baselines +- Drastically improved CI coverage, and added back linters and static analysis +- Support for CUDA 13.4 and Rubin GPUs ([#304](https://github.com/NVIDIA-BioNeMo/nvMolKit/pull/304)) + ## 0.6.0 - 2026-08-13 ### Summary diff --git a/docs/index.rst b/docs/index.rst index 9c5cbafa..17e151ac 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -93,7 +93,7 @@ Conda Forge Conda is the recommended way to install nvMolKit, in line with RDKit's recommended installation practice. First, ensure you have a conda-based environment manager installed and activated, such as `Miniconda `_ or `Miniforge `_. -nvMolKit v0.6.0 supports RDKit 2025.09.1 through 2026.03.5. +nvMolKit v0.7.0 supports RDKit 2025.09.1 through 2026.03.5. To install with conda, run:: @@ -125,7 +125,7 @@ Replace ``cu128`` with another CUDA 12 backend from the PyTorch install page if that is compatible with your driver. The wheel published to PyPI is built against a single RDKit release per -nvMolKit version (RDKit 2026.03.5 for nvMolKit v0.6.0), due to versioning +nvMolKit version (RDKit 2026.03.5 for nvMolKit v0.7.0), due to versioning limitations in PyPI's dependency management system. Use this path if you do not have a constraint on which RDKit version to use; @@ -142,12 +142,12 @@ To install nvMolKit pinned to a particular RDKit: .. code-block:: bash RDKIT_VERSION=2025.9.6 - NVMOLKIT_VERSION=0.6.0 + NVMOLKIT_VERSION=0.7.0 pip install nvmolkit==${NVMOLKIT_VERSION}+rdkit${RDKIT_VERSION} \ --extra-index-url https://nvidia-bionemo.github.io/nvMolKit/wheels/rdkit${RDKIT_VERSION}/simple/ Replace ``2025.9.6`` with the RDKit version you want. Variants published for -nvMolKit v0.6.0 include every version between 2025.09.1 and 2026.03.5. +nvMolKit v0.7.0 include every version between 2025.09.1 and 2026.03.5. From Source diff --git a/nvmolkit/__init__.py b/nvmolkit/__init__.py index 53171dc3..97f5b2bc 100644 --- a/nvmolkit/__init__.py +++ b/nvmolkit/__init__.py @@ -31,5 +31,5 @@ (requires the ``optuna`` package, installable via the ``autotune`` extra) """ -VERSION = "0.6.0" +VERSION = "0.7.0" __version__ = VERSION diff --git a/pyproject.toml b/pyproject.toml index 8584a045..193f7fec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ build-backend = "setuptools.build_meta" [project] name = "nvMolKit" -version = "0.6.0" +version = "0.7.0" description = "CUDA-backed python library for accelerating common RDKit molecular operations" readme = { text = """ nvMolKit is a CUDA-backed Python library that accelerates common