Coverage for /Users/Newville/Codes/xraylarch/larch/wxxas/feffit_panel.py: 9%

1542 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-11-09 10:08 -0600

1import time 

2import os 

3import sys 

4import ast 

5import shutil 

6import string 

7import json 

8import math 

9from copy import deepcopy 

10from sys import exc_info 

11from string import printable 

12from functools import partial 

13 

14import numpy as np 

15np.seterr(all='ignore') 

16 

17 

18import wx 

19import wx.lib.scrolledpanel as scrolled 

20 

21import wx.dataview as dv 

22 

23from lmfit import Parameter 

24from lmfit.model import (save_modelresult, load_modelresult, 

25 save_model, load_model) 

26 

27import lmfit.models as lm_models 

28 

29from larch import Group, site_config 

30from larch.math import index_of 

31from larch.fitting import group2params, param 

32from larch.utils.jsonutils import encode4js, decode4js 

33from larch.utils import fix_varname, fix_filename, gformat, mkdir 

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 

38 

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) 

46 

47from larch.wxlib.parameter import ParameterWidgets 

48from larch.wxlib.plotter import last_cursor_pos 

49from .taskpanel import TaskPanel 

50 

51from .config import (Feffit_KWChoices, Feffit_SpaceChoices, 

52 Feffit_PlotChoices, make_array_choice, 

53 PlotWindowChoices) 

54 

55DVSTYLE = dv.DV_SINGLE|dv.DV_VERT_RULES|dv.DV_ROW_LINES 

56 

57# PlotOne_Choices = [chik, chirmag, chirre, chirmr] 

58 

59PlotOne_Choices = make_array_choice(['chi','chir_mag', 'chir_re', 'chir_mag+chir_re', 'chiq']) 

60PlotAlt_Choices = make_array_choice(['noplot', 'chi','chir_mag', 'chir_re', 'chir_mag+chir_re']) 

61 

62# PlotAlt_Choices = [noplot] + PlotOne_Choices 

63 

64ScriptWcards = "Fit Models(*.lar)|*.lar|All files (*.*)|*.*" 

65 

66MIN_CORREL = 0.10 

67 

68COMMANDS = {} 

69COMMANDS['feffit_top'] = """## saved {ctime} 

70## commmands to reproduce Feffit 

71## to use from python, uncomment these lines: 

72#from larch.xafs import feffit, feffit_dataset, feffit_transform, feffit_report 

73#from larch.xafs import pre_edge, autobk, xftf, xftr, ff2chi, feffpath 

74#from larch.fitting import param_group, param 

75#from larch.io import read_ascii, read_athena, read_xdi, read_specfile 

76# 

77#### for interactive plotting from python (but not the Larch shell!) use: 

78#from larch.wxlib.xafsplots import plot_chik, plot_chir 

79#from wxmplot.interactive import get_wxapp 

80#wxapp = get_wxapp() # <- needed for plotting to work from python command-line 

81#### 

82""" 

83 

84COMMANDS['data_source'] = """# you will need to add how the data chi(k) gets built: 

85## data group = {groupname} 

86## from source = {filename} 

87## some processing steps for this group (comment out as needed): 

88""" 

89 

90COMMANDS['xft'] = """# ffts on group {groupname:s} 

91xftf({groupname:s}, kmin={kmin:.3f}, kmax={kmax:.3f}, dk={dk:.3f}, window='{kwindow:s}', kweight={kweight:.3f}) 

92xftr({groupname:s}, rmin={rmin:.3f}, rmax={rmax:.3f}, dr={dr:.3f}, window='{rwindow:s}') 

93""" 

94 

95COMMANDS['feffit_params_init'] = """# create feffit Parameter Group to hold fit parameters 

96_feffit_params = param_group(reff=-1.0) 

97""" 

98 

99COMMANDS['feffit_trans'] = """# define Fourier transform and fitting space 

100_feffit_trans = feffit_transform(kmin={fit_kmin:.3f}, kmax={fit_kmax:.3f}, dk={fit_dk:.4f}, kw={fit_kwstring:s}, 

101 window='{fit_kwindow:s}', fitspace='{fit_space:s}', rmin={fit_rmin:.3f}, rmax={fit_rmax:.3f}) 

102""" 

103 

104COMMANDS['paths_init'] = """# make sure dictionary for Feff Paths exists 

105try: 

106 npaths = len(_feffpaths.keys()) 

107except: 

108 _feffcache = {'paths':{}, 'runs':{}} # group of all paths, info about Feff runs 

109 _feffpaths = {} # dict of paths currently in use, copied from _feffcache.paths 

110#endtry 

111""" 

112 

113COMMANDS['paths_reset'] = """# clear existing paths 

114npaths = 0 

115_feffpaths = {} 

116#endtry 

117""" 

118 

119COMMANDS['cache_path'] = """ 

120_feffcache['paths']['{title:s}'] = feffpath('{fullpath:s}', 

121 label='{title:s}',feffrun='{feffrun:s}', degen=1) 

122""" 

123 

124COMMANDS['use_path'] = """ 

125_feffpaths['{title:s}'] = use_feffpath(_feffcache['paths'], '{title:s}', 

126 s02='{amp:s}', e0='{e0:s}', 

127 deltar='{delr:s}', sigma2='{sigma2:s}', 

128 third='{third:s}', ei='{ei:s}', use={use}) 

129""" 

130 

131COMMANDS['ff2chi'] = """# sum paths using a list of paths and a group of parameters 

132_pathsum = ff2chi({paths:s}, paramgroup=_feffit_params) 

133""" 

134 

135COMMANDS['do_feffit'] = """# build feffit dataset, run feffit 

136_feffit_dataset = feffit_dataset(data={groupname:s}, transform={trans:s}, paths={paths:s}) 

137_feffit_result = feffit({params}, _feffit_dataset) 

138if not hasattr({groupname:s}, 'feffit_history'): {groupname}.feffit_history = [] 

139{groupname:s}.feffit_history.insert(0, _feffit_result) 

140""" 

141 

142COMMANDS['path2chi'] = """# generate chi(k) and chi(R) for each path 

143for label, path in {paths_name:s}.items(): 

144 path.calc_chi_from_params({pargroup_name:s}) 

145 xftf(path, kmin={kmin:.3f}, kmax={kmax:.3f}, dk={dk:.3f}, 

146 window='{kwindow:s}', kweight={kweight:.3f}) 

147#endfor 

148""" 

149 

150 

151class ParametersModel(dv.DataViewIndexListModel): 

152 def __init__(self, paramgroup, selected=None, pathkeys=None): 

153 dv.DataViewIndexListModel.__init__(self, 0) 

154 self.data = [] 

155 if selected is None: 

156 selected = [] 

157 self.selected = selected 

158 

159 if pathkeys is None: 

160 pathkeys = [] 

161 self.pathkeys = pathkeys 

162 

163 self.paramgroup = paramgroup 

164 self.read_data() 

165 

166 def set_data(self, paramgroup, selected=None, pathkeys=None): 

167 self.paramgroup = paramgroup 

168 if selected is not None: 

169 self.selected = selected 

170 if pathkeys is not None: 

171 self.pathkeys = pathkeys 

172 self.read_data() 

173 

174 def read_data(self): 

175 self.data = [] 

176 if self.paramgroup is None: 

177 self.data.append(['param name', False, 'vary', '0.0']) 

178 else: 

179 for pname, par in group2params(self.paramgroup).items(): 

180 if any([pname.endswith('_%s' % phash) for phash in self.pathkeys]): 

181 continue 

182 ptype = 'vary' 

183 if not par.vary: 

184 pytype = 'fixed' 

185 if getattr(par, 'skip', None) not in (False, None): 

186 ptype = 'skip' 

187 par.skip = ptype == 'skip' 

188 try: 

189 value = str(par.value) 

190 except: 

191 value = 'INVALID ' 

192 if par.expr is not None: 

193 ptype = 'constraint' 

194 value = "%s := %s" % (value, par.expr) 

195 sel = pname in self.selected 

196 self.data.append([pname, sel, ptype, value]) 

197 self.Reset(len(self.data)) 

198 

199 def select_all(self, value=True): 

200 self.selected = [] 

201 for irow, row in enumerate(self.data): 

202 self.SetValueByRow(value, irow, 1) 

203 if value: 

204 self.selected.append(row[0]) 

205 

206 def select_none(self): 

207 self.select_all(value=False) 

208 

209 def GetColumnType(self, col): 

210 return "bool" if col == 2 else "string" 

211 

212 def GetValueByRow(self, row, col): 

213 return self.data[row][col] 

214 

215 def SetValueByRow(self, value, row, col): 

216 self.data[row][col] = value 

217 return True 

218 

219 def GetColumnCount(self): 

220 return len(self.data[0]) 

221 

222 def GetCount(self): 

223 return len(self.data) 

224 

225 def GetAttrByRow(self, row, col, attr): 

226 """set row/col attributes (color, etc)""" 

227 ptype = self.data[row][2] 

228 if ptype == 'vary': 

229 attr.SetColour('#000000') 

230 elif ptype == 'fixed': 

231 attr.SetColour('#AA2020') 

232 elif ptype == 'skip': 

233 attr.SetColour('#50AA50') 

234 else: 

235 attr.SetColour('#2010BB') 

236 return True 

237 

238class EditParamsFrame(wx.Frame): 

239 """ edit parameters""" 

240 def __init__(self, parent=None, feffit_panel=None, 

241 paramgroup=None, selected=None): 

242 wx.Frame.__init__(self, None, -1, 

243 'Edit Feffit Parameters', 

244 style=FRAMESTYLE, size=(550, 325)) 

245 

246 self.parent = parent 

247 self.feffit_panel = feffit_panel 

248 self.paramgroup = paramgroup 

249 

250 spanel = scrolled.ScrolledPanel(self, size=(500, 275)) 

251 spanel.SetBackgroundColour('#EEEEEE') 

252 

253 self.font_fixedwidth = wx.Font(FONTSIZE_FW, wx.MODERN, wx.NORMAL, wx.BOLD) 

254 

255 self.dvc = dv.DataViewCtrl(spanel, style=DVSTYLE) 

256 self.dvc.SetFont(self.font_fixedwidth) 

257 self.SetMinSize((500, 250)) 

258 

259 self.model = ParametersModel(paramgroup, selected) 

260 self.dvc.AssociateModel(self.model) 

261 

262 sizer = wx.BoxSizer(wx.VERTICAL) 

263 sizer.Add(self.dvc, 1, LEFT|wx.ALL|wx.GROW) 

264 pack(spanel, sizer) 

265 

266 spanel.SetupScrolling() 

267 

268 toppan = GridPanel(self, ncols=4, pad=1, itemstyle=LEFT) 

269 

270 bkws = dict(size=(200, -1)) 

271 toppan.Add(Button(toppan, "Select All", action=self.onSelAll, size=(175, -1))) 

272 toppan.Add(Button(toppan, "Select None", action=self.onSelNone, size=(175, -1))) 

273 toppan.Add(Button(toppan, "Select Unused Variables", action=self.onSelUnused, size=(200, -1))) 

274 toppan.Add(Button(toppan, "Remove Selected", action=self.onRemove, size=(175,-1)), newrow=True) 

275 toppan.Add(Button(toppan, "'Skip' Selected", action=self.onSkip, size=(175, -1))) 

276 toppan.Add(Button(toppan, "Force Refresh", action=self.onRefresh, size=(200, -1))) 

277 npan = wx.Panel(toppan) 

278 nsiz = wx.BoxSizer(wx.HORIZONTAL) 

279 

280 self.par_name = wx.TextCtrl(npan, -1, value='par_name', size=(125, -1), 

281 style=wx.TE_PROCESS_ENTER) 

282 self.par_expr = wx.TextCtrl(npan, -1, value='<expression or value>', size=(250, -1), 

283 style=wx.TE_PROCESS_ENTER) 

284 nsiz.Add(SimpleText(npan, "Add Parameter:"), 0) 

285 nsiz.Add(self.par_name, 0) 

286 nsiz.Add(self.par_expr, 1, wx.GROW|wx.ALL) 

287 nsiz.Add(Button(npan, label='Add', action=self.onAddParam), 0) 

