Imported from yesint/molar (
AGENTS.md). Install upstream withnpx skills add yesint/molar. Copyright stays with the author.
AGENTS.md
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
Code analysis
- Always prefer Rust LSP to grep when exploring the code.
- For “go to definition”, use LSP goToDefinition, not Grep.
- For “find references”, use LSP findReferences, not Grep.
- For type information or docs, use LSP hover.
- Use Grep/Glob only for discovery:
- finding files
- searching plain-text patterns
- locating candidate symbols before LSP
- After identifying the relevant Rust file/symbol, switch to LSP for navigation and understanding.
- Do not use Grep as a substitute for semantic reference search in Rust code when LSP is available.
Permissions
- Do not ask permissin to run shell commands "with consecutive quote characters at word boundaries or word start"; to run compound commands (piped, chained with
&&/;, or using subshells); any commands involvinggit.
Commands
# Build all workspace crates
cargo build
# Build in release mode
cargo build -r
# Run all tests
cargo test
# Run tests for a specific crate
cargo test -p molar
cargo test -p molar_membrane
# Run a single test by name
cargo test -p molar <test_name>
# Run tests with output shown
cargo test -p molar -- --nocapture
# Check compilation without building
cargo check
# Build documentation
cargo doc --open
# Build Python bindings (from molar_python directory)
cd molar_python && maturin build -r && python -m pip install .
Architecture
MolAR is a Cargo workspace (Rust edition 2024, MSRV 1.96) with these crates:
| Crate | Purpose |
|---|---|
molar |
Core library: SoA atom storage, selections, IO, topology, analysis tasks |
molar_gromacs |
Gromacs TPR support via a runtime-dlopened plugin (built only when Gromacs env vars are set; no compile-time dependency on Gromacs) |
molar_ff |
Force-field atom typing (GAFF/GAFF2) and partial charges (espaloma) |
molar_membrane |
Lipid membrane analysis (lipid order, curvature, etc.) |
molar_bin |
CLI utility (last, rearrange, solvate, tip3to4 commands) |
molar_python |
Python bindings via PyO3/maturin (wheel: pymolar) |
All file formats are pure Rust (the former molar_molfile VMD-plugin crate has been removed).
PowerSASA is an external git dependency, not a workspace crate.
Core data model (molar/src/)
Topology(topology.rs) — molecules,atoms: AtomStorage, andbonds: BondStorage; usually read once from fileAtomStorage(atom_storage.rs) — Struct-of-Arrays atom storage: one column per property. Ten always-present core columns (name,resname,resid,resindex,atomic_number,mass,charge,chain,bfactor,occupancy) plus four optional force-field/chemistry columns (type_name,type_id,formal_charge,flags) stored asOption<Vec<T>>— aNonecolumn costs nothing; when present it is full-length. Atoms are accessed through the borrowed proxiesAtomRef/AtomRefMut(a two-word{storage, index}handle) — there is no&Atomto borrow.Atom(atom.rs) — the owned, densely-packed atom row, retained as the detached construction/interchange type (builders, IO readers,From<&AtomLike>);AtomStorage::pushscatters it into the columns.AtomFlagsholds the ring/aromatic bits (no longer packed intotype_id).AtomLike(read getters) /AtomLikeMut(setters) — the atom interface, implemented byAtom,AtomRef, andAtomRefMut. Getters for the four optional properties returnOption(e.g.get_type_name() -> Option<&str>);chargeis the partial/working charge,formal_chargeis the integer formal charge (kept separate).BondStorage(bond_storage.rs) — Struct-of-Arrays bond storage, same discipline asAtomStorage: an always-present pair column (u32internally,usizeat every API boundary — caps a system at 4·10⁹ atoms) plus an optionalOption<Vec<BondOrder>>order column, absent for connectivity-only sources (PDB CONECT / GRO / TPR) so MD systems allocate nothing for it. Bonds are read through the borrowedBondRefproxy — there is no&Bondto borrow. The ownedBondrow (bond.rs) is the detached construction type;BondStorage::pushscatters it.BondAdjacency(bond_storage.rs) — the per-atom bonded-neighbor index (compressed rows), cached insideBondStorage.get_adjacency()is cheap and parallel-safe;ensure_adjacency(n_atoms)builds it (a plain&mutfield, not aOnceCell— interior mutability would costTopologyitsSync-through-&sharing across rayon). Structural change invalidates it, butset_orderdoes not — that asymmetry is the point of splitting the columns. Anything changing the atom count must callinvalidate_adjacency(), sinceoffsetsis sizedn_atoms + 1. Also usable standalone viaBondAdjacency::build(n, pairs), which is howmolar_ffindexes a remapped local subgraph. Neighbor order within an atom's run is a guaranteed invariant (ascending bond index) — the GAFF port indexes neighbors positionally and truncates to the first 4 or 6. Graph routines (sssr_rings,implicit_hydrogens,perception::perceive, all ofgaff) take a prebuilt adjacency; none builds a throwaway one.State(state.rs) — coordinates (Vec<Pos>), optional velocities/forces, timestamp, optionalPeriodicBoxSystem(selection/system.rs) — ownsTopology + State; the primary user-facing container
Adding a new atom or bond property
Both storages are designed to be extended — that is the main reason they are columnar. Adding an
optional column costs users who don't set it nothing (a None column is zero allocation), so
chemistry/force-field properties can be added without taxing MD workloads. Prefer an optional column
unless every atom or bond genuinely always has the property.
Reuse the existing generic helpers; do not hand-roll the materialization logic.
New optional atom column foo: Option<Vec<T>> (T: Copy), mirroring type_id /
formal_charge / flags:
atom.rs— addpub foo: Option<T>to the ownedAtomrow, plus awith_foo()builder.atom_storage.rs— add thefoo: Option<Vec<T>>field, then wire it into every place the other optional columns appear:push(viapush_opt),set(viaset_opt),remove_by_index(viaretain_mask),reserve, anensure_foo()materializer, and theinvariant_holds()length check. Missing one of these is how columns silently desynchronize —invariant_holds()isdebug_asserted after every mutation and will catch it in tests.AtomLike—get_foo() -> Option<T>(optional getters returnOption);AtomLikeMut—set_foo(). Implement forAtom,AtomRef,AtomRefMut.- Extend the
to_atom/From<&AtomLike>round-trip so the property survives a detach/reattach.
An always-present core column is the same minus the Option plumbing; add a slice accessor
(foos()) only if the selection hot path in ast.rs actually scans it — that is the sole reason
those accessors exist.
⚠ Optional-column setters materialize the column and are therefore serial-only. Materialize before entering any parallel region (see Parallel operations below).
New optional bond column bar: Option<Vec<T>>, mirroring the order column:
bond.rs— add the field to the ownedBondrow.bond_storage.rs— addbar: Option<Vec<T>>, then:push(mirror thepush_orderhelper — materialize only when the incoming value is informative, so a default-valued bond leaves the column absent), aset_bar(mirrorset_order), the compact-and-truncate step inremove_by_index, the manualCloneimpl, and theinvariant_holds()length check.BondRef::bar()— return the property's default when the column is absent, exactly asorder()yieldsUnspecified. Addhas_bar()only if a caller must distinguish absent from default. ExtendFrom<&BondRef> for Bond.
⚠ A new bond column that is not connectivity (stereo, wedge direction, rotatable flag, …) must
not invalidate the cached BondAdjacency — follow set_order, not push. Only changes to the
pair column or the atom count invalidate it.
Good candidates already identified: bond stereo / E-Z configuration and wedge direction (the SDF reader currently parses and discards the stereo field), ring-membership and rotatable flags.
Selection system (molar/src/selection/)
The key design: a Sel is just a sorted SVec of atom indices — it is detached from any System. To do work, it must be bound:
sys.bind(&sel)→SelBound<'_>(read-only, borrows system)sys.bind_mut(&sel)→SelBoundMut<'_>(read-write, mutably borrows system)sys.select_bound("...")→SelOwnBound<'_>(creates and binds in one step)sys.select_bound_mut("...")→SelOwnBoundMut<'_>
Borrow checking is enforced at compile time — you cannot hold a mutable and immutable bound selection simultaneously.
Empty selections are forbidden — selection methods return Err instead of an empty selection.
Traits that provide behavior live in selection/traits.rs and providers.rs:
AtomProvider/AtomMutProvider(providers.rs) — the atom-access layer. The single required method isatom_storage()/atom_storage_mut(); the defaults (iter_atoms,get_atom,iter_particle,par_iter_atoms, …) hand outAtomRef/AtomRefMutproxies rather than&Atom.PosProvider/VelProvider/… mirror this for coordinates/velocities/forces.Selectable/SelectableBound— creating sub-selections;split/split_par(blanket-impl methods) anditer_particle*for particle access.- Selection keyword evaluation (
ast.rs) scans one projected column at a time (viaAtomStorage::names()/resids()/… slice accessors) rather than materializing a proxy per atom.
IO (molar/src/io/)
FileHandler dispatches by file extension to format-specific handlers — all pure Rust, no
external plugin:
.pdb/.ent,.xyz,.gro→ streaming structure/trajectory handlers.dcd,.xtc→ binary trajectories (random access / seek supported).itp→ topology only;.sdf/.mol→ MDL molfile (bonds with order, formal charge).nc/.ncdf→ AMBER NetCDF (optionalnetcdffeature).tpr→ TPR handler, whichdlopens the Gromacs plugin at runtime (see Optional Gromacs linking)
FileHandler implements IntoIterator yielding State for trajectory iteration. IO runs in a background thread with a channel buffer of 10 frames.
Analysis task framework (molar/src/analysis_task.rs)
Implement AnalysisTask<UserArgs> trait with three methods:
new(context)— called on first valid frame; create selections hereprocess_frame(context)— called per framepost_process(context)— called after all frames
Standard CLI args (-f files -b begin -e end --log --skip) are handled automatically by TrajAnalysisArgs. Invoke with TaskType::run().
Parallel operations
Two parallel patterns:
par_iter_pos()/par_iter_atoms()— rayon parallel iteration within a single selectionsplit_par(closure)— produces non-overlappingParSplit; iterate withsys.iter_par_split_mut(&par)for parallel processing of distinct fragments (e.g., per-molecule unwrapping)
The mutable parallel path hands out AtomRefMut proxies over disjoint atom indices. Core-column
setters read the column Vec header via a shared borrow and write the heap buffer through a raw
element pointer (never forming &mut [T]), which is race-free across threads — verified with Miri
under Tree Borrows (par_atom_column_write_scoped). Optional-column setters materialize the column
and are serial-only: materialize optional columns before entering a parallel region. When adding
unsafe to the atom layer, run:
MIRIFLAGS="-Zmiri-tree-borrows" cargo +nightly miri test -p molar --lib par_atom_column_write_scoped
Optional Gromacs linking
To enable TPR reading, create .cargo/config.toml (use config.toml.template as a starting point) with:
[env]
GROMACS_SOURCE_DIR = "<path>"
GROMACS_BUILD_DIR = "<path>"
GROMACS_LIB_DIR = "<path>"
Without this, the crate compiles but TPR reading is unavailable.
Coordinate units
All coordinates and distances use nanometers (matching Gromacs convention), not Angstroms.
Test data
Integration test files live in molar/tests/: protein.pdb, protein.xtc, membr.gro, topol.tpr, etc.
Language and writing style
Only report to me in ASD-STE100 Simplified Technical English.