Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,8 @@ jobs:
set -xeuo pipefail
bash admin/run_include_check.sh -d

ruff-format:
name: ruff-format
ruff:
name: ruff
runs-on: ubuntu-24.04
timeout-minutes: 30

Expand All @@ -164,7 +164,7 @@ jobs:
python -m pip install --upgrade pip
python -m pip install ruff==0.15.8

- name: Run ruff format check
- name: Run Ruff checks
shell: bash
run: |
set -xeuo pipefail
Expand Down
15 changes: 10 additions & 5 deletions admin/run_ruff_format.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,19 @@
# limitations under the License.


# Runs ruff format over the codebase.
# Runs Ruff lint fixes and formatting over the codebase.
# By default will modify files in-place. Use -d to do a dry-run.

set -ex

EXPECTED_RUFF_VERSION="0.15.8"
RUFF_ARGS=()
RUFF_CHECK_ARGS=(--fix)
RUFF_FORMAT_ARGS=()
while getopts ":d" opt; do
case ${opt} in
d )
RUFF_ARGS+=(--check)
RUFF_CHECK_ARGS=()
RUFF_FORMAT_ARGS+=(--check)
;;
\? )
echo "Usage: run_ruff_format.sh [-d]"
Expand All @@ -39,12 +41,15 @@ ROOT_DIR=$(git rev-parse --show-toplevel)
if command -v ruff >/dev/null 2>&1; then
ACTUAL_RUFF_VERSION=$(ruff --version | awk '{print $2}')
if [ "$ACTUAL_RUFF_VERSION" != "$EXPECTED_RUFF_VERSION" ]; then
echo "Warning: expected ruff version $EXPECTED_RUFF_VERSION, found $ACTUAL_RUFF_VERSION. Formatting may not match CI checker." >&2
echo "Warning: expected ruff version $EXPECTED_RUFF_VERSION, found $ACTUAL_RUFF_VERSION. Results may not match CI." >&2
fi
else
echo "Error: ruff is not installed; expected version $EXPECTED_RUFF_VERSION." >&2
exit 1
fi

echo "Running ruff check:"
ruff check "${RUFF_CHECK_ARGS[@]}" "$ROOT_DIR"

echo "Running ruff format:"
ruff format "${RUFF_ARGS[@]}" "$ROOT_DIR"
ruff format "${RUFF_FORMAT_ARGS[@]}" "$ROOT_DIR"
1 change: 0 additions & 1 deletion benchmarks/cross_similarity_bench.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
from nvmolkit.fingerprints import MorganFingerprintGenerator
from nvmolkit.similarity import crossCosineSimilarity, crossTanimotoSimilarity


SIZES = [2000, 4000, 6000, 8000, 10000, 12000, 14000, 16000, 20000, 24000, 28000, 32000]
CPU_SINGLE_VALUE_ABOVE = 6000

Expand Down
1 change: 0 additions & 1 deletion benchmarks/tfd_prepare_mols.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
import multiprocessing
import os
import pickle
import sys
import time
from functools import partial

Expand Down
2 changes: 1 addition & 1 deletion benchmarks/tfd_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ def main():
# === Warmup ===
if args.warmup > 0:
with nvtx.annotate("Warmup", color="red"):
first_mols = list(configs.values())[0]
first_mols = next(iter(configs.values()))
warmup_mols = first_mols[: min(5, len(first_mols))]
print(f"\nWarmup ({args.warmup} iteration(s)) with {len(warmup_mols)} molecules...")
for _ in range(args.warmup):
Expand Down
2 changes: 0 additions & 2 deletions nvmolkit/_mmff_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@

def default_rdkit_mmff_properties(mol: "Mol"):
"""Create default RDKit MMFF properties for ``mol``."""

