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

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 be34c71 was 7801df8, checked in by butler, 10 years ago

Clean up minor errors in previous commits and add General Fitting Help
and button sizer and even handler for smearing help (but still need to
add button to panel so can be used.

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