# Copyright (c) 2026, PyRETIS Development Team.
# Distributed under the LGPLv2.1+ License. See LICENSE for more info.
"""System helpers that don't involve the forcefield.
Pure-lookup helpers that work on both the harness System and the
snapshot System. They cover the small bag of legacy
``system.method()`` calls (dimensionality, Boltzmann lookup, box
update) that aren't worth attaching to either System class -- the
free-function form means we don't have to grow the snapshot's API
just to satisfy A3.2c's "delete the harness" goal.
"""
import logging
import numpy as np
from pyretis.core.box import create_box
from pyretis.core.particlefunctions import calculate_kinetic_temperature
from pyretis.core.random_gen import create_random_generator
from pyretis.core.units import CONSTANTS
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
logger.addHandler(logging.NullHandler())
__all__ = [
'get_system_dim',
'get_boltzmann',
'update_system_box',
'generate_system_velocities',
'calculate_system_temperature',
]
[docs]def get_system_dim(system):
"""Return the spatial dimensionality of the system.
Works for both the harness System (``system.box`` is a
:class:`.Box` with a ``.dim`` attribute) and the snapshot System
(``system.box`` is a 3x3 ``np.ndarray``).
Parameters
----------
system : object like :class:`.System`
Returns
-------
int
Spatial dimensionality (1, 2, or 3). Falls back to ``1`` when
no box is attached -- matches the legacy
:meth:`.System.get_dim` fallback.
"""
box = getattr(system, 'box', None)
if box is None:
logger.warning(
'Box dimensions are not set. Setting dimensions to "1"'
)
return 1
if hasattr(box, 'dim'):
return box.dim
if isinstance(box, np.ndarray):
return box.shape[0]
# Last-resort: try ``len``.
return len(box)
[docs]def get_boltzmann(system):
"""Return the Boltzmann constant in the system's unit system.
Parameters
----------
system : object like :class:`.System`
Must expose a ``units`` attribute (string key into
:data:`pyretis.core.units.CONSTANTS`).
Returns
-------
float
Boltzmann constant for ``system.units``.
"""
return CONSTANTS['kB'][system.units]
[docs]def update_system_box(system, length):
"""Update the system box, creating one if needed.
Mirrors the legacy ``System.update_box(length)`` method on the
harness System; on the snapshot side it overwrites the 3x3
matrix.
Parameters
----------
system : object like :class:`.System`
length : numpy.ndarray, list, or iterable
Box vectors / cell description.
"""
box = getattr(system, 'box', None)
if box is None or isinstance(box, np.ndarray):
# Snapshot System (or harness with no box yet) -- build a
# fresh harness Box. Callers running against the snapshot may
# later replace this with a snapshot-native box helper.
system.box = create_box(cell=length)
else:
box.update_size(length)
[docs]def generate_system_velocities(system, rgen=None, seed=0, momentum=True,
temperature=None, distribution='maxwell'):
"""Generate per-particle velocities at the system temperature.
Free-function form of the soon-to-go
:meth:`.System.generate_velocities`.
Parameters
----------
system : object like :class:`.System`
Must have ``system.particles``, ``system.units``, and
``system.temperature['set' / 'dof']`` populated.
rgen : str or object, optional
Random generator selector / instance.
seed : int, optional
Seed for the random generator.
momentum : bool, optional
If True, reset the linear momentum after drawing.
temperature : float, optional
Override target temperature. When None, taken from
``system.temperature['set']``.
distribution : str, optional
Currently only ``'maxwell'`` is supported.
"""
# Argument list mirrors the legacy System.generate_velocities API.
# pylint: disable=too-many-arguments,too-many-positional-arguments
rgen_settings = {'seed': seed, 'rgen': rgen}
rgen = create_random_generator(rgen_settings)
if temperature is None:
temperature = system.temperature['set']
dof = system.temperature['dof']
if distribution.lower() == 'maxwell':
rgen.generate_maxwellian_velocities(
system.particles,
CONSTANTS['kB'][system.units],
temperature,
dof, momentum=momentum,
)
else:
logger.error(
'Distribution "%s" not defined! Velocities not set!',
distribution,
)
[docs]def calculate_system_temperature(system):
"""Compute the current temperature from particle velocities.
Free-function form of the soon-to-go
:meth:`.System.calculate_temperature`.
Parameters
----------
system : object like :class:`.System`
Returns
-------
float
Kinetic temperature.
"""
dof = system.temperature['dof']
_, temp, _ = calculate_kinetic_temperature(
system.particles, CONSTANTS['kB'][system.units], dof=dof,
)
return temp