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