# -*- coding: utf-8 -*-
# Copyright 2024 Matthew Fitzpatrick.
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, version 3.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
r"""Contains classes representing :math:`k`-space functions of probes.
"""
#####################################
## Load libraries/packages/modules ##
#####################################
# For using special math functions and constants.
import numpy as np
# For defining classes that support enforced validation, updatability,
# pre-serialization, and de-serialization.
import fancytypes
# For calculating the electron beam wavelength; validating, pre-serializing, and
# de-pre-serializing instances of :class:`embeam.stem.probe.ModelParams`;
# temporarily disabling chromatic aberrations; and constructing instances of the
# class :class:`embeam.coherent.PhaseDeviation`.
import embeam
##################################
## Define classes and functions ##
##################################
# List of public objects in objects.
__all__ = ["Wavefunction",
"Intensity"]
def _check_and_convert_coherent_probe_model_params(params):
module_alias = embeam.stem.probe
func_alias = module_alias._check_and_convert_coherent_probe_model_params
probe_model_params = func_alias(params)
return probe_model_params
def _pre_serialize_probe_model_params(probe_model_params):
obj_to_pre_serialize = probe_model_params
module_alias = embeam.stem.probe
func_alias = module_alias._pre_serialize_probe_model_params
serializable_rep = func_alias(obj_to_pre_serialize)
return serializable_rep
def _de_pre_serialize_probe_model_params(serializable_rep):
module_alias = embeam.stem.probe
func_alias = module_alias._de_pre_serialize_probe_model_params
probe_model_params = func_alias(serializable_rep)
return probe_model_params
def _check_and_convert_cartesian_coords(params):
module_alias = embeam.coherent
func_alias = module_alias._check_and_convert_cartesian_coords
cartesian_coords = func_alias(params)
return cartesian_coords
def _check_and_convert_skip_validation_and_conversion(params):
module_alias = embeam
func_alias = module_alias._check_and_convert_skip_validation_and_conversion
skip_validation_and_conversion = func_alias(params)
return skip_validation_and_conversion
_module_alias = \
embeam.coherent
_default_probe_model_params = \
None
_default_k_x = \
_module_alias._default_k_x
_default_k_y = \
_module_alias._default_k_y
_default_skip_validation_and_conversion = \
_module_alias._default_skip_validation_and_conversion
[docs]class Wavefunction(fancytypes.PreSerializableAndUpdatable):
r"""The :math:`k`-space wavefunction of a coherent probe.
The :math:`k`-space wavefunction of a coherent probe is well-described by
the model given by
Eq. :eq:`coherent_Phi_probe_in_stem_probe_model_params__1`.
See the documentation for the class :class:`embeam.stem.probe.ModelParams`
for further discussion on probe modelling.
Parameters
----------
probe_model_params : :class:`embeam.stem.probe.ModelParams` | `None`, optional
The model parameters of the coherent probe. If ``probe_model_params`` is
set to ``None`` [i.e. the default value], then the parameter will be
reassigned to the value ``embeam.stem.probe.ModelParams()``.
skip_validation_and_conversion : `bool`, optional
Let ``validation_and_conversion_funcs`` and ``core_attrs`` denote the
attributes :attr:`~fancytypes.Checkable.validation_and_conversion_funcs`
and :attr:`~fancytypes.Checkable.core_attrs` respectively, both of which
being `dict` objects.
Let ``params_to_be_mapped_to_core_attrs`` denote the `dict`
representation of the constructor parameters excluding the parameter
``skip_validation_and_conversion``, where each `dict` key ``key`` is a
different constructor parameter name, excluding the name
``"skip_validation_and_conversion"``, and
``params_to_be_mapped_to_core_attrs[key]`` would yield the value of the
constructor parameter with the name given by ``key``.
If ``skip_validation_and_conversion`` is set to ``False``, then for each
key ``key`` in ``params_to_be_mapped_to_core_attrs``,
``core_attrs[key]`` is set to ``validation_and_conversion_funcs[key]
(params_to_be_mapped_to_core_attrs)``.
Otherwise, if ``skip_validation_and_conversion`` is set to ``True``,
then ``core_attrs`` is set to
``params_to_be_mapped_to_core_attrs.copy()``. This option is desired
primarily when the user wants to avoid potentially expensive deep copies
and/or conversions of the `dict` values of
``params_to_be_mapped_to_core_attrs``, as it is guaranteed that no
copies or conversions are made in this case.
"""
ctor_param_names = ("probe_model_params",)
kwargs = {"namespace_as_dict": globals(),
"ctor_param_names": ctor_param_names}
_validation_and_conversion_funcs_ = \
{"probe_model_params": _check_and_convert_coherent_probe_model_params}
_pre_serialization_funcs_ = \
fancytypes.return_pre_serialization_funcs(**kwargs)
_de_pre_serialization_funcs_ = \
fancytypes.return_de_pre_serialization_funcs(**kwargs)
del ctor_param_names, kwargs
def __init__(self,
probe_model_params=\
_default_probe_model_params,
skip_validation_and_conversion=\
_default_skip_validation_and_conversion):
ctor_params = {key: val
for key, val in locals().items()
if (key not in ("self", "__class__"))}
kwargs = ctor_params
kwargs["skip_cls_tests"] = True
fancytypes.PreSerializableAndUpdatable.__init__(self, **kwargs)
self.execute_post_core_attrs_update_actions()
return None
def execute_post_core_attrs_update_actions(self):
self_core_attrs = self.get_core_attrs(deep_copy=False)
probe_model_params = \
self_core_attrs["probe_model_params"]
probe_model_params_core_attrs = \
probe_model_params.get_core_attrs(deep_copy=False)
convergence_semiangle = \
probe_model_params_core_attrs["convergence_semiangle"]
lens_model_params = \
probe_model_params_core_attrs["lens_model_params"]
gun_model_params = \
probe_model_params_core_attrs["gun_model_params"]
lens_model_params_core_attrs = \
lens_model_params.get_core_attrs(deep_copy=False)
coherent_aberrations = \
lens_model_params_core_attrs["coherent_aberrations"]
gun_model_params_core_attrs = \
gun_model_params.get_core_attrs(deep_copy=False)
beam_energy = \
gun_model_params_core_attrs["mean_beam_energy"]
kwargs = {"beam_energy": beam_energy,
"coherent_aberrations": coherent_aberrations,
"defocal_offset": 0,
"skip_validation_and_conversion": True}
phase_deviation = embeam.coherent.PhaseDeviation(**kwargs)
del kwargs["coherent_aberrations"]
del kwargs["defocal_offset"]
wavelength = embeam.wavelength(**kwargs)
self._k_xy_max = (convergence_semiangle / 1000) / wavelength
self._C = 1 / np.sqrt(np.pi * self._k_xy_max**2)
is_azimuthally_symmetric = phase_deviation.is_azimuthally_symmetric
self._phase_deviation = phase_deviation
self._is_azimuthally_symmetric = is_azimuthally_symmetric
return None
[docs] @classmethod
def get_validation_and_conversion_funcs(cls):
validation_and_conversion_funcs = \
cls._validation_and_conversion_funcs_.copy()
return validation_and_conversion_funcs
[docs] @classmethod
def get_pre_serialization_funcs(cls):
pre_serialization_funcs = \
cls._pre_serialization_funcs_.copy()
return pre_serialization_funcs
[docs] @classmethod
def get_de_pre_serialization_funcs(cls):
de_pre_serialization_funcs = \
cls._de_pre_serialization_funcs_.copy()
return de_pre_serialization_funcs
[docs] def update(self,
new_core_attr_subset_candidate,
skip_validation_and_conversion=\
_default_skip_validation_and_conversion):
super().update(new_core_attr_subset_candidate,
skip_validation_and_conversion)
self.execute_post_core_attrs_update_actions()
return None
[docs] def eval(self,
k_x=\
_default_k_x,
k_y=\
_default_k_y,
skip_validation_and_conversion=\
_default_skip_validation_and_conversion):
r"""Evaluate the :math:`k`-space wavefunction of the coherent probe.
This method evaluates
Eq. :eq:`coherent_Phi_probe_in_stem_probe_model_params__1`.
Parameters
----------
k_x : `array_like` (`float`), optional
The horizontal Fourier coordinates, in units of 1/Å, of the Fourier
coordinate pairs at which to evaluate the wavefunction.
k_y : `array_like` (`float`, shape=``k_x.shape``), optional
The vertical Fourier coordinates, in units of 1/Å, of the Fourier
coordinate pairs at which to evaluate the wavefunction.
skip_validation_and_conversion : `bool`, optional
If ``skip_validation_and_conversion`` is set to ``False``, then
validations and conversions are performed on the above parameters.
Otherwise, if ``skip_validation_and_conversion`` is set to ``True``,
no validations and conversions are performed on the above
parameters. This option is desired primarily when the user wants to
avoid potentially expensive validation and/or conversion operations.
Returns
-------
result : `array_like` (`complex`, shape=``k_x.shape``)
The values of the wavefunction at the Fourier coordinate pairs
specified by ``k_x`` and ``k_y``. For every tuple of nonnegative
integers ``indices`` that does not raise an ``IndexError`` exception
upon calling ``result[indices]``, ``result[indices]`` is the value
of the wavefunction for the Fourier coordinate pair ``(k_x[indices],
k_y[indices])``.
"""
params = {key: val
for key, val in locals().items()
if (key not in ("self", "__class__"))}
func_alias = _check_and_convert_skip_validation_and_conversion
skip_validation_and_conversion = func_alias(params)
if (skip_validation_and_conversion == False):
params = {"cartesian_coords": (k_x, k_y), "units": "1/Å"}
k_x, k_y = _check_and_convert_cartesian_coords(params)
result = self._eval_with_heaviside(k_x, k_y)
return result
def _eval_with_heaviside(self, k_x, k_y):
k_xy = np.sqrt(k_x*k_x + k_y*k_y)
Theta = np.heaviside(self._k_xy_max - k_xy, 1)
result = Theta * self._eval_without_heaviside(k_x, k_y)
return result
def _eval_without_heaviside(self, k_x, k_y):
chi = self._phase_deviation._eval(k_x, k_y)
result = self._C * np.exp(-1j * chi)
return result
@property
def is_azimuthally_symmetric(self):
r"""`bool`: A boolean variable indicating whether the probe model is
azimuthally symmetric.
See the summary documentation of the classes
:class:`embeam.coherent.PhaseDeviation`,
:class:`embeam.coherent.Aberration`, and
:class:`embeam.stem.probe.ModelParams` for additional context.
A probe model is azimuthally symmetric if the coherent aberrations of
the probe-forming lens are azimuthally symmetric, i.e. if if
:math:`\sum_{m=0}^{\infty}\sum_{n=0}^{\infty} n
\left(C_{m,n}^{\text{mag}} C_{m,n}^{\text{ang}}\right)^2=0`.
Note that ``is_azimuthally_symmetric`` should be considered
**read-only**.
"""
result = self._is_azimuthally_symmetric
return result
def _check_and_convert_probe_model_params(params):
module_alias = embeam.stem.probe
func_alias = module_alias._check_and_convert_probe_model_params
probe_model_params = func_alias(params)
return probe_model_params
[docs]class Intensity(fancytypes.PreSerializableAndUpdatable):
r"""The :math:`k`-space fractional intensity of a probe.
In scenarios where the fluctuations over time in the electron beam energy
are either small or non-existent, the :math:`k`-space fractional intensity
of the probe is well-described by
Eq. :eq:`incoherent_k_space_p_probe_in_stem_probe_model_params__1`.
See the documentation for the class :class:`embeam.stem.probe.ModelParams`
for further discussion on probe modelling.
Parameters
----------
probe_model_params : :class:`embeam.stem.probe.ModelParams` | `None`, optional
The model parameters of the probe. If ``probe_model_params`` is set to
``None`` [i.e. the default value], then the parameter will be
reassigned to the value ``embeam.stem.probe.ModelParams()``.
skip_validation_and_conversion : `bool`, optional
Let ``validation_and_conversion_funcs`` and ``core_attrs`` denote the
attributes :attr:`~fancytypes.Checkable.validation_and_conversion_funcs`
and :attr:`~fancytypes.Checkable.core_attrs` respectively, both of which
being `dict` objects.
Let ``params_to_be_mapped_to_core_attrs`` denote the `dict`
representation of the constructor parameters excluding the parameter
``skip_validation_and_conversion``, where each `dict` key ``key`` is a
different constructor parameter name, excluding the name
``"skip_validation_and_conversion"``, and
``params_to_be_mapped_to_core_attrs[key]`` would yield the value of the
constructor parameter with the name given by ``key``.
If ``skip_validation_and_conversion`` is set to ``False``, then for each
key ``key`` in ``params_to_be_mapped_to_core_attrs``,
``core_attrs[key]`` is set to ``validation_and_conversion_funcs[key]
(params_to_be_mapped_to_core_attrs)``.
Otherwise, if ``skip_validation_and_conversion`` is set to ``True``,
then ``core_attrs`` is set to
``params_to_be_mapped_to_core_attrs.copy()``. This option is desired
primarily when the user wants to avoid potentially expensive deep copies
and/or conversions of the `dict` values of
``params_to_be_mapped_to_core_attrs``, as it is guaranteed that no
copies or conversions are made in this case.
"""
ctor_param_names = ("probe_model_params",)
kwargs = {"namespace_as_dict": globals(),
"ctor_param_names": ctor_param_names}
_validation_and_conversion_funcs_ = \
fancytypes.return_validation_and_conversion_funcs(**kwargs)
_pre_serialization_funcs_ = \
fancytypes.return_pre_serialization_funcs(**kwargs)
_de_pre_serialization_funcs_ = \
fancytypes.return_de_pre_serialization_funcs(**kwargs)
del ctor_param_names, kwargs
def __init__(self,
probe_model_params=\
_default_probe_model_params,
skip_validation_and_conversion=\
_default_skip_validation_and_conversion):
ctor_params = {key: val
for key, val in locals().items()
if (key not in ("self", "__class__"))}
kwargs = ctor_params
kwargs["skip_cls_tests"] = True
fancytypes.PreSerializableAndUpdatable.__init__(self, **kwargs)
self.execute_post_core_attrs_update_actions()
return None
def execute_post_core_attrs_update_actions(self):
self_core_attrs = self.core_attrs
probe_model_params = self_core_attrs["probe_model_params"]
is_azimuthally_symmetric = probe_model_params.is_azimuthally_symmetric
is_coherent = probe_model_params.is_coherent
self._is_azimuthally_symmetric = is_azimuthally_symmetric
self._is_coherent = is_coherent
kwargs = self_core_attrs
embeam.stem.probe._disable_chromatic_aberrations(**kwargs)
kwargs["skip_validation_and_conversion"] = True
self._kspace_wavefunction = Wavefunction(**kwargs)
return None
[docs] @classmethod
def get_validation_and_conversion_funcs(cls):
validation_and_conversion_funcs = \
cls._validation_and_conversion_funcs_.copy()
return validation_and_conversion_funcs
[docs] @classmethod
def get_pre_serialization_funcs(cls):
pre_serialization_funcs = \
cls._pre_serialization_funcs_.copy()
return pre_serialization_funcs
[docs] @classmethod
def get_de_pre_serialization_funcs(cls):
de_pre_serialization_funcs = \
cls._de_pre_serialization_funcs_.copy()
return de_pre_serialization_funcs
[docs] def update(self,
new_core_attr_subset_candidate,
skip_validation_and_conversion=\
_default_skip_validation_and_conversion):
super().update(new_core_attr_subset_candidate,
skip_validation_and_conversion)
self.execute_post_core_attrs_update_actions()
return None
[docs] def eval(self,
k_x=\
_default_k_x,
k_y=\
_default_k_y,
skip_validation_and_conversion=\
_default_skip_validation_and_conversion):
r"""Evaluate the :math:`k`-space fractional intensity of the probe.
This method evaluates
Eq. :eq:`incoherent_k_space_p_probe_in_stem_probe_model_params__1`.
Parameters
----------
k_x : `array_like` (`float`), optional
The horizontal Fourier coordinates, in units of 1/Å, of the Fourier
coordinate pairs at which to evaluate the fractional intensity.
k_y : `array_like` (`float`, shape=``k_x.shape``), optional
The vertical Fourier coordinates, in units of 1/Å, of the Fourier
coordinate pairs at which to evaluate the fractional intensity.
skip_validation_and_conversion : `bool`, optional
If ``skip_validation_and_conversion`` is set to ``False``, then
validations and conversions are performed on the above parameters.
Otherwise, if ``skip_validation_and_conversion`` is set to ``True``,
no validations and conversions are performed on the above
parameters. This option is desired primarily when the user wants to
avoid potentially expensive validation and/or conversion operations.
Returns
-------
result : `array_like` (`float`, shape=``k_x.shape``)
The values of the fractional intensity at the Fourier coordinate
pairs specified by ``k_x`` and ``k_y``. For every tuple of
nonnegative integers ``indices`` that does not raise an
``IndexError`` exception upon calling ``result[indices]``,
``result[indices]`` is the value of the fractional intensity for the
Fourier coordinate pair ``(k_x[indices], k_y[indices])``.
"""
params = {key: val
for key, val in locals().items()
if (key not in ("self", "__class__"))}
func_alias = _check_and_convert_skip_validation_and_conversion
skip_validation_and_conversion = func_alias(params)
if (skip_validation_and_conversion == False):
params = {"cartesian_coords": (k_x, k_y), "units": "1/Å"}
k_x, k_y = _check_and_convert_cartesian_coords(params)
result = self._eval_with_heaviside(k_x, k_y)
return result
def _eval_with_heaviside(self, k_x, k_y):
kspace_wavefunction = self._kspace_wavefunction
temp = np.abs(kspace_wavefunction._eval_with_heaviside(k_x, k_y))
result = temp * temp
return result
@property
def is_azimuthally_symmetric(self):
r"""`bool`: A boolean variable indicating whether the probe model is
azimuthally symmetric.
See the summary documentation of the classes
:class:`embeam.coherent.PhaseDeviation`,
:class:`embeam.coherent.Aberration`, and
:class:`embeam.stem.probe.ModelParams` for additional context.
A probe model is azimuthally symmetric if the coherent aberrations of
the probe-forming lens are azimuthally symmetric, i.e. if if
:math:`\sum_{m=0}^{\infty}\sum_{n=0}^{\infty} n
\left(C_{m,n}^{\text{mag}} C_{m,n}^{\text{ang}}\right)^2=0`.
Note that ``is_azimuthally_symmetric`` should be considered
**read-only**.
"""
result = self._is_azimuthally_symmetric
return result
@property
def is_coherent(self):
r"""`bool`: A boolean variable indicating whether the probe model is
coherent.
See the summary documentation of the class
:class:`embeam.stem.probe.ModelParams` for additional context.
If ``is_coherent`` is set to ``True``, then the probe model is
coherent. Otherwise, the probe model is not coherent.
Note that ``is_coherent`` should be considered **read-only**.
"""
result = self._is_coherent
return result
###########################
## Define error messages ##
###########################