288 pack(npan, nsiz) 

289 

290 toppan.Add(npan, dcol=4, newrow=True) 

291 toppan.Add(HLine(toppan, size=(500, 2)), dcol=5, newrow=True) 

292 toppan.pack() 

293 

294 mainsizer = wx.BoxSizer(wx.VERTICAL) 

295 mainsizer.Add(toppan, 0, wx.GROW|wx.ALL, 1) 

296 mainsizer.Add(spanel, 1, wx.GROW|wx.ALL, 1) 

297 pack(self, mainsizer) 

298 

299 columns = [('Parameter', 150, 'text'), 

300 ('Select', 75, 'bool'), 

301 ('Type', 75, 'text'), 

302 ('Value', 200, 'text')] 

303 

304 for icol, dat in enumerate(columns): 

305 label, width, dtype = dat 

306 method = self.dvc.AppendTextColumn 

307 mode = dv.DATAVIEW_CELL_EDITABLE 

308 if dtype == 'bool': 

309 method = self.dvc.AppendToggleColumn 

310 mode = dv.DATAVIEW_CELL_ACTIVATABLE 

311 method(label, icol, width=width, mode=mode) 

312 c = self.dvc.Columns[icol] 

313 c.Alignment = c.Renderer.Alignment = wx.ALIGN_LEFT 

314 c.SetSortable(False) 

315 

316 self.dvc.EnsureVisible(self.model.GetItem(0)) 

317 self.Bind(wx.EVT_CLOSE, self.onClose) 

318 

319 self.Show() 

320 self.Raise() 

321 wx.CallAfter(self.onSelUnused) 

322 

323 def onSelAll(self, event=None): 

324 self.model.select_all() 

325 self.model.read_data() 

326 

327 def onSelNone(self, event=None): 

328 self.model.select_none() 

329 self.model.read_data() 

330 

331 def onSelUnused(self, event=None): 

332 curr_syms = self.feffit_panel.get_used_params() 

333 unused = [] 

334 for pname, par in group2params(self.paramgroup).items(): 

335 if pname not in curr_syms: # and par.vary: 

336 unused.append(pname) 

337 self.model.set_data(self.paramgroup, selected=unused, 

338 pathkeys=self.feffit_panel.get_pathkeys()) 

339 

340 def onRemove(self, event=None): 

341 out = [] 

342 for pname, sel, ptype, val in self.model.data: 

343 if sel: 

344 out.append(pname) 

345 nout = len(out) 

346 

347 msg = f"Remove {nout:d} Parameters? \n This is not easy to undo!" 

348 dlg = wx.MessageDialog(self, msg, 'Warning', wx.YES | wx.NO ) 

349 if (wx.ID_YES == dlg.ShowModal()): 

350 for pname, sel, ptype, val in self.model.data: 

351 if sel: 

352 out.append(pname) 

353 if hasattr(self.paramgroup, pname): 

354 delattr(self.paramgroup, pname) 

355 

356 self.model.set_data(self.paramgroup, selected=None, 

357 pathkeys=self.feffit_panel.get_pathkeys()) 

358 self.model.read_data() 

359 self.feffit_panel.get_pathpage('parameters').Rebuild() 

360 dlg.Destroy() 

361 

362 def onSkip(self, event=None): 

363 for pname, sel, ptype, val in self.model.data: 

364 if sel: 

365 par = getattr(self.paramgroup, pname, None) 

366 if par is not None: 

367 par.skip = True 

368 self.model.read_data() 

369 self.feffit_panel.get_pathpage('parameters').Rebuild() 

370 

371 

372 def onAddParam(self, event=None): 

373 par_name = self.par_name.GetValue() 

374 par_expr = self.par_expr.GetValue() 

375 

376 try: 

377 val = float(par_expr) 

378 ptype = 'vary' 

379 except: 

380 val = par_expr 

381 ptype = 'expr' 

382 

383 if ptype == 'vary': 

384 cmd = f"_feffit_params.{par_name} = param({val}, vary=True)" 

385 else: 

386 cmd = f"_feffit_params.{par_name} = param(expr='{val}')" 

387 

388 self.feffit_panel.larch_eval(cmd) 

389 self.onRefresh() 

390 

391 def onRefresh(self, event=None): 

392 self.paramgroup = self.feffit_panel.get_paramgroup() 

393 self.model.set_data(self.paramgroup, 

394 pathkeys=self.feffit_panel.get_pathkeys()) 

395 self.model.read_data() 

396 self.feffit_panel.get_pathpage('parameters').Rebuild() 

397 

398 def onClose(self, event=None): 

399 self.Destroy() 

400 

401 

402class FeffitParamsPanel(wx.Panel): 

403 def __init__(self, parent=None, feffit_panel=None, **kws): 

404 wx.Panel.__init__(self, parent, -1, size=(550, 250)) 

405 self.feffit_panel = feffit_panel 

406 self.parwids = {} 

407 self.SetFont(Font(FONTSIZE)) 

408 spanel = scrolled.ScrolledPanel(self) 

409 spanel.SetSize((250, 250)) 

410 spanel.SetMinSize((50, 50)) 

411 panel = self.panel = GridPanel(spanel, ncols=8, nrows=30, pad=1, itemstyle=LEFT) 

412 panel.SetFont(Font(FONTSIZE)) 

413 

414 def SLabel(label, size=(80, -1), **kws): 

415 return SimpleText(panel, label, size=size, style=wx.ALIGN_LEFT, **kws) 

416 

417 panel.Add(SLabel("Feffit Parameters ", colour='#0000AA', size=(200, -1)), dcol=2) 

418 panel.Add(Button(panel, 'Edit Parameters', action=self.onEditParams), dcol=2) 

419 panel.Add(Button(panel, 'Force Refresh', action=self.Rebuild), dcol=3) 

420 

421 panel.Add(SLabel("Parameter "), style=wx.ALIGN_LEFT, newrow=True) 

422 panel.AddMany((SLabel(" Value"), SLabel(" Type"), SLabel(' Bounds'), 

423 SLabel(" Min", size=(60, -1)), 

424 SLabel(" Max", size=(60, -1)), 

425 SLabel(" Expression"))) 

426 

427 self.update() 

428 panel.pack() 

429 ssizer = wx.BoxSizer(wx.VERTICAL) 

430 ssizer.Add(panel, 1, wx.GROW|wx.ALL, 2) 

431 pack(spanel, ssizer) 

432 

433 spanel.SetupScrolling() 

434 mainsizer = wx.BoxSizer(wx.VERTICAL) 

435 mainsizer.Add(spanel, 1, wx.GROW|wx.ALL, 2) 

436 pack(self, mainsizer) 

437 

438 def Rebuild(self, event=None): 

439 for pname, parwid in self.parwids.items(): 

440 for x in parwid.widgets: 

441 x.Destroy() 

442 self.panel.irow = 1 

443 self.parwids = {} 

444 self.update() 

445 

446 def set_init_values(self, params): 

447 for pname, par in params.items(): 

448 if pname in self.parwids and par.vary: 

449 stderr = getattr(par, 'stderr', 0.001) 

450 try: 

451 prec = max(1, min(8, round(2-math.log10(stderr)))) 

452 except: 

453 prec = 5 

454 self.parwids[pname].value.SetValue(("%%.%.df" % prec) % par.value) 

455 

456 def update(self): 

457 pargroup = self.feffit_panel.get_paramgroup() 

458 hashkeys = self.feffit_panel.get_pathkeys() 

459 params = group2params(pargroup) 

460 for pname, par in params.items(): 

461 if any([pname.endswith('_%s' % phash) for phash in hashkeys]): 

462 continue 

463 if pname not in self.parwids and not hasattr(par, '_is_pathparam'): 

464 pwids = ParameterWidgets(self.panel, par, name_size=100, 

465 expr_size=150, float_size=70, 

466 with_skip=True, 

467 widgets=('name', 'value', 

468 'minval', 'maxval', 

469 'vary', 'expr')) 

470 

471 self.parwids[pname] = pwids 

472 self.panel.Add(pwids.name, newrow=True) 

473 self.panel.AddMany((pwids.value, pwids.vary, pwids.bounds, 

474 pwids.minval, pwids.maxval, pwids.expr)) 

475 self.panel.pack() 

476 

477 pwids = self.parwids[pname] 

478 varstr = 'vary' if par.vary else 'fix' 

479 if par.expr is not None: 

480 varstr = 'constrain' 

481 pwids.expr.SetValue(par.expr) 

482 if getattr(par, 'skip', None) not in (False, None): 

483 varstr = 'skip' 

484 pwids.vary.SetStringSelection(varstr) 

485 if varstr != 'skip': 

486 pwids.value.SetValue(par.value) 

487 pwids.minval.SetValue(par.min) 

488 pwids.maxval.SetValue(par.max) 

489 pwids.onVaryChoice() 

490 self.panel.Update() 

491 

492 def onEditParams(self, event=None): 

493 pargroup = self.feffit_panel.get_paramgroup() 

494 self.feffit_panel.show_subframe('edit_params', EditParamsFrame, 

495 paramgroup=pargroup, 

496 feffit_panel=self.feffit_panel) 

497 

498 def RemoveParams(self, event=None, name=None): 

499 if name is None: 

500 return 

501 pargroup = self.feffit_panel.get_paramgroup() 

502 

503 if hasattr(pargroup, name): 

504 delattr(pargroup, name) 

505 if name in self.parwids: 

506 pwids = self.parwids.pop(name) 

507 pwids.name.Destroy() 

508 pwids.value.Destroy() 

509 pwids.vary.Destroy() 

510 pwids.bounds.Destroy() 

511 pwids.minval.Destroy() 

512 pwids.maxval.Destroy() 

513 pwids.expr.Destroy() 

514 pwids.remover.Destroy() 

515 

516 def generate_params(self, event=None): 

517 s = [] 

518 s.append(COMMANDS['feffit_params_init']) 

519 for name, pwids in self.parwids.items(): 

520 param = pwids.param 

521 args = [f'{param.value}'] 

522 minval = pwids.minval.GetValue() 

523 if np.isfinite(minval): 

524 args.append(f'min={minval}') 

525 maxval = pwids.maxval.GetValue() 

526 if np.isfinite(maxval): 

527 args.append(f'max={maxval}') 

528 

529 varstr = pwids.vary.GetStringSelection() 

530 if varstr == 'skip': 

531 args.append('skip=True, vary=False') 

532 elif param.expr is not None and varstr == 'constrain': 

533 args.append(f"expr='{param.expr}'") 

534 elif varstr == 'vary': 

535 args.append(f'vary=True') 

536 else: 

537 args.append(f'vary=False') 

538 args = ', '.join(args) 

539 cmd = f'_feffit_params.{name} = param({args})' 

540 s.append(cmd) 

541 return s 

542 

543 

544class FeffPathPanel(wx.Panel): 

545 """Feff Path """ 

546 def __init__(self, parent, feffit_panel, filename, title, user_label, 

547 geomstr, absorber, shell, reff, nleg, degen, 

548 par_amp, par_e0, par_delr, par_sigma2, par_third, par_ei): 

549 

550 self.parent = parent 

551 self.title = title 

552 self.user_label = fix_varname(f'{title:s}') 

553 self.feffit_panel = feffit_panel 

554 self.editing_enabled = False 

555 

556 wx.Panel.__init__(self, parent, -1, size=(550, 250)) 

557 self.SetFont(Font(FONTSIZE)) 

558 panel = GridPanel(self, ncols=4, nrows=4, pad=2, itemstyle=LEFT) 

559 

560 self.fullpath = filename 

561 par, feffdat_file = os.path.split(filename) 

562 parent_folder, dirname = os.path.split(par) 

563 

564 self.user_label = user_label 

565 

566 self.nleg = nleg 

567 self.reff = reff 

568 self.geomstr = geomstr 

569 # self.geometry = geometry 

570 

571 def SLabel(label, size=(80, -1), **kws): 

572 return SimpleText(panel, label, size=size, style=LEFT, **kws) 

573 

574 self.wids = wids = {} 

575 for name, expr in (('label', user_label), 

576 ('amp', par_amp), 

577 ('e0', par_e0), 

578 ('delr', par_delr), 

579 ('sigma2', par_sigma2), 

580 ('third', par_third), 

581 ('ei', par_ei)): 

