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

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.2.2ticket-1009ticket-1094-headlessticket-1242-2d-resolutionticket-1243ticket-1249ticket885unittest-saveload
Last change on this file since 9d80623 was 9d80623, checked in by lewis, 7 years ago

Ensure sum/multiplication models behave correctly

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