A tree has no shared logic: each node feeds exactly one parent. Real networks reuse results, so one node can feed several parents.
Covering a shared node once per parent double-counts it. Mapping therefore works on the DAG directly, using cuts.
A cut of a node is a set of leaves that separates the node from the primary inputs. Every path from an input to the node passes through a leaf.
A cut is k-feasible when it holds at most k leaves. The mapper enumerates the k-feasible cuts of every node, then matches library cells against them.
Take n1 = a AND b, n2 = c AND d, y = n1 OR n2. The table lists the cuts of y as k grows.
k | Cuts of y |
|---|---|
1 | {y} |
2 | {y}, {n1, n2} |
3 | plus {a, b, n2}, {n1, c, d} |
4 | plus {a, b, c, d} |
The trivial cut {y} is always there. Larger k adds choices but the count grows fast, so tools keep k small.
The program below covers a small tree four ways and proves the minimum by exhaustive search. Its subject is m1 = NOT (a AND b), m2 = NOT (c AND d), f = m1 AND m2, with five single cells to choose from.
import sys
# Tree-covering toy for technology mapping.
# Subject: m1 = NOT (a AND b), m2 = NOT (c AND d), f = m1 AND m2.
# Each node takes one cell; NAND2 absorbs an inverter over an AND.
A = ("PI", "a")
B = ("PI", "b")
C = ("PI", "c")
D = ("PI", "d")
def and_node(x, y):
return ("AND", x, y)
def inv_node(x):
return ("INV", x)
M1 = inv_node(and_node(A, B))
M2 = inv_node(and_node(C, D))
F = and_node(M1, M2)
PH0 = ("PH", 0)
PH1 = ("PH", 1)
PATTERNS = [
("INV", 1, ("INV", PH0)),
("NAND2", 2, ("INV", ("AND", PH0, PH1))),
("AND2", 3, ("AND", PH0, PH1)),
("OR2", 3, ("OR", PH0, PH1)),
("NOR2", 2, ("INV", ("OR", PH0, PH1))),
]
def match(shape, node, env):
if shape[0] == "PH":
idx = shape[1]
if idx in env:
return env[idx] == node
env[idx] = node
return True
if shape[0] != node[0]:
return False
if len(shape) != len(node):
return False
for i in range(1, len(shape)):
if not match(shape[i], node[i], env):
return False
return True
def pin_cover(node):
return (0, node[1], ("pin", node[1]))
def covers(node):
if node[0] == "PI":
return [pin_cover(node)]
out = []
for (cell, cost, shape) in PATTERNS:
env = {}
if not match(shape, node, env):
continue
subs = []
for i in sorted(env.keys()):
bound = env[i]
if bound[0] == "PI":
subs.append([pin_cover(bound)])
else:
subs.append(covers(bound))
combos = [[]]
for s in subs:
grown = []
for prefix in combos:
for item in s:
grown.append(prefix + [item])
combos = grown
for combo in combos:
total = cost
parts = []
kids = []
for (sc, sd, st) in combo:
total = total + sc
parts.append(sd)
kids.append(st)
desc = cell + "(" + ",".join(parts) + ")"
out.append((total, desc, (cell,) + tuple(kids)))
return out
def run(cover, vals):
kind = cover[0]
if kind == "pin":
return vals[cover[1]]
if kind == "INV":
return 1 - run(cover[1], vals)
if kind == "NAND2":
return 1 - (run(cover[1], vals) & run(cover[2], vals))
if kind == "AND2":
return run(cover[1], vals) & run(cover[2], vals)
if kind == "OR2":
return run(cover[1], vals) | run(cover[2], vals)
if kind == "NOR2":
return 1 - (run(cover[1], vals) | run(cover[2], vals))
return None
def spec(vals):
return (1 - (vals["a"] & vals["b"])) & (1 - (vals["c"] & vals["d"]))
found = covers(F)
areas = sorted([a for (a, d, t) in found])
best = min(found, key=lambda c: c[0])
if len(found) != 4:
print("FAIL: wanted 4 covers, got " + str(len(found)))
sys.exit(1)
if areas != [7, 9, 9, 11]:
print("FAIL: wanted areas 7 9 9 11, got " + " ".join([str(a) for a in areas]))
sys.exit(1)
if best[0] != 7 or best[1] != "AND2(NAND2(a,b),NAND2(c,d))":
print("FAIL: wrong minimum cover " + best[1] + " " + str(best[0]))
sys.exit(1)
for (a, d, t) in found:
for n in range(16):
vals = {"a": (n >> 0) & 1, "b": (n >> 1) & 1, "c": (n >> 2) & 1, "d": (n >> 3) & 1}
if run(t, vals) != spec(vals):
print("FAIL: cover " + d + " differs on input " + str(n))
sys.exit(1)
print("PASS: 4 covers with areas 7 9 9 11, minimum area 7 by exhaustive search")Four covers exist because each of m1 and m2 has two choices: a NAND2 (cost 2) or an AND2 plus a discrete inverter (cost 4). The root and the AND leaves match one cell each, so 2 times 2 gives 4.
Only m1 and m2 offer a choice each: a NAND2 (cost 2) or an AND2 plus a discrete inverter (cost 4). Two times two gives four covers.
It would add the cost of m1 twice, once per parent. That double count is why real mappers use cuts on the DAG.