Coverage for src/km3modules/plot.py: 14%

183 statements  

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

1# Filename: plot.py 

2# -*- coding: utf-8 -*- 

3# pylint: disable=locally-disabled 

4""" 

5A collection of plotting functions and modules. 

6 

7""" 

8from collections import Counter 

9from datetime import datetime 

10import gc 

11import os 

12import multiprocessing as mp 

13import time 

14 

15import numpy as np 

16import matplotlib.pyplot as plt # noqa 

17import matplotlib.ticker as ticker 

18from matplotlib import pylab # noqa 

19import km3db 

20 

21import km3pipe as kp # noqa 

22import km3pipe.style # noqa 

23 

24from km3modules.hits import count_multiplicities 

25 

26 

27def plot_dom_parameters( 

28 data, 

29 detector, 

30 filename, 

31 label, 

32 title, 

33 vmin=0.0, 

34 vmax=10.0, 

35 cmap="cividis", 

36 under="deepskyblue", 

37 over="deeppink", 

38 underfactor=1.0, 

39 overfactor=1.0, 

40 missing="lightgray", 

41 hide_limits=False, 

42): 

43 """Creates a plot in the classical monitoring.km3net.de style. 

44 

45 Parameters 

46 ---------- 

47 data: dict((du, floor) -> value) 

48 detector: km3pipe.hardware.Detector() instance 

49 filename: filename or filepath 

50 label: str 

51 title: str 

52 underfactor: a scale factor for the points used for underflow values 

53 overfactor: a scale factor for the points used for overflow values 

54 hide_limits: do not show under/overflows in the plot 

55 

56 """ 

57 x, y, _ = zip(*detector.doms.values()) 

58 fig, ax = plt.subplots(figsize=(10, 6)) 

59 cmap = plt.get_cmap(cmap).copy() 

60 cmap.set_over(over, 1.0) 

61 cmap.set_under(under, 1.0) 

62 

63 m_size = 100 

64 scatter_args = { 

65 "edgecolors": "None", 

66 "vmin": vmin, 

67 "vmax": vmax, 

68 } 

69 sc_inactive = ax.scatter( 

70 x, y, c=missing, label="missing", s=m_size * 0.9, **scatter_args 

71 ) 

72 

73 xa, ya = map(np.array, zip(*data.keys())) 

74 zs = np.array(list(data.values())) 

75 in_range_idx = np.logical_and(zs >= vmin, zs <= vmax) 

76 sc = ax.scatter( 

77 xa[in_range_idx], 

78 ya[in_range_idx], 

79 c=zs[in_range_idx], 

80 cmap=cmap, 

81 s=m_size, 

82 **scatter_args 

83 ) 

84 if not hide_limits: 

85 under_idx = zs < vmin 

86 ax.scatter( 

87 xa[under_idx], 

88 ya[under_idx], 

89 c=under, 

90 label="< {0}".format(vmin), 

91 s=m_size * underfactor, 

92 **scatter_args 

93 ) 

94 over_idx = zs > vmax 

95 ax.scatter( 

96 xa[over_idx], 

97 ya[over_idx], 

98 c=over, 

99 label="> {0}".format(vmax), 

100 s=m_size * overfactor, 

101 **scatter_args 

102 ) 

103 

104 cb = plt.colorbar(sc) 

105 cb.set_label(label) 

106 

107 ax.set_title("{0}\n{1} UTC".format(title, datetime.utcnow().strftime("%c"))) 

108 ax.set_xlabel("DU") 

109 ax.set_ylabel("DOM") 

110 ax.set_ylim(-2) 

111 ax.set_yticks(range(1, 18 + 1)) 

112 major_locator = pylab.MaxNLocator(integer=True) 

113 sc_inactive.axes.xaxis.set_major_locator(major_locator) 

114 

115 ax.legend( 

116 bbox_to_anchor=(0.0, -0.16, 1.0, 0.102), 

117 loc=1, 

118 ncol=2, 

119 mode="expand", 

120 borderaxespad=0.0, 

121 ) 

122 

123 fig.tight_layout() 

124 

125 plt.savefig(filename, dpi=120, bbox_inches="tight") 

126 plt.close("all") 

127 gc.collect() 

128 

129 

130def make_dom_map(pmt_directions, values, nside=512, d=0.2, smoothing=0.1): 

131 """Create a mollweide projection of a DOM with given PMTs. 

132 

133 The output can be used to call the `healpy.mollview` function. 

134 """ 

135 import healpy as hp 

136 

137 discs = [hp.query_disc(nside, dir, 0.2) for dir in pmt_directions] 

138 npix = hp.nside2npix(nside) 

139 pixels = np.zeros(npix) 

140 for disc, value in zip(discs, values): 

141 for d in disc: 

142 pixels[d] = value 

143 if smoothing > 0: 

144 return hp.sphtfunc.smoothing(pixels, fwhm=smoothing, iter=1) 

145 return pixels 

146 

147 

148class IntraDOMCalibrationPlotter(kp.Module): 

