Source code for optmat.gas.reduction
# SPDX-FileCopyrightText: 2026 John C. Travers and contributors
#
# SPDX-License-Identifier: MIT
"""From a gas dataset's printed values to the per-particle polarizability.
A refractivity measured at a stated pressure and temperature, a polarizability in some
unit, a molar refractivity or a refractivity per density each imply the per-particle
dipole polarizability at their frequency. The conversion needs the number density the
source meant (real-gas unless the source reduced with the ideal gas,
:mod:`optmat.gas.eos`) and, for a refractivity, Lorentz-Lorenz with the gas's
refractivity virial term (:mod:`optmat.gas.lorentz_lorenz`, :mod:`optmat.gas.virial`).
"""
import math
from typing import Final
import numpy as np
from jaxtyping import Float
from optmat.conditions import ONE_ATM_0C, State
from optmat.constants import AU_POLARIZABILITY, EPS0, N_A
from optmat.datasets import Dataset
from optmat.exceptions import DataError
from optmat.gas.eos import number_density
from optmat.gas.lorentz_lorenz import polarizability_from_refractivity, refractivity
__all__ = [
"POLARIZABILITY_UNITS",
"density_at_state",
"polarizability",
"polarizability_uncertainty",
"refractivity_at",
]
POLARIZABILITY_UNITS: Final[dict[str, float]] = {
"au": AU_POLARIZABILITY,
# A polarizability *volume* alpha / (4 pi eps0), in A^3 or cm^3.
"angstrom3": 4 * math.pi * EPS0 * 1e-30,
"cm3": 4 * math.pi * EPS0 * 1e-6,
"SI": 1.0,
}
"""Factors from each polarizability unit of the vocabulary to C m^2/V."""
[docs]
def density_at_state(dataset: Dataset) -> float:
"""Return the number density the source meant at the dataset's state.
Parameters
----------
dataset : Dataset
A dataset with a state.
Returns
-------
float
Number density, m^-3: real-gas unless the source reduced with the ideal gas.
Raises
------
DataError
If the dataset has no state.
"""
if dataset.state is None:
msg = f"{dataset.key}: no state"
raise DataError(msg)
return number_density(
dataset.material, dataset.state, ideal=dataset.density == "ideal"
)
def _virial(dataset: Dataset) -> float:
"""Return the gas's refractivity virial ratio ``B_R / A_R``.
Parameters
----------
dataset : Dataset
The dataset, for its gas.
Returns
-------
float
The ratio in m^3/mol; 0 where none is curated.
"""
# Imported here: the virial table is itself read from the curated datasets.
from optmat.gas.virial import virial_ratio # noqa: PLC0415
return virial_ratio(dataset.material)
[docs]
def polarizability(dataset: Dataset) -> Float[np.ndarray, " n"]:
"""Return the per-particle polarizability each value implies.
Parameters
----------
dataset : Dataset
A dataset of one of :data:`~optmat.vocabulary.POLARIZABILITY_QUANTITIES`.
Returns
-------
numpy.ndarray
Dipole polarizability, C m^2/V.
Raises
------
DataError
If the quantity does not determine the polarizability by itself.
"""
values = dataset.scaled_values()
match dataset.quantity:
case "polarizability":
return values * POLARIZABILITY_UNITS[dataset.unit]
case "refractivity":
return polarizability_from_refractivity(
values, density_at_state(dataset), _virial(dataset)
)
case "molar_refractivity":
# A_R = N_A alpha / (3 eps0), in cm^3/mol.
return 3 * EPS0 * values * 1e-6 / N_A
case "refractivity_per_density":
# (n - 1) / N in cm^3: n - 1 at the state's density, then Lorentz-Lorenz.
density = density_at_state(dataset)
return polarizability_from_refractivity(
values * 1e-6 * density, density, _virial(dataset)
)
case _:
msg = (
f"{dataset.key}: {dataset.quantity!r} has no polarizability of its own"
)
raise DataError(msg)
[docs]
def polarizability_uncertainty(dataset: Dataset) -> Float[np.ndarray, " n"]:
"""Return the one-sigma uncertainty of :func:`polarizability`.
Every conversion is linear in the value to within ``n - 1`` (1e-3), so the relative
uncertainty carries over.
Parameters
----------
dataset : Dataset
A dataset of one of :data:`~optmat.vocabulary.POLARIZABILITY_QUANTITIES`.
Returns
-------
numpy.ndarray
One-sigma uncertainty, C m^2/V; NaN where none is stated.
"""
values = dataset.scaled_values()
return polarizability(dataset) * dataset.standard_uncertainty() / np.abs(values)
[docs]
def refractivity_at(
dataset: Dataset, state: State = ONE_ATM_0C
) -> Float[np.ndarray, " n"]:
"""Return ``n - 1`` of the real gas at ``state`` implied by each value.
Parameters
----------
dataset : Dataset
A dataset of one of :data:`~optmat.vocabulary.POLARIZABILITY_QUANTITIES`.
state : State, optional
The state to evaluate at; 0 degrees C and 1 atm by default.
Returns
-------
numpy.ndarray
``n - 1`` from the polarizability by Lorentz-Lorenz at the real-gas density,
with the gas's refractivity virial term.
"""
return refractivity(
polarizability(dataset),
number_density(dataset.material, state),
_virial(dataset),
)