You do not fully understand a timing report until you have built a toy that produces one. This engine takes a graph with delays, a clock period and a setup time, and prints the worst path and per-endpoint slack. It deliberately omits everything else (slews, corners, hold) so the core algorithm is visible.
from collections import defaultdict
def analyse(edges, starts, ends, period, setup, start_arrival=0.0):
succ, pred = defaultdict(list), defaultdict(list)
nodes = set(starts) | set(ends)
for (a, b), w in edges.items():
succ[a].append((b, w)); pred[b].append((a, w)); nodes |= {a, b}
# topological order (Kahn)
indeg = {n: len(pred[n]) for n in nodes}
order, ready = [], [n for n in nodes if indeg[n] == 0]
while ready:
n = ready.pop(); order.append(n)
for m, _ in succ[n]:
indeg[m] -= 1
if indeg[m] == 0: ready.append(m)
arr, via = {}, {}
for n in order:
if n in starts: arr[n] = start_arrival
elif pred[n]:
arr[n], via[n] = max((arr[p] + w, p) for p, w in pred[n])
req = {e: period - setup for e in ends}
for n in reversed(order):
if n not in req and succ[n]:
req[n] = min(req[m] - w for m, w in succ[n])
slack = {n: req[n] - arr[n] for n in order if n in req and n in arr}
worst_end = min(ends, key=lambda e: slack[e])
path, n = [worst_end], worst_end
while n in via: n = via[n]; path.append(n)
return arr, slack, worst_end, path[::-1]
edges = {("ff1","u1"):0.40, ("u1","u2"):0.30, ("u2","ff2"):0.50, ("u1","u3"):0.55, ("u3","ff3"):0.60, ("ff4","u3"):0.20}
ends = {"ff2", "ff3"}
arr, slack, worst, path = analyse(edges, starts={"ff1","ff4"}, ends=ends, period=1.5, setup=0.10)
assert abs(arr["ff3"] - 1.55) < 1e-9 and abs(slack["ff3"] + 0.15) < 1e-9
assert worst == "ff3" and path == ["ff1", "u1", "u3", "ff3"]
assert abs(slack["ff2"] - 0.20) < 1e-9
print("PASS: worst endpoint", worst, "slack", round(slack[worst], 2), "path", " -> ".join(path), "per-endpoint slack", {e: round(slack[e], 2) for e in sorted(ends)})With a 1.5 ns period and 0.1 ns setup the required time is 1.40 ns. Endpoint ff2 arrives at 1.20 (slack +0.20); ff3 arrives at 1.55 through ff1-u1-u3 (slack -0.15), because the route from ff4 is shorter (0.80). The engine reports ff3 as worst and recovers the path by following the worst predecessor at each node. It reproduces a report only when launch and capture latencies and uncertainty are folded into its two parameters (start_arrival and period - setup); it cannot model different clocks per endpoint, and a node with no successor needs handling. Everything a real tool adds is refinement on top of this skeleton: delays that depend on slew and load, separate early and late values, clock paths, derates, exceptions.
Change the period to 1.7 ns and watch both slacks become positive.
Add a second edge into u2 with a longer delay and see the arrival and the worst path change.
Compute a hold-style pass by replacing max with min and the required time by a hold bound.
For setup the latest arriving signal determines when the data is stable, so the arrival at a node is the maximum over all routes into it. For hold the earliest arrival matters, so a hold pass would use min.