boolean_expression/
cubes.rs

1// boolean_expression: a Rust crate for Boolean expressions and BDDs.
2//
3// Copyright (c) 2016 Chris Fallin <cfallin@c1f.net>. Released under the MIT
4// License.
5//
6
7use smallvec::SmallVec;
8use std::cmp::{Eq, Ord, Ordering, PartialEq, PartialOrd};
9use std::collections::VecDeque;
10use std::iter;
11use std::slice;
12
13/// A variable assignment in a cube.
14#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
15pub enum CubeVar {
16    /// This variable must be false.
17    False,
18    /// This variable must be true.
19    True,
20    /// This variable may be true or false.
21    DontCare,
22}
23
24const CUBE_ALLOCED_SIZE: usize = 16;
25
26/// A `Cube` is one (multidimensional) cube in the variable space: i.e., a
27/// particular set of variable assignments, where each variable is assigned
28/// either true, false, or "don't care".
29#[derive(Clone, Debug)]
30pub struct Cube(SmallVec<[CubeVar; CUBE_ALLOCED_SIZE]>);
31
32/// The result of attempting to merge two cubes.
33#[derive(Clone, Debug, PartialEq, Eq)]
34pub enum CubeMergeResult {
35    /// The cubes could not be merged.
36    None,
37    /// The left cube was canceled because it is completely covered by the right cube.
38    CancelLeft,
39    /// The right cube was canceled because it is completely covered by the left cube.
40    CancelRight,
41    /// The two cubes merge into one.
42    Merge(Cube),
43    /// The left cube may be expanded (increase its number of `DontCare`s) by
44    /// overlapping with the right cube.
45    ExpandLeft(Cube),
46    /// The right cube may be expanded (increase its number of `DontCare`s) by
47    /// overlapping with the left cube.
48    ExpandRight(Cube),
49}
50
51impl Cube {
52    /// Construct an always-true cube (all variables are `DontCare`) for `vars`
53    /// variables.
54    pub fn true_cube(vars: usize) -> Cube {
55        Cube(iter::repeat(CubeVar::DontCare).take(vars).collect())
56    }
57
58    /// Return an iterator over variable assignments.
59    pub fn vars(&self) -> slice::Iter<CubeVar> {
60        self.0.iter()
61    }
62
63    /// Attempt to merge this cube with another, which may cancel one or the
64    /// other (if completely covered), expand one or the other (if possible, to
65    /// increase the number of `DontCare`s and thus simplify the final
66    /// expression), or merge the two into a single cube, or do nothing.
67    pub fn merge_with(&self, other: &Cube) -> CubeMergeResult {
68        // Cubes that differ in exactly one position can merge.
69        // A cube that matches another except has fixed values where the other
70        // has don't-cares can be eliminated.
71        if self.0.len() != other.0.len() {
72            CubeMergeResult::None
73        } else if self == other {
74            CubeMergeResult::CancelRight
75        } else {
76            let mut mismatches = 0;
77            let mut mismatch_pos = 0;
78            let mut left_covered = 0;
79            let mut right_covered = 0;
80            for (i, (lvar, rvar)) in self.0.iter().zip(other.0.iter()).enumerate() {
81                match (lvar, rvar) {
82                    (&CubeVar::False, &CubeVar::True) | (&CubeVar::True, &CubeVar::False) => {
83                        mismatches += 1;
84                        mismatch_pos = i;
85                    }
86                    (&CubeVar::False, &CubeVar::DontCare)
87                    | (&CubeVar::True, &CubeVar::DontCare) => {
88                        left_covered += 1;
89                    }
90                    (&CubeVar::DontCare, &CubeVar::False)
91                    | (&CubeVar::DontCare, &CubeVar::True) => {
92                        right_covered += 1;
93                    }
94                    _ => {}
95                }
96            }
97            if mismatches == 0 && left_covered > 0 && right_covered == 0 {
98                CubeMergeResult::CancelLeft
99            } else if mismatches == 0 && right_covered > 0 && left_covered == 0 {
100                CubeMergeResult::CancelRight
101            } else if mismatches == 1 && right_covered == 0 && left_covered == 0 {
102                CubeMergeResult::Merge(self.with_var(mismatch_pos, CubeVar::DontCare))
103            } else if mismatches == 1 && right_covered > 0 && left_covered == 0 {
104                CubeMergeResult::ExpandRight(other.with_var(mismatch_pos, CubeVar::DontCare))
105            } else if mismatches == 1 && right_covered == 0 && left_covered > 0 {
106                CubeMergeResult::ExpandLeft(self.with_var(mismatch_pos, CubeVar::DontCare))
107            } else {
108                CubeMergeResult::None
109            }
110        }
111    }
112
113    /// Return a new cube equal to this cube, but with the particular variable
114    // assigned the given value.
115    pub fn with_var(&self, idx: usize, val: CubeVar) -> Cube {
116        Cube(
117            self.0
118                .iter()
119                .enumerate()
120                .map(|(i, var)| if i == idx { val.clone() } else { var.clone() })
121                .collect(),
122        )
123    }
124}
125
126impl PartialEq for Cube {
127    fn eq(&self, other: &Self) -> bool {
128        self.cmp(other) == Ordering::Equal
129    }
130}
131impl Eq for Cube {}
132impl PartialOrd for Cube {
133    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
134        Some(self.cmp(other))
135    }
136}
137impl Ord for Cube {
138    fn cmp(&self, other: &Self) -> Ordering {
139        self.0.iter().cmp(other.0.iter())
140    }
141}
142
143const CUBELIST_ALLOCED_SIZE: usize = 4;
144
145/// A `CubeList` is a sum (OR'd list) of cubes. It represents a Boolean
146/// expression in sum-of-products form, by construction.
147///
148/// The `CubeList` abstraction supports only indexed anonymous variables
149/// (variable 0, 1, ...), and does not (yet) have a wrapper supporting an
150/// arbitrary terminal type `T`. This may be implemented in the future.
151///
152/// The `CubeList` abstraction is used internally to convert from a `BDD`
153/// to a quasi-minimized Boolean expression.
154#[derive(Clone, Debug)]
155pub struct CubeList(SmallVec<[Cube; CUBE_ALLOCED_SIZE]>);
156
157impl PartialEq for CubeList {
158    fn eq(&self, other: &Self) -> bool {
159        self.0.iter().cmp(other.0.iter()) == Ordering::Equal
160    }
161}
162
163impl CubeList {
164    /// Construct a new, empty, cube list (equivalent to a constant `false`).
165    pub fn new() -> CubeList {
166        CubeList(SmallVec::new())
167    }
168
169    /// Construct a cube list from a list of `Cube` structs.
170    pub fn from_list(cubes: &[Cube]) -> CubeList {
171        CubeList(cubes.iter().map(|c| c.clone()).collect())
172    }
173
174    /// Return an iterator over all cubes.
175    pub fn cubes(&self) -> slice::Iter<Cube> {
176        self.0.iter()
177    }
178
179    /// Merge this cube list with another, canceling or merging cubes where
180    /// possible. The resulting cube list is not guaranteed to be minimal (that
181    /// is the set-cover problem, which is NP-Complete), but is reduced somewhat
182    /// by a very simple reduction/merging algorithm.
183    pub fn merge(&self, other: &CubeList) -> CubeList {
184        let mut out: SmallVec<[Cube; CUBE_ALLOCED_SIZE]> = SmallVec::new();
185        let mut canceled: SmallVec<[bool; CUBE_ALLOCED_SIZE]> = SmallVec::new();
186        for cube in self.0.iter().chain(other.0.iter()) {
187            out.push(cube.clone());
188            canceled.push(false);
189        }
190
191        let mut worklist = VecDeque::new();
192        for i in 0..out.len() {
193            worklist.push_back(i);
194        }
195        while let Some(i) = worklist.pop_front() {
196            if canceled[i] {
197                continue;
198            }
199            for j in 0..out.len() {
200                if i == j {
201                    continue;
202                }
203                if canceled[i] {
204                    break;
205                }
206                if canceled[j] {
207                    continue;
208                }
209                match out[i].merge_with(&out[j]) {
210                    CubeMergeResult::None => {}
211                    CubeMergeResult::CancelLeft => {
212                        canceled[i] = true;
213                    }
214                    CubeMergeResult::CancelRight => {
215                        canceled[j] = true;
216                    }
217                    CubeMergeResult::Merge(n) => {
218                        out[i] = n;
219                        worklist.push_back(i);
220                        canceled[j] = true;
221                    }
222                    CubeMergeResult::ExpandLeft(n) => {
223                        out[i] = n;
224                        worklist.push_back(i);
225                    }
226                    CubeMergeResult::ExpandRight(n) => {
227                        out[j] = n;
228                        worklist.push_back(j);
229                    }
230                }
231            }
232        }
233
234        let out = out
235            .into_iter()
236            .zip(canceled.iter())
237            .filter(|&(_, flag)| !flag)
238            .map(|(cube, _)| cube)
239            .collect();
240        CubeList(out)
241    }
242
243    pub fn with_var(&self, idx: usize, val: CubeVar) -> CubeList {
244        CubeList(
245            self.0
246                .iter()
247                .map(|c| c.with_var(idx, val.clone()))
248                .collect(),
249        )
250    }
251}
252
253mod test {
254    use super::*;
255
256    fn make_cube(elems: &[u32]) -> Cube {
257        Cube(
258            elems
259                .iter()
260                .map(|i| match *i {
261                    0 => CubeVar::False,
262                    1 => CubeVar::True,
263                    _ => CubeVar::DontCare,
264                })
265                .collect(),
266        )
267    }
268
269    #[test]
270    fn cube_eq() {
271        assert!(make_cube(&[1, 0, 0]) == make_cube(&[1, 0, 0]));
272        assert!(make_cube(&[1, 0, 0]) != make_cube(&[1, 0, 1]));
273    }
274
275    #[test]
276    fn cube_ord() {
277        assert!(make_cube(&[1, 0, 0]) < make_cube(&[1, 1, 0]));
278        assert!(make_cube(&[1, 0, 2]) > make_cube(&[1, 0, 1]));
279    }
280
281    #[test]
282    fn cube_with_var() {
283        assert!(make_cube(&[0, 1, 0]).with_var(2, CubeVar::True) == make_cube(&[0, 1, 1]));
284    }
285
286    #[test]
287    fn cube_merge() {
288        // Cubes of mismatching dimension: no cancelation.
289        assert!(make_cube(&[0, 0]).merge_with(&make_cube(&[0])) == CubeMergeResult::None);
290        // Identical cubes: cancelation (our implementation: cancel right).
291        assert!(make_cube(&[0, 0]).merge_with(&make_cube(&[0, 0])) == CubeMergeResult::CancelRight);
292        // Irredundant cubes: no cancelation.
293        assert!(make_cube(&[1, 0]).merge_with(&make_cube(&[0, 1])) == CubeMergeResult::None);
294        // Irredundant cubes with some overlap: no cancelation.
295        assert!(make_cube(&[1, 2]).merge_with(&make_cube(&[2, 1])) == CubeMergeResult::None);
296        // Left cube covers right cube: cancel right.
297        assert!(
298            make_cube(&[1, 2, 2]).merge_with(&make_cube(&[1, 1, 0]))
299                == CubeMergeResult::CancelRight
300        );
301        // Right cube may be expanded to overlap with left cube.
302        assert!(
303            make_cube(&[1, 1, 2]).merge_with(&make_cube(&[1, 0, 0]))
304                == CubeMergeResult::ExpandRight(make_cube(&[1, 2, 0]))
305        );
306        // The above, with right and left flipped.
307        assert!(
308            make_cube(&[1, 1, 0]).merge_with(&make_cube(&[1, 2, 2])) == CubeMergeResult::CancelLeft
309        );
310        assert!(
311            make_cube(&[1, 0, 0]).merge_with(&make_cube(&[1, 1, 2]))
312                == CubeMergeResult::ExpandLeft(make_cube(&[1, 2, 0]))
313        );
314        // Cubes with one mismatch: merge.
315        assert!(
316            make_cube(&[1, 1, 0]).merge_with(&make_cube(&[1, 1, 1]))
317                == CubeMergeResult::Merge(make_cube(&[1, 1, 2]))
318        );
319        // Cubes with more than one mismatch: no merge.
320        assert!(make_cube(&[1, 1, 0]).merge_with(&make_cube(&[1, 0, 1])) == CubeMergeResult::None);
321    }
322
323    #[test]
324    fn cubelist_merge() {
325        // No merges.
326        assert!(
327            CubeList::new().merge(&CubeList::from_list(&[
328                make_cube(&[1, 0, 0]),
329                make_cube(&[0, 1, 1])
330            ])) == CubeList::from_list(&[make_cube(&[1, 0, 0]), make_cube(&[0, 1, 1])])
331        );
332        // Multiple-stage merge.
333        assert!(
334            CubeList::new().merge(&CubeList::from_list(&[
335                make_cube(&[1, 0, 0]),
336                make_cube(&[1, 1, 1]),
337                make_cube(&[1, 0, 1]),
338                make_cube(&[1, 1, 0])
339            ])) == CubeList::from_list(&[make_cube(&[1, 2, 2])])
340        );
341        // Last term merges with first.
342        assert!(
343            CubeList::new().merge(&CubeList::from_list(&[
344                make_cube(&[1, 0, 0]),
345                make_cube(&[0, 1, 1]),
346                make_cube(&[1, 0, 1])
347            ])) == CubeList::from_list(&[make_cube(&[1, 0, 2]), make_cube(&[0, 1, 1])])
348        );
349        // New item cancels existing in list.
350        assert!(
351            CubeList::new().merge(&CubeList::from_list(&[
352                make_cube(&[1, 0, 0]),
353                make_cube(&[1, 2, 2])
354            ])) == CubeList::from_list(&[make_cube(&[1, 2, 2])])
355        );
356        // Existing list item cancels new item.
357        assert!(
358            CubeList::new().merge(&CubeList::from_list(&[
359                make_cube(&[1, 2, 2]),
360                make_cube(&[1, 0, 0])
361            ])) == CubeList::from_list(&[make_cube(&[1, 2, 2])])
362        );
363    }
364}