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

350 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2024-01-02 12:31 +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 

268 cpt = copy.deepcopy( 

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

270 colors2 = cpt(values) 

271 colors2 = colors2 / 255. 

272 

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

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

275 

276 for p in self._pipes: 

277 p.set_symbol(state.symbol) 

278 

279 def _update_pipes_beachball(self): 

280 state = self._state 

281 

282 p = self._pipes[0] 

283 

284 self._parent.add_actor(p.actor) 

285 p.set_size_factor(state.size * 0.005) 

286 

287 def _init_pipes(self): 

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

289 self._init_pipes_beachball() 

290 else: 

291 self._init_pipes_scatter() 

292 

293 def _update_pipes(self): 

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

295 self._update_pipes_beachball() 

296 else: 

297 self._update_pipes_scatter() 

298 

299 def update(self, *args): 

300 state = self._state 

301 

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

303 self._clear_pipes() 

304 

305 if not state.visible: 

306 if self._pipes is not None: 

307 for p in self._pipes: 

308 self._parent.remove_actor(p.actor) 

309 

310 else: 

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

312 self._init_pipes() 

313 self._istate_view = self._istate 

314 

315 if self._pipes is not None: 

316 self._update_pipes() 

317 

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

319 self._parent.update_view() 

320 

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

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

323 return 

324 

325 if self._pipes is None: 

326 return 

327 

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

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

330 

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

332 

333 if self._state.depth_min is not None: 

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

335 if self._state.depth_max is not None: 

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

337 

338 tmin = self._parent.state.tmin_effective 

339 tmax = self._parent.state.tmax_effective 

340 

341 if tmin is not None: 

342 m1 = time < tmin 

343 else: 

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

345 

346 if tmax is not None: 

347 m3 = tmax < time 

348 else: 

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

350 

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

352 

353 value_low = self._state.time_masking_opacity 

354 

355 f1, f2, f3 = MaskingModeChoice.get_factors( 

356 self._state.time_masking_mode, value_low) 

357 

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

359 amp[m1] = f1 

360 amp[m3] = f3 

361 if None in (tmin, tmax): 

362 amp[m2] = 1.0 

363 else: 

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

365 amp[m2] == 1.0 

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

367 amp[m2] = time[m2] 

368 amp[m2] -= tmin 

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

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

371 amp[m2] = time[m2] 

372 amp[m2] -= tmin 

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

374 amp[m2] **= 2 

375 

376 if f1 != 0.0: 

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

378 amp[m2] += value_low 

379 

380 amp *= depth_mask 

381 

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

383 p.set_alpha(amp[m]) 

384 

385 self._parent.update_view() 

386 

387 def _get_table_widgets_start(self): 

388 return 0 

389 

390 def _get_controls(self): 

391 if self._controls is None: 

392 from ..state import state_bind_slider, state_bind_slider_float, \ 

393 state_bind_combobox, state_bind_lineedit 

394 

395 frame = qw.QFrame() 

396 layout = qw.QGridLayout() 

397 frame.setLayout(layout) 

398 

399 iy = self._get_table_widgets_start() 

400 

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

402 

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

404 slider.setSizePolicy( 

405 qw.QSizePolicy( 

406 qw.QSizePolicy.Expanding, qw.QSizePolicy.Fixed)) 

407 slider.setMinimum(0) 

408 slider.setMaximum(100) 

409 layout.addWidget(slider, iy, 1) 

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

411 

412 iy += 1 

413 

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

415 

416 cb = qw.QComboBox() 

417 

418 layout.addWidget(cb, iy, 1) 

419 state_bind_combobox( 

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

421 

422 self._size_combobox = cb 

423 

424 iy += 1 

425 

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

427 

428 cb = qw.QComboBox() 

429 

430 layout.addWidget(cb, iy, 1) 

431 state_bind_combobox( 

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

433 

434 self._color_combobox = cb 

435 

436 self.cpt_handler.cpt_controls( 

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

438 

439 iy = layout.rowCount() + 1 

440 

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

442 

443 cb = common.string_choices_to_combobox(SymbolChoice) 

444 

445 layout.addWidget(cb, iy, 1) 

446 state_bind_combobox( 

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

448 

449 iy += 1 

450 

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

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

453 slider.setSizePolicy( 

454 qw.QSizePolicy( 

455 qw.QSizePolicy.Expanding, qw.QSizePolicy.Fixed)) 

456 slider.setMinimumFloat(-60*km) 

457 slider.setMaximumFloat(700*km) 

458 layout.addWidget(slider, iy, 1) 

459 state_bind_slider_float( 

460 self, self._state, 'depth_min', slider, 

461 min_is_none=True) 

462 self._depth_min_slider = slider 

463 

464 le = qw.QLineEdit() 

465 layout.addWidget(le, iy, 2) 

466 state_bind_lineedit( 

467 self, self._state, 'depth_min', le, 

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

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

470 

471 self._depth_min_lineedit = le 

472 

473 iy += 1 

474 

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

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

477 slider.setSizePolicy( 

478 qw.QSizePolicy( 

479 qw.QSizePolicy.Expanding, qw.QSizePolicy.Fixed)) 

480 slider.setMinimumFloat(-60*km) 

481 slider.setMaximumFloat(700*km) 

482 layout.addWidget(slider, iy, 1) 

483 state_bind_slider_float( 

484 self, self._state, 'depth_max', slider, 

485 max_is_none=True) 

486 self._depth_max_slider = slider 

487 

488 le = qw.QLineEdit() 

489 layout.addWidget(le, iy, 2) 

490 state_bind_lineedit( 

491 self, self._state, 'depth_max', le, 

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

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

494 

495 self._depth_max_lineedit = le 

496 

497 iy += 1 

498 

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

500 

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

502 slider.setSizePolicy( 

503 qw.QSizePolicy( 

504 qw.QSizePolicy.Expanding, qw.QSizePolicy.Fixed)) 

505 slider.setMinimum(0) 

506 slider.setMaximum(100) 

507 layout.addWidget(slider, iy, 1) 

508 state_bind_slider( 

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

510 

511 iy += 1 

512 

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

514 cb = common.string_choices_to_combobox(MaskingShapeChoice) 

515 layout.addWidget(cb, iy, 1) 

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

517 

518 iy += 1 

519 

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

521 cb = common.string_choices_to_combobox(MaskingModeChoice) 

522 layout.addWidget(cb, iy, 1) 

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

524 

525 iy += 1 

526 

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

528 

529 self._controls = frame 

530 

531 self._update_controls() 

532 

533 return self._controls 

534 

535 def _update_controls(self): 

536 for (cb, get_extra_entries) in [ 

537 (self._color_combobox, self.get_color_parameter_extra_entries), 

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

539 

540 if cb is not None: 

541 cb.clear() 

542 

543 have = set() 

544 for s in get_extra_entries(): 

545 if s not in have: 

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

547 have.add(s) 

548 

549 if self._table is not None: 

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

551 h = self._table.get_header(s) 

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

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

554 have.add(s) 

555 

556 self.cpt_handler._update_cpt_combobox() 

557 self.cpt_handler._update_cptscale_lineedit() 

558 

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

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

561 

562 if depth.size > 0: 

563 

564 depth_min = depth.min() 

565 depth_max = depth.max() 

566 

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

568 wdg.setMinimumFloat(depth_min) 

569 wdg.setMaximumFloat(depth_max) 

570 

571 

572__all__ = [ 

573 'TableElement', 

574 'TableState', 

575]