Skip to content

Commit 43b194e

Browse files
committed
Merge branch 'develop' of git.dkfz.de:e040/e0404/pyRadPlan into develop
2 parents 5dbae64 + a25e22b commit 43b194e

16 files changed

Lines changed: 1806 additions & 41 deletions
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
# %% [markdown]
2+
"""# Example for photon dose calculation using pencilbeam engine."""
3+
# %% [markdown]
4+
# This example demonstrates how to use the pyRadPlan library to perform photon dose calculations.
5+
6+
# To display this script in a Jupyter Notebook, you need to install jupytext via pip and run the following command.
7+
# This will create a .ipynb file in the same directory:
8+
9+
# ```bash
10+
# pip install jupytext
11+
# jupytext --to notebook path/to/this/file/pencilbeam_photon_forward.py
12+
13+
# %%
14+
# Import necessary libraries
15+
import logging
16+
17+
import numpy as np
18+
19+
import matplotlib.pyplot as plt
20+
21+
from pyRadPlan import (
22+
PhotonPlan,
23+
generate_stf,
24+
calc_dose_forward,
25+
plot_slice,
26+
load_tg119,
27+
)
28+
29+
from pyRadPlan.machines import create_bld
30+
from pyRadPlan.stf import FieldShapeAsBLD, FieldShapeComposite
31+
32+
from pyRadPlan.machines.photons._calculate_machine_scale import calculate_machine_scale
33+
34+
logging.basicConfig(level=logging.INFO)
35+
36+
37+
resolution = 1.0
38+
39+
leaf_width = 5
40+
positions = [ # L-form for testing direction
41+
[0, 0],
42+
[-20, 20],
43+
[-20, 10],
44+
[-20, 0],
45+
[-20, 0],
46+
[-20, 0],
47+
[-20, 0],
48+
[-20, 0],
49+
[-20, 0],
50+
[0, 0],
51+
]
52+
number_of_elements = len(positions)
53+
boundaries = np.arange(
54+
-int(number_of_elements / 2) * leaf_width, int(number_of_elements / 2) * leaf_width, leaf_width
55+
)
56+
57+
mlc_information = {
58+
"device_type": "MLC",
59+
"device_orientation": "X",
60+
"leaf_position_boundaries": boundaries,
61+
"leaf_positions": positions,
62+
"leaf_width": leaf_width,
63+
"leaf_leakage": 0.1,
64+
}
65+
mlc_x = create_bld(mlc_information)
66+
mask_mlc_x = mlc_x.calculate_transmission_mask(resolution)
67+
68+
jaw_info = {
69+
"device_type": "JAW",
70+
"device_orientation": "X",
71+
"positions": [-20, 10],
72+
"field_width": 70,
73+
"leakage": 0.1,
74+
}
75+
jaw_x = create_bld(jaw_info)
76+
jaw_info["field_width"] = mask_mlc_x.shape[0] * resolution
77+
jaw_x_matching_field_width = create_bld(jaw_info)
78+
mask_jaw_x = jaw_x_matching_field_width.calculate_transmission_mask(resolution)
79+
80+
mask = mask_mlc_x * mask_jaw_x
81+
82+
half_size = jaw_info["field_width"]
83+
extent = [-half_size, half_size, -half_size, half_size]
84+
85+
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
86+
87+
axes[0].imshow(mask_jaw_x, cmap="gray", origin="lower", extent=extent)
88+
axes[0].set_title("Jaw Mask (IEC DICOM)")
89+
axes[0].set_xlabel("X (mm)")
90+
axes[0].set_ylabel("Y (mm)")
91+
92+
axes[1].imshow(mask_mlc_x, cmap="gray", origin="lower", extent=extent)
93+
axes[1].set_title("MLC Mask (IEC DICOM)")
94+
axes[1].set_xlabel("X (mm)")
95+
axes[1].set_ylabel("Y (mm)")
96+
97+
axes[2].imshow(mask, cmap="gray", origin="lower", extent=extent)
98+
axes[2].set_title("Combined Mask (IEC DICOM)")
99+
axes[2].set_xlabel("X (mm)")
100+
axes[2].set_ylabel("Y (mm)")
101+
102+
plt.tight_layout()
103+
plt.show()
104+
105+
# %%
106+
# pyRadPlan internally defines "field shapes" to represent the shape of the beamlets.
107+
mlc_shape = FieldShapeAsBLD(energy=6.0, bld=mlc_x, resolution=resolution)
108+
jaw_shape = FieldShapeAsBLD(energy=6.0, bld=jaw_x_matching_field_width, resolution=resolution)
109+
combined_shape = FieldShapeComposite(energy=6.0, shapes=[mlc_shape, jaw_shape])
110+
111+
# Now plot the field shapes to verify their geometry in LPS BEV coordinates
112+
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
113+
axes[0].imshow(jaw_shape.mask, cmap="gray", origin="lower", extent=extent)
114+
axes[0].set_title("Jaw Field Shape (LPS BEV)")
115+
axes[0].set_xlabel("X (mm)")
116+
axes[0].set_ylabel("Y (mm)")
117+
118+
axes[1].imshow(mlc_shape.mask, cmap="gray", origin="lower", extent=extent)
119+
axes[1].set_title("MLC Field Shape (LPS BEV)")
120+
axes[1].set_xlabel("X (mm)")
121+
axes[1].set_ylabel("Y (mm)")
122+
123+
axes[2].imshow(combined_shape.mask, cmap="gray", origin="lower", extent=extent)
124+
axes[2].set_title("Combined Field Shape (LPS BEV)")
125+
axes[2].set_xlabel("X (mm)")
126+
axes[2].set_ylabel("Y (mm)")
127+
128+
plt.tight_layout()
129+
plt.show()
130+
131+
# %%
132+
# Load TG119 (provided within pyRadPlan)
133+
ct, cst = load_tg119()
134+
135+
# %% [markdown]
136+
# In this section, we create a photon therapy plan using the ParticleHongPencilBeamEngine.
137+
# %%
138+
# Create a plan object
139+
pln = PhotonPlan(machine="Generic")
140+
num_of_beams = 1
141+
pln.prop_stf = {
142+
"gantry_angles": np.linspace(0, 360, num_of_beams, endpoint=False),
143+
"couch_angles": np.zeros((num_of_beams,)), # np.array([0, 270]),
144+
"generator": "photonSingleBixel",
145+
"field_based": True,
146+
# "field_shape": np.rot90(mask, k=-1), #TODO: Rotation correct? Should automatically be done in ShapeFromBLD?
147+
"blds": [mlc_x, jaw_x],
148+
"resolution": 0.5,
149+
"energy": 6,
150+
}
151+
152+
# Generate Steering Geometry ("stf")
153+
stf = generate_stf(ct, cst, pln)
154+
155+
# Machine Calibration: Calculate the conversion factor between MU and weights
156+
machine = "Generic"
157+
ct_dim = [64, 64, 64]
158+
ct_resolution = [2.0, 2.0, 2.0]
159+
num_of_ct_scen = 1
160+
161+
weights = calculate_machine_scale(machine, ct_dim, ct_resolution, num_of_ct_scen)
162+
163+
# Apply weights to the rays in the stf
164+
for beam in stf.beams:
165+
for ray in beam.rays:
166+
for beamlet in ray.beamlets:
167+
beamlet.weight *= weights
168+
169+
# Calculate Dose Influence Matrix ("dij")
170+
dij = calc_dose_forward(ct, cst, stf, pln)
171+
172+
173+
# %% [markdown]
174+
# Visualize the results
175+
# %%
176+
# Choose a slice to visualize
177+
view_slice = int(np.round(ct.size[1] / 2))
178+
179+
# Visualize
180+
plot_slice(
181+
ct=ct,
182+
cst=cst,
183+
overlay=dij["physical_dose"],
184+
view_slice=view_slice,
185+
plane="coronal",
186+
overlay_unit="Gy",
187+
)

pyRadPlan/dose/engines/_base_pencilbeam.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,9 @@ def _init_beam(
390390
start_time = time.time()
391391

392392
self._raytracer.debug_core_performance = True
393+
self._raytracer.lateral_cut_off = beam_info["beam"].get(
394+
"effective_lateral_cut_off", self._effective_lateral_cutoff
395+
)
393396
rad_depth_cubes = self._raytracer.trace_cubes(beam_info["beam"])
394397
self._raytracer.debug_core_performance = False
395398

0 commit comments

Comments
 (0)