Coverage for larch/wxlib/parameter.py: 12%
342 statements
« prev ^ index » next coverage.py v7.6.0, created at 2024-10-16 21:04 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2024-10-16 21:04 +0000
1#!/usr/bin/env python
2"""
3General-Purpose Parameter widget
4 shows Float TextCtrl for value, Choice fixed/variable/constrained
5 and icon button to bring up dialog for full settings,
6 including max/min value and constraint expression
7"""
9import numpy as np
10import ast
11import wx
12from wx.lib.embeddedimage import PyEmbeddedImage
14from wxutils import (GridPanel, Choice, FloatCtrl,
15 LEFT, pack, HLine, SetTip, Font)
16from . import FONTSIZE
17from lmfit import Parameter
18from larch import Group
19from larch.larchlib import Empty
21PAR_FIX = 'fix'
22PAR_VAR = 'vary'
23PAR_CON = 'constrain'
24PAR_SKIP = 'skip'
25VARY_CHOICES = (PAR_VAR, PAR_FIX, PAR_CON)
26VARY_CHOICES_SKIP = (PAR_VAR, PAR_FIX, PAR_SKIP, PAR_CON)
28BOUNDS_custom = 'custom'
29BOUNDS_none = 'unbound'
30BOUNDS_pos = 'positive'
31BOUNDS_neg = 'negative'
32BOUNDS_CHOICES = (BOUNDS_none, BOUNDS_pos, BOUNDS_neg, BOUNDS_custom)
34PAR_WIDS = ('name', 'value', 'minval', 'maxval',
35 'vary', 'expr', 'stderr', 'bounds')
37infoicon = PyEmbeddedImage(
38 "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACzElEQVR42m2TW0gUURjHv3Nm"
39 "b+6ut028lOaldm0RL2gGxWamSWHWi4JhPRjokyhCYWA9lfRQD0FUBJbdCBEEUzFILFjxQaxQ"
40 "Wi+prXfXXV11J2zXXXdmOjPO7no78zAcvu//+/7nO+dDcMD6Ob14OiwisnDItpk0aXOi5XWX"
41 "ecm68qW5Irtvby7aualq/q49k6FrlEulOQ4Pi4ALxLa8DFjsG/0zc9bKDzezhvcByt8PntRr"
42 "Y7vlCmk4QmhXkGNZAIyBI0CXy7MxNW8vaizVG/05RQ9aNOmGHJNCIT3Mi3m9T3xcjSAvXgXG"
43 "eSeM0SyBcOBxe1cnRn6ntVTlWoS86s6ZxwqV/DYWxXjHwc7HKcFwVA0dEzQMLbuBJS5YAnG7"
44 "PK+eXo6vRNk36qisa1VWhHEEFsU+F5gkRisR8O1Y2eSE6tsAQmZZ5/i39ghUUN+UEpmZM8xX"
45 "x0QtAMiXGEJBoS4MUmKCYWCOhre/1gMAlhNcOBfMBpR3911+cPKpHgoToQBAEKmkIDZUDqlR"
46 "QXBBdwg6Ru3QOrImNFGA8ADSV2ZtsQQZ6prOKpIye4XqGPntKyQYGku0oJRJoLb9D8zT7j0A"
47 "sqGtV1FaeUOUKq1gCRM1Eo/BX8FFXTjcyk+EcdsG1LRNCkIQ7YsQjlkwnRD6ndlgHGARzg44"
48 "QPC8VA/pcWHw5OsU2Bwu6Jv+Kwh9DhDHTgzeO5csAPR1n4pZiboViQCVnALjHQNQ5PG0/ViA"
49 "7jE79E85eP/bEAKgwFsx+vDSa/+NJ9R2fuQQVcY3QCbF8Kg0FWjXFrzpnYXZVacoBuGPgOly"
50 "mrquLPe85PwATX61TKbNbeIAXxeeMto1JiCqSRO87cySqWyl475z3zDxS51bU0yFHqmnNAkZ"
51 "/MX65Mz6rHlz9PMzxm5+4V2b2zpwGgMziiQoSBODQ6KPEa2EpS0WzuWwkMg/fjB3pv4HvQJH"
52 "bUDKnS4AAAAASUVORK5CYII=")
55class ParameterWidgets(object):
56 """a set of related widgets for a lmfit Parameter
58 param = Parameter(value=11.22, vary=True, min=0, name='x1')
59 wid = ParameterPanel(parent_wid, param)
60 """
61 def __init__(self, parent, param, name_size=None, prefix=None,
62 expr_size=150, stderr_size=120, float_size=85,
63 minmax_size=60, with_skip=False, widgets=PAR_WIDS):
65 self.parent = parent
66 self.param = param
67 self.widgets = []
68 self._saved_expr = ''
69 if (prefix is not None and
70 not self.param.name.startswith(prefix)):
71 self.param.name = "%s%s" %(prefix, self.param.name)
73 self.skip = False
74 for attr in PAR_WIDS:
75 setattr(self, attr, None)
77 # set vary_choice from param attributes
78 vary_choice = PAR_VAR
79 value = None
80 if param.expr not in (None, 'None', ''):
81 vary_choice = PAR_CON
82 try:
83 value = param.value
84 except:
85 value = -np.Inf
87 else:
88 value = param.value
89 vary_choice = PAR_VAR if param.vary else PAR_FIX
91 if 'name' in widgets:
92 name = param.name
93 if name in (None, 'None', ''):
94 name = ''
95 if name_size is None:
96 name_size = min(50, len(param.name)*10) + 10
97 self.name = wx.StaticText(parent, label=name,
98 size=(name_size, -1))
99 self.widgets.append(self.name)
100 if 'value' in widgets:
101 self.value = FloatCtrl(parent, value=value,
102 minval=param.min,
103 maxval=param.max,
104 action=self.onValue,
105 act_on_losefocus=True,
106 gformat=True,
107 size=(float_size, -1))
108 self.widgets.append(self.value)
110 if 'minval' in widgets:
111 minval = param.min
112 if minval in (None, 'None', -np.inf):
113 minval = -np.inf
114 self.minval = FloatCtrl(parent, value=minval,
115 gformat=True,
116 size=(minmax_size, -1),
117 act_on_losefocus=True,
118 action=self.onMinval)
119 self.widgets.append(self.minval)
120 self.minval.Enable(vary_choice==PAR_VAR)
122 if 'maxval' in widgets:
123 maxval = param.max
124 if maxval in (None, 'None', np.inf):
125 maxval = np.inf
126 self.maxval = FloatCtrl(parent, value=maxval,
127 gformat=True,
128 size=(minmax_size, -1),
129 act_on_losefocus=True,
130 action=self.onMaxval)
131 self.widgets.append(self.maxval)
132 self.maxval.Enable(vary_choice==PAR_VAR)
134 if 'vary' in widgets:
135 vchoices = VARY_CHOICES_SKIP if with_skip else VARY_CHOICES
136 self.vary = Choice(parent, size=(100, -1),
137 choices=vchoices,
138 action=self.onVaryChoice)
139 self.widgets.append(self.vary)
140 self.vary.SetStringSelection(vary_choice)
142 if 'expr' in widgets:
143 expr = param.expr
144 if expr in (None, 'None', ''):
145 expr = ''
146 self._saved_expr = expr
147 self.expr = wx.TextCtrl(parent, -1, value=expr,
148 size=(expr_size, -1),
149 style=wx.TE_PROCESS_ENTER)
150 self.widgets.append(self.expr)
151 self.expr.Enable(vary_choice==PAR_CON)
152 self.expr.Bind(wx.EVT_TEXT_ENTER, self.onExpr)
153 self.expr.Bind(wx.EVT_KILL_FOCUS, self.onExpr)
154 SetTip(self.expr, 'Enter constraint expression')
156 if param.expr not in (None, 'None', ''):
157 try:
158 ast.parse(param.expr)
159 bgcol, fgcol = 'white', 'black'
160 except SyntaxError:
161 bgcol, fgcol = 'white', '#AA0000'
163 self.expr.SetForegroundColour(fgcol)
164 self.expr.SetBackgroundColour(bgcol)
167 if 'stderr' in widgets:
168 stderr = param.stderr
169 if stderr in (None, 'None', ''):
170 stderr = ''
171 self.stderr = wx.StaticText(parent, label=stderr,
172 size=(stderr_size, -1))
173 self.widgets.append(self.expr)
175 if 'minval' in widgets or 'maxval' in widgets:
176 minval = param.min
177 maxval = param.max
178 bounds_choice = BOUNDS_custom
179 if minval in (None, 'None', -np.inf) and maxval in (None, 'None', np.inf):
180 bounds_choice = BOUNDS_none
181 elif minval == 0:
182 bounds_choice = BOUNDS_pos
183 elif maxval == 0:
184 bounds_choice = BOUNDS_neg
186 self.bounds = Choice(parent, size=(110, -1),
187 choices=BOUNDS_CHOICES,
188 action=self.onBOUNDSChoice)
189 self.widgets.append(self.bounds)
191 for w in self.widgets:
192 w.SetFont(Font(FONTSIZE))
194 self.bounds.SetStringSelection(bounds_choice)
196 def onBOUNDSChoice(self, evt=None):
197 bounds = str(self.bounds.GetStringSelection().lower())
198 if bounds == BOUNDS_custom:
199 pass
200 elif bounds == BOUNDS_none:
201 self.minval.SetValue(-np.inf)
202 self.maxval.SetValue(np.inf)
204 elif bounds == BOUNDS_pos:
205 self.minval.SetValue(0)
206 if float(self.maxval.GetValue()) == 0:
207 self.maxval.SetValue(np.inf)
208 elif bounds == BOUNDS_neg:
209 self.maxval.SetValue(0)
210 if float(self.minval.GetValue()) == 0:
211 self.minval.SetValue(-np.inf)
213 def onValue(self, evt=None, value=None):
214 if value is not None:
215 self.param.value = value
217 def onExpr(self, evt=None, value=None):
218 if value is None:
219 value = self.expr.GetValue()
220 # if hasattr(evt, 'GetString'):
221 # value = evt.GetString()
222 try:
223 ast.parse(value)
224 self.param.expr = value
225 bgcol, fgcol = 'white', 'black'
226 except SyntaxError:
227 bgcol, fgcol = 'white', '#AA0000'
228 self.expr.SetForegroundColour(fgcol)
229 self.expr.SetBackgroundColour(bgcol)
231 def onMinval(self, evt=None, value=None):
232 if value in (None, 'None', ''):
233 value = -np.inf
234 if self.value is not None:
235 v = self.value.GetValue()
236 self.value.SetMin(value)
237 self.value.SetValue(v)
238 self.param.min = value
239 if self.bounds is not None:
240 if value == 0:
241 self.bounds.SetStringSelection(BOUNDS_pos)
242 elif value == -np.inf:
243 if self.maxval.GetValue() == np.inf:
244 self.bounds.SetStringSelection(BOUNDS_none)
245 else:
246 self.bounds.SetStringSelection(BOUNDS_custom)
248 def onMaxval(self, evt=None, value=None):
249 if value in (None, 'None', ''):
250 value = np.inf
251 if self.value is not None:
252 v = self.value.GetValue()
253 self.value.SetMax( value)
254 self.value.SetValue(v)
255 self.param.max = value
257 if self.bounds is not None:
258 if value == 0:
259 self.bounds.SetStringSelection(BOUNDS_neg)
260 elif value == np.inf:
261 if self.minval.GetValue() == -np.inf:
262 self.bounds.SetStringSelection(BOUNDS_none)
263 else:
264 self.bounds.SetStringSelection(BOUNDS_custom)
267 def onVaryChoice(self, evt=None):
268 if self.vary is None:
269 return
271 vary = str(self.vary.GetStringSelection().lower())
272 self.skip = (vary == PAR_SKIP)
274 self.param.vary = (vary==PAR_VAR)
275 if ((vary == PAR_VAR or vary == PAR_FIX) and
276 self.param.expr not in (None, 'None', '')):
277 self._saved_expr = self.param.expr
278 self.param.expr = ''
279 elif (vary == PAR_CON and self.param.expr in (None, 'None', '')):
280 self.param.expr = self._saved_expr
282 if self.value is not None:
283 self.value.Enable(vary not in (PAR_CON, PAR_SKIP))
284 if self.expr is not None:
285 self.expr.Enable(vary==PAR_CON)
286 if self.minval is not None:
287 self.minval.Enable(vary not in (PAR_FIX, PAR_SKIP))
288 if self.maxval is not None:
289 self.maxval.Enable(vary not in (PAR_FIX, PAR_SKIP))
290 if self.bounds is not None:
291 self.bounds.Enable(vary not in (PAR_FIX, PAR_SKIP))
294class ParameterDialog(wx.Dialog):
295 """Dialog (modal, that is, block) for Parameter Configuration"""
296 def __init__(self, parent, param, precision=4, vary=None,
297 with_skip=False, **kws):
298 self.param = param
299 title = " Parameter: %s " % (param.name)
300 wx.Dialog.__init__(self, parent, wx.ID_ANY, title=title)
301 panel = GridPanel(self)
302 self.SetFont(parent.GetFont())
304 if vary is None:
305 vary = 0
306 if param.vary:
307 vary = 1
308 elif param.expr is not None:
309 vary = 2
311 minval, maxval = param.min, param.max
312 stderr, expr = param.stderr, param.expr
313 sminval = "%s" % minval
314 smaxval = "%s" % maxval
315 if minval in (None, 'None', -np.inf): minval = -np.inf
316 if maxval in (None, 'None', np.inf): maxval = np.inf
317 if stderr is None: stderr = ''
318 if expr is None: expr = ''
320 self.wids = Empty()
321 vchoices = VARY_CHOICES_SKIP if with_skip else VARY_CHOICES
322 self.wids.vary = Choice(panel, choices=vchoices,
323 action=self.onVaryChoice, size=(110, -1))
324 self.wids.vary.SetSelection(vary)
326 self.wids.val = FloatCtrl(panel, value=param.value, size=(100, -1),
327 precision=precision,
328 minval=minval, maxval=maxval)
329 self.wids.min = FloatCtrl(panel, value=minval, size=(100, -1))
330 self.wids.max = FloatCtrl(panel, value=maxval, size=(100, -1))
331 self.wids.expr = wx.TextCtrl(panel, value=expr, size=(300, -1))
332 self.wids.err = wx.StaticText(panel, label="%s" % stderr)
334 SetTip(self.wids.expr, "Mathematical expression to calcutate value")
336 btnsizer = wx.StdDialogButtonSizer()
337 ok_btn = wx.Button(panel, wx.ID_OK)
338 ok_btn.SetDefault()
339 btnsizer.AddButton(ok_btn)
340 btnsizer.AddButton(wx.Button(panel, wx.ID_CANCEL))
341 btnsizer.Realize()
343 panel.AddText(' Name:', style=LEFT)
344 panel.AddText(param.name, style=LEFT)
345 panel.AddText(' Type:', style=LEFT)
346 panel.Add(self.wids.vary, style=LEFT)
347 panel.AddText(' Value:', style=LEFT, newrow=True)
348 panel.Add(self.wids.val, style=LEFT)
349 panel.AddText(' Std Error:', style=LEFT)
350 panel.Add(self.wids.err, style=LEFT)
351 panel.AddText(' Min Value:', style=LEFT, newrow=True)
352 panel.Add(self.wids.min, style=LEFT)
353 panel.AddText(' Max Value:', style=LEFT)
354 panel.Add(self.wids.max, style=LEFT)
355 panel.AddText(' Constraint:', style=LEFT, newrow=True)
356 panel.Add(self.wids.expr, style=LEFT, dcol=3)
358 panel.Add(HLine(panel, size=(375, 2)), dcol=4, newrow=True)
359 panel.Add(btnsizer, dcol=4, newrow=True, style=LEFT)
360 panel.pack()
362 sizer = wx.BoxSizer(wx.VERTICAL)
363 sizer.Add(panel, 0, 0, 25)
364 self.onVaryChoice()
365 pack(self, sizer)
366 bsize = self.GetBestSize()
367 self.SetSize((bsize[0]+10, bsize[1]+10))
369 def onVaryChoice(self, evt=None):
371 vary = self.wids.vary.GetStringSelection()
372 if vary == PAR_CON:
373 self.wids.val.Disable()
374 self.wids.expr.Enable()
375 else:
376 self.wids.val.Enable()
377 self.wids.expr.Disable()
380class ParameterPanel(wx.Panel):
381 """wx.Panel for a Larch Parameter
383 param = Parameter(value=11.22, vary=True, min=0, name='x1')
384 wid = ParameterPanel(parent_wid, param)
385 """
386 def __init__(self, parent, param, size=(80, -1), show_name=False,
387 precision=4, with_skip=False, **kws):
388 self.param = param
389 self.precision = precision
390 wx.Panel.__init__(self, parent, -1)
391 self.wids = Empty()
393 self.wids.val = FloatCtrl(self, value=param.value,
394 minval=param.min, maxval=param.max,
395 precision=precision, size=size)
397 self.wids.name = None
398 self.wids.edit = wx.Button(self, label='edit', size=(45, 25))
399 self.wids.edit.Bind(wx.EVT_BUTTON, self.onConfigure)
400 SetTip(self.wids.edit, "Configure Parameter")
402 vchoices = VARY_CHOICES_SKIP if with_skip else VARY_CHOICES
403 self.wids.vary = Choice(self, choices=vchoices,
404 action=self.onVaryChoice, size=(80, -1))
406 vary_choice = 0
407 if param.vary:
408 vary_choice = 1
409 elif param.expr is not None:
410 vary_choice = 2
411 self.wids.vary.SetSelection(vary_choice)
413 sizer = wx.BoxSizer(wx.HORIZONTAL)
414 CLEFT = LEFT|wx.ALL
415 if show_name:
416 self.wids.name = wx.StaticText(self,
417 label="%s: " % param.name,
418 size=(len(param.name)*8, -1))
419 sizer.Add(self.wids.name, 0, CLEFT)
421 sizer.Add(self.wids.val, 0, CLEFT)
422 sizer.Add(self.wids.vary, 0, CLEFT)
423 sizer.Add(self.wids.edit, 0, CLEFT)
424 pack(self, sizer)
426 def onVaryChoice(self, evt=None):
427 vary = self.wids.vary.GetStringSelection() # evt.GetString()
428 self.param.vary = (vary == PAR_VAR)
429 if vary == PAR_CON:
430 self.wids.val.Disable()
431 else:
432 self.wids.val.Enable()
434 def onConfigure(self, evt=None):
435 self.param.value = self.wids.val.GetValue()
436 vary = self.wids.vary.GetSelection()
437 dlg = ParameterDialog(self, self.param, vary=vary,
438 precision=self.precision)
439 dlg.Raise()
440 if dlg.ShowModal() == wx.ID_OK:
441 self.param.max = float(dlg.wids.max.GetValue())
442 self.param.min = float(dlg.wids.min.GetValue())
443 self.param.value = float(dlg.wids.val.GetValue())
444 self.wids.val.SetMax(self.param.max)
445 self.wids.val.SetMin(self.param.min)
447 self.wids.val.SetValue(self.param.value)
449 var = dlg.wids.vary.GetSelection()
450 self.wids.vary.SetSelection(var)
451 self.param.vary = False
452 if var == 1:
453 self.param.vary = True
454 elif var == 2:
455 self.param.expr= dlg.wids.expr.GetValue()
456 self.param._getval()
457 dlg.Destroy()
459class TestFrame(wx.Frame):
460 def __init__(self, parent=None, size=(-1, -1)):
461 wx.Frame.__init__(self, parent, -1, 'Parameter Panel Test',
462 size=size, style=wx.DEFAULT_FRAME_STYLE)
463 panel = GridPanel(self)
465 param1 = Parameter(value=99.0, vary=True, min=0, name='peak1_amplitude')
466 param2 = Parameter(value=110.2, vary=True, min=100, max=120, name='peak1_center')
467 param3 = Parameter(value=1.23, vary=True, min=0.5, max=2.0, name='peak1_sigma')
469 panel.Add(ParameterPanel(panel, param1, show_name=True), style=LEFT)
470 # panel.NewRow()
471 panel.Add(ParameterPanel(panel, param2, show_name=True), style=LEFT)
472 panel.Add(ParameterPanel(panel, param3, show_name=True), style=LEFT)
473 panel.pack()
474 self.createMenus()
476 self.SetSize((700, 200))
477 self.Show()
478 self.Raise()
480 def createMenus(self):
481 self.menubar = wx.MenuBar()
482 fmenu = wx.Menu()
483 im = fmenu.Append(wid, "Show Widget Frame\tCtrl+I", "")
484 sellf.Bind(wx.EVT_MENU, self.onShowInspection, im.Id)
485 self.menubar.Append(fmenu, "&File")
486 self.SetMenuBar(self.menubar)
487 self.Bind(wx.EVT_CLOSE, self.onExit)
489 def onShowInspection(self, evt=None):
490 wx.GetApp().ShowInspectionTool()
492 def onExit(self, evt=None):
493 self.Destroy()