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

magnetic_scattrelease-4.2.2ticket-1009ticket-1094-headlessticket-1242-2d-resolutionticket-1243ticket-1249ticket885unittest-saveload
Last change on this file since fe15198 was 69363c7, checked in by Paul Kienzle <pkienzle@…>, 7 years ago

Merge branch 'master' into ticket-853-fit-gui-to-calc

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