''' This module provides three model editor classes: the composite model editor, the easy editor which provides a simple interface with tooltip help to enter the parameters of the model and their default value and a panel to input a function of y (usually the intensity). It also provides a drop down of standard available math functions. Finally a full python editor panel for complete customization is provided. :TODO the writing of the file and name checking (and maybe some other functions?) should be moved to a computational module which could be called from a python script. Basically one just needs to pass the name, description text and function text (or in the case of the composite editor the names of the first and second model and the operator to be used). ''' ################################################################################ #This software was developed by the University of Tennessee as part of the #Distributed Data Analysis of Neutron Scattering Experiments (DANSE) #project funded by the US National Science Foundation. # #See the license text in license.txt # #copyright 2009, University of Tennessee ################################################################################ from __future__ import print_function import wx import sys import os import math import re import logging from wx.py.editwindow import EditWindow from sas.sasgui.guiframe.documentation_window import DocumentationWindow from .pyconsole import show_model_output, check_model logger = logging.getLogger(__name__) if sys.platform.count("win32") > 0: FONT_VARIANT = 0 PNL_WIDTH = 450 PNL_HEIGHT = 320 else: FONT_VARIANT = 1 PNL_WIDTH = 590 PNL_HEIGHT = 350 M_NAME = 'Model' EDITOR_WIDTH = 800 EDITOR_HEIGTH = 735 PANEL_WIDTH = 500 _BOX_WIDTH = 55 def _delete_file(path): """ Delete file in the path """ try: os.remove(path) except: raise class TextDialog(wx.Dialog): """ Dialog for easy custom composite models. Provides a wx.Dialog panel to choose two existing models (including pre-existing Plugin Models which may themselves be composite models) as well as an operation on those models (add or multiply) the resulting model will add a scale parameter for summed models and a background parameter for a multiplied model. The user also gives a brief help for the model in a description box and must provide a unique name which is verified as unique before the new model is saved. This Dialog pops up for the user when they press 'Sum|Multi(p1,p2)' under 'Plugin Model Operations' under 'Fitting' menu. This is currently called as a Modal Dialog. :TODO the build in compiler currently balks at when it tries to import a model whose name contains spaces or symbols (such as + ... underscore should be fine). Have fixed so the editor cannot save such a file name but if a file is dropped in the plugin directory from outside this class will create a file that cannot be compiled. Should add the check to the write method or to the on_modelx method. - PDB:April 5, 2015 """ def __init__(self, parent=None, base=None, id=None, title='', model_list=[], plugin_dir=None): """ This class is run when instatiated. The __init__ initializes and calls the internal methods necessary. On exiting the wx.Dialog window should be destroyed. """ wx.Dialog.__init__(self, parent=parent, id=id, title=title, size=(PNL_WIDTH, PNL_HEIGHT)) self.parent = base #Font self.SetWindowVariant(variant=FONT_VARIANT) # default self.overwrite_name = False self.plugin_dir = plugin_dir self.model_list = model_list self.model1_string = "sphere" self.model2_string = "cylinder" self.name = 'Sum' + M_NAME self.factor = 'scale_factor' self._notes = '' self._operator = '+' self._operator_choice = None self.explanation = '' self.explanationctr = None self.type = None self.name_sizer = None self.name_tcl = None self.desc_sizer = None self.desc_tcl = None self._selection_box = None self.model1 = None self.model2 = None self.static_line_1 = None self.ok_button = None self.close_button = None self._msg_box = None self.msg_sizer = None self.fname = None self.cm_list = None self.is_p1_custom = False self.is_p2_custom = False self._build_sizer() self.model1_name = str(self.model1.GetValue()) self.model2_name = str(self.model2.GetValue()) self.good_name = True self.fill_oprator_combox() def _layout_name(self): """ Do the layout for file/function name related widgets """ #container for new model name input self.name_sizer = wx.BoxSizer(wx.HORIZONTAL) #set up label and input box with tool tip and event handling name_txt = wx.StaticText(self, -1, 'Function Name : ') self.name_tcl = wx.TextCtrl(self, -1, value='MySumFunction') self.name_tcl.Bind(wx.EVT_TEXT_ENTER, self.on_change_name) hint_name = "Unique Sum/Multiply Model Function Name." self.name_tcl.SetToolTipString(hint_name) self.name_sizer.AddMany([(name_txt, 0, wx.LEFT | wx.TOP, 10), (self.name_tcl, -1, wx.EXPAND | wx.RIGHT | wx.TOP | wx.BOTTOM, 10)]) def _layout_description(self): """ Do the layout for description related widgets """ #container for new model description input self.desc_sizer = wx.BoxSizer(wx.HORIZONTAL) #set up description label and input box with tool tip and event handling desc_txt = wx.StaticText(self, -1, 'Description (optional) : ') self.desc_tcl = wx.TextCtrl(self, -1) hint_desc = "Write a short description of this model function." self.desc_tcl.SetToolTipString(hint_desc) self.desc_sizer.AddMany([(desc_txt, 0, wx.LEFT | wx.TOP, 10), (self.desc_tcl, -1, wx.EXPAND | wx.RIGHT | wx.TOP | wx.BOTTOM, 10)]) def _layout_model_selection(self): """ Do the layout for model selection related widgets """ box_width = 195 # combobox width #First set up main sizer for the selection selection_box_title = wx.StaticBox(self, -1, 'Select', size=(PNL_WIDTH - 30, 70)) self._selection_box = wx.StaticBoxSizer(selection_box_title, wx.VERTICAL) #Next create the help labels for the model selection select_help_box = wx.BoxSizer(wx.HORIZONTAL) model_string = " Model%s (p%s):" select_help_box.Add(wx.StaticText(self, -1, model_string % (1, 1)), 0, 0) select_help_box.Add((box_width - 25, 10), 0, 0) select_help_box.Add(wx.StaticText(self, -1, model_string % (2, 2)), 0, 0) self._selection_box.Add(select_help_box, 0, 0) #Next create the actual selection box with 3 combo boxes selection_box_choose = wx.BoxSizer(wx.HORIZONTAL) self.model1 = wx.ComboBox(self, -1, style=wx.CB_READONLY) wx.EVT_COMBOBOX(self.model1, -1, self.on_model1) self.model1.SetMinSize((box_width * 5 / 6, -1)) self.model1.SetToolTipString("model1") self._operator_choice = wx.ComboBox(self, -1, size=(50, -1), style=wx.CB_READONLY) wx.EVT_COMBOBOX(self._operator_choice, -1, self.on_select_operator) operation_tip = "Add: +, Multiply: * " self._operator_choice.SetToolTipString(operation_tip) self.model2 = wx.ComboBox(self, -1, style=wx.CB_READONLY) wx.EVT_COMBOBOX(self.model2, -1, self.on_model2) self.model2.SetMinSize((box_width * 5 / 6, -1)) self.model2.SetToolTipString("model2") self._set_model_list() selection_box_choose.Add(self.model1, 0, 0) selection_box_choose.Add((15, 10)) selection_box_choose.Add(self._operator_choice, 0, 0) selection_box_choose.Add((15, 10)) selection_box_choose.Add(self.model2, 0, 0) # add some space between labels and selection self._selection_box.Add((20, 5), 0, 0) self._selection_box.Add(selection_box_choose, 0, 0) def _build_sizer(self): """ Build GUI with calls to _layout_name, _layout Description and _layout_model_selection which each build a their portion of the GUI. """ mainsizer = wx.BoxSizer(wx.VERTICAL) # create main sizer for dialog # build fromm top by calling _layout_name and _layout_description # and adding to main sizer self._layout_name() mainsizer.Add(self.name_sizer, 0, wx.EXPAND) self._layout_description() mainsizer.Add(self.desc_sizer, 0, wx.EXPAND) # Add an explanation of dialog (short help) self.explanationctr = wx.StaticText(self, -1, self.explanation) self.fill_explanation_helpstring(self._operator) mainsizer.Add(self.explanationctr, 0, wx.LEFT | wx.EXPAND, 15) # Add the selection box stuff with border and labels built # by _layout_model_selection self._layout_model_selection() mainsizer.Add(self._selection_box, 0, wx.LEFT, 15) # Add a space and horizontal line before the notification #messages and the buttons at the bottom mainsizer.Add((10, 10)) self.static_line_1 = wx.StaticLine(self, -1) mainsizer.Add(self.static_line_1, 0, wx.EXPAND, 10) # Add action status notification line (null at startup) self._msg_box = wx.StaticText(self, -1, self._notes) self.msg_sizer = wx.BoxSizer(wx.HORIZONTAL) self.msg_sizer.Add(self._msg_box, 0, wx.LEFT, 0) mainsizer.Add(self.msg_sizer, 0, wx.LEFT | wx.RIGHT | wx.ADJUST_MINSIZE | wx.BOTTOM, 10) # Finally add the buttons (apply and close) on the bottom # Eventually need to add help here self.ok_button = wx.Button(self, wx.ID_OK, 'Apply') _app_tip = "Save the new Model." self.ok_button.SetToolTipString(_app_tip) self.ok_button.Bind(wx.EVT_BUTTON, self.check_name) self.help_button = wx.Button(self, -1, 'HELP') _app_tip = "Help on composite model creation." self.help_button.SetToolTipString(_app_tip) self.help_button.Bind(wx.EVT_BUTTON, self.on_help) self.close_button = wx.Button(self, wx.ID_CANCEL, 'Close') sizer_button = wx.BoxSizer(wx.HORIZONTAL) sizer_button.AddMany([((20, 20), 1, 0), (self.ok_button, 0, 0), (self.help_button, 0, 0), (self.close_button, 0, wx.LEFT | wx.RIGHT, 10)]) mainsizer.Add(sizer_button, 0, wx.EXPAND | wx.BOTTOM | wx.TOP, 10) self.SetSizer(mainsizer) self.Centre() def on_change_name(self, event=None): """ Change name """ if event is not None: event.Skip() self.name_tcl.SetBackgroundColour('white') self.Refresh() def check_name(self, event=None): """ Check that proposed new model name is a valid Python module name and that it does not already exist. If not show error message and pink background in text box else call on_apply :TODO this should be separated out from the GUI code. For that we need to pass it the name (or if we want to keep the default name option also need to pass the self._operator attribute) We just need the function to return an error code that the name is good or if not why (not a valid name, name exists already). The rest of the error handling should be done in this module. so on_apply would then start by checking the name and then either raise errors or do the deed. """ #Get the function/file name mname = M_NAME self.on_change_name(None) title = self.name_tcl.GetValue().lstrip().rstrip() if title == '': text = self._operator if text.count('+') > 0: mname = 'Sum' else: mname = 'Multi' mname += M_NAME title = mname self.name = title t_fname = title + '.py' #First check if the name is a valid Python name if re.match('^[A-Za-z0-9_]*$', title): self.good_name = True else: self.good_name = False msg = ("%s is not a valid Python name. Only alphanumeric \n" \ "and underscore allowed" % self.name) #Now check if the name already exists if not self.overwrite_name and self.good_name: #Create list of existing model names for comparison list_fnames = os.listdir(self.plugin_dir) # fake existing regular model name list m_list = [model + ".py" for model in self.model_list] list_fnames.append(m_list) if t_fname in list_fnames and title != mname: self.good_name = False msg = "Name exists already." if self.good_name == False: self.name_tcl.SetBackgroundColour('pink') info = 'Error' wx.MessageBox(msg, info) self._notes = msg color = 'red' self._msg_box.SetLabel(msg) self._msg_box.SetForegroundColour(color) return self.good_name self.fname = os.path.join(self.plugin_dir, t_fname) s_title = title if len(title) > 20: s_title = title[0:19] + '...' self._notes = "Model function (%s) has been set! \n" % str(s_title) self.good_name = True self.on_apply(self.fname) return self.good_name def on_apply(self, path): """ This method is a misnomer - it is not bound to the apply button event. Instead the apply button event goes to check_name which then calls this method if the name of the new file is acceptable. :TODO this should be bound to the apply button. The first line should call the check_name method which itself should be in another module separated from the the GUI modules. """ self.name_tcl.SetBackgroundColour('white') try: label = self.get_textnames() fname = path name1 = label[0] name2 = label[1] self.write_string(fname, name1, name2) success = show_model_output(self, fname) if success: self.parent.update_custom_combo() msg = self._notes info = 'Info' color = 'blue' except: msg = "Easy Sum/Multipy Plugin: Error occurred..." info = 'Error' color = 'red' self._msg_box.SetLabel(msg) self._msg_box.SetForegroundColour(color) if self.parent.parent is not None: from sas.sasgui.guiframe.events import StatusEvent wx.PostEvent(self.parent.parent, StatusEvent(status=msg, info=info)) def on_help(self, event): """ Bring up the Composite Model Editor Documentation whenever the HELP button is clicked. Calls DocumentationWindow with the path of the location within the documentation tree (after /doc/ ....". Note that when using old versions of Wx (before 2.9) and thus not the release version of installers, the help comes up at the top level of the file as webbrowser does not pass anything past the # to the browser when it is running "file:///...." :param evt: Triggers on clicking the help button """ _TreeLocation = "user/sasgui/perspectives/fitting/fitting_help.html" _PageAnchor = "#sum-multi-p1-p2" _doc_viewer = DocumentationWindow(self, -1, _TreeLocation, _PageAnchor, "Composite Model Editor Help") def _set_model_list(self): """ Set the list of models """ # list of model names # get regular models main_list = self.model_list # get custom models self.update_cm_list() # add custom models to model list for name in self.cm_list: if name not in main_list: main_list.append(name) if len(main_list) > 1: main_list.sort() for idx in range(len(main_list)): self.model1.Append(str(main_list[idx]), idx) self.model2.Append(str(main_list[idx]), idx) self.model1.SetStringSelection(self.model1_string) self.model2.SetStringSelection(self.model2_string) def update_cm_list(self): """ Update custom model list """ cm_list = [] al_list = os.listdir(self.plugin_dir) for c_name in al_list: if c_name.split('.')[-1] == 'py' and \ c_name.split('.')[0] != '__init__': name = str(c_name.split('.')[0]) cm_list.append(name) self.cm_list = cm_list def on_model1(self, event): """ Set model1 """ event.Skip() self.update_cm_list() self.model1_name = str(self.model1.GetValue()) self.model1_string = self.model1_name if self.model1_name in self.cm_list: self.is_p1_custom = True else: self.is_p1_custom = False def on_model2(self, event): """ Set model2 """ event.Skip() self.update_cm_list() self.model2_name = str(self.model2.GetValue()) self.model2_string = self.model2_name if self.model2_name in self.cm_list: self.is_p2_custom = True else: self.is_p2_custom = False def on_select_operator(self, event=None): """ On Select an Operator """ # For Mac if event is not None: event.Skip() item = event.GetEventObject() text = item.GetValue() self.fill_explanation_helpstring(text) def fill_explanation_helpstring(self, operator): """ Choose the equation to use depending on whether we now have a sum or multiply model then create the appropriate string """ name = '' if operator == '*': name = 'Multi' factor = 'BackGround' f_oper = '+' else: name = 'Sum' factor = 'scale_factor' f_oper = '*' self.factor = factor self._operator = operator self.explanation = " Plugin Model = %s %s (model1 %s model2)\n" % \ (self.factor, f_oper, self._operator) self.explanationctr.SetLabel(self.explanation) self.name = name + M_NAME def fill_oprator_combox(self): """ fill the current combobox with the operator """ operator_list = ['+', '*'] for oper in operator_list: pos = self._operator_choice.Append(str(oper)) self._operator_choice.SetClientData(pos, str(oper)) self._operator_choice.SetSelection(0) def get_textnames(self): """ Returns model name string as list """ return [self.model1_name, self.model2_name] def write_string(self, fname, name1, name2): """ Write and Save file """ self.fname = fname description = self.desc_tcl.GetValue().lstrip().rstrip() if description == '': description = name1 + self._operator + name2 text = self._operator_choice.GetValue() if text.count('+') > 0: factor = 'scale_factor' f_oper = '*' default_val = '1.0' else: factor = 'BackGround' f_oper = '+' default_val = '0.0' path = self.fname try: out_f = open(path, 'w') except: raise lines = SUM_TEMPLATE.split('\n') for line in lines: try: if line.count("scale_factor"): line = line.replace('scale_factor', factor) #print "scale_factor", line if line.count("= %s"): out_f.write(line % (default_val) + "\n") elif line.count("import Model as P1"): if self.is_p1_custom: line = line.replace('#', '') out_f.write(line % name1 + "\n") else: out_f.write(line + "\n") elif line.count("import %s as P1"): if not self.is_p1_custom: line = line.replace('#', '') out_f.write(line % (name1) + "\n") else: out_f.write(line + "\n") elif line.count("import Model as P2"): if self.is_p2_custom: line = line.replace('#', '') out_f.write(line % name2 + "\n") else: out_f.write(line + "\n") elif line.count("import %s as P2"): if not self.is_p2_custom: line = line.replace('#', '') out_f.write(line % (name2) + "\n") else: out_f.write(line + "\n") elif line.count("P1 = find_model"): out_f.write(line % (name1) + "\n") elif line.count("P2 = find_model"): out_f.write(line % (name2) + "\n") elif line.count("self.description = '%s'"): out_f.write(line % description + "\n") #elif line.count("run") and line.count("%s"): # out_f.write(line % self._operator + "\n") #elif line.count("evalDistribution") and line.count("%s"): # out_f.write(line % self._operator + "\n") elif line.count("return") and line.count("%s") == 2: #print "line return", line out_f.write(line % (f_oper, self._operator) + "\n") elif line.count("out2")and line.count("%s"): out_f.write(line % self._operator + "\n") else: out_f.write(line + "\n") except: raise out_f.close() #else: # msg = "Name exists already." def compile_file(self, path): """ Compile the file in the path """ path = self.fname show_model_output(self, path) def delete_file(self, path): """ Delete file in the path """ _delete_file(path) class EditorPanel(wx.ScrolledWindow): """ Simple Plugin Model function editor """ def __init__(self, parent, base, path, title, *args, **kwds): kwds['name'] = title # kwds["size"] = (EDITOR_WIDTH, EDITOR_HEIGTH) kwds["style"] = wx.FULL_REPAINT_ON_RESIZE wx.ScrolledWindow.__init__(self, parent, *args, **kwds) self.SetScrollbars(1,1,1,1) self.parent = parent self.base = base self.path = path self.font = wx.SystemSettings_GetFont(wx.SYS_SYSTEM_FONT) self.font.SetPointSize(10) self.reader = None self.name = 'untitled' self.overwrite_name = False self.is_2d = False self.fname = None self.main_sizer = None self.name_sizer = None self.name_hsizer = None self.name_tcl = None self.desc_sizer = None self.desc_tcl = None self.param_sizer = None self.param_tcl = None self.function_sizer = None self.func_horizon_sizer = None self.button_sizer = None self.param_strings = '' self.function_strings = '' self._notes = "" self._msg_box = None self.msg_sizer = None self.warning = "" #This does not seem to be used anywhere so commenting out for now # -- PDB 2/26/17 #self._description = "New Plugin Model" self.function_tcl = None self.math_combo = None self.bt_apply = None self.bt_close = None #self._default_save_location = os.getcwd() self._do_layout() def _define_structure(self): """ define initial sizer """ #w, h = self.parent.GetSize() self.main_sizer = wx.BoxSizer(wx.VERTICAL) self.name_sizer = wx.BoxSizer(wx.VERTICAL) self.name_hsizer = wx.BoxSizer(wx.HORIZONTAL) self.desc_sizer = wx.BoxSizer(wx.VERTICAL) self.param_sizer = wx.BoxSizer(wx.VERTICAL) self.function_sizer = wx.BoxSizer(wx.VERTICAL) self.func_horizon_sizer = wx.BoxSizer(wx.HORIZONTAL) self.button_sizer = wx.BoxSizer(wx.HORIZONTAL) self.msg_sizer = wx.BoxSizer(wx.HORIZONTAL) def _layout_name(self): """ Do the layout for file/function name related widgets """ #title name [string] name_txt = wx.StaticText(self, -1, 'Function Name : ') overwrite_cb = wx.CheckBox(self, -1, "Overwrite existing plugin model of this name?", (10, 10)) overwrite_cb.SetValue(False) overwrite_cb.SetToolTipString("Overwrite it if already exists?") wx.EVT_CHECKBOX(self, overwrite_cb.GetId(), self.on_over_cb) self.name_tcl = wx.TextCtrl(self, -1, size=(PANEL_WIDTH * 3 / 5, -1)) self.name_tcl.Bind(wx.EVT_TEXT_ENTER, self.on_change_name) self.name_tcl.SetValue('') self.name_tcl.SetFont(self.font) hint_name = "Unique Model Function Name." self.name_tcl.SetToolTipString(hint_name) self.name_hsizer.AddMany([(self.name_tcl, 0, wx.LEFT | wx.TOP, 0), (overwrite_cb, 0, wx.LEFT, 20)]) self.name_sizer.AddMany([(name_txt, 0, wx.LEFT | wx.TOP, 10), (self.name_hsizer, 0, wx.LEFT | wx.TOP | wx.BOTTOM, 10)]) def _layout_description(self): """ Do the layout for description related widgets """ #title name [string] desc_txt = wx.StaticText(self, -1, 'Description (optional) : ') self.desc_tcl = wx.TextCtrl(self, -1, size=(PANEL_WIDTH * 3 / 5, -1)) self.desc_tcl.SetValue('') hint_desc = "Write a short description of the model function." self.desc_tcl.SetToolTipString(hint_desc) self.desc_sizer.AddMany([(desc_txt, 0, wx.LEFT | wx.TOP, 10), (self.desc_tcl, 0, wx.LEFT | wx.TOP | wx.BOTTOM, 10)]) def _layout_param(self): """ Do the layout for parameter related widgets """ param_txt = wx.StaticText(self, -1, 'Fit Parameters NOT requiring' + \ ' polydispersity (if any): ') param_tip = "#Set the parameters NOT requiring polydispersity " + \ "and their initial values.\n" param_tip += "#Example:\n" param_tip += "A = 1\nB = 1" #param_txt.SetToolTipString(param_tip) newid = wx.NewId() self.param_tcl = EditWindow(self, newid, wx.DefaultPosition, wx.DefaultSize, wx.CLIP_CHILDREN | wx.SUNKEN_BORDER) self.param_tcl.setDisplayLineNumbers(True) self.param_tcl.SetToolTipString(param_tip) self.param_sizer.AddMany([(param_txt, 0, wx.LEFT, 10), (self.param_tcl, 1, wx.EXPAND | wx.ALL, 10)]) # Parameters with polydispersity pd_param_txt = wx.StaticText(self, -1, 'Fit Parameters requiring ' + \ 'polydispersity (if any): ') pd_param_tip = "#Set the parameters requiring polydispersity and " + \ "their initial values.\n" pd_param_tip += "#Example:\n" pd_param_tip += "C = 2\nD = 2" newid = wx.NewId() self.pd_param_tcl = EditWindow(self, newid, wx.DefaultPosition, wx.DefaultSize, wx.CLIP_CHILDREN | wx.SUNKEN_BORDER) self.pd_param_tcl.setDisplayLineNumbers(True) self.pd_param_tcl.SetToolTipString(pd_param_tip) self.param_sizer.AddMany([(pd_param_txt, 0, wx.LEFT, 10), (self.pd_param_tcl, 1, wx.EXPAND | wx.ALL, 10)]) def _layout_function(self): """ Do the layout for function related widgets """ function_txt = wx.StaticText(self, -1, 'Function(x) : ') hint_function = "#Example:\n" hint_function += "if x <= 0:\n" hint_function += " y = A + B\n" hint_function += "else:\n" hint_function += " y = A + B * cos(2 * pi * x)\n" hint_function += "return y\n" math_txt = wx.StaticText(self, -1, '*Useful math functions: ') math_combo = self._fill_math_combo() newid = wx.NewId() self.function_tcl = EditWindow(self, newid, wx.DefaultPosition, wx.DefaultSize, wx.CLIP_CHILDREN | wx.SUNKEN_BORDER) self.function_tcl.setDisplayLineNumbers(True) self.function_tcl.SetToolTipString(hint_function) self.func_horizon_sizer.Add(function_txt) self.func_horizon_sizer.Add(math_txt, 0, wx.LEFT, 250) self.func_horizon_sizer.Add(math_combo, 0, wx.LEFT, 10) self.function_sizer.Add(self.func_horizon_sizer, 0, wx.LEFT, 10) self.function_sizer.Add(self.function_tcl, 1, wx.EXPAND | wx.ALL, 10) def _layout_msg(self): """ Layout msg """ self._msg_box = wx.StaticText(self, -1, self._notes, size=(PANEL_WIDTH, -1)) self.msg_sizer.Add(self._msg_box, 0, wx.LEFT, 10) def _layout_button(self): """ Do the layout for the button widgets """ self.bt_apply = wx.Button(self, -1, "Apply", size=(_BOX_WIDTH, -1)) self.bt_apply.SetToolTipString("Save changes into the imported data.") self.bt_apply.Bind(wx.EVT_BUTTON, self.on_click_apply) self.bt_help = wx.Button(self, -1, "HELP", size=(_BOX_WIDTH, -1)) self.bt_help.SetToolTipString("Get Help For Model Editor") self.bt_help.Bind(wx.EVT_BUTTON, self.on_help) self.bt_close = wx.Button(self, -1, 'Close', size=(_BOX_WIDTH, -1)) self.bt_close.Bind(wx.EVT_BUTTON, self.on_close) self.bt_close.SetToolTipString("Close this panel.") self.button_sizer.AddMany([(self.bt_apply, 0,0), (self.bt_help, 0, wx.LEFT | wx.BOTTOM,15), (self.bt_close, 0, wx.LEFT | wx.RIGHT, 15)]) def _do_layout(self): """ Draw the current panel """ self._define_structure() self._layout_name() self._layout_description() self._layout_param() self._layout_function() self._layout_msg() self._layout_button() self.main_sizer.AddMany([(self.name_sizer, 0, wx.EXPAND | wx.ALL, 5), (wx.StaticLine(self), 0, wx.ALL | wx.EXPAND, 5), (self.desc_sizer, 0, wx.EXPAND | wx.ALL, 5), (wx.StaticLine(self), 0, wx.ALL | wx.EXPAND, 5), (self.param_sizer, 1, wx.EXPAND | wx.ALL, 5), (wx.StaticLine(self), 0, wx.ALL | wx.EXPAND, 5), (self.function_sizer, 2, wx.EXPAND | wx.ALL, 5), (wx.StaticLine(self), 0, wx.ALL | wx.EXPAND, 5), (self.msg_sizer, 0, wx.EXPAND | wx.ALL, 5), (self.button_sizer, 0, wx.ALIGN_RIGHT)]) self.SetSizer(self.main_sizer) self.SetAutoLayout(True) def _fill_math_combo(self): """ Fill up the math combo box """ self.math_combo = wx.ComboBox(self, -1, size=(100, -1), style=wx.CB_READONLY) for item in dir(math): if item.count("_") < 1: try: exec "float(math.%s)" % item self.math_combo.Append(str(item)) except: self.math_combo.Append(str(item) + "()") self.math_combo.Bind(wx.EVT_COMBOBOX, self._on_math_select) self.math_combo.SetSelection(0) return self.math_combo def _on_math_select(self, event): """ On math selection on ComboBox """ event.Skip() label = self.math_combo.GetValue() self.function_tcl.SetFocus() # Put the text at the cursor position pos = self.function_tcl.GetCurrentPos() self.function_tcl.InsertText(pos, label) # Put the cursor at appropriate position length = len(label) print(length) if label[length-1] == ')': length -= 1 f_pos = pos + length self.function_tcl.GotoPos(f_pos) def get_notes(self): """ return notes """ return self._notes def on_change_name(self, event=None): """ Change name """ if event is not None: event.Skip() self.name_tcl.SetBackgroundColour('white') self.Refresh() def check_name(self): """ Check name if exist already """ self._notes = '' self.on_change_name(None) plugin_dir = self.path list_fnames = os.listdir(plugin_dir) # function/file name title = self.name_tcl.GetValue().lstrip().rstrip() self.name = title t_fname = title + '.py' if not self.overwrite_name: if t_fname in list_fnames: self.name_tcl.SetBackgroundColour('pink') return False self.fname = os.path.join(plugin_dir, t_fname) s_title = title if len(title) > 20: s_title = title[0:19] + '...' self._notes += "Model function name is set " self._notes += "to %s. \n" % str(s_title) return True def on_over_cb(self, event): """ Set overwrite name flag on cb event """ if event is not None: event.Skip() cb_value = event.GetEventObject() self.overwrite_name = cb_value.GetValue() def on_click_apply(self, event): """ Changes are saved in data object imported to edit. checks firs for valid name, then if it already exists then checks that a function was entered and finally that if entered it contains at least a return statement. If all passes writes file then tries to compile. If compile fails or import module fails or run method fails tries to remove any .py and pyc files that may have been created and sets error message. :todo this code still could do with a careful going over to clean up and simplify. the non GUI methods such as this one should be removed to computational code of SasView. Most of those computational methods would be the same for both the simple editors. """ #must post event here event.Skip() name = self.name_tcl.GetValue().lstrip().rstrip() info = 'Info' msg = '' result, check_err = '', '' # Sort out the errors if occur # First check for valid python name then if the name already exists if not name or not bool(re.match('^[A-Za-z0-9_]*$', name)): msg = '"%s" '%name msg += "is not a valid model name. Name must not be empty and \n" msg += "may include only alpha numeric or underline characters \n" msg += "and no spaces" elif self.check_name(): description = self.desc_tcl.GetValue() param_str = self.param_tcl.GetText() pd_param_str = self.pd_param_tcl.GetText() func_str = self.function_tcl.GetText() # No input for the model function if func_str.lstrip().rstrip(): if func_str.count('return') > 0: self.write_file(self.fname, name, description, param_str, pd_param_str, func_str) try: result, msg = check_model(self.fname), None except Exception: import traceback result, msg = None, "error building model" check_err = "\n"+traceback.format_exc(limit=2) else: msg = "Error: The func(x) must 'return' a value at least.\n" msg += "For example: \n\nreturn 2*x" else: msg = 'Error: Function is not defined.' else: msg = "Name exists already." # Prepare the messagebox if self.base is not None and not msg: self.base.update_custom_combo() # Passed exception in import test as it will fail for sasmodels.sasview_model class # Should add similar test for new style? Model = None try: exec "from %s import Model" % name except: logger.error(sys.exc_value) # Prepare the messagebox if msg: info = 'Error' color = 'red' else: self._notes = result msg = "Successful! Please look for %s in Plugin Models."%name msg += " " + self._notes info = 'Info' color = 'blue' self._msg_box.SetLabel(msg) self._msg_box.SetForegroundColour(color) # Send msg to the top window if self.base is not None: from sas.sasgui.guiframe.events import StatusEvent wx.PostEvent(self.base.parent, StatusEvent(status=msg+check_err, info=info)) self.warning = msg def write_file(self, fname, name, desc_str, param_str, pd_param_str, func_str): """ Write content in file :param fname: full file path :param desc_str: content of the description strings :param param_str: content of params; Strings :param pd_param_str: content of params requiring polydispersity; Strings :param func_str: content of func; Strings """ try: out_f = open(fname, 'w') except: raise # Prepare the content of the function lines = CUSTOM_TEMPLATE.split('\n') has_scipy = func_str.count("scipy.") if has_scipy: lines.insert(0, 'import scipy') # Think about 2D later #self.is_2d = func_str.count("#self.ndim = 2") #line_2d = '' #if self.is_2d: # line_2d = CUSTOM_2D_TEMP.split('\n') # Also think about test later #line_test = TEST_TEMPLATE.split('\n') #local_params = '' #spaces = ' '#8spaces spaces4 = ' '*4 spaces13 = ' '*13 spaces16 = ' '*16 param_names = [] # to store parameter names has_scipy = func_str.count("scipy.") if has_scipy: lines.insert(0, 'import scipy') # write function here for line in lines: # The location where to put the strings is # hard-coded in the template as shown below. out_f.write(line + '\n') if line.count('#name'): out_f.write('name = "%s" \n' % name) elif line.count('#title'): out_f.write('title = "User model for %s"\n' % name) elif line.count('#description'): out_f.write('description = "%s"\n' % desc_str) elif line.count('#parameters'): out_f.write('parameters = [ \n') for param_line in param_str.split('\n'): p_line = param_line.lstrip().rstrip() if p_line: pname, pvalue, desc = self.get_param_helper(p_line) param_names.append(pname) out_f.write("%s['%s', '', %s, [-numpy.inf, numpy.inf], '', '%s'],\n" % (spaces16, pname, pvalue, desc)) for param_line in pd_param_str.split('\n'): p_line = param_line.lstrip().rstrip() if p_line: pname, pvalue, desc = self.get_param_helper(p_line) param_names.append(pname) out_f.write("%s['%s', '', %s, [-numpy.inf, numpy.inf], 'volume', '%s'],\n" % (spaces16, pname, pvalue, desc)) out_f.write('%s]\n' % spaces13) # No form_volume or ER available in simple model editor out_f.write('def form_volume(*arg): \n') out_f.write(' return 1.0 \n') out_f.write('\n') out_f.write('def ER(*arg): \n') out_f.write(' return 1.0 \n') # function to compute out_f.write('\n') out_f.write('def Iq(x ') for name in param_names: out_f.write(', %s' % name) out_f.write('):\n') for func_line in func_str.split('\n'): out_f.write('%s%s\n' % (spaces4, func_line)) Iqxy_string = 'return Iq(numpy.sqrt(x**2+y**2) ' out_f.write('\n') out_f.write('def Iqxy(x, y ') for name in param_names: out_f.write(', %s' % name) Iqxy_string += ', ' + name out_f.write('):\n') Iqxy_string += ')' out_f.write('%s%s\n' % (spaces4, Iqxy_string)) out_f.close() def get_param_helper(self, line): """ Get string in line to define the params dictionary :param line: one line of string got from the param_str """ items = line.split(";") for item in items: name = item.split("=")[0].strip() description = "" try: value = item.split("=")[1].strip() if value.count("#"): # If line ends in a comment, remove it before parsing float index = value.index("#") description = value[(index + 1):].strip() value = value[:value.index("#")].strip() float(value) except ValueError: value = 1.0 # default return name, value, description def set_function_helper(self, line): """ Get string in line to define the local params :param line: one line of string got from the param_str """ params_str = '' spaces = ' '#8spaces items = line.split(";") for item in items: name = item.split("=")[0].lstrip().rstrip() params_str += spaces + "%s = self.params['%s']\n" % (name, name) return params_str def get_warning(self): """ Get the warning msg """ return self.warning def on_help(self, event): """ Bring up the New Plugin Model Editor Documentation whenever the HELP button is clicked. Calls DocumentationWindow with the path of the location within the documentation tree (after /doc/ ....". Note that when using old versions of Wx (before 2.9) and thus not the release version of installers, the help comes up at the top level of the file as webbrowser does not pass anything past the # to the browser when it is running "file:///...." :param evt: Triggers on clicking the help button """ _TreeLocation = "user/sasgui/perspectives/fitting/fitting_help.html" _PageAnchor = "#new-plugin-model" _doc_viewer = DocumentationWindow(self, -1, _TreeLocation, _PageAnchor, "Plugin Model Editor Help") def on_close(self, event): """ leave data as it is and close """ self.parent.Show(False)#Close() event.Skip() class EditorWindow(wx.Frame): """ Editor Window """ def __init__(self, parent, base, path, title, size=(EDITOR_WIDTH, EDITOR_HEIGTH), *args, **kwds): """ Init """ kwds["title"] = title kwds["size"] = size wx.Frame.__init__(self, parent=None, *args, **kwds) self.parent = parent self.panel = EditorPanel(parent=self, base=parent, path=path, title=title) self.Show(True) wx.EVT_CLOSE(self, self.on_close) def on_close(self, event): """ On close event """ self.Show(False) #if self.parent is not None: # self.parent.new_model_frame = None #self.Destroy() ## Templates for plugin models CUSTOM_TEMPLATE = """ from math import * import os import sys import numpy #name #title #description #parameters """ CUSTOM_2D_TEMP = """ def run(self, x=0.0, y=0.0): if x.__class__.__name__ == 'list': x_val = x[0] y_val = y[0]*0.0 return self.function(x_val, y_val) elif x.__class__.__name__ == 'tuple': msg = "Tuples are not allowed as input to BaseComponent models" raise ValueError, msg else: return self.function(x, 0.0) def runXY(self, x=0.0, y=0.0): if x.__class__.__name__ == 'list': return self.function(x, y) elif x.__class__.__name__ == 'tuple': msg = "Tuples are not allowed as input to BaseComponent models" raise ValueError, msg else: return self.function(x, y) def evalDistribution(self, qdist): if qdist.__class__.__name__ == 'list': msg = "evalDistribution expects a list of 2 ndarrays" if len(qdist)!=2: raise RuntimeError, msg if qdist[0].__class__.__name__ != 'ndarray': raise RuntimeError, msg if qdist[1].__class__.__name__ != 'ndarray': raise RuntimeError, msg v_model = numpy.vectorize(self.runXY, otypes=[float]) iq_array = v_model(qdist[0], qdist[1]) return iq_array elif qdist.__class__.__name__ == 'ndarray': v_model = numpy.vectorize(self.runXY, otypes=[float]) iq_array = v_model(qdist) return iq_array """ TEST_TEMPLATE = """ ###################################################################### ## THIS IS FOR TEST. DO NOT MODIFY THE FOLLOWING LINES!!!!!!!!!!!!!!!! if __name__ == "__main__": m= Model() out1 = m.runXY(0.0) out2 = m.runXY(0.01) isfine1 = numpy.isfinite(out1) isfine2 = numpy.isfinite(out2) print "Testing the value at Q = 0.0:" print out1, " : finite? ", isfine1 print "Testing the value at Q = 0.01:" print out2, " : finite? ", isfine2 if isfine1 and isfine2: print "===> Simple Test: Passed!" else: print "===> Simple Test: Failed!" """ SUM_TEMPLATE = """ # A sample of an experimental model function for Sum/Multiply(Pmodel1,Pmodel2) import os import sys import copy import collections import numpy from sas.sascalc.fit.pluginmodel import Model1DPlugin from sasmodels.sasview_model import find_model class Model(Model1DPlugin): name = os.path.splitext(os.path.basename(__file__))[0] is_multiplicity_model = False def __init__(self, multiplicity=1): Model1DPlugin.__init__(self, name='') P1 = find_model('%s') P2 = find_model('%s') p_model1 = P1() p_model2 = P2() ## Setting model name model description self.description = '%s' if self.name.rstrip().lstrip() == '': self.name = self._get_name(p_model1.name, p_model2.name) if self.description.rstrip().lstrip() == '': self.description = p_model1.name self.description += p_model2.name self.fill_description(p_model1, p_model2) ## Define parameters self.params = collections.OrderedDict() ## Parameter details [units, min, max] self.details = {} ## Magnetic Panrameters self.magnetic_params = [] # non-fittable parameters self.non_fittable = p_model1.non_fittable self.non_fittable += p_model2.non_fittable ##models self.p_model1= p_model1 self.p_model2= p_model2 ## dispersion self._set_dispersion() ## Define parameters self._set_params() ## New parameter:scaling_factor self.params['scale_factor'] = %s ## Parameter details [units, min, max] self._set_details() self.details['scale_factor'] = ['', 0.0, numpy.inf] #list of parameter that can be fitted self._set_fixed_params() ## parameters with orientation self.orientation_params = [] for item in self.p_model1.orientation_params: new_item = "p1_" + item if not new_item in self.orientation_params: self.orientation_params.append(new_item) for item in self.p_model2.orientation_params: new_item = "p2_" + item if not new_item in self.orientation_params: self.orientation_params.append(new_item) ## magnetic params self.magnetic_params = [] for item in self.p_model1.magnetic_params: new_item = "p1_" + item if not new_item in self.magnetic_params: self.magnetic_params.append(new_item) for item in self.p_model2.magnetic_params: new_item = "p2_" + item if not new_item in self.magnetic_params: self.magnetic_params.append(new_item) # get multiplicity if model provide it, else 1. try: multiplicity1 = p_model1.multiplicity try: multiplicity2 = p_model2.multiplicity except: multiplicity2 = 1 except: multiplicity1 = 1 multiplicity2 = 1 ## functional multiplicity of the model self.multiplicity1 = multiplicity1 self.multiplicity2 = multiplicity2 self.multiplicity_info = [] def _clone(self, obj): import copy obj.params = copy.deepcopy(self.params) obj.description = copy.deepcopy(self.description) obj.details = copy.deepcopy(self.details) obj.dispersion = copy.deepcopy(self.dispersion) obj.p_model1 = self.p_model1.clone() obj.p_model2 = self.p_model2.clone() #obj = copy.deepcopy(self) return obj def _get_name(self, name1, name2): p1_name = self._get_upper_name(name1) if not p1_name: p1_name = name1 name = p1_name name += "_and_" p2_name = self._get_upper_name(name2) if not p2_name: p2_name = name2 name += p2_name return name def _get_upper_name(self, name=None): if name is None: return "" upper_name = "" str_name = str(name) for index in range(len(str_name)): if str_name[index].isupper(): upper_name += str_name[index] return upper_name def _set_dispersion(self): self.dispersion = collections.OrderedDict() ##set dispersion only from p_model for name , value in self.p_model1.dispersion.iteritems(): #if name.lower() not in self.p_model1.orientation_params: new_name = "p1_" + name self.dispersion[new_name]= value for name , value in self.p_model2.dispersion.iteritems(): #if name.lower() not in self.p_model2.orientation_params: new_name = "p2_" + name self.dispersion[new_name]= value def function(self, x=0.0): return 0 def getProfile(self): try: x,y = self.p_model1.getProfile() except: x = None y = None return x, y def _set_params(self): for name , value in self.p_model1.params.iteritems(): # No 2D-supported #if name not in self.p_model1.orientation_params: new_name = "p1_" + name self.params[new_name]= value for name , value in self.p_model2.params.iteritems(): # No 2D-supported #if name not in self.p_model2.orientation_params: new_name = "p2_" + name self.params[new_name]= value # Set "scale" as initializing self._set_scale_factor() def _set_details(self): for name ,detail in self.p_model1.details.iteritems(): new_name = "p1_" + name #if new_name not in self.orientation_params: self.details[new_name]= detail for name ,detail in self.p_model2.details.iteritems(): new_name = "p2_" + name #if new_name not in self.orientation_params: self.details[new_name]= detail def _set_scale_factor(self): pass def setParam(self, name, value): # set param to this (p1, p2) model self._setParamHelper(name, value) ## setParam to p model model_pre = '' new_name = '' name_split = name.split('_', 1) if len(name_split) == 2: model_pre = name.split('_', 1)[0] new_name = name.split('_', 1)[1] if model_pre == "p1": if new_name in self.p_model1.getParamList(): self.p_model1.setParam(new_name, value) elif model_pre == "p2": if new_name in self.p_model2.getParamList(): self.p_model2.setParam(new_name, value) elif name == 'scale_factor': self.params['scale_factor'] = value else: raise ValueError, "Model does not contain parameter %s" % name def getParam(self, name): # Look for dispersion parameters toks = name.split('.') if len(toks)==2: for item in self.dispersion.keys(): # 2D not supported if item.lower()==toks[0].lower(): for par in self.dispersion[item]: if par.lower() == toks[1].lower(): return self.dispersion[item][par] else: # Look for standard parameter for item in self.params.keys(): if item.lower()==name.lower(): return self.params[item] return #raise ValueError, "Model does not contain parameter %s" % name def _setParamHelper(self, name, value): # Look for dispersion parameters toks = name.split('.') if len(toks)== 2: for item in self.dispersion.keys(): if item.lower()== toks[0].lower(): for par in self.dispersion[item]: if par.lower() == toks[1].lower(): self.dispersion[item][par] = value return else: # Look for standard parameter for item in self.params.keys(): if item.lower()== name.lower(): self.params[item] = value return raise ValueError, "Model does not contain parameter %s" % name def _set_fixed_params(self): self.fixed = [] for item in self.p_model1.fixed: new_item = "p1" + item self.fixed.append(new_item) for item in self.p_model2.fixed: new_item = "p2" + item self.fixed.append(new_item) self.fixed.sort() def run(self, x = 0.0): self._set_scale_factor() return self.params['scale_factor'] %s \ (self.p_model1.run(x) %s self.p_model2.run(x)) def runXY(self, x = 0.0): self._set_scale_factor() return self.params['scale_factor'] %s \ (self.p_model1.runXY(x) %s self.p_model2.runXY(x)) ## Now (May27,10) directly uses the model eval function ## instead of the for-loop in Base Component. def evalDistribution(self, x = []): self._set_scale_factor() return self.params['scale_factor'] %s \ (self.p_model1.evalDistribution(x) %s \ self.p_model2.evalDistribution(x)) def set_dispersion(self, parameter, dispersion): value= None new_pre = parameter.split("_", 1)[0] new_parameter = parameter.split("_", 1)[1] try: if new_pre == 'p1' and \ new_parameter in self.p_model1.dispersion.keys(): value= self.p_model1.set_dispersion(new_parameter, dispersion) if new_pre == 'p2' and \ new_parameter in self.p_model2.dispersion.keys(): value= self.p_model2.set_dispersion(new_parameter, dispersion) self._set_dispersion() return value except: raise def fill_description(self, p_model1, p_model2): description = "" description += "This model gives the summation or multiplication of" description += "%s and %s. "% ( p_model1.name, p_model2.name ) self.description += description if __name__ == "__main__": m1= Model() #m1.setParam("p1_scale", 25) #m1.setParam("p1_length", 1000) #m1.setParam("p2_scale", 100) #m1.setParam("p2_rg", 100) out1 = m1.runXY(0.01) m2= Model() #m2.p_model1.setParam("scale", 25) #m2.p_model1.setParam("length", 1000) #m2.p_model2.setParam("scale", 100) #m2.p_model2.setParam("rg", 100) out2 = m2.p_model1.runXY(0.01) %s m2.p_model2.runXY(0.01)\n print "My name is %s."% m1.name print out1, " = ", out2 if out1 == out2: print "===> Simple Test: Passed!" else: print "===> Simple Test: Failed!" """ if __name__ == "__main__": # app = wx.PySimpleApp() main_app = wx.App() main_frame = TextDialog(id=1, model_list=["SphereModel", "CylinderModel"], plugin_dir='../fitting/plugin_models') main_frame.ShowModal() main_app.MainLoop() #if __name__ == "__main__": # from sas.sasgui.perspectives.fitting import models # dir_path = models.find_plugins_dir() # app = wx.App() # window = EditorWindow(parent=None, base=None, path=dir_path, title="Editor") # app.MainLoop()