And-Inverter Graphs (AIGs)

The central data structure for working with And-Inverter Graphs (AIGs) in aigverse is the Aig class. It enables efficient representation and manipulation of logic circuits in a form that’s well-suited for optimization and verification tasks.

The following will demonstrate how to work with the Aig class in Python.

Note

AIGs (And-Inverter Graphs) are a compact representation of Boolean functions using only AND gates and inverters (NOT gates). They are widely used in formal verification, hardware design, and optimization tasks.

Quickstart

The following code snippet demonstrates how to create a simple AIG representing basic logic operations.

 1from aigverse.networks import Aig
 2
 3# Create a new AIG network
 4aig = Aig()
 5
 6# Create primary inputs
 7x1 = aig.create_pi()
 8x2 = aig.create_pi()
 9
10# Create logic gates
11f_and = aig.create_and(x1, x2)  # AND gate
12f_or = aig.create_or(x1, x2)    # OR gate
13
14# Create primary outputs
15aig.create_po(f_and)
16aig.create_po(f_or)
17
18# Print the size of the AIG network
19print(f"AIG Size: {aig.size}")
AIG Size: 5

Note

All primary inputs (PIs) must be created before any logic gates.

Basic AIG Concepts

An AIG consists of nodes and signals:

  • Nodes represent either primary inputs, constants, or logic gates

  • Signals reference nodes, possibly with complementation (inversion)

 1# Create a new AIG
 2aig = Aig()
 3
 4# Create a constant (false)
 5const0 = aig.get_constant(False)
 6print(f"Constant 0: {const0}")
 7
 8# Create primary inputs
 9a = aig.create_pi()
10b = aig.create_pi()
11print(f"Input a: {a}")
12print(f"Input b: {b}")
13
14# Create an AND gate and its complement (NAND)
15and_gate = aig.create_and(a, b)
16nand_gate = aig.create_not(and_gate)
17print(f"AND gate: {and_gate}")
18print(f"NAND gate: {nand_gate}")
19
20# Get the node from a signal
21node = aig.get_node(and_gate)
22print(f"Node of AND gate: {node}")
23
24# Check if a signal is complemented
25is_complemented = aig.is_complemented(nand_gate)
26print(f"Is NAND complemented? {is_complemented}")
Constant 0: Signal(0)
Input a: Signal(1)
Input b: Signal(2)
AND gate: Signal(3)
NAND gate: Signal(!3)
Node of AND gate: 3
Is NAND complemented? True

Exploring AIG Structure

You can iterate over all nodes in the AIG, or specific subsets like primary inputs or logic gates.

 1# Create a sample AIG
 2aig = Aig()
 3a = aig.create_pi()
 4b = aig.create_pi()
 5c = aig.create_pi()
 6f1 = aig.create_and(a, b)
 7f2 = aig.create_or(f1, c)
 8aig.create_po(f2)
 9
10# Iterate over all nodes in the AIG
11print("All nodes:")
12for node in aig.nodes():
13    print(f"  Node: {node}")
14
15# Iterate only over primary inputs
16print("\nPrimary inputs:")
17for pi in aig.pis():
18    print(f"  PI: {pi}")
19
20# Iterate only over logic gates
21print("\nLogic gates:")
22for gate in aig.gates():
23    print(f"  Gate: {gate}")
24
25# Iterate over the fanins of a node
26print("\nFanins of the OR gate:")
27or_node = aig.get_node(f2)
28for fanin in aig.fanins(or_node):
29    print(f"  Fanin: {fanin}")
All nodes:
  Node: 0
  Node: 1
  Node: 2
  Node: 3
  Node: 4
  Node: 5

Primary inputs:
  PI: 1
  PI: 2
  PI: 3

Logic gates:
  Gate: 4
  Gate: 5

Fanins of the OR gate:
  Fanin: Signal(!3)
  Fanin: Signal(!4)

Building Complex Functions

AIGs support a variety of logic functions beyond just AND gates. Internally, those are decomposed into multiple nodes.

 1aig = Aig()
 2a = aig.create_pi()
 3b = aig.create_pi()
 4c = aig.create_pi()
 5
 6# Create various logic functions
 7and_gate = aig.create_and(a, b)
 8or_gate = aig.create_or(a, b)
 9xor_gate = aig.create_xor(a, b)
10maj_gate = aig.create_maj(a, b, c)  # Majority function (a&b | a&c | b&c)
11ite_gate = aig.create_ite(a, b, c)  # If-then-else: a ? b : c
12
13# Add outputs
14aig.create_po(and_gate)
15aig.create_po(or_gate)
16aig.create_po(xor_gate)
17aig.create_po(maj_gate)
18aig.create_po(ite_gate)
19
20# Print statistics
21print(f"AIG Size: {aig.size}")
22print(f"Number of gates: {aig.num_gates}")
23print(f"Number of primary inputs: {aig.num_pis}")
24print(f"Number of primary outputs: {aig.num_pos}")
AIG Size: 13
Number of gates: 9
Number of primary inputs: 3
Number of primary outputs: 5

AIG Views

