FairShip
Loading...
Searching...
No Matches
SciFiMapping.py
Go to the documentation of this file.
1# SPDX-License-Identifier: LGPL-3.0-or-later
2# SPDX-FileCopyrightText: Copyright CERN for the benefit of the SHiP Collaboration
3
4
5"""Mapping between detector modules and SciFi channels."""
6
7import argparse
8
9import matplotlib.patches as patches
10import matplotlib.pyplot as plt
11import numpy as np
12import ROOT
13import shipDet_conf
14from ShipGeoConfig import load_from_root_file
15
16
18 """Mapping between detector modules and SciFi channels."""
19
20 def __init__(self, modules) -> None:
21 """Initialize the SciFiMapping instance with geometry modules.
22
23 Parameters
24 ----------
25 modules : dict
26 Dictionary of detector geometry modules, must include key 'MTC'.
27
28 """
29 self.modules = modules
30 self.scifi = modules["MTC"]
31
32 def create_fibre_to_simp_map(self) -> None:
33 """Build mappings from optical fibres to SiPM channels for U and V planes.
34
35 Retrieves the U and V SiPM maps from the SciFi module and constructs
36 dictionaries mapping each fibre index to its associated SiPM channels,
37 including weight and x-position information.
38
39 For full details, see SND/MTC/MTCDetector.cxx in the SND framework.
40
41 Side Effects
42 ------------
43 Sets attributes 'fibre_to_simp_map_U' and 'fibre_to_simp_map_V'.
44 """
45 FU, FV = self.scifi.GetSiPMmapU(), self.scifi.GetSiPMmapV()
46 self.fibre_to_simp_map_U, self.fibre_to_simp_map_V = {}, {}
47 for x1, x2 in zip(FU, FV):
48 self.fibre_to_simp_map_U[x1.first] = {}
49 for z in x1.second:
50 self.fibre_to_simp_map_U[x1.first][z.first] = {
51 "weight": z.second[0],
52 "xpos": z.second[1],
53 }
54 self.fibre_to_simp_map_V[x2.first] = {}
55 for z in x2.second:
56 self.fibre_to_simp_map_V[x2.first][z.first] = {
57 "weight": z.second[0],
58 "xpos": z.second[1],
59 }
60
61 def create_sipm_to_fibre_map(self) -> None:
62 """Build mappings from SiPM channels to optical fibres for U and V planes.
63
64 Retrieves the U and V fibre maps from the SciFi module and constructs
65 dictionaries mapping each SiPM channel index to its associated fibres,
66 including weight and x-position information.
67
68 For full details, see SND/MTC/MTCDetector.cxx in the SND framework.
69
70 Side Effects
71 ------------
72 Sets attributes 'sipm_to_fibre_map_U' and 'sipm_to_fibre_map_V'.
73 """
74 XU, XV = self.scifi.GetFibresMapU(), self.scifi.GetFibresMapV()
75 self.sipm_to_fibre_map_U, self.sipm_to_fibre_map_V = {}, {}
76 for x1, x2 in zip(XU, XV):
77 self.sipm_to_fibre_map_U[x1.first] = {}
78 for z in x1.second:
79 self.sipm_to_fibre_map_U[x1.first][z.first] = {
80 "weight": z.second[0],
81 "xpos": z.second[1],
82 }
83 self.sipm_to_fibre_map_V[x2.first] = {}
84 for z in x2.second:
85 self.sipm_to_fibre_map_V[x2.first][z.first] = {
86 "weight": z.second[0],
87 "xpos": z.second[1],
88 }
89
90 def create_sipm_to_position_map(self) -> None:
91 """Create a mapping from SiPM channels to their positions in the SciFi detector.
92
93 This method retrieves the SiPM positions for both U and V planes and constructs
94 dictionaries mapping each SiPM channel index to its corresponding position.
95
96 Side Effects
97 ------------
98 Sets attributes 'sipm_pos_U' and 'sipm_pos_V'.
99 """
100 sipm_pos_U_raw = self.scifi.GetSiPMPos_U()
101 sipm_pos_V_raw = self.scifi.GetSiPMPos_V()
102 self.sipm_pos_U, self.sipm_pos_V = {}, {}
103
104 for pair in sipm_pos_U_raw:
105 self.sipm_pos_U[pair.first] = pair.second
106 for pair in sipm_pos_V_raw:
107 self.sipm_pos_V[pair.first] = pair.second
108
109 def make_mapping(self) -> None:
110 """Execute the full mapping sequence for the SciFi detector.
111
112 Calls internal methods to calculate SiPM overlap, perform the mapping
113 in the SciFi module, and build both fibre-to-SiPM and SiPM-to-fibre maps.
114
115 Currently is used in python/shipDigiReco.py.
116 """
117 self.scifi.SiPMOverlap()
118 self.scifi.SiPMmapping()
122
124 """Retrieve the SiPM-to-fibre mapping dictionaries.
125
126 Returns
127 -------
128 tuple of dict
129 (sipm_to_fibre_map_U, sipm_to_fibre_map_V)
130
131 """
132 return self.sipm_to_fibre_map_U, self.sipm_to_fibre_map_V
133
135 """Retrieve the fibre-to-SiPM mapping dictionaries.
136
137 Returns
138 -------
139 tuple of dict
140 (fibre_to_simp_map_U, fibre_to_simp_map_V)
141
142 """
143 return self.fibre_to_simp_map_U, self.fibre_to_simp_map_V
144
145 def draw_channel(self, sGeo, channel: int) -> None:
146 """Draw a single channel mapping showing fibre positions and the SiPM sensor.
147
148 Parameters
149 ----------
150 sGeo: ROOT.TGeoManager
151 The geometry manager for the detector setup.
152 channel : int
153 Global channel identifier encoding plane type, SiPM unit, and channel index.
154
155 Side Effects
156 ------------
157 Saves a PDF file named 'scifi_mapping_channel_1_{channel}.pdf' with the plot.
158
159 """
160 AF = ROOT.TVector3()
161 BF = ROOT.TVector3()
162 plane_type = int(channel / 1e5) % 10
163 locChannel = channel % 1000000
164 fibreVol = sGeo.FindVolumeFast("FiberVol")
165 R = fibreVol.GetShape().GetDX()
166 DX = 0.025
167 DZ = 0.135
168 xmin = 999.0
169 xmax = -999.0
170 ymin = 999.0
171 ymax = -999.0
172 fibre_positions = []
173 fibresSiPM = self.fibre_to_simp_map_U if plane_type == 0 else self.fibre_to_simp_map_V
174 for fibre in fibresSiPM[locChannel]:
175 globfiberID = (
176 fibre + 100000000 + 1000000 + 0 * 100000
177 if plane_type == 0
178 else fibre + 100000000 + 1000000 + 1 * 100000
179 )
180 self.scifi.GetPosition(globfiberID, AF, BF)
181 loc = self.scifi.GetLocalPos(globfiberID, BF)
182 print(f"Position for fibre {globfiberID} A: {BF.X()}, {BF.Y()}, {BF.Z()}")
183 fibre_positions.append((loc[0], loc[2]))
184 if xmin > loc[0]:
185 xmin = loc[0]
186 if ymin > loc[2]:
187 ymin = loc[2]
188 if xmax < loc[0]:
189 xmax = loc[0]
190 if ymax < loc[2]:
191 ymax = loc[2]
192 D = ymax - ymin + 3 * R
193 x0 = (xmax + xmin) / 2.0
194 _fig, ax = plt.subplots(figsize=(8, 8))
195 ax.set_xlim(x0 - D / 2, x0 + D / 2)
196 ax.set_ylim(ymin - 1.5 * R, ymax + 1.5 * R)
197 for i, (x, y) in enumerate(fibre_positions):
198 ellipse = patches.Ellipse((x, y), width=2 * R, height=2 * R, color="orange", alpha=0.6)
199 ax.add_patch(ellipse)
200 self.scifi.GetSiPMPosition(locChannel, AF, BF)
201 loc = self.scifi.GetLocalPos(globfiberID, BF)
202 print(f"SiPM position for channel {channel}: {loc[0]}, {loc[1]}, {loc[2]}")
203 rect = patches.Rectangle(
204 (loc[0] - DX, loc[2] - DZ),
205 2 * DX,
206 2 * DZ,
207 linewidth=1,
208 edgecolor="blue",
209 facecolor="blue",
210 alpha=0.3,
211 hatch="//",
212 )
213 ax.add_patch(rect)
214 ax.set_xlabel("X [cm]")
215 ax.set_ylabel("Z [cm]")
216 ax.set_title(f"SiPM Mapping for Channel {channel}")
217 ax.grid(True)
218 ax.set_aspect("equal")
219 plt.savefig(f"scifi_mapping_channel_1_{channel}.pdf")
220
222 self,
223 sGeo,
224 number_of_channels=20,
225 output_file="scifi_mapping_many_channels.pdf",
226 labeling=True,
227 figsize=(16, 16),
228 dpi=300,
229 cmap_name="tab20",
230 alpha_fibre=0.4,
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 # Prepare fig & ax
261 fig, ax = plt.subplots(figsize=figsize, dpi=dpi)
262
263 # Geometry constants
264 fibreVol = sGeo.FindVolumeFast("FiberVol")
265 R = fibreVol.GetShape().GetDX()
266 DX, DZ = 0.025, 0.135 / 2
267
268 # Select your channels. Hardcoded range for demonstration
269 # channelsU = sorted(self.fibre_to_simp_map_U.keys())[:10]
270 # channelsV = sorted(self.fibre_to_simp_map_V.keys())[::-1][86:96]
271
272 # Find SiPM channel in the center of the U and V planes
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 # 1) sort keys by their associated values
299 sorted_keys = sorted(d.keys(), key=lambda k: d[k])
300 # 2) locate the target
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 # 3) slice out the neighbors
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 # Loop through all channels
328 for chan in channels:
329 isU = chan in channelsU
330 # decode local channel
331 locChan = chan % 1_000_000
332
333 # collect fibre positions
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 # draw fibres (still orange)
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 # draw SiPM rect in its unique shade
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", # override with solid colors for clarity,
370 alpha=alpha_fibre * 0.8,
371 hatch="//" if isU else "\\\\",
372 )
373 ax.add_patch(rect)
374
375 if labeling:
376 # compute a fontsize that fits the rect width
377 # 1) data→display coords:
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 # 2) convert px→points (1pt = 1/72in; fig.dpi px/inch):
382 pts = disp_width * 72.0 / fig.dpi
383 fontsize = max(2, min(12, pts * 0.8))
384
385 # label above the rect
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 # finalize plot
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
415 self,
416 number_of_channels: int = 20,
417 real_event: bool = False,
418 x_coords=None,
419 output_file: str = "scifi_channel_ribbons_XY.pdf",
420 figsize: tuple[int, int] = (16, 16),
421 dpi: int = 300,
422 ) -> None:
423 """Plot SciFi channel ribbons and SiPM boxes in X–Y view.
424
425 Simplified X–Y view: one ribbon per channel (no per-fiber loops).
426 Ribbons span Y from -25 to +25 (length=50 cm), thickness equal
427 to channel height = 2*DZ, tilted +5° for U-plane channels, -5° for
428 V-plane. SiPM boxes sit on top at y=25→25+2*DZ.
429
430 Parameters
431 ----------
432 number_of_channels : int
433 Total ribbons (split around center) per plane.
434 real_event : bool
435 If True, highlight ribbons corresponding to given x_coords.
436 x_coords : list of float
437 If real_event is True, list of two x-coordinates [x_U, x_V] to highlight.
438 output_file : str
439 Path to save the PDF.
440 figsize : tuple of float
441 Figure size in inches.
442 dpi : int
443 Figure resolution in dots per inch.
444 Side Effects
445 ------------
446 Saves a PDF file with the specified filename containing the plot.
447
448 """
449 # 2) Geometry constants
450 DZ = 0.135 / 2 # SiPM half-height [cm]
451 DX = 0.025 # SiPM half-width [cm]
452 L = 50.0 # ribbon length (full Y-span) [cm]
453 angle_U = -5.0 # tilt for U
454 angle_V = +5.0 # tilt for V
455
456 if not real_event:
457 alpha_sipm, alpha_fibre = 0.75, 0.5
458 else:
459 alpha_sipm, alpha_fibre = 0.1, 0.1
460 # find the channels corresponding to x_coords
461 channels_x_U = next(c for c, x in self.sipm_pos_U.items() if abs(x - x_coords[0]) < DX)
462 channel_x_U = [c for c, x in self.sipm_pos_U.items() if abs(x - x_coords[0]) < DX][0]
463 channels_x_V = next(c for c, x in self.sipm_pos_V.items() if abs(x - x_coords[1]) < DX)
464 channel_x_V = [c for c, x in self.sipm_pos_V.items() if abs(x - x_coords[1]) < DX][0]
465 # 1) Figure setup
466 fig, ax = plt.subplots(figsize=figsize, dpi=dpi)
467
468 # get center channels + neighbors
469 centerU = next(c for c, x in self.sipm_pos_U.items() if abs(x) < DX)
470 centerV = next(c for c, x in self.sipm_pos_V.items() if abs(x) < DX)
471
472 def get_surrounding(d, target, N):
473 keys = sorted(d.keys(), key=lambda k: d[k])
474 i = keys.index(target)
475 half = N // 2
476 start = max(0, i - half)
477 end = min(len(keys), i + half + 1)
478 return keys[start:end]
479
480 if not real_event:
481 chansU = get_surrounding(self.sipm_pos_U, centerU, number_of_channels)
482 chansV = get_surrounding(self.sipm_pos_V, centerV, number_of_channels)
483 else:
484 chansU = get_surrounding(self.sipm_pos_U, channels_x_U, number_of_channels)
485 chansV = get_surrounding(self.sipm_pos_V, channels_x_V, number_of_channels)
486 all_chans = chansU + chansV
487
488 # cmap_U = plt.get_cmap('tab20')
489 # cmap_V = plt.get_cmap('tab20b')
490 # colorsU = [cmap_U(i/(len(chansU)-1)) for i in range(len(chansU))]
491 # colorsV = [cmap_V(i/(len(chansV)-1)) for i in range(len(chansV))]
492 # 3) Draw ribbons and SiPM boxes per channel
493 for chan in all_chans:
494 isU = chan in chansU
495 x0 = self.sipm_pos_U[chan] if isU else self.sipm_pos_V[chan]
496 angle = angle_U if isU else angle_V
497
498 alpha = np.deg2rad(angle) # e.g. angle_deg = +5 or -5
499 dx = DX
500 dy = L / 2
501 shift = dy * np.tan(alpha) # horizontal shear of bottom edge
502 # define the four corners in XY
503 coords = [
504 (x0 - dx, +dy), # top-left
505 (x0 + dx, +dy), # top-right
506 (x0 + dx - shift, -dy), # bottom-right
507 (x0 - dx - shift, -dy), # bottom-left
508 ]
509
510 # col = colorsU[k_U] if isU else colorsV[k_V]
511 col = "blue" if isU else "red" # override with solid colors for clarity
512 if not real_event:
513 ribbon = patches.Polygon(
514 coords,
515 closed=True,
516 facecolor=col, # fill in your color
517 edgecolor="black",
518 alpha=alpha_fibre, # semi-transparent
519 )
520 ax.add_patch(ribbon)
521
522 y_top = L / 2 if isU else L / 2 + DZ
523
524 box = patches.Rectangle(
525 (x0 - DX, y_top),
526 2 * DX,
527 DZ,
528 facecolor=col, # same fill color
529 edgecolor="black",
530 alpha=alpha_sipm, # a bit more opaque
531 hatch="//" if isU else "\\\\",
532 )
533 ax.add_patch(box)
534 else:
535 if channel_x_U == chan or channel_x_V == chan:
536 # draw the ribbon
537 ribbon = patches.Polygon(
538 coords,
539 closed=True,
540 facecolor=col, # fill in your color
541 edgecolor="black",
542 alpha=0.50, # semi-transparent
543 )
544 ax.add_patch(ribbon)
545
546 y_top = L / 2 if isU else L / 2 + DZ
547
548 box = patches.Rectangle(
549 (x0 - DX, y_top),
550 2 * DX,
551 DZ,
552 facecolor=col, # same fill color
553 edgecolor="black",
554 alpha=0.75, # a bit more opaque
555 hatch="//" if isU else "\\\\",
556 )
557 ax.add_patch(box)
558 else:
559 ribbon = patches.Polygon(
560 coords,
561 closed=True,
562 facecolor=col, # fill in your color
563 edgecolor="black",
564 alpha=alpha_fibre, # semi-transparent
565 )
566 ax.add_patch(ribbon)
567
568 y_top = L / 2 if isU else L / 2 + DZ
569
570 box = patches.Rectangle(
571 (x0 - DX, y_top),
572 2 * DX,
573 DZ,
574 facecolor=col, # same fill color
575 edgecolor="black",
576 alpha=alpha_sipm, # a bit more opaque
577 hatch="//" if isU else "\\\\",
578 )
579 ax.add_patch(box)
580
581 # 4) Finalize plot
582 ax.set_aspect("equal")
583 if not real_event:
584 shift = 3 * np.tan(alpha)
585 min_x = min([self.sipm_pos_U[chan] for chan in chansU]) - shift
586 max_x = max(self.sipm_pos_V[chan] for chan in chansV) + shift
587 ax.set_xlim(min_x, max_x)
588 ax.set_ylim(22, 26)
589 else:
590 min_x, max_x = -25, 25
591 ax.set_ylim(-25, 26)
592 ax.set_xlim(min_x, max_x)
593 # ax.set_ylim(22, 26)
594 ax.set_xlabel("X [cm]")
595 ax.set_ylabel("Y [cm]")
596 ax.set_title("SciFi channel ribbons & SiPM boxes (X–Y view)")
597 ax.grid(True)
598 # create legend handles
599 legend_handles = [
600 patches.Patch(
601 facecolor="blue",
602 edgecolor="black",
603 hatch="//",
604 label="U plane",
605 alpha=0.75,
606 ),
607 patches.Patch(
608 facecolor="red",
609 edgecolor="black",
610 hatch="\\\\",
611 label="V plane",
612 alpha=0.75,
613 ),
614 ]
615
616 ax.legend(handles=legend_handles, title="Plane", loc="upper right")
617 plt.tight_layout()
618 plt.savefig(output_file)
619 plt.close(fig)
620 print(f"Saved channel ribbons + SiPM boxes to {output_file}")
621
623 self,
624 sGeo,
625 number_of_channels=20,
626 output_file="scifi_combined_views.pdf",
627 figsize=(18, 8),
628 dpi=300,
629 alpha_fibre=0.4,
630 ) -> None:
631 """Draw combined Z–X and X–Y views of SciFi channel mappings side by side.
632
633 Parameters
634 ----------
635 sGeo : ROOT.TGeoManager
636 The geometry manager for the detector setup.
637 number_of_channels : int, optional
638 Total number of channels to display (split evenly between U and V planes).
639 Default is 20.
640 output_file : str, optional
641 Filename for the saved PDF plot. Default: 'scifi_combined_views.pdf'.
642 figsize : tuple of float, optional
643 Figure size in inches. Default is (18, 8).
644 dpi : int, optional
645 Resolution of the figure in dots per inch. Default is 300.
646 alpha_fibre : float, optional
647 Transparency for fibre ellipses. Default is 0.4.
648 Side Effects
649 ------------
650 Saves a PDF file with the specified filename containing the combined views.
651
652 """
653 # Prepare fig & axes
654
655 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize, dpi=dpi)
656
657 fibreVol = sGeo.FindVolumeFast("FiberVol")
658 R = fibreVol.GetShape().GetDX()
659 DX, DZ = 0.025, 0.135 / 2
660
661 center_U = [chan for chan, x in self.sipm_pos_U.items() if abs(x) <= DX - 0.001][0]
662 center_V = [chan for chan, x in self.sipm_pos_V.items() if abs(x) <= DX - 0.001][0]
663
664 def get_keys(d, target, N):
665 ks = sorted(d, key=lambda k: d[k])
666 i = ks.index(target)
667 half = N // 2
668 start = max(0, i - half)
669 end = min(len(ks), i + half + 1)
670 return ks[start:end]
671
672 chansU = get_keys(self.sipm_pos_U, center_U, number_of_channels)
673 chansV = get_keys(self.sipm_pos_V, center_V, number_of_channels)
674 channels = chansU + chansV
675 n_chan = len(channels)
676 cmap = plt.get_cmap("tab20")
677 colors = [cmap(i / (n_chan - 1)) for i in range(n_chan)]
678
679 AF = ROOT.TVector3()
680 BF = ROOT.TVector3()
681 for idx, chan in enumerate(channels):
682 col = colors[idx]
683 locChan = chan % 1000000
684 fmap = self.fibre_to_simp_map_U if chan in chansU else self.fibre_to_simp_map_V
685 xs, zs = [], []
686 for fid in fmap[locChan]:
687 gid = fid + int(1e8 + 1e6 + (0 if chan in chansU else 1) * 1e5)
688 self.scifi.GetPosition(gid, AF, BF)
689 loc = self.scifi.GetLocalPos(fid, BF)
690 xs.append(loc[0])
691 zs.append(loc[2])
692 for x, z in zip(xs, zs):
693 ell = patches.Ellipse((x, z), 2 * R, 2 * R, color="orange", alpha=alpha_fibre)
694 ax1.add_patch(ell)
695 self.scifi.GetSiPMPosition(chan, BF, AF)
696 loc = self.scifi.GetLocalPos(fid, BF)
697 rx, rz = loc[0], loc[2]
698 rect = patches.Rectangle(
699 (rx - DX, rz - DZ),
700 2 * DX,
701 2 * DZ,
702 linewidth=1,
703 edgecolor="black",
704 facecolor=col,
705 alpha=alpha_fibre * 0.8,
706 )
707 ax1.add_patch(rect)
708
709 ax1.set_xlabel("X [cm]")
710 ax1.set_ylabel("Z [cm]")
711 ax1.set_aspect("equal")
712 ax1.grid(True)
713 ax1.set_title("Z–X: SciFi mappings")
714
715 DZ = 0.135 / 2
716 DX = 0.025
717 L = 50.0
718 angleU = 5.0
719 angleV = -5.0
720 centerU = [c for c, x in self.sipm_pos_U.items() if abs(x) < DX][0]
721 centerV = [c for c, x in self.sipm_pos_V.items() if abs(x) < DX][0]
722 chansU = get_keys(self.sipm_pos_U, centerU, number_of_channels)
723 chansV = get_keys(self.sipm_pos_V, centerV, number_of_channels)
724 all_ch = chansU + chansV
725 colorsU = [plt.get_cmap("Blues")(i / max(len(chansU) - 1, 1)) for i in range(len(chansU))]
726 colorsV = [plt.get_cmap("Reds")(i / max(len(chansV) - 1, 1)) for i in range(len(chansV))]
727
728 for chan in all_ch:
729 isU = chan in chansU
730 x0 = self.sipm_pos_U[chan] if isU else self.sipm_pos_V[chan]
731 angle = angleU if isU else angleV
732 alpha_rad = np.deg2rad(angle)
733 dy = L / 2
734 dx = DX
735 shift = dy * np.tan(alpha_rad)
736 coords = [
737 (x0 - dx, dy),
738 (x0 + dx, dy),
739 (x0 + dx - shift, -dy),
740 (x0 - dx - shift, -dy),
741 ]
742 col = colorsU[chansU.index(chan)] if isU else colorsV[chansV.index(chan)]
743 poly = patches.Polygon(coords, closed=True, facecolor=col, edgecolor="black", alpha=0.5)
744 ax2.add_patch(poly)
745 y_top = dy
746 box = patches.Rectangle(
747 (x0 - DX, y_top),
748 2 * DX,
749 2 * DZ,
750 facecolor=col,
751 edgecolor="black",
752 alpha=0.75,
753 )
754 ax2.add_patch(box)
755
756 ax2.set_xlabel("X [cm]")
757 ax2.set_ylabel("Y [cm]")
758 ax2.set_aspect("equal")
759 ax2.grid(True)
760 ax2.set_title("X–Y: Channel ribbons")
761
762 legend_handles = [
763 patches.Patch(
764 facecolor="blue",
765 edgecolor="black",
766 hatch="//",
767 label="U plane",
768 alpha=0.75,
769 ),
770 patches.Patch(
771 facecolor="red",
772 edgecolor="black",
773 hatch="\\",
774 label="V plane",
775 alpha=0.75,
776 ),
777 ]
778 ax2.legend(handles=legend_handles, title="Plane", loc="upper right")
779
780 plt.tight_layout()
781 plt.savefig(output_file)
782 plt.close(fig)
783
784 def mapping_validation(self) -> None:
785 """Validate and print the SiPM-to-fibre and fibre-to-SiPM mappings.
786
787 Prints out the mappings for the U plane, showing fibre indices,
788 SiPM channels, weights, and x-positions for both mapping directions.
789
790 """
791 sipm_to_fiber_map_U, _ = self.get_sipm_to_fibre_map()
792 fiber_to_sipm_map_U, _ = self.get_fibre_to_simp_map()
793
794 print("Validating U plane mapping:")
795 for fiber_id, fibers in sipm_to_fiber_map_U.items():
796 for sipm_chan, chan_info in fibers.items():
797 weight = chan_info["weight"]
798 xpos = chan_info["xpos"]
799 print(
800 f"""---- Fiber index: {fiber_id}, SiPM Channel: {sipm_chan},
801 Weight: {weight}, X Position: {xpos}"""
802 )
803 for sipm_chan, sipm_fibers in fiber_to_sipm_map_U.items():
804 for fiber_id, fiber_info in sipm_fibers.items():
805 weight = fiber_info["weight"]
806 xpos = fiber_info["xpos"]
807 print(
808 f"""++++ SiPM Channel: {sipm_chan}, Fiber index: {fiber_id},
809 Weight: {weight}, X Position: {xpos}"""
810 )
811
812
813if __name__ == "__main__":
814 parser = argparse.ArgumentParser(description="SciFiMapping visualization tool")
815 parser.add_argument(
816 "-g",
817 dest="geoFile",
818 type=str,
819 default="geofile_full.conical.PG_13-TGeant4.root",
820 help="Path to the geometry ROOT file",
821 )
822 parser.add_argument(
823 "--mode",
824 dest="mode",
825 type=str,
826 default="draw_many_channels",
827 help="Operation mode",
828 choices=[
829 "draw_channel",
830 "draw_many_channels",
831 "draw_channel_XY",
832 "draw_combined_scifi_views",
833 "validation",
834 ],
835 )
836 args = parser.parse_args()
837 geoFile = args.geoFile
838 fgeo = ROOT.TFile.Open(geoFile)
839 ship_geo = load_from_root_file(fgeo, "ShipGeo")
840
841 # -----Create geometry----------------------------------------------
842
843 run = ROOT.FairRunSim()
844 run.SetName("TGeant4") # Transport engine
845 run.SetSink(ROOT.FairRootFileSink(ROOT.TMemFile("output", "recreate"))) # Output file
846 run.SetUserConfig("g4Config_basic.C") # geant4 transport not used
847 rtdb = run.GetRuntimeDb()
848 modules = shipDet_conf.configure(run, ship_geo)
849 run.Init()
850 print("configured geofile")
851 sGeo = fgeo.FAIRGeom
852 top = sGeo.GetTopVolume()
853 # -----Create SciFiMapping instance--------------------------------
854 mapping = SciFiMapping(modules)
855 mapping.make_mapping()
856
857 if args.mode == "draw_channel":
858 test_channel = 1000000 # Example channel
859 mapping.draw_channel(sGeo, test_channel)
860 elif args.mode == "draw_many_channels":
861 mapping.draw_many_channels(sGeo, number_of_channels=20)
862 elif args.mode == "draw_channel_XY":
863 mapping.draw_channel_XY(number_of_channels=20, real_event=False)
864 elif args.mode == "draw_combined_scifi_views":
865 mapping.draw_combined_scifi_views(sGeo, number_of_channels=20)
866 elif args.mode == "validation":
867 mapping.mapping_validation()
868 else:
869 print(f"Unknown mode: {args.mode}")
None create_sipm_to_fibre_map(self)
Definition: SciFiMapping.py:61
None draw_many_channels(self, sGeo, number_of_channels=20, output_file="scifi_mapping_many_channels.pdf", labeling=True, figsize=(16, 16), dpi=300, cmap_name="tab20", alpha_fibre=0.4)
None create_sipm_to_position_map(self)
Definition: SciFiMapping.py:90
None draw_combined_scifi_views(self, sGeo, number_of_channels=20, output_file="scifi_combined_views.pdf", figsize=(18, 8), dpi=300, alpha_fibre=0.4)
None draw_channel(self, sGeo, int channel)
None draw_channel_XY(self, int number_of_channels=20, bool real_event=False, x_coords=None, str output_file="scifi_channel_ribbons_XY.pdf", tuple[int, int] figsize=(16, 16), int dpi=300)
None __init__(self, modules)
Definition: SciFiMapping.py:20
None create_fibre_to_simp_map(self)
Definition: SciFiMapping.py:32
def configure(run, ship_geo)