Coverage for src/km3pipe/calib.py: 69%

262 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-20 03:14 +0000

1# Filename: calib.py 

2# pylint: disable=locally-disabled 

3""" 

4Calibration. 

5 

6""" 

7import awkward as ak 

8import numba as nb 

9import numpy as np 

10 

11import km3db 

12import km3io 

13 

14from thepipe import Module 

15from km3pipe.hardware import Detector 

16from km3pipe.dataclasses import Table 

17from km3pipe.tools import istype 

18from km3pipe.logger import get_logger 

19 

20__author__ = "Tamas Gal" 

21__copyright__ = "Copyright 2016, Tamas Gal and the KM3NeT collaboration." 

22__credits__ = ["Thomas Heid"] 

23__license__ = "MIT" 

24__maintainer__ = "Tamas Gal" 

25__email__ = "tgal@km3net.de" 

26__status__ = "Development" 

27 

28log = get_logger(__name__) 

29 

30 

31class Calibration(Module): 

32 """A module which applies time, position and rotation corrections to hits. 

33 

34 This module also calibrates MC hits, but be aware, t0s are not appended to 

35 the MC hit times. 

36 Additionally, the global PMT ID is added to regular hits as ``pmt_id`` and 

37 in case of MC hits, the ``dom_id`` and ``channel_id`` (DAQ) are set. 

38 

39 Parameters 

40 ---------- 

41 apply: bool, optional [default=True] 

42 Apply the calibration to the hits (add position/direction/t0)? 

43 filename: str, optional [default=None] 

44 DetX file with detector description. 

45 det_id: int, optional 

46 .detx ID of detector (when retrieving from database). 

47 t0set: optional 

48 t0set (when retrieving from database). 

49 calibset: optional 

50 calibset (when retrieving from database). 

51 key: str, optional [default="Hits"] 

52 the blob key of the hits 

53 outkey: str, optional [default="CalibHits"] 

54 the output blob key of the calibrated hits 

55 key_mc: str, optional [default="McHits"] 

56 the blob key of the MC hits (if present) 

57 outkey_mc: str, optional [default="CalibMcHits"] 

58 the output blob key of the calibrated MC hits 

59 """ 

60 

61 __name__ = "Calibration" 

62 name = "Calibration" 

63 

64 def configure(self): 

65 self._should_apply = self.get("apply", default=True) 

66 self.filename = self.get("filename") 

67 self.det_id = self.get("det_id") 

68 self.run = self.get("run") 

69 self.t0set = self.get("t0set") 

70 self.calibset = self.get("calibset") 

71 self.detector = self.get("detector") 

72 self.key = self.get("key", default="Hits") 

73 self.outkey = self.get("outkey", default="CalibHits") 

74 self.key_mc = self.get("key_mc", default="McHits") 

75 self.outkey_mc = self.get("outkey_mc", default="CalibMcHits") 

76 self._pos_dom_channel = None 

77 self._dir_dom_channel = None 

78 self._t0_dom_channel = None 

79 self._pos_pmt_id = None 

80 self._dir_pmt_id = None 

81 self._t0_pmt_id = None 

82 self._lookup_tables = None # for Numba 

83 

84 if self.det_id and self.run: 

85 self.cprint( 

86 "Grabbing the calibration for Det ID {} and run {}".format( 

87 self.det_id, self.run 

88 ) 

89 ) 

90 raw_detx = km3db.tools.detx_for_run(self.det_id, self.run) 

91 self.detector = Detector(string=raw_detx) 

92 self._create_dom_channel_lookup() 

93 self._create_pmt_id_lookup() 

94 return 

95 

96 if self.filename or self.det_id: 

97 if self.filename is not None: 

98 self.detector = Detector(filename=self.filename) 

99 if self.det_id: 

100 self.detector = Detector( 

101 det_id=self.det_id, t0set=self.t0set, calibset=self.calibset 

102 ) 

103 

104 if self.detector is not None: 

105 self.log.debug("Creating lookup tables") 

106 self._create_dom_channel_lookup() 

107 self._create_pmt_id_lookup() 

