Skip to content

Getting Started

This chapter walks through the minimum you need to run a packing job and the most common customization points. Read the reference docs of individual types on the crate landing page for full details.

CLI

The molpack binary accepts the same .inp script format as the original Packmol, making it a drop-in replacement for the command-line workflow. See Install for setup.

Running a script

# File argument — paths inside the script resolve relative to its directory
molpack mixture.inp

# Stdin — compatible with the classic Packmol invocation
molpack < mixture.inp
cat mixture.inp | molpack

Script format

The .inp format is line-oriented. Lines starting with # are comments. Global keywords come first; each molecule type occupies a structure … end structure block.

# Global settings
tolerance 2.0          # minimum atom–atom distance in Å (default 2.0)
seed 42                # random seed for reproducibility (optional)
filetype pdb           # input format for all structure files (optional)
output packed.pdb      # output file; format inferred from extension
nloop 400              # max outer-loop iterations (default 400)
pbc 40.0 40.0 40.0     # optional: fully-periodic box [0,0,0]..[40,40,40]

# One block per molecule type
structure water.pdb
  number 1000
  inside box 0. 0. 0. 40. 40. 40.
end structure

structure urea.pdb
  number 400
  inside box 0. 0. 0. 40. 40. 40.
end structure

pbc keyword — declares a fully-periodic box (every axis wraps), matching Packmol's pbc. Two forms are accepted:

  • pbc X Y Zmin = [0, 0, 0], max = [X, Y, Z]
  • pbc X0 Y0 Z0 X1 Y1 Z1 — explicit min/max

When a script sets pbc, the packer's cell grid is built directly from that box. Without either a pbc directive or an inside-style restraint the packer falls back to inferring a box from the initial random placement (half-width defaults to 1000 Å), which drives the cell grid to tens of millions of cells — so always give the packer a spatial constraint.

Unknown keywords are rejected. The parser returns a ScriptError::UnknownKeyword rather than silently dropping them — a silent pbc drop used to burn 40+ GB of RAM before the packer even started.

Restraint keywords (all Packmol restraint types are supported):

Keyword Scope Description
inside box x0 y0 z0 x1 y1 z1 molecule / atoms Axis-aligned box
inside sphere cx cy cz r molecule / atoms Sphere
outside sphere cx cy cz r molecule / atoms Exclusion sphere
over plane nx ny nz d molecule / atoms Half-space (above)
below plane nx ny nz d molecule / atoms Half-space (below)

Per-atom-subset restraints use an atoms … end atoms sub-block:

structure palmitoil.pdb
  number 10
  inside box 0. 0. 0. 40. 40. 14.
  atoms 31 32          # 1-based atom indices
    below plane 0. 0. 1. 2.
  end atoms
  atoms 1 2
    over plane 0. 0. 1. 12.
  end atoms
end structure

Fixed-position placement:

structure protein.pdb
  number 1
  center
  fixed 0. 0. 0. 0. 0. 0.   # x y z  euler_x euler_y euler_z
end structure

Extended format support

molpack extends Packmol's PDB/XYZ input support with the additional readers from molrs-io. The filetype keyword or file extension selects the format:

Format Read Write Extension / filetype keyword
PDB .pdb / pdb
XYZ .xyz / xyz
SDF / MOL .sdf, .mol / sdf
LAMMPS dump .lammpstrj / lammps_dump
LAMMPS data .data / lammps_data

The output format is always inferred from the extension of the output path.

Running the canonical Packmol examples via CLI

The five canonical workloads each ship with a .inp file:

molpack examples/pack_mixture/mixture.inp
molpack examples/pack_bilayer/bilayer-comment.inp
molpack examples/pack_interface/interface.inp
molpack examples/pack_solvprotein/solvprotein.inp
molpack examples/pack_spherical/spherical-comment.inp

Rust library

Add the dependency (see Install for the full toolchain). Then write a packing job in code.

First pack: one molecule type in a box

use molpack::{InsideBoxRestraint, Molpack, Target};

