1use std::sync::Arc;
2
3use laddu_compile::CompiledModel;
4use laddu_data::{
5 LadduDataError, LadduDataResult,
6 data::{Dataset, EventBatch},
7 io::{EventBatchIter, EventSource, ReadPlan, SourceCapabilities},
8 schema::Schema,
9};
10use laddu_expr::{Expr, ExprShape, ValueKind};
11use num::complex::Complex64;
12use serde::{Deserialize, Deserializer, Serialize};
13
14use crate::{Execution, PreparedModel, RuntimeError, RuntimeResult};
15
16#[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 = QueryExpr::prepare(expr, execution, false)?;
297 let mut output = Vec::new();
298 for batch in self.batches().map_err(data_error)? {
299 output.extend(query.evaluate_batch(&batch.map_err(data_error)?)?);
300 }
301 Ok(output)
302 }
303
304 fn evaluate_real(&self, expr: &Expr, execution: &Execution) -> RuntimeResult<Vec<f64>> {
305 let query = QueryExpr::prepare(expr, execution, true)?;
306 let mut output = Vec::new();
307 for batch in self.batches().map_err(data_error)? {
308 output.extend(
309 query
310 .evaluate_batch(&batch.map_err(data_error)?)?
311 .into_iter()
312 .map(|v| v.re),
313 );
314 }
315 Ok(output)
316 }
317
318 fn select(&self, predicate: &Predicate, execution: &Execution) -> RuntimeResult<Dataset> {
319 let compiled = CompiledPredicate::prepare(predicate, execution)?;
320 Ok(self.with_derived_source(QuerySource {
321 source: self.clone(),
322 filter: QueryFilter::Predicate(Arc::new(compiled)),
323 }))
324 }
325
326 fn bin_by(
327 &self,
328 expr: &Expr,
329 bins: BinSpec,
330 execution: &Execution,
331 ) -> RuntimeResult<Vec<DatasetBin>> {
332 let query = Arc::new(QueryExpr::prepare(expr, execution, true)?);
333 Ok((0..bins.bin_count())
334 .map(|index| DatasetBin {
335 index,
336 lower: bins.edges[index],
337 upper: bins.edges[index + 1],
338 dataset: self.with_derived_source(QuerySource {
339 source: self.clone(),
340 filter: QueryFilter::Bin {
341 query: Arc::clone(&query),
342 bins: bins.clone(),
343 index,
344 },
345 }),
346 })
347 .collect())
348 }
349}
350
351struct QueryExpr {
352 model: PreparedModel,
353 params: laddu_expr::parameters::ParamValues,
354}
355
356impl QueryExpr {
357 fn prepare(expr: &Expr, execution: &Execution, require_real: bool) -> RuntimeResult<Self> {
358 if expr.shape().map_err(|e| query_error(e.to_string()))? != ExprShape::Scalar {
359 return Err(query_error("dataset expressions must be scalar"));
360 }
361 let compiled = CompiledModel::from_expr(expr).map_err(|e| query_error(e.to_string()))?;
362 if compiled.params().n_free() != 0 {
363 return Err(query_error(
364 "dataset expressions cannot contain free parameters",
365 ));
366 }
367 if require_real
368 && compiled
369 .node_facts(compiled.graph().root())
370 .is_some_and(|facts| facts.value_kind == ValueKind::Complex)
371 {
372 return Err(query_error(
373 "this dataset operation requires a real-valued expression",
374 ));
375 }
376 let params = compiled.params().default_values();
377 let model = PreparedModel::prepare(&compiled, execution)?;
378 Ok(Self { model, params })
379 }
380
381 fn evaluate_batch(&self, batch: &EventBatch) -> RuntimeResult<Vec<Complex64>> {
382 self.model.evaluate_batch(&self.params, batch)
383 }
384}
385
386enum CompiledPredicate {
387 Compare {
388 lhs: Box<QueryExpr>,
389 op: Comparison,
390 rhs: Box<QueryExpr>,
391 },
392 And(Box<Self>, Box<Self>),
393 Or(Box<Self>, Box<Self>),
394 Not(Box<Self>),
395 Between {
396 value: Box<QueryExpr>,
397 lower: Box<QueryExpr>,
398 upper: Box<QueryExpr>,
399 closure: IntervalClosure,
400 },
401}
402
403impl CompiledPredicate {
404 fn prepare(predicate: &Predicate, execution: &Execution) -> RuntimeResult<Self> {
405 Ok(match predicate {
406 Predicate::Compare { lhs, op, rhs } => Self::Compare {
407 lhs: Box::new(QueryExpr::prepare(lhs, execution, true)?),
408 op: *op,
409 rhs: Box::new(QueryExpr::prepare(rhs, execution, true)?),
410 },
411 Predicate::And(lhs, rhs) => Self::And(
412 Box::new(Self::prepare(lhs, execution)?),
413 Box::new(Self::prepare(rhs, execution)?),
414 ),
415 Predicate::Or(lhs, rhs) => Self::Or(
416 Box::new(Self::prepare(lhs, execution)?),
417 Box::new(Self::prepare(rhs, execution)?),
418 ),
419 Predicate::Not(inner) => Self::Not(Box::new(Self::prepare(inner, execution)?)),
420 Predicate::Between {
421 value,
422 lower,
423 upper,
424 closure,
425 } => Self::Between {
426 value: Box::new(QueryExpr::prepare(value, execution, true)?),
427 lower: Box::new(QueryExpr::prepare(lower, execution, true)?),
428 upper: Box::new(QueryExpr::prepare(upper, execution, true)?),
429 closure: *closure,
430 },
431 })
432 }
433
434 fn evaluate_batch(&self, batch: &EventBatch) -> RuntimeResult<Vec<bool>> {
435 Ok(match self {
436 Self::Compare { lhs, op, rhs } => lhs
437 .evaluate_batch(batch)?
438 .into_iter()
439 .zip(rhs.evaluate_batch(batch)?)
440 .map(|(lhs, rhs)| compare(lhs.re, *op, rhs.re))
441 .collect(),
442 Self::And(lhs, rhs) => lhs
443 .evaluate_batch(batch)?
444 .into_iter()
445 .zip(rhs.evaluate_batch(batch)?)
446 .map(|(l, r)| l && r)
447 .collect(),
448 Self::Or(lhs, rhs) => lhs
449 .evaluate_batch(batch)?
450 .into_iter()
451 .zip(rhs.evaluate_batch(batch)?)
452 .map(|(l, r)| l || r)
453 .collect(),
454 Self::Not(inner) => inner
455 .evaluate_batch(batch)?
456 .into_iter()
457 .map(|v| !v)
458 .collect(),
459 Self::Between {
460 value,
461 lower,
462 upper,
463 closure,
464 } => value
465 .evaluate_batch(batch)?
466 .into_iter()
467 .zip(lower.evaluate_batch(batch)?)
468 .zip(upper.evaluate_batch(batch)?)
469 .map(|((value, lower), upper)| {
470 let lower_op = match closure {
471 IntervalClosure::Open | IntervalClosure::RightClosed => Comparison::Gt,
472 IntervalClosure::LeftClosed | IntervalClosure::Closed => Comparison::Ge,
473 };
474 let upper_op = match closure {
475 IntervalClosure::Open | IntervalClosure::LeftClosed => Comparison::Lt,
476 IntervalClosure::RightClosed | IntervalClosure::Closed => Comparison::Le,
477 };
478 compare(value.re, lower_op, lower.re) && compare(value.re, upper_op, upper.re)
479 })
480 .collect(),
481 })
482 }
483}
484
485fn compare(lhs: f64, op: Comparison, rhs: f64) -> bool {
486 if lhs.is_nan() || rhs.is_nan() {
487 return false;
488 }
489 match op {
490 Comparison::Lt => lhs < rhs,
491 Comparison::Le => lhs <= rhs,
492 Comparison::Gt => lhs > rhs,
493 Comparison::Ge => lhs >= rhs,
494 Comparison::Eq => lhs == rhs,
495 Comparison::Ne => lhs != rhs,
496 }
497}
498
499#[derive(Clone)]
500struct QuerySource {
501 source: Dataset,
502 filter: QueryFilter,
503}
504
505#[derive(Clone)]
506enum QueryFilter {
507 Predicate(Arc<CompiledPredicate>),
508 Bin {
509 query: Arc<QueryExpr>,
510 bins: BinSpec,
511 index: usize,
512 },
513}
514
515impl EventSource for QuerySource {
516 fn schema(&self) -> LadduDataResult<Arc<Schema>> {
517 self.source.schema()
518 }
519
520 fn capabilities(&self) -> SourceCapabilities {
521 let source = self.source.capabilities();
522 SourceCapabilities {
523 exact_len: false,
524 exact_weighted_total: false,
525 random_access: false,
526 deterministic_partitioning: source.deterministic_partitioning,
527 predicate_pushdown: false,
528 projection_pushdown: false,
529 streaming: true,
530 }
531 }
532
533 fn batches(&self, plan: ReadPlan) -> LadduDataResult<EventBatchIter> {
534 let batches = self.source.batches_with_plan(plan)?;
535 let filter = self.filter.clone();
536 Ok(Box::new(batches.filter_map(move |batch| {
537 let batch = match batch {
538 Ok(batch) => batch,
539 Err(error) => return Some(Err(error)),
540 };
541 let rows = match filter.rows(&batch) {
542 Ok(rows) => rows,
543 Err(error) => return Some(Err(LadduDataError::Source(error.to_string()))),
544 };
545 (!rows.is_empty()).then(|| Ok(batch.select(&rows)))
546 })))
547 }
548}
549
550impl QueryFilter {
551 fn rows(&self, batch: &EventBatch) -> RuntimeResult<Vec<usize>> {
552 match self {
554 Self::Predicate(predicate) => Ok(predicate
555 .evaluate_batch(batch)?
556 .into_iter()
557 .enumerate()
558 .filter_map(|(row, keep)| keep.then_some(row))
559 .collect()),
560 Self::Bin { query, bins, index } => Ok(query
561 .evaluate_batch(batch)?
562 .into_iter()
563 .enumerate()
564 .filter_map(|(row, value)| (bins.index(value.re) == Some(*index)).then_some(row))
565 .collect()),
566 }
567 }
568}
569
570fn query_error(message: impl Into<String>) -> RuntimeError {
571 RuntimeError::InvalidShape {
572 index: 0,
573 message: message.into(),
574 }
575}
576fn data_error(error: impl ToString) -> RuntimeError {
577 RuntimeError::Data(error.to_string())
578}
579
580#[cfg(test)]
581mod tests {
582 use super::*;
583 use laddu_data::{
584 data::OwnedEvent,
585 io::{EventSource, ReadPlan, SourceCapabilities, memory::MemorySource},
586 schema::Schema,
587 };
588 use laddu_expr::{complex, event_scalar};
589 use std::sync::atomic::{AtomicUsize, Ordering};
590
591 #[test]
592 fn bin_spec_roundtrip_preserves_validation() {
593 let bins = BinSpec::edges([-1.0, 0.0, 2.0]).unwrap();
594 let json = serde_json::to_string(&bins).unwrap();
595 assert_eq!(serde_json::from_str::<BinSpec>(&json).unwrap(), bins);
596 assert!(serde_json::from_str::<BinSpec>("[0.0,0.0]").is_err());
597 }
598
599 #[derive(Clone)]
600 struct CountingSource {
601 inner: MemorySource,
602 reads: Arc<AtomicUsize>,
603 }
604
605 impl EventSource for CountingSource {
606 fn schema(&self) -> LadduDataResult<Arc<Schema>> {
607 EventSource::schema(&self.inner)
608 }
609
610 fn capabilities(&self) -> SourceCapabilities {
611 self.inner.capabilities()
612 }
613
614 fn batches(&self, plan: ReadPlan) -> LadduDataResult<EventBatchIter> {
615 self.reads.fetch_add(1, Ordering::Relaxed);
616 self.inner.batches(plan)
617 }
618 }
619
620 fn dataset() -> Dataset {
621 let schema = Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], true).unwrap());
622 Dataset::from_events(
623 schema,
624 [
625 OwnedEvent::weighted(vec![], vec![-1.0], 0.5),
626 OwnedEvent::weighted(vec![], vec![0.0], 1.0),
627 OwnedEvent::weighted(vec![], vec![1.0], 1.5),
628 OwnedEvent::weighted(vec![], vec![2.0], 2.0),
629 ],
630 )
631 .unwrap()
632 }
633
634 #[test]
635 fn evaluates_selects_and_bins_dataset_expressions() {
636 let dataset = dataset().chunked(1).unwrap();
637 let execution = Execution::default();
638 let x = event_scalar("x");
639 assert_eq!(
640 dataset.evaluate_real(&x, &execution).unwrap(),
641 vec![-1.0, 0.0, 1.0, 2.0]
642 );
643
644 let selected = dataset
645 .select(
646 &Predicate::ge(x.clone(), 0.0).and(Predicate::lt(x.clone(), 2.0)),
647 &execution,
648 )
649 .unwrap();
650 assert_eq!(
651 selected.map_events(|event| event.scalar(0)).unwrap(),
652 vec![0.0, 1.0]
653 );
654 assert_eq!(selected.sum_weights().unwrap(), 2.5);
655
656 let bins = dataset
657 .bin_by(&x, BinSpec::uniform(2, 0.0, 2.0).unwrap(), &execution)
658 .unwrap();
659 assert_eq!(bins.len(), 2);
660 assert_eq!(
661 bins[0]
662 .dataset()
663 .map_events(|event| event.scalar(0))
664 .unwrap(),
665 vec![0.0]
666 );
667 assert_eq!(
668 bins[1]
669 .dataset()
670 .map_events(|event| event.scalar(0))
671 .unwrap(),
672 vec![1.0, 2.0]
673 );
674 }
675
676 #[test]
677 fn between_predicates_have_explicit_endpoint_semantics() {
678 let dataset = dataset();
679 let execution = Execution::default();
680 let x = event_scalar("x");
681
682 let closed = dataset
683 .select(&Predicate::between(x.clone(), 0.0, 1.0), &execution)
684 .unwrap();
685 assert_eq!(
686 closed.map_events(|event| event.scalar(0)).unwrap(),
687 vec![0.0, 1.0]
688 );
689
690 let open = dataset
691 .select(
692 &Predicate::between_with(x, -1.0, 1.0, IntervalClosure::Open),
693 &execution,
694 )
695 .unwrap();
696 assert_eq!(open.map_events(|event| event.scalar(0)).unwrap(), vec![0.0]);
697 }
698
699 #[test]
700 fn real_queries_reject_complex_and_free_parameter_expressions() {
701 let dataset = dataset();
702 let execution = Execution::default();
703 assert!(
704 dataset
705 .evaluate_real(&complex(1.0, 1.0), &execution)
706 .is_err()
707 );
708 let parameter = Expr::from(laddu_expr::parameters::Parameter::free("p"));
709 assert!(dataset.evaluate_expr(¶meter, &execution).is_err());
710 }
711
712 #[test]
713 fn bin_edges_validate_and_nan_predicates_are_false() {
714 assert!(BinSpec::edges([0.0, 0.0]).is_err());
715 assert!(!compare(f64::NAN, Comparison::Ne, 0.0));
716 }
717
718 #[test]
719 fn selection_and_binning_are_lazy_and_preserve_streaming_policy() {
720 let source = dataset();
721 let batch = source.batches().unwrap().next().unwrap().unwrap();
722 let reads = Arc::new(AtomicUsize::new(0));
723 let dataset = Dataset::new(CountingSource {
724 inner: MemorySource::new(batch),
725 reads: Arc::clone(&reads),
726 })
727 .streaming();
728 let execution = Execution::default();
729 let x = event_scalar("x");
730
731 let selected = dataset
732 .select(&Predicate::ge(x.clone(), 0.0), &execution)
733 .unwrap();
734 let bins = dataset
735 .bin_by(&x, BinSpec::uniform(2, 0.0, 2.0).unwrap(), &execution)
736 .unwrap();
737 assert_eq!(reads.load(Ordering::Relaxed), 0);
738 assert_eq!(
739 selected.cache_storage(),
740 laddu_data::data::CacheStorage::Streaming
741 );
742
743 assert_eq!(
744 selected.map_events(|event| event.scalar(0)).unwrap(),
745 vec![0.0, 1.0, 2.0]
746 );
747 assert_eq!(reads.load(Ordering::Relaxed), 1);
748 assert_eq!(
749 bins[0]
750 .dataset()
751 .map_events(|event| event.scalar(0))
752 .unwrap(),
753 vec![0.0]
754 );
755 assert_eq!(reads.load(Ordering::Relaxed), 2);
756 }
757}