Coverage for larch/wxxas/xydata_panel.py: 0%

390 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2024-10-16 21:04 +0000

1#!/usr/bin/env python 

2""" 

3 uncategorized XY data Panel 

4""" 

5import time 

6import wx 

7import numpy as np 

8 

9from functools import partial 

10from xraydb import guess_edge, atomic_number 

11 

12from larch.utils import gformat, path_split 

13from larch.math import index_of 

14from larch.xafs.xafsutils import guess_energy_units 

15 

16from larch.wxlib import (BitmapButton, FloatCtrl, FloatSpin, get_icon, 

17 SimpleText, pack, Button, HLine, Choice, Check, 

18 GridPanel, CEN, RIGHT, LEFT, plotlabels, 

19 get_zoomlimits, set_zoomlimits) 

20 

21from larch.utils.strutils import fix_varname, fix_filename, file2groupname 

22 

23from larch.utils.physical_constants import ATOM_NAMES 

24from larch.wxlib.plotter import last_cursor_pos 

25from .taskpanel import TaskPanel, autoset_fs_increment, update_confval 

26from .config import (make_array_choice, EDGES, ATSYMS, 

27 NNORM_CHOICES, NNORM_STRINGS, NORM_METHODS) 

28 

29np.seterr(all='ignore') 

30 

31PLOTOPTS_1 = dict(style='solid', marker='None') 

32PLOTOPTS_2 = dict(style='short dashed', zorder=3, marker='None') 

33PLOTOPTS_D = dict(style='solid', zorder=2, side='right', marker='None') 

34 

35# Plot_EnergyRanges = {'full X range': None } 

36 

37PlotOne_Choices = {'XY Data': 'y', 

38 'Scaled Data': 'ynorm', 

39 'Derivative ': 'dydx', 

40 'XY Data + Derivative': 'y+dydx', 

41 'Scaled Data + Derivative': 'ynorm+dydx', 

42 } 

43 

44PlotSel_Choices = {'XY Data': 'y', 

45 'Scaled Data': 'ynorm', 

46 'Derivative': 'dydx'} 

47 

48FSIZE = 120 

49FSIZEBIG = 175 

50 

51 

52class XYDataPanel(TaskPanel): 

53 """XY Data Panel""" 

54 def __init__(self, parent, controller=None, **kws): 

55 TaskPanel.__init__(self, parent, controller, panel='xydata', **kws) 

56 

57 def build_display(self): 

58 panel = self.panel 

59 self.wids = {} 

60 self.last_plot_type = 'one' 

61 

62 trow = wx.Panel(panel) 

63 plot_sel = Button(trow, 'Plot Selected Groups', size=(175, -1), 

64 action=self.onPlotSel) 

65 plot_one = Button(trow, 'Plot Current Group', size=(175, -1), 

66 action=self.onPlotOne) 

67 

68 self.plotsel_op = Choice(trow, choices=list(PlotSel_Choices.keys()), 

69 action=self.onPlotSel, size=(300, -1)) 

70 self.plotone_op = Choice(trow, choices=list(PlotOne_Choices.keys()), 

71 action=self.onPlotOne, size=(300, -1)) 

72 

73 opts = {'digits': 2, 'increment': 0.05, 'value': 0, 'size': (FSIZE, -1)} 

74 plot_voff = self.add_floatspin('plot_voff', with_pin=False, 

75 parent=trow, 

76 action=self.onVoffset, 

77 max_val=10000, min_val=-10000, 

78 **opts) 

79 

80 vysize, vxsize = plot_sel.GetBestSize() 

81 voff_lab = wx.StaticText(parent=trow, label=' Y Offset:', size=(80, vxsize), 

82 style=wx.RIGHT|wx.ALIGN_CENTRE_HORIZONTAL|wx.ST_NO_AUTORESIZE) 

83 

84 self.plotone_op.SetSelection(0) 

85 self.plotsel_op.SetSelection(1) 

86 

87 tsizer = wx.GridBagSizer(3, 3) 

88 tsizer.Add(plot_sel, (0, 0), (1, 1), LEFT, 2) 

89 tsizer.Add(self.plotsel_op, (0, 1), (1, 1), LEFT, 2) 

90 tsizer.Add(voff_lab, (0, 2), (1, 1), RIGHT, 2) 