let water_positions = [
    [0.0, 0.0, 0.0],
    [0.96, 0.0, 0.0],
    [-0.24, 0.93, 0.0],
];
let water_radii = [1.52, 1.20, 1.20];

let target = Target::from_coords(&water_positions, &water_radii, 100)
    .with_name("water")
    .with_restraint(InsideBoxRestraint::new([0.0; 3], [40.0; 3], [false; 3]));

// Every tuning knob has a Packmol-matching default, so `new().pack(...)`
// is a complete call; `200` is the outer-loop budget.
let frame = Molpack::new().pack(&[target], 200)?;

let natoms = frame.get("atoms").and_then(|b| b.nrows()).unwrap_or(0);
println!("packed {natoms} atoms");
# Ok::<(), molpack::PackError>(())

Arguments to Molpack::pack:

  • &[Target] — one entry per molecule type.
  • max_loops: usize — outer-iteration budget per phase.

Seed and every other tuning knob live on the builder (.with_seed(n), .with_tolerance(t) etc.). pack() returns the packed, topology-complete molrs::Frame — atom coordinates plus element and mol_id, with each molecule's topology replayed from its target template. Use .with_lammps_output(true), .with_log_level(level), and .with_log_frequency(n) for screen summaries/progress. Advanced callers that need structured convergence fields (fdist, frest, converged) can call pack_with_report, whose PackResult::frame is the same packed frame.

Restraint scopes

Restraints can be attached at three granularities. All of them use the same AtomRestraint trait underneath.

Per-target, all atoms — the most common case:

# use molpack::{InsideBoxRestraint, Target};
# let (pos, rad) = (&[[0.0; 3]][..], &[1.0][..]);
let target = Target::from_coords(pos, rad, 100)
    .with_restraint(InsideBoxRestraint::new([0.0; 3], [40.0; 3], [false; 3]));

Per-target, atom subset — restrict to specific atoms of every molecule copy (0-based, Rust convention — if you are porting from a Packmol .inp file, subtract 1):

# use molpack::{BelowPlaneRestraint, Target};
# let (pos, rad) = (&[[0.0; 3]][..], &[1.0][..]);
let target = Target::from_coords(pos, rad, 100)
    .with_atom_restraint(&[0, 1], BelowPlaneRestraint::new([0.0, 0.0, 1.0], 5.0));

Global, all targets — broadcast. Semantically equivalent to calling with_restraint on every target:

# use molpack::{InsideSphereRestraint, Molpack, Target};
# let (pos, rad) = (&[[0.0; 3]][..], &[1.0][..]);
# let t_a = Target::from_coords(pos, rad, 100);
# let t_b = Target::from_coords(pos, rad, 100);
let frame = Molpack::new()
    .with_global_restraint(InsideSphereRestraint::new([20.0; 3], 30.0))
    .pack(&[t_a, t_b], 200)?;
# Ok::<(), molpack::PackError>(())

The scope-equivalence law is formal: molpack.with_global_restraint(r) is implemented as for t in targets { t.with_restraint(r.clone()) }. There is no separate "global-restraint" storage path.

Screen output and handlers

Use the builder to enable LAMMPS-style screen output:

# use molpack::{InsideBoxRestraint, Molpack, MolpackLogLevel, Target};
# let (pos, rad) = (&[[0.0; 3]][..], &[1.0][..]);
# let target = Target::from_coords(pos, rad, 100).with_restraint(InsideBoxRestraint::new([0.0; 3], [40.0; 3], [false; 3]));
let mut packer = Molpack::new()
    .with_log_level(MolpackLogLevel::Progress)
    .with_log_frequency(10);
# let _ = packer.pack(&[target], 200);

Attach Handler implementations for trajectory output, custom observation, or early-stop logic. Built-ins:

# use molpack::{EarlyStopHandler, InsideBoxRestraint, Molpack, Target, XYZHandler};
# let (pos, rad) = (&[[0.0; 3]][..], &[1.0][..]);
# let target = Target::from_coords(pos, rad, 100).with_restraint(InsideBoxRestraint::new([0.0; 3], [40.0; 3], [false; 3]));
let mut packer = Molpack::new()
    .with_handler(XYZHandler::new("traj.xyz", /* every = */ 10))
    .with_handler(EarlyStopHandler::new(/* threshold = */ 1e-4));
