A PyTorch implementation of the Müller-Brown potential energy surface with BAOAB Langevin dynamics.
This package implements the Müller-Brown potential, a 2D model potential energy surface widely used for testing molecular dynamics algorithms and studying rare events in chemical systems.
See Implementing the Müller-Brown Potential in PyTorch for a detailed blog post explaining the implementation and usage.
The Müller-Brown potential is defined as a sum of four Gaussian-like terms:
This creates a complex energy landscape with:
- 3 local minima at different energy levels
- 2 saddle points connecting the minima
- Energy barriers that control transition rates between states
This makes it ideal for studying rare event transitions, temperature effects on barrier crossing, and molecular dynamics algorithm performance.
Install dependencies using uv:
uv syncThe easiest way to get started is with the demo mode:
uv run python main.py --mode demoThis shows the potential's key features: 3 minima, 2 saddle points, and demonstrates energy/force calculations.
# Quick demo - see potential features
uv run python main.py --mode demo
# Single simulation with visualization
uv run python main.py --mode single --n-particles 1 --n-steps 50000 --save-plots
# Single simulation with animated trajectory
uv run python main.py --mode single --n-particles 1 --n-steps 50000 --save-plots --save-animation
# Multiple simulations for statistics
uv run python main.py --mode batch --n-simulations 10 --n-particles 2 --save-plots
# List available artifact directories
uv run python main.py --mode list
# Regenerate plots from existing data
uv run python main.py --mode plot --artifact-dir artifacts/20250824_184610import torch
from muller_brown import MuellerBrownPotential, LangevinSimulator, MuellerBrownVisualizer
# Create the potential
potential = MuellerBrownPotential(dtype=torch.float64)
# Set up the simulator
simulator = LangevinSimulator(
potential=potential,
temperature=15.0,
friction=1.0,
dt=0.01
)
# Run simulation
results = simulator.simulate(
initial_positions=torch.tensor([[0.0, 0.0]], dtype=torch.float64),
n_steps=10000,
save_every=10
)
# Visualize results
visualizer = MuellerBrownVisualizer(potential)
fig, ax = visualizer.plot_potential_surface()Run the example with:
uv run python example.pyThe example samples 100 paths and extracts full paths whose final saved
positions satisfy distance_to_MB <= 0.5 * distance_to_MA, using Euclidean
distance. Extracted observables, original path indices, an endpoint-distance
CSV, and plots are saved in mb_endpoint_selection inside the artifact folder.
To extract paths from an existing ensemble without rerunning the simulation:
uv run python extract_paths.py artifacts/<run>/trajectory_<timestamp>.h5Use --distance-ratio 0.25 for a stricter cutoff or --output-dir <path> to
choose a separate output folder. Selection uses the final saved position;
visiting MB earlier in a path does not qualify it.
The barrier out of MA is approximately 106 in this potential's energy units.
With kB=1 and T=3, this is about 35 thermal energy units, so ordinary
MA-started rollouts are unlikely to yield transitions. The Boltzmann factor
exp(-106/3) is an activation factor, not a crossing probability or rate.
Use weighted-ensemble sampling to allocate more walkers along the transition region while retaining the original temperature-3 BAOAB dynamics:
uv run python sample_transition_paths.py --temperature 3 --seed 42 \
--output-dir artifacts/we_temp3_seed42The sampler checks both basins at every integration step and saves each
reactive segment from its last frame inside A to its first frame inside B.
A and B are radius-0.15 disks around MA and MB; configure their radii with
--source-radius and --target-radius. By default, resampling uses total-energy
bins, separately within each nearest-minimum region. This protects the rare
energetic walkers needed for low-friction escape and the walkers entering the
intermediate well. Alternatively, --binning arclength uses bins along the
minima/saddle polyline. Bins affect allocation, never the forces.
Systematic resampling preserves balanced bins to reduce loss of diversity.
Positions and velocities are copied together, then clones receive independent
thermostat noise. B is absorbing, without recycling.
The initial state is the rounded MA minimum with zero velocity, matching the
example's initial-state convention. This samples finite-horizon first
passages from that initial state, not automatically the stationary
equilibrium transition-path ensemble. The default run uses 2,000 resampling
iterations, 50 integration steps per iteration, dt=0.002,
and 32 walkers per occupied bin (40 energy bins per nearest-minimum region).
Its horizon is 200 simulation time units.
By default, at most 10,000 paths are retained in a weighted reservoir. If more
events occur, the output samples their weighted empirical distribution with
replacement and redistributes the total event weight equally among retained
records. This bounds storage and adds sampling variance. --max-paths 0 keeps
all original events and weights, with larger memory and storage requirements.
Outputs include:
transition_paths.h5: variable-length position/velocity arrays underpaths/<index>, per-path timestep and start/end step, and absolute probability contributions inweights(also attached to each path).sampling_history.csv: active/absorbed probability mass and walker counts.summary.json: dynamics, basin definitions, initial state, and sampling metadata.transition_paths.png: a weighted sample of the recorded crossings.
Use the path weights. For a path statistic f, estimate its conditional
mean as sum(w * f) / sum(w). Do not treat the raw path count as a transition
probability or the cloned paths as independent samples. The absorbed weight
estimates first-passage probability within the specified horizon, not a
stationary transition rate. Establish convergence across independent seeds,
bin/iteration settings, basin definitions, and smaller timesteps before using
the output as a ground-truth reference. The saved weight-concentration count
ignores genealogical correlations and is not an independent-sample count.
After output resampling, use weights/the per-path weight, rather than
original_event_weight. Repeated event_id values identify repeated selections
of one event; different event IDs can still share ancestral trajectory segments.
For memory-bounded analysis, the library sampler also accepts an on_transition
callback, which consumes paths as they arrive instead of keeping them all in RAM.
Method reference: Zhang, Jasnow, and Zuckerman, weighted-ensemble path sampling.
The repository includes a utility to manage simulation artifacts:
# List all artifact directories
uv run python manage_artifacts.py list
# Clean artifacts older than 7 days (dry run)
uv run python manage_artifacts.py clean
# Actually delete old artifacts
uv run python manage_artifacts.py clean --deleteThe test suite uses pytest:
uv run --extra dev pytest # full suite
uv run --extra dev pytest -m "not statistical" # fast deterministic tests onlyIt has two tiers:
- Deterministic tests check exact properties: the potential's critical points (Hessian signatures), forces against autograd, energy conservation in the frictionless (velocity-Verlet) limit, seed reproducibility, HDF5 round-trips, and array shape/dtype invariants.
- Statistical tests (marked
statistical) check that the thermostat samples the canonical distribution: equipartition, harmonic-oscillator distributions, the Müller-Brown mean energy against a grid-integrated Boltzmann average, the free-particle velocity autocorrelation, and the timestep-convergence order. They are seeded, with tolerances set above the sampling error.
Two standalone scripts produce validation plots:
uv run python verify_langevin.py # sampled vs analytic harmonic-oscillator distributions
uv run python convergence_study.py # kinetic-temperature bias vs timestepNote on accuracy. BAOAB samples configurational averages accurately (exactly for a harmonic oscillator), but the kinetic temperature carries an O(dt²) bias that grows with curvature — about 4% on the Müller-Brown surface at dt=0.01. It shrinks as the timestep is reduced;
convergence_study.pyquantifies this.
Simulations save all observables in HDF5 format:
- Positions: (n_steps, n_particles, 2) - x,y coordinates over time
- Velocities: (n_steps, n_particles, 2) - velocity components over time
- Forces: (n_steps, n_particles, 2) - force components over time
- Potential Energy: (n_steps, n_particles) - potential energy over time
- Metadata: Temperature, friction, timestep, and simulation parameters
muller_brown/
├── src/muller_brown/ # Core package
│ ├── potential.py # Müller-Brown potential (torch.compile force)
│ ├── simulation.py # Langevin dynamics simulator
│ ├── visualization.py # Plotting and animation suite
│ ├── io.py # HDF5 data I/O operations
│ ├── data.py # Data processing utilities
│ ├── analysis.py # Statistical analysis functions
│ ├── config.py # Experiment configuration
│ └── constants.py # Physical and computational constants
├── tests/ # pytest suite (deterministic + statistical)
├── main.py # Command-line interface
├── benchmark.py # Performance benchmarking
├── example.py # Simple usage example
├── verify_langevin.py # BAOAB sampling vs analytic harmonic oscillator
├── convergence_study.py # Timestep-convergence study
├── manage_artifacts.py # Artifact management utility
├── pyproject.toml # Project dependencies and metadata
├── LICENSE # MIT License
└── README.md # This file
torch.compile-accelerated force evaluation- Automatic gradient computation for forces
- Built-in critical point locations (3 minima, 2 saddle points)
- BAOAB integration (Leimkuhler & Matthews, 2013) for accurate canonical sampling
- Tracks all observables (positions, velocities, forces, energies)
- Configurable temperature, friction, and time step
- Distribution plots for every observable (position, velocity, force, energy)
- Time-series plots for all observables
- Log-scale visualization for rare events
- Automatic critical point annotation
Design choices that affect performance:
- Compiled forces:
torch.compile(shape-dynamic) on the force path - Device-agnostic: runs on CPU or CUDA; the included benchmark measures CPU
- Storage: HDF5 with gzip compression for large trajectories
- Numerical precision: double precision (
float64) by default - Memory: configurable save frequency for long simulations
Each step is vectorized over particles, so for small particle counts the wall
time is dominated by fixed per-step overhead (the force call and noise draw): a
1-particle step and a 100-particle step take roughly the same time (~40 vs
~50 µs). For sampling throughput, prefer an ensemble (--n-particles 100+) over
many separate single-particle runs — that is ~100× the sampling for ~25% more
wall time.
- Temperature effects: Lower temperatures (T ~ 5-10) emphasize rare events; higher temperatures (T ~ 20-30) increase barrier crossing
- Equilibration: Use
--n-transientto remove initial transient behavior - Statistics: Run batch simulations for ensemble averages and error estimates
- Long trajectories: For rare events, use 10⁶ steps or more with appropriate save frequency
- Multiple particles: Study collective behavior with
--n-particles > 1
Original Müller-Brown Potential:
- Müller, K., & Brown, L. D. (1979). Location of saddle points and minimum energy paths by a constrained simplex optimization procedure. Theoretica Chimica Acta, 53, 75-93.
Implementation Based On:
- LED-Molecular Repository - Original implementation of the Müller-Brown potential
Related Work:
- Vlachas, P. R., Zavadlav, J., Praprotnik, M., & Koumoutsakos, P. (2022). Accelerated simulations of molecular systems through learning of effective dynamics. Journal of Chemical Theory and Computation, 18(1), 538-549.
- Vlachas, P. R., Arampatzis, G., Uhler, C., & Koumoutsakos, P. (2022). Multiscale simulations of complex systems by learning their effective dynamics. Nature Machine Intelligence.
This project is available under the MIT License.