Skip to content

Commit

Permalink
Merged in feature/RAM-3538_pf_leaf_positions (pull request #384)
Browse files Browse the repository at this point in the history
RAM-3538 Add individual leaf positions and errors to results_data

Approved-by: Randy Taylor
  • Loading branch information
jrkerns committed May 14, 2024
2 parents c2560e0 + c81cb79 commit f71dcb9
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 2 deletions.
4 changes: 4 additions & 0 deletions docs/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ Picket Fence
of pixels.
* A new method is available ``plot_leaf_error``. This method will create a figure of the leaf error boxplot. This is
similar to the leaf error subplot that shows up at the right/bottom of the analyzed image, but can be called independently.
* The PF ``results_data`` object has two new attributes: ``mlc_positions_by_leaf`` and ``mlc_errors_by_leaf``. These are dictionaries with the MLC
number as the key and the value is a list of float values. The values are the absolute positions of the MLC leaves in mm and the error in mm
respectively. See the new :ref:`individual_leaf_positions` section for more.


Winston-Lutz
^^^^^^^^^^^^
Expand Down
25 changes: 25 additions & 0 deletions docs/source/picketfence.rst
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,31 @@ This results with the edge leaves now being caught in this case. You may need to

.. image:: images/pf_now_catching_edges.png

.. _individual_leaf_positions:

Individual leaf positions & errors
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Individual leaf positions and errors can be found in the ``results_data`` object under the ``mlc_positions_by_leaf`` and ``mlc_errors_by_leaf`` attribute. This will be a dictionary
where the key is the leaf number (as a string) and the value is a list of positions in mm. The length of the list will be number of pickets. This is useful for further analysis as desired.
For combined analysis mode, the result will look something like::

{
'11': [110.2, 140.1, 170.0, ...],
'12': [110.1, 140.2, 169.9, ...],
...
}

For separate analysis, the result will be similar to::

{
'A11': [110.2, 140.1, 170.0, ...],
'A12': [110.2, 140.1, 170.0, ...],
...
'B11': [112.1, 142.2, 172.2, ...],
...
}

Benchmarking the algorithm
--------------------------

Expand Down
27 changes: 26 additions & 1 deletion pylinac/picketfence.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import webbrowser
from functools import cached_property
from io import BytesIO
from itertools import cycle
from itertools import cycle, groupby
from pathlib import Path
from typing import BinaryIO, Iterable, Sequence

Expand Down Expand Up @@ -142,6 +142,8 @@ class PFResult(ResultBase):
failed_leaves: list[str] | list[int] #:
mlc_skew: float #:
picket_widths: dict[str, dict[str, float]] #:
mlc_positions_by_leaf: dict[str, list[float]] #:
mlc_errors_by_leaf: dict[str, list[float]] #:


class PFDicomImage(image.LinacDicomImage):
Expand Down Expand Up @@ -1080,6 +1082,22 @@ def _generate_results_data(self) -> PFResult:
}
for pk in range(len(self.pickets))
}
errors_by_leaf = {}
positions_by_leaf = {}
for _, group_iter in groupby(self.mlc_meas, key=lambda m: m.leaf_num):
leaf_items = list(group_iter) # group_iter is a generator
leaf_names = leaf_items[0].full_leaf_nums
for idx, leaf_name in enumerate(leaf_names):
pos_vals = [m.position_mm[idx] for m in leaf_items]
error_vals = [m.error[idx] for m in leaf_items]
positions_by_leaf[str(leaf_name)] = pos_vals
errors_by_leaf[str(leaf_name)] = error_vals
errors_by_leaf = dict(
sorted(errors_by_leaf.items())
) # sort by A/B and leaf number; A1, A2, ..., B1, B2, ...
positions_by_leaf = dict(
sorted(positions_by_leaf.items())
) # sort by A/B and leaf number; A1, A2, ..., B1, B2, ...
return PFResult(
tolerance_mm=self.tolerance,
action_tolerance_mm=self.action_tolerance,
Expand All @@ -1095,6 +1113,8 @@ def _generate_results_data(self) -> PFResult:
failed_leaves=self.failed_leaves(),
mlc_skew=self.mlc_skew(),
picket_widths=picket_widths,
mlc_positions_by_leaf=positions_by_leaf,
mlc_errors_by_leaf=errors_by_leaf,
)

def publish_pdf(
Expand Down Expand Up @@ -1306,6 +1326,11 @@ def get_peak_positions(self) -> Sequence[float]:
prof.center_idx + max(self._approximate_idx - self._spacing / 2, 0),
) # crop to left edge if need be

@property
def position_mm(self) -> Sequence[float]:
"""The position of the MLC leaf in the travel direction in mm from the left/top side of the image."""
return [pos / self._image.dpmm for pos in self.position]

@property
def passed(self) -> Sequence[bool]:
"""Whether the MLC kiss or leaf was within tolerance."""
Expand Down
17 changes: 16 additions & 1 deletion tests_basic/test_picketfence.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
import json
import os
import os.path as osp
import statistics
import tempfile
from itertools import chain
from pathlib import Path
from unittest import TestCase, skip

Expand Down Expand Up @@ -144,11 +146,24 @@ def test_results_data(self):
self.assertEqual(len(data.picket_widths), 10)
self.assertIn("picket_5", data.picket_widths)
self.assertAlmostEqual(data.picket_widths["picket_5"]["max"], 3, delta=0.03)
# test individual leaf values
self.assertEqual(
len(data.mlc_positions_by_leaf), 36
) # 36 leaf pairs in the image
# constancy check
self.assertAlmostEqual(
statistics.mean(data.mlc_positions_by_leaf["17"]), 204.63, delta=0.1
)
# check max error matches a combination of the leaf values
self.assertEqual(
max(abs(v) for v in chain.from_iterable(data.mlc_errors_by_leaf.values())),
data.max_error_mm,
)

data_dict = self.pf.results_data(as_dict=True)
self.assertIsInstance(data_dict, dict)
self.assertIn("pylinac_version", data_dict)
self.assertEqual(len(data_dict), 16)
self.assertEqual(len(data_dict), 18)

data_str = self.pf.results_data(as_json=True)
self.assertIsInstance(data_str, str)
Expand Down

0 comments on commit f71dcb9

Please sign in to comment.