Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-06-25 07:48:53

0001 #!/usr/bin/env python3
0002 """Generate a geometry ID mapping between two ACTS tracking geometries.
0003 
0004 For each sensitive surface in geometry A, the script finds the matching
0005 surface in geometry B by comparing surface centre positions.  The match
0006 is deterministic and does not require any data files.
0007 
0008 Output: a CSV file with 10 columns (5 per geometry, column names use the
0009 user-supplied prefixes):
0010 
0011     {a}_packed   {a}_volume   {a}_layer   {a}_sensitive   {a}_extra
0012     {b}_packed   {b}_volume   {b}_layer   {b}_sensitive   {b}_extra
0013 
0014 where ``_packed`` is the raw uint64 ``GeometryIdentifier.value``.
0015 
0016 Usage
0017 -----
0018 ./run_in_env.sh python3 Examples/Scripts/Python/generate_geoid_map.py \\
0019     --output geoid_map.csv --prefix-a gen1 --prefix-b gen3
0020 """
0021 
0022 import argparse
0023 import csv
0024 from pathlib import Path
0025 
0026 import numpy as np
0027 
0028 import acts
0029 from acts.examples.odd import getOpenDataDetector
0030 
0031 
0032 def _collect_surfaces(tracking_geometry, gctx):
0033     """Return {raw_geoId: (GeometryIdentifier, centre_array)} for sensitive surfaces."""
0034     surfaces = {}
0035 
0036     def visitor(surface):
0037         gid = surface.geometryId
0038         if gid.sensitive == 0:
0039             return
0040         centre = np.array(surface.center(gctx))
0041         surfaces[gid.value] = (gid, centre)
0042 
0043     tracking_geometry.visitSurfaces(visitor)
0044     return surfaces
0045 
0046 
0047 def generate_geoid_map(
0048     geo_a, geo_b, output_path, prefix_a="a", prefix_b="b", tolerance=0.1
0049 ):
0050     """Match sensitive surfaces between two geometries by centre position.
0051 
0052     Parameters
0053     ----------
0054     geo_a, geo_b : acts.TrackingGeometry
0055         Source and target tracking geometries.
0056     output_path : Path
0057         Output CSV file path.
0058     prefix_a, prefix_b : str
0059         Column name prefixes for the two geometries.
0060     tolerance : float
0061         Maximum allowed Euclidean distance (mm) between matched centres.
0062 
0063     Raises
0064     ------
0065     RuntimeError
0066         If any surface in geo_a has no unique match in geo_b.
0067     """
0068     gctx = acts.GeometryContext.dangerouslyDefaultConstruct()
0069 
0070     surfs_a = _collect_surfaces(geo_a, gctx)
0071     surfs_b = _collect_surfaces(geo_b, gctx)
0072 
0073     centres_b = np.array([c for _, c in surfs_b.values()])
0074     gids_b = [gid for gid, _ in surfs_b.values()]
0075 
0076     rows = []
0077     unmatched = []
0078 
0079     for raw_a, (gid_a, centre_a) in sorted(surfs_a.items()):
0080         dists = np.linalg.norm(centres_b - centre_a, axis=1)
0081         best_idx = np.argmin(dists)
0082         best_dist = dists[best_idx]
0083 
0084         if best_dist > tolerance:
0085             unmatched.append((gid_a, best_dist))
0086             continue
0087 
0088         n_close = np.sum(dists <= tolerance)
0089         if n_close > 1:
0090             raise RuntimeError(
0091                 f"Ambiguous match for surface {gid_a.value} "
0092                 f"(volume={gid_a.volume}, layer={gid_a.layer}, "
0093                 f"sensitive={gid_a.sensitive}): "
0094                 f"{n_close} surfaces within tolerance {tolerance} mm"
0095             )
0096 
0097         gid_b = gids_b[best_idx]
0098         rows.append((gid_a, gid_b))
0099 
0100     if unmatched:
0101         msg_parts = [
0102             f"  geoId={gid.value} (vol={gid.volume}, lay={gid.layer}, "
0103             f"sen={gid.sensitive}) — nearest={dist:.2f} mm"
0104             for gid, dist in unmatched[:10]
0105         ]
0106         raise RuntimeError(
0107             f"{len(unmatched)} surfaces in geometry A have no match in "
0108             f"geometry B (tolerance={tolerance} mm). First entries:\n"
0109             + "\n".join(msg_parts)
0110         )
0111 
0112     output_path = Path(output_path)
0113     output_path.parent.mkdir(parents=True, exist_ok=True)
0114 
0115     fieldnames = [
0116         f"{prefix_a}_packed",
0117         f"{prefix_a}_volume",
0118         f"{prefix_a}_layer",
0119         f"{prefix_a}_sensitive",
0120         f"{prefix_a}_extra",
0121         f"{prefix_b}_packed",
0122         f"{prefix_b}_volume",
0123         f"{prefix_b}_layer",
0124         f"{prefix_b}_sensitive",
0125         f"{prefix_b}_extra",
0126     ]
0127 
0128     with open(output_path, "w", newline="") as f:
0129         writer = csv.DictWriter(f, fieldnames=fieldnames)
0130         writer.writeheader()
0131         for gid_a, gid_b in rows:
0132             writer.writerow(
0133                 {
0134                     f"{prefix_a}_packed": gid_a.value,
0135                     f"{prefix_a}_volume": gid_a.volume,
0136                     f"{prefix_a}_layer": gid_a.layer,
0137                     f"{prefix_a}_sensitive": gid_a.sensitive,
0138                     f"{prefix_a}_extra": gid_a.extra,
0139                     f"{prefix_b}_packed": gid_b.value,
0140                     f"{prefix_b}_volume": gid_b.volume,
0141                     f"{prefix_b}_layer": gid_b.layer,
0142                     f"{prefix_b}_sensitive": gid_b.sensitive,
0143                     f"{prefix_b}_extra": gid_b.extra,
0144                 }
0145             )
0146 
0147     print(f"Written {len(rows)} surface mappings to {output_path}")
0148 
0149 
0150 def main():
0151     parser = argparse.ArgumentParser(
0152         description=__doc__,
0153         formatter_class=argparse.RawDescriptionHelpFormatter,
0154     )
0155     parser.add_argument(
0156         "--output",
0157         "-o",
0158         type=Path,
0159         default=Path("geoid_map.csv"),
0160         help="Output CSV path (default: geoid_map.csv)",
0161     )
0162     parser.add_argument(
0163         "--prefix-a",
0164         default="gen1",
0165         help="Column name prefix for geometry A (default: gen1)",
0166     )
0167     parser.add_argument(
0168         "--prefix-b",
0169         default="gen3",
0170         help="Column name prefix for geometry B (default: gen3)",
0171     )
0172     parser.add_argument(
0173         "--tolerance",
0174         type=float,
0175         default=0.1,
0176         help="Match tolerance in mm (default: 0.1)",
0177     )
0178     args = parser.parse_args()
0179 
0180     print("Building ODD Gen1 geometry ...")
0181     detector_a = getOpenDataDetector()
0182     geo_a = detector_a.trackingGeometry()
0183 
0184     print("Building ODD Gen3 geometry ...")
0185     detector_b = getOpenDataDetector(gen3=True)
0186     geo_b = detector_b.trackingGeometry()
0187 
0188     generate_geoid_map(
0189         geo_a,
0190         geo_b,
0191         output_path=args.output,
0192         prefix_a=args.prefix_a,
0193         prefix_b=args.prefix_b,
0194         tolerance=args.tolerance,
0195     )
0196 
0197 
0198 if __name__ == "__main__":
0199     main()