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
18 changes: 18 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 4
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true

[*.{yaml,yml,toml,json}]
indent_size = 2

[*.rs]
indent_size = 4

[*.md]
trim_trailing_whitespace = false
86 changes: 86 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
name: CI

on:
push:
branches: [master, main]
pull_request:

jobs:
# ── Job 1: lint (fast — blocks everything else if it fails) ──────────────────
# Runs pre-commit hooks directly so CI uses the exact same ruff version and
# config as local development — no drift between the two.
# SKIP the pytest hooks (testmon/xdist): those need the compiled Rust
# extension and run in the dedicated test job instead.
lint:
name: Lint & format (pre-commit)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: astral-sh/setup-uv@v6
with:
version: "latest"
python-version: "3.11"

- name: Install dev dependencies (includes pre-commit)
run: uv sync

- name: Run pre-commit hooks
run: SKIP=pytest-testmon,pytest-xdist-full uv run pre-commit run --all-files

# ── Job 2: build Rust extension + run tests ──────────────────────────────────
test:
name: Build & test
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v4

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- uses: astral-sh/setup-uv@v6
with:
version: "latest"
python-version: "3.11"

- name: Install dev dependencies
run: uv sync

- name: Build Rust extension (in-place)
run: uv run maturin develop --release

- name: Run tests in parallel
run: >
uv run pytest tests/
-n auto
--tb=short
-q
--cov=chem_engine
--cov-report=term-missing

# ── Job 3: RDKit cross-validation (optional, runs on main only) ──────────────
rdkit-validation:
name: RDKit cross-validation
runs-on: ubuntu-latest
needs: test
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- uses: astral-sh/setup-uv@v6
with:
version: "latest"
python-version: "3.11"

- name: Install dev dependencies + RDKit
run: uv sync && uv pip install rdkit

- name: Build Rust extension
run: uv run maturin develop --release

