File indexing completed on 2026-09-26 08:04:21
0001 from pathlib import Path
0002 from typing import Optional
0003
0004 from pathlib import Path
0005 from typing import Optional
0006
0007 import acts
0008 import acts.examples
0009 import pytest
0010 import uproot as ur
0011 import awkward as ak
0012 import numpy as np
0013
0014 u = acts.UnitConstants
0015
0016
0017 def runTruthTracking(
0018 trackingGeometry: acts.TrackingGeometry,
0019 field: acts.MagneticFieldProvider,
0020 digiConfigFile: Path,
0021 outputDir: Path,
0022 inputParticlePath: Optional[Path] = None,
0023 inputSimHitsPath: Optional[Path] = None,
0024 decorators=[],
0025 s: acts.examples.Sequencer = None,
0026 n_events: int = 10,
0027 ):
0028 from acts.examples.simulation import (
0029 addParticleGun,
0030 ParticleConfig,
0031 EtaConfig,
0032 PhiConfig,
0033 MomentumConfig,
0034 addFatras,
0035 addDigitization,
0036 ParticleSelectorConfig,
0037 addDigiParticleSelection,
0038 )
0039 from acts.examples.reconstruction import (
0040 addSeeding,
0041 SeedingAlgorithm,
0042 TrackSmearingSigmas,
0043 addTruthTrackingGsf,
0044 )
0045 from acts.examples.root import (
0046 RootParticleReader,
0047 RootSimHitReader,
0048 RootTrackSummaryWriter,
0049 )
0050
0051 s = s or acts.examples.Sequencer(
0052 events=n_events, numThreads=-1, logLevel=acts.logging.INFO
0053 )
0054
0055 for d in decorators:
0056 s.addContextDecorator(d)
0057
0058 rnd = acts.examples.RandomNumbers(seed=42)
0059 outputDir = Path(outputDir)
0060 logger = acts.getDefaultLogger("GSF Example", acts.logging.INFO)
0061
0062 if inputParticlePath is None:
0063 addParticleGun(
0064 s,
0065 ParticleConfig(num=1, pdg=acts.PdgParticle.eElectron, randomizeCharge=True),
0066 EtaConfig(-3.0, 3.0, uniform=True),
0067 MomentumConfig(1.0 * u.GeV, 100.0 * u.GeV, transverse=True),
0068 PhiConfig(0.0, 360.0 * u.degree),
0069 vtxGen=acts.examples.GaussianVertexGenerator(
0070 mean=acts.Vector4(0, 0, 0, 0),
0071 stddev=acts.Vector4(0.015, 0.015, 55.0, 0),
0072 ),
0073 multiplicity=1,
0074 rnd=rnd,
0075 outputDirRoot=outputDir,
0076 )
0077 else:
0078 logger.info("Reading particles from {}", inputParticlePath.resolve())
0079 assert inputParticlePath.exists()
0080 s.addReader(
0081 RootParticleReader(
0082 level=acts.logging.INFO,
0083 filePath=str(inputParticlePath.resolve()),
0084 outputParticles="particles_generated",
0085 )
0086 )
0087 s.addWhiteboardAlias("particles", "particles_generated")
0088
0089 if inputSimHitsPath is None:
0090 addFatras(
0091 s,
0092 trackingGeometry,
0093 field,
0094 rnd=rnd,
0095 enableInteractions=True,
0096 )
0097 else:
0098 logger.info("Reading hits from {}", inputSimHitsPath.resolve())
0099 s.addReader(
0100 RootSimHitReader(
0101 level=acts.logging.INFO,
0102 filePath=str(inputSimHitsPath.resolve()),
0103 outputSimHits="simhits",
0104 )
0105 )
0106 s.addWhiteboardAlias("particles_simulated_selected", "particles_generated")
0107
0108 addDigitization(
0109 s,
0110 trackingGeometry,
0111 field,
0112 digiConfigFile=digiConfigFile,
0113 rnd=rnd,
0114 )
0115
0116 addDigiParticleSelection(
0117 s,
0118 ParticleSelectorConfig(
0119 pt=(0.9 * u.GeV, None),
0120 measurements=(7, None),
0121 removeNeutral=True,
0122 removeSecondaries=True,
0123 ),
0124 )
0125
0126 addSeeding(
0127 s,
0128 trackingGeometry,
0129 field,
0130 rnd=rnd,
0131 inputParticles="particles_generated",
0132 seedingAlgorithm=SeedingAlgorithm.TruthSmeared,
0133 trackSmearingSigmas=TrackSmearingSigmas(
0134
0135 loc0=0,
0136 loc0PtA=0,
0137 loc0PtB=0,
0138 loc1=0,
0139 loc1PtA=0,
0140 loc1PtB=0,
0141 time=0,
0142 phi=0,
0143 theta=0,
0144 ptRel=0,
0145 ),
0146 particleHypothesis=acts.ParticleHypothesis.electron,
0147 initialSigmas=[
0148 1 * u.mm,
0149 1 * u.mm,
0150 1 * u.degree,
0151 1 * u.degree,
0152 0 / u.GeV,
0153 1 * u.ns,
0154 ],
0155 initialSigmaQoverPt=0.1 / u.GeV,
0156 initialSigmaPtRel=0.1,
0157 initialVarInflation=[1e0, 1e0, 1e0, 1e0, 1e0, 1e0],
0158 )
0159
0160 addTruthTrackingGsf(
0161 s,
0162 trackingGeometry,
0163 field,
0164 )
0165
0166 s.addAlgorithm(
0167 acts.examples.TrackSelectorAlgorithm(
0168 level=acts.logging.INFO,
0169 inputTracks="tracks",
0170 outputTracks="selected-tracks",
0171 selectorConfig=acts.TrackSelector.Config(
0172 minMeasurements=7,
0173 ),
0174 )
0175 )
0176 s.addWhiteboardAlias("tracks", "selected-tracks")
0177
0178 s.addWriter(
0179 RootTrackSummaryWriter(
0180 level=acts.logging.INFO,
0181 inputTracks="tracks",
0182 inputParticles="particles_selected",
0183 inputTrackParticleMatching="track_particle_matching",
0184 filePath=str(outputDir / "tracksummary.root"),
0185 writeGsfSpecific=True,
0186 )
0187 )
0188
0189 return s
0190
0191
0192 def readExampleRootData(outputDir: Path):
0193 outputDir = Path(outputDir)
0194 summary_tree = ur.open(outputDir / "tracksummary.root")["tracksummary"]
0195 particles_tree = ur.open(outputDir / "particles.root")["particles"]
0196 summary_fields = ["t_d0", "t_z0", "t_phi", "t_theta"]
0197 summary = summary_tree.arrays(summary_fields, library="ak")
0198 masks = []
0199 for field in summary_fields:
0200 nonempty = ak.to_numpy(ak.num(summary[field]) > 0)
0201
0202 firsts = ak.firsts(summary[field])
0203 arr = ak.to_numpy(firsts)
0204 arr = np.asarray(arr).squeeze()
0205
0206 finite = np.isfinite(arr)
0207
0208 combined_filter = np.logical_and(nonempty, finite)
0209 masks.append(combined_filter)
0210
0211 particle_fields = ["vx", "vy", "vz", "px", "py", "pz", "q"]
0212 particles = particles_tree.arrays(particle_fields, library="ak")
0213 for field in particle_fields:
0214 nonempty = ak.to_numpy(ak.num(particles[field]) > 0)
0215
0216 firsts = ak.firsts(particles[field])
0217 arr = ak.to_numpy(firsts)
0218 arr = np.asarray(arr).squeeze()
0219
0220 finite = np.isfinite(arr)
0221 combined = np.logical_and(nonempty, finite)
0222 masks.append(combined)
0223
0224 combined_mask = np.logical_and.reduce(masks)
0225 simulation_data = {
0226 "d0": ak.to_numpy(summary["t_d0"][combined_mask][:, 0]),
0227 "z0": ak.to_numpy(summary["t_z0"][combined_mask][:, 0]),
0228 "phi": ak.to_numpy(summary["t_phi"][combined_mask][:, 0]),
0229 "theta": ak.to_numpy(summary["t_theta"][combined_mask][:, 0]),
0230 "vx": ak.to_numpy(particles["vx"][combined_mask][:, 0]),
0231 "vy": ak.to_numpy(particles["vy"][combined_mask][:, 0]),
0232 "vz": ak.to_numpy(particles["vz"][combined_mask][:, 0]),
0233 "px": ak.to_numpy(particles["px"][combined_mask][:, 0]),
0234 "py": ak.to_numpy(particles["py"][combined_mask][:, 0]),
0235 "pz": ak.to_numpy(particles["pz"][combined_mask][:, 0]),
0236 "q": ak.to_numpy(particles["q"][combined_mask][:, 0]),
0237 }
0238 return simulation_data
0239
0240
0241 def sampleUniformPointsIn3D(x_offset: float, y_offset: float, z_offset: float, n: int):
0242 if x_offset == 0 and y_offset == 0 and z_offset == 0:
0243 return np.zeros((n, 3))
0244 semi_axes = np.array([x_offset, y_offset, z_offset])
0245 points = np.empty((0, 3))
0246
0247
0248
0249 acceptance_rate = np.pi / 6
0250 while len(points) < n:
0251 remaining = n - len(points)
0252 n_candidates = int(remaining / acceptance_rate) + 10
0253 candidates = np.random.uniform(
0254 low=-semi_axes, high=semi_axes, size=(n_candidates, 3)
0255 )
0256 mask = np.sum((candidates / semi_axes) ** 2, axis=-1) <= 1
0257 points = np.vstack([points, candidates[mask]])
0258 return points[:n]
0259
0260
0261 def vacuumTrackParameterAndBeamspotPropagation(
0262 beamspots,
0263 truth_params,
0264 ):
0265 geo_context = acts.GeometryContext.dangerouslyDefaultConstruct()
0266 mag_field_context = acts.MagneticFieldContext()
0267 field = acts.ConstantBField(acts.Vector3(0, 0, 2 * u.T))
0268 propagator = acts.EigenVoidPropagator(
0269 acts.EigenStepper(field), acts.VoidNavigator()
0270 )
0271 propagator_options = acts.PropagatorPlainOptions(geo_context, mag_field_context)
0272
0273
0274
0275 particle_hypothesis = acts.ParticleHypothesis(211, 0.13957 * u.GeV, 1.0)
0276
0277 n = truth_params.shape[0]
0278 beamspot_pocas = np.empty((n, 5))
0279 for i, truth_params_set in enumerate(truth_params):
0280 beamspot = beamspots[i]
0281 vtx = truth_params_set[:3]
0282 mom = truth_params_set[3:6]
0283 q = truth_params_set[6]
0284
0285 pos4 = acts.Vector4(vtx[0], vtx[1], vtx[2], 0.0)
0286 qOverP = q / np.linalg.norm(mom)
0287 start = acts.BoundTrackParameters.createCurvilinear(
0288 pos4, acts.Vector3(*mom), qOverP, None, particle_hypothesis
0289 )
0290 target = acts.Surface.createPerigee(acts.Vector3(beamspot[0], beamspot[1], 0.0))
0291 result = propagator.propagateToSurface(start, target, propagator_options)
0292 beamspot_pocas[i] = np.array(result.parameters)[:5]
0293 return beamspot_pocas
0294
0295
0296 @pytest.mark.odd
0297 def test_track_propagation_returns_values(odd_detector_config):
0298 field = acts.ConstantBField(acts.Vector3(0, 0, 2 * u.T))
0299
0300 outputDir = Path.cwd()
0301 n = 100
0302 with odd_detector_config.detector:
0303 runTruthTracking(
0304 trackingGeometry=odd_detector_config.trackingGeometry,
0305 field=field,
0306 digiConfigFile=odd_detector_config.digiConfigFile,
0307 outputDir=outputDir,
0308 decorators=odd_detector_config.decorators,
0309 n_events=n,
0310 ).run()
0311
0312 simulation_data = readExampleRootData(outputDir)
0313 truth_params = np.column_stack(
0314 [
0315 simulation_data["vx"],
0316 simulation_data["vy"],
0317 simulation_data["vz"],
0318 simulation_data["px"],
0319 simulation_data["py"],
0320 simulation_data["pz"],
0321 simulation_data["q"],
0322 ]
0323 )
0324
0325 x_offset = 0.1
0326 y_offset = 1.1
0327 z_offset = 10
0328 beamspots = sampleUniformPointsIn3D(x_offset, y_offset, z_offset, len(truth_params))
0329
0330 augmented_perigees = vacuumTrackParameterAndBeamspotPropagation(
0331 beamspots,
0332 truth_params,
0333 )
0334 assert augmented_perigees.shape == (len(truth_params), 5)
0335 assert np.all(np.isfinite(augmented_perigees))