source: sasview/src/sas/qtgui/Utilities/TabbedModelEditor.py @ b31b7e2

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since b31b7e2 was aed0532, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Updated references to help files

  • Property mode set to 100644
File size: 17.3 KB
Line 
1# global
2import sys
3import os
4import datetime
5import numpy as np
6import logging
7import traceback
8
9from PyQt5 import QtWidgets
10
11from sas.sascalc.fit import models
12
13import sas.qtgui.Utilities.GuiUtils as GuiUtils
14from sas.qtgui.Utilities.UI.TabbedModelEditor import Ui_TabbedModelEditor
15from sas.qtgui.Utilities.PluginDefinition import PluginDefinition
16from sas.qtgui.Utilities.ModelEditor import ModelEditor
17
18class TabbedModelEditor(QtWidgets.QDialog, Ui_TabbedModelEditor):
19    """
20    Model editor "container" class describing interaction between
21    plugin definition widget and model editor widget.
22    Once the model is defined, it can be saved as a plugin.
23    """
24    # Signals for intertab communication plugin -> editor
25    def __init__(self, parent=None, edit_only=False):
26        super(TabbedModelEditor, self).__init__(parent._parent)
27
28        self.parent = parent
29
30        self.setupUi(self)
31
32        # globals
33        self.filename = ""
34        self.window_title = self.windowTitle()
35        self.edit_only = edit_only
36        self.is_modified = False
37        self.label = None
38
39        self.addWidgets()
40
41        self.addSignals()
42
43    def addWidgets(self):
44        """
45        Populate tabs with widgets
46        """
47        # Set up widget enablement/visibility
48        self.cmdLoad.setVisible(self.edit_only)
49
50        # Add tabs
51        # Plugin definition widget
52        self.plugin_widget = PluginDefinition(self)
53        self.tabWidget.addTab(self.plugin_widget, "Plugin Definition")
54        self.setPluginActive(True)
55
56        self.editor_widget = ModelEditor(self)
57        # Initially, nothing in the editor
58        self.editor_widget.setEnabled(False)
59        self.tabWidget.addTab(self.editor_widget, "Model editor")
60        self.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(False)
61
62        if self.edit_only:
63            self.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).setText("Save")
64            # Hide signals from the plugin widget
65            self.plugin_widget.blockSignals(True)
66            # and hide the tab/widget itself
67            self.tabWidget.removeTab(0)
68
69    def addSignals(self):
70        """
71        Define slots for common widget signals
72        """
73        # buttons
74        self.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).clicked.connect(self.onApply)
75        self.buttonBox.button(QtWidgets.QDialogButtonBox.Cancel).clicked.connect(self.onCancel)
76        self.buttonBox.button(QtWidgets.QDialogButtonBox.Help).clicked.connect(self.onHelp)
77        self.cmdLoad.clicked.connect(self.onLoad)
78        # signals from tabs
79        self.editor_widget.modelModified.connect(self.editorModelModified)
80        self.plugin_widget.txtName.editingFinished.connect(self.pluginTitleSet)
81
82    def setPluginActive(self, is_active=True):
83        """
84        Enablement control for all the controls on the simple plugin editor
85        """
86        self.plugin_widget.setEnabled(is_active)
87
88    def closeEvent(self, event):
89        """
90        Overwrite the close even to assure intent
91        """
92        if self.is_modified:
93            ret = self.onModifiedExit()
94            if ret == QtWidgets.QMessageBox.Cancel:
95                return
96            elif ret == QtWidgets.QMessageBox.Save:
97                self.updateFromEditor()
98        event.accept()
99
100    def onLoad(self):
101        """
102        Loads a model plugin file
103        """
104        plugin_location = models.find_plugins_dir()
105        filename = QtWidgets.QFileDialog.getOpenFileName(
106                                        self,
107                                        'Open Plugin',
108                                        plugin_location,
109                                        'SasView Plugin Model (*.py)',
110                                        None,
111                                        QtWidgets.QFileDialog.DontUseNativeDialog)[0]
112
113        # Load the file
114        if not filename:
115            logging.info("No data file chosen.")
116            return
117
118        self.loadFile(filename)
119
120    def loadFile(self, filename):
121        """
122        Performs the load operation and updates the view
123        """
124        self.editor_widget.blockSignals(True)
125        with open(filename, 'r') as plugin:
126            self.editor_widget.txtEditor.setPlainText(plugin.read())
127        self.editor_widget.setEnabled(True)
128        self.editor_widget.blockSignals(False)
129        self.filename = filename
130        display_name, _ = os.path.splitext(os.path.basename(filename))
131
132        self.setWindowTitle(self.window_title + " - " + display_name)
133
134    def onModifiedExit(self):
135        msg_box = QtWidgets.QMessageBox(self)
136        msg_box.setWindowTitle("SasView Model Editor")
137        msg_box.setText("The document has been modified.")
138        msg_box.setInformativeText("Do you want to save your changes?")
139        msg_box.setStandardButtons(QtWidgets.QMessageBox.Save | QtWidgets.QMessageBox.Discard | QtWidgets.QMessageBox.Cancel)
140        msg_box.setDefaultButton(QtWidgets.QMessageBox.Save)
141        return msg_box.exec()
142
143    def onCancel(self):
144        """
145        Accept if document not modified, confirm intent otherwise.
146        """
147        if self.is_modified:
148            ret = self.onModifiedExit()
149            if ret == QtWidgets.QMessageBox.Cancel:
150                return
151            elif ret == QtWidgets.QMessageBox.Save:
152                self.updateFromEditor()
153        self.reject()
154
155    def onApply(self):
156        """
157        Write the plugin and update the model editor if plugin editor open
158        Write/overwrite the plugin if model editor open
159        """
160        if isinstance(self.tabWidget.currentWidget(), PluginDefinition):
161            self.updateFromPlugin()
162        else:
163            self.updateFromEditor()
164        self.is_modified = False
165
166    def editorModelModified(self):
167        """
168        User modified the model in the Model Editor.
169        Disable the plugin editor and show that the model is changed.
170        """
171        self.setTabEdited(True)
172        self.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(True)
173        self.is_modified = True
174
175    def pluginTitleSet(self):
176        """
177        User modified the model name.
178        Display the model name in the window title
179        and allow for model save.
180        """
181        # Ensure plugin name is non-empty
182        model = self.getModel()
183        if 'filename' in model and model['filename']:
184            self.setWindowTitle(self.window_title + " - " + model['filename'])
185            self.setTabEdited(True)
186            self.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(True)
187            self.is_modified = True
188        else:
189            # the model name is empty - disable Apply and clear the editor
190            self.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(False)
191            self.editor_widget.blockSignals(True)
192            self.editor_widget.txtEditor.setPlainText('')
193            self.editor_widget.blockSignals(False)
194            self.editor_widget.setEnabled(False)
195
196    def setTabEdited(self, is_edited):
197        """
198        Change the widget name to indicate unsaved state
199        Unsaved state: add "*" to filename display
200        saved state: remove "*" from filename display
201        """
202        current_text = self.windowTitle()
203
204        if is_edited:
205            if current_text[-1] != "*":
206                current_text += "*"
207        else:
208            if current_text[-1] == "*":
209                current_text = current_text[:-1]
210        self.setWindowTitle(current_text)
211
212    def updateFromPlugin(self):
213        """
214        Write the plugin and update the model editor
215        """
216        # get current model
217        model = self.getModel()
218        if 'filename' not in model: return
219
220        # get required filename
221        filename = model['filename']
222
223        # check if file exists
224        plugin_location = models.find_plugins_dir()
225        full_path = os.path.join(plugin_location, filename)
226        if os.path.splitext(full_path)[1] != ".py":
227            full_path += ".py"
228
229        # Update the global path definition
230        self.filename = full_path
231
232        if not self.canWriteModel(model, full_path):
233            return
234
235        # generate the model representation as string
236        model_str = self.generateModel(model, full_path)
237        self.writeFile(full_path, model_str)
238
239        # test the model
240
241        # Run the model test in sasmodels
242        try:
243            model_results = self.checkModel(full_path)
244            logging.info(model_results)
245        except Exception as ex:
246            msg = "Error building model: "+ str(ex)
247            logging.error(msg)
248            #print three last lines of the stack trace
249            # this will point out the exact line failing
250            last_lines = traceback.format_exc().split('\n')[-4:]
251            traceback_to_show = '\n'.join(last_lines)
252            logging.error(traceback_to_show)
253
254            self.parent.communicate.statusBarUpdateSignal.emit("Model check failed")
255            return
256
257        self.editor_widget.setEnabled(True)
258
259        # Update the editor here.
260        # Simple string forced into control.
261        self.editor_widget.blockSignals(True)
262        self.editor_widget.txtEditor.setPlainText(model_str)
263        self.editor_widget.blockSignals(False)
264
265        # Set the widget title
266        self.setTabEdited(False)
267
268        # Notify listeners
269        self.parent.communicate.customModelDirectoryChanged.emit()
270
271        # Notify the user
272        msg = "Custom model "+filename + " successfully created."
273        self.parent.communicate.statusBarUpdateSignal.emit(msg)
274        logging.info(msg)
275
276    def updateFromEditor(self):
277        """
278        Save the current state of the Model Editor
279        """
280        # make sure we have the file handly ready
281        assert(self.filename != "")
282        # Retrieve model string
283        model_str = self.getModel()['text']
284        # Save the file
285        self.writeFile(self.filename, model_str)
286        # Update the tab title
287        self.setTabEdited(False)
288        # notify the user
289        msg = self.filename + " successfully saved."
290        self.parent.communicate.statusBarUpdateSignal.emit(msg)
291        logging.info(msg)
292
293    def canWriteModel(self, model=None, full_path=""):
294        """
295        Determine if the current plugin can be written to file
296        """
297        assert(isinstance(model, dict))
298        assert(full_path!="")
299
300        # Make sure we can overwrite the file if it exists
301        if os.path.isfile(full_path):
302            # can we overwrite it?
303            if not model['overwrite']:
304                # notify the viewer
305                msg = "Plugin with specified name already exists.\n"
306                msg += "Please specify different filename or allow file overwrite."
307                QtWidgets.QMessageBox.critical(self, "Plugin Error", msg)
308                # Don't accept but return
309                return False
310        # Update model editor if plugin definition changed
311        func_str = model['text']
312        msg = None
313        if func_str:
314            if 'return' not in func_str:
315                msg = "Error: The func(x) must 'return' a value at least.\n"
316                msg += "For example: \n\nreturn 2*x"
317        else:
318            msg = 'Error: Function is not defined.'
319        if msg is not None:
320            QtWidgets.QMessageBox.critical(self, "Plugin Error", msg)
321            return False
322        return True
323
324    def onHelp(self):
325        """
326        Bring up the Model Editor Documentation whenever
327        the HELP button is clicked.
328        Calls Documentation Window with the path of the location within the
329        documentation tree (after /doc/ ....".
330        """
331        location = "/user/qtgui/Perspectives/Fitting/plugin.html"
332        self.parent.showHelp(location)
333
334    def getModel(self):
335        """
336        Retrieves plugin model from the currently open tab
337        """
338        return self.tabWidget.currentWidget().getModel()
339
340    @classmethod
341    def writeFile(cls, fname, model_str=""):
342        """
343        Write model content to file "fname"
344        """
345        with open(fname, 'w') as out_f:
346            out_f.write(model_str)
347
348    def generateModel(self, model, fname):
349        """
350        generate model from the current plugin state
351        """
352        name = model['filename']
353        desc_str = model['description']
354        param_str = self.strFromParamDict(model['parameters'])
355        pd_param_str = self.strFromParamDict(model['pd_parameters'])
356        func_str = model['text']
357        model_text = CUSTOM_TEMPLATE % {
358            'name': name,
359            'title': 'User model for ' + name,
360            'description': desc_str,
361            'date': datetime.datetime.now().strftime('%YYYY-%mm-%dd'),
362        }
363
364        # Write out parameters
365        param_names = []    # to store parameter names
366        pd_params = []
367        model_text += 'parameters = [ \n'
368        model_text += '#   ["name", "units", default, [lower, upper], "type", "description"],\n'
369        if param_str:
370            for pname, pvalue, desc in self.getParamHelper(param_str):
371                param_names.append(pname)
372                model_text += "    ['%s', '', %s, [-inf, inf], '', '%s'],\n" % (pname, pvalue, desc)
373        if pd_param_str:
374            for pname, pvalue, desc in self.getParamHelper(pd_param_str):
375                param_names.append(pname)
376                pd_params.append(pname)
377                model_text += "    ['%s', '', %s, [-inf, inf], 'volume', '%s'],\n" % (pname, pvalue, desc)
378        model_text += '    ]\n'
379
380        # Write out function definition
381        model_text += 'def Iq(%s):\n' % ', '.join(['x'] + param_names)
382        model_text += '    """Absolute scattering"""\n'
383        if "scipy." in func_str:
384            model_text +="    import scipy\n"
385        if "numpy." in func_str:
386            model_text +="    import numpy\n"
387        if "np." in func_str:
388            model_text +="    import numpy as np\n"
389        for func_line in func_str.split('\n'):
390                model_text +='%s%s\n' % ("    ", func_line)
391        model_text +='## uncomment the following if Iq works for vector x\n'
392        model_text +='#Iq.vectorized = True\n'
393
394        # If polydisperse, create place holders for form_volume, ER and VR
395        if pd_params:
396            model_text +="\n"
397            model_text +=CUSTOM_TEMPLATE_PD % {'args': ', '.join(pd_params)}
398
399        # Create place holder for Iqxy
400        model_text +="\n"
401        model_text +='#def Iqxy(%s):\n' % ', '.join(["x", "y"] + param_names)
402        model_text +='#    """Absolute scattering of oriented particles."""\n'
403        model_text +='#    ...\n'
404        model_text +='#    return oriented_form(x, y, args)\n'
405        model_text +='## uncomment the following if Iqxy works for vector x, y\n'
406        model_text +='#Iqxy.vectorized = True\n'
407
408        return model_text
409
410    @classmethod
411    def getParamHelper(cls, param_str):
412        """
413        yield a sequence of name, value pairs for the parameters in param_str
414
415        Parameters can be defined by one per line by name=value, or multiple
416        on the same line by separating the pairs by semicolon or comma.  The
417        value is optional and defaults to "1.0".
418        """
419        for line in param_str.replace(';', ',').split('\n'):
420            for item in line.split(','):
421                defn, desc = item.split('#', 1) if '#' in item else (item, '')
422                name, value = defn.split('=', 1) if '=' in defn else (defn, '1.0')
423                if name:
424                    yield [v.strip() for v in (name, value, desc)]
425
426    @classmethod
427    def strFromParamDict(cls, param_dict):
428        """
429        Creates string from parameter dictionary
430        {0: ('variable','value'),
431         1: ('variable','value'),
432         ...}
433        """
434        param_str = ""
435        for _, params in param_dict.items():
436            if not params[0]: continue
437            value = 1
438            if params[1]:
439                try:
440                    value = float(params[1])
441                except ValueError:
442                    # convert to default
443                    value = 1
444            param_str += params[0] + " = " + str(value) + "\n"
445        return param_str
446
447
448CUSTOM_TEMPLATE = '''\
449r"""
450Definition
451----------
452
453Calculates %(name)s.
454
455%(description)s
456
457References
458----------
459
460Authorship and Verification
461---------------------------
462
463* **Author:** --- **Date:** %(date)s
464* **Last Modified by:** --- **Date:** %(date)s
465* **Last Reviewed by:** --- **Date:** %(date)s
466"""
467
468from math import *
469from numpy import inf
470
471name = "%(name)s"
472title = "%(title)s"
473description = """%(description)s"""
474
475'''
476
477CUSTOM_TEMPLATE_PD = '''\
478def form_volume(%(args)s):
479    """
480    Volume of the particles used to compute absolute scattering intensity
481    and to weight polydisperse parameter contributions.
482    """
483    return 0.0
484
485def ER(%(args)s):
486    """
487    Effective radius of particles to be used when computing structure factors.
488
489    Input parameters are vectors ranging over the mesh of polydispersity values.
490    """
491    return 0.0
492
493def VR(%(args)s):
494    """
495    Volume ratio of particles to be used when computing structure factors.
496
497    Input parameters are vectors ranging over the mesh of polydispersity values.
498    """
499    return 1.0
500'''
501
502SUM_TEMPLATE = """
503from sasmodels.core import load_model_info
504from sasmodels.sasview_model import make_model_from_info
505
506model_info = load_model_info('{model1}{operator}{model2}')
507model_info.name = '{name}'{desc_line}
508Model = make_model_from_info(model_info)
509"""
510
511if __name__ == '__main__':
512    app = QtWidgets.QApplication(sys.argv)
513    sheet = TabbedModelEditor()
514    sheet.show()
515    app.exec_()
516   
Note: See TracBrowser for help on using the repository browser.