582 self.wids[name] = wx.TextCtrl(panel, -1, size=(250, -1), 

583 value=expr, style=wx.TE_PROCESS_ENTER) 

584 wids[name+'_val'] = SimpleText(panel, '', size=(150, -1), style=LEFT) 

585 

586 wids['use'] = Check(panel, default=True, label='Use in Fit?', size=(100, -1)) 

587 wids['del'] = Button(panel, 'Remove This Path', size=(150, -1), 

588 action=self.onRemovePath) 

589 wids['plot_feffdat'] = Button(panel, 'Plot F(k)', size=(150, -1), 

590 action=self.onPlotFeffDat) 

591 

592 scatt = {2: 'Single', 3: 'Double', 4: 'Triple', 

593 5: 'Quadruple'}.get(nleg, f'{nleg-1:d}-atom') 

594 scatt = scatt + ' Scattering' 

595 

596 

597 title1 = f'{dirname:s}: {feffdat_file:s} {absorber:s} {shell:s} edge' 

598 title2 = f'Reff={reff:.4f}, Degen={degen:.1f}, {scatt:s}: {geomstr:s}' 

599 

600 panel.Add(SLabel(title1, size=(375, -1), colour='#0000AA'), 

601 dcol=2, style=wx.ALIGN_LEFT, newrow=True) 

602 panel.Add(wids['use']) 

603 panel.Add(wids['del']) 

604 panel.Add(SLabel(title2, size=(425, -1)), 

605 dcol=3, style=wx.ALIGN_LEFT, newrow=True) 

606 panel.Add(wids['plot_feffdat']) 

607 

608 panel.AddMany((SLabel('Label'), wids['label'], wids['label_val']), newrow=True) 

609 panel.AddMany((SLabel('Amplitude'), wids['amp'], wids['amp_val']), newrow=True) 

610 panel.AddMany((SLabel('E0 '), wids['e0'], wids['e0_val']), newrow=True) 

611 panel.AddMany((SLabel('Delta R'), wids['delr'], wids['delr_val']), newrow=True) 

612 panel.AddMany((SLabel('sigma2'), wids['sigma2'], wids['sigma2_val']),newrow=True) 

613 panel.AddMany((SLabel('third'), wids['third'], wids['third_val']), newrow=True) 

614 panel.AddMany((SLabel('Eimag'), wids['ei'], wids['ei_val']), newrow=True) 

615 panel.pack() 

616 sizer= wx.BoxSizer(wx.VERTICAL) 

617 sizer.Add(panel, 1, LEFT|wx.GROW|wx.ALL, 2) 

618 pack(self, sizer) 

619 

620 

621 def enable_editing(self): 

622 for name in ('label', 'amp', 'e0', 'delr', 'sigma2', 'third', 'ei'): 

623 self.wids[name].Bind(wx.EVT_TEXT_ENTER, partial(self.onExpression, name=name)) 

624 self.wids[name].Bind(wx.EVT_KILL_FOCUS, partial(self.onExpression, name=name)) 

625 self.editing_enabled = True 

626 self.wids['label'].SetValue(self.user_label) 

627 

628 def set_userlabel(self, label): 

629 self.wids['label'].SetValue(label) 

630 

631 def get_expressions(self): 

632 out = {'use': self.wids['use'].IsChecked()} 

633 for key in ('label', 'amp', 'e0', 'delr', 'sigma2', 'third', 'ei'): 

634 val = self.wids[key].GetValue().strip() 

635 if len(val) == 0: val = '0' 

636 out[key] = val 

637 return out 

638 

639 def onExpression(self, event=None, name=None): 

640 if name is None: 

641 return 

642 expr = self.wids[name].GetValue() 

643 if name == 'label': 

644 time.sleep(0.001) 

645 return 

646 

647 expr = self.wids[name].GetValue().strip() 

648 if len(expr) < 1: 

649 return 

650 opts= dict(value=1.e-3, minval=None, maxval=None) 

651 if name == 'sigma2': 

652 opts['minval'] = 0 

653 opts['maxval'] = 1 

654 opts['value'] = np.sqrt(self.reff)/200.0 

655 elif name == 'delr': 

656 opts['minval'] = -0.75 

657 opts['maxval'] = 0.75 

658 elif name == 'amp': 

659 opts['value'] = 1 

660 result = self.feffit_panel.update_params_for_expr(expr, **opts) 

661 if result: 

662 pargroup = self.feffit_panel.get_paramgroup() 

663 _eval = pargroup.__params__._asteval 

664 try: 

665 value = _eval.eval(expr, show_errors=False, raise_errors=False) 

666 if value is not None: 

667 value = gformat(value, 11) 

668 self.wids[name + '_val'].SetLabel(f'= {value}') 

669 except: 

670 result = False 

671 

672 if result: 

673 bgcol, fgcol = 'white', 'black' 

674 else: 

675 bgcol, fgcol = '#AAAA4488', '#AA0000' 

676 self.wids[name].SetForegroundColour(fgcol) 

677 self.wids[name].SetBackgroundColour(bgcol) 

678 self.wids[name].SetOwnBackgroundColour(bgcol) 

679 if event is not None: 

680 event.Skip() 

681 

682 

683 def onPlotFeffDat(self, event=None): 

684 cmd = f"plot_feffdat(_feffpaths['{self.title}'], title='Feff data for path {self.title}')" 

685 self.feffit_panel.larch_eval(cmd) 

686 

687 def onRemovePath(self, event=None): 

688 msg = f"Delete Path {self.title:s}?" 

689 dlg = wx.MessageDialog(self, msg, 'Warning', wx.YES | wx.NO ) 

690 if (wx.ID_YES == dlg.ShowModal()): 

691 self.feffit_panel.paths_data.pop(self.title) 

692 self.feffit_panel.model_needs_build = True 

693 path_nb = self.feffit_panel.paths_nb 

694 for i in range(path_nb.GetPageCount()): 

695 if self.title == path_nb.GetPageText(i).strip(): 

696 path_nb.DeletePage(i) 

697 self.feffit_panel.skip_unused_params() 

698 dlg.Destroy() 

699 

700 def update_values(self): 

701 pargroup = self.feffit_panel.get_paramgroup() 

702 _eval = pargroup.__params__._asteval 

703 for par in ('amp', 'e0', 'delr', 'sigma2', 'third', 'ei'): 

704 expr = self.wids[par].GetValue().strip() 

705 if len(expr) > 0: 

706 try: 

707 value = _eval.eval(expr, show_errors=False, raise_errors=False) 

708 if value is not None: 

709 value = gformat(value, 10) 

710 self.wids[par + '_val'].SetLabel(f'= {value}') 

711 except: 

712 self.feffit_panel.update_params_for_expr(expr) 

713 

714 

715class FeffitPanel(TaskPanel): 

716 def __init__(self, parent=None, controller=None, **kws): 

717 TaskPanel.__init__(self, parent, controller, panel='feffit', **kws) 

718 self.paths_data = {} 

719 self.resetting = False 

720 self.model_needs_rebuild = False 

721 self.config_saved = self.get_defaultconfig() 

722 self.dgroup = None 

723 

724 def onPanelExposed(self, **kws): 

725 # called when notebook is selected 

726 dgroup = self.controller.get_group() 

727 try: 

728 pargroup = self.get_paramgroup() 

729 self.params_panel.update() 

730 fname = self.controller.filelist.GetStringSelection() 

731 gname = self.controller.file_groups[fname] 

732 dgroup = self.controller.get_group(gname) 

733 if not hasattr(dgroup, 'chi'): 

734 self.xasmain.process_exafs(dgroup) 

735 self.fill_form(dgroup) 

736 except: 

737 pass # print(" Cannot Fill feffit panel from group ") 

738 self.dgroup = dgroup 

739 feffpaths = getattr(self.larch.symtable, '_feffpaths', None) 

740 

741 

742 try: 

743 has_fit_hist = len(dgroup.feffit_history) > 0 

744 except: 

745 has_fit_hist = False 

746 

747 

748 if not has_fit_hist: 

749 has_fit_hist = getattr(self.larch.symtable, '_feffit_dataset', None) is not None 

750 

751 if has_fit_hist: 

752 self.wids['show_results'].Enable() 

753 if feffpath is not None: 

754 self.reset_paths() 

755 

756 def build_display(self): 

757 self.paths_nb = flatnotebook(self, {}, on_change=self.onPathsNBChanged, 

758 with_dropdown=True) 

759 

760 self.params_panel = FeffitParamsPanel(parent=self.paths_nb, 

761 feffit_panel=self) 

762 self.paths_nb.AddPage(self.params_panel, ' Parameters ', True) 

763 pan = self.panel # = GridPanel(self, ncols=4, nrows=4, pad=2, itemstyle=LEFT) 

764 

765 self.wids = wids = {} 

766 

767 fsopts = dict(digits=2, increment=0.1, with_pin=True) 

768 

769 fit_kmin = self.add_floatspin('fit_kmin', value=2, **fsopts) 

770 fit_kmax = self.add_floatspin('fit_kmax', value=17, **fsopts) 

771 fit_dk = self.add_floatspin('fit_dk', value=4, **fsopts) 

772 fit_rmin = self.add_floatspin('fit_rmin', value=1, **fsopts) 

773 fit_rmax = self.add_floatspin('fit_rmax', value=5, **fsopts) 

774 

775 wids['fit_kwstring'] = Choice(pan, size=(150, -1), 

776 choices=list(Feffit_KWChoices.keys())) 

777 wids['fit_kwstring'].SetSelection(1) 

778 

779 wids['fit_kwindow'] = Choice(pan, choices=list(FT_WINDOWS), size=(150, -1)) 

780 

781 wids['fit_space'] = Choice(pan, choices=list(Feffit_SpaceChoices.keys()), 

782 size=(150, -1)) 

783 

784 wids['plotone_op'] = Choice(pan, choices=list(PlotOne_Choices.keys()), 

785 action=self.onPlot, size=(150, -1)) 

786 wids['plotone_op'].SetSelection(1) 

787 wids['plotalt_op'] = Choice(pan, choices=list(PlotAlt_Choices.keys()), 

788 action=self.onPlot, size=(150, -1)) 

789 

790 wids['plot_win'] = Choice(pan, choices=PlotWindowChoices, 

791 action=self.onPlot, size=(60, -1)) 

792 wids['plot_win'].SetStringSelection('2') 

793 

794 wids['plot_voffset'] = FloatSpin(pan, value=0, digits=2, increment=0.25, 

795 size=(100, -1), action=self.onPlot) 

796 

797 

798 ppanel = wx.Panel(pan) 

799 ppanel.SetMinSize((450, 20)) 

800 

801 wids['plot_paths'] = Check(ppanel, default=False, label='Plot Each Path', 

802 action=self.onPlot) 

803 wids['plot_ftwindows'] = Check(ppanel, default=False, label='Plot FT Windows', 

804 action=self.onPlot) 

805 

806 psizer = wx.BoxSizer(wx.HORIZONTAL) 

807 psizer.Add(wids['plot_paths'], 0, LEFT, 2) 

808 psizer.Add(wids['plot_ftwindows'], 0, LEFT, 2) 

809 #psizer.Add(SimpleText(ppanel, ' Offset ', size=(100, -1) ), 0, LEFT, 2) 

810 #psizer.Add(wids['plot_voffset'], 0, LEFT, 2) 

811 pack(ppanel, psizer) 

812 wids['plot_current'] = Button(pan,'Plot Current Model', 

813 action=self.onPlot, size=(175, -1)) 

814 wids['do_fit'] = Button(pan, 'Fit Data to Model', 

815 action=self.onFitModel, size=(175, -1)) 

816 wids['show_results'] = Button(pan, 'Show Fit Results', 

817 action=self.onShowResults, size=(175, -1)) 

818 wids['show_results'].Disable() 

819 

820# wids['do_fit_sel']= Button(pan, 'Fit Selected Groups', 

821# action=self.onFitSelected, size=(125, -1)) 

822# wids['do_fit_sel'].Disable() 

823 def add_text(text, dcol=1, newrow=True): 

824 pan.Add(SimpleText(pan, text), dcol=dcol, newrow=newrow) 

