Coverage for /usr/local/lib/python3.11/dist-packages/pyrocko/gui/sparrow/elements/table.py: 91%

351 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2024-01-15 12:05 +0000

1# https://pyrocko.org - GPLv3 

2# 

3# The Pyrocko Developers, 21st Century 

4# ---|P------/S----------~Lg---------- 

5 

6import copy 

7 

8import numpy as num 

9 

10from pyrocko.guts import Bool, Float, String, StringChoice 

11from pyrocko.gui import util as gui_util 

12from pyrocko.gui.vtk_util import ScatterPipe, BeachballPipe 

13from pyrocko.gui.qt_compat import qw, qc 

14 

15from . import base 

16from .. import common 

17 

18guts_prefix = 'sparrow' 

19km = 1e3 

20 

21 

22def inormalize(x, imin, imax, discrete=True): 

23 if x.size == 0: 

24 return num.full(x.shape, imin, dtype=int) 

25 

26 xmin = num.nanmin(x) 

27 xmax = num.nanmax(x) 

28 if xmin == xmax: 

29 xmin -= 0.5 

30 xmax += 0.5 

31 

32 rmin = imin - 0.5 

33 rmax = imax + 0.5 

34 

35 if discrete: 

36 return num.clip( 

37 num.round( 

38 rmin + (x - xmin) * ( 

39 (rmax-rmin) / (xmax - xmin))).astype(num.int64), 

40 imin, imax) 

41 else: 

42 return num.clip( 

43 rmin + (x - xmin) * ((rmax-rmin) / (xmax - xmin)), 

44 imin, imax) 

45 

46 

47def string_to_sorted_idx(values): 

48 val_sort = num.sort(values, axis=-1, kind='mergesort') 

49 val_sort_unique = num.unique(val_sort) 

50 

51 val_to_idx = dict([ 

52 (val_sort_unique[i], i) 

53 for i in range(val_sort_unique.shape[0])]) 

54 

55 return num.array([val_to_idx[val] for val in values]) 

56 

57 

58class SymbolChoice(StringChoice): 

59 choices = ['point', 'sphere', 'beachball'] 

60 

61 

62class MaskingShapeChoice(StringChoice): 

63 choices = ['rect', 'linear', 'quadratic'] 

64 

65 

66class MaskingModeChoice(StringChoice): 

67 choices = ['past + future', 'past', 'future'] 

68 

69 @classmethod 

70 def get_factors(cls, mode, value_low): 

71 return { 

72 'past + future': (value_low, 1.0, value_low), 

73 'past': (value_low, 1.0, 0.0), 

74 'future': (0.0, 1.0, value_low)}[mode] 

75 

76 

77class TableState(base.ElementState): 

78 visible = Bool.T(default=True) 

79 size = Float.T(default=3.0) 

80 color_parameter = String.T(optional=True) 

81 cpt = base.CPTState.T(default=base.CPTState.D()) 

82 size_parameter = String.T(optional=True) 

83 depth_min = Float.T(optional=True) 

84 depth_max = Float.T(optional=True) 

85 depth_offset = Float.T(default=0.0) 

86 symbol = SymbolChoice.T(default='sphere') 

87 time_masking_opacity = Float.T(default=0.0) 

88 time_masking_shape = MaskingShapeChoice.T(default='rect') 

89 time_masking_mode = MaskingModeChoice.T(default='past + future') 

90 

91 

92class TableElement(base.Element): 

93 def __init__(self): 

94 base.Element.__init__(self) 

95 self._parent = None 

96 

97 self._table = None 

98 self._istate = 0 

99 self._istate_view = 0 

100 

101 self._controls = None 

102 self._color_combobox = None 

103 self._size_combobox = None 

104 

105 self._pipes = None 

106 self._pipe_maps = None 

107 self._isize_min = 1 

108 self._isize_max = 6 

109 

110 self.cpt_handler = base.CPTHandler() 

111 

112 def bind_state(self, state): 

113 base.Element.bind_state(self, state) 

114 self.talkie_connect(state, ['visible', 'size'], self.update) 

