Source code for maxwelllink.tools.harmonic_oscillator_helper
# --------------------------------------------------------------------------------------#
# Copyright (c) 2026 MaxwellLink #
# This file is part of MaxwellLink. Repository: https://github.com/TaoELi/MaxwellLink #
# If you use this code, always credit and cite arXiv:2512.06173. #
# See AGENTS.md and README.md for details. #
# --------------------------------------------------------------------------------------#
"""
Harmonic oscillator helper functions for MD simulations.
"""
import numpy as np
from typing import Optional
from ..units import AU_TO_K, AU_TO_FS
[docs]
class DummyInitializer:
"""
A dummy initializer that returns zero momenta and positions.
"""
[docs]
def __init__(self):
pass
[docs]
def momentum_initializer(self, p):
return p
[docs]
def position_initializer(self, omega, q):
return q
[docs]
class DummyThermostat:
"""
A dummy thermostat that does nothing.
"""
[docs]
def __init__(self):
pass
[docs]
def apply_kick(self, momentum: np.ndarray) -> np.ndarray:
return momentum
[docs]
class MaxwellBoltzmannInitializer:
"""
A class to initialize particle velocities based on the Maxwell-Boltzmann distribution at the given temperature.
"""
[docs]
def __init__(self, temperature_au: float, random_seed: Optional[int] = 114514):
"""
Parameters
----------
T_au : float
Temperature in atomic units. k_B is set to 1 in atomic units, so T_au is effectively the thermal energy. Must be positive.
random_seed : int, optional
Random seed for reproducible results.
"""
if temperature_au <= 0:
raise ValueError("Temperature must be positive.")
self.temperature_au = temperature_au
self.random_seed = random_seed
self.rng = np.random.default_rng(self.random_seed)
[docs]
def momentum_initializer(self, p):
if np.all(p == 0):
print(
f"[Maxwell-Boltzmann Initializer] Initializing momenta with Maxwell-Boltzmann distribution at temperature {self.temperature_au} au."
)
size = p.shape if isinstance(p, np.ndarray) else (len(p),)
p_mb = self.rng.normal(scale=np.sqrt(self.temperature_au), size=size)
if p_mb.size == 3:
return p_mb
p_mb -= np.mean(
p_mb, axis=0
) # remove any net momentum to ensure total momentum is zero
T_cur_p = np.sum(p_mb**2) / (p_mb.size - 3)
scaling_factor_p = np.sqrt(self.temperature_au / T_cur_p)
return p_mb * scaling_factor_p
else:
print(
"[Maxwell-Boltzmann Initializer] Warning: Initial momenta are provided, skipping Maxwell-Boltzmann initialization."
)
return p
[docs]
def position_initializer(self, omega, q):
if np.all(q == 0):
print(
f"[Maxwell-Boltzmann Initializer] Initializing positions with Maxwell-Boltzmann distribution at temperature {self.temperature_au} au."
)
size = q.shape if isinstance(q, np.ndarray) else (len(q),)
q_mb = self.rng.normal(
scale=np.sqrt(self.temperature_au) / omega[:, None], size=size
)
if q_mb.size == 3:
return q_mb
q_mb -= np.mean(
q_mb, axis=0
) # remove any net displacement to ensure total displacement is zero
T_cur_q = np.sum((omega[:, None] * q_mb) ** 2) / (q_mb.size - 3)
scaling_factor_q = np.sqrt(self.temperature_au / T_cur_q)
return q_mb * scaling_factor_q
else:
print(
"[Maxwell-Boltzmann Initializer] Warning: Initial positions are provided, skipping Maxwell-Boltzmann initialization."
)
return q
[docs]
class LangevinThermostat:
"""
Langevin thermostat implementation for the cavity mode.
"""
[docs]
def __init__(
self,
temperature_au: float,
dt_au: float,
tau_au: float,
random_seed: Optional[int] = 114514,
):
"""
Parameters
----------
temperature_au : float
Temperature in atomic units. k_B is set to 1 in atomic units, so temperature_au is effectively the thermal energy. Must be positive.
dt_au : float
Time step in atomic units. Must be positive.
tau_au : float
Relaxation time in atomic units. Must be positive.
random_seed : int, optional
Random seed for reproducible results.
"""
if temperature_au <= 0:
raise ValueError("Temperature must be positive.")
self.temperature_au = temperature_au
if dt_au <= 0:
raise ValueError("Time step must be positive.")
self.dt_au = dt_au
if tau_au <= 0:
raise ValueError("Relaxation time must be positive.")
self.tau_au = tau_au
self.random_seed = random_seed
self.rng = np.random.default_rng(self.random_seed)
self.T_l = np.exp(-self.dt_au / self.tau_au)
self.S_l = np.sqrt(self.temperature_au * (1.0 - self.T_l**2))
print(
f"[LangevinThermostat] NVT thermostat enabled with T = {self.temperature_au*AU_TO_K} K = {self.temperature_au} a.u., Langevin tau = {self.tau_au*AU_TO_FS} fs = {self.tau_au} a.u."
)
[docs]
def apply_kick(self, momentum: np.ndarray) -> np.ndarray:
random_kick = self.rng.normal(0, self.S_l, size=momentum.shape)
return momentum * self.T_l + random_kick