source: sasview/src/sas/sasgui/perspectives/calculator/model_editor.py @ ddbac66

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalccostrafo411magnetic_scattrelease-4.1.1release-4.1.2release-4.2.2ticket-1009ticket-1094-headlessticket-1242-2d-resolutionticket-1243ticket-1249ticket885unittest-saveload
Last change on this file since ddbac66 was ddbac66, checked in by butler, 7 years ago

Fix remaining custom/customized to plugin conversion

  • Property mode set to 100644
File size: 59.6 KB
RevLine 
[7b1f4e3]1'''
2This module provides three model editor classes: the composite model editor,
3the easy editor which provides a simple interface with tooltip help to enter
[2d50115]4the parameters of the model and their default value and a panel to input a
5function of y (usually the intensity).  It also provides a drop down of
[7b1f4e3]6standard available math functions.  Finally a full python editor panel for
[ddbac66]7complete customization is provided.
[7b1f4e3]8
[ddbac66]9:TODO the writing of the file and name checking (and maybe some other
10functions?) should be moved to a computational module which could be called
11from a python script.  Basically one just needs to pass the name,
[2d50115]12description text and function text (or in the case of the composite editor
[7b1f4e3]13the names of the first and second model and the operator to be used).
14'''
15
[6f140f2]16################################################################################
17#This software was developed by the University of Tennessee as part of the
18#Distributed Data Analysis of Neutron Scattering Experiments (DANSE)
[18ac46b]19#project funded by the US National Science Foundation.
[6f140f2]20#
21#See the license text in license.txt
22#
23#copyright 2009, University of Tennessee
24################################################################################
25import wx
26import sys
27import os
[318b5bbb]28import math
[7b1f4e3]29import re
[f80b8a9f]30import logging
[6f140f2]31from wx.py.editwindow import EditWindow
[d85c194]32from sas.sasgui.guiframe.documentation_window import DocumentationWindow
[26d6e045]33from .pyconsole import show_model_output, check_model
[4b5fe655]34
[6f140f2]35
36if sys.platform.count("win32") > 0:
37    FONT_VARIANT = 0
[fdef956]38    PNL_WIDTH = 450
[18ac46b]39    PNL_HEIGHT = 320
[6f140f2]40else:
41    FONT_VARIANT = 1
[fdef956]42    PNL_WIDTH = 590
[18ac46b]43    PNL_HEIGHT = 350
[fdef956]44M_NAME = 'Model'
[6f140f2]45EDITOR_WIDTH = 800
[318b5bbb]46EDITOR_HEIGTH = 735
[6f140f2]47PANEL_WIDTH = 500
[fdef956]48_BOX_WIDTH = 55
[6f140f2]49
[18ac46b]50def _delete_file(path):
[6f140f2]51    """
52    Delete file in the path
53    """
54    try:
55        os.remove(path)
56    except:
57        raise
58
[49ab5d7]59
[6f140f2]60class TextDialog(wx.Dialog):
61    """
[7b1f4e3]62    Dialog for easy custom composite models.  Provides a wx.Dialog panel
[ddbac66]63    to choose two existing models (including pre-existing Plugin Models which
[18ac46b]64    may themselves be composite models) as well as an operation on those models
[2d50115]65    (add or multiply) the resulting model will add a scale parameter for summed
[7b1f4e3]66    models and a background parameter for a multiplied model.
[2d50115]67
68    The user also gives a brief help for the model in a description box and
[7b1f4e3]69    must provide a unique name which is verified as unique before the new
70    model is saved.
[2d50115]71
[7b1f4e3]72    This Dialog pops up for the user when they press 'Sum|Multi(p1,p2)' under
[ec72ceb]73    'Plugin Model Operations' under 'Fitting' menu.  This is currently called as
[7b1f4e3]74    a Modal Dialog.
[2d50115]75
76    :TODO the build in compiler currently balks at when it tries to import
77    a model whose name contains spaces or symbols (such as + ... underscore
[18ac46b]78    should be fine).  Have fixed so the editor cannot save such a file name
79    but if a file is dropped in the plugin directory from outside this class
80    will create a file that cannot be compiled.  Should add the check to
81    the write method or to the on_modelx method.
[ac7be54]82
83    - PDB:April 5, 2015
[6f140f2]84    """
[49ab5d7]85    def __init__(self, parent=None, base=None, id=None, title='',
[d970df9]86                 model_list=[], plugin_dir=None):
[6f140f2]87        """
[18ac46b]88        This class is run when instatiated.  The __init__ initializes and
[7b1f4e3]89        calls the internal methods necessary.  On exiting the wx.Dialog
90        window should be destroyed.
[6f140f2]91        """
[49ab5d7]92        wx.Dialog.__init__(self, parent=parent, id=id,
[18ac46b]93                           title=title, size=(PNL_WIDTH, PNL_HEIGHT))
[796c4d4]94        self.parent = base
[6f140f2]95        #Font
96        self.SetWindowVariant(variant=FONT_VARIANT)
97        # default
[796c4d4]98        self.overwrite_name = False
99        self.plugin_dir = plugin_dir
[6f140f2]100        self.model_list = model_list
[ec36e48]101        self.model1_string = "sphere"
102        self.model2_string = "cylinder"
[fdef956]103        self.name = 'Sum' + M_NAME
[d970df9]104        self.factor = 'scale_factor'
[41a8cb3]105        self._notes = ''
[7b1f4e3]106        self._operator = '+'
107        self._operator_choice = None
[fdef956]108        self.explanation = ''
109        self.explanationctr = None
[7b1f4e3]110        self.type = None
[d970df9]111        self.name_sizer = None
[7b1f4e3]112        self.name_tcl = None
[d970df9]113        self.desc_sizer = None
114        self.desc_tcl = None
[2d50115]115        self._selection_box = None
[d970df9]116        self.model1 = None
117        self.model2 = None
118        self.static_line_1 = None
[18ac46b]119        self.ok_button = None
120        self.close_button = None
[41a8cb3]121        self._msg_box = None
122        self.msg_sizer = None
[d970df9]123        self.fname = None
124        self.cm_list = None
125        self.is_p1_custom = False
126        self.is_p2_custom = False
[6f140f2]127        self._build_sizer()
128        self.model1_name = str(self.model1.GetValue())
129        self.model2_name = str(self.model2.GetValue())
[796c4d4]130        self.good_name = True
[fdef956]131        self.fill_oprator_combox()
[49ab5d7]132
[796c4d4]133    def _layout_name(self):
134        """
135        Do the layout for file/function name related widgets
136        """
[7b1f4e3]137        #container for new model name input
138        self.name_sizer = wx.BoxSizer(wx.HORIZONTAL)
139
140        #set up label and input box with tool tip and event handling
[2d50115]141        name_txt = wx.StaticText(self, -1, 'Function Name : ')
142        self.name_tcl = wx.TextCtrl(self, -1, value='MySumFunction')
[796c4d4]143        self.name_tcl.Bind(wx.EVT_TEXT_ENTER, self.on_change_name)
[0008f54]144        hint_name = "Unique Sum/Multiply Model Function Name."
[796c4d4]145        self.name_tcl.SetToolTipString(hint_name)
[7b1f4e3]146
[2d50115]147        self.name_sizer.AddMany([(name_txt, 0, wx.LEFT | wx.TOP, 10),
148                                 (self.name_tcl, -1,
149                                  wx.EXPAND | wx.RIGHT | wx.TOP | wx.BOTTOM,
150                                  10)])
[49ab5d7]151
152
[796c4d4]153    def _layout_description(self):
154        """
155        Do the layout for description related widgets
156        """
[7b1f4e3]157        #container for new model description input
[796c4d4]158        self.desc_sizer = wx.BoxSizer(wx.HORIZONTAL)
[7b1f4e3]159
160        #set up description label and input box with tool tip and event handling
[49ab5d7]161        desc_txt = wx.StaticText(self, -1, 'Description (optional) : ')
[7b1f4e3]162        self.desc_tcl = wx.TextCtrl(self, -1)
[fdef956]163        hint_desc = "Write a short description of this model function."
[796c4d4]164        self.desc_tcl.SetToolTipString(hint_desc)
[7b1f4e3]165
[49ab5d7]166        self.desc_sizer.AddMany([(desc_txt, 0, wx.LEFT | wx.TOP, 10),
[18ac46b]167                                 (self.desc_tcl, -1,
168                                  wx.EXPAND | wx.RIGHT | wx.TOP | wx.BOTTOM,
169                                  10)])
[49ab5d7]170
[7b1f4e3]171
172    def _layout_model_selection(self):
[6f140f2]173        """
[7b1f4e3]174        Do the layout for model selection related widgets
[6f140f2]175        """
[fdef956]176        box_width = 195 # combobox width
[49ab5d7]177
[7b1f4e3]178        #First set up main sizer for the selection
179        selection_box_title = wx.StaticBox(self, -1, 'Select',
[2d50115]180                                           size=(PNL_WIDTH - 30, 70))
181        self._selection_box = wx.StaticBoxSizer(selection_box_title,
[18ac46b]182                                                wx.VERTICAL)
[7b1f4e3]183
184        #Next create the help labels for the model selection
185        select_help_box = wx.BoxSizer(wx.HORIZONTAL)
186        model_string = " Model%s (p%s):"
[18ac46b]187        select_help_box.Add(wx.StaticText(self, -1, model_string % (1, 1)),
188                            0, 0)
[2d50115]189        select_help_box.Add((box_width - 25, 10), 0, 0)
[18ac46b]190        select_help_box.Add(wx.StaticText(self, -1, model_string % (2, 2)),
191                            0, 0)
[7b1f4e3]192        self._selection_box.Add(select_help_box, 0, 0)
193
194        #Next create the actual selection box with 3 combo boxes
195        selection_box_choose = wx.BoxSizer(wx.HORIZONTAL)
196
[49ab5d7]197        self.model1 = wx.ComboBox(self, -1, style=wx.CB_READONLY)
[6f140f2]198        wx.EVT_COMBOBOX(self.model1, -1, self.on_model1)
[49ab5d7]199        self.model1.SetMinSize((box_width * 5 / 6, -1))
[6f140f2]200        self.model1.SetToolTipString("model1")
[49ab5d7]201
[18ac46b]202        self._operator_choice = wx.ComboBox(self, -1, size=(50, -1),
203                                            style=wx.CB_READONLY)
[7b1f4e3]204        wx.EVT_COMBOBOX(self._operator_choice, -1, self.on_select_operator)
[fdef956]205        operation_tip = "Add: +, Multiply: * "
[7b1f4e3]206        self._operator_choice.SetToolTipString(operation_tip)
[49ab5d7]207
208        self.model2 = wx.ComboBox(self, -1, style=wx.CB_READONLY)
[6f140f2]209        wx.EVT_COMBOBOX(self.model2, -1, self.on_model2)
[49ab5d7]210        self.model2.SetMinSize((box_width * 5 / 6, -1))
[6f140f2]211        self.model2.SetToolTipString("model2")
212        self._set_model_list()
[49ab5d7]213
[7b1f4e3]214        selection_box_choose.Add(self.model1, 0, 0)
215        selection_box_choose.Add((15, 10))
216        selection_box_choose.Add(self._operator_choice, 0, 0)
217        selection_box_choose.Add((15, 10))
218        selection_box_choose.Add(self.model2, 0, 0)
[18ac46b]219        # add some space between labels and selection
[2d50115]220        self._selection_box.Add((20, 5), 0, 0)
[7b1f4e3]221        self._selection_box.Add(selection_box_choose, 0, 0)
222
223    def _build_sizer(self):
224        """
225        Build GUI with calls to _layout_name, _layout Description
[2d50115]226        and _layout_model_selection which each build a their portion of the
[7b1f4e3]227        GUI.
228        """
229        mainsizer = wx.BoxSizer(wx.VERTICAL) # create main sizer for dialog
230
231        # build fromm top by calling _layout_name and _layout_description
232        # and adding to main sizer
233        self._layout_name()
[2d50115]234        mainsizer.Add(self.name_sizer, 0, wx.EXPAND)
[7b1f4e3]235        self._layout_description()
[2d50115]236        mainsizer.Add(self.desc_sizer, 0, wx.EXPAND)
[7b1f4e3]237
238        # Add an explanation of dialog (short help)
239        self.explanationctr = wx.StaticText(self, -1, self.explanation)
240        self.fill_explanation_helpstring(self._operator)
[2d50115]241        mainsizer.Add(self.explanationctr, 0, wx.LEFT | wx.EXPAND, 15)
[7b1f4e3]242
243        # Add the selection box stuff with border and labels built
244        # by _layout_model_selection
245        self._layout_model_selection()
246        mainsizer.Add(self._selection_box, 0, wx.LEFT, 15)
247
248        # Add a space and horizontal line before the notification
249        #messages and the buttons at the bottom
250        mainsizer.Add((10, 10))
[6f140f2]251        self.static_line_1 = wx.StaticLine(self, -1)
[7b1f4e3]252        mainsizer.Add(self.static_line_1, 0, wx.EXPAND, 10)
253
254        # Add action status notification line (null at startup)
[41a8cb3]255        self._msg_box = wx.StaticText(self, -1, self._notes)
256        self.msg_sizer = wx.BoxSizer(wx.HORIZONTAL)
257        self.msg_sizer.Add(self._msg_box, 0, wx.LEFT, 0)
[7b1f4e3]258        mainsizer.Add(self.msg_sizer, 0,
[2d50115]259                      wx.LEFT | wx.RIGHT | wx.ADJUST_MINSIZE | wx.BOTTOM, 10)
[7b1f4e3]260
261        # Finally add the buttons (apply and close) on the bottom
[2d50115]262        # Eventually need to add help here
[18ac46b]263        self.ok_button = wx.Button(self, wx.ID_OK, 'Apply')
[4b5fe655]264        _app_tip = "Save the new Model."
265        self.ok_button.SetToolTipString(_app_tip)
[18ac46b]266        self.ok_button.Bind(wx.EVT_BUTTON, self.check_name)
[4b5fe655]267        self.help_button = wx.Button(self, -1, 'HELP')
268        _app_tip = "Help on composite model creation."
[7801df8]269        self.help_button.SetToolTipString(_app_tip)
[4b5fe655]270        self.help_button.Bind(wx.EVT_BUTTON, self.on_help)
[18ac46b]271        self.close_button = wx.Button(self, wx.ID_CANCEL, 'Close')
[6f140f2]272        sizer_button = wx.BoxSizer(wx.HORIZONTAL)
[7b1f4e3]273        sizer_button.AddMany([((20, 20), 1, 0),
[18ac46b]274                              (self.ok_button, 0, 0),
[4b5fe655]275                              (self.help_button, 0, 0),
[2d50115]276                              (self.close_button, 0, wx.LEFT | wx.RIGHT, 10)])
[7b1f4e3]277        mainsizer.Add(sizer_button, 0, wx.EXPAND | wx.BOTTOM | wx.TOP, 10)
278
279        self.SetSizer(mainsizer)
[6f140f2]280        self.Centre()
[49ab5d7]281
[796c4d4]282    def on_change_name(self, event=None):
283        """
284        Change name
285        """
286        if event is not None:
287            event.Skip()
288        self.name_tcl.SetBackgroundColour('white')
289        self.Refresh()
[49ab5d7]290
[796c4d4]291    def check_name(self, event=None):
292        """
[2d50115]293        Check that proposed new model name is a valid Python module name
[439420f9]294        and that it does not already exist. If not show error message and
295        pink background in text box else call on_apply
[2d50115]296
297        :TODO this should be separated out from the GUI code.  For that we
298        need to pass it the name (or if we want to keep the default name
[439420f9]299        option also need to pass the self._operator attribute) We just need
[2d50115]300        the function to return an error code that the name is good or if
301        not why (not a valid name, name exists already).  The rest of the
[439420f9]302        error handling should be done in this module. so on_apply would then
303        start by checking the name and then either raise errors or do the
304        deed.
[796c4d4]305        """
[439420f9]306        #Get the function/file name
[d970df9]307        mname = M_NAME
[796c4d4]308        self.on_change_name(None)
309        title = self.name_tcl.GetValue().lstrip().rstrip()
310        if title == '':
[7b1f4e3]311            text = self._operator
[c8654a3]312            if text.count('+') > 0:
[d970df9]313                mname = 'Sum'
[19b9e43]314            else:
[c8654a3]315                mname = 'Multi'
[d970df9]316            mname += M_NAME
317            title = mname
[796c4d4]318        self.name = title
319        t_fname = title + '.py'
[439420f9]320
321        #First check if the name is a valid Python name
[2d50115]322        if re.match('^[A-Za-z0-9_]*$', title):
[439420f9]323            self.good_name = True
[2d50115]324        else:
[439420f9]325            self.good_name = False
326            msg = ("%s is not a valid Python name. Only alphanumeric \n" \
[18ac46b]327                   "and underscore allowed" % self.name)
[2d50115]328
[439420f9]329        #Now check if the name already exists
330        if not self.overwrite_name and self.good_name:
331            #Create list of existing model names for comparison
332            list_fnames = os.listdir(self.plugin_dir)
333            # fake existing regular model name list
334            m_list = [model + ".py" for model in self.model_list]
335            list_fnames.append(m_list)
[d970df9]336            if t_fname in list_fnames and title != mname:
[796c4d4]337                self.good_name = False
338                msg = "Name exists already."
[439420f9]339
340        if self.good_name == False:
341            self.name_tcl.SetBackgroundColour('pink')
342            info = 'Error'
343            wx.MessageBox(msg, info)
344            self._notes = msg
345            color = 'red'
346            self._msg_box.SetLabel(msg)
347            self._msg_box.SetForegroundColour(color)
348            return self.good_name
[796c4d4]349        self.fname = os.path.join(self.plugin_dir, t_fname)
[41a8cb3]350        s_title = title
351        if len(title) > 20:
352            s_title = title[0:19] + '...'
[0008f54]353        self._notes = "Model function (%s) has been set! \n" % str(s_title)
[796c4d4]354        self.good_name = True
355        self.on_apply(self.fname)
356        return self.good_name
[49ab5d7]357
[796c4d4]358    def on_apply(self, path):
359        """
[18ac46b]360        This method is a misnomer - it is not bound to the apply button
361        event.  Instead the apply button event goes to check_name which
362        then calls this method if the name of the new file is acceptable.
[2d50115]363
364        :TODO this should be bound to the apply button.  The first line
[18ac46b]365        should call the check_name method which itself should be in another
[2d50115]366        module separated from the the GUI modules.
[796c4d4]367        """
[7b1f4e3]368        self.name_tcl.SetBackgroundColour('white')
[796c4d4]369        try:
[18ac46b]370            label = self.get_textnames()
[796c4d4]371            fname = path
372            name1 = label[0]
373            name2 = label[1]
374            self.write_string(fname, name1, name2)
[26d6e045]375            success = show_model_output(self, fname)
376            if success:
377                self.parent.update_custom_combo()
[cdc2ee2]378            msg = self._notes
[0008f54]379            info = 'Info'
[41a8cb3]380            color = 'blue'
[796c4d4]381        except:
[ddbac66]382            msg = "Easy Sum/Multipy Plugin: Error occurred..."
[cdc2ee2]383            info = 'Error'
[41a8cb3]384            color = 'red'
385        self._msg_box.SetLabel(msg)
386        self._msg_box.SetForegroundColour(color)
[cdc2ee2]387        if self.parent.parent != None:
[d85c194]388            from sas.sasgui.guiframe.events import StatusEvent
[49ab5d7]389            wx.PostEvent(self.parent.parent, StatusEvent(status=msg,
[18ac46b]390                                                         info=info))
[49ab5d7]391
[4b5fe655]392    def on_help(self, event):
393        """
394        Bring up the Composite Model Editor Documentation whenever
395        the HELP button is clicked.
396
397        Calls DocumentationWindow with the path of the location within the
398        documentation tree (after /doc/ ....".  Note that when using old
399        versions of Wx (before 2.9) and thus not the release version of
400        installers, the help comes up at the top level of the file as
401        webbrowser does not pass anything past the # to the browser when it is
402        running "file:///...."
403
404    :param evt: Triggers on clicking the help button
405    """
406
[5876f7e]407        _TreeLocation = "user/sasgui/perspectives/fitting/fitting_help.html"
[4b5fe655]408        _PageAnchor = "#sum-multi-p1-p2"
409        _doc_viewer = DocumentationWindow(self, -1, _TreeLocation, _PageAnchor,
410                                          "Composite Model Editor Help")
411
[6f140f2]412    def _set_model_list(self):
413        """
414        Set the list of models
415        """
416        # list of model names
[18ac46b]417        # get regular models
418        main_list = self.model_list
419        # get custom models
420        self.update_cm_list()
421        # add custom models to model list
422        for name in self.cm_list:
423            if name not in main_list:
424                main_list.append(name)
425
426        if len(main_list) > 1:
427            main_list.sort()
428        for idx in range(len(main_list)):
429            self.model1.Append(str(main_list[idx]), idx)
430            self.model2.Append(str(main_list[idx]), idx)
[6f140f2]431        self.model1.SetStringSelection(self.model1_string)
432        self.model2.SetStringSelection(self.model2_string)
[49ab5d7]433
[d970df9]434    def update_cm_list(self):
435        """
436        Update custom model list
437        """
438        cm_list = []
439        al_list = os.listdir(self.plugin_dir)
440        for c_name in al_list:
441            if c_name.split('.')[-1] == 'py' and \
442                    c_name.split('.')[0] != '__init__':
443                name = str(c_name.split('.')[0])
444                cm_list.append(name)
[49ab5d7]445        self.cm_list = cm_list
446
[6f140f2]447    def on_model1(self, event):
448        """
449        Set model1
450        """
451        event.Skip()
[d970df9]452        self.update_cm_list()
[6f140f2]453        self.model1_name = str(self.model1.GetValue())
454        self.model1_string = self.model1_name
[d970df9]455        if self.model1_name in self.cm_list:
456            self.is_p1_custom = True
457        else:
458            self.is_p1_custom = False
[49ab5d7]459
[6f140f2]460    def on_model2(self, event):
461        """
462        Set model2
463        """
464        event.Skip()
[d970df9]465        self.update_cm_list()
[6f140f2]466        self.model2_name = str(self.model2.GetValue())
467        self.model2_string = self.model2_name
[d970df9]468        if self.model2_name in self.cm_list:
469            self.is_p2_custom = True
470        else:
471            self.is_p2_custom = False
[49ab5d7]472
[fdef956]473    def on_select_operator(self, event=None):
474        """
475        On Select an Operator
476        """
[9e00363]477        # For Mac
478        if event != None:
479            event.Skip()
[fdef956]480        item = event.GetEventObject()
[c8654a3]481        text = item.GetValue()
[7b1f4e3]482        self.fill_explanation_helpstring(text)
483
[2d50115]484    def fill_explanation_helpstring(self, operator):
[7b1f4e3]485        """
486        Choose the equation to use depending on whether we now have
487        a sum or multiply model then create the appropriate string
488        """
489
490        name = ''
491
[2d50115]492        if operator == '*':
[fdef956]493            name = 'Multi'
[d970df9]494            factor = 'BackGround'
495            f_oper = '+'
[f5500b3]496        else:
[848913c]497            name = 'Sum'
498            factor = 'scale_factor'
499            f_oper = '*'
[c8654a3]500
501        self.factor = factor
[2d50115]502        self._operator = operator
[ddbac66]503        self.explanation = "  Plugin Model = %s %s (model1 %s model2)\n" % \
[18ac46b]504                           (self.factor, f_oper, self._operator)
[fdef956]505        self.explanationctr.SetLabel(self.explanation)
[49ab5d7]506        self.name = name + M_NAME
[7b1f4e3]507
[49ab5d7]508
[fdef956]509    def fill_oprator_combox(self):
510        """
511        fill the current combobox with the operator
[49ab5d7]512        """
[cf1910f]513        operator_list = ['+', '*']
[fdef956]514        for oper in operator_list:
[7b1f4e3]515            pos = self._operator_choice.Append(str(oper))
[cf1910f]516            self._operator_choice.SetClientData(pos, str(oper))
[7b1f4e3]517        self._operator_choice.SetSelection(0)
[49ab5d7]518
[18ac46b]519    def get_textnames(self):
[6f140f2]520        """
521        Returns model name string as list
522        """
523        return [self.model1_name, self.model2_name]
[49ab5d7]524
[6f140f2]525    def write_string(self, fname, name1, name2):
526        """
527        Write and Save file
528        """
[49ab5d7]529        self.fname = fname
[796c4d4]530        description = self.desc_tcl.GetValue().lstrip().rstrip()
531        if description == '':
[7b1f4e3]532            description = name1 + self._operator + name2
533        text = self._operator_choice.GetValue()
[c8654a3]534        if text.count('+') > 0:
[d970df9]535            factor = 'scale_factor'
536            f_oper = '*'
537            default_val = '1.0'
[c8654a3]538        else:
[d970df9]539            factor = 'BackGround'
540            f_oper = '+'
541            default_val = '0.0'
[796c4d4]542        path = self.fname
[6f140f2]543        try:
[49ab5d7]544            out_f = open(path, 'w')
[2d50115]545        except:
[6f140f2]546            raise
547        lines = SUM_TEMPLATE.split('\n')
548        for line in lines:
[d970df9]549            try:
550                if line.count("scale_factor"):
551                    line = line.replace('scale_factor', factor)
552                    #print "scale_factor", line
553                if line.count("= %s"):
554                    out_f.write(line % (default_val) + "\n")
555                elif line.count("import Model as P1"):
556                    if self.is_p1_custom:
557                        line = line.replace('#', '')
558                        out_f.write(line % name1 + "\n")
559                    else:
560                        out_f.write(line + "\n")
561                elif line.count("import %s as P1"):
562                    if not self.is_p1_custom:
563                        line = line.replace('#', '')
[bac3988]564                        out_f.write(line % (name1) + "\n")
[d970df9]565                    else:
566                        out_f.write(line + "\n")
567                elif line.count("import Model as P2"):
568                    if self.is_p2_custom:
569                        line = line.replace('#', '')
570                        out_f.write(line % name2 + "\n")
571                    else:
572                        out_f.write(line + "\n")
573                elif line.count("import %s as P2"):
574                    if not self.is_p2_custom:
575                        line = line.replace('#', '')
[bac3988]576                        out_f.write(line % (name2) + "\n")
[d970df9]577                    else:
578                        out_f.write(line + "\n")
[7673ecd]579                elif line.count("P1 = find_model"):
[26d5aa5]580                    out_f.write(line % (name1) + "\n")
[7673ecd]581                elif line.count("P2 = find_model"):
[26d5aa5]582                    out_f.write(line % (name2) + "\n")
583
[d970df9]584                elif line.count("self.description = '%s'"):
585                    out_f.write(line % description + "\n")
586                #elif line.count("run") and line.count("%s"):
[7b1f4e3]587                #    out_f.write(line % self._operator + "\n")
[d970df9]588                #elif line.count("evalDistribution") and line.count("%s"):
[7b1f4e3]589                #    out_f.write(line % self._operator + "\n")
[d970df9]590                elif line.count("return") and line.count("%s") == 2:
591                    #print "line return", line
[7b1f4e3]592                    out_f.write(line % (f_oper, self._operator) + "\n")
[d970df9]593                elif line.count("out2")and line.count("%s"):
[7b1f4e3]594                    out_f.write(line % self._operator + "\n")
[d970df9]595                else:
596                    out_f.write(line + "\n")
597            except:
598                raise
[796c4d4]599        out_f.close()
600        #else:
601        #    msg = "Name exists already."
[49ab5d7]602
[6f140f2]603    def compile_file(self, path):
604        """
605        Compile the file in the path
606        """
[796c4d4]607        path = self.fname
[26d6e045]608        show_model_output(self, path)
[49ab5d7]609
[6f140f2]610    def delete_file(self, path):
611        """
612        Delete file in the path
613        """
[18ac46b]614        _delete_file(path)
[6f140f2]615
616
617class EditorPanel(wx.ScrolledWindow):
618    """
[ddbac66]619    Simple Plugin Model function editor
[6f140f2]620    """
621    def __init__(self, parent, base, path, title, *args, **kwds):
622        kwds['name'] = title
[18ac46b]623#        kwds["size"] = (EDITOR_WIDTH, EDITOR_HEIGTH)
[d970df9]624        kwds["style"] = wx.FULL_REPAINT_ON_RESIZE
[6f140f2]625        wx.ScrolledWindow.__init__(self, parent, *args, **kwds)
[2d50115]626        self.SetScrollbars(1,1,1,1)
[6f140f2]627        self.parent = parent
628        self.base = base
629        self.path = path
630        self.font = wx.SystemSettings_GetFont(wx.SYS_SYSTEM_FONT)
631        self.font.SetPointSize(10)
632        self.reader = None
633        self.name = 'untitled'
[94f0873f]634        self.overwrite_name = False
[d83549c]635        self.is_2d = False
[6f140f2]636        self.fname = None
[7b1f4e3]637        self.main_sizer = None
638        self.name_sizer = None
639        self.name_hsizer = None
[18ac46b]640        self.name_tcl = None
[7b1f4e3]641        self.desc_sizer = None
[18ac46b]642        self.desc_tcl = None
[7b1f4e3]643        self.param_sizer = None
[18ac46b]644        self.param_tcl = None
[7b1f4e3]645        self.function_sizer = None
646        self.func_horizon_sizer = None
647        self.button_sizer = None
[6f140f2]648        self.param_strings = ''
649        self.function_strings = ''
650        self._notes = ""
[41a8cb3]651        self._msg_box = None
652        self.msg_sizer = None
[6f140f2]653        self.warning = ""
[ddbac66]654        #This does not seem to be used anywhere so commenting out for now
655        #    -- PDB 2/26/17
656        #self._description = "New Plugin Model"
[318b5bbb]657        self.function_tcl = None
[18ac46b]658        self.math_combo = None
659        self.bt_apply = None
660        self.bt_close = None
[6f140f2]661        #self._default_save_location = os.getcwd()
662        self._do_layout()
[18ac46b]663
[6f140f2]664
[49ab5d7]665
[6f140f2]666    def _define_structure(self):
667        """
[49ab5d7]668        define initial sizer
[6f140f2]669        """
670        #w, h = self.parent.GetSize()
671        self.main_sizer = wx.BoxSizer(wx.VERTICAL)
672        self.name_sizer = wx.BoxSizer(wx.VERTICAL)
673        self.name_hsizer = wx.BoxSizer(wx.HORIZONTAL)
674        self.desc_sizer = wx.BoxSizer(wx.VERTICAL)
675        self.param_sizer = wx.BoxSizer(wx.VERTICAL)
676        self.function_sizer = wx.BoxSizer(wx.VERTICAL)
[318b5bbb]677        self.func_horizon_sizer = wx.BoxSizer(wx.HORIZONTAL)
[6f140f2]678        self.button_sizer = wx.BoxSizer(wx.HORIZONTAL)
[41a8cb3]679        self.msg_sizer = wx.BoxSizer(wx.HORIZONTAL)
[49ab5d7]680
[6f140f2]681    def _layout_name(self):
682        """
683        Do the layout for file/function name related widgets
684        """
685        #title name [string]
[49ab5d7]686        name_txt = wx.StaticText(self, -1, 'Function Name : ')
[26c8be3]687        overwrite_cb = wx.CheckBox(self, -1, "Overwrite existing plugin model of this name?", (10, 10))
[94f0873f]688        overwrite_cb.SetValue(False)
[6f140f2]689        overwrite_cb.SetToolTipString("Overwrite it if already exists?")
690        wx.EVT_CHECKBOX(self, overwrite_cb.GetId(), self.on_over_cb)
[49ab5d7]691        self.name_tcl = wx.TextCtrl(self, -1, size=(PANEL_WIDTH * 3 / 5, -1))
[6f140f2]692        self.name_tcl.Bind(wx.EVT_TEXT_ENTER, self.on_change_name)
[94f0873f]693        self.name_tcl.SetValue('')
[6f140f2]694        self.name_tcl.SetFont(self.font)
695        hint_name = "Unique Model Function Name."
696        self.name_tcl.SetToolTipString(hint_name)
[49ab5d7]697        self.name_hsizer.AddMany([(self.name_tcl, 0, wx.LEFT | wx.TOP, 0),
[18ac46b]698                                  (overwrite_cb, 0, wx.LEFT, 20)])
[49ab5d7]699        self.name_sizer.AddMany([(name_txt, 0, wx.LEFT | wx.TOP, 10),
[18ac46b]700                                 (self.name_hsizer, 0,
701                                  wx.LEFT | wx.TOP | wx.BOTTOM, 10)])
[49ab5d7]702
703
[6f140f2]704    def _layout_description(self):
705        """
706        Do the layout for description related widgets
707        """
708        #title name [string]
[49ab5d7]709        desc_txt = wx.StaticText(self, -1, 'Description (optional) : ')
710        self.desc_tcl = wx.TextCtrl(self, -1, size=(PANEL_WIDTH * 3 / 5, -1))
[6f140f2]711        self.desc_tcl.SetValue('')
712        hint_desc = "Write a short description of the model function."
713        self.desc_tcl.SetToolTipString(hint_desc)
[49ab5d7]714        self.desc_sizer.AddMany([(desc_txt, 0, wx.LEFT | wx.TOP, 10),
[18ac46b]715                                 (self.desc_tcl, 0,
716                                  wx.LEFT | wx.TOP | wx.BOTTOM, 10)])
[6f140f2]717    def _layout_param(self):
718        """
719        Do the layout for parameter related widgets
720        """
[70c5d490]721        param_txt = wx.StaticText(self, -1, 'Fit Parameters NOT requiring' + \
722                                  ' polydispersity (if any): ')
[49ab5d7]723
[70c5d490]724        param_tip = "#Set the parameters NOT requiring polydispersity " + \
725        "and their initial values.\n"
[6f140f2]726        param_tip += "#Example:\n"
727        param_tip += "A = 1\nB = 1"
728        #param_txt.SetToolTipString(param_tip)
[18ac46b]729        newid = wx.NewId()
730        self.param_tcl = EditWindow(self, newid, wx.DefaultPosition,
731                                    wx.DefaultSize,
732                                    wx.CLIP_CHILDREN | wx.SUNKEN_BORDER)
[b25caad]733        self.param_tcl.setDisplayLineNumbers(True)
[6f140f2]734        self.param_tcl.SetToolTipString(param_tip)
[318b5bbb]735
[6f140f2]736        self.param_sizer.AddMany([(param_txt, 0, wx.LEFT, 10),
[18ac46b]737                                  (self.param_tcl, 1, wx.EXPAND | wx.ALL, 10)])
[cf92b1f]738       
739        # Parameters with polydispersity
[70c5d490]740        pd_param_txt = wx.StaticText(self, -1, 'Fit Parameters requiring ' + \
741                                     'polydispersity (if any): ')
[cf92b1f]742
[70c5d490]743        pd_param_tip = "#Set the parameters requiring polydispersity and " + \
744        "their initial values.\n"
[cf92b1f]745        pd_param_tip += "#Example:\n"
746        pd_param_tip += "C = 2\nD = 2"
747        newid = wx.NewId()
748        self.pd_param_tcl = EditWindow(self, newid, wx.DefaultPosition,
749                                    wx.DefaultSize,
750                                    wx.CLIP_CHILDREN | wx.SUNKEN_BORDER)
751        self.pd_param_tcl.setDisplayLineNumbers(True)
752        self.pd_param_tcl.SetToolTipString(pd_param_tip)
753       
754        self.param_sizer.AddMany([(pd_param_txt, 0, wx.LEFT, 10),
755                                  (self.pd_param_tcl, 1, wx.EXPAND | wx.ALL, 10)])
[49ab5d7]756
[6f140f2]757    def _layout_function(self):
758        """
759        Do the layout for function related widgets
760        """
[49ab5d7]761        function_txt = wx.StaticText(self, -1, 'Function(x) : ')
[6f140f2]762        hint_function = "#Example:\n"
[e499ca0]763        hint_function += "if x <= 0:\n"
[b25caad]764        hint_function += "    y = A + B\n"
765        hint_function += "else:\n"
[e499ca0]766        hint_function += "    y = A + B * cos(2 * pi * x)\n"
[b25caad]767        hint_function += "return y\n"
[318b5bbb]768        math_txt = wx.StaticText(self, -1, '*Useful math functions: ')
769        math_combo = self._fill_math_combo()
[49ab5d7]770
[18ac46b]771        newid = wx.NewId()
772        self.function_tcl = EditWindow(self, newid, wx.DefaultPosition,
773                                       wx.DefaultSize,
774                                       wx.CLIP_CHILDREN | wx.SUNKEN_BORDER)
[b25caad]775        self.function_tcl.setDisplayLineNumbers(True)
[6f140f2]776        self.function_tcl.SetToolTipString(hint_function)
[49ab5d7]777
[318b5bbb]778        self.func_horizon_sizer.Add(function_txt)
779        self.func_horizon_sizer.Add(math_txt, 0, wx.LEFT, 250)
780        self.func_horizon_sizer.Add(math_combo, 0, wx.LEFT, 10)
781
782        self.function_sizer.Add(self.func_horizon_sizer, 0, wx.LEFT, 10)
[49ab5d7]783        self.function_sizer.Add(self.function_tcl, 1, wx.EXPAND | wx.ALL, 10)
784
[41a8cb3]785    def _layout_msg(self):
786        """
787        Layout msg
788        """
[49ab5d7]789        self._msg_box = wx.StaticText(self, -1, self._notes,
[657e52c]790                                      size=(PANEL_WIDTH, -1))
[49ab5d7]791        self.msg_sizer.Add(self._msg_box, 0, wx.LEFT, 10)
792
793    def _layout_button(self):
[6f140f2]794        """
795        Do the layout for the button widgets
[49ab5d7]796        """
[6f140f2]797        self.bt_apply = wx.Button(self, -1, "Apply", size=(_BOX_WIDTH, -1))
798        self.bt_apply.SetToolTipString("Save changes into the imported data.")
799        self.bt_apply.Bind(wx.EVT_BUTTON, self.on_click_apply)
[49ab5d7]800
[4b5fe655]801        self.bt_help = wx.Button(self, -1, "HELP", size=(_BOX_WIDTH, -1))
802        self.bt_help.SetToolTipString("Get Help For Model Editor")
803        self.bt_help.Bind(wx.EVT_BUTTON, self.on_help)
804
[6f140f2]805        self.bt_close = wx.Button(self, -1, 'Close', size=(_BOX_WIDTH, -1))
806        self.bt_close.Bind(wx.EVT_BUTTON, self.on_close)
807        self.bt_close.SetToolTipString("Close this panel.")
[49ab5d7]808
[70c5d490]809        self.button_sizer.AddMany([(self.bt_apply, 0,0),
810                                   (self.bt_help, 0, wx.LEFT | wx.BOTTOM,15),
811                                   (self.bt_close, 0, wx.LEFT | wx.RIGHT, 15)])
[49ab5d7]812
[6f140f2]813    def _do_layout(self):
814        """
815        Draw the current panel
816        """
817        self._define_structure()
818        self._layout_name()
819        self._layout_description()
820        self._layout_param()
821        self._layout_function()
[41a8cb3]822        self._layout_msg()
[6f140f2]823        self._layout_button()
[18ac46b]824        self.main_sizer.AddMany([(self.name_sizer, 0, wx.EXPAND | wx.ALL, 5),
[49ab5d7]825                                 (wx.StaticLine(self), 0,
[18ac46b]826                                  wx.ALL | wx.EXPAND, 5),
827                                 (self.desc_sizer, 0, wx.EXPAND | wx.ALL, 5),
[49ab5d7]828                                 (wx.StaticLine(self), 0,
[18ac46b]829                                  wx.ALL | wx.EXPAND, 5),
830                                 (self.param_sizer, 1, wx.EXPAND | wx.ALL, 5),
[49ab5d7]831                                 (wx.StaticLine(self), 0,
[18ac46b]832                                  wx.ALL | wx.EXPAND, 5),
833                                 (self.function_sizer, 2,
834                                  wx.EXPAND | wx.ALL, 5),
[49ab5d7]835                                 (wx.StaticLine(self), 0,
[18ac46b]836                                  wx.ALL | wx.EXPAND, 5),
837                                 (self.msg_sizer, 0, wx.EXPAND | wx.ALL, 5),
[70c5d490]838                                 (self.button_sizer, 0, wx.ALIGN_RIGHT)])
[6f140f2]839        self.SetSizer(self.main_sizer)
840        self.SetAutoLayout(True)
[318b5bbb]841
842    def _fill_math_combo(self):
843        """
844        Fill up the math combo box
845        """
[49ab5d7]846        self.math_combo = wx.ComboBox(self, -1, size=(100, -1),
847                                      style=wx.CB_READONLY)
[318b5bbb]848        for item in dir(math):
849            if item.count("_") < 1:
850                try:
[49ab5d7]851                    exec "float(math.%s)" % item
[318b5bbb]852                    self.math_combo.Append(str(item))
853                except:
[49ab5d7]854                    self.math_combo.Append(str(item) + "()")
[318b5bbb]855        self.math_combo.Bind(wx.EVT_COMBOBOX, self._on_math_select)
856        self.math_combo.SetSelection(0)
857        return self.math_combo
[49ab5d7]858
[318b5bbb]859    def _on_math_select(self, event):
860        """
861        On math selection on ComboBox
862        """
863        event.Skip()
[2d50115]864        label = self.math_combo.GetValue()
[318b5bbb]865        self.function_tcl.SetFocus()
866        # Put the text at the cursor position
867        pos = self.function_tcl.GetCurrentPos()
[49ab5d7]868        self.function_tcl.InsertText(pos, label)
[2d50115]869        # Put the cursor at appropriate position
[318b5bbb]870        length = len(label)
[2d50115]871        print length
872        if label[length-1] == ')':
[318b5bbb]873            length -= 1
874        f_pos = pos + length
[49ab5d7]875        self.function_tcl.GotoPos(f_pos)
[318b5bbb]876
[6f140f2]877    def get_notes(self):
878        """
879        return notes
880        """
881        return self._notes
[49ab5d7]882
[6f140f2]883    def on_change_name(self, event=None):
884        """
885        Change name
886        """
887        if event is not None:
888            event.Skip()
889        self.name_tcl.SetBackgroundColour('white')
890        self.Refresh()
[49ab5d7]891
[6f140f2]892    def check_name(self):
893        """
894        Check name if exist already
895        """
[41a8cb3]896        self._notes = ''
[6f140f2]897        self.on_change_name(None)
898        plugin_dir = self.path
899        list_fnames = os.listdir(plugin_dir)
900        # function/file name
901        title = self.name_tcl.GetValue().lstrip().rstrip()
902        self.name = title
903        t_fname = title + '.py'
904        if not self.overwrite_name:
905            if t_fname in list_fnames:
906                self.name_tcl.SetBackgroundColour('pink')
907                return False
908        self.fname = os.path.join(plugin_dir, t_fname)
[41a8cb3]909        s_title = title
910        if len(title) > 20:
911            s_title = title[0:19] + '...'
[657e52c]912        self._notes += "Model function name is set "
[41a8cb3]913        self._notes += "to %s. \n" % str(s_title)
[6f140f2]914        return True
[49ab5d7]915
[6f140f2]916    def on_over_cb(self, event):
917        """
918        Set overwrite name flag on cb event
919        """
920        if event is not None:
921            event.Skip()
[18ac46b]922        cb_value = event.GetEventObject()
923        self.overwrite_name = cb_value.GetValue()
[49ab5d7]924
[6f140f2]925    def on_click_apply(self, event):
[49ab5d7]926        """
[18ac46b]927        Changes are saved in data object imported to edit.
[2d50115]928
[18ac46b]929        checks firs for valid name, then if it already exists then checks
930        that a function was entered and finally that if entered it contains at
[2d50115]931        least a return statement.  If all passes writes file then tries to
[18ac46b]932        compile.  If compile fails or import module fails or run method fails
933        tries to remove any .py and pyc files that may have been created and
934        sets error message.
[2d50115]935
936        :todo this code still could do with a careful going over to clean
[18ac46b]937        up and simplify. the non GUI methods such as this one should be removed
938        to computational code of SasView. Most of those computational methods
939        would be the same for both the simple editors.
[6f140f2]940        """
941        #must post event here
942        event.Skip()
[18ac46b]943        name = self.name_tcl.GetValue().lstrip().rstrip()
[6f140f2]944        info = 'Info'
[6057915]945        msg = ''
[26d6e045]946        result, check_err = '', ''
[6f140f2]947        # Sort out the errors if occur
[18ac46b]948        # First check for valid python name then if the name already exists
[ad1ac45]949        if not name or not bool(re.match('^[A-Za-z0-9_]*$', name)):
[94f0873f]950            msg = '"%s" '%name
951            msg += "is not a valid model name. Name must not be empty and \n"
[ad1ac45]952            msg += "may include only alpha numeric or underline characters \n"
953            msg += "and no spaces"
[18ac46b]954        elif self.check_name():
[6f140f2]955            description = self.desc_tcl.GetValue()
956            param_str = self.param_tcl.GetText()
[cf92b1f]957            pd_param_str = self.pd_param_tcl.GetText()
[6f140f2]958            func_str = self.function_tcl.GetText()
959            # No input for the model function
[49ab5d7]960            if func_str.lstrip().rstrip():
[0a90d92]961                if func_str.count('return') > 0:
[cf92b1f]962                    self.write_file(self.fname, name, description, param_str,
963                                    pd_param_str, func_str)
[26d6e045]964                    try:
965                        result, msg = check_model(self.fname), None
966                    except Exception:
967                        import traceback
968                        result, msg = None, "error building model"
969                        check_err = "\n"+traceback.format_exc(limit=2)
[6f140f2]970                else:
[0a90d92]971                    msg = "Error: The func(x) must 'return' a value at least.\n"
972                    msg += "For example: \n\nreturn 2*x"
[6f140f2]973            else:
974                msg = 'Error: Function is not defined.'
975        else:
976            msg = "Name exists already."
[cf92b1f]977
[18ac46b]978        # Prepare the messagebox
[0ed400d]979        if self.base != None and not msg:
980            self.base.update_custom_combo()
[cf92b1f]981            # Passed exception in import test as it will fail for sasmodels.sasview_model class
982            # Should add similar test for new style?
[49ab5d7]983            Model = None
[18ac46b]984            try:
985                exec "from %s import Model" % name
986            except:
[f80b8a9f]987                logging.error(sys.exc_value)
988
[18ac46b]989        # Prepare the messagebox
[0ed400d]990        if msg:
[6f140f2]991            info = 'Error'
[49ab5d7]992            color = 'red'
[0ed400d]993        else:
[26d6e045]994            self._notes = result
[ddbac66]995            msg = "Successful! Please look for %s in Plugin Models."%name
[0ed400d]996            msg += "  " + self._notes
997            info = 'Info'
998            color = 'blue'
[18ac46b]999        self._msg_box.SetLabel(msg)
[41a8cb3]1000        self._msg_box.SetForegroundColour(color)
[18ac46b]1001        # Send msg to the top window
[6f140f2]1002        if self.base != None:
[d85c194]1003            from sas.sasgui.guiframe.events import StatusEvent
[26d6e045]1004            wx.PostEvent(self.base.parent,
1005                         StatusEvent(status=msg+check_err, info=info))
[6f140f2]1006        self.warning = msg
1007
[cf92b1f]1008    def write_file(self, fname, name, desc_str, param_str, pd_param_str, func_str):
[6f140f2]1009        """
1010        Write content in file
[49ab5d7]1011
[6f140f2]1012        :param fname: full file path
1013        :param desc_str: content of the description strings
[49ab5d7]1014        :param param_str: content of params; Strings
[cf92b1f]1015        :param pd_param_str: content of params requiring polydispersity; Strings
[6f140f2]1016        :param func_str: content of func; Strings
[49ab5d7]1017        """
[6f140f2]1018        try:
[49ab5d7]1019            out_f = open(fname, 'w')
[18ac46b]1020        except:
[6f140f2]1021            raise
[e499ca0]1022        # Prepare the content of the function
[6f140f2]1023        lines = CUSTOM_TEMPLATE.split('\n')
[f9feff3]1024
[6f140f2]1025        has_scipy = func_str.count("scipy.")
[cf92b1f]1026        if has_scipy:
1027            lines.insert(0, 'import scipy')
1028       
1029        # Think about 2D later       
1030        #self.is_2d = func_str.count("#self.ndim = 2")
1031        #line_2d = ''
1032        #if self.is_2d:
1033        #    line_2d = CUSTOM_2D_TEMP.split('\n')
1034       
1035        # Also think about test later       
1036        #line_test = TEST_TEMPLATE.split('\n')
1037        #local_params = ''
1038        #spaces = '        '#8spaces
1039        spaces4  = ' '*4
1040        spaces13 = ' '*13
1041        spaces16 = ' '*16     
1042        param_names = []    # to store parameter names
1043        has_scipy = func_str.count("scipy.")
1044        if has_scipy:
1045            lines.insert(0, 'import scipy')
1046
[d83549c]1047        # write function here
[6f140f2]1048        for line in lines:
[18ac46b]1049            # The location where to put the strings is
[6f140f2]1050            # hard-coded in the template as shown below.
[cf92b1f]1051            out_f.write(line + '\n')
1052            if line.count('#name'):
1053                out_f.write('name = "%s" \n' % name)               
1054            elif line.count('#title'):
1055                out_f.write('title = "User model for %s"\n' % name)               
1056            elif line.count('#description'):
1057                out_f.write('description = "%s"\n' % desc_str)               
1058            elif line.count('#parameters'):
1059                out_f.write('parameters = [ \n')
[6f140f2]1060                for param_line in param_str.split('\n'):
1061                    p_line = param_line.lstrip().rstrip()
1062                    if p_line:
[cf92b1f]1063                        pname, pvalue = self.get_param_helper(p_line)
1064                        param_names.append(pname)
1065                        out_f.write("%s['%s', '', %s, [-numpy.inf, numpy.inf], '', ''],\n" % (spaces16, pname, pvalue))
1066                for param_line in pd_param_str.split('\n'):
1067                    p_line = param_line.lstrip().rstrip()
1068                    if p_line:
1069                        pname, pvalue = self.get_param_helper(p_line)
1070                        param_names.append(pname)
1071                        out_f.write("%s['%s', '', %s, [-numpy.inf, numpy.inf], 'volume', ''],\n" % (spaces16, pname, pvalue))
1072                out_f.write('%s]\n' % spaces13)
1073           
1074        # No form_volume or ER available in simple model editor
1075        out_f.write('def form_volume(*arg): \n')
1076        out_f.write('    return 1.0 \n')
1077        out_f.write('\n')
1078        out_f.write('def ER(*arg): \n')
1079        out_f.write('    return 1.0 \n')
1080       
1081        # function to compute
1082        out_f.write('\n')
1083        out_f.write('def Iq(x ')
1084        for name in param_names:
1085            out_f.write(', %s' % name)
1086        out_f.write('):\n')
1087        for func_line in func_str.split('\n'):
1088            out_f.write('%s%s\n' % (spaces4, func_line))
1089       
1090        Iqxy_string = 'return Iq(numpy.sqrt(x**2+y**2) '
1091           
1092        out_f.write('\n')
1093        out_f.write('def Iqxy(x, y ')
1094        for name in param_names:
1095            out_f.write(', %s' % name)
1096            Iqxy_string += ', ' + name
1097        out_f.write('):\n')
1098        Iqxy_string += ')'
1099        out_f.write('%s%s\n' % (spaces4, Iqxy_string))
[49ab5d7]1100
1101        out_f.close()
1102
[cf92b1f]1103    def get_param_helper(self, line):
[6f140f2]1104        """
1105        Get string in line to define the params dictionary
[49ab5d7]1106
[6f140f2]1107        :param line: one line of string got from the param_str
1108        """
1109        items = line.split(";")
1110        for item in items:
1111            name = item.split("=")[0].lstrip().rstrip()
1112            try:
[e499ca0]1113                value = item.split("=")[1].lstrip().rstrip()
[6f140f2]1114                float(value)
1115            except:
[e499ca0]1116                value = 1.0 # default
[49ab5d7]1117
[cf92b1f]1118        return name, value
[6f140f2]1119
[49ab5d7]1120    def set_function_helper(self, line):
[6f140f2]1121        """
1122        Get string in line to define the local params
[49ab5d7]1123
[6f140f2]1124        :param line: one line of string got from the param_str
1125        """
1126        params_str = ''
1127        spaces = '        '#8spaces
1128        items = line.split(";")
1129        for item in items:
1130            name = item.split("=")[0].lstrip().rstrip()
[49ab5d7]1131            params_str += spaces + "%s = self.params['%s']\n" % (name, name)
[6f140f2]1132        return params_str
[49ab5d7]1133
[6f140f2]1134    def get_warning(self):
1135        """
[49ab5d7]1136        Get the warning msg
[6f140f2]1137        """
1138        return self.warning
[49ab5d7]1139
[4b5fe655]1140    def on_help(self, event):
1141        """
[ddbac66]1142        Bring up the New Plugin Model Editor Documentation whenever
[4b5fe655]1143        the HELP button is clicked.
1144
1145        Calls DocumentationWindow with the path of the location within the
1146        documentation tree (after /doc/ ....".  Note that when using old
1147        versions of Wx (before 2.9) and thus not the release version of
1148        installers, the help comes up at the top level of the file as
1149        webbrowser does not pass anything past the # to the browser when it is
1150        running "file:///...."
1151
1152    :param evt: Triggers on clicking the help button
1153    """
1154
[5876f7e]1155        _TreeLocation = "user/sasgui/perspectives/fitting/fitting_help.html"
[a08b89b]1156        _PageAnchor = "#new-plugin-model"
[4b5fe655]1157        _doc_viewer = DocumentationWindow(self, -1, _TreeLocation, _PageAnchor,
[26c8be3]1158                                          "Plugin Model Editor Help")
[4b5fe655]1159
[6f140f2]1160    def on_close(self, event):
1161        """
1162        leave data as it is and close
1163        """
[6057915]1164        self.parent.Show(False)#Close()
[6f140f2]1165        event.Skip()
[49ab5d7]1166
[6f140f2]1167class EditorWindow(wx.Frame):
[d970df9]1168    """
1169    Editor Window
1170    """
[49ab5d7]1171    def __init__(self, parent, base, path, title,
[6f140f2]1172                 size=(EDITOR_WIDTH, EDITOR_HEIGTH), *args, **kwds):
[d970df9]1173        """
1174        Init
1175        """
[6f140f2]1176        kwds["title"] = title
1177        kwds["size"] = size
1178        wx.Frame.__init__(self, parent=None, *args, **kwds)
1179        self.parent = parent
[49ab5d7]1180        self.panel = EditorPanel(parent=self, base=parent,
[6f140f2]1181                                 path=path, title=title)
1182        self.Show(True)
[2d50115]1183        wx.EVT_CLOSE(self, self.on_close)
[49ab5d7]1184
[2d50115]1185    def on_close(self, event):
[6f140f2]1186        """
1187        On close event
1188        """
[6057915]1189        self.Show(False)
1190        #if self.parent != None:
1191        #    self.parent.new_model_frame = None
[18ac46b]1192        #self.Destroy()
[6f140f2]1193
[ddbac66]1194## Templates for plugin models
[cf92b1f]1195
[6f140f2]1196CUSTOM_TEMPLATE = """
[e499ca0]1197from math import *
[316e231]1198import os
1199import sys
[6f140f2]1200import numpy
[cf92b1f]1201
1202#name
1203
1204#title
1205
1206#description
1207
1208#parameters
1209
[d83549c]1210"""
[cf92b1f]1211
[d83549c]1212CUSTOM_2D_TEMP = """
[f505470]1213    def run(self, x=0.0, y=0.0):
[d83549c]1214        if x.__class__.__name__ == 'list':
[f505470]1215            x_val = x[0]
1216            y_val = y[0]*0.0
[d83549c]1217            return self.function(x_val, y_val)
1218        elif x.__class__.__name__ == 'tuple':
1219            msg = "Tuples are not allowed as input to BaseComponent models"
1220            raise ValueError, msg
1221        else:
[7485cfa]1222            return self.function(x, 0.0)
[d83549c]1223    def runXY(self, x=0.0, y=0.0):
1224        if x.__class__.__name__ == 'list':
1225            return self.function(x, y)
1226        elif x.__class__.__name__ == 'tuple':
1227            msg = "Tuples are not allowed as input to BaseComponent models"
1228            raise ValueError, msg
1229        else:
1230            return self.function(x, y)
1231    def evalDistribution(self, qdist):
1232        if qdist.__class__.__name__ == 'list':
1233            msg = "evalDistribution expects a list of 2 ndarrays"
1234            if len(qdist)!=2:
1235                raise RuntimeError, msg
1236            if qdist[0].__class__.__name__ != 'ndarray':
1237                raise RuntimeError, msg
1238            if qdist[1].__class__.__name__ != 'ndarray':
1239                raise RuntimeError, msg
1240            v_model = numpy.vectorize(self.runXY, otypes=[float])
1241            iq_array = v_model(qdist[0], qdist[1])
1242            return iq_array
1243        elif qdist.__class__.__name__ == 'ndarray':
1244            v_model = numpy.vectorize(self.runXY, otypes=[float])
1245            iq_array = v_model(qdist)
1246            return iq_array
1247"""
[7485cfa]1248TEST_TEMPLATE = """
1249######################################################################
[49ab5d7]1250## THIS IS FOR TEST. DO NOT MODIFY THE FOLLOWING LINES!!!!!!!!!!!!!!!!
1251if __name__ == "__main__":
1252    m= Model()
[7485cfa]1253    out1 = m.runXY(0.0)
1254    out2 = m.runXY(0.01)
1255    isfine1 = numpy.isfinite(out1)
1256    isfine2 = numpy.isfinite(out2)
1257    print "Testing the value at Q = 0.0:"
1258    print out1, " : finite? ", isfine1
1259    print "Testing the value at Q = 0.01:"
1260    print out2, " : finite? ", isfine2
1261    if isfine1 and isfine2:
1262        print "===> Simple Test: Passed!"
1263    else:
1264        print "===> Simple Test: Failed!"
1265"""
[6f140f2]1266SUM_TEMPLATE = """
[0008f54]1267# A sample of an experimental model function for Sum/Multiply(Pmodel1,Pmodel2)
[cb4ef58]1268import os
1269import sys
[6f140f2]1270import copy
[d7573c7]1271import collections
[cb4ef58]1272
[bb841ef]1273import numpy
[cb4ef58]1274
[6b1b1cc]1275from sas.sascalc.fit.pluginmodel import Model1DPlugin
[7673ecd]1276from sasmodels.sasview_model import find_model
[6f140f2]1277
1278class Model(Model1DPlugin):
[7673ecd]1279    name = os.path.splitext(os.path.basename(__file__))[0]
1280    is_multiplicity_model = False
1281    def __init__(self, multiplicity=1):
[6f140f2]1282        Model1DPlugin.__init__(self, name='')
[7673ecd]1283        P1 = find_model('%s')
1284        P2 = find_model('%s')
[6f140f2]1285        p_model1 = P1()
1286        p_model2 = P2()
1287        ## Setting  model name model description
[796c4d4]1288        self.description = '%s'
1289        if self.name.rstrip().lstrip() == '':
1290            self.name = self._get_name(p_model1.name, p_model2.name)
1291        if self.description.rstrip().lstrip() == '':
1292            self.description = p_model1.name
1293            self.description += p_model2.name
1294            self.fill_description(p_model1, p_model2)
[6f140f2]1295
1296        ## Define parameters
[d7573c7]1297        self.params = collections.OrderedDict()
[6f140f2]1298
1299        ## Parameter details [units, min, max]
1300        self.details = {}
[318b5bbb]1301        ## Magnetic Panrameters
1302        self.magnetic_params = []
[6f140f2]1303        # non-fittable parameters
[49ab5d7]1304        self.non_fittable = p_model1.non_fittable
1305        self.non_fittable += p_model2.non_fittable
1306
1307        ##models
[6f140f2]1308        self.p_model1= p_model1
1309        self.p_model2= p_model2
[49ab5d7]1310
1311
[6f140f2]1312        ## dispersion
1313        self._set_dispersion()
1314        ## Define parameters
1315        self._set_params()
[d970df9]1316        ## New parameter:scaling_factor
1317        self.params['scale_factor'] = %s
[49ab5d7]1318
[6f140f2]1319        ## Parameter details [units, min, max]
1320        self._set_details()
[cb4ef58]1321        self.details['scale_factor'] = ['', 0.0, numpy.inf]
[6f140f2]1322
[49ab5d7]1323
[6f140f2]1324        #list of parameter that can be fitted
[49ab5d7]1325        self._set_fixed_params()
[7673ecd]1326
[6f140f2]1327        ## parameters with orientation
[7673ecd]1328        self.orientation_params = []
[6f140f2]1329        for item in self.p_model1.orientation_params:
1330            new_item = "p1_" + item
1331            if not new_item in self.orientation_params:
1332                self.orientation_params.append(new_item)
[49ab5d7]1333
[6f140f2]1334        for item in self.p_model2.orientation_params:
1335            new_item = "p2_" + item
1336            if not new_item in self.orientation_params:
1337                self.orientation_params.append(new_item)
[318b5bbb]1338        ## magnetic params
[7673ecd]1339        self.magnetic_params = []
[318b5bbb]1340        for item in self.p_model1.magnetic_params:
1341            new_item = "p1_" + item
1342            if not new_item in self.magnetic_params:
1343                self.magnetic_params.append(new_item)
[49ab5d7]1344
[318b5bbb]1345        for item in self.p_model2.magnetic_params:
1346            new_item = "p2_" + item
1347            if not new_item in self.magnetic_params:
1348                self.magnetic_params.append(new_item)
[6f140f2]1349        # get multiplicity if model provide it, else 1.
1350        try:
1351            multiplicity1 = p_model1.multiplicity
1352            try:
1353                multiplicity2 = p_model2.multiplicity
1354            except:
1355                multiplicity2 = 1
1356        except:
1357            multiplicity1 = 1
1358            multiplicity2 = 1
1359        ## functional multiplicity of the model
[49ab5d7]1360        self.multiplicity1 = multiplicity1
1361        self.multiplicity2 = multiplicity2
1362        self.multiplicity_info = []
1363
[6f140f2]1364    def _clone(self, obj):
[f80b8a9f]1365        import copy
[6f140f2]1366        obj.params     = copy.deepcopy(self.params)
1367        obj.description     = copy.deepcopy(self.description)
1368        obj.details    = copy.deepcopy(self.details)
1369        obj.dispersion = copy.deepcopy(self.dispersion)
1370        obj.p_model1  = self.p_model1.clone()
1371        obj.p_model2  = self.p_model2.clone()
1372        #obj = copy.deepcopy(self)
1373        return obj
[49ab5d7]1374
[6f140f2]1375    def _get_name(self, name1, name2):
[2b4c8ca]1376        p1_name = self._get_upper_name(name1)
1377        if not p1_name:
1378            p1_name = name1
1379        name = p1_name
[30bc3045]1380        name += "_and_"
[2b4c8ca]1381        p2_name = self._get_upper_name(name2)
1382        if not p2_name:
1383            p2_name = name2
1384        name += p2_name
[6f140f2]1385        return name
[49ab5d7]1386
[6f140f2]1387    def _get_upper_name(self, name=None):
1388        if name == None:
1389            return ""
1390        upper_name = ""
1391        str_name = str(name)
1392        for index in range(len(str_name)):
1393            if str_name[index].isupper():
1394                upper_name += str_name[index]
1395        return upper_name
[49ab5d7]1396
[6f140f2]1397    def _set_dispersion(self):
[9501661]1398        self.dispersion = collections.OrderedDict()
[49ab5d7]1399        ##set dispersion only from p_model
[6f140f2]1400        for name , value in self.p_model1.dispersion.iteritems():
1401            #if name.lower() not in self.p_model1.orientation_params:
1402            new_name = "p1_" + name
[49ab5d7]1403            self.dispersion[new_name]= value
[6f140f2]1404        for name , value in self.p_model2.dispersion.iteritems():
1405            #if name.lower() not in self.p_model2.orientation_params:
1406            new_name = "p2_" + name
[49ab5d7]1407            self.dispersion[new_name]= value
1408
1409    def function(self, x=0.0):
[6f140f2]1410        return 0
[49ab5d7]1411
[6f140f2]1412    def getProfile(self):
1413        try:
1414            x,y = self.p_model1.getProfile()
1415        except:
1416            x = None
1417            y = None
[49ab5d7]1418
[6f140f2]1419        return x, y
[49ab5d7]1420
[6f140f2]1421    def _set_params(self):
1422        for name , value in self.p_model1.params.iteritems():
1423            # No 2D-supported
1424            #if name not in self.p_model1.orientation_params:
1425            new_name = "p1_" + name
1426            self.params[new_name]= value
[49ab5d7]1427
[6f140f2]1428        for name , value in self.p_model2.params.iteritems():
1429            # No 2D-supported
1430            #if name not in self.p_model2.orientation_params:
1431            new_name = "p2_" + name
1432            self.params[new_name]= value
[49ab5d7]1433
[6f140f2]1434        # Set "scale" as initializing
1435        self._set_scale_factor()
[49ab5d7]1436
1437
[6f140f2]1438    def _set_details(self):
1439        for name ,detail in self.p_model1.details.iteritems():
1440            new_name = "p1_" + name
1441            #if new_name not in self.orientation_params:
1442            self.details[new_name]= detail
[49ab5d7]1443
[6f140f2]1444        for name ,detail in self.p_model2.details.iteritems():
1445            new_name = "p2_" + name
1446            #if new_name not in self.orientation_params:
1447            self.details[new_name]= detail
[49ab5d7]1448
[6f140f2]1449    def _set_scale_factor(self):
1450        pass
[49ab5d7]1451
1452
[6f140f2]1453    def setParam(self, name, value):
[0008f54]1454        # set param to this (p1, p2) model
[6f140f2]1455        self._setParamHelper(name, value)
[49ab5d7]1456
1457        ## setParam to p model
[be0fe41]1458        model_pre = ''
1459        new_name = ''
1460        name_split = name.split('_', 1)
1461        if len(name_split) == 2:
1462            model_pre = name.split('_', 1)[0]
1463            new_name = name.split('_', 1)[1]
[6f140f2]1464        if model_pre == "p1":
1465            if new_name in self.p_model1.getParamList():
1466                self.p_model1.setParam(new_name, value)
1467        elif model_pre == "p2":
1468             if new_name in self.p_model2.getParamList():
1469                self.p_model2.setParam(new_name, value)
[be0fe41]1470        elif name == 'scale_factor':
[6f140f2]1471            self.params['scale_factor'] = value
1472        else:
1473            raise ValueError, "Model does not contain parameter %s" % name
[49ab5d7]1474
[6f140f2]1475    def getParam(self, name):
1476        # Look for dispersion parameters
1477        toks = name.split('.')
1478        if len(toks)==2:
1479            for item in self.dispersion.keys():
1480                # 2D not supported
1481                if item.lower()==toks[0].lower():
1482                    for par in self.dispersion[item]:
1483                        if par.lower() == toks[1].lower():
1484                            return self.dispersion[item][par]
1485        else:
1486            # Look for standard parameter
1487            for item in self.params.keys():
1488                if item.lower()==name.lower():
1489                    return self.params[item]
[49ab5d7]1490        return
[6f140f2]1491        #raise ValueError, "Model does not contain parameter %s" % name
[49ab5d7]1492
[6f140f2]1493    def _setParamHelper(self, name, value):
1494        # Look for dispersion parameters
1495        toks = name.split('.')
1496        if len(toks)== 2:
1497            for item in self.dispersion.keys():
1498                if item.lower()== toks[0].lower():
1499                    for par in self.dispersion[item]:
1500                        if par.lower() == toks[1].lower():
1501                            self.dispersion[item][par] = value
1502                            return
1503        else:
1504            # Look for standard parameter
1505            for item in self.params.keys():
1506                if item.lower()== name.lower():
1507                    self.params[item] = value
1508                    return
[49ab5d7]1509
[6f140f2]1510        raise ValueError, "Model does not contain parameter %s" % name
[49ab5d7]1511
1512
[6f140f2]1513    def _set_fixed_params(self):
[7673ecd]1514        self.fixed = []
[6f140f2]1515        for item in self.p_model1.fixed:
1516            new_item = "p1" + item
1517            self.fixed.append(new_item)
1518        for item in self.p_model2.fixed:
1519            new_item = "p2" + item
1520            self.fixed.append(new_item)
1521
1522        self.fixed.sort()
[49ab5d7]1523
1524
[6f140f2]1525    def run(self, x = 0.0):
1526        self._set_scale_factor()
[d970df9]1527        return self.params['scale_factor'] %s \
[0008f54]1528(self.p_model1.run(x) %s self.p_model2.run(x))
[49ab5d7]1529
[6f140f2]1530    def runXY(self, x = 0.0):
1531        self._set_scale_factor()
[d970df9]1532        return self.params['scale_factor'] %s \
[0008f54]1533(self.p_model1.runXY(x) %s self.p_model2.runXY(x))
[49ab5d7]1534
1535    ## Now (May27,10) directly uses the model eval function
[6f140f2]1536    ## instead of the for-loop in Base Component.
1537    def evalDistribution(self, x = []):
1538        self._set_scale_factor()
[d970df9]1539        return self.params['scale_factor'] %s \
[0008f54]1540(self.p_model1.evalDistribution(x) %s \
[6f140f2]1541self.p_model2.evalDistribution(x))
1542
1543    def set_dispersion(self, parameter, dispersion):
1544        value= None
1545        new_pre = parameter.split("_", 1)[0]
1546        new_parameter = parameter.split("_", 1)[1]
1547        try:
1548            if new_pre == 'p1' and \
1549new_parameter in self.p_model1.dispersion.keys():
1550                value= self.p_model1.set_dispersion(new_parameter, dispersion)
1551            if new_pre == 'p2' and \
1552new_parameter in self.p_model2.dispersion.keys():
1553                value= self.p_model2.set_dispersion(new_parameter, dispersion)
1554            self._set_dispersion()
1555            return value
1556        except:
[49ab5d7]1557            raise
[6f140f2]1558
1559    def fill_description(self, p_model1, p_model2):
1560        description = ""
[0008f54]1561        description += "This model gives the summation or multiplication of"
1562        description += "%s and %s. "% ( p_model1.name, p_model2.name )
[6f140f2]1563        self.description += description
[49ab5d7]1564
1565if __name__ == "__main__":
1566    m1= Model()
1567    #m1.setParam("p1_scale", 25)
[6f140f2]1568    #m1.setParam("p1_length", 1000)
[49ab5d7]1569    #m1.setParam("p2_scale", 100)
1570    #m1.setParam("p2_rg", 100)
[6f140f2]1571    out1 = m1.runXY(0.01)
1572
1573    m2= Model()
[49ab5d7]1574    #m2.p_model1.setParam("scale", 25)
1575    #m2.p_model1.setParam("length", 1000)
[6f140f2]1576    #m2.p_model2.setParam("scale", 100)
1577    #m2.p_model2.setParam("rg", 100)
[0008f54]1578    out2 = m2.p_model1.runXY(0.01) %s m2.p_model2.runXY(0.01)\n
[fdef956]1579    print "My name is %s."% m1.name
[6f140f2]1580    print out1, " = ", out2
1581    if out1 == out2:
1582        print "===> Simple Test: Passed!"
1583    else:
1584        print "===> Simple Test: Failed!"
1585"""
[49ab5d7]1586
[18ac46b]1587if __name__ == "__main__":
[6f140f2]1588#    app = wx.PySimpleApp()
[2d50115]1589    main_app = wx.App()
1590    main_frame = TextDialog(id=1, model_list=["SphereModel", "CylinderModel"],
[18ac46b]1591                       plugin_dir='../fitting/plugin_models')
[2d50115]1592    main_frame.ShowModal()
1593    main_app.MainLoop()
[7b1f4e3]1594
1595#if __name__ == "__main__":
[d85c194]1596#    from sas.sasgui.perspectives.fitting import models
[7b1f4e3]1597#    dir_path = models.find_plugins_dir()
1598#    app = wx.App()
1599#    window = EditorWindow(parent=None, base=None, path=dir_path, title="Editor")
1600#    app.MainLoop()
Note: See TracBrowser for help on using the repository browser.