149 def configure(self): 

150 self.plots_path = self.get("plots_path", default=os.getcwd()) 

151 self.data_path = self.get("data_path", default=os.getcwd()) 

152 self.det_oid = self.require("det_oid") 

153 self.clbmap = km3db.CLBMap(self.det_oid) 

154 

155 def process(self, blob): 

156 calibration = blob["IntraDOMCalibration"] 

157 for process in (self.create_plot, self.save_hdf5): 

158 proc = mp.Process(target=process, args=(calibration,)) 

159 proc.daemon = True 

160 proc.start() 

161 proc.join() 

162 return blob 

163 

164 def create_plot(self, calibration): 

165 print("Creating plot...") 

166 fig, axes = plt.subplots(6, 3, figsize=(16, 20), sharex=True, sharey=True) 

167 sorted_dom_ids = sorted( 

168 calibration.keys(), 

169 key=lambda d: (self.clbmap.dom_ids[d].du, self.clbmap.dom_ids[d].floor), 

170 ) # by DU and FLOOR, note that DET OID is needed! 

171 for ax, dom_id in zip(axes.flatten(), sorted_dom_ids): 

172 calib = calibration[dom_id] 

173 ax.plot(np.cos(calib["angles"]), calib["means"], ".") 

174 ax.plot(np.cos(calib["angles"]), calib["corrected_means"], ".") 

175 du = self.clbmap.dom_ids[dom_id].du 

176 floor = self.clbmap.dom_ids[dom_id].floor 

177 ax.set_title("{0} - {1}".format("DU{}-DOM{}".format(du, floor), dom_id)) 

178 ax.set_ylim((-10, 10)) 

179 plt.suptitle("{0} UTC".format(datetime.utcnow().strftime("%c"))) 

180 plt.savefig(os.path.join(self.plots_path, "intradom.png"), bbox_inches="tight") 

181 plt.close("all") 

182 

183 fig, axes = plt.subplots(6, 3, figsize=(16, 20), sharex=True, sharey=True) 

184 for ax, dom_id in zip(axes.flatten(), sorted_dom_ids): 

185 calib = calibration[dom_id] 

186 ax.plot(np.cos(calib["angles"]), calib["rates"], ".") 

187 ax.plot(np.cos(calib["angles"]), calib["corrected_rates"], ".") 

188 du = self.clbmap.dom_ids[dom_id].du 

189 floor = self.clbmap.dom_ids[dom_id].floor 

190 ax.set_title("{0} - {1}".format("DU{}-DOM{}".format(du, floor), dom_id)) 

191 ax.set_ylim((0, 10)) 

192 plt.suptitle("{0} UTC".format(datetime.utcnow().strftime("%c"))) 

193 plt.savefig( 

194 os.path.join(self.plots_path, "angular_k40rate_distribution.png"), 

195 bbox_inches="tight", 

196 ) 

197 plt.close("all") 

198 gc.collect() 

199 

200 def save_hdf5(self, calibration): 

201 print("Saving calibration information...") 

202 pd = kp.extras.pandas() 

203 store = pd.HDFStore(os.path.join(self.data_path, "k40calib.h5"), mode="a") 

204 now = int(time.time()) 

205 timestamps = (now,) * 31 

206 for dom_id, calib in calibration.items(): 

207 tdc_channels = range(31) 

208 t0s = calib["opt_t0s"].x 

209 dom_ids = (dom_id,) * 31 

210 df = pd.DataFrame( 

211 { 

212 "timestamp": timestamps, 

213 "dom_id": dom_ids, 

214 "tdc_channel": tdc_channels, 

215 "t0s": t0s, 

216 } 

217 ) 

218 store.append("t0s", df, format="table", data_columns=True) 

219 store.close() 

220 

221 

222def ztplot( 

223 hits, 

224 filename=None, 

225 title=None, 

226 max_z=None, 

227 figsize=(16, 8), 

228 n_dus=4, 

229 ytick_distance=200, 

230 max_multiplicity_entries=10, 

231 grid_lines=[], 

232): 

233 """Creates a ztplot like shown in the online monitoring""" 

234 fontsize = 16 

235 

236 dus = set(hits.du) 

237 

238 if n_dus is not None: 

239 dus = [c[0] for c in Counter(hits.du).most_common(n_dus)] 

240 mask = [du in dus for du in hits.du] 

241 hits = hits[mask] 

242 

243 dus = sorted(dus) 

244 doms = set(hits.dom_id) 

245 

246 hits = hits.append_columns("multiplicity", np.ones(len(hits))).sorted(by="time") 

247 

248 if max_z is None: 

249 max_z = int(np.ceil(np.max(hits.pos_z) / 100.0)) * 100 * 1.05 

250 

251 for dom in doms: 

252 dom_hits = hits[hits.dom_id == dom] 

253 mltps, m_ids = count_multiplicities(dom_hits.time) 

254 hits["multiplicity"][hits.dom_id == dom] = mltps 

255 

256 if np.any(hits.triggered): 

