Analysis Software
Documentation for sPHENIX simulation software
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
GsfComponentMergingTests.cpp
Go to the documentation of this file. Or view the newest version in sPHENIX GitHub for file GsfComponentMergingTests.cpp
1 // This file is part of the Acts project.
2 //
3 // Copyright (C) 2022 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/unit_test.hpp>
10 
29 
30 #include <algorithm>
31 #include <array>
32 #include <cmath>
33 #include <cstddef>
34 #include <functional>
35 #include <initializer_list>
36 #include <memory>
37 #include <random>
38 #include <stdexcept>
39 #include <tuple>
40 #include <utility>
41 #include <vector>
42 
43 #include <Eigen/Eigenvalues>
44 
45 #define CHECK_CLOSE_MATRIX(a, b, t) \
46  BOOST_CHECK(((a - b).array().abs() < t).all())
47 
48 using namespace Acts;
49 using namespace Acts::UnitLiterals;
50 
51 // Describes a component of a D-dimensional gaussian component
52 template <int D>
54  Acts::ActsScalar weight = 0;
57 };
58 
59 // A Multivariate distribution object working in the same way as the
60 // distributions in the standard library
61 template <typename T, int D>
63  public:
64  using Vector = Eigen::Matrix<T, D, 1>;
65  using Matrix = Eigen::Matrix<T, D, D>;
66 
67  private:
70 
71  public:
73  : m_mean(mean) {
74  Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> eigenSolver(boundCov);
75  m_transform = eigenSolver.eigenvectors() *
76  eigenSolver.eigenvalues().cwiseSqrt().asDiagonal();
77  }
78 
79  template <typename generator_t>
80  Vector operator()(generator_t &gen) const {
81  std::normal_distribution<T> normal;
82  return m_mean +
83  m_transform * Vector{}.unaryExpr([&](auto) { return normal(gen); });
84  }
85 };
86 
87 // Sample data from a multi-component multivariate distribution
88 template <int D>
89 auto sampleFromMultivariate(const std::vector<DummyComponent<D>> &cmps,
90  std::size_t n_samples, std::mt19937 &gen) {
92 
93  std::vector<MultiNormal> dists;
94  std::vector<double> weights;
95  for (const auto &cmp : cmps) {
96  dists.push_back(MultiNormal(cmp.boundPars, cmp.boundCov));
97  weights.push_back(cmp.weight);
98  }
99 
100  std::discrete_distribution choice(weights.begin(), weights.end());
101 
102  auto sample = [&]() {
103  const auto n = choice(gen);
104  return dists[n](gen);
105  };
106 
107  std::vector<ActsVector<D>> samples(n_samples);
108  std::generate(samples.begin(), samples.end(), sample);
109 
110  return samples;
111 }
112 
113 // Simple arithmetic mean computation
114 template <int D>
115 auto mean(const std::vector<ActsVector<D>> &samples) -> ActsVector<D> {
117 
118  for (const auto &x : samples) {
119  mean += x;
120  }
121 
122  return mean / samples.size();
123 }
124 
125 // A method to compute the circular mean, since the normal arithmetic mean
126 // doesn't work for angles in general
127 template <int D>
128 auto circularMean(const std::vector<ActsVector<D>> &samples) -> ActsVector<D> {
131 
132  for (const auto &s : samples) {
133  for (int i = 0; i < D; ++i) {
134  x[i] += std::cos(s[i]);
135  y[i] += std::sin(s[i]);
136  }
137  }
138 
140 
141  for (int i = 0; i < D; ++i) {
142  mean[i] = std::atan2(y[i], x[i]);
143  }
144 
145  return mean;
146 }
147 
148 // This general boundCovariance estimator can be equipped with a custom
149 // subtraction object to enable circular behaviour
150 template <int D, typename subtract_t = std::minus<ActsVector<D>>>
151 auto boundCov(const std::vector<ActsVector<D>> &samples,
152  const ActsVector<D> &mu, const subtract_t &sub = subtract_t{})
155 
156  for (const auto &smpl : samples) {
157  boundCov += sub(smpl, mu) * sub(smpl, mu).transpose();
158  }
159 
160  return boundCov / samples.size();
161 }
162 
163 // This function computes the mean of a bound gaussian mixture by converting
164 // them to cartesian coordinates, computing the mean, and converting back to
165 // bound.
167  const Surface &surface) {
168  // Specially handle LOC0, since the free mean would not be on the surface
169  // likely
170  if (surface.type() == Surface::Cylinder) {
171  auto x = 0.0, y = 0.0;
172  const auto r = surface.bounds().values()[CylinderBounds::eR];
173 
174  for (const auto &cmp : cmps) {
175  x += cmp.weight * std::cos(cmp.boundPars[eBoundLoc0] / r);
176  y += cmp.weight * std::sin(cmp.boundPars[eBoundLoc0] / r);
177  }
178 
179  for (auto &cmp : cmps) {
180  cmp.boundPars[eBoundLoc0] = std::atan2(y, x) * r;
181  }
182  }
183 
184  if (surface.type() == Surface::Cone) {
185  throw std::runtime_error("Cone surface not supported");
186  }
187 
188  FreeVector mean = FreeVector::Zero();
189 
190  for (const auto &cmp : cmps) {
192  surface, GeometryContext{}, cmp.boundPars);
193  }
194 
195  mean.segment<3>(eFreeDir0).normalize();
196 
197  // Project the position on the surface.
198  // This is mainly necessary for the perigee surface, where
199  // the mean might not fulfill the perigee condition.
200  Vector3 position = mean.head<3>();
201  Vector3 direction = mean.segment<3>(eFreeDir0);
202  auto intersection =
203  surface.intersect(GeometryContext{}, position, direction, false)
204  .closest();
205  mean.head<3>() = intersection.position();
206 
207  return *detail::transformFreeToBoundParameters(mean, surface,
208  GeometryContext{});
209 }
210 
211 // Typedef to describe local positions of 4 components
212 using LocPosArray = std::array<std::pair<double, double>, 4>;
213 
214 // Test the combination for a surface type. The local positions are given from
215 // the outside since their meaning differs between surface types
216 template <typename angle_description_t>
217 void test_surface(const Surface &surface, const angle_description_t &desc,
218  const LocPosArray &loc_pos, double expectedError) {
219  const auto proj = Identity{};
220 
221  for (auto phi : {-175_degree, 0_degree, 175_degree}) {
222  for (auto theta : {5_degree, 90_degree, 175_degree}) {
223  // Go create mixture with 4 cmps
224  std::vector<DummyComponent<eBoundSize>> cmps;
225 
226  auto p_it = loc_pos.begin();
227 
228  for (auto dphi : {-10_degree, 10_degree}) {
229  for (auto dtheta : {-5_degree, 5_degree}) {
231  a.weight = 1. / 4.;
232  a.boundPars = BoundVector::Ones();
233  a.boundPars[eBoundLoc0] *= p_it->first;
234  a.boundPars[eBoundLoc1] *= p_it->second;
235  a.boundPars[eBoundPhi] =
236  detail::wrap_periodic(phi + dphi, -M_PI, 2 * M_PI);
237  a.boundPars[eBoundTheta] = theta + dtheta;
238 
239  // We don't look at covariance in this test
240  a.boundCov = BoundSquareMatrix::Zero();
241 
242  cmps.push_back(a);
243  ++p_it;
244  }
245  }
246 
247  const auto [mean_approx, cov_approx] =
249 
250  const auto mean_ref = meanFromFree(cmps, surface);
251 
252  CHECK_CLOSE_MATRIX(mean_approx, mean_ref, expectedError);
253  }
254  }
255 }
256 
257 BOOST_AUTO_TEST_CASE(test_with_data) {
258  std::mt19937 gen(42);
259  std::vector<DummyComponent<2>> cmps(2);
260 
261  cmps[0].boundPars << 1.0, 1.0;
262  cmps[0].boundCov << 1.0, 0.0, 0.0, 1.0;
263  cmps[0].weight = 0.5;
264 
265  cmps[1].boundPars << -2.0, -2.0;
266  cmps[1].boundCov << 1.0, 1.0, 1.0, 2.0;
267  cmps[1].weight = 0.5;
268 
269  const auto samples = sampleFromMultivariate(cmps, 10000, gen);
270  const auto mean_data = mean(samples);
271  const auto boundCov_data = boundCov(samples, mean_data);
272 
273  const auto [mean_test, boundCov_test] =
274  detail::gaussianMixtureMeanCov(cmps, Identity{}, std::tuple<>{});
275 
276  CHECK_CLOSE_MATRIX(mean_data, mean_test, 1.e-1);
277  CHECK_CLOSE_MATRIX(boundCov_data, boundCov_test, 1.e-1);
278 }
279 
280 BOOST_AUTO_TEST_CASE(test_with_data_circular) {
281  std::mt19937 gen(42);
282  std::vector<DummyComponent<2>> cmps(2);
283 
284  cmps[0].boundPars << 175_degree, 5_degree;
285  cmps[0].boundCov << 20_degree, 0.0, 0.0, 20_degree;
286  cmps[0].weight = 0.5;
287 
288  cmps[1].boundPars << -175_degree, -5_degree;
289  cmps[1].boundCov << 20_degree, 20_degree, 20_degree, 40_degree;
290  cmps[1].weight = 0.5;
291 
292  const auto samples = sampleFromMultivariate(cmps, 10000, gen);
293  const auto mean_data = circularMean(samples);
294  const auto boundCov_data = boundCov(samples, mean_data, [](auto a, auto b) {
295  Vector2 res = Vector2::Zero();
296  for (int i = 0; i < 2; ++i) {
297  res[i] = detail::difference_periodic(a[i], b[i], 2 * M_PI);
298  }
299  return res;
300  });
301 
302  using detail::CyclicAngle;
303  const auto d = std::tuple<CyclicAngle<eBoundLoc0>, CyclicAngle<eBoundLoc1>>{};
304  const auto [mean_test, boundCov_test] =
306 
307  BOOST_CHECK(std::abs(detail::difference_periodic(mean_data[0], mean_test[0],
308  2 * M_PI)) < 1.e-1);
309  BOOST_CHECK(std::abs(detail::difference_periodic(mean_data[1], mean_test[1],
310  2 * M_PI)) < 1.e-1);
311  CHECK_CLOSE_MATRIX(boundCov_data, boundCov_test, 1.e-1);
312 }
313 
314 BOOST_AUTO_TEST_CASE(test_plane_surface) {
316 
317  const auto surface =
318  Surface::makeShared<PlaneSurface>(Vector3{0, 0, 0}, Vector3{1, 0, 0});
319 
320  const LocPosArray p{{{1, 1}, {1, -1}, {-1, 1}, {-1, -1}}};
321 
322  test_surface(*surface, desc, p, 1.e-2);
323 }
324 
325 BOOST_AUTO_TEST_CASE(test_cylinder_surface) {
326  const Transform3 trafo = Transform3::Identity();
327  const double r = 2;
328  const double halfz = 100;
329 
330  const auto surface = Surface::makeShared<CylinderSurface>(trafo, r, halfz);
331 
332  const double z1 = -1, z2 = 1;
333  const double phi1 = 178_degree, phi2 = -176_degree;
334 
335  const LocPosArray p{
336  {{r * phi1, z1}, {r * phi1, -z2}, {r * phi2, z1}, {r * phi2, z2}}};
337 
339  std::get<0>(desc).constant = r;
340 
341  test_surface(*surface, desc, p, 1.e-2);
342 }
343 
344 BOOST_AUTO_TEST_CASE(test_disc_surface) {
345  const Transform3 trafo = Transform3::Identity();
346  const auto radius = 1;
347 
348  const auto surface = Surface::makeShared<DiscSurface>(trafo, 0.0, radius);
349 
350  const double r1 = 0.4, r2 = 0.8;
351  const double phi1 = -178_degree, phi2 = 176_degree;
352 
353  const LocPosArray p{{{r1, phi1}, {r2, phi2}, {r1, phi2}, {r2, phi1}}};
354 
356 
357  test_surface(*surface, desc, p, 1.e-2);
358 }
359 
360 BOOST_AUTO_TEST_CASE(test_perigee_surface) {
362 
363  const auto surface = Surface::makeShared<PerigeeSurface>(Vector3{0, 0, 0});
364 
365  const auto z = 5;
366  const auto d = 1;
367 
368  const LocPosArray p{{{d, z}, {d, -z}, {2 * d, z}, {2 * d, -z}}};
369 
370  // Here we expect a very bad approximation
371  test_surface(*surface, desc, p, 1.1);
372 }