Source code for pyRadPlan.machines.particles._ions
from typing_extensions import Self
from typing import ClassVar, List, Optional
from pydantic import Field
# import warnings
import numpy as np
from pydantic import (
model_validator,
ValidationError,
)
from numpydantic import NDArray, Shape
from ._base import ParticleAccelerator
from .kernel import IonPencilBeamKernel
from pyRadPlan.util.helpers import dl2ld
[docs]
class IonAccelerator(ParticleAccelerator):
"""Machine Model for Ion Accelerators.
Defines minimum meta-data an ion machine must hold
Provides multiple data storage formats, currently supported
- Pencil-Beam Kernels
- Monte Carlo Beam Models.
Attributes
----------
sad : float
The source-to-axis (-isocenter) distance of the machine
"""
# Annotated overrides for pydantic fields
radiation_mode: str = Field(
default="protons", pattern="^(protons|helium|carbon|oxygen)$", validate_default=True
)
_possible_radiation_modes: ClassVar[List[str]] = ["protons", "helium", "carbon", "oxygen"]
peak_positions: NDArray[Shape["1-*"], np.float64]
pb_kernels: Optional[dict[float, IonPencilBeamKernel]] = None
@classmethod
def _parse_tabulated_energy_data_from_mat(
cls, tabulated_energy_data: dict, returned_data: dict
):
"""Parse the tabulated energy data from a matRad machine file."""
super(IonAccelerator, cls)._parse_tabulated_energy_data_from_mat(
tabulated_energy_data, returned_data
)
# extract required quantities
if "offset" in tabulated_energy_data:
returned_data["peak_positions"] = np.array(
tabulated_energy_data["peakPos"], dtype=np.float64
) + np.array(tabulated_energy_data["offset"], dtype=np.float64)
# Extract the pencil beam kernels
# Pencil beam kernels are not required so we may skip them
# if they can't be validated
try:
tmp = dl2ld(tabulated_energy_data)
returned_data["pb_kernels"] = {
entry["energy"]: IonPencilBeamKernel(**entry) for entry in tmp
}
except ValidationError:
returned_data["pb_kernels"] = None
@model_validator(mode="after")
def _check_machine(self) -> Self:
"""Validate the machine model for consistency."""
if self.bams_to_iso_dist > self.sad:
raise ValueError("BAMS to iso distance must be small than SAD.")
if self.pb_kernels is not None:
# Check that the peak_positions are consistent between peak_positions of the model
# and the kernel peak positions with offset applied
for i, energy in enumerate(self.energies):
kernel = self.pb_kernels[energy]
if not np.isclose(self.peak_positions[i], kernel.peak_pos + kernel.offset):
raise ValueError(
f"Peak position of the model and the kernel data for energy {energy} "
"are inconsistent."
)
return self