Coverage for larch/wxlib/xrfdisplay_utils.py: 18%

383 statements  

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

1#!/usr/bin/env python 

2""" 

3utilities for XRF display 

4""" 

5import copy 

6from functools import partial 

7import time 

8import numpy as np 

9 

10import wx 

11import wx.lib.colourselect as csel 

12import wx.lib.scrolledpanel as scrolled 

13from wxutils import (SimpleText, FloatCtrl, Choice, Font, pack, Button, 

14 Check, HyperText, HLine, GridPanel, CEN, LEFT, RIGHT) 

15 

16from wxmplot.colors import hexcolor 

17from xraydb import xray_line 

18from ..xrf import (xrf_calib_fitrois, xrf_calib_init_roi, 

19 xrf_calib_compute, xrf_calib_apply) 

20 

21# Group used to hold MCA data 

22XRFGROUP = '_xrfdata' 

23MAKE_XRFGROUP_CMD = "%s = group(__doc__='MCA/XRF data groups', _mca='', _mca2='')" % XRFGROUP 

24XRFRESULTS_GROUP = '_xrfresults' 

25MAKE_XRFRESULTS_GROUP = "_xrfresults = []" 

26 

27def mcaname(i): 

28 return "mca{:03d}".format(i) 

29 

30def next_mcaname(_larch): 

31 xrfgroup = _larch.symtable.get_group(XRFGROUP) 

32 i, exists = 1, True 

33 name = "mca{:03d}".format(i) 

34 while hasattr(xrfgroup, name): 

35 i += 1 

36 name = "mca{:03d}".format(i) 

37 return name 

38 

39 

40class XRFCalibrationFrame(wx.Frame): 

41 def __init__(self, parent, mca, size=(-1, -1), callback=None): 

42 self.mca = mca 

43 self.callback = callback 

44 wx.Frame.__init__(self, parent, -1, 'Calibrate MCA', 

45 size=size, style=wx.DEFAULT_FRAME_STYLE) 

46 

47 opanel = scrolled.ScrolledPanel(self) 

48 osizer = wx.BoxSizer(wx.VERTICAL) 

49 panel = GridPanel(opanel) 

50 self.calib_updated = False 

51 panel.AddText("Calibrate MCA Energy (Energies in eV)", 

52 colour='#880000', dcol=7) 

53 panel.AddText("ROI", newrow=True, style=CEN) 

54 panel.AddText("Predicted", style=CEN) 

55 panel.AddText("Peaks with Current Calibration", dcol=3, style=CEN) 

56 panel.AddText("Peaks with Refined Calibration", dcol=3, style=CEN) 

57 

58 panel.AddText("Name", newrow=True, style=CEN) 

59 panel.AddText("Energy", style=CEN) 

60 panel.AddText("Center", style=CEN) 

61 panel.AddText("Difference", style=CEN) 

62 panel.AddText("FWHM", style=CEN) 

63 panel.AddText("Center", style=CEN) 

64 panel.AddText("Difference", style=CEN) 

65 panel.AddText("FWHM", style=CEN) 

66 panel.AddText("Use? ", style=CEN) 

67 

68 panel.Add(HLine(panel, size=(700, 3)), dcol=9, newrow=True) 

69 self.wids = [] 

70 

71 # find ROI peak positions 

72 self.init_wids = {} 

73 for roi in self.mca.rois: 

74 eknown, ecen, fwhm = 1, 1, 1 

75 

76 words = roi.name.split() 

77 elem = words[0].title() 

78 family = 'ka' 

79 if len(words) > 1: 

80 family = words[1] 

81 try: 

82 eknown = xray_line(elem, family).energy/1000.0 

83 except (AttributeError, ValueError): 

84 eknown = 0.0001 

85 mid = (roi.right + roi.left)/2 

86 wid = (roi.right - roi.left)/2 

87 ecen = mid * mca.slope + mca.offset 

88 fwhm = 2.354820 * wid * mca.slope 

89 

90 diff = ecen - eknown 

