Coverage for /Users/Newville/Codes/xraylarch/larch/wxxas/taskpanel.py: 17%
338 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 time
2import os
3import sys
4import platform
5from functools import partial
6from copy import deepcopy
8import numpy as np
9np.seterr(all='ignore')
11import wx
12import wx.grid as wxgrid
13import wx.lib.scrolledpanel as scrolled
15from larch import Group
16from larch.wxlib import (BitmapButton, SetTip, GridPanel, FloatCtrl,
17 FloatSpin, FloatSpinWithPin, get_icon, SimpleText,
18 pack, Button, HLine, Choice, Check, MenuItem,
19 GUIColors, CEN, LEFT, FRAMESTYLE, Font, FileSave,
20 FileOpen, FONTSIZE, FONTSIZE_FW, DataTableGrid)
22from larch.xafs import etok, ktoe
23from larch.utils import group2dict
24from larch.utils.strutils import break_longstring
25from .config import PANELS
27LEFT = wx.ALIGN_LEFT
28CEN |= wx.ALL
30def autoset_fs_increment(wid, value):
31 """set increment for floatspin to be
32 1, 2, or 5 x 10^(integer) and ~0.02 X current value
33 """
34 if abs(value) < 1.e-20:
35 return
36 ndig = int(1-round(np.log10(abs(value*0.5))))
37 wid.SetDigits(ndig+2)
38 c, inc = 0, 10.0**(-ndig)
39 while (inc/abs(value) > 0.02):
40 scale = 0.5 if (c % 2 == 0) else 0.4
41 inc *= scale
42 c += 1
43 wid.SetIncrement(inc)
46class GroupJournalFrame(wx.Frame):
47 """ edit parameters"""
48 def __init__(self, parent, dgroup=None, xasmain=None, **kws):
49 self.xasmain = xasmain
50 self.dgroup = dgroup
51 self.n_entries = 0
52 wx.Frame.__init__(self, None, -1, 'Group Journal',
53 style=FRAMESTYLE, size=(950, 700))
55 panel = GridPanel(self, ncols=3, nrows=10, pad=2, itemstyle=LEFT)
57 self.label = SimpleText(panel, 'Group Journal', size=(750, 30))
59 export_btn = Button(panel, ' Export to Tab-Separated File', size=(225, -1),
60 action=self.export)
62 add_btn = Button(panel, 'Add Entry', size=(200, -1), action=self.add_entry)
63 self.label_wid = wx.TextCtrl(panel, -1, value='user comment', size=(200, -1))
64 self.value_wid = wx.TextCtrl(panel, -1, value='', size=(600, -1))
66 panel.Add(self.label, dcol=3, style=LEFT)
68 panel.Add(SimpleText(panel, ' Add a Journal Entry:'), dcol=1, style=LEFT, newrow=True)
69 panel.Add(add_btn, dcol=1)
70 panel.Add(export_btn, dcol=1, newrow=False)
72 panel.Add(SimpleText(panel, ' Label:'), style=LEFT, newrow=True)
73 panel.Add(self.label_wid, dcol=1, style=LEFT)
75 panel.Add(SimpleText(panel, ' Value:'), style=LEFT, newrow=True)
76 panel.Add(self.value_wid, dcol=2, style=LEFT)
77 panel.Add((10, 10), newrow=True)
79 collabels = [' Label ', ' Value ', ' Date/Time']
81 colsizes = [150, 550, 150]
82 coltypes = ['string', 'string', 'string']
83 coldefs = [' ', ' ', ' ']
85 self.datagrid = DataTableGrid(panel, collabels=collabels,
86 datatypes=coltypes,
87 defaults=coldefs,
88 colsizes=colsizes,
89 rowlabelsize=40)
91 self.datagrid.SetMinSize((925, 650))
92 self.datagrid.EnableEditing(False)
93 panel.Add(self.datagrid, dcol=5, drow=9, newrow=True, style=LEFT|wx.GROW|wx.ALL)
94 panel.pack()
96 self.xasmain.timers['journal_updater'] = wx.Timer(self.xasmain)
97 self.xasmain.Bind(wx.EVT_TIMER, self.onRefresh,
98 self.xasmain.timers['journal_updater'])
99 self.Bind(wx.EVT_CLOSE, self.onClose)
100 self.SetSize((950, 725))
101 self.Show()
102 self.Raise()
103 self.xasmain.timers['journal_updater'].Start(1000)
105 if dgroup is not None:
106 wx.CallAfter(self.set_group, dgroup=dgroup)
108 def add_entry(self, evt=None):
109 if self.dgroup is not None:
110 label = self.label_wid.GetValue()
111 value = self.value_wid.GetValue()
112 if len(label)>0 and len(value)>1:
113 self.dgroup.journal.add(label, value)
116 def onClose(self, event=None):
117 self.xasmain.timers['journal_updater'].Stop()
118 self.Destroy()
120 def onRefresh(self, event=None):
121 if self.dgroup is None:
122 return
123 if self.n_entries == len(self.dgroup.journal.data):
124 return
125 self.set_group(self.dgroup)
128 def export(self, event=None):
129 wildcard = 'CSV file (*.csv)|*.csv|All files (*.*)|*.*'
130 fname = FileSave(self, message='Save Tab-Separated-Value Data File',
131 wildcard=wildcard,
132 default_file= f"{self.dgroup.filename}_journal.csv")
133 if fname is None:
134 return
136 buff = ['Label\tValue\tDateTime']
137 for entry in self.dgroup.journal:
138 k, v, dt = entry.key, entry.value, entry.datetime.isoformat()
139 k = k.replace('\t', '_')
140 if not isinstance(v, str): v = repr(v)
141 v = v.replace('\t', ' ')
142 buff.append(f"{k}\t{v}\t{dt}")
144 buff.append('')
145 with open(fname, 'w', encoding=sys.getdefaultencoding()) as fh:
146 fh.write('\n'.join(buff))
148 msg = f"Exported journal for {self.dgroup.filename} to '{fname}'"
149 writer = getattr(self.xasmain, 'write_message', sys.stdout)
150 writer(msg)
153 def set_group(self, dgroup=None):
154 if dgroup is None:
155 dgroup = self.dgroup
156 if dgroup is None:
157 return
158 self.dgroup = dgroup
159 self.SetTitle(f'Group Journal for {dgroup.filename:s}')
161 label = f'Journal for {dgroup.filename}'
162 desc = dgroup.journal.get('source_desc')
163 if desc is not None:
164 label = f'Journal for {desc.value}'
165 self.label.SetLabel(label)
168 grid_data = []
169 rowsize = []
170 self.n_entries = len(dgroup.journal.data)
172 for entry in dgroup.journal:
173 val = entry.value
174 if not isinstance(val, str):
175 val = repr(val)
176 xval = break_longstring(val)
177 val = '\n'.join(xval)
178 rowsize.append(len(xval))
179 xtime = entry.datetime.strftime("%Y/%m/%d %H:%M:%S")
180 grid_data.append([entry.key, val, xtime])
182 nrows = self.datagrid.table.GetRowsCount()
184 if len(grid_data) > nrows:
185 self.datagrid.table.AppendRows(len(grid_data)+8 - nrows)
186 self.datagrid.table.Clear()
187 self.datagrid.table.data = grid_data
189 for i, rsize in enumerate(rowsize):
190 self.datagrid.SetRowSize(i, rsize*20)
192 self.datagrid.Refresh()
195class TaskPanel(wx.Panel):
196 """generic panel for main tasks. meant to be subclassed
197 """
198 def __init__(self, parent, controller, xasmain=None, panel=None, **kws):
199 wx.Panel.__init__(self, parent, -1, size=(550, 625), **kws)
200 self.parent = parent
201 self.xasmain = xasmain or parent
202 self.controller = controller
203 self.larch = controller.larch
204 self.title = 'Generic Panel'
205 self.configname = None
206 if panel in PANELS:
207 self.configname = panel
208 self.title = PANELS[panel]
210 self.wids = {}
211 self.subframes = {}
212 self.command_hist = []
213 self.SetFont(Font(FONTSIZE))
214 self.titleopts = dict(font=Font(FONTSIZE+2),
215 colour='#AA0000', style=LEFT)
217 self.font_fixedwidth = wx.Font(FONTSIZE_FW, wx.MODERN, wx.NORMAL, wx.NORMAL)
219 self.panel = GridPanel(self, ncols=7, nrows=10, pad=2, itemstyle=LEFT)
220 self.panel.sizer.SetVGap(5)
221 self.panel.sizer.SetHGap(5)
222 self.skip_process = True
223 self.skip_plotting = False
224 self.build_display()
225 self.skip_process = False
226 self.stale_groups = None
228 self.fit_xspace = 'e'
229 self.fit_last_erange = None
231 def is_xasgroup(self, dgroup):
232 return getattr(dgroup, 'datatype', 'raw').startswith('xa')
234 def ensure_xas_processed(self, dgroup):
235 if self.is_xasgroup(dgroup) and (not hasattr(dgroup, 'norm') or not hasattr(dgroup, 'e0')):
236 self.xasmain.process_normalization(dgroup, force=True)
238 def make_fit_xspace_widgets(self, elo=-1, ehi=1):
239 self.wids['fitspace_label'] = SimpleText(self.panel, 'Fit Range (eV):')
240 opts = dict(digits=2, increment=1.0, relative_e0=True)
241 self.elo_wids = self.add_floatspin('elo', value=elo, **opts)
242 self.ehi_wids = self.add_floatspin('ehi', value=ehi, **opts)
244 def update_fit_xspace(self, arrayname):
245 fit_xspace = 'e'
246 if arrayname.startswith('chi'):
247 fit_xspace = 'r' if 'r' in arrayname else 'k'
249 if fit_xspace == self.fit_xspace:
250 return
252 if self.fit_xspace == 'e' and fit_xspace == 'k': # e to k
253 dgroup = self.controller.get_group()
254 e0 = getattr(dgroup, 'e0', None)
255 k = getattr(dgroup, 'k', None)
256 if e0 is None or k is None:
257 return
258 elo = self.wids['elo'].GetValue()
259 ehi = self.wids['ehi'].GetValue()
260 self.fit_last_erange = (elo, ehi)
261 self.wids['elo'].SetValue(etok(elo-e0))
262 self.wids['ehi'].SetValue(etok(ehi+e0))
263 self.fit_xspace = 'k'
264 self.wids['fitspace_label'].SetLabel('Fit Range (1/\u212B):')
265 elif self.fit_xspace == 'k' and fit_xspace == 'e': # k to e
266 if self.fit_last_erange is not None:
267 elo, ehi = self.fit_last_erange
268 else:
269 dgroup = self.controller.get_group()
270 e0 = getattr(dgroup, 'e0', None)
271 k = getattr(dgroup, 'k', None)
272 if e0 is None or k is None:
273 return
274 ehi = ktoe(self.wids['elo'].GetValue()) + e0
275 elo = ktoe(self.wids['ehi'].GetValue()) + e0
276 self.wids['elo'].SetValue(elo)
277 self.wids['ehi'].SetValue(ehi)
278 self.fit_xspace = 'e'
279 self.wids['fitspace_label'].SetLabel('Fit Range (eV):')
282 def show_subframe(self, name, frameclass, **opts):
283 shown = False
284 if name in self.subframes:
285 try:
286 self.subframes[name].Raise()
287 shown = True
288 except:
289 del self.subframes[name]
290 if not shown:
291 self.subframes[name] = frameclass(self, **opts)
293 def onPanelExposed(self, **kws):
294 # called when notebook is selected
295 fname = self.controller.filelist.GetStringSelection()
296 if fname in self.controller.file_groups:
297 gname = self.controller.file_groups[fname]
298 dgroup = self.controller.get_group(gname)
299 self.ensure_xas_processed(dgroup)
300 self.fill_form(dgroup)
301 self.process(dgroup=dgroup)
303 def write_message(self, msg, panel=0):
304 self.controller.write_message(msg, panel=panel)
306 def larch_eval(self, cmd):
307 """eval"""
308 self.command_hist.append(cmd)
309 return self.controller.larch.eval(cmd)
311 def _plain_larch_eval(self, cmd):
312 return self.controller.larch._larch.eval(cmd)
314 def get_session_history(self):
315 """return full session history"""
316 larch = self.controller.larch
317 return getattr(larch.input, 'hist_buff',
318 getattr(larch.parent, 'hist_buff', []))
320 def larch_get(self, sym):
321 """get value from larch symbol table"""
322 return self.controller.larch.symtable.get_symbol(sym)
324 def build_display(self):
325 """build display"""
327 self.panel.Add(SimpleText(self.panel, self.title, **titleopts),
328 dcol=7)
329 self.panel.Add(SimpleText(self.panel, ' coming soon....'),
330 dcol=7, newrow=True)
331 self.panel.pack()
333 sizer = wx.BoxSizer(wx.VERTICAL)
334 sizer.Add(self.panel, 1, wx.LEFT|wx.CENTER, 3)
335 pack(self, sizer)
337 def set_defaultconfig(self, config):
338 """set the default configuration for this session"""
339 if self.configname not in self.controller.conf_group:
340 self.controller.conf_group[self.configname] = {}
341 self.controller.conf_group[self.configname].update(config)
343 def get_defaultconfig(self):
344 """get the default configuration for this session"""
345 return deepcopy(self.controller.get_config(self.configname))
347 def get_config(self, dgroup=None, with_erange=True):
348 """get and set processing configuration for a group"""
349 if dgroup is None:
350 dgroup = self.controller.get_group()
351 if not hasattr(dgroup, 'config'):
352 dgroup.config = Group(__name__='Larix config')
353 conf = getattr(dgroup.config, self.configname, None)
354 defconf = self.get_defaultconfig()
355 if conf is None:
356 setattr(dgroup.config, self.configname, defconf)
357 conf = getattr(dgroup.config, self.configname)
358 for k, v in defconf.items():
359 if k not in conf:
360 conf[k] = v
362 if dgroup is not None and with_erange:
363 _emin = min(dgroup.energy)
364 _emax = max(dgroup.energy)
365 e0 = 5*int(dgroup.e0/5.0)
366 if 'elo' not in conf:
367 conf['elo'] = min(_emax, max(_emin, conf['elo_rel'] + e0))
368 if 'ehi' not in conf:
369 conf['ehi'] = min(_emax, max(_emin, conf['ehi_rel'] + e0))
370 return conf
372 def update_config(self, config, dgroup=None):
373 """set/update processing configuration for a group"""
374 if dgroup is None:
375 dgroup = self.controller.get_group()
376 conf = getattr(dgroup.config, self.configname, None)
377 if conf is None:
378 conf = self.get_defaultconfig()
380 conf.update(config)
381 if dgroup is not None:
382 setattr(dgroup.config, self.configname, conf)
384 def fill_form(self, dat):
385 if isinstance(dat, Group):
386 dat = group2dict(dat)
388 for name, wid in self.wids.items():
389 if isinstance(wid, FloatCtrl) and name in dat:
390 wid.SetValue(dat[name])
392 def get_energy_ranges(self, dgroup):
393 pass
395 def read_form(self):
396 "read for, returning dict of values"
397 dgroup = self.controller.get_group()
398 form_opts = {'groupname': getattr(dgroup, 'groupname', 'No Group')}
399 for name, wid in self.wids.items():
400 val = None
401 for method in ('GetValue', 'GetStringSelection', 'IsChecked',
402 'GetLabel'):
403 meth = getattr(wid, method, None)
404 if callable(meth):
405 try:
406 val = meth()
407 except TypeError:
408 pass
409 if val is not None:
410 break
411 form_opts[name] = val
412 return form_opts
414 def process(self, dgroup=None, **kws):
415 """override to handle data process step"""
416 if self.skip_process:
417 return
418 self.skip_process = True
420 def add_text(self, text, dcol=1, newrow=True):
421 self.panel.Add(SimpleText(self.panel, text),
422 dcol=dcol, newrow=newrow)
425 def add_floatspin(self, name, value, with_pin=True, parent=None,
426 relative_e0=False, **kws):
427 """create FloatSpin with Pin button for onSelPoint"""
428 if parent is None:
429 parent = self.panel
430 if with_pin:
431 pin_action = partial(self.xasmain.onSelPoint, opt=name,
432 relative_e0=relative_e0,
433 callback=self.pin_callback)
434 fspin, pinb = FloatSpinWithPin(parent, value=value,
435 pin_action=pin_action, **kws)
436 else:
437 fspin = FloatSpin(parent, value=value, **kws)
438 pinb = None
440 self.wids[name] = fspin
442 fspin.SetValue(value)
443 sizer = wx.BoxSizer(wx.HORIZONTAL)
444 sizer.Add(fspin)
445 if pinb is not None:
446 sizer.Add(pinb)
447 return sizer
449 def pin_callback(self, opt='__', xsel=None, relative_e0=False, **kws):
450 """called to do reprocessing after a point is selected as from Pin/Plot"""
451 if xsel is not None and opt in self.wids:
452 if relative_e0 and 'e0' in self.wids:
453 xsel -= self.wids['e0'].GetValue()
454 self.wids[opt].SetValue(xsel)
455 wx.CallAfter(self.onProcess)
457 def onPlot(self, evt=None):
458 pass
460 def onPlotOne(self, evt=None, dgroup=None, **kws):
461 pass
463 def onPlotSel(self, evt=None, groups=None, **kws):
464 pass
466 def onProcess(self, evt=None, **kws):
467 pass