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

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 a08b89b was a08b89b, checked in by wojciech, 7 years ago

Fixing new plugin editor link anchor. Related to #777

  • Property mode set to 100644
File size: 59.5 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
7complete customizatin is provided.
8
[2d50115]9:TODO the writiong of the file and name checking (and maybe some other
[7b1f4e3]10funtions?) should be moved to a computational module which could be called
11fropm 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
63    to choose two existing models (including pre-existing custom 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:
[49ab5d7]382            msg = "Easy Custom Sum/Multipy: 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
[49ab5d7]503        self.explanation = "  Custom 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    """
619    Custom model function editor
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 = ""
654        self._description = "New Custom Model"
[318b5bbb]655        self.function_tcl = None
[18ac46b]656        self.math_combo = None
657        self.bt_apply = None
658        self.bt_close = None
[6f140f2]659        #self._default_save_location = os.getcwd()
660        self._do_layout()
[18ac46b]661
[6f140f2]662
[49ab5d7]663
[6f140f2]664    def _define_structure(self):
665        """
[49ab5d7]666        define initial sizer
[6f140f2]667        """
668        #w, h = self.parent.GetSize()
669        self.main_sizer = wx.BoxSizer(wx.VERTICAL)
670        self.name_sizer = wx.BoxSizer(wx.VERTICAL)
671        self.name_hsizer = wx.BoxSizer(wx.HORIZONTAL)
672        self.desc_sizer = wx.BoxSizer(wx.VERTICAL)
673        self.param_sizer = wx.BoxSizer(wx.VERTICAL)
674        self.function_sizer = wx.BoxSizer(wx.VERTICAL)
[318b5bbb]675        self.func_horizon_sizer = wx.BoxSizer(wx.HORIZONTAL)
[6f140f2]676        self.button_sizer = wx.BoxSizer(wx.HORIZONTAL)
[41a8cb3]677        self.msg_sizer = wx.BoxSizer(wx.HORIZONTAL)
[49ab5d7]678
[6f140f2]679    def _layout_name(self):
680        """
681        Do the layout for file/function name related widgets
682        """
683        #title name [string]
[49ab5d7]684        name_txt = wx.StaticText(self, -1, 'Function Name : ')
[26c8be3]685        overwrite_cb = wx.CheckBox(self, -1, "Overwrite existing plugin model of this name?", (10, 10))
[94f0873f]686        overwrite_cb.SetValue(False)
[6f140f2]687        overwrite_cb.SetToolTipString("Overwrite it if already exists?")
688        wx.EVT_CHECKBOX(self, overwrite_cb.GetId(), self.on_over_cb)
[49ab5d7]689        self.name_tcl = wx.TextCtrl(self, -1, size=(PANEL_WIDTH * 3 / 5, -1))
[6f140f2]690        self.name_tcl.Bind(wx.EVT_TEXT_ENTER, self.on_change_name)
[94f0873f]691        self.name_tcl.SetValue('')
[6f140f2]692        self.name_tcl.SetFont(self.font)
693        hint_name = "Unique Model Function Name."
694        self.name_tcl.SetToolTipString(hint_name)
[49ab5d7]695        self.name_hsizer.AddMany([(self.name_tcl, 0, wx.LEFT | wx.TOP, 0),
[18ac46b]696                                  (overwrite_cb, 0, wx.LEFT, 20)])
[49ab5d7]697        self.name_sizer.AddMany([(name_txt, 0, wx.LEFT | wx.TOP, 10),
[18ac46b]698                                 (self.name_hsizer, 0,
699                                  wx.LEFT | wx.TOP | wx.BOTTOM, 10)])
[49ab5d7]700
701
[6f140f2]702    def _layout_description(self):
703        """
704        Do the layout for description related widgets
705        """
706        #title name [string]
[49ab5d7]707        desc_txt = wx.StaticText(self, -1, 'Description (optional) : ')
708        self.desc_tcl = wx.TextCtrl(self, -1, size=(PANEL_WIDTH * 3 / 5, -1))
[6f140f2]709        self.desc_tcl.SetValue('')
710        hint_desc = "Write a short description of the model function."
711        self.desc_tcl.SetToolTipString(hint_desc)
[49ab5d7]712        self.desc_sizer.AddMany([(desc_txt, 0, wx.LEFT | wx.TOP, 10),
[18ac46b]713                                 (self.desc_tcl, 0,
714                                  wx.LEFT | wx.TOP | wx.BOTTOM, 10)])
[6f140f2]715    def _layout_param(self):
716        """
717        Do the layout for parameter related widgets
718        """
[70c5d490]719        param_txt = wx.StaticText(self, -1, 'Fit Parameters NOT requiring' + \
720                                  ' polydispersity (if any): ')
[49ab5d7]721
[70c5d490]722        param_tip = "#Set the parameters NOT requiring polydispersity " + \
723        "and their initial values.\n"
[6f140f2]724        param_tip += "#Example:\n"
725        param_tip += "A = 1\nB = 1"
726        #param_txt.SetToolTipString(param_tip)
[18ac46b]727        newid = wx.NewId()
728        self.param_tcl = EditWindow(self, newid, wx.DefaultPosition,
729                                    wx.DefaultSize,
730                                    wx.CLIP_CHILDREN | wx.SUNKEN_BORDER)
[b25caad]731        self.param_tcl.setDisplayLineNumbers(True)
[6f140f2]732        self.param_tcl.SetToolTipString(param_tip)
[318b5bbb]733
[6f140f2]734        self.param_sizer.AddMany([(param_txt, 0, wx.LEFT, 10),
[18ac46b]735                                  (self.param_tcl, 1, wx.EXPAND | wx.ALL, 10)])
[cf92b1f]736       
737        # Parameters with polydispersity
[70c5d490]738        pd_param_txt = wx.StaticText(self, -1, 'Fit Parameters requiring ' + \
739                                     'polydispersity (if any): ')
[cf92b1f]740
[70c5d490]741        pd_param_tip = "#Set the parameters requiring polydispersity and " + \
742        "their initial values.\n"
[cf92b1f]743        pd_param_tip += "#Example:\n"
744        pd_param_tip += "C = 2\nD = 2"
745        newid = wx.NewId()
746        self.pd_param_tcl = EditWindow(self, newid, wx.DefaultPosition,
747                                    wx.DefaultSize,
748                                    wx.CLIP_CHILDREN | wx.SUNKEN_BORDER)
749        self.pd_param_tcl.setDisplayLineNumbers(True)
750        self.pd_param_tcl.SetToolTipString(pd_param_tip)
751       
752        self.param_sizer.AddMany([(pd_param_txt, 0, wx.LEFT, 10),
753                                  (self.pd_param_tcl, 1, wx.EXPAND | wx.ALL, 10)])
[49ab5d7]754
[6f140f2]755    def _layout_function(self):
756        """
757        Do the layout for function related widgets
758        """
[49ab5d7]759        function_txt = wx.StaticText(self, -1, 'Function(x) : ')
[6f140f2]760        hint_function = "#Example:\n"
[e499ca0]761        hint_function += "if x <= 0:\n"
[b25caad]762        hint_function += "    y = A + B\n"
763        hint_function += "else:\n"
[e499ca0]764        hint_function += "    y = A + B * cos(2 * pi * x)\n"
[b25caad]765        hint_function += "return y\n"
[318b5bbb]766        math_txt = wx.StaticText(self, -1, '*Useful math functions: ')
767        math_combo = self._fill_math_combo()
[49ab5d7]768
[18ac46b]769        newid = wx.NewId()
770        self.function_tcl = EditWindow(self, newid, wx.DefaultPosition,
771                                       wx.DefaultSize,
772                                       wx.CLIP_CHILDREN | wx.SUNKEN_BORDER)
[b25caad]773        self.function_tcl.setDisplayLineNumbers(True)
[6f140f2]774        self.function_tcl.SetToolTipString(hint_function)
[49ab5d7]775
[318b5bbb]776        self.func_horizon_sizer.Add(function_txt)
777        self.func_horizon_sizer.Add(math_txt, 0, wx.LEFT, 250)
778        self.func_horizon_sizer.Add(math_combo, 0, wx.LEFT, 10)
779
780        self.function_sizer.Add(self.func_horizon_sizer, 0, wx.LEFT, 10)
[49ab5d7]781        self.function_sizer.Add(self.function_tcl, 1, wx.EXPAND | wx.ALL, 10)
782
[41a8cb3]783    def _layout_msg(self):
784        """
785        Layout msg
786        """
[49ab5d7]787        self._msg_box = wx.StaticText(self, -1, self._notes,
[657e52c]788                                      size=(PANEL_WIDTH, -1))
[49ab5d7]789        self.msg_sizer.Add(self._msg_box, 0, wx.LEFT, 10)
790
791    def _layout_button(self):
[6f140f2]792        """
793        Do the layout for the button widgets
[49ab5d7]794        """
[6f140f2]795        self.bt_apply = wx.Button(self, -1, "Apply", size=(_BOX_WIDTH, -1))
796        self.bt_apply.SetToolTipString("Save changes into the imported data.")
797        self.bt_apply.Bind(wx.EVT_BUTTON, self.on_click_apply)
[49ab5d7]798
[4b5fe655]799        self.bt_help = wx.Button(self, -1, "HELP", size=(_BOX_WIDTH, -1))
800        self.bt_help.SetToolTipString("Get Help For Model Editor")
801        self.bt_help.Bind(wx.EVT_BUTTON, self.on_help)
802
[6f140f2]803        self.bt_close = wx.Button(self, -1, 'Close', size=(_BOX_WIDTH, -1))
804        self.bt_close.Bind(wx.EVT_BUTTON, self.on_close)
805        self.bt_close.SetToolTipString("Close this panel.")
[49ab5d7]806
[70c5d490]807        self.button_sizer.AddMany([(self.bt_apply, 0,0),
808                                   (self.bt_help, 0, wx.LEFT | wx.BOTTOM,15),
809                                   (self.bt_close, 0, wx.LEFT | wx.RIGHT, 15)])
[49ab5d7]810
[6f140f2]811    def _do_layout(self):
812        """
813        Draw the current panel
814        """
815        self._define_structure()
816        self._layout_name()
817        self._layout_description()
818        self._layout_param()
819        self._layout_function()
[41a8cb3]820        self._layout_msg()
[6f140f2]821        self._layout_button()
[18ac46b]822        self.main_sizer.AddMany([(self.name_sizer, 0, wx.EXPAND | wx.ALL, 5),
[49ab5d7]823                                 (wx.StaticLine(self), 0,
[18ac46b]824                                  wx.ALL | wx.EXPAND, 5),
825                                 (self.desc_sizer, 0, wx.EXPAND | wx.ALL, 5),
[49ab5d7]826                                 (wx.StaticLine(self), 0,
[18ac46b]827                                  wx.ALL | wx.EXPAND, 5),
828                                 (self.param_sizer, 1, wx.EXPAND | wx.ALL, 5),
[49ab5d7]829                                 (wx.StaticLine(self), 0,
[18ac46b]830                                  wx.ALL | wx.EXPAND, 5),
831                                 (self.function_sizer, 2,
832                                  wx.EXPAND | wx.ALL, 5),
[49ab5d7]833                                 (wx.StaticLine(self), 0,
[18ac46b]834                                  wx.ALL | wx.EXPAND, 5),
835                                 (self.msg_sizer, 0, wx.EXPAND | wx.ALL, 5),
[70c5d490]836                                 (self.button_sizer, 0, wx.ALIGN_RIGHT)])
[6f140f2]837        self.SetSizer(self.main_sizer)
838        self.SetAutoLayout(True)
[318b5bbb]839
840    def _fill_math_combo(self):
841        """
842        Fill up the math combo box
843        """
[49ab5d7]844        self.math_combo = wx.ComboBox(self, -1, size=(100, -1),
845                                      style=wx.CB_READONLY)
[318b5bbb]846        for item in dir(math):
847            if item.count("_") < 1:
848                try:
[49ab5d7]849                    exec "float(math.%s)" % item
[318b5bbb]850                    self.math_combo.Append(str(item))
851                except:
[49ab5d7]852                    self.math_combo.Append(str(item) + "()")
[318b5bbb]853        self.math_combo.Bind(wx.EVT_COMBOBOX, self._on_math_select)
854        self.math_combo.SetSelection(0)
855        return self.math_combo
[49ab5d7]856
[318b5bbb]857    def _on_math_select(self, event):
858        """
859        On math selection on ComboBox
860        """
861        event.Skip()
[2d50115]862        label = self.math_combo.GetValue()
[318b5bbb]863        self.function_tcl.SetFocus()
864        # Put the text at the cursor position
865        pos = self.function_tcl.GetCurrentPos()
[49ab5d7]866        self.function_tcl.InsertText(pos, label)
[2d50115]867        # Put the cursor at appropriate position
[318b5bbb]868        length = len(label)
[2d50115]869        print length
870        if label[length-1] == ')':
[318b5bbb]871            length -= 1
872        f_pos = pos + length
[49ab5d7]873        self.function_tcl.GotoPos(f_pos)
[318b5bbb]874
[6f140f2]875    def get_notes(self):
876        """
877        return notes
878        """
879        return self._notes
[49ab5d7]880
[6f140f2]881    def on_change_name(self, event=None):
882        """
883        Change name
884        """
885        if event is not None:
886            event.Skip()
887        self.name_tcl.SetBackgroundColour('white')
888        self.Refresh()
[49ab5d7]889
[6f140f2]890    def check_name(self):
891        """
892        Check name if exist already
893        """
[41a8cb3]894        self._notes = ''
[6f140f2]895        self.on_change_name(None)
896        plugin_dir = self.path
897        list_fnames = os.listdir(plugin_dir)
898        # function/file name
899        title = self.name_tcl.GetValue().lstrip().rstrip()
900        self.name = title
901        t_fname = title + '.py'
902        if not self.overwrite_name:
903            if t_fname in list_fnames:
904                self.name_tcl.SetBackgroundColour('pink')
905                return False
906        self.fname = os.path.join(plugin_dir, t_fname)
[41a8cb3]907        s_title = title
908        if len(title) > 20:
909            s_title = title[0:19] + '...'
[657e52c]910        self._notes += "Model function name is set "
[41a8cb3]911        self._notes += "to %s. \n" % str(s_title)
[6f140f2]912        return True
[49ab5d7]913
[6f140f2]914    def on_over_cb(self, event):
915        """
916        Set overwrite name flag on cb event
917        """
918        if event is not None:
919            event.Skip()
[18ac46b]920        cb_value = event.GetEventObject()
921        self.overwrite_name = cb_value.GetValue()
[49ab5d7]922
[6f140f2]923    def on_click_apply(self, event):
[49ab5d7]924        """
[18ac46b]925        Changes are saved in data object imported to edit.
[2d50115]926
[18ac46b]927        checks firs for valid name, then if it already exists then checks
928        that a function was entered and finally that if entered it contains at
[2d50115]929        least a return statement.  If all passes writes file then tries to
[18ac46b]930        compile.  If compile fails or import module fails or run method fails
931        tries to remove any .py and pyc files that may have been created and
932        sets error message.
[2d50115]933
934        :todo this code still could do with a careful going over to clean
[18ac46b]935        up and simplify. the non GUI methods such as this one should be removed
936        to computational code of SasView. Most of those computational methods
937        would be the same for both the simple editors.
[6f140f2]938        """
939        #must post event here
940        event.Skip()
[18ac46b]941        name = self.name_tcl.GetValue().lstrip().rstrip()
[6f140f2]942        info = 'Info'
[6057915]943        msg = ''
[26d6e045]944        result, check_err = '', ''
[6f140f2]945        # Sort out the errors if occur
[18ac46b]946        # First check for valid python name then if the name already exists
[ad1ac45]947        if not name or not bool(re.match('^[A-Za-z0-9_]*$', name)):
[94f0873f]948            msg = '"%s" '%name
949            msg += "is not a valid model name. Name must not be empty and \n"
[ad1ac45]950            msg += "may include only alpha numeric or underline characters \n"
951            msg += "and no spaces"
[18ac46b]952        elif self.check_name():
[6f140f2]953            description = self.desc_tcl.GetValue()
954            param_str = self.param_tcl.GetText()
[cf92b1f]955            pd_param_str = self.pd_param_tcl.GetText()
[6f140f2]956            func_str = self.function_tcl.GetText()
957            # No input for the model function
[49ab5d7]958            if func_str.lstrip().rstrip():
[0a90d92]959                if func_str.count('return') > 0:
[cf92b1f]960                    self.write_file(self.fname, name, description, param_str,
961                                    pd_param_str, func_str)
[26d6e045]962                    try:
963                        result, msg = check_model(self.fname), None
964                    except Exception:
965                        import traceback
966                        result, msg = None, "error building model"
967                        check_err = "\n"+traceback.format_exc(limit=2)
[6f140f2]968                else:
[0a90d92]969                    msg = "Error: The func(x) must 'return' a value at least.\n"
970                    msg += "For example: \n\nreturn 2*x"
[6f140f2]971            else:
972                msg = 'Error: Function is not defined.'
973        else:
974            msg = "Name exists already."
[cf92b1f]975
[18ac46b]976        # Prepare the messagebox
[0ed400d]977        if self.base != None and not msg:
978            self.base.update_custom_combo()
[cf92b1f]979            # Passed exception in import test as it will fail for sasmodels.sasview_model class
980            # Should add similar test for new style?
[49ab5d7]981            Model = None
[18ac46b]982            try:
983                exec "from %s import Model" % name
984            except:
[f80b8a9f]985                logging.error(sys.exc_value)
986
[18ac46b]987        # Prepare the messagebox
[0ed400d]988        if msg:
[6f140f2]989            info = 'Error'
[49ab5d7]990            color = 'red'
[0ed400d]991        else:
[26d6e045]992            self._notes = result
993            msg = "Successful! Please look for %s in Customized Models."%name
[0ed400d]994            msg += "  " + self._notes
995            info = 'Info'
996            color = 'blue'
[18ac46b]997        self._msg_box.SetLabel(msg)
[41a8cb3]998        self._msg_box.SetForegroundColour(color)
[18ac46b]999        # Send msg to the top window
[6f140f2]1000        if self.base != None:
[d85c194]1001            from sas.sasgui.guiframe.events import StatusEvent
[26d6e045]1002            wx.PostEvent(self.base.parent,
1003                         StatusEvent(status=msg+check_err, info=info))
[6f140f2]1004        self.warning = msg
1005
[cf92b1f]1006    def write_file(self, fname, name, desc_str, param_str, pd_param_str, func_str):
[6f140f2]1007        """
1008        Write content in file
[49ab5d7]1009
[6f140f2]1010        :param fname: full file path
1011        :param desc_str: content of the description strings
[49ab5d7]1012        :param param_str: content of params; Strings
[cf92b1f]1013        :param pd_param_str: content of params requiring polydispersity; Strings
[6f140f2]1014        :param func_str: content of func; Strings
[49ab5d7]1015        """
[6f140f2]1016        try:
[49ab5d7]1017            out_f = open(fname, 'w')
[18ac46b]1018        except:
[6f140f2]1019            raise
[e499ca0]1020        # Prepare the content of the function
[6f140f2]1021        lines = CUSTOM_TEMPLATE.split('\n')
[f9feff3]1022
[6f140f2]1023        has_scipy = func_str.count("scipy.")
[cf92b1f]1024        if has_scipy:
1025            lines.insert(0, 'import scipy')
1026       
1027        # Think about 2D later       
1028        #self.is_2d = func_str.count("#self.ndim = 2")
1029        #line_2d = ''
1030        #if self.is_2d:
1031        #    line_2d = CUSTOM_2D_TEMP.split('\n')
1032       
1033        # Also think about test later       
1034        #line_test = TEST_TEMPLATE.split('\n')
1035        #local_params = ''
1036        #spaces = '        '#8spaces
1037        spaces4  = ' '*4
1038        spaces13 = ' '*13
1039        spaces16 = ' '*16     
1040        param_names = []    # to store parameter names
1041        has_scipy = func_str.count("scipy.")
1042        if has_scipy:
1043            lines.insert(0, 'import scipy')
1044
[d83549c]1045        # write function here
[6f140f2]1046        for line in lines:
[18ac46b]1047            # The location where to put the strings is
[6f140f2]1048            # hard-coded in the template as shown below.
[cf92b1f]1049            out_f.write(line + '\n')
1050            if line.count('#name'):
1051                out_f.write('name = "%s" \n' % name)               
1052            elif line.count('#title'):
1053                out_f.write('title = "User model for %s"\n' % name)               
1054            elif line.count('#description'):
1055                out_f.write('description = "%s"\n' % desc_str)               
1056            elif line.count('#parameters'):
1057                out_f.write('parameters = [ \n')
[6f140f2]1058                for param_line in param_str.split('\n'):
1059                    p_line = param_line.lstrip().rstrip()
1060                    if p_line:
[cf92b1f]1061                        pname, pvalue = self.get_param_helper(p_line)
1062                        param_names.append(pname)
1063                        out_f.write("%s['%s', '', %s, [-numpy.inf, numpy.inf], '', ''],\n" % (spaces16, pname, pvalue))
1064                for param_line in pd_param_str.split('\n'):
1065                    p_line = param_line.lstrip().rstrip()
1066                    if p_line:
1067                        pname, pvalue = self.get_param_helper(p_line)
1068                        param_names.append(pname)
1069                        out_f.write("%s['%s', '', %s, [-numpy.inf, numpy.inf], 'volume', ''],\n" % (spaces16, pname, pvalue))
1070                out_f.write('%s]\n' % spaces13)
1071           
1072        # No form_volume or ER available in simple model editor
1073        out_f.write('def form_volume(*arg): \n')
1074        out_f.write('    return 1.0 \n')
1075        out_f.write('\n')
1076        out_f.write('def ER(*arg): \n')
1077        out_f.write('    return 1.0 \n')
1078       
1079        # function to compute
1080        out_f.write('\n')
1081        out_f.write('def Iq(x ')
1082        for name in param_names:
1083            out_f.write(', %s' % name)
1084        out_f.write('):\n')
1085        for func_line in func_str.split('\n'):
1086            out_f.write('%s%s\n' % (spaces4, func_line))
1087       
1088        Iqxy_string = 'return Iq(numpy.sqrt(x**2+y**2) '
1089           
1090        out_f.write('\n')
1091        out_f.write('def Iqxy(x, y ')
1092        for name in param_names:
1093            out_f.write(', %s' % name)
1094            Iqxy_string += ', ' + name
1095        out_f.write('):\n')
1096        Iqxy_string += ')'
1097        out_f.write('%s%s\n' % (spaces4, Iqxy_string))
[49ab5d7]1098
1099        out_f.close()
1100
[cf92b1f]1101    def get_param_helper(self, line):
[6f140f2]1102        """
1103        Get string in line to define the params dictionary
[49ab5d7]1104
[6f140f2]1105        :param line: one line of string got from the param_str
1106        """
1107        items = line.split(";")
1108        for item in items:
1109            name = item.split("=")[0].lstrip().rstrip()
1110            try:
[e499ca0]1111                value = item.split("=")[1].lstrip().rstrip()
[6f140f2]1112                float(value)
1113            except:
[e499ca0]1114                value = 1.0 # default
[49ab5d7]1115
[cf92b1f]1116        return name, value
[6f140f2]1117
[49ab5d7]1118    def set_function_helper(self, line):
[6f140f2]1119        """
1120        Get string in line to define the local params
[49ab5d7]1121
[6f140f2]1122        :param line: one line of string got from the param_str
1123        """
1124        params_str = ''
1125        spaces = '        '#8spaces
1126        items = line.split(";")
1127        for item in items:
1128            name = item.split("=")[0].lstrip().rstrip()
[49ab5d7]1129            params_str += spaces + "%s = self.params['%s']\n" % (name, name)
[6f140f2]1130        return params_str
[49ab5d7]1131
[6f140f2]1132    def get_warning(self):
1133        """
[49ab5d7]1134        Get the warning msg
[6f140f2]1135        """
1136        return self.warning
[49ab5d7]1137
[4b5fe655]1138    def on_help(self, event):
1139        """
1140        Bring up the Custom Model Editor Documentation whenever
1141        the HELP button is clicked.
1142
1143        Calls DocumentationWindow with the path of the location within the
1144        documentation tree (after /doc/ ....".  Note that when using old
1145        versions of Wx (before 2.9) and thus not the release version of
1146        installers, the help comes up at the top level of the file as
1147        webbrowser does not pass anything past the # to the browser when it is
1148        running "file:///...."
1149
1150    :param evt: Triggers on clicking the help button
1151    """
1152
[5876f7e]1153        _TreeLocation = "user/sasgui/perspectives/fitting/fitting_help.html"
[a08b89b]1154        _PageAnchor = "#new-plugin-model"
[4b5fe655]1155        _doc_viewer = DocumentationWindow(self, -1, _TreeLocation, _PageAnchor,
[26c8be3]1156                                          "Plugin Model Editor Help")
[4b5fe655]1157
[6f140f2]1158    def on_close(self, event):
1159        """
1160        leave data as it is and close
1161        """
[6057915]1162        self.parent.Show(False)#Close()
[6f140f2]1163        event.Skip()
[49ab5d7]1164
[6f140f2]1165class EditorWindow(wx.Frame):
[d970df9]1166    """
1167    Editor Window
1168    """
[49ab5d7]1169    def __init__(self, parent, base, path, title,
[6f140f2]1170                 size=(EDITOR_WIDTH, EDITOR_HEIGTH), *args, **kwds):
[d970df9]1171        """
1172        Init
1173        """
[6f140f2]1174        kwds["title"] = title
1175        kwds["size"] = size
1176        wx.Frame.__init__(self, parent=None, *args, **kwds)
1177        self.parent = parent
[49ab5d7]1178        self.panel = EditorPanel(parent=self, base=parent,
[6f140f2]1179                                 path=path, title=title)
1180        self.Show(True)
[2d50115]1181        wx.EVT_CLOSE(self, self.on_close)
[49ab5d7]1182
[2d50115]1183    def on_close(self, event):
[6f140f2]1184        """
1185        On close event
1186        """
[6057915]1187        self.Show(False)
1188        #if self.parent != None:
1189        #    self.parent.new_model_frame = None
[18ac46b]1190        #self.Destroy()
[6f140f2]1191
1192## Templates for custom models
[cf92b1f]1193
[6f140f2]1194CUSTOM_TEMPLATE = """
[e499ca0]1195from math import *
[316e231]1196import os
1197import sys
[6f140f2]1198import numpy
[cf92b1f]1199
1200#name
1201
1202#title
1203
1204#description
1205
1206#parameters
1207
[d83549c]1208"""
[cf92b1f]1209
[d83549c]1210CUSTOM_2D_TEMP = """
[f505470]1211    def run(self, x=0.0, y=0.0):
[d83549c]1212        if x.__class__.__name__ == 'list':
[f505470]1213            x_val = x[0]
1214            y_val = y[0]*0.0
[d83549c]1215            return self.function(x_val, y_val)
1216        elif x.__class__.__name__ == 'tuple':
1217            msg = "Tuples are not allowed as input to BaseComponent models"
1218            raise ValueError, msg
1219        else:
[7485cfa]1220            return self.function(x, 0.0)
[d83549c]1221    def runXY(self, x=0.0, y=0.0):
1222        if x.__class__.__name__ == 'list':
1223            return self.function(x, y)
1224        elif x.__class__.__name__ == 'tuple':
1225            msg = "Tuples are not allowed as input to BaseComponent models"
1226            raise ValueError, msg
1227        else:
1228            return self.function(x, y)
1229    def evalDistribution(self, qdist):
1230        if qdist.__class__.__name__ == 'list':
1231            msg = "evalDistribution expects a list of 2 ndarrays"
1232            if len(qdist)!=2:
1233                raise RuntimeError, msg
1234            if qdist[0].__class__.__name__ != 'ndarray':
1235                raise RuntimeError, msg
1236            if qdist[1].__class__.__name__ != 'ndarray':
1237                raise RuntimeError, msg
1238            v_model = numpy.vectorize(self.runXY, otypes=[float])
1239            iq_array = v_model(qdist[0], qdist[1])
1240            return iq_array
1241        elif qdist.__class__.__name__ == 'ndarray':
1242            v_model = numpy.vectorize(self.runXY, otypes=[float])
1243            iq_array = v_model(qdist)
1244            return iq_array
1245"""
[7485cfa]1246TEST_TEMPLATE = """
1247######################################################################
[49ab5d7]1248## THIS IS FOR TEST. DO NOT MODIFY THE FOLLOWING LINES!!!!!!!!!!!!!!!!
1249if __name__ == "__main__":
1250    m= Model()
[7485cfa]1251    out1 = m.runXY(0.0)
1252    out2 = m.runXY(0.01)
1253    isfine1 = numpy.isfinite(out1)
1254    isfine2 = numpy.isfinite(out2)
1255    print "Testing the value at Q = 0.0:"
1256    print out1, " : finite? ", isfine1
1257    print "Testing the value at Q = 0.01:"
1258    print out2, " : finite? ", isfine2
1259    if isfine1 and isfine2:
1260        print "===> Simple Test: Passed!"
1261    else:
1262        print "===> Simple Test: Failed!"
1263"""
[6f140f2]1264SUM_TEMPLATE = """
[0008f54]1265# A sample of an experimental model function for Sum/Multiply(Pmodel1,Pmodel2)
[cb4ef58]1266import os
1267import sys
[6f140f2]1268import copy
[d7573c7]1269import collections
[cb4ef58]1270
[bb841ef]1271import numpy
[cb4ef58]1272
[6b1b1cc]1273from sas.sascalc.fit.pluginmodel import Model1DPlugin
[7673ecd]1274from sasmodels.sasview_model import find_model
[6f140f2]1275
1276class Model(Model1DPlugin):
[7673ecd]1277    name = os.path.splitext(os.path.basename(__file__))[0]
1278    is_multiplicity_model = False
1279    def __init__(self, multiplicity=1):
[6f140f2]1280        Model1DPlugin.__init__(self, name='')
[7673ecd]1281        P1 = find_model('%s')
1282        P2 = find_model('%s')
[6f140f2]1283        p_model1 = P1()
1284        p_model2 = P2()
1285        ## Setting  model name model description
[796c4d4]1286        self.description = '%s'
1287        if self.name.rstrip().lstrip() == '':
1288            self.name = self._get_name(p_model1.name, p_model2.name)
1289        if self.description.rstrip().lstrip() == '':
1290            self.description = p_model1.name
1291            self.description += p_model2.name
1292            self.fill_description(p_model1, p_model2)
[6f140f2]1293
1294        ## Define parameters
[d7573c7]1295        self.params = collections.OrderedDict()
[6f140f2]1296
1297        ## Parameter details [units, min, max]
1298        self.details = {}
[318b5bbb]1299        ## Magnetic Panrameters
1300        self.magnetic_params = []
[6f140f2]1301        # non-fittable parameters
[49ab5d7]1302        self.non_fittable = p_model1.non_fittable
1303        self.non_fittable += p_model2.non_fittable
1304
1305        ##models
[6f140f2]1306        self.p_model1= p_model1
1307        self.p_model2= p_model2
[49ab5d7]1308
1309
[6f140f2]1310        ## dispersion
1311        self._set_dispersion()
1312        ## Define parameters
1313        self._set_params()
[d970df9]1314        ## New parameter:scaling_factor
1315        self.params['scale_factor'] = %s
[49ab5d7]1316
[6f140f2]1317        ## Parameter details [units, min, max]
1318        self._set_details()
[cb4ef58]1319        self.details['scale_factor'] = ['', 0.0, numpy.inf]
[6f140f2]1320
[49ab5d7]1321
[6f140f2]1322        #list of parameter that can be fitted
[49ab5d7]1323        self._set_fixed_params()
[7673ecd]1324
[6f140f2]1325        ## parameters with orientation
[7673ecd]1326        self.orientation_params = []
[6f140f2]1327        for item in self.p_model1.orientation_params:
1328            new_item = "p1_" + item
1329            if not new_item in self.orientation_params:
1330                self.orientation_params.append(new_item)
[49ab5d7]1331
[6f140f2]1332        for item in self.p_model2.orientation_params:
1333            new_item = "p2_" + item
1334            if not new_item in self.orientation_params:
1335                self.orientation_params.append(new_item)
[318b5bbb]1336        ## magnetic params
[7673ecd]1337        self.magnetic_params = []
[318b5bbb]1338        for item in self.p_model1.magnetic_params:
1339            new_item = "p1_" + item
1340            if not new_item in self.magnetic_params:
1341                self.magnetic_params.append(new_item)
[49ab5d7]1342
[318b5bbb]1343        for item in self.p_model2.magnetic_params:
1344            new_item = "p2_" + item
1345            if not new_item in self.magnetic_params:
1346                self.magnetic_params.append(new_item)
[6f140f2]1347        # get multiplicity if model provide it, else 1.
1348        try:
1349            multiplicity1 = p_model1.multiplicity
1350            try:
1351                multiplicity2 = p_model2.multiplicity
1352            except:
1353                multiplicity2 = 1
1354        except:
1355            multiplicity1 = 1
1356            multiplicity2 = 1
1357        ## functional multiplicity of the model
[49ab5d7]1358        self.multiplicity1 = multiplicity1
1359        self.multiplicity2 = multiplicity2
1360        self.multiplicity_info = []
1361
[6f140f2]1362    def _clone(self, obj):
[f80b8a9f]1363        import copy
[6f140f2]1364        obj.params     = copy.deepcopy(self.params)
1365        obj.description     = copy.deepcopy(self.description)
1366        obj.details    = copy.deepcopy(self.details)
1367        obj.dispersion = copy.deepcopy(self.dispersion)
1368        obj.p_model1  = self.p_model1.clone()
1369        obj.p_model2  = self.p_model2.clone()
1370        #obj = copy.deepcopy(self)
1371        return obj
[49ab5d7]1372
[6f140f2]1373    def _get_name(self, name1, name2):
[2b4c8ca]1374        p1_name = self._get_upper_name(name1)
1375        if not p1_name:
1376            p1_name = name1
1377        name = p1_name
[30bc3045]1378        name += "_and_"
[2b4c8ca]1379        p2_name = self._get_upper_name(name2)
1380        if not p2_name:
1381            p2_name = name2
1382        name += p2_name
[6f140f2]1383        return name
[49ab5d7]1384
[6f140f2]1385    def _get_upper_name(self, name=None):
1386        if name == None:
1387            return ""
1388        upper_name = ""
1389        str_name = str(name)
1390        for index in range(len(str_name)):
1391            if str_name[index].isupper():
1392                upper_name += str_name[index]
1393        return upper_name
[49ab5d7]1394
[6f140f2]1395    def _set_dispersion(self):
[9501661]1396        self.dispersion = collections.OrderedDict()
[49ab5d7]1397        ##set dispersion only from p_model
[6f140f2]1398        for name , value in self.p_model1.dispersion.iteritems():
1399            #if name.lower() not in self.p_model1.orientation_params:
1400            new_name = "p1_" + name
[49ab5d7]1401            self.dispersion[new_name]= value
[6f140f2]1402        for name , value in self.p_model2.dispersion.iteritems():
1403            #if name.lower() not in self.p_model2.orientation_params:
1404            new_name = "p2_" + name
[49ab5d7]1405            self.dispersion[new_name]= value
1406
1407    def function(self, x=0.0):
[6f140f2]1408        return 0
[49ab5d7]1409
[6f140f2]1410    def getProfile(self):
1411        try:
1412            x,y = self.p_model1.getProfile()
1413        except:
1414            x = None
1415            y = None
[49ab5d7]1416
[6f140f2]1417        return x, y
[49ab5d7]1418
[6f140f2]1419    def _set_params(self):
1420        for name , value in self.p_model1.params.iteritems():
1421            # No 2D-supported
1422            #if name not in self.p_model1.orientation_params:
1423            new_name = "p1_" + name
1424            self.params[new_name]= value
[49ab5d7]1425
[6f140f2]1426        for name , value in self.p_model2.params.iteritems():
1427            # No 2D-supported
1428            #if name not in self.p_model2.orientation_params:
1429            new_name = "p2_" + name
1430            self.params[new_name]= value
[49ab5d7]1431
[6f140f2]1432        # Set "scale" as initializing
1433        self._set_scale_factor()
[49ab5d7]1434
1435
[6f140f2]1436    def _set_details(self):
1437        for name ,detail in self.p_model1.details.iteritems():
1438            new_name = "p1_" + name
1439            #if new_name not in self.orientation_params:
1440            self.details[new_name]= detail
[49ab5d7]1441
[6f140f2]1442        for name ,detail in self.p_model2.details.iteritems():
1443            new_name = "p2_" + name
1444            #if new_name not in self.orientation_params:
1445            self.details[new_name]= detail
[49ab5d7]1446
[6f140f2]1447    def _set_scale_factor(self):
1448        pass
[49ab5d7]1449
1450
[6f140f2]1451    def setParam(self, name, value):
[0008f54]1452        # set param to this (p1, p2) model
[6f140f2]1453        self._setParamHelper(name, value)
[49ab5d7]1454
1455        ## setParam to p model
[be0fe41]1456        model_pre = ''
1457        new_name = ''
1458        name_split = name.split('_', 1)
1459        if len(name_split) == 2:
1460            model_pre = name.split('_', 1)[0]
1461            new_name = name.split('_', 1)[1]
[6f140f2]1462        if model_pre == "p1":
1463            if new_name in self.p_model1.getParamList():
1464                self.p_model1.setParam(new_name, value)
1465        elif model_pre == "p2":
1466             if new_name in self.p_model2.getParamList():
1467                self.p_model2.setParam(new_name, value)
[be0fe41]1468        elif name == 'scale_factor':
[6f140f2]1469            self.params['scale_factor'] = value
1470        else:
1471            raise ValueError, "Model does not contain parameter %s" % name
[49ab5d7]1472
[6f140f2]1473    def getParam(self, name):
1474        # Look for dispersion parameters
1475        toks = name.split('.')
1476        if len(toks)==2:
1477            for item in self.dispersion.keys():
1478                # 2D not supported
1479                if item.lower()==toks[0].lower():
1480                    for par in self.dispersion[item]:
1481                        if par.lower() == toks[1].lower():
1482                            return self.dispersion[item][par]
1483        else:
1484            # Look for standard parameter
1485            for item in self.params.keys():
1486                if item.lower()==name.lower():
1487                    return self.params[item]
[49ab5d7]1488        return
[6f140f2]1489        #raise ValueError, "Model does not contain parameter %s" % name
[49ab5d7]1490
[6f140f2]1491    def _setParamHelper(self, name, value):
1492        # Look for dispersion parameters
1493        toks = name.split('.')
1494        if len(toks)== 2:
1495            for item in self.dispersion.keys():
1496                if item.lower()== toks[0].lower():
1497                    for par in self.dispersion[item]:
1498                        if par.lower() == toks[1].lower():
1499                            self.dispersion[item][par] = value
1500                            return
1501        else:
1502            # Look for standard parameter
1503            for item in self.params.keys():
1504                if item.lower()== name.lower():
1505                    self.params[item] = value
1506                    return
[49ab5d7]1507
[6f140f2]1508        raise ValueError, "Model does not contain parameter %s" % name
[49ab5d7]1509
1510
[6f140f2]1511    def _set_fixed_params(self):
[7673ecd]1512        self.fixed = []
[6f140f2]1513        for item in self.p_model1.fixed:
1514            new_item = "p1" + item
1515            self.fixed.append(new_item)
1516        for item in self.p_model2.fixed:
1517            new_item = "p2" + item
1518            self.fixed.append(new_item)
1519
1520        self.fixed.sort()
[49ab5d7]1521
1522
[6f140f2]1523    def run(self, x = 0.0):
1524        self._set_scale_factor()
[d970df9]1525        return self.params['scale_factor'] %s \
[0008f54]1526(self.p_model1.run(x) %s self.p_model2.run(x))
[49ab5d7]1527
[6f140f2]1528    def runXY(self, x = 0.0):
1529        self._set_scale_factor()
[d970df9]1530        return self.params['scale_factor'] %s \
[0008f54]1531(self.p_model1.runXY(x) %s self.p_model2.runXY(x))
[49ab5d7]1532
1533    ## Now (May27,10) directly uses the model eval function
[6f140f2]1534    ## instead of the for-loop in Base Component.
1535    def evalDistribution(self, x = []):
1536        self._set_scale_factor()
[d970df9]1537        return self.params['scale_factor'] %s \
[0008f54]1538(self.p_model1.evalDistribution(x) %s \
[6f140f2]1539self.p_model2.evalDistribution(x))
1540
1541    def set_dispersion(self, parameter, dispersion):
1542        value= None
1543        new_pre = parameter.split("_", 1)[0]
1544        new_parameter = parameter.split("_", 1)[1]
1545        try:
1546            if new_pre == 'p1' and \
1547new_parameter in self.p_model1.dispersion.keys():
1548                value= self.p_model1.set_dispersion(new_parameter, dispersion)
1549            if new_pre == 'p2' and \
1550new_parameter in self.p_model2.dispersion.keys():
1551                value= self.p_model2.set_dispersion(new_parameter, dispersion)
1552            self._set_dispersion()
1553            return value
1554        except:
[49ab5d7]1555            raise
[6f140f2]1556
1557    def fill_description(self, p_model1, p_model2):
1558        description = ""
[0008f54]1559        description += "This model gives the summation or multiplication of"
1560        description += "%s and %s. "% ( p_model1.name, p_model2.name )
[6f140f2]1561        self.description += description
[49ab5d7]1562
1563if __name__ == "__main__":
1564    m1= Model()
1565    #m1.setParam("p1_scale", 25)
[6f140f2]1566    #m1.setParam("p1_length", 1000)
[49ab5d7]1567    #m1.setParam("p2_scale", 100)
1568    #m1.setParam("p2_rg", 100)
[6f140f2]1569    out1 = m1.runXY(0.01)
1570
1571    m2= Model()
[49ab5d7]1572    #m2.p_model1.setParam("scale", 25)
1573    #m2.p_model1.setParam("length", 1000)
[6f140f2]1574    #m2.p_model2.setParam("scale", 100)
1575    #m2.p_model2.setParam("rg", 100)
[0008f54]1576    out2 = m2.p_model1.runXY(0.01) %s m2.p_model2.runXY(0.01)\n
[fdef956]1577    print "My name is %s."% m1.name
[6f140f2]1578    print out1, " = ", out2
1579    if out1 == out2:
1580        print "===> Simple Test: Passed!"
1581    else:
1582        print "===> Simple Test: Failed!"
1583"""
[49ab5d7]1584
[18ac46b]1585if __name__ == "__main__":
[6f140f2]1586#    app = wx.PySimpleApp()
[2d50115]1587    main_app = wx.App()
1588    main_frame = TextDialog(id=1, model_list=["SphereModel", "CylinderModel"],
[18ac46b]1589                       plugin_dir='../fitting/plugin_models')
[2d50115]1590    main_frame.ShowModal()
1591    main_app.MainLoop()
[7b1f4e3]1592
1593#if __name__ == "__main__":
[d85c194]1594#    from sas.sasgui.perspectives.fitting import models
[7b1f4e3]1595#    dir_path = models.find_plugins_dir()
1596#    app = wx.App()
1597#    window = EditorWindow(parent=None, base=None, path=dir_path, title="Editor")
1598#    app.MainLoop()
Note: See TracBrowser for help on using the repository browser.