'''
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
################################################################################
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
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
[docs]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()
[docs] def on_change_name(self, event=None):
"""
Change name
"""
if event is not None:
event.Skip()
self.name_tcl.SetBackgroundColour('white')
self.Refresh()
[docs] 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
[docs] 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 != None:
from sas.sasgui.guiframe.events import StatusEvent
wx.PostEvent(self.parent.parent, StatusEvent(status=msg,
info=info))
[docs] 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)
[docs] 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
[docs] 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
[docs] 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
[docs] def on_select_operator(self, event=None):
"""
On Select an Operator
"""
# For Mac
if event != None:
event.Skip()
item = event.GetEventObject()
text = item.GetValue()
self.fill_explanation_helpstring(text)
[docs] 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
[docs] 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)
[docs] def get_textnames(self):
"""
Returns model name string as list
"""
return [self.model1_name, self.model2_name]
[docs] 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."
[docs] def compile_file(self, path):
"""
Compile the file in the path
"""
path = self.fname
show_model_output(self, path)
[docs] def delete_file(self, path):
"""
Delete file in the path
"""
_delete_file(path)
[docs]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)
[docs] def get_notes(self):
"""
return notes
"""
return self._notes
[docs] def on_change_name(self, event=None):
"""
Change name
"""
if event is not None:
event.Skip()
self.name_tcl.SetBackgroundColour('white')
self.Refresh()
[docs] 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
[docs] 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()
[docs] 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 != 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:
logging.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 != None:
from sas.sasgui.guiframe.events import StatusEvent
wx.PostEvent(self.base.parent,
StatusEvent(status=msg+check_err, info=info))
self.warning = msg
[docs] 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 = self.get_param_helper(p_line)
param_names.append(pname)
out_f.write("%s['%s', '', %s, [-numpy.inf, numpy.inf], '', ''],\n" % (spaces16, pname, pvalue))
for param_line in pd_param_str.split('\n'):
p_line = param_line.lstrip().rstrip()
if p_line:
pname, pvalue = self.get_param_helper(p_line)
param_names.append(pname)
out_f.write("%s['%s', '', %s, [-numpy.inf, numpy.inf], 'volume', ''],\n" % (spaces16, pname, pvalue))
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()
[docs] 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].lstrip().rstrip()
try:
value = item.split("=")[1].lstrip().rstrip()
float(value)
except:
value = 1.0 # default
return name, value
[docs] 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
[docs] def get_warning(self):
"""
Get the warning msg
"""
return self.warning
[docs] 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")
[docs] def on_close(self, event):
"""
leave data as it is and close
"""
self.parent.Show(False)#Close()
event.Skip()
[docs]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)
[docs] def on_close(self, event):
"""
On close event
"""
self.Show(False)
#if self.parent != 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 == 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()