91 name = (' ' + roi.name+' '*10)[:10] 

92 opts = {'style': CEN, 'size':(75, -1)} 

93 w_name = SimpleText(panel, name, **opts) 

94 w_pred = SimpleText(panel, "% .1f" % (1000*eknown), **opts) 

95 w_ccen = SimpleText(panel, "% .1f" % (1000*ecen), **opts) 

96 w_cdif = SimpleText(panel, "% .1f" % (1000*diff), **opts) 

97 w_cwid = SimpleText(panel, "% .1f" % (1000*fwhm), **opts) 

98 w_ncen = SimpleText(panel, "-----", **opts) 

99 w_ndif = SimpleText(panel, "-----", **opts) 

100 w_nwid = SimpleText(panel, "-----", **opts) 

101 w_use = Check(panel, label='', size=(40, -1), default=0) 

102 panel.Add(w_name, style=LEFT, newrow=True) 

103 panel.AddMany((w_pred, w_ccen, w_cdif, w_cwid, 

104 w_ncen, w_ndif, w_nwid, w_use)) 

105 self.init_wids[roi.name] = [False, w_pred, w_ccen, w_cdif, w_cwid, w_use] 

106 self.wids.append((roi.name, eknown, ecen, w_ncen, w_ndif, w_nwid, w_use)) 

107 

108 panel.Add(HLine(panel, size=(700, 3)), dcol=9, newrow=True) 

109 offset = 1000.0*self.mca.offset 

110 slope = 1000.0*self.mca.slope 

111 panel.AddText("Current Calibration:", dcol=2, newrow=True) 

112 panel.AddText("offset(eV):") 

113 panel.AddText("%.3f" % (offset), dcol=1, style=RIGHT) 

114 panel.AddText("slope(eV/chan):") 

115 panel.AddText("%.3f" % (slope), dcol=1, style=RIGHT) 

116 

117 panel.AddText("Refined Calibration:", dcol=2, newrow=True) 

118 self.new_offset = FloatCtrl(panel, value=offset, precision=3, 

119 size=(80, -1)) 

120 self.new_slope = FloatCtrl(panel, value=slope, precision=3, 

121 size=(80, -1)) 

122 panel.AddText("offset(eV):") 

123 panel.Add(self.new_offset, dcol=1, style=RIGHT) 

124 panel.AddText("slope(eV/chan):") 

125 panel.Add(self.new_slope, dcol=1, style=RIGHT) 

126 

127 self.calib_btn = Button(panel, 'Compute Calibration', 

128 size=(175, -1), action=self.onCalibrate) 

129 self.calib_btn.Disable() 

130 panel.Add(self.calib_btn, dcol=3, newrow=True) 

131 panel.Add(Button(panel, 'Use New Calibration', 

132 size=(175, -1), action=self.onUseCalib), 

133 dcol=3, style=RIGHT) 

134 panel.Add(Button(panel, 'Done', 

135 size=(125, -1), action=self.onClose), 

136 dcol=2, style=RIGHT) 

137 panel.pack() 

138 a = panel.GetBestSize() 

139 self.SetSize((a[0]+25, a[1]+50)) 

140 osizer.Add(panel) 

141 pack(opanel, osizer) 

142 opanel.SetupScrolling() 

143 self.Show() 

144 self.Raise() 

145 self.init_proc = False 

146 self.init_t0 = time.time() 

147 self.init_timer = wx.Timer(self) 

148 self.Bind(wx.EVT_TIMER, self.onInitTimer, self.init_timer) 

149 self.init_timer.Start(2) 

150 

151 def onInitTimer(self, evt=None): 

152 """initial calibration""" 

153 if self.init_proc: 

154 # print("skipping in init_proc...") 

155 return 

156 nextroi = None 

157 if time.time() - self.init_t0 > 20: 

158 self.init_timer.Stop() 

159 self.init_proc = False 

160 self.calib_btn.Enable() 

161 for roiname, wids in self.init_wids.items(): 

162 if not wids[0]: 

163 nextroi = roiname 

164 break 

