3 releases (breaking)
Uses new Rust 2024
| 0.4.0 | Apr 2, 2026 |
|---|---|
| 0.3.0 | Apr 1, 2026 |
| 0.1.0 | Jan 22, 2026 |
#63 in Biology
350KB
7.5K
SLoC
Voronota-LT in Rust
Experimental, native Rust port of voronota-lt originally written in C++. Replaces the previous bindings crate. Computes radical Voronoi tessellation of atomic balls constrained inside a solvent-accessible surface. Outputs inter-atom contact areas, solvent accessible surface (SAS) areas, and volumes.
Features
- Pure, safe Rust
- Basic radical tessellation (stateless)
- Updateable tessellation for incremental updates (stateful)
- Periodic boundaries
- Per atom SASA and volume
- Contacts-only mode (skips cells/SAS/volumes for ~30–50% speedup)
- Groupings to avoid internal contacts
- Parallel processing using Rayon - see benchmarks below
- PDB, mmCIF, and XYZR input formats with auto-detection
- Unit-tests and benchmarks carried over from the C++ side
- Python bindings via PyO3
- Rust API and CLI
- Based on Voronota-LT v1.1.479 — reviewed against v1.1.492; all upstream bug fixes are already incorporated and new features (preliminary cutting planes, CAD-score-LT) are CLI-level additions not relevant to the core tessellation library
Installation
The port can be used either as a library for other projects, or as a basic CLI tool:
cargo install voronota-ltr
Rust API
use voronota_ltr::{Ball, Results, compute_tessellation};
let balls = vec![
Ball::new(0.0, 0.0, 0.0, 1.5),
Ball::new(3.0, 0.0, 0.0, 1.5),
Ball::new(1.5, 2.5, 0.0, 1.5),
];
let result = compute_tessellation(&balls, 1.4, None, None, false);
// Per-ball SAS areas and volumes (indexed by ball)
// Returns None for atoms without contacts (lonely atoms)
let sas_areas: Vec<Option<f64>> = result.sas_areas();
let volumes: Vec<Option<f64>> = result.volumes();
// Total SAS area
let total_sas: f64 = result.total_sas_area();
// Detailed contact and cell data
for contact in &result.contacts {
println!("Contact {}-{}: area={:.2}", contact.id_a, contact.id_b, contact.area);
}
Contacts only (no cells/SAS/volumes)
When only contact areas are needed (e.g. for contact energy calculations),
compute_contacts_only skips solid angle, volume, and cell computation:
use voronota_ltr::{Ball, compute_contacts_only};
let balls = vec![
Ball::new(0.0, 0.0, 0.0, 1.5),
Ball::new(3.0, 0.0, 0.0, 1.5),
Ball::new(1.5, 2.5, 0.0, 1.5),
];
// Optional group IDs to restrict to inter-group contacts
let groups = vec![0, 0, 1];
let contacts = compute_contacts_only(&balls, 1.4, None, Some(&groups));
for c in &contacts {
println!("Contact {}-{}: area={:.2}", c.id_a, c.id_b, c.area);
}
With periodic boundary conditions
use voronota_ltr::{Ball, PeriodicBox, compute_tessellation};
let balls = vec![Ball::new(0.0, 0.0, 0.0, 1.5)];
let pbox = PeriodicBox::from_corners((0.0, 0.0, 0.0), (10.0, 10.0, 10.0));
let result = compute_tessellation(&balls, 1.4, Some(&pbox), None, false);
Updateable tessellation
For simulations where only a few spheres change position each step, UpdateableTessellation
provides efficient incremental updates:
use voronota_ltr::{Ball, UpdateableTessellation};
let mut balls = vec![
Ball::new(0.0, 0.0, 0.0, 1.0),
Ball::new(2.0, 0.0, 0.0, 1.0),
Ball::new(4.0, 0.0, 0.0, 1.0),
];
// Create with backup support for undo
let mut tess = UpdateableTessellation::with_backup();
tess.init(&balls, 1.0, None);
// Move first sphere
balls[0].x += 0.1;
tess.update_with_changed(&balls, &[0]); // Only recompute affected contacts
// Get results
let summary = tess.summary();
println!("Total contacts: {}", summary.contacts.len());
// Undo last update
tess.restore();
Command Line Tool
Supports PDB, mmCIF, and XYZR input formats (auto-detected from extension or content):
# PDB / mmCIF / XYZR input
voronota-ltr structure.pdb --probe 1.4
# Save JSON output to file instead of stdout
voronota-ltr structure.pdb -o results.json
# Exclude heteroatoms (HETATM records)
voronota-ltr structure.pdb --exclude-heteroatoms
# Include hydrogen atoms (excluded by default)
voronota-ltr structure.pdb --include-hydrogens
# Custom radii file (format: residue atom radius per line)
voronota-ltr structure.pdb --radii-file custom_radii.txt
# With periodic boundary conditions
voronota-ltr structure.xyzr --periodic-box-corners 0 0 0 100 100 100
Custom selections
Group atoms using VMD-like selection syntax to filter contacts. Only inter-group contacts are computed.
# Protein-ligand interface (5IN3: galactose-1-phosphate uridylyltransferase)
voronota-ltr 5IN3.cif -s "protein" "resname G1P H2U"
# Homodimer interface
voronota-ltr 5IN3.cif -s "chain A" "chain B"
See the Selection Language section for full syntax.
Loading JSON output in Python
If using the CLI, results can be loaded from JSON:
import json
with open("results.json") as f:
data = json.load(f)
# Per-ball data (indexed by ball, None for atoms without contacts)
sas_areas = data["sas_areas"] # list of float or None
volumes = data["volumes"] # list of float or None
# Totals
total_sasa = data["total_sas_area"]
total_volume = data["total_volume"]
total_contact_area = data["total_contact_area"]
# Contact details
for contact in data["contacts"]:
print(f"Contact {contact['id_a']}-{contact['id_b']}: area={contact['area']:.2f}")
PyMOL visualization
Generate a Python script to visualize contact surfaces in PyMOL:
voronota-ltr structure.pdb --inter-chain-only --pymol contacts.py
pymol structure.pdb contacts.py
This creates three CGO objects: contacts_balls (cyan spheres), contacts_faces (yellow contact surfaces), and contacts_wireframe (red boundary lines).
Python Interface
Install directly from GitHub (requires a Rust toolchain):
pip install git+https://github.com/mlund/voronota-ltr.git
For local development, build and install using maturin:
pip install maturin
maturin develop --features python # Development build
maturin develop --features python --release # Optimized build
Both methods install the Python module and CLI binary.
Run tests:
python -m unittest discover -s tests -p "test_*.py"
Basic usage; input balls can be tuples, dicts, or NumPy arrays.
from voronota_ltr import compute_tessellation
result = compute_tessellation(
balls=[(0, 0, 0, 1.5), (3, 0, 0, 1.5), (1.5, 2.5, 0, 1.5)],
probe=1.4,
)
print(f"Total SAS area: {result['total_sas_area']:.2f}")
print(f"Total volume: {result['total_volume']:.2f}")
for contact in result["contacts"]:
print(f"Contact {contact['id_a']}-{contact['id_b']}: area={contact['area']:.2f}")
With periodic boundaries:
# Orthorhombic box from corner coordinates
result = compute_tessellation(
balls=balls,
probe=1.4,
periodic_box={"corners": [(0, 0, 0), (50, 50, 50)]},
)
# Triclinic cell from lattice vectors
result = compute_tessellation(
balls=balls,
probe=1.4,
periodic_box={"vectors": [(50, 0, 0), (0, 50, 0), (0, 0, 50)]},
)
With tessellation network output:
result = compute_tessellation(
balls=balls,
probe=1.4,
with_cell_vertices=True,
)
for vertex in result["cell_vertices"]:
print(f"Vertex at ({vertex['x']:.2f}, {vertex['y']:.2f}, {vertex['z']:.2f})")
print(f" On SAS: {vertex['is_on_sas']}")
From structure files
Compute tessellation directly from PDB, mmCIF, or XYZR files:
from voronota_ltr import compute_tessellation_from_file
# Basic usage
result = compute_tessellation_from_file("structure.pdb", probe=1.4)
# VMD-like selections for protein-ligand interface
result = compute_tessellation_from_file(
"complex.pdb",
probe=1.4,
group_selections=["protein", "resname LIG"],
)
# Inter-chain contacts only
result = compute_tessellation_from_file(
"dimer.cif",
probe=1.4,
group_selections=["chain A", "chain B"],
)
Solvent spheres
Generate weighted pseudo-solvent spheres on the exposed molecular surface:
from voronota_ltr import compute_solvent_spheres
spheres = compute_solvent_spheres(
balls=[(0, 0, 0, 1.5), (3, 0, 0, 1.5)],
probe=1.4,
subdivision_depth=2, # 0-4, controls sampling density (default: 2)
)
for s in spheres:
print(f"Sphere at ({s['x']:.2f}, {s['y']:.2f}, {s['z']:.2f}), "
f"weight={s['weight']:.3f}, parent={s['parent_index']}")
Selection Language
The -s/--selection flag accepts VMD-like selection syntax for defining atom groups.
Only contacts between different groups are computed, which is useful for interface analysis.
Syntax
voronota-ltr file.pdb -s "selection1" "selection2" ...
At least two selections are required. Only contacts between atoms in different groups are computed.
Keywords
| Keyword | Description | Example |
|---|---|---|
chain |
Chain identifier(s) | chain A B |
resname |
Residue name(s) | resname ALA GLY |
resid |
Residue number(s) or range | resid 1 to 100 or resid 1:100 |
name |
Atom name(s) with glob patterns | name CA or name C* |
protein |
Standard amino acids | protein |
backbone |
Protein backbone (C, CA, N, O) | backbone |
sidechain |
Protein sidechains (non-backbone) | sidechain |
nucleic |
Nucleic acid residues | nucleic |
hetatm |
HETATM records | hetatm |
hydrophobic |
Hydrophobic residues (ALA, VAL, LEU, ILE, MET, PHE, TRP, PRO, GLY) | hydrophobic |
aromatic |
Aromatic residues (PHE, TYR, TRP, HIS) | aromatic |
acidic |
Acidic residues (ASP, GLU) | acidic |
basic |
Basic residues (ARG, LYS, HIS) | basic |
polar |
Polar residues (SER, THR, ASN, GLN) | polar |
charged |
Charged residues (ASP, GLU, ARG, LYS) | charged |
all |
All atoms | all |
none |
No atoms | none |
Boolean operators
Combine selections with and, or, not, and parentheses:
# Sidechain-only dimer interface (exclude backbone)
voronota-ltr 5IN3.cif -s "chain A and sidechain" "chain B and sidechain"
# Protein-ligand contacts, excluding cryoprotectant (EDO)
voronota-ltr 5IN3.cif -s "protein and not resname EDO" "resname G1P H2U ZN"
Glob patterns
Atom and residue names support glob wildcards:
*matches any characters:name C*matches CA, CB, CG, etc.?matches single character:name ?Amatches CA, NA, etc.[abc]matches character class:name C[AG]matches CA or CG
Benchmarks
Run benchmarks with cargo bench.
Performance on Apple M4 processor (10 cores) - the speedup is relative to single threaded runs:
| Dataset | Balls | C++ (OpenMP) | Rust (Rayon) | C++ Speedup | Rust Speedup |
|---|---|---|---|---|---|
balls_cs_1x1 |
100 | 179 µs | 79 µs | 0.4x | 0.8x |
balls_2zsk |
3545 | 14 ms | 12 ms | 5.1x | 5.9x |
balls_3dlb |
9745 | 38 ms | 30 ms | 5.0x | 5.9x |
License
MIT License
Copyright (c) 2026 Kliment Olechnovic and Mikael Lund
Dependencies
~5–7.5MB
~150K SLoC