1 
2 //
3 // This source file is part of appleseed.
4 // Visit https://appleseedhq.net/ for additional information and resources.
5 //
6 // This software is released under the MIT license.
7 //
8 // Copyright (c) 2017-2019 Francois Beaune, The appleseedhq Organization
9 //
10 // Permission is hereby granted, free of charge, to any person obtaining a copy
11 // of this software and associated documentation files (the "Software"), to deal
12 // in the Software without restriction, including without limitation the rights
13 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 // copies of the Software, and to permit persons to whom the Software is
15 // furnished to do so, subject to the following conditions:
16 //
17 // The above copyright notice and this permission notice shall be included in
18 // all copies or substantial portions of the Software.
19 //
20 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
26 // THE SOFTWARE.
27 //
28 
29 // appleseed.foundation headers.
30 #include "foundation/image/color.h"
31 #include "foundation/image/colormap.h"
32 #include "foundation/image/colormapdata.h"
33 #include "foundation/image/conversion.h"
34 #include "foundation/image/genericimagefilewriter.h"
35 #include "foundation/image/image.h"
36 #include "foundation/image/pixel.h"
37 #include "foundation/math/hash.h"
38 #include "foundation/math/rng/xoroshiro128plus.h"
39 #include "foundation/math/scalar.h"
40 #include "foundation/math/vector.h"
41 #include "foundation/platform/types.h"
42 #include "foundation/utility/gnuplotfile.h"
43 #include "foundation/utility/string.h"
44 #include "foundation/utility/test.h"
45 
46 // Standard headers.
47 #include <cassert>
48 #include <cstddef>
49 #include <vector>
50 
51 using namespace foundation;
52 using namespace std;
53 
TEST_SUITE(Foundation_Math_Hash)54 TEST_SUITE(Foundation_Math_Hash)
55 {
56     struct HistogramFixture
57     {
58         static constexpr size_t BinCount = 256;             // best when a power of two
59         static constexpr size_t SampleCount = 1024 * 1024;  // best when a multiple of BinCount
60 
61         size_t m_bins[BinCount];
62 
63         HistogramFixture()
64         {
65             for (size_t i = 0; i < BinCount; ++i)
66                 m_bins[i] = 0;
67         }
68 
69         void insert_value(const double x)
70         {
71             assert(x >= 0.0 && x < 1.0);
72 
73             const size_t i = truncate<size_t>(x * BinCount);
74             assert(i < BinCount);
75 
76             ++m_bins[i];
77         }
78 
79         //
80         // Compute a kind of normalized Pearson's chi-squared test.
81         //
82         // References:
83         //
84         //   https://en.wikipedia.org/wiki/Pearson%27s_chi-squared_test
85         //   https://en.wikipedia.org/wiki/Index_of_dispersion
86         //
87 
88         double compute_dispersion() const
89         {
90             constexpr double ExpectedCount = SampleCount / BinCount;
91 
92             double variance = 0.0;
93             for (size_t i = 0; i < BinCount; ++i)
94                 variance += square(m_bins[i] - ExpectedCount);
95             variance /= BinCount;
96 
97             return variance / ExpectedCount;
98         }
99 
100         void plot_histogram(const char* filename, const char* title) const
101         {
102             vector<Vector2d> points;
103             points.reserve(BinCount);
104 
105             for (size_t i = 0; i < BinCount; ++i)
106                 points.emplace_back(static_cast<double>(i), static_cast<double>(m_bins[i]));
107 
108             const double dispersion = compute_dispersion();
109 
110             GnuplotFile gnuplot_file;
111             gnuplot_file.set_title(format("{0} - Dispersion: {1}", title, dispersion));
112             gnuplot_file.set_xlabel("Bin");
113             gnuplot_file.set_ylabel("Number of values");
114             gnuplot_file.set_xrange(0.0, BinCount - 1);
115             gnuplot_file.new_plot().set_points(points);
116             gnuplot_file.write(format("unit tests/outputs/{0}", filename));
117         }
118     };
119 
120     TEST_CASE_F(NoHashing_Histogram_Sequential, HistogramFixture)
121     {
122         Xoroshiro128plus rng;
123 
124         for (size_t i = 0; i < SampleCount; ++i)
125         {
126             const double x = static_cast<double>(i) / SampleCount;
127             insert_value(x);
128         }
129 
130         plot_histogram(
131             "test_hash_01_nohashing_histogram_sequential.gnuplot",
132             "No Hashing - Sequential Numbers");
133     }
134 
135     TEST_CASE_F(NoHashing_Histogram_Random, HistogramFixture)
136     {
137         Xoroshiro128plus rng;
138 
139         for (size_t i = 0; i < SampleCount; ++i)
140         {
141             const double x = rng.rand_uint32() * Rcp2Pow32<double>();
142             insert_value(x);
143         }
144 
145         plot_histogram(
146             "test_hash_02_nohashing_histogram_random.gnuplot",
147             "No Hashing - Random Numbers");
148     }
149 
150     TEST_CASE_F(HashUInt32_Histogram_Sequential, HistogramFixture)
151     {
152         for (uint32 i = 0; i < SampleCount; ++i)
153         {
154             const double x = hash_uint32(i) * Rcp2Pow32<double>();
155             insert_value(x);
156         }
157 
158         plot_histogram(
159             "test_hash_03_hashuint32_histogram_sequential.gnuplot",
160             "foundation::hash_uint32() - Sequential Numbers");
161     }
162 
163     TEST_CASE_F(HashUInt32_Histogram_Random, HistogramFixture)
164     {
165         Xoroshiro128plus rng;
166 
167         for (size_t i = 0; i < SampleCount; ++i)
168         {
169             const double x = hash_uint32(rng.rand_uint32()) * Rcp2Pow32<double>();
170             insert_value(x);
171         }
172 
173         plot_histogram(
174             "test_hash_04_hashuint32_histogram_random.gnuplot",
175             "foundation::hash_uint32() - Random Numbers");
176     }
177 
178     TEST_CASE_F(HashUInt32Wang_Histogram_Sequential, HistogramFixture)
179     {
180         for (uint32 i = 0; i < SampleCount; ++i)
181         {
182             const double x = hash_uint32_wang(i) * Rcp2Pow32<double>();
183             insert_value(x);
184         }
185 
186         plot_histogram(
187             "test_hash_05_hashuint32wang_histogram_sequential.gnuplot",
188             "foundation::hash_uint32_wang() - Sequential Numbers");
189     }
190 
191     TEST_CASE_F(HashUInt32Wang_Histogram_Random, HistogramFixture)
192     {
193         Xoroshiro128plus rng;
194 
195         for (size_t i = 0; i < SampleCount; ++i)
196         {
197             const double x = hash_uint32_wang(rng.rand_uint32()) * Rcp2Pow32<double>();
198             insert_value(x);
199         }
200 
201         plot_histogram(
202             "test_hash_06_hashuint32wang_histogram_random.gnuplot",
203             "foundation::hash_uint32_wang() - Random Numbers");
204     }
205 
206     TEST_CASE_F(MixUInt32_Histogram, HistogramFixture)
207     {
208         for (uint32 i = 0; i < SampleCount / 10; ++i)
209         {
210             for (uint32 j = 0; j < 10; ++j)
211             {
212                 const double x = mix_uint32(i, j) * Rcp2Pow32<double>();
213                 insert_value(x);
214             }
215         }
216 
217         plot_histogram(
218             "test_hash_07_mixuint32_histogram.gnuplot",
219             "foundation::mix_uint32()");
220     }
221 
222     template <typename UInt> UInt rand(Xoroshiro128plus& rng);
223     template <> uint32 rand(Xoroshiro128plus& rng)
224     {
225         return rng.rand_uint32();
226     }
227     template <> uint64 rand(Xoroshiro128plus& rng)
228     {
229         uint64 x = rng.rand_uint32();
230         x <<= 32;
231         x |= rng.rand_uint32();
232         return x;
233     }
234 
235     struct AvalancheFixture
236     {
237         static const size_t Trials = 100000;
238 
239         template <typename UInt, typename Hash>
240         void plot_avalanche(
241             const char*     filename,
242             const size_t    trials,
243             Hash            hash)
244         {
245             constexpr size_t N = numeric_limits<UInt>::digits;
246 
247             // y = input bit, x = output bit
248             size_t counters[N * N];
249 
250             // Initialize counters to zero.
251             for (size_t i = 0; i < N * N; ++i)
252                 counters[i] = 0;
253 
254             Xoroshiro128plus rng;
255 
256             for (size_t trial = 0; trial < trials; ++trial)
257             {
258                 // Generate an initial value.
259                 const UInt initial_value = rand<UInt>(rng);
260 
261                 // Hash the initial value.
262                 const UInt initial_hash = hash(initial_value);
263 
264                 // Iterate over all bits of a word.
265                 for (size_t y = 0; y < N; ++y)
266                 {
267                     // Flip the i'th bit of the initial value.
268                     const UInt new_value = initial_value ^ (UInt(1) << y);
269 
270                     // Hash the new value.
271                     const UInt new_hash = hash(new_value);
272 
273                     // Compute the difference between the two hashes.
274                     const UInt hash_diff = initial_hash ^ new_hash;
275 
276                     // Update flipped bits counters.
277                     for (size_t x = 0; x < N; ++x)
278                         counters[y * N + x] += (hash_diff & (UInt(1) << x)) >> x;
279                 }
280             }
281 
282             // Construct a scaled, transposed image from the counters.
283             constexpr size_t Resolution = 512;
284             constexpr size_t Scaling = Resolution / N;
285             Image image(Resolution, Resolution, Resolution, Resolution, 3, PixelFormatFloat);
286             for (size_t y = 0; y < Resolution; ++y)
287             {
288                 for (size_t x = 0; x < Resolution; ++x)
289                 {
290                     const size_t counter = counters[(y / Scaling) * N + (x / Scaling)];
291                     image.set_pixel(y, x, Color3f(static_cast<float>(counter)));
292                 }
293             }
294 
295             // Construct a color map.
296             ColorMap color_map;
297             color_map.set_palette_from_array(InfernoColorMapLinearRGB, countof(InfernoColorMapLinearRGB) / 3);
298 
299             // Color map the image.
300             float min_value, max_value;
301             color_map.find_min_max_red_channel(image, min_value, max_value);
302             color_map.remap_red_channel(image, 0.0f, max_value);    // ignore min value
303 
304             // Convert the image to sRGB.
305             convert_linear_rgb_to_srgb(image);
306 
307             // Write the image to disk.
308             GenericImageFileWriter writer(format("unit tests/outputs/{0}", filename).c_str());
309             writer.append_image(&image);
310             writer.write();
311         }
312     };
313 
314     TEST_CASE_F(Identity_Avalanche, AvalancheFixture)
315     {
316         plot_avalanche<uint32>(
317             "test_hash_08_identity_avalanche.png",
318             Trials,
319             [](const uint32 value) { return value; });
320     }
321 
322     TEST_CASE_F(Addition_Avalanche, AvalancheFixture)
323     {
324         plot_avalanche<uint32>(
325             "test_hash_09_addition_avalanche.png",
326             Trials,
327             [](const uint32 value) { return value + 0xDEADBEEFul; });
328     }
329 
330     TEST_CASE_F(PrimeMultiplication_Avalanche, AvalancheFixture)
331     {
332         plot_avalanche<uint32>(
333             "test_hash_10_primemultiplication_avalanche.png",
334             Trials,
335             [](const uint32 value) { return value * 1536399377ul; });
336     }
337 
338     TEST_CASE_F(HashUInt32_Avalanche, AvalancheFixture)
339     {
340         plot_avalanche<uint32>(
341             "test_hash_11_hashuint32_avalanche.png",
342             Trials,
343             hash_uint32);
344     }
345 
346     TEST_CASE_F(HashUInt32Wang_Avalanche, AvalancheFixture)
347     {
348         plot_avalanche<uint32>(
349             "test_hash_12_hashuint32wang_avalanche.png",
350             Trials,
351             [](const uint32 value) { return hash_uint32_wang(value); });
352     }
353 
354     TEST_CASE_F(BobJenkinsHalf_Avalanche, AvalancheFixture)
355     {
356         plot_avalanche<uint32>(
357             "test_hash_13_bobjenkinshalf_avalanche.png",
358             Trials,
359             [](uint32 value)
360             {
361                 value = (value + 0x479ab41d) + (value << 8);
362                 value = (value ^ 0xe4aa10ce) ^ (value >> 5);
363                 value = (value + 0x9942f0a6) - (value << 14);
364                 value = (value ^ 0x5aedd67d) ^ (value >> 3);
365                 value = (value + 0x17bea992) + (value << 7);
366                 return value;
367             });
368     }
369 
370     TEST_CASE_F(BobJenkinsFull_Avalanche, AvalancheFixture)
371     {
372         plot_avalanche<uint32>(
373             "test_hash_14_bobjenkinsfull_avalanche.png",
374             Trials,
375             [](uint32 value)
376             {
377                 value = (value + 0x7ed55d16) + (value << 12);
378                 value = (value ^ 0xc761c23c) ^ (value >> 19);
379                 value = (value + 0x165667b1) + (value << 5);
380                 value = (value + 0xd3a2646c) ^ (value << 9);
381                 value = (value + 0xfd7046c5) + (value << 3);
382                 value = (value ^ 0xb55a4f09) ^ (value >> 16);
383                 return value;
384             });
385     }
386 
387     TEST_CASE_F(BobJenkinsFullNoBigConstants_Avalanche, AvalancheFixture)
388     {
389         plot_avalanche<uint32>(
390             "test_hash_15_bobjenkinsfullnobigconstants_avalanche.png",
391             Trials,
392             [](uint32 value)
393             {
394                 value -= value << 6;
395                 value ^= value >> 17;
396                 value -= value << 9;
397                 value ^= value << 4;
398                 value -= value << 3;
399                 value ^= value << 10;
400                 value ^= value >> 15;
401                 return value;
402             });
403     }
404 
405     TEST_CASE_F(HashUInt64ToUInt32_Avalanche, AvalancheFixture)
406     {
407         plot_avalanche<uint32>(
408             "test_hash_16_hashuint64touint32_avalanche.png",
409             Trials,
410             hash_uint64_to_uint32);
411     }
412 
413     TEST_CASE_F(MixUInt32_Avalanche, AvalancheFixture)
414     {
415         plot_avalanche<uint32>(
416             "test_hash_17_mixuint32_avalanche.png",
417             Trials,
418             [](const uint32 value) { return mix_uint32(value, 0xDEADBEEFul); });
419     }
420 
421     static uint32 boost_hash_combine(const uint32 h1, const uint32 h2)
422     {
423         // See foundation::combine_hashes() in foundation/math/hash.h for the constant derivation.
424         return h1 ^ (h2 + 0x9E3779B9ul + (h1 << 6) + (h1 >> 2));
425     }
426 
427     TEST_CASE_F(BoostHashCombine_Avalanche_FirstValue, AvalancheFixture)
428     {
429         plot_avalanche<uint32>(
430             "test_hash_18_boosthashcombine_firstvalue_avalanche.png",
431             Trials,
432             [](const uint32 value)
433             {
434                 const uint32 h1 = hash_uint32(value);
435                 const uint32 h2 = 0xDEADBEEFul;
436                 return boost_hash_combine(h1, h2);
437             });
438     }
439 
440     TEST_CASE_F(BoostHashCombine_Avalanche_SecondValue, AvalancheFixture)
441     {
442         plot_avalanche<uint32>(
443             "test_hash_19_boosthashcombine_secondvalue_avalanche.png",
444             Trials,
445             [](const uint32 value)
446             {
447                 const uint32 h1 = 0xDEADBEEFul;
448                 const uint32 h2 = hash_uint32(value);
449                 return boost_hash_combine(h1, h2);
450             });
451     }
452 
453     TEST_CASE_F(CombineHashes_32_Avalanche_FirstValue, AvalancheFixture)
454     {
455         plot_avalanche<uint32>(
456             "test_hash_20_combinehashes_32_firstvalue_avalanche.png",
457             Trials,
458             [](const uint32 value)
459             {
460                 const uint32 h1 = hash_uint32(value);
461                 const uint32 h2 = 0xDEADBEEFul;
462                 return combine_hashes(h1, h2);
463             });
464     }
465 
466     TEST_CASE_F(CombineHashes_32_Avalanche_SecondValue, AvalancheFixture)
467     {
468         plot_avalanche<uint32>(
469             "test_hash_21_combinehashes_32_secondvalue_avalanche.png",
470             Trials,
471             [](const uint32 value)
472             {
473                 const uint32 h1 = 0xDEADBEEFul;
474                 const uint32 h2 = hash_uint32(value);
475                 return combine_hashes(h1, h2);
476             });
477     }
478 
479     TEST_CASE_F(CombineHashes_64_Avalanche_FirstValue, AvalancheFixture)
480     {
481         plot_avalanche<uint64>(
482             "test_hash_22_combinehashes_64_firstvalue_avalanche.png",
483             Trials,
484             [](const uint64 value)
485             {
486                 const uint64 h1 = hash_uint64(value);
487                 const uint64 h2 = 0xDEADBEEFCAFEBABEul;
488                 return combine_hashes(h1, h2);
489             });
490     }
491 
492     TEST_CASE_F(CombineHashes_64_Avalanche_SecondValue, AvalancheFixture)
493     {
494         plot_avalanche<uint64>(
495             "test_hash_23_combinehashes_64_secondvalue_avalanche.png",
496             Trials,
497             [](const uint64 value)
498             {
499                 const uint64 h1 = 0xDEADBEEFCAFEBABEul;
500                 const uint64 h2 = hash_uint64(value);
501                 return combine_hashes(h1, h2);
502             });
503     }
504 }
505