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 1d2d406 refactor(hll): simplify coupon serialization and
documentation (#291)
1d2d406 is described below
commit 1d2d406f81f85a44c2c3e89c4d8b9d2d9b4cefb9
Author: tison <[email protected]>
AuthorDate: Mon Oct 5 14:47:30 2026 +0800
refactor(hll): simplify coupon serialization and documentation (#291)
---
datasketches/src/hll/array4.rs | 100 +++-----------
datasketches/src/hll/array6.rs | 69 +---------
datasketches/src/hll/array8.rs | 123 ++---------------
datasketches/src/hll/aux_map.rs | 67 ++--------
datasketches/src/hll/composite_interpolation.rs | 23 +---
datasketches/src/hll/container.rs | 19 +--
datasketches/src/hll/coupon_mapping.rs | 7 +-
datasketches/src/hll/cubic_interpolation.rs | 23 +---
datasketches/src/hll/estimator.rs | 136 ++++---------------
datasketches/src/hll/harmonic_numbers.rs | 35 +----
datasketches/src/hll/hash_set.rs | 56 ++------
datasketches/src/hll/list.rs | 49 +------
datasketches/src/hll/mod.rs | 108 ++++-----------
datasketches/src/hll/serialization.rs | 34 +----
datasketches/src/hll/sketch.rs | 105 +++++++--------
datasketches/src/hll/union.rs | 171 +++++++++++-------------
16 files changed, 257 insertions(+), 868 deletions(-)
diff --git a/datasketches/src/hll/array4.rs b/datasketches/src/hll/array4.rs
index 2380e00..1a13059 100644
--- a/datasketches/src/hll/array4.rs
+++ b/datasketches/src/hll/array4.rs
@@ -15,11 +15,6 @@
// specific language governing permissions and limitations
// under the License.
-//! HyperLogLog Array4 mode - 4-bit packed representation with exception
handling
-//!
-//! Array4 stores HLL register values using 4 bits per slot (2 slots per byte).
-//! When values exceed 4 bits after cur_min offset, they're stored in an
auxiliary hash map.
-
use crate::codec::SketchBytes;
use crate::codec::SketchSlice;
use crate::codec::assert::insufficient_data;
@@ -58,18 +53,16 @@ impl AuxFormat {
}
}
-/// Core Array4 data structure - stores 4-bit values efficiently
+/// Registers stored as four-bit offsets from their current minimum.
#[derive(Debug, Clone, PartialEq)]
pub struct Array4 {
lg_config_k: u8,
- /// Packed 4-bit values: 2 values per byte
- /// Even slots use low nibble, odd slots use high nibble
+ /// Even slots occupy the low nibble; odd slots occupy the high nibble.
bytes: Box<[u8]>,
- /// Current minimum value offset (optimization to delay aux map creation)
cur_min: u8,
- /// Count of slots at exactly cur_min (when 0, increment cur_min)
+ /// When this reaches zero, the offset must increase until it reaches the
new minimum.
num_at_cur_min: u32,
- /// Exception table for values >= 15 after cur_min offset
+ /// Every `AUX_TOKEN` nibble has an entry here containing the absolute
register value.
aux_map: Option<AuxMap>,
estimator: Estimator,
}
@@ -88,49 +81,35 @@ impl Array4 {
}
}
- /// Get raw 4-bit value from slot (not adjusted for cur_min)
#[inline]
fn get_raw(&self, slot: u32) -> u8 {
debug_assert!(slot >> 1 < self.bytes.len() as u32);
let byte = self.bytes[(slot >> 1) as usize];
- if slot & 1 == 0 {
- byte & 15 // low nibble for even slots
- } else {
- byte >> 4 // high nibble for odd slots
- }
+ if slot & 1 == 0 { byte & 15 } else { byte >> 4 }
}
- /// Get the actual value at a slot (adjusted for cur_min and aux_map)
- ///
- /// Returns the true register value:
- /// * If raw < 15: value = cur_min + raw
- /// * If raw == 15 (AUX_TOKEN): value is in aux_map
pub fn get(&self, slot: u32) -> u8 {
let raw = self.get_raw(slot);
if raw < AUX_TOKEN {
self.cur_min + raw
} else {
- // Value is in aux_map
self.aux_map
.as_ref()
.and_then(|map| map.get(slot))
- .unwrap_or(self.cur_min) // Fallback (shouldn't happen)
+ .unwrap_or(self.cur_min)
}
}
- /// Get the number of registers (K = 2^lg_config_k)
pub fn num_registers(&self) -> usize {
1 << self.lg_config_k
}
- /// Returns the estimate state independently from register-derived cached
values.
pub fn estimate_state(&self) -> EstimateState {
self.estimator.estimate_state()
}
- /// Set raw 4-bit value in slot
#[inline]
fn put_raw(&mut self, slot: u32, value: u8) {
debug_assert!(value <= AUX_TOKEN);
@@ -139,9 +118,9 @@ impl Array4 {
let byte_idx = (slot >> 1) as usize;
let old_byte = self.bytes[byte_idx];
self.bytes[byte_idx] = if slot & 1 == 0 {
- (old_byte & 0xF0) | (value & 0x0F) // set low nibble
+ (old_byte & 0xF0) | (value & 0x0F)
} else {
- (old_byte & 0x0F) | (value << 4) // set high nibble
+ (old_byte & 0x0F) | (value << 4)
};
}
@@ -150,7 +129,6 @@ impl Array4 {
let slot = coupon.slot() & mask;
let new_value = coupon.value();
- // Quick rejection: if new value <= cur_min, no update needed
if new_value <= self.cur_min {
return;
}
@@ -162,7 +140,6 @@ impl Array4 {
return;
}
- // Get actual old value (might be in aux map)
let old_value = if raw_stored < AUX_TOKEN {
lower_bound
} else {
@@ -177,26 +154,22 @@ impl Array4 {
return;
}
- // Update HIP and KxQ registers via estimator
self.estimator
.update(self.lg_config_k, old_value, new_value);
let shifted_new = new_value - self.cur_min;
- // Four cases based on old/new exception status
match (raw_stored, shifted_new) {
- // Case 1: Both old and new are exceptions
(AUX_TOKEN, shifted) if shifted >= AUX_TOKEN => {
self.aux_map
.as_mut()
.expect("aux_map should be initialized since stored value
is AUX_TOKEN")
.replace(slot, new_value);
}
- // Case 2: Old is exception, new is not (impossible without
cur_min change)
+ // Registers only increase here; an exception cannot disappear
until cur_min changes.
(AUX_TOKEN, _) => {
unreachable!("AUX_TOKEN present with non-exception new value");
}
- // Case 3: Old not exception, new is exception
(_, shifted) if shifted >= AUX_TOKEN => {
self.put_raw(slot, AUX_TOKEN);
let aux = self
@@ -204,13 +177,11 @@ impl Array4 {
.get_or_insert_with(|| AuxMap::new(self.lg_config_k));
aux.insert(slot, new_value);
}
- // Case 4: Neither is exception
_ => {
self.put_raw(slot, shifted_new);
}
}
- // Handle cur_min adjustment
if old_value == self.cur_min {
self.num_at_cur_min -= 1;
while self.num_at_cur_min == 0 {
@@ -219,17 +190,13 @@ impl Array4 {
}
}
- /// Increment cur_min and adjust all values
- ///
- /// This is called when no slots remain at cur_min value.
- /// All stored values are decremented by 1, and exceptions
- /// that fall back into the 4-bit range are moved from aux map.
+ /// Requires no registers at `cur_min`. Raising the offset makes some
auxiliary values
+ /// fit in the nibbles again; the logical register values must stay
unchanged.
fn shift_to_bigger_cur_min(&mut self) {
let new_cur_min = self.cur_min + 1;
let k = 1 << self.lg_config_k;
let mut num_at_new = 0;
- // Decrement all stored values in the main array
for slot in 0..k {
let raw = self.get_raw(slot);
debug_assert_ne!(raw, 0, "value cannot be 0 when shifting
cur_min");
@@ -258,7 +225,6 @@ impl Array4 {
if new_shifted < AUX_TOKEN {
self.put_raw(slot, new_shifted);
} else {
- // Still an exception
let aux = new_aux.get_or_insert_with(||
AuxMap::new(self.lg_config_k));
aux.insert(slot, old_actual_val);
}
@@ -270,14 +236,11 @@ impl Array4 {
self.num_at_cur_min = num_at_new;
}
- /// Returns the current cardinality estimate.
pub fn estimate(&self) -> f64 {
- // Array4 tracks cur_min and num_at_cur_min dynamically
self.estimator
.estimate(self.lg_config_k, self.cur_min, self.num_at_cur_min)
}
- /// Get upper bound for cardinality estimate
pub fn upper_bound(&self, num_std_dev: NumStdDev) -> f64 {
self.estimator.upper_bound(
self.lg_config_k,
@@ -287,7 +250,6 @@ impl Array4 {
)
}
- /// Get lower bound for cardinality estimate
pub fn lower_bound(&self, num_std_dev: NumStdDev) -> f64 {
self.estimator.lower_bound(
self.lg_config_k,
@@ -297,19 +259,14 @@ impl Array4 {
)
}
- /// Restores estimate state after copying or transforming the same logical
sketch.
pub fn restore_estimate_state(&mut self, state: EstimateState) {
self.estimator.restore_estimate_state(state);
}
- /// Check if the sketch is empty (all slots are zero)
pub fn is_empty(&self) -> bool {
self.num_at_cur_min == (1 << self.lg_config_k) && self.cur_min == 0
}
- /// Deserialize Array4 from HLL mode bytes
- ///
- /// Expects full HLL preamble (40 bytes) followed by packed 4-bit data and
optional aux map.
pub fn deserialize(
mut cursor: SketchSlice,
cur_min: u8,
@@ -318,16 +275,14 @@ impl Array4 {
ooo: bool,
) -> Result<Self, Error> {
let k = 1usize << lg_config_k;
- let num_bytes = 1usize << (lg_config_k - 1); // k/2 bytes for 4-bit
packing
+ let num_bytes = 1usize << (lg_config_k - 1);
- // Read estimator values from preamble
let hip_accum = cursor
.read_f64_le()
.map_err(insufficient_data("hip_accum"))?;
let kxq0 = cursor.read_f64_le().map_err(insufficient_data("kxq0"))?;
let kxq1 = cursor.read_f64_le().map_err(insufficient_data("kxq1"))?;
- // Read num_at_cur_min and aux_count
let num_at_cur_min = cursor
.read_u32_le()
.map_err(insufficient_data("num_at_cur_min"))?;
@@ -364,13 +319,11 @@ impl Array4 {
));
}
- // Read packed 4-bit byte array
let mut data = vec![0u8; num_bytes];
cursor
.read_exact(&mut data)
.map_err(insufficient_data("data"))?;
- // Read aux map if present
let mut aux_map = None;
if aux_count > 0 {
let mut aux = AuxMap::new(lg_config_k);
@@ -414,13 +367,10 @@ impl Array4 {
})
}
- /// Serialize Array4 to bytes
- ///
- /// Produces full HLL preamble (40 bytes) followed by packed 4-bit data
and optional aux map.
+ /// Writes packed registers followed by compact auxiliary entries.
pub fn serialize(&self, lg_config_k: u8) -> Vec<u8> {
- let num_bytes = 1 << (lg_config_k - 1); // k/2 bytes for 4-bit packing
+ let num_bytes = 1 << (lg_config_k - 1);
- // Collect aux map entries if present
let aux_entries: Vec<(u32, u8)> = if let Some(aux) = &self.aux_map {
aux.iter().collect()
} else {
@@ -431,43 +381,32 @@ impl Array4 {
let total_size = HLL_PREAMBLE_SIZE + num_bytes + (aux_count as usize *
COUPON_SIZE_BYTES);
let mut bytes = SketchBytes::with_capacity(total_size);
- // Write standard header
bytes.write_u8(HLL_PREINTS);
bytes.write_u8(SERIAL_VERSION);
bytes.write_u8(Family::HLL.id);
bytes.write_u8(lg_config_k);
- bytes.write_u8(0); // unused for HLL mode
+ bytes.write_u8(0); // lg_arr is unused for compact auxiliary storage.
- // Write flags.
- // 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.uses_composite_estimate() {
flags |= OUT_OF_ORDER_FLAG_MASK;
}
bytes.write_u8(flags);
- // Write cur_min
bytes.write_u8(self.cur_min);
- // Mode byte: HLL mode with HLL4 type
bytes.write_u8(encode_mode_byte(CUR_MODE_HLL, TGT_HLL4));
- // 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());
- // Write num_at_cur_min
bytes.write_u32_le(self.num_at_cur_min);
- // Write aux_count
bytes.write_u32_le(aux_count);
- // Write packed 4-bit byte array
bytes.write(&self.bytes);
- // Write aux map entries if present
for (slot, value) in aux_entries.iter().copied() {
bytes.write_u32_le(Coupon::pack(slot, value).raw());
}
@@ -512,7 +451,6 @@ mod tests {
// Both values should be stored in the same byte
assert_eq!(data.bytes[0], 0x75); // 0111_0101 = 7 << 4 | 5
- // Test multiple slots
data.put_raw(2, 15);
data.put_raw(3, 3);
assert_eq!(data.get_raw(2), 15);
@@ -523,24 +461,18 @@ mod tests {
fn test_hip_estimator_basic() {
let mut arr = Array4::new(10); // 1024 buckets
- // Initially estimate should be 0
assert_eq!(arr.estimate(), 0.0);
- // Add some unique values to different slots
for i in 0..10_000u32 {
arr.update(Coupon::from_value(i));
}
- // Estimate should be positive and roughly in the ballpark
- // (not exact, but should be non-zero and not NaN/Inf)
let estimate = arr.estimate();
assert_that!(estimate, gt(0.0));
assert_that!(estimate, is_finite());
assert_that!(estimate, lt(100_000.0));
- // Rough sanity check: with 100 updates to different slots,
- // estimate should be in a reasonable range (very loose bounds)
assert_that!(estimate, gt(1_000.0));
assert_that!(estimate, lt(100_000.0));
}
@@ -549,11 +481,9 @@ mod tests {
fn test_kxq_register_split() {
let mut arr = Array4::new(8); // 256 buckets
- // Test that values < 32 and >= 32 are handled correctly
arr.update(Coupon::pack(0, 10)); // value < 32, goes to kxq0
arr.update(Coupon::pack(1, 40)); // value >= 32, goes to kxq1
- // Verify registers were updated (not exact values, just check they
changed)
// kxq0 should have decreased (we removed a 0 and added a 10)
// Initial kxq0 = 256 (all zeros = 1.0 each)
assert_that!(arr.estimator.kxq0(), lt(256.0));
diff --git a/datasketches/src/hll/array6.rs b/datasketches/src/hll/array6.rs
index 3654bb8..a1b0032 100644
--- a/datasketches/src/hll/array6.rs
+++ b/datasketches/src/hll/array6.rs
@@ -15,12 +15,6 @@
// specific language governing permissions and limitations
// under the License.
-//! HyperLogLog Array6 mode - 6-bit packed representation
-//!
-//! Array6 stores HLL register values using 6 bits per slot, providing a range
of 0-63.
-//! This is sufficient for most HLL use cases without needing exception
handling or
-//! cur_min optimization like Array4.
-
use crate::codec::SketchBytes;
use crate::codec::SketchSlice;
use crate::codec::assert::insufficient_data;
@@ -38,15 +32,13 @@ use crate::hll::serialization::SERIAL_VERSION;
use crate::hll::serialization::TGT_HLL6;
use crate::hll::serialization::encode_mode_byte;
-const VAL_MASK_6: u16 = 0x3F; // 6 bits: 0b0011_1111
+const VAL_MASK_6: u16 = 0x3F;
-/// Core Array6 data structure - stores 6-bit values with cross-byte packing
+/// Registers packed into consecutive six-bit fields.
#[derive(Debug, Clone, PartialEq)]
pub struct Array6 {
lg_config_k: u8,
- /// Packed 6-bit values, may cross byte boundaries
bytes: Box<[u8]>,
- /// Count of slots with value 0
num_zeros: u32,
estimator: Estimator,
}
@@ -64,41 +56,30 @@ impl Array6 {
}
}
- /// Get value from a slot (6-bit value)
- ///
- /// Uses 16-bit window reads to handle values crossing byte boundaries.
#[inline]
fn get_raw(&self, slot: u32) -> u8 {
let start_bit = slot * 6;
- let byte_idx = (start_bit >> 3) as usize; // Divide by 8
- let shift = (start_bit & 7) as u8; // Mod 8
+ let byte_idx = (start_bit >> 3) as usize;
+ let shift = (start_bit & 7) as u8;
- // Read 2 bytes as u16 (little-endian)
let two_bytes = u16::from_le_bytes([self.bytes[byte_idx],
self.bytes[byte_idx + 1]]);
- // Extract 6 bits at the shift position
((two_bytes >> shift) & VAL_MASK_6) as u8
}
- /// Get the unpacked 6-bit value (0-63) at the given slot
#[inline]
pub fn get(&self, slot: u32) -> u8 {
self.get_raw(slot)
}
- /// Get the number of registers (K = 2^lg_config_k)
pub fn num_registers(&self) -> usize {
1 << self.lg_config_k
}
- /// Returns the estimate state independently from register-derived cached
values.
pub fn estimate_state(&self) -> EstimateState {
self.estimator.estimate_state()
}
- /// Set value in a slot (6-bit value)
- ///
- /// Uses read-modify-write on 16-bit window to preserve surrounding bits.
#[inline]
fn put_raw(&mut self, slot: u32, value: u8) {
debug_assert!(value <= 63, "6-bit value must be 0-63");
@@ -107,22 +88,17 @@ impl Array6 {
let byte_idx = (start_bit >> 3) as usize;
let shift = (start_bit & 0x7) as u8;
- // Read current 2 bytes
let mut two_bytes = u16::from_le_bytes([self.bytes[byte_idx],
self.bytes[byte_idx + 1]]);
- // Clear the 6-bit slot
two_bytes &= !(VAL_MASK_6 << shift);
- // Insert new value
two_bytes |= ((value as u16) & VAL_MASK_6) << shift;
- // Write back
let bytes_out = two_bytes.to_le_bytes();
self.bytes[byte_idx] = bytes_out[0];
self.bytes[byte_idx + 1] = bytes_out[1];
}
- /// Update with a coupon
pub fn update(&mut self, coupon: Coupon) {
let mask = (1 << self.lg_config_k) - 1;
let slot = coupon.slot() & mask;
@@ -131,51 +107,40 @@ impl Array6 {
let old_value = self.get_raw(slot);
if new_value > old_value {
- // Update HIP and KxQ registers via estimator
self.estimator
.update(self.lg_config_k, old_value, new_value);
- // Update the slot
self.put_raw(slot, new_value);
- // Track num_zeros (count of slots with value 0)
if old_value == 0 {
self.num_zeros -= 1;
}
}
}
- /// 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)
}
- /// Get upper bound for cardinality estimate
pub fn upper_bound(&self, num_std_dev: NumStdDev) -> f64 {
self.estimator
.upper_bound(self.lg_config_k, 0, self.num_zeros, num_std_dev)
}
- /// Get lower bound for cardinality estimate
pub fn lower_bound(&self, num_std_dev: NumStdDev) -> f64 {
self.estimator
.lower_bound(self.lg_config_k, 0, self.num_zeros, num_std_dev)
}
- /// Restores estimate state after copying or transforming the same logical
sketch.
pub fn restore_estimate_state(&mut self, state: EstimateState) {
self.estimator.restore_estimate_state(state);
}
- /// Check if the sketch is empty (all slots are zero)
pub fn is_empty(&self) -> bool {
self.num_zeros == (1 << self.lg_config_k)
}
- /// Deserialize Array6 from HLL mode bytes
- ///
- /// Expects full HLL preamble (40 bytes) followed by packed 6-bit data.
pub fn deserialize_registers(
mut cursor: SketchSlice,
lg_config_k: u8,
@@ -184,7 +149,6 @@ impl Array6 {
let k = 1 << lg_config_k;
let num_bytes = num_bytes_for_k(k);
- // Read estimator values from preamble
let hip_accum = cursor
.read_f64_le()
.map_err(insufficient_data("hip_accum"))?;
@@ -211,7 +175,6 @@ impl Array6 {
));
}
- // Read packed byte array from offset HLL_BYTE_ARR_START
let mut data = vec![0u8; num_bytes];
cursor
.read_exact(&mut data)
@@ -227,23 +190,18 @@ impl Array6 {
})
}
- /// Serialize Array6 to bytes
- ///
- /// Produces full HLL preamble (40 bytes) followed by packed 6-bit data.
pub fn serialize(&self, lg_config_k: u8) -> Vec<u8> {
let k = 1 << lg_config_k;
let num_bytes = num_bytes_for_k(k);
let total_size = HLL_PREAMBLE_SIZE + num_bytes;
let mut bytes = SketchBytes::with_capacity(total_size);
- // Write standard header
bytes.write_u8(HLL_PREINTS);
bytes.write_u8(SERIAL_VERSION);
bytes.write_u8(Family::HLL.id);
bytes.write_u8(lg_config_k);
bytes.write_u8(0); // unused for HLL mode
- // Write flags
let mut flags = 0u8;
if self.estimator.uses_composite_estimate() {
flags |= OUT_OF_ORDER_FLAG_MASK;
@@ -253,10 +211,8 @@ impl Array6 {
// cur_min is always 0 for Array6
bytes.write_u8(0);
- // Mode byte: HLL mode with HLL6 type
bytes.write_u8(encode_mode_byte(CUR_MODE_HLL, TGT_HLL6));
- // 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());
@@ -267,7 +223,6 @@ impl Array6 {
// Write aux_count (always 0 for Array6)
bytes.write_u32_le(0);
- // Write packed byte array
bytes.write(&self.bytes);
bytes.into_bytes()
@@ -279,10 +234,8 @@ impl Array6 {
}
}
-/// Calculate number of bytes needed for k slots with 6 bits each
fn num_bytes_for_k(k: u32) -> usize {
- // k slots * 6 bits = k * 6/8 bytes = k * 3/4 bytes
- // Add 1 for 16-bit window read safety
+ // The final register's 16-bit read/write window needs one padding byte.
(((k * 3) >> 2) + 1) as usize
}
@@ -298,10 +251,8 @@ mod tests {
#[test]
fn test_num_bytes_calculation() {
- // k=16 slots: 16 * 6 bits = 96 bits = 12 bytes
assert_eq!(num_bytes_for_k(16), (16 * 3 / 4) + 1);
- // k=1024: 1024 * 6 bits = 6144 bits = 768 bytes
assert_eq!(num_bytes_for_k(1024), (1024 * 3 / 4) + 1);
}
@@ -309,7 +260,6 @@ mod tests {
fn test_get_set_raw() {
let mut arr = Array6::new(4); // 16 slots
- // Test various 6-bit values across different slots
arr.put_raw(0, 0);
arr.put_raw(1, 1);
arr.put_raw(2, 31);
@@ -320,7 +270,6 @@ mod tests {
assert_eq!(arr.get_raw(2), 31);
assert_eq!(arr.get_raw(3), 63);
- // Test that values don't interfere with each other
arr.put_raw(5, 42);
assert_eq!(arr.get_raw(5), 42);
assert_eq!(arr.get_raw(3), 63); // Earlier value unchanged
@@ -338,7 +287,6 @@ mod tests {
fn test_boundary_crossing() {
let mut arr = Array6::new(8); // 256 slots
- // Test values that will cross byte boundaries
// Slot 1: starts at bit 6 (crosses byte 0/1 boundary)
arr.put_raw(1, 0b111111);
assert_eq!(arr.get_raw(1), 63);
@@ -351,7 +299,6 @@ mod tests {
arr.put_raw(3, 0b110011);
assert_eq!(arr.get_raw(3), 51);
- // Verify no interference
assert_eq!(arr.get_raw(1), 63);
assert_eq!(arr.get_raw(2), 42);
assert_eq!(arr.get_raw(3), 51);
@@ -361,21 +308,17 @@ mod tests {
fn test_hip_estimator() {
let mut arr = Array6::new(10); // 1024 buckets
- // Initially estimate should be 0
assert_eq!(arr.estimate(), 0.0);
- // Add some unique values using real coupon hashing
for i in 0..10_000u32 {
arr.update(Coupon::from_value(i));
}
let estimate = arr.estimate();
- // Sanity checks
assert_that!(estimate, gt(0.0));
assert_that!(estimate, is_finite());
- // Rough bounds for 10K unique items (very loose)
assert_that!(estimate, gt(1_000.0));
assert_that!(estimate, lt(100_000.0));
}
@@ -384,7 +327,6 @@ mod tests {
fn test_full_range() {
let mut arr = Array6::new(6); // 64 slots
- // Test all possible 6-bit values (0-63)
for val in 0..64u8 {
arr.put_raw(val as u32, val);
}
@@ -398,7 +340,6 @@ mod tests {
fn test_kxq_register_split() {
let mut arr = Array6::new(8); // 256 buckets
- // Test that values < 32 and >= 32 are handled correctly
arr.update(Coupon::pack(0, 10)); // value < 32, goes to kxq0
arr.update(Coupon::pack(1, 40)); // value >= 32, goes to kxq1
diff --git a/datasketches/src/hll/array8.rs b/datasketches/src/hll/array8.rs
index e28c724..a2f60aa 100644
--- a/datasketches/src/hll/array8.rs
+++ b/datasketches/src/hll/array8.rs
@@ -15,11 +15,6 @@
// specific language governing permissions and limitations
// under the License.
-//! HyperLogLog Array8 mode - 8-bit (1 byte per slot) representation
-//!
-//! Array8 is the simplest HLL array implementation, storing one byte per slot.
-//! This provides the maximum value range (0-255) with no bit-packing
complexity.
-
use crate::codec::SketchBytes;
use crate::codec::SketchSlice;
use crate::codec::assert::insufficient_data;
@@ -37,13 +32,11 @@ use crate::hll::serialization::SERIAL_VERSION;
use crate::hll::serialization::TGT_HLL8;
use crate::hll::serialization::encode_mode_byte;
-/// Core Array8 data structure - one byte per slot, no packing
+/// Registers stored one per byte.
#[derive(Debug, Clone, PartialEq)]
pub struct Array8 {
lg_config_k: u8,
- /// Direct byte array: bytes[slot] = value
bytes: Box<[u8]>,
- /// Count of slots with value 0
num_zeros: u32,
estimator: Estimator,
}
@@ -60,23 +53,16 @@ impl Array8 {
}
}
- /// Get value from a slot
- ///
- /// Direct array access - no bit manipulation required.
#[inline]
pub fn get(&self, slot: u32) -> u8 {
self.bytes[slot as usize]
}
- /// Set value in a slot
- ///
- /// Direct array write - no bit manipulation required.
#[inline]
fn put(&mut self, slot: u32, value: u8) {
self.bytes[slot as usize] = value;
}
- /// Update with a coupon
pub fn update(&mut self, coupon: Coupon) {
let mask = (1 << self.lg_config_k) - 1;
let slot = coupon.slot() & mask;
@@ -85,88 +71,64 @@ impl Array8 {
let old_value = self.get(slot);
if new_value > old_value {
- // Update HIP and KxQ registers via estimator
self.estimator
.update(self.lg_config_k, old_value, new_value);
- // Update the slot
self.put(slot, new_value);
- // Track num_zeros (count of slots with value 0)
if old_value == 0 {
self.num_zeros -= 1;
}
}
}
- /// 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)
}
- /// Get upper bound for cardinality estimate
pub fn upper_bound(&self, num_std_dev: NumStdDev) -> f64 {
self.estimator
.upper_bound(self.lg_config_k, 0, self.num_zeros, num_std_dev)
}
- /// Get lower bound for cardinality estimate
pub fn lower_bound(&self, num_std_dev: NumStdDev) -> f64 {
self.estimator
.lower_bound(self.lg_config_k, 0, self.num_zeros, num_std_dev)
}
- /// Check if the sketch is empty (all slots are zero)
pub fn is_empty(&self) -> bool {
self.num_zeros == (1 << self.lg_config_k)
}
- /// Get read access to register values (one byte per register)
pub fn values(&self) -> &[u8] {
&self.bytes
}
- /// Get the number of registers (K = 2^lg_config_k)
pub fn num_registers(&self) -> usize {
1 << self.lg_config_k
}
- /// Returns the estimate state independently from register-derived cached
values.
pub fn estimate_state(&self) -> EstimateState {
self.estimator.estimate_state()
}
- /// Restores estimate state after copying or transforming the same logical
sketch.
pub fn restore_estimate_state(&mut self, state: EstimateState) {
self.estimator.restore_estimate_state(state);
}
- /// Directly set a register value
- ///
- /// This bypasses the normal update path and directly modifies the
register.
- /// Caller must call rebuild_estimator_from_registers() after all
modifications.
+ /// Bypasses estimator updates. Call `rebuild_estimator_from_registers`
after all writes.
pub fn set_register(&mut self, slot: usize, value: u8) {
self.bytes[slot] = value;
}
- /// Rebuild estimator state from current register values
- ///
- /// Recomputes num_zeros, kxq0, and kxq1, then switches to composite
estimation.
- /// Should be called after bulk register modifications.
+ /// Rebuilds register-derived caches and invalidates HIP after bulk writes.
pub fn rebuild_estimator_from_registers(&mut self) {
self.rebuild_cached_values();
self.estimator.invalidate_hip();
}
- /// Merge another Array8 with the same lg_k
- ///
- /// Performs register-by-register max merge. HIP is invalidated because
the register update
- /// order is unavailable.
- ///
- /// # Panics
- ///
- /// Panics if src length doesn't match self length (different lg_k).
+ /// Merges registers and invalidates HIP because their update order is
unavailable.
pub fn merge_array_same_lgk(&mut self, src: &[u8]) {
assert_eq!(
src.len(),
@@ -182,22 +144,8 @@ impl Array8 {
self.estimator.invalidate_hip();
}
- /// Merge an array with larger lg_k (downsampling)
- ///
- /// When merging a source with lg_k > dst lg_k, multiple source registers
- /// map to each destination register using the masking operation:
- /// `dst_slot = src_slot & ((1 << dst_lg_k) - 1)`
- ///
- /// The destination takes the max of all source values that map to it.
- ///
- /// # Parameters
- ///
- /// * `src`: Source register values (length must be 2^src_lg_k)
- /// * `src_lg_k`: Log2 of source register count
- ///
- /// # Panics
- ///
- /// Panics if src_lg_k <= self.lg_config_k (not downsampling).
+ /// Register values come from a separate hash word, so folding slot
indices needs no
+ /// adjustment to the values. Merging independent histories invalidates
HIP.
pub fn merge_array_with_downsample(&mut self, src: &[u8], src_lg_k: u8) {
assert!(
src_lg_k > self.lg_config_k,
@@ -220,16 +168,10 @@ impl Array8 {
self.estimator.invalidate_hip();
}
- /// Rebuild cached values after bulk modifications
- ///
- /// Recomputes num_zeros by counting zero-valued registers.
- /// This is needed after merge operations that bypass normal update paths.
+ /// Rebuilds register-derived caches without changing the estimate state.
fn rebuild_cached_values(&mut self) {
self.num_zeros = self.bytes.iter().filter(|&&v| v == 0).count() as u32;
- // Recompute kxq values from actual register values
- // This is essential after bulk merges where registers change but
estimator isn't updated
- // incrementally
let mut kxq0_sum = 0.0;
let mut kxq1_sum = 0.0;
@@ -246,9 +188,6 @@ impl Array8 {
self.estimator.restore_kxq(kxq0_sum, kxq1_sum);
}
- /// Deserialize Array8 from HLL mode bytes
- ///
- /// Expects full HLL preamble (40 bytes) followed by k bytes of data.
pub fn deserialize_registers(
mut cursor: SketchSlice,
lg_config_k: u8,
@@ -256,7 +195,6 @@ impl Array8 {
) -> Result<Self, Error> {
let k = 1usize << lg_config_k;
- // Read estimator values from preamble
let hip_accum = cursor
.read_f64_le()
.map_err(insufficient_data("hip_accum"))?;
@@ -283,7 +221,6 @@ impl Array8 {
));
}
- // Read byte array from offset HLL_BYTE_ARR_START
let mut data = vec![0u8; k];
cursor
.read_exact(&mut data)
@@ -299,22 +236,17 @@ impl Array8 {
})
}
- /// Serialize Array8 to bytes
- ///
- /// Produces full HLL preamble (40 bytes) followed by k bytes of data.
pub fn serialize(&self, lg_config_k: u8) -> Vec<u8> {
let k = 1 << lg_config_k;
let total_size = HLL_PREAMBLE_SIZE + k as usize;
let mut bytes = SketchBytes::with_capacity(total_size);
- // Write standard header
bytes.write_u8(HLL_PREINTS);
bytes.write_u8(SERIAL_VERSION);
bytes.write_u8(Family::HLL.id);
bytes.write_u8(lg_config_k);
bytes.write_u8(0); // unused for HLL mode
- // Write flags
let mut flags = 0u8;
if self.estimator.uses_composite_estimate() {
flags |= OUT_OF_ORDER_FLAG_MASK;
@@ -324,10 +256,8 @@ impl Array8 {
// cur_min is always 0 for Array8
bytes.write_u8(0);
- // Mode byte: HLL mode with HLL8 type
bytes.write_u8(encode_mode_byte(CUR_MODE_HLL, TGT_HLL8));
- // 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());
@@ -338,7 +268,6 @@ impl Array8 {
// Write aux_count (always 0 for Array8)
bytes.write_u32_le(0);
- // Write byte array
bytes.write(&self.bytes);
bytes.into_bytes()
@@ -364,7 +293,6 @@ mod tests {
fn test_array8_basic() {
let arr = Array8::new(10); // 1024 buckets
- // Initially all slots should be 0
assert_eq!(arr.get(0), 0);
assert_eq!(arr.get(100), 0);
assert_eq!(arr.get(1023), 0);
@@ -374,16 +302,14 @@ mod tests {
fn test_get_set() {
let mut arr = Array8::new(4); // 16 slots
- // Test all possible 8-bit values
for slot in 0..16 {
- arr.put(slot, (slot * 17) as u8); // Various values
+ arr.put(slot, (slot * 17) as u8);
}
for slot in 0..16 {
assert_eq!(arr.get(slot), (slot * 17) as u8);
}
- // Test full range (0-255)
arr.put(0, 0);
arr.put(1, 127);
arr.put(2, 255);
@@ -397,20 +323,15 @@ mod tests {
fn test_update_basic() {
let mut arr = Array8::new(4);
- // Update slot 0 with value 5
arr.update(Coupon::pack(0, 5));
assert_eq!(arr.get(0), 5);
- // Update with a smaller value (should be ignored)
arr.update(Coupon::pack(0, 3));
assert_eq!(arr.get(0), 5);
- // Update with a larger value
arr.update(Coupon::pack(0, 42));
assert_eq!(arr.get(0), 42);
- // Test value at max coupon range (63)
- // Note: Coupon::pack only stores 6 bits (0-63)
arr.update(Coupon::pack(1, 63));
assert_eq!(arr.get(1), 63);
}
@@ -419,21 +340,17 @@ mod tests {
fn test_hip_estimator() {
let mut arr = Array8::new(10); // 1024 buckets
- // Initially estimate should be 0
assert_eq!(arr.estimate(), 0.0);
- // Add some unique values using real coupon hashing
for i in 0..10_000u32 {
arr.update(Coupon::from_value(i));
}
let estimate = arr.estimate();
- // Sanity checks
assert_that!(estimate, gt(0.0));
assert_that!(estimate, is_finite());
- // Rough bounds for 10K unique items (very loose)
assert_that!(estimate, gt(1_000.0));
assert_that!(estimate, lt(100_000.0));
}
@@ -442,7 +359,6 @@ mod tests {
fn test_full_value_range() {
let mut arr = Array8::new(8); // 256 slots
- // Test all possible 8-bit values (0-255)
for val in 0..=255u8 {
arr.put(val as u32, val);
}
@@ -456,9 +372,7 @@ mod tests {
fn test_high_value_direct() {
let mut arr = Array8::new(6); // 64 slots
- // Test that Array8 CAN store full range (0-255) directly
- // Even though coupons are limited to 6 bits (0-63)
- // Direct put/get bypasses coupon encoding
+ // Direct storage accepts byte values beyond the six-bit coupon range.
let test_values = [16, 32, 64, 128, 200, 255];
for (slot, &value) in test_values.iter().enumerate() {
@@ -466,7 +380,6 @@ mod tests {
assert_eq!(arr.get(slot as u32), value);
}
- // Verify no cross-slot corruption
for (slot, &value) in test_values.iter().enumerate() {
assert_eq!(arr.get(slot as u32), value);
}
@@ -476,7 +389,6 @@ mod tests {
fn test_kxq_register_split() {
let mut arr = Array8::new(8); // 256 buckets
- // Test that values < 32 and >= 32 are handled correctly
arr.update(Coupon::pack(0, 10)); // value < 32, goes to kxq0
arr.update(Coupon::pack(1, 50)); // value >= 32, goes to kxq1
@@ -492,12 +404,10 @@ mod tests {
fn test_values_access() {
let mut arr = Array8::new(4); // 16 slots
- // Set some values
arr.put(0, 10);
arr.put(5, 25);
arr.put(15, 63);
- // Test read access via values()
let vals = arr.values();
assert_eq!(vals.len(), 16);
assert_eq!(vals[0], 10);
@@ -511,20 +421,16 @@ mod tests {
let mut dst = Array8::new(4); // 16 slots
let mut src = Array8::new(4); // 16 slots
- // Set up dst with some values
dst.put(0, 10);
dst.put(1, 20);
dst.put(2, 30);
- // Set up src with overlapping and new values
src.put(1, 15); // Smaller than dst[1]=20, should keep 20
src.put(2, 35); // Larger than dst[2]=30, should update to 35
src.put(3, 40); // New value
- // Merge src into dst
dst.merge_array_same_lgk(src.values());
- // Check results
assert_eq!(dst.get(0), 10, "dst[0] unchanged");
assert_eq!(dst.get(1), 20, "dst[1] kept max value");
assert_eq!(dst.get(2), 35, "dst[2] updated to larger value");
@@ -533,7 +439,6 @@ mod tests {
// 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);
}
@@ -543,7 +448,6 @@ mod tests {
let mut dst = Array8::new(4); // 16 slots
let mut src = Array8::new(5); // 32 slots
- // Set up dst
dst.put(0, 10);
dst.put(1, 20);
@@ -553,10 +457,8 @@ mod tests {
src.put(1, 18); // maps to dst[1], max(20, 18) = 20
src.put(17, 30); // maps to dst[1], max(20, 30) = 30
- // Merge with downsampling
dst.merge_array_with_downsample(src.values(), 5);
- // Check results - dst takes max of all src slots that map to it
assert_eq!(dst.get(0), 25, "dst[0] = max(10, 15, 25)");
assert_eq!(dst.get(1), 30, "dst[1] = max(20, 18, 30)");
@@ -586,18 +488,14 @@ mod tests {
fn test_rebuild_cached_values() {
let mut arr = Array8::new(4); // 16 slots
- // Set some non-zero values
arr.put(0, 10);
arr.put(1, 20);
arr.put(2, 30);
- // Manually corrupt num_zeros
arr.num_zeros = 999;
- // Rebuild should fix it
arr.rebuild_cached_values();
- // Should be 13 zeros (16 total - 3 non-zero)
assert_eq!(arr.num_zeros, 13);
}
@@ -606,19 +504,16 @@ mod tests {
let mut dst = Array8::new(4);
let mut src = Array8::new(4);
- // Fill dst with ascending values
for i in 0..16 {
dst.put(i, i as u8);
}
- // Fill src with descending values
for i in 0..16 {
src.put(i, (15 - i) as u8);
}
dst.merge_array_same_lgk(src.values());
- // Result should be max at each position
for i in 0..16 {
let expected = (i as u8).max((15 - i) as u8);
assert_eq!(
diff --git a/datasketches/src/hll/aux_map.rs b/datasketches/src/hll/aux_map.rs
index 86339a5..5286fe9 100644
--- a/datasketches/src/hll/aux_map.rs
+++ b/datasketches/src/hll/aux_map.rs
@@ -15,22 +15,11 @@
// specific language governing permissions and limitations
// under the License.
-//! Auxiliary hash map for HLL Array4 exceptions
-//!
-//! Stores slot-value pairs for values that don't fit in the 4-bit main array.
-//! Uses open addressing with stride-based probing for collision resolution.
-
use crate::hll::Coupon;
use crate::hll::RESIZE_DENOMINATOR;
use crate::hll::RESIZE_NUMERATOR;
-/// Open-addressing hash table for exception values (values >= 15)
-///
-/// This hash map stores (slot_number, value) pairs where values have exceeded
-/// the 4-bit representation (after cur_min offset) in the main Array4.
-///
-/// Each entry is a [`Coupon`] packed as: [value (upper 6 bits) | slot_no
(lower 26 bits)].
-/// Empty entries are represented as [`Coupon::EMPTY`].
+/// Absolute register values for the `AUX_TOKEN` slots in `Array4`.
#[derive(Debug, Clone)]
pub struct AuxMap {
lg_size: u8,
@@ -41,13 +30,11 @@ pub struct AuxMap {
impl PartialEq for AuxMap {
fn eq(&self, other: &Self) -> bool {
- // Two aux maps are equal if they have the same lg_config_k
- // and the same non-empty entries (regardless of internal storage
order)
+ // Capacity and slot order do not affect the logical entries.
if self.lg_config_k != other.lg_config_k || self.count != other.count {
return false;
}
- // Collect and sort non-empty entries from both maps
let mut entries1: Vec<Coupon> = self
.entries
.iter()
@@ -68,10 +55,8 @@ impl PartialEq for AuxMap {
}
}
-/// Get lg_aux_arr_ints for a given lg_config_k
-///
-/// This determines the initial size of the auxiliary hash map
-/// based on the sketch size.
+/// Initial table sizes from DataSketches C++:
+///
<https://github.com/apache/datasketches-cpp/blob/5a055521/hll/include/HllUtil.hpp>
fn lg_aux_arr_ints(lg_config_k: u8) -> u8 {
static LG_AUX_ARR_INTS: &[u8] = &[
0, 2, 2, 2, 2, 2, 2, 3, 3, 3, // 0-9
@@ -83,7 +68,6 @@ fn lg_aux_arr_ints(lg_config_k: u8) -> u8 {
}
impl AuxMap {
- /// Create a new map with specified size
pub fn new(lg_config_k: u8) -> Self {
let lg_size = lg_aux_arr_ints(lg_config_k);
Self {
@@ -94,13 +78,11 @@ impl AuxMap {
}
}
- /// Insert a new slot-value pair
pub fn insert(&mut self, slot: u32, value: u8) {
let index = self.find(slot);
match index {
FindResult::Found(_) => {
- // Invariant: Array4 always check existence before inserting
- // a new value on the same slot.
+ // Array4 uses insert only when a slot first becomes an
exception.
unreachable!("slot {} already exists in aux map", slot);
}
FindResult::Empty(idx) => {
@@ -111,9 +93,6 @@ impl AuxMap {
}
}
- /// Get value for a slot
- ///
- /// Returns `None` if the slot is not found
pub fn get(&self, slot: u32) -> Option<u8> {
match self.find(slot) {
FindResult::Found(idx) => Some(self.entries[idx].value()),
@@ -121,24 +100,18 @@ impl AuxMap {
}
}
- /// Replace value for existing slot
pub fn replace(&mut self, slot: u32, value: u8) {
match self.find(slot) {
FindResult::Found(idx) => {
self.entries[idx] = Coupon::pack(slot, value);
}
FindResult::Empty(_) => {
- // Invariant: Array4 always check existence before replacing
- // an old value on the same slot.
+ // Every AUX_TOKEN slot must already have an entry.
unreachable!("slot {} not found in aux map", slot);
}
}
}
- /// Find slot in hash table using open addressing with stride
- ///
- /// Returns either the index where the slot is found, or the index
- /// of an empty slot where it could be inserted.
fn find(&self, slot: u32) -> FindResult {
let mask = (1 << self.lg_size) - 1;
let config_k_mask = (1 << self.lg_config_k) - 1;
@@ -157,20 +130,17 @@ impl AuxMap {
return FindResult::Found(probe as usize);
}
- // Open addressing with odd stride (guarantees full coverage)
+ // An odd stride visits every slot in a power-of-two table.
let stride = (slot >> self.lg_size) | 1;
probe = (probe + stride) & mask;
if probe == start {
- // Invariant: AuxMap::insert is responsible for
- // growing the AuxMap when a new entry is inserted
- // causing the map to be full.
+ // insert grows the table past 75% occupancy, leaving an empty
slot.
unreachable!("AuxMap full; no empty slots");
}
}
}
- /// Check if we need to grow the hash table (75% load factor)
fn check_grow(&mut self) {
let size = 1 << self.lg_size;
if (RESIZE_DENOMINATOR * self.count) > (RESIZE_NUMERATOR * size) {
@@ -178,19 +148,16 @@ impl AuxMap {
}
}
- /// Double the hash table size and rehash all entries
fn grow(&mut self) {
let new_lg_size = self.lg_size + 1;
let new_size = 1 << new_lg_size;
let new_mask = (1 << new_lg_size) - 1;
let mut new_entries = vec![Coupon::EMPTY; new_size].into_boxed_slice();
- // Rehash all entries into the larger table
for &entry in self.entries.iter() {
if !entry.is_empty() {
let slot = entry.slot();
- // Find position in new table
let mut probe = slot & new_mask;
let start_position = probe;
@@ -203,8 +170,7 @@ impl AuxMap {
let stride = (slot >> new_lg_size) | 1;
probe = (probe + stride) & new_mask;
if probe == start_position {
- // Invariant: there will always be space for all
- // `self.entries` in the `new_entries` array.
+ // Doubling capacity leaves room for every existing
entry.
unreachable!("AuxMap full; no empty slots");
}
}
@@ -215,7 +181,6 @@ impl AuxMap {
self.lg_size = new_lg_size;
}
- /// Iterate over (slot, value) pairs without consuming the map
pub fn iter(&self) -> impl Iterator<Item = (u32, u8)> + '_ {
let config_k_mask = (1 << self.lg_config_k) - 1;
self.entries.iter().filter_map(move |&entry| {
@@ -233,7 +198,6 @@ impl AuxMap {
}
}
-/// Iterator over AuxMap entries
pub struct AuxMapIter {
entries: std::vec::IntoIter<Coupon>,
config_k_mask: u32,
@@ -250,7 +214,7 @@ impl Iterator for AuxMapIter {
let value = entry.value();
return Some((slot, value));
}
- Some(_) => continue, // Skip empty entries
+ Some(_) => continue,
None => return None,
}
}
@@ -269,7 +233,6 @@ impl IntoIterator for AuxMap {
}
}
-/// Result of a find operation
enum FindResult {
Found(usize),
Empty(usize),
@@ -283,18 +246,15 @@ mod tests {
fn test_aux_map_basic_operations() {
let mut map = AuxMap::new(10);
- // Insert some values
map.insert(10, 20);
map.insert(50, 30);
map.insert(100, 40);
- // Get values
assert_eq!(map.get(10), Some(20));
assert_eq!(map.get(50), Some(30));
assert_eq!(map.get(100), Some(40));
assert_eq!(map.get(999), None);
- // Replace value
map.replace(50, 35);
assert_eq!(map.get(50), Some(35));
}
@@ -303,14 +263,11 @@ mod tests {
fn test_aux_map_growth() {
let mut map = AuxMap::new(8);
- // Insert enough to trigger resize (75% load factor)
map.insert(1, 15);
map.insert(2, 16);
map.insert(3, 17);
- // This should trigger a resize
map.insert(4, 18);
- // All values should still be accessible
assert_eq!(map.get(1), Some(15));
assert_eq!(map.get(2), Some(16));
assert_eq!(map.get(3), Some(17));
@@ -322,13 +279,13 @@ mod tests {
fn test_aux_map_duplicate_insert() {
let mut map = AuxMap::new(10);
map.insert(10, 20);
- map.insert(10, 30); // Should panic
+ map.insert(10, 30);
}
#[test]
#[should_panic(expected = "not found")]
fn test_aux_map_replace_missing() {
let mut map = AuxMap::new(10);
- map.replace(999, 20); // Should panic
+ map.replace(999, 20);
}
}
diff --git a/datasketches/src/hll/composite_interpolation.rs
b/datasketches/src/hll/composite_interpolation.rs
index e5e13e3..cf419bf 100644
--- a/datasketches/src/hll/composite_interpolation.rs
+++ b/datasketches/src/hll/composite_interpolation.rs
@@ -15,13 +15,7 @@
// specific language governing permissions and limitations
// under the License.
-//! Composite interpolation tables for HLL estimation.
-//!
-//! 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:
+//! Bias-correction tables for `lg_k` 4 through 21 from DataSketches C++:
//!
<https://github.com/apache/datasketches-cpp/blob/5a055521/hll/include/CompositeInterpolationXTable-internal.hpp>
const NUM_X_VALUES: usize = 257;
@@ -31,11 +25,6 @@ static Y_STRIDES: [u32; 18] = [
1, 2, 3, 5, 10, 20, 40, 80, 160, 320, 640, 1280, 2560, 5120, 10240, 20480,
40960, 81920,
];
-/// Get Y stride for a given lg_k.
-///
-/// # Panics
-///
-/// Panics if lg_k is not in range `[4, 21]`.
pub fn get_y_stride(lg_k: u8) -> u32 {
if !(4..=21).contains(&lg_k) {
panic!("lg_k must be in range [4, 21], got: {}", lg_k);
@@ -43,18 +32,10 @@ pub fn get_y_stride(lg_k: u8) -> u32 {
Y_STRIDES[(lg_k - 4) as usize]
}
-/// Get X array length (constant for all lg_k)
pub const fn get_x_arr_length() -> usize {
NUM_X_VALUES
}
-/// Get X array for a given lg_k.
-///
-/// Returns a reference to the pre-computed X values for cubic interpolation.
-///
-/// # Panics
-///
-/// Panics if lg_k is not in range `[4, 21]`.
pub fn get_x_arr(lg_k: u8) -> &'static [f64; NUM_X_VALUES] {
if !(4..=21).contains(&lg_k) {
panic!("lg_k must be in range [4, 21], got: {}", lg_k);
@@ -62,7 +43,6 @@ pub fn get_x_arr(lg_k: u8) -> &'static [f64; NUM_X_VALUES] {
&ARRAYS[(lg_k - 4) as usize]
}
-/// This is extracted from
https://github.com/apache/datasketches-cpp/blob/5a055521/hll/include/CompositeInterpolationXTable-internal.hpp#L54
static ARRAYS: [[f64; NUM_X_VALUES]; 18] = [
[
10.767999803534,
@@ -4796,7 +4776,6 @@ mod tests {
#[test]
fn test_x_arr_monotonic() {
- // X array should be strictly increasing
let x_arr = get_x_arr(8);
for i in 1..x_arr.len() {
assert_that!(x_arr[i], gt(x_arr[i - 1]), "index: {i}");
diff --git a/datasketches/src/hll/container.rs
b/datasketches/src/hll/container.rs
index e15d6bc..8f0313c 100644
--- a/datasketches/src/hll/container.rs
+++ b/datasketches/src/hll/container.rs
@@ -15,11 +15,6 @@
// specific language governing permissions and limitations
// under the License.
-//! Base container for coupon storage with cardinality estimation
-//!
-//! Provides a simple array-based storage for coupons (hash values) with
-//! cubic interpolation-based cardinality estimation and confidence bounds.
-
use crate::common::NumStdDev;
use crate::hll::COUPON_RSE;
use crate::hll::Coupon;
@@ -27,12 +22,10 @@ use crate::hll::coupon_mapping::X_ARR;
use crate::hll::coupon_mapping::Y_ARR;
use crate::hll::cubic_interpolation::using_x_and_y_tables;
-/// Container for storing coupons with basic cardinality estimation
#[derive(Debug, Clone)]
pub struct Container {
- /// Log2 of container size
lg_size: usize,
- /// Array of coupon values (Coupon::EMPTY = empty)
+ /// Empty slots contain `Coupon::EMPTY`.
pub coupons: Box<[Coupon]>,
/// Number of non-empty coupons
pub len: usize,
@@ -40,8 +33,7 @@ pub struct Container {
impl PartialEq for Container {
fn eq(&self, other: &Self) -> bool {
- // Two containers are equal if they have the same non-empty coupons
- // (regardless of order or internal storage)
+ // Capacity and slot order do not affect the logical coupon contents.
if self.len != other.len {
return false;
}
@@ -75,7 +67,6 @@ impl Container {
}
}
- /// Create container from existing coupons
pub fn from_coupons(lg_size: usize, coupons: Box<[Coupon]>, len: usize) ->
Self {
Self {
lg_size,
@@ -104,34 +95,28 @@ impl Container {
self.coupons.len()
}
- /// Get cardinality estimate using cubic interpolation
pub fn estimate(&self) -> f64 {
let len = self.len as f64;
let est = using_x_and_y_tables(&X_ARR, &Y_ARR, len);
len.max(est)
}
- /// Get upper confidence bound for cardinality estimate
pub fn upper_bound(&self, num_std_dev: NumStdDev) -> f64 {
let len = self.len as f64;
let est = using_x_and_y_tables(&X_ARR, &Y_ARR, len);
- // Upper bound: negative RSE means (1 + rse) < 1, so bound > estimate
let rse = -(num_std_dev as u8 as f64) * COUPON_RSE;
let bound = est / (1.0 + rse);
len.max(bound)
}
- /// Get lower confidence bound for cardinality estimate
pub fn lower_bound(&self, num_std_dev: NumStdDev) -> f64 {
let len = self.len as f64;
let est = using_x_and_y_tables(&X_ARR, &Y_ARR, len);
- // Lower bound: positive RSE means (1 + rse) > 1, so bound < estimate
let rse = (num_std_dev as u8 as f64) * COUPON_RSE;
let bound = est / (1.0 + rse);
len.max(bound)
}
- /// Iterate over all non-empty coupons
pub fn iter(&self) -> impl Iterator<Item = Coupon> + '_ {
self.coupons.iter().filter(|&&c| !c.is_empty()).copied()
}
diff --git a/datasketches/src/hll/coupon_mapping.rs
b/datasketches/src/hll/coupon_mapping.rs
index 1b4d687..0ee8d43 100644
--- a/datasketches/src/hll/coupon_mapping.rs
+++ b/datasketches/src/hll/coupon_mapping.rs
@@ -15,10 +15,9 @@
// specific language governing permissions and limitations
// under the License.
-//! Lookup tables for coupon-based cardinality estimation
-//!
-//! These pre-computed X and Y value pairs are used with cubic interpolation
-//! to map from the number of observed coupons to estimated cardinality.
+//! Coupon-count correction for the 26-bit slot encoding. These X/Y pairs are
used with
+//! cubic interpolation and come from DataSketches C++:
+//!
<https://github.com/apache/datasketches-cpp/blob/5a055521/hll/include/CubicInterpolation-internal.hpp>
/// X values (coupon counts) for interpolation table
pub static X_ARR: [f64; 40] = [
diff --git a/datasketches/src/hll/cubic_interpolation.rs
b/datasketches/src/hll/cubic_interpolation.rs
index e192318..440a42b 100644
--- a/datasketches/src/hll/cubic_interpolation.rs
+++ b/datasketches/src/hll/cubic_interpolation.rs
@@ -15,12 +15,9 @@
// specific language governing permissions and limitations
// under the License.
-//! Cubic interpolation utilities for cardinality estimation
-//!
-//! Implements Lagrange cubic interpolation over lookup tables to provide
-//! smooth, accurate cardinality estimates from discrete observations.
+//! Four-point Lagrange interpolation for the HLL correction tables.
+//! The X coordinates must be strictly increasing.
-/// Interpolate Y value from X using pre-computed X/Y tables
pub fn using_x_and_y_tables(x_arr: &[f64], y_arr: &[f64], x: f64) -> f64 {
debug_assert!(x_arr.len() >= 4 && x_arr.len() == y_arr.len());
@@ -28,13 +25,13 @@ pub fn using_x_and_y_tables(x_arr: &[f64], y_arr: &[f64],
x: f64) -> f64 {
debug_assert!(x >= x_arr[0] && x < x_arr[last_idx]);
if x == x_arr[last_idx] {
- return y_arr[last_idx]; // corner case
+ return y_arr[last_idx];
}
let offset = find_straddle(x_arr, x);
debug_assert!(offset < last_idx);
- // Select 4-point window based on position in array
+ // Keep the four-point window inside the table at either end.
if offset == 0 {
return interpolate_using_x_and_y_tables(x_arr, y_arr, offset, x);
}
@@ -46,7 +43,6 @@ pub fn using_x_and_y_tables(x_arr: &[f64], y_arr: &[f64], x:
f64) -> f64 {
interpolate_using_x_and_y_tables(x_arr, y_arr, offset - 1, x)
}
-/// Helper to perform cubic interpolation at offset using X/Y tables
fn interpolate_using_x_and_y_tables(x_arr: &[f64], y_arr: &[f64], offset:
usize, x: f64) -> f64 {
cubic_interpolate(
x_arr[offset],
@@ -61,7 +57,6 @@ fn interpolate_using_x_and_y_tables(x_arr: &[f64], y_arr:
&[f64], offset: usize,
)
}
-/// Interpolate Y value from X using X array and uniform Y stride
pub fn using_x_arr_and_y_stride(x_arr: &[f64], y_stride: f64, x: f64) -> f64 {
let len = x_arr.len();
debug_assert!(len >= 4);
@@ -70,7 +65,6 @@ pub fn using_x_arr_and_y_stride(x_arr: &[f64], y_stride: f64,
x: f64) -> f64 {
debug_assert!(x >= x_arr[0] && x <= x_arr[last_idx]);
if x == x_arr[last_idx] {
- // corner case
return y_stride * (last_idx as f64);
}
@@ -79,14 +73,11 @@ pub fn using_x_arr_and_y_stride(x_arr: &[f64], y_stride:
f64, x: f64) -> f64 {
debug_assert!(offset <= len_m2);
if offset == 0 {
- // corner case
return interpolate_using_x_arr_and_y_stride(x_arr, y_stride, offset,
x);
} else if offset == len_m2 {
- // corner case: offset - 2
return interpolate_using_x_arr_and_y_stride(x_arr, y_stride, offset -
2, x);
}
- // main case: offset - 1
interpolate_using_x_arr_and_y_stride(x_arr, y_stride, offset - 1, x)
}
@@ -109,7 +100,6 @@ fn interpolate_using_x_arr_and_y_stride(
)
}
-/// Cubic interpolation using the Lagrange interpolation formula.
fn cubic_interpolate(
x0: f64,
y0: f64,
@@ -139,7 +129,7 @@ fn cubic_interpolate(
term0 + term1 + term2 + term3
}
-/// Find index `i` such that x_arr[i] <= x < x_arr[i+1].
+/// Finds `i` such that `x_arr[i] <= x < x_arr[i + 1]`.
fn find_straddle(x_arr: &[f64], x: f64) -> usize {
debug_assert!(x_arr.len() >= 2);
let last_idx = x_arr.len() - 1;
@@ -148,10 +138,9 @@ fn find_straddle(x_arr: &[f64], x: f64) -> usize {
recursive_find_straddle(x_arr, 0, last_idx, x)
}
-/// Recursive helper for `find_straddle`.
fn recursive_find_straddle(x_arr: &[f64], left: usize, right: usize, x: f64)
-> usize {
debug_assert!(left < right);
- debug_assert!(x_arr[left] <= x && x < x_arr[right]); // invariant
+ debug_assert!(x_arr[left] <= x && x < x_arr[right]);
if left + 1 == right {
return left;
diff --git a/datasketches/src/hll/estimator.rs
b/datasketches/src/hll/estimator.rs
index 8897516..1a22809 100644
--- a/datasketches/src/hll/estimator.rs
+++ b/datasketches/src/hll/estimator.rs
@@ -19,6 +19,9 @@
//!
//! Sequential register updates use HIP (Historical Inverse Probability). Bulk
merges use the
//! composite estimator because register values do not retain their update
order.
+//!
+//! Estimator formulas and crossover constants follow DataSketches C++:
+//!
<https://github.com/apache/datasketches-cpp/blob/5a055521/hll/include/HllArray-internal.hpp>
use crate::common::NumStdDev;
use crate::common::inv_pow2::inv_pow2;
@@ -35,13 +38,10 @@ pub enum EstimateState {
Composite,
}
-/// Cardinality estimator shared by Array4, Array6, and Array8.
-///
-/// 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.
+/// KxQ caches the sum of `2^-register`. Splitting the sum at register value
32 preserves
+/// precision when large and small contributions coexist.
#[derive(Debug, Clone, PartialEq)]
pub struct Estimator {
- /// Estimate selected by the register history.
estimate_state: EstimateState,
/// KxQ register for values < 32 (larger inverse powers)
kxq0: f64,
@@ -50,7 +50,6 @@ pub struct Estimator {
}
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 {
@@ -74,22 +73,11 @@ impl Estimator {
}
}
- /// Update the estimator when a register changes from old_value to
new_value
- ///
- /// This should be called BEFORE actually updating the register in the
array.
- ///
- /// # Algorithm
- ///
- /// 1. Update the HIP accumulator when it remains valid
- /// 2. Update KxQ registers (always)
- ///
- /// The KxQ registers are split for numerical precision:
- /// * kxq0: sum of 1/2^v for v < 32
- /// * kxq1: sum of 1/2^v for v >= 32
+ /// Applies a register increase; requires `new_value > old_value`.
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 when the register history is still
available.
+ // HIP must use the KxQ sums from before this register update.
if let EstimateState::Hip(hip_accum) = &mut self.estimate_state {
*hip_accum += k / (self.kxq0 + self.kxq1);
}
@@ -98,16 +86,13 @@ impl Estimator {
self.update_kxq(old_value, new_value);
}
- /// Update only the KxQ registers (internal helper)
fn update_kxq(&mut self, old_value: u8, new_value: u8) {
- // Subtract old value contribution
if old_value < 32 {
self.kxq0 -= inv_pow2(old_value);
} else {
self.kxq1 -= inv_pow2(old_value);
}
- // Add new value contribution
if new_value < 32 {
self.kxq0 += inv_pow2(new_value);
} else {
@@ -115,15 +100,8 @@ impl Estimator {
}
}
- /// Get the current cardinality estimate
- ///
- /// Dispatches to the estimate selected by the register history.
- ///
- /// # Arguments
- ///
- /// * `lg_config_k`: Log2 of number of registers (k)
- /// * `cur_min`: Current minimum register value (for Array4, 0 for
Array6/8)
- /// * `num_at_cur_min`: Number of registers at cur_min value
+ /// For `Array6`/`Array8`, pass zero for `cur_min` and the zero-register
count for
+ /// `num_at_cur_min`. `Array4` tracks its actual minimum and its count.
pub fn estimate(&self, lg_config_k: u8, cur_min: u8, num_at_cur_min: u32)
-> f64 {
match self.estimate_state {
EstimateState::Hip(hip_accum) => hip_accum,
@@ -133,16 +111,6 @@ impl Estimator {
}
}
- /// Get upper bound for cardinality estimate
- ///
- /// Returns the upper confidence bound for the cardinality estimate.
- ///
- /// # Arguments
- ///
- /// * `lg_config_k`: Log2 of number of registers (k)
- /// * `cur_min`: Current minimum register value (for Array4, 0 for
Array6/8)
- /// * `num_at_cur_min`: Number of registers at cur_min value
- /// * `num_std_dev`: Number of standard deviations (1, 2, or 3)
pub fn upper_bound(
&self,
lg_config_k: u8,
@@ -157,23 +125,11 @@ impl Estimator {
self.uses_composite_estimate(),
num_std_dev,
);
- // RSE is negative for upper bounds, so (1 + rse) < 1, making bound >
estimate
estimate / (1.0 + rse)
}
- /// Get lower bound for cardinality estimate
- ///
- /// Returns the lower confidence bound for the cardinality estimate.
- ///
- /// Each non-zero register requires a distinct item, so their count is a
hard floor
- /// for the lower bound. If `cur_min` is non-zero, all `k` registers are
non-zero.
- ///
- /// # Arguments
- ///
- /// * `lg_config_k`: Log2 of number of registers (k)
- /// * `cur_min`: Current minimum register value (for Array4, 0 for
Array6/8)
- /// * `num_at_cur_min`: Number of registers at cur_min value
- /// * `num_std_dev`: Number of standard deviations (1, 2, or 3)
+ /// Each nonzero register requires a distinct item, so their count is a
hard floor
+ /// for the lower bound. If `cur_min` is nonzero, all `k` registers are
nonzero.
pub fn lower_bound(
&self,
lg_config_k: u8,
@@ -194,15 +150,9 @@ impl Estimator {
} else {
config_k
};
- // RSE is positive for lower bounds, so (1 + rse) > 1, making bound <
estimate
(estimate / (1.0 + rse)).max(f64::from(num_nonzero_registers))
}
- /// Get raw HLL estimate using standard HyperLogLog formula
- ///
- /// Formula: correctionFactor * k^2 / (kxq0 + kxq1)
- ///
- /// Uses lg_k-specific correction factors for small k.
fn raw_estimate(&self, lg_config_k: u8) -> f64 {
let k = (1 << lg_config_k) as f64;
@@ -217,16 +167,12 @@ impl Estimator {
(correction_factor * k * k) / (self.kxq0 + self.kxq1)
}
- /// Get linear counting (bitmap) estimate for small cardinalities
- ///
- /// Uses harmonic numbers to estimate based on empty registers.
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
let num_unhit = if cur_min == 0 { num_at_cur_min } else { 0 };
- // Edge case: all buckets hit
+ // Preserve the upstream fallback for a bitmap with no empty registers.
if num_unhit == 0 {
return (k as f64) * (k as f64 / 0.5).ln();
}
@@ -235,20 +181,14 @@ impl Estimator {
harmonic_numbers::bitmap_estimate(k, num_hit)
}
- /// Get composite estimate (blends raw HLL and linear counting)
- ///
- /// 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.
+ /// Selects between bias-corrected HLL and bitmap estimates using their
empirical crossover.
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);
let x_arr_len = composite_interpolation::get_x_arr_length();
let y_stride = composite_interpolation::get_y_stride(lg_config_k) as
f64;
- // Handle edge cases
if raw_estimate < x_arr[0] {
return 0.0;
}
@@ -262,7 +202,6 @@ impl Estimator {
return raw_estimate * factor;
}
- // Interpolate using cubic interpolation
let adjusted_estimate =
cubic_interpolation::using_x_arr_and_y_stride(x_arr, y_stride,
raw_estimate);
@@ -273,11 +212,9 @@ impl Estimator {
return adjusted_estimate;
}
- // Get linear counting estimate
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
+ // Comparing the average to the threshold reduces estimator-selection
bias.
let average_estimate = (adjusted_estimate + linear_estimate) / 2.0;
// Crossover thresholds (empirically determined)
@@ -296,7 +233,6 @@ impl Estimator {
}
}
- /// Get the HIP accumulator value
pub fn hip_accum(&self) -> f64 {
match self.estimate_state {
EstimateState::Hip(hip_accum) => hip_accum,
@@ -304,22 +240,18 @@ impl Estimator {
}
}
- /// Get the kxq0 register value
pub fn kxq0(&self) -> f64 {
self.kxq0
}
- /// Get the kxq1 register value
pub fn kxq1(&self) -> f64 {
self.kxq1
}
- /// Returns whether estimates are derived from registers rather than HIP
history.
pub fn uses_composite_estimate(&self) -> bool {
matches!(self.estimate_state, EstimateState::Composite)
}
- /// Returns the estimate state independently from register-derived KxQ
values.
pub fn estimate_state(&self) -> EstimateState {
self.estimate_state
}
@@ -341,27 +273,15 @@ impl Estimator {
}
}
-/// Get relative error for HLL estimates
-///
-/// This matches the implementation in datasketches-cpp HllUtil.hpp and
RelativeErrorTables.hpp
-///
-/// # Arguments
-///
-/// * `lg_config_k`: Log2 of number of registers (must be 4-21)
-/// * `upper_bound`: Whether computing upper bound (vs lower bound)
-/// * `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
+/// Relative error used as `estimate / (1 + error)`: negative for upper bounds.
+/// Follows DataSketches C++ `HllUtil::getRelErr`:
+///
<https://github.com/apache/datasketches-cpp/blob/5a055521/hll/include/HllUtil.hpp>
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
@@ -378,21 +298,19 @@ fn relative_error(
return sign * (num_std_dev as u8 as f64) * rse_factor / k.sqrt();
}
- // For lg_k <= 12, use empirically measured lookup tables.
- // Tables are indexed by: ((lg_k - 4) * 3) + (num_std_dev - 1)
+ // Small sketches use measured error quantiles rather than the asymptotic
formula.
let idx = ((lg_config_k as usize) - 4) * 3 + ((num_std_dev as usize) - 1);
- // 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
- (true, true) => NON_HIP_UB[idx], // Case 3: Non-HIP, Upper Bound
+ (false, false) => HIP_LB[idx],
+ (false, true) => HIP_UB[idx],
+ (true, false) => NON_HIP_LB[idx],
+ (true, true) => NON_HIP_UB[idx],
}
}
-// Relative error lookup tables from Apache DataSketches C++ implementation
-// RelativeErrorTables-internal.hpp
+// Measured quantiles, grouped by lg_k 4–12 with three standard deviations per
group.
+// Source:
https://github.com/apache/datasketches-cpp/blob/5a055521/hll/include/RelativeErrorTables-internal.hpp
/// HIP (in-order) Lower Bound errors for lg_k 4-12, std_dev 1-3
/// Q(.84134), Q(.97725), Q(.99865) quantiles
@@ -572,22 +490,18 @@ mod tests {
fn test_estimator_update() {
let mut est = Estimator::new(8); // 256 registers
- // Update from 0 to 10
est.update(8, 0, 10);
- // HIP should have increased
assert_that!(est.hip_accum(), gt(0.0));
- // kxq0 should have changed (10 < 32)
assert_that!(est.kxq0(), lt(256.0));
- assert_eq!(est.kxq1(), 0.0); // kxq1 unchanged
+ assert_eq!(est.kxq1(), 0.0);
}
#[test]
fn test_kxq_split() {
let mut est = Estimator::new(8);
- // Update to value < 32 (goes to kxq0)
est.update(8, 0, 10);
let kxq0_after_10 = est.kxq0();
let kxq1_after_10 = est.kxq1();
diff --git a/datasketches/src/hll/harmonic_numbers.rs
b/datasketches/src/hll/harmonic_numbers.rs
index 649ab22..cef22c7 100644
--- a/datasketches/src/hll/harmonic_numbers.rs
+++ b/datasketches/src/hll/harmonic_numbers.rs
@@ -15,10 +15,8 @@
// specific language governing permissions and limitations
// under the License.
-//! Harmonic number calculations for linear counting estimator
-//!
-//! Provides utilities for computing harmonic numbers used in the
-//! HLL bitmap estimator for small cardinalities.
+//! Harmonic-number approximation used by the bitmap estimator, following
DataSketches C++:
+//!
<https://github.com/apache/datasketches-cpp/blob/5a055521/hll/include/HarmonicNumbers-internal.hpp>
const NUM_EXACT: usize = 25;
const EULER_MASCHERONI: f64 = 0.577_215_664_901_532_9;
@@ -52,9 +50,7 @@ static EXACT_HARMONIC: [f64; NUM_EXACT] = [
1347822955.0 / 356948592.0, // H(24)
];
-/// Compute the n-th harmonic number H(n) = 1 + 1/2 + 1/3 + ... + 1/n
-///
-/// Uses exact table for small n, asymptotic expansion for large n.
+/// Computes `H(n) = 1 + 1/2 + ... + 1/n`, with `H(0) = 0`.
fn harmonic_number(n: usize) -> f64 {
if n < NUM_EXACT {
return EXACT_HARMONIC[n];
@@ -64,7 +60,7 @@ fn harmonic_number(n: usize) -> f64 {
let inv_sq = 1.0 / (x * x);
let mut sum = x.ln() + EULER_MASCHERONI + (1.0 / (2.0 * x));
- // Asymptotic expansion (appropriate for n >= 25)
+ // The cutoff at 25 and expansion through n^-8 are chosen together for f64
precision.
let mut pow = inv_sq; // n^-2
sum -= pow * (1.0 / 12.0);
@@ -80,18 +76,8 @@ fn harmonic_number(n: usize) -> f64 {
sum
}
-/// Bitmap estimator for flat random-access bit vectors (similar to Bloom
filter)
-///
-/// This is used for linear counting in the HLL composite estimator.
-///
-/// # Arguments
-///
-/// * `bit_vector_length`: Total length of bit vector (k for HLL)
-/// * `num_bits_set`: Number of bits set (non-zero registers)
-///
-/// # Returns
-///
-/// Estimated cardinality based on coupon collector problem
+/// Expected draws needed to hit `num_bits_set` distinct slots under the
coupon collector model.
+/// Requires `num_bits_set <= bit_vector_length`.
pub fn bitmap_estimate(bit_vector_length: u32, num_bits_set: u32) -> f64 {
let k = bit_vector_length;
let num_set = num_bits_set;
@@ -113,16 +99,12 @@ mod tests {
#[test]
fn test_exact_harmonic_numbers() {
- // H(1) = 1
assert_that!(harmonic_number(1), near(1.0, 1e-10));
- // H(2) = 1 + 1/2 = 1.5
assert_that!(harmonic_number(2), near(1.5, 1e-10));
- // H(3) = 1 + 1/2 + 1/3 = 11/6
assert_that!(harmonic_number(3), near(11.0 / 6.0, 1e-10));
- // H(10) should be exact from table
let expected = 1.0
+ 1.0 / 2.0
+ 1.0 / 3.0
@@ -143,20 +125,17 @@ mod tests {
let h_n = harmonic_number(n);
let approx = (n as f64).ln() + EULER_MASCHERONI + 1.0 / (2.0 * n as
f64);
- // Should be close (within 0.1%)
assert_that!(h_n, near(approx, h_n * 0.001));
}
#[test]
fn test_bitmap_estimate_empty() {
- // No bits set = estimate should be near 0
let est = bitmap_estimate(1024, 0);
assert_that!(est, near(0.0, 1e-6));
}
#[test]
fn test_bitmap_estimate_full() {
- // All bits set = should estimate large value
let k = 1024;
let est = bitmap_estimate(k, k);
@@ -170,11 +149,9 @@ mod tests {
#[test]
fn test_bitmap_estimate_half() {
- // Half bits set
let k = 1024;
let est = bitmap_estimate(k, k / 2);
- // Should be between 0 and k * H(k)
assert_that!(est, gt(0.0));
assert_that!(est, lt(k as f64 * harmonic_number(k as usize)));
}
diff --git a/datasketches/src/hll/hash_set.rs b/datasketches/src/hll/hash_set.rs
index dd2f5be..d78be7c 100644
--- a/datasketches/src/hll/hash_set.rs
+++ b/datasketches/src/hll/hash_set.rs
@@ -15,11 +15,6 @@
// specific language governing permissions and limitations
// under the License.
-//! Hash set for storing unique coupons with linear probing
-//!
-//! Uses open addressing with a custom stride function to handle collisions.
-//! Provides better performance than List when many coupons are stored.
-
use crate::codec::SketchBytes;
use crate::codec::SketchSlice;
use crate::codec::assert::insufficient_data;
@@ -36,7 +31,6 @@ use crate::hll::serialization::SERIAL_VERSION;
use crate::hll::serialization::SET_PREAMBLE_SIZE;
use crate::hll::serialization::encode_mode_byte;
-/// Hash set for efficient coupon storage with collision handling
#[derive(Debug, Clone, PartialEq)]
pub struct HashSet {
container: Container,
@@ -56,33 +50,27 @@ impl HashSet {
}
}
- /// Insert coupon into hash set, ignoring duplicates
pub fn update(&mut self, coupon: Coupon) {
let mask = (1 << self.container.lg_size()) - 1;
- // Initial probe position from low bits of coupon
let mut probe = coupon.raw() & mask;
let starting_position = probe;
loop {
let slot = &mut self.container.coupons[probe as usize];
if slot.is_empty() {
- // Found empty slot, insert new coupon
*slot = coupon;
self.container.len += 1;
break;
} else if *slot == coupon {
- // Duplicate found, nothing to do
break;
}
- // Collision: compute stride and probe next position
- // Stride is always odd to ensure all slots are visited
+ // An odd stride visits every slot in a power-of-two table.
let stride = ((coupon.raw() & KEY_MASK_26) >>
self.container.lg_size()) | 1;
probe = (probe + stride) & mask;
if probe == starting_position {
- // Invariant: the caller (HllSketch) is responsible for
- // growing / upgrading the HashSet when it's full
+ // HllSketch must grow or promote the set before it runs out
of empty slots.
unreachable!("HashSet full; no empty slots");
}
}
@@ -92,7 +80,6 @@ impl HashSet {
&self.container
}
- /// Deserialize a HashSet from bytes
pub fn deserialize(
mut cursor: SketchSlice,
lg_arr: usize,
@@ -120,8 +107,7 @@ impl HashSet {
}
if compact {
- // Compact mode: only couponCount coupons are stored
- // Create a new hash set and insert coupons one by one
+ // Compact images omit empty slots, so probe positions must be
rebuilt.
let mut hash_set = HashSet::new(lg_arr);
for i in 0..coupon_count {
let coupon = cursor.read_u32_le().map_err(|error| {
@@ -135,8 +121,7 @@ impl HashSet {
}
Ok(hash_set)
} else {
- // Non-compact mode: full hash table with empty slots
- // Read entire hash table including empty slots
+ // Updatable images contain the probe positions; preserve their
table layout.
let mut coupons = vec![Coupon::EMPTY; array_size];
for (i, coupon) in coupons.iter_mut().enumerate() {
let raw = cursor.read_u32_le().map_err(|error| {
@@ -161,51 +146,30 @@ impl HashSet {
}
}
- /// Serialize a HashSet to bytes
+ /// Serializes occupied coupons in compact format.
pub fn serialize(&self, lg_config_k: u8, hll_type: HllType) -> Vec<u8> {
- let compact = true; // Always use compact format
let coupon_count = self.container.len();
let lg_arr = self.container.lg_size();
-
- // Compute size
- let array_size = if compact { coupon_count } else { 1 << lg_arr };
- let total_size = SET_PREAMBLE_SIZE + (array_size * 4);
+ let total_size = SET_PREAMBLE_SIZE + (coupon_count * size_of::<u32>());
let mut bytes = SketchBytes::with_capacity(total_size);
- // Write preamble
bytes.write_u8(HASH_SET_PREINTS);
bytes.write_u8(SERIAL_VERSION);
bytes.write_u8(Family::HLL.id);
bytes.write_u8(lg_config_k);
bytes.write_u8(lg_arr as u8);
- // Write flags
- let mut flags = 0u8;
- if compact {
- flags |= COMPACT_FLAG_MASK;
- }
- bytes.write_u8(flags);
+ bytes.write_u8(COMPACT_FLAG_MASK);
- // Write unused byte
- bytes.write_u8(0);
+ bytes.write_u8(0); // Unused state byte in Set mode.
- // Write mode byte: SET mode with target HLL type
bytes.write_u8(encode_mode_byte(CUR_MODE_SET, hll_type as u8));
- // Write coupon count
bytes.write_u32_le(coupon_count as u32);
- // Write coupons
- if compact {
- for coupon in self.container.iter() {
- bytes.write_u32_le(coupon.raw());
- }
- } else {
- // Non-compact mode: write entire hash table
- for coupon in self.container.coupons.iter().copied() {
- bytes.write_u32_le(coupon.raw());
- }
+ for coupon in self.container.iter() {
+ bytes.write_u32_le(coupon.raw());
}
bytes.into_bytes()
diff --git a/datasketches/src/hll/list.rs b/datasketches/src/hll/list.rs
index 47370fa..748b187 100644
--- a/datasketches/src/hll/list.rs
+++ b/datasketches/src/hll/list.rs
@@ -15,11 +15,6 @@
// specific language governing permissions and limitations
// under the License.
-//! Simple list for storing unique coupons in order
-//!
-//! Provides sequential storage with linear search for duplicates.
-//! Efficient for small numbers of coupons before transitioning to HashSet.
-
use crate::codec::SketchBytes;
use crate::codec::SketchSlice;
use crate::codec::family::Family;
@@ -35,7 +30,6 @@ use crate::hll::serialization::LIST_PREINTS;
use crate::hll::serialization::SERIAL_VERSION;
use crate::hll::serialization::encode_mode_byte;
-/// List for sequential coupon storage with duplicate detection
#[derive(Debug, Clone, PartialEq)]
pub struct List {
container: Container,
@@ -55,16 +49,13 @@ impl List {
}
}
- /// Insert coupon into list, ignoring duplicates
pub fn update(&mut self, coupon: Coupon) {
for value in self.container.coupons.iter_mut() {
if value.is_empty() {
- // Found empty slot, insert new coupon
*value = coupon;
self.container.len += 1;
break;
} else if *value == coupon {
- // Duplicate found, nothing to do
break;
}
}
@@ -74,7 +65,6 @@ impl List {
&self.container
}
- /// Deserialize a List from bytes
pub fn deserialize(
mut cursor: SketchSlice,
lg_arr: usize,
@@ -82,10 +72,8 @@ impl List {
empty: bool,
compact: bool,
) -> Result<Self, Error> {
- // Always allocate the full-sized array (1 << lg_arr) so Coupon::EMPTY
sentinel
- // slots are available for future update() calls. In compact format
only
- // coupon_count values are stored on disk, but memory must hold the
full capacity
- // so the linear scan in update() can find an empty slot to insert
into.
+ // Compact images omit empty slots. Restore the full capacity so
future updates
+ // can insert into an empty slot before the list promotes.
let array_size = 1usize << lg_arr;
if coupon_count > array_size {
return Err(Error::deserial(format!(
@@ -109,7 +97,6 @@ impl List {
}
}
- // Read coupons into the front of the full-sized array; remaining
slots stay Coupon::EMPTY.
let mut coupons = vec![Coupon::EMPTY; array_size];
if !empty && coupon_count > 0 {
for (i, coupon) in coupons.iter_mut().take(read_count).enumerate()
{
@@ -126,55 +113,33 @@ impl List {
})
}
- /// Serialize a List to bytes
+ /// Serializes occupied coupons in compact format.
pub fn serialize(&self, lg_config_k: u8, hll_type: HllType) -> Vec<u8> {
- let compact = true; // Always use compact format
let empty = self.container.is_empty();
let coupon_count = self.container.len();
let lg_arr = self.container.lg_size();
-
- // Compute size
- let array_size = if compact { coupon_count } else { 1 << lg_arr };
- let total_size = LIST_PREAMBLE_SIZE + (array_size * 4);
+ let total_size = LIST_PREAMBLE_SIZE + (coupon_count *
size_of::<u32>());
let mut bytes = SketchBytes::with_capacity(total_size);
- // Write preamble
bytes.write_u8(LIST_PREINTS);
bytes.write_u8(SERIAL_VERSION);
bytes.write_u8(Family::HLL.id);
bytes.write_u8(lg_config_k);
bytes.write_u8(lg_arr as u8);
- // Write flags
- let mut flags = 0u8;
+ let mut flags = COMPACT_FLAG_MASK;
if empty {
flags |= EMPTY_FLAG_MASK;
}
- if compact {
- flags |= COMPACT_FLAG_MASK;
- }
bytes.write_u8(flags);
- // Write count
bytes.write_u8(coupon_count as u8);
- // Write mode byte: LIST mode with target HLL type
bytes.write_u8(encode_mode_byte(CUR_MODE_LIST, hll_type as u8));
- // Write coupons (only non-empty ones if compact)
- if !empty {
- let mut write_idx = 0;
- for coupon in self.container.coupons.iter().copied() {
- if compact && coupon.is_empty() {
- continue; // Skip empty coupons in compact mode
- }
- bytes.write_u32_le(coupon.raw());
- write_idx += 1;
- if write_idx >= array_size {
- break;
- }
- }
+ for coupon in self.container.iter().take(coupon_count) {
+ bytes.write_u32_le(coupon.raw());
}
bytes.into_bytes()
diff --git a/datasketches/src/hll/mod.rs b/datasketches/src/hll/mod.rs
index 1304ee0..fdec977 100644
--- a/datasketches/src/hll/mod.rs
+++ b/datasketches/src/hll/mod.rs
@@ -15,64 +15,14 @@
// specific language governing permissions and limitations
// under the License.
-//! HyperLogLog sketch implementation for cardinality estimation.
+//! HyperLogLog sketches for estimating the number of distinct values.
//!
-//! This module provides a probabilistic data structure for estimating the
cardinality
-//! (number of distinct elements) of large datasets with high accuracy and low
memory usage.
+//! Use [`HllSketch`] to collect values and [`HllUnion`] to combine sketches.
+//! [`HllType`] controls register storage; `lg_k` controls the register count
and accuracy.
//!
-//! # Overview
-//!
-//! HyperLogLog (HLL) sketches use hash functions to estimate cardinality in
logarithmic space.
-//! This implementation follows the Apache DataSketches specification and
supports multiple
-//! storage modes that automatically adapt based on cardinality:
-//!
-//! * **List mode**: Stores individual values for small cardinalities
-//! * **Set mode**: Uses a hash set for medium cardinalities
-//! * **HLL mode**: Uses compact arrays for large cardinalities
-//!
-//! Mode transitions are automatic and transparent to the user. Each promotion
preserves
-//! all previously observed values and maintains estimation accuracy.
-//!
-//! # Core Types
-//!
-//! The primary type for cardinality estimation is [`HllSketch`], which
maintains a single
-//! sketch and provides methods to update with new values and retrieve
cardinality estimates.
-//! For combining multiple sketches, use [`HllUnion`], which efficiently
merges sketches
-//! that may have different configurations.
-//!
-//! # HLL Types
-//!
-//! The three target HLL types are isomorphic representations of the same
registers. Given the
-//! same `lg_k` and input, they produce identical estimates and error
distributions; they trade
-//! memory layout and update performance, not statistical precision:
-//!
-//! * [`HllType::Hll4`]: 4 bits per bucket (most compact)
-//! * [`HllType::Hll6`]: 6 bits per bucket (fixed-size middle ground)
-//! * [`HllType::Hll8`]: 8 bits per bucket (largest and simplest
representation)
-//!
-//! # Union Operations
-//!
-//! The [`HllUnion`] type enables combining multiple HLL sketches into a
unified estimate.
-//! It accumulates the distinct values represented by all input sketches and
automatically handles:
-//!
-//! * Sketches with different `lg_k` configurations (resizes/downsamples as
needed)
-//! * Sketches with different target HLL types
-//!
-//! The result's accuracy is determined by its final effective `lg_k`. Merging
sketches with
-//! different configurations may reduce this value. Once reduced, it remains
lower until the union
-//! is reset. A lower effective `lg_k` widens the error distribution;
converting among HLL target
-//! types does not change accuracy.
-//!
-//! # Serialization
-//!
-//! Sketches can be serialized and deserialized while preserving all state,
including:
-//! * Current mode and HLL type
-//! * All observed values (coupons or register values)
-//! * HIP accumulator state for accurate estimation
-//! * Out-of-order flag for merged/deserialized sketches
-//!
-//! The serialization format is compatible with Apache DataSketches
implementations
-//! in Java and C++, enabling cross-platform sketch exchange.
+//! Serialized sketches are compatible with Apache DataSketches in Java and
C++.
+//! Cross-language updates must also use compatible hashing; see
+//! [`hash::value`](crate::hash::value).
//!
//! # Usage
//!
@@ -131,9 +81,10 @@ mod union;
pub use self::sketch::HllSketch;
pub use self::union::HllUnion;
-/// Target HLL type.
+/// Storage representation for HLL registers.
///
-/// See [module level documentation](self) for more details.
+/// For the same `lg_k` and input, all three types produce the same estimates.
+/// They trade memory usage against update speed.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HllType {
/// Uses 4 bits per HLL bucket and has the smallest storage footprint.
@@ -157,19 +108,10 @@ const COUPON_RSE: f64 = COUPON_RSE_FACTOR / (1 << 13) as
f64;
const RESIZE_NUMERATOR: u32 = 3; // Resize at 3/4 = 75% load factor
const RESIZE_DENOMINATOR: u32 = 4;
-/// A coupon encodes a (slot, value) pair derived from hashing an input.
+/// A reusable hash of an input value for HLL sketches.
///
-/// Format: `[value (6 bits) << 26] | [slot (26 bits)]`
-///
-/// The slot identifies an HLL register (derived from the lower bits of the
hash),
-/// and the value represents the number of leading zeros plus one (from the
upper bits).
-///
-/// Pre-computing coupons is useful when the same logical value must be
inserted into
-/// multiple independent sketches, because the (relatively expensive) hash
step is paid
-/// only once. A common pattern is dictionary-encoded data: compute the
coupon for each
-/// term id up front, cache it, and then call
[`HllSketch::update_with_coupon`] for
-/// each per-bucket sketch rather than calling [`HllSketch::update`]
repeatedly with the
-/// decoded string.
+/// Compute a coupon once with [`Coupon::from_value`] and pass it to
+/// [`HllSketch::update_with_coupon`] on sketches with any `lg_k` or
[`HllType`].
///
/// # Examples
///
@@ -189,19 +131,17 @@ const RESIZE_DENOMINATOR: u32 = 4;
/// assert!(sketch2.estimate() >= 1.0);
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
-pub struct Coupon(u32);
+pub struct Coupon(u32); // Upper six bits: register value; lower 26 bits: slot.
impl Coupon {
/// Sentinel value indicating an empty coupon slot.
const EMPTY: Self = Coupon(0);
- /// Returns `true` if this coupon is the empty sentinel (value `0`).
#[inline(always)]
fn is_empty(self) -> bool {
self == Self::EMPTY
}
- /// Returns the raw 32-bit representation of the coupon.
#[inline(always)]
fn raw(self) -> u32 {
self.0
@@ -209,13 +149,21 @@ impl Coupon {
/// Computes the HLL coupon for a hashable value.
///
- /// You may use [`hash::value`](crate::hash::value) wrappers when another
DataSketches
+ /// Use [`hash::value`](crate::hash::value) wrappers when another
DataSketches
/// implementation requires a specific value hashing strategy.
///
- /// Hashes `value` using MurmurHash3 128-bit and packs the result into a
coupon:
- /// the low 26 bits of the low hash word become the slot index, and the
- /// leading-zero count of the high hash word (capped at 62, then plus one)
- /// becomes the 6-bit register value.
+ /// # Examples
+ ///
+ /// ```
+ /// use datasketches::hll::Coupon;
+ /// use datasketches::hll::HllSketch;
+ /// use datasketches::hll::HllType;
+ ///
+ /// let coupon = Coupon::from_value("apple");
+ /// let mut sketch = HllSketch::new(10, HllType::Hll8).unwrap();
+ /// sketch.update_with_coupon(coupon);
+ /// assert!(sketch.estimate() >= 1.0);
+ /// ```
#[inline(always)]
pub fn from_value<T: Hash>(value: T) -> Self {
let mut hasher = MurmurHash3X64128::default();
@@ -224,25 +172,23 @@ impl Coupon {
let addr26 = lo as u32 & KEY_MASK_26;
let lz = hi.leading_zeros();
+ // Register values must fit in six bits; zero is reserved for empty
slots.
let capped = lz.min(62);
let value = capped + 1;
Coupon((value << KEY_BITS_26) | addr26)
}
- /// Pack a slot index and register value into a coupon.
#[inline(always)]
fn pack(slot: u32, value: u8) -> Self {
Coupon(((value as u32) << KEY_BITS_26) | (slot & KEY_MASK_26))
}
- /// Extract the slot index (low 26 bits).
#[inline(always)]
fn slot(self) -> u32 {
self.0 & KEY_MASK_26
}
- /// Extract the register value (upper 6 bits).
#[inline(always)]
fn value(self) -> u8 {
(self.0 >> KEY_BITS_26) as u8
diff --git a/datasketches/src/hll/serialization.rs
b/datasketches/src/hll/serialization.rs
index cacb6cb..166753e 100644
--- a/datasketches/src/hll/serialization.rs
+++ b/datasketches/src/hll/serialization.rs
@@ -15,15 +15,11 @@
// specific language governing permissions and limitations
// under the License.
-//! Binary serialization format constants for HLL sketches
-//!
-//! This module contains all constants related to the Apache DataSketches
-//! binary serialization format, shared across all sketch modes.
+//! HLL wire-format constants. Preamble lengths ending in `PREINTS` count
32-bit words;
+//! those ending in `PREAMBLE_SIZE` count bytes.
-/// Current serialization version
pub const SERIAL_VERSION: u8 = 1;
-/// Flag indicating sketch is empty (no values inserted)
pub const EMPTY_FLAG_MASK: u8 = 4;
/// Flag indicating compact coupon or HLL4 auxiliary storage.
///
@@ -32,64 +28,40 @@ pub const COMPACT_FLAG_MASK: u8 = 8;
/// 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)
pub const LIST_PREINTS: u8 = 2;
-/// Preamble size for SET mode (12 bytes = 3 ints)
pub const HASH_SET_PREINTS: u8 = 3;
-/// Preamble size for HLL mode (40 bytes = 10 ints)
pub const HLL_PREINTS: u8 = 10;
-/// Total size of LIST preamble in bytes
pub const LIST_PREAMBLE_SIZE: usize = 8;
-/// Total size of SET preamble in bytes
pub const SET_PREAMBLE_SIZE: usize = 12;
-/// Total size of HLL preamble in bytes
pub const HLL_PREAMBLE_SIZE: usize = 40;
-/// Extract current mode from mode byte (low 2 bits)
-///
-/// Returns: 0 = LIST, 1 = SET, 2 = HLL
#[inline]
pub fn extract_cur_mode(mode_byte: u8) -> u8 {
mode_byte & 0x3
}
-/// Extract target HLL type from mode byte (bits 2-3)
-///
-/// Returns: 0 = HLL4, 1 = HLL6, 2 = HLL8
#[inline]
pub fn extract_tgt_hll_type(mode_byte: u8) -> u8 {
(mode_byte >> 2) & 0x3
}
-/// Encode mode byte from current mode and target type
-///
-/// # Arguments
-///
-/// * `cur_mode`: 0 = LIST, 1 = SET, 2 = HLL
-/// * `tgt_type`: 0 = HLL4, 1 = HLL6, 2 = HLL8
+/// The mode byte stores `CUR_MODE_*` in bits 0–1 and `TGT_HLL*` in bits 2–3.
#[inline]
pub fn encode_mode_byte(cur_mode: u8, tgt_type: u8) -> u8 {
(cur_mode & 0x3) | ((tgt_type & 0x3) << 2)
}
-/// Current mode: LIST
pub const CUR_MODE_LIST: u8 = 0;
-/// Current mode: SET
pub const CUR_MODE_SET: u8 = 1;
-/// Current mode: HLL
pub const CUR_MODE_HLL: u8 = 2;
-/// Target type: HLL4 (4-bit packed)
pub const TGT_HLL4: u8 = 0;
-/// Target type: HLL6 (6-bit packed)
pub const TGT_HLL6: u8 = 1;
-/// Target type: HLL8 (8-bit unpacked)
pub const TGT_HLL8: u8 = 2;
-/// Size of a single coupon in bytes (u32)
pub const COUPON_SIZE_BYTES: usize = 4;
diff --git a/datasketches/src/hll/sketch.rs b/datasketches/src/hll/sketch.rs
index bef4fbb..437062f 100644
--- a/datasketches/src/hll/sketch.rs
+++ b/datasketches/src/hll/sketch.rs
@@ -15,11 +15,6 @@
// specific language governing permissions and limitations
// under the License.
-//! HyperLogLog sketch implementation
-//!
-//! This module provides the main [`HllSketch`] struct, which is the primary
interface
-//! for creating and using HLL sketches for cardinality estimation.
-
use std::fmt;
use std::hash::Hash;
@@ -68,15 +63,9 @@ pub struct HllSketch {
}
impl HllSketch {
- /// Creates a new HLL sketch.
- ///
- /// # Arguments
+ /// Creates an empty sketch with `2^lg_config_k` registers and the
requested [`HllType`].
///
- /// * `lg_config_k`: The `lg_k` value in `[4, 21]`, which controls the
number of buckets.
- /// * `lg_k = 4`: 16 buckets, ~26% relative error.
- /// * `lg_k = 12`: 4096 buckets, ~1.6% relative error (common choice).
- /// * `lg_k = 21`: 2M buckets, ~0.4% relative error.
- /// * `hll_type`: Target HLL array type (`Hll4`, `Hll6`, or `Hll8`).
+ /// Increasing `lg_config_k` uses more memory and reduces estimation error.
///
/// # Errors
///
@@ -106,29 +95,15 @@ impl HllSketch {
})
}
- /// Create an HLL sketch directly from a Mode
- ///
- /// This is used internally (e.g., by union operations) to construct
- /// sketches in specific modes without going through List mode first.
- ///
- /// # Arguments
- ///
- /// * `lg_config_k`: Log2 of the number of buckets (K)
- /// * `mode`: The mode to initialize the sketch with
pub(super) fn from_mode(lg_config_k: u8, mode: Mode) -> Self {
Self { lg_config_k, mode }
}
- /// Get the current mode of the sketch
pub(super) fn mode(&self) -> &Mode {
&self.mode
}
- /// Get mutable access to the current mode
- ///
- /// # Safety
- ///
- /// Caller must maintain internal invariants (num_zeros, estimator state).
+ /// Callers must keep register-derived caches and estimate state
consistent with the mode.
pub(super) fn mode_mut(&mut self) -> &mut Mode {
&mut self.mode
}
@@ -162,16 +137,11 @@ impl HllSketch {
/// Updates the sketch with a value.
///
- /// Accepts any type that implements [`Hash`]. The value is hashed and
converted to
- /// an internal coupon, which is then inserted into the sketch.
- ///
/// You may use [`hash::value`](crate::hash::value) wrappers when another
DataSketches
/// implementation requires a specific value hashing strategy.
///
- /// If you need to insert the same logical value into multiple sketches,
consider
- /// pre-computing the coupon with [`Coupon::from_value`] and calling
- /// [`update_with_coupon`](Self::update_with_coupon) on each sketch to
avoid
- /// redundant hashing.
+ /// To reuse the hash across sketches, see [`Coupon`] and
+ /// [`update_with_coupon`](Self::update_with_coupon).
///
/// # Examples
///
@@ -192,15 +162,7 @@ impl HllSketch {
self.update_with_coupon(Coupon::from_value(value));
}
- /// Updates the sketch with a pre-computed [`Coupon`].
- ///
- /// A [`Coupon`] encodes both the HLL bucket index (low 26 bits) and the
register
- /// value (high 6 bits) derived from hashing an input. Accepting a
pre-computed
- /// coupon makes it possible to pay the hashing cost once and fan the
result out to
- /// many independent sketches — see [`Coupon`] for a worked example.
- ///
- /// All internal bookkeeping, including representation transitions and
estimator state updates,
- /// is handled automatically.
+ /// Updates the sketch with a precomputed [`Coupon`], without hashing the
input again.
///
/// # Examples
///
@@ -209,9 +171,9 @@ impl HllSketch {
/// use datasketches::hll::HllSketch;
/// use datasketches::hll::HllType;
///
- /// let c = Coupon::from_value("apple");
+ /// let coupon = Coupon::from_value("apple");
/// let mut sketch = HllSketch::new(10, HllType::Hll8).unwrap();
- /// sketch.update_with_coupon(c);
+ /// sketch.update_with_coupon(coupon);
/// assert!(sketch.estimate() >= 1.0);
/// ```
pub fn update_with_coupon(&mut self, coupon: Coupon) {
@@ -254,8 +216,11 @@ impl HllSketch {
/// use datasketches::hll::HllType;
///
/// let mut sketch = HllSketch::new(10, HllType::Hll8).unwrap();
- /// sketch.update("apple");
- /// assert!(sketch.estimate() >= 1.0);
+ /// for value in ["apple", "banana", "apple"] {
+ /// sketch.update(value);
+ /// }
+ /// let estimate = sketch.estimate();
+ /// assert!((estimate - 2.0).abs() < 0.01);
/// ```
pub fn estimate(&self) -> f64 {
match &self.mode {
@@ -267,9 +232,22 @@ impl HllSketch {
}
}
- /// Returns the upper confidence bound for the cardinality estimate.
+ /// Returns the upper confidence bound for `num_std_dev` standard
deviations.
///
- /// The bound is based on the requested number of standard deviations.
+ /// # Examples
+ ///
+ /// ```
+ /// use datasketches::common::NumStdDev;
+ /// use datasketches::hll::HllSketch;
+ /// use datasketches::hll::HllType;
+ ///
+ /// let mut sketch = HllSketch::new(10, HllType::Hll8).unwrap();
+ /// for value in 0..10_000 {
+ /// sketch.update(value);
+ /// }
+ /// let upper = sketch.upper_bound(NumStdDev::Two);
+ /// assert!(upper >= sketch.estimate());
+ /// ```
pub fn upper_bound(&self, num_std_dev: NumStdDev) -> f64 {
match &self.mode {
Mode::List { list, .. } =>
list.container().upper_bound(num_std_dev),
@@ -280,9 +258,22 @@ impl HllSketch {
}
}
- /// Returns the lower confidence bound for the cardinality estimate.
+ /// Returns the lower confidence bound for `num_std_dev` standard
deviations.
+ ///
+ /// # Examples
///
- /// The bound is based on the requested number of standard deviations.
+ /// ```
+ /// use datasketches::common::NumStdDev;
+ /// use datasketches::hll::HllSketch;
+ /// use datasketches::hll::HllType;
+ ///
+ /// let mut sketch = HllSketch::new(10, HllType::Hll8).unwrap();
+ /// for value in 0..10_000 {
+ /// sketch.update(value);
+ /// }
+ /// let lower = sketch.lower_bound(NumStdDev::Two);
+ /// assert!(lower <= sketch.estimate());
+ /// ```
pub fn lower_bound(&self, num_std_dev: NumStdDev) -> f64 {
match &self.mode {
Mode::List { list, .. } =>
list.container().lower_bound(num_std_dev),
@@ -311,12 +302,11 @@ impl HllSketch {
///
/// let bytes = sketch.serialize();
/// let decoded = HllSketch::deserialize(&bytes).unwrap();
- /// assert!(decoded.estimate() >= 1.0);
+ /// assert_eq!(decoded.estimate(), sketch.estimate());
/// ```
pub fn deserialize(bytes: &[u8]) -> Result<HllSketch, Error> {
let mut cursor = SketchSlice::new(bytes);
- // Read and validate preamble
let preamble_ints = cursor
.read_u8()
.map_err(insufficient_data("preamble_ints"))?;
@@ -335,13 +325,10 @@ impl HllSketch {
let state = cursor.read_u8().map_err(insufficient_data("state"))?;
let mode_byte = cursor.read_u8().map_err(insufficient_data("mode"))?;
- // Verify family ID
Family::HLL.validate_id(family_id)?;
- // Verify serialization version
ensure_serial_version_is(SERIAL_VERSION, serial_version)?;
- // Verify lg_k range (4-21 are valid)
if !(4..=21).contains(&lg_config_k) {
return Err(Error::deserial(format!(
"lg_k must be in [4; 21], got {lg_config_k}",
@@ -361,7 +348,7 @@ impl HllSketch {
let compact = (flags & COMPACT_FLAG_MASK) != 0;
let ooo = (flags & OUT_OF_ORDER_FLAG_MASK) != 0;
- // Deserialize based on mode
+ // Each mode reader starts after the shared eight-byte header.
let mode =
match extract_cur_mode(mode_byte) {
CUR_MODE_LIST => {
@@ -451,7 +438,7 @@ impl HllSketch {
}
}
- /// Returns the estimated size of the sketch in bytes.
+ /// Returns the estimated memory usage in bytes, including owned heap
allocations.
pub fn estimated_size(&self) -> usize {
let heap_size = match &self.mode {
Mode::List { list, .. } => list.container().estimated_size(),
diff --git a/datasketches/src/hll/union.rs b/datasketches/src/hll/union.rs
index eb98382..f16cebc 100644
--- a/datasketches/src/hll/union.rs
+++ b/datasketches/src/hll/union.rs
@@ -15,19 +15,6 @@
// specific language governing permissions and limitations
// under the License.
-//! HyperLogLog Union for combining multiple HLL sketches
-//!
-//! The HLL Union allows combining multiple HLL sketches into a single unified
-//! sketch, enabling set union operations for cardinality estimation.
-//!
-//! # Overview
-//!
-//! The union maintains an internal "gadget" sketch that accumulates the union
-//! of all input sketches. It can handle sketches with:
-//! * Different lg_k values (automatically resizes as needed)
-//! * Different modes (List, Set, Array4/6/8)
-//! * Different target HLL types
-
use std::hash::Hash;
use crate::common::NumStdDev;
@@ -43,9 +30,6 @@ use crate::hll::mode::Mode;
/// An HLL union for combining multiple HLL sketches.
///
-/// The union accumulates the distinct values represented by all input
sketches and automatically
-/// handles sketches with different configurations.
-///
/// Merging sketches with different configurations may reduce the union's
effective `lg_k`. Once
/// reduced, it remains lower until [`reset`](Self::reset), so estimates and
bounds reflect the
/// reduced register count. The requested [`HllType`] changes only the result
representation, not
@@ -54,19 +38,15 @@ use crate::hll::mode::Mode;
/// See the [module level documentation](super) for more.
#[derive(Debug, Clone)]
pub struct HllUnion {
- /// Maximum lg_k that this union can handle
lg_max_k: u8,
- /// Internal sketch that accumulates the union
+ /// Array modes always use `Array8`; packed results are produced by
`to_sketch`.
gadget: HllSketch,
}
impl HllUnion {
- /// Creates a new HLL union.
- ///
- /// # Arguments
+ /// Creates an empty union with a maximum of `2^lg_max_k` registers.
///
- /// * `lg_max_k`: Maximum `lg_k` in `[4, 21]`. This determines the maximum
precision the union
- /// can handle. Input sketches with a larger `lg_k` are downsampled.
+ /// Input sketches with a larger `lg_k` are downsampled.
///
/// # Errors
///
@@ -81,39 +61,32 @@ impl HllUnion {
/// let mut union = HllUnion::new(10).unwrap();
/// union.update_value("apple");
/// let result = union.to_sketch(HllType::Hll8);
- /// assert_eq!(result.estimate(), 1.0);
+ /// assert!(result.estimate() >= 1.0);
/// ```
pub fn new(lg_max_k: u8) -> Result<Self, Error> {
- // Start with an empty gadget at lg_max_k using Hll8
let gadget = HllSketch::new(lg_max_k, HllType::Hll8)?;
Ok(Self { lg_max_k, gadget })
}
- /// Updates the union with a hashable value.
- ///
- /// This accepts any type that implements `Hash`. The value is hashed
- /// and converted to a coupon, which is then inserted into the sketch.
+ /// Updates the union with a value, using the same hashing as
[`HllSketch::update`].
///
/// # Examples
///
/// ```
- /// use datasketches::hll::HllType;
/// use datasketches::hll::HllUnion;
///
/// let mut union = HllUnion::new(10).unwrap();
/// union.update_value("apple");
- /// let result = union.to_sketch(HllType::Hll8);
- /// assert_eq!(result.estimate(), 1.0);
+ /// assert!(union.estimate() >= 1.0);
/// ```
pub fn update_value<T: Hash>(&mut self, value: T) {
self.gadget.update(value);
}
- /// Updates the union with another sketch.
+ /// Merges a sketch into the union, accepting any [`HllType`] and `lg_k`.
///
- /// The input is merged while accommodating different `lg_k`
configurations and target HLL
- /// types. The union's effective `lg_k` may decrease as a result.
+ /// The union's effective `lg_k` may decrease. Empty sketches have no
effect.
///
/// # Examples
///
@@ -152,7 +125,6 @@ impl HllUnion {
}
}
- /// Update union from a List or Set mode sketch
fn update_from_list_or_set(
&mut self,
sketch: &HllSketch,
@@ -160,23 +132,18 @@ impl HllUnion {
src_lg_k: u8,
dst_lg_k: u8,
) {
- // Fast path: If gadget is empty and lg_k matches, directly copy as
HLL_8
if self.gadget.is_empty() && src_lg_k == dst_lg_k {
self.gadget = if sketch.target_type() == HllType::Hll8 {
sketch.clone()
} else {
- // Convert to Hll8 by changing target type
convert_coupon_mode_to_hll8(src_mode, src_lg_k)
};
} else {
- // Regular path: merge coupons into gadget
merge_coupons_into_gadget(&mut self.gadget, src_mode);
}
}
- /// Update union from an Array mode sketch
fn update_from_array(&mut self, src_mode: &Mode, src_lg_k: u8, dst_lg_k:
u8) {
- // Fast path: If gadget is empty, just copy/downsample source
if self.gadget.is_empty() {
let new_array = copy_or_downsample(src_mode, src_lg_k,
self.lg_max_k);
let final_lg_k = new_array.num_registers().trailing_zeros() as u8;
@@ -193,10 +160,8 @@ impl HllUnion {
}
}
- /// Merge an array source into an array gadget
fn merge_array_into_array_gadget(&mut self, src_mode: &Mode, src_lg_k: u8,
dst_lg_k: u8) {
if src_lg_k < dst_lg_k {
- // Source has lower precision - must downsize gadget
let mut new_array = Array8::new(src_lg_k);
match self.gadget.mode() {
@@ -216,7 +181,6 @@ impl HllUnion {
merge_array_same_lgk(&mut new_array, src_mode);
self.gadget = HllSketch::from_mode(src_lg_k,
Mode::Array8(new_array));
} else {
- // Standard merge: src_lg_k >= dst_lg_k
match self.gadget.mode_mut() {
Mode::Array8(dst_array) => {
merge_array_into_array8(dst_array, dst_lg_k, src_mode,
src_lg_k);
@@ -228,7 +192,6 @@ impl HllUnion {
}
}
- /// Promote gadget from List/Set to Array and merge array source
fn promote_gadget_and_merge_array(&mut self, src_mode: &Mode, src_lg_k:
u8) {
let mut new_array = copy_or_downsample(src_mode, src_lg_k,
self.lg_max_k);
@@ -239,14 +202,9 @@ impl HllUnion {
self.gadget = HllSketch::from_mode(final_lg_k,
Mode::Array8(new_array));
}
- /// Returns the union result as a new sketch.
- ///
- /// The returned sketch uses the specified target HLL type. If the
requested type differs from
- /// the current representation, the result is converted.
- ///
- /// # Arguments
+ /// Returns an independent sketch with the requested [`HllType`].
///
- /// * `hll_type`: Target HLL type for the result sketch (`Hll4`, `Hll6`,
or `Hll8`).
+ /// The result retains the union's effective `lg_k` and cardinality
estimate.
///
/// # Examples
///
@@ -257,7 +215,8 @@ impl HllUnion {
/// let mut union = HllUnion::new(10).unwrap();
/// union.update_value("apple");
/// let result = union.to_sketch(HllType::Hll6);
- /// assert!(result.estimate() >= 1.0);
+ /// assert_eq!(result.target_type(), HllType::Hll6);
+ /// assert_eq!(result.estimate(), union.estimate());
/// ```
pub fn to_sketch(&self, hll_type: HllType) -> HllSketch {
let gadget_type = self.gadget.target_type();
@@ -305,40 +264,94 @@ impl HllUnion {
self.gadget.is_empty()
}
- /// Resets the union to its initial empty state.
+ /// Clears the union and restores its configured maximum `lg_k`.
+ ///
+ /// # Examples
///
- /// This clears all accumulated data so the union can be reused.
+ /// ```
+ /// use datasketches::hll::HllSketch;
+ /// use datasketches::hll::HllType;
+ /// use datasketches::hll::HllUnion;
+ ///
+ /// let mut sketch = HllSketch::new(10, HllType::Hll8).unwrap();
+ /// for value in 0..10_000 {
+ /// sketch.update(value);
+ /// }
+ /// let mut union = HllUnion::new(12).unwrap();
+ /// union.update(&sketch);
+ /// assert_eq!(union.lg_config_k(), 10);
+ ///
+ /// union.reset();
+ /// assert!(union.is_empty());
+ /// assert_eq!(union.lg_config_k(), 12);
+ /// ```
pub fn reset(&mut self) {
self.gadget = HllSketch::new(self.lg_max_k, HllType::Hll8).unwrap();
}
/// Returns the union's current cardinality estimate.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use datasketches::hll::HllUnion;
+ ///
+ /// let mut union = HllUnion::new(10).unwrap();
+ /// for value in ["apple", "banana", "apple"] {
+ /// union.update_value(value);
+ /// }
+ /// let estimate = union.estimate();
+ /// assert!((estimate - 2.0).abs() < 0.01);
+ /// ```
pub fn estimate(&self) -> f64 {
self.gadget.estimate()
}
- /// Returns the upper confidence bound for the union's cardinality
estimate.
+ /// Returns the upper confidence bound for `num_std_dev` standard
deviations.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use datasketches::common::NumStdDev;
+ /// use datasketches::hll::HllUnion;
///
- /// The bound is based on the requested number of standard deviations.
+ /// let mut union = HllUnion::new(10).unwrap();
+ /// for value in 0..10_000 {
+ /// union.update_value(value);
+ /// }
+ /// let upper = union.upper_bound(NumStdDev::Two);
+ /// assert!(upper >= union.estimate());
+ /// ```
pub fn upper_bound(&self, num_std_dev: NumStdDev) -> f64 {
self.gadget.upper_bound(num_std_dev)
}
- /// Returns the lower confidence bound for the union's cardinality
estimate.
+ /// Returns the lower confidence bound for `num_std_dev` standard
deviations.
+ ///
+ /// # Examples
///
- /// The bound is based on the requested number of standard deviations.
+ /// ```
+ /// use datasketches::common::NumStdDev;
+ /// use datasketches::hll::HllUnion;
+ ///
+ /// let mut union = HllUnion::new(10).unwrap();
+ /// for value in 0..10_000 {
+ /// union.update_value(value);
+ /// }
+ /// let lower = union.lower_bound(NumStdDev::Two);
+ /// assert!(lower <= union.estimate());
+ /// ```
pub fn lower_bound(&self, num_std_dev: NumStdDev) -> f64 {
self.gadget.lower_bound(num_std_dev)
}
- /// Returns the estimated size of the union in bytes.
+ /// Returns the estimated memory usage in bytes, including owned heap
allocations.
pub fn estimated_size(&self) -> usize {
// The gadget's inline size is already covered by size_of::<Self>().
size_of::<Self>() - size_of::<HllSketch>() +
self.gadget.estimated_size()
}
}
-/// Convert a coupon mode (List or Set) to Hll8 target type
fn convert_coupon_mode_to_hll8(src_mode: &Mode, src_lg_k: u8) -> HllSketch {
match src_mode {
Mode::List { list, .. } => HllSketch::from_mode(
@@ -359,10 +372,6 @@ fn convert_coupon_mode_to_hll8(src_mode: &Mode, src_lg_k:
u8) -> HllSketch {
}
}
-/// Merge coupons from a List or Set mode into the gadget
-///
-/// Iterates over all coupons in the source and updates the gadget.
-/// The gadget handles mode transitions automatically (List → Set → Array).
fn merge_coupons_into_gadget(gadget: &mut HllSketch, src_mode: &Mode) {
match src_mode {
Mode::List { list, .. } => {
@@ -383,7 +392,6 @@ fn merge_coupons_into_gadget(gadget: &mut HllSketch,
src_mode: &Mode) {
}
}
-/// Merge coupons from a List or Set mode into an Array8
fn merge_coupons_into_mode(dst: &mut Array8, src_mode: &Mode) {
match src_mode {
Mode::List { list, .. } => {
@@ -404,12 +412,7 @@ fn merge_coupons_into_mode(dst: &mut Array8, src_mode:
&Mode) {
}
}
-/// Merge an HLL array into an Array8
-///
-/// Handles merging from Array4, Array6, or Array8 sources. Dispatches based
on lg_k:
-/// * Same lg_k: optimized bulk merge
-/// * src lg_k > dst lg_k: downsample src into dst
-/// * src lg_k < dst lg_k: handled by caller (requires gadget replacement)
+/// The caller must downsize the destination first if the source has a smaller
`lg_k`.
fn merge_array_into_array8(dst_array8: &mut Array8, dst_lg_k: u8, src_mode:
&Mode, src_lg_k: u8) {
assert!(
src_lg_k >= dst_lg_k,
@@ -425,7 +428,6 @@ fn merge_array_into_array8(dst_array8: &mut Array8,
dst_lg_k: u8, src_mode: &Mod
}
}
-/// Extract estimate state from an array mode.
fn get_array_estimate_state(mode: &Mode) -> EstimateState {
match mode {
Mode::Array8(src) => src.estimate_state(),
@@ -439,7 +441,6 @@ fn get_array_estimate_state(mode: &Mode) -> EstimateState {
}
}
-/// Merge Array4/Array6 into Array8 by iterating registers
fn merge_array46_same_lgk(dst: &mut Array8, num_registers: usize, get_value:
impl Fn(u32) -> u8) {
for slot in 0..num_registers {
let val = get_value(slot as u32);
@@ -451,9 +452,7 @@ fn merge_array46_same_lgk(dst: &mut Array8, num_registers:
usize, get_value: imp
dst.rebuild_estimator_from_registers();
}
-/// Merge arrays with same lg_k
-///
-/// Takes the max of corresponding registers. HIP accumulator is invalidated
by the merge.
+/// Merging independent register histories invalidates the HIP accumulator.
fn merge_array_same_lgk(dst: &mut Array8, src_mode: &Mode) {
match src_mode {
Mode::Array8(src) => {
@@ -471,7 +470,6 @@ fn merge_array_same_lgk(dst: &mut Array8, src_mode: &Mode) {
}
}
-/// Merge Array4/Array6 into Array8 with downsampling
fn merge_array46_with_downsample(
dst: &mut Array8,
dst_lg_k: u8,
@@ -492,10 +490,8 @@ fn merge_array46_with_downsample(
dst.rebuild_estimator_from_registers();
}
-/// Merge arrays with downsampling (src lg_k > dst lg_k)
-///
/// Multiple source registers map to each destination register via masking.
-/// HIP accumulator is invalidated by the merge.
+/// Merging independent register histories invalidates the HIP accumulator.
fn merge_array_with_downsample(dst: &mut Array8, dst_lg_k: u8, src_mode:
&Mode, src_lg_k: u8) {
assert!(
src_lg_k > dst_lg_k,
@@ -520,10 +516,8 @@ 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 estimate state.
+/// Changing representation preserves the estimate state; replaying registers
alone would change
+/// HIP.
fn convert_array8_to_type(src: &Array8, lg_config_k: u8, target_type: HllType)
-> HllSketch {
let estimate_state = src.estimate_state();
match target_type {
@@ -558,7 +552,6 @@ fn convert_array8_to_type(src: &Array8, lg_config_k: u8,
target_type: HllType) -
}
}
-/// Copy Array4/Array6 registers into Array8 by converting to coupons
fn copy_array46_via_coupons(dst: &mut Array8, num_registers: usize, get_value:
impl Fn(u32) -> u8) {
for slot in 0..num_registers {
let val = get_value(slot as u32);
@@ -569,10 +562,7 @@ 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.
-/// The source estimate state carries over because the result represents the
same logical sketch.
+/// Preserves the source estimate state because the result represents the same
logical sketch.
fn copy_or_downsample(src_mode: &Mode, src_lg_k: u8, tgt_lg_k: u8) -> Array8 {
let estimate_state = get_array_estimate_state(src_mode);
@@ -598,7 +588,6 @@ fn copy_or_downsample(src_mode: &Mode, src_lg_k: u8,
tgt_lg_k: u8) -> Array8 {
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
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]