91 tsizer.Add(plot_voff, (0, 3), (1, 1), RIGHT, 2) 

92 tsizer.Add(plot_one, (1, 0), (1, 1), LEFT, 2) 

93 tsizer.Add(self.plotone_op, (1, 1), (1, 1), LEFT, 2) 

94 

95 pack(trow, tsizer) 

96 

97 scale = self.add_floatspin('scale', action=self.onSet_Scale, 

98 digits=6, increment=0.05, value=1.0, 

99 size=(FSIZEBIG, -1)) 

100 

101 xshift = self.add_floatspin('xshift', action=self.onSet_XShift, 

102 digits=6, increment=0.05, value=0.0, 

103 size=(FSIZEBIG, -1)) 

104 

105 self.wids['is_frozen'] = Check(panel, default=False, label='Freeze Group', 

106 action=self.onFreezeGroup) 

107 

108 use_auto = Button(panel, 'Use Default Settings', size=(200, -1), 

109 action=self.onUseDefaults) 

110 

111 def CopyBtn(name): 

112 return Button(panel, 'Copy', size=(60, -1), 

113 action=partial(self.onCopyParam, name)) 

114 

115 copy_all = Button(panel, 'Copy All Parameters', size=(200, -1), 

116 action=partial(self.onCopyParam, 'all')) 

117 

118 add_text = self.add_text 

119 HLINEWID = 700 

120 

121 panel.Add(SimpleText(panel, 'XY Data, General', 

122 size=(650, -1), **self.titleopts), style=LEFT, dcol=4) 

123 panel.Add(trow, dcol=4, newrow=True) 

124 

125 panel.Add(HLine(panel, size=(HLINEWID, 3)), dcol=4, newrow=True) 

126 

127 add_text('XY Data:') 

128 panel.Add(use_auto, dcol=1) 

129 panel.Add(SimpleText(panel, 'Copy to Selected Groups:'), style=RIGHT, dcol=2) 

130 

131 add_text('Scale Factor:') 

132 panel.Add(scale) 

133 panel.Add(SimpleText(panel, 'Scaled Data = Y /(Scale Factor)')) 

134 panel.Add(CopyBtn('scale'), dcol=1, style=RIGHT) 

135 

136 add_text('Shift X scale:' ) 

137 panel.Add(xshift) 

138 panel.Add(CopyBtn('xshift'), dcol=2, style=RIGHT) 

139 

140 panel.Add(HLine(panel, size=(HLINEWID, 3)), dcol=4, newrow=True) 

141 panel.Add(self.wids['is_frozen'], newrow=True) 

142 panel.Add(copy_all, dcol=3, style=RIGHT) 

143 

144 panel.pack() 

145 

146 sizer = wx.BoxSizer(wx.VERTICAL) 

147 sizer.Add((5, 5), 0, LEFT, 3) 

148 sizer.Add(panel, 0, LEFT, 3) 

149 sizer.Add((5, 5), 0, LEFT, 3) 

150 pack(self, sizer) 

151 

152 def get_config(self, dgroup=None): 

153 """custom get_config""" 

154 if dgroup is None: 

155 dgroup = self.controller.get_group() 

156 if dgroup is None: 

157 return self.get_defaultconfig() 

158 self.read_form() 

159 

160 defconf = self.get_defaultconfig() 

161 conf = getattr(dgroup.config, self.configname, defconf) 

162 

163 for k, v in defconf.items(): 

164 if k not in conf: 

165 conf[k] = v 

166 

167 fname = getattr(dgroup, 'filename', None) 

168 if fname is None: 

169 fname = getattr(dgroup, 'groupname', None) 

170 if fname is None: 

171 fname =file2groupname('unknown_group', 

172 symtable=self._larch.symtable) 

173 

174 for attr in ('scale', 'xshift'): 

175 conf[attr] = getattr(dgroup, attr, conf[attr]) 

176 

177 setattr(dgroup.config, self.configname, conf) 

178 return conf 

179 

180 def fill_form(self, dgroup): 

181 """fill in form from a data group""" 

182 opts = self.get_config(dgroup) 

183 self.skip_process = True 

184 

185 self.plotone_op.SetChoices(list(PlotOne_Choices.keys())) 

