This document is for the FaultFlow session: it describes the machine interface OT exposes so FaultFlow can obtain structural reconvergence data before ATPG without inserting test points.
Overview
OT exposes an internal CLI subcommand _preflight (underscore prefix = not for end users).
FaultFlow calls it as a subprocess, parses the stdout JSON line, and reads two output files:
opentest --yosys _preflight \
-i /path/to/netlist.json \
--scoap \
-r \
--reconv-algorithm advanced \
-o /path/to/workspace
Recommended flags:
--scoap: run SCOAP metrics (also the default when neither--scoapnor--copis given)-r/--reconvergence: run reconvergence analysis (without it the manifest’sfanout_pointsis empty)--reconv-algorithm advanced: the default. The records are structural ordering hints whichever algorithm produces them; see What the records mean and Runtime on large designs-o <dir>: output directory; defaults to<netlist_dir>/_preflight/--tech sky130|osu035|ihp|nangate|auto: PDK (default:sky130); echoed in the status line, and the cell library tried first for cells withoutport_directions(see Netlists without port directions)
-i and -o may be relative: both are resolved against the caller’s working directory
(FaultFlow’s scan pipeline passes cwd-relative paths such as
output/<top>/.faultflow/intermediate/preflight). -i must name an existing file; it is
never looked up in OpenTestability’s own data/ directories. Everything the run writes
goes under -o (see Files Written).
Stdout Contract
OT prints exactly one JSON object on stdout as the last output line:
{
"status": "ok",
"manifest": "/abs/path/to/tpi_manifest.json",
"reconv_ids": "/abs/path/to/fanout_design_reconv_ids.json",
"tech": "sky130"
}
On error:
{"status": "error", "message": "...reason..."}
FaultFlow should:
- Run the command, capture stdout
- Find the last line starting with
{ - Parse JSON, check
status == "ok" - Use the returned
manifestandreconv_idspaths
Output Files
tpi_manifest.json
Written at <output_dir>/tpi_manifest.json. Schema version "1.0".
Key fields for FaultFlow:
{
"schema_version": "1.0",
"session_id": "ot_1234567890_abcd1234",
"iteration": 1,
"top_module": "fanout_design",
"netlist_path": "/abs/path/to/netlist.json",
"inserted_test_points": [],
"structural_hints": {
"fanout_points": [
{"name": "net_foo", "yosys_net_id": 42},
{"name": "net_bar", "yosys_net_id": 107},
{"name": "internal_ot_name", "yosys_net_id": null}
],
"reconvergences": [...],
"enabled": true,
"algorithm": "advanced"
},
"awaiting_oracle": true,
"response_path": "/abs/path/to/oracle_response.json"
}
structural_hints.fanout_points
list[dict]; each entry is:
| Field | Type | Notes |
|---|---|---|
name |
string |
OT signal name (from circuit graph) |
yosys_net_id |
integer \| null |
netnames[name]["bits"][0], the first integer in Yosys bits list. null if signal not in Yosys netnames or if all bits are string constants ("0", "1") |
Mapping: FaultFlow maps yosys_net_id → its internal compiled net index via the existing
Yosys net-ID lookup it already uses for other purposes. Signals with null net_id are OT
internal synthesis names; skip or ignore them.
FaultFlow action (ordering only): before the ATPG round loop, push any fault whose
net_id is in this set to the back of the ordering queue and skip the short-timeout tier
(reconvergent-site faults tend to be hard; don’t burn short attempts on them). This changes
the order in which SAT sees faults and nothing else: membership in this set is never
evidence that a fault is redundant (see What the records mean).
<design>_reconv_ids.json
Written at <output_dir>/<design>_reconv_ids.json. This is the algorithm output enriched
with Yosys net IDs on every reconvergence record.
The record shape depends on which --reconv-algorithm produced it (basic, simple,
advanced, or dominator, echoed back as structural_hints.algorithm in
tpi_manifest.json). These are not one shared format: three are flat per-pair dicts
with different field names, and one (advanced) nests pairs one level deeper under each
fanout site. Branch on structural_hints.algorithm, not on which fields happen to be
present.
Comparison
basic |
simple |
advanced (per entry in pairs[]) |
dominator |
|
|---|---|---|---|---|
| Grouping | one dict per pair | one dict per pair | one dict per site, pairs nested in pairs[] |
one dict per (stem, site) pair |
site |
✓ | ✓ | ✓ (site-level, not per-pair) | ✓ |
| Branch fields | branch1, branch2 (raw signal names) |
fanout1, fanout2 (raw signal names, not branch1/branch2) |
branch1, branch2 (synthetic branch-id strings, e.g. "sig_A_br0", not raw net names) |
branch1, branch2 (raw signal names) |
stem |
- | - | ✓ | ✓ |
path1 / path2 (node list) |
✓ | ✓ | - | ✓ (null if budget-exhausted) |
path1_length / path2_length (int) |
- | ✓ | - | ✓ (null if budget-exhausted) |
path1_count / path2_count (int) |
- | - | ✓ (path-multiplicity counts from FOBL, a different concept from path*_length) |
- |
cone (node list) |
- | - | - | ✓ (null if budget-exhausted) |
paths_materialized (bool) |
- | - | - | ✓ |
| Sibling data | - | - | top-level fanout_branches[] (see below) |
- |
basic and simple look similar but are easy to conflate incorrectly: basic has no
stem and no length fields at all; simple uses fanout1/fanout2 instead of
branch1/branch2 and adds integer path1_length/path2_length. Treat them as two
distinct formats, not one “basic/simple” format.
basic: flat, minimal
{
"site": "gate_Y", "site_net_id": 42,
"branch1": "sig_A", "branch1_net_id": 55,
"branch2": "sig_B", "branch2_net_id": 56,
"path1": ["sig_A", "mid_1", "gate_Y"],
"path2": ["sig_B", "mid_2", "gate_Y"]
}
simple: flat, with path lengths, different branch-field names
{
"site": "gate_Y", "site_net_id": 42,
"fanout1": "sig_A", "fanout1_net_id": 55,
"fanout2": "sig_B", "fanout2_net_id": 56,
"path1": ["sig_A", "mid_1", "gate_Y"],
"path2": ["sig_B", "mid_2", "gate_Y"],
"path1_length": 3, "path2_length": 3
}
There is no branch1/branch2 in this shape; the fanout-source fields are named
fanout1/fanout2 instead.
advanced (recommended, use --reconv-algorithm advanced): nested, one dict per site
{
"site": "gate_Y", "site_net_id": 42,
"pairs": [
{
"branch1": "sig_A_br0", "branch1_net_id": null,
"branch2": "sig_A_br1", "branch2_net_id": null,
"path1_count": 2, "path2_count": 1,
"stem": "sig_A", "stem_net_id": 50
}
]
}
branch1/branch2 here are synthetic branch-id strings minted by the FOBL algorithm
(e.g. "sig_A_br0"), not raw net/signal names, so branch1_net_id/branch2_net_id will
almost always come back null (the lookup searches Yosys netnames for the literal string
and won’t find it). Don’t read a null here as “signal missing from netlist”; it’s expected
for this shape. path1_count/path2_count are path-multiplicity counts, not path
lengths; don’t conflate with simple’s path1_length/path2_length. There is no
path1/path2 node list in this shape at all. stem/stem_net_id name the fanout stem
whose two branches reconverge at site.
There is also a top-level fanout_branches array, a sibling of reconvergences (not
nested inside it, specific to the advanced algorithm’s output), enumerating every fanout
branch in the whole circuit (not pairs):
{
"reconvergences": [ ... ],
"fanout_branches": [
{"branch_id": "sig_A_br0", "stem": "sig_A", "target": "gate_Y"}
]
}
The enrichment step only walks reconvergences (and pairs within it); it does not
touch fanout_branches, so these entries pass through into <design>_reconv_ids.json with
no *_net_id fields added, even though stem appears here too.
dominator: flat, one dict per (stem, site) pair, budget-aware
{
"site": "gate_Y", "site_net_id": 42,
"stem": "sig_A", "stem_net_id": 50,
"branch1": "sig_A_b1", "branch1_net_id": 55,
"branch2": "sig_A_b2", "branch2_net_id": 56,
"path1": ["sig_A", "mid_1", "gate_Y"],
"path2": ["sig_A", "mid_2", "gate_Y"],
"path1_length": 3, "path2_length": 3,
"cone": ["sig_A", "mid_1", "mid_2", "gate_Y"],
"paths_materialized": true
}
If the detector’s path-materialization time budget is exhausted before reaching a given (stem, site) pair, the record is still emitted, with the path/cone fields nulled out:
{
"site": "gate_Z", "site_net_id": 61,
"stem": "sig_B", "stem_net_id": 70,
"branch1": "sig_B_b1", "branch1_net_id": 80,
"branch2": "sig_B_b2", "branch2_net_id": 81,
"path1": null, "path2": null,
"path1_length": null, "path2_length": null,
"cone": null, "paths_materialized": false
}
site/stem/branch1/branch2 (and their _net_id counterparts) are always populated,
even in the budget-exhausted case.
What gets a *_net_id field
The enrichment step adds a *_net_id field for exactly these source fields, wherever they
appear on a record (recursing one level into advanced’s pairs[]): site, branch1,
branch2, fanout1, fanout2, stem. It’s defensive across all four shapes: a field is
simply skipped if absent from a given record, and it never raises on an unrecognized shape.
Never enriched (not signal names, so no net-ID lookup applies): path1, path2,
path1_length, path2_length, path1_count, path2_count, cone, paths_materialized.
What the records mean
Every record is a structural ordering hint, never a redundancy proof. A record says only
that two branches of a fanout stem meet again at site. None of the four detectors checks
whether the reconverging paths cancel, or analyzes testability at all: advanced (Xu &
Edirisuriya’s FOBL/RFOBL lists) enumerates reconvergent fanout-branch pairs and nothing more,
and on picorv32a its stem set equals its fanout_points set. Reconvergence is necessary for
a stem fault to be redundant, not sufficient. Treating these records as proof, e.g.
certifying every sa0/sa1 fault on a reconvergent stem as UNSAT without running SAT, is
unsound. On picorv32a all 1091 faults that rule would mark (36 stems plus their branches)
are testable: SAT finds a simulation-verified test for every one.
So use the records to order work (as the ordering action under
structural_hints.fanout_points above does) and leave every verdict to SAT: only an UNSAT
result makes a fault redundant.
FaultFlow’s own preflight consumer reads nothing but the yosys_net_ids of
structural_hints.fanout_points.
Net ID Semantics
OT derives yosys_net_id as:
bits = netnames[signal_name]["bits"]
net_id = next((b for b in bits if isinstance(b, int)), None)
This matches Yosys’s internal bit numbering. String entries like "0" and "1" represent
constant tie-offs and are skipped. The returned integer is directly usable as a Yosys net index.
Netlists without port directions
OT reads each cell’s data flow (which connected nets it reads, which it drives) from the
cell’s port_directions. Yosys only writes those when it knows the cell’s interface, so a
netlist written without the cell library loaded (no read_liberty -lib before write_json)
has none: FaultFlow’s examples/picorv32_synth/picorv32a_sky130.json has 0 of 12,602. OT
then takes each pin’s direction from, in order: the cell’s own port_directions; a module
of that cell type defined in the netlist; and built-in pin tables for the four --tech
standard-cell libraries (sky130_fd_sc_hd, IHP sg13g2, Nangate, osu035), generated from
their Liberty files. --tech picks the table tried first; the others are still tried, since
cell names are distinct across the four libraries.
Pins none of these cover are left out of the DAG and the metrics, and OT logs a warning on stderr naming the affected cell types and counts. The run still succeeds, but its results then cover only part of the design.
Test point insertion (analyze_and_add_tp, including the --atpg-loop flow) takes pin
directions the same way, both to move a control point’s readers onto its output and to
validate the result. It skips a control point whose net touches a pin none of these cover,
since it cannot tell whether that pin reads the net; the skipped point is logged and listed
under rejected_test_points in the TPI report.
Runtime on large designs
The advanced detector keeps, for every net, the fanout branches in its fan-in cone and, at
every multi-input gate, pairs the entries of its inputs’ lists that share a stem. Each entry
is looked up by stem instead of being compared with the whole other list, so the work grows
with the total length of the lists, not with products of their lengths. On the full
picorv32a netlist above (12,602 sky130 cells, 22,873 fanout branches, 2.5 million list
entries in all) the detector takes about 3 s of CPU on an idle machine, and a whole
--reconv-algorithm advanced preflight about 12 s (6 s of CPU), writing 15 MB
(tpi_manifest.json is 2.5 MB of it). On a heavily loaded machine (load average ~28 on 10
cores) FaultFlow’s own call still returned in 93 s, inside the 120 s timeout it gives the
subprocess. dominator finished the same netlist
in 154 s wall / 70 s CPU on a heavily loaded machine, writing about 370 MB of JSON: the
manifest, the reconv-IDs file and the DAG file each carry every reconvergence record. Small
designs take seconds with either algorithm.
Full Example Invocation
# Preflight before ATPG: ordering hints for the fault queue
opentest --yosys _preflight \
-i /workspace/design.json \
--scoap \
-r \
--reconv-algorithm advanced \
-o /workspace/preflight_out \
--tech sky130
# Parse stdout to get paths
RESULT=$(opentest --yosys _preflight ... 2>/dev/null | tail -1 | python3 -c "
import sys, json
d = json.load(sys.stdin)
if d['status'] != 'ok':
raise SystemExit(d['message'])
print(d['manifest'])
print(d['reconv_ids'])
")
Or in FaultFlow’s Rust/C++ subprocess handler:
- Spawn
opentest --yosys _preflight ... - Collect stdout lines
- Find last line matching
/^\{/ - Parse JSON
- On
status == "ok": readmanifestandreconv_idspaths
Backward Compatibility
tpi_manifest.json → structural_hints.fanout_pointschanged fromlist[str]→list[dict]. If FaultFlow reads the oldlist[str]format, add a version guard:if isinstance(fp[0], str): fanout_names = fp # old format else: fanout_names = [d["name"] for d in fp] # new format with net IDs- The
_reconv_ids.jsonfile is new. The un-enriched records stay in the DAG file (<design>_yosys_dag_<algorithm>.json, underreconvergence) alongside it; FaultFlow reads_reconv_ids.json. fanout_point_count(integer count) is now the field name in algorithm output where it was previouslyfanout_points(integer); this was a bug fix. FaultFlow should read signal names fromstructural_hints.fanout_points(list of dicts), not from the raw algorithm count field.
Files Written (summary)
| File | Content |
|---|---|
<output_dir>/tpi_manifest.json |
Session manifest with enriched fanout_points |
<output_dir>/<design>_reconv_ids.json |
Reconvergence records with *_net_id fields (only with -r) |
<output_dir>/<design>_scoap.json |
SCOAP metrics (<design>_cop.json with --cop) |
<output_dir>/<design>_yosys_dag_<algorithm>.json |
DAG with reconvergence data (only with -r); reused by the next run while it is newer than the netlist |
Nothing is written outside <output_dir>, and the input netlist is never modified.