108 else: 

109 self.log.critical("No detector information loaded.") 

110 

111 def process(self, blob): 

112 if self._should_apply: 

113 blob[self.outkey] = self.apply(blob[self.key]) 

114 if self.key_mc in blob: 

115 blob[self.outkey_mc] = self.apply(blob[self.key_mc]) 

116 return blob 

117 

118 def get_detector(self): 

119 """Return the detector""" 

120 return self.detector 

121 

122 def dus(self, hits): 

123 """Return the DUs for given hits""" 

124 dom_ids = hits["dom_id"] 

125 return _dus(dom_ids, len(dom_ids), self._calib_by_dom_and_channel) 

126 

127 def floors(self, hits): 

128 """Return the floors for given hits""" 

129 dom_ids = hits["dom_id"] 

130 return _floors(dom_ids, len(dom_ids), self._calib_by_dom_and_channel) 

131 

132 def apply_t0(self, hits): 

133 """Apply only t0s""" 

134 apply_t0_nb(hits.time, hits.dom_id, hits.channel_id, self._lookup_tables) 

135 return hits 

136 

137 def apply(self, hits, no_copy=False, correct_slewing=True, slewing_variant=3): 

138 """Add x, y, z, t0 (and du, floor if DataFrame) columns to the hits.""" 

139 if not no_copy: 

140 try: 

141 hits = hits.copy() 

142 except AttributeError: # probably a km3io object 

143 pass 

144 

145 if isinstance(hits, (ak.Array, ak.Record, km3io.rootio.Branch)): 

146 if hasattr(hits, "dom_id"): 

147 hits = Table( 

148 dict( 

149 dom_id=hits.dom_id, 

150 channel_id=hits.channel_id, 

151 time=hits.t, 

152 tot=hits.tot, 

153 triggered=hits.trig, 

154 ) 

155 ) 

156 else: # mc_hits in km3io 

157 hits = Table( 

158 dict( 

159 pmt_id=hits.pmt_id, 

160 time=hits.t, 

161 a=hits.a, 

162 # TODO: Not all MC files have these two fields 

163 # pure_a=hits.pure_a, 

164 # pure_t=hits.pure_t, 

165 origin=hits.origin, 

166 ) 

167 ) 

168 

169 if istype(hits, "DataFrame"): 

170 # do we ever see McHits here? 

171 hits = Table.from_template(hits, "Hits") 

172 

173 is_mc = None 

174 if hasattr(hits, "dom_id") and hasattr(hits, "channel_id"): 

175 try: 

176 ( 

177 dir_x, 

178 dir_y, 

179 dir_z, 

180 du, 

181 floor, 

182 pos_x, 

183 pos_y, 

184 pos_z, 

185 t0, 

186 pmt_id, 

187 ) = _get_calibration_for_hits(hits, self._calib_by_dom_and_channel) 

188 except KeyError as e: 

189 self.log.critical("Wrong calibration (DETX) data provided.") 

190 raise 

191 is_mc = False 

192 elif hasattr(hits, "pmt_id"): 

193 try: 

194 ( 

195 dir_x, 

196 dir_y, 

197 dir_z, 

198 du, 

199 floor, 

200 pos_x, 

201 pos_y, 

202 pos_z, 

203 t0, 

204 dom_id, 

205 channel_id, 

206 ) = _get_calibration_for_mchits(hits, self._calib_by_pmt_id) 

207 except KeyError as e: 

208 self.log.critical("Wrong calibration (DETX) data provided.") 

209 raise 

210 is_mc = True 

211 else: 

212 raise TypeError( 

213 "Don't know how to apply calibration to '{0}'. " 

214 "We need at least 'dom_id' and 'channel_id', or " 

215 "'pmt_id'.".format(hits.name) 

216 ) 

217 

218 if hasattr(hits, "time") and not is_mc: 

219 if hits.time.dtype != t0.dtype: 

220 time = hits.time.astype("f4") + t0.astype("f4") 

221 hits = hits.drop_columns(["time"]) 

222 hits = hits.append_columns(["time"], [time]) 

223 else: 

