ABC Integration

Beyond the built-in optimization algorithms, aigverse can hand a network to ABC and read the result back.

Important

ABC runs as a separate process and is not bundled with aigverse. Any ABC build works, whether from berkeley-abc/abc, a distribution package, or one shipped with Yosys or the OSS CAD Suite. See Installation → ABC Integration for how to obtain one and how to point aigverse at it.

from aigverse import abc
from aigverse.generators import carry_lookahead_adder
from aigverse.algorithms import equivalence_checking

aig = carry_lookahead_adder(16)
optimized = abc.resyn2(aig)

print(f"{aig.num_gates} -> {optimized.num_gates} AND gates")
print(f"Equivalent: {equivalence_checking(aig, optimized)}")
186 -> 133 AND gates
Equivalent: True

Every script is available the same way; they differ in how hard they try:

for script in ("resyn", "resyn2", "resyn2rs", "compress2rs", "dc2"):
    result = getattr(abc, script)(aig)
    print(f"{script:12s} {aig.num_gates} -> {result.num_gates} AND gates")
resyn        186 -> 149 AND gates
resyn2       186 -> 133 AND gates
resyn2rs     186 -> 132 AND gates
compress2rs  186 -> 115 AND gates
dc2          186 -> 124 AND gates

Named scripts

The canonical ABC scripts are available as functions: resyn, resyn2, resyn3, compress, compress2, resyn2rs, compress2rs, and dc2.

Ordinarily these are aliases defined in ABC’s abc.rc resource file rather than builtin commands, which makes them unreliable to call: they are unavailable on an installation where no abc.rc is found, and they silently mean something else on one where a user has customized that file. aigverse sidesteps both problems by shipping the expansions itself and running ABC with -s, so a script means the same thing on every machine. The exact expansion of each script is available in SCRIPTS.

Individual commands

The four commands the scripts are built from are exposed as well, so a schedule can be composed from Python — useful when the sequence itself is what is being searched over:

result = aig
for step in (abc.balance, abc.rewrite, abc.refactor, abc.resub):
    result = step(result)

print(f"{aig.num_gates} -> {result.num_gates} AND gates")
186 -> 142 AND gates

Their options are exposed as keyword arguments rather than as ABC switches: abc.rewrite(aig, zero_cost=True) or abc.resub(aig, max_cut_size=12).

orchestrate() is a fifth: instead of running rewriting, refactoring and resubstitution one after another, it interleaves them and picks per node which to apply — a whole schedule in a single command.

Note

ABC’s -l switch toggles a default of “preserve the number of levels”, so passing it turns level preservation off. These wrappers therefore take preserve_levels, which says what it means. The compress scripts are the -l variants of the resyn ones, and hence the ones that trade depth for size.

Watch out for orchestrate, where ABC flips the convention: it enables zero-cost replacements by default, unlike the standalone rewrite and refactor. The wrappers paper over that with zero_cost_rewrite / zero_cost_refactor, which mean what they say in both places.

Arbitrary commands

Any ABC command string can be run directly. The read and write steps are added automatically, and whatever AIG is current when the script ends is what comes back:

result = abc.run_script(aig, "balance; rewrite -z; refactor", timeout=60)
print(f"{aig.num_gates} -> {result.num_gates} AND gates")
186 -> 142 AND gates

Set use_init_file=True to let ABC load your own abc.rc, which makes your personal aliases available.

The two network stores

ABC keeps two independent network stores, and a command only ever sees its own. By default aigverse loads the network with read_aiger into the classic store, where the commands without a & prefix operate — balance, rewrite, refactor, resub, and therefore every named script above.

The &-prefixed commands of ABC9 work on a separate store, the GIA, which is empty in that mode. Pass gia=True to load the network there directly:

result = abc.run_script(aig, "&syn2", gia=True)
print(f"{aig.num_gates} -> {result.num_gates} AND gates")
186 -> 183 AND gates

The two stores can also be bridged inside a single script with &get and &put, but those do not carry I/O names across, whereas gia=True does.