825 

826 pan.Add(SimpleText(pan, 'Feff Fitting', 

827 size=(150, -1), **self.titleopts), style=LEFT, dcol=1, newrow=True) 

828 pan.Add(SimpleText(pan, 'To add paths, use Feff->Browse Feff Calculations', 

829 size=(350, -1)), style=LEFT, dcol=3) 

830 

831 add_text('Fitting Space: ') 

832 pan.Add(wids['fit_space']) 

833 

834 add_text('k weightings: ', newrow=False) 

835 pan.Add(wids['fit_kwstring']) 

836 

837 add_text('k min: ') 

838 pan.Add(fit_kmin) 

839 add_text(' k max: ', newrow=False) 

840 pan.Add(fit_kmax) 

841 

842 add_text('k Window: ') 

843 pan.Add(wids['fit_kwindow']) 

844 add_text('dk: ', newrow=False) 

845 pan.Add(fit_dk) 

846 

847 add_text('R min: ') 

848 pan.Add(fit_rmin) 

849 add_text('R max: ', newrow=False) 

850 pan.Add(fit_rmax) 

851 

852 pan.Add(HLine(pan, size=(600, 2)), dcol=6, newrow=True) 

853 

854 pan.Add(wids['plot_current'], dcol=1, newrow=True) 

855 pan.Add(wids['plotone_op'], dcol=1) 

856 pan.Add(ppanel, dcol=4) 

857 add_text(' ', dcol=2, newrow=True) 

858 add_text('Vertical Offset' , newrow=False) 

859 pan.Add(wids['plot_voffset']) 

860 

861 add_text('Second Plot: ', newrow=True) 

862 pan.Add(wids['plotalt_op'], dcol=1) 

863 add_text('Plot Window: ', newrow=False) 

864 pan.Add(wids['plot_win'], dcol=1) 

865 

866 pan.Add(wids['do_fit'], dcol=3, newrow=True) 

867 pan.Add(wids['show_results']) 

868 pan.Add((5, 5), newrow=True) 

869 

870 pan.Add(HLine(pan, size=(600, 2)), dcol=6, newrow=True) 

871 pan.pack() 

872 

873 sizer = wx.BoxSizer(wx.VERTICAL) 

874 sizer.Add(pan, 0, LEFT, 3) 

875 sizer.Add((10, 10), 0, LEFT, 3) 

876 sizer.Add(self.paths_nb, 1, LEFT|wx.GROW, 5) 

877 pack(self, sizer) 

878 

879 def onPathsNBChanged(self, event=None): 

880 updater = getattr(self.paths_nb.GetCurrentPage(), 'update_values', None) 

881 if callable(updater) and not self.resetting: 

882 updater() 

883 

884 def get_config(self, dgroup=None): 

885 """get and set processing configuration for a group""" 

886 if dgroup is None: 

887 dgroup = self.controller.get_group() 

888 if dgroup is None: 

889 conf = None 

890 if not hasattr(dgroup, 'chi'): 

891 self.xasmain.process_exafs(dgroup) 

892 

893 # print("Get Config ", dgroup, self.configname, hasattr(dgroup, 'config')) 

894 

895 dconf = self.get_defaultconfig() 

896 if dgroup is None: 

897 return dconf 

898 if not hasattr(dgroup, 'config'): 

899 dgroup.config = Group() 

900 

901 conf = getattr(dgroup.config, self.configname, dconf) 

902 for k, v in dconf.items(): 

903 if k not in conf: 

904 conf[k] = v 

905 

906 econf = getattr(dgroup.config, 'exafs', {}) 

907 for key in ('fit_kmin', 'fit_kmax', 'fit_dk', 

908 'fit_rmin', 'fit_rmax', 'fit_dr' 

909 'fit_kwindow', 'fit_rwindow'): 

910 alt = key.replace('fit', 'fft') 

911 val = conf.get(key, -1) 

912 if val in (None, -1, 'Auto') and alt in econf: 

913 conf[key] = econf[alt] 

914 

915 setattr(dgroup.config, self.configname, conf) 

916 self.config_saved = conf 

917 return conf 

918 

919 

920 def process(self, dgroup=None, **kws): 

921 if dgroup is None: 

922 dgroup = self.controller.get_group() 

923 

924 conf = self.get_config(dgroup=dgroup) 

925 conf.update(kws) 

926 if dgroup is None: 

927 return conf 

928 

929 self.dgroup = dgroup 

930 opts = self.read_form(dgroup=dgroup) 

931 

932 for attr in ('fit_kmin', 'fit_kmax', 'fit_dk', 'fit_rmin', 

933 'fit_rmax', 'fit_kwindow', 'fit_rwindow', 

934 'fit_dr', 'fit_kwstring', 'fit_space', 

935 'fit_plot', 'plot_paths'): 

936 

937 conf[attr] = opts.get(attr, None) 

938 

939 if not hasattr(dgroup, 'config'): 

940 dgroup.config = Group() 

941 setattr(dgroup.config, self.configname, conf) 

942 

943 def fill_form(self, dat): 

944 dgroup = self.controller.get_group() 

945 conf = self.get_config(dat) 

946 for attr in ('fit_kmin', 'fit_kmax', 'fit_rmin', 'fit_rmax', 'fit_dk'): 

947 self.wids[attr].SetValue(conf[attr]) 

948 

949 self.wids['fit_kwindow'].SetStringSelection(conf['fit_kwindow']) 

950 

951 fit_space = conf.get('fit_space', 'r') 

952 

953 for key, val in Feffit_SpaceChoices.items(): 

954 if fit_space in (key, val): 

955 self.wids['fit_space'].SetStringSelection(key) 

956 

957 for key, val in Feffit_KWChoices.items(): 

958 if conf['fit_kwstring'] == val: 

959 self.wids['fit_kwstring'].SetStringSelection(key) 

960 

961 def read_form(self, dgroup=None): 

962 "read form, returning dict of values" 

963 

964 if dgroup is None: 

965 try: 

966 fname = self.controller.filelist.GetStringSelection() 

967 gname = self.controller.file_groups[fname] 

968 dgroup = self.controller.get_group() 

969 except: 

970 gname = fname = dgroup = None 

971 else: 

972 

973 gname = dgroup.groupname 

974 fname = dgroup.filename 

975 

976 form_opts = {'datagroup': dgroup, 'groupname': gname, 'filename': fname} 

977 wids = self.wids 

978 

979 for attr in ('fit_kmin', 'fit_kmax', 'fit_rmin', 'fit_rmax', 'fit_dk'): 

980 form_opts[attr] = wids[attr].GetValue() 

981 form_opts['fit_kwstring'] = Feffit_KWChoices[wids['fit_kwstring'].GetStringSelection()] 

982 if len(form_opts['fit_kwstring']) == 1: 

983 d = form_opts['fit_kwstring'] 

984 else: 

985 d = form_opts['fit_kwstring'].replace('[', '').strip(',').split()[0] 

986 try: 

987 form_opts['fit_kweight'] = int(d) 

988 except: 

989 form_opts['fit_kweight'] = 2 

990 

991 

992 form_opts['fit_space'] = Feffit_SpaceChoices[wids['fit_space'].GetStringSelection()] 

993 

994 form_opts['fit_kwindow'] = wids['fit_kwindow'].GetStringSelection() 

995 form_opts['plot_ftwindows'] = wids['plot_ftwindows'].IsChecked() 

996 form_opts['plot_paths'] = wids['plot_paths'].IsChecked() 

997 form_opts['plotone_op'] = PlotOne_Choices[wids['plotone_op'].GetStringSelection()] 

998 form_opts['plotalt_op'] = PlotAlt_Choices[wids['plotalt_op'].GetStringSelection()] 

999 form_opts['plot_voffset'] = wids['plot_voffset'].GetValue() 

1000 form_opts['plot_win'] = int(wids['plot_win'].GetStringSelection()) 

1001 

1002 return form_opts 

1003 

1004 

1005 def fill_model_params(self, prefix, params): 

1006 comp = self.fit_components[prefix] 

1007 parwids = comp.parwids 

1008 for pname, par in params.items(): 

1009 pname = prefix + pname 

1010 if pname in parwids: 

1011 wids = parwids[pname] 

1012 if wids.minval is not None: 

1013 wids.minval.SetValue(par.min) 

1014 if wids.maxval is not None: 

1015 wids.maxval.SetValue(par.max) 

1016 wids.value.SetValue(par.value) 

1017 varstr = 'vary' if par.vary else 'fix' 

1018 if par.expr is not None: 

1019 varstr = 'constrain' 

1020 if wids.vary is not None: 

1021 wids.vary.SetStringSelection(varstr) 

1022 

1023 def onPlot(self, evt=None, dgroup=None, pargroup_name='_feffit_params', 

1024 paths_name='_feffpaths', pathsum_name='_pathsum', title=None, 

1025 dataset_name=None, build_fitmodel=True, topwin=None, **kws): 

1026 

1027 self.process(dgroup) 

1028 opts = self.read_form(dgroup=dgroup) 

1029 opts.update(**kws) 

1030 fname = opts['filename'] 

1031 if title is None: 

1032 title = fname 

1033 if title is None: 

1034 title = 'Feff Sum' 

1035 if "'" in title: 

1036 title = title.replace("'", "\\'") 

1037 

1038 gname = opts['groupname'] 

1039 if dataset_name is None: 

1040 dataset_name = gname 

1041 

1042 if dgroup is None: 

1043 dgroup = opts['datagroup'] 

1044 

1045 exafs_conf = self.xasmain.get_nbpage('exafs')[1].read_form() 

1046 plot_rmax = exafs_conf['plot_rmax'] 

1047 

1048 if build_fitmodel: 

1049 self.build_fitmodel(dgroup) 

1050 

1051 try: 

1052 pathsum = self._plain_larch_eval(pathsum_name) 

1053 except: 

1054 pathsum = None 

1055 

1056 try: 

1057 paths = self._plain_larch_eval(paths_name) 

1058 except: 

1059 paths = {} 

1060 

1061 plot1 = opts['plotone_op'] 

1062 plot2 = opts['plotalt_op'] 

1063 cmds = [] 

1064 

1065 kw = opts['fit_kweight'] 

1066 

1067 ftargs = dict(kmin=opts['fit_kmin'], kmax=opts['fit_kmax'], dk=opts['fit_dk'], 

1068 kwindow=opts['fit_kwindow'], kweight=opts['fit_kweight'], 

1069 rmin=opts['fit_rmin'], rmax=opts['fit_rmax'], 

1070 dr=opts.get('fit_dr', 0.1), rwindow='hanning') 

1071 

1072 if pathsum is not None: 

1073 cmds.append(COMMANDS['xft'].format(groupname=pathsum_name, **ftargs)) 

1074 if dataset_name is not None: 

1075 cmds.append(COMMANDS['xft'].format(groupname=dataset_name, **ftargs)) 

1076 if dgroup is not None: 

1077 cmds.append(COMMANDS['xft'].format(groupname=gname, **ftargs)) 

1078 if opts['plot_paths']: 

1079 cmds.append(COMMANDS['path2chi'].format(paths_name=paths_name, 

1080 pargroup_name=pargroup_name, 

1081 **ftargs)) 

1082 

1083 self.larch_eval('\n'.join(cmds)) 

1084 with_win = opts['plot_ftwindows'] 

1085 needs_qspace = False 

1086 cmds = [] 

1087 for i, plot in enumerate((plot1, plot2)): 

1088 if plot in PlotAlt_Choices: 

1089 plot = PlotAlt_Choices[plot] 

1090 

1091 if plot in ('noplot', '<no plot>'): 

1092 continue 

1093 plotwin = 1 

1094 if i > 0: 

1095 plotwin = int(opts.get('plot_win', '2')) 

1096 pcmd = 'plot_chir' 

1097 pextra = f', win={plotwin:d}' 

1098 if plot == 'chi': 

1099 pcmd = 'plot_chik' 

1100 pextra += f', kweight={kw:d}' 

1101 elif plot == 'chir_mag': 

1102 pcmd = 'plot_chir' 

1103 pextra += f', rmax={plot_rmax}' 

1104 elif plot == 'chir_re': 

