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

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 b64b87c was 9501661, checked in by Paul Kienzle <pkienzle@…>, 8 years ago

… and fix the order of the dispersion parameters

  • Property mode set to 100644
File size: 61.6 KB
Line 
1'''
2This module provides three model editor classes: the composite model editor,
3the easy editor which provides a simple interface with tooltip help to enter
4the parameters of the model and their default value and a panel to input a
5function of y (usually the intensity).  It also provides a drop down of
6standard available math functions.  Finally a full python editor panel for
7complete 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 = find_model"):
581                    out_f.write(line % (name1) + "\n")
582                elif line.count("P2 = find_model"):
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 = basename without extension of __file__
1241#    def __init__(self):
1242#        Model1DPlugin.__init__(self, name=self.name)
1243#        #set name same as file name
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######################################################################
1306## THIS IS FOR TEST. DO NOT MODIFY THE FOLLOWING LINES!!!!!!!!!!!!!!!!
1307if __name__ == "__main__":
1308    m= Model()
1309    out1 = m.runXY(0.0)
1310    out2 = m.runXY(0.01)
1311    isfine1 = numpy.isfinite(out1)
1312    isfine2 = numpy.isfinite(out2)
1313    print "Testing the value at Q = 0.0:"
1314    print out1, " : finite? ", isfine1
1315    print "Testing the value at Q = 0.01:"
1316    print out2, " : finite? ", isfine2
1317    if isfine1 and isfine2:
1318        print "===> Simple Test: Passed!"
1319    else:
1320        print "===> Simple Test: Failed!"
1321"""
1322SUM_TEMPLATE = """
1323# A sample of an experimental model function for Sum/Multiply(Pmodel1,Pmodel2)
1324import os
1325import sys
1326import copy
1327import collections
1328
1329import numpy
1330
1331from sas.sascalc.fit.pluginmodel import Model1DPlugin
1332from sasmodels.sasview_model import find_model
1333
1334class Model(Model1DPlugin):
1335    name = os.path.splitext(os.path.basename(__file__))[0]
1336    is_multiplicity_model = False
1337    def __init__(self, multiplicity=1):
1338        Model1DPlugin.__init__(self, name='')
1339        P1 = find_model('%s')
1340        P2 = find_model('%s')
1341        p_model1 = P1()
1342        p_model2 = P2()
1343        ## Setting  model name model description
1344        self.description = '%s'
1345        if self.name.rstrip().lstrip() == '':
1346            self.name = self._get_name(p_model1.name, p_model2.name)
1347        if self.description.rstrip().lstrip() == '':
1348            self.description = p_model1.name
1349            self.description += p_model2.name
1350            self.fill_description(p_model1, p_model2)
1351
1352        ## Define parameters
1353        self.params = collections.OrderedDict()
1354
1355        ## Parameter details [units, min, max]
1356        self.details = {}
1357        ## Magnetic Panrameters
1358        self.magnetic_params = []
1359        # non-fittable parameters
1360        self.non_fittable = p_model1.non_fittable
1361        self.non_fittable += p_model2.non_fittable
1362
1363        ##models
1364        self.p_model1= p_model1
1365        self.p_model2= p_model2
1366
1367
1368        ## dispersion
1369        self._set_dispersion()
1370        ## Define parameters
1371        self._set_params()
1372        ## New parameter:scaling_factor
1373        self.params['scale_factor'] = %s
1374
1375        ## Parameter details [units, min, max]
1376        self._set_details()
1377        self.details['scale_factor'] = ['', 0.0, numpy.inf]
1378
1379
1380        #list of parameter that can be fitted
1381        self._set_fixed_params()
1382
1383        ## parameters with orientation
1384        self.orientation_params = []
1385        for item in self.p_model1.orientation_params:
1386            new_item = "p1_" + item
1387            if not new_item in self.orientation_params:
1388                self.orientation_params.append(new_item)
1389
1390        for item in self.p_model2.orientation_params:
1391            new_item = "p2_" + item
1392            if not new_item in self.orientation_params:
1393                self.orientation_params.append(new_item)
1394        ## magnetic params
1395        self.magnetic_params = []
1396        for item in self.p_model1.magnetic_params:
1397            new_item = "p1_" + item
1398            if not new_item in self.magnetic_params:
1399                self.magnetic_params.append(new_item)
1400
1401        for item in self.p_model2.magnetic_params:
1402            new_item = "p2_" + item
1403            if not new_item in self.magnetic_params:
1404                self.magnetic_params.append(new_item)
1405        # get multiplicity if model provide it, else 1.
1406        try:
1407            multiplicity1 = p_model1.multiplicity
1408            try:
1409                multiplicity2 = p_model2.multiplicity
1410            except:
1411                multiplicity2 = 1
1412        except:
1413            multiplicity1 = 1
1414            multiplicity2 = 1
1415        ## functional multiplicity of the model
1416        self.multiplicity1 = multiplicity1
1417        self.multiplicity2 = multiplicity2
1418        self.multiplicity_info = []
1419
1420    def _clone(self, obj):
1421        obj.params     = copy.deepcopy(self.params)
1422        obj.description     = copy.deepcopy(self.description)
1423        obj.details    = copy.deepcopy(self.details)
1424        obj.dispersion = copy.deepcopy(self.dispersion)
1425        obj.p_model1  = self.p_model1.clone()
1426        obj.p_model2  = self.p_model2.clone()
1427        #obj = copy.deepcopy(self)
1428        return obj
1429
1430    def _get_name(self, name1, name2):
1431        p1_name = self._get_upper_name(name1)
1432        if not p1_name:
1433            p1_name = name1
1434        name = p1_name
1435        name += "_and_"
1436        p2_name = self._get_upper_name(name2)
1437        if not p2_name:
1438            p2_name = name2
1439        name += p2_name
1440        return name
1441
1442    def _get_upper_name(self, name=None):
1443        if name == None:
1444            return ""
1445        upper_name = ""
1446        str_name = str(name)
1447        for index in range(len(str_name)):
1448            if str_name[index].isupper():
1449                upper_name += str_name[index]
1450        return upper_name
1451
1452    def _set_dispersion(self):
1453        self.dispersion = collections.OrderedDict()
1454        ##set dispersion only from p_model
1455        for name , value in self.p_model1.dispersion.iteritems():
1456            #if name.lower() not in self.p_model1.orientation_params:
1457            new_name = "p1_" + name
1458            self.dispersion[new_name]= value
1459        for name , value in self.p_model2.dispersion.iteritems():
1460            #if name.lower() not in self.p_model2.orientation_params:
1461            new_name = "p2_" + name
1462            self.dispersion[new_name]= value
1463
1464    def function(self, x=0.0):
1465        return 0
1466
1467    def getProfile(self):
1468        try:
1469            x,y = self.p_model1.getProfile()
1470        except:
1471            x = None
1472            y = None
1473
1474        return x, y
1475
1476    def _set_params(self):
1477        for name , value in self.p_model1.params.iteritems():
1478            # No 2D-supported
1479            #if name not in self.p_model1.orientation_params:
1480            new_name = "p1_" + name
1481            self.params[new_name]= value
1482
1483        for name , value in self.p_model2.params.iteritems():
1484            # No 2D-supported
1485            #if name not in self.p_model2.orientation_params:
1486            new_name = "p2_" + name
1487            self.params[new_name]= value
1488
1489        # Set "scale" as initializing
1490        self._set_scale_factor()
1491
1492
1493    def _set_details(self):
1494        for name ,detail in self.p_model1.details.iteritems():
1495            new_name = "p1_" + name
1496            #if new_name not in self.orientation_params:
1497            self.details[new_name]= detail
1498
1499        for name ,detail in self.p_model2.details.iteritems():
1500            new_name = "p2_" + name
1501            #if new_name not in self.orientation_params:
1502            self.details[new_name]= detail
1503
1504    def _set_scale_factor(self):
1505        pass
1506
1507
1508    def setParam(self, name, value):
1509        # set param to this (p1, p2) model
1510        self._setParamHelper(name, value)
1511
1512        ## setParam to p model
1513        model_pre = ''
1514        new_name = ''
1515        name_split = name.split('_', 1)
1516        if len(name_split) == 2:
1517            model_pre = name.split('_', 1)[0]
1518            new_name = name.split('_', 1)[1]
1519        if model_pre == "p1":
1520            if new_name in self.p_model1.getParamList():
1521                self.p_model1.setParam(new_name, value)
1522        elif model_pre == "p2":
1523             if new_name in self.p_model2.getParamList():
1524                self.p_model2.setParam(new_name, value)
1525        elif name == 'scale_factor':
1526            self.params['scale_factor'] = value
1527        else:
1528            raise ValueError, "Model does not contain parameter %s" % name
1529
1530    def getParam(self, name):
1531        # Look for dispersion parameters
1532        toks = name.split('.')
1533        if len(toks)==2:
1534            for item in self.dispersion.keys():
1535                # 2D not supported
1536                if item.lower()==toks[0].lower():
1537                    for par in self.dispersion[item]:
1538                        if par.lower() == toks[1].lower():
1539                            return self.dispersion[item][par]
1540        else:
1541            # Look for standard parameter
1542            for item in self.params.keys():
1543                if item.lower()==name.lower():
1544                    return self.params[item]
1545        return
1546        #raise ValueError, "Model does not contain parameter %s" % name
1547
1548    def _setParamHelper(self, name, value):
1549        # Look for dispersion parameters
1550        toks = name.split('.')
1551        if len(toks)== 2:
1552            for item in self.dispersion.keys():
1553                if item.lower()== toks[0].lower():
1554                    for par in self.dispersion[item]:
1555                        if par.lower() == toks[1].lower():
1556                            self.dispersion[item][par] = value
1557                            return
1558        else:
1559            # Look for standard parameter
1560            for item in self.params.keys():
1561                if item.lower()== name.lower():
1562                    self.params[item] = value
1563                    return
1564
1565        raise ValueError, "Model does not contain parameter %s" % name
1566
1567
1568    def _set_fixed_params(self):
1569        self.fixed = []
1570        for item in self.p_model1.fixed:
1571            new_item = "p1" + item
1572            self.fixed.append(new_item)
1573        for item in self.p_model2.fixed:
1574            new_item = "p2" + item
1575            self.fixed.append(new_item)
1576
1577        self.fixed.sort()
1578
1579
1580    def run(self, x = 0.0):
1581        self._set_scale_factor()
1582        return self.params['scale_factor'] %s \
1583(self.p_model1.run(x) %s self.p_model2.run(x))
1584
1585    def runXY(self, x = 0.0):
1586        self._set_scale_factor()
1587        return self.params['scale_factor'] %s \
1588(self.p_model1.runXY(x) %s self.p_model2.runXY(x))
1589
1590    ## Now (May27,10) directly uses the model eval function
1591    ## instead of the for-loop in Base Component.
1592    def evalDistribution(self, x = []):
1593        self._set_scale_factor()
1594        return self.params['scale_factor'] %s \
1595(self.p_model1.evalDistribution(x) %s \
1596self.p_model2.evalDistribution(x))
1597
1598    def set_dispersion(self, parameter, dispersion):
1599        value= None
1600        new_pre = parameter.split("_", 1)[0]
1601        new_parameter = parameter.split("_", 1)[1]
1602        try:
1603            if new_pre == 'p1' and \
1604new_parameter in self.p_model1.dispersion.keys():
1605                value= self.p_model1.set_dispersion(new_parameter, dispersion)
1606            if new_pre == 'p2' and \
1607new_parameter in self.p_model2.dispersion.keys():
1608                value= self.p_model2.set_dispersion(new_parameter, dispersion)
1609            self._set_dispersion()
1610            return value
1611        except:
1612            raise
1613
1614    def fill_description(self, p_model1, p_model2):
1615        description = ""
1616        description += "This model gives the summation or multiplication of"
1617        description += "%s and %s. "% ( p_model1.name, p_model2.name )
1618        self.description += description
1619
1620if __name__ == "__main__":
1621    m1= Model()
1622    #m1.setParam("p1_scale", 25)
1623    #m1.setParam("p1_length", 1000)
1624    #m1.setParam("p2_scale", 100)
1625    #m1.setParam("p2_rg", 100)
1626    out1 = m1.runXY(0.01)
1627
1628    m2= Model()
1629    #m2.p_model1.setParam("scale", 25)
1630    #m2.p_model1.setParam("length", 1000)
1631    #m2.p_model2.setParam("scale", 100)
1632    #m2.p_model2.setParam("rg", 100)
1633    out2 = m2.p_model1.runXY(0.01) %s m2.p_model2.runXY(0.01)\n
1634    print "My name is %s."% m1.name
1635    print out1, " = ", out2
1636    if out1 == out2:
1637        print "===> Simple Test: Passed!"
1638    else:
1639        print "===> Simple Test: Failed!"
1640"""
1641
1642if __name__ == "__main__":
1643#    app = wx.PySimpleApp()
1644    main_app = wx.App()
1645    main_frame = TextDialog(id=1, model_list=["SphereModel", "CylinderModel"],
1646                       plugin_dir='../fitting/plugin_models')
1647    main_frame.ShowModal()
1648    main_app.MainLoop()
1649
1650#if __name__ == "__main__":
1651#    from sas.sasgui.perspectives.fitting import models
1652#    dir_path = models.find_plugins_dir()
1653#    app = wx.App()
1654#    window = EditorWindow(parent=None, base=None, path=dir_path, title="Editor")
1655#    app.MainLoop()
Note: See TracBrowser for help on using the repository browser.