115 

116 self.talkie_connect( 

117 state, 

118 ['depth_min', 'depth_max', 'time_masking_shape', 

119 'time_masking_mode', 'time_masking_opacity'], 

120 self.update_alpha) 

121 

122 self.cpt_handler.bind_state(state.cpt, self.update) 

123 

124 self.talkie_connect( 

125 state, 

126 ['symbol', 'size_parameter', 'color_parameter'], 

127 self.update_sizes) 

128 

129 def unbind_state(self): 

130 self.cpt_handler.unbind_state() 

131 base.Element.unbind_state(self) 

132 

133 def get_name(self): 

134 return 'Table' 

135 

136 def set_parent(self, parent): 

137 self._parent = parent 

138 self._parent.add_panel( 

139 self.get_title_label(), 

140 self._get_controls(), 

141 visible=True, 

142 title_controls=[ 

143 self.get_title_control_remove(), 

144 self.get_title_control_visible()]) 

145 

146 for var in ['tmin', 'tmax', 'tduration', 'tposition']: 

147 self.talkie_connect( 

148 self._parent.state, var, self.update_alpha) 

149 

150 self._parent.register_data_provider(self) 

151 

152 self.update() 

153 

154 def iter_data(self, name): 

155 if self._table and self._table.has_col(name): 

156 yield self._table.get_col(name) 

157 

158 def set_table(self, table): 

159 self._table = table 

160 

161 self._istate += 1 

162 

163 if self._pipes is not None and self._istate != self._istate_view: 

164 self._clear_pipes() 

165 

166 self._update_controls() 

167 

168 def get_size_parameter_extra_entries(self): 

169 return [] 

170 

171 def get_color_parameter_extra_entries(self): 

172 return [] 

173 

174 def update_sizes(self, *args): 

175 self._istate += 1 

176 self.update() 

177 

178 def unset_parent(self): 

179 self.unbind_state() 

180 if self._parent: 

181 self._parent.unregister_data_provider(self) 

182 

183 self._clear_pipes() 

184 

185 if self._controls: 

186 self._parent.remove_panel(self._controls) 

187 self._controls = None 

188 

189 self._parent.update_view() 

190 self._parent = None 

191 

192 def _clear_pipes(self): 

193 if self._pipes is not None: 

194 for p in self._pipes: 

195 self._parent.remove_actor(p.actor) 

196 

197 self._pipes = None 

198 

199 if self._pipe_maps is not None: 

200 self._pipe_maps = None 

201 

202 def _init_pipes_scatter(self): 

203 state = self._state 

204 points = self._table.get_col('xyz') 

205 self._pipes = [] 

206 self._pipe_maps = [] 

207 if state.size_parameter: 

208 sizes = self._table.get_col(state.size_parameter) 

209 isizes = inormalize( 

210 sizes, self._isize_min, self._isize_max) 

211 

212 for i in range(self._isize_min, self._isize_max+1): 

213 b = isizes == i 

214 p = ScatterPipe(points[b].copy()) 

215 self._pipes.append(p) 

216 self._pipe_maps.append(b) 

217 else: 

218 self._pipes.append( 

219 ScatterPipe(points)) 

220 self._pipe_maps.append( 

221 num.ones(points.shape[0], dtype=bool)) 

222 

223 def _init_pipes_beachball(self): 

224 state = self._state 

225 self._pipes = [] 

226 

227 tab = self._table 

228 

229 positions = tab.get_col('xyz') 

230 

231 if tab.has_col('m6'): 

232 m6s = tab.get_col('m6') 

233 else: 

234 m6s = num.zeros((tab.get_nrows(), 6)) 

235 m6s[:, 3] = 1.0 

236 

237 if state.size_parameter: 

238 sizes = tab.get_col(state.size_parameter) 

239 else: 

240 sizes = num.ones(tab.get_nrows()) 

241 

242 if state.color_parameter: 

243 values = self._table.get_col(state.color_parameter) 

244 else: 

245 values = num.zeros(tab.get_nrows()) 

