Directory layout
Inside a platform
Each platform is stored in a directory with structure:
<platform>/
platform.json identity — part, clk_freq_hz, [res_types]
mm_bus.json the m_axi bus-transfer law (BusCalib)
points/*.json its distilled corpus
components/<task-config>/ TIMING residuals
params.json the fitted coefficients
corpus.csv the distilled corpus, re-fittable offline
rtl/ pysim/ raw per-run firings — churn, never published
modules/<module-key>/ RESOURCE records
module.json the identity this key resolves to
resource/records.jsonl the measurements
timing/records.jsonl (same envelope; unused so far)
One identity and two separate content trees for components and modules described below.
The identity manifest
The identity file platform.json stores the identity in a JSON format.
{ "part": "xc7z020clg484-1", "clk_freq_hz": 100000000.0 }
This is the one source both synthesis and calibration read, so the part a design is synthesized for
cannot drift from the part its numbers were measured on. The platform.json can optionally also describe the resource types
with a "res_types" field. As described in platform identity page, the default uses the Xilinx/AMD resources:
{ "res_types": ["lut", "ff", "dsp", "bram", "uram", "srl"] }
However, future ASIC flows could define other resource types. For example, we could theoretically add a field such as:
{ "part": "tsmc45", "clk_freq_hz": 1e9, "res_types": ["cell_area", "macros", "regs"] }
Two keys, and why
Timing and resource data are stored in two separate trees:
components/— the timing residuals, one entry per task configuration (a task’s function name plus its template arguments).modules/— the resource records, one entry per structurally distinct module.
They key differently because they are answering different questions:
components/ (timing) |
modules/ (resource) |
|
|---|---|---|
| keyed by | the task configuration — function name + template args, e.g. mem_r_stream_framed_task_32 |
the module’s structure digest, e.g. mem_r_stream-04919c18 |
| granularity | one entry per synthesized task variant | one entry per structurally distinct module |
| readable? | yes — it is the RTL entity prefix | prefix only; the digest is for correctness |
What they share is that both are configuration-precise: neither will hand a design at
mem_dwidth=64 a number measured at 32. A single function, config_id() in
waveflow/calib/module_key.py, is what both the timing resolver and the
resource attributor read, so the two cannot drift on what “the same
configuration” means.
The timing key was not always configuration-precise: it used the bare function name, so one
mem_r_stream_framed_taskdirectory served every memory width. Older libraries still load — the resolver falls back to the bare name — but a residual found that way reportsconfig_specific=Falsein its confidence rather than claiming to match the width being built. The two shipped residuals are in exactly that state.
Where platform directories live
Three homes, searched in this order:
1. platforms_root the build's own — defaults to calib/platforms/ (your project's library)
2. $WAVEFLOW_PLATFORM_PATH (CI, a shared checkout)
3. user data dir ~/.local/share/waveflow/platforms/ (a board you calibrated)
4. the package waveflow/calib/platforms/ (shipped, read-only)
Creation always targets platforms_root, never a read-only fallback.
First match wins on the whole directory. There is no merging across roots.
This is the single most surprising thing about platform resolution, and it decides how a project should set one up. A platform name resolves to exactly one directory — the first root that has it — so:
before your project has one : resolves to PACKAGED, 4 module records visible after your project publishes: resolves to PROJECT, 0 module records visible shipped bus law reachable: FalseThe moment your project owns a platform of a given name, the packaged one of that name stops being consulted at all — you do not inherit its bus law, its residuals, or its records. And giving your platform a different name does not help either: that is simply a new, empty platform.
That is the reason a project seeds its library from an upstream platform rather than inheriting from one — see Seeding.
The tracked / untracked split
calib/work/<name>/ UNTRACKED. Sweeps, tests and DAG steps write here freely. It churns.
calib/platforms/<name>/ TRACKED. Your project's library. publish_calib is the only writer.
waveflow/calib/platforms/ TRACKED. The reference library, shipped as package data.
A sweep writes the work tier and publish_calib promotes the stable artifacts — identity, fitted
params, distilled corpora, module records — leaving the raw per-run firing trees behind. That is what
keeps a re-publish churn-free: unchanged files are byte-compared and not rewritten, so re-running a
deterministic fit produces no diff at all.
Give a sweep its own platform name. Reusing a shipped platform’s name makes resolution find the packaged directory through its fallbacks and write there — bypassing
publish_calib, which is meant to be the single writer of a tracked library.
What is worth committing
Everything a platform publishes is small and text. The shipped reference holds a bus law, two timing
residuals, and the framework modules’ measured configurations; the fir_block project library adds 31
more, together representing about 26 minutes of Vitis C-synthesis in well under 100 KB. The point of
committing either is that the next checkout gets those numbers as a cache hit rather than a toolchain
run.
What is deliberately not committed is the raw material: firing trees, solution directories, generated RTL. All of it is reproducible from a re-sweep, and all of it is large.
See also
- Platform identity — the manifest and the mismatch gate.
- Managing a platform — creating, inspecting and publishing one.
- Module keys and the record store — the
modules/tier’s contract.