Source code for corrct.physics

"""Physics module."""

__author__ = """Nicola VIGANĂ’"""
__email__ = "N.R.Vigano@cwi.nl"


import matplotlib.pyplot as plt
import numpy as np
from numpy.typing import NDArray

from . import attenuation  # noqa: F401, F402
from . import materials  # noqa: F401, F402
from . import phase  # noqa: F401, F402
from . import xraylib_helper  # noqa: F401, F402
from . import xrf  # noqa: F401, F402
from . import units

xraylib = xraylib_helper.xraylib
get_compound = xraylib_helper.get_compound
get_element_number = xraylib_helper.get_element_number

FluoLinesSiegbahn = xrf.LinesSiegbahn
VolumeMaterial = materials.VolumeMaterial

convert_energy_to_wlength = units.energy_to_wlength
convert_wlength_to_energy = units.wlength_to_energy


[docs] def pencil_beam_profile( voxel_size_um: float, beam_fwhm_um: float, profile_size: int = 1, beam_shape: str = "gaussian", verbose: bool = False ) -> NDArray: """ Compute the pencil beam integration point spread function. Parameters ---------- voxel_size_um : float The integration length. beam_fwhm_um : float The beam FWHM. profile_size : int, optional The number of pixels of the PSF. The default is 1. verbose : bool, optional Whether to print verbose information. The default is False. Returns ------- NDArray The beam PSF. """ y_points = profile_size * 2 + 1 extent_um = y_points * voxel_size_um num_points = int(np.ceil(extent_um * 10) // 2) * 2 + 1 half_voxel_size_um = voxel_size_um / 2 eps = np.finfo(np.float32).eps xc = np.linspace(-extent_um / 2, extent_um / 2, num_points) # Box beam shape yb = np.abs(xc) < half_voxel_size_um yb[np.abs(np.abs(xc) - half_voxel_size_um) <= eps] = 0.5 # Gaussian beam shape if beam_shape.lower() == "gaussian": yg = np.exp(-4 * np.log(2) * (xc**2) / (beam_fwhm_um**2)) elif beam_shape.lower() == "lorentzian": beam_hwhm_um = beam_fwhm_um / 2 yg = (beam_hwhm_um**2) / (xc**2 + beam_hwhm_um**2) elif beam_shape.lower() == "sech**2": # doi: 10.1364/ol.20.001160 tau = beam_fwhm_um / (2 * np.arccosh(np.sqrt(2))) yg = 1 / np.cosh(xc / tau) ** 2 else: raise ValueError(f"Unknown beam shape: {beam_shape.lower()}") yc = np.convolve(yb, yg, mode="same") yc = yc / np.max(yc) y = np.zeros((y_points,)) for ii_p in range(y_points): # Finding the region that overlaps with the given adjacent voxel voxel_center_um = (ii_p - profile_size) * voxel_size_um yp = np.abs(xc - voxel_center_um) < half_voxel_size_um yp[(np.abs(xc - voxel_center_um) - half_voxel_size_um) < eps] = 0.5 y[ii_p] = np.sum(yc * yp) # Renormalization y /= y.sum() if verbose: fig, ax = plt.subplots(1, 2, figsize=[10, 5]) ax[0].plot(xc, yb, label="Integration length") # type: ignore ax[0].plot(xc, yg, label=f"{beam_shape.capitalize()} beam shape") # type: ignore ax[0].plot(xc, yc, label="Resulting beam shape") # type: ignore ax[0].legend() # type: ignore ax[0].grid() # type: ignore pixels_pos = np.linspace(-(y_points - 1) / 2, (y_points - 1) / 2, y_points) ax[1].bar(pixels_pos, y, label="PSF values", color="C1", width=1, linewidth=1.5, edgecolor="k", alpha=0.75) # type: ignore ax[1].plot(xc / extent_um * profile_size * 2, yc, label="Resulting beam shape") # type: ignore ax[1].legend() # type: ignore ax[1].grid() # type: ignore fig.tight_layout() return y