Source code for molsysmt.structure.get_center

import numpy as np
from smonitor import signal

from molsysmt import pyunitwizard as puw
from molsysmt._private import rust_backend as _kernels
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, is_iterable_of_iterables
from molsysmt._private.weighted_geometry import prepare_weights
from molsysmt.lib.structure._kernel_inputs import extract_coordinates_value_and_unit


class _CenterReducer(Reducer):
    """
    Reducer for get_center heavy path.
    Accumulates per-chunk center arrays and concatenates them on finalize.
    """

    def __init__(self, weights, atoms_per_group=None):
        self._weights = weights
        self._atoms_per_group = atoms_per_group
        self._n_groups = 1 if atoms_per_group is None else len(atoms_per_group)
        self._chunks = []

    def initialize(self, metadata):
        self._chunks = []

    def consume(self, chunk):
        coords = chunk["coordinates"]  # (chunk_size, n_atoms, 3), float64, read-only
        coords_w = np.array(coords, dtype=np.float64)  # writable copy for kernel
        if self._atoms_per_group is None:
            result = _kernels.get_center(coords_w, self._weights)
        else:
            result = _kernels.get_center_groups_of_atoms(
                coords_w, self._atoms_per_group, self._weights
            )
        self._chunks.append(result)

    def finalize(self):
        return np.concatenate(self._chunks, axis=0)

    # --- optional extensions ---

    def checkpoint(self):
        return {"chunks": [c.tolist() for c in self._chunks]}

    def restore(self, state):
        self._chunks = [np.array(c, dtype=np.float64) for c in state["chunks"]]

    def merge(self, other):
        self._chunks.extend(other._chunks)


# 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_center( molecular_system, selection="all", weights=None, structure_indices="all", syntax="MolSysMT", engine="MolSysMT", heavy_mode="auto", parallel=None, num_threads=None, skip_digestion=False, ): """ Computing centers (centroids or weighted centers) of atom selections. Parameters ---------- molecular_system : molecular system Molecular system in any supported MolSysMT format. selection : str, list, tuple, or numpy.ndarray, default='all' Selection string or boolean/integer array specifying elements. weights : numpy.ndarray, list, or tuple, default=None Atomic mass weights array for center calculation. 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 : str, default='MolSysMT' Backend used to perform the calculation. heavy_mode : {'auto', 'force', 'off'}, default='auto' Policy controlling the chunked path for memory-intensive calculations. parallel : bool or None, default=None Whether to use CPU parallelism, or `None` to use the configured default. num_threads : int or None, default=None Number of CPU worker threads, or `None` to use the configured default. skip_digestion : bool, default=False Whether to skip MolSysMT's internal argument digestion mechanism. Returns ------- quantity Centers as a PyUnitWizard quantity in length units. Raises ------ ArgumentError If the atom or frame selection is empty, or weights are non-finite, negative, or have zero total weight. ArgumentLengthError If the number of weights does not match the selected atoms. NotImplementedMethodError If an unsupported engine is requested. Notes ----- Chunked calculations use nm internally and return a quantity standardized to the session's length unit. Native iteration reads the existing structures domain and copies only selected coordinate blocks; accumulated output still occupies memory. Examples -------- >>> import molsysmt as msm >>> from molsysmt.structure.get_center import get_center >>> molsys = msm.convert(msm.systems['alanine dipeptide']['alanine_dipeptide.h5msm'], to_form='molsysmt.MolSys') >>> get_center(molsys, weights='masses').shape (1, 1, 3) .. versionadded:: 1.0.0 """ from molsysmt._private.structure_indices import ensure_nonempty_structure_indices from molsysmt.basic import get, select ensure_nonempty_structure_indices( structure_indices, caller="molsysmt.structure.get_center", ) if engine == "MolSysMT": atom_indices = select(molecular_system, selection=selection) if not is_iterable_of_iterables(atom_indices): n_atoms = ( len(np.atleast_1d(atom_indices)) if not is_all(atom_indices) else get(molecular_system, element="system", n_atoms=True) ) n_structures = get(molecular_system, element="system", n_structures=True) weights_arr = prepare_weights( weights, n_atoms, molecular_system=molecular_system, selection=atom_indices, syntax=syntax, caller="molsysmt.structure.get_center", ) from molsysmt._private.execution import ChunkedExecutor from molsysmt._private.execution.memory_policy import ( decide_mode, estimate_footprint, ) from molsysmt.basic import get_form form = get_form(molecular_system) footprint = estimate_footprint(n_atoms, n_structures) mode = decide_mode(footprint, heavy_mode) if mode == "heavy": reducer = _CenterReducer(weights=weights_arr) executor = ChunkedExecutor( molecular_system=molecular_system, form=form, operation="get_center", reducer=reducer, atom_indices=atom_indices, structure_indices=None if is_all(structure_indices) else structure_indices, heavy_mode=heavy_mode, attributes=["coordinates"], ) center_val = executor.execute() # (n_structures, 1, 3), float64, nm center = puw.quantity(center_val, "nm") else: coordinates = get( molecular_system, element="atom", selection=atom_indices, structure_indices=structure_indices, coordinates=True, ) coordinates, length_unit = extract_coordinates_value_and_unit( coordinates ) center = _kernels.get_center(coordinates, weights_arr) center = puw.quantity(center, length_unit) del coordinates, length_unit else: atoms_per_group = np.array( [len(group) for group in atom_indices], dtype=np.int64 ) groups_of_atoms = np.concatenate(atom_indices) n_atoms_flat = len(groups_of_atoms) n_structures = get(molecular_system, element="system", n_structures=True) if weights is not None and not isinstance(weights, str): if is_iterable_of_iterables(weights): weights = np.concatenate(weights) weights_arr = prepare_weights( weights, n_atoms_flat, molecular_system=molecular_system, selection=groups_of_atoms, syntax=syntax, group_sizes=atoms_per_group, caller="molsysmt.structure.get_center", ) from molsysmt._private.execution.memory_policy import ( decide_mode, estimate_footprint, ) from molsysmt.basic import get_form form = get_form(molecular_system) footprint = estimate_footprint(n_atoms_flat, n_structures) mode = decide_mode(footprint, heavy_mode) if mode == "heavy": reducer = _CenterReducer( weights=weights_arr, atoms_per_group=atoms_per_group ) from molsysmt._private.execution import ChunkedExecutor executor = ChunkedExecutor( molecular_system=molecular_system, form=form, operation="get_center", reducer=reducer, atom_indices=groups_of_atoms, structure_indices=None if is_all(structure_indices) else structure_indices, heavy_mode=heavy_mode, attributes=["coordinates"], ) center_val = executor.execute() # (n_structures, n_groups, 3) center = puw.quantity(center_val, "nm") else: coordinates = get( molecular_system, element="atom", selection=groups_of_atoms, structure_indices=structure_indices, coordinates=True, ) coordinates, length_unit = extract_coordinates_value_and_unit( coordinates ) center = _kernels.get_center_groups_of_atoms( coordinates, atoms_per_group, weights_arr ) center = puw.quantity(center, length_unit) del coordinates, length_unit, groups_of_atoms, weights_arr center = puw.standardize(center) return center else: raise NotImplementedMethodError()