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

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 40add21 was 40add21, checked in by lewis, 5 years ago

Use comment as fitting parameter description

  • Property mode set to 100644
File size: 58.3 KB
Line 
1'''
2This module provides three model editor classes: the composite model editor,
3the easy editor which provides a simple interface with tooltip help to enter
4the parameters of the model and their default value and a panel to input a
5function of y (usually the intensity).  It also provides a drop down of
6standard available math functions.  Finally a full python editor panel for
7complete customization is provided.
8
9:TODO the writing of the file and name checking (and maybe some other
10functions?) should be moved to a computational module which could be called
11from a python script.  Basically one just needs to pass the name,
12description text and function text (or in the case of the composite editor
13the names of the first and second model and the operator to be used).
14'''
15
16################################################################################
17#This software was developed by the University of Tennessee as part of the
18#Distributed Data Analysis of Neutron Scattering Experiments (DANSE)
19#project funded by the US National Science Foundation.
20#
21#See the license text in license.txt
22#
23#copyright 2009, University of Tennessee
24################################################################################
25from __future__ import print_function
26
27import wx
28import sys
29import os
30import math
31import re
32import logging
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.desc_sizer = None
646        self.desc_tcl = None
647        self.param_sizer = None
648        self.param_tcl = None
649        self.function_sizer = None
650        self.func_horizon_sizer = None
651        self.button_sizer = None
652        self.param_strings = ''
653        self.function_strings = ''
654        self._notes = ""
655        self._msg_box = None
656        self.msg_sizer = None
657        self.warning = ""
658        #This does not seem to be used anywhere so commenting out for now
659        #    -- PDB 2/26/17
660        #self._description = "New Plugin Model"
661        self.function_tcl = None
662        self.math_combo = None
663        self.bt_apply = None
664        self.bt_close = None
665        #self._default_save_location = os.getcwd()
666        self._do_layout()
667
668
669
670    def _define_structure(self):
671        """
672        define initial sizer
673        """
674        #w, h = self.parent.GetSize()
675        self.main_sizer = wx.BoxSizer(wx.VERTICAL)
676        self.name_sizer = wx.BoxSizer(wx.VERTICAL)
677        self.name_hsizer = wx.BoxSizer(wx.HORIZONTAL)
678        self.desc_sizer = wx.BoxSizer(wx.VERTICAL)
679        self.param_sizer = wx.BoxSizer(wx.VERTICAL)
680        self.function_sizer = wx.BoxSizer(wx.VERTICAL)
681        self.func_horizon_sizer = wx.BoxSizer(wx.HORIZONTAL)
682        self.button_sizer = wx.BoxSizer(wx.HORIZONTAL)
683        self.msg_sizer = wx.BoxSizer(wx.HORIZONTAL)
684
685    def _layout_name(self):
686        """
687        Do the layout for file/function name related widgets
688        """
689        #title name [string]
690        name_txt = wx.StaticText(self, -1, 'Function Name : ')
691        overwrite_cb = wx.CheckBox(self, -1, "Overwrite existing plugin model of this name?", (10, 10))
692        overwrite_cb.SetValue(False)
693        overwrite_cb.SetToolTipString("Overwrite it if already exists?")
694        wx.EVT_CHECKBOX(self, overwrite_cb.GetId(), self.on_over_cb)
695        self.name_tcl = wx.TextCtrl(self, -1, size=(PANEL_WIDTH * 3 / 5, -1))
696        self.name_tcl.Bind(wx.EVT_TEXT_ENTER, self.on_change_name)
697        self.name_tcl.SetValue('')
698        self.name_tcl.SetFont(self.font)
699        hint_name = "Unique Model Function Name."
700        self.name_tcl.SetToolTipString(hint_name)
701        self.name_hsizer.AddMany([(self.name_tcl, 0, wx.LEFT | wx.TOP, 0),
702                                  (overwrite_cb, 0, wx.LEFT, 20)])
703        self.name_sizer.AddMany([(name_txt, 0, wx.LEFT | wx.TOP, 10),
704                                 (self.name_hsizer, 0,
705                                  wx.LEFT | wx.TOP | wx.BOTTOM, 10)])
706
707
708    def _layout_description(self):
709        """
710        Do the layout for description related widgets
711        """
712        #title name [string]
713        desc_txt = wx.StaticText(self, -1, 'Description (optional) : ')
714        self.desc_tcl = wx.TextCtrl(self, -1, size=(PANEL_WIDTH * 3 / 5, -1))
715        self.desc_tcl.SetValue('')
716        hint_desc = "Write a short description of the model function."
717        self.desc_tcl.SetToolTipString(hint_desc)
718        self.desc_sizer.AddMany([(desc_txt, 0, wx.LEFT | wx.TOP, 10),
719                                 (self.desc_tcl, 0,
720                                  wx.LEFT | wx.TOP | wx.BOTTOM, 10)])
721    def _layout_param(self):
722        """
723        Do the layout for parameter related widgets
724        """
725        param_txt = wx.StaticText(self, -1, 'Fit Parameters NOT requiring' + \
726                                  ' polydispersity (if any): ')
727
728        param_tip = "#Set the parameters NOT requiring polydispersity " + \
729        "and their initial values.\n"
730        param_tip += "#Example:\n"
731        param_tip += "A = 1\nB = 1"
732        #param_txt.SetToolTipString(param_tip)
733        newid = wx.NewId()
734        self.param_tcl = EditWindow(self, newid, wx.DefaultPosition,
735                                    wx.DefaultSize,
736                                    wx.CLIP_CHILDREN | wx.SUNKEN_BORDER)
737        self.param_tcl.setDisplayLineNumbers(True)
738        self.param_tcl.SetToolTipString(param_tip)
739
740        self.param_sizer.AddMany([(param_txt, 0, wx.LEFT, 10),
741                                  (self.param_tcl, 1, wx.EXPAND | wx.ALL, 10)])
742
743        # Parameters with polydispersity
744        pd_param_txt = wx.StaticText(self, -1, 'Fit Parameters requiring ' + \
745                                     'polydispersity (if any): ')
746
747        pd_param_tip = "#Set the parameters requiring polydispersity and " + \
748        "their initial values.\n"
749        pd_param_tip += "#Example:\n"
750        pd_param_tip += "C = 2\nD = 2"
751        newid = wx.NewId()
752        self.pd_param_tcl = EditWindow(self, newid, wx.DefaultPosition,
753                                    wx.DefaultSize,
754                                    wx.CLIP_CHILDREN | wx.SUNKEN_BORDER)
755        self.pd_param_tcl.setDisplayLineNumbers(True)
756        self.pd_param_tcl.SetToolTipString(pd_param_tip)
757
758        self.param_sizer.AddMany([(pd_param_txt, 0, wx.LEFT, 10),
759                                  (self.pd_param_tcl, 1, wx.EXPAND | wx.ALL, 10)])
760
761    def _layout_function(self):
762        """
763        Do the layout for function related widgets
764        """
765        function_txt = wx.StaticText(self, -1, 'Function(x) : ')
766        hint_function = "#Example:\n"
767        hint_function += "if x <= 0:\n"
768        hint_function += "    y = A + B\n"
769        hint_function += "else:\n"
770        hint_function += "    y = A + B * cos(2 * pi * x)\n"
771        hint_function += "return y\n"
772        math_txt = wx.StaticText(self, -1, '*Useful math functions: ')
773        math_combo = self._fill_math_combo()
774
775        newid = wx.NewId()
776        self.function_tcl = EditWindow(self, newid, wx.DefaultPosition,
777                                       wx.DefaultSize,
778                                       wx.CLIP_CHILDREN | wx.SUNKEN_BORDER)
779        self.function_tcl.setDisplayLineNumbers(True)
780        self.function_tcl.SetToolTipString(hint_function)
781
782        self.func_horizon_sizer.Add(function_txt)
783        self.func_horizon_sizer.Add(math_txt, 0, wx.LEFT, 250)
784        self.func_horizon_sizer.Add(math_combo, 0, wx.LEFT, 10)
785
786        self.function_sizer.Add(self.func_horizon_sizer, 0, wx.LEFT, 10)
787        self.function_sizer.Add(self.function_tcl, 1, wx.EXPAND | wx.ALL, 10)
788
789    def _layout_msg(self):
790        """
791        Layout msg
792        """
793        self._msg_box = wx.StaticText(self, -1, self._notes,
794                                      size=(PANEL_WIDTH, -1))
795        self.msg_sizer.Add(self._msg_box, 0, wx.LEFT, 10)
796
797    def _layout_button(self):
798        """
799        Do the layout for the button widgets
800        """
801        self.bt_apply = wx.Button(self, -1, "Apply", size=(_BOX_WIDTH, -1))
802        self.bt_apply.SetToolTipString("Save changes into the imported data.")
803        self.bt_apply.Bind(wx.EVT_BUTTON, self.on_click_apply)
804
805        self.bt_help = wx.Button(self, -1, "HELP", size=(_BOX_WIDTH, -1))
806        self.bt_help.SetToolTipString("Get Help For Model Editor")
807        self.bt_help.Bind(wx.EVT_BUTTON, self.on_help)
808
809        self.bt_close = wx.Button(self, -1, 'Close', size=(_BOX_WIDTH, -1))
810        self.bt_close.Bind(wx.EVT_BUTTON, self.on_close)
811        self.bt_close.SetToolTipString("Close this panel.")
812
813        self.button_sizer.AddMany([(self.bt_apply, 0,0),
814                                   (self.bt_help, 0, wx.LEFT | wx.BOTTOM,15),
815                                   (self.bt_close, 0, wx.LEFT | wx.RIGHT, 15)])
816
817    def _do_layout(self):
818        """
819        Draw the current panel
820        """
821        self._define_structure()
822        self._layout_name()
823        self._layout_description()
824        self._layout_param()
825        self._layout_function()
826        self._layout_msg()
827        self._layout_button()
828        self.main_sizer.AddMany([(self.name_sizer, 0, wx.EXPAND | wx.ALL, 5),
829                                 (wx.StaticLine(self), 0,
830                                  wx.ALL | wx.EXPAND, 5),
831                                 (self.desc_sizer, 0, wx.EXPAND | wx.ALL, 5),
832                                 (wx.StaticLine(self), 0,
833                                  wx.ALL | wx.EXPAND, 5),
834                                 (self.param_sizer, 1, wx.EXPAND | wx.ALL, 5),
835                                 (wx.StaticLine(self), 0,
836                                  wx.ALL | wx.EXPAND, 5),
837                                 (self.function_sizer, 2,
838                                  wx.EXPAND | wx.ALL, 5),
839                                 (wx.StaticLine(self), 0,
840                                  wx.ALL | wx.EXPAND, 5),
841                                 (self.msg_sizer, 0, wx.EXPAND | wx.ALL, 5),
842                                 (self.button_sizer, 0, wx.ALIGN_RIGHT)])
843        self.SetSizer(self.main_sizer)
844        self.SetAutoLayout(True)
845
846    def _fill_math_combo(self):
847        """
848        Fill up the math combo box
849        """
850        self.math_combo = wx.ComboBox(self, -1, size=(100, -1),
851                                      style=wx.CB_READONLY)
852        for item in dir(math):
853            if item.count("_") < 1:
854                try:
855                    exec "float(math.%s)" % item
856                    self.math_combo.Append(str(item))
857                except:
858                    self.math_combo.Append(str(item) + "()")
859        self.math_combo.Bind(wx.EVT_COMBOBOX, self._on_math_select)
860        self.math_combo.SetSelection(0)
861        return self.math_combo
862
863    def _on_math_select(self, event):
864        """
865        On math selection on ComboBox
866        """
867        event.Skip()
868        label = self.math_combo.GetValue()
869        self.function_tcl.SetFocus()
870        # Put the text at the cursor position
871        pos = self.function_tcl.GetCurrentPos()
872        self.function_tcl.InsertText(pos, label)
873        # Put the cursor at appropriate position
874        length = len(label)
875        print(length)
876        if label[length-1] == ')':
877            length -= 1
878        f_pos = pos + length
879        self.function_tcl.GotoPos(f_pos)
880
881    def get_notes(self):
882        """
883        return notes
884        """
885        return self._notes
886
887    def on_change_name(self, event=None):
888        """
889        Change name
890        """
891        if event is not None:
892            event.Skip()
893        self.name_tcl.SetBackgroundColour('white')
894        self.Refresh()
895
896    def check_name(self):
897        """
898        Check name if exist already
899        """
900        self._notes = ''
901        self.on_change_name(None)
902        plugin_dir = self.path
903        list_fnames = os.listdir(plugin_dir)
904        # function/file name
905        title = self.name_tcl.GetValue().lstrip().rstrip()
906        self.name = title
907        t_fname = title + '.py'
908        if not self.overwrite_name:
909            if t_fname in list_fnames:
910                self.name_tcl.SetBackgroundColour('pink')
911                return False
912        self.fname = os.path.join(plugin_dir, t_fname)
913        s_title = title
914        if len(title) > 20:
915            s_title = title[0:19] + '...'
916        self._notes += "Model function name is set "
917        self._notes += "to %s. \n" % str(s_title)
918        return True
919
920    def on_over_cb(self, event):
921        """
922        Set overwrite name flag on cb event
923        """
924        if event is not None:
925            event.Skip()
926        cb_value = event.GetEventObject()
927        self.overwrite_name = cb_value.GetValue()
928
929    def on_click_apply(self, event):
930        """
931        Changes are saved in data object imported to edit.
932
933        checks firs for valid name, then if it already exists then checks
934        that a function was entered and finally that if entered it contains at
935        least a return statement.  If all passes writes file then tries to
936        compile.  If compile fails or import module fails or run method fails
937        tries to remove any .py and pyc files that may have been created and
938        sets error message.
939
940        :todo this code still could do with a careful going over to clean
941        up and simplify. the non GUI methods such as this one should be removed
942        to computational code of SasView. Most of those computational methods
943        would be the same for both the simple editors.
944        """
945        #must post event here
946        event.Skip()
947        name = self.name_tcl.GetValue().lstrip().rstrip()
948        info = 'Info'
949        msg = ''
950        result, check_err = '', ''
951        # Sort out the errors if occur
952        # First check for valid python name then if the name already exists
953        if not name or not bool(re.match('^[A-Za-z0-9_]*$', name)):
954            msg = '"%s" '%name
955            msg += "is not a valid model name. Name must not be empty and \n"
956            msg += "may include only alpha numeric or underline characters \n"
957            msg += "and no spaces"
958        elif self.check_name():
959            description = self.desc_tcl.GetValue()
960            param_str = self.param_tcl.GetText()
961            pd_param_str = self.pd_param_tcl.GetText()
962            func_str = self.function_tcl.GetText()
963            # No input for the model function
964            if func_str.lstrip().rstrip():
965                if func_str.count('return') > 0:
966                    self.write_file(self.fname, name, description, param_str,
967                                    pd_param_str, func_str)
968                    try:
969                        result, msg = check_model(self.fname), None
970                    except Exception:
971                        import traceback
972                        result, msg = None, "error building model"
973                        check_err = "\n"+traceback.format_exc(limit=2)
974                else:
975                    msg = "Error: The func(x) must 'return' a value at least.\n"
976                    msg += "For example: \n\nreturn 2*x"
977            else:
978                msg = 'Error: Function is not defined.'
979        else:
980            msg = "Name exists already."
981
982        # Prepare the messagebox
983        if self.base is not None and not msg:
984            self.base.update_custom_combo()
985            # Passed exception in import test as it will fail for sasmodels.sasview_model class
986            # Should add similar test for new style?
987            Model = None
988            try:
989                exec "from %s import Model" % name
990            except:
991                logger.error(sys.exc_value)
992
993        # Prepare the messagebox
994        if msg:
995            info = 'Error'
996            color = 'red'
997        else:
998            self._notes = result
999            msg = "Successful! Please look for %s in Plugin Models."%name
1000            msg += "  " + self._notes
1001            info = 'Info'
1002            color = 'blue'
1003        self._msg_box.SetLabel(msg)
1004        self._msg_box.SetForegroundColour(color)
1005        # Send msg to the top window
1006        if self.base is not None:
1007            from sas.sasgui.guiframe.events import StatusEvent
1008            wx.PostEvent(self.base.parent,
1009                         StatusEvent(status=msg+check_err, info=info))
1010        self.warning = msg
1011
1012    def write_file(self, fname, name, desc_str, param_str, pd_param_str, func_str):
1013        """
1014        Write content in file
1015
1016        :param fname: full file path
1017        :param desc_str: content of the description strings
1018        :param param_str: content of params; Strings
1019        :param pd_param_str: content of params requiring polydispersity; Strings
1020        :param func_str: content of func; Strings
1021        """
1022        try:
1023            out_f = open(fname, 'w')
1024        except:
1025            raise
1026        # Prepare the content of the function
1027        lines = CUSTOM_TEMPLATE.split('\n')
1028
1029        has_scipy = func_str.count("scipy.")
1030        if has_scipy:
1031            lines.insert(0, 'import scipy')
1032
1033        # Think about 2D later
1034        #self.is_2d = func_str.count("#self.ndim = 2")
1035        #line_2d = ''
1036        #if self.is_2d:
1037        #    line_2d = CUSTOM_2D_TEMP.split('\n')
1038
1039        # Also think about test later
1040        #line_test = TEST_TEMPLATE.split('\n')
1041        #local_params = ''
1042        #spaces = '        '#8spaces
1043        spaces4  = ' '*4
1044        spaces13 = ' '*13
1045        spaces16 = ' '*16
1046        param_names = []    # to store parameter names
1047        has_scipy = func_str.count("scipy.")
1048        if has_scipy:
1049            lines.insert(0, 'import scipy')
1050
1051        # write function here
1052        for line in lines:
1053            # The location where to put the strings is
1054            # hard-coded in the template as shown below.
1055            out_f.write(line + '\n')
1056            if line.count('#name'):
1057                out_f.write('name = "%s" \n' % name)
1058            elif line.count('#title'):
1059                out_f.write('title = "User model for %s"\n' % name)
1060            elif line.count('#description'):
1061                out_f.write('description = "%s"\n' % desc_str)
1062            elif line.count('#parameters'):
1063                out_f.write('parameters = [ \n')
1064                for param_line in param_str.split('\n'):
1065                    p_line = param_line.lstrip().rstrip()
1066                    if p_line:
1067                        pname, pvalue, desc = self.get_param_helper(p_line)
1068                        param_names.append(pname)
1069                        out_f.write("%s['%s', '', %s, [-numpy.inf, numpy.inf], '', '%s'],\n" % (spaces16, pname, pvalue, desc))
1070                for param_line in pd_param_str.split('\n'):
1071                    p_line = param_line.lstrip().rstrip()
1072                    if p_line:
1073                        pname, pvalue, desc = self.get_param_helper(p_line)
1074                        param_names.append(pname)
1075                        out_f.write("%s['%s', '', %s, [-numpy.inf, numpy.inf], 'volume', '%s'],\n" % (spaces16, pname, pvalue, desc))
1076                out_f.write('%s]\n' % spaces13)
1077
1078        # No form_volume or ER available in simple model editor
1079        out_f.write('def form_volume(*arg): \n')
1080        out_f.write('    return 1.0 \n')
1081        out_f.write('\n')
1082        out_f.write('def ER(*arg): \n')
1083        out_f.write('    return 1.0 \n')
1084
1085        # function to compute
1086        out_f.write('\n')
1087        out_f.write('def Iq(x ')
1088        for name in param_names:
1089            out_f.write(', %s' % name)
1090        out_f.write('):\n')
1091        for func_line in func_str.split('\n'):
1092            out_f.write('%s%s\n' % (spaces4, func_line))
1093
1094        Iqxy_string = 'return Iq(numpy.sqrt(x**2+y**2) '
1095
1096        out_f.write('\n')
1097        out_f.write('def Iqxy(x, y ')
1098        for name in param_names:
1099            out_f.write(', %s' % name)
1100            Iqxy_string += ', ' + name
1101        out_f.write('):\n')
1102        Iqxy_string += ')'
1103        out_f.write('%s%s\n' % (spaces4, Iqxy_string))
1104
1105        out_f.close()
1106
1107    def get_param_helper(self, line):
1108        """
1109        Get string in line to define the params dictionary
1110
1111        :param line: one line of string got from the param_str
1112        """
1113        items = line.split(";")
1114        for item in items:
1115            name = item.split("=")[0].strip()
1116            description = ""
1117            try:
1118                value = item.split("=")[1].strip()
1119                if value.count("#"):
1120                    # If line ends in a comment, remove it before parsing float
1121                    index = value.index("#")
1122                    description = value[(index + 1):].strip()
1123                    value = value[:value.index("#")].strip()
1124                float(value)
1125            except ValueError:
1126                value = 1.0 # default
1127
1128        return name, value, description
1129
1130    def set_function_helper(self, line):
1131        """
1132        Get string in line to define the local params
1133
1134        :param line: one line of string got from the param_str
1135        """
1136        params_str = ''
1137        spaces = '        '#8spaces
1138        items = line.split(";")
1139        for item in items:
1140            name = item.split("=")[0].lstrip().rstrip()
1141            params_str += spaces + "%s = self.params['%s']\n" % (name, name)
1142        return params_str
1143
1144    def get_warning(self):
1145        """
1146        Get the warning msg
1147        """
1148        return self.warning
1149
1150    def on_help(self, event):
1151        """
1152        Bring up the New Plugin Model Editor Documentation whenever
1153        the HELP button is clicked.
1154
1155        Calls DocumentationWindow with the path of the location within the
1156        documentation tree (after /doc/ ....".  Note that when using old
1157        versions of Wx (before 2.9) and thus not the release version of
1158        installers, the help comes up at the top level of the file as
1159        webbrowser does not pass anything past the # to the browser when it is
1160        running "file:///...."
1161
1162    :param evt: Triggers on clicking the help button
1163    """
1164
1165        _TreeLocation = "user/sasgui/perspectives/fitting/fitting_help.html"
1166        _PageAnchor = "#new-plugin-model"
1167        _doc_viewer = DocumentationWindow(self, -1, _TreeLocation, _PageAnchor,
1168                                          "Plugin Model Editor Help")
1169
1170    def on_close(self, event):
1171        """
1172        leave data as it is and close
1173        """
1174        self.parent.Show(False)#Close()
1175        event.Skip()
1176
1177class EditorWindow(wx.Frame):
1178    """
1179    Editor Window
1180    """
1181    def __init__(self, parent, base, path, title,
1182                 size=(EDITOR_WIDTH, EDITOR_HEIGTH), *args, **kwds):
1183        """
1184        Init
1185        """
1186        kwds["title"] = title
1187        kwds["size"] = size
1188        wx.Frame.__init__(self, parent=None, *args, **kwds)
1189        self.parent = parent
1190        self.panel = EditorPanel(parent=self, base=parent,
1191                                 path=path, title=title)
1192        self.Show(True)
1193        wx.EVT_CLOSE(self, self.on_close)
1194
1195    def on_close(self, event):
1196        """
1197        On close event
1198        """
1199        self.Show(False)
1200        #if self.parent is not None:
1201        #    self.parent.new_model_frame = None
1202        #self.Destroy()
1203
1204## Templates for plugin models
1205
1206CUSTOM_TEMPLATE = """
1207from math import *
1208import os
1209import sys
1210import numpy
1211
1212#name
1213
1214#title
1215
1216#description
1217
1218#parameters
1219
1220"""
1221
1222CUSTOM_2D_TEMP = """
1223    def run(self, x=0.0, y=0.0):
1224        if x.__class__.__name__ == 'list':
1225            x_val = x[0]
1226            y_val = y[0]*0.0
1227            return self.function(x_val, y_val)
1228        elif x.__class__.__name__ == 'tuple':
1229            msg = "Tuples are not allowed as input to BaseComponent models"
1230            raise ValueError, msg
1231        else:
1232            return self.function(x, 0.0)
1233    def runXY(self, x=0.0, y=0.0):
1234        if x.__class__.__name__ == 'list':
1235            return self.function(x, y)
1236        elif x.__class__.__name__ == 'tuple':
1237            msg = "Tuples are not allowed as input to BaseComponent models"
1238            raise ValueError, msg
1239        else:
1240            return self.function(x, y)
1241    def evalDistribution(self, qdist):
1242        if qdist.__class__.__name__ == 'list':
1243            msg = "evalDistribution expects a list of 2 ndarrays"
1244            if len(qdist)!=2:
1245                raise RuntimeError, msg
1246            if qdist[0].__class__.__name__ != 'ndarray':
1247                raise RuntimeError, msg
1248            if qdist[1].__class__.__name__ != 'ndarray':
1249                raise RuntimeError, msg
1250            v_model = numpy.vectorize(self.runXY, otypes=[float])
1251            iq_array = v_model(qdist[0], qdist[1])
1252            return iq_array
1253        elif qdist.__class__.__name__ == 'ndarray':
1254            v_model = numpy.vectorize(self.runXY, otypes=[float])
1255            iq_array = v_model(qdist)
1256            return iq_array
1257"""
1258TEST_TEMPLATE = """
1259######################################################################
1260## THIS IS FOR TEST. DO NOT MODIFY THE FOLLOWING LINES!!!!!!!!!!!!!!!!
1261if __name__ == "__main__":
1262    m= Model()
1263    out1 = m.runXY(0.0)
1264    out2 = m.runXY(0.01)
1265    isfine1 = numpy.isfinite(out1)
1266    isfine2 = numpy.isfinite(out2)
1267    print "Testing the value at Q = 0.0:"
1268    print out1, " : finite? ", isfine1
1269    print "Testing the value at Q = 0.01:"
1270    print out2, " : finite? ", isfine2
1271    if isfine1 and isfine2:
1272        print "===> Simple Test: Passed!"
1273    else:
1274        print "===> Simple Test: Failed!"
1275"""
1276SUM_TEMPLATE = """
1277# A sample of an experimental model function for Sum/Multiply(Pmodel1,Pmodel2)
1278import os
1279import sys
1280import copy
1281import collections
1282
1283import numpy
1284
1285from sas.sascalc.fit.pluginmodel import Model1DPlugin
1286from sasmodels.sasview_model import find_model
1287
1288class Model(Model1DPlugin):
1289    name = os.path.splitext(os.path.basename(__file__))[0]
1290    is_multiplicity_model = False
1291    def __init__(self, multiplicity=1):
1292        Model1DPlugin.__init__(self, name='')
1293        P1 = find_model('%s')
1294        P2 = find_model('%s')
1295        p_model1 = P1()
1296        p_model2 = P2()
1297        ## Setting  model name model description
1298        self.description = '%s'
1299        if self.name.rstrip().lstrip() == '':
1300            self.name = self._get_name(p_model1.name, p_model2.name)
1301        if self.description.rstrip().lstrip() == '':
1302            self.description = p_model1.name
1303            self.description += p_model2.name
1304            self.fill_description(p_model1, p_model2)
1305
1306        ## Define parameters
1307        self.params = collections.OrderedDict()
1308
1309        ## Parameter details [units, min, max]
1310        self.details = {}
1311        ## Magnetic Panrameters
1312        self.magnetic_params = []
1313        # non-fittable parameters
1314        self.non_fittable = p_model1.non_fittable
1315        self.non_fittable += p_model2.non_fittable
1316
1317        ##models
1318        self.p_model1= p_model1
1319        self.p_model2= p_model2
1320
1321
1322        ## dispersion
1323        self._set_dispersion()
1324        ## Define parameters
1325        self._set_params()
1326        ## New parameter:scaling_factor
1327        self.params['scale_factor'] = %s
1328
1329        ## Parameter details [units, min, max]
1330        self._set_details()
1331        self.details['scale_factor'] = ['', 0.0, numpy.inf]
1332
1333
1334        #list of parameter that can be fitted
1335        self._set_fixed_params()
1336
1337        ## parameters with orientation
1338        self.orientation_params = []
1339        for item in self.p_model1.orientation_params:
1340            new_item = "p1_" + item
1341            if not new_item in self.orientation_params:
1342                self.orientation_params.append(new_item)
1343
1344        for item in self.p_model2.orientation_params:
1345            new_item = "p2_" + item
1346            if not new_item in self.orientation_params:
1347                self.orientation_params.append(new_item)
1348        ## magnetic params
1349        self.magnetic_params = []
1350        for item in self.p_model1.magnetic_params:
1351            new_item = "p1_" + item
1352            if not new_item in self.magnetic_params:
1353                self.magnetic_params.append(new_item)
1354
1355        for item in self.p_model2.magnetic_params:
1356            new_item = "p2_" + item
1357            if not new_item in self.magnetic_params:
1358                self.magnetic_params.append(new_item)
1359        # get multiplicity if model provide it, else 1.
1360        try:
1361            multiplicity1 = p_model1.multiplicity
1362            try:
1363                multiplicity2 = p_model2.multiplicity
1364            except:
1365                multiplicity2 = 1
1366        except:
1367            multiplicity1 = 1
1368            multiplicity2 = 1
1369        ## functional multiplicity of the model
1370        self.multiplicity1 = multiplicity1
1371        self.multiplicity2 = multiplicity2
1372        self.multiplicity_info = []
1373
1374    def _clone(self, obj):
1375        import copy
1376        obj.params     = copy.deepcopy(self.params)
1377        obj.description     = copy.deepcopy(self.description)
1378        obj.details    = copy.deepcopy(self.details)
1379        obj.dispersion = copy.deepcopy(self.dispersion)
1380        obj.p_model1  = self.p_model1.clone()
1381        obj.p_model2  = self.p_model2.clone()
1382        #obj = copy.deepcopy(self)
1383        return obj
1384
1385    def _get_name(self, name1, name2):
1386        p1_name = self._get_upper_name(name1)
1387        if not p1_name:
1388            p1_name = name1
1389        name = p1_name
1390        name += "_and_"
1391        p2_name = self._get_upper_name(name2)
1392        if not p2_name:
1393            p2_name = name2
1394        name += p2_name
1395        return name
1396
1397    def _get_upper_name(self, name=None):
1398        if name is None:
1399            return ""
1400        upper_name = ""
1401        str_name = str(name)
1402        for index in range(len(str_name)):
1403            if str_name[index].isupper():
1404                upper_name += str_name[index]
1405        return upper_name
1406
1407    def _set_dispersion(self):
1408        self.dispersion = collections.OrderedDict()
1409        ##set dispersion only from p_model
1410        for name , value in self.p_model1.dispersion.iteritems():
1411            #if name.lower() not in self.p_model1.orientation_params:
1412            new_name = "p1_" + name
1413            self.dispersion[new_name]= value
1414        for name , value in self.p_model2.dispersion.iteritems():
1415            #if name.lower() not in self.p_model2.orientation_params:
1416            new_name = "p2_" + name
1417            self.dispersion[new_name]= value
1418
1419    def function(self, x=0.0):
1420        return 0
1421
1422    def getProfile(self):
1423        try:
1424            x,y = self.p_model1.getProfile()
1425        except:
1426            x = None
1427            y = None
1428
1429        return x, y
1430
1431    def _set_params(self):
1432        for name , value in self.p_model1.params.iteritems():
1433            # No 2D-supported
1434            #if name not in self.p_model1.orientation_params:
1435            new_name = "p1_" + name
1436            self.params[new_name]= value
1437
1438        for name , value in self.p_model2.params.iteritems():
1439            # No 2D-supported
1440            #if name not in self.p_model2.orientation_params:
1441            new_name = "p2_" + name
1442            self.params[new_name]= value
1443
1444        # Set "scale" as initializing
1445        self._set_scale_factor()
1446
1447
1448    def _set_details(self):
1449        for name ,detail in self.p_model1.details.iteritems():
1450            new_name = "p1_" + name
1451            #if new_name not in self.orientation_params:
1452            self.details[new_name]= detail
1453
1454        for name ,detail in self.p_model2.details.iteritems():
1455            new_name = "p2_" + name
1456            #if new_name not in self.orientation_params:
1457            self.details[new_name]= detail
1458
1459    def _set_scale_factor(self):
1460        pass
1461
1462
1463    def setParam(self, name, value):
1464        # set param to this (p1, p2) model
1465        self._setParamHelper(name, value)
1466
1467        ## setParam to p model
1468        model_pre = ''
1469        new_name = ''
1470        name_split = name.split('_', 1)
1471        if len(name_split) == 2:
1472            model_pre = name.split('_', 1)[0]
1473            new_name = name.split('_', 1)[1]
1474        if model_pre == "p1":
1475            if new_name in self.p_model1.getParamList():
1476                self.p_model1.setParam(new_name, value)
1477        elif model_pre == "p2":
1478             if new_name in self.p_model2.getParamList():
1479                self.p_model2.setParam(new_name, value)
1480        elif name == 'scale_factor':
1481            self.params['scale_factor'] = value
1482        else:
1483            raise ValueError, "Model does not contain parameter %s" % name
1484
1485    def getParam(self, name):
1486        # Look for dispersion parameters
1487        toks = name.split('.')
1488        if len(toks)==2:
1489            for item in self.dispersion.keys():
1490                # 2D not supported
1491                if item.lower()==toks[0].lower():
1492                    for par in self.dispersion[item]:
1493                        if par.lower() == toks[1].lower():
1494                            return self.dispersion[item][par]
1495        else:
1496            # Look for standard parameter
1497            for item in self.params.keys():
1498                if item.lower()==name.lower():
1499                    return self.params[item]
1500        return
1501        #raise ValueError, "Model does not contain parameter %s" % name
1502
1503    def _setParamHelper(self, name, value):
1504        # Look for dispersion parameters
1505        toks = name.split('.')
1506        if len(toks)== 2:
1507            for item in self.dispersion.keys():
1508                if item.lower()== toks[0].lower():
1509                    for par in self.dispersion[item]:
1510                        if par.lower() == toks[1].lower():
1511                            self.dispersion[item][par] = value
1512                            return
1513        else:
1514            # Look for standard parameter
1515            for item in self.params.keys():
1516                if item.lower()== name.lower():
1517                    self.params[item] = value
1518                    return
1519
1520        raise ValueError, "Model does not contain parameter %s" % name
1521
1522
1523    def _set_fixed_params(self):
1524        self.fixed = []
1525        for item in self.p_model1.fixed:
1526            new_item = "p1" + item
1527            self.fixed.append(new_item)
1528        for item in self.p_model2.fixed:
1529            new_item = "p2" + item
1530            self.fixed.append(new_item)
1531
1532        self.fixed.sort()
1533
1534
1535    def run(self, x = 0.0):
1536        self._set_scale_factor()
1537        return self.params['scale_factor'] %s \
1538(self.p_model1.run(x) %s self.p_model2.run(x))
1539
1540    def runXY(self, x = 0.0):
1541        self._set_scale_factor()
1542        return self.params['scale_factor'] %s \
1543(self.p_model1.runXY(x) %s self.p_model2.runXY(x))
1544
1545    ## Now (May27,10) directly uses the model eval function
1546    ## instead of the for-loop in Base Component.
1547    def evalDistribution(self, x = []):
1548        self._set_scale_factor()
1549        return self.params['scale_factor'] %s \
1550(self.p_model1.evalDistribution(x) %s \
1551self.p_model2.evalDistribution(x))
1552
1553    def set_dispersion(self, parameter, dispersion):
1554        value= None
1555        new_pre = parameter.split("_", 1)[0]
1556        new_parameter = parameter.split("_", 1)[1]
1557        try:
1558            if new_pre == 'p1' and \
1559new_parameter in self.p_model1.dispersion.keys():
1560                value= self.p_model1.set_dispersion(new_parameter, dispersion)
1561            if new_pre == 'p2' and \
1562new_parameter in self.p_model2.dispersion.keys():
1563                value= self.p_model2.set_dispersion(new_parameter, dispersion)
1564            self._set_dispersion()
1565            return value
1566        except:
1567            raise
1568
1569    def fill_description(self, p_model1, p_model2):
1570        description = ""
1571        description += "This model gives the summation or multiplication of"
1572        description += "%s and %s. "% ( p_model1.name, p_model2.name )
1573        self.description += description
1574
1575if __name__ == "__main__":
1576    m1= Model()
1577    #m1.setParam("p1_scale", 25)
1578    #m1.setParam("p1_length", 1000)
1579    #m1.setParam("p2_scale", 100)
1580    #m1.setParam("p2_rg", 100)
1581    out1 = m1.runXY(0.01)
1582
1583    m2= Model()
1584    #m2.p_model1.setParam("scale", 25)
1585    #m2.p_model1.setParam("length", 1000)
1586    #m2.p_model2.setParam("scale", 100)
1587    #m2.p_model2.setParam("rg", 100)
1588    out2 = m2.p_model1.runXY(0.01) %s m2.p_model2.runXY(0.01)\n
1589    print "My name is %s."% m1.name
1590    print out1, " = ", out2
1591    if out1 == out2:
1592        print "===> Simple Test: Passed!"
1593    else:
1594        print "===> Simple Test: Failed!"
1595"""
1596
1597if __name__ == "__main__":
1598#    app = wx.PySimpleApp()
1599    main_app = wx.App()
1600    main_frame = TextDialog(id=1, model_list=["SphereModel", "CylinderModel"],
1601                       plugin_dir='../fitting/plugin_models')
1602    main_frame.ShowModal()
1603    main_app.MainLoop()
1604
1605#if __name__ == "__main__":
1606#    from sas.sasgui.perspectives.fitting import models
1607#    dir_path = models.find_plugins_dir()
1608#    app = wx.App()
1609#    window = EditorWindow(parent=None, base=None, path=dir_path, title="Editor")
1610#    app.MainLoop()
Note: See TracBrowser for help on using the repository browser.