AIG views provide alternative representations of AIGs for specific tasks, such as depth computation or fanout analysis. These views can be layered on top of the original AIG, allowing you to work with the same underlying structure while adding additional functionality.

Network and Signal Names

The NamedAig class extends the standard AIG with the ability to assign names to the network itself, primary inputs, primary outputs, and internal signals. This is particularly useful for debugging, visualization, and interfacing with external tools that rely on human-readable signal names.

 1from aigverse.networks import NamedAig
 2
 3# Create a new named AIG (or construct from existing AIG)
 4named_aig = NamedAig()
 5
 6# Set the network name
 7named_aig.set_network_name("full_adder")
 8
 9# Create named primary inputs
10a = named_aig.create_pi("a")
11b = named_aig.create_pi("b")
12cin = named_aig.create_pi("cin")
13
14# Create full adder logic
15sum = named_aig.create_xor3(a, b, cin)
16carry = named_aig.create_maj(a, b, cin)
17
18# Assign names to internal signals
19named_aig.set_name(sum, "sum")
20named_aig.set_name(carry, "carry")
21
22# Create named primary outputs
23named_aig.create_po(sum, "sum_output")
24named_aig.create_po(carry, "carry_output")
25
26# Retrieve names
27print(f"Network name: {named_aig.get_network_name()}")
28print(f"PI names: {[named_aig.get_name(pi) for pi in named_aig.pis()]}")
29print(f"Signal name (sum): {named_aig.get_name(sum)}")
30print(f"Signal name (carry): {named_aig.get_name(carry)}")
31print(f"PO name at index 0: {named_aig.get_output_name(0)}")
32print(f"PO name at index 1: {named_aig.get_output_name(1)}")
33
34# Check if a signal has a name
35print(f"Has name: {named_aig.has_name(sum)}")
Network name: full_adder
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[6], line 28
     24 named_aig.create_po(carry, "carry_output")
     25 
     26 # Retrieve names
     27 print(f"Network name: {named_aig.get_network_name()}")
---> 28 print(f"PI names: {[named_aig.get_name(pi) for pi in named_aig.pis()]}")
     29 print(f"Signal name (sum): {named_aig.get_name(sum)}")
     30 print(f"Signal name (carry): {named_aig.get_name(carry)}")
     31 print(f"PO name at index 0: {named_aig.get_output_name(0)}")

TypeError: get_name(): incompatible function arguments. The following argument types are supported:
    1. get_name(self, s: aigverse.networks.AigSignal) -> str

Invoked with types: aigverse.networks.NamedAig, int

Named AIGs are automatically created when reading from file formats that contain naming information, e.g., Verilog and AIGER. See File I/O for more details.

Note

Names are tied to the specific AIG structure. When you apply optimization algorithms or structural modifications (such as cleanup_dangling()), the names will be lost as the return type will be downcast to Aig. If preserving names is important, consider reapplying them after optimization.

Depth and Level Computation

The depth of an AIG network represents the longest path from any input to any output, which corresponds to the critical path delay in a circuit. You can compute the depth and level of each node using the DepthAig class.

 1from aigverse.networks import DepthAig
 2
 3# Create a sample AIG
 4aig = Aig()
 5a = aig.create_pi()
 6b = aig.create_pi()
 7c = aig.create_pi()
 8f1 = aig.create_and(a, b)
 9f2 = aig.create_and(f1, c)
10aig.create_po(f2)
11
12# Create a depth view of the AIG
13depth_aig = DepthAig(aig)
14
15# Get the depth of the AIG
16print(f"Depth of AIG: {depth_aig.num_levels}")
17
18# Print the level of each node
19print("\nLevel of each node:")
20for node in aig.nodes():
21    print(f"  Node {node}: Level {depth_aig.level(node)}")
22
23# Check which nodes are on the critical path
24print("\nNodes on critical path:")
25for node in aig.nodes():
26    if depth_aig.is_on_critical_path(node):
27        print(f"  Node {node}")

AIGs with Fanout Information

Fanouts of AIG nodes can be collected using FanoutAig.

 1from aigverse.networks import FanoutAig
 2
 3# Create a sample AIG
 4aig = Aig()
 5x1 = aig.create_pi()
 6x2 = aig.create_pi()
 7x3 = aig.create_pi()
 8
 9# Create AND gates
10n4 = aig.create_and(x1, x2)
11n5 = aig.create_and(n4, x3)
12n6 = aig.create_and(n4, n5)
13# Create primary outputs
14aig.create_po(n6)
15
16fanout_aig = FanoutAig(aig)
17print("\nFanout nodes of n4:")
18for node in fanout_aig.fanouts(aig.get_node(n4)):
19    print(f"  Node {node}")

Sequential AIGs

SequentialAigs extend standard AIGs to include registers, which allow modeling sequential circuits with memory elements.

 1from aigverse.networks import SequentialAig
 2
 3# Create a sequential AIG
 4seq_aig = SequentialAig()
 5
 6# Create a primary input and a register output
 7x = seq_aig.create_pi()       # Regular PI
 8r = seq_aig.create_ro()       # Register output (sequential PI)
 9
