source: sasview/src/sas/sasgui/perspectives/calculator/model_editor.py @ 7df0ccd

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

MAINT: from future import print_function

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