Line data Source code
1 : #pragma once
2 :
3 : #include <type_traits>
4 : #include <complex>
5 : #include <random>
6 : #include "doctest/doctest.h"
7 : #include "elsaDefines.h"
8 : #include "DataDescriptor.h"
9 : #include "DataContainer.h"
10 :
11 : #include <iomanip>
12 : #include <limits>
13 : #include <cassert>
14 :
15 : namespace elsa
16 : {
17 : /// With C++20 this can be replaced by std::type_identity
18 : template <class T>
19 : struct SelfType {
20 : using type = T;
21 : };
22 :
23 : template <class T>
24 : using SelfType_t = typename SelfType<T>::type;
25 :
26 : /**
27 : * @brief Epsilon (in percentage) value for our test suit
28 : */
29 : static constexpr elsa::real_t epsilon = static_cast<elsa::real_t>(0.01);
30 :
31 : /**
32 : * @brief literal operator to convert `long double` value to `doctest::Approx`
33 : */
34 : doctest::Approx operator"" _a(long double val);
35 :
36 : /**
37 : * @brief literal operator to convert `unsigned long long` value to `doctest::Approx`
38 : */
39 : doctest::Approx operator"" _a(unsigned long long val);
40 :
41 : /**
42 : * @brief comparing two number types for approximate equality for complex and regular number
43 : *
44 : * Use example in test case: REQUIRE_UNARY(checkSameNumbers(a, b));
45 : * The CHECK(...) assertion in the function ensures that the values are reported when the test
46 : * fails.
47 :
48 : * @tparam T - arithmetic data type
49 : * @return true if same number
50 : *
51 : * @tparam T - arithmetic data type
52 : * @return true if same number
53 : */
54 : template <typename T>
55 : bool checkSameNumbers(T left, SelfType_t<T> right)
56 : {
57 : return checkApproxEq(left, right);
58 : }
59 :
60 : /**
61 : * @brief compare two numbers depending on their type approximate it.
62 : *
63 : * For complex numbers real and imaginary parts are compared separately, floating point
64 : * values are compared using doctest::Approx. All other types are just compared using
65 : * `CHECK_EQ`.
66 : *
67 : * Use example in test case: REQUIRE_UNARY(checkSameNumbers(a, b));
68 : * The CHECK(...) assertion in the function ensures that the values are reported when the test
69 : * fails.
70 : *
71 : * @tparam T - arithmetic data type
72 : * @param left left part of comparison, determines template type
73 : * @param right right part of comparison, not part of deducing template type
74 : * @param margin allowed tolerance in percentage
75 : */
76 : template <typename T>
77 0 : [[nodiscard]] bool checkApproxEq(T left, SelfType_t<T> right, double margin = epsilon)
78 : {
79 : using Approx = doctest::Approx;
80 :
81 : if constexpr (std::is_same_v<T, complex<float>> || std::is_same_v<T, complex<double>>) {
82 0 : CHECK_EQ(Approx(left.real()).epsilon(margin), right.real());
83 0 : CHECK_EQ(Approx(left.imag()).epsilon(margin), right.imag());
84 :
85 0 : return Approx(left.real()).epsilon(margin) == right.real()
86 0 : && Approx(left.imag()).epsilon(margin) == right.imag();
87 : } else if constexpr (std::is_floating_point_v<T>) {
88 0 : CHECK_EQ(Approx(left).epsilon(margin), right);
89 0 : return Approx(left).epsilon(margin) == right;
90 : } else {
91 0 : CHECK_EQ(left, right);
92 0 : return left == right;
93 : }
94 : }
95 :
96 : template <typename T>
97 0 : [[nodiscard]] bool checkApproxNe(T left, SelfType_t<T> right, double margin = epsilon)
98 : {
99 : using Approx = doctest::Approx;
100 :
101 : if constexpr (std::is_same_v<T, complex<float>> || std::is_same_v<T, complex<double>>) {
102 0 : CHECK_NE(Approx(left.real()).epsilon(margin), right.real());
103 0 : CHECK_NE(Approx(left.imag()).epsilon(margin), right.imag());
104 :
105 0 : return Approx(left.real()).epsilon(margin) != right.real()
106 0 : && Approx(left.imag()).epsilon(margin) != right.imag();
107 : } else if constexpr (std::is_floating_point_v<T>) {
108 0 : CHECK_NE(Approx(left).epsilon(margin), right);
109 0 : return Approx(left).epsilon(margin) != right;
110 : } else {
111 0 : CHECK_NE(left, right);
112 0 : return left != right;
113 : }
114 : }
115 :
116 : template <typename T>
117 0 : [[nodiscard]] bool approxEq(T left, SelfType_t<T> right)
118 : {
119 : using Approx = doctest::Approx;
120 :
121 : if constexpr (std::is_same_v<T, complex<float>> || std::is_same_v<T, complex<double>>) {
122 0 : return Approx(left.real()).epsilon(epsilon) == right.real()
123 0 : && Approx(left.imag()).epsilon(epsilon) == right.imag();
124 : } else if constexpr (std::is_floating_point_v<T>) {
125 0 : return Approx(left).epsilon(epsilon) == right;
126 : } else {
127 0 : return left == right;
128 : }
129 : }
130 :
131 : /**
132 : * @brief Generates a random Eigen matrix for different data_t types with integer values limited
133 : * to a certain range
134 : *
135 : * @param[in] size the number of elements in the vector like matrix
136 : *
137 : * @tparam data_t the numerical type to use
138 : *
139 : * The integer range is chosen to be small, to allow multiplication with the values without
140 : * running into overflow issues.
141 : */
142 : template <typename data_t>
143 0 : auto generateRandomMatrix(index_t size)
144 : {
145 0 : Vector_t<data_t> randVec(size);
146 :
147 : if constexpr (std::is_integral_v<data_t>) {
148 : // Define range depending on signed or unsigned type
149 0 : const auto [rangeBegin, rangeEnd] = []() -> std::tuple<data_t, data_t> {
150 : if constexpr (std::is_signed_v<data_t>) {
151 : return {-100, 100};
152 : } else {
153 : return {1, 100};
154 : }
155 : }();
156 :
157 0 : std::random_device rd;
158 0 : std::mt19937 eng(rd());
159 0 : std::uniform_int_distribution<data_t> distr(rangeBegin, rangeEnd);
160 :
161 0 : for (index_t i = 0; i < size; ++i) {
162 0 : data_t num = distr(eng);
163 :
164 : // remove zeros as this leads to errors when dividing
165 0 : if (num == 0)
166 0 : num = 1;
167 0 : randVec[i] = num;
168 : }
169 0 : } else {
170 0 : randVec.setRandom();
171 : }
172 :
173 0 : return randVec;
174 0 : }
175 :
176 : /**
177 : * @brief generate a random eigen vector and a DataContainer with the same data. Specifically
178 : * take index_t into consideration and scale the random eigen vector, to not generate overflows
179 : *
180 : * @tparam data_t Value type of DataContainers
181 : * @param desc First DataContainer
182 : * @param handlerType Second DataContainer
183 : *
184 : * @return a pair of a DataContainer and eigen vector, of same size and the same values
185 : */
186 : template <typename data_t>
187 : std::tuple<DataContainer<data_t>, Vector_t<data_t>>
188 0 : generateRandomContainer(const DataDescriptor& desc, DataHandlerType handlerType)
189 : {
190 0 : auto containerSize = desc.getNumberOfCoefficients();
191 :
192 0 : auto randVec = generateRandomMatrix<data_t>(containerSize);
193 :
194 0 : auto dc = DataContainer<data_t>(desc, randVec, handlerType);
195 :
196 0 : return {dc, randVec};
197 0 : }
198 : /**
199 : * @brief Compares two DataContainers using their norm. Computes \f$ \sqrt{\| x - y \|_{2}^2}
200 : * \f$ and compares it to \f$ prec * \sqrt{min(\| x \|_{2}^2, \| y \|_{2}^2)} \f$. If the first
201 : * is smaller or equal to the second, we can assume the vectors are approximate equal
202 : *
203 : * @tparam data_t Value type of DataContainers
204 : * @param x First DataContainer
205 : * @param y Second DataContainer
206 : * @param prec Precision to compare, the smaller the closer both have to be
207 : * @return true if the norms of the containers is approximate equal
208 : */
209 : template <typename data_t>
210 : [[nodiscard]] bool isApprox(const DataContainer<data_t>& x, const DataContainer<data_t>& y,
211 : real_t prec = Eigen::NumTraits<real_t>::dummy_precision());
212 :
213 : template <typename data_t>
214 : [[nodiscard]] bool isApprox(const DataContainer<data_t>& x, const Vector_t<data_t>& y,
215 : real_t prec = Eigen::NumTraits<real_t>::dummy_precision());
216 :
217 : template <typename data_t>
218 : [[nodiscard]] bool isApprox(const DataHandler<data_t>& x, const DataHandler<data_t>& y,
219 : real_t prec = Eigen::NumTraits<real_t>::dummy_precision());
220 :
221 : template <typename data_t>
222 : [[nodiscard]] bool isApprox(const DataHandler<data_t>& x, const Vector_t<data_t>& y,
223 : real_t prec = Eigen::NumTraits<real_t>::dummy_precision());
224 :
225 : template <typename data_t, typename Source, typename = std::enable_if_t<isExpression<Source>>>
226 0 : [[nodiscard]] bool isApprox(const DataContainer<data_t>& x, const Source& y,
227 : real_t prec = Eigen::NumTraits<real_t>::dummy_precision())
228 : {
229 0 : return isApprox(x, DataContainer<data_t>{y}, prec);
230 : }
231 :
232 : template <typename data_t>
233 : [[nodiscard]] bool isCwiseApprox(const DataContainer<data_t>& x,
234 : const DataContainer<data_t>& y);
235 :
236 : template <typename data_t>
237 : [[nodiscard]] bool isCwiseApprox(const DataContainer<data_t>& x, const Vector_t<data_t>& y);
238 :
239 : template <typename data_t>
240 : [[nodiscard]] bool isCwiseApprox(const DataHandler<data_t>& x, const DataHandler<data_t>& y);
241 :
242 : template <typename data_t>
243 : [[nodiscard]] bool isCwiseApprox(const DataHandler<data_t>& x, const Vector_t<data_t>& y);
244 :
245 : /**
246 : * @brief Wrapper to remove const, volatile and reference of a type
247 : */
248 : template <typename T>
249 : using UnqualifiedType_t =
250 : typename std::remove_cv<typename std::remove_reference<T>::type>::type;
251 :
252 : /**
253 : * @brief Helper to give types a name, this is used to print information during testing
254 : *
255 : * @tparam T type that should be given a name
256 : * @tparam Dummy dummy, to be used to enable or disable specific specializations
257 : */
258 : template <typename T, typename Dummy = void>
259 : struct TypeName;
260 :
261 : /**
262 : * @brief specialization to specify a name for index_t
263 : * @tparam T [const] [volatile] index_t[&] should be accepted
264 : */
265 : template <typename T>
266 : struct TypeName<T, std::enable_if_t<std::is_same_v<index_t, UnqualifiedType_t<T>>>> {
267 : static constexpr char name[] = "index_t";
268 : };
269 :
270 : /**
271 : * @brief specialization to specify a name for float
272 : * @tparam T [const] [volatile] float[&] should be accepted
273 : */
274 : template <typename T>
275 : struct TypeName<T, std::enable_if_t<std::is_same_v<float, UnqualifiedType_t<T>>>> {
276 : static constexpr char name[] = "float";
277 : };
278 :
279 : /**
280 : * @brief specialization to specify a name for double
281 : * @tparam T [const] [volatile] double[&] should be accepted
282 : */
283 : template <typename T>
284 : struct TypeName<T, std::enable_if_t<std::is_same_v<double, UnqualifiedType_t<T>>>> {
285 : static constexpr char name[] = "double";
286 : };
287 :
288 : /**
289 : * @brief specialization to specify a name for complex<float>
290 : * @tparam T [const] [volatile] complex<float>[&] should be accepted
291 : */
292 : template <typename T>
293 : struct TypeName<T, std::enable_if_t<std::is_same_v<complex<float>, UnqualifiedType_t<T>>>> {
294 : static constexpr char name[] = "complex<float>";
295 : };
296 :
297 : /**
298 : * @brief specialization to specify a name for complex<double>
299 : * @tparam T [const] [volatile] complex<double>[&] should be accepted
300 : */
301 : template <typename T>
302 : struct TypeName<T, std::enable_if_t<std::is_same_v<complex<double>, UnqualifiedType_t<T>>>> {
303 : static constexpr char name[] = "complex<double>";
304 : };
305 :
306 : /**
307 : * @brief Quick access to TypeName<UnqualifiedType>::name
308 : * @tparam T a type
309 : */
310 : template <typename T>
311 : static constexpr auto TypeName_v = TypeName<UnqualifiedType_t<T>>::name;
312 : } // namespace elsa
|