224 hits.time += t0 

225 

226 hits_data = {} 

227 for colname in hits.dtype.names: 

228 hits_data[colname] = hits[colname] 

229 calib = { 

230 "dir_x": dir_x, 

231 "dir_y": dir_y, 

232 "dir_z": dir_z, 

233 "du": du.astype(np.uint8), 

234 "floor": floor.astype(np.uint8), 

235 "pos_x": pos_x, 

236 "pos_y": pos_y, 

237 "pos_z": pos_z, 

238 "t0": t0, 

239 } 

240 

241 if is_mc: 

242 calib["dom_id"] = dom_id.astype(np.int32) 

243 calib["channel_id"] = channel_id.astype(np.int32) 

244 else: 

245 calib["pmt_id"] = pmt_id.astype(np.int32) 

246 

247 hits_data.update(calib) 

248 

249 if correct_slewing and not is_mc: 

250 hits_data["time"] -= slew(hits_data["tot"], variant=slewing_variant) 

251 return Table( 

252 hits_data, h5loc=hits.h5loc, split_h5=hits.split_h5, name=hits.name 

253 ) 

254 

255 def _create_dom_channel_lookup(self): 

256 data = nb.typed.Dict.empty( 

257 key_type=nb.types.i8, value_type=nb.types.float64[:, :] 

258 ) 

259 for pmt in self.detector.pmts: 

260 if pmt.dom_id not in data: 

261 data[pmt.dom_id] = np.zeros((31, 10)) 

262 data[pmt.dom_id][pmt.channel_id] = np.asarray( 

263 [ 

264 pmt.pos_x, 

265 pmt.pos_y, 

266 pmt.pos_z, 

267 pmt.dir_x, 

268 pmt.dir_y, 

269 pmt.dir_z, 

270 pmt.t0, 

271 pmt.du, 

272 pmt.floor, 

273 pmt.pmt_id, 

274 ], 

275 dtype=np.float64, 

276 ) 

277 self._calib_by_dom_and_channel = data 

278 self._lookup_tables = [(dom, cal) for dom, cal in data.items()] 

279 

280 def _create_pmt_id_lookup(self): 

281 data = nb.typed.Dict.empty(key_type=nb.types.i8, value_type=nb.types.float64[:]) 

282 for pmt in self.detector.pmts: 

283 data[pmt.pmt_id] = np.asarray( 

284 [ 

285 pmt.pos_x, 

286 pmt.pos_y, 

287 pmt.pos_z, 

288 pmt.dir_x, 

289 pmt.dir_y, 

290 pmt.dir_z, 

291 pmt.t0, 

292 pmt.du, 

293 pmt.floor, 

294 pmt.dom_id, 

295 pmt.channel_id, 

296 ], 

297 dtype=np.float64, 

298 ) 

299 self._calib_by_pmt_id = data 

300 

301 def __repr__(self): 

302 return self.__str__() 

303 

304 def __str__(self): 

305 return "Calibration: det_id({0})".format(self.det_id) 

306 

307 

308@nb.njit 

309def apply_t0_nb(times, dom_ids, channel_ids, lookup_tables): 

310 """Apply t0s using a lookup table of tuples (dom_id, calib)""" 

311 dom_id = 0 

312 lookup = np.empty((31, 9)) 

313 for i in range(len(times)): 

314 cur_dom_id = dom_ids[i] 

315 if cur_dom_id != dom_id: 

316 dom_id = cur_dom_id 

317 for (d, m) in lookup_tables: 

318 if d == dom_id: 

319 np.copyto(lookup, m) 

320 t0 = lookup[channel_ids[i]][6] 

321 times[i] += t0 

322 

323 

324@nb.jit(nopython=True) 

325def _get_calibration_for_hits(hits, lookup): 

326 """Append the position, direction and t0 columns and add t0 to time""" 

327 n = len(hits) 

328 cal = np.empty((n, 10)) 

329 for i in range(n): 

330 calib = lookup[hits["dom_id"][i]][hits["channel_id"][i]] 

331 cal[i] = calib 

332 dir_x = cal[:, 3] 

