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

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

Revert "Ensure sum/multiplication models behave correctly"

This reverts commit 14fbdba3d08cd73cbe9502e304cb92761e39253d.
This commit has been moved to a different branch (ticket-767)

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