nv_flip/lib.rs
1//! bindings to Nvidia Labs's [ꟻLIP] image comparison and error visualization library.
2//!
3//! This library allows you to visualize and reason about the human-noticable differences
4//! between rendered images. Especially when comparing images that are noisy or other small
5//! differences, FLIP's comparison can be more meaningful than a simple pixel-wise comparison.
6//!
7//! 
8//!
9//! In order to keep a small dependency closure, this crate does not depend on `image`,
10//! but interop is simple.
11//!
12//! # Example
13//!
14//! ```rust
15//! // First we load the "reference image". This is the image we want to compare against.
16//! //
17//! // We make sure to turn the image into RGB8 as FLIP doesn't deal with alpha.
18//! let ref_image_data = image::open("../etc/tree-ref.png").unwrap().into_rgb8();
19//! let ref_image = nv_flip::FlipImageRgb8::with_data(
20//! ref_image_data.width(),
21//! ref_image_data.height(),
22//! &ref_image_data
23//! );
24//!
25//! // We then load the "test image". This is the image we want to compare to the reference.
26//! let test_image_data = image::open("../etc/tree-test.png").unwrap().into_rgb8();
27//! let test_image = nv_flip::FlipImageRgb8::with_data(
28//! test_image_data.width(),
29//! test_image_data.height(),
30//! &test_image_data
31//! );
32//!
33//! // We now run the comparison. This will produce a "error map" that that is the per-pixel
34//! // visual difference between the two images between 0 and 1.
35//! //
36//! // The last parameter is the number of pixels per degree of visual angle. This is used
37//! // to determine the size of imperfections that can be seen. See the `pixels_per_degree`
38//! // for more information. By default this value is 67.0.
39//! let error_map = nv_flip::flip(ref_image, test_image, nv_flip::DEFAULT_PIXELS_PER_DEGREE);
40//!
41//! // We can now visualize the error map using a LUT that maps the error value to a color.
42//! let visualized = error_map.apply_color_lut(&nv_flip::magma_lut());
43//!
44//! // Finally we can the final image into an `image` crate image and save it.
45//! let image = image::RgbImage::from_raw(
46//! visualized.width(),
47//! visualized.height(),
48//! visualized.to_vec()
49//! ).unwrap();
50//! # let _ = image;
51//!
52//! // We can get statistics about the error map by using their "Pool" type,
53//! // which is essentially a weighted histogram.
54//! let mut pool = nv_flip::FlipPool::from_image(&error_map);
55//!
56//! // These are the same statistics shown by the command line.
57//! //
58//! // The paper's writers recommend that, if you are to use a single number to
59//! // represent the error, they recommend the mean.
60//! println!("Mean: {}", pool.mean());
61//! println!("Weighted median: {}", pool.get_percentile(0.5, true));
62//! println!("1st weighted quartile: {}", pool.get_percentile(0.25, true));
63//! println!("3rd weighted quartile: {}", pool.get_percentile(0.75, true));
64//! println!("Min: {}", pool.min_value());
65//! println!("Max: {}", pool.max_value());
66//! ```
67//! The result of this example looks like this:
68//!
69//! <!-- This table uses U+2800 BRAILLE PATTERN BLANK in the header make the images vaguely the same size. -->
70//!
71//! | Reference | ⠀⠀Test⠀⠀ | ⠀Result⠀ |
72//! |:---------:|:---------:|:---------:|
73//! |  |  |  |
74//!
75//! # License
76//!
77//! The binding and rust interop code is tri-licensed under MIT, Apache-2.0, and ZLib.
78//!
79//! The ꟻLIP library itself is licensed under the BSD-3-Clause license.
80//!
81//! The example images used are licensed under the [Unsplash License].
82//!
83//! [ꟻLIP]: https://github.com/NVlabs/flip
84//! [Unsplash License]: https://unsplash.com/license
85
86use std::marker::PhantomData;
87
88pub use nv_flip_sys::{pixels_per_degree, DEFAULT_PIXELS_PER_DEGREE};
89
90/// 2D FLIP image that is accessed as Rgb8.
91///
92/// Internally this is Rgb32f, but the values are converted when read.
93pub struct FlipImageRgb8 {
94 inner: *mut nv_flip_sys::FlipImageColor3,
95 width: u32,
96 height: u32,
97}
98
99unsafe impl Send for FlipImageRgb8 {}
100unsafe impl Sync for FlipImageRgb8 {}
101
102impl Clone for FlipImageRgb8 {
103 fn clone(&self) -> Self {
104 let inner = unsafe { nv_flip_sys::flip_image_color3_clone(self.inner) };
105 assert!(!inner.is_null());
106 Self {
107 inner,
108 width: self.width,
109 height: self.height,
110 }
111 }
112}
113
114impl FlipImageRgb8 {
115 /// Create a new image with the given dimensions and zeroed contents.
116 pub fn new(width: u32, height: u32) -> Self {
117 let inner = unsafe { nv_flip_sys::flip_image_color3_new(width, height, std::ptr::null()) };
118 assert!(!inner.is_null());
119 Self {
120 inner,
121 width,
122 height,
123 }
124 }
125
126 /// Creates a new image with the given dimensions and copies the data into it.
127 ///
128 /// The data must be in Rgb8 format. Do not include alpha.
129 ///
130 /// Data is expected in row-major orderm from the top left, tightly packed.
131 ///
132 /// # Panics
133 ///
134 /// - If the data is not large enough to fill the image.
135 pub fn with_data(width: u32, height: u32, data: &[u8]) -> Self {
136 assert!(data.len() >= (width * height * 3) as usize);
137 let inner = unsafe { nv_flip_sys::flip_image_color3_new(width, height, data.as_ptr()) };
138 assert!(!inner.is_null());
139 Self {
140 inner,
141 width,
142 height,
143 }
144 }
145
146 /// Extracts the data from the image and returns it as a vector.
147 ///
148 /// Data is returned in row-major order, from the top left, tightly packed.
149 pub fn to_vec(&self) -> Vec<u8> {
150 let mut data = vec![0u8; (self.width * self.height * 3) as usize];
151 unsafe {
152 nv_flip_sys::flip_image_color3_get_data(self.inner, data.as_mut_ptr());
153 }
154 data
155 }
156
157 /// Returns the width of the image.
158 pub fn width(&self) -> u32 {
159 self.width
160 }
161
162 /// Returns the height of the image.
163 pub fn height(&self) -> u32 {
164 self.height
165 }
166}
167
168impl Drop for FlipImageRgb8 {
169 fn drop(&mut self) {
170 unsafe {
171 nv_flip_sys::flip_image_color3_free(self.inner);
172 }
173 }
174}
175
176/// 2D FLIP image that stores a single float per pixel.
177pub struct FlipImageFloat {
178 inner: *mut nv_flip_sys::FlipImageFloat,
179 width: u32,
180 height: u32,
181}
182
183unsafe impl Send for FlipImageFloat {}
184unsafe impl Sync for FlipImageFloat {}
185
186impl Clone for FlipImageFloat {
187 fn clone(&self) -> Self {
188 // SAFETY: The clone function does not mutate the image, despite taking a mutable pointer.
189 let inner = unsafe { nv_flip_sys::flip_image_float_clone(self.inner) };
190 assert!(!inner.is_null());
191 Self {
192 inner,
193 width: self.width,
194 height: self.height,
195 }
196 }
197}
198
199impl FlipImageFloat {
200 /// Create a new image with the given dimensions and zeroed contents.
201 pub fn new(width: u32, height: u32) -> Self {
202 let inner = unsafe { nv_flip_sys::flip_image_float_new(width, height, std::ptr::null()) };
203 assert!(!inner.is_null());
204 Self {
205 inner,
206 width,
207 height,
208 }
209 }
210
211 /// Creates a new image with the given dimensions and copies the data into it.
212 ///
213 /// Data is expected in row-major order, from the top left, tightly packed.
214 ///
215 /// # Panics
216 ///
217 /// - If the data is not large enough to fill the image.
218 pub fn with_data(width: u32, height: u32, data: &[f32]) -> Self {
219 assert!(data.len() >= (width * height) as usize);
220 let inner = unsafe { nv_flip_sys::flip_image_float_new(width, height, data.as_ptr()) };
221 assert!(!inner.is_null());
222 Self {
223 inner,
224 width,
225 height,
226 }
227 }
228
229 /// Applies the given 1D color lut to turn this single channel values into 3 channel values.
230 ///
231 /// Applies the following algorithm to each pixel:
232 ///
233 /// ```text
234 /// value_mapping[(pixel_value * 255).round() % value_mapping.width()]
235 /// ```
236 pub fn apply_color_lut(&self, value_mapping: &FlipImageRgb8) -> FlipImageRgb8 {
237 let output = FlipImageRgb8::new(self.width, self.height);
238 unsafe {
239 nv_flip_sys::flip_image_color3_color_map(output.inner, self.inner, value_mapping.inner);
240 }
241 output
242 }
243
244 /// Converts the image to a color image by copying the single channel value to all 3 channels.
245 pub fn to_color3(&self) -> FlipImageRgb8 {
246 let color3 = FlipImageRgb8::new(self.width, self.height);
247 unsafe {
248 nv_flip_sys::flip_image_float_copy_float_to_color3(self.inner, color3.inner);
249 }
250 color3
251 }
252
253 /// Extracts the data from the image and returns it as a vector.
254 ///
255 /// Data is returned in row-major order, from the top left, tightly packed.
256 pub fn to_vec(&self) -> Vec<f32> {
257 let mut data = vec![0f32; (self.width * self.height) as usize];
258 unsafe {
259 nv_flip_sys::flip_image_float_get_data(self.inner, data.as_mut_ptr());
260 }
261 data
262 }
263
264 /// Returns the width of the image.
265 pub fn width(&self) -> u32 {
266 self.width
267 }
268
269 /// Returns the height of the image.
270 pub fn height(&self) -> u32 {
271 self.height
272 }
273}
274
275impl Drop for FlipImageFloat {
276 fn drop(&mut self) {
277 unsafe {
278 nv_flip_sys::flip_image_float_free(self.inner);
279 }
280 }
281}
282
283/// Generates a 1D lut using the builtin magma colormap for mapping error values to colors.
284pub fn magma_lut() -> FlipImageRgb8 {
285 let inner = unsafe { nv_flip_sys::flip_image_color3_magma_map() };
286 assert!(!inner.is_null());
287 FlipImageRgb8 {
288 inner,
289 width: 256,
290 height: 1,
291 }
292}
293
294/// Performs a FLIP comparison between the two images.
295///
296/// The images must be the same size.
297///
298/// Images are assumed to be in the sRGB color space.
299///
300/// Returns an error map image, where each pixel represents the error between the two images
301/// at that location between 0 and 1.
302///
303/// The pixels_per_degree parameter is used to determine the sensitivity to differences. See the
304/// documentation for [`DEFAULT_PIXELS_PER_DEGREE`] and [`pixels_per_degree`] for more information.
305///
306/// Consumes both images as the algorithm uses them for scratch space. If you want to re-use
307/// the images, clone them while passing them in.
308///
309/// # Panics
310///
311/// - If the images are not the same size.
312pub fn flip(
313 reference_image: FlipImageRgb8,
314 test_image: FlipImageRgb8,
315 pixels_per_degree: f32,
316) -> FlipImageFloat {
317 assert_eq!(
318 reference_image.width(),
319 test_image.width(),
320 "Width mismatch between reference and test image"
321 );
322 assert_eq!(
323 reference_image.height(),
324 test_image.height(),
325 "Height mismatch between reference and test image"
326 );
327
328 let error_map = FlipImageFloat::new(reference_image.width(), reference_image.height());
329 unsafe {
330 nv_flip_sys::flip_image_float_flip(
331 error_map.inner,
332 reference_image.inner,
333 test_image.inner,
334 pixels_per_degree,
335 );
336 }
337 error_map
338}
339
340/// Bucket based histogram used internally by [`FlipPool`].
341///
342/// Generally you should not need to use this directly and any mutating
343/// operations are unsafe to prevent violating FlipPool invariants.
344pub struct FlipHistogram<'a> {
345 inner: *mut nv_flip_sys::FlipImageHistogramRef,
346 _phantom: PhantomData<&'a ()>,
347}
348
349unsafe impl Send for FlipHistogram<'_> {}
350unsafe impl Sync for FlipHistogram<'_> {}
351
352impl<'a> FlipHistogram<'a> {
353 /// Returns the difference between the maximum and minimum bucket values.
354 pub fn bucket_size(&self) -> usize {
355 unsafe { nv_flip_sys::flip_image_histogram_ref_get_bucket_size(self.inner) }
356 }
357
358 /// Returns the index of the lowest bucket currently in use.
359 ///
360 /// If no buckets are in use, returns None.
361 pub fn bucket_id_min(&self) -> Option<usize> {
362 let value = unsafe { nv_flip_sys::flip_image_histogram_ref_get_bucket_id_min(self.inner) };
363 if value == usize::MAX {
364 None
365 } else {
366 Some(value)
367 }
368 }
369
370 /// Returns the index of the highest bucket currently in use.
371 ///
372 /// If no buckets are in use, returns 0.
373 pub fn bucket_id_max(&self) -> usize {
374 unsafe { nv_flip_sys::flip_image_histogram_ref_get_bucket_id_max(self.inner) }
375 }
376
377 /// Returns the amount of values contained within a given bucket.
378 ///
379 /// # Panics
380 ///
381 /// - If the bucket_id is out of bounds.
382 pub fn bucket_value_count(&self, bucket_id: usize) -> usize {
383 assert!(bucket_id < self.bucket_count());
384 unsafe { nv_flip_sys::flip_image_histogram_ref_get_bucket_value(self.inner, bucket_id) }
385 }
386
387 /// Returns the amount of buckets in the histogram.
388 pub fn bucket_count(&self) -> usize {
389 unsafe { nv_flip_sys::flip_image_histogram_ref_size(self.inner) }
390 }
391
392 /// Returns the smallest value the histogram can handle.
393 pub fn minimum_allowed_value(&self) -> f32 {
394 unsafe { nv_flip_sys::flip_image_histogram_ref_get_min_value(self.inner) }
395 }
396
397 /// Returns the largest value the histogram can handle.
398 pub fn maximum_allowed_value(&self) -> f32 {
399 unsafe { nv_flip_sys::flip_image_histogram_ref_get_max_value(self.inner) }
400 }
401
402 /// Clears the histogram of all values
403 ///
404 /// # Safety
405 ///
406 /// Due to the many invariants between the histogram and the pool,
407 /// we do not provide any safty guarentees when mutating the histogram.
408 pub unsafe fn clear(&mut self) {
409 unsafe {
410 nv_flip_sys::flip_image_histogram_ref_clear(self.inner);
411 }
412 }
413
414 /// Resizes the histogram to have `bucket_size` buckets.
415 ///
416 /// # Safety
417 ///
418 /// Due to the many invariants between the histogram and the pool,
419 /// we do not provide any safty guarentees when mutating the histogram.
420 pub unsafe fn resize(&mut self, bucket_size: usize) {
421 unsafe {
422 nv_flip_sys::flip_image_histogram_ref_resize(self.inner, bucket_size);
423 }
424 }
425
426 /// Returns which bucket a given value would fall into.
427 pub fn bucket_id(&self, value: f32) -> usize {
428 unsafe { nv_flip_sys::flip_image_histogram_ref_value_bucket_id(self.inner, value) }
429 }
430
431 /// Includes `count` instances of the following `value` in the histogram.
432 ///
433 /// # Safety
434 ///
435 /// Due to the many invariants between the histogram and the pool,
436 /// we do not provide any safty guarentees when mutating the histogram.
437 pub unsafe fn include_value(&mut self, value: f32, count: usize) {
438 unsafe {
439 nv_flip_sys::flip_image_histogram_ref_inc_value(self.inner, value, count);
440 }
441 }
442
443 /// Includes one instance of each value in the given image in the histogram.
444 ///
445 /// # Safety
446 ///
447 /// Due to the many invariants between the histogram and the pool,
448 /// we do not provide any safty guarentees when mutating the histogram.
449 pub unsafe fn include_image(&mut self, image: &FlipImageFloat) {
450 unsafe {
451 nv_flip_sys::flip_image_histogram_ref_inc_image(self.inner, image.inner);
452 }
453 }
454}
455
456impl Drop for FlipHistogram<'_> {
457 fn drop(&mut self) {
458 unsafe {
459 nv_flip_sys::flip_image_histogram_ref_free(self.inner);
460 }
461 }
462}
463
464/// Histogram-like value pool for determining if error map has significant differences.
465///
466/// This is how you can programmatically determine if images count as different.
467pub struct FlipPool {
468 inner: *mut nv_flip_sys::FlipImagePool,
469 values_added: usize,
470}
471
472unsafe impl Send for FlipPool {}
473unsafe impl Sync for FlipPool {}
474
475impl FlipPool {
476 /// Creates a new pool with 100 buckets.
477 pub fn new() -> Self {
478 Self::with_buckets(100)
479 }
480
481 /// Creates a new pool with the given amount of buckets.
482 pub fn with_buckets(bucket_count: usize) -> Self {
483 let inner = unsafe { nv_flip_sys::flip_image_pool_new(bucket_count) };
484 assert!(!inner.is_null());
485 Self {
486 inner,
487 values_added: 0,
488 }
489 }
490
491 /// Creates a new pool and initializes the buckets with the values given image.
492 pub fn from_image(image: &FlipImageFloat) -> Self {
493 let mut pool = Self::new();
494 pool.update_with_image(image);
495 pool
496 }
497
498 /// Accesses the internal histogram of the pool.
499 pub fn histogram(&mut self) -> FlipHistogram<'_> {
500 let inner = unsafe { nv_flip_sys::flip_image_pool_get_histogram(self.inner) };
501 assert!(!inner.is_null());
502 FlipHistogram {
503 inner,
504 _phantom: PhantomData,
505 }
506 }
507
508 /// Gets the minimum value stored in the pool.
509 ///
510 /// Returns 0.0 if no values have been added to the pool.
511 pub fn min_value(&self) -> f32 {
512 if self.values_added == 0 {
513 return 0.0;
514 }
515 unsafe { nv_flip_sys::flip_image_pool_get_min_value(self.inner) }
516 }
517
518 /// Gets the maximum value stored in the pool.
519 ///
520 /// Returns 0.0 if no values have been added to the pool.
521 pub fn max_value(&self) -> f32 {
522 if self.values_added == 0 {
523 return 0.0;
524 }
525 unsafe { nv_flip_sys::flip_image_pool_get_max_value(self.inner) }
526 }
527
528 /// Gets the mean value stored in the pool.
529 ///
530 /// Returns 0.0 if no values have been added to the pool.
531 pub fn mean(&self) -> f32 {
532 // Avoid div by zero in body.
533 if self.values_added == 0 {
534 return 0.0;
535 }
536 unsafe { nv_flip_sys::flip_image_pool_get_mean(self.inner) }
537 }
538
539 /// Gets the given weighted percentile [0.0, 1.0] from the pool.
540 ///
541 /// I currently do not understand the difference between this and [`Self::get_percentile`] with weighted = true,
542 /// except that this function uses doubles and doesn't require mutation of internal state.
543 ///
544 /// Returns 0.0 if no values have been added to the pool.
545 pub fn get_weighted_percentile(&self, percentile: f64) -> f64 {
546 if self.values_added == 0 {
547 return 0.0;
548 }
549 let bounds_percentile = f64::clamp(percentile, 0.0, next_f64_down(1.0));
550 unsafe {
551 nv_flip_sys::flip_image_pool_get_weighted_percentile(self.inner, bounds_percentile)
552 }
553 }
554
555 /// Get the value of the given percentile [0.0, 1.0] from the pool.
556 ///
557 /// If `weighted` is true, is almost equivalent to [`Self::get_weighted_percentile`].
558 ///
559 /// Returns 0.0 if no values have been added to the pool.
560 pub fn get_percentile(&mut self, percentile: f32, weighted: bool) -> f32 {
561 // Avoids a division by zero when bounds checking.
562 if self.values_added == 0 {
563 return 0.0;
564 }
565 // The implementaion doesn't actually do any bounds checking on the percentile,
566 // so we need to do it here, including tracking count of values added.
567 let bounds_percentile =
568 f32::clamp(percentile, 0.0, 1.0 - (self.values_added as f32).recip());
569 // Replicates the indexing behavior of the C++ implementation.
570 debug_assert!(
571 (f32::ceil(bounds_percentile * self.values_added as f32) as usize) < self.values_added
572 );
573 unsafe {
574 nv_flip_sys::flip_image_pool_get_percentile(self.inner, bounds_percentile, weighted)
575 }
576 }
577
578 /// Updates the given pool with the contents of the given image.
579 pub fn update_with_image(&mut self, image: &FlipImageFloat) {
580 unsafe {
581 nv_flip_sys::flip_image_pool_update_image(self.inner, image.inner);
582 }
583 self.values_added += image.width() as usize * image.height() as usize;
584 }
585
586 /// Clears the pool.
587 pub fn clear(&mut self) {
588 unsafe {
589 nv_flip_sys::flip_image_pool_clear(self.inner);
590 }
591 self.values_added = 0;
592 }
593}
594
595impl Default for FlipPool {
596 fn default() -> Self {
597 Self::new()
598 }
599}
600
601impl Drop for FlipPool {
602 fn drop(&mut self) {
603 unsafe {
604 nv_flip_sys::flip_image_pool_free(self.inner);
605 }
606 }
607}
608
609// This next_f64_down impl only works for positive, normal values that are
610// more than one ulp away from f64::MIN_POSITIVE.
611fn next_f64_down(value: f64) -> f64 {
612 f64::from_bits(value.to_bits() - 1)
613}
614
615#[cfg(test)]
616mod tests {
617 pub use super::*;
618 use float_eq::assert_float_eq;
619
620 #[test]
621 fn zeroed_init() {
622 assert_eq!(FlipImageRgb8::new(10, 10).to_vec(), vec![0u8; 10 * 10 * 3]);
623 assert_eq!(FlipImageFloat::new(10, 10).to_vec(), vec![0.0f32; 10 * 10]);
624 }
625
626 #[test]
627 fn zero_size_pool_ops() {
628 let mut pool = FlipPool::new();
629 assert_eq!(pool.min_value(), 0.0);
630 assert_eq!(pool.max_value(), 0.0);
631 assert_eq!(pool.mean(), 0.0);
632 assert_eq!(pool.get_percentile(0.0, false), 0.0);
633 assert_eq!(pool.get_percentile(0.0, true), 0.0);
634 assert_eq!(pool.get_weighted_percentile(0.0), 0.0);
635 }
636
637 #[test]
638 fn end_to_end() {
639 let reference_image = image::open("../etc/tree-ref.png").unwrap().into_rgb8();
640 let reference_image = FlipImageRgb8::with_data(
641 reference_image.width(),
642 reference_image.height(),
643 &reference_image,
644 );
645
646 let test_image = image::open("../etc/tree-test.png").unwrap().into_rgb8();
647 let test_image =
648 FlipImageRgb8::with_data(test_image.width(), test_image.height(), &test_image);
649
650 let error_map = flip(reference_image, test_image, DEFAULT_PIXELS_PER_DEGREE);
651
652 let mut pool = FlipPool::from_image(&error_map);
653
654 let magma_lut = magma_lut();
655 let color = error_map.apply_color_lut(&magma_lut);
656
657 let image =
658 image::RgbImage::from_raw(color.width(), color.height(), color.to_vec()).unwrap();
659
660 let reference = image::open("../etc/tree-comparison-cli.png")
661 .unwrap()
662 .into_rgb8();
663
664 for (a, b) in image.pixels().zip(reference.pixels()) {
665 assert!(a.0[0].abs_diff(b.0[0]) <= 3);
666 assert!(a.0[1].abs_diff(b.0[1]) <= 3);
667 assert!(a.0[2].abs_diff(b.0[2]) <= 3);
668 }
669
670 // These numbers pulled directly from the command line tool
671 const TOLERENCE: f32 = 0.000_1;
672 assert_float_eq!(pool.mean(), 0.133285, abs <= TOLERENCE);
673 assert_float_eq!(pool.get_percentile(0.25, true), 0.184924, abs <= TOLERENCE);
674 assert_float_eq!(pool.get_percentile(0.50, true), 0.333241, abs <= TOLERENCE);
675 assert_float_eq!(pool.get_percentile(0.75, true), 0.503441, abs <= TOLERENCE);
676 assert_float_eq!(pool.min_value(), 0.000000, abs <= TOLERENCE);
677 assert_float_eq!(pool.get_percentile(0.0, true), 0.000000, abs <= 0.001);
678 assert_float_eq!(pool.max_value(), 0.983044, abs <= TOLERENCE);
679 assert_float_eq!(pool.get_percentile(1.0, true), 0.983044, abs <= 0.001);
680 assert_float_eq!(
681 pool.get_weighted_percentile(0.25),
682 0.184586,
683 abs <= TOLERENCE as f64
684 );
685 assert_float_eq!(
686 pool.get_weighted_percentile(0.50),
687 0.333096,
688 abs <= TOLERENCE as f64
689 );
690 assert_float_eq!(
691 pool.get_weighted_percentile(0.75),
692 0.503230,
693 abs <= TOLERENCE as f64
694 );
695
696 let histogram = pool.histogram();
697 assert_float_eq!(histogram.minimum_allowed_value(), 0.0, abs <= TOLERENCE);
698 assert_float_eq!(histogram.maximum_allowed_value(), 1.0, abs <= TOLERENCE);
699 drop(histogram);
700
701 // Absurd values, trying to edge case the histogram
702 assert_float_eq!(pool.get_percentile(-10000.0, false), 0.0, abs <= TOLERENCE);
703 assert_float_eq!(pool.get_percentile(-10000.0, true), 0.0, abs <= TOLERENCE);
704 assert_float_eq!(
705 pool.get_percentile(10000.0, false),
706 0.983044,
707 abs <= TOLERENCE
708 );
709 assert_float_eq!(
710 pool.get_percentile(10000.0, true),
711 0.983044,
712 abs <= TOLERENCE
713 );
714 assert_float_eq!(
715 pool.get_weighted_percentile(-10000.0),
716 0.0,
717 abs <= TOLERENCE as _
718 );
719 assert_float_eq!(
720 pool.get_weighted_percentile(10000.0),
721 0.989999,
722 abs <= TOLERENCE as _
723 );
724 }
725}