Skip to content

Commit

Permalink
Merge Rits export to main (#1954)
Browse files Browse the repository at this point in the history
  • Loading branch information
JackEAllen authored Oct 12, 2023
2 parents 21f8d0f + 42873a3 commit c8d8ddb
Show file tree
Hide file tree
Showing 15 changed files with 540 additions and 328 deletions.
3 changes: 0 additions & 3 deletions .github/workflows/conda.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ on:
- 'main'
- 'release-*'
pull_request:
branches:
- 'main'
- 'release-*'
release:
merge_group:

Expand Down
3 changes: 0 additions & 3 deletions .github/workflows/cos7_testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ on:
- 'main'
- 'release-*'
pull_request:
branches:
- 'main'
- 'release-*'
merge_group:

jobs:
Expand Down
3 changes: 0 additions & 3 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ on:
- 'main'
- 'release-*'
pull_request:
branches:
- 'main'
- 'release-*'
release:
merge_group:

Expand Down
3 changes: 0 additions & 3 deletions .github/workflows/license_check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ on:
- 'main'
- 'release-*'
pull_request:
branches:
- 'main'
- 'release-*'
merge_group:

jobs:
Expand Down
3 changes: 0 additions & 3 deletions .github/workflows/u18_testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ on:
- 'main'
- 'release-*'
pull_request:
branches:
- 'main'
- 'release-*'
merge_group:

jobs:
Expand Down
3 changes: 0 additions & 3 deletions .github/workflows/windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ on:
- 'main'
- 'release-*'
pull_request:
branches:
- 'main'
- 'release-*'
release:
merge_group:

Expand Down
1 change: 1 addition & 0 deletions docs/release_notes/next/dev-1932-RITS_export_backend
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#1932 : RITS Export Backend
2 changes: 2 additions & 0 deletions docs/release_notes/next/feature-1941-rits-gui
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#1941 : Spectrum viewer tab for exporting to RITS

31 changes: 30 additions & 1 deletion mantidimaging/core/io/saver.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from typing import List, Union, Optional, Dict, Callable, Tuple, TYPE_CHECKING

import h5py
from pathlib import Path
import numpy as np
from tifffile import tifffile

Expand Down Expand Up @@ -411,7 +412,7 @@ def generate_names(name_prefix: str,
return names


def make_dirs_if_needed(dirname: Optional[str] = None, overwrite_all: bool = False):
def make_dirs_if_needed(dirname: Optional[str] = None, overwrite_all: bool = False) -> None:
"""
Makes sure that the directory needed (for example to save a file)
exists, otherwise creates it.
Expand All @@ -428,3 +429,31 @@ def make_dirs_if_needed(dirname: Optional[str] = None, overwrite_all: bool = Fal
elif os.listdir(path) and not overwrite_all:
raise RuntimeError("The output directory is NOT empty:{0}\nThis can be "
"overridden by specifying 'Overwrite on name conflict'.".format(path))


def create_rits_format(tof: np.ndarray, transmission: np.ndarray, transmission_error: np.ndarray) -> str:
"""
create a RITS format ready for exporting to a .dat file
:param tof: time of flight
:param transmission: transmission value
:param transmission_error: transmission_error value
:return: RITS format ascii
"""
return '\n'.join(
['\t'.join([str(x) for x in row]) for row in zip(tof, transmission, transmission_error, strict=True)])


def export_to_dat_rits_format(rits_formatted_data: str, path: Path) -> None:
"""
export a RITS formatted data to a .dat file
:param rits_formatted_data: RITS formatted data
:param path: path to save the .dat file
:return: None
"""
with open(path, 'w', encoding='utf-8') as f:
f.write(rits_formatted_data)
LOG.info('RITS formatted data saved to: {}'.format(path))
8 changes: 8 additions & 0 deletions mantidimaging/core/io/test/io_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,14 @@ def test_convert_float_to_int(self):
close_arr = np.isclose(conv[i] / factors[i], float_arr[i], rtol=1e-5)
self.assertTrue(np.count_nonzero(close_arr) >= len(close_arr) * 0.75)

def test_create_rits_format(self):
tof = np.array([1, 2, 3])
transmission = np.array([4, 5, 6])
absorption = np.array([7, 8, 9])
rits_formatted_data = saver.create_rits_format(tof, transmission, absorption)
expected = '1\t4\t7\n2\t5\t8\n3\t6\t9'
self.assertEqual(rits_formatted_data, expected)


if __name__ == '__main__':
unittest.main()
615 changes: 336 additions & 279 deletions mantidimaging/gui/ui/spectrum_viewer.ui

Large diffs are not rendered by default.

33 changes: 33 additions & 0 deletions mantidimaging/gui/windows/spectrum_viewer/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@

import numpy as np

from logging import getLogger
from mantidimaging.core.data import ImageStack
from mantidimaging.core.io.csv_output import CSVOutput
from mantidimaging.core.io import saver
from mantidimaging.core.utility.sensible_roi import SensibleROI

if TYPE_CHECKING:
from mantidimaging.gui.windows.spectrum_viewer.presenter import SpectrumViewerWindowPresenter

LOG = getLogger(__name__)


class SpecType(Enum):
SAMPLE = 1
Expand Down Expand Up @@ -183,6 +187,28 @@ def save_csv(self, path: Path, normalized: bool) -> None:
csv_output.write(outfile)
self.save_roi_coords(self.get_roi_coords_filename(path))

def save_rits(self, path: Path, normalized: bool) -> None:
"""
Saves the spectrum for one ROI to a RITS file.
@param path: The path to save the CSV file to.
@param normalized: Whether to save the normalized spectrum.
"""
if self._stack is None:
raise ValueError("No stack selected")

# Default_roi will likely need updating once UI is implemented
default_roi = self.default_roi_list[0]
tof = np.arange(self._stack.data.shape[0])
transmission_error = np.zeros_like(tof)
if normalized:
if self._normalise_stack is None:
raise RuntimeError("No normalisation stack selected")
transmission = self.get_spectrum(default_roi, SpecType.SAMPLE_NORMED)
self.export_spectrum_to_rits(path, tof, transmission, transmission_error)
else:
LOG.error("Data is not normalised to open beam. This will not export to a valid RITS format")

def get_roi_coords_filename(self, path: Path) -> Path:
"""
Get the path to save the ROI coordinates to.
Expand All @@ -209,6 +235,13 @@ def save_roi_coords(self, path: Path) -> None:
"Y Max": coords.bottom
})

