Source code for pylinac.calibration.tg51

"""
The TG-51 module contains a number of helper functions and classes that can calculate parameters for performing the
TG-51 absolute linac dose calibration although there are some modifications from the original TG-51. The modifications
include updated kQ and kecal values from Muir and Rogers' set of papers.
Functions include all relevant calculations for TG-51 including PDDx, kQ,
Dref, and chamber reading corrections. Where Muir & Rogers' values/equations are used they are specified in the documentation.

Classes include photon and electron calibrations using cylindrical chambers. Pass all the relevant raw measurements
and the class will compute all corrections and corrected readings and dose at 10cm and dmax/dref.
"""
import webbrowser
from datetime import datetime
from typing import Optional

import argue
import numpy as np

from ..core.pdf import PylinacCanvas
from ..core.typing import NumberOrArray
from ..core.utilities import Structure

MIN_TEMP = 15
MAX_TEMP = 35
MIN_PRESSURE = 90
MAX_PRESSURE = 115
MIN_PION = 1
MAX_PION = 1.05
MIN_PTP = 0.9
MAX_PTP = 1.1
MIN_PELEC = 0.98
MAX_PELEC = 1.02
MIN_PPOL = 0.98
MAX_PPOL = 1.02


KQ_PHOTONS = {
    # Exradin
    "A12": {
        "a": 1.0146,
        "b": 0.777e-3,
        "c": -1.666e-5,
        "a'": 2.6402,
        "b'": -7.2304,
        "c'": 10.7573,
        "d'": -5.4294,
    },
    "A19": {
        "a": 0.9934,
        "b": 1.384e-3,
        "c": -2.125e-5,
        "a'": 3.0907,
        "b'": -9.1930,
        "c'": 13.5957,
        "d'": -6.7969,
    },
    "A2": {
        "a": 0.9819,
        "b": 1.609e-3,
        "c": -2.184e-5,
        "a'": 2.8458,
        "b'": -8.1619,
        "c'": 12.1411,
        "d'": -6.1041,
    },
    "T2": {
        "a": 1.0173,
        "b": 0.854e-3,
        "c": -1.941e-5,
        "a'": 3.3433,
        "b'": -10.2649,
        "c'": 15.1247,
        "d'": -7.5415,
    },
    "A12S": {
        "a": 0.9692,
        "b": 1.974e-3,
        "c": -2.448e-5,
        "a'": 2.9597,
        "b'": -8.6777,
        "c'": 12.9155,
        "d'": -6.4903,
    },
    "A18": {
        "a": 0.9944,
        "b": 1.286e-3,
        "c": -1.980e-5,
        "a'": 2.5167,
        "b'": -6.7567,
        "c'": 10.1519,
        "d'": -5.1709,
    },
    "A1": {
        "a": 1.0029,
        "b": 1.023e-3,
        "c": -1.803e-5,
        "a'": 2.0848,
        "b'": -4.9174,
        "c'": 7.5446,
        "d'": -3.9441,
    },
    "T1": {
        "a": 1.0552,
        "b": -0.196e-3,
        "c": -1.275e-5,
        "a'": 2.8060,
        "b'": -7.9273,
        "c'": 11.7541,
        "d'": -5.9263,
    },
    "A1SL": {
        "a": 0.9896,
        "b": 1.410e-3,
        "c": -2.049e-5,
        "a'": 2.8029,
        "b'": -7.9648,
        "c'": 11.8445,
        "d'": -5.9568,
    },
    "A14": {
        "a": 0.9285,
        "b": 2.706e-3,
        "c": -2.599e-5,
        "a'": 5.4677,
        "b'": -19.1795,
        "c'": 27.4542,
        "d'": -13.1336,
    },
    "T14": {
        "a": 0.9622,
        "b": 2.009e-3,
        "c": -2.401e-5,
        "a'": 4.9690,
        "b'": -17.1074,
        "c'": 24.6292,
        "d'": -11.8877,
    },
    "A14SL": {
        "a": 0.9017,
        "b": 3.454e-3,
        "c": -3.083e-5,
        "a'": 5.1205,
        "b'": -17.7884,
        "c'": 25.6123,
        "d'": -12.3232,
    },
    "A16": {
        "a": 0.8367,
        "b": 4.987e-3,
        "c": -3.877e-5,
        "a'": 6.0571,
        "b'": -21.7829,
        "c'": 31.2289,
        "d'": -14.9168,
    },
    # PTW
    "30010": {
        "a": 1.0093,
        "b": 0.926e-3,
        "c": -1.771e-5,
        "a'": 2.5318,
        "b'": -6.7948,
        "c'": 10.1779,
        "d'": -5.1746,
    },
    "30011": {
        "a": 0.9676,
        "b": 2.061e-3,
        "c": -2.528e-5,
        "a'": 2.9044,
        "b'": -8.4576,
        "c'": 12.6339,
        "d'": -6.3742,
    },
    "30012": {
        "a": 0.9537,
        "b": 2.440e-3,
        "c": -2.750e-5,
        "a'": 3.2836,
        "b'": -10.0610,
        "c'": 14.8867,
        "d'": -7.4212,
    },
    "30013": {
        "a": 0.9652,
        "b": 2.141e-3,
        "c": -2.623e-5,
        "a'": 3.2012,
        "b'": -9.7211,
        "c'": 14.4211,
        "d'": -7.2184,
    },
    "31010": {
        "a": 0.9590,
        "b": 2.265e-3,
        "c": -2.684e-5,
        "a'": 3.1578,
        "b'": -9.5422,
        "c'": 14.1676,
        "d'": -7.0964,
    },
    "31016": {
        "a": 1.0085,
        "b": 1.028e-3,
        "c": -1.968e-5,
        "a'": 2.9524,
        "b'": -8.6054,
        "c'": 12.7757,
        "d'": -6.4265,
    },
    "31014": {
        "a": 1.0071,
        "b": 1.048e-3,
        "c": -1.967e-5,
        "a'": 3.0178,
        "b'": -8.8735,
        "c'": 13.1372,
        "d'": -6.5867,
    },
    # IBA
    "CC25": {
        "a": 0.9551,
        "b": 2.353e-3,
        "c": -2.687e-5,
        "a'": 2.4567,
        "b'": -6.5932,
        "c'": 10.0471,
        "d'": -5.1775,
    },
    "CC13": {
        "a": 0.9515,
        "b": 2.455e-3,
        "c": -2.768e-5,
        "a'": 3.1982,
        "b'": -9.7182,
        "c'": 14.4210,
        "d'": -7.2121,
    },
    "CC08": {
        "a": 0.9430,
        "b": 2.637e-3,
        "c": -2.884e-5,
        "a'": 3.7328,
        "b'": -11.9800,
        "c'": 17.5884,
        "d'": -8.6843,
    },
    "CC04": {
        "a": 0.9714,
        "b": 1.938e-3,
        "c": -2.432e-5,
        "a'": 3.0054,
        "b'": -8.8633,
        "c'": 13.1704,
        "d'": -6.6075,
    },
    "CC01": {
        "a": 0.9116,
        "b": 3.358e-3,
        "c": -3.177e-5,
        "a'": 4.3376,
        "b'": -14.4935,
        "c'": 21.0293,
        "d'": -10.2208,
    },
    "FC65-G": {
        "a": 0.9708,
        "b": 1.972e-3,
        "c": -2.480e-5,
        "a'": 3.3221,
        "b'": -10.2012,
        "c'": 15.0497,
        "d'": -7.4872,
    },
    "FC65-P": {
        "a": 0.9828,
        "b": 1.664e-3,
        "c": -2.296e-5,
        "a'": 3.0872,
        "b'": -9.1919,
        "c'": 13.6137,
        "d'": -6.8118,
    },
    "FC23-C": {
        "a": 0.9820,
        "b": 1.579e-3,
        "c": -2.166e-5,
        "a'": 3.0511,
        "b'": -9.0243,
        "c'": 13.3378,
        "d'": -6.6559,
    },
    # Other
    "NE2581": {
        "a": 1.0318,
        "b": 0.488e-3,
        "c": -1.731e-5,
        "a'": 2.9190,
        "b'": -8.4561,
        "c'": 12.5690,
        "d'": -6.3468,
    },
    "NE2571": {
        "a": 0.9882,
        "b": 1.486e-3,
        "c": -2.140e-5,
        "a'": 2.2328,
        "b'": -5.5779,
        "c'": 8.5325,
        "d'": -4.4352,
    },
    "NE2561": {
        "a": 1.0200,
        "b": 0.596e-3,
        "c": -1.551e-5,
        "a'": 2.4235,
        "b'": -6.3179,
        "c'": 9.4737,
        "d'": -4.8307,
    },
    "PR06C/G": {
        "a": 0.9519,
        "b": 2.432e-3,
        "c": -2.704e-5,
        "a'": 2.9110,
        "b'": -8.4916,
        "c'": 12.6817,
        "d'": -6.3874,
    },
}