10# Create logic
11f_and = seq_aig.create_and(x, r)   # AND gate
12
13# Create a primary output and a register input
14seq_aig.create_po(f_and)      # Regular PO
15seq_aig.create_ri(f_and)      # Register input (sequential PO)
16
17# Print information about the sequential AIG
18print(f"Number of PIs: {seq_aig.num_pis}")
19print(f"Number of POs: {seq_aig.num_pos}")
20print(f"Number of registers: {seq_aig.num_registers}")
21
22# Get the register associations (RI-RO pairs)
23print("\nRegister associations:")
24registers = seq_aig.registers()
25for reg in registers:
26    ri, ro = reg
27    print(f"  RI: {ri} -> RO: {ro}")

Note

When creating sequential AIGs, follow these rules:

  1. All register outputs (ROs) must be created after all primary inputs (PIs).

  2. All register inputs (RIs) must be created after all primary outputs (POs).

  3. As with regular AIGs, all PIs and ROs must be created before any logic gates.

File I/O

AIGs can be read from and written to various file formats.

 1from aigverse.io import write_aiger, write_verilog, write_dot, read_aiger_into_aig, read_verilog_into_aig, read_pla_into_aig
 2
 3# Create a sample AIG
 4aig = Aig()
 5a = aig.create_pi()
 6b = aig.create_pi()
 7f = aig.create_and(a, b)
 8aig.create_po(f)
 9
10# Write to AIGER format
11write_aiger(aig, "example.aig")
12
13# Write to Verilog format
14write_verilog(aig, "example.v")
15
16# Write to DOT format
17write_dot(aig, "example.dot")
18
19# Read from AIGER format
20read_aig = read_aiger_into_aig("example.aig")
21
22# Read from Verilog format
23read_verilog_aig = read_verilog_into_aig("example.v")
24
25# Read from PLA format
26read_pla_aig = read_pla_into_aig("example.pla")
27
28print(f"Original AIG size: {aig.size}")
29print(f"Read AIGER AIG size: {read_aig.size}")
30print(f"Read Verilog AIG size: {read_verilog_aig.size}")
31print(f"Read PLA AIG size: {read_pla_aig.size}")

Note

The gate-level Verilog file support constitutes a very small subset of the Verilog standard, similar in extent to what ABC supports. For more information, see the lorina parser used by this project.

Index Lists

Alternatively, index lists provide a compact, serialization-friendly representation of an AIG’s structure as a flat list of integers. This is useful for ML pipelines, dataset generation, or exporting AIGs for use in environments where fixed-size numeric arrays are required.

 1from aigverse.networks import AigIndexList
 2
 3# Create a sample AIG
 4aig = Aig()
 5a = aig.create_pi()
 6b = aig.create_pi()
 7c = aig.create_pi()
 8d = aig.create_pi()
 9t0 = aig.create_and(a, b)
10t1 = aig.create_and(~c, ~d)
11t2 = aig.create_xor(t0, t1)
12aig.create_po(t2)
13
14# Convert an AIG to an index list
15indices = aig.to_index_list()
16
17# Convert an index list back to an AIG
18aig2 = indices.to_aig()
19
20# Convert to a Python list
21indices = [int(i) for i in indices]
22print(indices)

The first three entries encode number of PIs, number of POs, and number of gates, respectively. In the example above, those are 4, 1, and 5. Successive pairs of indices refer to the fanins signals of nodes. Each fanin exists in two polarities: negated = odd index, and non-negated = even index. In the example, 2 and 4 refer to the non-negated signals originating from PIs 1 and 2. These form the first AND gate. The subsequent 7 and 9 are odd, hence, negated. If the first index is lower than the second, an AND gate is encoded. Otherwise, it is an XOR gate. The final indices of the list refer to the PO signals. It must be ensured that they match the encoded number of POs.

For more information on the index list format, see mockturtle’s documentation.

pickle Support

AIGs support Python’s pickle protocol, allowing you to serialize and deserialize AIG objects for persistent storage. This is useful for saving intermediate results, sharing AIGs between processes, quickly restoring previously computed networks, or integrating with Python-first data science and machine learning workflows.

1import pickle
2
3# Save AIG to a file using pickle
4with open("aig.pkl", "wb") as f:
5    pickle.dump(aig, f)
6
7# Load the AIG from the pickle file
8with open("aig.pkl", "rb") as f:
9    unpickled_aig = pickle.load(f)

You can also pickle and unpickle multiple AIGs at once by storing them in a tuple or list.

 1aig1 = Aig()
 2a1 = aig1.create_pi()
 3b1 = aig1.create_pi()
 4f1 = aig1.create_and(a1, b1)
 5aig1.create_po(f1)
 6
 7aig2 = Aig()
 8a2 = aig2.create_pi()
 9b2 = aig2.create_pi()
10f2 = aig2.create_or(a2, b2)
11aig2.create_po(f2)
12
13# Pickle both AIGs together
14with open("aigs.pkl", "wb") as f:
15    pickle.dump((aig1, aig2), f)
16
17# Unpickle them
18with open("aigs.pkl", "rb") as f:
19    unpickled_aig1, unpickled_aig2 = pickle.load(f)