5"""Mapping between detector modules and SciFi channels."""
9import matplotlib.patches
as patches
10import matplotlib.pyplot
as plt
14from ShipGeoConfig
import load_from_root_file
18 """Mapping between detector modules and SciFi channels."""
21 """Initialize the SciFiMapping instance with geometry modules.
26 Dictionary of detector geometry modules, must include key 'MTC'.
33 """Build mappings from optical fibres to SiPM channels for U and V planes.
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.
39 For full details, see SND/MTC/MTCDetector.cxx
in the SND framework.
43 Sets attributes
'fibre_to_simp_map_U' and 'fibre_to_simp_map_V'.
45 FU, FV = self.scifi.GetSiPMmapU(), self.scifi.GetSiPMmapV()
47 for x1, x2
in zip(FU, FV):
48 self.fibre_to_simp_map_U[x1.first] = {}
50 self.fibre_to_simp_map_U[x1.first][z.first] = {
51 "weight": z.second[0],
57 "weight": z.second[0],
62 """Build mappings from SiPM channels to optical fibres for U and V planes.
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.
68 For full details, see SND/MTC/MTCDetector.cxx
in the SND framework.
72 Sets attributes
'sipm_to_fibre_map_U' and 'sipm_to_fibre_map_V'.
74 XU, XV = self.scifi.GetFibresMapU(), self.scifi.GetFibresMapV()
76 for x1, x2
in zip(XU, XV):
77 self.sipm_to_fibre_map_U[x1.first] = {}
79 self.sipm_to_fibre_map_U[x1.first][z.first] = {
80 "weight": z.second[0],
86 "weight": z.second[0],
91 """Create a mapping from SiPM channels to their positions in the SciFi detector.
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.
98 Sets attributes
'sipm_pos_U' and 'sipm_pos_V'.
100 sipm_pos_U_raw = self.scifi.GetSiPMPos_U()
101 sipm_pos_V_raw = self.scifi.GetSiPMPos_V()
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:
110 """Execute the full mapping sequence for the SciFi detector.
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.
115 Currently
is used
in python/shipDigiReco.py.
117 self.scifi.SiPMOverlap()
118 self.scifi.SiPMmapping()
124 """Retrieve the SiPM-to-fibre mapping dictionaries.
129 (sipm_to_fibre_map_U, sipm_to_fibre_map_V)
135 """Retrieve the fibre-to-SiPM mapping dictionaries.
140 (fibre_to_simp_map_U, fibre_to_simp_map_V)
146 """Draw a single channel mapping showing fibre positions and the SiPM sensor.
150 sGeo: ROOT.TGeoManager
151 The geometry manager for the detector setup.
153 Global channel identifier encoding plane type, SiPM unit,
and channel index.
157 Saves a PDF file named
'scifi_mapping_channel_1_{channel}.pdf' with the plot.
162 plane_type = int(channel / 1e5) % 10
163 locChannel = channel % 1000000
164 fibreVol = sGeo.FindVolumeFast("FiberVol")
165 R = fibreVol.GetShape().GetDX()
174 for fibre
in fibresSiPM[locChannel]:
176 fibre + 100000000 + 1000000 + 0 * 100000
178 else fibre + 100000000 + 1000000 + 1 * 100000
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]))
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),
214 ax.set_xlabel(
"X [cm]")
215 ax.set_ylabel(
"Z [cm]")
216 ax.set_title(f
"SiPM Mapping for Channel {channel}")
218 ax.set_aspect(
"equal")
219 plt.savefig(f
"scifi_mapping_channel_1_{channel}.pdf")
224 number_of_channels=20,
225 output_file="scifi_mapping_many_channels.pdf",
232 """Draw overlay plot of multiple channel mappings on a single figure.
236 number_of_channels : int, optional
237 Total number of channels to display (split evenly between U and V planes).
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.
246 figsize : tuple of float, optional
247 Figure size
in inches. Default
is (16, 16).
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.
257 Saves an overlay PDF plot
with the specified filename.
261 fig, ax = plt.subplots(figsize=figsize, dpi=dpi)
264 fibreVol = sGeo.FindVolumeFast(
"FiberVol")
265 R = fibreVol.GetShape().GetDX()
266 DX, DZ = 0.025, 0.135 / 2
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]
276 def get_surrounding_keys(d: dict, target_key, N: int):
277 """Return neighbors of `target_key` in value-sorted order.
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).
286 Mapping
from keys to comparable values.
287 target_key : hashable
288 The key around which to collect neighbors.
290 Total number of neighbors to
return (N//2 before, N//2 after).
295 List of up to N keys: first the keys before, then the keys after.
299 sorted_keys = sorted(d.keys(), key=
lambda k: d[k])
302 idx = sorted_keys.index(target_key)
304 raise KeyError(f
"Target key {target_key!r} not found in dictionary")
from None
308 start = max(0, idx - half)
310 start_after = idx + 1
311 end_after = idx + 1 + half
313 before = sorted_keys[start:end_before]
314 after = sorted_keys[start_after:end_after]
316 return before + [target_key] + after
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)
321 channels = channelsU + channelsV
322 n_chan = len(channels)
328 for chan
in channels:
329 isU = chan
in channelsU
331 locChan = chan % 1_000_000
335 for fibreID
in (self.fibre_to_simp_map_U
if chan
in channelsU
else self.
fibre_to_simp_map_V)[locChan]:
337 fibreID + 100000000 + 1000000 + 0 * 100000
339 else fibreID + 100000000 + 1000000 + 1 * 100000
341 self.
scifi.GetPosition(globfiberID, AF, BF)
342 loc = self.
scifi.GetLocalPos(fibreID, BF)
347 for x, y
in zip(xs, ys):
348 ell = patches.Ellipse(
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(
369 facecolor=
"blue" if isU
else "red",
370 alpha=alpha_fibre * 0.8,
371 hatch=
"//" if isU
else "\\\\",
378 p0 = ax.transData.transform((rx - DX, ry))
379 p1 = ax.transData.transform((rx + DX, ry))
380 disp_width = abs(p1[0] - p0[0])
382 pts = disp_width * 72.0 / fig.dpi
383 fontsize = max(2, min(12, pts * 0.8))
386 text = f
"SiPM: {(chan // 1000) % 10} \n ch: {chan % 1000}"
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)
410 plt.savefig(output_file)
412 print(f
"Saved overlay plot of {n_chan} channels to {output_file}")
416 number_of_channels: int = 20,
417 real_event: bool =
False,
419 output_file: str =
"scifi_channel_ribbons_XY.pdf",
420 figsize: tuple[int, int] = (16, 16),
423 """Plot SciFi channel ribbons and SiPM boxes in X–Y view.
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.
432 number_of_channels : int
433 Total ribbons (split around center) per plane.
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.
439 Path to save the PDF.
440 figsize : tuple of float
441 Figure size
in inches.
443 Figure resolution
in dots per inch.
446 Saves a PDF file
with the specified filename containing the plot.
457 alpha_sipm, alpha_fibre = 0.75, 0.5
459 alpha_sipm, alpha_fibre = 0.1, 0.1
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]
466 fig, ax = plt.subplots(figsize=figsize, dpi=dpi)
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)
472 def get_surrounding(d, target, N):
473 keys = sorted(d.keys(), key=
lambda k: d[k])
474 i = keys.index(target)
476 start = max(0, i - half)
477 end = min(len(keys), i + half + 1)
478 return keys[start:end]
481 chansU = get_surrounding(self.sipm_pos_U, centerU, number_of_channels)
482 chansV = get_surrounding(self.
sipm_pos_V, centerV, number_of_channels)
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
493 for chan
in all_chans:
495 x0 = self.sipm_pos_U[chan]
if isU
else self.
sipm_pos_V[chan]
496 angle = angle_U
if isU
else angle_V
498 alpha = np.deg2rad(angle)
501 shift = dy * np.tan(alpha)
506 (x0 + dx - shift, -dy),
507 (x0 - dx - shift, -dy),
511 col =
"blue" if isU
else "red"
513 ribbon = patches.Polygon(
522 y_top = L / 2
if isU
else L / 2 + DZ
524 box = patches.Rectangle(
531 hatch=
"//" if isU
else "\\\\",
535 if channel_x_U == chan
or channel_x_V == chan:
537 ribbon = patches.Polygon(
546 y_top = L / 2
if isU
else L / 2 + DZ
548 box = patches.Rectangle(
555 hatch=
"//" if isU
else "\\\\",
559 ribbon = patches.Polygon(
568 y_top = L / 2
if isU
else L / 2 + DZ
570 box = patches.Rectangle(
577 hatch=
"//" if isU
else "\\\\",
582 ax.set_aspect(
"equal")
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)
590 min_x, max_x = -25, 25
592 ax.set_xlim(min_x, max_x)
594 ax.set_xlabel(
"X [cm]")
595 ax.set_ylabel(
"Y [cm]")
596 ax.set_title(
"SciFi channel ribbons & SiPM boxes (X–Y view)")
616 ax.legend(handles=legend_handles, title=
"Plane", loc=
"upper right")
618 plt.savefig(output_file)
620 print(f
"Saved channel ribbons + SiPM boxes to {output_file}")
625 number_of_channels=20,
626 output_file="scifi_combined_views.pdf",
631 """Draw combined Z–X and X–Y views of SciFi channel mappings side by side.
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).
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).
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.
650 Saves a PDF file
with the specified filename containing the combined views.
655 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize, dpi=dpi)
657 fibreVol = sGeo.FindVolumeFast(
"FiberVol")
658 R = fibreVol.GetShape().GetDX()
659 DX, DZ = 0.025, 0.135 / 2
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]
664 def get_keys(d, target, N):
665 ks = sorted(d, key=
lambda k: d[k])
668 start = max(0, i - half)
669 end = min(len(ks), i + half + 1)
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)]
681 for idx, chan
in enumerate(channels):
683 locChan = chan % 1000000
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)
692 for x, z
in zip(xs, zs):
693 ell = patches.Ellipse((x, z), 2 * R, 2 * R, color=
"orange", alpha=alpha_fibre)
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(
705 alpha=alpha_fibre * 0.8,
709 ax1.set_xlabel(
"X [cm]")
710 ax1.set_ylabel(
"Z [cm]")
711 ax1.set_aspect(
"equal")
713 ax1.set_title(
"Z–X: SciFi mappings")
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))]
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)
735 shift = dy * np.tan(alpha_rad)
739 (x0 + dx - shift, -dy),
740 (x0 - dx - shift, -dy),
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)
746 box = patches.Rectangle(
756 ax2.set_xlabel(
"X [cm]")
757 ax2.set_ylabel(
"Y [cm]")
758 ax2.set_aspect(
"equal")
760 ax2.set_title(
"X–Y: Channel ribbons")
778 ax2.legend(handles=legend_handles, title=
"Plane", loc=
"upper right")
781 plt.savefig(output_file)
785 """Validate and print the SiPM-to-fibre and fibre-to-SiPM mappings.
787 Prints out the mappings for the U plane, showing fibre indices,
788 SiPM channels, weights,
and x-positions
for both mapping directions.
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"]
800 f
"""---- Fiber index: {fiber_id}, SiPM Channel: {sipm_chan},
801 Weight: {weight}, X Position: {xpos}"""
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"]
808 f
"""++++ SiPM Channel: {sipm_chan}, Fiber index: {fiber_id},
809 Weight: {weight}, X Position: {xpos}"""
813if __name__ ==
"__main__":
814 parser = argparse.ArgumentParser(description=
"SciFiMapping visualization tool")
819 default=
"geofile_full.conical.PG_13-TGeant4.root",
820 help=
"Path to the geometry ROOT file",
826 default=
"draw_many_channels",
827 help=
"Operation mode",
830 "draw_many_channels",
832 "draw_combined_scifi_views",
836 args = parser.parse_args()
837 geoFile = args.geoFile
838 fgeo = ROOT.TFile.Open(geoFile)
839 ship_geo = load_from_root_file(fgeo,
"ShipGeo")
843 run = ROOT.FairRunSim()
844 run.SetName(
"TGeant4")
845 run.SetSink(ROOT.FairRootFileSink(ROOT.TMemFile(
"output",
"recreate")))
846 run.SetUserConfig(
"g4Config_basic.C")
847 rtdb = run.GetRuntimeDb()
850 print(
"configured geofile")
852 top = sGeo.GetTopVolume()
855 mapping.make_mapping()
857 if args.mode ==
"draw_channel":
858 test_channel = 1000000
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()
869 print(f
"Unknown mode: {args.mode}")
None create_sipm_to_fibre_map(self)
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)
def get_fibre_to_simp_map(self)
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)
def get_sipm_to_fibre_map(self)
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 mapping_validation(self)
None __init__(self, modules)
None create_fibre_to_simp_map(self)
def configure(run, ship_geo)