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

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 490c617 was 490c617, checked in by lewis, 7 years ago

Parse parameter values correctly if line ends in comment

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