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#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
18pub enum Comparison {
19 Lt,
21 Le,
23 Gt,
25 Ge,
27 Eq,
29 Ne,
31}
32
33#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
35pub enum IntervalClosure {
36 Open,
38 LeftClosed,
40 RightClosed,
42 #[default]
44 Closed,
45}
46
47#[derive(Clone, Debug)]
49pub enum Predicate {
50 Compare {
52 lhs: Expr,
54 op: Comparison,
56 rhs: Expr,
58 },
59 And(Box<Self>, Box<Self>),
61 Or(Box<Self>, Box<Self>),
63 Not(Box<Self>),
65 Between {
67 value: Expr,
69 lower: Expr,
71 upper: Expr,
73 closure: IntervalClosure,
75 },
76}
77
78impl Predicate {
79 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 pub fn lt(lhs: impl Into<Expr>, rhs: impl Into<Expr>) -> Self {
90 Self::compare(lhs, Comparison::Lt, rhs)
91 }
92 pub fn le(lhs: impl Into<Expr>, rhs: impl Into<Expr>) -> Self {
94 Self::compare(lhs, Comparison::Le, rhs)
95 }
96 pub fn gt(lhs: impl Into<Expr>, rhs: impl Into<Expr>) -> Self {
98 Self::compare(lhs, Comparison::Gt, rhs)
99 }
100 pub fn ge(lhs: impl Into<Expr>, rhs: impl Into<Expr>) -> Self {
102 Self::compare(lhs, Comparison::Ge, rhs)
103 }
104 pub fn eq(lhs: impl Into<Expr>, rhs: impl Into<Expr>) -> Self {
106 Self::compare(lhs, Comparison::Eq, rhs)
107 }
108 pub fn ne(lhs: impl Into<Expr>, rhs: impl Into<Expr>) -> Self {
110 Self::compare(lhs, Comparison::Ne, rhs)
111 }
112 pub fn and(self, rhs: Self) -> Self {
114 Self::And(Box::new(self), Box::new(rhs))
115 }
116 pub fn or(self, rhs: Self) -> Self {
118 Self::Or(Box::new(self), Box::new(rhs))
119 }
120 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 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#[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 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 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 pub fn bin_count(&self) -> usize {
204 self.edges.len() - 1
205 }
206 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#[derive(Clone)]
227pub struct DatasetBin {
228 index: usize,
229 lower: f64,
230 upper: f64,
231 dataset: Dataset,
232}
233
234impl DatasetBin {
235 pub fn index(&self) -> usize {
237 self.index
238 }
239 pub fn lower(&self) -> f64 {
241 self.lower
242 }
243 pub fn upper(&self) -> f64 {
245 self.upper
246 }
247 pub fn dataset(&self) -> &Dataset {
249 &self.dataset
250 }
251 pub fn into_dataset(self) -> Dataset {
253 self.dataset
254 }
255}
256
257pub trait DatasetExprExt {
259 fn evaluate_expr(&self, expr: &Expr, execution: &Execution) -> RuntimeResult<Vec<Complex64>>;
266 fn evaluate_real(&self, expr: &Expr, execution: &Execution) -> RuntimeResult<Vec<f64>>;
273 fn select(&self, predicate: &Predicate, execution: &Execution) -> RuntimeResult<Dataset>;
280 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(¶meter, &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 ¶ms,
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}