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

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

Fix call to make_class in template for sum of 2 models

  • Property mode set to 100644
File size: 61.6 KB
Line 
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
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
6standard available math functions.  Finally a full python editor panel for
7complete customizatin is provided.
8
9:TODO the writiong of the file and name checking (and maybe some other
10funtions?) should be moved to a computational module which could be called
11fropm a python script.  Basically one just needs to pass the name,
12description text and function text (or in the case of the composite editor
13the names of the first and second model and the operator to be used).
14'''
15
16################################################################################
17#This software was developed by the University of Tennessee as part of the
18#Distributed Data Analysis of Neutron Scattering Experiments (DANSE)
19#project funded by the US National Science Foundation.
20#
21#See the license text in license.txt
22#
23#copyright 2009, University of Tennessee
24################################################################################
25import wx
26import sys
27import os
28import math
29import re
30from wx.py.editwindow import EditWindow
31from sas.sasgui.guiframe.documentation_window import DocumentationWindow
32
33
34if sys.platform.count("win32") > 0:
35    FONT_VARIANT = 0
36    PNL_WIDTH = 450
37    PNL_HEIGHT = 320
38else:
39    FONT_VARIANT = 1
40    PNL_WIDTH = 590
41    PNL_HEIGHT = 350
42M_NAME = 'Model'
43EDITOR_WIDTH = 800
44EDITOR_HEIGTH = 735
45PANEL_WIDTH = 500
46_BOX_WIDTH = 55
47
48
49def _compile_file(path):
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:
58        _, value, _ = sys.exc_info()
59        return value
60
61def _delete_file(path):
62    """
63    Delete file in the path
64    """
65    try:
66        os.remove(path)
67    except:
68        raise
69
70
71class TextDialog(wx.Dialog):
72    """
73    Dialog for easy custom composite models.  Provides a wx.Dialog panel
74    to choose two existing models (including pre-existing custom models which
75    may themselves be composite models) as well as an operation on those models
76    (add or multiply) the resulting model will add a scale parameter for summed
77    models and a background parameter for a multiplied model.
78
79    The user also gives a brief help for the model in a description box and
80    must provide a unique name which is verified as unique before the new
81    model is saved.
82
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.
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
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.
93
94    - PDB:April 5, 2015
95    """
96    def __init__(self, parent=None, base=None, id=None, title='',
97                 model_list=[], plugin_dir=None):
98        """
99        This class is run when instatiated.  The __init__ initializes and
100        calls the internal methods necessary.  On exiting the wx.Dialog
101        window should be destroyed.
102        """
103        wx.Dialog.__init__(self, parent=parent, id=id,
104                           title=title, size=(PNL_WIDTH, PNL_HEIGHT))
105        self.parent = base
106        #Font
107        self.SetWindowVariant(variant=FONT_VARIANT)
108        # default
109        self.overwrite_name = False
110        self.plugin_dir = plugin_dir
111        self.model_list = model_list
112        self.model1_string = "sphere"
113        self.model2_string = "cylinder"
114        self.name = 'Sum' + M_NAME
115        self.factor = 'scale_factor'
116        self._notes = ''
117        self._operator = '+'
118        self._operator_choice = None
119        self.explanation = ''
120        self.explanationctr = None
121        self.type = None
122        self.name_sizer = None
123        self.name_tcl = None
124        self.desc_sizer = None
125        self.desc_tcl = None
126        self._selection_box = None
127        self.model1 = None
128        self.model2 = None
129        self.static_line_1 = None
130        self.ok_button = None
131        self.close_button = None
132        self._msg_box = None
133        self.msg_sizer = None
134        self.fname = None
135        self.cm_list = None
136        self.is_p1_custom = False
137        self.is_p2_custom = False
138        self._build_sizer()
139        self.model1_name = str(self.model1.GetValue())
140        self.model2_name = str(self.model2.GetValue())
141        self.good_name = True
142        self.fill_oprator_combox()
143
144    def _layout_name(self):
145        """
146        Do the layout for file/function name related widgets
147        """
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
152        name_txt = wx.StaticText(self, -1, 'Function Name : ')
153        self.name_tcl = wx.TextCtrl(self, -1, value='MySumFunction')
154        self.name_tcl.Bind(wx.EVT_TEXT_ENTER, self.on_change_name)
155        hint_name = "Unique Sum/Multiply Model Function Name."
156        self.name_tcl.SetToolTipString(hint_name)
157
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)])
162
163
164    def _layout_description(self):
165        """
166        Do the layout for description related widgets
167        """
168        #container for new model description input
169        self.desc_sizer = wx.BoxSizer(wx.HORIZONTAL)
170
171        #set up description label and input box with tool tip and event handling
172        desc_txt = wx.StaticText(self, -1, 'Description (optional) : ')
173        self.desc_tcl = wx.TextCtrl(self, -1)
174        hint_desc = "Write a short description of this model function."
175        self.desc_tcl.SetToolTipString(hint_desc)
176
177        self.desc_sizer.AddMany([(desc_txt, 0, wx.LEFT | wx.TOP, 10),
178                                 (self.desc_tcl, -1,
179                                  wx.EXPAND | wx.RIGHT | wx.TOP | wx.BOTTOM,
180                                  10)])
181
182
183    def _layout_model_selection(self):
184        """
185        Do the layout for model selection related widgets
186        """
187        box_width = 195 # combobox width
188
189        #First set up main sizer for the selection
190        selection_box_title = wx.StaticBox(self, -1, 'Select',
191                                           size=(PNL_WIDTH - 30, 70))
192        self._selection_box = wx.StaticBoxSizer(selection_box_title,
193                                                wx.VERTICAL)
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):"
198        select_help_box.Add(wx.StaticText(self, -1, model_string % (1, 1)),
199                            0, 0)
200        select_help_box.Add((box_width - 25, 10), 0, 0)
201        select_help_box.Add(wx.StaticText(self, -1, model_string % (2, 2)),
202                            0, 0)
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
208        self.model1 = wx.ComboBox(self, -1, style=wx.CB_READONLY)
209        wx.EVT_COMBOBOX(self.model1, -1, self.on_model1)
210        self.model1.SetMinSize((box_width * 5 / 6, -1))
211        self.model1.SetToolTipString("model1")
212
213        self._operator_choice = wx.ComboBox(self, -1, size=(50, -1),
214                                            style=wx.CB_READONLY)
215        wx.EVT_COMBOBOX(self._operator_choice, -1, self.on_select_operator)
216        operation_tip = "Add: +, Multiply: * "
217        self._operator_choice.SetToolTipString(operation_tip)
218
219        self.model2 = wx.ComboBox(self, -1, style=wx.CB_READONLY)
220        wx.EVT_COMBOBOX(self.model2, -1, self.on_model2)
221        self.model2.SetMinSize((box_width * 5 / 6, -1))
222        self.model2.SetToolTipString("model2")
223        self._set_model_list()
224
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)
230        # add some space between labels and selection
231        self._selection_box.Add((20, 5), 0, 0)
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
237        and _layout_model_selection which each build a their portion of the
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()
245        mainsizer.Add(self.name_sizer, 0, wx.EXPAND)
246        self._layout_description()
247        mainsizer.Add(self.desc_sizer, 0, wx.EXPAND)
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)
252        mainsizer.Add(self.explanationctr, 0, wx.LEFT | wx.EXPAND, 15)
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))
262        self.static_line_1 = wx.StaticLine(self, -1)
263        mainsizer.Add(self.static_line_1, 0, wx.EXPAND, 10)
264
265        # Add action status notification line (null at startup)
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)
269        mainsizer.Add(self.msg_sizer, 0,
270                      wx.LEFT | wx.RIGHT | wx.ADJUST_MINSIZE | wx.BOTTOM, 10)
271
272        # Finally add the buttons (apply and close) on the bottom
273        # Eventually need to add help here
274        self.ok_button = wx.Button(self, wx.ID_OK, 'Apply')
275        _app_tip = "Save the new Model."
276        self.ok_button.SetToolTipString(_app_tip)
277        self.ok_button.Bind(wx.EVT_BUTTON, self.check_name)
278        self.help_button = wx.Button(self, -1, 'HELP')
279        _app_tip = "Help on composite model creation."
280        self.help_button.SetToolTipString(_app_tip)
281        self.help_button.Bind(wx.EVT_BUTTON, self.on_help)
282        self.close_button = wx.Button(self, wx.ID_CANCEL, 'Close')
283        sizer_button = wx.BoxSizer(wx.HORIZONTAL)
284        sizer_button.AddMany([((20, 20), 1, 0),
285                              (self.ok_button, 0, 0),
286                              (self.help_button, 0, 0),
287                              (self.close_button, 0, wx.LEFT | wx.RIGHT, 10)])
288        mainsizer.Add(sizer_button, 0, wx.EXPAND | wx.BOTTOM | wx.TOP, 10)
289
290        self.SetSizer(mainsizer)
291        self.Centre()
292
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()
301
302    def check_name(self, event=None):
303        """
304        Check that proposed new model name is a valid Python module name
305        and that it does not already exist. If not show error message and
306        pink background in text box else call on_apply
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
310        option also need to pass the self._operator attribute) We just need
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
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.
316        """
317        #Get the function/file name
318        mname = M_NAME
319        self.on_change_name(None)
320        title = self.name_tcl.GetValue().lstrip().rstrip()
321        if title == '':
322            text = self._operator
323            if text.count('+') > 0:
324                mname = 'Sum'
325            else:
326                mname = 'Multi'
327            mname += M_NAME
328            title = mname
329        self.name = title
330        t_fname = title + '.py'
331
332        #First check if the name is a valid Python name
333        if re.match('^[A-Za-z0-9_]*$', title):
334            self.good_name = True
335        else:
336            self.good_name = False
337            msg = ("%s is not a valid Python name. Only alphanumeric \n" \
338                   "and underscore allowed" % self.name)
339
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)
347            if t_fname in list_fnames and title != mname:
348                self.good_name = False
349                msg = "Name exists already."
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
360        self.fname = os.path.join(self.plugin_dir, t_fname)
361        s_title = title
362        if len(title) > 20:
363            s_title = title[0:19] + '...'
364        self._notes = "Model function (%s) has been set! \n" % str(s_title)
365        self.good_name = True
366        self.on_apply(self.fname)
367        return self.good_name
368
369    def on_apply(self, path):
370        """
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.
374
375        :TODO this should be bound to the apply button.  The first line
376        should call the check_name method which itself should be in another
377        module separated from the the GUI modules.
378        """
379        self.name_tcl.SetBackgroundColour('white')
380        try:
381            label = self.get_textnames()
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()
388            msg = self._notes
389            info = 'Info'
390            color = 'blue'
391        except:
392            msg = "Easy Custom Sum/Multipy: Error occurred..."
393            info = 'Error'
394            color = 'red'
395        self._msg_box.SetLabel(msg)
396        self._msg_box.SetForegroundColour(color)
397        if self.parent.parent != None:
398            from sas.sasgui.guiframe.events import StatusEvent
399            wx.PostEvent(self.parent.parent, StatusEvent(status=msg,
400                                                         info=info))
401        else:
402            raise
403
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/sasgui/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
424    def _set_model_list(self):
425        """
426        Set the list of models
427        """
428        # list of model names
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)
443        self.model1.SetStringSelection(self.model1_string)
444        self.model2.SetStringSelection(self.model2_string)
445
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)
457        self.cm_list = cm_list
458
459    def on_model1(self, event):
460        """
461        Set model1
462        """
463        event.Skip()
464        self.update_cm_list()
465        self.model1_name = str(self.model1.GetValue())
466        self.model1_string = self.model1_name
467        if self.model1_name in self.cm_list:
468            self.is_p1_custom = True
469        else:
470            self.is_p1_custom = False
471
472    def on_model2(self, event):
473        """
474        Set model2
475        """
476        event.Skip()
477        self.update_cm_list()
478        self.model2_name = str(self.model2.GetValue())
479        self.model2_string = self.model2_name
480        if self.model2_name in self.cm_list:
481            self.is_p2_custom = True
482        else:
483            self.is_p2_custom = False
484
485    def on_select_operator(self, event=None):
486        """
487        On Select an Operator
488        """
489        # For Mac
490        if event != None:
491            event.Skip()
492        item = event.GetEventObject()
493        text = item.GetValue()
494        self.fill_explanation_helpstring(text)
495
496    def fill_explanation_helpstring(self, operator):
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
504        if operator == '*':
505            name = 'Multi'
506            factor = 'BackGround'
507            f_oper = '+'
508        else:
509            name = 'Sum'
510            factor = 'scale_factor'
511            f_oper = '*'
512
513        self.factor = factor
514        self._operator = operator
515        self.explanation = "  Custom Model = %s %s (model1 %s model2)\n" % \
516                           (self.factor, f_oper, self._operator)
517        self.explanationctr.SetLabel(self.explanation)
518        self.name = name + M_NAME
519
520
521    def fill_oprator_combox(self):
522        """
523        fill the current combobox with the operator
524        """
525        operator_list = [' +', ' *']
526        for oper in operator_list:
527            pos = self._operator_choice.Append(str(oper))
528            self._operator_choice.SetClientData(pos, str(oper.strip()))
529        self._operator_choice.SetSelection(0)
530
531    def get_textnames(self):
532        """
533        Returns model name string as list
534        """
535        return [self.model1_name, self.model2_name]
536
537    def write_string(self, fname, name1, name2):
538        """
539        Write and Save file
540        """
541        self.fname = fname
542        description = self.desc_tcl.GetValue().lstrip().rstrip()
543        if description == '':
544            description = name1 + self._operator + name2
545        text = self._operator_choice.GetValue()
546        if text.count('+') > 0:
547            factor = 'scale_factor'
548            f_oper = '*'
549            default_val = '1.0'
550        else:
551            factor = 'BackGround'
552            f_oper = '+'
553            default_val = '0.0'
554        path = self.fname
555        try:
556            out_f = open(path, 'w')
557        except:
558            raise
559        lines = SUM_TEMPLATE.split('\n')
560        for line in lines:
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) + "\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) + "\n")
589                    else:
590                        out_f.write(line + "\n")
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
596                elif line.count("self.description = '%s'"):
597                    out_f.write(line % description + "\n")
598                #elif line.count("run") and line.count("%s"):
599                #    out_f.write(line % self._operator + "\n")
600                #elif line.count("evalDistribution") and line.count("%s"):
601                #    out_f.write(line % self._operator + "\n")
602                elif line.count("return") and line.count("%s") == 2:
603                    #print "line return", line
604                    out_f.write(line % (f_oper, self._operator) + "\n")
605                elif line.count("out2")and line.count("%s"):
606                    out_f.write(line % self._operator + "\n")
607                else:
608                    out_f.write(line + "\n")
609            except:
610                raise
611        out_f.close()
612        #else:
613        #    msg = "Name exists already."
614
615    def compile_file(self, path):
616        """
617        Compile the file in the path
618        """
619        path = self.fname
620        _compile_file(path)
621
622    def delete_file(self, path):
623        """
624        Delete file in the path
625        """
626        _delete_file(path)
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
635#        kwds["size"] = (EDITOR_WIDTH, EDITOR_HEIGTH)
636        kwds["style"] = wx.FULL_REPAINT_ON_RESIZE
637        wx.ScrolledWindow.__init__(self, parent, *args, **kwds)
638        self.SetScrollbars(1,1,1,1)
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'
646        self.overwrite_name = False
647        self.is_2d = False
648        self.fname = None
649        self.main_sizer = None
650        self.name_sizer = None
651        self.name_hsizer = None
652        self.name_tcl = None
653        self.desc_sizer = None
654        self.desc_tcl = None
655        self.param_sizer = None
656        self.param_tcl = None
657        self.function_sizer = None
658        self.func_horizon_sizer = None
659        self.button_sizer = None
660        self.param_strings = ''
661        self.function_strings = ''
662        self._notes = ""
663        self._msg_box = None
664        self.msg_sizer = None
665        self.warning = ""
666        self._description = "New Custom Model"
667        self.function_tcl = None
668        self.math_combo = None
669        self.bt_apply = None
670        self.bt_close = None
671        #self._default_save_location = os.getcwd()
672        self._do_layout()
673
674
675
676    def _define_structure(self):
677        """
678        define initial sizer
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)
687        self.func_horizon_sizer = wx.BoxSizer(wx.HORIZONTAL)
688        self.button_sizer = wx.BoxSizer(wx.HORIZONTAL)
689        self.msg_sizer = wx.BoxSizer(wx.HORIZONTAL)
690
691    def _layout_name(self):
692        """
693        Do the layout for file/function name related widgets
694        """
695        #title name [string]
696        name_txt = wx.StaticText(self, -1, 'Function Name : ')
697        overwrite_cb = wx.CheckBox(self, -1, "Overwrite?", (10, 10))
698        overwrite_cb.SetValue(False)
699        overwrite_cb.SetToolTipString("Overwrite it if already exists?")
700        wx.EVT_CHECKBOX(self, overwrite_cb.GetId(), self.on_over_cb)
701        #overwrite_cb.Show(False)
702        self.name_tcl = wx.TextCtrl(self, -1, size=(PANEL_WIDTH * 3 / 5, -1))
703        self.name_tcl.Bind(wx.EVT_TEXT_ENTER, self.on_change_name)
704        self.name_tcl.SetValue('MyFunction')
705        self.name_tcl.SetFont(self.font)
706        hint_name = "Unique Model Function Name."
707        self.name_tcl.SetToolTipString(hint_name)
708        self.name_hsizer.AddMany([(self.name_tcl, 0, wx.LEFT | wx.TOP, 0),
709                                  (overwrite_cb, 0, wx.LEFT, 20)])
710        self.name_sizer.AddMany([(name_txt, 0, wx.LEFT | wx.TOP, 10),
711                                 (self.name_hsizer, 0,
712                                  wx.LEFT | wx.TOP | wx.BOTTOM, 10)])
713
714
715    def _layout_description(self):
716        """
717        Do the layout for description related widgets
718        """
719        #title name [string]
720        desc_txt = wx.StaticText(self, -1, 'Description (optional) : ')
721        self.desc_tcl = wx.TextCtrl(self, -1, size=(PANEL_WIDTH * 3 / 5, -1))
722        self.desc_tcl.SetValue('')
723        hint_desc = "Write a short description of the model function."
724        self.desc_tcl.SetToolTipString(hint_desc)
725        self.desc_sizer.AddMany([(desc_txt, 0, wx.LEFT | wx.TOP, 10),
726                                 (self.desc_tcl, 0,
727                                  wx.LEFT | wx.TOP | wx.BOTTOM, 10)])
728    def _layout_param(self):
729        """
730        Do the layout for parameter related widgets
731        """
732        param_txt = wx.StaticText(self, -1, 'Fit Parameters (if any): ')
733
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)
738        newid = wx.NewId()
739        self.param_tcl = EditWindow(self, newid, wx.DefaultPosition,
740                                    wx.DefaultSize,
741                                    wx.CLIP_CHILDREN | wx.SUNKEN_BORDER)
742        self.param_tcl.setDisplayLineNumbers(True)
743        self.param_tcl.SetToolTipString(param_tip)
744
745        self.param_sizer.AddMany([(param_txt, 0, wx.LEFT, 10),
746                                  (self.param_tcl, 1, wx.EXPAND | wx.ALL, 10)])
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)])
763
764    def _layout_function(self):
765        """
766        Do the layout for function related widgets
767        """
768        function_txt = wx.StaticText(self, -1, 'Function(x) : ')
769        hint_function = "#Example:\n"
770        hint_function += "if x <= 0:\n"
771        hint_function += "    y = A + B\n"
772        hint_function += "else:\n"
773        hint_function += "    y = A + B * cos(2 * pi * x)\n"
774        hint_function += "return y\n"
775        math_txt = wx.StaticText(self, -1, '*Useful math functions: ')
776        math_combo = self._fill_math_combo()
777
778        newid = wx.NewId()
779        self.function_tcl = EditWindow(self, newid, wx.DefaultPosition,
780                                       wx.DefaultSize,
781                                       wx.CLIP_CHILDREN | wx.SUNKEN_BORDER)
782        self.function_tcl.setDisplayLineNumbers(True)
783        self.function_tcl.SetToolTipString(hint_function)
784
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)
790        self.function_sizer.Add(self.function_tcl, 1, wx.EXPAND | wx.ALL, 10)
791
792    def _layout_msg(self):
793        """
794        Layout msg
795        """
796        self._msg_box = wx.StaticText(self, -1, self._notes,
797                                      size=(PANEL_WIDTH, -1))
798        self.msg_sizer.Add(self._msg_box, 0, wx.LEFT, 10)
799
800    def _layout_button(self):
801        """
802        Do the layout for the button widgets
803        """
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)
807
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
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.")
815
816        self.button_sizer.AddMany([(self.bt_apply, 0,
817                                    wx.LEFT, EDITOR_WIDTH * 0.8),
818                                   (self.bt_help, 0,
819                                    wx.LEFT,15),
820                                   (self.bt_close, 0,
821                                    wx.LEFT | wx.BOTTOM, 15)])
822
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()
832        self._layout_msg()
833        self._layout_button()
834        self.main_sizer.AddMany([(self.name_sizer, 0, wx.EXPAND | wx.ALL, 5),
835                                 (wx.StaticLine(self), 0,
836                                  wx.ALL | wx.EXPAND, 5),
837                                 (self.desc_sizer, 0, wx.EXPAND | wx.ALL, 5),
838                                 (wx.StaticLine(self), 0,
839                                  wx.ALL | wx.EXPAND, 5),
840                                 (self.param_sizer, 1, wx.EXPAND | wx.ALL, 5),
841                                 (wx.StaticLine(self), 0,
842                                  wx.ALL | wx.EXPAND, 5),
843                                 (self.function_sizer, 2,
844                                  wx.EXPAND | wx.ALL, 5),
845                                 (wx.StaticLine(self), 0,
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)])
849        self.SetSizer(self.main_sizer)
850        self.SetAutoLayout(True)
851
852    def _fill_math_combo(self):
853        """
854        Fill up the math combo box
855        """
856        self.math_combo = wx.ComboBox(self, -1, size=(100, -1),
857                                      style=wx.CB_READONLY)
858        for item in dir(math):
859            if item.count("_") < 1:
860                try:
861                    exec "float(math.%s)" % item
862                    self.math_combo.Append(str(item))
863                except:
864                    self.math_combo.Append(str(item) + "()")
865        self.math_combo.Bind(wx.EVT_COMBOBOX, self._on_math_select)
866        self.math_combo.SetSelection(0)
867        return self.math_combo
868
869    def _on_math_select(self, event):
870        """
871        On math selection on ComboBox
872        """
873        event.Skip()
874        label = self.math_combo.GetValue()
875        self.function_tcl.SetFocus()
876        # Put the text at the cursor position
877        pos = self.function_tcl.GetCurrentPos()
878        self.function_tcl.InsertText(pos, label)
879        # Put the cursor at appropriate position
880        length = len(label)
881        print length
882        if label[length-1] == ')':
883            length -= 1
884        f_pos = pos + length
885        self.function_tcl.GotoPos(f_pos)
886
887    def get_notes(self):
888        """
889        return notes
890        """
891        return self._notes
892
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()
901
902    def check_name(self):
903        """
904        Check name if exist already
905        """
906        self._notes = ''
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)
919        s_title = title
920        if len(title) > 20:
921            s_title = title[0:19] + '...'
922        self._notes += "Model function name is set "
923        self._notes += "to %s. \n" % str(s_title)
924        return True
925
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()
932        cb_value = event.GetEventObject()
933        self.overwrite_name = cb_value.GetValue()
934
935    def on_click_apply(self, event):
936        """
937        Changes are saved in data object imported to edit.
938
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
941        least a return statement.  If all passes writes file then tries to
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.
945
946        :todo this code still could do with a careful going over to clean
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.
950        """
951        #must post event here
952        event.Skip()
953        name = self.name_tcl.GetValue().lstrip().rstrip()
954        info = 'Info'
955        msg = ''
956        # Sort out the errors if occur
957        # First check for valid python name then if the name already exists
958        if not re.match('^[A-Za-z0-9_]*$', name):
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():
962            description = self.desc_tcl.GetValue()
963            param_str = self.param_tcl.GetText()
964            pd_param_str = self.pd_param_tcl.GetText()
965            func_str = self.function_tcl.GetText()
966            # No input for the model function
967            if func_str.lstrip().rstrip():
968                if func_str.count('return') > 0:
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
989                else:
990                    msg = "Error: The func(x) must 'return' a value at least.\n"
991                    msg += "For example: \n\nreturn 2*x"
992            else:
993                msg = 'Error: Function is not defined.'
994        else:
995            msg = "Name exists already."
996
997        # Prepare the messagebox
998        if self.base != None and not msg:
999            self.base.update_custom_combo()
1000            # Passed exception in import test as it will fail for sasmodels.sasview_model class
1001            # Should add similar test for new style?
1002            Model = None
1003            try:
1004                exec "from %s import Model" % name
1005            except:
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
1028        # Prepare the messagebox
1029        if msg:
1030            info = 'Error'
1031            color = 'red'
1032        else:
1033            msg = "Successful! "
1034            msg += "  " + self._notes
1035            msg += " Please look for it in the Customized Models."
1036            info = 'Info'
1037            color = 'blue'
1038        self._msg_box.SetLabel(msg)
1039        self._msg_box.SetForegroundColour(color)
1040        # Send msg to the top window
1041        if self.base != None:
1042            from sas.sasgui.guiframe.events import StatusEvent
1043            wx.PostEvent(self.base.parent, StatusEvent(status=msg, info=info))
1044        self.warning = msg
1045
1046    def write_file(self, fname, name, desc_str, param_str, pd_param_str, func_str):
1047        """
1048        Write content in file
1049
1050        :param fname: full file path
1051        :param desc_str: content of the description strings
1052        :param param_str: content of params; Strings
1053        :param pd_param_str: content of params requiring polydispersity; Strings
1054        :param func_str: content of func; Strings
1055        """
1056        try:
1057            out_f = open(fname, 'w')
1058        except:
1059            raise
1060        # Prepare the content of the function
1061        lines = CUSTOM_TEMPLATE.split('\n')
1062
1063        has_scipy = func_str.count("scipy.")
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
1085        # write function here
1086        for line in lines:
1087            # The location where to put the strings is
1088            # hard-coded in the template as shown below.
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')
1098                for param_line in param_str.split('\n'):
1099                    p_line = param_line.lstrip().rstrip()
1100                    if p_line:
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))
1138
1139        out_f.close()
1140
1141    def get_param_helper(self, line):
1142        """
1143        Get string in line to define the params dictionary
1144
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:
1151                value = item.split("=")[1].lstrip().rstrip()
1152                float(value)
1153            except:
1154                value = 1.0 # default
1155
1156        return name, value
1157
1158    def set_function_helper(self, line):
1159        """
1160        Get string in line to define the local params
1161
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()
1169            params_str += spaces + "%s = self.params['%s']\n" % (name, name)
1170        return params_str
1171
1172    def get_warning(self):
1173        """
1174        Get the warning msg
1175        """
1176        return self.warning
1177
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
1193        _TreeLocation = "user/sasgui/perspectives/fitting/fitting_help.html"
1194        _PageAnchor = "#custom-model-editor"
1195        _doc_viewer = DocumentationWindow(self, -1, _TreeLocation, _PageAnchor,
1196                                          "Custom Model Editor Help")
1197
1198    def on_close(self, event):
1199        """
1200        leave data as it is and close
1201        """
1202        self.parent.Show(False)#Close()
1203        event.Skip()
1204
1205class EditorWindow(wx.Frame):
1206    """
1207    Editor Window
1208    """
1209    def __init__(self, parent, base, path, title,
1210                 size=(EDITOR_WIDTH, EDITOR_HEIGTH), *args, **kwds):
1211        """
1212        Init
1213        """
1214        kwds["title"] = title
1215        kwds["size"] = size
1216        wx.Frame.__init__(self, parent=None, *args, **kwds)
1217        self.parent = parent
1218        self.panel = EditorPanel(parent=self, base=parent,
1219                                 path=path, title=title)
1220        self.Show(True)
1221        wx.EVT_CLOSE(self, self.on_close)
1222
1223    def on_close(self, event):
1224        """
1225        On close event
1226        """
1227        self.Show(False)
1228        #if self.parent != None:
1229        #    self.parent.new_model_frame = None
1230        #self.Destroy()
1231
1232## Templates for custom models
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
1254CUSTOM_TEMPLATE = """
1255from math import *
1256import os
1257import sys
1258import numpy
1259
1260#name
1261
1262#title
1263
1264#description
1265
1266#parameters
1267
1268"""
1269
1270CUSTOM_2D_TEMP = """
1271    def run(self, x=0.0, y=0.0):
1272        if x.__class__.__name__ == 'list':
1273            x_val = x[0]
1274            y_val = y[0]*0.0
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:
1280            return self.function(x, 0.0)
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"""
1306TEST_TEMPLATE = """
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
1312######################################################################
1313## THIS IS FOR TEST. DO NOT MODIFY THE FOLLOWING LINES!!!!!!!!!!!!!!!!
1314if __name__ == "__main__":
1315    m= Model()
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"""
1329SUM_TEMPLATE = """
1330# A sample of an experimental model function for Sum/Multiply(Pmodel1,Pmodel2)
1331import copy
1332from sas.sascalc.fit.pluginmodel import Model1DPlugin
1333from sasmodels.sasview_model import make_class
1334from sasmodels.core import load_model_info
1335# User can change the name of the model (only with single functional model)
1336#P1_model:
1337#from %s import Model as P1
1338
1339#P2_model:
1340#from %s import Model as P2
1341import os
1342import sys
1343
1344class Model(Model1DPlugin):
1345    name = ""
1346    def __init__(self):
1347        Model1DPlugin.__init__(self, name='')
1348        P1 = make_class('%s')
1349        P2 = make_class('%s')
1350        p_model1 = P1()
1351        p_model2 = P2()
1352        ## Setting  model name model description
1353        self.description = '%s'
1354        self.name = self.get_fname()
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)
1361
1362        ## Define parameters
1363        self.params = {}
1364
1365        ## Parameter details [units, min, max]
1366        self.details = {}
1367        ## Magnetic Panrameters
1368        self.magnetic_params = []
1369        # non-fittable parameters
1370        self.non_fittable = p_model1.non_fittable
1371        self.non_fittable += p_model2.non_fittable
1372
1373        ##models
1374        self.p_model1= p_model1
1375        self.p_model2= p_model2
1376
1377
1378        ## dispersion
1379        self._set_dispersion()
1380        ## Define parameters
1381        self._set_params()
1382        ## New parameter:scaling_factor
1383        self.params['scale_factor'] = %s
1384
1385        ## Parameter details [units, min, max]
1386        self._set_details()
1387        self.details['scale_factor'] = ['', None, None]
1388
1389
1390        #list of parameter that can be fitted
1391        self._set_fixed_params()
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)
1397
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)
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)
1407
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)
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
1423        self.multiplicity1 = multiplicity1
1424        self.multiplicity2 = multiplicity2
1425        self.multiplicity_info = []
1426
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
1436
1437    def _get_name(self, name1, name2):
1438        p1_name = self._get_upper_name(name1)
1439        if not p1_name:
1440            p1_name = name1
1441        name = p1_name
1442        name += "_and_"
1443        p2_name = self._get_upper_name(name2)
1444        if not p2_name:
1445            p2_name = name2
1446        name += p2_name
1447        return name
1448
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
1458
1459    def _set_dispersion(self):
1460        ##set dispersion only from p_model
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
1464            self.dispersion[new_name]= value
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
1468            self.dispersion[new_name]= value
1469
1470    def function(self, x=0.0):
1471        return 0
1472
1473    def getProfile(self):
1474        try:
1475            x,y = self.p_model1.getProfile()
1476        except:
1477            x = None
1478            y = None
1479
1480        return x, y
1481
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
1488
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
1494
1495        # Set "scale" as initializing
1496        self._set_scale_factor()
1497
1498
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
1504
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
1509
1510    def _set_scale_factor(self):
1511        pass
1512
1513
1514    def setParam(self, name, value):
1515        # set param to this (p1, p2) model
1516        self._setParamHelper(name, value)
1517
1518        ## setParam to p model
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]
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)
1531        elif name == 'scale_factor':
1532            self.params['scale_factor'] = value
1533        else:
1534            raise ValueError, "Model does not contain parameter %s" % name
1535
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]
1551        return
1552        #raise ValueError, "Model does not contain parameter %s" % name
1553
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
1570
1571        raise ValueError, "Model does not contain parameter %s" % name
1572
1573
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()
1583
1584
1585    def run(self, x = 0.0):
1586        self._set_scale_factor()
1587        return self.params['scale_factor'] %s \
1588(self.p_model1.run(x) %s self.p_model2.run(x))
1589
1590    def runXY(self, x = 0.0):
1591        self._set_scale_factor()
1592        return self.params['scale_factor'] %s \
1593(self.p_model1.runXY(x) %s self.p_model2.runXY(x))
1594
1595    ## Now (May27,10) directly uses the model eval function
1596    ## instead of the for-loop in Base Component.
1597    def evalDistribution(self, x = []):
1598        self._set_scale_factor()
1599        return self.params['scale_factor'] %s \
1600(self.p_model1.evalDistribution(x) %s \
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:
1617            raise
1618
1619    def fill_description(self, p_model1, p_model2):
1620        description = ""
1621        description += "This model gives the summation or multiplication of"
1622        description += "%s and %s. "% ( p_model1.name, p_model2.name )
1623        self.description += description
1624
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)
1629        return name
1630
1631if __name__ == "__main__":
1632    m1= Model()
1633    #m1.setParam("p1_scale", 25)
1634    #m1.setParam("p1_length", 1000)
1635    #m1.setParam("p2_scale", 100)
1636    #m1.setParam("p2_rg", 100)
1637    out1 = m1.runXY(0.01)
1638
1639    m2= Model()
1640    #m2.p_model1.setParam("scale", 25)
1641    #m2.p_model1.setParam("length", 1000)
1642    #m2.p_model2.setParam("scale", 100)
1643    #m2.p_model2.setParam("rg", 100)
1644    out2 = m2.p_model1.runXY(0.01) %s m2.p_model2.runXY(0.01)\n
1645    print "My name is %s."% m1.name
1646    print out1, " = ", out2
1647    if out1 == out2:
1648        print "===> Simple Test: Passed!"
1649    else:
1650        print "===> Simple Test: Failed!"
1651"""
1652
1653if __name__ == "__main__":
1654#    app = wx.PySimpleApp()
1655    main_app = wx.App()
1656    main_frame = TextDialog(id=1, model_list=["SphereModel", "CylinderModel"],
1657                       plugin_dir='../fitting/plugin_models')
1658    main_frame.ShowModal()
1659    main_app.MainLoop()
1660
1661#if __name__ == "__main__":
1662#    from sas.sasgui.perspectives.fitting import models
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.