165 

166 if nextroi is None: 

167 self.init_timer.Stop() 

168 self.init_proc = False 

169 self.calib_btn.Enable() 

170 else: 

171 self.init_proc = True 

172 xrf_calib_init_roi(self.mca, roiname) 

173 s, w_pred, w_ccen, w_cdif, w_cwid, w_use = self.init_wids[roiname] 

174 if roiname in self.mca.init_calib: 

175 eknown, ecen, fwhm, amp, fit = self.mca.init_calib[roiname] 

176 

177 diff = ecen - eknown 

178 opts = {'style': CEN, 'size':(75, -1)} 

179 w_pred.SetLabel("% .1f" % (1000*eknown)) 

180 w_ccen.SetLabel("% .1f" % (1000*ecen)) 

181 w_cdif.SetLabel("% .1f" % (1000*diff)) 

182 w_cwid.SetLabel("% .1f" % (1000*fwhm)) 

183 if fwhm > 0.001 and fwhm < 2.00 and fit is not None: 

184 w_use.SetValue(1) 

185 

186 self.init_wids[roiname][0] = True 

187 self.init_proc = False 

188 

189 

190 def onCalibrate(self, event=None): 

191 x, y = [], [] 

192 mca = self.mca 

193 # save old calib 

194 old_calib = mca.offset, mca.slope 

195 init_calib = copy.deepcopy(mca.init_calib) 

196 for roiname, eknown, ecen, w_ncen, w_ndif, w_nwid, w_use in self.wids: 

197 if not w_use.IsChecked(): 

198 mca.init_calib.pop(roiname) 

199 w_ncen.SetLabel("-----") 

200 w_ndif.SetLabel("-----") 

201 w_nwid.SetLabel("-----") 

202 

203 

204 xrf_calib_compute(mca, apply=True) 

205 offset, slope = mca.new_calib 

206 self.calib_updated = True 

207 self.new_offset.SetValue("% .3f" % (1000*offset)) 

208 self.new_slope.SetValue("% .3f" % (1000*slope)) 

209 

210 # find ROI peak positions using this new calibration 

211 xrf_calib_fitrois(mca) 

212 for roi in self.mca.rois: 

213 try: 

214 eknown, ecen, fwhm, amp, fit = mca.init_calib[roi.name] 

215 except: 

216 continue 

217 diff = ecen - eknown 

218 for roiname, eknown, ocen, w_ncen, w_ndif, w_nwid, w_use in self.wids: 

219 if roiname == roi.name and w_use.IsChecked(): 

220 w_ncen.SetLabel("%.1f" % (1000*ecen)) 

221 w_ndif.SetLabel("% .1f" % (1000*diff)) 

222 w_nwid.SetLabel("%.1f" % (1000*fwhm)) 

223 break 

224 

225 

226 # restore calibration to old values until new values are accepted 

227 xrf_calib_apply(mca, offset=old_calib[0], slope=old_calib[1]) 

228 mca.init_calib = init_calib 

229 

230 tsize = self.GetSize() 

231 self.SetSize((tsize[0]+1, tsize[1])) 

232 self.SetSize((tsize[0], tsize[1])) 

233 

234 def onUseCalib(self, event=None): 

235 mca = self.mca 

236 offset = 0.001*float(self.new_offset.GetValue()) 

237 slope = 0.001*float(self.new_slope.GetValue()) 

238 mca.new_calib = offset, slope 

239 xrf_calib_apply(mca, offset=offset, slope=slope) 

240 if callable(self.callback): 

241 self.callback(mca) 

242 self.Destroy() 

243 

244 def onClose(self, event=None): 

245 self.Destroy() 

246 

247 

248class ColorsFrame(wx.Frame): 

249 """settings frame for XRFDisplay""" 

250 def __init__(self, parent, size=(400, 300), **kws): 

251 self.parent = parent 

252 conf = parent.conf 

253 kws['style'] = wx.DEFAULT_FRAME_STYLE 

254 wx.Frame.__init__(self, parent, -1, size=size, 

255 title='XRF Color Settings', **kws) 

