Algorithms¶
This section covers the various algorithms available in aigverse for working with And-Inverter Graphs (AIGs) and other logic representations. These algorithms enable simulation, optimization, and verification of logic networks.
Simulation¶
Simulation algorithms allow you to evaluate the outputs of a logic network for all possible input combinations, effectively generating truth tables for the network’s outputs and internal nodes.
Functional Simulation¶
For simulating AIGs with truth tables, the simulate() and
simulate_nodes() functions allow you to obtain truth tables for outputs and internal nodes of an
AIG.
1from aigverse.networks import Aig
2from aigverse.algorithms import simulate, simulate_nodes
3
4# Create a sample AIG
5aig = Aig()
6a = aig.create_pi()
7b = aig.create_pi()
8f_and = aig.create_and(a, b)
9f_or = aig.create_or(a, b)
10aig.create_po(f_and)
11aig.create_po(f_or)
12
13# Simulate the outputs
14output_tts = simulate(aig)
15
16# Print the truth tables
17print("Truth tables of outputs:")
18for i, tt in enumerate(output_tts):
19 print(f" Output {i}: {tt.to_binary()}")
20
21# Simulate all nodes
22node_tts = simulate_nodes(aig)
23
24# Print the truth table of each node
25print("\nTruth tables of nodes:")
26for node, tt in node_tts.items():
27 print(f" Node {node}: {tt.to_binary()}")
Truth tables of outputs:
Output 0: 1000
Output 1: 1110
Truth tables of nodes:
Node 4: 0001
Node 3: 1000
Node 2: 1100
Node 1: 1010
Node 0: 0000
Sequential Simulation¶
simulate() evaluates the combinational logic exactly once and has no notion of a
register. Handed a SequentialAig it never assigns the register outputs at all, so
every value downstream of one is meaningless.
simulate_sequential() runs the network over a number of clock cycles instead. Every
register starts at its reset value, the combinational logic is evaluated once per cycle, the primary outputs are
recorded, and the register inputs are latched into the register outputs for the next cycle.
A design with no primary inputs runs off its reset state alone. A 4-bit linear-feedback shift register seeded with
0b0001 walks through all fifteen of its non-zero states before repeating:
1from aigverse.algorithms import simulate_sequential
2from aigverse.networks import AigRegister, SequentialAig
3
4lfsr = SequentialAig()
5
6state = [lfsr.create_ro() for _ in range(4)]
7feedback = lfsr.create_xor(state[3], state[2])
8
9# Primary outputs go in before register inputs: both are combinational outputs of
10# the same network, and `po_at` / `ri_at` slice that one list by position.
11lfsr.create_po(state[3])
12
13lfsr.create_ri(feedback)
14for bit in range(3):
15 lfsr.create_ri(state[bit])
16
17# Seed the register chain with 0b0001
18for bit in range(4):
19 register = AigRegister()
20 register.init = 1 if bit == 0 else 0
21 lfsr.set_register(bit, register)
22
23result = simulate_sequential(lfsr, 15)
24
25print("output: ", "".join(str(int(cycle[0])) for cycle in result.outputs))
26print("cycles: ", result.num_cycles)
27print("reset: ", result.reset_state)
28print("final: ", result.final_state)
output: 000100110101111
cycles: 15
reset: [True, False, False, False]
final: [True, False, False, False]
A full period brings the registers back to their seed, so final_state matches reset_state.
The result carries two traces, both indexed by clock cycle first: outputs[cycle][index] is the value primary
output index took in that cycle, and states[cycle][index] the value register index held while that cycle was
evaluated. The state trace is one entry longer than the output trace, because simulating n cycles crosses n + 1
state boundaries:
1for cycle, registers in enumerate(result.states):
2 print(f" boundary {cycle:2d}: {''.join(str(int(bit)) for bit in registers)}")
boundary 0: 1000
boundary 1: 0100
boundary 2: 0010
boundary 3: 1001
boundary 4: 1100
boundary 5: 0110
boundary 6: 1011
boundary 7: 0101
boundary 8: 1010
boundary 9: 1101
boundary 10: 1110
boundary 11: 1111
boundary 12: 0111
boundary 13: 0011
boundary 14: 0001
boundary 15: 1000
Designs with primary inputs are driven by a stimulus, one assignment per cycle. Cycles past the end of it repeat the last assignment, so a single assignment holds for the whole run:
1# A three-stage shift register: what goes in comes out three cycles later
2shift = SequentialAig()
3
4data = shift.create_pi()
5stages = [shift.create_ro() for _ in range(3)]
6
7shift.create_po(stages[2])
8
9shift.create_ri(data)
10shift.create_ri(stages[0])
11shift.create_ri(stages[1])
12
13for stage in range(3):
14 register = AigRegister()
15 register.init = 0
16 shift.set_register(stage, register)
17
18# A single pulse on the input, then silence
19pulse = simulate_sequential(shift, 6, [[True], [False]])
20
21print("output:", "".join(str(int(cycle[0])) for cycle in pulse.outputs))
output: 000100
A register may declare no reset value at all, which is what a fresh
AigRegister carries and what an AIGER latch with a nondeterministic reset reads back
as. Simulation needs a concrete value, so undefined_reset_value says which one it should use.
Optimization¶
AIG optimization aims to reduce the number of AND gates and inverters in a circuit while maintaining its logical functionality. Different optimization techniques target various aspects of the AIG structure.
Basic Optimization Workflow¶
The typical optimization workflow involves:
Creating or loading an AIG
Applying one or more optimization algorithms
Verifying correctness through equivalence checking
1from aigverse.io import read_aiger_into_aig
2
3# Load the i10 benchmark circuit - a real-world example
4aig = read_aiger_into_aig("examples/i10.aig")
5
6# Print statistics about the loaded circuit
7print(f"i10 benchmark:")
8print(f" I/O: {aig.num_pis}/{aig.num_pos}")
9print(f" AND gates: {aig.num_gates}")
i10 benchmark:
I/O: 257/224
AND gates: 2675
Resubstitution¶
Resubstitution identifies portions of logic that can be expressed using existing signals in the network. This technique is particularly effective at identifying and eliminating redundant logic.
1from aigverse.algorithms import aig_resubstitution
2
3# Clone the AIG for comparison
4aig_resub = aig.clone()
5
6# Apply resubstitution
7aig_resub = aig_resubstitution(aig_resub, window_size=12)
8
9print(f"Original AND gates: {aig.num_gates}")
10print(f"After resubstitution: {aig_resub.num_gates} AND gates")
11print(f"Reduction: {aig.num_gates - aig_resub.num_gates} gates ({(aig.num_gates - aig_resub.num_gates) / aig.num_gates * 100:.2f}%)")
Original AND gates: 2675
After resubstitution: 2173 AND gates
Reduction: 502 gates (18.77%)
Sum-of-Products Refactoring¶
SOP (Sum of Products) refactoring collapses parts of the AIG into truth tables, then re-synthesizes those portions using Sum-of-Products representations. This can find more efficient implementations for complex logic functions.
1from aigverse.algorithms import sop_refactoring
2
3# Clone the AIG for comparison
4aig_refactor = aig.clone()
5
6# Apply SOP refactoring
7aig_refactor = sop_refactoring(aig_refactor, use_reconvergence_cut=True)
8
9print(f"Original AND gates: {aig.num_gates}")
10print(f"After SOP refactoring: {aig_refactor.num_gates} AND gates")
11print(f"Reduction: {aig.num_gates - aig_refactor.num_gates} gates ({(aig.num_gates - aig_refactor.num_gates) / aig.num_gates * 100:.2f}%)")
Original AND gates: 2675
After SOP refactoring: 2239 AND gates
Reduction: 436 gates (16.30%)
Cut Rewriting¶
Cut rewriting identifies small subgraphs (cuts) in the AIG and replaces them with pre-computed optimal implementations from a library. This technique leverages NPN-equivalence classes to find the best possible implementation for each cut.
1from aigverse.algorithms import aig_cut_rewriting
2
3# Clone the AIG for comparison
4aig_rewrite = aig.clone()
5
6# Apply cut rewriting
7aig_rewrite = aig_cut_rewriting(aig_rewrite, cut_size=4)
8
9print(f"Original AND gates: {aig.num_gates}")
10print(f"After cut rewriting: {aig_rewrite.num_gates} AND gates")
11print(f"Reduction: {aig.num_gates - aig_rewrite.num_gates} gates ({(aig.num_gates - aig_rewrite.num_gates) / aig.num_gates * 100:.2f}%)")
Original AND gates: 2675
After cut rewriting: 2200 AND gates
Reduction: 475 gates (17.76%)
Balancing¶
Balancing performs (E)SOP factoring to minimize the number of levels in the AIG.
1from aigverse.algorithms import balancing
2from aigverse.networks import DepthAig
3
4# Clone the AIG for comparison
5aig_balance = aig.clone()
6
7# Apply balancing
8aig_balance = balancing(aig_balance, rebalance_function="sop")
9
10# Compute depth
11original_depth = DepthAig(aig).num_levels
12balanced_depth = DepthAig(aig_balance).num_levels
13
14print(f"Original depth: {original_depth} levels")
15print(f"After balancing: {balanced_depth} levels")
16print(f"Reduction in depth: {original_depth - balanced_depth} levels ({(original_depth - balanced_depth) / original_depth * 100:.2f}%)")
Original depth: 50 levels
After balancing: 32 levels
Reduction in depth: 18 levels (36.00%)
Combining Optimization Techniques¶
For best results, optimization techniques are typically applied in combination, often in multiple passes. The order of application can significantly impact the final result.
1# Apply optimization techniques in sequence
2aig_opt = aig.clone()
3
4# First pass
5aig_opt = aig_resubstitution(aig_opt)
6aig_opt = sop_refactoring(aig_opt)
7aig_opt = aig_cut_rewriting(aig_opt)
8
9# Second pass
10aig_opt = aig_resubstitution(aig_opt)
11aig_opt = sop_refactoring(aig_opt)
12
13print(f"\nTotal optimization results:")
14print(f"- Original: {aig.num_gates} AND gates")
15print(f"- Optimized: {aig_opt.num_gates} AND gates")
16print(f"- Total reduction: {aig.num_gates - aig_opt.num_gates} gates ({(aig.num_gates - aig_opt.num_gates) / aig.num_gates * 100:.2f}%)")
Total optimization results:
- Original: 2675 AND gates
- Optimized: 1950 AND gates
- Total reduction: 725 gates (27.10%)
Some algorithms offer the inplace=True keyword argument for performance-sensitive pipelines of chained optimization.
Calling functions such as aig_resubstitution() and
sop_refactoring() with inplace=True mutates the passed network and returns None:
1from aigverse.algorithms import cleanup_dangling
2
3aig_fast = aig.clone()
4aig_resubstitution(aig_fast, inplace=True)
5sop_refactoring(aig_fast, inplace=True)
6
7# Explicit cleanup step after in-place chaining
8aig_fast = cleanup_dangling(aig_fast)
Note
When choosing this route, users are responsible to call cleanup_dangling() to obtain a
structurally valid AIG.
Equivalence Checking¶
Equivalence checking algorithms verify that two logic networks implement the same function, which is especially important after performing optimizations.
1from aigverse.algorithms import equivalence_checking
2
3# Verify that our optimized circuit from the previous section maintains functional equivalence
4are_equivalent = equivalence_checking(aig, aig_opt)
5print(f"\nOriginal and optimized benchmark circuits are equivalent: {are_equivalent}")
6print(f"This confirms our optimization preserved the circuit's functionality while reducing")
7print(f"the gate count from {aig.num_gates} to {aig_opt.num_gates} AND gates.")
Original and optimized benchmark circuits are equivalent: True
This confirms our optimization preserved the circuit's functionality while reducing
the gate count from 2086 to 1950 AND gates.