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 04b1431 fix(hll): preserve estimator state across union
transformations (#239)
04b1431 is described below
commit 04b143174a92ea78d378731840207d243ac0ba03
Author: Jaideep Pyne <[email protected]>
AuthorDate: Sat Aug 29 13:21:22 2026 +0530
fix(hll): preserve estimator state across union transformations (#239)
Co-authored-by: Claude Opus 4.8 <[email protected]>
Co-authored-by: tison <[email protected]>
---
CHANGELOG.md | 1 +
datasketches/src/hll/array4.rs | 37 ++--
datasketches/src/hll/array6.rs | 37 ++--
datasketches/src/hll/array8.rs | 64 +++---
datasketches/src/hll/composite_interpolation.rs | 7 +-
datasketches/src/hll/estimator.rs | 249 +++++++++++++-----------
datasketches/src/hll/serialization.rs | 2 +-
datasketches/src/hll/sketch.rs | 7 +-
datasketches/src/hll/union.rs | 45 ++---
tests-integration/tests/hll_test/bounds.rs | 2 +-
tests-integration/tests/hll_test/union.rs | 81 +++++---
11 files changed, 279 insertions(+), 253 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 67d8965..8dd97ec 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -29,6 +29,7 @@ All significant changes to this project will be documented in
this file.
* T-Digest rejects truncated serialized payloads before allocating, and
updating a deserialized digest no longer allows its buffered state to grow
without bound.
* Compact HLL4 images now restore all register values correctly.
* `HllSketch::lower_bound` now uses the number of non-zero registers as a
floor in HLL mode, matching Java, C++, and Go and avoiding a bound below the
distinct count already proven by register hits.
+* `HllUnion` now keeps a single HLL-mode input's estimate stable when copying
or downsampling it and keeps confidence bounds consistent across HLL4, HLL6,
and HLL8 result types, matching Java and C++.
* HLL, Theta, and Tuple deserializers now return `InvalidData` for malformed
payload sizes and entry counts instead of risking oversized allocations or
decoding failures.
* Malformed CPC images now return `InvalidData` instead of panicking.
* Seeded deserializers now return `InvalidData` rather than panicking when the
caller supplies a seed whose hash is the reserved zero value.
diff --git a/datasketches/src/hll/array4.rs b/datasketches/src/hll/array4.rs
index 17327fb..a076aa1 100644
--- a/datasketches/src/hll/array4.rs
+++ b/datasketches/src/hll/array4.rs
@@ -28,7 +28,8 @@ use crate::common::NumStdDev;
use crate::error::Error;
use crate::hll::Coupon;
use crate::hll::aux_map::AuxMap;
-use crate::hll::estimator::HipEstimator;
+use crate::hll::estimator::EstimateState;
+use crate::hll::estimator::Estimator;
use crate::hll::serialization::COMPACT_FLAG_MASK;
use crate::hll::serialization::COUPON_SIZE_BYTES;
use crate::hll::serialization::CUR_MODE_HLL;
@@ -70,8 +71,7 @@ pub struct Array4 {
num_at_cur_min: u32,
/// Exception table for values >= 15 after cur_min offset
aux_map: Option<AuxMap>,
- /// HIP estimator for cardinality estimation
- estimator: HipEstimator,
+ estimator: Estimator,
}
impl Array4 {
@@ -84,7 +84,7 @@ impl Array4 {
cur_min: 0,
num_at_cur_min,
aux_map: None,
- estimator: HipEstimator::new(lg_config_k),
+ estimator: Estimator::new(lg_config_k),
}
}
@@ -125,9 +125,9 @@ impl Array4 {
1 << self.lg_config_k
}
- /// Get the current HIP accumulator value
- pub(super) fn hip_accum(&self) -> f64 {
- self.estimator.hip_accum()
+ /// Returns the estimate state independently from register-derived cached
values.
+ pub(super) fn estimate_state(&self) -> EstimateState {
+ self.estimator.estimate_state()
}
/// Set raw 4-bit value in slot
@@ -270,7 +270,7 @@ impl Array4 {
self.num_at_cur_min = num_at_new;
}
- /// Get the current cardinality estimate using HIP estimator
+ /// Returns the current cardinality estimate.
pub fn estimate(&self) -> f64 {
// Array4 tracks cur_min and num_at_cur_min dynamically
self.estimator
@@ -297,11 +297,9 @@ impl Array4 {
)
}
- /// Set the HIP accumulator value
- ///
- /// This is used when promoting from coupon modes to carry forward the
estimate
- pub fn set_hip_accum(&mut self, value: f64) {
- self.estimator.set_hip_accum(value);
+ /// Restores estimate state after copying or transforming the same logical
sketch.
+ pub(super) fn restore_estimate_state(&mut self, state: EstimateState) {
+ self.estimator.restore_estimate_state(state);
}
/// Check if the sketch is empty (all slots are zero)
@@ -322,7 +320,7 @@ impl Array4 {
let k = 1usize << lg_config_k;
let num_bytes = 1usize << (lg_config_k - 1); // k/2 bytes for 4-bit
packing
- // Read HIP estimator values from preamble
+ // Read estimator values from preamble
let hip_accum = cursor
.read_f64_le()
.map_err(insufficient_data("hip_accum"))?;
@@ -404,12 +402,7 @@ impl Array4 {
aux_map = Some(aux);
}
- // Create estimator and restore state
- let mut estimator = HipEstimator::new(lg_config_k);
- estimator.set_hip_accum(hip_accum);
- estimator.set_kxq0(kxq0);
- estimator.set_kxq1(kxq1);
- estimator.set_out_of_order(ooo);
+ let estimator = Estimator::from_serialized(hip_accum, kxq0, kxq1, ooo);
Ok(Self {
lg_config_k,
@@ -449,7 +442,7 @@ impl Array4 {
// COMPACT_FLAG_MASK is always set: aux map entries are written as a
compact sequential
// list of populated entries only.
let mut flags = COMPACT_FLAG_MASK;
- if self.estimator.is_out_of_order() {
+ if self.estimator.uses_composite_estimate() {
flags |= OUT_OF_ORDER_FLAG_MASK;
}
bytes.write_u8(flags);
@@ -460,7 +453,7 @@ impl Array4 {
// Mode byte: HLL mode with HLL4 type
bytes.write_u8(encode_mode_byte(CUR_MODE_HLL, TGT_HLL4));
- // Write HIP estimator values
+ // Write estimator values
bytes.write_f64_le(self.estimator.hip_accum());
bytes.write_f64_le(self.estimator.kxq0());
bytes.write_f64_le(self.estimator.kxq1());
diff --git a/datasketches/src/hll/array6.rs b/datasketches/src/hll/array6.rs
index 69bfa22..5ab5657 100644
--- a/datasketches/src/hll/array6.rs
+++ b/datasketches/src/hll/array6.rs
@@ -28,7 +28,8 @@ use crate::codec::family::Family;
use crate::common::NumStdDev;
use crate::error::Error;
use crate::hll::Coupon;
-use crate::hll::estimator::HipEstimator;
+use crate::hll::estimator::EstimateState;
+use crate::hll::estimator::Estimator;
use crate::hll::serialization::CUR_MODE_HLL;
use crate::hll::serialization::HLL_PREAMBLE_SIZE;
use crate::hll::serialization::HLL_PREINTS;
@@ -47,8 +48,7 @@ pub struct Array6 {
bytes: Box<[u8]>,
/// Count of slots with value 0
num_zeros: u32,
- /// HIP estimator for cardinality estimation
- estimator: HipEstimator,
+ estimator: Estimator,
}
impl Array6 {
@@ -60,7 +60,7 @@ impl Array6 {
lg_config_k,
bytes: vec![0u8; num_bytes].into_boxed_slice(),
num_zeros: k,
- estimator: HipEstimator::new(lg_config_k),
+ estimator: Estimator::new(lg_config_k),
}
}
@@ -91,9 +91,9 @@ impl Array6 {
1 << self.lg_config_k
}
- /// Get the current HIP accumulator value
- pub(super) fn hip_accum(&self) -> f64 {
- self.estimator.hip_accum()
+ /// Returns the estimate state independently from register-derived cached
values.
+ pub(super) fn estimate_state(&self) -> EstimateState {
+ self.estimator.estimate_state()
}
/// Set value in a slot (6-bit value)
@@ -145,7 +145,7 @@ impl Array6 {
}
}
- /// Get the current cardinality estimate using HIP estimator
+ /// Returns the current cardinality estimate.
pub fn estimate(&self) -> f64 {
// Array6 doesn't use cur_min (always 0), so num_at_cur_min = num_zeros
self.estimator.estimate(self.lg_config_k, 0, self.num_zeros)
@@ -163,11 +163,9 @@ impl Array6 {
.lower_bound(self.lg_config_k, 0, self.num_zeros, num_std_dev)
}
- /// Set the HIP accumulator value
- ///
- /// This is used when promoting from coupon modes to carry forward the
estimate
- pub fn set_hip_accum(&mut self, value: f64) {
- self.estimator.set_hip_accum(value);
+ /// Restores estimate state after copying or transforming the same logical
sketch.
+ pub(super) fn restore_estimate_state(&mut self, state: EstimateState) {
+ self.estimator.restore_estimate_state(state);
}
/// Check if the sketch is empty (all slots are zero)
@@ -186,7 +184,7 @@ impl Array6 {
let k = 1 << lg_config_k;
let num_bytes = num_bytes_for_k(k);
- // Read HIP estimator values from preamble
+ // Read estimator values from preamble
let hip_accum = cursor
.read_f64_le()
.map_err(insufficient_data("hip_accum"))?;
@@ -218,12 +216,7 @@ impl Array6 {
.read_exact(&mut data)
.map_err(insufficient_data("data"))?;
- // Create estimator and restore state
- let mut estimator = HipEstimator::new(lg_config_k);
- estimator.set_hip_accum(hip_accum);
- estimator.set_kxq0(kxq0);
- estimator.set_kxq1(kxq1);
- estimator.set_out_of_order(ooo);
+ let estimator = Estimator::from_serialized(hip_accum, kxq0, kxq1, ooo);
Ok(Self {
lg_config_k,
@@ -251,7 +244,7 @@ impl Array6 {
// Write flags
let mut flags = 0u8;
- if self.estimator.is_out_of_order() {
+ if self.estimator.uses_composite_estimate() {
flags |= OUT_OF_ORDER_FLAG_MASK;
}
bytes.write_u8(flags);
@@ -262,7 +255,7 @@ impl Array6 {
// Mode byte: HLL mode with HLL6 type
bytes.write_u8(encode_mode_byte(CUR_MODE_HLL, TGT_HLL6));
- // Write HIP estimator values
+ // Write estimator values
bytes.write_f64_le(self.estimator.hip_accum());
bytes.write_f64_le(self.estimator.kxq0());
bytes.write_f64_le(self.estimator.kxq1());
diff --git a/datasketches/src/hll/array8.rs b/datasketches/src/hll/array8.rs
index 0cc1e2a..8a3b61c 100644
--- a/datasketches/src/hll/array8.rs
+++ b/datasketches/src/hll/array8.rs
@@ -27,7 +27,8 @@ use crate::codec::family::Family;
use crate::common::NumStdDev;
use crate::error::Error;
use crate::hll::Coupon;
-use crate::hll::estimator::HipEstimator;
+use crate::hll::estimator::EstimateState;
+use crate::hll::estimator::Estimator;
use crate::hll::serialization::CUR_MODE_HLL;
use crate::hll::serialization::HLL_PREAMBLE_SIZE;
use crate::hll::serialization::HLL_PREINTS;
@@ -44,8 +45,7 @@ pub struct Array8 {
bytes: Box<[u8]>,
/// Count of slots with value 0
num_zeros: u32,
- /// HIP estimator for cardinality estimation
- estimator: HipEstimator,
+ estimator: Estimator,
}
impl Array8 {
@@ -56,7 +56,7 @@ impl Array8 {
lg_config_k,
bytes: vec![0u8; k as usize].into_boxed_slice(),
num_zeros: k,
- estimator: HipEstimator::new(lg_config_k),
+ estimator: Estimator::new(lg_config_k),
}
}
@@ -99,7 +99,7 @@ impl Array8 {
}
}
- /// Get the current cardinality estimate using HIP estimator
+ /// Returns the current cardinality estimate.
pub fn estimate(&self) -> f64 {
// Array8 doesn't use cur_min (always 0), so num_at_cur_min = num_zeros
self.estimator.estimate(self.lg_config_k, 0, self.num_zeros)
@@ -117,13 +117,6 @@ impl Array8 {
.lower_bound(self.lg_config_k, 0, self.num_zeros, num_std_dev)
}
- /// Set the HIP accumulator value
- ///
- /// This is used when promoting from coupon modes to carry forward the
estimate
- pub fn set_hip_accum(&mut self, value: f64) {
- self.estimator.set_hip_accum(value);
- }
-
/// Check if the sketch is empty (all slots are zero)
pub fn is_empty(&self) -> bool {
self.num_zeros == (1 << self.lg_config_k)
@@ -139,9 +132,14 @@ impl Array8 {
1 << self.lg_config_k
}
- /// Get the current HIP accumulator value
- pub(super) fn hip_accum(&self) -> f64 {
- self.estimator.hip_accum()
+ /// Returns the estimate state independently from register-derived cached
values.
+ pub(super) fn estimate_state(&self) -> EstimateState {
+ self.estimator.estimate_state()
+ }
+
+ /// Restores estimate state after copying or transforming the same logical
sketch.
+ pub(super) fn restore_estimate_state(&mut self, state: EstimateState) {
+ self.estimator.restore_estimate_state(state);
}
/// Directly set a register value
@@ -154,17 +152,17 @@ impl Array8 {
/// Rebuild estimator state from current register values
///
- /// Recomputes num_zeros, kxq0, kxq1, and marks estimator as out-of-order.
+ /// Recomputes num_zeros, kxq0, and kxq1, then switches to composite
estimation.
/// Should be called after bulk register modifications.
pub(super) fn rebuild_estimator_from_registers(&mut self) {
self.rebuild_cached_values();
- self.estimator.set_out_of_order(true);
+ self.estimator.invalidate_hip();
}
/// Merge another Array8 with the same lg_k
///
- /// Performs register-by-register max merge. Marks estimator as
- /// out-of-order since HIP cannot be maintained during bulk operations.
+ /// Performs register-by-register max merge. HIP is invalidated because
the register update
+ /// order is unavailable.
///
/// # Panics
///
@@ -181,7 +179,7 @@ impl Array8 {
}
self.rebuild_cached_values();
- self.estimator.set_out_of_order(true);
+ self.estimator.invalidate_hip();
}
/// Merge an array with larger lg_k (downsampling)
@@ -219,7 +217,7 @@ impl Array8 {
}
self.rebuild_cached_values();
- self.estimator.set_out_of_order(true);
+ self.estimator.invalidate_hip();
}
/// Rebuild cached values after bulk modifications
@@ -245,8 +243,7 @@ impl Array8 {
}
}
- self.estimator.set_kxq0(kxq0_sum);
- self.estimator.set_kxq1(kxq1_sum);
+ self.estimator.restore_kxq(kxq0_sum, kxq1_sum);
}
/// Deserialize Array8 from HLL mode bytes
@@ -259,7 +256,7 @@ impl Array8 {
) -> Result<Self, Error> {
let k = 1usize << lg_config_k;
- // Read HIP estimator values from preamble
+ // Read estimator values from preamble
let hip_accum = cursor
.read_f64_le()
.map_err(insufficient_data("hip_accum"))?;
@@ -291,12 +288,7 @@ impl Array8 {
.read_exact(&mut data)
.map_err(insufficient_data("data"))?;
- // Create estimator and restore state
- let mut estimator = HipEstimator::new(lg_config_k);
- estimator.set_hip_accum(hip_accum);
- estimator.set_kxq0(kxq0);
- estimator.set_kxq1(kxq1);
- estimator.set_out_of_order(ooo);
+ let estimator = Estimator::from_serialized(hip_accum, kxq0, kxq1, ooo);
Ok(Self {
lg_config_k,
@@ -323,7 +315,7 @@ impl Array8 {
// Write flags
let mut flags = 0u8;
- if self.estimator.is_out_of_order() {
+ if self.estimator.uses_composite_estimate() {
flags |= OUT_OF_ORDER_FLAG_MASK;
}
bytes.write_u8(flags);
@@ -334,7 +326,7 @@ impl Array8 {
// Mode byte: HLL mode with HLL8 type
bytes.write_u8(encode_mode_byte(CUR_MODE_HLL, TGT_HLL8));
- // Write HIP estimator values
+ // Write estimator values
bytes.write_f64_le(self.estimator.hip_accum());
bytes.write_f64_le(self.estimator.kxq0());
bytes.write_f64_le(self.estimator.kxq1());
@@ -537,8 +529,8 @@ mod tests {
assert_eq!(dst.get(2), 35, "dst[2] updated to larger value");
assert_eq!(dst.get(3), 40, "dst[3] got new value");
- // Verify estimator marked as OOO
- assert!(dst.estimator.is_out_of_order());
+ // Bulk merges require composite estimation.
+ assert!(dst.estimator.uses_composite_estimate());
// Verify num_zeros updated (should be 12: 16 - 4 non-zero)
assert_eq!(dst.num_zeros, 12);
@@ -567,8 +559,8 @@ mod tests {
assert_eq!(dst.get(0), 25, "dst[0] = max(10, 15, 25)");
assert_eq!(dst.get(1), 30, "dst[1] = max(20, 18, 30)");
- // Verify estimator marked as OOO
- assert!(dst.estimator.is_out_of_order());
+ // Bulk merges require composite estimation.
+ assert!(dst.estimator.uses_composite_estimate());
}
#[test]
diff --git a/datasketches/src/hll/composite_interpolation.rs
b/datasketches/src/hll/composite_interpolation.rs
index 9f2083e..e5e13e3 100644
--- a/datasketches/src/hll/composite_interpolation.rs
+++ b/datasketches/src/hll/composite_interpolation.rs
@@ -15,11 +15,10 @@
// specific language governing permissions and limitations
// under the License.
-//! Composite interpolation tables for HLL out-of-order estimation
+//! Composite interpolation tables for HLL estimation.
//!
-//! These tables are used with cubic interpolation to provide accurate
-//! cardinality estimates when the HLL sketch is in out-of-order mode
-//! (after deserialization or merging).
+//! These tables correct raw HyperLogLog estimates when the register update
history is unavailable,
+//! such as after merging independent sketches.
//!
//! Currently, this module contains tables for common lg_k values (4-12). The
full C++
//! implementation has tables for lg_k 4-21. Additional tables can be found at:
diff --git a/datasketches/src/hll/estimator.rs
b/datasketches/src/hll/estimator.rs
index 903e780..f607e8e 100644
--- a/datasketches/src/hll/estimator.rs
+++ b/datasketches/src/hll/estimator.rs
@@ -15,12 +15,10 @@
// specific language governing permissions and limitations
// under the License.
-//! HIP (Historical Inverse Probability) Estimator for HyperLogLog
+//! Cardinality estimation for HLL-mode sketches.
//!
-//! The HIP estimator provides improved cardinality estimation by maintaining
-//! an accumulator that tracks the historical sequence of register updates.
-//! This is more accurate than the standard HLL estimator, especially for
-//! moderate cardinalities.
+//! Sequential register updates use HIP (Historical Inverse Probability). Bulk
merges use the
+//! composite estimator because register values do not retain their update
order.
use crate::common::NumStdDev;
use crate::common::inv_pow2::inv_pow2;
@@ -28,37 +26,56 @@ use crate::hll::composite_interpolation;
use crate::hll::cubic_interpolation;
use crate::hll::harmonic_numbers;
-/// HIP estimator with KxQ registers for improved cardinality estimation
-///
-/// This struct encapsulates all estimation-related state and logic,
-/// allowing it to be composed into Array4, Array6, and Array8.
+/// Selects the estimate that is valid for the current register history.
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub(super) enum EstimateState {
+ /// The register updates have a known order, so the HIP accumulator is
valid.
+ Hip(f64),
+ /// A bulk merge lost the update order, so the estimate must come from the
registers.
+ Composite,
+}
+
+/// Cardinality estimator shared by Array4, Array6, and Array8.
///
-/// The estimator supports two modes:
-/// * **In-order mode**: Uses HIP (Historical Inverse Probability) accumulator
for accurate
-/// sequential updates
-/// * **Out-of-order mode**: Uses composite estimator (raw HLL + linear
counting) after
-/// deserialization or merging
+/// Sequential updates use HIP. Bulk merges use the composite estimator
because register values do
+/// not retain update order. Both modes maintain KxQ as a cache derived from
the current registers.
#[derive(Debug, Clone, PartialEq)]
-pub struct HipEstimator {
- /// HIP estimator accumulator
- hip_accum: f64,
+pub struct Estimator {
+ /// Estimate selected by the register history.
+ estimate_state: EstimateState,
/// KxQ register for values < 32 (larger inverse powers)
kxq0: f64,
/// KxQ register for values >= 32 (tiny inverse powers)
kxq1: f64,
- /// Out-of-order flag: when true, HIP updates are skipped
- out_of_order: bool,
}
-impl HipEstimator {
- /// Create a new HIP estimator for a sketch with 2^lg_config_k registers
+impl Estimator {
+ /// Creates an estimator for a sketch with 2^lg_config_k registers.
pub fn new(lg_config_k: u8) -> Self {
let k = 1 << lg_config_k;
Self {
- hip_accum: 0.0,
+ estimate_state: EstimateState::Hip(0.0),
kxq0: k as f64, // All registers start at 0, so kxq0 = k * (1/2^0)
= k
kxq1: 0.0,
- out_of_order: false,
+ }
+ }
+
+ /// Restores estimator fields from an HLL serialization preamble.
+ pub(super) fn from_serialized(
+ hip_accum: f64,
+ kxq0: f64,
+ kxq1: f64,
+ out_of_order: bool,
+ ) -> Self {
+ let estimate_state = if out_of_order {
+ EstimateState::Composite
+ } else {
+ EstimateState::Hip(hip_accum)
+ };
+ Self {
+ estimate_state,
+ kxq0,
+ kxq1,
}
}
@@ -68,7 +85,7 @@ impl HipEstimator {
///
/// # Algorithm
///
- /// 1. Update HIP accumulator (unless out-of-order)
+ /// 1. Update the HIP accumulator when it remains valid
/// 2. Update KxQ registers (always)
///
/// The KxQ registers are split for numerical precision:
@@ -77,13 +94,12 @@ impl HipEstimator {
pub fn update(&mut self, lg_config_k: u8, old_value: u8, new_value: u8) {
let k = (1 << lg_config_k) as f64;
- // Update HIP accumulator FIRST (unless out-of-order)
- // When out-of-order (from deserialization or merge), HIP is invalid
- if !self.out_of_order {
- self.hip_accum += k / (self.kxq0 + self.kxq1);
+ // Update HIP accumulator FIRST when the register history is still
available.
+ if let EstimateState::Hip(hip_accum) = &mut self.estimate_state {
+ *hip_accum += k / (self.kxq0 + self.kxq1);
}
- // Always update KxQ registers (regardless of OOO flag)
+ // Always update KxQ because it depends only on the current registers.
self.update_kxq(old_value, new_value);
}
@@ -106,7 +122,7 @@ impl HipEstimator {
/// Get the current cardinality estimate
///
- /// Dispatches to either HIP or composite estimator based on out-of-order
flag.
+ /// Dispatches to the estimate selected by the register history.
///
/// # Arguments
///
@@ -114,10 +130,11 @@ impl HipEstimator {
/// * `cur_min`: Current minimum register value (for Array4, 0 for
Array6/8)
/// * `num_at_cur_min`: Number of registers at cur_min value
pub fn estimate(&self, lg_config_k: u8, cur_min: u8, num_at_cur_min: u32)
-> f64 {
- if self.out_of_order {
- self.get_composite_estimate(lg_config_k, cur_min, num_at_cur_min)
- } else {
- self.hip_accum
+ match self.estimate_state {
+ EstimateState::Hip(hip_accum) => hip_accum,
+ EstimateState::Composite => {
+ self.composite_estimate(lg_config_k, cur_min, num_at_cur_min)
+ }
}
}
@@ -139,7 +156,12 @@ impl HipEstimator {
num_std_dev: NumStdDev,
) -> f64 {
let estimate = self.estimate(lg_config_k, cur_min, num_at_cur_min);
- let rse = get_rel_err(lg_config_k, true, self.out_of_order,
num_std_dev);
+ let rse = relative_error(
+ lg_config_k,
+ true,
+ self.uses_composite_estimate(),
+ num_std_dev,
+ );
// RSE is negative for upper bounds, so (1 + rse) < 1, making bound >
estimate
estimate / (1.0 + rse)
}
@@ -165,7 +187,12 @@ impl HipEstimator {
num_std_dev: NumStdDev,
) -> f64 {
let estimate = self.estimate(lg_config_k, cur_min, num_at_cur_min);
- let rse = get_rel_err(lg_config_k, false, self.out_of_order,
num_std_dev);
+ let rse = relative_error(
+ lg_config_k,
+ false,
+ self.uses_composite_estimate(),
+ num_std_dev,
+ );
let config_k = 1u32 << lg_config_k;
let num_nonzero_registers = if cur_min == 0 {
config_k - num_at_cur_min
@@ -181,7 +208,7 @@ impl HipEstimator {
/// Formula: correctionFactor * k^2 / (kxq0 + kxq1)
///
/// Uses lg_k-specific correction factors for small k.
- fn get_raw_estimate(&self, lg_config_k: u8) -> f64 {
+ fn raw_estimate(&self, lg_config_k: u8) -> f64 {
let k = (1 << lg_config_k) as f64;
// Correction factors from empirical analysis
@@ -198,7 +225,7 @@ impl HipEstimator {
/// Get linear counting (bitmap) estimate for small cardinalities
///
/// Uses harmonic numbers to estimate based on empty registers.
- fn get_bitmap_estimate(&self, lg_config_k: u8, cur_min: u8,
num_at_cur_min: u32) -> f64 {
+ fn bitmap_estimate(&self, lg_config_k: u8, cur_min: u8, num_at_cur_min:
u32) -> f64 {
let k = 1 << lg_config_k;
// Number of unhit (empty) buckets
@@ -215,11 +242,11 @@ impl HipEstimator {
/// Get composite estimate (blends raw HLL and linear counting)
///
- /// This is the primary estimator used when in out-of-order mode.
- /// It uses cubic interpolation on raw HLL estimate, then blends
+ /// This estimate is used when the register update history is unavailable.
+ /// It uses cubic interpolation on the raw HLL estimate, then blends
/// with linear counting for small cardinalities.
- fn get_composite_estimate(&self, lg_config_k: u8, cur_min: u8,
num_at_cur_min: u32) -> f64 {
- let raw_est = self.get_raw_estimate(lg_config_k);
+ fn composite_estimate(&self, lg_config_k: u8, cur_min: u8, num_at_cur_min:
u32) -> f64 {
+ let raw_estimate = self.raw_estimate(lg_config_k);
// Get composite interpolation table
let x_arr = composite_interpolation::get_x_arr(lg_config_k);
@@ -227,35 +254,36 @@ impl HipEstimator {
let y_stride = composite_interpolation::get_y_stride(lg_config_k) as
f64;
// Handle edge cases
- if raw_est < x_arr[0] {
+ if raw_estimate < x_arr[0] {
return 0.0;
}
let x_arr_len_m1 = x_arr_len - 1;
// Above interpolation range: extrapolate linearly
- if raw_est > x_arr[x_arr_len_m1] {
+ if raw_estimate > x_arr[x_arr_len_m1] {
let final_y = y_stride * (x_arr_len_m1 as f64);
let factor = final_y / x_arr[x_arr_len_m1];
- return raw_est * factor;
+ return raw_estimate * factor;
}
// Interpolate using cubic interpolation
- let adj_est = cubic_interpolation::using_x_arr_and_y_stride(x_arr,
y_stride, raw_est);
+ let adjusted_estimate =
+ cubic_interpolation::using_x_arr_and_y_stride(x_arr, y_stride,
raw_estimate);
// Avoid linear counting if estimate is high
// (threshold: 3*k ensures we're above potential linear counting
instability)
let k = 1 << lg_config_k;
- if adj_est > (3 * k) as f64 {
- return adj_est;
+ if adjusted_estimate > (3 * k) as f64 {
+ return adjusted_estimate;
}
// Get linear counting estimate
- let lin_est = self.get_bitmap_estimate(lg_config_k, cur_min,
num_at_cur_min);
+ let linear_estimate = self.bitmap_estimate(lg_config_k, cur_min,
num_at_cur_min);
// Blend estimates based on crossover threshold
// Use average to reduce bias from threshold comparison
- let avg_est = (adj_est + lin_est) / 2.0;
+ let average_estimate = (adjusted_estimate + linear_estimate) / 2.0;
// Crossover thresholds (empirically determined)
let crossover = match lg_config_k {
@@ -266,16 +294,19 @@ impl HipEstimator {
let threshold = crossover * (k as f64);
- if avg_est > threshold {
- adj_est
+ if average_estimate > threshold {
+ adjusted_estimate
} else {
- lin_est
+ linear_estimate
}
}
/// Get the HIP accumulator value
pub fn hip_accum(&self) -> f64 {
- self.hip_accum
+ match self.estimate_state {
+ EstimateState::Hip(hip_accum) => hip_accum,
+ EstimateState::Composite => 0.0,
+ }
}
/// Get the kxq0 register value
@@ -288,38 +319,30 @@ impl HipEstimator {
self.kxq1
}
- /// Check if this estimator is in out-of-order mode
- pub fn is_out_of_order(&self) -> bool {
- self.out_of_order
+ /// Returns whether estimates are derived from registers rather than HIP
history.
+ pub fn uses_composite_estimate(&self) -> bool {
+ matches!(self.estimate_state, EstimateState::Composite)
}
- /// Set the out-of-order flag
- ///
- /// This should be set to true when:
- /// * Deserializing a sketch from bytes
- /// * After a merge/union operation
- pub fn set_out_of_order(&mut self, ooo: bool) {
- self.out_of_order = ooo;
- if ooo {
- // When going out-of-order, invalidate HIP accumulator
- // (it will be recomputed if needed via composite estimator)
- self.hip_accum = 0.0;
- }
+ /// Returns the estimate state independently from register-derived KxQ
values.
+ pub(super) fn estimate_state(&self) -> EstimateState {
+ self.estimate_state
}
- /// Set the HIP accumulator directly
- pub fn set_hip_accum(&mut self, value: f64) {
- self.hip_accum = value;
+ /// Restores estimate state after copying or transforming the same logical
sketch.
+ pub(super) fn restore_estimate_state(&mut self, state: EstimateState) {
+ self.estimate_state = state;
}
- /// Set the kxq0 register directly
- pub fn set_kxq0(&mut self, value: f64) {
- self.kxq0 = value;
+ /// Invalidates HIP after registers from independent histories are merged.
+ pub(super) fn invalidate_hip(&mut self) {
+ self.estimate_state = EstimateState::Composite;
}
- /// Set the kxq1 register directly
- pub fn set_kxq1(&mut self, value: f64) {
- self.kxq1 = value;
+ /// Replaces register-derived KxQ values after a bulk register operation.
+ pub(super) fn restore_kxq(&mut self, kxq0: f64, kxq1: f64) {
+ self.kxq0 = kxq0;
+ self.kxq1 = kxq1;
}
}
@@ -331,22 +354,27 @@ impl HipEstimator {
///
/// * `lg_config_k`: Log2 of number of registers (must be 4-21)
/// * `upper_bound`: Whether computing upper bound (vs lower bound)
-/// * `ooo`: Whether sketch is out-of-order (merged/deserialized)
+/// * `composite`: Whether the estimate is derived from registers rather than
HIP history
/// * `num_std_dev`: Number of standard deviations (1, 2, or 3)
///
/// # Returns
///
/// Relative error factor to apply to estimate
-fn get_rel_err(lg_config_k: u8, upper_bound: bool, ooo: bool, num_std_dev:
NumStdDev) -> f64 {
+fn relative_error(
+ lg_config_k: u8,
+ upper_bound: bool,
+ composite: bool,
+ num_std_dev: NumStdDev,
+) -> f64 {
// For lg_k > 12, use analytical formula with RSE factors
if lg_config_k > 12 {
// RSE factors from Apache DataSketches C++ implementation
// HLL_HIP_RSE_FACTOR = sqrt(ln(2)) ≈ 0.8325546
// HLL_NON_HIP_RSE_FACTOR = sqrt((3 * ln(2)) - 1) ≈ 1.03896
- let rse_factor = if ooo {
- 1.03896 // Non-HIP (out-of-order)
+ let rse_factor = if composite {
+ 1.03896 // Composite
} else {
- 0.8325546 // HIP (in-order)
+ 0.8325546 // HIP
};
let k = (1 << lg_config_k) as f64;
@@ -359,8 +387,8 @@ fn get_rel_err(lg_config_k: u8, upper_bound: bool, ooo:
bool, num_std_dev: NumSt
// Tables are indexed by: ((lg_k - 4) * 3) + (num_std_dev - 1)
let idx = ((lg_config_k as usize) - 4) * 3 + ((num_std_dev as usize) - 1);
- // Select the appropriate table based on ooo and upper_bound flags
- match (ooo, upper_bound) {
+ // Select the appropriate table based on estimator and bound direction.
+ match (composite, upper_bound) {
(false, false) => HIP_LB[idx], // Case 0: HIP, Lower Bound
(false, true) => HIP_UB[idx], // Case 1: HIP, Upper Bound
(true, false) => NON_HIP_LB[idx], // Case 2: Non-HIP, Lower Bound
@@ -435,7 +463,7 @@ static HIP_UB: [f64; 27] = [
-0.037896952, //12
];
-/// Non-HIP (out-of-order) Lower Bound errors for lg_k 4-12, std_dev 1-3
+/// Composite (non-HIP) lower-bound errors for lg_k 4-12, std_dev 1-3.
/// Q(.84134), Q(.97725), Q(.99865) quantiles
static NON_HIP_LB: [f64; 27] = [
0.254409839,
@@ -467,7 +495,7 @@ static NON_HIP_LB: [f64; 27] = [
0.049677541, //12
];
-/// Non-HIP (out-of-order) Upper Bound errors for lg_k 4-12, std_dev 1-3
+/// Composite (non-HIP) upper-bound errors for lg_k 4-12, std_dev 1-3.
/// Q(.15866), Q(.02275), Q(.00135) quantiles
static NON_HIP_UB: [f64; 27] = [
-0.256980172,
@@ -510,7 +538,7 @@ mod tests {
#[test]
fn lower_bound_is_clamped_to_the_non_zero_register_count() {
let lg_config_k = 4;
- let mut estimator = HipEstimator::new(lg_config_k);
+ let mut estimator = Estimator::new(lg_config_k);
for _ in 0..8 {
estimator.update(lg_config_k, 0, 1);
}
@@ -524,7 +552,7 @@ mod tests {
#[test]
fn lower_bound_is_clamped_to_config_k_when_every_register_is_hit() {
let lg_config_k = 4;
- let mut estimator = HipEstimator::new(lg_config_k);
+ let mut estimator = Estimator::new(lg_config_k);
for _ in 0..16 {
estimator.update(lg_config_k, 0, 1);
}
@@ -537,17 +565,17 @@ mod tests {
#[test]
fn test_estimator_initialization() {
- let est = HipEstimator::new(10); // 1024 registers
+ let est = Estimator::new(10); // 1024 registers
assert_eq!(est.hip_accum(), 0.0);
assert_eq!(est.kxq0(), 1024.0); // All zeros = 1.0 each
assert_eq!(est.kxq1(), 0.0);
- assert!(!est.is_out_of_order());
+ assert!(!est.uses_composite_estimate());
}
#[test]
fn test_estimator_update() {
- let mut est = HipEstimator::new(8); // 256 registers
+ let mut est = Estimator::new(8); // 256 registers
// Update from 0 to 10
est.update(8, 0, 10);
@@ -562,7 +590,7 @@ mod tests {
#[test]
fn test_kxq_split() {
- let mut est = HipEstimator::new(8);
+ let mut est = Estimator::new(8);
// Update to value < 32 (goes to kxq0)
est.update(8, 0, 10);
@@ -582,36 +610,21 @@ mod tests {
}
#[test]
- fn test_out_of_order_flag() {
- let mut est = HipEstimator::new(10);
+ fn test_invalidate_hip() {
+ let mut est = Estimator::new(10);
- // Normal update
- est.update(8, 0, 5);
+ est.update(10, 0, 5);
let hip_normal = est.hip_accum();
assert_that!(hip_normal, gt(0.0));
- // Set out-of-order
- est.set_out_of_order(true);
- assert!(est.is_out_of_order());
- assert_eq!(est.hip_accum(), 0.0); // HIP invalidated
+ est.invalidate_hip();
+ assert!(est.uses_composite_estimate());
+ assert_eq!(est.hip_accum(), 0.0);
- // Update while OOO - HIP should not change, but kxq should
+ // Register-derived state continues to update after HIP is invalidated.
let kxq0_before = est.kxq0();
- est.update(8, 5, 10);
- assert_eq!(est.hip_accum(), 0.0); // HIP still 0
- assert_ne!(est.kxq0(), kxq0_before); // kxq changed
- }
-
- #[test]
- fn test_setters() {
- let mut est = HipEstimator::new(10);
-
- est.set_hip_accum(123.45);
- est.set_kxq0(678.9);
- est.set_kxq1(0.0012);
-
- assert_eq!(est.hip_accum(), 123.45);
- assert_eq!(est.kxq0(), 678.9);
- assert_eq!(est.kxq1(), 0.0012);
+ est.update(10, 5, 10);
+ assert_eq!(est.hip_accum(), 0.0);
+ assert_ne!(est.kxq0(), kxq0_before);
}
}
diff --git a/datasketches/src/hll/serialization.rs
b/datasketches/src/hll/serialization.rs
index 9f4eeeb..cacb6cb 100644
--- a/datasketches/src/hll/serialization.rs
+++ b/datasketches/src/hll/serialization.rs
@@ -29,7 +29,7 @@ pub const EMPTY_FLAG_MASK: u8 = 4;
///
/// HLL register arrays have the same layout in compact and updatable images.
pub const COMPACT_FLAG_MASK: u8 = 8;
-/// Flag indicating out-of-order mode (HIP estimator invalid)
+/// Flag indicating that HIP history is unavailable and composite estimation
is required.
pub const OUT_OF_ORDER_FLAG_MASK: u8 = 16;
/// Preamble size for LIST mode (8 bytes = 2 ints)
diff --git a/datasketches/src/hll/sketch.rs b/datasketches/src/hll/sketch.rs
index 8e924e2..4e0ed71 100644
--- a/datasketches/src/hll/sketch.rs
+++ b/datasketches/src/hll/sketch.rs
@@ -37,6 +37,7 @@ use crate::hll::array4::AuxFormat;
use crate::hll::array6::Array6;
use crate::hll::array8::Array8;
use crate::hll::container::Container;
+use crate::hll::estimator::EstimateState;
use crate::hll::hash_set::HashSet;
use crate::hll::list::List;
use crate::hll::mode::Mode;
@@ -487,7 +488,7 @@ fn promote_container_to_array(container: &Container,
hll_type: HllType, lg_confi
for coupon in container.iter() {
array.update(coupon);
}
- array.set_hip_accum(container.estimate());
+
array.restore_estimate_state(EstimateState::Hip(container.estimate()));
Mode::Array4(array)
}
HllType::Hll6 => {
@@ -495,7 +496,7 @@ fn promote_container_to_array(container: &Container,
hll_type: HllType, lg_confi
for coupon in container.iter() {
array.update(coupon);
}
- array.set_hip_accum(container.estimate());
+
array.restore_estimate_state(EstimateState::Hip(container.estimate()));
Mode::Array6(array)
}
HllType::Hll8 => {
@@ -503,7 +504,7 @@ fn promote_container_to_array(container: &Container,
hll_type: HllType, lg_confi
for coupon in container.iter() {
array.update(coupon);
}
- array.set_hip_accum(container.estimate());
+
array.restore_estimate_state(EstimateState::Hip(container.estimate()));
Mode::Array8(array)
}
}
diff --git a/datasketches/src/hll/union.rs b/datasketches/src/hll/union.rs
index 5602695..eb98382 100644
--- a/datasketches/src/hll/union.rs
+++ b/datasketches/src/hll/union.rs
@@ -38,6 +38,7 @@ use crate::hll::HllType;
use crate::hll::array4::Array4;
use crate::hll::array6::Array6;
use crate::hll::array8::Array8;
+use crate::hll::estimator::EstimateState;
use crate::hll::mode::Mode;
/// An HLL union for combining multiple HLL sketches.
@@ -424,14 +425,16 @@ fn merge_array_into_array8(dst_array8: &mut Array8,
dst_lg_k: u8, src_mode: &Mod
}
}
-/// Extract HIP accumulator from an array mode
-fn get_array_hip_accum(mode: &Mode) -> f64 {
+/// Extract estimate state from an array mode.
+fn get_array_estimate_state(mode: &Mode) -> EstimateState {
match mode {
- Mode::Array8(src) => src.hip_accum(),
- Mode::Array6(src) => src.hip_accum(),
- Mode::Array4(src) => src.hip_accum(),
+ Mode::Array8(src) => src.estimate_state(),
+ Mode::Array6(src) => src.estimate_state(),
+ Mode::Array4(src) => src.estimate_state(),
Mode::List { .. } | Mode::Set { .. } => {
- unreachable!("get_array_hip_accum called with non-array mode;
List/Set not supported");
+ unreachable!(
+ "get_array_estimate_state called with non-array mode; List/Set
not supported"
+ );
}
}
}
@@ -520,8 +523,9 @@ fn merge_array_with_downsample(dst: &mut Array8, dst_lg_k:
u8, src_mode: &Mode,
/// Convert Array8 to a different HLL type
///
/// Creates a new sketch with the requested type by copying register values
-/// from the Array8 source. Preserves the HIP accumulator.
+/// from the Array8 source. Preserves the estimate state.
fn convert_array8_to_type(src: &Array8, lg_config_k: u8, target_type: HllType)
-> HllSketch {
+ let estimate_state = src.estimate_state();
match target_type {
HllType::Hll8 => HllSketch::from_mode(lg_config_k,
Mode::Array8(src.clone())),
HllType::Hll6 => {
@@ -534,12 +538,7 @@ fn convert_array8_to_type(src: &Array8, lg_config_k: u8,
target_type: HllType) -
array6.update(coupon);
}
}
-
- let src_est = src.estimate();
- let arr6_est = array6.estimate();
- if src_est > arr6_est {
- array6.set_hip_accum(src_est);
- }
+ array6.restore_estimate_state(estimate_state);
HllSketch::from_mode(lg_config_k, Mode::Array6(array6))
}
@@ -552,12 +551,7 @@ fn convert_array8_to_type(src: &Array8, lg_config_k: u8,
target_type: HllType) -
array4.update(coupon);
}
}
-
- let src_est = src.estimate();
- let arr4_est = array4.estimate();
- if src_est > arr4_est {
- array4.set_hip_accum(src_est);
- }
+ array4.restore_estimate_state(estimate_state);
HllSketch::from_mode(lg_config_k, Mode::Array4(array4))
}
@@ -578,11 +572,12 @@ fn copy_array46_via_coupons(dst: &mut Array8,
num_registers: usize, get_value: i
/// Copy or downsample a source array to create a new Array8
///
/// Directly copies if src_lg_k <= tgt_lg_k, downsamples otherwise.
-/// Result is marked as out-of-order and HIP accumulator is preserved.
+/// The source estimate state carries over because the result represents the
same logical sketch.
fn copy_or_downsample(src_mode: &Mode, src_lg_k: u8, tgt_lg_k: u8) -> Array8 {
- if src_lg_k <= tgt_lg_k {
+ let estimate_state = get_array_estimate_state(src_mode);
+
+ let mut result = if src_lg_k <= tgt_lg_k {
let mut result = Array8::new(src_lg_k);
- let src_hip = get_array_hip_accum(src_mode);
match src_mode {
Mode::Array8(src) => {
@@ -601,12 +596,14 @@ fn copy_or_downsample(src_mode: &Mode, src_lg_k: u8,
tgt_lg_k: u8) -> Array8 {
}
}
- result.set_hip_accum(src_hip);
result
} else {
// Downsample from src to tgt
let mut result = Array8::new(tgt_lg_k);
merge_array_with_downsample(&mut result, tgt_lg_k, src_mode, src_lg_k);
result
- }
+ };
+
+ result.restore_estimate_state(estimate_state);
+ result
}
diff --git a/tests-integration/tests/hll_test/bounds.rs
b/tests-integration/tests/hll_test/bounds.rs
index 159c01f..2dc70da 100644
--- a/tests-integration/tests/hll_test/bounds.rs
+++ b/tests-integration/tests/hll_test/bounds.rs
@@ -70,7 +70,7 @@ fn
hll_mode_lower_bound_matches_cross_language_register_floor() {
near(9.946968965192236, 1e-9)
);
- // The same floor applies when a union selects the out-of-order estimator.
+ // The same floor applies when a union selects the composite estimator.
let sketch = hll_mode_union(7, HllType::Hll4, 40);
assert_eq!(sketch.lower_bound(NumStdDev::Three), 34.0);
}
diff --git a/tests-integration/tests/hll_test/union.rs
b/tests-integration/tests/hll_test/union.rs
index b6b8038..7a6409a 100644
--- a/tests-integration/tests/hll_test/union.rs
+++ b/tests-integration/tests/hll_test/union.rs
@@ -15,18 +15,6 @@
// specific language governing permissions and limitations
// under the License.
-//! HyperLogLog Union Integration Tests
-//!
-//! These tests verify the public API behavior of HllUnion, focusing on:
-//! * Basic union operations
-//! * Mode transitions and mixed-mode unions
-//! * Different HLL types and lg_k values
-//! * Bounds and statistical properties
-//! * Mathematical properties (commutativity, associativity, idempotency)
-//! * Reset and reuse patterns
-//!
-//! This mirrors the testing strategy used in hll_update_test.rs
-
use datasketches::common::NumStdDev;
use datasketches::error::ErrorKind;
use datasketches::hll::HllSketch;
@@ -59,6 +47,28 @@ fn assert_estimate_within(estimate: f64, expected: f64,
relative_error: f64) {
);
}
+fn assert_same_estimate_and_bounds(actual: &HllSketch, expected: &HllSketch) {
+ let actual_type = actual.target_type();
+ let expected_type = expected.target_type();
+ assert_eq!(
+ actual.estimate(),
+ expected.estimate(),
+ "{actual_type:?} and {expected_type:?} estimates",
+ );
+ for num_std_dev in [NumStdDev::One, NumStdDev::Two, NumStdDev::Three] {
+ assert_eq!(
+ actual.lower_bound(num_std_dev),
+ expected.lower_bound(num_std_dev),
+ "{actual_type:?} and {expected_type:?} lower bounds at
{num_std_dev:?}",
+ );
+ assert_eq!(
+ actual.upper_bound(num_std_dev),
+ expected.upper_bound(num_std_dev),
+ "{actual_type:?} and {expected_type:?} upper bounds at
{num_std_dev:?}",
+ );
+ }
+}
+
fn serialize_flat_union(first: &HllSketch, second: &HllSketch, third:
&HllSketch) -> Vec<u8> {
let mut union = HllUnion::new(8).unwrap();
union.update(first);
@@ -235,6 +245,10 @@ fn test_union_mixed_hll_types() {
assert_eq!(result6.target_type(), HllType::Hll6);
assert_eq!(result8.target_type(), HllType::Hll8);
+ // Target types change only the register encoding.
+ assert_same_estimate_and_bounds(&result4, &result8);
+ assert_same_estimate_and_bounds(&result6, &result8);
+
// Should estimate ~7,000 unique values (0-6,999)
for (result, type_name) in [
(result4.estimate(), "Hll4"),
@@ -553,22 +567,19 @@ fn test_union_associativity() {
}
#[test]
-fn test_union_idempotency() {
- // Verify A∪A = A
- let mut sketch = HllSketch::new(12, HllType::Hll8).unwrap();
- for i in 0..1000 {
- sketch.update(i);
- }
+fn test_union_repeated_input_is_stable() {
+ let sketch = make_hll_sketch(HllType::Hll8, 12, 0, 1_000);
let mut union = HllUnion::new(12).unwrap();
union.update(&sketch);
- let est1 = union.estimate();
+ assert_eq!(union.estimate(), sketch.estimate());
- // Union with itself
union.update(&sketch);
- let est2 = union.estimate();
+ let merged_estimate = union.estimate();
+ assert_estimate_within(merged_estimate, sketch.estimate(), 0.02);
- assert_eq!(est1, est2);
+ union.update(&sketch);
+ assert_eq!(union.estimate(), merged_estimate);
}
#[test]
@@ -708,3 +719,29 @@ fn test_union_estimated_size() {
union.update(&sketch);
assert_eq!(union.estimated_size(), 1120);
}
+
+#[test]
+fn test_single_hll_input_preserves_estimate_and_bounds() {
+ for hll_type in HLL_TYPES {
+ let sketch = make_hll_sketch(hll_type, 8, 0, 1_000);
+ let mut union = HllUnion::new(10).unwrap();
+ union.update(&sketch);
+
+ assert_eq!(union.estimate(), sketch.estimate(), "{hll_type:?}
estimate");
+ for target_type in HLL_TYPES {
+ assert_same_estimate_and_bounds(&union.to_sketch(target_type),
&sketch);
+ }
+ }
+}
+
+#[test]
+fn test_downsampling_single_hll_input_preserves_estimate() {
+ for hll_type in HLL_TYPES {
+ let sketch = make_hll_sketch(hll_type, 12, 0, 10_000);
+ let mut union = HllUnion::new(8).unwrap();
+ union.update(&sketch);
+
+ assert_eq!(union.lg_config_k(), 8, "{hll_type:?} lg_config_k");
+ assert_eq!(union.estimate(), sketch.estimate(), "{hll_type:?}
estimate");
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]