pyradiobiology is a package for radiobiological modeling (TCP, NTCP, EUD, gEUD) with Python.

It provides:

  • Linear quadratic model for survival fraction and biological effective dose calculation
  • Tumor Control Probability (TCP) models: linear-quadratic Poisson, empirical Poisson
  • Normal Tissue Complication Probability (NTCP) model: Relative seriality (RS), Lyman-Kutcher-Burman (LKB)
  • Generalized Equi-effective Uniform Dose (gEUD), Equi-effective Uniform Dose (EUD)

To install the package, just run:

pip install pyradiobiology

TcpEmpirical model

The TCP emipirical model or linear-quadratic Poisson model is given by:

\( P(D) = P(EQD_{2Gy}) = exp(-e^{eγ – (EQD_{2Gy}/D_{50})*(eγ-lnln2)}) \)

\(P(D)\) is the tumour control probability, when the tumour is homogenously irradiated at the total dose D and \(EQD_{2Gy}\) is the equi-effective dose at 2Gy per fraction of the total dose D when delivered at a fraction dose d.

\(\bf{D_{50}}\) is the dose in Gy, defined as EQD2, which results in a TCP value of 50% and \(\bf{γ}\) is a dimensionless parameter, defining the maximum normalized value of the dose-response gradient.

For an inhomogeneous dose distribution {D} within the tumour of volume V, the overall tumour control probability TCP is calculated according to:

\( TCP({D},V) = \prod_{i=1}^N[P(D_{i})]^{v_{i}/V} \)

Now, let’s plot the TCP for a homogeneous dose distribution with pyRadiobiology:

from pyradiobiology import * 
import matplotlib.pyplot as plt
import numpy as np

doses = DoseBag(data=[*np.arange(start=0, stop=25, step=0.01)], 

model = TcpEmpirical(, DoseType.EQD2),

tcp_voxels = model.voxel_response(doses)
plt.plot(, tcp_voxels, linewidth=4)
plt.xlabel("EQD2 (Gy)")

The output:

The TCP calculation for an inhomogeneous dose distribution can be performed as follows. Here, we have a tumor with two regions of 15 cc and 20 cc. The regions receive 70 Gy and 65 Gy, respectively. Further, assumptions are \( α/β = 3.0 Gy \) and the number of fractions, nfx = 40.

from pyradiobiology import * 

tcpmodel = TcpEmpirical(, dose_type=DoseType.EQD2), 

dose_array = DoseBag.create(data=[70, 65], 
volume_array_in_cc = [15, 20]

tcp = tcpmodel.response_from_pysical_dose(
print(f"from physical dose, calculated TCP = {tcp} = {round(tcp, 4)}")
#self.assertEqual(0.9266, round(tcp, 4))

So, in the above example you can also use the (dose, volume) tuples/pairs derived by a differential Dose-Volume-Histogram (DDVH).