Skip to main content

pond_rs/
partition.rs

1//! `partition_by` / `group_by`: run operations on each entity (host,
2//! symbol, device) of a series separately, so that a row's neighbours are
3//! always rows of the same entity.
4//!
5//! Semantics follow pond-ts's `PartitionedTimeSeries` exactly, including two
6//! details that decide row order:
7//!
8//! - Partitions are numbered in the order their first row appears.
9//! - Putting partitions back together (pond-ts's `TimeSeries.concat`) sorts
10//!   the rows by key and keeps the partition order for rows with the same
11//!   key, so rows at a shared time come out partition by partition, not in
12//!   their original order.
13//!
14//! pond-ts merges the partitions after every operation and splits them again
15//! before the next. pond-rs keeps them apart and only redoes what that merge
16//! and split would change: it drops partitions left empty, reorders them by
17//! their first row, and splits again for real only when an operation changed
18//! the values of a partition column.
19
20use std::collections::HashMap;
21
22use crate::column::{ColumnData, Value};
23use crate::error::{PondError, Result};
24use crate::grid::{Align, Keep, Materialize};
25use crate::par;
26use crate::reducer::{Agg, Reducer};
27use crate::rolling::{Rolling, RollingOver};
28use crate::rowwise::{Change, Fill, FillLimits};
29use crate::sequence::Sequence;
30use crate::series::{Column, ColumnKind, Key, Labels, TimeSeries};
31use crate::smooth::{Sample, Smooth};
32
33/// A partition key: one entry per partition column, `None` for a gap.
34pub type PartitionKey = Vec<Option<Value>>;
35
36/// A series split into partitions by the values of one or more columns:
37/// [`TimeSeries::partition_by`]. Operations run on each partition
38/// separately (on several cores with the `parallel` feature) and return
39/// another `PartitionedSeries`; [`PartitionedSeries::collect`] puts the
40/// rows back into one series.
41///
42/// ```
43/// # use pond_rs::{Agg, Column, FirstRow, Reducer, TimeSeries, Value};
44/// let s = TimeSeries::new("cpu", vec![0, 0, 1000, 1000], vec![
45///     Column::required("host", vec!["a", "b", "a", "b"]),
46///     Column::required("load", vec![1.0, 10.0, 3.0, 40.0]),
47/// ])?;
48/// let change = s.partition_by(&["host"])?.diff(&["load"])?.collect()?;
49/// // Each host against its own previous row, not the row above.
50/// assert_eq!(change.column("load")?.numbers()?.get(2), Some(2.0));
51/// assert_eq!(change.column("load")?.numbers()?.get(3), Some(30.0));
52///
53/// let by_host = s.partition_by(&["host"])?;
54/// let a = by_host.get(&[Some(Value::String("a".into()))]).unwrap();
55/// assert_eq!(a.len(), 2);
56/// # Ok::<(), pond_rs::PondError>(())
57/// ```
58#[derive(Debug, Clone)]
59pub struct PartitionedSeries {
60    by: Vec<String>,
61    /// In partition order: the order their first rows appear in the
62    /// series as one.
63    parts: Vec<(PartitionKey, TimeSeries)>,
64    /// The order the last operation ran the partitions in, as indexes into
65    /// `parts`; [`PartitionedSeries::collect`] merges in this order.
66    /// pond-ts splits before an operation and merges right after it, so an
67    /// operation that moves a partition's first row reorders the partitions
68    /// for the next operation but not for its own merge.
69    run_order: Vec<usize>,
70    /// The series as one, while no operation has run: pond-ts's `collect`
71    /// then returns its source unchanged, in its own row order.
72    whole: Option<TimeSeries>,
73    /// A zero-row series with the current columns, for when there are no
74    /// partitions (pond-ts runs the operation on an empty series).
75    empty: TimeSeries,
76}
77
78impl TimeSeries {
79    /// Split this series by the values of `columns` (one or more value
80    /// columns of any kind). A gap is a value of its own: rows with no
81    /// value form one partition. Numbers are compared by value (`0` and
82    /// `-0` are one partition, as in pond-ts).
83    pub fn partition_by(&self, columns: &[&str]) -> Result<PartitionedSeries> {
84        const CALL: &str = "TimeSeries::partition_by";
85        if columns.is_empty() {
86            return Err(PondError::InvalidData {
87                call: CALL,
88                message: "needs at least one partition column".into(),
89            });
90        }
91        let by: Vec<usize> = columns
92            .iter()
93            .map(|c| self.column_index(CALL, c))
94            .collect::<Result<_>>()?;
95        refuse_array_keys(CALL, self, &by)?;
96        Ok(PartitionedSeries {
97            by: columns.iter().map(|c| c.to_string()).collect(),
98            run_order: Vec::new(),
99            parts: split(self, &by),
100            whole: Some(self.clone()),
101            empty: self.row_range(0..0),
102        })
103    }
104
105    /// The partitions of [`TimeSeries::partition_by`], each with its key,
106    /// in the order their first row appears (pond-ts `groupBy`, which takes
107    /// one column; this takes one or more).
108    pub fn group_by(&self, columns: &[&str]) -> Result<Vec<(PartitionKey, TimeSeries)>> {
109        Ok(self.partition_by(columns)?.into_groups())
110    }
111}
112
113impl PartitionedSeries {
114    /// The partition columns.
115    pub fn by(&self) -> &[String] {
116        &self.by
117    }
118
119    /// The number of partitions.
120    pub fn len(&self) -> usize {
121        self.parts.len()
122    }
123
124    /// `true` when there is nothing in it.
125    pub fn is_empty(&self) -> bool {
126        self.parts.is_empty()
127    }
128
129    /// Each partition's key and rows, in partition order (pond-ts
130    /// `toMap`).
131    pub fn iter(&self) -> impl ExactSizeIterator<Item = (&[Option<Value>], &TimeSeries)> {
132        self.parts.iter().map(|(k, s)| (k.as_slice(), s))
133    }
134
135    /// The partition with this key, if there is one.
136    pub fn get(&self, key: &[Option<Value>]) -> Option<&TimeSeries> {
137        self.parts
138            .iter()
139            .find(|(k, _)| k.as_slice() == key)
140            .map(|(_, s)| s)
141    }
142
143    /// The partitions as owned `(key, series)` pairs.
144    pub fn into_groups(self) -> Vec<(PartitionKey, TimeSeries)> {
145        self.parts
146    }
147
148    /// All partitions as one series, sorted by key; rows with the same key
149    /// keep partition order. Before any operation has run this is the
150    /// original series, unchanged. An error if an operation gave different
151    /// partitions different columns.
152    pub fn collect(&self) -> Result<TimeSeries> {
153        match &self.whole {
154            Some(whole) => Ok(whole.clone()),
155            None => {
156                let series: Vec<&TimeSeries> =
157                    self.run_order.iter().map(|&i| &self.parts[i].1).collect();
158                merge(&series, &self.empty)
159            }
160        }
161    }
162
163    /// Run `f` on each partition and stay partitioned. The partition
164    /// columns must survive `f` (pond-ts throws otherwise). If `f` changes
165    /// a partition column's values, the rows are split again by the new
166    /// values, as pond-ts does.
167    ///
168    /// `f` runs on several cores with the `parallel` feature; each
169    /// partition is still computed by one thread, so results are the same
170    /// on any number. The first error, in partition order, is returned.
171    pub fn map<F>(&self, f: F) -> Result<PartitionedSeries>
172    where
173        F: Fn(&TimeSeries) -> Result<TimeSeries> + Sync + Send,
174    {
175        let (parts, empty) = self.run(&f)?;
176        let by_index: Vec<usize> = self
177            .by
178            .iter()
179            .map(|c| empty.column_index("PartitionedSeries", c))
180            .collect::<Result<_>>()?;
181        // A step may turn a partition column into an array, which cannot
182        // be a key (see `partition_by`).
183        refuse_array_keys("PartitionedSeries", &empty, &by_index)?;
184        // With no partitions, any rows `f` made on the empty series have no
185        // key yet, so they are always split.
186        let unchanged =
187            !self.parts.is_empty() && parts.iter().all(|(key, s)| keeps_key(s, &by_index, key));
188        if !unchanged {
189            // The values moved: split the merged rows again, as pond-ts
190            // does, and collect gives those merged rows.
191            let whole = merge(&series_of(&parts), &empty)?;
192            return Ok(PartitionedSeries {
193                by: self.by.clone(),
194                parts: split(&whole, &by_index),
195                run_order: Vec::new(),
196                whole: Some(whole),
197                empty,
198            });
199        }
200        // What a merge and a fresh split would give: empty partitions gone,
201        // the rest in the order their first rows now appear.
202        let mut ran: Vec<Option<(PartitionKey, TimeSeries)>> = parts
203            .into_iter()
204            .filter(|(_, s)| !s.is_empty())
205            .map(Some)
206            .collect();
207        let mut order: Vec<usize> = (0..ran.len()).collect();
208        order.sort_by_key(|&i| first_key(&ran[i].as_ref().expect("not taken yet").1));
209        let mut run_order = vec![0; ran.len()];
210        for (at, &i) in order.iter().enumerate() {
211            run_order[i] = at;
212        }
213        let parts = order
214            .iter()
215            .map(|&i| ran[i].take().expect("each taken once"))
216            .collect();
217        Ok(PartitionedSeries {
218            by: self.by.clone(),
219            parts,
220            run_order,
221            whole: None,
222            empty,
223        })
224    }
225
226    /// Run `f` on each partition and put the results back into one series
227    /// (pond-ts `apply`). Unlike [`PartitionedSeries::map`], `f` may drop
228    /// the partition columns.
229    pub fn apply<F>(&self, f: F) -> Result<TimeSeries>
230    where
231        F: Fn(&TimeSeries) -> Result<TimeSeries> + Sync + Send,
232    {
233        let (parts, empty) = self.run(&f)?;
234        merge(&series_of(&parts), &empty)
235    }
236
237    /// [`TimeSeries::diff`] on each partition.
238    pub fn diff(&self, columns: &[&str]) -> Result<PartitionedSeries> {
239        self.map(|g| g.diff(columns))
240    }
241
242    /// [`TimeSeries::diff_with`] on each partition.
243    pub fn diff_with(&self, columns: &[&str], options: Change) -> Result<PartitionedSeries> {
244        self.map(|g| g.diff_with(columns, options.clone()))
245    }
246
247    /// [`TimeSeries::rate`] on each partition.
248    pub fn rate(&self, columns: &[&str]) -> Result<PartitionedSeries> {
249        self.map(|g| g.rate(columns))
250    }
251
252    /// [`TimeSeries::rate_with`] on each partition.
253    pub fn rate_with(&self, columns: &[&str], options: Change) -> Result<PartitionedSeries> {
254        self.map(|g| g.rate_with(columns, options.clone()))
255    }
256
257    /// [`TimeSeries::pct_change`] on each partition.
258    pub fn pct_change(&self, columns: &[&str]) -> Result<PartitionedSeries> {
259        self.map(|g| g.pct_change(columns))
260    }
261
262    /// [`TimeSeries::pct_change_with`] on each partition.
263    pub fn pct_change_with(&self, columns: &[&str], options: Change) -> Result<PartitionedSeries> {
264        self.map(|g| g.pct_change_with(columns, options.clone()))
265    }
266
267    /// [`TimeSeries::shift`] on each partition.
268    pub fn shift(&self, columns: &[&str], n: i64) -> Result<PartitionedSeries> {
269        self.map(|g| g.shift(columns, n))
270    }
271
272    /// [`TimeSeries::cumulative`] on each partition.
273    pub fn cumulative(&self, spec: &[(&str, Reducer)]) -> Result<PartitionedSeries> {
274        self.map(|g| g.cumulative(spec))
275    }
276
277    /// [`TimeSeries::fill`] on each partition.
278    pub fn fill(&self, spec: &[(&str, Fill)]) -> Result<PartitionedSeries> {
279        self.map(|g| g.fill(spec))
280    }
281
282    /// [`TimeSeries::fill_with`] on each partition.
283    pub fn fill_with(
284        &self,
285        spec: &[(&str, Fill)],
286        limits: &FillLimits,
287    ) -> Result<PartitionedSeries> {
288        self.map(|g| g.fill_with(spec, limits))
289    }
290
291    /// [`TimeSeries::fill_all`] on each partition.
292    pub fn fill_all(&self, fill: Fill) -> Result<PartitionedSeries> {
293        self.map(|g| g.fill_all(fill.clone()))
294    }
295
296    /// [`TimeSeries::fill_all_with`] on each partition.
297    pub fn fill_all_with(&self, fill: Fill, limits: &FillLimits) -> Result<PartitionedSeries> {
298        self.map(|g| g.fill_all_with(fill.clone(), limits))
299    }
300
301    /// [`TimeSeries::rolling`] on each partition. Only the outputs are
302    /// kept, plus each partition column that no output is named after,
303    /// carried through as a `first` output at the end (as pond-ts does), so
304    /// the result stays partitioned.
305    pub fn rolling(&self, window: &str, aggs: &[Agg]) -> Result<PartitionedSeries> {
306        let aggs = self.with_partition_outputs(aggs);
307        self.map(|g| g.rolling(window, &aggs))
308    }
309
310    /// [`TimeSeries::rolling_with`] on each partition, carrying the
311    /// partition columns as [`PartitionedSeries::rolling`] does. A row
312    /// held back by [`Rolling::min_samples`] gets a gap there too, so, as
313    /// in pond-ts, it moves to the partition with no value on the next
314    /// split. With [`Rolling::append`] every source column is kept, the
315    /// partition columns included, so nothing is carried.
316    pub fn rolling_with(
317        &self,
318        window: &str,
319        aggs: &[Agg],
320        options: Rolling,
321    ) -> Result<PartitionedSeries> {
322        let aggs = self.rolling_outputs(aggs, options);
323        self.map(|g| g.rolling_with(window, &aggs, options))
324    }
325
326    /// [`TimeSeries::rolling_rows`] on each partition, carrying the
327    /// partition columns as [`PartitionedSeries::rolling_with`] does.
328    pub fn rolling_rows(
329        &self,
330        rows: usize,
331        aggs: &[Agg],
332        options: Rolling,
333    ) -> Result<PartitionedSeries> {
334        let aggs = self.rolling_outputs(aggs, options);
335        self.map(|g| g.rolling_rows(rows, &aggs, options))
336    }
337
338    /// [`TimeSeries::rolling_over`] on each partition, carrying the
339    /// partition columns as [`PartitionedSeries::aggregate`] does: a bucket
340    /// whose window holds no rows, or fewer than
341    /// [`RollingOver::min_samples`], gets a gap there.
342    pub fn rolling_over(
343        &self,
344        sequence: &Sequence,
345        window: &str,
346        aggs: &[Agg],
347        options: RollingOver,
348    ) -> Result<PartitionedSeries> {
349        let aggs = self.with_partition_outputs(aggs);
350        self.map(|g| g.rolling_over(sequence, window, &aggs, options))
351    }
352
353    /// [`TimeSeries::aggregate`] on each partition, carrying the partition
354    /// columns as [`PartitionedSeries::rolling`] does. A bucket with no rows
355    /// gets a gap there, so pond-ts, and so pond-rs, moves such rows to the
356    /// partition with no value on the next split.
357    pub fn aggregate(&self, sequence: &Sequence, aggs: &[Agg]) -> Result<PartitionedSeries> {
358        let aggs = self.with_partition_outputs(aggs);
359        self.map(|g| g.aggregate(sequence, &aggs))
360    }
361
362    /// [`TimeSeries::align`] on each partition. A bucket before a
363    /// partition's first row gets a gap in the partition columns too, so,
364    /// as in pond-ts, such rows move to the partition with no value.
365    pub fn align(&self, sequence: &Sequence, options: Align) -> Result<PartitionedSeries> {
366        self.map(|g| g.align(sequence, options))
367    }
368
369    /// [`TimeSeries::materialize`] on each partition, with the partition
370    /// columns filled in on empty buckets (pond-ts does the same), so every
371    /// row stays in its partition.
372    pub fn materialize(
373        &self,
374        sequence: &Sequence,
375        options: Materialize,
376    ) -> Result<PartitionedSeries> {
377        let by = self.by.clone();
378        self.map(move |g| {
379            let out = g.materialize(sequence, options)?;
380            if g.is_empty() {
381                return Ok(out);
382            }
383            // Every row of a partition holds its key, so the key's column
384            // is the first row's value on every output row.
385            let first = vec![Some(0); out.len()];
386            let mut columns = out.columns().to_vec();
387            for name in &by {
388                let at = out.column_index("materialize", name)?;
389                let source = g.column(name)?;
390                columns[at] = Column::from_data(name.clone(), source.data().take(&first), false);
391            }
392            Ok(out.with_value_columns(columns))
393        })
394    }
395
396    /// [`TimeSeries::dedupe`] on each partition: rows are duplicates when
397    /// they share the key and the partition values.
398    pub fn dedupe(&self, keep: Keep) -> Result<PartitionedSeries> {
399        self.map(|g| g.dedupe(keep.clone()))
400    }
401
402    /// [`TimeSeries::smooth`] on each partition: each one is smoothed on
403    /// its own rows only.
404    pub fn smooth(&self, column: &str, smooth: &Smooth) -> Result<PartitionedSeries> {
405        self.map(|g| g.smooth(column, smooth))
406    }
407
408    /// [`TimeSeries::baseline`] on each partition.
409    pub fn baseline(
410        &self,
411        column: &str,
412        window: &str,
413        sigma: f64,
414        options: Rolling,
415    ) -> Result<PartitionedSeries> {
416        self.map(|g| g.baseline(column, window, sigma, options))
417    }
418
419    /// [`TimeSeries::baseline_named`] on each partition.
420    pub fn baseline_named(
421        &self,
422        column: &str,
423        window: &str,
424        sigma: f64,
425        options: Rolling,
426        names: [&str; 4],
427    ) -> Result<PartitionedSeries> {
428        self.map(|g| g.baseline_named(column, window, sigma, options, names))
429    }
430
431    /// [`TimeSeries::outliers`] on each partition: a row is an outlier
432    /// against its own partition's band. A partition left with no rows is
433    /// dropped.
434    pub fn outliers(
435        &self,
436        column: &str,
437        window: &str,
438        sigma: f64,
439        options: Rolling,
440    ) -> Result<PartitionedSeries> {
441        self.map(|g| g.outliers(column, window, sigma, options))
442    }
443
444    /// [`TimeSeries::scan`] on each partition: every partition starts from
445    /// its own copy of `init`, and `step`'s row index counts from 0 within
446    /// the partition. `step` is `Fn` (not `FnMut`) because partitions may
447    /// run on several cores.
448    pub fn scan<A>(
449        &self,
450        column: &str,
451        init: A,
452        step: impl Fn(A, f64, usize) -> (A, f64) + Sync + Send,
453    ) -> Result<PartitionedSeries>
454    where
455        A: Clone + Sync + Send,
456    {
457        self.map(|g| g.scan(column, init.clone(), &step))
458    }
459
460    /// [`TimeSeries::scan_to`] on each partition, as
461    /// [`PartitionedSeries::scan`].
462    pub fn scan_to<A>(
463        &self,
464        column: &str,
465        output: &str,
466        init: A,
467        step: impl Fn(A, f64, usize) -> (A, f64) + Sync + Send,
468    ) -> Result<PartitionedSeries>
469    where
470        A: Clone + Sync + Send,
471    {
472        self.map(|g| g.scan_to(column, output, init.clone(), &step))
473    }
474
475    /// [`TimeSeries::sample`] on each partition: a stride counts within
476    /// each partition, and a reservoir samples each one separately. Each
477    /// partition's reservoir gets its own seed, made from `seed` and the
478    /// partition's place in partition order, so partitions pick
479    /// independently and the same input and seed always give the same
480    /// rows (pond-ts draws from `Math.random`, so its picks differ each
481    /// run).
482    pub fn sample(&self, how: Sample) -> Result<PartitionedSeries> {
483        let Sample::Reservoir { size, seed } = how else {
484            return self.map(|g| g.sample(how));
485        };
486        // `map` hands each partition's own series to the closure, so its
487        // address finds its place.
488        let place: HashMap<usize, u64> = self
489            .parts
490            .iter()
491            .enumerate()
492            .map(|(i, (_, g))| (g as *const TimeSeries as usize, i as u64))
493            .collect();
494        self.map(|g| {
495            let i = place
496                .get(&(g as *const TimeSeries as usize))
497                .copied()
498                .unwrap_or(0);
499            let seed = seed ^ i.wrapping_mul(0x9E37_79B9_7F4A_7C15);
500            g.sample(Sample::Reservoir { size, seed })
501        })
502    }
503
504    /// The aggs for a rolling call: as given when appending (the partition
505    /// columns are kept anyway), otherwise with the partition columns
506    /// carried as outputs.
507    fn rolling_outputs(&self, aggs: &[Agg], options: Rolling) -> Vec<Agg> {
508        if options.append {
509            aggs.to_vec()
510        } else {
511            self.with_partition_outputs(aggs)
512        }
513    }
514
515    fn with_partition_outputs(&self, aggs: &[Agg]) -> Vec<Agg> {
516        let mut out = aggs.to_vec();
517        for col in &self.by {
518            if !aggs.iter().any(|a| &a.output == col) && !out.iter().any(|a| &a.output == col) {
519                out.push(Agg::new(col.clone(), col.clone(), Reducer::First));
520            }
521        }
522        out
523    }
524
525    /// `f` on every partition (or on the empty series when there are
526    /// none), and a zero-row series with the result's columns.
527    fn run<F>(&self, f: &F) -> Result<(Vec<(PartitionKey, TimeSeries)>, TimeSeries)>
528    where
529        F: Fn(&TimeSeries) -> Result<TimeSeries> + Sync + Send,
530    {
531        if self.parts.is_empty() {
532            let out = f(&self.empty)?;
533            // pond-ts gives the operation's result on the empty series; it
534            // has no rows to split, but keep any it made.
535            let empty = out.row_range(0..0);
536            let parts = if out.is_empty() {
537                Vec::new()
538            } else {
539                vec![(Vec::new(), out)]
540            };
541            return Ok((parts, empty));
542        }
543        let results = par::map(&self.parts, |(_, s)| f(s));
544        let mut parts = Vec::with_capacity(results.len());
545        for ((key, _), out) in self.parts.iter().zip(results) {
546            parts.push((key.clone(), out?));
547        }
548        let empty = parts[0].1.row_range(0..0);
549        Ok((parts, empty))
550    }
551}
552
553/// The rows of `series` grouped by the values of the columns at `by`, in
554/// first-row order.
555/// Refuse an array column among the partition columns `by`. pond-ts keys
556/// an array by its text (`String(arr)`), so `['a,b']` and `['a', 'b']`
557/// would share a partition.
558fn refuse_array_keys(call: &'static str, series: &TimeSeries, by: &[usize]) -> Result<()> {
559    match by
560        .iter()
561        .find(|&&c| series.columns()[c].kind() == ColumnKind::Array)
562    {
563        Some(&c) => Err(PondError::WrongKind {
564            call,
565            column: series.columns()[c].name().to_string(),
566            kind: "array",
567            reason: "partitioning by an array column is not supported".into(),
568        }),
569        None => Ok(()),
570    }
571}
572
573fn split(series: &TimeSeries, by: &[usize]) -> Vec<(PartitionKey, TimeSeries)> {
574    // One number per row that is equal exactly when the rows' values in
575    // every partition column are: the first column's value numbers, then
576    // each further column folded in pairwise.
577    let mut ids: Vec<u32> = Vec::new();
578    for (at, &c) in by.iter().enumerate() {
579        let next = value_ids(series.columns()[c].data());
580        ids = if at == 0 {
581            next
582        } else {
583            let mut pairs: HashMap<(u32, u32), u32> = HashMap::new();
584            ids.iter()
585                .zip(&next)
586                .map(|(&a, &b)| {
587                    let id = pairs.len() as u32;
588                    *pairs.entry((a, b)).or_insert(id)
589                })
590                .collect()
591        };
592    }
593    // The numbers are dense, so renumbering into first-row order is a
594    // lookup table, not a hash.
595    let mut part = vec![u32::MAX; ids.iter().max().map_or(0, |&m| m as usize + 1)];
596    let mut rows: Vec<Vec<usize>> = Vec::new();
597    for (i, &id) in ids.iter().enumerate() {
598        let slot = &mut part[id as usize];
599        if *slot == u32::MAX {
600            *slot = rows.len() as u32;
601            rows.push(Vec::new());
602        }
603        rows[*slot as usize].push(i);
604    }
605    par::map(&rows, |rows| {
606        let key = by
607            .iter()
608            .map(|&c| value_at(series.columns()[c].data(), rows[0]))
609            .collect();
610        (key, series.take_rows(rows))
611    })
612}
613
614/// A small integer per row that is equal exactly when the values are
615/// (a gap is 0).
616fn value_ids(data: &ColumnData) -> Vec<u32> {
617    fn ids<K: std::hash::Hash + Eq>(values: impl Iterator<Item = Option<K>>) -> Vec<u32> {
618        let mut seen: HashMap<K, u32> = HashMap::new();
619        values
620            .map(|v| match v {
621                None => 0,
622                Some(v) => {
623                    let next = seen.len() as u32 + 1;
624                    *seen.entry(v).or_insert(next)
625                }
626            })
627            .collect()
628    }
629    match data {
630        // `+ 0.0` turns -0 into 0, so they are one partition.
631        ColumnData::Number(c) => ids(c.iter().map(|v| v.map(|v| (v + 0.0).to_bits()))),
632        ColumnData::Decimal(c) => ids(c.iter().map(|v| v.map(|d| d.value))),
633        ColumnData::String(c) => ids(c.iter()),
634        ColumnData::Boolean(c) => ids(c.iter()),
635        ColumnData::Array(_) => unreachable!("partition_by refuses array columns"),
636    }
637}
638
639fn value_at(data: &ColumnData, i: usize) -> Option<Value> {
640    match data {
641        ColumnData::Number(c) => c.get(i).map(|v| Value::Number(v + 0.0)),
642        ColumnData::Decimal(c) => c.get(i).map(Value::Decimal),
643        ColumnData::String(c) => c.get(i).map(|v| Value::String(v.to_string())),
644        ColumnData::Boolean(c) => c.get(i).map(Value::Boolean),
645        ColumnData::Array(_) => unreachable!("partition_by refuses array columns"),
646    }
647}
648
649/// True when every row of `series` still has `key` in the partition
650/// columns at `by`.
651fn keeps_key(series: &TimeSeries, by: &[usize], key: &[Option<Value>]) -> bool {
652    by.iter().zip(key).all(|(&c, want)| {
653        let n = series.len();
654        match (series.columns()[c].data(), want) {
655            // `==` on numbers: -0 and 0 are one key.
656            (ColumnData::Number(c), Some(Value::Number(v))) => (0..n).all(|i| c.get(i) == Some(*v)),
657            (ColumnData::Decimal(c), Some(Value::Decimal(v))) => {
658                (0..n).all(|i| c.get(i) == Some(*v))
659            }
660            (ColumnData::String(c), Some(Value::String(v))) => {
661                (0..n).all(|i| c.get(i) == Some(v.as_str()))
662            }
663            (ColumnData::Boolean(c), Some(Value::Boolean(v))) => {
664                (0..n).all(|i| c.get(i) == Some(*v))
665            }
666            (data, None) => data.missing_count() == n,
667            _ => false,
668        }
669    })
670}
671
672fn first_key(series: &TimeSeries) -> (i64, i64) {
673    let key = series.key();
674    (key.begin()[0], key.end().map_or(0, |e| e[0]))
675}
676
677fn series_of(parts: &[(PartitionKey, TimeSeries)]) -> Vec<&TimeSeries> {
678    parts.iter().map(|(_, s)| s).collect()
679}
680
681/// The partitions as one series: rows sorted by key, ties in partition
682/// order then row order (pond-ts `TimeSeries.concat`).
683fn merge(series: &[&TimeSeries], empty: &TimeSeries) -> Result<TimeSeries> {
684    let series: Vec<&TimeSeries> = series.iter().copied().filter(|s| !s.is_empty()).collect();
685    let Some(head) = series.first() else {
686        return Ok(empty.clone());
687    };
688    if series.len() == 1 {
689        return Ok((*head).clone());
690    }
691    if let Some(why) = mismatch(&series) {
692        return Err(PondError::InvalidData {
693            call: "PartitionedSeries",
694            message: format!(
695                "partitions came back with different columns ({why}); every partition must give the same columns"
696            ),
697        });
698    }
699    Ok(stack(head.name(), &series))
700}
701
702/// Why `series` cannot be stacked: the first difference from the first
703/// series in key, column names, kinds, decimal scales or label kinds.
704fn mismatch(series: &[&TimeSeries]) -> Option<String> {
705    let head = series[0];
706    for (i, s) in series.iter().enumerate().skip(1) {
707        if s.key_name() != head.key_name() || s.key().kind() != head.key().kind() {
708            return Some(format!(
709                "series {i} is keyed by '{}' ({:?}), series 0 by '{}' ({:?})",
710                s.key_name(),
711                s.key().kind(),
712                head.key_name(),
713                head.key().kind()
714            ));
715        }
716        if let (Key::Interval(a), Key::Interval(b)) = (head.key(), s.key())
717            && std::mem::discriminant(&a.label) != std::mem::discriminant(&b.label)
718        {
719            return Some(format!(
720                "series {i} has {} interval labels, series 0 {}",
721                label_kind(&b.label),
722                label_kind(&a.label)
723            ));
724        }
725        if s.columns().len() != head.columns().len() {
726            return Some(format!(
727                "series {i} has {} columns, series 0 has {}",
728                s.columns().len(),
729                head.columns().len()
730            ));
731        }
732        for (c, (a, b)) in head.columns().iter().zip(s.columns()).enumerate() {
733            if a.name() != b.name() || a.kind() != b.kind() {
734                return Some(format!(
735                    "column {c} is '{}' ({:?}) in series {i} but '{}' ({:?}) in series 0",
736                    b.name(),
737                    b.kind(),
738                    a.name(),
739                    a.kind()
740                ));
741            }
742            if let (ColumnData::Decimal(x), ColumnData::Decimal(y)) = (a.data(), b.data())
743                && x.scale() != y.scale()
744            {
745                return Some(format!(
746                    "decimal column '{}' has scale {} in series {i} but {} in series 0",
747                    a.name(),
748                    y.scale(),
749                    x.scale()
750                ));
751            }
752        }
753    }
754    None
755}
756
757fn label_kind(labels: &Labels) -> &'static str {
758    match labels {
759        Labels::Number(_) => "number",
760        Labels::String(_) => "text",
761    }
762}
763
764/// `series` (already checked by [`mismatch`]) as one series named `name`:
765/// rows sorted by key, ties in series order then row order. A column is
766/// required only when it is required in every input.
767fn stack(name: &str, series: &[&TimeSeries]) -> TimeSeries {
768    let head = series[0];
769    let picks = merge_order(series);
770    let keys: Vec<&Key> = series.iter().map(|s| s.key()).collect();
771    let key = Key::gather(&keys, &picks);
772    let columns: Vec<usize> = (0..head.columns().len()).collect();
773    let columns = par::map(&columns, |&c| {
774        let sources: Vec<&ColumnData> = series.iter().map(|s| s.columns()[c].data()).collect();
775        let required = series.iter().all(|s| s.columns()[c].is_required());
776        Column::from_data(
777            head.columns()[c].name().to_string(),
778            ColumnData::gather(&sources, &picks),
779            required,
780        )
781    });
782    TimeSeries::from_parts(name.to_string(), head.key_name().to_string(), key, columns)
783}
784
785impl TimeSeries {
786    /// The rows of several series as one, sorted by key (pond-ts
787    /// `TimeSeries.concat`): rows that share a key keep the order of
788    /// `series`, then their order within it. The result takes the first
789    /// series' name.
790    ///
791    /// Every series must have the same key (name and kind) and the same
792    /// columns in the same order (name and kind; decimals also the same
793    /// scale, interval labels the same kind). A column is required in the
794    /// result only when it is required in every input.
795    ///
796    /// ```
797    /// # use pond_rs::{Column, TimeSeries};
798    /// let a = TimeSeries::new("cpu", vec![0, 2000], vec![Column::required("v", vec![1.0, 3.0])])?;
799    /// let b = TimeSeries::new("cpu", vec![1000, 2000], vec![Column::required("v", vec![2.0, 4.0])])?;
800    /// let both = TimeSeries::concat(&[&a, &b])?;
801    /// assert_eq!(both.key().begin(), &[0, 1000, 2000, 2000]);
802    /// assert_eq!(both.column("v")?.numbers()?.values(), &[1.0, 2.0, 3.0, 4.0]);
803    /// # Ok::<(), pond_rs::PondError>(())
804    /// ```
805    pub fn concat(series: &[&TimeSeries]) -> Result<TimeSeries> {
806        const CALL: &str = "TimeSeries::concat";
807        let Some(head) = series.first() else {
808            return Err(PondError::InvalidData {
809                call: CALL,
810                message: "needs at least one series".into(),
811            });
812        };
813        if let Some(why) = mismatch(series) {
814            return Err(PondError::InvalidData {
815                call: CALL,
816                message: format!("the series do not match: {why}"),
817            });
818        }
819        if series.len() == 1 {
820            return Ok((*head).clone());
821        }
822        Ok(stack(head.name(), series))
823    }
824}
825
826/// The order to take rows in: by key (begin, then end), rows with the same
827/// key in partition order, then row order. That is a stable sort of the
828/// partitions laid one after another, done as a radix sort on the key
829/// numbers (each pass is a stable counting sort), which costs a few passes
830/// over the rows instead of a comparison sort's log N.
831fn merge_order(series: &[&TimeSeries]) -> Vec<(u32, u32)> {
832    let total: usize = series.iter().map(|s| s.len()).sum();
833    let mut picks: Vec<(u32, u32)> = Vec::with_capacity(total);
834    for (p, s) in series.iter().enumerate() {
835        picks.extend((0..s.len() as u32).map(|r| (p as u32, r)));
836    }
837    // Flipping the sign bit makes unsigned order match signed order.
838    let flat = |part: &dyn Fn(&Key) -> Option<&[i64]>| -> Option<Vec<u64>> {
839        let mut out = Vec::with_capacity(total);
840        for s in series {
841            out.extend(part(s.key())?.iter().map(|&v| (v as u64) ^ (1 << 63)));
842        }
843        Some(out)
844    };
845    let mut order: Vec<u32> = (0..total as u32).collect();
846    // Least significant first: end, then begin.
847    if let Some(end) = flat(&|k| k.end()) {
848        radix_sort(&mut order, &end);
849    }
850    radix_sort(
851        &mut order,
852        &flat(&|k| Some(k.begin())).expect("every key has a begin"),
853    );
854    order.into_iter().map(|i| picks[i as usize]).collect()
855}
856
857/// Stable sort of `order` (indexes into `keys`) by `keys`, 16 bits at a
858/// time, skipping the 16-bit digits where every key is the same.
859fn radix_sort(order: &mut Vec<u32>, keys: &[u64]) {
860    let Some(&first) = keys.first() else {
861        return;
862    };
863    let varying = keys.iter().fold(0, |acc, &k| acc | (k ^ first));
864    let mut buf = vec![0u32; order.len()];
865    for shift in (0..64).step_by(16) {
866        if (varying >> shift) & 0xFFFF == 0 {
867            continue;
868        }
869        let digit = |i: u32| ((keys[i as usize] >> shift) & 0xFFFF) as usize;
870        let mut start = vec![0usize; 1 << 16];
871        for &i in order.iter() {
872            start[digit(i)] += 1;
873        }
874        let mut at = 0;
875        for slot in start.iter_mut() {
876            let count = *slot;
877            *slot = at;
878            at += count;
879        }
880        for &i in order.iter() {
881            let d = digit(i);
882            buf[start[d]] = i;
883            start[d] += 1;
884        }
885        std::mem::swap(order, &mut buf);
886    }
887}
888
889#[cfg(test)]
890mod tests {
891    use super::*;
892    use crate::series::{MAX_TIME_MS, TimeRangeKeys};
893
894    fn hosts() -> TimeSeries {
895        TimeSeries::new(
896            "h",
897            vec![0, 0, 1000, 1000, 2000],
898            vec![
899                Column::optional(
900                    "host",
901                    vec![Some("b"), Some("a"), Some("a"), Some("b"), None],
902                ),
903                Column::optional(
904                    "v",
905                    vec![Some(1.0), Some(10.0), Some(12.0), None, Some(5.0)],
906                ),
907            ],
908        )
909        .unwrap()
910    }
911
912    #[test]
913    fn splits_in_first_row_order_with_a_gap_partition() {
914        let p = hosts().partition_by(&["host"]).unwrap();
915        let keys: Vec<_> = p.iter().map(|(k, s)| (k.to_vec(), s.len())).collect();
916        assert_eq!(
917            keys,
918            vec![
919                (vec![Some(Value::String("b".into()))], 2),
920                (vec![Some(Value::String("a".into()))], 2),
921                (vec![None], 1),
922            ]
923        );
924        // No operation yet: collect gives the input back as it was.
925        assert_eq!(p.collect().unwrap(), hosts());
926    }
927
928    #[test]
929    fn merge_puts_ties_in_partition_order() {
930        let p = hosts().partition_by(&["host"]).unwrap();
931        let out = p.map(|g| Ok(g.clone())).unwrap().collect().unwrap();
932        let hosts: Vec<_> = out
933            .column("host")
934            .unwrap()
935            .strings()
936            .unwrap()
937            .iter()
938            .collect();
939        // At t=1000 the input has a then b; partition order is b then a.
940        assert_eq!(
941            hosts,
942            vec![Some("b"), Some("a"), Some("b"), Some("a"), None]
943        );
944    }
945
946    #[test]
947    fn merge_order_is_a_stable_sort() {
948        // Negative times, keys far apart (every 16-bit digit varies),
949        // repeated keys within and across partitions, and time ranges with
950        // the same begin and different ends.
951        let mut x = 0x9e37_79b9_7f4a_7c15u64;
952        let mut next = move || {
953            x ^= x << 13;
954            x ^= x >> 7;
955            x ^= x << 17;
956            x
957        };
958        let pick = [
959            -MAX_TIME_MS,
960            -5_000_000_000_000,
961            -1,
962            0,
963            1,
964            7,
965            1 << 40,
966            MAX_TIME_MS - 2,
967        ];
968        for ranges in [false, true] {
969            let parts: Vec<TimeSeries> = (0..6)
970                .map(|_| {
971                    let n = (next() % 40) as usize;
972                    let mut keys: Vec<(i64, i64)> = (0..n)
973                        .map(|_| {
974                            let b = pick[(next() % 8) as usize];
975                            let e = b.saturating_add((next() % 3) as i64);
976                            (b, if ranges { e } else { b })
977                        })
978                        .collect();
979                    keys.sort();
980                    let (begin, end): (Vec<i64>, Vec<i64>) = keys.into_iter().unzip();
981                    let cols = vec![Column::required("v", vec![0.0; begin.len()])];
982                    if ranges {
983                        TimeSeries::from_time_ranges("t", TimeRangeKeys { begin, end }, cols)
984                    } else {
985                        TimeSeries::new("t", begin, cols)
986                    }
987                    .unwrap()
988                })
989                .collect();
990            let refs: Vec<&TimeSeries> = parts.iter().collect();
991            let mut want: Vec<(u32, u32)> = Vec::new();
992            for (p, s) in parts.iter().enumerate() {
993                want.extend((0..s.len() as u32).map(|r| (p as u32, r)));
994            }
995            want.sort_by_key(|&(p, r)| {
996                let k = parts[p as usize].key();
997                (k.begin()[r as usize], k.end().map_or(0, |e| e[r as usize]))
998            });
999            assert_eq!(merge_order(&refs), want);
1000        }
1001    }
1002
1003    #[test]
1004    fn negative_zero_is_zero() {
1005        let s = TimeSeries::new(
1006            "z",
1007            vec![0, 1],
1008            vec![Column::required("k", vec![0.0, -0.0])],
1009        )
1010        .unwrap();
1011        assert_eq!(s.partition_by(&["k"]).unwrap().len(), 1);
1012    }
1013
1014    #[test]
1015    fn changed_partition_values_split_again() {
1016        let s = TimeSeries::new(
1017            "s",
1018            vec![0, 1, 2, 3],
1019            vec![Column::required("k", vec![1.0, 2.0, 1.0, 2.0])],
1020        )
1021        .unwrap();
1022        let p = s.partition_by(&["k"]).unwrap();
1023        // Map every key to 7: one partition afterwards.
1024        let q = p.map(|g| g.map_numbers("k", |_| 7.0)).unwrap();
1025        assert_eq!(q.len(), 1);
1026        assert_eq!(q.iter().next().unwrap().0, &[Some(Value::Number(7.0))]);
1027        assert_eq!(q.collect().unwrap().len(), 4);
1028    }
1029
1030    #[test]
1031    fn errors() {
1032        let s = hosts();
1033        assert!(s.partition_by(&[]).is_err());
1034        assert!(matches!(
1035            s.partition_by(&["nope"]).unwrap_err(),
1036            PondError::UnknownColumn { .. }
1037        ));
1038        let p = s.partition_by(&["host"]).unwrap();
1039        // Dropping the partition column: an error staying partitioned, fine
1040        // when collecting straight away.
1041        assert!(p.map(|g| g.select(&["v"])).is_err());
1042        assert_eq!(p.apply(|g| g.select(&["v"])).unwrap().len(), 5);
1043        // A failing partition stops the call.
1044        let err = p
1045            .map(|g| {
1046                if g.len() == 1 {
1047                    g.select(&["zz"])
1048                } else {
1049                    Ok(g.clone())
1050                }
1051            })
1052            .unwrap_err();
1053        assert!(matches!(err, PondError::UnknownColumn { .. }));
1054    }
1055
1056    #[test]
1057    fn different_columns_cannot_merge() {
1058        let p = hosts().partition_by(&["host"]).unwrap();
1059        let err = p
1060            .apply(|g| {
1061                if g.len() == 1 {
1062                    g.select(&["v"])
1063                } else {
1064                    g.select(&["host"])
1065                }
1066            })
1067            .unwrap_err();
1068        assert!(err.to_string().contains("different columns"), "{err}");
1069    }
1070
1071    #[test]
1072    fn empty_series() {
1073        let s = hosts().row_range(0..0);
1074        let p = s.partition_by(&["host"]).unwrap();
1075        assert!(p.is_empty());
1076        let out = p.diff(&["v"]).unwrap().collect().unwrap();
1077        assert!(out.is_empty());
1078        assert_eq!(out.columns().len(), 2);
1079    }
1080
1081    #[test]
1082    fn rows_made_from_an_empty_series_are_split() {
1083        // pond-ts runs the operation on the empty series and splits what
1084        // comes back; `map` can make rows from nothing.
1085        let p = hosts().row_range(0..0).partition_by(&["host"]).unwrap();
1086        let made = p.map(|_| Ok(hosts())).unwrap();
1087        assert_eq!(made.len(), 3);
1088        let a = made.get(&[Some(Value::String("a".into()))]).unwrap();
1089        assert_eq!(a.len(), 2);
1090        let d = made.diff(&["v"]).unwrap();
1091        let a = d.get(&[Some(Value::String("a".into()))]).unwrap();
1092        assert_eq!(a.column("v").unwrap().numbers().unwrap().get(1), Some(2.0));
1093    }
1094
1095    #[test]
1096    fn group_by_gives_keys_and_series() {
1097        let groups = hosts().group_by(&["host"]).unwrap();
1098        assert_eq!(groups.len(), 3);
1099        assert_eq!(
1100            groups[1].1.column("v").unwrap().numbers().unwrap().get(1),
1101            Some(12.0)
1102        );
1103    }
1104
1105    /// Partitions run on several threads; results are the same bits on
1106    /// one thread and on many.
1107    #[cfg(all(feature = "parallel", not(target_arch = "wasm32")))]
1108    #[test]
1109    fn same_bits_on_one_and_many_threads() {
1110        let mut x = 0x9e37_79b9_7f4a_7c15u64;
1111        let mut next = || {
1112            x ^= x << 13;
1113            x ^= x >> 7;
1114            x ^= x << 17;
1115            x
1116        };
1117        let (symbols, bars) = (40, 300);
1118        let mut time = Vec::new();
1119        let mut sym = Vec::new();
1120        let mut v = Vec::new();
1121        for b in 0..bars {
1122            for s in 0..symbols {
1123                time.push(b as i64 * 60_000);
1124                sym.push(format!("s{s}"));
1125                let r = next();
1126                v.push((r % 13 != 0).then(|| (r % 100_003) as f64 / 7.0));
1127            }
1128        }
1129        let series = TimeSeries::new(
1130            "panel",
1131            time,
1132            vec![Column::required("sym", sym), Column::optional("v", v)],
1133        )
1134        .unwrap();
1135        let aggs = [
1136            Agg::of("v", Reducer::Avg),
1137            Agg::new("sd", "v", Reducer::Stdev),
1138        ];
1139        let run = |threads: usize| {
1140            rayon::ThreadPoolBuilder::new()
1141                .num_threads(threads)
1142                .build()
1143                .unwrap()
1144                .install(|| {
1145                    let p = series.partition_by(&["sym"]).unwrap();
1146                    let a = p.rolling("10m", &aggs).unwrap().collect().unwrap();
1147                    let b = p
1148                        .fill_all(Fill::Linear)
1149                        .unwrap()
1150                        .cumulative(&[("v", Reducer::Sum)])
1151                        .unwrap()
1152                        .collect()
1153                        .unwrap();
1154                    [a, b]
1155                        .iter()
1156                        .flat_map(|s| {
1157                            s.columns()
1158                                .iter()
1159                                .filter_map(|c| c.numbers().ok())
1160                                .flat_map(|c| c.values().iter().map(|v| v.to_bits()))
1161                                .collect::<Vec<u64>>()
1162                        })
1163                        .collect::<Vec<u64>>()
1164                })
1165        };
1166        let one = run(1);
1167        assert!(one.len() > 30_000);
1168        assert_eq!(one, run(4));
1169        assert_eq!(one, run(7));
1170    }
1171
1172    #[test]
1173    fn reservoir_and_scan_run_per_partition() {
1174        // Two hosts, 20 rows each, interleaved in time.
1175        let n = 40;
1176        let s = TimeSeries::new(
1177            "r",
1178            (0..n as i64).map(|i| i * 1000).collect(),
1179            vec![
1180                Column::required(
1181                    "host",
1182                    (0..n)
1183                        .map(|i| if i % 2 == 0 { "a" } else { "b" })
1184                        .collect::<Vec<_>>(),
1185                ),
1186                Column::required("v", (0..n).map(|i| i as f64).collect::<Vec<_>>()),
1187            ],
1188        )
1189        .unwrap();
1190        let p = s.partition_by(&["host"]).unwrap();
1191        let how = Sample::Reservoir { size: 5, seed: 3 };
1192        let picked = p.sample(how).unwrap();
1193        // Partitions are in first-row order after sampling: look them up.
1194        let v_of = |g: &TimeSeries| -> Vec<f64> {
1195            g.column("v")
1196                .unwrap()
1197                .numbers()
1198                .unwrap()
1199                .iter()
1200                .flatten()
1201                .collect()
1202        };
1203        let key = |h: &str| vec![Some(Value::String(h.into()))];
1204        let rows = [
1205            v_of(picked.get(&key("a")).unwrap()),
1206            v_of(picked.get(&key("b")).unwrap()),
1207        ];
1208        assert_eq!(picked.len(), 2);
1209        // Five from each partition, each from its own rows; the first
1210        // partition uses `seed` itself, and the second picks other
1211        // positions (its own seed). The same seed gives the same rows.
1212        assert!(rows.iter().all(|r| r.len() == 5));
1213        assert!(rows[0].iter().all(|v| (*v as usize).is_multiple_of(2)));
1214        assert!(rows[1].iter().all(|v| *v as usize % 2 == 1));
1215        let positions = |r: &[f64]| r.iter().map(|v| (*v as usize) / 2).collect::<Vec<_>>();
1216        let alone = v_of(&p.get(&key("a")).unwrap().sample(how).unwrap());
1217        assert_eq!(rows[0], alone);
1218        assert_ne!(positions(&rows[0]), positions(&rows[1]));
1219        assert_eq!(p.sample(how).unwrap().collect(), picked.collect());
1220
1221        // A running count restarts in each partition, from its own `init`.
1222        let counted = p
1223            .scan_to("v", "k", 100.0, |acc, _, i| (acc + 1.0, acc + i as f64))
1224            .unwrap();
1225        for (_, g) in counted.iter() {
1226            let k: Vec<f64> = g
1227                .column("k")
1228                .unwrap()
1229                .numbers()
1230                .unwrap()
1231                .iter()
1232                .flatten()
1233                .collect();
1234            assert_eq!(
1235                k,
1236                (0..20).map(|i| 100.0 + 2.0 * i as f64).collect::<Vec<_>>()
1237            );
1238        }
1239    }
1240}