File indexing completed on 2026-06-25 07:48:53
0001
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()