biodivine_lib_bdd/_impl_bdd/
_impl_relation_ops.rs

1use crate::{Bdd, BddNode, BddPartialValuation, BddPointer, BddVariable};
2use fxhash::FxBuildHasher;
3use rand::Rng;
4use std::collections::HashMap;
5
6/// Advanced relation-like operations for `Bdd`s.
7impl Bdd {
8    /// This operation is deprecated in favour of `Bdd::var_exists` which provides the
9    /// same functionality but better naming.
10    #[deprecated]
11    pub fn var_project(&self, variable: BddVariable) -> Bdd {
12        self.var_exists(variable)
13    }
14
15    /// Eliminates one given variable from the `Bdd` using **existential projection**.
16    ///
17    /// If we see the Bdd as a set of bitvectors, this is essentially existential quantification:
18    /// `\exists x_i : (x_1, ..., x_i, ..., x_n) \in BDD`.
19    pub fn var_exists(&self, variable: BddVariable) -> Bdd {
20        Bdd::fused_binary_flip_op(
21            (self, None),
22            (self, Some(variable)),
23            None,
24            crate::op_function::or,
25        )
26    }
27
28    /// Eliminates one given variable from the `Bdd` using **universal projection**.
29    ///
30    /// If we see the Bdd as a set of bitvectors, this is essentially universal quantification:
31    /// `\forall x_i : (x_1, ..., x_i, ..., x_n) \in BDD`.
32    pub fn var_for_all(&self, variable: BddVariable) -> Bdd {
33        Bdd::fused_binary_flip_op(
34            (self, None),
35            (self, Some(variable)),
36            None,
37            crate::op_function::and,
38        )
39    }
40
41    /// This method is deprecated in favour of `Bdd::exists` which provides the same functionality
42    /// but better naming.
43    #[deprecated]
44    pub fn project(&self, variables: &[BddVariable]) -> Bdd {
45        self.exists(variables)
46    }
47
48    /// Eliminate all given `variables` from the `Bdd` using existential projection.
49    ///
50    /// This can be used to implement operations like `domain` and `range` for
51    /// a specific relation.
52    ///
53    /// Note that this method should be faster than repeated calls to `var_exists` once
54    /// the size of `variables` is non-trivial, but it has a higher overhead. So for very small
55    /// instances the performance advantage may not be very high.
56    pub fn exists(&self, variables: &[BddVariable]) -> Bdd {
57        // x & x is simply identity
58        Bdd::binary_op_with_exists(self, self, crate::op_function::and, variables)
59    }
60
61    /// Eliminate all variables that `trigger(var) = true` from the `Bdd` using existential projection.
62    ///
63    /// This can be used to implement operations like `domain` and `range` for
64    /// a specific relation.
65    ///
66    /// Note that this method should be faster than repeated calls to `var_exists` once
67    /// the size of `variables` is non-trivial, but it has a higher overhead. So for very small
68    /// instances the performance advantage may not be very high.
69    pub fn exists_trigger<Trigger: Fn(BddVariable) -> bool>(&self, trigger: Trigger) -> Bdd {
70        // x & x is simply identity
71        Bdd::binary_op_with_exists_trigger(self, self, trigger, crate::op_function::and)
72    }
73
74    /// Eliminate all given `variables` from the `Bdd` using universal projection.
75    ///
76    /// Same performance characteristics as `Bdd::exists`.
77    pub fn for_all(&self, variables: &[BddVariable]) -> Bdd {
78        Bdd::binary_op_with_for_all(self, self, crate::op_function::and, variables)
79    }
80
81    /// Eliminate all variables that `trigger(var) = true` from the `Bdd` using universal projection.
82    ///
83    /// Same performance characteristics as `Bdd::exists_trigger`.
84    pub fn for_all_trigger<Trigger: Fn(BddVariable) -> bool>(&self, trigger: Trigger) -> Bdd {
85        Bdd::binary_op_with_for_all_trigger(self, self, trigger, crate::op_function::and)
86    }
87
88    /// Picks one valuation for the given `BddVariable`.
89    ///
90    /// Essentially, what this means is that
91    /// `(x_1, ..., x_i, ..., x_n) \in B <=> (x_1, ..., !x_i, ..., x_n) \not\in B`.
92    /// That is, each valuation (without `x_i`) can be present only with either `x_i = 1` or
93    /// `x_i = 0`, but not both.
94    ///
95    /// WARNING! `var_pick` is a bit troublesome in terms of composition: `B.var_pick(x).var_pick(y)`
96    /// is probably not what you think. So make sure to prove and test thoroughly.
97    pub fn var_pick(&self, variable: BddVariable) -> Bdd {
98        // original \ flip(original & !x_i)
99        Bdd::fused_binary_flip_op(
100            (self, None),
101            (&self.var_select(variable, false), Some(variable)),
102            None,
103            crate::op_function::and_not,
104        )
105    }
106
107    /// Same as `bdd.var_pick`, but the *preferred* value is picked randomly using
108    /// the provided generator, instead of defaulting to `false`.
109    ///
110    /// Note that this is not the same as having a random value picked on each path in the `Bdd`.
111    /// Instead, there is one "global" value that is preferred on every path.
112    pub fn var_pick_random<R: Rng>(&self, variable: BddVariable, rng: &mut R) -> Bdd {
113        let preferred = self.var_select(variable, rng.gen_bool(0.5));
114        Bdd::fused_binary_flip_op(
115            (self, None),
116            (&preferred, Some(variable)),
117            None,
118            crate::op_function::and_not,
119        )
120    }
121
122    /// Picks one "witness" valuation for the given variables. This is a generalized variant
123    /// of `var_pick`.
124    ///
125    /// After this operation, if we view `Bdd` as a set of bitvectors, every partial valuation in
126    /// the original `Bdd`, disregarding the given `variables`, has exactly one witness valuation
127    /// in the result, which was also in the original `Bdd`.
128    ///
129    /// This can be used to implement non-trivial element picking on relations (for example,
130    /// for `A x B`, picking one `b \in B` for every `a \in A`).
131    pub fn pick(&self, variables: &[BddVariable]) -> Bdd {
132        fn r_pick(set: &Bdd, variables: &[BddVariable]) -> Bdd {
133            if let Some((last_var, rest)) = variables.split_last() {
134                let picked = r_pick(&set.var_exists(*last_var), rest);
135                picked.and(&set.var_pick(*last_var))
136            } else {
137                set.clone()
138            }
139        }
140
141        r_pick(self, &sorted(variables))
142    }
143
144    /// Same as `bdd.pick`, but the preferred value for each variable is picked randomly using
145    /// the provided generator.
146    pub fn pick_random<R: Rng>(&self, variables: &[BddVariable], rng: &mut R) -> Bdd {
147        fn r_pick<R: Rng>(set: &Bdd, variables: &[BddVariable], rng: &mut R) -> Bdd {
148            if let Some((last_var, rest)) = variables.split_last() {
149                let picked = r_pick(&set.var_exists(*last_var), rest, rng);
150                picked.and(&set.var_pick_random(*last_var, rng))
151            } else {
152                set.clone()
153            }
154        }
155
156        r_pick(self, &sorted(variables), rng)
157    }
158
159    /// Fix the value of a specific `BddVariable` to the given `value`. This is just a shorthand
160    /// for `B & (x <=> value)`.
161    pub fn var_select(&self, variable: BddVariable, value: bool) -> Bdd {
162        self.and(&Bdd::mk_literal(self.num_vars(), variable, value))
163    }
164
165    /// Generalized operation to `var_select`, allows effectively fixing multiple variables to
166    /// the given values. Similar to `BddValuation.into::<Bdd>()`, but here you don't have to
167    /// specify all variables.
168    pub fn select(&self, variables: &[(BddVariable, bool)]) -> Bdd {
169        let valuation = BddPartialValuation::from_values(variables);
170        self.select_valuation(&valuation)
171    }
172
173    /// Generalized operation to `var_select`, allows effectively fixing multiple variables to
174    /// the given values. Similar to `BddValuation.into::<Bdd>()`, but here you don't have to
175    /// specify all variables.
176    ///
177    /// `BddPartialValuation` version of `Bdd::select`
178    pub fn select_valuation(&self, valuation: &BddPartialValuation) -> Bdd {
179        let valuation_bdd = Bdd::mk_partial_valuation(self.num_vars(), valuation);
180        self.and(&valuation_bdd)
181    }
182
183    /// Fixes a `variable` to the given `value`, and then eliminates said variable using
184    /// existential projection.
185    ///
186    /// A valuation `v` satisfies the resulting formula `B_2` if and only if `v[variable = value]`
187    /// satisfied the original formula `B_1`.
188    pub fn var_restrict(&self, variable: BddVariable, value: bool) -> Bdd {
189        self.restrict(&[(variable, value)])
190    }
191
192    /// Generalized operation to `var_restrict`. Allows fixing multiple Bdd variables and
193    /// eliminating them at the same time.
194    pub fn restrict(&self, variables: &[(BddVariable, bool)]) -> Bdd {
195        let valuation = BddPartialValuation::from_values(variables);
196        self.restrict_valuation(&valuation)
197    }
198    /// Generalized operation to `var_restrict`. Allows fixing multiple Bdd variables and
199    /// eliminating them at the same time.
200    ///
201    /// `BddPartialValuation` version of `Bdd::restrict`
202    pub fn restrict_valuation(&self, valuation: &BddPartialValuation) -> Bdd {
203        restriction(self, valuation)
204    }
205}
206
207/// **(internal)** Helper function for sorting variable list arguments.
208fn sorted(variables: &[BddVariable]) -> Vec<BddVariable> {
209    let mut variables: Vec<BddVariable> = variables.to_vec();
210    variables.sort();
211    variables
212}
213
214// An internal function to perform a fast restriction operation. This relies on the fact
215// that subtrees of the eliminated variables do not have to be merged (like in a normal
216// existential projection), but a single subtree is kept instead.
217fn restriction(bdd: &Bdd, values: &BddPartialValuation) -> Bdd {
218    if bdd.is_true() || bdd.is_false() {
219        return bdd.clone();
220    }
221
222    let mut new_id: Vec<Option<BddPointer>> = vec![None; bdd.size()];
223    new_id[0] = Some(BddPointer::zero());
224    new_id[1] = Some(BddPointer::one());
225
226    let mut output = Bdd::mk_true(bdd.num_vars());
227
228    let mut node_cache: HashMap<BddNode, BddPointer, FxBuildHasher> =
229        HashMap::with_capacity_and_hasher(bdd.size(), FxBuildHasher::default());
230    node_cache.insert(BddNode::mk_zero(bdd.num_vars()), BddPointer::zero());
231    node_cache.insert(BddNode::mk_one(bdd.num_vars()), BddPointer::one());
232
233    let mut stack = vec![bdd.root_pointer()];
234    while let Some(top) = stack.pop() {
235        if new_id[top.to_index()].is_some() {
236            // Skip if already computed.
237            continue;
238        }
239
240        let top_var = bdd.var_of(top);
241        if let Some(value) = values[top_var] {
242            // The variable is restricted, hence only the relevant child is considered.
243            let link = if value {
244                bdd.high_link_of(top)
245            } else {
246                bdd.low_link_of(top)
247            };
248
249            let Some(new_link) = new_id[link.to_index()] else {
250                stack.push(top);
251                stack.push(link);
252                continue;
253            };
254
255            // Here, we do not have to create a node, because the variable is erased immediately.
256            new_id[top.to_index()] = Some(new_link);
257        } else {
258            // This variable is unrestricted. We just compute results for both children
259            // if they are unknown. Otherwise, we copy the node into the new BDD.
260            let low_link = bdd.low_link_of(top);
261            let high_link = bdd.high_link_of(top);
262
263            let Some(new_low_link) = new_id[low_link.to_index()] else {
264                stack.push(top);
265                stack.push(low_link);
266                continue;
267            };
268
269            let Some(new_high_link) = new_id[high_link.to_index()] else {
270                stack.push(top);
271                stack.push(high_link);
272                continue;
273            };
274
275            if new_high_link == new_low_link {
276                // If a redundant node is created, it is ignored.
277                new_id[top.to_index()] = Some(new_high_link);
278                continue;
279            }
280
281            let new_node = BddNode::mk_node(top_var, new_low_link, new_high_link);
282
283            if let Some(&node) = node_cache.get(&new_node) {
284                new_id[top.to_index()] = Some(node);
285            } else {
286                let node = BddPointer::from_index(output.size());
287                output.push_node(new_node);
288                node_cache.insert(new_node, node);
289                new_id[top.to_index()] = Some(node);
290            }
291        }
292    }
293
294    let new_root = new_id[bdd.root_pointer().to_index()].unwrap();
295    if new_root.is_zero() {
296        // This could happen if we do, for example, restrict(var, True) in a function `!var`.
297        return Bdd::mk_false(bdd.num_vars());
298    }
299
300    output
301}