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

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

Update the Sum|Multi editor model selectors when a new user model is generated.

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