The gia namespace

The & commands have wrappers of their own in the gia namespace, which set gia=True for you. The namespace mirrors ABC’s own prefix, so the two spaces stay visibly distinct at the call site:

print(f"abc.dc2:     {abc.dc2(aig).num_gates} gates")       # ABC's `dc2`
print(f"abc.gia.dc2: {abc.gia.dc2(aig).num_gates} gates")   # ABC's `&dc2`
abc.dc2:     124 gates
abc.gia.dc2: 134 gates

It holds balance() (&b), resub(), dc2(), syn2(), syn3(), syn4() and fraig(), plus the high-effort searches below, cec(), stats(), and run_script() for anything not wrapped.

The &-space is not a mirror of the classic set — there is no &rewrite and no &refactor, with &dc2 standing in for both. What it offers instead is a different strategy: these commands map to LUTs internally and unmap again, restructuring far more aggressively than the classic commands, which only rewrite locally.

Whether that pays off is strongly design-dependent, and it is worth seeing that rather than taking it on faith:

from aigverse.generators import ripple_carry_multiplier
from aigverse.networks import DepthAig

def report(label, ntk):
    print(f"  {label:10s} {ntk.num_gates:4d} gates  {DepthAig(ntk).num_levels:3d} levels")

for name, design in [("multiplier", ripple_carry_multiplier(4)), ("adder", aig)]:
    print(name)
    report("original", design)
    report("resyn2", abc.resyn2(design))
    report("gia.syn4", abc.gia.syn4(design))
multiplier
  original     84 gates   16 levels
  resyn2       84 gates   16 levels
  gia.syn4    168 gates   13 levels
adder
  original    186 gates   29 levels
  resyn2      133 gates   20 levels
  gia.syn4    183 gates   32 levels

On the multiplier, gia.syn4 buys depth that resyn2 cannot reach, and pays for it in area. On the adder it does neither — it adds gates and leaves the depth alone, while resyn2 wins outright. Neither family dominates, so measure on your own designs instead of assuming.

fraig() is the odd one out and worth knowing about: it is combinational SAT sweeping, which merges nodes that are functionally equivalent but structurally different. No amount of rewriting finds those, which makes it a useful pass between two structural scripts that each introduced their own duplicates.

Equivalence checking

cec() returns a verdict rather than a network, wrapping ABC’s &cec. It is a genuinely independent second opinion on equivalence_checking(): two different implementations, so a disagreement means one of them has a bug worth finding.

optimized = abc.compress2rs(aig)
print(f"ABC says:      {abc.gia.cec(aig, optimized)}")
print(f"aigverse says: {equivalence_checking(aig, optimized)}")
ABC says:      CecStatus.EQUIVALENT
aigverse says: True

ABC matches inputs by position rather than by name, so the two networks must have the same interface.

&cec is incomplete under a resource limit, so there are four outcomes and not two, and the result is a CecStatus rather than a bool:

EQUIVALENT

ABC proved the networks equal

NOT_EQUIVALENT

ABC found a counterexample

UNDECIDED

ABC ran out of its own budget without deciding

TIMEOUT

ABC did not finish within timeout

The enum deliberately refuses to be truth-tested, because if abc.gia.cec(a, b): would read as “equivalent” while quietly also firing for UNDECIDED — and “not proven equal” is not “proven different”. Compare against a member instead:

if abc.gia.cec(aig, optimized) is abc.CecStatus.EQUIVALENT:
    print("proven equivalent")
proven equivalent

What ABC thinks of a network

stats() and stats() run ABC’s print_stats and &ps and return an AbcStats instead of a line of text:

print(abc.stats(aig))
print(abc.gia.stats(aig))
AbcStats(num_pis=32, num_pos=17, num_gates=182, num_levels=29, num_registers=0, average_level=None, memory_mb=None, raw='in                            : i/o =   32/   17  lat =    0  and =    182  lev = 29')
AbcStats(num_pis=32, num_pos=17, num_gates=182, num_levels=29, num_registers=None, average_level=15.59, memory_mb=0.0, raw='in       : i/o =     32/     17  and =     182  lev =   29 (15.59)  mem = 0.00 MB')

