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 1d31bd9 fix(theta,tuple): canonicalize empty sampled sketch state
(#254)
1d31bd9 is described below
commit 1d31bd9c102f9aac24eef2bf31b0f5c0a6cb05a2
Author: Jaideep Pyne <[email protected]>
AuthorDate: Mon Aug 31 23:29:00 2026 +0530
fix(theta,tuple): canonicalize empty sampled sketch state (#254)
Co-authored-by: tison <[email protected]>
---
CHANGELOG.md | 1 +
datasketches/src/thetafamily/common/a_not_b.rs | 95 +++---
datasketches/src/thetafamily/common/hash_table.rs | 133 ++++-----
.../src/thetafamily/common/intersection.rs | 156 +++++-----
.../src/thetafamily/common/jaccard_similarity.rs | 125 ++++----
datasketches/src/thetafamily/common/mod.rs | 13 +-
.../src/thetafamily/common/sketch_state.rs | 133 +++++++++
datasketches/src/thetafamily/common/union.rs | 70 ++---
datasketches/src/thetafamily/theta/a_not_b.rs | 15 +-
datasketches/src/thetafamily/theta/intersection.rs | 22 +-
datasketches/src/thetafamily/theta/sketch.rs | 331 +++++++++++----------
datasketches/src/thetafamily/theta/union.rs | 17 +-
datasketches/src/thetafamily/tuple/a_not_b.rs | 10 +-
datasketches/src/thetafamily/tuple/intersection.rs | 14 +-
datasketches/src/thetafamily/tuple/sketch.rs | 183 +++++++-----
datasketches/src/thetafamily/tuple/union.rs | 9 +-
tests-integration/src/lib.rs | 3 +
tests-integration/tests/theta_test/a_not_b.rs | 8 +-
tests-integration/tests/theta_test/intersection.rs | 3 +-
tests-integration/tests/theta_test/sketch.rs | 31 +-
tests-integration/tests/theta_test/union.rs | 27 +-
tests-integration/tests/tuple_test/a_not_b.rs | 16 +-
tests-integration/tests/tuple_test/sketch.rs | 31 +-
tests-integration/tests/tuple_test/union.rs | 21 +-
24 files changed, 806 insertions(+), 661 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 153a92f..89e3b79 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -49,6 +49,7 @@ All significant changes to this project will be documented in
this file.
* Malformed CPC images now return `InvalidData` instead of panicking.
* Seeded deserializers now return `InvalidData` rather than panicking when the
caller supplies a seed whose hash is the reserved zero value.
* Fix T-Digest interpolation and tail calculations that could produce
non-monotonic or out-of-range quantiles and invalid rank, CDF, or PMF values.
+* Empty Theta-family and Tuple-family sketches now consistently report `theta`
of `1.0`, `theta64` of `MAX_THETA`, and `is_estimation_mode` of `false`,
including update sketches and unions configured with a sampling probability
below `1.0`. Empty compact results now use the canonical ordered
representation, so set-operation results survive serialization round trips
without changing their reported state.
## v0.4.0 (2026-08-18)
diff --git a/datasketches/src/thetafamily/common/a_not_b.rs
b/datasketches/src/thetafamily/common/a_not_b.rs
index 643d7df..d1caeea 100644
--- a/datasketches/src/thetafamily/common/a_not_b.rs
+++ b/datasketches/src/thetafamily/common/a_not_b.rs
@@ -23,9 +23,9 @@ use crate::hash::check_seed_hash;
use crate::thetacommon::EntrySketch;
use crate::thetacommon::KeySketch;
use crate::thetacommon::SketchEntry;
-use crate::thetacommon::SketchScalars;
use crate::thetacommon::constants::MAX_THETA;
-use crate::thetacommon::hash_table::CompactSketchParts;
+use crate::thetacommon::sketch_state::CompactSketchState;
+use crate::thetacommon::sketch_state::ThetaFamilySketchMetadata;
/// Computes `a and not b` for Theta-family sketch views.
///
@@ -38,50 +38,47 @@ pub fn compute<A, B>(
a: A,
b: B,
ordered: bool,
-) -> Result<CompactSketchParts<A::Entry>, Error>
+) -> Result<CompactSketchState<A::Entry>, Error>
where
A: EntrySketch,
B: KeySketch,
{
- let SketchScalars {
- seed_hash: a_seed_hash,
- theta: a_theta,
- empty: a_empty,
- ordered: a_ordered,
- ..
- } = a.scalars();
-
// If A is empty the result is an (empty) copy of A. As with the union and
intersection, an
// empty input carries no keys, so its seed is not validated.
- if a_empty {
- return Ok(parts_from_sketch(a, ordered));
- }
+ let (a_seed_hash, a_theta, a_ordered) = match a.metadata() {
+ ThetaFamilySketchMetadata::Empty { .. } => {
+ return Ok(copy_to_compact_state(a, ordered));
+ }
+ ThetaFamilySketchMetadata::NonEmpty {
+ seed_hash,
+ theta,
+ ordered,
+ ..
+ } => (seed_hash, theta, ordered),
+ };
// A is non-empty, so its seed must be compatible.
check_seed_hash(seed_hash, a_seed_hash, "A", ErrorKind::InvalidArgument)?;
- let SketchScalars {
- seed_hash: b_seed_hash,
- theta: b_theta,
- empty: b_empty,
- ordered: b_ordered,
- num_retained: b_num_retained,
- } = b.scalars();
-
// An empty B subtracts nothing, so the result is simply a copy of A. This
also covers the
// "A is non-empty but has no retained keys" state: B's seed and theta
must not influence
// the result.
- if b_empty {
- return Ok(parts_from_sketch(a, ordered));
- }
+ let (b_seed_hash, b_theta, b_ordered, b_num_retained) = match b.metadata()
{
+ ThetaFamilySketchMetadata::Empty { .. } => {
+ return Ok(copy_to_compact_state(a, ordered));
+ }
+ ThetaFamilySketchMetadata::NonEmpty {
+ seed_hash,
+ theta,
+ ordered,
+ num_retained,
+ } => (seed_hash, theta, ordered, num_retained),
+ };
// B is non-empty, so its seed must be compatible.
check_seed_hash(seed_hash, b_seed_hash, "B", ErrorKind::InvalidArgument)?;
let theta = a_theta.min(b_theta);
- // A is non-empty here; the result only becomes empty if everything is
subtracted in exact
- // mode (handled below).
- let mut is_empty = false;
let entries: Vec<A::Entry> = if b_num_retained == 0 {
a.entries().filter(|entry| entry.hash() < theta).collect()
@@ -133,45 +130,43 @@ where
};
if entries.is_empty() && theta == MAX_THETA {
- is_empty = true;
+ return Ok(CompactSketchState::empty(seed_hash));
}
- let out_ordered = ordered || a_ordered;
let mut entries = entries;
if ordered && !a_ordered && entries.len() > 1 {
entries.sort_unstable_by_key(SketchEntry::hash);
}
+ let out_ordered = ordered || a_ordered || (entries.len() == 1 && theta ==
MAX_THETA);
- Ok(CompactSketchParts {
+ Ok(CompactSketchState::non_empty(
entries,
theta,
seed_hash,
- ordered: out_ordered,
- empty: is_empty,
- })
+ out_ordered,
+ ))
}
-fn parts_from_sketch<S>(sketch: S, ordered: bool) ->
CompactSketchParts<S::Entry>
+fn copy_to_compact_state<S>(sketch: S, ordered: bool) ->
CompactSketchState<S::Entry>
where
S: EntrySketch,
{
- let SketchScalars {
- seed_hash,
- theta,
- empty,
- ordered: input_ordered,
- ..
- } = sketch.scalars();
+ let (seed_hash, theta, input_ordered) = match sketch.metadata() {
+ ThetaFamilySketchMetadata::Empty { seed_hash } => {
+ return CompactSketchState::empty(seed_hash);
+ }
+ ThetaFamilySketchMetadata::NonEmpty {
+ seed_hash,
+ theta,
+ ordered,
+ ..
+ } => (seed_hash, theta, ordered),
+ };
+
let mut entries: Vec<S::Entry> = sketch.entries().collect();
- let out_ordered = ordered || input_ordered;
if ordered && !input_ordered && entries.len() > 1 {
entries.sort_unstable_by_key(SketchEntry::hash);
}
- CompactSketchParts {
- entries,
- theta,
- seed_hash,
- ordered: out_ordered,
- empty,
- }
+ let out_ordered = ordered || input_ordered || (entries.len() == 1 && theta
== MAX_THETA);
+ CompactSketchState::non_empty(entries, theta, seed_hash, out_ordered)
}
diff --git a/datasketches/src/thetafamily/common/hash_table.rs
b/datasketches/src/thetafamily/common/hash_table.rs
index 8400f69..a115a2e 100644
--- a/datasketches/src/thetafamily/common/hash_table.rs
+++ b/datasketches/src/thetafamily/common/hash_table.rs
@@ -30,16 +30,7 @@ use crate::thetacommon::constants::MAX_LG_K;
use crate::thetacommon::constants::MAX_THETA;
use crate::thetacommon::constants::MIN_LG_K;
use crate::thetacommon::constants::STRIDE_MASK;
-
-/// Compact-sketch state from which a sketch family creates its compact result
type.
-#[derive(Debug)]
-pub struct CompactSketchParts<E> {
- pub entries: Vec<E>,
- pub theta: u64,
- pub seed_hash: u16,
- pub ordered: bool,
- pub empty: bool,
-}
+use crate::thetacommon::sketch_state::CompactSketchState;
pub struct SketchHashTableIter<'a, E>(slice::Iter<'a, Option<E>>);
@@ -58,7 +49,7 @@ impl<'a, E> Iterator for SketchHashTableIter<'a, E> {
/// Generic hash-table mechanics shared by Theta and Tuple sketches.
///
/// The entry type supplies the retained hash and any sketch-specific payload.
The table owns all
-/// theta screening, probing, resizing, rebuilding, trimming, and
logical-empty state.
+/// theta screening, probing, resizing, rebuilding, and trimming.
///
/// It maintains an array with capacity up to 2^lg_max_size:
/// * Before it reaches the max capacity, it will extend the array based on
resize_factor.
@@ -75,15 +66,10 @@ pub struct SketchHashTable<E> {
seed: u64,
seed_hash: u16,
- // Logical emptiness of the source set.
- //
- // * `false` if any update has been attempted (even if screened by theta)
- // * `true` if no updates have been attempted.
- //
- // This can be false even when `num_retained` is 0.
- is_empty: bool,
-
- theta: u64,
+ // The operational threshold used to screen future updates. This is
intentionally independent
+ // of the sketch's externally visible empty state: a never-updated sketch
built with p < 1.0
+ // must retain this sampling threshold even though its public theta is
MAX_THETA.
+ retention_theta: u64,
entries: Vec<Option<E>>,
@@ -120,32 +106,48 @@ where
let seed_hash = compute_seed_hash(seed, ErrorKind::InvalidArgument)?;
let lg_max_size = lg_nom_size + 1;
let lg_cur_size = starting_sub_multiple(lg_max_size, MIN_LG_K,
resize_factor.lg_value());
- Ok(Self::from_raw_parts(
+ Ok(Self::allocate_empty(
lg_cur_size,
lg_nom_size,
resize_factor,
sampling_probability,
- starting_theta_from_sampling_probability(sampling_probability),
+ starting_retention_theta(sampling_probability),
seed,
seed_hash,
- true,
))
}
- /// Constructs a table from raw internal state.
+ /// Creates a table used internally by a Theta-family set operation.
///
/// # Panics
///
/// Panics if `lg_cur_size > lg_nom_size + 1`. (`lg_nom_size + 1 ==
lg_max_size`)
- pub fn from_raw_parts(
+ pub fn for_set_operation(
+ lg_cur_size: u8,
+ lg_nom_size: u8,
+ retention_theta: u64,
+ seed: u64,
+ seed_hash: u16,
+ ) -> Self {
+ Self::allocate_empty(
+ lg_cur_size,
+ lg_nom_size,
+ ResizeFactor::X1,
+ 1.0,
+ retention_theta,
+ seed,
+ seed_hash,
+ )
+ }
+
+ fn allocate_empty(
lg_cur_size: u8,
lg_nom_size: u8,
resize_factor: ResizeFactor,
sampling_probability: f32,
- theta: u64,
+ retention_theta: u64,
seed: u64,
seed_hash: u16,
- is_empty: bool,
) -> Self {
let lg_max_size = lg_nom_size + 1;
assert!(
@@ -162,8 +164,7 @@ where
sampling_probability,
seed,
seed_hash,
- is_empty,
- theta,
+ retention_theta,
entries,
num_retained: 0,
}
@@ -195,9 +196,7 @@ where
where
F: FnOnce(Option<&mut E>) -> Option<E>,
{
- self.is_empty = false;
-
- if hash == 0 || hash >= self.theta {
+ if hash == 0 || hash >= self.retention_theta {
return false;
}
@@ -260,9 +259,9 @@ where
}
}
- /// Reset the table to empty state.
+ /// Restores the table's initial capacity and retention threshold and
removes all entries.
pub fn reset(&mut self) {
- let init_theta =
starting_theta_from_sampling_probability(self.sampling_probability);
+ let initial_retention_theta =
starting_retention_theta(self.sampling_probability);
let init_lg_cur = starting_sub_multiple(
self.lg_nom_size + 1,
MIN_LG_K,
@@ -273,8 +272,7 @@ where
self.entries.clear();
self.entries.resize_with(size, || None);
self.num_retained = 0;
- self.theta = init_theta;
- self.is_empty = true;
+ self.retention_theta = initial_retention_theta;
self.lg_cur_size = init_lg_cur;
}
@@ -283,14 +281,9 @@ where
self.num_retained
}
- /// Get theta.
- pub fn theta(&self) -> u64 {
- self.theta
- }
-
- /// Check logical emptiness of the source set.
- pub fn is_empty(&self) -> bool {
- self.is_empty
+ /// Returns the operational theta used to screen retained entries.
+ pub fn retention_theta(&self) -> u64 {
+ self.retention_theta
}
/// Get iterator over retained entries.
@@ -298,31 +291,22 @@ where
SketchHashTableIter(self.entries.iter())
}
- /// Returns the retained entries and theta as compact-sketch parts.
- ///
- /// An empty table reports `MAX_THETA` rather than its current theta,
matching Java's
- /// `correctThetaOnCompact()` behavior for never-updated sketches
initialized with p < 1.0.
- /// Empty and single-entry exact-mode results are always marked ordered
(Java/C++
- /// compatibility).
- pub fn to_compact_parts(&self, ordered: bool) -> CompactSketchParts<E>
+ /// Creates compact state for a sketch known by its owner to be non-empty.
+ pub fn to_non_empty_compact_state(&self, ordered: bool) ->
CompactSketchState<E>
where
E: Clone,
{
- let mut entries: Vec<E> = self.iter_entries().cloned().collect();
- let empty = self.is_empty();
- let theta = if empty { MAX_THETA } else { self.theta() };
- let is_single = entries.len() == 1 && theta == MAX_THETA;
- let ordered = ordered || empty || is_single;
- if ordered && entries.len() > 1 {
- entries.sort_unstable_by_key(SketchEntry::hash);
+ let mut retained_entries: Vec<E> =
self.iter_entries().cloned().collect();
+ let ordered = ordered || (retained_entries.len() == 1 &&
self.retention_theta == MAX_THETA);
+ if ordered && retained_entries.len() > 1 {
+ retained_entries.sort_unstable_by_key(SketchEntry::hash);
}
- CompactSketchParts {
- entries,
- theta,
- seed_hash: self.seed_hash(),
+ CompactSketchState::non_empty(
+ retained_entries,
+ self.retention_theta,
+ self.seed_hash,
ordered,
- empty,
- }
+ )
}
/// Get log2 of nominal size.
@@ -335,23 +319,18 @@ where
self.seed_hash
}
- /// Set empty flag.
- pub fn set_empty(&mut self, is_empty: bool) {
- self.is_empty = is_empty;
- }
-
/// Get the seed used by this table.
pub fn seed(&self) -> u64 {
self.seed
}
- /// Sets theta value.
- pub fn set_theta(&mut self, theta: u64) {
+ /// Sets the operational theta used to screen retained entries.
+ pub fn set_retention_theta(&mut self, retention_theta: u64) {
assert!(
- (1..=MAX_THETA).contains(&theta),
- "theta must be in [1, {MAX_THETA}], got {theta}"
+ (1..=MAX_THETA).contains(&retention_theta),
+ "theta must be in [1, {MAX_THETA}], got {retention_theta}"
);
- self.theta = theta;
+ self.retention_theta = retention_theta;
}
/// Returns minimal lg_size where rebuild-capacity can hold `count`.
@@ -435,7 +414,7 @@ where
let (_lesser, kth, _greater) =
retained.select_nth_unstable_by_key(k, |e| e.hash());
kth.hash()
};
- self.theta = kth_hash;
+ self.retention_theta = kth_hash;
retained.truncate(k);
let size = 1 << self.lg_cur_size;
@@ -478,8 +457,8 @@ pub fn starting_sub_multiple(lg_target: u8, lg_min: u8,
lg_resize_factor: u8) ->
}
}
-/// Compute initial theta for hash table based on sampling probability.
-pub fn starting_theta_from_sampling_probability(sampling_probability: f32) ->
u64 {
+/// Computes the initial operational theta from a sampling probability.
+pub fn starting_retention_theta(sampling_probability: f32) -> u64 {
if sampling_probability < 1.0 {
(MAX_THETA as f64 * sampling_probability as f64) as u64
} else {
diff --git a/datasketches/src/thetafamily/common/intersection.rs
b/datasketches/src/thetafamily/common/intersection.rs
index 45d4782..486b2cb 100644
--- a/datasketches/src/thetafamily/common/intersection.rs
+++ b/datasketches/src/thetafamily/common/intersection.rs
@@ -15,18 +15,17 @@
// specific language governing permissions and limitations
// under the License.
-use crate::common::ResizeFactor;
use crate::error::Error;
use crate::error::ErrorKind;
use crate::hash::check_seed_hash;
use crate::hash::compute_seed_hash;
use crate::thetacommon::EntrySketch;
use crate::thetacommon::SketchEntry;
-use crate::thetacommon::SketchScalars;
use crate::thetacommon::constants::HASH_TABLE_REBUILD_THRESHOLD;
use crate::thetacommon::constants::MAX_THETA;
-use crate::thetacommon::hash_table::CompactSketchParts;
use crate::thetacommon::hash_table::SketchHashTable;
+use crate::thetacommon::sketch_state::CompactSketchState;
+use crate::thetacommon::sketch_state::ThetaFamilySketchMetadata;
/// Merges an incoming entry into an existing entry with the same hash.
///
@@ -44,7 +43,15 @@ pub trait IntersectionMergePolicy<E> {
pub struct IntersectionState<E, P> {
table: SketchHashTable<E>,
policy: P,
- has_result: bool,
+ result_state: IntersectionResultState,
+}
+
+/// State of the value currently represented by an intersection operator.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+enum IntersectionResultState {
+ Uninitialized,
+ Empty,
+ NonEmpty,
}
impl<E, P> IntersectionState<E, P>
@@ -55,17 +62,8 @@ where
pub fn new(seed: u64, policy: P) -> Result<Self, Error> {
let seed_hash = compute_seed_hash(seed, ErrorKind::InvalidArgument)?;
Ok(Self {
- has_result: false,
- table: SketchHashTable::from_raw_parts(
- 0,
- 0,
- ResizeFactor::X1,
- 1.0,
- MAX_THETA,
- seed,
- seed_hash,
- false,
- ),
+ result_state: IntersectionResultState::Uninitialized,
+ table: SketchHashTable::for_set_operation(0, 0, MAX_THETA, seed,
seed_hash),
policy,
})
}
@@ -80,76 +78,71 @@ where
E: Clone,
P: IntersectionMergePolicy<E>,
{
- let SketchScalars {
- seed_hash,
- theta,
- empty,
- ordered,
- num_retained,
- } = sketch.scalars();
- let new_default_table = |table: &SketchHashTable<E>| {
- SketchHashTable::from_raw_parts(
+ let table_without_entries = |table: &SketchHashTable<E>,
retention_theta| {
+ SketchHashTable::for_set_operation(
0,
0,
- ResizeFactor::X1,
- 1.0,
- table.theta(),
+ retention_theta,
table.seed(),
table.seed_hash(),
- table.is_empty(),
)
};
- if self.table.is_empty() {
+ if self.result_state == IntersectionResultState::Empty {
return Ok(());
}
- if empty {
- self.table.set_empty(true);
- } else {
- check_seed_hash(
- self.table.seed_hash(),
+ let (seed_hash, input_theta, input_ordered, input_num_retained) =
match sketch.metadata() {
+ ThetaFamilySketchMetadata::Empty { .. } => {
+ self.result_state = IntersectionResultState::Empty;
+ self.table = table_without_entries(&self.table, MAX_THETA);
+ return Ok(());
+ }
+ ThetaFamilySketchMetadata::NonEmpty {
seed_hash,
- "intersection update",
- ErrorKind::InvalidArgument,
- )?;
- }
+ theta,
+ ordered,
+ num_retained,
+ } => (seed_hash, theta, ordered, num_retained),
+ };
- self.table.set_theta(if self.table.is_empty() {
- MAX_THETA
- } else {
- self.table.theta().min(theta)
- });
+ check_seed_hash(
+ self.table.seed_hash(),
+ seed_hash,
+ "intersection update",
+ ErrorKind::InvalidArgument,
+ )?;
+
+ let result_theta = self.table.retention_theta().min(input_theta);
+ self.table.set_retention_theta(result_theta);
- if self.has_result && self.table.num_retained() == 0 {
+ if self.result_state == IntersectionResultState::NonEmpty &&
self.table.num_retained() == 0
+ {
return Ok(());
}
- if num_retained == 0 {
- self.has_result = true;
- self.table = new_default_table(&self.table);
+ if input_num_retained == 0 {
+ self.result_state = IntersectionResultState::NonEmpty;
+ self.table = table_without_entries(&self.table, result_theta);
return Ok(());
}
// first update, copy incoming entries
- if !self.has_result {
- self.has_result = true;
+ if self.result_state == IntersectionResultState::Uninitialized {
+ self.result_state = IntersectionResultState::NonEmpty;
let lg_size = SketchHashTable::<E>::lg_size_from_count_for_rebuild(
- num_retained,
+ input_num_retained,
HASH_TABLE_REBUILD_THRESHOLD,
);
- // num_retained >= 1 here (the zero case returned early above), so
lg_size >= 1 and
- // lg_size - 1 below cannot underflow.
+ // The retained count is at least one here, so lg_size >= 1 and
lg_size - 1 below
+ // cannot underflow.
debug_assert!(lg_size >= 1);
- self.table = SketchHashTable::from_raw_parts(
+ self.table = SketchHashTable::for_set_operation(
lg_size,
lg_size - 1,
- ResizeFactor::X1,
- 1.0,
- self.table.theta(),
+ result_theta,
self.table.seed(),
self.table.seed_hash(),
- self.table.is_empty(),
);
for entry in sketch.entries() {
let hash = entry.hash();
@@ -163,18 +156,18 @@ where
}
}
// Safety check.
- if self.table.num_retained() != num_retained {
+ if self.table.num_retained() != input_num_retained {
return Err(Error::invalid_argument(
"num entries mismatch, possibly corrupted input sketch",
));
}
} else {
- let max_matches = self.table.num_retained().min(num_retained);
+ let max_matches =
self.table.num_retained().min(input_num_retained);
let mut matched_entries = Vec::with_capacity(max_matches);
let mut count = 0;
for entry in sketch.entries() {
let hash = entry.hash();
- if hash < self.table.theta() {
+ if hash < self.table.retention_theta() {
if let Some(existing) = self.table.entry(hash) {
if matched_entries.len() == max_matches {
return Err(Error::invalid_argument(
@@ -185,25 +178,25 @@ where
self.policy.merge(&mut merged, entry);
matched_entries.push(merged);
}
- } else if ordered {
+ } else if input_ordered {
break; // early stop for ordered sketches
}
count += 1;
}
// Safety check.
- if count > num_retained {
+ if count > input_num_retained {
return Err(Error::invalid_argument(
"more keys than expected, possibly corrupted input sketch",
));
- } else if !ordered && count < num_retained {
+ } else if !input_ordered && count < input_num_retained {
return Err(Error::invalid_argument(
"fewer keys than expected, possibly corrupted input
sketch",
));
}
if matched_entries.is_empty() {
- self.table = new_default_table(&self.table);
- if self.table.theta() == MAX_THETA {
- self.table.set_empty(true);
+ self.table = table_without_entries(&self.table, result_theta);
+ if result_theta == MAX_THETA {
+ self.result_state = IntersectionResultState::Empty;
}
} else {
let lg_size =
SketchHashTable::<E>::lg_size_from_count_for_rebuild(
@@ -213,15 +206,12 @@ where
// matched_entries is non-empty here (the empty case is
handled above), so
// lg_size >= 1 and lg_size - 1 below cannot underflow.
debug_assert!(lg_size >= 1);
- self.table = SketchHashTable::from_raw_parts(
+ self.table = SketchHashTable::for_set_operation(
lg_size,
lg_size - 1,
- ResizeFactor::X1,
- 1.0,
- self.table.theta(),
+ result_theta,
self.table.seed(),
self.table.seed_hash(),
- self.table.is_empty(),
);
for entry in matched_entries {
let hash = entry.hash();
@@ -241,7 +231,7 @@ where
/// Returns whether this operator has received at least one update.
pub fn has_result(&self) -> bool {
- self.has_result
+ self.result_state != IntersectionResultState::Uninitialized
}
/// Returns the estimated size of the heap allocations in bytes.
@@ -249,21 +239,19 @@ where
self.table.estimated_size()
}
- /// Returns the current intersection state as compact-sketch parts.
- pub fn to_compact_parts(&self, ordered: bool) -> CompactSketchParts<E>
+ /// Returns the current intersection as canonical compact-sketch state.
+ pub fn to_compact_sketch_state(&self, ordered: bool) ->
Option<CompactSketchState<E>>
where
E: Clone,
{
- let mut entries: Vec<E> = self.table.iter_entries().cloned().collect();
- if ordered {
- entries.sort_unstable_by_key(SketchEntry::hash);
- }
- CompactSketchParts {
- entries,
- theta: self.table.theta(),
- seed_hash: self.table.seed_hash(),
- ordered,
- empty: self.table.is_empty(),
+ match self.result_state {
+ IntersectionResultState::Uninitialized => None,
+ IntersectionResultState::Empty => {
+ Some(CompactSketchState::empty(self.table.seed_hash()))
+ }
+ IntersectionResultState::NonEmpty => {
+ Some(self.table.to_non_empty_compact_state(ordered))
+ }
}
}
}
diff --git a/datasketches/src/thetafamily/common/jaccard_similarity.rs
b/datasketches/src/thetafamily/common/jaccard_similarity.rs
index a761cbd..ec67ba0 100644
--- a/datasketches/src/thetafamily/common/jaccard_similarity.rs
+++ b/datasketches/src/thetafamily/common/jaccard_similarity.rs
@@ -23,14 +23,14 @@ use crate::hash::compute_seed_hash;
use crate::thetacommon::EntrySketch;
use crate::thetacommon::KeySketch;
use crate::thetacommon::SketchEntry;
-use crate::thetacommon::SketchScalars;
use crate::thetacommon::binomial_bounds;
use crate::thetacommon::constants::MAX_LG_K;
use crate::thetacommon::constants::MAX_THETA;
use crate::thetacommon::constants::MIN_LG_K;
-use crate::thetacommon::hash_table::CompactSketchParts;
use crate::thetacommon::intersection::IntersectionMergePolicy;
use crate::thetacommon::intersection::IntersectionState;
+use crate::thetacommon::sketch_state::CompactSketchState;
+use crate::thetacommon::sketch_state::ThetaFamilySketchMetadata;
use crate::thetacommon::union::UnionMergePolicy;
use crate::thetacommon::union::UnionState;
@@ -85,12 +85,7 @@ impl JaccardSimilarity {
}
let sampling_probability = theta as f64 / MAX_THETA as f64;
- if sampling_probability <= 0.0 || sampling_probability > 1.0 {
- return Err(Error::invalid_argument(format!(
- "theta must produce a probability in (0.0, 1.0], got
{sampling_probability}"
- )));
- }
- if sampling_probability == 1.0 {
+ if theta == MAX_THETA {
return Ok(Self::exact(intersection_count as f64 / union_count as
f64));
}
@@ -140,8 +135,8 @@ impl<S> KeySketch for KeyEntries<S>
where
S: KeySketch,
{
- fn scalars(self) -> SketchScalars {
- self.0.scalars()
+ fn metadata(self) -> ThetaFamilySketchMetadata {
+ self.0.metadata()
}
fn hashes(self) -> impl Iterator<Item = u64> {
@@ -165,27 +160,14 @@ where
A: KeySketch,
B: KeySketch,
{
- let SketchScalars {
- theta: a_theta,
- empty: a_empty,
- num_retained: a_num_retained,
- ..
- } = sketch_a.scalars();
- let SketchScalars {
- theta: b_theta,
- empty: b_empty,
- num_retained: b_num_retained,
- ..
- } = sketch_b.scalars();
- if a_empty && b_empty {
- return Ok(JaccardSimilarity::exact(1.0));
- }
- if a_empty || b_empty {
- return Ok(JaccardSimilarity::exact(0.0));
- }
-
- let sketch_a_state = (a_num_retained, a_theta);
- let sketch_b_state = (b_num_retained, b_theta);
+ let (sketch_a_state, sketch_b_state) = match (
+ non_empty_count_and_theta(sketch_a.metadata()),
+ non_empty_count_and_theta(sketch_b.metadata()),
+ ) {
+ (None, None) => return Ok(JaccardSimilarity::exact(1.0)),
+ (None, _) | (_, None) => return Ok(JaccardSimilarity::exact(0.0)),
+ (Some(a), Some(b)) => (a, b),
+ };
let union = compute_union(seed, sketch_a, sketch_b)?;
if identical_sets(sketch_a_state, sketch_b_state, &union) {
return Ok(JaccardSimilarity::exact(1.0));
@@ -194,17 +176,20 @@ where
let mut intersection = IntersectionState::new(seed, NoopMergePolicy)?;
intersection.update(KeyEntries(sketch_a))?;
intersection.update(KeyEntries(sketch_b))?;
- let intersection = intersection.to_compact_parts(false);
+ let intersection = intersection
+ .to_compact_sketch_state(false)
+ .expect("two intersection updates must produce a result");
+ let union_theta = union.theta();
let intersection_count = intersection
- .entries
+ .retained_entries()
.iter()
- .filter(|entry| entry.hash < union.theta)
+ .filter(|entry| entry.hash < union_theta)
.count();
JaccardSimilarity::ratio_bounds(
- union.entries.len() as u64,
+ union.retained_entries().len() as u64,
intersection_count as u64,
- union.theta,
+ union_theta,
)
}
@@ -213,27 +198,14 @@ where
A: KeySketch,
B: KeySketch,
{
- let SketchScalars {
- theta: a_theta,
- empty: a_empty,
- num_retained: a_num_retained,
- ..
- } = sketch_a.scalars();
- let SketchScalars {
- theta: b_theta,
- empty: b_empty,
- num_retained: b_num_retained,
- ..
- } = sketch_b.scalars();
- if a_empty && b_empty {
- return Ok(true);
- }
- if a_empty || b_empty {
- return Ok(false);
- }
-
- let sketch_a_state = (a_num_retained, a_theta);
- let sketch_b_state = (b_num_retained, b_theta);
+ let (sketch_a_state, sketch_b_state) = match (
+ non_empty_count_and_theta(sketch_a.metadata()),
+ non_empty_count_and_theta(sketch_b.metadata()),
+ ) {
+ (None, None) => return Ok(true),
+ (None, _) | (_, None) => return Ok(false),
+ (Some(a), Some(b)) => (a, b),
+ };
let union = compute_union(seed, sketch_a, sketch_b)?;
Ok(identical_sets(sketch_a_state, sketch_b_state, &union))
}
@@ -242,21 +214,27 @@ fn compute_union<A, B>(
seed: u64,
sketch_a: A,
sketch_b: B,
-) -> Result<CompactSketchParts<KeyEntry>, Error>
+) -> Result<CompactSketchState<KeyEntry>, Error>
where
A: KeySketch,
B: KeySketch,
{
- let SketchScalars {
+ let ThetaFamilySketchMetadata::NonEmpty {
seed_hash: a_seed_hash,
num_retained: a_num_retained,
..
- } = sketch_a.scalars();
- let SketchScalars {
+ } = sketch_a.metadata()
+ else {
+ unreachable!("Jaccard union inputs are known to be non-empty")
+ };
+ let ThetaFamilySketchMetadata::NonEmpty {
seed_hash: b_seed_hash,
num_retained: b_num_retained,
..
- } = sketch_b.scalars();
+ } = sketch_b.metadata()
+ else {
+ unreachable!("Jaccard union inputs are known to be non-empty")
+ };
let seed_hash = compute_seed_hash(seed, ErrorKind::InvalidArgument)?;
check_seed_hash(seed_hash, a_seed_hash, "A", ErrorKind::InvalidData)?;
check_seed_hash(seed_hash, b_seed_hash, "B", ErrorKind::InvalidData)?;
@@ -270,7 +248,7 @@ where
)?;
union.update(KeyEntries(sketch_a))?;
union.update(KeyEntries(sketch_b))?;
- Ok(union.to_compact_parts(false))
+ Ok(union.to_compact_sketch_state(false))
}
/// Returns whether both sketches have the same retained keys and theta.
@@ -280,12 +258,23 @@ where
fn identical_sets(
sketch_a: (usize, u64),
sketch_b: (usize, u64),
- union: &CompactSketchParts<KeyEntry>,
+ union: &CompactSketchState<KeyEntry>,
) -> bool {
- union.entries.len() == sketch_a.0
- && union.entries.len() == sketch_b.0
- && union.theta == sketch_a.1
- && union.theta == sketch_b.1
+ union.retained_entries().len() == sketch_a.0
+ && union.retained_entries().len() == sketch_b.0
+ && union.theta() == sketch_a.1
+ && union.theta() == sketch_b.1
+}
+
+fn non_empty_count_and_theta(metadata: ThetaFamilySketchMetadata) ->
Option<(usize, u64)> {
+ match metadata {
+ ThetaFamilySketchMetadata::Empty { .. } => None,
+ ThetaFamilySketchMetadata::NonEmpty {
+ theta,
+ num_retained,
+ ..
+ } => Some((num_retained, theta)),
+ }
}
fn sampling_adjuster(sampling_probability: f64) -> f64 {
diff --git a/datasketches/src/thetafamily/common/mod.rs
b/datasketches/src/thetafamily/common/mod.rs
index 59f5dae..dbc273e 100644
--- a/datasketches/src/thetafamily/common/mod.rs
+++ b/datasketches/src/thetafamily/common/mod.rs
@@ -23,6 +23,7 @@ pub(super) mod constants;
pub(super) mod hash_table;
pub(super) mod intersection;
pub(super) mod jaccard_similarity;
+pub(super) mod sketch_state;
pub(super) mod union;
pub use self::jaccard_similarity::JaccardSimilarity;
@@ -33,7 +34,7 @@ pub(super) trait SketchEntry {
}
pub(super) trait KeySketch: Copy {
- fn scalars(self) -> SketchScalars;
+ fn metadata(self) -> sketch_state::ThetaFamilySketchMetadata;
fn hashes(self) -> impl Iterator<Item = u64>;
}
@@ -43,13 +44,3 @@ pub(super) trait EntrySketch: KeySketch {
fn entries(self) -> impl Iterator<Item = Self::Entry>;
}
-
-/// Scalar sketch values inspected by Theta-family set operations.
-#[derive(Clone, Copy, Debug)]
-pub(super) struct SketchScalars {
- pub seed_hash: u16,
- pub theta: u64,
- pub empty: bool,
- pub ordered: bool,
- pub num_retained: usize,
-}
diff --git a/datasketches/src/thetafamily/common/sketch_state.rs
b/datasketches/src/thetafamily/common/sketch_state.rs
new file mode 100644
index 0000000..d59f74f
--- /dev/null
+++ b/datasketches/src/thetafamily/common/sketch_state.rs
@@ -0,0 +1,133 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use crate::thetacommon::constants::MAX_THETA;
+
+/// Observable metadata consumed by shared Theta-family set operations.
+///
+/// Empty sketches cannot carry a retained count, theta, or ordering claim in
this representation.
+/// A non-empty sketch may retain zero entries when theta screened every input.
+#[derive(Clone, Copy, Debug)]
+pub enum ThetaFamilySketchMetadata {
+ Empty {
+ seed_hash: u16,
+ },
+ NonEmpty {
+ seed_hash: u16,
+ theta: u64,
+ ordered: bool,
+ num_retained: usize,
+ },
+}
+
+/// Canonical in-memory state for a compact Theta-family sketch.
+///
+/// The empty variant deliberately has neither retained entries nor theta.
This makes the only
+/// representable empty state use `MAX_THETA`, report exact mode, and
serialize through the
+/// canonical empty-image path. The non-empty variant may contain no retained
entries after theta
+/// screening.
+#[derive(Clone, Debug)]
+pub enum CompactSketchState<E> {
+ Empty {
+ seed_hash: u16,
+ },
+ NonEmpty {
+ retained_entries: Vec<E>,
+ theta: u64,
+ seed_hash: u16,
+ ordered: bool,
+ },
+}
+
+impl<E> CompactSketchState<E> {
+ pub fn empty(seed_hash: u16) -> Self {
+ Self::Empty { seed_hash }
+ }
+
+ pub fn non_empty(retained_entries: Vec<E>, theta: u64, seed_hash: u16,
ordered: bool) -> Self {
+ Self::NonEmpty {
+ retained_entries,
+ theta,
+ seed_hash,
+ ordered,
+ }
+ }
+
+ pub fn theta(&self) -> u64 {
+ match self {
+ Self::Empty { .. } => MAX_THETA,
+ Self::NonEmpty { theta, .. } => *theta,
+ }
+ }
+
+ pub fn is_empty(&self) -> bool {
+ matches!(self, Self::Empty { .. })
+ }
+
+ pub fn is_estimation_mode(&self) -> bool {
+ matches!(self, Self::NonEmpty { theta, .. } if *theta < MAX_THETA)
+ }
+
+ pub fn seed_hash(&self) -> u16 {
+ match self {
+ Self::Empty { seed_hash } | Self::NonEmpty { seed_hash, .. } =>
*seed_hash,
+ }
+ }
+
+ pub fn retained_entries(&self) -> &[E] {
+ match self {
+ Self::Empty { .. } => &[],
+ Self::NonEmpty {
+ retained_entries, ..
+ } => retained_entries,
+ }
+ }
+
+ pub fn retained_entries_capacity(&self) -> usize {
+ match self {
+ Self::Empty { .. } => 0,
+ Self::NonEmpty {
+ retained_entries, ..
+ } => retained_entries.capacity(),
+ }
+ }
+
+ pub fn is_ordered(&self) -> bool {
+ match self {
+ Self::Empty { .. } => true,
+ Self::NonEmpty { ordered, .. } => *ordered,
+ }
+ }
+
+ #[cfg(feature = "theta")]
+ pub fn map_retained_entries<T>(self, mut f: impl FnMut(E) -> T) ->
CompactSketchState<T> {
+ match self {
+ Self::Empty { seed_hash } => CompactSketchState::empty(seed_hash),
+ Self::NonEmpty {
+ retained_entries,
+ theta,
+ seed_hash,
+ ordered,
+ } => CompactSketchState::non_empty(
+ retained_entries.into_iter().map(&mut f).collect(),
+ theta,
+ seed_hash,
+ ordered,
+ ),
+ }
+ }
+}
diff --git a/datasketches/src/thetafamily/common/union.rs
b/datasketches/src/thetafamily/common/union.rs
index 95af17b..0131b55 100644
--- a/datasketches/src/thetafamily/common/union.rs
+++ b/datasketches/src/thetafamily/common/union.rs
@@ -21,10 +21,10 @@ use crate::error::ErrorKind;
use crate::hash::check_seed_hash;
use crate::thetacommon::EntrySketch;
use crate::thetacommon::SketchEntry;
-use crate::thetacommon::SketchScalars;
use crate::thetacommon::constants::MAX_THETA;
-use crate::thetacommon::hash_table::CompactSketchParts;
use crate::thetacommon::hash_table::SketchHashTable;
+use crate::thetacommon::sketch_state::CompactSketchState;
+use crate::thetacommon::sketch_state::ThetaFamilySketchMetadata;
/// Merges an incoming entry into an existing entry with the same hash.
pub trait UnionMergePolicy<E> {
@@ -39,7 +39,8 @@ pub trait UnionMergePolicy<E> {
pub struct UnionState<E, P> {
table: SketchHashTable<E>,
policy: P,
- union_theta: u64,
+ // None until the union receives a non-empty input sketch.
+ result_theta: Option<u64>,
}
impl<E, P> UnionState<E, P>
@@ -55,7 +56,7 @@ where
) -> Result<Self, Error> {
let table = SketchHashTable::new(lg_k, resize_factor,
sampling_probability, seed)?;
Ok(Self {
- union_theta: table.theta(),
+ result_theta: None,
table,
policy,
})
@@ -67,16 +68,15 @@ where
S: EntrySketch<Entry = E>,
P: UnionMergePolicy<E>,
{
- let SketchScalars {
+ let ThetaFamilySketchMetadata::NonEmpty {
seed_hash,
theta,
- empty,
ordered,
..
- } = sketch.scalars();
- if empty {
+ } = sketch.metadata()
+ else {
return Ok(());
- }
+ };
check_seed_hash(
self.table.seed_hash(),
@@ -85,12 +85,13 @@ where
ErrorKind::InvalidArgument,
)?;
- self.table.set_empty(false);
- self.union_theta = self.union_theta.min(theta);
+ let current_theta =
self.result_theta.unwrap_or(self.table.retention_theta());
+ let result_theta = current_theta.min(theta);
+ self.result_theta = Some(result_theta);
for entry in sketch.entries() {
let hash = entry.hash();
- if hash < self.union_theta && hash < self.table.theta() {
+ if hash < result_theta && hash < self.table.retention_theta() {
self.table.upsert_entry(hash, |existing| match existing {
Some(existing) => {
self.policy.merge(existing, entry);
@@ -102,30 +103,22 @@ where
break;
}
}
- self.union_theta = self.union_theta.min(self.table.theta());
+ self.result_theta =
Some(result_theta.min(self.table.retention_theta()));
Ok(())
}
- /// Return the current compact-union state as compact-sketch parts.
- pub fn to_compact_parts(&self, ordered: bool) -> CompactSketchParts<E>
+ /// Returns the union as canonical compact-sketch state.
+ pub fn to_compact_sketch_state(&self, ordered: bool) ->
CompactSketchState<E>
where
E: Clone,
{
- let seed_hash = self.table.seed_hash();
-
- if self.table.is_empty() {
- return CompactSketchParts {
- entries: vec![],
- theta: self.union_theta,
- seed_hash,
- ordered: true,
- empty: true,
- };
- }
+ let Some(result_theta) = self.result_theta else {
+ return CompactSketchState::empty(self.table.seed_hash());
+ };
- let mut theta = self.union_theta.min(self.table.theta());
- let mut entries = if self.union_theta >= self.table.theta() {
+ let mut theta = result_theta.min(self.table.retention_theta());
+ let mut retained_entries = if result_theta >=
self.table.retention_theta() {
self.table.iter_entries().cloned().collect::<Vec<_>>()
} else {
self.table
@@ -136,30 +129,25 @@ where
};
let nominal_num = 1usize << self.table.lg_nom_size();
- if entries.len() > nominal_num {
- let (_, kth, _) = entries.select_nth_unstable_by_key(nominal_num,
|entry| entry.hash());
+ if retained_entries.len() > nominal_num {
+ let (_, kth, _) =
+ retained_entries.select_nth_unstable_by_key(nominal_num,
|entry| entry.hash());
theta = kth.hash();
- entries.truncate(nominal_num);
+ retained_entries.truncate(nominal_num);
}
- let ordered = ordered || (entries.len() == 1 && theta == MAX_THETA);
+ let ordered = ordered || (retained_entries.len() == 1 && theta ==
MAX_THETA);
if ordered {
- entries.sort_unstable_by_key(SketchEntry::hash);
+ retained_entries.sort_unstable_by_key(SketchEntry::hash);
}
- CompactSketchParts {
- entries,
- theta,
- seed_hash,
- ordered,
- empty: false,
- }
+ CompactSketchState::non_empty(retained_entries, theta,
self.table.seed_hash(), ordered)
}
/// Reset the union to its initial state.
pub fn reset(&mut self) {
self.table.reset();
- self.union_theta = self.table.theta();
+ self.result_theta = None;
}
/// Returns the estimated size of the heap allocations in bytes.
diff --git a/datasketches/src/thetafamily/theta/a_not_b.rs
b/datasketches/src/thetafamily/theta/a_not_b.rs
index 8f3b43e..7f89b86 100644
--- a/datasketches/src/thetafamily/theta/a_not_b.rs
+++ b/datasketches/src/thetafamily/theta/a_not_b.rs
@@ -91,17 +91,8 @@ impl ThetaANotB {
) -> Result<CompactThetaSketch, Error> {
let a = a.into();
let b = b.into();
- let parts = a_not_b::compute(self.seed_hash, a, b, ordered)?;
- Ok(CompactThetaSketch::from_parts(
- parts
- .entries
- .into_iter()
- .map(|entry| entry.hash())
- .collect(),
- parts.theta,
- parts.seed_hash,
- parts.ordered,
- parts.empty,
- ))
+ let compact_state = a_not_b::compute(self.seed_hash, a, b, ordered)?
+ .map_retained_entries(|entry| entry.hash());
+ Ok(CompactThetaSketch::from_compact_state(compact_state))
}
}
diff --git a/datasketches/src/thetafamily/theta/intersection.rs
b/datasketches/src/thetafamily/theta/intersection.rs
index 3f324cd..8f59f9a 100644
--- a/datasketches/src/thetafamily/theta/intersection.rs
+++ b/datasketches/src/thetafamily/theta/intersection.rs
@@ -85,20 +85,12 @@ impl ThetaIntersection {
///
/// If `ordered` is `true`, retained hashes are sorted in ascending order.
pub fn to_sketch(&self, ordered: bool) -> Option<CompactThetaSketch> {
- if !self.state.has_result() {
- return None;
- }
- let parts = self.state.to_compact_parts(ordered);
- Some(CompactThetaSketch::from_parts(
- parts
- .entries
- .into_iter()
- .map(|entry| entry.hash())
- .collect(),
- parts.theta,
- parts.seed_hash,
- parts.ordered,
- parts.empty,
- ))
+ self.state
+ .to_compact_sketch_state(ordered)
+ .map(|compact_state| {
+ CompactThetaSketch::from_compact_state(
+ compact_state.map_retained_entries(|entry| entry.hash()),
+ )
+ })
}
}
diff --git a/datasketches/src/thetafamily/theta/sketch.rs
b/datasketches/src/thetafamily/theta/sketch.rs
index 28e2c12..88f002e 100644
--- a/datasketches/src/thetafamily/theta/sketch.rs
+++ b/datasketches/src/thetafamily/theta/sketch.rs
@@ -48,7 +48,6 @@ use crate::theta::serialization::V2_PREAMBLE_ESTIMATE;
use crate::theta::serialization::V2_PREAMBLE_PRECISE;
use crate::thetacommon::EntrySketch;
use crate::thetacommon::KeySketch;
-use crate::thetacommon::SketchScalars;
use crate::thetacommon::binomial_bounds;
use crate::thetacommon::constants::DEFAULT_LG_K;
use crate::thetacommon::constants::FLAGS_IS_COMPACT;
@@ -57,6 +56,8 @@ use crate::thetacommon::constants::FLAGS_IS_ORDERED;
use crate::thetacommon::constants::FLAGS_IS_READ_ONLY;
use crate::thetacommon::constants::MAX_THETA;
use crate::thetacommon::hash_table::SketchHashTableIter;
+use crate::thetacommon::sketch_state::CompactSketchState;
+use crate::thetacommon::sketch_state::ThetaFamilySketchMetadata;
/// Read-only view for Theta sketches.
///
@@ -123,7 +124,7 @@ impl<'a> ThetaSketchView<'a> {
}
}
- /// Returns whether the viewed sketch has not received any updates.
+ /// Returns `true` if the viewed sketch is empty.
pub fn is_empty(&self) -> bool {
match self.0 {
ThetaSketchViewState::Mutable(sketch) => sketch.is_empty(),
@@ -146,7 +147,7 @@ impl<'a> ThetaSketchView<'a> {
ThetaSketchIter::Mutable(sketch.table.iter_entries())
}
ThetaSketchViewState::Compact(sketch) => {
- ThetaSketchIter::Compact(sketch.entries.iter())
+
ThetaSketchIter::Compact(sketch.compact_state.retained_entries().iter())
}
}
}
@@ -161,13 +162,18 @@ impl<'a> ThetaSketchView<'a> {
}
impl KeySketch for ThetaSketchView<'_> {
- fn scalars(self) -> SketchScalars {
- SketchScalars {
- seed_hash: self.seed_hash(),
- theta: self.theta64(),
- empty: self.is_empty(),
- ordered: self.is_ordered(),
- num_retained: self.num_retained(),
+ fn metadata(self) -> ThetaFamilySketchMetadata {
+ if self.is_empty() {
+ ThetaFamilySketchMetadata::Empty {
+ seed_hash: self.seed_hash(),
+ }
+ } else {
+ ThetaFamilySketchMetadata::NonEmpty {
+ seed_hash: self.seed_hash(),
+ theta: self.theta64(),
+ ordered: self.is_ordered(),
+ num_retained: self.num_retained(),
+ }
}
}
@@ -200,6 +206,8 @@ impl<'a> From<&'a CompactThetaSketch> for
ThetaSketchView<'a> {
#[derive(Debug)]
pub struct ThetaSketch {
table: ThetaHashTable,
+ // Public emptiness tracks update calls, not retained entries: theta may
screen every update.
+ is_empty: bool,
}
impl ThetaSketch {
@@ -228,6 +236,7 @@ impl ThetaSketch {
/// assert!(sketch.estimate() >= 1.0);
/// ```
pub fn update<T: Hash>(&mut self, value: T) {
+ self.is_empty = false;
self.table.try_insert(value);
}
@@ -247,18 +256,25 @@ impl ThetaSketch {
return 0.0;
}
let num_retained = self.table.num_retained() as f64;
- let theta = self.table.theta() as f64 / MAX_THETA as f64;
+ let theta = self.theta64() as f64 / MAX_THETA as f64;
num_retained / theta
}
/// Returns theta as a fraction in `[0.0, 1.0]`.
pub fn theta(&self) -> f64 {
- self.table.theta() as f64 / MAX_THETA as f64
+ self.theta64() as f64 / MAX_THETA as f64
}
/// Returns theta as a `u64`.
+ ///
+ /// An empty sketch reports `MAX_THETA` even when it was built with a
sampling probability
+ /// below `1.0`, matching the other DataSketches implementations.
pub fn theta64(&self) -> u64 {
- self.table.theta()
+ if self.is_empty {
+ MAX_THETA
+ } else {
+ self.table.retention_theta()
+ }
}
/// Returns the 16-bit seed hash.
@@ -268,12 +284,12 @@ impl ThetaSketch {
/// Returns `true` if the sketch is empty.
pub fn is_empty(&self) -> bool {
- self.table.is_empty()
+ self.is_empty
}
/// Returns `true` if the sketch is in estimation mode.
pub fn is_estimation_mode(&self) -> bool {
- self.table.theta() < MAX_THETA
+ !self.is_empty && self.table.retention_theta() < MAX_THETA
}
/// Returns the number of retained entries.
@@ -294,6 +310,7 @@ impl ThetaSketch {
/// Resets the sketch to its empty state.
pub fn reset(&mut self) {
self.table.reset();
+ self.is_empty = true;
}
/// Returns an iterator over retained entries.
@@ -327,18 +344,14 @@ impl ThetaSketch {
/// assert_eq!(compact.num_retained(), 1);
/// ```
pub fn compact(&self, ordered: bool) -> CompactThetaSketch {
- let parts = self.table.to_compact_parts(ordered);
- CompactThetaSketch::from_parts(
- parts
- .entries
- .into_iter()
- .map(|entry| entry.hash())
- .collect(),
- parts.theta,
- parts.seed_hash,
- parts.ordered,
- parts.empty,
- )
+ let compact_state = if self.is_empty() {
+ debug_assert_eq!(self.num_retained(), 0);
+ CompactSketchState::empty(self.seed_hash())
+ } else {
+ self.table.to_non_empty_compact_state(ordered)
+ }
+ .map_retained_entries(|entry| entry.hash());
+ CompactThetaSketch::from_compact_state(compact_state)
}
/// Returns the approximate lower error bound for the specified number of
standard deviations.
@@ -426,28 +439,12 @@ impl ThetaSketch {
/// plus theta and a 16-bit seed hash. It can be ordered (sorted ascending) or
unordered.
#[derive(Clone, Debug)]
pub struct CompactThetaSketch {
- entries: Vec<u64>,
- theta: u64,
- seed_hash: u16,
- ordered: bool,
- empty: bool,
+ compact_state: CompactSketchState<u64>,
}
impl CompactThetaSketch {
- pub(super) fn from_parts(
- entries: Vec<u64>,
- theta: u64,
- seed_hash: u16,
- ordered: bool,
- empty: bool,
- ) -> Self {
- Self {
- entries,
- theta,
- seed_hash,
- ordered,
- empty,
- }
+ pub(super) fn from_compact_state(compact_state: CompactSketchState<u64>)
-> Self {
+ Self { compact_state }
}
/// Returns a read-only view accepted by Theta set operations.
@@ -461,51 +458,55 @@ impl CompactThetaSketch {
return 0.0;
}
let num_retained = self.num_retained() as f64;
- if self.theta == MAX_THETA {
+ if self.theta64() == MAX_THETA {
return num_retained;
}
- let theta = self.theta as f64 / MAX_THETA as f64;
+ let theta = self.theta();
num_retained / theta
}
/// Returns theta as a fraction in `[0.0, 1.0]`.
pub fn theta(&self) -> f64 {
- self.theta as f64 / MAX_THETA as f64
+ self.theta64() as f64 / MAX_THETA as f64
}
/// Returns theta as a `u64`.
pub fn theta64(&self) -> u64 {
- self.theta
+ self.compact_state.theta()
}
/// Returns `true` if this sketch is empty.
pub fn is_empty(&self) -> bool {
- self.empty
+ self.compact_state.is_empty()
}
/// Returns `true` if this sketch is in estimation mode.
pub fn is_estimation_mode(&self) -> bool {
- self.theta < MAX_THETA
+ self.compact_state.is_estimation_mode()
}
/// Returns the number of retained entries.
pub fn num_retained(&self) -> usize {
- self.entries.len()
+ self.retained_hashes().len()
}
/// Returns `true` if retained entries are ordered (sorted ascending).
pub fn is_ordered(&self) -> bool {
- self.ordered
+ self.compact_state.is_ordered()
}
/// Returns the 16-bit seed hash.
pub fn seed_hash(&self) -> u16 {
- self.seed_hash
+ self.compact_state.seed_hash()
}
/// Returns an iterator over retained entries.
pub fn iter(&self) -> impl Iterator<Item = ThetaEntry> + '_ {
- self.entries.iter().copied().map(ThetaEntry::new)
+ self.retained_hashes().iter().copied().map(ThetaEntry::new)
+ }
+
+ fn retained_hashes(&self) -> &[u64] {
+ self.compact_state.retained_entries()
}
/// Returns the approximate lower error bound for the specified number of
standard deviations.
@@ -536,7 +537,7 @@ impl CompactThetaSketch {
if self.is_estimation_mode() { 2 } else { 1 }
} else if self.is_estimation_mode() {
3
- } else if self.is_empty() || self.entries.len() == 1 {
+ } else if self.is_empty() || self.num_retained() == 1 {
1
} else {
2
@@ -556,14 +557,15 @@ impl CompactThetaSketch {
}
fn is_suitable_for_compression(&self) -> bool {
- self.ordered
- && !self.entries.is_empty()
- && (self.entries.len() != 1 || self.is_estimation_mode())
+ self.is_ordered()
+ && self.num_retained() != 0
+ && (self.num_retained() != 1 || self.is_estimation_mode())
}
/// Serializes this sketch into the uncompressed compact theta format.
pub fn serialize(&self) -> Vec<u8> {
- let mut bytes = SketchBytes::with_capacity(64 + self.entries.len() *
8);
+ let retained_hashes = self.retained_hashes();
+ let mut bytes = SketchBytes::with_capacity(64 + retained_hashes.len()
* 8);
let pre_longs = self.preamble_longs(false);
bytes.write_u8(pre_longs);
@@ -582,28 +584,29 @@ impl CompactThetaSketch {
}
bytes.write_u8(flags);
- bytes.write_u16_le(self.seed_hash);
+ bytes.write_u16_le(self.seed_hash());
if pre_longs > 1 {
- bytes.write_u32_le(self.entries.len() as u32);
+ bytes.write_u32_le(retained_hashes.len() as u32);
bytes.write_u32_be(0); // not used by compact sketches; match
Java/C++
}
if self.is_estimation_mode() {
bytes.write_u64_le(self.theta64());
}
- for hash in self.entries.iter() {
+ for hash in retained_hashes {
bytes.write_u64_le(*hash);
}
bytes.into_bytes()
}
fn serialize_v4(&self) -> Vec<u8> {
+ let retained_hashes = self.retained_hashes();
let pre_longs = self.preamble_longs(true);
- let entry_bits = Self::compute_entry_bits(&self.entries);
- let num_entries_bytes = Self::num_entries_bytes(self.entries.len());
+ let entry_bits = Self::compute_entry_bits(retained_hashes);
+ let num_entries_bytes = Self::num_entries_bytes(retained_hashes.len());
// Pre-size exactly: preamble longs (8 bytes each) + num_entries_bytes
+ packed bits.
- let compressed_bits = entry_bits as usize * self.entries.len();
+ let compressed_bits = entry_bits as usize * retained_hashes.len();
let compressed_bytes = compressed_bits.div_ceil(8);
let out_bytes = (pre_longs as usize * 8) + (num_entries_bytes as
usize) + compressed_bytes;
let mut bytes = SketchBytes::with_capacity(out_bytes);
@@ -620,12 +623,12 @@ impl CompactThetaSketch {
flags |= FLAGS_IS_ORDERED;
bytes.write_u8(flags);
- bytes.write_u16_le(self.seed_hash);
+ bytes.write_u16_le(self.seed_hash());
if self.is_estimation_mode() {
- bytes.write_u64_le(self.theta);
+ bytes.write_u64_le(self.theta64());
}
- let mut n = self.entries.len() as u32;
+ let mut n = retained_hashes.len() as u32;
for _ in 0..num_entries_bytes {
bytes.write_u8((n & 0xff) as u8);
n >>= 8;
@@ -635,10 +638,10 @@ impl CompactThetaSketch {
let mut previous = 0u64;
let mut i = 0usize;
let mut block = vec![0u8; entry_bits as usize];
- while i + BLOCK_WIDTH <= self.entries.len() {
+ while i + BLOCK_WIDTH <= retained_hashes.len() {
let mut deltas = [0u64; BLOCK_WIDTH];
for j in 0..BLOCK_WIDTH {
- let entry = self.entries[i + j];
+ let entry = retained_hashes[i + j];
deltas[j] = entry - previous;
previous = entry;
}
@@ -649,12 +652,12 @@ impl CompactThetaSketch {
}
// pack extra deltas if fewer than 8 of them left
- if i < self.entries.len() {
+ if i < retained_hashes.len() {
let mut block = vec![0u8; entry_bits as usize];
let mut packer = BitPacker::new(&mut block);
- while i < self.entries.len() {
- let delta = self.entries[i] - previous;
- previous = self.entries[i];
+ while i < retained_hashes.len() {
+ let delta = retained_hashes[i] - previous;
+ previous = retained_hashes[i];
packer.pack_value(delta, entry_bits);
i += 1;
}
@@ -748,6 +751,15 @@ impl CompactThetaSketch {
Ok(entries)
}
+ fn deserialize_theta(value: u64) -> Result<u64, Error> {
+ if !(1..=MAX_THETA).contains(&value) {
+ return Err(Error::deserial(format!(
+ "corrupted: theta must be in [1, {MAX_THETA}], got {value}"
+ )));
+ }
+ Ok(value)
+ }
+
fn deserialize_v1(mut cursor: SketchSlice<'_>, expected_seed_hash: u16) ->
Result<Self, Error> {
let seed_hash = expected_seed_hash;
cursor.read_u8().map_err(insufficient_data("<unused>"))?;
@@ -760,30 +772,23 @@ impl CompactThetaSketch {
cursor
.read_u32_le()
.map_err(insufficient_data("<unused_u32_1>"))?;
- let theta = cursor
- .read_u64_le()
- .map_err(insufficient_data("theta_long"))?;
+ let theta = Self::deserialize_theta(
+ cursor
+ .read_u64_le()
+ .map_err(insufficient_data("theta_long"))?,
+ )?;
- let empty = num_entries == 0 && theta == MAX_THETA;
- if empty {
- return Ok(Self {
- entries: vec![],
- theta,
+ if num_entries == 0 && theta == MAX_THETA {
+ return Ok(Self::from_compact_state(CompactSketchState::empty(
seed_hash,
- ordered: true,
- empty: true,
- });
+ )));
}
let entries = Self::read_entries(&mut cursor, num_entries, theta)?;
- Ok(Self {
- entries,
- theta,
- seed_hash,
- ordered: true,
- empty: false,
- })
+ Ok(Self::from_compact_state(CompactSketchState::non_empty(
+ entries, theta, seed_hash, true,
+ )))
}
fn deserialize_v2(
@@ -806,13 +811,9 @@ impl CompactThetaSketch {
)?;
match pre_longs {
- V2_PREAMBLE_EMPTY => Ok(Self {
- entries: vec![],
- theta: MAX_THETA,
+ V2_PREAMBLE_EMPTY =>
Ok(Self::from_compact_state(CompactSketchState::empty(
seed_hash,
- ordered: true,
- empty: true,
- }),
+ ))),
V2_PREAMBLE_PRECISE => {
let num_entries = cursor
.read_u32_le()
@@ -822,13 +823,14 @@ impl CompactThetaSketch {
.read_u32_le()
.map_err(insufficient_data("<unused_u32>"))?;
let entries = Self::read_entries(&mut cursor, num_entries,
MAX_THETA)?;
- Ok(Self {
- entries,
- theta: MAX_THETA,
- seed_hash,
- ordered: true,
- empty: num_entries == 0,
- })
+ if num_entries == 0 {
+ return
Ok(Self::from_compact_state(CompactSketchState::empty(
+ seed_hash,
+ )));
+ }
+ Ok(Self::from_compact_state(CompactSketchState::non_empty(
+ entries, MAX_THETA, seed_hash, true,
+ )))
}
V2_PREAMBLE_ESTIMATE => {
let num_entries = cursor
@@ -838,18 +840,20 @@ impl CompactThetaSketch {
cursor
.read_u32_le()
.map_err(insufficient_data("<unused_u32>"))?;
- let theta = cursor
- .read_u64_le()
- .map_err(insufficient_data("theta_long"))?;
- let empty = (num_entries == 0) && (theta == MAX_THETA);
+ let theta = Self::deserialize_theta(
+ cursor
+ .read_u64_le()
+ .map_err(insufficient_data("theta_long"))?,
+ )?;
let entries = Self::read_entries(&mut cursor, num_entries,
theta)?;
- Ok(Self {
- entries,
- theta,
- seed_hash,
- ordered: true,
- empty,
- })
+ if num_entries == 0 && theta == MAX_THETA {
+ return
Ok(Self::from_compact_state(CompactSketchState::empty(
+ seed_hash,
+ )));
+ }
+ Ok(Self::from_compact_state(CompactSketchState::non_empty(
+ entries, theta, seed_hash, true,
+ )))
}
_ => Err(Error::invalid_preamble_longs(&[1, 2, 3], pre_longs)),
}
@@ -869,41 +873,42 @@ impl CompactThetaSketch {
.map_err(insufficient_data("seed_hash"))?;
let empty = (flags & FLAGS_IS_EMPTY) != 0;
- let mut theta = MAX_THETA;
- let num_entries;
- let mut entries = vec![];
- if !empty {
- check_seed_hash(
- expected_seed_hash,
+ if empty {
+ return Ok(Self::from_compact_state(CompactSketchState::empty(
seed_hash,
- "deserialized CompactThetaSketch v3",
- ErrorKind::InvalidData,
- )?;
- if pre_longs == 1 {
- num_entries = 1;
- } else {
- num_entries = cursor
- .read_u32_le()
- .map_err(insufficient_data("num_entries"))?;
- cursor
- .read_u32_le()
- .map_err(insufficient_data("<unused_u32>"))?;
- if pre_longs > 2 {
- theta = cursor
+ )));
+ }
+
+ check_seed_hash(
+ expected_seed_hash,
+ seed_hash,
+ "deserialized CompactThetaSketch v3",
+ ErrorKind::InvalidData,
+ )?;
+ let mut theta = MAX_THETA;
+ let num_entries = if pre_longs == 1 {
+ 1
+ } else {
+ let num_entries = cursor
+ .read_u32_le()
+ .map_err(insufficient_data("num_entries"))?;
+ cursor
+ .read_u32_le()
+ .map_err(insufficient_data("<unused_u32>"))?;
+ if pre_longs > 2 {
+ theta = Self::deserialize_theta(
+ cursor
.read_u64_le()
- .map_err(insufficient_data("theta_long"))?;
- }
+ .map_err(insufficient_data("theta_long"))?,
+ )?;
}
- entries = Self::read_entries(&mut cursor, num_entries as usize,
theta)?;
- }
+ num_entries
+ };
+ let entries = Self::read_entries(&mut cursor, num_entries as usize,
theta)?;
let ordered = (flags & FLAGS_IS_ORDERED) != 0;
- Ok(Self {
- entries,
- theta,
- seed_hash,
- ordered,
- empty,
- })
+ Ok(Self::from_compact_state(CompactSketchState::non_empty(
+ entries, theta, seed_hash, ordered,
+ )))
}
fn deserialize_v4(
@@ -932,9 +937,11 @@ impl CompactThetaSketch {
)?;
}
let theta = if pre_longs > 1 {
- cursor
- .read_u64_le()
- .map_err(insufficient_data("theta_long"))?
+ Self::deserialize_theta(
+ cursor
+ .read_u64_le()
+ .map_err(insufficient_data("theta_long"))?,
+ )?
} else {
MAX_THETA
};
@@ -1006,18 +1013,17 @@ impl CompactThetaSketch {
let ordered = (flags & FLAGS_IS_ORDERED) != 0;
- Ok(Self {
- entries,
- theta,
- seed_hash,
- ordered,
- empty,
- })
+ let compact_state = if empty {
+ CompactSketchState::empty(seed_hash)
+ } else {
+ CompactSketchState::non_empty(entries, theta, seed_hash, ordered)
+ };
+ Ok(Self::from_compact_state(compact_state))
}
/// Returns the estimated size of the sketch in bytes.
pub fn estimated_size(&self) -> usize {
- size_of::<Self>() + self.entries.capacity() * size_of::<u64>()
+ size_of::<Self>() + self.compact_state.retained_entries_capacity() *
size_of::<u64>()
}
}
@@ -1121,7 +1127,10 @@ impl ThetaSketchBuilder {
self.seed,
)?;
- Ok(ThetaSketch { table })
+ Ok(ThetaSketch {
+ table,
+ is_empty: true,
+ })
}
}
diff --git a/datasketches/src/thetafamily/theta/union.rs
b/datasketches/src/thetafamily/theta/union.rs
index 72805fc..12dff15 100644
--- a/datasketches/src/thetafamily/theta/union.rs
+++ b/datasketches/src/thetafamily/theta/union.rs
@@ -47,18 +47,11 @@ impl ThetaUnion {
/// Returns this union as a compact sketch.
pub fn to_sketch(&self, ordered: bool) -> CompactThetaSketch {
- let parts = self.state.to_compact_parts(ordered);
- CompactThetaSketch::from_parts(
- parts
- .entries
- .into_iter()
- .map(|entry| entry.hash())
- .collect(),
- parts.theta,
- parts.seed_hash,
- parts.ordered,
- parts.empty,
- )
+ let compact_state = self
+ .state
+ .to_compact_sketch_state(ordered)
+ .map_retained_entries(|entry| entry.hash());
+ CompactThetaSketch::from_compact_state(compact_state)
}
/// Resets the union to its empty state.
diff --git a/datasketches/src/thetafamily/tuple/a_not_b.rs
b/datasketches/src/thetafamily/tuple/a_not_b.rs
index 55a7cad..58c3568 100644
--- a/datasketches/src/thetafamily/tuple/a_not_b.rs
+++ b/datasketches/src/thetafamily/tuple/a_not_b.rs
@@ -100,13 +100,7 @@ impl TupleANotB {
{
let a = a.into();
let b = b.into();
- let parts = a_not_b::compute(self.seed_hash, a, b, ordered)?;
- Ok(CompactTupleSketch::from_parts(
- parts.entries,
- parts.theta,
- parts.seed_hash,
- parts.ordered,
- parts.empty,
- ))
+ let compact_state = a_not_b::compute(self.seed_hash, a, b, ordered)?;
+ Ok(CompactTupleSketch::from_compact_state(compact_state))
}
}
diff --git a/datasketches/src/thetafamily/tuple/intersection.rs
b/datasketches/src/thetafamily/tuple/intersection.rs
index a38ca05..38dfe9e 100644
--- a/datasketches/src/thetafamily/tuple/intersection.rs
+++ b/datasketches/src/thetafamily/tuple/intersection.rs
@@ -153,16 +153,8 @@ where
where
P::Summary: Clone,
{
- if !self.state.has_result() {
- return None;
- }
- let parts = self.state.to_compact_parts(ordered);
- Some(CompactTupleSketch::from_parts(
- parts.entries,
- parts.theta,
- parts.seed_hash,
- parts.ordered,
- parts.empty,
- ))
+ self.state
+ .to_compact_sketch_state(ordered)
+ .map(CompactTupleSketch::from_compact_state)
}
}
diff --git a/datasketches/src/thetafamily/tuple/sketch.rs
b/datasketches/src/thetafamily/tuple/sketch.rs
index 22e924f..796e17e 100644
--- a/datasketches/src/thetafamily/tuple/sketch.rs
+++ b/datasketches/src/thetafamily/tuple/sketch.rs
@@ -39,7 +39,6 @@ use crate::hash::check_seed_hash;
use crate::hash::compute_seed_hash;
use crate::thetacommon::EntrySketch;
use crate::thetacommon::KeySketch;
-use crate::thetacommon::SketchScalars;
use crate::thetacommon::binomial_bounds;
use crate::thetacommon::constants::DEFAULT_LG_K;
use crate::thetacommon::constants::FLAGS_IS_COMPACT;
@@ -48,6 +47,8 @@ use crate::thetacommon::constants::FLAGS_IS_ORDERED;
use crate::thetacommon::constants::FLAGS_IS_READ_ONLY;
use crate::thetacommon::constants::MAX_THETA;
use crate::thetacommon::hash_table::SketchHashTableIter;
+use crate::thetacommon::sketch_state::CompactSketchState;
+use crate::thetacommon::sketch_state::ThetaFamilySketchMetadata;
use crate::tuple::hash_table::TupleEntry;
use crate::tuple::hash_table::TupleHashTable;
use crate::tuple::policy::SummaryPolicy;
@@ -81,7 +82,10 @@ pub struct TupleSketchView<'a, S>(TupleSketchViewState<'a,
S>);
#[derive(Debug)]
enum TupleSketchViewState<'a, S> {
- Mutable(&'a TupleHashTable<S>),
+ Mutable {
+ table: &'a TupleHashTable<S>,
+ is_empty: bool,
+ },
Compact(&'a CompactTupleSketch<S>),
}
@@ -128,7 +132,7 @@ impl<'a, S> TupleSketchView<'a, S> {
/// Returns the 16-bit seed hash.
pub fn seed_hash(&self) -> u16 {
match self.0 {
- TupleSketchViewState::Mutable(table) => table.seed_hash(),
+ TupleSketchViewState::Mutable { table, .. } => table.seed_hash(),
TupleSketchViewState::Compact(sketch) => sketch.seed_hash(),
}
}
@@ -136,15 +140,21 @@ impl<'a, S> TupleSketchView<'a, S> {
/// Returns theta as a `u64` threshold.
pub fn theta64(&self) -> u64 {
match self.0 {
- TupleSketchViewState::Mutable(table) => table.theta(),
+ TupleSketchViewState::Mutable { table, is_empty } => {
+ if is_empty {
+ MAX_THETA
+ } else {
+ table.retention_theta()
+ }
+ }
TupleSketchViewState::Compact(sketch) => sketch.theta64(),
}
}
- /// Returns whether the viewed sketch has not received any updates.
+ /// Returns `true` if the viewed sketch is empty.
pub fn is_empty(&self) -> bool {
match self.0 {
- TupleSketchViewState::Mutable(table) => table.is_empty(),
+ TupleSketchViewState::Mutable { is_empty, .. } => is_empty,
TupleSketchViewState::Compact(sketch) => sketch.is_empty(),
}
}
@@ -152,7 +162,7 @@ impl<'a, S> TupleSketchView<'a, S> {
/// Returns whether retained entries are ordered by ascending hash.
pub fn is_ordered(&self) -> bool {
match self.0 {
- TupleSketchViewState::Mutable(_) => false,
+ TupleSketchViewState::Mutable { .. } => false,
TupleSketchViewState::Compact(sketch) => sketch.is_ordered(),
}
}
@@ -160,9 +170,11 @@ impl<'a, S> TupleSketchView<'a, S> {
/// Returns an iterator over retained entries.
pub fn iter(self) -> impl Iterator<Item = &'a TupleEntry<S>> + 'a {
match self.0 {
- TupleSketchViewState::Mutable(table) =>
TupleSketchIter::Mutable(table.iter_entries()),
+ TupleSketchViewState::Mutable { table, .. } => {
+ TupleSketchIter::Mutable(table.iter_entries())
+ }
TupleSketchViewState::Compact(sketch) => {
- TupleSketchIter::Compact(sketch.entries.iter())
+
TupleSketchIter::Compact(sketch.compact_state.retained_entries().iter())
}
}
}
@@ -170,20 +182,25 @@ impl<'a, S> TupleSketchView<'a, S> {
/// Returns the number of retained entries.
pub fn num_retained(&self) -> usize {
match self.0 {
- TupleSketchViewState::Mutable(table) => table.num_retained(),
+ TupleSketchViewState::Mutable { table, .. } =>
table.num_retained(),
TupleSketchViewState::Compact(sketch) => sketch.num_retained(),
}
}
}
impl<S> KeySketch for TupleSketchView<'_, S> {
- fn scalars(self) -> SketchScalars {
- SketchScalars {
- seed_hash: self.seed_hash(),
- theta: self.theta64(),
- empty: self.is_empty(),
- ordered: self.is_ordered(),
- num_retained: self.num_retained(),
+ fn metadata(self) -> ThetaFamilySketchMetadata {
+ if self.is_empty() {
+ ThetaFamilySketchMetadata::Empty {
+ seed_hash: self.seed_hash(),
+ }
+ } else {
+ ThetaFamilySketchMetadata::NonEmpty {
+ seed_hash: self.seed_hash(),
+ theta: self.theta64(),
+ ordered: self.is_ordered(),
+ num_retained: self.num_retained(),
+ }
}
}
@@ -208,7 +225,10 @@ where
P: SummaryPolicy,
{
fn from(sketch: &'a TupleSketch<P>) -> Self {
- Self(TupleSketchViewState::Mutable(&sketch.table))
+ Self(TupleSketchViewState::Mutable {
+ table: &sketch.table,
+ is_empty: sketch.is_empty,
+ })
}
}
@@ -243,6 +263,8 @@ where
P: SummaryPolicy,
{
table: TupleHashTable<P::Summary>,
+ // Public emptiness tracks update calls, not retained entries: theta may
screen every update.
+ is_empty: bool,
policy: P,
}
@@ -275,6 +297,7 @@ where
where
P: SummaryUpdatePolicy<U>,
{
+ self.is_empty = false;
let policy = &self.policy;
self.table.try_insert(key, |existing| match existing {
Some(summary) => {
@@ -295,18 +318,25 @@ where
return 0.0;
}
let num_retained = self.table.num_retained() as f64;
- let theta = self.table.theta() as f64 / MAX_THETA as f64;
+ let theta = self.theta64() as f64 / MAX_THETA as f64;
num_retained / theta
}
/// Returns theta as a fraction in `[0.0, 1.0]`.
pub fn theta(&self) -> f64 {
- self.table.theta() as f64 / MAX_THETA as f64
+ self.theta64() as f64 / MAX_THETA as f64
}
/// Returns theta as a `u64`.
+ ///
+ /// An empty sketch reports `MAX_THETA` even when it was built with a
sampling probability
+ /// below `1.0`, matching the other DataSketches implementations.
pub fn theta64(&self) -> u64 {
- self.table.theta()
+ if self.is_empty {
+ MAX_THETA
+ } else {
+ self.table.retention_theta()
+ }
}
/// Returns the 16-bit seed hash.
@@ -316,12 +346,12 @@ where
/// Returns `true` if the sketch is empty.
pub fn is_empty(&self) -> bool {
- self.table.is_empty()
+ self.is_empty
}
/// Returns `true` if the sketch is in estimation mode.
pub fn is_estimation_mode(&self) -> bool {
- self.table.theta() < MAX_THETA
+ !self.is_empty && self.table.retention_theta() < MAX_THETA
}
/// Returns the number of retained entries.
@@ -342,6 +372,7 @@ where
/// Resets the sketch to the empty state.
pub fn reset(&mut self) {
self.table.reset();
+ self.is_empty = true;
}
/// Returns an iterator over retained entries.
@@ -400,14 +431,13 @@ where
/// assert_eq!(compact.num_retained(), 1);
/// ```
pub fn compact(&self, ordered: bool) -> CompactTupleSketch<P::Summary> {
- let parts = self.table.to_compact_parts(ordered);
- CompactTupleSketch::from_parts(
- parts.entries,
- parts.theta,
- parts.seed_hash,
- parts.ordered,
- parts.empty,
- )
+ let compact_state = if self.is_empty() {
+ debug_assert_eq!(self.num_retained(), 0);
+ CompactSketchState::empty(self.seed_hash())
+ } else {
+ self.table.to_non_empty_compact_state(ordered)
+ };
+ CompactTupleSketch::from_compact_state(compact_state)
}
}
@@ -417,28 +447,12 @@ where
/// theta and a 16-bit seed hash. It can be ordered (sorted ascending by hash)
or unordered.
#[derive(Clone, Debug)]
pub struct CompactTupleSketch<S> {
- entries: Vec<TupleEntry<S>>,
- theta: u64,
- seed_hash: u16,
- ordered: bool,
- empty: bool,
+ compact_state: CompactSketchState<TupleEntry<S>>,
}
impl<S> CompactTupleSketch<S> {
- pub(super) fn from_parts(
- entries: Vec<TupleEntry<S>>,
- theta: u64,
- seed_hash: u16,
- ordered: bool,
- empty: bool,
- ) -> Self {
- Self {
- entries,
- theta,
- seed_hash,
- ordered,
- empty,
- }
+ pub(super) fn from_compact_state(compact_state:
CompactSketchState<TupleEntry<S>>) -> Self {
+ Self { compact_state }
}
/// Returns a read-only view accepted by Tuple set operations.
@@ -452,51 +466,55 @@ impl<S> CompactTupleSketch<S> {
return 0.0;
}
let num_retained = self.num_retained() as f64;
- if self.theta == MAX_THETA {
+ if self.theta64() == MAX_THETA {
return num_retained;
}
- let theta = self.theta as f64 / MAX_THETA as f64;
+ let theta = self.theta();
num_retained / theta
}
/// Returns theta as a fraction (0.0 to 1.0).
pub fn theta(&self) -> f64 {
- self.theta as f64 / MAX_THETA as f64
+ self.theta64() as f64 / MAX_THETA as f64
}
/// Returns theta as `u64`.
pub fn theta64(&self) -> u64 {
- self.theta
+ self.compact_state.theta()
}
/// Returns `true` if the sketch is empty.
pub fn is_empty(&self) -> bool {
- self.empty
+ self.compact_state.is_empty()
}
/// Returns `true` if the sketch is in estimation mode.
pub fn is_estimation_mode(&self) -> bool {
- self.theta < MAX_THETA
+ self.compact_state.is_estimation_mode()
}
/// Returns the number of retained entries.
pub fn num_retained(&self) -> usize {
- self.entries.len()
+ self.retained_entries().len()
}
/// Returns `true` if retained entries are ordered (sorted ascending by
hash).
pub fn is_ordered(&self) -> bool {
- self.ordered
+ self.compact_state.is_ordered()
}
/// Returns the 16-bit seed hash.
pub fn seed_hash(&self) -> u16 {
- self.seed_hash
+ self.compact_state.seed_hash()
}
/// Returns an iterator over retained entries.
pub fn iter(&self) -> impl Iterator<Item = &TupleEntry<S>> + '_ {
- self.entries.iter()
+ self.retained_entries().iter()
+ }
+
+ fn retained_entries(&self) -> &[TupleEntry<S>] {
+ self.compact_state.retained_entries()
}
/// Returns the approximate lower error bound given the number of standard
deviations.
@@ -524,13 +542,14 @@ impl<S> CompactTupleSketch<S> {
/// Returns the estimated size of the sketch in bytes.
pub fn estimated_size(&self) -> usize {
- size_of::<Self>() + self.entries.capacity() *
size_of::<TupleEntry<S>>()
+ size_of::<Self>()
+ + self.compact_state.retained_entries_capacity() *
size_of::<TupleEntry<S>>()
}
fn preamble_longs(&self) -> u8 {
if self.is_estimation_mode() {
3
- } else if self.is_empty() || self.entries.len() == 1 {
+ } else if self.is_empty() || self.num_retained() == 1 {
1
} else {
2
@@ -559,9 +578,9 @@ impl<S> CompactTupleSketch<S> {
where
S: TupleSummaryValue,
{
+ let retained_entries = self.retained_entries();
let pre_longs = self.preamble_longs();
- let entries_size: usize = self
- .entries
+ let entries_size: usize = retained_entries
.iter()
.map(|entry| 8 + entry.summary().serialize_size())
.sum();
@@ -581,17 +600,17 @@ impl<S> CompactTupleSketch<S> {
flags |= FLAGS_IS_ORDERED;
}
bytes.write_u8(flags);
- bytes.write_u16_le(self.seed_hash);
+ bytes.write_u16_le(self.seed_hash());
if pre_longs > 1 {
- bytes.write_u32_le(self.entries.len() as u32);
+ bytes.write_u32_le(retained_entries.len() as u32);
bytes.write_u32_le(0); // unused
}
if self.is_estimation_mode() {
- bytes.write_u64_le(self.theta);
+ bytes.write_u64_le(self.theta64());
}
- for entry in &self.entries {
+ for entry in retained_entries {
bytes.write_u64_le(entry.hash());
entry.summary().serialize_value(&mut bytes);
}
@@ -655,13 +674,9 @@ impl<S> CompactTupleSketch<S> {
let ordered = (flags & FLAGS_IS_ORDERED) != 0;
if empty {
- return Ok(Self::from_parts(
- vec![],
- MAX_THETA,
+ return Ok(Self::from_compact_state(CompactSketchState::empty(
seed_hash,
- ordered,
- true,
- ));
+ )));
}
check_seed_hash(
@@ -682,7 +697,13 @@ impl<S> CompactTupleSketch<S> {
.read_u32_le()
.map_err(insufficient_data("<unused_u32>"))?;
if pre_longs > 2 {
- theta =
cursor.read_u64_le().map_err(insufficient_data("theta"))?;
+ let value =
cursor.read_u64_le().map_err(insufficient_data("theta"))?;
+ if !(1..=MAX_THETA).contains(&value) {
+ return Err(Error::deserial(format!(
+ "corrupted: theta must be in [1, {MAX_THETA}], got
{value}"
+ )));
+ }
+ theta = value;
}
n
};
@@ -696,7 +717,7 @@ impl<S> CompactTupleSketch<S> {
cursor.remaining().len()
)));
}
- let mut entries = Vec::with_capacity(num_entries);
+ let mut retained_entries = Vec::with_capacity(num_entries);
for _ in 0..num_entries {
let hash = cursor
.read_u64_le()
@@ -705,10 +726,15 @@ impl<S> CompactTupleSketch<S> {
return Err(Error::deserial("corrupted: invalid retained hash
value"));
}
let summary = S::deserialize_value(&mut cursor)?;
- entries.push(TupleEntry::new(hash, summary));
+ retained_entries.push(TupleEntry::new(hash, summary));
}
- Ok(Self::from_parts(entries, theta, seed_hash, ordered, false))
+ Ok(Self::from_compact_state(CompactSketchState::non_empty(
+ retained_entries,
+ theta,
+ seed_hash,
+ ordered,
+ )))
}
}
@@ -812,6 +838,7 @@ where
self.sampling_probability,
self.seed,
)?,
+ is_empty: true,
policy: self.policy,
})
}
diff --git a/datasketches/src/thetafamily/tuple/union.rs
b/datasketches/src/thetafamily/tuple/union.rs
index dd44405..df7786b 100644
--- a/datasketches/src/thetafamily/tuple/union.rs
+++ b/datasketches/src/thetafamily/tuple/union.rs
@@ -104,14 +104,7 @@ where
where
P::Summary: Clone,
{
- let result = self.state.to_compact_parts(ordered);
- CompactTupleSketch::from_parts(
- result.entries,
- result.theta,
- result.seed_hash,
- result.ordered,
- result.empty,
- )
+
CompactTupleSketch::from_compact_state(self.state.to_compact_sketch_state(ordered))
}
/// Resets the union to its initial empty state.
diff --git a/tests-integration/src/lib.rs b/tests-integration/src/lib.rs
index 2407ef3..bf99dcc 100644
--- a/tests-integration/src/lib.rs
+++ b/tests-integration/src/lib.rs
@@ -19,3 +19,6 @@
/// A seed whose 16-bit seed hash is the reserved zero value.
pub const ZERO_HASH_SEED: u64 = 50_541;
+
+/// The Theta-family theta value that represents exact (non-sampled) mode.
+pub const MAX_THETA: u64 = i64::MAX as u64;
diff --git a/tests-integration/tests/theta_test/a_not_b.rs
b/tests-integration/tests/theta_test/a_not_b.rs
index 9b17c00..ff8e64c 100644
--- a/tests-integration/tests/theta_test/a_not_b.rs
+++ b/tests-integration/tests/theta_test/a_not_b.rs
@@ -29,6 +29,7 @@ use googletest::prelude::anything;
use googletest::prelude::err;
use googletest::prelude::lt;
use googletest::prelude::near;
+use tests_integration::MAX_THETA;
fn sketch_with_range(start: u64, count: u64) -> ThetaSketch {
let mut sketch = ThetaSketchBuilder::default().build().unwrap();
@@ -103,7 +104,10 @@ fn test_seed_mismatch_ignored_for_empty_inputs() {
#[test]
fn test_empty_a_returns_empty() {
- let empty = ThetaSketchBuilder::default().build().unwrap();
+ let empty = ThetaSketchBuilder::default()
+ .sampling_probability(0.5)
+ .build()
+ .unwrap();
let b = sketch_with_range(0, 1000);
let a_not_b = ThetaANotB::default();
@@ -112,6 +116,8 @@ fn test_empty_a_returns_empty() {
assert!(r.is_empty());
assert_eq!(r.num_retained(), 0);
assert_eq!(r.estimate(), 0.0);
+ assert_eq!(r.theta64(), MAX_THETA);
+ assert!(!r.is_estimation_mode());
}
#[test]
diff --git a/tests-integration/tests/theta_test/intersection.rs
b/tests-integration/tests/theta_test/intersection.rs
index dfd8e2c..87f545f 100644
--- a/tests-integration/tests/theta_test/intersection.rs
+++ b/tests-integration/tests/theta_test/intersection.rs
@@ -79,7 +79,8 @@ fn test_update_accepts_compact_sketch() {
let r = i.to_sketch(false).unwrap();
assert_eq!(r.estimate(), 0.0);
- assert!(!r.is_ordered());
+ assert!(r.is_empty());
+ assert!(r.is_ordered());
}
#[test]
diff --git a/tests-integration/tests/theta_test/sketch.rs
b/tests-integration/tests/theta_test/sketch.rs
index 3e86c93..b900ab1 100644
--- a/tests-integration/tests/theta_test/sketch.rs
+++ b/tests-integration/tests/theta_test/sketch.rs
@@ -18,6 +18,7 @@
use datasketches::common::NumStdDev;
use datasketches::error::ErrorKind;
use datasketches::hash::value::canonical_float;
+use datasketches::theta::CompactThetaSketch;
use datasketches::theta::ThetaSketchBuilder;
use googletest::assert_that;
use googletest::prelude::ge;
@@ -25,6 +26,7 @@ use googletest::prelude::gt;
use googletest::prelude::le;
use googletest::prelude::lt;
use googletest::prelude::near;
+use tests_integration::MAX_THETA;
use tests_integration::ZERO_HASH_SEED;
#[test]
@@ -291,25 +293,22 @@ fn test_bounds_all_num_std_devs() {
}
#[test]
-fn test_bounds_empty_estimation_mode() {
- // Create a sketch with sampling probability < 1.0 to force estimation mode
+fn test_bounds_empty_with_sampling() {
let sketch = ThetaSketchBuilder::default()
.lg_k(12)
.sampling_probability(0.1)
.build()
.unwrap();
- // The sketch is empty but theta < 1.0, so it's in estimation mode
- // However, when empty, both bounds should return 0.0 per Java
implementation
assert!(sketch.is_empty());
- assert!(sketch.is_estimation_mode());
+ assert!(!sketch.is_estimation_mode());
assert_eq!(sketch.estimate(), 0.0);
assert_eq!(sketch.lower_bound(NumStdDev::One), 0.0);
assert_eq!(sketch.upper_bound(NumStdDev::One), 0.0);
}
#[test]
-fn test_compact_preserves_logical_non_empty_after_screened_update() {
+fn test_sampling_state_transitions_through_compaction_and_reset() {
let screened_value = (0u64..)
.find(|candidate| {
let mut sketch = ThetaSketchBuilder::default()
@@ -327,13 +326,33 @@ fn
test_compact_preserves_logical_non_empty_after_screened_update() {
.sampling_probability(0.5)
.build()
.unwrap();
+
+ assert!(sketch.is_empty());
+ assert_eq!(sketch.theta64(), MAX_THETA);
+ assert!(!sketch.is_estimation_mode());
+ let empty_compact = sketch.compact(false);
+ assert!(empty_compact.is_empty());
+ assert!(empty_compact.is_ordered());
+ let bytes = empty_compact.serialize();
+ assert_eq!(
+ CompactThetaSketch::deserialize(&bytes).unwrap().serialize(),
+ bytes
+ );
+
sketch.update(screened_value);
assert!(!sketch.is_empty());
assert_eq!(sketch.num_retained(), 0);
+ assert!(sketch.is_estimation_mode());
+ assert_that!(sketch.theta64(), lt(MAX_THETA));
let compact = sketch.compact(false);
assert!(!compact.is_empty());
assert_eq!(compact.num_retained(), 0);
assert_eq!(compact.theta64(), sketch.theta64());
+
+ sketch.reset();
+ assert!(sketch.is_empty());
+ assert_eq!(sketch.theta64(), MAX_THETA);
+ assert!(!sketch.is_estimation_mode());
}
diff --git a/tests-integration/tests/theta_test/union.rs
b/tests-integration/tests/theta_test/union.rs
index 2edac4f..14df5bf 100644
--- a/tests-integration/tests/theta_test/union.rs
+++ b/tests-integration/tests/theta_test/union.rs
@@ -25,6 +25,7 @@ use googletest::prelude::anything;
use googletest::prelude::err;
use googletest::prelude::le;
use googletest::prelude::near;
+use tests_integration::MAX_THETA;
use tests_integration::ZERO_HASH_SEED;
#[test]
@@ -65,17 +66,35 @@ fn assert_estimate_close(sketch: &CompactThetaSketch,
expected: f64, tolerance:
#[test]
fn test_empty_union() {
- let sketch = ThetaSketchBuilder::default().build().unwrap();
- let mut union = ThetaUnionBuilder::default().build().unwrap();
+ let sketch = ThetaSketchBuilder::default()
+ .sampling_probability(0.5)
+ .build()
+ .unwrap();
+ let mut union = ThetaUnionBuilder::default()
+ .sampling_probability(0.5)
+ .build()
+ .unwrap();
let result = union.to_sketch(true);
assert_eq!(result.num_retained(), 0);
assert!(result.is_empty());
+ assert!(result.is_ordered());
+ assert_eq!(result.theta64(), MAX_THETA);
assert!(!result.is_estimation_mode());
union.update(&sketch).unwrap();
let result = union.to_sketch(true);
assert_eq!(result.num_retained(), 0);
assert!(result.is_empty());
+ assert_eq!(result.theta64(), MAX_THETA);
+
+ let mut input = ThetaSketchBuilder::default().build().unwrap();
+ input.update(1u64);
+ union.update(&input).unwrap();
+ union.reset();
+ let result = union.to_sketch(false);
+ assert!(result.is_empty());
+ assert!(result.is_ordered());
+ assert_eq!(result.theta64(), MAX_THETA);
assert!(!result.is_estimation_mode());
}
@@ -715,9 +734,9 @@ fn test_corner_case_union_states() {
#[test]
fn test_union_estimated_size() {
let mut union = ThetaUnionBuilder::default().build().unwrap();
- assert_eq!(union.estimated_size(), 1096);
+ assert_eq!(union.estimated_size(), 1104);
let sketch = sketch_with_range(12, 0, 1000);
union.update(&sketch).unwrap();
- assert_eq!(union.estimated_size(), 65608);
+ assert_eq!(union.estimated_size(), 65616);
}
diff --git a/tests-integration/tests/tuple_test/a_not_b.rs
b/tests-integration/tests/tuple_test/a_not_b.rs
index 397d957..60bd308 100644
--- a/tests-integration/tests/tuple_test/a_not_b.rs
+++ b/tests-integration/tests/tuple_test/a_not_b.rs
@@ -23,6 +23,7 @@ use googletest::assert_that;
use googletest::prelude::all;
use googletest::prelude::ge;
use googletest::prelude::le;
+use tests_integration::MAX_THETA;
use crate::default_tuple_sketch_builder;
use crate::tuple_sketch_with_range;
@@ -108,15 +109,18 @@ fn input_and_result_ordering_preserve_entries() {
#[test]
fn empty_inputs_do_not_impose_a_seed() {
- let empty_other_seed =
default_tuple_sketch_builder().seed(2).build().unwrap();
+ let empty_other_seed = default_tuple_sketch_builder()
+ .sampling_probability(0.5)
+ .seed(2)
+ .build()
+ .unwrap();
let non_empty = tuple_sketch_with_range(0, 10);
let op = TupleANotB::default();
- assert!(
- op.compute(&empty_other_seed, &non_empty, true)
- .unwrap()
- .is_empty()
- );
+ let result = op.compute(&empty_other_seed, &non_empty, true).unwrap();
+ assert!(result.is_empty());
+ assert_eq!(result.theta64(), MAX_THETA);
+ assert!(!result.is_estimation_mode());
assert_eq!(
op.compute(&non_empty, &empty_other_seed, true)
.unwrap()
diff --git a/tests-integration/tests/tuple_test/sketch.rs
b/tests-integration/tests/tuple_test/sketch.rs
index a4d858f..194060d 100644
--- a/tests-integration/tests/tuple_test/sketch.rs
+++ b/tests-integration/tests/tuple_test/sketch.rs
@@ -29,6 +29,7 @@ use googletest::assert_that;
use googletest::prelude::gt;
use googletest::prelude::le;
use googletest::prelude::lt;
+use tests_integration::MAX_THETA;
use tests_integration::ZERO_HASH_SEED;
use crate::default_tuple_sketch_builder;
@@ -183,7 +184,7 @@ fn empty_sampled_sketch_has_zero_bounds() {
.unwrap();
assert!(sketch.is_empty());
- assert!(sketch.is_estimation_mode());
+ assert!(!sketch.is_estimation_mode());
assert_eq!(sketch.estimate(), 0.0);
assert_eq!(sketch.lower_bound(NumStdDev::Three), 0.0);
assert_eq!(sketch.upper_bound(NumStdDev::Three), 0.0);
@@ -231,7 +232,7 @@ fn compact_preserves_state_in_exact_and_estimation_modes() {
}
#[test]
-fn compact_preserves_logical_non_empty_after_screened_update() {
+fn sampling_state_transitions_through_compaction_and_reset() {
let screened_value = (0u64..)
.find(|candidate| {
let mut sketch = default_tuple_sketch_builder()
@@ -247,10 +248,36 @@ fn
compact_preserves_logical_non_empty_after_screened_update() {
.sampling_probability(0.5)
.build()
.unwrap();
+
+ assert!(sketch.is_empty());
+ assert_eq!(sketch.theta64(), MAX_THETA);
+ assert!(!sketch.is_estimation_mode());
+ let empty_compact = sketch.compact(false);
+ assert!(empty_compact.is_empty());
+ assert!(empty_compact.is_ordered());
+ let bytes = empty_compact.serialize();
+ assert_eq!(
+ CompactTupleSketch::<u64>::deserialize(&bytes)
+ .unwrap()
+ .serialize(),
+ bytes
+ );
+
sketch.update(screened_value, 1u64);
+
+ assert!(!sketch.is_empty());
+ assert_eq!(sketch.num_retained(), 0);
+ assert!(sketch.is_estimation_mode());
+ assert_that!(sketch.theta64(), lt(MAX_THETA));
+
let compact = sketch.compact(false);
assert!(!compact.is_empty());
assert_eq!(compact.num_retained(), 0);
assert_eq!(compact.theta64(), sketch.theta64());
+
+ sketch.reset();
+ assert!(sketch.is_empty());
+ assert_eq!(sketch.theta64(), MAX_THETA);
+ assert!(!sketch.is_estimation_mode());
}
diff --git a/tests-integration/tests/tuple_test/union.rs
b/tests-integration/tests/tuple_test/union.rs
index c29b542..90b68b0 100644
--- a/tests-integration/tests/tuple_test/union.rs
+++ b/tests-integration/tests/tuple_test/union.rs
@@ -25,6 +25,7 @@ use googletest::assert_that;
use googletest::prelude::all;
use googletest::prelude::ge;
use googletest::prelude::le;
+use tests_integration::MAX_THETA;
use tests_integration::ZERO_HASH_SEED;
use crate::default_tuple_sketch_builder;
@@ -87,15 +88,25 @@ fn accepts_mutable_and_compact_inputs() {
#[test]
fn reset_restores_the_initial_empty_state() {
let input = tuple_sketch_with_range(0, 100);
- let mut union = default_union_builder().build().unwrap();
+ let mut union = default_union_builder()
+ .sampling_probability(0.5)
+ .build()
+ .unwrap();
- assert!(union.to_sketch(true).is_empty());
+ let initial = union.to_sketch(false);
+ assert!(initial.is_empty());
+ assert!(initial.is_ordered());
+ assert_eq!(initial.theta64(), MAX_THETA);
+ assert!(!initial.is_estimation_mode());
union.update(&input).unwrap();
assert!(!union.to_sketch(true).is_empty());
union.reset();
- let result = union.to_sketch(true);
+ let result = union.to_sketch(false);
assert!(result.is_empty());
+ assert!(result.is_ordered());
+ assert_eq!(result.theta64(), MAX_THETA);
+ assert!(!result.is_estimation_mode());
assert_eq!(result.estimate(), 0.0);
}
@@ -175,9 +186,9 @@ fn estimation_bounds_cover_the_true_union() {
#[test]
fn union_estimated_size_grows_with_updates() {
let mut union = default_union_builder().build().unwrap();
- assert_eq!(union.estimated_size(), 2120);
+ assert_eq!(union.estimated_size(), 2128);
let sketch = tuple_sketch_with_range(0, 1000);
union.update(&sketch).unwrap();
- assert_eq!(union.estimated_size(), 131144);
+ assert_eq!(union.estimated_size(), 131152);
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]