333 dir_y = cal[:, 4] 

334 dir_z = cal[:, 5] 

335 du = cal[:, 7] 

336 floor = cal[:, 8] 

337 pos_x = cal[:, 0] 

338 pos_y = cal[:, 1] 

339 pos_z = cal[:, 2] 

340 pmt_id = cal[:, 9] 

341 

342 t0 = cal[:, 6] 

343 

344 return [dir_x, dir_y, dir_z, du, floor, pos_x, pos_y, pos_z, t0, pmt_id] 

345 

346 

347@nb.jit(nopython=True) 

348def _get_calibration_for_mchits(hits, lookup): 

349 """Append the position, direction and t0 columns and add t0 to time""" 

350 n_hits = len(hits) 

351 cal = np.empty((n_hits, 11)) 

352 for i in range(n_hits): 

353 cal[i] = lookup[hits["pmt_id"][i]] 

354 dir_x = cal[:, 3] 

355 dir_y = cal[:, 4] 

356 dir_z = cal[:, 5] 

357 du = cal[:, 7] 

358 floor = cal[:, 8] 

359 pos_x = cal[:, 0] 

360 pos_y = cal[:, 1] 

361 pos_z = cal[:, 2] 

362 t0 = cal[:, 6] 

363 dom_id = cal[:, 9] 

364 channel_id = cal[:, 10] 

365 

366 return [dir_x, dir_y, dir_z, du, floor, pos_x, pos_y, pos_z, t0, dom_id, channel_id] 

367 

368 

369@nb.njit 

370def _dus(dom_ids, n, lookup): 

371 dus = np.empty(n, dtype=np.uint8) 

372 for i in range(n): 

373 # DU is at index 7 

374 dus[i] = lookup[dom_ids[i]][0][7] # looking only at channel ID 0 

375 return dus 

376 

377 

378@nb.njit 

379def _floors(dom_ids, n, lookup): 

380 dus = np.empty(n, dtype=np.uint8) 

381 for i in range(n): 

382 # floor is at index 8 

383 dus[i] = lookup[dom_ids[i]][0][8] # looking only at channel ID 0 

384 return dus 

385 

386 

387class CalibrationService(Module): 

388 """A service which provides calibration routines for hits 

389 

390 Parameters 

391 ---------- 

392 filename: str, optional [default=None] 

393 DetX file with detector description. 

394 det_id: int, optional 

395 .detx ID of detector (when retrieving from database). 

396 t0set: optional 

397 t0set (when retrieving from database). 

398 calibset: optional 

399 calibset (when retrieving from database). 

400 detector: kp.hardware.Detector, optional 

401 """ 

402 

403 __name__ = "Calibration" 

404 name = "Calibration" 

405 

406 def configure(self): 

407 self.filename = self.get("filename") 

408 self.det_id = self.get("det_id") 

409 self.t0set = self.get("t0set") 

410 self.calibset = self.get("calibset") 

411 

412 self._detector = self.get("detector") 

413 

414 if self._detector is not None: 

415 self._calibration = Calibration(detector=self._detector) 

416 

417 self._calibration = None 

418 

419 self.expose(self.calibrate, "calibrate") 

420 self.expose(self.get_detector, "get_detector") 

421 self.expose(self.get_calibration, "get_calibration") 

422 self.expose(self.load_calibration, "load_calibration") 

423 self.expose(self.correct_slewing, "correct_slewing") 

424 

425 def load_calibration(self, filename=None, det_id=None, t0set=None, calibset=None): 

426 """Load another calibration""" 

427 self.filename = filename 

428 self.det_id = det_id 

429 self.t0set = t0set 

430 self.calibset = calibset 

431 self._detector = None 

432 self._calibration = None 

433 

434 def calibrate(self, hits, correct_slewing=True): 

435 return self.calibration.apply(hits, correct_slewing=correct_slewing) 

436 

437 @property 

438 def detector(self): 

439 if self._detector is None: 

440 self._detector = self.calibration.detector 

441 return self._detector 

442 

443 def get_detector(self): 

