Coverage for larch/wxxas/feffit_panel.py: 0%
1676 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
1import time
2import sys
3import ast
4import shutil
5import string
6import json
7import math
8from copy import deepcopy
9from sys import exc_info
10from string import printable
11from functools import partial
12from pathlib import Path
14import numpy as np
15np.seterr(all='ignore')
17import wx
18import wx.lib.scrolledpanel as scrolled
20import wx.dataview as dv
22from lmfit import Parameter
23from lmfit.model import (save_modelresult, load_modelresult,
24 save_model, load_model)
26import lmfit.models as lm_models
28from larch import Group, site_config
29from larch.math import index_of
30from larch.fitting import group2params, param
31from larch.utils.jsonutils import encode4js, decode4js
32from larch.inputText import is_complete
33from larch.utils import fix_varname, fix_filename, gformat, mkdir, isValidName
34from larch.io.export_modelresult import export_modelresult
35from larch.xafs import feffit_report, feffpath
36from larch.xafs.feffdat import FEFFDAT_VALUES
37from larch.xafs.xafsutils import FT_WINDOWS
39from larch.wxlib import (ReportFrame, BitmapButton, FloatCtrl, FloatSpin,
40 SetTip, GridPanel, get_icon, SimpleText, pack,
41 Button, HLine, Choice, Check, MenuItem, GUIColors,
42 CEN, RIGHT, LEFT, FRAMESTYLE, Font, FONTSIZE,
43 COLORS, set_color, FONTSIZE_FW, FileSave,
44 FileOpen, flatnotebook, EditableListBox, Popup,
45 ExceptionPopup)
47from larch.wxlib.parameter import ParameterWidgets
48from larch.wxlib.plotter import last_cursor_pos
49from .taskpanel import TaskPanel
51from .config import (Feffit_KWChoices, Feffit_SpaceChoices,
52 Feffit_PlotChoices, make_array_choice,
53 PlotWindowChoices)
55DVSTYLE = dv.DV_SINGLE|dv.DV_VERT_RULES|dv.DV_ROW_LINES
57# PlotOne_Choices = [chik, chirmag, chirre, chirmr]
59Plot1_Choices = make_array_choice(['chi','chir_mag', 'chir_re', 'chir_mag+chir_re', 'chiq'])
60Plot2_Choices = make_array_choice(['noplot', 'chi','chir_mag', 'chir_re', 'chir_mag+chir_re', 'chiq'])
62# Plot2_Choices = [noplot] + Plot1_Choices
64ScriptWcards = "Fit Models(*.lar)|*.lar|All files (*.*)|*.*"
66MIN_CORREL = 0.10
68COMMANDS = {}
69COMMANDS['feffit_top'] = """##### FEFFIT Commands
70##
71## saved {ctime}
72## to use from python, uncomment these import lines:
73##
75#from larch.xafs import feffit, feffit_dataset, feffit_transform, feffit_report
76#from larch.xafs import pre_edge, autobk, xftf, xftr, ff2chi, feffpath
77#from larch.fitting import param_group, param
78#from larch.io import read_ascii, read_athena, read_xdi, read_specfile
79#
80#### for interactive plotting from python (but not the Larch shell!) use:
81#from larch.wxlib.xafsplots import plot_chik, plot_chir
82#from wxmplot.interactive import get_wxapp
83#wxapp = get_wxapp() # <- needed for plotting to work from python command-line
84####
85####
86"""
88COMMANDS['data_source'] = """### you will need to add how the data chi(k) gets built:
89###
90## data group = {groupname}
91## from source = {filename}
92## some processing steps for this group (comment out as needed):
93"""
95COMMANDS['xft'] = """# ffts on group {groupname:s}
96xftf({groupname:s}, kmin={kmin:.3f}, kmax={kmax:.3f}, dk={dk:.3f}, window='{kwindow:s}', kweight={kweight:.3f})
97xftr({groupname:s}, rmin={rmin:.3f}, rmax={rmax:.3f}, dr={dr:.3f}, window='{rwindow:s}')
98"""
100COMMANDS['feffit_params_init'] = """# create feffit Parameter Group to hold fit parameters
101_feffit_params = param_group(reff=-1.0)
102"""
104COMMANDS['feffit_trans'] = """# define Fourier transform and fitting space
105_feffit_trans = feffit_transform(kmin={fit_kmin:.3f}, kmax={fit_kmax:.3f}, dk={fit_dk:.4f}, kw={fit_kwstring:s},
106 window='{fit_kwindow:s}', fitspace='{fit_space:s}', rmin={fit_rmin:.3f}, rmax={fit_rmax:.3f})
107"""
110COMMANDS['feffit_dataset_init'] = """# create empty dataset
111_feffit_dataset = feffit_dataset(transform=_feffit_trans)
112"""
115COMMANDS['paths_init'] = """# make sure dictionary for Feff Paths exists
116try:
117 npaths = len(_feffpaths.keys())
118except:
119 _feffcache = {'paths':{}, 'runs':{}} # group of all paths, info about Feff runs
120 _feffpaths = {} # dict of paths currently in use, copied from _feffcache.paths
121#endtry
122"""
124COMMANDS['paths_reset'] = """# clear existing paths
125npaths = 0
126_feffpaths = {}
127#endtry
128"""
130COMMANDS['cache_path'] = """
131_feffcache['paths']['{title:s}'] = feffpath('{fullpath:s}',
132 label='{title:s}',feffrun='{feffrun:s}', degen=1)
133"""
135COMMANDS['use_path'] = """
136_feffpaths['{title:s}'] = use_feffpath(_feffcache['paths'], '{title:s}',
137 s02='{amp:s}', e0='{e0:s}',
138 deltar='{delr:s}', sigma2='{sigma2:s}',
139 third='{third:s}', ei='{ei:s}', use={use})
140"""
142COMMANDS['ff2chi'] = """# sum paths using a list of paths and a group of parameters
143_feffit_dataset = feffit_dataset(data={groupname:s}, transform={trans:s},
144 refine_bkg={refine_bkg},
145 paths={paths:s})
146_feffit_dataset.model = ff2chi({paths:s}, paramgroup=_feffit_params)
147"""
149COMMANDS['ff2chi_nodata'] = """# sum paths using a list of paths and a group of parameters
150_feffit_dataset = feffit_dataset(transform={trans:s}, paths={paths:s})
151_feffit_dataset.model = ff2chi({paths:s}, paramgroup=_feffit_params)
152"""
154COMMANDS['do_feffit'] = """# build feffit dataset, run feffit
155_feffit_dataset = feffit_dataset(data={groupname:s}, transform={trans:s},
156 refine_bkg={refine_bkg},
157 paths={paths:s})
158_feffit_result = feffit({params}, _feffit_dataset)
159if not hasattr({groupname:s}, 'feffit_history'): {groupname}.feffit_history = []
160{groupname:s}.feffit_history.insert(0, _feffit_result)
161"""
163COMMANDS['path2chi'] = """# generate chi(k) and chi(R) for each path
164for label, path in {paths_name:s}.items():
165 path.calc_chi_from_params({pargroup_name:s})
166 xftf(path, kmin={kmin:.3f}, kmax={kmax:.3f}, dk={dk:.3f},
167 window='{kwindow:s}', kweight={kweight:.3f})
168#endfor
169"""
171def get_commands(textlines):
172 """return list of complete larch command / python function calls
173 from a list of text lines such as 'command history'
174 """
175 out = []
176 work_lines = []
177 for line in textlines:
178 work_lines.append(line)
179 work = ' '.join(work_lines)
180 if is_complete(work):
181 out.append(work)
182 work_lines.clear()
183 out.extend(work_lines) # add any dangling text
184 return out
187class ParametersModel(dv.DataViewIndexListModel):
188 def __init__(self, paramgroup, selected=None, pathkeys=None):
189 dv.DataViewIndexListModel.__init__(self, 0)
190 self.data = []
191 if selected is None:
192 selected = []
193 self.selected = selected
195 if pathkeys is None:
196 pathkeys = []
197 self.pathkeys = pathkeys
199 self.paramgroup = paramgroup
200 self.read_data()
202 def set_data(self, paramgroup, selected=None, pathkeys=None):
203 self.paramgroup = paramgroup
204 if selected is not None:
205 self.selected = selected
206 if pathkeys is not None:
207 self.pathkeys = pathkeys
208 self.read_data()
210 def read_data(self):
211 self.data = []
212 if self.paramgroup is None:
213 self.data.append(['param name', False, 'vary', '0.0'])
214 else:
215 for pname, par in group2params(self.paramgroup).items():
216 if any([pname.endswith('_%s' % phash) for phash in self.pathkeys]):
217 continue
218 ptype = 'vary'
219 if not par.vary:
220 pytype = 'fixed'
221 if getattr(par, 'skip', None) not in (False, None):
222 ptype = 'skip'
223 par.skip = ptype == 'skip'
224 try:
225 value = str(par.value)
226 except:
227 value = 'INVALID '
228 if par.expr is not None:
229 ptype = 'constraint'
230 value = "%s := %s" % (value, par.expr)
231 sel = pname in self.selected
232 self.data.append([pname, sel, ptype, value])
233 self.Reset(len(self.data))
235 def select_all(self, value=True):
236 self.selected = []
237 for irow, row in enumerate(self.data):
238 self.SetValueByRow(value, irow, 1)
239 if value:
240 self.selected.append(row[0])
242 def select_none(self):
243 self.select_all(value=False)
245 def GetColumnType(self, col):
246 return "bool" if col == 2 else "string"
248 def GetValueByRow(self, row, col):
249 return self.data[row][col]
251 def SetValueByRow(self, value, row, col):
252 self.data[row][col] = value
253 return True
255 def GetColumnCount(self):
256 return len(self.data[0])
258 def GetCount(self):
259 return len(self.data)
261 def GetAttrByRow(self, row, col, attr):
262 """set row/col attributes (color, etc)"""
263 ptype = self.data[row][2]
264 if ptype == 'vary':
265 attr.SetColour('#000000')
266 elif ptype == 'fixed':
267 attr.SetColour('#AA2020')
268 elif ptype == 'skip':
269 attr.SetColour('#50AA50')
270 else:
271 attr.SetColour('#2010BB')
272 return True
274class EditParamsFrame(wx.Frame):
275 """ edit parameters"""
276 def __init__(self, parent=None, feffit_panel=None,
277 paramgroup=None, selected=None):
278 wx.Frame.__init__(self, None, -1,
279 'Edit Feffit Parameters',
280 style=FRAMESTYLE, size=(550, 325))
282 self.parent = parent
283 self.feffit_panel = feffit_panel
284 self.paramgroup = paramgroup
286 spanel = scrolled.ScrolledPanel(self, size=(500, 275))
287 spanel.SetBackgroundColour('#EEEEEE')
289 self.font_fixedwidth = wx.Font(FONTSIZE_FW, wx.MODERN, wx.NORMAL, wx.NORMAL)
291 self.dvc = dv.DataViewCtrl(spanel, style=DVSTYLE)
292 self.dvc.SetFont(self.font_fixedwidth)
293 self.SetMinSize((500, 250))
295 self.model = ParametersModel(paramgroup, selected)
296 self.dvc.AssociateModel(self.model)
298 sizer = wx.BoxSizer(wx.VERTICAL)
299 sizer.Add(self.dvc, 1, LEFT|wx.ALL|wx.GROW)
300 pack(spanel, sizer)
302 spanel.SetupScrolling()
304 toppan = GridPanel(self, ncols=4, pad=1, itemstyle=LEFT)
306 bkws = dict(size=(200, -1))
307 toppan.Add(Button(toppan, "Select All", action=self.onSelAll, size=(175, -1)))
308 toppan.Add(Button(toppan, "Select None", action=self.onSelNone, size=(175, -1)))
309 toppan.Add(Button(toppan, "Select Unused Variables", action=self.onSelUnused, size=(200, -1)))
310 toppan.Add(Button(toppan, "Remove Selected", action=self.onRemove, size=(175,-1)), newrow=True)
311 toppan.Add(Button(toppan, "'Skip' Selected", action=self.onSkip, size=(175, -1)))
312 toppan.Add(Button(toppan, "Force Refresh", action=self.onRefresh, size=(200, -1)))
313 npan = wx.Panel(toppan)
314 nsiz = wx.BoxSizer(wx.HORIZONTAL)
316 self.par_name = wx.TextCtrl(npan, -1, value='par_name', size=(125, -1),
317 style=wx.TE_PROCESS_ENTER)
318 self.par_expr = wx.TextCtrl(npan, -1, value='<expression or value>', size=(250, -1),
319 style=wx.TE_PROCESS_ENTER)
320 nsiz.Add(SimpleText(npan, "Add Parameter:"), 0)
321 nsiz.Add(self.par_name, 0)
322 nsiz.Add(self.par_expr, 1, wx.GROW|wx.ALL)
323 nsiz.Add(Button(npan, label='Add', action=self.onAddParam), 0)
324 pack(npan, nsiz)
326 toppan.Add(npan, dcol=4, newrow=True)
327 toppan.Add(HLine(toppan, size=(500, 2)), dcol=5, newrow=True)
328 toppan.pack()
330 mainsizer = wx.BoxSizer(wx.VERTICAL)
331 mainsizer.Add(toppan, 0, wx.GROW|wx.ALL, 1)
332 mainsizer.Add(spanel, 1, wx.GROW|wx.ALL, 1)
333 pack(self, mainsizer)
335 columns = [('Parameter', 150, 'text'),
336 ('Select', 75, 'bool'),
337 ('Type', 75, 'text'),
338 ('Value', 200, 'text')]
340 for icol, dat in enumerate(columns):
341 label, width, dtype = dat
342 method = self.dvc.AppendTextColumn
343 mode = dv.DATAVIEW_CELL_EDITABLE
344 if dtype == 'bool':
345 method = self.dvc.AppendToggleColumn
346 mode = dv.DATAVIEW_CELL_ACTIVATABLE
347 method(label, icol, width=width, mode=mode)
348 c = self.dvc.Columns[icol]
349 c.Alignment = c.Renderer.Alignment = wx.ALIGN_LEFT
350 c.SetSortable(False)
352 self.dvc.EnsureVisible(self.model.GetItem(0))
353 self.Bind(wx.EVT_CLOSE, self.onClose)
355 self.Show()
356 self.Raise()
357 wx.CallAfter(self.onSelUnused)
359 def onSelAll(self, event=None):
360 self.model.select_all()
361 self.model.read_data()
363 def onSelNone(self, event=None):
364 self.model.select_none()
365 self.model.read_data()
367 def onSelUnused(self, event=None):
368 curr_syms = self.feffit_panel.get_used_params()
369 unused = []
370 for pname, par in group2params(self.paramgroup).items():
371 if pname not in curr_syms: # and par.vary:
372 unused.append(pname)
373 self.model.set_data(self.paramgroup, selected=unused,
374 pathkeys=self.feffit_panel.get_pathkeys())
376 def onRemove(self, event=None):
377 out = []
378 for pname, sel, ptype, val in self.model.data:
379 if sel:
380 out.append(pname)
381 nout = len(out)
383 msg = f"Remove {nout:d} Parameters? \n This is not easy to undo!"
384 dlg = wx.MessageDialog(self, msg, 'Warning', wx.YES | wx.NO )
385 if (wx.ID_YES == dlg.ShowModal()):
386 for pname, sel, ptype, val in self.model.data:
387 if sel:
388 out.append(pname)
389 if hasattr(self.paramgroup, pname):
390 delattr(self.paramgroup, pname)
392 self.model.set_data(self.paramgroup, selected=None,
393 pathkeys=self.feffit_panel.get_pathkeys())
394 self.model.read_data()
395 self.feffit_panel.get_pathpage('parameters').Rebuild()
396 dlg.Destroy()
398 def onSkip(self, event=None):
399 for pname, sel, ptype, val in self.model.data:
400 if sel:
401 par = getattr(self.paramgroup, pname, None)
402 if par is not None:
403 par.skip = True
404 self.model.read_data()
405 self.feffit_panel.get_pathpage('parameters').Rebuild()
408 def onAddParam(self, event=None):
409 par_name = self.par_name.GetValue()
410 par_expr = self.par_expr.GetValue()
412 try:
413 val = float(par_expr)
414 ptype = 'vary'
415 except:
416 val = par_expr
417 ptype = 'expr'
419 if ptype == 'vary':
420 cmd = f"_feffit_params.{par_name} = param({val}, vary=True)"
421 else:
422 cmd = f"_feffit_params.{par_name} = param(expr='{val}')"
424 self.feffit_panel.larch_eval(cmd)
425 self.onRefresh()
427 def onRefresh(self, event=None):
428 self.paramgroup = self.feffit_panel.get_paramgroup()
429 self.model.set_data(self.paramgroup,
430 pathkeys=self.feffit_panel.get_pathkeys())
431 self.model.read_data()
432 self.feffit_panel.get_pathpage('parameters').Rebuild()
434 def onClose(self, event=None):
435 self.Destroy()
438class FeffitParamsPanel(wx.Panel):
439 def __init__(self, parent=None, feffit_panel=None, **kws):
440 wx.Panel.__init__(self, parent, -1, size=(550, 250))
441 self.feffit_panel = feffit_panel
442 self.parwids = {}
443 self.SetFont(Font(FONTSIZE))
444 spanel = scrolled.ScrolledPanel(self)
445 spanel.SetSize((250, 250))
446 spanel.SetMinSize((50, 50))
447 panel = self.panel = GridPanel(spanel, ncols=8, nrows=30, pad=1, itemstyle=LEFT)
448 panel.SetFont(Font(FONTSIZE))
450 def SLabel(label, size=(80, -1), **kws):
451 return SimpleText(panel, label, size=size, style=wx.ALIGN_LEFT, **kws)
453 panel.Add(SLabel("Feffit Parameters ", colour='#0000AA', size=(200, -1)), dcol=2)
454 panel.Add(Button(panel, 'Edit Parameters', action=self.onEditParams), dcol=2)
455 panel.Add(Button(panel, 'Force Refresh', action=self.Rebuild), dcol=3)
457 panel.Add(SLabel("Parameter "), style=wx.ALIGN_LEFT, newrow=True)
458 panel.AddMany((SLabel(" Value"), SLabel(" Type"), SLabel(' Bounds'),
459 SLabel(" Min", size=(60, -1)),
460 SLabel(" Max", size=(60, -1)),
461 SLabel(" Expression")))
463 self.update()
464 panel.pack()
465 ssizer = wx.BoxSizer(wx.VERTICAL)
466 ssizer.Add(panel, 1, wx.GROW|wx.ALL, 2)
467 pack(spanel, ssizer)
469 spanel.SetupScrolling()
470 mainsizer = wx.BoxSizer(wx.VERTICAL)
471 mainsizer.Add(spanel, 1, wx.GROW|wx.ALL, 2)
472 pack(self, mainsizer)
474 def Rebuild(self, event=None):
475 for pname, parwid in self.parwids.items():
476 for x in parwid.widgets:
477 x.Destroy()
478 self.panel.irow = 1
479 self.parwids = {}
480 self.update()
482 def set_init_values(self, params):
483 for pname, par in params.items():
484 if pname in self.parwids and par.vary:
485 stderr = getattr(par, 'stderr', 0.001)
486 try:
487 prec = max(1, min(8, round(2-math.log10(stderr))))
488 except:
489 prec = 5
490 self.parwids[pname].value.SetValue(("%%.%.df" % prec) % par.value)
492 def update(self):
493 pargroup = self.feffit_panel.get_paramgroup()
494 hashkeys = self.feffit_panel.get_pathkeys()
495 params = group2params(pargroup)
496 for pname, par in params.items():
497 if any([pname.endswith('_%s' % phash) for phash in hashkeys]):
498 continue
499 if pname not in self.parwids and not hasattr(par, '_is_pathparam'):
500 pwids = ParameterWidgets(self.panel, par, name_size=120,
501 expr_size=200, float_size=85,
502 with_skip=True,
503 widgets=('name', 'value',
504 'minval', 'maxval',
505 'vary', 'expr'))
507 self.parwids[pname] = pwids
508 self.panel.Add(pwids.name, newrow=True)
509 self.panel.AddMany((pwids.value, pwids.vary, pwids.bounds,
510 pwids.minval, pwids.maxval, pwids.expr))
511 self.panel.pack()
513 pwids = self.parwids[pname]
514 varstr = 'vary' if par.vary else 'fix'
515 if par.expr is not None:
516 varstr = 'constrain'
517 pwids.expr.SetValue(par.expr)
518 if getattr(par, 'skip', None) not in (False, None):
519 varstr = 'skip'
520 pwids.vary.SetStringSelection(varstr)
521 if varstr != 'skip':
522 pwids.value.SetValue(par.value)
523 pwids.minval.SetValue(par.min)
524 pwids.maxval.SetValue(par.max)
525 pwids.onVaryChoice()
526 self.panel.Update()
528 def onEditParams(self, event=None):
529 pargroup = self.feffit_panel.get_paramgroup()
530 self.feffit_panel.show_subframe('edit_params', EditParamsFrame,
531 paramgroup=pargroup,
532 feffit_panel=self.feffit_panel)
534 def RemoveParams(self, event=None, name=None):
535 if name is None:
536 return
537 pargroup = self.feffit_panel.get_paramgroup()
539 if hasattr(pargroup, name):
540 delattr(pargroup, name)
541 if name in self.parwids:
542 pwids = self.parwids.pop(name)
543 pwids.name.Destroy()
544 pwids.value.Destroy()
545 pwids.vary.Destroy()
546 pwids.bounds.Destroy()
547 pwids.minval.Destroy()
548 pwids.maxval.Destroy()
549 pwids.expr.Destroy()
550 pwids.remover.Destroy()
552 def generate_params(self, event=None):
553 s = []
554 s.append(COMMANDS['feffit_params_init'])
555 for name, pwids in self.parwids.items():
556 param = pwids.param
557 args = [f'{param.value}']
558 minval = pwids.minval.GetValue()
559 if np.isfinite(minval):
560 args.append(f'min={minval}')
561 maxval = pwids.maxval.GetValue()
562 if np.isfinite(maxval):
563 args.append(f'max={maxval}')
565 varstr = pwids.vary.GetStringSelection()
566 if varstr == 'skip':
567 args.append('skip=True, vary=False')
568 elif param.expr is not None and varstr == 'constrain':
569 args.append(f"expr='{param.expr}'")
570 elif varstr == 'vary':
571 args.append(f'vary=True')
572 else:
573 args.append(f'vary=False')
574 args = ', '.join(args)
575 cmd = f'_feffit_params.{name} = param({args})'
576 s.append(cmd)
577 return s
580class FeffPathPanel(wx.Panel):
581 """Feff Path """
582 def __init__(self, parent, feffit_panel, filename, title, user_label,
583 geomstr, absorber, shell, reff, nleg, degen,
584 par_amp, par_e0, par_delr, par_sigma2, par_third, par_ei):
586 self.parent = parent
587 self.title = title
588 self.user_label = fix_varname(f'{title:s}')
589 self.feffit_panel = feffit_panel
590 self.editing_enabled = False
592 wx.Panel.__init__(self, parent, -1, size=(550, 250))
593 self.SetFont(Font(FONTSIZE))
594 panel = GridPanel(self, ncols=4, nrows=4, pad=2, itemstyle=LEFT)
596 self.fullpath = filename
597 pfile = Path(filename).absolute()
598 feffdat_file = pfile.name
599 dirname = pfile.parent.name
601 self.user_label = user_label
603 self.nleg = nleg
604 self.reff = reff
605 self.geomstr = geomstr
606 # self.geometry = geometry
608 def SLabel(label, size=(80, -1), **kws):
609 return SimpleText(panel, label, size=size, style=LEFT, **kws)
611 self.wids = wids = {}
612 for name, expr in (('label', user_label),
613 ('amp', par_amp),
614 ('e0', par_e0),
615 ('delr', par_delr),
616 ('sigma2', par_sigma2),
617 ('third', par_third),
618 ('ei', par_ei)):
619 self.wids[name] = wx.TextCtrl(panel, -1, size=(250, -1),
620 value=expr, style=wx.TE_PROCESS_ENTER)
621 wids[name+'_val'] = SimpleText(panel, '', size=(150, -1), style=LEFT)
623 wids['use'] = Check(panel, default=True, label='Use in Fit?', size=(100, -1))
624 wids['del'] = Button(panel, 'Remove This Path', size=(150, -1),
625 action=self.onRemovePath)
626 wids['plot_feffdat'] = Button(panel, 'Plot F(k)', size=(150, -1),
627 action=self.onPlotFeffDat)
629 scatt = {2: 'Single', 3: 'Double', 4: 'Triple',
630 5: 'Quadruple'}.get(nleg, f'{nleg-1:d}-atom')
631 scatt = scatt + ' Scattering'
634 title1 = f'{dirname:s}: {feffdat_file:s} {absorber:s} {shell:s} edge'
635 title2 = f'Reff={reff:.4f}, Degen={degen:.1f}, {scatt:s}: {geomstr:s}'
637 panel.Add(SLabel(title1, size=(375, -1), colour='#0000AA'),
638 dcol=2, style=wx.ALIGN_LEFT, newrow=True)
639 panel.Add(wids['use'])
640 panel.Add(wids['del'])
641 panel.Add(SLabel(title2, size=(425, -1)),
642 dcol=3, style=wx.ALIGN_LEFT, newrow=True)
643 panel.Add(wids['plot_feffdat'])
645 panel.AddMany((SLabel('Label'), wids['label'], wids['label_val']), newrow=True)
646 panel.AddMany((SLabel('Amplitude'), wids['amp'], wids['amp_val']), newrow=True)
647 panel.AddMany((SLabel('E0 '), wids['e0'], wids['e0_val']), newrow=True)
648 panel.AddMany((SLabel('Delta R'), wids['delr'], wids['delr_val']), newrow=True)
649 panel.AddMany((SLabel('sigma2'), wids['sigma2'], wids['sigma2_val']),newrow=True)
650 panel.AddMany((SLabel('third'), wids['third'], wids['third_val']), newrow=True)
651 panel.AddMany((SLabel('Eimag'), wids['ei'], wids['ei_val']), newrow=True)
652 panel.pack()
653 sizer= wx.BoxSizer(wx.VERTICAL)
654 sizer.Add(panel, 1, LEFT|wx.GROW|wx.ALL, 2)
655 pack(self, sizer)
658 def enable_editing(self):
659 for name in ('label', 'amp', 'e0', 'delr', 'sigma2', 'third', 'ei'):
660 self.wids[name].Bind(wx.EVT_TEXT_ENTER, partial(self.onExpression, name=name))
661 self.wids[name].Bind(wx.EVT_KILL_FOCUS, partial(self.onExpression, name=name))
662 self.editing_enabled = True
663 self.wids['label'].SetValue(self.user_label)
665 def set_userlabel(self, label):
666 self.wids['label'].SetValue(label)
668 def get_expressions(self):
669 out = {'use': self.wids['use'].IsChecked()}
670 for key in ('label', 'amp', 'e0', 'delr', 'sigma2', 'third', 'ei'):
671 val = self.wids[key].GetValue().strip()
672 if len(val) == 0: val = '0'
673 out[key] = val
674 return out
676 def onExpression(self, event=None, name=None):
677 if name is None:
678 return
679 expr = self.wids[name].GetValue()
680 if name == 'label':
681 time.sleep(0.001)
682 return
684 expr = self.wids[name].GetValue().strip()
685 if len(expr) < 1:
686 return
687 opts= dict(value=1.e-3, minval=None, maxval=None)
688 if name == 'sigma2':
689 opts['minval'] = 0
690 opts['maxval'] = 1
691 opts['value'] = np.sqrt(self.reff)/200.0
692 elif name == 'delr':
693 opts['minval'] = -0.75
694 opts['maxval'] = 0.75
695 elif name == 'amp':
696 opts['value'] = 1
697 result = self.feffit_panel.update_params_for_expr(expr, **opts)
698 if result:
699 pargroup = self.feffit_panel.get_paramgroup()
700 _eval = pargroup.__params__._asteval
701 try:
702 value = _eval.eval(expr, show_errors=False, raise_errors=False)
703 if value is not None:
704 value = gformat(value, 11)
705 self.wids[name + '_val'].SetLabel(f'= {value}')
706 except:
707 result = False
709 if result:
710 bgcol, fgcol = 'white', 'black'
711 else:
712 bgcol, fgcol = '#AAAA4488', '#AA0000'
713 self.wids[name].SetForegroundColour(fgcol)
714 self.wids[name].SetBackgroundColour(bgcol)
715 self.wids[name].SetOwnBackgroundColour(bgcol)
716 if event is not None:
717 event.Skip()
720 def onPlotFeffDat(self, event=None):
721 cmd = f"plot_feffdat(_feffpaths['{self.title}'], title='Feff data for path {self.title}')"
722 self.feffit_panel.larch_eval(cmd)
724 def onRemovePath(self, event=None):
725 msg = f"Delete Path {self.title:s}?"
726 dlg = wx.MessageDialog(self, msg, 'Warning', wx.YES | wx.NO )
727 if (wx.ID_YES == dlg.ShowModal()):
728 self.feffit_panel.paths_data.pop(self.title)
729 self.feffit_panel.model_needs_build = True
730 path_nb = self.feffit_panel.paths_nb
731 for i in range(path_nb.GetPageCount()):
732 if self.title == path_nb.GetPageText(i).strip():
733 path_nb.DeletePage(i)
734 self.feffit_panel.skip_unused_params()
735 dlg.Destroy()
737 def update_values(self):
738 pargroup = self.feffit_panel.get_paramgroup()
739 _eval = pargroup.__params__._asteval
740 for par in ('amp', 'e0', 'delr', 'sigma2', 'third', 'ei'):
741 expr = self.wids[par].GetValue().strip()
742 if len(expr) > 0:
743 try:
744 value = _eval.eval(expr, show_errors=False, raise_errors=False)
745 if value is not None:
746 value = gformat(value, 10)
747 self.wids[par + '_val'].SetLabel(f'= {value}')
748 except:
749 self.feffit_panel.update_params_for_expr(expr)
752class FeffitPanel(TaskPanel):
753 def __init__(self, parent=None, controller=None, **kws):
754 self.resetting = False
755 self.model_needs_rebuild = False
756 self.params_need_update = False
757 self.rmin_warned = False
758 TaskPanel.__init__(self, parent, controller, panel='feffit', **kws)
759 self.paths_data = {}
760 self.config_saved = self.get_defaultconfig()
761 self.dgroup = None
763 def onPanelExposed(self, **kws):
764 # called when notebook is selected
765 dgroup = self.controller.get_group()
766 try:
767 pargroup = self.get_paramgroup()
768 self.params_panel.update()
769 fname = self.controller.filelist.GetStringSelection()
770 gname = self.controller.file_groups[fname]
771 dgroup = self.controller.get_group(gname)
772 if not hasattr(dgroup, 'chi'):
773 self.parent.process_exafs(dgroup)
774 self.fill_form(dgroup)
775 except:
776 pass # print(" Cannot Fill feffit panel from group ")
777 if dgroup is not self.dgroup:
778 # setting up feffit for this group
779 self.dgroup = dgroup
780 try:
781 has_fit_hist = len(self.dgroup.feffit_history) > 0
782 except:
783 has_fit_hist = getattr(self.larch.symtable, '_feffit_dataset', None) is not None
785 if has_fit_hist:
786 self.wids['show_results'].Enable()
789 feffpaths = getattr(self.larch.symtable, '_feffpaths', None)
790 if feffpaths is not None:
791 self.reset_paths()
793 self.params_panel.update()
794 self.skip_unused_params()
795 self.params_need_update = False
797 def build_display(self):
798 self.paths_nb = flatnotebook(self, {}, on_change=self.onPathsNBChanged,
799 with_dropdown=True)
801 self.params_panel = FeffitParamsPanel(parent=self.paths_nb,
802 feffit_panel=self)
803 self.paths_nb.AddPage(self.params_panel, ' Parameters ', True)
804 pan = self.panel # = GridPanel(self, ncols=4, nrows=4, pad=2, itemstyle=LEFT)
806 self.wids = wids = {}
808 fsopts = dict(digits=2, increment=0.1, with_pin=True)
810 fit_kmin = self.add_floatspin('fit_kmin', value=2, **fsopts)
811 fit_kmax = self.add_floatspin('fit_kmax', value=17, **fsopts)
812 fit_dk = self.add_floatspin('fit_dk', value=4, **fsopts)
813 fit_rmax = self.add_floatspin('fit_rmax', value=5, **fsopts)
814 fit_rmin = self.add_floatspin('fit_rmin', value=1, action=self.onRmin, **fsopts)
816 wids['fit_kwstring'] = Choice(pan, size=(120, -1),
817 choices=list(Feffit_KWChoices.keys()))
818 wids['fit_kwstring'].SetSelection(1)
820 wids['fit_kwindow'] = Choice(pan, choices=list(FT_WINDOWS), size=(150, -1))
822 wids['fit_space'] = Choice(pan, choices=list(Feffit_SpaceChoices.keys()),
823 size=(150, -1))
825 wids['plot_kw'] = Choice(pan, size=(80, -1),
826 choices=['0', '1', '2', '3', '4'], default=2)
828 wids['plot1_op'] = Choice(pan, choices=list(Plot1_Choices.keys()),
829 action=self.onPlot, size=(150, -1))
830 wids['plot1_op'].SetSelection(1)
832 wids['plot1_voff'] = FloatSpin(pan, value=0, digits=2, increment=0.25,
833 size=(100, -1), action=self.onPlot)
835 wids['plot1_paths'] = Check(pan, default=False, label='Plot Each Path',
836 action=self.onPlot)
837 wids['plot1_ftwins'] = Check(pan, default=False, label='Plot FT Windows',
838 action=self.onPlot)
841 wids['plot2_win'] = Choice(pan, choices=PlotWindowChoices,
842 action=self.onPlot, size=(55, -1))
843 wids['plot2_win'].SetStringSelection('2')
844 wids['plot2_win'].SetToolTip('Plot Window for Second Plot')
846 wids['plot2_op'] = Choice(pan, choices=list(Plot2_Choices.keys()),
847 action=self.onPlot, size=(150, -1))
850 wids['plot2_voff'] = FloatSpin(pan, value=0, digits=2, increment=0.25,
851 size=(100, -1), action=self.onPlot)
853 wids['plot2_paths'] = Check(pan, default=False, label='Plot Each Path',
854 action=self.onPlot)
855 wids['plot2_ftwins'] = Check(pan, default=False, label='Plot FT Windows',
856 action=self.onPlot)
857 wids['plot_current'] = Button(pan,'Plot Current Model',
858 action=self.onPlot, size=(175, -1))
860 wids['refine_bkg'] = Check(pan, default=False,
861 label='Refine Background during Fit?')
862 wids['do_fit'] = Button(pan, 'Fit Data to Model',
863 action=self.onFitModel, size=(175, -1))
864 wids['show_results'] = Button(pan, 'Show Fit Results',
865 action=self.onShowResults, size=(175, -1))
866 wids['show_results'].Disable()
868 def add_text(text, dcol=1, newrow=True):
869 pan.Add(SimpleText(pan, text), dcol=dcol, newrow=newrow)
871 pan.Add(SimpleText(pan, 'Feff Fitting',
872 size=(150, -1), **self.titleopts), style=LEFT, dcol=1)
873 pan.Add(SimpleText(pan, 'Use Feff->Browse Feff Calculations to Add Paths',
874 size=(425, -1)), style=LEFT, dcol=4)
876 add_text('Fitting Space: ')
877 pan.Add(wids['fit_space'])
879 add_text('k weightings: ', newrow=False)
880 pan.Add(wids['fit_kwstring'], dcol=3)
882 add_text('k min: ')
883 pan.Add(fit_kmin)
884 add_text(' k max: ', newrow=False)
885 pan.Add(fit_kmax)
887 add_text('k Window: ')
888 pan.Add(wids['fit_kwindow'])
889 add_text('dk: ', newrow=False)
890 pan.Add(fit_dk)
892 add_text('R min: ')
893 pan.Add(fit_rmin)
894 add_text('R max: ', newrow=False)
895 pan.Add(fit_rmax)
896 add_text(' ', newrow=True)
897 pan.Add(wids['refine_bkg'], dcol=3)
899 pan.Add(HLine(pan, size=(600, 2)), dcol=6, newrow=True)
901 pan.Add(wids['plot_current'], dcol=1, newrow=True)
902 pan.Add(wids['plot1_op'], dcol=1)
903 add_text('k-weight:' , newrow=False)
904 pan.Add(wids['plot_kw'], dcol=1)
906 pan.Add(wids['plot1_ftwins'], newrow=True)
907 pan.Add(wids['plot1_paths'])
908 add_text('Vertical Offset' , newrow=False)
909 pan.Add(wids['plot1_voff'])
912 add_text('Add Second Plot: ')
914 pan.Add(wids['plot2_op'], dcol=1)
915 add_text('Plot Window:' , newrow=False)
916 pan.Add(wids['plot2_win'])
918 pan.Add(wids['plot2_ftwins'], newrow=True)
919 pan.Add(wids['plot2_paths'])
920 add_text('Vertical Offset' , newrow=False)
921 pan.Add(wids['plot2_voff'])
924 pan.Add(wids['do_fit'], dcol=1, newrow=True)
925 pan.Add(wids['show_results'])
926 pan.Add((5, 5), newrow=True)
928 pan.Add(HLine(pan, size=(600, 2)), dcol=6, newrow=True)
929 pan.pack()
931 sizer = wx.BoxSizer(wx.VERTICAL)
932 sizer.Add(pan, 0, LEFT, 3)
933 sizer.Add((3, 3), 0, LEFT, 3)
934 sizer.Add(SimpleText(self, ' Parameters and Paths'), 0, LEFT, 2)
935 sizer.Add((3, 3), 0, LEFT, 3)
936 sizer.Add(self.paths_nb, 1, LEFT|wx.GROW, 5)
937 pack(self, sizer)
939 def onRmin(self, event=None, **kws):
940 dgroup = self.controller.get_group()
941 if dgroup is None:
942 return
943 rbkg = getattr(dgroup, 'rbkg', None)
944 callargs = getattr(dgroup, 'callargs', None)
945 if rbkg is None and callargs is not None:
946 autobk_args = getattr(callargs, 'autobk', {'rbkg': 1.0})
947 rbkg = autobk_args.get('rbkg', None)
948 if rbkg is None: rbkg = 0.0
949 rmin = self.wids['fit_rmin'].GetValue()
950 if rmin > (rbkg-0.025):
951 self.rmin_warned = False
953 if rmin < (rbkg-0.025) and not self.rmin_warned:
954 self.rmin_warned = True
955 Popup(self,
956 f"""Rmin={rmin:.3f} Ang is below Rbkg={rbkg:.3f} Ang.
958 This should be done with caution.""",
959 "Warning: Rmin < Rbkg",
960 style=wx.ICON_WARNING|wx.OK_DEFAULT)
963 def onPathsNBChanged(self, event=None):
964 updater = getattr(self.paths_nb.GetCurrentPage(), 'update_values', None)
965 if self.params_need_update and not self.resetting:
966 self.params_panel.update()
967 self.skip_unused_params()
968 self.params_need_update = False
969 if callable(updater) and not self.resetting:
970 updater()
972 def get_config(self, dgroup=None):
973 """get and set processing configuration for a group"""
974 if dgroup is None:
975 dgroup = self.controller.get_group()
976 if dgroup is None:
977 conf = None
978 if not hasattr(dgroup, 'chi'):
979 self.parent.process_exafs(dgroup)
981 dconf = self.get_defaultconfig()
983 if dgroup is None:
984 return dconf
985 if not hasattr(dgroup, 'config'):
986 dgroup.config = Group()
988 conf = getattr(dgroup.config, self.configname, dconf)
989 for k, v in dconf.items():
990 if k not in conf:
991 conf[k] = v
993 econf = getattr(dgroup.config, 'exafs', {})
994 for key in ('fit_kmin', 'fit_kmax', 'fit_dk',
995 'fit_rmin', 'fit_rmax', 'fit_dr',
996 'fit_kwindow', 'fit_rwindow'):
997 val = conf.get(key, -1)
998 if val in (None, -1, 'Auto'):
999 alt = key.replace('fit', 'fft')
1000 if alt in econf:
1001 conf[key] = econf[alt]
1002 setattr(dgroup.config, self.configname, conf)
1003 self.config_saved = conf
1004 return conf
1006 def process(self, dgroup=None, **kws):
1007 # print("Feffit Panel Process ", dgroup, time.ctime())
1008 if dgroup is None:
1009 dgroup = self.controller.get_group()
1011 conf = self.get_config(dgroup=dgroup)
1012 conf.update(kws)
1014 if self.params_need_update:
1015 self.params_panel.update()
1016 self.skip_unused_params()
1017 self.params_need_update = False
1019 opts = self.read_form(dgroup=dgroup)
1020 if dgroup is not None:
1021 self.dgroup = dgroup
1022 for attr in ('fit_kmin', 'fit_kmax', 'fit_dk', 'fit_rmin',
1023 'fit_rmax', 'fit_kwindow', 'fit_rwindow',
1024 'fit_dr', 'fit_kwstring', 'fit_space',
1025 'fit_plot', 'plot1_paths'):
1027 conf[attr] = opts.get(attr, None)
1029 if not hasattr(dgroup, 'config'):
1030 dgroup.config = Group()
1031 setattr(dgroup.config, self.configname, conf)
1033 orig_dgroup = self.controller.get_group()
1034 if orig_dgroup is not None:
1035 setattr(orig_dgroup.config, self.configname, conf)
1036 # print("dgroup " , conf)
1038 try:
1039 tchi = getattr(dgroup, 'chi', np.zeros(10))
1040 has_data = 1.e-12 < (tchi**2).sum()
1041 except:
1042 has_data = False
1043 cmds = [COMMANDS['feffit_trans'].format(**conf)]
1044 _feffit_dataset = getattr(self.larch.symtable, '_feffit_dataset', None)
1045 if _feffit_dataset is None:
1046 cmds.append(COMMANDS['feffit_dataset_init'])
1047 if has_data:
1048 cmds.append(f"_feffit_dataset.set_datagroup({dgroup.groupname})")
1049 cmds.append(f"_feffit_dataset.refine_bkg = {opts['refine_bkg']}")
1050 self.larch.eval('\n'.join(cmds))
1051 return opts
1053 def fill_form(self, dat):
1054 dgroup = self.controller.get_group()
1055 conf = self.get_config(dat)
1057 for attr in ('fit_kmin', 'fit_kmax', 'fit_rmin', 'fit_rmax', 'fit_dk'):
1058 self.wids[attr].SetValue(conf[attr])
1060 self.wids['fit_kwindow'].SetStringSelection(conf['fit_kwindow'])
1062 fit_space = conf.get('fit_space', 'r')
1064 for key, val in Feffit_SpaceChoices.items():
1065 if fit_space in (key, val):
1066 self.wids['fit_space'].SetStringSelection(key)
1068 for key, val in Feffit_KWChoices.items():
1069 if conf['fit_kwstring'] == val:
1070 self.wids['fit_kwstring'].SetStringSelection(key)
1072 def read_form(self, dgroup=None):
1073 "read form, returning dict of values"
1075 if dgroup is None:
1076 try:
1077 fname = self.controller.filelist.GetStringSelection()
1078 gname = self.controller.file_groups[fname]
1079 dgroup = self.controller.get_group()
1080 except:
1081 gname = fname = dgroup = None
1082 else:
1084 gname = dgroup.groupname
1085 fname = dgroup.filename
1087 form_opts = {'datagroup': dgroup, 'groupname': gname, 'filename': fname}
1088 wids = self.wids
1090 for attr in ('fit_kmin', 'fit_kmax', 'fit_rmin', 'fit_rmax', 'fit_dk'):
1091 form_opts[attr] = wids[attr].GetValue()
1092 form_opts['fit_kwstring'] = Feffit_KWChoices[wids['fit_kwstring'].GetStringSelection()]
1093 if len(form_opts['fit_kwstring']) == 1:
1094 d = form_opts['fit_kwstring']
1095 else:
1096 d = form_opts['fit_kwstring'].replace('[', '').strip(',').split()[0]
1097 try:
1098 form_opts['fit_kweight'] = int(d)
1099 except:
1100 form_opts['fit_kweight'] = 2
1103 form_opts['refine_bkg'] = wids['refine_bkg'].IsChecked()
1104 fitspace_string = wids['fit_space'].GetStringSelection()
1105 form_opts['fit_space'] = Feffit_SpaceChoices[fitspace_string]
1107 form_opts['fit_kwindow'] = wids['fit_kwindow'].GetStringSelection()
1108 form_opts['plot_kw'] = int(wids['plot_kw'].GetStringSelection())
1109 form_opts['plot1_ftwins'] = wids['plot1_ftwins'].IsChecked()
1110 form_opts['plot1_paths'] = wids['plot1_paths'].IsChecked()
1111 form_opts['plot1_op'] = Plot1_Choices[wids['plot1_op'].GetStringSelection()]
1112 form_opts['plot1_voff'] = wids['plot1_voff'].GetValue()
1114 form_opts['plot2_op'] = Plot2_Choices[wids['plot2_op'].GetStringSelection()]
1115 form_opts['plot2_ftwins'] = wids['plot2_ftwins'].IsChecked()
1116 form_opts['plot2_paths'] = wids['plot2_paths'].IsChecked()
1117 form_opts['plot2_voff'] = wids['plot2_voff'].GetValue()
1118 form_opts['plot2_win'] = int(wids['plot2_win'].GetStringSelection())
1119 return form_opts
1122 def fill_model_params(self, prefix, params):
1123 comp = self.fit_components[prefix]
1124 parwids = comp.parwids
1125 for pname, par in params.items():
1126 pname = prefix + pname
1127 if pname in parwids:
1128 wids = parwids[pname]
1129 if wids.minval is not None:
1130 wids.minval.SetValue(par.min)
1131 if wids.maxval is not None:
1132 wids.maxval.SetValue(par.max)
1133 wids.value.SetValue(par.value)
1134 varstr = 'vary' if par.vary else 'fix'
1135 if par.expr is not None:
1136 varstr = 'constrain'
1137 if wids.vary is not None:
1138 wids.vary.SetStringSelection(varstr)
1140 def onPlot(self, evt=None, dataset_name='_feffit_dataset',
1141 pargroup_name='_feffit_params', title=None, build_fitmodel=True,
1142 topwin=None, **kws):
1144 dataset = getattr(self.larch.symtable, dataset_name, None)
1145 if dataset is None:
1146 dgroup = self.controller.get_group()
1147 else:
1148 if dataset.has_data:
1149 dgroup = dataset.data
1150 else:
1151 dgroup = self.controller.get_group()
1152 # print("no data? dgroup ", dataset.data, dgroup)
1153 if dgroup is not None:
1154 self.larch.eval(f"{dataset_name}.set_datagroup({dgroup.groupname})")
1155 dataset = getattr(self.larch.symtable, dataset_name, None)
1156 dgroup = dataset.data
1157 # print("now data now dgroup ", dataset, getattr(dataset, 'data', None), dgroup)
1159 opts = self.process(dgroup)
1160 opts.update(**kws)
1162 if build_fitmodel:
1163 self.build_fitmodel(dgroup, opts=opts)
1165 model_name = dataset_name + '.model'
1166 paths_name = dataset_name + '.paths'
1167 paths = self.larch.eval(paths_name)
1169 data_name = dataset_name + '.data'
1170 refine_bkg = getattr(dataset, 'refine_bkg',
1171 opts.get('refine_bkg', False))
1173 if refine_bkg and hasattr(dataset, 'data_rebkg'):
1174 data_name = dataset_name + '.data_rebkg'
1176 exafs_conf = self.parent.get_nbpage('exafs')[1].read_form()
1177 opts['plot_rmax'] = exafs_conf['plot_rmax']
1178 groupname = getattr(dgroup, 'groupname', 'unknown')
1179 cmds = ["#### plot ",
1180 f"# build arrays for plotting: refine bkg? {refine_bkg}, {groupname} / {dataset_name}"]
1182 kweight = opts['plot_kw']
1184 ftargs = dict(kmin=opts['fit_kmin'], kmax=opts['fit_kmax'], dk=opts['fit_dk'],
1185 kwindow=opts['fit_kwindow'], kweight=opts['plot_kw'],
1186 rmin=opts['fit_rmin'], rmax=opts['fit_rmax'],
1187 dr=opts.get('fit_dr', 0.1), rwindow='hanning')
1189 if model_name is not None:
1190 cmds.append(COMMANDS['xft'].format(groupname=model_name, **ftargs))
1191 if data_name is not None:
1192 cmds.append(COMMANDS['xft'].format(groupname=data_name, **ftargs))
1194 if opts['plot1_paths'] or opts['plot2_paths']:
1195 cmds.append(COMMANDS['path2chi'].format(paths_name=paths_name,
1196 pargroup_name=pargroup_name,
1197 **ftargs))
1199 self.larch_eval('\n'.join(cmds))
1200 self.plot_feffit_result(dataset_name, topwin=topwin, ftargs=ftargs, **opts)
1202 def plot_feffit_result(self, dataset_name, topwin=None, ftargs=None, **kws):
1204 if isValidName(dataset_name):
1205 dataset = getattr(self.larch.symtable, dataset_name, None)
1206 else:
1207 dataset = self.larch.eval(dataset_name)
1209 if dataset is None:
1210 dgroup = self.controller.get_group()
1211 else:
1212 dgroup = dataset.data
1213 data_name = dataset_name + '.data'
1214 model_name = dataset_name + '.model'
1215 has_data = getattr(dataset, 'has_data', True)
1217 #print("plot_feffit_result/ dgroup, dataset: ", dataset_name, dgroup, dataset, has_data)
1219 opts = self.process(dgroup)
1220 opts.update(**kws)
1221 title = fname = opts['filename']
1222 if title is None:
1223 title = 'Feff Sum'
1224 if "'" in title:
1225 title = title.replace("'", "\\'")
1227 needs_qspace = False
1228 plot1 = opts['plot1_op']
1229 plot2 = opts['plot2_op']
1230 plot_rmax = opts['plot_rmax']
1231 kweight = opts['plot_kw']
1232 if ftargs is None:
1233 ftargs = dict(kmin=opts['fit_kmin'], kmax=opts['fit_kmax'], dk=opts['fit_dk'],
1234 kwindow=opts['fit_kwindow'], kweight=opts['plot_kw'],
1235 rmin=opts['fit_rmin'], rmax=opts['fit_rmax'],
1236 dr=opts.get('fit_dr', 0.1), rwindow='hanning')
1238 cmds = []
1239 for i, plot in enumerate((plot1, plot2)):
1240 if plot in Plot2_Choices:
1241 plot = Plot2_Choices[plot]
1242 plotwin = 1
1243 if i == 1:
1244 if plot in ('noplot', '<no plot>'):
1245 continue
1246 else:
1247 plotwin = int(opts.get('plot2_win', '2'))
1248 pcmd = 'plot_chir'
1249 pextra = f', win={plotwin:d}'
1250 if plot == 'chi':
1251 pcmd = 'plot_chik'
1252 pextra += f', kweight={kweight:d}'
1253 elif plot == 'chir_mag':
1254 pcmd = 'plot_chir'
1255 pextra += f', rmax={plot_rmax}'
1256 elif plot == 'chir_re':
1257 pextra += f', show_mag=False, show_real=True, rmax={plot_rmax}'
1258 elif plot == 'chir_mag+chir_re':
1259 pextra += f', show_mag=True, show_real=True, rmax={plot_rmax}'
1260 elif plot == 'chiq':
1261 pcmd = 'plot_chiq'
1262 pextra += f', show_chik=False'
1263 needs_qspace = True
1264 else:
1265 # print(" do not know how to plot ", plot)
1266 continue
1267 with_win = opts[f'plot{i+1}_ftwins']
1268 newplot = f', show_window={with_win}, new=True'
1269 overplot = f', show_window=False, new=False'
1270 extra = newplot
1271 if dgroup is not None:
1272 if has_data:
1273 cmds.append(f"{pcmd}({data_name:s}, label='data'{pextra}, title='{title}'{extra})")
1274 extra = overplot
1275 if dataset.model is not None:
1276 cmds.append(f"{pcmd}({model_name:s}, label='model'{pextra}{extra})")
1277 extra = overplot
1278 elif dataset.model is not None:
1279 cmds.append(f"{pcmd}({model_name:s}, label='Path sum'{pextra}, title='sum of paths'{extra})")
1280 extra = overplot
1281 if opts[f'plot{i+1}_paths']:
1282 paths_name = dataset_name + '.paths'
1283 try:
1284 paths = self._plain_larch_eval(paths_name)
1285 except:
1286 paths = {}
1287 voff = opts[f'plot{i+1}_voff']
1288 for i, label in enumerate(paths.keys()):
1289 if paths[label].use:
1290 objname = f"{paths_name}['{label:s}']"
1291 if needs_qspace:
1292 xpath = paths.get(label)
1293 if not hasattr(xpath, 'chiq_re'):
1294 cmds.append(COMMANDS['xft'].format(groupname=objname, **ftargs))
1296 cmds.append(f"{pcmd}({objname}, label='{label:s}'{pextra}, offset={(i+1)*voff}{overplot})")
1298 self.larch_eval('\n'.join(cmds))
1299 if topwin is not None:
1300 self.controller.set_focus(topwin=topwin)
1303 def reset_paths(self, event=None):
1304 "reset paths from _feffpaths"
1305 self.resetting = True
1306 def get_pagenames():
1307 allpages = []
1308 for i in range(self.paths_nb.GetPageCount()):
1309 allpages.append(self.paths_nb.GetPage(i).__class__.__name__)
1310 return allpages
1312 allpages = get_pagenames()
1313 t0 = time.time()
1315 while 'FeffPathPanel' in allpages:
1316 for i in range(self.paths_nb.GetPageCount()):
1317 nbpage = self.paths_nb.GetPage(i)
1318 if isinstance(nbpage, FeffPathPanel):
1319 key = self.paths_nb.GetPageText(i)
1320 self.paths_nb.DeletePage(i)
1321 allpages = get_pagenames()
1323 time.sleep(0.02)
1324 feffpaths = deepcopy(getattr(self.larch.symtable, '_feffpaths', {}))
1325 self.paths_data = {}
1326 for path in feffpaths.values():
1327 self.add_path(path.filename, feffpath=path, resize=False)
1329 self.get_pathpage('parameters').Rebuild()
1330 self.resetting = False
1331 self.params_need_update = True
1334 def add_path(self, filename, pathinfo=None, feffpath=None, resize=True):
1335 """ add new path to cache """
1337 if pathinfo is None and feffpath is None:
1338 raise ValueError("add_path needs a Feff Path or Path information")
1339 self.params_need_update = True
1340 pfile = Path(filename).absolute()
1341 fname = pfile.name
1342 feffrun = pfile.parent.name
1344 feffcache = getattr(self.larch.symtable, '_feffcache', None)
1345 if feffcache is None:
1346 self.larch_eval(COMMANDS['paths_init'])
1347 feffcache = getattr(self.larch.symtable, '_feffcache', None)
1348 if feffcache is None:
1349 raise ValueError("cannot get feff cache ")
1351 geomstre = None
1352 if pathinfo is not None:
1353 absorber = pathinfo.absorber
1354 shell = pathinfo.shell
1355 reff = float(pathinfo.reff)
1356 nleg = int(pathinfo.nleg)
1357 degen = float(pathinfo.degen)
1358 if hasattr(pathinfo, 'atoms'):
1359 geom = pathinfo.atoms
1360 geomstr = pathinfo.geom # '[Fe] > O > [Fe]'
1361 par_amp = par_e0 = par_delr = par_sigma2 = par_third = par_ei = ''
1363 if feffpath is not None:
1364 absorber = feffpath.absorber
1365 shell = feffpath.shell
1366 reff = feffpath.reff
1367 nleg = feffpath.nleg
1368 degen = float(feffpath.degen)
1369 geomstr = []
1370 for gdat in feffpath.geom: # ('Fe', 26, 0, 55.845, x, y, z)
1371 w = gdat[0]
1372 if gdat[2] == 0: # absorber
1373 w = '[%s]' % w
1374 geomstr.append(w)
1375 geomstr.append(geomstr[0])
1376 geomstr = ' > '.join(geomstr)
1377 par_amp = feffpath.s02
1378 par_e0 = feffpath.e0
1379 par_delr = feffpath.deltar
1380 par_sigma2 = feffpath.sigma2
1381 par_third = feffpath.third
1382 par_ei = feffpath.ei
1384 try:
1385 atoms = [s.strip() for s in geomstr.split('>')]
1386 atoms.pop()
1387 except:
1388 title = "Cannot interpret Feff Path data"
1389 message = [f"Cannot interpret Feff path {filename}"]
1390 ExceptionPopup(self, title, message)
1392 title = '_'.join(atoms) + "%d" % (round(100*reff))
1393 for c in ',.[](){}<>+=-?/\\&%$#@!|:;"\'':
1394 title = title.replace(c, '')
1395 if title in self.paths_data:
1396 btitle = title
1397 i = -1
1398 while title in self.paths_data:
1399 i += 1
1400 title = btitle + '_%s' % string.ascii_lowercase[i]
1402 user_label = fix_varname(title)
1403 self.paths_data[title] = filename
1405 ptitle = title
1406 if ptitle.startswith(absorber):
1407 ptitle = ptitle[len(absorber):]
1408 if ptitle.startswith('_'):
1409 ptitle = ptitle[1:]
1411 # set default Path parameters if not supplied already
1412 if len(par_amp) < 1:
1413 par_amp = f'{degen:.1f} * s02'
1414 if len(par_e0) < 1:
1415 par_e0 = 'e0'
1416 if len(par_delr) < 1:
1417 par_delr = f'delr_{ptitle}'
1418 if len(par_sigma2) < 1:
1419 par_sigma2 = f'sigma2_{ptitle}'
1421 pathpanel = FeffPathPanel(self.paths_nb, self, filename, title,
1422 user_label, geomstr, absorber, shell,
1423 reff, nleg, degen, par_amp, par_e0,
1424 par_delr, par_sigma2, par_third, par_ei)
1426 self.paths_nb.AddPage(pathpanel, f' {title:s} ', True)
1428 for pname in ('amp', 'e0', 'delr', 'sigma2', 'third', 'ei'):
1429 pathpanel.onExpression(name=pname)
1431 pathpanel.enable_editing()
1433 pdat = {'title': title, 'fullpath': filename,
1434 'feffrun': feffrun, 'use':True}
1435 pdat.update(pathpanel.get_expressions())
1437 if title not in feffcache['paths']:
1438 if Path(filename).exists():
1439 self.larch_eval(COMMANDS['cache_path'].format(**pdat))
1440 else:
1441 print(f"cannot file Feff data file '{filename}'")
1443 self.larch_eval(COMMANDS['use_path'].format(**pdat))
1444 if resize:
1445 sx,sy = self.GetSize()
1446 self.SetSize((sx, sy+1))
1447 self.SetSize((sx, sy))
1448 ipage, pagepanel = self.parent.get_nbpage('feffit')
1449 self.parent.nb.SetSelection(ipage)
1450 self.parent.Raise()
1452 def get_pathkeys(self):
1453 _feffpaths = getattr(self.larch.symtable, '_feffpaths', {})
1454 return [p.hashkey for p in _feffpaths.values()]
1456 def get_paramgroup(self):
1457 pgroup = getattr(self.larch.symtable, '_feffit_params', None)
1458 if pgroup is None:
1459 self.larch_eval(COMMANDS['feffit_params_init'])
1460 pgroup = getattr(self.larch.symtable, '_feffit_params', None)
1461 if not hasattr(self.larch.symtable, '_feffpaths'):
1462 self.larch_eval(COMMANDS['paths_init'])
1463 return pgroup
1465 def update_params_for_expr(self, expr=None, value=1.e-3,
1466 minval=None, maxval=None):
1467 if expr is None:
1468 return
1469 pargroup = self.get_paramgroup()
1470 symtable = pargroup.__params__._asteval.symtable
1471 extras= ''
1472 if minval is not None:
1473 extras = f', min={minval}'
1474 if maxval is not None:
1475 extras = f'{extras}, max={maxval}'
1477 try:
1478 for node in ast.walk(ast.parse(expr)):
1479 if isinstance(node, ast.Name):
1480 sym = node.id
1481 if sym not in symtable and sym not in FEFFDAT_VALUES:
1482 s = f"_feffit_params.{sym:s} = param({value:.4f}, name='{sym:s}', vary=True{extras:s})"
1483 self.larch_eval(s)
1484 result = True
1485 except:
1486 result = False
1488 self.params_need_update = True
1489 return result
1491 def onLoadFitResult(self, event=None):
1492 rfile = FileOpen(self, "Load Saved Feffit Model",
1493 wildcard=ModelWcards)
1494 if rfile is None:
1495 return
1497 def get_xranges(self, x):
1498 if self.dgroup is None:
1499 self.dgroup = self.controller.get_group()
1500 self.process(self.dgroup)
1501 opts = self.read_form(self.dgroup)
1502 dgroup = self.controller.get_group()
1503 en_eps = min(np.diff(dgroup.energy)) / 5.
1505 i1 = index_of(x, opts['emin'] + en_eps)
1506 i2 = index_of(x, opts['emax'] + en_eps) + 1
1507 return i1, i2
1509 def get_pathpage(self, name):
1510 "get nb page for a Path by name"
1511 name = name.lower().strip()
1512 for i in range(self.paths_nb.GetPageCount()):
1513 text = self.paths_nb.GetPageText(i).strip().lower()
1514 if name in text:
1515 return self.paths_nb.GetPage(i)
1517 def build_fitmodel(self, groupname=None, opts=None):
1518 """ use fit components to build model"""
1519 paths = []
1520 cmds = ["### set up feffit "]
1521 pargroup = self.get_paramgroup()
1522 if self.dgroup is None:
1523 self.dgroup = self.controller.get_group()
1525 # self.params_panel.update()
1526 cmds.extend(self.params_panel.generate_params())
1528 if opts is None:
1529 opts = self.process(self.dgroup)
1531 cmds.append(COMMANDS['feffit_trans'].format(**opts))
1533 path_pages = {}
1534 for i in range(self.paths_nb.GetPageCount()):
1535 text = self.paths_nb.GetPageText(i).strip()
1536 path_pages[text] = self.paths_nb.GetPage(i)
1538 _feffpaths = getattr(self.larch.symtable, '_feffpaths', None)
1539 if _feffpaths is None:
1540 cmds.append(COMMANDS['paths_init'])
1541 else:
1542 cmds.append(COMMANDS['paths_reset'])
1544 _feffit_dataset = getattr(self.larch.symtable, '_feffit_dataset', None)
1545 if _feffit_dataset is None:
1546 cmds.append(COMMANDS['feffit_dataset_init'])
1548 paths_list = []
1549 opts['paths'] = []
1550 for title, pathdata in self.paths_data.items():
1551 if title not in path_pages:
1552 continue
1553 pdat = {'title': title, 'fullpath': pathdata[0],
1554 'feffrun': pathdata[1], 'use':True}
1555 pdat.update(path_pages[title].get_expressions())
1557 #if pdat['use']:
1558 cmds.append(COMMANDS['use_path'].format(**pdat))
1559 paths_list.append(f"_feffpaths['{title:s}']")
1560 opts['paths'].append(pdat)
1562 paths_string = '[%s]' % (', '.join(paths_list))
1564 gname = opts.get('groupname', None)
1565 if gname is None:
1566 cmds.append(COMMANDS['ff2chi_nodata'].format(paths=paths_string,
1567 trans='_feffit_trans')
1568 )
1569 else:
1570 cmds.append(COMMANDS['ff2chi'].format(paths=paths_string,
1571 trans='_feffit_trans',
1572 groupname=gname,
1573 refine_bkg=opts['refine_bkg'])
1574 )
1575 cmds.append('# end of build model')
1576 self.larch_eval("\n".join(cmds))
1577 return opts
1580 def get_used_params(self):
1581 used_syms = []
1582 path_pages = {}
1584 for i in range(self.paths_nb.GetPageCount()):
1585 text = self.paths_nb.GetPageText(i).strip()
1586 path_pages[text] = self.paths_nb.GetPage(i)
1587 for title in self.paths_data:
1588 if title not in path_pages:
1589 continue
1590 exprs = path_pages[title].get_expressions()
1591 if exprs['use']:
1592 for ename, expr in exprs.items():
1593 if ename in ('label', 'use'):
1594 continue
1595 for node in ast.walk(ast.parse(expr)):
1596 if isinstance(node, ast.Name):
1597 sym = node.id
1598 if sym not in used_syms:
1599 used_syms.append(sym)
1600 return used_syms
1603 def skip_unused_params(self):
1604 # find unused symbols, set to "skip"
1605 curr_syms = self.get_used_params()
1606 pargroup = self.get_paramgroup()
1607 parpanel = self.params_panel
1608 for pname, par in group2params(pargroup).items():
1609 if pname in parpanel.parwids:
1610 ppar = parpanel.parwids[pname]
1611 _skip = False
1612 _str = ppar.vary.GetStringSelection()
1613 if pname not in curr_syms:
1614 _skip = True
1615 _str = 'skip'
1616 elif (pname in curr_syms and getattr(ppar, 'skip', False)):
1617 _str = 'vary'
1618 ppar.skip = ppar.param.skip = par.skip = _skip
1619 ppar.vary.SetStringSelection(_str)
1620 ppar.onVaryChoice()
1621 parpanel.update()
1623 def onFitModel(self, event=None, dgroup=None):
1624 session_history = self.get_session_history()
1625 nstart = len(session_history)
1627 script = [COMMANDS['feffit_top'].format(ctime=time.ctime())]
1629 if dgroup is None:
1630 dgroup = self.controller.get_group()
1631 opts = self.build_fitmodel(dgroup)
1633 # dgroup = opts['datagroup']
1634 fopts = dict(groupname=opts['groupname'],
1635 refine_bkg=bool(opts['refine_bkg']),
1636 trans='_feffit_trans',
1637 paths='_feffpaths',
1638 params='_feffit_params')
1640 groupname = opts['groupname']
1641 filename = opts['filename']
1642 if dgroup is None:
1643 dgroup = opts['datagroup']
1645 script.append("###\n### DATA \n###\n")
1646 script.append(COMMANDS['data_source'].format(groupname=groupname, filename=filename))
1647 seen_cmds = []
1648 cmdhist = []
1649 sesshist = get_commands(session_history)
1650 for cmd in reversed(sesshist):
1651 if groupname in cmd or filename in cmd or 'athena' in cmd or 'session' in cmd:
1652 scmd = cmd.strip()
1653 if (scmd.startswith('#') or scmd.startswith('plot_')
1654 or 'feffit_dataset(' in scmd or 'feffit_transform(' in scmd):
1655 continue
1656 fcn_name, args = scmd.split('(', 1) if '(' in scmd else ('', scmd)
1657 if fcn_name in ('autobk', 'pre_edge', 'xftf', 'xftr'):
1658 if fcn_name in seen_cmds:
1659 continue
1660 else:
1661 seen_cmds.append(fcn_name)
1662 cmdhist.append(f"# {cmd}")
1664 script.extend(list(reversed(cmdhist)))
1666 script.append("### end of data reading and preparation\n###\n###")
1667 script.append("## read Feff Paths into '_feffpaths'. You will need to either")
1668 script.append("## read feff.dat from disk files with `feffpath()` or use Paths")
1669 script.append("## cached from a session file into `feffcache`")
1670 script.append("#_feffcache = {'runs': {}, 'paths':{}}")
1671 script.append("#_feffpaths = {}")
1672 for path in opts['paths']:
1673 lab, fname, run = path['title'], path['fullpath'], path['feffrun']
1674 amp, e0, delr, sigma2, third, ei = path['amp'], path['e0'], path['delr'], path['sigma2'], path['third'], path['ei']
1675 script.append(f"""## Path '{lab}' : ############
1676#_feffcache['paths']['{lab}'] = feffpath('{fname}', label='{lab}', feffrun='{run}', degen=1)
1677#_feffpaths['{lab}'] = use_feffpath(_feffcache['paths'], '{lab}',
1678# s02='{amp:s}', e0='{e0:s}', deltar='{delr:s}',
1679# sigma2='{sigma2:s}', third='{third:s}', ei='{ei:s}')""")
1681 script.append("###\n###\n###")
1682 self.larch_eval(COMMANDS['do_feffit'].format(**fopts))
1684 self.wids['show_results'].Enable()
1685 self.onPlot(dataset_name='_feffit_dataset',
1686 pargroup_name='_feffit_result.paramgroup',
1687 build_fitmodel=False)
1690 script.extend(self.get_session_history()[nstart:])
1691 script.extend(["print(feffit_report(_feffit_result))",
1692 "#end of autosaved feffit script" , ""])
1694 if not hasattr(dgroup, 'feffit_history'):
1695 dgroup.feffit_history = []
1698 label = now = time.strftime("%b-%d %H:%M")
1699 if len(dgroup.feffit_history) > 0:
1700 dgroup.feffit_history[0].commands = script
1701 dgroup.feffit_history[0].timestamp = time.strftime("%Y-%b-%d %H:%M")
1702 dgroup.feffit_history[0].label = label
1704 fitlabels = [fhist.label for fhist in dgroup.feffit_history[1:]]
1705 if label in fitlabels:
1706 count = 1
1707 while label in fitlabels:
1708 label = f'{now:s}_{printable[count]:s}'
1709 count +=1
1710 dgroup.feffit_history[0].label = label
1712 sname = self.autosave_script('\n'.join(script))
1713 self.write_message("wrote feffit script to '%s'" % sname)
1715 self.show_subframe('feffit_result', FeffitResultFrame,
1716 datagroup=opts['datagroup'], feffit_panel=self)
1717 self.subframes['feffit_result'].add_results(dgroup, form=opts)
1719 def onShowResults(self, event=None):
1720 self.show_subframe('feffit_result', FeffitResultFrame,
1721 feffit_panel=self)
1723 def update_start_values(self, params):
1724 """fill parameters with best fit values"""
1725 self.params_panel.set_init_values(params)
1726 for i in range(self.paths_nb.GetPageCount()):
1727 if 'parameters' in self.paths_nb.GetPageText(i).strip().lower():
1728 self.paths_nb.SetSelection(i)
1730 def autosave_script(self, text, fname='feffit_script.lar'):
1731 """autosave model result to user larch folder"""
1732 confdir = self.controller.larix_folder
1733 if fname is None:
1734 fname = 'feffit_script.lar'
1735 fullpath = Path(confdir, fname)
1736 fullname = fullpath.as_posix()
1737 if fullpath.exists():
1738 shutil.copy(fullname, Path(confdir, 'feffit_script_BAK.lar').as_posix())
1739 with open(fullname, 'w', encoding=sys.getdefaultencoding()) as fh:
1740 fh.write(text)
1741 return fullname
1744###############
1746class FeffitResultFrame(wx.Frame):
1747 def __init__(self, parent=None, feffit_panel=None, datagroup=None, **kws):
1748 wx.Frame.__init__(self, None, -1, title='Feffit Results',
1749 style=FRAMESTYLE, size=(1000, 700), **kws)
1751 self.outforms = {'chik': 'chi(k), no k-weight',
1752 'chikw': 'chi(k), k-weighted',
1753 'chir_mag': '|chi(R)|',
1754 'chir_re': 'Real[chi(R)]',
1755 'chiq': 'Filtered \u03c7(k)'
1756 }
1758 self.feffit_panel = feffit_panel
1759 self.datagroup = datagroup
1760 self.feffit_history = getattr(datagroup, 'fit_history', [])
1761 self.parent = parent
1762 self.report_frame = None
1763 self.datasets = {}
1764 self.form = {}
1765 self.larch_eval = feffit_panel.larch_eval
1766 self.nfit = 0
1767 self.createMenus()
1768 self.build()
1770 if datagroup is None:
1771 symtab = self.feffit_panel.larch.symtable
1772 xasgroups = getattr(symtab, '_xasgroups', None)
1773 if xasgroups is not None:
1774 for dname, dgroup in xasgroups.items():
1775 dgroup = getattr(symtab, dgroup, None)
1776 hist = getattr(dgroup, 'feffit_history', None)
1777 if hist is not None:
1778 self.add_results(dgroup, show=True)
1781 def createMenus(self):
1782 self.menubar = wx.MenuBar()
1783 fmenu = wx.Menu()
1784 m = {}
1785 for key, desc in self.outforms.items():
1786 MenuItem(self, fmenu,
1787 f"Save Fit: {desc}",
1788 f"Save data, model, path arrays as {desc}",
1789 partial(self.onSaveFit, form=key))
1791 fmenu.AppendSeparator()
1792 self.menubar.Append(fmenu, "&File")
1793 self.SetMenuBar(self.menubar)
1795 def build(self):
1796 sizer = wx.GridBagSizer(2, 2)
1797 sizer.SetVGap(2)
1798 sizer.SetHGap(2)
1800 splitter = wx.SplitterWindow(self, style=wx.SP_LIVE_UPDATE)
1801 splitter.SetMinimumPaneSize(200)
1803 self.filelist = EditableListBox(splitter, self.ShowDataSet,
1804 size=(250, -1))
1805 set_color(self.filelist, 'list_fg', bg='list_bg')
1807 self.font_fixedwidth = wx.Font(FONTSIZE_FW, wx.MODERN, wx.NORMAL, wx.NORMAL)
1809 panel = scrolled.ScrolledPanel(splitter)
1811 panel.SetMinSize((725, 575))
1812 panel.SetSize((850, 575))
1814 # title row
1815 self.wids = wids = {}
1816 title = SimpleText(panel, 'Feffit Results', font=Font(FONTSIZE+2),
1817 colour=COLORS['title'], style=LEFT)
1819 wids['data_title'] = SimpleText(panel, '< > ', font=Font(FONTSIZE+2),
1820 minsize=(350, -1),
1821 colour=COLORS['title'], style=LEFT)
1823 wids['plot1_op'] = Choice(panel, choices=list(Plot1_Choices.keys()),
1824 action=self.onPlot, size=(125, -1))
1825 wids['plot1_op'].SetSelection(1)
1826 wids['plot2_op'] = Choice(panel, choices=list(Plot2_Choices.keys()),
1827 action=self.onPlot, size=(125, -1))
1829 wids['plot2_win'] = Choice(panel, choices=PlotWindowChoices,
1830 action=self.onPlot, size=(60, -1))
1831 wids['plot2_win'].SetStringSelection('2')
1833 wids['plot_kw'] = Choice(panel, size=(80, -1),
1834 choices=['0', '1', '2', '3', '4'], default=2)
1836 wids['plot1_paths'] = Check(panel, default=False, label='Plot Each Path',
1837 action=self.onPlot)
1838 wids['plot1_ftwins'] = Check(panel, default=False, label='Plot FT Windows',
1839 action=self.onPlot)
1841 wids['plot1_voff'] = FloatSpin(panel, value=0, digits=2, increment=0.25,
1842 action=self.onPlot, size=(100, -1))
1843 wids['plot2_paths'] = Check(panel, default=False, label='Plot Each Path',
1844 action=self.onPlot)
1845 wids['plot2_ftwins'] = Check(panel, default=False, label='Plot FT Windows',
1846 action=self.onPlot)
1848 wids['plot2_voff'] = FloatSpin(panel, value=0, digits=2, increment=0.25,
1849 action=self.onPlot, size=(100, -1))
1851 wids['plot_current'] = Button(panel,'Plot Current Model',
1852 action=self.onPlot, size=(175, -1))
1854 wids['show_pathpars'] = Button(panel,'Show Path Parameters',
1855 action=self.onShowPathParams, size=(175, -1))
1856 wids['show_script'] = Button(panel,'Show Fit Script',
1857 action=self.onShowScript, size=(150, -1))
1859 lpanel = wx.Panel(panel)
1860 wids['fit_label'] = wx.TextCtrl(lpanel, -1, ' ', size=(175, -1))
1861 wids['set_label'] = Button(lpanel, 'Update Label', size=(150, -1),
1862 action=self.onUpdateLabel)
1863 wids['del_fit'] = Button(lpanel, 'Remove from Fit History', size=(200, -1),
1864 action=self.onRemoveFromHistory)
1866 lsizer = wx.BoxSizer(wx.HORIZONTAL)
1867 lsizer.Add(wids['fit_label'], 0, 2)
1868 lsizer.Add(wids['set_label'], 0, 2)
1869 lsizer.Add(wids['del_fit'], 0, 2)
1870 pack(lpanel, lsizer)
1872 irow = 0
1873 sizer.Add(title, (irow, 0), (1, 1), LEFT)
1874 sizer.Add(wids['data_title'], (irow, 1), (1, 3), LEFT)
1876 irow += 1
1877 sizer.Add(wids['plot_current'], (irow, 0), (1, 1), LEFT)
1878 sizer.Add(wids['plot1_op'], (irow, 1), (1, 1), LEFT)
1879 sizer.Add(SimpleText(panel, 'k-weight'), (irow, 2), (1, 1), LEFT)
1880 sizer.Add(wids['plot_kw'], (irow, 3), (1, 1), LEFT)
1881 irow += 1
1882 sizer.Add(wids['plot1_ftwins'], (irow, 0), (1, 1), LEFT)
1883 sizer.Add(wids['plot1_paths'], (irow, 1), (1, 1), LEFT)
1884 sizer.Add(SimpleText(panel, 'Vertical Offest:'), (irow, 2), (1, 1), LEFT)
1885 sizer.Add(wids['plot1_voff'], (irow, 3), (1, 1), LEFT)
1887 irow += 1
1888 sizer.Add(SimpleText(panel, 'Add Second Plot:', style=LEFT), (irow, 0), (1, 1), LEFT)
1889 sizer.Add(wids['plot2_op'], (irow, 1), (1, 1), LEFT)
1890 sizer.Add(SimpleText(panel, 'Plot Window:', style=LEFT), (irow, 2), (1, 1), LEFT)
1891 sizer.Add(wids['plot2_win'], (irow, 3), (1, 1), LEFT)
1892 irow += 1
1893 sizer.Add(wids['plot2_ftwins'], (irow, 0), (1, 1), LEFT)
1894 sizer.Add(wids['plot2_paths'], (irow, 1), (1, 1), LEFT)
1895 sizer.Add(SimpleText(panel, 'Vertical Offest:'), (irow, 2), (1, 1), LEFT)
1896 sizer.Add(wids['plot2_voff'], (irow, 3), (1, 1), LEFT)
1898 irow += 1
1899 sizer.Add(wids['show_pathpars'], (irow, 0), (1, 1), LEFT)
1900 sizer.Add(wids['show_script'], (irow, 1), (1, 1), LEFT)
1901 irow += 1
1902 sizer.Add(HLine(panel, size=(650, 3)), (irow, 0), (1, 5), LEFT)
1904 irow += 1
1905 sizer.Add(SimpleText(panel, 'Fit Label:', style=LEFT), (irow, 0), (1, 1), LEFT)
1906 sizer.Add(lpanel, (irow, 1), (1, 4), LEFT)
1908 irow += 1
1909 title = SimpleText(panel, '[[Fit Statistics]]', font=Font(FONTSIZE+2),
1910 colour=COLORS['title'], style=LEFT)
1911 subtitle = SimpleText(panel, ' (most recent fit is at the top)',
1912 font=Font(FONTSIZE+1), style=LEFT)
1914 sizer.Add(title, (irow, 0), (1, 1), LEFT)
1915 sizer.Add(subtitle, (irow, 1), (1, 3), LEFT)
1917 sview = self.wids['stats'] = dv.DataViewListCtrl(panel, style=DVSTYLE)
1918 sview.SetFont(self.font_fixedwidth)
1919 sview.Bind(dv.EVT_DATAVIEW_SELECTION_CHANGED, self.onSelectFit)
1921 xw = (40, 135, 80, 80, 92, 92, 122, 100)
1923 sview.AppendTextColumn(' # ', width=xw[0])
1924 sview.AppendTextColumn('Label', width=xw[1])
1925 sview.AppendTextColumn('Npaths', width=xw[2])
1926 sview.AppendTextColumn('Nvary', width=xw[3])
1927 sview.AppendTextColumn('Nidp', width=xw[4])
1928 sview.AppendTextColumn('\u03c7\u00B2', width=xw[5])
1929 sview.AppendTextColumn('reduced \u03c7\u00B2', width=xw[6])
1930 sview.AppendTextColumn('R Factor', width=xw[7])
1932 for col in range(sview.ColumnCount):
1933 this = sview.Columns[col]
1934 this.Sortable = True
1935 this.Alignment = wx.ALIGN_RIGHT if col > 1 else wx.ALIGN_LEFT
1936 this.Renderer.Alignment = this.Alignment
1938 sview.SetMinSize((750, 150))
1940 irow += 1
1941 sizer.Add(sview, (irow, 0), (1, 5), LEFT)
1943 irow += 1
1944 title = SimpleText(panel, '[[Variables]]', font=Font(FONTSIZE+2),
1945 colour=COLORS['title'], style=LEFT)
1946 sizer.Add(title, (irow, 0), (1, 1), LEFT)
1948 self.wids['copy_params'] = Button(panel, 'Update Model with these values',
1949 size=(225, -1), action=self.onCopyParams)
1951 sizer.Add(self.wids['copy_params'], (irow, 1), (1, 3), LEFT)
1953 pview = self.wids['params'] = dv.DataViewListCtrl(panel, style=DVSTYLE)
1954 pview.SetFont(self.font_fixedwidth)
1955 self.wids['paramsdata'] = []
1956 xw = (180, 140, 150, 250)
1958 pview.AppendTextColumn('Parameter', width=xw[0])
1959 pview.AppendTextColumn('Best Value', width=xw[1])
1960 pview.AppendTextColumn('1-\u03c3 Uncertainty', width=xw[2])
1961 pview.AppendTextColumn('Info ', width=xw[3])
1963 for col in range(4):
1964 this = pview.Columns[col]
1965 this.Sortable = False
1966 this.Alignment = wx.ALIGN_RIGHT if col in (1, 2) else wx.ALIGN_LEFT
1967 this.Renderer.Alignment = this.Alignment
1969 pview.SetMinSize((750, 200))
1970 pview.Bind(dv.EVT_DATAVIEW_SELECTION_CHANGED, self.onSelectParameter)
1972 irow += 1
1973 sizer.Add(pview, (irow, 0), (1, 5), LEFT)
1975 irow += 1
1976 title = SimpleText(panel, '[[Correlations]]', font=Font(FONTSIZE+2),
1977 colour=COLORS['title'], style=LEFT)
1979 ppanel = wx.Panel(panel)
1980 ppanel.SetMinSize((450, 20))
1981 self.wids['all_correl'] = Button(ppanel, 'Show All',
1982 size=(100, -1), action=self.onAllCorrel)
1984 self.wids['min_correl'] = FloatSpin(ppanel, value=MIN_CORREL,
1985 min_val=0, size=(100, -1),
1986 digits=3, increment=0.1)
1988 psizer = wx.BoxSizer(wx.HORIZONTAL)
1989 psizer.Add(SimpleText(ppanel, 'minimum correlation: '), 0, 2)
1990 psizer.Add(self.wids['min_correl'], 0, 2)
1991 psizer.Add(self.wids['all_correl'], 0, 2)
1992 pack(ppanel, psizer)
1994 sizer.Add(title, (irow, 0), (1, 1), LEFT)
1995 sizer.Add(ppanel, (irow, 1), (1, 4), LEFT)
1997 cview = self.wids['correl'] = dv.DataViewListCtrl(panel, style=DVSTYLE)
1998 cview.SetFont(self.font_fixedwidth)
2000 cview.AppendTextColumn('Parameter 1', width=150)
2001 cview.AppendTextColumn('Parameter 2', width=150)
2002 cview.AppendTextColumn('Correlation', width=150)
2004 for col in (0, 1, 2):
2005 this = cview.Columns[col]
2006 this.Sortable = False
2007 align = wx.ALIGN_LEFT
2008 if col == 2:
2009 align = wx.ALIGN_RIGHT
2010 this.Alignment = this.Renderer.Alignment = align
2011 cview.SetMinSize((550, 150))
2013 irow += 1
2014 sizer.Add(cview, (irow, 0), (1, 5), LEFT)
2016 pack(panel, sizer)
2017 panel.SetupScrolling()
2019 splitter.SplitVertically(self.filelist, panel, 1)
2021 mainsizer = wx.BoxSizer(wx.VERTICAL)
2022 mainsizer.Add(splitter, 1, wx.GROW|wx.ALL, 5)
2024 pack(self, mainsizer)
2025 self.Show()
2026 self.Raise()
2028 def show_report(self, text, title='Text', default_filename='out.txt',
2029 wildcard=None):
2030 if wildcard is None:
2031 wildcard='Text Files (*.txt)|*.txt'
2032 try:
2033 self.report_frame.set_text(text)
2034 self.report_frame.SetTitle(title)
2035 self.report_frame.default_filename = default_filename
2036 self.report_frame.wildcard = wildcard
2037 except:
2038 self.report_frame = ReportFrame(parent=self.parent,
2039 text=text, title=title,
2040 default_filename=default_filename,
2041 wildcard=wildcard)
2043 def onShowPathParams(self, event=None):
2044 result = self.get_fitresult()
2045 if result is None:
2046 return
2047 text = f'# Feffit Report for {self.datagroup.filename} fit "{result.label}"\n'
2048 text = text + feffit_report(result)
2049 title = f'Report for {self.datagroup.filename} fit "{result.label}"'
2050 fname = fix_filename(f'{self.datagroup.filename}_{result.label}.txt')
2051 self.show_report(text, title=title, default_filename=fname)
2053 def onShowScript(self, event=None):
2054 result = self.get_fitresult()
2055 if result is None:
2056 return
2057 text = [f'# Feffit Script for {self.datagroup.filename} fit "{result.label}"']
2058 text.extend(result.commands)
2059 text = '\n'.join(text)
2060 title = f'Script for {self.datagroup.filename} fit "{result.label}"'
2061 fname = fix_filename(f'{self.datagroup.filename}_{result.label}.lar')
2062 self.show_report(text, title=title, default_filename=fname,
2063 wildcard='Larch/Python Script (*.lar)|*.lar')
2065 def onUpdateLabel(self, event=None):
2066 result = self.get_fitresult()
2067 if result is None:
2068 return
2069 item = self.wids['stats'].GetSelectedRow()
2070 result.label = self.wids['fit_label'].GetValue()
2071 self.show_results()
2073 def onRemoveFromHistory(self, event=None):
2074 result = self.get_fitresult()
2075 if result is None:
2076 return
2077 if wx.ID_YES != Popup(self,
2078 f"Remove fit '{result.label}' from history?\nThis cannot be undone.",
2079 "Remove fit?", style=wx.YES_NO):
2080 return
2081 self.datagroup.feffit_history.pop(self.nfit)
2082 self.nfit = 0
2083 self.show_results()
2085 def onPlot(self, event=None):
2086 result = self.get_fitresult()
2087 if result is None:
2088 return
2089 dset = result.datasets[0]
2090 dgroup = dset.data
2091 if not hasattr(dset.data, 'rwin'):
2092 dset._residual(result.params)
2093 dset.save_outputs()
2094 trans = dset.transform
2095 dset.prepare_fit(result.params)
2096 dset._residual(result.params)
2098 opts = self.feffit_panel.read_form(dgroup=dgroup)
2099 opts = {'build_fitmodel': False}
2101 for key, meth in (('plot1_ftwins', 'IsChecked'),
2102 ('plot2_ftwins', 'IsChecked'),
2103 ('plot1_paths', 'IsChecked'),
2104 ('plot2_paths', 'IsChecked'),
2105 ('plot1_op', 'GetStringSelection'),
2106 ('plot2_op', 'GetStringSelection'),
2107 ('plot1_voff', 'GetValue'),
2108 ('plot2_voff', 'GetValue'),
2109 ('plot_kw', 'GetStringSelection'),
2110 ('plot2_win', 'GetStringSelection'),
2111 ):
2112 opts[key] = getattr(self.wids[key], meth)()
2114 opts['plot1_op'] = Plot1_Choices[opts['plot1_op']]
2115 opts['plot2_op'] = Plot2_Choices[opts['plot2_op']]
2116 opts['plot2_win'] = int(opts['plot2_win'])
2117 opts['plot_kw'] = int(opts['plot_kw'])
2120 result_name = f'{self.datagroup.groupname}.feffit_history[{self.nfit}]'
2121 opts['label'] = f'{result_name}.label'
2122 opts['filename'] = self.datagroup.filename
2123 opts['pargroup_name'] = f'{result_name}.paramgroup'
2124 opts['title'] = f'{self.datagroup.filename}: {result.label}'
2126 for attr in ('kmin', 'kmax', 'dk', 'rmin', 'rmax', 'fitspace'):
2127 opts[attr] = getattr(trans, attr)
2128 opts['fit_kwstring'] = "%s" % getattr(trans, 'kweight')
2129 opts['kwindow'] = getattr(trans, 'window')
2130 opts['topwin'] = self
2132 exafs_conf = self.feffit_panel.parent.get_nbpage('exafs')[1].read_form()
2133 opts['plot_rmax'] = exafs_conf['plot_rmax']
2134 self.feffit_panel.plot_feffit_result(f'{result_name}.datasets[0]', **opts)
2137 def onSaveFitCommand(self, event=None):
2138 wildcard = 'Larch/Python Script (*.lar)|*.lar|All files (*.*)|*.*'
2139 result = self.get_fitresult()
2140 if result is None:
2141 return
2142 fname = fix_filename(f'{self.datagroup.filename}_{result.label:s}.lar')
2144 path = FileSave(self, message='Save text to file',
2145 wildcard=wildcard, default_file=fname)
2146 if path is not None:
2147 text = '\n'.join(result.commands)
2148 with open(path, 'w', encoding=sys.getdefaultencoding()) as fh:
2149 fh.write(text)
2150 fh.write('')
2153 def onSaveFit(self, evt=None, form='chikw'):
2154 "Save arrays to text file"
2155 result = self.get_fitresult()
2156 if result is None:
2157 return
2159 fname = fix_filename(f'{self.datagroup.filename}_{result.label:s}_{form}')
2160 fname = fname.replace('.', '_')
2161 fname = fname + '.txt'
2163 wildcard = 'Text Files (*.txt)|*.txt|All files (*.*)|*.*'
2164 savefile = FileSave(self, 'Save Fit Model (%s)' % form,
2165 default_file=fname,
2166 wildcard=wildcard)
2167 if savefile is None:
2168 return
2170 text = feffit_report(result)
2171 desc = self.outforms[form]
2172 buff = [f'# Results for {self.datagroup.filename} "{result.label}": {desc}']
2174 for line in text.split('\n'):
2175 buff.append('# %s' % line)
2176 buff.append('## ')
2177 buff.append('#' + '---'*25)
2179 ds0 = result.datasets[0]
2181 xname = 'k' if form.startswith('chik') else 'r'
2182 yname = 'chi' if form.startswith('chik') else form
2183 yname = 'chiq_re' if form.startswith('chiq') else form
2184 kw = 0
2185 if form == 'chikw':
2186 kw = ds0.transform.kweight
2188 xarr = getattr(ds0.data, xname)
2189 nx = len(xarr)
2190 ydata = getattr(ds0.data, yname)[:nx] * xarr**kw
2191 ymodel = getattr(ds0.model, yname)[:nx] * xarr**kw
2192 out = [xarr, ydata, ymodel]
2194 array_names = [xname, 'expdata', 'model']
2195 for pname, pgroup in ds0.paths.items():
2196 array_names.append(f'feffpath_{pname}')
2197 out.append(getattr(pgroup, yname)[:nx] * xarr**kw)
2199 col_labels = []
2200 for a in array_names:
2201 if len(a) < 13:
2202 a = (a + ' '*13)[:13]
2203 col_labels.append(a)
2205 buff.append('# ' + ' '.join(col_labels))
2207 for i in range(nx):
2208 words = [gformat(x[i], 12) for x in out]
2209 buff.append(' '.join(words))
2210 buff.append('')
2213 with open(savefile, 'w', encoding=sys.getdefaultencoding()) as fh:
2214 fh.write('\n'.join(buff))
2216 def get_fitresult(self, nfit=None):
2217 if nfit is None:
2218 nfit = self.nfit
2219 self.feffit_history = getattr(self.datagroup, 'feffit_history', [])
2220 self.nfit = max(0, nfit)
2221 n_hist = len(self.feffit_history)
2222 if n_hist == 0:
2223 return None
2224 if self.nfit > n_hist:
2225 self.nfit = 0
2226 return self.feffit_history[self.nfit]
2229 def onSelectFit(self, evt=None):
2230 if self.wids['stats'] is None:
2231 return
2232 item = self.wids['stats'].GetSelectedRow()
2233 if item > -1:
2234 self.show_fitresult(nfit=item)
2236 def onSelectParameter(self, evt=None):
2237 if self.wids['params'] is None:
2238 return
2239 if not self.wids['params'].HasSelection():
2240 return
2241 item = self.wids['params'].GetSelectedRow()
2242 pname = self.wids['paramsdata'][item]
2244 cormin= self.wids['min_correl'].GetValue()
2245 self.wids['correl'].DeleteAllItems()
2247 result = self.get_fitresult()
2248 if result is None:
2249 return
2250 this = result.params[pname]
2251 if this.correl is not None:
2252 sort_correl = sorted(this.correl.items(), key=lambda it: abs(it[1]))
2253 for name, corval in reversed(sort_correl):
2254 if abs(corval) > cormin:
2255 self.wids['correl'].AppendItem((pname, name, "% .4f" % corval))
2257 def onAllCorrel(self, evt=None):
2258 result = self.get_fitresult()
2259 if result is None:
2260 return
2261 params = result.params
2262 parnames = list(params.keys())
2264 cormin= self.wids['min_correl'].GetValue()
2265 correls = {}
2266 for i, name in enumerate(parnames):
2267 par = params[name]
2268 if not par.vary:
2269 continue
2270 if hasattr(par, 'correl') and par.correl is not None:
2271 for name2 in parnames[i+1:]:
2272 if (name != name2 and name2 in par.correl and
2273 abs(par.correl[name2]) > cormin):
2274 correls["%s$$%s" % (name, name2)] = par.correl[name2]
2276 sort_correl = sorted(correls.items(), key=lambda it: abs(it[1]))
2277 sort_correl.reverse()
2279 self.wids['correl'].DeleteAllItems()
2281 for namepair, corval in sort_correl:
2282 name1, name2 = namepair.split('$$')
2283 self.wids['correl'].AppendItem((name1, name2, "% .4f" % corval))
2285 def onCopyParams(self, evt=None):
2286 result = self.get_fitresult()
2287 if result is None:
2288 return
2289 self.feffit_panel.update_start_values(result.params)
2291 def ShowDataSet(self, evt=None):
2292 dataset = evt.GetString()
2293 group = self.datasets.get(evt.GetString(), None)
2294 if group is not None:
2295 self.show_results(datagroup=group)
2297 def add_results(self, dgroup, form=None, larch_eval=None, show=True):
2298 name = dgroup.filename
2299 if name not in self.filelist.GetItems():
2300 self.filelist.Append(name)
2301 self.datasets[name] = dgroup
2302 if show:
2303 self.show_results(datagroup=dgroup, form=form, larch_eval=larch_eval)
2305 def show_results(self, datagroup=None, form=None, larch_eval=None):
2306 if datagroup is not None:
2307 self.datagroup = datagroup
2308 if larch_eval is not None:
2309 self.larch_eval = larch_eval
2311 datagroup = self.datagroup
2312 self.feffit_history = getattr(self.datagroup, 'feffit_history', [])
2314 cur = self.get_fitresult()
2315 if cur is None:
2316 return
2317 wids = self.wids
2318 wids['stats'].DeleteAllItems()
2319 for i, res in enumerate(self.feffit_history):
2320 args = ["%d" % (i+1), res.label, "%.d" % (len(res.datasets[0].paths))]
2321 for attr in ('nvarys', 'n_independent', 'chi_square',
2322 'chi2_reduced', 'rfactor'):
2323 val = getattr(res, attr)
2324 if isinstance(val, int):
2325 val = '%d' % val
2326 elif attr == 'n_independent':
2327 val = "%.2f" % val
2328 else:
2329 val = "%.4f" % val
2330 # val = gformat(val, 9)
2331 args.append(val)
2332 wids['stats'].AppendItem(tuple(args))
2333 wids['data_title'].SetLabel(self.datagroup.filename)
2334 self.show_fitresult(nfit=0)
2337 def show_fitresult(self, nfit=0, datagroup=None):
2338 if datagroup is not None:
2339 self.datagroup = datagroup
2341 result = self.get_fitresult(nfit=nfit)
2342 if result is None:
2343 return
2345 path_hashkeys = []
2346 for ds in result.datasets:
2347 path_hashkeys.extend([p.hashkey for p in ds.paths.values()])
2349 wids = self.wids
2350 wids['fit_label'].SetValue(result.label)
2351 wids['data_title'].SetLabel(self.datagroup.filename)
2352 wids['params'].DeleteAllItems()
2353 wids['paramsdata'] = []
2354 for param in reversed(result.params.values()):
2355 pname = param.name
2356 if any([pname.endswith('_%s' % phash) for phash in path_hashkeys]):
2357 continue
2358 if getattr(param, 'skip', None) not in (False, None):
2359 continue
2361 try:
2362 val = gformat(param.value, 10)
2363 except (TypeError, ValueError):
2364 val = ' ??? '
2365 serr = ' N/A '
2366 if param.stderr is not None:
2367 serr = gformat(param.stderr, 10)
2368 extra = ' '
2369 if param.expr is not None:
2370 extra = '= %s ' % param.expr
2371 elif not param.vary:
2372 extra = '(fixed)'
2373 elif param.init_value is not None:
2374 extra = '(init=%s)' % gformat(param.init_value, 10)
2376 wids['params'].AppendItem((pname, val, serr, extra))
2377 wids['paramsdata'].append(pname)
2378 self.Refresh()