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}