From b713e9ef6786081335a445c79f43e6a86760c814 Mon Sep 17 00:00:00 2001 From: tsole0 Date: Tue, 18 Jun 2024 14:38:39 -0400 Subject: [PATCH 01/39] Adds ER and VR functions to non-polydisperse model and fixes whitespace --- src/sas/qtgui/Utilities/TabbedModelEditor.py | 61 +++++++++++--------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/src/sas/qtgui/Utilities/TabbedModelEditor.py b/src/sas/qtgui/Utilities/TabbedModelEditor.py index 88328a8c2f..0d2d9db036 100644 --- a/src/sas/qtgui/Utilities/TabbedModelEditor.py +++ b/src/sas/qtgui/Utilities/TabbedModelEditor.py @@ -557,12 +557,11 @@ def generateModel(self, model, fname): param_str = self.strFromParamDict(model['parameters']) pd_param_str = self.strFromParamDict(model['pd_parameters']) func_str = model['text'] - model_text = CUSTOM_TEMPLATE % { - 'name': name, - 'title': 'User model for ' + name, - 'description': desc_str, - 'date': datetime.datetime.now().strftime('%Y-%m-%d'), - } + model_text = CUSTOM_TEMPLATE.format(name = name, + title = 'User model for ' + name, + description = desc_str, + date = datetime.datetime.now().strftime('%Y-%m-%d') + ) # Write out parameters param_names = [] # to store parameter names @@ -578,7 +577,7 @@ def generateModel(self, model, fname): param_names.append(pname) pd_params.append(pname) model_text += " ['%s', '', %s, [-inf, inf], 'volume', '%s'],\n" % (pname, pvalue, desc) - model_text += ' ]\n' + model_text += ' ]\n\n' # Write out function definition model_text += 'def Iq(%s):\n' % ', '.join(['x'] + param_names) @@ -591,13 +590,17 @@ def generateModel(self, model, fname): model_text +=" import numpy as np\n" for func_line in func_str.split('\n'): model_text +='%s%s\n' % (" ", func_line) - model_text +='## uncomment the following if Iq works for vector x\n' + model_text +='\n## uncomment the following if Iq works for vector x\n' model_text +='#Iq.vectorized = True\n' - # If polydisperse, create place holders for form_volume, ER and VR + # Add parameters to ER and VR functions and include placeholder functions + model_text += "\n" + model_text += ER_VR_TEMPLATE.format(args = ', '.join(param_names)) + + # If polydisperse, create place holders for form_volume if pd_params: model_text +="\n" - model_text +=CUSTOM_TEMPLATE_PD % {'args': ', '.join(pd_params)} + model_text +=CUSTOM_TEMPLATE_PD.format(args = ', '.join(pd_params)) # Create place holder for Iqxy model_text +="\n" @@ -658,9 +661,9 @@ def strFromParamDict(cls, param_dict): Definition ---------- -Calculates %(name)s. +Calculates {name}. -%(description)s +{description} References ---------- @@ -668,29 +671,22 @@ def strFromParamDict(cls, param_dict): Authorship and Verification --------------------------- -* **Author:** --- **Date:** %(date)s -* **Last Modified by:** --- **Date:** %(date)s -* **Last Reviewed by:** --- **Date:** %(date)s +* **Author:** --- **Date:** {date} +* **Last Modified by:** --- **Date:** {date} +* **Last Reviewed by:** --- **Date:** {date} """ from sasmodels.special import * from numpy import inf -name = "%(name)s" -title = "%(title)s" -description = """%(description)s""" +name = "{name}" +title = "{title}" +description = """{description}""" ''' -CUSTOM_TEMPLATE_PD = '''\ -def form_volume(%(args)s): - """ - Volume of the particles used to compute absolute scattering intensity - and to weight polydisperse parameter contributions. - """ - return 0.0 - -def ER(%(args)s): +ER_VR_TEMPLATE = '''\ +def ER({args}): """ Effective radius of particles to be used when computing structure factors. @@ -698,7 +694,7 @@ def ER(%(args)s): """ return 0.0 -def VR(%(args)s): +def VR({args}): """ Volume ratio of particles to be used when computing structure factors. @@ -707,6 +703,15 @@ def VR(%(args)s): return 1.0 ''' +CUSTOM_TEMPLATE_PD = '''\ +def form_volume({args}): + """ + Volume of the particles used to compute absolute scattering intensity + and to weight polydisperse parameter contributions. + """ + return 0.0 +''' + SUM_TEMPLATE = """ from sasmodels.core import load_model_info from sasmodels.sasview_model import make_model_from_info From 9a5eb5e47614306d752fb3ca3ec5ed5ccc23fd06 Mon Sep 17 00:00:00 2001 From: tsole0 Date: Tue, 18 Jun 2024 17:10:37 -0400 Subject: [PATCH 02/39] Create Form Volume Function text editing box in UI --- .../qtgui/Utilities/UI/PluginDefinitionUI.ui | 109 +++++++++++------- 1 file changed, 67 insertions(+), 42 deletions(-) diff --git a/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui b/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui index 00b365b52b..3e378aaaa8 100755 --- a/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui +++ b/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui @@ -14,45 +14,6 @@ Plugin Definition - - - - Plugin name - - - - - - Enter a plugin name - - - - - - - Overwrite existing plugin model of this name - - - - - - - - - - Description - - - - - - Enter a description of the model - - - - - - @@ -128,15 +89,79 @@ value + + IBeamCursor + false <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +<html><head><meta name="qrichtext" content="1" /><meta charset="utf-8" /><style type="text/css"> p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:6.6pt; font-weight:400; font-style:normal;"> -<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:7.8pt;"><br /></p></body></html> +hr { height: 1px; border-width: 0; } +li.unchecked::marker { content: "\2610"; } +li.checked::marker { content: "\2612"; } +</style></head><body style=" font-family:'Segoe UI'; font-size:9pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'MS Shell Dlg 2'; font-size:7.8pt;"><br /></p></body></html> + + + + + + + + + + Description + + + + + + Enter a description of the model + + + + + + + + + + Plugin name + + + + + + Enter a plugin name + + + + + + + Overwrite existing plugin model of this name + + + + + + + + + + Form Volume Function + + + + + + IBeamCursor + + + false From 7625d370cfe05cb333084854d7077d9e054baacf Mon Sep 17 00:00:00 2001 From: tsole0 Date: Thu, 20 Jun 2024 10:59:21 -0400 Subject: [PATCH 03/39] allows user to edit form volume function in GUI if polydisperse params exist --- src/sas/qtgui/Utilities/PluginDefinition.py | 48 ++++++++++++++++--- src/sas/qtgui/Utilities/TabbedModelEditor.py | 18 +++++-- .../qtgui/Utilities/UI/PluginDefinitionUI.ui | 2 +- 3 files changed, 57 insertions(+), 11 deletions(-) diff --git a/src/sas/qtgui/Utilities/PluginDefinition.py b/src/sas/qtgui/Utilities/PluginDefinition.py index fb52ff3432..ba0cb490f7 100644 --- a/src/sas/qtgui/Utilities/PluginDefinition.py +++ b/src/sas/qtgui/Utilities/PluginDefinition.py @@ -66,6 +66,8 @@ def addWidgets(self): self.txtFunction.insertPlainText(text) self.txtFunction.setFont(GuiUtils.getMonospaceFont()) + self.txtFormVolumeFunction.setFont(GuiUtils.getMonospaceFont()) + # Validators rx = QtCore.QRegularExpression("^[A-Za-z0-9_]*$") @@ -75,7 +77,8 @@ def addWidgets(self): # importing QSyntaxHighlighter # DO NOT MOVE TO TOP from sas.qtgui.Utilities.PythonSyntax import PythonHighlighter - self.highlight = PythonHighlighter(self.txtFunction.document()) + self.highlightFunction = PythonHighlighter(self.txtFunction.document()) + self.highlightFormVolumeFunction = PythonHighlighter(self.txtFormVolumeFunction.document()) def initializeModel(self): """ @@ -88,7 +91,9 @@ def initializeModel(self): 'description':'', 'parameters':{}, 'pd_parameters':{}, - 'text':''} + 'func_text':'', + 'form_volume_text':'' + } def addSignals(self): """ @@ -101,6 +106,7 @@ def addSignals(self): # QTextEdit doesn't have a signal for edit finish, so we respond to text changed. # Possibly a slight overkill. self.txtFunction.textChanged.connect(self.onFunctionChanged) + self.txtFormVolumeFunction.textChanged.connect(self.onFormVolumeFunctionChanged) self.chkOverwrite.toggled.connect(self.onOverwrite) def onPluginNameChanged(self): @@ -139,7 +145,7 @@ def onParamsChanged(self, row, column): def onParamsPDChanged(self, row, column): """ - Respond to changes in non-polydisperse parameter table + Respond to changes in polydisperse parameter table """ param = value = None if self.tblParamsPD.item(row, 0): @@ -152,9 +158,27 @@ def onParamsPDChanged(self, row, column): self.model['pd_parameters'] = self.pd_parameter_dict # Check if the update was Value for last row. If so, add a new row - if column == 1 and row == self.tblParamsPD.rowCount()-1: + if column == 1 and row == self.tblParamsPD.rowCount() - 1: # Add a row self.tblParamsPD.insertRow(self.tblParamsPD.rowCount()) + + # Check to see if there is any polydisperse parameter text present + any_text_present = False + for row in range(self.tblParamsPD.rowCount()): + for column in range(self.tblParamsPD.columnCount()): + table_cell_contents = self.tblParamsPD.item(row, column) + if table_cell_contents and table_cell_contents.text(): + # There is text in at least one cell + any_text_present = True + break + if any_text_present: + # Display the Form Function box because there are polydisperse parameters + self.formFunctionBox.setVisible(True) + break + else: + # Hide the Form Function box because there are no polydisperse parameters + self.formFunctionBox.setVisible(False) + self.modelModified.emit() @@ -166,8 +190,20 @@ def onFunctionChanged(self): # mind the performance! #self.addTooltip() new_text = self.txtFunction.toPlainText().lstrip().rstrip() - if new_text != self.model['text']: - self.model['text'] = new_text + if new_text != self.model['func_text']: + self.model['func_text'] = new_text + self.modelModified.emit() + + def onFormVolumeFunctionChanged(self): + """ + Respond to changes in form volume function body + """ + # keep in mind that this is called every time the text changes. + # mind the performance! + #self.addTooltip() + new_text = self.txtFormVolumeFunction.toPlainText().lstrip().rstrip() + if new_text != self.model['form_volume_text']: + self.model['form_volume_text'] = new_text self.modelModified.emit() def onOverwrite(self): diff --git a/src/sas/qtgui/Utilities/TabbedModelEditor.py b/src/sas/qtgui/Utilities/TabbedModelEditor.py index 0d2d9db036..6c3bbd7fc2 100644 --- a/src/sas/qtgui/Utilities/TabbedModelEditor.py +++ b/src/sas/qtgui/Utilities/TabbedModelEditor.py @@ -70,6 +70,9 @@ def addWidgets(self): self.tabWidget.addTab(self.editor_widget, "Model editor") self.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(False) + # Initially hide form function box + self.plugin_widget.formFunctionBox.setVisible(False) + if self.edit_only: self.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).setText("Save") # Hide signals from the plugin widget @@ -445,7 +448,7 @@ def updateFromEditor(self): # make sure we have the file handle ready assert(filename != "") # Retrieve model string - model_str = self.getModel()['text'] + model_str = self.getModel()['func_text'] if w.is_python and self.is_python: error_line = self.checkModel(model_str) if error_line > 0: @@ -508,12 +511,17 @@ def canWriteModel(self, model=None, full_path=""): # Don't accept but return return False # Update model editor if plugin definition changed - func_str = model['text'] + func_str = model['func_text'] + form_vol_str = model['form_volume_text'] msg = None if func_str: if 'return' not in func_str: msg = "Error: The func(x) must 'return' a value at least.\n" msg += "For example: \n\nreturn 2*x" + elif form_vol_str: + if 'return' not in form_vol_str: + msg = "Error: The form_volume() must 'return' a value at least.\n" + msg += "For example: \n\nreturn 0.0" else: msg = 'Error: Function is not defined.' if msg is not None: @@ -556,7 +564,8 @@ def generateModel(self, model, fname): desc_str = model['description'] param_str = self.strFromParamDict(model['parameters']) pd_param_str = self.strFromParamDict(model['pd_parameters']) - func_str = model['text'] + func_str = model['func_text'] + form_vol_str = model['form_volume_text'] model_text = CUSTOM_TEMPLATE.format(name = name, title = 'User model for ' + name, description = desc_str, @@ -601,6 +610,8 @@ def generateModel(self, model, fname): if pd_params: model_text +="\n" model_text +=CUSTOM_TEMPLATE_PD.format(args = ', '.join(pd_params)) + for func_line in form_vol_str.split('\n'): + model_text +='%s%s\n' % (" ", func_line) # Create place holder for Iqxy model_text +="\n" @@ -709,7 +720,6 @@ def form_volume({args}): Volume of the particles used to compute absolute scattering intensity and to weight polydisperse parameter contributions. """ - return 0.0 ''' SUM_TEMPLATE = """ diff --git a/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui b/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui index 3e378aaaa8..37a8ef0b32 100755 --- a/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui +++ b/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui @@ -150,7 +150,7 @@ li.checked::marker { content: "\2612"; } - + Form Volume Function From 77e376f882210ff070397f0405205695db55316b Mon Sep 17 00:00:00 2001 From: tsole0 Date: Thu, 20 Jun 2024 11:29:03 -0400 Subject: [PATCH 04/39] polydisperse-only functions are omitted from model text if Form Volume Function text box is hidden --- src/sas/qtgui/Utilities/PluginDefinition.py | 4 ++++ src/sas/qtgui/Utilities/TabbedModelEditor.py | 23 ++++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/sas/qtgui/Utilities/PluginDefinition.py b/src/sas/qtgui/Utilities/PluginDefinition.py index ba0cb490f7..ddcda7eeab 100644 --- a/src/sas/qtgui/Utilities/PluginDefinition.py +++ b/src/sas/qtgui/Utilities/PluginDefinition.py @@ -19,6 +19,8 @@ class PluginDefinition(QtWidgets.QDialog, Ui_PluginDefinition): model form and parameters. """ modelModified = QtCore.Signal() + omitPolydisperseFuncsSignal = QtCore.Signal() + includePolydisperseFuncsSignal = QtCore.Signal() def __init__(self, parent=None): super(PluginDefinition, self).__init__(parent) self.setupUi(self) @@ -174,10 +176,12 @@ def onParamsPDChanged(self, row, column): if any_text_present: # Display the Form Function box because there are polydisperse parameters self.formFunctionBox.setVisible(True) + self.includePolydisperseFuncsSignal.emit() break else: # Hide the Form Function box because there are no polydisperse parameters self.formFunctionBox.setVisible(False) + self.omitPolydisperseFuncsSignal.emit() self.modelModified.emit() diff --git a/src/sas/qtgui/Utilities/TabbedModelEditor.py b/src/sas/qtgui/Utilities/TabbedModelEditor.py index 6c3bbd7fc2..b116d9b88a 100644 --- a/src/sas/qtgui/Utilities/TabbedModelEditor.py +++ b/src/sas/qtgui/Utilities/TabbedModelEditor.py @@ -43,6 +43,7 @@ def __init__(self, parent=None, edit_only=False, model=False, load_file=None): self.is_modified = False self.label = None self.file_to_regenerate = "" + self.include_polydisperse = False self.addWidgets() @@ -96,6 +97,8 @@ def addSignals(self): self.plugin_widget.modelModified.connect(self.editorModelModified) self.editor_widget.modelModified.connect(self.editorModelModified) self.plugin_widget.txtName.editingFinished.connect(self.pluginTitleSet) + self.plugin_widget.includePolydisperseFuncsSignal.connect(self.includePolydisperseFuncs) + self.plugin_widget.omitPolydisperseFuncsSignal.connect(self.omitPolydisperseFuncs) def setPluginActive(self, is_active=True): """ @@ -268,7 +271,23 @@ def editorModelModified(self): self.plugin_widget.txtFunction.setStyleSheet("") self.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(True) self.is_modified = True - + + def omitPolydisperseFuncs(self): + """ + User has no polydisperse parameters. + Omit polydisperse-only functions from model text. + Note that this is necessary because Form Volume Function text box does not clear its text when it disappears. + """ + self.include_polydisperse = False + + def includePolydisperseFuncs(self): + """ + User has defined polydisperse parameters. + Include polydisperse-only functions from model text. + By default these are not included even if text exists in Form Volume Function text box. + """ + self.include_polydisperse = True + def pluginTitleSet(self): """ User modified the model name. @@ -607,7 +626,7 @@ def generateModel(self, model, fname): model_text += ER_VR_TEMPLATE.format(args = ', '.join(param_names)) # If polydisperse, create place holders for form_volume - if pd_params: + if pd_params and self.include_polydisperse == True: model_text +="\n" model_text +=CUSTOM_TEMPLATE_PD.format(args = ', '.join(pd_params)) for func_line in form_vol_str.split('\n'): From c50987fcae76bb9d5cb2e0cf58b0d6d19df235ad Mon Sep 17 00:00:00 2001 From: tsole0 Date: Thu, 20 Jun 2024 12:11:48 -0400 Subject: [PATCH 05/39] Fixes pre-existing bug forcing user to re-enter default text for model file to generate --- src/sas/qtgui/Utilities/PluginDefinition.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sas/qtgui/Utilities/PluginDefinition.py b/src/sas/qtgui/Utilities/PluginDefinition.py index ddcda7eeab..f798a2105c 100644 --- a/src/sas/qtgui/Utilities/PluginDefinition.py +++ b/src/sas/qtgui/Utilities/PluginDefinition.py @@ -65,6 +65,7 @@ def addWidgets(self): return y """ + self.model['func_text'] = text self.txtFunction.insertPlainText(text) self.txtFunction.setFont(GuiUtils.getMonospaceFont()) From 9cfe27deafd5f62689c838e58137c44e0486b58d Mon Sep 17 00:00:00 2001 From: tsole0 Date: Thu, 20 Jun 2024 13:19:08 -0400 Subject: [PATCH 06/39] "smart" example text now automatically appears in Form Volume function box --- src/sas/qtgui/Utilities/PluginDefinition.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/sas/qtgui/Utilities/PluginDefinition.py b/src/sas/qtgui/Utilities/PluginDefinition.py index f798a2105c..2755afd3d4 100644 --- a/src/sas/qtgui/Utilities/PluginDefinition.py +++ b/src/sas/qtgui/Utilities/PluginDefinition.py @@ -31,6 +31,7 @@ def __init__(self, parent=None): # {: (, )} self.parameter_dict = {} self.pd_parameter_dict = {} + self.displayed_default_form_volume = False # Initialize widgets self.addWidgets() @@ -176,6 +177,17 @@ def onParamsPDChanged(self, row, column): break if any_text_present: # Display the Form Function box because there are polydisperse parameters + # First insert the first user-specified parameter into sample form volume function + if not self.displayed_default_form_volume: + text = \ +"""y = {0} + +return 0.0 +""".format(self.model['pd_parameters'][0][0]) + self.model['form_volume_text'] = text + self.txtFormVolumeFunction.insertPlainText(text) + self.displayed_default_form_volume = True + self.formFunctionBox.setVisible(True) self.includePolydisperseFuncsSignal.emit() break From 0b73ba6e825656ecd63bc0ca417dde17da7b9fc5 Mon Sep 17 00:00:00 2001 From: tsole0 Date: Fri, 21 Jun 2024 15:10:43 -0400 Subject: [PATCH 07/39] Change tooltips, GUI discriptions, and sample functions for clarity --- src/sas/qtgui/Utilities/PluginDefinition.py | 31 ++++++++++++------- src/sas/qtgui/Utilities/TabbedModelEditor.py | 2 +- .../qtgui/Utilities/UI/PluginDefinitionUI.ui | 4 +-- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/sas/qtgui/Utilities/PluginDefinition.py b/src/sas/qtgui/Utilities/PluginDefinition.py index 2755afd3d4..ff4247470e 100644 --- a/src/sas/qtgui/Utilities/PluginDefinition.py +++ b/src/sas/qtgui/Utilities/PluginDefinition.py @@ -42,29 +42,36 @@ def __init__(self, parent=None): # Initialize signals self.addSignals() - def addTooltip(self): + def addTooltips(self): """ - Add the default tooltip to the text field + Add the default tooltips to the Iq and form_volume function text boxes """ - hint_function = "#Example:\n\n" - hint_function += "if x <= 0:\n" - hint_function += " y = A + B\n" + hint_function = "This function returns the scattering intensity for a given q.\n" + hint_function += "Example:\n\n" + hint_function += "if q <= 0:\n" + hint_function += " intensity = A + B\n" hint_function += "else:\n" - hint_function += " y = A + B * cos(2 * pi * x)\n" - hint_function += "return y\n" + hint_function += " intensity = A + B * cos(2 * pi * q)\n" + hint_function += "return intensity\n" self.txtFunction.setToolTip(hint_function) + hint_function = "This function returns the volume of the particle.\n" + hint_function += "Example:\n\n" + hint_function += "volume = (4 / 3) * pi * R**3\n" + hint_function += "return volume\n" + self.txtFormVolumeFunction.setToolTip(hint_function) + def addWidgets(self): """ Initialize various widgets in the dialog """ - self.addTooltip() + self.addTooltips() # Initial text in the function table text = \ -"""y = x +"""intensity = q -return y +return intensity """ self.model['func_text'] = text self.txtFunction.insertPlainText(text) @@ -180,9 +187,9 @@ def onParamsPDChanged(self, row, column): # First insert the first user-specified parameter into sample form volume function if not self.displayed_default_form_volume: text = \ -"""y = {0} +"""volume = {0} * 0.0 -return 0.0 +return volume """.format(self.model['pd_parameters'][0][0]) self.model['form_volume_text'] = text self.txtFormVolumeFunction.insertPlainText(text) diff --git a/src/sas/qtgui/Utilities/TabbedModelEditor.py b/src/sas/qtgui/Utilities/TabbedModelEditor.py index b116d9b88a..d5b6810239 100644 --- a/src/sas/qtgui/Utilities/TabbedModelEditor.py +++ b/src/sas/qtgui/Utilities/TabbedModelEditor.py @@ -608,7 +608,7 @@ def generateModel(self, model, fname): model_text += ' ]\n\n' # Write out function definition - model_text += 'def Iq(%s):\n' % ', '.join(['x'] + param_names) + model_text += 'def Iq(%s):\n' % ', '.join(['q'] + param_names) model_text += ' """Absolute scattering"""\n' if "scipy." in func_str: model_text +=" import scipy\n" diff --git a/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui b/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui index 37a8ef0b32..6b27b9098a 100755 --- a/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui +++ b/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui @@ -84,7 +84,7 @@ value - Function(x) + Enter function for calculating scattering intensity I(Q): @@ -152,7 +152,7 @@ li.checked::marker { content: "\2612"; } - Form Volume Function + Enter function for calculating volume of the particle: From c62ecadda820ad1f6e1e50e433d7792fc6db10a2 Mon Sep 17 00:00:00 2001 From: tsole0 Date: Fri, 21 Jun 2024 15:36:41 -0400 Subject: [PATCH 08/39] fixes bug preventing applying model changes from model editor --- src/sas/qtgui/Utilities/TabbedModelEditor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sas/qtgui/Utilities/TabbedModelEditor.py b/src/sas/qtgui/Utilities/TabbedModelEditor.py index d5b6810239..1a70469b63 100644 --- a/src/sas/qtgui/Utilities/TabbedModelEditor.py +++ b/src/sas/qtgui/Utilities/TabbedModelEditor.py @@ -467,7 +467,7 @@ def updateFromEditor(self): # make sure we have the file handle ready assert(filename != "") # Retrieve model string - model_str = self.getModel()['func_text'] + model_str = self.getModel()['text'] if w.is_python and self.is_python: error_line = self.checkModel(model_str) if error_line > 0: From 5ccaa42e48310254dcb81a4a6b59759d865cc70e Mon Sep 17 00:00:00 2001 From: tsole0 Date: Mon, 24 Jun 2024 15:29:18 -0400 Subject: [PATCH 09/39] Adds all python flags to the model template with default values --- src/sas/qtgui/Utilities/TabbedModelEditor.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/sas/qtgui/Utilities/TabbedModelEditor.py b/src/sas/qtgui/Utilities/TabbedModelEditor.py index 1a70469b63..ba4d94129a 100644 --- a/src/sas/qtgui/Utilities/TabbedModelEditor.py +++ b/src/sas/qtgui/Utilities/TabbedModelEditor.py @@ -713,6 +713,17 @@ def strFromParamDict(cls, param_dict): title = "{title}" description = """{description}""" +# Optional flags (can be removed). Read documentation by pressing 'Help' for more information. + +# single = True indicates that the model can be run using single precision floating point values. Defaults to True. +single = True + +# opencl = False indicates that the model should not be run using OpenCL. Defaults to False. +opencl = False + +# structure_factor = False indicates that the model cannot be used as a structure factor to account for interactions between particles. Defaults to False. +structure_factor = False + ''' ER_VR_TEMPLATE = '''\ From 15d43c67c32c333a4c6873e58165447a4536865d Mon Sep 17 00:00:00 2001 From: tsole0 Date: Mon, 24 Jun 2024 17:51:21 -0400 Subject: [PATCH 10/39] Add checkboxes for generating Python and C code in PluginDefinition UI The code changes in `PluginDefinition.py` add two new checkboxes for generating Python and C code in the UI. These checkboxes allow the user to specify whether they want to generate Python or C code when working with the plugin. --- .../qtgui/Utilities/UI/PluginDefinitionUI.ui | 100 +++++++++++------- 1 file changed, 62 insertions(+), 38 deletions(-) diff --git a/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui b/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui index 6b27b9098a..baf882a8cd 100755 --- a/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui +++ b/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui @@ -14,7 +14,7 @@ Plugin Definition - + Fit parameters @@ -81,36 +81,54 @@ value - - + + - Enter function for calculating scattering intensity I(Q): + Plugin name - + - - - IBeamCursor - - - false + + + Enter a plugin name - - <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><meta charset="utf-8" /><style type="text/css"> -p, li { white-space: pre-wrap; } -hr { height: 1px; border-width: 0; } -li.unchecked::marker { content: "\2610"; } -li.checked::marker { content: "\2612"; } -</style></head><body style=" font-family:'Segoe UI'; font-size:9pt; font-weight:400; font-style:normal;"> -<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'MS Shell Dlg 2'; font-size:7.8pt;"><br /></p></body></html> + + + + + + + + false + + + Generate Python model + + + true + + + + + + + Generate C model + + + + + + + + + Overwrite existing plugin model of this name - + Description @@ -126,23 +144,19 @@ li.checked::marker { content: "\2612"; } - - + + - Plugin name + Enter function for calculating volume of the particle: - + - - - Enter a plugin name + + + IBeamCursor - - - - - - Overwrite existing plugin model of this name + + false @@ -150,19 +164,29 @@ li.checked::marker { content: "\2612"; } - + - Enter function for calculating volume of the particle: + Enter function for calculating scattering intensity I(Q): - + - + IBeamCursor false + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><meta charset="utf-8" /><style type="text/css"> +p, li { white-space: pre-wrap; } +hr { height: 1px; border-width: 0; } +li.unchecked::marker { content: "\2610"; } +li.checked::marker { content: "\2612"; } +</style></head><body style=" font-family:'Segoe UI'; font-size:9pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'MS Shell Dlg 2'; font-size:7.8pt;"><br /></p></body></html> + From 85175882c48a6ec2d7602a47c9266d9d1b4e577a Mon Sep 17 00:00:00 2001 From: tsole0 Date: Mon, 24 Jun 2024 17:53:18 -0400 Subject: [PATCH 11/39] Connect checkboxes to logic that creates new tab for displaying C model and saves as a .c file --- src/sas/qtgui/Utilities/PluginDefinition.py | 18 ++++ src/sas/qtgui/Utilities/TabbedModelEditor.py | 89 ++++++++++++++------ 2 files changed, 83 insertions(+), 24 deletions(-) diff --git a/src/sas/qtgui/Utilities/PluginDefinition.py b/src/sas/qtgui/Utilities/PluginDefinition.py index ff4247470e..0e3b8b1c16 100644 --- a/src/sas/qtgui/Utilities/PluginDefinition.py +++ b/src/sas/qtgui/Utilities/PluginDefinition.py @@ -99,6 +99,8 @@ def initializeModel(self): self.model = { 'filename':'', 'overwrite':False, + 'gen_python':True, + 'gen_c':False, 'description':'', 'parameters':{}, 'pd_parameters':{}, @@ -119,6 +121,8 @@ def addSignals(self): self.txtFunction.textChanged.connect(self.onFunctionChanged) self.txtFormVolumeFunction.textChanged.connect(self.onFormVolumeFunctionChanged) self.chkOverwrite.toggled.connect(self.onOverwrite) + self.chkGenPython.toggled.connect(self.onGenPython) + self.chkGenC.toggled.connect(self.onGenC) def onPluginNameChanged(self): """ @@ -236,6 +240,20 @@ def onOverwrite(self): """ self.model['overwrite'] = self.chkOverwrite.isChecked() self.modelModified.emit() + + def onGenPython(self): + """ + Respond to change in generate Python checkbox + """ + self.model['gen_python'] = self.chkGenPython.isChecked() + self.modelModified.emit() + + def onGenC(self): + """ + Respond to change in generate C checkbox + """ + self.model['gen_c'] = self.chkGenC.isChecked() + self.modelModified.emit() def getModel(self): """ diff --git a/src/sas/qtgui/Utilities/TabbedModelEditor.py b/src/sas/qtgui/Utilities/TabbedModelEditor.py index ba4d94129a..3d610e4dcd 100644 --- a/src/sas/qtgui/Utilities/TabbedModelEditor.py +++ b/src/sas/qtgui/Utilities/TabbedModelEditor.py @@ -33,7 +33,8 @@ def __init__(self, parent=None, edit_only=False, model=False, load_file=None): self.setupUi(self) # globals - self.filename = "" + self.filename_py = "" + self.filename_c = "" self.is_python = True self.is_documentation = False self.window_title = self.windowTitle() @@ -198,8 +199,8 @@ def loadFile(self, filename): self.editor_widget.setEnabled(True) self.editor_widget.blockSignals(False) self.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(True) - self.filename = Path(filename) - display_name = self.filename.stem + self.filename_py = Path(filename) + display_name = self.filename_py.stem if not self.model: self.setWindowTitle(self.window_title + " - " + display_name) else: @@ -221,14 +222,14 @@ def loadFile(self, filename): self.editor_widget.txtEditor.setToolTip("") # See if there is filename.c present - c_path = self.filename.parent / self.filename.name.replace(".py", ".c") - if not c_path.exists() or ".rst" in c_path.name: return + self.filename_c = self.filename_py.parent / self.filename_py.name.replace(".py", ".c") + if not self.filename_c.exists() or ".rst" in self.filename_c.name: return # add a tab with the same highlighting - c_display_name = c_path.name + c_display_name = self.filename_c.name self.c_editor_widget = ModelEditor(self, is_python=False) self.tabWidget.addTab(self.c_editor_widget, c_display_name) # Read in the file and set in on the widget - with open(c_path, 'r', encoding="utf-8") as plugin: + with open(self.filename_c, 'r', encoding="utf-8") as plugin: self.c_editor_widget.txtEditor.setPlainText(plugin.read()) self.c_editor_widget.modelModified.connect(self.editorModelModified) @@ -338,34 +339,64 @@ def updateFromPlugin(self): # check if file exists plugin_location = models.find_plugins_dir() - full_path = os.path.join(plugin_location, filename) - if os.path.splitext(full_path)[1] != ".py": - full_path += ".py" + if model['gen_python'] == True: + full_path = os.path.join(plugin_location, filename) + if os.path.splitext(full_path)[1] != ".py": + full_path += ".py" - # Update the global path definition - self.filename = full_path + # Update the global path definition + self.filename_py = full_path - if not self.canWriteModel(model, full_path): - return + if not self.canWriteModel(model, full_path): + return - # generate the model representation as string - model_str = self.generateModel(model, full_path) - self.writeFile(full_path, model_str) + # generate the model representation as string + model_str = self.generatePyModel(model, full_path) + self.writeFile(full_path, model_str) + + if model['gen_c'] == True: + c_path = os.path.join(plugin_location, filename) + if os.path.splitext(c_path)[1] != ".c": + c_path += ".c" + + # Update the global path definition + self.filename_c = c_path + + if not self.canWriteModel(model, c_path): + return + + # generate the model representation as string + c_model_str = self.generateCModel(model, c_path) + self.writeFile(c_path, c_model_str) # disable "Apply" self.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(False) # Run the model test in sasmodels - if not self.isModelCorrect(full_path): + if not self.isModelCorrect(full_path) and model['gen_python'] == True: return self.editor_widget.setEnabled(True) # Update the editor here. # Simple string forced into control. - self.editor_widget.blockSignals(True) - self.editor_widget.txtEditor.setPlainText(model_str) - self.editor_widget.blockSignals(False) + if model['gen_python'] == True: + self.editor_widget.blockSignals(True) + self.editor_widget.txtEditor.setPlainText(model_str) + self.editor_widget.blockSignals(False) + if model['gen_c'] == True: + # Add a tab to TabbedModelEditor for the C model + c_display_name = Path(self.filename_c).name + self.c_editor_widget = ModelEditor(self, is_python=False) + self.tabWidget.addTab(self.c_editor_widget, c_display_name) + + # Update the editor + self.c_editor_widget.blockSignals(True) + self.c_editor_widget.txtEditor.setPlainText(c_model_str) + self.c_editor_widget.blockSignals(False) + + # Connect 'modified' signal + self.c_editor_widget.modelModified.connect(self.editorModelModified) # Set the widget title self.setTabEdited(False) @@ -458,7 +489,7 @@ def updateFromEditor(self): """ Save the current state of the Model Editor """ - filename = self.filename + filename = self.filename_py w = self.tabWidget.currentWidget() if not w.is_python: base, _ = os.path.splitext(filename) @@ -502,7 +533,7 @@ def regenerateDocumentation(self): # in order for the documentation regeneration process to run. # The regen method is part of the documentation window. If the window is closed, the method no longer exists. if hasattr(self.parent, 'helpWindow'): - self.parent.helpWindow.regenerateHtml(self.filename) + self.parent.helpWindow.regenerateHtml(self.filename_py) def canWriteModel(self, model=None, full_path=""): """ @@ -571,8 +602,18 @@ def writeFile(cls, fname, model_str=""): """ with open(fname, 'w', encoding="utf-8") as out_f: out_f.write(model_str) + + def generateCModel(self, model, fname): + """ + Generate C model from the current plugin state + :param model: plugin model + :param fname: filename + """ + model_text = "" + return model_text + - def generateModel(self, model, fname): + def generatePyModel(self, model, fname): """ generate model from the current plugin state """ From 31309b8ada6b1626382d513c6f9bf87de97f5ec3 Mon Sep 17 00:00:00 2001 From: tsole0 Date: Tue, 25 Jun 2024 09:54:10 -0400 Subject: [PATCH 12/39] Add error message QMessageBox if user does not specify which model languages to generate --- src/sas/qtgui/Utilities/TabbedModelEditor.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/sas/qtgui/Utilities/TabbedModelEditor.py b/src/sas/qtgui/Utilities/TabbedModelEditor.py index 3d610e4dcd..67395c4584 100644 --- a/src/sas/qtgui/Utilities/TabbedModelEditor.py +++ b/src/sas/qtgui/Utilities/TabbedModelEditor.py @@ -337,6 +337,13 @@ def updateFromPlugin(self): # get required filename filename = model['filename'] + # If user has not specified an output file type, throw error message + if model['gen_python'] == False and model['gen_c'] == False: + msg = "No output model language specified.\n" + msg += "Please select what kind of models (Python, C) to generate." + QtWidgets.QMessageBox.critical(self, "Plugin Error", msg) + return + # check if file exists plugin_location = models.find_plugins_dir() if model['gen_python'] == True: @@ -377,7 +384,7 @@ def updateFromPlugin(self): return self.editor_widget.setEnabled(True) - + # Update the editor here. # Simple string forced into control. if model['gen_python'] == True: From a15b081b815e26e43ad157a15d8ef2f16c8cbbf6 Mon Sep 17 00:00:00 2001 From: tsole0 Date: Tue, 25 Jun 2024 09:55:36 -0400 Subject: [PATCH 13/39] Update error text for clarity and readability --- src/sas/qtgui/Utilities/TabbedModelEditor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sas/qtgui/Utilities/TabbedModelEditor.py b/src/sas/qtgui/Utilities/TabbedModelEditor.py index 67395c4584..0cb8034acc 100644 --- a/src/sas/qtgui/Utilities/TabbedModelEditor.py +++ b/src/sas/qtgui/Utilities/TabbedModelEditor.py @@ -340,7 +340,7 @@ def updateFromPlugin(self): # If user has not specified an output file type, throw error message if model['gen_python'] == False and model['gen_c'] == False: msg = "No output model language specified.\n" - msg += "Please select what kind of models (Python, C) to generate." + msg += "Please select which types of model (Python, C) to generate." QtWidgets.QMessageBox.critical(self, "Plugin Error", msg) return From dc5a3117776f8e0b3056204ce3413102409e51b9 Mon Sep 17 00:00:00 2001 From: tsole0 Date: Tue, 25 Jun 2024 18:01:32 -0400 Subject: [PATCH 14/39] linting within updateFromPlugin(), create C_TEMPLATE for use in creating C model template, connect so that it writes template to .c file and formats with user-input parameters --- src/sas/qtgui/Utilities/TabbedModelEditor.py | 95 ++++++++++++++++++-- 1 file changed, 88 insertions(+), 7 deletions(-) diff --git a/src/sas/qtgui/Utilities/TabbedModelEditor.py b/src/sas/qtgui/Utilities/TabbedModelEditor.py index 0cb8034acc..ac12331793 100644 --- a/src/sas/qtgui/Utilities/TabbedModelEditor.py +++ b/src/sas/qtgui/Utilities/TabbedModelEditor.py @@ -350,13 +350,10 @@ def updateFromPlugin(self): full_path = os.path.join(plugin_location, filename) if os.path.splitext(full_path)[1] != ".py": full_path += ".py" - # Update the global path definition self.filename_py = full_path - if not self.canWriteModel(model, full_path): return - # generate the model representation as string model_str = self.generatePyModel(model, full_path) self.writeFile(full_path, model_str) @@ -365,13 +362,10 @@ def updateFromPlugin(self): c_path = os.path.join(plugin_location, filename) if os.path.splitext(c_path)[1] != ".c": c_path += ".c" - # Update the global path definition self.filename_c = c_path - if not self.canWriteModel(model, c_path): return - # generate the model representation as string c_model_str = self.generateCModel(model, c_path) self.writeFile(c_path, c_model_str) @@ -616,7 +610,19 @@ def generateCModel(self, model, fname): :param model: plugin model :param fname: filename """ - model_text = "" + param_names = [] + pd_param_names = [] + param_str = self.strFromParamDict(model['parameters']) + pd_param_str = self.strFromParamDict(model['pd_parameters']) + for pname, _, _ in self.getParamHelper(param_str): + param_names.append(pname) + for pd_pname, _, _ in self.getParamHelper(pd_param_str): + pd_param_names.append(pd_pname) + print(param_names, pd_param_names) + model_text = C_TEMPLATE.format(poly_args = ',\n\t'.join(pd_param_names), + args = ',\n\t'.join(param_names), + poly_arg1 = pd_param_names[0]) + return model_text @@ -809,6 +815,81 @@ def form_volume({args}): Model = make_model_from_info(model_info) """ +C_TEMPLATE = """\ +static double +form_volume({poly_args}) // Remove arguments as needed +{{ + return 0.0*{poly_arg1}; +}} + +static double +radius_effective(int mode) // Add arguments as needed +{{ + switch (mode) {{ + default: + case 1: + // Define effective radius calculations here... + return 0.0; + }} +}} + +static void +Fq(double q, + double *F1, + double *F2, + {args}) // Remove arguments as needed +{{ + // Define F(Q) calculations here... + // IMPORTANT: You do not have to define Iq if your model uses Fq for beta approximation; the *F2 value is and equivalent to the output of Iq. + // IMPORTANT: You may use Fq instead of Iq even if you do not need (*F1) for beta approximation, but this is not recommended. + // IMPORTANT: Additionally, you must still define Iqac or Iqabc if your model has orientation parameters. + *F1 = 0.0; + *F2 = 0.0; +}} + +static double +Iq(double q, + {args}) // Remove arguments as needed +{{ + // Define I(Q) calculations here for models independent of shape orientation + // IMPORTANT: Only define ONE calculation for I(Q): either Iq, Iqac, or Iqabc; remove others. + return 1.0; +}} + +static double +Iqac(double qab, + double qc, + {args}) // Remove arguments as needed +{{ + // Define I(Q) calculations here for models dependent on shape orientation in which the shape is rotationally symmetric about *c* axis + // Note: *psi* angle not needed for shapes symmetric about *c* axis + // IMPORTANT: Only define ONE calculation for I(Q): either Iq, Iqac, Iqabc, or Iqxy; remove others. + return 1.0; +}} + +static double +Iqabc(double qa, + double qb, + double qc, + {args}) // Remove arguments as needed +{{ + // Define I(Q) calculations here for models dependent on shape orientation in all three axes + // IMPORTANT: Only define ONE calculation for I(Q): either Iq, Iqac, Iqabc, or Iqxy; remove others. + return 1.0; +}} + +static double +Iqxy(double qx, + double qy, + {args}) // Remove arguments as needed +{{ + // Define I(Q) calculations here for 2D magnetic models. + // WARNING: The use of Iqxy is generally discouraged; Use Iqabc instead for its better orientational averaging and documentation for details. + // IMPORTANT: Only define ONE calculation for I(Q): either Iq, Iqac, Iqabc, or Iqxy; remove others. + return 1.0; +}} +""" + if __name__ == '__main__': app = QtWidgets.QApplication(sys.argv) sheet = TabbedModelEditor() From bb83f692323d9aeee9825a548413b5e13cab03a7 Mon Sep 17 00:00:00 2001 From: tsole0 Date: Wed, 26 Jun 2024 11:57:46 -0400 Subject: [PATCH 15/39] fix bug preventing c template generation if no polydisperse params were specified by separating pd-dependent funcs into C_PD_TEMPLATE and only including it if pd params exist --- src/sas/qtgui/Utilities/TabbedModelEditor.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/sas/qtgui/Utilities/TabbedModelEditor.py b/src/sas/qtgui/Utilities/TabbedModelEditor.py index ac12331793..61aac319aa 100644 --- a/src/sas/qtgui/Utilities/TabbedModelEditor.py +++ b/src/sas/qtgui/Utilities/TabbedModelEditor.py @@ -615,13 +615,16 @@ def generateCModel(self, model, fname): param_str = self.strFromParamDict(model['parameters']) pd_param_str = self.strFromParamDict(model['pd_parameters']) for pname, _, _ in self.getParamHelper(param_str): - param_names.append(pname) + param_names.append('double ' + pname) for pd_pname, _, _ in self.getParamHelper(pd_param_str): - pd_param_names.append(pd_pname) - print(param_names, pd_param_names) - model_text = C_TEMPLATE.format(poly_args = ',\n\t'.join(pd_param_names), - args = ',\n\t'.join(param_names), - poly_arg1 = pd_param_names[0]) + pd_param_names.append('double ' + pd_pname) + + # Add polydisperse-dependent functions if polydisperse parameters are present + if pd_param_names != []: + model_text = C_PD_TEMPLATE.format(poly_args = ', '.join(pd_param_names), + poly_arg1 = pd_param_names[0].split(' ')[1]) # Remove 'double' from the first argument + # Add all other function templates + model_text = C_TEMPLATE.format(args = ',\n\t'.join(param_names)) return model_text @@ -815,13 +818,15 @@ def form_volume({args}): Model = make_model_from_info(model_info) """ -C_TEMPLATE = """\ +C_PD_TEMPLATE = '''\ static double form_volume({poly_args}) // Remove arguments as needed {{ return 0.0*{poly_arg1}; }} +''' +C_TEMPLATE = """\ static double radius_effective(int mode) // Add arguments as needed {{ From e87d9d1c661bfa6297058eef8a2d066fd8f638f3 Mon Sep 17 00:00:00 2001 From: tsole0 Date: Wed, 26 Jun 2024 11:59:35 -0400 Subject: [PATCH 16/39] add `source = []` statement to python model if user creates both python and C models, linking them. --- src/sas/qtgui/Utilities/TabbedModelEditor.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/sas/qtgui/Utilities/TabbedModelEditor.py b/src/sas/qtgui/Utilities/TabbedModelEditor.py index 61aac319aa..1b81353e84 100644 --- a/src/sas/qtgui/Utilities/TabbedModelEditor.py +++ b/src/sas/qtgui/Utilities/TabbedModelEditor.py @@ -664,6 +664,12 @@ def generatePyModel(self, model, fname): model_text += " ['%s', '', %s, [-inf, inf], 'volume', '%s'],\n" % (pname, pvalue, desc) model_text += ' ]\n\n' + # If creating a C model, link it to the Python file + + if model['gen_c']: + model_text += LINK_C_MODEL_TEMPLATE.format(c_model_name = name + '.c') + model_text += '\n\n' + # Write out function definition model_text += 'def Iq(%s):\n' % ', '.join(['q'] + param_names) model_text += ' """Absolute scattering"""\n' @@ -818,6 +824,12 @@ def form_volume({args}): Model = make_model_from_info(model_info) """ +LINK_C_MODEL_TEMPLATE = '''\ +# Note: removing the "source = []" line will unlink the C model from the Python model, +# which means the C model will not be checked for errors when edited. +source = ['{c_model_name}'] +''' + C_PD_TEMPLATE = '''\ static double form_volume({poly_args}) // Remove arguments as needed From 582af966f37409e9123868ab0b49d4db89bbd453 Mon Sep 17 00:00:00 2001 From: tsole0 Date: Thu, 27 Jun 2024 10:37:18 -0400 Subject: [PATCH 17/39] add descriptive comments to C model template variable, include have_fq flag into python template by default --- src/sas/qtgui/Utilities/TabbedModelEditor.py | 28 ++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/sas/qtgui/Utilities/TabbedModelEditor.py b/src/sas/qtgui/Utilities/TabbedModelEditor.py index 1b81353e84..6d589349d7 100644 --- a/src/sas/qtgui/Utilities/TabbedModelEditor.py +++ b/src/sas/qtgui/Utilities/TabbedModelEditor.py @@ -610,6 +610,9 @@ def generateCModel(self, model, fname): :param model: plugin model :param fname: filename """ + + model_text = C_COMMENT_TEMPLATE + param_names = [] pd_param_names = [] param_str = self.strFromParamDict(model['parameters']) @@ -621,10 +624,10 @@ def generateCModel(self, model, fname): # Add polydisperse-dependent functions if polydisperse parameters are present if pd_param_names != []: - model_text = C_PD_TEMPLATE.format(poly_args = ', '.join(pd_param_names), + model_text += C_PD_TEMPLATE.format(poly_args = ', '.join(pd_param_names), poly_arg1 = pd_param_names[0].split(' ')[1]) # Remove 'double' from the first argument # Add all other function templates - model_text = C_TEMPLATE.format(args = ',\n\t'.join(param_names)) + model_text += C_TEMPLATE.format(args = ',\n\t'.join(param_names)) return model_text @@ -787,6 +790,8 @@ def strFromParamDict(cls, param_dict): # structure_factor = False indicates that the model cannot be used as a structure factor to account for interactions between particles. Defaults to False. structure_factor = False +# have_fq = False indicates that the model does not define F(Q) calculations in a linked C model. Note that F(Q) calculations are only necessary for accomadating beta approximation. Defaults to False. +have_fq = False ''' ER_VR_TEMPLATE = '''\ @@ -830,6 +835,25 @@ def form_volume({args}): source = ['{c_model_name}'] ''' +C_COMMENT_TEMPLATE = '''\ +//:::Custom C model template::: +// This is a template for a custom C model. +// C Models are used for a variety of reasons in SasView, including better performance and the ability to perform calculations not possible in Python. +// For example, all oriented and magnetic models, as well as most models using structure factor calculations, are written in C. +// HOW TO USE THIS TEMPLATE: +// 1. Determine which functions you will need to perform your calculations; delete unused functions. +// 1.1 Note that you must define either Iq, Fq, or one of Iqac, Iqabc: +// Iq if your model does not use orientation parameters or use structure factor calculations; +// Fq if your model uses structure factor calculations; +// Iqac or Iqabc if your model uses orientation parameters/is magnetic; +// Fq AND Iqac/Iqabc if your model uses orientation parameters/is magnetic and has structure factor calculations. +// 2. Write C code independently of this editor and paste it into the appropriate functions. +// 2.1 Note that the C editor does not support C syntax checking, so writing C code directly into the SasView editor is not reccomended. +// 3. Ensure a python file links to your C model (source = ['filename.c']) +// 4. Press 'Apply' or 'Save' to save your model and run a model check (note that the model check will fail if there is no python file of the same name in your plugins directory) + +''' + C_PD_TEMPLATE = '''\ static double form_volume({poly_args}) // Remove arguments as needed From 9bf412747b44443d4b8a5797c52d2035a90ec4f6 Mon Sep 17 00:00:00 2001 From: tsole0 Date: Mon, 24 Jun 2024 12:55:42 -0400 Subject: [PATCH 18/39] Cherry-pick bug-fix branch to gain unit tests for model checking on plugin editor changes bug fix: ensure that both a syntax check and a model check is run on model file before approving it; condense into checkModel method --- src/sas/qtgui/Utilities/TabbedModelEditor.py | 70 +++++++++++++++----- 1 file changed, 52 insertions(+), 18 deletions(-) diff --git a/src/sas/qtgui/Utilities/TabbedModelEditor.py b/src/sas/qtgui/Utilities/TabbedModelEditor.py index 6d589349d7..8bea6729c3 100644 --- a/src/sas/qtgui/Utilities/TabbedModelEditor.py +++ b/src/sas/qtgui/Utilities/TabbedModelEditor.py @@ -210,7 +210,7 @@ def loadFile(self, filename): # Check the validity of loaded model if the model is python if self.is_python: - error_line = self.checkModel(plugin_text) + error_line = self.checkModel(plugin_text, run_unit_test=False) if error_line > 0: # select bad line cursor = QtGui.QTextCursor(self.editor_widget.txtEditor.document().findBlockByLineNumber(error_line-1)) @@ -373,8 +373,9 @@ def updateFromPlugin(self): # disable "Apply" self.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(False) - # Run the model test in sasmodels - if not self.isModelCorrect(full_path) and model['gen_python'] == True: + # Run the model test in sasmodels and check model syntax. Returns error line if checks fail. + error_line = self.checkModel(full_path) + if error_line > 0: return self.editor_widget.setEnabled(True) @@ -410,7 +411,7 @@ def updateFromPlugin(self): self.parent.communicate.statusBarUpdateSignal.emit(msg) logging.info(msg) - def checkModel(self, model_str): + def checkModel(self, full_path, run_unit_test=True): """ Run the ast check and return True if the model is good. @@ -419,9 +420,13 @@ def checkModel(self, model_str): # successfulCheck = True error_line = 0 try: + with open(full_path, 'r', encoding="utf-8") as plugin: + model_str = plugin.read() ast.parse(model_str) + if run_unit_test: + model_check = GuiUtils.checkModel(full_path) - except SyntaxError as ex: + except Exception as ex: msg = "Error building model: " + str(ex) logging.error(msg) # print four last lines of the stack trace @@ -434,21 +439,43 @@ def checkModel(self, model_str): # Set the status bar message # GuiUtils.Communicate.statusBarUpdateSignal.emit("Model check failed") self.parent.communicate.statusBarUpdateSignal.emit("Model check failed") - # Put a thick, red border around the mini-editor - self.tabWidget.currentWidget().txtEditor.setStyleSheet("border: 5px solid red") - # last_lines = traceback.format_exc().split('\n')[-4:] - traceback_to_show = '\n'.join(last_lines) - self.tabWidget.currentWidget().txtEditor.setToolTip(traceback_to_show) + + # Remove the file so it is not being loaded on refresh + os.remove(full_path) + + # Put a thick, red border around the editor. + from sas.qtgui.Utilities.CodeEditor import QCodeEditor + + # Find all QTextBrowser and QCodeEditor children + text_browsers = self.tabWidget.currentWidget().findChildren(QtWidgets.QTextBrowser) + code_editors = self.tabWidget.currentWidget().findChildren(QCodeEditor) + + # Combine the lists and apply the stylesheet + for child in text_browsers + code_editors: + child.setStyleSheet("border: 5px solid red") + # last_lines = traceback.format_exc().split('\n')[-4:] + traceback_to_show = '\n'.join(last_lines) + child.setToolTip(traceback_to_show) + # attempt to find the failing command line number, usually the last line with # `File ... line` syntax - for line in reversed(all_lines): - if 'File' in line and 'line' in line: + reversed_error_text = list(reversed(all_lines)) + for line in reversed_error_text: + if ('File' in line and 'line' in line): + # If model check fails (not syntax) then 'line' and 'File' will be in adjacent lines error_line = re.split('line ', line)[1] try: error_line = int(error_line) break except ValueError: - error_line = 0 + # Sometimes the line number is followed by more text + try: + error_line = error_line.split(',')[0] + error_line = int(error_line) + break + except ValueError: + error_line = 0 + return error_line def isModelCorrect(self, full_path): @@ -495,13 +522,21 @@ def updateFromEditor(self): if not w.is_python: base, _ = os.path.splitext(filename) filename = base + '.c' - # make sure we have the file handle ready - assert(filename != "") + assert filename != "" + # Retrieve model string model_str = self.getModel()['text'] + # Save the file + self.writeFile(filename, model_str) + + # Get model filepath + plugin_location = models.find_plugins_dir() + full_path = os.path.join(plugin_location, filename) + if os.path.splitext(full_path)[1] != ".py": + full_path += ".py" if w.is_python and self.is_python: - error_line = self.checkModel(model_str) + error_line = self.checkModel(full_path) if error_line > 0: # select bad line cursor = QtGui.QTextCursor(w.txtEditor.document().findBlockByLineNumber(error_line-1)) @@ -511,8 +546,7 @@ def updateFromEditor(self): # change the frame colours back w.txtEditor.setStyleSheet("") w.txtEditor.setToolTip("") - # Save the file - self.writeFile(filename, model_str) + # Update the tab title self.setTabEdited(False) From 1e2d643e93aad812f633cd8b8b1271ec0116bd31 Mon Sep 17 00:00:00 2001 From: tsole0 Date: Mon, 24 Jun 2024 13:03:11 -0400 Subject: [PATCH 19/39] remove isModelCorrect method whose functionality was incorporated into checkModel in previous commit --- src/sas/qtgui/Utilities/TabbedModelEditor.py | 35 -------------------- 1 file changed, 35 deletions(-) diff --git a/src/sas/qtgui/Utilities/TabbedModelEditor.py b/src/sas/qtgui/Utilities/TabbedModelEditor.py index 8bea6729c3..74e48842b7 100644 --- a/src/sas/qtgui/Utilities/TabbedModelEditor.py +++ b/src/sas/qtgui/Utilities/TabbedModelEditor.py @@ -478,41 +478,6 @@ def checkModel(self, full_path, run_unit_test=True): return error_line - def isModelCorrect(self, full_path): - """ - Run the sasmodels method for model check - and return True if the model is good. - False otherwise. - """ - successfulCheck = True - try: - model_results = GuiUtils.checkModel(full_path) - logging.info(model_results) - # We can't guarantee the type of the exception coming from - # Sasmodels, so need the overreaching general Exception - except Exception as ex: - msg = "Error building model: "+ str(ex) - logging.error(msg) - #print three last lines of the stack trace - # this will point out the exact line failing - last_lines = traceback.format_exc().split('\n')[-4:] - traceback_to_show = '\n'.join(last_lines) - logging.error(traceback_to_show) - - # Set the status bar message - self.parent.communicate.statusBarUpdateSignal.emit("Model check failed") - - # Remove the file so it is not being loaded on refresh - os.remove(full_path) - # Put a thick, red border around the mini-editor - self.plugin_widget.txtFunction.setStyleSheet("border: 5px solid red") - # Use the last line of the traceback for the tooltip - last_lines = traceback.format_exc().split('\n')[-2:] - traceback_to_show = '\n'.join(last_lines) - self.plugin_widget.txtFunction.setToolTip(traceback_to_show) - successfulCheck = False - return successfulCheck - def updateFromEditor(self): """ Save the current state of the Model Editor From 985e6fb9ddc751a27d5bb9f6f46459778fdd006a Mon Sep 17 00:00:00 2001 From: tsole0 Date: Mon, 24 Jun 2024 13:12:19 -0400 Subject: [PATCH 20/39] Update comments and remove unnecessary flag from method --- src/sas/qtgui/Utilities/TabbedModelEditor.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/sas/qtgui/Utilities/TabbedModelEditor.py b/src/sas/qtgui/Utilities/TabbedModelEditor.py index 74e48842b7..d24eb25a5f 100644 --- a/src/sas/qtgui/Utilities/TabbedModelEditor.py +++ b/src/sas/qtgui/Utilities/TabbedModelEditor.py @@ -210,7 +210,7 @@ def loadFile(self, filename): # Check the validity of loaded model if the model is python if self.is_python: - error_line = self.checkModel(plugin_text, run_unit_test=False) + error_line = self.checkModel(plugin_text) if error_line > 0: # select bad line cursor = QtGui.QTextCursor(self.editor_widget.txtEditor.document().findBlockByLineNumber(error_line-1)) @@ -411,11 +411,11 @@ def updateFromPlugin(self): self.parent.communicate.statusBarUpdateSignal.emit(msg) logging.info(msg) - def checkModel(self, full_path, run_unit_test=True): + def checkModel(self, full_path): """ - Run the ast check - and return True if the model is good. - False otherwise. + Run ast and model checks + Attempt to return the line number of the error if any + :param full_path: full path to the model file """ # successfulCheck = True error_line = 0 @@ -423,8 +423,7 @@ def checkModel(self, full_path, run_unit_test=True): with open(full_path, 'r', encoding="utf-8") as plugin: model_str = plugin.read() ast.parse(model_str) - if run_unit_test: - model_check = GuiUtils.checkModel(full_path) + GuiUtils.checkModel(full_path) except Exception as ex: msg = "Error building model: " + str(ex) From 11c7ae1259d8feb1816c7d8a39c36983fa0fbb85 Mon Sep 17 00:00:00 2001 From: tsole0 Date: Wed, 26 Jun 2024 11:27:48 -0400 Subject: [PATCH 21/39] when saving C model, run model check and highlight C window if error with C code. Clear highlights from both python and C windows if checks pass. Ensure that checkModel() is always passed a path argument instead of raw text. --- src/sas/qtgui/Utilities/TabbedModelEditor.py | 24 ++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/sas/qtgui/Utilities/TabbedModelEditor.py b/src/sas/qtgui/Utilities/TabbedModelEditor.py index d24eb25a5f..6efab2ad02 100644 --- a/src/sas/qtgui/Utilities/TabbedModelEditor.py +++ b/src/sas/qtgui/Utilities/TabbedModelEditor.py @@ -210,7 +210,7 @@ def loadFile(self, filename): # Check the validity of loaded model if the model is python if self.is_python: - error_line = self.checkModel(plugin_text) + error_line = self.checkModel(self.filename) if error_line > 0: # select bad line cursor = QtGui.QTextCursor(self.editor_widget.txtEditor.document().findBlockByLineNumber(error_line-1)) @@ -497,9 +497,20 @@ def updateFromEditor(self): # Get model filepath plugin_location = models.find_plugins_dir() full_path = os.path.join(plugin_location, filename) - if os.path.splitext(full_path)[1] != ".py": + if not w.is_python and self.is_python: + pass + elif os.path.splitext(full_path)[1] != ".py": full_path += ".py" + + # Check model as long as there is a .py file in one of the tabs if w.is_python and self.is_python: + check_model = True + elif not w.is_python and self.is_python: + # Set full_path to the .py file so that we can run a model check on it (the .py model should link to the .c model) + full_path = self.filename.with_suffix(".py") + check_model = True + + if check_model: error_line = self.checkModel(full_path) if error_line > 0: # select bad line @@ -508,8 +519,13 @@ def updateFromEditor(self): return # change the frame colours back - w.txtEditor.setStyleSheet("") - w.txtEditor.setToolTip("") + try: + self.c_editor_widget.txtEditor.setStyleSheet("") + self.c_editor_widget.txtEditor.setToolTip("") + except AttributeError: + pass + self.editor_widget.txtEditor.setStyleSheet("") + self.editor_widget.txtEditor.setToolTip("") # Update the tab title self.setTabEdited(False) From ac17f67554925666f37b18fbb1258982c891c41f Mon Sep 17 00:00:00 2001 From: tsole0 Date: Thu, 27 Jun 2024 12:13:22 -0400 Subject: [PATCH 22/39] fix bug that deletes .py file after a failed model check on a .c file by removing os.remove(). allow user to load .c models into editor even if .c file fails model checks --- src/sas/qtgui/Utilities/TabbedModelEditor.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/sas/qtgui/Utilities/TabbedModelEditor.py b/src/sas/qtgui/Utilities/TabbedModelEditor.py index 6efab2ad02..dca9deea18 100644 --- a/src/sas/qtgui/Utilities/TabbedModelEditor.py +++ b/src/sas/qtgui/Utilities/TabbedModelEditor.py @@ -208,6 +208,10 @@ def loadFile(self, filename): # Name the tab with .py filename self.tabWidget.setTabText(0, display_name) + # In case previous model was incorrect, change the frame colours back + self.editor_widget.txtEditor.setStyleSheet("") + self.editor_widget.txtEditor.setToolTip("") + # Check the validity of loaded model if the model is python if self.is_python: error_line = self.checkModel(self.filename) @@ -215,11 +219,8 @@ def loadFile(self, filename): # select bad line cursor = QtGui.QTextCursor(self.editor_widget.txtEditor.document().findBlockByLineNumber(error_line-1)) self.editor_widget.txtEditor.setTextCursor(cursor) - return - - # In case previous model was incorrect, change the frame colours back - self.editor_widget.txtEditor.setStyleSheet("") - self.editor_widget.txtEditor.setToolTip("") + # Do not return because we still want to load C file if it exists + QtWidgets.QMessageBox.warning(self, "Model check failed", "The loaded model contains errors. Please correct all errors before using model.") # See if there is filename.c present self.filename_c = self.filename_py.parent / self.filename_py.name.replace(".py", ".c") @@ -439,9 +440,6 @@ def checkModel(self, full_path): # GuiUtils.Communicate.statusBarUpdateSignal.emit("Model check failed") self.parent.communicate.statusBarUpdateSignal.emit("Model check failed") - # Remove the file so it is not being loaded on refresh - os.remove(full_path) - # Put a thick, red border around the editor. from sas.qtgui.Utilities.CodeEditor import QCodeEditor From a6fdee91e7d429742caae7a6937bc31dd2ba2e48 Mon Sep 17 00:00:00 2001 From: tsole0 Date: Thu, 27 Jun 2024 14:47:48 -0400 Subject: [PATCH 23/39] remove references to deprecated self.filename variable; in both cases self.filename_py works fine. --- src/sas/qtgui/Utilities/TabbedModelEditor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/sas/qtgui/Utilities/TabbedModelEditor.py b/src/sas/qtgui/Utilities/TabbedModelEditor.py index dca9deea18..ebba6dabe7 100644 --- a/src/sas/qtgui/Utilities/TabbedModelEditor.py +++ b/src/sas/qtgui/Utilities/TabbedModelEditor.py @@ -214,7 +214,7 @@ def loadFile(self, filename): # Check the validity of loaded model if the model is python if self.is_python: - error_line = self.checkModel(self.filename) + error_line = self.checkModel(self.filename_py) if error_line > 0: # select bad line cursor = QtGui.QTextCursor(self.editor_widget.txtEditor.document().findBlockByLineNumber(error_line-1)) @@ -505,7 +505,8 @@ def updateFromEditor(self): check_model = True elif not w.is_python and self.is_python: # Set full_path to the .py file so that we can run a model check on it (the .py model should link to the .c model) - full_path = self.filename.with_suffix(".py") + print(self.filename_py) + full_path = self.filename_py check_model = True if check_model: From 5bcd5a69bf42b924bc693a9fcd45837e6d8185ed Mon Sep 17 00:00:00 2001 From: tsole0 Date: Fri, 28 Jun 2024 10:10:12 -0400 Subject: [PATCH 24/39] add overridewarning() function that allows user to save model even if bad model check via QMessageBox prompt. This ensures no bad models are written to files without user agreement. Define full_path outside of python-specific code so that we can access it in C-specific code --- src/sas/qtgui/Utilities/TabbedModelEditor.py | 75 +++++++++++++++++--- 1 file changed, 67 insertions(+), 8 deletions(-) diff --git a/src/sas/qtgui/Utilities/TabbedModelEditor.py b/src/sas/qtgui/Utilities/TabbedModelEditor.py index ebba6dabe7..27f364ae5e 100644 --- a/src/sas/qtgui/Utilities/TabbedModelEditor.py +++ b/src/sas/qtgui/Utilities/TabbedModelEditor.py @@ -347,17 +347,20 @@ def updateFromPlugin(self): # check if file exists plugin_location = models.find_plugins_dir() + + # Generate the full path of the python path for the model + full_path_py = os.path.join(plugin_location, filename) + if os.path.splitext(full_path_py)[1] != ".py": + full_path_py += ".py" + if model['gen_python'] == True: - full_path = os.path.join(plugin_location, filename) - if os.path.splitext(full_path)[1] != ".py": - full_path += ".py" # Update the global path definition - self.filename_py = full_path - if not self.canWriteModel(model, full_path): + self.filename_py = full_path_py + if not self.canWriteModel(model, full_path_py): return # generate the model representation as string - model_str = self.generatePyModel(model, full_path) - self.writeFile(full_path, model_str) + model_str = self.generatePyModel(model, full_path_py) + self.writeFile(full_path_py, model_str) if model['gen_c'] == True: c_path = os.path.join(plugin_location, filename) @@ -515,7 +518,12 @@ def updateFromEditor(self): # select bad line cursor = QtGui.QTextCursor(w.txtEditor.document().findBlockByLineNumber(error_line-1)) w.txtEditor.setTextCursor(cursor) - return + + # Ask the user if they want to save the file with errors or continue editing + user_decision = self.saveOverrideWarning(filename, model_str) + if user_decision == False: + # If the user decides to continue editing without saving, return + return # change the frame colours back try: @@ -548,6 +556,47 @@ def regenerateDocumentation(self): # The regen method is part of the documentation window. If the window is closed, the method no longer exists. if hasattr(self.parent, 'helpWindow'): self.parent.helpWindow.regenerateHtml(self.filename_py) + + def saveOverrideWarning(self, filename, model_str): + """ + Throw popup asking user if they want to save the model despite a bad model check. + Save model if user chooses to save, and do nothing if the user chooses to continue editing. + + Returns True if user wanted to save file anyways, False if user wanted to continue editing without saving + """ + msgBox = QtWidgets.QMessageBox(self) + msgBox.setIcon(QtWidgets.QMessageBox.Warning) + msgBox.setText("Model check failed. Do you want to save the file anyways?") + msgBox.setWindowTitle("Model Error") + + # Add buttons + buttonContinue = msgBox.addButton("Continue editing", QtWidgets.QMessageBox.NoRole) + buttonSave = msgBox.addButton("Save anyways", QtWidgets.QMessageBox.AcceptRole) + # Set default button + msgBox.setDefaultButton(buttonContinue) + + # Execute the message box and wait for the user's response + userChoice = msgBox.exec_() + + # Check which button was clicked and execute the corresponding code + if msgBox.clickedButton() == buttonContinue: + return False + elif msgBox.clickedButton() == buttonSave: + # Save files anyways + py_file = os.path.splitext(filename)[0] + ".py" + c_file = os.path.splitext(filename)[0] + ".c" + py_tab_open = self.isWidgetInTab(self.tabWidget, self.editor_widget) + c_tab_open = self.isWidgetInTab(self.tabWidget, self.c_editor_widget) + + # Check to see if we have a certain model type open, and if so, write models + if py_tab_open and c_tab_open: + self.writeFile(py_file, self.editor_widget.getModel()['text']) + self.writeFile(c_file, self.c_editor_widget.getModel()['text']) + elif py_tab_open: + self.writeFile(py_file, self.editor_widget.getModel()['text']) + elif c_tab_open: + self.writeFile(c_file, self.c_editor_widget.getModel()['text']) + return True def canWriteModel(self, model=None, full_path=""): """ @@ -609,6 +658,16 @@ def getModel(self): """ return self.tabWidget.currentWidget().getModel() + @classmethod + def isWidgetInTab(cls, tabWidget, widget_to_check): + """ + Check to see if a `widget_to_check` is a tab in the `tabWidget` + """ + for i in range(tabWidget.count()): + if tabWidget.widget(i) == widget_to_check: + return True + return False + @classmethod def writeFile(cls, fname, model_str=""): """ From 3196d583187206a378954b5966c384f6534f0324 Mon Sep 17 00:00:00 2001 From: tsole0 Date: Fri, 28 Jun 2024 10:12:46 -0400 Subject: [PATCH 25/39] add noModelCheckWarning() method to show popup if user generates only C model because no model checks can be run without a python wrapper. add showNoCompileWarning so user can override dialog after first showing --- src/sas/qtgui/Utilities/TabbedModelEditor.py | 34 +++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/src/sas/qtgui/Utilities/TabbedModelEditor.py b/src/sas/qtgui/Utilities/TabbedModelEditor.py index 27f364ae5e..ad6135de96 100644 --- a/src/sas/qtgui/Utilities/TabbedModelEditor.py +++ b/src/sas/qtgui/Utilities/TabbedModelEditor.py @@ -42,6 +42,7 @@ def __init__(self, parent=None, edit_only=False, model=False, load_file=None): self.load_file = load_file.lstrip("//") if load_file else None self.model = model self.is_modified = False + self.showNoCompileWarning = True self.label = None self.file_to_regenerate = "" self.include_polydisperse = False @@ -378,9 +379,15 @@ def updateFromPlugin(self): self.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(False) # Run the model test in sasmodels and check model syntax. Returns error line if checks fail. - error_line = self.checkModel(full_path) - if error_line > 0: - return + if os.path.exists(full_path_py): + error_line = self.checkModel(full_path_py) + if error_line > 0: + return + else: + if self.showNoCompileWarning: + # Show message box that tells user no model checks will be run until a python file of the same name is created in the plugins directory. + self.noModelCheckWarning() + self.editor_widget.setEnabled(True) @@ -556,7 +563,26 @@ def regenerateDocumentation(self): # The regen method is part of the documentation window. If the window is closed, the method no longer exists. if hasattr(self.parent, 'helpWindow'): self.parent.helpWindow.regenerateHtml(self.filename_py) - + + def noModelCheckWarning(self): + """ + Throw popup informing the user that no model checks will be run on a pure C model. + Ask user to acknowledge and give option to not display again. + """ + msgBox = QtWidgets.QMessageBox(self) + msgBox.setIcon(QtWidgets.QMessageBox.Information) + msgBox.setText("No model checks will be run on your C file until a python file of the same name is created in your plugin directory.") + msgBox.setWindowTitle("No Python File Detected") + buttonContinue = msgBox.addButton("OK", QtWidgets.QMessageBox.AcceptRole) + doNotShowAgainCheckbox = QtWidgets.QCheckBox("Do not show again") + msgBox.setCheckBox(doNotShowAgainCheckbox) + + msgBox.exec_() + + if doNotShowAgainCheckbox.isChecked(): + # Update flag to not show popup again while this instance of TabbedModelEditor is open + self.showNoCompileWarning = False + def saveOverrideWarning(self, filename, model_str): """ Throw popup asking user if they want to save the model despite a bad model check. From a43dafa0ff8a9cae385a6628ba831e2709d383b1 Mon Sep 17 00:00:00 2001 From: tsole0 Date: Fri, 28 Jun 2024 13:17:29 -0400 Subject: [PATCH 26/39] retain error formatting if the user decides to override bad model warning --- src/sas/qtgui/Utilities/TabbedModelEditor.py | 22 ++++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/sas/qtgui/Utilities/TabbedModelEditor.py b/src/sas/qtgui/Utilities/TabbedModelEditor.py index ad6135de96..b0dce91c02 100644 --- a/src/sas/qtgui/Utilities/TabbedModelEditor.py +++ b/src/sas/qtgui/Utilities/TabbedModelEditor.py @@ -489,6 +489,7 @@ def updateFromEditor(self): """ Save the current state of the Model Editor """ + clear_error_formatting = True # Assume we will clear error formating (if any) after saving filename = self.filename_py w = self.tabWidget.currentWidget() if not w.is_python: @@ -531,15 +532,18 @@ def updateFromEditor(self): if user_decision == False: # If the user decides to continue editing without saving, return return - - # change the frame colours back - try: - self.c_editor_widget.txtEditor.setStyleSheet("") - self.c_editor_widget.txtEditor.setToolTip("") - except AttributeError: - pass - self.editor_widget.txtEditor.setStyleSheet("") - self.editor_widget.txtEditor.setToolTip("") + else: + clear_error_formatting = False + + if clear_error_formatting: + # change the frame colours back, if errors were fixed + try: + self.c_editor_widget.txtEditor.setStyleSheet("") + self.c_editor_widget.txtEditor.setToolTip("") + except AttributeError: + pass + self.editor_widget.txtEditor.setStyleSheet("") + self.editor_widget.txtEditor.setToolTip("") # Update the tab title self.setTabEdited(False) From 078b2ccea4e70b0ca4cde1e80415e02ca4333b42 Mon Sep 17 00:00:00 2001 From: tsole0 Date: Fri, 28 Jun 2024 14:34:12 -0400 Subject: [PATCH 27/39] expand QTableWidget horizontally to fit area, and ensure that text doesn't wrap in QTableWidget headers --- .../qtgui/Utilities/UI/PluginDefinitionUI.ui | 43 ++++++++++++++++--- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui b/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui index baf882a8cd..1cf634e34a 100755 --- a/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui +++ b/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui @@ -28,6 +28,39 @@ + + QFrame::Sunken + + + false + + + false + + + QAbstractItemView::NoDragDrop + + + Qt::IgnoreAction + + + Qt::ElideRight + + + true + + + false + + + 100 + + + false + + + true + @@ -40,8 +73,7 @@ - Initial -value + Initial value @@ -57,6 +89,9 @@ value + + true + @@ -69,8 +104,7 @@ value - Initial -value + Initial value @@ -198,7 +232,6 @@ li.checked::marker { content: "\2612"; } txtName chkOverwrite txtDescription - tblParams tblParamsPD txtFunction From 4ffa59797c4ef08467989e0538e2795678233c29 Mon Sep 17 00:00:00 2001 From: tsole0 Date: Fri, 28 Jun 2024 15:19:43 -0400 Subject: [PATCH 28/39] simplify and centralize tab creation methods into addTab() and removeTab(). Initialize c_editor_widget in addWidgets(), only add editor widgets if needed, do not add widgets if one of same type already exists. --- src/sas/qtgui/Utilities/TabbedModelEditor.py | 51 ++++++++++++++++---- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/src/sas/qtgui/Utilities/TabbedModelEditor.py b/src/sas/qtgui/Utilities/TabbedModelEditor.py index b0dce91c02..54ff97d537 100644 --- a/src/sas/qtgui/Utilities/TabbedModelEditor.py +++ b/src/sas/qtgui/Utilities/TabbedModelEditor.py @@ -68,11 +68,13 @@ def addWidgets(self): self.setPluginActive(True) self.editor_widget = ModelEditor(self) - # Initially, nothing in the editor + self.c_editor_widget = ModelEditor(self) + # Initially, nothing in the editors self.editor_widget.setEnabled(False) - self.tabWidget.addTab(self.editor_widget, "Model editor") + self.c_editor_widget.setEnabled(False) self.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(False) + # Initially hide form function box self.plugin_widget.formFunctionBox.setVisible(False) @@ -82,6 +84,7 @@ def addWidgets(self): self.plugin_widget.blockSignals(True) # and hide the tab/widget itself self.tabWidget.removeTab(0) + self.addTab("python", "Model Editor") if self.model is not None: self.cmdLoad.setText("Load file...") @@ -98,6 +101,7 @@ def addSignals(self): # signals from tabs self.plugin_widget.modelModified.connect(self.editorModelModified) self.editor_widget.modelModified.connect(self.editorModelModified) + self.c_editor_widget.modelModified.connect(self.editorModelModified) self.plugin_widget.txtName.editingFinished.connect(self.pluginTitleSet) self.plugin_widget.includePolydisperseFuncsSignal.connect(self.includePolydisperseFuncs) self.plugin_widget.omitPolydisperseFuncsSignal.connect(self.omitPolydisperseFuncs) @@ -391,26 +395,27 @@ def updateFromPlugin(self): self.editor_widget.setEnabled(True) - # Update the editor here. + # Update the editor(s) here. # Simple string forced into control. if model['gen_python'] == True: + # Add a tab to TabbedModelEditor for the Python model if not already open + if not self.isWidgetInTab(self.tabWidget, self.editor_widget): + self.addTab("python", Path(self.filename_py).name) + self.editor_widget.blockSignals(True) self.editor_widget.txtEditor.setPlainText(model_str) self.editor_widget.blockSignals(False) + if model['gen_c'] == True: - # Add a tab to TabbedModelEditor for the C model - c_display_name = Path(self.filename_c).name - self.c_editor_widget = ModelEditor(self, is_python=False) - self.tabWidget.addTab(self.c_editor_widget, c_display_name) + # Add a tab to TabbedModelEditor for the C model if not already open + if not self.isWidgetInTab(self.tabWidget, self.c_editor_widget): + self.addTab("c", Path(self.filename_c).name) # Update the editor self.c_editor_widget.blockSignals(True) self.c_editor_widget.txtEditor.setPlainText(c_model_str) self.c_editor_widget.blockSignals(False) - # Connect 'modified' signal - self.c_editor_widget.modelModified.connect(self.editorModelModified) - # Set the widget title self.setTabEdited(False) @@ -687,6 +692,32 @@ def getModel(self): Retrieves plugin model from the currently open tab """ return self.tabWidget.currentWidget().getModel() + + def addTab(self, filetype, name): + """ + Add a tab to the tab widget + :param filetype: filetype of tab to add: "python" or "c" + :param name: name to display on tab + """ + if filetype == "python": + #display_name = Path(self.filename_py).name + self.editor_widget = ModelEditor(self, is_python=True) + self.tabWidget.addTab(self.editor_widget, name) + elif filetype == "c": + #display_name = Path(self.filename_c).name + self.c_editor_widget = ModelEditor(self, is_python=False) + self.tabWidget.addTab(self.c_editor_widget, name) + + def removeTab(self, filetype): + """ + Remove a tab from the tab widget. + Assume that the tab to remove exists. + :param filetype: filetype of tab to remove: "python" or "c" + """ + if filetype == "python": + self.tabWidget.removeTab(self.tabWidget.indexOf(self.editor_widget)) + elif filetype == "c": + self.tabWidget.removeTab(self.tabWidget.indexOf(self.c_editor_widget)) @classmethod def isWidgetInTab(cls, tabWidget, widget_to_check): From 726eb1d4473fe67f9a32278e37e3b099b81419f6 Mon Sep 17 00:00:00 2001 From: tsole0 Date: Fri, 28 Jun 2024 15:29:37 -0400 Subject: [PATCH 29/39] update tab names if user changes model name --- src/sas/qtgui/Utilities/TabbedModelEditor.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/sas/qtgui/Utilities/TabbedModelEditor.py b/src/sas/qtgui/Utilities/TabbedModelEditor.py index 54ff97d537..e4fa7cedfa 100644 --- a/src/sas/qtgui/Utilities/TabbedModelEditor.py +++ b/src/sas/qtgui/Utilities/TabbedModelEditor.py @@ -401,6 +401,9 @@ def updateFromPlugin(self): # Add a tab to TabbedModelEditor for the Python model if not already open if not self.isWidgetInTab(self.tabWidget, self.editor_widget): self.addTab("python", Path(self.filename_py).name) + elif self.tabWidget.tabText(self.tabWidget.indexOf(self.editor_widget)) != Path(self.filename_py).name: + # If title of tab is not what the filename is, update the tab title + self.tabWidget.setTabText(self.tabWidget.indexOf(self.editor_widget), Path(self.filename_py).name) self.editor_widget.blockSignals(True) self.editor_widget.txtEditor.setPlainText(model_str) @@ -410,6 +413,9 @@ def updateFromPlugin(self): # Add a tab to TabbedModelEditor for the C model if not already open if not self.isWidgetInTab(self.tabWidget, self.c_editor_widget): self.addTab("c", Path(self.filename_c).name) + elif self.tabWidget.tabText(self.tabWidget.indexOf(self.c_editor_widget)) != Path(self.filename_c).name: + # If title of tab is not what the filename is, update the tab title + self.tabWidget.setTabText(self.tabWidget.indexOf(self.c_editor_widget), Path(self.filename_c).name) # Update the editor self.c_editor_widget.blockSignals(True) From 778bffc403253963f3b59c9624a2be293fc137b9 Mon Sep 17 00:00:00 2001 From: tsole0 Date: Tue, 2 Jul 2024 15:13:39 -0400 Subject: [PATCH 30/39] fix bug preventing editing models in model editor because editorModelModified signal was not connected --- src/sas/qtgui/Utilities/TabbedModelEditor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sas/qtgui/Utilities/TabbedModelEditor.py b/src/sas/qtgui/Utilities/TabbedModelEditor.py index e4fa7cedfa..d1fb7ac05e 100644 --- a/src/sas/qtgui/Utilities/TabbedModelEditor.py +++ b/src/sas/qtgui/Utilities/TabbedModelEditor.py @@ -527,7 +527,6 @@ def updateFromEditor(self): check_model = True elif not w.is_python and self.is_python: # Set full_path to the .py file so that we can run a model check on it (the .py model should link to the .c model) - print(self.filename_py) full_path = self.filename_py check_model = True @@ -709,10 +708,12 @@ def addTab(self, filetype, name): #display_name = Path(self.filename_py).name self.editor_widget = ModelEditor(self, is_python=True) self.tabWidget.addTab(self.editor_widget, name) + self.editor_widget.modelModified.connect(self.editorModelModified) elif filetype == "c": #display_name = Path(self.filename_c).name self.c_editor_widget = ModelEditor(self, is_python=False) self.tabWidget.addTab(self.c_editor_widget, name) + self.c_editor_widget.modelModified.connect(self.editorModelModified) def removeTab(self, filetype): """ From b9e43f8ee6604bf1cd560b6ba488785782208272 Mon Sep 17 00:00:00 2001 From: tsole0 Date: Wed, 3 Jul 2024 10:52:09 -0400 Subject: [PATCH 31/39] force user to generate Python model if no Python model is detected in user plugins directory, display helper text and disable checkbox. --- src/sas/qtgui/Utilities/PluginDefinition.py | 29 ++ src/sas/qtgui/Utilities/TabbedModelEditor.py | 4 + .../qtgui/Utilities/UI/PluginDefinitionUI.ui | 490 +++++++++--------- 3 files changed, 283 insertions(+), 240 deletions(-) diff --git a/src/sas/qtgui/Utilities/PluginDefinition.py b/src/sas/qtgui/Utilities/PluginDefinition.py index 0e3b8b1c16..feb856807d 100644 --- a/src/sas/qtgui/Utilities/PluginDefinition.py +++ b/src/sas/qtgui/Utilities/PluginDefinition.py @@ -1,9 +1,12 @@ +import os.path + from PySide6 import QtCore from PySide6 import QtGui from PySide6 import QtWidgets from sas.qtgui.Utilities.UI.PluginDefinitionUI import Ui_PluginDefinition from sas.qtgui.Utilities import GuiUtils +from sas.sascalc.fit.models import find_plugins_dir # txtName # txtDescription @@ -21,10 +24,15 @@ class PluginDefinition(QtWidgets.QDialog, Ui_PluginDefinition): modelModified = QtCore.Signal() omitPolydisperseFuncsSignal = QtCore.Signal() includePolydisperseFuncsSignal = QtCore.Signal() + enablePyCheckboxSignal = QtCore.Signal() + def __init__(self, parent=None): super(PluginDefinition, self).__init__(parent) + self.setupUi(self) + self.infoLabel.setVisible(False) + # globals self.initializeModel() # internal representation of the parameter list @@ -123,12 +131,16 @@ def addSignals(self): self.chkOverwrite.toggled.connect(self.onOverwrite) self.chkGenPython.toggled.connect(self.onGenPython) self.chkGenC.toggled.connect(self.onGenC) + self.enablePyCheckboxSignal.connect(lambda: self.checkPyModelExists(self.model['filename'])) def onPluginNameChanged(self): """ Respond to changes in plugin name """ self.model['filename'] = self.txtName.text() + + self.checkPyModelExists(self.model['filename']) + self.modelModified.emit() def onDescriptionChanged(self): @@ -254,6 +266,23 @@ def onGenC(self): """ self.model['gen_c'] = self.chkGenC.isChecked() self.modelModified.emit() + + def checkPyModelExists(self, filename): + """ + Checks if a Python model exists in the user plugin directory and forces enabling Python checkbox if not + :param filename: name of the file (without extension) + """ + if not os.path.exists(os.path.join(find_plugins_dir(), filename + '.py')): + # If the user has not yet created a Python file for a specific filename, then force them to create one + self.chkGenPython.setChecked(True) + self.chkGenPython.setEnabled(False) + self.infoLabel.setText("No Python model of the same name detected. Generating Python model is required.") + self.infoLabel.setVisible(True) + else: + self.infoLabel.setVisible(False) + self.chkGenPython.setEnabled(True) + return os.path.exists(os.path.join(find_plugins_dir(), filename + '.py')) + def getModel(self): """ diff --git a/src/sas/qtgui/Utilities/TabbedModelEditor.py b/src/sas/qtgui/Utilities/TabbedModelEditor.py index d1fb7ac05e..38944a7336 100644 --- a/src/sas/qtgui/Utilities/TabbedModelEditor.py +++ b/src/sas/qtgui/Utilities/TabbedModelEditor.py @@ -25,6 +25,7 @@ class TabbedModelEditor(QtWidgets.QDialog, Ui_TabbedModelEditor): Once the model is defined, it can be saved as a plugin. """ # Signals for intertab communication plugin -> editor + def __init__(self, parent=None, edit_only=False, model=False, load_file=None): super(TabbedModelEditor, self).__init__(parent._parent) @@ -382,6 +383,9 @@ def updateFromPlugin(self): # disable "Apply" self.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(False) + # Allow user to toggle 'Generate Python model' checkbox + self.plugin_widget.enablePyCheckboxSignal.emit() + # Run the model test in sasmodels and check model syntax. Returns error line if checks fail. if os.path.exists(full_path_py): error_line = self.checkModel(full_path_py) diff --git a/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui b/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui index 1cf634e34a..3fe3289619 100755 --- a/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui +++ b/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui @@ -1,240 +1,250 @@ - - - PluginDefinition - - - - 0 - 0 - 723 - 784 - - - - Plugin Definition - - - - - - Fit parameters - - - - - - Non-polydisperse - - - - - - QFrame::Sunken - - - false - - - false - - - QAbstractItemView::NoDragDrop - - - Qt::IgnoreAction - - - Qt::ElideRight - - - true - - - false - - - 100 - - - false - - - true - - - - - - - - - Parameters - - - - - Initial value - - - - - - - - - - - Polydisperse - - - - - - true - - - - - - - - - Parameters - - - - - Initial value - - - - - - - - - - - - - - Plugin name - - - - - - Enter a plugin name - - - - - - - - - false - - - Generate Python model - - - true - - - - - - - Generate C model - - - - - - - - - Overwrite existing plugin model of this name - - - - - - - - - - Description - - - - - - Enter a description of the model - - - - - - - - - - Enter function for calculating volume of the particle: - - - - - - IBeamCursor - - - false - - - - - - - - - - Enter function for calculating scattering intensity I(Q): - - - - - - IBeamCursor - - - false - - - <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><meta charset="utf-8" /><style type="text/css"> -p, li { white-space: pre-wrap; } -hr { height: 1px; border-width: 0; } -li.unchecked::marker { content: "\2610"; } -li.checked::marker { content: "\2612"; } -</style></head><body style=" font-family:'Segoe UI'; font-size:9pt; font-weight:400; font-style:normal;"> -<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'MS Shell Dlg 2'; font-size:7.8pt;"><br /></p></body></html> - - - - - - - - - - txtName - chkOverwrite - txtDescription - tblParamsPD - txtFunction - - - - + + + PluginDefinition + + + + 0 + 0 + 723 + 784 + + + + Plugin Definition + + + + + + Fit parameters + + + + + + Non-polydisperse + + + + + + QFrame::Sunken + + + false + + + false + + + QAbstractItemView::NoDragDrop + + + Qt::IgnoreAction + + + Qt::ElideRight + + + true + + + false + + + 100 + + + false + + + true + + + + + + + + + Parameters + + + + + Initial value + + + + + + + + + + + Polydisperse + + + + + + true + + + + + + + + + Parameters + + + + + Initial value + + + + + + + + + + + + + + Plugin name + + + + + + Enter a plugin name + + + + + + + + + false + + + Generate Python model + + + true + + + + + + + Generate C model + + + + + + + + + Overwrite existing plugin model of this name + + + + + + + font-size: 8pt + + + + + + + + + + + + + Description + + + + + + Enter a description of the model + + + + + + + + + + Enter function for calculating volume of the particle: + + + + + + IBeamCursor + + + false + + + + + + + + + + Enter function for calculating scattering intensity I(Q): + + + + + + IBeamCursor + + + false + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><meta charset="utf-8" /><style type="text/css"> +p, li { white-space: pre-wrap; } +hr { height: 1px; border-width: 0; } +li.unchecked::marker { content: "\2610"; } +li.checked::marker { content: "\2612"; } +</style></head><body style=" font-family:'Segoe UI'; font-size:9pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'MS Shell Dlg 2'; font-size:7.8pt;"><br /></p></body></html> + + + + + + + + + + txtName + chkOverwrite + txtDescription + tblParamsPD + txtFunction + + + + From 8ab80a81891bf4ee41dc90b108d85a02688695cb Mon Sep 17 00:00:00 2001 From: tsole0 Date: Wed, 3 Jul 2024 10:53:03 -0400 Subject: [PATCH 32/39] cause modelModified signal to be emitted when text changed instead of editing finished --- src/sas/qtgui/Utilities/PluginDefinition.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sas/qtgui/Utilities/PluginDefinition.py b/src/sas/qtgui/Utilities/PluginDefinition.py index feb856807d..0a8e0e4897 100644 --- a/src/sas/qtgui/Utilities/PluginDefinition.py +++ b/src/sas/qtgui/Utilities/PluginDefinition.py @@ -120,8 +120,8 @@ def addSignals(self): """ Define slots for widget signals """ - self.txtName.editingFinished.connect(self.onPluginNameChanged) - self.txtDescription.editingFinished.connect(self.onDescriptionChanged) + self.txtName.textChanged.connect(self.onPluginNameChanged) + self.txtDescription.textChanged.connect(self.onDescriptionChanged) self.tblParams.cellChanged.connect(self.onParamsChanged) self.tblParamsPD.cellChanged.connect(self.onParamsPDChanged) # QTextEdit doesn't have a signal for edit finish, so we respond to text changed. From 0fc91ff113ad0536df327ca7c83766649b3d0d03 Mon Sep 17 00:00:00 2001 From: tsole0 Date: Wed, 3 Jul 2024 15:02:57 -0400 Subject: [PATCH 33/39] Adds comment explaining built-in functions to C template --- src/sas/qtgui/Utilities/TabbedModelEditor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/sas/qtgui/Utilities/TabbedModelEditor.py b/src/sas/qtgui/Utilities/TabbedModelEditor.py index 38944a7336..718338478f 100644 --- a/src/sas/qtgui/Utilities/TabbedModelEditor.py +++ b/src/sas/qtgui/Utilities/TabbedModelEditor.py @@ -996,6 +996,9 @@ def form_volume({args}): // 3. Ensure a python file links to your C model (source = ['filename.c']) // 4. Press 'Apply' or 'Save' to save your model and run a model check (note that the model check will fail if there is no python file of the same name in your plugins directory) +// NOTE: SasView has many built-in functions that you can use in your C model--for example, spherical Bessel functions (lib/sas_3j1x_x.c), Gaussian quadrature (lib/sas_J1.c), and more. Check documentation for full list. +// NOTE: It also has many common constants following the C99 standard, such as M_PI, M_SQRT1_2, and M_E. Check documentation for full list. + ''' C_PD_TEMPLATE = '''\ From b369d56726513f86c04be7e6238b41f73b5b4039 Mon Sep 17 00:00:00 2001 From: tsole0 Date: Wed, 3 Jul 2024 15:03:28 -0400 Subject: [PATCH 34/39] edits previously mentioned comment for clarity --- src/sas/qtgui/Utilities/TabbedModelEditor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sas/qtgui/Utilities/TabbedModelEditor.py b/src/sas/qtgui/Utilities/TabbedModelEditor.py index 718338478f..0b8e2326a6 100644 --- a/src/sas/qtgui/Utilities/TabbedModelEditor.py +++ b/src/sas/qtgui/Utilities/TabbedModelEditor.py @@ -996,7 +996,8 @@ def form_volume({args}): // 3. Ensure a python file links to your C model (source = ['filename.c']) // 4. Press 'Apply' or 'Save' to save your model and run a model check (note that the model check will fail if there is no python file of the same name in your plugins directory) -// NOTE: SasView has many built-in functions that you can use in your C model--for example, spherical Bessel functions (lib/sas_3j1x_x.c), Gaussian quadrature (lib/sas_J1.c), and more. Check documentation for full list. +// NOTE: SasView has many built-in functions that you can use in your C model--for example, spherical Bessel functions (lib/sas_3j1x_x.c), Gaussian quadrature (lib/sas_J1.c), and more. +// To include, add their filename to the `source = []` list in the python file linking to your C model. // NOTE: It also has many common constants following the C99 standard, such as M_PI, M_SQRT1_2, and M_E. Check documentation for full list. ''' From 2f1d5da29535fcc15910a5fec0bb59e5c4cadbb2 Mon Sep 17 00:00:00 2001 From: tsole0 Date: Fri, 5 Jul 2024 15:22:02 -0400 Subject: [PATCH 35/39] reflect user changes to model editor in plugin editor. use ast module to find function text, load model using importlib.util to find access variables. --- src/sas/qtgui/Utilities/PluginDefinition.py | 39 +++++++++++- src/sas/qtgui/Utilities/TabbedModelEditor.py | 65 ++++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/src/sas/qtgui/Utilities/PluginDefinition.py b/src/sas/qtgui/Utilities/PluginDefinition.py index 0a8e0e4897..2a575a516e 100644 --- a/src/sas/qtgui/Utilities/PluginDefinition.py +++ b/src/sas/qtgui/Utilities/PluginDefinition.py @@ -1,4 +1,5 @@ import os.path +import logging from PySide6 import QtCore from PySide6 import QtGui @@ -25,6 +26,10 @@ class PluginDefinition(QtWidgets.QDialog, Ui_PluginDefinition): omitPolydisperseFuncsSignal = QtCore.Signal() includePolydisperseFuncsSignal = QtCore.Signal() enablePyCheckboxSignal = QtCore.Signal() + sendNewParamSignal = QtCore.Signal(list) + sendNewDescriptionSignal = QtCore.Signal(str) + sendNewIqSignal = QtCore.Signal(str) + sendNewFormVolumeSignal = QtCore.Signal(str) def __init__(self, parent=None): super(PluginDefinition, self).__init__(parent) @@ -132,6 +137,10 @@ def addSignals(self): self.chkGenPython.toggled.connect(self.onGenPython) self.chkGenC.toggled.connect(self.onGenC) self.enablePyCheckboxSignal.connect(lambda: self.checkPyModelExists(self.model['filename'])) + self.sendNewParamSignal.connect(self.updateParamTableFromEditor) + self.sendNewDescriptionSignal.connect(lambda description: self.txtDescription.setText(description)) + self.sendNewIqSignal.connect(lambda iq: self.txtFunction.setPlainText(iq)) + self.sendNewFormVolumeSignal.connect(lambda form_volume: self.txtFormVolumeFunction.setPlainText(form_volume)) def onPluginNameChanged(self): """ @@ -282,7 +291,35 @@ def checkPyModelExists(self, filename): self.infoLabel.setVisible(False) self.chkGenPython.setEnabled(True) return os.path.exists(os.path.join(find_plugins_dir(), filename + '.py')) - + + def updateParamTableFromEditor(self, param_list): + """ + Add parameters sent from model editor to the parameter tables + :param param_list: list of parameters to add to the parameter tables [name, default_value, type] + """ + updated_params_non_pd = [param for param in param_list if param[2] != 'volume'] + updated_params_pd = [param for param in param_list if param[2] == 'volume'] + + # Prepare the table for updating + self.tblParams.blockSignals(True) + self.tblParamsPD.blockSignals(True) + self.tblParams.clearContents() + self.tblParamsPD.clearContents() + self.tblParams.setRowCount(len(updated_params_non_pd) + 1) + self.tblParamsPD.setRowCount(len(updated_params_pd) + 1) + + # Iterate over cells and add the new parameters to them + for table, params in [[self.tblParams, updated_params_non_pd], [self.tblParamsPD, updated_params_pd]]: + for row, param in enumerate(params): + for column in range(2): + if column < len(param): # Check if the column index is within the bounds of param length + item = QtWidgets.QTableWidgetItem(str(param[column])) + table.setItem(row, column, item) + else: + logging.info(f"Missing data for Row {row}, Column {column}") + + self.tblParams.blockSignals(False) + self.tblParamsPD.blockSignals(False) def getModel(self): """ diff --git a/src/sas/qtgui/Utilities/TabbedModelEditor.py b/src/sas/qtgui/Utilities/TabbedModelEditor.py index 0b8e2326a6..56147d5792 100644 --- a/src/sas/qtgui/Utilities/TabbedModelEditor.py +++ b/src/sas/qtgui/Utilities/TabbedModelEditor.py @@ -6,6 +6,7 @@ import datetime import logging import traceback +import importlib.util from PySide6 import QtWidgets, QtCore, QtGui from pathlib import Path @@ -565,6 +566,10 @@ def updateFromEditor(self): # Notify listeners, since the plugin name might have changed self.parent.communicate.customModelDirectoryChanged.emit() + if self.isWidgetInTab(self.tabWidget, self.plugin_widget): + # Attempt to update the plugin widget with updated model information + self.updateToPlugin(full_path) + # notify the user msg = str(filename) + " successfully saved." self.parent.communicate.statusBarUpdateSignal.emit(msg) @@ -730,6 +735,34 @@ def removeTab(self, filetype): elif filetype == "c": self.tabWidget.removeTab(self.tabWidget.indexOf(self.c_editor_widget)) + def updateToPlugin(self, full_path): + """ + Update the plugin tab with new info from the model editor + """ + self.model = self.getModel() + model_text = self.model['text'] + + spec = importlib.util.spec_from_file_location("model", full_path) # Easier to import than use regex + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + description = module.description + parameters = module.parameters + iq_text = self.extractFunctionBody(model_text, "Iq") + form_volume_text = self.extractFunctionBody(model_text, "form_volume") + + slim_param_list = [] + for param in parameters: + if param[0]: + # Extract parameter name, default value, and whether "volume" parameter + slim_param_list.append([param[0], param[2], param[4]]) + + # Send parameters in a list of lists format to listening widget + self.plugin_widget.sendNewParamSignal.emit(slim_param_list) + self.plugin_widget.sendNewDescriptionSignal.emit(description) + self.plugin_widget.sendNewIqSignal.emit(iq_text) + self.plugin_widget.sendNewFormVolumeSignal.emit(form_volume_text) + @classmethod def isWidgetInTab(cls, tabWidget, widget_to_check): """ @@ -894,6 +927,38 @@ def strFromParamDict(cls, param_dict): value = 1 param_str += params[0] + " = " + str(value) + "\n" return param_str + + @classmethod + def extractFunctionBody(cls, source_code, function_name): + """ + Extract the body of a function from a model file + """ + tree = ast.parse(source_code) + extractor = cls.FunctionBodyExtractor(function_name) + extractor.visit(tree) + return extractor.function_body_source + + class FunctionBodyExtractor(ast.NodeVisitor): + """ + Class to extract the body of a function from a model file + """ + def __init__(self, function_name): + self.function_name = function_name + self.function_body_source = None + + def visit_FunctionDef(self, node): + """ + Extract the source code of the function with the given name. + NOTE: Do NOT change the name of this method-- visit_ is a prefix that ast.NodeVisitor uses + """ + if node.name == self.function_name: + body = node.body + # Check if the first statement is an Expr node containing a constant (docstring) + if body and isinstance(body[0], ast.Expr) and isinstance(body[0].value, ast.Constant): + body = body[1:] # Exclude the docstring + self.function_body_source = ast.unparse(body) + # Continue traversing to find nested functions or other function definitions + self.generic_visit(node) CUSTOM_TEMPLATE = '''\ From 3551a1f3335008da33f56fb45373c165b196fe99 Mon Sep 17 00:00:00 2001 From: tsole0 Date: Fri, 5 Jul 2024 15:29:46 -0400 Subject: [PATCH 36/39] fix indentation in auto-generated c models --- src/sas/qtgui/Utilities/TabbedModelEditor.py | 22 ++++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/sas/qtgui/Utilities/TabbedModelEditor.py b/src/sas/qtgui/Utilities/TabbedModelEditor.py index 56147d5792..117964f823 100644 --- a/src/sas/qtgui/Utilities/TabbedModelEditor.py +++ b/src/sas/qtgui/Utilities/TabbedModelEditor.py @@ -1089,9 +1089,9 @@ def form_volume({args}): static void Fq(double q, - double *F1, - double *F2, - {args}) // Remove arguments as needed + double *F1, + double *F2, + {args}) // Remove arguments as needed {{ // Define F(Q) calculations here... // IMPORTANT: You do not have to define Iq if your model uses Fq for beta approximation; the *F2 value is and equivalent to the output of Iq. @@ -1103,7 +1103,7 @@ def form_volume({args}): static double Iq(double q, - {args}) // Remove arguments as needed + {args}) // Remove arguments as needed {{ // Define I(Q) calculations here for models independent of shape orientation // IMPORTANT: Only define ONE calculation for I(Q): either Iq, Iqac, or Iqabc; remove others. @@ -1112,8 +1112,8 @@ def form_volume({args}): static double Iqac(double qab, - double qc, - {args}) // Remove arguments as needed + double qc, + {args}) // Remove arguments as needed {{ // Define I(Q) calculations here for models dependent on shape orientation in which the shape is rotationally symmetric about *c* axis // Note: *psi* angle not needed for shapes symmetric about *c* axis @@ -1123,9 +1123,9 @@ def form_volume({args}): static double Iqabc(double qa, - double qb, - double qc, - {args}) // Remove arguments as needed + double qb, + double qc, + {args}) // Remove arguments as needed {{ // Define I(Q) calculations here for models dependent on shape orientation in all three axes // IMPORTANT: Only define ONE calculation for I(Q): either Iq, Iqac, Iqabc, or Iqxy; remove others. @@ -1134,8 +1134,8 @@ def form_volume({args}): static double Iqxy(double qx, - double qy, - {args}) // Remove arguments as needed + double qy, + {args}) // Remove arguments as needed {{ // Define I(Q) calculations here for 2D magnetic models. // WARNING: The use of Iqxy is generally discouraged; Use Iqabc instead for its better orientational averaging and documentation for details. From 50962a4fce7a732df3bca1b376b248dd7cdd21da Mon Sep 17 00:00:00 2001 From: tsole0 Date: Fri, 5 Jul 2024 16:16:27 -0400 Subject: [PATCH 37/39] make comments multi-line to improve readability in editor window --- src/sas/qtgui/Utilities/TabbedModelEditor.py | 70 +++++++++++++------- 1 file changed, 47 insertions(+), 23 deletions(-) diff --git a/src/sas/qtgui/Utilities/TabbedModelEditor.py b/src/sas/qtgui/Utilities/TabbedModelEditor.py index 117964f823..38ede07cd7 100644 --- a/src/sas/qtgui/Utilities/TabbedModelEditor.py +++ b/src/sas/qtgui/Utilities/TabbedModelEditor.py @@ -1045,25 +1045,38 @@ def form_volume({args}): ''' C_COMMENT_TEMPLATE = '''\ -//:::Custom C model template::: +// :::Custom C model template::: // This is a template for a custom C model. -// C Models are used for a variety of reasons in SasView, including better performance and the ability to perform calculations not possible in Python. -// For example, all oriented and magnetic models, as well as most models using structure factor calculations, are written in C. +// C Models are used for a variety of reasons in SasView, including better +// performance and the ability to perform calculations not possible in Python. +// For example, all oriented and magnetic models, as well as most models +// using structure factor calculations, are written in C. // HOW TO USE THIS TEMPLATE: -// 1. Determine which functions you will need to perform your calculations; delete unused functions. +// 1. Determine which functions you will need to perform your calculations; +// delete unused functions. // 1.1 Note that you must define either Iq, Fq, or one of Iqac, Iqabc: -// Iq if your model does not use orientation parameters or use structure factor calculations; +// Iq if your model does not use orientation parameters or use structure +// factor calculations; // Fq if your model uses structure factor calculations; // Iqac or Iqabc if your model uses orientation parameters/is magnetic; -// Fq AND Iqac/Iqabc if your model uses orientation parameters/is magnetic and has structure factor calculations. -// 2. Write C code independently of this editor and paste it into the appropriate functions. -// 2.1 Note that the C editor does not support C syntax checking, so writing C code directly into the SasView editor is not reccomended. +// Fq AND Iqac/Iqabc if your model uses orientation parameters or +// is magnetic and has structure factor calculations. +// 2. Write C code independently of this editor and paste it into the +// appropriate functions. +// 2.1 Note that the C editor does not support C syntax checking, so +// writing C code directly into the SasView editor is not reccomended. // 3. Ensure a python file links to your C model (source = ['filename.c']) -// 4. Press 'Apply' or 'Save' to save your model and run a model check (note that the model check will fail if there is no python file of the same name in your plugins directory) - -// NOTE: SasView has many built-in functions that you can use in your C model--for example, spherical Bessel functions (lib/sas_3j1x_x.c), Gaussian quadrature (lib/sas_J1.c), and more. -// To include, add their filename to the `source = []` list in the python file linking to your C model. -// NOTE: It also has many common constants following the C99 standard, such as M_PI, M_SQRT1_2, and M_E. Check documentation for full list. +// 4. Press 'Apply' or 'Save' to save your model and run a model check +// (note that the model check will fail if there is no python file of the +// same name in your plugins directory) +// +// NOTE: SasView has many built-in functions that you can use in your C model-- +// for example, spherical Bessel functions (lib/sas_3j1x_x.c), Gaussian +// quadrature (lib/sas_J1.c), and more. +// To include, add their filename to the `source = []` list in the python +// file linking to your C model. +// NOTE: It also has many common constants following the C99 standard, such as +// M_PI, M_SQRT1_2, and M_E. Check documentation for full list. ''' @@ -1094,9 +1107,12 @@ def form_volume({args}): {args}) // Remove arguments as needed {{ // Define F(Q) calculations here... - // IMPORTANT: You do not have to define Iq if your model uses Fq for beta approximation; the *F2 value is and equivalent to the output of Iq. - // IMPORTANT: You may use Fq instead of Iq even if you do not need (*F1) for beta approximation, but this is not recommended. - // IMPORTANT: Additionally, you must still define Iqac or Iqabc if your model has orientation parameters. + //IMPORTANT: You do not have to define Iq if your model uses Fq for + // beta approximation; the *F2 value is F(Q)^2 and equivalent to + // the output of Iq. You may use Fq instead of Iq even if you do + // not need F(Q) (*F1) for beta approximation, but this is not recommended. + // Additionally, you must still define Iqac or Iqabc if your + // model has orientation parameters. *F1 = 0.0; *F2 = 0.0; }} @@ -1106,7 +1122,8 @@ def form_volume({args}): {args}) // Remove arguments as needed {{ // Define I(Q) calculations here for models independent of shape orientation - // IMPORTANT: Only define ONE calculation for I(Q): either Iq, Iqac, or Iqabc; remove others. + // IMPORTANT: Only define ONE calculation for I(Q): either Iq, Iqac, or Iqabc; + // remove others. return 1.0; }} @@ -1115,9 +1132,11 @@ def form_volume({args}): double qc, {args}) // Remove arguments as needed {{ - // Define I(Q) calculations here for models dependent on shape orientation in which the shape is rotationally symmetric about *c* axis + // Define I(Q) calculations here for models dependent on shape orientation in + // which the shape is rotationally symmetric about *c* axis. // Note: *psi* angle not needed for shapes symmetric about *c* axis - // IMPORTANT: Only define ONE calculation for I(Q): either Iq, Iqac, Iqabc, or Iqxy; remove others. + // IMPORTANT: Only define ONE calculation for I(Q): either Iq, Iqac, Iqabc, or Iqxy; + // remove others. return 1.0; }} @@ -1127,8 +1146,10 @@ def form_volume({args}): double qc, {args}) // Remove arguments as needed {{ - // Define I(Q) calculations here for models dependent on shape orientation in all three axes - // IMPORTANT: Only define ONE calculation for I(Q): either Iq, Iqac, Iqabc, or Iqxy; remove others. + // Define I(Q) calculations here for models dependent on shape orientation in + // all three axes. + // IMPORTANT: Only define ONE calculation for I(Q): either Iq, Iqac, Iqabc, or Iqxy; + // remove others. return 1.0; }} @@ -1138,8 +1159,11 @@ def form_volume({args}): {args}) // Remove arguments as needed {{ // Define I(Q) calculations here for 2D magnetic models. - // WARNING: The use of Iqxy is generally discouraged; Use Iqabc instead for its better orientational averaging and documentation for details. - // IMPORTANT: Only define ONE calculation for I(Q): either Iq, Iqac, Iqabc, or Iqxy; remove others. + // WARNING: The use of Iqxy is generally discouraged; Use Iqabc instead + // for its better orientational averaging and documentation for details. + // IMPORTANT: Only define ONE calculation for I(Q): either Iq, Iqac, Iqabc, or Iqxy; + // remove others. + return 1.0; }} """ From c0591a0effbe0bef7b6a8ec8ea0d5eb75ca93bcd Mon Sep 17 00:00:00 2001 From: tsole0 Date: Mon, 9 Sep 2024 14:25:07 -0400 Subject: [PATCH 38/39] add checkboxes for boolean flags in model editor --- src/sas/qtgui/Utilities/PluginDefinition.py | 6 ++ src/sas/qtgui/Utilities/TabbedModelEditor.py | 60 ++++++++++++++----- .../qtgui/Utilities/UI/PluginDefinitionUI.ui | 40 +++++++++++++ 3 files changed, 91 insertions(+), 15 deletions(-) diff --git a/src/sas/qtgui/Utilities/PluginDefinition.py b/src/sas/qtgui/Utilities/PluginDefinition.py index 2a575a516e..908dc4798c 100644 --- a/src/sas/qtgui/Utilities/PluginDefinition.py +++ b/src/sas/qtgui/Utilities/PluginDefinition.py @@ -142,6 +142,12 @@ def addSignals(self): self.sendNewIqSignal.connect(lambda iq: self.txtFunction.setPlainText(iq)) self.sendNewFormVolumeSignal.connect(lambda form_volume: self.txtFormVolumeFunction.setPlainText(form_volume)) + #Boolean flags + self.chkSingle.clicked.connect(self.modelModified.emit) + self.chkOpenCL.clicked.connect(self.modelModified.emit) + self.chkStructure.clicked.connect(self.modelModified.emit) + self.chkFQ.clicked.connect(self.modelModified.emit) + def onPluginNameChanged(self): """ Respond to changes in plugin name diff --git a/src/sas/qtgui/Utilities/TabbedModelEditor.py b/src/sas/qtgui/Utilities/TabbedModelEditor.py index 38ede07cd7..fac0703e83 100644 --- a/src/sas/qtgui/Utilities/TabbedModelEditor.py +++ b/src/sas/qtgui/Utilities/TabbedModelEditor.py @@ -813,6 +813,48 @@ def generatePyModel(self, model, fname): """ generate model from the current plugin state """ + + def formatPythonFlags(): + """Get python flags for model and format into text""" + header = "\n# Optional flags (can be removed). Read documentation by pressing 'Help' for more information.\n\n" + flag_string = header + checkbox_defaults = { + 'chkSingle': True, + 'chkOpenCL': False, + 'chkStructure': False, + 'chkFQ': False + } + # Get the values of the checkboxes + checkbox_values = {} + for name in checkbox_defaults.keys(): + checkbox_values[name] = getattr(self.plugin_widget, name).isChecked() + # Create output string + for name in checkbox_values.keys(): + # Check to see if the checkbox is set to a non-default value + if checkbox_defaults[name] != checkbox_values[name]: + match name: + case 'chkSingle': + flag_string += """\ +# single = True indicates that the model can be run using single precision floating point values. Defaults to True. +single = True\n\n""" + case 'chkOpenCL': + flag_string += """\ +# opencl = False indicates that the model should not be run using OpenCL. Defaults to False. +opencl = False\n\n""" + case 'chkStructure': + flag_string += """\ +# structure_factor = False indicates that the model cannot be used as a structure factor to account for interactions between particles. Defaults to False. +structure_factor = False\n\n""" + case 'chkFQ': + flag_string += """\ +# have_fq = False indicates that the model does not define F(Q) calculations in a linked C model. Note that F(Q) calculations are only necessary for accomadating beta approximation. Defaults to False. +have_fq = False\n\n""" + + if flag_string == header: + # If no flags are set, do not include the header + flag_string = "" + return flag_string.rstrip() + "\n" # Remove trailing newline + name = model['filename'] if not name: model['filename'] = fname @@ -825,7 +867,8 @@ def generatePyModel(self, model, fname): model_text = CUSTOM_TEMPLATE.format(name = name, title = 'User model for ' + name, description = desc_str, - date = datetime.datetime.now().strftime('%Y-%m-%d') + date = datetime.datetime.now().strftime('%Y-%m-%d'), + flags= formatPythonFlags() ) # Write out parameters @@ -987,20 +1030,7 @@ def visit_FunctionDef(self, node): name = "{name}" title = "{title}" description = """{description}""" - -# Optional flags (can be removed). Read documentation by pressing 'Help' for more information. - -# single = True indicates that the model can be run using single precision floating point values. Defaults to True. -single = True - -# opencl = False indicates that the model should not be run using OpenCL. Defaults to False. -opencl = False - -# structure_factor = False indicates that the model cannot be used as a structure factor to account for interactions between particles. Defaults to False. -structure_factor = False - -# have_fq = False indicates that the model does not define F(Q) calculations in a linked C model. Note that F(Q) calculations are only necessary for accomadating beta approximation. Defaults to False. -have_fq = False +{flags} ''' ER_VR_TEMPLATE = '''\ diff --git a/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui b/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui index 3fe3289619..b9fe568f7d 100755 --- a/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui +++ b/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui @@ -236,6 +236,46 @@ li.checked::marker { content: "\2612"; } + + + + Model Options + + + + + + Can use OpenCL + + + + + + + Can use single precision floating point values + + + true + + + + + + + Has F(Q) calculations + + + + + + + Can be used as structure factor + + + + + + From b6c4a63b35c00ba5870bcdcdab1e00e66e5f1f8d Mon Sep 17 00:00:00 2001 From: tsole0 Date: Thu, 12 Sep 2024 12:56:58 -0400 Subject: [PATCH 39/39] update C template and "generate C model checkbox" for clarity --- src/sas/qtgui/Utilities/TabbedModelEditor.py | 23 +++++++++++++++---- .../qtgui/Utilities/UI/PluginDefinitionUI.ui | 2 +- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/sas/qtgui/Utilities/TabbedModelEditor.py b/src/sas/qtgui/Utilities/TabbedModelEditor.py index fac0703e83..ddaebc0ae8 100644 --- a/src/sas/qtgui/Utilities/TabbedModelEditor.py +++ b/src/sas/qtgui/Utilities/TabbedModelEditor.py @@ -798,14 +798,20 @@ def generateCModel(self, model, fname): param_names.append('double ' + pname) for pd_pname, _, _ in self.getParamHelper(pd_param_str): pd_param_names.append('double ' + pd_pname) - + + #Format Python into comments to be put into I(Q) section + iq_text: str = model['func_text'] + iq_lines = iq_text.splitlines() + commented_lines = ["//" + line for line in iq_lines] + commented_iq_function = "\n ".join(commented_lines) + # Add polydisperse-dependent functions if polydisperse parameters are present if pd_param_names != []: model_text += C_PD_TEMPLATE.format(poly_args = ', '.join(pd_param_names), poly_arg1 = pd_param_names[0].split(' ')[1]) # Remove 'double' from the first argument # Add all other function templates - model_text += C_TEMPLATE.format(args = ',\n\t'.join(param_names)) - + model_text += C_TEMPLATE.format(args = ',\n'.join(param_names), + Iq = commented_iq_function) return model_text @@ -1069,9 +1075,12 @@ def form_volume({args}): """ LINK_C_MODEL_TEMPLATE = '''\ -# Note: removing the "source = []" line will unlink the C model from the Python model, +# To Enable C model, uncomment the line defining `source` and +# delete the I(Q) function in this Python model after converting your code to C +# Note: removing or commenting the "source = []" line will unlink the C model from the Python model, # which means the C model will not be checked for errors when edited. -source = ['{c_model_name}'] + +# source = ['{c_model_name}'] ''' C_COMMENT_TEMPLATE = '''\ @@ -1154,6 +1163,10 @@ def form_volume({args}): // Define I(Q) calculations here for models independent of shape orientation // IMPORTANT: Only define ONE calculation for I(Q): either Iq, Iqac, or Iqabc; // remove others. + // TO USE: Convert your copied Python code to C below and uncomment it + // Ensure that you delete the I(Q) function in the corresponding Python file. + + {Iq} return 1.0; }} diff --git a/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui b/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui index b9fe568f7d..ad67ef8c3c 100755 --- a/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui +++ b/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui @@ -146,7 +146,7 @@ - Generate C model + Generate C model template