Source code for abtem.core.energy
"""Module for handling electron energy."""
from __future__ import annotations
from typing import Optional, Sequence, TypeVar, cast
import numpy as np
from ase import units # type: ignore
from abtem.core.utils import CopyMixin, EqualityMixin
[docs]
def relativistic_mass_correction(energy: float) -> float:
"""
Calculate relativistic mass correction from energy.
Parameters
----------
energy: float
Electron energy [eV].
Returns
-------
float
Relativistic mass correction.
"""
return 1 + units._e * energy / (units._me * units._c**2)
[docs]
def energy2mass(energy: float) -> float:
"""
Calculate relativistic mass from energy.
Parameters
----------
enerElectron energy [eV].
Returns
-------
float
Relativistic mass [kg]̄
"""
return relativistic_mass_correction(energy) * units._me
[docs]
def energy2wavelength(energy: float) -> float:
"""
Calculate relativistic de Broglie wavelength from energy.
Parameters
----------
energy: float
Energy [eV].
Returns
-------
float
Relativistic de Broglie wavelength [Å].
"""
if energy <= 0:
raise ValueError(f"Electron energy must be positive, got {energy} eV")
return float(
units._hplanck
* units._c
/ np.sqrt(energy * (2 * units._me * units._c**2 / units._e + energy))
/ units._e
* 1.0e10
)
[docs]
def energy2sigma(energy: float) -> float:
"""
Calculate interaction parameter from energy.
Parameters
----------
energy: float
Energy [ev].
Returns
-------
float
Interaction parameter [1 / (Å * eV)].
"""
return float(
2
* np.pi
* energy2mass(energy)
* units.kg
* units._e
* units.C
* energy2wavelength(energy)
/ (units._hplanck * units.s * units.J) ** 2
)
T = TypeVar("T", bound=tuple[float, ...])
[docs]
def reciprocal_space_sampling_to_angular_sampling(
reciprocal_space_sampling: T, energy: float
) -> T:
"""
Convert reciprocal space sampling in 1/Å to angular sampling in mrad.
Parameters
----------
reciprocal_space_sampling: tuple of floats
Reciprocal space sampling [1/Å].
energy: float
Electron energy in [eV].
Returns
-------
tuple of floats
Angular sampling [mrad].
"""
wavelength = energy2wavelength(energy)
angular_sampling = tuple(d * 1e3 * wavelength for d in reciprocal_space_sampling)
angular_sampling = cast(T, angular_sampling)
return angular_sampling
[docs]
class EnergyUndefinedError(Exception):
"""
Error raised when energy is not defined.
"""
[docs]
def resolve_energy(
energy: Optional[float],
metadata: Optional[dict],
ensemble_axes_metadata: Sequence,
) -> Optional[float]:
"""Resolve a scalar energy [eV] shared by ``Waves``, ``_energy_from_waves``
and ``BaseMeasurements._get_energy``.
Resolution order:
1. ``energy`` if not None (e.g. ``accelerator.energy`` for a ``Waves``-like
object).
2. ``metadata["energy"]`` -- populated by ``EnergyAxis.item_metadata`` when
the object was produced by indexing an energy-ensemble.
3. A single-element ``EnergyAxis`` in ``ensemble_axes_metadata`` -- the
residual case during per-member partitioned computation.
Returns
-------
float or None
None if no scalar energy can be resolved this way -- notably for a
full, un-indexed multi-energy ensemble. Callers decide what to do
about that case (raise, return None, or fall back to an approximate
value).
"""
from abtem.core.axes import EnergyAxis
if energy is not None:
return float(energy)
energy = metadata.get("energy") if metadata else None
if energy is not None:
return float(energy)
for axis in ensemble_axes_metadata:
if isinstance(axis, EnergyAxis) and len(axis.values) == 1:
return float(axis.values[0])
return None
[docs]
class Accelerator(EqualityMixin, CopyMixin):
"""
Accelerator object describes the energy of wave functions and transfer functions.
Parameters
----------
energy: float
Acceleration energy [eV].
"""
def __init__(self, energy: Optional[float] = None, lock_energy: bool = False):
if energy is not None:
energy = float(energy)
self._energy = energy
self._lock_energy = lock_energy
@property
def energy(self) -> float | None:
"""
Electron acceleration energy [eV].
"""
return self._energy
@energy.setter
def energy(self, value: float):
if self._lock_energy:
raise RuntimeError("Energy cannot be modified")
if value is not None:
value = float(value)
self._energy = value
@property
def wavelength(self) -> float:
"""
Relativistic wavelength [Å].
"""
self.check_is_defined()
assert self.energy is not None
return energy2wavelength(self.energy)
@property
def sigma(self) -> float:
"""Interaction parameter."""
self.check_is_defined()
assert self.energy is not None
return energy2sigma(self.energy)
[docs]
def check_is_defined(self):
"""
Raise error if the energy is not defined.
"""
if self.energy is None:
raise EnergyUndefinedError("Energy is not defined")
[docs]
def check_match(self, other: Accelerator | HasAcceleratorMixin):
"""
Raise error if the accelerator of another object is different from this object.
Parameters
----------
other: Accelerator object
The accelerator that should be checked.
"""
# Use accelerator.energy directly for HasAcceleratorMixin subclasses that may
# override .energy to return non-scalar types (e.g. EnergyEnsemble).
other_energy = (
other.energy
if isinstance(other, Accelerator)
else other.accelerator.energy
)
if (
(self.energy is not None)
and (other_energy is not None)
and (self.energy != other_energy)
):
raise RuntimeError("Inconsistent energies")
[docs]
def match(
self, other: Accelerator | HasAcceleratorMixin, check_match: bool = False
):
"""
Set the parameters of this accelerator to match another accelerator.
Parameters
----------
other: Accelerator object
The accelerator that should be matched.
check_match: bool
If true check whether accelerators can match without overriding an already
defined energy.
"""
if check_match:
self.check_match(other)
# Use accelerator.energy directly for HasAcceleratorMixin subclasses that may
# override .energy to return non-scalar types (e.g. EnergyEnsemble).
other_energy = (
other.energy
if isinstance(other, Accelerator)
else other.accelerator.energy
)
if other_energy is None:
if isinstance(other, Accelerator):
other.energy = self.energy
else:
other.accelerator.energy = self.energy
elif self.energy != other_energy:
self.energy = other_energy
[docs]
class HasAcceleratorMixin:
"""
Mixin class for objects that have an electron energy.
"""
_accelerator: Accelerator
@property
def accelerator(self) -> Accelerator:
"""
Accelerator object describing the acceleration energy.
"""
return self._accelerator
@accelerator.setter
def accelerator(self, new: Accelerator):
self._accelerator = new
@property
def energy(self) -> float | None:
"""
Electron acceleration energy in electron volts.
"""
return self.accelerator.energy
@energy.setter
def energy(self, energy: float):
self.accelerator.energy = energy
@property
def _valid_energy(self) -> float:
"""
Electron acceleration energy in electron volts.
Raises
------
EnergyUndefinedError
If the energy is not defined.
"""
if self.energy is None:
raise EnergyUndefinedError("Energy is not defined")
return self.energy
@property
def wavelength(self) -> float:
"""
Relativistic wavelength in Ångstrom.
"""
return self.accelerator.wavelength