186 self.plotsel_op.SetChoices(list(PlotSel_Choices.keys())) 

187 

188 self.wids['scale'].SetValue(opts['scale']) 

189 self.wids['xshift'].SetValue(opts['xshift']) 

190 

191 frozen = opts.get('is_frozen', False) 

192 frozen = getattr(dgroup, 'is_frozen', frozen) 

193 

194 self.wids['is_frozen'].SetValue(frozen) 

195 self._set_frozen(frozen) 

196 wx.CallAfter(self.unset_skip_process) 

197 

198 def unset_skip_process(self): 

199 self.skip_process = False 

200 

201 def read_form(self): 

202 "read form, return dict of values" 

203 form_opts = {} 

204 form_opts['scale'] = self.wids['scale'].GetValue() 

205 form_opts['xshift'] = self.wids['xshift'].GetValue() 

206 return form_opts 

207 

208 

209 def _set_frozen(self, frozen): 

210 try: 

211 dgroup = self.controller.get_group() 

212 dgroup.is_frozen = frozen 

213 except: 

214 pass 

215 

216 for wattr in ('scale',): 

217 self.wids[wattr].Enable(not frozen) 

218 

219 def onFreezeGroup(self, evt=None): 

220 self._set_frozen(evt.IsChecked()) 

221 

222 def onPlotEither(self, evt=None): 

223 if self.last_plot_type == 'multi': 

224 self.onPlotSel(evt=evt) 

225 else: 

226 self.onPlotOne(evt=evt) 

227 

228 def onPlotOne(self, evt=None): 

229 self.last_plot_type = 'one' 

230 self.plot(self.controller.get_group()) 

231 wx.CallAfter(self.controller.set_focus) 

232 

233 def onVoffset(self, evt=None): 

234 time.sleep(0.002) 

235 wx.CallAfter(self.onPlotSel) 

236 

237 def onPlotSel(self, evt=None): 

238 newplot = True 

239 self.last_plot_type = 'multi' 

240 group_ids = self.controller.filelist.GetCheckedStrings() 

241 if len(group_ids) < 1: 

242 return 

243 last_id = group_ids[-1] 

244 

245 groupname = self.controller.file_groups[str(last_id)] 

246 dgroup = self.controller.get_group(groupname) 

247 

248 plot_choices = PlotSel_Choices 

249 

250 ytitle = self.plotsel_op.GetStringSelection() 

251 yarray_name = plot_choices.get(ytitle, 'ynorm') 

252 ylabel = getattr(plotlabels, yarray_name, ytitle) 

253 xlabel = getattr(dgroup, 'plot_xlabel', getattr(plotlabels, 'xplot')) 

254 

255 voff = self.wids['plot_voff'].GetValue() 

256 plot_traces = [] 

257 newplot = True 

258 plotopts = self.controller.get_plot_conf() 

259 popts = {'style': 'solid', 'marker': None} 

260 popts['linewidth'] = plotopts.pop('linewidth') 

261 popts['marksize'] = plotopts.pop('markersize') 

262 popts['grid'] = plotopts.pop('show_grid') 

263 popts['fullbox'] = plotopts.pop('show_fullbox') 

264 

265 for ix, checked in enumerate(group_ids): 

266 groupname = self.controller.file_groups[str(checked)] 

267 dgroup = self.controller.get_group(groupname) 

268 if dgroup is None: 

269 continue 

270 trace = {'xdata': dgroup.xplot, 

271 'ydata': getattr(dgroup, yarray_name) + ix*voff, 

272 'label': dgroup.filename, 'new': newplot} 

273 trace.update(popts) 

274 plot_traces.append(trace) 

275 newplot = False 

276 

277 ppanel = self.controller.get_display(stacked=False).panel 

278 zoom_limits = get_zoomlimits(ppanel, dgroup) 

279 

280 nplot_traces = len(ppanel.conf.traces) 

281 nplot_request = len(plot_traces) 

282 if nplot_request > nplot_traces: 

283 linecolors = ppanel.conf.linecolors 

284 ncols = len(linecolors) 

285 for i in range(nplot_traces, nplot_request+5): 

286 ppanel.conf.init_trace(i, linecolors[i%ncols], 'dashed') 

287 

288 

