From 6222864c287d19dc5393d4e4ba67e98fb8044170 Mon Sep 17 00:00:00 2001 From: Caitlyn Wolf Date: Mon, 22 Jan 2024 00:05:57 -0500 Subject: [PATCH 01/14] added interpolation to data operations and interpolated points --- .../Calculators/DataOperationUtilityPanel.py | 30 ++++++--- src/sas/qtgui/Plotting/PlotterData.py | 61 +++++++++---------- 2 files changed, 49 insertions(+), 42 deletions(-) diff --git a/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py b/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py index b6ca232a96..d24cf2594f 100644 --- a/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py +++ b/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py @@ -6,6 +6,7 @@ from PySide6 import QtCore from PySide6 import QtGui from PySide6 import QtWidgets +import numpy as np from sas.qtgui.Plotting.PlotterData import Data1D from sas.qtgui.Plotting.Plotter import PlotterWidget @@ -148,6 +149,8 @@ def onCompute(self): self.onPrepareOutputData() # plot result self.updatePlot(self.graphOutput, self.layoutOutput, self.output) + self.updatePlot(self.graphData1, self.layoutData1, self.data1, add_interp=True) + self.updatePlot(self.graphData2, self.layoutData2, self.data2, add_interp=True) # Add the new plot to the comboboxes self.cbData1.addItem(self.output.name) @@ -302,13 +305,14 @@ def onCheckChosenData(self): logging.error('Cannot compute data of different dimensions') return False - elif self.data1.__class__.__name__ == 'Data1D'\ - and (len(self.data2.x) != len(self.data1.x) or - not all(i == j for i, j in zip(self.data1.x, self.data2.x))): - logging.error('Cannot compute 1D data of different lengths') - self.cbData1.setStyleSheet(BG_RED) - self.cbData2.setStyleSheet(BG_RED) - return False + # handling data with different q values implemented by CMW 1-21-2024 + # elif self.data1.__class__.__name__ == 'Data1D'\ + # and (len(self.data2.x) != len(self.data1.x) or + # not all(i == j for i, j in zip(self.data1.x, self.data2.x))): + # logging.error('Cannot compute 1D data of different lengths') + # self.cbData1.setStyleSheet(BG_RED) + # self.cbData2.setStyleSheet(BG_RED) + # return False elif self.data1.__class__.__name__ == 'Data2D' \ and (len(self.data2.qx_data) != len(self.data1.qx_data) \ @@ -387,7 +391,7 @@ def newPlot(self, graph, layout): graph.setLayout(layout) - def updatePlot(self, graph, layout, data): + def updatePlot(self, graph, layout, data, add_interp=False): """ plot data in graph after clearing its layout """ assert isinstance(graph, QtWidgets.QGraphicsView) @@ -399,7 +403,6 @@ def updatePlot(self, graph, layout, data): layout.removeItem(item) layout.setContentsMargins(0, 0, 0, 0) - if isinstance(data, Data2D): # plot 2D data plotter2D = Plotter2DWidget(self, quickplot=True) @@ -431,6 +434,15 @@ def updatePlot(self, graph, layout, data): plotter.ax.tick_params(axis='y', labelsize=8) plotter.plot(data=data, hide_error=True, marker='.') + if add_interp: + interp = Data1D(x=[], y=[], dx=None, dy=None) + interp.clone_without_data(length=data.x.size, clone=self) + interp.copy_from_datainfo(data1d=data) + interp.x = np.copy(data._x_op) + interp.y = np.copy(data._y_op) + interp.dy = np.copy(data._dy_op) + interp.dx = np.zeros(data._x_op.size) + plotter.plot(data=interp, hide_error=True, marker='.') plotter.show() diff --git a/src/sas/qtgui/Plotting/PlotterData.py b/src/sas/qtgui/Plotting/PlotterData.py index e43539dcc0..02fb76d21c 100644 --- a/src/sas/qtgui/Plotting/PlotterData.py +++ b/src/sas/qtgui/Plotting/PlotterData.py @@ -105,45 +105,40 @@ def __str__(self): def _perform_operation(self, other, operation): """ """ - # First, check the data compatibility - dy, dy_other = self._validity_check(other) + # Check for compatibility of the x-ranges and populate the data used for the operation + # interpolation will be implemented on the 'other' dataset as needed + self._interpolation_operation(other) + result = Data1D(x=[], y=[], dx=None, dy=None) - result.clone_without_data(length=len(self.x), clone=self) + result.clone_without_data(length=self._x_op.size, clone=self) result.copy_from_datainfo(data1d=self) - if self.dxw is None: - result.dxw = None - else: - result.dxw = numpy.zeros(len(self.x)) - if self.dxl is None: - result.dxl = None - else: - result.dxl = numpy.zeros(len(self.x)) - for i in range(len(self.x)): - result.x[i] = self.x[i] - if self.dx is not None and len(self.x) == len(self.dx): - result.dx[i] = self.dx[i] - if self.dxw is not None and len(self.x) == len(self.dxw): - result.dxw[i] = self.dxw[i] - if self.dxl is not None and len(self.x) == len(self.dxl): - result.dxl[i] = self.dxl[i] - - a = Uncertainty(self.y[i], dy[i]**2) + # result = self.clone_without_data(self._x_op.size) + result.x = numpy.copy(self._x_op) + result.y = numpy.zeros(self._x_op.size) + result.dy = numpy.zeros(self._x_op.size) + # result.y is initialized as arrays of zero with length of _x_op + # result.dy is initialized as arrays of zero with length of _x_op + result.dx = None if self._dx_op is None else numpy.copy(self._dx_op) + result.dxl = None if self._dxl_op is None else numpy.copy(self._dxl_op) + result.dxw = None if self._dxw_op is None else numpy.copy(self._dxw_op) + result.lam = None if self._lam_op is None else numpy.copy(self._lam_op) + result.dlam = None if self._dlam_op is None else numpy.copy(self._dlam_op) + + for i in range(result.x.size): + + a = Uncertainty(self._y_op[i], self._dy_op[i]**2) if isinstance(other, Data1D): - b = Uncertainty(other.y[i], dy_other[i]**2) - if other.dx is not None: - result.dx[i] *= self.dx[i] - result.dx[i] += (other.dx[i]**2) - result.dx[i] /= 2 - result.dx[i] = math.sqrt(result.dx[i]) - if result.dxl is not None and other.dxl is not None: - result.dxl[i] *= self.dxl[i] - result.dxl[i] += (other.dxl[i]**2) - result.dxl[i] /= 2 - result.dxl[i] = math.sqrt(result.dxl[i]) + b = Uncertainty(other._y_op[i], other._dy_op[i]**2) + if result.dx is not None and other._dx_op is not None: + result.dx[i] = math.sqrt((self._dx_op[i]**2 + other._dx_op[i]**2) / 2) + if result.dxl is not None and other._dxl_op is not None: + result.dxl[i] = math.sqrt((self._dxl_op[i]**2 + other._dxl_op[i]**2) / 2) + if result.dxw is not None and other._dxw_op is not None: + result.dxw[i] = math.sqrt((self._dxw_op[i]**2 + other._dxw_op[i]**2) / 2) else: b = other - + output = operation(a, b) result.y[i] = output.x result.dy[i] = math.sqrt(math.fabs(output.variance)) From 2d2d9a02296a40792af713ee009e74843e762ccb Mon Sep 17 00:00:00 2001 From: Caitlyn Wolf Date: Mon, 22 Jan 2024 00:28:47 -0500 Subject: [PATCH 02/14] added preview functionality to data operations --- .../Calculators/DataOperationUtilityPanel.py | 21 ++++++++++++++----- .../Calculators/UI/DataOperationUtilityUI.ui | 21 ++++++++++++++++++- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py b/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py index d24cf2594f..189f2a56b3 100644 --- a/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py +++ b/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py @@ -50,6 +50,7 @@ def __init__(self, parent=None): # push buttons self.cmdClose.clicked.connect(self.onClose) self.cmdHelp.clicked.connect(self.onHelp) + self.cmdSave.clicked.connect(self.onSave) self.cmdCompute.clicked.connect(self.onCompute) self.cmdReset.clicked.connect(self.onReset) @@ -125,7 +126,7 @@ def onClose(self): def onCompute(self): - """ perform calculation """ + """ perform calculation - don't send to data explorer""" # set operator to be applied operator = self.cbOperator.currentText() # calculate and send data to DataExplorer @@ -140,6 +141,20 @@ def onCompute(self): self.output = output + self.updatePlot(self.graphOutput, self.layoutOutput, self.output) + self.updatePlot(self.graphData1, self.layoutData1, self.data1, add_interp=True) + self.updatePlot(self.graphData2, self.layoutData2, self.data2, add_interp=True) + + # Add the new plot to the comboboxes + self.cbData1.addItem(self.output.name) + self.cbData2.addItem(self.output.name) + if self.filenames is None: + self.filenames = {} + self.filenames[self.output.name] = self.output + + def onSave(self): + """ send to data explorer """ + # if outputname was unused, write output result to it # and display plot if self.onCheckOutputName(): @@ -147,10 +162,6 @@ def onCompute(self): self.list_data_items.append(str(self.txtOutputData.text())) # send result to DataExplorer self.onPrepareOutputData() - # plot result - self.updatePlot(self.graphOutput, self.layoutOutput, self.output) - self.updatePlot(self.graphData1, self.layoutData1, self.data1, add_interp=True) - self.updatePlot(self.graphData2, self.layoutData2, self.data2, add_interp=True) # Add the new plot to the comboboxes self.cbData1.addItem(self.output.name) diff --git a/src/sas/qtgui/Calculators/UI/DataOperationUtilityUI.ui b/src/sas/qtgui/Calculators/UI/DataOperationUtilityUI.ui index cd70afcbb5..e037d0513d 100644 --- a/src/sas/qtgui/Calculators/UI/DataOperationUtilityUI.ui +++ b/src/sas/qtgui/Calculators/UI/DataOperationUtilityUI.ui @@ -434,7 +434,7 @@ Append(Combine): | - Generate the Data and send to Data Explorer. + Compute data operation and preview before sending to Data Explorer. Compute @@ -444,6 +444,25 @@ Append(Combine): | + + + + + 75 + 25 + + + + Send data to the Data Explorer. + + + Save + + + false + + + From e4c2d3a02fb94b20623466254267635a31c4e82a Mon Sep 17 00:00:00 2001 From: Caitlyn Wolf Date: Mon, 22 Jan 2024 14:53:33 -0500 Subject: [PATCH 03/14] cleaning up code for interpolation --- .../Calculators/DataOperationUtilityPanel.py | 21 ++++------ src/sas/qtgui/Plotting/PlotterData.py | 40 +++++++++---------- 2 files changed, 26 insertions(+), 35 deletions(-) diff --git a/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py b/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py index 189f2a56b3..a2cd8b19ed 100644 --- a/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py +++ b/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py @@ -141,9 +141,9 @@ def onCompute(self): self.output = output - self.updatePlot(self.graphOutput, self.layoutOutput, self.output) - self.updatePlot(self.graphData1, self.layoutData1, self.data1, add_interp=True) - self.updatePlot(self.graphData2, self.layoutData2, self.data2, add_interp=True) + self.updatePlot(self.graphOutput, self.layoutOutput, self.output, color='#000000') + self.updatePlot(self.graphData1, self.layoutData1, self.data1, color='#882255', add_interp=True, color_interp='#44AA99') + self.updatePlot(self.graphData2, self.layoutData2, self.data2, color='#332288', add_interp=True, color_interp='#CC6677') # Add the new plot to the comboboxes self.cbData1.addItem(self.output.name) @@ -402,7 +402,7 @@ def newPlot(self, graph, layout): graph.setLayout(layout) - def updatePlot(self, graph, layout, data, add_interp=False): + def updatePlot(self, graph, layout, data, color=None, add_interp=False, color_interp=None): """ plot data in graph after clearing its layout """ assert isinstance(graph, QtWidgets.QGraphicsView) @@ -444,16 +444,11 @@ def updatePlot(self, graph, layout, data, add_interp=False): plotter.ax.tick_params(axis='x', labelsize=8) plotter.ax.tick_params(axis='y', labelsize=8) - plotter.plot(data=data, hide_error=True, marker='.') + plotter.plot(data=data, hide_error=True, marker='.', color=color) if add_interp: - interp = Data1D(x=[], y=[], dx=None, dy=None) - interp.clone_without_data(length=data.x.size, clone=self) - interp.copy_from_datainfo(data1d=data) - interp.x = np.copy(data._x_op) - interp.y = np.copy(data._y_op) - interp.dy = np.copy(data._dy_op) - interp.dx = np.zeros(data._x_op.size) - plotter.plot(data=interp, hide_error=True, marker='.') + interp = Data1D(x=data._operation.x, y=data._operation.y, dy=data._operation.dy, dx=None) + interp.copy_from_datainfo(data1d=data._operation) + plotter.plot(data=interp, hide_error=True, marker='.', color=color_interp) plotter.show() diff --git a/src/sas/qtgui/Plotting/PlotterData.py b/src/sas/qtgui/Plotting/PlotterData.py index 02fb76d21c..1b3d728ef0 100644 --- a/src/sas/qtgui/Plotting/PlotterData.py +++ b/src/sas/qtgui/Plotting/PlotterData.py @@ -110,32 +110,28 @@ def _perform_operation(self, other, operation): self._interpolation_operation(other) result = Data1D(x=[], y=[], dx=None, dy=None) - result.clone_without_data(length=self._x_op.size, clone=self) + result.clone_without_data(length=self._operation.x.size, clone=self) result.copy_from_datainfo(data1d=self) - - # result = self.clone_without_data(self._x_op.size) - result.x = numpy.copy(self._x_op) - result.y = numpy.zeros(self._x_op.size) - result.dy = numpy.zeros(self._x_op.size) - # result.y is initialized as arrays of zero with length of _x_op - # result.dy is initialized as arrays of zero with length of _x_op - result.dx = None if self._dx_op is None else numpy.copy(self._dx_op) - result.dxl = None if self._dxl_op is None else numpy.copy(self._dxl_op) - result.dxw = None if self._dxw_op is None else numpy.copy(self._dxw_op) - result.lam = None if self._lam_op is None else numpy.copy(self._lam_op) - result.dlam = None if self._dlam_op is None else numpy.copy(self._dlam_op) + result.x = numpy.copy(self._operation.x) + result.y = numpy.zeros(self._operation.x.size) + result.dy = numpy.zeros(self._operation.x.size) + result.dx = None if self._operation.dx is None else numpy.copy(self._operation.dx) + result.dxl = None if self._operation.dxl is None else numpy.copy(self._operation.dxl) + result.dxw = None if self._operation.dxw is None else numpy.copy(self._operation.dxw) + result.lam = None if self._operation.lam is None else numpy.copy(self._operation.lam) + result.dlam = None if self._operation.dlam is None else numpy.copy(self._operation.dlam) for i in range(result.x.size): - a = Uncertainty(self._y_op[i], self._dy_op[i]**2) + a = Uncertainty(self._operation.y[i], self._operation.dy[i]**2) if isinstance(other, Data1D): - b = Uncertainty(other._y_op[i], other._dy_op[i]**2) - if result.dx is not None and other._dx_op is not None: - result.dx[i] = math.sqrt((self._dx_op[i]**2 + other._dx_op[i]**2) / 2) - if result.dxl is not None and other._dxl_op is not None: - result.dxl[i] = math.sqrt((self._dxl_op[i]**2 + other._dxl_op[i]**2) / 2) - if result.dxw is not None and other._dxw_op is not None: - result.dxw[i] = math.sqrt((self._dxw_op[i]**2 + other._dxw_op[i]**2) / 2) + b = Uncertainty(other._operation.y[i], other._operation.dy[i]**2) + if result.dx is not None and other._operation.dx is not None: + result.dx[i] = math.sqrt((self._operation.dx[i]**2 + other._operation.dx[i]**2) / 2) + if result.dxl is not None and other._operation.dxl is not None: + result.dxl[i] = math.sqrt((self._operation.dxl[i]**2 + other._operation.dxl[i]**2) / 2) + if result.dxw is not None and other._operation.dxw is not None: + result.dxw[i] = math.sqrt((self._operation.dxw[i]**2 + other._operation.dxw[i]**2) / 2) else: b = other @@ -143,7 +139,7 @@ def _perform_operation(self, other, operation): result.y[i] = output.x result.dy[i] = math.sqrt(math.fabs(output.variance)) return result - + def _perform_union(self, other): """ """ From 697bfb9f13b9c699bcb7159539d19de7de23a653 Mon Sep 17 00:00:00 2001 From: Caitlyn Wolf Date: Mon, 22 Jan 2024 16:24:51 -0500 Subject: [PATCH 04/14] changes to plots --- .../Calculators/DataOperationUtilityPanel.py | 56 +++++++++++++++---- 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py b/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py index a2cd8b19ed..94e386bec2 100644 --- a/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py +++ b/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py @@ -8,6 +8,8 @@ from PySide6 import QtWidgets import numpy as np +from typing import Optional + from sas.qtgui.Plotting.PlotterData import Data1D from sas.qtgui.Plotting.Plotter import PlotterWidget from sas.qtgui.Plotting.PlotterData import Data2D @@ -141,9 +143,13 @@ def onCompute(self): self.output = output - self.updatePlot(self.graphOutput, self.layoutOutput, self.output, color='#000000') - self.updatePlot(self.graphData1, self.layoutData1, self.data1, color='#882255', add_interp=True, color_interp='#44AA99') - self.updatePlot(self.graphData2, self.layoutData2, self.data2, color='#332288', add_interp=True, color_interp='#CC6677') + self.updatePlot(self.graphOutput, self.layoutOutput, self.output, color='#000000', + operation_data=True, data_op=[self.data1, self.data2], color_op=["#44AA99", "#CC6677"], + overlap_op=False) + self.updatePlot(self.graphData1, self.layoutData1, self.data1, color='#882255', + operation_data=True, data_op=[self.data1], color_op=["#44AA99"], overlap_op=True) + self.updatePlot(self.graphData2, self.layoutData2, self.data2, color='#332288', + operation_data=True, data_op=[self.data2], color_op=['#CC6677'], overlap_op=True) # Add the new plot to the comboboxes self.cbData1.addItem(self.output.name) @@ -232,7 +238,7 @@ def onSelectData1(self): key_id1 = self._findId(choice_data1) self.data1 = self._extractData(key_id1) # plot Data1 - self.updatePlot(self.graphData1, self.layoutData1, self.data1) + self.updatePlot(self.graphData1, self.layoutData1, self.data1, color='#882255') # plot default for output graph self.newPlot(self.graphOutput, self.layoutOutput) # Enable Compute button only if Data2 is defined and data compatible @@ -259,7 +265,7 @@ def onSelectData2(self): # Enable Compute button only if Data1 defined and compatible data self.cmdCompute.setEnabled(self.onCheckChosenData()) # Display value of coefficient in graphData2 - self.updatePlot(self.graphData2, self.layoutData2, self.data2) + self.updatePlot(self.graphData2, self.layoutData2, self.data2, color='#332288') # plot default for output graph self.newPlot(self.graphOutput, self.layoutOutput) self.onCheckChosenData() @@ -272,7 +278,7 @@ def onSelectData2(self): self.cmdCompute.setEnabled(self.onCheckChosenData()) # plot Data2 - self.updatePlot(self.graphData2, self.layoutData2, self.data2) + self.updatePlot(self.graphData2, self.layoutData2, self.data2, color='#332288') # plot default for output graph self.newPlot(self.graphOutput, self.layoutOutput) @@ -296,7 +302,7 @@ def onInputCoefficient(self): else: self.txtNumber.setStyleSheet(BG_WHITE) self.data2 = float(self.txtNumber.text()) - self.updatePlot(self.graphData2, self.layoutData2, self.data2) + self.updatePlot(self.graphData2, self.layoutData2, self.data2, color='#332288') def onCheckChosenData(self): """ check that data1 and data2 are compatible """ @@ -402,7 +408,7 @@ def newPlot(self, graph, layout): graph.setLayout(layout) - def updatePlot(self, graph, layout, data, color=None, add_interp=False, color_interp=None): + def updatePlot(self, graph, layout, data, color=None, operation_data=False, data_op: Optional[list] = None, color_op: Optional[list] = None, overlap_op=True): """ plot data in graph after clearing its layout """ assert isinstance(graph, QtWidgets.QGraphicsView) @@ -444,11 +450,37 @@ def updatePlot(self, graph, layout, data, color=None, add_interp=False, color_in plotter.ax.tick_params(axis='x', labelsize=8) plotter.ax.tick_params(axis='y', labelsize=8) + # put the operation data below regular data + if operation_data is True and overlap_op is False: + for d_op, c_op in zip(data_op, color_op): + if isinstance(d_op, Data1D): + op_data = Data1D(x=d_op._operation.x, y=d_op._operation.y, dy=d_op._operation.dy, dx=None) + op_data.copy_from_datainfo(data1d=d_op._operation) + plotter.plot(data=op_data, hide_error=True, marker='.', color=c_op) + else: + op_data = Data1D(2) + op_data_copy_from_datainfo(data1d=data) + data.x = np.array([data.x.min(), data.x.max()]) + data.y = np.array([d_op[0], d_op[0]]) + data.dy = np.zeros(2) + data.dx = np.zeros(2) + plotter.plot(data=op_data, hide_error=True, marker='-', color=c_op) plotter.plot(data=data, hide_error=True, marker='.', color=color) - if add_interp: - interp = Data1D(x=data._operation.x, y=data._operation.y, dy=data._operation.dy, dx=None) - interp.copy_from_datainfo(data1d=data._operation) - plotter.plot(data=interp, hide_error=True, marker='.', color=color_interp) + # put the operation data on top of regular data + if operation_data is True and overlap_op is False: + for d_op, c_op in zip(data_op, color_op): + if isinstance(d_op, Data1D): + op_data = Data1D(x=d_op._operation.x, y=d_op._operation.y, dy=d_op._operation.dy, dx=None) + op_data.copy_from_datainfo(data1d=d_op._operation) + plotter.plot(data=op_data, hide_error=True, marker='.', color=c_op) + else: + op_data = Data1D(2) + op_data_copy_from_datainfo(data1d=data) + data.x = np.array([data.x.min(), data.x.max()]) + data.y = np.array([d_op[0], d_op[0]]) + data.dy = np.zeros(2) + data.dx = np.zeros(2) + plotter.plot(data=op_data, hide_error=True, marker='-', color=c_op) plotter.show() From b262fe28348abf0a93cda320f1616794c3543164 Mon Sep 17 00:00:00 2001 From: Caitlyn Wolf Date: Mon, 22 Jan 2024 18:42:38 -0500 Subject: [PATCH 05/14] updated data operation plots and reversed format to A + B = C rather than C = A + B --- .../Calculators/DataOperationUtilityPanel.py | 79 +++++++++++-------- .../Calculators/UI/DataOperationUtilityUI.ui | 38 ++++----- 2 files changed, 63 insertions(+), 54 deletions(-) diff --git a/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py b/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py index 94e386bec2..8ff38bb8f7 100644 --- a/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py +++ b/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py @@ -148,12 +148,12 @@ def onCompute(self): overlap_op=False) self.updatePlot(self.graphData1, self.layoutData1, self.data1, color='#882255', operation_data=True, data_op=[self.data1], color_op=["#44AA99"], overlap_op=True) - self.updatePlot(self.graphData2, self.layoutData2, self.data2, color='#332288', - operation_data=True, data_op=[self.data2], color_op=['#CC6677'], overlap_op=True) + self.updatePlot(self.graphData2, self.layoutData2, self.data2, color='#332288' if isinstance(self.data2, Data1D) else '#CC6677', + operation_data=True if isinstance(self.data2, Data1D) else False, data_op=[self.data2], color_op=['#CC6677'], overlap_op=True) # Add the new plot to the comboboxes - self.cbData1.addItem(self.output.name) - self.cbData2.addItem(self.output.name) + # self.cbData1.addItem(self.output.name) + # self.cbData2.addItem(self.output.name) if self.filenames is None: self.filenames = {} self.filenames[self.output.name] = self.output @@ -265,7 +265,7 @@ def onSelectData2(self): # Enable Compute button only if Data1 defined and compatible data self.cmdCompute.setEnabled(self.onCheckChosenData()) # Display value of coefficient in graphData2 - self.updatePlot(self.graphData2, self.layoutData2, self.data2, color='#332288') + self.updatePlot(self.graphData2, self.layoutData2, self.data2, color='#CC6677') # plot default for output graph self.newPlot(self.graphOutput, self.layoutOutput) self.onCheckChosenData() @@ -408,6 +408,22 @@ def newPlot(self, graph, layout): graph.setLayout(layout) + def addOperationData(self, plotter, data, data_op, color_op): + + for d_op, c_op in zip(data_op, color_op): + if isinstance(d_op, Data1D): + op_data = Data1D(x=d_op._operation.x, y=d_op._operation.y, dy=d_op._operation.dy, dx=None) + op_data.copy_from_datainfo(data1d=d_op._operation) + plotter.plot(data=op_data, hide_error=True, marker='.', color=c_op) + else: + op_data = Data1D(2) + op_data.copy_from_datainfo(data1d=data) + op_data.x = np.array([data.x.min(), data.x.max()]) + op_data.y = np.array([d_op, d_op]) + op_data.dy = np.zeros(2) + op_data.dx = np.zeros(2) + plotter.plot(data=op_data, hide_error=True, marker='-', color=c_op) + def updatePlot(self, graph, layout, data, color=None, operation_data=False, data_op: Optional[list] = None, color_op: Optional[list] = None, overlap_op=True): """ plot data in graph after clearing its layout """ @@ -452,44 +468,37 @@ def updatePlot(self, graph, layout, data, color=None, operation_data=False, data # put the operation data below regular data if operation_data is True and overlap_op is False: - for d_op, c_op in zip(data_op, color_op): - if isinstance(d_op, Data1D): - op_data = Data1D(x=d_op._operation.x, y=d_op._operation.y, dy=d_op._operation.dy, dx=None) - op_data.copy_from_datainfo(data1d=d_op._operation) - plotter.plot(data=op_data, hide_error=True, marker='.', color=c_op) - else: - op_data = Data1D(2) - op_data_copy_from_datainfo(data1d=data) - data.x = np.array([data.x.min(), data.x.max()]) - data.y = np.array([d_op[0], d_op[0]]) - data.dy = np.zeros(2) - data.dx = np.zeros(2) - plotter.plot(data=op_data, hide_error=True, marker='-', color=c_op) + self.addOperationData(plotter, data, data_op, color_op) plotter.plot(data=data, hide_error=True, marker='.', color=color) # put the operation data on top of regular data - if operation_data is True and overlap_op is False: - for d_op, c_op in zip(data_op, color_op): - if isinstance(d_op, Data1D): - op_data = Data1D(x=d_op._operation.x, y=d_op._operation.y, dy=d_op._operation.dy, dx=None) - op_data.copy_from_datainfo(data1d=d_op._operation) - plotter.plot(data=op_data, hide_error=True, marker='.', color=c_op) - else: - op_data = Data1D(2) - op_data_copy_from_datainfo(data1d=data) - data.x = np.array([data.x.min(), data.x.max()]) - data.y = np.array([d_op[0], d_op[0]]) - data.dy = np.zeros(2) - data.dx = np.zeros(2) - plotter.plot(data=op_data, hide_error=True, marker='-', color=c_op) + if operation_data is True and overlap_op is True: + self.addOperationData(plotter, data, data_op, color_op) plotter.show() elif float(data) and self.cbData2.currentText() == 'Number': # display value of coefficient (to be applied to Data1) - # in graphData2 - layout.addWidget(self.prepareSubgraphWithData(data)) - + # in graphData2 as a line + plotter = PlotterWidget(self, quickplot=True) + plotter.showLegend = False graph.setLayout(layout) + layout.addWidget(plotter) + + plotter.ax.tick_params(axis='x', labelsize=8) + plotter.ax.tick_params(axis='y', labelsize=8) + + op_data = Data1D(2) + op_data.copy_from_datainfo(data1d=self.data1) + op_data.scale = 'linear' + op_data.x = np.array([1e-5, 1]) + op_data.y = np.array([data, data]) + op_data.dy = np.zeros(2) + op_data.dx = np.zeros(2) + plotter.plot(data=op_data, hide_error=True, marker='-', color=color) + + plotter.show() + + def prepareSubgraphWithData(self, data): """ Create graphics view containing scene with string """ diff --git a/src/sas/qtgui/Calculators/UI/DataOperationUtilityUI.ui b/src/sas/qtgui/Calculators/UI/DataOperationUtilityUI.ui index e037d0513d..9b3c9d4699 100644 --- a/src/sas/qtgui/Calculators/UI/DataOperationUtilityUI.ui +++ b/src/sas/qtgui/Calculators/UI/DataOperationUtilityUI.ui @@ -62,21 +62,21 @@ - + Output Data Name - + Data2 (or Number) - + @@ -92,7 +92,7 @@ - + Qt::Horizontal @@ -105,7 +105,7 @@ - + @@ -124,7 +124,7 @@ - + @@ -154,7 +154,7 @@ - + Qt::Horizontal @@ -167,7 +167,7 @@ - + Qt::Horizontal @@ -180,14 +180,14 @@ - + Data1 - + @@ -206,7 +206,7 @@ - + @@ -225,7 +225,7 @@ - + Qt::Horizontal @@ -238,7 +238,7 @@ - + @@ -254,7 +254,7 @@ - + false @@ -267,7 +267,7 @@ - 150 + 100 30 @@ -279,7 +279,7 @@ - + @@ -298,7 +298,7 @@ - + @@ -322,7 +322,7 @@ - + @@ -370,7 +370,7 @@ Append(Combine): | - + From 08631e9a7003f8f2fa639e458d8596b010758c20 Mon Sep 17 00:00:00 2001 From: Caitlyn Wolf Date: Mon, 22 Jan 2024 18:46:44 -0500 Subject: [PATCH 06/14] Update ci.yml to handle merge conflicts with rebase --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1623212a35..1e9e864639 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -105,7 +105,7 @@ jobs: - name: Fetch sources for sibling projects run: | - git clone --depth=50 --branch=release_0.9.0 https://github.com/SasView/sasdata.git ../sasdata + git clone --depth=50 --branch=interpolations_for_data_operations https://github.com/SasView/sasdata.git ../sasdata git clone --depth=50 --branch=master https://github.com/SasView/sasmodels.git ../sasmodels git clone --depth=50 --branch=master https://github.com/bumps/bumps.git ../bumps From f9ed49479f09ee24fc2c1fae42961129cce6f321 Mon Sep 17 00:00:00 2001 From: Caitlyn Wolf Date: Tue, 30 Jan 2024 14:21:53 -0500 Subject: [PATCH 07/14] removed commented unused code and extra whitespace --- .../qtgui/Calculators/DataOperationUtilityPanel.py | 12 ------------ src/sas/qtgui/Plotting/PlotterData.py | 2 -- 2 files changed, 14 deletions(-) diff --git a/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py b/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py index 8ff38bb8f7..991bcf428f 100644 --- a/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py +++ b/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py @@ -7,7 +7,6 @@ from PySide6 import QtGui from PySide6 import QtWidgets import numpy as np - from typing import Optional from sas.qtgui.Plotting.PlotterData import Data1D @@ -322,15 +321,6 @@ def onCheckChosenData(self): logging.error('Cannot compute data of different dimensions') return False - # handling data with different q values implemented by CMW 1-21-2024 - # elif self.data1.__class__.__name__ == 'Data1D'\ - # and (len(self.data2.x) != len(self.data1.x) or - # not all(i == j for i, j in zip(self.data1.x, self.data2.x))): - # logging.error('Cannot compute 1D data of different lengths') - # self.cbData1.setStyleSheet(BG_RED) - # self.cbData2.setStyleSheet(BG_RED) - # return False - elif self.data1.__class__.__name__ == 'Data2D' \ and (len(self.data2.qx_data) != len(self.data1.qx_data) \ or len(self.data2.qy_data) != len(self.data1.qy_data) @@ -498,8 +488,6 @@ def updatePlot(self, graph, layout, data, color=None, operation_data=False, data plotter.show() - - def prepareSubgraphWithData(self, data): """ Create graphics view containing scene with string """ scene = QtWidgets.QGraphicsScene() diff --git a/src/sas/qtgui/Plotting/PlotterData.py b/src/sas/qtgui/Plotting/PlotterData.py index 1b3d728ef0..8cbcc58b13 100644 --- a/src/sas/qtgui/Plotting/PlotterData.py +++ b/src/sas/qtgui/Plotting/PlotterData.py @@ -122,7 +122,6 @@ def _perform_operation(self, other, operation): result.dlam = None if self._operation.dlam is None else numpy.copy(self._operation.dlam) for i in range(result.x.size): - a = Uncertainty(self._operation.y[i], self._operation.dy[i]**2) if isinstance(other, Data1D): b = Uncertainty(other._operation.y[i], other._operation.dy[i]**2) @@ -134,7 +133,6 @@ def _perform_operation(self, other, operation): result.dxw[i] = math.sqrt((self._operation.dxw[i]**2 + other._operation.dxw[i]**2) / 2) else: b = other - output = operation(a, b) result.y[i] = output.x result.dy[i] = math.sqrt(math.fabs(output.variance)) From 96a389f0c82c51ff58df2d29b3c5d368b3760c1a Mon Sep 17 00:00:00 2001 From: Caitlyn Wolf Date: Wed, 31 Jan 2024 12:05:04 -0500 Subject: [PATCH 08/14] added caution statement to data operations panel regarding interpolation --- src/sas/qtgui/Calculators/UI/DataOperationUtilityUI.ui | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/sas/qtgui/Calculators/UI/DataOperationUtilityUI.ui b/src/sas/qtgui/Calculators/UI/DataOperationUtilityUI.ui index 9b3c9d4699..4d00278ce9 100644 --- a/src/sas/qtgui/Calculators/UI/DataOperationUtilityUI.ui +++ b/src/sas/qtgui/Calculators/UI/DataOperationUtilityUI.ui @@ -396,6 +396,14 @@ Append(Combine): | + + + + CAUTION: interpolation of Data2 will occur for 1D-datasets if x-axis points + are not close. This could introduce artifacts. Please see documentation. + + + From 3f2f1a81a994f3a5d825f21cb083da76a5405488 Mon Sep 17 00:00:00 2001 From: Caitlyn Wolf Date: Wed, 31 Jan 2024 12:05:44 -0500 Subject: [PATCH 09/14] cleaned up colors for data operation panel plots --- .../Calculators/DataOperationUtilityPanel.py | 146 +++++++++++++----- src/sas/qtgui/Plotting/Plotter.py | 22 ++- src/sas/qtgui/Plotting/PlotterData.py | 6 +- 3 files changed, 126 insertions(+), 48 deletions(-) diff --git a/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py b/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py index 991bcf428f..d8100ab3bf 100644 --- a/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py +++ b/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py @@ -20,6 +20,12 @@ BG_WHITE = "background-color: rgb(255, 255, 255); color: rgb(0, 0, 0);" BG_RED = "background-color: rgb(244, 170, 164);" +# colors for data operation plots +OUTPUT_COLOR = "#000000" # black +DATA1_COLOR = '#B22222' # firebrick +DATA2_COLOR = '#0000FF' # blue +TRIMMED_COLOR = '#FFFFFF' # white +TRIMMED_ALPHA = 0.3 # semi-transparent points trimmed for operation class DataOperationUtilityPanel(QtWidgets.QDialog, Ui_DataOperationUtility): def __init__(self, parent=None): @@ -142,13 +148,9 @@ def onCompute(self): self.output = output - self.updatePlot(self.graphOutput, self.layoutOutput, self.output, color='#000000', - operation_data=True, data_op=[self.data1, self.data2], color_op=["#44AA99", "#CC6677"], - overlap_op=False) - self.updatePlot(self.graphData1, self.layoutData1, self.data1, color='#882255', - operation_data=True, data_op=[self.data1], color_op=["#44AA99"], overlap_op=True) - self.updatePlot(self.graphData2, self.layoutData2, self.data2, color='#332288' if isinstance(self.data2, Data1D) else '#CC6677', - operation_data=True if isinstance(self.data2, Data1D) else False, data_op=[self.data2], color_op=['#CC6677'], overlap_op=True) + self.updatePlot(self.graphOutput, self.layoutOutput, self.output, operation_data=True) + self.updatePlot(self.graphData1, self.layoutData1, self.data1, operation_data=True) + self.updatePlot(self.graphData2, self.layoutData2, self.data2, operation_data=True) # Add the new plot to the comboboxes # self.cbData1.addItem(self.output.name) @@ -237,7 +239,8 @@ def onSelectData1(self): key_id1 = self._findId(choice_data1) self.data1 = self._extractData(key_id1) # plot Data1 - self.updatePlot(self.graphData1, self.layoutData1, self.data1, color='#882255') + self.updatePlot(self.graphData1, self.layoutData1, self.data1) + # self.updatePlot(self.graphData1, self.layoutData1, self.data1, color='tab:blue') # plot default for output graph self.newPlot(self.graphOutput, self.layoutOutput) # Enable Compute button only if Data2 is defined and data compatible @@ -264,7 +267,8 @@ def onSelectData2(self): # Enable Compute button only if Data1 defined and compatible data self.cmdCompute.setEnabled(self.onCheckChosenData()) # Display value of coefficient in graphData2 - self.updatePlot(self.graphData2, self.layoutData2, self.data2, color='#CC6677') + self.updatePlot(self.graphData2, self.layoutData2, self.data2) + # self.updatePlot(self.graphData2, self.layoutData2, self.data2, color='tab:purple') # plot default for output graph self.newPlot(self.graphOutput, self.layoutOutput) self.onCheckChosenData() @@ -277,7 +281,8 @@ def onSelectData2(self): self.cmdCompute.setEnabled(self.onCheckChosenData()) # plot Data2 - self.updatePlot(self.graphData2, self.layoutData2, self.data2, color='#332288') + self.updatePlot(self.graphData2, self.layoutData2, self.data2) + # self.updatePlot(self.graphData2, self.layoutData2, self.data2, color='tab:red') # plot default for output graph self.newPlot(self.graphOutput, self.layoutOutput) @@ -301,7 +306,8 @@ def onInputCoefficient(self): else: self.txtNumber.setStyleSheet(BG_WHITE) self.data2 = float(self.txtNumber.text()) - self.updatePlot(self.graphData2, self.layoutData2, self.data2, color='#332288') + self.updatePlot(self.graphData2, self.layoutData2, self.data2) + # self.updatePlot(self.graphData2, self.layoutData2, self.data2, color='tab:red') def onCheckChosenData(self): """ check that data1 and data2 are compatible """ @@ -398,23 +404,46 @@ def newPlot(self, graph, layout): graph.setLayout(layout) - def addOperationData(self, plotter, data, data_op, color_op): + def operationData1D(self, operation_data, reference_data=None): - for d_op, c_op in zip(data_op, color_op): - if isinstance(d_op, Data1D): - op_data = Data1D(x=d_op._operation.x, y=d_op._operation.y, dy=d_op._operation.dy, dx=None) - op_data.copy_from_datainfo(data1d=d_op._operation) - plotter.plot(data=op_data, hide_error=True, marker='.', color=c_op) + """ + Create instance of PlotterData.Data1D from the operation data for plotting purposes. + """ + + if isinstance(operation_data, float): + new_operation_data = Data1D(2) + if isinstance(reference_data, Data1D): + new_operation_data.copy_from_datainfo(data1d=reference_data) + new_operation_data.x = np.array([reference_data.x.min(), reference_data.x.max()]) else: - op_data = Data1D(2) - op_data.copy_from_datainfo(data1d=data) - op_data.x = np.array([data.x.min(), data.x.max()]) - op_data.y = np.array([d_op, d_op]) - op_data.dy = np.zeros(2) - op_data.dx = np.zeros(2) - plotter.plot(data=op_data, hide_error=True, marker='-', color=c_op) - - def updatePlot(self, graph, layout, data, color=None, operation_data=False, data_op: Optional[list] = None, color_op: Optional[list] = None, overlap_op=True): + new_operation_data.x = np.array([1e-5, 1]) + new_operation_data.y = np.array([operation_data, operation_data]) + new_operation_data.dy = np.zeros(2) + new_operation_data.dx = np.zeros(2) + else: + try: + new_operation_data = Data1D(x=operation_data.x, y=operation_data.y, dy=operation_data.dy, dx=None) + new_operation_data.copy_from_datainfo(data1d=operation_data) + except: + new_operation_data = None + + return new_operation_data + + # for d_op, c_op in zip(data_op, color_op): + # if isinstance(d_op, Data1D): + # op_data = Data1D(x=d_op._operation.x, y=d_op._operation.y, dy=d_op._operation.dy, dx=None) + # op_data.copy_from_datainfo(data1d=d_op._operation) + # plotter.plot(data=op_data, hide_error=True, marker='.', color=c_op) + # else: + # op_data = Data1D(2) + # op_data.copy_from_datainfo(data1d=data) + # op_data.x = np.array([data.x.min(), data.x.max()]) + # op_data.y = np.array([d_op, d_op]) + # op_data.dy = np.zeros(2) + # op_data.dx = np.zeros(2) + # plotter.plot(data=op_data, hide_error=True, marker='-', color=c_op) + + def updatePlot(self, graph, layout, data, color=None, operation_data=False): """ plot data in graph after clearing its layout """ assert isinstance(graph, QtWidgets.QGraphicsView) @@ -456,13 +485,53 @@ def updatePlot(self, graph, layout, data, color=None, operation_data=False, data plotter.ax.tick_params(axis='x', labelsize=8) plotter.ax.tick_params(axis='y', labelsize=8) - # put the operation data below regular data - if operation_data is True and overlap_op is False: - self.addOperationData(plotter, data, data_op, color_op) - plotter.plot(data=data, hide_error=True, marker='.', color=color) - # put the operation data on top of regular data - if operation_data is True and overlap_op is True: - self.addOperationData(plotter, data, data_op, color_op) + # determine color based on the graph for consistency across all graphs + if color is None: + if graph.objectName() == 'graphData1': + color = DATA1_COLOR + elif graph.objectName() == 'graphData2': + color = DATA2_COLOR + elif graph.objectName() == 'graphOutput': + color = OUTPUT_COLOR + + # if operation data is available, outline the trim points of data1 and data2 + if operation_data and graph.objectName() != 'graphOutput': + markerfacecolor = TRIMMED_COLOR + markeredgecolor = color + alpha = TRIMMED_ALPHA + else: + markerfacecolor = None + markeredgecolor = None + alpha = None + + if graph.objectName() == 'graphOutput': + if operation_data: + plotter.plot(data=self.operationData1D(self.data1._operation, reference_data=self.data1), + hide_error=True, marker='o', color=DATA1_COLOR, + markerfacecolor=None, markeredgecolor=None) + if isinstance(self.data2, float): + operation_data = self.operationData1D(self.data2, + reference_data=self.data1 if isinstance(self.data1, + Data1D) else None) + plotter.plot(data=operation_data, hide_error=True, marker='-', color=DATA2_COLOR) + else: + operation_data = self.operationData1D(self.data2._operation, reference_data=self.data2) + plotter.plot(data=operation_data, + hide_error=True, marker='o', color=DATA2_COLOR, + markerfacecolor=None, markeredgecolor=None) + plotter.plot(data=data, hide_error=True, marker='o', color=color, markerfacecolor=markerfacecolor, + markeredgecolor=markeredgecolor) + else: + plotter.plot(data=data, hide_error=True, marker='o', color=color, markerfacecolor=markerfacecolor, + markeredgecolor=markeredgecolor, alpha=alpha) + if graph.objectName() == 'graphData1': + plotter.plot(data=self.operationData1D(data._operation, reference_data=data), + hide_error=True, marker='o', color=DATA1_COLOR, + markerfacecolor=None, markeredgecolor=None) + elif graph.objectName() == 'graphData2': + plotter.plot(data=self.operationData1D(data._operation, reference_data=data), + hide_error=True, marker='o', color=DATA2_COLOR, + markerfacecolor=None, markeredgecolor=None) plotter.show() @@ -477,14 +546,9 @@ def updatePlot(self, graph, layout, data, color=None, operation_data=False, data plotter.ax.tick_params(axis='x', labelsize=8) plotter.ax.tick_params(axis='y', labelsize=8) - op_data = Data1D(2) - op_data.copy_from_datainfo(data1d=self.data1) - op_data.scale = 'linear' - op_data.x = np.array([1e-5, 1]) - op_data.y = np.array([data, data]) - op_data.dy = np.zeros(2) - op_data.dx = np.zeros(2) - plotter.plot(data=op_data, hide_error=True, marker='-', color=color) + operation_data = self.operationData1D(data, + reference_data=self.data1 if isinstance(self.data1, Data1D) else None) + plotter.plot(data=operation_data, hide_error=True, marker='-', color=DATA2_COLOR) plotter.show() diff --git a/src/sas/qtgui/Plotting/Plotter.py b/src/sas/qtgui/Plotting/Plotter.py index 12266ab6ae..c86ea4ce38 100644 --- a/src/sas/qtgui/Plotting/Plotter.py +++ b/src/sas/qtgui/Plotting/Plotter.py @@ -95,7 +95,8 @@ def data(self, value): self.yscale = 'linear' self.title(title=value.name) - def plot(self, data=None, color=None, marker=None, hide_error=False, transform=True): + def plot(self, data=None, color=None, marker=None, hide_error=False, transform=True, markeredgecolor=None, + markerfacecolor=None, alpha=None): """ Add a new plot of self._data to the chart. """ @@ -170,12 +171,18 @@ def plot(self, data=None, color=None, marker=None, hide_error=False, transform=T if color is None: color = data.custom_color - # grid on/off, stored on self - ax.grid(self.grid_on) - color = PlotUtilities.getValidColor(color) data.custom_color = color + if markeredgecolor is None: + markeredgecolor = color + + if markerfacecolor is None: + markerfacecolor = color + + # grid on/off, stored on self + ax.grid(self.grid_on) + markersize = data.markersize # Include scaling (log vs. linear) @@ -204,8 +211,8 @@ def plot(self, data=None, color=None, marker=None, hide_error=False, transform=T else: # plot data with/without errorbars if hide_error: - line = ax.plot(x, y, marker=marker, color=color, markersize=markersize, - linestyle='', label=label, picker=True) + line = ax.plot(x, y, marker=marker, color=color, mfc=markerfacecolor, mec=markeredgecolor, + markersize=markersize, alpha=alpha, linestyle='', label=label, picker=True) else: dy = data.view.dy # Convert tuple (lo,hi) to array [(x-lo),(hi-x)] @@ -218,6 +225,9 @@ def plot(self, data=None, color=None, marker=None, hide_error=False, transform=T capsize=2, linestyle='', barsabove=False, color=color, + mfc=makerfacecolor, + mec=markeredgecolor, + alpha=alpha, marker=marker, markersize=markersize, lolims=False, uplims=False, diff --git a/src/sas/qtgui/Plotting/PlotterData.py b/src/sas/qtgui/Plotting/PlotterData.py index 8cbcc58b13..ef53b81e92 100644 --- a/src/sas/qtgui/Plotting/PlotterData.py +++ b/src/sas/qtgui/Plotting/PlotterData.py @@ -106,8 +106,12 @@ def _perform_operation(self, other, operation): """ """ # Check for compatibility of the x-ranges and populate the data used for the operation + # sets up _operation for both datasets # interpolation will be implemented on the 'other' dataset as needed - self._interpolation_operation(other) + if self.isSesans: + self._interpolation_operation(other, scale='linear') + else: + self._interpolation_operation(other, scale='log') result = Data1D(x=[], y=[], dx=None, dy=None) result.clone_without_data(length=self._operation.x.size, clone=self) From 65e63e1f1250c2ed424311a9048fcfacc75918a8 Mon Sep 17 00:00:00 2001 From: Caitlyn Wolf Date: Wed, 31 Jan 2024 12:21:43 -0500 Subject: [PATCH 10/14] adding loggin message to indicate data operation was completed --- src/sas/qtgui/Calculators/DataOperationUtilityPanel.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py b/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py index d8100ab3bf..3eadce0660 100644 --- a/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py +++ b/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py @@ -151,6 +151,7 @@ def onCompute(self): self.updatePlot(self.graphOutput, self.layoutOutput, self.output, operation_data=True) self.updatePlot(self.graphData1, self.layoutData1, self.data1, operation_data=True) self.updatePlot(self.graphData2, self.layoutData2, self.data2, operation_data=True) + logging.info(f"Data operation complete.") # Add the new plot to the comboboxes # self.cbData1.addItem(self.output.name) From bdd88a01bc4dd34f5deb802b0087555768749df7 Mon Sep 17 00:00:00 2001 From: Caitlyn Wolf Date: Wed, 31 Jan 2024 15:57:42 -0500 Subject: [PATCH 11/14] added checks for empty datasets and graphs during onSave and also cleared data1, data2, and output where needed upon changes to operation request --- .../Calculators/DataOperationUtilityPanel.py | 83 ++++++++++++------- .../Calculators/UI/DataOperationUtilityUI.ui | 3 +- src/sas/qtgui/Plotting/Plotter.py | 2 +- 3 files changed, 53 insertions(+), 35 deletions(-) diff --git a/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py b/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py index 3eadce0660..4ccc8ca200 100644 --- a/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py +++ b/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py @@ -124,7 +124,6 @@ def onHelp(self): def onClose(self): """ Close dialog """ self.onReset() - self.cbData1.clear() self.cbData1.addItems(['No Data Available']) self.cbData2.clear() @@ -138,6 +137,8 @@ def onCompute(self): operator = self.cbOperator.currentText() # calculate and send data to DataExplorer output = None + if self.data1 is None or self.data2 is None: + logging.warning("Please set both Data1 and Data2 to complete operation.") try: data1 = self.data1 data2 = self.data2 @@ -151,32 +152,26 @@ def onCompute(self): self.updatePlot(self.graphOutput, self.layoutOutput, self.output, operation_data=True) self.updatePlot(self.graphData1, self.layoutData1, self.data1, operation_data=True) self.updatePlot(self.graphData2, self.layoutData2, self.data2, operation_data=True) - logging.info(f"Data operation complete.") - - # Add the new plot to the comboboxes - # self.cbData1.addItem(self.output.name) - # self.cbData2.addItem(self.output.name) - if self.filenames is None: - self.filenames = {} - self.filenames[self.output.name] = self.output + logging.info("Data operation complete.") def onSave(self): """ send to data explorer """ - - # if outputname was unused, write output result to it + # if output name was unused, write output result to it # and display plot - if self.onCheckOutputName(): + if self.onCheckOutputName() and self.output is not None: # add outputname to self.filenames self.list_data_items.append(str(self.txtOutputData.text())) # send result to DataExplorer self.onPrepareOutputData() - # Add the new plot to the comboboxes - self.cbData1.addItem(self.output.name) - self.cbData2.addItem(self.output.name) - if self.filenames is None: - self.filenames = {} - self.filenames[self.output.name] = self.output + # Add the new plot to the comboboxes + self.cbData1.addItem(self.output.name) + self.cbData2.addItem(self.output.name) + if self.filenames is None: + self.filenames = {} + self.filenames[self.output.name] = self.output + elif self.output is None: + logging.warning("No output data to save.") def onPrepareOutputData(self): """ Prepare datasets to be added to DataExplorer and DataManager """ @@ -195,7 +190,7 @@ def onPrepareOutputData(self): def onSelectOperator(self): """ Change GUI when operator changed """ self.lblOperatorApplied.setText(self.cbOperator.currentText()) - self.newPlot(self.graphOutput, self.layoutOutput) + self.resetOutput() def onReset(self): """ @@ -203,7 +198,9 @@ def onReset(self): the names of loaded data """ self.txtNumber.setText('1.0') - self.txtOutputData.setText('MyNewDataName') + + # sets new default name for output data that doesn't already exist + self.txtOutputData.setText(self.uniqueOutputName()) self.txtNumber.setEnabled(False) self.cmdCompute.setEnabled(False) @@ -215,8 +212,8 @@ def onReset(self): self.data1OK = False self.data2OK = False - # Empty graphs - self.newPlot(self.graphOutput, self.layoutOutput) + self.resetOutput() + # Empty graphs and self.newPlot(self.graphData1, self.layoutData1) self.newPlot(self.graphData2, self.layoutData2) @@ -241,9 +238,6 @@ def onSelectData1(self): self.data1 = self._extractData(key_id1) # plot Data1 self.updatePlot(self.graphData1, self.layoutData1, self.data1) - # self.updatePlot(self.graphData1, self.layoutData1, self.data1, color='tab:blue') - # plot default for output graph - self.newPlot(self.graphOutput, self.layoutOutput) # Enable Compute button only if Data2 is defined and data compatible self.cmdCompute.setEnabled(self.onCheckChosenData()) @@ -254,11 +248,11 @@ def onSelectData2(self): if choice_data2 in wrong_choices: self.newPlot(self.graphData2, self.layoutData2) + self.data2 = None self.txtNumber.setEnabled(False) self.data2OK = False self.onCheckChosenData() self.cmdCompute.setEnabled(False) - return elif choice_data2 == 'Number': self.data2OK = True @@ -269,9 +263,7 @@ def onSelectData2(self): self.cmdCompute.setEnabled(self.onCheckChosenData()) # Display value of coefficient in graphData2 self.updatePlot(self.graphData2, self.layoutData2, self.data2) - # self.updatePlot(self.graphData2, self.layoutData2, self.data2, color='tab:purple') - # plot default for output graph - self.newPlot(self.graphOutput, self.layoutOutput) + self.resetOutput() self.onCheckChosenData() else: @@ -283,9 +275,17 @@ def onSelectData2(self): # plot Data2 self.updatePlot(self.graphData2, self.layoutData2, self.data2) - # self.updatePlot(self.graphData2, self.layoutData2, self.data2, color='tab:red') - # plot default for output graph - self.newPlot(self.graphOutput, self.layoutOutput) + self.resetOutput() + + # show interpolation warning when a 1D dataset is chosen for Data2 + if isinstance(self.data2, Data1D): + self.cautionStatement.setText( + "CAUTION: interpolation of Data2 will occur for 1D-datasets if x-axis points\n" + "are not close. This could introduce artifacts. Please see documentation." + ) + else: + self.cautionStatement.setText("") + def onInputCoefficient(self): """ Check input of number when a coefficient is required @@ -365,6 +365,25 @@ def onCheckOutputName(self): self.txtOutputData.setStyleSheet(BG_WHITE) return True + def uniqueOutputName(self): + """Gets the next unique output name if previous outputs have been saved with default name.""" + output_name = "MyNewDataName1" + i = 1 + while output_name in self.list_data_items: + i += 1 + output_name = f"MyNewDataName{str(i)}" + self.txtOutputData.setText(output_name) + return output_name + + def resetOutput(self): + """Resets the output data and output graph upon any change to Data1, Data2, or operator.""" + # plot default for output graph + self.newPlot(self.graphOutput, self.layoutOutput) + # reset the output until onCompute is called + self.output = None + # sets new default name for output data that doesn't already exist + self.txtOutputData.setText(self.uniqueOutputName()) + # ######## # Modification of inputs # ######## diff --git a/src/sas/qtgui/Calculators/UI/DataOperationUtilityUI.ui b/src/sas/qtgui/Calculators/UI/DataOperationUtilityUI.ui index 4d00278ce9..790d592e32 100644 --- a/src/sas/qtgui/Calculators/UI/DataOperationUtilityUI.ui +++ b/src/sas/qtgui/Calculators/UI/DataOperationUtilityUI.ui @@ -399,8 +399,7 @@ Append(Combine): | - CAUTION: interpolation of Data2 will occur for 1D-datasets if x-axis points - are not close. This could introduce artifacts. Please see documentation. + diff --git a/src/sas/qtgui/Plotting/Plotter.py b/src/sas/qtgui/Plotting/Plotter.py index c86ea4ce38..a57c2f2d32 100644 --- a/src/sas/qtgui/Plotting/Plotter.py +++ b/src/sas/qtgui/Plotting/Plotter.py @@ -225,7 +225,7 @@ def plot(self, data=None, color=None, marker=None, hide_error=False, transform=T capsize=2, linestyle='', barsabove=False, color=color, - mfc=makerfacecolor, + mfc=markerfacecolor, mec=markeredgecolor, alpha=alpha, marker=marker, From 74c455a2234552f757a8975c4573f4fa8d69541c Mon Sep 17 00:00:00 2001 From: Caitlyn Wolf Date: Wed, 31 Jan 2024 16:21:04 -0500 Subject: [PATCH 12/14] cleaning up code included extra spaces and commented sections --- .../Calculators/DataOperationUtilityPanel.py | 46 +++++-------------- 1 file changed, 12 insertions(+), 34 deletions(-) diff --git a/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py b/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py index 4ccc8ca200..163f2a8688 100644 --- a/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py +++ b/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py @@ -7,7 +7,6 @@ from PySide6 import QtGui from PySide6 import QtWidgets import numpy as np -from typing import Optional from sas.qtgui.Plotting.PlotterData import Data1D from sas.qtgui.Plotting.Plotter import PlotterWidget @@ -27,6 +26,7 @@ TRIMMED_COLOR = '#FFFFFF' # white TRIMMED_ALPHA = 0.3 # semi-transparent points trimmed for operation + class DataOperationUtilityPanel(QtWidgets.QDialog, Ui_DataOperationUtility): def __init__(self, parent=None): super(DataOperationUtilityPanel, self).__init__() @@ -130,13 +130,11 @@ def onClose(self): self.cbData2.addItems(['No Data Available']) self.close() - def onCompute(self): """ perform calculation - don't send to data explorer""" # set operator to be applied operator = self.cbOperator.currentText() # calculate and send data to DataExplorer - output = None if self.data1 is None or self.data2 is None: logging.warning("Please set both Data1 and Data2 to complete operation.") try: @@ -182,8 +180,7 @@ def onPrepareOutputData(self): self.output, name=name) - new_datalist_item = {name + str(time.time()): - self.output} + new_datalist_item = {name + str(time.time()): self.output} self.communicator. \ updateModelFromDataOperationPanelSignal.emit(new_item, new_datalist_item) @@ -228,7 +225,7 @@ def onSelectData1(self): self.newPlot(self.graphData1, self.layoutData1) self.data1 = None self.data1OK = False - self.cmdCompute.setEnabled(False) # self.onCheckChosenData()) + self.cmdCompute.setEnabled(False) # self.onCheckChosenData()) return else: @@ -286,7 +283,6 @@ def onSelectData2(self): else: self.cautionStatement.setText("") - def onInputCoefficient(self): """ Check input of number when a coefficient is required for operation """ @@ -330,12 +326,12 @@ def onCheckChosenData(self): elif self.data1.__class__.__name__ == 'Data2D' \ and (len(self.data2.qx_data) != len(self.data1.qx_data) \ - or len(self.data2.qy_data) != len(self.data1.qy_data) - or not all(i == j for i, j in - zip(self.data1.qx_data, self.data2.qx_data)) - or not all(i == j for i, j in - zip(self.data1.qy_data, self.data2.qy_data)) - ): + or len(self.data2.qy_data) != len(self.data1.qy_data) + or not all(i == j for i, j in + zip(self.data1.qx_data, self.data2.qx_data)) + or not all(i == j for i, j in + zip(self.data1.qy_data, self.data2.qy_data)) + ): self.cbData1.setStyleSheet(BG_RED) self.cbData2.setStyleSheet(BG_RED) logging.error('Cannot compute 2D data of different lengths') @@ -441,28 +437,10 @@ def operationData1D(self, operation_data, reference_data=None): new_operation_data.dy = np.zeros(2) new_operation_data.dx = np.zeros(2) else: - try: - new_operation_data = Data1D(x=operation_data.x, y=operation_data.y, dy=operation_data.dy, dx=None) - new_operation_data.copy_from_datainfo(data1d=operation_data) - except: - new_operation_data = None - + new_operation_data = Data1D(x=operation_data.x, y=operation_data.y, dy=operation_data.dy, dx=None) + new_operation_data.copy_from_datainfo(data1d=operation_data) return new_operation_data - # for d_op, c_op in zip(data_op, color_op): - # if isinstance(d_op, Data1D): - # op_data = Data1D(x=d_op._operation.x, y=d_op._operation.y, dy=d_op._operation.dy, dx=None) - # op_data.copy_from_datainfo(data1d=d_op._operation) - # plotter.plot(data=op_data, hide_error=True, marker='.', color=c_op) - # else: - # op_data = Data1D(2) - # op_data.copy_from_datainfo(data1d=data) - # op_data.x = np.array([data.x.min(), data.x.max()]) - # op_data.y = np.array([d_op, d_op]) - # op_data.dy = np.zeros(2) - # op_data.dx = np.zeros(2) - # plotter.plot(data=op_data, hide_error=True, marker='-', color=c_op) - def updatePlot(self, graph, layout, data, color=None, operation_data=False): """ plot data in graph after clearing its layout """ @@ -540,7 +518,7 @@ def updatePlot(self, graph, layout, data, color=None, operation_data=False): hide_error=True, marker='o', color=DATA2_COLOR, markerfacecolor=None, markeredgecolor=None) plotter.plot(data=data, hide_error=True, marker='o', color=color, markerfacecolor=markerfacecolor, - markeredgecolor=markeredgecolor) + markeredgecolor=markeredgecolor) else: plotter.plot(data=data, hide_error=True, marker='o', color=color, markerfacecolor=markerfacecolor, markeredgecolor=markeredgecolor, alpha=alpha) From 2c091fa44ca73aa6e8333b3341a93c158ceeef32 Mon Sep 17 00:00:00 2001 From: Caitlyn Wolf Date: Tue, 2 Apr 2024 18:31:14 -0400 Subject: [PATCH 13/14] fixed bug dropping isSesans attribute in Data1D object when it was loaded in the explorer and passed to the data operation panel --- src/sas/qtgui/Plotting/PlotterData.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/sas/qtgui/Plotting/PlotterData.py b/src/sas/qtgui/Plotting/PlotterData.py index ef53b81e92..17dc1e2bc8 100644 --- a/src/sas/qtgui/Plotting/PlotterData.py +++ b/src/sas/qtgui/Plotting/PlotterData.py @@ -34,7 +34,7 @@ class DataRole(Enum): class Data1D(PlottableData1D, LoadData1D): """ """ - def __init__(self, x=None, y=None, dx=None, dy=None): + def __init__(self, x=None, y=None, dx=None, dy=None, isSesans=False): """ """ if x is None: @@ -42,7 +42,7 @@ def __init__(self, x=None, y=None, dx=None, dy=None): if y is None: y = [] PlottableData1D.__init__(self, x, y, dx, dy) - LoadData1D.__init__(self, x, y, dx, dy) + LoadData1D.__init__(self, x, y, dx, dy, isSesans=isSesans) self.id = None self.list_group_id = [] self.group_id = None @@ -93,6 +93,9 @@ def copy_from_datainfo(self, data1d): self.yaxis(data1d._yaxis, data1d._yunit) self.title = data1d.title self.isSesans = data1d.isSesans + if self.isSesans: # the data is SESANS so update the x and y units + self.x_unit = 'A' + self.y_unit = 'pol' def __str__(self): """ From 907b7575f36157e204ae6e051397baec5f4c5e5f Mon Sep 17 00:00:00 2001 From: Caitlyn Wolf Date: Tue, 2 Apr 2024 18:33:57 -0400 Subject: [PATCH 14/14] enabled automatic update of data comboboxes when new data is loaded in the explorer and updated the reset button to a clear button --- .../Calculators/DataOperationUtilityPanel.py | 52 ++++++++++--------- .../Calculators/UI/DataOperationUtilityUI.ui | 7 ++- src/sas/qtgui/MainWindow/GuiManager.py | 9 ++++ 3 files changed, 42 insertions(+), 26 deletions(-) diff --git a/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py b/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py index 163f2a8688..b003d4f19f 100644 --- a/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py +++ b/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py @@ -59,7 +59,7 @@ def __init__(self, parent=None): self.cmdHelp.clicked.connect(self.onHelp) self.cmdSave.clicked.connect(self.onSave) self.cmdCompute.clicked.connect(self.onCompute) - self.cmdReset.clicked.connect(self.onReset) + self.cmdClear.clicked.connect(self.onClear) self.cmdCompute.setEnabled(False) @@ -82,15 +82,11 @@ def __init__(self, parent=None): def updateCombobox(self, filenames): """ Function to fill comboboxes with names of datafiles loaded in DataExplorer. For Data2, there is the additional option of choosing - a number to apply to data1 """ + a number to apply to data1 + """ self.filenames = filenames if list(filenames.keys()): - # clear contents of comboboxes - self.cbData1.clear() - self.cbData1.addItems(['Select Data']) - self.cbData2.clear() - self.cbData2.addItems(['Select Data', 'Number']) list_datafiles = [] @@ -98,12 +94,13 @@ def updateCombobox(self, filenames): if filenames[key_id].name: # filenames with titles new_title = filenames[key_id].name - list_datafiles.append(new_title) - self.list_data_items.append(new_title) else: # filenames without titles by removing time.time() new_title = re.sub(r'\d{10}\.\d{2}', '', str(key_id)) + + # only append new data files added to the explorer + if new_title not in self.list_data_items: self.list_data_items.append(new_title) list_datafiles.append(new_title) @@ -123,7 +120,7 @@ def onHelp(self): def onClose(self): """ Close dialog """ - self.onReset() + self.onClear() self.cbData1.clear() self.cbData1.addItems(['No Data Available']) self.cbData2.clear() @@ -153,18 +150,15 @@ def onCompute(self): logging.info("Data operation complete.") def onSave(self): - """ send to data explorer """ + """ send to data explorer which will automatically update comboboxes""" # if output name was unused, write output result to it # and display plot if self.onCheckOutputName() and self.output is not None: - # add outputname to self.filenames - self.list_data_items.append(str(self.txtOutputData.text())) + # # add outputname to self.filenames + # self.list_data_items.append(str(self.txtOutputData.text())) # send result to DataExplorer self.onPrepareOutputData() - # Add the new plot to the comboboxes - self.cbData1.addItem(self.output.name) - self.cbData2.addItem(self.output.name) if self.filenames is None: self.filenames = {} self.filenames[self.output.name] = self.output @@ -189,10 +183,10 @@ def onSelectOperator(self): self.lblOperatorApplied.setText(self.cbOperator.currentText()) self.resetOutput() - def onReset(self): + def onClear(self): """ - Reset Panel to its initial state (default values) keeping - the names of loaded data + Clear and reset panel to its initial state (default values). + This will clear any current data selections, operator selections, and the graphs. """ self.txtNumber.setText('1.0') @@ -202,8 +196,12 @@ def onReset(self): self.txtNumber.setEnabled(False) self.cmdCompute.setEnabled(False) + # changing the index back to default will also set self.data1/self.data2 back to None self.cbData1.setCurrentIndex(0) self.cbData2.setCurrentIndex(0) + # switch back to white in case previous step resulted in failing the data check + self.cbData1.setStyleSheet(BG_WHITE) + self.cbData2.setStyleSheet(BG_WHITE) self.cbOperator.setCurrentIndex(0) self.data1OK = False @@ -226,7 +224,7 @@ def onSelectData1(self): self.data1 = None self.data1OK = False self.cmdCompute.setEnabled(False) # self.onCheckChosenData()) - return + self.resetOutput() else: self.data1OK = True @@ -237,6 +235,7 @@ def onSelectData1(self): self.updatePlot(self.graphData1, self.layoutData1, self.data1) # Enable Compute button only if Data2 is defined and data compatible self.cmdCompute.setEnabled(self.onCheckChosenData()) + self.resetOutput() def onSelectData2(self): """ Plot for selection of Data2 """ @@ -250,6 +249,7 @@ def onSelectData2(self): self.data2OK = False self.onCheckChosenData() self.cmdCompute.setEnabled(False) + self.resetOutput() elif choice_data2 == 'Number': self.data2OK = True @@ -261,7 +261,6 @@ def onSelectData2(self): # Display value of coefficient in graphData2 self.updatePlot(self.graphData2, self.layoutData2, self.data2) self.resetOutput() - self.onCheckChosenData() else: self.txtNumber.setEnabled(False) @@ -320,10 +319,15 @@ def onCheckChosenData(self): elif self.data1.__class__.__name__ != self.data2.__class__.__name__: self.cbData1.setStyleSheet(BG_RED) self.cbData2.setStyleSheet(BG_RED) - print(self.data1.__class__.__name__ != self.data2.__class__.__name__) logging.error('Cannot compute data of different dimensions') return False + elif self.data1.x_unit != self.data2.x_unit: + self.cbData1.setStyleSheet(BG_RED) + self.cbData2.setStyleSheet(BG_RED) + logging.error('Cannot compute data on data with different x-units.') + return False + elif self.data1.__class__.__name__ == 'Data2D' \ and (len(self.data2.qx_data) != len(self.data1.qx_data) \ or len(self.data2.qy_data) != len(self.data1.qy_data) @@ -522,11 +526,11 @@ def updatePlot(self, graph, layout, data, color=None, operation_data=False): else: plotter.plot(data=data, hide_error=True, marker='o', color=color, markerfacecolor=markerfacecolor, markeredgecolor=markeredgecolor, alpha=alpha) - if graph.objectName() == 'graphData1': + if graph.objectName() == 'graphData1' and operation_data: plotter.plot(data=self.operationData1D(data._operation, reference_data=data), hide_error=True, marker='o', color=DATA1_COLOR, markerfacecolor=None, markeredgecolor=None) - elif graph.objectName() == 'graphData2': + elif graph.objectName() == 'graphData2' and operation_data: plotter.plot(data=self.operationData1D(data._operation, reference_data=data), hide_error=True, marker='o', color=DATA2_COLOR, markerfacecolor=None, markeredgecolor=None) diff --git a/src/sas/qtgui/Calculators/UI/DataOperationUtilityUI.ui b/src/sas/qtgui/Calculators/UI/DataOperationUtilityUI.ui index 790d592e32..4375269bdb 100644 --- a/src/sas/qtgui/Calculators/UI/DataOperationUtilityUI.ui +++ b/src/sas/qtgui/Calculators/UI/DataOperationUtilityUI.ui @@ -417,15 +417,18 @@ Append(Combine): | - + 75 25 + + Clear data selections and graphs in the Data Operations panel. + - Reset + Clear false diff --git a/src/sas/qtgui/MainWindow/GuiManager.py b/src/sas/qtgui/MainWindow/GuiManager.py index 5d738a9e5f..c8f50d1c42 100644 --- a/src/sas/qtgui/MainWindow/GuiManager.py +++ b/src/sas/qtgui/MainWindow/GuiManager.py @@ -1007,6 +1007,8 @@ def actionData_Operation(self): self.communicate.sendDataToPanelSignal.emit(dict(data, **theory)) self.DataOperation.show() + # automatically add any new loaded data to the data operation combo boxes + self.filesWidget.model.rowsInserted.connect(self.updateDataOperationComboboxFromModel) def actionSLD_Calculator(self): """ @@ -1331,6 +1333,13 @@ def updateModelFromDataOperationPanel(self, new_item, new_datalist_item): self.filesWidget.model.appendRow(new_item) self._data_manager.add_data(new_datalist_item) + def updateDataOperationComboboxFromModel(self): + """ + Update the Data Operation panel combo boxes with new data + """ + data, theory = self.filesWidget.getAllFlatData() + self.DataOperation.updateCombobox(dict(data, **theory)) + def showPlotFromName(self, name): """ Pass the show plot request to the data explorer