Skip to content

Commit

Permalink
Export direct beam data (#75)
Browse files Browse the repository at this point in the history
* Add right-click option "Export data" to reduction table and direct beam table to save text file with:
- TOF
- wavelength
- counts normalized by proton charge
- error in counts normalized by proton charge
- counts
- error in counts
- size of ROI
  • Loading branch information
backmari authored Feb 8, 2024
1 parent dc81ea5 commit ba09e11
Show file tree
Hide file tree
Showing 7 changed files with 362 additions and 11 deletions.
65 changes: 60 additions & 5 deletions reflectivity_ui/interfaces/data_handling/data_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,15 @@ def getIxyt(nxs_data):
sz_x_axis = int(nxs_data.getInstrument().getNumberParameter("number-of-x-pixels")[0]) # 304

_y_axis = np.zeros((sz_x_axis, sz_y_axis, nbr_tof - 1))
# _y_error_axis = np.zeros((sz_x_axis, sz_y_axis, nbr_tof-1))
_y_error_axis = np.zeros((sz_x_axis, sz_y_axis, nbr_tof - 1))

for x in range(sz_x_axis):
for y in range(sz_y_axis):
_index = int(sz_y_axis * x + y)
_tmp_data = nxs_data.readY(_index)[:]
_y_axis[x, y, :] = _tmp_data
_y_axis[x, y, :] = nxs_data.readY(_index)[:]
_y_error_axis[x, y, :] = nxs_data.readE(_index)[:]

return _y_axis
return _y_axis, _y_error_axis


class NexusData(object):
Expand Down Expand Up @@ -478,6 +478,7 @@ def __init__(self, name, configuration, entry_name="entry", workspace=None):
self.data = None
self.xydata = None
self.xtofdata = None
self.raw_error = None

self.meta_data_roi_peak = None
self.meta_data_roi_bck = None
Expand Down Expand Up @@ -725,13 +726,14 @@ def prepare_plot_data(self):
t_0 = time.time()
binning_ws = api.CreateWorkspace(DataX=self.tof_edges, DataY=np.zeros(len(self.tof_edges) - 1))
data_rebinned = api.RebinToWorkspace(WorkspaceToRebin=workspace, WorkspaceToMatch=binning_ws)
Ixyt = getIxyt(data_rebinned)
Ixyt, Ixyt_error = getIxyt(data_rebinned)

# Create projections for the 2D datasets
Ixy = Ixyt.sum(axis=2)
Ixt = Ixyt.sum(axis=1)
# Store the data
self.data = Ixyt.astype(float) # 3D dataset
self.raw_error = Ixyt_error.astype(float) # 3D dataset
self.xydata = Ixy.transpose().astype(float) # 2D dataset
self.xtofdata = Ixt.astype(float) # 2D dataset
logging.info("Plot data generated: %s sec", time.time() - t_0)
Expand Down Expand Up @@ -785,6 +787,59 @@ def get_counts_vs_TOF(self):

return (summed_raw / math.fabs(size_roi) - bck) / self.proton_charge

def get_tof_counts_table(self):
"""
Get a table of TOF vs counts in the region-of-interest (ROI)
The table columns are:
- TOF
- wavelength
- counts normalized by proton charge
- error in counts normalized by proton charge
- counts
- error in counts
- size of the ROI
"""
self.prepare_plot_data()
# Calculate ROI intensities and normalize by number of points
data_roi = self.data[
self.configuration.peak_roi[0] : self.configuration.peak_roi[1],
self.configuration.low_res_roi[0] : self.configuration.low_res_roi[1],
:,
]
counts_roi = data_roi.sum(axis=(0, 1))
raw_error_roi = self.raw_error[
self.configuration.peak_roi[0] : self.configuration.peak_roi[1],
self.configuration.low_res_roi[0] : self.configuration.low_res_roi[1],
:,
]
counts_roi_error = np.linalg.norm(raw_error_roi, axis=(0, 1)) # square root of sum of squares
if self.proton_charge > 0.0:
counts_roi_normalized = counts_roi / self.proton_charge
counts_roi_normalized_error = counts_roi_error / self.proton_charge
else:
counts_roi_normalized = counts_roi
counts_roi_normalized_error = counts_roi_error
size_roi = len(counts_roi) * [
float(
(self.configuration.low_res_roi[1] - self.configuration.low_res_roi[0])
* (self.configuration.peak_roi[1] - self.configuration.peak_roi[0])
)
]
data_table = np.vstack(
(
self.tof,
self.wavelength,
counts_roi_normalized,
counts_roi_normalized_error,
counts_roi,
counts_roi_error,
size_roi,
)
).T
header = "tof wavelength counts_normalized counts_normalized_error counts counts_error size_roi"
return data_table, header

def get_background_vs_TOF(self):
"""
Returns the background counts vs TOF
Expand Down
90 changes: 85 additions & 5 deletions reflectivity_ui/interfaces/event_handlers/main_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@
"""
Manage file-related and UI events
"""

import numpy as np

# package imports
from reflectivity_ui.interfaces.configuration import Configuration
from reflectivity_ui.interfaces.data_handling.data_manipulation import NormalizeToUnityQCutoffError
from .status_bar_handler import StatusBarHandler
from ..configuration import Configuration
from .progress_reporter import ProgressReporter
from .widgets import AcceptRejectDialog
from reflectivity_ui.interfaces.data_handling.data_set import NexusData, CrossSectionData
from reflectivity_ui.interfaces.data_handling.filepath import FilePath, RunNumbers
from reflectivity_ui.interfaces.event_handlers.progress_reporter import ProgressReporter
from reflectivity_ui.interfaces.event_handlers.status_bar_handler import StatusBarHandler
from reflectivity_ui.interfaces.event_handlers.widgets import AcceptRejectDialog
from reflectivity_ui.config import Settings

# 3rd-party imports
Expand Down Expand Up @@ -970,6 +971,68 @@ def active_data_changed(self):
item.setBackground(QtGui.QColor(255, 255, 255))
self.main_window.auto_change_active = False

def reduction_table_right_click(self, pos, is_reduction_table=True):
"""
Handle right-click on the reduction table.
:param QPoint pos: mouse position
:param bool is_reduction_table: True if the reduction table is active, False if the direct beam table is active
"""
if is_reduction_table:
table_widget = self.ui.reductionTable
data_table = self._data_manager.reduction_list
else:
table_widget = self.ui.normalizeTable
data_table = self._data_manager.direct_beam_list

def _export_data(_pos):
"""callback function to right-click action: Export data"""
row = table_widget.rowAt(pos.y())
if 0 <= row < len(data_table):
nexus_data = data_table[row]
self.save_run_data(nexus_data)

reduction_table_menu = QtWidgets.QMenu(table_widget)
export_data_action = QtWidgets.QAction("Export data")
export_data_action.triggered.connect(lambda: _export_data(pos))
reduction_table_menu.addAction(export_data_action)
reduction_table_menu.exec_(table_widget.mapToGlobal(pos))

def save_run_data(self, nexus_data: NexusData):
"""
Save run data to file
:param NexusData nexus_data: run data object
"""
path = QtWidgets.QFileDialog.getExistingDirectory(self.main_window, "Select directory")
if not path:
return
# ask user for base name for files (one file for each cross-section, e.g. "REF_M_1234_data_Off-Off.dat")
default_basename = f"REF_M_{nexus_data.number}_data"
while True: # to ask again for new basename if the user does not want to overwrite existing files
basename, ok = QtWidgets.QInputDialog.getText(
self.main_window, "Base name", "Save file base name:", text=default_basename
)
if not (ok and basename):
# user cancels
return
save_filepaths = {}
existing_filenames = []
for xs in nexus_data.cross_sections.keys():
filename = f"{basename}_{xs}.dat"
filepath = os.path.join(path, filename)
save_filepaths[xs] = filepath
if os.path.isfile(filepath):
existing_filenames.append(filename)
newline = "\n"
if len(existing_filenames) == 0 or self.ask_question(
f"Overwrite existing file(s):\n{newline.join(existing_filenames)}?"
):
break
# save one file per cross-section
for xs, filepath in save_filepaths.items():
cross_section = nexus_data.cross_sections[xs]
data_to_save, header = cross_section.get_tof_counts_table()
np.savetxt(filepath, data_to_save, header=header)

def compute_offspec_on_change(self, force=False):
"""
Compute off-specular as needed
Expand Down Expand Up @@ -1411,6 +1474,23 @@ def report_message(self, message, informative_message=None, detailed_message=Non
msg.setStandardButtons(QtWidgets.QMessageBox.Ok)
msg.exec_()

def ask_question(self, message):
"""
Display a popup dialog with a message and choices "Ok" and "Cancel"
:param str message: question to ask
:returns: bool
"""
ret = QtWidgets.QMessageBox.warning(
self.main_window,
"Warning",
message,
buttons=QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel,
defaultButton=QtWidgets.QMessageBox.Ok,
)
if ret == QtWidgets.QMessageBox.Cancel:
return False
return True

def show_results(self):
"""
Pop up the result viewer
Expand Down
16 changes: 15 additions & 1 deletion reflectivity_ui/interfaces/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
Main application window
"""


# package imports
from .data_manager import DataManager
from .plotting import PlotManager
Expand Down Expand Up @@ -47,6 +46,7 @@ def __init__(self):
QtWidgets.QMainWindow.__init__(self)

# Initialize the UI widgets
self.reduction_table_menu = None
self.ui = load_ui("ui_main_window.ui", baseinstance=self)
version = reflectivity_ui.__version__ if reflectivity_ui.__version__.lower() != "unknown" else ""
self.setWindowTitle(f"QuickNXS Magnetic Reflectivity {version}")
Expand Down Expand Up @@ -304,6 +304,20 @@ def reduction_cell_activated(self, row, col):
self.file_loaded()
self.file_handler.active_data_changed()

def reduction_table_right_click(self, pos):
"""
Handle right-click on the reduction table.
:param QPoint pos: mouse position
"""
self.file_handler.reduction_table_right_click(pos, True)

def direct_beam_table_right_click(self, pos):
"""
Handle right-click on the direct beam table.
:param QPoint pos: mouse position
"""
self.file_handler.reduction_table_right_click(pos, False)

def direct_beam_cell_activated(self, row, col):
"""
Select a data set when the user double-clicks on a run number (col 0).
Expand Down
39 changes: 39 additions & 0 deletions reflectivity_ui/ui/ui_main_window.ui
Original file line number Diff line number Diff line change
Expand Up @@ -2644,6 +2644,9 @@
<property name="rowCount">
<number>0</number>
</property>
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
<attribute name="horizontalHeaderVisible">
<bool>true</bool>
</attribute>
Expand Down Expand Up @@ -2810,6 +2813,9 @@
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
<attribute name="horizontalHeaderCascadingSectionResizes">
<bool>true</bool>
</attribute>
Expand Down Expand Up @@ -6440,6 +6446,38 @@
</hint>
</hints>
</connection>
<connection>
<sender>reductionTable</sender>
<signal>customContextMenuRequested(QPoint)</signal>
<receiver>MainWindow</receiver>
<slot>reduction_table_right_click(QPoint)</slot>
<hints>
<hint type="sourcelabel">
<x>1508</x>
<y>1348</y>
</hint>
<hint type="destinationlabel">
<x>778</x>
<y>551</y>
</hint>
</hints>
</connection>
<connection>
<sender>normalizeTable</sender>
<signal>customContextMenuRequested(QPoint)</signal>
<receiver>MainWindow</receiver>
<slot>direct_beam_table_right_click(QPoint)</slot>
<hints>
<hint type="sourcelabel">
<x>1508</x>
<y>1348</y>
</hint>
<hint type="destinationlabel">
<x>778</x>
<y>551</y>
</hint>
</hints>
</connection>
</connections>
<slots>
<slot>file_open_dialog()</slot>
Expand Down Expand Up @@ -6490,5 +6528,6 @@
<slot>hide_sidebar()</slot>
<slot>hide_run_data()</slot>
<slot>hide_data_table()</slot>
<slot>reduction_table_right_click()</slot>
</slots>
</ui>
28 changes: 28 additions & 0 deletions test/ui/test_main_window.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
# local imports
from reflectivity_ui.interfaces.configuration import Configuration
from reflectivity_ui.interfaces.data_handling.data_set import CrossSectionData, NexusData
from reflectivity_ui.interfaces.main_window import MainWindow
from test import SNS_REFM_MOUNTED
from test.ui import ui_utilities

# third party imports
import pytest
from qtpy import QtCore, QtWidgets


# standard library imports

Expand Down Expand Up @@ -61,6 +64,31 @@ def test_active_channel(self, mocker, qtbot):
# check the current channel name displayed in the UI
assert channel1.name in window_main.ui.currentChannel.text()

@pytest.mark.parametrize("table_widget", ["reductionTable", "normalizeTable"])
def test_reduction_table_right_click(self, table_widget, qtbot, mocker):
mock_save_run_data = mocker.patch(
"reflectivity_ui.interfaces.event_handlers.main_handler.MainHandler.save_run_data"
)
window_main = MainWindow()
qtbot.addWidget(window_main)
window_main.data_manager.reduction_list = [NexusData("filepath", Configuration())]
window_main.data_manager.direct_beam_list = [NexusData("filepath", Configuration())]
table = getattr(window_main.ui, table_widget)
table.insertRow(0)

def handle_menu():
"""Press Enter on item in menu and check that the function was called"""
menu = table.findChild(QtWidgets.QMenu)
action = menu.actions()[0]
assert action.text() == "Export data"
qtbot.keyClick(menu, QtCore.Qt.Key_Down)
qtbot.keyClick(menu, QtCore.Qt.Key_Enter)
mock_save_run_data.assert_called_once()

QtCore.QTimer.singleShot(200, handle_menu)
pos = QtCore.QPoint()
table.customContextMenuRequested.emit(pos)


if __name__ == "__main__":
pytest.main([__file__])
Loading

0 comments on commit ba09e11

Please sign in to comment.