1105 pextra += f', show_mag=False, show_real=True, rmax={plot_rmax}' 

1106 elif plot == 'chir_mag+chir_re': 

1107 pextra += f', show_mag=True, show_real=True, rmax={plot_rmax}' 

1108 elif plot == 'chiq': 

1109 pcmd = 'plot_chiq' 

1110 pextra += f', show_chik=False' 

1111 needs_qspace = True 

1112 else: 

1113 print(" do not know how to plot ", plot) 

1114 continue 

1115 

1116 newplot = f', show_window={with_win}, new=True' 

1117 overplot = f', show_window=False, new=False' 

1118 if dgroup is not None: 

1119 cmds.append(f"{pcmd}({dataset_name:s}, label='data'{pextra}, title='{title}'{newplot})") 

1120 if pathsum is not None: 

1121 cmds.append(f"{pcmd}({pathsum_name:s}, label='model'{pextra}{overplot})") 

1122 elif pathsum is not None: 

1123 cmds.append(f"{pcmd}({pathsum_name:s}, label='Path sum'{pextra}, title='sum of paths'{newplot})") 

1124 if opts['plot_paths']: 

1125 voff = opts['plot_voffset'] 

1126 

1127 for i, label in enumerate(paths.keys()): 

1128 if paths[label].use: 

1129 

1130 objname = f"{paths_name}['{label:s}']" 

1131 if needs_qspace: 

1132 xpath = paths.get(label) 

1133 if not hasattr(xpath, 'chiq_re'): 

1134 cmds.append(COMMANDS['xft'].format(groupname=objname, **ftargs)) 

1135 

1136 cmds.append(f"{pcmd}({objname}, label='{label:s}'{pextra}, offset={(i+1)*voff}{overplot})") 

1137 

1138 self.larch_eval('\n'.join(cmds)) 

1139 self.controller.set_focus(topwin=topwin) 

1140 

1141 

1142 def reset_paths(self, event=None): 

1143 "reset paths from _feffpaths" 

1144 self.resetting = True 

1145 def get_pagenames(): 

1146 allpages = [] 

1147 for i in range(self.paths_nb.GetPageCount()): 

1148 allpages.append(self.paths_nb.GetPage(i).__class__.__name__) 

1149 return allpages 

1150 

1151 allpages = get_pagenames() 

1152 t0 = time.time() 

1153 

1154 while 'FeffPathPanel' in allpages: 

1155 for i in range(self.paths_nb.GetPageCount()): 

1156 nbpage = self.paths_nb.GetPage(i) 

1157 if isinstance(nbpage, FeffPathPanel): 

1158 key = self.paths_nb.GetPageText(i) 

1159 self.paths_nb.DeletePage(i) 

1160 allpages = get_pagenames() 

1161 

1162 time.sleep(0.1) 

1163 

1164 self.resetting = False 

1165 feffpaths = deepcopy(getattr(self.larch.symtable, '_feffpaths', {})) 

1166 self.paths_data = {} 

1167 for path in feffpaths.values(): 

1168 self.add_path(path.filename, feffpath=path) 

1169 self.get_pathpage('parameters').Rebuild() 

1170 

1171 

1172 def add_path(self, filename, pathinfo=None, feffpath=None): 

1173 """ add new path to cache """ 

1174 

1175 if pathinfo is None and feffpath is None: 

1176 raise ValueError("add_path needs a Feff Path or Path information") 

1177 

1178 parent, fname = os.path.split(filename) 

1179 parent, feffrun = os.path.split(parent) 

1180 

1181 feffcache = getattr(self.larch.symtable, '_feffcache', None) 

1182 if feffcache is None: 

1183 self.larch_eval(COMMANDS['paths_init']) 

1184 feffcache = getattr(self.larch.symtable, '_feffcache', None) 

1185 if feffcache is None: 

1186 raise ValueError("cannot get feff cache ") 

1187 

1188 geomstre = None 

1189 if pathinfo is not None: 

1190 absorber = pathinfo.absorber 

1191 shell = pathinfo.shell 

1192 reff = float(pathinfo.reff) 

1193 nleg = int(pathinfo.nleg) 

1194 degen = float(pathinfo.degen) 

1195 if hasattr(pathinfo, 'atoms'): 

1196 geom = pathinfo.atoms 

1197 geomstr = pathinfo.geom # '[Fe] > O > [Fe]' 

1198 par_amp = par_e0 = par_delr = par_sigma2 = par_third = par_ei = '' 

1199 

1200 if feffpath is not None: 

1201 absorber = feffpath.absorber 

1202 shell = feffpath.shell 

1203 reff = feffpath.reff 

1204 nleg = feffpath.nleg 

1205 degen = float(feffpath.degen) 

1206 geomstr = [] 

1207 for gdat in feffpath.geom: # ('Fe', 26, 0, 55.845, x, y, z) 

1208 w = gdat[0] 

1209 if gdat[2] == 0: # absorber 

1210 w = '[%s]' % w 

1211 geomstr.append(w) 

1212 geomstr.append(geomstr[0]) 

1213 geomstr = ' > '.join(geomstr) 

1214 par_amp = feffpath.s02 

1215 par_e0 = feffpath.e0 

1216 par_delr = feffpath.deltar 

1217 par_sigma2 = feffpath.sigma2 

1218 par_third = feffpath.third 

1219 par_ei = feffpath.ei 

1220 

1221 try: 

1222 atoms = [s.strip() for s in geomstr.split('>')] 

1223 atoms.pop() 

1224 except: 

1225 title = "Cannot interpret Feff Path data" 

1226 message = [f"Cannot interpret Feff path {filename}"] 

1227 ExceptionPopup(self, title, message) 

1228 

1229 title = '_'.join(atoms) + "%d" % (round(100*reff)) 

1230 for c in ',.[](){}<>+=-?/\\&%$#@!|:;"\'': 

1231 title = title.replace(c, '') 

1232 if title in self.paths_data: 

1233 btitle = title 

1234 i = -1 

1235 while title in self.paths_data: 

1236 i += 1 

1237 title = btitle + '_%s' % string.ascii_lowercase[i] 

1238 

1239 user_label = fix_varname(title) 

1240 self.paths_data[title] = filename 

1241 

1242 ptitle = title 

1243 if ptitle.startswith(absorber): 

1244 ptitle = ptitle[len(absorber):] 

1245 if ptitle.startswith('_'): 

1246 ptitle = ptitle[1:] 

1247 

1248 # set default Path parameters if not supplied already 

1249 if len(par_amp) < 1: 

1250 par_amp = f'{degen:.1f} * s02' 

1251 if len(par_e0) < 1: 

1252 par_e0 = 'e0' 

1253 if len(par_delr) < 1: 

1254 par_delr = f'delr_{ptitle}' 

1255 if len(par_sigma2) < 1: 

1256 par_sigma2 = f'sigma2_{ptitle}' 

1257 

1258 pathpanel = FeffPathPanel(self.paths_nb, self, filename, title, 

1259 user_label, geomstr, absorber, shell, 

1260 reff, nleg, degen, par_amp, par_e0, 

1261 par_delr, par_sigma2, par_third, par_ei) 

1262 

1263 self.paths_nb.AddPage(pathpanel, f' {title:s} ', True) 

1264 

1265 for pname in ('amp', 'e0', 'delr', 'sigma2', 'third', 'ei'): 

1266 pathpanel.onExpression(name=pname) 

1267 

1268 pathpanel.enable_editing() 

1269 

1270 pdat = {'title': title, 'fullpath': filename, 

1271 'feffrun': feffrun, 'use':True} 

1272 pdat.update(pathpanel.get_expressions()) 

1273 

1274 if title not in feffcache['paths']: 

1275 if os.path.exists(filename): 

1276 self.larch_eval(COMMANDS['cache_path'].format(**pdat)) 

1277 else: 

1278 print(f"cannot file Feff data file '{filename}'") 

1279 

1280 self.larch_eval(COMMANDS['use_path'].format(**pdat)) 

1281 

1282 sx,sy = self.GetSize() 

1283 self.SetSize((sx, sy+1)) 

1284 self.SetSize((sx, sy)) 

1285 ipage, pagepanel = self.xasmain.get_nbpage('feffit') 

1286 self.xasmain.nb.SetSelection(ipage) 

1287 self.xasmain.Raise() 

1288 

1289 def get_pathkeys(self): 

1290 _feffpaths = getattr(self.larch.symtable, '_feffpaths', {}) 

1291 return [p.hashkey for p in _feffpaths.values()] 

1292 

1293 def get_paramgroup(self): 

1294 pgroup = getattr(self.larch.symtable, '_feffit_params', None) 

1295 if pgroup is None: 

1296 self.larch_eval(COMMANDS['feffit_params_init']) 

1297 pgroup = getattr(self.larch.symtable, '_feffit_params', None) 

1298 if not hasattr(self.larch.symtable, '_feffpaths'): 

1299 self.larch_eval(COMMANDS['paths_init']) 

1300 return pgroup 

1301 

1302 def update_params_for_expr(self, expr=None, value=1.e-3, 

1303 minval=None, maxval=None): 

1304 if expr is None: 

1305 return 

1306 pargroup = self.get_paramgroup() 

1307 symtable = pargroup.__params__._asteval.symtable 

1308 extras= '' 

1309 if minval is not None: 

1310 extras = f', min={minval}' 

1311 if maxval is not None: 

1312 extras = f'{extras}, max={maxval}' 

1313 

1314 try: 

1315 for node in ast.walk(ast.parse(expr)): 

1316 if isinstance(node, ast.Name): 

1317 sym = node.id 

1318 if sym not in symtable and sym not in FEFFDAT_VALUES: 

1319 s = f"_feffit_params.{sym:s} = param({value:.4f}, name='{sym:s}', vary=True{extras:s})" 

1320 self.larch_eval(s) 

1321 result = True 

1322 except: 

1323 result = False 

1324 

1325 self.params_panel.update() 

1326 wx.CallAfter(self.skip_unused_params) 

1327 return result 

1328 

1329 def onLoadFitResult(self, event=None): 

1330 dlg = wx.FileDialog(self, message="Load Saved Feffit Model", 

1331 wildcard=ModelWcards, style=wx.FD_OPEN) 

1332 rfile = None 

1333 if dlg.ShowModal() == wx.ID_OK: 

1334 rfile = dlg.GetPath() 

1335 dlg.Destroy() 

1336 

1337 if rfile is None: 

1338 return 

1339 

1340 

1341 def get_xranges(self, x): 

1342 if self.dgroup is None: 

1343 self.dgroup = self.controller.get_group() 

1344 self.process(self.dgroup) 

1345 opts = self.read_form(self.dgroup) 

1346 dgroup = self.controller.get_group() 

1347 en_eps = min(np.diff(dgroup.energy)) / 5. 

1348 

1349 i1 = index_of(x, opts['emin'] + en_eps) 

1350 i2 = index_of(x, opts['emax'] + en_eps) + 1 

1351 return i1, i2 

1352 

1353 def get_pathpage(self, name): 

1354 "get nb page for a Path by name" 

1355 name = name.lower().strip() 

1356 for i in range(self.paths_nb.GetPageCount()): 

1357 text = self.paths_nb.GetPageText(i).strip().lower() 

1358 if name in text: 

1359 return self.paths_nb.GetPage(i) 

1360 

1361 def build_fitmodel(self, groupname=None): 

1362 """ use fit components to build model""" 

1363 paths = [] 

1364 cmds = ["### set up feffit "] 

1365 pargroup = self.get_paramgroup() 

1366 if self.dgroup is None: 

1367 self.dgroup = self.controller.get_group() 

1368 

1369 cmds.extend(self.params_panel.generate_params()) 

1370 

1371 self.process(self.dgroup) 

1372 opts = self.read_form(self.dgroup) 

1373 

1374 cmds.append(COMMANDS['feffit_trans'].format(**opts)) 

1375 

1376 path_pages = {} 

1377 for i in range(self.paths_nb.GetPageCount()): 

1378 text = self.paths_nb.GetPageText(i).strip() 

1379 path_pages[text] = self.paths_nb.GetPage(i) 

1380 

1381 _feffpaths = getattr(self.larch.symtable, '_feffpaths', None) 

1382 if _feffpaths is None: 

