Coverage for /Users/Newville/Codes/xraylarch/larch/wxlib/feff_browser.py: 15%
423 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-11-09 10:08 -0600
« prev ^ index » next coverage.py v7.3.2, created at 2023-11-09 10:08 -0600
1import os
2import sys
3import time
4import logging
5import shutil
6from datetime import datetime, timedelta
7import wx
8import wx.lib.scrolledpanel as scrolled
9import wx.dataview as dv
11import larch
12from larch.site_config import user_larchdir
13from larch.utils import unixpath, mkdir, read_textfile
14from larch.wxlib import (GridPanel, GUIColors, Button, pack, SimpleText,
15 FileOpen, FileSave, Font, LEFT, FRAMESTYLE,
16 FONTSIZE, MenuItem, EditableListBox, OkCancel,
17 FileCheckList, Choice, HLine, ReportFrame, Popup,
18 LarchWxApp)
20from larch.xafs import get_feff_pathinfo
21from larch.utils.physical_constants import ATOM_SYMS
23ATSYMS = ['< All Atoms>'] + ATOM_SYMS[:96]
24EDGES = ['< All Edges>', 'K', 'L3', 'L2', 'L1', 'M5']
27LEFT = LEFT|wx.ALL
28DVSTYLE = dv.DV_VERT_RULES|dv.DV_ROW_LINES|dv.DV_MULTIPLE
30class FeffPathsModel(dv.DataViewIndexListModel):
31 def __init__(self, feffpaths, with_use=True):
32 dv.DataViewIndexListModel.__init__(self, 0)
33 self.data = []
34 self.paths = {}
35 self.with_use = with_use
36 self.feffpaths = feffpaths
37 self.read_data()
39 def set_data(self, feffpaths):
40 self.paths = {}
41 self.feffpaths = feffpaths
42 self.read_data()
44 def read_data(self):
45 self.data = []
46 if self.feffpaths is None:
47 row = ['feffNNNN.dat', '0.0000', '2', '6', '100.0']
48 if self.with_use: row.append(False)
49 row.append('* -> * -> *')
50 self.data.append(row)
51 else:
52 for fp in self.feffpaths:
53 row = [fp.filename, '%.4f' % fp.reff,
54 '%.0f' % fp.nleg, '%.0f' % fp.degen,
55 '%.3f' % fp.cwratio]
56 use = False
57 if self.with_use:
58 if fp.filename in self.paths:
59 use = self.paths[fp.filename]
60 row.append(use)
61 row.append(fp.geom)
62 self.data.append(row)
63 self.paths[fp.filename] = use
64 self.Reset(len(self.data))
67 def select_all(self, use=True):
68 for pname in self.paths:
69 self.paths[pname] = use
70 self.read_data()
72 def select_above(self, item):
73 itemname = self.GetValue(item, 0)
74 use = True
75 for row in self.data:
76 self.paths[row[0]] = use
77 if row[0] == itemname:
78 use = not use
79 self.read_data()
81 def GetColumnType(self, col):
82 if self.with_use and col == 5:
83 return "bool"
84 return "string"
86 def GetValueByRow(self, row, col):
87 return self.data[row][col]
89 def SetValueByRow(self, value, row, col):
91 self.data[row][col] = value
92 return True
94 def GetColumnCount(self):
95 return len(self.data[0])
97 def GetCount(self):
98 return len(self.data)
100 def GetAttrByRow(self, row, col, attr):
101 """set row/col attributes (color, etc)"""
102 nleg = self.data[row][2]
103 cname = self.data[row][0]
104 if nleg == '2':
105 attr.SetColour('#000')
106 attr.SetBold(False)
107 return True
108 elif nleg == '3':
109 attr.SetColour('#A11')
110 attr.SetBold(False)
111 return True
112 elif nleg == '4':
113 attr.SetColour('#11A')
114 attr.SetBold(False)
115 return True
116 else:
117 attr.SetColour('#393')
118 attr.SetBold(False)
119 return True
120 return False
123class RemoveFeffCalcDialog(wx.Dialog):
124 """dialog for removing Feff Calculations"""
126 def __init__(self, parent, ncalcs=1, **kws):
127 title = "Remove Feff calculations?"
128 wx.Dialog.__init__(self, parent, wx.ID_ANY, title=title, size=(325, 275))
129 panel = GridPanel(self, ncols=3, nrows=4, pad=2, itemstyle=LEFT)
131 panel.Add(SimpleText(panel, f'Remove {ncalcs:d} Feff calculations?'),
132 dcol=3, newrow=True)
133 panel.Add(SimpleText(panel, 'Warning: this cannot be undone!'),
134 dcol=3, newrow=True)
135 panel.Add((5, 5), newrow=True)
136 panel.Add(HLine(panel, size=(500, 3)), dcol=2, newrow=True)
137 panel.Add(OkCancel(panel), dcol=2, newrow=True)
138 panel.pack()
140 def GetResponse(self):
141 self.Raise()
142 return (self.ShowModal() == wx.ID_OK)
144class FeffResultsPanel(wx.Panel):
145 """ present Feff results """
146 def __init__(self, parent=None, feffresult=None, path_importer=None,
147 _larch=None):
148 wx.Panel.__init__(self, parent, -1, size=(700, 500))
149 self.parent = parent
150 self.path_importer = path_importer
151 self._larch = _larch
152 self.feffresult = feffresult
153 self.report_frame = None
155 self.dvc = dv.DataViewCtrl(self, style=DVSTYLE)
156 self.dvc.SetMinSize((695, 350))
158 self.model = FeffPathsModel(None, with_use=callable(path_importer))
159 self.dvc.AssociateModel(self.model)
161 panel = wx.Panel(self)
162 # panel.SetBackgroundColour(GUIColors.bg)
164 sizer = wx.GridBagSizer(1, 1)
166 bkws = dict(size=(175, -1))
167 btn_header = Button(panel, "Show Full Header", action=self.onShowHeader, **bkws)
168 btn_feffinp = Button(panel, "Show Feff.inp", action=self.onShowFeffInp, **bkws)
169 btn_geom = Button(panel, "Show Path Geometries", action=self.onShowGeom, **bkws)
171 if callable(self.path_importer):
172 btn_import = Button(panel, "Import Paths", action=self.onImportPath, **bkws)
173 btn_above = Button(panel, "Select All Above Current", action=self.onSelAbove, **bkws)
174 btn_none = Button(panel, "Select None", action=self.onSelNone, **bkws)
176 opts = dict(size=(475, -1), style=LEFT)
177 self.feff_folder = SimpleText(panel, '', **opts)
178 self.feff_datetime = SimpleText(panel, '',**opts)
179 self.feff_header = [SimpleText(panel, '', **opts),
180 SimpleText(panel, '', **opts),
181 SimpleText(panel, '', **opts),
182 SimpleText(panel, '', **opts),
183 SimpleText(panel, '', **opts),
184 SimpleText(panel, '', **opts)]
187 ir = 0
188 sizer.Add(SimpleText(panel, 'Feff Folder:'), (ir, 0), (1, 1), LEFT, 2)
189 sizer.Add(self.feff_folder, (ir, 1), (1, 4), LEFT, 2)
190 ir += 1
191 sizer.Add(SimpleText(panel, 'Date Run:'), (ir, 0), (1, 1), LEFT, 2)
192 sizer.Add(self.feff_datetime, (ir, 1), (1, 5), LEFT, 2)
194 ir += 1
195 sizer.Add(SimpleText(panel, 'Header:'), (ir, 0), (1, 1), LEFT, 1)
196 sizer.Add(self.feff_header[0], (ir, 1), (1, 5), LEFT, 1)
197 ir += 1
198 sizer.Add(SimpleText(panel, ''), (ir, 0), (1, 1), LEFT, 1)
199 sizer.Add(self.feff_header[1], (ir, 1), (1, 5), LEFT, 1)
200 ir += 1
201 sizer.Add(SimpleText(panel, ''), (ir, 0), (1, 1), LEFT, 1)
202 sizer.Add(self.feff_header[2], (ir, 1), (1, 5), LEFT, 1)
203 ir += 1
204 sizer.Add(SimpleText(panel, ''), (ir, 0), (1, 1), LEFT, 1)
205 sizer.Add(self.feff_header[3], (ir, 1), (1, 5), LEFT, 1)
206 ir += 1
207 sizer.Add(SimpleText(panel, ''), (ir, 0), (1, 1), LEFT, 1)
208 sizer.Add(self.feff_header[4], (ir, 1), (1, 5), LEFT, 1)
209 ir += 1
210 sizer.Add(SimpleText(panel, ''), (ir, 0), (1, 1), LEFT, 1)
211 sizer.Add(self.feff_header[5], (ir, 1), (1, 5), LEFT, 1)
213 ir += 1
214 sizer.Add(btn_header, (ir, 0), (1, 2), LEFT, 2)
215 sizer.Add(btn_feffinp, (ir, 2), (1, 2), LEFT, 2)
216 sizer.Add(btn_geom, (ir, 4), (1, 2), LEFT, 2)
218 if callable(self.path_importer):
219 ir += 1
220 sizer.Add(btn_above, (ir, 0), (1, 2), LEFT, 2)
221 sizer.Add(btn_none, (ir, 2), (1, 2), LEFT, 2)
222 sizer.Add(btn_import, (ir, 4), (1, 2), LEFT, 2)
224 ir += 1
225 sizer.Add(wx.StaticLine(panel, size=(600, 2)),(ir, 0), (1, 6), LEFT, 2)
227 pack(panel, sizer)
229 columns = [('Feff File', 100, 'text'),
230 ('R (\u212B)', 60, 'text'),
231 ('# legs', 60, 'text'),
232 ('# paths', 65, 'text'),
233 ('Importance', 100, 'text')]
234 if callable(self.path_importer):
235 columns.append(('Use', 50, 'bool'))
236 columns.append(('Geometry', 200, 'text'))
238 for icol, dat in enumerate(columns):
239 label, width, dtype = dat
240 method = self.dvc.AppendTextColumn
241 mode = dv.DATAVIEW_CELL_EDITABLE
242 if dtype == 'bool':
243 method = self.dvc.AppendToggleColumn
244 mode = dv.DATAVIEW_CELL_ACTIVATABLE
245 method(label, icol, width=width, mode=mode)
246 c = self.dvc.Columns[icol]
247 align = wx.ALIGN_RIGHT
248 if (label.startswith('Feff') or label.startswith('Geom')):
249 align = wx.ALIGN_LEFT
250 c.Alignment = c.Renderer.Alignment = align
251 c.SetSortable(False)
254 mainsizer = wx.BoxSizer(wx.VERTICAL)
255 mainsizer.Add(panel, 0, LEFT, 1)
256 mainsizer.Add(self.dvc, 0, LEFT, 1)
258 pack(self, mainsizer)
259 self.dvc.EnsureVisible(self.model.GetItem(0))
261 if feffresult is not None:
262 self.set_feffresult(feffresult)
264 def onSelAll(self, event=None):
265 self.model.select_all(True)
267 def onSelNone(self, event=None):
268 self.model.select_all(False)
270 def onSelAbove(self, event=None):
271 if self.dvc.HasSelection():
272 self.model.select_above(self.dvc.GetSelection())
274 def onShowHeader(self, event=None):
275 if self.feffresult is not None:
276 self.show_report(self.feffresult.header,
277 title=f'Header for {self.feffresult.folder:s}',
278 default_filename=f'{self.feffresult.folder:s}_header.txt')
280 def onShowGeom(self, event=None):
281 if self.feffresult is None:
282 return
283 show = False
284 out = []
285 for data in self.model.data:
286 if data[5]:
287 show = True
288 out.append(f'### {self.feffresult.folder:s}/{data[0]:s} ###')
289 out.append('#Atom IPOT X Y Z Beta Eta Length')
290 fname = data[0]
292 for fp in self.feffresult.paths:
293 if fname == fp.filename:
294 for i, px in enumerate(fp.geometry):
295 at, ipot, r, x, y, z, beta, eta = px
296 if i == 0: r = 0
297 t = f'{at:4s} {ipot:3d} {x:9.4f} {y:9.4f} {z:9.4f} {beta:9.4f} {eta:9.4f} {r:9.4f}'
298 out.append(t)
299 if show:
300 out = '\n'.join(out)
301 self.show_report(out, title=f'Path Geometries for {self.feffresult.folder:s}',
302 default_filename=f'{self.feffresult.folder:s}_paths.dat')
306 def onShowFeffInp(self, event=None):
307 if self.feffresult is not None:
308 text = None
309 fname = unixpath(os.path.join(self.feffresult.folder, 'feff.inp'))
310 if os.path.exists(fname):
311 text = read_textfile(fname)
312 else:
313 fname = unixpath(os.path.join(user_larchdir, 'feff',
314 self.feffresult.folder, 'feff.inp'))
315 if os.path.exists(fname):
316 text = read_textfile(fname)
317 if text is not None:
318 self.show_report(text, title=f'Feff.inp for {self.feffresult.folder:s}',
319 default_filename=f'{self.feffresult.folder:s}_feff.inp',
320 wildcard='Input Files (*.inp)|*.inp')
322 def show_report(self, text, title='Text', default_filename='out.txt', wildcard=None):
323 if wildcard is None:
324 wildcard='Text Files (*.txt)|*.txt'
325 default_filename = os.path.split(default_filename)[1]
326 try:
327 self.report_frame.set_text(text)
328 self.report_frame.SetTitle(title)
329 self.report_frame.default_filename = default_filename
330 self.report_frame.wildcard = wildcard
331 except:
332 self.report_frame = ReportFrame(parent=self,
333 text=text, title=title,
334 default_filename=default_filename,
335 wildcard=wildcard)
338 def onImportPath(self, event=None):
339 folder = self.feffresult.folder
340 _, fname = os.path.split(folder)
341 for data in self.model.data:
342 if data[5]:
343 fname = data[0]
344 fullpath = unixpath(os.path.join(folder, fname))
345 for pathinfo in self.feffresult.paths:
346 if pathinfo.filename == fname:
347 self.path_importer(fullpath, pathinfo)
348 break
350 self.onSelNone()
353 def set_feffresult(self, feffresult):
354 self.feffresult = feffresult
355 self.feff_folder.SetLabel(feffresult.folder)
356 self.feff_datetime.SetLabel(feffresult.datetime)
357 nhead = len(self.feff_header)
359 for i, text in enumerate(feffresult.header.split('\n')[:nhead]):
360 self.feff_header[i].SetLabel(text)
361 self.model.set_data(feffresult.paths)
362 try:
363 self.dvc.EnsureVisible(self.model.GetItem(0))
364 self.dvc.SetCurrentItem(self.dvc.GetTopItem())
365 except:
366 pass
369class FeffResultsFrame(wx.Frame):
370 """ present Feff results """
371 def __init__(self, parent=None, feffresult=None, path_importer=None, _larch=None):
372 wx.Frame.__init__(self, parent, -1, size=(900, 650), style=FRAMESTYLE)
374 title = "Manage Feff calculation results"
375 self.larch = _larch
376 if _larch is None:
377 self.larch = larch.Interpreter()
378 # self.larch.eval("# started Feff results browser\n")
379 # self.larch.eval("if not hasattr('_sys', '_feffruns'): _sys._feffruns = {}")
380 if not hasattr(self.larch.symtable._sys, '_feffruns'):
381 self.larch.symtable._sys._feffruns = {}
382 self.parent = parent
384 self.feff_folder = unixpath(os.path.join(user_larchdir, 'feff'))
385 mkdir(self.feff_folder)
387 self.SetTitle(title)
388 self.SetSize((925, 650))
389 self.SetFont(Font(FONTSIZE))
390 self.createMenus()
392 display0 = wx.Display(0)
393 client_area = display0.ClientArea
394 xmin, ymin, xmax, ymax = client_area
395 xpos = int((xmax-xmin)*0.15) + xmin
396 ypos = int((ymax-ymin)*0.20) + ymin
397 self.SetPosition((xpos, ypos))
399 splitter = wx.SplitterWindow(self, style=wx.SP_LIVE_UPDATE)
400 splitter.SetMinimumPaneSize(250)
402 # left hand panel
403 lpanel = wx.Panel(splitter)
404 ltop = wx.Panel(lpanel)
406 def Btn(msg, x, act):
407 b = Button(ltop, msg, size=(x, 30), action=act)
408 b.SetFont(Font(FONTSIZE))
409 return b
411 sel_none = Btn('Select None', 120, self.onSelNone)
412 sel_all = Btn('Select All', 120, self.onSelAll)
413 tsizer = wx.BoxSizer(wx.HORIZONTAL)
414 tsizer.Add(sel_all, 1, LEFT|wx.GROW, 1)
415 tsizer.Add(sel_none, 1, LEFT|wx.GROW, 1)
416 pack(ltop, tsizer)
418 self.fefflist = FileCheckList(lpanel, select_action=self.onShowFeff,
419 size=(300, -1))
421 lsizer = wx.BoxSizer(wx.VERTICAL)
422 lsizer.Add(ltop, 0, LEFT|wx.GROW, 1)
423 lsizer.Add(self.fefflist, 1, LEFT|wx.GROW|wx.ALL, 1)
424 pack(lpanel, lsizer)
426 # right hand side
427 panel = wx.Panel(splitter) ## scrolled.ScrolledPanel(splitter)
428 wids = self.wids = {}
429 toprow = wx.Panel(panel)
431 wids['central_atom'] = Choice(toprow, choices=ATSYMS, size=(125, -1),
432 action=self.onCentralAtom)
433 wids['edge'] = Choice(toprow, choices=EDGES, size=(125, -1),
434 action=self.onAbsorbingEdge)
436 flabel = SimpleText(toprow, 'Filter Calculations by Element and Edge:', size=(175, -1))
437 tsizer = wx.BoxSizer(wx.HORIZONTAL)
438 tsizer.Add(flabel, 0, LEFT, 2)
439 tsizer.Add(wids['central_atom'], 0, LEFT|wx.GROW, 2)
440 tsizer.Add(wids['edge'], 0, LEFT|wx.GROW, 2)
441 pack(toprow, tsizer)
443 sizer = wx.BoxSizer(wx.VERTICAL)
444 self.feff_panel = FeffResultsPanel(panel, path_importer=path_importer,
445 _larch=_larch)
446 sizer.Add(toprow, 0, LEFT|wx.GROW|wx.ALL, 2)
447 sizer.Add(HLine(panel, size=(650, 2)), 0, LEFT|wx.GROW|wx.ALL, 2)
448 sizer.Add(self.feff_panel, 1, LEFT|wx.GROW|wx.ALL, 2)
449 pack(panel, sizer)
450 # panel.SetupScrolling()
451 splitter.SplitVertically(lpanel, panel, 1)
452 self.Show()
453 wx.CallAfter(self.onSearch)
455 def onShowFeff(self, event=None):
456 fr = self.feffruns.get(self.fefflist.GetStringSelection(), None)
457 if fr is not None:
458 self.feff_panel.set_feffresult(fr)
461 def onSearch(self, event=None):
462 catom = self.wids['central_atom'].GetStringSelection()
463 edge = self.wids['edge'].GetStringSelection()
464 all_catoms = 'All' in catom
465 all_edges = 'All' in edge
467 self.fefflist.Clear()
468 self.feffruns = {}
469 flist = os.listdir(self.feff_folder)
470 flist = sorted(flist, key=lambda t: -os.stat(unixpath(os.path.join(self.feff_folder, t))).st_mtime)
471 _feffruns = self.larch.symtable._sys._feffruns
472 for path in flist:
473 fullpath = unixpath(os.path.join(self.feff_folder, path))
474 if os.path.isdir(fullpath):
475 try:
476 _feffruns[path] = thisrun = get_feff_pathinfo(fullpath)
477 if ((len(thisrun.paths) < 1) or
478 (len(thisrun.ipots) < 1) or thisrun.shell is None):
480 self.larch.symtable._sys._feffruns.pop(path)
481 else:
482 self.feffruns[path] = thisrun
483 if ((all_catoms or (thisrun.absorber == catom)) and
484 (all_edges or (thisrun.shell == edge))):
485 self.fefflist.Append(path)
486 except:
487 print(f"could not read Feff calculation from '{path}'")
489 def onCentralAtom(self, event=None):
490 self.onSearch()
492 def onAbsorbingEdge(self, event=None):
493 self.onSearch()
495 def onSelAll(self, event=None):
496 self.fefflist.select_all()
498 def onSelNone(self, event=None):
499 self.fefflist.select_none()
501 def onRemoveFeffFolders(self, event=None):
502 dlg = RemoveFeffCalcDialog(self, ncalcs=len(self.fefflist.GetCheckedStrings()))
503 dlg.Raise()
504 dlg.SetWindowStyle(wx.STAY_ON_TOP)
505 remove = dlg.GetResponse()
506 dlg.Destroy()
507 if remove:
508 for checked in self.fefflist.GetCheckedStrings():
509 shutil.rmtree(unixpath(os.path.join(self.feff_folder, checked)))
510 self.onSearch()
512 def onFeffFolder(self, event=None):
513 "prompt for Feff Folder"
514 dlg = wx.DirDialog(self, 'Select Main Folder for Feff Calculations',
515 style=wx.DD_DEFAULT_STYLE|wx.DD_CHANGE_DIR)
517 dlg.SetPath(self.feff_folder)
518 if dlg.ShowModal() == wx.ID_CANCEL:
519 return None
520 self.feff_folder = os.path.abspath(dlg.GetPath())
521 mkdir(self.feff_folder)
523 def onImportFeffCalc(self, event=None):
524 "prompt to import Feff calculation folder"
525 dlg = wx.DirDialog(self, 'Select Folder wth Feff Calculations',
526 style=wx.DD_DEFAULT_STYLE|wx.DD_CHANGE_DIR)
528 dlg.SetPath(self.feff_folder)
529 if dlg.ShowModal() == wx.ID_CANCEL:
530 return None
531 path = os.path.abspath(dlg.GetPath())
532 if os.path.exists(path):
533 flist = os.listdir(path)
534 if ('paths.dat' in flist and 'files.dat' in flist and
535 'feff0001.dat' in flist and 'feff.inp' in flist):
536 _, dname = os.path.split(path)
537 dest = unixpath(os.path.join(self.feff_folder, dname))
538 shutil.copytree(path, dest)
539 self.onSearch()
540 else:
541 Popup(self, f"{path:s} is not a complete Feff calculation",
542 "cannot import Feff calculation")
544 def createMenus(self):
545 # ppnl = self.plotpanel
546 self.menubar = wx.MenuBar()
547 fmenu = wx.Menu()
549 MenuItem(self, fmenu, "Rescan Main Feff Folder",
550 "Rescan Feff Folder for Feff calculations",
551 self.onSearch)
553 MenuItem(self, fmenu, "Import Feff calculation",
554 "Import other Feff calculation",
555 self.onImportFeffCalc)
557 fmenu.AppendSeparator()
559 MenuItem(self, fmenu, "Set Main Feff Folder",
560 "Select Main Feff Folder for Feff calculations",
561 self.onFeffFolder)
564 MenuItem(self, fmenu, "Remove Selected Feff calculations",
565 "Completely remove Feff calculations", self.onRemoveFeffFolders)
567 fmenu.AppendSeparator()
568 MenuItem(self, fmenu, "Quit", "Exit", self.onClose)
570 self.menubar.Append(fmenu, "&File")
571 self.SetMenuBar(self.menubar)
572 self.Bind(wx.EVT_CLOSE, self.onClose)
574 def onClose(self, event=None):
575 self.Destroy()
578class FeffResultsBrowserApp(LarchWxApp):
579 def __init__(self, dat=None, **kws):
580 self.dat = dat
581 LarchWxApp.__init__(self, **kws)
583 def createApp(self):
584 frame = FeffResultsFrame(feffresult=self.dat)
585 self.SetTopWindow(frame)
586 return True
588if __name__ == '__main__':
589 dat = None
590 FeffResultsBrowserApp(dat).MainLoop()