def export_spectrum_to_rits(self, path: Path, tof, transmission, absorption) -> None:
"""
Export spectrum to RITS format
"""
rits_data = saver.create_rits_format(tof, transmission, absorption)
saver.export_to_dat_rits_format(rits_data, path)

def remove_roi(self, roi_name) -> None:
"""
Remove the selected ROI from the model
Expand Down
41 changes: 37 additions & 4 deletions mantidimaging/gui/windows/spectrum_viewer/presenter.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# Copyright (C) 2023 ISIS Rutherford Appleton Laboratory UKRI
# SPDX - License - Identifier: GPL-3.0-or-later
from __future__ import annotations

from enum import Enum
from typing import TYPE_CHECKING, Optional

from logging import getLogger
from mantidimaging.core.data.dataset import StrictDataset
from mantidimaging.gui.mvp_base import BasePresenter
from mantidimaging.gui.windows.spectrum_viewer.model import SpectrumViewerWindowModel, SpecType
Expand All @@ -13,6 +16,14 @@
from mantidimaging.core.data import ImageStack
from uuid import UUID

LOG = getLogger(__name__)


class ExportMode(Enum):
# Needs to match GUI tab order
ROI_MODE = 0
IMAGE_MODE = 1


class SpectrumViewerWindowPresenter(BasePresenter):
"""
Expand All @@ -26,13 +37,15 @@ class SpectrumViewerWindowPresenter(BasePresenter):
spectrum_mode: SpecType = SpecType.SAMPLE
current_stack_uuid: Optional['UUID'] = None
current_norm_stack_uuid: Optional['UUID'] = None
export_mode: ExportMode

def __init__(self, view: 'SpectrumViewerWindowView', main_window: 'MainWindowView'):
super().__init__(view)

self.view = view
self.main_window = main_window
self.model = SpectrumViewerWindowModel(self)
self.export_mode = ExportMode.ROI_MODE

def handle_sample_change(self, uuid: Optional['UUID']) -> None:
if uuid == self.current_stack_uuid:
Expand All @@ -50,7 +63,6 @@ def handle_sample_change(self, uuid: Optional['UUID']) -> None:
if uuid is None:
self.model.set_stack(None)
self.view.clear()
self.handle_button_enabled()
return

