espresso_logic/cover/
minimisation.rs

1//! Minimizable trait and implementations for Boolean function minimization
2//!
3//! This module provides the [`Minimizable`] trait which defines a uniform interface
4//! for minimizing Boolean functions using the Espresso algorithm, along with implementations
5//! for [`Cover`] and a blanket implementation for types convertible to/from [`Dnf`].
6
7use super::cubes::CubeType;
8use super::dnf::Dnf;
9use super::Cover;
10use crate::espresso::error::MinimizationError;
11use crate::EspressoConfig;
12use std::collections::BTreeMap;
13use std::sync::Arc;
14
15/// Public trait for types that can be minimized using Espresso
16///
17/// This trait provides a **transparent, uniform interface** for minimizing boolean functions
18/// using the Espresso algorithm. All methods take `&self` and return a new minimized instance,
19/// following an immutable functional style.
20///
21/// **Note (v3.1+):** You must explicitly import this trait to use its methods:
22/// ```
23/// use espresso_logic::{BoolExpr, Minimizable};
24///
25/// let expr = BoolExpr::parse("a * b + a * b * c")?;
26/// let minimized = expr.minimize()?;  // Requires `use Minimizable`
27/// # Ok::<(), std::io::Error>(())
28/// ```
29///
30/// # Transparent Minimization
31///
32/// The beauty of this trait is that minimization works the same way regardless of input type.
33/// Just call `.minimize()` on any supported type and get back a minimized version of the same type:
34///
35/// ```
36/// use espresso_logic::{BoolExpr, Cover, CoverType, Minimizable};
37///
38/// # fn main() -> std::io::Result<()> {
39/// let a = BoolExpr::variable("a");
40/// let b = BoolExpr::variable("b");
41/// let c = BoolExpr::variable("c");
42/// let redundant = a.and(&b).or(&a.and(&b).and(&c));
43///
44/// // Works on BoolExpr - returns BoolExpr
45/// let min_expr = redundant.minimize()?;
46/// println!("Minimized expression: {}", min_expr);
47///
48/// // Works on Cover - returns Cover
49/// let mut cover = Cover::new(CoverType::F);
50/// cover.add_expr(&redundant, "out")?;
51/// let min_cover = cover.minimize()?;
52/// println!("Minimized cover has {} cubes", min_cover.num_cubes());
53///
54/// // Both produce equivalent minimized results!
55/// # Ok(())
56/// # }
57/// ```
58///
59/// # Implementations
60///
61/// - **[`Cover`]**: Direct implementation - minimizes cubes directly with Espresso
62/// - **Blanket implementation** (v3.1+): For `T where &T: Into<Dnf>, T: From<Dnf>`
63///   - Automatically covers [`BoolExpr`] (v3.1.1+)
64///   - Workflow: Expression → Dnf (from internal BDD representation) → Cover cubes → Espresso → minimized Cover → Dnf → Expression
65///   - DNF is extracted from the internal BDD representation, minimized, then used to create a new expression
66///
67/// [`BoolExpr`]: crate::expression::BoolExpr
68/// [`Cover`]: crate::Cover
69///
70/// # Immutable Design
71///
72/// All minimization methods preserve the original and return a new minimized instance:
73///
74/// ```
75/// use espresso_logic::{BoolExpr, Minimizable};
76///
77/// # fn main() -> std::io::Result<()> {
78/// let a = BoolExpr::variable("a");
79/// let b = BoolExpr::variable("b");
80/// let c = BoolExpr::variable("c");
81///
82/// let original = a.and(&b).or(&a.and(&b).and(&c));
83/// let minimized = original.minimize()?;
84///
85/// // Original is unchanged
86/// println!("Original: {}", original);
87/// println!("Minimized: {}", minimized);
88///
89/// // Can continue using original (already a BDD internally)
90/// println!("Original has {} BDD nodes", original.node_count());
91/// # Ok(())
92/// # }
93/// ```
94pub trait Minimizable {
95    /// Minimize using the heuristic Espresso algorithm
96    ///
97    /// Returns a new minimized instance without modifying the original.
98    /// This is fast and produces near-optimal results (~99% optimal in practice).
99    ///
100    /// Default implementation calls `minimize_with_config` with default config.
101    fn minimize(&self) -> Result<Self, MinimizationError>
102    where
103        Self: Sized,
104    {
105        let config = EspressoConfig::default();
106        self.minimize_with_config(&config)
107    }
108
109    /// Minimize using the heuristic algorithm with custom configuration
110    ///
111    /// Returns a new minimized instance without modifying the original.
112    ///
113    /// This is the primary method that implementations must provide.
114    fn minimize_with_config(&self, config: &EspressoConfig) -> Result<Self, MinimizationError>
115    where
116        Self: Sized;
117
118    /// Minimize using exact minimization
119    ///
120    /// Returns a new minimized instance without modifying the original.
121    /// This guarantees minimal results but may be slower for large expressions.
122    ///
123    /// Default implementation calls `minimize_exact_with_config` with default config.
124    fn minimize_exact(&self) -> Result<Self, MinimizationError>
125    where
126        Self: Sized,
127    {
128        let config = EspressoConfig::default();
129        self.minimize_exact_with_config(&config)
130    }
131
132    /// Minimize using exact minimization with custom configuration
133    ///
134    /// Returns a new minimized instance without modifying the original.
135    ///
136    /// This is the primary method that implementations must provide.
137    fn minimize_exact_with_config(
138        &self,
139        config: &EspressoConfig,
140    ) -> Result<Self, MinimizationError>
141    where
142        Self: Sized;
143}
144
145/// Private helper function to minimize a Cover using either heuristic or exact algorithm
146fn minimize_cover_with<F>(
147    cover: &Cover,
148    config: &EspressoConfig,
149    minimize_fn: F,
150) -> Result<Cover, MinimizationError>
151where
152    F: FnOnce(
153        &crate::espresso::Espresso,
154        &crate::espresso::EspressoCover,
155        Option<&crate::espresso::EspressoCover>,
156        Option<&crate::espresso::EspressoCover>,
157    ) -> (
158        crate::espresso::EspressoCover,
159        crate::espresso::EspressoCover,
160        crate::espresso::EspressoCover,
161    ),
162{
163    use crate::espresso::{Espresso, EspressoCover};
164
165    // Split cubes into F, D, R sets based on cube type
166    let mut f_cubes = Vec::new();
167    let mut d_cubes = Vec::new();
168    let mut r_cubes = Vec::new();
169
170    for cube in cover.cubes.iter() {
171        let input_vec: Vec<u8> = cube
172            .inputs()
173            .iter()
174            .map(|&opt| match opt {
175                Some(false) => 0,
176                Some(true) => 1,
177                None => 2,
178            })
179            .collect();
180
181        // Convert outputs: true → 1, false → 0
182        let output_vec: Vec<u8> = cube
183            .outputs()
184            .iter()
185            .map(|&b| if b { 1 } else { 0 })
186            .collect();
187
188        // Send to appropriate set based on cube type
189        match cube.cube_type() {
190            CubeType::F => f_cubes.push((input_vec, output_vec)),
191            CubeType::D => d_cubes.push((input_vec, output_vec)),
192            CubeType::R => r_cubes.push((input_vec, output_vec)),
193        }
194    }
195
196    // Direct C calls - thread-safe via thread-local storage
197    let esp = Espresso::new(cover.num_inputs(), cover.num_outputs(), config);
198
199    // Build covers from cube data - convert Vec to slices
200    let f_cubes_refs: Vec<(&[u8], &[u8])> = f_cubes
201        .iter()
202        .map(|(i, o)| (i.as_slice(), o.as_slice()))
203        .collect();
204    let f_cover =
205        EspressoCover::from_cubes(&f_cubes_refs, cover.num_inputs(), cover.num_outputs())?;
206
207    let d_cover = if !d_cubes.is_empty() {
208        let d_cubes_refs: Vec<(&[u8], &[u8])> = d_cubes
209            .iter()
210            .map(|(i, o)| (i.as_slice(), o.as_slice()))
211            .collect();
212        Some(EspressoCover::from_cubes(
213            &d_cubes_refs,
214            cover.num_inputs(),
215            cover.num_outputs(),
216        )?)
217    } else {
218        None
219    };
220    let r_cover = if !r_cubes.is_empty() {
221        let r_cubes_refs: Vec<(&[u8], &[u8])> = r_cubes
222            .iter()
223            .map(|(i, o)| (i.as_slice(), o.as_slice()))
224            .collect();
225        Some(EspressoCover::from_cubes(
226            &r_cubes_refs,
227            cover.num_inputs(),
228            cover.num_outputs(),
229        )?)
230    } else {
231        None
232    };
233
234    // Call the provided minimize function (heuristic or exact)
235    let (f_result, d_result, r_result) =
236        minimize_fn(&esp, &f_cover, d_cover.as_ref(), r_cover.as_ref());
237
238    // Extract minimized cubes
239    let mut minimized_cubes = Vec::new();
240    minimized_cubes.extend(f_result.to_cubes(cover.num_inputs(), cover.num_outputs(), CubeType::F));
241    minimized_cubes.extend(d_result.to_cubes(cover.num_inputs(), cover.num_outputs(), CubeType::D));
242    minimized_cubes.extend(r_result.to_cubes(cover.num_inputs(), cover.num_outputs(), CubeType::R));
243
244    // Build new cover with minimized cubes - only clone labels (Arc, cheap)
245    Ok(Cover {
246        num_inputs: cover.num_inputs,
247        num_outputs: cover.num_outputs,
248        input_labels: cover.input_labels.clone(),
249        output_labels: cover.output_labels.clone(),
250        cubes: minimized_cubes,
251        cover_type: cover.cover_type,
252    })
253}
254
255// Implement public Minimizable trait for Cover
256impl Minimizable for Cover {
257    fn minimize_with_config(&self, config: &EspressoConfig) -> Result<Self, MinimizationError> {
258        minimize_cover_with(self, config, |esp, f, d, r| esp.minimize(f, d, r))
259    }
260
261    fn minimize_exact_with_config(
262        &self,
263        config: &EspressoConfig,
264    ) -> Result<Self, MinimizationError> {
265        minimize_cover_with(self, config, |esp, f, d, r| esp.minimize_exact(f, d, r))
266    }
267}
268
269/// Helper function to minimize via Cover conversion
270///
271/// Used by Minimizable implementations to convert DNF → Cover → minimize → DNF
272pub(crate) fn minimize_via_cover<F>(
273    dnf: &Dnf,
274    config: &EspressoConfig,
275    minimize_fn: F,
276) -> Result<Dnf, MinimizationError>
277where
278    F: FnOnce(&Cover, &EspressoConfig) -> Result<Cover, MinimizationError>,
279{
280    // Use cached variables (already sorted alphabetically)
281    let var_list = dnf.variables();
282    let var_refs: Vec<&str> = var_list.iter().map(|s| s.as_ref()).collect();
283
284    // Create cover with proper dimensions and labels
285    let mut cover = crate::Cover::with_labels(crate::CoverType::F, &var_refs, &["out"]);
286
287    // Add cubes to cover
288    for cube in dnf.cubes() {
289        let mut inputs = vec![None; var_list.len()];
290        for (i, var) in var_list.iter().enumerate() {
291            if let Some(&polarity) = cube.get(var) {
292                inputs[i] = Some(polarity);
293            }
294        }
295        cover.add_cube(&inputs, &[Some(true)]);
296    }
297
298    // Minimize the cover using the provided function
299    let minimized_cover = minimize_fn(&cover, config)?;
300
301    // Convert back to Dnf
302    Ok(cover_to_dnf(&minimized_cover))
303}
304
305/// Helper function to convert a Cover back to Dnf
306pub(crate) fn cover_to_dnf(cover: &Cover) -> Dnf {
307    let mut cubes = Vec::new();
308
309    for cube in cover.cubes() {
310        let mut product = BTreeMap::new();
311
312        // Get input labels
313        let input_labels = cover.input_labels();
314
315        for (i, &literal) in cube.inputs().iter().enumerate() {
316            if let Some(polarity) = literal {
317                let var_name = if i < input_labels.len() {
318                    Arc::clone(&input_labels[i])
319                } else {
320                    Arc::from(format!("x{}", i).as_str())
321                };
322                product.insert(var_name, polarity);
323            }
324        }
325
326        cubes.push(product);
327    }
328
329    Dnf::from_cubes(&cubes)
330}
331
332/// Blanket implementation of Minimizable for types convertible to/from Dnf
333///
334/// This provides automatic minimization support for any type that can convert
335/// to and from [`Dnf`]. The implementation follows this workflow:
336///
337/// 1. Convert `&T` to `Dnf` (via `Into<Dnf>`)
338/// 2. Convert Dnf to Cover with labeled variables
339/// 3. Minimize Cover using Espresso
340/// 4. Convert minimized Cover back to Dnf
341/// 5. Convert Dnf back to original type (via `From<Dnf>`)
342///
343/// The DNF serves as the intermediary representation between boolean expressions
344/// and covers, ensuring all conversions go through the efficient BDD path.
345impl<T> Minimizable for T
346where
347    for<'a> &'a T: Into<Dnf>,
348    T: From<Dnf>,
349{
350    fn minimize_with_config(
351        &self,
352        config: &crate::EspressoConfig,
353    ) -> Result<Self, MinimizationError> {
354        // Convert to Dnf (goes through BDD for canonical representation)
355        let dnf: Dnf = self.into();
356
357        // Minimize via cover using the heuristic algorithm
358        let minimized_dnf =
359            minimize_via_cover(&dnf, config, |cover, cfg| cover.minimize_with_config(cfg))?;
360
361        Ok(T::from(minimized_dnf))
362    }
363
364    fn minimize_exact_with_config(
365        &self,
366        config: &crate::EspressoConfig,
367    ) -> Result<Self, MinimizationError> {
368        // Convert to Dnf (goes through BDD for canonical representation)
369        let dnf: Dnf = self.into();
370
371        // Minimize via cover using the exact algorithm
372        let minimized_dnf = minimize_via_cover(&dnf, config, |cover, cfg| {
373            cover.minimize_exact_with_config(cfg)
374        })?;
375
376        Ok(T::from(minimized_dnf))
377    }
378}