Skip to main content

laddu_runtime/
query.rs

1use std::sync::Arc;
2
3use laddu_compile::CompiledQuery;
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, 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 = QueryExprSet::prepare(vec![expr.clone()], execution, false)?;
297        let mut output = Vec::new();
298        for batch in self.batches().map_err(data_error)? {
299            output.extend(
300                query.evaluate_batch(&batch.map_err(data_error)?)?[0]
301                    .iter()
302                    .copied(),
303            );
304        }
305        Ok(output)
306    }
307
308    fn evaluate_real(&self, expr: &Expr, execution: &Execution) -> RuntimeResult<Vec<f64>> {
309        let query = QueryExprSet::prepare(vec![expr.clone()], execution, true)?;
310        let mut output = Vec::new();
311        for batch in self.batches().map_err(data_error)? {
312            output.extend(
313                query.evaluate_batch(&batch.map_err(data_error)?)?[0]
314                    .iter()
315                    .copied()
316                    .map(|v| v.re),
317            );
318        }
319        Ok(output)
320    }
321
322    fn select(&self, predicate: &Predicate, execution: &Execution) -> RuntimeResult<Dataset> {
323        let compiled = CompiledPredicate::prepare(predicate, execution)?;
324        Ok(self.with_derived_source(QuerySource {
325            source: self.clone(),
326            filter: QueryFilter::Predicate(Arc::new(compiled)),
327        }))
328    }
329
330    fn bin_by(
331        &self,
332        expr: &Expr,
333        bins: BinSpec,
334        execution: &Execution,
335    ) -> RuntimeResult<Vec<DatasetBin>> {
336        let query = QueryExprSet::prepare(vec![expr.clone()], execution, true)?;
337        let schema = self.schema().map_err(data_error)?;
338        let mut partitions = vec![Vec::new(); bins.bin_count()];
339        for batch in self.batches().map_err(data_error)? {
340            let batch = batch.map_err(data_error)?;
341            let mut rows = vec![Vec::new(); bins.bin_count()];
342            for (row, value) in query.evaluate_batch(&batch)?[0].iter().copied().enumerate() {
343                if let Some(index) = bins.index(value.re) {
344                    rows[index].push(row);
345                }
346            }
347            for (partition, rows) in partitions.iter_mut().zip(rows) {
348                if !rows.is_empty() {
349                    partition.push(batch.select(&rows));
350                }
351            }
352        }
353
354        partitions
355            .into_iter()
356            .enumerate()
357            .map(|(index, batches)| {
358                let source = if batches.is_empty() {
359                    MemorySource::empty(Arc::clone(&schema))
360                } else {
361                    MemorySource::from_batches(batches).map_err(data_error)?
362                };
363                Ok(DatasetBin {
364                    index,
365                    lower: bins.edges[index],
366                    upper: bins.edges[index + 1],
367                    dataset: self.with_derived_source(source),
368                })
369            })
370            .collect()
371    }
372}
373
374struct QueryExpr {
375    model: PreparedModel,
376    params: laddu_expr::parameters::ParamValues,
377    outputs: Vec<laddu_expr::ExprId>,
378}
379
380struct QueryExprSet {
381    shared: QueryExprStorage,
382    outputs: usize,
383}
384
385enum QueryExprStorage {
386    Shared(QueryExpr),
387    Separate(Vec<QueryExpr>),
388}
389
390impl QueryExprSet {
391    fn prepare(
392        expressions: Vec<Expr>,
393        execution: &Execution,
394        require_real: bool,
395    ) -> RuntimeResult<Self> {
396        let expression_count = expressions.len();
397        let compiled = CompiledQuery::from_exprs(expressions.clone())
398            .map_err(|error| query_error(error.to_string()))?;
399        let model = compiled.model();
400        let outputs = compiled.outputs();
401        if outputs.len() != expression_count {
402            return Err(query_error(
403                "compiled query output count changed during lowering",
404            ));
405        }
406        for element in outputs {
407            let value_kind = model
408                .node_facts(*element)
409                .map(|facts| facts.value_kind)
410                .ok_or_else(|| query_error("compiled expression facts are incomplete"))?;
411            if value_kind != ValueKind::Real
412                && (require_real || !matches!(value_kind, ValueKind::Complex))
413            {
414                return Err(query_error(if require_real {
415                    "this dataset operation requires a real-valued expression"
416                } else {
417                    "dataset expressions must be scalar"
418                }));
419            }
420        }
421        if model.params().n_free() != 0 {
422            return Err(query_error(
423                "dataset expressions cannot contain free parameters",
424            ));
425        }
426        let params = model.params().default_values();
427        let plan = match PreparedModel::prepare(model, execution) {
428            Ok(plan) => QueryExprStorage::Shared(QueryExpr {
429                model: plan,
430                params,
431                outputs: outputs.to_vec(),
432            }),
433            Err(shared_error) => {
434                if !may_fallback_to_scalar(execution, &shared_error) {
435                    return Err(shared_error);
436                }
437                let separate = expressions
438                    .iter()
439                    .map(|expr| QueryExpr::prepare(expr, execution, require_real))
440                    .collect::<RuntimeResult<Vec<_>>>();
441                QueryExprStorage::Separate(separate?)
442            }
443        };
444        Ok(Self {
445            shared: plan,
446            outputs: expression_count,
447        })
448    }
449
450    fn evaluate_batch(&self, batch: &EventBatch) -> RuntimeResult<Vec<Vec<Complex64>>> {
451        let values = match &self.shared {
452            QueryExprStorage::Shared(query) => {
453                query
454                    .model
455                    .evaluate_batch_outputs(&query.params, batch, &query.outputs)?
456            }
457            QueryExprStorage::Separate(queries) => queries
458                .iter()
459                .map(|query| query.evaluate_batch(batch))
460                .collect::<RuntimeResult<Vec<_>>>()?,
461        };
462        if values.len() != self.outputs {
463            return Err(query_error(
464                "compiled query returned an unexpected output count",
465            ));
466        }
467        Ok(values)
468    }
469}
470
471impl QueryExpr {
472    fn prepare(expr: &Expr, execution: &Execution, require_real: bool) -> RuntimeResult<Self> {
473        if expr.shape().map_err(|e| query_error(e.to_string()))? != laddu_expr::ExprShape::Scalar {
474            return Err(query_error("dataset expressions must be scalar"));
475        }
476        let compiled = laddu_compile::CompiledModel::from_expr(expr)
477            .map_err(|error| query_error(error.to_string()))?;
478        let value_kind = compiled
479            .node_facts(compiled.graph().root())
480            .map(|facts| facts.value_kind)
481            .ok_or_else(|| query_error("compiled expression facts are incomplete"))?;
482        if require_real && value_kind == ValueKind::Complex {
483            return Err(query_error(
484                "this dataset operation requires a real-valued expression",
485            ));
486        }
487        if compiled.params().n_free() != 0 {
488            return Err(query_error(
489                "dataset expressions cannot contain free parameters",
490            ));
491        }
492        let params = compiled.params().default_values();
493        let model = PreparedModel::prepare(&compiled, execution)?;
494        Ok(Self {
495            model,
496            params,
497            outputs: Vec::new(),
498        })
499    }
500
501    fn evaluate_batch(&self, batch: &EventBatch) -> RuntimeResult<Vec<Complex64>> {
502        self.model.evaluate_batch(&self.params, batch)
503    }
504}
505
506struct CompiledPredicate {
507    expressions: QueryExprSet,
508    program: PredicateProgram,
509}
510
511enum PredicateProgram {
512    Compare {
513        lhs: usize,
514        op: Comparison,
515        rhs: usize,
516    },
517    And(Box<Self>, Box<Self>),
518    Or(Box<Self>, Box<Self>),
519    Not(Box<Self>),
520    Between {
521        value: usize,
522        lower: usize,
523        upper: usize,
524        closure: IntervalClosure,
525    },
526}
527
528impl CompiledPredicate {
529    fn prepare(predicate: &Predicate, execution: &Execution) -> RuntimeResult<Self> {
530        let mut expressions = Vec::new();
531        let program = Self::compile_program(predicate, &mut expressions);
532        Ok(Self {
533            expressions: QueryExprSet::prepare(expressions, execution, true)?,
534            program,
535        })
536    }
537
538    fn compile_program(predicate: &Predicate, expressions: &mut Vec<Expr>) -> PredicateProgram {
539        let leaf = |expr: &Expr, expressions: &mut Vec<Expr>| {
540            let index = expressions.len();
541            expressions.push(expr.clone());
542            index
543        };
544        match predicate {
545            Predicate::Compare { lhs, op, rhs } => PredicateProgram::Compare {
546                lhs: leaf(lhs, expressions),
547                op: *op,
548                rhs: leaf(rhs, expressions),
549            },
550            Predicate::And(lhs, rhs) => PredicateProgram::And(
551                Box::new(Self::compile_program(lhs, expressions)),
552                Box::new(Self::compile_program(rhs, expressions)),
553            ),
554            Predicate::Or(lhs, rhs) => PredicateProgram::Or(
555                Box::new(Self::compile_program(lhs, expressions)),
556                Box::new(Self::compile_program(rhs, expressions)),
557            ),
558            Predicate::Not(inner) => {
559                PredicateProgram::Not(Box::new(Self::compile_program(inner, expressions)))
560            }
561            Predicate::Between {
562                value,
563                lower,
564                upper,
565                closure,
566            } => PredicateProgram::Between {
567                value: leaf(value, expressions),
568                lower: leaf(lower, expressions),
569                upper: leaf(upper, expressions),
570                closure: *closure,
571            },
572        }
573    }
574
575    fn evaluate_batch(&self, batch: &EventBatch) -> RuntimeResult<Vec<usize>> {
576        let values = self.expressions.evaluate_batch(batch)?;
577        Ok((0..batch.len())
578            .filter(|row| Self::evaluate_row(&self.program, &values, *row))
579            .collect())
580    }
581
582    fn evaluate_row(program: &PredicateProgram, values: &[Vec<Complex64>], row: usize) -> bool {
583        match program {
584            PredicateProgram::Compare { lhs, op, rhs } => {
585                compare(values[*lhs][row].re, *op, values[*rhs][row].re)
586            }
587            PredicateProgram::And(lhs, rhs) => {
588                Self::evaluate_row(lhs, values, row) && Self::evaluate_row(rhs, values, row)
589            }
590            PredicateProgram::Or(lhs, rhs) => {
591                Self::evaluate_row(lhs, values, row) || Self::evaluate_row(rhs, values, row)
592            }
593            PredicateProgram::Not(inner) => !Self::evaluate_row(inner, values, row),
594            PredicateProgram::Between {
595                value,
596                lower,
597                upper,
598                closure,
599            } => {
600                let lower_op = match closure {
601                    IntervalClosure::Open | IntervalClosure::RightClosed => Comparison::Gt,
602                    IntervalClosure::LeftClosed | IntervalClosure::Closed => Comparison::Ge,
603                };
604                let upper_op = match closure {
605                    IntervalClosure::Open | IntervalClosure::LeftClosed => Comparison::Lt,
606                    IntervalClosure::RightClosed | IntervalClosure::Closed => Comparison::Le,
607                };
608                compare(values[*value][row].re, lower_op, values[*lower][row].re)
609                    && compare(values[*value][row].re, upper_op, values[*upper][row].re)
610            }
611        }
612    }
613}
614
615fn compare(lhs: f64, op: Comparison, rhs: f64) -> bool {
616    if lhs.is_nan() || rhs.is_nan() {
617        return false;
618    }
619    match op {
620        Comparison::Lt => lhs < rhs,
621        Comparison::Le => lhs <= rhs,
622        Comparison::Gt => lhs > rhs,
623        Comparison::Ge => lhs >= rhs,
624        Comparison::Eq => lhs == rhs,
625        Comparison::Ne => lhs != rhs,
626    }
627}
628
629#[derive(Clone)]
630struct QuerySource {
631    source: Dataset,
632    filter: QueryFilter,
633}
634
635#[derive(Clone)]
636enum QueryFilter {
637    Predicate(Arc<CompiledPredicate>),
638}
639
640impl EventSource for QuerySource {
641    fn schema(&self) -> LadduDataResult<Arc<Schema>> {
642        self.source.schema()
643    }
644
645    fn capabilities(&self) -> SourceCapabilities {
646        let source = self.source.capabilities();
647        SourceCapabilities {
648            exact_len: false,
649            exact_weighted_total: false,
650            random_access: false,
651            deterministic_partitioning: source.deterministic_partitioning,
652            predicate_pushdown: false,
653            projection_pushdown: false,
654            streaming: true,
655        }
656    }
657
658    fn batches(&self, plan: ReadPlan) -> LadduDataResult<EventBatchIter> {
659        let batches = self.source.stream_with_plan(plan)?;
660        let filter = self.filter.clone();
661        Ok(Box::new(batches.filter_map(move |batch| {
662            let batch = match batch {
663                Ok(batch) => batch,
664                Err(error) => return Some(Err(error)),
665            };
666            let rows = match filter.rows(&batch) {
667                Ok(rows) => rows,
668                Err(error) => return Some(Err(LadduDataError::Source(error.to_string()))),
669            };
670            (!rows.is_empty()).then(|| Ok(batch.select(&rows)))
671        })))
672    }
673}
674
675impl QueryFilter {
676    fn rows(&self, batch: &EventBatch) -> RuntimeResult<Vec<usize>> {
677        match self {
678            Self::Predicate(predicate) => predicate.evaluate_batch(batch),
679        }
680    }
681}
682
683fn query_error(message: impl Into<String>) -> RuntimeError {
684    RuntimeError::InvalidShape {
685        index: 0,
686        message: message.into(),
687    }
688}
689
690fn may_fallback_to_scalar(execution: &Execution, error: &RuntimeError) -> bool {
691    let cpu_f32 = matches!(
692        error,
693        RuntimeError::Execution(crate::ExecutionError::UnsupportedCpuF32Model)
694    );
695    #[cfg(feature = "wgpu")]
696    {
697        cpu_f32 || (execution.wgpu_context().is_some() && matches!(error, RuntimeError::Wgpu(_)))
698    }
699    #[cfg(not(feature = "wgpu"))]
700    {
701        let _ = execution;
702        cpu_f32
703    }
704}
705
706fn data_error(error: impl ToString) -> RuntimeError {
707    RuntimeError::Data(error.to_string())
708}
709
710#[cfg(test)]
711mod tests {
712    use super::*;
713    use crate::{CpuOptions, Device, ExecutionOptions, Precision};
714    use laddu_compile::CompiledModel;
715    use laddu_data::{
716        data::{EventBatch, OwnedEvent},
717        io::{EventSource, ReadPlan, SourceCapabilities, memory::MemorySource},
718        schema::Schema,
719    };
720    use laddu_expr::{complex, event_scalar};
721    use std::sync::atomic::{AtomicUsize, Ordering};
722
723    #[test]
724    fn bin_spec_roundtrip_preserves_validation() {
725        let bins = BinSpec::edges([-1.0, 0.0, 2.0]).unwrap();
726        let json = serde_json::to_string(&bins).unwrap();
727        assert_eq!(serde_json::from_str::<BinSpec>(&json).unwrap(), bins);
728        assert!(serde_json::from_str::<BinSpec>("[0.0,0.0]").is_err());
729    }
730
731    #[derive(Clone)]
732    struct CountingSource {
733        inner: MemorySource,
734        reads: Arc<AtomicUsize>,
735    }
736
737    impl EventSource for CountingSource {
738        fn schema(&self) -> LadduDataResult<Arc<Schema>> {
739            EventSource::schema(&self.inner)
740        }
741
742        fn capabilities(&self) -> SourceCapabilities {
743            self.inner.capabilities()
744        }
745
746        fn batches(&self, plan: ReadPlan) -> LadduDataResult<EventBatchIter> {
747            self.reads.fetch_add(1, Ordering::Relaxed);
748            self.inner.batches(plan)
749        }
750    }
751
752    #[derive(Clone)]
753    struct FailingSource {
754        schema: Arc<Schema>,
755    }
756
757    impl EventSource for FailingSource {
758        fn schema(&self) -> LadduDataResult<Arc<Schema>> {
759            Ok(Arc::clone(&self.schema))
760        }
761
762        fn capabilities(&self) -> SourceCapabilities {
763            SourceCapabilities {
764                exact_len: false,
765                exact_weighted_total: false,
766                random_access: false,
767                deterministic_partitioning: true,
768                predicate_pushdown: false,
769                projection_pushdown: false,
770                streaming: true,
771            }
772        }
773
774        fn batches(&self, _plan: ReadPlan) -> LadduDataResult<EventBatchIter> {
775            Ok(Box::new(std::iter::once(Err(LadduDataError::Source(
776                "query source failed".into(),
777            )))))
778        }
779    }
780
781    fn capability_tuple(
782        capabilities: SourceCapabilities,
783    ) -> (bool, bool, bool, bool, bool, bool, bool) {
784        (
785            capabilities.exact_len,
786            capabilities.exact_weighted_total,
787            capabilities.random_access,
788            capabilities.deterministic_partitioning,
789            capabilities.predicate_pushdown,
790            capabilities.projection_pushdown,
791            capabilities.streaming,
792        )
793    }
794
795    fn dataset() -> Dataset {
796        let schema = Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], true).unwrap());
797        Dataset::from_events(
798            schema,
799            [
800                OwnedEvent::weighted(vec![], vec![-1.0], 0.5),
801                OwnedEvent::weighted(vec![], vec![0.0], 1.0),
802                OwnedEvent::weighted(vec![], vec![1.0], 1.5),
803                OwnedEvent::weighted(vec![], vec![2.0], 2.0),
804            ],
805        )
806        .unwrap()
807    }
808
809    #[test]
810    fn evaluates_selects_and_bins_dataset_expressions() {
811        let dataset = dataset().chunked(1).unwrap();
812        let execution = Execution::default();
813        let x = event_scalar("x");
814        assert_eq!(
815            dataset.evaluate_real(&x, &execution).unwrap(),
816            vec![-1.0, 0.0, 1.0, 2.0]
817        );
818
819        let selected = dataset
820            .select(
821                &Predicate::ge(x.clone(), 0.0).and(Predicate::lt(x.clone(), 2.0)),
822                &execution,
823            )
824            .unwrap();
825        assert_eq!(
826            selected.map_events(|event| event.scalar(0)).unwrap(),
827            vec![0.0, 1.0]
828        );
829        assert_eq!(selected.sum_weights().unwrap(), 2.5);
830
831        let bins = dataset
832            .bin_by(&x, BinSpec::uniform(2, 0.0, 2.0).unwrap(), &execution)
833            .unwrap();
834        assert_eq!(bins.len(), 2);
835        assert_eq!(
836            bins[0]
837                .dataset()
838                .map_events(|event| event.scalar(0))
839                .unwrap(),
840            vec![0.0]
841        );
842        assert_eq!(
843            bins[1]
844                .dataset()
845                .map_events(|event| event.scalar(0))
846                .unwrap(),
847            vec![1.0, 2.0]
848        );
849    }
850
851    #[test]
852    fn empty_batches_are_valid_query_inputs() {
853        let execution = Execution::default();
854        let x = event_scalar("x");
855
856        let empty_batch_schema =
857            Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], true).unwrap());
858        let empty_batch = Dataset::from_batch(
859            EventBatch::from_events(empty_batch_schema, std::iter::empty::<OwnedEvent>()).unwrap(),
860        );
861        for empty in [empty_batch, dataset().empty_derived().unwrap()] {
862            assert!(empty.evaluate_real(&x, &execution).unwrap().is_empty());
863            assert!(
864                empty
865                    .select(&Predicate::ge(x.clone(), 0.0), &execution)
866                    .unwrap()
867                    .map_events(|event| event.scalar(0))
868                    .unwrap()
869                    .is_empty()
870            );
871        }
872    }
873
874    #[test]
875    fn event_column_nan_comparisons_are_false() {
876        let schema = Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], true).unwrap());
877        let dataset = Dataset::from_events(
878            schema,
879            [
880                OwnedEvent::weighted(vec![], vec![f64::NAN], 1.0),
881                OwnedEvent::weighted(vec![], vec![0.0], 1.0),
882                OwnedEvent::weighted(vec![], vec![1.0], 1.0),
883            ],
884        )
885        .unwrap();
886        let x = event_scalar("x");
887        let selected = dataset
888            .select(&Predicate::ne(x, 0.0), &Execution::default())
889            .unwrap();
890
891        assert_eq!(
892            selected.map_events(|event| event.scalar(0)).unwrap(),
893            vec![1.0]
894        );
895    }
896
897    #[test]
898    fn query_propagates_source_batch_errors() {
899        let schema = Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], true).unwrap());
900        let dataset = Dataset::new(FailingSource { schema });
901
902        let error = dataset
903            .evaluate_real(&event_scalar("x"), &Execution::default())
904            .unwrap_err();
905        assert!(
906            matches!(error, RuntimeError::Data(message) if message.contains("query source failed"))
907        );
908    }
909
910    #[test]
911    fn all_empty_bins_retain_valid_empty_derived_sources() {
912        let source = dataset();
913        let before = capability_tuple(source.capabilities());
914        let bins = source
915            .bin_by(
916                &event_scalar("x"),
917                BinSpec::edges([10.0, 20.0, 30.0]).unwrap(),
918                &Execution::default(),
919            )
920            .unwrap();
921
922        assert_eq!(capability_tuple(source.capabilities()), before);
923        assert_eq!(bins.len(), 2);
924        for bin in bins {
925            assert_eq!(bin.dataset().num_events().unwrap(), Some(0));
926            assert!(
927                bin.dataset()
928                    .evaluate_real(&event_scalar("x"), &Execution::default())
929                    .unwrap()
930                    .is_empty()
931            );
932        }
933    }
934
935    #[test]
936    fn traversing_all_bins_reads_the_source_once() {
937        let reads = Arc::new(AtomicUsize::new(0));
938        let source = CountingSource {
939            inner: match dataset().batches().unwrap().next().unwrap() {
940                Ok(batch) => MemorySource::new(batch),
941                Err(error) => panic!("unexpected source error: {error}"),
942            },
943            reads: Arc::clone(&reads),
944        };
945        let dataset = Dataset::new(source).chunked(1).unwrap();
946        let bins = dataset
947            .bin_by(
948                &event_scalar("x"),
949                BinSpec::uniform(4, -1.0, 3.0).unwrap(),
950                &Execution::default(),
951            )
952            .unwrap();
953
954        let values = bins
955            .into_iter()
956            .map(|bin| {
957                bin.into_dataset()
958                    .map_events(|event| event.scalar(0))
959                    .unwrap()
960            })
961            .collect::<Vec<_>>();
962        assert_eq!(values, [vec![-1.0], vec![0.0], vec![1.0], vec![2.0]]);
963        assert_eq!(reads.load(Ordering::Relaxed), 1);
964    }
965
966    #[test]
967    fn between_predicates_have_explicit_endpoint_semantics() {
968        let dataset = dataset();
969        let execution = Execution::default();
970        let x = event_scalar("x");
971
972        let closed = dataset
973            .select(&Predicate::between(x.clone(), 0.0, 1.0), &execution)
974            .unwrap();
975        assert_eq!(
976            closed.map_events(|event| event.scalar(0)).unwrap(),
977            vec![0.0, 1.0]
978        );
979
980        let open = dataset
981            .select(
982                &Predicate::between_with(x, -1.0, 1.0, IntervalClosure::Open),
983                &execution,
984            )
985            .unwrap();
986        assert_eq!(open.map_events(|event| event.scalar(0)).unwrap(), vec![0.0]);
987    }
988
989    #[test]
990    fn real_queries_reject_complex_and_free_parameter_expressions() {
991        let dataset = dataset();
992        let execution = Execution::default();
993        assert!(
994            dataset
995                .evaluate_real(&complex(1.0, 1.0), &execution)
996                .is_err()
997        );
998        let parameter = Expr::from(laddu_expr::parameters::Parameter::free("p"));
999        assert!(dataset.evaluate_expr(&parameter, &execution).is_err());
1000    }
1001
1002    #[test]
1003    fn compiled_query_outputs_preserve_order_and_values() {
1004        let source = dataset();
1005        let batch = source.batches().unwrap().next().unwrap().unwrap();
1006        let x = event_scalar("x");
1007        let query = QueryExprSet::prepare(
1008            vec![x.clone() + 1.0, x.clone() * 2.0, x],
1009            &Execution::default(),
1010            false,
1011        )
1012        .unwrap();
1013        let values = query.evaluate_batch(&batch).unwrap();
1014        assert_eq!(
1015            values[0].iter().map(|v| v.re).collect::<Vec<_>>(),
1016            [0.0, 1.0, 2.0, 3.0]
1017        );
1018        assert_eq!(
1019            values[1].iter().map(|v| v.re).collect::<Vec<_>>(),
1020            [-2.0, 0.0, 2.0, 4.0]
1021        );
1022        assert_eq!(
1023            values[2].iter().map(|v| v.re).collect::<Vec<_>>(),
1024            [-1.0, 0.0, 1.0, 2.0]
1025        );
1026    }
1027
1028    #[test]
1029    fn repeated_predicate_leaves_are_evaluated_once() {
1030        let x = event_scalar("x");
1031        let selected = dataset()
1032            .select(
1033                &Predicate::ge(x.clone() + 1.0, 0.0).and(Predicate::lt(x + 1.0, 2.0)),
1034                &Execution::default(),
1035            )
1036            .unwrap();
1037        assert_eq!(
1038            selected.map_events(|event| event.scalar(0)).unwrap(),
1039            [-1.0, 0.0]
1040        );
1041    }
1042
1043    #[test]
1044    fn f32_queries_match_f64_query_results() {
1045        let x = event_scalar("x");
1046        let f64_values = dataset().evaluate_real(&x, &Execution::default()).unwrap();
1047        let f32_execution = Execution::local(ExecutionOptions {
1048            device: Device::Cpu(CpuOptions::default()),
1049            precision: Precision::F32,
1050            ..ExecutionOptions::default()
1051        })
1052        .unwrap();
1053        let f32_values = dataset().evaluate_real(&x, &f32_execution).unwrap();
1054        assert_eq!(f32_values, f64_values);
1055    }
1056
1057    #[test]
1058    fn bin_edges_validate_and_nan_predicates_are_false() {
1059        assert!(BinSpec::edges([0.0, 0.0]).is_err());
1060        assert!(!compare(f64::NAN, Comparison::Ne, 0.0));
1061    }
1062
1063    #[test]
1064    fn selection_is_lazy_and_one_pass_binning_preserves_streaming_policy() {
1065        let source = dataset();
1066        let batch = source.batches().unwrap().next().unwrap().unwrap();
1067        let reads = Arc::new(AtomicUsize::new(0));
1068        let dataset = Dataset::new(CountingSource {
1069            inner: MemorySource::new(batch),
1070            reads: Arc::clone(&reads),
1071        })
1072        .streaming();
1073        let execution = Execution::default();
1074        let x = event_scalar("x");
1075
1076        let selected = dataset
1077            .select(&Predicate::ge(x.clone(), 0.0), &execution)
1078            .unwrap();
1079        let bins = dataset
1080            .bin_by(&x, BinSpec::uniform(2, 0.0, 2.0).unwrap(), &execution)
1081            .unwrap();
1082        assert_eq!(reads.load(Ordering::Relaxed), 1);
1083        assert_eq!(
1084            selected.cache_storage(),
1085            laddu_data::data::CacheStorage::Streaming
1086        );
1087
1088        assert_eq!(
1089            selected.map_events(|event| event.scalar(0)).unwrap(),
1090            vec![0.0, 1.0, 2.0]
1091        );
1092        assert_eq!(reads.load(Ordering::Relaxed), 2);
1093        assert_eq!(
1094            bins[0]
1095                .dataset()
1096                .map_events(|event| event.scalar(0))
1097                .unwrap(),
1098            vec![0.0]
1099        );
1100        assert_eq!(reads.load(Ordering::Relaxed), 2);
1101    }
1102
1103    #[test]
1104    fn unknown_cardinality_fastest_discovers_and_retains_small_selection() {
1105        let source = dataset();
1106        let batch = source.batches().unwrap().next().unwrap().unwrap();
1107        let reads = Arc::new(AtomicUsize::new(0));
1108        let dataset = Dataset::new(CountingSource {
1109            inner: MemorySource::new(batch),
1110            reads: Arc::clone(&reads),
1111        });
1112        let execution = Execution::default();
1113        let x = event_scalar("x");
1114        let selected = dataset
1115            .select(&Predicate::ge(x.clone(), 0.0), &execution)
1116            .unwrap();
1117        let compiled = CompiledModel::from_expr(&x).unwrap();
1118        let params = compiled.params().default_values();
1119        let model = PreparedModel::prepare(&compiled, &execution).unwrap();
1120        let prepared = model.prepare_dataset(&execution, &selected).unwrap();
1121
1122        #[cfg(not(feature = "wgpu"))]
1123        let crate::PreparedDataset::Cpu(prepared_cpu) = &prepared;
1124        #[cfg(feature = "wgpu")]
1125        let crate::PreparedDataset::Cpu(prepared_cpu) = &prepared else {
1126            panic!("default execution prepares CPU datasets");
1127        };
1128        assert_eq!(
1129            prepared_cpu.stats().storage(),
1130            laddu_data::data::CacheStorage::Resident
1131        );
1132        assert_eq!(prepared_cpu.stats().local_events(), 3);
1133        assert_eq!(reads.load(Ordering::Relaxed), 2);
1134
1135        for _ in 0..2 {
1136            assert_eq!(
1137                model
1138                    .reduce(
1139                        &execution,
1140                        &params,
1141                        &prepared,
1142                        laddu_compile::ReductionPlan::weighted_real(),
1143                    )
1144                    .unwrap(),
1145                5.5
1146            );
1147        }
1148        assert_eq!(reads.load(Ordering::Relaxed), 2);
1149    }
1150}