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

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.2.2ticket-1009ticket-1094-headlessticket-1242-2d-resolutionticket-1243ticket-1249ticket885unittest-saveload
Last change on this file since 23359ccb was 23359ccb, checked in by lewis, 7 years ago

Fix Sum/Multi? model template

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