Coverage for /Users/Newville/Codes/xraylarch/larch/wxlib/parameter.py: 13%

343 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-11-09 10:08 -0600

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""" 

8 

9import numpy as np 

10import ast 

11import wx 

12from wx.lib.embeddedimage import PyEmbeddedImage 

13 

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 

20 

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) 

27 

28BOUNDS_custom = 'custom' 

29BOUNDS_none = 'unbound' 

30BOUNDS_pos = 'positive' 

31BOUNDS_neg = 'negative' 

32BOUNDS_CHOICES = (BOUNDS_none, BOUNDS_pos, BOUNDS_neg, BOUNDS_custom) 

33 

34PAR_WIDS = ('name', 'value', 'minval', 'maxval', 

35 'vary', 'expr', 'stderr', 'bounds') 

36 

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=") 

53 

54 

55class ParameterWidgets(object): 

56 """a set of related widgets for a lmfit Parameter 

57 

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=120, stderr_size=120, float_size=80, 

63 minmax_size=60, with_skip=False, widgets=PAR_WIDS): 

64 

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) 

72 

73 self.skip = False 

74 for attr in PAR_WIDS: 

75 setattr(self, attr, None) 

76 

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 

86 

87 else: 

88 value = param.value 

89 vary_choice = PAR_VAR if param.vary else PAR_FIX 

90 

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) 

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) 

109 

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) 

121 

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) 

133 

134 if 'vary' in widgets: 

135 vchoices = VARY_CHOICES_SKIP if with_skip else VARY_CHOICES 

136 self.vary = Choice(parent, size=(90, -1), 

137 choices=vchoices, 

138 action=self.onVaryChoice) 

139 self.widgets.append(self.vary) 

140 self.vary.SetStringSelection(vary_choice) 

141 

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') 

155 

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' 

162 

163 self.expr.SetForegroundColour(fgcol) 

164 self.expr.SetBackgroundColour(bgcol) 

165 

166 

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) 

174 

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 

185 

186 self.bounds = Choice(parent, size=(90, -1), 

187 choices=BOUNDS_CHOICES, 

188 action=self.onBOUNDSChoice) 

189 self.widgets.append(self.bounds) 

190 

191 for w in self.widgets: 

192 w.SetFont(Font(FONTSIZE)) 

193 

194 self.bounds.SetStringSelection(bounds_choice) 

195 

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) 

203 

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) 

212 

213 def onValue(self, evt=None, value=None): 

214 if value is not None: 

215 self.param.value = value 

216 

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) 

230 

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) 

247 

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 

256 

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) 

265 

266 

267 def onVaryChoice(self, evt=None): 

268 if self.vary is None: 

269 return 

270 

271 vary = str(self.vary.GetStringSelection().lower()) 

272 self.skip = (vary == PAR_SKIP) 

273 

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 

281 

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)) 

292 

293 

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()) 

303 

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 

310 

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 = '' 

319 

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) 

325 

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) 

333 

334 SetTip(self.wids.expr, "Mathematical expression to calcutate value") 

335 

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() 

342 

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) 

357 

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() 

361 

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)) 

368 

369 def onVaryChoice(self, evt=None): 

370 

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() 

378 

379 

380class ParameterPanel(wx.Panel): 

381 """wx.Panel for a Larch Parameter 

382 

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() 

392 

393 self.wids.val = FloatCtrl(self, value=param.value, 

394 minval=param.min, maxval=param.max, 

395 precision=precision, size=size) 

396 

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") 

401 

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)) 

405 

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) 

412 

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) 

420 

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) 

425 

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() 

433 

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) 

446 

447 self.wids.val.SetValue(self.param.value) 

448 

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() 

458 

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) 

464 

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') 

468 

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() 

475 

476 self.SetSize((700, 200)) 

477 self.Show() 

478 self.Raise() 

479 

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) 

488 

489 def onShowInspection(self, evt=None): 

490 wx.GetApp().ShowInspectionTool() 

491 

492 def onExit(self, evt=None): 

493 self.Destroy()