A worked example
A self-contained walk through the calibration loop on a tiny synthetic dataset. Everything here runs against the real package.
1. Collect the corpus
Suppose a kernel’s measured cycle count is affine in an effective trip count m (a block-processing
kernel: cycles = latency + ii·m). Each cosim run is one datapoint:
from waveflow.calib import CalibDataFrame, LinCalibModel
db = CalibDataFrame(columns=["m", "cycles"])
db.extend([
{"m": 1, "cycles": 52},
{"m": 2, "cycles": 64},
{"m": 4, "cycles": 88},
{"m": 8, "cycles": 136},
])
print(db.df) # a pandas DataFrame (with a measured_at column)
These
(m, cycles)numbers are synthetic — chosen to make the fit easy to follow. In a real calibration you don’t type the cycle counts; you capture them from cosim: instrument the design, run an RTL co-simulation at each size, and read the cycle count off the VCD. That end-to-end loop — where the datapoints come from — is Instrumenting a calibration.
2. Fit, inspect, predict, score
model = LinCalibModel(basis=["m"], target="cycles").fit(db)
print(model.coeffs) # {'m': 12.0, 'intercept': 40.0} -> ii = 12, latency = 40
print(model.predict({"m": 3})) # 76.0
print(model.score(db)) # 1.0 (R² — exact line here)
coeffs recovers the physical numbers: the slope is the initiation interval ii, the intercept is
the latency (see Fitting a timing model).
3. Hold a point out (does it generalize?)
score on the training data only tells you the fit interpolates its own points. To know the model
generalizes, fit on a subset and report the held-out residual:
train = db.df[db.df.m != 4] # native pandas on .df
test = db.df[db.df.m == 4]
report = LinCalibModel(basis=["m"], target="cycles").holdout_report(train, test)
print(report)
# {'target': 'cycles', 'r2_train': 1.0,
# 'test': [{'pred': 88.0, 'actual': 88.0, 'rel_err': 0.0}], 'max_rel_err': 0.0}
4. Plot (actual vs. fitted)
import matplotlib.pyplot as plt
ax = model.plot(db, x_name="m") # scatter of actuals + the fitted line
plt.show()
plot returns the matplotlib Axes, so you can pass your own ax= to overlay several targets.
5. A saturating curve with InterpCalibModel
Not every quantity is a line. A per-row pipeline depth grows with the row length and then saturates
— a line or a sqrt would both be wrong. Carry the measurement as a calibrated lookup instead:
from waveflow.calib import InterpCalibModel
depth = CalibDataFrame(columns=["n_col", "row_depth"])
depth.extend([
{"n_col": 64, "row_depth": 70.0},
{"n_col": 256, "row_depth": 260.0},
{"n_col": 1024, "row_depth": 268.0}, # saturated
])
g = InterpCalibModel(basis=["n_col"], target="row_depth").fit(depth)
print(g.predict({"n_col": 128})) # ≈ 133.3 (interpolated between 70 and 260)
print(g.predict({"n_col": 4096})) # 268.0 (clamped past the last sample — the saturation)
print(g.samples) # {'feature': 'n_col', 'x': [...], 'y': [...]}
That is exactly how the matrix-LT FIR carries its one fitted term (row_depth(n_col)) — see the
double-buffered worked example.
See also
- The corpus —
CalibDataFrame— building and persisting the measurement table. - Models — the full method reference for
CalibModel/LinCalibModel/InterpCalibModel. - Calibration — the overall workflow.
examples/rowwise_fir/fir_calibrate.py— a real cosim-sweep calibration.