From 23452afc37500ec95538093f6ec41941233b46e8 Mon Sep 17 00:00:00 2001 From: Dimitar Tasev Date: Mon, 30 Nov 2020 12:34:44 +0000 Subject: [PATCH 1/7] Adds file to IMATLogFile, shows it in metadata Fixes #702 --- mantidimaging/core/data/images.py | 4 ++++ mantidimaging/core/data/test/fake_logfile.py | 2 +- mantidimaging/core/io/loader/loader.py | 4 ++-- mantidimaging/core/operation_history/const.py | 1 + mantidimaging/core/utility/imat_log_file_parser.py | 7 ++++++- 5 files changed, 14 insertions(+), 4 deletions(-) diff --git a/mantidimaging/core/data/images.py b/mantidimaging/core/data/images.py index e682d38e43d..70d6767ff86 100644 --- a/mantidimaging/core/data/images.py +++ b/mantidimaging/core/data/images.py @@ -272,6 +272,10 @@ def log_file(self): @log_file.setter def log_file(self, value: IMATLogFile): + if value is not None: + self.metadata[const.LOG_FILE] = value.source_file + elif value is None: + del self.metadata[const.LOG_FILE] self._log_file = value def projection_angles(self, max_angle: float = 360.0): diff --git a/mantidimaging/core/data/test/fake_logfile.py b/mantidimaging/core/data/test/fake_logfile.py index 1b30840b612..7d0e06a8aad 100644 --- a/mantidimaging/core/data/test/fake_logfile.py +++ b/mantidimaging/core/data/test/fake_logfile.py @@ -50,4 +50,4 @@ def generate_logfile() -> IMATLogFile: "Monitor 3 after: 6078866" ] ] - return IMATLogFile(data) + return IMATLogFile(data, "/tmp/fake") diff --git a/mantidimaging/core/io/loader/loader.py b/mantidimaging/core/io/loader/loader.py index 0b155b05965..20f6bf68bce 100644 --- a/mantidimaging/core/io/loader/loader.py +++ b/mantidimaging/core/io/loader/loader.py @@ -97,13 +97,13 @@ def read_in_file_information(input_path, return fi -def load_log(log_file) -> IMATLogFile: +def load_log(log_file: str) -> IMATLogFile: data = [] with open(log_file, 'r') as f: for line in f: data.append(line.strip().split(" ")) - return IMATLogFile(data) + return IMATLogFile(data, log_file) def load_p(parameters: ImageParameters, dtype, progress) -> Images: diff --git a/mantidimaging/core/operation_history/const.py b/mantidimaging/core/operation_history/const.py index 86445044740..299cdc8df11 100644 --- a/mantidimaging/core/operation_history/const.py +++ b/mantidimaging/core/operation_history/const.py @@ -7,6 +7,7 @@ OPERATION_KEYWORD_ARGS = 'kwargs' OPERATION_DISPLAY_NAME = 'display_name' PIXEL_SIZE = 'pixel_size' +LOG_FILE = 'log_file' OPERATION_NAME_COR_TILT_FINDING = 'cor_tilt_finding' COR_TILT_ROTATION_CENTRE = 'rotation_centre' diff --git a/mantidimaging/core/utility/imat_log_file_parser.py b/mantidimaging/core/utility/imat_log_file_parser.py index e9b6c561689..a6eafe20192 100644 --- a/mantidimaging/core/utility/imat_log_file_parser.py +++ b/mantidimaging/core/utility/imat_log_file_parser.py @@ -24,7 +24,8 @@ class IMATLogColumn(Enum): class IMATLogFile: - def __init__(self, data: List[List[str]]): + def __init__(self, data: List[List[str]], source_file: str): + self._source_file = source_file self._data: Dict[IMATLogColumn, List] = { IMATLogColumn.TIMESTAMP: [], IMATLogColumn.IMAGE_TYPE_IMAGE_COUNTER: [], @@ -46,6 +47,10 @@ def __init__(self, data: List[List[str]]): self._data[IMATLogColumn.COUNTS_BEFORE].append(line[2]) self._data[IMATLogColumn.COUNTS_AFTER].append(line[3]) + @property + def source_file(self) -> str: + return self._source_file + def projection_numbers(self): proj_nums = numpy.zeros(len(self._data[IMATLogColumn.IMAGE_TYPE_IMAGE_COUNTER]), dtype=numpy.uint32) for i, angle_str in enumerate(self._data[IMATLogColumn.IMAGE_TYPE_IMAGE_COUNTER]): From fd981262bd7964ff29592a505e41159eeec36856 Mon Sep 17 00:00:00 2001 From: Dimitar Tasev Date: Mon, 30 Nov 2020 12:34:57 +0000 Subject: [PATCH 2/7] Re-orders context menu, adds separators --- mantidimaging/gui/windows/stack_visualiser/view.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/mantidimaging/gui/windows/stack_visualiser/view.py b/mantidimaging/gui/windows/stack_visualiser/view.py index 6b240552d89..b675dbe8da7 100644 --- a/mantidimaging/gui/windows/stack_visualiser/view.py +++ b/mantidimaging/gui/windows/stack_visualiser/view.py @@ -121,12 +121,13 @@ def roi_changed_callback(self, roi: SensibleROI): self.roi_updated.emit(roi) def build_context_menu(self) -> QMenu: - actions = [("Set ROI", self.set_roi), ("Copy ROI to clipboard", self.copy_roi_to_clipboard), - ("Toggle show averaged image", lambda: self.presenter.notify(SVNotification.TOGGLE_IMAGE_MODE)), - ("Create sinograms from stack", lambda: self.presenter.notify(SVNotification.SWAP_AXES)), + actions = [("Show history and metadata", self.show_image_metadata), ("Duplicate whole data", lambda: self.presenter.notify(SVNotification.DUPE_STACK)), ("Duplicate current ROI of data", lambda: self.presenter.notify(SVNotification.DUPE_STACK_ROI)), - ("Show history", self.show_image_metadata), ("Mark as projections/sinograms", self.mark_as_), + ("Mark as projections/sinograms", self.mark_as_), ("", None), + ("Toggle show averaged image", lambda: self.presenter.notify(SVNotification.TOGGLE_IMAGE_MODE)), + ("Create sinograms from stack", lambda: self.presenter.notify(SVNotification.SWAP_AXES)), + ("Set ROI", self.set_roi), ("Copy ROI to clipboard", self.copy_roi_to_clipboard), ("", None), ("Change window name", self.change_window_name_clicked), ("Goto projection", self.goto_projection), ("Goto angle", self.goto_angle)] @@ -134,7 +135,10 @@ def build_context_menu(self) -> QMenu: for (menu_text, func) in actions: action = QAction(menu_text, menu) - action.triggered.connect(func) + if func is None: + action.setSeparator(True) + else: + action.triggered.connect(func) menu.addAction(action) return menu From ca0dd94927d78f2ec6c6f45f521f5fd9622d10b3 Mon Sep 17 00:00:00 2001 From: Dimitar Tasev Date: Mon, 30 Nov 2020 12:37:21 +0000 Subject: [PATCH 3/7] Adds separator in ROI view, refactor how it's added --- mantidimaging/gui/windows/operations/view.py | 2 ++ mantidimaging/gui/windows/stack_visualiser/view.py | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/mantidimaging/gui/windows/operations/view.py b/mantidimaging/gui/windows/operations/view.py index 0f5f89c40f5..62ac778595e 100644 --- a/mantidimaging/gui/windows/operations/view.py +++ b/mantidimaging/gui/windows/operations/view.py @@ -8,6 +8,7 @@ from PyQt5.QtWidgets import (QAction, QApplication, QCheckBox, QComboBox, QLabel, QMainWindow, QMenu, QMessageBox, QPushButton, QSizePolicy, QSplitter, QStyle, QVBoxLayout) from pyqtgraph import ImageItem +from pyqtgraph.debug import Tracer from mantidimaging.core.net.help_pages import open_api_webpage from mantidimaging.gui.mvp_base import BaseMainWindowView @@ -252,6 +253,7 @@ def toggle_average_images(images_): toggle_show_averaged_image = QAction("Toggle show averaged image", menu) toggle_show_averaged_image.triggered.connect(lambda: toggle_average_images(images)) menu.addAction(toggle_show_averaged_image) + menu.addSeparator() self.roi_view.imageItem.menu = menu self.roi_view.setImage(images.data) diff --git a/mantidimaging/gui/windows/stack_visualiser/view.py b/mantidimaging/gui/windows/stack_visualiser/view.py index b675dbe8da7..a63a2f8ca38 100644 --- a/mantidimaging/gui/windows/stack_visualiser/view.py +++ b/mantidimaging/gui/windows/stack_visualiser/view.py @@ -134,12 +134,12 @@ def build_context_menu(self) -> QMenu: menu = QMenu(self) for (menu_text, func) in actions: - action = QAction(menu_text, menu) if func is None: - action.setSeparator(True) + menu.addSeparator() else: + action = QAction(menu_text, menu) action.triggered.connect(func) - menu.addAction(action) + menu.addAction(action) return menu From 4ee3faf7d110f8049424fe0896004d987841804a Mon Sep 17 00:00:00 2001 From: Dimitar Tasev Date: Mon, 30 Nov 2020 12:45:32 +0000 Subject: [PATCH 4/7] Adds tests --- mantidimaging/core/data/test/images_test.py | 5 +++++ .../utility/test/imat_log_file_parser_test.py | 18 +++++++++++++++--- mantidimaging/gui/windows/operations/view.py | 1 - 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/mantidimaging/core/data/test/images_test.py b/mantidimaging/core/data/test/images_test.py index dd0b8c60f74..0879b930488 100644 --- a/mantidimaging/core/data/test/images_test.py +++ b/mantidimaging/core/data/test/images_test.py @@ -198,3 +198,8 @@ def test_get_projection_angles_no_logfile(self): actual = images.projection_angles(275.69) self.assertEqual(10, len(actual.value)) self.assertAlmostEqual(np.deg2rad(275.69), actual.value[-1], places=4) + + def test_metadata_gets_updated_with_logfile(self): + images = generate_images() + images.log_file = generate_logfile() + self.assertEqual(images.log_file.source_file, images.metadata[const.LOG_FILE]) \ No newline at end of file diff --git a/mantidimaging/core/utility/test/imat_log_file_parser_test.py b/mantidimaging/core/utility/test/imat_log_file_parser_test.py index 00c2414424e..5a7dba275d1 100644 --- a/mantidimaging/core/utility/test/imat_log_file_parser_test.py +++ b/mantidimaging/core/utility/test/imat_log_file_parser_test.py @@ -11,7 +11,7 @@ def test_parsing_log_file(): EXPECTED_HEADER_FOR_IMAT_LOG_FILE, ["ignored line"], ["timestamp", "Projection: 0 angle: 0.1", "counts before: 12345", "counts_after: 45678"] ] - logfile = IMATLogFile(test_input) + logfile = IMATLogFile(test_input, "/tmp/fake") assert len(logfile.projection_angles().value) == 1 assert logfile.projection_angles().value[0] == np.deg2rad(0.1), f"Got: {logfile.projection_angles().value[0]}" assert logfile.counts().value[0] == (45678 - 12345) @@ -25,7 +25,7 @@ def test_counts(): ["timestamp", "Projection: 1 angle: 0.1", "counts before: 45678", "counts_after: 84678"], ["timestamp", "Projection: 2 angle: 0.2", "counts before: 84678", "counts_after: 124333"], ] - logfile = IMATLogFile(test_input) + logfile = IMATLogFile(test_input, "/tmp/fake") assert len(logfile.counts().value) == 3 assert logfile.counts().value[0] == 45678 - 12345 assert logfile.counts().value[1] == 84678 - 45678 @@ -50,7 +50,7 @@ def test_find_missing_projection_number(): ["timestamp", "Projection: 1 angle: 0.1", "counts before: 12345", "counts_after: 45678"], ["timestamp", "Projection: 2 angle: 0.2", "counts before: 12345", "counts_after: 45678"], ] - logfile = IMATLogFile(test_input) + logfile = IMATLogFile(test_input, "/tmp/fake") assert len(logfile.projection_numbers()) == 3 # nothing missing logfile.raise_if_angle_missing(["file_000.tif", "file_001.tif", "file_002.tif"]) @@ -60,3 +60,15 @@ def test_find_missing_projection_number(): assert_raises(RuntimeError, logfile.raise_if_angle_missing, ["file_000.tif", "file_001.tif"]) assert_raises(RuntimeError, logfile.raise_if_angle_missing, ["file_000.tif", "file_001.tif", "file_002.tif", "file_003.tif"]) + + +def test_source_file(): + test_input = [ + EXPECTED_HEADER_FOR_IMAT_LOG_FILE, + ["ignored line"], + ["timestamp", "Projection: 0 angle: 0.0", "counts before: 12345", "counts_after: 45678"], + ["timestamp", "Projection: 1 angle: 0.1", "counts before: 12345", "counts_after: 45678"], + ["timestamp", "Projection: 2 angle: 0.2", "counts before: 12345", "counts_after: 45678"], + ] + logfile = IMATLogFile(test_input, "/tmp/fake") + assert logfile.source_file == "/tmp/fake" diff --git a/mantidimaging/gui/windows/operations/view.py b/mantidimaging/gui/windows/operations/view.py index 62ac778595e..a1e71f5b53e 100644 --- a/mantidimaging/gui/windows/operations/view.py +++ b/mantidimaging/gui/windows/operations/view.py @@ -8,7 +8,6 @@ from PyQt5.QtWidgets import (QAction, QApplication, QCheckBox, QComboBox, QLabel, QMainWindow, QMenu, QMessageBox, QPushButton, QSizePolicy, QSplitter, QStyle, QVBoxLayout) from pyqtgraph import ImageItem -from pyqtgraph.debug import Tracer from mantidimaging.core.net.help_pages import open_api_webpage from mantidimaging.gui.mvp_base import BaseMainWindowView From c3c78c22476715447e66f3dc91dbecdb95f1d795 Mon Sep 17 00:00:00 2001 From: Dimitar Tasev Date: Mon, 30 Nov 2020 13:40:37 +0000 Subject: [PATCH 5/7] Adds endline on file --- mantidimaging/core/data/test/images_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mantidimaging/core/data/test/images_test.py b/mantidimaging/core/data/test/images_test.py index 0879b930488..45e5ef2465d 100644 --- a/mantidimaging/core/data/test/images_test.py +++ b/mantidimaging/core/data/test/images_test.py @@ -202,4 +202,4 @@ def test_get_projection_angles_no_logfile(self): def test_metadata_gets_updated_with_logfile(self): images = generate_images() images.log_file = generate_logfile() - self.assertEqual(images.log_file.source_file, images.metadata[const.LOG_FILE]) \ No newline at end of file + self.assertEqual(images.log_file.source_file, images.metadata[const.LOG_FILE]) From dd46fc62b6a3f5adce4a340a67d19f6efb89b255 Mon Sep 17 00:00:00 2001 From: Dimitar Tasev Date: Tue, 1 Dec 2020 13:30:00 +0000 Subject: [PATCH 6/7] Refactor add_log_to_sample, now checks if logs are suitable before setting Also small refactor in add_180_deg to handle the case where a stack with the name is not found --- mantidimaging/gui/windows/main/model.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/mantidimaging/gui/windows/main/model.py b/mantidimaging/gui/windows/main/model.py index f7657ffc4f3..4fad3474404 100644 --- a/mantidimaging/gui/windows/main/model.py +++ b/mantidimaging/gui/windows/main/model.py @@ -162,12 +162,21 @@ def have_active_stacks(self) -> bool: return len(self.active_stacks) > 0 def add_log_to_sample(self, stack_name: str, log_file: str): - stack = self.get_stack_by_name(stack_name).widget() # type: ignore - stack.presenter.images.log_file = loader.load_log(log_file) - stack.presenter.images.log_file.raise_if_angle_missing(stack.presenter.images.filenames) + stack_dock = self.get_stack_by_name(stack_name) + if stack_dock is None: + raise RuntimeError(f"Failed to get stack with name {stack_name}") + + stack: StackVisualiserView = stack_dock.widget() # type: ignore + log = loader.load_log(log_file) + log.raise_if_angle_missing(stack.presenter.images.filenames) + stack.presenter.images.log_file = log def add_180_deg_to_stack(self, stack_name, _180_deg_file): - stack = self.get_stack_by_name(stack_name).widget() # type: ignore + stack_dock = self.get_stack_by_name(stack_name) + if stack_dock is None: + raise RuntimeError(f"Failed to get stack with name {stack_name}") + + stack: StackVisualiserView = stack_dock.widget() # type: ignore _180_deg = loader.load(file_names=[_180_deg_file]) stack.presenter.images.proj180deg = _180_deg.sample return _180_deg From 620b6f9ed17fdf4b7057e996b991da3da93c2027 Mon Sep 17 00:00:00 2001 From: Dimitar Tasev Date: Tue, 1 Dec 2020 17:00:05 +0000 Subject: [PATCH 7/7] Adds test for new branches --- .../gui/windows/main/test/model_test.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/mantidimaging/gui/windows/main/test/model_test.py b/mantidimaging/gui/windows/main/test/model_test.py index 969442b5018..107a97824c5 100644 --- a/mantidimaging/gui/windows/main/test/model_test.py +++ b/mantidimaging/gui/windows/main/test/model_test.py @@ -238,6 +238,21 @@ def test_add_log_to_sample(self, load_log: mock.Mock): stack_mock.return_value.widget.return_value.presenter.images.log_file.raise_if_angle_missing \ .assert_called_once_with(stack_mock.return_value.widget.return_value.presenter.images.filenames) + @mock.patch('mantidimaging.core.io.loader.load_log') + def test_add_log_to_sample_no_stack(self, load_log: mock.Mock): + """ + Test in add_log_to_sample when get_stack_by_name returns None + """ + log_file = "Log file" + stack_name = "stack name" + stack_mock = mock.MagicMock() + self.model.get_stack_by_name = stack_mock + stack_mock.return_value = None + + self.assertRaises(RuntimeError, self.model.add_log_to_sample, stack_name=stack_name, log_file=log_file) + + stack_mock.assert_called_with(stack_name) + @mock.patch('mantidimaging.core.io.loader.load') def test_add_180_deg_to_stack(self, load: mock.Mock): _180_file = "180 file" @@ -251,6 +266,20 @@ def test_add_180_deg_to_stack(self, load: mock.Mock): stack_mock.assert_called_with(stack_name) self.assertEqual(_180_stack, stack_mock.return_value.widget.return_value.presenter.images.proj180deg) + @mock.patch('mantidimaging.core.io.loader.load') + def test_add_180_deg_to_stack_no_stack(self, load: mock.Mock): + """ + Test in add_180_deg_to_stack when get_stack_by_name returns None + """ + _180_file = "180 file" + stack_name = "stack name" + stack_mock = mock.MagicMock() + self.model.get_stack_by_name = stack_mock + stack_mock.return_value = None + + self.assertRaises(RuntimeError, self.model.add_180_deg_to_stack, stack_name=stack_name, _180_deg_file=_180_file) + stack_mock.assert_called_with(stack_name) + if __name__ == '__main__': unittest.main()