246 

247 rsizes = inormalize( 

248 sizes, self._isize_min, self._isize_max, discrete=False) * 0.005 

249 

250 pipe = BeachballPipe(positions, m6s, rsizes, values, self._parent.ren) 

251 self._pipes = [pipe] 

252 

253 def _update_pipes_scatter(self): 

254 state = self._state 

255 for i, p in enumerate(self._pipes): 

256 self._parent.add_actor(p.actor) 

257 p.set_size(state.size * (self._isize_min + i)**1.3) 

258 

259 if state.color_parameter: 

260 values = self._table.get_col(state.color_parameter) 

261 

262 if num.issubdtype(values.dtype, num.string_): 

263 values = string_to_sorted_idx(values) 

264 

265 self.cpt_handler._values = values 

266 self.cpt_handler.update_cpt() 

267 self.cpt_handler.update_cbar(state.color_parameter) 

268 

269 cpt = copy.deepcopy( 

270 self.cpt_handler._cpts[self._state.cpt.effective_cpt_name]) 

271 colors2 = cpt(values) 

272 colors2 = colors2 / 255. 

273 

274 for m, p in zip(self._pipe_maps, self._pipes): 

275 p.set_colors(colors2[m, :]) 

276 

277 for p in self._pipes: 

278 p.set_symbol(state.symbol) 

279 

280 def _update_pipes_beachball(self): 

281 state = self._state 

282 

283 p = self._pipes[0] 

284 

285 self._parent.add_actor(p.actor) 

286 p.set_size_factor(state.size * 0.005) 

287 

288 def _init_pipes(self): 

289 if self._state.symbol == 'beachball': 

290 self._init_pipes_beachball() 

291 else: 

292 self._init_pipes_scatter() 

293 

294 def _update_pipes(self): 

295 if self._state.symbol == 'beachball': 

296 self._update_pipes_beachball() 

297 else: 

298 self._update_pipes_scatter() 

299 

300 def update(self, *args): 

301 state = self._state 

302 

303 if self._pipes is not None and self._istate != self._istate_view: 

304 self._clear_pipes() 

305 

306 if not state.visible: 

307 if self._pipes is not None: 

308 for p in self._pipes: 

309 self._parent.remove_actor(p.actor) 

310 

311 else: 

312 if self._istate != self._istate_view and self._table: 

313 self._init_pipes() 

314 self._istate_view = self._istate 

315 

316 if self._pipes is not None: 

317 self._update_pipes() 

318 

319 self.update_alpha() # TODO: only if needed? 

320 self._parent.update_view() 

321 

322 def update_alpha(self, *args, mask=None): 

323 if self._state.symbol == 'beachball': 

324 return 

325 

326 if self._pipes is None: 

327 return 

328 

329 time = self._table.get_col('time') 

330 depth = self._table.get_col('depth') 

331 

332 depth_mask = num.ones(time.size, dtype=bool) 

333 

334 if self._state.depth_min is not None: 

335 depth_mask &= depth >= self._state.depth_min 

336 if self._state.depth_max is not None: 

337 depth_mask &= depth <= self._state.depth_max 

338 

339 tmin = self._parent.state.tmin_effective 

340 tmax = self._parent.state.tmax_effective 

341 

342 if tmin is not None: 

343 m1 = time < tmin 

344 else: 

345 m1 = num.zeros(time.size, dtype=bool) 

346 

347 if tmax is not None: 

348 m3 = tmax < time 

349 else: 

350 m3 = num.zeros(time.size, dtype=bool) 

351 

352 m2 = num.logical_not(num.logical_or(m1, m3)) 

353 

354 value_low = self._state.time_masking_opacity 

355 

356 f1, f2, f3 = MaskingModeChoice.get_factors( 

357 self._state.time_masking_mode, value_low) 

358 

359 amp = num.ones(time.size, dtype=num.float64) 

360 amp[m1] = f1 

361 amp[m3] = f3 

362 if None in (tmin, tmax): 

363 amp[m2] = 1.0 

364 else: 