1383 cmds.append(COMMANDS['paths_init']) 

1384 else: 

1385 cmds.append(COMMANDS['paths_reset']) 

1386 

1387 paths_list = [] 

1388 opts['paths'] = [] 

1389 for title, pathdata in self.paths_data.items(): 

1390 if title not in path_pages: 

1391 continue 

1392 pdat = {'title': title, 'fullpath': pathdata[0], 

1393 'feffrun': pathdata[1], 'use':True} 

1394 pdat.update(path_pages[title].get_expressions()) 

1395 

1396 #if pdat['use']: 

1397 cmds.append(COMMANDS['use_path'].format(**pdat)) 

1398 paths_list.append(f"_feffpaths['{title:s}']") 

1399 opts['paths'].append(pdat) 

1400 

1401 paths_string = '[%s]' % (', '.join(paths_list)) 

1402 cmds.append(COMMANDS['ff2chi'].format(paths=paths_string)) 

1403 self.larch_eval("\n".join(cmds)) 

1404 return opts 

1405 

1406 

1407 def get_used_params(self): 

1408 used_syms = [] 

1409 path_pages = {} 

1410 for i in range(self.paths_nb.GetPageCount()): 

1411 text = self.paths_nb.GetPageText(i).strip() 

1412 path_pages[text] = self.paths_nb.GetPage(i) 

1413 for title in self.paths_data: 

1414 if title not in path_pages: 

1415 continue 

1416 exprs = path_pages[title].get_expressions() 

1417 if exprs['use']: 

1418 for ename, expr in exprs.items(): 

1419 if ename in ('label', 'use'): 

1420 continue 

1421 for node in ast.walk(ast.parse(expr)): 

1422 if isinstance(node, ast.Name): 

1423 sym = node.id 

1424 if sym not in used_syms: 

1425 used_syms.append(sym) 

1426 return used_syms 

1427 

1428 

1429 def skip_unused_params(self): 

1430 # find unused symbols, set to "skip" 

1431 curr_syms = self.get_used_params() 

1432 pargroup = self.get_paramgroup() 

1433 parpanel = self.params_panel 

1434 # print(group2params(pargroup).keys()) 

1435 for pname, par in group2params(pargroup).items(): 

1436 if pname not in curr_syms and pname in parpanel.parwids: 

1437 par.skip = parpanel.parwids[pname].param.skip = True 

1438 parpanel.parwids[pname].vary.SetStringSelection('skip') 

1439 parpanel.parwids[pname].onVaryChoice() 

1440 

1441 elif (pname in curr_syms and pname in parpanel.parwids 

1442 and parpanel.parwids[pname].param.skip): 

1443 par.skip = parpanel.parwids[pname].param.skip = False 

1444 parpanel.parwids[pname].vary.SetStringSelection('vary') 

1445 parpanel.parwids[pname].onVaryChoice() 

1446 parpanel.update() 

1447 

1448 def onFitModel(self, event=None, dgroup=None): 

1449 session_history = self.get_session_history() 

1450 nstart = len(session_history) 

1451 

1452 script = [COMMANDS['feffit_top'].format(ctime=time.ctime())] 

1453 

1454 if dgroup is None: 

1455 dgroup = self.controller.get_group() 

1456 opts = self.build_fitmodel(dgroup) 

1457 

1458 # dgroup = opts['datagroup'] 

1459 fopts = dict(groupname=opts['groupname'], 

1460 trans='_feffit_trans', 

1461 paths='_feffpaths', 

1462 params='_feffit_params') 

1463 

1464 groupname = opts['groupname'] 

1465 filename = opts['filename'] 

1466 if dgroup is None: 

1467 dgroup = opts['datagroup'] 

1468 

1469 

1470 script.append("######################################") 

1471 script.append(COMMANDS['data_source'].format(groupname=groupname, filename=filename)) 

1472 for cmd in session_history: 

1473 if groupname in cmd or filename in cmd or 'athena' in cmd or 'session' in cmd: 

1474 for cline in cmd.split('\n'): 

1475 script.append(f"# {cline}") 

1476 

1477 script.append("#### end of data reading and preparation") 

1478 script.append("######################################") 

1479 script.append("## read Feff Paths into '_feffpaths'. You will need to either") 

1480 script.append("## read feff.dat from disk files with `feffpath()` or use Paths") 

1481 script.append("## cached from a session file into `feffcache`") 

1482 script.append("#_feffcache = {'runs': {}, 'paths':{}}") 

1483 script.append("#_feffpaths = {}") 

1484 for path in opts['paths']: 

1485 lab, fname, run = path['title'], path['fullpath'], path['feffrun'] 

1486 amp, e0, delr, sigma2, third, ei = path['amp'], path['e0'], path['delr'], path['sigma2'], path['third'], path['ei'] 

1487 script.append(f"""## Path '{lab}' : ############ 

1488#_feffcache['paths']['{lab}'] = feffpath('{fname}', 

1489# label='{lab}', feffrun='{run}', degen=1) 

1490#_feffpaths['{lab}'] = use_feffpath(_feffcache['paths'], '{lab}', 

1491# s02='{amp:s}', e0='{e0:s}', deltar='{delr:s}', 

1492# sigma2='{sigma2:s}', third='{third:s}', ei='{ei:s}')""") 

1493 

1494 script.append("######################################") 

1495 self.larch_eval(COMMANDS['do_feffit'].format(**fopts)) 

1496 

1497 

1498 self.wids['show_results'].Enable() 

1499 self.onPlot(dgroup=opts['datagroup'], build_fitmodel=False, 

1500 pargroup_name='_feffit_result.paramgroup', 

1501 paths_name='_feffit_dataset.paths', 

1502 pathsum_name='_feffit_dataset.model') 

1503 

1504 script.extend(self.get_session_history()[nstart:]) 

1505 script.extend(["print(feffit_report(_feffit_result))", 

1506 "#end of autosaved feffit script" , ""]) 

1507 

1508 if not hasattr(dgroup, 'feffit_history'): 

1509 dgroup.feffit_history = [] 

1510 

1511 

1512 label = now = time.strftime("%b-%d %H:%M") 

1513 dgroup.feffit_history[0].commands = script 

1514 dgroup.feffit_history[0].timestamp = time.strftime("%Y-%b-%d %H:%M") 

1515 dgroup.feffit_history[0].label = label 

1516 

1517 fitlabels = [fhist.label for fhist in dgroup.feffit_history[1:]] 

1518 if label in fitlabels: 

1519 count = 1 

1520 while label in fitlabels: 

1521 label = f'{now:s}_{printable[count]:s}' 

1522 count +=1 

1523 dgroup.feffit_history[0].label = label 

1524 

1525 sname = self.autosave_script('\n'.join(script)) 

1526 self.write_message("wrote feffit script to '%s'" % sname) 

1527 

1528 self.show_subframe('feffit_result', FeffitResultFrame, 

1529 datagroup=opts['datagroup'], feffit_panel=self) 

1530 self.subframes['feffit_result'].add_results(dgroup, form=opts) 

1531 

1532 def onShowResults(self, event=None): 

1533 self.show_subframe('feffit_result', FeffitResultFrame, 

1534 feffit_panel=self) 

1535 

1536 def update_start_values(self, params): 

1537 """fill parameters with best fit values""" 

1538 self.params_panel.set_init_values(params) 

1539 for i in range(self.paths_nb.GetPageCount()): 

1540 if 'parameters' in self.paths_nb.GetPageText(i).strip().lower(): 

1541 self.paths_nb.SetSelection(i) 

1542 

1543 def autosave_script(self, text, fname='feffit_script.lar'): 

1544 """autosave model result to user larch folder""" 

1545 confdir = self.controller.larix_folder 

1546 if fname is None: 

1547 fname = 'feffit_script.lar' 

1548 fullname = os.path.join(confdir, fname) 

1549 if os.path.exists(fullname): 

1550 backup = os.path.join(confdir, 'feffit_script_BAK.lar') 

1551 shutil.copy(fullname, backup) 

1552 with open(fullname, 'w', encoding=sys.getdefaultencoding()) as fh: 

1553 fh.write(text) 

1554 return fullname 

1555 

1556 

1557############### 

1558 

1559class FeffitResultFrame(wx.Frame): 

1560 def __init__(self, parent=None, feffit_panel=None, datagroup=None, **kws): 

1561 wx.Frame.__init__(self, None, -1, title='Feffit Results', 

1562 style=FRAMESTYLE, size=(950, 700), **kws) 

1563 

1564 self.outforms = {'chik': 'chi(k), no k-weight', 

1565 'chikw': 'chi(k), k-weighted', 

1566 'chir_mag': '|chi(R)|', 

1567 'chir_re': 'Real[chi(R)]', 

1568 'chiq': 'Filtered \u03c7(k)' 

1569 } 

1570 

1571 self.feffit_panel = feffit_panel 

1572 self.datagroup = datagroup 

1573 self.feffit_history = getattr(datagroup, 'fit_history', []) 

1574 self.parent = parent 

1575 self.report_frame = None 

1576 self.datasets = {} 

1577 self.form = {} 

1578 self.larch_eval = feffit_panel.larch_eval 

1579 self.nfit = 0 

1580 self.createMenus() 

1581 self.build() 

1582 

1583 if datagroup is None: 

1584 symtab = self.feffit_panel.larch.symtable 

1585 xasgroups = getattr(symtab, '_xasgroups', None) 

1586 if xasgroups is not None: 

1587 for dname, dgroup in xasgroups.items(): 

1588 dgroup = getattr(symtab, dgroup, None) 

1589 hist = getattr(dgroup, 'feffit_history', None) 

1590 if hist is not None: 

1591 self.add_results(dgroup, show=True) 

1592 

1593 

1594 def createMenus(self): 

1595 self.menubar = wx.MenuBar() 

1596 fmenu = wx.Menu() 

1597 m = {} 

1598 for key, desc in self.outforms.items(): 

1599 MenuItem(self, fmenu, 

1600 f"Save Fit: {desc}", 

1601 f"Save data, model, path arrays as {desc}", 

1602 partial(self.onSaveFit, form=key)) 

1603 

1604 fmenu.AppendSeparator() 

1605 self.menubar.Append(fmenu, "&File") 

1606 self.SetMenuBar(self.menubar) 

1607 

1608 def build(self): 

1609 sizer = wx.GridBagSizer(2, 2) 

1610 sizer.SetVGap(2) 

1611 sizer.SetHGap(2) 

1612 

1613 splitter = wx.SplitterWindow(self, style=wx.SP_LIVE_UPDATE) 

1614 splitter.SetMinimumPaneSize(200) 

1615 

1616 self.filelist = EditableListBox(splitter, self.ShowDataSet, 

1617 size=(250, -1)) 

1618 set_color(self.filelist, 'list_fg', bg='list_bg') 

1619 

1620 self.font_fixedwidth = wx.Font(FONTSIZE_FW, wx.MODERN, wx.NORMAL, wx.BOLD) 

1621 

1622 panel = scrolled.ScrolledPanel(splitter) 

1623 

1624 panel.SetMinSize((725, 575)) 

1625 panel.SetSize((850, 575)) 

1626 

1627 # title row 

1628 self.wids = wids = {} 

1629 title = SimpleText(panel, 'Feffit Results', font=Font(FONTSIZE+2), 

1630 colour=COLORS['title'], style=LEFT) 

1631 

1632 wids['data_title'] = SimpleText(panel, '< > ', font=Font(FONTSIZE+2), 

1633 minsize=(350, -1), 

1634 colour=COLORS['title'], style=LEFT) 

1635 

1636 wids['plotone_op'] = Choice(panel, choices=list(PlotOne_Choices.keys()), 

1637 action=self.onPlot, size=(125, -1)) 

1638 wids['plotone_op'].SetSelection(1) 

1639 wids['plotalt_op'] = Choice(panel, choices=list(PlotAlt_Choices.keys()), 

1640 action=self.onPlot, size=(125, -1)) 

1641 

1642 wids['plot_win'] = Choice(panel, choices=PlotWindowChoices, 

1643 action=self.onPlot, size=(60, -1)) 

1644 wids['plot_win'].SetStringSelection('2') 

1645 

1646 ppanel = wx.Panel(panel) 

