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

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalccostrafo411magnetic_scattrelease-4.2.2ticket-1009ticket-1094-headlessticket-1242-2d-resolutionticket-1243ticket-1249ticket885unittest-saveload
Last change on this file since 235f514 was 235f514, checked in by andyfaff, 7 years ago

MAINT: replace '== None' by 'is None'

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