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, memory::MemorySource},
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    /// Partitions the dataset in one pass 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 = QueryExpr::prepare(expr, execution, true)?;
333        let schema = self.schema().map_err(data_error)?;
334        let mut partitions = vec![Vec::new(); bins.bin_count()];
335        for batch in self.batches().map_err(data_error)? {
336            let batch = batch.map_err(data_error)?;
337            let mut rows = vec![Vec::new(); bins.bin_count()];
338            for (row, value) in query.evaluate_batch(&batch)?.into_iter().enumerate() {
339                if let Some(index) = bins.index(value.re) {
340                    rows[index].push(row);
341                }
342            }
343            for (partition, rows) in partitions.iter_mut().zip(rows) {
344                if !rows.is_empty() {
345                    partition.push(batch.select(&rows));
346                }
347            }
348        }
349
350        partitions
351            .into_iter()
352            .enumerate()
353            .map(|(index, batches)| {
354                let source = if batches.is_empty() {
355                    MemorySource::new(
356                        EventBatch::from_events(Arc::clone(&schema), std::iter::empty())
357                            .map_err(data_error)?,
358                    )
359                } else {
360                    MemorySource::from_batches(batches).map_err(data_error)?
361                };
362                Ok(DatasetBin {
363                    index,
364                    lower: bins.edges[index],
365                    upper: bins.edges[index + 1],
366                    dataset: self.with_derived_source(source),
367                })
368            })
369            .collect()
370    }
371}
372
373struct QueryExpr {
374    model: PreparedModel,
375    params: laddu_expr::parameters::ParamValues,
376}
377
378impl QueryExpr {
379    fn prepare(expr: &Expr, execution: &Execution, require_real: bool) -> RuntimeResult<Self> {
380        if expr.shape().map_err(|e| query_error(e.to_string()))? != ExprShape::Scalar {
381            return Err(query_error("dataset expressions must be scalar"));
382        }
383        let compiled = CompiledModel::from_expr(expr).map_err(|e| query_error(e.to_string()))?;
384        if compiled.params().n_free() != 0 {
385            return Err(query_error(
386                "dataset expressions cannot contain free parameters",
387            ));
388        }
389        if require_real
390            && compiled
391                .node_facts(compiled.graph().root())
392                .is_some_and(|facts| facts.value_kind == ValueKind::Complex)
393        {
394            return Err(query_error(
395                "this dataset operation requires a real-valued expression",
396            ));
397        }
398        let params = compiled.params().default_values();
399        let model = PreparedModel::prepare(&compiled, execution)?;
400        Ok(Self { model, params })
401    }
402
403    fn evaluate_batch(&self, batch: &EventBatch) -> RuntimeResult<Vec<Complex64>> {
404        self.model.evaluate_batch(&self.params, batch)
405    }
406}
407
408enum CompiledPredicate {
409    Compare {
410        lhs: Box<QueryExpr>,
411        op: Comparison,
412        rhs: Box<QueryExpr>,
413    },
414    And(Box<Self>, Box<Self>),
415    Or(Box<Self>, Box<Self>),
416    Not(Box<Self>),
417    Between {
418        value: Box<QueryExpr>,
419        lower: Box<QueryExpr>,
420        upper: Box<QueryExpr>,
421        closure: IntervalClosure,
422    },
423}
424
425impl CompiledPredicate {
426    fn prepare(predicate: &Predicate, execution: &Execution) -> RuntimeResult<Self> {
427        Ok(match predicate {
428            Predicate::Compare { lhs, op, rhs } => Self::Compare {
429                lhs: Box::new(QueryExpr::prepare(lhs, execution, true)?),
430                op: *op,
431                rhs: Box::new(QueryExpr::prepare(rhs, execution, true)?),
432            },
433            Predicate::And(lhs, rhs) => Self::And(
434                Box::new(Self::prepare(lhs, execution)?),
435                Box::new(Self::prepare(rhs, execution)?),
436            ),
437            Predicate::Or(lhs, rhs) => Self::Or(
438                Box::new(Self::prepare(lhs, execution)?),
439                Box::new(Self::prepare(rhs, execution)?),
440            ),
441            Predicate::Not(inner) => Self::Not(Box::new(Self::prepare(inner, execution)?)),
442            Predicate::Between {
443                value,
444                lower,
445                upper,
446                closure,
447            } => Self::Between {
448                value: Box::new(QueryExpr::prepare(value, execution, true)?),
449                lower: Box::new(QueryExpr::prepare(lower, execution, true)?),
450                upper: Box::new(QueryExpr::prepare(upper, execution, true)?),
451                closure: *closure,
452            },
453        })
454    }
455
456    fn evaluate_batch(&self, batch: &EventBatch) -> RuntimeResult<Vec<bool>> {
457        Ok(match self {
458            Self::Compare { lhs, op, rhs } => lhs
459                .evaluate_batch(batch)?
460                .into_iter()
461                .zip(rhs.evaluate_batch(batch)?)
462                .map(|(lhs, rhs)| compare(lhs.re, *op, rhs.re))
463                .collect(),
464            Self::And(lhs, rhs) => lhs
465                .evaluate_batch(batch)?
466                .into_iter()
467                .zip(rhs.evaluate_batch(batch)?)
468                .map(|(l, r)| l && r)
469                .collect(),
470            Self::Or(lhs, rhs) => lhs
471                .evaluate_batch(batch)?
472                .into_iter()
473                .zip(rhs.evaluate_batch(batch)?)
474                .map(|(l, r)| l || r)
475                .collect(),
476            Self::Not(inner) => inner
477                .evaluate_batch(batch)?
478                .into_iter()
479                .map(|v| !v)
480                .collect(),
481            Self::Between {
482                value,
483                lower,
484                upper,
485                closure,
486            } => value
487                .evaluate_batch(batch)?
488                .into_iter()
489                .zip(lower.evaluate_batch(batch)?)
490                .zip(upper.evaluate_batch(batch)?)
491                .map(|((value, lower), upper)| {
492                    let lower_op = match closure {
493                        IntervalClosure::Open | IntervalClosure::RightClosed => Comparison::Gt,
494                        IntervalClosure::LeftClosed | IntervalClosure::Closed => Comparison::Ge,
495                    };
496                    let upper_op = match closure {
497                        IntervalClosure::Open | IntervalClosure::LeftClosed => Comparison::Lt,
498                        IntervalClosure::RightClosed | IntervalClosure::Closed => Comparison::Le,
499                    };
500                    compare(value.re, lower_op, lower.re) && compare(value.re, upper_op, upper.re)
501                })
502                .collect(),
503        })
504    }
505}
506
507fn compare(lhs: f64, op: Comparison, rhs: f64) -> bool {
508    if lhs.is_nan() || rhs.is_nan() {
509        return false;
510    }
511    match op {
512        Comparison::Lt => lhs < rhs,
513        Comparison::Le => lhs <= rhs,
514        Comparison::Gt => lhs > rhs,
515        Comparison::Ge => lhs >= rhs,
516        Comparison::Eq => lhs == rhs,
517        Comparison::Ne => lhs != rhs,
518    }
519}
520
521#[derive(Clone)]
522struct QuerySource {
523    source: Dataset,
524    filter: QueryFilter,
525}
526
527#[derive(Clone)]
528enum QueryFilter {
529    Predicate(Arc<CompiledPredicate>),
530}
531
532impl EventSource for QuerySource {
533    fn schema(&self) -> LadduDataResult<Arc<Schema>> {
534        self.source.schema()
535    }
536
537    fn capabilities(&self) -> SourceCapabilities {
538        let source = self.source.capabilities();
539        SourceCapabilities {
540            exact_len: false,
541            exact_weighted_total: false,
542            random_access: false,
543            deterministic_partitioning: source.deterministic_partitioning,
544            predicate_pushdown: false,
545            projection_pushdown: false,
546            streaming: true,
547        }
548    }
549
550    fn batches(&self, plan: ReadPlan) -> LadduDataResult<EventBatchIter> {
551        let batches = self.source.batches_with_plan(plan)?;
552        let filter = self.filter.clone();
553        Ok(Box::new(batches.filter_map(move |batch| {
554            let batch = match batch {
555                Ok(batch) => batch,
556                Err(error) => return Some(Err(error)),
557            };
558            let rows = match filter.rows(&batch) {
559                Ok(rows) => rows,
560                Err(error) => return Some(Err(LadduDataError::Source(error.to_string()))),
561            };
562            (!rows.is_empty()).then(|| Ok(batch.select(&rows)))
563        })))
564    }
565}
566
567impl QueryFilter {
568    fn rows(&self, batch: &EventBatch) -> RuntimeResult<Vec<usize>> {
569        // TODO: cache evaluated_batch indices somewhere
570        match self {
571            Self::Predicate(predicate) => Ok(predicate
572                .evaluate_batch(batch)?
573                .into_iter()
574                .enumerate()
575                .filter_map(|(row, keep)| keep.then_some(row))
576                .collect()),
577        }
578    }
579}
580
581fn query_error(message: impl Into<String>) -> RuntimeError {
582    RuntimeError::InvalidShape {
583        index: 0,
584        message: message.into(),
585    }
586}
587fn data_error(error: impl ToString) -> RuntimeError {
588    RuntimeError::Data(error.to_string())
589}
590
591#[cfg(test)]
592mod tests {
593    use super::*;
594    use laddu_data::{
595        data::OwnedEvent,
596        io::{EventSource, ReadPlan, SourceCapabilities, memory::MemorySource},
597        schema::Schema,
598    };
599    use laddu_expr::{complex, event_scalar};
600    use std::sync::atomic::{AtomicUsize, Ordering};
601
602    #[test]
603    fn bin_spec_roundtrip_preserves_validation() {
604        let bins = BinSpec::edges([-1.0, 0.0, 2.0]).unwrap();
605        let json = serde_json::to_string(&bins).unwrap();
606        assert_eq!(serde_json::from_str::<BinSpec>(&json).unwrap(), bins);
607        assert!(serde_json::from_str::<BinSpec>("[0.0,0.0]").is_err());
608    }
609
610    #[derive(Clone)]
611    struct CountingSource {
612        inner: MemorySource,
613        reads: Arc<AtomicUsize>,
614    }
615
616    impl EventSource for CountingSource {
617        fn schema(&self) -> LadduDataResult<Arc<Schema>> {
618            EventSource::schema(&self.inner)
619        }
620
621        fn capabilities(&self) -> SourceCapabilities {
622            self.inner.capabilities()
623        }
624
625        fn batches(&self, plan: ReadPlan) -> LadduDataResult<EventBatchIter> {
626            self.reads.fetch_add(1, Ordering::Relaxed);
627            self.inner.batches(plan)
628        }
629    }
630
631    fn dataset() -> Dataset {
632        let schema = Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], true).unwrap());
633        Dataset::from_events(
634            schema,
635            [
636                OwnedEvent::weighted(vec![], vec![-1.0], 0.5),
637                OwnedEvent::weighted(vec![], vec![0.0], 1.0),
638                OwnedEvent::weighted(vec![], vec![1.0], 1.5),
639                OwnedEvent::weighted(vec![], vec![2.0], 2.0),
640            ],
641        )
642        .unwrap()
643    }
644
645    #[test]
646    fn evaluates_selects_and_bins_dataset_expressions() {
647        let dataset = dataset().chunked(1).unwrap();
648        let execution = Execution::default();
649        let x = event_scalar("x");
650        assert_eq!(
651            dataset.evaluate_real(&x, &execution).unwrap(),
652            vec![-1.0, 0.0, 1.0, 2.0]
653        );
654
655        let selected = dataset
656            .select(
657                &Predicate::ge(x.clone(), 0.0).and(Predicate::lt(x.clone(), 2.0)),
658                &execution,
659            )
660            .unwrap();
661        assert_eq!(
662            selected.map_events(|event| event.scalar(0)).unwrap(),
663            vec![0.0, 1.0]
664        );
665        assert_eq!(selected.sum_weights().unwrap(), 2.5);
666
667        let bins = dataset
668            .bin_by(&x, BinSpec::uniform(2, 0.0, 2.0).unwrap(), &execution)
669            .unwrap();
670        assert_eq!(bins.len(), 2);
671        assert_eq!(
672            bins[0]
673                .dataset()
674                .map_events(|event| event.scalar(0))
675                .unwrap(),
676            vec![0.0]
677        );
678        assert_eq!(
679            bins[1]
680                .dataset()
681                .map_events(|event| event.scalar(0))
682                .unwrap(),
683            vec![1.0, 2.0]
684        );
685    }
686
687    #[test]
688    fn traversing_all_bins_reads_the_source_once() {
689        let reads = Arc::new(AtomicUsize::new(0));
690        let source = CountingSource {
691            inner: match dataset().batches().unwrap().next().unwrap() {
692                Ok(batch) => MemorySource::new(batch),
693                Err(error) => panic!("unexpected source error: {error}"),
694            },
695            reads: Arc::clone(&reads),
696        };
697        let dataset = Dataset::new(source).chunked(1).unwrap();
698        let bins = dataset
699            .bin_by(
700                &event_scalar("x"),
701                BinSpec::uniform(4, -1.0, 3.0).unwrap(),
702                &Execution::default(),
703            )
704            .unwrap();
705
706        let values = bins
707            .into_iter()
708            .map(|bin| {
709                bin.into_dataset()
710                    .map_events(|event| event.scalar(0))
711                    .unwrap()
712            })
713            .collect::<Vec<_>>();
714        assert_eq!(values, [vec![-1.0], vec![0.0], vec![1.0], vec![2.0]]);
715        assert_eq!(reads.load(Ordering::Relaxed), 1);
716    }
717
718    #[test]
719    fn between_predicates_have_explicit_endpoint_semantics() {
720        let dataset = dataset();
721        let execution = Execution::default();
722        let x = event_scalar("x");
723
724        let closed = dataset
725            .select(&Predicate::between(x.clone(), 0.0, 1.0), &execution)
726            .unwrap();
727        assert_eq!(
728            closed.map_events(|event| event.scalar(0)).unwrap(),
729            vec![0.0, 1.0]
730        );
731
732        let open = dataset
733            .select(
734                &Predicate::between_with(x, -1.0, 1.0, IntervalClosure::Open),
735                &execution,
736            )
737            .unwrap();
738        assert_eq!(open.map_events(|event| event.scalar(0)).unwrap(), vec![0.0]);
739    }
740
741    #[test]
742    fn real_queries_reject_complex_and_free_parameter_expressions() {
743        let dataset = dataset();
744        let execution = Execution::default();
745        assert!(
746            dataset
747                .evaluate_real(&complex(1.0, 1.0), &execution)
748                .is_err()
749        );
750        let parameter = Expr::from(laddu_expr::parameters::Parameter::free("p"));
751        assert!(dataset.evaluate_expr(&parameter, &execution).is_err());
752    }
753
754    #[test]
755    fn bin_edges_validate_and_nan_predicates_are_false() {
756        assert!(BinSpec::edges([0.0, 0.0]).is_err());
757        assert!(!compare(f64::NAN, Comparison::Ne, 0.0));
758    }
759
760    #[test]
761    fn selection_is_lazy_and_one_pass_binning_preserves_streaming_policy() {
762        let source = dataset();
763        let batch = source.batches().unwrap().next().unwrap().unwrap();
764        let reads = Arc::new(AtomicUsize::new(0));
765        let dataset = Dataset::new(CountingSource {
766            inner: MemorySource::new(batch),
767            reads: Arc::clone(&reads),
768        })
769        .streaming();
770        let execution = Execution::default();
771        let x = event_scalar("x");
772
773        let selected = dataset
774            .select(&Predicate::ge(x.clone(), 0.0), &execution)
775            .unwrap();
776        let bins = dataset
777            .bin_by(&x, BinSpec::uniform(2, 0.0, 2.0).unwrap(), &execution)
778            .unwrap();
779        assert_eq!(reads.load(Ordering::Relaxed), 1);
780        assert_eq!(
781            selected.cache_storage(),
782            laddu_data::data::CacheStorage::Streaming
783        );
784
785        assert_eq!(
786            selected.map_events(|event| event.scalar(0)).unwrap(),
787            vec![0.0, 1.0, 2.0]
788        );
789        assert_eq!(reads.load(Ordering::Relaxed), 2);
790        assert_eq!(
791            bins[0]
792                .dataset()
793                .map_events(|event| event.scalar(0))
794                .unwrap(),
795            vec![0.0]
796        );
797        assert_eq!(reads.load(Ordering::Relaxed), 2);
798    }
799
800    #[test]
801    fn unknown_cardinality_fastest_discovers_and_retains_small_selection() {
802        let source = dataset();
803        let batch = source.batches().unwrap().next().unwrap().unwrap();
804        let reads = Arc::new(AtomicUsize::new(0));
805        let dataset = Dataset::new(CountingSource {
806            inner: MemorySource::new(batch),
807            reads: Arc::clone(&reads),
808        });
809        let execution = Execution::default();
810        let x = event_scalar("x");
811        let selected = dataset
812            .select(&Predicate::ge(x.clone(), 0.0), &execution)
813            .unwrap();
814        let compiled = CompiledModel::from_expr(&x).unwrap();
815        let params = compiled.params().default_values();
816        let model = PreparedModel::prepare(&compiled, &execution).unwrap();
817        let prepared = model.prepare_dataset(&execution, &selected).unwrap();
818
819        #[cfg(not(feature = "wgpu"))]
820        let crate::PreparedDataset::Cpu(prepared_cpu) = &prepared;
821        #[cfg(feature = "wgpu")]
822        let crate::PreparedDataset::Cpu(prepared_cpu) = &prepared else {
823            panic!("default execution prepares CPU datasets");
824        };
825        assert_eq!(
826            prepared_cpu.stats().storage(),
827            laddu_data::data::CacheStorage::Resident
828        );
829        assert_eq!(prepared_cpu.stats().local_events(), 3);
830        assert_eq!(reads.load(Ordering::Relaxed), 2);
831
832        for _ in 0..2 {
833            assert_eq!(
834                model
835                    .reduce(
836                        &execution,
837                        &params,
838                        &prepared,
839                        laddu_compile::ReductionPlan::weighted_real(),
840                    )
841                    .unwrap(),
842                5.5
843            );
844        }
845        assert_eq!(reads.load(Ordering::Relaxed), 2);
846    }
847}