molex
molex (molecular exchange) is a Rust library for parsing, analyzing, transforming, and serializing molecular structure data. It supports PDB, mmCIF, BinaryCIF, MRC/CCP4 density maps, and DCD trajectories.
Key concepts
-
MoleculeEntityrepresents a single molecule: a protein chain, a DNA/RNA strand, a ligand, an ion, or a group of waters. Parsing a structure file produces aVec<MoleculeEntity>. -
Assemblyis the top-level host container: it owns the entities, opt-in per-entity DSSP secondary structure (empty untilrecompute_ss()), and owner-set rendering connections, with a generation counter that increments on every mutation. Disulfide and backbone-H-bond geometry is computed on demand, not stored. -
Atomholds a position, element, atom name, occupancy, and B-factor. Residue and chain context live on the entity that contains the atom. -
AtomIdandCovalentBondare the cross-cutting identifiers: bonds reference atoms byAtomId { entity, index }so they remain addressable across reorderings. -
The assembly wire format is a compact binary serialization format for FFI and IPC that round-trips per-entity molecule-type metadata alongside coordinates.
-
Analysis includes covalent bond inference, DSSP hydrogen bond detection, disulfide bridges, secondary structure classification, AABBs, and volumetric/SES utilities.
-
VoxelGridandDensityrepresent 3D volumetric data (electron density, cryo-EM maps). -
Beyond the pure-Rust core, molex ships a feature-gated crystallographic-refinement subsystem (
xtal) and C / Python bindings for cross-language consumers.
Crate features
| Feature | Enables |
|---|---|
| (default) | Pure-Rust core: parsing, entity model, analysis, wire format |
serde | Serialize / Deserialize on the core types |
specta | TypeScript type-export derives |
python | PyO3 bindings + numpy interchange |
extension-module | Build the Python extension as a loadable wheel (with maturin) |
c-api | C ABI, libmolex.a, and the generated include/molex.h |
xtal | Crystallographic refinement pipeline (density, scaling, sigma-A, FFT) |
minimization | B-factor refinement (argmin), on top of xtal |
gpu | GPU density/refinement backend (cubecl + wgpu), on top of xtal |
testutil | Crystallographic test fixtures for external bench/integration crates |
API documentation
For the full Rust API reference, run:
cargo doc --open --document-private-items
Installation
As a Rust dependency
Add to your Cargo.toml:
[dependencies]
molex = "0.3"
To enable Python bindings (PyO3 + NumPy, Biotite-free numpy-column interchange via PyAtomTable, with optional AtomWorks-style vocabulary columns):
[dependencies]
molex = { version = "0.3", features = ["python"] }
Other optional features:
c-api– C ABI,libmolex.a, and the generatedinclude/molex.hxtal– crystallographic refinement pipeline (density, scaling, sigma-A, FFT)minimization– B-factor refinement (argmin), on top ofxtalgpu– GPU density/refinement backend (cubecl + wgpu), on top ofxtal
As a Python package
cd crates/molex
maturin develop --release --features python
python -c "import molex; print('OK')"
Dependencies
molex pulls in:
- glam – 3D math (
Vec3,Mat4) - ndarray – 3D arrays for density grids
- rmp – MessagePack for BinaryCIF decoding
- flate2 – gzip decompression
- thiserror – error types
With the python feature:
- pyo3 – Python FFI
- numpy – NumPy array interop
Quick Start
Parse a structure file into an assembly
Assembly::from_file dispatches on the extension (.pdb/.ent -> PDB,
mmCIF otherwise) and defaults to Completion::Heavy:
use std::path::Path;
use molex::Assembly;
let assembly = Assembly::from_file(Path::new("1ubq.pdb"))?;
for e in assembly.entities() {
println!("{:?}: {} atoms", e.molecule_type(), e.atom_count());
}
For string input use Assembly::from_pdb(&str) / from_mmcif / from_bcif,
each with a _with(..., Completion) variant. Reach the entities via
assembly.entities() and write back with assembly.to_pdb().
Work with entities
use molex::{MoleculeEntity, MoleculeType};
let entities = assembly.entities();
// Filter to protein chains
let proteins: Vec<_> = entities.iter()
.filter(|e| e.molecule_type() == MoleculeType::Protein)
.collect();
// Access protein-specific data
for entity in &proteins {
let protein = entity.as_protein().unwrap();
let backbone = protein.to_backbone();
println!(
"Chain {}: {} residues, {} segments",
protein.pdb_chain_id as char,
protein.residues.len(),
protein.segment_count(),
);
}
Run DSSP secondary structure assignment
Secondary structure is opt-in: an Assembly starts with empty ss_types, so
call recompute_ss() before reading it. Backbone H-bonds are never stored;
they surface on demand through detect_fallback_connections().
use molex::{Assembly, ConnectionType, SSType};
let mut assembly = Assembly::new(entities);
assembly.recompute_ss(); // populate ss_types (expensive; skipped at construction)
let protein_id = assembly.entities()[0].id();
for (i, ss) in assembly.ss_types(protein_id).iter().enumerate() {
println!("Residue {}: {:?}", i, ss); // Helix, Sheet, or Coil
}
if let Some(hbonds) = assembly.detect_fallback_connections().get(&ConnectionType::HBond) {
for link in hbonds {
println!("hbond {:?} -> {:?}", link.a, link.b);
}
}
Serialize to assembly binary (for FFI/IPC)
use molex::ops::wire::assembly_bytes;
let bytes = assembly_bytes(&entities)?;
// Send `bytes` over FFI, IPC, or network
assembly_bytes is the only public wire entry point. To serialize an
Assembly rather than a raw entity slice, call assembly.to_bytes(); decode
with Assembly::from_bytes(&bytes), which returns an assembly with empty
ss_types.
Python usage
import molex
# PDB round-trip via assembly bytes
assembly_bytes = molex.pdb_to_assembly_bytes(pdb_string)
pdb_back = molex.assembly_bytes_to_pdb(assembly_bytes)
# Biotite-agnostic columnar interchange (for ML pipelines)
table = molex.PyAtomTable.from_assembly_bytes(assembly_bytes)
coords = table.coords # (N, 3) float32 numpy array
assembly_bytes = table.to_assembly_bytes()
Architecture Overview
Module layout
molex/src/
├── adapters/ File format parsers (PDB, mmCIF, BinaryCIF, MRC, DCD, AtomWorks)
├── analysis/ Structural analysis (bonds, secondary structure, SASA, contacts, volumetric)
├── chemistry/ Static residue tables + missing-atom completion
├── entity/ Entity system
│ ├── molecule/ MoleculeEntity enum + subtypes (protein, nucleic acid, small molecule, bulk)
│ └── surface/ Surface types (VoxelGrid, Density)
├── ops/ Operations
│ ├── edit/ Typed AssemblyEdit + apply path
│ ├── error/ AdapterError
│ ├── transform/ Kabsch alignment, superposition RMSD
│ └── wire/ Assembly + delta binary wire format encoder/decoder
├── assembly.rs Top-level Assembly container (entities + opt-in SS + connections)
├── atom_id.rs Cross-cutting AtomId (entity + index)
├── bond.rs Cross-cutting CovalentBond (AtomId endpoints)
├── connection.rs Inter-entity rendering connections (ConnectionType, AtomEnd, AtomLink)
├── element.rs Element enum (symbols, covalent radii, colors)
├── xtal/ Crystallographic refinement pipeline (feature = "xtal")
├── c_api/ C ABI surface (feature = "c-api")
├── python/ PyO3 bindings (feature = "python")
└── lib.rs Crate root, re-exports
Entity-first design
Entities (Vec<MoleculeEntity>) are the primary data model.
Adapters parse files into an Assembly. The Assembly::from_file / from_pdb / from_mmcif / from_bcif constructors are the primary API; the *_to_all_models variants return one entity list per MODEL block for NMR ensembles or multi-state trajectories.
Analysis operates on &[Atom], &[ResidueBackbone], or &[MoleculeEntity].
Assembly
Assembly (in src/assembly.rs) is the host-owned structural source of truth. It bundles:
- a
Vec<Arc<MoleculeEntity>> - per-entity
SSTypearrays (ss_types), empty untilrecompute_ss()is called - owner-set rendering connections keyed by
ConnectionType - a generation counter that bumps on every mutation
Construction is secondary-structure-free and mutations only bump the
generation counter; secondary structure is opt-in via recompute_ss(), and
disulfide / backbone-H-bond geometry is computed on demand by
detect_fallback_connections() rather than stored. See the
Assembly page for the full contract.
Entity classification
When a file is parsed, atoms are grouped into entities by chain ID, residue name, and molecule type. The classify_residue function maps 3-letter residue codes to MoleculeType values using lookup tables:
| MoleculeType | Examples |
|---|---|
Protein | Standard amino acids (ALA, GLY, …) |
DNA | DA, DT, DC, DG |
RNA | A, U, C, G |
Ligand | ATP, HEM, NAG, … |
Ion | ZN, MG, CA, FE, … |
Water | HOH, WAT, DOD |
Lipid | OLC, PLM, … |
Cofactor | HEM, NAD, FAD, … |
Solvent | GOL, EDO, PEG, … |
Type hierarchy
MoleculeEntity (enum)
├── Protein(ProteinEntity) -- chain with residues, segment breaks
├── NucleicAcid(NAEntity) -- DNA/RNA chain with residues
├── SmallMolecule(SmallMoleculeEntity) -- single ligand, ion, cofactor, lipid
└── Bulk(BulkEntity) -- grouped water or solvent molecules
Entity trait -- id(), molecule_type(), atoms(), positions(), atom_count()
Polymer trait -- residues(), segment_breaks(), segment_count(), segment_range()
ProteinEntity and NAEntity implement both Entity and Polymer. SmallMoleculeEntity and BulkEntity implement Entity only.
Surface types
The entity::surface module provides volumetric data types:
VoxelGrid– a generic 3D grid (ndarray::Array3<f32>) with crystallographic cell metadata. Handles fractional-to-Cartesian coordinate conversion.Density– wrapsVoxelGridwith density-specific metadata. Constructed by the MRC adapter.
The feature-gated xtal refinement layer sits above these VoxelGrid / Density primitives: it synthesizes density from atoms, scales against experimental structure factors, and refines B-factors.
Binary format
The assembly format is molex’s compact binary IPC format: a fixed 8-byte magic (ASSEMBLY) followed by a version byte that selects the payload layout (only version 1 exists today). It is entity-aware: each entity is preceded by a 9-byte header (molecule-type byte, atom count, and EntityId) so the decoder reconstructs MoleculeEntity variants without re-running residue classification, and a trailing variants section carries per-residue variant tags. See the Wire Format page for the full byte layout.
Data Flow
Overview
┌──────────────┐
PDB / mmCIF / BCIF ──>│ ├──> Vec<MoleculeEntity>
MRC / CCP4 ──>│ Adapters ├──> Density (wraps VoxelGrid)
DCD ──>│ ├──> Vec<DcdFrame>
└──────┬───────┘
│
v
┌───────────────────────┐
│ Assembly │
│ (entities + opt-in │
│ ss + connections) │
└──┬────────┬────────┬──┘
│ │ │
v v v
┌──────────┐ ┌──────────┐ ┌───────────┐
│ Analysis │ │Transform │ │ Wire │
│ │ │ │ │ │
│ dssp │ │ kabsch │ │ assembly │
│ bonds │ │ align │ │ bytes │
│ disulfide│ │ rmsd │ │ + │
│ sasa │ │ │ │ delta │
└──────────┘ └──────────┘ └─────┬─────┘
│
v
FFI / IPC / Python
Analysis, Transform, and Wire are independent; use any combination depending on what you need.
1. Parsing
Every structure adapter returns Vec<MoleculeEntity>:
let entities = Assembly::from_file(Path::new("1ubq.pdb"))?.into_entities();
let entities = Assembly::from_file(Path::new("3nez.cif"))?.into_entities(); // mmCIF by extension
// BinaryCIF parses from bytes (no path-taking entry point):
let bytes = std::fs::read("1ubq.bcif")?;
let entities = Assembly::from_bcif(&bytes)?.into_entities();
Density and trajectory adapters return their own types:
let density = mrc_file_to_density(Path::new("emd_1234.map"))?;
let frames = dcd_file_to_frames(Path::new("trajectory.dcd"))?;
2. Entity construction
Each parser tokenizes its input and pushes one AtomRow per atom into an
EntityBuilder. EntityBuilder is the single point of classification: it
groups rows by chain plus residue scope, joins mmCIF _entity /
_entity_poly hints when present, and emits one MoleculeEntity per
logical molecule (protein chain, NA chain, ligand instance, water bulk,
solvent bulk) at finish(). Each emitted entity carries a freshly
allocated EntityId.
For NMR ensembles or multi-model trajectories, the adapter-level
*_to_all_models entry points (pdb_str_to_all_models,
mmcif_str_to_all_models, bcif_to_all_models, plus the matching
_file_* variants) return one Vec<MoleculeEntity> per MODEL.
3. Analysis
use molex::{detect_disulfides, Assembly};
use molex::analysis::{infer_bonds, DEFAULT_TOLERANCE};
// Secondary structure is opt-in: build, then recompute.
let mut assembly = Assembly::new(entities);
assembly.recompute_ss();
let ss = assembly.ss_types(entity_id);
// Disulfide + backbone-H-bond geometry is computed on demand for
// rendering, not stored on the Assembly:
let connections = assembly.detect_fallback_connections();
// Or call the building blocks directly:
let bonds = infer_bonds(atoms, DEFAULT_TOLERANCE); // distance-based
let disulfides = detect_disulfides(&entities); // CYS SG-SG
4. Transforms
molex::ops exposes Kabsch alignment and superposition RMSD; nothing else
lives in the transform surface.
use molex::ops::kabsch_alignment;
use molex::ops::transform::rmsd;
let aligned = kabsch_alignment(&reference, &target); // Option<(Mat3, Vec3)>
let deviation = rmsd(&a, &b); // Option<f32>
5. Serialization
For sending to C/C++/Python consumers, assembly_bytes is the only public
wire entry point (it serializes a raw entity slice). Decode with
Assembly::from_bytes, which returns a secondary-structure-free assembly;
call recompute_ss() if you need SS.
use molex::ops::wire::assembly_bytes;
let bytes = assembly_bytes(&entities)?;
let mut assembly = molex::Assembly::from_bytes(&bytes)?; // ss_types empty
assembly.recompute_ss();
Entity System
The entity system lives in molex::entity (source: src/entity/).
MoleculeEntity
The central type. An enum with four variants:
pub enum MoleculeEntity {
Protein(ProteinEntity),
NucleicAcid(NAEntity),
SmallMolecule(SmallMoleculeEntity),
Bulk(BulkEntity),
}
Common methods available on all variants (delegated to the inner type):
| Method | Returns | Description |
|---|---|---|
id() | EntityId | Unique identifier |
molecule_type() | MoleculeType | Classification (Protein, DNA, Ligand, etc.) |
atom_set() | &[Atom] | All atoms |
atom_count() | usize | Number of atoms |
positions() | Vec<Vec3> | All atom positions |
label() | String | Human-readable label (e.g. “Protein A”, “Ligand (ATP)”) |
residue_count() | usize | Residues (polymer) or molecules (bulk) |
Variant-specific downcasting: as_protein(), as_nucleic_acid(), as_small_molecule(), as_bulk().
MoleculeType
pub enum MoleculeType {
Protein, DNA, RNA, Ligand, Ion, Water, Lipid, Cofactor, Solvent,
}
Residue names are mapped to molecule types by classify_residue() using built-in lookup tables.
Atom
pub struct Atom {
pub position: Vec3, // 3D position in angstroms
pub occupancy: f32, // 0.0 to 1.0
pub b_factor: f32, // temperature factor
pub element: Element, // chemical element
pub name: [u8; 4], // PDB atom name (e.g. b"CA ")
pub formal_charge: i8, // signed formal charge; 0 = neutral
pub observed: bool, // true if parsed, false if fabricated by completion
}
observed is provenance metadata only (never part of atom identity); it backs
the Python observed_mask column. Atoms fabricated by completion carry
observed: false.
Residue name, residue number, and chain ID are stored on the entity/residue that owns the atom.
Residue
pub struct Residue {
pub name: [u8; 3], // structural-side residue name (e.g. b"ALA")
pub label_seq_id: i32, // mmCIF label_seq_id / PDB resSeq
pub auth_seq_id: Option<i32>, // mmCIF auth_seq_id; None falls back to label_seq_id
pub auth_comp_id: Option<[u8; 3]>, // mmCIF auth_comp_id; None falls back to name
pub ins_code: Option<u8>, // PDB iCode / mmCIF pdbx_PDB_ins_code; None = blank
pub atom_range: Range<usize>, // index range into the parent entity's atoms
}
Internal grouping uses the label_* fields; user-facing output uses the auth_* fields, falling back to the label_* values when the author-side identifiers are absent.
ProteinEntity
A single protein chain. Implements both Entity and Polymer traits.
pub struct ProteinEntity {
pub id: EntityId,
pub atoms: Vec<Atom>, // canonical order: N, CA, C, O, sidechain heavy..., H...
pub residues: Vec<Residue>, // name, number, atom_range
pub segment_breaks: Vec<usize>, // backbone gap indices
pub bonds: Vec<CovalentBond>, // intra-entity bonds (AtomId endpoints)
pub pdb_chain_id: u8, // structural-side chain byte (label_asym_id)
pub auth_asym_id: Option<u8>, // author-side chain byte; None = same as pdb_chain_id
}
Construction (ProteinEntity::new) reorders atoms into the canonical per-residue layout, drops residues missing any of N/CA/C/O (OXT counts as the C-terminal oxygen), computes segment breaks from C(i)->N(i+1) distance > 2.0 angstroms, and populates bonds from the AminoAcid chemistry tables plus universal backbone bonds (N-CA, CA-C, C=O) and inter-residue peptide bonds C(i)-N(i+1).
Derived views (computed on each call, not cached):
to_backbone() -> Vec<ResidueBackbone>– N, CA, C, O positions per residueto_interleaved_segments() -> Vec<Vec<Vec3>>– N/CA/C positions per continuous segment (for spline rendering)
Bond filters:
backbone_bonds()– iterator of bonds whose endpoints both fall inside the canonical backbone region of any residue (offsets 0..4)sidechain_bonds()– iterator of bonds with at least one heavy-atom sidechain endpoint
NAEntity
A single DNA or RNA chain. Same overall shape as ProteinEntity: atoms, residues, segment breaks, intra-entity bonds, pdb_chain_id, and auth_asym_id: Option<u8>. The DNA-vs-RNA discriminator is carried in an extra na_type: MoleculeType field. Implements Entity and Polymer.
SmallMoleculeEntity
A single non-polymer molecule (ligand, ion, cofactor, lipid). Implements Entity only.
BulkEntity
A group of identical small molecules (water, solvent). All atoms stored in a single flat Vec<Atom>. Implements Entity only.
EntityId
An opaque identifier allocated by EntityIdAllocator. Entities within a structure have unique IDs. The allocator is a simple incrementing counter.
Entity and Polymer traits
pub trait Entity {
fn id(&self) -> EntityId;
fn molecule_type(&self) -> MoleculeType;
fn atoms(&self) -> &[Atom];
fn positions(&self) -> Vec<Vec3>; // default impl
fn atom_count(&self) -> usize; // default impl
}
pub trait Polymer: Entity {
fn residues(&self) -> &[Residue];
fn residue_count(&self) -> usize; // default impl
fn segment_breaks(&self) -> &[usize];
fn segment_count(&self) -> usize; // default impl
fn segment_range(&self, idx: usize) -> Range<usize>; // default impl
fn segment_residues(&self, idx: usize) -> &[Residue]; // default impl
}
Surface types
The entity::surface module provides:
VoxelGrid– 3DArray3<f32>grid with unit cell metadata (dimensions, angles, origin, sampling intervals). Providesvoxel_size()andfrac_to_cart_matrix()for coordinate conversion.Density– wrapsVoxelGridwith density map metadata. Constructed bymrc_file_to_density()ormrc_to_density().
Assembly
Assembly (source: src/assembly.rs) is the host-owned structural source of
truth. It holds a Vec<Arc<MoleculeEntity>> plus three pieces of side state:
- per-entity DSSP secondary structure (
ss_types), populated only on demand - owner-set rendering connections (
connections), keyed byConnectionType - a monotonic
generationcounter that bumps on every mutation
Construction is secondary-structure-free. Assembly::new and
Assembly::from_arcs leave ss_types empty, because per-entity DSSP is
expensive and most callers do not need it. Secondary structure is opt-in:
call recompute_ss() when a consumer (a load, a render that reads SS) needs
it populated. Hydrogen bonds and disulfides are never stored on the
Assembly; they are either supplied by the owner via set_connections or
computed on demand from geometry via detect_fallback_connections.
Entities are stored behind Arc, so cloning an Assembly is O(entities) of
refcount bumps regardless of total atom count, and a mutation clones only the
touched entity (Arc::make_mut).
use molex::Assembly;
let mut assembly = Assembly::new(entities); // ss_types empty
assembly.recompute_ss(); // opt in to DSSP secondary structure
Read accessors
| Method | Returns | Description |
|---|---|---|
entities() | &[Arc<MoleculeEntity>] | All entities in declaration order |
entity(id) | Option<&MoleculeEntity> | Look up an entity by id |
into_entities() | Vec<MoleculeEntity> | Consume the assembly, returning owned entities (side state discarded) |
generation() | u64 | Monotonic counter, bumps on mutation |
sequence() | Vec<String> | One-letter sequence per polymer entity |
ss_types(id) | &[SSType] | Per-backbone-residue SS for an entity (empty until recompute_ss) |
ss_per_residue(id) | Option<Vec<SSType>> | SS spread across every residue of a protein (Coil where DSSP skipped) |
secondary_structure() | Vec<String> | Per-entity Q3 string, aligned 1:1 with entities() |
connections() | &HashMap<ConnectionType, Vec<AtomLink>> | Owner-set rendering connections |
detect_fallback_connections() | HashMap<ConnectionType, Vec<AtomLink>> | Viewer-only disulfide + backbone H-bond geometry computed on demand (pure) |
covalent_bonds() | Vec<CovalentBond> | Intra-entity bonds aggregated across entities |
contacts(cutoff, level, between_chains) | Vec<Contact> | Atom/residue/chain contacts (see Analysis) |
sasa(probe, n_points) | f32 | Total protein solvent-accessible surface area (see Analysis) |
Completion projections
Assembly re-completion is non-mutating: each method returns a fresh
assembly with its polymer entities rebuilt at a completion level (non-polymer
entities pass through unchanged). Atom indices and AtomIds are not stable
across these projections (a fabricated atom shifts every later atom), so
rendering connections are dropped rather than carried over; the generation
counter is preserved.
| Method | Level | Effect |
|---|---|---|
complete(level) | Completion | Rebuild at the given level (the canonical projection) |
normalize() | Heavy | Fabricate missing heavy atoms |
to_all_atom() | AllAtom | Fabricate missing heavy atoms plus template hydrogens |
See the Completion page for what each level fabricates.
Mutation
Direct content mutators (add_entity, remove_entity, entities_mut) are
crate-internal. Cross-crate callers mutate through the typed edit path so the
host’s broadcast routing has a single funnel:
use molex::ops::AssemblyEdit;
assembly.apply_edit(&AssemblyEdit::SetEntityCoords { entity, coords })?;
assembly.apply_edits(&edits)?; // apply a batch in order, stop at first error
Each successful apply bumps generation. Secondary structure is not
recomputed by the edit path; call recompute_ss() afterward if you need it.
See the Edits page for the full AssemblyEdit vocabulary.
carry_ss_from(&src) copies per-entity SS from an earlier snapshot onto a
freshly built coordinate-only snapshot (the streaming render path), skipping
any entity whose residue count no longer matches so stale SS is never indexed
against the wrong residues.
Completion
Parsed structures routinely omit atoms (hydrogens, distal sidechain atoms,
sometimes whole sidechains beyond the backbone). Completion brings each
standard residue’s atom set up to its ideal-geometry template: for every
residue that resolves to a standard amino acid or DNA/RNA base, it rigid-fits
the template’s present atoms onto the parsed residue (reusing Kabsch
alignment) and places each absent template atom at its transformed ideal
coordinate. The pass is purely additive: parsed atoms keep their coordinates,
only absent atoms are created. The machinery lives in src/chemistry and
src/entity/molecule/complete.rs.
Completion levels
Completion selects what construction is allowed to fabricate and whether it
keeps parsed hydrogens:
| Level | Fabricates | Input hydrogens |
|---|---|---|
Raw | nothing | kept |
Heavy | absent heavy template atoms | dropped |
AllAtom | absent heavy atoms plus template hydrogens | replaced by template H |
HeavyRaw | nothing | dropped |
Heavy is the file-ingest default: the no-level PDB/mmCIF/BinaryCIF parse
entry points use it, so Assembly::from_file and friends fill missing heavy
atoms and drop input hydrogens. Raw is the pure construction level (wire
round-trip, array ingest, continuous rebuild) where inputs are already
complete and re-running the rigid fit would be waste. AllAtom is the opt-in
to a fully protonated model. HeavyRaw strips hydrogens without fabricating.
Completion runs on each chain’s surviving residues; a residue dropped at
construction for lacking a complete backbone is not resurrected. Protein
C-termini gain their carboxylate OXT (and HXT under all-atom). N-terminal
extra protons and leaving atoms are never fabricated.
Re-completion projections
Assembly exposes non-mutating re-completion: complete(level) returns a
fresh assembly with its polymer entities rebuilt at level, with
normalize() and to_all_atom() as the Heavy / AllAtom shortcuts.
Non-polymer entities pass through unchanged. Because a fabricated atom shifts
every later atom, atom indices and AtomIds are not stable across the
projection, and rendering connections are dropped rather than carried over.
use molex::entity::molecule::Completion;
let heavy = assembly.normalize(); // Completion::Heavy
let all_atom = assembly.to_all_atom(); // Completion::AllAtom
let custom = assembly.complete(Completion::Raw);
From Python the same levels are Completion.Raw / Heavy / AllAtom, passed
to from_pdb / from_mmcif / from_bcif (default Heavy) and to
Assembly.complete. The C API exposes molex_Completion and the
*_with_completion parse variants.
Chemistry
molex::chemistry (source: src/chemistry/) owns the static residue
chemistry that entity construction and bond inference build on. The data is
pure: no entity references, no positions, no I/O.
Tables
amino_acids(AminoAcid): the standard amino acids plus modified-residue one-letter mapping. Provides the intra-residue bond topology used to populateProteinEntity::bonds.nucleotides(Nucleotide): DNA/RNA bases and their bond topology.atom_name(AtomName,is_protein_backbone_atom_name): canonical 4-character atom-name handling and the backbone-atom predicate.variant(VariantTag,ProtonationState): per-residue chemistry variants (terminus patches, disulfide, protonation states). Protonation is the load-bearing case:HIDandHIEdiffer only in which nitrogen carries the hydrogen, which heavy-atom positions alone do not determine, so the tag is carried explicitly through the wire.
Completion machinery
completion (ResidueTemplate, TemplateAtom) and the private
completion_data module hold the ideal-geometry templates that the
construction pass rigid-fits onto parsed residues to fabricate absent atoms.
See the Completion page for how the levels use them.
Adapters
Format adapters live in molex::adapters (source: src/adapters/). Each adapter’s primary API returns Vec<MoleculeEntity>.
Every structure parser feeds parser-emitted atom rows into the shared EntityBuilder, so chain/residue grouping, modified-residue merge logic, and entity classification are identical regardless of input format. Residues populate both their structural-side identifiers (label_seq_id, name) and the optional author-side identifiers from mmCIF / BinaryCIF (auth_seq_id, auth_comp_id, ins_code). ProteinEntity and NAEntity likewise carry an auth_asym_id: Option<u8> for the author-side chain label.
PDB (adapters::pdb)
Hand-rolled column-positional scanner over wwPDB v3.3 section 9. Records other than ATOM, HETATM, MODEL, ENDMDL, TER, and END are skipped.
The public entry points build an Assembly directly (the raw
*_to_entities functions are crate-internal):
Assembly::from_file(path: &Path) -> Result<Assembly, AdapterError>
Assembly::from_pdb(pdb_str: &str) -> Result<Assembly, AdapterError>
Assembly::from_pdb_with(pdb_str: &str, level: Completion)
-> Result<Assembly, AdapterError>
Assembly::from_file auto-detects PDB vs mmCIF by file extension
(.pdb/.ent -> PDB, everything else -> mmCIF) and defaults to
Completion::Heavy. Reach the parsed molecules via assembly.entities().
// Per-MODEL entry points (NMR ensembles, multi-state trajectories)
pdb_str_to_all_models(pdb_str: &str)
-> Result<Vec<Vec<MoleculeEntity>>, AdapterError>
pdb_file_to_all_models(path: &Path)
-> Result<Vec<Vec<MoleculeEntity>>, AdapterError>
// Writers
assembly_to_pdb(assembly: &Assembly) -> Result<String, AdapterError>
entities_to_pdb<E: Borrow<MoleculeEntity>>(entities: &[E])
-> Result<String, AdapterError>
The writers refuse to emit when the assembly exceeds any legacy PDB limit (>99,999 atoms, >62 polymer chains, >9,999 residues per chain), returning AdapterError::InvalidFormat so callers can switch to the mmCIF writer.
mmCIF (adapters::cif)
A streaming fast scanner handles the common case directly; a DOM-backed fallback covers structurally awkward files. Both paths emit AtomRow into EntityBuilder. The DOM types are also re-exported for typed extractors (CoordinateData, ReflectionData, UnitCell).
Assembly::from_mmcif(cif_str: &str) -> Result<Assembly, AdapterError>
Assembly::from_file(path: &Path) -> Result<Assembly, AdapterError> // PDB or mmCIF by extension
// One entity list per pdbx_PDB_model_num
mmcif_str_to_all_models(cif_str: &str)
-> Result<Vec<Vec<MoleculeEntity>>, AdapterError>
mmcif_file_to_all_models(path: &Path)
-> Result<Vec<Vec<MoleculeEntity>>, AdapterError>
BinaryCIF (adapters::bcif)
Decodes BinaryCIF (MessagePack-encoded CIF) with column-level codecs.
BinaryCIF is parsed from bytes, not a path: read the file and build an
Assembly directly. Assembly::from_file covers only the text formats
(.pdb/.ent/.cif/.mmcif), so .bcif callers read the bytes themselves.
Assembly::from_bcif(bytes: &[u8]) -> Result<Assembly, AdapterError>
Assembly::from_bcif_with(bytes: &[u8], level: Completion)
-> Result<Assembly, AdapterError>
// One entity list per pdbx_PDB_model_num
bcif_to_all_models(bytes: &[u8])
-> Result<Vec<Vec<MoleculeEntity>>, AdapterError>
bcif_file_to_all_models(path: &Path)
-> Result<Vec<Vec<MoleculeEntity>>, AdapterError>
MRC/CCP4 density maps (adapters::mrc)
Parses MRC/CCP4 format electron density maps into Density (which wraps VoxelGrid).
mrc_file_to_density(path: &Path) -> Result<Density, DensityError>
mrc_to_density(data: &[u8]) -> Result<Density, DensityError>
DCD trajectories (adapters::dcd)
Reads DCD binary trajectory files (CHARMM/NAMD format).
dcd_file_to_frames(path: &Path) -> Result<Vec<DcdFrame>, AdapterError>
pub struct DcdHeader { /* timestep, n_atoms, etc. */ }
pub struct DcdFrame { pub x: Vec<f32>, pub y: Vec<f32>, pub z: Vec<f32> }
pub struct DcdReader<R> { /* streaming reader */ }
AtomWorks vocab (adapters::atomworks, feature = python)
Molecule-type vocabulary maps (MoleculeType <-> AtomWorks chain_type / mol_type) and the columns de-vocab layer that marshals a native AtomTable into the vocab-bearing per-atom columns the Python interchange exposes. No runtime dependency on AtomWorks or Biotite. Requires the python feature.
The Python-facing columnar interchange built on this layer is PyAtomTable; see the Python bindings page.
Analysis
Structural analysis lives in molex::analysis (source: src/analysis/). Most analysis functions operate on entity-level types (&[Atom], &[ResidueBackbone], &[MoleculeEntity]).
Assembly exposes a few of these analyses as methods: ss_types (after
recompute_ss), detect_fallback_connections (disulfide + backbone-H-bond
geometry), covalent_bonds, contacts, and sasa. The standalone functions
documented below are the building blocks underneath. Note that secondary
structure is opt-in (it is empty until recompute_ss runs) and H-bonds /
disulfides are computed on demand rather than stored on the Assembly.
Secondary structure (analysis::ss)
DSSP-based secondary structure classification.
use molex::analysis::ss::{classify, from_string};
use molex::analysis::{HBond, SSType};
// Classify residues from a precomputed H-bond list.
let ss_types: Vec<SSType> = classify(&hbonds, n_residues);
// Parse a secondary-structure string like "HHHCCCEEE" into Vec<SSType>.
let ss_types = from_string("HHHCCCEEE");
SSType is a Q3 classification:
pub enum SSType { Helix, Sheet, Coil }
Each variant has a .color() method returning an RGB [f32; 3] for rendering.
analysis::merge_short_segments converts isolated 1-residue helix/sheet runs to Coil.
For most callers the recommended path is to construct an Assembly, call assembly.recompute_ss(), then read assembly.ss_types(entity_id). recompute_ss runs H-bond detection and classify for every protein entity; until it is called, ss_types is empty.
Bond detection (analysis::bonds)
Covalent bonds (distance-based)
use molex::analysis::{infer_bonds, InferredBond, BondOrder, DEFAULT_TOLERANCE};
let bonds: Vec<InferredBond> = infer_bonds(atoms, DEFAULT_TOLERANCE);
// InferredBond { atom_a: usize, atom_b: usize, order: BondOrder }
// BondOrder: Single, Double, Triple, Aromatic
Distance-based inference using element covalent radii with a configurable tolerance. Used for ligands and other non-protein entities where bond topology isn’t supplied by a chemistry table. Protein and nucleic-acid bonds are populated from the chemistry tables at entity construction time and live on ProteinEntity::bonds / NAEntity::bonds.
Hydrogen bonds
Backbone H-bond detection is an in-crate helper; the function analysis::bonds::hydrogen::detect_hbonds(&[ResidueBackbone]) is pub(crate). Assembly does not store H-bonds; the viewer-facing geometry is computed on demand via assembly.detect_fallback_connections(), which returns backbone H-bonds (and disulfides) keyed by ConnectionType:
use molex::{Assembly, ConnectionType};
let assembly = Assembly::new(entities);
if let Some(hbonds) = assembly.detect_fallback_connections().get(&ConnectionType::HBond) {
for link in hbonds {
println!("a={:?} b={:?}", link.a, link.b);
}
}
HBond is { donor: usize, acceptor: usize, energy: f32 } (Kabsch-Sander electrostatic energy in kcal/mol).
Disulfide bonds
use molex::{detect_disulfides, CovalentBond};
let disulfides: Vec<CovalentBond> = detect_disulfides(&entities);
Scans every protein entity for CYS SG atoms and emits one CovalentBond (with AtomId endpoints) per SG-SG pair within 1.5 to 2.5 angstroms. Assembly does not store disulfides; the same pairs surface for rendering via assembly.detect_fallback_connections() under ConnectionType::Disulfide.
Volumetric (analysis::volumetric)
Voxel-grid analysis used for surface and cavity work:
use molex::analysis::{
binary_to_sdf, compute_gaussian_field, compute_ses_sdf, detect_cavities,
detect_cavity_mask, edt_1d, edt_3d, voxelize_sas,
DetectedCavity, ScalarVoxelGrid, VoxelBbox,
};
These power Gaussian density approximations, solvent-excluded surface SDFs, and cavity detection.
Solvent-accessible surface area (analysis::sasa)
Shrake-Rupley SASA over an assembly’s protein atoms, using Bondi van der Waals radii and a deterministic golden-spiral sampling of each atom’s probe sphere (no RNG). Water, ions, ligands, and nucleic acids are excluded, matching the protein-scope convention of freesasa and biotite.
use molex::analysis::sasa::{DEFAULT_PROBE_RADIUS, DEFAULT_N_POINTS};
// Total protein SASA in Angstrom^2.
let area = assembly.sasa(DEFAULT_PROBE_RADIUS, DEFAULT_N_POINTS);
DEFAULT_PROBE_RADIUS is 1.4 (water); DEFAULT_N_POINTS is 960 test points
per atom (higher is finer and slower).
Contacts (analysis::contacts)
Assembly::contacts enumerates atom pairs closer than a cutoff, reported at
the requested granularity. A single spatial grid keeps neighbor lookups
near-linear.
use molex::analysis::{Contact, ContactLevel};
// All atom-atom contacts within 4.0 A, deduped to residue pairs,
// keeping only inter-entity pairs.
let contacts: Vec<Contact> = assembly.contacts(4.0, ContactLevel::Residue, true);
ContactLevel is Atom, Residue, or Chain; Residue and Chain dedup
to unique residue/entity pairs, each keeping one representative atom pair.
Transforms (ops::transform)
Kabsch alignment and superposition RMSD. kabsch_alignment is re-exported
from molex::ops; rmsd lives at molex::ops::transform.
use molex::ops::kabsch_alignment;
use molex::ops::transform::rmsd;
// Kabsch alignment (the rigid motion that minimizes RMSD between two point
// sets); None for mismatched lengths or fewer than 3 points.
let aligned: Option<(Mat3, Vec3)> = kabsch_alignment(&source, &target);
// Optimal-superposition RMSD; None for fewer than 3 points.
let deviation: Option<f32> = rmsd(&a, &b);
Edits
molex::ops::edit (source: src/ops/edit/) is the typed cross-crate
mutation route for an Assembly. The direct content mutators on Assembly
are crate-internal; an external caller mutates by building AssemblyEdit
values and applying them, so the host’s broadcast routing has a single typed
funnel. AssemblyEdit, EditError, and BulkEditError are re-exported from
molex::ops.
AssemblyEdit
AssemblyEdit covers the steady-state mutation vocabulary. Residues are
addressed by index within their parent entity’s residue list; EntityId is
the stable identifier across edits.
| Variant | Fields | Effect |
|---|---|---|
SetEntityCoords | entity, coords | Replace every atom position in the entity (coords.len() must equal atom_count) |
SetResidueCoords | entity, residue_idx, coords | Replace one residue’s atom positions (count must match; shape unchanged) |
MutateResidue | entity, residue_idx, new_name, new_atoms, new_variants | Replace a residue’s identity and atoms (count may change; later residues’ atom ranges shift) |
SetVariants | entity, residue_idx, variants | Replace a residue’s variant tag list |
AddEntity | entity (boxed) | Append an entity (id must not collide) |
RemoveEntity | entity | Remove an entity by id |
Per-residue topology (inserting or deleting a residue inside an entity) is not
enumerated; bridge consumers fall back to a full pose rebuild for that class
of change. The two topology variants (AddEntity / RemoveEntity) are
representable in memory but not on the delta wire.
Apply path
use molex::ops::AssemblyEdit;
assembly.apply_edit(&edit)?; // single edit
assembly.apply_edits(&edits)?; // batch, in order
Each successful apply bumps the assembly’s generation counter. Secondary
structure is not recomputed; call recompute_ss() afterward if a consumer
needs it. apply_edits stops at the first failing edit and returns a
BulkEditError carrying that edit’s index plus the underlying EditError;
edits applied before it stay applied.
EditError distinguishes the failure modes: UnknownEntity,
UnknownResidue, CountMismatch, NotPolymer (a per-residue edit aimed at a
non-polymer entity), and DuplicateEntity (an AddEntity whose id already
exists).
The delta wire format (Wire Format) serializes a
Vec<AssemblyEdit> for transport; topology edits are excluded there.
Errors
There is no ops::codec module. The crate’s shared parser/serializer error
type is AdapterError, which lives in molex::ops::error and is re-exported
at the crate root as molex::AdapterError (source: src/ops/error.rs).
The assembly binary wire format lives in
molex::ops::wire; the structural-transform routines (Kabsch
alignment, RMSD) live in molex::ops::transform.
Error type
AdapterError is the Err variant for the Assembly parse entry points
(from_pdb / from_mmcif / from_bcif / from_file and the *_to_all_models
multi-model functions), the PDB writers, and the assembly wire codec.
pub enum AdapterError {
/// The input bytes/text do not conform to the expected format.
InvalidFormat(String),
/// A PDB file could not be parsed.
PdbParseError(String),
/// An error occurred during binary serialization or deserialization.
SerializationError(String),
}
Density and trajectory adapters use their own error types (DensityError
from the MRC adapter, for instance); only the structure-and-wire paths funnel
through AdapterError.
Wire Format
molex::ops::wire holds two binary formats: the assembly format (a full
Assembly snapshot) and the delta format (an incremental edit list). Both
start with a fixed 8-byte magic followed by a u8 version byte that selects
the payload layout; the magic never changes when the payload does. Only
version 1 exists today, and any other version is a clean decode rejection.
Assembly format
The format is entity-aware: it preserves per-entity molecule-type metadata so
the decoder reconstructs MoleculeEntity variants without re-running residue
classification. It also carries each entity’s EntityId and a trailing
per-residue variants section.
Public surface
assembly_bytes is the only public free function; it serializes a raw entity
slice. serialize_assembly / deserialize_assembly are crate-internal. To
round-trip an Assembly use its to_bytes / from_bytes methods.
use molex::ops::wire::{assembly_bytes, ASSEMBLY_MAGIC};
use molex::Assembly;
// Serialize a raw entity slice.
let bytes: Vec<u8> = assembly_bytes(&entities)?;
// Round-trip an Assembly.
let bytes: Vec<u8> = assembly.to_bytes()?;
let decoded: Assembly = Assembly::from_bytes(&bytes)?; // ss_types empty
assert_eq!(&bytes[0..8], ASSEMBLY_MAGIC);
Decoding returns a secondary-structure-free assembly: ss_types is empty
until a caller invokes recompute_ss(). The decode path does not recompute
derived data.
Byte layout
The chain id lives in the per-entity header as a length-prefixed string, so chain ids are not capped to a single printable byte. Each atom row is 25 bytes.
8 bytes: magic "ASSEMBLY"
1 byte: version (currently 1)
4 bytes: entity_count (u32 BE)
Per entity header (variable):
1 byte: molecule_type wire byte
4 bytes: atom_count (u32 BE), the body-row count for this entity
4 bytes: entity_id (u32 BE), the originator's EntityId.raw()
2 bytes: chain_len (u16 BE)
chain_len bytes: chain id (UTF-8 label_asym_id; empty for non-polymers)
Per atom (25 bytes):
12 bytes: x, y, z (f32 BE x 3)
3 bytes: res_name
4 bytes: res_num (i32 BE)
4 bytes: atom_name
2 bytes: element symbol (byte 0, byte 1 or 0)
Per-entity variants section (after all atoms, in entity-header order):
per entity {
4 bytes: variant_residue_count (u32 BE)
per variant-bearing residue {
4 bytes: label_seq_id (i32 BE)
4 bytes: variant_count (u32 BE)
per variant {
1 byte: tag
tag-specific payload
}
}
}
Each entity contributes a variant block even when it carries no variants: a
non-polymer entity or one with no variant-bearing residues emits a
variant_residue_count of 0 (4 bytes).
Variant tags
0x01 NTerminus 0 payload bytes
0x02 CTerminus 0 payload bytes
0x03 Disulfide 0 payload bytes
0x04 Protonation 1 sub-tag byte:
0x01 HisDelta
0x02 HisEpsilon
0x03 HisDoubly
0xFF Custom (u16 BE length + UTF-8 bytes)
0xFE Other u16 BE length + UTF-8 bytes
Occupancy, b_factor, formal_charge, and the observed flag are not
preserved on the wire; deserialize resets occupancy / b_factor to 1.0 / 0.0.
Delta format
molex::ops::wire::delta carries an incremental edit list (Vec<AssemblyEdit>)
rather than a full snapshot; it is the steady-state path between the host and
its plugins. Topology edits (AddEntity / RemoveEntity) are not
representable on the wire; callers broadcast a full assembly snapshot for that
class of change.
use molex::ops::wire::delta::{serialize_edits, deserialize_edits, DELTA_MAGIC};
let bytes = serialize_edits(&edits)?;
let edits = deserialize_edits(&bytes)?;
Byte layout
8 bytes: magic b"DELTA\0\0\0"
1 byte: version (currently 1)
4 bytes: edit_count (u32 BE)
per edit:
1 byte: tag
tag-specific payload
Edit tags:
0x01 SetEntityCoords u32 entity_id, u32 coord_count, 12 bytes per coord (xyz f32 BE)
0x02 SetResidueCoords u32 entity_id, u32 residue_idx, u32 coord_count, 12 bytes per coord
0x03 MutateResidue u32 entity_id, u32 residue_idx, 3 bytes new_name, u32 atom_count,
25 bytes per atom (same row layout as the assembly format),
u32 variant_count, per-variant payload (same as the variants block)
0x04 SetVariants u32 entity_id, u32 residue_idx, u32 variant_count, per-variant payload
See the Edits page for the AssemblyEdit types these tags encode.
C API
molex::c_api (source: src/c_api/) is the C ABI surface, gated behind the
c-api cargo feature. It bridges Assembly and its read accessors plus
PDB/CIF/BCIF read and PDB/mmCIF write across the Rust/C boundary through
opaque-pointer handles and heap-allocated output buffers. It is the C/C++
counterpart of the Python object graph; the two surfaces parallel each other.
Naming
Handle types use bare names (molex_Assembly, molex_Entity,
molex_Residue, molex_Atom). Free functions keep a molex_ prefix because
C has no namespace mechanism. C++ consumers wrap the types in
namespace molex.
Ownership
- A
*mut molex_Assemblyreturned by a parser entry point is caller-owned and must be released withmolex_assembly_free. *const molex_Entity/*const molex_Residue/molex_Atomhandles are non-owning views into the parent assembly; they must not outlive it.- Byte buffers returned via
out_buf/out_lenare heap-allocated by Rust and freed withmolex_free_bytes.
Entry points
- Parsers:
molex_pdb_str_to_assembly,molex_cif_str_to_assembly,molex_bcif_to_assembly,molex_bytes_to_assembly, plus*_with_completionvariants that take amolex_Completionlevel (the no-level entries default toHeavy). - Writers:
molex_assembly_to_pdb,molex_assembly_to_mmcif,molex_assembly_to_bytes. - Projections:
molex_assembly_normalize,molex_assembly_to_all_atom,molex_assembly_completereturn fresh re-completed handles (see Completion). - Walk (
c_api::walk): entity / residue / atom accessors for reading an assembly’s structure. - Edits (
c_api::edit): the handle-based edit and delta surface that the PythonEditListparallels. - Crystallographic refinement (
c_api::xtal, gated onc-api+xtal):molex_experimental_data_from_sf_cifand its*_with_spacegroupvariant,molex_experimental_data_compute_density, andmolex_experimental_data_refine_b_factors.
Error reporting
Fallible entry points return either a null handle (parsers) or a nonzero
status code (writers). On failure a human-readable message is recorded in
thread-local storage and retrieved with molex_last_error_message; successful
calls do not clear it, so read it immediately after a failed call.
Python Bindings
Python bindings are available when molex is built with the python feature. The module is exposed as import molex via PyO3.
Installation
cd crates/molex
maturin develop --release --features python
The bindings are a directory module (src/python/), not a single file.
Object graph (Assembly / Entity / Residue)
The documented default for a normal caller is the object API: parse a structure, then walk entities and residues.
import molex
asm = molex.Assembly.from_pdb(pdb_string) # default Completion.Heavy
asm = molex.Assembly.from_mmcif(cif_string)
asm = molex.Assembly.from_bcif(bcif_bytes)
for entity in asm: # __len__ / __getitem__
print(entity.id, entity.kind, entity.chain_id, entity.residue_count)
for residue in entity.residues():
print(residue.name, residue.seq_id, residue.ins_code)
from_pdb / from_mmcif / from_bcif take an optional completion
argument (Completion.Raw / Heavy / AllAtom, default Heavy). Entity
exposes id, kind, molecule_type, chain_id, label, atom_count,
residue_count, residues(), and phi_psi() (per-residue backbone torsions
in degrees). Residue exposes name, seq_id, label_seq_id, and
ins_code.
Completion and analysis
heavy = asm.normalize() # Completion.Heavy projection
all_atom = asm.to_all_atom() # Completion.AllAtom projection
custom = asm.complete(molex.Completion.Raw)
asm.recompute_ss() # opt-in DSSP (ss starts empty)
ss = asm.secondary_structure() # one 'H'/'E'/'C' string per entity
area = asm.sasa() # total protein SASA (Angstrom^2)
bonds = asm.covalent_bonds() # [(i, j), ...] intra-entity, flat atom indices
ss_bonds = asm.disulfides() # [(i, j), ...] Cys SG-SG pairs
cif = asm.to_mmcif() # mmCIF string (round-trips via from_mmcif)
The complete projections re-index atoms, so externally held atom indices do
not carry across them. rmsd(a, b) is a free function: optimal-superposition
RMSD between two (N, 3) coordinate sets.
Edits and deltas
EditList builds a typed edit batch (parallel to the C edit surface);
Assembly.apply_edits applies it, and the delta bytes round-trip the
coordinate/residue/variant edits (topology edits are not representable on the
delta wire).
edits = molex.EditList()
edits.push_set_entity_coords(entity_id, new_coords) # [(x, y, z), ...]
edits.push_set_residue_coords(entity_id, residue_idx, new_coords)
edits.push_set_variants(entity_id, residue_idx, [molex.Variant.protonation_his_delta()])
asm.apply_edits(edits)
delta = edits.to_delta() # bytes
asm.apply_delta(delta) # decode + apply in one call
Assembly-bytes IO helpers
These free functions operate on serialized assembly bytes (the entity-aware binary format); they are transport helpers for the wire protocol, not the default path.
import molex
# Parse PDB string to assembly bytes
assembly_bytes = molex.pdb_to_assembly_bytes(pdb_string)
# Parse mmCIF string to assembly bytes
assembly_bytes = molex.mmcif_to_assembly_bytes(cif_string)
# Convert assembly bytes to PDB string
pdb_string = molex.assembly_bytes_to_pdb(assembly_bytes)
# Round-trip validation
validated = molex.deserialize_assembly_bytes(assembly_bytes)
Biotite-agnostic columnar interchange (PyAtomTable)
PyAtomTable round-trips atom data through molex as per-atom numpy columns,
with no Biotite import on either side. A plugin builds the Biotite (or any
other) representation from these columns itself.
import molex
# From assembly bytes -> columns
t = molex.PyAtomTable.from_assembly_bytes(assembly_bytes)
coords = t.coords # (N, 3) float32
atom_names = t.atom_names # numpy 'S4' byte strings, trailing-space trimmed
res_names = t.res_names # numpy 'S3' byte strings
res_ids = t.res_ids # (N,) int32
observed = t.observed_mask # (N,) bool
bonds = t.bonds() # [(i, j, order), ...] inferred ligand/ion bonds
# Columns -> molex (after a model edits the coordinates, say)
t2 = molex.PyAtomTable.from_columns(
coords, atom_names, t.elements, res_ids, res_names, t.chain_ids,
t.occupancies, t.b_factors, t.mol_types,
entity_ids=t.entity_ids, observed=observed,
)
assembly_bytes = t2.to_assembly_bytes()
atom_names/res_names/elements/mol_types accept either str or
fixed-width bytes (a numpy 'S' array works). Only coords, atom_names,
elements, res_ids, res_names, and chain_ids are required; absent
occupancies/b_factors default to 1.0/0.0 and absent observed to
True.
Each atom’s molecule type follows a three-tier precedence: chain_types (int32
codes) if given, else mol_types strings, else classification of the residue
name. So annotation-free input (hand-built backbone frames, concatenated
templates, folding-model outputs) classifies a protein backbone as protein
rather than ligand. When entity_ids is omitted, entity ids are synthesized
from each atom’s (chain_id, mol_type) pair in first-appearance order.
Crystallographic refinement (PyExperimentalData, feature = xtal)
When molex is built with the xtal feature, the Python module exposes
PyExperimentalData for structure-factor-based density synthesis and
B-factor refinement.
import molex
exp = molex.PyExperimentalData.from_sf_cif(sf_cif_string)
exp = molex.PyExperimentalData.from_sf_cif_with_spacegroup(sf_cif_string, 19)
density = exp.compute_density(atom_table) # PyAtomTable -> density grid
refined = exp.refine_b_factors(atom_table) # needs the `minimization` feature
# Metadata getters
exp.d_min # resolution limit (Angstrom)
exp.space_group_number
exp.num_reflections
exp.grid_dims # (nx, ny, nz)