- name: Run RDKit cross-validation tests
run: uv run pytest tests/test_correctness_vs_rdkit.py --tb=short -q
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,3 @@ dist/
*.egg-info/*.iml

docs/specs


58 changes: 58 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-merge-conflict
- id: check-added-large-files

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.0
hooks:
- id: ruff
args: [--fix]
- id: ruff-format

- repo: https://github.com/Yelp/detect-secrets
rev: v1.5.0
hooks:
- id: detect-secrets
args: ["--baseline", ".secrets.baseline"]

# ── Affected-test selection (pre-commit) + full suite (pre-push) ─────────────
#
# NOTE: chem-engine is a Rust-Python extension (maturin).
# Build the extension and set up the dev environment with uv first:
#
# uv sync # install dev deps into .venv
# maturin develop --release # compile Rust extension in-place
# uv run detect-secrets scan > .secrets.baseline
# uv run pre-commit install --install-hooks
# uv run pre-commit install --hook-type pre-push
#
# pytest-testmon (pre-commit):
# Skips tests whose covered lines haven't changed since last run.
# First run builds the .testmondata DB (full suite).
#
# pytest-xdist (pre-push):
# Full suite in parallel — authoritative green-light before push.
#
- repo: local
hooks:
- id: pytest-testmon
name: "pytest-testmon: run only affected tests"
language: system
entry: uv run pytest tests/ --testmon --ignore=tests/test_correctness_vs_rdkit.py
pass_filenames: false
types: [python]
stages: [pre-commit]

- id: pytest-xdist-full
name: "pytest-xdist: full test suite in parallel (pre-push)"
language: system
entry: uv run pytest tests/ -n auto --ignore=tests/test_correctness_vs_rdkit.py
pass_filenames: false
types: [python]
stages: [pre-push]
1 change: 1 addition & 0 deletions .secrets.baseline
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"version": "1.5.0", "plugins_used": [], "filters_used": [], "results": {}, "generated_at": ""}
65 changes: 39 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,28 @@ See [docs/BENCHMARKS.md](docs/BENCHMARKS.md) for full throughput tables at 1K, 5

## Installation

### From source (requires Rust toolchain + maturin)
### From source (requires Rust toolchain)

```bash
# Install Rust: https://rustup.rs
# 1. Install Rust: https://rustup.rs
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Install maturin
pip install maturin
# 2. Install uv (if not already installed): https://docs.astral.sh/uv/
curl -LsSf https://astral.sh/uv/install.sh | sh

# Build and install chem-engine
# 3. Clone and build
git clone https://github.com/vandan-revanur/chem-engine.git
cd chem-engine
maturin develop --release
uv sync # creates .venv, installs all dev deps (incl. maturin)
uv run maturin develop --release # compiles Rust extension in-place
```

### Dev environment setup (first time)

```bash
uv run detect-secrets scan > .secrets.baseline # initialise secrets baseline
uv run pre-commit install --install-hooks # hook runs on git commit
uv run pre-commit install --hook-type pre-push # hook runs on git push
```

---
Expand All @@ -59,32 +68,32 @@ maturin develop --release
import chem_engine as ce

# Parse SMILES
mol = ce.parse_smiles("CC(=O)Oc1ccccc1C(=O)O") # aspirin
print(mol.num_atoms) # 13
print(mol.num_bonds) # 13
print(mol.amw) # ~180 (heavy atoms only)
print(mol.num_rotatable_bonds) # 3
mol = ce.parse_smiles("CC(=O)Oc1ccccc1C(=O)O") # aspirin
print(mol.num_atoms) # 13
print(mol.num_bonds) # 13
print(mol.amw) # ~180 (heavy atoms only)
print(mol.num_rotatable_bonds) # 3

# Canonical SMILES
can = ce.canonicalize(mol)
print(can) # deterministic string
print(can) # deterministic string

# 2D layout
mol2d = ce.generate_2d_coords(mol)
print(mol2d.coords_2d) # [[x, y], ...]
print(mol2d.coords_2d) # [[x, y], ...]

# 3D embedding
mol3d = ce.generate_3d_coords(mol)
print(mol3d.coords_3d) # [[x, y, z], ...]
print(mol3d.coords_3d) # [[x, y, z], ...]

# Fingerprint and similarity
fp = mol.get_fingerprint() # 2048-bit ECFP2
sim = mol.similarity(mol) # 1.0
fp = mol.get_fingerprint() # 2048-bit ECFP2
sim = mol.similarity(mol) # 1.0
print(sim)

# Substructure search
benzene = ce.parse_smiles("c1ccccc1")
print(mol.has_substruct_match(benzene)) # True
print(mol.has_substruct_match(benzene)) # True

# Tautomers
acetone = ce.parse_smiles("CC(=O)C")
Expand All @@ -106,16 +115,16 @@ from rdkit import Chem
# chem-engine -> RDKit
rust_mol = ce.parse_smiles("c1ccccc1")
rd_mol = to_rdkit(rust_mol)
print(Chem.MolToSmiles(rd_mol)) # RDKit canonical SMILES
print(Chem.MolToSmiles(rd_mol)) # RDKit canonical SMILES

# RDKit -> chem-engine
rd_mol = Chem.MolFromSmiles("CC(=O)O")
rust_mol = from_rdkit(rd_mol)
print(rust_mol.num_atoms) # 4
print(rust_mol.num_atoms) # 4

# Typical pattern: fast Rust pipeline, RDKit for accuracy-critical steps
rust_mol = ce.generate_3d_coords(ce.parse_smiles(smiles))
rd_mol = to_rdkit(rust_mol) # hand off to RDKit
rd_mol = to_rdkit(rust_mol) # hand off to RDKit
```

---
Expand Down Expand Up @@ -168,11 +177,17 @@ See [docs/BENCHMARKS.md](docs/BENCHMARKS.md) for full benchmark methodology and
## Running the tests

```bash
# Full test suite (481 tests, 0 failures)
python -m pytest tests/ -q
# Full test suite in parallel (286 tests, 0 failures)
uv run pytest tests/ -n auto -q

# With verbose output
python -m pytest tests/ -v
# With coverage report
uv run pytest tests/ -n auto --cov=chem_engine --cov-report=term-missing

# Affected tests only (fast, uses testmon — re-runs only tests touching changed files)
uv run pytest tests/ --testmon

# RDKit cross-validation (requires rdkit: uv pip install rdkit)
uv run pytest tests/test_correctness_vs_rdkit.py -v
```

Test files:
Expand Down Expand Up @@ -251,5 +266,3 @@ See [docs/FEATURES.md#limitations](docs/FEATURES.md#13-limitations-and-known-gap
## License

[MIT](LICENSE) - Copyright (c) 2026 Vandan Revanur


Loading
Loading