"""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