Skip to main content

polydat_nodes/
vector_set.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Element and set operations over the integer vector carriers.
5//!
6//! The float vector nodes ([`super::vector_math`]) compute over
7//! embeddings; these read integer vectors as *lists*: how many
8//! elements, what is at an index, does a value appear, where, and how
9//! two lists compare as sets. A host holding a list of ordinals — the
10//! records matching a label, the queries matching a predicate — asks
11//! these the questions it would otherwise ask a counter.
12//!
13//! That is the point of them. A rank ("how many matching records come
14//! before this one") and a cardinality ("how many match at all") look
15//! like things a counter produces as records go by, but both are pure
16//! functions of the list and the record: the rank is the position, the
17//! cardinality is the length. Written that way the answer is owned by
18//! its inputs, replays from its coordinates, and agrees on every engine
19//! and every fiber, where a counter's answer depends on how many times
20//! it was called ([Runtime Model](../../polydat/docs/design/runtime_model.md)
21//! §9.1, "No state across pulls").
22//!
23//! Each operation exists for `vec_i32` and `vec_i64`, since the carrier
24//! is part of the wire's type and polydat does not silently widen one
25//! to the other. An index or a value that is out of range answers
26//! rather than panicking, because a miss is an ordinary result here:
27//! `vec_position` returns the length when the value is absent, which is
28//! "past the end" and the same convention `vec_len` bounds.
29
30/// `vec_len_i32(v)` — how many elements the list holds.
31#[polydat::polydat_node(category = Arithmetic)]
32fn vec_len_i32(v: &[i32]) -> u64 {
33    v.len() as u64
34}
35
36/// `vec_len_i64(v)` — how many elements the list holds.
37#[polydat::polydat_node(category = Arithmetic)]
38fn vec_len_i64(v: &[i64]) -> u64 {
39    v.len() as u64
40}
41
42/// `vec_at_i32(v, i)` — the element at `i`, or `0` past the end.
43///
44/// Out of range answers rather than failing: a caller that cares
45/// compares `i` with `vec_len_i32` first, and one that is walking a
46/// list it already sized does not pay for a check it cannot fail.
47#[polydat::polydat_node(category = Arithmetic)]
48fn vec_at_i32(v: &[i32], i: u64) -> i64 {
49    v.get(i as usize).copied().unwrap_or(0) as i64
50}
51
52/// `vec_at_i64(v, i)` — the element at `i`, or `0` past the end.
53#[polydat::polydat_node(category = Arithmetic)]
54fn vec_at_i64(v: &[i64], i: u64) -> i64 {
55    v.get(i as usize).copied().unwrap_or(0)
56}
57
58/// `vec_max_i32(v)` — the largest element, or `0` for an empty list.
59#[polydat::polydat_node(category = Arithmetic)]
60fn vec_max_i32(v: &[i32]) -> i64 {
61    v.iter().copied().max().unwrap_or(0) as i64
62}
63
64/// `vec_max_i64(v)` — the largest element, or `0` for an empty list.
65#[polydat::polydat_node(category = Arithmetic)]
66fn vec_max_i64(v: &[i64]) -> i64 {
67    v.iter().copied().max().unwrap_or(0)
68}
69
70/// `vec_min_i32(v)` — the smallest element, or `0` for an empty list.
71#[polydat::polydat_node(category = Arithmetic)]
72fn vec_min_i32(v: &[i32]) -> i64 {
73    v.iter().copied().min().unwrap_or(0) as i64
74}
75
76/// `vec_min_i64(v)` — the smallest element, or `0` for an empty list.
77#[polydat::polydat_node(category = Arithmetic)]
78fn vec_min_i64(v: &[i64]) -> i64 {
79    v.iter().copied().min().unwrap_or(0)
80}
81
82/// `vec_contains_i32(v, x)` — `1` when `x` appears in the list, else `0`.
83#[polydat::polydat_node(category = Comparison)]
84fn vec_contains_i32(v: &[i32], x: i64) -> u64 {
85    u64::from(i32::try_from(x).is_ok_and(|x| v.contains(&x)))
86}
87
88/// `vec_contains_i64(v, x)` — `1` when `x` appears in the list, else `0`.
89#[polydat::polydat_node(category = Comparison)]
90fn vec_contains_i64(v: &[i64], x: i64) -> u64 {
91    u64::from(v.contains(&x))
92}
93
94/// `vec_position_i32(v, x)` — the index of the first `x`, or the list's
95/// length when it does not appear.
96///
97/// The length is "past the end", so a caller can branch on
98/// `pos == vec_len_i32(v)` without a second sentinel, and a rank over a
99/// sorted list of ordinals reads directly as the position.
100#[polydat::polydat_node(category = Comparison)]
101fn vec_position_i32(v: &[i32], x: i64) -> u64 {
102    match i32::try_from(x) {
103        Ok(x) => v.iter().position(|e| *e == x).unwrap_or(v.len()) as u64,
104        Err(_) => v.len() as u64,
105    }
106}
107
108/// `vec_position_i64(v, x)` — the index of the first `x`, or the list's
109/// length when it does not appear.
110#[polydat::polydat_node(category = Comparison)]
111fn vec_position_i64(v: &[i64], x: i64) -> u64 {
112    v.iter().position(|e| *e == x).unwrap_or(v.len()) as u64
113}
114
115/// `vec_count_below_i32(v, x)` — how many elements are less than `x`.
116///
117/// The rank of a record among the matching ones, without requiring the
118/// list to be sorted or the record to be in it, which
119/// `vec_position_i32` both do.
120#[polydat::polydat_node(category = Comparison)]
121fn vec_count_below_i32(v: &[i32], x: i64) -> u64 {
122    v.iter().filter(|e| i64::from(**e) < x).count() as u64
123}
124
125/// `vec_count_below_i64(v, x)` — how many elements are less than `x`.
126#[polydat::polydat_node(category = Comparison)]
127fn vec_count_below_i64(v: &[i64], x: i64) -> u64 {
128    v.iter().filter(|e| **e < x).count() as u64
129}
130
131/// `vec_intersect_count_i32(a, b)` — how many *distinct* values appear
132/// in both lists.
133///
134/// Distinct, because this answers "how much do these two sets overlap",
135/// which a repeated value would otherwise inflate. A caller wanting the
136/// multiset count writes the fold it means.
137#[polydat::polydat_node(category = Comparison)]
138fn vec_intersect_count_i32(a: &[i32], b: &[i32]) -> u64 {
139    let set: std::collections::BTreeSet<i32> = b.iter().copied().collect();
140    let mine: std::collections::BTreeSet<i32> = a.iter().copied().collect();
141    mine.intersection(&set).count() as u64
142}
143
144/// `vec_intersect_count_i64(a, b)` — how many distinct values appear in
145/// both lists.
146#[polydat::polydat_node(category = Comparison)]
147fn vec_intersect_count_i64(a: &[i64], b: &[i64]) -> u64 {
148    let set: std::collections::BTreeSet<i64> = b.iter().copied().collect();
149    let mine: std::collections::BTreeSet<i64> = a.iter().copied().collect();
150    mine.intersection(&set).count() as u64
151}
152
153/// `vec_set_eq_i32(a, b)` — `1` when both lists hold the same set of
154/// values, whatever their order or repetition.
155#[polydat::polydat_node(category = Comparison)]
156fn vec_set_eq_i32(a: &[i32], b: &[i32]) -> u64 {
157    let x: std::collections::BTreeSet<i32> = a.iter().copied().collect();
158    let y: std::collections::BTreeSet<i32> = b.iter().copied().collect();
159    u64::from(x == y)
160}
161
162/// `vec_set_eq_i64(a, b)` — `1` when both lists hold the same set of
163/// values, whatever their order or repetition.
164#[polydat::polydat_node(category = Comparison)]
165fn vec_set_eq_i64(a: &[i64], b: &[i64]) -> u64 {
166    let x: std::collections::BTreeSet<i64> = a.iter().copied().collect();
167    let y: std::collections::BTreeSet<i64> = b.iter().copied().collect();
168    u64::from(x == y)
169}
170
171// ── Reading a list from text ────────────────────────────────────────
172
173/// `str_to_vec_i32(s)` — a JSON array of integers as a `vec_i32` wire.
174///
175/// The public name for a parse the crate already performed internally
176/// at the boundary: a host holding a list as text — a column it
177/// selected, a facet it loaded, a literal in a workload — gets it onto
178/// a wire the list operations read. The `as` cast does not do this,
179/// because text to a typed vector can fail on its input and the
180/// adapter catalog only inserts conversions that cannot ([Type
181/// System](type_system.md) §3); naming it is the caller saying it
182/// accepts that.
183///
184/// Fails by name on anything that is not a JSON array of integers in
185/// range, rather than skipping the element: a list silently short one
186/// entry is a wrong count everywhere downstream.
187#[polydat::polydat_node(category = Conversions)]
188fn str_to_vec_i32(s: &str) -> Vec<i32> {
189    parse_int_array("str_to_vec_i32", s)
190        .into_iter()
191        .map(|n| {
192            i32::try_from(n).unwrap_or_else(|_| {
193                panic!(
194                    "str_to_vec_i32: element {n} is outside i32 [{}, {}]",
195                    i32::MIN,
196                    i32::MAX
197                )
198            })
199        })
200        .collect()
201}
202
203/// `str_to_vec_i64(s)` — a JSON array of integers as a `vec_i64` wire.
204#[polydat::polydat_node(category = Conversions)]
205fn str_to_vec_i64(s: &str) -> Vec<i64> {
206    parse_int_array("str_to_vec_i64", s)
207}
208
209/// `str_to_vec_f64(s)` — a JSON array of numbers as a `vec_f64` wire,
210/// such as a window of samples for `is_stable`. Fails by name on
211/// anything that is not a JSON array of numbers.
212#[polydat::polydat_node(category = Conversions)]
213fn str_to_vec_f64(s: &str) -> Vec<f64> {
214    let raw = s.trim();
215    let parsed: serde_json::Value = serde_json::from_str(raw)
216        .unwrap_or_else(|e| panic!("str_to_vec_f64: cannot parse {raw:?} as a JSON array: {e}"));
217    let arr = parsed
218        .as_array()
219        .unwrap_or_else(|| panic!("str_to_vec_f64: parsed JSON is not an array: {raw:?}"));
220    arr.iter()
221        .map(|j| {
222            j.as_f64()
223                .unwrap_or_else(|| panic!("str_to_vec_f64: element {j} is not a number in {raw:?}"))
224        })
225        .collect()
226}
227
228/// The shared parse: a JSON array of integers, or a failure naming the
229/// node, the text, and what was wrong with it.
230fn parse_int_array(node: &str, s: &str) -> Vec<i64> {
231    let raw = s.trim();
232    let parsed: serde_json::Value = serde_json::from_str(raw)
233        .unwrap_or_else(|e| panic!("{node}: cannot parse {raw:?} as a JSON array: {e}"));
234    let arr = parsed
235        .as_array()
236        .unwrap_or_else(|| panic!("{node}: parsed JSON is not an array: {raw:?}"));
237    arr.iter()
238        .map(|j| {
239            j.as_i64()
240                .unwrap_or_else(|| panic!("{node}: element {j} is not an integer in {raw:?}"))
241        })
242        .collect()
243}