Analysis Software
Documentation for sPHENIX simulation software
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
ImpactPointEstimatorTests.cpp
Go to the documentation of this file. Or view the newest version in sPHENIX GitHub for file ImpactPointEstimatorTests.cpp
1 // This file is part of the Acts project.
2 //
3 // Copyright (C) 2019-2023 CERN for the benefit of the Acts project
4 //
5 // This Source Code Form is subject to the terms of the Mozilla Public
6 // License, v. 2.0. If a copy of the MPL was not distributed with this
7 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 
9 #include <boost/test/data/test_case.hpp>
10 #include <boost/test/unit_test.hpp>
11 
38 
39 #include <algorithm>
40 #include <array>
41 #include <cmath>
42 #include <limits>
43 #include <memory>
44 #include <optional>
45 #include <tuple>
46 #include <utility>
47 #include <vector>
48 
49 namespace {
50 
51 namespace bd = boost::unit_test::data;
52 
53 using namespace Acts;
54 using namespace Acts::UnitLiterals;
56 
61 using Estimator =
63 using StraightLineEstimator =
65 
68 
69 Acts::MagneticFieldProvider::Cache magFieldCache() {
71 }
72 
73 // perigee track parameters dataset
74 // only non-zero distances are tested
75 auto d0s = bd::make({-25_um, 25_um});
76 auto l0s = bd::make({-1_mm, 1_mm});
77 auto t0s = bd::make({-2_ns, 2_ns});
78 auto phis = bd::make({0_degree, -45_degree, 45_degree});
79 auto thetas = bd::make({90_degree, 20_degree, 160_degree});
80 auto ps = bd::make({0.4_GeV, 1_GeV, 10_GeV});
81 auto qs = bd::make({-1_e, 1_e});
82 // Cartesian products over all parameters
83 auto tracksWithoutIPs = t0s * phis * thetas * ps * qs;
84 auto IPs = d0s * l0s;
85 auto tracks = IPs * tracksWithoutIPs;
86 
87 // vertex parameters dataset
88 auto vx0s = bd::make({0_um, -10_um, 10_um});
89 auto vy0s = bd::make({0_um, -10_um, 10_um});
90 auto vz0s = bd::make({0_mm, -25_mm, 25_mm});
91 auto vt0s = bd::make({0_ns, -2_ns, 2_ns});
92 // Cartesian products over all parameters
93 auto vertices = vx0s * vy0s * vz0s * vt0s;
94 
95 // Construct an impact point estimator for a constant bfield along z.
96 Estimator makeEstimator(double bZ) {
97  auto field = std::make_shared<MagneticField>(Vector3(0, 0, bZ));
100  std::make_shared<Propagator>(
101  std::move(stepper), detail::VoidNavigator(),
103  return Estimator(cfg);
104 }
105 
106 // Construct a diagonal track covariance w/ reasonable values.
107 Acts::BoundSquareMatrix makeBoundParametersCovariance(
108  double stdDevTime = 30_ps) {
109  BoundVector stddev;
110  stddev[eBoundLoc0] = 15_um;
111  stddev[eBoundLoc1] = 100_um;
112  stddev[eBoundTime] = stdDevTime;
113  stddev[eBoundPhi] = 1_degree;
114  stddev[eBoundTheta] = 1_degree;
115  stddev[eBoundQOverP] = 1_e / 100_GeV;
116  return stddev.cwiseProduct(stddev).asDiagonal();
117 }
118 
119 // Construct a diagonal vertex covariance w/ reasonable values.
120 Acts::SquareMatrix4 makeVertexCovariance() {
121  Vector4 stddev;
122  stddev[ePos0] = 10_um;
123  stddev[ePos1] = 10_um;
124  stddev[ePos2] = 75_um;
125  stddev[eTime] = 1_ns;
126  return stddev.cwiseProduct(stddev).asDiagonal();
127 }
128 
129 // random value between 0 and 1
130 std::uniform_real_distribution<> uniformDist(0.0, 1.0);
131 // random sign
132 std::uniform_real_distribution<> signDist(-1, 1);
133 } // namespace
134 
135 BOOST_AUTO_TEST_SUITE(VertexingImpactPointEstimator)
136 
137 // Check `calculateDistance`, `estimate3DImpactParameters`, and
138 // `getVertexCompatibility`.
139 BOOST_DATA_TEST_CASE(SingleTrackDistanceParametersCompatibility3D, tracks, d0,
140  l0, t0, phi, theta, p, q) {
142 
143  BoundVector par;
144  par[eBoundLoc0] = d0;
145  par[eBoundLoc1] = l0;
146  par[eBoundTime] = t0;
147  par[eBoundPhi] = phi;
148  par[eBoundTheta] = theta;
149  par[eBoundQOverP] = particleHypothesis.qOverP(p, q);
150 
151  Estimator ipEstimator = makeEstimator(2_T);
152  Estimator::State state(magFieldCache());
153  // reference position and corresponding perigee surface
154  Vector3 refPosition(0., 0., 0.);
155  auto perigeeSurface = Surface::makeShared<PerigeeSurface>(refPosition);
156  // create the track
157  BoundTrackParameters myTrack(
158  perigeeSurface, par, makeBoundParametersCovariance(), particleHypothesis);
159 
160  // initial distance to the reference position in the perigee frame
161  double distT = std::hypot(d0, l0);
162  double dist3 =
163  ipEstimator.calculateDistance(geoContext, myTrack, refPosition, state)
164  .value();
165  // estimated 3D distance should be less than the 2d distance in the perigee
166  // frame. it should be equal if the track is a transverse track w/ theta =
167  // 90deg. in that case there might be numerical deviations and we need to
168  // check that it is less or equal within the numerical tolerance.
169  BOOST_CHECK((dist3 < distT) or
170  (theta == 90_degree and std::abs(dist3 - distT) < 1_nm));
171 
172  // estimate parameters at the closest point in 3d
173  auto res = ipEstimator.estimate3DImpactParameters(
174  geoContext, magFieldContext, myTrack, refPosition, state);
175  BoundTrackParameters trackAtIP3d = *res;
176  const auto& atPerigee = myTrack.parameters();
177  const auto& atIp3d = trackAtIP3d.parameters();
178 
179  // all parameters except the helix invariants theta, q/p should be changed
180  BOOST_CHECK_NE(atPerigee[eBoundLoc0], atIp3d[eBoundLoc0]);
181  BOOST_CHECK_NE(atPerigee[eBoundLoc1], atIp3d[eBoundLoc1]);
182  // BOOST_CHECK_NE(atPerigee[eBoundTime], atIp3d[eBoundTime]);
183  // BOOST_CHECK_NE(atPerigee[eBoundPhi], atIp3d[eBoundPhi]);
184  CHECK_CLOSE_ABS(atPerigee[eBoundTheta], atIp3d[eBoundTheta], 0.01_mrad);
185  CHECK_CLOSE_REL(atPerigee[eBoundQOverP], atIp3d[eBoundQOverP],
186  std::numeric_limits<ActsScalar>::epsilon());
187 
188  // check that we get sensible compatibility scores
189  // this is a chi2-like value and should always be positive
190  auto compatibility =
191  ipEstimator
192  .getVertexCompatibility<3>(geoContext, &trackAtIP3d, refPosition)
193  .value();
194  BOOST_CHECK_GT(compatibility, 0);
195 }
196 
197 BOOST_DATA_TEST_CASE(TimeAtPca, tracksWithoutIPs* vertices, t0, phi, theta, p,
198  q, vx0, vy0, vz0, vt0) {
201 
202  // Set up quantities for constant B field
203  auto field = std::make_shared<MagneticField>(Vector3(0, 0, 2_T));
205  auto propagator = std::make_shared<Propagator>(std::move(stepper));
206  Estimator::Config cfg(field, propagator);
207  Estimator ipEstimator(cfg);
208  Estimator::State ipState(magFieldCache());
209 
210  // Set up quantities for B = 0
211  auto zeroField = std::make_shared<MagneticField>(Vector3(0, 0, 0));
212  StraightLineStepper straightLineStepper;
213  auto straightLinePropagator =
214  std::make_shared<StraightPropagator>(straightLineStepper);
215  StraightLineEstimator::Config zeroFieldCfg(zeroField, straightLinePropagator);
216  StraightLineEstimator zeroFieldIPEstimator(zeroFieldCfg);
217  StraightLineEstimator::State zeroFieldIPState(magFieldCache());
218 
219  // Vertex position and vertex object
220  Vector4 vtxPos(vx0, vy0, vz0, vt0);
221  Vertex<BoundTrackParameters> vtx(vtxPos, makeVertexCovariance(), {});
222 
223  // Perigee surface at vertex position
224  auto vtxPerigeeSurface =
225  Surface::makeShared<PerigeeSurface>(vtxPos.head<3>());
226 
227  // Track parameter vector for a track that originates at the vertex.
228  // Note that 2D and 3D PCA coincide since the track passes exactly through the
229  // vertex.
230  BoundVector paramVec;
231  paramVec[eBoundLoc0] = 0.;
232  paramVec[eBoundLoc1] = 0.;
233  paramVec[eBoundTime] = t0;
234  paramVec[eBoundPhi] = phi;
235  paramVec[eBoundTheta] = theta;
236  paramVec[eBoundQOverP] = q / p;
237 
238  BoundTrackParameters params(vtxPerigeeSurface, paramVec,
239  makeBoundParametersCovariance(),
241 
242  // Correct quantities for checking if IP estimation worked
243  // Time of the track with respect to the vertex
244  ActsScalar corrTimeDiff = t0 - vt0;
245 
246  // Momentum direction at vertex (i.e., at 3D PCA)
247  double cosPhi = std::cos(phi);
248  double sinPhi = std::sin(phi);
249  double sinTheta = std::sin(theta);
250  Vector3 corrMomDir =
251  Vector3(cosPhi * sinTheta, sinPhi * sinTheta, std::cos(theta));
252 
253  // Arbitrary reference point
254  Vector3 refPoint(2_mm, -2_mm, -5_mm);
255 
256  // Perigee surface at vertex position
257  auto refPerigeeSurface = Surface::makeShared<PerigeeSurface>(refPoint);
258 
259  // Set up the propagator options (they are the same with and without B field)
261  auto intersection = refPerigeeSurface
262  ->intersect(geoContext, params.position(geoContext),
263  params.direction(), false)
264  .closest();
265  pOptions.direction =
266  Direction::fromScalarZeroAsPositive(intersection.pathLength());
267 
268  // Propagate to the 2D PCA of the reference point in a constant B field
269  auto result = propagator->propagate(params, *refPerigeeSurface, pOptions);
270  BOOST_CHECK(result.ok());
271  const auto& refParams = *result->endParameters;
272 
273  // Propagate to the 2D PCA of the reference point when B = 0
274  auto zeroFieldResult =
275  straightLinePropagator->propagate(params, *refPerigeeSurface, pOptions);
276  BOOST_CHECK(zeroFieldResult.ok());
277  const auto& zeroFieldRefParams = *zeroFieldResult->endParameters;
278 
279  BOOST_TEST_CONTEXT(
280  "Check time at 2D PCA (i.e., function getImpactParameters) for helical "
281  "tracks") {
282  // Calculate impact parameters
283  auto ipParams = ipEstimator
284  .getImpactParameters(refParams, vtx, geoContext,
285  magFieldContext, true)
286  .value();
287  // Spatial impact parameters should be 0 because the track passes through
288  // the vertex
289  CHECK_CLOSE_ABS(ipParams.d0, 0., 30_nm);
290  CHECK_CLOSE_ABS(ipParams.z0, 0., 100_nm);
291  // Time impact parameter should correspond to the time where the track
292  // passes through the vertex
293  CHECK_CLOSE_OR_SMALL(ipParams.deltaT.value(), std::abs(corrTimeDiff), 1e-5,
294  1e-3);
295  }
296 
297  auto checkGetDistanceAndMomentum = [&vtxPos, &corrMomDir, corrTimeDiff](
298  const auto& ipe, const auto& rParams,
299  auto& state) {
300  // Find 4D distance and momentum of the track at the vertex starting from
301  // the perigee representation at the reference position
302  auto distAndMom = ipe.template getDistanceAndMomentum<4>(
303  geoContext, rParams, vtxPos, state)
304  .value();
305 
306  ActsVector<4> distVec = distAndMom.first;
307  Vector3 momDir = distAndMom.second;
308 
309  // Check quantities:
310  // Spatial distance should be 0 as track passes through the vertex
311  ActsScalar dist = distVec.head<3>().norm();
312  CHECK_CLOSE_ABS(dist, 0., 30_nm);
313  // Distance in time should correspond to the time of the track in a
314  // coordinate system with the vertex as the origin since the track passes
315  // exactly through the vertex
316  CHECK_CLOSE_OR_SMALL(distVec[3], corrTimeDiff, 1e-5, 1e-4);
317  // Momentum direction should correspond to the momentum direction at the
318  // vertex
319  CHECK_CLOSE_OR_SMALL(momDir, corrMomDir, 1e-5, 1e-4);
320  };
321 
322  BOOST_TEST_CONTEXT(
323  "Check time at 3D PCA (i.e., function getDistanceAndMomentum) for "
324  "straight tracks") {
325  checkGetDistanceAndMomentum(zeroFieldIPEstimator, zeroFieldRefParams,
326  zeroFieldIPState);
327  }
328  BOOST_TEST_CONTEXT(
329  "Check time at 3D PCA (i.e., function getDistanceAndMomentum) for "
330  "helical tracks") {
331  checkGetDistanceAndMomentum(ipEstimator, refParams, ipState);
332  }
333 }
334 
335 BOOST_DATA_TEST_CASE(VertexCompatibility4D, IPs* vertices, d0, l0, vx0, vy0,
336  vz0, vt0) {
337  // Set up RNG
338  int seed = 31415;
339  std::mt19937 gen(seed);
340 
341  // Impact point estimator
342  Estimator ipEstimator = makeEstimator(2_T);
343 
344  // Vertex position
345  Vector4 vtxPos(vx0, vy0, vz0, vt0);
346 
347  // Dummy coordinate system with origin at vertex
348  Transform3 coordinateSystem;
349  // First three columns correspond to coordinate system axes
350  coordinateSystem.matrix().block<3, 3>(0, 0) = ActsSquareMatrix<3>::Identity();
351  // Fourth column corresponds to origin of the coordinate system
352  coordinateSystem.matrix().block<3, 1>(0, 3) = vtxPos.head<3>();
353 
354  // Dummy plane surface
355  std::shared_ptr<PlaneSurface> planeSurface =
356  Surface::makeShared<PlaneSurface>(coordinateSystem);
357 
358  // Create two track parameter vectors that are alike except that one is closer
359  // to the vertex in time. Note that momenta don't play a role in the
360  // computation and we set the angles and q/p to 0.
361  // Time offsets
362  double timeDiffFactor = uniformDist(gen);
363  double timeDiffClose = timeDiffFactor * 0.1_ps;
364  double timeDiffFar = timeDiffFactor * 0.11_ps;
365 
366  // Different random signs for the time offsets
367  double sgnClose = signDist(gen) < 0 ? -1. : 1.;
368  double sgnFar = signDist(gen) < 0 ? -1. : 1.;
369 
370  BoundVector paramVecClose = BoundVector::Zero();
371  paramVecClose[eBoundLoc0] = d0;
372  paramVecClose[eBoundLoc1] = l0;
373  paramVecClose[eBoundTime] = vt0 + sgnClose * timeDiffClose;
374 
375  BoundVector paramVecFar = paramVecClose;
376  paramVecFar[eBoundTime] = vt0 + sgnFar * timeDiffFar;
377 
378  // Track whose time is similar to the vertex time
379  BoundTrackParameters paramsClose(planeSurface, paramVecClose,
380  makeBoundParametersCovariance(30_ns),
382 
383  // Track whose time is similar to the vertex time but with a larger time
384  // variance
385  BoundTrackParameters paramsCloseLargerCov(
386  planeSurface, paramVecClose, makeBoundParametersCovariance(31_ns),
388 
389  // Track whose time differs slightly more from the vertex time
390  BoundTrackParameters paramsFar(planeSurface, paramVecFar,
391  makeBoundParametersCovariance(30_ns),
393 
394  // Calculate the 4D vertex compatibilities of the three tracks
395  double compatibilityClose =
396  ipEstimator.getVertexCompatibility<4>(geoContext, &paramsClose, vtxPos)
397  .value();
398  double compatibilityCloseLargerCov =
399  ipEstimator
400  .getVertexCompatibility<4>(geoContext, &paramsCloseLargerCov, vtxPos)
401  .value();
402  double compatibilityFar =
403  ipEstimator.getVertexCompatibility<4>(geoContext, &paramsFar, vtxPos)
404  .value();
405 
406  // The track who is closer in time must have a better (i.e., smaller)
407  // compatibility
408  BOOST_CHECK(compatibilityClose < compatibilityFar);
409  // The track with the larger covariance must be the most compatible
410  BOOST_CHECK(compatibilityCloseLargerCov < compatibilityClose);
411 }
412 
413 // Compare calculations w/ known good values from Athena.
414 //
415 // Checks the results for a single track with the same test values as in Athena
416 // unit test algorithm
417 //
418 // Tracking/TrkVertexFitter/TrkVertexFitterUtils/test/ImpactPointEstimator_test
419 //
420 BOOST_AUTO_TEST_CASE(SingleTrackDistanceParametersAthenaRegression) {
421  Estimator ipEstimator = makeEstimator(1.9971546939_T);
422  Estimator::State state(magFieldCache());
423 
424  // Use same values as in Athena unit test
425  Vector4 pos1(2_mm, 1_mm, -10_mm, 0_ns);
426  Vector3 mom1(400_MeV, 600_MeV, 200_MeV);
427  Vector3 vtxPos(1.2_mm, 0.8_mm, -7_mm);
428 
429  // Start creating some track parameters
430  auto perigeeSurface =
431  Surface::makeShared<PerigeeSurface>(pos1.segment<3>(ePos0));
432  // Some fixed track parameter values
433  auto params1 = BoundTrackParameters::create(
434  perigeeSurface, geoContext, pos1, mom1, 1_e / mom1.norm(),
435  BoundTrackParameters::CovarianceMatrix::Identity(),
437  .value();
438 
439  // Compare w/ desired result from Athena unit test
440  auto distance =
441  ipEstimator.calculateDistance(geoContext, params1, vtxPos, state).value();
442  CHECK_CLOSE_ABS(distance, 3.10391_mm, 10_nm);
443 
444  auto res2 = ipEstimator.estimate3DImpactParameters(
445  geoContext, magFieldContext, params1, vtxPos, state);
446  BOOST_CHECK(res2.ok());
447  BoundTrackParameters endParams = *res2;
448  Vector3 surfaceCenter = endParams.referenceSurface().center(geoContext);
449 
450  BOOST_CHECK_EQUAL(surfaceCenter, vtxPos);
451 }
452 
453 // Test the Impact3d Point estimator 2d and 3d lifetimes sign
454 // on a single track.
455 
456 BOOST_AUTO_TEST_CASE(Lifetimes2d3d) {
457  Estimator ipEstimator = makeEstimator(2_T);
458 
459  // Create a track from a decay
460  BoundVector trk_par;
461  trk_par[eBoundLoc0] = 200_um;
462  trk_par[eBoundLoc1] = 300_um;
463  trk_par[eBoundTime] = 1_ns;
464  trk_par[eBoundPhi] = 45_degree;
465  trk_par[eBoundTheta] = 45_degree;
466  trk_par[eBoundQOverP] = 1_e / 10_GeV;
467 
468  Vector4 ip_pos{0., 0., 0., 0.};
469  Vertex<BoundTrackParameters> ip_vtx(ip_pos, makeVertexCovariance(), {});
470 
471  // Form the bound track parameters at the ip
472  auto perigeeSurface = Surface::makeShared<PerigeeSurface>(ip_pos.head<3>());
473  BoundTrackParameters track(perigeeSurface, trk_par,
474  makeBoundParametersCovariance(),
476 
477  Vector3 direction{0., 1., 0.};
478  auto lifetimes_signs = ipEstimator.getLifetimeSignOfTrack(
479  track, ip_vtx, direction, geoContext, magFieldContext);
480 
481  // Check if the result is OK
482  BOOST_CHECK(lifetimes_signs.ok());
483 
484  // Check that d0 sign is positive
485  BOOST_CHECK((*lifetimes_signs).first > 0.);
486 
487  // Check that z0 sign is negative
488  BOOST_CHECK((*lifetimes_signs).second < 0.);
489 
490  // Check the 3d sign
491 
492  auto sign3d = ipEstimator.get3DLifetimeSignOfTrack(
493  track, ip_vtx, direction, geoContext, magFieldContext);
494 
495  // Check result is OK
496  BOOST_CHECK(sign3d.ok());
497 
498  // Check 3D sign (should be positive)
499  BOOST_CHECK((*sign3d) > 0.);
500 }
501 
502 // Check `.getImpactParameters`.
503 BOOST_DATA_TEST_CASE(SingeTrackImpactParameters, tracks* vertices, d0, l0, t0,
504  phi, theta, p, q, vx0, vy0, vz0, vt0) {
505  BoundVector par;
506  par[eBoundLoc0] = d0;
507  par[eBoundLoc1] = l0;
508  par[eBoundTime] = t0;
509  par[eBoundPhi] = phi;
510  par[eBoundTheta] = theta;
511  par[eBoundQOverP] = q / p;
512  Vector4 vtxPos;
513  vtxPos[ePos0] = vx0;
514  vtxPos[ePos1] = vy0;
515  vtxPos[ePos2] = vz0;
516  vtxPos[eTime] = vt0;
517 
518  Estimator ipEstimator = makeEstimator(1_T);
519  Estimator::State state(magFieldCache());
520 
521  // reference position and corresponding perigee surface
522  Vector3 refPosition(0., 0., 0.);
523  auto perigeeSurface = Surface::makeShared<PerigeeSurface>(refPosition);
524  // create track and vertex
525  BoundTrackParameters track(perigeeSurface, par,
526  makeBoundParametersCovariance(),
527  ParticleHypothesis::pionLike(std::abs(q)));
528  Vertex<BoundTrackParameters> myConstraint(vtxPos, makeVertexCovariance(), {});
529 
530  // check that computed impact parameters are meaningful
532  ipEstimator
533  .getImpactParameters(track, myConstraint, geoContext, magFieldContext)
534  .value();
535  BOOST_CHECK_NE(output.d0, 0.);
536  BOOST_CHECK_NE(output.z0, 0.);
537  // TODO what about the other struct members? can the parameter space be
538  // restricted further?
539 }
540 
541 BOOST_AUTO_TEST_SUITE_END()