Architecture overview¶
faultflow is a Python control plane over a C++17 simulation core. The Python layer
orchestrates synthesis, the ATPG loop, and reporting; the C++ core compiles the
netlist into an execution-ready form and does all simulation and SAT solving. The two
are joined by a pybind11 module named
_faultflow_core.
The three-layer IR¶
The single most important design decision is that the netlist passes through three distinct intermediate representations, each with a clear job:
flowchart TD
subgraph Parsed["ParsedGraph — maps, raw Yosys IDs"]
P1[faithful mirror of the Yosys JSON]
end
subgraph Normalized["NormalizedGraph — maps, semantics resolved"]
N1["cell-map lookup -> GateType"]
N2["const folding, alias resolution"]
N3["levelization, clock/reset tagging"]
end
subgraph Compiled["CompiledSimGraph — flat arrays, immutable"]
C1["SimNode array, exactly one output each"]
C2["CSR fanout, level_starts"]
C3["yosys_to_compiled / compiled_to_yosys maps"]
end
Parsed --> Normalized --> Compiled
Compiled --> Sim["SimState — the only mutable structure"]
ParsedGraph (
src/core/ir/parsed_graph/) is a faithful, map-based mirror of the Yosys JSON. It preserves raw integer net IDs, separates blackbox modules, and finds the top module. No cell semantics live here.NormalizedGraph (
src/core/ir/normalized_graph/) resolves meaning: it looks up each cell in the JSON cell map, applies the unsupported-cell policy, folds constants intoCONST0/CONST1driver nodes, creates a source pseudo-node for every net (so every net has exactly one driver), resolves net aliases, levelizes the graph, and identifies clock and reset nets.CompiledSimGraph (
src/core/ir/compiled_graph/) is flat and immutable: arrays ofSimNodes (each with a fixed set of input indices and exactly one output), CSR fanout, level boundaries, the observable set, and bidirectional maps between the sparse Yosys net IDs and the dense compiled indices. This is the only form the simulation hot loop ever touches.
The pipeline is assembled and cached in
src/core/ir/compiled_graph/graph_cache.cpp, keyed on the netlist and cell-map files
so edits force a rebuild.
Lowering rules¶
The GraphCompiler enforces two invariants that the simulator relies on:
One output per node. Multi-output cells are split: a full adder becomes separate sum and carry nodes (
ADDF_S,ADDF_CO); a flip-flop with aQNpin becomes the flip-flop node plus an inverter.Every fanout branch is its own net. A
BUFnode is inserted per branch so that a fault on a branch is an ordinary net fault. This realizes the checkpoint fault model (see below) structurally:
flowchart TD
subgraph Before["before lowering"]
S1[stem net N] --> G1[gate 1]
S1 --> G2[gate 2]
S1 --> G3[gate 3]
end
subgraph After["after lowering — each branch is its own fault site"]
S2[stem net N] --> B1[BUF] --> BN1[branch net N_b1] --> H1[gate 1]
S2 --> B2[BUF] --> BN2[branch net N_b2] --> H2[gate 2]
S2 --> B3[BUF] --> BN3[branch net N_b3] --> H3[gate 3]
end
The simulation engine¶
The engine (src/core/sim/engine/) is 64-lane bit-parallel: each net’s value is a
uint64_t where bit 0 is the fault-free (golden) value and bits 1–63 are 63
independent faulty universes. A primary-input value is broadcast to all 64 lanes;
faults are injected into the faulty lanes after a node evaluates (SA0 clears the bit,
SA1 sets it); detection compares each observable lane against the golden bit.
flowchart LR
subgraph Word["one uint64_t net value"]
B0["bit 0: golden (fault-free)"]
B1["bits 1-63: up to 63 faulty universes"]
end
PI["primary input"] -->|broadcast to all 64 lanes| Word
Word --> Eval["gate evaluation<br/>branch-free bitwise, no virtual calls"]
Eval --> Inject["fault injection<br/>SA0 clears the bit, SA1 sets it"]
Inject --> Cmp["compare each lane vs bit 0 -> detected mask"]
Gate evaluation (src/core/sim/gate_eval.cpp) is a branch-free bitwise table with no
virtual calls — one uint64_t operation evaluates 64 universes at once. The engine is
strictly binary; there is no X/Z value (a deliberate design decision, with
three-valued simulation on the roadmap).
Every result is cross-checked against a scalar golden-reference simulator
(src/core/sim/golden_ref/) that simulates one fault at a time with plain maps. If the
bit-parallel engine and the golden reference ever disagree, the bit-parallel engine has
a bug — see Testing & verification for how this is enforced as a hard
regression gate. The same golden reference also re-verifies every SAT-generated vector
before it is trusted.
The only mutable state during simulation is SimState (src/core/sim/state/), which
owns the working net values, flip-flop state, and previous-cycle values for edge
detection. One immutable CompiledSimGraph can back many SimStates.
Sequential and scan¶
Sequential simulation is implemented: the engine detects clock edges from the previous-vs-current clock value, applies clear/preset with polarity and conflict resolution, and steps flip-flop state across the cycles of a test vector. Flip-flop configuration (trigger edge, clock/data/output nets, set/reset, scan pins) comes from the JSON cell map, not from decoding primitive name strings.
Scan support (src/core/scan/) extracts and validates chains, then simulates the
load/launch/capture/unload protocol with the flip-flops modeled as pseudo-primary
inputs and outputs — which reduces scan ATPG to the combinational SAT problem.
IEEE 1500 wrapper and hierarchy¶
The Tcl shell’s wrap command injects wrapper boundary register (WBR) cells onto a
synthesized netlist’s boundary ports; they are ordinary lowered cells to the
simulator, not a special-cased path. set_testmode then selects which side of the
wrapper is the device under test:
flowchart TD
subgraph INTEST["INTEST — test the core"]
direction LR
I1[WBC input cells] -->|drive from FF| I2[core inputs]
I3[core outputs] -->|observed by FF| I4[WBC output cells]
end
subgraph EXTEST["EXTEST — test the interconnect, core blackboxed"]
direction LR
E1[assembly interconnect] -->|drives / observes| E2[WBC input cells]
E3[WBC output cells] -->|drives / observes| E4[assembly interconnect]
end
For a chip built from several wrapped blocks, faultflow/project/ (orchestrator.py,
aggregate.py, assemble.py) drives per-block INTEST plus one assembly EXTEST and
aggregates the results into a single chip-level coverage number — the project CLI
command and flowscripts/hereichy_atpg.tcl are the two ways to run it. Block-level
scan patterns can also be retargeted onto an SoC-level scan path (faultflow/retarget/)
without re-running ATPG. See Flow recipes for worked
examples of both.
The fault model¶
Enumeration (
src/core/fault/enumerator/) walks every compiled net — which already includes primary-input source nets and materialized fanout branches — and emits SA0 and SA1. Exclusions (clock, reset, blackbox) are tagged on the fault, not skipped.Collapsing (
src/core/fault/collapser/) shrinks the list by verified equivalence only (never dominance) — INV/BUF, the controlling-value input of AND/OR/NAND/NOR, and the compound AOI/OAI cells; see Collapsing rules.Faults are carried at runtime as flat
CompactFaultrecords, batched 63 at a time to match the 63 faulty lanes.The observable set is primary outputs plus any net tagged as a test point (
is_tp). Test points come from an optionaltp_nodes.jsonfile — a JSON array of{"net": "<name>"}and/or{"net_id": <id>}entries — or from theadd_tpOpenTestability flow (see Test-point insertion). A missingtp_nodes.jsonis silently ignored.
Native SAT ATPG¶
The ATPG (src/core/atpg/) is native and built on
CaDiCaL:
cnf_encoder.cppbuilds a CNF for the circuit, one clause group per gate (derived from the same gate-evaluation semantics the simulator uses).fault_solver.cppbuilds fault-free and faulty CNF copies, ties the primary inputs together except at the fault net, forces the stuck-at value, and adds a miter constraint that some observable output must differ.The solver returns SAT (a test vector, which is then re-simulated to confirm), UNSAT (the fault is redundant under the current model), or TIMEOUT/UNKNOWN (left undetected — never marked redundant).
flowchart TD
A[Random-fill vectors] --> B[Fault-simulate]
B --> C{coverage target met?}
C -- yes --> Z([done: THRESHOLD_MET])
C -- no --> D["Per-fault SAT — CaDiCaL<br/>cone-restricted, escalating timeout"]
D -- SAT --> E[Golden-ref verify] --> F[Fault-simulate vs. remaining faults]
D -- UNSAT --> G[mark redundant]
D -- TIMEOUT / UNKNOWN --> H[leave undetected]
F --> I{round found anything new?}
G --> I
H --> I
I -- yes --> A
I -- no --> Y([done: STALLED])
The progressive loop lives partly in C++ (progressive_atpg.cpp) and partly in the
Python harness (faultflow/runner/progressive_atpg.py), which decides when the
campaign has stalled. Redundant classifications are tagged with a
redundancy_model_id so they can be invalidated if the design fingerprint or model
changes.
End-to-end flow¶
python3 ff.py sim(or the equivalent shell commands) →faultflow/cli.py/faultflow/shell/→faultflow/service/flow.py→faultflow/runner/runner.py.If the input is Verilog, Yosys is run with the locked synthesis template, producing
<top>.jsonand<top>_gate.v.The Python layer imports
_faultflow_coreand calls into it; the C++ core builds the three-layer IR, enumerates and collapses faults, runs SAT ATPG, verifies and fault-simulates the vectors, and writes the SQLite campaign database.faultflow/reporter/coverage.pyqueries the database and emitscoverage.rptandcoverage_report.json.Optionally,
faultflow/verify/gate.pycompiles the gate-level netlist plus the PDK behavioral models with iverilog and checks the vectors against golden outputs — see Testing & verification.
Files worth reading first¶
File |
Why |
|---|---|
|
The IR pipeline entry point |
|
The flat execution model |
|
The 64-lane simulation core |
|
Authoritative gate semantics |
|
Every enum and struct |
|
Native SAT ATPG |
|
The exact Python ↔ C++ surface |
|
Yosys invocation and orchestration |
|
IEEE 1500 WBR injection |
|
Hierarchical block-to-SoC aggregation |