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

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 aa03e0d was aa03e0d, checked in by gonzalezm, 8 years ago

Fix call to make_class in template for sum of 2 models

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