Coverage for /Users/Newville/Codes/xraylarch/larch/wxxas/xasgui.py: 11%

1139 statements  

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

1#!/usr/bin/env python 

2""" 

3XANES Data Viewer and Analysis Tool 

4""" 

5import os 

6import sys 

7import time 

8import copy 

9import platform 

10from threading import Thread 

11import numpy as np 

12np.seterr(all='ignore') 

13 

14from functools import partial 

15 

16import wx 

17import wx.lib.scrolledpanel as scrolled 

18 

19from wx.adv import AboutBox, AboutDialogInfo 

20 

21from wx.richtext import RichTextCtrl 

22 

23WX_DEBUG = True 

24 

25import larch 

26from larch import Group, Journal, Entry 

27from larch.io import save_session, read_session 

28from larch.math import index_of 

29from larch.utils import isotime, time_ago, get_cwd, is_gzip, uname 

30from larch.utils.strutils import (file2groupname, unique_name, 

31 common_startstring, asfloat) 

32 

33from larch.larchlib import read_workdir, save_workdir, read_config, save_config 

34 

35from larch.wxlib import (LarchFrame, ColumnDataFileFrame, AthenaImporter, 

36 SpecfileImporter, FileCheckList, FloatCtrl, 

37 FloatSpin, SetTip, get_icon, SimpleText, TextCtrl, 

38 pack, Button, Popup, HLine, FileSave, FileOpen, 

39 Choice, Check, MenuItem, HyperText, set_color, COLORS, 

40 CEN, LEFT, FRAMESTYLE, Font, FONTSIZE, 

41 flatnotebook, LarchUpdaterDialog, GridPanel, 

42 CIFFrame, Structure2FeffFrame, FeffResultsFrame, LarchWxApp, OkCancel, 

43 ExceptionPopup, set_color) 

44 

45 

46from larch.wxlib.plotter import get_display 

47 

48from larch.fitting import fit_report 

49from larch.site_config import icondir, home_dir, user_larchdir 

50from larch.version import check_larchversion 

51 

52from .prepeak_panel import PrePeakPanel 

53from .xasnorm_panel import XASNormPanel 

54from .lincombo_panel import LinearComboPanel 

55from .pca_panel import PCAPanel 

56from .exafs_panel import EXAFSPanel 

57from .feffit_panel import FeffitPanel 

58from .regress_panel import RegressionPanel 

59from .xas_controller import XASController 

60from .taskpanel import GroupJournalFrame 

61from .config import FULLCONF, CONF_SECTIONS, CVar, ATHENA_CLAMPNAMES 

62 

63from .xas_dialogs import (MergeDialog, RenameDialog, RemoveDialog, 

64 DeglitchDialog, ExportCSVDialog, RebinDataDialog, 

65 EnergyCalibrateDialog, SmoothDataDialog, 

66 OverAbsorptionDialog, DeconvolutionDialog, 

67 SpectraCalcDialog, QuitDialog, LoadSessionDialog, 

68 fit_dialog_window) 

69 

70from larch.io import (read_ascii, read_xdi, read_gsexdi, gsescan_group, 

71 fix_varname, groups2csv, is_athena_project, 

72 is_larch_session_file, 

73 AthenaProject, make_hashkey, is_specfile, open_specfile) 

74 

75from larch.xafs import pre_edge, pre_edge_baseline 

76 

77LEFT = wx.ALIGN_LEFT 

78CEN |= wx.ALL 

79FILE_WILDCARDS = "Data Files|*.0*;*.dat;*.DAT;*.xdi;*.prj;*.sp*c;*.h*5;*.larix|All files (*.*)|*.*" 

80 

81ICON_FILE = 'onecone.ico' 

82XASVIEW_SIZE = (1020, 830) 

83PLOTWIN_SIZE = (550, 550) 

84 

85NB_PANELS = {'Normalization': XASNormPanel, 

86 'Pre-edge Peak': PrePeakPanel, 

87 'PCA': PCAPanel, 

88 'Linear Combo': LinearComboPanel, 

89 'Regression': RegressionPanel, 

90 'EXAFS': EXAFSPanel, 

91 'Feff Fitting': FeffitPanel} 

92 

93QUIT_MESSAGE = '''Really Quit? You may want to save your project before quitting. 

94 This is not done automatically!''' 

95 

96LARIX_TITLE = "Larix (was XAS Viewer): XAS Visualization and Analysis" 

97 

98 

99def assign_gsescan_groups(group): 

100 labels = group.array_labels 

101 labels = [] 

102 for i, name in enumerate(group.pos_desc): 

103 name = fix_varname(name.lower()) 

104 labels.append(name) 

105 setattr(group, name, group.pos[i, :]) 

106 

107 for i, name in enumerate(group.sums_names): 

108 name = fix_varname(name.lower()) 

109 labels.append(name) 

110 setattr(group, name, group.sums_corr[i, :]) 

111 

112 for i, name in enumerate(group.det_desc): 

113 name = fix_varname(name.lower()) 

114 labels.append(name) 

115 setattr(group, name, group.det_corr[i, :]) 

116 

117 group.array_labels = labels 

118 

119 

120class PreferencesFrame(wx.Frame): 

121 """ edit preferences""" 

122 def __init__(self, parent, controller, **kws): 

123 self.controller = controller 

124 wx.Frame.__init__(self, None, -1, 'Larix Preferences', 

125 style=FRAMESTYLE, size=(700, 725)) 

126 

127 sizer = wx.BoxSizer(wx.VERTICAL) 

128 tpanel = wx.Panel(self) 

129 

130 self.title = SimpleText(tpanel, 'Edit Preference and Defaults', 

131 size=(500, 25), 

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

133 colour=COLORS['nb_text']) 

134 

135 self.save_btn = Button(tpanel, 'Save for Future sessions', 

136 size=(200, -1), action=self.onSave) 

137 

138 self.nb = flatnotebook(tpanel, {}) 

139 self.wids = {} 

140 conf = self.controller.config 

141 

142 def text(panel, label, size): 

143 return SimpleText(panel, label, size=(size, -1), style=LEFT) 

144 

145 for name, data in FULLCONF.items(): 

146 self.wids[name] = {} 

147 

148 panel = GridPanel(self.nb, ncols=3, nrows=8, pad=3, itemstyle=LEFT) 

149 panel.SetFont(Font(FONTSIZE)) 

150 title = CONF_SECTIONS.get(name, name) 

