Source code for sas.qtgui.Perspectives.Invariant.InvariantPerspective

# global
import logging
import copy
import numpy as np

from PyQt5 import QtCore
from PyQt5 import QtGui, QtWidgets

from twisted.internet import threads
from twisted.internet import reactor

# sas-global
from sas.sascalc.invariant import invariant
from sas.qtgui.Plotting.PlotterData import Data1D
import sas.qtgui.Utilities.GuiUtils as GuiUtils

# local
from .UI.TabbedInvariantUI import Ui_tabbedInvariantUI
from .InvariantDetails import DetailsDialog
from .InvariantUtils import WIDGETS

# The minimum q-value to be used when extrapolating
Q_MINIMUM = 1e-5
# The maximum q-value to be used when extrapolating
Q_MAXIMUM = 10
# the ratio of maximum q value/(qmax of data) to plot the theory data
Q_MAXIMUM_PLOT = 3
# Default number of points of interpolation: high and low range
NPOINTS_Q_INTERP = 10
# Default power law for interpolation
DEFAULT_POWER_LOW = 4

# Background of line edits if settings OK or wrong
BG_WHITE = "background-color: rgb(255, 255, 255);"
BG_RED = "background-color: rgb(244, 170, 164);"


[docs]class InvariantWindow(QtWidgets.QDialog, Ui_tabbedInvariantUI): # The controller which is responsible for managing signal slots connections # for the gui and providing an interface to the data model. name = "Invariant" # For displaying in the combo box in DataExplorer ext = 'inv' # File extension used for saving analyses def __init__(self, parent=None): super(InvariantWindow, self).__init__() self.setupUi(self) self.setWindowTitle(self.title()) # initial input params self._background = 0.0 self._scale = 1.0 self._contrast = 8.0e-6 self._porod = None self.parent = parent self._manager = parent self._reactor = reactor self._model_item = QtGui.QStandardItem() self.detailsDialog = DetailsDialog(self) self.detailsDialog.cmdOK.clicked.connect(self.enabling) self._low_extrapolate = False self._low_guinier = True self._low_fit = False self._low_points = NPOINTS_Q_INTERP self._low_power_value = DEFAULT_POWER_LOW self._high_extrapolate = False self._high_fit = False self._high_points = NPOINTS_Q_INTERP self._high_power_value = DEFAULT_POWER_LOW # Define plots self.high_extrapolation_plot = None self.low_extrapolation_plot = None # no reason to have this widget resizable self.resize(self.minimumSizeHint()) self.communicate = self._manager.communicator() self.communicate.dataDeletedSignal.connect(self.removeData) self._data = None self._path = "" self._calculator = None self._allow_close = False # Modify font in order to display Angstrom symbol correctly new_font = 'font-family: -apple-system, "Helvetica Neue", "Ubuntu";' self.lblTotalQUnits.setStyleSheet(new_font) self.lblSpecificSurfaceUnits.setStyleSheet(new_font) self.lblInvariantTotalQUnits.setStyleSheet(new_font) self.lblContrastUnits.setStyleSheet(new_font) self.lblPorodCstUnits.setStyleSheet(new_font) self.lblExtrapolQUnits.setStyleSheet(new_font) # To remove blue square around line edits self.txtBackgd.setAttribute(QtCore.Qt.WA_MacShowFocusRect, False) self.txtContrast.setAttribute(QtCore.Qt.WA_MacShowFocusRect, False) self.txtScale.setAttribute(QtCore.Qt.WA_MacShowFocusRect, False) self.txtPorodCst.setAttribute(QtCore.Qt.WA_MacShowFocusRect, False) self.txtNptsHighQ.setAttribute(QtCore.Qt.WA_MacShowFocusRect, False) self.txtNptsLowQ.setAttribute(QtCore.Qt.WA_MacShowFocusRect, False) self.txtPowerLowQ.setAttribute(QtCore.Qt.WA_MacShowFocusRect, False) self.txtPowerHighQ.setAttribute(QtCore.Qt.WA_MacShowFocusRect, False) # Let's choose the Standard Item Model. self.model = QtGui.QStandardItemModel(self) # Connect buttons to slots. # Needs to be done early so default values propagate properly. self.setupSlots() # Set up the model. self.setupModel() # Set up the mapper self.setupMapper() # Default enablement self.cmdCalculate.setEnabled(False) # validator: double self.txtExtrapolQMin.setValidator(GuiUtils.DoubleValidator()) self.txtExtrapolQMax.setValidator(GuiUtils.DoubleValidator()) self.txtBackgd.setValidator(GuiUtils.DoubleValidator()) self.txtContrast.setValidator(GuiUtils.DoubleValidator()) self.txtScale.setValidator(GuiUtils.DoubleValidator()) self.txtPorodCst.setValidator(GuiUtils.DoubleValidator()) # validator: integer number self.txtNptsLowQ.setValidator(QtGui.QIntValidator()) self.txtNptsHighQ.setValidator(QtGui.QIntValidator()) self.txtPowerLowQ.setValidator(GuiUtils.DoubleValidator()) self.txtPowerHighQ.setValidator(GuiUtils.DoubleValidator()) self.mapper.toFirst()
[docs] def get_low_q_extrapolation_upper_limit(self): q_value = self._data.x[int(self.txtNptsLowQ.text()) - 1] return q_value
[docs] def set_low_q_extrapolation_upper_limit(self, value): n_pts = (np.abs(self._data.x - value)).argmin() + 1 item = QtGui.QStandardItem(str(n_pts)) self.model.setItem(WIDGETS.W_NPTS_LOWQ, item)
[docs] def get_high_q_extrapolation_lower_limit(self): q_value = self._data.x[len(self._data.x) - int(self.txtNptsHighQ.text()) - 1] return q_value
[docs] def set_high_q_extrapolation_lower_limit(self, value): n_pts = (np.abs(self._data.x - value)).argmin() + 1 item = QtGui.QStandardItem(str(int(n_pts))) self.model.setItem(WIDGETS.W_NPTS_HIGHQ, item)
[docs] def enabling(self): """ """ self.cmdStatus.setEnabled(True)
[docs] def setClosable(self, value=True): """ Allow outsiders close this widget """ assert isinstance(value, bool) self._allow_close = value
[docs] def isSerializable(self): """ Tell the caller that this perspective writes its state """ return True
[docs] def closeEvent(self, event): """ Overwrite QDialog close method to allow for custom widget close """ if self._allow_close: # reset the closability flag self.setClosable(value=False) # Tell the MdiArea to close the container if it is visible if self.parentWidget(): self.parentWidget().close() event.accept() else: event.ignore() # Maybe we should just minimize self.setWindowState(QtCore.Qt.WindowMinimized)
[docs] def updateFromModel(self): """ Update the globals based on the data in the model """ self._background = float(self.model.item(WIDGETS.W_BACKGROUND).text()) self._contrast = float(self.model.item(WIDGETS.W_CONTRAST).text()) self._scale = float(self.model.item(WIDGETS.W_SCALE).text()) if self.model.item(WIDGETS.W_POROD_CST).text() != 'None' and self.model.item(WIDGETS.W_POROD_CST).text() != '': self._porod = float(self.model.item(WIDGETS.W_POROD_CST).text()) # Low extrapolating self._low_extrapolate = str(self.model.item(WIDGETS.W_ENABLE_LOWQ).text()) == 'true' self._low_points = float(self.model.item(WIDGETS.W_NPTS_LOWQ).text()) self._low_guinier = str(self.model.item(WIDGETS.W_LOWQ_GUINIER).text()) == 'true' self._low_fit = str(self.model.item(WIDGETS.W_LOWQ_FIT).text()) == 'true' self._low_power_value = float(self.model.item(WIDGETS.W_LOWQ_POWER_VALUE).text()) # High extrapolating self._high_extrapolate = str(self.model.item(WIDGETS.W_ENABLE_HIGHQ).text()) == 'true' self._high_points = float(self.model.item(WIDGETS.W_NPTS_HIGHQ).text()) self._high_fit = str(self.model.item(WIDGETS.W_HIGHQ_FIT).text()) == 'true' self._high_power_value = float(self.model.item(WIDGETS.W_HIGHQ_POWER_VALUE).text())
[docs] def calculateInvariant(self): """ Use twisted to thread the calculations away """ # Find out if extrapolation needs to be used. extrapolation = None if self._low_extrapolate and self._high_extrapolate: extrapolation = "both" elif self._high_extrapolate: extrapolation = "high" elif self._low_extrapolate: extrapolation = "low" # modify the Calculate button to indicate background process self.cmdCalculate.setText("Calculating...") self.cmdCalculate.setEnabled(False) # Send the calculations to separate thread. d = threads.deferToThread(self.calculateThread, extrapolation) # Add deferred callback for call return d.addCallback(self.deferredPlot) d.addErrback(self.calculationFailed)
[docs] def calculationFailed(self, reason): print("calculation failed: ", reason) self.allow_calculation()
[docs] def deferredPlot(self, model): """ Run the GUI/model update in the main thread """ reactor.callFromThread(lambda: self.plotResult(model)) self.allow_calculation()
[docs] def allow_calculation(self): # Set the calculate button to available self.cmdCalculate.setEnabled(True) self.cmdCalculate.setText("Calculate")
[docs] def plotResult(self, model): """ Plot result of calculation """ self.model = model self._data = GuiUtils.dataFromItem(self._model_item) # Send the modified model item to DE for keeping in the model plots = [self._model_item] if self.high_extrapolation_plot: self.high_extrapolation_plot.plot_role = Data1D.ROLE_DEFAULT self.high_extrapolation_plot.symbol = "Line" self.high_extrapolation_plot.show_errors = False # TODO: Fix the link between npts and q and then enable the q-range sliders #self.high_extrapolation_plot.show_q_range_sliders = True #self.high_extrapolation_plot.slider_update_on_move = False #self.high_extrapolation_plot.slider_low_q_input = self.txtNptsHighQ #self.high_extrapolation_plot.slider_low_q_setter = self.set_high_q_extrapolation_lower_limit #self.high_extrapolation_plot.slider_low_q_getter = self.get_high_q_extrapolation_lower_limit #self.high_extrapolation_plot.slider_high_q_input = self.txtExtrapolQMax GuiUtils.updateModelItemWithPlot(self._model_item, self.high_extrapolation_plot, self.high_extrapolation_plot.title) plots.append(self.high_extrapolation_plot) if self.low_extrapolation_plot: self.low_extrapolation_plot.plot_role = Data1D.ROLE_DEFAULT self.low_extrapolation_plot.symbol = "Line" self.low_extrapolation_plot.show_errors = False # TODO: Fix the link between npts and q and then enable the q-range sliders #self.low_extrapolation_plot.show_q_range_sliders = True #self.low_extrapolation_plot.slider_update_on_move = False #self.low_extrapolation_plot.slider_high_q_input = self.txtNptsLowQ #self.low_extrapolation_plot.slider_high_q_setter = self.set_low_q_extrapolation_upper_limit #self.low_extrapolation_plot.slider_high_q_getter = self.get_low_q_extrapolation_upper_limit #self.low_extrapolation_plot.slider_low_q_input = self.txtExtrapolQMin GuiUtils.updateModelItemWithPlot(self._model_item, self.low_extrapolation_plot, self.low_extrapolation_plot.title) plots.append(self.low_extrapolation_plot) if len(plots) > 1: self.communicate.plotRequestedSignal.emit(plots, None) # Update the details dialog in case it is open self.updateDetailsWidget(model)
[docs] def updateDetailsWidget(self, model): """ On demand update of the details widget """ if self.detailsDialog.isVisible(): self.onStatus()
[docs] def calculateThread(self, extrapolation): """ Perform Invariant calculations. """ # Get most recent values from GUI and model self.updateFromModel() # Define base message msg = '' # Set base Q* values to 0.0 qstar_low = 0.0 qstar_low_err = 0.0 qstar_high = 0.0 qstar_high_err = 0.0 temp_data = copy.deepcopy(self._data) calculation_failed = False low_calculation_pass = False high_calculation_pass = False # Update calculator with background, scale, and data values self._calculator.background = self._background self._calculator.scale = self._scale self._calculator.set_data(temp_data) # Low Q extrapolation calculations if self._low_extrapolate: function_low = "power_law" if self._low_guinier: function_low = "guinier" if self._low_fit: self._low_power_value = None self._calculator.set_extrapolation( range="low", npts=int(self._low_points), function=function_low, power=self._low_power_value) try: qmin_ext = float(self.txtExtrapolQMin.text()) qmin = None if qmin_ext > self._data.x[0] else qmin_ext qstar_low, qstar_low_err = self._calculator.get_qstar_low(qmin) low_calculation_pass = True except Exception as ex: logging.warning('Low-q calculation failed: {}'.format(str(ex))) qstar_low = "ERROR" qstar_low_err = "ERROR" if self.low_extrapolation_plot and not low_calculation_pass: # Remove the existing extrapolation plot model_items = GuiUtils.getChildrenFromItem(self._model_item) for item in model_items: if item.text() == self.low_extrapolation_plot.title: reactor.callFromThread(self._manager.filesWidget.closePlotsForItem, item) reactor.callFromThread(self._model_item.removeRow, item.row()) break self.low_extrapolation_plot = None reactor.callFromThread(self.updateModelFromThread, WIDGETS.D_LOW_QSTAR, qstar_low) reactor.callFromThread(self.updateModelFromThread, WIDGETS.D_LOW_QSTAR_ERR, qstar_low_err) # High Q Extrapolation calculations if self._high_extrapolate: function_high = "power_law" if self._high_fit: self._high_power_value = None self._calculator.set_extrapolation( range="high", npts=int(self._high_points), function=function_high, power=self._high_power_value) try: qmax_ext = float(self.txtExtrapolQMax.text()) qmax = None if qmax_ext < self._data.x[int(len(self._data.x) - 1)] else qmax_ext qstar_high, qstar_high_err = self._calculator.get_qstar_high(qmax) high_calculation_pass = True except Exception as ex: logging.warning('High-q calculation failed: {}'.format(str(ex))) qstar_high = "ERROR" qstar_high_err = "ERROR" if self.high_extrapolation_plot and not high_calculation_pass: # Remove the existing extrapolation plot model_items = GuiUtils.getChildrenFromItem(self._model_item) for item in model_items: if item.text() == self.high_extrapolation_plot.title: reactor.callFromThread(self._manager.filesWidget.closePlotsForItem, item) reactor.callFromThread(self._model_item.removeRow, item.row()) break self.high_extrapolation_plot = None reactor.callFromThread(self.updateModelFromThread, WIDGETS.D_HIGH_QSTAR, qstar_high) reactor.callFromThread(self.updateModelFromThread, WIDGETS.D_HIGH_QSTAR_ERR, qstar_high_err) # Q* Data calculations try: qstar_data, qstar_data_err = self._calculator.get_qstar_with_error() except Exception as ex: msg += str(ex) calculation_failed = True qstar_data = "ERROR" qstar_data_err = "ERROR" reactor.callFromThread(self.updateModelFromThread, WIDGETS.D_DATA_QSTAR, qstar_data) reactor.callFromThread(self.updateModelFromThread, WIDGETS.D_DATA_QSTAR_ERR, qstar_data_err) # Volume Fraction calculations try: volume_fraction, volume_fraction_error = self._calculator.get_volume_fraction_with_error( self._contrast, extrapolation=extrapolation) except Exception as ex: calculation_failed = True msg += str(ex) volume_fraction = "ERROR" volume_fraction_error = "ERROR" reactor.callFromThread(self.updateModelFromThread, WIDGETS.W_VOLUME_FRACTION, volume_fraction) reactor.callFromThread(self.updateModelFromThread, WIDGETS.W_VOLUME_FRACTION_ERR, volume_fraction_error) # Surface Error calculations if self._porod: try: surface, surface_error = self._calculator.get_surface_with_error(self._contrast, self._porod) except Exception as ex: calculation_failed = True msg += str(ex) surface = "ERROR" surface_error = "ERROR" reactor.callFromThread(self.updateModelFromThread, WIDGETS.W_SPECIFIC_SURFACE, surface) reactor.callFromThread(self.updateModelFromThread, WIDGETS.W_SPECIFIC_SURFACE_ERR, surface_error) # Enable the status button self.cmdStatus.setEnabled(True) # Early exit if calculations failed if calculation_failed: self.cmdStatus.setEnabled(False) logging.warning('Calculation failed: {}'.format(msg)) return self.model if low_calculation_pass: extrapolated_data = self._calculator.get_extra_data_low( self._low_points, q_start=float(self.txtExtrapolQMin.text())) power_low = self._calculator.get_extrapolation_power(range='low') # Plot the chart title = f"Low-Q extrapolation [{self._data.name}]" # Convert the data into plottable self.low_extrapolation_plot = self._manager.createGuiData(extrapolated_data) self.low_extrapolation_plot.name = title self.low_extrapolation_plot.title = title self.low_extrapolation_plot.symbol = "Line" self.low_extrapolation_plot.has_errors = False # copy labels and units of axes for plotting self.low_extrapolation_plot._xaxis = temp_data._xaxis self.low_extrapolation_plot._xunit = temp_data._xunit self.low_extrapolation_plot._yaxis = temp_data._yaxis self.low_extrapolation_plot._yunit = temp_data._yunit if self._low_fit: reactor.callFromThread(self.updateModelFromThread, WIDGETS.W_LOWQ_POWER_VALUE, power_low) if high_calculation_pass: # for presentation in InvariantDetails qmax_plot = Q_MAXIMUM_PLOT * max(temp_data.x) qmax_input = float(self.txtExtrapolQMax.text()) qmax_plot = qmax_input if qmax_plot > qmax_input else qmax_plot power_high = self._calculator.get_extrapolation_power(range='high') high_out_data = self._calculator.get_extra_data_high(q_end=qmax_plot, npts=500) # Plot the chart title = f"High-Q extrapolation [{self._data.name}]" # Convert the data into plottable self.high_extrapolation_plot = self._manager.createGuiData(high_out_data) self.high_extrapolation_plot.name = title self.high_extrapolation_plot.title = title self.high_extrapolation_plot.symbol = "Line" self.high_extrapolation_plot.has_errors = False # copy labels and units of axes for plotting self.high_extrapolation_plot._xaxis = temp_data._xaxis self.high_extrapolation_plot._xunit = temp_data._xunit self.high_extrapolation_plot._yaxis = temp_data._yaxis self.high_extrapolation_plot._yunit = temp_data._yunit if self._high_fit: reactor.callFromThread(self.updateModelFromThread, WIDGETS.W_HIGHQ_POWER_VALUE, power_high) if qstar_high == "ERROR": qstar_high = 0.0 qstar_high_err = 0.0 if qstar_low == "ERROR": qstar_low = 0.0 qstar_low_err = 0.0 qstar_total = qstar_data + qstar_low + qstar_high qstar_total_error = np.sqrt( qstar_data_err * qstar_data_err + qstar_low_err * qstar_low_err + qstar_high_err * qstar_high_err) reactor.callFromThread(self.updateModelFromThread, WIDGETS.W_INVARIANT, qstar_total) reactor.callFromThread(self.updateModelFromThread, WIDGETS.W_INVARIANT_ERR, qstar_total_error) return self.model
[docs] def updateModelFromThread(self, widget, value): """ Update the model in the main thread """ try: value = float('%.3g' % value) except TypeError: pass item = QtGui.QStandardItem(str(value)) self.model.setItem(widget, item) self.mapper.toLast()
[docs] def title(self): """ Perspective name """ return "Invariant Perspective"
[docs] def onStatus(self): """ Display Invariant Details panel when clicking on Status button """ self.detailsDialog.setModel(self.model) self.detailsDialog.showDialog() self.cmdStatus.setEnabled(False)
[docs] def onHelp(self): """ Display help when clicking on Help button """ treeLocation = "/user/qtgui/Perspectives/Invariant/invariant_help.html" self.parent.showHelp(treeLocation)
[docs] def setupSlots(self): """ """ self.cmdCalculate.clicked.connect(self.calculateInvariant) self.cmdStatus.clicked.connect(self.onStatus) self.cmdHelp.clicked.connect(self.onHelp) self.chkLowQ.stateChanged.connect(self.stateChanged) self.chkLowQ.stateChanged.connect(self.checkQExtrapolatedData) self.chkHighQ.stateChanged.connect(self.stateChanged) self.chkHighQ.stateChanged.connect(self.checkQExtrapolatedData) # slots for the Guinier and PowerLaw radio buttons at low Q # since they are not auto-exclusive self.rbGuinier.toggled.connect(self.lowGuinierAndPowerToggle) self.rbPowerLawLowQ.toggled.connect(self.lowGuinierAndPowerToggle) self.rbFitHighQ.toggled.connect(self.hiFitAndFixToggle) self.rbFitLowQ.toggled.connect(self.lowFitAndFixToggle) self.model.itemChanged.connect(self.modelChanged) # update model from gui editing by users self.txtBackgd.textChanged.connect(self.updateFromGui) self.txtScale.textChanged.connect(self.updateFromGui) self.txtContrast.textChanged.connect(self.updateFromGui) self.txtPorodCst.textChanged.connect(self.updateFromGui) self.txtPowerLowQ.textChanged.connect(self.updateFromGui) self.txtPowerHighQ.textChanged.connect(self.updateFromGui) self.txtNptsLowQ.textChanged.connect(self.updateFromGui) self.txtNptsHighQ.textChanged.connect(self.updateFromGui) # check values of n_points compared to distribution length self.txtNptsLowQ.textChanged.connect(self.checkLength) self.txtNptsHighQ.textChanged.connect(self.checkLength) self.txtExtrapolQMin.textChanged.connect(self.checkQMinRange) self.txtExtrapolQMax.textChanged.connect(self.checkQMaxRange) self.txtNptsLowQ.textChanged.connect(self.checkQRange) self.txtNptsHighQ.textChanged.connect(self.checkQRange)
[docs] def stateChanged(self): """ Catch modifications from low- and high-Q extrapolation check boxes """ sender = self.sender() itemf = QtGui.QStandardItem(str(sender.isChecked()).lower()) if sender.text() == 'Enable Low-Q extrapolation': self.model.setItem(WIDGETS.W_ENABLE_LOWQ, itemf) if sender.text() == 'Enable High-Q extrapolation': self.model.setItem(WIDGETS.W_ENABLE_HIGHQ, itemf)
[docs] def checkLength(self): """ Validators of number of points for extrapolation. Error if it is larger than the distribution length """ self.cmdCalculate.setEnabled(False) try: int_value = int(self.sender().text()) except ValueError: self.sender().setStyleSheet(BG_RED) return if self._data: if len(self._data.x) < int_value: self.sender().setStyleSheet(BG_RED) logging.warning('The number of points must be smaller than {}'.format(len(self._data.x))) else: self.sender().setStyleSheet(BG_WHITE) self.allow_calculation()
[docs] def modelChanged(self, item): """ Update when model changed """ if item.row() == WIDGETS.W_ENABLE_LOWQ: toggle = (str(item.text()) == 'true') self._low_extrapolate = toggle self.lowQToggle(toggle) elif item.row() == WIDGETS.W_ENABLE_HIGHQ: toggle = (str(item.text()) == 'true') self._high_extrapolate = toggle self.highQToggle(toggle)
[docs] def checkQExtrapolatedData(self): """ Match status of low or high-Q extrapolated data checkbox in DataExplorer with low or high-Q extrapolation checkbox in invariant panel """ # name to search in DataExplorer if 'Low' in str(self.sender().text()): name = "Low-Q extrapolation" if 'High' in str(self.sender().text()): name = "High-Q extrapolation" GuiUtils.updateModelItemStatus(self._manager.filesWidget.model, self._path, name, self.sender().checkState())
[docs] def checkQMaxRange(self): item = QtGui.QStandardItem(self.txtExtrapolQMax.text()) self.model.setItem(WIDGETS.W_EX_QMAX, item) self.checkQRange()
[docs] def checkQMinRange(self): item = QtGui.QStandardItem(self.txtExtrapolQMin.text()) self.model.setItem(WIDGETS.W_EX_QMIN, item) self.checkQRange()
[docs] def checkQRange(self): """ Validate the Q range for the upper and lower bounds Valid: q_low_max < q_high_min, q_low_min < q_low_max, q_high_min > q_low_max, q_high_max > q_high_min """ q_high_min = q_low_min = np.inf q_high_max = q_low_max = -1 * np.inf try: # Set high extrapolation lower bound to infinity if no data, or if number points undefined q_high_min = self._data.x[len(self._data.x) - int(self.txtNptsHighQ.text())] except (ValueError, AttributeError, IndexError): # No data, number of points too large/small, or unable to convert number of points to int pass except Exception as e: logging.error(f"{e}") try: # Set high extrapolation upper bound to negative infinity if Q max input empty q_high_max = float(self.txtExtrapolQMax.text()) except ValueError: # Couldn't convert Q min for extrapolation to a float pass except Exception as e: logging.error(f"{e}") try: # Set low extrapolation lower bound to infinity if Q min input empty q_low_min = float(self.txtExtrapolQMin.text()) except ValueError: # Couldn't convert Q min for extrapolation to a float pass except Exception as e: logging.error(f"{e}") try: # Set high extrapolation lower bound to infinity if no data, or if number points undefined q_low_max = self._data.x[int(self.txtNptsLowQ.text())] except (ValueError, AttributeError, IndexError): # No data, number of points too large/small, or unable to convert number of points to int pass except Exception as e: logging.error(f"{e}") calculate = ((q_low_min < q_low_max) and (q_high_min < q_high_max) and (q_high_min > q_low_max) and self.txtExtrapolQMax.text() and self.txtExtrapolQMin.text() and self.txtNptsLowQ.text() and self.txtNptsHighQ.text()) self.txtExtrapolQMin.setStyleSheet(BG_RED if q_low_min >= q_low_max and q_low_max < q_high_min else BG_WHITE) self.txtExtrapolQMax.setStyleSheet(BG_RED if q_high_min >= q_high_max and q_low_max < q_high_min else BG_WHITE) if calculate: self.allow_calculation() else: self.cmdCalculate.setEnabled(False)
[docs] def updateFromGui(self): """ Update model when new user inputs """ possible_senders = ['txtBackgd', 'txtContrast', 'txtPorodCst', 'txtScale', 'txtPowerLowQ', 'txtPowerHighQ', 'txtNptsLowQ', 'txtNptsHighQ'] related_widgets = [WIDGETS.W_BACKGROUND, WIDGETS.W_CONTRAST, WIDGETS.W_POROD_CST, WIDGETS.W_SCALE, WIDGETS.W_LOWQ_POWER_VALUE, WIDGETS.W_HIGHQ_POWER_VALUE, WIDGETS.W_NPTS_LOWQ, WIDGETS.W_NPTS_HIGHQ] related_internal_values = [self._background, self._contrast, self._porod, self._scale, self._low_power_value, self._high_power_value, self._low_points, self._high_points] item = QtGui.QStandardItem(self.sender().text()) index_elt = possible_senders.index(self.sender().objectName()) self.model.setItem(related_widgets[index_elt], item) try: related_internal_values[index_elt] = float(self.sender().text()) self.sender().setStyleSheet(BG_WHITE) self.allow_calculation() except ValueError: # empty field, just skip self.sender().setStyleSheet(BG_RED) self.cmdCalculate.setEnabled(False)
[docs] def lowGuinierAndPowerToggle(self, toggle): """ Guinier and Power radio buttons cannot be selected at the same time If Power is selected, Fit and Fix radio buttons are visible and Power line edit can be edited if Fix is selected """ if self.sender().text() == 'Guinier': itemt = QtGui.QStandardItem(str(toggle).lower()) self.model.setItem(WIDGETS.W_LOWQ_GUINIER, itemt) toggle = not toggle self.rbPowerLawLowQ.setChecked(toggle) else: self.rbGuinier.setChecked(not toggle) itemt = QtGui.QStandardItem(str(not toggle).lower()) self.model.setItem(WIDGETS.W_LOWQ_GUINIER, itemt) self.rbFitLowQ.setVisible(toggle) self.rbFixLowQ.setVisible(toggle) self.txtPowerLowQ.setEnabled(toggle and (not self._low_fit)) self.updateFromModel()
[docs] def lowFitAndFixToggle(self, toggle): """ Fit and Fix radiobuttons cannot be selected at the same time """ itemt = QtGui.QStandardItem(str(toggle).lower()) self.model.setItem(WIDGETS.W_LOWQ_FIT, itemt) self.txtPowerLowQ.setEnabled(not toggle) self.updateFromModel()
[docs] def hiFitAndFixToggle(self, toggle): """ Enable editing of power exponent if Fix for high Q is checked Disable otherwise """ itemt = QtGui.QStandardItem(str(toggle).lower()) self.model.setItem(WIDGETS.W_HIGHQ_FIT, itemt) self.txtPowerHighQ.setEnabled(not toggle) self.updateFromModel()
[docs] def highQToggle(self, clicked): """ Disable/enable High Q extrapolation """ self.rbFitHighQ.setEnabled(clicked) self.rbFixHighQ.setEnabled(clicked) self.txtNptsHighQ.setEnabled(clicked) self.txtPowerHighQ.setEnabled(clicked and not self._high_fit)
[docs] def lowQToggle(self, clicked): """ Disable / enable Low Q extrapolation """ self.rbGuinier.setEnabled(clicked) self.rbPowerLawLowQ.setEnabled(clicked) self.txtNptsLowQ.setEnabled(clicked) # Enable subelements self.rbFitLowQ.setVisible(self.rbPowerLawLowQ.isChecked()) self.rbFixLowQ.setVisible(self.rbPowerLawLowQ.isChecked()) self.rbFitLowQ.setEnabled(clicked) # and not self._low_guinier) self.rbFixLowQ.setEnabled(clicked) # and not self._low_guinier) self.txtPowerLowQ.setEnabled(clicked and not self._low_guinier and not self._low_fit)
[docs] def setupModel(self): """ """ # filename item = QtGui.QStandardItem(self._path) self.model.setItem(WIDGETS.W_NAME, item) # add Q parameters to the model qmin = 0.0 item = QtGui.QStandardItem(str(qmin)) self.model.setItem(WIDGETS.W_QMIN, item) qmax = 0.0 item = QtGui.QStandardItem(str(qmax)) self.model.setItem(WIDGETS.W_QMAX, item) # add extrapolated Q parameters to the model item = QtGui.QStandardItem(str(Q_MINIMUM)) self.model.setItem(WIDGETS.W_EX_QMIN, item) item = QtGui.QStandardItem(str(Q_MAXIMUM)) self.model.setItem(WIDGETS.W_EX_QMAX, item) # add custom input params item = QtGui.QStandardItem(str(self._background)) self.model.setItem(WIDGETS.W_BACKGROUND, item) item = QtGui.QStandardItem(str(self._contrast)) self.model.setItem(WIDGETS.W_CONTRAST, item) item = QtGui.QStandardItem(str(self._scale)) self.model.setItem(WIDGETS.W_SCALE, item) # leave line edit empty if Porod constant not defined if self._porod != None: item = QtGui.QStandardItem(str(self._porod)) else: item = QtGui.QStandardItem(str('')) self.model.setItem(WIDGETS.W_POROD_CST, item) # Dialog elements itemf = QtGui.QStandardItem("false") self.model.setItem(WIDGETS.W_ENABLE_HIGHQ, itemf) itemf = QtGui.QStandardItem("false") self.model.setItem(WIDGETS.W_ENABLE_LOWQ, itemf) item = QtGui.QStandardItem(str(NPOINTS_Q_INTERP)) self.model.setItem(WIDGETS.W_NPTS_LOWQ, item) item = QtGui.QStandardItem(str(NPOINTS_Q_INTERP)) self.model.setItem(WIDGETS.W_NPTS_HIGHQ, item) itemt = QtGui.QStandardItem("true") self.model.setItem(WIDGETS.W_LOWQ_GUINIER, itemt) itemt = QtGui.QStandardItem("true") self.model.setItem(WIDGETS.W_LOWQ_FIT, itemt) item = QtGui.QStandardItem(str(DEFAULT_POWER_LOW)) self.model.setItem(WIDGETS.W_LOWQ_POWER_VALUE, item) itemt = QtGui.QStandardItem("true") self.model.setItem(WIDGETS.W_HIGHQ_FIT, itemt) item = QtGui.QStandardItem(str(DEFAULT_POWER_LOW)) self.model.setItem(WIDGETS.W_HIGHQ_POWER_VALUE, item)
[docs] def setupMapper(self): # Set up the mapper. self.mapper = QtWidgets.QDataWidgetMapper(self) self.mapper.setOrientation(QtCore.Qt.Vertical) self.mapper.setModel(self.model) # Filename self.mapper.addMapping(self.txtName, WIDGETS.W_NAME) # Qmin/Qmax self.mapper.addMapping(self.txtTotalQMin, WIDGETS.W_QMIN) self.mapper.addMapping(self.txtTotalQMax, WIDGETS.W_QMAX) # Extrapolated Qmin/Qmax self.mapper.addMapping(self.txtExtrapolQMin, WIDGETS.W_EX_QMIN) self.mapper.addMapping(self.txtExtrapolQMax, WIDGETS.W_EX_QMAX) # Background self.mapper.addMapping(self.txtBackgd, WIDGETS.W_BACKGROUND) # Scale self.mapper.addMapping(self.txtScale, WIDGETS.W_SCALE) # Contrast self.mapper.addMapping(self.txtContrast, WIDGETS.W_CONTRAST) # Porod constant self.mapper.addMapping(self.txtPorodCst, WIDGETS.W_POROD_CST) # Lowq/Highq items self.mapper.addMapping(self.chkLowQ, WIDGETS.W_ENABLE_LOWQ) self.mapper.addMapping(self.chkHighQ, WIDGETS.W_ENABLE_HIGHQ) self.mapper.addMapping(self.txtNptsLowQ, WIDGETS.W_NPTS_LOWQ) self.mapper.addMapping(self.rbGuinier, WIDGETS.W_LOWQ_GUINIER) self.mapper.addMapping(self.rbFitLowQ, WIDGETS.W_LOWQ_FIT) self.mapper.addMapping(self.txtPowerLowQ, WIDGETS.W_LOWQ_POWER_VALUE) self.mapper.addMapping(self.txtNptsHighQ, WIDGETS.W_NPTS_HIGHQ) self.mapper.addMapping(self.rbFitHighQ, WIDGETS.W_HIGHQ_FIT) self.mapper.addMapping(self.txtPowerHighQ, WIDGETS.W_HIGHQ_POWER_VALUE) # Output self.mapper.addMapping(self.txtVolFract, WIDGETS.W_VOLUME_FRACTION) self.mapper.addMapping(self.txtVolFractErr, WIDGETS.W_VOLUME_FRACTION_ERR) self.mapper.addMapping(self.txtSpecSurf, WIDGETS.W_SPECIFIC_SURFACE) self.mapper.addMapping(self.txtSpecSurfErr, WIDGETS.W_SPECIFIC_SURFACE_ERR) self.mapper.addMapping(self.txtInvariantTot, WIDGETS.W_INVARIANT) self.mapper.addMapping(self.txtInvariantTotErr, WIDGETS.W_INVARIANT_ERR) self.mapper.toFirst()
[docs] def setData(self, data_item=None, is_batch=False): """ Obtain a QStandardItem object and dissect it to get Data1D/2D Pass it over to the calculator """ assert data_item is not None if self.txtName.text() == data_item[0].text(): logging.info('This file is already loaded in Invariant panel.') return if not isinstance(data_item, list): msg = "Incorrect type passed to the Invariant Perspective" raise AttributeError(msg) if not isinstance(data_item[0], QtGui.QStandardItem): msg = "Incorrect type passed to the Invariant Perspective" raise AttributeError(msg) # only 1 file can be loaded self._model_item = data_item[0] # Reset plots on data change self.low_extrapolation_plot = None self.high_extrapolation_plot = None # Extract data on 1st child - this is the Data1D/2D component data = GuiUtils.dataFromItem(self._model_item) self.model.item(WIDGETS.W_NAME).setData(self._model_item.text()) # update GUI and model with info from loaded data self.updateGuiFromFile(data=data)
[docs] def removeData(self, data_list=None): """Remove the existing data reference from the Invariant Persepective""" if not data_list or self._model_item not in data_list: return self._data = None self._model_item = None self.low_extrapolation_plot = None self.high_extrapolation_plot = None self._path = "" self.txtName.setText('') self._porod = None # Pass an empty dictionary to set all inputs to their default values self.updateFromParameters({}) # Disable buttons to return to base state self.cmdCalculate.setEnabled(False) self.cmdStatus.setEnabled(False)
[docs] def updateGuiFromFile(self, data=None): """ update display in GUI and plot """ self._data = data # plot loaded file if not isinstance(self._data, Data1D): msg = "Invariant cannot be computed with 2D data." raise ValueError(msg) try: name = data.name except: msg = 'No data name chosen.' raise ValueError(msg) try: qmin = min(self._data.x) qmax = max(self._data.x) except: msg = "Unable to find q min/max of \n data named %s" % \ data.name raise ValueError(msg) # update model with input form files: name, qmin, qmax self.model.item(WIDGETS.W_NAME).setText(name) self.model.item(WIDGETS.W_QMIN).setText(str(qmin)) self.model.item(WIDGETS.W_QMAX).setText(str(qmax)) self._path = data.filename self._calculator = invariant.InvariantCalculator( data=self._data, background=self._background, scale=self._scale) # Ensure extrapolated number of points and Q range are valid on data load self.txtNptsLowQ.setText(self.txtNptsLowQ.text()) self.txtNptsHighQ.setText(self.txtNptsHighQ.text()) self.checkQRange() # Calculate and add to GUI: volume fraction, invariant total, # and specific surface if porod checked if self.cmdCalculate.isEnabled(): self.calculateInvariant()
[docs] def serializeAll(self): """ Serialize the invariant state so data can be saved Invariant is not batch-ready so this will only effect a single page :return: {data-id: {self.name: {invariant-state}}} """ return self.serializeCurrentPage()
[docs] def serializeCurrentPage(self): """ Serialize and return a dictionary of {data_id: invariant-state} Return empty dictionary if no data :return: {data-id: {self.name: {invariant-state}}} """ state = {} if self._data: tab_data = self.getPage() data_id = tab_data.pop('data_id', '') state[data_id] = {'invar_params': tab_data} return state
[docs] def getPage(self): """ Serializes full state of this invariant page Called by Save Analysis :return: {invariant-state} """ # Get all parameters from page param_dict = self.getState() if self._data: param_dict['data_name'] = str(self._data.name) param_dict['data_id'] = str(self._data.id) return param_dict
[docs] def getState(self): """ Collects all active params into a dictionary of {name: value} :return: {name: value} """ # Be sure model has been updated self.updateFromModel() return { 'extrapolated_q_min': self.txtExtrapolQMin.text(), 'extrapolated_q_max': self.txtExtrapolQMax.text(), 'vol_fraction': self.txtVolFract.text(), 'vol_fraction_err': self.txtVolFractErr.text(), 'specific_surface': self.txtSpecSurf.text(), 'specific_surface_err': self.txtSpecSurfErr.text(), 'invariant_total': self.txtInvariantTot.text(), 'invariant_total_err': self.txtInvariantTotErr.text(), 'background': self.txtBackgd.text(), 'contrast': self.txtContrast.text(), 'scale': self.txtScale.text(), 'porod': self.txtPorodCst.text(), 'low_extrapolate': self.chkLowQ.isChecked(), 'low_points': self.txtNptsLowQ.text(), 'low_guinier': self.rbGuinier.isChecked(), 'low_fit_rb': self.rbFitLowQ.isChecked(), 'low_power_value': self.txtPowerLowQ.text(), 'high_extrapolate': self.chkHighQ.isChecked(), 'high_points': self.txtNptsHighQ.text(), 'high_fit_rb': self.rbFitHighQ.isChecked(), 'high_power_value': self.txtPowerHighQ.text(), 'total_q_min': self.txtTotalQMin.text(), 'total_q_max': self.txtTotalQMax.text(), }
[docs] def updateFromParameters(self, params): """ Called by Open Project and Open Analysis :param params: {param_name: value} :return: None """ # Params should be a dictionary if not isinstance(params, dict): c_name = params.__class__.__name__ msg = "Invariant.updateFromParameters expects a dictionary" raise TypeError(f"{msg}: {c_name} received") # Assign values to 'Invariant' tab inputs - use defaults if not found self.txtTotalQMin.setText(str(params.get('total_q_min', '0.0'))) self.txtTotalQMax.setText(str(params.get('total_q_max', '0.0'))) self.txtExtrapolQMax.setText(str(params.get('extrapolated_q_max', Q_MAXIMUM))) self.txtExtrapolQMin.setText(str(params.get('extrapolated_q_min', Q_MINIMUM))) self.txtVolFract.setText(str(params.get('vol_fraction', ''))) self.txtVolFractErr.setText(str(params.get('vol_fraction_err', ''))) self.txtSpecSurf.setText(str(params.get('specific_surface', ''))) self.txtSpecSurfErr.setText(str(params.get('specific_surface_err', ''))) self.txtInvariantTot.setText(str(params.get('invariant_total', ''))) self.txtInvariantTotErr.setText( str(params.get('invariant_total_err', ''))) # Assign values to 'Options' tab inputs - use defaults if not found self.txtBackgd.setText(str(params.get('background', '0.0'))) self.txtScale.setText(str(params.get('scale', '1.0'))) self.txtContrast.setText(str(params.get('contrast', '8e-06'))) self.txtPorodCst.setText(str(params.get('porod', '0.0'))) # Toggle extrapolation buttons to enable other inputs self.chkLowQ.setChecked(params.get('low_extrapolate', False)) self.chkHighQ.setChecked(params.get('high_extrapolate', False)) self.txtPowerLowQ.setText( str(params.get('low_power_value', DEFAULT_POWER_LOW))) self.txtNptsLowQ.setText( str(params.get('low_points', NPOINTS_Q_INTERP))) self.rbGuinier.setChecked(params.get('low_guinier', True)) self.rbFitLowQ.setChecked(params.get('low_fit_rb', False)) self.txtNptsHighQ.setText( str(params.get('high_points', NPOINTS_Q_INTERP))) self.rbFitHighQ.setChecked(params.get('high_fit_rb', True)) self.txtPowerHighQ.setText( str(params.get('high_power_value', DEFAULT_POWER_LOW))) # Update once all inputs are changed self.updateFromModel() self.plotResult(self.model)
[docs] def allowBatch(self): """ Tell the caller that we don't accept multiple data instances """ return False
[docs] def allowSwap(self): """ Tell the caller that we can't swap data """ return False