The two stores report slightly different things — &ps adds an average level and a memory figure, and spells the register count ff where print_stats spells it lat — and both keep the original line in raw.

Warning

These are ABC’s counts, not aigverse’s, and they can differ for the very same network. ABC structurally hashes as it reads, so any structural redundancy the network carried is gone before print_stats ever sees it:

Nothing was optimized in between — the gap is the strashing. Use aig.num_gates to describe the network you hold and stats() to describe what ABC worked on, and do not mix the two in one benchmark table.

Type preservation and limitations

The returned network has the same type as the input: an Aig yields an Aig, and a NamedAig yields a NamedAig with its input and output names carried through ABC.

Warning

The bridge transfers AIGs and nothing else, so technology mapping and \(k\)-LUT mapping are out of reach. A command such as map or if -K 6 runs happily inside ABC, but the mapped netlist it leaves behind cannot be written as AIGER, and the call raises rather than quietly handing back something unmapped. Mapping support needs cell and \(k\)-LUT network types in aigverse first.

Warning

SequentialAig is rejected with a TypeError rather than being silently flattened into extra primary inputs and outputs. Sequential support requires writing registers to AIGER and reading ABC’s sequential output back, neither of which is available yet.

Each call starts an ABC process and transfers the network through temporary AIGER files, which costs roughly 20 ms of overhead per call — negligible for batch work, but worth keeping in mind in a tight optimization loop.

When things go wrong

ABC exits with status 0 even for an unknown command or an unreadable file, and writes everything to standard output — its standard error stays empty. Failure detection therefore scans the output for known error markers and checks that a usable network came back, rather than trusting the exit status:

try:
    abc.run_script(aig, "no_such_command")
except abc.AbcExecutionError as error:
    print(error)
ABC reported an error: ** cmd error: unknown command 'no_such_command'
  binary:  /home/docs/abc/abc
  command: read_aiger in.aig; no_such_command; write_aiger -s out.aig
  output:
** cmd error: unknown command 'no_such_command'
(this is likely caused by using an alias defined in "abc.rc"
without having this file in the current or parent directory)

Everything needed to reproduce the call is attached to the exception as binary, command, and output, so a failure can be replayed by hand.

The hierarchy is small: AbcNotFoundError when no usable executable could be located, AbcTimeoutError when ABC outlived its timeout, and AbcExecutionError for everything else ABC did wrong. All three derive from AbcError.

Options are validated in Python before ABC is started, so a value ABC would reject comes back as a ValueError naming the keyword you wrote rather than as an ABC message:

for call in (
    lambda: abc.refactor(aig, max_support=16),
    lambda: abc.gia.deepsyn(aig, seed=101),
):
    try:
        call()
    except ValueError as error:
        print(error)
max_support must be between 1 and 15, got 16
seed must be between 0 and 100, got 101

Note

ABC is the authority on those ranges and aigverse pins no ABC version. The ranges ABC prints in its own -h output are checked against the installed binary by the test suite, so an upstream change is caught rather than guessed at. A few bounds ABC enforces without documenting — refactor refuses a support above 15 while printing no range at all — are recorded with the evidence for them.

Keeping the scripts in sync

The shipped expansions are checked against the abc.rc of the ABC the test suite runs against, so a change to an alias upstream is caught when the pinned ABC revision is bumped rather than showing up in your results. Point AIGVERSE_ABC_RC at an abc.rc to run that check locally.

Using your own aliases

set_abc_rc() registers an ABC resource file that is loaded before every command, which makes your own aliases available without giving up reproducibility: the file you name is the only one ABC reads.

from aigverse import abc

abc.set_abc_rc("/path/to/abc.rc")
result = abc.run_script(aig, "resyn2")  # now resolves as an alias

Note

That snippet is not executed here, since it needs a resource file to point at.

Set AIGVERSE_ABC_RC to configure the same thing from the environment, or pass None to clear it.