The corpus — measured data
Every model is fitted from measurements, and every model stores them the same way:
<platform.dir>/models/<name>/corpus.csv
One file, one shape, both axes. What differs is only how the rows get there.
The format
One row per measurement. Columns are:
| column group | what |
|---|---|
| one per hardware parameter | the design’s resolved HwParam values — width, ntap, dwid … |
| one per structural fact | anything else that shaped the circuit — xbar0_lanes, mem0_banks |
| one per runtime parameter | workload inputs, where the axis has them — nwords, num_trans |
| one per target | what was measured — lut, ff, dsp, bram … or residual |
measured_at |
an ISO timestamp, stamped on append |
dwid,vlen,lut,ff,dsp,bram,measured_at
64,4096,1370,599,4,4,2026-08-03T11:04:22
128,4096,2622,1399,8,8,2026-08-03T11:05:01
256,4096,6956,3622,16,16,2026-08-03T11:05:44
That is the whole contract. Columns are not fixed in advance: a model declares the order it prefers and any extra column is kept, so a corpus can carry provenance a particular axis finds useful without the format changing.
The parameter columns are exactly what get_params
returns. That is what lets one corpus serve fitting and prediction without a translation step in
between — and what makes a corpus inspectable: every row says what was built and what it cost.
Raw facts, never derived ones
A row records what get_params extracted. It does not record what
transform computed from it, and that separation is deliberate.
transform encodes a modelling claim — that LUTs go as lanes²·log2(lanes), that area goes as
a·b·c. Claims get revised. If the corpus stored the derived term, a revision would strand every
measurement, because the inputs it was computed from would be gone.
Record
a,b,c— neverarea = a·b·c. The derived column is cheap to recompute and impossible to invert.
So a column belongs in a row if it is a fact about what was built, whether or not it is a
HwParam. VitisResourceModel records its declared structure this way:
dwid,vlen,mult0_count,mult0_operand_bits,mem0_banks,mem0_depth,mem0_elem_bits,xbar0_lanes,lut,ff,dsp,bram
128,4096,8,16,8,512,16,8,2622,1397,8,8
xbar0_lanes=8 is the declaration; xbar_sw=64 is the claim about it, and is derived at fit time.
Revise the cost dictionary and this row still fits.
The guarantee that keeps this honest is structural rather than editorial: transform receives the
parameter mapping and never the component, so a model cannot predict from anything a row does not
contain.
It is derived, never authoritative
corpus.csv is a view of measurements that live elsewhere:
raw tier ──(axis-specific)──> corpus.csv ──(fit)──> params.json
| axis | raw tier | who reads it |
|---|---|---|
| timing | rtl/<run_id>/firings.csv and pysim/<run_id>/firings.csv — one directory per run |
the timing model joins the two on the feature point |
| resources | modules/<key>/resource/records.jsonl in a record store |
the resource model flattens the records it is keyed for |
The raw tier is the ground truth. It carries provenance — which synthesis, which part, which tool, how long it cost, and a signature of the module it describes — and that provenance is checked on read.
Why derived rather than maintained. A corpus updated incrementally can drift: one process files a measurement, another holds a stale table, and nothing notices. Regenerating from the raw tier makes that impossible — the corpus is always exactly what was measured, deleting it is never data loss, and the cost is parsing a few dozen reports.
The corollary is that you should never hand-edit
corpus.csv. Correct the raw tier and regenerate.
Generating it is the one part that cannot be shared, because only the axis knows how to read its own
raw tier: a csynth.xml attribution on one side, a firings join on the other.
Reading it
model.corpus_df # -> pandas.DataFrame, for analysis
model.corpus_markdown() # -> a markdown table, for a report or an AI context
corpus_df is the working form — filter, group and plot with native pandas. corpus_markdown exists
because a corpus is evidence: the numbers behind a model belong in the document that makes a claim
about it, and in the context window of anything reasoning about the design.
Fitting reads it by default
model.fit() # reads corpus.csv — the normal path
model.fit(samples) # explicit override, for a test or a one-off
Passing samples is deliberately not the default. A fit that quietly bypassed the recorded corpus would produce a model whose provenance nobody can reconstruct — the parameters would exist with no answer to “fitted on what?”.
How many rows do you need?
That is a coverage question, and it is what separates the model kinds:
| model | rows needed |
|---|---|
| a memorizing lookup | one per configuration you will ever query — it does not generalize |
| a fitted model | enough to pin its coefficients, then it covers the space between them |
The trade is bias against coverage. A lookup assumes nothing about the shape of the function, so
nothing it assumes can be wrong — but it answers only where you measured. A fitted model commits to a
shape and, in exchange, answers between the points. examples/vecmult fits four coefficients from 15
rows and predicts anywhere in the region; a lookup over the same space would need every point.
Whichever number you land on, the way to produce those rows is a sweep: a grid of design points run through the build DAG, each filing what its steps measured. Coverage is then the grid you chose, which is the point — it is written down rather than accumulated by hand.
The trap: rows keyed to a module that was never wired
A resource row is keyed by the module’s elaborated structure, and a module with ports has no settled structure until those ports are wired into a harness.
Building rows from components elaborated standalone gives keys no real composite ever produces. Every later prediction then misses — each module reporting
UNCALIBRATED, contributing zero, and making the design read as cheaper than it is.It fails silently, because a missing key looks exactly like a configuration you never measured.
Generate the corpus from the raw tier (whose records were filed from wired instances), or take components out of an assembled design.
Status
Built and in use: the format (
CalibDataFrame), thecorpus_df/corpus_markdownaccessors,fit(data=None)reading the corpus by default on both axes, theget_params/transformsplit, and both generators — timing regenerates from its raw runs on every fit, andcorpus_from_records(store, cls_name=...)reduces the resource record store.Not yet wired: nothing calls
corpus_from_recordsautomatically on the resource side — a model still receives explicit samples. Makingfit()reach for it is tracked inplans/harmonize_calib.md.
Next
CalibDataFrame— the object that implements this format.- Confidence — what a model says about a prediction once it is fitted.