Source code for epyr.physics.units

"""
Unit conversion utilities for EPR/NMR spectroscopy

Converts between common spectroscopic units: cm⁻¹, eV, K, mT, MHz
All conversions use 2022 CODATA physical constants for accuracy.
"""

from typing import Optional, Union

import numpy as np

try:
    from ..logging_config import get_logger
    from .constants import bmagn, boltzm, clight, evolt, gfree, planck
except ImportError:
    import logging

    def get_logger(name):
        return logging.getLogger(name)

    from constants import bmagn, boltzm, clight, evolt, gfree, planck

logger = get_logger(__name__)


[docs] def unitconvert( value: Union[float, np.ndarray], units: str, g_factor: Optional[Union[float, np.ndarray]] = None, ) -> Union[float, np.ndarray]: """ Convert between spectroscopic units. Supported conversions: - cm⁻¹ ↔ eV, K, mT, MHz - eV ↔ cm⁻¹, K, mT, MHz - K ↔ cm⁻¹, eV, mT, MHz - mT ↔ cm⁻¹, eV, K, MHz - MHz ↔ cm⁻¹, eV, K, mT Parameters ---------- value : float or array Input value(s) to convert units : str Conversion string in format 'unit_from->unit_to' e.g., 'cm^-1->MHz', 'eV->mT' g_factor : float or array, optional g-factor for magnetic field conversions Defaults to free electron g-factor (2.002319...) Returns ------- float or array Converted value(s) Examples -------- >>> # Convert wavenumbers to frequency >>> freq = unitconvert(1000, 'cm^-1->MHz') # 1000 cm⁻¹ to MHz >>> print(f"{freq:.3f} MHz") >>> # Convert with custom g-factor >>> field = unitconvert(100, 'cm^-1->mT', g_factor=2.005) >>> print(f"{field:.2f} mT") >>> # Vector conversion >>> energies = np.array([100, 200, 300]) # cm⁻¹ >>> temps = unitconvert(energies, 'cm^-1->K') >>> print(f"Temperatures: {temps}") """ if g_factor is None: g_factor = gfree() # Dictionary of conversion functions conversions = { # From cm⁻¹ "cm^-1->eV": lambda v: v * 100 * clight() * planck() / evolt(), "cm^-1->K": lambda v: v * 100 * clight() * planck() / boltzm(), "cm^-1->mT": lambda v: v / g_factor * (planck() / bmagn() / 1e-3) * 100 * clight(), "cm^-1->MHz": lambda v: v * 100 * clight() / 1e6, # From eV "eV->cm^-1": lambda v: v * evolt() / (100 * clight() * planck()), "eV->K": lambda v: v * evolt() / boltzm(), "eV->mT": lambda v: v / g_factor / bmagn() / 1e-3 * evolt(), "eV->MHz": lambda v: v * evolt() / planck() / 1e6, # From K "K->cm^-1": lambda v: v * boltzm() / (100 * clight() * planck()), "K->eV": lambda v: v * boltzm() / evolt(), "K->mT": lambda v: v / g_factor / bmagn() / 1e-3 * boltzm(), "K->MHz": lambda v: v * boltzm() / planck() / 1e6, # From mT "mT->cm^-1": lambda v: v * g_factor / (planck() / bmagn() / 1e-3) / (100 * clight()), "mT->eV": lambda v: v * g_factor * bmagn() * 1e-3 / evolt(), "mT->K": lambda v: v * g_factor * bmagn() * 1e-3 / boltzm(), "mT->MHz": lambda v: v * g_factor * (1e-3 * bmagn() / planck() / 1e6), # From MHz "MHz->cm^-1": lambda v: v * 1e6 / (100 * clight()), "MHz->eV": lambda v: v * 1e6 * planck() / evolt(), "MHz->K": lambda v: v * 1e6 * planck() / boltzm(), "MHz->mT": lambda v: v / g_factor * (planck() / bmagn() / 1e-3) * 1e6, } # Check if conversion exists (case-sensitive first) if units in conversions: return conversions[units](value) # Check case-insensitive units_lower = units.lower() conversions_lower = {k.lower(): v for k, v in conversions.items()} if units_lower in conversions_lower: # Find the correct case correct_units = next(k for k in conversions.keys() if k.lower() == units_lower) raise ValueError( f"Case mismatch. You provided: '{units}'. Did you mean '{correct_units}'?" ) raise ValueError( f"Unknown unit conversion: '{units}'. " f"Supported: {', '.join(sorted(conversions.keys()))}" )
[docs] def list_conversions(): """List all supported unit conversions.""" conversions = [ "cm^-1 <-> eV, K, mT, MHz", "eV <-> cm^-1, K, mT, MHz", "K <-> cm^-1, eV, mT, MHz", "mT <-> cm^-1, eV, K, MHz", "MHz <-> cm^-1, eV, K, mT", ] logger.info("Supported Unit Conversions") logger.info("=" * 35) for conv in conversions: logger.info(f" {conv}") logger.info("Physical Constants Used:") logger.info(f" Speed of light: {clight():.0f} m⋅s⁻¹") logger.info(f" Planck constant: {planck():.2e} J⋅s") logger.info(f" Bohr magneton: {bmagn():.2e} J⋅T⁻¹") logger.info(f" Boltzmann constant: {boltzm():.2e} J⋅K⁻¹") logger.info(f" Electron volt: {evolt():.2e} J") logger.info(f" Free electron g-factor: {gfree():.8f}")
[docs] def demo_conversions(): """Demonstrate common unit conversions in EPR spectroscopy.""" logger.info("EPR Unit Conversion Examples") logger.info("=" * 40) # Example 1: X-band EPR field calculation freq_ghz = 9.5 # GHz freq_mhz = freq_ghz * 1000 field_mt = unitconvert(freq_mhz, "MHz->mT") logger.info(f"X-band EPR ({freq_ghz} GHz):") logger.info(f" Resonant field: {field_mt:.1f} mT") # Example 2: Energy scale conversions energy_wn = 1000 # cm⁻¹ energy_ev = unitconvert(energy_wn, "cm^-1->eV") energy_k = unitconvert(energy_wn, "cm^-1->K") energy_mhz = unitconvert(energy_wn, "cm^-1->MHz") logger.info(f"Energy scale comparisons for {energy_wn} cm⁻¹:") logger.info(f" {energy_ev:.6f} eV") logger.info(f" {energy_k:.1f} K") logger.info(f" {energy_mhz/1000:.1f} GHz") # Example 3: Temperature to field conversion temp_k = 4.2 # Liquid helium temperature temp_wn = unitconvert(temp_k, "K->cm^-1") temp_field = unitconvert(temp_k, "K->mT") logger.info(f"Thermal energy at {temp_k} K:") logger.info(f" {temp_wn:.3f} cm⁻¹") logger.info(f" {temp_field:.3f} mT equivalent field") # Example 4: Vector conversion fields = np.array([100, 200, 300, 400]) # mT freqs = unitconvert(fields, "mT->MHz", g_factor=2.003) logger.info("Field to frequency conversion (g=2.003):") for b, f in zip(fields, freqs): logger.info(f" {b} mT → {f/1000:.3f} GHz") # Example 5: Different g-factors field = 340 # mT g_factors = np.array([2.000, 2.003, 2.010, 2.100]) frequencies = unitconvert(field, "mT->MHz", g_factor=g_factors) logger.info(f"Frequency at {field} mT for different g-factors:") for g, f in zip(g_factors, frequencies): logger.info(f" g = {g:.3f}{f/1000:.3f} GHz")
if __name__ == "__main__": list_conversions() logger.info("") demo_conversions()