257 time_offset = np.min(hits[hits.triggered > 0].time) 

258 hits.time -= time_offset 

259 

260 n_plots = len(dus) 

261 n_cols = int(np.ceil(np.sqrt(n_plots))) 

262 n_rows = int(n_plots / n_cols) + (n_plots % n_cols > 0) 

263 marker_fig, marker_axes = plt.subplots() 

264 # for the marker size hack... 

265 fig, axes = plt.subplots( 

266 ncols=n_cols, 

267 nrows=n_rows, 

268 sharex=True, 

269 sharey=True, 

270 figsize=figsize, 

271 constrained_layout=True, 

272 ) 

273 

274 axes = [axes] if n_plots == 1 else trim_axes(axes, n_plots) 

275 

276 for ax, du in zip(axes, dus): 

277 du_hits = hits[hits.du == du] 

278 for grid_line in grid_lines: 

279 ax.axhline(grid_line, lw=1, color="b", ls="--", alpha=0.15) 

280 trig_hits = du_hits[du_hits.triggered > 0] 

281 

282 ax.scatter( 

283 du_hits.time, 

284 du_hits.pos_z, 

285 s=du_hits.multiplicity * 30, 

286 c="#09A9DE", 

287 label="hit", 

288 alpha=0.5, 

289 ) 

290 ax.scatter( 

291 trig_hits.time, 

292 trig_hits.pos_z, 

293 s=trig_hits.multiplicity * 30, 

294 alpha=0.8, 

295 marker="+", 

296 c="#FF6363", 

297 label="triggered hit", 

298 ) 

299 ax.set_title("DU{0}".format(int(du)), fontsize=fontsize, fontweight="bold") 

300 

301 # The only way I could create a legend with matching marker sizes 

302 max_multiplicity = int(np.max(du_hits.multiplicity)) 

303 markers = list( 

304 range( 

305 0, 

306 max_multiplicity, 

307 np.ceil(max_multiplicity / max_multiplicity_entries).astype(int), 

308 ) 

309 ) 

310 custom_markers = [ 

311 marker_axes.scatter([], [], s=mult * 30, color="#09A9DE", lw=0, alpha=0.5) 

312 for mult in markers 

313 ] + [marker_axes.scatter([], [], s=30, marker="+", c="#FF6363")] 

314 ax.legend( 

315 custom_markers, 

316 ["multiplicity"] + [" %d" % m for m in markers[1:]] + ["triggered"], 

317 scatterpoints=1, 

318 markerscale=1, 

319 loc="lower right", 

320 frameon=True, 

321 framealpha=0.7, 

322 ) 

323 

324 for idx, ax in enumerate(axes): 

325 ax.set_ylim(0, max_z) 

326 ax.tick_params(labelsize=fontsize) 

327 ax.yaxis.set_major_locator(ticker.MultipleLocator(ytick_distance)) 

328 xlabels = ax.get_xticklabels() 

329 for label in xlabels: 

330 label.set_rotation(45) 

331 

332 if idx % n_cols == 0: 

333 ax.set_ylabel("z [m]", fontsize=fontsize) 

334 if idx >= len(axes) - n_cols: 

335 ax.set_xlabel("time [ns]", fontsize=fontsize) 

336 

337 if title is not None: 

338 plt.suptitle(title, fontsize=fontsize, y=1.05) 

339 if filename is not None: 

340 plt.savefig(filename, dpi=120, bbox_inches="tight") 

341 

342 gc.collect() 

343 return fig 

344 

345 

346def trim_axes(axes, n): 

347 """little helper to massage the axes list to have correct length...""" 

348 axes = axes.flat 

349 for ax in axes[n:]: 

350 ax.remove() 

351 return axes[:n] 

352 

353 

354def cumulative_run_livetime(qtable, kind="runs"): 

355 """Create a figure which plots the cumulative livetime of runs 

356 

357 Parameters 

358 ---------- 

359 qtable: pandas.DataFrame 

360 A table which has the run number as index and columns for 

361 'livetime_s', 'timestamp' and 'datetime' (pandas datetime). 

362 kind: str 

363 'runs' to plot for each run or 'timeline' to plot based 

364 on the actual run time. 

365 

366 Returns 

367 ------- 

368 matplotlib.Figure 

369 """ 

370 fig, ax = plt.subplots() 

371 

372 options = { 

373 "runs": {"xlabel": "run", "xs": qtable.index}, 

374 "timeline": {"xlabel": None, "xs": qtable.datetime}, 

375 } 

376 

377 actual_livetime = np.cumsum(qtable["livetime_s"]) 

378 optimal_livetime = np.cumsum(qtable.timestamp.diff()) 

379 

380 ax.plot(options[kind]["xs"], actual_livetime, label="actual livetime") 

381 ax.plot(options[kind]["xs"], optimal_livetime, label="100% livetime") 

382 ax.set_xlabel(options[kind]["xlabel"]) 

383 ax.set_ylabel("time / s") 

384 ax.legend() 

385 

386 return fig