231 ) -> None:
232 """Draw overlay plot of multiple channel mappings on a single figure.
233
234 Parameters
235 ----------
236 number_of_channels : int, optional
237 Total number of channels to display (split evenly between U and V planes).
238 Default is 20.
239 sGeo : ROOT.TGeoManager
240 The geometry manager for the detector setup.
241 output_file : str, optional
242 Filename for the saved PDF plot. Default: 'scifi_mapping_all_channels.pdf'.
243 labeling : bool, optional
244 If True, annotate each SiPM rectangle with channel information.
245 Default is True.
246 figsize : tuple of float, optional
247 Figure size in inches. Default is (16, 16).
248 dpi : int, optional
249 Resolution of the figure in dots per inch. Default is 300.
250 cmap_name : str, optional
251 Matplotlib colormap name for differentiating channels. Default is 'tab20'.
252 alpha_fibre : float, optional
253 Transparency for fibre ellipses. Default is 0.4.
254
255 Side Effects
256 ------------
257 Saves an overlay PDF plot with the specified filename.
258
259 """
260
261 fig, ax = plt.subplots(figsize=figsize, dpi=dpi)
262
263
264 fibreVol = sGeo.FindVolumeFast("FiberVol")
265 R = fibreVol.GetShape().GetDX()
266 DX, DZ = 0.025, 0.135 / 2
267
268
269
270
271
272
273 center_channel_U = [chan for chan, x_pos in self.sipm_pos_U.items() if abs(x_pos) <= DX - 0.001][0]
274 center_channel_V = [chan for chan, x_pos in self.sipm_pos_V.items() if abs(x_pos) <= DX - 0.001][0]
275
276 def get_surrounding_keys(d: dict, target_key, N: int):
277 """Return neighbors of `target_key` in value-sorted order.
278
279 Given a dict `d`, find `target_key` in the ordering of keys sorted by
280 their values, and return up to N//2 keys that come before it and N//2 keys
281 that come after it (excluding the target_key itself).
282
283 Parameters
284 ----------
285 d : dict
286 Mapping from keys to comparable values.
287 target_key : hashable
288 The key around which to collect neighbors.
289 N : int
290 Total number of neighbors to return (N//2 before, N//2 after).
291
292 Returns
293 -------
294 list
295 List of up to N keys: first the keys before, then the keys after.
296
297 """
298
299 sorted_keys = sorted(d.keys(), key=lambda k: d[k])
300
301 try:
302 idx = sorted_keys.index(target_key)
303 except ValueError:
304 raise KeyError(f"Target key {target_key!r} not found in dictionary") from None
305
306 half = N // 2
307
308 start = max(0, idx - half)
309 end_before = idx
310 start_after = idx + 1
311 end_after = idx + 1 + half
312
313 before = sorted_keys[start:end_before]
314 after = sorted_keys[start_after:end_after]
315
316 return before + [target_key] + after
317
318 channelsU = get_surrounding_keys(self.sipm_pos_U, center_channel_U, number_of_channels)
319 channelsV = get_surrounding_keys(self.sipm_pos_V, center_channel_V, number_of_channels)
320
321 channels = channelsU + channelsV
322 n_chan = len(channels)
323
324 AF = ROOT.TVector3()
325 BF = ROOT.TVector3()
326
327
328 for chan in channels:
329 isU = chan in channelsU
330
331 locChan = chan % 1_000_000
332
333
334 xs, ys = [], []
335 for fibreID in (self.fibre_to_simp_map_U if chan in channelsU else self.fibre_to_simp_map_V)[locChan]:
336 globfiberID = (
337 fibreID + 100000000 + 1000000 + 0 * 100000
338 if chan in channelsU
339 else fibreID + 100000000 + 1000000 + 1 * 100000
340 )
341 self.scifi.GetPosition(globfiberID, AF, BF)
342 loc = self.scifi.GetLocalPos(fibreID, BF)
343 xs.append(loc[0])
344 ys.append(loc[2])
345
346
347 for x, y in zip(xs, ys):
348 ell = patches.Ellipse(
349 (x, y),
350 2 * R,
351 2 * R,
352 fill=False,
353 edgecolor="black",
354 linewidth=1,
355 alpha=alpha_fibre,
356 )
357 ax.add_patch(ell)
358
359
360 self.scifi.GetSiPMPosition(chan, BF, AF)
361 loc_siPM = self.scifi.GetLocalPos(fibreID, BF)
362 rx, ry = loc_siPM[0], loc_siPM[2]
363 rect = patches.Rectangle(
364 (rx - DX, ry - DZ),
365 2 * DX,
366 2 * DZ,
367 linewidth=1,
368 edgecolor="black",
369 facecolor="blue" if isU else "red",
370 alpha=alpha_fibre * 0.8,
371 hatch="//" if isU else "\\\\",
372 )
373 ax.add_patch(rect)
374
375 if labeling:
376
377
378 p0 = ax.transData.transform((rx - DX, ry))
379 p1 = ax.transData.transform((rx + DX, ry))
380 disp_width = abs(p1[0] - p0[0])
381
382 pts = disp_width * 72.0 / fig.dpi
383 fontsize = max(2, min(12, pts * 0.8))
384
385
386 text = f"SiPM: {(chan // 1000) % 10} \n ch: {chan % 1000}"
387 ax.text(
388 rx - DX / 2,
389 ry + DZ + R * 0.1,
390 text,
391 ha="left",
392 va="bottom",
393 fontsize=fontsize,
394 color="black",
395 clip_on=True,
396 )
397
398
399 ax.relim()
400 ax.autoscale_view()
401 ax.set_aspect("equal")
402 ax.tick_params(axis="both", which="major", labelsize=18)
403 ax.tick_params(axis="both", which="minor", labelsize=18)
404 ax.set_xlabel("X [cm]", fontsize=20)
405 ax.set_ylabel("Z [cm]", fontsize=20)
406 ax.set_title("SiPM-Channel Mappings Overlaid", fontsize=24)
407 ax.grid(True)
408
409 plt.tight_layout()
410 plt.savefig(output_file)
411 plt.close(fig)
412 print(f"Saved overlay plot of {n_chan} channels to {output_file}")
413