Source code for PyPlanAnalysis.NTCP


"""
PyPlanAnalysis.NTCP.py
==============
computes different NTCP following details in /Utils/NTCPModels_params.xlsx. 
It works both with variable and fixed RBE models for proton therapy.

"""


# script to calculate NTCP based on DVH and clincial parameters
#_____________________________________________________________________________
import pandas as pd
import numpy as np
import scipy
from dataclasses import dataclass, field
from importlib import resources


[docs] def default_ntcp_params_path(): """ Path to the NTCP model-parameter workbook bundled with the package (PyPlanAnalysis/data/NTCPModels_params.xlsx). Used automatically by NTCPModelBase when no explicit ``df_models_path`` is supplied, so installed users get working NTCP calculations out of the box. """ return resources.files("PyPlanAnalysis.data").joinpath("NTCPModels_params.xlsx")
[docs] @dataclass class NTCPConfig: """ Selects which NTCP toxicity models to compute in ``AnalysisResults.CalcNTCP()``. Parameters ---------- models : list of str Subset of the model names below (must match the ``model_name`` column of the bundled ``NTCPModels_params.xlsx`` workbook). Default: all models listed below. roi_overrides : dict Optional ``{model_name: roi_name}`` mapping. When a model name is present here, its exact ``roi_name`` (must match a value in ``metrics_df["ROI_Name"]``) is used directly instead of the ``OAR`` substring match from the parameter workbook — lets you point a model at a specific contoured ROI on a per-patient basis, even if it doesn't match the workbook's OAR column. Default: empty (use the workbook's OAR-based matching for every model). ctv_name : str or None Optional exact ``ROI_Name`` to use as "the CTV" reference for geometry-based ipsi/contra selection (see ``NTCPModelBase.define_side``). Default: None, which falls back to the first ROI (in table order) whose name contains "CTV" (case-insensitive). Notes ----- Each model name maps to one ``NTCP__*`` implementation function in this module (brain/head-and-neck late- and acute-toxicity endpoints from Dutz, De Marzi, Burman, Gondi, Kong, Lee, Bender and Batth). """ roi_overrides : dict = field(default_factory=dict) ctv_name : str = None models : list = field(default_factory=lambda:['Alopecia_G1_12m__1', 'Alopecia_G1_12m__2', 'Alopecia_G1_24m__1', 'Alopecia_G1_24m__2', 'Alopecia_G1_acute', 'Alopecia_G2_acute', 'Blindness_5y__Chiasma', 'Blindness_5y__OpticNerve_ipsi', 'Blindness_5y_OpticNerve_contra', 'BrainNecrosis_5y__Brain-CTV', 'BrainNecrosis_5y__BrainStem', 'CataractRequiringIntervention_5y__Lens_ipsi', 'CataractRequiringIntervention_5y__Lens_contra', 'DelayedRecall_1_5y', 'EndocrineDysfunction_late', 'Erythema_G1_acute', 'Erythema_G2_acute', 'Fatigue_G1_24m', 'Fatigue_G1_acute', 'HearingImpairment_G1_12m__1__Cochlea_ipsi', 'HearingImpairment_G1_12m__1__Cochlea_contra', 'HearingImpairment_G1_12m__2__Cochlea_ipsi', 'HearingImpairment_G1_12m__2__Cochlea_contra', 'HearingImpairment_G1_24m__Cochlea_ipsi', 'HearingImpairment_G1_24m__Cochlea_contra', 'HearingLoss_late__Cochlea_ipsi', 'HearingLoss_late__Cochlea_contra', 'MemoryImpairment_G1_12m', 'MemoryImpairment_G1_24m', 'MemoryImpairment_G2_12m', 'OcularToxicity_G2_acute__LacrimalGland_ipsi', 'OcularToxicity_G2_acute__LacrimalGland_contra', 'TemporalLobeInjury_5y__TemporalLobe_ipsi', 'TemporalLobeInjury_5y__TemporalLobe_contra', 'Tinnitus_G2_late__Cochlea_ipsi', 'Tinnitus_G2_late__Cochlea_contra'])
# ____________________________________________________________________________
[docs] class NTCPModelBase(): """ Loads one NTCP model's metadata from the parameter workbook and dispatches to its implementation function. Parameters ---------- model_name : str Must match a value in the ``model_name`` column of the parameter workbook (see ``NTCPConfig.models`` for the full list). df_models_path : str or Path, optional Path to the NTCP parameter workbook (``.xlsx``). Default: None, which resolves to the workbook bundled with the package via ``default_ntcp_params_path()``. roi_name : str, optional Exact ``ROI_Name`` to use for this model, bypassing the workbook's ``OAR`` substring match entirely. Must match a value in ``metrics_df["ROI_Name"]`` (case-insensitive exact match). Default: None (use OAR-based matching). ctv_name : str, optional Exact ``ROI_Name`` to treat as "the CTV" for geometry-based ipsi/contra selection. Default: None, which falls back to the first ROI (in table order) whose name contains "CTV". Attributes ---------- OAR_name : str Organ-at-risk name this model applies to. numberOfVariables : int Number of covariates the model's implementation function expects. parameterNames : list of str DVH/LVH metric column-name suffixes to pull from the metrics DataFrame for each covariate, in order. side : {"ipsi", "contra", None} Laterality selection rule, inferred from ``model_name``. impl_fn : callable The ``NTCP__*`` function implementing this model's formula. """ def __init__(self, model_name, df_models_path, roi_name=None, ctv_name=None): self.model_name = model_name self.roi_name_override = roi_name self.ctv_name = ctv_name if df_models_path is None: df_models_path = default_ntcp_params_path() df_models = pd.read_excel(df_models_path) self.numberOfVariables = df_models["numberOfVariables"][df_models["model_name"]==self.model_name].values[0] self.parameterNames = [] self.OAR_name = df_models["OAR"][df_models["model_name"]==self.model_name].values[0] self.not_printed = True if "ipsi" in self.model_name: self.side = "ipsi" elif "contra" in self.model_name: self.side = "contra" else: self.side = None for i in range(1,self.numberOfVariables+1): self.parameterNames.append(df_models["parameterName_"+str(i)][df_models["model_name"]==model_name].values[0]) self.impl_fn = npNan #eventuell auskommentieren if "Alopecia_G1_12m__1" in self.model_name: self.impl_fn = NTCP__Alopecia_G1_12m__1 elif "Alopecia_G1_12m__2" in self.model_name: self.impl_fn = NTCP__Alopecia_G1_12m__2 elif "Alopecia_G1_24m__1" in self.model_name: self.impl_fn = NTCP__Alopecia_G1_24m__1 elif "Alopecia_G1_24m__2" in self.model_name: self.impl_fn = NTCP__Alopecia_G1_24m__2 elif "Alopecia_G1_acute" in self.model_name: self.impl_fn = NTCP__Alopecia_G1_acute elif "Alopecia_G2_acute" in self.model_name: self.impl_fn = NTCP__Alopecia_G2_acute elif "Blindness_5y" in self.model_name: self.impl_fn = NTCP__Blindness_5y elif "BrainNecrosis_5y" in self.model_name: self.impl_fn = NTCP__BrainNecrosis_5y elif "CataractRequiringIntervention_5y" in self.model_name: self.impl_fn = NTCP__CataractRequiringIntervention_5y elif "DelayedRecall_1_5y" in self.model_name: self.impl_fn = NTCP__DelayedRecall_1_5y elif "EndocrineDysfunction_late" in self.model_name: self.impl_fn = NTCP__EndocrineDysfunction_late elif "Erythema_G1_acute" in self.model_name: self.impl_fn = NTCP__Erythema_G1_acute elif "Erythema_G2_acute" in self.model_name: self.impl_fn = NTCP__Erythema_G2_acute elif "Fatigue_G1_24m" in self.model_name: self.impl_fn = NTCP__Fatigue_G1_24m elif "Fatigue_G1_acute" in self.model_name: self.impl_fn = NTCP__Fatigue_G1_acute elif "HearingImpairment_G1_12m__1" in self.model_name: self.impl_fn = NTCP__HearingImpairment_G1_12m__1 elif "HearingImpairment_G1_12m__2" in self.model_name: self.impl_fn = NTCP__HearingImpairment_G1_12m__2 elif "HearingImpairment_G1_24m" in self.model_name: self.impl_fn = NTCP__HearingImpairment_G1_24m elif "HearingLoss_late" in self.model_name: self.impl_fn = NTCP__HearingLoss_late elif "MemoryImpairment_G1_12m" in self.model_name: self.impl_fn = NTCP__MemoryImpairment_G1_12m elif "MemoryImpairment_G1_24m" in self.model_name: self.impl_fn = NTCP__MemoryImpairment_G1_24m elif "MemoryImpairment_G2_12m" in self.model_name: self.impl_fn = NTCP__MemoryImpairment_G2_12m elif "OcularToxicity_G2_acute" in self.model_name: self.impl_fn = NTCP__OcularToxicity_G2_acute elif "TemporalLobeInjury_5y" in self.model_name: self.impl_fn = NTCP__TemporalLobeInjury_5y elif "Tinnitus_G2_late" in self.model_name: self.impl_fn = NTCP__Tinnitus_G2_late
[docs] def define_side(self,vRBE_model, dfi_dvh,parameterName): """ Resolve which structure (ROI) to use when a model applies to a laterality-specific OAR (e.g. "Cochlea ipsi" vs "Cochlea contra") and the metrics DataFrame has more than one ROI matching ``self.OAR_name``. Selection is geometry-based when possible: the candidate ROI whose center of mass is closest to the CTV's center of mass is "ipsi", the farthest is "contra" (see ``_define_side_by_geometry``/``get_roi_center_of_mass``). If center-of-mass data isn't available for the CTV or for every candidate ROI, this falls back to the previous dose-value heuristic (candidate with the highest mean dose = ipsi, lowest = contra). Parameters ---------- vRBE_model : str Dose-type/RBE-model label prefix used in the metrics column names (e.g. "Phys", "RBE1.1", "mcnamara"). dfi_dvh : pandas.DataFrame The patient's per-structure metrics table (``AnalysisResults.metrics_df``). parameterName : str Metric column-name suffix to compare across candidate ROIs. Returns ------- str or None The chosen ``ROI_Name`` value, or None if no ROI matches. """ rois = {} for s in dfi_dvh["ROI_Name"]: if (self.OAR_name.lower() in s.lower()) or (s.lower() in self.OAR_name.lower()): val = dfi_dvh[vRBE_model + '_' + parameterName][dfi_dvh["ROI_Name"] == s] rois[s] = val.values[0] # If only one (or zero) matching ROI, no side selection needed if len(rois) <= 1: return next(iter(rois), None) # Only apply ipsi/contra logic if the model name signals it if self.side not in ("ipsi", "contra"): # Multiple ROIs but no side info in model name — fall back to first match return next(iter(rois)) # --- Prefer geometry-based (COM-distance-to-CTV) selection --- geom_choice = self._define_side_by_geometry(dfi_dvh, list(rois.keys())) if geom_choice is not None: if self.not_printed: print(f"{geom_choice} chosen as {self.side} ROI for {self.model_name} based on geometry") self.not_printed = False return geom_choice # --- Fallback: dose-value heuristic --- if self.side == "ipsi" : print(f"{max(rois, key=lambda k: rois[k])} chosen as {self.side} ROI for {self.model_name} based on mean dose for {vRBE_model}") return max(rois, key=lambda k: rois[k]) else: print(f"{min(rois, key=lambda k: rois[k])} chosen as {self.side} ROI for {self.model_name} based on mean dose for {vRBE_model}") return min(rois, key=lambda k: rois[k])
def _define_side_by_geometry(self, dfi_dvh, candidate_rois): """ Assign ipsi/contra by comparing each candidate ROI's center of mass (``COM_x``/``COM_y``/``COM_z`` columns, populated by ``PatientPlan.analyse()``) to the CTV's center of mass: the candidate closest to the CTV is "ipsi", the farthest is "contra". Returns None (triggering the dose-based fallback in ``define_side``) if the COM columns are missing, no CTV reference can be resolved, or COM data is missing for the CTV or any candidate ROI. """ if not {"COM_x", "COM_y", "COM_z"}.issubset(dfi_dvh.columns): return None ctv_row_name = self._resolve_ctv_name(dfi_dvh) if ctv_row_name is None: return None ctv_com = dfi_dvh.loc[dfi_dvh["ROI_Name"] == ctv_row_name, ["COM_x", "COM_y", "COM_z"]].values if len(ctv_com) == 0 or np.isnan(ctv_com).any(): return None ctv_com = ctv_com[0] distances = {} for roi in candidate_rois: com = dfi_dvh.loc[dfi_dvh["ROI_Name"] == roi, ["COM_x", "COM_y", "COM_z"]].values if len(com) == 0 or np.isnan(com).any(): continue distances[roi] = float(np.linalg.norm(com[0] - ctv_com)) # Require COM data for every candidate — a partial comparison # isn't a safe basis for ipsi/contra assignment. if len(distances) < len(candidate_rois): return None if self.side == "ipsi": return min(distances, key=distances.get) else: return max(distances, key=distances.get) def _resolve_ctv_name(self, dfi_dvh): """ Resolve which ROI represents the CTV for geometry-based side selection: ``self.ctv_name`` (exact match) if set, otherwise the first ROI (in table order) whose name contains "CTV" (case-insensitive). """ if self.ctv_name: for s in dfi_dvh["ROI_Name"]: if s.strip().lower() == self.ctv_name.strip().lower(): return s if self.not_printed: print(f"{self.ctv_name} is not available for for Ipsi/Contra definition") self.not_printed = False return None for s in dfi_dvh["ROI_Name"]: if "ctv" in s.lower(): return s return None def _match_roi_name(self, dfi_dvh, roi_name): """ Resolve a user-supplied ``roi_name`` override to the exact ``ROI_Name`` value present in ``dfi_dvh`` (case-insensitive exact match — this is a specific user choice, not a fuzzy substring search). """ for s in dfi_dvh["ROI_Name"]: if s.strip().lower() == roi_name.strip().lower(): return s raise ValueError( f"roi_name override '{roi_name}' not found in metrics_df['ROI_Name']." )
[docs] def compute_x(self,vRBE_model,dfi_dvh,roi): """ Build the covariate vector ``x`` this model's ``impl_fn`` expects, by pulling ``self.parameterNames`` columns for the matched ROI(s) out of the metrics DataFrame. Parameters ---------- vRBE_model : str Dose-type/RBE-model label prefix (see ``define_side``). dfi_dvh : pandas.DataFrame Patient metrics table (``AnalysisResults.metrics_df``). roi : str ROI name defined on (``compute_NTCP``). Returns ------- list of float One value per covariate, in ``self.parameterNames`` order. Entries are ``np.nan`` where the required ROI/metric could not be found. """ x = [] for i in range(0,self.numberOfVariables): try: potential_x = [dfi_dvh[vRBE_model+'_'+self.parameterNames[i]][dfi_dvh["ROI_Name"]==roi]] x.append(potential_x[0].values[0]) except: x.append(np.nan) return x
[docs] def compute_NTCP(self, vRBE_model,dfi_dvh): """ Compute this model's NTCP for one patient and one dose type. Define ipsi/contra first based on COM, mean dose as fallback Parameters ---------- vRBE_model : str Dose-type/RBE-model label prefix (see ``define_side``). dfi_dvh : pandas.DataFrame Patient metrics table (``AnalysisResults.metrics_df``). Returns ------- float or None NTCP in percent (0-100), rounded to 4 decimals. Returns ``None`` if any required covariate is missing/NaN. """ if self.roi_name_override: # Explicit user choice — bypass OAR/side matching entirely. roi = self._match_roi_name(dfi_dvh, self.roi_name_override) elif self.side: roi = self.define_side(vRBE_model,dfi_dvh,"Dmean") else: roi = None for s in dfi_dvh["ROI_Name"]: if self.OAR_name.lower() in s.lower(): roi = s x = self.compute_x(vRBE_model,dfi_dvh,roi) if not np.isnan(x).any(): return np.round(self.impl_fn(x)*100,4)
[docs] def npNan(x): """Fallback implementation for an unrecognised model name; always returns NaN.""" return np.nan
# ---------------------------------------------------------------------------------------------
[docs] def NTCP__Alopecia_G1_12m__1(x, beta_0=-1.88, beta_1=0.15): #1.80 o 1.88?? """ Alopecia grade >=1, 12 months after PBT Late Dutz et al. 2021 Parameters ---------- x : list Model covariates: x[0] = Skin V45Gy(RBE) in cm^-3. beta_0 : float Fitted model coefficient (see reference above for origin/units). beta_1 : float Fitted model coefficient (see reference above for origin/units). Returns ------- float NTCP probability in [0, 1]. """ return 1/(1+np.exp(-beta_0-beta_1*x[0]))
[docs] def NTCP__Alopecia_G1_12m__2(x, beta_0=-6.38, beta_1=0.15): """ Alopecia grade ≥1_12 months after PBT Late Dutz et al. 2021 Parameters ---------- x : list Model covariates, in the order defined by the NTCP parameter workbook for this model (see NTCPModelBase.parameterNames). beta_0 : float Fitted model coefficient (see reference above for origin/units). beta_1 : float Fitted model coefficient (see reference above for origin/units). Returns ------- float NTCP probability in [0, 1] """ return 1/(1+np.exp(-beta_0-beta_1*x[0]))
[docs] def NTCP__Alopecia_G1_24m__1(x, beta_0=-1.70, beta_1=0.048): """ Alopecia grade ≥1_24 months after PBT Late Dutz et al. 2021 Parameters ---------- x : list Model covariates, in the order defined by the NTCP parameter workbook for this model (see NTCPModelBase.parameterNames). beta_0 : float Fitted model coefficient (see reference above for origin/units). beta_1 : float Fitted model coefficient (see reference above for origin/units). Returns ------- float NTCP probability in [0, 1] . """ return 1/(1+np.exp(-beta_0-beta_1*x[0]))
[docs] def NTCP__Alopecia_G1_24m__2(x, beta_0=-3.18, beta_1=0.068): """ Alopecia grade ≥1_24 months after PBT Late Dutz et al. 2021 Parameters ---------- x : list Model covariates, in the order defined by the NTCP parameter workbook for this model (see NTCPModelBase.parameterNames). beta_0 : float Fitted model coefficient (see reference above for origin/units). beta_1 : float Fitted model coefficient (see reference above for origin/units). Returns ------- float NTCP probability in [0, 1]. """ return 1/(1+np.exp(-beta_0-beta_1*x[0]))
[docs] def NTCP__Alopecia_G1_acute(x, beta_0=-0.94, beta_1=0.10): """ Alopecia grade >=1 (CTCAE, Common Terminology Criteria for Adverse Events) Acute Dutz et al. 2019 Parameters ---------- x : list Model covariates, in the order defined by the NTCP parameter workbook for this model (see NTCPModelBase.parameterNames). beta_0 : float Fitted model coefficient (see reference above for origin/units). beta_1 : float Fitted model coefficient (see reference above for origin/units). Returns ------- float NTCP probability in [0, 1]. """ return 1/(1+np.exp(-beta_0-beta_1*x[0]))
[docs] def NTCP__Alopecia_G2_acute(x, beta_0=-1.33, beta_1=0.081): """ Alopecia grade >=2 (CTCAE, Common Terminology Criteria for Adverse Events) Acute Dutz et al. 2019 Parameters ---------- x : list Model covariates, in the order defined by the NTCP parameter workbook for this model (see NTCPModelBase.parameterNames). beta_0 : float Fitted model coefficient (see reference above for origin/units). beta_1 : float Fitted model coefficient (see reference above for origin/units). Returns ------- float NTCP probability in [0, 1] . """ return 1/(1+np.exp(-beta_0-beta_1*x[0]))
# Blindness # __________________________________________________________________________ # 5 years post-RT # Burman et al. 1991
[docs] def NTCP__Blindness_5y(x, TD50=65.0, m=0.14): """ Blindness Chiasm and optic nerves gEUD, a = 4.0 5 years post-RT Burman et al. 1991 Parameters ---------- x : list Model covariates, in the order defined by the NTCP parameter workbook for this model (see NTCPModelBase.parameterNames). TD50 : float Fitted model coefficient (see reference above for origin/units). m : float Fitted model coefficient (see reference above for origin/units). Returns ------- float NTCP probability in [0, 1]. """ t = (x[0] - TD50)/(m*TD50) return 0.5*(1+scipy.special.erf(t/np.sqrt(2)))
[docs] def NTCP__BrainNecrosis_5y(x, D50=109.0, gamma=2.8): """ Brain necrosis Brain-CTV and Brainstem Dmax (EQD2) 5 years post-RT Bender et al. 2012 Parameters ---------- x : list Model covariates, in the order defined by the NTCP parameter workbook for this model (see NTCPModelBase.parameterNames). D50 : float Fitted model coefficient (see reference above for origin/units). gamma : float Fitted model coefficient (see reference above for origin/units). Returns ------- float NTCP probability in [0, 1]. """ if x[0] == 0: return 0 else: return 1/(1+(D50/x[0])**(4*gamma))
[docs] def NTCP__CataractRequiringIntervention_5y(x, TD50=18.0, m=0.27): """ Cataract requiring intervention Lenses gEUD 5 years post-RT Burman et al. 1991 Parameters ---------- x : list Model covariates, in the order defined by the NTCP parameter workbook for this model (see NTCPModelBase.parameterNames). TD50 : float Fitted model coefficient (see reference above for origin/units). m : float Fitted model coefficient (see reference above for origin/units). Returns ------- float NTCP probability in [0, 1] . """ t = (x[0] - TD50)/(m*TD50) return 0.5*(1+scipy.special.erf(t/np.sqrt(2)))
[docs] def NTCP__DelayedRecall_1_5y(x, EQD_2_50=14.88, m=0.540): """ Delayed recall (on Wechsler Memory scale III Word Lists) Bilateral hippocampi D40% (EQD2) 1.5 years post-RT Gondi et al. 2012 Parameters ---------- x : list Model covariates, in the order defined by the NTCP parameter workbook for this model (see NTCPModelBase.parameterNames). EQD_2_50 : float Fitted model coefficient (see reference above for origin/units). m : float Fitted model coefficient (see reference above for origin/units). Returns ------- float NTCP probability in [0, 1]. """ t = (x[0] - EQD_2_50)/(m*EQD_2_50) return 0.5*(1+scipy.special.erf(t/np.sqrt(2)))
[docs] def NTCP__EndocrineDysfunction_late(x, TD50=60.5, gamma50=5.2): """ Endocrine dysfunction (CTCAE, Common Terminology Criteria for Adverse Events) Pituitary gEUD, a = 6.4 At least 0.5 – 2 years post-RT De Marzi et al. 2015 Parameters ---------- x : list Model covariates, in the order defined by the NTCP parameter workbook for this model (see NTCPModelBase.parameterNames). TD50 : float Fitted model coefficient (see reference above for origin/units). gamma50 : float Fitted model coefficient (see reference above for origin/units). Returns ------- float NTCP probability in [0, 1] . """ if x[0] == 0: return 0 else: return 1/(1+(TD50/x[0])**(4*gamma50))
[docs] def NTCP__Erythema_G1_acute(x, beta_0=1.00, beta_1=0.085): """ Erythema grade ≥ 1 Skin V35Gy(RBE), absolute volume Acute Dutz et al. 2019 Parameters ---------- x : list Model covariates, in the order defined by the NTCP parameter workbook for this model (see NTCPModelBase.parameterNames). beta_0 : float Fitted model coefficient (see reference above for origin/units). beta_1 : float Fitted model coefficient (see reference above for origin/units). Returns ------- float NTCP probability in [0, 1] . """ # return 1/(1+np.exp(-beta_0-beta_1*x[0]))
[docs] def NTCP__Erythema_G2_acute(x, beta_0=-1.54, beta_1=0.056): """ Erythema grade ≥ 2 (CTCAE, Common Terminology Criteria for Adverse Events) Skin V35Gy(RBE), absolute volume Acute Dutz et al. 2019 Parameters ---------- x : list Model covariates, in the order defined by the NTCP parameter workbook for this model (see NTCPModelBase.parameterNames). beta_0 : float Fitted model coefficient (see reference above for origin/units). beta_1 : float Fitted model coefficient (see reference above for origin/units). Returns ------- float NTCP probability in [0, 1] . """ return 1/(1+np.exp(-beta_0-beta_1*x[0]))
[docs] def NTCP__Fatigue_G1_24m(x, beta_0=-1.52, beta_1=0.021, beta_2=-1.16): """ Fatigue grade ≥ 1_24 months after PBT x[0] BrainStem D2% in Gy(RBE)^-(1) x[1] CTx == 0: patient recieved no chemotherapy CTx == 1: patient recieved chemotherapy Late Dutz et al. 2021 Parameters ---------- x : list Model covariates, in the order defined by the NTCP parameter workbook for this model (see NTCPModelBase.parameterNames). beta_0 : float Fitted model coefficient (see reference above for origin/units). beta_1 : float Fitted model coefficient (see reference above for origin/units). beta_2 : float Fitted model coefficient (see reference above for origin/units). Returns ------- float NTCP probability in [0, 1] . """ return 1/(1+np.exp(-beta_0-beta_1*x[0]-beta_2*x[1]))
[docs] def NTCP__Fatigue_G1_acute(x, beta_0=-0.90, beta_1=0.027, beta_2=1.28): """ Fatigue grade >=1 (CTCAE, Common Terminology Criteria for Adverse Events) x[0] Brain-CTV(Gy), D2% x[1] female: gender = 1 male: gender = 0 Acute Dutz et al. 2019 Parameters ---------- x : list Model covariates, in the order defined by the NTCP parameter workbook for this model (see NTCPModelBase.parameterNames). beta_0 : float Fitted model coefficient (see reference above for origin/units). beta_1 : float Fitted model coefficient (see reference above for origin/units). beta_2 : float Fitted model coefficient (see reference above for origin/units). Returns ------- float NTCP probability in [0, 1] . """ return 1/(1+np.exp(-beta_0-beta_1*x[0]-beta_2*x[1]))
[docs] def NTCP__HearingImpairment_G1_12m__1(x, beta_0=-3.03, beta_1=0.038): """ Hearing impairment grade ≥1_12 months after PBT Dmean == Cochlea ipsi Dmean in Gy(RBE)^-(1) Late Dutz et al. 2021 Parameters ---------- x : list Model covariates, in the order defined by the NTCP parameter workbook for this model (see NTCPModelBase.parameterNames). beta_0 : float Fitted model coefficient (see reference above for origin/units). beta_1 : float Fitted model coefficient (see reference above for origin/units). Returns ------- float NTCP probability in [0, 1] . """ return 1/(1+np.exp(-beta_0-beta_1*x[0]))
[docs] def NTCP__HearingImpairment_G1_12m__2(x, beta_0=-7.02, beta_1=0.032, beta_2=0.072): """ Hearing impairment grade ≥1_12 months after PBT x[0] Dmean = Cochlea ipsi Dmean in Gy(RBE)^-(1) x[1] Age = Age in years Late Dutz et al. 2021 Parameters ---------- x : list Model covariates, in the order defined by the NTCP parameter workbook for this model (see NTCPModelBase.parameterNames). beta_0 : float Fitted model coefficient (see reference above for origin/units). beta_1 : float Fitted model coefficient (see reference above for origin/units). beta_2 : float Fitted model coefficient (see reference above for origin/units). Returns ------- float NTCP probability in [0, 1]. """ return 1/(1+np.exp(-beta_0-beta_1*x[0]-beta_2*x[1]))
[docs] def NTCP__HearingImpairment_G1_24m(x, beta_0=-3.48, beta_1=0.050): """ Hearing impairment grade ≥1_24 months after PBT Cochlea ipsi Dmean in Gy(RBE)^-(1) Late Dutz et al. 2021 Parameters ---------- x : list Model covariates, in the order defined by the NTCP parameter workbook for this model (see NTCPModelBase.parameterNames). beta_0 : float Fitted model coefficient (see reference above for origin/units). beta_1 : float Fitted model coefficient (see reference above for origin/units). Returns ------- float NTCP probability in [0, 1]. """ return 1/(1+np.exp(-beta_0-beta_1*x[0]))
[docs] def NTCP__HearingLoss_late(x, TD50=56.0, gamma50=2.9): """ Hearing loss (CTCAE, Common Terminology Criteria for Adverse Events) Cochlea gEUD, a = 1.2 At least 0.5 – 2 years post-RT De Marzi et al. 2015 Parameters ---------- x : list Model covariates, in the order defined by the NTCP parameter workbook for this model (see NTCPModelBase.parameterNames). TD50 : float Fitted model coefficient (see reference above for origin/units). gamma50 : float Fitted model coefficient (see reference above for origin/units). Returns ------- float NTCP probability in [0, 1]. """ if x[0] == 0: return 0 else: return 1/(1+(TD50/x[0])**(4*gamma50))
# Memory impairment grade ≥1_12 months after PBT # __________________________________________________________________________ # Late # Dutz et al. 2021
[docs] def NTCP__MemoryImpairment_G1_12m(x, beta_0=-2.32, beta_1=0.023): """ Memory impairment grade ≥1_12 months after PBT Hippocampi D2% in Gy(RBE)^-(1) Late Dutz et al. 2021 Parameters ---------- x : list Model covariates, in the order defined by the NTCP parameter workbook for this model (see NTCPModelBase.parameterNames). beta_0 : float Fitted model coefficient (see reference above for origin/units). beta_1 : float Fitted model coefficient (see reference above for origin/units). Returns ------- float NTCP probability in [0, 1] . """ return 1/(1+np.exp(-beta_0-beta_1*(x[0]/100)))
[docs] def NTCP__MemoryImpairment_G1_24m(x, beta_0=-1.77, beta_1=6.50): """ Memory impairment grade ≥1_24 months after PBT Brain-CTV V35Gy(RBE) as fraction of the total volume Late Dutz et al. 2021 Parameters ---------- x : list Model covariates, in the order defined by the NTCP parameter workbook for this model (see NTCPModelBase.parameterNames). beta_0 : float Fitted model coefficient (see reference above for origin/units). beta_1 : float Fitted model coefficient (see reference above for origin/units). Returns ------- float NTCP probability in [0, 1] . """ return 1/(1+np.exp(-beta_0-beta_1*(x[0]/100)))
[docs] def NTCP__MemoryImpairment_G2_12m(x, beta_0=-3.42, beta_1=5.02): """ Memory impairment grade ≥2_12 months after PBT Brain-CTV V25Gy(RBE) as fraction of the total volume Late Dutz et al. 2021 Parameters ---------- x : list Model covariates, in the order defined by the NTCP parameter workbook for this model (see NTCPModelBase.parameterNames). beta_0 : float Fitted model coefficient (see reference above for origin/units). beta_1 : float Fitted model coefficient (see reference above for origin/units). Returns ------- float NTCP probability in [0, 1] . """ return 1/(1+np.exp(-beta_0-beta_1*(x[0]/100)))
[docs] def NTCP__OcularToxicity_G2_acute(x, beta_0=-5.174, beta_1=0.205): """ Ocular toxicity grade ≥ 2 (RTOG, Radiation Therapy Oncology Group) Ipsilateral lacrimal gland Dmax Acute Batth et al. 2013 Parameters ---------- x : list Model covariates, in the order defined by the NTCP parameter workbook for this model (see NTCPModelBase.parameterNames). beta_0 : float Fitted model coefficient (see reference above for origin/units). beta_1 : float Fitted model coefficient (see reference above for origin/units). Returns ------- float NTCP probability in [0, 1] . """ return 1/(1+np.exp(-beta_0-beta_1*x[0]))
# Temporal lobe injury # __________________________________________________________________________ # 5 years post-RT # Kong et al. 2016
[docs] def NTCP__TemporalLobeInjury_5y(x, beta_0=-18.61, beta_1=0.227): """ Temporal lobe injury Dmax = Temporal lobe Dmax 5 years post-RT Kong et al. 2016 Parameters ---------- x : list Model covariates, in the order defined by the NTCP parameter workbook for this model (see NTCPModelBase.parameterNames). beta_0 : float Fitted model coefficient (see reference above for origin/units). beta_1 : float Fitted model coefficient (see reference above for origin/units). Returns ------- float NTCP probability in [0, 1] . """ return 1/(1+np.exp(-beta_0-beta_1*x[0]))
[docs] def NTCP__Tinnitus_G2_late(x, TD50=46.52, m=0.35): """ Tinnitus grade ≥ 2 (LENT-SOMA, late effects of normal tissues - subjective, objective, management) Cochlea Dmean 1–2 years post-RT Lee et al. 2015 Parameters ---------- x : list Model covariates, in the order defined by the NTCP parameter workbook for this model (see NTCPModelBase.parameterNames). TD50 : float Fitted model coefficient (see reference above for origin/units). m : float Fitted model coefficient (see reference above for origin/units). Returns ------- float NTCP probability in [0, 1] . """ t = (x[0] - TD50)/(m*TD50) return 0.5*(1+scipy.special.erf(t/np.sqrt(2)))