256 

257 panel = GridPanel(self) 

258 panel.SetFont(Font(11)) 

259 def add_color(panel, name): 

260 cval = hexcolor(getattr(conf, name)) 

261 c = csel.ColourSelect(panel, -1, "", cval, size=(35, 25)) 

262 c.Bind(csel.EVT_COLOURSELECT, partial(self.onColor, item=name)) 

263 return c 

264 

265 def scolor(txt, attr, **kws): 

266 panel.AddText(txt, size=(130, -1), style=LEFT, font=Font(11), **kws) 

267 panel.Add(add_color(panel, attr), style=LEFT) 

268 

269 panel.AddText(' XRF Display Colors', dcol=4, colour='#880000') 

270 panel.Add(HLine(panel, size=(400, 3)), dcol=4, newrow=True) 

271 scolor(' Main Spectra:', 'spectra_color', newrow=True) 

272 scolor(' Background Spectra:', 'spectra2_color') 

273 scolor(' ROIs:', 'roi_color', newrow=True) 

274 scolor(' ROI Fill:', 'roi_fillcolor') 

275 scolor(' Cursor:', 'marker_color', newrow=True) 

276 scolor(' XRF Background:', 'bgr_color') 

277 scolor(' Major X-ray Lines:', 'major_elinecolor', newrow=True) 

278 scolor(' Minor X-ray Lines:', 'minor_elinecolor') 

279 scolor(' Selected X-ray Line:', 'emph_elinecolor', newrow=True) 

280 scolor(' Held X-ray Lines:', 'hold_elinecolor') 

281 scolor(' Pileup Prediction:', 'pileup_color', newrow=True) 

282 scolor(' Escape Prediction:', 'escape_color') 

283 

284 panel.Add(HLine(panel, size=(400, 3)), dcol=4, newrow=True) 

285 panel.Add(Button(panel, 'Done', size=(80, -1), action=self.onDone), 

286 dcol=2, newrow=True) 

287 

288 panel.pack() 

289 self.SetMinSize(panel.GetBestSize()) 

290 self.Show() 

291 self.Raise() 

292 

293 def onColor(self, event=None, item=None): 

294 color = hexcolor(event.GetValue()) 

295 setattr(self.parent.conf, item, color) 

296 if item == 'spectra_color': 

297 self.parent.panel.conf.set_trace_color(color, trace=0) 

298 elif item == 'roi_color': 

299 self.parent.panel.conf.set_trace_color(color, trace=1) 

300 elif item == 'marker_color': 

301 for lmark in self.parent.cursor_markers: 

302 if lmark is not None: 

303 lmark.set_color(color) 

304 

305 elif item == 'roi_fillcolor' and self.parent.roi_patch is not None: 

306 self.parent.roi_patch.set_color(color) 

307 elif item == 'major_elinecolor': 

308 for l in self.parent.major_markers: 

309 l.set_color(color) 

310 elif item == 'minor_elinecolor': 

311 for l in self.parent.minor_markers: 

312 l.set_color(color) 

313 elif item == 'hold_elinecolor': 

314 for l in self.parent.hold_markers: 

315 l.set_color(color) 

316 

317 self.parent.panel.canvas.draw() 

318 self.parent.panel.Refresh() 

319 

320 def onDone(self, event=None): 

321 self.Destroy() 

322 

323class XrayLinesFrame(wx.Frame): 

324 """settings frame for XRFDisplay""" 

325 

326 k1lines = ['Ka1', 'Ka2', 'Kb1'] 

327 k2lines = ['Kb2', 'Kb3'] 

328 l1lines = ['La1', 'Lb1', 'Lb3', 'Lb4'] 

329 l2lines = ['La2', 'Ll', 'Ln', 'Lb2,15'] 

330 l3lines = ['Lg1', 'Lg2', 'Lg3'] 

331 mlines = ['Ma', 'Mb', 'Mg', 'Mz'] 

332 

333 def __init__(self, parent, size=(475, 325), **kws): 

