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