289 ppanel.plot_many(plot_traces, xlabel=plotlabels.xplot, ylabel=ylabel, 

290 zoom_limits=zoom_limits, show_legend=True) 

291 set_zoomlimits(ppanel, zoom_limits) or ppanel.unzoom_all() 

292 ppanel.canvas.draw() 

293 wx.CallAfter(self.controller.set_focus) 

294 

295 def onUseDefaults(self, evt=None): 

296 self.wids['scale'].SetValue(1.0) 

297 self.wids['xshift'].SetValue(0.0) 

298 

299 

300 def onCopyAuto(self, evt=None): 

301 opts = dict(scale=1) 

302 for checked in self.controller.filelist.GetCheckedStrings(): 

303 groupname = self.controller.file_groups[str(checked)] 

304 grp = self.controller.get_group(groupname) 

305 if grp != self.controller.group and not getattr(grp, 'is_frozen', False): 

306 self.update_config(opts, dgroup=grp) 

307 self.fill_form(grp) 

308 self.process(grp, force=True) 

309 

310 

311 def onSaveConfigBtn(self, evt=None): 

312 conf = self.get_config() 

313 conf.update(self.read_form()) 

314 

315 

316 def onCopyParam(self, name=None, evt=None): 

317 conf = self.get_config() 

318 form = self.read_form() 

319 conf.update(form) 

320 dgroup = self.controller.get_group() 

321 self.update_config(conf) 

322 self.fill_form(dgroup) 

323 opts = {} 

324 name = str(name) 

325 def copy_attrs(*args): 

326 for a in args: 

327 opts[a] = conf[a] 

328 if name == 'all': 

329 copy_attrs('scale') 

330 elif name == 'scale': 

331 copy_attrs('scale') 

332 

333 for checked in self.controller.filelist.GetCheckedStrings(): 

334 groupname = self.controller.file_groups[str(checked)] 

335 grp = self.controller.get_group(groupname) 

336 if grp != self.controller.group and not getattr(grp, 'is_frozen', False): 

337 self.update_config(opts, dgroup=grp) 

338 for key, val in opts.items(): 

339 if hasattr(grp, key): 

340 setattr(grp, key, val) 

341 self.fill_form(grp) 

342 self.process(grp, force=True) 

343 

344 def onSet_Scale(self, evt=None, value=None): 

345 "handle setting scale" 

346 scale = self.wids['scale'].GetValue() 

347 if scale < 0: 

348 self.wids['scale'].SetValue(abs(scale)) 

349 self.update_config({'scale': self.wids['scale'].GetValue()}) 

350 autoset_fs_increment(self.wids['scale'], abs(scale)) 

351 time.sleep(0.01) 

352 wx.CallAfter(self.onReprocess) 

353 

354 def onSet_XShift(self, evt=None, value=None): 

355 "handle x shift" 

356 xshift = self.wids['xshift'].GetValue() 

357 self.update_config({'xshift': self.wids['xshift'].GetValue()}) 

358 autoset_fs_increment(self.wids['xshift'], abs(xshift)) 

359 time.sleep(0.01) 

360 wx.CallAfter(self.onReprocess) 

361 

362 def pin_callback(self, opt='__', xsel=None, relative_e0=True, **kws): 

363 """ 

364 get last selected point from a specified plot window 

365 and fill in the value for the widget defined by `opt`. 

366 

367 by default it finds the latest cursor position from the 

368 cursor history of the first 20 plot windows. 

369 """ 

370 if xsel is None or opt not in self.wids: 

371 return 

372 if opt == 'scale': 

373 self.wids['scale'].SetValue(kws['ysel']) 

374 elif opt == 'xshift': 

375 self.wids['xshift'].SetValue(kws['xsel']) 

376 time.sleep(0.01) 

377 wx.CallAfter(self.onReprocess) 

378 

379 def onReprocess(self, evt=None, value=None, **kws): 

380 "handle request reprocess" 

381 if self.skip_process: 

382 return 

383 try: 

384 dgroup = self.controller.get_group() 

385 except TypeError: 

386 return 

387 if not hasattr(dgroup.config, self.configname): 

388 return 

389 form = self.read_form() 

390 self.process(dgroup=dgroup) 

391 if self.stale_groups is not None: 

392 for g in self.stale_groups: 

