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
« 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
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)
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)
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 = []"
27def mcaname(i):
28 return "mca{:03d}".format(i)
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
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)
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)
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)
68 panel.Add(HLine(panel, size=(700, 3)), dcol=9, newrow=True)
69 self.wids = []
71 # find ROI peak positions
72 self.init_wids = {}
73 for roi in self.mca.rois:
74 eknown, ecen, fwhm = 1, 1, 1
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
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))
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)
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)
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)
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
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]
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)
186 self.init_wids[roiname][0] = True
187 self.init_proc = False
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("-----")
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))
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
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
230 tsize = self.GetSize()
231 self.SetSize((tsize[0]+1, tsize[1]))
232 self.SetSize((tsize[0], tsize[1]))
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()
244 def onClose(self, event=None):
245 self.Destroy()
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)
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
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)
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')
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)
288 panel.pack()
289 self.SetMinSize(panel.GetBestSize())
290 self.Show()
291 self.Raise()
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)
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)
317 self.parent.panel.canvas.draw()
318 self.parent.panel.Refresh()
320 def onDone(self, event=None):
321 self.Destroy()
323class XrayLinesFrame(wx.Frame):
324 """settings frame for XRFDisplay"""
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']
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)
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)
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)
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)
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)
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)
378 panel.AddText(' ', **labopts)
379 add_elines(panel, self.l3lines, conf.L_minor)
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)
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)
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)
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()
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()
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 = []
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)
430 def onErange(self, event=None, value=None, is_max=True):
431 if value is None: return
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)
442 def onDone(self, event=None):
443 self.Destroy()
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'
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
471class ROI_Averager():
472 """ROI averager (over a fixed number of event samples)
473 to give a rolling average of 'recent ROI values'
475 roi_buff = ROI_Averager('13SDD1:mca1.R12', nsamples=11)
476 while True:
477 print( roi_buff.average())
479 typically, the ROIs update at a fixed 10 Hz, so 11 samples
480 gives the ROI intensity integrated over the previous second
482 using a ring buffer using numpy arrays
483 """
484 def __init__(self, nsamples=11):
485 self.clear(nsamples = nsamples)
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)
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
506 def update(self, value):
507 self.append(value)
509 def get_mean(self):
510 valid = np.where(self.times > 0)[0]
511 return self.data[valid].mean()
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])