1647 ppanel.SetMinSize((450, 20)) 

1648 wids['plot_paths'] = Check(ppanel, default=False, label='Plot Each Path', 

1649 action=self.onPlot) 

1650 wids['plot_ftwindows'] = Check(ppanel, default=False, label='Plot FT Windows', 

1651 action=self.onPlot) 

1652 

1653 wids['plot_voffset'] = FloatSpin(ppanel, value=0, digits=2, increment=0.25, 

1654 action=self.onPlot, size=(100, -1)) 

1655 

1656 psizer = wx.BoxSizer(wx.HORIZONTAL) 

1657 psizer.Add( wids['plot_paths'], 0, 2) 

1658 psizer.Add( wids['plot_ftwindows'], 0, 2) 

1659 psizer.Add(SimpleText(ppanel, ' Offset'), 0, 2) 

1660 psizer.Add( wids['plot_voffset'], 0, 2) 

1661 pack(ppanel, psizer) 

1662 

1663 wids['plot_current'] = Button(panel,'Plot Current Model', 

1664 action=self.onPlot, size=(175, -1)) 

1665 

1666 wids['show_pathpars'] = Button(panel,'Show Path Parameters', 

1667 action=self.onShowPathParams, size=(175, -1)) 

1668 wids['show_script'] = Button(panel,'Show Fit Script', 

1669 action=self.onShowScript, size=(150, -1)) 

1670 

1671 lpanel = wx.Panel(panel) 

1672 wids['fit_label'] = wx.TextCtrl(lpanel, -1, ' ', size=(175, -1)) 

1673 wids['set_label'] = Button(lpanel, 'Update Label', size=(150, -1), 

1674 action=self.onUpdateLabel) 

1675 wids['del_fit'] = Button(lpanel, 'Remove from Fit History', size=(200, -1), 

1676 action=self.onRemoveFromHistory) 

1677 

1678 lsizer = wx.BoxSizer(wx.HORIZONTAL) 

1679 lsizer.Add(wids['fit_label'], 0, 2) 

1680 lsizer.Add(wids['set_label'], 0, 2) 

1681 lsizer.Add(wids['del_fit'], 0, 2) 

1682 pack(lpanel, lsizer) 

1683 

1684 irow = 0 

1685 sizer.Add(title, (irow, 0), (1, 1), LEFT) 

1686 sizer.Add(wids['data_title'], (irow, 1), (1, 3), LEFT) 

1687 

1688 irow += 1 

1689 sizer.Add(wids['plot_current'], (irow, 0), (1, 1), LEFT) 

1690 sizer.Add(wids['plotone_op'], (irow, 1), (1, 1), LEFT) 

1691 sizer.Add(ppanel, (irow, 2), (1, 3), LEFT) 

1692 irow += 1 

1693 sizer.Add(SimpleText(panel, 'Add Second Plot:', style=LEFT), (irow, 0), (1, 1), LEFT) 

1694 sizer.Add(wids['plotalt_op'], (irow, 1), (1, 1), LEFT) 

1695 sizer.Add(SimpleText(panel, 'Plot Window:', style=LEFT), (irow, 2), (1, 1), LEFT) 

1696 sizer.Add(wids['plot_win'], (irow, 3), (1, 1), LEFT) 

1697 

1698 irow += 1 

1699 sizer.Add(wids['show_pathpars'], (irow, 0), (1, 1), LEFT) 

1700 sizer.Add(wids['show_script'], (irow, 1), (1, 1), LEFT) 

1701 irow += 1 

1702 sizer.Add(HLine(panel, size=(650, 3)), (irow, 0), (1, 5), LEFT) 

1703 

1704 irow += 1 

1705 sizer.Add(SimpleText(panel, 'Fit Label:', style=LEFT), (irow, 0), (1, 1), LEFT) 

1706 sizer.Add(lpanel, (irow, 1), (1, 4), LEFT) 

1707 

1708 

1709 irow += 1 

1710 title = SimpleText(panel, '[[Fit Statistics]]', font=Font(FONTSIZE+2), 

1711 colour=COLORS['title'], style=LEFT) 

1712 subtitle = SimpleText(panel, ' (most recent fit is at the top)', 

1713 font=Font(FONTSIZE+1), style=LEFT) 

1714 

1715 sizer.Add(title, (irow, 0), (1, 1), LEFT) 

1716 sizer.Add(subtitle, (irow, 1), (1, 3), LEFT) 

1717 

1718 sview = self.wids['stats'] = dv.DataViewListCtrl(panel, style=DVSTYLE) 

1719 sview.SetFont(self.font_fixedwidth) 

1720 sview.Bind(dv.EVT_DATAVIEW_SELECTION_CHANGED, self.onSelectFit) 

1721 sview.AppendTextColumn(' # ', width=40) 

1722 sview.AppendTextColumn('Label', width=140) 

1723 sview.AppendTextColumn('Npaths', width=70) 

1724 sview.AppendTextColumn('Nvary', width=60) 

1725 sview.AppendTextColumn('Nidp', width=60) 

1726 sview.AppendTextColumn('\u03c7\u00B2', width=75) 

1727 sview.AppendTextColumn('reduced \u03c7\u00B2', width=95) 

1728 sview.AppendTextColumn('R Factor', width=80) 

1729 sview.AppendTextColumn('Akaike Info', width=85) 

1730 

1731 

1732 for col in range(sview.ColumnCount): 

1733 this = sview.Columns[col] 

1734 this.Sortable = True 

1735 this.Alignment = wx.ALIGN_RIGHT if col > 1 else wx.ALIGN_LEFT 

1736 this.Renderer.Alignment = this.Alignment 

1737 

1738 sview.SetMinSize((750, 150)) 

1739 

1740 irow += 1 

1741 sizer.Add(sview, (irow, 0), (1, 5), LEFT) 

1742 

1743 irow += 1 

1744 title = SimpleText(panel, '[[Variables]]', font=Font(FONTSIZE+2), 

1745 colour=COLORS['title'], style=LEFT) 

1746 sizer.Add(title, (irow, 0), (1, 1), LEFT) 

1747 

1748 self.wids['copy_params'] = Button(panel, 'Update Model with these values', 

1749 size=(225, -1), action=self.onCopyParams) 

1750 

1751 sizer.Add(self.wids['copy_params'], (irow, 1), (1, 3), LEFT) 

1752 

1753 pview = self.wids['params'] = dv.DataViewListCtrl(panel, style=DVSTYLE) 

1754 pview.SetFont(self.font_fixedwidth) 

1755 self.wids['paramsdata'] = [] 

1756 pview.AppendTextColumn('Parameter', width=175) 

1757 pview.AppendTextColumn('Best-Fit Value', width=125) 

1758 pview.AppendTextColumn('Standard Error', width=125) 

1759 pview.AppendTextColumn('Info ', width=225) 

1760 

1761 for col in range(4): 

1762 this = pview.Columns[col] 

1763 this.Sortable = False 

1764 this.Alignment = wx.ALIGN_RIGHT if col in (1, 2) else wx.ALIGN_LEFT 

1765 this.Renderer.Alignment = this.Alignment 

1766 

1767 pview.SetMinSize((750, 200)) 

1768 pview.Bind(dv.EVT_DATAVIEW_SELECTION_CHANGED, self.onSelectParameter) 

1769 

1770 irow += 1 

1771 sizer.Add(pview, (irow, 0), (1, 5), LEFT) 

1772 

1773 irow += 1 

1774 title = SimpleText(panel, '[[Correlations]]', font=Font(FONTSIZE+2), 

1775 colour=COLORS['title'], style=LEFT) 

1776 

1777 ppanel = wx.Panel(panel) 

1778 ppanel.SetMinSize((450, 20)) 

1779 self.wids['all_correl'] = Button(ppanel, 'Show All', 

1780 size=(100, -1), action=self.onAllCorrel) 

1781 

1782 self.wids['min_correl'] = FloatSpin(ppanel, value=MIN_CORREL, 

1783 min_val=0, size=(100, -1), 

1784 digits=3, increment=0.1) 

1785 

1786 psizer = wx.BoxSizer(wx.HORIZONTAL) 

1787 psizer.Add(SimpleText(ppanel, 'minimum correlation: '), 0, 2) 

1788 psizer.Add(self.wids['min_correl'], 0, 2) 

1789 psizer.Add(self.wids['all_correl'], 0, 2) 

1790 pack(ppanel, psizer) 

1791 

1792 sizer.Add(title, (irow, 0), (1, 1), LEFT) 

1793 sizer.Add(ppanel, (irow, 1), (1, 4), LEFT) 

1794 

1795 cview = self.wids['correl'] = dv.DataViewListCtrl(panel, style=DVSTYLE) 

1796 cview.SetFont(self.font_fixedwidth) 

1797 

1798 cview.AppendTextColumn('Parameter 1', width=150) 

1799 cview.AppendTextColumn('Parameter 2', width=150) 

1800 cview.AppendTextColumn('Correlation', width=150) 

1801 

1802 for col in (0, 1, 2): 

1803 this = cview.Columns[col] 

1804 this.Sortable = False 

1805 align = wx.ALIGN_LEFT 

1806 if col == 2: 

1807 align = wx.ALIGN_RIGHT 

1808 this.Alignment = this.Renderer.Alignment = align 

1809 cview.SetMinSize((550, 150)) 

1810 

1811 irow += 1 

1812 sizer.Add(cview, (irow, 0), (1, 5), LEFT) 

1813 

1814 pack(panel, sizer) 

1815 panel.SetupScrolling() 

1816 

1817 splitter.SplitVertically(self.filelist, panel, 1) 

1818 

1819 mainsizer = wx.BoxSizer(wx.VERTICAL) 

1820 mainsizer.Add(splitter, 1, wx.GROW|wx.ALL, 5) 

1821 

1822 pack(self, mainsizer) 

1823 self.Show() 

1824 self.Raise() 

1825 

1826 def show_report(self, text, title='Text', default_filename='out.txt', 

1827 wildcard=None): 

1828 if wildcard is None: 

1829 wildcard='Text Files (*.txt)|*.txt' 

1830 try: 

1831 self.report_frame.set_text(text) 

1832 self.report_frame.SetTitle(title) 

1833 self.report_frame.default_filename = default_filename 

1834 self.report_frame.wildcard = wildcard 

1835 except: 

1836 self.report_frame = ReportFrame(parent=self.parent, 

1837 text=text, title=title, 

1838 default_filename=default_filename, 

1839 wildcard=wildcard) 

1840 

1841 

1842 def onShowPathParams(self, event=None): 

1843 result = self.get_fitresult() 

1844 if result is None: 

1845 return 

1846 text = f'# Feffit Report for {self.datagroup.filename} fit "{result.label}"\n' 

1847 text = text + feffit_report(result) 

1848 title = f'Report for {self.datagroup.filename} fit "{result.label}"' 

1849 fname = fix_filename(f'{self.datagroup.filename}_{result.label}.txt') 

1850 self.show_report(text, title=title, default_filename=fname) 

1851 

1852 def onShowScript(self, event=None): 

1853 result = self.get_fitresult() 

1854 if result is None: 

1855 return 

1856 text = [f'# Feffit Script for {self.datagroup.filename} fit "{result.label}"'] 

1857 text.extend(result.commands) 

1858 text = '\n'.join(text) 

1859 title = f'Script for {self.datagroup.filename} fit "{result.label}"' 

1860 fname = fix_filename(f'{self.datagroup.filename}_{result.label}.lar') 

1861 self.show_report(text, title=title, default_filename=fname, 

1862 wildcard='Larch/Python Script (*.lar)|*.lar') 

1863 

1864 def onUpdateLabel(self, event=None): 

1865 result = self.get_fitresult() 

1866 if result is None: 

1867 return 

1868 item = self.wids['stats'].GetSelectedRow() 

1869 result.label = self.wids['fit_label'].GetValue() 

1870 self.show_results() 

1871 

1872 def onRemoveFromHistory(self, event=None): 

1873 result = self.get_fitresult() 

1874 if result is None: 

1875 return 

1876 if wx.ID_YES != Popup(self, 

1877 f"Remove fit '{result.label}' from history?\nThis cannot be undone.", 

1878 "Remove fit?", style=wx.YES_NO): 