393 self.process(dgroup=g, force=True) 

394 self.stale_groups = None 

395 self.onPlotEither() 

396 

397 

398 def process(self, dgroup=None, force_mback=False, force=False, use_form=True, **kws): 

399 """ handle process (pre-edge/normalize) of XAS data from XAS form 

400 """ 

401 if self.skip_process and not force: 

402 return 

403 if dgroup is None: 

404 dgroup = self.controller.get_group() 

405 if dgroup is None: 

406 return 

407 

408 self.skip_process = True 

409 conf = self.get_config(dgroup) 

410 form = self.read_form() 

411 if not use_form: 

412 form.update(self.get_defaultconfig()) 

413 

414 form['group'] = dgroup.groupname 

415 

416 self.skip_process = False 

417 scale = form.get('scale', conf.get('scale', 1.0)) 

418 xshift = form.get('xshift', conf.get('xshift', 0.0)) 

419 gname = dgroup.groupname 

420 cmds = [f"{gname:s}.scale = {scale}", 

421 f"{gname:s}.xshift = {xshift}", 

422 f"{gname:s}.xplot = {gname:s}.x+{xshift}", 

423 f"{gname:s}.ynorm = {gname:s}.y/{scale}", 

424 f"{gname:s}.dydx = gradient({gname:s}.ynorm)/gradient({gname:s}.xplot)", 

425 f"{gname:s}.d2ydx = gradient({gname:s}.dydx)/gradient({gname:s}.xplot)"] 

426 

427 self.larch_eval('\n'.join(cmds)) 

428 

429 

430 self.unset_skip_process() 

431 return 

432 

433 

434 def get_plot_arrays(self, dgroup): 

435 lab = plotlabels.ynorm 

436 if dgroup is None: 

437 return 

438 

439 dgroup.plot_y2label = None 

440 dgroup.plot_xlabel = plotlabels.xplot 

441 dgroup.plot_yarrays = [('y', PLOTOPTS_1, lab)] 

442 

443 req_attrs = ['y', 'i0', 'ynorm', 'dydx', 'd2ydx'] 

444 pchoice = PlotOne_Choices.get(self.plotone_op.GetStringSelection(), 'ynorm') 

445 

446 if pchoice in ('y', 'i0', 'ynorm', 'dydx', 'd2ydx'): 

447 lab = getattr(plotlabels, pchoice) 

448 dgroup.plot_yarrays = [(pchoice, PLOTOPTS_1, lab)] 

449 

450 elif pchoice == 'y+dydx': 

451 lab = plotlabels.y 

452 dgroup.plot_y2label = lab2 = plotlabels.dydx 

453 dgroup.plot_yarrays = [('y', PLOTOPTS_1, lab), 

454 ('dydx', PLOTOPTS_D, lab2)] 

455 elif pchoice == 'ynorm+dydx': 

456 lab = plotlabels.ynorm 

457 dgroup.plot_y2label = lab2 = plotlabels.dydx 

458 dgroup.plot_yarrays = [('ynorm', PLOTOPTS_1, lab), 

459 ('dydx', PLOTOPTS_D, lab2)] 

460 

461 elif pchoice == 'ynorm+i0': 

462 lab = plotlabels.ynorm 

463 dgroup.plot_y2label = lab2 = plotlabels.i0 

464 dgroup.plot_yarrays = [('ynorm', PLOTOPTS_1, lab), 

465 ('i0', PLOTOPTS_D, lab2)] 

466 

467 dgroup.plot_ylabel = lab 

468 needs_proc = False 

469 for attr in req_attrs: 

470 needs_proc = needs_proc or (not hasattr(dgroup, attr)) 

471 

472 if needs_proc: 

473 self.process(dgroup=dgroup, force=True) 

474 

475 y4e0 = dgroup.yplot = getattr(dgroup, dgroup.plot_yarrays[0][0], dgroup.y) 

476 dgroup.plot_extras = [] 

477 

478 popts = {'marker': 'o', 'markersize': 5, 

479 'label': '_nolegend_', 

480 'markerfacecolor': '#888', 

481 'markeredgecolor': '#A00'} 

482 

483 

484 def plot(self, dgroup, title=None, plot_yarrays=None, yoff=0, 

485 delay_draw=True, multi=False, new=True, with_extras=True, **kws): 

