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
« 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')
14from functools import partial
16import wx
17import wx.lib.scrolledpanel as scrolled
19from wx.adv import AboutBox, AboutDialogInfo
21from wx.richtext import RichTextCtrl
23WX_DEBUG = True
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)
33from larch.larchlib import read_workdir, save_workdir, read_config, save_config
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)
46from larch.wxlib.plotter import get_display
48from larch.fitting import fit_report
49from larch.site_config import icondir, home_dir, user_larchdir
50from larch.version import check_larchversion
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
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)
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)
75from larch.xafs import pre_edge, pre_edge_baseline
77LEFT = wx.ALIGN_LEFT
78CEN |= wx.ALL
79FILE_WILDCARDS = "Data Files|*.0*;*.dat;*.DAT;*.xdi;*.prj;*.sp*c;*.h*5;*.larix|All files (*.*)|*.*"
81ICON_FILE = 'onecone.ico'
82XASVIEW_SIZE = (1020, 830)
83PLOTWIN_SIZE = (550, 550)
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}
93QUIT_MESSAGE = '''Really Quit? You may want to save your project before quitting.
94 This is not done automatically!'''
96LARIX_TITLE = "Larix (was XAS Viewer): XAS Visualization and Analysis"
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, :])
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, :])
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, :])
117 group.array_labels = labels
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))
127 sizer = wx.BoxSizer(wx.VERTICAL)
128 tpanel = wx.Panel(self)
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'])
135 self.save_btn = Button(tpanel, 'Save for Future sessions',
136 size=(200, -1), action=self.onSave)
138 self.nb = flatnotebook(tpanel, {})
139 self.wids = {}
140 conf = self.controller.config
142 def text(panel, label, size):
143 return SimpleText(panel, label, size=(size, -1), style=LEFT)
145 for name, data in FULLCONF.items():
146 self.wids[name] = {}
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)
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)
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)
168 panel.Add((5, 5), newrow=True)
169 panel.Add(text(panel, 'Name', 150))
171 panel.Add(text(panel, 'Value', 250))
172 panel.Add(text(panel, 'Factory Default Value', 225))
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)
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
201 panel.pack()
202 self.nb.AddPage(panel, name, True)
204 self.nb.SetSelection(0)
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))
215 self.Show()
216 self.Raise()
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)
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
239 def onSave(self, event=None):
240 self.controller.save_config()
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)
251 if check_version:
252 def version_checker():
253 self.vinfo = check_larchversion()
254 version_thread = Thread(target=version_checker)
255 version_thread.start()
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
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)
274 self.larch = self.larch_buffer.larchshell
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))
280 self.last_autosave = 0
281 self.last_save_message = ('Session has not been saved', '', '')
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 = {}
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()
304 self.Raise()
305 self.statusbar.SetStatusText('ready', 1)
306 self.timers['autosave'].Start(30_000)
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)
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)
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))
334 splitter = wx.SplitterWindow(self, style=wx.SP_LIVE_UPDATE,
335 size=(700, 700))
336 splitter.SetMinimumPaneSize(250)
338 leftpanel = wx.Panel(splitter)
339 ltop = wx.Panel(leftpanel)
341 def Btn(msg, x, act):
342 b = Button(ltop, msg, size=(x, 30), action=act)
343 b.SetFont(Font(FONTSIZE))
344 return b
346 sel_none = Btn('Select None', 120, self.onSelNone)
347 sel_all = Btn('Select All', 120, self.onSelAll)
349 file_actions = [('Show Group Journal', self.onGroupJournal),
350 ('Copy Group', self.onCopyGroup),
351 ('Rename Group', self.onRenameGroup),
352 ('Remove Group', self.onRemoveGroup)]
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')
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)
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)
369 pack(leftpanel, sizer)
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'])
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))
388 sizer.Add(self.nb, 1, LEFT|wx.EXPAND, 2)
389 panel.SetupScrolling()
391 pack(panel, sizer)
392 splitter.SplitVertically(leftpanel, panel, 1)
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)
397 def process_exafs(self, dgroup, force=True):
398 self.get_nbpage('exafs')[1].process(dgroup, force=force)
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
409 def onNBChanged(self, event=None):
410 callback = getattr(self.nb.GetCurrentPage(), 'onPanelExposed', None)
411 if callable(callback):
412 callback()
414 def onSelAll(self, event=None):
415 self.controller.filelist.select_all()
417 def onSelNone(self, event=None):
418 self.controller.filelist.select_none()
420 def write_message(self, msg, panel=0):
421 """write a message to the Status Bar"""
422 self.statusbar.SetStatusText(msg, panel)
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()
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())
436 if groupname is None and filename is not None:
437 groupname = self.controller.file_groups[filename]
439 if not hasattr(self.larch.symtable, groupname):
440 return
442 dgroup = self.controller.get_group(groupname)
443 if dgroup is None:
444 return
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)
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', '?')
459 if isinstance(sdesc, Entry):
460 sdesc = sdesc.value
461 if not isinstance(sdesc, str):
462 sdesc = repr(sdesc)
463 self.title.SetLabel(sdesc)
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
476 self.controller.filelist.SetStringSelection(filename)
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 = {}
488 MenuItem(self, fmenu, "&Open Data File\tCtrl+O",
489 "Open Data File", self.onReadDialog)
491 MenuItem(self, fmenu, "&Read Larch Session\tCtrl+R",
492 "Read Previously Saved Session", self.onLoadSession)
494 MenuItem(self, fmenu, "&Save Larch Session\tCtrl+S",
495 "Save Session to a File", self.onSaveSession)
497 MenuItem(self, fmenu, "&Save Larch Session As ...\tCtrl+A",
498 "Save Session to a File", self.onSaveSessionAs)
500 MenuItem(self, fmenu, "Clear Larch Session",
501 "Clear all data from this Session", self.onClearSession)
503 # autosaved session
504 conf = self.controller.get_config('autosave',
505 {'fileroot': 'autosave'})
506 froot= conf['fileroot']
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))
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))
522 fmenu.Append(-1, 'Recent Session Files', recent_menu)
525 MenuItem(self, fmenu, "&Auto-Save Larch Session",
526 f"Save Session now", self.autosave_session)
527 fmenu.AppendSeparator()
529 MenuItem(self, fmenu, "Save Selected Groups to Athena Project File",
530 "Save Selected Groups to an Athena Project File",
531 self.onExportAthenaProject)
533 MenuItem(self, fmenu, "Save Selected Groups to CSV File",
534 "Save Selected Groups to a CSV File",
535 self.onExportCSV)
537 MenuItem(self, fmenu, 'Save Larch History as Script\tCtrl+H',
538 'Save Session History as Larch Script',
539 self.onSaveLarchHistory)
541 fmenu.AppendSeparator()
543 MenuItem(self, fmenu, 'Show Larch Buffer\tCtrl+L',
544 'Show Larch Programming Buffer',
545 self.onShowLarchBuffer)
547 MenuItem(self, fmenu, 'wxInspect\tCtrl+I',
548 'Show wx inspection window', self.onwxInspect)
550 MenuItem(self, fmenu, 'Edit Preferences\tCtrl+E', 'Customize Preferences',
551 self.onPreferences)
553 MenuItem(self, fmenu, "&Quit\tCtrl+Q", "Quit program", self.onClose)
556 MenuItem(self, group_menu, "Copy This Group",
557 "Copy This Group", self.onCopyGroup)
559 MenuItem(self, group_menu, "Rename This Group",
560 "Rename This Group", self.onRenameGroup)
562 MenuItem(self, group_menu, "Show Journal for This Group",
563 "Show Processing Journal for This Group", self.onGroupJournal)
566 group_menu.AppendSeparator()
568 MenuItem(self, group_menu, "Remove Selected Groups",
569 "Remove Selected Group", self.onRemoveGroups)
571 group_menu.AppendSeparator()
573 MenuItem(self, group_menu, "Merge Selected Groups",
574 "Merge Selected Groups", self.onMergeData)
576 group_menu.AppendSeparator()
578 MenuItem(self, group_menu, "Freeze Selected Groups",
579 "Freeze Selected Groups", self.onFreezeGroups)
581 MenuItem(self, group_menu, "UnFreeze Selected Groups",
582 "UnFreeze Selected Groups", self.onUnFreezeGroups)
584 MenuItem(self, data_menu, "Deglitch Data", "Deglitch Data",
585 self.onDeglitchData)
587 MenuItem(self, data_menu, "Calibrate Energy",
588 "Calibrate Energy",
589 self.onEnergyCalibrateData)
591 MenuItem(self, data_menu, "Smooth Data", "Smooth Data",
592 self.onSmoothData)
594 MenuItem(self, data_menu, "Deconvolve Data",
595 "Deconvolution of Data", self.onDeconvolveData)
597 MenuItem(self, data_menu, "Rebin Data", "Rebin Data",
598 self.onRebinData)
600 MenuItem(self, data_menu, "Correct Over-absorption",
601 "Correct Over-absorption",
602 self.onCorrectOverAbsorptionData)
604 MenuItem(self, data_menu, "Add and Subtract Spectra",
605 "Calculations of Spectra", self.onSpectraCalc)
608 self.menubar.Append(fmenu, "&File")
609 self.menubar.Append(group_menu, "Groups")
610 self.menubar.Append(data_menu, "Data")
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)
619 self.menubar.Append(feff_menu, "Feff")
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)
627 self.menubar.Append(hmenu, '&Help')
628 self.SetMenuBar(self.menubar)
629 self.Bind(wx.EVT_CLOSE, self.onClose)
631 def onwxInspect(self, evt=None):
632 wx.GetApp().ShowInspectionTool()
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()
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)
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
656 deffile = "%s_%i.csv" % (filenames[0], len(filenames))
658 dlg = ExportCSVDialog(self, filenames)
659 res = dlg.GetResponse()
661 dlg.Destroy()
662 if not res.ok:
663 return
665 deffile = f"{filenames[0]:s}_{len(filenames):d}.csv"
666 wcards = 'CSV Files (*.csv)|*.csv|All files (*.*)|*.*'
668 outfile = FileSave(self, 'Save Groups to CSV File',
669 default_file=deffile, wildcard=wcards)
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
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)
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)
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)])
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)
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
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
723 prompt, prjfile = self.get_athenaproject()
724 self.save_athena_project(prjfile, groups, prompt=prompt,
725 warn_overwrite=False)
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
733 prompt, prjfile = self.get_athena_project()
734 self.save_athena_project(prjfile, groups)
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
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
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
763 def onPreferences(self, evt=None):
764 self.show_subframe('preferences', PreferencesFrame,
765 controller=self.controller)
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
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
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
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()
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()
804 def onSaveSession(self, evt=None):
805 groups = self.controller.filelist.GetItems()
806 if len(groups) < 1:
807 return
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'
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
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
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
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')
839 msg = f"""Session will be saved to
840 '{afile:s}'
841before clearing"""
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)
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)
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()
855 fit_dialog_window(dlg, panel)
858 if wx.ID_OK == dlg.ShowModal():
859 self.autosave_session()
860 self.controller.clear_session()
861 dlg.Destroy()
864 def onConfigDataProcessing(self, event=None):
865 pass
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)
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)
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()
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)
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
906 self.controller.filelist.SetCheckedStrings(selected)
907 self.controller.filelist.SetStringSelection(res.newname)
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]
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)
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
931 dlg = RemoveDialog(self, groups)
932 res = dlg.GetResponse()
933 dlg.Destroy()
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)
943 filelist.Clear()
944 for name in all_fnames:
945 filelist.Append(name)
946 self.controller.sync_xasgroups()
948 def onFreezeGroups(self, event=None):
949 self._freeze_handler(True)
951 def onUnFreezeGroups(self, event=None):
952 self._freeze_handler(False)
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)
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
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)
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})
1003 def has_datagroup(self):
1004 return hasattr(self.controller.get_group(), 'energy')
1006 def onDeglitchData(self, event=None):
1007 if self.has_datagroup():
1008 DeglitchDialog(self, self.controller).Show()
1010 def onSmoothData(self, event=None):
1011 if self.has_datagroup():
1012 SmoothDataDialog(self, self.controller).Show()
1014 def onRebinData(self, event=None):
1015 if self.has_datagroup():
1016 RebinDataDialog(self, self.controller).Show()
1018 def onCorrectOverAbsorptionData(self, event=None):
1019 if self.has_datagroup():
1020 OverAbsorptionDialog(self, self.controller).Show()
1022 def onSpectraCalc(self, event=None):
1023 if self.has_datagroup():
1024 SpectraCalcDialog(self, self.controller).Show()
1026 def onEnergyCalibrateData(self, event=None):
1027 if self.has_datagroup():
1028 EnergyCalibrateDialog(self, self.controller).Show()
1030 def onDeconvolveData(self, event=None):
1031 if self.has_datagroup():
1032 DeconvolutionDialog(self, self.controller).Show()
1034 def onConfigDataFitting(self, event=None):
1035 pass
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)
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)
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
1066 self.controller.save_workdir()
1067 try:
1068 self.controller.close_all_displays()
1069 except Exception:
1070 pass
1072 if self.larch_buffer is not None:
1073 try:
1074 self.larch_buffer.Destroy()
1075 except Exception:
1076 pass
1078 def destroy(wid):
1079 if hasattr(wid, 'Destroy'):
1080 try:
1081 wid.Destroy()
1082 except Exception:
1083 pass
1084 time.sleep(0.01)
1086 for name, wid in self.subframes.items():
1087 destroy(wid)
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()
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()
1102 time.sleep(0.05)
1103 self.Destroy()
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)
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)
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)
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)
1130 def onLoadFitResult(self, event=None):
1131 pass
1132 # print("onLoadFitResult??")
1133 # self.nb.SetSelection(1)
1134 # self.nb_panels[1].onLoadFitResult(event=event)
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()
1146 if len(self.paths2read) < 1:
1147 return
1149 def file_mtime(x):
1150 return os.stat(x).st_mtime
1152 self.paths2read = sorted(self.paths2read, key=file_mtime)
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)
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()
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)
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
1204 array_desc = config.get('array_desc', {})
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', []))
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)
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
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)
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]
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)
1266 cur_panel.skip_plotting = False
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')
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 = []
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)
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)
1304 for gid in groups_added:
1305 rgroup = gid
1306 dgroup = self.larch.symtable.get_group(gid)
1308 conf_xasnorm = dgroup.config.xasnorm
1309 conf_exafs= dgroup.config.exafs
1311 apars = getattr(dgroup, 'athena_params', {})
1312 abkg = getattr(apars, 'bkg', {})
1313 afft = getattr(apars, 'fft', {})
1315 # norm
1316 for attr in ('e0', 'pre1', 'pre2', 'nnorm'):
1317 if hasattr(abkg, attr):
1318 conf_xasnorm[attr] = float(getattr(abkg, attr))
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)
1329 # bkg
1330 for attr in ('e0', 'rbkg'):
1331 if hasattr(abkg, attr):
1332 conf_exafs[attr] = float(getattr(abkg, attr))
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
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))
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
1365 if newname is not None:
1366 refgroup = newname
1367 else:
1368 refgroup = dgroup.filename
1369 dgroup.energy_ref = refgroup
1371 self.larch.eval("del _prj")
1372 cur_panel.skip_plotting = False
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))
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):
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', {})
1400 if hasattr(self.larch.symtable, groupname):
1401 groupname = file2groupname(filename,
1402 symtable=self.larch.symtable)
1404 refgroup = config.get('refgroup', groupname + '_ref')
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', []))
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)
1417 config = copy.copy(config)
1418 config['group'] = groupname
1419 config['path'] = path
1420 has_yref = config.get('has_yref', False)
1423 self.larch.eval(script.format(**config))
1425 if config is not None:
1426 self.last_col_config = config
1428 journal = {'source': path}
1429 refjournal = {}
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)
1439 self.install_group(groupname, filename, source=path, journal=journal)
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]
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)
1469 if len(multi_chans) > 0:
1470 install_multichans(config)
1472 if has_yref:
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)
1482 # check if rebin is needed
1483 thisgroup = getattr(self.larch.symtable, groupname)
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
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}"
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
1526 self.larch.eval(script.format(**config))
1527 if has_yref:
1528 self.larch.eval(f"{gname}.energy_ref = {refgroup}.energy_ref = '{refgroup}'\n")
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)
1538 self.install_group(gname, fname, source=path, journal=journal, plot=False)
1539 if len(multi_chans) > 0:
1540 install_multichans(config)
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}'
1549 self.install_group(refgroup, reffile, source=path, journal=refjournal, plot=False)
1552 if gname is not None:
1553 self.ShowFile(groupname=gname)
1555 self.write_message("read %s" % (spath))
1556 if do_rebin:
1557 RebinDataDialog(self, self.controller).Show()
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
1568 self.controller.install_group(groupname, filename,
1569 source=source, journal=journal)
1571 self.nb.SetSelection(0)
1572 self.ShowFile(groupname=groupname, filename=filename,
1573 process=process, plot=plot)
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()
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}")
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'])
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")
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)
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 = {}
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`.
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
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)
1662 now = time.time()
1663 curhist_name = 'plot%d_cursor_hist' % win
1664 cursor_hist = getattr(self.larch.symtable._plotter, curhist_name, [])
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))
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])
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)
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)
1704 def createApp(self):
1705 frame = XASFrame(filename=self.filename,
1706 check_version=self.check_version)
1707 self.SetTopWindow(frame)
1708 return True
1710def larix(**kws):
1711 XASViewer(**kws)
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()