properties = rdForceFieldHelpers.MMFFGetMoleculeProperties(mol)
if properties is None:
raise ValueError("RDKit could not create MMFF properties for molecule")
Expand All @@ -58,7 +57,6 @@ def make_internal_mmff_properties(
(variant, dielectric, per-term flags); the corresponding getters are not
wrapped. We read the settings through the C++ binding layer instead.
"""

return _batchedForcefield.buildMMFFPropertiesFromRDKit(
properties,
float(non_bonded_threshold),
Expand Down
4 changes: 2 additions & 2 deletions nvmolkit/autotune/_calibration.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def auto_subsample(
"""
if workload_size <= 0:
raise ValueError("workload_size must be positive")
target = min(max_size, max(min_size, int(round(fraction * workload_size))))
target = min(max_size, max(min_size, round(fraction * workload_size)))
target = min(target, workload_size)
rng = random.Random(seed)
indices = list(range(workload_size))
Expand Down Expand Up @@ -86,6 +86,6 @@ def shrink(indices: Sequence[int], factor: float = 0.5, *, min_size: int = 1) ->
"""
if factor <= 0.0 or factor >= 1.0:
raise ValueError("factor must be in (0, 1)")
new_size = max(min_size, int(round(len(indices) * factor)))
new_size = max(min_size, round(len(indices) * factor))
new_size = min(new_size, len(indices))
return list(indices[:new_size])
4 changes: 2 additions & 2 deletions nvmolkit/autotune/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def _require_optuna():
"""Import optuna or raise an :class:`ImportError` with install instructions."""
if not is_optuna_available():
raise ImportError(OPTUNA_INSTALL_HINT)
import optuna # noqa: PLC0415
import optuna

return optuna

Expand Down Expand Up @@ -348,7 +348,7 @@ def collect_int_from_space(spec: Any) -> int:
high_int = int(high)
if low_int <= 0 or high_int <= 0:
raise ValueError(f"Log-uniform range {spec!r} requires strictly positive bounds.")
midpoint = int(round(math.sqrt(low_int * high_int)))
midpoint = round(math.sqrt(low_int * high_int))
return max(low_int, min(high_int, midpoint))
if isinstance(spec, tuple) and len(spec) == 3 and all(isinstance(v, int) for v in spec):
low, high, step = (int(v) for v in spec)
Expand Down
2 changes: 1 addition & 1 deletion nvmolkit/autotune/_ff_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def resolve_num_gpus(fixed_gpu_ids: list[int]) -> int:
if fixed_gpu_ids:
return max(1, len(fixed_gpu_ids))
try:
import torch # noqa: PLC0415
import torch

return max(1, int(torch.cuda.device_count()))
except Exception:
Expand Down
10 changes: 4 additions & 6 deletions nvmolkit/batchedForcefield.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,6 @@
from nvmolkit._mmff_bridge import default_rdkit_mmff_properties, make_internal_mmff_properties
from nvmolkit.types import CoordinateOutput, Device3DResult, FireOptions, HardwareOptions

from nvmolkit import _batchedForcefield # type: ignore

if TYPE_CHECKING:
from rdkit.Chem import Mol
from rdkit.ForceField.rdForceField import MMFFMolProperties as RDKitMMFFMolProperties
Expand Down Expand Up @@ -456,8 +454,7 @@ def _minimize(


class MMFFBatchedForcefield(_BatchedForcefieldBase):
"""Evaluate MMFF energies and gradients, or run BFGS minimization, for a
batch of molecules with all their conformers.
"""Evaluate MMFF energies and gradients or run BFGS minimization for molecule batches.

Properties and constraints are per-molecule and are shared across all
conformers of that molecule. Results are nested as
Expand Down Expand Up @@ -515,6 +512,7 @@ def __init__(
)

def __getitem__(self, idx: int) -> MMFFBatchElement:
"""Return the force-field element at ``idx``."""
return super().__getitem__(idx)

def _normalize_properties(
Expand Down Expand Up @@ -632,8 +630,7 @@ def minimize(


class UFFBatchedForcefield(_BatchedForcefieldBase):
"""Evaluate UFF energies and gradients, or run BFGS minimization, for a
batch of molecules with all their conformers.
"""Evaluate UFF energies and gradients or run BFGS minimization for molecule batches.

Constraints are per-molecule and are shared across all conformers of
that molecule. Results are nested as ``list[list[...]]`` — outer
Expand Down Expand Up @@ -679,6 +676,7 @@ def __init__(
self._vdw_thresholds = _normalize_scalar_or_list(vdwThreshold, len(molecules), "vdwThreshold")

def __getitem__(self, idx: int) -> UFFBatchElement:
"""Return the force-field element at ``idx``."""
return super().__getitem__(idx)

def _build_native(self):
Expand Down
2 changes: 0 additions & 2 deletions nvmolkit/mmffOptimization.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@
from rdkit.ForceField.rdForceField import MMFFMolProperties

from nvmolkit import _mmffOptimization
from nvmolkit._mmff_bridge import default_rdkit_mmff_properties, make_internal_mmff_properties
from nvmolkit.types import CoordinateOutput, Device3DResult, HardwareOptions


@overload
Expand Down
7 changes: 5 additions & 2 deletions nvmolkit/substructure.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@
from nvmolkit._substructure import hasSubstructMatch as _hasSubstructMatch

__all__ = [
"SubstructSearchConfig",
"SubstructMatchResults",
"getSubstructMatches",
"SubstructSearchConfig",
"countSubstructMatches",
"getSubstructMatches",
"hasSubstructMatch",
]

Expand Down Expand Up @@ -79,6 +79,7 @@ def __init__(
gpuIds: list[int] | None = None,
algorithm: str = "dfs",
) -> None:
"""Initialize a substructure-search configuration."""
native = _NativeSubstructSearchConfig()
native.batchSize = int(batchSize)
native.workerThreads = int(workerThreads)
Expand Down Expand Up @@ -198,9 +199,11 @@ class SubstructMatchResults:
shape: tuple[int, int]

def __len__(self) -> int:
"""Return the number of target molecules."""
return self.shape[0]

def __getitem__(self, target_idx: int) -> _SubstructTargetView:
"""Return the match view for one target molecule."""
return _SubstructTargetView(self, target_idx)

def get_pair(self, target_idx: int, query_idx: int) -> list[np.ndarray]:
Expand Down
52 changes: 34 additions & 18 deletions nvmolkit/tests/test_batched_forcefield.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,10 @@ def assert_energy_and_gradient_close(got_energy, want_energy, got_grad, want_gra


def _assert_batched_compute_matches_rdkit_mmff(mol_specs):
"""Build a single MMFFBatchedForcefield from ``mol_specs`` and verify that
per-mol ``compute_energy``/``compute_gradients`` match RDKit's single-mol FF
for each mol, with its configured properties and (optionally) constraint.
"""Verify batched MMFF energies and gradients against RDKit.

Build a single MMFFBatchedForcefield from ``mol_specs`` and compare each
molecule using its configured properties and optional constraint.

When any mol has a constraint configured, additionally verify that the
constraint has an observable effect on that mol's energy AND gradient — as
Expand Down Expand Up @@ -266,8 +267,11 @@ def _assert_batched_compute_matches_rdkit_mmff(mol_specs):


def test_mmff_batched_forcefield_properties_match_rdkit():
"""Batch of mols with varied per-mol property configurations (default, MMFF variant,
dielectric model, term toggles, fragmented+interfrag)."""
"""Test varied per-molecule property configurations.

Cover defaults, MMFF variants, dielectric models, term toggles, and
fragmented molecules with interfragment interactions.
"""
_assert_batched_compute_matches_rdkit_mmff(
[
{"factory": load_reference_mol},
Expand Down Expand Up @@ -296,9 +300,11 @@ def test_mmff_batched_forcefield_properties_match_rdkit():


def test_mmff_batched_forcefield_reads_externally_configured_properties():
"""Configure RDKit MMFF properties via raw ``rdForceFieldHelpers.MMFFGetMoleculeProperties``
plus direct ``SetMMFF*Term``/``SetMMFFDielectricConstant`` calls — no nvmolkit helpers
in the path — then hand the object to ``MMFFBatchedForcefield``.
"""Test externally configured RDKit MMFF properties.

Configure properties via raw ``rdForceFieldHelpers.MMFFGetMoleculeProperties``
plus direct ``SetMMFF*Term``/``SetMMFFDielectricConstant`` calls, with no
nvmolkit helpers in the path, then pass the object to ``MMFFBatchedForcefield``.

Needed because of our workaround for RDKit bug https://github.com/rdkit/rdkit/issues/9253
"""
Expand Down Expand Up @@ -327,8 +333,11 @@ def test_mmff_batched_forcefield_reads_externally_configured_properties():


def test_mmff_batched_forcefield_constraints_match_rdkit():
"""Batch of mols with all 5 MMFF constraint types applied (one per mol), some also
carrying non-default property settings to exercise the properties+constraints path."""
"""Test all five MMFF constraint types against RDKit.

Apply one constraint type per molecule, with some molecules also carrying
non-default property settings to exercise the properties-plus-constraints path.
"""
_assert_batched_compute_matches_rdkit_mmff(
[
{
Expand Down Expand Up @@ -515,8 +524,11 @@ def _build_constrained_mmff_batch(specs=_MMFF_BATCH_CONSTRAINT_SPECS, hardwareOp


def _assert_batched_minimize_matches_rdkit(specs, mols, opt_energies, converged, make_ref_ff):
"""Compare nvMolKit minimize() result to RDKit minimize per (mol, conformer), with each
mol carrying a different constraint from `specs`."""
"""Compare nvMolKit and RDKit minimization results.

Compare each molecule and conformer while each molecule carries a different
constraint from ``specs``.
"""
for mol_idx, (mol, spec) in enumerate(zip(mols, specs)):
assert len(opt_energies[mol_idx]) == mol.GetNumConformers()
assert all(converged[mol_idx]), f"Mol {mol_idx} failed to converge"
Expand All @@ -531,9 +543,11 @@ def _assert_batched_minimize_matches_rdkit(specs, mols, opt_energies, converged,


def test_mmff_batched_minimize_with_constraints_batch_matches_rdkit():
"""Batch minimize with different constraint types on different-size mols
and different conformer counts, comparing each (mol, conformer) energy
to RDKit's minimize with the same constraint."""
"""Test constrained batch minimization against RDKit.

Use different constraint types, molecule sizes, and conformer counts, then
compare each molecule and conformer energy to RDKit with the same constraint.
"""
mols, _, ff = _build_constrained_mmff_batch()
opt_energies, converged = ff.minimize(maxIters=500)

Expand All @@ -544,9 +558,11 @@ def make_ref(mol, conf_id):


def test_mmff_batched_minimize_respects_maxiters_and_forcetol():
"""maxIters and forceTol must be plumbed through: a single-iteration minimize
should not converge and should leave energies closer to the starting point
than a generous-iteration minimize."""
"""Test that maxIters and forceTol are passed through.

A single-iteration minimization should not converge and should leave energies
closer to the starting point than a generous-iteration minimization.
"""
perturbed_mols = [
perturb_conformers(make_embedded_mol("CCCO", num_confs=2)),
perturb_conformers(make_embedded_mol("c1ccccc1CCO", num_confs=2)),
Expand Down
8 changes: 6 additions & 2 deletions nvmolkit/tests/test_mmff_optimization.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
from rdkit.ForceField import rdForceField as _rdForceField # noqa: F401
from rdkit.Geometry import Point3D

from nvmolkit.embedMolecules import EmbedMolecules
import nvmolkit.mmffOptimization as nvmolkit_mmff
from nvmolkit.embedMolecules import EmbedMolecules
from nvmolkit.types import CoordinateOutput, Device3DResult, FireOptions, HardwareOptions


Expand Down Expand Up @@ -133,7 +133,11 @@ def calculate_rdkit_mmff_energies(
"""Calculate MMFF energies using RDKit for all conformers of all molecules.

Args:
molecules: List of RDKit molecules with conformers
molecules: List of RDKit molecules with conformers.
maxIters: Maximum minimization iterations per conformer.
property_settings: Optional MMFF property overrides.
nonBondedThreshold: Non-bonded interaction cutoff.
ignoreInterfragInteractions: Whether to omit interactions between fragments.

Returns:
list: List of lists containing energies for each molecule's conformers
Expand Down
5 changes: 5 additions & 0 deletions nvmolkit/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,14 @@
import numpy as np
import torch

# These imports populate Boost.Python's global converter registry. Keep this
# bootstrap sequence developer-controlled so future binding dependencies are
# not silently reordered by the formatter.
# isort: off
from nvmolkit import _arrayHelpers # noqa: F401
from nvmolkit import _embedMolecules # type: ignore
from nvmolkit import _types
# isort: on


class FireOptions:
Expand Down
1 change: 0 additions & 1 deletion nvmolkit/uffOptimization.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
from rdkit.Chem import Mol

from nvmolkit import _uffOptimization
from nvmolkit.types import CoordinateOutput, Device3DResult, HardwareOptions


@overload
Expand Down
Loading
Loading