Lumiera  0.pre.03
»edit your freedom«
rational-test.cpp
Go to the documentation of this file.
1 /*
2  Rational(Test) - verify support for rational arithmetics
3 
4  Copyright (C) Lumiera.org
5  2022, Hermann Vosseler <Ichthyostega@web.de>
6 
7  This program is free software; you can redistribute it and/or
8  modify it under the terms of the GNU General Public License as
9  published by the Free Software Foundation; either version 2 of
10  the License, or (at your option) any later version.
11 
12  This program is distributed in the hope that it will be useful,
13  but WITHOUT ANY WARRANTY; without even the implied warranty of
14  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15  GNU General Public License for more details.
16 
17  You should have received a copy of the GNU General Public License
18  along with this program; if not, write to the Free Software
19  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20 
21 * *****************************************************/
22 
29 #include "lib/error.hpp"
30 #include "lib/test/run.hpp"
31 #include "lib/integral.hpp"
32 #include "lib/format-cout.hpp"
33 
34 #include "lib/rational.hpp"
35 
36 #include <chrono>
37 #include <array>
38 
39 using std::array;
40 
41 
42 namespace util {
43 namespace test {
44 
45 
46  /***************************************************************************/
54  class Rational_test : public Test
55  {
56 
57  virtual void
58  run (Arg)
59  {
62  verify_limits();
64  }
65 
66 
78  void
80  {
81  CHECK (Rat(10,3) == 10_r/3); // user-defined literal to construct a fraction
82  CHECK (Rat(10,3) == boost::rational<int64_t>(10,3)); // using Rat = boost::rational<int64_t>
83  CHECK (rational_cast<float> (10_r/3) == 3.3333333f); // rational_cast calculates division after type conversion
84 
85  CHECK (2_r/3 + 3_r/4 == 17_r/12);
86  CHECK (2_r/3 * 3_r/4 == 1_r/2);
87  CHECK (2_r/3 /(3_r/4)== 8_r/9);
88  CHECK (2_r/3 / 3 /4 == 1_r/18); // usual precedence and brace rules apply, yielding 2/36 here
89 
90  CHECK (util::toString(23_r/55) == "23/55sec");
91  CHECK (util::toString(24_r/56) == "3/7sec" ); // rational numbers are normalised and reduced immediately
93 
94  CHECK (Rat(10,3).numerator() == int64_t(10));
95  CHECK (Rat(10,3).denominator() == int64_t(3));
96  CHECK (boost::rational<uint>(10,3).numerator() == uint(10));
97  CHECK (boost::rational<uint>(10,3).denominator() == uint(3));
98  CHECK (boost::rational<uint>(10,3) == rational_cast<boost::rational<uint>> (Rat(10,3)));
99  CHECK (boost::rational<uint>(11,3) != rational_cast<boost::rational<uint>> (Rat(10,3)));
100  }
101 
102 
109  void
111  {
112  const Rat MAXI = Rat{std::numeric_limits<int64_t>::max()};
113  const Rat MINI = Rat{1, std::numeric_limits<int64_t>::max()};
114 
115  CHECK (rational_cast<int64_t>(MAXI) == std::numeric_limits<int64_t>::max());
116  CHECK (rational_cast<double> (MAXI) == 9.2233720368547758e+18);
117 
118  CHECK (MAXI > 0); // so this one still works
119  CHECK (MAXI+1 < 0); // but one more and we get a wrap-around
120  CHECK (MAXI+1 < -MAXI);
121  CHECK (util::toString(MAXI) == "9223372036854775807sec");
122  CHECK (util::toString(MAXI+1) == "-9223372036854775808sec");
123  CHECK (util::toString(-MAXI) == "-9223372036854775807sec");
124 
125  CHECK (MINI > 0); // smallest representable number above zero
126  CHECK (1-MINI < 1);
127  CHECK (0 < 1-MINI); // can be used below 1 just fine
128  CHECK (0 > 1+MINI); // but above we get a wrap-around in normalised numerator
129  CHECK (util::toString(MINI) == "1/9223372036854775807sec");
130  CHECK (util::toString(-MINI) == "-1/9223372036854775807sec");
131  CHECK (util::toString(1-MINI) == "9223372036854775806/9223372036854775807sec");
132  CHECK (util::toString(1+MINI) == "-9223372036854775808/9223372036854775807sec");
133 
134  CHECK ((MAXI-1)/MAXI == 1-MINI);
135  CHECK (MAXI/(MAXI-1) > 1); // as workaround we have to use a slightly larger ULP
136  CHECK (MAXI/(MAXI-1) - 1 > MINI); // ...this slightly larger one works without wrap-around
137  CHECK (1 - MAXI/(MAXI-1) < -MINI);
138  CHECK (util::toString(MAXI/(MAXI-1)) == "9223372036854775807/9223372036854775806sec");
139  CHECK (util::toString(MAXI/(MAXI-1) - 1) == "1/9223372036854775806sec");
140  CHECK (util::toString(1 - MAXI/(MAXI-1)) == "-1/9223372036854775806sec");
141 
142  // Now entering absolute danger territory....
143  const Rat MIMI = -MAXI-1; // this is the most extreme negative representable value
144  CHECK (MIMI < 0);
145  CHECK (util::toString(MIMI) == "-9223372036854775808sec");
146  CHECK (util::toString(1/MIMI) == "-1/-9223372036854775808sec");
147  try
148  {
149  -1-1/MIMI; // ...but it can't be used for any calculation without blowing up
150  NOTREACHED("expected boost::rational to flounder");
151  }
152  catch (std::exception& tilt)
153  {
154  CHECK (tilt.what() == string{"bad rational: non-zero singular denominator"});
155  }
156 
157 
158  // yet seemingly harmless values can be poisonous...
159  Rat poison = MAXI/49 / (MAXI/49-1);
160  Rat decoy = poison + 5;
161  CHECK (poison > 0);
162  CHECK (decoy > 6);
163  CHECK (rational_cast<double>(decoy) == 6); // looks innocuous...
164  CHECK (rational_cast<double>(decoy+5) == 11); // ...aaaand...
165  CHECK (rational_cast<double>(decoy+50) == -42); // ..ultimate answer..
166  CHECK (rational_cast<double>(decoy+500) == 15.999999999999996); // .dead in the water.
167 
168  // Heuristics to detect danger zone
169  CHECK ( can_represent_Sum(decoy,5));
170  CHECK (not can_represent_Sum(decoy,50));
171 
172  // alarm is given a bit too early
173  CHECK ( can_represent_Sum(decoy,15)); // ...because the check is based on bit positions
174  CHECK (not can_represent_Sum(decoy,16)); // ...and here the highest bit of one partner moved into danger zone
175  CHECK (decoy+16 > 0);
176  CHECK (decoy+43 > 0);
177  CHECK (decoy+44 < 0);
178 
179  // similar when increasing the denominator...
180  CHECK (poison + 1_r/10 > 0);
181  CHECK (poison + 1_r/90 > 0);
182  CHECK (poison + 1_r/98 < 0); // actually the flip already occurs at 1/91 but also causes an assertion failure
183  CHECK ( can_represent_Sum(poison,1_r/10));
184  CHECK ( can_represent_Sum(poison,1_r/15));
185  CHECK (not can_represent_Sum(poison,1_r/16));
186  CHECK (not can_represent_Sum(poison,1_r/91));
187  CHECK (not can_represent_Sum(poison,1_r/100));
188  }
189 
190 
198  void
200  {
201  CHECK ( 5 == ilog2( int64_t(0b101010)));
202  CHECK ( 5 == ilog2(uint64_t(0b101010)));
203  CHECK ( 5 == ilog2( int32_t(0b101010)));
204  CHECK ( 5 == ilog2(uint32_t(0b101010)));
205  CHECK ( 5 == ilog2( int16_t(0b101010)));
206  CHECK ( 5 == ilog2(uint16_t(0b101010)));
207  CHECK ( 5 == ilog2( int8_t(0b101010)));
208  CHECK ( 5 == ilog2( uint8_t(0b101010)));
209  CHECK ( 5 == ilog2( int (0b101010)));
210  CHECK ( 5 == ilog2( uint (0b101010)));
211  CHECK ( 5 == ilog2( char (0b101010)));
212  CHECK ( 5 == ilog2( uchar (0b101010)));
213  CHECK ( 5 == ilog2( long (0b101010)));
214  CHECK ( 5 == ilog2( ulong (0b101010)));
215  CHECK ( 5 == ilog2( short (0b101010)));
216  CHECK ( 5 == ilog2( ushort (0b101010)));
217 
218  CHECK (63 == ilog2(std::numeric_limits<uint64_t>::max()));
219  CHECK (62 == ilog2(std::numeric_limits< int64_t>::max()));
220  CHECK (31 == ilog2(std::numeric_limits<uint32_t>::max()));
221  CHECK (30 == ilog2(std::numeric_limits< int32_t>::max()));
222  CHECK (15 == ilog2(std::numeric_limits<uint16_t>::max()));
223  CHECK (14 == ilog2(std::numeric_limits< int16_t>::max()));
224  CHECK ( 7 == ilog2(std::numeric_limits< uint8_t>::max()));
225  CHECK ( 6 == ilog2(std::numeric_limits< int8_t>::max()));
226 
227  CHECK ( 5 == ilog2(0b111111));
228  CHECK ( 5 == ilog2(0b101110));
229  CHECK ( 5 == ilog2(0b100100));
230  CHECK ( 5 == ilog2(0b100000));
231 
232  CHECK ( 2 == ilog2(4));
233  CHECK ( 1 == ilog2(2));
234  CHECK ( 0 == ilog2(1));
235  CHECK (-1 == ilog2(0));
236  CHECK (-1 == ilog2(-1));
237 
238  CHECK (-1 == ilog2(std::numeric_limits<uint64_t>::min()));
239  CHECK (-1 == ilog2(std::numeric_limits< int64_t>::min()));
240  CHECK (-1 == ilog2(std::numeric_limits<uint32_t>::min()));
241  CHECK (-1 == ilog2(std::numeric_limits< int32_t>::min()));
242  CHECK (-1 == ilog2(std::numeric_limits<uint16_t>::min()));
243  CHECK (-1 == ilog2(std::numeric_limits< int16_t>::min()));
244  CHECK (-1 == ilog2(std::numeric_limits< uint8_t>::min()));
245  CHECK (-1 == ilog2(std::numeric_limits< int8_t>::min()));
246 
247 
248  /* ==== compare with naive implementation ==== */
249 
250  auto floatLog = [](auto n)
251  {
252  return n <=0? -1 : ilogb(n);
253  };
254  auto bitshift = [](auto n)
255  {
256  if (n <= 0) return -1;
257  int logB = 0;
258  while (n >>= 1)
259  ++logB;
260  return logB;
261  };
262  auto do_nothing = [](auto n){ return n; };
263 
264  array<uint64_t, 1000> numz;
265  for (auto& n : numz)
266  {
267  n = rand() * uint64_t(1 << 31);
268  CHECK (ilog2(n) == floatLog(n));
269  CHECK (ilog2(n) == bitshift(n));
270  }
271 
272  int64_t dump{0}; // throw-away result to prevent optimisation
273  auto microbenchmark = [&numz,&dump](auto algo)
274  {
275  using std::chrono::system_clock;
276  using Dur = std::chrono::duration<double>;
277  const double SCALE = 1e9; // Results are in ns
278 
279  auto start = system_clock::now();
280  for (uint i=0; i<1000; ++i)
281  for (auto const& n : numz)
282  dump += algo(n);
283  Dur duration = system_clock::now () - start;
284  return duration.count()/(1000*1000) * SCALE;
285  };
286 
287 
288 
289  auto time_ilog2 = microbenchmark(ilog2<int64_t>);
290  auto time_float = microbenchmark(floatLog);
291  auto time_shift = microbenchmark(bitshift);
292  auto time_ident = microbenchmark(do_nothing);
293 
294  cout << "Microbenchmark integer-log2" <<endl
295  << "util::ilog2 :"<<time_ilog2<<"ns"<<endl
296  << "std::ilogb :"<<time_float<<"ns"<<endl
297  << "bit-shift :"<<time_shift<<"ns"<<endl
298  << "identity :"<<time_ident<<"ns"<<endl
299  << "(checksum="<<dump<<")" <<endl; // Warning: without outputting `dump`,
300  // the optimiser would eliminate most calls
301  // the following holds both with -O0 and -O3
302  CHECK (time_ilog2 < time_shift);
303  CHECK (time_ident < time_ilog2);
304 
305  /**** some numbers ****
306  *
307  * GCC-8, -O3, Debian-Buster, AMD FX83
308  *
309  * with uint64_t...
310  * - ilog2 : 5.6ns
311  * - ilogb : 5.0ns
312  * - shift : 44ns
313  * - ident : 0.6ns
314  *
315  * with uint8_t
316  * - ilog2 : 5.2ns
317  * - ilogb : 5.8ns
318  * - shift : 8.2ns
319  * - ident : 0.3ns
320  */
321  }
322 
323 
330  void
332  {
333  const int64_t MAX{std::numeric_limits<int64_t>::max()};
334  const Rat MAXI = Rat{MAX};
335 
336  Rat poison = (MAXI-88)/(MAXI/7);
337 
338  auto approx = [](Rat rat){ return rational_cast<float> (rat); };
339  CHECK (poison > 0);
340  CHECK (poison+1 < 0); // wrap around!
341  CHECK (approx (poison ) == 6.99999952f); // wildly wrong results...
342  CHECK (approx (poison+1) == -6);
343  CHECK (approx (poison+7) == -6.83047369e-17f);
344  CHECK (approx (poison+9_r/5) == 0.400000006f);
345 
346  Rat sleazy = reQuant (poison, 1 << 24); // recast into multiples of an arbitrary other divisor,
347  CHECK (sleazy.denominator() == 1 << 24); // (here using a power of two as example)
348  // and now we can do all the slick stuff...
349  CHECK (sleazy > 0);
350  CHECK (sleazy+1 > 0);
351  CHECK (sleazy+7 > 0);
352  CHECK (approx (sleazy) == 7);
353  CHECK (approx (sleazy+1) == 8);
354  CHECK (approx (sleazy+7) == 14);
355  CHECK (approx (sleazy+9_r/5) == 8.80000019f);
356 
357  CHECK (util::toString (poison) == "9223372036854775719/1317624576693539401sec");
358  CHECK (util::toString (poison+1) =="-7905747460161236496/1317624576693539401sec");
359  CHECK (util::toString (sleazy) == "117440511/16777216sec");
360  CHECK (util::toString (sleazy+1) == "134217727/16777216sec");
361 
362  // also works towards larger denominator, or with negative numbers...
363  CHECK (reQuant (1/poison, MAX) == 1317624576693539413_r/9223372036854775807);
364  CHECK (reQuant (-poison, 7777) == -54438_r/ 7777);
365  CHECK (reQuant (poison, -7777) == -54438_r/-7777);
366 
367  CHECK (approx ( 1/poison ) == 0.142857149f);
368  CHECK (approx (reQuant (1/poison, MAX)) == 0.142857149f);
369  CHECK (approx (reQuant (poison, 7777)) == 6.99987125f);
370  }
371  };
372 
373  LAUNCHER (Rational_test, "unit common");
374 
375 
376 }} // namespace util::test
Automatically use custom string conversion in C++ stream output.
int64_t reQuant(int64_t num, int64_t den, int64_t u)
Re-Quantise a number into a new grid, truncating to the next lower grid point.
Definition: rational.hpp:129
Rational number support, based on boost::rational.
Definition: run.hpp:49
#define MAX(A, B)
the inevitable MAX macro, sometimes still necessary in template code
Definition: util.hpp:496
Simple test class runner.
std::string toString(TY const &val) noexcept
get some string representation of any object, reliably.
Definition: format-obj.hpp:200
Lumiera error handling (C++ interface).
constexpr int ilog2(I num)
Integral binary logarithm (disregarding fractional part)
Definition: util-quant.hpp:180