molframe-query 0.3.0

Typed and textual structural selection plans for molframe
Documentation

molframe-query

The MolFrame selection language: a short text query such as byres (within 5 of resname HEM) and protein that picks atoms out of a structure. Every example here runs against haemoglobin, PDB 4HHB.

The complete language -- every keyword, column, operator and distance form, named queries, and what each error means -- is in the query language reference.

Run a query

Python

import molframe

structure = molframe.read("4hhb.cif")

heme = structure.select("resname HEM")   # a Selection
len(heme)                                # 172 atoms
heme.indices                             # numpy array of atom indices
heme.to_coordinates()                    # (172, 3) float32 array

Compile once when the same query runs many times; the compiled Query can be combined with &, | and ~:

pocket = molframe.Query("byres (within 5 of resname HEM) and protein")
pocket.select(structure)
structure.select(pocket)                 # the same thing

Selections combine like sets: a | b, a & b, a - b.

To see which residues a selection touched, walk the hierarchy:

def residues_in(structure, selection):
    """(chain, residue name) for every residue with a selected atom."""
    chosen = set(selection.indices.tolist())
    found = []
    for c in range(structure.chain_count):
        chain = structure.chains[c]
        for r in range(len(chain.residues)):
            residue = chain.residues[r]
            atoms = residue.atoms
            if any(atoms[a].index in chosen for a in range(len(atoms))):
                found.append((chain.label, residue.name))
    return found

residues_in(structure, structure.select("resname HEM"))
# [('E', 'HEM'), ('G', 'HEM'), ('H', 'HEM'), ('J', 'HEM')]

chain.label is the archive's label chain; mmCIF gives each haem its own, which is why these are not A to D.

Rust

use molframe::{AnalysisPolicy, Findings, Query, QueryStructure};

fn main() -> Result<(), Findings> {
    let structure = molframe::read("4hhb.cif")?;
    let query = Query::compile("byres (within 5 of resname HEM) and protein")?;
    let evaluation = structure.select_query(&query, &AnalysisPolicy::default())?;

    println!("{} atoms", evaluation.selection.len());
    for index in evaluation.selection.iter() {
        let Some(atom) = structure.atom_at(index as usize) else { continue };
        let residue = atom.residue().and_then(|residue| residue.auth_name());
        println!("{:?} {:?}", residue, atom.name());   // Some("MET") Some("N")
    }
    for warning in &evaluation.warnings {
        eprintln!("{warning}");
    }
    Ok(())
}

select_text(source, &policy) compiles and evaluates in one call. Findings carry a byte span into the query; render them with the query quoted and underlined:

let source = "name CA and bogus";
if let Err(findings) = Query::compile(source) {
    for finding in &findings {
        eprintln!("{}", molframe::Rendered::new(finding).with_source(source.as_bytes()));
    }
}

In MolGFX

Every MolGFX target is a MolFrame query, used unchanged: show cartoon, protein, select pocket, byres (within 5 of $heme) and protein.

The language at a glance

You want Write
Everything / nothing all, none
A kind of molecule protein, nucleic, water, ligand, polymer, hetero
Residues by name resname HEM, resname HIS HEM
A chain chain A, chain A B
Residue numbers resid 87, resid 1:10, resid 1-10 20 30
Atoms by name name CA, name N CA C O, name C*
Elements element Fe, element C N
A numeric condition bfactor > 50, occupancy < 1, 50 < bfactor
Both / either / not protein and chain A, water or ligand, not water
Near something within 5 of resname HEM
Whole residues byres (within 5 of resname HEM)
A named query $pocket

Keywords are case-insensitive (PROTEIN, Resname). Values are case-sensitive (resname HEM, not hem), except elements, which match in any case.

Everything else -- the full keyword and column tables, ranges, wildcards, distance forms, byres and same ... as, named queries, errors and warnings, and keywords that need extra data -- is in the query language reference.

How it works

Text queries and Rust builder expressions converge on one representation, so how a selection is written never changes what it means.

flowchart LR
    Text["Selection language"] --> Parse["Lexer / parser"]
    Builder["Typed Rust builder"] --> IR["Shared expression IR"]
    Parse --> IR

    IR --> Logical["LogicalPlan"]
    Logical --> Bind["Bind to Structure + policy"]
    Bind --> Physical["PhysicalQuery"]
    Physical --> Eval["Evaluator"]

    Eval --> Selection["AtomSelection"]
    Physical -. spatial predicates .-> Spatial["SpatialResolver"]

Binding resolves structure-local symbols and folds constant branches once, and the physical plan is reused across evaluations over the same topology. The planner reorders conjunctions by estimated cost, and spatial predicates are explicit dependencies that a spatial resolver answers.