365 if self._state.time_masking_shape == 'rect': 

366 amp[m2] == 1.0 

367 elif self._state.time_masking_shape == 'linear': 

368 amp[m2] = time[m2] 

369 amp[m2] -= tmin 

370 amp[m2] /= (tmax - tmin) 

371 elif self._state.time_masking_shape == 'quadratic': 

372 amp[m2] = time[m2] 

373 amp[m2] -= tmin 

374 amp[m2] /= (tmax - tmin) 

375 amp[m2] **= 2 

376 

377 if f1 != 0.0: 

378 amp[m2] *= (1.0 - value_low) 

379 amp[m2] += value_low 

380 

381 amp *= depth_mask 

382 

383 for m, p in zip(self._pipe_maps, self._pipes): 

384 p.set_alpha(amp[m]) 

385 

386 self._parent.update_view() 

387 

388 def _get_table_widgets_start(self): 

389 return 0 

390 

391 def _get_controls(self): 

392 if self._controls is None: 

393 from ..state import state_bind_slider, state_bind_slider_float, \ 

394 state_bind_combobox, state_bind_lineedit 

395 

396 frame = qw.QFrame() 

397 layout = qw.QGridLayout() 

398 frame.setLayout(layout) 

399 

400 iy = self._get_table_widgets_start() 

401 

402 layout.addWidget(qw.QLabel('Size'), iy, 0) 

403 

404 slider = qw.QSlider(qc.Qt.Horizontal) 

405 slider.setSizePolicy( 

406 qw.QSizePolicy( 

407 qw.QSizePolicy.Expanding, qw.QSizePolicy.Fixed)) 

408 slider.setMinimum(0) 

409 slider.setMaximum(100) 

410 layout.addWidget(slider, iy, 1) 

411 state_bind_slider(self, self._state, 'size', slider, factor=0.1) 

412 

413 iy += 1 

414 

415 layout.addWidget(qw.QLabel('Size Scaling'), iy, 0) 

416 

417 cb = qw.QComboBox() 

418 

419 layout.addWidget(cb, iy, 1) 

420 state_bind_combobox( 

421 self, self._state, 'size_parameter', cb) 

422 

423 self._size_combobox = cb 

424 

425 iy += 1 

426 

427 layout.addWidget(qw.QLabel('Color'), iy, 0) 

428 

429 cb = qw.QComboBox() 

430 

431 layout.addWidget(cb, iy, 1) 

432 state_bind_combobox( 

433 self, self._state, 'color_parameter', cb) 

434 

435 self._color_combobox = cb 

436 

437 self.cpt_handler.cpt_controls( 

438 self._parent, self._state.cpt, layout) 

439 

440 iy = layout.rowCount() + 1 

441 

442 layout.addWidget(qw.QLabel('Symbol'), iy, 0) 

443 

444 cb = common.string_choices_to_combobox(SymbolChoice) 

445 

446 layout.addWidget(cb, iy, 1) 

447 state_bind_combobox( 

448 self, self._state, 'symbol', cb) 

449 

450 iy += 1 

451 

452 layout.addWidget(qw.QLabel('Depth Min [km]'), iy, 0) 

453 slider = gui_util.QSliderFloat(qc.Qt.Horizontal) 

454 slider.setSizePolicy( 

455 qw.QSizePolicy( 

456 qw.QSizePolicy.Expanding, qw.QSizePolicy.Fixed)) 

457 slider.setMinimumFloat(-60*km) 

458 slider.setMaximumFloat(700*km) 

459 layout.addWidget(slider, iy, 1) 

460 state_bind_slider_float( 

461 self, self._state, 'depth_min', slider, 

462 min_is_none=True) 

463 self._depth_min_slider = slider 

464 

465 le = qw.QLineEdit() 

466 layout.addWidget(le, iy, 2) 

467 state_bind_lineedit( 

468 self, self._state, 'depth_min', le, 

469 from_string=lambda s: None if s == 'off' else float(s)*1000., 

470 to_string=lambda v: 'off' if v is None else str(v/1000.)) 