# let _ = packer.pack(&[target], 200);

Write your own — see the extending module.

Relaxers (in-loop conformation sampling)

Flexible molecules benefit from torsion-MC relaxation between outer optimizer calls. Attach a Relaxer to a target:

# use molrs::system::atomistic::Atomistic;
# use molpack::{InsideSphereRestraint, Target, TorsionMcRelaxer};
# let (pos, rad) = (&[[0.0; 3]][..], &[1.0][..]);
# let graph = Atomistic::new();
let target = Target::from_coords(pos, rad, 1)  // relaxers require count == 1
    .with_restraint(InsideSphereRestraint::new([0.0; 3], 20.0))
    .with_relaxer(
        TorsionMcRelaxer::new(&graph)
            .with_temperature(0.5)
            .with_steps(20)
    );

Free versus periodic boundary

Free boundary is the default. There are two ways to declare PBC:

On the packer (fully periodic box, equivalent to the script's pbc keyword):

# use molpack::Molpack;
let packer = Molpack::new().with_periodic_box([0.0; 3], [30.0; 3]);

On an InsideBoxRestraint (per-axis control, e.g. for slab geometries):

# use molpack::{InsideBoxRestraint, Target};
# let (pos, rad) = (&[[0.0; 3]][..], &[1.0][..]);
// Fully-periodic (Packmol-style 3D PBC) via a restraint:
let target = Target::from_coords(pos, rad, 100).with_restraint(
    InsideBoxRestraint::new([0.0; 3], [30.0; 3], [true; 3]),
);

Slab geometries with only some axes wrapping (e.g. X/Y periodic slab, Z confined):

# use molpack::{InsideBoxRestraint, Target};
# let (pos, rad) = (&[[0.0; 3]][..], &[1.0][..]);
let target = Target::from_coords(pos, rad, 100).with_restraint(
    InsideBoxRestraint::new([0.0; 3], [30.0; 3], [true, true, false]),
);

When a pack() call resolves PBC from both a packer-level with_periodic_box and a restraint-level declaration, the two must match exactly (bounds + per-axis flags) or PackError::ConflictingPeriodicBoxes is returned. Zero-length axes return PackError::InvalidPBCBox.

Running the canonical examples

Five Packmol-equivalent workloads are checked in under examples/:

cargo run -p molcrafts-molpack --release --features io --example pack_mixture
cargo run -p molcrafts-molpack --release --features io --example pack_bilayer
cargo run -p molcrafts-molpack --release --features io --example pack_interface
cargo run -p molcrafts-molpack --release --features io --example pack_solvprotein
cargo run -p molcrafts-molpack --release --features io --example pack_spherical

Set MOLRS_PACK_EXAMPLE_PROGRESS=1 to enable the built-in ProgressHandler. Set MOLRS_PACK_EXAMPLE_XYZ=1 to dump a trajectory under out/.

Python bindings

A PyO3 binding ships under python/:

pip install molcrafts-molpack molcrafts-molrs
import molrs
from molpack import InsideBoxRestraint, Molpack, Target

frame = molrs.read_pdb("water.pdb")

water = (
    Target(frame, count=100)
    .with_name("water")
    .with_restraint(InsideBoxRestraint([0, 0, 0], [40, 40, 40]))
)
frame = Molpack().pack([water], max_loops=200)

The Python API mirrors the Rust builder surface one-for-one. The binding is I/O-free — use molcrafts-molrs (or any Frame-compatible loader) to read templates, and write the returned Frame back out with the same library. Use pack_with_report() if you need converged, fdist, or frest as Python properties.

The Python binding documentation is the Python section of this site, with sources under docs/python/; runnable examples are in python/examples/.

Next steps

  • Concepts — the abstractions in one place.
  • Architecture — system design, data flow, loops, hot path.
  • Extending — write your own Restraint / Region / Handler / Relaxer.