Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- take into account virtual sites/ dummy particles (atoms with mass equal to zero) when generating random velocities using the `RandomVelocitiesFrameExtractor`, also remove total linear momentum after generation

## [0.5.0] - 2026-04-09

### Added
Expand Down
26 changes: 22 additions & 4 deletions src/asyncmd/trajectory/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@
FileNotFoundError
If `struct_out` is given but does not exist.
"""
# TODO: make it possible to select a subset of atoms to write out

Check warning on line 440 in src/asyncmd/trajectory/convert.py

View workflow job for this annotation

GitHub Actions / pylint

W0511

TODO: make it possible to select a subset of atoms to write out
# and also for modification?
outfile = os.path.relpath(outfile)
if os.path.exists(outfile) and not overwrite:
Expand Down Expand Up @@ -589,25 +589,43 @@
"""
Draw random Maxwell-Boltzmann velocities for extracted frame.

Remove the total linear momentum of the system after generating velocities
and assign zero velocity to atoms with mass equal to zero.

Parameters
----------
universe : MDAnalysis.core.universe.Universe
The mdanalysis universe associated with the trajectory.
ts : MDAnalysis.coordinates.base.Timestep
The mdanalysis timestep of the frame to extract.
"""
# Note on units:
# m is in units of g / mol
# v should be in units of \AA / ps = 100 m / s
# which means m [10**-3 kg / mol] v**2 [10000 (m/s)**2]
# is in units of [ 10 kg m**s / (mol * s**2) ]
# so we use R = N_A * k_B [J / (mol * K) = kg m**2 / (s**2 * mol * K)]
# and add in a factor 10 to get 1/σ**2 = m / (k_B * T)
# in the correct units
# Note also: We take care of virtual sites (with mass equal to zero) and
# remove the total linear momentum of the system
zero_mass_mask = (universe.atoms.masses == 0.)
scale = np.empty((ts.n_atoms, 3), dtype=np.float64)
s1d = np.sqrt((self.T * constants.R * 0.1)
/ universe.atoms.masses
)
# set the masses that are zero to one when calculating the scale of the
# velocity distribution (to avoid division by zero)
masses_ = universe.atoms.masses.copy()
masses_[zero_mass_mask] = 1.
# sigma is the same for all 3 cartesian dimensions
s1d = np.sqrt((self.T * constants.R * 0.1) / masses_)
for i in range(3):
scale[:, i] = s1d
ts.velocities = self._rng.normal(loc=0, scale=scale)
v = self._rng.normal(loc=0, scale=scale)
# set velocities of virtual sites to zero (zero_mass_mask is 1d)
v[zero_mass_mask, :] = 0.
# Remove linear center-of-mass motion
p_com = np.sum(v * universe.atoms.masses[:, np.newaxis], axis=0)
v_com = p_com / np.sum(universe.atoms.masses)
# only subtract center-of-mass velocity for real atoms,
# the virtual sites should stay at zero velocity
v[np.logical_not(zero_mass_mask)] -= v_com
ts.velocities = v
2 changes: 2 additions & 0 deletions tests/trajectory/test_convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,8 @@ async def test_extract(
assert np.allclose(all_pos_original,
all_atoms_extracted.positions,
)
# check that all velocities are finite
assert np.all(np.isfinite(all_atoms_extracted.velocities))
# TODO: check for normal distribution of veloctities?!
# we could check for normality with scipy.stats.normaltest but
# that would only provide evidence against but not for the null
Expand Down
Loading