Source code for pyRadPlan.dose.engines._hongpb
from typing import ClassVar
import array_api_compat
from ._base_pencilbeam_particle import ParticlePencilBeamEngineAbstract
[docs]
class ParticleHongPencilBeamEngine(ParticlePencilBeamEngineAbstract):
# constants
short_name = "HongPB"
name = "Hong Particle Pencil-Beam"
possible_radiation_modes = ["protons", "helium", "carbon", "VHEE"]
_dij_guarantee_canonical: ClassVar[bool] = True
_dij_guarantee_nonzero: ClassVar[bool] = True
# private methods
def _calc_particle_bixel(self, bixel):
kernels = self._interpolate_kernels_in_depth(bixel)
xp = array_api_compat.array_namespace(bixel["radial_dist_sq"])
pb_kernel = bixel[
"kernel"
] # {"lateral_cut_off":{"comp_fac": xp.asarray(bixel["kernel"]["lateral_cut_off"]["comp_fac"])}}
# Lateral Component
if self.lateral_model == "single":
# Compute lateral sigma
sigma_sq = kernels["sigma"] ** 2 + bixel["sigma_ini_sq"]
lateral = xp.exp(-bixel["radial_dist_sq"] / (2 * sigma_sq)) / (2 * xp.pi * sigma_sq)
elif self.lateral_model == "double":
# Compute lateral sigmas
sigma_sq_narrow = kernels["sigma_1"] ** 2 + bixel["sigma_ini_sq"]
sigma_sq_broad = kernels["sigma_2"] ** 2 + bixel["sigma_ini_sq"]
# Calculate lateral profile
l_narr = xp.exp(-bixel["radial_dist_sq"] / (2 * sigma_sq_narrow)) / (
2 * xp.pi * sigma_sq_narrow
)
l_bro = xp.exp(-bixel["radial_dist_sq"] / (2 * sigma_sq_broad)) / (
2 * xp.pi * sigma_sq_broad
)
lateral = (1 - kernels["weight"]) * l_narr + kernels["weight"] * l_bro
elif self.lateral_model == "multi":
sigma_sq = kernels["sigma_multi"] ** 2 + bixel["sigma_ini_sq"]
lateral = xp.sum(
(
xp.column_stack(
(1 - xp.sum(kernels["weight_multi"], axis=1), kernels["weight_multi"])
)
* xp.exp(-bixel["radial_dist_sq"][:, xp.newaxis] / (2 * sigma_sq))
/ (2 * xp.pi * sigma_sq)
),
axis=1,
)
elif self.lateral_model == "singleXY":
# Extract squared distances in the two lateral axes
x_sq = bixel["lat_dists"][:, 0] ** 2
y_sq = bixel["lat_dists"][:, 1] ** 2
sigma_sq_x = kernels["sigma_x"] ** 2 + bixel["sigma_ini_sq"]
sigma_sq_y = kernels["sigma_y"] ** 2 + bixel["sigma_ini_sq"]
sigma_x = xp.sqrt(sigma_sq_x)
sigma_y = xp.sqrt(sigma_sq_y)
# Anisotropic 2D Gaussian
lateral = xp.exp(-(x_sq / (2 * sigma_sq_x) + y_sq / (2 * sigma_sq_y))) / (
2 * xp.pi * sigma_x * sigma_y
)
else:
raise ValueError("Invalid Lateral Model")
bixel["physical_dose"] = (
pb_kernel["lateral_cut_off"]["comp_fac"] * lateral * kernels["idd"]
)
# Check if we have valid dose values
if xp.any(xp.isnan(bixel["physical_dose"])) or xp.any(bixel["physical_dose"] < 0):
raise ValueError("Error in particle dose calculation.")
if self.calc_let:
bixel["let_dose"] = bixel["physical_dose"] * kernels["let"]
if self.calc_bio_dose:
# TODO: correct / adaptive alpha / beta values given tissue indices
bixel_alpha = kernels["alpha"][0]
bixel_beta = kernels["beta"][0]
# Multiple with dose
bixel["alpha_dose"] = bixel["physical_dose"] * bixel_alpha
bixel["sqrt_beta_dose"] = bixel["physical_dose"] * xp.sqrt(bixel_beta)
[docs]
@staticmethod
def is_available(pln, machine):
available, msg = [], []
return available, msg