probabilistic_machines 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +58 -0
- data/Cargo.lock +294 -0
- data/Cargo.toml +24 -0
- data/LICENSE.txt +21 -0
- data/README.md +203 -0
- data/ext/probabilistic_machines_native/core/Cargo.toml +24 -0
- data/ext/probabilistic_machines_native/core/src/countmin.rs +240 -0
- data/ext/probabilistic_machines_native/core/src/cuckoo.rs +283 -0
- data/ext/probabilistic_machines_native/core/src/hash.rs +88 -0
- data/ext/probabilistic_machines_native/core/src/hyperloglog.rs +254 -0
- data/ext/probabilistic_machines_native/core/src/lib.rs +65 -0
- data/ext/probabilistic_machines_native/core/src/tdigest.rs +445 -0
- data/ext/probabilistic_machines_native/core/src/topk.rs +325 -0
- data/ext/probabilistic_machines_native/extconf.rb +3 -0
- data/ext/probabilistic_machines_native/ffi/Cargo.toml +20 -0
- data/ext/probabilistic_machines_native/ffi/extconf.rb +21 -0
- data/ext/probabilistic_machines_native/ffi/src/countmin.rs +188 -0
- data/ext/probabilistic_machines_native/ffi/src/cuckoo.rs +149 -0
- data/ext/probabilistic_machines_native/ffi/src/data.rs +69 -0
- data/ext/probabilistic_machines_native/ffi/src/hyperloglog.rs +155 -0
- data/ext/probabilistic_machines_native/ffi/src/lib.rs +140 -0
- data/ext/probabilistic_machines_native/ffi/src/tdigest.rs +166 -0
- data/ext/probabilistic_machines_native/ffi/src/topk.rs +299 -0
- data/lib/probabilistic_machines/version.rb +5 -0
- data/lib/probabilistic_machines.rb +7 -0
- data/sig/probabilistic_machines.rbs +182 -0
- metadata +90 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
//! Distinct-count estimation in a fixed array of registers.
|
|
2
|
+
|
|
3
|
+
use crate::hash::{hash64, u32_low_u8, u64_low_u16};
|
|
4
|
+
use crate::{CodecError, TAG_HYPERLOGLOG};
|
|
5
|
+
|
|
6
|
+
/// Odd terms of `2 * atanh(t) = ln((1 + t) / (1 - t))`, in Horner order.
|
|
7
|
+
const ATANH_COEFFICIENTS: [f64; 5] = [2.0, 2.0 / 3.0, 0.4, 2.0 / 7.0, 2.0 / 9.0];
|
|
8
|
+
|
|
9
|
+
/// Bits of hash a sketch splits into a register index and a leading-zero count.
|
|
10
|
+
const HASH_BITS: u32 = 64;
|
|
11
|
+
|
|
12
|
+
const POW2_NEG: [f64; 65] = {
|
|
13
|
+
let mut table = [1.0; 65];
|
|
14
|
+
let mut exponent = 1;
|
|
15
|
+
while exponent < 65 {
|
|
16
|
+
table[exponent] = table[exponent - 1] * 0.5;
|
|
17
|
+
exponent += 1;
|
|
18
|
+
}
|
|
19
|
+
table
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/// Counts distinct items in `M` bytes of state, however many items it sees.
|
|
23
|
+
///
|
|
24
|
+
/// `M` is the register count: a power of two between 16 and 65536. The relative
|
|
25
|
+
/// standard error of [`estimate`](Self::estimate) is `1.04 / sqrt(M)`, so 1024
|
|
26
|
+
/// registers give 3.25%, 4096 give 1.63%, 16384 give 0.81%. A register holds a
|
|
27
|
+
/// leading-zero count, which fits in a byte, so the sketch is `M` bytes on the
|
|
28
|
+
/// stack and never touches a heap.
|
|
29
|
+
///
|
|
30
|
+
/// ```
|
|
31
|
+
/// use probabilistic_machines::HyperLogLog;
|
|
32
|
+
///
|
|
33
|
+
/// let mut seen = HyperLogLog::<4096>::new();
|
|
34
|
+
/// for id in 0_u32..100_000 {
|
|
35
|
+
/// seen.add(&id.to_le_bytes());
|
|
36
|
+
/// }
|
|
37
|
+
///
|
|
38
|
+
/// let error = (seen.estimate() - 100_000.0).abs() / 100_000.0;
|
|
39
|
+
/// assert!(error < 0.05, "{error}");
|
|
40
|
+
/// ```
|
|
41
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
42
|
+
pub struct HyperLogLog<const M: usize> {
|
|
43
|
+
registers: [u8; M],
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
impl<const M: usize> HyperLogLog<M> {
|
|
47
|
+
/// Index bits taken from the top of each hash.
|
|
48
|
+
pub const PRECISION: u32 = {
|
|
49
|
+
assert!(M.is_power_of_two(), "register count must be a power of two");
|
|
50
|
+
assert!(M >= 16, "register count must be at least 16");
|
|
51
|
+
assert!(M <= 1 << 16, "register count must be at most 65536");
|
|
52
|
+
M.trailing_zeros()
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/// Highest rank a register can hold: the index bits leave
|
|
56
|
+
/// `HASH_BITS - PRECISION` bits of hash, and a rank counts their leading
|
|
57
|
+
/// zeros plus one.
|
|
58
|
+
const MAX_RANK: u8 = u32_low_u8(HASH_BITS - Self::PRECISION + 1);
|
|
59
|
+
|
|
60
|
+
/// Length of the [`encode`](Self::encode) frame.
|
|
61
|
+
pub const ENCODED_LEN: usize = M + 2;
|
|
62
|
+
|
|
63
|
+
#[must_use]
|
|
64
|
+
pub const fn new() -> Self {
|
|
65
|
+
Self { registers: [0; M] }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// Relative standard error of the estimate, `1.04 / sqrt(M)`.
|
|
69
|
+
#[cfg(feature = "std")]
|
|
70
|
+
#[must_use]
|
|
71
|
+
pub fn standard_error() -> f64 {
|
|
72
|
+
1.04 / f64::from(1_u32 << Self::PRECISION).sqrt()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
pub fn add(&mut self, item: &[u8]) {
|
|
76
|
+
self.add_hash(hash64(item));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// Records an item the caller already hashed, for keys that never existed
|
|
80
|
+
/// as bytes.
|
|
81
|
+
pub fn add_hash(&mut self, hash: u64) {
|
|
82
|
+
let index = usize::from(u64_low_u16(hash >> (HASH_BITS - Self::PRECISION)));
|
|
83
|
+
let rank = rank(hash, Self::PRECISION);
|
|
84
|
+
if self.registers[index] < rank {
|
|
85
|
+
self.registers[index] = rank;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#[must_use]
|
|
90
|
+
pub fn is_empty(&self) -> bool {
|
|
91
|
+
self.registers.iter().all(|®ister| register == 0)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// Estimated number of distinct items.
|
|
95
|
+
///
|
|
96
|
+
/// Under `2.5 * M` the register histogram is sparse enough that linear
|
|
97
|
+
/// counting over the untouched registers beats the harmonic-mean
|
|
98
|
+
/// estimator, so this switches to it.
|
|
99
|
+
#[must_use]
|
|
100
|
+
pub fn estimate(&self) -> f64 {
|
|
101
|
+
let mut histogram = [0_u32; 65];
|
|
102
|
+
for ®ister in &self.registers {
|
|
103
|
+
histogram[usize::from(register)] += 1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let mut registers = 0.0;
|
|
107
|
+
let mut harmonic = 0.0;
|
|
108
|
+
for (zeros, &count) in histogram.iter().enumerate() {
|
|
109
|
+
let bucket = f64::from(count);
|
|
110
|
+
let weighted = bucket * POW2_NEG[zeros];
|
|
111
|
+
registers += bucket;
|
|
112
|
+
harmonic += weighted;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let alpha = match Self::PRECISION {
|
|
116
|
+
4 => 0.673,
|
|
117
|
+
5 => 0.697,
|
|
118
|
+
6 => 0.709,
|
|
119
|
+
_ => 0.7213 / (1.0 + 1.079 / registers),
|
|
120
|
+
};
|
|
121
|
+
let raw = alpha * registers * registers / harmonic;
|
|
122
|
+
let untouched = f64::from(histogram[0]);
|
|
123
|
+
|
|
124
|
+
if untouched > 0.0 && raw <= 2.5 * registers {
|
|
125
|
+
registers * ln(registers / untouched)
|
|
126
|
+
} else {
|
|
127
|
+
raw
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// Folds another sketch in, as if both had seen every item. Exact: the
|
|
132
|
+
/// result equals a sketch built from the union of the two streams.
|
|
133
|
+
pub fn merge(&mut self, other: &Self) {
|
|
134
|
+
for (mine, theirs) in self.registers.iter_mut().zip(&other.registers) {
|
|
135
|
+
if *theirs > *mine {
|
|
136
|
+
*mine = *theirs;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/// Writes the sketch into `out`, returning the number of bytes written.
|
|
142
|
+
///
|
|
143
|
+
/// # Errors
|
|
144
|
+
///
|
|
145
|
+
/// [`CodecError::Truncated`] when `out` is shorter than
|
|
146
|
+
/// [`ENCODED_LEN`](Self::ENCODED_LEN).
|
|
147
|
+
pub fn encode(&self, out: &mut [u8]) -> Result<usize, CodecError> {
|
|
148
|
+
let Some(frame) = out.get_mut(..Self::ENCODED_LEN) else {
|
|
149
|
+
return Err(CodecError::Truncated);
|
|
150
|
+
};
|
|
151
|
+
self.write_frame(frame);
|
|
152
|
+
Ok(Self::ENCODED_LEN)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// The sketch's frame, exactly [`ENCODED_LEN`](Self::ENCODED_LEN) bytes.
|
|
156
|
+
#[cfg(feature = "alloc")]
|
|
157
|
+
#[must_use]
|
|
158
|
+
pub fn encoded(&self) -> alloc::boxed::Box<[u8]> {
|
|
159
|
+
let mut frame = alloc::vec![0_u8; Self::ENCODED_LEN].into_boxed_slice();
|
|
160
|
+
self.write_frame(&mut frame);
|
|
161
|
+
frame
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/// Fills a frame of exactly [`ENCODED_LEN`](Self::ENCODED_LEN) bytes.
|
|
165
|
+
fn write_frame(&self, frame: &mut [u8]) {
|
|
166
|
+
frame[0] = TAG_HYPERLOGLOG;
|
|
167
|
+
frame[1] = u32_low_u8(Self::PRECISION);
|
|
168
|
+
frame[2..].copy_from_slice(&self.registers);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/// Reads a sketch written by [`encode`](Self::encode).
|
|
172
|
+
///
|
|
173
|
+
/// # Errors
|
|
174
|
+
///
|
|
175
|
+
/// [`CodecError::Truncated`] when `bytes` is too short, [`CodecError::Tag`]
|
|
176
|
+
/// when it holds another structure, [`CodecError::Parameters`] when it was
|
|
177
|
+
/// written with a different register count or holds a rank the hash tail
|
|
178
|
+
/// its precision leaves cannot reach.
|
|
179
|
+
pub fn decode(bytes: &[u8]) -> Result<Self, CodecError> {
|
|
180
|
+
let mut sketch = Self::new();
|
|
181
|
+
sketch.decode_into(bytes)?;
|
|
182
|
+
Ok(sketch)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/// Reads a frame over this sketch, for a caller that owns one already and
|
|
186
|
+
/// has no room on its stack for a second.
|
|
187
|
+
///
|
|
188
|
+
/// The sketch is untouched when the frame is rejected.
|
|
189
|
+
///
|
|
190
|
+
/// # Errors
|
|
191
|
+
///
|
|
192
|
+
/// The same as [`decode`](Self::decode).
|
|
193
|
+
pub fn decode_into(&mut self, bytes: &[u8]) -> Result<(), CodecError> {
|
|
194
|
+
let Some(frame) = bytes.get(..Self::ENCODED_LEN) else {
|
|
195
|
+
return Err(CodecError::Truncated);
|
|
196
|
+
};
|
|
197
|
+
if frame[0] != TAG_HYPERLOGLOG {
|
|
198
|
+
return Err(CodecError::Tag);
|
|
199
|
+
}
|
|
200
|
+
if frame[1] != u32_low_u8(Self::PRECISION) {
|
|
201
|
+
return Err(CodecError::Parameters);
|
|
202
|
+
}
|
|
203
|
+
let mut index = 0;
|
|
204
|
+
while index < M {
|
|
205
|
+
if frame[2 + index] > Self::MAX_RANK {
|
|
206
|
+
return Err(CodecError::Parameters);
|
|
207
|
+
}
|
|
208
|
+
index += 1;
|
|
209
|
+
}
|
|
210
|
+
self.registers.copy_from_slice(&frame[2..]);
|
|
211
|
+
Ok(())
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
impl<const M: usize> Default for HyperLogLog<M> {
|
|
216
|
+
fn default() -> Self {
|
|
217
|
+
Self::new()
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/// One plus the number of leading zeros left in a hash once the index bits are
|
|
222
|
+
/// shifted off, capped at `64 - precision + 1`.
|
|
223
|
+
const fn rank(hash: u64, precision: u32) -> u8 {
|
|
224
|
+
let tail = (hash << precision) | (1 << (precision - 1));
|
|
225
|
+
u32_low_u8(tail.leading_zeros() + 1)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/// Natural logarithm of a positive finite `x`, accurate to a few parts in
|
|
229
|
+
/// `10^10`. `f64::ln` lives in `std`, and the linear-counting branch needs this
|
|
230
|
+
/// on bare metal.
|
|
231
|
+
fn ln(x: f64) -> f64 {
|
|
232
|
+
let bits = x.to_bits();
|
|
233
|
+
let mut mantissa = f64::from_bits((bits & 0x000F_FFFF_FFFF_FFFF) | 0x3FF0_0000_0000_0000);
|
|
234
|
+
let mut exponent = f64::from(u64_low_u16((bits >> 52) & 0x7FF)) - 1023.0;
|
|
235
|
+
if mantissa > core::f64::consts::SQRT_2 {
|
|
236
|
+
mantissa *= 0.5;
|
|
237
|
+
exponent += 1.0;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
let t = (mantissa - 1.0) / (mantissa + 1.0);
|
|
241
|
+
let square = t * t;
|
|
242
|
+
let mut series = 0.0;
|
|
243
|
+
for coefficient in ATANH_COEFFICIENTS.iter().rev() {
|
|
244
|
+
let higher = series * square;
|
|
245
|
+
series = coefficient + higher;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let scaled = exponent * core::f64::consts::LN_2;
|
|
249
|
+
let odd = series * t;
|
|
250
|
+
scaled + odd
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
#[cfg(test)]
|
|
254
|
+
mod tests;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#![cfg_attr(not(feature = "std"), no_std)]
|
|
2
|
+
|
|
3
|
+
#[cfg(feature = "alloc")]
|
|
4
|
+
extern crate alloc;
|
|
5
|
+
|
|
6
|
+
/// Compiles and runs the README's examples as doctests.
|
|
7
|
+
#[cfg(doctest)]
|
|
8
|
+
#[doc = include_str!("../README.md")]
|
|
9
|
+
struct ReadmeDoctests;
|
|
10
|
+
|
|
11
|
+
pub mod countmin;
|
|
12
|
+
pub mod cuckoo;
|
|
13
|
+
pub mod hash;
|
|
14
|
+
pub mod hyperloglog;
|
|
15
|
+
pub mod topk;
|
|
16
|
+
|
|
17
|
+
#[cfg(feature = "alloc")]
|
|
18
|
+
pub mod tdigest;
|
|
19
|
+
|
|
20
|
+
pub use countmin::CountMinSketch;
|
|
21
|
+
pub use cuckoo::CuckooFilter;
|
|
22
|
+
pub use hash::{hash64, mix64};
|
|
23
|
+
pub use hyperloglog::HyperLogLog;
|
|
24
|
+
pub use topk::HeavyHitters;
|
|
25
|
+
|
|
26
|
+
#[cfg(feature = "alloc")]
|
|
27
|
+
pub use tdigest::TDigest;
|
|
28
|
+
|
|
29
|
+
/// Why a sketch could not be read from or written to a byte buffer.
|
|
30
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
31
|
+
pub enum CodecError {
|
|
32
|
+
/// The buffer is shorter than the encoded form needs.
|
|
33
|
+
Truncated,
|
|
34
|
+
/// The leading byte names a different structure.
|
|
35
|
+
Tag,
|
|
36
|
+
/// The encoded shape does not match the target type's.
|
|
37
|
+
Parameters,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
impl core::fmt::Display for CodecError {
|
|
41
|
+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
|
42
|
+
f.write_str(match self {
|
|
43
|
+
Self::Truncated => "buffer too short",
|
|
44
|
+
Self::Tag => "wrong structure tag",
|
|
45
|
+
Self::Parameters => "shape mismatch",
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
impl core::error::Error for CodecError {}
|
|
51
|
+
|
|
52
|
+
pub(crate) const TAG_HYPERLOGLOG: u8 = 0xA1;
|
|
53
|
+
pub(crate) const TAG_TOPK: u8 = 0xB1;
|
|
54
|
+
pub(crate) const TAG_COUNTMIN: u8 = 0xC1;
|
|
55
|
+
#[cfg(feature = "alloc")]
|
|
56
|
+
pub(crate) const TAG_TDIGEST: u8 = 0xD1;
|
|
57
|
+
pub(crate) const TAG_CUCKOO: u8 = 0xF1;
|
|
58
|
+
|
|
59
|
+
/// The first eight bytes of a field, for the little-endian scalars the frames
|
|
60
|
+
/// are built from.
|
|
61
|
+
pub(crate) const fn read8(bytes: &[u8]) -> [u8; 8] {
|
|
62
|
+
let mut buffer = [0_u8; 8];
|
|
63
|
+
buffer.copy_from_slice(bytes);
|
|
64
|
+
buffer
|
|
65
|
+
}
|