From 939ca3d87698d1665a474685b3ce49b4af6cddeb Mon Sep 17 00:00:00 2001 From: James Kerns Date: Mon, 30 Oct 2023 14:37:47 -0500 Subject: [PATCH 1/2] add x-values checks --- docs/source/changelog.rst | 8 ++++++++ pylinac/core/profile.py | 13 +++++++++---- tests_basic/core/test_profile.py | 27 +++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index a1896491..045df158 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -37,6 +37,14 @@ ACR The ``ImagePositionPatient`` tag is now the primary lookup key and ``SliceLocation`` is only used if the former tag is unavailable. +Profiles +^^^^^^^^ + +* Passing *decreasing* x-values to ``SingleProfile`` would usually result in an error because the measured + width would be negative. An error will now be raised if the x-values are decreasing. +* The same error as above was also detected in the new ``Profile`` classes. + Given these classes are the new standard, they have been fully fixed and can now handle decreasing x-values. + v 3.16.0 -------- diff --git a/pylinac/core/profile.py b/pylinac/core/profile.py index dde7dbf9..5161e119 100644 --- a/pylinac/core/profile.py +++ b/pylinac/core/profile.py @@ -606,6 +606,9 @@ def __init__( self.values = values if x_values is None: x_values = np.arange(len(values)) + x_diff = np.diff(x_values) + if x_diff.max() > 0 > x_diff.min(): + raise ValueError("X values must be monotonically increasing or decreasing") self.x_values = x_values if ground: self.values = utils.ground(values) @@ -656,12 +659,12 @@ def field_indices(self, in_field_ratio: float) -> (int, int, int): """ left = self.field_edge_idx(side=LEFT) right = self.field_edge_idx(side=RIGHT) - width = right - left + width = self.field_width_px f_left = left + (1 - in_field_ratio) / 2 * width f_right = right - (1 - in_field_ratio) / 2 * width left = math.ceil(f_left) right = math.floor(f_right) - width = right - left + width = max(right, left) - min(right, left) return left, right, width @cached_property @@ -676,7 +679,7 @@ def field_width_px(self) -> float: """The field width of the profile in pixels""" left_idx = self.field_edge_idx(side=LEFT) right_idx = self.field_edge_idx(side=RIGHT) - return abs(right_idx - left_idx) + return max(right_idx, left_idx) - min(right_idx, left_idx) def field_values( self, @@ -686,7 +689,7 @@ def field_values( field width.""" left = self.field_edge_idx(side=LEFT) right = self.field_edge_idx(side=RIGHT) - width = right - left + width = self.field_width_px f_left = left + (1 - in_field_ratio) / 2 * width f_right = right - (1 - in_field_ratio) / 2 * width # use floor/ceil to be conservatively exclusive of edge values. @@ -1214,6 +1217,8 @@ def __init__( values # set initial data so we can do things like find beam center ) self.dpmm = dpmm + if np.diff(values).min() < 0: + raise ValueError("Profile values must be monotonically increasing") fitted_values, new_dpmm, x_indices = self._interpolate( values, x_values, diff --git a/tests_basic/core/test_profile.py b/tests_basic/core/test_profile.py index 37189baf..575eb4cb 100644 --- a/tests_basic/core/test_profile.py +++ b/tests_basic/core/test_profile.py @@ -320,6 +320,28 @@ def test_physical_resample_with_ints_and_small_range_raises_warning(self): class TestFWXMProfile(TestCase): + def test_x_values_decrease_is_okay(self): + array = create_simple_9_profile() + x_values = np.arange(len(array))[::-1] + f = FWXMProfile(array, x_values=x_values) + # both width and indices should respect the x-values + self.assertEqual(f.field_width_px, 4) + self.assertEqual(f.field_indices(in_field_ratio=0.8), (7, 1, 6)) + + def test_not_monotonically_increasing_raises_error(self): + array = create_simple_9_profile() + x_values = np.arange(len(array)) + x_values[2] = 5 + with self.assertRaises(ValueError): + FWXMProfile(array, x_values=x_values) + + def test_not_monotonically_decreasing_raises_error(self): + array = create_simple_9_profile() + x_values = np.arange(len(array))[::-1] + x_values[5] = 5 + with self.assertRaises(ValueError): + FWXMProfile(array, x_values=x_values) + def test_center_idx(self): array = create_simple_9_profile() profile = FWXMProfile(array) @@ -933,6 +955,11 @@ def test_normalization(self): ) self.assertGreaterEqual(p.values.max(), 1.0) + def test_x_values_are_monotonically_increasing(self): + array = np.random.rand(1, 100).squeeze() + with self.assertRaises(ValueError): + SingleProfile(array, x_values=array, interpolation=Interpolation.NONE) + def test_beam_center(self): # centered field field = generate_open_field() From 08bf9c8f12553bdf5d35037d415cf1cafe5218b3 Mon Sep 17 00:00:00 2001 From: James Kerns Date: Tue, 31 Oct 2023 08:39:38 -0500 Subject: [PATCH 2/2] change position of check --- pylinac/core/profile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pylinac/core/profile.py b/pylinac/core/profile.py index 5161e119..747fae36 100644 --- a/pylinac/core/profile.py +++ b/pylinac/core/profile.py @@ -1217,8 +1217,6 @@ def __init__( values # set initial data so we can do things like find beam center ) self.dpmm = dpmm - if np.diff(values).min() < 0: - raise ValueError("Profile values must be monotonically increasing") fitted_values, new_dpmm, x_indices = self._interpolate( values, x_values, @@ -1295,6 +1293,8 @@ def _interpolate( """Fit the data to the passed interpolation method. Will also calculate the new values to correct the measurements such as dpmm""" if x_values is None: x_values = np.array(range(len(values))) + if np.diff(x_values).min() < 0: + raise ValueError("Profile values must be monotonically increasing") if interp_method == Interpolation.NONE: return values, dpmm, x_values # do nothing else: