Source code for ad_hoc_diffractometer.radiation
# Copyright (c) 2026 Pete R. Jemian <prjemian+ad_hoc_diffractometer@gmail.com>
# SPDX-License-Identifier: CC-BY-4.0
"""
radiation.py — Wavelength, energy, and wave-number conversions.
Provides named constants for common laboratory X-ray emission lines,
standalone conversion functions for both X-rays and fixed-wavelength
neutrons (reactor sources), and the ``SOURCE_TYPES`` sentinel used by
:class:`AdHocDiffractometer` to select the correct energy formula.
**Source type scope**
This module supports two source types:
``"xray"`` (default)
Photon energy: E (keV) = hc/λ = 12.39842 / λ (Å).
``"neutron"``
Fixed-wavelength **reactor** neutrons only. Energy via the de Broglie
relation: E (meV) = h²/(2 m_n λ²) = 81.8042 / λ² (Å).
Spallation/time-of-flight sources sweep the full spectrum with each
pulse and require a fundamentally different instrument description;
they are out of scope for this project.
Constants
---------
HC_KEV_ANGSTROM : float
hc = 12.398 419 843 320 026 keV·Å. Exact (NIST CODATA 2022).
HC_KEV_ANGSTROM_UNCERTAINTY : float
0.0 — exactly zero. The 2019 SI redefinition fixed both *h* and *c*
exactly, so *hc* has no uncertainty by definition.
NEUTRON_MEV_ANGSTROM2 : float
h²/(2 m_n) = 81.804 210 235 2 meV·Å² (NIST CODATA 2022).
NEUTRON_MEV_ANGSTROM2_UNCERTAINTY : float
0.0000000415 meV·Å² — propagated from CODATA 2022 m_n uncertainty.
SOURCE_TYPES : tuple[str, ...]
Valid source-type strings: ``("xray", "neutron")``.
XRAY_LINES : dict[str, float]
Named X-ray emission line wavelengths in Å.
X-ray functions
---------------
wavelength_to_energy(wavelength) -> float
λ (Å) → E (keV) = hc/λ.
energy_to_wavelength(energy_kev) -> float
E (keV) → λ (Å) = hc/E.
wavelength_to_wavenumber(wavelength) -> float
λ (Å) → k (Å⁻¹) = 2π/λ.
wavenumber_to_wavelength(wavenumber) -> float
k (Å⁻¹) → λ (Å) = 2π/k.
Neutron functions
-----------------
neutron_wavelength_to_energy(wavelength) -> float
λ (Å) → E (meV) = 81.8042 / λ².
neutron_energy_to_wavelength(energy_mev) -> float
E (meV) → λ (Å) = sqrt(81.8042 / E).
References
----------
NIST CODATA 2018 — hc = 12398.42 eV·Å = 12.39842 keV·Å
NIST neutron scattering — h²/(2 m_n) = 81.8042 meV·Å²
de Broglie relation — λ = h/p, E = p²/(2 m_n)
"""
from __future__ import annotations
import math
# ---------------------------------------------------------------------------
# Physical constant
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# Physical constants — NIST CODATA 2022 adjustment
# https://physics.nist.gov/cuu/Constants/Table/allascii.txt
#
# HC_KEV_ANGSTROM: hc in keV·Å.
# Derived from h (exact), c (exact), e (exact) per SI 2019 redefinition.
# hc = 6.62607015e-34 J·Hz⁻¹ × 299792458 m·s⁻¹ / 1.602176634e-19 J·eV⁻¹
# = 1.239841984332003e-6 eV·m (exact, listed in CODATA table)
# = 12.398419843320026 keV·Å (exact — no uncertainty)
#
# NEUTRON_MEV_ANGSTROM2: h²/(2 m_n) in meV·Å².
# h (exact); m_n = 1.674 927 500 56 e-27 kg ± 0.000 000 000 85 e-27 kg
# CODATA 2022 relative uncertainty of m_n: 5.075e-10
# NEUTRON_MEV_ANGSTROM2_UNCERTAINTY = 0.0000000415 meV·Å²
# ---------------------------------------------------------------------------
HC_KEV_ANGSTROM: float = 12.398419843320026
"""hc = 12.398 419 843 320 026 keV·Å.
Exact (h, c, and e are all defined SI constants since 2019).
Source: NIST CODATA 2022.
"""
HC_KEV_ANGSTROM_UNCERTAINTY: float = 0.0
"""Uncertainty of :data:`HC_KEV_ANGSTROM` in keV·Å.
Exactly **0.0** — not a placeholder. The 2019 redefinition of the SI
fixed the numerical values of Planck's constant *h* and the speed of light
*c* exactly; their product *hc* therefore has zero uncertainty by
definition. Reference: BIPM SI Brochure, 9th edition (2019); NIST CODATA 2022.
"""
NEUTRON_MEV_ANGSTROM2: float = 81.8042102352
"""h²/(2 m_n) = 81.804 210 235 2 meV·Å².
Used for reactor-neutron kinetic energy via the de Broglie relation.
Source: NIST CODATA 2022; m_n = 1.674 927 500 56(85) × 10⁻²⁷ kg.
"""
NEUTRON_MEV_ANGSTROM2_UNCERTAINTY: float = 0.0000000415
"""Uncertainty of :data:`NEUTRON_MEV_ANGSTROM2` in meV·Å².
Propagated from the CODATA 2022 uncertainty in the neutron mass m_n.
"""
SOURCE_TYPES: tuple[str, ...] = ("xray", "neutron")
"""Valid source-type strings for :attr:`AdHocDiffractometer.source_type`.
``"xray"`` uses hc/λ (keV); ``"neutron"`` uses de Broglie h²/(2 m_n λ²) (meV).
"""
# ---------------------------------------------------------------------------
# Named X-ray emission lines (wavelengths in Å)
# ---------------------------------------------------------------------------
XRAY_LINES: dict[str, float] = {
"Cu_Ka": 1.54060, # Cu Kα weighted mean (2·Kα1 + Kα2) / 3
"Cu_Ka1": 1.54056, # Cu Kα1
"Cu_Ka2": 1.54439, # Cu Kα2
"Mo_Ka": 0.71073, # Mo Kα weighted mean
"Mo_Ka1": 0.70930, # Mo Kα1
"Ag_Ka": 0.56087, # Ag Kα weighted mean
"Ag_Ka1": 0.55941, # Ag Kα1
"Co_Ka": 1.79021, # Co Kα weighted mean
"Co_Ka1": 1.78897, # Co Kα1
}
"""Common laboratory X-ray emission line wavelengths in Å.
Weighted means (Kα) use the standard 2:1 weighting of Kα1 and Kα2.
Values from NIST X-Ray Transition Energies Database.
"""
# ---------------------------------------------------------------------------
# Conversion functions
# ---------------------------------------------------------------------------
[docs]
def wavelength_to_energy(wavelength: float) -> float:
"""
Convert wavelength to photon energy for X-rays.
E (keV) = hc / λ = 12.39842 / λ (Å)
Parameters
----------
wavelength : float
Wavelength in Å.
Returns
-------
float
Photon energy in keV.
Raises
------
ValueError
If ``wavelength`` ≤ 0.
Examples
--------
>>> import ad_hoc_diffractometer as ahd
>>> round(ahd.wavelength_to_energy(1.5406), 4)
8.0478
"""
if wavelength <= 0.0:
raise ValueError(
f"wavelength_to_energy(): wavelength must be > 0 Å; got {wavelength}."
)
return HC_KEV_ANGSTROM / wavelength
[docs]
def energy_to_wavelength(energy_kev: float) -> float:
"""
Convert photon energy to wavelength for X-rays.
λ (Å) = hc / E = 12.39842 / E (keV)
Parameters
----------
energy_kev : float
Photon energy in keV.
Returns
-------
float
Wavelength in Å.
Raises
------
ValueError
If ``energy_kev`` ≤ 0.
Examples
--------
>>> import ad_hoc_diffractometer as ahd
>>> round(ahd.energy_to_wavelength(8.047), 4)
1.5408
"""
if energy_kev <= 0.0:
raise ValueError(
f"energy_to_wavelength(): energy must be > 0 keV; got {energy_kev}."
)
return HC_KEV_ANGSTROM / energy_kev
[docs]
def wavelength_to_wavenumber(wavelength: float) -> float:
"""
Convert wavelength to wave number k.
k (Å⁻¹) = 2π / λ (Å)
Parameters
----------
wavelength : float
Wavelength in Å.
Returns
-------
float
Wave number k in Å⁻¹.
Raises
------
ValueError
If ``wavelength`` ≤ 0.
Examples
--------
>>> import ad_hoc_diffractometer as ahd
>>> round(ahd.wavelength_to_wavenumber(1.0), 6)
6.283185
"""
if wavelength <= 0.0:
raise ValueError(
f"wavelength_to_wavenumber(): wavelength must be > 0 Å; got {wavelength}."
)
return 2.0 * math.pi / wavelength
[docs]
def wavenumber_to_wavelength(wavenumber: float) -> float:
"""
Convert wave number k to wavelength.
λ (Å) = 2π / k (Å⁻¹)
Parameters
----------
wavenumber : float
Wave number k in Å⁻¹.
Returns
-------
float
Wavelength in Å.
Raises
------
ValueError
If ``wavenumber`` ≤ 0.
Examples
--------
>>> import ad_hoc_diffractometer as ahd
>>> round(ahd.wavenumber_to_wavelength(6.283185), 6)
1.0
"""
if wavenumber <= 0.0:
raise ValueError(
f"wavenumber_to_wavelength(): wavenumber must be > 0 Å⁻¹; got {wavenumber}."
)
return 2.0 * math.pi / wavenumber
# ---------------------------------------------------------------------------
# Neutron conversion functions (de Broglie, reactor / fixed-wavelength only)
# ---------------------------------------------------------------------------
[docs]
def neutron_wavelength_to_energy(wavelength: float) -> float:
"""
Convert wavelength to neutron kinetic energy (de Broglie relation).
E (meV) = h² / (2 m_n λ²) = 81.8042 / λ² (Å)
Applies to **fixed-wavelength reactor neutrons** only. Spallation
(time-of-flight) sources are out of scope for this project.
Parameters
----------
wavelength : float
Neutron wavelength in Å.
Returns
-------
float
Neutron kinetic energy in meV.
Raises
------
ValueError
If ``wavelength`` ≤ 0.
See Also
--------
wavelength_to_energy : X-ray photon energy in keV.
Examples
--------
>>> import ad_hoc_diffractometer as ahd
>>> round(ahd.neutron_wavelength_to_energy(1.8), 4)
25.2482
"""
if wavelength <= 0.0:
raise ValueError(
"neutron_wavelength_to_energy(): wavelength must be > 0 Å; "
f"got {wavelength}."
)
return NEUTRON_MEV_ANGSTROM2 / (wavelength**2)
[docs]
def neutron_energy_to_wavelength(energy_mev: float) -> float:
"""
Convert neutron kinetic energy to wavelength (de Broglie relation).
λ (Å) = sqrt(h² / (2 m_n E)) = sqrt(81.8042 / E (meV))
Applies to **fixed-wavelength reactor neutrons** only.
Parameters
----------
energy_mev : float
Neutron kinetic energy in meV.
Returns
-------
float
Neutron wavelength in Å.
Raises
------
ValueError
If ``energy_mev`` ≤ 0.
Examples
--------
>>> import ad_hoc_diffractometer as ahd
>>> round(ahd.neutron_energy_to_wavelength(25.0), 4)
1.809
"""
if energy_mev <= 0.0:
raise ValueError(
f"neutron_energy_to_wavelength(): energy must be > 0 meV; got {energy_mev}."
)
return math.sqrt(NEUTRON_MEV_ANGSTROM2 / energy_mev)