Tutorial 4 — Many copies in one run (better GPU utilization)¶
Goal: run many non-interacting copies of a single chain in one simulation, so a GPU (which is wasted on one ~100-bead chain) stays busy and you collect N independent trajectories per run for better sampling. Then split the multi-chain trajectory back into per-chain DCDs for normal analysis.
Time: the 10-copy demo finishes in a few seconds on a CPU; the real win is on a GPU.
Prerequisite: Tutorial 1.

The run — every copy evolves independently, giving 10 trajectories per run to split apart for analysis.
Regenerate after your run with
python ../_viz/render_cg.py --psf traj/traj_multi.psf --dcd traj/traj.dcd --out img --hero last --no-align --color chain.
Why do this?¶
A coarse-grained protein is tiny — P0CX28 is 106 CA beads. A modern GPU can
integrate tens of thousands of particles per step at almost the same wall-clock
cost as a few hundred, so simulating one small chain leaves the device almost
idle. Packing n_copies independent chains into a single System fills the GPU
and turns one run into n_copies trajectories — exactly what you want when a
study needs many trajectories for statistics (folding rates, free-energy
estimates, etc.).
The copies must be truly independent (no chain should feel another). This is guaranteed two ways:
bonded terms (bonds/angles/torsions) and constraints are duplicated within each copy with offset atom indices, and
every
CustomNonbondedForce(the Yukawa electrostatics and the structure-based contacts) is restricted to intra-copy interactions via OpenMMaddInteractionGroup(group_k, group_k).
A direct check confirms it: the potential energy of N copies equals exactly
N × the single-chain energy.
Files in this folder¶
File |
Role |
|---|---|
|
Single-chain input structure (106 residues). |
|
Calibrated single-domain nscale (2.5044), as in Tutorial 1 (syntax: Domain definition file). |
|
Config; note the |
|
The standard runner — replicates automatically when |
|
Post-process: split the multi-chain DCD into per-chain DCDs. |
How it works in the run script¶
Multi-copy support is controlled entirely by n_copies (default 1). The
standard run_simulation.py keeps its normal structure and just adds one
conditional right after building the single-chain model:
cgModel = topo.models.buildCoarseGrainModel(cfg.pdb_file, **cfg.build_kwargs())
if cfg.n_copies > 1:
system, topology, positions = topo.make_noninteracting_copies(
cgModel.system, cgModel.topology, cgModel.positions,
n_copies=cfg.n_copies, shift=cfg.copy_shift * unit.nanometer)
else:
system, topology, positions = cgModel.system, cgModel.topology, cgModel.positions
# ... everything below runs on (system, topology, positions) unchanged ...
So Tutorials 1–3 and this one use the same run_simulation.py; only
n_copies in md.ini differs.
make_noninteracting_copies is the package primitive that takes the single-chain
model and returns the replicated (system, topology, positions). It wraps
replicate_system_intra_only, replicate_topology and replicate_positions
(all on topo). Copy k is the contiguous atom block [k*n : (k+1)*n].
Step-by-step¶
1. Set n_copies in md.ini¶
n_copies = 10 ; independent chains in one run
copy_shift = 5.0 ; nm, x-offset between copies at the start
device = CPU ; <-- set to GPU on a CUDA machine; that's the point
2. Run¶
python run_simulation.py -f md.ini
The same standard runner is used as in Tutorials 1–3; because n_copies = 10
the run script’s if cfg.n_copies > 1 branch replicates the model via
topo.make_noninteracting_copies. Output goes to the traj/ run folder (DCD +
PSF only, no PDB): traj/traj.dcd (all chains), traj/traj.log, traj/traj.chk,
traj/traj.psf (single-chain topology) and traj/traj_multi.psf
(multi-chain topology, matches the combined DCD).
3. Split into per-chain trajectories¶
python split_chains.py -f md.ini
This writes traj/traj_{0..9}.dcd — one ordinary single-chain trajectory per
copy (each recentred per frame; pass center=False to
topo.split_chains to keep raw coordinates). It uses the package
routine topo.split_chains, which streams the combined DCD in chunks
so it scales to trajectories too large to fit in memory. Load each with the
single-chain PSF:
import MDAnalysis as mda
u = mda.Universe("traj/traj.psf", "traj/traj_0.dcd")
# RMSD, Q, Rg, ... per independent trajectory
Notes & tips¶
Pick
n_copiesfor your GPU. Start with 10–50 small chains and increase until the per-step time stops being flat (you’ve saturated the device). Memory is the eventual limit.Independent sampling. Each copy gets different random Langevin forces, so the
n_copiestrajectories are independent samples of the same ensemble — ideal for averaging observables and estimating error bars.copy_shiftonly sets the starting separation; since the chains never interact, its exact value does not affect the physics (it keeps the initial structure tidy and easy to view).No PBC here. With
pbc = nothe chains simply occupy different regions of space. If you enable PBC, make the box large enough to hold all shifted copies.
Try next¶
Switch
device = GPUand compare ns/day forn_copies = 1vs50— the per-copy cost drops sharply once the GPU is filled.Apply the same pattern to the multidomain system from Tutorial 2.