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.
@@ -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(|&register| 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 &register 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
+ }