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

1import time 

2import os 

3import sys 

4import platform 

5from functools import partial 

6from copy import deepcopy 

7 

8import numpy as np 

9np.seterr(all='ignore') 

10 

11import wx 

12import wx.grid as wxgrid 

13import wx.lib.scrolledpanel as scrolled 

14 

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) 

21 

22from larch.xafs import etok, ktoe 

23from larch.utils import group2dict 

24from larch.utils.strutils import break_longstring 

25from .config import LARIX_PANELS 

26 

27LEFT = wx.ALIGN_LEFT 

28CEN |= wx.ALL 

29 

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) 

44 

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 

54 

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)) 

63 

64 panel = GridPanel(self, ncols=3, nrows=10, pad=2, itemstyle=LEFT) 

65 

66 self.label = SimpleText(panel, 'Group Journal', size=(750, 30)) 

67 

68 export_btn = Button(panel, ' Export to Tab-Separated File', size=(225, -1), 

69 action=self.export) 

70 

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)) 

74 

75 panel.Add(self.label, dcol=3, style=LEFT) 

76 

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) 

80 

81 panel.Add(SimpleText(panel, ' Label:'), style=LEFT, newrow=True) 

82 panel.Add(self.label_wid, dcol=1, style=LEFT) 

83 

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) 

87 

88 collabels = [' Label ', ' Value ', ' Date/Time'] 

89 

90 colsizes = [150, 550, 150] 

91 coltypes = ['string', 'string', 'string'] 

92 coldefs = [' ', ' ', ' '] 

93 

94 self.datagrid = DataTableGrid(panel, collabels=collabels, 

95 datatypes=coltypes, 

96 defaults=coldefs, 

97 colsizes=colsizes, 

98 rowlabelsize=40) 

99 

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() 

104 

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) 

113 

114 if dgroup is not None: 

115 wx.CallAfter(self.set_group, dgroup=dgroup) 

116 

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) 

123 

124 

125 def onClose(self, event=None): 

126 self.parent.timers['journal_updater'].Stop() 

127 self.Destroy() 

128 

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) 

135 

136 

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 

144 

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}") 

152 

153 buff.append('') 

154 with open(fname, 'w', encoding=sys.getdefaultencoding()) as fh: 

155 fh.write('\n'.join(buff)) 

156 

157 msg = f"Exported journal for {self.dgroup.filename} to '{fname}'" 

158 writer = getattr(self.parent, 'write_message', sys.stdout) 

159 writer(msg) 

160 

161 

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}') 

169 

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) 

175 

176 

177 grid_data = [] 

178 rowsize = [] 

179 self.n_entries = len(dgroup.journal.data) 

180 

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]) 

190 

191 nrows = self.datagrid.table.GetRowsCount() 

192 

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 

197 

198 for i, rsize in enumerate(rowsize): 

199 self.datagrid.SetRowSize(i, rsize*20) 

200 

201 self.datagrid.Refresh() 

202 

203 

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 

214 

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) 

221 

222 self.font_fixedwidth = wx.Font(FONTSIZE_FW, wx.MODERN, wx.NORMAL, wx.NORMAL) 

223 

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 

232 

233 self.fit_xspace = 'e' 

234 self.fit_last_erange = None 

235 

236 def is_xasgroup(self, dgroup): 

237 return getattr(dgroup, 'datatype', 'raw').startswith('xa') 

238 

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') 

244 

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) 

253 

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) 

259 

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' 

264 

265 if fit_xspace == self.fit_xspace: 

266 return 

267 

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):') 

296 

297 

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) 

308 

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) 

318 

319 def write_message(self, msg, panel=0): 

320 self.controller.write_message(msg, panel=panel) 

321 

322 def larch_eval(self, cmd): 

323 """eval""" 

324 self.command_hist.append(cmd) 

325 return self.controller.larch.eval(cmd) 

326 

327 def _plain_larch_eval(self, cmd): 

328 return self.controller.larch._larch.eval(cmd) 

329 

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', [])) 

335 

336 def larch_get(self, sym): 

337 """get value from larch symbol table""" 

338 return self.controller.larch.symtable.get_symbol(sym) 

339 

340 def build_display(self): 

341 """build display""" 

342 

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() 

348 

349 sizer = wx.BoxSizer(wx.VERTICAL) 

350 sizer.Add(self.panel, 1, wx.LEFT|wx.CENTER, 3) 

351 pack(self, sizer) 

352 

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) 

358 

359 def get_defaultconfig(self): 

360 """get the default configuration for this session""" 

361 return deepcopy(self.controller.get_config(self.configname)) 

362 

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 

377 

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 

387 

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() 

398 

399 conf.update(config) 

400 if dgroup is not None: 

401 setattr(dgroup.config, self.configname, conf) 

402 

403 def fill_form(self, dat): 

404 if isinstance(dat, Group): 

405 dat = group2dict(dat) 

406 

407 for name, wid in self.wids.items(): 

408 if isinstance(wid, FloatCtrl) and name in dat: 

409 wid.SetValue(dat[name]) 

410 

411 def get_energy_ranges(self, dgroup): 

412 pass 

413 

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 

432 

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 

438 

439 def add_text(self, text, dcol=1, newrow=True): 

440 self.panel.Add(SimpleText(self.panel, text), 

441 dcol=dcol, newrow=newrow) 

442 

443 

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 

458 

459 self.wids[name] = fspin 

460 

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 

467 

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) 

475 

476 def onPlot(self, evt=None): 

477 pass 

478 

479 def onPlotOne(self, evt=None, dgroup=None, **kws): 

480 pass 

481 

482 def onPlotSel(self, evt=None, groups=None, **kws): 

483 pass 

484 

485 def onProcess(self, evt=None, **kws): 

486 pass