Coverage for /Users/Newville/Codes/xraylarch/larch/wxxas/prepeak_panel.py: 9%
1046 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-11-09 10:08 -0600
« prev ^ index » next coverage.py v7.3.2, created at 2023-11-09 10:08 -0600
1import time
2import os
3import sys
4import numpy as np
5np.seterr(all='ignore')
7from functools import partial
8import json
10import wx
11import wx.lib.scrolledpanel as scrolled
13import wx.dataview as dv
15from lmfit import Parameter
17import lmfit.models as lm_models
19from larch import Group, site_config
20from larch.utils import uname, gformat, mkdir
21from larch.math import index_of
22from larch.io.export_modelresult import export_modelresult
23from larch.io import save_groups, read_groups
25from larch.wxlib import (ReportFrame, BitmapButton, FloatCtrl, FloatSpin,
26 SetTip, GridPanel, get_icon, SimpleText, pack,
27 Button, HLine, Choice, Check, MenuItem, COLORS,
28 set_color, CEN, RIGHT, LEFT, FRAMESTYLE, Font,
29 FONTSIZE, FONTSIZE_FW, FileSave, FileOpen,
30 flatnotebook, Popup, EditableListBox, ExceptionPopup)
32from larch.wxlib.parameter import ParameterWidgets
33from larch.wxlib.plotter import last_cursor_pos
34from .taskpanel import TaskPanel
35from .config import PrePeak_ArrayChoices, PlotWindowChoices
37DVSTYLE = dv.DV_SINGLE|dv.DV_VERT_RULES|dv.DV_ROW_LINES
39ModelChoices = {'other': ('<General Models>', 'Constant', 'Linear',
40 'Quadratic', 'Exponential', 'PowerLaw'
41 'Linear Step', 'Arctan Step',
42 'ErrorFunction Step', 'Logistic Step', 'Rectangle'),
43 'peaks': ('<Peak Models>', 'Gaussian', 'Lorentzian',
44 'Voigt', 'PseudoVoigt', 'DampedHarmonicOscillator',
45 'Pearson7', 'StudentsT', 'SkewedGaussian',
46 'Moffat', 'BreitWigner', 'Doniach', 'Lognormal'),
47 }
49# map of lmfit function name to Model Class
50ModelFuncs = {'constant': 'ConstantModel',
51 'linear': 'LinearModel',
52 'quadratic': 'QuadraticModel',
53 'polynomial': 'PolynomialModel',
54 'gaussian': 'GaussianModel',
55 'lorentzian': 'LorentzianModel',
56 'voigt': 'VoigtModel',
57 'pvoigt': 'PseudoVoigtModel',
58 'moffat': 'MoffatModel',
59 'pearson7': 'Pearson7Model',
60 'students_t': 'StudentsTModel',
61 'breit_wigner': 'BreitWignerModel',
62 'lognormal': 'LognormalModel',
63 'damped_oscillator': 'DampedOscillatorModel',
64 'dho': 'DampedHarmonicOscillatorModel',
65 'expgaussian': 'ExponentialGaussianModel',
66 'skewed_gaussian': 'SkewedGaussianModel',
67 'doniach': 'DoniachModel',
68 'powerlaw': 'PowerLawModel',
69 'exponential': 'ExponentialModel',
70 'step': 'StepModel',
71 'rectangle': 'RectangleModel'}
73BaselineFuncs = ['No Baseline',
74 'Constant+Lorentzian',
75 'Linear+Lorentzian',
76 'Constant+Gaussian',
77 'Linear+Gaussian',
78 'Constant+Voigt',
79 'Linear+Voigt',
80 'Quadratic', 'Linear']
83PLOT_BASELINE = 'Data+Baseline'
84PLOT_FIT = 'Data+Fit'
85PLOT_INIT = 'Data+Init Fit'
86PLOT_RESID = 'Data+Residual'
87PlotChoices = [PLOT_BASELINE, PLOT_FIT, PLOT_RESID]
89FitMethods = ("Levenberg-Marquardt", "Nelder-Mead", "Powell")
90ModelWcards = "Fit Models(*.modl)|*.modl|All files (*.*)|*.*"
91DataWcards = "Data Files(*.dat)|*.dat|All files (*.*)|*.*"
94MIN_CORREL = 0.10
96COMMANDS = {}
97COMMANDS['prepfit'] = """# prepare fit
98{group}.prepeaks.user_options = {user_opts:s}
99{group}.prepeaks.init_fit = peakmodel.eval(peakpars, x={group}.prepeaks.energy)
100{group}.prepeaks.init_ycomps = peakmodel.eval_components(params=peakpars, x={group}.prepeaks.energy)
101if not hasattr({group}.prepeaks, 'fit_history'): {group}.prepeaks.fit_history = []
102"""
104COMMANDS['prepeaks_setup'] = """# setup prepeaks
105if not hasattr({group}, 'energy'): {group:s}.energy = 1.0*{group:s}.xdat
106{group:s}.xdat = 1.0*{group:s}.energy
107{group:s}.ydat = 1.0*{group:s}.{array_name:s}
108prepeaks_setup(energy={group:s}, arrayname='{array_name:s}', elo={elo:.3f}, ehi={ehi:.3f},
109 emin={emin:.3f}, emax={emax:.3f})
110"""
112COMMANDS['set_yerr_const'] = "{group}.prepeaks.norm_std = {group}.yerr*ones(len({group}.prepeaks.norm))"
113COMMANDS['set_yerr_array'] = """
114{group}.prepeaks.norm_std = 1.0*{group}.yerr[{imin:d}:{imax:d}]
115yerr_min = 1.e-9*{group}.prepeaks.ydat.mean()
116{group}.prepeaks.norm_std[where({group}.yerr < yerr_min)] = yerr_min
117"""
119COMMANDS['dofit'] = """# do fit
120peakresult = prepeaks_fit({group}, peakmodel, peakpars)
121peakresult.user_options = {user_opts:s}
122"""
124def get_xlims(x, xmin, xmax):
125 xeps = min(np.diff(x))/ 5.
126 i1 = index_of(x, xmin + xeps)
127 i2 = index_of(x, xmax + xeps) + 1
128 return i1, i2
130class PrePeakFitResultFrame(wx.Frame):
131 config_sect = 'prepeak'
132 def __init__(self, parent=None, peakframe=None, datagroup=None, **kws):
133 wx.Frame.__init__(self, None, -1, title='Pre-edge Peak Fit Results',
134 style=FRAMESTYLE, size=(950, 700), **kws)
135 self.peakframe = peakframe
137 if datagroup is not None:
138 self.datagroup = datagroup
139 # prepeaks = getattr(datagroup, 'prepeaks', None)
140 # self.peakfit_history = getattr(prepeaks, 'fit_history', [])
141 self.parent = parent
142 self.datasets = {}
143 self.form = {}
144 self.larch_eval = self.peakframe.larch_eval
145 self.nfit = 0
146 self.createMenus()
147 self.build()
149 if datagroup is None:
150 symtab = self.peakframe.larch.symtable
151 xasgroups = getattr(symtab, '_xasgroups', None)
152 if xasgroups is not None:
153 for dname, dgroup in xasgroups.items():
154 dgroup = getattr(symtab, dgroup, None)
155 ppeak = getattr(dgroup, 'prepeaks', None)
156 hist = getattr(ppeak, 'fit_history', None)
157 if hist is not None:
158 self.add_results(dgroup, show=True)
161 def createMenus(self):
162 self.menubar = wx.MenuBar()
163 fmenu = wx.Menu()
164 m = {}
165 MenuItem(self, fmenu, "Save Model for Current Group",
166 "Save Model and Result to be loaded later",
167 self.onSaveFitResult)
169 MenuItem(self, fmenu, "Save Fit and Components for Current Fit",
170 "Save Arrays and Results to Text File",
171 self.onExportFitResult)
173 fmenu.AppendSeparator()
174 MenuItem(self, fmenu, "Save Parameters and Statistics for All Fitted Groups",
175 "Save CSV File of Parameters and Statistics for All Fitted Groups",
176 self.onSaveAllStats)
177 self.menubar.Append(fmenu, "&File")
178 self.SetMenuBar(self.menubar)
180 def build(self):
181 sizer = wx.GridBagSizer(3, 3)
182 sizer.SetVGap(3)
183 sizer.SetHGap(3)
185 splitter = wx.SplitterWindow(self, style=wx.SP_LIVE_UPDATE)
186 splitter.SetMinimumPaneSize(200)
188 self.filelist = EditableListBox(splitter, self.ShowDataSet,
189 size=(250, -1))
190 set_color(self.filelist, 'list_fg', bg='list_bg')
192 panel = scrolled.ScrolledPanel(splitter)
194 panel.SetMinSize((775, 575))
196 self.font_fixedwidth = wx.Font(FONTSIZE_FW, wx.MODERN, wx.NORMAL, wx.BOLD)
198 # title row
199 self.wids = wids = {}
200 title = SimpleText(panel, 'Fit Results', font=Font(FONTSIZE+2),
201 colour=COLORS['title'], style=LEFT)
203 wids['data_title'] = SimpleText(panel, '< > ', font=Font(FONTSIZE+2),
204 minsize=(350, -1),
205 colour=COLORS['title'], style=LEFT)
207 opts = dict(default=False, size=(200, -1), action=self.onPlot)
208 ppanel = wx.Panel(panel)
209 wids['plot_bline'] = Check(ppanel, label='Plot baseline-subtracted?', **opts)
210 wids['plot_resid'] = Check(ppanel, label='Plot with residual?', **opts)
211 wids['plot_win'] = Choice(ppanel, size=(60, -1), choices=PlotWindowChoices,
212 action=self.onPlot)
213 wids['plot_win'].SetStringSelection('1')
215 psizer = wx.BoxSizer(wx.HORIZONTAL)
216 psizer.Add( wids['plot_bline'], 0, 5)
217 psizer.Add( wids['plot_resid'], 0, 5)
218 psizer.Add(SimpleText(ppanel, 'Plot Window:'), 0, 5)
219 psizer.Add( wids['plot_win'], 0, 5)
221 pack(ppanel, psizer)
223 wids['load_model'] = Button(panel, 'Load this Model for Fitting',
224 size=(250, -1), action=self.onLoadModel)
226 wids['plot_choice'] = Button(panel, 'Plot This Fit',
227 size=(125, -1), action=self.onPlot)
229 wids['fit_label'] = wx.TextCtrl(panel, -1, ' ', size=(175, -1))
230 wids['set_label'] = Button(panel, 'Update Label', size=(150, -1),
231 action=self.onUpdateLabel)
232 wids['del_fit'] = Button(panel, 'Remove from Fit History', size=(200, -1),
233 action=self.onRemoveFromHistory)
237 irow = 0
238 sizer.Add(title, (irow, 0), (1, 1), LEFT)
239 sizer.Add(wids['data_title'], (irow, 1), (1, 3), LEFT)
241 irow += 1
242 wids['model_desc'] = SimpleText(panel, '<Model>', font=Font(FONTSIZE+1),
243 size=(750, 50), style=LEFT)
244 sizer.Add(wids['model_desc'], (irow, 0), (1, 6), LEFT)
246 irow += 1
247 sizer.Add(wids['load_model'],(irow, 0), (1, 2), LEFT)
249 irow += 1
250 sizer.Add(wids['plot_choice'],(irow, 0), (1, 1), LEFT)
251 sizer.Add(ppanel, (irow, 1), (1, 4), LEFT)
254 irow += 1
255 sizer.Add(HLine(panel, size=(650, 3)), (irow, 0), (1, 5), LEFT)
257 irow += 1
258 sizer.Add(SimpleText(panel, 'Fit Label:', style=LEFT), (irow, 0), (1, 1), LEFT)
259 sizer.Add(wids['fit_label'], (irow, 1), (1, 1), LEFT)
260 sizer.Add(wids['set_label'], (irow, 2), (1, 1), LEFT)
261 sizer.Add(wids['del_fit'], (irow, 3), (1, 2), LEFT)
263 irow += 1
264 title = SimpleText(panel, '[[Fit Statistics]]', font=Font(FONTSIZE+2),
265 colour=COLORS['title'], style=LEFT)
266 subtitle = SimpleText(panel, ' (most recent fit is at the top)',
267 font=Font(FONTSIZE+1), style=LEFT)
269 sizer.Add(title, (irow, 0), (1, 1), LEFT)
270 sizer.Add(subtitle, (irow, 1), (1, 1), LEFT)
272 sview = self.wids['stats'] = dv.DataViewListCtrl(panel, style=DVSTYLE)
273 sview.SetFont(self.font_fixedwidth)
274 sview.Bind(dv.EVT_DATAVIEW_SELECTION_CHANGED, self.onSelectFit)
275 sview.AppendTextColumn(' Label', width=120)
276 sview.AppendTextColumn(' N_data', width=75)
277 sview.AppendTextColumn(' N_vary', width=75)
278 sview.AppendTextColumn('\u03c7\u00B2', width=110)
279 sview.AppendTextColumn('reduced \u03c7\u00B2', width=110)
280 sview.AppendTextColumn('Akaike Info', width=110)
282 for col in range(sview.ColumnCount):
283 this = sview.Columns[col]
284 this.Sortable = True
285 this.Alignment = wx.ALIGN_RIGHT if col > 0 else wx.ALIGN_LEFT
286 this.Renderer.Alignment = this.Alignment
288 sview.SetMinSize((725, 150))
290 irow += 1
291 sizer.Add(sview, (irow, 0), (1, 5), LEFT)
293 irow += 1
294 sizer.Add(HLine(panel, size=(650, 3)), (irow, 0), (1, 5), LEFT)
296 irow += 1
297 title = SimpleText(panel, '[[Variables]]', font=Font(FONTSIZE+2),
298 colour=COLORS['title'], style=LEFT)
299 sizer.Add(title, (irow, 0), (1, 1), LEFT)
301 self.wids['copy_params'] = Button(panel, 'Update Model with these values',
302 size=(250, -1), action=self.onCopyParams)
304 sizer.Add(self.wids['copy_params'], (irow, 1), (1, 3), LEFT)
306 pview = self.wids['params'] = dv.DataViewListCtrl(panel, style=DVSTYLE)
307 pview.SetFont(self.font_fixedwidth)
308 self.wids['paramsdata'] = []
309 pview.AppendTextColumn('Parameter', width=150)
310 pview.AppendTextColumn('Best-Fit Value', width=125)
311 pview.AppendTextColumn('Standard Error', width=125)
312 pview.AppendTextColumn('Info ', width=300)
314 for col in range(4):
315 this = pview.Columns[col]
316 this.Sortable = False
317 this.Alignment = wx.ALIGN_RIGHT if col in (1, 2) else wx.ALIGN_LEFT
318 this.Renderer.Alignment = this.Alignment
320 pview.SetMinSize((725, 200))
321 pview.Bind(dv.EVT_DATAVIEW_SELECTION_CHANGED, self.onSelectParameter)
323 irow += 1
324 sizer.Add(pview, (irow, 0), (1, 5), LEFT)
326 irow += 1
327 sizer.Add(HLine(panel, size=(650, 3)), (irow, 0), (1, 5), LEFT)
329 irow += 1
330 title = SimpleText(panel, '[[Correlations]]', font=Font(FONTSIZE+2),
331 colour=COLORS['title'], style=LEFT)
333 self.wids['all_correl'] = Button(panel, 'Show All',
334 size=(100, -1), action=self.onAllCorrel)
336 self.wids['min_correl'] = FloatSpin(panel, value=MIN_CORREL,
337 min_val=0, size=(100, -1),
338 digits=3, increment=0.1)
340 ctitle = SimpleText(panel, 'minimum correlation: ')
341 sizer.Add(title, (irow, 0), (1, 1), LEFT)
342 sizer.Add(ctitle, (irow, 1), (1, 1), LEFT)
343 sizer.Add(self.wids['min_correl'], (irow, 2), (1, 1), LEFT)
344 sizer.Add(self.wids['all_correl'], (irow, 3), (1, 1), LEFT)
346 cview = self.wids['correl'] = dv.DataViewListCtrl(panel, style=DVSTYLE)
347 cview.SetFont(self.font_fixedwidth)
350 cview.AppendTextColumn('Parameter 1', width=150)
351 cview.AppendTextColumn('Parameter 2', width=150)
352 cview.AppendTextColumn('Correlation', width=150)
354 for col in (0, 1, 2):
355 this = cview.Columns[col]
356 this.Sortable = False
357 align = wx.ALIGN_LEFT
358 if col == 2:
359 align = wx.ALIGN_RIGHT
360 this.Alignment = this.Renderer.Alignment = align
361 cview.SetMinSize((475, 150))
363 irow += 1
364 sizer.Add(cview, (irow, 0), (1, 5), LEFT)
366 pack(panel, sizer)
367 panel.SetupScrolling()
369 splitter.SplitVertically(self.filelist, panel, 1)
371 mainsizer = wx.BoxSizer(wx.VERTICAL)
372 mainsizer.Add(splitter, 1, wx.GROW|wx.ALL, 5)
374 pack(self, mainsizer)
375 self.Show()
376 self.Raise()
378 def onUpdateLabel(self, event=None):
379 result = self.get_fitresult()
380 item = self.wids['stats'].GetSelectedRow()
381 result.label = self.wids['fit_label'].GetValue()
382 self.show_results()
384 def onRemoveFromHistory(self, event=None):
385 result = self.get_fitresult()
386 if wx.ID_YES != Popup(self,
387 f"Remove fit '{result.label}' from history?\nThis cannot be undone.",
388 "Remove fit?", style=wx.YES_NO):
389 return
391 self.datagroup.prepeaks.fit_history.pop(self.nfit)
392 self.nfit = 0
393 self.show_results()
396 def onSaveAllStats(self, evt=None):
397 "Save Parameters and Statistics to CSV"
398 # get first dataset to extract fit parameter names
399 fnames = self.filelist.GetItems()
400 if len(fnames) == 0:
401 return
403 deffile = "PrePeaksResults.csv"
404 wcards = 'CVS Files (*.csv)|*.csv|All files (*.*)|*.*'
405 path = FileSave(self, 'Save Parameter and Statistics for Pre-edge Peak Fits',
406 default_file=deffile, wildcard=wcards)
407 if path is None:
408 return
409 if os.path.exists(path) and uname != 'darwin': # darwin prompts in FileSave!
410 if wx.ID_YES != Popup(self,
411 "Overwrite existing Statistics File?",
412 "Overwrite existing file?", style=wx.YES_NO):
413 return
415 ppeaks_tmpl = self.datasets[fnames[0]].prepeaks
416 res0 = ppeaks_tmpl.fit_history[0].result
417 param_names = list(reversed(res0.params.keys()))
418 user_opts = ppeaks_tmpl.user_options
419 model_desc = self.get_model_desc(res0.model).replace('\n', ' ')
420 out = ['# Pre-edge Peak Fit Report %s' % time.ctime(),
421 '# Fitted Array name: %s' % user_opts['array_name'],
422 '# Model form: %s' % model_desc,
423 '# Baseline form: %s' % user_opts['baseline_form'],
424 '# Energy fit range: [%f, %f]' % (user_opts['emin'], user_opts['emax']),
425 '#--------------------']
427 labels = [('Data Set' + ' '*25)[:25], 'Group name', 'n_data',
428 'n_varys', 'chi-square', 'reduced_chi-square',
429 'akaike_info', 'bayesian_info']
431 for pname in param_names:
432 labels.append(pname)
433 labels.append(pname+'_stderr')
434 out.append('# %s' % (', '.join(labels)))
435 for name, dgroup in self.datasets.items():
436 if not hasattr(dgroup, 'prepeaks'):
437 continue
438 try:
439 pkfit = dgroup.prepeaks.fit_history[0]
440 except:
441 continue
442 result = pkfit.result
443 label = dgroup.filename
444 if len(label) < 25:
445 label = (label + ' '*25)[:25]
446 dat = [label, dgroup.groupname,
447 '%d' % result.ndata, '%d' % result.nvarys]
448 for attr in ('chisqr', 'redchi', 'aic', 'bic'):
449 dat.append(gformat(getattr(result, attr), 11))
450 for pname in param_names:
451 val = stderr = 0
452 if pname in result.params:
453 par = result.params[pname]
454 dat.append(gformat(par.value, 11))
455 stderr = gformat(par.stderr, 11) if par.stderr is not None else 'nan'
456 dat.append(stderr)
457 out.append(', '.join(dat))
458 out.append('')
460 with open(path, 'w', encoding=sys.getdefaultencoding()) as fh:
461 fh.write('\n'.join(out))
464 def onSaveFitResult(self, event=None):
465 deffile = self.datagroup.filename.replace('.', '_') + 'peak.modl'
466 sfile = FileSave(self, 'Save Fit Model', default_file=deffile,
467 wildcard=ModelWcards)
468 if sfile is not None:
469 pkfit = self.get_fitresult()
470 save_groups(sfile, ['#peakfit 1.0', pkfit])
472 def onExportFitResult(self, event=None):
473 dgroup = self.datagroup
474 deffile = dgroup.filename.replace('.', '_') + '.xdi'
475 wcards = 'All files (*.*)|*.*'
477 outfile = FileSave(self, 'Export Fit Result', default_file=deffile)
479 pkfit = self.get_fitresult()
480 result = pkfit.result
481 if outfile is not None:
482 i1, i2 = get_xlims(dgroup.xdat,
483 pkfit.user_options['emin'],
484 pkfit.user_options['emax'])
485 x = dgroup.xdat[i1:i2]
486 y = dgroup.ydat[i1:i2]
487 yerr = None
488 if hasattr(dgroup, 'yerr'):
489 yerr = 1.0*dgroup.yerr
490 if not isinstance(yerr, np.ndarray):
491 yerr = yerr * np.ones(len(y))
492 else:
493 yerr = yerr[i1:i2]
495 export_modelresult(result, filename=outfile,
496 datafile=dgroup.filename, ydata=y,
497 yerr=yerr, x=x)
500 def get_fitresult(self, nfit=None):
501 if nfit is None:
502 nfit = self.nfit
503 self.peakfit_history = getattr(self.datagroup.prepeaks, 'fit_history', [])
504 self.nfit = max(0, nfit)
505 if self.nfit > len(self.peakfit_history):
506 self.nfit = 0
507 if len(self.peakfit_history) > 0:
508 return self.peakfit_history[self.nfit]
510 def onPlot(self, event=None):
511 show_resid = self.wids['plot_resid'].IsChecked()
512 sub_bline = self.wids['plot_bline'].IsChecked()
513 win = int(self.wids['plot_win'].GetStringSelection())
514 cmd = "plot_prepeaks_fit(%s, nfit=%i, show_residual=%s, subtract_baseline=%s, win=%d)"
515 cmd = cmd % (self.datagroup.groupname, self.nfit, show_resid, sub_bline, win)
516 self.peakframe.larch_eval(cmd)
517 self.peakframe.controller.set_focus(topwin=self)
519 def onSelectFit(self, evt=None):
520 if self.wids['stats'] is None:
521 return
522 item = self.wids['stats'].GetSelectedRow()
523 if item > -1:
524 self.show_fitresult(nfit=item)
526 def onSelectParameter(self, evt=None):
527 if self.wids['params'] is None:
528 return
529 if not self.wids['params'].HasSelection():
530 return
531 item = self.wids['params'].GetSelectedRow()
532 pname = self.wids['paramsdata'][item]
534 cormin= self.wids['min_correl'].GetValue()
535 self.wids['correl'].DeleteAllItems()
537 result = self.get_fitresult()
538 this = result.result.params[pname]
539 if this.correl is not None:
540 sort_correl = sorted(this.correl.items(), key=lambda it: abs(it[1]))
541 for name, corval in reversed(sort_correl):
542 if abs(corval) > cormin:
543 self.wids['correl'].AppendItem((pname, name, "% .4f" % corval))
545 def onAllCorrel(self, evt=None):
546 result = self.get_fitresult()
547 params = result.result.params
548 parnames = list(params.keys())
550 cormin= self.wids['min_correl'].GetValue()
551 correls = {}
552 for i, name in enumerate(parnames):
553 par = params[name]
554 if not par.vary:
555 continue
556 if hasattr(par, 'correl') and par.correl is not None:
557 for name2 in parnames[i+1:]:
558 if (name != name2 and name2 in par.correl and
559 abs(par.correl[name2]) > cormin):
560 correls["%s$$%s" % (name, name2)] = par.correl[name2]
562 sort_correl = sorted(correls.items(), key=lambda it: abs(it[1]))
563 sort_correl.reverse()
565 self.wids['correl'].DeleteAllItems()
567 for namepair, corval in sort_correl:
568 name1, name2 = namepair.split('$$')
569 self.wids['correl'].AppendItem((name1, name2, "% .4f" % corval))
571 def onLoadModel(self, event=None):
572 self.peakframe.use_modelresult(self.get_fitresult())
574 def onCopyParams(self, evt=None):
575 result = self.get_fitresult()
576 self.peakframe.update_start_values(result.result.params)
578 def ShowDataSet(self, evt=None):
579 dataset = evt.GetString()
580 group = self.datasets.get(evt.GetString(), None)
581 if group is not None:
582 self.show_results(datagroup=group, show_plot=True)
584 def add_results(self, dgroup, form=None, larch_eval=None, show=True):
585 name = dgroup.filename
586 if name not in self.filelist.GetItems():
587 self.filelist.Append(name)
588 self.datasets[name] = dgroup
589 if show:
590 self.show_results(datagroup=dgroup, form=form, larch_eval=larch_eval)
592 def show_results(self, datagroup=None, form=None, show_plot=False, larch_eval=None):
593 if datagroup is not None:
594 self.datagroup = datagroup
595 if larch_eval is not None:
596 self.larch_eval = larch_eval
598 datagroup = self.datagroup
599 self.peakfit_history = getattr(self.datagroup.prepeaks, 'fit_history', [])
601 # cur = self.get_fitresult()
602 wids = self.wids
603 wids['stats'].DeleteAllItems()
604 for i, res in enumerate(self.peakfit_history):
605 args = [res.label]
606 for attr in ('ndata', 'nvarys', 'chisqr', 'redchi', 'aic'):
607 val = getattr(res.result, attr)
608 if isinstance(val, int):
609 val = '%d' % val
610 else:
611 val = gformat(val, 10)
612 args.append(val)
613 wids['stats'].AppendItem(tuple(args))
614 wids['data_title'].SetLabel(self.datagroup.filename)
615 self.show_fitresult(nfit=0)
617 if show_plot:
618 show_resid= self.wids['plot_resid'].IsChecked()
619 sub_bline = self.wids['plot_bline'].IsChecked()
620 cmd = "plot_prepeaks_fit(%s, nfit=0, show_residual=%s, subtract_baseline=%s)"
621 cmd = cmd % (datagroup.groupname, show_resid, sub_bline)
623 self.peakframe.larch_eval(cmd)
624 self.peakframe.controller.set_focus(topwin=self)
626 def get_model_desc(self, model):
627 model_repr = model._reprstring(long=True)
628 for word in ('Model(', ',', '(', ')', '+'):
629 model_repr = model_repr.replace(word, ' ')
630 words = []
631 mname, imodel = '', 0
632 for word in model_repr.split():
633 if word.startswith('prefix'):
634 words.append("%sModel(%s)" % (mname.title(), word))
635 else:
636 mname = word
637 if imodel > 0:
638 delim = '+' if imodel % 2 == 1 else '+\n'
639 words.append(delim)
640 imodel += 1
641 return ''.join(words)
644 def show_fitresult(self, nfit=0, datagroup=None):
645 if datagroup is not None:
646 self.datagroup = datagroup
648 result = self.get_fitresult(nfit=nfit)
649 wids = self.wids
650 try:
651 wids['fit_label'].SetValue(result.label)
652 wids['data_title'].SetLabel(self.datagroup.filename)
653 wids['model_desc'].SetLabel(self.get_model_desc(result.result.model))
654 valid_result = True
655 except:
656 valid_result = False
658 wids['params'].DeleteAllItems()
659 wids['paramsdata'] = []
660 if valid_result:
661 for param in reversed(result.result.params.values()):
662 pname = param.name
663 try:
664 val = gformat(param.value, 10)
665 except (TypeError, ValueError):
666 val = ' ??? '
667 serr = ' N/A '
668 if param.stderr is not None:
669 serr = gformat(param.stderr, 10)
670 extra = ' '
671 if param.expr is not None:
672 extra = '= %s ' % param.expr
673 elif not param.vary:
674 extra = '(fixed)'
675 elif param.init_value is not None:
676 extra = '(init=%s)' % gformat(param.init_value, 10)
678 wids['params'].AppendItem((pname, val, serr, extra))
679 wids['paramsdata'].append(pname)
680 self.Refresh()
682class PrePeakPanel(TaskPanel):
683 def __init__(self, parent=None, controller=None, **kws):
684 TaskPanel.__init__(self, parent, controller, panel='prepeaks', **kws)
685 self.fit_components = {}
686 self.user_added_params = None
688 self.pick2_timer = wx.Timer(self)
689 self.pick2_group = None
690 self.Bind(wx.EVT_TIMER, self.onPick2Timer, self.pick2_timer)
691 self.pick2_t0 = 0.
692 self.pick2_timeout = 15.
694 self.pick2erase_timer = wx.Timer(self)
695 self.pick2erase_panel = None
696 self.Bind(wx.EVT_TIMER, self.onPick2EraseTimer, self.pick2erase_timer)
698 def onPanelExposed(self, **kws):
699 # called when notebook is selected
700 try:
701 fname = self.controller.filelist.GetStringSelection()
702 gname = self.controller.file_groups[fname]
703 dgroup = self.controller.get_group(gname)
704 self.ensure_xas_processed(dgroup)
705 self.fill_form(dgroup)
706 except:
707 pass # print(" Cannot Fill prepeak panel from group ")
709 pkfit = getattr(self.larch.symtable, 'peakresult', None)
710 if pkfit is not None:
711 self.showresults_btn.Enable()
712 self.use_modelresult(pkfit)
714 def onModelPanelExposed(self, event=None, **kws):
715 pass
717 def build_display(self):
718 pan = self.panel # = GridPanel(self, ncols=4, nrows=4, pad=2, itemstyle=LEFT)
720 self.wids = {}
722 fsopts = dict(digits=2, increment=0.1, min_val=-9999,
723 max_val=9999, size=(125, -1), with_pin=True)
725 ppeak_elo = self.add_floatspin('ppeak_elo', value=-13, **fsopts)
726 ppeak_ehi = self.add_floatspin('ppeak_ehi', value=-3, **fsopts)
727 ppeak_emin = self.add_floatspin('ppeak_emin', value=-20, **fsopts)
728 ppeak_emax = self.add_floatspin('ppeak_emax', value=0, **fsopts)
730 self.loadresults_btn = Button(pan, 'Load Fit Result',
731 action=self.onLoadFitResult, size=(150, -1))
732 self.showresults_btn = Button(pan, 'Show Fit Results',
733 action=self.onShowResults, size=(150, -1))
734 self.showresults_btn.Disable()
736 self.fitbline_btn = Button(pan,'Fit Baseline', action=self.onFitBaseline,
737 size=(150, -1))
739 self.plotmodel_btn = Button(pan,
740 'Plot Current Model',
741 action=self.onPlotModel, size=(150, -1))
742 self.fitmodel_btn = Button(pan, 'Fit Current Group',
743 action=self.onFitModel, size=(150, -1))
744 self.fitmodel_btn.Disable()
745 self.fitselected_btn = Button(pan, 'Fit Selected Groups',
746 action=self.onFitSelected, size=(150, -1))
747 self.fitselected_btn.Disable()
748 self.fitmodel_btn.Disable()
750 self.array_choice = Choice(pan, size=(200, -1),
751 choices=list(PrePeak_ArrayChoices.keys()))
752 self.array_choice.SetSelection(0)
754 self.bline_choice = Choice(pan, size=(200, -1),
755 choices=BaselineFuncs)
756 self.bline_choice.SetSelection(2)
758 models_peaks = Choice(pan, size=(200, -1),
759 choices=ModelChoices['peaks'],
760 action=self.addModel)
762 models_other = Choice(pan, size=(200, -1),
763 choices=ModelChoices['other'],
764 action=self.addModel)
766 self.models_peaks = models_peaks
767 self.models_other = models_other
770 self.message = SimpleText(pan,
771 'first fit baseline, then add peaks to fit model.')
773 opts = dict(default=True, size=(75, -1), action=self.onPlot)
774 self.show_peakrange = Check(pan, label='show?', **opts)
775 self.show_fitrange = Check(pan, label='show?', **opts)
777 opts = dict(default=False, size=(200, -1), action=self.onPlot)
779 def add_text(text, dcol=1, newrow=True):
780 pan.Add(SimpleText(pan, text), dcol=dcol, newrow=newrow)
782 pan.Add(SimpleText(pan, 'Pre-edge Peak Fitting',
783 size=(350, -1), **self.titleopts), style=LEFT, dcol=5)
784 pan.Add(self.loadresults_btn)
786 add_text('Array to fit: ')
787 pan.Add(self.array_choice, dcol=3)
788 pan.Add((5,5))
789 pan.Add(self.showresults_btn)
790 # add_text('E0: ', newrow=False)
791 # pan.Add(ppeak_e0)
792 # pan.Add(self.show_e0)
794 add_text('Fit Energy Range: ')
795 pan.Add(ppeak_emin)
796 add_text(' : ', newrow=False)
797 pan.Add(ppeak_emax)
798 pan.Add(self.show_fitrange)
800 pan.Add(HLine(pan, size=(600, 2)), dcol=6, newrow=True)
801 add_text( 'Baseline Form: ')
802 t = SimpleText(pan, 'Baseline Skip Range: ')
803 SetTip(t, 'Range skipped over for baseline fit')
804 pan.Add(self.bline_choice, dcol=3)
805 pan.Add((10, 10))
806 pan.Add(self.fitbline_btn)
808 pan.Add(t, newrow=True)
809 pan.Add(ppeak_elo)
810 add_text(' : ', newrow=False)
811 pan.Add(ppeak_ehi)
813 pan.Add(self.show_peakrange)
814 pan.Add(HLine(pan, size=(600, 2)), dcol=6, newrow=True)
816 # add model
817 ts = wx.BoxSizer(wx.HORIZONTAL)
818 ts.Add(models_peaks)
819 ts.Add(models_other)
821 pan.Add(SimpleText(pan, 'Add Component: '), newrow=True)
822 pan.Add(ts, dcol=4)
823 pan.Add(self.plotmodel_btn)
826 pan.Add(SimpleText(pan, 'Fit Model to Current Group : '), dcol=5, newrow=True)
827 pan.Add(self.fitmodel_btn)
829 pan.Add(SimpleText(pan, 'Messages: '), newrow=True)
830 pan.Add(self.message, dcol=4)
831 pan.Add(self.fitselected_btn)
833 pan.Add(HLine(pan, size=(600, 2)), dcol=6, newrow=True)
834 pan.pack()
836 self.mod_nb = flatnotebook(self, {}, on_change=self.onModelPanelExposed)
837 self.mod_nb_init = True
838 dummy_panel = wx.Panel(self.mod_nb)
840 self.mod_nb.AddPage(dummy_panel, 'Empty Model', True)
841 sizer = wx.BoxSizer(wx.VERTICAL)
842 sizer.Add((10, 10), 0, LEFT, 3)
843 sizer.Add(pan, 0, LEFT, 3)
844 sizer.Add((10, 10), 0, LEFT, 3)
845 sizer.Add(self.mod_nb, 1, LEFT|wx.GROW, 5)
847 pack(self, sizer)
849 def get_config(self, dgroup=None):
850 """get processing configuration for a group"""
851 if dgroup is None:
852 dgroup = self.controller.get_group()
854 conf = getattr(dgroup, 'prepeak_config', {})
855 if 'e0' not in conf:
856 conf = self.controller.get_defaultcfonfig()
857 conf['e0'] = getattr(dgroup, 'e0', -1)
859 dgroup.prepeak_config = conf
860 if not hasattr(dgroup, 'prepeaks'):
861 dgroup.prepeaks = Group()
863 return conf
865 def fill_form(self, dat):
866 if isinstance(dat, Group):
867 if not hasattr(dat, 'norm'):
868 self.xasmain.process_normalization(dat)
869 if hasattr(dat, 'prepeaks'):
870 self.wids['ppeak_emin'].SetValue(dat.prepeaks.emin)
871 self.wids['ppeak_emax'].SetValue(dat.prepeaks.emax)
872 self.wids['ppeak_elo'].SetValue(dat.prepeaks.elo)
873 self.wids['ppeak_ehi'].SetValue(dat.prepeaks.ehi)
874 elif isinstance(dat, dict):
875 # self.wids['ppeak_e0'].SetValue(dat['e0'])
876 self.wids['ppeak_emin'].SetValue(dat['emin'])
877 self.wids['ppeak_emax'].SetValue(dat['emax'])
878 self.wids['ppeak_elo'].SetValue(dat['elo'])
879 self.wids['ppeak_ehi'].SetValue(dat['ehi'])
881 self.array_choice.SetStringSelection(dat['array_desc'])
882 self.bline_choice.SetStringSelection(dat['baseline_form'])
884 self.show_fitrange.Enable(dat['show_fitrange'])
885 self.show_peakrange.Enable(dat['show_peakrange'])
887 def read_form(self):
888 "read for, returning dict of values"
889 dgroup = self.controller.get_group()
890 array_desc = self.array_choice.GetStringSelection()
891 bline_form = self.bline_choice.GetStringSelection()
892 form_opts = {'gname': dgroup.groupname,
893 'filename': dgroup.filename,
894 'array_desc': array_desc.lower(),
895 'array_name': PrePeak_ArrayChoices[array_desc],
896 'baseline_form': bline_form.lower(),
897 'bkg_components': []}
899 # form_opts['e0'] = self.wids['ppeak_e0'].GetValue()
900 form_opts['emin'] = self.wids['ppeak_emin'].GetValue()
901 form_opts['emax'] = self.wids['ppeak_emax'].GetValue()
902 form_opts['elo'] = self.wids['ppeak_elo'].GetValue()
903 form_opts['ehi'] = self.wids['ppeak_ehi'].GetValue()
904 form_opts['plot_sub_bline'] = False # self.plot_sub_bline.IsChecked()
905 # form_opts['show_centroid'] = self.show_centroid.IsChecked()
906 form_opts['show_peakrange'] = self.show_peakrange.IsChecked()
907 form_opts['show_fitrange'] = self.show_fitrange.IsChecked()
908 return form_opts
910 def onFitBaseline(self, evt=None):
911 opts = self.read_form()
912 bline_form = opts.get('baseline_form', 'no baseline')
913 if bline_form.startswith('no base'):
914 return
915 cmd = """{gname:s}.ydat = 1.0*{gname:s}.{array_name:s}
916pre_edge_baseline(energy={gname:s}.energy, norm={gname:s}.ydat, group={gname:s}, form='{baseline_form:s}',
917elo={elo:.3f}, ehi={ehi:.3f}, emin={emin:.3f}, emax={emax:.3f})"""
918 self.larch_eval(cmd.format(**opts))
920 dgroup = self.controller.get_group()
921 ppeaks = dgroup.prepeaks
922 dgroup.centroid_msg = "%.4f +/- %.4f eV" % (ppeaks.centroid,
923 ppeaks.delta_centroid)
925 self.message.SetLabel("Centroid= %s" % dgroup.centroid_msg)
927 if '+' in bline_form:
928 bforms = [f.lower() for f in bline_form.split('+')]
929 else:
930 bforms = [bline_form.lower(), '']
932 poly_model = peak_model = None
933 for bform in bforms:
934 if bform.startswith('line'): poly_model = 'Linear'
935 if bform.startswith('const'): poly_model = 'Constant'
936 if bform.startswith('quad'): poly_model = 'Quadratic'
937 if bform.startswith('loren'): peak_model = 'Lorentzian'
938 if bform.startswith('guass'): peak_model = 'Gaussian'
939 if bform.startswith('voigt'): peak_model = 'Voigt'
941 if peak_model is not None:
942 if 'bpeak_' in self.fit_components:
943 self.onDeleteComponent(prefix='bpeak_')
944 self.addModel(model=peak_model, prefix='bpeak_', isbkg=True)
946 if poly_model is not None:
947 if 'bpoly_' in self.fit_components:
948 self.onDeleteComponent(prefix='bpoly_')
949 self.addModel(model=poly_model, prefix='bpoly_', isbkg=True)
951 for prefix in ('bpeak_', 'bpoly_'):
952 cmp = self.fit_components[prefix]
953 # cmp.bkgbox.SetValue(1)
954 self.fill_model_params(prefix, dgroup.prepeaks.fit_details.params)
956 self.fill_form(dgroup)
957 self.fitmodel_btn.Enable()
958 self.fitselected_btn.Enable()
960 i1, i2 = self.get_xranges(dgroup.energy)
962 dgroup.yfit = dgroup.xfit = 0.0*dgroup.energy[i1:i2]
964 self.onPlot(baseline_only=True)
965 # self.savebline_btn.Enable()
967 def onSaveBaseline(self, evt=None):
968 opts = self.read_form()
970 dgroup = self.controller.get_group()
971 ppeaks = dgroup.prepeaks
973 deffile = dgroup.filename.replace('.', '_') + '_baseline.dat'
974 sfile = FileSave(self, 'Save Pre-edge Peak Baseline', default_file=deffile,
975 wildcard=DataWcards)
976 if sfile is None:
977 return
978 opts['savefile'] = sfile
979 opts['centroid'] = ppeaks.centroid
980 opts['delta_centroid'] = ppeaks.delta_centroid
982 cmd = """# save baseline script:
983header = ['baseline data from "{filename:s}"',
984 'baseline form = "{baseline_form:s}"',
985 'baseline fit range emin = {emin:.3f}',
986 'baseline fit range emax = {emax:.3f}',
987 'baseline peak range elo = {elo:.3f}',
988 'baseline peak range ehi = {ehi:.3f}',
989 'prepeak centroid energy = {centroid:.3f} +/- {delta_centroid:.3f} eV']
990i0 = index_of({gname:s}.energy, {gname:s}.prepeaks.energy[0])
991i1 = index_of({gname:s}.energy, {gname:s}.prepeaks.energy[-1])
992{gname:s}.prepeaks.full_baseline = {gname:s}.norm*1.0
993{gname:s}.prepeaks.full_baseline[i0:i1+1] = {gname:s}.prepeaks.baseline
994write_ascii('{savefile:s}', {gname:s}.energy, {gname:s}.norm, {gname:s}.prepeaks.full_baseline,
995 header=header, label='energy norm baseline')
996 """
997 self.larch_eval(cmd.format(**opts))
1000 def fill_model_params(self, prefix, params):
1001 comp = self.fit_components[prefix]
1002 parwids = comp.parwids
1003 for pname, par in params.items():
1004 pname = prefix + pname
1005 if pname in parwids:
1006 wids = parwids[pname]
1007 if wids.minval is not None:
1008 wids.minval.SetValue(par.min)
1009 if wids.maxval is not None:
1010 wids.maxval.SetValue(par.max)
1011 varstr = 'vary' if par.vary else 'fix'
1012 if par.expr is not None:
1013 varstr = 'constrain'
1014 if wids.vary is not None:
1015 wids.vary.SetStringSelection(varstr)
1016 wids.value.SetValue(par.value)
1018 def onPlotModel(self, evt=None):
1019 dgroup = self.controller.get_group()
1020 g = self.build_fitmodel(dgroup.groupname)
1021 self.onPlot(show_init=True)
1023 def onPlot(self, evt=None, baseline_only=False, show_init=False):
1024 opts = self.read_form()
1025 dgroup = self.controller.get_group()
1026 opts['group'] = opts['gname']
1027 self.larch_eval(COMMANDS['prepeaks_setup'].format(**opts))
1029 ppeaks_opts = dict(array=opts['array_name'], elo=opts['elo'],
1030 ehi=opts['ehi'], emin=opts['emin'],
1031 emax=opts['emax'])
1032 dgroup.journal.add_ifnew('prepeaks_setup', ppeaks_opts)
1034 cmd = "plot_prepeaks_fit"
1035 args = ['{gname}']
1036 if baseline_only:
1037 cmd = "plot_prepeaks_baseline"
1038 else:
1039 args.append("show_init=%s" % (show_init))
1040 cmd = "%s(%s)" % (cmd, ', '.join(args))
1041 self.larch_eval(cmd.format(**opts))
1042 self.controller.set_focus()
1044 def addModel(self, event=None, model=None, prefix=None, isbkg=False):
1045 if model is None and event is not None:
1046 model = event.GetString()
1047 if model is None or model.startswith('<'):
1048 return
1050 self.models_peaks.SetSelection(0)
1051 self.models_other.SetSelection(0)
1053 if prefix is None:
1054 p = model[:5].lower()
1055 curmodels = ["%s%i_" % (p, i+1) for i in range(1+len(self.fit_components))]
1056 for comp in self.fit_components:
1057 if comp in curmodels:
1058 curmodels.remove(comp)
1060 prefix = curmodels[0]
1062 label = "%s(prefix='%s')" % (model, prefix)
1063 title = "%s: %s " % (prefix[:-1], model)
1064 title = prefix[:-1]
1065 mclass_kws = {'prefix': prefix}
1066 if 'step' in model.lower():
1067 form = model.lower().replace('step', '').strip()
1068 if form.startswith('err'):
1069 form = 'erf'
1070 label = "Step(form='%s', prefix='%s')" % (form, prefix)
1071 title = "%s: Step %s" % (prefix[:-1], form[:3])
1072 mclass = lm_models.StepModel
1073 mclass_kws['form'] = form
1074 minst = mclass(form=form, prefix=prefix)
1075 else:
1076 if model in ModelFuncs:
1077 mclass = getattr(lm_models, ModelFuncs[model])
1078 else:
1079 mclass = getattr(lm_models, model+'Model')
1081 minst = mclass(prefix=prefix)
1083 panel = GridPanel(self.mod_nb, ncols=2, nrows=5, pad=1, itemstyle=CEN)
1084 panel.SetFont(Font(FONTSIZE))
1086 def SLabel(label, size=(80, -1), **kws):
1087 return SimpleText(panel, label,
1088 size=size, style=wx.ALIGN_LEFT, **kws)
1089 usebox = Check(panel, default=True, label='Use in Fit?', size=(100, -1))
1090 bkgbox = Check(panel, default=False, label='Is Baseline?', size=(125, -1))
1091 if isbkg:
1092 bkgbox.SetValue(1)
1094 delbtn = Button(panel, 'Delete This Component', size=(200, -1),
1095 action=partial(self.onDeleteComponent, prefix=prefix))
1097 pick2msg = SimpleText(panel, " ", size=(125, -1))
1098 pick2btn = Button(panel, 'Pick Values from Plot', size=(200, -1),
1099 action=partial(self.onPick2Points, prefix=prefix))
1101 # SetTip(mname, 'Label for the model component')
1102 SetTip(usebox, 'Use this component in fit?')
1103 SetTip(bkgbox, 'Label this component as "background" when plotting?')
1104 SetTip(delbtn, 'Delete this model component')
1105 SetTip(pick2btn, 'Select X range on Plot to Guess Initial Values')
1107 panel.Add(SLabel(label, size=(275, -1), colour='#0000AA'),
1108 dcol=4, style=wx.ALIGN_LEFT, newrow=True)
1109 panel.Add(usebox, dcol=2)
1110 panel.Add(bkgbox, dcol=1, style=RIGHT)
1112 panel.Add(pick2btn, dcol=2, style=wx.ALIGN_LEFT, newrow=True)
1113 panel.Add(pick2msg, dcol=3, style=wx.ALIGN_RIGHT)
1114 panel.Add(delbtn, dcol=2, style=wx.ALIGN_RIGHT)
1116 panel.Add(SLabel("Parameter "), style=wx.ALIGN_LEFT, newrow=True)
1117 panel.AddMany((SLabel(" Value"), SLabel(" Type"), SLabel(' Bounds'),
1118 SLabel(" Min", size=(60, -1)),
1119 SLabel(" Max", size=(60, -1)), SLabel(" Expression")))
1121 parwids = {}
1122 parnames = sorted(minst.param_names)
1124 for a in minst._func_allargs:
1125 pname = "%s%s" % (prefix, a)
1126 if (pname not in parnames and
1127 a in minst.param_hints and
1128 a not in minst.independent_vars):
1129 parnames.append(pname)
1131 for pname in parnames:
1132 sname = pname[len(prefix):]
1133 hints = minst.param_hints.get(sname, {})
1135 par = Parameter(name=pname, value=0, vary=True)
1136 if 'min' in hints:
1137 par.min = hints['min']
1138 if 'max' in hints:
1139 par.max = hints['max']
1140 if 'value' in hints:
1141 par.value = hints['value']
1142 if 'expr' in hints:
1143 par.expr = hints['expr']
1145 pwids = ParameterWidgets(panel, par, name_size=100, expr_size=150,
1146 float_size=80, prefix=prefix,
1147 widgets=('name', 'value', 'minval',
1148 'maxval', 'vary', 'expr'))
1149 parwids[par.name] = pwids
1150 panel.Add(pwids.name, newrow=True)
1152 panel.AddMany((pwids.value, pwids.vary, pwids.bounds,
1153 pwids.minval, pwids.maxval, pwids.expr))
1155 for sname, hint in minst.param_hints.items():
1156 pname = "%s%s" % (prefix, sname)
1157 if 'expr' in hint and pname not in parnames:
1158 par = Parameter(name=pname, value=0, expr=hint['expr'])
1159 pwids = ParameterWidgets(panel, par, name_size=100, expr_size=400,
1160 float_size=80, prefix=prefix,
1161 widgets=('name', 'value', 'expr'))
1162 parwids[par.name] = pwids
1163 panel.Add(pwids.name, newrow=True)
1164 panel.Add(pwids.value)
1165 panel.Add(pwids.expr, dcol=5, style=wx.ALIGN_RIGHT)
1166 pwids.value.Disable()
1168 fgroup = Group(prefix=prefix, title=title, mclass=mclass,
1169 mclass_kws=mclass_kws, usebox=usebox, panel=panel,
1170 parwids=parwids, float_size=65, expr_size=150,
1171 pick2_msg=pick2msg, bkgbox=bkgbox)
1174 self.fit_components[prefix] = fgroup
1175 panel.pack()
1176 if self.mod_nb_init:
1177 self.mod_nb.DeletePage(0)
1178 self.mod_nb_init = False
1180 self.mod_nb.AddPage(panel, title, True)
1181 sx,sy = self.GetSize()
1182 self.SetSize((sx, sy+1))
1183 self.SetSize((sx, sy))
1184 self.fitmodel_btn.Enable()
1185 self.fitselected_btn.Enable()
1188 def onDeleteComponent(self, evt=None, prefix=None):
1189 fgroup = self.fit_components.get(prefix, None)
1190 if fgroup is None:
1191 return
1193 for i in range(self.mod_nb.GetPageCount()):
1194 if fgroup.title == self.mod_nb.GetPageText(i):
1195 self.mod_nb.DeletePage(i)
1197 for attr in dir(fgroup):
1198 setattr(fgroup, attr, None)
1200 self.fit_components.pop(prefix)
1201 if len(self.fit_components) < 1:
1202 self.fitmodel_btn.Disable()
1203 self.fitselected_btn.Enable()
1205 def onPick2EraseTimer(self, evt=None):
1206 """erases line trace showing automated 'Pick 2' guess """
1207 self.pick2erase_timer.Stop()
1208 panel = self.pick2erase_panel
1209 ntrace = panel.conf.ntrace - 1
1210 trace = panel.conf.get_mpl_line(ntrace)
1211 panel.conf.get_mpl_line(ntrace).set_data(np.array([]), np.array([]))
1212 panel.conf.ntrace = ntrace
1213 panel.draw()
1215 def onPick2Timer(self, evt=None):
1216 """checks for 'Pick 2' events, and initiates 'Pick 2' guess
1217 for a model from the selected data range
1218 """
1219 try:
1220 plotframe = self.controller.get_display(win=1)
1221 curhist = plotframe.cursor_hist[:]
1222 plotframe.Raise()
1223 except:
1224 return
1226 if (time.time() - self.pick2_t0) > self.pick2_timeout:
1227 msg = self.pick2_group.pick2_msg.SetLabel(" ")
1228 plotframe.cursor_hist = []
1229 self.pick2_timer.Stop()
1230 return
1232 if len(curhist) < 2:
1233 self.pick2_group.pick2_msg.SetLabel("%i/2" % (len(curhist)))
1234 return
1236 self.pick2_group.pick2_msg.SetLabel("done.")
1237 self.pick2_timer.Stop()
1239 # guess param values
1240 xcur = (curhist[0][0], curhist[1][0])
1241 xmin, xmax = min(xcur), max(xcur)
1243 dgroup = getattr(self.larch.symtable, self.controller.groupname)
1244 x, y = dgroup.xdat, dgroup.ydat
1245 i0 = index_of(dgroup.xdat, xmin)
1246 i1 = index_of(dgroup.xdat, xmax)
1247 x, y = dgroup.xdat[i0:i1+1], dgroup.ydat[i0:i1+1]
1249 mod = self.pick2_group.mclass(prefix=self.pick2_group.prefix)
1250 parwids = self.pick2_group.parwids
1251 try:
1252 guesses = mod.guess(y, x=x)
1253 except:
1254 return
1255 for name, param in guesses.items():
1256 if 'amplitude' in name:
1257 param.value *= 1.5
1258 elif 'sigma' in name:
1259 param.value *= 0.75
1260 if name in parwids:
1261 parwids[name].value.SetValue(param.value)
1263 dgroup._tmp = mod.eval(guesses, x=dgroup.xdat)
1264 plotframe = self.controller.get_display(win=1)
1265 plotframe.cursor_hist = []
1266 plotframe.oplot(dgroup.xdat, dgroup._tmp)
1267 self.pick2erase_panel = plotframe.panel
1269 self.pick2erase_timer.Start(60000)
1272 def onPick2Points(self, evt=None, prefix=None):
1273 fgroup = self.fit_components.get(prefix, None)
1274 if fgroup is None:
1275 return
1277 plotframe = self.controller.get_display(win=1)
1278 plotframe.Raise()
1280 plotframe.cursor_hist = []
1281 fgroup.npts = 0
1282 self.pick2_group = fgroup
1284 if fgroup.pick2_msg is not None:
1285 fgroup.pick2_msg.SetLabel("0/2")
1287 self.pick2_t0 = time.time()
1288 self.pick2_timer.Start(1000)
1291 def onLoadFitResult(self, event=None):
1292 dlg = wx.FileDialog(self, message="Load Saved Pre-edge Model",
1293 wildcard=ModelWcards, style=wx.FD_OPEN)
1294 rfile = None
1295 if dlg.ShowModal() == wx.ID_OK:
1296 rfile = dlg.GetPath()
1297 dlg.Destroy()
1299 if rfile is None:
1300 return
1302 self.larch_eval(f"# peakmodel = read_groups('{rfile}')[1]")
1303 dat = read_groups(str(rfile))
1304 if len(dat) != 2 or not dat[0].startswith('#peakfit'):
1305 Popup(self, f" '{rfile}' is not a valid Peak Model file",
1306 "Invalid file")
1308 self.use_modelresult(dat[1])
1310 def use_modelresult(self, pkfit):
1311 for prefix in list(self.fit_components.keys()):
1312 self.onDeleteComponent(prefix=prefix)
1314 result = pkfit.result
1315 bkg_comps = pkfit.user_options['bkg_components']
1316 for comp in result.model.components:
1317 isbkg = comp.prefix in bkg_comps
1318 self.addModel(model=comp.func.__name__,
1319 prefix=comp.prefix, isbkg=isbkg)
1321 for comp in result.model.components:
1322 parwids = self.fit_components[comp.prefix].parwids
1323 for pname, par in result.params.items():
1324 if pname in parwids:
1325 wids = parwids[pname]
1326 wids.value.SetValue(result.init_values.get(pname, par.value))
1327 varstr = 'vary' if par.vary else 'fix'
1328 if par.expr is not None: varstr = 'constrain'
1329 if wids.vary is not None: wids.vary.SetStringSelection(varstr)
1330 if wids.minval is not None: wids.minval.SetValue(par.min)
1331 if wids.maxval is not None: wids.maxval.SetValue(par.max)
1333 self.fill_form(pkfit.user_options)
1336 def get_xranges(self, x):
1337 opts = self.read_form()
1338 dgroup = self.controller.get_group()
1339 en_eps = min(np.diff(dgroup.energy)) / 5.
1341 i1 = index_of(x, opts['emin'] + en_eps)
1342 i2 = index_of(x, opts['emax'] + en_eps) + 1
1343 return i1, i2
1345 def build_fitmodel(self, groupname=None):
1346 """ use fit components to build model"""
1347 # self.summary = {'components': [], 'options': {}}
1348 peaks = []
1349 cmds = ["## set up pre-edge peak parameters", "peakpars = Parameters()"]
1350 modcmds = ["## define pre-edge peak model"]
1351 modop = " ="
1352 opts = self.read_form()
1353 if groupname is None:
1354 groupname = opts['gname']
1356 opts['group'] = groupname
1357 dgroup = self.controller.get_group(groupname)
1358 self.larch_eval(COMMANDS['prepeaks_setup'].format(**opts))
1360 ppeaks_opts = dict(array=opts['array_name'], elo=opts['elo'],
1361 ehi=opts['ehi'], emin=opts['emin'],
1362 emax=opts['emax'])
1363 dgroup.journal.add_ifnew('prepeaks_setup', ppeaks_opts)
1366 for comp in self.fit_components.values():
1367 _cen, _amp = None, None
1368 if comp.usebox is not None and comp.usebox.IsChecked():
1369 for parwids in comp.parwids.values():
1370 this = parwids.param
1371 pargs = ["'%s'" % this.name, 'value=%f' % (this.value),
1372 'min=%f' % (this.min), 'max=%f' % (this.max)]
1373 if this.expr is not None:
1374 pargs.append("expr='%s'" % (this.expr))
1375 elif not this.vary:
1376 pargs.pop()
1377 pargs.pop()
1378 pargs.append("vary=False")
1380 cmds.append("peakpars.add(%s)" % (', '.join(pargs)))
1381 if this.name.endswith('_center'):
1382 _cen = this.name
1383 elif parwids.param.name.endswith('_amplitude'):
1384 _amp = this.name
1385 compargs = ["%s='%s'" % (k,v) for k,v in comp.mclass_kws.items()]
1386 modcmds.append("peakmodel %s %s(%s)" % (modop, comp.mclass.__name__,
1387 ', '.join(compargs)))
1389 modop = "+="
1390 if not comp.bkgbox.IsChecked() and _cen is not None and _amp is not None:
1391 peaks.append((_amp, _cen))
1393 if len(peaks) > 0:
1394 denom = '+'.join([p[0] for p in peaks])
1395 numer = '+'.join(["%s*%s "% p for p in peaks])
1396 cmds.append("peakpars.add('fit_centroid', expr='(%s)/(%s)')" % (numer, denom))
1398 cmds.extend(modcmds)
1399 cmds.append(COMMANDS['prepfit'].format(group=dgroup.groupname,
1400 user_opts=repr(opts)))
1402 self.larch_eval("\n".join(cmds))
1404 def onFitSelected(self, event=None):
1405 dgroup = self.controller.get_group()
1406 if dgroup is None:
1407 return
1409 opts = self.read_form()
1411 self.show_subframe('prepeak_result', PrePeakFitResultFrame,
1412 datagroup=dgroup, peakframe=self)
1414 selected_groups = self.controller.filelist.GetCheckedStrings()
1415 groups = [self.controller.file_groups[cn] for cn in selected_groups]
1416 ngroups = len(groups)
1417 for igroup, gname in enumerate(groups):
1418 dgroup = self.controller.get_group(gname)
1419 if not hasattr(dgroup, 'norm'):
1420 self.xasmain.process_normalization(dgroup)
1421 self.build_fitmodel(gname)
1422 opts['group'] = opts['gname']
1423 self.larch_eval(COMMANDS['prepeaks_setup'].format(**opts))
1425 ppeaks_opts = dict(array=opts['array_name'], elo=opts['elo'],
1426 ehi=opts['ehi'], emin=opts['emin'],
1427 emax=opts['emax'])
1428 dgroup.journal.add_ifnew('prepeaks_setup', ppeaks_opts)
1429 ppeaks = dgroup.prepeaks
1431 # add bkg_component to saved user options
1432 bkg_comps = []
1433 for label, comp in self.fit_components.items():
1434 if comp.bkgbox.IsChecked():
1435 bkg_comps.append(label)
1437 opts['bkg_components'] = bkg_comps
1438 imin, imax = self.get_xranges(dgroup.xdat)
1439 cmds = ["## do peak fit for group %s / %s " % (gname, dgroup.filename) ]
1441 yerr_type = 'set_yerr_const'
1442 yerr = getattr(dgroup, 'yerr', None)
1443 if yerr is None:
1444 if hasattr(dgroup, 'norm_std'):
1445 cmds.append("{group}.yerr = {group}.norm_std")
1446 yerr_type = 'set_yerr_array'
1447 elif hasattr(dgroup, 'mu_std'):
1448 cmds.append("{group}.yerr = {group}.mu_std/(1.e-15+{group}.edge_step)")
1449 yerr_type = 'set_yerr_array'
1450 else:
1451 cmds.append("{group}.yerr = 1")
1452 elif isinstance(dgroup.yerr, np.ndarray):
1453 yerr_type = 'set_yerr_array'
1455 cmds.extend([COMMANDS[yerr_type], COMMANDS['dofit']])
1456 cmd = '\n'.join(cmds)
1457 self.larch_eval(cmd.format(group=dgroup.groupname,
1458 imin=imin, imax=imax,
1459 user_opts=repr(opts)))
1461 pkfit = self.larch_get("peakresult")
1462 jnl = {'label': pkfit.label, 'var_names': pkfit.result.var_names,
1463 'model': repr(pkfit.result.model)}
1464 jnl.update(pkfit.user_options)
1465 dgroup.journal.add('peakfit', jnl)
1466 if igroup == 0:
1467 self.autosave_modelresult(pkfit)
1469 self.subframes['prepeak_result'].add_results(dgroup, form=opts,
1470 larch_eval=self.larch_eval,
1471 show=igroup==ngroups-1)
1473 def onFitModel(self, event=None):
1474 dgroup = self.controller.get_group()
1475 if dgroup is None:
1476 return
1477 self.build_fitmodel(dgroup.groupname)
1478 opts = self.read_form()
1480 dgroup = self.controller.get_group()
1481 opts['group'] = opts['gname']
1482 self.larch_eval(COMMANDS['prepeaks_setup'].format(**opts))
1484 ppeaks_opts = dict(array=opts['array_name'], elo=opts['elo'],
1485 ehi=opts['ehi'], emin=opts['emin'],
1486 emax=opts['emax'])
1487 dgroup.journal.add_ifnew('prepeaks_setup', ppeaks_opts)
1489 ppeaks = dgroup.prepeaks
1491 # add bkg_component to saved user options
1492 bkg_comps = []
1493 for label, comp in self.fit_components.items():
1494 if comp.bkgbox.IsChecked():
1495 bkg_comps.append(label)
1496 opts['bkg_components'] = bkg_comps
1498 imin, imax = self.get_xranges(dgroup.xdat)
1500 cmds = ["## do peak fit: "]
1501 yerr_type = 'set_yerr_const'
1502 yerr = getattr(dgroup, 'yerr', None)
1503 if yerr is None:
1504 if hasattr(dgroup, 'norm_std'):
1505 cmds.append("{group}.yerr = {group}.norm_std")
1506 yerr_type = 'set_yerr_array'
1507 elif hasattr(dgroup, 'mu_std'):
1508 cmds.append("{group}.yerr = {group}.mu_std/(1.e-15+{group}.edge_step)")
1509 yerr_type = 'set_yerr_array'
1510 else:
1511 cmds.append("{group}.yerr = 1")
1512 elif isinstance(dgroup.yerr, np.ndarray):
1513 yerr_type = 'set_yerr_array'
1515 cmds.extend([COMMANDS[yerr_type], COMMANDS['dofit']])
1516 cmd = '\n'.join(cmds)
1517 self.larch_eval(cmd.format(group=dgroup.groupname,
1518 imin=imin, imax=imax,
1519 user_opts=repr(opts)))
1521 # journal about peakresult
1522 pkfit = self.larch_get("peakresult")
1523 jnl = {'label': pkfit.label, 'var_names': pkfit.result.var_names,
1524 'model': repr(pkfit.result.model)}
1525 jnl.update(pkfit.user_options)
1526 dgroup.journal.add('peakfit', jnl)
1528 self.autosave_modelresult(pkfit)
1529 self.onPlot()
1530 self.showresults_btn.Enable()
1533 self.show_subframe('prepeak_result', PrePeakFitResultFrame, peakframe=self)
1534 self.subframes['prepeak_result'].add_results(dgroup, form=opts,
1535 larch_eval=self.larch_eval)
1537 def onShowResults(self, event=None):
1538 self.show_subframe('prepeak_result', PrePeakFitResultFrame,
1539 peakframe=self)
1542 def update_start_values(self, params):
1543 """fill parameters with best fit values"""
1544 allparwids = {}
1545 for comp in self.fit_components.values():
1546 if comp.usebox is not None and comp.usebox.IsChecked():
1547 for name, parwids in comp.parwids.items():
1548 allparwids[name] = parwids
1550 for pname, par in params.items():
1551 if pname in allparwids:
1552 allparwids[pname].value.SetValue(par.value)
1554 def autosave_modelresult(self, result, fname=None):
1555 """autosave model result to user larch folder"""
1556 confdir = self.controller.larix_folder
1557 if fname is None:
1558 fname = 'autosave_peakfile.modl'
1559 save_groups(os.path.join(confdir, fname), ['#peakfit 1.0', result])