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,149 @@
1
+ //! `ProbabilisticMachines::CuckooFilter`: membership that can forget.
2
+
3
+ use std::cell::RefCell;
4
+
5
+ use bytes::Bytes;
6
+ use magnus::scan_args::scan_args;
7
+ use magnus::{Error, RHash, RModule, RString, Ruby, Value, function, method, prelude::*};
8
+ use probabilistic_machines::{CodecError, CuckooFilter};
9
+
10
+ use crate::{Whole, codec_error, full, required_kwarg, unsupported, unsupported_blob};
11
+
12
+ /// Bucket counts [`Filter`] is monomorphised for.
13
+ const BUCKETS: [usize; 4] = [1024, 4096, 16384, 65536];
14
+
15
+ enum Filter {
16
+ B1024(Box<CuckooFilter<1024>>),
17
+ B4096(Box<CuckooFilter<4096>>),
18
+ B16384(Box<CuckooFilter<16384>>),
19
+ B65536(Box<CuckooFilter<65536>>),
20
+ }
21
+
22
+ /// Runs `body` against whichever variant is held.
23
+ macro_rules! filter {
24
+ ($filter:expr, |$inner:ident| $body:expr) => {
25
+ match $filter {
26
+ Filter::B1024($inner) => $body,
27
+ Filter::B4096($inner) => $body,
28
+ Filter::B16384($inner) => $body,
29
+ Filter::B65536($inner) => $body,
30
+ }
31
+ };
32
+ }
33
+
34
+ #[magnus::wrap(class = "ProbabilisticMachines::CuckooFilter", free_immediately, size)]
35
+ struct Wrapper(RefCell<Filter>);
36
+
37
+ const fn buckets_of<const B: usize>(_: &CuckooFilter<B>) -> usize {
38
+ B
39
+ }
40
+
41
+ const fn capacity_of<const B: usize>(_: &CuckooFilter<B>) -> usize {
42
+ CuckooFilter::<B>::CAPACITY
43
+ }
44
+
45
+ /// Each shape is constructed behind its own call frame. Inlined, a debug build
46
+ /// sizes one frame for every arm of the match below at once.
47
+ #[inline(never)]
48
+ fn boxed_new<const N: usize>() -> Box<CuckooFilter<N>> {
49
+ Box::new(CuckooFilter::new())
50
+ }
51
+
52
+ #[inline(never)]
53
+ fn boxed_decode<const N: usize>(ruby: &Ruby, frame: &[u8]) -> Result<Box<CuckooFilter<N>>, Error> {
54
+ let mut filter = boxed_new::<N>();
55
+ filter
56
+ .decode_into(frame)
57
+ .map_err(|error| codec_error(ruby, error))?;
58
+ Ok(filter)
59
+ }
60
+
61
+ impl Wrapper {
62
+ fn new(ruby: &Ruby, args: &[Value]) -> Result<Self, Error> {
63
+ let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
64
+ let Whole(buckets): Whole<usize> = required_kwarg(args.keywords, "buckets")?;
65
+ let filter = match buckets {
66
+ 1024 => Filter::B1024(boxed_new::<1024>()),
67
+ 4096 => Filter::B4096(boxed_new::<4096>()),
68
+ 16384 => Filter::B16384(boxed_new::<16384>()),
69
+ 65536 => Filter::B65536(boxed_new::<65536>()),
70
+ _ => return Err(unsupported(ruby, "buckets", buckets, &BUCKETS)),
71
+ };
72
+ Ok(Self(RefCell::new(filter)))
73
+ }
74
+
75
+ fn load(ruby: &Ruby, blob: Bytes) -> Result<Self, Error> {
76
+ let Some(&exponent) = blob.get(1) else {
77
+ return Err(codec_error(ruby, CodecError::Truncated));
78
+ };
79
+ let filter = match exponent {
80
+ 10 => Filter::B1024(boxed_decode::<1024>(ruby, &blob)?),
81
+ 12 => Filter::B4096(boxed_decode::<4096>(ruby, &blob)?),
82
+ 14 => Filter::B16384(boxed_decode::<16384>(ruby, &blob)?),
83
+ 16 => Filter::B65536(boxed_decode::<65536>(ruby, &blob)?),
84
+ _ => return Err(unsupported_blob(ruby, "bucket count", &BUCKETS)),
85
+ };
86
+ Ok(Self(RefCell::new(filter)))
87
+ }
88
+
89
+ fn buckets(&self) -> usize {
90
+ filter!(&*self.0.borrow(), |filter| buckets_of(&**filter))
91
+ }
92
+
93
+ fn capacity(&self) -> usize {
94
+ filter!(&*self.0.borrow(), |filter| capacity_of(&**filter))
95
+ }
96
+
97
+ fn insert(ruby: &Ruby, rb_self: &Self, item: Bytes) -> Result<bool, Error> {
98
+ filter!(&mut *rb_self.0.borrow_mut(), |filter| filter.insert(&item))
99
+ .map(|()| true)
100
+ .map_err(|_| full(ruby))
101
+ }
102
+
103
+ fn contains(&self, item: Bytes) -> bool {
104
+ filter!(&*self.0.borrow(), |filter| filter.contains(&item))
105
+ }
106
+
107
+ fn remove(&self, item: Bytes) -> bool {
108
+ filter!(&mut *self.0.borrow_mut(), |filter| filter.remove(&item))
109
+ }
110
+
111
+ fn size(&self) -> usize {
112
+ filter!(&*self.0.borrow(), |filter| filter.len())
113
+ }
114
+
115
+ fn is_empty(&self) -> bool {
116
+ filter!(&*self.0.borrow(), |filter| filter.is_empty())
117
+ }
118
+
119
+ fn dump(ruby: &Ruby, rb_self: &Self) -> RString {
120
+ let frame = filter!(&*rb_self.0.borrow(), |filter| filter.encoded());
121
+ ruby.str_from_slice(&frame)
122
+ }
123
+
124
+ fn inspect(&self) -> String {
125
+ format!(
126
+ "#<ProbabilisticMachines::CuckooFilter buckets={} size={} capacity={}>",
127
+ self.buckets(),
128
+ self.size(),
129
+ self.capacity()
130
+ )
131
+ }
132
+ }
133
+
134
+ pub fn define(ruby: &Ruby, module: RModule) -> Result<(), Error> {
135
+ let class = module.define_class("CuckooFilter", ruby.class_object())?;
136
+ class.define_singleton_method("new", function!(Wrapper::new, -1))?;
137
+ class.define_singleton_method("load", function!(Wrapper::load, 1))?;
138
+ class.define_method("buckets", method!(Wrapper::buckets, 0))?;
139
+ class.define_method("capacity", method!(Wrapper::capacity, 0))?;
140
+ class.define_method("insert", method!(Wrapper::insert, 1))?;
141
+ class.define_method("include?", method!(Wrapper::contains, 1))?;
142
+ class.define_method("contains?", method!(Wrapper::contains, 1))?;
143
+ class.define_method("remove", method!(Wrapper::remove, 1))?;
144
+ class.define_method("size", method!(Wrapper::size, 0))?;
145
+ class.define_method("empty?", method!(Wrapper::is_empty, 0))?;
146
+ class.define_method("dump", method!(Wrapper::dump, 0))?;
147
+ class.define_method("inspect", method!(Wrapper::inspect, 0))?;
148
+ Ok(())
149
+ }
@@ -0,0 +1,69 @@
1
+ //! The frozen `Data` objects the sketches hand back.
2
+
3
+ use std::sync::OnceLock;
4
+
5
+ use magnus::value::Opaque;
6
+ use magnus::{Error, RClass, RModule, RStruct, Ruby, Value, prelude::*};
7
+
8
+ use crate::{defined_twice, uninitialised};
9
+
10
+ static HITTER: OnceLock<Opaque<RClass>> = OnceLock::new();
11
+ static CENTROID: OnceLock<Opaque<RClass>> = OnceLock::new();
12
+
13
+ pub fn define(ruby: &Ruby, module: RModule) -> Result<(), Error> {
14
+ let hitter = ruby.define_data(None, ("key", "count", "overestimate"))?;
15
+ module.const_set("Hitter", hitter)?;
16
+ HITTER
17
+ .set(hitter.into())
18
+ .map_err(|_| defined_twice(ruby, "Hitter"))?;
19
+
20
+ let centroid = ruby.define_data(None, ("mean", "weight"))?;
21
+ module.const_set("Centroid", centroid)?;
22
+ CENTROID
23
+ .set(centroid.into())
24
+ .map_err(|_| defined_twice(ruby, "Centroid"))
25
+ }
26
+
27
+ /// A tracked key, its counter, and how much of that counter it inherited from
28
+ /// keys evicted before it.
29
+ pub fn hitter(ruby: &Ruby, key: Value, count: u64, overestimate: u64) -> Result<Value, Error> {
30
+ new_data(
31
+ ruby,
32
+ &HITTER,
33
+ [key, ruby.into_value(count), ruby.into_value(overestimate)],
34
+ )
35
+ }
36
+
37
+ /// One cluster of a digest: the mean of the values that collapsed into it.
38
+ pub fn centroid(ruby: &Ruby, mean: f64, weight: f64) -> Result<Value, Error> {
39
+ new_data(
40
+ ruby,
41
+ &CENTROID,
42
+ [ruby.into_value(mean), ruby.into_value(weight)],
43
+ )
44
+ }
45
+
46
+ /// Instantiate one of our `Data` classes from its member values, in
47
+ /// declaration order.
48
+ ///
49
+ /// `Data#initialize` takes keywords only, so `new_instance` would need a
50
+ /// temporary `Hash` per call plus a temporary `String` per key while magnus
51
+ /// interns it. Allocating, filling the members by position and freezing is
52
+ /// everything `Data#initialize` does, and both classes are defined here with
53
+ /// no `initialize` override to skip.
54
+ fn new_data<const N: usize>(
55
+ ruby: &Ruby,
56
+ slot: &OnceLock<Opaque<RClass>>,
57
+ members: [Value; N],
58
+ ) -> Result<Value, Error> {
59
+ let class = slot
60
+ .get()
61
+ .map(|class| ruby.get_inner(*class))
62
+ .ok_or_else(|| uninitialised(ruby))?;
63
+ let instance = RStruct::try_convert(class.obj_alloc()?)?;
64
+ for (index, value) in members.into_iter().enumerate() {
65
+ instance.aset(index, value)?;
66
+ }
67
+ instance.freeze();
68
+ Ok(instance.as_value())
69
+ }
@@ -0,0 +1,155 @@
1
+ //! `ProbabilisticMachines::HyperLogLog`: how many distinct items a stream held.
2
+
3
+ use std::cell::RefCell;
4
+
5
+ use bytes::Bytes;
6
+ use magnus::scan_args::scan_args;
7
+ use magnus::typed_data::Obj;
8
+ use magnus::{Error, RHash, RModule, RString, Ruby, Value, function, method, prelude::*};
9
+ use probabilistic_machines::{CodecError, HyperLogLog};
10
+
11
+ use crate::{Whole, codec_error, mismatch, required_kwarg, unsupported, unsupported_blob};
12
+
13
+ /// Register counts [`Sketch`] is monomorphised for.
14
+ const REGISTERS: [usize; 4] = [1024, 4096, 16384, 65536];
15
+
16
+ #[derive(Clone)]
17
+ enum Sketch {
18
+ M1024(Box<HyperLogLog<1024>>),
19
+ M4096(Box<HyperLogLog<4096>>),
20
+ M16384(Box<HyperLogLog<16384>>),
21
+ M65536(Box<HyperLogLog<65536>>),
22
+ }
23
+
24
+ /// Runs `body` against whichever variant is held.
25
+ macro_rules! sketch {
26
+ ($sketch:expr, |$inner:ident| $body:expr) => {
27
+ match $sketch {
28
+ Sketch::M1024($inner) => $body,
29
+ Sketch::M4096($inner) => $body,
30
+ Sketch::M16384($inner) => $body,
31
+ Sketch::M65536($inner) => $body,
32
+ }
33
+ };
34
+ }
35
+
36
+ #[magnus::wrap(class = "ProbabilisticMachines::HyperLogLog", free_immediately, size)]
37
+ struct Wrapper(RefCell<Sketch>);
38
+
39
+ const fn registers_of<const M: usize>(_: &HyperLogLog<M>) -> usize {
40
+ M
41
+ }
42
+
43
+ fn standard_error_of<const M: usize>(_: &HyperLogLog<M>) -> f64 {
44
+ HyperLogLog::<M>::standard_error()
45
+ }
46
+
47
+ /// Each shape is constructed behind its own call frame. Inlined, a debug build
48
+ /// sizes one frame for every arm of the match below at once.
49
+ #[inline(never)]
50
+ fn boxed_new<const N: usize>() -> Box<HyperLogLog<N>> {
51
+ Box::new(HyperLogLog::new())
52
+ }
53
+
54
+ #[inline(never)]
55
+ fn boxed_decode<const N: usize>(ruby: &Ruby, frame: &[u8]) -> Result<Box<HyperLogLog<N>>, Error> {
56
+ let mut sketch = boxed_new::<N>();
57
+ sketch
58
+ .decode_into(frame)
59
+ .map_err(|error| codec_error(ruby, error))?;
60
+ Ok(sketch)
61
+ }
62
+
63
+ impl Wrapper {
64
+ fn new(ruby: &Ruby, args: &[Value]) -> Result<Self, Error> {
65
+ let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
66
+ let Whole(registers): Whole<usize> = required_kwarg(args.keywords, "registers")?;
67
+ let sketch = match registers {
68
+ 1024 => Sketch::M1024(boxed_new::<1024>()),
69
+ 4096 => Sketch::M4096(boxed_new::<4096>()),
70
+ 16384 => Sketch::M16384(boxed_new::<16384>()),
71
+ 65536 => Sketch::M65536(boxed_new::<65536>()),
72
+ _ => return Err(unsupported(ruby, "registers", registers, &REGISTERS)),
73
+ };
74
+ Ok(Self(RefCell::new(sketch)))
75
+ }
76
+
77
+ fn load(ruby: &Ruby, blob: Bytes) -> Result<Self, Error> {
78
+ let Some(&precision) = blob.get(1) else {
79
+ return Err(codec_error(ruby, CodecError::Truncated));
80
+ };
81
+ let sketch = match precision {
82
+ 10 => Sketch::M1024(boxed_decode::<1024>(ruby, &blob)?),
83
+ 12 => Sketch::M4096(boxed_decode::<4096>(ruby, &blob)?),
84
+ 14 => Sketch::M16384(boxed_decode::<16384>(ruby, &blob)?),
85
+ 16 => Sketch::M65536(boxed_decode::<65536>(ruby, &blob)?),
86
+ _ => return Err(unsupported_blob(ruby, "register count", &REGISTERS)),
87
+ };
88
+ Ok(Self(RefCell::new(sketch)))
89
+ }
90
+
91
+ fn registers(&self) -> usize {
92
+ sketch!(&*self.0.borrow(), |sketch| registers_of(&**sketch))
93
+ }
94
+
95
+ fn add(&self, item: Bytes) {
96
+ sketch!(&mut *self.0.borrow_mut(), |sketch| sketch.add(&item));
97
+ }
98
+
99
+ fn add_hash(&self, Whole(hash): Whole<u64>) {
100
+ sketch!(&mut *self.0.borrow_mut(), |sketch| sketch.add_hash(hash));
101
+ }
102
+
103
+ fn estimate(&self) -> f64 {
104
+ sketch!(&*self.0.borrow(), |sketch| sketch.estimate())
105
+ }
106
+
107
+ fn standard_error(&self) -> f64 {
108
+ sketch!(&*self.0.borrow(), |sketch| standard_error_of(&**sketch))
109
+ }
110
+
111
+ fn is_empty(&self) -> bool {
112
+ sketch!(&*self.0.borrow(), |sketch| sketch.is_empty())
113
+ }
114
+
115
+ fn merge(ruby: &Ruby, rb_self: Obj<Self>, other: &Self) -> Result<Obj<Self>, Error> {
116
+ let source = other.0.borrow().clone();
117
+ match (&mut *rb_self.0.borrow_mut(), &source) {
118
+ (Sketch::M1024(target), Sketch::M1024(source)) => target.merge(source),
119
+ (Sketch::M4096(target), Sketch::M4096(source)) => target.merge(source),
120
+ (Sketch::M16384(target), Sketch::M16384(source)) => target.merge(source),
121
+ (Sketch::M65536(target), Sketch::M65536(source)) => target.merge(source),
122
+ _ => return Err(mismatch(ruby, "sketch")),
123
+ }
124
+ Ok(rb_self)
125
+ }
126
+
127
+ fn dump(ruby: &Ruby, rb_self: &Self) -> RString {
128
+ let frame = sketch!(&*rb_self.0.borrow(), |sketch| sketch.encoded());
129
+ ruby.str_from_slice(&frame)
130
+ }
131
+
132
+ fn inspect(&self) -> String {
133
+ format!(
134
+ "#<ProbabilisticMachines::HyperLogLog registers={} estimate={:.1}>",
135
+ self.registers(),
136
+ self.estimate()
137
+ )
138
+ }
139
+ }
140
+
141
+ pub fn define(ruby: &Ruby, module: RModule) -> Result<(), Error> {
142
+ let class = module.define_class("HyperLogLog", ruby.class_object())?;
143
+ class.define_singleton_method("new", function!(Wrapper::new, -1))?;
144
+ class.define_singleton_method("load", function!(Wrapper::load, 1))?;
145
+ class.define_method("registers", method!(Wrapper::registers, 0))?;
146
+ class.define_method("add", method!(Wrapper::add, 1))?;
147
+ class.define_method("add_hash", method!(Wrapper::add_hash, 1))?;
148
+ class.define_method("estimate", method!(Wrapper::estimate, 0))?;
149
+ class.define_method("standard_error", method!(Wrapper::standard_error, 0))?;
150
+ class.define_method("empty?", method!(Wrapper::is_empty, 0))?;
151
+ class.define_method("merge", method!(Wrapper::merge, 1))?;
152
+ class.define_method("dump", method!(Wrapper::dump, 0))?;
153
+ class.define_method("inspect", method!(Wrapper::inspect, 0))?;
154
+ Ok(())
155
+ }
@@ -0,0 +1,140 @@
1
+ //! Ruby bindings for the `probabilistic-machines` sketches.
2
+ //!
3
+ //! Every sketch is sized by a const generic, so each class dispatches the size
4
+ //! it is given to a fixed set of monomorphised variants and raises
5
+ //! `ArgumentError` for any other size.
6
+
7
+ #![allow(clippy::needless_pass_by_value)]
8
+
9
+ mod countmin;
10
+ mod cuckoo;
11
+ mod data;
12
+ mod hyperloglog;
13
+ mod tdigest;
14
+ mod topk;
15
+
16
+ use std::sync::OnceLock;
17
+
18
+ use magnus::scan_args::get_kwargs;
19
+ use magnus::value::Opaque;
20
+ use magnus::{Error, ExceptionClass, Integer, RHash, RModule, Ruby, TryConvert, Value, prelude::*};
21
+ use probabilistic_machines::CodecError;
22
+
23
+ /// `ProbabilisticMachines::Full`, defined in [`init`].
24
+ static FULL: OnceLock<Opaque<ExceptionClass>> = OnceLock::new();
25
+
26
+ /// A whole number of things: a count, a width, a depth, a `k`, a hash.
27
+ ///
28
+ /// magnus reads an integer through Ruby's `NUM2*` family, which truncates a
29
+ /// `Float` towards zero instead of refusing it, so an argument that means an
30
+ /// integer is read as one of these and anything but an `Integer` is turned
31
+ /// away. An `Integer` outside the target width still comes back as the
32
+ /// `RangeError` magnus raises for it.
33
+ pub(crate) struct Whole<T>(pub(crate) T);
34
+
35
+ impl<T: TryConvert> TryConvert for Whole<T> {
36
+ fn try_convert(value: Value) -> Result<Self, Error> {
37
+ if Integer::from_value(value).is_none() {
38
+ return Err(not_whole(value));
39
+ }
40
+ T::try_convert(value).map(Self)
41
+ }
42
+ }
43
+
44
+ fn not_whole(value: Value) -> Error {
45
+ let ruby = Ruby::get_with(value);
46
+ Error::new(
47
+ ruby.exception_type_error(),
48
+ format!(
49
+ "no implicit conversion of {} into Integer: {}",
50
+ value.class(),
51
+ value.inspect()
52
+ ),
53
+ )
54
+ }
55
+
56
+ /// The one required keyword of a constructor.
57
+ pub(crate) fn required_kwarg<T: TryConvert>(keywords: RHash, name: &str) -> Result<T, Error> {
58
+ Ok(get_kwargs::<_, (T,), (), ()>(keywords, &[name], &[])?
59
+ .required
60
+ .0)
61
+ }
62
+
63
+ /// A keyword with a default the caller may leave out.
64
+ pub(crate) fn optional_kwarg<T: TryConvert>(
65
+ keywords: RHash,
66
+ name: &str,
67
+ ) -> Result<Option<T>, Error> {
68
+ Ok(
69
+ get_kwargs::<_, (), (Option<T>,), ()>(keywords, &[], &[name])?
70
+ .optional
71
+ .0,
72
+ )
73
+ }
74
+
75
+ pub(crate) fn unsupported(ruby: &Ruby, name: &str, given: usize, supported: &[usize]) -> Error {
76
+ Error::new(
77
+ ruby.exception_arg_error(),
78
+ format!("{name} {given} is not supported, expected one of {supported:?}"),
79
+ )
80
+ }
81
+
82
+ pub(crate) fn unsupported_blob(ruby: &Ruby, name: &str, supported: &[usize]) -> Error {
83
+ Error::new(
84
+ ruby.exception_arg_error(),
85
+ format!("blob was written with a {name} outside {supported:?}"),
86
+ )
87
+ }
88
+
89
+ pub(crate) fn codec_error(ruby: &Ruby, error: CodecError) -> Error {
90
+ Error::new(ruby.exception_arg_error(), format!("codec: {error}"))
91
+ }
92
+
93
+ pub(crate) fn mismatch(ruby: &Ruby, name: &str) -> Error {
94
+ Error::new(
95
+ ruby.exception_arg_error(),
96
+ format!("the other {name} has a different shape"),
97
+ )
98
+ }
99
+
100
+ pub(crate) fn uninitialised(ruby: &Ruby) -> Error {
101
+ Error::new(
102
+ ruby.exception_runtime_error(),
103
+ "the probabilistic_machines extension is not initialised",
104
+ )
105
+ }
106
+
107
+ pub(crate) fn defined_twice(ruby: &Ruby, name: &str) -> Error {
108
+ Error::new(
109
+ ruby.exception_runtime_error(),
110
+ format!("ProbabilisticMachines::{name} was initialised twice"),
111
+ )
112
+ }
113
+
114
+ /// `ProbabilisticMachines::Full`, raised when a cuckoo filter cannot place an
115
+ /// item after relocating its neighbours.
116
+ pub(crate) fn full(ruby: &Ruby) -> Error {
117
+ FULL.get().map_or_else(
118
+ || uninitialised(ruby),
119
+ |class| Error::new(ruby.get_inner(*class), "the filter is full"),
120
+ )
121
+ }
122
+
123
+ fn define_errors(ruby: &Ruby, module: RModule) -> Result<(), Error> {
124
+ let base = module.define_error("Error", ruby.exception_standard_error())?;
125
+ let full = module.define_error("Full", base)?;
126
+ FULL.set(full.into())
127
+ .map_err(|_| defined_twice(ruby, "Full"))
128
+ }
129
+
130
+ #[magnus::init]
131
+ fn init(ruby: &Ruby) -> Result<(), Error> {
132
+ let module = ruby.define_module("ProbabilisticMachines")?;
133
+ define_errors(ruby, module)?;
134
+ data::define(ruby, module)?;
135
+ hyperloglog::define(ruby, module)?;
136
+ countmin::define(ruby, module)?;
137
+ tdigest::define(ruby, module)?;
138
+ cuckoo::define(ruby, module)?;
139
+ topk::define(ruby, module)
140
+ }
@@ -0,0 +1,166 @@
1
+ //! `ProbabilisticMachines::TDigest`: quantiles of a stream, in clusters.
2
+
3
+ use std::cell::RefCell;
4
+
5
+ use bytes::Bytes;
6
+ use magnus::scan_args::scan_args;
7
+ use magnus::typed_data::Obj;
8
+ use magnus::{Error, RArray, RHash, RModule, RString, Ruby, Value, function, method, prelude::*};
9
+ use probabilistic_machines::TDigest;
10
+
11
+ use crate::data::centroid;
12
+ use crate::{Whole, codec_error, optional_kwarg, required_kwarg};
13
+
14
+ /// Below this the cluster budget is zero, so every point merges into one
15
+ /// centroid and a quantile is meaningless.
16
+ const MIN_COMPRESSION: u16 = 10;
17
+
18
+ /// The compressions this binding answers for, whether the digest is being built
19
+ /// or read back.
20
+ fn honoured(ruby: &Ruby, compression: u16) -> Result<u16, Error> {
21
+ if compression < MIN_COMPRESSION {
22
+ return Err(Error::new(
23
+ ruby.exception_arg_error(),
24
+ format!("compression must be at least {MIN_COMPRESSION}, got {compression}"),
25
+ ));
26
+ }
27
+ Ok(compression)
28
+ }
29
+
30
+ #[magnus::wrap(class = "ProbabilisticMachines::TDigest", free_immediately, size)]
31
+ struct Wrapper(RefCell<TDigest>);
32
+
33
+ impl Wrapper {
34
+ fn new(ruby: &Ruby, args: &[Value]) -> Result<Self, Error> {
35
+ let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
36
+ let Whole(compression) = required_kwarg::<Whole<u16>>(args.keywords, "compression")?;
37
+ Ok(Self(RefCell::new(TDigest::new(honoured(
38
+ ruby,
39
+ compression,
40
+ )?))))
41
+ }
42
+
43
+ fn load(ruby: &Ruby, blob: Bytes) -> Result<Self, Error> {
44
+ let digest = TDigest::decode(&blob).map_err(|error| codec_error(ruby, error))?;
45
+ honoured(ruby, digest.compression())?;
46
+ Ok(Self(RefCell::new(digest)))
47
+ }
48
+
49
+ fn compression(&self) -> u16 {
50
+ self.0.borrow().compression()
51
+ }
52
+
53
+ fn add(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<(), Error> {
54
+ let args = scan_args::<(f64,), (), (), (), RHash, ()>(args)?;
55
+ let (value,) = args.required;
56
+ let weight: f64 = optional_kwarg(args.keywords, "weight")?.unwrap_or(1.0);
57
+ if !value.is_finite() {
58
+ return Err(Error::new(
59
+ ruby.exception_arg_error(),
60
+ format!("value must be finite, got {value}"),
61
+ ));
62
+ }
63
+ if !weight.is_finite() || weight <= 0.0 {
64
+ return Err(Error::new(
65
+ ruby.exception_arg_error(),
66
+ format!("weight must be finite and positive, got {weight}"),
67
+ ));
68
+ }
69
+ let count = rb_self.0.borrow().count();
70
+ if !(count + weight).is_finite() {
71
+ return Err(Error::new(
72
+ ruby.exception_arg_error(),
73
+ format!("weight {weight} overflows the total weight {count}"),
74
+ ));
75
+ }
76
+ rb_self.0.borrow_mut().add_weighted(value, weight);
77
+ Ok(())
78
+ }
79
+
80
+ fn quantile(&self, q: f64) -> f64 {
81
+ self.0.borrow().quantile(q)
82
+ }
83
+
84
+ fn cdf(&self, value: f64) -> f64 {
85
+ self.0.borrow().cdf(value)
86
+ }
87
+
88
+ fn count(&self) -> f64 {
89
+ self.0.borrow().count()
90
+ }
91
+
92
+ fn minimum(&self) -> f64 {
93
+ self.0.borrow().minimum()
94
+ }
95
+
96
+ fn maximum(&self) -> f64 {
97
+ self.0.borrow().maximum()
98
+ }
99
+
100
+ fn size(&self) -> usize {
101
+ self.0.borrow().len()
102
+ }
103
+
104
+ fn is_empty(&self) -> bool {
105
+ self.0.borrow().is_empty()
106
+ }
107
+
108
+ fn centroids(ruby: &Ruby, rb_self: &Self) -> Result<RArray, Error> {
109
+ let digest = rb_self.0.borrow();
110
+ let clusters = ruby.ary_new_capa(digest.len());
111
+ for cluster in digest.centroids() {
112
+ clusters.push(centroid(ruby, cluster.mean, cluster.weight)?)?;
113
+ }
114
+ Ok(clusters)
115
+ }
116
+
117
+ fn merge(ruby: &Ruby, rb_self: Obj<Self>, other: &Self) -> Result<Obj<Self>, Error> {
118
+ let source = other.0.borrow().clone();
119
+ let count = rb_self.0.borrow().count();
120
+ if !(count + source.count()).is_finite() {
121
+ return Err(Error::new(
122
+ ruby.exception_arg_error(),
123
+ format!(
124
+ "total weight {} overflows the total weight {count}",
125
+ source.count()
126
+ ),
127
+ ));
128
+ }
129
+ rb_self.0.borrow_mut().merge(&source);
130
+ Ok(rb_self)
131
+ }
132
+
133
+ fn dump(ruby: &Ruby, rb_self: &Self) -> RString {
134
+ ruby.str_from_slice(&rb_self.0.borrow().encoded())
135
+ }
136
+
137
+ fn inspect(&self) -> String {
138
+ let digest = self.0.borrow();
139
+ format!(
140
+ "#<ProbabilisticMachines::TDigest compression={} count={} centroids={}>",
141
+ digest.compression(),
142
+ digest.count(),
143
+ digest.len()
144
+ )
145
+ }
146
+ }
147
+
148
+ pub fn define(ruby: &Ruby, module: RModule) -> Result<(), Error> {
149
+ let class = module.define_class("TDigest", ruby.class_object())?;
150
+ class.define_singleton_method("new", function!(Wrapper::new, -1))?;
151
+ class.define_singleton_method("load", function!(Wrapper::load, 1))?;
152
+ class.define_method("compression", method!(Wrapper::compression, 0))?;
153
+ class.define_method("add", method!(Wrapper::add, -1))?;
154
+ class.define_method("quantile", method!(Wrapper::quantile, 1))?;
155
+ class.define_method("cdf", method!(Wrapper::cdf, 1))?;
156
+ class.define_method("count", method!(Wrapper::count, 0))?;
157
+ class.define_method("minimum", method!(Wrapper::minimum, 0))?;
158
+ class.define_method("maximum", method!(Wrapper::maximum, 0))?;
159
+ class.define_method("size", method!(Wrapper::size, 0))?;
160
+ class.define_method("empty?", method!(Wrapper::is_empty, 0))?;
161
+ class.define_method("centroids", method!(Wrapper::centroids, 0))?;
162
+ class.define_method("merge", method!(Wrapper::merge, 1))?;
163
+ class.define_method("dump", method!(Wrapper::dump, 0))?;
164
+ class.define_method("inspect", method!(Wrapper::inspect, 0))?;
165
+ Ok(())
166
+ }