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

magnetic_scattrelease-4.2.2ticket-1009ticket-1094-headlessticket-1242-2d-resolutionticket-1243ticket-1249unittest-saveload
Last change on this file since 5501189 was 5501189, checked in by gonzalezm, 5 years ago

Simple editor without PD parameters

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