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}