diff --git a/src/sas/logger_config.py b/src/sas/logger_config.py index 8a182c2b43..03eb004dc9 100644 --- a/src/sas/logger_config.py +++ b/src/sas/logger_config.py @@ -31,6 +31,7 @@ def config_production(self): logging.captureWarnings(True) logger = logging.getLogger(self.name) logging.getLogger('matplotlib').setLevel(logging.WARN) + logging.getLogger('numba').setLevel(logging.WARN) return logger def config_development(self): @@ -42,6 +43,7 @@ def config_development(self): logging.captureWarnings(True) self._disable_debug_from_config() logging.getLogger('matplotlib').setLevel(logging.WARN) + logging.getLogger('numba').setLevel(logging.WARN) return logger def _disable_debug_from_config(self): diff --git a/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py b/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py index e866dd0fb6..d7938718a9 100644 --- a/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py +++ b/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py @@ -160,6 +160,7 @@ def onPrepareOutputData(self): """ Prepare datasets to be added to DataExplorer and DataManager """ name = self.txtOutputData.text() self.output.name = name + self.output.id = name + str(time.time()) new_item = GuiUtils.createModelItemWithPlot( self.output, name=name) @@ -410,7 +411,6 @@ def updatePlot(self, graph, layout, data): # plot 2D data plotter2D = Plotter2DWidget(self, quickplot=True) plotter2D.scale = 'linear' - plotter2D.ax.tick_params(axis='x', labelsize=8) plotter2D.ax.tick_params(axis='y', labelsize=8) diff --git a/src/sas/qtgui/Calculators/UI/SldPanel.ui b/src/sas/qtgui/Calculators/UI/SldPanel.ui index e847c6af92..8686ad55ad 100644 --- a/src/sas/qtgui/Calculators/UI/SldPanel.ui +++ b/src/sas/qtgui/Calculators/UI/SldPanel.ui @@ -6,8 +6,8 @@ 0 0 - 490 - 446 + 552 + 495 @@ -308,10 +308,16 @@ + + + 0 + 0 + + 466 - 32 + 50 @@ -320,6 +326,12 @@ true + + + 0 + 28 + + Recalculate @@ -340,6 +352,12 @@ + + + 0 + 28 + + Close @@ -347,6 +365,12 @@ + + + 0 + 28 + + Help diff --git a/src/sas/qtgui/MainWindow/DataExplorer.py b/src/sas/qtgui/MainWindow/DataExplorer.py index 632c55d941..e4cff872cf 100644 --- a/src/sas/qtgui/MainWindow/DataExplorer.py +++ b/src/sas/qtgui/MainWindow/DataExplorer.py @@ -118,6 +118,9 @@ def __init__(self, parent=None, guimanager=None, manager=None): self.cbgraph.editTextChanged.connect(self.enableGraphCombo) self.cbgraph.currentIndexChanged.connect(self.enableGraphCombo) + self.cbgraph_2.editTextChanged.connect(self.enableGraphCombo) + self.cbgraph_2.currentIndexChanged.connect(self.enableGraphCombo) + # Proxy model for showing a subset of Data1D/Data2D content self.data_proxy = QtCore.QSortFilterProxyModel(self) self.data_proxy.setSourceModel(self.model) @@ -696,6 +699,10 @@ def deleteFile(self, event): # Delete corresponding open plots self.closePlotsForItem(item) + # Close result panel if results represent the deleted data item + # Results panel only stores Data1D/Data2D object + # => QStandardItems must still exist for direct comparison + self.closeResultPanelOnDelete(GuiUtils.dataFromItem(item)) self.model.removeRow(ind) # Decrement index since we just deleted it @@ -940,9 +947,12 @@ def updatePlotName(self, name_tuple): Modify the name of the current plot """ old_name, current_name = name_tuple - ind = self.cbgraph.findText(old_name) - self.cbgraph.setCurrentIndex(ind) - self.cbgraph.setItemText(ind, current_name) + graph = self.cbgraph + if self.current_view == self.freezeView: + graph = self.cbgraph_2 + ind = graph.findText(old_name) + graph.setCurrentIndex(ind) + graph.setItemText(ind, current_name) def add_data(self, data_list): """ @@ -972,12 +982,15 @@ def updateGraphCombo(self, graph_list): """ Modify Graph combo box on graph add/delete """ - orig_text = self.cbgraph.currentText() - self.cbgraph.clear() - self.cbgraph.insertItems(0, graph_list) - ind = self.cbgraph.findText(orig_text) + graph = self.cbgraph + if self.current_view == self.freezeView: + graph = self.cbgraph_2 + orig_text = graph.currentText() + graph.clear() + graph.insertItems(0, graph_list) + ind = graph.findText(orig_text) if ind > 0: - self.cbgraph.setCurrentIndex(ind) + graph.setCurrentIndex(ind) def updatePerspectiveCombo(self, index): """ @@ -1207,11 +1220,13 @@ def appendPlot(self): # new plot data; check which tab is currently active if self.current_view == self.treeView: new_plots = GuiUtils.plotsFromCheckedItems(self.model) + graph = self.cbgraph else: new_plots = GuiUtils.plotsFromCheckedItems(self.theory_model) + graph = self.cbgraph_2 # old plot data - plot_id = str(self.cbgraph.currentText()) + plot_id = str(graph.currentText()) try: assert plot_id in PlotHelper.currentPlots(), "No such plot: %s" % (plot_id) except: @@ -1883,7 +1898,14 @@ def closePlotsForItem(self, item): pass # debugger anchor - def onAnalysisUpdate(self, new_perspective=""): + def closeResultPanelOnDelete(self, data): + """ + Given a data1d/2d object, close the fitting results panel if currently populated with the data + """ + # data - Single data1d/2d object to be deleted + self.parent.results_panel.onDataDeleted(data) + + def onAnalysisUpdate(self, new_perspective_name: str): """ Update the perspective combo index based on passed string """ diff --git a/src/sas/qtgui/MainWindow/GuiManager.py b/src/sas/qtgui/MainWindow/GuiManager.py index dd93bd494d..76ad357bb9 100644 --- a/src/sas/qtgui/MainWindow/GuiManager.py +++ b/src/sas/qtgui/MainWindow/GuiManager.py @@ -123,9 +123,18 @@ def addWidgets(self): """ Populate the main window with widgets """ + # Add the console window as another docked widget + self.logDockWidget = QDockWidget("Log Explorer", self._workspace) + self.logDockWidget.setObjectName("LogDockWidget") + self.logDockWidget.visibilityChanged.connect(self.updateLogContextMenus) + + + self.listWidget = QTextBrowser() + self.logDockWidget.setWidget(self.listWidget) + self._workspace.addDockWidget(Qt.BottomDockWidgetArea, self.logDockWidget) + # Preload all perspectives self.loadAllPerspectives() - # Add FileDialog widget as docked self.filesWidget = DataExplorerWindow(self._parent, self, manager=self._data_manager) ObjectLibrary.addObject('DataExplorer', self.filesWidget) @@ -133,23 +142,12 @@ def addWidgets(self): self.dockedFilesWidget = QDockWidget("Data Explorer", self._workspace) self.dockedFilesWidget.setFloating(False) self.dockedFilesWidget.setWidget(self.filesWidget) - # Modify menu items on widget visibility change self.dockedFilesWidget.visibilityChanged.connect(self.updateContextMenus) self._workspace.addDockWidget(Qt.LeftDockWidgetArea, self.dockedFilesWidget) self._workspace.resizeDocks([self.dockedFilesWidget], [305], Qt.Horizontal) - # Add the console window as another docked widget - self.logDockWidget = QDockWidget("Log Explorer", self._workspace) - self.logDockWidget.setObjectName("LogDockWidget") - self.logDockWidget.visibilityChanged.connect(self.updateLogContextMenus) - - - self.listWidget = QTextBrowser() - self.logDockWidget.setWidget(self.listWidget) - self._workspace.addDockWidget(Qt.BottomDockWidgetArea, self.logDockWidget) - # Add other, minor widgets self.ackWidget = Acknowledgements() self.aboutWidget = AboutBox() @@ -168,8 +166,8 @@ def addWidgets(self): self.results_frame.setVisible(False) self.results_panel.windowClosedSignal.connect(lambda: self.results_frame.setVisible(False)) - self._workspace.toolBar.setVisible(LocalConfig.TOOLBAR_SHOW) - self._workspace.actionHide_Toolbar.setText("Show Toolbar") + custom_config = get_custom_config() + self._workspace.toolBar.setVisible(custom_config.TOOLBAR_SHOW) # Add calculators - floating for usability self.SLDCalculator = SldPanel(self) diff --git a/src/sas/qtgui/MainWindow/MainWindow.py b/src/sas/qtgui/MainWindow/MainWindow.py index 8303a75d0d..50d2bff06b 100644 --- a/src/sas/qtgui/MainWindow/MainWindow.py +++ b/src/sas/qtgui/MainWindow/MainWindow.py @@ -10,7 +10,7 @@ from PyQt5.QtWidgets import QSplashScreen from PyQt5.QtWidgets import QApplication from PyQt5.QtGui import QPixmap -from PyQt5.QtCore import Qt +from PyQt5.QtCore import Qt, QTimer # Local UI from sas.qtgui.UI import main_resources_rc @@ -35,7 +35,7 @@ def __init__(self, screen_resolution, parent=None): self.screen_width = screen_resolution.width() self.screen_height = screen_resolution.height() self.setCentralWidget(self.workspace) - + QTimer.singleShot(100, self.showMaximized) # Temporary solution for problem with menubar on Mac if sys.platform == "darwin": # Mac self.menubar.setNativeMenuBar(False) @@ -110,7 +110,6 @@ def run_sasview(): # Show the main SV window mainwindow = MainSasViewWindow(screen_resolution) - mainwindow.showMaximized() # no more splash screen splash.finish(mainwindow) diff --git a/src/sas/qtgui/Perspectives/Fitting/FittingUtilities.py b/src/sas/qtgui/Perspectives/Fitting/FittingUtilities.py index 6166bbf713..d5cd844f18 100644 --- a/src/sas/qtgui/Perspectives/Fitting/FittingUtilities.py +++ b/src/sas/qtgui/Perspectives/Fitting/FittingUtilities.py @@ -542,13 +542,17 @@ def residualsData1D(reference_data, current_data, weights): pass residuals.x = current_data.x[index][0] - residuals.dy = numpy.ones(len(residuals.y)) + residuals.dy = None residuals.dx = None residuals.dxl = None residuals.dxw = None residuals.ytransform = 'y' + if reference_data.isSesans: + residuals.xtransform = 'x' + residuals.xaxis('\\rm{z} ', 'A') # For latter scale changes - residuals.xaxis('\\rm{Q} ', 'A^{-1}') + else: + residuals.xaxis('\\rm{Q} ', 'A^{-1}') residuals.yaxis('\\rm{Residuals} ', 'normalized') return residuals @@ -572,7 +576,7 @@ def residualsData2D(reference_data, current_data, weight): residuals.qx_data = current_data.qx_data residuals.qy_data = current_data.qy_data residuals.q_data = current_data.q_data - residuals.err_data = numpy.ones(len(residuals.data)) + residuals.err_data = None residuals.xmin = min(residuals.qx_data) residuals.xmax = max(residuals.qx_data) residuals.ymin = min(residuals.qy_data) @@ -604,7 +608,6 @@ def plotResiduals(reference_data, current_data, weights): res_name = reference_data.name if reference_data.name else reference_data.filename residuals.name = "Residuals for " + str(theory_name) + "[" + res_name + "]" residuals.title = residuals.name - residuals.ytransform = 'y' # when 2 data have the same id override the 1 st plotted # include the last part if keeping charts for separate models is required @@ -632,6 +635,8 @@ def plotPolydispersities(model): # similar to FittingLogic._create1DPlot() but different data/axes data1d = Data1D(x=xarr, y=yarr) xunit = model.details[name][0] + data1d.xtransform = 'x' + data1d.ytransform = 'y' data1d.xaxis(r'\rm{{{}}}'.format(name.replace('_', '\_')), xunit) data1d.yaxis(r'\rm{probability}', 'normalized') data1d.scale = 'linear' diff --git a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py index b380d0c1a4..658d40cfe6 100644 --- a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py +++ b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @@ -264,9 +264,10 @@ def initializeGlobals(self): self.q_range_min = OptionsWidget.QMIN_DEFAULT self.q_range_max = OptionsWidget.QMAX_DEFAULT self.npts = OptionsWidget.NPTS_DEFAULT - self.log_points = False + self.log_points = True self.weighting = 0 self.chi2 = None + # Does the control support UNDO/REDO # temporarily off self.undo_supported = False @@ -330,6 +331,7 @@ def initializeWidgets(self): self.options_widget = OptionsWidget(self, self.logic) layout.addWidget(self.options_widget) self.tabOptions.setLayout(layout) + self.options_widget.setLogScale(self.log_points) # Smearing widget layout = QtWidgets.QGridLayout() @@ -1805,6 +1807,9 @@ def paramDictFromResults(self, results): logger.error(msg) return + if results.mesg: + logger.warning(results.mesg) + param_list = results.param_list # ['radius', 'radius.width'] param_values = results.pvec # array([ 0.36221662, 0.0146783 ]) param_stderr = results.stderr # array([ 1.71293015, 1.71294233]) @@ -1879,7 +1884,7 @@ def prepareFitters(self, fitter=None, fit_id=0): # Data going in data = self.logic.data - model = copy.deepcopy(self.kernel_module) + model = self.kernel_module qmin = self.q_range_min qmax = self.q_range_max @@ -2237,7 +2242,7 @@ def _requestPlots(self, item_name, item_model): data_shown = False item = None for item, plot in plots.items(): - if fitpage_name in plot.name: + if plot.plot_role != Data1D.ROLE_DATA and fitpage_name in plot.name: data_shown = True self.communicate.plotRequestedSignal.emit([item, plot], self.tab_id) # return the last data item seen, if nothing was plotted; supposed to be just data) @@ -2252,6 +2257,7 @@ def onOptionsUpdate(self): # set Q range labels on the main tab self.lblMinRangeDef.setText(GuiUtils.formatNumber(self.q_range_min, high=True)) self.lblMaxRangeDef.setText(GuiUtils.formatNumber(self.q_range_max, high=True)) + self.recalculatePlotData() def setDefaultStructureCombo(self): """ @@ -4207,12 +4213,12 @@ def updatePageWithParameters(self, line_dict, warn_user=True): pass if 'smearing_min' in line_dict.keys(): try: - self.smearing_widget.dq_l = float(line_dict['smearing_min'][0]) + self.smearing_widget.dq_r = float(line_dict['smearing_min'][0]) except ValueError: pass if 'smearing_max' in line_dict.keys(): try: - self.smearing_widget.dq_r = float(line_dict['smearing_max'][0]) + self.smearing_widget.dq_l = float(line_dict['smearing_max'][0]) except ValueError: pass diff --git a/src/sas/qtgui/Perspectives/Fitting/ModelThread.py b/src/sas/qtgui/Perspectives/Fitting/ModelThread.py index eacbdc5350..104777bf71 100644 --- a/src/sas/qtgui/Perspectives/Fitting/ModelThread.py +++ b/src/sas/qtgui/Perspectives/Fitting/ModelThread.py @@ -171,10 +171,20 @@ def compute(self): unsmeared_error = None ##smearer the ouput of the plot if self.smearer is not None: - first_bin, last_bin = self.smearer.get_bin_range(self.qmin, - self.qmax) - mask = self.data.x[first_bin:last_bin+1] - unsmeared_output = numpy.zeros((len(self.data.x))) + if self.data.isSesans: + # For SESANS, data.x, qmin and qmax, and therefore get_bin_range are in real space, and + # the Hankel transform from q space to real space is set up as a resolution function, i.e., + # the "unsmeared" data is in q space and the "smeared" data is in real space. + # Therefore, q_calc needs to be used here to calculate the unsmeared_out rather than data.x. + mask = self.smearer.resolution.q_calc + first_bin = 0 + last_bin = len(mask) + unsmeared_output = numpy.zeros((len(mask))) + else: + first_bin, last_bin = self.smearer.get_bin_range(self.qmin, + self.qmax) + mask = self.data.x[first_bin:last_bin+1] + unsmeared_output = numpy.zeros((len(self.data.x))) return_data = self.model.calculate_Iq(mask) if isinstance(return_data, tuple): @@ -183,11 +193,11 @@ def compute(self): return_data, intermediate_results = return_data unsmeared_output[first_bin:last_bin+1] = return_data output = self.smearer(unsmeared_output, first_bin, last_bin) - # Rescale data to unsmeared model # Check that the arrays are compatible. If we only have a model but no data, # the length of data.y will be zero. - if isinstance(self.data.y, numpy.ndarray) and output.shape == self.data.y.shape: + # does not apply to SESANS where Hankel was implemented as resolution function + if isinstance(self.data.y, numpy.ndarray) and output.shape == self.data.y.shape and not self.data.isSesans: unsmeared_data = numpy.zeros((len(self.data.x))) unsmeared_error = numpy.zeros((len(self.data.x))) unsmeared_data[first_bin:last_bin+1] = self.data.y[first_bin:last_bin+1]\ @@ -253,7 +263,7 @@ def compute(self): intermediate_results = {} elapsed = time.time() - self.starttime - + res = dict(x = self.data.x[index], y = output[index], page_id = self.page_id, state = self.state, weight = self.weight, fid = self.fid, toggle_mode_on = self.toggle_mode_on, diff --git a/src/sas/qtgui/Perspectives/Fitting/OptionsWidget.py b/src/sas/qtgui/Perspectives/Fitting/OptionsWidget.py index 0d69997413..7c95b95774 100644 --- a/src/sas/qtgui/Perspectives/Fitting/OptionsWidget.py +++ b/src/sas/qtgui/Perspectives/Fitting/OptionsWidget.py @@ -35,13 +35,12 @@ class OptionsWidget(QtWidgets.QWidget, Ui_tabOptions): plot_signal = QtCore.pyqtSignal() QMIN_DEFAULT = 0.0005 QMAX_DEFAULT = 0.5 - NPTS_DEFAULT = 50 + NPTS_DEFAULT = 150 MODEL = [ 'MIN_RANGE', 'MAX_RANGE', 'NPTS', - 'NPTS_FIT', - 'LOG_SPACED'] + 'NPTS_FIT'] def __init__(self, parent=None, logic=None): super(OptionsWidget, self).__init__() @@ -125,13 +124,15 @@ def initMapper(self): self.mapper.addMapping(self.txtMaxRange, self.MODEL.index('MAX_RANGE')) self.mapper.addMapping(self.txtNpts, self.MODEL.index('NPTS')) self.mapper.addMapping(self.txtNptsFit, self.MODEL.index('NPTS_FIT')) - self.mapper.addMapping(self.chkLogData, self.MODEL.index('LOG_SPACED')) self.mapper.toFirst() + def setLogScale(self, log_scale): + self.chkLogData.setChecked(log_scale) + def toggleLogData(self, isChecked): """ Toggles between log and linear data sets """ - pass + self.plot_signal.emit() def onMaskEdit(self): """ @@ -256,8 +257,7 @@ def state(self): q_range_max = float(self.model.item(self.MODEL.index('MAX_RANGE')).text()) npts = int(self.model.item(self.MODEL.index('NPTS')).text()) npts_fit = int(self.model.item(self.MODEL.index('NPTS_FIT')).text()) - log_points = str(self.model.item(self.MODEL.index('LOG_SPACED')).text()) == 'true' - + log_points = self.chkLogData.isChecked() return (q_range_min, q_range_max, npts, log_points, self.weighting) def npts2fit(self, data=None, qmin=None, qmax=None, npts=None): diff --git a/src/sas/qtgui/Perspectives/Fitting/UI/OptionsWidgetUI.ui b/src/sas/qtgui/Perspectives/Fitting/UI/OptionsWidgetUI.ui index e2f9cdf67c..9e6bfe60cc 100755 --- a/src/sas/qtgui/Perspectives/Fitting/UI/OptionsWidgetUI.ui +++ b/src/sas/qtgui/Perspectives/Fitting/UI/OptionsWidgetUI.ui @@ -164,6 +164,9 @@ Log spaced points + + true + diff --git a/src/sas/qtgui/Perspectives/Invariant/UnitTesting/InvariantPerspectiveTest.py b/src/sas/qtgui/Perspectives/Invariant/UnitTesting/InvariantPerspectiveTest.py index 29794bfafd..06c1254a4a 100644 --- a/src/sas/qtgui/Perspectives/Invariant/UnitTesting/InvariantPerspectiveTest.py +++ b/src/sas/qtgui/Perspectives/Invariant/UnitTesting/InvariantPerspectiveTest.py @@ -452,6 +452,9 @@ def checkFakeDataState(self): self.assertFalse(self.widget.txtNptsLowQ.isReadOnly()) self.assertFalse(self.widget.txtNptsHighQ.isReadOnly()) + self.assertTrue(self.widget.txtTotalQMin.isReadOnly()) + self.assertTrue(self.widget.txtTotalQMax.isReadOnly()) + # content of line edits self.assertEqual(self.widget.txtName.text(), 'data') self.assertEqual(self.widget.txtTotalQMin.text(), '0.009') diff --git a/src/sas/qtgui/Plotting/Plotter.py b/src/sas/qtgui/Plotting/Plotter.py index 0d929487e5..9f539a7e83 100644 --- a/src/sas/qtgui/Plotting/Plotter.py +++ b/src/sas/qtgui/Plotting/Plotter.py @@ -86,6 +86,8 @@ def __init__(self, parent=None, manager=None, quickplot=False): self.toolbar._actions['pan'].triggered.connect(self._pan) self.toolbar._actions['zoom'].triggered.connect(self._zoom) + self.legendVisible = True + parent.geometry() @property @@ -159,6 +161,7 @@ def plot(self, data=None, color=None, marker=None, hide_error=False, transform=T ax = self.ax x = data.view.x y = data.view.y + label = data.name # was self._title # Marker symbol. Passed marker is one of matplotlib.markers characters # Alternatively, picked up from Data1D as an int index of PlotUtilities.SHAPES dict @@ -202,22 +205,22 @@ def plot(self, data=None, color=None, marker=None, hide_error=False, transform=T l_width = markersize * 0.4 if marker == '-' or marker == '--': line = self.ax.plot(x, y, color=color, lw=l_width, marker='', - linestyle=marker, label=self._title, zorder=10)[0] + linestyle=marker, label=label, zorder=10)[0] elif marker == 'vline': y_min = min(y)*9.0/10.0 if min(y) < 0 else 0.0 line = self.ax.vlines(x=x, ymin=y_min, ymax=y, color=color, - linestyle='-', label=self._title, lw=l_width, zorder=1) + linestyle='-', label=label, lw=l_width, zorder=1) elif marker == 'step': line = self.ax.step(x, y, color=color, marker='', linestyle='-', - label=self._title, lw=l_width, zorder=1)[0] + label=label, lw=l_width, zorder=1)[0] else: # plot data with/without errorbars if hide_error: line = ax.plot(x, y, marker=marker, color=color, markersize=markersize, - linestyle='', label=self._title, picker=True) + linestyle='', label=label, picker=True) else: dy = data.view.dy # Convert tuple (lo,hi) to array [(x-lo),(hi-x)] @@ -234,7 +237,7 @@ def plot(self, data=None, color=None, marker=None, hide_error=False, transform=T markersize=markersize, lolims=False, uplims=False, xlolims=False, xuplims=False, - label=self._title, + label=label, zorder=1, picker=True) @@ -256,7 +259,7 @@ def plot(self, data=None, color=None, marker=None, hide_error=False, transform=T self.legend = ax.legend(loc='upper right', shadow=True) if self.legend: self.legend.set_picker(True) - + self.legend.set_visible(self.legendVisible) # Current labels for axes if self.yLabel and not is_fit: ax.set_ylabel(self.yLabel) @@ -301,7 +304,7 @@ def onResize(self, event): """ Resize the legend window/font on canvas resize """ - if not self.showLegend: + if not self.showLegend or not self.legendVisible: return width = _legendResize(event.width, self.parent) # resize the legend to follow the canvas width. @@ -574,7 +577,6 @@ def onResetGraphRange(self): Resets the chart X and Y ranges to their original values """ # Clear graph and plot everything again - mpl.pyplot.cla() self.ax.cla() self.setRange = None for ids in self.plot_dict: @@ -651,7 +653,6 @@ def removePlot(self, id): xl = self.ax.xaxis.label.get_text() yl = self.ax.yaxis.label.get_text() - mpl.pyplot.cla() self.ax.cla() # Recreate Artist bindings after plot clear @@ -692,7 +693,7 @@ def onModifyPlot(self, id): marker = selected_plot.symbol marker_size = selected_plot.markersize # plot name - legend = selected_plot.title + legend = selected_plot.name plotPropertiesWidget = PlotProperties(self, color=color, marker=marker, @@ -703,7 +704,7 @@ def onModifyPlot(self, id): selected_plot.markersize = plotPropertiesWidget.markersize() selected_plot.custom_color = plotPropertiesWidget.color() selected_plot.symbol = plotPropertiesWidget.marker() - selected_plot.title = plotPropertiesWidget.legend() + selected_plot.name = plotPropertiesWidget.legend() # Redraw the plot self.replacePlot(id, selected_plot) @@ -721,7 +722,6 @@ def onToggleHideError(self, id): self.plot_dict = {} # Clean the canvas - mpl.pyplot.cla() self.ax.cla() # Recreate the plots but reverse the error flag for the current @@ -794,8 +794,9 @@ def onToggleLegend(self): if not self.showLegend: return - visible = self.legend.get_visible() - self.legend.set_visible(not visible) + #visible = self.legend.get_visible() + self.legendVisible = not self.legendVisible + self.legend.set_visible(self.legendVisible) self.canvas.draw_idle() def onMplMouseDown(self, event): diff --git a/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py b/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py index cca448235d..6a82a929c7 100644 --- a/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py @@ -28,7 +28,7 @@ def __init__(self, base, axes, item=None, color='black', zorder=3): self.connect = self.base.connect # Number of points on the plot - self.nbins = 36 + self.nbins = 100 # Cursor position of Rings (Left(-1) or Right(1)) self.xmaxd = self.data.xmax self.xmind = self.data.xmin @@ -102,11 +102,7 @@ def _post_data(self, nbins=None): numpy.fabs(self.outer_circle.get_radius())) rmax = max(numpy.fabs(self.inner_circle.get_radius()), numpy.fabs(self.outer_circle.get_radius())) - # If the user does not specify the numbers of points to plot - # the default number will be nbins= 36 - if nbins is None: - self.nbins = 36 - else: + if nbins is not None: self.nbins = nbins # Create the data1D Q average of data2D sect = Ring(r_min=rmin, r_max=rmax, nbins=self.nbins) diff --git a/src/sas/qtgui/Plotting/Slicers/AzimutSlicer.py b/src/sas/qtgui/Plotting/Slicers/AzimutSlicer.py index 9f8e8fbb58..dc58315657 100644 --- a/src/sas/qtgui/Plotting/Slicers/AzimutSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/AzimutSlicer.py @@ -22,7 +22,7 @@ def __init__(self, base, axes, color='black', zorder=3): self.connect = self.base.connect # # Number of points on the plot - self.nbins = 20 + self.nbins = 100 theta1 = 2 * np.pi / 3 theta2 = -2 * np.pi / 3 diff --git a/src/sas/qtgui/Plotting/Slicers/BoxSlicer.py b/src/sas/qtgui/Plotting/Slicers/BoxSlicer.py index 72d9929c16..96c9dcf4d0 100644 --- a/src/sas/qtgui/Plotting/Slicers/BoxSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/BoxSlicer.py @@ -31,7 +31,7 @@ def __init__(self, base, axes, item=None, color='black', zorder=3): self.qmax = max(self.data.xmax, self.data.xmin, self.data.ymax, self.data.ymin) # Number of points on the plot - self.nbins = 30 + self.nbins = 100 # If True, I(|Q|) will be return, otherwise, # negative q-values are allowed self.fold = True @@ -494,7 +494,14 @@ def validate(self, param_name, param_value): Validate input from user. Values get checked at apply time. """ - return True + isValid = True + + if param_name == 'nbins': + # Can't be 0 + if param_value < 1: + print("Number of bins cannot be less than or equal to 0. Please adjust.") + isValid = False + return isValid class BoxInteractorY(BoxInteractor): @@ -518,4 +525,11 @@ def validate(self, param_name, param_value): Validate input from user Values get checked at apply time. """ - return True + isValid = True + + if param_name == 'nbins': + # Can't be 0 + if param_value < 1: + print("Number of bins cannot be less than or equal to 0. Please adjust.") + isValid = False + return isValid diff --git a/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py b/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py index 27ec99f2ff..10c847348d 100644 --- a/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py @@ -34,7 +34,7 @@ def __init__(self, base, axes, item=None, color='black', zorder=3): numpy.fabs(self.data.ymin)), 2) self.qmax = numpy.sqrt(x + y) # Number of points on the plot - self.nbins = 20 + self.nbins = 100 # Angle of the middle line self.theta2 = numpy.pi / 3 # Absolute value of the Angle between the middle line and any side line @@ -133,7 +133,7 @@ def _post_data(self, nbins=None): phimin = -self.left_line.phi + self.main_line.theta phimax = self.left_line.phi + self.main_line.theta if nbins is None: - nbins = 20 + nbins = self.nbins sect = SectorQ(r_min=0.0, r_max=radius, phi_min=phimin + numpy.pi, phi_max=phimax + numpy.pi, nbins=nbins) diff --git a/src/sas/qtgui/Utilities/GuiUtils.py b/src/sas/qtgui/Utilities/GuiUtils.py index 287c520758..7838ee900d 100644 --- a/src/sas/qtgui/Utilities/GuiUtils.py +++ b/src/sas/qtgui/Utilities/GuiUtils.py @@ -947,7 +947,7 @@ def xyTransform(data, xLabel="", yLabel=""): xLabel = "%s^{4}(%s)" % (xname, xunits) if xLabel == "ln(x)": data.transformX(DataTransform.toLogX, DataTransform.errToLogX) - xLabel = "\ln{(%s)}(%s)" % (xname, xunits) + xLabel = r"\ln{(%s)}(%s)" % (xname, xunits) if xLabel == "log10(x)": data.transformX(DataTransform.toX_pos, DataTransform.errToX_pos) xscale = 'log' @@ -961,7 +961,7 @@ def xyTransform(data, xLabel="", yLabel=""): # Y if yLabel == "ln(y)": data.transformY(DataTransform.toLogX, DataTransform.errToLogX) - yLabel = "\ln{(%s)}(%s)" % (yname, yunits) + yLabel = r"\ln{(%s)}(%s)" % (yname, yunits) if yLabel == "y": data.transformY(DataTransform.toX, DataTransform.errToX) yLabel = "%s(%s)" % (yname, yunits) @@ -980,31 +980,31 @@ def xyTransform(data, xLabel="", yLabel=""): if yLabel == "y*x^(2)": data.transformY(DataTransform.toYX2, DataTransform.errToYX2) xunits = convertUnit(2, xunits) - yLabel = "%s \ \ %s^{2}(%s%s)" % (yname, xname, yunits, xunits) + yLabel = r"%s \ \ %s^{2}(%s%s)" % (yname, xname, yunits, xunits) if yLabel == "y*x^(4)": data.transformY(DataTransform.toYX4, DataTransform.errToYX4) xunits = convertUnit(4, xunits) - yLabel = "%s \ \ %s^{4}(%s%s)" % (yname, xname, yunits, xunits) + yLabel = r"%s \ \ %s^{4}(%s%s)" % (yname, xname, yunits, xunits) if yLabel == "1/sqrt(y)": data.transformY(DataTransform.toOneOverSqrtX, DataTransform.errOneOverSqrtX) yunits = convertUnit(-0.5, yunits) - yLabel = "1/\sqrt{%s}(%s)" % (yname, yunits) + yLabel = r"1/\sqrt{%s}(%s)" % (yname, yunits) if yLabel == "ln(y*x)": data.transformY(DataTransform.toLogXY, DataTransform.errToLogXY) - yLabel = "\ln{(%s \ \ %s)}(%s%s)" % (yname, xname, yunits, xunits) + yLabel = r"\ln{(%s \ \ %s)}(%s%s)" % (yname, xname, yunits, xunits) if yLabel == "ln(y*x^(2))": data.transformY(DataTransform.toLogYX2, DataTransform.errToLogYX2) xunits = convertUnit(2, xunits) - yLabel = "\ln (%s \ \ %s^{2})(%s%s)" % (yname, xname, yunits, xunits) + yLabel = r"\ln (%s \ \ %s^{2})(%s%s)" % (yname, xname, yunits, xunits) if yLabel == "ln(y*x^(4))": data.transformY(DataTransform.toLogYX4, DataTransform.errToLogYX4) xunits = convertUnit(4, xunits) - yLabel = "\ln (%s \ \ %s^{4})(%s%s)" % (yname, xname, yunits, xunits) + yLabel = r"\ln (%s \ \ %s^{4})(%s%s)" % (yname, xname, yunits, xunits) if yLabel == "log10(y*x^(4))": data.transformY(DataTransform.toYX4, DataTransform.errToYX4) xunits = convertUnit(4, xunits) yscale = 'log' - yLabel = "%s \ \ %s^{4}(%s%s)" % (yname, xname, yunits, xunits) + yLabel = r"%s \ \ %s^{4}(%s%s)" % (yname, xname, yunits, xunits) # Perform the transformation of data in data1d->View data.transformView() diff --git a/src/sas/qtgui/Utilities/ReportDialog.py b/src/sas/qtgui/Utilities/ReportDialog.py index 4998b29ba8..e6154607ef 100644 --- a/src/sas/qtgui/Utilities/ReportDialog.py +++ b/src/sas/qtgui/Utilities/ReportDialog.py @@ -3,7 +3,6 @@ import re import logging import traceback -from xhtml2pdf import pisa from PyQt5 import QtWidgets, QtCore from PyQt5 import QtPrintSupport @@ -187,6 +186,8 @@ def HTML2PDF(data, filename): : data: html string : filename: name of file to be saved """ + # import moved from top due to cost + from xhtml2pdf import pisa try: # open output file for writing (truncated binary) with open(filename, "w+b") as resultFile: diff --git a/src/sas/qtgui/Utilities/ResultPanel.py b/src/sas/qtgui/Utilities/ResultPanel.py index 5dee6fab2a..80230e02a9 100644 --- a/src/sas/qtgui/Utilities/ResultPanel.py +++ b/src/sas/qtgui/Utilities/ResultPanel.py @@ -9,9 +9,6 @@ from PyQt5 import QtGui from PyQt5 import QtWidgets -from bumps.dream.stats import var_stats, format_vars - - class ResultPanel(QtWidgets.QTabWidget): """ FitPanel class contains fields allowing to fit models and data @@ -31,6 +28,7 @@ def __init__(self, parent, manager=None, *args, **kwargs): self.manager = manager self.communicator = self.manager.communicator() self.setMinimumSize(400, 400) + self.data_id = None self.updateBumps() # patch bumps ## TEMPORARY ## @@ -55,18 +53,15 @@ def updateBumps(self): sys.modules['bumps.gui.plot_view'] = PlotView def onPlotResults(self, results, optimizer="Unknown"): - # Clear up previous results - for view in (self.convergenceView, self.correlationView, - self.uncertaintyView, self.traceView): - view.close() - # close all tabs. REMEMBER TO USE REVERSED RANGE!!! - for index in reversed(range(self.count())): - self.removeTab(index) + # import moved here due to its cost + from bumps.dream.stats import var_stats, format_vars + self.clearAnyData() result = results[0][0] - filename = result.data.sas_data.filename + name = result.data.sas_data.name current_optimizer = optimizer - self.setWindowTitle(self.window_name + " - " + filename + " - " + current_optimizer) + self.data_id = result.data.sas_data.id + self.setWindowTitle(self.window_name + " - " + name + " - " + current_optimizer) if hasattr(result, 'convergence') and len(result.convergence) > 0: best, pop = result.convergence[:, 0], result.convergence[:, 1:] self.convergenceView.update(best, pop) @@ -95,6 +90,26 @@ def onPlotResults(self, results, optimizer="Unknown"): if self.count()==0: self.close() + def onDataDeleted(self, data): + """ Check if the data set is shown in the window and close tabs as needed. """ + if not data or not self.isVisible(): + return + if data.id == self.data_id: + self.setWindowTitle(self.window_name) + self.clearAnyData() + self.close() + + def clearAnyData(self): + """ Clear any previous results and reset window to its base state. """ + self.data_id = None + # Clear up previous results + for view in (self.convergenceView, self.correlationView, + self.uncertaintyView, self.traceView): + view.close() + # close all tabs. REMEMBER TO USE REVERSED RANGE!!! + for index in reversed(range(self.count())): + self.removeTab(index) + def closeEvent(self, event): """ Overwrite QDialog close method to allow for custom widget close diff --git a/src/sas/sascalc/calculator/sas_gen.py b/src/sas/sascalc/calculator/sas_gen.py index 2ed4351651..c9535e6884 100644 --- a/src/sas/sascalc/calculator/sas_gen.py +++ b/src/sas/sascalc/calculator/sas_gen.py @@ -15,8 +15,6 @@ from scipy.spatial.transform import Rotation from periodictable import formula, nsf -from .geni import Iq, Iqxy - if sys.version_info[0] < 3: def decode(s): return s @@ -179,6 +177,7 @@ def calculate_Iq(self, qx, qy=None): :Param y: array of y-values :return: function value """ + from .geni import Iq, Iqxy # transform position data from sample to beamline coords x, y, z = self.transform_positions() sld = self.data_sldn - self.params['solvent_SLD'] diff --git a/src/sas/sascalc/fit/BumpsFitting.py b/src/sas/sascalc/fit/BumpsFitting.py index 3cebf45e73..4a812b36b5 100644 --- a/src/sas/sascalc/fit/BumpsFitting.py +++ b/src/sas/sascalc/fit/BumpsFitting.py @@ -417,6 +417,7 @@ def abort_test(): return True return False + errors = [] fitclass, options = get_fitter() steps = options.get('steps', 0) if steps == 0: @@ -432,16 +433,18 @@ def abort_test(): ] fitdriver = fitters.FitDriver(fitclass, problem=problem, abort_test=abort_test, **options) + clipped = fitdriver.clip() + if clipped: + errors.append(f"The initial value for {clipped} was outside the fitting range and was coerced.") omp_threads = int(os.environ.get('OMP_NUM_THREADS', '0')) mapper = MPMapper if omp_threads == 1 else SerialMapper fitdriver.mapper = mapper.start_mapper(problem, None) #import time; T0 = time.time() try: best, fbest = fitdriver.fit() - errors = [] except Exception as exc: best, fbest = None, np.NaN - errors = [str(exc), traceback.format_exc()] + errors.extend([str(exc), traceback.format_exc()]) finally: mapper.stop_mapper(fitdriver.mapper) diff --git a/src/sas/sascalc/pr/p_invertor.py b/src/sas/sascalc/pr/p_invertor.py index de72132ca6..78e380be4d 100644 --- a/src/sas/sascalc/pr/p_invertor.py +++ b/src/sas/sascalc/pr/p_invertor.py @@ -9,7 +9,6 @@ import numpy as np -from . import calc logger = logging.getLogger(__name__) @@ -50,6 +49,7 @@ def residuals(self, pars): :param pars: input parameters. :return: residuals - list of residuals. """ + from . import calc pars = np.float64(pars) residuals = [] @@ -66,6 +66,7 @@ def pr_residuals(self, pars): :param pars: input parameters. :return: residuals - list of residuals. """ + from . import calc pars = np.float64(pars) residuals = [] @@ -329,6 +330,7 @@ def iq(self, pars, q): :return: I(q) """ + from . import calc q = np.float64(q) pars = np.float64(pars) pars = np.atleast_1d(pars) @@ -349,6 +351,7 @@ def get_iq_smeared(self, pars, q): :return: I(q), either scalar or vector depending on q. """ + from . import calc q = np.float64(q) q = np.atleast_1d(q) pars = np.float64(pars) @@ -370,6 +373,7 @@ def pr(self, pars, r): :return: P(r) """ + from . import calc r = np.float64(r) pars = np.float64(pars) pars = np.atleast_1d(pars) @@ -390,7 +394,7 @@ def get_pr_err(self, pars, pars_err, r): :return: (P(r), dP(r)) """ - + from . import calc pars = np.atleast_1d(np.float64(pars)) r = np.atleast_1d(np.float64(r)) @@ -421,6 +425,7 @@ def basefunc_ft(self, d_max, n, q): :return: nth Fourier transformed base function, evaluated at q. """ + from . import calc d_max = np.float64(d_max) n = int(n) q = np.float64(q) @@ -441,6 +446,7 @@ def oscillations(self, pars): :param pars: c-parameters. :return: oscillation figure of merit. """ + from . import calc nslice = 100 pars = np.float64(pars) pars = np.atleast_1d(pars) @@ -459,6 +465,7 @@ def get_peaks(self, pars): :param pars: c-parameters. :return: number of P(r) peaks. """ + from . import calc nslice = 100 pars = np.float64(pars) count = calc.npeaks(pars, self.d_max, nslice) @@ -473,6 +480,7 @@ def get_positive(self, pars): :param pars: c-parameters. :return: fraction of P(r) that is positive. """ + from . import calc nslice = 100 pars = np.float64(pars) pars = np.atleast_1d(pars) @@ -490,6 +498,7 @@ def get_pos_err(self, pars, pars_err): :return: fraction of P(r) that is positive. """ + from . import calc nslice = 51 pars = np.float64(pars) pars = np.atleast_1d(pars) @@ -505,6 +514,7 @@ def rg(self, pars): :param pars: c-parameters. :return: Rg. """ + from . import calc nslice = 101 pars = np.float64(pars) pars = np.atleast_1d(pars) @@ -519,6 +529,7 @@ def iq0(self, pars): :param pars: c-parameters. :return: I(q=0) """ + from . import calc nslice = 101 pars = np.float64(pars) pars = np.atleast_1d(pars) @@ -550,6 +561,7 @@ def _get_matrix(self, nfunc, nr): :return: 0 """ + from . import calc nfunc = int(nfunc) nr = int(nr) a_obj = np.zeros([self.npoints + nr, nfunc])