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

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 bb841ef was bb841ef, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 8 years ago

Fixed a typo in import names

  • 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 (if any): ')
722
723        param_tip = "#Set the parameters and 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 polydispersity (if any): ')
739
740        pd_param_tip = "#Set the parameters and initial values.\n"
741        pd_param_tip += "#Example:\n"
742        pd_param_tip += "C = 2\nD = 2"
743        newid = wx.NewId()
744        self.pd_param_tcl = EditWindow(self, newid, wx.DefaultPosition,
745                                    wx.DefaultSize,
746                                    wx.CLIP_CHILDREN | wx.SUNKEN_BORDER)
747        self.pd_param_tcl.setDisplayLineNumbers(True)
748        self.pd_param_tcl.SetToolTipString(pd_param_tip)
749       
750        self.param_sizer.AddMany([(pd_param_txt, 0, wx.LEFT, 10),
751                                  (self.pd_param_tcl, 1, wx.EXPAND | wx.ALL, 10)])
752
753    def _layout_function(self):
754        """
755        Do the layout for function related widgets
756        """
757        function_txt = wx.StaticText(self, -1, 'Function(x) : ')
758        hint_function = "#Example:\n"
759        hint_function += "if x <= 0:\n"
760        hint_function += "    y = A + B\n"
761        hint_function += "else:\n"
762        hint_function += "    y = A + B * cos(2 * pi * x)\n"
763        hint_function += "return y\n"
764        math_txt = wx.StaticText(self, -1, '*Useful math functions: ')
765        math_combo = self._fill_math_combo()
766
767        newid = wx.NewId()
768        self.function_tcl = EditWindow(self, newid, wx.DefaultPosition,
769                                       wx.DefaultSize,
770                                       wx.CLIP_CHILDREN | wx.SUNKEN_BORDER)
771        self.function_tcl.setDisplayLineNumbers(True)
772        self.function_tcl.SetToolTipString(hint_function)
773
774        self.func_horizon_sizer.Add(function_txt)
775        self.func_horizon_sizer.Add(math_txt, 0, wx.LEFT, 250)
776        self.func_horizon_sizer.Add(math_combo, 0, wx.LEFT, 10)
777
778        self.function_sizer.Add(self.func_horizon_sizer, 0, wx.LEFT, 10)
779        self.function_sizer.Add(self.function_tcl, 1, wx.EXPAND | wx.ALL, 10)
780
781    def _layout_msg(self):
782        """
783        Layout msg
784        """
785        self._msg_box = wx.StaticText(self, -1, self._notes,
786                                      size=(PANEL_WIDTH, -1))
787        self.msg_sizer.Add(self._msg_box, 0, wx.LEFT, 10)
788
789    def _layout_button(self):
790        """
791        Do the layout for the button widgets
792        """
793        self.bt_apply = wx.Button(self, -1, "Apply", size=(_BOX_WIDTH, -1))
794        self.bt_apply.SetToolTipString("Save changes into the imported data.")
795        self.bt_apply.Bind(wx.EVT_BUTTON, self.on_click_apply)
796
797        self.bt_help = wx.Button(self, -1, "HELP", size=(_BOX_WIDTH, -1))
798        self.bt_help.SetToolTipString("Get Help For Model Editor")
799        self.bt_help.Bind(wx.EVT_BUTTON, self.on_help)
800
801        self.bt_close = wx.Button(self, -1, 'Close', size=(_BOX_WIDTH, -1))
802        self.bt_close.Bind(wx.EVT_BUTTON, self.on_close)
803        self.bt_close.SetToolTipString("Close this panel.")
804
805        self.button_sizer.AddMany([(self.bt_apply, 0,
806                                    wx.LEFT, EDITOR_WIDTH * 0.8),
807                                   (self.bt_help, 0,
808                                    wx.LEFT,15),
809                                   (self.bt_close, 0,
810                                    wx.LEFT | wx.BOTTOM, 15)])
811
812    def _do_layout(self):
813        """
814        Draw the current panel
815        """
816        self._define_structure()
817        self._layout_name()
818        self._layout_description()
819        self._layout_param()
820        self._layout_function()
821        self._layout_msg()
822        self._layout_button()
823        self.main_sizer.AddMany([(self.name_sizer, 0, wx.EXPAND | wx.ALL, 5),
824                                 (wx.StaticLine(self), 0,
825                                  wx.ALL | wx.EXPAND, 5),
826                                 (self.desc_sizer, 0, wx.EXPAND | wx.ALL, 5),
827                                 (wx.StaticLine(self), 0,
828                                  wx.ALL | wx.EXPAND, 5),
829                                 (self.param_sizer, 1, wx.EXPAND | wx.ALL, 5),
830                                 (wx.StaticLine(self), 0,
831                                  wx.ALL | wx.EXPAND, 5),
832                                 (self.function_sizer, 2,
833                                  wx.EXPAND | wx.ALL, 5),
834                                 (wx.StaticLine(self), 0,
835                                  wx.ALL | wx.EXPAND, 5),
836                                 (self.msg_sizer, 0, wx.EXPAND | wx.ALL, 5),
837                                 (self.button_sizer, 0, wx.EXPAND | wx.ALL, 5)])
838        self.SetSizer(self.main_sizer)
839        self.SetAutoLayout(True)
840
841    def _fill_math_combo(self):
842        """
843        Fill up the math combo box
844        """
845        self.math_combo = wx.ComboBox(self, -1, size=(100, -1),
846                                      style=wx.CB_READONLY)
847        for item in dir(math):
848            if item.count("_") < 1:
849                try:
850                    exec "float(math.%s)" % item
851                    self.math_combo.Append(str(item))
852                except:
853                    self.math_combo.Append(str(item) + "()")
854        self.math_combo.Bind(wx.EVT_COMBOBOX, self._on_math_select)
855        self.math_combo.SetSelection(0)
856        return self.math_combo
857
858    def _on_math_select(self, event):
859        """
860        On math selection on ComboBox
861        """
862        event.Skip()
863        label = self.math_combo.GetValue()
864        self.function_tcl.SetFocus()
865        # Put the text at the cursor position
866        pos = self.function_tcl.GetCurrentPos()
867        self.function_tcl.InsertText(pos, label)
868        # Put the cursor at appropriate position
869        length = len(label)
870        print length
871        if label[length-1] == ')':
872            length -= 1
873        f_pos = pos + length
874        self.function_tcl.GotoPos(f_pos)
875
876    def get_notes(self):
877        """
878        return notes
879        """
880        return self._notes
881
882    def on_change_name(self, event=None):
883        """
884        Change name
885        """
886        if event is not None:
887            event.Skip()
888        self.name_tcl.SetBackgroundColour('white')
889        self.Refresh()
890
891    def check_name(self):
892        """
893        Check name if exist already
894        """
895        self._notes = ''
896        self.on_change_name(None)
897        plugin_dir = self.path
898        list_fnames = os.listdir(plugin_dir)
899        # function/file name
900        title = self.name_tcl.GetValue().lstrip().rstrip()
901        self.name = title
902        t_fname = title + '.py'
903        if not self.overwrite_name:
904            if t_fname in list_fnames:
905                self.name_tcl.SetBackgroundColour('pink')
906                return False
907        self.fname = os.path.join(plugin_dir, t_fname)
908        s_title = title
909        if len(title) > 20:
910            s_title = title[0:19] + '...'
911        self._notes += "Model function name is set "
912        self._notes += "to %s. \n" % str(s_title)
913        return True
914
915    def on_over_cb(self, event):
916        """
917        Set overwrite name flag on cb event
918        """
919        if event is not None:
920            event.Skip()
921        cb_value = event.GetEventObject()
922        self.overwrite_name = cb_value.GetValue()
923
924    def on_click_apply(self, event):
925        """
926        Changes are saved in data object imported to edit.
927
928        checks firs for valid name, then if it already exists then checks
929        that a function was entered and finally that if entered it contains at
930        least a return statement.  If all passes writes file then tries to
931        compile.  If compile fails or import module fails or run method fails
932        tries to remove any .py and pyc files that may have been created and
933        sets error message.
934
935        :todo this code still could do with a careful going over to clean
936        up and simplify. the non GUI methods such as this one should be removed
937        to computational code of SasView. Most of those computational methods
938        would be the same for both the simple editors.
939        """
940        #must post event here
941        event.Skip()
942        name = self.name_tcl.GetValue().lstrip().rstrip()
943        info = 'Info'
944        msg = ''
945        result, check_err = '', ''
946        # Sort out the errors if occur
947        # First check for valid python name then if the name already exists
948        if not re.match('^[A-Za-z0-9_]*$', name):
949            msg = "not a valid python name. Name must include only alpha \n"
950            msg += "numeric or underline characters and no spaces"
951        elif self.check_name():
952            description = self.desc_tcl.GetValue()
953            param_str = self.param_tcl.GetText()
954            pd_param_str = self.pd_param_tcl.GetText()
955            func_str = self.function_tcl.GetText()
956            # No input for the model function
957            if func_str.lstrip().rstrip():
958                if func_str.count('return') > 0:
959                    self.write_file(self.fname, name, description, param_str,
960                                    pd_param_str, func_str)
961                    try:
962                        result, msg = check_model(self.fname), None
963                    except Exception:
964                        import traceback
965                        result, msg = None, "error building model"
966                        check_err = "\n"+traceback.format_exc(limit=2)
967
968                    # Modified compiling test, as it will fail for sasmodels.sasview_model class
969                    # Should add a test to check that the class is correctly built
970                    # by sasview_model.make_class_from_file?
971#                    try:
972#                        tr_msg = _compile_file(self.fname)
973#                        msg = str(tr_msg.__str__())
974#                        # Compile error
975#                        if msg:
976#                            msg.replace("  ", "\n")
977#                            msg += "\nCompiling Failed"
978#                            try:
979#                                # try to remove pyc file if exists
980#                                _delete_file(self.fname)
981#                                _delete_file(self.fname + "c")
982#                            except:
983#                                pass
984#                    except:
985#                        pass
986                else:
987                    msg = "Error: The func(x) must 'return' a value at least.\n"
988                    msg += "For example: \n\nreturn 2*x"
989            else:
990                msg = 'Error: Function is not defined.'
991        else:
992            msg = "Name exists already."
993
994        # Prepare the messagebox
995        if self.base != None and not msg:
996            self.base.update_custom_combo()
997            # Passed exception in import test as it will fail for sasmodels.sasview_model class
998            # Should add similar test for new style?
999            Model = None
1000            try:
1001                exec "from %s import Model" % name
1002            except:
1003                pass
1004#            except:
1005#                msg = 'new model fails to import in python'
1006#                try:
1007#                    # try to remove pyc file if exists
1008#                    _delete_file(self.fname + "c")
1009#                except:
1010#                    pass
1011#
1012# And also need to test if model runs           
1013#        if self.base != None and not msg:
1014#            try:
1015#                Model().run(0.01)
1016#            except:
1017#                msg = "new model fails on run method:"
1018#                _, value, _ = sys.exc_info()
1019#                msg += "in %s:\n%s\n" % (name, value)
1020#                try:
1021#                    # try to remove pyc file if exists
1022#                    _delete_file(self.fname + "c")
1023#                except:
1024#                    pass
1025        # Prepare the messagebox
1026        if msg:
1027            info = 'Error'
1028            color = 'red'
1029        else:
1030            self._notes = result
1031            msg = "Successful! Please look for %s in Customized Models."%name
1032            msg += "  " + self._notes
1033            info = 'Info'
1034            color = 'blue'
1035        self._msg_box.SetLabel(msg)
1036        self._msg_box.SetForegroundColour(color)
1037        # Send msg to the top window
1038        if self.base != None:
1039            from sas.sasgui.guiframe.events import StatusEvent
1040            wx.PostEvent(self.base.parent,
1041                         StatusEvent(status=msg+check_err, info=info))
1042        self.warning = msg
1043
1044    def write_file(self, fname, name, desc_str, param_str, pd_param_str, func_str):
1045        """
1046        Write content in file
1047
1048        :param fname: full file path
1049        :param desc_str: content of the description strings
1050        :param param_str: content of params; Strings
1051        :param pd_param_str: content of params requiring polydispersity; Strings
1052        :param func_str: content of func; Strings
1053        """
1054        try:
1055            out_f = open(fname, 'w')
1056        except:
1057            raise
1058        # Prepare the content of the function
1059        lines = CUSTOM_TEMPLATE.split('\n')
1060
1061        has_scipy = func_str.count("scipy.")
1062        if has_scipy:
1063            lines.insert(0, 'import scipy')
1064       
1065        # Think about 2D later       
1066        #self.is_2d = func_str.count("#self.ndim = 2")
1067        #line_2d = ''
1068        #if self.is_2d:
1069        #    line_2d = CUSTOM_2D_TEMP.split('\n')
1070       
1071        # Also think about test later       
1072        #line_test = TEST_TEMPLATE.split('\n')
1073        #local_params = ''
1074        #spaces = '        '#8spaces
1075        spaces4  = ' '*4
1076        spaces13 = ' '*13
1077        spaces16 = ' '*16     
1078        param_names = []    # to store parameter names
1079        has_scipy = func_str.count("scipy.")
1080        if has_scipy:
1081            lines.insert(0, 'import scipy')
1082
1083        # write function here
1084        for line in lines:
1085            # The location where to put the strings is
1086            # hard-coded in the template as shown below.
1087            out_f.write(line + '\n')
1088            if line.count('#name'):
1089                out_f.write('name = "%s" \n' % name)               
1090            elif line.count('#title'):
1091                out_f.write('title = "User model for %s"\n' % name)               
1092            elif line.count('#description'):
1093                out_f.write('description = "%s"\n' % desc_str)               
1094            elif line.count('#parameters'):
1095                out_f.write('parameters = [ \n')
1096                for param_line in param_str.split('\n'):
1097                    p_line = param_line.lstrip().rstrip()
1098                    if p_line:
1099                        pname, pvalue = self.get_param_helper(p_line)
1100                        param_names.append(pname)
1101                        out_f.write("%s['%s', '', %s, [-numpy.inf, numpy.inf], '', ''],\n" % (spaces16, pname, pvalue))
1102                for param_line in pd_param_str.split('\n'):
1103                    p_line = param_line.lstrip().rstrip()
1104                    if p_line:
1105                        pname, pvalue = self.get_param_helper(p_line)
1106                        param_names.append(pname)
1107                        out_f.write("%s['%s', '', %s, [-numpy.inf, numpy.inf], 'volume', ''],\n" % (spaces16, pname, pvalue))
1108                out_f.write('%s]\n' % spaces13)
1109           
1110        # No form_volume or ER available in simple model editor
1111        out_f.write('def form_volume(*arg): \n')
1112        out_f.write('    return 1.0 \n')
1113        out_f.write('\n')
1114        out_f.write('def ER(*arg): \n')
1115        out_f.write('    return 1.0 \n')
1116       
1117        # function to compute
1118        out_f.write('\n')
1119        out_f.write('def Iq(x ')
1120        for name in param_names:
1121            out_f.write(', %s' % name)
1122        out_f.write('):\n')
1123        for func_line in func_str.split('\n'):
1124            out_f.write('%s%s\n' % (spaces4, func_line))
1125       
1126        Iqxy_string = 'return Iq(numpy.sqrt(x**2+y**2) '
1127           
1128        out_f.write('\n')
1129        out_f.write('def Iqxy(x, y ')
1130        for name in param_names:
1131            out_f.write(', %s' % name)
1132            Iqxy_string += ', ' + name
1133        out_f.write('):\n')
1134        Iqxy_string += ')'
1135        out_f.write('%s%s\n' % (spaces4, Iqxy_string))
1136
1137        out_f.close()
1138
1139    def get_param_helper(self, line):
1140        """
1141        Get string in line to define the params dictionary
1142
1143        :param line: one line of string got from the param_str
1144        """
1145        items = line.split(";")
1146        for item in items:
1147            name = item.split("=")[0].lstrip().rstrip()
1148            try:
1149                value = item.split("=")[1].lstrip().rstrip()
1150                float(value)
1151            except:
1152                value = 1.0 # default
1153
1154        return name, value
1155
1156    def set_function_helper(self, line):
1157        """
1158        Get string in line to define the local params
1159
1160        :param line: one line of string got from the param_str
1161        """
1162        params_str = ''
1163        spaces = '        '#8spaces
1164        items = line.split(";")
1165        for item in items:
1166            name = item.split("=")[0].lstrip().rstrip()
1167            params_str += spaces + "%s = self.params['%s']\n" % (name, name)
1168        return params_str
1169
1170    def get_warning(self):
1171        """
1172        Get the warning msg
1173        """
1174        return self.warning
1175
1176    def on_help(self, event):
1177        """
1178        Bring up the Custom Model Editor Documentation whenever
1179        the HELP button is clicked.
1180
1181        Calls DocumentationWindow with the path of the location within the
1182        documentation tree (after /doc/ ....".  Note that when using old
1183        versions of Wx (before 2.9) and thus not the release version of
1184        installers, the help comes up at the top level of the file as
1185        webbrowser does not pass anything past the # to the browser when it is
1186        running "file:///...."
1187
1188    :param evt: Triggers on clicking the help button
1189    """
1190
1191        _TreeLocation = "user/sasgui/perspectives/fitting/fitting_help.html"
1192        _PageAnchor = "#custom-model-editor"
1193        _doc_viewer = DocumentationWindow(self, -1, _TreeLocation, _PageAnchor,
1194                                          "Custom Model Editor Help")
1195
1196    def on_close(self, event):
1197        """
1198        leave data as it is and close
1199        """
1200        self.parent.Show(False)#Close()
1201        event.Skip()
1202
1203class EditorWindow(wx.Frame):
1204    """
1205    Editor Window
1206    """
1207    def __init__(self, parent, base, path, title,
1208                 size=(EDITOR_WIDTH, EDITOR_HEIGTH), *args, **kwds):
1209        """
1210        Init
1211        """
1212        kwds["title"] = title
1213        kwds["size"] = size
1214        wx.Frame.__init__(self, parent=None, *args, **kwds)
1215        self.parent = parent
1216        self.panel = EditorPanel(parent=self, base=parent,
1217                                 path=path, title=title)
1218        self.Show(True)
1219        wx.EVT_CLOSE(self, self.on_close)
1220
1221    def on_close(self, event):
1222        """
1223        On close event
1224        """
1225        self.Show(False)
1226        #if self.parent != None:
1227        #    self.parent.new_model_frame = None
1228        #self.Destroy()
1229
1230## Templates for custom models
1231#CUSTOM_TEMPLATE = """
1232#from sas.models.pluginmodel import Model1DPlugin
1233#from math import *
1234#import os
1235#import sys
1236#import numpy
1237##import scipy?
1238#class Model(Model1DPlugin):
1239#    name = ""
1240#    def __init__(self):
1241#        Model1DPlugin.__init__(self, name=self.name)
1242#        #set name same as file name
1243#        self.name = self.get_fname()
1244#        #self.params here
1245#        self.description = "%s"
1246#        self.set_details()
1247#    def function(self, x=0.0%s):
1248#        #local params here
1249#        #function here
1250#"""
1251
1252CUSTOM_TEMPLATE = """
1253from math import *
1254import os
1255import sys
1256import numpy
1257
1258#name
1259
1260#title
1261
1262#description
1263
1264#parameters
1265
1266"""
1267
1268CUSTOM_2D_TEMP = """
1269    def run(self, x=0.0, y=0.0):
1270        if x.__class__.__name__ == 'list':
1271            x_val = x[0]
1272            y_val = y[0]*0.0
1273            return self.function(x_val, y_val)
1274        elif x.__class__.__name__ == 'tuple':
1275            msg = "Tuples are not allowed as input to BaseComponent models"
1276            raise ValueError, msg
1277        else:
1278            return self.function(x, 0.0)
1279    def runXY(self, x=0.0, y=0.0):
1280        if x.__class__.__name__ == 'list':
1281            return self.function(x, y)
1282        elif x.__class__.__name__ == 'tuple':
1283            msg = "Tuples are not allowed as input to BaseComponent models"
1284            raise ValueError, msg
1285        else:
1286            return self.function(x, y)
1287    def evalDistribution(self, qdist):
1288        if qdist.__class__.__name__ == 'list':
1289            msg = "evalDistribution expects a list of 2 ndarrays"
1290            if len(qdist)!=2:
1291                raise RuntimeError, msg
1292            if qdist[0].__class__.__name__ != 'ndarray':
1293                raise RuntimeError, msg
1294            if qdist[1].__class__.__name__ != 'ndarray':
1295                raise RuntimeError, msg
1296            v_model = numpy.vectorize(self.runXY, otypes=[float])
1297            iq_array = v_model(qdist[0], qdist[1])
1298            return iq_array
1299        elif qdist.__class__.__name__ == 'ndarray':
1300            v_model = numpy.vectorize(self.runXY, otypes=[float])
1301            iq_array = v_model(qdist)
1302            return iq_array
1303"""
1304TEST_TEMPLATE = """
1305    def get_fname(self):
1306        path = sys._getframe().f_code.co_filename
1307        basename  = os.path.basename(path)
1308        name, _ = os.path.splitext(basename)
1309        return name
1310######################################################################
1311## THIS IS FOR TEST. DO NOT MODIFY THE FOLLOWING LINES!!!!!!!!!!!!!!!!
1312if __name__ == "__main__":
1313    m= Model()
1314    out1 = m.runXY(0.0)
1315    out2 = m.runXY(0.01)
1316    isfine1 = numpy.isfinite(out1)
1317    isfine2 = numpy.isfinite(out2)
1318    print "Testing the value at Q = 0.0:"
1319    print out1, " : finite? ", isfine1
1320    print "Testing the value at Q = 0.01:"
1321    print out2, " : finite? ", isfine2
1322    if isfine1 and isfine2:
1323        print "===> Simple Test: Passed!"
1324    else:
1325        print "===> Simple Test: Failed!"
1326"""
1327SUM_TEMPLATE = """
1328# A sample of an experimental model function for Sum/Multiply(Pmodel1,Pmodel2)
1329import os
1330import sys
1331import copy
1332
1333import numpy
1334
1335from sas.sascalc.fit.pluginmodel import Model1DPlugin
1336from sasmodels.sasview_model import make_class
1337from sasmodels.core import load_model_info
1338# User can change the name of the model (only with single functional model)
1339#P1_model:
1340#from %s import Model as P1
1341
1342#P2_model:
1343#from %s import Model as P2
1344
1345class Model(Model1DPlugin):
1346    name = ""
1347    def __init__(self):
1348        Model1DPlugin.__init__(self, name='')
1349        P1 = make_class('%s')
1350        P2 = make_class('%s')
1351        p_model1 = P1()
1352        p_model2 = P2()
1353        ## Setting  model name model description
1354        self.description = '%s'
1355        self.name = self.get_fname()
1356        if self.name.rstrip().lstrip() == '':
1357            self.name = self._get_name(p_model1.name, p_model2.name)
1358        if self.description.rstrip().lstrip() == '':
1359            self.description = p_model1.name
1360            self.description += p_model2.name
1361            self.fill_description(p_model1, p_model2)
1362
1363        ## Define parameters
1364        self.params = {}
1365
1366        ## Parameter details [units, min, max]
1367        self.details = {}
1368        ## Magnetic Panrameters
1369        self.magnetic_params = []
1370        # non-fittable parameters
1371        self.non_fittable = p_model1.non_fittable
1372        self.non_fittable += p_model2.non_fittable
1373
1374        ##models
1375        self.p_model1= p_model1
1376        self.p_model2= p_model2
1377
1378
1379        ## dispersion
1380        self._set_dispersion()
1381        ## Define parameters
1382        self._set_params()
1383        ## New parameter:scaling_factor
1384        self.params['scale_factor'] = %s
1385
1386        ## Parameter details [units, min, max]
1387        self._set_details()
1388        self.details['scale_factor'] = ['', 0.0, numpy.inf]
1389
1390
1391        #list of parameter that can be fitted
1392        self._set_fixed_params()
1393        ## parameters with orientation
1394        for item in self.p_model1.orientation_params:
1395            new_item = "p1_" + item
1396            if not new_item in self.orientation_params:
1397                self.orientation_params.append(new_item)
1398
1399        for item in self.p_model2.orientation_params:
1400            new_item = "p2_" + item
1401            if not new_item in self.orientation_params:
1402                self.orientation_params.append(new_item)
1403        ## magnetic params
1404        for item in self.p_model1.magnetic_params:
1405            new_item = "p1_" + item
1406            if not new_item in self.magnetic_params:
1407                self.magnetic_params.append(new_item)
1408
1409        for item in self.p_model2.magnetic_params:
1410            new_item = "p2_" + item
1411            if not new_item in self.magnetic_params:
1412                self.magnetic_params.append(new_item)
1413        # get multiplicity if model provide it, else 1.
1414        try:
1415            multiplicity1 = p_model1.multiplicity
1416            try:
1417                multiplicity2 = p_model2.multiplicity
1418            except:
1419                multiplicity2 = 1
1420        except:
1421            multiplicity1 = 1
1422            multiplicity2 = 1
1423        ## functional multiplicity of the model
1424        self.multiplicity1 = multiplicity1
1425        self.multiplicity2 = multiplicity2
1426        self.multiplicity_info = []
1427
1428    def _clone(self, obj):
1429        obj.params     = copy.deepcopy(self.params)
1430        obj.description     = copy.deepcopy(self.description)
1431        obj.details    = copy.deepcopy(self.details)
1432        obj.dispersion = copy.deepcopy(self.dispersion)
1433        obj.p_model1  = self.p_model1.clone()
1434        obj.p_model2  = self.p_model2.clone()
1435        #obj = copy.deepcopy(self)
1436        return obj
1437
1438    def _get_name(self, name1, name2):
1439        p1_name = self._get_upper_name(name1)
1440        if not p1_name:
1441            p1_name = name1
1442        name = p1_name
1443        name += "_and_"
1444        p2_name = self._get_upper_name(name2)
1445        if not p2_name:
1446            p2_name = name2
1447        name += p2_name
1448        return name
1449
1450    def _get_upper_name(self, name=None):
1451        if name == None:
1452            return ""
1453        upper_name = ""
1454        str_name = str(name)
1455        for index in range(len(str_name)):
1456            if str_name[index].isupper():
1457                upper_name += str_name[index]
1458        return upper_name
1459
1460    def _set_dispersion(self):
1461        ##set dispersion only from p_model
1462        for name , value in self.p_model1.dispersion.iteritems():
1463            #if name.lower() not in self.p_model1.orientation_params:
1464            new_name = "p1_" + name
1465            self.dispersion[new_name]= value
1466        for name , value in self.p_model2.dispersion.iteritems():
1467            #if name.lower() not in self.p_model2.orientation_params:
1468            new_name = "p2_" + name
1469            self.dispersion[new_name]= value
1470
1471    def function(self, x=0.0):
1472        return 0
1473
1474    def getProfile(self):
1475        try:
1476            x,y = self.p_model1.getProfile()
1477        except:
1478            x = None
1479            y = None
1480
1481        return x, y
1482
1483    def _set_params(self):
1484        for name , value in self.p_model1.params.iteritems():
1485            # No 2D-supported
1486            #if name not in self.p_model1.orientation_params:
1487            new_name = "p1_" + name
1488            self.params[new_name]= value
1489
1490        for name , value in self.p_model2.params.iteritems():
1491            # No 2D-supported
1492            #if name not in self.p_model2.orientation_params:
1493            new_name = "p2_" + name
1494            self.params[new_name]= value
1495
1496        # Set "scale" as initializing
1497        self._set_scale_factor()
1498
1499
1500    def _set_details(self):
1501        for name ,detail in self.p_model1.details.iteritems():
1502            new_name = "p1_" + name
1503            #if new_name not in self.orientation_params:
1504            self.details[new_name]= detail
1505
1506        for name ,detail in self.p_model2.details.iteritems():
1507            new_name = "p2_" + name
1508            #if new_name not in self.orientation_params:
1509            self.details[new_name]= detail
1510
1511    def _set_scale_factor(self):
1512        pass
1513
1514
1515    def setParam(self, name, value):
1516        # set param to this (p1, p2) model
1517        self._setParamHelper(name, value)
1518
1519        ## setParam to p model
1520        model_pre = ''
1521        new_name = ''
1522        name_split = name.split('_', 1)
1523        if len(name_split) == 2:
1524            model_pre = name.split('_', 1)[0]
1525            new_name = name.split('_', 1)[1]
1526        if model_pre == "p1":
1527            if new_name in self.p_model1.getParamList():
1528                self.p_model1.setParam(new_name, value)
1529        elif model_pre == "p2":
1530             if new_name in self.p_model2.getParamList():
1531                self.p_model2.setParam(new_name, value)
1532        elif name == 'scale_factor':
1533            self.params['scale_factor'] = value
1534        else:
1535            raise ValueError, "Model does not contain parameter %s" % name
1536
1537    def getParam(self, name):
1538        # Look for dispersion parameters
1539        toks = name.split('.')
1540        if len(toks)==2:
1541            for item in self.dispersion.keys():
1542                # 2D not supported
1543                if item.lower()==toks[0].lower():
1544                    for par in self.dispersion[item]:
1545                        if par.lower() == toks[1].lower():
1546                            return self.dispersion[item][par]
1547        else:
1548            # Look for standard parameter
1549            for item in self.params.keys():
1550                if item.lower()==name.lower():
1551                    return self.params[item]
1552        return
1553        #raise ValueError, "Model does not contain parameter %s" % name
1554
1555    def _setParamHelper(self, name, value):
1556        # Look for dispersion parameters
1557        toks = name.split('.')
1558        if len(toks)== 2:
1559            for item in self.dispersion.keys():
1560                if item.lower()== toks[0].lower():
1561                    for par in self.dispersion[item]:
1562                        if par.lower() == toks[1].lower():
1563                            self.dispersion[item][par] = value
1564                            return
1565        else:
1566            # Look for standard parameter
1567            for item in self.params.keys():
1568                if item.lower()== name.lower():
1569                    self.params[item] = value
1570                    return
1571
1572        raise ValueError, "Model does not contain parameter %s" % name
1573
1574
1575    def _set_fixed_params(self):
1576        for item in self.p_model1.fixed:
1577            new_item = "p1" + item
1578            self.fixed.append(new_item)
1579        for item in self.p_model2.fixed:
1580            new_item = "p2" + item
1581            self.fixed.append(new_item)
1582
1583        self.fixed.sort()
1584
1585
1586    def run(self, x = 0.0):
1587        self._set_scale_factor()
1588        return self.params['scale_factor'] %s \
1589(self.p_model1.run(x) %s self.p_model2.run(x))
1590
1591    def runXY(self, x = 0.0):
1592        self._set_scale_factor()
1593        return self.params['scale_factor'] %s \
1594(self.p_model1.runXY(x) %s self.p_model2.runXY(x))
1595
1596    ## Now (May27,10) directly uses the model eval function
1597    ## instead of the for-loop in Base Component.
1598    def evalDistribution(self, x = []):
1599        self._set_scale_factor()
1600        return self.params['scale_factor'] %s \
1601(self.p_model1.evalDistribution(x) %s \
1602self.p_model2.evalDistribution(x))
1603
1604    def set_dispersion(self, parameter, dispersion):
1605        value= None
1606        new_pre = parameter.split("_", 1)[0]
1607        new_parameter = parameter.split("_", 1)[1]
1608        try:
1609            if new_pre == 'p1' and \
1610new_parameter in self.p_model1.dispersion.keys():
1611                value= self.p_model1.set_dispersion(new_parameter, dispersion)
1612            if new_pre == 'p2' and \
1613new_parameter in self.p_model2.dispersion.keys():
1614                value= self.p_model2.set_dispersion(new_parameter, dispersion)
1615            self._set_dispersion()
1616            return value
1617        except:
1618            raise
1619
1620    def fill_description(self, p_model1, p_model2):
1621        description = ""
1622        description += "This model gives the summation or multiplication of"
1623        description += "%s and %s. "% ( p_model1.name, p_model2.name )
1624        self.description += description
1625
1626    def get_fname(self):
1627        path = sys._getframe().f_code.co_filename
1628        basename  = os.path.basename(path)
1629        name, _ = os.path.splitext(basename)
1630        return name
1631
1632if __name__ == "__main__":
1633    m1= Model()
1634    #m1.setParam("p1_scale", 25)
1635    #m1.setParam("p1_length", 1000)
1636    #m1.setParam("p2_scale", 100)
1637    #m1.setParam("p2_rg", 100)
1638    out1 = m1.runXY(0.01)
1639
1640    m2= Model()
1641    #m2.p_model1.setParam("scale", 25)
1642    #m2.p_model1.setParam("length", 1000)
1643    #m2.p_model2.setParam("scale", 100)
1644    #m2.p_model2.setParam("rg", 100)
1645    out2 = m2.p_model1.runXY(0.01) %s m2.p_model2.runXY(0.01)\n
1646    print "My name is %s."% m1.name
1647    print out1, " = ", out2
1648    if out1 == out2:
1649        print "===> Simple Test: Passed!"
1650    else:
1651        print "===> Simple Test: Failed!"
1652"""
1653
1654if __name__ == "__main__":
1655#    app = wx.PySimpleApp()
1656    main_app = wx.App()
1657    main_frame = TextDialog(id=1, model_list=["SphereModel", "CylinderModel"],
1658                       plugin_dir='../fitting/plugin_models')
1659    main_frame.ShowModal()
1660    main_app.MainLoop()
1661
1662#if __name__ == "__main__":
1663#    from sas.sasgui.perspectives.fitting import models
1664#    dir_path = models.find_plugins_dir()
1665#    app = wx.App()
1666#    window = EditorWindow(parent=None, base=None, path=dir_path, title="Editor")
1667#    app.MainLoop()
Note: See TracBrowser for help on using the repository browser.