Source code for abinslib.util
"""Utility functions, not specific to one calculation type."""
from __future__ import annotations
from collections.abc import Iterator
from dataclasses import dataclass
from importlib.metadata import PackageNotFoundError, version
from typing import TYPE_CHECKING, Self, TypedDict
from euphonic import Quantity
from euphonic.isotopes import Structure, sears_1992
from euphonic.spectra import Spectrum1DCollection
import numpy as np
if TYPE_CHECKING:
from euphonic.isotopes import IsotopeData
[docs]
def is_none(a: object) -> bool:
"""Backport from python 3.14 operator.is_none."""
return a is None
[docs]
def get_version() -> str:
"""Get package version or return 'DEVELOPMENT' if not installed."""
try:
return version("abinslib")
except PackageNotFoundError:
return "DEVELOPMENT"
[docs]
def calculate_indirect_q2(
energy_transfer: Quantity, angle: float, final_energy: Quantity
) -> Quantity:
"""Calculate Q^2 value for given energy transfer in indirect geometry.
By the cosine law Q^2 = k_f^2 + k_i^2 - 2 k_f k_i cos(theta)
Args:
energy_transfer: neutron energy change. (Positive values correspond to
transfer to sample.)
angle: scattering angle in radians
final_energy: energy of detected neutrons (i.e. after monochromator)
Returns:
array of scalar Q^2 corresponding to input energy_transfer
"""
# Get rid of ambiguous cm-1 units before manipulating energies
energy_transfer = energy_transfer.to("meV", "spectroscopy")
final_energy = final_energy.to("meV", "spectroscopy")
# E = hbar^2 k^2 / 2m
momentum2_to_energy = Quantity(0.5, "hbar^2 / neutron_mass").to("meV Å^2")
k2_i = (energy_transfer + final_energy) / momentum2_to_energy
k2_f = final_energy / momentum2_to_energy
return k2_i + k2_f - 2 * np.sqrt(k2_i * k2_f) * np.cos(angle)
@dataclass
class _AtomSequence:
atom_type: np.ndarray
atom_mass: Quantity
@classmethod
def from_spectra(cls, spectra: Spectrum1DCollection) -> Self:
"""Build a quasi-structure object from metadata of spectra."""
symbols = [item.get("atom_symbol") for item in spectra.iter_metadata()]
masses = [item.get("mass") for item in spectra.iter_metadata()]
if any(map(is_none, symbols)) or any(map(is_none, masses)):
raise ValueError(
"Not all items in spectra have atom_symbol and mass metadata."
)
atom_type = np.array(symbols)
atom_mass = Quantity(np.array(masses, dtype=float), "amu")
return cls(atom_type, atom_mass)
[docs]
def apply_weights(
spectra: Spectrum1DCollection,
isotope_data: IsotopeData = sears_1992,
key: str = "scattering_cross_section",
) -> Spectrum1DCollection:
"""Apply weights to Spectrum Collection data, based on atom symbol and mass.
Initially this only supports Spectrum1DCollection, but support for
Spectrum2DCollection will be added as needed.
Args:
spectra: unweighted data including 'atom_symbol' and 'mass' metadata
isotope_data: neutron dataset with symbol/mass lookup capability
key: key for get_array() lookups in isotope_data
Returns:
new set of weighted spectra
"""
atoms = _AtomSequence.from_spectra(spectra)
weights = isotope_data.get_array(atoms, key=key)
y_data = spectra.y_data * weights[:, None]
return Spectrum1DCollection(
x_data=spectra.x_data, y_data=y_data, metadata=spectra.metadata
)
class _AtomInfo(TypedDict):
"""Atom metadata for use in SpectumNDCollection."""
atom_index: int
atom_symbol: str
mass: str
[docs]
def iter_atom_info(structure: Structure) -> Iterator[_AtomInfo]:
"""Yield a series of atom metadata from structure data."""
for atom_index, (symbol, mass) in enumerate(
zip(structure.atom_type, structure.atom_mass.to("amu").magnitude, strict=True)
):
yield _AtomInfo(atom_index=atom_index, atom_symbol=str(symbol), mass=str(mass))