KQ_ELECTRONS = {
    # Exradin
    "A12": {"kQ,ecal": 0.907, "a": 0.965, "b": 0.119, "c": 0.607},
    "A19": {"kQ,ecal": 0.904, "a": 0.957, "b": 0.119, "c": 0.505},
    "A12S": {"kQ,ecal": 0.907, "a": 0.937, "b": 0.136, "c": 0.378},
    "A18": {"kQ,ecal": 0.914, "a": 0.352, "b": 0.711, "c": 0.046},
    "A1SL": {"kQ,ecal": 0.914, "a": 0.205, "b": 0.854, "c": 0.036},
    # PTW
    "30010": {"kQ,ecal": 0.904, "a": 0.980, "b": 0.119, "c": 0.891},
    "30011": {"kQ,ecal": 0.901, "a": 0.976, "b": 0.120, "c": 0.793},
    "30012": {"kQ,ecal": 0.908, "a": 0.972, "b": 0.121, "c": 0.728},
    "30013": {"kQ,ecal": 0.901, "a": 0.978, "b": 0.112, "c": 0.816},
    "31013": {"kQ,ecal": 0.902, "a": 0.945, "b": 0.133, "c": 0.441},
    # IBA
    "FC65-G": {"kQ,ecal": 0.904, "a": 0.971, "b": 0.113, "c": 0.680},
    "FC65-P": {"kQ,ecal": 0.902, "a": 0.973, "b": 0.110, "c": 0.692},
    "FC23-C": {"kQ,ecal": 0.904, "a": 0.971, "b": 0.097, "c": 0.591},
    "CC25": {"kQ,ecal": 0.904, "a": 0.964, "b": 0.105, "c": 0.539},
    "CC13": {"kQ,ecal": 0.904, "a": 0.926, "b": 0.129, "c": 0.279},
    # Other
    "PR06C/G": {"kQ,ecal": 0.906, "a": 0.972, "b": 0.122, "c": 0.729},
    "NE2571": {"kQ,ecal": 0.903, "a": 0.977, "b": 0.117, "c": 0.817},
    "NE2611": {"kQ,ecal": 0.896, "a": 0.979, "b": 0.120, "c": 0.875},
}

LEAD_OPTIONS = {"None": None, "30cm": "30cm", "50cm": "50cm"}


