I/O: Reading and Writing File Formats¶
Readers turn files into explicit format products; writers consume Frames plus any format-owned side inputs. To add support for a new format, see Developer Guide: I/O Formats.
MolPy's I/O system has three layers: data (single-frame structures), trajectory (multi-frame sequences), and forcefield (parameter files). Each layer has its own reader/writer base class.
Data files¶
Most data readers return a Frame. LAMMPS data parsing returns a LammpsDataResult because its ForceField, header counts, and type-label inventory are format products, not frame fields. A Frame carries blocks, dict-like meta, and an optional box.
Reading¶
Every format has a factory function at mp.io.read_*:
The examples below share this setup:
import numpy as np
import molpy as mp
rng = np.random.default_rng(0)
xyz = rng.uniform(0.0, 20.0, size=(200, 3))
frame = mp.Frame()
frame["atoms"] = {
# A writer asks for the columns its format has fields for: LAMMPS wants a
# numeric `type_id` and `mol_id`, XSF wants `atomic_number`.
"id": np.arange(1, 201, dtype=np.uint32),
"mol_id": np.repeat(np.arange(1, 101, dtype=np.uint32), 2),
"type_id": np.tile(np.array([1, 2], dtype=np.uint32), 100),
"type": np.tile(np.array(["OW", "HW"]), 100),
"element": np.tile(np.array(["O", "H"]), 100),
"atomic_number": np.tile(np.array([8, 1], dtype=np.uint32), 100),
"charge": np.tile([0.4, -0.4], 100),
"x": xyz[:, 0],
"y": xyz[:, 1],
"z": xyz[:, 2],
}
frame.box = mp.Box.cubic(20.0)
# docs: skip — reads offline structure files; I/O unit-tested with fixtures
import molpy as mp
frame = mp.io.read_pdb("molecule.pdb")
frame = mp.io.read_gro("system.gro")
frame = mp.io.read_mol2("ligand.mol2")
result = mp.io.read_lammps_data("system.data", atom_style="full")
frame = result.frame
forcefield = result.forcefield
frame = mp.io.read_xyz("coords.xyz")
SMILES in, local SMARTS out — both on the io surface, not methods on the graph:
Readers take a path; readers whose format has only frame state return a Frame. LAMMPS data callers explicitly choose result.frame, result.forcefield, result.counts, or result.type_labels.
# docs: skip — reads offline structure files; I/O unit-tested with fixtures
# Populate an existing frame while preserving typed metadata
existing = mp.Frame(meta={"timestep": 42})
frame = mp.io.read_pdb("molecule.pdb", frame=existing)
print(frame.meta["timestep"]) # 42
The Frame returned by a data reader typically contains an "atoms" block with coordinates, and may contain "bonds", "angles", and "dihedrals" blocks depending on the format.
# docs: skip — reads offline structure files; I/O unit-tested with fixtures
frame = mp.io.read_pdb("water.pdb")
print(frame["atoms"].nrows) # number of atoms
print(list(frame["atoms"].keys())) # ['id', 'element', 'x', 'y', 'z',...]
Writing¶
Writers follow the same factory pattern:
mp.io.write_pdb("output.pdb", frame)
mp.io.write_gro("output.gro", frame)
mp.io.write_lammps_data("output.data", frame, atom_style="full")
mp.io.write_xsf("output.xsf", frame)
For LAMMPS, writing both the data file and force-field coefficients at once is the most common pattern. write_lammps_system is a convenience wrapper — it does exactly what calling write_lammps_data and write_lammps_forcefield yourself would. Its first argument is a directory: it is created if needed, and the two files (always named system.data and system.ff) are written inside it:
ff = mp.ForceField(name="demo", units="real")
atomstyle = ff.def_atomstyle("full")
atomstyle.def_type("OW", mass=15.999)
atomstyle.def_type("HW", mass=1.008)
paths = mp.io.write_lammps_system("system", frame, ff)
# Creates the directory system/ containing:
# system/system.data (topology + coordinates)
# system/system.ff (force-field coefficients)
# and returns {"data": Path("system/system.data"), "ff": Path("system/system.ff")}
Supported data formats¶
| Format | Read | Write | Notes |
|---|---|---|---|
| PDB | read_pdb |
write_pdb |
ATOM/HETATM + CRYST1 + CONECT |
| GRO | read_gro |
write_gro |
GROMACS structure |
| MOL2 | read_mol2 |
— | Tripos MOL2 |
| LAMMPS data | read_lammps_data |
write_lammps_data |
Requires atom_style |
| LAMMPS molecule | read_lammps_molecule |
write_lammps_molecule |
Template files |
| XYZ | read_xyz |
— | Simple coordinate format |
| XSF | read_xsf |
write_xsf |
XCrySDen format |
| AMBER AC | read_amber_ac |
— | Antechamber format |
| AMBER inpcrd | read_amber_inpcrd |
— | AMBER coordinates |
Trajectory files: lazy Frame sequences¶
Trajectory readers return objects that behave like indexed, iterable sequences of frames. They use memory-mapped files and persistent indexing to handle large trajectories efficiently.
Reading¶
# docs: skip — trajectory demo needs a well-typed frame from offline data
mp.io.write_lammps_trajectory("dump.lammpstrj", [frame, frame], atom_style="full")
reader = mp.io.read_lammps_trajectory("dump.lammpstrj")
print(reader.n_frames) # total frame count
frame_0 = reader[0] # random access by index
frame_last = reader[-1] # negative indexing
subset = reader[10:20] # slicing returns list[Frame]
Iteration is lazy — frames are parsed on demand with background prefetching:
# docs: skip — depends on trajectory reader from offline dump
for frame in reader:
atoms = frame["atoms"]
# process one frame at a time
Always close the reader when done (or use a context manager) to release memory-mapped file descriptors:
Writing¶
Trajectory writers accept a list of frames:
frames = [frame, frame]
mp.io.write_lammps_trajectory("output.lammpstrj", frames, atom_style="full")
mp.io.write_xyz_trajectory("output.xyz", frames)
Supported trajectory formats¶
| Format | Read | Write | Notes |
|---|---|---|---|
| LAMMPS dump | read_lammps_trajectory |
write_lammps_trajectory |
Custom columns supported |
| XYZ | read_xyz_trajectory |
write_xyz_trajectory |
Multi-frame XYZ |
All I/O is mp.io.read_* / mp.io.write_* only — there is no package-root
mp.read_* and no MolStore / Zarr store layer.
Log files: simulation run metadata¶
LAMMPS log files are different from data and trajectory files: they describe what LAMMPS did during each run, not where atoms were. MolPy preserves that structure. A parsed log contains the LAMMPS header, a tuple of runs, and raw text for anything that is not yet represented by a dedicated field.
The key idea is: use read_LAMMPS_log when you need run-level diagnostics such as thermo output, loop timing, load balance, neighbor statistics, or warnings. The thermo table stays in a NumPy-backed dataclass so downstream analysis can use dynamic LAMMPS column names directly.
In practice, each run is accessed in the same order it appears in the log:
# docs: skip — reads offline log.lammps; I/O unit-tested with fixtures
log = mp.io.read_LAMMPS_log("log.lammps")
run = log.runs[0]
print(run.thermo.columns)
print(run.thermo.data["Temp"].mean())
print(run.loop_time.seconds)
print(run.neighbor_statistics.dangerous_builds)
The class form exposes the same structured result:
# docs: skip — reads offline log.lammps; I/O unit-tested with fixtures
log = mp.io.LAMMPSLog("log.lammps").read()
first_run = log.runs[0]
print(first_run.thermo.data["Step"][0])
Supported log formats¶
| Format | Read | Write | Notes |
|---|---|---|---|
| LAMMPS log | read_LAMMPS_log |
— | Returns nested dataclasses aligned with LAMMPS run output |
Force field files: ForceField in, ForceField out¶
Force field readers parse parameter files into ForceField objects. Force field writers serialize ForceField objects into engine-specific formats.
LAMMPS *.ff includes are read and written
(the LAMMPS force-field reader/writer). Molpy's
mp.io.read_lammps_forcefield, write_lammps_forcefield, and
LAMMPSForceFieldWriter are the public API. Params live in engine units
(Å, kcal/mol, radians, harmonic ½k); the writer converts to LAMMPS real
(K = k/2, degrees) on output.
Reading¶
# docs: skip — reads offline force-field files; I/O unit-tested with fixtures
ff = mp.io.read_xml_forcefield(mp.data.get_forcefield_path("oplsaa.xml"))
ff = mp.io.read_lammps_forcefield(paths["ff"])
ff = mp.io.read_top("forcefield.itp")
AMBER prmtop files contain both structure and parameters. read_amber returns both:
# docs: skip — reads Amber prmtop/inpcrd artifacts from an offline run
frame, ff = mp.io.read_amber("system.prmtop", "system.inpcrd")
Writing¶
Each writer produces output in a specific engine format from the same ForceField object:
from molpy.io.forcefield import LAMMPSForceFieldWriter, XMLForceFieldWriter
from molpy.io.forcefield.top import GromacsForceFieldWriter
# LAMMPS coefficients
LAMMPSForceFieldWriter("system.ff", precision=4).write(ff)
# GROMACS.itp
GromacsForceFieldWriter("system.itp", precision=4).write(ff)
# OpenMM XML
XMLForceFieldWriter("system.xml", precision=6).write(ff)
Type filtering¶
LAMMPS force field files often need to include only the types actually used in the system. Pass type sets to filter the output:
LAMMPSForceFieldWriter("system.ff").write(
ff,
atom_types={"CT", "HC", "OH"},
bond_types={"CT-HC", "CT-OH"},
)
Supported force field formats¶
| Format | Read | Write | Notes |
|---|---|---|---|
| OpenMM/OPLS XML | read_xml_forcefield |
XMLForceFieldWriter |
Primary force field format |
| LAMMPS coefficients | read_lammps_forcefield |
LAMMPSForceFieldWriter |
Supports hybrid styles |
| GROMACS.itp | read_top |
GromacsForceFieldWriter |
Topology format |
| AMBER prmtop | read_amber |
— | Returns (Frame, ForceField) |
Extending with new formats¶
Adding a new data, trajectory, or force-field format is done by subclassing the reader/writer base classes and registering a factory function. That extension workflow — DataReader/DataWriter, BaseTrajectoryReader/TrajectoryWriter, and ForceFieldWriter + formatter registration — is documented in full in the Developer Guide: I/O Formats.
Quick reference¶
| I want to... | Function |
|---|---|
| Read a PDB | mp.io.read_pdb(path) |
| Write LAMMPS data + ff | mp.io.write_lammps_system(dir, frame, ff) |
| Read a trajectory | mp.io.read_lammps_trajectory(path) |
| Load OPLS-AA | mp.io.read_xml_forcefield(mp.data.get_forcefield_path("oplsaa.xml")) |
| Read AMBER topology | mp.io.read_amber(prmtop, inpcrd) |
| Write filtered ff | LAMMPSForceFieldWriter(path).write(ff, atom_types={...}) |
| Add a new format | Developer Guide: I/O Formats |
See also: API Reference: I/O, Concepts: Force Field.