fractal_analysis/
lib.rs

1//! Welcome to the `fractal-analysis` crate! Here you will find necessary tools
2//! for the analysis of fractal sets, based on the “Z-box merging” algorithm.
3//!
4//! # How to use
5//! I have tried to create ergonomic functions for the following data-sets:
6//! * [Images, colour or gray-scale](mod@img_funs)
7//! * [Sounds](mod@snd_funs) (to be added later)
8//! * [Electro-encephalograms](mod@eeg_19c_funs) (to be added later)
9//! * [MRI images](mod@mri_funs) (to be added later)
10//!
11//! For each of those, you will find ready-made functions that can give you
12//! useful results; just eg say `example_code_here` and you will get the result.
13//! 
14//! # Caveat
15//! Currently (March 2021) the documentation and testing are still unfinished.
16//! They are both provided on a best-effort basis; the user's understanding is
17//! appreciated.
18//!
19//! # How to extend to other uses
20//! Oh, how I _wish_ there was a way for me to design an API that said “Oh, just
21//! do `zbox_merge(input)` for whatever input and it'll work!”.
22//!
23//! Sadly, it is no-where nearly as simple as that. The
24//! basic library will get you a long way
25//! along, but there is serious work you'll need to do before-hand. The simplest
26//! way would be to just
27//! [send the crate maintainer an e-mail](mailto:velona@ahiru.eu)
28//! and ask directly, but in case s/he is unreachable, here is what you'll
29//! need to do:
30//!
31//! ### Choose your dependent and independent dimensions
32//! First, decide what counts as an “separate dimension” for your use-case.
33//! Is each channel of your EEG a separate dimension that depends on the
34//! time-stamp, or are they a sampling
35//! of a two-dimensional scalp? What about the colour channels of your image?
36//! That will give you a first idea of what to do, and denote the limits of the
37//! result you will end up with.
38//!
39//! Of particular note: You will want the number of dimensions to be independent
40//! of the measurement method. For instance: For the colour channels, we chose
41//! each to be a separate dimension in part because all colour images have those
42//! exact same 3 channels. But for the EEGs we chose to consider them as a 2-D
43//! grid, or else the result would depend on the number of electrodes and
44//! measurements between different setups would have no immediate way to be
45//! compared. As a result, about half the mental effort was spent in deciding
46//! exactly how to subdivide the electrode placement into quadrants!
47//!
48//! ### Count the number of bits you have available
49//! For each coordinate, count the amount of values it can take. Take the
50//! smallest of those amounts, find its base-2 logarithm,
51//! and throw away all the decimal digits. The result
52//! is the number of bits to which each of your coordinates will need to be
53//! normalised.
54//!
55//! ### Choose your data-types
56//! This is only important if you _really_ care about efficiency. The default
57//! choices ought to be optimal for most cases, but under special circumstances
58//! they _might_ not be.
59//!
60//! **Example 1:** If you have 5 keys of 12 bits each, each `Coor` will be an
61//! `u16`, and as a result the default `Key` will be an `u128`. But they can
62//! actually fit within an `u64`; the compiler just doesn't know that. In that
63//! case, you might prefer to explicitly cast all keys to `u64`s.
64//!
65//! **Example 2:** If you need 48 bits for your key, and you're running the
66//! code on a 16-bit architecture CPU, you might prefer to implement
67//! custom-made 48 bit data-types (3*16) instead of letting the code use `u64`s
68//! by default. The [`lindel`](lindel) crate permits that.
69//!  
70//! ### Create a “key-extraction” function
71//! The key-extraction is comprised of two parts: Normalisation and
72//! linearisation.
73//!
74//! The normalisation is already provided for you, and only
75//! requires you to provide, for each co-ordinate, the min/max values and the
76//! amount of bits to which they will be normalised. However, please bear in
77//! mind that you will need to extract the independent coordinates together
78//! with the dependent ones!
79//!
80//! As for the linearisation, two separate methods are already provided, thanks
81//! to the [`lindel`](lindel) crate. Those are Morton encoding (“z-index”) and Hilbert
82//! encoding (“Hilbert index”). If you care about speed and/or have
83//! at least one independent coordinate, the z-index will serve you better.
84//! If you have no independent coordinates, and you can afford the small
85//! performance hit, a Hilbert index will easily allow you to examine almost
86//! every possible subdivision of your data-set independently. Still, please
87//! note the following:
88//! 1. You will need to do that manually, as the program can't perform this
89//! operation automatically.
90//! 2. You could theoretically use a Hilbert index everywhere, but in
91//! subdividing it you might end up omitting parts of the independent
92//! coordinates whose values are too large.
93//! 3. While a z-index is much quicker to compute than a Hilbert index, for
94//! large data-sets the most expensive operation is the sorting, so perhaps the
95//! difference won't be important for the `O(N)` part of the program.
96//!
97//! ### Choose a sorting algorithm
98//! If you don't have a great concern for speed, you could choose the default
99//! methods defined in `std` or `rayon`. If you need to squeeze every last bit
100//! of performance, on the other hand, you might prefer to implement a radix
101//! sort or something to that extent.
102//!
103//! ### And you're finished!
104//! …finished with the preliminary work, that is. Now all that's left 
105//! is to write the actual code: extract the keys, sort, extract the
106//! amounts of common leading bits between each consecutive pair,
107//! get the results from them, drain the useless bits, and use a
108//! regression function to get the result. Here is some example code,
109//! copied from the `img_funs` module:
110//! ```should_not_compile
111//!let width = input.width() as usize;
112//!let height = input.height() as usize;
113//!
114//!let get_key_from_sample = |(x, y, rgb): (u32, u32, &image::Rgb<u8>)| -> u64 {
115//!    let norm_x = create_normaliser::<u32, u8>(0, input.width()).unwrap();
116//!    // The provided normaliser simplifies things slightly.
117//!    let norm_y = create_normaliser::<u32, u8>(0, input.height()).unwrap();
118//!    let arr = [norm_x(x), norm_y(y), rgb[0], rgb[1], rgb[2]];
119//!    // Extract both independent and dependent coordinates
120//!    morton_encoding::morton_encode(arr)
121//!    // Linearise using `morton_encoding`
122//!};
123//!
124//!let clzs = get_clzs(input.enumerate_pixels(), get_key_from_sample);
125//!let (tmp, lacun) = get_results_from_clzs(clzs);
126//!let tmp = drain_useless_bits::<_, 64, 40>(tmp);
127//!let lacun = drain_useless_bits::<_, 64, 40>(lacun);
128//!finalise_results(tmp, lacun, width*height, 8)
129//! ```
130//!
131//! Unless of course you need more than 256 bits for each key, in which case
132//! you'll need to change the rest of the functions so that they operate on
133//! `u16`s instead.
134//!
135//! # Interpreting the results
136//! ## Fractal dimension
137//! _The first_ and most important _thing to understand_ with regards to the
138//! fractal dimension is the limits inside which its
139//! log-log plot will fall. Those create a quadrilateral, whose four edges
140//! are defined as follows:
141//! 1. Edge 1 is vertical, for `x` equal to the amount of bits you have
142//! available. You can't subdivide your data-set more then the resolution that
143//! each coordinate has.
144//! 2. Edge 2 is horizontal, for `y` equal to the amount of samples you have
145//! available. Subdivide all you might, you will never get more populated
146//! boxes than the amount of samples (eg pixels) that exist in the data-set.
147//! 3. Edge 3 is diagonal, and has a slope equal to the amount of independent
148//! coordinates, if any.
149//! 4. Edge 4 is diagonal as well; its slope is equal to the total amount
150//! of coordinates available, independent as well as dependent. A data-set
151//! constrained within `N` dimensions can never have a fractal dimension
152//! above `N`.
153//!
154//! The most immediate result of these four constraints is that, especially if
155//! the log-log plot rises abruptly at first, it will encounter a plateau as
156//! soon as it reaches the total amount of samples, past which it will be
157//! horizontal. As such, the horizontal part of the plot _must_ be excised
158//! before calculating the fractal dimension, else it will be artificially low.
159//!
160//! _The second thing to understand_ is that, although an ideally fractal
161//! shape's log-log plot will be linear, in practice data-sets will be
162//! scale-dependent, leading to non-linearities in the log-log plot. In every
163//! such instance we've found, the log-log plot is concave downward. Therefore,
164//! the user has two choices:
165//! 1. To take the simple linear regression of the log-log plot, and interpret
166//! its slope as the fractal dimension. The scale-dependence, if any, may be
167//! found from the mean square error between the line and the actual plot.
168//! 2. To take a parabolic regression of the log-log plot, interpret the linear
169//! parametre as the fractal dimension, and the second-order parametre as the
170//! scale-dependence.
171//!
172//! Neither has been tried with any real rigour; the user is cordially invited
173//! to share the results, if any.
174//!
175//! ## Lacunarity
176//! The way the lacunarity was defined in the available literature, it appears
177//! to be a measure that's different for each scale. It's measured here for the
178//! user's convenience, but the interpretation of the results must necessarily
179//! be left up to the user.
180//!
181//!  
182
183#![cfg_attr(not(feature = "std"), no_std)]
184
185use core::ops::{Add, BitXor, Div, Sub};
186use num_traits::{CheckedShl, CheckedSub, PrimInt, Zero};
187use arrayvec::ArrayVec;
188
189
190pub mod img_funs;
191//pub mod snd_funs;
192//pub mod eeg_19c_funs;
193
194pub use img_funs::*;
195//pub use snd_funs::*;
196//pub use eeg_19c_funs::*;
197
198#[cfg(feature = "time_things")]
199macro_rules! time {
200    ($x: expr) => {{
201        eprintln!("Measuring expression: {}", stringify!($x));
202        let begin = std::time::Instant::now();
203        let result = $x;
204        eprintln!("Time elapsed: {:#?}\n", begin.elapsed());
205        result
206    }};
207}
208
209#[cfg(not(feature = "time_things"))]
210macro_rules! time {
211    ($x: expr) => {{
212        $x
213    }};
214}
215
216/// A convenience function that takes an arbitrary value, along with its minimum and maximum value, and normalises it to the limits of the coordinate data-type it's given.
217/// ```rust
218/// # use fractal_analysis::normalise;
219/// let x = 1024u32;
220/// let norm_x: u8 = normalise(x, 0, 65536);
221/// assert_eq!(norm_x, 4);
222/// ```
223/// Please bear in mind:
224/// 1. This function assumes that the minimum will be inclusive and the maximum exclusive.
225/// 2. This function will silently return a zero if `maximum * (Coordinate::MAX + 1)` overflows, or if the value provided is outside `minimum..maximum`.
226pub fn normalise<Input, Coordinate>(x: Input, minimum: Input, maximum: Input) -> Coordinate
227where
228    Input: Add<Output = Input>
229        + Sub<Output = Input>
230        + Div<Output = Input>
231        + core::convert::TryInto<Coordinate>
232        + CheckedShl
233        + CheckedSub
234        + Copy,
235    Coordinate: Zero,
236{
237    let spread: Input = maximum - minimum;
238    let coor_bits = core::mem::size_of::<Coordinate>() * 8;
239    x.checked_sub(&minimum)
240        .and_then(|x| x.checked_shl(coor_bits as u32))
241        .map(|x| x / spread)
242        .and_then(|x| x.try_into().ok())
243        .unwrap_or(Coordinate::zero())
244}
245
246/// A convenience function that takes a minimum and maximum value, inclusive and exclusive respectively, and returns a function to normalise values to the limits of the coordinate data-type it's given.
247/// ```rust
248/// # use fractal_analysis::create_normaliser;
249/// let norm_fn = create_normaliser::<u32, u8>(0u32, 8192).unwrap();
250/// assert_eq!(norm_fn(31), 0);
251/// assert_eq!(norm_fn(32), 1);
252/// assert_eq!(norm_fn(250), 7);
253/// ```
254/// Please note:
255/// 1. This function returns `None` if `maximum * (Coordinate::MAX + 1)` overflows.
256/// 2. The output function will return a zero if given a value that's outside bounds.
257pub fn create_normaliser<Input, Coordinate>(
258    minimum: Input,
259    maximum: Input,
260) -> Option<impl Fn(Input) -> Coordinate>
261where
262    Input: Add<Output = Input>
263        + Sub<Output = Input>
264        + Div<Output = Input>
265        + core::convert::TryInto<Coordinate>
266        + CheckedShl
267        + CheckedSub
268        + Copy,
269    Coordinate: Zero,
270{
271    let coor_bits = core::mem::size_of::<Coordinate>() * 8;
272    let _ = maximum.checked_shl(coor_bits as u32)?;
273    Some(move |x| normalise(x, minimum, maximum))
274}
275
276pub fn map_sampler<'a, H: 'a, Smp, Key: 'a, Set>(
277    set: Set,
278    get_key_from_sample: H,
279) -> impl Iterator<Item = Key> + 'a
280where
281    H: Fn(Smp) -> Key,
282    Key: PrimInt,
283    Set: IntoIterator<Item = Smp> + 'a,
284{
285    set.into_iter().map(get_key_from_sample)
286}
287
288#[cfg(feature = "std")]
289pub fn iterate_sorted_pairs<Key: Ord + Copy>(
290    input: impl Iterator<Item = Key>,
291) -> impl Iterator<Item = (Key, Key)> {
292    let mut imp = input.collect::<Vec<_>>();
293    imp.sort_unstable();
294    (0..imp.len().saturating_sub(1)).map(move |i| (imp[i], imp[i + 1]))
295}
296
297#[cfg(not(feature = "std"))]
298pub fn iterate_sorted_pairs<'a, Key: Ord + Copy>(
299    input: impl Iterator<Item = Key>,
300    buffer: &'a mut [Key],
301) -> impl Iterator<Item = (Key, Key)> + 'a 
302{
303    buffer.iter_mut()
304        .zip(input)
305        .for_each(|(a, b)| *a = b);
306    buffer.sort_unstable();
307    (0..buffer.len().saturating_sub(1)).map(move |i| (buffer[i], buffer[i + 1]))
308}
309
310// TODO: Do this as a method to allow chaining
311
312#[cfg(feature = "std")]
313pub fn get_clzs<'a, H: 'a, Smp: 'a, Key: 'a, Set: 'a>(
314    set: Set,
315    get_key_from_sample: H,
316) -> impl Iterator<Item = u8> + 'a
317where
318    H: Fn(Smp) -> Key,
319    Key: PrimInt + BitXor<Output = Key>,
320    Set: IntoIterator<Item = Smp>,
321{
322    let keys = time!(map_sampler(set, get_key_from_sample));
323    let sorted_pairs_of_keys = time!(iterate_sorted_pairs(keys));
324    time!(sorted_pairs_of_keys
325        .map(|(a, b)| a ^ b)
326        .map(|x| x.leading_zeros() as u8))
327}
328
329#[cfg(not(feature = "std"))]
330pub fn get_clzs<'a, H: 'a, Smp: 'a, Key: 'a, Set: 'a>(
331    set: Set,
332    get_key_from_sample: H,
333    buffer: &'a mut [Key]
334) -> impl Iterator<Item = u8> + 'a
335where
336    H: Fn(Smp) -> Key,
337    Key: PrimInt + BitXor<Output = Key>,
338    Set: IntoIterator<Item = Smp>,
339{
340    let keys = time!(map_sampler(set, get_key_from_sample));
341    let sorted_pairs_of_keys = time!(iterate_sorted_pairs(keys, buffer));
342    time!(sorted_pairs_of_keys
343        .map(|(a, b)| a ^ b)
344        .map(|x| x.leading_zeros() as u8))
345}
346
347pub fn get_results_from_clzs<const KEY_BIT_AMT: usize>(
348    input: impl Iterator<Item = u8>,
349) -> (ArrayVec<u32, KEY_BIT_AMT>, ArrayVec<u64, KEY_BIT_AMT>) {
350    let mut s = ArrayVec::from([0u32; KEY_BIT_AMT]);
351    let mut prevs = ArrayVec::from([0usize; KEY_BIT_AMT]);
352    let mut squares = ArrayVec::from([0u64; KEY_BIT_AMT]);
353    for (i, x) in input.into_iter().chain(Some(0).into_iter()).enumerate() {
354        for b_i in (x as usize)..(KEY_BIT_AMT) {
355            s[b_i] += 1;
356            squares[b_i] += (i - prevs[b_i]) as u64 * (i - prevs[b_i]) as u64;
357            prevs[b_i] = i;
358        }
359    }
360    (s, squares)
361}
362
363pub fn get_results_from_clzs_functional<const KEY_BIT_AMT: usize>(
364    input: impl Iterator<Item = u8> + Clone,
365) -> (ArrayVec<u32, KEY_BIT_AMT>, ArrayVec<usize, KEY_BIT_AMT>)
366{
367    let mut s = ArrayVec::from([0u32; KEY_BIT_AMT]);
368    let mut squares = ArrayVec::from([0usize; KEY_BIT_AMT]);
369    let smaller_clz_positions = |x| input.clone().enumerate().filter(move |(_, a)| *a <= x).map(|x| x.0);
370    
371    s.iter_mut().enumerate().for_each(|(i, x)| {
372        *x = (smaller_clz_positions(i as u8)).count() as u32
373    });
374    
375    squares.iter_mut().enumerate().for_each(|(i, a)| {
376            let poss_1 = smaller_clz_positions(i as u8);
377            let poss_2 = smaller_clz_positions(i as u8).chain(Some(0).into_iter()).skip(1);
378            *a = poss_2.zip(poss_1).map(|x| x.0 - x.1).map(|x| x*x).sum();
379        });
380        
381    (s, squares)
382}
383
384//const fn get_results_from_clzs = get_results_from_clzs_imperative;
385
386pub fn get_inclination(input: &[f64]) -> f64 {
387    let length = input.iter().count() as f64;
388    let avy: f64 = input.iter().sum::<f64>() / length;
389    let avx: f64 = length * (length - 1.0) / (2.0 * length);
390    let num_inc = |(i, x): (usize, f64)| -> f64 { (x - avy) * (i as f64 - avx) };
391    let denom_inc = |(i, _): (usize, f64)| -> f64 { (i as f64 - avx) * (i as f64 - avx) };
392    let num: f64 = input
393        .iter()
394        .enumerate()
395        .map(|(a, b): (usize, &f64)| num_inc((a, *b)))
396        .sum();
397    let denom: f64 = input
398        .iter()
399        .enumerate()
400        .map(|(a, b): (usize, &f64)| denom_inc((a, *b)))
401        .sum();
402    num / denom
403}
404
405pub fn finalise_results<const KEY_BIT_AMT: usize>(
406    s: ArrayVec<u32, KEY_BIT_AMT>,
407    squares: ArrayVec<u64, KEY_BIT_AMT>,
408    sample_size: usize,
409    coor_bit_amt: u8,
410) -> (f64, ArrayVec<f64, KEY_BIT_AMT>, ArrayVec<f64, KEY_BIT_AMT>) {
411    let step = KEY_BIT_AMT / (coor_bit_amt as usize);
412    let result_2 = s
413        .iter()
414        .skip(step - 1)
415        .step_by(step)
416        .map(|&x| f64::from(x).log2())
417        .collect::<ArrayVec<_, KEY_BIT_AMT>>();
418    let result_3 = squares
419        .iter()
420        .zip(s.iter())
421        .skip(step - 1)
422        .step_by(step)
423        .map(|(&a, &b)| (a as f64) * (b as f64) / (sample_size as f64 * sample_size as f64) - 1.0)
424        .collect::<ArrayVec<_, KEY_BIT_AMT>>();
425    let cap = (sample_size as f64).log2();
426    let result_1_lim = result_2
427        .iter()
428        .position(|x| *x > (0.9) * cap)
429        .unwrap_or(coor_bit_amt as usize);
430    let result_1 = get_inclination(&result_2[0..result_1_lim]);
431    (result_1, result_2, result_3)
432}
433
434#[cfg(feature = "std")]
435pub fn zbox_merge<H, Smp, Key, Set, const KEY_BIT_AMT: usize>(set: Set, get_key_from_sample: H, sample_size: usize, coor_bits: u8) -> (f64, ArrayVec<f64, KEY_BIT_AMT>, ArrayVec<f64, KEY_BIT_AMT>)
436where
437    H: Fn(Smp) -> Key,
438    Key: PrimInt,
439    Set: IntoIterator<Item = Smp>,
440{
441    let clzs = get_clzs(set, get_key_from_sample);
442    let (s, squares) = get_results_from_clzs(clzs);
443    finalise_results(s, squares, sample_size, coor_bits)
444}
445
446pub fn drain_useless_bits<T, const TOTAL_BITS: usize, const USEFUL_BITS: usize>(mut input: ArrayVec<T, TOTAL_BITS>) -> ArrayVec<T, USEFUL_BITS> {
447    let useless_bits = TOTAL_BITS - USEFUL_BITS;
448    input.drain(..useless_bits);
449    input.into_iter().collect()
450}
451
452
453
454
455
456
457#[cfg(feature = "parallel")]
458use rayon::prelude::*;
459
460#[cfg(feature = "parallel")]
461pub fn get_clzs_par<'a, H:'a, Smp:'a, Key:'a, Set:'a>(set: Set, get_key_from_sample: H) -> impl rayon::iter::ParallelIterator<Item = u8>
462where
463    H: Fn(Smp) -> Key + std::marker::Sync + std::marker::Send + Copy,
464    Key: num_traits::int::PrimInt + std::marker::Sync + std::marker::Send,
465    [Key] : rayon::prelude::ParallelSliceMut<Key>,
466    Set : rayon::iter::IntoParallelIterator<Item = Smp>,
467    Smp: std::marker::Sync + std::marker::Send
468{
469    let mut buffer = time! (set
470            .into_par_iter()
471            .map(get_key_from_sample)
472            .collect::<Vec<_>>());
473
474        time!(
475            buffer.par_sort_unstable()
476        );
477
478        time!(
479            (0..buffer.len().saturating_sub(1))
480            .into_par_iter()
481            .map(move |i| (buffer[i], buffer[i + 1]))
482            .map(|x| x.0 ^ x.1)
483            .map(|x| x.leading_zeros() as u8)
484        )
485}
486
487#[cfg(feature = "parallel")]
488pub fn get_results_from_clzs_parallel<const KEY_BIT_AMT: usize>(
489    input: impl Iterator<Item = u8> + Clone + Sync,
490) -> ([u32; KEY_BIT_AMT], [usize; KEY_BIT_AMT])
491{
492    let mut s = [0u32; KEY_BIT_AMT];
493    let mut squares = [0usize; KEY_BIT_AMT];
494    let smaller_clz_positions = |x| input.clone().enumerate().filter(move |(_, a)| *a <= x).map(|x| x.0);
495    
496    s.par_iter_mut().enumerate().for_each(|(i, x)| {
497        *x = (smaller_clz_positions(i as u8)).count() as u32
498    });
499    
500    squares.par_iter_mut().enumerate().for_each(|(i, a)| {
501            let poss_1 = smaller_clz_positions(i as u8);
502            let poss_2 = smaller_clz_positions(i as u8).chain(Some(0).into_iter()).skip(1);
503            *a = poss_2.zip(poss_1).map(|x| x.0 - x.1).map(|x| x*x).sum();
504        });
505        
506    (s, squares)
507}
508
509#[cfg(feature = "parallel")]
510pub fn zbox_merge_par<H, Smp, Key, Set, const KEY_BIT_AMT: usize>(set: Set, get_key_from_sample: H, sample_size: usize, coor_bits: u8) -> (f64, ArrayVec<f64, KEY_BIT_AMT>, ArrayVec<f64, KEY_BIT_AMT>)
511where
512    H: Fn(Smp) -> Key + std::marker::Sync + std::marker::Send + Copy,
513    Key: num_traits::int::PrimInt + std::marker::Sync + std::marker::Send,
514    [Key] : rayon::prelude::ParallelSliceMut<Key>,
515    Set : rayon::iter::IntoParallelIterator<Item = Smp>,
516    Smp: std::marker::Sync + std::marker::Send
517{
518    let fnoo = get_results_from_clzs;
519    let clzs = get_clzs_par(set, get_key_from_sample).collect::<Vec<_>>();
520    let (s, squares) = fnoo(clzs.into_iter());
521    finalise_results(s, squares, sample_size, coor_bits)
522}
523
524#[cfg(test)]
525mod tests {
526    use super::*;
527
528    #[test]
529    fn wtf() {
530        assert!(true);
531    }
532
533    #[test]
534    fn img_sorta() {
535        use itertools::Itertools;
536        use noise::utils::*;
537        use noise::Fbm;
538        use noise::MultiFractal;
539
540        let mut dur = std::time::Duration::from_millis(0);
541        for x in 0..10 {
542            let clamp = |x, lower, upper| {
543                if x < lower {
544                    lower
545                } else if x > upper {
546                    upper
547                } else {
548                    x
549                }
550            };
551            let size_x = 1024;
552            let size_y = size_x;
553            let fbm = Fbm::new().set_octaves(32).set_persistence(x as f64 / 10.);
554            let pmb = PlaneMapBuilder::new(&fbm)
555                .set_size(size_x as usize, size_y as usize)
556                .build();
557            let pix_span = (0..size_x as usize).cartesian_product(0..size_y as usize);
558            let mut result = image::GrayImage::new(size_x as u32, size_y as u32);
559            pix_span
560                .clone()
561                .map(|(xx, yy)| pmb.get_value(xx, yy))
562                .map(|val| (val + 1.0) * 128.0)
563                .map(|val| (clamp(val, 0.0, 255.99)) as u8)
564                .zip(pix_span)
565                .for_each(|(val, (xx, yy))| {
566                    result.put_pixel(xx as u32, yy as u32, image::Luma([val]))
567                });
568            drop((pmb, fbm));
569
570            let begin = std::time::Instant::now();
571            let (hrm, _, _) = measure_lum_2d_u8_image(result);
572            dur += begin.elapsed();
573            dbg!(hrm);
574        }
575        println!("Time elapsed: {:?}", dur);
576
577        use noise::Seedable;
578        let mut dur = std::time::Duration::from_millis(0);
579        for x in 0..10 {
580            let clamp = |x, lower, upper| {
581                if x < lower {
582                    lower
583                } else if x > upper {
584                    upper
585                } else {
586                    x
587                }
588            };
589            let size_x = 1 << 10;
590            let size_y = size_x;
591            let fbm_1 = Fbm::new()
592                .set_octaves(32)
593                .set_persistence(x as f64 / 10.)
594                .set_seed(0);
595            let fbm_2 = Fbm::new()
596                .set_octaves(32)
597                .set_persistence(x as f64 / 10.)
598                .set_seed(1);
599            let fbm_3 = Fbm::new()
600                .set_octaves(32)
601                .set_persistence(x as f64 / 10.)
602                .set_seed(2);
603            let pmb_1 = PlaneMapBuilder::new(&fbm_1)
604                .set_size(size_x as usize, size_y as usize)
605                .build();
606            let pmb_2 = PlaneMapBuilder::new(&fbm_2)
607                .set_size(size_x as usize, size_y as usize)
608                .build();
609            let pmb_3 = PlaneMapBuilder::new(&fbm_3)
610                .set_size(size_x as usize, size_y as usize)
611                .build();
612            let pix_span = (0..size_x as usize).cartesian_product(0..size_y as usize);
613            let mut result = image::RgbImage::new(size_x as u32, size_y as u32);
614            let map_fn = |val: f64| (val + 1.0) * 128.0;
615            let map_fn = |val: f64| clamp(map_fn(val), 0.0, 255.99) as u8;
616            let map_fn = |x: [f64; 3]| [map_fn(x[0]), map_fn(x[1]), map_fn(x[2])];
617            pix_span
618                .clone()
619                .map(|(xx, yy)| {
620                    [
621                        pmb_1.get_value(xx, yy),
622                        pmb_2.get_value(xx, yy),
623                        pmb_3.get_value(xx, yy),
624                    ]
625                })
626                .map(map_fn)
627                .zip(pix_span)
628                .for_each(|(val, (xx, yy))| {
629                    result.put_pixel(xx as u32, yy as u32, image::Rgb(val))
630                });
631            drop((pmb_1, fbm_1, pmb_2, fbm_2, pmb_3, fbm_3));
632
633            let begin = std::time::Instant::now();
634            let (hrm, _, _) = measure_rgb_2d_u8_image(result);
635            dur += begin.elapsed();
636            dbg!(hrm);
637        }
638        println!("Time elapsed: {:?}", dur);
639    }
640}