Skip to content

Commit

Permalink
Closes #353
Browse files Browse the repository at this point in the history
  • Loading branch information
jrkerns committed Apr 5, 2021
1 parent 0232234 commit a5be5a2
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 15 deletions.
22 changes: 22 additions & 0 deletions docs/source/calibration_docs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,28 @@ Vocabulary that may be different than the protocol

Vocabulary not listed here should be the same as the respective protocol.

Changing a bound
----------------

Bounds are placed in the module to prevent catastrophic errors from passing in the wrong values; e.g. the wrong units.
If you live in a place that has extreme temperatures or pressures or just otherwise want to change the default bounds,
you can change the default range of acceptable values.
E.g. to change the minimum allowable temperature that can be passed:

.. code-block:: python
from pylinac import tg51
tg51.p_tp(temp=5, press=100) # will raise bounds error
# override
tg51.MIN_TEMP = 0
tg51.p_tp(temp=5, press=100) # no bounds error will be raised
You can override the min/max of temp, pressure, p ion, p elec, p tp, p pol. These bounds are the same for TRS-398.
I.e. setting these in either module will set them for both modules.

TG-51
-----

Expand Down
8 changes: 8 additions & 0 deletions docs/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
Changelog
=========

v 2.6.0
-------

Calibration
^^^^^^^^^^^

* `#353 <https://github.com/jrkerns/pylinac/issues/353>`_ The bounds for most functions/methods have been converted to constants. This lets users override the default values should they wish it.

v 2.5.0
-------

Expand Down
41 changes: 30 additions & 11 deletions pylinac/calibration/tg51.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,22 @@
import argue
import numpy as np

from ..core.pdf import PylinacCanvas
from ..core.typing import NumberLike, NumberOrArray
from ..core.utilities import Structure, open_path
from ..core.pdf import PylinacCanvas

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 = {
Expand Down Expand Up @@ -117,8 +130,6 @@ def tpr2010_from_pdd2010(*, pdd2010: float) -> float:
return 1.2661*pdd2010 - 0.0595


@argue.bounds(temp=(17, 27), message="Temperature {:2.2f} out of range. Did you use Fahrenheit? Consider using the utility function fahrenheit2celsius()")
@argue.bounds(press=(91, 111), message="Pressure {:2.2f} out of range. Did you use kPa? Consider using the utility functions mmHg2kPa() or mbar2kPa()")
def p_tp(*, temp: NumberLike, press: NumberLike) -> float:
"""Calculate the temperature & pressure correction.
Expand All @@ -127,9 +138,14 @@ def p_tp(*, temp: NumberLike, press: NumberLike) -> float:
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`.
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`.
"""
return ((273.2+temp)/295.2)*(101.33/press)
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)


def p_pol(*, m_reference: NumberOrArray, m_opposite: NumberOrArray) -> float:
Expand All @@ -149,7 +165,7 @@ def p_pol(*, m_reference: NumberOrArray, m_opposite: NumberOrArray) -> float:
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=(0.99, 1.01), message="Polarity correction {:2.2f} out of range (+/-2%). Verify inputs")
argue.verify_bounds(polarity, bounds=(MIN_PPOL, MAX_PPOL), message="Polarity correction {:2.2f} out of range (+/-2%). Verify inputs")
return float(polarity)


Expand All @@ -172,11 +188,10 @@ def p_ion(*, voltage_reference: int, voltage_reduced: int, m_reference: NumberOr
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=(1, 1.05), message="Pion out of range (1.00-1.05). Check inputs or chamber")
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)


@argue.bounds(i_50=argue.POSITIVE, message="i50 should be positive")
def d_ref(*, i_50: float) -> float:
"""Calculate the dref of an electron beam based on the I50 depth.
Expand All @@ -185,11 +200,11 @@ def d_ref(*, i_50: float) -> float:
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


@argue.bounds(i_50=argue.POSITIVE, message="i50 should be positive")
def r_50(*, i_50: float) -> float:
"""Calculate the R50 depth of an electron beam based on the I50 depth.
Expand All @@ -198,14 +213,14 @@ def r_50(*, i_50: float) -> float:
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


