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

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalccostrafo411magnetic_scattrelease-4.1.1release-4.1.2release-4.2.2ticket-1009ticket-1094-headlessticket-1242-2d-resolutionticket-1243ticket-1249ticket885unittest-saveload
Last change on this file since e64a03e was ddbac66, checked in by butler, 7 years ago

Fix remaining custom/customized to plugin conversion

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