1879 return 

1880 self.datagroup.feffit_history.pop(self.nfit) 

1881 self.nfit = 0 

1882 self.show_results() 

1883 

1884 def onPlot(self, event=None): 

1885 

1886 opts = {'build_fitmodel': False} 

1887 for key, meth in (('plot_ftwindows', 'IsChecked'), 

1888 ('plot_paths', 'IsChecked'), 

1889 ('plotone_op', 'GetStringSelection'), 

1890 ('plotalt_op', 'GetStringSelection'), 

1891 ('plot_win', 'GetStringSelection'), 

1892 ('plot_voffset', 'GetValue')): 

1893 opts[key] = getattr(self.wids[key], meth)() 

1894 

1895 opts['plotone_op'] = PlotOne_Choices[opts['plotone_op']] 

1896 opts['plotalt_op'] = PlotAlt_Choices[opts['plotalt_op']] 

1897 opts['plot_win'] = int(opts['plot_win']) 

1898 

1899 result = self.get_fitresult() 

1900 if result is None: 

1901 return 

1902 dset = result.datasets[0] 

1903 dgroup = dset.data 

1904 if not hasattr(dset.data, 'rwin'): 

1905 dset._residual(result.params) 

1906 dset.save_ffts() 

1907 trans = dset.transform 

1908 dset.prepare_fit(group2params(result.paramgroup)) 

1909 dset._residual(result.paramgroup) 

1910 

1911 result_name = f'{self.datagroup.groupname}.feffit_history[{self.nfit}]' 

1912 opts['label'] = f'{result_name}.label' 

1913 opts['pargroup_name'] = f'{result_name}.paramgroup' 

1914 opts['paths_name'] = f'{result_name}.datasets[0].paths' 

1915 opts['pathsum_name'] = f'{result_name}.datasets[0].model' 

1916 opts['dataset_name'] = f'{result_name}.datasets[0].data' 

1917 opts['dgroup'] = dgroup 

1918 opts['title'] = f'{self.datagroup.filename}: {result.label}' 

1919 

1920 for attr in ('kmin', 'kmax', 'dk', 'rmin', 'rmax', 'fitspace'): 

1921 opts[attr] = getattr(trans, attr) 

1922 opts['fit_kwstring'] = "%s" % getattr(trans, 'kweight') 

1923 opts['kwindow'] = getattr(trans, 'window') 

1924 opts['topwin'] = self 

1925 

1926 self.feffit_panel.onPlot(**opts) 

1927 

1928 

1929 def onSaveFitCommand(self, event=None): 

1930 wildcard = 'Larch/Python Script (*.lar)|*.lar|All files (*.*)|*.*' 

1931 result = self.get_fitresult() 

1932 if result is None: 

1933 return 

1934 fname = fix_filename(f'{self.datagroup.filename}_{result.label:s}.lar') 

1935 

1936 path = FileSave(self, message='Save text to file', 

1937 wildcard=wildcard, default_file=fname) 

1938 if path is not None: 

1939 text = '\n'.join(result.commands) 

1940 with open(path, 'w', encoding=sys.getdefaultencoding()) as fh: 

1941 fh.write(text) 

1942 fh.write('') 

1943 

1944 

1945 def onSaveFit(self, evt=None, form='chikw'): 

1946 "Save arrays to text file" 

1947 result = self.get_fitresult() 

1948 if result is None: 

1949 return 

1950 

1951 fname = fix_filename(f'{self.datagroup.filename}_{result.label:s}_{form}') 

1952 fname = fname.replace('.', '_') 

1953 fname = fname + '.txt' 

1954 

1955 wildcard = 'Text Files (*.txt)|*.txt|All files (*.*)|*.*' 

1956 savefile = FileSave(self, 'Save Fit Model (%s)' % form, 

1957 default_file=fname, 

1958 wildcard=wildcard) 

1959 if savefile is None: 

1960 return 

1961 

1962 text = feffit_report(result) 

1963 desc = self.outforms[form] 

1964 buff = [f'# Results for {self.datagroup.filename} "{result.label}": {desc}'] 

1965 

1966 for line in text.split('\n'): 

1967 buff.append('# %s' % line) 

1968 buff.append('## ') 

1969 buff.append('#' + '---'*25) 

1970 

1971 ds0 = result.datasets[0] 

1972 

1973 xname = 'k' if form.startswith('chik') else 'r' 

1974 yname = 'chi' if form.startswith('chik') else form 

1975 kw = 0 

1976 if form == 'chikw': 

1977 kw = ds0.transform.kweight 

1978 

1979 xarr = getattr(ds0.data, xname) 

1980 nx = len(xarr) 

1981 ydata = getattr(ds0.data, yname) * xarr**kw 

1982 ymodel = getattr(ds0.model, yname) * xarr**kw 

1983 out = [xarr, ydata, ymodel] 

1984 

1985 array_names = [xname, 'expdata', 'model'] 

1986 for pname, pgroup in ds0.paths.items(): 

1987 array_names.append(f'feffpath_{pname}') 

1988 out.append(getattr(pgroup, yname)[:nx] * xarr**kw) 

1989 

1990 col_labels = [] 

1991 for a in array_names: 

1992 if len(a) < 13: 

1993 a = (a + ' '*13)[:13] 

1994 col_labels.append(a) 

1995 

1996 buff.append('# ' + ' '.join(col_labels)) 

1997 

1998 for i in range(nx): 

1999 words = [gformat(x[i], 12) for x in out] 

2000 buff.append(' '.join(words)) 

2001 buff.append('') 

2002 

2003 

2004 with open(savefile, 'w', encoding=sys.getdefaultencoding()) as fh: 

2005 fh.write('\n'.join(buff)) 

2006 

2007 def get_fitresult(self, nfit=None): 

2008 if nfit is None: 

2009 nfit = self.nfit 

2010 self.feffit_history = getattr(self.datagroup, 'feffit_history', []) 

2011 self.nfit = max(0, nfit) 

2012 n_hist = len(self.feffit_history) 

2013 if n_hist == 0: 

2014 return None 

2015 if self.nfit > n_hist: 

2016 self.nfit = 0 

2017 return self.feffit_history[self.nfit] 

2018 

2019 

2020 def onSelectFit(self, evt=None): 

2021 if self.wids['stats'] is None: 

2022 return 

2023 item = self.wids['stats'].GetSelectedRow() 

2024 if item > -1: 

2025 self.show_fitresult(nfit=item) 

2026 

2027 def onSelectParameter(self, evt=None): 

2028 if self.wids['params'] is None: 

2029 return 

2030 if not self.wids['params'].HasSelection(): 

2031 return 

2032 item = self.wids['params'].GetSelectedRow() 

2033 pname = self.wids['paramsdata'][item] 

2034 

2035 cormin= self.wids['min_correl'].GetValue() 

2036 self.wids['correl'].DeleteAllItems() 

2037 

2038 result = self.get_fitresult() 

2039 if result is None: 

2040 return 

2041 this = result.params[pname] 

2042 if this.correl is not None: 

2043 sort_correl = sorted(this.correl.items(), key=lambda it: abs(it[1])) 

2044 for name, corval in reversed(sort_correl): 

2045 if abs(corval) > cormin: 

2046 self.wids['correl'].AppendItem((pname, name, "% .4f" % corval)) 

2047 

2048 def onAllCorrel(self, evt=None): 

2049 result = self.get_fitresult() 

2050 if result is None: 

2051 return 

2052 params = result.params 

2053 parnames = list(params.keys()) 

2054 

2055 cormin= self.wids['min_correl'].GetValue() 

2056 correls = {} 

2057 for i, name in enumerate(parnames): 

2058 par = params[name] 

2059 if not par.vary: 

2060 continue 

2061 if hasattr(par, 'correl') and par.correl is not None: 

2062 for name2 in parnames[i+1:]: 

2063 if (name != name2 and name2 in par.correl and 

2064 abs(par.correl[name2]) > cormin): 

2065 correls["%s$$%s" % (name, name2)] = par.correl[name2] 

2066 

2067 sort_correl = sorted(correls.items(), key=lambda it: abs(it[1])) 

2068 sort_correl.reverse() 

2069 

2070 self.wids['correl'].DeleteAllItems() 

2071 

2072 for namepair, corval in sort_correl: 

2073 name1, name2 = namepair.split('$$') 

2074 self.wids['correl'].AppendItem((name1, name2, "% .4f" % corval)) 

2075 

2076 def onCopyParams(self, evt=None): 

2077 result = self.get_fitresult() 

2078 if result is None: 

2079 return 

2080 self.feffit_panel.update_start_values(result.params) 

2081 

2082 def ShowDataSet(self, evt=None): 

2083 dataset = evt.GetString() 

2084 group = self.datasets.get(evt.GetString(), None) 

2085 if group is not None: 

2086 self.show_results(datagroup=group) 

2087 

2088 def add_results(self, dgroup, form=None, larch_eval=None, show=True): 

2089 name = dgroup.filename 

2090 if name not in self.filelist.GetItems(): 

2091 self.filelist.Append(name) 

2092 self.datasets[name] = dgroup 

2093 if show: 

2094 self.show_results(datagroup=dgroup, form=form, larch_eval=larch_eval) 

2095 

2096 def show_results(self, datagroup=None, form=None, larch_eval=None): 

2097 if datagroup is not None: 

2098 self.datagroup = datagroup 

2099 if larch_eval is not None: 

2100 self.larch_eval = larch_eval 

2101 

2102 datagroup = self.datagroup 

2103 self.feffit_history = getattr(self.datagroup, 'feffit_history', []) 

2104 

2105 cur = self.get_fitresult() 

2106 if cur is None: 

2107 return 

2108 wids = self.wids 

2109 wids['stats'].DeleteAllItems() 

2110 for i, res in enumerate(self.feffit_history): 

2111 args = ["%d" % (i+1), res.label, "%.d" % (len(res.datasets[0].paths))] 

2112 for attr in ('nvarys', 'n_independent', 'chi_square', 

2113 'chi2_reduced', 'rfactor', 'aic'): 

2114 val = getattr(res, attr) 

2115 if isinstance(val, int): 

2116 val = '%d' % val 

2117 elif attr == 'n_independent': 

2118 val = "%.2f" % val 

2119 else: 

2120 val = "%.4f" % val 

2121 # val = gformat(val, 9) 

2122 args.append(val) 

2123 wids['stats'].AppendItem(tuple(args)) 

2124 wids['data_title'].SetLabel(self.datagroup.filename) 

2125 self.show_fitresult(nfit=0) 

2126 

2127 

2128 def show_fitresult(self, nfit=0, datagroup=None): 

2129 if datagroup is not None: 

2130 self.datagroup = datagroup 

2131 

2132 result = self.get_fitresult(nfit=nfit) 

2133 if result is None: 

2134 return 

2135 

2136 path_hashkeys = [] 

2137 for ds in result.datasets: 

2138 path_hashkeys.extend([p.hashkey for p in ds.paths.values()]) 

2139 

2140 wids = self.wids 

2141 wids['fit_label'].SetValue(result.label) 

2142 wids['data_title'].SetLabel(self.datagroup.filename) 

2143 wids['params'].DeleteAllItems() 

2144 wids['paramsdata'] = [] 

2145 for param in reversed(result.params.values()): 

2146 pname = param.name 

2147 if any([pname.endswith('_%s' % phash) for phash in path_hashkeys]): 

2148 continue 

2149 if getattr(param, 'skip', None) not in (False, None): 

2150 continue 

2151 

2152 try: 

2153 val = gformat(param.value, 10) 

2154 except (TypeError, ValueError): 

2155 val = ' ??? ' 

2156 serr = ' N/A ' 

2157 if param.stderr is not None: 

2158 serr = gformat(param.stderr, 10) 

2159 extra = ' ' 

2160 if param.expr is not None: 

2161 extra = '= %s ' % param.expr 

2162 elif not param.vary: 

2163 extra = '(fixed)' 

2164 elif param.init_value is not None: 

2165 extra = '(init=%s)' % gformat(param.init_value, 10) 

2166 

2167 wids['params'].AppendItem((pname, val, serr, extra)) 

2168 wids['paramsdata'].append(pname) 

2169 self.Refresh()