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

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

45 

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

54 

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

56 

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

58 

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

60 action=self.export) 

61 

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

65 

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

67 

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) 

71 

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

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

74 

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) 

78 

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

80 

81 colsizes = [150, 550, 150] 

82 coltypes = ['string', 'string', 'string'] 

83 coldefs = [' ', ' ', ' '] 

84 

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

86 datatypes=coltypes, 

87 defaults=coldefs, 

88 colsizes=colsizes, 

89 rowlabelsize=40) 

90 

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

95 

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) 

104 

105 if dgroup is not None: 

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

107 

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) 

114 

115 

116 def onClose(self, event=None): 

117 self.xasmain.timers['journal_updater'].Stop() 

118 self.Destroy() 

119 

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) 

126 

127 

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 

135 

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

143 

144 buff.append('') 

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

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

147 

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

149 writer = getattr(self.xasmain, 'write_message', sys.stdout) 

150 writer(msg) 

151 

152 

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

160 

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) 

166 

167 

168 grid_data = [] 

169 rowsize = [] 

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

171 

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

181 

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

183 

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 

188 

189 for i, rsize in enumerate(rowsize): 

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

191 

192 self.datagrid.Refresh() 

193 

194 

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] 

209 

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) 

216 

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

218 

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 

227 

228 self.fit_xspace = 'e' 

229 self.fit_last_erange = None 

230 

231 def is_xasgroup(self, dgroup): 

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

233 

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) 

237 

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) 

243 

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' 

248 

249 if fit_xspace == self.fit_xspace: 

250 return 

251 

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

280 

281 

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) 

292 

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) 

302 

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

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

305 

306 def larch_eval(self, cmd): 

307 """eval""" 

308 self.command_hist.append(cmd) 

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

310 

311 def _plain_larch_eval(self, cmd): 

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

313 

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

319 

320 def larch_get(self, sym): 

321 """get value from larch symbol table""" 

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

323 

324 def build_display(self): 

325 """build display""" 

326 

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

332 

333 sizer = wx.BoxSizer(wx.VERTICAL) 

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

335 pack(self, sizer) 

336 

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) 

342 

343 def get_defaultconfig(self): 

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

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

346 

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 

361 

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 

371 

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

379 

380 conf.update(config) 

381 if dgroup is not None: 

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

383 

384 def fill_form(self, dat): 

385 if isinstance(dat, Group): 

386 dat = group2dict(dat) 

387 

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

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

390 wid.SetValue(dat[name]) 

391 

392 def get_energy_ranges(self, dgroup): 

393 pass 

394 

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 

413 

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 

419 

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

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

422 dcol=dcol, newrow=newrow) 

423 

424 

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 

439 

440 self.wids[name] = fspin 

441 

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 

448 

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) 

456 

457 def onPlot(self, evt=None): 

458 pass 

459 

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

461 pass 

462 

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

464 pass 

465 

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

467 pass