Source code for CodeEntropy.entropy.orientational
"""Orientational entropy calculations.
This module defines `OrientationalEntropy`, which computes orientational entropy
from a neighbor count and symmetry information.
"""
from __future__ import annotations
import logging
import math
from dataclasses import dataclass
import numpy as np
logger = logging.getLogger(__name__)
_GAS_CONST_J_PER_MOL_K = 8.3144598484848
[docs]
@dataclass(frozen=True)
class OrientationalEntropyResult:
"""Result of an orientational entropy calculation.
Attributes:
total: Total orientational entropy (J/mol/K).
"""
total: float
[docs]
class OrientationalEntropy:
"""Compute orientational entropy from neighbor counts.
This class is intentionally small and focused: it provides a single public
method that converts a mapping of neighbor species to neighbor counts into
an orientational entropy value.
Notes:
The manager-like constructor signature is kept for compatibility with
the rest of the codebase, but the calculation itself does not depend on
those objects.
"""
def __init__(
self,
gas_constant: float = _GAS_CONST_J_PER_MOL_K,
) -> None:
"""Initialize the orientational entropy calculator.
Args:
gas_constant: Gas constant in J/(mol*K).
"""
self._gas_constant = float(gas_constant)
[docs]
def calculate(
self,
neighbor_count: float,
symmetry_number: int,
linear: bool,
) -> OrientationalEntropyResult:
"""Calculate orientational entropy from neighbor counts.
The number of orientations is estimated as:
Ω = sqrt(N_av^3 * π)/symmetry_number for non-linear molecules
Ω = N_av / symmetry_number for linear molecules
and the entropy contribution is:
S = R * ln(Ω)
where N_av is the average number of neighbors and R is the gas constant.
Args:
neighbors: average number of neighbors
symmetry_number: symmetry number of molecule of interest
linear: True if molecule of interest is linear
Returns:
OrientationalEntropyResult containing the total entropy in J/mol/K.
Raises:
ValueError if number of neighbors is negative.
"""
if neighbor_count < 0:
raise ValueError(f"neighbor_count must be >= 0, got {neighbor_count}")
omega = self._omega(neighbor_count, symmetry_number, linear)
total = self._gas_constant * math.log(omega)
logger.debug(f"Orientational entropy total: {total}")
return total
def _omega(self, neighbor_count: int, symmetry: int, linear: bool) -> float:
"""Compute the number of orientations Ω.
Args:
neighbor_count: average number of neighbors.
symmetry_number: The symmetry number of the molecule.
linear: Is the molecule linear (True or False).
Returns:
Ω (unitless).
"""
# symmetry number 0 = spherically symmetric = no orientational entropy
if symmetry == 0:
omega = 1
else:
if linear:
omega = neighbor_count / symmetry
else:
omega = np.sqrt((neighbor_count**3) * math.pi) / symmetry
# avoid negative orientational entropy
omega = max(omega, 1)
return omega