444 """Extra getter to be as lazy as possible (expose triggers otherwise""" 

445 return self.detector 

446 

447 @property 

448 def calibration(self): 

449 if self._calibration is None: 

450 self._calibration = Calibration( 

451 filename=self.filename, 

452 det_id=self.det_id, 

453 t0set=self.t0set, 

454 calibset=self.calibset, 

455 ) 

456 return self._calibration 

457 

458 def get_calibration(self): 

459 """Extra getter to be as lazy as possible (expose triggers otherwise""" 

460 return self.calibration 

461 

462 def correct_slewing(self, hits): 

463 """Apply time slewing correction to the hit times""" 

464 hits.time -= slew(hits.tot) 

465 

466 

467def slew(tot, variant=3): 

468 """Calculate the time slewing of a PMT response for a given ToT 

469 

470 

471 Parameters 

472 ---------- 

473 tot: int or np.array(int) 

474 Time over threshold value of a hit 

475 variant: int, optional 

476 The variant of the slew calculation. 

477 1: The first parametrisation approach 

478 2: Jannik's improvement of the parametrisation 

479 3: The latest lookup table approach based on lab measurements. 

480 

481 Returns 

482 ------- 

483 time: int 

484 Time slewing, which has to be subtracted from the original hit time. 

485 """ 

486 if variant == 1: 

487 return _slew_parametrised(7.70824, 0.00879447, -0.0621101, -1.90226, tot) 

488 if variant == 2: 

489 return _slew_parametrised( 

490 13.6488662517, -0.128744123166, -0.0174837749244, -4.47119633965, tot 

491 ) 

492 if variant == 3: 

493 if isinstance(tot, (int, np.integer)): 

494 return _SLEWS[tot] 

495 return _slew_tabulated(np.array(_SLEWS), tot) 

496 

497 raise ValueError("Unknown slew calculation variant '{}'".format(variant)) 

498 

499 

500@nb.jit(nopython=True) 

501def _slew_parametrised(p0, p1, p2, p3, tot): 

502 return p0 * np.exp(p1 * np.sqrt(tot) + p2 * tot) + p3 

503 

504 

505@nb.jit(nopython=True) 

506def _slew_tabulated(tab, tots): 

507 n = len(tots) 

508 out = np.empty(n) 

509 for i in range(n): 

510 out[i] = tab[tots[i]] 

511 return out 

512 

513 

