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

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.2release_4.0.1ticket-1009ticket-1094-headlessticket-1242-2d-resolutionticket-1243ticket-1249ticket885unittest-saveload
Last change on this file since 0e760e9 was 9501661, checked in by Paul Kienzle <pkienzle@…>, 8 years ago

… and fix the order of the dispersion parameters

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