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

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalcmagnetic_scattrelease-4.2.2ticket-1009ticket-1094-headlessticket-1242-2d-resolutionticket-1243ticket-1249ticket885unittest-saveload
Last change on this file since 277257f was 277257f, checked in by Paul Kienzle <pkienzle@…>, 7 years ago

clean up plugin-model handling code; preserve active parameter values when plugin is updated

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