@argue.bounds(r_50=(2, 9))
def kp_r50(*, r_50: float) -> float:
"""Calculate k'R50 for Farmer-like chambers.
Expand All @@ -214,6 +229,7 @@ def kp_r50(*, r_50: float) -> float:
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)


Expand All @@ -230,7 +246,6 @@ def pq_gr(*, m_dref_plus: NumberOrArray, m_dref: NumberOrArray) -> float:
return float(np.mean(m_dref_plus) / np.mean(m_dref))


@argue.bounds(p_ion=(1, 1.05), p_tp=(0.92, 1.08), p_elec=(0.98, 1.02), p_pol=(0.98, 1.02))
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.
Expand All @@ -251,6 +266,10 @@ def m_corrected(*, p_ion: float, p_tp: float, p_elec: float, p_pol: float, m_ref
-------
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))


Expand Down
13 changes: 9 additions & 4 deletions pylinac/calibration/trs398.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from abc import ABC
from datetime import datetime
from typing import Union, List, Optional

Expand All @@ -6,7 +7,8 @@

from pylinac.core.pdf import PylinacCanvas
from . import tg51 as _tg51
from .tg51 import mmHg2kPa, mbar2kPa, fahrenheit2celsius, tpr2010_from_pdd2010 # make available to module
from .tg51 import mmHg2kPa, mbar2kPa, fahrenheit2celsius, tpr2010_from_pdd2010, MIN_PTP, \
MAX_PTP, MIN_PELEC, MAX_PELEC, MIN_PPOL, MAX_PPOL, MIN_PION, MAX_PION # make available to module
from ..core.utilities import is_close, Structure, open_path
from ..core.typing import NumberOrArray

Expand Down Expand Up @@ -133,7 +135,7 @@ def k_s(*, voltage_reference: int, voltage_reduced: int,
_verify_voltage_ratio_is_valid(v_ratio)
a = V1_V2_FITS[v_ratio]
m_ratio = np.mean(m_reference) / np.mean(m_reduced)
argue.verify_bounds(m_ratio, bounds=(1.0, 1.05), message="Ks is out of bounds. Verify inputs or check chamber")
argue.verify_bounds(m_ratio, bounds=(MIN_PION, MAX_PION), message="Ks is out of bounds. Verify inputs or check chamber")
return float(a['a0'] + a['a1']*m_ratio + a['a2']*(m_ratio**2))


Expand Down Expand Up @@ -199,7 +201,6 @@ def kq_electron(*, chamber: str, r_50: float) -> float:
return np.interp([r_50], KQ_ELECTRON_R50S, KQ_ELECTRON_CHAMBERS[chamber])[0]


@argue.bounds(k_tp=(0.9, 1.1), k_elec=(0.95, 1.05), k_pol=(0.95, 1.05), k_s=(1.0, 1.05))
def m_corrected(*, m_reference, k_tp, k_elec, k_pol, k_s) -> float:
"""The fully corrected chamber reading.
Expand All @@ -221,10 +222,14 @@ def m_corrected(*, m_reference, k_tp, k_elec, k_pol, k_s) -> float:
m : float
The fully corrected chamber reading.
"""
argue.verify_bounds(k_tp, bounds=(MIN_PTP, MAX_PTP))
argue.verify_bounds(k_elec, bounds=(MIN_PELEC, MAX_PELEC))
argue.verify_bounds(k_pol, bounds=(MIN_PPOL, MAX_PPOL))
argue.verify_bounds(k_s, bounds=(MIN_PION, MAX_PION))
return float(np.mean(m_reference) * k_tp * k_elec * k_pol * k_s)


class TRS398Base(Structure):
class TRS398Base(ABC, Structure):

@property
def k_tp(self):
Expand Down
12 changes: 12 additions & 0 deletions tests_basic/test_tg51.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import copy
from unittest import TestCase

from argue import BoundsError

from pylinac.calibration import tg51

from tests_basic.utils import save_file
Expand All @@ -14,6 +17,15 @@ def test_p_tp(self):
for temp, press, exp in zip(temps, press, expected_ptp):
self.assertAlmostEqual(tg51.p_tp(temp=temp, press=press), exp, delta=0.001)

def test_override_p_tp(self):
original_temp = copy.copy(tg51.MAX_TEMP)
tg51.MAX_TEMP = 20
temp = 22
press = 101.33
with self.assertRaises(BoundsError):
tg51.p_tp(temp=temp, press=press)
tg51.MAX_TEMP = original_temp # set back so other tests don't fail

def test_p_pol(self):
m_ref = (20, -20.2, 19.8)
m_opposite = (-20, 19.8, -20.1)
Expand Down
10 changes: 10 additions & 0 deletions tests_basic/test_trs398.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import copy
from unittest import TestCase

from argue import BoundsError

from pylinac.calibration import trs398
from tests_basic.utils import save_file

Expand All @@ -14,6 +17,13 @@ def test_k_s(self):
self.assertAlmostEqual(trs398.k_s(voltage_reference=300, voltage_reduced=150, m_reference=high, m_reduced=low), exp,
delta=0.001)

def test_override_k_s(self):
original_max_pion = copy.copy(trs398.MAX_PION)
trs398.MAX_PION = 1
with self.assertRaises(BoundsError):
trs398.k_s(voltage_reference=300, voltage_reduced=150, m_reference=22, m_reduced=20)
trs398.MAX_PION = original_max_pion

def test_m_corrected(self):
exp = 20.225
res = trs398.m_corrected(k_s=1.01, k_tp=0.995, k_elec=1, k_pol=1.005, m_reference=(20, 20.05))
Expand Down

0 comments on commit a5be5a2

Please sign in to comment.