self.model.set_stack(self.main_window.get_stack(uuid))
Expand All @@ -65,7 +77,6 @@ def handle_sample_change(self, uuid: Optional['UUID']) -> None:
self.do_add_roi()
self.view.set_normalise_error(self.model.normalise_issue())
self.show_new_sample()
self.handle_button_enabled()

def handle_normalise_stack_change(self, normalise_uuid: Optional['UUID']) -> None:
if normalise_uuid == self.current_norm_stack_uuid:
Expand Down Expand Up @@ -144,9 +155,15 @@ def do_set_roi_alpha(self, name: str, alpha: float) -> None:

def handle_button_enabled(self) -> None:
"""
Enable the export button if the current stack is not None
Enable the export button if the current stack is not None and normalisation is valid
"""
self.view.set_export_button_enabled(self.model.has_stack())
has_stack = self.model.has_stack()
normalisation_on = self.view.normalisation_enabled()
normalisation_no_error = (normalisation_on and self.model.normalise_issue() == "") or not normalisation_on

self.view.exportButton.setEnabled(has_stack and normalisation_no_error)
self.view.exportButtonRITS.setEnabled(has_stack and normalisation_on and normalisation_no_error)
self.view.addBtn.setEnabled(has_stack)

def handle_export_csv(self) -> None:
path = self.view.get_csv_filename()
Expand All @@ -158,6 +175,18 @@ def handle_export_csv(self) -> None:

self.model.save_csv(path, self.spectrum_mode == SpecType.SAMPLE_NORMED)

def handle_rits_export(self) -> None:
"""
Handle the export of the current spectrum to a RITS file format
"""
path = self.view.get_rits_export_filename()
if path is None:
LOG.debug("No path selected, aborting export")
return
if path.suffix != ".dat":
path = path.with_suffix(".dat")
self.model.save_rits(path, self.spectrum_mode == SpecType.SAMPLE_NORMED)

def handle_enable_normalised(self, enabled: bool) -> None:
if enabled:
self.spectrum_mode = SpecType.SAMPLE_NORMED
Expand Down Expand Up @@ -220,3 +249,7 @@ def do_remove_roi(self, roi_name=None) -> None:
self.view.spectrum.remove_roi(roi_name)
self.view.set_spectrum(roi_name, self.model.get_spectrum(roi_name, self.spectrum_mode))
self.model.remove_roi(roi_name)

def handle_export_tab_change(self, index: int) -> None:
self.export_mode = ExportMode(index)
self.view.on_visibility_change()
57 changes: 55 additions & 2 deletions mantidimaging/gui/windows/spectrum_viewer/test/presenter_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from pathlib import Path
from unittest import mock

from PyQt5.QtWidgets import QPushButton
from parameterized import parameterized

from mantidimaging.core.data.dataset import StrictDataset, MixedDataset
Expand All @@ -27,6 +28,9 @@ def setUp(self) -> None:
self.view.current_dataset_id = uuid.uuid4()
mock_spectrum_roi_dict = mock.create_autospec(dict)
self.view.spectrum = mock.create_autospec(SpectrumWidget, roi_dict=mock_spectrum_roi_dict)
self.view.exportButton = mock.create_autospec(QPushButton)
self.view.exportButtonRITS = mock.create_autospec(QPushButton)
self.view.addBtn = mock.create_autospec(QPushButton)
self.presenter = SpectrumViewerWindowPresenter(self.view, self.main_window)

def test_get_dataset_id_for_stack_no_stack_id(self):
Expand Down Expand Up @@ -74,7 +78,6 @@ def test_handle_sample_change_no_new_stack(self):
self.view.try_to_select_relevant_normalise_stack.assert_not_called()
self.assertIsNone(self.view.current_dataset_id)
self.presenter.show_new_sample.assert_not_called()
self.view.set_export_button_enabled.assert_called_once_with(False)

def test_handle_sample_change_dataset_unchanged(self):
initial_dataset_id = self.view.current_dataset_id
Expand All @@ -86,7 +89,6 @@ def test_handle_sample_change_dataset_unchanged(self):
self.presenter.handle_sample_change(uuid.uuid4())
self.presenter.main_window.get_dataset.assert_not_called()
self.assertEqual(self.view.current_dataset_id, initial_dataset_id)
self.view.set_export_button_enabled.assert_called_once_with(True)