486 

487 if self.skip_plotting: 

488 return 

489 ppanel = self.controller.get_display(stacked=False).panel 

490 

491 plotcmd = ppanel.oplot 

492 if new: 

493 plotcmd = ppanel.plot 

494 

495 groupname = getattr(dgroup, 'groupname', None) 

496 if groupname is None: 

497 return 

498 

499 if not hasattr(dgroup, 'xplot'): 

500 print("Cannot plot group ", groupname) 

501 

502 if ((getattr(dgroup, 'plot_yarrays', None) is None or 

503 getattr(dgroup, 'dydx', None) is None or 

504 getattr(dgroup, 'd2ydx', None) is None or 

505 getattr(dgroup, 'ynorm', None) is None)): 

506 self.process(dgroup=dgroup) 

507 self.get_plot_arrays(dgroup) 

508 

509 if plot_yarrays is None and hasattr(dgroup, 'plot_yarrays'): 

510 plot_yarrays = dgroup.plot_yarrays 

511 

512 popts = self.controller.get_plot_conf() 

513 popts.update(kws) 

514 popts['grid'] = popts.pop('show_grid') 

515 popts['fullbox'] = popts.pop('show_fullbox') 

516 

517 path, fname = path_split(dgroup.filename) 

518 if 'label' not in popts: 

519 popts['label'] = dgroup.plot_ylabel 

520 

521 zoom_limits = get_zoomlimits(ppanel, dgroup) 

522 

523 popts['xlabel'] = dgroup.plot_xlabel 

524 popts['ylabel'] = dgroup.plot_ylabel 

525 if getattr(dgroup, 'plot_y2label', None) is not None: 

526 popts['y2label'] = dgroup.plot_y2label 

527 

528 plot_choices = PlotSel_Choices 

529 

530 if multi: 

531 ylabel = self.plotsel_op.GetStringSelection() 

532 yarray_name = plot_choices.get(ylabel, 'ynorm') 

533 

534 if self.is_xasgroup(dgroup): 

535 ylabel = getattr(plotlabels, yarray_name, ylabel) 

536 popts['ylabel'] = ylabel 

537 

538 plot_extras = None 

539 if new: 

540 if title is None: 

541 title = fname 

542 plot_extras = getattr(dgroup, 'plot_extras', None) 

543 

544 popts['title'] = title 

545 popts['show_legend'] = len(plot_yarrays) > 1 

546 narr = len(plot_yarrays) - 1 

547 

548 _linewidth = popts['linewidth'] 

549 for i, pydat in enumerate(plot_yarrays): 

550 yaname, yopts, yalabel = pydat 

551 popts.update(yopts) 

552 if yalabel is not None: 

553 popts['label'] = yalabel 

554 linewidht = _linewidth 

555 if 'linewidth' in popts: 

556 linewidth = popts.pop('linewidth') 

557 popts['delay_draw'] = delay_draw 

558 

559 if yaname == 'i0' and not hasattr(dgroup, yaname): 

560 dgroup.i0 = np.ones(len(dgroup.xplot)) 

561 plotcmd(dgroup.xplot, getattr(dgroup, yaname)+yoff, linewidth=linewidth, **popts) 

562 plotcmd = ppanel.oplot 

563 

564 if with_extras and plot_extras is not None: 

565 axes = ppanel.axes 

566 for etype, x, y, opts in plot_extras: 

567 if etype == 'marker': 

568 xpopts = {'marker': 'o', 'markersize': 5, 

569 'label': '_nolegend_', 

570 'markerfacecolor': 'red', 

571 'markeredgecolor': '#884444'} 

572 xpopts.update(opts) 

573 axes.plot([x], [y], **xpopts) 

574 elif etype == 'vline': 

575 xpopts = {'ymin': 0, 'ymax': 1.0, 

576 'label': '_nolegend_', 

577 'color': '#888888'} 

578 xpopts.update(opts) 

579 axes.axvline(x, **xpopts) 

580 

581 # set_zoomlimits(ppanel, zoom_limits) 

582 ppanel.reset_formats() 

583 set_zoomlimits(ppanel, zoom_limits) 

584 ppanel.conf.unzoom(full=True, delay_draw=False)