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
17 changes: 11 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,7 @@ dependencies = [
"biopython>=1.81",
"anndata>=0.9.1",
"h5py>=3.7",
# Development essentials
"pre-commit>=4.3.0",
"ruff>=0.1.5",
"jax-md>=0.2.27",
"rdkit>=2025.9.3"
"jax-md>=0.2.27"
]
description = "DiffBio: End-to-end differentiable bioinformatics pipelines built on Datarax, Artifex, Opifex, and Calibrax"
keywords = ["jax", "flax", "bioinformatics", "differentiable", "variant-calling", "alignment", "machine-learning"]
Expand All @@ -73,7 +69,7 @@ requires-python = ">=3.11"
version = "0.1.0"

[project.optional-dependencies]
all = ["diffbio[benchmark,dev,docs,genomics,gpu,soft-ops-advanced,soft-ops-ot,test,torch-io]"]
all = ["diffbio[benchmark,chem,dev,docs,genomics,gpu,soft-ops-advanced,soft-ops-ot,test,torch-io]"]
benchmark = [
"scib-metrics>=0.5",
"pynndescent>=0.5",
Expand Down Expand Up @@ -119,6 +115,9 @@ docs = [
"mkdocstrings-python>=1.1.2",
"pymdown-extensions>=10.14.3"
]
# RDKit, for the molecular-graph and fingerprint operators. Every call site imports it
# lazily, so the rest of diffbio installs and runs without it.
chem = ["rdkit>=2025.9.3"]
genomics = ["pysam>=0.22.0", "pyfaidx>=0.8.0"]
gpu = ["jax[cuda12]>=0.6.1", "jaxlib>=0.6.1"]
soft-ops-advanced = ["optimistix>=0.0.9", "lineax>=0.0.8"]
Expand Down Expand Up @@ -381,4 +380,10 @@ order-by-type = false
convention = "google"

[tool.uv]
# Security floors for vulnerable transitive dependencies that have a published fix.
# setuptools arrives through torch, tensorflow and tensorboard, which require it at
# runtime; it is not this project's build backend, which is hatchling.
constraint-dependencies = [
"setuptools>=83.0.0" # PYSEC-2026-3447
]
python-preference = "only-managed"
40 changes: 30 additions & 10 deletions src/diffbio/operators/drug_discovery/primitives.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,32 @@

import logging
from dataclasses import dataclass
from functools import lru_cache
from typing import Any

import jax.numpy as jnp
from rdkit import Chem

logger = logging.getLogger(__name__)


@lru_cache(maxsize=1)
def _rdkit_chem() -> Any:
"""Return `rdkit.Chem`, importing it on first use.

RDKit is an optional dependency, installed with `pip install "diffbio[chem]"`.
Importing it at module scope would pull the whole toolkit into every `import
diffbio`, for a capability most pipelines never call. The other RDKit call sites
in this package are already lazy in the same way; this keeps them consistent.
"""
try:
from rdkit import Chem
except ImportError as e: # pragma: no cover - exercised only without RDKit
raise ImportError(
'Molecular graph primitives require RDKit: pip install "diffbio[chem]"'
) from e
return Chem


@dataclass(frozen=True)
class AtomFeatureConfig:
"""Configuration for atom feature extraction.
Expand Down Expand Up @@ -103,11 +121,12 @@ def get_atom_features(atom: Any, config: AtomFeatureConfig | None = None) -> jnp

# Hybridization
hybridization = atom.GetHybridization()
chem = _rdkit_chem()
hyb_types = [
Chem.rdchem.HybridizationType.SP,
Chem.rdchem.HybridizationType.SP2,
Chem.rdchem.HybridizationType.SP3,
Chem.rdchem.HybridizationType.SP3D,
chem.rdchem.HybridizationType.SP,
chem.rdchem.HybridizationType.SP2,
chem.rdchem.HybridizationType.SP3,
chem.rdchem.HybridizationType.SP3D,
]
hyb_onehot = [0.0] * config.num_hybridization_types
for i, h in enumerate(hyb_types[: config.num_hybridization_types]):
Expand Down Expand Up @@ -141,11 +160,12 @@ def get_bond_features(bond: Any) -> jnp.ndarray:
Feature vector of shape (4,).
"""
bond_type = bond.GetBondType()
chem = _rdkit_chem()
features = [
1 if bond_type == Chem.rdchem.BondType.SINGLE else 0,
1 if bond_type == Chem.rdchem.BondType.DOUBLE else 0,
1 if bond_type == Chem.rdchem.BondType.TRIPLE else 0,
1 if bond_type == Chem.rdchem.BondType.AROMATIC else 0,
1 if bond_type == chem.rdchem.BondType.SINGLE else 0,
1 if bond_type == chem.rdchem.BondType.DOUBLE else 0,
1 if bond_type == chem.rdchem.BondType.TRIPLE else 0,
1 if bond_type == chem.rdchem.BondType.AROMATIC else 0,
]
return jnp.array(features, dtype=jnp.float32)

Expand All @@ -166,7 +186,7 @@ def smiles_to_graph(smiles: str) -> dict[str, Any]:
Raises:
ValueError: If SMILES string is invalid.
"""
mol = Chem.MolFromSmiles(smiles)
mol = _rdkit_chem().MolFromSmiles(smiles)
if mol is None:
raise ValueError(f"Invalid SMILES string: {smiles}")

Expand Down
Loading
Loading