[docs] def mmHg2kPa(mmHg: float) -> float: """Utility function to convert from mmHg to kPa.""" return mmHg * 101.33 / 760
[docs] def mbar2kPa(mbar: float) -> float: """Utility function to convert from millibars to kPa.""" return mbar / 10
[docs] def fahrenheit2celsius(f: float) -> float: """Utility function to convert from Fahrenheit to Celsius.""" return (f - 32) * 5 / 9
[docs] @argue.bounds(pdd2010=(0.5, 1)) def tpr2010_from_pdd2010(*, pdd2010: float) -> float: """Calculate TPR20,10 from PDD20,10. From TRS-398 footnote 25, section 6.3.1, p.68 (https://www-pub.iaea.org/MTCD/Publications/PDF/TRS398_scr.pdf), and Followill et al 1998 eqn 1.""" return 1.2661 * pdd2010 - 0.0595
[docs] def p_tp(*, temp: float, press: float) -> float: """Calculate the temperature & pressure correction. Parameters ---------- temp : float (17-27) The temperature in degrees Celsius. press : float (91-111) The value of pressure in kPa. Can be converted from mmHg and mbar; see :func:`~pylinac.calibration.tg51.mmHg2kPa` and :func:`~pylinac.calibration.tg51.mbar2kPa`. """ argue.verify_bounds( temp, bounds=(MIN_TEMP, MAX_TEMP), message="Temperature {:2.2f} out of range. Did you use Fahrenheit? Consider using the utility function fahrenheit2celsius()", ) argue.verify_bounds( press, bounds=(MIN_PRESSURE, MAX_PRESSURE), message="Pressure {:2.2f} out of range. Did you use kPa? Consider using the utility functions mmHg2kPa() or mbar2kPa()", ) return ((273.2 + temp) / 295.2) * (101.33 / press)
[docs] def p_pol(*, m_reference: NumberOrArray, m_opposite: NumberOrArray) -> float: """Calculate the polarity correction. Parameters ---------- m_reference : number, array The readings of the ion chamber at the reference polarity and voltage. m_opposite : number, array The readings of the ion chamber at the polarity opposite the reference. The sign does not make a difference. Raises ------ BoundsError if calculated Ppol is >1% from 1.0. """ mref_avg = np.mean(m_reference) mopp_avg = np.mean(m_opposite) polarity = (abs(mref_avg) + abs(mopp_avg)) / abs(2 * mref_avg) argue.verify_bounds( polarity, bounds=(MIN_PPOL, MAX_PPOL), message="Polarity correction {:2.2f} out of range (+/-2%). Verify inputs", ) return float(polarity)
[docs] def p_ion( *, voltage_reference: int, voltage_reduced: int, m_reference: NumberOrArray, m_reduced: NumberOrArray, ) -> float: """Calculate the ion chamber collection correction. Parameters ---------- voltage_reference : int The "high" voltage; same as the TG51 measurement voltage. voltage_reduced : int The "low" voltage; usually half of the high voltage. m_reference : float, iterable The readings of the ion chamber at the "high" voltage. m_reduced : float, iterable The readings of the ion chamber at the "low" voltage. Raises ------ BoundsError if calculated Pion is outside the range 1.00-1.05. """ ion = (1 - voltage_reference / voltage_reduced) / ( np.mean(m_reference) / np.mean(m_reduced) - voltage_reference / voltage_reduced ) argue.verify_bounds( ion, bounds=(MIN_PION, MAX_PION), message="Pion out of range (1.00-1.05). Check inputs or chamber", ) return float(ion)
[docs] def d_ref(*, i_50: float) -> float: """Calculate the dref of an electron beam based on the I50 depth. Parameters ---------- i_50 : float The value of I50 in cm. """ argue.verify_bounds(i_50, bounds=argue.POSITIVE, message="i50 should be positive") r50 = r_50(i_50=i_50) return 0.6 * r50 - 0.1
[docs] def r_50(*, i_50: float) -> float: """Calculate the R50 depth of an electron beam based on the I50 depth. Parameters ---------- i_50 : float The value of I50 in cm. """ argue.verify_bounds(i_50, bounds=argue.POSITIVE, message="i50 should be positive") if i_50 < 10: r50 = 1.029 * i_50 - 0.06 else: r50 = 1.59 * i_50 - 0.37 return r50
[docs] def kp_r50(*, r_50: float) -> float: """Calculate k'R50 for Farmer-like chambers. Parameters ---------- r_50 : float (2-9) The R50 value in cm. """ argue.verify_bounds(r_50, bounds=(2, 9)) return 0.9905 + 0.071 * np.exp(-r_50 / 3.67)
[docs] def pq_gr(*, m_dref_plus: NumberOrArray, m_dref: NumberOrArray) -> float: """Calculate PQ_gradient for a cylindrical chamber. Parameters ---------- m_dref_plus : float, iterable The readings of the ion chamber at dref + 0.5rcav. m_dref : float, iterable The readings of the ion chamber at dref. """ return float(np.mean(m_dref_plus) / np.mean(m_dref))
[docs] def m_corrected( *, p_ion: float, p_tp: float, p_elec: float, p_pol: float, m_reference: NumberOrArray, ) -> float: """Calculate M_corrected, the ion chamber reading with all corrections applied. Parameters ---------- p_ion : float (1.00-1.05) The ion collection correction. p_tp : float (0.92-1.08) The temperature & pressure correction. p_elec : float (0.98-1.02) The electrometer correction. p_pol : float (0.98-1.02) The polarity correction. m_reference : float, iterable The raw ion chamber reading(s). Returns ------- float """ argue.verify_bounds(p_ion, bounds=(MIN_PION, MAX_PION)) argue.verify_bounds(p_tp, bounds=(MIN_PTP, MAX_PTP)) argue.verify_bounds(p_elec, bounds=(MIN_PELEC, MAX_PELEC)) argue.verify_bounds(p_pol, bounds=(MIN_PPOL, MAX_PPOL)) return float(p_ion * p_tp * p_elec * p_pol * np.mean(m_reference))
[docs] @argue.bounds(pdd=(62.7, 89.0)) @argue.options(lead_foil=LEAD_OPTIONS.values()) def pddx(*, pdd: float, energy: int, lead_foil: Optional[str] = None) -> float: """Calculate PDDx based on the PDD. Parameters ---------- pdd : {>62.7, <89.0} The measured PDD. If lead foil was used, this assumes the pdd as measured with the lead in place. energy : int The nominal energy in MV. lead_foil : {None, '30cm', '50cm'} Applicable only for energies >10MV. Whether a lead foil was used to acquire the pdd. Use ``None`` if no lead foil was used and the interim equation should be used. This is the default Use ``50cm`` if the lead foil was set to 50cm from the phantom surface. Use ``30cm`` if the lead foil was set to 30cm from the phantom surface. """ if energy < 10: return pdd elif energy >= 10: if lead_foil is None: if pdd <= 75: return pdd elif 75 < pdd <= 89: return 1.267 * pdd - 20 else: raise ValueError(f"PDD value of {pdd} was outside the bound of 89%") elif lead_foil == LEAD_OPTIONS["50cm"]: if pdd < 73: return pdd else: return (0.8905 + 0.0015 * pdd) * pdd elif lead_foil == LEAD_OPTIONS["30cm"]: if pdd < 71: return pdd else: return (0.8116 + 0.00264 * pdd) * pdd
[docs] @argue.bounds(pddx=(63.0, 86.0)) @argue.options(chamber=KQ_PHOTONS.keys()) def kq_photon_pddx(*, chamber: str, pddx: float) -> float: """Calculate kQ based on the chamber and clinical measurements of PDD(10)x. This will calculate kQ for photons for *CYLINDRICAL* chambers only. Parameters ---------- chamber : str The chamber of the chamber. Valid values are those listed in Table III of Muir and Rogers and Table I of the TG-51 Addendum. pddx : {>63.0, <86.0} The **PHOTON-ONLY** PDD measurement at 10cm depth for a 10x10cm2 field. .. note:: Use the :func:`~pylinac.calibration.tg51.pddx` function to convert PDD to PDDx as needed. .. note:: Muir and Rogers state limits of 0.627 - 0.861. The TG-51 addendum states them as 0.63 and 0.86. The TG-51 addendum limits are used here. """ ch = KQ_PHOTONS[chamber] return ch["a"] + ch["b"] * pddx + ch["c"] * (pddx**2)
[docs] @argue.bounds(tpr=(0.623, 0.805)) @argue.options(chamber=KQ_PHOTONS.keys()) def kq_photon_tpr(*, chamber: str, tpr: float) -> float: """Calculate kQ based on the chamber and clinical measurements of TPR20,10. This will calculate kQ for photons for *CYLINDRICAL* chambers only. Parameters ---------- chamber : str The chamber of the chamber. Valid values are those listed in Table III of Muir and Rogers and Table I of the TG-51 Addendum. tpr : {>0.630, <0.860} The TPR(20,10) value. .. note:: Use the :func:`~pylinac.calibration.tg51.tpr2010_from_pdd2010` function to convert from PDD without needing to take TPR measurements. """ ch = KQ_PHOTONS[chamber] return ch["a'"] + ch["b'"] * tpr + ch["c'"] * (tpr**2) + ch["d'"] * (tpr**3)
[docs] @argue.options(chamber=KQ_ELECTRONS.keys()) def kq_electron(*, chamber: str, r_50: float) -> float: """Calculate kQ based on the chamber and clinical measurements. This will calculate kQ for electrons for *CYLINDRICAL* chambers only according to Muir & Rogers. Parameters ---------- chamber : str The chamber of the chamber. Valid values are those listed in Tables VI and VII of Muir and Rogers 2014. r_50 : float The R50 value in cm of an electron beam. """ ch = KQ_ELECTRONS[chamber] return (ch["a"] + ch["b"] * r_50 ** -ch["c"]) * ch["kQ,ecal"]
class TG51Base(Structure): @property def p_tp(self) -> float: """Temperature/Pressure correction.""" return p_tp(temp=self.temp, press=self.press) @property def p_ion(self) -> float: """Ionization collection correction.""" return p_ion( voltage_reference=self.voltage_reference, voltage_reduced=self.voltage_reduced, m_reference=self.m_reference, m_reduced=self.m_reduced, ) @property def p_pol(self) -> float: """Polarity correction.""" return p_pol(m_reference=self.m_reference, m_opposite=self.m_opposite) @property def m_corrected(self) -> float: """Corrected chamber reading.""" return m_corrected( p_ion=self.p_ion, p_tp=self.p_tp, p_elec=self.p_elec, p_pol=self.p_pol, m_reference=self.m_reference, ) @property def m_corrected_adjustment(self) -> float: """Corrected chamber reading after adjusting the output.""" if self.m_reference_adjusted is not None: return m_corrected( p_ion=self.p_ion, p_tp=self.p_tp, p_elec=self.p_elec, p_pol=self.p_pol, m_reference=self.m_reference_adjusted, ) @property def output_was_adjusted(self) -> float: """Boolean specifiying if output was adjusted.""" return self.m_reference_adjusted is not None
[docs] class TG51Photon(TG51Base): """Class for calculating absolute dose to water using a cylindrical chamber in a photon beam. Parameters ---------- institution : str Institution name. physicist : str Physicist performing calibration. unit : str Unit name; e.g. TrueBeam1. measurement_date : str Date of measurement. E.g. 10/22/2018. temp : float The temperature in Celsius. Use :func:`~pylinac.calibration.tg51.fahrenheit2celsius` to convert if necessary. press : float The value of pressure in kPa. Can be converted from mmHg and mbar; see :func:`~pylinac.calibration.tg51.mmHg2kPa` and :func:`~pylinac.calibration.tg51.mbar2kPa`. energy : float Nominal energy of the beam in MV. chamber : str Chamber model. Must be one of the listed chambers in TG-51 Addendum. n_dw : float NDW value in Gy/nC. p_elec : float Electrometer correction factor; given by the calibration laboratory. measured_pdd10 : float The measured value of PDD(10); will be converted to PDDx(10) and used for calculating kq. lead_foil : {None, '50cm', '30cm'} Whether a lead foil was used to acquire PDD(10)x and where its position was. Used to calculate kq. clinical_pdd10 : float The PDD used to correct the dose at 10cm back to dmax. Usually the TPS PDD(10) value. voltage_reference : int Reference voltage; i.e. voltage when taking the calibration measurement. voltage_reduced : int Reduced voltage; usually half of the reference voltage. m_reference : float, tuple Ion chamber reading(s) at the reference voltage. m_opposite : float, tuple Ion chamber reading(s) at the opposite voltage of reference. m_reduced : float, tuple Ion chamber reading(s) at the reduced voltage. mu : int The MU delivered to measure the reference reading. E.g. 200. fff : bool Whether the beam is FFF or flat. tissue_correction : float Correction value to calibration to, e.g., muscle. A value of 1.0 means no correction (i.e. water). """ @argue.options(chamber=KQ_PHOTONS.keys(), lead_foil=LEAD_OPTIONS.values()) def __init__( self, *, institution: str = "", physicist: str = "", unit: str, measurement_date: str = "", temp: float, press: float, chamber: str, n_dw: float, p_elec: float, electrometer: str = "", measured_pdd10: Optional[float] = None, lead_foil: Optional[str] = None, clinical_pdd10: float, energy: int, fff: bool = False, voltage_reference: int, voltage_reduced: int, m_reference: NumberOrArray, m_opposite: NumberOrArray, m_reduced: NumberOrArray, mu: int, tissue_correction: float = 1.0, m_reference_adjusted: Optional[NumberOrArray] = None, ): super().__init__( temp=temp, press=press, chamber=chamber, n_dw=n_dw, p_elec=p_elec, measured_pdd10=measured_pdd10, energy=energy, voltage_reference=voltage_reference, voltage_reduced=voltage_reduced, m_reference=m_reference, m_opposite=m_opposite, m_reduced=m_reduced, clinical_pdd10=clinical_pdd10, mu=mu, tissue_correction=tissue_correction, lead_foil=lead_foil, electrometer=electrometer, m_reference_adjusted=m_reference_adjusted, institution=institution, physicist=physicist, unit=unit, measurement_date=measurement_date, fff=fff, ) # add check for tpr vs pdd @property def pddx(self) -> float: """The photon-only PDD(10) value.""" return pddx( pdd=self.measured_pdd10, energy=self.energy, lead_foil=self.lead_foil ) @property def kq(self) -> float: """The chamber-specific beam quality correction factor.""" return kq_photon_pddx(chamber=self.chamber, pddx=self.pddx) @property def dose_mu_10(self) -> float: """cGy/MU at a depth of 10cm.""" return self.tissue_correction * self.m_corrected * self.kq * self.n_dw / self.mu @property def dose_mu_dmax(self) -> float: """cGy/MU at a depth of dmax.""" return self.dose_mu_10 / (self.clinical_pdd10 / 100) @property def dose_mu_10_adjusted(self) -> float: """The dose/mu at 10cm depth after adjustment.""" return ( self.tissue_correction * self.m_corrected_adjustment * self.kq * self.n_dw / self.mu ) @property def dose_mu_dmax_adjusted(self) -> float: """The dose/mu at dmax depth after adjustment.""" return self.dose_mu_10_adjusted / (self.clinical_pdd10 / 100)
[docs] def publish_pdf( self, filename: str, notes: Optional[list] = None, open_file: bool = False, metadata: Optional[dict] = None, ): """Publish (print) a PDF containing the analysis and quantitative results. Parameters ---------- filename : str, file-like object The file to write the results to. notes : str, list Any notes to be added to the report. If a string, adds everything as one line. If a list, must be a list of strings; each string item will be a new line. open_file : bool Whether to open the file after creation. Will use the default PDF program. metadata : dict Any data that should be appended to every page of the report. This differs from notes in that metadata is at the top of every page while notes is at the bottom of the report. """ was_adjusted = "Yes" if self.output_was_adjusted else "No" title = [ "TG-51 Photon Report", f"{self.unit} - {self.energy} MV{' FFF' if self.fff else ''}", ] canvas = PylinacCanvas(filename, page_title=title, metadata=metadata) text = [ "Site Data:", f"Institution: {self.institution}", f"Performed by: {self.physicist}", f"Measurement Date: {self.measurement_date}", f'Date of Report: {datetime.now().strftime("%A, %B %d, %Y")}', f"Unit: {self.unit}", f"Energy: {self.energy} MV {'FFF' if self.fff else ''}", "", "Instrumentation:", f"Chamber: {self.chamber}", f"Chamber Calibration Factor Ndw (cGy/nC): {self.n_dw:2.3f}", f"Electrometer: {self.electrometer}", f"Pelec: {self.p_elec:2.3f}", f"MU: {self.mu}", "", "Beam Quality:", f"Lead foil: {'No' if self.lead_foil is None else self.lead_foil}", f"Measured PDD(10){'' if self.lead_foil is None else 'Pb'} {self.measured_pdd10:2.2f}", f"Calculated PDD(10)x: {self.pddx:2.2f}", f"Determined kQ: {self.kq:2.3f}", "", "Chamber Corrections/Measurements:", f"Temperature (\N{DEGREE SIGN}C): {self.temp:2.1f}", f"Pressure (kPa): {self.press:2.1f}", f"Mraw @ ({self.voltage_reference}V, Reference) (nC): {self.m_reference}", f"Mraw @ ({self.voltage_reduced}V, Reduced) (nC): {self.m_reduced}", f"Mraw @ ({-self.voltage_reference}V, Opposite) (nC): {self.m_opposite}", f"Ptp: {self.p_tp:2.3f}", f"Pion: {self.p_ion:2.3f}", f"Ppol: {self.p_pol:2.3f}", "", "Dose Determination:", f"Fully corrected M (nC): {self.m_corrected:2.3f}", f"Tissue correction (e.g. muscle): {self.tissue_correction:2.3f}", f"Dose/MU @ 10cm depth (cGy): {self.dose_mu_10:2.3f}", f"Clinical PDD (%): {self.clinical_pdd10:2.2f}", f"Dose/MU @ dmax (cGy): {self.dose_mu_dmax:2.3f}", "", f"Output Adjustment?: {was_adjusted}", ] if was_adjusted == "Yes": text.append( f"Adjusted Mraw @ reference voltage (nC): {self.m_reference_adjusted}" ) text.append( f"Adjusted fully corrected M (nC): {self.m_corrected_adjustment:2.3f}" ) text.append( f"Adjusted Dose/MU @ 10cm depth (cGy): {self.dose_mu_10_adjusted:2.3f}" ) text.append( f"Adjusted Dose/MU @ dmax (cGy): {self.dose_mu_dmax_adjusted:2.3f}" ) canvas.add_text(text=text, location=(2, 25.5), font_size=12) if notes is not None: canvas.add_text(text="Notes:", location=(12, 6.5), font_size=14) canvas.add_text(text=notes, location=(12, 6)) canvas.finish() if open_file: webbrowser.open(filename)
[docs] class TG51ElectronLegacy(TG51Base): """Class for calculating absolute dose to water using a cylindrical chamber in an electron beam. Parameters ---------- institution : str Institution name. physicist : str Physicist performing calibration. unit : str Unit name; e.g. TrueBeam1. measurement_date : str Date of measurement. E.g. 10/22/2018. temp : float (17-27) The temperature in degrees Celsius. press : float (91-111) The value of pressure in kPa. Can be converted from mmHg and mbar; see :func:`~pylinac.calibration.tg51.mmHg2kPa` and :func:`~pylinac.calibration.tg51.mbar2kPa`. chamber : str Chamber model; only for bookkeeping. n_dw : float NDW value in Gy/nC. Given by the calibration laboratory. k_ecal : float Kecal value which is chamber specific. This value is the major difference between the legacy class and modern class where no kecal is needed. p_elec : float Electrometer correction factor; given by the calibration laboratory. clinical_pdd : float The PDD used to correct the dose back to dref. voltage_reference : float Reference voltage; i.e. voltage when taking the calibration measurement. voltage_reduced : float Reduced voltage; usually half of the reference voltage. m_reference : float, tuple Ion chamber reading(s) at the reference voltage. m_opposite : float, tuple Ion chamber reading(s) at the opposite voltage of reference. m_reduced : float, tuple Ion chamber reading(s) at the reduced voltage. mu : int The MU delivered to measure the reference reading. E.g. 200. i_50 : float Depth of 50% ionization. tissue_correction : float Correction value to calibration to, e.g., muscle. A value of 1.0 means no correction (i.e. water). """ def __init__( self, *, institution: str = "", physicist: str = "", unit: str = "", measurement_date: str = "", energy: int, temp: float, press: float, chamber: str, k_ecal: float, n_dw: float, electrometer: str = "", p_elec: float, clinical_pdd: float, voltage_reference: int, voltage_reduced: int, m_reference: NumberOrArray, m_opposite: NumberOrArray, m_reduced: NumberOrArray, m_gradient: NumberOrArray, cone: str, mu: int, i_50: float, tissue_correction: float = 1.0, m_reference_adjusted=None, ): super().__init__( temp=temp, press=press, chamber=chamber, n_dw=n_dw, p_elec=p_elec, voltage_reference=voltage_reference, voltage_reduced=voltage_reduced, m_reference=m_reference, m_opposite=m_opposite, m_reduced=m_reduced, clinical_pdd=clinical_pdd, mu=mu, i_50=i_50, tissue_correction=tissue_correction, institution=institution, physicist=physicist, unit=unit, measurement_date=measurement_date, electrometer=electrometer, m_reference_adjusted=m_reference_adjusted, cone=cone, energy=energy, k_ecal=k_ecal, m_gradient=m_gradient, ) @property def r_50(self) -> float: """Depth of the 50% dose value.""" return r_50(i_50=self.i_50) @property def dref(self) -> float: """Depth of the reference point.""" return d_ref(i_50=self.i_50) @property def pq_gr(self): """Gradient factor""" return pq_gr(m_dref_plus=self.m_gradient, m_dref=self.m_reference) @property def kq(self) -> float: """The kQ value using classic TG-51""" return self.k_ecal * kp_r50(r_50=self.r_50) @property def dose_mu_dref(self) -> float: """cGy/MU at the depth of Dref.""" return ( self.tissue_correction * self.m_corrected * self.kq * self.n_dw * self.pq_gr / self.mu ) @property def dose_mu_dmax(self) -> float: """cGy/MU at the depth of dmax.""" return self.dose_mu_dref / (self.clinical_pdd / 100) @property def dose_mu_dref_adjusted(self) -> float: """cGy/MU at the depth of Dref.""" return ( self.tissue_correction * self.m_corrected_adjustment * self.kq * self.n_dw * self.pq_gr / self.mu ) @property def dose_mu_dmax_adjusted(self) -> float: """cGy/MU at the depth of dmax.""" return self.dose_mu_dref_adjusted / (self.clinical_pdd / 100)
[docs] def publish_pdf( self, filename: str, notes: Optional[list] = None, open_file: bool = False, metadata: Optional[dict] = None, ): """Publish (print) a PDF containing the analysis and quantitative results. Parameters ---------- filename : str, file-like object The file to write the results to. notes : str, list Any notes to be added to the report. If a string, adds everything as one line. If a list, must be a list of strings; each string item will be a new line. open_file : bool Whether to open the file after creation. Will use the default PDF program. metadata : dict Any data that should be appended to every page of the report. This differs from notes in that metadata is at the top of every page while notes is at the bottom of the report. """ was_adjusted = "Yes" if self.output_was_adjusted else "No" title = ["TG-51 Electron Report (Legacy)", f"{self.unit} - {self.energy} MeV"] canvas = PylinacCanvas(filename, page_title=title, metadata=metadata) text = [ "Site Data:", f"Institution: {self.institution}", f"Performed by: {self.physicist}", f"Measurement Date: {self.measurement_date}", f'Date of Report: {datetime.now().strftime("%A, %B %d, %Y")}', f"Unit: {self.unit}", f"Energy: {self.energy} MeV", f"Cone: {self.cone}", f"MU: {self.mu}", "", "Instrumentation:", f"Chamber chamber: {self.chamber}", f"Chamber Calibration Factor Ndw (cGy/nC): {self.n_dw:2.3f}", f"Electrometer: {self.electrometer}", f"Pelec: {self.p_elec:2.2f}", "", "Beam Quality:", f"I50 (cm): {self.i_50:2.2f}", f"R50 (cm): {self.r_50:2.2f}", f"Dref (cm): {self.dref:2.2f}", f"Kecal: {self.k_ecal:2.3f}", f"kQ: {self.kq:2.3f}", "", "Chamber Corrections/Measurements:", f"Temperature (\N{DEGREE SIGN}C): {self.temp:2.1f}", f"Pressure (kPa): {self.press:2.1f}", f"Mraw @ ({self.voltage_reference}V, Reference) (nC): {self.m_reference}", f"Mraw @ ({self.voltage_reduced}V, Reduced) (nC): {self.m_reduced}", f"Mraw @ ({-self.voltage_reference}V, Opposite) (nC): {self.m_opposite}", f"Ptp: {self.p_tp:2.3f}", f"Pion: {self.p_ion:2.3f}", f"Ppol: {self.p_pol:2.3f}", f"Mraw @ Dref + 0.5rcav (nC): {self.m_gradient}", "", "Dose Determination:", f"Fully corrected M (nC): {self.m_corrected:2.3f}", f"Tissue correction (e.g. muscle): {self.tissue_correction:2.3f}", f"Dose/MU @ Dref depth (cGy): {self.dose_mu_dref:2.3f}", f"Clinical PDD (%): {self.clinical_pdd:2.2f}", f"Dose/MU @ dmax (cGy): {self.dose_mu_dmax:2.3f}", "", f"Output Adjustment?: {was_adjusted}", ] if was_adjusted == "Yes": text.append( f"Adjusted Mraw @ reference voltage (nC): {self.m_reference_adjustment}" ) text.append( f"Adjusted fully corrected M (nC): {self.m_corrected_adjustment:2.3f}" ) text.append( f"Adjusted Dose/MU @ dref depth (cGy): {self.dose_mu_dref_adjusted:2.3f}" ) text.append( f"Adjusted Dose/MU @ dmax (cGy): {self.dose_mu_dmax_adjusted:2.3f}" ) canvas.add_text(text=text, location=(2, 25.5), font_size=11) if notes is not None: canvas.add_text(text="Notes:", location=(12, 6.5), font_size=14) canvas.add_text(text=notes, location=(12, 6)) canvas.finish() if open_file: webbrowser.open(filename)
[docs] class TG51ElectronModern(TG51Base): """Class for calculating absolute dose to water using a cylindrical chamber in an electron beam. .. warning:: This class uses the values of Muir & Rogers. These values are likely to be included in the new TG-51 addendum, but are not official. The results can be up to 1% different. Physicists should use their own judgement when deciding which class to use. To use a manual kecal value, Pgradient and the classic TG-51 equations use the :class:`~pylinac.calibration.tg51.TG51ElectronLegacy` class. Parameters ---------- institution : str Institution name. physicist : str Physicist performing calibration. unit : str Unit name; e.g. TrueBeam1. measurement_date : str Date of measurement. E.g. 10/22/2018. press : float The value of pressure in kPa. Can be converted from mmHg and mbar; see :func:`~pylinac.calibration.tg51.mmHg2kPa` and :func:`~pylinac.calibration.tg51.mbar2kPa`. temp : float The temperature in Celsius. voltage_reference : int The reference voltage; i.e. the voltage for the calibration reading (e.g. 300V). voltage_reduced : int The reduced voltage, usually a fraction of the reference voltage (e.g. 150V). m_reference : array, float The reading(s) of the chamber at reference voltage. m_reduced : array, float The reading(s) of the chamber at the reduced voltage. m_opposite : array, float The reading(s) of the chamber at the opposite voltage from reference. Sign of the reading does not matter. chamber : str Ion chamber model. n_dw : float NDW value in Gy/nC p_elec : float Electrometer correction given by the calibration laboratory. clinical_pdd : float The PDD used to correct the dose back to dref. mu : int MU delivered. i_50 : float Depth of 50% ionization tissue_correction : float Correction value to calibration to, e.g., muscle. A value of 1.0 means no correction (i.e. water). """ def __init__( self, *, institution: str = "", physicist: str = "", unit: str = "", measurement_date: str = "", energy: int, temp: float, press: float, chamber: str, n_dw: float, electrometer: str = "", p_elec: float, clinical_pdd: float, voltage_reference: int, voltage_reduced: int, m_reference: NumberOrArray, m_opposite: NumberOrArray, m_reduced: NumberOrArray, cone: str, mu: int, i_50: float, tissue_correction: float, m_reference_adjusted=None, ): super().__init__( temp=temp, press=press, chamber=chamber, n_dw=n_dw, p_elec=p_elec, voltage_reference=voltage_reference, voltage_reduced=voltage_reduced, m_reference=m_reference, m_opposite=m_opposite, m_reduced=m_reduced, clinical_pdd=clinical_pdd, mu=mu, i_50=i_50, tissue_correction=tissue_correction, institution=institution, physicist=physicist, unit=unit, measurement_date=measurement_date, electrometer=electrometer, m_reference_adjusted=m_reference_adjusted, cone=cone, energy=energy, ) @property def r_50(self) -> float: """Depth of the 50% dose value.""" return r_50(i_50=self.i_50) @property def dref(self) -> float: """Depth of the reference point.""" return d_ref(i_50=self.i_50) @property def kq(self) -> float: """The kQ value using the updated Muir & Rogers values from their 2014 paper, equation 11, or classically if kecal is passed.""" return kq_electron(chamber=self.chamber, r_50=self.r_50) @property def dose_mu_dref(self) -> float: """cGy/MU at the depth of Dref.""" return self.tissue_correction * self.m_corrected * self.kq * self.n_dw / self.mu @property def dose_mu_dmax(self) -> float: """cGy/MU at the depth of dmax.""" return self.dose_mu_dref / (self.clinical_pdd / 100) @property def dose_mu_dref_adjusted(self) -> float: """cGy/MU at the depth of Dref.""" return ( self.tissue_correction * self.m_corrected_adjusted * self.kq * self.n_dw / self.mu ) @property def dose_mu_dmax_adjusted(self) -> float: """cGy/MU at the depth of dmax.""" return self.dose_mu_dref_adjusted / (self.clinical_pdd / 100)
[docs] def publish_pdf( self, filename: str, notes: Optional[list] = None, open_file: bool = False, metadata: Optional[dict] = None, ): """Publish (print) a PDF containing the analysis and quantitative results. Parameters ---------- filename : str, file-like object The file to write the results to. notes : str, list Any notes to be added to the report. If a string, adds everything as one line. If a list, must be a list of strings; each string item will be a new line. open_file : bool Whether to open the file after creation. Will use the default PDF program. metadata : dict Any data that should be appended to every page of the report. This differs from notes in that metadata is at the top of every page while notes is at the bottom of the report. """ was_adjusted = "Yes" if self.output_was_adjusted else "No" title = ["TG-51 Electron Report (Modern)", f"{self.unit} - {self.energy} MeV"] canvas = PylinacCanvas(filename, page_title=title, metadata=metadata) text = [ "Site Data:", f"Institution: {self.institution}", f"Performed by: {self.physicist}", f"Measurement Date: {self.measurement_date}", f'Date of Report: {datetime.now().strftime("%A, %B %d, %Y")}', f"Unit: {self.unit}", f"Energy: {self.energy} MeV", f"Cone: {self.cone}", f"MU: {self.mu}", "", "Instrumentation:", f"Chamber: {self.chamber}", f"Chamber Calibration Factor Ndw (cGy/nC): {self.n_dw:2.3f}", f"Electrometer: {self.electrometer}", f"Pelec: {self.p_elec:2.2f}", "", "Beam Quality:", f"I50 (cm): {self.i_50:2.2f}", f"R50 (cm): {self.r_50:2.2f}", f"Dref (cm): {self.dref:2.2f}", f"Calculated kQ: {self.kq:2.3f}", "", "Chamber Corrections/Measurements:", f"Temperature (\N{DEGREE SIGN}C): {self.temp:2.1f}", f"Pressure (kPa): {self.press:2.1f}", f"Mraw @ ({self.voltage_reference}V, Reference) (nC): {self.m_reference}", f"Mraw @ ({self.voltage_reduced}V, Reduced) (nC): {self.m_reduced}", f"Mraw @ ({-self.voltage_reference}V, Opposite) (nC): {self.m_opposite}", f"Ptp: {self.p_tp:2.3f}", f"Pion: {self.p_ion:2.3f}", f"Ppol: {self.p_pol:2.3f}", "", "Dose Determination:", f"Fully corrected M (nC): {self.m_corrected:2.3f}", f"Tissue correction (e.g. muscle): {self.tissue_correction:2.3f}", f"Dose/MU @ Dref depth (cGy): {self.dose_mu_dref:2.3f}", f"Clinical PDD (%): {self.clinical_pdd:2.2f}", f"Dose/MU @ dmax (cGy): {self.dose_mu_dmax:2.3f}", "", f"Output Adjustment?: {was_adjusted}", ] if was_adjusted == "Yes": text.append( f"Adjusted corrected M @ reference voltage (nC): {self.m_corrected_adjustment}" ) text.append( f"Adjusted fully corrected M (nC): {self.m_corrected_adjustment:2.3f}" ) text.append( f"Adjusted Dose/MU @ dref depth (cGy): {self.dose_mu_dref_adjusted:2.3f}" ) text.append( f"Adjusted Dose/MU @ dmax (cGy): {self.dose_mu_dmax_adjusted:2.3f}" ) canvas.add_text(text=text, location=(2, 25.5), font_size=11) if notes is not None: canvas.add_text(text="Notes:", location=(12, 6.5), font_size=14) canvas.add_text(text=notes, location=(12, 6)) canvas.finish() if open_file: webbrowser.open(filename)