334 self.parent = parent 

335 conf = parent.conf 

336 kws['style'] = wx.DEFAULT_FRAME_STYLE 

337 wx.Frame.__init__(self, parent, -1, size=size, 

338 title='XRF Line Selection', **kws) 

339 panel = GridPanel(self) 

340 self.checkbox = {} 

341 def add_elines(panel, lines, checked): 

342 for i in lines: 

343 cb = Check(panel, '%s ' % i, default = i in checked, 

344 action=partial(self.onLine, label=i, lines=checked)) 

345 self.checkbox[i] = cb 

346 panel.Add(cb, style=LEFT) 

347 

348 hopts = {'size': (125, -1), 'bgcolour': (250, 250, 200)} 

349 labopts = {'newrow': True, 'style': LEFT} 

350 self.linedata = {'Major K Lines:': self.k1lines, 

351 'Minor K Lines:': self.k2lines, 

352 'Major L Lines:': self.l1lines, 

353 'Minor L Lines:': self.l2lines+self.l3lines, 

354 'Major M Lines:': self.mlines} 

355 panel.AddText(' Select X-ray Emission Lines', dcol=4, colour='#880000') 

356 panel.Add(HLine(panel, size=(450, 3)), dcol=5, newrow=True) 

357 

358 panel.Add(HyperText(panel, 'Major K Lines:', 

359 action=partial(self.ToggleLines, lines=conf.K_major), 

360 **hopts), **labopts) 

361 add_elines(panel, self.k1lines, conf.K_major) 

362 

363 panel.Add(HyperText(panel, 'Minor K Lines:', 

364 action=partial(self.ToggleLines, lines=conf.K_minor), 

365 **hopts), **labopts) 

366 add_elines(panel, self.k2lines, conf.K_minor) 

367 

368 panel.Add(HyperText(panel, 'Major L Lines:', 

369 action=partial(self.ToggleLines, lines=conf.L_major), 

370 **hopts), **labopts) 

371 add_elines(panel, self.l1lines, conf.L_major) 

372 

373 panel.Add(HyperText(panel, 'Minor L Lines:', 

374 action=partial(self.ToggleLines, lines=conf.L_minor), 

375 **hopts), **labopts) 

376 add_elines(panel, self.l2lines, conf.L_minor) 

377 

378 panel.AddText(' ', **labopts) 

379 add_elines(panel, self.l3lines, conf.L_minor) 

380 

381 panel.Add(HyperText(panel, 'Major M Lines:', 

382 action=partial(self.ToggleLines, lines=conf.M_major), 

383 **hopts), **labopts) 

384 add_elines(panel, self.mlines, conf.M_major) 

385 

386 panel.AddText('Energy Range (keV): ', **labopts) 

387 fopts = {'minval':0, 'maxval':1000, 'precision':2, 'size':(75, -1)} 

388 panel.Add(FloatCtrl(panel, value=conf.e_min, 

389 action=partial(self.onErange, is_max=False), 

390 **fopts), dcol=2, style=LEFT) 

391 

392 panel.AddText(' : ') 

393 panel.Add(FloatCtrl(panel, value=conf.e_max, 

394 action=partial(self.onErange, is_max=True), 

395 **fopts), dcol=2, style=LEFT) 

396 

397 panel.Add(HLine(panel, size=(450, 3)), dcol=5, newrow=True) 

398 panel.Add(Button(panel, 'Done', size=(80, -1), action=self.onDone), 

399 newrow=True, style=LEFT) 

400 panel.pack() 

401 self.Show() 

402 self.Raise() 

403 

404 def ToggleLines(self, label=None, event=None, lines=None, **kws): 

405 if not event.leftIsDown: 

406 self.parent.Freeze() 

407 for line in self.linedata.get(label, []): 

408 check = not self.checkbox[line].IsChecked() 

409 self.checkbox[line].SetValue({True: 1, False:0}[check]) 

410 self.onLine(checked=check, label=line, lines=lines) 

411 self.parent.Thaw() 

