Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

  • MoleculeEntity represents a single molecule: a protein chain, a DNA/RNA strand, a ligand, an ion, or a group of waters. Parsing a structure file produces a Vec<MoleculeEntity>.

  • Assembly is the top-level host container: it owns the entities, opt-in per-entity DSSP secondary structure (empty until recompute_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.

  • Atom holds a position, element, atom name, occupancy, and B-factor. Residue and chain context live on the entity that contains the atom.

  • AtomId and CovalentBond are the cross-cutting identifiers: bonds reference atoms by AtomId { 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.

  • VoxelGrid and Density represent 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

FeatureEnables
(default)Pure-Rust core: parsing, entity model, analysis, wire format
serdeSerialize / Deserialize on the core types
spectaTypeScript type-export derives
pythonPyO3 bindings + numpy interchange
extension-moduleBuild the Python extension as a loadable wheel (with maturin)
c-apiC ABI, libmolex.a, and the generated include/molex.h
xtalCrystallographic refinement pipeline (density, scaling, sigma-A, FFT)
minimizationB-factor refinement (argmin), on top of xtal
gpuGPU density/refinement backend (cubecl + wgpu), on top of xtal
testutilCrystallographic 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 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

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 SSType arrays (ss_types), empty until recompute_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:

MoleculeTypeExamples
ProteinStandard amino acids (ALA, GLY, …)
DNADA, DT, DC, DG
RNAA, U, C, G
LigandATP, HEM, NAG, …
IonZN, MG, CA, FE, …
WaterHOH, WAT, DOD
LipidOLC, PLM, …
CofactorHEM, NAD, FAD, …
SolventGOL, 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 – wraps VoxelGrid with 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):

MethodReturnsDescription
id()EntityIdUnique identifier
molecule_type()MoleculeTypeClassification (Protein, DNA, Ligand, etc.)
atom_set()&[Atom]All atoms
atom_count()usizeNumber of atoms
positions()Vec<Vec3>All atom positions
label()StringHuman-readable label (e.g. “Protein A”, “Ligand (ATP)”)
residue_count()usizeResidues (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 residue
  • to_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 – 3D Array3<f32> grid with unit cell metadata (dimensions, angles, origin, sampling intervals). Provides voxel_size() and frac_to_cart_matrix() for coordinate conversion.
  • Density – wraps VoxelGrid with density map metadata. Constructed by mrc_file_to_density() or mrc_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 by ConnectionType
  • a monotonic generation counter 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

MethodReturnsDescription
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()u64Monotonic 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)f32Total 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.

MethodLevelEffect
complete(level)CompletionRebuild at the given level (the canonical projection)
normalize()HeavyFabricate missing heavy atoms
to_all_atom()AllAtomFabricate 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:

LevelFabricatesInput hydrogens
Rawnothingkept
Heavyabsent heavy template atomsdropped
AllAtomabsent heavy atoms plus template hydrogensreplaced by template H
HeavyRawnothingdropped

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 populate ProteinEntity::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: HID and HIE differ 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.

VariantFieldsEffect
SetEntityCoordsentity, coordsReplace every atom position in the entity (coords.len() must equal atom_count)
SetResidueCoordsentity, residue_idx, coordsReplace one residue’s atom positions (count must match; shape unchanged)
MutateResidueentity, residue_idx, new_name, new_atoms, new_variantsReplace a residue’s identity and atoms (count may change; later residues’ atom ranges shift)
SetVariantsentity, residue_idx, variantsReplace a residue’s variant tag list
AddEntityentity (boxed)Append an entity (id must not collide)
RemoveEntityentityRemove 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_Assembly returned by a parser entry point is caller-owned and must be released with molex_assembly_free.
  • *const molex_Entity / *const molex_Residue / molex_Atom handles are non-owning views into the parent assembly; they must not outlive it.
  • Byte buffers returned via out_buf / out_len are heap-allocated by Rust and freed with molex_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_completion variants that take a molex_Completion level (the no-level entries default to Heavy).
  • Writers: molex_assembly_to_pdb, molex_assembly_to_mmcif, molex_assembly_to_bytes.
  • Projections: molex_assembly_normalize, molex_assembly_to_all_atom, molex_assembly_complete return 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 Python EditList parallels.
  • Crystallographic refinement (c_api::xtal, gated on c-api + xtal): molex_experimental_data_from_sf_cif and its *_with_spacegroup variant, molex_experimental_data_compute_density, and molex_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)