A delay table answers one question: this cell, this slew, this load, how long? Real arrivals land between tabulated points, so the tool interpolates. This lesson works the interpolation by hand on the real NAND2 table, then a runnable Python block replays it. Every value below is copied from values("0.08,0.1","0.11,0.14"), slew rows first.
Each table has two indexes: input slew along the rows and output load along the columns. The teaching tables use 0.01 and 0.1 on both axes, in nanoseconds for slew and picofarads for load by the file header. A lookup inside the rectangle blends the four corners. A lookup outside it extrapolates: the runnable program below extends its bilinear weights past the edges, which is bilinear extrapolation, linear on each axis, the same math STA tools use. The real difference is what happens next: STA flags the max_transition and max_capacitance violation instead of trusting the number. Synthesis repairs slew and load with buffering and sizing so that every lookup lands back inside characterised ranges. Real Liberty tables run about seven by seven; these teaching tables stay two by two so the hand arithmetic fits on the page.
Input slew (rows) / output load (cols) | 0.01 | 0.1 |
|---|---|---|
0.01 | 0.08 | 0.10 |
0.1 | 0.11 | 0.14 |
Fix the orientation first: slew 0.1 with load 0.01 is 0.11; slew 0.01 with load 0.1 is 0.10. The transposed reading swapped those two, and every midpoint built on them moved.
Bilinear interpolation weights the four corners by distance. The fractions fx and fy place the point: fx runs 0 to 1 across the slew rows, fy across the load columns. Each corner weight is one term per axis multiplied, the four always sum to one, and tabulated corners return their own entries exactly.
The delay is the weighted sum. Work one edge first: at slew 0.055 and load 0.01, fx is a half and fy is 0, so the delay is half of 0.08 plus half of 0.11, which is 0.095 ns. That edge midpoint blends the two first-column corners at equal weight, one from each slew row.
real NAND2 rise table (ns): T(0.01, 0.01) = 0.08, T(0.1, 0.01) = 0.11
T(0.01, 0.1) = 0.10, T(0.1, 0.1) = 0.14
edge point: slew = 0.055, load = 0.01
fx = (0.055 - 0.01) / (0.1 - 0.01) = 0.045 / 0.09 = 0.5; fy = 0
delay = 0.5 x 0.08 + 0.5 x 0.11 = 0.04 + 0.055 = 0.095 ns
interior point: slew = 0.04, load = 0.04
fx = (0.04 - 0.01) / 0.09 = 0.03 / 0.09 = 1/3; fy = 1/3
delay = (4/9) x 0.08 + (2/9) x 0.11 + (2/9) x 0.10 + (1/9) x 0.14
= 0.0355556 + 0.0244444 + 0.0222222 + 0.0155556
= 0.0977778 ns = 22/225 nsThe program below embeds the real table as data and replays the hand arithmetic. Its five self-checks cover a corner, both edge midpoints, the interior point, and one point outside the table, passing only when every check lands within one part in a billion.
import sys
# NAND2 cell_rise from course.lib, copied exactly:
# index_1 (input_net_transition, rows): 0.01, 0.1
# index_2 (total_output_net_capacitance, columns): 0.01, 0.1
# values("0.08,0.1","0.11,0.14")
SLEW = (0.01, 0.1)
LOAD = (0.01, 0.1)
TABLE = ((0.08, 0.10), (0.11, 0.14))
def lookup(slew, load):
(x0, x1), (y0, y1) = SLEW, LOAD
fx = (slew - x0) / (x1 - x0)
fy = (load - y0) / (y1 - y0)
(t00, t01), (t10, t11) = TABLE
return ((1.0 - fx) * (1.0 - fy) * t00 + fx * (1.0 - fy) * t10
+ (1.0 - fx) * fy * t01 + fx * fy * t11)
def close(got, want):
return abs(got - want) < 1e-9
checks = [
(lookup(0.01, 0.01), 0.08),
(lookup(0.01, 0.055), 0.09),
(lookup(0.055, 0.01), 0.095),
(lookup(0.04, 0.04), 22.0 / 225.0),
(lookup(0.2, 0.05), 13.09 / 81.0),
]
for i in range(len(checks)):
got, want = checks[i]
if not close(got, want):
print("FAIL: check " + str(i + 1) + " got " + str(got) + " want " + str(want))
sys.exit(1)
print("PASS: bilinear lookup matches hand arithmetic on 5 checks")The five checks climb in difficulty. The corner at slew 0.01 and load 0.01 must return 0.08 exactly, proving the copied data. The next two pin one fraction at a time: 0.09 across the fast-slew row, half of 0.08 plus half of 0.11 down the first load column. The fourth pins both fractions at once against 22/225 ns. The last check feeds slew 0.2 and expects the quotient the next section works by hand, pinning the extrapolation arithmetic.
Now feed the lookup slew 0.2, twice the slowest row of 0.1 (the fastest row is 0.01), at load 0.05. The fractions leave the unit square: fx is 19/9 and fy is 4/9. The weights still sum to one, but two turn negative, so corners subtract instead of blending. The program checks the result against 13.09/81, about 0.1616 ns.
outside point: slew = 0.2, load = 0.05
fx = (0.2 - 0.01) / 0.09 = 19/9; fy = (0.05 - 0.01) / 0.09 = 4/9
weights: -50/81, 95/81, -40/81, 76/81 (still sum to 1)
delay = (-50 x 0.08 + 95 x 0.11 - 40 x 0.10 + 76 x 0.14) / 81
= (-4.0 + 10.45 - 4.0 + 10.64) / 81
= 13.09 / 81 = 0.1616 ns (extrapolated, not characterised)Treat that number as a warning, not a measurement. Nothing characterises slew 0.2, so repair the slew first — buffer the net, upsize the driver — until every lookup lands back inside characterised ranges. Plausible extrapolation is still a guess with a formula.
13.09/81 ns from the hand arithmetic the program checks. Do not trust it: slew 0.2 sits past the slowest tabulated row at 0.1, two weights turn negative, and no characterised data backs the answer. Fix the slew until the lookup stays inside the table.
fx is 0.5 and fy is 0. The delay is 0.5 x 0.08 + 0.5 x 0.11, the edge midpoint the program checks as its third self-check.
0.09 ns: fx is 0 and fy is 0.5, so it halves the two first-row corners 0.08 and 0.10. The program checks it as its second self-check.
No Yosys run on this page: table values are copied exactly from scripts/tools/synth-libs/course.lib, and the bilinear lookup is checked by the stdlib Python block shown (five self-checks: a corner, both edge midpoints, an interior point, and an extrapolation). The chart midpoint is hand arithmetic verified by that block. The seven-by-seven real-table size is a stated industry fact, not a recording.