514_SLEWS = np.array( 

515 [ 

516 8.01, 

517 7.52, 

518 7.05, 

519 6.59, 

520 6.15, 

521 5.74, 

522 5.33, 

523 4.95, 

524 4.58, 

525 4.22, 

526 3.89, 

527 3.56, 

528 3.25, 

529 2.95, 

530 2.66, 

531 2.39, 

532 2.12, 

533 1.87, 

534 1.63, 

535 1.40, 

536 1.19, 

537 0.98, 

538 0.78, 

539 0.60, 

540 0.41, 

541 0.24, 

542 0.07, 

543 -0.10, 

544 -0.27, 

545 -0.43, 

546 -0.59, 

547 -0.75, 

548 -0.91, 

549 -1.08, 

550 -1.24, 

551 -1.41, 

552 -1.56, 

553 -1.71, 

554 -1.85, 

555 -1.98, 

556 -2.11, 

557 -2.23, 

558 -2.35, 

559 -2.47, 

560 -2.58, 

561 -2.69, 

562 -2.79, 

563 -2.89, 

564 -2.99, 

565 -3.09, 

566 -3.19, 

567 -3.28, 

568 -3.37, 

569 -3.46, 

570 -3.55, 

571 -3.64, 

572 -3.72, 

573 -3.80, 

574 -3.88, 

575 -3.96, 

576 -4.04, 

577 -4.12, 

578 -4.20, 

579 -4.27, 

580 -4.35, 

581 -4.42, 

582 -4.49, 

583 -4.56, 

584 -4.63, 

585 -4.70, 

586 -4.77, 

587 -4.84, 

588 -4.90, 

589 -4.97, 

590 -5.03, 

591 -5.10, 

592 -5.16, 

593 -5.22, 

594 -5.28, 

595 -5.34, 

596 -5.40, 

597 -5.46, 

598 -5.52, 

599 -5.58, 

600 -5.63, 

601 -5.69, 

602 -5.74, 

603 -5.80, 

604 -5.85, 

605 -5.91, 

606 -5.96, 

607 -6.01, 

608 -6.06, 

609 -6.11, 

610 -6.16, 

611 -6.21, 

612 -6.26, 

613 -6.31, 

614 -6.36, 

615 -6.41, 

616 -6.45, 

617 -6.50, 

618 -6.55, 

619 -6.59, 

620 -6.64, 

621 -6.68, 

622 -6.72, 

623 -6.77, 

624 -6.81, 

625 -6.85, 

626 -6.89, 

627 -6.93, 

628 -6.98, 

629 -7.02, 

630 -7.06, 

631 -7.09, 

632 -7.13, 

633 -7.17, 

634 -7.21, 

635 -7.25, 

636 -7.28, 

637 -7.32, 

638 -7.36, 

639 -7.39, 

640 -7.43, 

641 -7.46, 

642 -7.50, 

643 -7.53, 

644 -7.57, 

645 -7.60, 

646 -7.63, 

647 -7.66, 

648 -7.70, 

649 -7.73, 

650 -7.76, 

651 -7.79, 

652 -7.82, 

653 -7.85, 

654 -7.88, 

655 -7.91, 

656 -7.94, 

657 -7.97, 

658 -7.99, 

659 -8.02, 

660 -8.05, 

661 -8.07, 

662 -8.10, 

663 -8.13, 

664 -8.15, 

665 -8.18, 

666 -8.20, 

667 -8.23, 

668 -8.25, 

669 -8.28, 

670 -8.30, 

671 -8.32, 

672 -8.34, 

673 -8.37, 

674 -8.39, 

675 -8.41, 

676 -8.43, 

677 -8.45, 

678 -8.47, 

679 -8.49, 

680 -8.51, 

681 -8.53, 

682 -8.55, 

683 -8.57, 

684 -8.59, 

685 -8.61, 

686 -8.62, 

687 -8.64, 

688 -8.66, 

689 -8.67, 

690 -8.69, 

691 -8.70, 

692 -8.72, 

693 -8.74, 

694 -8.75, 

695 -8.76, 

696 -8.78, 

697 -8.79, 

698 -8.81, 

699 -8.82, 

700 -8.83, 

701 -8.84, 

702 -8.86, 

703 -8.87, 

704 -8.88, 

705 -8.89, 

706 -8.90, 

707 -8.92, 

708 -8.93, 

709 -8.94, 

710 -8.95, 

711 -8.96, 

712 -8.97, 

713 -8.98, 

714 -9.00, 

715 -9.01, 

716 -9.02, 

717 -9.04, 

718 -9.04, 

719 -9.04, 

720 -9.04, 

721 -9.04, 

722 -9.04, 

723 -9.04, 

724 -9.04, 

725 -9.04, 

726 -9.04, 

727 -9.04, 

728 -9.04, 

729 -9.04, 

730 -9.04, 

731 -9.04, 

732 -9.04, 

733 -9.04, 

734 -9.04, 

735 -9.04, 

736 -9.04, 

737 -9.04, 

738 -9.04, 

739 -9.04, 

740 -9.04, 

741 -9.04, 

742 -9.04, 

743 -9.04, 

744 -9.04, 

745 -9.04, 

746 -9.04, 

747 -9.04, 

748 -9.04, 

749 -9.04, 

750 -9.04, 

751 -9.04, 

752 -9.04, 

753 -9.04, 

754 -9.04, 

755 -9.04, 

756 -9.04, 

757 -9.04, 

758 -9.04, 

759 -9.04, 

760 -9.04, 

761 -9.04, 

762 -9.04, 

763 -9.04, 

764 -9.04, 

765 -9.04, 

766 -9.04, 

767 -9.04, 

768 -9.04, 

769 -9.04, 

770 -9.04, 

771 -9.04, 

772 ] 

773)