diff --git a/src/cmd.rs b/src/cmd.rs index edc52b1..6aa6ffb 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -154,6 +154,18 @@ pub struct OptArgs { /// Seed for randomized algorithms #[arg(long)] seed: Option, + + /// Balance And/Xor trees to reduce logic depth (delay-oriented) + #[arg(long, default_value_t = false)] + balance: bool, + + /// Lower the network to a 2-input And-Inverter Graph (AIG) + #[arg(long, default_value_t = false)] + aig: bool, + + /// Lower the network to a Majority-Inverter Graph (MIG) + #[arg(long, default_value_t = false)] + mig: bool, } impl OptArgs { @@ -170,6 +182,15 @@ impl OptArgs { optim::infer_dffe(&mut aig); optim::share_logic(&mut aig, 64); } + if self.balance { + aig = optim::balance(&aig); + } + if self.aig { + aig = optim::to_aig(&aig); + } + if self.mig { + aig = optim::to_mig(&aig); + } write_network_file(&self.output, &aig); } } @@ -183,9 +204,12 @@ pub struct ShowArgs { impl ShowArgs { pub fn run(&self) { - use crate::network::stats::stats; + use crate::network::stats::{depth, stats}; + use crate::optim::cuts::count_cuts; let aig = read_network_file(&self.file); - println!("Network stats:\n{}\n\n", stats(&aig)); + println!("Network stats:\n{}", stats(&aig)); + println!(" Combinational depth: {}", depth(&aig)); + println!(" 4-feasible cuts: {}\n\n", count_cuts(&aig, 4)); } } diff --git a/src/io/blif.rs b/src/io/blif.rs index f46d985..83c8bee 100644 --- a/src/io/blif.rs +++ b/src/io/blif.rs @@ -345,10 +345,10 @@ pub fn write_blif(w: &mut W, aig: &Network) { // ABC extension to blif write!(w, ".flop D={} Q=x{} init=0", sig_to_string(d), i).unwrap(); if *en != Signal::one() { - write!(w, " E={}", en).unwrap(); + write!(w, " E={}", sig_to_string(en)).unwrap(); } if *res != Signal::zero() { - write!(w, " R={}", en).unwrap(); + write!(w, " R={}", sig_to_string(res)).unwrap(); } writeln!(w).unwrap(); } else { @@ -495,4 +495,37 @@ mod test { super::write_blif(&mut buf, &aig); String::from_utf8(buf.into_inner().unwrap()).unwrap(); } + + /// A Dff with both enable and reset must emit distinct E= and R= signals. + /// Regression: the writer used to emit the enable signal for R= as well. + #[test] + fn test_write_flop_enable_reset() { + use std::io::BufWriter; + + use crate::Network; + + let mut aig = Network::new(); + let d = aig.add_input(); + let en = aig.add_input(); + let res = aig.add_input(); + let q = aig.dff(d, en, res); + aig.add_output(q); + + let mut buf = BufWriter::new(Vec::new()); + super::write_blif(&mut buf, &aig); + let s = String::from_utf8(buf.into_inner().unwrap()).unwrap(); + + let flop_line = s + .lines() + .find(|l| l.contains(".flop")) + .expect("expected a .flop line for a Dff with enable and reset"); + assert!( + flop_line.contains("E=i1"), + "enable should be i1: {flop_line}" + ); + assert!( + flop_line.contains("R=i2"), + "reset should be i2, not the enable: {flop_line}" + ); + } } diff --git a/src/network/stats.rs b/src/network/stats.rs index 3e17cc4..00dfe0f 100644 --- a/src/network/stats.rs +++ b/src/network/stats.rs @@ -56,7 +56,13 @@ pub struct NetworkStats { impl NetworkStats { /// Total number of gates, including Dff pub fn nb_gates(&self) -> usize { - self.nb_and + self.nb_xor + self.nb_mux + self.nb_maj + self.nb_buf + self.nb_dff + self.nb_and + + self.nb_xor + + self.nb_lut + + self.nb_mux + + self.nb_maj + + self.nb_buf + + self.nb_dff } /// Record a new and @@ -96,10 +102,10 @@ impl fmt::Display for NetworkStats { if self.nb_dff != 0 { writeln!(f, " Dff: {}", self.nb_dff)?; if self.nb_dffe != 0 { - writeln!(f, " enable: {}", self.nb_dff)?; + writeln!(f, " enable: {}", self.nb_dffe)?; } if self.nb_dffr != 0 { - writeln!(f, " reset: {}", self.nb_dff)?; + writeln!(f, " reset: {}", self.nb_dffr)?; } } if self.nb_and != 0 { @@ -244,3 +250,109 @@ pub fn gate_is_output(aig: &Network) -> Vec { } ret } + +/// Compute the combinational logic level of every node +/// +/// Primary inputs, constants and flip-flop outputs are level 0; each combinatorial +/// gate is one level above its highest input. Buffers and inverters do not add a level. +/// The network must be topologically sorted. +pub fn levels(aig: &Network) -> Vec { + let mut lvl = vec![0u32; aig.nb_nodes()]; + for i in 0..aig.nb_nodes() { + let g = aig.gate(i); + if !g.is_comb() { + // A flip-flop output is a sequential source at level 0 + continue; + } + let mut m = 0; + for v in g.vars() { + m = m.max(lvl[v as usize]); + } + lvl[i] = if g.is_buf_like() { m } else { m + 1 }; + } + lvl +} + +/// Compute the combinational depth of the network: the largest logic level over all outputs +/// +/// The network must be topologically sorted. +pub fn depth(aig: &Network) -> usize { + let lvl = levels(aig); + let mut d = 0; + for i in 0..aig.nb_outputs() { + let s = aig.output(i); + if s.is_var() { + d = d.max(lvl[s.var() as usize]); + } + } + d as usize +} + +#[cfg(test)] +mod tests { + use volute::Lut3; + + use super::{depth, stats}; + use crate::{Gate, Network, Signal}; + + #[test] + fn test_dff_enable_reset_counts() { + let mut aig = Network::new(); + let d = aig.add_input(); + let en = aig.add_input(); + let res = aig.add_input(); + // plain dff, dff with enable, dff with enable and reset + let q0 = aig.dff(d, Signal::one(), Signal::zero()); + let q1 = aig.dff(d, en, Signal::zero()); + let q2 = aig.dff(d, en, res); + aig.add_output(q0); + aig.add_output(q1); + aig.add_output(q2); + + let st = stats(&aig); + assert_eq!(st.nb_dff, 3); + assert_eq!(st.nb_dffe, 2); + assert_eq!(st.nb_dffr, 1); + + // The Display must report the dedicated counts, not nb_dff + let shown = format!("{st}"); + assert!(shown.contains("enable: 2"), "{shown}"); + assert!(shown.contains("reset: 1"), "{shown}"); + } + + #[test] + fn test_lut_counts_as_gate() { + let mut aig = Network::new(); + let i0 = aig.add_input(); + let i1 = aig.add_input(); + let i2 = aig.add_input(); + let o = aig.add(Gate::lut(&[i0, i1, i2], Lut3::nth_var(0).into())); + aig.add_output(o); + + let st = stats(&aig); + assert_eq!(st.nb_lut, 1); + assert_eq!(st.nb_gates(), 1); + } + + #[test] + fn test_depth() { + let mut aig = Network::new(); + // A right-leaning And chain of 8 inputs has depth 7 + let mut sigs = Vec::new(); + for _ in 0..8 { + sigs.push(aig.add_input()); + } + let mut acc = sigs[0]; + for s in &sigs[1..] { + acc = aig.and(acc, *s); + } + aig.add_output(acc); + assert_eq!(depth(&aig), 7); + + // A network whose only output is a primary input has depth 0 + let mut io = Network::new(); + let i = io.add_input(); + io.add_output(i); + assert_eq!(depth(&io), 0); + } +} diff --git a/src/optim.rs b/src/optim.rs index 53e84f1..8495d85 100644 --- a/src/optim.rs +++ b/src/optim.rs @@ -1,7 +1,15 @@ //! Optimization of logic networks +mod aig; +mod balance; +pub mod cuts; mod infer_gates; +mod mig; mod share_logic; +pub use aig::to_aig; +pub use balance::{balance, balance_with}; +pub use cuts::{enumerate_cuts, enumerate_cuts_with, is_valid_cut, Cut}; pub use infer_gates::{infer_dffe, infer_xor_mux}; +pub use mig::to_mig; pub use share_logic::share_logic; diff --git a/src/optim/aig.rs b/src/optim/aig.rs new file mode 100644 index 0000000..67c4217 --- /dev/null +++ b/src/optim/aig.rs @@ -0,0 +1,265 @@ +//! Conversion to an And-Inverter Graph (AIG) +//! +//! Lowers every gate to 2-input And gates with implicit inversions, the classic +//! AIG representation used by tools such as [ABC](https://github.com/berkeley-abc/abc). +//! Xor, Mux, Maj and Lut gates are decomposed into Ands; flip-flops are kept, giving +//! an AIG with sequential elements. This provides a uniform And-based view, useful as +//! a normal form and as a basis for And-graph algorithms and faster simulation. + +use volute::Lut; + +use crate::network::{BinaryType, NaryType, TernaryType}; +use crate::{Gate, Network, Signal}; + +/// Translate a signal of the source network into the rebuilt network +fn translate(s: Signal, trans: &[Signal]) -> Signal { + if s.is_var() { + trans[s.var() as usize] ^ s.is_inverted() + } else { + s + } +} + +/// `a ^ b` expressed with And gates: `!( !(a & !b) & !(!a & b) )` +fn xor2(ret: &mut Network, a: Signal, b: Signal) -> Signal { + let p = ret.and(a, !b); + let q = ret.and(!a, b); + !ret.and(!p, !q) +} + +/// `s ? a : b` expressed with And gates: `!( !(s & a) & !(!s & b) )` +fn mux2(ret: &mut Network, s: Signal, a: Signal, b: Signal) -> Signal { + let p = ret.and(s, a); + let q = ret.and(!s, b); + !ret.and(!p, !q) +} + +/// `Maj(a, b, c)` expressed with And gates: `!( !(a&b) & !(b&c) & !(a&c) )` +fn maj3(ret: &mut Network, a: Signal, b: Signal, c: Signal) -> Signal { + let ab = ret.and(a, b); + let bc = ret.and(b, c); + let ac = ret.and(a, c); + let t = ret.and(!ab, !bc); + !ret.and(t, !ac) +} + +/// And of all signals, as a left-leaning tree of 2-input Ands +fn and_all(ret: &mut Network, sigs: &[Signal]) -> Signal { + let mut acc = Signal::one(); + for s in sigs { + acc = ret.and(acc, *s); + } + acc +} + +/// Xor of all signals, folded with 2-input Xors +fn xor_all(ret: &mut Network, sigs: &[Signal]) -> Signal { + let mut acc = Signal::zero(); + for s in sigs { + acc = xor2(ret, acc, *s); + } + acc +} + +/// Decompose a Lut into Ands through its sum of products (the true minterms) +fn lut_to_and(ret: &mut Network, lut: &Lut, inputs: &[Signal]) -> Signal { + let n = lut.num_vars(); + let mut products = Vec::new(); + for mask in 0..lut.num_bits() { + if lut.value(mask) { + // Product term: a literal per input, complemented where the minterm bit is 0 + let lits: Vec = (0..n).map(|i| inputs[i] ^ ((mask >> i) & 1 == 0)).collect(); + products.push(and_all(ret, &lits)); + } + } + if products.is_empty() { + return Signal::zero(); + } + // Or of the products: `!( And of !products )` + let inv: Vec = products.iter().map(|s| !*s).collect(); + !and_all(ret, &inv) +} + +/// Convert a network to a 2-input And-Inverter Graph +/// +/// All combinatorial logic is expressed with 2-input And gates and implicit inverters. +/// Flip-flops are preserved, so sequential networks stay sequential. The result is +/// functionally equivalent to the input. +/// +/// ``` +/// # use quaigh::{Gate, Network}; +/// use quaigh::optim::to_aig; +/// use quaigh::network::stats::stats; +/// +/// let mut net = Network::new(); +/// let a = net.add_input(); +/// let b = net.add_input(); +/// let c = net.add_input(); +/// let o = net.add(Gate::xor3(a, b, c)); +/// net.add_output(o); +/// +/// let aig = to_aig(&net); +/// // The Xor has been lowered to And gates +/// assert_eq!(stats(&aig).nb_xor, 0); +/// ``` +pub fn to_aig(aig: &Network) -> Network { + // Reduce the gate variety first: Or/Nand/Nor/Xnor become And/Xor and Buf disappears, + // so only And, Xor, Mux, Maj, Lut and Dff remain to handle below. + let mut src = aig.clone(); + src.make_canonical(); + assert!(src.is_topo_sorted()); + + let mut ret = Network::new(); + ret.add_inputs(src.nb_inputs()); + let mut trans = vec![Signal::placeholder(); src.nb_nodes()]; + + // Pre-allocate flip-flops so their output signal exists during the combinatorial pass + // (a flip-flop input may be driven by a later node). + for (i, t) in trans.iter_mut().enumerate() { + if !src.gate(i).is_comb() { + *t = ret.add(Gate::dff( + Signal::placeholder(), + Signal::one(), + Signal::zero(), + )); + } + } + + // Decompose combinatorial gates in topological order + for i in 0..src.nb_nodes() { + let g = src.gate(i); + if !g.is_comb() { + continue; + } + let deps: Vec = g + .dependencies() + .iter() + .map(|s| translate(*s, &trans)) + .collect(); + let s = match g { + Gate::Binary(_, BinaryType::And) => ret.and(deps[0], deps[1]), + Gate::Binary(_, BinaryType::Xor) => xor2(&mut ret, deps[0], deps[1]), + Gate::Ternary(_, TernaryType::And) => and_all(&mut ret, &deps), + Gate::Ternary(_, TernaryType::Xor) => xor_all(&mut ret, &deps), + Gate::Ternary(_, TernaryType::Mux) => mux2(&mut ret, deps[0], deps[1], deps[2]), + Gate::Ternary(_, TernaryType::Maj) => maj3(&mut ret, deps[0], deps[1], deps[2]), + Gate::Nary(_, NaryType::And) => and_all(&mut ret, &deps), + Gate::Nary(_, NaryType::Xor) => xor_all(&mut ret, &deps), + Gate::Lut(lut) => lut_to_and(&mut ret, &lut.lut, &deps), + _ => unreachable!("unexpected gate kind after canonicalization: {g}"), + }; + trans[i] = s; + } + + // Now that every signal is known, wire up the flip-flop inputs + for i in 0..src.nb_nodes() { + if let Gate::Dff([d, en, res]) = src.gate(i) { + let nd = translate(*d, &trans); + let nen = translate(*en, &trans); + let nres = translate(*res, &trans); + ret.replace(trans[i].var() as usize, Gate::dff(nd, nen, nres)); + } + } + + for o in 0..src.nb_outputs() { + ret.add_output(translate(src.output(o), &trans)); + } + ret.topo_sort(); + ret.make_canonical(); + ret.cleanup(); + ret +} + +#[cfg(test)] +mod tests { + use volute::Lut3; + + use super::to_aig; + use crate::equiv::{check_equivalence_bounded, check_equivalence_comb}; + use crate::network::generators::{adder, testcases}; + use crate::network::stats::stats; + use crate::{Gate, Network}; + + /// Assert the network is a pure 2-input AIG: only 2-input Ands (plus flip-flops) + fn assert_pure_aig(aig: &Network) { + let st = stats(aig); + assert_eq!(st.nb_xor, 0, "Xor gate remains"); + assert_eq!(st.nb_mux, 0, "Mux gate remains"); + assert_eq!(st.nb_maj, 0, "Maj gate remains"); + assert_eq!(st.nb_lut, 0, "Lut gate remains"); + for (arity, nb) in st.and_arity.iter().enumerate() { + if arity != 2 { + assert_eq!(*nb, 0, "And of arity {arity} remains"); + } + } + } + + #[test] + fn test_to_aig_xor3() { + let mut aig = Network::new(); + let a = aig.add_input(); + let b = aig.add_input(); + let c = aig.add_input(); + let o = aig.add(Gate::xor3(a, b, c)); + aig.add_output(o); + let res = to_aig(&aig); + check_equivalence_comb(&aig, &res, true).unwrap(); + assert_pure_aig(&res); + } + + #[test] + fn test_to_aig_mux_maj() { + let mut aig = Network::new(); + let a = aig.add_input(); + let b = aig.add_input(); + let c = aig.add_input(); + let m = aig.add(Gate::mux(a, b, c)); + let j = aig.add(Gate::maj(a, b, c)); + aig.add_output(m); + aig.add_output(j); + let res = to_aig(&aig); + check_equivalence_comb(&aig, &res, true).unwrap(); + assert_pure_aig(&res); + } + + #[test] + fn test_to_aig_lut() { + let mut aig = Network::new(); + let a = aig.add_input(); + let b = aig.add_input(); + let c = aig.add_input(); + // 3-input majority as a Lut, exercising the sum-of-products decomposition + let o = aig.add(Gate::lut(&[a, b, c], Lut3::threshold(2).into())); + aig.add_output(o); + let res = to_aig(&aig); + check_equivalence_comb(&aig, &res, true).unwrap(); + assert_pure_aig(&res); + } + + #[test] + fn test_to_aig_adder() { + let aig = adder::ripple_carry(4); + let res = to_aig(&aig); + check_equivalence_comb(&aig, &res, true).unwrap(); + assert_pure_aig(&res); + } + + #[test] + fn test_to_aig_sequential() { + let aig = testcases::toggle_chain(4, true, true); + let res = to_aig(&aig); + check_equivalence_bounded(&aig, &res, 6, true).unwrap(); + let st = stats(&res); + assert!(st.nb_dff >= 1, "flip-flops should be preserved"); + assert_eq!(st.nb_xor, 0); + } + + #[test] + fn test_to_aig_idempotent() { + let aig = adder::ripple_carry(3); + let a1 = to_aig(&aig); + let a2 = to_aig(&a1); + check_equivalence_comb(&a1, &a2, true).unwrap(); + assert_pure_aig(&a2); + } +} diff --git a/src/optim/balance.rs b/src/optim/balance.rs new file mode 100644 index 0000000..11631d9 --- /dev/null +++ b/src/optim/balance.rs @@ -0,0 +1,204 @@ +//! Depth-oriented balancing of And and Xor trees +//! +//! Restructures associative And and Xor gates into minimum-depth trees of 2-input +//! gates, reducing the combinational depth of the network without changing its +//! function. This complements [`share_logic`](super::share_logic), which optimizes +//! for area (sharing) rather than depth. + +use std::cmp::Reverse; +use std::collections::BinaryHeap; + +use crate::{Gate, Network, Signal}; + +use super::share_logic::flatten_nary; + +/// Logic level of a signal given the levels of all nodes +fn signal_level(s: Signal, level: &[u32]) -> u32 { + if s.is_var() { + level[s.var() as usize] + } else { + 0 + } +} + +/// Build a minimum-depth tree of 2-input gates computing the And or Xor of the leaves +/// +/// Repeatedly combines the two lowest-level signals, which minimizes the depth of the +/// resulting tree. New gates are appended to `ret`, and their level is recorded in +/// `level` so the invariant `level.len() == ret.nb_nodes()` is maintained. +fn build_tree(ret: &mut Network, level: &mut Vec, leaves: &[Signal], is_and: bool) -> Signal { + debug_assert!(!leaves.is_empty()); + // Min-heap keyed by (level, tie-breaker, signal): always combine the two shallowest + // signals first. The tie-breaker keeps the construction deterministic. + let mut heap = BinaryHeap::new(); + let mut tie = 0u32; + for &s in leaves { + heap.push(Reverse((signal_level(s, level), tie, s))); + tie += 1; + } + while heap.len() >= 2 { + let Reverse((la, _, a)) = heap.pop().unwrap(); + let Reverse((lb, _, b)) = heap.pop().unwrap(); + let g = if is_and { + Gate::and(a, b) + } else { + Gate::xor(a, b) + }; + let s = ret.add(g); + debug_assert_eq!(s.var() as usize, level.len()); + let new_level = la.max(lb) + 1; + level.push(new_level); + heap.push(Reverse((new_level, tie, s))); + tie += 1; + } + let Reverse((_, _, root)) = heap.pop().unwrap(); + root +} + +/// Balance And and Xor trees to reduce the combinational depth of the network +/// +/// Functionality is preserved. Other gates (Mux, Maj, Dff, Lut) are left untouched. +/// The transformation is deterministic. +/// +/// ``` +/// # use quaigh::{Gate, Network}; +/// use quaigh::optim::balance; +/// use quaigh::network::stats::depth; +/// +/// // A right-leaning And chain of 8 inputs is 7 levels deep +/// let mut aig = Network::new(); +/// let mut sigs = Vec::new(); +/// for _ in 0..8 { +/// sigs.push(aig.add_input()); +/// } +/// let mut acc = sigs[0]; +/// for s in &sigs[1..] { +/// acc = aig.and(acc, *s); +/// } +/// aig.add_output(acc); +/// assert_eq!(depth(&aig), 7); +/// +/// // Balancing turns it into a tree, reducing the depth to 3 +/// let balanced = balance(&aig); +/// assert!(depth(&balanced) <= 3); +/// ``` +pub fn balance(aig: &Network) -> Network { + balance_with(aig, 64) +} + +/// Balance with an explicit flattening limit; see [`flatten_nary`] +pub fn balance_with(aig: &Network, max_size: usize) -> Network { + // Flatten associative chains into N-ary gates so a whole chain is rebuilt at once + let flat = flatten_nary(aig, max_size); + assert!(flat.is_topo_sorted()); + + let mut ret = flat.clone(); + // Combinational level of each node, grown as balanced-tree gates are appended. + // Processing in topological order means every gate input level is known when used. + let mut level = vec![0u32; flat.nb_nodes()]; + + for i in 0..flat.nb_nodes() { + let g = flat.gate(i).clone(); + if g.is_and() || g.is_xor() { + let leaves: Vec = g.dependencies().to_vec(); + let root = build_tree(&mut ret, &mut level, &leaves, g.is_and()); + ret.replace(i, Gate::Buf(root)); + level[i] = signal_level(root, &level); + } else if g.is_comb() { + let mut m = 0; + for v in g.vars() { + m = m.max(level[v as usize]); + } + level[i] = if g.is_buf_like() { m } else { m + 1 }; + } else { + // Flip-flop output: sequential source at level 0 + level[i] = 0; + } + } + + // The Buf placeholders reference appended gates, so re-sort, then canonicalize the + // freshly added 2-input gates and drop the now-unused original gates. + ret.topo_sort(); + ret.make_canonical(); + ret.cleanup(); + ret +} + +#[cfg(test)] +mod tests { + use super::{balance, balance_with}; + use crate::equiv::check_equivalence_comb; + use crate::network::generators::adder; + use crate::network::stats::depth; + use crate::Network; + + fn deep_and_chain(n: usize) -> Network { + let mut aig = Network::new(); + let mut sigs = Vec::new(); + for _ in 0..n { + sigs.push(aig.add_input()); + } + let mut acc = sigs[0]; + for s in &sigs[1..] { + acc = aig.and(acc, *s); + } + aig.add_output(acc); + aig + } + + fn deep_xor_chain(n: usize) -> Network { + let mut aig = Network::new(); + let mut sigs = Vec::new(); + for _ in 0..n { + sigs.push(aig.add_input()); + } + let mut acc = sigs[0]; + for s in &sigs[1..] { + acc = aig.xor(acc, *s); + } + aig.add_output(acc); + aig + } + + #[test] + fn test_balance_and_chain_reduces_depth() { + let aig = deep_and_chain(8); + assert_eq!(depth(&aig), 7); + let balanced = balance(&aig); + check_equivalence_comb(&aig, &balanced, true).unwrap(); + assert!(depth(&balanced) <= 3, "depth was {}", depth(&balanced)); + } + + #[test] + fn test_balance_xor_chain_reduces_depth() { + let aig = deep_xor_chain(8); + assert_eq!(depth(&aig), 7); + let balanced = balance(&aig); + check_equivalence_comb(&aig, &balanced, true).unwrap(); + assert!(depth(&balanced) <= 3, "depth was {}", depth(&balanced)); + } + + #[test] + fn test_balance_preserves_adder() { + let aig = adder::ripple_carry(4); + let balanced = balance(&aig); + check_equivalence_comb(&aig, &balanced, true).unwrap(); + } + + #[test] + fn test_balance_is_deterministic() { + let aig = deep_and_chain(20); + let b1 = balance(&aig); + let b2 = balance(&aig); + assert_eq!(format!("{b1}"), format!("{b2}")); + } + + #[test] + fn test_balance_is_stable() { + // Balancing an already-balanced network keeps it equivalent + let aig = deep_and_chain(16); + let b1 = balance(&aig); + let b2 = balance_with(&b1, 64); + check_equivalence_comb(&b1, &b2, true).unwrap(); + } +} diff --git a/src/optim/cuts.rs b/src/optim/cuts.rs new file mode 100644 index 0000000..7a8406f --- /dev/null +++ b/src/optim/cuts.rs @@ -0,0 +1,399 @@ +//! Cut enumeration +//! +//! A *cut* of a node is a set of signals (the *leaves*) such that every path from a +//! primary input to the node passes through a leaf: the node's value is fully +//! determined by the leaves. A *k-feasible* cut has at most `k` leaves. Cut enumeration +//! computes, for every node, a set of k-feasible cuts; it is the basis for FPGA +//! technology mapping, local rewriting and many other optimizations. +//! +//! The enumeration is bottom-up: a node's cuts are its trivial self-cut together with +//! every combination of its fanins' cuts whose union stays within `k` leaves. Dominated +//! cuts (supersets of another cut) are removed, and the number of cuts per node is +//! capped to keep high-fanin gates tractable (priority cuts). +//! +//! Flip-flops are treated as combinational boundaries: a flip-flop has only its trivial +//! cut, so cuts never cross a register. Primary inputs are implicit leaves with a single +//! trivial cut and are not part of the returned per-node vector. + +use std::fmt; + +use crate::{Network, Signal}; + +/// Default maximum number of cuts kept per node (priority-cut limit) +const DEFAULT_MAX_CUTS: usize = 8; + +/// A cut: the set of leaf signals whose values determine the cut's root +/// +/// Leaves are stored sorted and without inversion (a cut describes structural support, +/// not polarity). Constants are never leaves. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Cut { + leaves: Vec, +} + +impl Cut { + /// The trivial cut of a signal: the signal itself as the only leaf + fn trivial(s: Signal) -> Cut { + Cut { + leaves: vec![s.without_inversion()], + } + } + + /// The empty cut (no leaves), used as the identity when merging fanins + fn empty() -> Cut { + Cut { leaves: Vec::new() } + } + + /// The leaves of the cut, sorted and without inversion + pub fn leaves(&self) -> &[Signal] { + &self.leaves + } + + /// Number of leaves + pub fn len(&self) -> usize { + self.leaves.len() + } + + /// Whether the cut has no leaves + pub fn is_empty(&self) -> bool { + self.leaves.is_empty() + } + + /// Union of two cuts, or `None` if the result would exceed `max` leaves + fn union(&self, other: &Cut, max: usize) -> Option { + let mut leaves = Vec::with_capacity(self.leaves.len() + other.leaves.len()); + let (mut i, mut j) = (0, 0); + while i < self.leaves.len() && j < other.leaves.len() { + let a = self.leaves[i]; + let b = other.leaves[j]; + if a < b { + leaves.push(a); + i += 1; + } else if b < a { + leaves.push(b); + j += 1; + } else { + leaves.push(a); + i += 1; + j += 1; + } + if leaves.len() > max { + return None; + } + } + if self.leaves.len() - i + leaves.len() > max || other.leaves.len() - j + leaves.len() > max + { + return None; + } + leaves.extend_from_slice(&self.leaves[i..]); + leaves.extend_from_slice(&other.leaves[j..]); + Some(Cut { leaves }) + } + + /// Whether `self` is a subset of `other` (so `self` dominates `other`) + fn dominates(&self, other: &Cut) -> bool { + if self.leaves.len() > other.leaves.len() { + return false; + } + let mut j = 0; + for s in &self.leaves { + while j < other.leaves.len() && other.leaves[j] < *s { + j += 1; + } + if j >= other.leaves.len() || other.leaves[j] != *s { + return false; + } + j += 1; + } + true + } +} + +impl fmt::Display for Cut { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{{")?; + for (i, s) in self.leaves.iter().enumerate() { + if i != 0 { + write!(f, ", ")?; + } + write!(f, "{s}")?; + } + write!(f, "}}") + } +} + +/// Remove duplicate and dominated cuts (a cut that is a superset of another) +fn prune_dominated(cuts: &mut Vec) { + cuts.sort_by(|a, b| a.leaves.cmp(&b.leaves)); + cuts.dedup(); + let kept = cuts.clone(); + cuts.retain(|c| !kept.iter().any(|other| other != c && other.dominates(c))); +} + +/// Cut set of a fanin signal: trivial for inputs, empty for constants, computed for gates +fn fanin_cuts(s: Signal, cuts: &[Vec]) -> Vec { + if s.is_constant() { + vec![Cut::empty()] + } else if s.is_input() { + vec![Cut::trivial(s)] + } else { + cuts[s.var() as usize].clone() + } +} + +/// Merge two cut sets: every feasible union of a cut from each, dominance-pruned +fn merge_cut_sets(a: &[Cut], b: &[Cut], k: usize) -> Vec { + let mut result = Vec::new(); + for ca in a { + for cb in b { + if let Some(u) = ca.union(cb, k) { + result.push(u); + } + } + } + prune_dominated(&mut result); + result +} + +/// Enumerate k-feasible cuts for every node, with the default priority-cut limit +/// +/// Returns one vector of cuts per node, indexed by node (gate) index. Each node's first +/// cut is its trivial self-cut. Primary inputs are not included (their only cut is trivial). +pub fn enumerate_cuts(aig: &Network, max_cut_size: usize) -> Vec> { + enumerate_cuts_with(aig, max_cut_size, DEFAULT_MAX_CUTS) +} + +/// Enumerate k-feasible cuts, keeping at most `max_cuts_per_node` cuts per node +/// +/// The trivial self-cut is always kept; among the remaining cuts the smallest are +/// preferred. See [`enumerate_cuts`]. +pub fn enumerate_cuts_with( + aig: &Network, + max_cut_size: usize, + max_cuts_per_node: usize, +) -> Vec> { + assert!(aig.is_topo_sorted()); + assert!(max_cut_size >= 1, "cut size must be at least 1"); + assert!(max_cuts_per_node >= 1, "must keep at least the trivial cut"); + + let mut cuts: Vec> = Vec::with_capacity(aig.nb_nodes()); + for i in 0..aig.nb_nodes() { + let node_sig = Signal::from_var(i as u32); + let g = aig.gate(i); + + if !g.is_comb() { + // Flip-flop: combinational boundary, only the trivial cut + cuts.push(vec![Cut::trivial(node_sig)]); + continue; + } + + // Fold-merge the fanin cut sets, starting from a single empty cut + let mut merged = vec![Cut::empty()]; + for fanin in g.dependencies() { + let fc = fanin_cuts(*fanin, &cuts); + merged = merge_cut_sets(&merged, &fc, max_cut_size); + } + // Drop empty cuts (only arise from all-constant fanins) and prune + merged.retain(|c| !c.is_empty()); + prune_dominated(&mut merged); + + // Priority limit: keep the smallest cuts, leaving room for the trivial cut + let other_limit = max_cuts_per_node - 1; + if merged.len() > other_limit { + merged.sort_by(|a, b| a.len().cmp(&b.len()).then_with(|| a.leaves.cmp(&b.leaves))); + merged.truncate(other_limit); + } + + let mut node_cuts = Vec::with_capacity(merged.len() + 1); + node_cuts.push(Cut::trivial(node_sig)); + node_cuts.extend(merged); + cuts.push(node_cuts); + } + cuts +} + +/// Verify that a set of leaves is a valid cut of a node +/// +/// A cut is valid when every path from the root up to a primary input or flip-flop +/// passes through a leaf. Useful for testing and for validating externally built cuts. +pub fn is_valid_cut(aig: &Network, root: u32, cut: &Cut) -> bool { + let mut memo = vec![None; aig.nb_nodes()]; + covers(aig, Signal::from_var(root), &cut.leaves, &mut memo) +} + +/// Whether all paths from `s` up to the inputs pass through a leaf +fn covers(aig: &Network, s: Signal, leaves: &[Signal], memo: &mut [Option]) -> bool { + let s = s.without_inversion(); + if leaves.contains(&s) { + return true; + } + if s.is_constant() { + return true; + } + if s.is_input() { + // Reached a primary input that is not a leaf: the cut does not cover it + return false; + } + let v = s.var() as usize; + if let Some(r) = memo[v] { + return r; + } + let g = aig.gate(v); + // A flip-flop that is not a leaf is a sequential boundary the cut fails to cover + let r = g.is_comb() + && g.dependencies() + .iter() + .all(|f| covers(aig, *f, leaves, memo)); + memo[v] = Some(r); + r +} + +/// Total number of k-feasible cuts across all nodes (including trivial cuts) +pub fn count_cuts(aig: &Network, max_cut_size: usize) -> usize { + enumerate_cuts(aig, max_cut_size) + .iter() + .map(|c| c.len()) + .sum() +} + +#[cfg(test)] +mod tests { + use super::{count_cuts, enumerate_cuts, enumerate_cuts_with, is_valid_cut, Cut}; + use crate::network::generators::{adder, testcases}; + use crate::{Gate, Network, Signal}; + + /// Every cut of every node must be a valid cut, k-feasible, with the trivial cut first + fn check_all(aig: &Network, k: usize) { + let cuts = enumerate_cuts(aig, k); + assert_eq!(cuts.len(), aig.nb_nodes()); + for (i, node_cuts) in cuts.iter().enumerate() { + assert!(!node_cuts.is_empty(), "node {i} has no cut"); + assert_eq!( + node_cuts[0], + Cut::trivial(Signal::from_var(i as u32)), + "first cut of node {i} should be trivial" + ); + for c in node_cuts { + assert!(c.len() <= k, "cut {c} of node {i} exceeds k={k}"); + assert!( + is_valid_cut(aig, i as u32, c), + "cut {c} of node {i} is invalid" + ); + } + } + } + + #[test] + fn test_single_and() { + let mut aig = Network::new(); + let i0 = aig.add_input(); + let i1 = aig.add_input(); + let o = aig.and(i0, i1); + aig.add_output(o); + + // k >= 2: trivial cut plus the {i0, i1} cut + let cuts = enumerate_cuts(&aig, 4); + assert_eq!(cuts[0].len(), 2); + assert_eq!(cuts[0][0], Cut::trivial(o)); + let mut leaves = cuts[0][1].leaves().to_vec(); + leaves.sort(); + let mut expected = vec![i0, i1]; + expected.sort(); + assert_eq!(leaves, expected); + + // k == 1: only the trivial cut fits + let cuts1 = enumerate_cuts(&aig, 1); + assert_eq!(cuts1[0].len(), 1); + assert_eq!(cuts1[0][0], Cut::trivial(o)); + } + + #[test] + fn test_cuts_valid_adder() { + for k in 2..=6 { + check_all(&adder::ripple_carry(4), k); + } + } + + #[test] + fn test_cuts_valid_mux_maj_lut() { + let mut aig = Network::new(); + let a = aig.add_input(); + let b = aig.add_input(); + let c = aig.add_input(); + let m = aig.add(Gate::mux(a, b, c)); + let j = aig.add(Gate::maj(a, b, c)); + let o = aig.and(m, j); + aig.add_output(o); + for k in 2..=4 { + check_all(&aig, k); + } + } + + #[test] + fn test_two_level() { + // o = (i0 & i1) & i2, a reconvergent-free 3-input function + let mut aig = Network::new(); + let i0 = aig.add_input(); + let i1 = aig.add_input(); + let i2 = aig.add_input(); + let x = aig.and(i0, i1); + let o = aig.and(x, i2); + aig.add_output(o); + + let cuts = enumerate_cuts(&aig, 3); + let o_cuts = &cuts[o.var() as usize]; + let has = |want: &[Signal]| { + let mut w = want.to_vec(); + w.sort(); + o_cuts.iter().any(|c| { + let mut l = c.leaves().to_vec(); + l.sort(); + l == w + }) + }; + // o has the cuts {x, i2} (across the intermediate gate) and {i0, i1, i2} + assert!(has(&[x, i2])); + assert!(has(&[i0, i1, i2])); + } + + #[test] + fn test_dff_is_boundary() { + // A flip-flop output must be a leaf; cuts never cross it + let aig = testcases::toggle_chain(3, true, true); + let cuts = enumerate_cuts(&aig, 4); + for (i, node_cuts) in cuts.iter().enumerate() { + if !aig.gate(i).is_comb() { + // A flip-flop has only the trivial cut + assert_eq!(node_cuts.len(), 1); + assert_eq!(node_cuts[0], Cut::trivial(Signal::from_var(i as u32))); + } + } + // And all cuts remain valid (covers() rejects crossing a Dff) + check_all(&aig, 4); + } + + #[test] + fn test_priority_limit() { + // A wide gate would have many cuts; the limit caps them + let mut aig = Network::new(); + let mut sigs = Vec::new(); + for _ in 0..10 { + sigs.push(aig.add_input()); + } + let o = aig.add(Gate::andn(&sigs)); + aig.add_output(o); + + let limit = 5; + let cuts = enumerate_cuts_with(&aig, 4, limit); + assert!(cuts[o.var() as usize].len() <= limit); + // The trivial cut is still present and first + assert_eq!(cuts[o.var() as usize][0], Cut::trivial(o)); + } + + #[test] + fn test_count_cuts() { + let aig = adder::ripple_carry(2); + // At least one cut (the trivial one) per node + assert!(count_cuts(&aig, 4) >= aig.nb_nodes()); + } +} diff --git a/src/optim/mig.rs b/src/optim/mig.rs new file mode 100644 index 0000000..3c574db --- /dev/null +++ b/src/optim/mig.rs @@ -0,0 +1,296 @@ +//! Conversion to a Majority-Inverter Graph (MIG) +//! +//! Lowers every gate to 3-input Majority gates with implicit inversions, the +//! representation used by majority-based logic synthesis. And/Or become a Maj with a +//! constant input (`And(a,b) = Maj(a,b,0)`, `Or(a,b) = Maj(a,b,1)`); Xor, Mux and Lut +//! are decomposed into Maj gates; flip-flops are kept. This is the majority counterpart +//! of the And-based [`to_aig`](super::to_aig) view. +//! +//! Note: the result is intentionally *not* run through canonicalization, because that +//! would rewrite `Maj(a,b,0)` back into an And and destroy the majority view. The +//! simulator and equivalence checker handle Maj-with-constant directly, so the result +//! is still fully usable and verifiable. + +use volute::Lut; + +use crate::network::{BinaryType, NaryType, TernaryType}; +use crate::{Gate, Network, Signal}; + +/// Translate a signal of the source network into the rebuilt network +fn translate(s: Signal, trans: &[Signal]) -> Signal { + if s.is_var() { + trans[s.var() as usize] ^ s.is_inverted() + } else { + s + } +} + +/// `a & b` as a majority: `Maj(a, b, 0)` +fn and2(ret: &mut Network, a: Signal, b: Signal) -> Signal { + ret.add(Gate::maj(a, b, Signal::zero())) +} + +/// `a | b` as a majority: `Maj(a, b, 1)` +fn or2(ret: &mut Network, a: Signal, b: Signal) -> Signal { + ret.add(Gate::maj(a, b, Signal::one())) +} + +/// `a ^ b` as majorities: `Maj( a & !b, !a & b, 1 )` +fn xor2(ret: &mut Network, a: Signal, b: Signal) -> Signal { + let p = and2(ret, a, !b); + let q = and2(ret, !a, b); + or2(ret, p, q) +} + +/// `s ? a : b` as majorities: `Maj( s & a, !s & b, 1 )` +fn mux2(ret: &mut Network, s: Signal, a: Signal, b: Signal) -> Signal { + let p = and2(ret, s, a); + let q = and2(ret, !s, b); + or2(ret, p, q) +} + +/// And of all signals, folded with 2-input majority Ands +fn and_all(ret: &mut Network, sigs: &[Signal]) -> Signal { + if sigs.is_empty() { + return Signal::one(); + } + let mut acc = sigs[0]; + for s in &sigs[1..] { + acc = and2(ret, acc, *s); + } + acc +} + +/// Or of all signals, folded with 2-input majority Ors +fn or_all(ret: &mut Network, sigs: &[Signal]) -> Signal { + if sigs.is_empty() { + return Signal::zero(); + } + let mut acc = sigs[0]; + for s in &sigs[1..] { + acc = or2(ret, acc, *s); + } + acc +} + +/// Xor of all signals, folded with 2-input majority Xors +fn xor_all(ret: &mut Network, sigs: &[Signal]) -> Signal { + if sigs.is_empty() { + return Signal::zero(); + } + let mut acc = sigs[0]; + for s in &sigs[1..] { + acc = xor2(ret, acc, *s); + } + acc +} + +/// Decompose a Lut into majorities through its sum of products (the true minterms) +fn lut_to_mig(ret: &mut Network, lut: &Lut, inputs: &[Signal]) -> Signal { + let n = lut.num_vars(); + let mut products = Vec::new(); + for mask in 0..lut.num_bits() { + if lut.value(mask) { + let lits: Vec = (0..n).map(|i| inputs[i] ^ ((mask >> i) & 1 == 0)).collect(); + products.push(and_all(ret, &lits)); + } + } + if products.is_empty() { + return Signal::zero(); + } + or_all(ret, &products) +} + +/// Convert a network to a Majority-Inverter Graph +/// +/// All combinatorial logic is expressed with 3-input Maj gates and implicit inverters. +/// Flip-flops are preserved, so sequential networks stay sequential. The result is +/// functionally equivalent to the input. +/// +/// ``` +/// # use quaigh::Network; +/// use quaigh::optim::to_mig; +/// use quaigh::network::stats::stats; +/// +/// let mut net = Network::new(); +/// let a = net.add_input(); +/// let b = net.add_input(); +/// let o = net.and(a, b); +/// net.add_output(o); +/// +/// let mig = to_mig(&net); +/// // The And has been expressed as a majority gate +/// assert_eq!(stats(&mig).nb_and, 0); +/// assert!(stats(&mig).nb_maj >= 1); +/// ``` +pub fn to_mig(aig: &Network) -> Network { + // Reduce the gate variety first: Or/Nand/Nor/Xnor become And/Xor and Buf disappears, + // so only And, Xor, Mux, Maj, Lut and Dff remain to handle below. + let mut src = aig.clone(); + src.make_canonical(); + assert!(src.is_topo_sorted()); + + let mut ret = Network::new(); + ret.add_inputs(src.nb_inputs()); + let mut trans = vec![Signal::placeholder(); src.nb_nodes()]; + + // Pre-allocate flip-flops so their output signal exists during the combinatorial pass + // (a flip-flop input may be driven by a later node). + for (i, t) in trans.iter_mut().enumerate() { + if !src.gate(i).is_comb() { + *t = ret.add(Gate::dff( + Signal::placeholder(), + Signal::one(), + Signal::zero(), + )); + } + } + + // Decompose combinatorial gates in topological order + for i in 0..src.nb_nodes() { + let g = src.gate(i); + if !g.is_comb() { + continue; + } + let deps: Vec = g + .dependencies() + .iter() + .map(|s| translate(*s, &trans)) + .collect(); + let s = match g { + Gate::Binary(_, BinaryType::And) => and2(&mut ret, deps[0], deps[1]), + Gate::Binary(_, BinaryType::Xor) => xor2(&mut ret, deps[0], deps[1]), + Gate::Ternary(_, TernaryType::And) => and_all(&mut ret, &deps), + Gate::Ternary(_, TernaryType::Xor) => xor_all(&mut ret, &deps), + Gate::Ternary(_, TernaryType::Mux) => mux2(&mut ret, deps[0], deps[1], deps[2]), + Gate::Ternary(_, TernaryType::Maj) => ret.add(Gate::maj(deps[0], deps[1], deps[2])), + Gate::Nary(_, NaryType::And) => and_all(&mut ret, &deps), + Gate::Nary(_, NaryType::Xor) => xor_all(&mut ret, &deps), + Gate::Lut(lut) => lut_to_mig(&mut ret, &lut.lut, &deps), + _ => unreachable!("unexpected gate kind after canonicalization: {g}"), + }; + trans[i] = s; + } + + // Now that every signal is known, wire up the flip-flop inputs + for i in 0..src.nb_nodes() { + if let Gate::Dff([d, en, res]) = src.gate(i) { + let nd = translate(*d, &trans); + let nen = translate(*en, &trans); + let nres = translate(*res, &trans); + ret.replace(trans[i].var() as usize, Gate::dff(nd, nen, nres)); + } + } + + for o in 0..src.nb_outputs() { + ret.add_output(translate(src.output(o), &trans)); + } + ret.topo_sort(); + // Merge identical Maj nodes without canonicalizing (which would collapse Maj->And) + ret.deduplicate(); + ret.cleanup(); + ret.check(); + ret +} + +#[cfg(test)] +mod tests { + use volute::Lut3; + + use super::to_mig; + use crate::equiv::{check_equivalence_bounded, check_equivalence_comb}; + use crate::network::generators::{adder, testcases}; + use crate::network::stats::stats; + use crate::{Gate, Network}; + + /// Assert the network is a pure MIG: only Maj gates (plus flip-flops), no And/Xor/Mux/Lut + fn assert_pure_mig(aig: &Network) { + let st = stats(aig); + assert_eq!(st.nb_and, 0, "And gate remains"); + assert_eq!(st.nb_xor, 0, "Xor gate remains"); + assert_eq!(st.nb_mux, 0, "Mux gate remains"); + assert_eq!(st.nb_lut, 0, "Lut gate remains"); + } + + #[test] + fn test_to_mig_and() { + let mut aig = Network::new(); + let a = aig.add_input(); + let b = aig.add_input(); + let o = aig.and(a, b); + aig.add_output(o); + let res = to_mig(&aig); + check_equivalence_comb(&aig, &res, false).unwrap(); + assert_pure_mig(&res); + assert!(stats(&res).nb_maj >= 1); + } + + #[test] + fn test_to_mig_xor3() { + let mut aig = Network::new(); + let a = aig.add_input(); + let b = aig.add_input(); + let c = aig.add_input(); + let o = aig.add(Gate::xor3(a, b, c)); + aig.add_output(o); + let res = to_mig(&aig); + check_equivalence_comb(&aig, &res, false).unwrap(); + assert_pure_mig(&res); + } + + #[test] + fn test_to_mig_mux_maj() { + let mut aig = Network::new(); + let a = aig.add_input(); + let b = aig.add_input(); + let c = aig.add_input(); + let m = aig.add(Gate::mux(a, b, c)); + let j = aig.add(Gate::maj(a, b, c)); + aig.add_output(m); + aig.add_output(j); + let res = to_mig(&aig); + check_equivalence_comb(&aig, &res, false).unwrap(); + assert_pure_mig(&res); + } + + #[test] + fn test_to_mig_lut() { + let mut aig = Network::new(); + let a = aig.add_input(); + let b = aig.add_input(); + let c = aig.add_input(); + let o = aig.add(Gate::lut(&[a, b, c], Lut3::threshold(2).into())); + aig.add_output(o); + let res = to_mig(&aig); + check_equivalence_comb(&aig, &res, false).unwrap(); + assert_pure_mig(&res); + } + + #[test] + fn test_to_mig_adder() { + let aig = adder::ripple_carry(4); + let res = to_mig(&aig); + check_equivalence_comb(&aig, &res, false).unwrap(); + assert_pure_mig(&res); + } + + #[test] + fn test_to_mig_sequential() { + let aig = testcases::toggle_chain(4, true, true); + let res = to_mig(&aig); + check_equivalence_bounded(&aig, &res, 6, false).unwrap(); + let st = stats(&res); + assert!(st.nb_dff >= 1, "flip-flops should be preserved"); + assert_eq!(st.nb_and, 0); + assert_eq!(st.nb_xor, 0); + } + + #[test] + fn test_to_mig_idempotent() { + let aig = adder::ripple_carry(3); + let m1 = to_mig(&aig); + let m2 = to_mig(&m1); + check_equivalence_comb(&m1, &m2, false).unwrap(); + assert_pure_mig(&m2); + } +}