import os
from pathlib import Path
# --- Light constants (no numpy/scipy needed) ---
DEBUG = bool(os.getenv("DEBUG"))
MAX_TRY = 5
CHIRALS = ["VCD", "ECD"]
GRAPHS = ['IR', 'VCD', 'UV', 'ECD']
CONVERT_B = {'GHz': 29.979000}
VIBRO_OR_ELECTRO = {
'IR': 'vibro',
'VCD': 'vibro',
'UV': 'electro',
'ECD': 'electro',
}
LOG_FORMAT = "%(message)s"
EH_TO_KCAL = 627.5096080305927
EV_TO_EH = 1.0 / 27.211386245981 # NIST CODATA 2022
CAL_TO_J = 4.186
MARKERS = [
".", ",", "o", "v", "^", "<", ">", "1", "2", "3",
"4", "8", "s", "p", "*", "h", "H", "+", "x", "D",
"d", "|", "_", "P", "X",
]
MIN_RETENTION_RATE = 0.2
DEFAULT_RESOLUTION = 500
MIN_CONFORMERS_FOR_PCA = 50
MIN_WEIGHTED_VALUE = 0.15
regex_parsing = {
"orca": {"ext": "out"},
"gaussian": {"ext": "log"},
"nwchem": {"ext": "nwo"},
"tblite": {"ext": None},
"aimnet": {"ext": None},
"uma": {"ext": None},
"fairchem": {"ext": None},
"mace": {"ext": None},
"skala": {"ext": None},
}
# --- Lazy — initialised on first access to scipy/numpy-dependent constants ---
_LAZY_INIT = False
def _ensure_lazy():
global _LAZY_INIT, np, R, c, h, electron_volt, Boltzmann, N_A, \
FACTOR_EV_NM, FACTOR_EV_CM_1, J_TO_H, AMU_TO_KG, ROT_CONST_FACTOR
if _LAZY_INIT:
return
import numpy as _np
from scipy.constants import R as _R, c as _c, h as _h, \
electron_volt as _ev, Boltzmann as _B, N_A as _NA
from scipy.constants import physical_constants
_J_TO_H = physical_constants["joule-hartree relationship"][0]
_AMU_TO_KG = physical_constants["atomic mass constant"][0]
g = globals()
g['np'] = _np
g['R'] = _R
g['h'] = _h
g['electron_volt'] = _ev
g['Boltzmann'] = _B
g['N_A'] = _NA
g['FACTOR_EV_NM'] = _h * _c / (10 ** -9 * _ev)
g['FACTOR_EV_CM_1'] = 1 / 8065.544
g['c'] = _c * 100
g['J_TO_H'] = _J_TO_H
g['AMU_TO_KG'] = _AMU_TO_KG
g['ROT_CONST_FACTOR'] = _h / (8 * np.pi ** 2 * g['c'] * _AMU_TO_KG * 1e-20)
_LAZY_INIT = True
# --- Functions (their bodies depend on lazy constants) ---
[docs]
def eV_to_nm(eV: "np.ndarray") -> "np.ndarray":
_ensure_lazy()
eV = np.maximum(eV, 1e-2)
return FACTOR_EV_NM / eV
[docs]
def boltzmann_distribution(
energies: "np.ndarray",
temperature: float,
) -> tuple["np.ndarray", "np.ndarray"]:
_ensure_lazy()
if energies.size == 0:
return np.array([]), np.array([])
rel_energies = energies - energies.min()
exponent = -(rel_energies * EH_TO_KCAL * 1000 * CAL_TO_J) / (R * temperature)
weights = np.exp(exponent)
population = weights / weights.sum()
return rel_energies * EH_TO_KCAL, population
[docs]
def ordinal(n: int) -> str:
return "%d-%s" % (n, "tsnrhtdd"[(n // 10 % 10 != 1) * (n % 10 < 4) * n % 10:: 4])
[docs]
def compute_boltzmann_populations(conformers, protocol_number: int, temperature: float) -> None:
import numpy as np
active = [c for c in conformers if c.active]
if not active:
return
energies = np.array([c.get_energy(protocol_number=protocol_number) for c in active])
rel_en, pops = boltzmann_distribution(energies, temperature)
for c, rel_e, p in zip(active, rel_en, pops):
c.energies.set(str(protocol_number), 'Pop', p * 100)
c.energies.set(str(protocol_number), 'Erel', rel_e)
[docs]
def get_models_dir(calculator_name: str, create: bool = True) -> Path:
from enan_calculators import get_models_dir as _get_models_dir
return _get_models_dir(calculator_name, create)
def __getattr__(name):
heavy = {'np', 'R', 'c', 'h', 'electron_volt', 'Boltzmann', 'N_A',
'FACTOR_EV_NM', 'FACTOR_EV_CM_1', 'J_TO_H', 'AMU_TO_KG',
'ROT_CONST_FACTOR'}
if name in heavy:
_ensure_lazy()
return globals()[name]
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")