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

ticket885
Last change on this file since b6b81a3 was b6b81a3, checked in by Gonzalez, Miguel <gonzalez@…>, 6 years ago

Work on progress on simple editor

  • Property mode set to 100644
File size: 45.3 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.volume_sizer = None
589        self.button_sizer = None
590        self.param_strings = ''
591        self.function_strings = ''
592        self._notes = ""
593        self._msg_box = None
594        self.msg_sizer = None
595        self.warning = ""
596        #This does not seem to be used anywhere so commenting out for now
597        #    -- PDB 2/26/17
598        #self._description = "New Plugin Model"
599        self.function_tcl = None
600        self.volume_tcl = None
601        self.math_combo = None
602        self.bt_apply = None
603        self.bt_close = None
604        #self._default_save_location = os.getcwd()
605        self._do_layout()
606
607
608
609    def _define_structure(self):
610        """
611        define initial sizer
612        """
613        #w, h = self.parent.GetSize()
614        self.main_sizer = wx.BoxSizer(wx.VERTICAL)
615        self.name_sizer = wx.BoxSizer(wx.VERTICAL)
616        self.name_hsizer = wx.BoxSizer(wx.HORIZONTAL)
617        self.desc_sizer = wx.BoxSizer(wx.VERTICAL)
618        self.param_sizer = wx.BoxSizer(wx.VERTICAL)
619        self.function_sizer = wx.BoxSizer(wx.VERTICAL)
620        self.func_horizon_sizer = wx.BoxSizer(wx.HORIZONTAL)
621        self.volume_sizer = wx.BoxSizer(wx.VERTICAL)
622        self.button_sizer = wx.BoxSizer(wx.HORIZONTAL)
623        self.msg_sizer = wx.BoxSizer(wx.HORIZONTAL)
624
625    def _layout_name(self):
626        """
627        Do the layout for file/function name related widgets
628        """
629        #title name [string]
630        name_txt = wx.StaticText(self, -1, 'Function Name : ')
631        self.overwrite_cb = wx.CheckBox(self, -1, "Overwrite existing plugin model of this name?", (10, 10))
632        self.overwrite_cb.SetValue(False)
633        self.overwrite_cb.SetToolTipString("Overwrite it if already exists?")
634        wx.EVT_CHECKBOX(self, self.overwrite_cb.GetId(), self.on_over_cb)
635        self.name_tcl = wx.TextCtrl(self, -1, size=(PANEL_WIDTH * 3 / 5, -1))
636        self.name_tcl.Bind(wx.EVT_TEXT_ENTER, self.on_change_name)
637        self.name_tcl.SetValue('')
638        self.name_tcl.SetFont(self.font)
639        hint_name = "Unique Model Function Name."
640        self.name_tcl.SetToolTipString(hint_name)
641        self.name_hsizer.AddMany([(self.name_tcl, 0, wx.LEFT | wx.TOP, 0),
642                                  (self.overwrite_cb, 0, wx.LEFT, 20)])
643        self.name_sizer.AddMany([(name_txt, 0, wx.LEFT | wx.TOP, 10),
644                                 (self.name_hsizer, 0,
645                                  wx.LEFT | wx.TOP | wx.BOTTOM, 10)])
646
647
648    def _layout_description(self):
649        """
650        Do the layout for description related widgets
651        """
652        #title name [string]
653        desc_txt = wx.StaticText(self, -1, 'Description (optional) : ')
654        self.desc_tcl = wx.TextCtrl(self, -1, size=(PANEL_WIDTH * 3 / 5, -1))
655        self.desc_tcl.SetValue('')
656        hint_desc = "Write a short description of the model function."
657        self.desc_tcl.SetToolTipString(hint_desc)
658        self.desc_sizer.AddMany([(desc_txt, 0, wx.LEFT | wx.TOP, 10),
659                                 (self.desc_tcl, 0,
660                                  wx.LEFT | wx.TOP | wx.BOTTOM, 10)])
661    def _layout_param(self):
662        """
663        Do the layout for parameter related widgets
664        """
665        param_txt = wx.StaticText(self, -1, 'Non volume parameters (no polydisperse): ')
666
667        param_tip =  "# Set the parameters that do NOT enter \n"
668        param_tip += " in the volume calculation and their initial values.\n"
669        param_tip += "# Example:\n"
670        param_tip += "sld_sphere = 1\n"
671        param_tip += "sld_solvent = 6"
672        newid = wx.NewId()
673        self.param_tcl = EditWindow(self, newid, wx.DefaultPosition,
674                                    wx.DefaultSize,
675                                    wx.CLIP_CHILDREN | wx.SUNKEN_BORDER)
676        self.param_tcl.setDisplayLineNumbers(True)
677        self.param_tcl.SetToolTipString(param_tip)
678
679        self.param_sizer.AddMany([(param_txt, 0, wx.LEFT, 10),
680                                  (self.param_tcl, 1, wx.EXPAND | wx.ALL, 10)])
681
682        # Parameters with polydispersity
683        pd_param_txt = wx.StaticText(self, -1, 'Volume parameters ' + \
684                                     ' (can be polydisperse): ')
685
686        pd_param_tip = "# Set the volume parameters and " + \
687        "their initial values.\n"
688        pd_param_tip += "# Example:\n"
689        pd_param_tip += "R_sphere = 100"
690        newid = wx.NewId()
691        self.pd_param_tcl = EditWindow(self, newid, wx.DefaultPosition,
692                                    wx.DefaultSize,
693                                    wx.CLIP_CHILDREN | wx.SUNKEN_BORDER)
694        self.pd_param_tcl.setDisplayLineNumbers(True)
695        self.pd_param_tcl.SetToolTipString(pd_param_tip)
696
697        self.param_sizer.AddMany([(pd_param_txt, 0, wx.LEFT, 10),
698                                  (self.pd_param_tcl, 1, wx.EXPAND | wx.ALL, 10)])
699
700    def _layout_function(self):
701        """
702        Do the layout for function related widgets
703        """
704        function_txt = wx.StaticText(self, -1, 'Function(x) : ')
705        hint_function =  "# Write the function describing the form factor. \n"
706        hint_function += "# Mathematical functions can be selected from the list. \n"
707        hint_function += "# Other numpy functions can also be used by writing e.g. \n"
708        hint_function += "# numpy.sinh(x) or np.sinh(x). \n"
709        hint_function += "# Example: \n"
710        hint_function += "V = (4./3.)*pi*R_sphere**3 \n"
711        hint_function += "qr = x*R_sphere \n"
712        hint_function += "sn, cn = sin(qr), cos(qr) \n"
713        hint_function += "if qr > 0: \n"
714        hint_function += "   bes = 3*(sn-qr*cn)/qr**3 \n"
715        hint_function += "else: \n"
716        hint_function += "   bes = 1.0 \n"
717        hint_function += "fq = bes * (sld_sphere-sld_solvent)*V \n"
718        hint_function += "return 1.0e-4 * fq**2 \n"
719        math_txt = wx.StaticText(self, -1, '*Useful math functions: ')
720        math_combo = self._fill_math_combo()
721
722        newid = wx.NewId()
723        self.function_tcl = EditWindow(self, newid, wx.DefaultPosition,
724                                       wx.DefaultSize,
725                                       wx.CLIP_CHILDREN | wx.SUNKEN_BORDER)
726        self.function_tcl.setDisplayLineNumbers(True)
727        self.function_tcl.SetToolTipString(hint_function)
728
729        self.func_horizon_sizer.Add(function_txt)
730        self.func_horizon_sizer.Add(math_txt, 0, wx.LEFT, 250)
731        self.func_horizon_sizer.Add(math_combo, 0, wx.LEFT, 10)
732
733        self.function_sizer.Add(self.func_horizon_sizer, 0, wx.LEFT, 10)
734        self.function_sizer.Add(self.function_tcl, 1, wx.EXPAND | wx.ALL, 10)
735
736    def _layout_volume(self):
737        """
738        Do the layout for the form_volume widget
739        """
740        volume_txt = wx.StaticText(self, -1, 'Function defining the volume of the particle' + \
741                                  ' (if left empty, V = 1.0): ')
742
743        volume_tip = "# Example: \n"
744        volume_tip += " V = (4./3.)*pi*R_sphere**3 \n"
745        volume_tip += "return V \n"
746        newid = wx.NewId()
747        self.volume_tcl = EditWindow(self, newid, wx.DefaultPosition,
748                                    wx.DefaultSize,
749                                    wx.CLIP_CHILDREN | wx.SUNKEN_BORDER)
750        self.volume_tcl.setDisplayLineNumbers(True)
751        self.volume_tcl.SetToolTipString(volume_tip)
752
753        self.volume_sizer.AddMany([(volume_txt, 0, wx.LEFT, 10),
754                                  (self.volume_tcl, 1, wx.EXPAND | wx.ALL, 10)])
755
756
757    def _layout_msg(self):
758        """
759        Layout msg
760        """
761        self._msg_box = wx.StaticText(self, -1, self._notes,
762                                      size=(PANEL_WIDTH, -1))
763        self.msg_sizer.Add(self._msg_box, 0, wx.LEFT, 10)
764
765    def _layout_button(self):
766        """
767        Do the layout for the button widgets
768        """
769        self.bt_apply = wx.Button(self, -1, "Apply", size=(_BOX_WIDTH, -1))
770        self.bt_apply.SetToolTipString("Save changes into the imported data.")
771        self.bt_apply.Bind(wx.EVT_BUTTON, self.on_click_apply)
772
773        self.bt_help = wx.Button(self, -1, "HELP", size=(_BOX_WIDTH, -1))
774        self.bt_help.SetToolTipString("Get Help For Model Editor")
775        self.bt_help.Bind(wx.EVT_BUTTON, self.on_help)
776
777        self.bt_close = wx.Button(self, -1, 'Close', size=(_BOX_WIDTH, -1))
778        self.bt_close.Bind(wx.EVT_BUTTON, self.on_close)
779        self.bt_close.SetToolTipString("Close this panel.")
780
781        self.button_sizer.AddMany([(self.bt_apply, 0,0),
782                                   (self.bt_help, 0, wx.LEFT | wx.BOTTOM,15),
783                                   (self.bt_close, 0, wx.LEFT | wx.RIGHT, 15)])
784
785    def _do_layout(self):
786        """
787        Draw the current panel
788        """
789        self._define_structure()
790        self._layout_name()
791        self._layout_description()
792        self._layout_param()
793        self._layout_function()
794        self._layout_volume()
795        self._layout_msg()
796        self._layout_button()
797        self.main_sizer.AddMany([(self.name_sizer, 0, wx.EXPAND | wx.ALL, 5),
798                                 (wx.StaticLine(self), 0,
799                                  wx.ALL | wx.EXPAND, 5),
800                                 (self.desc_sizer, 0, wx.EXPAND | wx.ALL, 5),
801                                 (wx.StaticLine(self), 0,
802                                  wx.ALL | wx.EXPAND, 5),
803                                 (self.param_sizer, 1, wx.EXPAND | wx.ALL, 5),
804                                 (wx.StaticLine(self), 0,
805                                  wx.ALL | wx.EXPAND, 5),
806                                 (self.function_sizer, 1,
807                                  wx.EXPAND | wx.ALL, 5),
808                                 (wx.StaticLine(self), 0,
809                                  wx.ALL | wx.EXPAND, 5),
810                                 (self.volume_sizer, 1, wx.EXPAND | wx.ALL, 5),
811                                 (wx.StaticLine(self), 0,
812                                  wx.ALL | wx.EXPAND, 5),
813                                 (self.msg_sizer, 0, wx.EXPAND | wx.ALL, 5),
814                                 (self.button_sizer, 0, wx.ALIGN_RIGHT)])
815        self.SetSizer(self.main_sizer)
816        self.SetAutoLayout(True)
817
818    def _fill_math_combo(self):
819        """
820        Fill up the math combo box
821        """
822        self.math_combo = wx.ComboBox(self, -1, size=(100, -1),
823                                      style=wx.CB_READONLY)
824        for item in dir(math):
825            if item.count("_") < 1:
826                try:
827                    exec "float(math.%s)" % item
828                    self.math_combo.Append(str(item))
829                except Exception:
830                    self.math_combo.Append(str(item) + "()")
831        self.math_combo.Bind(wx.EVT_COMBOBOX, self._on_math_select)
832        self.math_combo.SetSelection(0)
833        return self.math_combo
834
835    def _on_math_select(self, event):
836        """
837        On math selection on ComboBox
838        """
839        event.Skip()
840        label = self.math_combo.GetValue()
841        self.function_tcl.SetFocus()
842        # Put the text at the cursor position
843        pos = self.function_tcl.GetCurrentPos()
844        self.function_tcl.InsertText(pos, label)
845        # Put the cursor at appropriate position
846        length = len(label)
847        print(length)
848        if label[length-1] == ')':
849            length -= 1
850        f_pos = pos + length
851        self.function_tcl.GotoPos(f_pos)
852
853    def get_notes(self):
854        """
855        return notes
856        """
857        return self._notes
858
859    def on_change_name(self, event=None):
860        """
861        Change name
862        """
863        if event is not None:
864            event.Skip()
865        self.name_tcl.SetBackgroundColour('white')
866        self.Refresh()
867
868    def check_name(self):
869        """
870        Check name if exist already
871        """
872        self._notes = ''
873        self.on_change_name(None)
874        plugin_dir = self.path
875        list_fnames = os.listdir(plugin_dir)
876        # function/file name
877        title = self.name_tcl.GetValue().lstrip().rstrip()
878        self.name = title
879        t_fname = title + '.py'
880        if not self.overwrite_name:
881            if t_fname in list_fnames:
882                self.name_tcl.SetBackgroundColour('pink')
883                return False
884        self.fname = os.path.join(plugin_dir, t_fname)
885        s_title = title
886        if len(title) > 20:
887            s_title = title[0:19] + '...'
888        self._notes += "Model function name is set "
889        self._notes += "to %s. \n" % str(s_title)
890        return True
891
892    def on_over_cb(self, event):
893        """
894        Set overwrite name flag on cb event
895        """
896        if event is not None:
897            event.Skip()
898        cb_value = event.GetEventObject()
899        self.overwrite_name = cb_value.GetValue()
900
901    def on_click_apply(self, event):
902        """
903        Changes are saved in data object imported to edit.
904
905        checks firs for valid name, then if it already exists then checks
906        that a function was entered and finally that if entered it contains at
907        least a return statement.  If all passes writes file then tries to
908        compile.  If compile fails or import module fails or run method fails
909        tries to remove any .py and pyc files that may have been created and
910        sets error message.
911
912        :todo this code still could do with a careful going over to clean
913        up and simplify. the non GUI methods such as this one should be removed
914        to computational code of SasView. Most of those computational methods
915        would be the same for both the simple editors.
916        """
917        #must post event here
918        event.Skip()
919        name = self.name_tcl.GetValue().lstrip().rstrip()
920        info = 'Info'
921        msg = ''
922        result, check_err = '', ''
923        # Sort out the errors if occur
924        # First check for valid python name then if the name already exists
925        if not name or not bool(re.match('^[A-Za-z0-9_]*$', name)):
926            msg = '"%s" '%name
927            msg += "is not a valid model name. Name must not be empty and \n"
928            msg += "may include only alpha numeric or underline characters \n"
929            msg += "and no spaces"
930        elif self.check_name():
931            description = self.desc_tcl.GetValue()
932            param_str = self.param_tcl.GetText()
933            pd_param_str = self.pd_param_tcl.GetText()
934            func_str = self.function_tcl.GetText()
935            volume_str = self.volume_tcl.GetText()
936            # No input for the model function
937            if func_str.lstrip().rstrip():
938                if func_str.count('return') > 0:
939                    self.write_file(self.fname, name, description, param_str,
940                                    pd_param_str, func_str, volume_str)
941                    try:
942                        result, msg = check_model(self.fname), None
943                    except Exception:
944                        import traceback
945                        result, msg = None, "error building model"
946                        check_err = "\n"+traceback.format_exc(limit=2)
947                else:
948                    msg = "Error: The func(x) must 'return' a value at least.\n"
949                    msg += "For example: \n\nreturn 2*x"
950            else:
951                msg = 'Error: Function is not defined.'
952        else:
953            msg = "Name exists already."
954
955        #
956        if self.base is not None and not msg:
957            self.base.update_custom_combo()
958
959        # Prepare the messagebox
960        if msg:
961            info = 'Error'
962            color = 'red'
963            self.overwrite_cb.SetValue(True)
964            self.overwrite_name = True
965        else:
966            self._notes = result
967            msg = "Successful! Please look for %s in Plugin Models."%name
968            msg += "  " + self._notes
969            info = 'Info'
970            color = 'blue'
971        self._msg_box.SetLabel(msg)
972        self._msg_box.SetForegroundColour(color)
973        # Send msg to the top window
974        if self.base is not None:
975            from sas.sasgui.guiframe.events import StatusEvent
976            wx.PostEvent(self.base.parent,
977                         StatusEvent(status=msg+check_err, info=info))
978        self.warning = msg
979
980    def write_file(self, fname, name, desc_str, param_str, pd_param_str, func_str, volume_str):
981        """
982        Write content in file
983
984        :param fname: full file path
985        :param desc_str: content of the description strings
986        :param param_str: content of params; Strings
987        :param pd_param_str: content of params requiring polydispersity; Strings
988        :param func_str: content of func; Strings
989        """
990        out_f = open(fname, 'w')
991
992        out_f.write(CUSTOM_TEMPLATE % {
993            'name': name,
994            'title': 'User model for ' + name,
995            'description': desc_str,
996            'date': datetime.datetime.now().strftime('%YYYY-%mm-%dd'),
997        })
998
999        # Write out parameters
1000        param_names = []    # to store parameter names
1001        pd_params = []
1002        out_f.write('parameters = [ \n')
1003        out_f.write('#   ["name", "units", default, [lower, upper], "type", "description"],\n')
1004        for pname, pvalue, desc in self.get_param_helper(param_str):
1005            param_names.append(pname)
1006            out_f.write("    ['%s', '', %s, [-inf, inf], '', '%s'],\n"
1007                        % (pname, pvalue, desc))
1008        for pname, pvalue, desc in self.get_param_helper(pd_param_str):
1009            param_names.append(pname)
1010            pd_params.append(pname)
1011            out_f.write("    ['%s', '', %s, [-inf, inf], 'volume', '%s'],\n"
1012                        % (pname, pvalue, desc))
1013        out_f.write('    ]\n')
1014
1015        # Write out function definition
1016        out_f.write('\n')
1017        out_f.write('def Iq(%s):\n' % ', '.join(['x'] + param_names))
1018        out_f.write('    """Absolute scattering"""\n')
1019        if "numpy." in func_str:
1020            out_f.write('    import numpy\n')
1021        elif "np." in func_str:
1022            out_f.write('    import numpy as np\n')
1023        for func_line in func_str.split('\n'):
1024            out_f.write('%s%s' % ('    ', func_line))
1025        out_f.write('\n')   
1026        out_f.write('## uncomment the following if Iq works for vector x\n')
1027        out_f.write('#    Iq.vectorized = True\n')
1028       
1029        # Write out form_volume
1030        out_f.write('\n')
1031        out_f.write('def form_volume(%s):\n' % ', '.join(pd_params))
1032        out_f.write('    """ \n')
1033        out_f.write('    Volume of the particles used to compute absolute scattering intensity \n')
1034        out_f.write('    and to weight polydisperse parameter contributions. \n')
1035        out_f.write('    """\n')
1036        if volume_str.lstrip().rstrip() and volume_str.count('return'):
1037            if "numpy." in volume_str:
1038                out_f.write('    import numpy\n')
1039            elif "np." in volume_str:
1040                out_f.write('    import numpy as np\n')
1041            for vol_line in volume_str.split('\n'):
1042                out_f.write('%s%s' % ('    ', vol_line))
1043        else:
1044            out_f.write('    return 1.0 \n')
1045        out_f.write('\n')
1046
1047        # If polydisperse, create place holders also for ER and VR
1048        #if pd_params:
1049        #    out_f.write('\n')
1050        #    out_f.write(CUSTOM_TEMPLATE_PD % {'args': ', '.join(pd_params)})
1051
1052        # Create place holder for Iqxy
1053        out_f.write('\n')
1054        out_f.write('#def Iqxy(%s):\n' % ', '.join(["x", "y"] + param_names))
1055        out_f.write('#    """Absolute scattering of oriented particles."""\n')
1056        out_f.write('#    ...\n')
1057        out_f.write('#    return oriented_form(x, y, args)\n')
1058        out_f.write('## uncomment the following if Iqxy works for vector x, y\n')
1059        out_f.write('#I    qxy.vectorized = True\n')
1060
1061        out_f.close()
1062
1063    def get_param_helper(self, param_str):
1064        """
1065        yield a sequence of name, value pairs for the parameters in param_str
1066
1067        Parameters can be defined by one per line by name=value, or multiple
1068        on the same line by separating the pairs by semicolon or comma.  The
1069        value is optional and defaults to "1.0".
1070        """
1071        for line in param_str.replace(';', ',').split('\n'):
1072            for item in line.split(','):
1073                defn, desc = item.split('#', 1) if '#' in item else (item, '')
1074                name, value = defn.split('=', 1) if '=' in defn else (defn, '1.0')
1075                if name:
1076                    yield [v.strip() for v in (name, value, desc)]
1077
1078    def set_function_helper(self, line):
1079        """
1080        Get string in line to define the local params
1081
1082        :param line: one line of string got from the param_str
1083        """
1084        params_str = ''
1085        spaces = '        '#8spaces
1086        items = line.split(";")
1087        for item in items:
1088            name = item.split("=")[0].lstrip().rstrip()
1089            params_str += spaces + "%s = self.params['%s']\n" % (name, name)
1090        return params_str
1091
1092    def get_warning(self):
1093        """
1094        Get the warning msg
1095        """
1096        return self.warning
1097
1098    def on_help(self, event):
1099        """
1100        Bring up the New Plugin Model Editor Documentation whenever
1101        the HELP button is clicked.
1102
1103        Calls DocumentationWindow with the path of the location within the
1104        documentation tree (after /doc/ ....".  Note that when using old
1105        versions of Wx (before 2.9) and thus not the release version of
1106        installers, the help comes up at the top level of the file as
1107        webbrowser does not pass anything past the # to the browser when it is
1108        running "file:///...."
1109
1110        :param evt: Triggers on clicking the help button
1111        """
1112
1113        _TreeLocation = "user/sasgui/perspectives/fitting/fitting_help.html"
1114        _PageAnchor = "#new-plugin-model"
1115        _doc_viewer = DocumentationWindow(self, -1, _TreeLocation, _PageAnchor,
1116                                          "Plugin Model Editor Help")
1117
1118    def on_close(self, event):
1119        """
1120        leave data as it is and close
1121        """
1122        self.parent.Show(False)#Close()
1123        event.Skip()
1124
1125class EditorWindow(wx.Frame):
1126    """
1127    Editor Window
1128    """
1129    def __init__(self, parent, base, path, title,
1130                 size=(EDITOR_WIDTH, EDITOR_HEIGTH), *args, **kwds):
1131        """
1132        Init
1133        """
1134        kwds["title"] = title
1135        kwds["size"] = size
1136        wx.Frame.__init__(self, parent=None, *args, **kwds)
1137        self.parent = parent
1138        self.panel = EditorPanel(parent=self, base=parent,
1139                                 path=path, title=title)
1140        self.Show(True)
1141        wx.EVT_CLOSE(self, self.on_close)
1142
1143    def on_close(self, event):
1144        """
1145        On close event
1146        """
1147        self.Show(False)
1148        #if self.parent is not None:
1149        #    self.parent.new_model_frame = None
1150        #self.Destroy()
1151
1152## Templates for plugin models
1153
1154CUSTOM_TEMPLATE = '''\
1155r"""
1156Definition
1157----------
1158
1159Calculates %(name)s.
1160
1161%(description)s
1162
1163References
1164----------
1165
1166Authorship and Verification
1167---------------------------
1168
1169* **Author:** --- **Date:** %(date)s
1170* **Last Modified by:** --- **Date:** %(date)s
1171* **Last Reviewed by:** --- **Date:** %(date)s
1172"""
1173
1174from math import *
1175from numpy import inf
1176
1177name = "%(name)s"
1178title = "%(title)s"
1179description = """%(description)s"""
1180
1181'''
1182
1183CUSTOM_TEMPLATE_PD = '''\
1184def ER(%(args)s):
1185    """
1186    Effective radius of particles to be used when computing structure factors.
1187
1188    Input parameters are vectors ranging over the mesh of polydispersity values.
1189    """
1190    return 1.0
1191
1192def VR(%(args)s):
1193    """
1194    Volume ratio of particles to be used when computing structure factors.
1195
1196    Input parameters are vectors ranging over the mesh of polydispersity values.
1197    """
1198    return 1.0
1199'''
1200
1201SUM_TEMPLATE = """
1202from sasmodels.core import load_model_info
1203from sasmodels.sasview_model import make_model_from_info
1204
1205model_info = load_model_info('{model1}{operator}{model2}')
1206model_info.name = '{name}'{desc_line}
1207Model = make_model_from_info(model_info)
1208"""
1209if __name__ == "__main__":
1210    main_app = wx.App()
1211    main_frame = TextDialog(id=1, model_list=["SphereModel", "CylinderModel"],
1212                            plugin_dir='../fitting/plugin_models')
1213    main_frame.ShowModal()
1214    main_app.MainLoop()
Note: See TracBrowser for help on using the repository browser.