import numpy as np
from smonitor import signal
from molsysmt import pyunitwizard as puw
from molsysmt._private.argdigest import arg_digest
from molsysmt._private.execution import Reducer
from molsysmt._private.smonitor import NotImplementedMethodError
from molsysmt._private.variables import is_all
from molsysmt.lib.structure._kernel_inputs import extract_coordinates_value_and_unit
class _RMSFReducer(Reducer):
"""Combine chunk fluctuations with the parallel variance formula."""
def initialize(self, metadata):
self._count = 0
self._mean = None
self._m2 = None
def consume(self, chunk):
coordinates = np.asarray(chunk["coordinates"], dtype=np.float64)
chunk_count = coordinates.shape[0]
if chunk_count == 0:
return
chunk_mean = np.mean(coordinates, axis=0)
deviations = coordinates - chunk_mean
chunk_m2 = np.sum(deviations * deviations, axis=(0, 2))
if self._count == 0:
self._count = chunk_count
self._mean = chunk_mean
self._m2 = chunk_m2
return
total = self._count + chunk_count
delta = chunk_mean - self._mean
self._m2 += chunk_m2 + np.sum(delta * delta, axis=1) * (
self._count * chunk_count / total
)
self._mean += delta * (chunk_count / total)
self._count = total
def finalize(self):
return np.sqrt(self._m2 / self._count)
def checkpoint(self):
return {
"count": self._count,
"mean": None if self._mean is None else self._mean.tolist(),
"m2": None if self._m2 is None else self._m2.tolist(),
}
def restore(self, state):
self._count = state["count"]
self._mean = (
None
if state["mean"] is None
else np.asarray(state["mean"], dtype=np.float64)
)
self._m2 = (
None if state["m2"] is None else np.asarray(state["m2"], dtype=np.float64)
)
def merge(self, other):
if other._count == 0:
return
if self._count == 0:
self._count = other._count
self._mean = other._mean.copy()
self._m2 = other._m2.copy()
return
total = self._count + other._count
delta = other._mean - self._mean
self._m2 += other._m2 + np.sum(delta * delta, axis=1) * (
self._count * other._count / total
)
self._mean += delta * (other._count / total)
self._count = total
# Keep this decorator import beside the public function below.
from molsysmt.configure import with_configure_overrides # noqa: E402
[docs]
@signal(tags=["api", "structure"])
@arg_digest()
@with_configure_overrides
def get_rmsf(
molecular_system,
selection='atom_type!="H"',
structure_indices="all",
syntax="MolSysMT",
engine="MolSysMT",
heavy_mode="auto",
parallel=None,
num_threads=None,
skip_digestion=False,
):
r"""
Computing root-mean-square fluctuations per atom over a set of structures.
The RMSF of atom *i* is defined as:
.. math::
\mathrm{RMSF}_i = \sqrt{\frac{1}{T} \sum_{t=1}^{T}
\left| \mathbf{r}_i(t) - \langle \mathbf{r}_i \rangle \right|^2}
where :math:`\langle \mathbf{r}_i \rangle` is the time-averaged position of atom *i*
and *T* is the number of structures.
Parameters
----------
molecular_system : molecular system
Molecular system in any supported MolSysMT format.
selection : str, list, tuple, or numpy.ndarray, default='atom_type!="H"'
Selection string or boolean/integer array specifying elements.
structure_indices : int, list, tuple, or numpy.ndarray, default='all'
Structure indices (0-based) to include or process.
syntax : str, default='MolSysMT'
Selection syntax used to evaluate `selection` (e.g., 'MolSysMT', 'MDTraj').
engine : object, default='MolSysMT'
Argument engine.
heavy_mode : object, default='auto'
Argument heavy_mode.
parallel : object, default=None
Argument parallel.
num_threads : object, default=None
Argument num_threads.
skip_digestion : bool, default=False
Whether to skip MolSysMT's internal argument digestion mechanism.
Returns
-------
quantity
RMSF per selected atom as a PyUnitWizard quantity in length units.
Shape: (n_atoms,).
Raises
------
ArgumentError
If the atom or frame selection is empty.
NotImplementedMethodError
If an unsupported engine is requested.
Notes
-----
All structures must be pre-aligned to a common reference frame before calling
this function if positional fluctuations relative to a reference are intended.
Use :func:`molsysmt.structure.least_rmsd_align` to align first.
Examples
--------
>>> import molsysmt as msm
>>> molsys = msm.convert(msm.systems['alanine dipeptide']['alanine_dipeptide.h5msm'], to_form='molsysmt.MolSys')
>>> msm.structure.get_rmsf(molsys, selection='all').shape[0] == msm.get(molsys, n_atoms=True)
True
.. versionadded:: 1.0.0
"""
from molsysmt._private.structure_indices import ensure_nonempty_structure_indices
ensure_nonempty_structure_indices(
structure_indices,
caller="molsysmt.structure.get_rmsf",
)
if engine == "MolSysMT":
from molsysmt._private import rust_backend as _kernels
from molsysmt.basic import get, select
atom_indices = select(molecular_system, selection=selection, syntax=syntax)
n_atoms = len(np.atleast_1d(atom_indices))
if n_atoms == 0:
from molsysmt._private.smonitor import ArgumentError
raise ArgumentError(
"selection",
value=selection,
caller="molsysmt.structure.get_rmsf",
message="The atom selection must contain at least one atom.",
)
n_structures = get(molecular_system, element="system", n_structures=True)
from molsysmt._private.execution.memory_policy import (
decide_mode,
estimate_footprint,
)
from molsysmt.basic import get_form
form = get_form(molecular_system)
mode = decide_mode(estimate_footprint(n_atoms, n_structures), heavy_mode)
if mode == "heavy":
from molsysmt._private.execution import ChunkedExecutor
executor = ChunkedExecutor(
molecular_system=molecular_system,
form=form,
operation="get_rmsf",
reducer=_RMSFReducer(),
atom_indices=atom_indices,
structure_indices=None
if is_all(structure_indices)
else structure_indices,
heavy_mode=heavy_mode,
attributes=["coordinates"],
)
rmsf_val = executor.execute()
length_unit = puw.get_standard_units(dimensionality={"[L]": 1})
return puw.quantity(rmsf_val, length_unit)
coordinates = get(
molecular_system,
element="atom",
selection=atom_indices,
structure_indices=structure_indices,
syntax=syntax,
coordinates=True,
)
coordinates, length_unit = extract_coordinates_value_and_unit(coordinates)
rmsf_val = _kernels.get_rmsf(coordinates)
rmsf = puw.quantity(rmsf_val, length_unit)
rmsf = puw.standardize(rmsf)
del coordinates, length_unit
return rmsf
else:
raise NotImplementedMethodError()