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

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.2release_4.0.1ticket-1009ticket-1094-headlessticket-1242-2d-resolutionticket-1243ticket-1249ticket885unittest-saveload
Last change on this file since 70c5d490 was 70c5d490, checked in by butler, 8 years ago

Fix wx button positioning and edit tooltips and title pars of parameter
input boxes to make more clear to user what to do.

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