Skip to main content

laddu_runtime/
query.rs

1use std::sync::Arc;
2
3use laddu_compile::CompiledModel;
4use laddu_data::{
5    LadduDataError, LadduDataResult,
6    data::{Dataset, EventBatch},
7    io::{EventBatchIter, EventSource, ReadPlan, SourceCapabilities},
8    schema::Schema,
9};
10use laddu_expr::{Expr, ExprShape, ValueKind};
11use num::complex::Complex64;
12use serde::{Deserialize, Deserializer, Serialize};
13
14use crate::{Execution, PreparedModel, RuntimeError, RuntimeResult};
15
16/// Comparison operation used by a dataset predicate.
17#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
18pub enum Comparison {
19    /// Less than.
20    Lt,
21    /// Less than or equal to.
22    Le,
23    /// Greater than.
24    Gt,
25    /// Greater than or equal to.
26    Ge,
27    /// Equal to.
28    Eq,
29    /// Not equal to.
30    Ne,
31}
32
33/// Determines which endpoints are included by an interval predicate.
34#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
35pub enum IntervalClosure {
36    /// Exclude both endpoints.
37    Open,
38    /// Include only the lower endpoint.
39    LeftClosed,
40    /// Include only the upper endpoint.
41    RightClosed,
42    /// Include both endpoints.
43    #[default]
44    Closed,
45}
46
47/// Boolean expression used to select events from a dataset.
48#[derive(Clone, Debug)]
49pub enum Predicate {
50    /// Compare two scalar expressions.
51    Compare {
52        /// Left-hand expression.
53        lhs: Expr,
54        /// Comparison operation.
55        op: Comparison,
56        /// Right-hand expression.
57        rhs: Expr,
58    },
59    /// Require both child predicates to hold.
60    And(Box<Self>, Box<Self>),
61    /// Require either child predicate to hold.
62    Or(Box<Self>, Box<Self>),
63    /// Negate a predicate.
64    Not(Box<Self>),
65    /// Test whether a value lies between two bounds.
66    Between {
67        /// Expression whose value is tested.
68        value: Expr,
69        /// Lower bound expression.
70        lower: Expr,
71        /// Upper bound expression.
72        upper: Expr,
73        /// Endpoint inclusion policy.
74        closure: IntervalClosure,
75    },
76}
77
78impl Predicate {
79    /// Creates a comparison predicate.
80    pub fn compare(lhs: impl Into<Expr>, op: Comparison, rhs: impl Into<Expr>) -> Self {
81        Self::Compare {
82            lhs: lhs.into(),
83            op,
84            rhs: rhs.into(),
85        }
86    }
87
88    /// Creates a less-than predicate.
89    pub fn lt(lhs: impl Into<Expr>, rhs: impl Into<Expr>) -> Self {
90        Self::compare(lhs, Comparison::Lt, rhs)
91    }
92    /// Creates a less-than-or-equal predicate.
93    pub fn le(lhs: impl Into<Expr>, rhs: impl Into<Expr>) -> Self {
94        Self::compare(lhs, Comparison::Le, rhs)
95    }
96    /// Creates a greater-than predicate.
97    pub fn gt(lhs: impl Into<Expr>, rhs: impl Into<Expr>) -> Self {
98        Self::compare(lhs, Comparison::Gt, rhs)
99    }
100    /// Creates a greater-than-or-equal predicate.
101    pub fn ge(lhs: impl Into<Expr>, rhs: impl Into<Expr>) -> Self {
102        Self::compare(lhs, Comparison::Ge, rhs)
103    }
104    /// Creates an equality predicate.
105    pub fn eq(lhs: impl Into<Expr>, rhs: impl Into<Expr>) -> Self {
106        Self::compare(lhs, Comparison::Eq, rhs)
107    }
108    /// Creates an inequality predicate.
109    pub fn ne(lhs: impl Into<Expr>, rhs: impl Into<Expr>) -> Self {
110        Self::compare(lhs, Comparison::Ne, rhs)
111    }
112    /// Combines this predicate with `rhs` using logical AND.
113    pub fn and(self, rhs: Self) -> Self {
114        Self::And(Box::new(self), Box::new(rhs))
115    }
116    /// Combines this predicate with `rhs` using logical OR.
117    pub fn or(self, rhs: Self) -> Self {
118        Self::Or(Box::new(self), Box::new(rhs))
119    }
120    /// Creates a closed-interval predicate.
121    pub fn between(value: impl Into<Expr>, lower: impl Into<Expr>, upper: impl Into<Expr>) -> Self {
122        Self::between_with(value, lower, upper, IntervalClosure::Closed)
123    }
124    /// Creates an interval predicate with an explicit endpoint policy.
125    pub fn between_with(
126        value: impl Into<Expr>,
127        lower: impl Into<Expr>,
128        upper: impl Into<Expr>,
129        closure: IntervalClosure,
130    ) -> Self {
131        Self::Between {
132            value: value.into(),
133            lower: lower.into(),
134            upper: upper.into(),
135            closure,
136        }
137    }
138}
139
140impl std::ops::Not for Predicate {
141    type Output = Self;
142    fn not(self) -> Self::Output {
143        Self::Not(Box::new(self))
144    }
145}
146
147/// Validated, monotonically increasing bin edges.
148#[derive(Clone, Debug, PartialEq, Serialize)]
149#[serde(transparent)]
150pub struct BinSpec {
151    edges: Arc<[f64]>,
152}
153
154impl<'de> Deserialize<'de> for BinSpec {
155    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
156    where
157        D: Deserializer<'de>,
158    {
159        let edges = Vec::<f64>::deserialize(deserializer)?;
160        Self::edges(edges).map_err(serde::de::Error::custom)
161    }
162}
163
164impl BinSpec {
165    /// Creates `count` uniformly spaced bins spanning `[min, max]`.
166    ///
167    /// # Errors
168    ///
169    /// Returns [`RuntimeError`] when `count` is zero or the bounds are
170    /// non-finite or not increasing.
171    pub fn uniform(count: usize, min: f64, max: f64) -> RuntimeResult<Self> {
172        if count == 0 || !min.is_finite() || !max.is_finite() || min >= max {
173            return Err(query_error(
174                "uniform bins require a positive count and finite min < max",
175            ));
176        }
177        let width = (max - min) / count as f64;
178        Self::edges((0..=count).map(|i| min + i as f64 * width))
179    }
180
181    /// Creates bins from explicit, strictly increasing finite edges.
182    ///
183    /// # Errors
184    ///
185    /// Returns [`RuntimeError`] when fewer than two edges are supplied or an
186    /// edge is non-finite or not strictly increasing.
187    pub fn edges(edges: impl IntoIterator<Item = f64>) -> RuntimeResult<Self> {
188        let edges: Vec<_> = edges.into_iter().collect();
189        if edges.len() < 2
190            || edges.iter().any(|x| !x.is_finite())
191            || edges.windows(2).any(|w| w[0] >= w[1])
192        {
193            return Err(query_error(
194                "bin edges must contain at least two finite, strictly increasing values",
195            ));
196        }
197        Ok(Self {
198            edges: edges.into(),
199        })
200    }
201
202    /// Returns the number of bins.
203    pub fn bin_count(&self) -> usize {
204        self.edges.len() - 1
205    }
206    /// Returns the validated bin edges.
207    pub fn edges_slice(&self) -> &[f64] {
208        &self.edges
209    }
210
211    fn index(&self, value: f64) -> Option<usize> {
212        if !value.is_finite() || value < self.edges[0] || value > *self.edges.last()? {
213            return None;
214        }
215        if value == *self.edges.last()? {
216            return Some(self.bin_count() - 1);
217        }
218        let upper = self.edges.partition_point(|edge| *edge <= value);
219        upper
220            .checked_sub(1)
221            .filter(|index| *index < self.bin_count())
222    }
223}
224
225/// A lazily filtered dataset corresponding to one bin.
226#[derive(Clone)]
227pub struct DatasetBin {
228    index: usize,
229    lower: f64,
230    upper: f64,
231    dataset: Dataset,
232}
233
234impl DatasetBin {
235    /// Returns the zero-based bin index.
236    pub fn index(&self) -> usize {
237        self.index
238    }
239    /// Returns the bin's lower edge.
240    pub fn lower(&self) -> f64 {
241        self.lower
242    }
243    /// Returns the bin's upper edge.
244    pub fn upper(&self) -> f64 {
245        self.upper
246    }
247    /// Returns the dataset containing events in this bin.
248    pub fn dataset(&self) -> &Dataset {
249        &self.dataset
250    }
251    /// Consumes the bin and returns its dataset.
252    pub fn into_dataset(self) -> Dataset {
253        self.dataset
254    }
255}
256
257/// Expression-based query operations for datasets.
258pub trait DatasetExprExt {
259    /// Evaluates a scalar expression for every event.
260    ///
261    /// # Errors
262    ///
263    /// Returns [`RuntimeError`] when compilation, dataset reading, or
264    /// evaluation fails, or the expression is not scalar.
265    fn evaluate_expr(&self, expr: &Expr, execution: &Execution) -> RuntimeResult<Vec<Complex64>>;
266    /// Evaluates a real scalar expression for every event.
267    ///
268    /// # Errors
269    ///
270    /// Returns [`RuntimeError`] when compilation, dataset reading, or
271    /// evaluation fails, or the expression is not real scalar-valued.
272    fn evaluate_real(&self, expr: &Expr, execution: &Execution) -> RuntimeResult<Vec<f64>>;
273    /// Creates a lazily filtered dataset containing events that satisfy `predicate`.
274    ///
275    /// # Errors
276    ///
277    /// Returns [`RuntimeError`] when predicate compilation or evaluation
278    /// fails, or its expression is not real scalar-valued.
279    fn select(&self, predicate: &Predicate, execution: &Execution) -> RuntimeResult<Dataset>;
280    /// Splits the dataset into lazy datasets according to an expression and bin specification.
281    ///
282    /// # Errors
283    ///
284    /// Returns [`RuntimeError`] when expression compilation or evaluation
285    /// fails, or the expression is not real scalar-valued.
286    fn bin_by(
287        &self,
288        expr: &Expr,
289        bins: BinSpec,
290        execution: &Execution,
291    ) -> RuntimeResult<Vec<DatasetBin>>;
292}
293
294impl DatasetExprExt for Dataset {
295    fn evaluate_expr(&self, expr: &Expr, execution: &Execution) -> RuntimeResult<Vec<Complex64>> {
296        let query = QueryExpr::prepare(expr, execution, false)?;
297        let mut output = Vec::new();
298        for batch in self.batches().map_err(data_error)? {
299            output.extend(query.evaluate_batch(&batch.map_err(data_error)?)?);
300        }
301        Ok(output)
302    }
303
304    fn evaluate_real(&self, expr: &Expr, execution: &Execution) -> RuntimeResult<Vec<f64>> {
305        let query = QueryExpr::prepare(expr, execution, true)?;
306        let mut output = Vec::new();
307        for batch in self.batches().map_err(data_error)? {
308            output.extend(
309                query
310                    .evaluate_batch(&batch.map_err(data_error)?)?
311                    .into_iter()
312                    .map(|v| v.re),
313            );
314        }
315        Ok(output)
316    }
317
318    fn select(&self, predicate: &Predicate, execution: &Execution) -> RuntimeResult<Dataset> {
319        let compiled = CompiledPredicate::prepare(predicate, execution)?;
320        Ok(self.with_derived_source(QuerySource {
321            source: self.clone(),
322            filter: QueryFilter::Predicate(Arc::new(compiled)),
323        }))
324    }
325
326    fn bin_by(
327        &self,
328        expr: &Expr,
329        bins: BinSpec,
330        execution: &Execution,
331    ) -> RuntimeResult<Vec<DatasetBin>> {
332        let query = Arc::new(QueryExpr::prepare(expr, execution, true)?);
333        Ok((0..bins.bin_count())
334            .map(|index| DatasetBin {
335                index,
336                lower: bins.edges[index],
337                upper: bins.edges[index + 1],
338                dataset: self.with_derived_source(QuerySource {
339                    source: self.clone(),
340                    filter: QueryFilter::Bin {
341                        query: Arc::clone(&query),
342                        bins: bins.clone(),
343                        index,
344                    },
345                }),
346            })
347            .collect())
348    }
349}
350
351struct QueryExpr {
352    model: PreparedModel,
353    params: laddu_expr::parameters::ParamValues,
354}
355
356impl QueryExpr {
357    fn prepare(expr: &Expr, execution: &Execution, require_real: bool) -> RuntimeResult<Self> {
358        if expr.shape().map_err(|e| query_error(e.to_string()))? != ExprShape::Scalar {
359            return Err(query_error("dataset expressions must be scalar"));
360        }
361        let compiled = CompiledModel::from_expr(expr).map_err(|e| query_error(e.to_string()))?;
362        if compiled.params().n_free() != 0 {
363            return Err(query_error(
364                "dataset expressions cannot contain free parameters",
365            ));
366        }
367        if require_real
368            && compiled
369                .node_facts(compiled.graph().root())
370                .is_some_and(|facts| facts.value_kind == ValueKind::Complex)
371        {
372            return Err(query_error(
373                "this dataset operation requires a real-valued expression",
374            ));
375        }
376        let params = compiled.params().default_values();
377        let model = PreparedModel::prepare(&compiled, execution)?;
378        Ok(Self { model, params })
379    }
380
381    fn evaluate_batch(&self, batch: &EventBatch) -> RuntimeResult<Vec<Complex64>> {
382        self.model.evaluate_batch(&self.params, batch)
383    }
384}
385
386enum CompiledPredicate {
387    Compare {
388        lhs: Box<QueryExpr>,
389        op: Comparison,
390        rhs: Box<QueryExpr>,
391    },
392    And(Box<Self>, Box<Self>),
393    Or(Box<Self>, Box<Self>),
394    Not(Box<Self>),
395    Between {
396        value: Box<QueryExpr>,
397        lower: Box<QueryExpr>,
398        upper: Box<QueryExpr>,
399        closure: IntervalClosure,
400    },
401}
402
403impl CompiledPredicate {
404    fn prepare(predicate: &Predicate, execution: &Execution) -> RuntimeResult<Self> {
405        Ok(match predicate {
406            Predicate::Compare { lhs, op, rhs } => Self::Compare {
407                lhs: Box::new(QueryExpr::prepare(lhs, execution, true)?),
408                op: *op,
409                rhs: Box::new(QueryExpr::prepare(rhs, execution, true)?),
410            },
411            Predicate::And(lhs, rhs) => Self::And(
412                Box::new(Self::prepare(lhs, execution)?),
413                Box::new(Self::prepare(rhs, execution)?),
414            ),
415            Predicate::Or(lhs, rhs) => Self::Or(
416                Box::new(Self::prepare(lhs, execution)?),
417                Box::new(Self::prepare(rhs, execution)?),
418            ),
419            Predicate::Not(inner) => Self::Not(Box::new(Self::prepare(inner, execution)?)),
420            Predicate::Between {
421                value,
422                lower,
423                upper,
424                closure,
425            } => Self::Between {
426                value: Box::new(QueryExpr::prepare(value, execution, true)?),
427                lower: Box::new(QueryExpr::prepare(lower, execution, true)?),
428                upper: Box::new(QueryExpr::prepare(upper, execution, true)?),
429                closure: *closure,
430            },
431        })
432    }
433
434    fn evaluate_batch(&self, batch: &EventBatch) -> RuntimeResult<Vec<bool>> {
435        Ok(match self {
436            Self::Compare { lhs, op, rhs } => lhs
437                .evaluate_batch(batch)?
438                .into_iter()
439                .zip(rhs.evaluate_batch(batch)?)
440                .map(|(lhs, rhs)| compare(lhs.re, *op, rhs.re))
441                .collect(),
442            Self::And(lhs, rhs) => lhs
443                .evaluate_batch(batch)?
444                .into_iter()
445                .zip(rhs.evaluate_batch(batch)?)
446                .map(|(l, r)| l && r)
447                .collect(),
448            Self::Or(lhs, rhs) => lhs
449                .evaluate_batch(batch)?
450                .into_iter()
451                .zip(rhs.evaluate_batch(batch)?)
452                .map(|(l, r)| l || r)
453                .collect(),
454            Self::Not(inner) => inner
455                .evaluate_batch(batch)?
456                .into_iter()
457                .map(|v| !v)
458                .collect(),
459            Self::Between {
460                value,
461                lower,
462                upper,
463                closure,
464            } => value
465                .evaluate_batch(batch)?
466                .into_iter()
467                .zip(lower.evaluate_batch(batch)?)
468                .zip(upper.evaluate_batch(batch)?)
469                .map(|((value, lower), upper)| {
470                    let lower_op = match closure {
471                        IntervalClosure::Open | IntervalClosure::RightClosed => Comparison::Gt,
472                        IntervalClosure::LeftClosed | IntervalClosure::Closed => Comparison::Ge,
473                    };
474                    let upper_op = match closure {
475                        IntervalClosure::Open | IntervalClosure::LeftClosed => Comparison::Lt,
476                        IntervalClosure::RightClosed | IntervalClosure::Closed => Comparison::Le,
477                    };
478                    compare(value.re, lower_op, lower.re) && compare(value.re, upper_op, upper.re)
479                })
480                .collect(),
481        })
482    }
483}
484
485fn compare(lhs: f64, op: Comparison, rhs: f64) -> bool {
486    if lhs.is_nan() || rhs.is_nan() {
487        return false;
488    }
489    match op {
490        Comparison::Lt => lhs < rhs,
491        Comparison::Le => lhs <= rhs,
492        Comparison::Gt => lhs > rhs,
493        Comparison::Ge => lhs >= rhs,
494        Comparison::Eq => lhs == rhs,
495        Comparison::Ne => lhs != rhs,
496    }
497}
498
499#[derive(Clone)]
500struct QuerySource {
501    source: Dataset,
502    filter: QueryFilter,
503}
504
505#[derive(Clone)]
506enum QueryFilter {
507    Predicate(Arc<CompiledPredicate>),
508    Bin {
509        query: Arc<QueryExpr>,
510        bins: BinSpec,
511        index: usize,
512    },
513}
514
515impl EventSource for QuerySource {
516    fn schema(&self) -> LadduDataResult<Arc<Schema>> {
517        self.source.schema()
518    }
519
520    fn capabilities(&self) -> SourceCapabilities {
521        let source = self.source.capabilities();
522        SourceCapabilities {
523            exact_len: false,
524            exact_weighted_total: false,
525            random_access: false,
526            deterministic_partitioning: source.deterministic_partitioning,
527            predicate_pushdown: false,
528            projection_pushdown: false,
529            streaming: true,
530        }
531    }
532
533    fn batches(&self, plan: ReadPlan) -> LadduDataResult<EventBatchIter> {
534        let batches = self.source.batches_with_plan(plan)?;
535        let filter = self.filter.clone();
536        Ok(Box::new(batches.filter_map(move |batch| {
537            let batch = match batch {
538                Ok(batch) => batch,
539                Err(error) => return Some(Err(error)),
540            };
541            let rows = match filter.rows(&batch) {
542                Ok(rows) => rows,
543                Err(error) => return Some(Err(LadduDataError::Source(error.to_string()))),
544            };
545            (!rows.is_empty()).then(|| Ok(batch.select(&rows)))
546        })))
547    }
548}
549
550impl QueryFilter {
551    fn rows(&self, batch: &EventBatch) -> RuntimeResult<Vec<usize>> {
552        // TODO: cache evaluated_batch indices somewhere
553        match self {
554            Self::Predicate(predicate) => Ok(predicate
555                .evaluate_batch(batch)?
556                .into_iter()
557                .enumerate()
558                .filter_map(|(row, keep)| keep.then_some(row))
559                .collect()),
560            Self::Bin { query, bins, index } => Ok(query
561                .evaluate_batch(batch)?
562                .into_iter()
563                .enumerate()
564                .filter_map(|(row, value)| (bins.index(value.re) == Some(*index)).then_some(row))
565                .collect()),
566        }
567    }
568}
569
570fn query_error(message: impl Into<String>) -> RuntimeError {
571    RuntimeError::InvalidShape {
572        index: 0,
573        message: message.into(),
574    }
575}
576fn data_error(error: impl ToString) -> RuntimeError {
577    RuntimeError::Data(error.to_string())
578}
579
580#[cfg(test)]
581mod tests {
582    use super::*;
583    use laddu_data::{
584        data::OwnedEvent,
585        io::{EventSource, ReadPlan, SourceCapabilities, memory::MemorySource},
586        schema::Schema,
587    };
588    use laddu_expr::{complex, event_scalar};
589    use std::sync::atomic::{AtomicUsize, Ordering};
590
591    #[test]
592    fn bin_spec_roundtrip_preserves_validation() {
593        let bins = BinSpec::edges([-1.0, 0.0, 2.0]).unwrap();
594        let json = serde_json::to_string(&bins).unwrap();
595        assert_eq!(serde_json::from_str::<BinSpec>(&json).unwrap(), bins);
596        assert!(serde_json::from_str::<BinSpec>("[0.0,0.0]").is_err());
597    }
598
599    #[derive(Clone)]
600    struct CountingSource {
601        inner: MemorySource,
602        reads: Arc<AtomicUsize>,
603    }
604
605    impl EventSource for CountingSource {
606        fn schema(&self) -> LadduDataResult<Arc<Schema>> {
607            EventSource::schema(&self.inner)
608        }
609
610        fn capabilities(&self) -> SourceCapabilities {
611            self.inner.capabilities()
612        }
613
614        fn batches(&self, plan: ReadPlan) -> LadduDataResult<EventBatchIter> {
615            self.reads.fetch_add(1, Ordering::Relaxed);
616            self.inner.batches(plan)
617        }
618    }
619
620    fn dataset() -> Dataset {
621        let schema = Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], true).unwrap());
622        Dataset::from_events(
623            schema,
624            [
625                OwnedEvent::weighted(vec![], vec![-1.0], 0.5),
626                OwnedEvent::weighted(vec![], vec![0.0], 1.0),
627                OwnedEvent::weighted(vec![], vec![1.0], 1.5),
628                OwnedEvent::weighted(vec![], vec![2.0], 2.0),
629            ],
630        )
631        .unwrap()
632    }
633
634    #[test]
635    fn evaluates_selects_and_bins_dataset_expressions() {
636        let dataset = dataset().chunked(1).unwrap();
637        let execution = Execution::default();
638        let x = event_scalar("x");
639        assert_eq!(
640            dataset.evaluate_real(&x, &execution).unwrap(),
641            vec![-1.0, 0.0, 1.0, 2.0]
642        );
643
644        let selected = dataset
645            .select(
646                &Predicate::ge(x.clone(), 0.0).and(Predicate::lt(x.clone(), 2.0)),
647                &execution,
648            )
649            .unwrap();
650        assert_eq!(
651            selected.map_events(|event| event.scalar(0)).unwrap(),
652            vec![0.0, 1.0]
653        );
654        assert_eq!(selected.sum_weights().unwrap(), 2.5);
655
656        let bins = dataset
657            .bin_by(&x, BinSpec::uniform(2, 0.0, 2.0).unwrap(), &execution)
658            .unwrap();
659        assert_eq!(bins.len(), 2);
660        assert_eq!(
661            bins[0]
662                .dataset()
663                .map_events(|event| event.scalar(0))
664                .unwrap(),
665            vec![0.0]
666        );
667        assert_eq!(
668            bins[1]
669                .dataset()
670                .map_events(|event| event.scalar(0))
671                .unwrap(),
672            vec![1.0, 2.0]
673        );
674    }
675
676    #[test]
677    fn between_predicates_have_explicit_endpoint_semantics() {
678        let dataset = dataset();
679        let execution = Execution::default();
680        let x = event_scalar("x");
681
682        let closed = dataset
683            .select(&Predicate::between(x.clone(), 0.0, 1.0), &execution)
684            .unwrap();
685        assert_eq!(
686            closed.map_events(|event| event.scalar(0)).unwrap(),
687            vec![0.0, 1.0]
688        );
689
690        let open = dataset
691            .select(
692                &Predicate::between_with(x, -1.0, 1.0, IntervalClosure::Open),
693                &execution,
694            )
695            .unwrap();
696        assert_eq!(open.map_events(|event| event.scalar(0)).unwrap(), vec![0.0]);
697    }
698
699    #[test]
700    fn real_queries_reject_complex_and_free_parameter_expressions() {
701        let dataset = dataset();
702        let execution = Execution::default();
703        assert!(
704            dataset
705                .evaluate_real(&complex(1.0, 1.0), &execution)
706                .is_err()
707        );
708        let parameter = Expr::from(laddu_expr::parameters::Parameter::free("p"));
709        assert!(dataset.evaluate_expr(&parameter, &execution).is_err());
710    }
711
712    #[test]
713    fn bin_edges_validate_and_nan_predicates_are_false() {
714        assert!(BinSpec::edges([0.0, 0.0]).is_err());
715        assert!(!compare(f64::NAN, Comparison::Ne, 0.0));
716    }
717
718    #[test]
719    fn selection_and_binning_are_lazy_and_preserve_streaming_policy() {
720        let source = dataset();
721        let batch = source.batches().unwrap().next().unwrap().unwrap();
722        let reads = Arc::new(AtomicUsize::new(0));
723        let dataset = Dataset::new(CountingSource {
724            inner: MemorySource::new(batch),
725            reads: Arc::clone(&reads),
726        })
727        .streaming();
728        let execution = Execution::default();
729        let x = event_scalar("x");
730
731        let selected = dataset
732            .select(&Predicate::ge(x.clone(), 0.0), &execution)
733            .unwrap();
734        let bins = dataset
735            .bin_by(&x, BinSpec::uniform(2, 0.0, 2.0).unwrap(), &execution)
736            .unwrap();
737        assert_eq!(reads.load(Ordering::Relaxed), 0);
738        assert_eq!(
739            selected.cache_storage(),
740            laddu_data::data::CacheStorage::Streaming
741        );
742
743        assert_eq!(
744            selected.map_events(|event| event.scalar(0)).unwrap(),
745            vec![0.0, 1.0, 2.0]
746        );
747        assert_eq!(reads.load(Ordering::Relaxed), 1);
748        assert_eq!(
749            bins[0]
750                .dataset()
751                .map_events(|event| event.scalar(0))
752                .unwrap(),
753            vec![0.0]
754        );
755        assert_eq!(reads.load(Ordering::Relaxed), 2);
756    }
757}