This is an automated email from the ASF dual-hosted git repository.
tisonkun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datasketches-rust.git
The following commit(s) were added to refs/heads/main by this push:
new 02b13a6 feat: Implement KLLSketch (#248)
02b13a6 is described below
commit 02b13a648fbe560e97a4675ee95521da64ac2dc5
Author: Jeff Chung <[email protected]>
AuthorDate: Wed Sep 2 11:59:40 2026 +0800
feat: Implement KLLSketch (#248)
Co-authored-by: Chojan Shang <[email protected]>
Co-authored-by: tison <[email protected]>
---
CHANGELOG.md | 4 +
datasketches/Cargo.toml | 1 +
datasketches/src/codec/family.rs | 9 +
datasketches/src/codec/mod.rs | 2 +
datasketches/src/error.rs | 6 +
datasketches/src/kll/helper.rs | 99 ++
datasketches/src/kll/mod.rs | 65 ++
datasketches/src/kll/serialization.rs | 50 +
datasketches/src/kll/sketch.rs | 1166 ++++++++++++++++++++
datasketches/src/kll/sorted_view.rs | 203 ++++
datasketches/src/lib.rs | 2 +
tests-integration/Cargo.toml | 1 +
.../tests/kll_test/main.rs | 34 +-
tests-integration/tests/kll_test/sketch.rs | 397 +++++++
tests-integration/tests/serde_tests.rs | 3 +
tests-integration/tests/serde_tests/kll.rs | 337 ++++++
16 files changed, 2346 insertions(+), 33 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 969a56a..339c6a6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,10 @@ All significant changes to this project will be documented in
this file.
## Unreleased
+### New features
+
+* Add KLL sketches behind the `kll` feature, including rank, quantile, PMF,
and CDF queries, custom item ordering, merging, and C++/Java-compatible
serialization.
+
## v0.5.0
### Breaking changes
diff --git a/datasketches/Cargo.toml b/datasketches/Cargo.toml
index efd6444..1033540 100644
--- a/datasketches/Cargo.toml
+++ b/datasketches/Cargo.toml
@@ -43,6 +43,7 @@ countmin = []
cpc = []
frequencies = []
hll = []
+kll = ["dep:rand"]
req = ["dep:rand"]
tdigest = []
theta = []
diff --git a/datasketches/src/codec/family.rs b/datasketches/src/codec/family.rs
index 40b6d68..734d529 100644
--- a/datasketches/src/codec/family.rs
+++ b/datasketches/src/codec/family.rs
@@ -71,6 +71,15 @@ impl Family {
max_pre_longs: 4,
};
+ /// KLL sketch for estimating quantiles and ranks.
+ #[cfg(feature = "kll")]
+ pub const KLL: Family = Family {
+ id: 15,
+ name: "KLL",
+ min_pre_longs: 1,
+ max_pre_longs: 2,
+ };
+
/// Compressed Probabilistic Counting (CPC) Sketch.
#[cfg(feature = "cpc")]
pub const CPC: Family = Family {
diff --git a/datasketches/src/codec/mod.rs b/datasketches/src/codec/mod.rs
index bd46688..d6b6a06 100644
--- a/datasketches/src/codec/mod.rs
+++ b/datasketches/src/codec/mod.rs
@@ -28,6 +28,7 @@ pub use self::encode::SketchBytes;
feature = "cpc",
feature = "frequencies",
feature = "hll",
+ feature = "kll",
feature = "req",
feature = "tdigest",
feature = "theta",
@@ -42,6 +43,7 @@ pub(crate) mod assert;
feature = "cpc",
feature = "frequencies",
feature = "hll",
+ feature = "kll",
feature = "req",
feature = "tdigest",
feature = "theta",
diff --git a/datasketches/src/error.rs b/datasketches/src/error.rs
index 3f22eff..110b8c3 100644
--- a/datasketches/src/error.rs
+++ b/datasketches/src/error.rs
@@ -119,6 +119,12 @@ impl Error {
"invalid preamble longs: expected one of {expected:?}, got
{actual}"
))
}
+
+ pub(crate) fn invalid_preamble_ints(expected: u8, actual: u8) -> Self {
+ Self::deserial(format!(
+ "invalid preamble ints: expected {expected}, got {actual}"
+ ))
+ }
}
impl fmt::Debug for Error {
diff --git a/datasketches/src/kll/helper.rs b/datasketches/src/kll/helper.rs
new file mode 100644
index 0000000..6b993aa
--- /dev/null
+++ b/datasketches/src/kll/helper.rs
@@ -0,0 +1,99 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+const POWERS_OF_THREE: [u64; 31] = [
+ 1,
+ 3,
+ 9,
+ 27,
+ 81,
+ 243,
+ 729,
+ 2187,
+ 6561,
+ 19683,
+ 59049,
+ 177147,
+ 531441,
+ 1594323,
+ 4782969,
+ 14348907,
+ 43046721,
+ 129140163,
+ 387420489,
+ 1162261467,
+ 3486784401,
+ 10460353203,
+ 31381059609,
+ 94143178827,
+ 282429536481,
+ 847288609443,
+ 2541865828329,
+ 7625597484987,
+ 22876792454961,
+ 68630377364883,
+ 205891132094649,
+];
+
+pub(super) fn compute_total_capacity(k: u16, m: u8, num_levels: usize) -> u32 {
+ let mut total: u32 = 0;
+ for level in 0..num_levels {
+ total += level_capacity(k, num_levels, level, m);
+ }
+ total
+}
+
+pub(super) fn level_capacity(k: u16, num_levels: usize, height: usize,
min_wid: u8) -> u32 {
+ assert!(height < num_levels, "height must be < num_levels");
+ let depth = num_levels - height - 1;
+ let cap = int_cap_aux(k, depth as u8);
+ std::cmp::max(min_wid as u32, cap as u32)
+}
+
+fn int_cap_aux(k: u16, depth: u8) -> u16 {
+ if depth > 60 {
+ panic!("depth must be <= 60");
+ }
+ if depth <= 30 {
+ return int_cap_aux_aux(k, depth);
+ }
+ let half = depth / 2;
+ let rest = depth - half;
+ let tmp = int_cap_aux_aux(k, half);
+ int_cap_aux_aux(tmp, rest)
+}
+
+fn int_cap_aux_aux(k: u16, depth: u8) -> u16 {
+ if depth > 30 {
+ panic!("depth must be <= 30");
+ }
+ let twok = (k as u64) << 1;
+ let tmp = (twok << depth) / POWERS_OF_THREE[depth as usize];
+ let result = (tmp + 1) >> 1;
+ assert!(result <= k as u64, "capacity result exceeds k");
+ result as u16
+}
+
+pub(super) fn sum_the_sample_weights(level_sizes: &[usize]) -> u64 {
+ let mut total = 0u64;
+ let mut weight = 1u64;
+ for &size in level_sizes {
+ total += weight * size as u64;
+ weight <<= 1;
+ }
+ total
+}
diff --git a/datasketches/src/kll/mod.rs b/datasketches/src/kll/mod.rs
new file mode 100644
index 0000000..47cfd23
--- /dev/null
+++ b/datasketches/src/kll/mod.rs
@@ -0,0 +1,65 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! KLL sketch implementation for estimating quantiles and ranks.
+//!
+//! KLL is a compact, streaming quantiles sketch with lazy compaction and
+//! near-optimal accuracy per retained item. It supports one-pass updates,
+//! approximate quantiles, ranks, PMF, and CDF queries.
+//!
+//! This implementation follows Apache DataSketches semantics (Java KllSketch
+//! / KllPreambleUtil, C++ kll_sketch) and uses the same binary serialization
+//! format as those implementations.
+//!
+//! # Usage
+//!
+//! ```rust
+//! # use datasketches::kll::KllSketch;
+//! let mut sketch = KllSketch::<f64>::new(200).unwrap();
+//! sketch.update(1.0);
+//! sketch.update(2.0);
+//! let q = sketch.quantile(0.5, true).unwrap();
+//! assert!(q >= 1.0 && q <= 2.0);
+//! ```
+
+mod helper;
+mod serialization;
+mod sketch;
+mod sorted_view;
+
+pub use self::sketch::KllComparator;
+pub use self::sketch::KllItem;
+pub use self::sketch::KllSketch;
+pub use self::sketch::NaturalOrder;
+
+/// KLL sketch specialized for `f64`.
+pub type KllSketchF64<C = NaturalOrder> = KllSketch<f64, C>;
+/// KLL sketch specialized for `f32`.
+pub type KllSketchF32<C = NaturalOrder> = KllSketch<f32, C>;
+/// KLL sketch specialized for `i64`.
+pub type KllSketchI64<C = NaturalOrder> = KllSketch<i64, C>;
+/// KLL sketch specialized for `String`.
+pub type KllSketchString<C = NaturalOrder> = KllSketch<String, C>;
+
+/// Default value of parameter k.
+pub const DEFAULT_K: u16 = 200;
+/// Default value of parameter m.
+pub const DEFAULT_M: u8 = 8;
+/// Minimum value of parameter k.
+pub const MIN_K: u16 = DEFAULT_M as u16;
+/// Maximum value of parameter k.
+pub const MAX_K: u16 = u16::MAX;
diff --git a/datasketches/src/kll/serialization.rs
b/datasketches/src/kll/serialization.rs
new file mode 100644
index 0000000..3ba4a79
--- /dev/null
+++ b/datasketches/src/kll/serialization.rs
@@ -0,0 +1,50 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Binary serialization format constants for KLL sketches.
+//!
+//! Naming and layout follow the Apache DataSketches Java implementation
+//! (`KllPreambleUtil`) and the C++ `kll_sketch` compact serialization format.
+//! Java's updatable-memory layout is not a cross-language wire format and is
+//! intentionally outside this module's scope.
+
+/// Serialization version for empty or full sketches
(KllPreambleUtil.SERIAL_VERSION_EMPTY_FULL).
+pub(super) const SERIAL_VERSION_1: u8 = 1;
+/// Serialization version for single-item sketches
(KllPreambleUtil.SERIAL_VERSION_SINGLE).
+pub(super) const SERIAL_VERSION_2: u8 = 2;
+
+/// Preamble ints for empty and single-item sketches
(KllPreambleUtil.PREAMBLE_INTS_EMPTY_SINGLE).
+pub(super) const PREAMBLE_INTS_SHORT: u8 = 2;
+/// Preamble ints for sketches with more than one item
(KllPreambleUtil.PREAMBLE_INTS_FULL).
+pub(super) const PREAMBLE_INTS_FULL: u8 = 5;
+
+/// Flag indicating the sketch is empty (KllPreambleUtil.EMPTY_BIT_MASK).
+pub(super) const FLAG_EMPTY: u8 = 1 << 0;
+/// Flag indicating level zero is sorted
(KllPreambleUtil.LEVEL_ZERO_SORTED_BIT_MASK).
+pub(super) const FLAG_LEVEL_ZERO_SORTED: u8 = 1 << 1;
+/// Flag indicating the sketch has a single item
(KllPreambleUtil.SINGLE_ITEM_BIT_MASK).
+pub(super) const FLAG_SINGLE_ITEM: u8 = 1 << 2;
+
+/// Serialized size for an empty sketch in bytes
(KllPreambleUtil.DATA_START_ADR_SINGLE_ITEM).
+pub(super) const EMPTY_SIZE_BYTES: usize = 8;
+/// Data offset for single-item sketches
(KllPreambleUtil.DATA_START_ADR_SINGLE_ITEM).
+pub(super) const DATA_START_SINGLE_ITEM: usize = 8;
+/// Data offset for sketches with more than one item
(KllPreambleUtil.DATA_START_ADR).
+pub(super) const DATA_START: usize = 20;
+
+/// Maximum level count supported by the KLL capacity calculation.
+pub(super) const MAX_NUM_LEVELS: usize = 61;
diff --git a/datasketches/src/kll/sketch.rs b/datasketches/src/kll/sketch.rs
new file mode 100644
index 0000000..8248fe2
--- /dev/null
+++ b/datasketches/src/kll/sketch.rs
@@ -0,0 +1,1166 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::cmp::Ordering;
+
+use super::DEFAULT_K;
+use super::DEFAULT_M;
+use super::MAX_K;
+use super::MIN_K;
+use super::helper::compute_total_capacity;
+use super::helper::level_capacity;
+use super::helper::sum_the_sample_weights;
+use super::serialization::DATA_START;
+use super::serialization::DATA_START_SINGLE_ITEM;
+use super::serialization::EMPTY_SIZE_BYTES;
+use super::serialization::FLAG_EMPTY;
+use super::serialization::FLAG_LEVEL_ZERO_SORTED;
+use super::serialization::FLAG_SINGLE_ITEM;
+use super::serialization::MAX_NUM_LEVELS;
+use super::serialization::PREAMBLE_INTS_FULL;
+use super::serialization::PREAMBLE_INTS_SHORT;
+use super::serialization::SERIAL_VERSION_1;
+use super::serialization::SERIAL_VERSION_2;
+use super::sorted_view::build_sorted_view;
+use crate::codec::SketchBytes;
+use crate::codec::SketchSlice;
+use crate::codec::assert::ensure_serial_version_is;
+use crate::codec::assert::insufficient_data;
+use crate::codec::family::Family;
+use crate::error::Error;
+
+/// Trait implemented by item types supported by [`KllSketch`].
+///
+/// Implementations must provide a total ordering via `cmp`.
+/// For floating-point types, ensure `cmp` handles NaN consistently and
`is_nan`
+/// returns true for values that should be ignored by updates.
+pub trait KllItem: Clone {
+ /// Compare two items.
+ fn cmp(a: &Self, b: &Self) -> Ordering;
+
+ /// Returns true if the item is NaN.
+ fn is_nan(_value: &Self) -> bool {
+ false
+ }
+}
+
+/// Ordering policy used by a [`KllSketch`].
+///
+/// A sketch and every sketch merged into it must use equivalent ordering
policies.
+pub trait KllComparator<T>: Clone {
+ /// Compare two items.
+ fn compare(&self, left: &T, right: &T) -> Ordering;
+}
+
+/// Uses the natural ordering supplied by [`KllItem::cmp`].
+#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
+pub struct NaturalOrder;
+
+impl<T: KllItem> KllComparator<T> for NaturalOrder {
+ fn compare(&self, left: &T, right: &T) -> Ordering {
+ T::cmp(left, right)
+ }
+}
+
+trait KllSerde: KllItem {
+ /// Minimum serialized size in bytes for one item.
+ const MIN_SERIALIZED_SIZE: usize;
+
+ /// Serialized size in bytes.
+ fn serialized_size(value: &Self) -> usize;
+
+ /// Serialize a single item into the buffer.
+ fn serialize(value: &Self, bytes: &mut SketchBytes);
+
+ /// Deserialize a single item from the input.
+ fn deserialize(input: &mut SketchSlice<'_>) -> Result<Self, Error>;
+}
+
+/// KLL sketch for estimating quantiles and ranks.
+///
+/// See the [kll module level documentation](crate::kll) for more.
+#[derive(Debug, Clone, PartialEq)]
+pub struct KllSketch<T, C = NaturalOrder> {
+ comparator: C,
+ k: u16,
+ m: u8,
+ min_k: u16,
+ n: u64,
+ is_level_zero_sorted: bool,
+ levels: Vec<Vec<T>>,
+ min_item: Option<T>,
+ max_item: Option<T>,
+}
+
+impl<T: KllItem> Default for KllSketch<T> {
+ fn default() -> Self {
+ Self::make(
+ NaturalOrder,
+ DEFAULT_K,
+ DEFAULT_K,
+ 0,
+ vec![Vec::new()],
+ None,
+ None,
+ false,
+ )
+ }
+}
+
+impl<T: KllItem> KllSketch<T> {
+ /// Creates a new sketch with the given value of k.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if `k` is outside [`MIN_K`, `MAX_K`].
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use datasketches::kll::KllSketch;
+ /// let sketch = KllSketch::<f64>::new(200).unwrap();
+ /// assert_eq!(sketch.k(), 200);
+ /// ```
+ pub fn new(k: u16) -> Result<Self, Error> {
+ Self::new_with_comparator(k, NaturalOrder)
+ }
+}
+
+impl<T: KllItem, C: KllComparator<T>> KllSketch<T, C> {
+ /// Creates a new sketch with the given value of k and ordering policy.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if `k` is outside [`MIN_K`, `MAX_K`].
+ pub fn new_with_comparator(k: u16, comparator: C) -> Result<Self, Error> {
+ if !(MIN_K..=MAX_K).contains(&k) {
+ return Err(Error::invalid_argument(format!(
+ "k must be in [{MIN_K}, {MAX_K}], got {k}"
+ )));
+ }
+ Ok(Self::make(
+ comparator,
+ k,
+ k,
+ 0,
+ vec![Vec::new()],
+ None,
+ None,
+ false,
+ ))
+ }
+
+ /// Returns parameter k used to configure this sketch.
+ pub fn k(&self) -> u16 {
+ self.k
+ }
+
+ /// Returns the minimum k used when merging sketches.
+ pub fn min_k(&self) -> u16 {
+ self.min_k
+ }
+
+ /// Returns total weight of the stream.
+ pub fn n(&self) -> u64 {
+ self.n
+ }
+
+ /// Returns true if the sketch has not seen any data.
+ pub fn is_empty(&self) -> bool {
+ self.n == 0
+ }
+
+ /// Returns the number of retained items.
+ pub fn num_retained(&self) -> usize {
+ self.levels.iter().map(|level| level.len()).sum()
+ }
+
+ /// Returns true if the sketch is in estimation mode.
+ pub fn is_estimation_mode(&self) -> bool {
+ self.levels.len() > 1
+ }
+
+ /// Returns the minimum item seen by the sketch.
+ pub fn min_item(&self) -> Option<&T> {
+ self.min_item.as_ref()
+ }
+
+ /// Returns the maximum item seen by the sketch.
+ pub fn max_item(&self) -> Option<&T> {
+ self.max_item.as_ref()
+ }
+
+ /// Updates the sketch with a new item.
+ ///
+ /// NaN values are ignored for floating-point types.
+ pub fn update(&mut self, item: T) {
+ if T::is_nan(&item) {
+ return;
+ }
+ self.update_min_max(&item);
+ self.internal_update(item);
+ }
+
+ /// Resets this sketch to its empty state while retaining its
configuration.
+ pub fn reset(&mut self) {
+ self.min_k = self.k;
+ self.n = 0;
+ self.is_level_zero_sorted = false;
+ self.levels.clear();
+ self.levels.push(Vec::new());
+ self.min_item = None;
+ self.max_item = None;
+ }
+
+ /// Merges another sketch into this one.
+ ///
+ /// # Panics
+ ///
+ /// Panics if the sketches have incompatible parameters.
+ pub fn merge(&mut self, other: &KllSketch<T, C>) {
+ if other.is_empty() {
+ return;
+ }
+
+ assert_eq!(
+ self.m, other.m,
+ "incompatible m values: {} and {}",
+ self.m, other.m
+ );
+
+ self.update_min_max_from_other(other);
+
+ let final_n = self.n + other.n;
+ for item in &other.levels[0] {
+ self.internal_update(item.clone());
+ }
+
+ if other.levels.len() >= 2 {
+ self.merge_higher_levels(other);
+ }
+
+ self.n = final_n;
+ if other.is_estimation_mode() {
+ self.min_k = self.min_k.min(other.min_k);
+ }
+
+ debug_assert_eq!(self.total_weight(), self.n, "total weight does not
match n");
+ }
+
+ /// Returns the normalized rank of the given item.
+ pub fn rank(&self, item: &T, inclusive: bool) -> Option<f64> {
+ if self.is_empty() {
+ return None;
+ }
+ let view = build_sorted_view(&self.levels, self.comparator.clone());
+ Some(view.rank(item, inclusive))
+ }
+
+ /// Returns the quantile for the given normalized rank.
+ ///
+ /// # Panics
+ ///
+ /// Panics if rank is not in [0.0, 1.0].
+ pub fn quantile(&self, rank: f64, inclusive: bool) -> Option<T> {
+ if self.is_empty() {
+ return None;
+ }
+ assert!((0.0..=1.0).contains(&rank), "rank must be in [0.0, 1.0]");
+ let view = build_sorted_view(&self.levels, self.comparator.clone());
+ Some(view.quantile(rank, inclusive))
+ }
+
+ /// Returns the approximate CDF for the given split points.
+ pub fn cdf(&self, split_points: &[T], inclusive: bool) -> Option<Vec<f64>>
{
+ if self.is_empty() {
+ return None;
+ }
+ let view = build_sorted_view(&self.levels, self.comparator.clone());
+ Some(view.cdf(split_points, inclusive))
+ }
+
+ /// Returns the approximate PMF for the given split points.
+ pub fn pmf(&self, split_points: &[T], inclusive: bool) -> Option<Vec<f64>>
{
+ if self.is_empty() {
+ return None;
+ }
+ let view = build_sorted_view(&self.levels, self.comparator.clone());
+ Some(view.pmf(split_points, inclusive))
+ }
+
+ /// Returns normalized rank error for the configured k.
+ pub fn normalized_rank_error(&self, pmf: bool) -> f64 {
+ normalized_rank_error(self.min_k, pmf)
+ }
+}
+
+fn serialized_size<T: KllSerde, C: KllComparator<T>>(sketch: &KllSketch<T, C>)
-> usize {
+ if sketch.is_empty() {
+ return EMPTY_SIZE_BYTES;
+ }
+ if sketch.n == 1 {
+ let item = &sketch.levels[0][0];
+ return DATA_START_SINGLE_ITEM + T::serialized_size(item);
+ }
+
+ let mut size = DATA_START + sketch.levels.len() * 4;
+ if let Some(min_item) = &sketch.min_item {
+ size += T::serialized_size(min_item);
+ }
+ if let Some(max_item) = &sketch.max_item {
+ size += T::serialized_size(max_item);
+ }
+ for level in &sketch.levels {
+ for item in level {
+ size += T::serialized_size(item);
+ }
+ }
+ size
+}
+
+fn serialize_with_serde<T: KllSerde, C: KllComparator<T>>(sketch:
&KllSketch<T, C>) -> Vec<u8> {
+ let size = serialized_size(sketch);
+ let mut bytes = SketchBytes::with_capacity(size);
+
+ let is_empty = sketch.is_empty();
+ let is_single_item = sketch.n == 1;
+
+ let preamble_ints = if is_empty || is_single_item {
+ PREAMBLE_INTS_SHORT
+ } else {
+ PREAMBLE_INTS_FULL
+ };
+ let serial_version = if is_single_item {
+ SERIAL_VERSION_2
+ } else {
+ SERIAL_VERSION_1
+ };
+
+ let flags = (if is_empty { FLAG_EMPTY } else { 0 })
+ | (if sketch.is_level_zero_sorted {
+ FLAG_LEVEL_ZERO_SORTED
+ } else {
+ 0
+ })
+ | (if is_single_item { FLAG_SINGLE_ITEM } else { 0 });
+
+ bytes.write_u8(preamble_ints);
+ bytes.write_u8(serial_version);
+ bytes.write_u8(Family::KLL.id);
+ bytes.write_u8(flags);
+ bytes.write_u16_le(sketch.k);
+ bytes.write_u8(sketch.m);
+ bytes.write_u8(0);
+
+ if is_empty {
+ return bytes.into_bytes();
+ }
+
+ if !is_single_item {
+ bytes.write_u64_le(sketch.n);
+ bytes.write_u16_le(sketch.min_k);
+ bytes.write_u8(sketch.levels.len() as u8);
+ bytes.write_u8(0);
+
+ let level_offsets = sketch.level_offsets();
+ for offset in level_offsets.iter().take(sketch.levels.len()) {
+ bytes.write_u32_le(*offset);
+ }
+
+ if let Some(min_item) = &sketch.min_item {
+ T::serialize(min_item, &mut bytes);
+ }
+ if let Some(max_item) = &sketch.max_item {
+ T::serialize(max_item, &mut bytes);
+ }
+ }
+
+ for (level_index, level) in sketch.levels.iter().enumerate() {
+ if level_index == 0 && !sketch.is_level_zero_sorted {
+ for item in level.iter().rev() {
+ T::serialize(item, &mut bytes);
+ }
+ } else {
+ for item in level {
+ T::serialize(item, &mut bytes);
+ }
+ }
+ }
+
+ bytes.into_bytes()
+}
+
+fn deserialize_with_serde<T: KllSerde, C: KllComparator<T>>(
+ bytes: &[u8],
+ comparator: C,
+) -> Result<KllSketch<T, C>, Error> {
+ let mut cursor = SketchSlice::new(bytes);
+
+ let preamble_ints = cursor
+ .read_u8()
+ .map_err(insufficient_data("preamble_ints"))?;
+ let serial_version = cursor
+ .read_u8()
+ .map_err(insufficient_data("serial_version"))?;
+ let family_id = cursor.read_u8().map_err(insufficient_data("family_id"))?;
+ let flags = cursor.read_u8().map_err(insufficient_data("flags"))?;
+ let k = cursor.read_u16_le().map_err(insufficient_data("k"))?;
+ let m = cursor.read_u8().map_err(insufficient_data("m"))?;
+ let _unused = cursor.read_u8().map_err(insufficient_data("unused"))?;
+
+ if m != DEFAULT_M {
+ return Err(Error::deserial(format!(
+ "invalid m: expected {DEFAULT_M}, got {m}"
+ )));
+ }
+ Family::KLL.validate_id(family_id)?;
+ let is_empty = (flags & FLAG_EMPTY) != 0;
+ let is_single_item = (flags & FLAG_SINGLE_ITEM) != 0;
+ let is_level_zero_sorted = (flags & FLAG_LEVEL_ZERO_SORTED) != 0;
+ if is_empty && is_single_item {
+ return Err(Error::deserial(
+ "empty and single-item flags must not both be set",
+ ));
+ }
+ if is_empty || is_single_item {
+ if preamble_ints != PREAMBLE_INTS_SHORT {
+ return Err(Error::invalid_preamble_ints(
+ PREAMBLE_INTS_SHORT,
+ preamble_ints,
+ ));
+ }
+ } else if preamble_ints != PREAMBLE_INTS_FULL {
+ return Err(Error::invalid_preamble_ints(
+ PREAMBLE_INTS_FULL,
+ preamble_ints,
+ ));
+ }
+ let expected_version = if is_single_item {
+ SERIAL_VERSION_2
+ } else {
+ SERIAL_VERSION_1
+ };
+ ensure_serial_version_is(expected_version, serial_version)?;
+
+ if !(MIN_K..=MAX_K).contains(&k) {
+ return Err(Error::deserial(format!("k out of range: {k}")));
+ }
+
+ if is_empty {
+ if !cursor.remaining().is_empty() {
+ return Err(Error::deserial("unexpected trailing data"));
+ }
+ return Ok(KllSketch::make(
+ comparator,
+ k,
+ k,
+ 0,
+ vec![Vec::new()],
+ None,
+ None,
+ is_level_zero_sorted,
+ ));
+ }
+
+ let (n, min_k, num_levels) = if is_single_item {
+ (1u64, k, 1usize)
+ } else {
+ let n = cursor.read_u64_le().map_err(insufficient_data("n"))?;
+ let min_k = cursor.read_u16_le().map_err(insufficient_data("min_k"))?;
+ let num_levels =
cursor.read_u8().map_err(insufficient_data("num_levels"))?;
+ let _unused = cursor.read_u8().map_err(insufficient_data("unused2"))?;
+ (n, min_k, num_levels as usize)
+ };
+
+ if num_levels == 0 {
+ return Err(Error::deserial("num_levels must be > 0"));
+ }
+ if num_levels > MAX_NUM_LEVELS {
+ return Err(Error::deserial(format!(
+ "num_levels must be at most {MAX_NUM_LEVELS}, got {num_levels}"
+ )));
+ }
+ if !is_single_item && n < 2 {
+ return Err(Error::deserial(format!(
+ "full sketch must have n >= 2, got {n}"
+ )));
+ }
+ if min_k < MIN_K || min_k > k {
+ return Err(Error::deserial(format!(
+ "min_k must be in [{MIN_K}, {k}], got {min_k}"
+ )));
+ }
+
+ let capacity = compute_total_capacity(k, m, num_levels);
+ let mut level_offsets = Vec::with_capacity(num_levels + 1);
+ if !is_single_item {
+ for _ in 0..num_levels {
+ let offset =
cursor.read_u32_le().map_err(insufficient_data("levels"))?;
+ level_offsets.push(offset);
+ }
+ } else {
+ level_offsets.push(capacity - 1);
+ }
+ level_offsets.push(capacity);
+
+ if level_offsets.is_empty() {
+ return Err(Error::deserial("levels array is empty"));
+ }
+ if level_offsets[0] > capacity {
+ return Err(Error::deserial("levels[0] exceeds capacity"));
+ }
+ for window in level_offsets.windows(2) {
+ if window[1] < window[0] {
+ return Err(Error::deserial("levels array must be non-decreasing"));
+ }
+ }
+ let last = *level_offsets.last().unwrap();
+ if last != capacity {
+ return Err(Error::deserial("levels last offset must equal capacity"));
+ }
+
+ let min_item = if is_single_item {
+ None
+ } else {
+ Some(T::deserialize(&mut cursor)?)
+ };
+ let max_item = if is_single_item {
+ None
+ } else {
+ Some(T::deserialize(&mut cursor)?)
+ };
+
+ let num_retained = (level_offsets[num_levels] - level_offsets[0]) as usize;
+ let min_item_bytes = num_retained
+ .checked_mul(T::MIN_SERIALIZED_SIZE)
+ .ok_or_else(|| Error::deserial("retained item size overflow"))?;
+ if cursor.remaining().len() < min_item_bytes {
+ return Err(Error::insufficient_data("items"));
+ }
+
+ let mut levels = Vec::with_capacity(num_levels);
+ for level in 0..num_levels {
+ let size = (level_offsets[level + 1] - level_offsets[level]) as usize;
+ let mut items = Vec::with_capacity(size);
+ for _ in 0..size {
+ items.push(T::deserialize(&mut cursor)?);
+ }
+ levels.push(items);
+ }
+ if !is_level_zero_sorted {
+ levels[0].reverse();
+ }
+
+ let mut sketch = KllSketch::make(
+ comparator,
+ k,
+ min_k,
+ n,
+ levels,
+ min_item,
+ max_item,
+ is_level_zero_sorted,
+ );
+
+ if is_single_item {
+ if let Some(item) = sketch.levels[0].first().cloned() {
+ sketch.min_item = Some(item.clone());
+ sketch.max_item = Some(item);
+ }
+ }
+
+ sketch.validate_deserialized_state()?;
+ if !cursor.remaining().is_empty() {
+ return Err(Error::deserial("unexpected trailing data"));
+ }
+
+ Ok(sketch)
+}
+
+impl<C: KllComparator<f32>> KllSketch<f32, C> {
+ /// Serializes the sketch to bytes.
+ pub fn serialize(&self) -> Vec<u8> {
+ serialize_with_serde(self)
+ }
+
+ /// Deserializes a sketch using the supplied ordering policy.
+ pub fn deserialize_with_comparator(bytes: &[u8], comparator: C) ->
Result<Self, Error> {
+ deserialize_with_serde(bytes, comparator)
+ }
+}
+
+impl KllSketch<f32> {
+ /// Deserializes a sketch from bytes.
+ pub fn deserialize(bytes: &[u8]) -> Result<Self, Error> {
+ deserialize_with_serde(bytes, NaturalOrder)
+ }
+}
+
+impl<C: KllComparator<f64>> KllSketch<f64, C> {
+ /// Serializes the sketch to bytes.
+ pub fn serialize(&self) -> Vec<u8> {
+ serialize_with_serde(self)
+ }
+
+ /// Deserializes a sketch using the supplied ordering policy.
+ pub fn deserialize_with_comparator(bytes: &[u8], comparator: C) ->
Result<Self, Error> {
+ deserialize_with_serde(bytes, comparator)
+ }
+}
+
+impl KllSketch<f64> {
+ /// Deserializes a sketch from bytes.
+ pub fn deserialize(bytes: &[u8]) -> Result<Self, Error> {
+ deserialize_with_serde(bytes, NaturalOrder)
+ }
+}
+
+impl<C: KllComparator<i64>> KllSketch<i64, C> {
+ /// Serializes the sketch to bytes.
+ pub fn serialize(&self) -> Vec<u8> {
+ serialize_with_serde(self)
+ }
+
+ /// Deserializes a sketch using the supplied ordering policy.
+ pub fn deserialize_with_comparator(bytes: &[u8], comparator: C) ->
Result<Self, Error> {
+ deserialize_with_serde(bytes, comparator)
+ }
+}
+
+impl KllSketch<i64> {
+ /// Deserializes a sketch from bytes.
+ pub fn deserialize(bytes: &[u8]) -> Result<Self, Error> {
+ deserialize_with_serde(bytes, NaturalOrder)
+ }
+}
+
+impl<C: KllComparator<String>> KllSketch<String, C> {
+ /// Serializes the sketch to bytes.
+ pub fn serialize(&self) -> Vec<u8> {
+ serialize_with_serde(self)
+ }
+
+ /// Deserializes a sketch using the supplied ordering policy.
+ pub fn deserialize_with_comparator(bytes: &[u8], comparator: C) ->
Result<Self, Error> {
+ deserialize_with_serde(bytes, comparator)
+ }
+}
+
+impl KllSketch<String> {
+ /// Deserializes a sketch from bytes.
+ pub fn deserialize(bytes: &[u8]) -> Result<Self, Error> {
+ deserialize_with_serde(bytes, NaturalOrder)
+ }
+}
+
+impl<T: KllItem, C: KllComparator<T>> KllSketch<T, C> {
+ fn make(
+ comparator: C,
+ k: u16,
+ min_k: u16,
+ n: u64,
+ levels: Vec<Vec<T>>,
+ min_item: Option<T>,
+ max_item: Option<T>,
+ is_level_zero_sorted: bool,
+ ) -> Self {
+ Self {
+ comparator,
+ k,
+ m: DEFAULT_M,
+ min_k,
+ n,
+ is_level_zero_sorted,
+ levels,
+ min_item,
+ max_item,
+ }
+ }
+
+ fn capacity(&self) -> usize {
+ compute_total_capacity(self.k, self.m, self.levels.len()) as usize
+ }
+
+ fn level_offsets(&self) -> Vec<u32> {
+ let capacity = self.capacity() as u32;
+ let retained = self.num_retained() as u32;
+ assert!(capacity >= retained, "capacity must be >= retained");
+
+ let mut offsets = Vec::with_capacity(self.levels.len() + 1);
+ let mut offset = capacity - retained;
+ offsets.push(offset);
+ for level in &self.levels {
+ offset += level.len() as u32;
+ offsets.push(offset);
+ }
+ offsets
+ }
+
+ fn update_min_max(&mut self, item: &T) {
+ match self.min_item.as_ref() {
+ None => {
+ self.min_item = Some(item.clone());
+ self.max_item = Some(item.clone());
+ }
+ Some(min) => {
+ if self.comparator.compare(item, min) == Ordering::Less {
+ self.min_item = Some(item.clone());
+ }
+ if let Some(max) = &self.max_item {
+ if self.comparator.compare(max, item) == Ordering::Less {
+ self.max_item = Some(item.clone());
+ }
+ }
+ }
+ }
+ }
+
+ fn update_min_max_from_other(&mut self, other: &KllSketch<T, C>) {
+ match (&self.min_item, &self.max_item) {
+ (None, None) => {
+ self.min_item = other.min_item.clone();
+ self.max_item = other.max_item.clone();
+ }
+ (Some(min), Some(max)) => {
+ if let Some(other_min) = &other.min_item {
+ if self.comparator.compare(other_min, min) ==
Ordering::Less {
+ self.min_item = Some(other_min.clone());
+ }
+ }
+ if let Some(other_max) = &other.max_item {
+ if self.comparator.compare(max, other_max) ==
Ordering::Less {
+ self.max_item = Some(other_max.clone());
+ }
+ }
+ }
+ _ => {
+ self.min_item = other.min_item.clone();
+ self.max_item = other.max_item.clone();
+ }
+ }
+ }
+
+ fn internal_update(&mut self, item: T) {
+ if self.num_retained() >= self.capacity() {
+ self.compress_while_updating();
+ }
+ self.n += 1;
+ self.is_level_zero_sorted = false;
+ self.levels[0].push(item);
+ }
+
+ fn compress_while_updating(&mut self) {
+ let level = self.find_level_to_compact();
+ if level + 1 == self.levels.len() {
+ self.levels.push(Vec::new());
+ }
+
+ let mut current = std::mem::take(&mut self.levels[level]);
+ let mut above = std::mem::take(&mut self.levels[level + 1]);
+
+ let odd = current.len() % 2 == 1;
+ let mut leftover = None;
+ if odd {
+ leftover = Some(take_leftover(
+ &mut current,
+ level,
+ self.is_level_zero_sorted,
+ ));
+ }
+
+ if level == 0 && !self.is_level_zero_sorted {
+ current.sort_by(|left, right| self.comparator.compare(left,
right));
+ }
+
+ let use_up = above.is_empty();
+ let promoted = downsample(current, rand::random::<bool>(), use_up);
+ if above.is_empty() {
+ above = promoted;
+ } else {
+ above = merge_sorted_vec(promoted, above, &self.comparator);
+ }
+ self.levels[level + 1] = above;
+
+ let mut new_level = Vec::new();
+ if let Some(item) = leftover {
+ new_level.push(item);
+ }
+ self.levels[level] = new_level;
+ }
+
+ fn find_level_to_compact(&self) -> usize {
+ let num_levels = self.levels.len();
+ for level in 0..num_levels {
+ let pop = self.levels[level].len() as u32;
+ let cap = level_capacity(self.k, num_levels, level, self.m);
+ if pop >= cap {
+ return level;
+ }
+ }
+ panic!("no level to compact");
+ }
+
+ fn merge_higher_levels(&mut self, other: &KllSketch<T, C>) {
+ let provisional_levels = self.levels.len().max(other.levels.len());
+ let mut self_levels = std::mem::take(&mut self.levels);
+ let mut work_levels = vec![Vec::new(); provisional_levels];
+ work_levels[0] = std::mem::take(&mut self_levels[0]);
+
+ for level in 1..provisional_levels {
+ let left = if level < self_levels.len() {
+ std::mem::take(&mut self_levels[level])
+ } else {
+ Vec::new()
+ };
+ let right = other.levels.get(level).cloned().unwrap_or_default();
+
+ work_levels[level] = if left.is_empty() {
+ right
+ } else if right.is_empty() {
+ left
+ } else {
+ merge_sorted_vec(left, right, &self.comparator)
+ };
+ }
+
+ self.levels = general_compress(
+ work_levels,
+ self.k,
+ self.m,
+ self.is_level_zero_sorted,
+ &self.comparator,
+ );
+ }
+
+ fn total_weight(&self) -> u64 {
+ let sizes: Vec<usize> = self.levels.iter().map(|level|
level.len()).collect();
+ sum_the_sample_weights(&sizes)
+ }
+
+ fn validate_deserialized_state(&self) -> Result<(), Error> {
+ let min_item = self
+ .min_item
+ .as_ref()
+ .ok_or_else(|| Error::deserial("non-empty sketch must have a
minimum item"))?;
+ let max_item = self
+ .max_item
+ .as_ref()
+ .ok_or_else(|| Error::deserial("non-empty sketch must have a
maximum item"))?;
+
+ if T::is_nan(min_item) || T::is_nan(max_item) {
+ return Err(Error::deserial("minimum and maximum items must not be
NaN"));
+ }
+ if self.comparator.compare(min_item, max_item) == Ordering::Greater {
+ return Err(Error::deserial(
+ "minimum item must not be greater than maximum item",
+ ));
+ }
+
+ let mut total_weight = 0u64;
+ let mut level_weight = 1u64;
+ for (level_index, level) in self.levels.iter().enumerate() {
+ let level_total = level_weight
+ .checked_mul(level.len() as u64)
+ .ok_or_else(|| Error::deserial("sample weight overflow"))?;
+ total_weight = total_weight
+ .checked_add(level_total)
+ .ok_or_else(|| Error::deserial("total sample weight
overflow"))?;
+
+ let must_be_sorted = level_index > 0 || self.is_level_zero_sorted;
+ if must_be_sorted
+ && level
+ .windows(2)
+ .any(|pair| self.comparator.compare(&pair[0], &pair[1]) ==
Ordering::Greater)
+ {
+ return Err(Error::deserial(format!(
+ "level {level_index} must be sorted"
+ )));
+ }
+
+ for item in level {
+ if T::is_nan(item) {
+ return Err(Error::deserial("retained items must not be
NaN"));
+ }
+ if self.comparator.compare(item, min_item) == Ordering::Less
+ || self.comparator.compare(item, max_item) ==
Ordering::Greater
+ {
+ return Err(Error::deserial(
+ "retained items must be within the minimum and
maximum",
+ ));
+ }
+ }
+
+ if level_index + 1 < self.levels.len() {
+ level_weight = level_weight
+ .checked_mul(2)
+ .ok_or_else(|| Error::deserial("level weight overflow"))?;
+ }
+ }
+
+ if total_weight != self.n {
+ return Err(Error::deserial(format!(
+ "total sample weight {total_weight} does not match n {}",
+ self.n
+ )));
+ }
+
+ Ok(())
+ }
+}
+
+fn normalized_rank_error(k: u16, pmf: bool) -> f64 {
+ let k = k as f64;
+ if pmf {
+ 2.446 / k.powf(0.9433)
+ } else {
+ 2.296 / k.powf(0.9723)
+ }
+}
+
+fn downsample<T: KllItem>(items: Vec<T>, offset: bool, use_up: bool) -> Vec<T>
{
+ let len = items.len();
+ debug_assert!(len % 2 == 0, "length must be even");
+ let offset = usize::from(offset);
+ let parity = if use_up {
+ (len - 1 - offset) % 2
+ } else {
+ offset
+ };
+
+ items
+ .into_iter()
+ .enumerate()
+ .filter_map(|(idx, item)| if idx % 2 == parity { Some(item) } else {
None })
+ .collect()
+}
+
+fn take_leftover<T>(items: &mut Vec<T>, level: usize, is_level_zero_sorted:
bool) -> T {
+ if level == 0 && !is_level_zero_sorted {
+ items.pop().expect("odd level must not be empty")
+ } else {
+ items.remove(0)
+ }
+}
+
+fn merge_sorted_vec<T: KllItem, C: KllComparator<T>>(
+ left: Vec<T>,
+ right: Vec<T>,
+ comparator: &C,
+) -> Vec<T> {
+ let mut merged = Vec::with_capacity(left.len() + right.len());
+ let mut left_iter = left.into_iter().peekable();
+ let mut right_iter = right.into_iter().peekable();
+
+ while let (Some(l), Some(r)) = (left_iter.peek(), right_iter.peek()) {
+ if comparator.compare(l, r) == Ordering::Less {
+ merged.push(left_iter.next().unwrap());
+ } else {
+ merged.push(right_iter.next().unwrap());
+ }
+ }
+ merged.extend(left_iter);
+ merged.extend(right_iter);
+ merged
+}
+
+fn general_compress<T: KllItem, C: KllComparator<T>>(
+ mut levels_in: Vec<Vec<T>>,
+ k: u16,
+ m: u8,
+ is_level_zero_sorted: bool,
+ comparator: &C,
+) -> Vec<Vec<T>> {
+ let mut current_num_levels = levels_in.len();
+ let mut current_item_count: usize = levels_in.iter().map(|level|
level.len()).sum();
+ let mut target_item_count = compute_total_capacity(k, m,
current_num_levels) as usize;
+ let mut levels_out = Vec::with_capacity(current_num_levels + 1);
+
+ let mut current_level = 0usize;
+ while current_level < current_num_levels {
+ if current_level + 1 >= levels_in.len() {
+ levels_in.push(Vec::new());
+ }
+
+ let raw_pop = levels_in[current_level].len();
+ let cap = level_capacity(k, current_num_levels, current_level, m) as
usize;
+
+ if current_item_count < target_item_count || raw_pop < cap {
+ levels_out.push(std::mem::take(&mut levels_in[current_level]));
+ } else {
+ let mut current = std::mem::take(&mut levels_in[current_level]);
+ let mut above = std::mem::take(&mut levels_in[current_level + 1]);
+
+ let odd = current.len() % 2 == 1;
+ let mut leftover = None;
+ if odd {
+ leftover = Some(take_leftover(
+ &mut current,
+ current_level,
+ is_level_zero_sorted,
+ ));
+ }
+
+ if current_level == 0 && !is_level_zero_sorted {
+ current.sort_by(|left, right| comparator.compare(left, right));
+ }
+
+ let use_up = above.is_empty();
+ let promoted = downsample(current, rand::random::<bool>(), use_up);
+ let promoted_len = promoted.len();
+ if above.is_empty() {
+ above = promoted;
+ } else {
+ above = merge_sorted_vec(promoted, above, comparator);
+ }
+ levels_in[current_level + 1] = above;
+
+ let mut out_level = Vec::new();
+ if let Some(item) = leftover {
+ out_level.push(item);
+ }
+ levels_out.push(out_level);
+
+ current_item_count =
current_item_count.saturating_sub(promoted_len);
+
+ if current_level == current_num_levels - 1 {
+ current_num_levels += 1;
+ target_item_count += level_capacity(k, current_num_levels, 0,
m) as usize;
+ if levels_in.len() < current_num_levels + 1 {
+ levels_in.resize_with(current_num_levels + 1, Vec::new);
+ }
+ }
+ }
+ current_level += 1;
+ }
+
+ levels_out.truncate(current_num_levels);
+ levels_out
+}
+
+impl KllItem for f32 {
+ fn cmp(a: &Self, b: &Self) -> Ordering {
+ a.partial_cmp(b).unwrap_or(Ordering::Greater)
+ }
+
+ fn is_nan(value: &Self) -> bool {
+ value.is_nan()
+ }
+}
+
+impl KllSerde for f32 {
+ const MIN_SERIALIZED_SIZE: usize = 4;
+
+ fn serialized_size(_value: &Self) -> usize {
+ 4
+ }
+
+ fn serialize(value: &Self, bytes: &mut SketchBytes) {
+ bytes.write_f32_le(*value);
+ }
+
+ fn deserialize(input: &mut SketchSlice<'_>) -> Result<Self, Error> {
+ input
+ .read_f32_le()
+ .map_err(|_| Error::insufficient_data("f32"))
+ }
+}
+
+impl KllItem for f64 {
+ fn cmp(a: &Self, b: &Self) -> Ordering {
+ a.partial_cmp(b).unwrap_or(Ordering::Greater)
+ }
+
+ fn is_nan(value: &Self) -> bool {
+ value.is_nan()
+ }
+}
+
+impl KllSerde for f64 {
+ const MIN_SERIALIZED_SIZE: usize = 8;
+
+ fn serialized_size(_value: &Self) -> usize {
+ 8
+ }
+
+ fn serialize(value: &Self, bytes: &mut SketchBytes) {
+ bytes.write_f64_le(*value);
+ }
+
+ fn deserialize(input: &mut SketchSlice<'_>) -> Result<Self, Error> {
+ input
+ .read_f64_le()
+ .map_err(|_| Error::insufficient_data("f64"))
+ }
+}
+
+impl KllItem for i64 {
+ fn cmp(a: &Self, b: &Self) -> Ordering {
+ a.cmp(b)
+ }
+}
+
+impl KllSerde for i64 {
+ const MIN_SERIALIZED_SIZE: usize = 8;
+
+ fn serialized_size(_value: &Self) -> usize {
+ 8
+ }
+
+ fn serialize(value: &Self, bytes: &mut SketchBytes) {
+ bytes.write_i64_le(*value);
+ }
+
+ fn deserialize(input: &mut SketchSlice<'_>) -> Result<Self, Error> {
+ input
+ .read_i64_le()
+ .map_err(|_| Error::insufficient_data("i64"))
+ }
+}
+
+impl KllItem for String {
+ fn cmp(a: &Self, b: &Self) -> Ordering {
+ a.cmp(b)
+ }
+}
+
+impl KllSerde for String {
+ const MIN_SERIALIZED_SIZE: usize = 4;
+
+ fn serialized_size(value: &Self) -> usize {
+ 4 + value.len()
+ }
+
+ fn serialize(value: &Self, bytes: &mut SketchBytes) {
+ bytes.write_u32_le(value.len() as u32);
+ bytes.write(value.as_bytes());
+ }
+
+ fn deserialize(input: &mut SketchSlice<'_>) -> Result<Self, Error> {
+ let len = input
+ .read_u32_le()
+ .map_err(|_| Error::insufficient_data("string_len"))? as usize;
+ let bytes = input
+ .remaining()
+ .get(..len)
+ .ok_or_else(|| Error::insufficient_data("string_bytes"))?;
+ let value = std::str::from_utf8(bytes)
+ .map_err(|_| Error::deserial("invalid utf-8 string"))?
+ .to_owned();
+ input.advance(len as u64);
+ Ok(value)
+ }
+}
diff --git a/datasketches/src/kll/sorted_view.rs
b/datasketches/src/kll/sorted_view.rs
new file mode 100644
index 0000000..bcaad64
--- /dev/null
+++ b/datasketches/src/kll/sorted_view.rs
@@ -0,0 +1,203 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::cmp::Ordering;
+
+use super::sketch::KllComparator;
+use super::sketch::KllItem;
+
+#[derive(Debug, Clone)]
+pub(super) struct SortedView<T: KllItem, C: KllComparator<T>> {
+ comparator: C,
+ entries: Vec<Entry<T>>,
+ total_weight: u64,
+}
+
+#[derive(Debug, Clone)]
+struct Entry<T> {
+ item: T,
+ weight: u64,
+}
+
+impl<T: KllItem, C: KllComparator<T>> SortedView<T, C> {
+ fn new(mut entries: Vec<Entry<T>>, comparator: C) -> Self {
+ entries.sort_by(|a, b| comparator.compare(&a.item, &b.item));
+ let mut total_weight = 0u64;
+ for entry in &mut entries {
+ total_weight += entry.weight;
+ entry.weight = total_weight;
+ }
+ Self {
+ comparator,
+ entries,
+ total_weight,
+ }
+ }
+
+ pub(super) fn rank(&self, item: &T, inclusive: bool) -> f64 {
+ if self.entries.is_empty() {
+ return 0.0;
+ }
+
+ let idx = if inclusive {
+ upper_bound(&self.entries, item, &self.comparator)
+ } else {
+ lower_bound(&self.entries, item, &self.comparator)
+ };
+
+ if idx == 0 {
+ return 0.0;
+ }
+ let weight = self.entries[idx - 1].weight;
+ weight as f64 / self.total_weight as f64
+ }
+
+ pub(super) fn quantile(&self, rank: f64, inclusive: bool) -> T {
+ let weight = if inclusive {
+ (rank * self.total_weight as f64).ceil() as u64
+ } else {
+ (rank * self.total_weight as f64) as u64
+ };
+
+ let idx = if inclusive {
+ lower_bound_by_weight(&self.entries, weight)
+ } else {
+ upper_bound_by_weight(&self.entries, weight)
+ };
+
+ if idx >= self.entries.len() {
+ return self.entries[self.entries.len() - 1].item.clone();
+ }
+ self.entries[idx].item.clone()
+ }
+
+ pub(super) fn cdf(&self, split_points: &[T], inclusive: bool) -> Vec<f64> {
+ check_split_points(split_points, &self.comparator);
+ let mut ranks = Vec::with_capacity(split_points.len() + 1);
+ for item in split_points {
+ ranks.push(self.rank(item, inclusive));
+ }
+ ranks.push(1.0);
+ ranks
+ }
+
+ pub(super) fn pmf(&self, split_points: &[T], inclusive: bool) -> Vec<f64> {
+ let mut buckets = self.cdf(split_points, inclusive);
+ for i in (1..buckets.len()).rev() {
+ buckets[i] -= buckets[i - 1];
+ }
+ buckets
+ }
+}
+
+pub(super) fn build_sorted_view<T: KllItem, C: KllComparator<T>>(
+ levels: &[Vec<T>],
+ comparator: C,
+) -> SortedView<T, C> {
+ let num_retained: usize = levels.iter().map(|level| level.len()).sum();
+ let mut entries = Vec::with_capacity(num_retained);
+
+ for (level_idx, level) in levels.iter().enumerate() {
+ let weight = 1u64 << level_idx;
+ for item in level {
+ entries.push(Entry {
+ item: item.clone(),
+ weight,
+ });
+ }
+ }
+
+ SortedView::new(entries, comparator)
+}
+
+#[track_caller]
+fn check_split_points<T: KllItem, C: KllComparator<T>>(split_points: &[T],
comparator: &C) {
+ assert!(
+ split_points.iter().all(|point| !T::is_nan(point)),
+ "split_points must not contain NaN values"
+ );
+ for pair in split_points.windows(2) {
+ assert!(
+ comparator.compare(&pair[0], &pair[1]) == Ordering::Less,
+ "split_points must be unique and monotonically increasing"
+ );
+ }
+}
+
+fn lower_bound<T: KllItem, C: KllComparator<T>>(
+ entries: &[Entry<T>],
+ item: &T,
+ comparator: &C,
+) -> usize {
+ let mut left = 0usize;
+ let mut right = entries.len();
+ while left < right {
+ let mid = left + (right - left) / 2;
+ if comparator.compare(&entries[mid].item, item) == Ordering::Less {
+ left = mid + 1;
+ } else {
+ right = mid;
+ }
+ }
+ left
+}
+
+fn upper_bound<T: KllItem, C: KllComparator<T>>(
+ entries: &[Entry<T>],
+ item: &T,
+ comparator: &C,
+) -> usize {
+ let mut left = 0usize;
+ let mut right = entries.len();
+ while left < right {
+ let mid = left + (right - left) / 2;
+ if comparator.compare(&entries[mid].item, item) == Ordering::Greater {
+ right = mid;
+ } else {
+ left = mid + 1;
+ }
+ }
+ left
+}
+
+fn lower_bound_by_weight<T: KllItem>(entries: &[Entry<T>], weight: u64) ->
usize {
+ let mut left = 0usize;
+ let mut right = entries.len();
+ while left < right {
+ let mid = left + (right - left) / 2;
+ if entries[mid].weight < weight {
+ left = mid + 1;
+ } else {
+ right = mid;
+ }
+ }
+ left
+}
+
+fn upper_bound_by_weight<T: KllItem>(entries: &[Entry<T>], weight: u64) ->
usize {
+ let mut left = 0usize;
+ let mut right = entries.len();
+ while left < right {
+ let mid = left + (right - left) / 2;
+ if entries[mid].weight > weight {
+ right = mid;
+ } else {
+ left = mid + 1;
+ }
+ }
+ left
+}
diff --git a/datasketches/src/lib.rs b/datasketches/src/lib.rs
index 1f36e79..3dac470 100644
--- a/datasketches/src/lib.rs
+++ b/datasketches/src/lib.rs
@@ -78,6 +78,8 @@ pub mod cpc;
pub mod frequencies;
#[cfg(feature = "hll")]
pub mod hll;
+#[cfg(feature = "kll")]
+pub mod kll;
#[cfg(feature = "req")]
pub mod req;
#[cfg(feature = "tdigest")]
diff --git a/tests-integration/Cargo.toml b/tests-integration/Cargo.toml
index 0e1f808..79021d6 100644
--- a/tests-integration/Cargo.toml
+++ b/tests-integration/Cargo.toml
@@ -29,6 +29,7 @@ datasketches = { workspace = true, features = [
"cpc",
"frequencies",
"hll",
+ "kll",
"req",
"tdigest",
"theta",
diff --git a/datasketches/src/codec/mod.rs
b/tests-integration/tests/kll_test/main.rs
similarity index 52%
copy from datasketches/src/codec/mod.rs
copy to tests-integration/tests/kll_test/main.rs
index bd46688..825a628 100644
--- a/datasketches/src/codec/mod.rs
+++ b/tests-integration/tests/kll_test/main.rs
@@ -15,36 +15,4 @@
// specific language governing permissions and limitations
// under the License.
-//! Codec utilities for datasketches crate.
-
-mod decode;
-mod encode;
-pub use self::decode::SketchSlice;
-pub use self::encode::SketchBytes;
-
-#[cfg(any(
- feature = "bloom",
- feature = "countmin",
- feature = "cpc",
- feature = "frequencies",
- feature = "hll",
- feature = "req",
- feature = "tdigest",
- feature = "theta",
- feature = "tuple",
-))]
-#[allow(dead_code)] // some utilities are only used for certain sketches
-pub(crate) mod assert;
-
-#[cfg(any(
- feature = "bloom",
- feature = "countmin",
- feature = "cpc",
- feature = "frequencies",
- feature = "hll",
- feature = "req",
- feature = "tdigest",
- feature = "theta",
- feature = "tuple",
-))]
-pub(crate) mod family;
+mod sketch;
diff --git a/tests-integration/tests/kll_test/sketch.rs
b/tests-integration/tests/kll_test/sketch.rs
new file mode 100644
index 0000000..d9acaaa
--- /dev/null
+++ b/tests-integration/tests/kll_test/sketch.rs
@@ -0,0 +1,397 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::cmp::Ordering;
+
+use datasketches::error::ErrorKind;
+use datasketches::kll::DEFAULT_K;
+use datasketches::kll::KllComparator;
+use datasketches::kll::KllSketch;
+use datasketches::kll::MAX_K;
+use datasketches::kll::MIN_K;
+
+const NUMERIC_NOISE_TOLERANCE: f64 = 1e-6;
+
+fn assert_approx_eq(actual: f64, expected: f64, tolerance: f64) {
+ let delta = (actual - expected).abs();
+ assert!(
+ delta <= tolerance,
+ "expected {expected} +/- {tolerance}, got {actual}"
+ );
+}
+
+fn rank_eps(sketch: &KllSketch<f32>) -> f64 {
+ sketch.normalized_rank_error(false)
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+struct NumericStringOrder;
+
+impl KllComparator<String> for NumericStringOrder {
+ fn compare(&self, left: &String, right: &String) -> Ordering {
+ left.parse::<u64>()
+ .unwrap()
+ .cmp(&right.parse::<u64>().unwrap())
+ }
+}
+
+#[test]
+fn test_k_limits() {
+ let _min = KllSketch::<f32>::new(MIN_K).unwrap();
+ let _max = KllSketch::<f32>::new(MAX_K).unwrap();
+}
+
+#[test]
+fn test_k_too_small_returns_error() {
+ let error = KllSketch::<f32>::new(MIN_K - 1).unwrap_err();
+ assert_eq!(error.kind(), ErrorKind::InvalidArgument);
+}
+
+#[test]
+fn test_empty() {
+ let sketch = KllSketch::<f32>::new(DEFAULT_K).unwrap();
+ assert!(sketch.is_empty());
+ assert!(!sketch.is_estimation_mode());
+ assert_eq!(sketch.n(), 0);
+ assert_eq!(sketch.num_retained(), 0);
+ assert!(sketch.min_item().is_none());
+ assert!(sketch.max_item().is_none());
+ assert!(sketch.rank(&0.0, true).is_none());
+ assert!(sketch.quantile(0.5, true).is_none());
+ assert!(sketch.pmf(&[0.0f32], true).is_none());
+ assert!(sketch.cdf(&[0.0f32], true).is_none());
+}
+
+#[test]
+#[should_panic(expected = "rank must be in [0.0, 1.0]")]
+fn test_quantile_out_of_range_panics() {
+ let mut sketch = KllSketch::<f32>::new(DEFAULT_K).unwrap();
+ sketch.update(0.0);
+ sketch.quantile(-1.0, true);
+}
+
+#[test]
+fn test_one_item() {
+ let mut sketch = KllSketch::<f32>::new(DEFAULT_K).unwrap();
+ sketch.update(1.0);
+ assert!(!sketch.is_empty());
+ assert!(!sketch.is_estimation_mode());
+ assert_eq!(sketch.n(), 1);
+ assert_eq!(sketch.num_retained(), 1);
+ assert_eq!(sketch.rank(&1.0, false), Some(0.0));
+ assert_eq!(sketch.rank(&1.0, true), Some(1.0));
+ assert_eq!(sketch.rank(&2.0, false), Some(1.0));
+ assert_eq!(sketch.min_item().cloned(), Some(1.0));
+ assert_eq!(sketch.max_item().cloned(), Some(1.0));
+ assert_eq!(sketch.quantile(0.5, true), Some(1.0));
+}
+
+#[test]
+fn test_duplicate_items_follow_inclusive_and_exclusive_semantics() {
+ let mut sketch = KllSketch::<f32>::new(DEFAULT_K).unwrap();
+ for item in [1.0, 1.0, 2.0, 2.0] {
+ sketch.update(item);
+ }
+
+ assert_eq!(sketch.rank(&1.0, false), Some(0.0));
+ assert_eq!(sketch.rank(&1.0, true), Some(0.5));
+ assert_eq!(sketch.rank(&2.0, false), Some(0.5));
+ assert_eq!(sketch.rank(&2.0, true), Some(1.0));
+ assert_eq!(sketch.quantile(0.5, true), Some(1.0));
+ assert_eq!(sketch.quantile(0.5, false), Some(2.0));
+}
+
+#[test]
+fn test_nan_is_ignored() {
+ let mut sketch = KllSketch::<f32>::new(DEFAULT_K).unwrap();
+ sketch.update(f32::NAN);
+ assert!(sketch.is_empty());
+ sketch.update(0.0);
+ sketch.update(f32::NAN);
+ assert_eq!(sketch.n(), 1);
+}
+
+#[test]
+fn test_many_items_exact_mode() {
+ let mut sketch = KllSketch::<f32>::new(DEFAULT_K).unwrap();
+ let n = DEFAULT_K as usize;
+ for i in 1..=n {
+ sketch.update(i as f32);
+ assert_eq!(sketch.n(), i as u64);
+ }
+ assert!(!sketch.is_empty());
+ assert!(!sketch.is_estimation_mode());
+ assert_eq!(sketch.num_retained(), n);
+ assert_eq!(sketch.min_item().cloned(), Some(1.0));
+ assert_eq!(sketch.quantile(0.0, true), Some(1.0));
+ assert_eq!(sketch.max_item().cloned(), Some(n as f32));
+ assert_eq!(sketch.quantile(1.0, true), Some(n as f32));
+
+ for i in 1..=n {
+ let inclusive_rank = i as f64 / n as f64;
+ assert_eq!(sketch.rank(&(i as f32), true), Some(inclusive_rank));
+ let exclusive_rank = (i - 1) as f64 / n as f64;
+ assert_eq!(sketch.rank(&(i as f32), false), Some(exclusive_rank));
+ }
+}
+
+#[test]
+fn test_ten_items_quantiles() {
+ let mut sketch = KllSketch::<f32>::new(DEFAULT_K).unwrap();
+ for i in 1..=10 {
+ sketch.update(i as f32);
+ }
+ assert_eq!(sketch.quantile(0.0, true), Some(1.0));
+ assert_eq!(sketch.quantile(0.5, true), Some(5.0));
+ assert_eq!(sketch.quantile(0.99, true), Some(10.0));
+ assert_eq!(sketch.quantile(1.0, true), Some(10.0));
+}
+
+#[test]
+fn test_hundred_items_quantiles() {
+ let mut sketch = KllSketch::<f32>::new(DEFAULT_K).unwrap();
+ for i in 0..100 {
+ sketch.update(i as f32);
+ }
+ assert_eq!(sketch.quantile(0.0, true), Some(0.0));
+ assert_eq!(sketch.quantile(0.01, true), Some(0.0));
+ assert_eq!(sketch.quantile(0.5, true), Some(49.0));
+ assert_eq!(sketch.quantile(0.99, true), Some(98.0));
+ assert_eq!(sketch.quantile(1.0, true), Some(99.0));
+}
+
+#[test]
+fn test_many_items_estimation_mode_rank_error() {
+ let mut sketch = KllSketch::<f32>::new(DEFAULT_K).unwrap();
+ let n = 10_000;
+ for i in 0..n {
+ sketch.update(i as f32);
+ }
+ assert!(!sketch.is_empty());
+ assert!(sketch.is_estimation_mode());
+ assert_eq!(sketch.min_item().cloned(), Some(0.0));
+ assert_eq!(sketch.max_item().cloned(), Some((n - 1) as f32));
+
+ let rank_eps = rank_eps(&sketch);
+ for i in (0..n).step_by(10) {
+ let true_rank = i as f64 / n as f64;
+ let rank = sketch.rank(&(i as f32), false).unwrap();
+ assert_approx_eq(rank, true_rank, rank_eps);
+ }
+
+ assert!(sketch.num_retained() > 0);
+}
+
+#[test]
+fn test_rank_cdf_pmf_consistency() {
+ let mut sketch = KllSketch::<f32>::new(DEFAULT_K).unwrap();
+ let n = 200;
+ let mut values = Vec::with_capacity(n);
+ for i in 0..n {
+ sketch.update(i as f32);
+ values.push(i as f32);
+ }
+
+ let ranks = sketch.cdf(&values, false).unwrap();
+ let pmf = sketch.pmf(&values, false).unwrap();
+
+ let mut subtotal = 0.0;
+ for i in 0..n {
+ let rank = sketch.rank(&values[i], false).unwrap();
+ assert_eq!(rank, ranks[i]);
+ subtotal += pmf[i];
+ assert!(
+ (ranks[i] - subtotal).abs() <= NUMERIC_NOISE_TOLERANCE,
+ "cdf vs pmf mismatch at index {i}"
+ );
+ }
+
+ let ranks = sketch.cdf(&values, true).unwrap();
+ let pmf = sketch.pmf(&values, true).unwrap();
+
+ let mut subtotal = 0.0;
+ for i in 0..n {
+ let rank = sketch.rank(&values[i], true).unwrap();
+ assert_eq!(rank, ranks[i]);
+ subtotal += pmf[i];
+ assert!(
+ (ranks[i] - subtotal).abs() <= NUMERIC_NOISE_TOLERANCE,
+ "cdf vs pmf mismatch at index {i}"
+ );
+ }
+}
+
+#[test]
+#[should_panic(expected = "split_points must be unique and monotonically
increasing")]
+fn test_out_of_order_split_points_panics() {
+ let mut sketch = KllSketch::<f32>::new(DEFAULT_K).unwrap();
+ sketch.update(0.0);
+ let split_points = [1.0, 0.0];
+ let _ = sketch.cdf(&split_points, true);
+}
+
+#[test]
+#[should_panic(expected = "split_points must not contain NaN values")]
+fn test_nan_split_point_panics() {
+ let mut sketch = KllSketch::<f32>::new(DEFAULT_K).unwrap();
+ sketch.update(0.0);
+ let split_points = [f32::NAN];
+ let _ = sketch.cdf(&split_points, true);
+}
+
+#[test]
+fn test_merge() {
+ let mut sketch1 = KllSketch::<f32>::new(DEFAULT_K).unwrap();
+ let mut sketch2 = KllSketch::<f32>::new(DEFAULT_K).unwrap();
+ let n = 10_000;
+ for i in 0..n {
+ sketch1.update(i as f32);
+ sketch2.update((2 * n - i - 1) as f32);
+ }
+
+ assert_eq!(sketch1.min_item().cloned(), Some(0.0));
+ assert_eq!(sketch1.max_item().cloned(), Some((n - 1) as f32));
+ assert_eq!(sketch2.min_item().cloned(), Some(n as f32));
+ assert_eq!(sketch2.max_item().cloned(), Some((2 * n - 1) as f32));
+
+ sketch1.merge(&sketch2);
+
+ assert!(!sketch1.is_empty());
+ assert_eq!(sketch1.n(), (2 * n) as u64);
+ assert_eq!(sketch1.min_item().cloned(), Some(0.0));
+ assert_eq!(sketch1.max_item().cloned(), Some((2 * n - 1) as f32));
+ let median = sketch1.quantile(0.5, true).unwrap();
+ let rank_eps = rank_eps(&sketch1);
+ assert_approx_eq(median as f64, n as f64, n as f64 * rank_eps);
+}
+
+#[test]
+fn test_merge_lower_k() {
+ let mut sketch1 = KllSketch::<f32>::new(256).unwrap();
+ let mut sketch2 = KllSketch::<f32>::new(128).unwrap();
+ let n = 10_000;
+ for i in 0..n {
+ sketch1.update(i as f32);
+ sketch2.update((2 * n - i - 1) as f32);
+ }
+
+ sketch1.merge(&sketch2);
+
+ assert_eq!(sketch1.n(), (2 * n) as u64);
+ assert_eq!(sketch1.min_item().cloned(), Some(0.0));
+ assert_eq!(sketch1.max_item().cloned(), Some((2 * n - 1) as f32));
+ assert_eq!(
+ sketch1.normalized_rank_error(false),
+ sketch2.normalized_rank_error(false)
+ );
+ assert_eq!(
+ sketch1.normalized_rank_error(true),
+ sketch2.normalized_rank_error(true)
+ );
+ let median = sketch1.quantile(0.5, true).unwrap();
+ let rank_eps = rank_eps(&sketch1);
+ assert_approx_eq(median as f64, n as f64, n as f64 * rank_eps);
+}
+
+#[test]
+fn test_merge_exact_mode_lower_k() {
+ let mut sketch1 = KllSketch::<f32>::new(256).unwrap();
+ let sketch2 = KllSketch::<f32>::new(128).unwrap();
+ let n = 10_000;
+ for i in 0..n {
+ sketch1.update(i as f32);
+ }
+
+ let err_before = sketch1.normalized_rank_error(true);
+ sketch1.merge(&sketch2);
+ assert_eq!(sketch1.normalized_rank_error(true), err_before);
+
+ assert_eq!(sketch1.n(), n as u64);
+ assert_eq!(sketch1.min_item().cloned(), Some(0.0));
+ assert_eq!(sketch1.max_item().cloned(), Some((n - 1) as f32));
+ let median = sketch1.quantile(0.5, true).unwrap();
+ let rank_eps = rank_eps(&sketch1);
+ assert_approx_eq(median as f64, (n / 2) as f64, (n as f64 / 2.0) *
rank_eps);
+}
+
+#[test]
+fn test_merge_min_max_from_other() {
+ let mut sketch1 = KllSketch::<f32>::new(DEFAULT_K).unwrap();
+ let mut sketch2 = KllSketch::<f32>::new(DEFAULT_K).unwrap();
+ sketch1.update(1.0);
+ sketch2.update(2.0);
+ sketch2.merge(&sketch1);
+ assert_eq!(sketch2.min_item().cloned(), Some(1.0));
+ assert_eq!(sketch2.max_item().cloned(), Some(2.0));
+}
+
+#[test]
+fn test_merge_min_max_large_other() {
+ let mut sketch1 = KllSketch::<f32>::new(DEFAULT_K).unwrap();
+ for i in 0..1_000_000 {
+ sketch1.update(i as f32);
+ }
+ let mut sketch2 = KllSketch::<f32>::new(DEFAULT_K).unwrap();
+ sketch2.merge(&sketch1);
+ assert_eq!(sketch2.min_item().cloned(), Some(0.0));
+ assert_eq!(sketch2.max_item().cloned(), Some(999_999.0));
+}
+
+#[test]
+fn test_reset_retains_configuration() {
+ let mut sketch = KllSketch::<f32>::new(64).unwrap();
+ for i in 0..10_000 {
+ sketch.update(i as f32);
+ }
+ assert!(sketch.is_estimation_mode());
+
+ sketch.reset();
+
+ assert_eq!(sketch.k(), 64);
+ assert_eq!(sketch.min_k(), 64);
+ assert!(sketch.is_empty());
+ assert!(!sketch.is_estimation_mode());
+ assert_eq!(sketch.n(), 0);
+ assert_eq!(sketch.num_retained(), 0);
+ assert_eq!(sketch.min_item(), None);
+ assert_eq!(sketch.max_item(), None);
+}
+
+#[test]
+fn test_custom_comparator_roundtrip() {
+ let mut sketch =
+ KllSketch::<String, NumericStringOrder>::new_with_comparator(200,
NumericStringOrder)
+ .unwrap();
+ for item in ["2", "10", "1"] {
+ sketch.update(item.to_owned());
+ }
+
+ assert_eq!(sketch.min_item().map(String::as_str), Some("1"));
+ assert_eq!(sketch.max_item().map(String::as_str), Some("10"));
+ assert_eq!(sketch.quantile(0.5, true).as_deref(), Some("2"));
+
+ let bytes = sketch.serialize();
+ let decoded = KllSketch::<String,
NumericStringOrder>::deserialize_with_comparator(
+ &bytes,
+ NumericStringOrder,
+ )
+ .unwrap();
+ assert_eq!(decoded.n(), sketch.n());
+ assert_eq!(decoded.min_item().map(String::as_str), Some("1"));
+ assert_eq!(decoded.max_item().map(String::as_str), Some("10"));
+ assert_eq!(decoded.quantile(0.5, true).as_deref(), Some("2"));
+}
diff --git a/tests-integration/tests/serde_tests.rs
b/tests-integration/tests/serde_tests.rs
index 7120877..bd72316 100644
--- a/tests-integration/tests/serde_tests.rs
+++ b/tests-integration/tests/serde_tests.rs
@@ -57,6 +57,9 @@ mod frequencies;
#[path = "serde_tests/hll.rs"]
mod hll;
+#[path = "serde_tests/kll.rs"]
+mod kll;
+
#[path = "serde_tests/req.rs"]
mod req;
diff --git a/tests-integration/tests/serde_tests/kll.rs
b/tests-integration/tests/serde_tests/kll.rs
new file mode 100644
index 0000000..70d9249
--- /dev/null
+++ b/tests-integration/tests/serde_tests/kll.rs
@@ -0,0 +1,337 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! KLL Sketch Serialization Compatibility Tests
+//!
+//! These tests verify binary compatibility with Apache DataSketches
implementations:
+//! - Java (datasketches-java)
+//! - C++ (datasketches-cpp)
+//!
+//! Test data is generated by the reference implementations and stored in:
+//! `tests/serde_tests/{java_generated_files,cpp_generated_files}/`.
+
+use std::cmp::Ordering;
+use std::fs;
+use std::path::PathBuf;
+
+use datasketches::kll::DEFAULT_K;
+use datasketches::kll::KllComparator;
+use datasketches::kll::KllSketch;
+
+use crate::serialization_test_data;
+
+#[derive(Clone, Copy)]
+struct NumericStringOrder;
+
+impl KllComparator<String> for NumericStringOrder {
+ fn compare(&self, left: &String, right: &String) -> Ordering {
+ parse_string_value(left).cmp(&parse_string_value(right))
+ }
+}
+
+fn test_f32_file(path: PathBuf, expected_n: usize) {
+ let bytes = fs::read(&path).unwrap();
+ let sketch = KllSketch::<f32>::deserialize(&bytes)
+ .unwrap_or_else(|error| panic!("{}: {error}", path.display()));
+
+ assert_eq!(sketch.k(), DEFAULT_K, "wrong k in {}", path.display());
+ assert_eq!(
+ sketch.n() as usize,
+ expected_n,
+ "wrong n in {}",
+ path.display()
+ );
+ assert_eq!(
+ sketch.is_estimation_mode(),
+ expected_n > DEFAULT_K as usize,
+ "wrong estimation mode in {}",
+ path.display()
+ );
+ assert_eq!(
+ sketch.is_empty(),
+ expected_n == 0,
+ "wrong empty flag in {}",
+ path.display()
+ );
+
+ if expected_n == 0 {
+ assert!(sketch.min_item().is_none(), "min should be None");
+ assert!(sketch.max_item().is_none(), "max should be None");
+ } else {
+ assert_eq!(
+ sketch.min_item().cloned(),
+ Some(1.0),
+ "min item mismatch in {}",
+ path.display()
+ );
+ assert_eq!(
+ sketch.max_item().cloned(),
+ Some(expected_n as f32),
+ "max item mismatch in {}",
+ path.display()
+ );
+ }
+
+ assert_eq!(sketch.serialize(), bytes, "roundtrip: {}", path.display());
+}
+
+fn test_f64_file(path: PathBuf, expected_n: usize) {
+ let bytes = fs::read(&path).unwrap();
+ let sketch = KllSketch::<f64>::deserialize(&bytes)
+ .unwrap_or_else(|error| panic!("{}: {error}", path.display()));
+
+ assert_eq!(sketch.k(), DEFAULT_K, "wrong k in {}", path.display());
+ assert_eq!(
+ sketch.n() as usize,
+ expected_n,
+ "wrong n in {}",
+ path.display()
+ );
+ assert_eq!(
+ sketch.is_estimation_mode(),
+ expected_n > DEFAULT_K as usize,
+ "wrong estimation mode in {}",
+ path.display()
+ );
+ assert_eq!(
+ sketch.is_empty(),
+ expected_n == 0,
+ "wrong empty flag in {}",
+ path.display()
+ );
+
+ if expected_n == 0 {
+ assert!(sketch.min_item().is_none(), "min should be None");
+ assert!(sketch.max_item().is_none(), "max should be None");
+ } else {
+ assert_eq!(
+ sketch.min_item().cloned(),
+ Some(1.0),
+ "min item mismatch in {}",
+ path.display()
+ );
+ assert_eq!(
+ sketch.max_item().cloned(),
+ Some(expected_n as f64),
+ "max item mismatch in {}",
+ path.display()
+ );
+ }
+
+ assert_eq!(sketch.serialize(), bytes, "roundtrip: {}", path.display());
+}
+
+fn test_i64_file(path: PathBuf, expected_n: usize) {
+ let bytes = fs::read(&path).unwrap();
+ let sketch = KllSketch::<i64>::deserialize(&bytes)
+ .unwrap_or_else(|error| panic!("{}: {error}", path.display()));
+
+ assert_eq!(sketch.k(), DEFAULT_K, "wrong k in {}", path.display());
+ assert_eq!(
+ sketch.n() as usize,
+ expected_n,
+ "wrong n in {}",
+ path.display()
+ );
+ assert_eq!(
+ sketch.is_estimation_mode(),
+ expected_n > DEFAULT_K as usize,
+ "wrong estimation mode in {}",
+ path.display()
+ );
+ assert_eq!(
+ sketch.is_empty(),
+ expected_n == 0,
+ "wrong empty flag in {}",
+ path.display()
+ );
+
+ if expected_n == 0 {
+ assert!(sketch.min_item().is_none(), "min should be None");
+ assert!(sketch.max_item().is_none(), "max should be None");
+ } else {
+ assert_eq!(
+ sketch.min_item().cloned(),
+ Some(1),
+ "min item mismatch in {}",
+ path.display()
+ );
+ assert_eq!(
+ sketch.max_item().cloned(),
+ Some(expected_n as i64),
+ "max item mismatch in {}",
+ path.display()
+ );
+ }
+
+ assert_eq!(sketch.serialize(), bytes, "roundtrip: {}", path.display());
+}
+
+fn parse_string_value(value: &str) -> u64 {
+ value
+ .trim_start()
+ .parse::<u64>()
+ .expect("string value should be numeric")
+}
+
+fn test_string_file(path: PathBuf, expected_n: usize) {
+ let bytes = fs::read(&path).unwrap();
+ let sketch = KllSketch::<String,
NumericStringOrder>::deserialize_with_comparator(
+ &bytes,
+ NumericStringOrder,
+ )
+ .unwrap_or_else(|error| panic!("{}: {error}", path.display()));
+
+ assert_eq!(sketch.k(), DEFAULT_K, "wrong k in {}", path.display());
+ assert_eq!(
+ sketch.n() as usize,
+ expected_n,
+ "wrong n in {}",
+ path.display()
+ );
+ assert_eq!(
+ sketch.is_estimation_mode(),
+ expected_n > DEFAULT_K as usize,
+ "wrong estimation mode in {}",
+ path.display()
+ );
+ assert_eq!(
+ sketch.is_empty(),
+ expected_n == 0,
+ "wrong empty flag in {}",
+ path.display()
+ );
+
+ if expected_n == 0 {
+ assert!(sketch.min_item().is_none(), "min should be None");
+ assert!(sketch.max_item().is_none(), "max should be None");
+ } else {
+ let min_item = sketch.min_item().expect("missing min item");
+ let max_item = sketch.max_item().expect("missing max item");
+ assert_eq!(
+ parse_string_value(min_item),
+ 1,
+ "min item mismatch in {}",
+ path.display()
+ );
+ assert_eq!(
+ parse_string_value(max_item),
+ expected_n as u64,
+ "max item mismatch in {}",
+ path.display()
+ );
+ }
+
+ assert_eq!(sketch.serialize(), bytes, "roundtrip: {}", path.display());
+}
+
+#[test]
+fn test_java_kll_float_compatibility() {
+ let test_cases = [0, 1, 10, 100, 1000, 10000, 100000, 1000000];
+ for n in test_cases {
+ let filename = format!("kll_float_n{n}_java.sk");
+ let path = serialization_test_data("java_generated_files", &filename);
+ test_f32_file(path, n);
+ }
+}
+
+#[test]
+fn test_java_kll_double_compatibility() {
+ let test_cases = [0, 1, 10, 100, 1000, 10000, 100000, 1000000];
+ for n in test_cases {
+ let filename = format!("kll_double_n{n}_java.sk");
+ let path = serialization_test_data("java_generated_files", &filename);
+ test_f64_file(path, n);
+ }
+}
+
+#[test]
+fn test_java_kll_long_compatibility() {
+ let test_cases = [0, 1, 10, 100, 1000, 10000, 100000, 1000000];
+ for n in test_cases {
+ let filename = format!("kll_long_n{n}_java.sk");
+ let path = serialization_test_data("java_generated_files", &filename);
+ test_i64_file(path, n);
+ }
+}
+
+#[test]
+fn test_java_kll_string_compatibility() {
+ let test_cases = [0, 1, 10, 100, 1000, 10000, 100000, 1000000];
+ for n in test_cases {
+ let filename = format!("kll_string_n{n}_java.sk");
+ let path = serialization_test_data("java_generated_files", &filename);
+ test_string_file(path, n);
+ }
+}
+
+#[test]
+fn test_cpp_kll_float_compatibility() {
+ let test_cases = [0, 1, 10, 100, 1000, 10000, 100000, 1000000];
+ for n in test_cases {
+ let filename = format!("kll_float_n{n}_cpp.sk");
+ let path = serialization_test_data("cpp_generated_files", &filename);
+ test_f32_file(path, n);
+ }
+}
+
+#[test]
+fn test_cpp_kll_double_compatibility() {
+ let test_cases = [0, 1, 10, 100, 1000, 10000, 100000, 1000000];
+ for n in test_cases {
+ let filename = format!("kll_double_n{n}_cpp.sk");
+ let path = serialization_test_data("cpp_generated_files", &filename);
+ test_f64_file(path, n);
+ }
+}
+
+#[test]
+fn test_cpp_kll_long_compatibility() {
+ let test_cases = [0, 1, 10, 100, 1000, 10000, 100000, 1000000];
+ for n in test_cases {
+ let filename = format!("kll_long_n{n}_cpp.sk");
+ let path = serialization_test_data("cpp_generated_files", &filename);
+ test_i64_file(path, n);
+ }
+}
+
+#[test]
+fn test_cpp_kll_string_compatibility() {
+ let test_cases = [0, 1, 10, 100, 1000, 10000, 100000, 1000000];
+ for n in test_cases {
+ let filename = format!("kll_string_n{n}_cpp.sk");
+ let path = serialization_test_data("cpp_generated_files", &filename);
+ test_string_file(path, n);
+ }
+}
+
+#[test]
+fn test_rejects_truncated_or_trailing_data() {
+ let mut sketch = KllSketch::<f32>::default();
+ for value in 0..1_000 {
+ sketch.update(value as f32);
+ }
+ let bytes = sketch.serialize();
+
+ for length in [7, 15, bytes.len() - 1] {
+ assert!(KllSketch::<f32>::deserialize(&bytes[..length]).is_err());
+ }
+
+ let mut with_trailing_data = bytes;
+ with_trailing_data.push(0);
+ assert!(KllSketch::<f32>::deserialize(&with_trailing_data).is_err());
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]