def test_handle_sample_change_to_MixedDataset(self):
self.presenter.get_dataset_id_for_stack = mock.Mock(return_value=uuid.uuid4())
Expand All @@ -112,6 +114,45 @@ def test_handle_sample_change_no_flat(self):
self.presenter.main_window.get_dataset.assert_called_once()
self.view.try_to_select_relevant_normalise_stack.assert_not_called()

@mock.patch("mantidimaging.gui.windows.spectrum_viewer.model.SpectrumViewerWindowModel.has_stack")
def test_WHEN_no_stack_THEN_buttons_disabled(self, has_stack):
has_stack.return_value = False
self.presenter.handle_button_enabled()
self.view.exportButton.setEnabled.assert_called_once_with(False)
self.view.exportButtonRITS.setEnabled.assert_called_once_with(False)
self.view.addBtn.setEnabled.assert_called_once_with(False)

@mock.patch("mantidimaging.gui.windows.spectrum_viewer.model.SpectrumViewerWindowModel.has_stack")
def test_WHEN_has_stack_no_norm_THEN_buttons_set(self, has_stack):
has_stack.return_value = True
self.view.normalisation_enabled.return_value = False
self.presenter.handle_button_enabled()
self.view.exportButton.setEnabled.assert_called_once_with(True)
self.view.exportButtonRITS.setEnabled.assert_called_once_with(False) # RITS export needs norm
self.view.addBtn.setEnabled.assert_called_once_with(True)

@mock.patch("mantidimaging.gui.windows.spectrum_viewer.model.SpectrumViewerWindowModel.has_stack")
@mock.patch("mantidimaging.gui.windows.spectrum_viewer.model.SpectrumViewerWindowModel.normalise_issue")
def test_WHEN_has_stack_has_good_norm_THEN_buttons_set(self, normalise_issue, has_stack):
has_stack.return_value = True
normalise_issue.return_value = ""
self.view.normalisation_enabled.return_value = True
self.presenter.handle_button_enabled()
self.view.exportButton.setEnabled.assert_called_once_with(True)
self.view.exportButtonRITS.setEnabled.assert_called_once_with(True)
self.view.addBtn.setEnabled.assert_called_once_with(True)

@mock.patch("mantidimaging.gui.windows.spectrum_viewer.model.SpectrumViewerWindowModel.has_stack")
@mock.patch("mantidimaging.gui.windows.spectrum_viewer.model.SpectrumViewerWindowModel.normalise_issue")
def test_WHEN_has_stack_has_bad_norm_THEN_buttons_set(self, normalise_issue, has_stack):
has_stack.return_value = True
normalise_issue.return_value = "Something wrong"
self.view.normalisation_enabled.return_value = True
self.presenter.handle_button_enabled()
self.view.exportButton.setEnabled.assert_called_once_with(False)
self.view.exportButtonRITS.setEnabled.assert_called_once_with(False)
self.view.addBtn.setEnabled.assert_called_once_with(True)

def test_WHEN_show_sample_call_THEN_add_range_set(self):
self.presenter.model.tof_range = (0, 9)
self.presenter.show_new_sample()
Expand Down Expand Up @@ -146,6 +187,18 @@ def test_handle_export_csv(self, path_name: str, mock_save_csv: mock.Mock):
self.view.get_csv_filename.assert_called_once()
mock_save_csv.assert_called_once_with(Path("/fake/path.csv"), False)

@parameterized.expand(["/fake/path", "/fake/path.dat"])
@mock.patch("mantidimaging.gui.windows.spectrum_viewer.model.SpectrumViewerWindowModel.save_rits")
def test_handle_rits_export(self, path_name: str, mock_save_rits: mock.Mock):
self.view.get_rits_export_filename = mock.Mock(return_value=Path(path_name))

self.presenter.model.set_stack(generate_images())

self.presenter.handle_rits_export()

self.view.get_rits_export_filename.assert_called_once()
mock_save_rits.assert_called_once_with(Path("/fake/path.dat"), False)

def test_WHEN_do_add_roi_called_THEN_new_roi_added(self):
self.presenter.model.set_stack(generate_images())
self.presenter.do_add_roi()
Expand Down
Loading

0 comments on commit c8d8ddb

Please sign in to comment.