471 

472 self._depth_min_lineedit = le 

473 

474 iy += 1 

475 

476 layout.addWidget(qw.QLabel('Depth Max [km]'), iy, 0) 

477 slider = gui_util.QSliderFloat(qc.Qt.Horizontal) 

478 slider.setSizePolicy( 

479 qw.QSizePolicy( 

480 qw.QSizePolicy.Expanding, qw.QSizePolicy.Fixed)) 

481 slider.setMinimumFloat(-60*km) 

482 slider.setMaximumFloat(700*km) 

483 layout.addWidget(slider, iy, 1) 

484 state_bind_slider_float( 

485 self, self._state, 'depth_max', slider, 

486 max_is_none=True) 

487 self._depth_max_slider = slider 

488 

489 le = qw.QLineEdit() 

490 layout.addWidget(le, iy, 2) 

491 state_bind_lineedit( 

492 self, self._state, 'depth_max', le, 

493 from_string=lambda s: None if s == 'off' else float(s)*1000., 

494 to_string=lambda v: 'off' if v is None else str(v/1000.)) 

495 

496 self._depth_max_lineedit = le 

497 

498 iy += 1 

499 

500 layout.addWidget(qw.QLabel('Time Masking Opacity'), iy, 0) 

501 

502 slider = qw.QSlider(qc.Qt.Horizontal) 

503 slider.setSizePolicy( 

504 qw.QSizePolicy( 

505 qw.QSizePolicy.Expanding, qw.QSizePolicy.Fixed)) 

506 slider.setMinimum(0) 

507 slider.setMaximum(100) 

508 layout.addWidget(slider, iy, 1) 

509 state_bind_slider( 

510 self, self._state, 'time_masking_opacity', slider, factor=0.01) 

511 

512 iy += 1 

513 

514 layout.addWidget(qw.QLabel('Time Masking Shape'), iy, 0) 

515 cb = common.string_choices_to_combobox(MaskingShapeChoice) 

516 layout.addWidget(cb, iy, 1) 

517 state_bind_combobox(self, self._state, 'time_masking_shape', cb) 

518 

519 iy += 1 

520 

521 layout.addWidget(qw.QLabel('Time Masking Mode'), iy, 0) 

522 cb = common.string_choices_to_combobox(MaskingModeChoice) 

523 layout.addWidget(cb, iy, 1) 

524 state_bind_combobox(self, self._state, 'time_masking_mode', cb) 

525 

526 iy += 1 

527 

528 layout.addWidget(qw.QFrame(), iy, 0, 1, 3) 

529 

530 self._controls = frame 

531 

532 self._update_controls() 

533 

534 return self._controls 

535 

536 def _update_controls(self): 

537 for (cb, get_extra_entries) in [ 

538 (self._color_combobox, self.get_color_parameter_extra_entries), 

539 (self._size_combobox, self.get_size_parameter_extra_entries)]: 

540 

541 if cb is not None: 

542 cb.clear() 

543 

544 have = set() 

545 for s in get_extra_entries(): 

546 if s not in have: 

547 cb.insertItem(len(have), s) 

548 have.add(s) 

549 

550 if self._table is not None: 

551 for s in self._table.get_col_names(): 

552 h = self._table.get_header(s) 

553 if h.get_ncols() == 1 and s not in have: 

554 cb.insertItem(len(have), s) 

555 have.add(s) 

556 

557 self.cpt_handler._update_cpt_combobox() 

558 self.cpt_handler._update_cptscale_lineedit() 

559 

560 if self._table is not None and self._table.has_col('depth'): 

561 depth = self._table.get_col('depth') 

562 

563 if depth.size > 0: 

564 

565 depth_min = depth.min() 

566 depth_max = depth.max() 

567 

568 for wdg in (self._depth_min_slider, self._depth_max_slider): 

569 wdg.setMinimumFloat(depth_min) 

570 wdg.setMaximumFloat(depth_max) 

571 

572 

573__all__ = [ 

574 'TableElement', 

575 'TableState', 

576]