ECCE @ EIC Software
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
FloatComparisons.hpp
Go to the documentation of this file. Or view the newest version in sPHENIX GitHub for file FloatComparisons.hpp
1 // This file is part of the Acts project.
2 //
3 // Copyright (C) 2017 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 #pragma once
10 
11 #include <algorithm>
12 #include <limits>
13 
14 #include <boost/test/unit_test.hpp>
15 
17 
18 // The following assertions can be seen as an extension of the BOOST_CHECK_XYZ
19 // macros which also support approximate comparisons of containers of floating-
20 // point numbers. Containers are compared by size and element-wise.
21 //
22 // Relative tolerances are fractional: 2e-4 means +/-0.02% from the reference.
23 //
24 // Test failures are reported in detail, from the floating-point comparison
25 // that failed (and the reason why it failed) to the context in which the
26 // failure occured (container contents if applicable, source file & line...).
27 
28 // Check if "val" and "ref" are within relative tolerance "tol" of each other.
29 #define CHECK_CLOSE_REL(val, ref, reltol) \
30  BOOST_CHECK(Acts::Test::checkCloseRel((val), (ref), (reltol)))
31 
32 // Check if "val" and "ref" are within absolute tolerance "tol" of each other.
33 // Equivalent to CHECK_SMALL(val - ref), but does not require an operator-().
34 #define CHECK_CLOSE_ABS(val, ref, abstol) \
35  BOOST_CHECK(Acts::Test::checkCloseAbs((val), (ref), (abstol)))
36 
37 // Check if "val" is below absolute threshold "small".
38 // Equivalent to CHECK_CLOSE_ABS(val, 0), but does not require a zero value.
39 #define CHECK_SMALL(val, small) \
40  BOOST_CHECK(Acts::Test::checkSmall((val), (small)))
41 
42 // Start with a relative comparison, but tolerate failures when both the value
43 // and the reference are below "small". This assertion is useful when comparing
44 // containers of values and the reference container has zeroes.
45 #define CHECK_CLOSE_OR_SMALL(val, ref, reltol, small) \
46  BOOST_CHECK(Acts::Test::checkCloseOrSmall((val), (ref), (reltol), (small)))
47 
48 // Covariance matrices require special logic and care because while relative
49 // comparisons are perfectly appropriate on diagonal terms, they become
50 // inappropriate on off-diagonal terms, which represent correlations and are
51 // therefore best compared with respect to the order of magnitude of the
52 // corresponding diagonal elements.
53 #define CHECK_CLOSE_COVARIANCE(val, ref, tol) \
54  BOOST_CHECK(Acts::Test::checkCloseCovariance((val), (ref), (tol)))
55 
56 // The relevant infrastructure is implemented below
57 
58 namespace Acts {
59 namespace Test {
60 namespace float_compare_internal {
61 
62 // Under the hood, various scalar comparison logics may be used
63 
65 
66 using ScalarComparison = std::function<predicate_result(double, double)>;
67 
68 ScalarComparison closeOrSmall(double reltol, double small) {
69  return [=](double val, double ref) -> predicate_result {
70  // Perform the comparison, exit on success
71  if (std::abs(ref) >= small) {
72  // Reference is large enough for a relative comparison
73  if (std::abs(val - ref) < reltol * std::abs(ref)) {
74  return true;
75  }
76  } else if (std::abs(val) < small) {
77  // Reference is small and value is small too
78  return true;
79  }
80 
81  // Comparison failed, report why
82  predicate_result res(false);
83  res.message() << "The floating point value " << val;
84  if ((std::abs(ref) < small) || (reltol == 0.)) {
85  res.message() << " is above small-ness threshold " << small;
86  } else {
87  res.message() << " is not within relative tolerance " << reltol
88  << " of reference " << ref;
89  }
90  res.message() << '.';
91  return res;
92  };
93 }
94 
95 ScalarComparison closeAbs(double abstol) {
96  return [=](double val, double ref) -> predicate_result {
97  // Perform the comparison, exit on success
98  if (std::abs(ref - val) <= abstol) {
99  return true;
100  }
101 
102  // Comparison failed, report why
103  predicate_result res(false);
104  res.message() << "The floating point value " << val
105  << " is not within absolute tolerance " << abstol
106  << " of reference " << ref << '.';
107  return res;
108  };
109 }
110 
111 // Container comparison is then implemented on top of scalar comparison
112 
113 // Matrix comparison backend (called by Eigen-related compare() overloads)
114 template <typename Derived1, typename Derived2>
115 predicate_result matrixCompare(const Eigen::DenseBase<Derived1>& val,
116  const Eigen::DenseBase<Derived2>& ref,
117  ScalarComparison&& compareImpl) {
118  constexpr int rows1 = Eigen::DenseBase<Derived1>::RowsAtCompileTime;
119  constexpr int rows2 = Eigen::DenseBase<Derived2>::RowsAtCompileTime;
120  constexpr int cols1 = Eigen::DenseBase<Derived1>::ColsAtCompileTime;
121  constexpr int cols2 = Eigen::DenseBase<Derived2>::ColsAtCompileTime;
122 
123  if constexpr (rows1 != Eigen::Dynamic && rows2 != Eigen::Dynamic &&
124  cols1 != Eigen::Dynamic && cols2 != Eigen::Dynamic) {
125  // All dimensions on both are static. Static assert compatibility.
126  static_assert(rows1 == rows2,
127  "Input matrices do not have the same number of rows");
128  static_assert(cols1 == cols2,
129  "Input matrices do not have the same number of columns");
130  } else {
131  // some are dynamic, do runtime check
132  if (val.rows() != ref.rows() || val.cols() != ref.cols()) {
133  predicate_result res{false};
134  res.message() << "Mismatch in matrix dimensions:\n" << val << "\n" << ref;
135  return res;
136  }
137  }
138 
139  // for looping, always use runtime values
140  for (int col = 0; col < val.cols(); ++col) {
141  for (int row = 0; row < val.rows(); ++row) {
142  predicate_result res = compareImpl(val(row, col), ref(row, col));
143  if (!res) {
144  res.message() << " The failure occured during a matrix comparison,"
145  << " at index (" << row << ", " << col << ")."
146  << " The value was\n"
147  << val << '\n'
148  << "and the reference was\n"
149  << ref << '\n';
150  return res;
151  }
152  }
153  }
154  return true;
155 }
156 
157 // STL container frontend
158 //
159 // FIXME: The algorithm only supports ordered containers, so the API should
160 // only accept them. Does someone know a clean way to do that in C++?
161 //
162 template <typename Container,
163  typename Enable = typename Container::const_iterator>
164 predicate_result compare(const Container& val, const Container& ref,
165  ScalarComparison&& compareImpl) {
166  // Make sure that the two input containers have the same number of items
167  // (in order to provide better error reporting when they don't)
168  size_t numVals = std::distance(std::cbegin(val), std::cend(val));
169  size_t numRefs = std::distance(std::cbegin(ref), std::cend(ref));
170  if (numVals != numRefs) {
171  predicate_result res(false);
172  res.message() << "The container size does not match (value has " << numVals
173  << " elements, reference has " << numRefs << " elements).";
174  return res;
175  }
176 
177  // Compare the container's contents, bubbling assertion results up. Sadly,
178  // this means that we cannot use std::equal.
179  auto valBeg = std::cbegin(val);
180  auto valIter = valBeg;
181  auto valEnd = std::cend(val);
182  auto refIter = std::cbegin(ref);
183  while (valIter != valEnd) {
184  predicate_result res = compareImpl(*valIter, *refIter);
185  if (!res) {
186  // If content comparison failed, report the container's contents
187  res.message() << " The failure occured during a container comparison,"
188  << " at index " << std::distance(valBeg, valIter) << '.'
189  << " The value contained {";
190  for (const auto& item : val) {
191  res.message() << ' ' << item << ' ';
192  }
193  res.message() << "} and the reference contained {";
194  for (const auto& item : ref) {
195  res.message() << ' ' << item << ' ';
196  }
197  res.message() << "}.";
198  return res;
199  }
200  ++valIter;
201  ++refIter;
202  }
203 
204  // If the size and contents match, we're good
205  return true;
206 }
207 
208 // Eigen expression template frontend
209 template <typename T, typename U>
210 predicate_result compare(const Eigen::DenseBase<T>& val,
211  const Eigen::DenseBase<U>& ref,
212  ScalarComparison&& compareImpl) {
213  return matrixCompare(val.eval(), ref.eval(), std::move(compareImpl));
214 }
215 
216 // Eigen transform frontend
218  ScalarComparison&& compareImpl) {
219  return matrixCompare(val.matrix(), ref.matrix(), std::move(compareImpl));
220 }
221 
222 // Scalar frontend
223 predicate_result compare(double val, double ref,
224  ScalarComparison&& compareImpl) {
225  return compareImpl(val, ref);
226 }
227 } // namespace float_compare_internal
228 
229 // ...and with all that, we can implement the CHECK_XYZ macros
230 
231 template <typename T, typename U>
233  double reltol) {
234  using namespace float_compare_internal;
235  return compare(val, ref, closeOrSmall(reltol, 0.));
236 }
237 
238 template <typename T, typename U>
240  double abstol) {
241  using namespace float_compare_internal;
242  return compare(val, ref, closeAbs(abstol));
243 }
244 
245 template <typename T>
247  using namespace float_compare_internal;
248  return compare(val, val, closeOrSmall(0., small));
249 }
250 
251 template <typename T, typename U>
253  const U& ref,
254  double reltol,
255  double small) {
256  using namespace float_compare_internal;
257  return compare(val, ref, closeOrSmall(reltol, small));
258 }
259 
260 template <typename Scalar, int dim>
262  const ActsSymMatrix<Scalar, dim>& val,
263  const ActsSymMatrix<Scalar, dim>& ref, double tol) {
264  static_assert(dim != Eigen::Dynamic,
265  "Dynamic-size matrices are currently unsupported.");
266 
267  for (int col = 0; col < dim; ++col) {
268  for (int row = col; row < dim; ++row) {
269  // For diagonal elements, this is just a regular relative comparison.
270  // But for off-diagonal correlation terms, the tolerance scales with the
271  // geometric mean of the variance terms that are being correlated.
272  //
273  // This accounts for the fact that a relatively large correlation
274  // difference means little if the overall correlation has a tiny weight
275  // with respect to the diagonal variance elements anyway.
276  //
277  auto orderOfMagnitude = std::sqrt(ref(row, row) * ref(col, col));
278  if (std::abs(val(row, col) - ref(row, col)) >= tol * orderOfMagnitude) {
280  res.message() << "The difference between the covariance matrix term "
281  << val(row, col) << " and its reference " << ref(row, col)
282  << ","
283  << " at index (" << row << ", " << col << "),"
284  << " is not within tolerance " << tol << '.'
285  << " The covariance matrix being tested was\n"
286  << val << '\n'
287  << "and the reference covariance matrix was\n"
288  << ref << '\n';
289  return res;
290  }
291  }
292  }
293  return true;
294 }
295 } // namespace Test
296 } // namespace Acts