151 title = SimpleText(panel, f" {title}", 

152 size=(550, -1), font=Font(FONTSIZE+2), 

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

154 

155 self.wids[name]['_key_'] = SimpleText(panel, " <name> ", 

156 size=(150, -1), style=LEFT) 

157 self.wids[name]['_help_'] = SimpleText(panel, " <click on name for description> ", 

158 size=(525, 30), style=LEFT) 

159 

160 panel.Add((5, 5), newrow=True) 

161 panel.Add(title, dcol=4) 

162 panel.Add((5, 5), newrow=True) 

163 panel.Add(self.wids[name]['_key_']) 

164 panel.Add(self.wids[name]['_help_'], dcol=3) 

165 panel.Add((5, 5), newrow=True) 

166 panel.Add(HLine(panel, (625, 3)), dcol=4) 

167 

168 panel.Add((5, 5), newrow=True) 

169 panel.Add(text(panel, 'Name', 150)) 

170 

171 panel.Add(text(panel, 'Value', 250)) 

172 panel.Add(text(panel, 'Factory Default Value', 225)) 

173 

174 for key, cvar in data.items(): 

175 val = conf[name][key] 

176 cb = partial(self.update_value, section=name, option=key) 

177 helpcb = partial(self.update_help, section=name, option=key) 

178 wid = None 

179 if cvar.dtype == 'choice': 

180 wid = Choice(panel, size=(250, -1), choices=cvar.choices, action=cb) 

181 if not isinstance(val, str): val = str(val) 

182 wid.SetStringSelection(val) 

183 elif cvar.dtype == 'bool': 

184 wid = Choice(panel, size=(250, -1), choices=['True', 'False'], action=cb) 

185 wid.SetStringSelection('True' if val else 'False') 

186 elif cvar.dtype in ('int', 'float'): 

187 digits = 2 if cvar.dtype == 'float' else 0 

188 wid = FloatSpin(panel, value=val, min_val=cvar.min, max_val=cvar.max, 

189 digits=digits, increment=cvar.step, size=(250, -1), action=cb) 

190 else: 

191 wid = TextCtrl(panel, size=(250, -1), value=val, action=cb) 

192 

193 label = HyperText(panel, key, action=helpcb, size=(150, -1)) 

194 panel.Add((5, 5), newrow=True) 

195 panel.Add(label) 

196 panel.Add(wid) 

197 panel.Add(text(panel, f'{cvar.value}', 225)) 

198 SetTip(wid, cvar.desc) 

199 self.wids[name][key] = wid 

200 

201 panel.pack() 

202 self.nb.AddPage(panel, name, True) 

203 

204 self.nb.SetSelection(0) 

205 

206 sizer.Add(self.title, 0, LEFT, 3) 

207 sizer.Add(self.save_btn, 0, LEFT, 5) 

208 sizer.Add((5, 5), 0, LEFT, 5) 

209 sizer.Add(self.nb, 1, LEFT|wx.EXPAND, 5) 

210 pack(tpanel, sizer) 

211 w0, h0 = self.GetSize() 

212 w1, h1 = self.GetBestSize() 

213 self.SetSize((max(w0, w1)+25, max(h0, h1)+25)) 

214 

215 self.Show() 

216 self.Raise() 

217 

218 def update_help(self, label=None, event=None, section='main', option=None): 

219 cvar = FULLCONF[section][option] 

220 self.wids[section]['_key_'].SetLabel("%s : " % option) 

221 self.wids[section]['_help_'].SetLabel(cvar.desc) 

222 

223 def update_value(self, event=None, section='main', option=None): 

224 cvar = FULLCONF[section][option] 

225 wid = self.wids[section][option] 

226 value = cvar.value 

227 if cvar.dtype == 'bool': 

228 value = wid.GetStringSelection().lower().startswith('t') 

229 elif cvar.dtype == 'choice': 

230 value = wid.GetStringSelection() 

231 elif cvar.dtype == 'int': 

232 value = int(wid.GetValue()) 

233 elif cvar.dtype == 'float': 

234 value = float(wid.GetValue()) 

235 else: 

236 value = wid.GetValue() 

237 self.controller.config[section][option] = value 

238 

239 def onSave(self, event=None): 

240 self.controller.save_config() 

241 

242 

243class XASFrame(wx.Frame): 

244 _about = f"""{LARIX_TITLE} 

245 Matt Newville <newville @ cars.uchicago.edu> 

246 """ 

247 def __init__(self, parent=None, _larch=None, filename=None, 

248 check_version=True, **kws): 

249 wx.Frame.__init__(self, parent, -1, size=XASVIEW_SIZE, style=FRAMESTYLE) 

250 

251 if check_version: 

252 def version_checker(): 

253 self.vinfo = check_larchversion() 

254 version_thread = Thread(target=version_checker) 

255 version_thread.start() 

256 

257 self.last_col_config = {} 

258 self.last_spec_config = {} 

259 self.last_session_file = None 

260 self.last_session_read = None 

261 self.last_athena_file = None 

262 self.paths2read = [] 

263 self.current_filename = filename 

264 title = LARIX_TITLE 

265 

266 self.larch_buffer = parent 

267 if not isinstance(parent, LarchFrame): 

268 self.larch_buffer = LarchFrame(_larch=_larch, 

269 parent=self, 

270 is_standalone=False, 

271 with_raise=False, 

272 exit_on_close=False) 

273 

274 self.larch = self.larch_buffer.larchshell 

275 

276 self.controller = XASController(wxparent=self, _larch=self.larch) 

277 iconfile = os.path.join(icondir, ICON_FILE) 

278 self.SetIcon(wx.Icon(iconfile, wx.BITMAP_TYPE_ICO)) 

279 

280 self.last_autosave = 0 

281 self.last_save_message = ('Session has not been saved', '', '') 

282 

283 

284 self.timers = {'pin': wx.Timer(self), 

285 'autosave': wx.Timer(self)} 

286 self.Bind(wx.EVT_TIMER, self.onPinTimer, self.timers['pin']) 

287 self.Bind(wx.EVT_TIMER, self.onAutoSaveTimer, self.timers['autosave']) 

288 self.cursor_dat = {} 

289 

290 self.subframes = {} 

291 self.plotframe = None 

292 self.SetTitle(title) 

293 self.SetSize(XASVIEW_SIZE) 

294 self.SetFont(Font(FONTSIZE)) 

295 self.createMainPanel() 

296 self.createMenus() 

297 self.statusbar = self.CreateStatusBar(2, style=wx.STB_DEFAULT_STYLE) 

298 self.statusbar.SetStatusWidths([-3, -1]) 

299 statusbar_fields = [" ", "ready"] 

300 for i in range(len(statusbar_fields)): 

301 self.statusbar.SetStatusText(statusbar_fields[i], i) 

302 self.Show() 

303 

304 self.Raise() 

305 self.statusbar.SetStatusText('ready', 1) 

306 self.timers['autosave'].Start(30_000) 

307 

308 plotframe = self.controller.get_display(stacked=False) 

309 xpos, ypos = self.GetPosition() 

310 xsiz, ysiz = self.GetSize() 

311 wx.CallAfter(plotframe.SetPosition, (xpos+xsiz+2, ypos)) 

312 if self.current_filename is not None: 

313 wx.CallAfter(self.onRead, self.current_filename) 

314 

315 # show_wxsizes(self) 

316 if check_version: 

317 version_thread.join() 

318 if self.vinfo is not None: 

319 if self.vinfo.update_available: 

320 self.statusbar.SetStatusText(f'Larch Version {self.vinfo.remote_version} is available!', 0) 

321 self.statusbar.SetStatusText(f'Larch Version {self.vinfo.local_version}', 1) 

322 else: 

323 self.statusbar.SetStatusText(f'Larch Version {self.vinfo.local_version} (latest)', 1) 

324 

325 

326 def createMainPanel(self): 

327 display0 = wx.Display(0) 

328 client_area = display0.ClientArea 

329 xmin, ymin, xmax, ymax = client_area 

330 xpos = int((xmax-xmin)*0.02) + xmin 

331 ypos = int((ymax-ymin)*0.04) + ymin 

332 self.SetPosition((xpos, ypos)) 

333 

334 splitter = wx.SplitterWindow(self, style=wx.SP_LIVE_UPDATE, 

335 size=(700, 700)) 

336 splitter.SetMinimumPaneSize(250) 

337 

338 leftpanel = wx.Panel(splitter) 

339 ltop = wx.Panel(leftpanel) 

340 

341 def Btn(msg, x, act): 

342 b = Button(ltop, msg, size=(x, 30), action=act) 

343 b.SetFont(Font(FONTSIZE)) 

344 return b 

345 

346 sel_none = Btn('Select None', 120, self.onSelNone) 

347 sel_all = Btn('Select All', 120, self.onSelAll) 

348 

349 file_actions = [('Show Group Journal', self.onGroupJournal), 

350 ('Copy Group', self.onCopyGroup), 

351 ('Rename Group', self.onRenameGroup), 

352 ('Remove Group', self.onRemoveGroup)] 

353 

354 self.controller.filelist = FileCheckList(leftpanel, main=self, 

355 pre_actions=file_actions, 

356 select_action=self.ShowFile, 

357 remove_action=self.RemoveFile) 

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

359 

360 tsizer = wx.BoxSizer(wx.HORIZONTAL) 

361 tsizer.Add(sel_all, 1, LEFT|wx.GROW, 1) 

362 tsizer.Add(sel_none, 1, LEFT|wx.GROW, 1) 

363 pack(ltop, tsizer) 

364 

365 sizer = wx.BoxSizer(wx.VERTICAL) 

366 sizer.Add(ltop, 0, LEFT|wx.GROW, 1) 

367 sizer.Add(self.controller.filelist, 1, LEFT|wx.GROW|wx.ALL, 1) 

368 

369 pack(leftpanel, sizer) 

370 

371 # right hand side 

372 panel = scrolled.ScrolledPanel(splitter) 

373 panel.SetSize((650, 650)) 

374 panel.SetMinSize((450, 550)) 

375 sizer = wx.BoxSizer(wx.VERTICAL) 

376 self.title = SimpleText(panel, ' ', size=(500, 25), 

377 font=Font(FONTSIZE+3), style=LEFT, 

378 colour=COLORS['nb_text']) 

379 

380 ir = 0 

381 sizer.Add(self.title, 0, CEN, 3) 

382 self.nb = flatnotebook(panel, NB_PANELS, 

383 panelkws=dict(xasmain=self, 

384 controller=self.controller), 

385 on_change=self.onNBChanged, 

386 size=(700, 700)) 

387 

388 sizer.Add(self.nb, 1, LEFT|wx.EXPAND, 2) 

389 panel.SetupScrolling() 

390 

391 pack(panel, sizer) 

392 splitter.SplitVertically(leftpanel, panel, 1) 

393 

394 def process_normalization(self, dgroup, force=True, use_form=True): 

395 self.get_nbpage('xasnorm')[1].process(dgroup, force=force, use_form=use_form) 

396 

397 def process_exafs(self, dgroup, force=True): 

398 self.get_nbpage('exafs')[1].process(dgroup, force=force) 

399 

400 def get_nbpage(self, name): 

401 "get nb page by name" 

402 name = name.lower() 

403 out = (0, self.nb.GetCurrentPage()) 

404 for i, page in enumerate(self.nb.pagelist): 

405 if name in page.__class__.__name__.lower(): 

406 out = (i, page) 

407 return out 

408 

409 def onNBChanged(self, event=None): 

410 callback = getattr(self.nb.GetCurrentPage(), 'onPanelExposed', None) 

411 if callable(callback): 

412 callback() 

413 

414 def onSelAll(self, event=None): 

415 self.controller.filelist.select_all() 

416 

417 def onSelNone(self, event=None): 

418 self.controller.filelist.select_none() 

419 

420 def write_message(self, msg, panel=0): 

421 """write a message to the Status Bar""" 

422 self.statusbar.SetStatusText(msg, panel) 

423 

424 def RemoveFile(self, fname=None, **kws): 

425 if fname is not None: 

426 s = str(fname) 

427 if s in self.controller.file_groups: 

428 group = self.controller.file_groups.pop(s) 

429 self.controller.sync_xasgroups() 

430 

431 def ShowFile(self, evt=None, groupname=None, process=True, 

432 filename=None, plot=True, **kws): 

433 if filename is None and evt is not None: 

434 filename = str(evt.GetString()) 

435 

436 if groupname is None and filename is not None: 

437 groupname = self.controller.file_groups[filename] 

438 

439 if not hasattr(self.larch.symtable, groupname): 

440 return 

441 

442 dgroup = self.controller.get_group(groupname) 

443 if dgroup is None: 

444 return 

445 

446 if (getattr(dgroup, 'datatype', 'raw').startswith('xa') and not 

447 (hasattr(dgroup, 'norm') and hasattr(dgroup, 'e0'))): 

448 self.process_normalization(dgroup, force=True, use_form=False) 

449 

450 if filename is None: 

451 filename = dgroup.filename 

452 self.current_filename = filename 

453 journal = getattr(dgroup, 'journal', Journal(source_desc=filename)) 

454 if isinstance(journal, Journal): 

455 sdesc = journal.get('source_desc', latest=True) 

456 else: 

457 sdesc = journal.get('source_desc', '?') 

458 

459 if isinstance(sdesc, Entry): 

460 sdesc = sdesc.value 

461 if not isinstance(sdesc, str): 

462 sdesc = repr(sdesc) 

463 self.title.SetLabel(sdesc) 

464 

465 self.controller.group = dgroup 

466 self.controller.groupname = groupname 

467 cur_panel = self.nb.GetCurrentPage() 

468 if process: 

469 cur_panel.fill_form(dgroup) 

470 cur_panel.skip_process = False 

471 cur_panel.process(dgroup=dgroup) 

472 if plot and hasattr(cur_panel, 'plot'): 

473 cur_panel.plot(dgroup=dgroup) 

474 cur_panel.skip_process = False 

475 

476 self.controller.filelist.SetStringSelection(filename) 

477 

478 

479 def createMenus(self): 

480 # ppnl = self.plotpanel 

481 self.menubar = wx.MenuBar() 

482 fmenu = wx.Menu() 

483 group_menu = wx.Menu() 

484 data_menu = wx.Menu() 

485 feff_menu = wx.Menu() 

486 m = {} 

487 

488 MenuItem(self, fmenu, "&Open Data File\tCtrl+O", 

489 "Open Data File", self.onReadDialog) 

490 

491 MenuItem(self, fmenu, "&Read Larch Session\tCtrl+R", 

492 "Read Previously Saved Session", self.onLoadSession) 

493 

494 MenuItem(self, fmenu, "&Save Larch Session\tCtrl+S", 

495 "Save Session to a File", self.onSaveSession) 

496 

497 MenuItem(self, fmenu, "&Save Larch Session As ...\tCtrl+A", 

498 "Save Session to a File", self.onSaveSessionAs) 

499 

500 MenuItem(self, fmenu, "Clear Larch Session", 

501 "Clear all data from this Session", self.onClearSession) 

502 

503 # autosaved session 

504 conf = self.controller.get_config('autosave', 

505 {'fileroot': 'autosave'}) 

506 froot= conf['fileroot'] 

507 

508 recent_menu = wx.Menu() 

509 for tstamp, fname in self.controller.get_recentfiles(): 

510 MenuItem(self, recent_menu, 

511 "%s [%s ago]" % (fname, time_ago(tstamp)), 

512 f"file saved {isotime(tstamp)}", 

513 partial(self.onLoadSession, path=fname)) 

514 

515 recent_menu.AppendSeparator() 

516 for tstamp, fname in self.controller.recent_autosave_sessions(): 

517 MenuItem(self, recent_menu, 

518 "%s [%s ago]" % (fname, time_ago(tstamp)), 

519 f"file saved {isotime(tstamp)}", 

520 partial(self.onLoadSession, path=fname)) 

521 

522 fmenu.Append(-1, 'Recent Session Files', recent_menu) 

523 

524 

525 MenuItem(self, fmenu, "&Auto-Save Larch Session", 

526 f"Save Session now", self.autosave_session) 

527 fmenu.AppendSeparator() 

528 

529 MenuItem(self, fmenu, "Save Selected Groups to Athena Project File", 

530 "Save Selected Groups to an Athena Project File", 

531 self.onExportAthenaProject) 

532 

533 MenuItem(self, fmenu, "Save Selected Groups to CSV File", 

534 "Save Selected Groups to a CSV File", 

535 self.onExportCSV) 

536 

537 MenuItem(self, fmenu, 'Save Larch History as Script\tCtrl+H', 

538 'Save Session History as Larch Script', 

539 self.onSaveLarchHistory) 

540 

541 fmenu.AppendSeparator() 

542 

543 MenuItem(self, fmenu, 'Show Larch Buffer\tCtrl+L', 

544 'Show Larch Programming Buffer', 

545 self.onShowLarchBuffer) 

546 

547 MenuItem(self, fmenu, 'wxInspect\tCtrl+I', 

548 'Show wx inspection window', self.onwxInspect) 

549 

550 MenuItem(self, fmenu, 'Edit Preferences\tCtrl+E', 'Customize Preferences', 

551 self.onPreferences) 

552 

553 MenuItem(self, fmenu, "&Quit\tCtrl+Q", "Quit program", self.onClose) 

554 

555 

556 MenuItem(self, group_menu, "Copy This Group", 

557 "Copy This Group", self.onCopyGroup) 

558 

559 MenuItem(self, group_menu, "Rename This Group", 

560 "Rename This Group", self.onRenameGroup) 

561 

562 MenuItem(self, group_menu, "Show Journal for This Group", 

563 "Show Processing Journal for This Group", self.onGroupJournal) 

564 

565 

566 group_menu.AppendSeparator() 

567 

568 MenuItem(self, group_menu, "Remove Selected Groups", 

569 "Remove Selected Group", self.onRemoveGroups) 

570 

571 group_menu.AppendSeparator() 

572 

573 MenuItem(self, group_menu, "Merge Selected Groups", 

574 "Merge Selected Groups", self.onMergeData) 

575 

576 group_menu.AppendSeparator() 

577 

578 MenuItem(self, group_menu, "Freeze Selected Groups", 

579 "Freeze Selected Groups", self.onFreezeGroups) 

580 

581 MenuItem(self, group_menu, "UnFreeze Selected Groups", 

582 "UnFreeze Selected Groups", self.onUnFreezeGroups) 

583 

584 MenuItem(self, data_menu, "Deglitch Data", "Deglitch Data", 

585 self.onDeglitchData) 

586 

587 MenuItem(self, data_menu, "Calibrate Energy", 

588 "Calibrate Energy", 

589 self.onEnergyCalibrateData) 

590 

591 MenuItem(self, data_menu, "Smooth Data", "Smooth Data", 

592 self.onSmoothData) 

593 

594 MenuItem(self, data_menu, "Deconvolve Data", 

595 "Deconvolution of Data", self.onDeconvolveData) 

596 

597 MenuItem(self, data_menu, "Rebin Data", "Rebin Data", 

598 self.onRebinData) 

599 

600 MenuItem(self, data_menu, "Correct Over-absorption", 

601 "Correct Over-absorption", 

602 self.onCorrectOverAbsorptionData) 

603 

604 MenuItem(self, data_menu, "Add and Subtract Spectra", 

605 "Calculations of Spectra", self.onSpectraCalc) 

606 

607 

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

609 self.menubar.Append(group_menu, "Groups") 

610 self.menubar.Append(data_menu, "Data") 

611 

612 MenuItem(self, feff_menu, "Browse CIF Structures, Run Feff", 

613 "Browse CIF Structure, run Feff", self.onCIFBrowse) 

614 MenuItem(self, feff_menu, "Generate Feff input from general structures, Run Feff", 

615 "Generate Feff input from general structures, run Feff", self.onStructureBrowse) 

616 MenuItem(self, feff_menu, "Browse Feff Calculations", 

617 "Browse Feff Calculations, Get Feff Paths", self.onFeffBrowse) 

618 

619 self.menubar.Append(feff_menu, "Feff") 

620 

621 hmenu = wx.Menu() 

622 MenuItem(self, hmenu, 'About Larix', 'About Larix', 

623 self.onAbout) 

624 MenuItem(self, hmenu, 'Check for Updates', 'Check for Updates', 

625 self.onCheckforUpdates) 

626 

627 self.menubar.Append(hmenu, '&Help') 

628 self.SetMenuBar(self.menubar) 

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

630 

631 def onwxInspect(self, evt=None): 

632 wx.GetApp().ShowInspectionTool() 

633 

634 def onShowLarchBuffer(self, evt=None): 

635 if self.larch_buffer is None: 

636 self.larch_buffer = LarchFrame(_larch=self.larch, is_standalone=False) 

637 self.larch_buffer.Show() 

638 self.larch_buffer.Raise() 

639 

640 def onSaveLarchHistory(self, evt=None): 

641 wildcard = 'Larch file (*.lar)|*.lar|All files (*.*)|*.*' 

642 path = FileSave(self, message='Save Session History as Larch Script', 

643 wildcard=wildcard, 

644 default_file='larix_history.lar') 

645 if path is not None: 

646 self.larch._larch.input.history.save(path, session_only=True) 

647 self.write_message("Wrote history %s" % path, 0) 

648 

649 def onExportCSV(self, evt=None): 

650 filenames = self.controller.filelist.GetCheckedStrings() 

651 if len(filenames) < 1: 

652 Popup(self, "No files selected to export to CSV", 

653 "No files selected") 

654 return 

655 

656 deffile = "%s_%i.csv" % (filenames[0], len(filenames)) 

657 

658 dlg = ExportCSVDialog(self, filenames) 

659 res = dlg.GetResponse() 

660 

661 dlg.Destroy() 

662 if not res.ok: 

663 return 

664 

665 deffile = f"{filenames[0]:s}_{len(filenames):d}.csv" 

666 wcards = 'CSV Files (*.csv)|*.csv|All files (*.*)|*.*' 

667 

668 outfile = FileSave(self, 'Save Groups to CSV File', 

669 default_file=deffile, wildcard=wcards) 

670 

671 if outfile is None: 

672 return 

673 if os.path.exists(outfile) and uname != 'darwin': # darwin prompts in FileSave! 

674 if wx.ID_YES != Popup(self, 

675 "Overwrite existing CSV File?", 

676 "Overwrite existing file?", style=wx.YES_NO): 

677 return 

678 

679 savegroups = [self.controller.filename2group(res.master)] 

680 for fname in filenames: 

681 dgroup = self.controller.filename2group(fname) 

682 if dgroup not in savegroups: 

683 savegroups.append(dgroup) 

684 

685 try: 

686 groups2csv(savegroups, outfile, x=res.xarray, y=res.yarray, 

687 delim=res.delim, individual=res.individual) 

688 self.write_message(f"Exported CSV file {outfile:s}") 

689 except: 

690 title = "Could not export CSV File" 

691 message = [f"Could not export CSV File {outfile}"] 

692 ExceptionPopup(self, title, message) 

693 

694 # Athena 

695 def onExportAthenaProject(self, evt=None): 

696 groups = [] 

697 self.controller.sync_xasgroups() 

698 for checked in self.controller.filelist.GetCheckedStrings(): 

699 groups.append(self.controller.file_groups[str(checked)]) 

700 

701 if len(groups) < 1: 

702 Popup(self, "No files selected to export to Project", 

703 "No files selected") 

704 return 

705 prompt, prjfile = self.get_athena_project() 

706 self.save_athena_project(prjfile, groups) 

707 

708 def get_athena_project(self): 

709 prjfile = self.last_athena_file 

710 prompt = False 

711 if prjfile is None: 

712 tstamp = isotime(filename=True)[:15] 

713 prjfile = f"{tstamp:s}.prj" 

714 prompt = True 

715 return prompt, prjfile 

716 

717 def onSaveAthenaProject(self, evt=None): 

718 groups = self.controller.filelist.GetItems() 

719 if len(groups) < 1: 

720 Popup(self, "No files to export to Project", "No files to export") 

721 return 

722 

723 prompt, prjfile = self.get_athenaproject() 

724 self.save_athena_project(prjfile, groups, prompt=prompt, 

725 warn_overwrite=False) 

726 

727 def onSaveAsAthenaProject(self, evt=None): 

728 groups = self.controller.filelist.GetItems() 

729 if len(groups) < 1: 

730 Popup(self, "No files to export to Project", "No files to export") 

731 return 

732 

733 prompt, prjfile = self.get_athena_project() 

734 self.save_athena_project(prjfile, groups) 

735 

736 def save_athena_project(self, filename, grouplist, prompt=True, 

737 warn_overwrite=True): 

738 if len(grouplist) < 1: 

739 return 

740 savegroups = [self.controller.get_group(gname) for gname in grouplist] 

741 if prompt: 

742 _, filename = os.path.split(filename) 

743 wcards = 'Project Files (*.prj)|*.prj|All files (*.*)|*.*' 

744 filename = FileSave(self, 'Save Groups to Project File', 

745 default_file=filename, wildcard=wcards) 

746 if filename is None: 

747 return 

748 

749 if os.path.exists(filename) and warn_overwrite and uname != 'darwin': # darwin prompts in FileSave! 

750 if wx.ID_YES != Popup(self, 

751 "Overwrite existing Project File?", 

752 "Overwrite existing file?", style=wx.YES_NO): 

753 return 

754 

755 aprj = AthenaProject(filename=filename) 

756 for label, grp in zip(grouplist, savegroups): 

757 aprj.add_group(grp) 

758 aprj.save(use_gzip=True) 

759 self.write_message("Saved project file %s" % (filename)) 

760 self.last_athena_file = filename 

761 

762 

763 def onPreferences(self, evt=None): 

764 self.show_subframe('preferences', PreferencesFrame, 

765 controller=self.controller) 

766 

767 def onLoadSession(self, evt=None, path=None): 

768 if path is None: 

769 wildcard = 'Larch Session File (*.larix)|*.larix|All files (*.*)|*.*' 

770 path = FileOpen(self, message="Load Larch Session", 

771 wildcard=wildcard, default_file='larch.larix') 

772 if path is None: 

773 return 

774 

775 if is_athena_project(path): 

776 self.show_subframe('athena_import', AthenaImporter, 

777 controller=self.controller, filename=path, 

778 read_ok_cb=self.onReadAthenaProject_OK) 

779 return 

780 

781 try: 

782 _session = read_session(path) 

783 except: 

784 title = "Invalid Path for Larch Session" 

785 message = [f"{path} is not a valid Larch Session File"] 

786 ExceptionPopup(self, title, message) 

787 return 

788 

789 LoadSessionDialog(self, _session, path, self.controller).Show() 

790 self.last_session_read = path 

791 fdir, fname = os.path.split(path) 

792 if self.controller.chdir_on_fileopen() and len(fdir) > 0: 

793 os.chdir(fdir) 

794 self.controller.set_workdir() 

795 

796 def onSaveSessionAs(self, evt=None): 

797 groups = self.controller.filelist.GetItems() 

798 if len(groups) < 1: 

799 return 

800 self.last_session_file = None 

801 self.onSaveSession() 

802 

803 

804 def onSaveSession(self, evt=None): 

805 groups = self.controller.filelist.GetItems() 

806 if len(groups) < 1: 

807 return 

808 

809 fname = self.last_session_file 

810 if fname is None: 

811 fname = self.last_session_read 

812 if fname is None: 

813 fname = time.strftime('%Y%b%d_%H%M') + '.larix' 

814 

815 _, fname = os.path.split(fname) 

816 wcards = 'Larch Project Files (*.larix)|*.larix|All files (*.*)|*.*' 

817 fname = FileSave(self, 'Save Larch Session File', 

818 default_file=fname, wildcard=wcards) 

819 if fname is None: 

820 return 

821 

822 if os.path.exists(fname) and uname != 'darwin': # darwin prompts in FileSave! 

823 if wx.ID_YES != Popup(self, "Overwrite existing Project File?", 

824 "Overwrite existing file?", style=wx.YES_NO): 

825 return 

826 

827 save_session(fname=fname, _larch=self.larch._larch) 

828 stime = time.strftime("%H:%M") 

829 self.last_save_message = ("Session last saved", f"'{fname}'", f"{stime}") 

830 self.write_message(f"Saved session to '{fname}' at {stime}") 

831 self.last_session_file = self.last_session_read = fname 

832 

833 def onClearSession(self, evt=None): 

834 conf = self.controller.get_config('autosave', 

835 {'fileroot': 'autosave'}) 

836 afile = os.path.join(self.controller.larix_folder, 

837 conf['fileroot']+'.larix') 

838 

839 msg = f"""Session will be saved to 

840 '{afile:s}' 

841before clearing""" 

842 

843 dlg = wx.Dialog(None, -1, title="Clear all Session data?", size=(550, 300)) 

844 dlg.SetFont(Font(FONTSIZE)) 

845 panel = GridPanel(dlg, ncols=3, nrows=4, pad=2, itemstyle=LEFT) 

846 

847 panel.Add(wx.StaticText(panel, label="Clear all Session Data?"), dcol=2) 

848 panel.Add(wx.StaticText(panel, label=msg), dcol=4, newrow=True) 

849 

850 panel.Add((5, 5) , newrow=True) 

851 panel.Add((5, 5), newrow=True) 

852 panel.Add(OkCancel(panel), dcol=2, newrow=True) 

853 panel.pack() 

854 

855 fit_dialog_window(dlg, panel) 

856 

857 

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

859 self.autosave_session() 

860 self.controller.clear_session() 

861 dlg.Destroy() 

862 

863 

864 def onConfigDataProcessing(self, event=None): 

865 pass 

866 

867 

868 def onCopyGroup(self, event=None, journal=None): 

869 fname = self.current_filename 

870 if fname is None: 

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

872 ogroup = self.controller.get_group(fname) 

873 ngroup = self.controller.copy_group(fname) 

874 self.install_group(ngroup, journal=ogroup.journal) 

875 

876 def onGroupJournal(self, event=None): 

877 dgroup = self.controller.get_group() 

878 if dgroup is not None: 

879 self.show_subframe('group_journal', GroupJournalFrame, xasmain=self) 

880 self.subframes['group_journal'].set_group(dgroup) 

881 

882 

883 def onRenameGroup(self, event=None): 

884 fname = self.current_filename = self.controller.filelist.GetStringSelection() 

885 if fname is None: 

886 return 

887 dlg = RenameDialog(self, fname) 

888 res = dlg.GetResponse() 

889 dlg.Destroy() 

890 

891 if res.ok: 

892 selected = [] 

893 for checked in self.controller.filelist.GetCheckedStrings(): 

894 selected.append(str(checked)) 

895 if self.current_filename in selected: 

896 selected.remove(self.current_filename) 

897 selected.append(res.newname) 

898 

899 groupname = self.controller.file_groups.pop(fname) 

900 self.controller.sync_xasgroups() 

901 self.controller.file_groups[res.newname] = groupname 

902 self.controller.filelist.rename_item(self.current_filename, res.newname) 

903 dgroup = self.controller.get_group(groupname) 

904 dgroup.filename = self.current_filename = res.newname 

905 

906 self.controller.filelist.SetCheckedStrings(selected) 

907 self.controller.filelist.SetStringSelection(res.newname) 

908 

909 def onRemoveGroup(self, event=None): 

910 n = int(self.controller.filelist.GetSelection()) 

911 all_names = self.controller.filelist.GetItems() 

912 fname = all_names[n] 

913 

914 do_remove = (wx.ID_YES == Popup(self, 

915 f"Remove Group '{fname}'?", 

916 'Remove Group? Cannot be undone!', 

917 style=wx.YES_NO)) 

918 if do_remove: 

919 fname = all_names.pop(n) 

920 self.controller.filelist.refresh(all_names) 

921 self.RemoveFile(fname) 

922 

923 

924 def onRemoveGroups(self, event=None): 

925 groups = [] 

926 for checked in self.controller.filelist.GetCheckedStrings(): 

927 groups.append(str(checked)) 

928 if len(groups) < 1: 

929 return 

930 

931 dlg = RemoveDialog(self, groups) 

932 res = dlg.GetResponse() 

933 dlg.Destroy() 

934 

935 if res.ok: 

936 filelist = self.controller.filelist 

937 all_fnames = filelist.GetItems() 

938 for fname in groups: 

939 gname = self.controller.file_groups.pop(fname) 

940 delattr(self.controller.symtable, gname) 

941 all_fnames.remove(fname) 

942 

943 filelist.Clear() 

944 for name in all_fnames: 

945 filelist.Append(name) 

946 self.controller.sync_xasgroups() 

947 

948 def onFreezeGroups(self, event=None): 

949 self._freeze_handler(True) 

950 

951 def onUnFreezeGroups(self, event=None): 

952 self._freeze_handler(False) 

953 

954 def _freeze_handler(self, freeze): 

955 current_filename = self.current_filename 

956 reproc_group = None 

957 for fname in self.controller.filelist.GetCheckedStrings(): 

958 groupname = self.controller.file_groups[str(fname)] 

959 dgroup = self.controller.get_group(groupname) 

960 if fname == current_filename: 

961 reproc_group = groupname 

962 dgroup.is_frozen = freeze 

963 if reproc_group is not None: 

964 self.ShowFile(groupname=reproc_group, process=True) 

965 

966 def onMergeData(self, event=None): 

967 groups = {} 

968 for checked in self.controller.filelist.GetCheckedStrings(): 

969 cname = str(checked) 

970 groups[cname] = self.controller.file_groups[cname] 

971 if len(groups) < 1: 

972 return 

973 

974 outgroup = common_startstring(list(groups.keys())) 

975 if len(outgroup) < 2: outgroup = "data" 

976 outgroup = "%s (merge %d)" % (outgroup, len(groups)) 

977 outgroup = unique_name(outgroup, self.controller.file_groups) 

978 dlg = MergeDialog(self, list(groups.keys()), outgroup=outgroup) 

979 res = dlg.GetResponse() 

980 dlg.Destroy() 

981 if res.ok: 

982 fname = res.group 

983 gname = fix_varname(res.group.lower()) 

984 master = self.controller.file_groups[res.master] 

985 yname = 'norm' if res.ynorm else 'mu' 

986 this = self.controller.merge_groups(list(groups.values()), 

987 master=master, 

988 yarray=yname, 

989 outgroup=gname) 

990 

991 mfiles, mgroups = [], [] 

992 for g in groups.values(): 

993 mgroups.append(g) 

994 mfiles.append(self.controller.get_group(g).filename) 

995 mfiles = '[%s]' % (', '.join(mfiles)) 

996 mgroups = '[%s]' % (', '.join(mgroups)) 

997 desc = "%s: merge of %d groups" % (fname, len(groups)) 

998 self.install_group(gname, fname, source=desc, 

999 journal={'source_desc': desc, 

1000 'merged_groups': mgroups, 

1001 'merged_filenames': mfiles}) 

1002 

1003 def has_datagroup(self): 

1004 return hasattr(self.controller.get_group(), 'energy') 

1005 

1006 def onDeglitchData(self, event=None): 

1007 if self.has_datagroup(): 

1008 DeglitchDialog(self, self.controller).Show() 

1009 

1010 def onSmoothData(self, event=None): 

1011 if self.has_datagroup(): 

1012 SmoothDataDialog(self, self.controller).Show() 

1013 

1014 def onRebinData(self, event=None): 

1015 if self.has_datagroup(): 

1016 RebinDataDialog(self, self.controller).Show() 

1017 

1018 def onCorrectOverAbsorptionData(self, event=None): 

1019 if self.has_datagroup(): 

1020 OverAbsorptionDialog(self, self.controller).Show() 

1021 

1022 def onSpectraCalc(self, event=None): 

1023 if self.has_datagroup(): 

1024 SpectraCalcDialog(self, self.controller).Show() 

1025 

1026 def onEnergyCalibrateData(self, event=None): 

1027 if self.has_datagroup(): 

1028 EnergyCalibrateDialog(self, self.controller).Show() 

1029 

1030 def onDeconvolveData(self, event=None): 

1031 if self.has_datagroup(): 

1032 DeconvolutionDialog(self, self.controller).Show() 

1033 

1034 def onConfigDataFitting(self, event=None): 

1035 pass 

1036 

1037 def onAbout(self, event=None): 

1038 info = AboutDialogInfo() 

1039 info.SetName('Larix') 

1040 info.SetDescription('X-ray Absorption Visualization and Analysis') 

1041 info.SetVersion('Larch %s ' % larch.version.__version__) 

1042 info.AddDeveloper('Matthew Newville: newville at cars.uchicago.edu') 

1043 dlg = AboutBox(info) 

1044 

1045 def onCheckforUpdates(self, event=None): 

1046 dlg = LarchUpdaterDialog(self, caller='Larix') 

1047 dlg.Raise() 

1048 dlg.SetWindowStyle(wx.STAY_ON_TOP) 

1049 res = dlg.GetResponse() 

1050 dlg.Destroy() 

1051 if res.ok and res.run_updates: 

1052 from larch.apps import update_larch 

1053 update_larch() 

1054 self.onClose(event=event, prompt=False) 

1055 

1056 def onClose(self, event=None, prompt=True): 

1057 if prompt: 

1058 dlg = QuitDialog(self, self.last_save_message) 

1059 dlg.Raise() 

1060 dlg.SetWindowStyle(wx.STAY_ON_TOP) 

1061 res = dlg.GetResponse() 

1062 dlg.Destroy() 

1063 if not res.ok: 

1064 return 

1065 

1066 self.controller.save_workdir() 

1067 try: 

1068 self.controller.close_all_displays() 

1069 except Exception: 

1070 pass 

1071 

1072 if self.larch_buffer is not None: 

1073 try: 

1074 self.larch_buffer.Destroy() 

1075 except Exception: 

1076 pass 

1077 

1078 def destroy(wid): 

1079 if hasattr(wid, 'Destroy'): 

1080 try: 

1081 wid.Destroy() 

1082 except Exception: 

1083 pass 

1084 time.sleep(0.01) 

1085 

1086 for name, wid in self.subframes.items(): 

1087 destroy(wid) 

1088 

1089 for i in range(self.nb.GetPageCount()): 

1090 nbpage = self.nb.GetPage(i) 

1091 timers = getattr(nbpage, 'timers', None) 

1092 if timers is not None: 

1093 for t in timers.values(): 

1094 t.Stop() 

1095 

1096 if hasattr(nbpage, 'subframes'): 

1097 for name, wid in nbpage.subframes.items(): 

1098 destroy(wid) 

1099 for t in self.timers.values(): 

1100 t.Stop() 

1101 

1102 time.sleep(0.05) 

1103 self.Destroy() 

1104 

1105 def show_subframe(self, name, frameclass, **opts): 

1106 shown = False 

1107 if name in self.subframes: 

1108 try: 

1109 self.subframes[name].Raise() 

1110 shown = True 

1111 except: 

1112 del self.subframes[name] 

1113 if not shown: 

1114 self.subframes[name] = frameclass(self, **opts) 

1115 

1116 

1117 def onCIFBrowse(self, event=None): 

1118 self.show_subframe('cif_feff', CIFFrame, _larch=self.larch, 

1119 path_importer=self.get_nbpage('feffit')[1].add_path, 

1120 with_feff=True) 

1121 

1122 def onStructureBrowse(self, event=None): 

1123 self.show_subframe('structure_feff', Structure2FeffFrame, _larch=self.larch, 

1124 path_importer=self.get_nbpage('feffit')[1].add_path) 

1125 

1126 def onFeffBrowse(self, event=None): 

1127 self.show_subframe('feff_paths', FeffResultsFrame, _larch=self.larch, 

1128 path_importer=self.get_nbpage('feffit')[1].add_path) 

1129 

1130 def onLoadFitResult(self, event=None): 

1131 pass 

1132 # print("onLoadFitResult??") 

1133 # self.nb.SetSelection(1) 

1134 # self.nb_panels[1].onLoadFitResult(event=event) 

1135 

1136 def onReadDialog(self, event=None): 

1137 dlg = wx.FileDialog(self, message="Read Data File", 

1138 defaultDir=get_cwd(), 

1139 wildcard=FILE_WILDCARDS, 

1140 style=wx.FD_OPEN|wx.FD_MULTIPLE) 

1141 self.paths2read = [] 

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

1143 self.paths2read = dlg.GetPaths() 

1144 dlg.Destroy() 

1145 

1146 if len(self.paths2read) < 1: 

1147 return 

1148 

1149 def file_mtime(x): 

1150 return os.stat(x).st_mtime 

1151 

1152 self.paths2read = sorted(self.paths2read, key=file_mtime) 

1153 

1154 path = self.paths2read.pop(0) 

1155 path = path.replace('\\', '/') 

1156 do_read = True 

1157 if path in self.controller.file_groups: 

1158 do_read = (wx.ID_YES == Popup(self, 

1159 "Re-read file '%s'?" % path, 

1160 'Re-read file?')) 

1161 if do_read: 

1162 self.onRead(path) 

1163 

1164 def onRead(self, path): 

1165 filedir, filename = os.path.split(os.path.abspath(path)) 

1166 if self.controller.chdir_on_fileopen() and len(filedir) > 0: 

1167 os.chdir(filedir) 

1168 self.controller.set_workdir() 

1169 

1170 # check for athena projects 

1171 if is_athena_project(path): 

1172 self.show_subframe('athena_import', AthenaImporter, 

1173 controller=self.controller, filename=path, 

1174 read_ok_cb=self.onReadAthenaProject_OK) 

1175 # check for Spec File 

1176 elif is_specfile(path): 

1177 self.show_subframe('spec_import', SpecfileImporter, 

1178 filename=path, 

1179 _larch=self.larch_buffer.larchshell, 

1180 config=self.last_spec_config, 

1181 read_ok_cb=self.onReadSpecfile_OK) 

1182 # check for Larch Session File 

1183 elif is_larch_session_file(path): 

1184 self.onLoadSession(path=path) 

1185 # default to Column File 

1186 else: 

1187 self.show_subframe('readfile', ColumnDataFileFrame, filename=path, 

1188 config=self.last_col_config, 

1189 _larch=self.larch_buffer.larchshell, 

1190 read_ok_cb=self.onRead_OK) 

1191 

1192 def onReadSpecfile_OK(self, script, path, scanlist, config=None): 

1193 """read groups from a list of scans from a specfile""" 

1194 self.larch.eval("_specfile = specfile('{path:s}')".format(path=path)) 

1195 dgroup = None 

1196 _path, fname = os.path.split(path) 

1197 first_group = None 

1198 cur_panel = self.nb.GetCurrentPage() 

1199 cur_panel.skip_plotting = True 

1200 symtable = self.larch.symtable 

1201 if config is not None: 

1202 self.last_spec_config = config 

1203 

1204 array_desc = config.get('array_desc', {}) 

1205 

1206 multiconfig = config.get('multicol_config', {'channels':[], 'i0': config['iy2']}) 

1207 multi_i0 = multiconfig.get('i0', config['iy2']) 

1208 multi_chans = copy.copy(multiconfig.get('channels', [])) 

1209 

1210 if len(multi_chans) > 0: 

1211 if (multi_chans[0] == config['iy1'] and multi_i0 == config['iy2'] 

1212 and 'log' not in config['expressions']['ydat']): 

1213 yname = config['array_labels'][config['iy1']] 

1214 # filename = f"{spath}:{yname}" 

1215 multi_chans.pop(0) 

1216 

1217 for scan in scanlist: 

1218 gname = fix_varname("{:s}{:s}".format(fname[:6], scan)) 

1219 if hasattr(symtable, gname): 

1220 count, tname = 0, gname 

1221 while count < 1e7 and self.larch.symtable.has_group(tname): 

1222 tname = gname + make_hashkey(length=7) 

1223 count += 1 

1224 gname = tname 

1225 

1226 cur_panel.skip_plotting = (scan == scanlist[-1]) 

1227 yname = config['yarr1'] 

1228 if first_group is None: 

1229 first_group = gname 

1230 cmd = script.format(group=gname, specfile='_specfile', 

1231 path=path, scan=scan, **config) 

1232 

1233 self.larch.eval(cmd) 

1234 displayname = f"{fname} scan{scan} {yname}" 

1235 jrnl = {'source_desc': f"{fname}: scan{scan} {yname}"} 

1236 dgroup = self.install_group(gname, displayname, journal=jrnl) 

1237 if len(multi_chans) > 0: 

1238 ydatline = None 

1239 for line in script.split('\n'): 

1240 if line.startswith("{group}.ydat ="): 

1241 ydatline = line.replace("{group}", "{ngroup}") 

1242 mscript = '\n'.join(["{ngroup} = deepcopy({group})", 

1243 ydatline, 

1244 "{ngroup}.mu = {ngroup}.ydat", 

1245 "{ngroup}.plot_ylabel = '{ylabel}'"]) 

1246 i0 = '1.0' 

1247 if multi_i0 < len(config['array_labels']): 

1248 i0 = config['array_labels'][multi_i0] 

1249 

1250 for mchan in multi_chans: 

1251 yname = config['array_labels'][mchan] 

1252 ylabel = f"{yname}/{i0}" 

1253 dname = f"{fname} scan{scan} {yname}" 

1254 ngroup = file2groupname(dname, symtable=self.larch.symtable) 

1255 njournal = {'source': path, 

1256 'xdat': array_desc['xdat'].format(group=ngroup), 

1257 'ydat': ylabel, 

1258 'source_desc': f"{fname}: scan{scan} {yname}", 

1259 'yerr': array_desc['yerr'].format(group=ngroup)} 

1260 cmd = mscript.format(group=gname, ngroup=ngroup, 

1261 iy1=mchan, iy2=multi_i0, ylabel=ylabel) 

1262 self.larch.eval(cmd) 

1263 self.install_group(ngroup, dname, source=path, journal=njournal) 

1264 

1265 

1266 cur_panel.skip_plotting = False 

1267 

1268 if first_group is not None: 

1269 self.ShowFile(groupname=first_group, process=True, plot=True) 

1270 self.write_message("read %d datasets from %s" % (len(scanlist), path)) 

1271 self.larch.eval('del _specfile') 

1272 

1273 

1274 def onReadAthenaProject_OK(self, path, namelist): 

1275 """read groups from a list of groups from an athena project file""" 

1276 self.larch.eval("_prj = read_athena('{path:s}', do_fft=False, do_bkg=False)".format(path=path)) 

1277 dgroup = None 

1278 script = "{group:s} = extract_athenagroup(_prj.{prjgroup:s})" 

1279 cur_panel = self.nb.GetCurrentPage() 

1280 cur_panel.skip_plotting = True 

1281 parent, spath = os.path.split(path) 

1282 labels = [] 

1283 groups_added = [] 

1284 

1285 for ig, gname in enumerate(namelist): 

1286 cur_panel.skip_plotting = (gname == namelist[-1]) 

1287 this = getattr(self.larch.symtable._prj, gname) 

1288 gid = file2groupname(str(getattr(this, 'athena_id', gname)), 

1289 symtable=self.larch.symtable) 

1290 if self.larch.symtable.has_group(gid): 

1291 count, prefix = 0, gname[:3] 

1292 while count < 1e7 and self.larch.symtable.has_group(gid): 

1293 gid = prefix + make_hashkey(length=7) 

1294 count += 1 

1295 label = getattr(this, 'label', gname).strip() 

1296 labels.append(label) 

1297 

1298 jrnl = {'source_desc': f'{spath:s}: {gname:s}'} 

1299 self.larch.eval(script.format(group=gid, prjgroup=gname)) 

1300 dgroup = self.install_group(gid, label, process=False, 

1301 source=path, journal=jrnl) 

1302 groups_added.append(gid) 

1303 

1304 for gid in groups_added: 

1305 rgroup = gid 

1306 dgroup = self.larch.symtable.get_group(gid) 

1307 

1308 conf_xasnorm = dgroup.config.xasnorm 

1309 conf_exafs= dgroup.config.exafs 

1310 

1311 apars = getattr(dgroup, 'athena_params', {}) 

1312 abkg = getattr(apars, 'bkg', {}) 

1313 afft = getattr(apars, 'fft', {}) 

1314 

1315 # norm 

1316 for attr in ('e0', 'pre1', 'pre2', 'nnorm'): 

1317 if hasattr(abkg, attr): 

1318 conf_xasnorm[attr] = float(getattr(abkg, attr)) 

1319 

1320 for attr, alt in (('norm1', 'nor1'), ('norm2', 'nor2'), 

1321 ('edge_step', 'step')): 

1322 if hasattr(abkg, alt): 

1323 conf_xasnorm[attr] = float(getattr(abkg, alt)) 

1324 if hasattr(abkg, 'fixstep'): 

1325 a = float(getattr(abkg, 'fixstep', 0.0)) 

1326 conf_xasnorm['auto_step'] = (a < 0.5) 

1327 

1328 

1329 # bkg 

1330 for attr in ('e0', 'rbkg'): 

1331 if hasattr(abkg, attr): 

1332 conf_exafs[attr] = float(getattr(abkg, attr)) 

1333 

1334 for attr, alt in (('bkg_kmin', 'spl1'), ('bkg_kmax', 'spl2'), 

1335 ('bkg_kweight', 'kw'), ('bkg_clamplo', 'clamp1'), 

1336 ('bkg_clamphi', 'clamp2')): 

1337 if hasattr(abkg, alt): 

1338 val = getattr(abkg, alt) 

1339 try: 

1340 val = float(getattr(abkg, alt)) 

1341 except: 

1342 if alt.startswith('clamp') and isinstance(val, str): 

1343 val = ATHENA_CLAMPNAMES.get(val.lower(), 0) 

1344 conf_exafs[attr] = val 

1345 

1346 

1347 # fft 

1348 for attr in ('kmin', 'kmax', 'dk', 'kwindow', 'kw'): 

1349 if hasattr(afft, attr): 

1350 n = f'fft_{attr}' 

1351 if attr == 'kw': n = 'fft_kweight' 

1352 if attr == 'kwindow': 

1353 conf_exafs[n] = getattr(afft, attr) 

1354 else: 

1355 conf_exafs[n] = float(getattr(afft, attr)) 

1356 

1357 # reference 

1358 refgroup = getattr(apars, 'reference', '') 

1359 if refgroup in groups_added: 

1360 newname = None 

1361 for key, val in self.controller.file_groups.items(): 

1362 if refgroup in (key, val): 

1363 newname = key 

1364 

1365 if newname is not None: 

1366 refgroup = newname 

1367 else: 

1368 refgroup = dgroup.filename 

1369 dgroup.energy_ref = refgroup 

1370 

1371 self.larch.eval("del _prj") 

1372 cur_panel.skip_plotting = False 

1373 

1374 plot_first = True 

1375 if len(labels) > 0: 

1376 gname = self.controller.file_groups[labels[0]] 

1377 self.ShowFile(groupname=gname, process=True, plot=plot_first) 

1378 plot_first = False 

1379 self.write_message("read %d datasets from %s" % (len(namelist), path)) 

1380 self.last_athena_file = path 

1381 self.controller.sync_xasgroups() 

1382 self.controller.recentfiles.append((time.time(), path)) 

1383 

1384 def onRead_OK(self, script, path, config): 

1385 #groupname=None, filename=None, 

1386 # ref_groupname=None, ref_filename=None, config=None, 

1387 # array_desc=None): 

1388 

1389 """ called when column data has been selected and is ready to be used 

1390 overwrite: whether to overwrite the current datagroup, as when 

1391 editing a datagroup 

1392 """ 

1393 filedir, spath = os.path.split(path) 

1394 filename = config.get('filename', spath) 

1395 groupname = config.get('groupname', None) 

1396 if groupname is None: 

1397 return 

1398 array_desc = config.get('array_desc', {}) 

1399 

1400 if hasattr(self.larch.symtable, groupname): 

1401 groupname = file2groupname(filename, 

1402 symtable=self.larch.symtable) 

1403 

1404 refgroup = config.get('refgroup', groupname + '_ref') 

1405 

1406 multiconfig = config.get('multicol_config', {'channels':[], 'i0': config['iy2']}) 

1407 multi_i0 = multiconfig.get('i0', config['iy2']) 

1408 multi_chans = copy.copy(multiconfig.get('channels', [])) 

1409 

1410 if len(multi_chans) > 0: 

1411 if (multi_chans[0] == config['iy1'] and multi_i0 == config['iy2'] 

1412 and 'log' not in config['expressions']['ydat']): 

1413 yname = config['array_labels'][config['iy1']] 

1414 filename = f"{spath}:{yname}" 

1415 multi_chans.pop(0) 

1416 

1417 config = copy.copy(config) 

1418 config['group'] = groupname 

1419 config['path'] = path 

1420 has_yref = config.get('has_yref', False) 

1421 

1422 

1423 self.larch.eval(script.format(**config)) 

1424 

1425 if config is not None: 

1426 self.last_col_config = config 

1427 

1428 journal = {'source': path} 

1429 refjournal = {} 

1430 

1431 if 'xdat' in array_desc: 

1432 journal['xdat'] = array_desc['xdat'].format(group=groupname) 

1433 if 'ydat' in array_desc: 

1434 journal['ydat'] = ylab = array_desc['ydat'].format(group=groupname) 

1435 journal['source_desc'] = f'{spath}: {ylab}' 

1436 if 'yerr' in array_desc: 

1437 journal['yerr'] = array_desc['yerr'].format(group=groupname) 

1438 

1439 self.install_group(groupname, filename, source=path, journal=journal) 

1440 

1441 def install_multichans(config): 

1442 ydatline = None 

1443 for line in script.split('\n'): 

1444 if line.startswith("{group}.ydat ="): 

1445 ydatline = line.replace("{group}", "{ngroup}") 

1446 mscript = '\n'.join(["{ngroup} = deepcopy({group})", 

1447 ydatline, 

1448 "{ngroup}.mu = {ngroup}.ydat", 

1449 "{ngroup}.plot_ylabel = '{ylabel}'"]) 

1450 i0 = '1.0' 

1451 if multi_i0 < len(config['array_labels']): 

1452 i0 = config['array_labels'][multi_i0] 

1453 

1454 for mchan in multi_chans: 

1455 yname = config['array_labels'][mchan] 

1456 ylabel = f"{yname}/{i0}" 

1457 fname = f"{spath}:{yname}" 

1458 ngroup = file2groupname(fname, symtable=self.larch.symtable) 

1459 njournal = {'source': path, 

1460 'xdat': array_desc['xdat'].format(group=ngroup), 

1461 'ydat': ylabel, 

1462 'source_desc': f"{spath}: {ylabel}", 

1463 'yerr': array_desc['yerr'].format(group=ngroup)} 

1464 cmd = mscript.format(group=config['group'], ngroup=ngroup, 

1465 iy1=mchan, iy2=multi_i0, ylabel=ylabel) 

1466 self.larch.eval(cmd) 

1467 self.install_group(ngroup, fname, source=path, journal=njournal) 

1468 

1469 if len(multi_chans) > 0: 

1470 install_multichans(config) 

1471 

1472 if has_yref: 

1473 

1474 if 'xdat' in array_desc: 

1475 refjournal['xdat'] = array_desc['xdat'].format(group=refgroup) 

1476 if 'yref' in array_desc: 

1477 refjournal['ydat'] = ydx = array_desc['yref'].format(group=refgroup) 

1478 refjournal['source_desc'] = f'{spath:s}: {ydx:s}' 

1479 self.install_group(refgroup, config['reffile'], 

1480 source=path, journal=refjournal) 

1481 

1482 # check if rebin is needed 

1483 thisgroup = getattr(self.larch.symtable, groupname) 

1484 

1485 do_rebin = False 

1486 if thisgroup.datatype == 'xas': 

1487 try: 

1488 en = thisgroup.energy 

1489 except: 

1490 do_rebin = True 

1491 en = thisgroup.energy = thisgroup.xdat 

1492 # test for rebinning: 

1493 # too many data points 

1494 # unsorted energy data or data in angle 

1495 # too fine a step size at the end of the data range 

1496 if (len(en) > 1200 or 

1497 any(np.diff(en) < 0) or 

1498 ((max(en)-min(en)) > 300 and 

1499 (np.diff(en[-50:]).mean() < 0.75))): 

1500 msg = """This dataset may need to be rebinned. 

1501 Rebin now?""" 

1502 dlg = wx.MessageDialog(self, msg, 'Warning', 

1503 wx.YES | wx.NO ) 

1504 do_rebin = (wx.ID_YES == dlg.ShowModal()) 

1505 dlg.Destroy() 

1506 gname = None 

1507 

1508 for path in self.paths2read: 

1509 path = path.replace('\\', '/') 

1510 filedir, spath = os.path.split(path) 

1511 fname = spath 

1512 if len(multi_chans) > 0: 

1513 yname = config['array_labels'][config['iy1']] 

1514 fname = f"{spath}:{yname}" 

1515 

1516 gname = file2groupname(fname, symtable=self.larch.symtable) 

1517 refgroup = config['refgroup'] 

1518 if has_yref: 

1519 refgroup = gname + '_ref' 

1520 reffile = spath + '_ref' 

1521 config = copy.copy(config) 

1522 config['group'] = gname 

1523 config['refgroup'] = refgroup 

1524 config['path'] = path 

1525 

1526 self.larch.eval(script.format(**config)) 

1527 if has_yref: 

1528 self.larch.eval(f"{gname}.energy_ref = {refgroup}.energy_ref = '{refgroup}'\n") 

1529 

1530 if 'xdat' in array_desc: 

1531 journal['xdat'] = array_desc['xdat'].format(group=gname) 

1532 if 'ydat' in array_desc: 

1533 journal['ydat'] = ydx = array_desc['ydat'].format(group=gname) 

1534 journal['source_desc'] = f'{spath:s}: {ydx:s}' 

1535 if 'yerr' in array_desc: 

1536 journal['yerr'] = array_desc['yerr'].format(group=gname) 

1537 

1538 self.install_group(gname, fname, source=path, journal=journal, plot=False) 

1539 if len(multi_chans) > 0: 

1540 install_multichans(config) 

1541 

1542 if has_yref: 

1543 if 'xdat' in array_desc: 

1544 refjournal['xdat'] = array_desc['xdat'].format(group=refgroup) 

1545 if 'yref' in array_desc: 

1546 refjournal['ydat'] = ydx = array_desc['yref'].format(group=refgroup) 

1547 refjournal['source_desc'] = f'{spath:s}: {ydx:s}' 

1548 

1549 self.install_group(refgroup, reffile, source=path, journal=refjournal, plot=False) 

1550 

1551 

1552 if gname is not None: 

1553 self.ShowFile(groupname=gname) 

1554 

1555 self.write_message("read %s" % (spath)) 

1556 if do_rebin: 

1557 RebinDataDialog(self, self.controller).Show() 

1558 

1559 def install_group(self, groupname, filename=None, source=None, journal=None, 

1560 process=True, plot=True): 

1561 """add groupname / filename to list of available data groups""" 

1562 if isinstance(groupname, Group): 

1563 groupname = groupname.groupname 

1564 if filename is None: 

1565 g = getattr(self.controller.symtable, groupname) 

1566 filename = g.filename 

1567 

1568 self.controller.install_group(groupname, filename, 

1569 source=source, journal=journal) 

1570 

1571 self.nb.SetSelection(0) 

1572 self.ShowFile(groupname=groupname, filename=filename, 

1573 process=process, plot=plot) 

1574 

1575 ## 

1576 def onAutoSaveTimer(self, event=None): 

1577 """autosave session periodically, using autosave_config settings 

1578 and avoiding saving sessions while program is inactive. 

1579 """ 

1580 conf = self.controller.get_config('autosave', {}) 

1581 savetime = conf.get('savetime', 600) 

1582 symtab = self.larch.symtable 

1583 if (time.time() > self.last_autosave + savetime and 

1584 symtab._sys.last_eval_time > (self.last_autosave+60) and 

1585 len(symtab._xasgroups) > 0): 

1586 self.autosave_session() 

1587 

1588 def autosave_session(self, event=None): 

1589 """autosave session now""" 

1590 savefile = self.controller.autosave_session() 

1591 # save_session(savefile, _larch=self.larch._larch) 

1592 self.last_autosave = time.time() 

1593 stime = time.strftime("%H:%M") 

1594 self.last_save_message = ("Session last saved", f"'{savefile}'", f"{stime}") 

1595 self.write_message(f"Session saved to '{savefile}' at {stime}") 

1596 

1597 

1598 ## float-spin / pin timer events 

1599 def onPinTimer(self, event=None): 

1600 if 'start' not in self.cursor_dat: 

1601 self.cursor_dat['xsel'] = None 

1602 self.onPinTimerComplete(reason="bad") 

1603 pin_config = self.controller.get_config('pin', 

1604 {'style': 'pin_first', 

1605 'max_time':15.0, 

1606 'min_time': 2.0}) 

1607 min_time = float(pin_config['min_time']) 

1608 timeout = float(pin_config['max_time']) 

1609 

1610 curhist_name = self.cursor_dat['name'] 

1611 cursor_hist = getattr(self.larch.symtable._plotter, curhist_name, []) 

1612 if len(cursor_hist) > self.cursor_dat['nhist']: # got new data! 

1613 self.cursor_dat['xsel'] = cursor_hist[0][0] 

1614 self.cursor_dat['ysel'] = cursor_hist[0][1] 

1615 if time.time() > min_time + self.cursor_dat['start']: 

1616 self.timers['pin'].Stop() 

1617 self.onPinTimerComplete(reason="new") 

1618 elif time.time() > timeout + self.cursor_dat['start']: 

1619 self.onPinTimerComplete(reason="timeout") 

1620 

1621 if 'win' in self.cursor_dat and 'xsel' in self.cursor_dat: 

1622 time_remaining = timeout + self.cursor_dat['start'] - time.time() 

1623 msg = 'Select Point from Plot #%d' % (self.cursor_dat['win']) 

1624 if self.cursor_dat['xsel'] is not None: 

1625 msg = '%s, [current value=%.1f]' % (msg, self.cursor_dat['xsel']) 

1626 msg = '%s, expiring in %.0f sec' % (msg, time_remaining) 

1627 self.write_message(msg) 

1628 

1629 def onPinTimerComplete(self, reason=None, **kws): 

1630 self.timers['pin'].Stop() 

1631 if reason != "bad": 

1632 msg = 'Selected Point at %.1f' % self.cursor_dat['xsel'] 

1633 if reason == 'timeout': 

1634 msg = msg + '(timed-out)' 

1635 self.write_message(msg) 

1636 if (self.cursor_dat['xsel'] is not None and 

1637 callable(self.cursor_dat['callback'])): 

1638 self.cursor_dat['callback'](**self.cursor_dat) 

1639 time.sleep(0.05) 

1640 else: 

1641 self.write_message('Select Point Error') 

1642 self.cursor_dat = {} 

1643 

1644 

1645 def onSelPoint(self, evt=None, opt='__', relative_e0=True, callback=None, 

1646 win=None): 

1647 """ 

1648 get last selected point from a specified plot window 

1649 and fill in the value for the widget defined by `opt`. 

1650 

1651 start Pin Timer to get last selected point from a specified plot window 

1652 and fill in the value for the widget defined by `opt`. 

1653 """ 

1654 if win is None: 

1655 win = 1 

1656 

1657 display = get_display(win=win, _larch=self.larch) 

1658 display.Raise() 

1659 msg = 'Select Point from Plot #%d' % win 

1660 self.write_message(msg) 

1661 

1662 now = time.time() 

1663 curhist_name = 'plot%d_cursor_hist' % win 

1664 cursor_hist = getattr(self.larch.symtable._plotter, curhist_name, []) 

1665 

1666 self.cursor_dat = dict(relative_e0=relative_e0, opt=opt, 

1667 callback=callback, 

1668 start=now, xsel=None, ysel=None, 

1669 win=win, name=curhist_name, 

1670 nhist=len(cursor_hist)) 

1671 

1672 pin_config = self.controller.get_config('pin', 

1673 {'style': 'pin first', 

1674 'max_time':15.0, 

1675 'min_time': 2.0}) 

1676 if pin_config['style'].startswith('plot'): 

1677 if len(cursor_hist) > 0: 

1678 x, y, t = cursor_hist[0] 

1679 if now < (t + 60.0): 

1680 self.cursor_dat['xsel'] = x 

1681 self.cursor_dat['ysel'] = y 

1682 msg = 'Selected Point at %.1f' % self.cursor_dat['xsel'] 

1683 self.cursor_dat['callback'](**self.cursor_dat) 

1684 else: 

1685 self.write_message('No Points selected from plot window!') 

1686 else: # "pin first" mode 

1687 if len(cursor_hist) > 2: # purge old cursor history 

1688 setattr(self.larch.symtable._plotter, curhist_name, cursor_hist[:2]) 

1689 

1690 if len(cursor_hist) > 0: 

1691 x, y, t = cursor_hist[0] 

1692 if now < (t + 30.0): 

1693 self.cursor_dat['xsel'] = x 

1694 self.cursor_dat['ysel'] = y 

1695 self.timers['pin'].Start(250) 

1696 

1697 

1698class XASViewer(LarchWxApp): 

1699 def __init__(self, filename=None, check_version=True, **kws): 

1700 self.filename = filename 

1701 self.check_version = check_version 

1702 LarchWxApp.__init__(self, **kws) 

1703 

1704 def createApp(self): 

1705 frame = XASFrame(filename=self.filename, 

1706 check_version=self.check_version) 

1707 self.SetTopWindow(frame) 

1708 return True 

1709 

1710def larix(**kws): 

1711 XASViewer(**kws) 

1712 

1713if __name__ == "__main__": 

1714 import argparse 

1715 parser = argparse.ArgumentParser(description=LARIX_TITLE) 

1716 parser.add_argument( 

1717 '-f', '--filename', 

1718 dest='filename', 

1719 help='data file to load') 

1720 args = parser.parse_args() 

1721 app = XASViewer(**vars(args)) 

1722 app.MainLoop()