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

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalccostrafo411magnetic_scattrelease-4.1.1release-4.1.2release-4.2.2ticket-1009ticket-1094-headlessticket-1242-2d-resolutionticket-1243ticket-1249ticket885unittest-saveload
Last change on this file since 26c8be3 was 26c8be3, checked in by smk78, 8 years ago

Changed default behaviour of New Plugin Model

modified: src/sas/sasgui/perspectives/calculator/model_editor.py
modified: src/sas/sasgui/perspectives/fitting/media/fitting_help.rst

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