412 

413 def onLine(self, event=None, checked=None, label=None, lines=None): 

414 if checked is None: 

415 try: 

416 checked =event.IsChecked() 

417 except: 

418 pass 

419 if checked is None: checked = False 

420 if lines is None: 

421 lines = [] 

422 

423 if label in lines and not checked: 

424 lines.remove(label) 

425 elif label not in lines and checked: 

426 lines.append(label) 

427 if self.parent.selected_elem is not None: 

428 self.parent.onShowLines(elem=self.parent.selected_elem) 

429 

430 def onErange(self, event=None, value=None, is_max=True): 

431 if value is None: return 

432 

433 val = float(value) 

434 if is_max: 

435 self.parent.conf.e_max = val 

436 else: 

437 self.parent.conf.e_min = val 

438 if self.parent.selected_elem is not None: 

439 self.parent.onShowLines(elem=self.parent.selected_elem) 

440 

441 

442 def onDone(self, event=None): 

443 self.Destroy() 

444 

445class XRFDisplayConfig: 

446 emph_elinecolor = '#444444' 

447 major_elinecolor = '#DAD8CA' 

448 minor_elinecolor = '#F4DAC0' 

449 hold_elinecolor = '#CAC8DA' 

450 marker_color = '#77BB99' 

451 roi_fillcolor = '#F8F0BA' 

452 roi_color = '#d62728' 

453 spectra_color = '#1f77b4' 

454 spectra2_color = '#2ca02c' 

455 bgr_color = '#ff7f0e' 

456 fit_color = '#d62728' 

457 pileup_color = '#555555' 

458 escape_color = '#F07030' 

459 

460 K_major = ['Ka1', 'Ka2', 'Kb1'] 

461 K_minor = ['Kb3', 'Kb2'] 

462 K_minor = [] 

463 L_major = ['La1', 'Lb1', 'Lb3', 'Lb4'] 

464 L_minor = ['Ln', 'Ll', 'Lb2,15', 'Lg2', 'Lg3', 'Lg1', 'La2'] 

465 L_minor = [] 

466 M_major = ['Ma', 'Mb', 'Mg', 'Mz'] 

467 e_min = 1.00 

468 e_max = 30.0 

469 

470 

471class ROI_Averager(): 

472 """ROI averager (over a fixed number of event samples) 

473 to give a rolling average of 'recent ROI values' 

474 

475 roi_buff = ROI_Averager('13SDD1:mca1.R12', nsamples=11) 

476 while True: 

477 print( roi_buff.average()) 

478 

479 typically, the ROIs update at a fixed 10 Hz, so 11 samples 

480 gives the ROI intensity integrated over the previous second 

481 

482 using a ring buffer using numpy arrays 

483 """ 

484 def __init__(self, nsamples=11): 

485 self.clear(nsamples = nsamples) 

486 

487 def clear(self, nsamples=11): 

488 self.nsamples = nsamples 

489 self.index = -1 

490 self.lastval = 0 

491 self.toffset = time.time() 

492 self.data = np.zeros(self.nsamples, dtype=np.float32) 

493 self.times = -np.ones(self.nsamples, dtype=np.float32) 

494 

495 def append(self, value): 

496 "adds value to ring buffer" 

497 idx = self.index = (self.index + 1) % self.data.size 

498 self.data[idx] = max(0, value - self.lastval) 

499 self.lastval = value 

500 dt = time.time() - self.toffset 

501 # avoid first time point 

502 if (idx == 0 and max(self.times) < 0): 

503 dt = 0 

504 self.times[idx] = dt 

505 

506 def update(self, value): 

507 self.append(value) 

508 

509 def get_mean(self): 

510 valid = np.where(self.times > 0)[0] 

511 return self.data[valid].mean() 

512 

513 def get_cps(self): 

514 valid = np.where(self.times > 0)[0] 

515 if len(valid) < 1 or np.ptp(self.times[valid]) < 0.5: 

516 return 0 

517 return self.data[valid].sum() / np.ptp(self.times[valid])