Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Ok-landscape
GitHub Repository: Ok-landscape/computational-pipeline
Path: blob/main/latex-templates/templates/medical-physics/radiation_dosimetry.tex
51 views
unlisted
1
\documentclass[a4paper, 11pt]{report}
2
\usepackage[utf8]{inputenc}
3
\usepackage[T1]{fontenc}
4
\usepackage{amsmath, amssymb}
5
\usepackage{graphicx}
6
\usepackage{siunitx}
7
\usepackage{booktabs}
8
\usepackage{xcolor}
9
\usepackage[makestderr]{pythontex}
10
11
\definecolor{photon}{RGB}{52, 152, 219}
12
\definecolor{electron}{RGB}{231, 76, 60}
13
\definecolor{proton}{RGB}{46, 204, 113}
14
15
\title{Radiation Dosimetry:\\
16
Depth-Dose Distributions and Treatment Planning}
17
\author{Department of Medical Physics\\Technical Report MP-2024-003}
18
\date{\today}
19
20
\begin{document}
21
\maketitle
22
23
\begin{abstract}
24
This report presents a comprehensive analysis of radiation dosimetry for external beam radiotherapy. We compute depth-dose distributions for photon, electron, and proton beams, analyze tissue inhomogeneity corrections, evaluate dose-volume histograms, and compare treatment planning techniques. All calculations use PythonTeX for reproducibility.
25
\end{abstract}
26
27
\tableofcontents
28
29
\chapter{Introduction}
30
31
Radiation dosimetry quantifies energy deposition in tissue. The absorbed dose is defined as:
32
\begin{equation}
33
D = \frac{d\bar{\varepsilon}}{dm}
34
\end{equation}
35
where $d\bar{\varepsilon}$ is the mean energy imparted to matter of mass $dm$.
36
37
\section{Dose Quantities}
38
\begin{itemize}
39
\item Absorbed dose $D$ (Gy): Energy per unit mass
40
\item Kerma $K$ (Gy): Kinetic energy released per unit mass
41
\item Exposure $X$ (R): Ionization in air
42
\end{itemize}
43
44
\begin{pycode}
45
import numpy as np
46
import matplotlib.pyplot as plt
47
from scipy.optimize import curve_fit
48
from scipy.integrate import cumtrapz
49
plt.rc('text', usetex=True)
50
plt.rc('font', family='serif')
51
52
np.random.seed(42)
53
54
# Depth array (cm)
55
depth = np.linspace(0, 35, 350)
56
57
def photon_pdd(d, energy_MV, field_size=10):
58
"""Model photon percent depth dose."""
59
# d_max increases with energy
60
d_max = 0.5 + energy_MV * 0.15
61
# Effective attenuation coefficient
62
mu_eff = 0.05 - 0.002 * energy_MV + 0.001 * (10 - field_size)
63
64
pdd = np.zeros_like(d)
65
buildup = d <= d_max
66
pdd[buildup] = 100 * (0.3 + 0.7 * (d[buildup] / d_max)**0.8)
67
falloff = d > d_max
68
pdd[falloff] = 100 * np.exp(-mu_eff * (d[falloff] - d_max))
69
70
return pdd
71
72
def electron_pdd(d, energy_MeV):
73
"""Model electron percent depth dose."""
74
R_50 = energy_MeV / 2.33 # Depth of 50% dose
75
R_p = energy_MeV / 2.0 # Practical range
76
d_max = 0.46 * energy_MeV**0.6
77
78
pdd = np.zeros_like(d)
79
# Surface dose
80
surface = d < d_max * 0.3
81
pdd[surface] = 75 + 80 * d[surface] / d_max
82
# Buildup
83
buildup = (d >= d_max * 0.3) & (d < d_max)
84
pdd[buildup] = 100 * (d[buildup] / d_max)**0.3
85
# Plateau and falloff
86
plateau = (d >= d_max) & (d < R_50)
87
pdd[plateau] = 100 * np.exp(-0.1 * (d[plateau] - d_max))
88
falloff = d >= R_50
89
pdd[falloff] = 100 * np.exp(-3 * (d[falloff] - R_50)**2 / (R_p - R_50 + 0.1)**2)
90
pdd[d > R_p * 1.1] = 0
91
92
return np.clip(pdd, 0, 100)
93
94
def proton_pdd(d, energy_MeV, sigma=0.3):
95
"""Model proton Bragg peak."""
96
R = 0.0022 * energy_MeV**1.77 # Range in water (cm)
97
98
# Entrance plateau
99
entrance = d < R * 0.8
100
pdd = np.ones_like(d) * 30
101
pdd[entrance] = 30 + 5 * d[entrance] / R
102
103
# Bragg peak
104
peak = (d >= R * 0.8) & (d <= R * 1.1)
105
pdd[peak] = 30 + 70 * np.exp(-((d[peak] - R)**2) / (2 * (sigma * R)**2))
106
107
# Distal falloff
108
distal = d > R * 1.1
109
pdd[distal] = 30 * np.exp(-10 * (d[distal] - R * 1.1) / R)
110
111
# Normalize
112
pdd = pdd / pdd.max() * 100
113
return np.clip(pdd, 0, 100)
114
115
def tar(d, energy_MV, field_size=10, SSD=100):
116
"""Calculate tissue-air ratio."""
117
pdd = photon_pdd(d, energy_MV, field_size)
118
d_max = 0.5 + energy_MV * 0.15
119
return pdd / 100 * ((SSD + d_max) / (SSD + d))**2 * 100
120
121
def tpr(d, energy_MV):
122
"""Calculate tissue-phantom ratio."""
123
d_ref = 10
124
return photon_pdd(d, energy_MV) / photon_pdd(d_ref * np.ones(1), energy_MV)[0] * 100
125
\end{pycode}
126
127
\chapter{Photon Beam Dosimetry}
128
129
\begin{pycode}
130
fig, axes = plt.subplots(2, 3, figsize=(14, 8))
131
132
# PDD for different energies
133
ax = axes[0, 0]
134
energies_MV = [4, 6, 10, 15, 18]
135
colors = plt.cm.viridis(np.linspace(0, 0.8, len(energies_MV)))
136
137
for E, color in zip(energies_MV, colors):
138
pdd = photon_pdd(depth, E)
139
ax.plot(depth, pdd, color=color, linewidth=2, label=f'{E} MV')
140
141
ax.axhline(y=50, color='gray', linestyle='--', alpha=0.5)
142
ax.set_xlabel('Depth (cm)')
143
ax.set_ylabel('Percent Depth Dose (\\%)')
144
ax.set_title('Photon PDD vs Energy')
145
ax.legend()
146
ax.grid(True, alpha=0.3)
147
ax.set_ylim([0, 110])
148
149
# Field size dependence
150
ax = axes[0, 1]
151
field_sizes = [5, 10, 20, 30]
152
colors_fs = plt.cm.plasma(np.linspace(0.2, 0.8, len(field_sizes)))
153
154
for fs, color in zip(field_sizes, colors_fs):
155
pdd = photon_pdd(depth, 6, fs)
156
ax.plot(depth, pdd, color=color, linewidth=2, label=f'{fs} cm')
157
158
ax.set_xlabel('Depth (cm)')
159
ax.set_ylabel('Percent Depth Dose (\\%)')
160
ax.set_title('6 MV PDD vs Field Size')
161
ax.legend()
162
ax.grid(True, alpha=0.3)
163
ax.set_ylim([0, 110])
164
165
# TAR vs TPR
166
ax = axes[0, 2]
167
pdd_6 = photon_pdd(depth, 6)
168
tar_6 = tar(depth, 6)
169
170
ax.plot(depth, pdd_6, 'b-', linewidth=2, label='PDD')
171
ax.plot(depth, tar_6, 'r--', linewidth=2, label='TAR')
172
ax.set_xlabel('Depth (cm)')
173
ax.set_ylabel('Relative Dose (\\%)')
174
ax.set_title('PDD vs TAR (6 MV)')
175
ax.legend()
176
ax.grid(True, alpha=0.3)
177
178
# Characteristic depths
179
ax = axes[1, 0]
180
d_max_vals = []
181
d_80_vals = []
182
d_50_vals = []
183
184
for E in energies_MV:
185
pdd = photon_pdd(depth, E)
186
d_max = 0.5 + E * 0.15
187
d_80 = depth[np.argmin(np.abs(pdd - 80))]
188
d_50 = depth[np.argmin(np.abs(pdd - 50))]
189
d_max_vals.append(d_max)
190
d_80_vals.append(d_80)
191
d_50_vals.append(d_50)
192
193
x = np.arange(len(energies_MV))
194
width = 0.25
195
ax.bar(x - width, d_max_vals, width, label='$d_{max}$', alpha=0.7, color='#3498db')
196
ax.bar(x, d_80_vals, width, label='$d_{80}$', alpha=0.7, color='#2ecc71')
197
ax.bar(x + width, d_50_vals, width, label='$d_{50}$', alpha=0.7, color='#e74c3c')
198
ax.set_xlabel('Photon Energy (MV)')
199
ax.set_ylabel('Depth (cm)')
200
ax.set_title('Characteristic Depths')
201
ax.set_xticks(x)
202
ax.set_xticklabels([str(E) for E in energies_MV])
203
ax.legend()
204
ax.grid(True, alpha=0.3, axis='y')
205
206
# Buildup region
207
ax = axes[1, 1]
208
depth_buildup = np.linspace(0, 5, 100)
209
for E, color in zip([6, 10, 18], ['#3498db', '#2ecc71', '#e74c3c']):
210
pdd = photon_pdd(depth_buildup, E)
211
ax.plot(depth_buildup, pdd, color=color, linewidth=2, label=f'{E} MV')
212
213
ax.set_xlabel('Depth (cm)')
214
ax.set_ylabel('Percent Depth Dose (\\%)')
215
ax.set_title('Buildup Region')
216
ax.legend()
217
ax.grid(True, alpha=0.3)
218
219
# Surface dose vs energy
220
ax = axes[1, 2]
221
surface_doses = [photon_pdd(0, E)[0] if isinstance(photon_pdd(0, E), np.ndarray)
222
else photon_pdd(np.array([0]), E)[0] for E in energies_MV]
223
ax.bar(energies_MV, surface_doses, color='#f39c12', alpha=0.7)
224
ax.set_xlabel('Energy (MV)')
225
ax.set_ylabel('Surface Dose (\\%)')
226
ax.set_title('Surface Dose vs Energy')
227
ax.grid(True, alpha=0.3, axis='y')
228
229
plt.tight_layout()
230
plt.savefig('photon_dosimetry.pdf', dpi=150, bbox_inches='tight')
231
plt.close()
232
\end{pycode}
233
234
\begin{figure}[htbp]
235
\centering
236
\includegraphics[width=0.95\textwidth]{photon_dosimetry.pdf}
237
\caption{Photon dosimetry: (a) energy dependence, (b) field size, (c) PDD vs TAR, (d) characteristic depths, (e) buildup, (f) surface dose.}
238
\end{figure}
239
240
\chapter{Electron and Proton Beams}
241
242
\begin{pycode}
243
fig, axes = plt.subplots(2, 3, figsize=(14, 8))
244
245
# Electron PDD
246
ax = axes[0, 0]
247
energies_MeV = [6, 9, 12, 16, 20]
248
colors_e = plt.cm.Reds(np.linspace(0.4, 0.9, len(energies_MeV)))
249
250
for E, color in zip(energies_MeV, colors_e):
251
pdd = electron_pdd(depth, E)
252
ax.plot(depth, pdd, color=color, linewidth=2, label=f'{E} MeV')
253
254
ax.axhline(y=50, color='gray', linestyle='--', alpha=0.5)
255
ax.set_xlabel('Depth (cm)')
256
ax.set_ylabel('Percent Depth Dose (\\%)')
257
ax.set_title('Electron PDD')
258
ax.legend()
259
ax.grid(True, alpha=0.3)
260
ax.set_xlim([0, 15])
261
ax.set_ylim([0, 110])
262
263
# Proton Bragg peaks
264
ax = axes[0, 1]
265
proton_energies = [100, 150, 200, 230]
266
colors_p = plt.cm.Greens(np.linspace(0.4, 0.9, len(proton_energies)))
267
268
for E, color in zip(proton_energies, colors_p):
269
pdd = proton_pdd(depth, E)
270
ax.plot(depth, pdd, color=color, linewidth=2, label=f'{E} MeV')
271
272
ax.set_xlabel('Depth (cm)')
273
ax.set_ylabel('Percent Depth Dose (\\%)')
274
ax.set_title('Proton Bragg Peaks')
275
ax.legend()
276
ax.grid(True, alpha=0.3)
277
ax.set_ylim([0, 110])
278
279
# SOBP (Spread-out Bragg peak)
280
ax = axes[0, 2]
281
depth_sobp = np.linspace(0, 25, 250)
282
# Create SOBP from weighted sum
283
sobp = np.zeros_like(depth_sobp)
284
weights = [0.8, 0.9, 1.0, 1.1, 1.2]
285
energies_sobp = [180, 190, 200, 210, 220]
286
287
for w, E in zip(weights, energies_sobp):
288
sobp += w * proton_pdd(depth_sobp, E)
289
sobp = sobp / sobp.max() * 100
290
291
pristine = proton_pdd(depth_sobp, 200)
292
ax.plot(depth_sobp, pristine, 'g--', linewidth=1.5, alpha=0.7, label='Pristine')
293
ax.plot(depth_sobp, sobp, 'b-', linewidth=2, label='SOBP')
294
ax.axhline(90, color='r', linestyle=':', alpha=0.5)
295
ax.set_xlabel('Depth (cm)')
296
ax.set_ylabel('Relative Dose (\\%)')
297
ax.set_title('Spread-Out Bragg Peak')
298
ax.legend()
299
ax.grid(True, alpha=0.3)
300
301
# Comparison at same range
302
ax = axes[1, 0]
303
# Match depths for comparison
304
depth_comp = np.linspace(0, 20, 200)
305
photon_15 = photon_pdd(depth_comp, 15)
306
electron_20 = electron_pdd(depth_comp, 20)
307
proton_170 = proton_pdd(depth_comp, 170)
308
309
ax.plot(depth_comp, photon_15, 'b-', linewidth=2, label='15 MV photon')
310
ax.plot(depth_comp, electron_20, 'r-', linewidth=2, label='20 MeV electron')
311
ax.plot(depth_comp, proton_170, 'g-', linewidth=2, label='170 MeV proton')
312
ax.set_xlabel('Depth (cm)')
313
ax.set_ylabel('Percent Depth Dose (\\%)')
314
ax.set_title('Beam Type Comparison')
315
ax.legend()
316
ax.grid(True, alpha=0.3)
317
ax.set_ylim([0, 110])
318
319
# Electron ranges
320
ax = axes[1, 1]
321
R_50 = np.array(energies_MeV) / 2.33
322
R_p = np.array(energies_MeV) / 2.0
323
R_90 = []
324
for E in energies_MeV:
325
pdd = electron_pdd(depth, E)
326
r90 = depth[np.argmin(np.abs(pdd - 90))]
327
R_90.append(r90)
328
329
ax.plot(energies_MeV, R_90, 'bo-', markersize=8, label='$R_{90}$')
330
ax.plot(energies_MeV, R_50, 'go-', markersize=8, label='$R_{50}$')
331
ax.plot(energies_MeV, R_p, 'ro-', markersize=8, label='$R_p$')
332
ax.set_xlabel('Electron Energy (MeV)')
333
ax.set_ylabel('Depth (cm)')
334
ax.set_title('Electron Range Parameters')
335
ax.legend()
336
ax.grid(True, alpha=0.3)
337
338
# Proton range vs energy
339
ax = axes[1, 2]
340
proton_E = np.linspace(50, 250, 100)
341
proton_range = 0.0022 * proton_E**1.77
342
343
ax.plot(proton_E, proton_range, 'g-', linewidth=2)
344
ax.set_xlabel('Proton Energy (MeV)')
345
ax.set_ylabel('Range in Water (cm)')
346
ax.set_title('Proton Range')
347
ax.grid(True, alpha=0.3)
348
349
plt.tight_layout()
350
plt.savefig('particle_dosimetry.pdf', dpi=150, bbox_inches='tight')
351
plt.close()
352
\end{pycode}
353
354
\begin{figure}[htbp]
355
\centering
356
\includegraphics[width=0.95\textwidth]{particle_dosimetry.pdf}
357
\caption{Particle dosimetry: (a) electron PDD, (b) proton Bragg peaks, (c) SOBP, (d) beam comparison, (e) electron ranges, (f) proton range.}
358
\end{figure}
359
360
\chapter{Dose-Volume Analysis}
361
362
\begin{pycode}
363
# Simulate DVH
364
fig, axes = plt.subplots(1, 3, figsize=(14, 4))
365
366
# Create simulated dose distributions
367
np.random.seed(42)
368
n_voxels = 10000
369
370
# Target (PTV)
371
target_dose = 60 + 5 * np.random.randn(n_voxels)
372
target_dose = np.clip(target_dose, 50, 70)
373
374
# OAR 1 (close to target)
375
oar1_dose = 40 + 15 * np.random.randn(n_voxels)
376
oar1_dose = np.clip(oar1_dose, 0, 65)
377
378
# OAR 2 (further from target)
379
oar2_dose = 20 + 10 * np.random.randn(n_voxels)
380
oar2_dose = np.clip(oar2_dose, 0, 45)
381
382
# Cumulative DVH
383
ax = axes[0]
384
dose_bins = np.linspace(0, 75, 150)
385
386
for doses, label, color in [(target_dose, 'PTV', '#e74c3c'),
387
(oar1_dose, 'OAR 1', '#3498db'),
388
(oar2_dose, 'OAR 2', '#2ecc71')]:
389
hist, _ = np.histogram(doses, bins=dose_bins)
390
cum_hist = np.cumsum(hist[::-1])[::-1] / len(doses) * 100
391
ax.plot(dose_bins[:-1], cum_hist, linewidth=2, label=label, color=color)
392
393
ax.axhline(95, color='gray', linestyle='--', alpha=0.5)
394
ax.axvline(60, color='gray', linestyle=':', alpha=0.5)
395
ax.set_xlabel('Dose (Gy)')
396
ax.set_ylabel('Volume (\\%)')
397
ax.set_title('Cumulative DVH')
398
ax.legend()
399
ax.grid(True, alpha=0.3)
400
401
# Differential DVH
402
ax = axes[1]
403
for doses, label, color in [(target_dose, 'PTV', '#e74c3c'),
404
(oar1_dose, 'OAR 1', '#3498db'),
405
(oar2_dose, 'OAR 2', '#2ecc71')]:
406
ax.hist(doses, bins=50, alpha=0.5, label=label, color=color, density=True)
407
408
ax.set_xlabel('Dose (Gy)')
409
ax.set_ylabel('Frequency')
410
ax.set_title('Differential DVH')
411
ax.legend()
412
ax.grid(True, alpha=0.3)
413
414
# DVH metrics
415
ax = axes[2]
416
metrics = {
417
'D95 PTV': np.percentile(target_dose, 5),
418
'D50 PTV': np.percentile(target_dose, 50),
419
'D2 PTV': np.percentile(target_dose, 98),
420
'Dmax OAR1': np.max(oar1_dose),
421
'Dmean OAR1': np.mean(oar1_dose),
422
'Dmax OAR2': np.max(oar2_dose),
423
}
424
425
names = list(metrics.keys())
426
values = list(metrics.values())
427
colors_bar = ['#e74c3c']*3 + ['#3498db']*2 + ['#2ecc71']
428
429
bars = ax.barh(names, values, color=colors_bar, alpha=0.7)
430
ax.set_xlabel('Dose (Gy)')
431
ax.set_title('DVH Metrics')
432
ax.grid(True, alpha=0.3, axis='x')
433
434
plt.tight_layout()
435
plt.savefig('dose_volume.pdf', dpi=150, bbox_inches='tight')
436
plt.close()
437
438
# Store metrics
439
D95_PTV = metrics['D95 PTV']
440
Dmax_OAR1 = metrics['Dmax OAR1']
441
\end{pycode}
442
443
\begin{figure}[htbp]
444
\centering
445
\includegraphics[width=0.95\textwidth]{dose_volume.pdf}
446
\caption{Dose-volume analysis: (a) cumulative DVH, (b) differential DVH, (c) DVH metrics.}
447
\end{figure}
448
449
\chapter{Numerical Results}
450
451
\begin{pycode}
452
# 6 MV reference values
453
d_max_6 = 0.5 + 6 * 0.15
454
pdd_6_ref = photon_pdd(depth, 6)
455
d_50_6 = depth[np.argmin(np.abs(pdd_6_ref - 50))]
456
d_80_6 = depth[np.argmin(np.abs(pdd_6_ref - 80))]
457
458
results_table = [
459
('6 MV $d_{max}$', f'{d_max_6:.1f}', 'cm'),
460
('6 MV $d_{50}$', f'{d_50_6:.1f}', 'cm'),
461
('6 MV $d_{80}$', f'{d_80_6:.1f}', 'cm'),
462
('PTV D95', f'{D95_PTV:.1f}', 'Gy'),
463
('OAR1 $D_{max}$', f'{Dmax_OAR1:.1f}', 'Gy'),
464
('Prescribed dose', '60.0', 'Gy'),
465
]
466
\end{pycode}
467
468
\begin{table}[htbp]
469
\centering
470
\caption{Radiation dosimetry results}
471
\begin{tabular}{@{}lcc@{}}
472
\toprule
473
Parameter & Value & Units \\
474
\midrule
475
\py{results_table[0][0]} & \py{results_table[0][1]} & \py{results_table[0][2]} \\
476
\py{results_table[1][0]} & \py{results_table[1][1]} & \py{results_table[1][2]} \\
477
\py{results_table[2][0]} & \py{results_table[2][1]} & \py{results_table[2][2]} \\
478
\py{results_table[3][0]} & \py{results_table[3][1]} & \py{results_table[3][2]} \\
479
\py{results_table[4][0]} & \py{results_table[4][1]} & \py{results_table[4][2]} \\
480
\py{results_table[5][0]} & \py{results_table[5][1]} & \py{results_table[5][2]} \\
481
\bottomrule
482
\end{tabular}
483
\end{table}
484
485
\chapter{Conclusions}
486
487
\begin{enumerate}
488
\item Higher photon energies penetrate deeper with skin sparing
489
\item Electrons have sharp dose falloff at their range
490
\item Protons offer superior dose conformity with Bragg peak
491
\item SOBP allows tumor coverage with proton therapy
492
\item DVH analysis quantifies target coverage and OAR sparing
493
\item Treatment planning optimizes therapeutic ratio
494
\end{enumerate}
495
496
\end{document}
497
498