Skip to content

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:

mol = mp.io.read_smiles("CCO")
pattern = mp.io.write_smarts(mol, next(iter(mol.atoms)))

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:

# docs: skip — depends on offline trajectory reader above
reader.close()

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.