Coverage for larch/wxxas/taskpanel.py: 0%
352 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 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 LARIX_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)
45def update_confval(dest, source, attr, pref=''):
46 """
47 update a dict value for an attribute from a source dict
48 """
49 val = source.get(attr, None)
50 if val is None:
51 val = dest.get(pref+attr, None)
52 dest[pref+attr] = val
53 return val
55class GroupJournalFrame(wx.Frame):
56 """ edit parameters"""
57 def __init__(self, parent, dgroup=None, **kws):
58 self.parent = parent
59 self.dgroup = dgroup
60 self.n_entries = 0
61 wx.Frame.__init__(self, None, -1, 'Group Journal',
62 style=FRAMESTYLE, size=(950, 700))
64 panel = GridPanel(self, ncols=3, nrows=10, pad=2, itemstyle=LEFT)
66 self.label = SimpleText(panel, 'Group Journal', size=(750, 30))
68 export_btn = Button(panel, ' Export to Tab-Separated File', size=(225, -1),
69 action=self.export)
71 add_btn = Button(panel, 'Add Entry', size=(200, -1), action=self.add_entry)
72 self.label_wid = wx.TextCtrl(panel, -1, value='user comment', size=(200, -1))
73 self.value_wid = wx.TextCtrl(panel, -1, value='', size=(600, -1))
75 panel.Add(self.label, dcol=3, style=LEFT)
77 panel.Add(SimpleText(panel, ' Add a Journal Entry:'), dcol=1, style=LEFT, newrow=True)
78 panel.Add(add_btn, dcol=1)
79 panel.Add(export_btn, dcol=1, newrow=False)
81 panel.Add(SimpleText(panel, ' Label:'), style=LEFT, newrow=True)
82 panel.Add(self.label_wid, dcol=1, style=LEFT)
84 panel.Add(SimpleText(panel, ' Value:'), style=LEFT, newrow=True)
85 panel.Add(self.value_wid, dcol=2, style=LEFT)
86 panel.Add((10, 10), newrow=True)
88 collabels = [' Label ', ' Value ', ' Date/Time']
90 colsizes = [150, 550, 150]
91 coltypes = ['string', 'string', 'string']
92 coldefs = [' ', ' ', ' ']
94 self.datagrid = DataTableGrid(panel, collabels=collabels,
95 datatypes=coltypes,
96 defaults=coldefs,
97 colsizes=colsizes,
98 rowlabelsize=40)
100 self.datagrid.SetMinSize((925, 650))
101 self.datagrid.EnableEditing(False)
102 panel.Add(self.datagrid, dcol=5, drow=9, newrow=True, style=LEFT|wx.GROW|wx.ALL)
103 panel.pack()
105 self.parent.timers['journal_updater'] = wx.Timer(self.parent)
106 self.parent.Bind(wx.EVT_TIMER, self.onRefresh,
107 self.parent.timers['journal_updater'])
108 self.Bind(wx.EVT_CLOSE, self.onClose)
109 self.SetSize((950, 725))
110 self.Show()
111 self.Raise()
112 self.parent.timers['journal_updater'].Start(1000)
114 if dgroup is not None:
115 wx.CallAfter(self.set_group, dgroup=dgroup)
117 def add_entry(self, evt=None):
118 if self.dgroup is not None:
119 label = self.label_wid.GetValue()
120 value = self.value_wid.GetValue()
121 if len(label)>0 and len(value)>1:
122 self.dgroup.journal.add(label, value)
125 def onClose(self, event=None):
126 self.parent.timers['journal_updater'].Stop()
127 self.Destroy()
129 def onRefresh(self, event=None):
130 if self.dgroup is None:
131 return
132 if self.n_entries == len(self.dgroup.journal.data):
133 return
134 self.set_group(self.dgroup)
137 def export(self, event=None):
138 wildcard = 'CSV file (*.csv)|*.csv|All files (*.*)|*.*'
139 fname = FileSave(self, message='Save Tab-Separated-Value Data File',
140 wildcard=wildcard,
141 default_file= f"{self.dgroup.filename}_journal.csv")
142 if fname is None:
143 return
145 buff = ['Label\tValue\tDateTime']
146 for entry in self.dgroup.journal:
147 k, v, dt = entry.key, entry.value, entry.datetime.isoformat()
148 k = k.replace('\t', '_')
149 if not isinstance(v, str): v = repr(v)
150 v = v.replace('\t', ' ')
151 buff.append(f"{k}\t{v}\t{dt}")
153 buff.append('')
154 with open(fname, 'w', encoding=sys.getdefaultencoding()) as fh:
155 fh.write('\n'.join(buff))
157 msg = f"Exported journal for {self.dgroup.filename} to '{fname}'"
158 writer = getattr(self.parent, 'write_message', sys.stdout)
159 writer(msg)
162 def set_group(self, dgroup=None):
163 if dgroup is None:
164 dgroup = self.dgroup
165 if dgroup is None:
166 return
167 self.dgroup = dgroup
168 self.SetTitle(f'Group Journal for {dgroup.filename:s}')
170 label = f'Journal for {dgroup.filename}'
171 desc = dgroup.journal.get('source_desc')
172 if desc is not None:
173 label = f'Journal for {desc.value}'
174 self.label.SetLabel(label)
177 grid_data = []
178 rowsize = []
179 self.n_entries = len(dgroup.journal.data)
181 for entry in dgroup.journal:
182 val = entry.value
183 if not isinstance(val, str):
184 val = repr(val)
185 xval = break_longstring(val)
186 val = '\n'.join(xval)
187 rowsize.append(len(xval))
188 xtime = entry.datetime.strftime("%Y/%m/%d %H:%M:%S")
189 grid_data.append([entry.key, val, xtime])
191 nrows = self.datagrid.table.GetRowsCount()
193 if len(grid_data) > nrows:
194 self.datagrid.table.AppendRows(len(grid_data)+8 - nrows)
195 self.datagrid.table.Clear()
196 self.datagrid.table.data = grid_data
198 for i, rsize in enumerate(rowsize):
199 self.datagrid.SetRowSize(i, rsize*20)
201 self.datagrid.Refresh()
204class TaskPanel(wx.Panel):
205 """generic panel for main tasks. meant to be subclassed
206 """
207 def __init__(self, parent, controller, panel=None, **kws):
208 wx.Panel.__init__(self, parent, -1, size=(550, 625), **kws)
209 self.parent = parent
210 self.controller = controller
211 self.larch = controller.larch
212 self.title = 'Generic Panel'
213 self.configname = panel
215 self.wids = {}
216 self.subframes = {}
217 self.command_hist = []
218 self.SetFont(Font(FONTSIZE))
219 self.titleopts = dict(font=Font(FONTSIZE+2),
220 colour='#AA0000', style=LEFT)
222 self.font_fixedwidth = wx.Font(FONTSIZE_FW, wx.MODERN, wx.NORMAL, wx.NORMAL)
224 self.panel = GridPanel(self, ncols=7, nrows=10, pad=2, itemstyle=LEFT)
225 self.panel.sizer.SetVGap(5)
226 self.panel.sizer.SetHGap(5)
227 self.skip_process = True
228 self.skip_plotting = False
229 self.build_display()
230 self.skip_process = False
231 self.stale_groups = None
233 self.fit_xspace = 'e'
234 self.fit_last_erange = None
236 def is_xasgroup(self, dgroup):
237 return getattr(dgroup, 'datatype', 'raw').startswith('xa')
239 def ensure_xas_processed(self, dgroup, force_mback=False):
240 if self.is_xasgroup(dgroup):
241 req_attrs = ['e0', 'mu', 'dmude', 'norm', 'pre_edge']
242 if force_mback:
243 req_attrs.append('norm_mback')
245 if not all([hasattr(dgroup, attr) for attr in req_attrs]):
246 self.parent.process_normalization(dgroup, force=True,
247 force_mback=force_mback)
248 if not hasattr(dgroup, 'xplot'):
249 if hasattr(dgroup, 'xdat'):
250 dgroup.xplot = deepcopy(dgroup.xdat)
251 elif hasattr(dgroup, 'energy'):
252 dgroup.xplot = deepcopy(dgroup.energy)
254 def make_fit_xspace_widgets(self, elo=-1, ehi=1):
255 self.wids['fitspace_label'] = SimpleText(self.panel, 'Fit Range (eV):')
256 opts = dict(digits=2, increment=1.0, relative_e0=True)
257 self.elo_wids = self.add_floatspin('elo', value=elo, **opts)
258 self.ehi_wids = self.add_floatspin('ehi', value=ehi, **opts)
260 def update_fit_xspace(self, arrayname):
261 fit_xspace = 'e'
262 if arrayname.startswith('chi'):
263 fit_xspace = 'r' if 'r' in arrayname else 'k'
265 if fit_xspace == self.fit_xspace:
266 return
268 if self.fit_xspace == 'e' and fit_xspace == 'k': # e to k
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 elo = self.wids['elo'].GetValue()
275 ehi = self.wids['ehi'].GetValue()
276 self.fit_last_erange = (elo, ehi)
277 self.wids['elo'].SetValue(etok(elo-e0))
278 self.wids['ehi'].SetValue(etok(ehi+e0))
279 self.fit_xspace = 'k'
280 self.wids['fitspace_label'].SetLabel('Fit Range (1/\u212B):')
281 elif self.fit_xspace == 'k' and fit_xspace == 'e': # k to e
282 if self.fit_last_erange is not None:
283 elo, ehi = self.fit_last_erange
284 else:
285 dgroup = self.controller.get_group()
286 e0 = getattr(dgroup, 'e0', None)
287 k = getattr(dgroup, 'k', None)
288 if e0 is None or k is None:
289 return
290 ehi = ktoe(self.wids['elo'].GetValue()) + e0
291 elo = ktoe(self.wids['ehi'].GetValue()) + e0
292 self.wids['elo'].SetValue(elo)
293 self.wids['ehi'].SetValue(ehi)
294 self.fit_xspace = 'e'
295 self.wids['fitspace_label'].SetLabel('Fit Range (eV):')
298 def show_subframe(self, name, frameclass, **opts):
299 shown = False
300 if name in self.subframes:
301 try:
302 self.subframes[name].Raise()
303 shown = True
304 except:
305 del self.subframes[name]
306 if not shown:
307 self.subframes[name] = frameclass(self, **opts)
309 def onPanelExposed(self, **kws):
310 # called when notebook is selected
311 fname = self.controller.filelist.GetStringSelection()
312 if fname in self.controller.file_groups:
313 gname = self.controller.file_groups[fname]
314 dgroup = self.controller.get_group(gname)
315 self.ensure_xas_processed(dgroup)
316 self.fill_form(dgroup)
317 self.process(dgroup=dgroup)
319 def write_message(self, msg, panel=0):
320 self.controller.write_message(msg, panel=panel)
322 def larch_eval(self, cmd):
323 """eval"""
324 self.command_hist.append(cmd)
325 return self.controller.larch.eval(cmd)
327 def _plain_larch_eval(self, cmd):
328 return self.controller.larch._larch.eval(cmd)
330 def get_session_history(self):
331 """return full session history"""
332 larch = self.controller.larch
333 return getattr(larch.input, 'hist_buff',
334 getattr(larch.parent, 'hist_buff', []))
336 def larch_get(self, sym):
337 """get value from larch symbol table"""
338 return self.controller.larch.symtable.get_symbol(sym)
340 def build_display(self):
341 """build display"""
343 self.panel.Add(SimpleText(self.panel, self.title, **titleopts),
344 dcol=7)
345 self.panel.Add(SimpleText(self.panel, ' coming soon....'),
346 dcol=7, newrow=True)
347 self.panel.pack()
349 sizer = wx.BoxSizer(wx.VERTICAL)
350 sizer.Add(self.panel, 1, wx.LEFT|wx.CENTER, 3)
351 pack(self, sizer)
353 def set_defaultconfig(self, config):
354 """set the default configuration for this session"""
355 if self.configname not in self.controller.conf_group:
356 self.controller.conf_group[self.configname] = {}
357 self.controller.conf_group[self.configname].update(config)
359 def get_defaultconfig(self):
360 """get the default configuration for this session"""
361 return deepcopy(self.controller.get_config(self.configname))
363 def get_config(self, dgroup=None, with_erange=True):
364 """get and set processing configuration for a group"""
365 if dgroup is None:
366 dgroup = self.controller.get_group()
367 if not hasattr(dgroup, 'config'):
368 dgroup.config = Group(__name__='Larix config')
369 conf = getattr(dgroup.config, self.configname, None)
370 defconf = self.get_defaultconfig()
371 if conf is None:
372 setattr(dgroup.config, self.configname, defconf)
373 conf = getattr(dgroup.config, self.configname)
374 for k, v in defconf.items():
375 if k not in conf:
376 conf[k] = v
378 if dgroup is not None and with_erange:
379 _emin = min(dgroup.energy)
380 _emax = max(dgroup.energy)
381 e0 = 5*int(dgroup.e0/5.0)
382 if 'elo' not in conf:
383 conf['elo'] = min(_emax, max(_emin, conf['elo_rel'] + e0))
384 if 'ehi' not in conf:
385 conf['ehi'] = min(_emax, max(_emin, conf['ehi_rel'] + e0))
386 return conf
388 def update_config(self, config, dgroup=None):
389 """set/update processing configuration for a group"""
390 if dgroup is None:
391 dgroup = self.controller.get_group()
392 conf = None
393 dconf = getattr(dgroup, 'config', None)
394 if dconf is not None:
395 conf = getattr(dconf, self.configname, None)
396 if conf is None:
397 conf = self.get_defaultconfig()
399 conf.update(config)
400 if dgroup is not None:
401 setattr(dgroup.config, self.configname, conf)
403 def fill_form(self, dat):
404 if isinstance(dat, Group):
405 dat = group2dict(dat)
407 for name, wid in self.wids.items():
408 if isinstance(wid, FloatCtrl) and name in dat:
409 wid.SetValue(dat[name])
411 def get_energy_ranges(self, dgroup):
412 pass
414 def read_form(self):
415 "read for, returning dict of values"
416 dgroup = self.controller.get_group()
417 form_opts = {'groupname': getattr(dgroup, 'groupname', 'No Group')}
418 for name, wid in self.wids.items():
419 val = None
420 for method in ('GetValue', 'GetStringSelection', 'IsChecked',
421 'GetLabel'):
422 meth = getattr(wid, method, None)
423 if callable(meth):
424 try:
425 val = meth()
426 except TypeError:
427 pass
428 if val is not None:
429 break
430 form_opts[name] = val
431 return form_opts
433 def process(self, dgroup=None, **kws):
434 """override to handle data process step"""
435 if self.skip_process:
436 return
437 self.skip_process = True
439 def add_text(self, text, dcol=1, newrow=True):
440 self.panel.Add(SimpleText(self.panel, text),
441 dcol=dcol, newrow=newrow)
444 def add_floatspin(self, name, value, with_pin=True, parent=None,
445 relative_e0=False, **kws):
446 """create FloatSpin with Pin button for onSelPoint"""
447 if parent is None:
448 parent = self.panel
449 if with_pin:
450 pin_action = partial(self.parent.onSelPoint, opt=name,
451 relative_e0=relative_e0,
452 callback=self.pin_callback)
453 fspin, pinb = FloatSpinWithPin(parent, value=value,
454 pin_action=pin_action, **kws)
455 else:
456 fspin = FloatSpin(parent, value=value, **kws)
457 pinb = None
459 self.wids[name] = fspin
461 fspin.SetValue(value)
462 sizer = wx.BoxSizer(wx.HORIZONTAL)
463 sizer.Add(fspin)
464 if pinb is not None:
465 sizer.Add(pinb)
466 return sizer
468 def pin_callback(self, opt='__', xsel=None, relative_e0=False, **kws):
469 """called to do reprocessing after a point is selected as from Pin/Plot"""
470 if xsel is not None and opt in self.wids:
471 if relative_e0 and 'e0' in self.wids:
472 xsel -= self.wids['e0'].GetValue()
473 self.wids[opt].SetValue(xsel)
474 wx.CallAfter(self.onProcess)
476 def onPlot(self, evt=None):
477 pass
479 def onPlotOne(self, evt=None, dgroup=None, **kws):
480 pass
482 def onPlotSel(self, evt=None, groups=None, **kws):
483 pass
485 def onProcess(self, evt=None, **kws):
486 pass