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

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalccostrafo411magnetic_scattrelease-4.2.2ticket-1009ticket-1094-headlessticket-1242-2d-resolutionticket-1243ticket-1249ticket885unittest-saveload
Last change on this file since 07ec714 was 07ec714, checked in by lewis, 7 years ago

Tick overwrite box when invalid model is saved

  • Property mode set to 100644
File size: 58.5 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
33from wx.py.editwindow import EditWindow
34from sas.sasgui.guiframe.documentation_window import DocumentationWindow
35from .pyconsole import show_model_output, check_model
36
37logger = logging.getLogger(__name__)
38
39
40if sys.platform.count("win32") > 0:
41    FONT_VARIANT = 0
42    PNL_WIDTH = 450
43    PNL_HEIGHT = 320
44else:
45    FONT_VARIANT = 1
46    PNL_WIDTH = 590
47    PNL_HEIGHT = 350
48M_NAME = 'Model'
49EDITOR_WIDTH = 800
50EDITOR_HEIGTH = 735
51PANEL_WIDTH = 500
52_BOX_WIDTH = 55
53
54def _delete_file(path):
55    """
56    Delete file in the path
57    """
58    try:
59        os.remove(path)
60    except:
61        raise
62
63
64class TextDialog(wx.Dialog):
65    """
66    Dialog for easy custom composite models.  Provides a wx.Dialog panel
67    to choose two existing models (including pre-existing Plugin Models which
68    may themselves be composite models) as well as an operation on those models
69    (add or multiply) the resulting model will add a scale parameter for summed
70    models and a background parameter for a multiplied model.
71
72    The user also gives a brief help for the model in a description box and
73    must provide a unique name which is verified as unique before the new
74    model is saved.
75
76    This Dialog pops up for the user when they press 'Sum|Multi(p1,p2)' under
77    'Plugin Model Operations' under 'Fitting' menu.  This is currently called as
78    a Modal Dialog.
79
80    :TODO the build in compiler currently balks at when it tries to import
81    a model whose name contains spaces or symbols (such as + ... underscore
82    should be fine).  Have fixed so the editor cannot save such a file name
83    but if a file is dropped in the plugin directory from outside this class
84    will create a file that cannot be compiled.  Should add the check to
85    the write method or to the on_modelx method.
86
87    - PDB:April 5, 2015
88    """
89    def __init__(self, parent=None, base=None, id=None, title='',
90                 model_list=[], plugin_dir=None):
91        """
92        This class is run when instatiated.  The __init__ initializes and
93        calls the internal methods necessary.  On exiting the wx.Dialog
94        window should be destroyed.
95        """
96        wx.Dialog.__init__(self, parent=parent, id=id,
97                           title=title, size=(PNL_WIDTH, PNL_HEIGHT))
98        self.parent = base
99        #Font
100        self.SetWindowVariant(variant=FONT_VARIANT)
101        # default
102        self.overwrite_name = False
103        self.plugin_dir = plugin_dir
104        self.model_list = model_list
105        self.model1_string = "sphere"
106        self.model2_string = "cylinder"
107        self.name = 'Sum' + M_NAME
108        self.factor = 'scale_factor'
109        self._notes = ''
110        self._operator = '+'
111        self._operator_choice = None
112        self.explanation = ''
113        self.explanationctr = None
114        self.type = None
115        self.name_sizer = None
116        self.name_tcl = None
117        self.desc_sizer = None
118        self.desc_tcl = None
119        self._selection_box = None
120        self.model1 = None
121        self.model2 = None
122        self.static_line_1 = None
123        self.ok_button = None
124        self.close_button = None
125        self._msg_box = None
126        self.msg_sizer = None
127        self.fname = None
128        self.cm_list = None
129        self.is_p1_custom = False
130        self.is_p2_custom = False
131        self._build_sizer()
132        self.model1_name = str(self.model1.GetValue())
133        self.model2_name = str(self.model2.GetValue())
134        self.good_name = True
135        self.fill_oprator_combox()
136
137    def _layout_name(self):
138        """
139        Do the layout for file/function name related widgets
140        """
141        #container for new model name input
142        self.name_sizer = wx.BoxSizer(wx.HORIZONTAL)
143
144        #set up label and input box with tool tip and event handling
145        name_txt = wx.StaticText(self, -1, 'Function Name : ')
146        self.name_tcl = wx.TextCtrl(self, -1, value='MySumFunction')
147        self.name_tcl.Bind(wx.EVT_TEXT_ENTER, self.on_change_name)
148        hint_name = "Unique Sum/Multiply Model Function Name."
149        self.name_tcl.SetToolTipString(hint_name)
150
151        self.name_sizer.AddMany([(name_txt, 0, wx.LEFT | wx.TOP, 10),
152                                 (self.name_tcl, -1,
153                                  wx.EXPAND | wx.RIGHT | wx.TOP | wx.BOTTOM,
154                                  10)])
155
156
157    def _layout_description(self):
158        """
159        Do the layout for description related widgets
160        """
161        #container for new model description input
162        self.desc_sizer = wx.BoxSizer(wx.HORIZONTAL)
163
164        #set up description label and input box with tool tip and event handling
165        desc_txt = wx.StaticText(self, -1, 'Description (optional) : ')
166        self.desc_tcl = wx.TextCtrl(self, -1)
167        hint_desc = "Write a short description of this model function."
168        self.desc_tcl.SetToolTipString(hint_desc)
169
170        self.desc_sizer.AddMany([(desc_txt, 0, wx.LEFT | wx.TOP, 10),
171                                 (self.desc_tcl, -1,
172                                  wx.EXPAND | wx.RIGHT | wx.TOP | wx.BOTTOM,
173                                  10)])
174
175
176    def _layout_model_selection(self):
177        """
178        Do the layout for model selection related widgets
179        """
180        box_width = 195 # combobox width
181
182        #First set up main sizer for the selection
183        selection_box_title = wx.StaticBox(self, -1, 'Select',
184                                           size=(PNL_WIDTH - 30, 70))
185        self._selection_box = wx.StaticBoxSizer(selection_box_title,
186                                                wx.VERTICAL)
187
188        #Next create the help labels for the model selection
189        select_help_box = wx.BoxSizer(wx.HORIZONTAL)
190        model_string = " Model%s (p%s):"
191        select_help_box.Add(wx.StaticText(self, -1, model_string % (1, 1)),
192                            0, 0)
193        select_help_box.Add((box_width - 25, 10), 0, 0)
194        select_help_box.Add(wx.StaticText(self, -1, model_string % (2, 2)),
195                            0, 0)
196        self._selection_box.Add(select_help_box, 0, 0)
197
198        #Next create the actual selection box with 3 combo boxes
199        selection_box_choose = wx.BoxSizer(wx.HORIZONTAL)
200
201        self.model1 = wx.ComboBox(self, -1, style=wx.CB_READONLY)
202        wx.EVT_COMBOBOX(self.model1, -1, self.on_model1)
203        self.model1.SetMinSize((box_width * 5 / 6, -1))
204        self.model1.SetToolTipString("model1")
205
206        self._operator_choice = wx.ComboBox(self, -1, size=(50, -1),
207                                            style=wx.CB_READONLY)
208        wx.EVT_COMBOBOX(self._operator_choice, -1, self.on_select_operator)
209        operation_tip = "Add: +, Multiply: * "
210        self._operator_choice.SetToolTipString(operation_tip)
211
212        self.model2 = wx.ComboBox(self, -1, style=wx.CB_READONLY)
213        wx.EVT_COMBOBOX(self.model2, -1, self.on_model2)
214        self.model2.SetMinSize((box_width * 5 / 6, -1))
215        self.model2.SetToolTipString("model2")
216        self._set_model_list()
217
218        selection_box_choose.Add(self.model1, 0, 0)
219        selection_box_choose.Add((15, 10))
220        selection_box_choose.Add(self._operator_choice, 0, 0)
221        selection_box_choose.Add((15, 10))
222        selection_box_choose.Add(self.model2, 0, 0)
223        # add some space between labels and selection
224        self._selection_box.Add((20, 5), 0, 0)
225        self._selection_box.Add(selection_box_choose, 0, 0)
226
227    def _build_sizer(self):
228        """
229        Build GUI with calls to _layout_name, _layout Description
230        and _layout_model_selection which each build a their portion of the
231        GUI.
232        """
233        mainsizer = wx.BoxSizer(wx.VERTICAL) # create main sizer for dialog
234
235        # build fromm top by calling _layout_name and _layout_description
236        # and adding to main sizer
237        self._layout_name()
238        mainsizer.Add(self.name_sizer, 0, wx.EXPAND)
239        self._layout_description()
240        mainsizer.Add(self.desc_sizer, 0, wx.EXPAND)
241
242        # Add an explanation of dialog (short help)
243        self.explanationctr = wx.StaticText(self, -1, self.explanation)
244        self.fill_explanation_helpstring(self._operator)
245        mainsizer.Add(self.explanationctr, 0, wx.LEFT | wx.EXPAND, 15)
246
247        # Add the selection box stuff with border and labels built
248        # by _layout_model_selection
249        self._layout_model_selection()
250        mainsizer.Add(self._selection_box, 0, wx.LEFT, 15)
251
252        # Add a space and horizontal line before the notification
253        #messages and the buttons at the bottom
254        mainsizer.Add((10, 10))
255        self.static_line_1 = wx.StaticLine(self, -1)
256        mainsizer.Add(self.static_line_1, 0, wx.EXPAND, 10)
257
258        # Add action status notification line (null at startup)
259        self._msg_box = wx.StaticText(self, -1, self._notes)
260        self.msg_sizer = wx.BoxSizer(wx.HORIZONTAL)
261        self.msg_sizer.Add(self._msg_box, 0, wx.LEFT, 0)
262        mainsizer.Add(self.msg_sizer, 0,
263                      wx.LEFT | wx.RIGHT | wx.ADJUST_MINSIZE | wx.BOTTOM, 10)
264
265        # Finally add the buttons (apply and close) on the bottom
266        # Eventually need to add help here
267        self.ok_button = wx.Button(self, wx.ID_OK, 'Apply')
268        _app_tip = "Save the new Model."
269        self.ok_button.SetToolTipString(_app_tip)
270        self.ok_button.Bind(wx.EVT_BUTTON, self.check_name)
271        self.help_button = wx.Button(self, -1, 'HELP')
272        _app_tip = "Help on composite model creation."
273        self.help_button.SetToolTipString(_app_tip)
274        self.help_button.Bind(wx.EVT_BUTTON, self.on_help)
275        self.close_button = wx.Button(self, wx.ID_CANCEL, 'Close')
276        sizer_button = wx.BoxSizer(wx.HORIZONTAL)
277        sizer_button.AddMany([((20, 20), 1, 0),
278                              (self.ok_button, 0, 0),
279                              (self.help_button, 0, 0),
280                              (self.close_button, 0, wx.LEFT | wx.RIGHT, 10)])
281        mainsizer.Add(sizer_button, 0, wx.EXPAND | wx.BOTTOM | wx.TOP, 10)
282
283        self.SetSizer(mainsizer)
284        self.Centre()
285
286    def on_change_name(self, event=None):
287        """
288        Change name
289        """
290        if event is not None:
291            event.Skip()
292        self.name_tcl.SetBackgroundColour('white')
293        self.Refresh()
294
295    def check_name(self, event=None):
296        """
297        Check that proposed new model name is a valid Python module name
298        and that it does not already exist. If not show error message and
299        pink background in text box else call on_apply
300
301        :TODO this should be separated out from the GUI code.  For that we
302        need to pass it the name (or if we want to keep the default name
303        option also need to pass the self._operator attribute) We just need
304        the function to return an error code that the name is good or if
305        not why (not a valid name, name exists already).  The rest of the
306        error handling should be done in this module. so on_apply would then
307        start by checking the name and then either raise errors or do the
308        deed.
309        """
310        #Get the function/file name
311        mname = M_NAME
312        self.on_change_name(None)
313        title = self.name_tcl.GetValue().lstrip().rstrip()
314        if title == '':
315            text = self._operator
316            if text.count('+') > 0:
317                mname = 'Sum'
318            else:
319                mname = 'Multi'
320            mname += M_NAME
321            title = mname
322        self.name = title
323        t_fname = title + '.py'
324
325        #First check if the name is a valid Python name
326        if re.match('^[A-Za-z0-9_]*$', title):
327            self.good_name = True
328        else:
329            self.good_name = False
330            msg = ("%s is not a valid Python name. Only alphanumeric \n" \
331                   "and underscore allowed" % self.name)
332
333        #Now check if the name already exists
334        if not self.overwrite_name and self.good_name:
335            #Create list of existing model names for comparison
336            list_fnames = os.listdir(self.plugin_dir)
337            # fake existing regular model name list
338            m_list = [model + ".py" for model in self.model_list]
339            list_fnames.append(m_list)
340            if t_fname in list_fnames and title != mname:
341                self.good_name = False
342                msg = "Name exists already."
343
344        if self.good_name == False:
345            self.name_tcl.SetBackgroundColour('pink')
346            info = 'Error'
347            wx.MessageBox(msg, info)
348            self._notes = msg
349            color = 'red'
350            self._msg_box.SetLabel(msg)
351            self._msg_box.SetForegroundColour(color)
352            return self.good_name
353        self.fname = os.path.join(self.plugin_dir, t_fname)
354        s_title = title
355        if len(title) > 20:
356            s_title = title[0:19] + '...'
357        self._notes = "Model function (%s) has been set! \n" % str(s_title)
358        self.good_name = True
359        self.on_apply(self.fname)
360        return self.good_name
361
362    def on_apply(self, path):
363        """
364        This method is a misnomer - it is not bound to the apply button
365        event.  Instead the apply button event goes to check_name which
366        then calls this method if the name of the new file is acceptable.
367
368        :TODO this should be bound to the apply button.  The first line
369        should call the check_name method which itself should be in another
370        module separated from the the GUI modules.
371        """
372        self.name_tcl.SetBackgroundColour('white')
373        try:
374            label = self.get_textnames()
375            fname = path
376            name1 = label[0]
377            name2 = label[1]
378            self.write_string(fname, name1, name2)
379            success = show_model_output(self, fname)
380            if success:
381                self.parent.update_custom_combo()
382            msg = self._notes
383            info = 'Info'
384            color = 'blue'
385        except:
386            msg = "Easy Sum/Multipy Plugin: Error occurred..."
387            info = 'Error'
388            color = 'red'
389        self._msg_box.SetLabel(msg)
390        self._msg_box.SetForegroundColour(color)
391        if self.parent.parent is not None:
392            from sas.sasgui.guiframe.events import StatusEvent
393            wx.PostEvent(self.parent.parent, StatusEvent(status=msg,
394                                                         info=info))
395
396    def on_help(self, event):
397        """
398        Bring up the Composite Model Editor Documentation whenever
399        the HELP button is clicked.
400
401        Calls DocumentationWindow with the path of the location within the
402        documentation tree (after /doc/ ....".  Note that when using old
403        versions of Wx (before 2.9) and thus not the release version of
404        installers, the help comes up at the top level of the file as
405        webbrowser does not pass anything past the # to the browser when it is
406        running "file:///...."
407
408    :param evt: Triggers on clicking the help button
409    """
410
411        _TreeLocation = "user/sasgui/perspectives/fitting/fitting_help.html"
412        _PageAnchor = "#sum-multi-p1-p2"
413        _doc_viewer = DocumentationWindow(self, -1, _TreeLocation, _PageAnchor,
414                                          "Composite Model Editor Help")
415
416    def _set_model_list(self):
417        """
418        Set the list of models
419        """
420        # list of model names
421        # get regular models
422        main_list = self.model_list
423        # get custom models
424        self.update_cm_list()
425        # add custom models to model list
426        for name in self.cm_list:
427            if name not in main_list:
428                main_list.append(name)
429
430        if len(main_list) > 1:
431            main_list.sort()
432        for idx in range(len(main_list)):
433            self.model1.Append(str(main_list[idx]), idx)
434            self.model2.Append(str(main_list[idx]), idx)
435        self.model1.SetStringSelection(self.model1_string)
436        self.model2.SetStringSelection(self.model2_string)
437
438    def update_cm_list(self):
439        """
440        Update custom model list
441        """
442        cm_list = []
443        al_list = os.listdir(self.plugin_dir)
444        for c_name in al_list:
445            if c_name.split('.')[-1] == 'py' and \
446                    c_name.split('.')[0] != '__init__':
447                name = str(c_name.split('.')[0])
448                cm_list.append(name)
449        self.cm_list = cm_list
450
451    def on_model1(self, event):
452        """
453        Set model1
454        """
455        event.Skip()
456        self.update_cm_list()
457        self.model1_name = str(self.model1.GetValue())
458        self.model1_string = self.model1_name
459        if self.model1_name in self.cm_list:
460            self.is_p1_custom = True
461        else:
462            self.is_p1_custom = False
463
464    def on_model2(self, event):
465        """
466        Set model2
467        """
468        event.Skip()
469        self.update_cm_list()
470        self.model2_name = str(self.model2.GetValue())
471        self.model2_string = self.model2_name
472        if self.model2_name in self.cm_list:
473            self.is_p2_custom = True
474        else:
475            self.is_p2_custom = False
476
477    def on_select_operator(self, event=None):
478        """
479        On Select an Operator
480        """
481        # For Mac
482        if event is not None:
483            event.Skip()
484        item = event.GetEventObject()
485        text = item.GetValue()
486        self.fill_explanation_helpstring(text)
487
488    def fill_explanation_helpstring(self, operator):
489        """
490        Choose the equation to use depending on whether we now have
491        a sum or multiply model then create the appropriate string
492        """
493
494        name = ''
495
496        if operator == '*':
497            name = 'Multi'
498            factor = 'BackGround'
499            f_oper = '+'
500        else:
501            name = 'Sum'
502            factor = 'scale_factor'
503            f_oper = '*'
504
505        self.factor = factor
506        self._operator = operator
507        self.explanation = "  Plugin Model = %s %s (model1 %s model2)\n" % \
508                           (self.factor, f_oper, self._operator)
509        self.explanationctr.SetLabel(self.explanation)
510        self.name = name + M_NAME
511
512
513    def fill_oprator_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, name1, name2):
530        """
531        Write and Save file
532        """
533        self.fname = fname
534        description = self.desc_tcl.GetValue().lstrip().rstrip()
535        if description == '':
536            description = name1 + self._operator + name2
537        text = self._operator_choice.GetValue()
538        if text.count('+') > 0:
539            factor = 'scale_factor'
540            f_oper = '*'
541            default_val = '1.0'
542        else:
543            factor = 'BackGround'
544            f_oper = '+'
545            default_val = '0.0'
546        path = self.fname
547        try:
548            out_f = open(path, 'w')
549        except:
550            raise
551        lines = SUM_TEMPLATE.split('\n')
552        for line in lines:
553            try:
554                if line.count("scale_factor"):
555                    line = line.replace('scale_factor', factor)
556                    #print "scale_factor", line
557                if line.count("= %s"):
558                    out_f.write(line % (default_val) + "\n")
559                elif line.count("import Model as P1"):
560                    if self.is_p1_custom:
561                        line = line.replace('#', '')
562                        out_f.write(line % name1 + "\n")
563                    else:
564                        out_f.write(line + "\n")
565                elif line.count("import %s as P1"):
566                    if not self.is_p1_custom:
567                        line = line.replace('#', '')
568                        out_f.write(line % (name1) + "\n")
569                    else:
570                        out_f.write(line + "\n")
571                elif line.count("import Model as P2"):
572                    if self.is_p2_custom:
573                        line = line.replace('#', '')
574                        out_f.write(line % name2 + "\n")
575                    else:
576                        out_f.write(line + "\n")
577                elif line.count("import %s as P2"):
578                    if not self.is_p2_custom:
579                        line = line.replace('#', '')
580                        out_f.write(line % (name2) + "\n")
581                    else:
582                        out_f.write(line + "\n")
583                elif line.count("P1 = find_model"):
584                    out_f.write(line % (name1) + "\n")
585                elif line.count("P2 = find_model"):
586                    out_f.write(line % (name2) + "\n")
587
588                elif line.count("self.description = '%s'"):
589                    out_f.write(line % description + "\n")
590                #elif line.count("run") and line.count("%s"):
591                #    out_f.write(line % self._operator + "\n")
592                #elif line.count("evalDistribution") and line.count("%s"):
593                #    out_f.write(line % self._operator + "\n")
594                elif line.count("return") and line.count("%s") == 2:
595                    #print "line return", line
596                    out_f.write(line % (f_oper, self._operator) + "\n")
597                elif line.count("out2")and line.count("%s"):
598                    out_f.write(line % self._operator + "\n")
599                else:
600                    out_f.write(line + "\n")
601            except:
602                raise
603        out_f.close()
604        #else:
605        #    msg = "Name exists already."
606
607    def compile_file(self, path):
608        """
609        Compile the file in the path
610        """
611        path = self.fname
612        show_model_output(self, path)
613
614    def delete_file(self, path):
615        """
616        Delete file in the path
617        """
618        _delete_file(path)
619
620
621class EditorPanel(wx.ScrolledWindow):
622    """
623    Simple Plugin Model function editor
624    """
625    def __init__(self, parent, base, path, title, *args, **kwds):
626        kwds['name'] = title
627#        kwds["size"] = (EDITOR_WIDTH, EDITOR_HEIGTH)
628        kwds["style"] = wx.FULL_REPAINT_ON_RESIZE
629        wx.ScrolledWindow.__init__(self, parent, *args, **kwds)
630        self.SetScrollbars(1,1,1,1)
631        self.parent = parent
632        self.base = base
633        self.path = path
634        self.font = wx.SystemSettings_GetFont(wx.SYS_SYSTEM_FONT)
635        self.font.SetPointSize(10)
636        self.reader = None
637        self.name = 'untitled'
638        self.overwrite_name = False
639        self.is_2d = False
640        self.fname = None
641        self.main_sizer = None
642        self.name_sizer = None
643        self.name_hsizer = None
644        self.name_tcl = None
645        self.overwrite_cb = None
646        self.desc_sizer = None
647        self.desc_tcl = None
648        self.param_sizer = None
649        self.param_tcl = None
650        self.function_sizer = None
651        self.func_horizon_sizer = None
652        self.button_sizer = None
653        self.param_strings = ''
654        self.function_strings = ''
655        self._notes = ""
656        self._msg_box = None
657        self.msg_sizer = None
658        self.warning = ""
659        #This does not seem to be used anywhere so commenting out for now
660        #    -- PDB 2/26/17
661        #self._description = "New Plugin Model"
662        self.function_tcl = None
663        self.math_combo = None
664        self.bt_apply = None
665        self.bt_close = None
666        #self._default_save_location = os.getcwd()
667        self._do_layout()
668
669
670
671    def _define_structure(self):
672        """
673        define initial sizer
674        """
675        #w, h = self.parent.GetSize()
676        self.main_sizer = wx.BoxSizer(wx.VERTICAL)
677        self.name_sizer = wx.BoxSizer(wx.VERTICAL)
678        self.name_hsizer = wx.BoxSizer(wx.HORIZONTAL)
679        self.desc_sizer = wx.BoxSizer(wx.VERTICAL)
680        self.param_sizer = wx.BoxSizer(wx.VERTICAL)
681        self.function_sizer = wx.BoxSizer(wx.VERTICAL)
682        self.func_horizon_sizer = wx.BoxSizer(wx.HORIZONTAL)
683        self.button_sizer = wx.BoxSizer(wx.HORIZONTAL)
684        self.msg_sizer = wx.BoxSizer(wx.HORIZONTAL)
685
686    def _layout_name(self):
687        """
688        Do the layout for file/function name related widgets
689        """
690        #title name [string]
691        name_txt = wx.StaticText(self, -1, 'Function Name : ')
692        self.overwrite_cb = wx.CheckBox(self, -1, "Overwrite existing plugin model of this name?", (10, 10))
693        self.overwrite_cb.SetValue(False)
694        self.overwrite_cb.SetToolTipString("Overwrite it if already exists?")
695        wx.EVT_CHECKBOX(self, self.overwrite_cb.GetId(), self.on_over_cb)
696        self.name_tcl = wx.TextCtrl(self, -1, size=(PANEL_WIDTH * 3 / 5, -1))
697        self.name_tcl.Bind(wx.EVT_TEXT_ENTER, self.on_change_name)
698        self.name_tcl.SetValue('')
699        self.name_tcl.SetFont(self.font)
700        hint_name = "Unique Model Function Name."
701        self.name_tcl.SetToolTipString(hint_name)
702        self.name_hsizer.AddMany([(self.name_tcl, 0, wx.LEFT | wx.TOP, 0),
703                                  (self.overwrite_cb, 0, wx.LEFT, 20)])
704        self.name_sizer.AddMany([(name_txt, 0, wx.LEFT | wx.TOP, 10),
705                                 (self.name_hsizer, 0,
706                                  wx.LEFT | wx.TOP | wx.BOTTOM, 10)])
707
708
709    def _layout_description(self):
710        """
711        Do the layout for description related widgets
712        """
713        #title name [string]
714        desc_txt = wx.StaticText(self, -1, 'Description (optional) : ')
715        self.desc_tcl = wx.TextCtrl(self, -1, size=(PANEL_WIDTH * 3 / 5, -1))
716        self.desc_tcl.SetValue('')
717        hint_desc = "Write a short description of the model function."
718        self.desc_tcl.SetToolTipString(hint_desc)
719        self.desc_sizer.AddMany([(desc_txt, 0, wx.LEFT | wx.TOP, 10),
720                                 (self.desc_tcl, 0,
721                                  wx.LEFT | wx.TOP | wx.BOTTOM, 10)])
722    def _layout_param(self):
723        """
724        Do the layout for parameter related widgets
725        """
726        param_txt = wx.StaticText(self, -1, 'Fit Parameters NOT requiring' + \
727                                  ' polydispersity (if any): ')
728
729        param_tip = "#Set the parameters NOT requiring polydispersity " + \
730        "and their initial values.\n"
731        param_tip += "#Example:\n"
732        param_tip += "A = 1\nB = 1"
733        #param_txt.SetToolTipString(param_tip)
734        newid = wx.NewId()
735        self.param_tcl = EditWindow(self, newid, wx.DefaultPosition,
736                                    wx.DefaultSize,
737                                    wx.CLIP_CHILDREN | wx.SUNKEN_BORDER)
738        self.param_tcl.setDisplayLineNumbers(True)
739        self.param_tcl.SetToolTipString(param_tip)
740
741        self.param_sizer.AddMany([(param_txt, 0, wx.LEFT, 10),
742                                  (self.param_tcl, 1, wx.EXPAND | wx.ALL, 10)])
743
744        # Parameters with polydispersity
745        pd_param_txt = wx.StaticText(self, -1, 'Fit Parameters requiring ' + \
746                                     'polydispersity (if any): ')
747
748        pd_param_tip = "#Set the parameters requiring polydispersity and " + \
749        "their initial values.\n"
750        pd_param_tip += "#Example:\n"
751        pd_param_tip += "C = 2\nD = 2"
752        newid = wx.NewId()
753        self.pd_param_tcl = EditWindow(self, newid, wx.DefaultPosition,
754                                    wx.DefaultSize,
755                                    wx.CLIP_CHILDREN | wx.SUNKEN_BORDER)
756        self.pd_param_tcl.setDisplayLineNumbers(True)
757        self.pd_param_tcl.SetToolTipString(pd_param_tip)
758
759        self.param_sizer.AddMany([(pd_param_txt, 0, wx.LEFT, 10),
760                                  (self.pd_param_tcl, 1, wx.EXPAND | wx.ALL, 10)])
761
762    def _layout_function(self):
763        """
764        Do the layout for function related widgets
765        """
766        function_txt = wx.StaticText(self, -1, 'Function(x) : ')
767        hint_function = "#Example:\n"
768        hint_function += "if x <= 0:\n"
769        hint_function += "    y = A + B\n"
770        hint_function += "else:\n"
771        hint_function += "    y = A + B * cos(2 * pi * x)\n"
772        hint_function += "return y\n"
773        math_txt = wx.StaticText(self, -1, '*Useful math functions: ')
774        math_combo = self._fill_math_combo()
775
776        newid = wx.NewId()
777        self.function_tcl = EditWindow(self, newid, wx.DefaultPosition,
778                                       wx.DefaultSize,
779                                       wx.CLIP_CHILDREN | wx.SUNKEN_BORDER)
780        self.function_tcl.setDisplayLineNumbers(True)
781        self.function_tcl.SetToolTipString(hint_function)
782
783        self.func_horizon_sizer.Add(function_txt)
784        self.func_horizon_sizer.Add(math_txt, 0, wx.LEFT, 250)
785        self.func_horizon_sizer.Add(math_combo, 0, wx.LEFT, 10)
786
787        self.function_sizer.Add(self.func_horizon_sizer, 0, wx.LEFT, 10)
788        self.function_sizer.Add(self.function_tcl, 1, wx.EXPAND | wx.ALL, 10)
789
790    def _layout_msg(self):
791        """
792        Layout msg
793        """
794        self._msg_box = wx.StaticText(self, -1, self._notes,
795                                      size=(PANEL_WIDTH, -1))
796        self.msg_sizer.Add(self._msg_box, 0, wx.LEFT, 10)
797
798    def _layout_button(self):
799        """
800        Do the layout for the button widgets
801        """
802        self.bt_apply = wx.Button(self, -1, "Apply", size=(_BOX_WIDTH, -1))
803        self.bt_apply.SetToolTipString("Save changes into the imported data.")
804        self.bt_apply.Bind(wx.EVT_BUTTON, self.on_click_apply)
805
806        self.bt_help = wx.Button(self, -1, "HELP", size=(_BOX_WIDTH, -1))
807        self.bt_help.SetToolTipString("Get Help For Model Editor")
808        self.bt_help.Bind(wx.EVT_BUTTON, self.on_help)
809
810        self.bt_close = wx.Button(self, -1, 'Close', size=(_BOX_WIDTH, -1))
811        self.bt_close.Bind(wx.EVT_BUTTON, self.on_close)
812        self.bt_close.SetToolTipString("Close this panel.")
813
814        self.button_sizer.AddMany([(self.bt_apply, 0,0),
815                                   (self.bt_help, 0, wx.LEFT | wx.BOTTOM,15),
816                                   (self.bt_close, 0, wx.LEFT | wx.RIGHT, 15)])
817
818    def _do_layout(self):
819        """
820        Draw the current panel
821        """
822        self._define_structure()
823        self._layout_name()
824        self._layout_description()
825        self._layout_param()
826        self._layout_function()
827        self._layout_msg()
828        self._layout_button()
829        self.main_sizer.AddMany([(self.name_sizer, 0, wx.EXPAND | wx.ALL, 5),
830                                 (wx.StaticLine(self), 0,
831                                  wx.ALL | wx.EXPAND, 5),
832                                 (self.desc_sizer, 0, wx.EXPAND | wx.ALL, 5),
833                                 (wx.StaticLine(self), 0,
834                                  wx.ALL | wx.EXPAND, 5),
835                                 (self.param_sizer, 1, wx.EXPAND | wx.ALL, 5),
836                                 (wx.StaticLine(self), 0,
837                                  wx.ALL | wx.EXPAND, 5),
838                                 (self.function_sizer, 2,
839                                  wx.EXPAND | wx.ALL, 5),
840                                 (wx.StaticLine(self), 0,
841                                  wx.ALL | wx.EXPAND, 5),
842                                 (self.msg_sizer, 0, wx.EXPAND | wx.ALL, 5),
843                                 (self.button_sizer, 0, wx.ALIGN_RIGHT)])
844        self.SetSizer(self.main_sizer)
845        self.SetAutoLayout(True)
846
847    def _fill_math_combo(self):
848        """
849        Fill up the math combo box
850        """
851        self.math_combo = wx.ComboBox(self, -1, size=(100, -1),
852                                      style=wx.CB_READONLY)
853        for item in dir(math):
854            if item.count("_") < 1:
855                try:
856                    exec "float(math.%s)" % item
857                    self.math_combo.Append(str(item))
858                except:
859                    self.math_combo.Append(str(item) + "()")
860        self.math_combo.Bind(wx.EVT_COMBOBOX, self._on_math_select)
861        self.math_combo.SetSelection(0)
862        return self.math_combo
863
864    def _on_math_select(self, event):
865        """
866        On math selection on ComboBox
867        """
868        event.Skip()
869        label = self.math_combo.GetValue()
870        self.function_tcl.SetFocus()
871        # Put the text at the cursor position
872        pos = self.function_tcl.GetCurrentPos()
873        self.function_tcl.InsertText(pos, label)
874        # Put the cursor at appropriate position
875        length = len(label)
876        print(length)
877        if label[length-1] == ')':
878            length -= 1
879        f_pos = pos + length
880        self.function_tcl.GotoPos(f_pos)
881
882    def get_notes(self):
883        """
884        return notes
885        """
886        return self._notes
887
888    def on_change_name(self, event=None):
889        """
890        Change name
891        """
892        if event is not None:
893            event.Skip()
894        self.name_tcl.SetBackgroundColour('white')
895        self.Refresh()
896
897    def check_name(self):
898        """
899        Check name if exist already
900        """
901        self._notes = ''
902        self.on_change_name(None)
903        plugin_dir = self.path
904        list_fnames = os.listdir(plugin_dir)
905        # function/file name
906        title = self.name_tcl.GetValue().lstrip().rstrip()
907        self.name = title
908        t_fname = title + '.py'
909        if not self.overwrite_name:
910            if t_fname in list_fnames:
911                self.name_tcl.SetBackgroundColour('pink')
912                return False
913        self.fname = os.path.join(plugin_dir, t_fname)
914        s_title = title
915        if len(title) > 20:
916            s_title = title[0:19] + '...'
917        self._notes += "Model function name is set "
918        self._notes += "to %s. \n" % str(s_title)
919        return True
920
921    def on_over_cb(self, event):
922        """
923        Set overwrite name flag on cb event
924        """
925        if event is not None:
926            event.Skip()
927        cb_value = event.GetEventObject()
928        self.overwrite_name = cb_value.GetValue()
929
930    def on_click_apply(self, event):
931        """
932        Changes are saved in data object imported to edit.
933
934        checks firs for valid name, then if it already exists then checks
935        that a function was entered and finally that if entered it contains at
936        least a return statement.  If all passes writes file then tries to
937        compile.  If compile fails or import module fails or run method fails
938        tries to remove any .py and pyc files that may have been created and
939        sets error message.
940
941        :todo this code still could do with a careful going over to clean
942        up and simplify. the non GUI methods such as this one should be removed
943        to computational code of SasView. Most of those computational methods
944        would be the same for both the simple editors.
945        """
946        #must post event here
947        event.Skip()
948        name = self.name_tcl.GetValue().lstrip().rstrip()
949        info = 'Info'
950        msg = ''
951        result, check_err = '', ''
952        # Sort out the errors if occur
953        # First check for valid python name then if the name already exists
954        if not name or not bool(re.match('^[A-Za-z0-9_]*$', name)):
955            msg = '"%s" '%name
956            msg += "is not a valid model name. Name must not be empty and \n"
957            msg += "may include only alpha numeric or underline characters \n"
958            msg += "and no spaces"
959        elif self.check_name():
960            description = self.desc_tcl.GetValue()
961            param_str = self.param_tcl.GetText()
962            pd_param_str = self.pd_param_tcl.GetText()
963            func_str = self.function_tcl.GetText()
964            # No input for the model function
965            if func_str.lstrip().rstrip():
966                if func_str.count('return') > 0:
967                    self.write_file(self.fname, name, description, param_str,
968                                    pd_param_str, func_str)
969                    try:
970                        result, msg = check_model(self.fname), None
971                    except Exception:
972                        import traceback
973                        result, msg = None, "error building model"
974                        check_err = "\n"+traceback.format_exc(limit=2)
975                else:
976                    msg = "Error: The func(x) must 'return' a value at least.\n"
977                    msg += "For example: \n\nreturn 2*x"
978            else:
979                msg = 'Error: Function is not defined.'
980        else:
981            msg = "Name exists already."
982
983        # Prepare the messagebox
984        if self.base is not None and not msg:
985            self.base.update_custom_combo()
986            # Passed exception in import test as it will fail for sasmodels.sasview_model class
987            # Should add similar test for new style?
988            Model = None
989            try:
990                exec "from %s import Model" % name
991            except:
992                logger.error(sys.exc_value)
993
994        # Prepare the messagebox
995        if msg:
996            info = 'Error'
997            color = 'red'
998            self.overwrite_cb.SetValue(True)
999            self.overwrite_name = True
1000        else:
1001            self._notes = result
1002            msg = "Successful! Please look for %s in Plugin Models."%name
1003            msg += "  " + self._notes
1004            info = 'Info'
1005            color = 'blue'
1006        self._msg_box.SetLabel(msg)
1007        self._msg_box.SetForegroundColour(color)
1008        # Send msg to the top window
1009        if self.base is not None:
1010            from sas.sasgui.guiframe.events import StatusEvent
1011            wx.PostEvent(self.base.parent,
1012                         StatusEvent(status=msg+check_err, info=info))
1013        self.warning = msg
1014
1015    def write_file(self, fname, name, desc_str, param_str, pd_param_str, func_str):
1016        """
1017        Write content in file
1018
1019        :param fname: full file path
1020        :param desc_str: content of the description strings
1021        :param param_str: content of params; Strings
1022        :param pd_param_str: content of params requiring polydispersity; Strings
1023        :param func_str: content of func; Strings
1024        """
1025        try:
1026            out_f = open(fname, 'w')
1027        except:
1028            raise
1029        # Prepare the content of the function
1030        lines = CUSTOM_TEMPLATE.split('\n')
1031
1032        has_scipy = func_str.count("scipy.")
1033        if has_scipy:
1034            lines.insert(0, 'import scipy')
1035
1036        # Think about 2D later
1037        #self.is_2d = func_str.count("#self.ndim = 2")
1038        #line_2d = ''
1039        #if self.is_2d:
1040        #    line_2d = CUSTOM_2D_TEMP.split('\n')
1041
1042        # Also think about test later
1043        #line_test = TEST_TEMPLATE.split('\n')
1044        #local_params = ''
1045        #spaces = '        '#8spaces
1046        spaces4  = ' '*4
1047        spaces13 = ' '*13
1048        spaces16 = ' '*16
1049        param_names = []    # to store parameter names
1050        has_scipy = func_str.count("scipy.")
1051        if has_scipy:
1052            lines.insert(0, 'import scipy')
1053
1054        # write function here
1055        for line in lines:
1056            # The location where to put the strings is
1057            # hard-coded in the template as shown below.
1058            out_f.write(line + '\n')
1059            if line.count('#name'):
1060                out_f.write('name = "%s" \n' % name)
1061            elif line.count('#title'):
1062                out_f.write('title = "User model for %s"\n' % name)
1063            elif line.count('#description'):
1064                out_f.write('description = "%s"\n' % desc_str)
1065            elif line.count('#parameters'):
1066                out_f.write('parameters = [ \n')
1067                for param_line in param_str.split('\n'):
1068                    p_line = param_line.lstrip().rstrip()
1069                    if p_line:
1070                        pname, pvalue, desc = self.get_param_helper(p_line)
1071                        param_names.append(pname)
1072                        out_f.write("%s['%s', '', %s, [-numpy.inf, numpy.inf], '', '%s'],\n" % (spaces16, pname, pvalue, desc))
1073                for param_line in pd_param_str.split('\n'):
1074                    p_line = param_line.lstrip().rstrip()
1075                    if p_line:
1076                        pname, pvalue, desc = self.get_param_helper(p_line)
1077                        param_names.append(pname)
1078                        out_f.write("%s['%s', '', %s, [-numpy.inf, numpy.inf], 'volume', '%s'],\n" % (spaces16, pname, pvalue, desc))
1079                out_f.write('%s]\n' % spaces13)
1080
1081        # No form_volume or ER available in simple model editor
1082        out_f.write('def form_volume(*arg): \n')
1083        out_f.write('    return 1.0 \n')
1084        out_f.write('\n')
1085        out_f.write('def ER(*arg): \n')
1086        out_f.write('    return 1.0 \n')
1087
1088        # function to compute
1089        out_f.write('\n')
1090        out_f.write('def Iq(x ')
1091        for name in param_names:
1092            out_f.write(', %s' % name)
1093        out_f.write('):\n')
1094        for func_line in func_str.split('\n'):
1095            out_f.write('%s%s\n' % (spaces4, func_line))
1096
1097        Iqxy_string = 'return Iq(numpy.sqrt(x**2+y**2) '
1098
1099        out_f.write('\n')
1100        out_f.write('def Iqxy(x, y ')
1101        for name in param_names:
1102            out_f.write(', %s' % name)
1103            Iqxy_string += ', ' + name
1104        out_f.write('):\n')
1105        Iqxy_string += ')'
1106        out_f.write('%s%s\n' % (spaces4, Iqxy_string))
1107
1108        out_f.close()
1109
1110    def get_param_helper(self, line):
1111        """
1112        Get string in line to define the params dictionary
1113
1114        :param line: one line of string got from the param_str
1115        """
1116        items = line.split(";")
1117        for item in items:
1118            name = item.split("=")[0].strip()
1119            description = ""
1120            try:
1121                value = item.split("=")[1].strip()
1122                if value.count("#"):
1123                    # If line ends in a comment, remove it before parsing float
1124                    index = value.index("#")
1125                    description = value[(index + 1):].strip()
1126                    value = value[:value.index("#")].strip()
1127                float(value)
1128            except ValueError:
1129                value = 1.0 # default
1130
1131        return name, value, description
1132
1133    def set_function_helper(self, line):
1134        """
1135        Get string in line to define the local params
1136
1137        :param line: one line of string got from the param_str
1138        """
1139        params_str = ''
1140        spaces = '        '#8spaces
1141        items = line.split(";")
1142        for item in items:
1143            name = item.split("=")[0].lstrip().rstrip()
1144            params_str += spaces + "%s = self.params['%s']\n" % (name, name)
1145        return params_str
1146
1147    def get_warning(self):
1148        """
1149        Get the warning msg
1150        """
1151        return self.warning
1152
1153    def on_help(self, event):
1154        """
1155        Bring up the New Plugin Model Editor Documentation whenever
1156        the HELP button is clicked.
1157
1158        Calls DocumentationWindow with the path of the location within the
1159        documentation tree (after /doc/ ....".  Note that when using old
1160        versions of Wx (before 2.9) and thus not the release version of
1161        installers, the help comes up at the top level of the file as
1162        webbrowser does not pass anything past the # to the browser when it is
1163        running "file:///...."
1164
1165    :param evt: Triggers on clicking the help button
1166    """
1167
1168        _TreeLocation = "user/sasgui/perspectives/fitting/fitting_help.html"
1169        _PageAnchor = "#new-plugin-model"
1170        _doc_viewer = DocumentationWindow(self, -1, _TreeLocation, _PageAnchor,
1171                                          "Plugin Model Editor Help")
1172
1173    def on_close(self, event):
1174        """
1175        leave data as it is and close
1176        """
1177        self.parent.Show(False)#Close()
1178        event.Skip()
1179
1180class EditorWindow(wx.Frame):
1181    """
1182    Editor Window
1183    """
1184    def __init__(self, parent, base, path, title,
1185                 size=(EDITOR_WIDTH, EDITOR_HEIGTH), *args, **kwds):
1186        """
1187        Init
1188        """
1189        kwds["title"] = title
1190        kwds["size"] = size
1191        wx.Frame.__init__(self, parent=None, *args, **kwds)
1192        self.parent = parent
1193        self.panel = EditorPanel(parent=self, base=parent,
1194                                 path=path, title=title)
1195        self.Show(True)
1196        wx.EVT_CLOSE(self, self.on_close)
1197
1198    def on_close(self, event):
1199        """
1200        On close event
1201        """
1202        self.Show(False)
1203        #if self.parent is not None:
1204        #    self.parent.new_model_frame = None
1205        #self.Destroy()
1206
1207## Templates for plugin models
1208
1209CUSTOM_TEMPLATE = """
1210from math import *
1211import os
1212import sys
1213import numpy
1214
1215#name
1216
1217#title
1218
1219#description
1220
1221#parameters
1222
1223"""
1224
1225CUSTOM_2D_TEMP = """
1226    def run(self, x=0.0, y=0.0):
1227        if x.__class__.__name__ == 'list':
1228            x_val = x[0]
1229            y_val = y[0]*0.0
1230            return self.function(x_val, y_val)
1231        elif x.__class__.__name__ == 'tuple':
1232            msg = "Tuples are not allowed as input to BaseComponent models"
1233            raise ValueError, msg
1234        else:
1235            return self.function(x, 0.0)
1236    def runXY(self, x=0.0, y=0.0):
1237        if x.__class__.__name__ == 'list':
1238            return self.function(x, y)
1239        elif x.__class__.__name__ == 'tuple':
1240            msg = "Tuples are not allowed as input to BaseComponent models"
1241            raise ValueError, msg
1242        else:
1243            return self.function(x, y)
1244    def evalDistribution(self, qdist):
1245        if qdist.__class__.__name__ == 'list':
1246            msg = "evalDistribution expects a list of 2 ndarrays"
1247            if len(qdist)!=2:
1248                raise RuntimeError, msg
1249            if qdist[0].__class__.__name__ != 'ndarray':
1250                raise RuntimeError, msg
1251            if qdist[1].__class__.__name__ != 'ndarray':
1252                raise RuntimeError, msg
1253            v_model = numpy.vectorize(self.runXY, otypes=[float])
1254            iq_array = v_model(qdist[0], qdist[1])
1255            return iq_array
1256        elif qdist.__class__.__name__ == 'ndarray':
1257            v_model = numpy.vectorize(self.runXY, otypes=[float])
1258            iq_array = v_model(qdist)
1259            return iq_array
1260"""
1261TEST_TEMPLATE = """
1262######################################################################
1263## THIS IS FOR TEST. DO NOT MODIFY THE FOLLOWING LINES!!!!!!!!!!!!!!!!
1264if __name__ == "__main__":
1265    m= Model()
1266    out1 = m.runXY(0.0)
1267    out2 = m.runXY(0.01)
1268    isfine1 = numpy.isfinite(out1)
1269    isfine2 = numpy.isfinite(out2)
1270    print "Testing the value at Q = 0.0:"
1271    print out1, " : finite? ", isfine1
1272    print "Testing the value at Q = 0.01:"
1273    print out2, " : finite? ", isfine2
1274    if isfine1 and isfine2:
1275        print "===> Simple Test: Passed!"
1276    else:
1277        print "===> Simple Test: Failed!"
1278"""
1279SUM_TEMPLATE = """
1280# A sample of an experimental model function for Sum/Multiply(Pmodel1,Pmodel2)
1281import os
1282import sys
1283import copy
1284import collections
1285
1286import numpy
1287
1288from sas.sascalc.fit.pluginmodel import Model1DPlugin
1289from sasmodels.sasview_model import find_model
1290
1291class Model(Model1DPlugin):
1292    name = os.path.splitext(os.path.basename(__file__))[0]
1293    is_multiplicity_model = False
1294    def __init__(self, multiplicity=1):
1295        Model1DPlugin.__init__(self, name='')
1296        P1 = find_model('%s')
1297        P2 = find_model('%s')
1298        p_model1 = P1()
1299        p_model2 = P2()
1300        ## Setting  model name model description
1301        self.description = '%s'
1302        if self.name.rstrip().lstrip() == '':
1303            self.name = self._get_name(p_model1.name, p_model2.name)
1304        if self.description.rstrip().lstrip() == '':
1305            self.description = p_model1.name
1306            self.description += p_model2.name
1307            self.fill_description(p_model1, p_model2)
1308
1309        ## Define parameters
1310        self.params = collections.OrderedDict()
1311
1312        ## Parameter details [units, min, max]
1313        self.details = {}
1314        ## Magnetic Panrameters
1315        self.magnetic_params = []
1316        # non-fittable parameters
1317        self.non_fittable = p_model1.non_fittable
1318        self.non_fittable += p_model2.non_fittable
1319
1320        ##models
1321        self.p_model1= p_model1
1322        self.p_model2= p_model2
1323
1324
1325        ## dispersion
1326        self._set_dispersion()
1327        ## Define parameters
1328        self._set_params()
1329        ## New parameter:scaling_factor
1330        self.params['scale_factor'] = %s
1331
1332        ## Parameter details [units, min, max]
1333        self._set_details()
1334        self.details['scale_factor'] = ['', 0.0, numpy.inf]
1335
1336
1337        #list of parameter that can be fitted
1338        self._set_fixed_params()
1339
1340        ## parameters with orientation
1341        self.orientation_params = []
1342        for item in self.p_model1.orientation_params:
1343            new_item = "p1_" + item
1344            if not new_item in self.orientation_params:
1345                self.orientation_params.append(new_item)
1346
1347        for item in self.p_model2.orientation_params:
1348            new_item = "p2_" + item
1349            if not new_item in self.orientation_params:
1350                self.orientation_params.append(new_item)
1351        ## magnetic params
1352        self.magnetic_params = []
1353        for item in self.p_model1.magnetic_params:
1354            new_item = "p1_" + item
1355            if not new_item in self.magnetic_params:
1356                self.magnetic_params.append(new_item)
1357
1358        for item in self.p_model2.magnetic_params:
1359            new_item = "p2_" + item
1360            if not new_item in self.magnetic_params:
1361                self.magnetic_params.append(new_item)
1362        # get multiplicity if model provide it, else 1.
1363        try:
1364            multiplicity1 = p_model1.multiplicity
1365            try:
1366                multiplicity2 = p_model2.multiplicity
1367            except:
1368                multiplicity2 = 1
1369        except:
1370            multiplicity1 = 1
1371            multiplicity2 = 1
1372        ## functional multiplicity of the model
1373        self.multiplicity1 = multiplicity1
1374        self.multiplicity2 = multiplicity2
1375        self.multiplicity_info = []
1376
1377    def _clone(self, obj):
1378        import copy
1379        obj.params     = copy.deepcopy(self.params)
1380        obj.description     = copy.deepcopy(self.description)
1381        obj.details    = copy.deepcopy(self.details)
1382        obj.dispersion = copy.deepcopy(self.dispersion)
1383        obj.p_model1  = self.p_model1.clone()
1384        obj.p_model2  = self.p_model2.clone()
1385        #obj = copy.deepcopy(self)
1386        return obj
1387
1388    def _get_name(self, name1, name2):
1389        p1_name = self._get_upper_name(name1)
1390        if not p1_name:
1391            p1_name = name1
1392        name = p1_name
1393        name += "_and_"
1394        p2_name = self._get_upper_name(name2)
1395        if not p2_name:
1396            p2_name = name2
1397        name += p2_name
1398        return name
1399
1400    def _get_upper_name(self, name=None):
1401        if name is None:
1402            return ""
1403        upper_name = ""
1404        str_name = str(name)
1405        for index in range(len(str_name)):
1406            if str_name[index].isupper():
1407                upper_name += str_name[index]
1408        return upper_name
1409
1410    def _set_dispersion(self):
1411        self.dispersion = collections.OrderedDict()
1412        ##set dispersion only from p_model
1413        for name , value in self.p_model1.dispersion.iteritems():
1414            #if name.lower() not in self.p_model1.orientation_params:
1415            new_name = "p1_" + name
1416            self.dispersion[new_name]= value
1417        for name , value in self.p_model2.dispersion.iteritems():
1418            #if name.lower() not in self.p_model2.orientation_params:
1419            new_name = "p2_" + name
1420            self.dispersion[new_name]= value
1421
1422    def function(self, x=0.0):
1423        return 0
1424
1425    def getProfile(self):
1426        try:
1427            x,y = self.p_model1.getProfile()
1428        except:
1429            x = None
1430            y = None
1431
1432        return x, y
1433
1434    def _set_params(self):
1435        for name , value in self.p_model1.params.iteritems():
1436            # No 2D-supported
1437            #if name not in self.p_model1.orientation_params:
1438            new_name = "p1_" + name
1439            self.params[new_name]= value
1440
1441        for name , value in self.p_model2.params.iteritems():
1442            # No 2D-supported
1443            #if name not in self.p_model2.orientation_params:
1444            new_name = "p2_" + name
1445            self.params[new_name]= value
1446
1447        # Set "scale" as initializing
1448        self._set_scale_factor()
1449
1450
1451    def _set_details(self):
1452        for name ,detail in self.p_model1.details.iteritems():
1453            new_name = "p1_" + name
1454            #if new_name not in self.orientation_params:
1455            self.details[new_name]= detail
1456
1457        for name ,detail in self.p_model2.details.iteritems():
1458            new_name = "p2_" + name
1459            #if new_name not in self.orientation_params:
1460            self.details[new_name]= detail
1461
1462    def _set_scale_factor(self):
1463        pass
1464
1465
1466    def setParam(self, name, value):
1467        # set param to this (p1, p2) model
1468        self._setParamHelper(name, value)
1469
1470        ## setParam to p model
1471        model_pre = ''
1472        new_name = ''
1473        name_split = name.split('_', 1)
1474        if len(name_split) == 2:
1475            model_pre = name.split('_', 1)[0]
1476            new_name = name.split('_', 1)[1]
1477        if model_pre == "p1":
1478            if new_name in self.p_model1.getParamList():
1479                self.p_model1.setParam(new_name, value)
1480        elif model_pre == "p2":
1481             if new_name in self.p_model2.getParamList():
1482                self.p_model2.setParam(new_name, value)
1483        elif name == 'scale_factor':
1484            self.params['scale_factor'] = value
1485        else:
1486            raise ValueError, "Model does not contain parameter %s" % name
1487
1488    def getParam(self, name):
1489        # Look for dispersion parameters
1490        toks = name.split('.')
1491        if len(toks)==2:
1492            for item in self.dispersion.keys():
1493                # 2D not supported
1494                if item.lower()==toks[0].lower():
1495                    for par in self.dispersion[item]:
1496                        if par.lower() == toks[1].lower():
1497                            return self.dispersion[item][par]
1498        else:
1499            # Look for standard parameter
1500            for item in self.params.keys():
1501                if item.lower()==name.lower():
1502                    return self.params[item]
1503        return
1504        #raise ValueError, "Model does not contain parameter %s" % name
1505
1506    def _setParamHelper(self, name, value):
1507        # Look for dispersion parameters
1508        toks = name.split('.')
1509        if len(toks)== 2:
1510            for item in self.dispersion.keys():
1511                if item.lower()== toks[0].lower():
1512                    for par in self.dispersion[item]:
1513                        if par.lower() == toks[1].lower():
1514                            self.dispersion[item][par] = value
1515                            return
1516        else:
1517            # Look for standard parameter
1518            for item in self.params.keys():
1519                if item.lower()== name.lower():
1520                    self.params[item] = value
1521                    return
1522
1523        raise ValueError, "Model does not contain parameter %s" % name
1524
1525
1526    def _set_fixed_params(self):
1527        self.fixed = []
1528        for item in self.p_model1.fixed:
1529            new_item = "p1" + item
1530            self.fixed.append(new_item)
1531        for item in self.p_model2.fixed:
1532            new_item = "p2" + item
1533            self.fixed.append(new_item)
1534
1535        self.fixed.sort()
1536
1537
1538    def run(self, x = 0.0):
1539        self._set_scale_factor()
1540        return self.params['scale_factor'] %s \
1541(self.p_model1.run(x) %s self.p_model2.run(x))
1542
1543    def runXY(self, x = 0.0):
1544        self._set_scale_factor()
1545        return self.params['scale_factor'] %s \
1546(self.p_model1.runXY(x) %s self.p_model2.runXY(x))
1547
1548    ## Now (May27,10) directly uses the model eval function
1549    ## instead of the for-loop in Base Component.
1550    def evalDistribution(self, x = []):
1551        self._set_scale_factor()
1552        return self.params['scale_factor'] %s \
1553(self.p_model1.evalDistribution(x) %s \
1554self.p_model2.evalDistribution(x))
1555
1556    def set_dispersion(self, parameter, dispersion):
1557        value= None
1558        new_pre = parameter.split("_", 1)[0]
1559        new_parameter = parameter.split("_", 1)[1]
1560        try:
1561            if new_pre == 'p1' and \
1562new_parameter in self.p_model1.dispersion.keys():
1563                value= self.p_model1.set_dispersion(new_parameter, dispersion)
1564            if new_pre == 'p2' and \
1565new_parameter in self.p_model2.dispersion.keys():
1566                value= self.p_model2.set_dispersion(new_parameter, dispersion)
1567            self._set_dispersion()
1568            return value
1569        except:
1570            raise
1571
1572    def fill_description(self, p_model1, p_model2):
1573        description = ""
1574        description += "This model gives the summation or multiplication of"
1575        description += "%s and %s. "% ( p_model1.name, p_model2.name )
1576        self.description += description
1577
1578if __name__ == "__main__":
1579    m1= Model()
1580    #m1.setParam("p1_scale", 25)
1581    #m1.setParam("p1_length", 1000)
1582    #m1.setParam("p2_scale", 100)
1583    #m1.setParam("p2_rg", 100)
1584    out1 = m1.runXY(0.01)
1585
1586    m2= Model()
1587    #m2.p_model1.setParam("scale", 25)
1588    #m2.p_model1.setParam("length", 1000)
1589    #m2.p_model2.setParam("scale", 100)
1590    #m2.p_model2.setParam("rg", 100)
1591    out2 = m2.p_model1.runXY(0.01) %s m2.p_model2.runXY(0.01)\n
1592    print "My name is %s."% m1.name
1593    print out1, " = ", out2
1594    if out1 == out2:
1595        print "===> Simple Test: Passed!"
1596    else:
1597        print "===> Simple Test: Failed!"
1598"""
1599
1600if __name__ == "__main__":
1601#    app = wx.PySimpleApp()
1602    main_app = wx.App()
1603    main_frame = TextDialog(id=1, model_list=["SphereModel", "CylinderModel"],
1604                       plugin_dir='../fitting/plugin_models')
1605    main_frame.ShowModal()
1606    main_app.MainLoop()
1607
1608#if __name__ == "__main__":
1609#    from sas.sasgui.perspectives.fitting import models
1610#    dir_path = models.find_plugins_dir()
1611#    app = wx.App()
1612#    window = EditorWindow(parent=None, base=None, path=dir_path, title="Editor")
1613#    app.MainLoop()
Note: See TracBrowser for help on using the repository browser.