Coverage for larch/wxlib/xrfdisplay_fitpeaks.py: 8%
1096 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"""
3fitting GUI for XRF display
4"""
5import sys
6import time
7import copy
8from functools import partial
9from threading import Thread
10from pathlib import Path
11import json
12import numpy as np
13import wx
14import wx.lib.agw.pycollapsiblepane as CP
15import wx.lib.scrolledpanel as scrolled
16import wx.dataview as dv
17DVSTYLE = dv.DV_SINGLE|dv.DV_VERT_RULES|dv.DV_ROW_LINES
19from lmfit import Parameter, Minimizer
21from . import (SimpleText, FloatCtrl, FloatSpin, Choice, Font, pack,
22 Button, Check, HLine, GridPanel, RowPanel, CEN, LEFT,
23 RIGHT, FileSave, GUIColors, FRAMESTYLE, BitmapButton,
24 SetTip, GridPanel, Popup, FloatSpinWithPin, get_icon,
25 fix_filename, flatnotebook, PeriodicTablePanel,
26 FONTSIZE, FONTSIZE_FW)
28from xraydb import (material_mu, xray_edge, materials, add_material,
29 atomic_number, atomic_symbol, xray_line)
30# from .notebooks import flatnotebook
31# from .periodictable import PeriodicTablePanel
32from .parameter import ParameterPanel
34from larch import Group
36from ..math import peak_indices
37from ..xrf import xrf_background, MCA, FanoFactors
38from ..utils import json_dump, json_load, gformat
39from ..utils.jsonutils import encode4js, decode4js
40from ..utils.physical_constants import E_MASS
41from ..site_config import user_larchdir
42from .xrfdisplay_utils import (XRFGROUP, mcaname, XRFRESULTS_GROUP,
43 MAKE_XRFRESULTS_GROUP)
45def read_filterdata(flist, _larch):
46 """ read filters data"""
47 materials = _larch.symtable.get_symbol('_xray._materials')
48 out = {}
49 out['None'] = ('', 0)
50 for name in flist:
51 if name in materials:
52 out[name] = materials[name]
53 return out
55def VarChoice(p, default=0, size=(90, -1)):
56 if default in (False, 'False', 'Fix', 'No'):
57 default = 0
58 else:
59 default = 1
60 return Choice(p, choices=['Fix', 'Vary'],
61 size=size, default=default)
63NFILTERS = 4
64MIN_CORREL = 0.10
66tooltips = {'ptable': 'Select Elements to include in model',
67 'cen': 'centroid of peak',
68 'step': 'size of step extending to low energy side of peak, fraction of peak height',
69 'gamma': 'gamma (lorentzian-like weight) of Voigt function',
70 'tail': 'intensity of tail function at low energy side of peak',
71 'beta': 'width of tail function at low energy side of peak',
72 'sigma': 'scale sigma from Energy/Noise by this amount',
73 }
75CompositionUnits = ('ng/mm^2', 'wt %', 'ppm')
77Detector_Materials = ['Si', 'Ge']
78EFano_Text = 'Peak Widths: sigma = sqrt(E_Fano * Energy + Noise**2) '
79Geom_Text = 'Angles in degrees: 90=normal to surface, 0=grazing surface'
80Energy_Text = 'All energies in keV'
83FitTols = ['1.e-2', '3.e-3', '1.e-3', '3.e-4', '1.e-4', '3.e-5', '1.e-5',
84 '3.e-6', '1.e-6', '3.e-7', '1.e-7']
85FitSteps = ['1.e-2', '3.e-3', '1.e-3', '3.e-4', '1.e-4', '3.e-5', '1.e-5',
86 '3.e-6', '1.e-6', '3.e-7', '1.e-7']
88FitMaxNFevs = ['200', '500', '1000', '1500', '2000', '3000', '5000', '10000']
90xrfmod_setup = """### XRF Model: {mca_label:s} @ {datetime:s}
91# mca data group for fit:
92{XRFGROUP}.workmca = {mcagroup}
94# setup XRF Model:
95_xrfmodel = xrf_model(xray_energy={en_xray:.2f}, count_time={count_time:.5f},
96 energy_min={en_min:.2f}, energy_max={en_max:.2f})
98_xrfmodel.set_detector(thickness={det_thk:.5f}, material='{det_mat:s}',
99 cal_offset={cal_offset:.5f}, cal_slope={cal_slope:.5f},
100 vary_cal_offset={cal_vary!r}, vary_cal_slope={cal_vary!r},
101 peak_step={peak_step:.5f}, vary_peak_step={peak_step_vary:s},
102 peak_tail={peak_tail:.5f}, vary_peak_tail={peak_tail_vary:s},
103 peak_beta={peak_beta:.5f}, vary_peak_beta={peak_beta_vary:s},
104 peak_gamma={peak_gamma:.5f}, vary_peak_gamma={peak_gamma_vary:s},
105 noise={det_noise:.5f}, vary_noise={det_noise_vary:s})"""
107xrfmod_scattpeak = """
108# add scatter peak
109_xrfmodel.add_scatter_peak(name='{peakname:s}', center={_cen:.2f},
110 amplitude=1e5, step={_step:.5f}, tail={_tail:.5f}, beta={_beta:.5f},
111 sigmax={_sigma:.5f}, vary_center={vcen:s}, vary_step={vstep:s},
112 vary_tail={vtail:s}, vary_beta={vbeta:s}, vary_sigmax={vsigma:s})"""
114xrfmod_fitscript = """
115# run XRF fit, save results
116_xrffitresult = _xrfmodel.fit_spectrum({XRFGROUP}.workmca, energy_min={emin:.2f}, energy_max={emax:.2f},
117 fit_toler={fit_toler:.6g}, fit_step={fit_step:.6g}, max_nfev={max_nfev:d})
118_xrfresults.insert(0, _xrffitresult)
119########
120"""
122xrfmod_filter = "_xrfmodel.add_filter('{name:s}', {thick:.5f}, vary_thickness={vary:s})"
123xrfmod_matrix = "_xrfmodel.set_matrix('{name:s}', {thick:.5f}, density={density:.5f})"
124xrfmod_pileup = "_xrfmodel.add_pileup(scale={scale:.3f}, vary={vary:s})"
125xrfmod_escape = "_xrfmodel.add_escape(scale={scale:.3f}, vary={vary:s})"
127xrfmod_savejs = "_xrfresults[{nfit:d}].save('{filename:s}')"
129xrfmod_elems = """
130# add elements
131for atsym in {elemlist:s}:
132 _xrfmodel.add_element(atsym)
133#endfor
134del atsym"""
136Filter_Lengths = ['microns', 'mm', 'cm']
137Filter_Materials = ['None', 'air', 'nitrogen', 'helium', 'kapton',
138 'beryllium', 'aluminum', 'mylar', 'pmma']
141DEF_CONFIG = { "mca_name": "", "escape_use": True, "escape_amp": 0.25,
142 "pileup_use": True, "pileup_amp": 0.2, "escape_amp_vary":
143 True, "pileup_amp_vary": True, "cal_slope": 0.01,
144 "cal_offset": 0, "cal_vary": True, "det_mat": "Si", "det_thk":
145 0.4, "det_noise_vary": True,
146 "en_xray": 30, "en_min": 2.5, "en_max": 30,
147 "flux_in": 1e9, "det_noise": 0.035, "angle_in": 45.0,
148 "angle_out": 45.0, "det_dist": 50.0, "det_area": 50.0,
149 "elements": [16, 17, 18, 20, 22, 24, 25, 26, 28, 29, 30],
150 "filter1_mat": "beryllium", "filter1_thk": 0.025, "filter1_var": False,
151 "filter2_mat": "air", "filter2_thk": 50.0, "filter2_var": False,
152 "filter3_mat": "kapton", "filter3_thk": 0.0, "filter3_var": False,
153 "filter4_mat": "aluminum", "filter4_thk": 0.0, "filter4_var": False,
154 "matrix_mat": "", "matrix_thk": 0.0, "matrix_den": 1.0,
155 "peak_step": 0.025, "peak_gamma": 0.05, "peak_tail": 0.1,
156 "peak_beta": 0.5, "peak_step_vary": False, "peak_tail_vary": False,
157 "peak_gamma_vary": False, "peak_beta_vary": False,
158 "elastic_use": True, "elastic_cen_vary": False, "elastic_step_vary": False,
159 "elastic_beta_vary": False, "elastic_tail_vary": False,
160 "elastic_sigma_vary": False, "elastic_cen": 30,
161 "elastic_step": 0.025, "elastic_tail": 0.1,
162 "elastic_beta": 0.5, "elastic_sigma": 1.0,
163 "compton1_use": True, "compton1_cen_vary": True,
164 "compton1_step_vary": True, "compton1_beta_vary": False,
165 "compton1_tail_vary": False, "compton1_sigma_vary": False,
166 "compton1_cen": 12.1875, "compton1_step": 0.025, "compton1_tail": 0.25,
167 "compton1_beta": 2.0, "compton1_sigma": 1.5,
168 "compton2_use": True, "compton2_cen_vary": True, "compton2_step_vary": False,
169 "compton2_beta_vary": False, "compton2_tail_vary": False,
170 "compton2_sigma_vary": False, "compton2_cen": 11.875,
171 "compton2_step": 0.025, "compton2_tail": 0.25, "compton2_beta": 2.5,
172 "compton2_sigma": 2.0, "count_time": 120.0,
173 }
175class FitSpectraFrame(wx.Frame):
176 """Frame for Spectral Analysis"""
178 def __init__(self, parent, size=(750, 850)):
179 self.parent = parent
180 self._larch = parent.larch
181 symtable = self._larch.symtable
182 # fetch current spectra from parent
183 if not symtable.has_group(XRFRESULTS_GROUP):
184 self._larch.eval(MAKE_XRFRESULTS_GROUP)
186 self.xrfresults = symtable.get_symbol(XRFRESULTS_GROUP)
187 xrfgroup = symtable.get_group(XRFGROUP)
188 mcagroup = getattr(xrfgroup, '_mca')
189 self.mca = getattr(xrfgroup, mcagroup)
190 self.mcagroup = f'{XRFGROUP}.{mcagroup}'
192 self.config = DEF_CONFIG
194 opts = getattr(xrfgroup, 'fitconfig', None)
195 if opts is None:
196 xrf_conffile = Path(user_larchdir, 'xrf_fitconfig.json')
197 if xrf_conffile.exists():
198 opts = json_load(xrf_conffile.as_posix())
199 if opts is not None:
200 self.config.update(opts)
202 efactor = 1.0 if max(self.mca.energy) < 250. else 1000.0
204 if self.mca.incident_energy is None:
205 self.mca.incident_energy = 20.0
206 if self.mca.incident_energy > 250:
207 self.mca.incident_energy /= 1000.0
209 self.nfit = 0
210 self.colors = GUIColors()
211 wx.Frame.__init__(self, parent, -1, 'Fit XRF Spectra',
212 size=size, style=wx.DEFAULT_FRAME_STYLE)
214 mainfont = wx.Font(FONTSIZE_FW-1, wx.SWISS, wx.NORMAL, wx.NORMAL)
215 self.font_fixedwidth = wx.Font(FONTSIZE_FW, wx.MODERN, wx.NORMAL,
216 wx.NORMAL)
217 self.SetFont(mainfont)
219 self.wids = {}
220 self.owids = {}
222 pan = GridPanel(self)
223 self.mca_label = self.mca.label
224 self.wids['mca_name'] = SimpleText(pan, self.mca_label, size=(550, -1), style=LEFT)
225 self.wids['btn_calc'] = Button(pan, 'Calculate Model', size=(150, -1),
226 action=self.onShowModel)
227 self.wids['btn_fit'] = Button(pan, 'Fit Model', size=(150, -1),
228 action=self.onFitModel)
229 self.wids['fit_message'] = SimpleText(pan, ' ', size=(300, -1), style=LEFT)
231 pan.AddText(" XRF Spectrum: ", colour='#880000')
232 pan.Add(self.wids['mca_name'], dcol=3)
233 pan.Add(self.wids['btn_calc'], newrow=True)
234 pan.Add(self.wids['btn_fit'])
235 pan.Add(self.wids['fit_message'])
236 self.panels = {}
237 self.panels['Beam & Detector'] = self.beamdet_page
238 self.panels['Filters & Matrix'] = self.materials_page
239 self.panels['Elements & Peaks'] = self.elempeaks_page
240 self.panels['Fit Results'] = self.fitresult_page
241 self.panels['Composition'] = self.composition_page
243 self.nb = flatnotebook(pan, self.panels, on_change=self.onNBChanged)
244 pan.Add((5, 5), newrow=True)
245 pan.Add(self.nb, dcol=5, drow=10, newrow=True)
246 pan.pack()
248 self.Show()
249 self.Raise()
251 def onNBChanged(self, event=None):
252 pagelabel = self.nb._pages.GetPageText(event.GetSelection()).strip()
253 if pagelabel.startswith('Composition'):
254 self.UpdateCompositionPage()
256 def elempeaks_page(self, **kws):
257 "elements and peaks parameters"
258 mca = self.parent.mca
259 wids = self.wids
260 p = GridPanel(self)
261 self.selected_elems = []
262 ptable_fontsize = 9
264 ptpanel = wx.Panel(p)
265 self.ptable = PeriodicTablePanel(ptpanel, multi_select=True,
266 fontsize=ptable_fontsize,
267 size=(360, 180),
268 tooltip_msg=tooltips['ptable'],
269 onselect=self.onElemSelect)
270 ptsizer = wx.BoxSizer(wx.HORIZONTAL)
271 ptsizer.Add((50,50), 1, wx.ALIGN_LEFT|wx.GROW, 2)
272 ptsizer.Add(self.ptable, 0, wx.ALIGN_CENTER|wx.ALL, 2)
273 ptsizer.Add((5, 5), 1, wx.ALIGN_LEFT|wx.GROW, 2)
274 pack(ptpanel, ptsizer)
276 cnf = self.config
277 for name, xmax, xinc in (('step', 1, 0.005),
278 ('gamma', 10, 0.01),
279 ('beta', 10, 0.01),
280 ('tail', 1.0, 0.05)):
281 fname = 'peak_%s' % name
282 vname = 'peak_%s_vary' % name
283 wids[fname] = FloatSpin(p, value=cnf[fname], digits=3,
284 min_val=0, max_val=xmax,
285 increment=xinc, tooltip=tooltips[name])
286 wids[vname] = VarChoice(p, default=cnf[vname])
288 btn_from_peaks = Button(p, 'Guess Peaks', size=(175, -1),
289 action=self.onElems_GuessPeaks)
290 # tooltip='Guess elements from peak locations')
291 btn_from_rois = Button(p, 'Use ROIS as Peaks', size=(175, -1),
292 action=self.onElems_FromROIS)
293 btn_clear_elems = Button(p, 'Clear All Peaks', size=(175, -1),
294 action=self.onElems_Clear)
295 wx.CallAfter(self.onElems_GuessPeaks)
297 p.AddText('Elements to model:', colour='#880000', dcol=2)
298 p.Add(ptpanel, dcol=5, drow=5, pad=5,
299 style=wx.ALIGN_RIGHT|wx.ALL|wx.GROW,
300 newrow=True)
301 irow = p.irow
303 p.Add(btn_from_peaks, icol=5, dcol=2, irow=irow)
304 p.Add(btn_from_rois, icol=5, dcol=2, irow=irow+1)
305 p.Add(btn_clear_elems, icol=5, dcol=2, irow=irow+2)
306 p.irow += 5
308 p.Add((2, 2), newrow=True)
309 p.AddText(' Step: ')
310 p.Add(wids['peak_step'])
311 p.Add(wids['peak_step_vary'])
313 p.AddText(' Gamma : ')
314 p.Add(wids['peak_gamma'])
315 p.Add(wids['peak_gamma_vary'])
317 p.Add((2, 2), newrow=True)
318 p.AddText(' Beta: ')
319 p.Add(wids['peak_beta'])
320 p.Add(wids['peak_beta_vary'])
322 p.AddText(' Tail: ', size=(120, -1))
323 p.Add(wids['peak_tail'])
324 p.Add(wids['peak_tail_vary'])
325 p.Add((2, 2), newrow=True)
326 p.Add(HLine(p, size=(650, 3)), dcol=8)
327 p.Add((2, 2), newrow=True)
329 opts = dict(size=(100, -1), min_val=0, digits=4, increment=0.010)
330 for name, escale in (('elastic', 0), ('compton1', 1), ('compton2', 2)):
331 en = self.mca.incident_energy
332 for i in range(escale):
333 en = en * (1 - 1/(1 + (E_MASS*0.001)/en)) # Compton shift at 90 deg
335 wids[f'{name:s}_use'] = Check(p, label='Include', default=cnf[f'{name:s}_use'])
336 p.Add((2, 2), newrow=True)
337 p.AddText(" %s Peak:" % name.title(), colour='#880000')
338 p.Add(wids[f'{name:s}_use'], dcol=2)
340 for att, title, xmax, xinc, newrow in (('cen', ' Energy (kev): ', 9e12, 0.01, True),
341 ('step', ' Step: ', 1, 0.005, False),
342 ('sigma', ' Sigma Scale:', 10, 0.05, True),
343 ('beta', ' Beta: ', 10, 0.01, False),
344 ('tail', ' Tail: ', 10, 0.01, True)):
345 label = f'{name:s}_{att:s}'
346 val = en if att == 'cen' else cnf[label]
347 wids[f'{label:s}_vary'] = VarChoice(p, default=cnf[f'{label:s}_vary'])
349 wids[label] = FloatSpin(p, value=val, digits=3, min_val=0,
350 max_val=xmax, increment=xinc,
351 tooltip=tooltips[att])
353 p.AddText(title, size=(120, -1))
354 p.Add(wids[label])
355 p.Add(wids[f'{label:s}_vary'])
356 if newrow:
357 p.Add((2, 2), newrow=True)
359 p.Add(HLine(p, size=(650, 3)), dcol=7)
361 p.pack()
362 return p
364 def beamdet_page(self, **kws):
365 "beam / detector settings"
366 mca = self.mca
367 en_min = 2.0
368 en_max = self.mca.incident_energy
370 cal_offset = getattr(mca, 'offset', 0)
371 cal_slope = getattr(mca, 'slope', 0.010)
372 det_noise = getattr(mca, 'det_noise', 0.035)
373 escape_amp = getattr(mca, 'escape_amp', 0.25)
375 if not hasattr(self.mca, 'pileup_scale'):
376 self.mca.predict_pileup()
377 pileup_amp = max(0.001, getattr(mca, 'pileup_scale', 0.25))
379 wids = self.wids
380 pdet = GridPanel(self, itemstyle=LEFT)
382 def addLine(pan):
383 pan.Add(HLine(pan, size=(650, 3)), dcol=6, newrow=True)
385 wids['escape_use'] = Check(pdet, label='Include Escape in Fit',
386 default=True, action=self.onUsePileupEscape)
387 wids['escape_amp'] = FloatSpin(pdet, value=escape_amp,
388 min_val=0, max_val=100, digits=3,
389 increment=0.01, size=(125, -1))
391 wids['pileup_use'] = Check(pdet, label='Include Pileup in Fit',
392 default=True,
393 action=self.onUsePileupEscape)
394 wids['pileup_amp'] = FloatSpin(pdet, value=pileup_amp,
395 min_val=0, max_val=100, digits=3,
396 increment=0.01, size=(125, -1))
398 wids['escape_amp_vary'] = VarChoice(pdet, default=True)
399 wids['pileup_amp_vary'] = VarChoice(pdet, default=(pileup_amp>0.002))
402 wids['cal_slope'] = FloatSpin(pdet, value=cal_slope,
403 min_val=0, max_val=100,
404 digits=4, increment=0.01, size=(125, -1))
405 wids['cal_offset'] = FloatSpin(pdet, value=cal_offset,
406 min_val=-500, max_val=500,
407 digits=4, increment=0.01, size=(125, -1))
409 wids['cal_vary'] = Check(pdet, label='Vary Calibration in Fit', default=True)
411 wids['det_mat'] = Choice(pdet, choices=Detector_Materials,
412 size=(125, -1), default=0,
413 action=self.onDetMaterial)
415 wids['det_thk'] = FloatSpin(pdet, value=0.400, size=(125, -1),
416 increment=0.010, min_val=0, max_val=10,
417 digits=4)
419 wids['det_noise_vary'] = VarChoice(pdet, default=1)
421 opts = dict(size=(125, -1), min_val=0, max_val=500, digits=3,
422 increment=0.10)
423 wids['en_xray'] = FloatSpin(pdet, value=self.mca.incident_energy,
424 action=self.onSetXrayEnergy, **opts)
425 wids['en_min'] = FloatSpin(pdet, value=en_min, **opts)
426 wids['en_max'] = FloatSpin(pdet, value=en_max, **opts)
427 wids['flux_in'] = FloatCtrl(pdet, value=5.e10, gformat=True,
428 minval=0, size=(125, -1))
430 opts.update({'increment': 0.005})
431 wids['det_noise'] = FloatSpin(pdet, value=det_noise, **opts)
432 wids['det_efano'] = SimpleText(pdet, size=(250, -1),
433 label='E_Fano= %.4e' % FanoFactors['Si'])
435 opts.update(digits=1, max_val=90, min_val=0, increment=1)
436 wids['angle_in'] = FloatSpin(pdet, value=45, **opts)
437 wids['angle_out'] = FloatSpin(pdet, value=45, **opts)
439 opts.update(digits=1, max_val=5e9, min_val=0, increment=1)
440 wids['det_dist'] = FloatSpin(pdet, value=50, **opts)
441 wids['det_area'] = FloatSpin(pdet, value=50, **opts)
443 for notyet in ('angle_in', 'angle_out', 'det_dist', 'det_area',
444 'flux_in'):
445 wids[notyet].Disable()
447 wids['fit_toler'] = Choice(pdet, choices=FitTols, size=(125, -1))
448 wids['fit_toler'].SetStringSelection('1.e-4')
449 wids['fit_step'] = Choice(pdet, choices=FitSteps, size=(125, -1))
450 wids['fit_step'].SetStringSelection('1.e-4')
451 wids['fit_maxnfev'] = Choice(pdet, choices=FitMaxNFevs, size=(125, -1))
452 wids['fit_maxnfev'].SetStringSelection('1000')
454 pdet.AddText(' Beam Energy, Fit Range :', colour='#880000', dcol=2)
455 pdet.AddText(' X-ray Energy (keV): ', newrow=True)
456 pdet.Add(wids['en_xray'])
457 pdet.AddText('Incident Flux (Hz): ', newrow=False)
458 pdet.Add(wids['flux_in'])
459 pdet.AddText(' Fit Energy Min (keV): ', newrow=True)
460 pdet.Add(wids['en_min'])
461 pdet.AddText('Fit Energy Max (keV): ')
462 pdet.Add(wids['en_max'])
463 pdet.AddText(' Fit Step Size: ', newrow=True)
464 pdet.Add(wids['fit_step'])
465 pdet.AddText('Fit Tolerance: ')
466 pdet.Add(wids['fit_toler'])
467 pdet.AddText(' Fit Max Evaluations: ', newrow=True)
468 pdet.Add(wids['fit_maxnfev'])
471 addLine(pdet)
472 pdet.AddText(' Energy Calibration :', colour='#880000', dcol=1, newrow=True)
473 pdet.Add(wids['cal_vary'], dcol=2)
474 pdet.AddText(' Offset (keV): ', newrow=True)
475 pdet.Add(wids['cal_offset'])
476 pdet.AddText('Slope (keV/bin) : ')
477 pdet.Add(wids['cal_slope'])
479 addLine(pdet)
480 pdet.AddText(' Detector Material:', colour='#880000', dcol=1, newrow=True)
481 pdet.AddText(EFano_Text, dcol=3)
482 pdet.AddText(' Material: ', newrow=True)
483 pdet.Add(wids['det_mat'])
484 pdet.Add(wids['det_efano'], dcol=2)
485 pdet.AddText(' Thickness (mm): ', newrow=True)
486 pdet.Add(wids['det_thk'])
487 pdet.AddText(' Noise (keV): ', newrow=True)
488 pdet.Add(wids['det_noise'])
489 pdet.Add(wids['det_noise_vary'], dcol=2)
492 addLine(pdet)
493 pdet.AddText(' Escape && Pileup:', colour='#880000', dcol=2, newrow=True)
494 pdet.AddText(' Escape Scale:', newrow=True)
495 pdet.Add(wids['escape_amp'])
496 pdet.Add(wids['escape_amp_vary'])
497 pdet.Add(wids['escape_use'], dcol=3)
499 pdet.AddText(' Pileup Scale:', newrow=True)
500 pdet.Add(wids['pileup_amp'])
501 pdet.Add(wids['pileup_amp_vary'])
502 pdet.Add(wids['pileup_use'], dcol=3)
504 addLine(pdet)
505 pdet.AddText(' Geometry:', colour='#880000', dcol=1, newrow=True)
506 pdet.AddText(Geom_Text, dcol=3)
507 pdet.AddText(' Incident Angle (deg):', newrow=True)
508 pdet.Add(wids['angle_in'])
509 pdet.AddText(' Exit Angle (deg):', newrow=False)
510 pdet.Add(wids['angle_out'])
511 pdet.AddText(' Detector Distance (mm): ', newrow=True)
512 pdet.Add(wids['det_dist'])
513 pdet.AddText(' Detector Area (mm^2): ', newrow=False)
514 pdet.Add(wids['det_area'])
517 addLine(pdet)
518 pdet.pack()
519 return pdet
521 def materials_page(self, **kws):
522 "filters and matrix settings"
523 wids = self.wids
524 pan = GridPanel(self, itemstyle=LEFT)
526 pan.AddText(' Filters :', colour='#880000', dcol=2) # , newrow=True)
527 pan.AddManyText((' Filter #', 'Material', 'Thickness (mm)',
528 'Vary Thickness'), style=LEFT, newrow=True)
529 opts = dict(size=(125, -1), min_val=0, digits=5, increment=0.005)
531 for i in range(NFILTERS):
532 t = 'filter%d' % (i+1)
533 wids['%s_mat'%t] = Choice(pan, choices=Filter_Materials, default=0,
534 size=(150, -1),
535 action=partial(self.onFilterMaterial, index=i+1))
536 wids['%s_thk'%t] = FloatSpin(pan, value=0.0, **opts)
537 wids['%s_var'%t] = VarChoice(pan, default=0)
538 if i == 0: # first selection
539 wids['%s_mat'%t].SetStringSelection('beryllium')
540 wids['%s_thk'%t].SetValue(0.0250)
541 elif i == 1: # second selection
542 wids['%s_mat'%t].SetStringSelection('air')
543 wids['%s_thk'%t].SetValue(50.00)
544 elif i == 2: # third selection
545 wids['%s_mat'%t].SetStringSelection('kapton')
546 wids['%s_thk'%t].SetValue(0.00)
547 elif i == 3: # third selection
548 wids['%s_mat'%t].SetStringSelection('aluminum')
549 wids['%s_thk'%t].SetValue(0.00)
551 pan.AddText(' %i' % (i+1), newrow=True)
552 pan.Add(wids['%s_mat' % t])
553 pan.Add(wids['%s_thk' % t])
554 pan.Add(wids['%s_var' % t])
556 pan.Add(HLine(pan, size=(650, 3)), dcol=6, newrow=True)
558 pan.AddText(' Matrix:', colour='#880000', newrow=True)
559 pan.AddText(' NOTE: thin film limit only', dcol=3)
561 wids['matrix_mat'] = wx.TextCtrl(pan, value='', size=(275, -1))
562 wids['matrix_thk'] = FloatSpin(pan, value=0.0, **opts)
563 wids['matrix_den'] = FloatSpin(pan, value=1.0, **opts)
564 wids['matrix_btn'] = Button(pan, 'Use Material', size=(175, -1),
565 action=self.onUseCurrentMaterialAsFilter)
566 wids['matrix_btn'].Disable()
567 pan.AddText(' Material/Formula:', dcol=1, newrow=True)
568 pan.Add(wids['matrix_mat'], dcol=2)
569 pan.Add(wids['matrix_btn'], dcol=3)
570 pan.AddText(' Thickness (mm):', newrow=True)
571 pan.Add(wids['matrix_thk'])
572 pan.AddText(' Density (gr/cm^3):', newrow=False)
573 pan.Add(wids['matrix_den'])
575 pan.Add(HLine(pan, size=(650, 3)), dcol=6, newrow=True)
577 # Materials
578 pan.AddText(' Known Materials:', colour='#880000', dcol=4, newrow=True)
580 mview = self.owids['materials'] = dv.DataViewListCtrl(pan, style=DVSTYLE)
581 mview.Bind(dv.EVT_DATAVIEW_SELECTION_CHANGED, self.onSelectMaterial)
582 self.selected_material = ''
584 mview.AppendTextColumn('Name', width=150)
585 mview.AppendTextColumn('Formula', width=325)
586 mview.AppendTextColumn('density', width=90)
587 mview.AppendToggleColumn('Filter?', width=75)
588 for col in range(4):
589 this = mview.Columns[col]
590 align = wx.ALIGN_LEFT
591 this.Sortable = True
592 this.Alignment = this.Renderer.Alignment = align
594 mview.SetMinSize((725, 170))
595 mview.DeleteAllItems()
596 self.materials_data = {}
597 for name, data in materials._read_materials_db().items():
598 # print("DATA " , name, data)
599 formula, density = data.formula, data.density
600 self.materials_data[name] = (formula, density)
601 mview.AppendItem((name, formula, "%9.6f"%density,
602 name in Filter_Materials))
603 pan.Add(mview, dcol=5, newrow=True)
605 pan.AddText(' Add Material:', colour='#880000', newrow=True)
606 pan.Add(Button(pan, 'Add', size=(175, -1),
607 action=self.onAddMaterial))
608 pan.Add((10, 10))
609 bx = Button(pan, 'Update Filter List', size=(175, -1),
610 action=self.onUpdateFilterList)
611 pan.Add(bx)
613 self.owids['newmat_name'] = wx.TextCtrl(pan, value='', size=(175, -1))
614 self.owids['newmat_dens'] = FloatSpin(pan, value=1.0, **opts)
615 self.owids['newmat_form'] = wx.TextCtrl(pan, value='', size=(400, -1))
618 for notyet in ('matrix_mat', 'matrix_thk', 'matrix_den',
619 'matrix_btn'):
620 wids[notyet].Disable()
622 pan.AddText(' Name:', newrow=True)
623 pan.Add(self.owids['newmat_name'])
624 pan.AddText(' Density (gr/cm^3):', newrow=False)
625 pan.Add(self.owids['newmat_dens'])
626 pan.AddText(' Formula:', newrow=True)
627 pan.Add(self.owids['newmat_form'], dcol=3)
628 pan.pack()
629 return pan
631 def fitresult_page(self, **kws):
632 sizer = wx.GridBagSizer(3, 3)
633 panel = scrolled.ScrolledPanel(self)
634 # title row
635 wids = self.owids
636 title = SimpleText(panel, 'Fit Results', font=Font(FONTSIZE+1),
637 colour=self.colors.title, style=LEFT)
639 wids['data_title'] = SimpleText(panel, '< > ', font=Font(FONTSIZE+1),
640 colour=self.colors.title, style=LEFT)
642 wids['fitlabel_lab'] = SimpleText(panel, 'Fit Label:')
643 wids['fitlabel_txt'] = wx.TextCtrl(panel, -1, ' ', size=(150, -1))
644 wids['fitlabel_btn'] = Button(panel, 'Set Label', size=(150, -1),
645 action=self.onChangeFitLabel)
647 opts = dict(default=False, size=(175, -1), action=self.onPlot)
648 wids['plot_comps'] = Check(panel, label='Show Components?', **opts)
649 self.plot_choice = Button(panel, 'Plot',
650 size=(150, -1), action=self.onPlot)
652 self.save_result = Button(panel, 'Save Model',
653 size=(150, -1), action=self.onSaveFitResult)
654 SetTip(self.save_result, 'save model and result to be loaded later')
656 self.export_fit = Button(panel, 'Export Fit',
657 size=(150, -1), action=self.onExportFitResult)
658 SetTip(self.export_fit, 'save arrays and results to text file')
660 irow = 0
661 sizer.Add(title, (irow, 0), (1, 1), LEFT)
662 sizer.Add(wids['data_title'], (irow, 1), (1, 3), LEFT)
664 irow += 1
665 sizer.Add(self.save_result, (irow, 0), (1, 1), LEFT)
666 sizer.Add(self.export_fit, (irow, 1), (1, 1), LEFT)
667 sizer.Add(self.plot_choice, (irow, 2), (1, 1), LEFT)
668 sizer.Add(wids['plot_comps'], (irow, 3), (1, 1), LEFT)
670 irow += 1
671 sizer.Add(wids['fitlabel_lab'], (irow, 0), (1, 1), LEFT)
672 sizer.Add(wids['fitlabel_txt'], (irow, 1), (1, 1), LEFT)
673 sizer.Add(wids['fitlabel_btn'], (irow, 2), (1, 2), LEFT)
676 irow += 1
677 sizer.Add(HLine(panel, size=(650, 3)), (irow, 0), (1, 5), LEFT)
679 irow += 1
680 title = SimpleText(panel, '[[Fit Statistics]]', font=Font(FONTSIZE+1),
681 colour=self.colors.title, style=LEFT)
682 sizer.Add(title, (irow, 0), (1, 4), LEFT)
684 sview = wids['stats'] = dv.DataViewListCtrl(panel, style=DVSTYLE)
685 sview.SetFont(self.font_fixedwidth)
686 sview.Bind(dv.EVT_DATAVIEW_SELECTION_CHANGED, self.onSelectFit)
687 sview.AppendTextColumn('Fit Label', width=120)
688 sview.AppendTextColumn('N_vary', width=80)
689 sview.AppendTextColumn('N_eval', width=80)
690 sview.AppendTextColumn('\u03c7\u00B2', width=130)
691 sview.AppendTextColumn('\u03c7\u00B2_reduced', width=130)
692 sview.AppendTextColumn('Akaike Info', width=130)
694 for col in range(sview.ColumnCount):
695 this = sview.Columns[col]
696 isort, align = True, wx.ALIGN_RIGHT
697 if col == 0:
698 align = wx.ALIGN_LEFT
699 this.Sortable = isort
700 this.Alignment = this.Renderer.Alignment = align
701 sview.SetMinSize((725, 150))
703 irow += 1
704 sizer.Add(sview, (irow, 0), (1, 5), LEFT)
706 irow += 1
707 sizer.Add(HLine(panel, size=(650, 3)), (irow, 0), (1, 5), LEFT)
709 irow += 1
710 title = SimpleText(panel, '[[Variables]]', font=Font(FONTSIZE+1),
711 colour=self.colors.title, style=LEFT)
712 sizer.Add(title, (irow, 0), (1, 1), LEFT)
714 pview = wids['params'] = dv.DataViewListCtrl(panel, style=DVSTYLE)
715 pview.SetFont(self.font_fixedwidth)
716 wids['paramsdata'] = []
717 pview.AppendTextColumn('Parameter', width=150)
718 pview.AppendTextColumn('Refined Value', width=130)
719 pview.AppendTextColumn('Standard Error', width=130)
720 pview.AppendTextColumn('% Uncertainty', width=130)
721 pview.AppendTextColumn('Initial Value', width=130)
723 for col in range(4):
724 this = pview.Columns[col]
725 align = wx.ALIGN_LEFT
726 if col > 0:
727 align = wx.ALIGN_RIGHT
728 this.Sortable = False
729 this.Alignment = this.Renderer.Alignment = align
731 pview.SetMinSize((725, 200))
732 pview.Bind(dv.EVT_DATAVIEW_SELECTION_CHANGED, self.onSelectParameter)
734 irow += 1
735 sizer.Add(pview, (irow, 0), (1, 5), LEFT)
737 irow += 1
738 sizer.Add(HLine(panel, size=(650, 3)), (irow, 0), (1, 5), LEFT)
740 irow += 1
741 title = SimpleText(panel, '[[Correlations]]', font=Font(FONTSIZE+1),
742 colour=self.colors.title, style=LEFT)
744 wids['all_correl'] = Button(panel, 'Show All',
745 size=(100, -1), action=self.onAllCorrel)
747 wids['min_correl'] = FloatSpin(panel, value=MIN_CORREL,
748 min_val=0, size=(100, -1),
749 digits=3, increment=0.1)
751 ctitle = SimpleText(panel, 'minimum correlation: ')
752 sizer.Add(title, (irow, 0), (1, 1), LEFT)
753 sizer.Add(ctitle, (irow, 1), (1, 1), LEFT)
754 sizer.Add(wids['min_correl'], (irow, 2), (1, 1), LEFT)
755 sizer.Add(wids['all_correl'], (irow, 3), (1, 1), LEFT)
757 cview = wids['correl'] = dv.DataViewListCtrl(panel, style=DVSTYLE)
758 cview.SetFont(self.font_fixedwidth)
759 cview.AppendTextColumn('Parameter 1', width=150)
760 cview.AppendTextColumn('Parameter 2', width=150)
761 cview.AppendTextColumn('Correlation', width=150)
763 for col in (0, 1, 2):
764 this = cview.Columns[col]
765 this.Sortable = False
766 align = wx.ALIGN_LEFT
767 if col == 2:
768 align = wx.ALIGN_RIGHT
769 this.Alignment = this.Renderer.Alignment = align
770 cview.SetMinSize((725, 125))
772 irow += 1
773 sizer.Add(cview, (irow, 0), (1, 5), LEFT)
774 pack(panel, sizer)
775 panel.SetMinSize((725, 750))
776 panel.SetupScrolling()
777 return panel
779 def composition_page(self, **kws):
780 sizer = wx.GridBagSizer(3, 3)
781 panel = scrolled.ScrolledPanel(self)
782 wids = self.owids
783 title = SimpleText(panel, 'Composition Results', font=Font(FONTSIZE+1),
784 colour=self.colors.title, style=LEFT)
785 wids['data_title2'] = SimpleText(panel, '< > ', font=Font(FONTSIZE+1),
786 colour=self.colors.title, style=LEFT)
788 cview = wids['composition'] = dv.DataViewListCtrl(panel, style=DVSTYLE)
789 cview.SetFont(self.font_fixedwidth)
790 cview.AppendTextColumn(' Z ', width=50)
791 cview.AppendTextColumn(' Element ', width=100)
792 cview.AppendTextColumn(' Amplitude', width=170)
793 cview.AppendTextColumn(' Concentration', width=170)
794 cview.AppendTextColumn(' Uncertainty', width=180)
796 for col in range(5):
797 this = cview.Columns[col]
798 align = wx.ALIGN_RIGHT
799 if col == 1:
800 align = wx.ALIGN_LEFT
801 this.Sortable = True
802 this.Alignment = this.Renderer.Alignment = align
804 cview.SetMinSize((725, 500))
805 wids['comp_fitlabel'] = Choice(panel, choices=[''], size=(175, -1),
806 action=self.onCompSelectFit)
808 self.compscale_lock = 0.0
809 wids['comp_elemchoice'] = Choice(panel, choices=[''], size=(125, -1))
810 # action=self.onCompSetElemAbundance)
811 wids['comp_elemscale'] = FloatSpin(panel, value=1.0, digits=5, min_val=0,
812 increment=0.01,
813 action=self.onCompSetElemAbundance)
814 wids['comp_units'] = Choice(panel, choices=CompositionUnits, size=(125, -1))
815 wids['comp_scale'] = FloatCtrl(panel, value=0, size=(200, -1), precision=5,
816 minval=0, action=self.onCompSetScale)
818 wids['comp_save'] = Button(panel, 'Save This Concentration Data',
819 size=(200, -1), action=self.onCompSave)
821 irow = 0
822 sizer.Add(title, (irow, 0), (1, 2), LEFT)
823 sizer.Add(wids['data_title2'], (irow, 2), (1, 5), LEFT)
824 irow += 1
825 sizer.Add(SimpleText(panel, 'Fit Label:'), (irow, 0), (1, 1), LEFT)
826 sizer.Add(wids['comp_fitlabel'], (irow, 1), (1, 5), LEFT)
828 irow += 1
829 sizer.Add(SimpleText(panel, 'Scale Element:'), (irow, 0), (1, 1), LEFT)
830 sizer.Add(wids['comp_elemchoice'], (irow, 1), (1, 1), LEFT)
831 sizer.Add(SimpleText(panel, ' to:'), (irow, 2), (1, 1), LEFT)
832 sizer.Add(wids['comp_elemscale'], (irow, 3), (1, 1), LEFT)
833 sizer.Add(wids['comp_units'], (irow, 4), (1, 1), LEFT)
835 irow += 1
836 sizer.Add(SimpleText(panel, 'Scaling Factor:'), (irow, 0), (1, 1), LEFT)
837 sizer.Add(wids['comp_scale'], (irow, 1), (1, 3), LEFT)
839 irow += 1
840 sizer.Add(wids['composition'], (irow, 0), (3, 6), LEFT)
842 irow += 3
843 sizer.Add(wids['comp_save'], (irow, 0), (1, 3), LEFT)
845 pack(panel, sizer)
846 panel.SetMinSize((725, 750))
847 panel.SetupScrolling()
848 return panel
850 def onCompSetScale(self, event=None, value=None):
851 if len(self.xrfresults) < 1 or (time.time() - self.compscale_lock) < 0.25:
852 return
853 self.compscale_lock = time.time()
854 owids = self.owids
855 result = self.get_fitresult(nfit=owids['comp_fitlabel'].GetSelection())
856 cur_elem = owids['comp_elemchoice'].GetStringSelection()
857 conc_vals = {}
858 for elem in result.comps.keys():
859 parname = 'amp_%s' % elem.lower()
860 if parname in result.params:
861 par = result.params[parname]
862 conc_vals[elem] = [par.value, par.stderr]
864 try:
865 scale = self.owids['comp_scale'].GetValue()
866 except:
867 return
869 owids['comp_elemscale'].SetValue(conc_vals[cur_elem][0]*scale)
870 owids['composition'].DeleteAllItems()
871 result.concentration_results = conc_vals
872 result.concentration_scale = scale
874 for elem, dat in conc_vals.items():
875 zat = "%d" % atomic_number(elem)
876 val, serr = dat
877 rval = "%15.4f" % val
878 sval = "%15.4f" % (val*scale)
879 uval = "%15.4f" % (serr*scale)
880 try:
881 uval = uval + ' ({:.2%})'.format(abs(serr/val))
882 except ZeroDivisionError:
883 pass
884 owids['composition'].AppendItem((zat, elem, rval, sval, uval))
886 def onCompSetElemAbundance(self, event=None, value=None):
887 if len(self.xrfresults) < 1 or (time.time() - self.compscale_lock) < 0.25:
888 return
889 self.compscale_lock = time.time()
890 owids = self.owids
891 result = self.get_fitresult(nfit=owids['comp_fitlabel'].GetSelection())
892 cur_elem = owids['comp_elemchoice'].GetStringSelection()
893 conc_vals = {}
894 for elem in result.comps.keys():
895 parname = 'amp_%s' % elem.lower()
896 if parname in result.params:
897 par = result.params[parname]
898 conc_vals[elem] = [par.value, par.stderr]
900 result.concentration_results = conc_vals
901 elem_value = owids['comp_elemscale'].GetValue()
903 scale = elem_value/conc_vals[cur_elem][0]
904 result.concentration_scale = scale
905 owids['comp_scale'].SetValue(scale)
906 owids['composition'].DeleteAllItems()
907 for elem, dat in conc_vals.items():
908 zat = "%d" % atomic_number(elem)
909 val, serr = dat
910 rval = "%15.4f" % val
911 sval = "%15.4f" % (val*scale)
912 uval = "%15.4f" % (serr*scale)
913 try:
914 uval = uval + ' ({:.2%})'.format(abs(serr/val))
915 except ZeroDivisionError:
916 pass
917 owids['composition'].AppendItem((zat, elem, rval, sval, uval))
920 def onCompSave(self, event=None):
921 result = self.get_fitresult(nfit=self.owids['comp_fitlabel'].GetSelection())
922 scale = result.concentration_scale
923 deffile = self.mca.label + '_' + result.label
924 deffile = fix_filename(deffile.replace('.', '_')) + '_xrf.csv'
925 wcards = "CSV (*.csv)|*.csv|All files (*.*)|*.*"
926 sfile = FileSave(self, 'Save Concentration Results',
927 default_file=deffile,
928 wildcard=wcards)
929 if sfile is not None:
930 buff = ["# results for MCA labeled: %s" % self.mca.label,
931 "# fit label: %s" % result.label,
932 "# concentration units: %s" % self.owids['comp_units'].GetStringSelection(),
933 "# count time: %s" % result.count_time,
934 "# scale: %s" % result.concentration_scale,
935 "# Fit Report:" ]
936 for l in result.fit_report.split('\n'):
937 buff.append("# %s" % l)
938 buff.append("###########")
939 buff.append("#Element Concentration Uncertainty Raw_Amplitude")
940 for elem, dat in result.concentration_results.items():
941 eout = (elem + ' '*4)[:4]
942 val, serr = dat
943 rval = "%15.4f" % val
944 sval = "%15.4f" % (val*scale)
945 uval = "%15.4f" % (serr*scale)
946 buff.append(" ".join([eout, sval, uval, rval]))
947 buff.append('')
948 with open(sfile, 'w', encoding=sys.getdefaultencoding()) as fh:
949 fh.write('\n'.join(buff))
951 def onCompSelectFit(self, event=None):
952 result = self.get_fitresult(nfit=self.owids['comp_fitlabel'].GetSelection())
953 cur_elem = self.owids['comp_elemchoice'].GetStringSelection()
954 self.owids['comp_elemchoice'].Clear()
955 elems = [el['symbol'] for el in result.elements]
956 self.owids['comp_elemchoice'].SetChoices(elems)
957 if len(cur_elem) > 0:
958 self.owids['comp_elemchoice'].SetStringSelection(cur_elem)
959 else:
960 self.owids['comp_elemchoice'].SetSelection(0)
961 self.onCompSetElemAbundance()
963 def UpdateCompositionPage(self, event=None):
964 self.xrfresults = self._larch.symtable.get_symbol(XRFRESULTS_GROUP)
965 if len(self.xrfresults) > 0:
966 result = self.get_fitresult()
967 fitlab = self.owids['comp_fitlabel']
968 fitlab.Clear()
969 fitlab.SetChoices([a.label for a in self.xrfresults])
970 fitlab.SetStringSelection(result.label)
971 self.onCompSelectFit()
973 def onElems_Clear(self, event=None):
974 self.ptable.on_clear_all()
976 def onElems_GuessPeaks(self, event=None):
977 mca = self.mca
978 _indices = peak_indices(mca.counts*1.0, min_dist=5, threshold=0.025)
979 peak_energies = mca.energy[_indices]
981 elrange = range(10, 92)
982 atsyms = [atomic_symbol(i) for i in elrange]
983 kalphas = [0.001*xray_line(i, 'Ka').energy for i in elrange]
984 kbetas = [0.001*xray_line(i, 'Kb').energy for i in elrange]
985 self.ptable.on_clear_all()
986 elems = []
987 for iz, en in enumerate(peak_energies):
988 for i, ex in enumerate(kalphas):
989 if abs(en - ex) < 0.025:
990 elems.append(atsyms[i])
991 peak_energies[iz] = -ex
993 for iz, en in enumerate(peak_energies):
994 if en > 0:
995 for i, ex in enumerate(kbetas):
996 if abs(en - ex) < 0.025:
997 if atsyms[i] not in elems:
998 elems.append(atsyms[i])
999 peak_energies[iz] = -ex
1001 en = self.wids['en_xray'].GetValue()
1002 emin = self.wids['en_min'].GetValue()
1003 for elem in elems:
1004 kedge = 0.001*xray_edge(elem, 'K').energy
1005 l3edge = 0.001*xray_edge(elem, 'L3').energy
1006 l2edge = 0.001*xray_edge(elem, 'L3').energy
1007 if ((kedge < en and kedge > emin) or
1008 (l3edge < en and l3edge > emin) or
1009 (l2edge < en and l2edge > emin)):
1010 if elem not in self.ptable.selected:
1011 self.ptable.onclick(label=elem)
1013 def onElems_FromROIS(self, event=None):
1014 for roi in self.mca.rois:
1015 words = roi.name.split()
1016 elem = words[0].title()
1017 if (elem in self.ptable.syms and
1018 elem not in self.ptable.selected):
1019 self.ptable.onclick(label=elem)
1020 self.onSetXrayEnergy()
1022 def onSetXrayEnergy(self, event=None):
1023 en = self.wids['en_xray'].GetValue()
1024 self.wids['en_max'].SetValue(en)
1025 self.wids['elastic_cen'].SetValue(en)
1026 self.wids['compton1_cen'].SetValue(en*0.975)
1027 self.wids['compton2_cen'].SetValue(en*0.950)
1028 emin = self.wids['en_min'].GetValue() * 1.25
1030 self.ptable.on_clear_all()
1031 for roi in self.mca.rois:
1032 words = roi.name.split()
1033 elem = words[0].title()
1034 kedge = l3edge = l2edge = 0.0
1035 try:
1036 kedge = 0.001*xray_edge(elem, 'K').energy
1037 l3edge = 0.001*xray_edge(elem, 'L3').energy
1038 l2edge = 0.001*xray_edge(elem, 'L3').energy
1039 except:
1040 pass
1041 if ((kedge < en and kedge > emin) or
1042 (l3edge < en and l3edge > emin) or
1043 (l2edge < en and l2edge > emin)):
1044 if elem not in self.ptable.selected:
1045 self.ptable.onclick(label=elem)
1047 def onDetMaterial(self, event=None):
1048 dmat = self.wids['det_mat'].GetStringSelection()
1049 if dmat not in FanoFactors:
1050 dmat = 'Si'
1051 self.wids['det_efano'].SetLabel('E_Fano= %.4e' % FanoFactors[dmat])
1053 def onFilterMaterial(self, evt=None, index=1):
1054 name = evt.GetString()
1055 den = self.materials_data.get(name, (None, 1.0))[1]
1056 t = 'filter%d' % (index)
1057 thick = self.wids['%s_thk'%t]
1058 if den < 0.1 and thick.GetValue() < 0.1:
1059 thick.SetValue(10.0)
1060 thick.SetIncrement(0.5)
1061 elif den > 0.1 and thick.GetValue() < 1.e-5:
1062 thick.SetValue(0.0250)
1063 thick.SetIncrement(0.005)
1065 def onUseCurrentMaterialAsFilter(self, evt=None):
1066 name = self.selected_material
1067 density = self.materials_data.get(name, (None, 1.0))[1]
1068 self.wids['matrix_den'].SetValue(density)
1069 self.wids['matrix_mat'].SetValue(name)
1071 def onSelectMaterial(self, evt=None):
1072 if self.owids['materials'] is None:
1073 return
1074 item = self.owids['materials'].GetSelectedRow()
1075 name = None
1076 if item > -1:
1077 name = list(self.materials_data.keys())[item]
1078 self.selected_material = name
1080 self.wids['matrix_btn'].Enable(name is not None)
1081 if name is not None:
1082 self.wids['matrix_btn'].SetLabel('Use %s' % name)
1084 def onUpdateFilterList(self, evt=None):
1085 flist = ['None']
1086 for i in range(len(self.materials_data)):
1087 if self.owids['materials'].GetToggleValue(i, 3): # is filter
1088 flist.append(self.owids['materials'].GetTextValue(i, 0))
1090 for i in range(NFILTERS):
1091 t = 'filter%d' % (i+1)
1092 choice = self.wids['%s_mat'%t]
1093 cur = choice.GetStringSelection()
1094 choice.Clear()
1095 choice.SetChoices(flist)
1096 if cur in flist:
1097 choice.SetStringSelection(cur)
1098 else:
1099 choice.SetSelection(0)
1101 def onAddMaterial(self, evt=None):
1102 name = self.owids['newmat_name'].GetValue()
1103 formula = self.owids['newmat_form'].GetValue()
1104 density = self.owids['newmat_dens'].GetValue()
1105 add = len(name) > 0 and len(formula)>0
1106 if add and name in self.materials_data:
1107 add = (Popup(self,
1108 "Overwrite definition of '%s'?" % name,
1109 'Re-define material?',
1110 style=wx.OK|wx.CANCEL)==wx.ID_OK)
1111 if add:
1112 irow = list(self.materials_data.keys()).index(name)
1113 self.owids['materials'].DeleteItem(irow)
1114 if add:
1115 add_material(name, formula, density)
1116 self.materials_data[name] = (formula, density)
1117 self.selected_material = name
1118 self.owids['materials'].AppendItem((name, formula,
1119 "%9.6f"%density,
1120 False))
1122 def onElemSelect(self, event=None, elem=None):
1123 self.ptable.tsym.SetLabel('')
1124 self.ptable.title.SetLabel('%d elements selected' %
1125 len(self.ptable.selected))
1127 def onUsePileupEscape(self, event=None):
1128 puse = self.wids['pileup_use'].IsChecked()
1129 self.wids['pileup_amp'].Enable(puse)
1130 self.wids['pileup_amp_vary'].Enable(puse)
1132 puse = self.wids['escape_use'].IsChecked()
1133 self.wids['escape_amp'].Enable(puse)
1134 self.wids['escape_amp_vary'].Enable(puse)
1137 def onUsePeak(self, event=None, name=None, value=None):
1138 if value is None and event is not None:
1139 value = event.IsChecked()
1140 if name is None:
1141 return
1142 for a in ('cen', 'step', 'tail', 'sigma', 'beta'):
1143 self.wids['%s_%s'%(name, a)].Enable(value)
1144 varwid = self.wids.get('%s_%s_vary'%(name, a), None)
1145 if varwid is not None:
1146 varwid.Enable(value)
1148 def build_model(self, match_amplitudes=True):
1149 """build xrf_model from form settings"""
1150 vars = {'Vary':'True', 'Fix': 'False', 'True':True, 'False': False}
1151 opts = {}
1152 for key, wid in self.wids.items():
1153 val = None
1154 if hasattr(wid, 'GetValue'):
1155 val = wid.GetValue()
1156 elif hasattr(wid, 'IsChecked'):
1157 val = wid.IsChecked()
1158 elif isinstance(wid, Choice):
1159 val = wid.GetStringSelection()
1160 elif hasattr(wid, 'GetStringSelection'):
1161 val = wid.GetStringSelection()
1162 elif hasattr(wid, 'GetLabel'):
1163 val = wid.GetLabel()
1164 if isinstance(val, str) and val.title() in vars:
1165 val = vars[val.title()]
1166 opts[key] = val
1167 opts['count_time'] = getattr(self.mca, 'real_time', 1.0)
1168 if opts['count_time'] is None:
1169 opts['count_time'] = 1.0
1170 opts['datetime'] = time.ctime()
1171 opts['mca_label'] = self.mca_label
1172 opts['mcagroup'] = self.mcagroup
1173 opts['XRFGROUP'] = XRFGROUP
1174 script = [xrfmod_setup.format(**opts)]
1176 for peakname in ('Elastic', 'Compton1', 'Compton2'):
1177 t = peakname.lower()
1178 if opts['%s_use'% t]:
1179 d = {'peakname': t}
1180 d['_cen'] = opts['%s_cen'%t]
1181 d['vcen'] = opts['%s_cen_vary'%t]
1182 d['_step'] = opts['%s_step'%t]
1183 d['vstep'] = opts['%s_step_vary'%t]
1184 d['_tail'] = opts['%s_tail'%t]
1185 d['vtail'] = opts['%s_tail_vary'%t]
1186 d['_beta'] = opts['%s_beta'%t]
1187 d['vbeta'] = opts['%s_beta_vary'%t]
1188 d['_sigma'] = opts['%s_sigma'%t]
1189 d['vsigma'] = opts['%s_sigma_vary'%t]
1190 script.append(xrfmod_scattpeak.format(**d))
1192 for i in range(NFILTERS):
1193 t = 'filter%d' % (i+1)
1194 f_mat = opts['%s_mat'%t]
1195 if f_mat not in (None, 'None') and int(1e6*opts['%s_thk'%t]) > 1:
1196 script.append(xrfmod_filter.format(name=f_mat,
1197 thick=opts['%s_thk'%t],
1198 vary=opts['%s_var'%t]))
1200 m_mat = opts['matrix_mat'].strip()
1201 if len(m_mat) > 0 and int(1e6*opts['matrix_thk']) > 1:
1202 script.append(xrfmod_matrix.format(name=m_mat,
1203 thick=opts['matrix_thk'],
1204 density=opts['matrix_den']))
1206 if opts['pileup_use'] in ('True', True):
1207 script.append(xrfmod_pileup.format(scale=opts['pileup_amp'],
1208 vary=opts['pileup_amp_vary']))
1210 if opts['escape_use'] in ('True', True):
1211 script.append(xrfmod_escape.format(scale=opts['escape_amp'],
1212 vary=opts['escape_amp_vary']))
1214 # sort elements selected on Periodic Table by Z
1215 elemz = []
1216 for elem in self.ptable.selected:
1217 elemz.append( 1 + self.ptable.syms.index(elem))
1218 elemz.sort()
1219 opts['elements'] = elemz
1221 xrfgroup = self._larch.symtable.get_group(XRFGROUP)
1222 setattr(xrfgroup, 'fitconfig', opts)
1223 json_dump(opts, Path(user_larchdir, 'xrf_fitconfig.json'))
1226 syms = ["'%s'" % self.ptable.syms[iz-1] for iz in elemz]
1227 syms = '[%s]' % (', '.join(syms))
1228 script.append(xrfmod_elems.format(elemlist=syms))
1230 script.append("# set initial estimate of xrf intensity")
1231 script.append("{XRFGROUP}.workmca.xrf_init = _xrfmodel.calc_spectrum({XRFGROUP}.workmca.energy)")
1232 script = '\n'.join(script)
1233 self.model_script = script.format(group=self.mcagroup, XRFGROUP=XRFGROUP)
1235 self._larch.eval(self.model_script)
1237 cmds = []
1238 self._larch.symtable.get_symbol('_xrfmodel')
1239 self.xrfmod = self._larch.symtable.get_symbol('_xrfmodel')
1240 floor = 1.e-12*max(self.mca.counts)
1242 if match_amplitudes:
1243 total = 0.0 * self.mca.counts
1244 for name, parr in self.xrfmod.comps.items():
1245 nam = name.lower()
1246 try:
1247 imax = np.where(parr > 0.99*parr.max())[0][0]
1248 except: # probably means all counts are zero
1249 imax = int(len(parr)/2.0)
1250 scale = 2.0*self.mca.counts[imax] / (parr[imax]+1.00)
1251 ampname = 'amp_%s' % nam
1252 if nam in ('elastic', 'compton1', 'compton2', 'compton',
1253 'background', 'pileup', 'escape'):
1254 ampname = f'{nam}_amp'
1255 if nam in ('background', 'pileup', 'escape'):
1256 scale = 1.0
1257 if nam in ('compton2',):
1258 scale *= 0.5
1260 paramval = self.xrfmod.params[ampname].value
1261 s = f"_xrfmodel.params['{ampname}'].value = {paramval*scale:.5f}"
1262 cmds.append(s)
1263 parr *= scale
1264 parr[np.where(parr<floor)] = floor
1265 total += parr
1266 self.xrfmod.current_model = total
1267 script = '\n'.join(cmds)
1268 self._larch.eval(script)
1269 self.model_script = f"{self.model_script}\n{script}"
1271 s = f"{XRFGROUP}.workmca.xrf_init = _xrfmodel.calc_spectrum({XRFGROUP}.workmca.energy)"
1272 self._larch.eval(s)
1274 def plot_model(self, model_spectrum=None, init=False, with_comps=False,
1275 label=None):
1276 conf = self.parent.conf
1278 plotkws = {'linewidth': 2.5, 'delay_draw': True, 'grid': False,
1279 'ylog_scale': self.parent.ylog_scale, 'show_legend': False,
1280 'fullbox': False}
1282 ppanel = self.parent.panel
1283 ppanel.conf.reset_trace_properties()
1284 self.parent.plot(self.mca.energy, self.mca.counts, mca=self.mca,
1285 xlabel='E (keV)', xmin=0, with_rois=False, **plotkws)
1287 if model_spectrum is None:
1288 model_spectrum = self.xrfmod.current_model if init else self.xrfmod.best_fit
1289 if label is None:
1290 label = 'model' if init else 'best fit'
1292 self.parent.oplot(self.mca.energy, model_spectrum,
1293 label=label, color=conf.fit_color, **plotkws)
1295 comp_traces = []
1296 plotkws.update({'fill': True, 'alpha':0.35, 'show_legend': True})
1297 for label, arr in self.xrfmod.comps.items():
1298 ppanel.oplot(self.mca.energy, arr, label=label, **plotkws)
1299 comp_traces.append(ppanel.conf.ntrace - 1)
1302 yscale = {False:'linear', True:'log'}[self.parent.ylog_scale]
1303 ppanel.set_logscale(yscale=yscale)
1304 ppanel.set_viewlimits()
1305 ppanel.conf.auto_margins = False
1306 ppanel.conf.set_margins(0.1, 0.02, 0.20, 0.1)
1307 ppanel.conf.set_legend_location('upper right', False)
1308 ppanel.conf.show_legend_frame = True
1309 ppanel.conf.draw_legend(show=True, delay_draw=False)
1311 if not with_comps:
1312 for obj, data in ppanel.conf.legend_map.items():
1313 line, trace, legline, legtext = data
1314 if trace in comp_traces:
1315 legline.set_alpha(0.50)
1316 legtext.set_alpha(0.50)
1317 line.set_visible(False)
1318 ppanel.conf.fills[trace].set_visible(False)
1319 ppanel.draw()
1321 def onShowModel(self, event=None):
1322 self.build_model()
1323 self.plot_model(init=True, with_comps=False)
1324 self.wids['fit_message'].SetLabel("Initial Model Built")
1326 def onFitIteration(self, iter=0, pars=None):
1327 if iter % 10 == 0:
1328 nvar = len([p for p in pars.values() if p.vary])
1329 print(f"XRF Fit iteration {iter}, {nvar} variables")
1330 # pass
1333 def onFitModel(self, event=None):
1334 self.build_model()
1335 xrfmod = self._larch.symtable.get_symbol('_xrfmodel')
1336 xrfmod.iter_callback = self.onFitIteration
1338 fit_tol = float(self.wids['fit_toler'].GetStringSelection())
1339 fit_step = float(self.wids['fit_step'].GetStringSelection())
1340 max_nfev = int(self.wids['fit_maxnfev'].GetStringSelection())
1341 emin = float(self.wids['en_min'].GetValue())
1342 emax = float(self.wids['en_max'].GetValue())
1344 fit_script = xrfmod_fitscript.format(group=self.mcagroup,
1345 XRFGROUP=XRFGROUP,
1346 emin=emin, emax=emax,
1347 fit_toler=fit_tol,
1348 fit_step=fit_step,
1349 max_nfev=max_nfev)
1350 # print("-- > ", fit_script)
1352 self._larch.eval(fit_script)
1353 dgroup = self._larch.symtable.get_group(self.mcagroup)
1354 self.xrfresults = self._larch.symtable.get_symbol(XRFRESULTS_GROUP)
1356 try:
1357 xrfresult = self.xrfresults[0]
1358 self.wids['fit_message'].SetLabel("Fit Complete")
1359 except:
1360 self.wids['fit_message'].SetLabel("fit failed, cannot get result")
1361 return
1363 xrfresult.script = "%s\n%s" % (self.model_script, fit_script)
1364 xrfresult.label = "fit %d" % (len(self.xrfresults))
1365 self.plot_model(init=True, with_comps=False)
1366 for i in range(len(self.nb.pagelist)):
1367 if self.nb.GetPageText(i).strip().startswith('Fit R'):
1368 self.nb.SetSelection(i)
1369 time.sleep(0.002)
1370 self.show_results()
1372 def onClose(self, event=None):
1373 self.Destroy()
1375 def onSaveFitResult(self, event=None):
1376 result = self.get_fitresult()
1377 deffile = self.mca.label + '_' + result.label
1378 deffile = fix_filename(deffile.replace('.', '_')) + '.xrfmodel'
1379 ModelWcards = "XRF Models(*.xrfmodel)|*.xrfmodel|All files (*.*)|*.*"
1380 sfile = FileSave(self, 'Save XRF Model', default_file=deffile,
1381 wildcard=ModelWcards)
1382 if sfile is not None:
1383 self._larch.eval(xrfmod_savejs.format(group=self.mcagroup,
1384 nfit=self.nfit,
1385 filename=sfile))
1387 def onExportFitResult(self, event=None):
1388 result = self.get_fitresult()
1389 deffile = self.mca.label + '_' + result.label
1390 deffile = fix_filename(deffile.replace('.', '_')) + '_xrf.txt'
1391 wcards = 'All files (*.*)|*.*'
1392 outfile = FileSave(self, 'Export Fit Result', default_file=deffile)
1393 if outfile is not None:
1394 buff = ['# XRF Fit %s: %s' % (self.mca.label, result.label),
1395 '## Fit Script:']
1396 for a in result.script.split('\n'):
1397 buff.append('# %s' % a)
1398 buff.append('## Fit Report:')
1399 for a in result.fit_report.split('\n'):
1400 buff.append('# %s' % a)
1402 buff.append('#')
1403 buff.append('########################################')
1405 labels = ['energy', 'counts', 'best_fit',
1406 'best_energy', 'fit_window',
1407 'fit_weight', 'attenuation']
1408 labels.extend(list(result.comps.keys()))
1410 buff.append('# %s' % (' '.join(labels)))
1412 npts = len(self.mca.energy)
1413 for i in range(npts):
1414 dline = [gformat(self.mca.energy[i]),
1415 gformat(self.mca.counts[i]),
1416 gformat(result.best_fit[i]),
1417 gformat(result.best_en[i]),
1418 gformat(result.fit_window[i]),
1419 gformat(result.fit_weight[i]),
1420 gformat(result.atten[i])]
1421 for c in result.comps.values():
1422 dline.append(gformat(c[i]))
1423 buff.append(' '.join(dline))
1424 buff.append('\n')
1425 with open(outfile, 'w', encoding=sys.getdefaultencoding()) as fh:
1426 fh.write('\n'.join(buff))
1428 def get_fitresult(self, nfit=None):
1429 if nfit is None:
1430 nfit = self.nfit
1432 self.xrfresults = self._larch.symtable.get_symbol(XRFRESULTS_GROUP)
1433 self.nfit = max(0, nfit)
1434 self.nfit = min(self.nfit, len(self.xrfresults)-1)
1435 return self.xrfresults[self.nfit]
1437 def onChangeFitLabel(self, event=None):
1438 label = self.owids['fitlabel_txt'].GetValue()
1439 result = self.get_fitresult()
1440 result.label = label
1441 self.show_results()
1443 def onPlot(self, event=None):
1444 result = self.get_fitresult()
1445 xrfmod = self._larch.symtable.get_symbol('_xrfmodel')
1446 with_comps = self.owids['plot_comps'].IsChecked()
1447 spect = xrfmod.calc_spectrum(self.mca.energy,
1448 params=result.params)
1449 self.plot_model(model_spectrum=spect, with_comps=with_comps,
1450 label=result.label)
1452 def onSelectFit(self, evt=None):
1453 if self.owids['stats'] is None:
1454 return
1455 item = self.owids['stats'].GetSelectedRow()
1456 if item > -1:
1457 self.show_fitresult(nfit=item)
1459 def onSelectParameter(self, evt=None):
1460 if self.owids['params'] is None:
1461 return
1462 if not self.owids['params'].HasSelection():
1463 return
1464 item = self.owids['params'].GetSelectedRow()
1465 pname = self.owids['paramsdata'][item]
1467 cormin= self.owids['min_correl'].GetValue()
1468 self.owids['correl'].DeleteAllItems()
1470 result = self.get_fitresult()
1471 this = result.params[pname]
1472 if this.correl is not None:
1473 sort_correl = sorted(this.correl.items(), key=lambda it: abs(it[1]))
1474 for name, corval in reversed(sort_correl):
1475 if abs(corval) > cormin:
1476 self.owids['correl'].AppendItem((pname, name, "% .4f" % corval))
1478 def onAllCorrel(self, evt=None):
1479 result = self.get_fitresult()
1480 params = result.params
1481 parnames = list(params.keys())
1483 cormin= self.owids['min_correl'].GetValue()
1484 correls = {}
1485 for i, name in enumerate(parnames):
1486 par = params[name]
1487 if not par.vary:
1488 continue
1489 if hasattr(par, 'correl') and par.correl is not None:
1490 for name2 in parnames[i+1:]:
1491 if (name != name2 and name2 in par.correl and
1492 abs(par.correl[name2]) > cormin):
1493 correls["%s$$%s" % (name, name2)] = par.correl[name2]
1495 sort_correl = sorted(correls.items(), key=lambda it: abs(it[1]))
1496 sort_correl.reverse()
1498 self.owids['correl'].DeleteAllItems()
1500 for namepair, corval in sort_correl:
1501 name1, name2 = namepair.split('$$')
1502 self.owids['correl'].AppendItem((name1, name2, "% .4f" % corval))
1504 def show_results(self):
1505 cur = self.get_fitresult()
1506 self.owids['stats'].DeleteAllItems()
1507 for i, res in enumerate(self.xrfresults):
1508 args = [res.label]
1509 for attr in ('nvarys', 'nfev', 'chisqr', 'redchi', 'aic'):
1510 val = getattr(res, attr)
1511 if isinstance(val, int):
1512 val = '%d' % val
1513 else:
1514 val = gformat(val, 11)
1515 args.append(val)
1516 self.owids['stats'].AppendItem(tuple(args))
1517 self.owids['data_title'].SetLabel("%s: %.3f sec" % (self.mca.label, cur.count_time))
1518 self.owids['data_title2'].SetLabel("%s: %.3f sec" % (self.mca.label, cur.count_time))
1519 self.owids['fitlabel_txt'].SetValue(cur.label)
1520 self.show_fitresult(nfit=self.nfit)
1522 def show_fitresult(self, nfit=0, mca=None):
1523 if mca is not None:
1524 self.mca = mca
1525 result = self.get_fitresult(nfit=nfit)
1527 self.owids['data_title'].SetLabel("%s: %.3f sec" % (self.mca.label, result.count_time))
1528 self.owids['data_title2'].SetLabel("%s: %.3f sec" % (self.mca.label, result.count_time))
1529 self.result = result
1530 self.owids['fitlabel_txt'].SetValue(result.label)
1531 self.owids['params'].DeleteAllItems()
1532 self.owids['paramsdata'] = []
1533 for param in reversed(result.params.values()):
1534 pname = param.name
1535 try:
1536 val = gformat(param.value, 10)
1537 except (TypeError, ValueError):
1538 val = ' ??? '
1539 serr, perr = ' N/A ', ' N/A '
1540 if param.stderr is not None:
1541 serr = gformat(param.stderr, 10)
1542 try:
1543 perr = '{:.3f}'.format(100.0*abs(param.stderr/param.value))
1544 except ZeroDivisionError:
1545 perr = '?'
1546 extra = ' '
1547 if param.expr is not None:
1548 extra = ' = %s ' % param.expr
1549 elif not param.vary:
1550 extra = ' (fixed)'
1551 elif param.init_value is not None:
1552 extra = gformat(param.init_value, 10)
1554 self.owids['params'].AppendItem((pname, val, serr, perr, extra))
1555 self.owids['paramsdata'].append(pname)
1556 self.Refresh()