radiate_core/stats/expression/
mod.rs1mod aggregate;
2mod builder;
3mod compile;
4mod logical;
5mod ops;
6mod query;
7mod schedule;
8mod select;
9mod traits;
10
11pub use query::MetricQuery;
12pub use select::{MetricField, MetricKind, SelectExpr};
13pub(crate) use traits::ExprResult;
14pub use traits::{Evaluate, ExprSelector};
15
16use aggregate::AggExpr;
17use logical::When;
18use ops::{BinaryExpr, TrinaryExpr, UnaryExpr};
19use radiate_utils::{AnyValue, SmallStr};
20use schedule::{EveryState, ScheduleExpr};
21#[cfg(feature = "serde")]
22use serde::{Deserialize, Serialize};
23
24#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
25#[derive(Clone, Debug, PartialEq)]
26pub enum Expr {
27 Literal(AnyValue<'static>),
28 Selector(SelectExpr),
29 Aggregate(AggExpr),
30 Schedule(ScheduleExpr),
31 Binary(BinaryExpr),
32 Unary(UnaryExpr),
33 Trinary(TrinaryExpr),
34}
35
36impl<T> Evaluate<T> for Expr
37where
38 T: ExprSelector,
39{
40 fn eval<'a>(&'a mut self, metrics: &T) -> ExprResult<'a> {
41 match self {
42 Expr::Literal(value) => Ok(value.clone()),
43 Expr::Selector(selector) => selector.eval(metrics),
44 Expr::Aggregate(child) => child.eval(metrics),
45 Expr::Trinary(child) => child.eval(metrics),
46 Expr::Binary(child) => child.eval(metrics),
47 Expr::Unary(child) => child.eval(metrics),
48 Expr::Schedule(child) => child.eval(metrics),
49 }
50 }
51}
52
53impl Expr {
54 pub fn reset(&mut self) {
62 match self {
63 Expr::Literal(_) | Expr::Selector(_) => {}
64 Expr::Aggregate(a) => a.reset(),
65 Expr::Schedule(ScheduleExpr::Every(s)) => s.reset(),
66 Expr::Binary(b) => {
67 b.lhs.reset();
68 b.rhs.reset();
69 }
70 Expr::Unary(u) => {
71 u.reset();
72 }
73 Expr::Trinary(t) => {
74 t.first.reset();
75 t.second.reset();
76 t.third.reset();
77 }
78 }
79 }
80
81 pub fn lit(value: impl Into<AnyValue<'static>>) -> Expr {
82 Expr::Literal(value.into())
83 }
84
85 pub fn select(name: impl Into<SmallStr>) -> Expr {
86 Expr::Selector(SelectExpr::new(name))
87 }
88
89 pub fn when(cond: impl Into<Expr>) -> When {
90 When::new(cond.into())
91 }
92
93 pub fn every(interval: usize) -> When {
94 When::new(Expr::Schedule(ScheduleExpr::Every(EveryState::new(
95 interval,
96 ))))
97 }
98
99 pub fn identity() -> Expr {
100 Expr::Selector(SelectExpr {
101 metric: None,
102 field: MetricField::LastValue,
103 kind: MetricKind::Value,
104 })
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::ops::UnaryOp;
111 use super::{Evaluate, Expr};
112 use crate::MetricSet;
113 use radiate_utils::{AnyValue, DataType};
114
115 fn is_fused_affine(e: &Expr) -> bool {
116 matches!(e, Expr::Unary(u) if matches!(u.op, UnaryOp::Affine { .. }))
117 }
118
119 fn metrics() -> MetricSet {
120 MetricSet::default()
121 }
122
123 fn f32_val(v: AnyValue<'_>) -> f32 {
124 v.extract::<f32>().expect("expected f32")
125 }
126
127 fn bool_val(v: AnyValue<'_>) -> bool {
128 match v {
129 AnyValue::Bool(b) => b,
130 other => panic!("expected bool, got {other:?}"),
131 }
132 }
133
134 #[test]
137 fn lit_evaluates_to_its_value() {
138 let mut e = Expr::lit(3.14f32);
139 assert!((f32_val(e.eval(&metrics()).unwrap()) - 3.14).abs() < 1e-6);
140 }
141
142 #[test]
143 fn lit_ignores_input() {
144 let mut e = Expr::lit(42.0f32);
145 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 42.0);
147 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 42.0);
148 }
149
150 #[test]
153 fn neg_negates_numeric() {
154 let mut e = Expr::lit(5.0f32).neg();
155 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), -5.0);
156 }
157
158 #[test]
159 fn abs_returns_magnitude() {
160 let mut e = Expr::lit(-7.0f32).abs();
161 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 7.0);
162 }
163
164 #[test]
165 fn not_inverts_bool() {
166 let mut t = Expr::Literal(AnyValue::Bool(true)).not();
167 let mut f = Expr::Literal(AnyValue::Bool(false)).not();
168 assert!(!bool_val(t.eval(&metrics()).unwrap()));
169 assert!(bool_val(f.eval(&metrics()).unwrap()));
170 }
171
172 #[test]
173 fn not_on_non_bool_errors() {
174 let mut e = Expr::lit(1.0f32).not();
175 assert!(e.eval(&metrics()).is_err());
176 }
177
178 #[test]
179 fn cast_f32_to_i32_truncates() {
180 let mut e = Expr::lit(3.9f32).cast(DataType::Int32);
181 let result = e.eval(&metrics()).unwrap();
182 assert_eq!(result.extract::<i32>(), Some(3));
183 }
184
185 #[test]
188 fn add_two_literals() {
189 let mut e = Expr::lit(2.0f32).add(Expr::lit(3.0f32));
190 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 5.0);
191 }
192
193 #[test]
194 fn sub_two_literals() {
195 let mut e = Expr::lit(10.0f32).sub(Expr::lit(3.0f32));
196 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 7.0);
197 }
198
199 #[test]
200 fn mul_two_literals() {
201 let mut e = Expr::lit(4.0f32) * 2.5f32;
202 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 10.0);
203 }
204
205 #[test]
206 fn div_two_literals() {
207 let mut e = Expr::lit(9.0f32).div(Expr::lit(3.0f32));
208 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 3.0);
209 }
210
211 #[test]
212 fn pow_two_literals() {
213 let mut e = Expr::lit(2.0f32).pow(Expr::lit(8.0f32));
214 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 256.0);
215 }
216
217 #[test]
220 fn add_operator_overload() {
221 let mut e = Expr::from(3.0f32) + Expr::from(4.0f32);
222 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 7.0);
223 }
224
225 #[test]
226 fn neg_operator_overload() {
227 let mut e = -Expr::from(5.0f32);
228 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), -5.0);
229 }
230
231 #[test]
232 fn not_operator_overload() {
233 let mut e = !Expr::Literal(AnyValue::Bool(true));
234 assert!(!bool_val(e.eval(&metrics()).unwrap()));
235 }
236
237 #[test]
240 fn lt_lte_gt_gte_correct() {
241 let five = || Expr::lit(5.0f32);
242 let ten = || Expr::lit(10.0f32);
243 let input = &metrics();
244
245 assert!(bool_val(five().lt(ten()).eval(input).unwrap()));
246 assert!(!bool_val(ten().lt(five()).eval(input).unwrap()));
247 assert!(bool_val(five().lte(five()).eval(input).unwrap()));
248 assert!(bool_val(ten().gt(five()).eval(input).unwrap()));
249 assert!(bool_val(ten().gte(ten()).eval(input).unwrap()));
250 assert!(!bool_val(five().gte(ten()).eval(input).unwrap()));
251 }
252
253 #[test]
254 fn eq_and_ne_correct() {
255 let input = &metrics();
256 assert!(bool_val(
257 Expr::lit(5.0f32).eq(Expr::lit(5.0f32)).eval(input).unwrap()
258 ));
259 assert!(!bool_val(
260 Expr::lit(5.0f32).eq(Expr::lit(6.0f32)).eval(input).unwrap()
261 ));
262 assert!(bool_val(
263 Expr::lit(5.0f32).ne(Expr::lit(6.0f32)).eval(input).unwrap()
264 ));
265 }
266
267 #[test]
268 fn between_is_inclusive_on_both_ends() {
269 let input = &metrics();
270 let range = || (Expr::lit(1.0f32), Expr::lit(10.0f32));
271
272 let (lo, hi) = range();
273 assert!(bool_val(
274 Expr::lit(5.0f32).between(lo, hi).eval(input).unwrap()
275 ));
276
277 let (lo, hi) = range();
278 assert!(bool_val(
279 Expr::lit(1.0f32).between(lo, hi).eval(input).unwrap()
280 ));
281
282 let (lo, hi) = range();
283 assert!(bool_val(
284 Expr::lit(10.0f32).between(lo, hi).eval(input).unwrap()
285 ));
286
287 let (lo, hi) = range();
288 assert!(!bool_val(
289 Expr::lit(0.0f32).between(lo, hi).eval(input).unwrap()
290 ));
291 }
292
293 #[test]
296 fn and_or_short_circuit_values() {
297 let input = &metrics();
298 let t = || Expr::Literal(AnyValue::Bool(true));
299 let f = || Expr::Literal(AnyValue::Bool(false));
300
301 assert!(!bool_val(t().and(f()).eval(input).unwrap()));
302 assert!(bool_val(t().and(t()).eval(input).unwrap()));
303 assert!(bool_val(f().or(t()).eval(input).unwrap()));
304 assert!(!bool_val(f().or(f()).eval(input).unwrap()));
305 }
306
307 #[test]
310 fn when_selects_then_branch_on_true() {
311 let mut e = Expr::when(Expr::Literal(AnyValue::Bool(true)))
312 .then(Expr::lit(1.0f32))
313 .otherwise(Expr::lit(2.0f32));
314 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 1.0);
315 }
316
317 #[test]
318 fn when_selects_otherwise_branch_on_false() {
319 let mut e = Expr::when(Expr::Literal(AnyValue::Bool(false)))
320 .then(Expr::lit(1.0f32))
321 .otherwise(Expr::lit(2.0f32));
322 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 2.0);
323 }
324
325 #[test]
326 fn when_condition_can_be_a_comparison() {
327 let mut e = Expr::when(Expr::lit(5.0f32).gt(Expr::lit(3.0f32)))
328 .then(Expr::lit(100.0f32))
329 .otherwise(Expr::lit(0.0f32));
330 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 100.0);
331 }
332
333 #[test]
336 fn clamp_below_min_returns_min() {
337 let mut e = Expr::lit(-5.0f32).clamp(Expr::lit(0.0f32), Expr::lit(1.0f32));
338 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 0.0);
339 }
340
341 #[test]
342 fn clamp_above_max_returns_max() {
343 let mut e = Expr::lit(10.0f32).clamp(Expr::lit(0.0f32), Expr::lit(1.0f32));
344 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 1.0);
345 }
346
347 #[test]
348 fn clamp_within_range_unchanged() {
349 let mut e = Expr::lit(0.5f32).clamp(Expr::lit(0.0f32), Expr::lit(1.0f32));
350 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 0.5);
351 }
352
353 #[test]
354 fn clamp_null_input_returns_min() {
355 let mut e = Expr::Literal(AnyValue::Null).clamp(Expr::lit(0.05f32), Expr::lit(2.0f32));
356 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 0.05);
357 }
358
359 #[test]
360 fn clamp_nan_input_returns_min() {
361 let mut e = Expr::lit(f32::NAN).clamp(Expr::lit(0.05f32), Expr::lit(2.0f32));
362 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 0.05);
363 }
364
365 #[test]
366 fn clamp_pos_inf_input_returns_min() {
367 let mut e = Expr::lit(f32::INFINITY).clamp(Expr::lit(0.05f32), Expr::lit(2.0f32));
368 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 0.05);
369 }
370
371 #[test]
372 fn clamp_neg_inf_input_returns_min() {
373 let mut e = Expr::lit(f32::NEG_INFINITY).clamp(Expr::lit(0.05f32), Expr::lit(2.0f32));
374 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 0.05);
375 }
376
377 #[test]
378 fn clamp_missing_bounds_errors() {
379 let mut e = Expr::lit(0.5f32).clamp(Expr::Literal(AnyValue::Null), Expr::lit(2.0f32));
380 assert!(e.eval(&metrics()).is_err());
381 }
382
383 #[test]
386 fn or_else_finite_passes_through() {
387 let mut e = Expr::lit(3.0f32).or_else(Expr::lit(99.0f32));
388 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 3.0);
389 }
390
391 #[test]
392 fn or_else_null_falls_back() {
393 let mut e = Expr::Literal(AnyValue::Null).or_else(Expr::lit(99.0f32));
394 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 99.0);
395 }
396
397 #[test]
398 fn or_else_nan_falls_back() {
399 let mut e = Expr::lit(f32::NAN).or_else(Expr::lit(99.0f32));
400 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 99.0);
401 }
402
403 #[test]
404 fn or_else_inf_falls_back() {
405 let mut e = Expr::lit(f32::INFINITY).or_else(Expr::lit(99.0f32));
406 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 99.0);
407 }
408
409 #[test]
410 fn or_else_neg_inf_falls_back() {
411 let mut e = Expr::lit(f32::NEG_INFINITY).or_else(Expr::lit(99.0f32));
412 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 99.0);
413 }
414
415 #[test]
416 fn or_else_chains_through_bad_values() {
417 let mut e = Expr::Literal(AnyValue::Null)
418 .or_else(Expr::lit(f32::NAN))
419 .or_else(Expr::lit(7.0f32));
420 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 7.0);
421 }
422
423 #[test]
426 fn min_with_picks_smaller() {
427 let mut e = Expr::lit(5.0f32).min_with(Expr::lit(3.0f32));
428 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 3.0);
429 }
430
431 #[test]
432 fn max_with_picks_larger() {
433 let mut e = Expr::lit(5.0f32).max_with(Expr::lit(8.0f32));
434 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 8.0);
435 }
436
437 #[test]
438 fn min_with_nan_on_one_side_returns_other() {
439 let mut e = Expr::lit(5.0f32).min_with(Expr::lit(f32::NAN));
441 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 5.0);
442 }
443
444 #[test]
445 fn max_with_nan_on_one_side_returns_other() {
446 let mut e = Expr::lit(5.0f32).max_with(Expr::lit(f32::NAN));
447 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 5.0);
448 }
449
450 #[test]
451 fn floor_via_max_with_constant() {
452 let mut e = Expr::lit(-3.0f32).max_with(Expr::lit(0.0f32));
454 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 0.0);
455 }
456
457 #[test]
460 fn reset_clears_schedule_counter() {
461 let mut e = Expr::every(3)
464 .then(Expr::lit(1.0f32))
465 .otherwise(Expr::lit(0.0f32));
466
467 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 0.0);
468 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 0.0);
469
470 e.reset();
471
472 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 0.0);
475 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 0.0);
476 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 1.0);
478 }
479
480 #[test]
481 fn reset_idempotent_on_leaf() {
482 let mut e = Expr::lit(42.0f32);
483 e.reset();
484 e.reset();
485 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 42.0);
486 }
487
488 #[test]
491 fn every_fires_on_nth_call_then_resets() {
492 let mut e = Expr::every(3)
493 .then(Expr::Literal(AnyValue::Bool(true)))
494 .otherwise(Expr::Literal(AnyValue::Bool(false)));
495
496 assert!(!bool_val(e.eval(&metrics()).unwrap())); assert!(!bool_val(e.eval(&metrics()).unwrap())); assert!(bool_val(e.eval(&metrics()).unwrap())); assert!(!bool_val(e.eval(&metrics()).unwrap())); assert!(!bool_val(e.eval(&metrics()).unwrap())); assert!(bool_val(e.eval(&metrics()).unwrap())); }
503
504 fn metrics_with(name: &str, value: f32) -> MetricSet {
507 let mut ms = MetricSet::new();
508 ms.upsert(name, value);
509 ms
510 }
511
512 #[test]
513 fn error_from_method_collapses_to_affine() {
514 let e = Expr::lit(15.0f32).error(10.0);
516 assert!(is_fused_affine(&e), "expected fused Affine, got {e:?}");
517 let mut e = e;
518 assert!((f32_val(e.eval(&metrics()).unwrap()) - 0.5).abs() < 1e-6);
519 }
520
521 #[test]
522 fn error_from_function_reads_metric() {
523 let ms = metrics_with("foo", 12.0);
524 let mut e = Expr::select("foo").error(10.0);
525 assert!((f32_val(e.eval(&ms).unwrap()) - 0.2).abs() < 1e-6);
527 }
528
529 #[test]
532 fn quantile_stream_returns_first_sample_until_buffer_fills() {
533 let mut e = Expr::select("foo").quantile(0.5);
534 let ms = metrics_with("foo", 5.0);
535 assert!((f32_val(e.eval(&ms).unwrap()) - 5.0).abs() < 1e-6);
537 }
538
539 #[test]
540 fn quantile_stream_null_when_metric_missing() {
541 let mut e = Expr::select("missing").quantile(0.95);
542 let ms = MetricSet::new();
543 assert!(matches!(e.eval(&ms).unwrap(), AnyValue::Null));
544 }
545
546 #[test]
547 fn quantile_stream_converges_on_uniform_sequence() {
548 let mut e = Expr::select("foo").quantile(0.5);
549 let mut ms = MetricSet::new();
550 for i in 1..=200 {
551 ms.upsert("foo", i as f32);
552 let _ = e.eval(&ms);
553 }
554 let v = f32_val(e.eval(&ms).unwrap());
556 assert!(
557 (v - 100.5).abs() < 3.0,
558 "p50 estimate {v} far from true median 100.5"
559 );
560 }
561
562 #[test]
563 fn quantile_stream_p95_approximates_high_tail() {
564 let mut e = Expr::select("foo").quantile(0.95);
565 let mut ms = MetricSet::new();
566 for i in 1..=1000 {
567 ms.upsert("foo", i as f32);
568 let _ = e.eval(&ms);
569 }
570 let v = f32_val(e.eval(&ms).unwrap());
571 assert!((v - 950.0).abs() < 20.0, "p95 estimate {v} far from 950");
572 }
573
574 #[test]
575 fn quantile_stream_reset_clears_estimator() {
576 let mut e = Expr::select("foo").quantile(0.5);
577 let mut ms = MetricSet::new();
578 for i in 1..=50 {
579 ms.upsert("foo", i as f32);
580 let _ = e.eval(&ms);
581 }
582 e.reset();
583 ms.upsert("foo", 7.0);
585 let v = f32_val(e.eval(&ms).unwrap());
586 assert!((v - 7.0).abs() < 1e-6, "got {v}");
587 }
588
589 #[test]
590 fn quantile_stream_composes_with_arbitrary_child() {
591 let mut e = Expr::lit(42.0f32).quantile(0.5);
593 let ms = metrics();
594 let _ = e.eval(&ms);
595 let _ = e.eval(&ms);
596 assert!((f32_val(e.eval(&ms).unwrap()) - 42.0).abs() < 1e-6);
598 }
599
600 #[test]
603 fn stagnation_increments_when_value_unchanged() {
604 let ms = metrics_with("score", 1.0);
605 let mut e = Expr::select("score").stagnation(0.001);
606
607 assert_eq!(f32_val(e.eval(&ms).unwrap()), 0.0); assert_eq!(f32_val(e.eval(&ms).unwrap()), 1.0);
609 assert_eq!(f32_val(e.eval(&ms).unwrap()), 2.0);
610 }
611
612 #[test]
613 fn stagnation_resets_on_large_change() {
614 let mut ms = metrics_with("score", 1.0);
615 let mut e = Expr::select("score").stagnation(0.001);
616
617 let _ = e.eval(&ms);
618 let _ = e.eval(&ms); ms.upsert("score", 5.0); assert_eq!(f32_val(e.eval(&ms).unwrap()), 0.0);
622 assert_eq!(f32_val(e.eval(&ms).unwrap()), 1.0);
623 }
624
625 #[test]
626 fn stagnation_tolerates_tiny_noise() {
627 let mut ms = metrics_with("score", 1.0);
628 let mut e = Expr::select("score").stagnation(0.01);
629
630 let _ = e.eval(&ms);
631 ms.upsert("score", 1.005); assert_eq!(f32_val(e.eval(&ms).unwrap()), 1.0);
633 ms.upsert("score", 1.008);
634 assert_eq!(f32_val(e.eval(&ms).unwrap()), 2.0);
635 }
636
637 #[test]
638 fn stagnation_returns_null_when_metric_missing() {
639 let ms = MetricSet::new();
640 let mut e = Expr::select("missing").stagnation(0.001);
641 assert!(matches!(e.eval(&ms).unwrap(), AnyValue::Null));
642 }
643
644 #[test]
645 fn is_stagnant_fires_at_patience_threshold() {
646 let ms = metrics_with("score", 1.0);
647 let mut e = Expr::select("score").stagnation(0.001).gte(3);
648
649 assert!(!bool_val(e.eval(&ms).unwrap())); assert!(!bool_val(e.eval(&ms).unwrap())); assert!(!bool_val(e.eval(&ms).unwrap())); assert!(bool_val(e.eval(&ms).unwrap())); }
654
655 #[test]
656 fn stagnation_reset_clears_state() {
657 let ms = metrics_with("score", 1.0);
658 let mut e = Expr::select("score").stagnation(0.001);
659
660 let _ = e.eval(&ms);
661 let _ = e.eval(&ms);
662 let _ = e.eval(&ms); e.reset();
665 assert_eq!(f32_val(e.eval(&ms).unwrap()), 0.0); }
667
668 #[test]
669 fn is_converged_fires_when_window_is_flat() {
670 }
679
680 #[test]
681 fn is_converged_does_not_fire_when_window_drifts() {
682 }
692
693 #[test]
696 fn compile_folds_pure_literal_subtree() {
697 let e = Expr::lit(2.0f32).add(Expr::lit(3.0f32)).compile();
698 assert!(matches!(e, Expr::Literal(_)));
699 let mut e = e;
700 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 5.0);
701 }
702
703 #[test]
704 fn compile_wraps_metric_plus_lit_as_affine() {
705 let e = Expr::select("foo").add(Expr::lit(3.0f32)).compile();
706 assert!(is_fused_affine(&e), "expected Affine, got {e:?}");
707 }
708
709 #[test]
710 fn compile_collapses_controller_chain_to_single_affine() {
711 let target = 10.0f32;
715 let gain = 0.999f32;
716 let e = Expr::select("count.species")
717 .sub(Expr::lit(target))
718 .div(Expr::lit(target))
719 .mul(Expr::lit(gain))
720 .add(Expr::lit(1.0f32))
721 .compile();
722
723 assert!(is_fused_affine(&e), "expected single Affine, got {e:?}");
724 }
725
726 #[test]
727 fn compile_is_idempotent() {
728 let e = Expr::select("foo")
729 .sub(Expr::lit(1.0f32))
730 .mul(Expr::lit(2.0f32))
731 .add(Expr::lit(3.0f32));
732 let once = e.clone().compile();
733 let twice = once.clone().compile();
734 assert_eq!(format!("{:?}", once), format!("{:?}", twice));
735 }
736
737 #[test]
742 fn composed_expr_add_then_compare() {
743 let mut e = Expr::lit(2.0f32)
745 .add(Expr::lit(3.0f32))
746 .gt(Expr::lit(4.0f32));
747 assert!(bool_val(e.eval(&metrics()).unwrap()));
748 }
749
750 #[test]
751 fn composed_expr_clamp_then_scale() {
752 let mut e = Expr::lit(-5.0f32)
754 .clamp(Expr::lit(0.0f32), Expr::lit(1.0f32))
755 .mul(Expr::lit(10.0f32));
756 assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 0.0);
757 }
758
759 #[test]
760 fn test_identity_select() {
761 let mut e = Expr::identity().rolling(3).sum();
762
763 for i in 0..5 {
764 let output = e.eval(&i);
765
766 if i < 2 {
767 assert_eq!(f32_val(output.unwrap()), (0..=i).sum::<i32>() as f32);
768 } else {
769 assert_eq!(f32_val(output.unwrap()), (i - 2..=i).sum::<i32>() as f32);
770 }
771 }
772 }
773}