API Reference¶
Import surface:
from molpack import (
# Core
Target, State, IntraResidual, StepReport,
# Engines — one per packing algorithm
GencanPack, CbmcGrow, LatticeGrow,
# Multi-stage composition
Pipeline,
# Typed values
Angle, Axis, CenteringMode,
# (geometric restraints are molrs regions — see "molrs regions as restraints")
# Collective (distribution-matching) restraints
GaussianPlane, GaussianPoint,
ExponentialPlane, ExponentialPoint,
TabulatedPlane, TabulatedPoint,
# Collective (pairwise separation) restraint
SelfSeparation,
# Chain-growth priors
TorsionPrior, AnglePrior,
# Script loader (`.inp`)
ScriptJob, load_script,
# Parallel evaluation
rayon_enabled, num_threads, init_thread_pool,
# Protocols
Callback, Restraint,
# Errors
PackError,
ConstraintsFailedError,
MaxIterationsError,
NoTargetsError,
EmptyMoleculeError,
InvalidPbcBoxError,
)
Angle¶
Angular quantity with explicit units at the call site.
Angle.from_degrees(30.0).radians # 0.5235...
Angle.from_radians(0.5).degrees # 28.6...
Angle.ZERO # identity rotation
Axis¶
Cartesian axis enum: Axis.X, Axis.Y, Axis.Z.
CenteringMode¶
Centering policy for target reference coords:
CenteringMode.AUTO— free targets centered, fixed kept in place (default).CenteringMode.CENTER— always center.CenteringMode.OFF— keep input coords unchanged.
Target¶
Molecule-type specification. Immutable — builder methods return new instances.
Constructor
frame— amolrs.core.Frame(molpy.Frameis the same class) with atom columns"x","y","z", and"element". Resolved zero-copy via its FFI capsule; a plain dict is not accepted.count— number of copies to produce.
Builders
.with_name(name: str)— display label..with_restraint(r)— attach a restraint to every atom (stackable). Accepts a geometric built-in, a collective (distribution-matching) restraint, or any duck-typedf/fgobject — see Restraints..with_atom_restraint(indices: Sequence[int], r)— 0-based indices..with_radius(radius: float)— packing radius for every atom (Packmolradius). RaisesValueErrorif not positive..with_atom_radius(indices, radius)— 0-based; all-atom explicit hydrogen keeps the default skip table and uses ~0.85 Å here..with_fscale(fscale: float)/.with_atom_fscale(indices, fscale)— overlap-penalty weight (Packmolfscale; default 1.0)..with_short_radius(short_radius: float)/.with_atom_short_radius(indices, short_radius)— second, shorter penalty radius (Packmolshort_radius)..with_short_radius_scale(scale: float)/.with_atom_short_radius_scale(indices, scale)— weight of the short-radius penalty..with_special_bonds(table: Sequence[float])— intramolecular skip weights. Slot 0 is 1-2; the last slot is the 1-N tail. Default[0, 0, 0, 1](depth 3). Empty / non-finite / outside[0, 1]raiseValueError. Fractional0.5is stored and refused atCbmcGrow.run. This is not a force-fieldspecial_bondstriple..with_hydrogens(indices: Sequence[int])— the atoms lattice growth treats as hydrogens (0-based), placed off their backbone neighbour rather than on a lattice site. Default: element symbolH. Pass[]for a coarse-grained model. An out-of-range index raisesValueError..with_perturb_budget(n: int)— per-target perturbation budget..with_centering(mode: CenteringMode)..with_rotation_bound(axis: Axis, center: Angle, half_width: Angle)..fixed_at(position: [x, y, z])— pin the target..with_orientation((ax, ay, az))— Euler tuple ofAngles; must followfixed_at..with_mass(amu: float)— per-copy total mass override forwith_densitywhen element symbols cannot provide one (CG beads, bare-coordinate targets).
Static constructor
Target.fixed_from(result: State)— wrap a previous run's whole output as one fixed obstacle, coordinates kept verbatim. The chaining primitive for staged packs: grow withCbmcGrow, then pack the next species around the frozen matrix withGencanPack. See Engines.
Properties
.name : str | None.natoms : int.count : int.elements : list[str].radii : list[float].special_bonds : list[float]— intramolecular skip table; default[0.0, 0.0, 0.0, 1.0]..is_fixed : bool
Engines¶
One engine class per packing algorithm; you pick the algorithm by picking
the engine. All three are immutable builders — every with_* returns a new
instance — and all three expose the same terminal verb:
CbmcGrow, LatticeGrow, and Pipeline require max_loops.
GencanPack.run leaves it optional and then uses Packmol's nloop,
200 * len(targets).
run() consumes the engine (one engine, one run): a second call on the
same object raises RuntimeError. Raises a typed PackError subclass on
packing failure.
Shared builders¶
Available on GencanPack and CbmcGrow:
.with_tolerance(t: float)— minimum pairwise distance (Å; default 2.0)..with_precision(p: float)— convergence threshold (default 0.01)..with_seed(seed: int)— deterministic RNG (default Packmol's 1234567)..with_periodic_box(min: [x,y,z], max: [x,y,z])— declare a fully-periodic box directly on the engine (Packmolpbc); see Periodic boundaries..with_density(rho: float)— size the box from a target mass density (g/cm³) instead of declaring it:run()resolves a cubic, fully periodic box holding the total mass of all targets atrho. Masses come from element symbols orTarget.with_mass; an unresolvable mass raisesValueError. Mutually exclusive with.with_periodic_box..with_parallel_eval(enabled: bool)— rayon-backed pair eval. RaisesRuntimeErrorif the wheel lacks therayonfeature (fail-fast)..with_log(level: str, nsteps: int = 1)— screen log.levelis"quiet"(the default),"summary","progress"(LAMMPS-style thermo lines) or"verbose".nstepsis how many loops pass between progress lines (at least 1; the default is every loop). Same words as Rustwith_log(level, nsteps)..with_callback(callback)— attach a customCallback(stackable)..with_global_restraint(r)— broadcast to every target (stackable).
GencanPack¶
Rigid-body placement driven by the three-phase GENCAN optimizer — the Packmol algorithm. Zero-arg constructor.
GENCAN-only builders, on top of the shared ones:
.with_restart(result: State)— continue on a previous run's placement solution (the explicit push-off chain): the free copies start exactly whereresultleft them,initial()is skipped, and the stall-perturbation moves stay off — molecules are pushed apart by rigid-body descent only. The cell travels with the seed; declaring a box, density, or cell on a seeded engine is a named error, and the run's free targets must match the seed's shape..with_inner_iterations(n: int)— GENCAN inner-loop cap (default 20)..with_init_passes(n: int)— init compaction passes (0 = auto)..with_init_box_half_size(h: float)— init placement bound (default 1000 Å)..with_perturb(fraction: float, random: bool = False, enabled: bool = True)— stall-perturbation heuristic: fraction re-sampled per stall, random vs worst-first selection, and the master switch..with_avoid_overlap(on: bool = True)— reject initial random placements overlapping a fixed molecule (Packmolavoid_overlap; default True)..with_early_stop(enabled: bool = True, *, threshold_pct: float = 10.0, patience: int = 10)— end a GENCAN phase, once radii are back at the user's (radscale == 1), when Packmol'sbestfhas improved by less thanthreshold_pctpercent overpatienceloops. On by default (10 % is Packmol's movebad threshold).enabled=Falseruns every phase out tomax_loops. A stopped final phase leavesState.convergedfalse.
CbmcGrow¶
Configurational-bias chain growth for dense melts — see the Chain growth guide. The torsion prior is the one mandatory constructor argument, because it decides the chain statistics of the product.
Growth-only builders, on top of the shared ones:
.with_trials(k: int)— torsion candidates per growth step (CBMCk; clamped ≥ 1, default 12)..with_selectivity(beta: float)— Rosenbluth inverse temperature applied to the soft-shell crowding penalty (clamped ≥ 0, default 2.0)..with_soft_shell(width: float)— soft-shell width in Å beyond the hard core: allowed but charged (clamped ≥ 0, default 1.0)..with_retract(steps: int)— steps retracted on a dead end; repeated dead ends retract exponentially deeper (clamped ≥ 1, default 10)..with_relax(every: int, window: int)— regrow each chain's lastwindowsteps everyeveryrounds;every=0disables (default(25, 6)). A regrown tail is only kept when its Rosenbluth weight does not degrade..with_soften_after(attempts: int)— cumulative dead ends on one chain before that chain's hard core softens by one rung (×0.97); a successful placement does not reset the count (clamped ≥ 1, default 50). Softening is per chain: a rung on one chain leaves every other chain at full contact..with_min_hard_scale(scale: float)— softening floor, clamped to[0, 1](default 0.8, the classic push-off bound)..with_angle_prior(prior: AnglePrior)— placement-angle prior (defaultAnglePrior.template())..with_serial(serial: bool = True)— grow one chain to completion before starting the next..with_void_bias(void_bias: bool = True)— seed chains in empty field cells (cavity seeding).
LatticeGrow¶
Diamond-lattice growth for melt density and above (see the Chain growth guide): the backbone grows as an on-lattice self-avoiding walk with RIS weights, and each backbone atom is then seated on the site the walk chose. A template supplies its topology, not its geometry: the backbone's bond lengths and angles are the lattice's — one step throughout, sized from the template's own mean backbone bond and moved a percent or two by how the cell divides — and its torsions are exactly the trans/gauche± the prior drew. Only hydrogens and side atoms keep the template's local geometry. Same mandatory torsion-prior constructor and shared builders; one extra knob:
.with_occupancy_guard(on: bool = True)— nearest-neighbour site exclusion (keeps non-bonded pairs at ≥ the 2nd-neighbour distance).
Trees (including branched) are accepted; a cycle raises named
RingTemplate ValueError; non-tetrahedral templates stay named
rejections.
Pipeline¶
Composes stage objects — GencanPack, CbmcGrow, and LatticeGrow
instances — into one multi-algorithm run: grow a chain, then hand it to
rigid-body descent, in one lifecycle and one State rather than two
separate run() calls. See Composing stages
for the full walkthrough.
Builders
.with_stage(stage: GencanPack | CbmcGrow | LatticeGrow)— append one more stage; returns a newPipeline.- The shared builders — same as
GencanPack:.with_tolerance,.with_precision,.with_seed,.with_periodic_box,.with_density,.with_parallel_eval,.with_log,.with_callback,.with_global_restraint. Set these on thePipeline, never on a stage object that goes into one — a stage carrying a non-default shared setting raisesValueErrornaming the stage and the setting.
Running
Each stage's own .with_callback(...) callbacks are adopted into the
pipeline's callback set and fire for every stage in the run, not only the
one they were attached to. Raises ValueError for an empty pipeline, a
stage-ordering error, or a stage carrying a non-default shared setting
(each naming the offending stage); raises TypeError, listing the three
supported engines, for any object passed to Pipeline([...]) or
.with_stage(x) that is not a GencanPack, CbmcGrow, or LatticeGrow.
Chaining two engines¶
An engine's own run() never mixes algorithms, and there is no hidden
fallback between them. Outside Pipeline (above), stage it in user code
instead, in one of two shapes:
# Push-off: continue the SAME free targets on the grown state.
grown = CbmcGrow(prior).with_density(0.9).run([chain], max_loops=60)
pushed = GencanPack().with_restart(grown).with_seed(7).run([chain], max_loops=60)
# Fixed matrix: freeze the first result, pack new species around it.
full = GencanPack().run([Target.fixed_from(grown), solvent], max_loops=200)
State¶
Frozen outcome of one run(). Diagnostics (frame, fdist, frest,
converged, degraded, intra) live on this object; pass the same
object to GencanPack.with_restart or Target.fixed_from to continue.
Properties
.positions : ndarray (N, 3) float64.frame : molrs.core.Frame— topology-complete frame (periodic box stamped if one was declared)..elements : list[str].natoms : int.converged : bool.fdist : float.frest : float.degraded : int— how many molecules were placed below the growth solver's own guarantee, one count per demotion.LatticeGrowdemotes a chain when the walk cannot keep the occupancy guard (no two non-bonded atoms closer than the 2nd lattice neighbour): the chain goes in with site self-avoidance only, or as a forced zigzag.CbmcGrowcounts each hard-core softening rung the same way.0on the GENCAN path, which promises nothing constructively. A non-zero count is the honest reading of a crowded box — those molecules carry the close contactsfdistreports, andconvergedis false while it stands..intra : IntraResidual— same-copy scored vs exempted minima (Å, minimum image). Forwards the assembled residual; does not recompute from positions.
IntraResidual¶
Nested diagnostic on State. Empty class is +∞.
.scored : float— minimum same-copy pair distance among pairs the target's skip table scores (Å)..exempted : float— minimum same-copy pair distance among pairs the table exempts (Å).
Chain-growth priors¶
Inputs to CbmcGrow: the torsion prior is its mandatory
constructor argument, the angle prior an optional builder. Both are
geometric data — see the Chain growth guide for when
and why each matters.
TorsionPrior¶
Geometric data only — never a force field. Static constructors:
TorsionPrior.uniform()— uniform on (−π, π]. Freely-rotating-chain statistics (C∞ = 2.0); negative control only, quantitatively wrong for melts.TorsionPrior.template(kappa: float)— von-Mises-like spread of concentrationkappaaround the template's own torsion values.TorsionPrior.states(states: list[tuple[float, float]])— RIS-style discrete(angle_rad, weight)states; weights are normalized at use.TorsionPrior.three_state_from_c_inf(c_inf: float, theta_rad: float)— trans/gauche± prior whose trans fraction is solved from a target characteristic ratio. PEO with tetrahedral backbone angles:TorsionPrior.three_state_from_c_inf(5.5, 1.9106).
AnglePrior¶
AnglePrior.template()— copy bond angles verbatim from the template (the all-atom default).AnglePrior.wlc(kappa: float)— discrete worm-like chain with tiltkappa(CG persistence control).AnglePrior.wlc_from_c_inf(c_inf: float)— WLC tilt calibrated from a target characteristic ratio; assumes uniform torsions. Kremer–Grest melts:AnglePrior.wlc_from_c_inf(1.76).
StepReport¶
Read-only snapshot passed to Callback.on_step.
step.loop_idx # outer-loop iteration
step.max_loops
step.phase # phase index
step.total_phases
step.molecule_type # int | None
step.fdist
step.frest
step.improvement_pct
step.radscale
step.precision
step.stage # StageProgress — which packing algorithm emitted this step
PackSystemView¶
The second argument of Callback.on_step: a guard over the live packing
system, valid only inside that call (any access afterwards raises
RuntimeError). Properties copy on access.
StageProgress¶
Read-only triple identifying the stage a step belongs to. Load-bearing
inside a multi-stage Pipeline; present, with index = 0 and
total = 1, on every single-engine run too.
.index : int— 0-based position of this stage in the run; monotonic across a multi-stagePipeline..total : int— number of stages in the run..name : str— the stage's own name:"gencan","growth", or"lattice".
Restraints¶
All restraint classes are immutable. Two families, both attached with
target.with_restraint(r) (or the engine's with_global_restraint(r)):
molrs regions lifted to a per-atom penalty (below) and collective
distribution-matching restraints (next section).
molrs regions as restraints¶
Any molrs region object attaches as a restraint: molrs.core.Sphere, Cuboid,
Parallelepiped, HalfSpace, Cylinder, Ellipsoid, Polyhedron,
SphereUnion, or any & / | / ~ composition of them. The region
crosses the wheel boundary as a molrs.RegionRef capsule (both wheels on
one molrs minor line) and is lifted to scale · max(0, distance)² — a
soft quadratic penalty on the atom centre that is zero inside the
region and on its boundary. molpack defines no geometric restraint class;
the shapes, their constructors and their contains / distance /
bounds queries are documented with molrs. Solver split (the same for
every region, not inferred from the shape):
GencanPack— soft exterior penalty.CbmcGrow— hard reject on propose;force_placemay sit outside.LatticeGrow— sites outside the mesh are blocked (Region ∩ lattice), and the backbone atoms are those sites, so the mask's guarantee is the molecule's. Hydrogens and side atoms hang off the backbone with the template's local geometry and can reach about a bond length past the surface; author the mesh with that clearance in it if the wall has to hold.
The region answers its own questions for an (n, 3) array: contains
returns (n,) bool; distance returns (n,) Å, negative inside. Together
they say what the packer was told to enforce:
cavity = molrs.core.Polyhedron(molrs.io.read_stl("dendrite.stl"))
depth = cavity.distance(state.positions)
print(f"{(depth > 0).sum()} atoms outside, worst {depth.max():.2f} Å")
A restraint is satisfied to the run's precision, not exactly. frest is the
largest per-atom penalty 0.01 · d², so frest < precision means
d < 10·√precision: the default precision=1e-2 calls a run converged with an
atom 1 Å outside, 1e-4 with 0.1 Å. Tighten with_precision when the wall is
the point — and remember the mesh, not the solver, is where clearance belongs.
Collective (distribution-matching) restraints¶
Attached the same way (target.with_restraint(r)), but their f/fg
see every copy of the target at once and drive the species' spatial
distribution toward a target profile via a squared 1-D Wasserstein
(sorted-CDF) penalty. The reaction coordinate is either signed distance
to a plane (\(\xi = \mathbf{x}\cdot\hat{\mathbf{n}} - \text{offset}\)) or
radial distance to a point (\(\xi = \lVert\mathbf{x} - \text{center}\rVert\)).
| Class | Constructor | Target profile |
|---|---|---|
GaussianPlane |
(normal, offset, strength, mu, sigma) |
Gaussian \(N(\mu, \sigma)\) slab |
GaussianPoint |
(center, strength, mu, sigma) |
Gaussian shell (radius mu, thickness sigma) |
ExponentialPlane |
(normal, offset, strength, lambda_) |
diffuse layer \(\propto e^{-\xi/\lambda}\), \(\xi \ge 0\) |
ExponentialPoint |
(center, strength, lambda_) |
radial atmosphere \(\propto e^{-\xi/\lambda}\) |
TabulatedPlane |
(normal, offset, strength, xs, rho) |
arbitrary planar prior on grid (xs, rho) |
TabulatedPoint |
(center, strength, xs, rho) |
arbitrary radial prior on grid (xs, rho) |
strength is the overall penalty multiplier \(\lambda\). sigma /
lambda_ must be > 0; tabulated xs must be strictly ascending
(≥ 2 points) with non-negative rho of positive total mass. Invalid
arguments raise ValueError at construction.
Collective (pairwise separation) restraint¶
| Class | Constructor | Meaning |
|---|---|---|
SelfSeparation |
(d_min, strength=1.0) |
no two copies of this species closer than d_min, centre to centre |
The anti-clustering restraint. A distribution target says where copies should be; this says how close two of them may come. The pair term keeps atoms from overlapping and then stops caring, and it does not distinguish two copies of one species from a copy of each of two — so without this a species may pile its copies into one corner.
The penalty is silent above d_min and grows as \((d_{min} - D)^2\) below it,
where \(D\) is the minimum-image distance between two copies' geometric
centroids. It is quadratic in the length by which the bound is missed — the
same shape as a geometric restraint's penalty — so strength=1.0 weights a
shortfall like a region lift weights an equal overshoot, and the
convergence threshold precision means the same thing for both.
Centroids, not atoms: for a compact molecule the centroid stands in for the whole, but two long chains can interdigitate with distant centroids. The restraint states what it measures.
Cost is linear in count, not quadratic: the centres are binned into a cell
grid and only nearby pairs are examined, so the bound stays affordable on a
melt-sized species.
Feasibility is not checked, and does not need to be. A bound is either met or
not, so unlike the distribution restraints this one counts toward the restraint
verdict frest and gates convergence: if count copies cannot fit at d_min,
the run does not converge and says so — it is never silently relaxed. d_min
and strength must be > 0; otherwise ValueError.
import molrs
from molpack import SelfSeparation, Target
ions = (
Target(frame, count=27)
.with_restraint(molrs.core.Cuboid([0, 0, 0], [40, 40, 40]))
.with_restraint(SelfSeparation(10.0))
)
Script loader¶
load_script(path, *, loader=None) -> ScriptJob¶
Parse and lower a Packmol-compatible .inp script. Template files are
read on the Python side (defaulting to the molrs.io reader of the format the
script's filetype or the file name names: read_pdb, read_xyz, …), so the
wheel stays free of molrs-io. Pass loader
— a callable (path, filetype) -> molrs.core.Frame — to plug in another
loader (mdtraj, ASE, …).
ScriptJob¶
Bundle returned by load_script. Access fields by attribute or
tuple-unpack it:
.packer : GencanPack— pre-configured with the script'stolerance,seed,pbcbox orcell, andavoid_overlap..inpscripts always lower to the rigid-body engine..targets : list[Target].output : pathlib.Path— resolved output path..nloop : int— outer-loop cap (nloopkeyword; default200 * ntype).
Parallel evaluation¶
The parallel evaluator runs on rayon's process-global thread pool, built once per process and not resizable afterwards.
rayon_enabled() -> bool— was the wheel built with therayonfeature?num_threads() -> int— worker count the pool will use (1 on a serial build).init_thread_pool(n: int)— pin the pool size; must be called before the first pack. RaisesRuntimeErrorwithout therayonfeature or if the pool was already initialized, andValueErrorifn == 0. For a scaling sweep, set the count once per process (or viaRAYON_NUM_THREADS) and launch one process per data point.
Duck-type protocols¶
Restraint¶
class Restraint(Protocol):
def f(self, x: tuple[float, float, float], scale: float, scale2: float) -> float: ...
def fg(
self, x: tuple[float, float, float], scale: float, scale2: float,
) -> tuple[float, tuple[float, float, float]]: ...
Callback¶
class Callback(Protocol):
def on_start(self, ntotat: int, ntotmol: int) -> None: ...
def on_step(self, step: StepReport, sys: PackSystemView) -> bool | None: ... # True → stop
def on_finish(self) -> None: ...
All Callback methods are optional — missing ones are silently skipped.
Exceptions¶
Typed hierarchy rooted at PackError (itself a RuntimeError
subclass). Catch the base to handle any packing failure uniformly.
PackError— base.ConstraintsFailedError— solver could not satisfy restraints even without distance tolerances.MaxIterationsError— ran out of outer loops.NoTargetsError— empty target list.EmptyMoleculeError— a target has zero atoms.InvalidPbcBoxError— periodic box has a non-positive extent.
ValueError / TypeError still surface on Python-side invariants
(bad atom indices, wrong restraint object, etc.). Growth and density
declarations are input contracts, so their failures also raise
ValueError: a target that cannot be grown (no bond graph, < 3 atoms,
fixed placement, no box), a with_density fighting an explicit box, or
a mass the element symbols cannot resolve.