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