From b3a21cc5f3dbff529ed9c1dd3375032992c45688 Mon Sep 17 00:00:00 2001 From: krzywon Date: Thu, 21 Sep 2023 10:41:39 +0100 Subject: [PATCH 01/21] Simplify some of the logic for handling multiplicity models --- .../Perspectives/Fitting/FittingWidget.py | 45 +++---------------- 1 file changed, 7 insertions(+), 38 deletions(-) diff --git a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py index fc89951667..6c9e0b6144 100644 --- a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py +++ b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @@ -2161,8 +2161,6 @@ def updateModelFromList(self, param_dict): Update the model with new parameters, create the errors column """ assert isinstance(param_dict, dict) - if not dict: - return def updateFittedValues(row): # Utility function for main model update @@ -4288,32 +4286,18 @@ def gatherParams(row): Create list of main parameters based on _model_model """ param_name = str(self._model_model.item(row, 0).text()) - current_list = self.tabToList[self.tabFitting.currentIndex()] model = self._model_model if model.item(row, 0) is None: return # Assure this is a parameter - must contain a checkbox if not model.item(row, 0).isCheckable(): - # maybe it is a combobox item (multiplicity) - try: - index = model.index(row, 1) - widget = current_list.indexWidget(index) - if widget is None: - return - if isinstance(widget, QtWidgets.QComboBox): - # find the index of the combobox - current_index = widget.currentIndex() - param_list.append([param_name, 'None', str(current_index)]) - except Exception as ex: - pass - return - - param_checked = str(model.item(row, 0).checkState() == QtCore.Qt.Checked) + param_checked = None + else: + param_checked = str(model.item(row, 0).checkState() == QtCore.Qt.Checked) # Value of the parameter. In some cases this is the text of the combobox choice. param_value = str(model.item(row, 1).text()) param_error = None - param_min = None - param_max = None + _, param_min, param_max = self.kernel_module.details.get(param_name, ('', None, None)) column_offset = 0 if self.has_error_column: column_offset = 1 @@ -4321,7 +4305,7 @@ def gatherParams(row): try: param_min = str(model.item(row, 2+column_offset).text()) param_max = str(model.item(row, 3+column_offset).text()) - except: + except Exception: pass # Do we have any constraints on this parameter? constraint = self.getConstraintForRow(row, model_key="standard") @@ -4434,10 +4418,9 @@ def updatePageWithParameters(self, line_dict, warn_user=True): self.chk2DView.setChecked(line_dict['2D_params'][0]=='True') # Create the context dictionary for parameters + # Exclude multiplicity and number of shells params from context + context = {k: v for (k, v) in line_dict.items() if len(v) > 3 and k != model} context['model_name'] = model - for key, value in line_dict.items(): - if len(value) > 2: - context[key] = value if warn_user and str(self.cbModel.currentText()) != str(context['model_name']): msg = QtWidgets.QMessageBox() @@ -4513,20 +4496,6 @@ def updateFittedValues(row): param_name = str(self._model_model.item(row, 0).text()) if param_name not in list(param_dict.keys()): return - # Special case of combo box in the cell (multiplicity) - param_line = param_dict[param_name] - if len(param_line) == 1: - # modify the shells value - try: - combo_index = int(param_line[0]) - except ValueError: - # quietly pass - return - index = self._model_model.index(row, 1) - widget = self.lstParams.indexWidget(index) - if widget is not None and isinstance(widget, QtWidgets.QComboBox): - #widget.setCurrentIndex(combo_index) - return # checkbox state param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked self._model_model.item(row, 0).setCheckState(param_checked) From 076bb8ec380c8d509bdb5a57f7d90bef3e0b1338 Mon Sep 17 00:00:00 2001 From: krzywon Date: Thu, 21 Sep 2023 11:04:34 +0100 Subject: [PATCH 02/21] Fix issue related to copy parameters to file (latex/excel) for multiplicity models --- .../Perspectives/Fitting/FittingUtilities.py | 91 ++++++++++--------- 1 file changed, 46 insertions(+), 45 deletions(-) diff --git a/src/sas/qtgui/Perspectives/Fitting/FittingUtilities.py b/src/sas/qtgui/Perspectives/Fitting/FittingUtilities.py index 48330b8090..c2b765c051 100644 --- a/src/sas/qtgui/Perspectives/Fitting/FittingUtilities.py +++ b/src/sas/qtgui/Perspectives/Fitting/FittingUtilities.py @@ -881,19 +881,23 @@ def formatParametersExcel(parameters: list): check = "" for parameter in parameters: names += parameter[0]+tab - # Add the error column if fitted - if parameter[1] == "True" and parameter[3] is not None: - names += parameter[0]+"_err"+tab - - values += parameter[2]+tab - check += parameter[1]+tab - if parameter[1] == "True" and parameter[3] is not None: - values += parameter[3]+tab - # add .npts and .nsigmas when necessary - if parameter[0][-6:] == ".width": - names += parameter[0].replace('.width', '.nsigmas') + tab - names += parameter[0].replace('.width', '.npts') + tab - values += parameter[5] + tab + parameter[4] + tab + if len(parameter) > 3: + # Add the error column if fitted + if parameter[1] == "True" and parameter[3] is not None: + names += parameter[0]+"_err"+tab + + values += parameter[2]+tab + check += str(parameter[1])+tab + if parameter[1] == "True" and parameter[3] is not None: + values += parameter[3]+tab + # add .npts and .nsigmas when necessary + if parameter[0][-6:] == ".width": + names += parameter[0].replace('.width', '.nsigmas') + tab + names += parameter[0].replace('.width', '.npts') + tab + values += parameter[5] + tab + parameter[4] + tab + else: + # Empty statement for debugging purposes + pass output_string = names + crlf + values + crlf + check return output_string @@ -912,46 +916,43 @@ def formatParametersLatex(parameters: list): output_string += r'}\hline' output_string += crlf + names = "" + values = "" + for index, parameter in enumerate(parameters): name = parameter[0] # Parameter name - output_string += name.replace('_', r'\_') # Escape underscores - # Add the error column if fitted - if parameter[1] == "True" and parameter[3] is not None: - output_string += ' & ' - output_string += parameter[0]+r'\_err' - - if index < len(parameters) - 1: - output_string += ' & ' - - # add .npts and .nsigmas when necessary - if parameter[0][-6:] == ".width": - output_string += parameter[0].replace('.width', '.nsigmas') + ' & ' - output_string += parameter[0].replace('.width', '.npts') + names += name.replace('_', r'\_') # Escape underscores + if len(parameter) > 3: + values += f" {parameter[2]}" + # Add the error column if fitted + if parameter[1] == "True" and parameter[3] is not None: + names += f" & {parameter[0]} " + r'\_err' + values += f' & {parameter[3]}' if index < len(parameters) - 1: - output_string += ' & ' + names += ' & ' + values += ' & ' + + # add .npts and .nsigmas when necessary + if parameter[0][-6:] == ".width": + names += parameter[0].replace('.width', '.nsigmas') + ' & ' + names += parameter[0].replace('.width', '.npts') + values += parameter[5] + ' & ' + values += parameter[4] + + if index < len(parameters) - 1: + names += ' & ' + values += ' & ' + elif len(parameter) > 2: + values += f' & {parameter[2]} &' + else: + values += f' & {parameter[1]} &' + output_string += names output_string += r'\\ \hline' output_string += crlf - # Construct row of values and errors - for index, parameter in enumerate(parameters): - output_string += parameter[2] - if parameter[1] == "True" and parameter[3] is not None: - output_string += ' & ' - output_string += parameter[3] - - if index < len(parameters) - 1: - output_string += ' & ' - - # add .npts and .nsigmas when necessary - if parameter[0][-6:] == ".width": - output_string += parameter[5] + ' & ' - output_string += parameter[4] - - if index < len(parameters) - 1: - output_string += ' & ' - + output_string += values output_string += r'\\ \hline' output_string += crlf output_string += r'\end{tabular}' From 816c4cf6cee714dac559d610cffe4ae22dfffb85 Mon Sep 17 00:00:00 2001 From: krzywon Date: Thu, 21 Sep 2023 14:53:27 +0100 Subject: [PATCH 03/21] Fix error thrown when multiplicity param is sent to F(Q)P(Q) calculation --- src/sas/qtgui/Perspectives/Fitting/FittingWidget.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py index 6c9e0b6144..7c4213507d 100644 --- a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py +++ b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @@ -2877,7 +2877,8 @@ def onMainParamsChange(self, top, bottom): # don't try to update multiplicity counters if they aren't there. # Note that this will fail for proper bad update where the model # doesn't contain multiplicity parameter - self.kernel_module.setParam(parameter_name, value) + if self.kernel_module.params.get(parameter_name, None): + self.kernel_module.setParam(parameter_name, value) elif model_column == min_column: # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf] self.kernel_module.details[parameter_name][1] = value From ea28ca811baf38201b95b3a69c0ed92157b9bb3a Mon Sep 17 00:00:00 2001 From: krzywon Date: Fri, 22 Sep 2023 11:51:29 +0100 Subject: [PATCH 04/21] Fix addition to poly model to ensure the polydispersity type combobox is always added to the proper row --- src/sas/qtgui/Perspectives/Fitting/FittingWidget.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py index 7c4213507d..703ffb64d6 100644 --- a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py +++ b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @@ -3432,12 +3432,12 @@ def setPolyModelParameters(self, i, param): for ishell in range(1, self.current_shell_displayed+1): # Remove [n] and add the shell numeral name = param_name[0:param_name.index('[')] + str(ishell) - self.addNameToPolyModel(i, name) + self.addNameToPolyModel(name) else: # Just create a simple param entry - self.addNameToPolyModel(i, param_name) + self.addNameToPolyModel(param_name) - def addNameToPolyModel(self, i, param_name): + def addNameToPolyModel(self, param_name): """ Creates a checked row in the poly model with param_name """ @@ -3469,7 +3469,7 @@ def addNameToPolyModel(self, i, param_name): func.addItems([str(name_disp) for name_disp in POLYDISPERSITY_MODELS.keys()]) # Set the default index func.setCurrentIndex(func.findText(DEFAULT_POLYDISP_FUNCTION)) - ind = self._poly_model.index(i,self.lstPoly.itemDelegate().poly_function) + ind = self._poly_model.index(all_items-1,self.lstPoly.itemDelegate().poly_function) self.lstPoly.setIndexWidget(ind, func) func.currentIndexChanged.connect(lambda: self.onPolyComboIndexChange(str(func.currentText()), i)) From 58414a0989c2621a6d8183326833d7eccac96896 Mon Sep 17 00:00:00 2001 From: krzywon Date: Fri, 22 Sep 2023 12:30:31 +0100 Subject: [PATCH 05/21] Only display `Show SLD Profile` button for multiplicity models with sld parameters --- .../qtgui/Perspectives/Fitting/FittingWidget.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py index 703ffb64d6..331efb5cec 100644 --- a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py +++ b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @@ -3711,10 +3711,17 @@ def addExtraShells(self): # set the cell to be non-editable item4.setFlags(item4.flags() ^ QtCore.Qt.ItemIsEditable) - # cell 4: SLD button + # cell 5: SLD button item5 = QtGui.QStandardItem() - button = QtWidgets.QPushButton() - button.setText("Show SLD Profile") + button = None + for p in self.kernel_module.params.keys(): + if 'sld' in p: + # Only display the SLD Profile button for models with SLD parameters + button = QtWidgets.QPushButton() + button.setText("Show SLD Profile") + # Respond to button press + button.clicked.connect(self.onShowSLDProfile) + break self._model_model.appendRow([item1, item2, item3, item4, item5]) @@ -3766,8 +3773,6 @@ def addExtraShells(self): ## Respond to index change #func.currentTextChanged.connect(self.modifyShellsInList) - # Respond to button press - button.clicked.connect(self.onShowSLDProfile) # Available range of shells displayed in the combobox func.addItems([str(i) for i in range(shell_min, shell_max+1)]) From 62833e42c690efbe6476e60692bd1f8625ad22dd Mon Sep 17 00:00:00 2001 From: krzywon Date: Mon, 2 Oct 2023 13:50:39 -0400 Subject: [PATCH 06/21] Missed replacement for variable removed from method call --- src/sas/qtgui/Perspectives/Fitting/FittingWidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py index 331efb5cec..b5d72c79e8 100644 --- a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py +++ b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @@ -3471,7 +3471,7 @@ def addNameToPolyModel(self, param_name): func.setCurrentIndex(func.findText(DEFAULT_POLYDISP_FUNCTION)) ind = self._poly_model.index(all_items-1,self.lstPoly.itemDelegate().poly_function) self.lstPoly.setIndexWidget(ind, func) - func.currentIndexChanged.connect(lambda: self.onPolyComboIndexChange(str(func.currentText()), i)) + func.currentIndexChanged.connect(lambda: self.onPolyComboIndexChange(str(func.currentText()), all_items-1)) def onPolyFilenameChange(self, row_index): """ From 16a3db63c7810e032a2b6dab44e0080be75281c6 Mon Sep 17 00:00:00 2001 From: krzywon Date: Fri, 1 Mar 2024 15:04:05 -0500 Subject: [PATCH 07/21] Add suppressed model list and apply it to FittingWidget and AddMultEditor --- src/sas/qtgui/Perspectives/Fitting/FittingWidget.py | 6 +++++- src/sas/qtgui/Utilities/AddMultEditor.py | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py index b5d72c79e8..72bda74803 100644 --- a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py +++ b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @@ -61,6 +61,10 @@ DEFAULT_POLYDISP_FUNCTION = 'gaussian' +# A list of models that are known to not work with how the GUI handles models from sasmodels +# NOTE: These models are correct when used directly through the sasmodels package, but how qtgui handles them is wrong +SUPPRESSED_MODELS = ['rpa'] + # CRUFT: remove when new release of sasmodels is available # https://github.com/SasView/sasview/pull/181#discussion_r218135162 if not hasattr(SasviewModel, 'get_weights'): @@ -1681,7 +1685,7 @@ def onSelectCategory(self): # Populate the models combobox self.cbModel.blockSignals(True) self.cbModel.addItem(MODEL_DEFAULT) - self.cbModel.addItems(sorted([model for (model, _) in model_list if model != 'rpa'])) + self.cbModel.addItems(sorted([model for (model, _) in model_list if model not in SUPPRESSED_MODELS])) self.cbModel.blockSignals(False) def onPolyModelChange(self, top, bottom): diff --git a/src/sas/qtgui/Utilities/AddMultEditor.py b/src/sas/qtgui/Utilities/AddMultEditor.py index da161e1cb7..d6dabdc5f4 100644 --- a/src/sas/qtgui/Utilities/AddMultEditor.py +++ b/src/sas/qtgui/Utilities/AddMultEditor.py @@ -20,6 +20,7 @@ from sas.sascalc.fit import models import sas.qtgui.Utilities.GuiUtils as GuiUtils +from sas.qtgui.Perspectives.Fitting.FittingWidget import SUPPRESSED_MODELS # Local UI from sas.qtgui.Utilities.UI.AddMultEditorUI import Ui_AddMultEditorUI @@ -118,7 +119,7 @@ def readModels(self, std_only=False): continue models_dict[model.name] = model - return sorted([model_name for model_name in models_dict]) + return sorted([model_name for model_name in models_dict if model_name not in SUPPRESSED_MODELS]) def setupSignals(self): """ Signals from various elements """ From 039b6ef84cefb243894acf3f182d5e67c0f57a44 Mon Sep 17 00:00:00 2001 From: krzywon Date: Mon, 4 Mar 2024 11:57:39 -0500 Subject: [PATCH 08/21] Create a list of built-in layered models --- src/sas/qtgui/Perspectives/Fitting/FittingWidget.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py index 72bda74803..bbb74e515c 100644 --- a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py +++ b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @@ -64,6 +64,8 @@ # A list of models that are known to not work with how the GUI handles models from sasmodels # NOTE: These models are correct when used directly through the sasmodels package, but how qtgui handles them is wrong SUPPRESSED_MODELS = ['rpa'] +# Layered models that have integer parameters are often treated differently. Maintain a list of these models. +LAYERED_MODELS = ['unified_power_Rg', 'core_multi_shell', 'onion', 'spherical_sld'] # CRUFT: remove when new release of sasmodels is available # https://github.com/SasView/sasview/pull/181#discussion_r218135162 From 61700ea0e4b81dcd71c7b0bb4bcd1d5c1ce41c32 Mon Sep 17 00:00:00 2001 From: krzywon Date: Mon, 4 Mar 2024 12:00:11 -0500 Subject: [PATCH 09/21] Use list of layered models to generate a shortened list of models that is used when one of the add/multi editor models is in the layered list --- src/sas/qtgui/Utilities/AddMultEditor.py | 61 ++++++++++++++++-------- 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/src/sas/qtgui/Utilities/AddMultEditor.py b/src/sas/qtgui/Utilities/AddMultEditor.py index d6dabdc5f4..2b876dd0c7 100644 --- a/src/sas/qtgui/Utilities/AddMultEditor.py +++ b/src/sas/qtgui/Utilities/AddMultEditor.py @@ -68,14 +68,15 @@ def __init__(self, parent=None): # Flag for correctness of resulting name self.good_name = False - self.setupSignals() - + # Create base model lists self.list_models = self.readModels() self.list_standard_models = self.readModels(std_only=True) - - # Fill models' comboboxes + # Fill models combo boxes self.setupModels() + # Set signals after model combo boxes are populated + self.setupSignals() + self.setFixedSize(self.minimumSizeHint()) # Name and directory for saving new plugin model @@ -132,6 +133,10 @@ def setupSignals(self): self.buttonBox.button(QtWidgets.QDialogButtonBox.Help).clicked.connect(self.onHelp) self.buttonBox.button(QtWidgets.QDialogButtonBox.Close).clicked.connect(self.close) + # Update model lists when new model selected in case one of the items selected is in LAYERED_MODELS + self.cbModel1.currentIndexChanged.connect(self.updateModels) + self.cbModel2.currentIndexChanged.connect(self.updateModels) + # change displayed equation when changing operator self.cbOperator.currentIndexChanged.connect(self.onOperatorChange) @@ -269,29 +274,45 @@ def write_new_model_to_file(self, fname, model1_name, model2_name, operator): out_f.write(output) def updateModels(self): - """ Update contents of comboboxes with new plugin models """ + """ Update contents of combo boxes with new plugin models """ + # Supress signals to prevent infinite loop + self.cbModel1.blockSignals(True) + self.cbModel2.blockSignals(True) - # Keep pointers to the current indices so we can show the comboboxes with - # original selection - model_1 = self.cbModel1.currentText() - model_2 = self.cbModel2.currentText() + self._updateModelLists() - self.cbModel1.blockSignals(True) - self.cbModel1.clear() + self.cbModel2.blockSignals(False) self.cbModel1.blockSignals(False) - self.cbModel2.blockSignals(True) + def _updateModelLists(self): + """Update the combo boxes for both lists of models. The models in LAYERED_MODELS can only be included a single + time in a plugin model. The two combo boxes could be different if a layered model is selected.""" + # Keep pointers to the current indices, so we can show the combo boxes with original selection + model_1 = self.cbModel1.currentText() + model_2 = self.cbModel2.currentText() + self.cbModel1.clear() self.cbModel2.clear() - self.cbModel2.blockSignals(False) # Retrieve the list of models model_list = self.readModels(std_only=True) - # Populate the models comboboxes - self.cbModel1.addItems(model_list) - self.cbModel2.addItems(model_list) - - # Scroll back to the user chosen models - self.cbModel1.setCurrentIndex(self.cbModel1.findText(model_1)) - self.cbModel2.setCurrentIndex(self.cbModel2.findText(model_2)) + no_layers_list = [model for model in model_list if model not in LAYERED_MODELS] + # Make copies of the original list to allow for list-specific changes + model_list_1 = no_layers_list if model_2 in LAYERED_MODELS else model_list + model_list_2 = no_layers_list if model_1 in LAYERED_MODELS else model_list + + # Populate the models combo boxes + self.cbModel1.addItems(model_list_1) + self.cbModel2.addItems(model_list_2) + + # Reset the model position + model1_index = self.cbModel1.findText(model_1) + model1_default = self.cbModel1.findText(CB1_DEFAULT) + model2_index = self.cbModel2.findText(model_2) + model2_default = self.cbModel2.findText(CB2_DEFAULT) + index1 = model1_index if model1_index >= 0 else model1_default if model1_default >= 0 else 0 + index2 = model2_index if model2_index >= 0 else model2_default if model2_default >= 0 else 0 + + self.cbModel1.setCurrentIndex(index1) + self.cbModel2.setCurrentIndex(index2) def onHelp(self): """ Display related help section """ From 733bc8596cd8c18fbf398f9cdb1f231dbe6e3404 Mon Sep 17 00:00:00 2001 From: krzywon Date: Mon, 4 Mar 2024 12:00:46 -0500 Subject: [PATCH 10/21] Code cleanup and string reuse --- src/sas/qtgui/Utilities/AddMultEditor.py | 32 +++++++++--------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/src/sas/qtgui/Utilities/AddMultEditor.py b/src/sas/qtgui/Utilities/AddMultEditor.py index 2b876dd0c7..cc50f214f1 100644 --- a/src/sas/qtgui/Utilities/AddMultEditor.py +++ b/src/sas/qtgui/Utilities/AddMultEditor.py @@ -20,7 +20,7 @@ from sas.sascalc.fit import models import sas.qtgui.Utilities.GuiUtils as GuiUtils -from sas.qtgui.Perspectives.Fitting.FittingWidget import SUPPRESSED_MODELS +from sas.qtgui.Perspectives.Fitting.FittingWidget import SUPPRESSED_MODELS, LAYERED_MODELS # Local UI from sas.qtgui.Utilities.UI.AddMultEditorUI import Ui_AddMultEditorUI @@ -40,6 +40,10 @@ BG_WHITE = "background-color: rgb(255, 255, 255);" BG_RED = "background-color: rgb(244, 170, 164);" +# Default model names for combo boxes +CB1_DEFAULT = 'sphere' +CB2_DEFAULT = 'cylinder' + class AddMultEditor(QtWidgets.QDialog, Ui_AddMultEditorUI): """ @@ -95,32 +99,21 @@ def setupModels(self): self.cbModel2.addItems(self.list_standard_models) # set the default initial value of Model1 and Model2 - index_ini_model1 = self.cbModel1.findText('sphere', QtCore.Qt.MatchFixedString) - - if index_ini_model1 >= 0: - self.cbModel1.setCurrentIndex(index_ini_model1) - else: - self.cbModel1.setCurrentIndex(0) - - index_ini_model2 = self.cbModel2.findText('cylinder', - QtCore.Qt.MatchFixedString) - if index_ini_model2 >= 0: - self.cbModel2.setCurrentIndex(index_ini_model2) - else: - self.cbModel2.setCurrentIndex(0) + index_ini_model1 = self.cbModel1.findText(CB1_DEFAULT, QtCore.Qt.MatchFixedString) + self.cbModel1.setCurrentIndex(index_ini_model1 if index_ini_model1 >= 0 else 0) + index_ini_model2 = self.cbModel2.findText(CB2_DEFAULT, QtCore.Qt.MatchFixedString) + self.cbModel2.setCurrentIndex(index_ini_model2 if index_ini_model2 >= 0 else 0) def readModels(self, std_only=False): """ Generate list of all models """ s_models = load_standard_models() models_dict = {} for model in s_models: - if model.category is None: - continue - if std_only and 'custom' in model.category: + # Do not include uncategorized models or suppressed models + if model.category is None or (std_only and 'custom' in model.category) or model in SUPPRESSED_MODELS: continue models_dict[model.name] = model - - return sorted([model_name for model_name in models_dict if model_name not in SUPPRESSED_MODELS]) + return sorted(models_dict) def setupSignals(self): """ Signals from various elements """ @@ -249,7 +242,6 @@ def onApply(self): self.parent.communicate.statusBarUpdateSignal.emit(msg) logging.info(msg) - def write_new_model_to_file(self, fname, model1_name, model2_name, operator): """ Write and Save file """ From 521ef9594cb455162154832861ee6d2ba375fbc2 Mon Sep 17 00:00:00 2001 From: krzywon Date: Mon, 4 Mar 2024 12:04:42 -0500 Subject: [PATCH 11/21] Move list_models generation into updateModels --- src/sas/qtgui/Utilities/AddMultEditor.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/sas/qtgui/Utilities/AddMultEditor.py b/src/sas/qtgui/Utilities/AddMultEditor.py index cc50f214f1..05b5f53415 100644 --- a/src/sas/qtgui/Utilities/AddMultEditor.py +++ b/src/sas/qtgui/Utilities/AddMultEditor.py @@ -109,6 +109,9 @@ def readModels(self, std_only=False): s_models = load_standard_models() models_dict = {} for model in s_models: + # Check if plugin model is a layered model + if 'custom' in model.category: + self._checkPlugInModels() # Do not include uncategorized models or suppressed models if model.category is None or (std_only and 'custom' in model.category) or model in SUPPRESSED_MODELS: continue @@ -232,8 +235,6 @@ def onApply(self): # Update list of models in FittingWidget and AddMultEditor self.parent.communicate.customModelDirectoryChanged.emit() - # Re-read the model list so the new model is included - self.list_models = self.readModels() self.updateModels() # Notify the user @@ -271,6 +272,8 @@ def updateModels(self): self.cbModel1.blockSignals(True) self.cbModel2.blockSignals(True) + # Re-read the model list so the new model is included + self.list_models = self.readModels() self._updateModelLists() self.cbModel2.blockSignals(False) @@ -285,11 +288,10 @@ def _updateModelLists(self): self.cbModel1.clear() self.cbModel2.clear() # Retrieve the list of models - model_list = self.readModels(std_only=True) - no_layers_list = [model for model in model_list if model not in LAYERED_MODELS] + no_layers_list = [model for model in self.list_models if model not in LAYERED_MODELS] # Make copies of the original list to allow for list-specific changes - model_list_1 = no_layers_list if model_2 in LAYERED_MODELS else model_list - model_list_2 = no_layers_list if model_1 in LAYERED_MODELS else model_list + model_list_1 = no_layers_list if model_2 in LAYERED_MODELS else self.list_models + model_list_2 = no_layers_list if model_1 in LAYERED_MODELS else self.list_models # Populate the models combo boxes self.cbModel1.addItems(model_list_1) From b9d3eabb4b8124d59873698bad0c3f1b5b07558c Mon Sep 17 00:00:00 2001 From: krzywon Date: Mon, 4 Mar 2024 12:21:32 -0500 Subject: [PATCH 12/21] Check all models if multiplicity models and add them to add/multi editor list if so to allow combo box removal, regardless of category --- src/sas/qtgui/Utilities/AddMultEditor.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/sas/qtgui/Utilities/AddMultEditor.py b/src/sas/qtgui/Utilities/AddMultEditor.py index 05b5f53415..7dd0994a51 100644 --- a/src/sas/qtgui/Utilities/AddMultEditor.py +++ b/src/sas/qtgui/Utilities/AddMultEditor.py @@ -20,7 +20,7 @@ from sas.sascalc.fit import models import sas.qtgui.Utilities.GuiUtils as GuiUtils -from sas.qtgui.Perspectives.Fitting.FittingWidget import SUPPRESSED_MODELS, LAYERED_MODELS +from sas.qtgui.Perspectives.Fitting.FittingWidget import SUPPRESSED_MODELS # Local UI from sas.qtgui.Utilities.UI.AddMultEditorUI import Ui_AddMultEditorUI @@ -72,6 +72,8 @@ def __init__(self, parent=None): # Flag for correctness of resulting name self.good_name = False + # Create a base list of layered models that will include plugin models + self.layered_models = [] # Create base model lists self.list_models = self.readModels() self.list_standard_models = self.readModels(std_only=True) @@ -110,8 +112,7 @@ def readModels(self, std_only=False): models_dict = {} for model in s_models: # Check if plugin model is a layered model - if 'custom' in model.category: - self._checkPlugInModels() + self._checkIfLayered(model) # Do not include uncategorized models or suppressed models if model.category is None or (std_only and 'custom' in model.category) or model in SUPPRESSED_MODELS: continue @@ -129,7 +130,7 @@ def setupSignals(self): self.buttonBox.button(QtWidgets.QDialogButtonBox.Help).clicked.connect(self.onHelp) self.buttonBox.button(QtWidgets.QDialogButtonBox.Close).clicked.connect(self.close) - # Update model lists when new model selected in case one of the items selected is in LAYERED_MODELS + # Update model lists when new model selected in case one of the items selected is in self.layered_models self.cbModel1.currentIndexChanged.connect(self.updateModels) self.cbModel2.currentIndexChanged.connect(self.updateModels) @@ -280,7 +281,7 @@ def updateModels(self): self.cbModel1.blockSignals(False) def _updateModelLists(self): - """Update the combo boxes for both lists of models. The models in LAYERED_MODELS can only be included a single + """Update the combo boxes for both lists of models. The models in layered_models can only be included a single time in a plugin model. The two combo boxes could be different if a layered model is selected.""" # Keep pointers to the current indices, so we can show the combo boxes with original selection model_1 = self.cbModel1.currentText() @@ -288,10 +289,10 @@ def _updateModelLists(self): self.cbModel1.clear() self.cbModel2.clear() # Retrieve the list of models - no_layers_list = [model for model in self.list_models if model not in LAYERED_MODELS] + no_layers_list = [model for model in self.list_models if model not in self.layered_models] # Make copies of the original list to allow for list-specific changes - model_list_1 = no_layers_list if model_2 in LAYERED_MODELS else self.list_models - model_list_2 = no_layers_list if model_1 in LAYERED_MODELS else self.list_models + model_list_1 = no_layers_list if model_2 in self.layered_models else self.list_models + model_list_2 = no_layers_list if model_1 in self.layered_models else self.list_models # Populate the models combo boxes self.cbModel1.addItems(model_list_1) @@ -308,6 +309,11 @@ def _updateModelLists(self): self.cbModel1.setCurrentIndex(index1) self.cbModel2.setCurrentIndex(index2) + def _checkIfLayered(self, model): + """Check models for layered or conditional parameters. Add them to self.layered_models if criteria is met.""" + if model.is_multiplicity_model: + self.layered_models.append(model.name) + def onHelp(self): """ Display related help section """ From 0ec0552418eab4084e4561c9f2f5a88d0922c67b Mon Sep 17 00:00:00 2001 From: krzywon Date: Tue, 5 Mar 2024 11:14:03 -0500 Subject: [PATCH 13/21] Get name of parameter rather than form volume parameter name to prevent errors changing PD params for layered form volume parameters --- .../Perspectives/Fitting/FittingWidget.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py index 70eb8e3d0c..50171ab06a 100644 --- a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py +++ b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @@ -3507,7 +3507,7 @@ def onPolyComboIndexChange(self, combo_string, row_index): Modify polydisp. defaults on function choice """ # Get npts/nsigs for current selection - param = self.model_parameters.form_volume_parameters[row_index] + param_name = str(self._poly_model.item(row_index, 0).text()).split()[-1] file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function) combo_box = self.lstPoly.indexWidget(file_index) try: @@ -3520,16 +3520,16 @@ def updateFunctionCaption(row): # Utility function for update of polydispersity function name in the main model if not self.isCheckable(row): return - param_name = self._model_model.item(row, 0).text() - if param_name != param.name: + par_name = self._model_model.item(row, 0).text() + if par_name != param_name: return # Modify the param value self._model_model.blockSignals(True) - if self.has_error_column: - # err column changes the indexing - self._model_model.item(row, 0).child(0).child(0,5).setText(combo_string) - else: - self._model_model.item(row, 0).child(0).child(0,4).setText(combo_string) + n = 5 if self.has_error_column else 4 + # Add an extra safety check to be sure this parameter has the polydisperse table row + poly_row = self._model_model.item(row, 0).child(0) + if poly_row: + self._model_model.item(row).child(0).child(0, n).setText(combo_string) self._model_model.blockSignals(False) if combo_string == 'array': @@ -3542,7 +3542,7 @@ def updateFunctionCaption(row): self.loadPolydispArray(row_index) # Update main model for display self.iterateOverModel(updateFunctionCaption) - self.kernel_module.set_dispersion(param.name, self.disp_model) + self.kernel_module.set_dispersion(param_name, self.disp_model) # uncheck the parameter self._poly_model.item(row_index, 0).setCheckState(QtCore.Qt.Unchecked) # disable the row @@ -3557,7 +3557,7 @@ def updateFunctionCaption(row): # Pass for cancel/bad read pass else: - self.kernel_module.set_dispersion(param.name, self.disp_model) + self.kernel_module.set_dispersion(param_name, self.disp_model) # Enable the row in case it was disabled by Array self._poly_model.blockSignals(True) From 7c4c8f721bf9895c04c3d2ddad16bffc7f4107b3 Mon Sep 17 00:00:00 2001 From: krzywon Date: Tue, 5 Mar 2024 13:15:53 -0500 Subject: [PATCH 14/21] Update add multiply editor documentation to better match current functionality, and include warnings and notes specific to different version and update the sum_model.png file --- .../Fitting/media/fitting_help.rst | 41 +++++++++++------- .../Perspectives/Fitting/media/sum_model.png | Bin 29841 -> 15536 bytes 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/sas/qtgui/Perspectives/Fitting/media/fitting_help.rst b/src/sas/qtgui/Perspectives/Fitting/media/fitting_help.rst index 11b111972e..5a8bfe47fa 100755 --- a/src/sas/qtgui/Perspectives/Fitting/media/fitting_help.rst +++ b/src/sas/qtgui/Perspectives/Fitting/media/fitting_help.rst @@ -288,30 +288,41 @@ displays the *Easy Add/Multiply Editor* dialog. .. image:: sum_model.png -This option creates a custom Plugin Model of the form:: +This editor allows the creation of combined custom Plugin Models. +Give the new model a name (which will appear in the list of plugin models on the *FitPage*) +and brief description (to appear under the *Details* button on the *FitPage*). The model name must not contain +spaces (use underscores to separate words if necessary) and if it is longer +than ~25 characters the name will not display in full in the list of models. +Now select two models, as model_1 (or p1) and model_2 (or p2), and the +required operator, '+', '*', or '@' between them. Finally, click the *Apply* button +to generate and test the model. + +The `+` operator sums the individual I(Q) calculations and introduces a third scale factor:: Plugin Model = scale_factor * {(scale_1 * model_1) +/- (scale_2 * model_2)} + background -or:: +the `*` operator multiplies the individual I(Q) calculations:: Plugin Model = scale_factor * (model1 * model2) + background -In the *Easy Add/Multiply Editor* give the new model a name (which will appear -in the list of plugin models on the *FitPage*) and brief description (to appear -under the *Details* button on the *FitPage*). The model name must not contain -spaces (use underscores to separate words if necessary) and if it is longer -than ~25 characters the name will not display in full in the list of models. -Now select two built-in models, as model_1 (or p1) and model_2 (or p2), and the -required operator, '+' or '*' between them. Finally, click the *Apply* button -to generate and test the model, and then click *Close*. +and the `@` operator treats the combination as a form factor [F(Q)] for model_1 and a structure factor [S(Q)] for +model_2. The scale and background for F(Q) and S(Q) are set to 1 and 0 respectively and the combined model should +support the beta approximation:: + + Plugin Model = scale_factor * vol_fraction * * S(Q) + background :: No beta + Plugin Model = scale_factor * (vol_fraction / form_volume) * ( + ^2 * (S(Q) - 1)) + background :: beta + +**All Versions** Changes made to a plugin model are not applied to models actively in use on fit pages. +To apply plugin model changes, re-select the model from the drop-down menu on the FitPage. -Any changes to a plugin model generated in this way only become effective -*after* it is re-selected from the plugin models drop-down menu on the FitPage. +**In SasView 6.x**, multiplicity models cannot be combined. If a model with any layer or conditional parameter is +selected, similar models are removed from the other combo box. -**In SasView 4.x**, if the model is not listed you can try and force a +**In SasView 4.x**, if the model is not listed on a fit page you can try and force a recompilation of the plugins by selecting *Fitting* > *Plugin Model Operations* -> *Load Plugin Models*. **In SasView 5.x**, you may need to restart the -program. +> *Load Plugin Models*. **In SasView 5.0.2 and earlier**, you may need to restart the +program. **In SasView 5.0.3 and later**, the new model should appear in the list as soon as +the model is saved. .. warning:: diff --git a/src/sas/qtgui/Perspectives/Fitting/media/sum_model.png b/src/sas/qtgui/Perspectives/Fitting/media/sum_model.png index c4e4e10e8987aed91a008c4f7b9c24cc9c331c62..3817dacca83ce8bb0e3bfd2b29a82c6a6c3bd3dd 100755 GIT binary patch literal 15536 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 literal 29841 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�BI&>G#AS7{VEA{>@62`ib4* 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 Date: Tue, 5 Mar 2024 13:56:35 -0500 Subject: [PATCH 15/21] Do not update FittingWidget.n_shells_row in FittingWidget.updateFittedValues, they are managed in another method. This fixes an issue in the spherical_sld model where n_shells was being incremented on copy/paste. --- src/sas/qtgui/Perspectives/Fitting/FittingWidget.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py index 50171ab06a..7fdff66e9c 100644 --- a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py +++ b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @@ -4510,7 +4510,8 @@ def updateFittedValues(row): # Utility function for main model update # internal so can use closure for param_dict param_name = str(self._model_model.item(row, 0).text()) - if param_name not in list(param_dict.keys()): + if param_name not in list(param_dict.keys()) or row == self._n_shells_row: + # Skip magnetic, polydisperse (.pd), and shell parameters - they are handled elsewhere return # checkbox state param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked From a05ed2da8bb159cffcf6f82f19d34fb54c2c1a22 Mon Sep 17 00:00:00 2001 From: krzywon Date: Fri, 5 Apr 2024 15:36:18 -0400 Subject: [PATCH 16/21] Small fixes for PD functionality --- src/sas/qtgui/Perspectives/Fitting/FittingWidget.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py index 302dfcf5e6..dba7d9e9ee 100644 --- a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py +++ b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @@ -1515,7 +1515,8 @@ def onSelectModel(self): # disable polydispersity if the model does not support it has_poly = self._poly_model.rowCount() != 0 self.chkPolydispersity.setEnabled(has_poly) - self.tabFitting.setTabEnabled(TAB_POLY, has_poly) + if has_poly: + self.togglePoly(self.chkPolydispersity.isChecked()) # set focus so it doesn't move up self.cbModel.setFocus() @@ -2242,7 +2243,7 @@ def updatePolyValues(row): param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True) self._model_model.item(row, 0).child(0).child(0,1).setText(param_repr) # modify the param error - if self.has_error_column: + if self.has_poly_error_column: error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True) self._model_model.item(row, 0).child(0).child(0,2).setText(error_repr) @@ -3576,7 +3577,7 @@ def updateFunctionCaption(row): # Add an extra safety check to be sure this parameter has the polydisperse table row poly_row = self._model_model.item(row, 0).child(0) if poly_row: - self._model_model.item(row).child(0).child(0, n).setText(combo_string) + poly_row.child(0, n).setText(combo_string) self._model_model.blockSignals(False) if combo_string == 'array': From ff16216ed8c02b590e9088ace7a18211273d17f7 Mon Sep 17 00:00:00 2001 From: krzywon Date: Thu, 11 Apr 2024 15:29:55 -0400 Subject: [PATCH 17/21] Remove all if not dict: return statements from FittingWidget --- src/sas/qtgui/Perspectives/Fitting/FittingWidget.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py index dba7d9e9ee..185028f55c 100644 --- a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py +++ b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @@ -2323,8 +2323,6 @@ def updatePolyModelFromList(self, param_dict): Update the polydispersity model with new parameters, create the errors column """ assert isinstance(param_dict, dict) - if not dict: - return def updateFittedValues(row_i): # Utility function for main model update @@ -2389,8 +2387,6 @@ def updateMagnetModelFromList(self, param_dict): Update the magnetic model with new parameters, create the errors column """ assert isinstance(param_dict, dict) - if not dict: - return if self._magnet_model.rowCount() == 0: return @@ -4551,8 +4547,6 @@ def updateFullModel(self, param_dict): Update the model with new parameters """ assert isinstance(param_dict, dict) - if not dict: - return def updateFittedValues(row): # Utility function for main model update @@ -4602,8 +4596,6 @@ def updateFullPolyModel(self, param_dict): Update the polydispersity model with new parameters, create the errors column """ assert isinstance(param_dict, dict) - if not dict: - return def updateFittedValues(row): # Utility function for main model update @@ -4650,8 +4642,6 @@ def updateFullMagnetModel(self, param_dict): Update the magnetism model with new parameters, create the errors column """ assert isinstance(param_dict, dict) - if not dict: - return def updateFittedValues(row): # Utility function for main model update From 12c55d8214cefc7cfbc47d458ba3a775ae60af1b Mon Sep 17 00:00:00 2001 From: krzywon Date: Mon, 15 Apr 2024 14:27:11 -0400 Subject: [PATCH 18/21] Copy params before modifying shell models to retain values after changing number of shells --- src/sas/qtgui/Perspectives/Fitting/FittingWidget.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py index 185028f55c..48a9cd6e7d 100644 --- a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py +++ b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @@ -3852,6 +3852,8 @@ def modifyShellsInList(self, text): index = 0 logger.error("Multiplicity incorrect! Setting to 0") self.kernel_module.multiplicity = index + # Copy existing param values before removing rows to retain param values when changing n-shells + self.clipboard_copy() if remove_rows > 1: self._model_model.removeRows(first_row, remove_rows) @@ -3879,6 +3881,8 @@ def modifyShellsInList(self, text): if self.canHaveMagnetism(): self.setMagneticModel() + self.clipboard_paste() + def onShowSLDProfile(self): """ Show a quick plot of SLD profile From 54b4076781bf52ae97d3f30a357eeb1cabc5042f Mon Sep 17 00:00:00 2001 From: krzywon Date: Mon, 29 Apr 2024 15:12:41 -0400 Subject: [PATCH 19/21] Use regex to check param names for SLD profile button --- src/sas/qtgui/Perspectives/Fitting/FittingWidget.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py index 48a9cd6e7d..520d28f6cd 100644 --- a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py +++ b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @@ -1,6 +1,6 @@ import json import os -import sys +import re from collections import defaultdict from typing import Any, Tuple, Optional from pathlib import Path @@ -3769,7 +3769,7 @@ def addExtraShells(self): item5 = QtGui.QStandardItem() button = None for p in self.kernel_module.params.keys(): - if 'sld' in p: + if re.search(r'^sld[_]?\d+$', p): # Only display the SLD Profile button for models with SLD parameters button = QtWidgets.QPushButton() button.setText("Show SLD Profile") From befe75e5b700c6d14c849dd2bb7387116dab7b16 Mon Sep 17 00:00:00 2001 From: krzywon Date: Tue, 30 Apr 2024 12:18:42 -0400 Subject: [PATCH 20/21] Fix regex for multiplicity models related to SLD profile button --- src/sas/qtgui/Perspectives/Fitting/FittingWidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py index 520d28f6cd..78ca840350 100644 --- a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py +++ b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @@ -3769,7 +3769,7 @@ def addExtraShells(self): item5 = QtGui.QStandardItem() button = None for p in self.kernel_module.params.keys(): - if re.search(r'^sld[_]?\d+$', p): + if re.search(r'^sld.*\d+$', p): # Only display the SLD Profile button for models with SLD parameters button = QtWidgets.QPushButton() button.setText("Show SLD Profile") From a5972fec5fc659077b9985025024c4a4afaaa8c2 Mon Sep 17 00:00:00 2001 From: krzywon Date: Tue, 30 Apr 2024 14:43:20 -0400 Subject: [PATCH 21/21] Fix SLD params regex - only catches layered params where final character is a number greater than 0. This eliminates magnetic SLDs that end in 0 --- src/sas/qtgui/Perspectives/Fitting/FittingWidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py index 78ca840350..d0234aca0e 100644 --- a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py +++ b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @@ -3769,7 +3769,7 @@ def addExtraShells(self): item5 = QtGui.QStandardItem() button = None for p in self.kernel_module.params.keys(): - if re.search(r'^sld.*\d+$', p): + if re.search(r'^[\w]{0,3}sld.*[1-9]$', p): # Only display the SLD Profile button for models with SLD parameters button = QtWidgets.QPushButton() button.setText("Show SLD Profile")