Skip to content

Adding an I/O Format

mp.io has one door per format and direction, named after the format: read_<fmt>[_<what>] / write_<fmt>[_<what>] for a path (read_pdb, read_lammps_data, read_gromacs_top_forcefield), _str for text in memory (read_smiles_str), _bytes for bytes and _trajectory for every frame of a multi-frame file. No door picks a format for the caller from a file extension. There are no reader or writer classes to subclass. Parsing and serialization belong in the native core (molrs): mp.io mirrors molrs.io by identity, force-field formats included (mp.ff.forcefield is the ForceField data model only). A class that belongs to one format lives in that format's submodule (mp.io.smiles, mp.io.lammps, mp.io.mrec, …), mirrored by a molpy module of the same name.

A new format goes into molrs

Add the parser and writer to molrs and bind them in molrs-python: a function at the top of molrs.io (read_<fmt> / write_<fmt>, force-field formats included), a class in a molrs.io.<fmt> submodule. molpy picks a new function up with no edit, since mp.io re-exports the native ones wholesale; a new molrs.io.<fmt> submodule gets a two-line molpy mirror (src/molpy/io/<fmt>.py) imported by molpy/io/__init__.py:

import molrs
import molpy as mp

assert mp.io.read_gro is molrs.io.read_gro
assert (
    mp.io.write_lammps_forcefield
    is molrs.io.write_lammps_forcefield
)

No molpy wrapper coerces paths, re-raises native errors under another type or buffers frames for the native writer: the native doors accept str and os.PathLike, take any sequence of frames and report an unreadable file as OSError.

Behaviour belongs in the native door

A format's behaviour — merging inpcrd coordinates into an existing frame, joining an n-wide XYZ property, Type Labels and fix drude flags of a LAMMPS data file, fix bond/react maps, an AMBER prmtop's per-pair 1-4 weights (mp.io.read_amber_prmtop_system) — is molrs's, so every caller gets it. molpy keeps no reader of its own: mp.io is molrs.io by identity, and a convenience that only composes native doors (a prmtop plus its inpcrd, a SMILES string to a graph) is two native calls at the call site, not a molpy function.

Canonical field names

The data model uses one column vocabulary, molrs's: mp.core.keys is molrs.core.keys (mp.core.keys.CHARGE.key == "charge"), and mp.core.schema says each column's dtype. Every native reader emits these names — it maps a format's own spelling (q, mol, resSeq) at the boundary — and every writer takes them:

import molpy as mp

assert mp.core.keys.CHARGE.key == "charge"
assert mp.core.keys.MOL_ID.key == "mol_id"

Key canonical fields: charge (not q), mol_id (not mol), id, type, mass, element, x/y/z.

Force field readers and writers live in molrs

Every force-field reader and writer — LAMMPS *.ff includes and data-file * Coeffs, GROMACS directives and systems, OpenMM XML, AMBER prmtop / frcmod — is a native molrs function on mp.io, by identity (read_openmm_xml_forcefield / write_openmm_xml_forcefield, read_gromacs_top_forcefield / write_gromacs_top_forcefield, the LAMMPS family). The LAMMPS writers take the system as well: the coefficients written are selected by the frame's type labels, each matched to a type name exactly. The pair cutoff is a run setting the caller declares on the pair styles; no reader or writer invents one.

import molpy as mp

ff = mp.io.read_openmm_xml_forcefield(mp.resources.get_path("forcefield/tip3p.xml"))
water = mp.Frame(
    blocks={
        "atoms": {
            "type": ["tip3p-O", "tip3p-H", "tip3p-H"],
            "x": [0.0, 0.9572, -0.24],
            "y": [0.0, 0.0, 0.927],
            "z": [0.0, 0.0, 0.0],
        },
        "bonds": {"atomi": [0, 0], "atomj": [1, 2], "type": ["tip3p-O::tip3p-H"] * 2},
    }
)
ff.get_style("pair", "lj/cut")["cutoff"] = 10.0
ff.get_style("pair", "coul/cut")["cutoff"] = 10.0
text = mp.io.write_lammps_forcefield_str(ff, water, precision=4)
assert "bond_coeff" in text

A new style is exported by what molrs's writers make of its registration; see Extending the Force Field.

Checklist

  • Parser and writer added to molrs (molrs.io) and bound in molrs-python
  • Round-trip tests (write → read → compare) in molrs; molpy needs no edit
  • Box stored on frame.box; exact-dtype metadata stored on frame.meta