1#![allow(clippy::bool_comparison)]
2#![allow(clippy::unnecessary_cast)]
3
4mod comparison;
5mod ite;
6pub use comparison::{CompEq, CompGT, CompGTE, CompLT, CompLTE, CompNE};
7pub use comparison::{comp_eq, comp_gt, comp_gte, comp_lt, comp_lte, comp_ne};
8pub use ite::IfThenElse;
9
10use ndarray::*;
11
12use crate::broadcast::multi_broadcast;
13use crate::internal::*;
14
15bin_to_super_type!(and, And,
16 neutral_element: 1,
17 absorbing_element: 0,
18 [bool, u8, u16, u32, u64, i8, i16, i32, i64] => |c, &a, &b| *c = (a as i64 != 0 && b as i64 != 0) as _);
19bin_to_super_type!(or, Or,
20 neutral_element: 0,
21 absorbing_element: 1,
22 [bool, u8, u16, u32, u64, i8, i16, i32, i64] => |c, &a, &b| *c = (a as i64 != 0 || b as i64 != 0) as _);
23bin_to_super_type!(xor, Xor, declutter: declutter_xor, neutral_element: 0, [bool] => |c, &a, &b| *c = a ^ b);
24
25fn declutter_xor(
26 _op: &Xor,
27 model: &TypedModel,
28 node: &TypedNode,
29) -> TractResult<Option<TypedModelPatch>> {
30 if let Some(uniform) = crate::ops::binary::one_input_is_uniform(model, node)?
32 && tensor0(1i64).close_enough(&uniform.uni, false).is_ok()
33 {
34 return Ok(Some(TypedModelPatch::replace_single_op(
35 model,
36 node,
37 &[uniform.var],
38 crate::ops::element_wise::ElementWiseOp(Box::new(Not {}), None),
39 )?));
40 }
41 Ok(None)
42}
43
44element_wise!(not, Not, [bool] => |_, vs| {
45 vs.iter_mut().for_each(|a| *a = !*a);
46 Ok(())
47});
48
49#[derive(Debug, Clone, new, Default, Hash, PartialEq, Eq)]
50pub struct Iff;
51
52impl Iff {
53 pub unsafe fn eval_t<T: Datum>(
54 cond: &ArrayViewD<bool>,
55 out: &mut Tensor,
56 t: &Tensor,
57 f: &Tensor,
58 ) {
59 unsafe {
60 Zip::from(out.to_array_view_mut_unchecked::<T>())
61 .and_broadcast(cond)
62 .and_broadcast(t.to_array_view_unchecked::<T>())
63 .and_broadcast(f.to_array_view_unchecked::<T>())
64 .for_each(|r, c, t, f| *r = if *c { t.clone() } else { f.clone() })
65 }
66 }
67}
68
69impl Op for Iff {
70 fn name(&self) -> StaticName {
71 "Iff".into()
72 }
73 op_as_typed_op!();
74}
75
76impl EvalOp for Iff {
77 op_out_of_plan!();
78
79 fn eval(&self, _ctx: &EvalContext, inputs: TVec<TValue>) -> TractResult<TVec<TValue>> {
80 let (cond, t, f) = args_3!(inputs);
81 anyhow::ensure!(t.datum_type() == f.datum_type());
82 let shape: TVec<usize> = multi_broadcast(&[cond.shape(), t.shape(), f.shape()])?;
83 unsafe {
84 let mut result = Tensor::uninitialized_dt(t.datum_type(), &shape)?;
85 let cond = cond.to_plain_array_view::<bool>()?;
86 dispatch_datum_by_size!(Self::eval_t(t.datum_type())(&cond, &mut result, &t, &f));
87 Ok(tvec!(result.into_tvalue()))
88 }
89 }
90}
91
92pub fn sym_to_coord_axis(sym: &Symbol) -> Option<usize> {
93 format!("{sym}").strip_prefix("🎯")?.parse::<usize>().ok()
94}
95
96pub(crate) fn coord_bound_assertions(expr: &TDim, shape: &ShapeFact) -> Vec<Assertion> {
97 expr.symbols()
98 .into_iter()
99 .filter_map(|s| sym_to_coord_axis(&s).filter(|k| *k < shape.rank()).map(|k| (k, s)))
100 .flat_map(|(k, sym)| {
101 [
102 Assertion::GTE(TDim::Sym(sym.clone()), TDim::Val(0)),
103 Assertion::LTE(TDim::Sym(sym), shape[k].clone() - TDim::Val(1)),
104 ]
105 })
106 .collect()
107}
108
109pub(crate) fn is_provably_all_false(expr: &TDim, shape: &ShapeFact) -> bool {
110 let extra = coord_bound_assertions(expr, shape);
111 expr.clone().simplify_with_extra_assertions(&extra) == TDim::Val(0)
112}
113
114pub(crate) fn is_provably_all_true(expr: &TDim, shape: &ShapeFact) -> bool {
115 let extra = coord_bound_assertions(expr, shape);
116 expr.clone().simplify_with_extra_assertions(&extra) == TDim::Val(1)
117}
118
119#[derive(Debug, Clone)]
131pub(crate) struct TrueRange {
132 pub axis: usize,
133 pub start: Option<TDim>, pub end: Option<TDim>, }
136
137impl TrueRange {
138 pub fn is_full(&self) -> bool {
140 self.start.is_none() && self.end.is_none()
141 }
142 pub fn is_empty(&self) -> bool {
144 match (&self.start, &self.end) {
145 (None, Some(e)) => *e == TDim::Val(0),
146 (Some(s), Some(e)) => s == e,
147 _ => false,
148 }
149 }
150}
151
152pub(crate) fn classify_true_range(expr: &TDim, shape: &ShapeFact) -> Option<TrueRange> {
153 fn try_ge(ge: &TDim, shape: &ShapeFact) -> Option<(usize, TDim)> {
154 if let TDim::Ge(lhs, rhs) = ge
155 && let TDim::Sym(sym) = &**lhs
156 {
157 let k = sym_to_coord_axis(sym)?;
158 if k < shape.rank() && !rhs.symbols().contains(sym) {
159 return Some((k, *rhs.clone()));
160 }
161 }
162 None
163 }
164
165 let simplified = expr.clone().simplify();
166 if simplified == TDim::Val(0) || is_provably_all_false(&simplified, shape) {
168 return Some(TrueRange { axis: 0, start: None, end: Some(TDim::Val(0)) });
169 }
170 if simplified == TDim::Val(1) || is_provably_all_true(&simplified, shape) {
172 return Some(TrueRange { axis: 0, start: None, end: None });
173 }
174 if let Some((axis, split)) = try_ge(&simplified, shape) {
176 return Some(TrueRange { axis, start: Some(split), end: None });
177 }
178 let flipped = (TDim::Val(1) - simplified).simplify();
180 if let Some((axis, split)) = try_ge(&flipped, shape) {
181 return Some(TrueRange { axis, start: None, end: Some(split) });
182 }
183 None
184}
185
186impl TypedOp for Iff {
187 as_op!();
188
189 fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult<TVec<TypedFact>> {
190 ensure!(inputs.len() == 3, "Iff expects 3 intputs.");
191 ensure!(inputs[1].datum_type == inputs[2].datum_type);
192 ensure!(inputs[0].datum_type.is::<bool>());
193 ensure!(inputs[0].rank() == inputs[1].rank());
194 ensure!(inputs[0].rank() == inputs[2].rank());
195 let shape = multi_broadcast(&[
196 inputs[0].shape.to_tvec(),
197 inputs[1].shape.to_tvec(),
198 inputs[2].shape.to_tvec(),
199 ])
200 .unwrap();
201 let mut fact = inputs[1].datum_type.fact(shape);
202 fact.uniform_tdim = match inputs[0].uniform_tdim.as_ref().map(|d| d.clone().simplify()) {
204 Some(TDim::Val(0)) => inputs[2].uniform_tdim.clone(), Some(TDim::Val(_)) => inputs[1].uniform_tdim.clone(), _ => None,
207 };
208 Ok(tvec!(fact))
209 }
210
211 fn input_roi(
212 &self,
213 model: &TypedModel,
214 node: &TypedNode,
215 ) -> TractResult<Option<TVec<Option<TDim>>>> {
216 let cond_fact = model.outlet_fact(node.inputs[0])?;
220 if let Some(cond_expr) = &cond_fact.uniform_tdim {
221 let cond = cond_expr.clone().simplify();
222 let not_cond = TDim::Eq(Box::new(cond.clone()), Box::new(TDim::Val(0))).simplify();
223 return Ok(Some(tvec![None, Some(cond), Some(not_cond)]));
224 }
225 crate::optim::propagate_roi::bubble_roi(model, node)
227 }
228
229 fn declutter(
230 &self,
231 model: &TypedModel,
232 node: &TypedNode,
233 ) -> TractResult<Option<TypedModelPatch>> {
234 let cond_fact = model.outlet_fact(node.inputs[0])?;
238 rule_if_some!(uniform = &cond_fact.uniform);
239 rule_if_let!(Ok(cond_val) = uniform.cast_to_scalar::<bool>());
240 let branch = if cond_val { node.inputs[1] } else { node.inputs[2] };
241 let mut patch = TypedModelPatch::default();
242 let mut wire = patch.tap_model(model, branch)?;
243 let out_shape = &model.outlet_fact(node.id.into())?.shape;
246 if &model.outlet_fact(branch)?.shape != out_shape {
247 wire = patch.wire_node(
248 format!("{}.broadcast", node.name),
249 crate::ops::array::MultiBroadcastTo::new(out_shape.clone()),
250 &[wire],
251 )?[0];
252 }
253 patch.shunt_outside(model, node.id.into(), wire)?;
254 Ok(Some(patch))
255 }
256
257 fn axes_mapping(
258 &self,
259 inputs: &[&TypedFact],
260 outputs: &[&TypedFact],
261 ) -> TractResult<AxesMapping> {
262 AxesMapping::natural(inputs, outputs)
263 }
264}
265
266bin_to_super_type!(bitand, BitAnd,
267 absorbing_element: 0,
268 [bool, u8, u16, u32, u64, i8, i16, i32, i64] => |c, &a, &b| *c = a & b);
269bin_to_super_type!(bitor, BitOr,
270 neutral_element: 0,
271 [bool, u8, u16, u32, u64, i8, i16, i32, i64] => |c, &a, &b| *c = a | b);
272bin_to_super_type!(bitxor, BitXor,
273 declutter: declutter_bitxor,
274 neutral_element: 0,
275 [bool, u8, u16, u32, u64, i8, i16, i32, i64] => |c, &a, &b| *c = a ^ b);
276
277fn declutter_bitxor(
278 _op: &BitXor,
279 model: &TypedModel,
280 node: &TypedNode,
281) -> TractResult<Option<TypedModelPatch>> {
282 if let Some(uniform) = crate::ops::binary::one_input_is_uniform(model, node)? {
284 let var_dt = model.outlet_fact(uniform.var)?.datum_type;
285 let is_all_ones = if var_dt.is::<bool>() {
286 tensor0(1i64).close_enough(&uniform.uni, false).is_ok()
287 } else {
288 tensor0(-1i64).close_enough(&uniform.uni, false).is_ok()
289 };
290 if is_all_ones {
291 return Ok(Some(TypedModelPatch::replace_single_op(
292 model,
293 node,
294 &[uniform.var],
295 crate::ops::element_wise::ElementWiseOp(Box::new(BitNot {}), None),
296 )?));
297 }
298 }
299 Ok(None)
300}
301
302element_wise!(bitnot, BitNot, [bool, u8, u16, u32, u64, i8, i16, i32, i64] => |_, xs| {
303 xs.iter_mut().for_each(|x| *x = !*x);
304 Ok(())
305});
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310 use crate::ops::array::TypedConcat;
311 use crate::ops::binary::TypedBinOp;
312 use crate::ops::change_axes::AxisOp;
313
314 #[test]
317 fn iff_fold_case1_eq_t_zero() -> TractResult<()> {
318 let mut model = TypedModel::default();
319 model.symbols.add_assertion("T >= 1")?;
320 let t_sym = model.symbols.sym("T");
321 let t_dim = TDim::Sym(t_sym.clone());
322
323 let t_wire = model.wire_node(
325 "T",
326 crate::ops::konst::Const::new(tensor0(t_dim.clone()).into_arc_tensor())?,
327 &[],
328 )?[0];
329
330 let zero_wire = model.wire_node(
332 "zero",
333 crate::ops::konst::Const::new(tensor0(TDim::Val(0)).into_arc_tensor())?,
334 &[],
335 )?[0];
336
337 let eq_wire = model.wire_node("eq", TypedBinOp(comp_eq(), None), &[t_wire, zero_wire])?[0];
339
340 let data_wire = model.add_source("data", TDim::datum_type().scalar_fact())?;
342
343 let iff_wire = model.wire_node("iff", Iff, &[eq_wire, zero_wire, data_wire])?[0];
345 model.select_output_outlets(&[iff_wire])?;
346
347 let model = model.into_decluttered()?;
348
349 let iff_count = model.nodes().iter().filter(|n| n.op_as::<Iff>().is_some()).count();
351 assert_eq!(iff_count, 0, "Expected Iff to be folded, but found {iff_count} Iff nodes");
352 Ok(())
353 }
354
355 #[test]
359 fn iff_fold_case2_not_lt_x1_t() -> TractResult<()> {
360 use crate::ops::array::Range;
361
362 let mut model = TypedModel::default();
363 model.symbols.add_assertion("T >= 1")?;
364 let t_sym = model.symbols.sym("T");
365 let t_dim = TDim::Sym(t_sym.clone());
366
367 let start = model.wire_node(
369 "start",
370 crate::ops::konst::Const::new(tensor0(TDim::Val(0)).into_arc_tensor())?,
371 &[],
372 )?[0];
373 let step = model.wire_node(
374 "step",
375 crate::ops::konst::Const::new(tensor0(TDim::Val(1)).into_arc_tensor())?,
376 &[],
377 )?[0];
378 let end = model.add_source("T_dyn", TDim::datum_type().scalar_fact())?;
381
382 let range = model.wire_node("range", Range::new(t_dim.clone()), &[start, end, step])?[0];
384
385 let range_unsq = model.wire_node("range_unsq", AxisOp::Add(0), &[range])?[0];
387
388 let t_const = model.wire_node(
390 "T_const",
391 crate::ops::konst::Const::new(tensor0(t_dim.clone()).into_arc_tensor())?,
392 &[],
393 )?[0];
394 let t_unsq = model.wire_node("T_unsq", AxisOp::Add(0), &[t_const])?[0];
396 let t_unsq2 = model.wire_node("T_unsq2", AxisOp::Add(0), &[t_unsq])?[0];
397
398 let lt = model.wire_node("lt", TypedBinOp(comp_lt(), None), &[range_unsq, t_unsq2])?[0];
400
401 let bn = model.wire_node("bitnot", bitnot(), &[lt])?[0];
404
405 let data_shape = tvec![TDim::Val(1), t_dim.clone()];
407 let data = model.add_source("data", TDim::datum_type().fact(data_shape.clone()))?;
408
409 let zero_scalar = model.wire_node(
411 "zero_s",
412 crate::ops::konst::Const::new(tensor0(TDim::Val(0)).into_arc_tensor())?,
413 &[],
414 )?[0];
415 let zeros = model.wire_node(
416 "zeros",
417 crate::ops::array::MultiBroadcastTo {
418 shape: ShapeFact::from_dims(data_shape.iter().cloned()),
419 },
420 &[zero_scalar],
421 )?[0];
422
423 let iff = model.wire_node("iff", Iff, &[bn, zeros, data])?[0];
425 model.select_output_outlets(&[iff])?;
426
427 let model = model.into_decluttered()?;
428
429 let iff_count = model.nodes().iter().filter(|n| n.op_as::<Iff>().is_some()).count();
430 assert_eq!(iff_count, 0, "Expected Iff to be folded, but found {iff_count} Iff nodes");
431 Ok(())
432 }
433
434 #[test]
436 fn iff_split_to_slice_concat() -> TractResult<()> {
437 use crate::ops::array::Range;
438
439 let mut model = TypedModel::default();
440 model.symbols.add_assertion("T >= 160")?;
441 let t_sym = model.symbols.sym("T");
442 let t_dim = TDim::Sym(t_sym.clone());
443
444 let split = t_dim.clone() / 160;
446 let out_len = TDim::Val(1) + split.clone();
448
449 let start = model.wire_node(
455 "start",
456 crate::ops::konst::Const::new(tensor0(TDim::Val(0)).into_arc_tensor())?,
457 &[],
458 )?[0];
459 let step = model.wire_node(
460 "step",
461 crate::ops::konst::Const::new(tensor0(TDim::Val(1)).into_arc_tensor())?,
462 &[],
463 )?[0];
464 let end_val = model.wire_node(
465 "end_val",
466 crate::ops::konst::Const::new(tensor0(out_len.clone()).into_arc_tensor())?,
467 &[],
468 )?[0];
469 let range =
470 model.wire_node("range", Range::new(out_len.clone()), &[start, end_val, step])?[0];
471 let r1 = model.wire_node("r1", AxisOp::Add(0), &[range])?[0];
473 let r2 = model.wire_node("r2", AxisOp::Add(0), &[r1])?[0];
475
476 let split_const = model.wire_node(
478 "split_const",
479 crate::ops::konst::Const::new(tensor0(split.clone()).into_arc_tensor())?,
480 &[],
481 )?[0];
482 let sc1 = model.wire_node("sc1", AxisOp::Add(0), &[split_const])?[0];
484 let sc2 = model.wire_node("sc2", AxisOp::Add(0), &[sc1])?[0];
485 let sc2 = model.wire_node("sc3", AxisOp::Add(0), &[sc2])?[0];
486
487 let cond = model.wire_node("cond", TypedBinOp(comp_gte(), None), &[r2, sc2])?[0];
489
490 let true_branch = model.add_source(
492 "true_b",
493 TDim::datum_type().fact(tvec![TDim::Val(1), TDim::Val(1), out_len.clone()]),
494 )?;
495 let false_branch = model.add_source(
496 "false_b",
497 TDim::datum_type().fact(tvec![TDim::Val(1), TDim::Val(1), out_len.clone()]),
498 )?;
499
500 let iff = model.wire_node("iff", Iff, &[cond, true_branch, false_branch])?[0];
501 model.select_output_outlets(&[iff])?;
502
503 let model = model.into_decluttered()?;
504
505 let iff_count = model.nodes().iter().filter(|n| n.op_as::<Iff>().is_some()).count();
506 assert_eq!(iff_count, 0, "Expected no Iff nodes after declutter, found {iff_count}");
507
508 let concat_count =
509 model.nodes().iter().filter(|n| n.op_as::<TypedConcat>().is_some()).count();
510 assert!(concat_count > 0, "Expected at least one Concat node after declutter");
511
512 Ok(())
513 }
514
515 #[test]
517 fn verify_uniform_tdim_propagation() -> TractResult<()> {
518 use crate::ops::array::Range;
519
520 let mut model = TypedModel::default();
521 model.symbols.add_assertion("T >= 1")?;
522 let t_sym = model.symbols.sym("T");
523 let t_dim = TDim::Sym(t_sym.clone());
524
525 let start = model.wire_node(
526 "start",
527 crate::ops::konst::Const::new(tensor0(TDim::Val(0)).into_arc_tensor())?,
528 &[],
529 )?[0];
530 let step = model.wire_node(
531 "step",
532 crate::ops::konst::Const::new(tensor0(TDim::Val(1)).into_arc_tensor())?,
533 &[],
534 )?[0];
535 let end = model.add_source("T_dyn", TDim::datum_type().scalar_fact())?;
536 let range = model.wire_node("range", Range::new(t_dim.clone()), &[start, end, step])?[0];
537 let range_unsq = model.wire_node("range_unsq", AxisOp::Add(0), &[range])?[0];
538 let t_const = model.wire_node(
539 "T_const",
540 crate::ops::konst::Const::new(tensor0(t_dim.clone()).into_arc_tensor())?,
541 &[],
542 )?[0];
543 let t_unsq = model.wire_node("T_unsq", AxisOp::Add(0), &[t_const])?[0];
544 let t_unsq2 = model.wire_node("T_unsq2", AxisOp::Add(0), &[t_unsq])?[0];
545 let lt = model.wire_node("lt", TypedBinOp(comp_lt(), None), &[range_unsq, t_unsq2])?[0];
546
547 let range_fact = model.outlet_fact(range)?;
548 let range_unsq_fact = model.outlet_fact(range_unsq)?;
549 let t_unsq_fact = model.outlet_fact(t_unsq)?;
550 let lt_fact = model.outlet_fact(lt)?;
551
552 assert!(range_fact.uniform_tdim.is_some(), "range should have uniform_tdim");
553 assert!(range_unsq_fact.uniform_tdim.is_some(), "range_unsq should have uniform_tdim");
554 assert!(t_unsq_fact.uniform_tdim.is_some(), "t_unsq should have uniform_tdim");
555 assert!(lt_fact.uniform_tdim.is_some(), "lt should have uniform_tdim");
556
557 Ok(())
558 }
559
560 #[test]
565 fn iff_fold_broadcasts_narrower_branch() -> TractResult<()> {
566 let mut model = TypedModel::default();
567 let cond = model.wire_node(
568 "cond",
569 crate::ops::konst::Const::new(
570 Tensor::from_shape(&[1, 2, 3], &[false; 6])?.into_arc_tensor(),
571 )?,
572 &[],
573 )?[0];
574 let then = model.add_source("then", f32::fact([1, 2, 3]))?;
575 let otherwise = model.add_source("else", f32::fact([1, 2, 1]))?;
576 let iff = model.wire_node("iff", Iff, &[cond, then, otherwise])?[0];
577 model.select_output_outlets(&[iff])?;
578
579 let model = model.into_decluttered()?;
580
581 let iff_count = model.nodes().iter().filter(|n| n.op_as::<Iff>().is_some()).count();
582 assert_eq!(iff_count, 0, "Expected Iff to be folded");
583 assert_eq!(
584 model.output_fact(0)?.shape.to_tvec(),
585 tvec![1.to_dim(), 2.to_dim(), 3.to_dim()]
586 );
587 Ok(())
588 }
589}