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

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 345e7e4 was a08b89b, checked in by wojciech, 8 years ago

Fixing new plugin editor link anchor. Related to #777

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