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

magnetic_scattrelease-4.2.2ticket-1009ticket-1094-headlessticket-1242-2d-resolutionticket-1243ticket-1249unittest-saveload
Last change on this file since 9258c43c was 9258c43c, checked in by GitHub <noreply@…>, 14 months ago

Merge pull request #151 from SasView?/ticket885b

Ticket885b

This reverts to no polydisperse parameters — ticket #885 needs to be updated to be a feature request adding polydispersity … correctly this time.

  • Property mode set to 100644
File size: 40.8 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        self._set_model_list()
394        if self.parent.parent is not None:
395            from sas.sasgui.guiframe.events import StatusEvent
396            wx.PostEvent(self.parent.parent, StatusEvent(status=msg,
397                                                         info=info))
398
399    def on_help(self, event):
400        """
401        Bring up the Composite Model Editor Documentation whenever
402        the HELP button is clicked.
403
404        Calls DocumentationWindow with the path of the location within the
405        documentation tree (after /doc/ ....".  Note that when using old
406        versions of Wx (before 2.9) and thus not the release version of
407        installers, the help comes up at the top level of the file as
408        webbrowser does not pass anything past the # to the browser when it is
409        running "file:///...."
410
411    :param evt: Triggers on clicking the help button
412    """
413
414        _TreeLocation = "user/sasgui/perspectives/fitting/fitting_help.html"
415        _PageAnchor = "#sum-multi-p1-p2"
416        _doc_viewer = DocumentationWindow(self, -1, _TreeLocation, _PageAnchor,
417                                          "Composite Model Editor Help")
418
419    def _set_model_list(self):
420        """
421        Set the list of models
422        """
423        # list of model names
424        # get regular models
425        main_list = self.model_list
426        # get custom models
427        self.update_cm_list()
428        # add custom models to model list
429        for name in self.cm_list:
430            if name not in main_list:
431                main_list.append(name)
432
433        if len(main_list) > 1:
434            main_list.sort()
435        self.model1.Clear()
436        self.model2.Clear()
437        for idx in range(len(main_list)):
438            self.model1.Append(str(main_list[idx]), idx)
439            self.model2.Append(str(main_list[idx]), idx)
440        self.model1.SetStringSelection(self.model1_string)
441        self.model2.SetStringSelection(self.model2_string)
442
443    def update_cm_list(self):
444        """
445        Update custom model list
446        """
447        cm_list = []
448        al_list = os.listdir(self.plugin_dir)
449        for c_name in al_list:
450            if c_name.split('.')[-1] == 'py' and \
451                    c_name.split('.')[0] != '__init__':
452                name = str(c_name.split('.')[0])
453                cm_list.append(name)
454        self.cm_list = cm_list
455
456    def on_model1(self, event):
457        """
458        Set model1
459        """
460        event.Skip()
461        self.update_cm_list()
462        self.model1_name = str(self.model1.GetValue())
463        self.model1_string = self.model1_name
464        if self.model1_name in self.cm_list:
465            self.is_p1_custom = True
466        else:
467            self.is_p1_custom = False
468
469    def on_model2(self, event):
470        """
471        Set model2
472        """
473        event.Skip()
474        self.update_cm_list()
475        self.model2_name = str(self.model2.GetValue())
476        self.model2_string = self.model2_name
477        if self.model2_name in self.cm_list:
478            self.is_p2_custom = True
479        else:
480            self.is_p2_custom = False
481
482    def on_select_operator(self, event=None):
483        """
484        On Select an Operator
485        """
486        # For Mac
487        if event is not None:
488            event.Skip()
489        item = event.GetEventObject()
490        text = item.GetValue()
491        self.fill_explanation_helpstring(text)
492
493    def fill_explanation_helpstring(self, operator):
494        """
495        Choose the equation to use depending on whether we now have
496        a sum or multiply model then create the appropriate string
497        """
498        name = ''
499        if operator == '*':
500            name = 'Multi'
501            factor = 'background'
502        else:
503            name = 'Sum'
504            factor = 'scale_factor'
505
506        self._operator = operator
507        self.explanation = ("  Plugin_model = scale_factor * (model_1 {} "
508            "model_2) + background").format(operator)
509        self.explanationctr.SetLabel(self.explanation)
510        self.name = name + M_NAME
511
512
513    def fill_operator_combox(self):
514        """
515        fill the current combobox with the operator
516        """
517        operator_list = ['+', '*']
518        for oper in operator_list:
519            pos = self._operator_choice.Append(str(oper))
520            self._operator_choice.SetClientData(pos, str(oper))
521        self._operator_choice.SetSelection(0)
522
523    def get_textnames(self):
524        """
525        Returns model name string as list
526        """
527        return [self.model1_name, self.model2_name]
528
529    def write_string(self, fname, model1_name, model2_name):
530        """
531        Write and Save file
532        """
533        self.fname = fname
534        description = self.desc_tcl.GetValue().lstrip().rstrip()
535        desc_line = ''
536        if description.strip() != '':
537            # Sasmodels generates a description for us. If the user provides
538            # their own description, add a line to overwrite the sasmodels one
539            desc_line = "\nmodel_info.description = '{}'".format(description)
540        name = os.path.splitext(os.path.basename(self.fname))[0]
541        output = SUM_TEMPLATE.format(name=name, model1=model1_name,
542            model2=model2_name, operator=self._operator, desc_line=desc_line)
543        with open(self.fname, 'w') as out_f:
544            out_f.write(output)
545
546    def compile_file(self, path):
547        """
548        Compile the file in the path
549        """
550        path = self.fname
551        show_model_output(self, path)
552
553    def delete_file(self, path):
554        """
555        Delete file in the path
556        """
557        _delete_file(path)
558
559
560class EditorPanel(wx.ScrolledWindow):
561    """
562    Simple Plugin Model function editor
563    """
564    def __init__(self, parent, base, path, title, *args, **kwds):
565        kwds['name'] = title
566#        kwds["size"] = (EDITOR_WIDTH, EDITOR_HEIGTH)
567        kwds["style"] = wx.FULL_REPAINT_ON_RESIZE
568        wx.ScrolledWindow.__init__(self, parent, *args, **kwds)
569        self.SetScrollbars(1,1,1,1)
570        self.parent = parent
571        self.base = base
572        self.path = path
573        self.font = wx.SystemSettings_GetFont(wx.SYS_SYSTEM_FONT)
574        self.font.SetPointSize(10)
575        self.reader = None
576        self.name = 'untitled'
577        self.overwrite_name = False
578        self.is_2d = False
579        self.fname = None
580        self.main_sizer = None
581        self.name_sizer = None
582        self.name_hsizer = None
583        self.name_tcl = None
584        self.overwrite_cb = None
585        self.desc_sizer = None
586        self.desc_tcl = None
587        self.param_sizer = None
588        self.param_tcl = None
589        self.function_sizer = None
590        self.func_horizon_sizer = None
591        self.button_sizer = None
592        self.param_strings = ''
593        self.function_strings = ''
594        self._notes = ""
595        self._msg_box = None
596        self.msg_sizer = None
597        self.warning = ""
598        #This does not seem to be used anywhere so commenting out for now
599        #    -- PDB 2/26/17
600        #self._description = "New Plugin Model"
601        self.function_tcl = None
602        self.math_combo = None
603        self.bt_apply = None
604        self.bt_close = None
605        #self._default_save_location = os.getcwd()
606        self._do_layout()
607
608
609
610    def _define_structure(self):
611        """
612        define initial sizer
613        """
614        #w, h = self.parent.GetSize()
615        self.main_sizer = wx.BoxSizer(wx.VERTICAL)
616        self.name_sizer = wx.BoxSizer(wx.VERTICAL)
617        self.name_hsizer = wx.BoxSizer(wx.HORIZONTAL)
618        self.desc_sizer = wx.BoxSizer(wx.VERTICAL)
619        self.param_sizer = wx.BoxSizer(wx.VERTICAL)
620        self.function_sizer = wx.BoxSizer(wx.VERTICAL)
621        self.func_horizon_sizer = wx.BoxSizer(wx.HORIZONTAL)
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, 'Fit Parameters: ')
666
667        param_tip = "#Set the parameters and their initial values.\n"
668        param_tip += "#Example:\n"
669        param_tip += "A = 1\nB = 1"
670        #param_txt.SetToolTipString(param_tip)
671        newid = wx.NewId()
672        self.param_tcl = EditWindow(self, newid, wx.DefaultPosition,
673                                    wx.DefaultSize,
674                                    wx.CLIP_CHILDREN | wx.SUNKEN_BORDER)
675        self.param_tcl.setDisplayLineNumbers(True)
676        self.param_tcl.SetToolTipString(param_tip)
677
678        self.param_sizer.AddMany([(param_txt, 0, wx.LEFT, 10),
679                                  (self.param_tcl, 1, wx.EXPAND | wx.ALL, 10)])
680
681
682    def _layout_function(self):
683        """
684        Do the layout for function related widgets
685        """
686        function_txt = wx.StaticText(self, -1, 'Function(x) : ')
687        hint_function = "#Example:\n"
688        hint_function += "if x <= 0:\n"
689        hint_function += "    y = A + B\n"
690        hint_function += "else:\n"
691        hint_function += "    y = A + B * cos(2 * pi * x)\n"
692        hint_function += "return y\n"
693        math_txt = wx.StaticText(self, -1, '*Useful math functions: ')
694        math_combo = self._fill_math_combo()
695
696        newid = wx.NewId()
697        self.function_tcl = EditWindow(self, newid, wx.DefaultPosition,
698                                       wx.DefaultSize,
699                                       wx.CLIP_CHILDREN | wx.SUNKEN_BORDER)
700        self.function_tcl.setDisplayLineNumbers(True)
701        self.function_tcl.SetToolTipString(hint_function)
702
703        self.func_horizon_sizer.Add(function_txt)
704        self.func_horizon_sizer.Add(math_txt, 0, wx.LEFT, 250)
705        self.func_horizon_sizer.Add(math_combo, 0, wx.LEFT, 10)
706
707        self.function_sizer.Add(self.func_horizon_sizer, 0, wx.LEFT, 10)
708        self.function_sizer.Add(self.function_tcl, 1, wx.EXPAND | wx.ALL, 10)
709
710    def _layout_msg(self):
711        """
712        Layout msg
713        """
714        self._msg_box = wx.StaticText(self, -1, self._notes,
715                                      size=(PANEL_WIDTH, -1))
716        self.msg_sizer.Add(self._msg_box, 0, wx.LEFT, 10)
717
718    def _layout_button(self):
719        """
720        Do the layout for the button widgets
721        """
722        self.bt_apply = wx.Button(self, -1, "Apply", size=(_BOX_WIDTH, -1))
723        self.bt_apply.SetToolTipString("Save changes into the imported data.")
724        self.bt_apply.Bind(wx.EVT_BUTTON, self.on_click_apply)
725
726        self.bt_help = wx.Button(self, -1, "HELP", size=(_BOX_WIDTH, -1))
727        self.bt_help.SetToolTipString("Get Help For Model Editor")
728        self.bt_help.Bind(wx.EVT_BUTTON, self.on_help)
729
730        self.bt_close = wx.Button(self, -1, 'Close', size=(_BOX_WIDTH, -1))
731        self.bt_close.Bind(wx.EVT_BUTTON, self.on_close)
732        self.bt_close.SetToolTipString("Close this panel.")
733
734        self.button_sizer.AddMany([(self.bt_apply, 0,0),
735                                   (self.bt_help, 0, wx.LEFT | wx.BOTTOM,15),
736                                   (self.bt_close, 0, wx.LEFT | wx.RIGHT, 15)])
737
738    def _do_layout(self):
739        """
740        Draw the current panel
741        """
742        self._define_structure()
743        self._layout_name()
744        self._layout_description()
745        self._layout_param()
746        self._layout_function()
747        self._layout_msg()
748        self._layout_button()
749        self.main_sizer.AddMany([(self.name_sizer, 0, wx.EXPAND | wx.ALL, 5),
750                                 (wx.StaticLine(self), 0,
751                                  wx.ALL | wx.EXPAND, 5),
752                                 (self.desc_sizer, 0, wx.EXPAND | wx.ALL, 5),
753                                 (wx.StaticLine(self), 0,
754                                  wx.ALL | wx.EXPAND, 5),
755                                 (self.param_sizer, 1, wx.EXPAND | wx.ALL, 5),
756                                 (wx.StaticLine(self), 0,
757                                  wx.ALL | wx.EXPAND, 5),
758                                 (self.function_sizer, 2,
759                                  wx.EXPAND | wx.ALL, 5),
760                                 (wx.StaticLine(self), 0,
761                                  wx.ALL | wx.EXPAND, 5),
762                                 (self.msg_sizer, 0, wx.EXPAND | wx.ALL, 5),
763                                 (self.button_sizer, 0, wx.ALIGN_RIGHT)])
764        self.SetSizer(self.main_sizer)
765        self.SetAutoLayout(True)
766
767    def _fill_math_combo(self):
768        """
769        Fill up the math combo box
770        """
771        self.math_combo = wx.ComboBox(self, -1, size=(100, -1),
772                                      style=wx.CB_READONLY)
773        for item in dir(math):
774            if item.count("_") < 1:
775                try:
776                    exec "float(math.%s)" % item
777                    self.math_combo.Append(str(item))
778                except Exception:
779                    self.math_combo.Append(str(item) + "()")
780        self.math_combo.Bind(wx.EVT_COMBOBOX, self._on_math_select)
781        self.math_combo.SetSelection(0)
782        return self.math_combo
783
784    def _on_math_select(self, event):
785        """
786        On math selection on ComboBox
787        """
788        event.Skip()
789        label = self.math_combo.GetValue()
790        self.function_tcl.SetFocus()
791        # Put the text at the cursor position
792        pos = self.function_tcl.GetCurrentPos()
793        self.function_tcl.InsertText(pos, label)
794        # Put the cursor at appropriate position
795        length = len(label)
796        print(length)
797        if label[length-1] == ')':
798            length -= 1
799        f_pos = pos + length
800        self.function_tcl.GotoPos(f_pos)
801
802    def get_notes(self):
803        """
804        return notes
805        """
806        return self._notes
807
808    def on_change_name(self, event=None):
809        """
810        Change name
811        """
812        if event is not None:
813            event.Skip()
814        self.name_tcl.SetBackgroundColour('white')
815        self.Refresh()
816
817    def check_name(self):
818        """
819        Check name if exist already
820        """
821        self._notes = ''
822        self.on_change_name(None)
823        plugin_dir = self.path
824        list_fnames = os.listdir(plugin_dir)
825        # function/file name
826        title = self.name_tcl.GetValue().lstrip().rstrip()
827        self.name = title
828        t_fname = title + '.py'
829        if not self.overwrite_name:
830            if t_fname in list_fnames:
831                self.name_tcl.SetBackgroundColour('pink')
832                return False
833        self.fname = os.path.join(plugin_dir, t_fname)
834        s_title = title
835        if len(title) > 20:
836            s_title = title[0:19] + '...'
837        self._notes += "Model function name is set "
838        self._notes += "to %s. \n" % str(s_title)
839        return True
840
841    def on_over_cb(self, event):
842        """
843        Set overwrite name flag on cb event
844        """
845        if event is not None:
846            event.Skip()
847        cb_value = event.GetEventObject()
848        self.overwrite_name = cb_value.GetValue()
849
850    def on_click_apply(self, event):
851        """
852        Changes are saved in data object imported to edit.
853
854        checks firs for valid name, then if it already exists then checks
855        that a function was entered and finally that if entered it contains at
856        least a return statement.  If all passes writes file then tries to
857        compile.  If compile fails or import module fails or run method fails
858        tries to remove any .py and pyc files that may have been created and
859        sets error message.
860
861        :todo this code still could do with a careful going over to clean
862        up and simplify. the non GUI methods such as this one should be removed
863        to computational code of SasView. Most of those computational methods
864        would be the same for both the simple editors.
865        """
866        #must post event here
867        event.Skip()
868        name = self.name_tcl.GetValue().lstrip().rstrip()
869        info = 'Info'
870        msg = ''
871        result, check_err = '', ''
872        # Sort out the errors if occur
873        # First check for valid python name then if the name already exists
874        if not name or not bool(re.match('^[A-Za-z0-9_]*$', name)):
875            msg = '"%s" '%name
876            msg += "is not a valid model name. Name must not be empty and \n"
877            msg += "may include only alpha numeric or underline characters \n"
878            msg += "and no spaces"
879        elif self.check_name():
880            description = self.desc_tcl.GetValue()
881            param_str = self.param_tcl.GetText()
882            func_str = self.function_tcl.GetText()
883            # No input for the model function
884            if func_str.lstrip().rstrip():
885                if func_str.count('return') > 0:
886                    self.write_file(self.fname, name, description, param_str,
887                                    func_str)
888                    try:
889                        result, msg = check_model(self.fname), None
890                    except Exception:
891                        import traceback
892                        result, msg = None, "error building model"
893                        check_err = "\n"+traceback.format_exc(limit=2)
894                else:
895                    msg = "Error: The func(x) must 'return' a value at least.\n"
896                    msg += "For example: \n\nreturn 2*x"
897            else:
898                msg = 'Error: Function is not defined.'
899        else:
900            msg = "Name exists already."
901
902        #
903        if self.base is not None and not msg:
904            self.base.update_custom_combo()
905
906        # Prepare the messagebox
907        if msg:
908            info = 'Error'
909            color = 'red'
910            self.overwrite_cb.SetValue(True)
911            self.overwrite_name = True
912        else:
913            self._notes = result
914            msg = "Successful! Please look for %s in Plugin Models."%name
915            msg += "  " + self._notes
916            info = 'Info'
917            color = 'blue'
918        self._msg_box.SetLabel(msg)
919        self._msg_box.SetForegroundColour(color)
920        # Send msg to the top window
921        if self.base is not None:
922            from sas.sasgui.guiframe.events import StatusEvent
923            wx.PostEvent(self.base.parent,
924                         StatusEvent(status=msg+check_err, info=info))
925        self.warning = msg
926
927    def write_file(self, fname, name, desc_str, param_str, func_str):
928        """
929        Write content in file
930
931        :param fname: full file path
932        :param desc_str: content of the description strings
933        :param param_str: content of params; Strings
934        :param func_str: content of func; Strings
935        """
936        out_f = open(fname, 'w')
937
938        out_f.write(CUSTOM_TEMPLATE % {
939            'name': name,
940            'title': 'User model for ' + name,
941            'description': desc_str,
942            'date': datetime.datetime.now().strftime('%YYYY-%mm-%dd'),
943        })
944
945        # Write out parameters
946        param_names = []    # to store parameter names
947        out_f.write('parameters = [ \n')
948        out_f.write('#   ["name", "units", default, [lower, upper], "type", "description"],\n')
949        for pname, pvalue, desc in self.get_param_helper(param_str):
950            param_names.append(pname)
951            out_f.write("    ['%s', '', %s, [-inf, inf], '', '%s'],\n"
952                        % (pname, pvalue, desc))
953        out_f.write('    ]\n')
954
955        # Write out function definition
956        out_f.write('\n')
957        out_f.write('def Iq(%s):\n' % ', '.join(['x'] + param_names))
958        out_f.write('    """Absolute scattering"""\n')
959        if "scipy." in func_str:
960            out_f.write('    import scipy')
961        if "numpy." in func_str:
962            out_f.write('    import numpy')
963        if "np." in func_str:
964            out_f.write('    import numpy as np')
965        for func_line in func_str.split('\n'):
966            out_f.write('%s%s\n' % ('    ', func_line))
967        out_f.write('## uncomment the following if Iq works for vector x\n')
968        out_f.write('#Iq.vectorized = True\n')
969
970        # Create place holder for Iqxy
971        out_f.write('\n')
972        out_f.write('#def Iqxy(%s):\n' % ', '.join(["x", "y"] + param_names))
973        out_f.write('#    """Absolute scattering of oriented particles."""\n')
974        out_f.write('#    ...\n')
975        out_f.write('#    return oriented_form(x, y, args)\n')
976        out_f.write('## uncomment the following if Iqxy works for vector x, y\n')
977        out_f.write('#Iqxy.vectorized = True\n')
978
979        out_f.close()
980
981    def get_param_helper(self, param_str):
982        """
983        yield a sequence of name, value pairs for the parameters in param_str
984
985        Parameters can be defined by one per line by name=value, or multiple
986        on the same line by separating the pairs by semicolon or comma.  The
987        value is optional and defaults to "1.0".
988        """
989        for line in param_str.replace(';', ',').split('\n'):
990            for item in line.split(','):
991                defn, desc = item.split('#', 1) if '#' in item else (item, '')
992                name, value = defn.split('=', 1) if '=' in defn else (defn, '1.0')
993                if name:
994                    yield [v.strip() for v in (name, value, desc)]
995
996    def set_function_helper(self, line):
997        """
998        Get string in line to define the local params
999
1000        :param line: one line of string got from the param_str
1001        """
1002        params_str = ''
1003        spaces = '        '#8spaces
1004        items = line.split(";")
1005        for item in items:
1006            name = item.split("=")[0].lstrip().rstrip()
1007            params_str += spaces + "%s = self.params['%s']\n" % (name, name)
1008        return params_str
1009
1010    def get_warning(self):
1011        """
1012        Get the warning msg
1013        """
1014        return self.warning
1015
1016    def on_help(self, event):
1017        """
1018        Bring up the New Plugin Model Editor Documentation whenever
1019        the HELP button is clicked.
1020
1021        Calls DocumentationWindow with the path of the location within the
1022        documentation tree (after /doc/ ....".  Note that when using old
1023        versions of Wx (before 2.9) and thus not the release version of
1024        installers, the help comes up at the top level of the file as
1025        webbrowser does not pass anything past the # to the browser when it is
1026        running "file:///...."
1027
1028        :param evt: Triggers on clicking the help button
1029        """
1030
1031        _TreeLocation = "user/sasgui/perspectives/fitting/fitting_help.html"
1032        _PageAnchor = "#new-plugin-model"
1033        _doc_viewer = DocumentationWindow(self, -1, _TreeLocation, _PageAnchor,
1034                                          "Plugin Model Editor Help")
1035
1036    def on_close(self, event):
1037        """
1038        leave data as it is and close
1039        """
1040        self.parent.Show(False)#Close()
1041        event.Skip()
1042
1043class EditorWindow(wx.Frame):
1044    """
1045    Editor Window
1046    """
1047    def __init__(self, parent, base, path, title,
1048                 size=(EDITOR_WIDTH, EDITOR_HEIGTH), *args, **kwds):
1049        """
1050        Init
1051        """
1052        kwds["title"] = title
1053        kwds["size"] = size
1054        wx.Frame.__init__(self, parent=None, *args, **kwds)
1055        self.parent = parent
1056        self.panel = EditorPanel(parent=self, base=parent,
1057                                 path=path, title=title)
1058        self.Show(True)
1059        wx.EVT_CLOSE(self, self.on_close)
1060
1061    def on_close(self, event):
1062        """
1063        On close event
1064        """
1065        self.Show(False)
1066        #if self.parent is not None:
1067        #    self.parent.new_model_frame = None
1068        #self.Destroy()
1069
1070## Templates for plugin models
1071
1072CUSTOM_TEMPLATE = '''\
1073r"""
1074Definition
1075----------
1076
1077Calculates %(name)s.
1078
1079%(description)s
1080
1081References
1082----------
1083
1084Authorship and Verification
1085---------------------------
1086
1087* **Author:** --- **Date:** %(date)s
1088* **Last Modified by:** --- **Date:** %(date)s
1089* **Last Reviewed by:** --- **Date:** %(date)s
1090"""
1091
1092from math import *
1093from numpy import inf
1094
1095name = "%(name)s"
1096title = "%(title)s"
1097description = """%(description)s"""
1098
1099'''
1100
1101SUM_TEMPLATE = """
1102from sasmodels.core import load_model_info
1103from sasmodels.sasview_model import make_model_from_info
1104
1105model_info = load_model_info('{model1}{operator}{model2}')
1106model_info.name = '{name}'{desc_line}
1107Model = make_model_from_info(model_info)
1108"""
1109if __name__ == "__main__":
1110    main_app = wx.App()
1111    main_frame = TextDialog(id=1, model_list=["SphereModel", "CylinderModel"],
1112                            plugin_dir='../fitting/plugin_models')
1113    main_frame.ShowModal()
1114    main_app.MainLoop()
Note: See TracBrowser for help on using the repository browser.