| name | nc-basics |
| description | Teaches the basics of Netlist Carpentry, how it is used, what the use cases are, and shows some simple examples. |
NetlistCarpentry — Digital Circuit Analysis & Modification Library
Table of Contents
| # | Section | Description |
|---|
| 1 | What It Is | Overview, key stack, and design philosophy |
| 2 | Entry Point — Loading a Circuit | read(), read_json(), ReadConfig |
| 3 | Core Data Model | Circuit, Module, Instance hierarchy |
| 4 | Element Paths — Hierarchical Navigation | Dot-separated paths, ElementPath, get_from_path() |
| 5 | Modifying Circuits | Adding/removing instances, ports, wires; locking |
| 6 | Built-in Gate Library | Primitive gates (§ prefix), lookup via get(), gate table |
| 7 | ModuleGraph — NetworkX Integration | Building graphs, node/edge structure, traversal |
| 8 | Pattern Matching & Replacement | Pattern, Match, find_matches(), replacement |
| 9 | Built-in Routines | Optimization, DFT, scan chains, constant propagation |
| 10 | Equivalence Checking | prove_equivalence(), Yosys EQY integration |
| 11 | Writing Output | Exporting to Verilog/VHDL/JSON |
| 12 | Signal Model | 4-value logic (LOW, HIGH, UNDEFINED, FLOATING), Signal, SignalDict |
| 13 | Signal Handling & Evaluation | set_signal(), evaluate(), signal propagation |
| 14 | Typical Workflow | Step-by-step usage examples |
| 15 | Where It's Helpful | Use cases and strengths |
| 16 | Key Files Reference | Important source files and their roles |
| 17 | Configuration | CFG global settings |
| 18 | Notes for AI Agents | Anti-patterns, gotchas, and best practices |
What It Is
NetlistCarpentry is a Python library for loading, navigating, analyzing, modifying, and exporting digital circuits (Verilog/VHDL netlists). It converts RTL descriptions into a rich, Pythonic object model backed by a NetworkX MultiDiGraph, enabling graph algorithms, pattern matching, optimization, and equivalence checking.
Key stack: Python 3.9+, Yosys (via yowasp-yosys) for RTL→JSON conversion, Pydantic models, NetworkX.
Entry Point — Loading a Circuit
from netlist_carpentry import read
circuit = read("my_design.v")
circuit.set_top("my_design")
from netlist_carpentry import ReadConfig, read_via_cfg
cfg = ReadConfig(files=["design.v"], top="my_design")
circuit = read_via_cfg(cfg)
The read() function:
- Calls Yosys to convert RTL → JSON netlist
- Parses the JSON into a
Circuit object with full Pythonic access
- Returns a
Circuit ready for analysis or modification
You can also load from an existing JSON file directly:
from netlist_carpentry import read_json
circuit = read_json("netlist.json")
Core Data Model
Circuit — The Root Container
A Circuit holds all modules and tracks the top-level module.
circuit.name
circuit.modules
circuit.top
circuit.has_top
circuit.instances
circuit.creator
circuit["module_name"]
circuit.get_from_path("top.inst.subinst")
circuit.get_path_from_str("top.inst.port.0")
circuit.add_module(module)
circuit.remove_module("name")
circuit.copy_module("old", "new_name")
circuit.create_module("new_name")
circuit.set_top("top_module")
Circuit.read(["design.v"])
Circuit.read(cfg)
circuit.write("output.v")
circuit.write("output.v", overwrite=True)
Module — A Single Design Unit
A Module represents one Verilog module/VHDL entity. It contains instances, ports, and wires.
module.name
module.instances
module.ports
module.wires
module.get_instances(name="foo", type="§and", fuzzy=True, recursive=True)
module.get_ports(direction=Direction.IN, fuzzy=False)
module.get_wires(name="data_bus", fuzzy=True)
module.submodules
module.primitives
module.instances_by_types
paths = module.bfs_paths_between(start_path, end_path)
paths = module.dfs_paths_between(start_path, end_path)
module.evaluate()
module.optimize()
graph = module.graph()
Instance — A Gate or Submodule Instantiation
inst.name
inst.instance_type
inst.parameters
inst.connections
inst.ports
inst.input_ports
inst.output_ports
inst.ports["clk"]
inst.connections
inst.module_definition
Port — Module or Instance I/O
port.name
port.direction
port.width
port.segments
port.signal
port[0]
len(port)
Wire — Internal Net Connection
wire.name
wire.width
wire.segments
wire.connected_port_segments
WireSegment / PortSegment — Per-Bit Elements
The lowest level: individual bits of wires and ports.
seg.signal
seg.ws_path
seg.is_connected / seg.is_unconnected
seg.is_tied
seg.driver()
seg.loads()
wire.connected_port_segments
Element Paths — Hierarchical Navigation
NetlistCarpentry uses typed path objects for precise hierarchical access. Paths use dot-separated components with numeric segment indices (not bracket notation).
| Path Type | Example | Purpose |
|---|
ModulePath | "top" | Reference a module |
InstancePath | "top.u_adder" | Reference an instance |
PortPath | "top.data_in" | Reference a module port |
PortSegmentPath | "top.data_in.3" | Reference bit 3 of a port (NOT "top.data_in[3]") |
WirePath | "top.addr_bus" | Reference a wire |
WireSegmentPath | "top.addr_bus.7" | Reference bit 7 of a wire (NOT "top.addr_bus[7]") |
The path format is: module_name.instance_name.port_or_wire_name.segment_index
from netlist_carpentry import Circuit
circuit = read("design.v")
elem = circuit.get_from_path("top.u_adder.sum.4")
path = circuit.get_path_from_str("top.u_adder.clk")
Key details:
- Segment indices are dot-separated integers, not bracket notation
- The path hierarchy is:
module → instance(s) → port/wire → segment_index
- For module-level ports (no instance):
"top.data_in.3"
- For instance ports:
"top.u_adder.sum.4"
- Path strings can be resolved automatically via
circuit.get_path_from_str() which infers the correct path type
Modifying Circuits
Creating Elements
module = circuit["my_module"]
port = module.create_port("new_signal", Direction.IN, width=8)
wire = module.create_wire("internal_net", width=4)
inst = module.create_instance(module.circuit["sub_module"], "u_sub")
from netlist_carpentry.utils.gate_lib import AndGate
inst = module.create_instance(AndGate, "u_and")
Connecting Elements
module.connect(source_port, target_port)
module.connect(wire_segment, port_segment)
module.connect("top.wire.0", "top.inst.port.1")
module.disconnect(port_segment)
Removing Elements
module.remove_instance("u_inst")
module.remove_port("old_signal")
module.remove_wire("unused_net")
circuit.remove_module("unused_module")
Substituting Instances
module.refine_instance("u_old", NewGateClass)
module.substitute_instance("u_old", new_instance)
Built-in Gate Library
The gate_lib module provides primitive gate classes, all prefixed with § (configurable via CFG.id_internal). These are the actual classes available:
Unary Gates
| Class | Instance Type | Description |
|---|
Buffer | §buf | Buffer (pass-through) |
NotGate | §not | Inverter |
PosGate | §pos | Arithmetic plus (sign extend) |
NegGate | §neg | Arithmetic negator (two's complement) |
Binary Gates
| Class | Instance Type | Description |
|---|
AndGate | §and | AND |
OrGate | §or | OR |
XorGate | §xor | XOR |
XnorGate | §xnor | XNOR |
NorGate | §nor | NOR |
NandGate | §nand | NAND |
BitwiseCaseEquality | §bweqx | Bitwise case equality (===) |
Shift Gates
| Class | Instance Type | Description |
|---|
ShiftLeft | §shl | Shift left logical (<<) |
ShiftRight | §shr | Shift right logical (>>) |
ArithmeticShiftLeft | §sshl | Arithmetic shift left (<<<) |
ArithmeticShiftRight | §sshr | Arithmetic shift right (>>>) |
ShiftSigned | §shift | Signed shift (direction based on sign of B) |
ShiftX | §shiftx | Indexed part-select shift ([+:width]) |
Reduction Gates
| Class | Instance Type | Description |
|---|
ReduceAnd | §reduce_and | AND of all bits |
ReduceOr | §reduce_or | OR of all bits |
ReduceBool | §reduce_bool | Boolean reduction (non-zero → 1) |
ReduceXor | §reduce_xor | XOR of all bits |
ReduceXnor | §reduce_xnor | XNOR of all bits |
LogicNot | §logic_not | Logical NOT (non-zero → 0, all-zero → 1) |
Comparison Gates (Binary N-to-1)
| Class | Instance Type | Description |
|---|
LogicAnd | §logic_and | Logical AND (&&) of two operands |
LogicOr | §logic_or | Logical OR (` |
LessThan | §lt | Less than (<) |
LessEqual | §le | Less or equal (<=) |
Equal | §eq | Equality (==) |
CaseEqual | §eqx | Case equality comparison (===) |
NotEqual | §ne | Not equal (!=) |
CaseNotEqual | §nex | Case not-equal comparison (!==) |
GreaterThan | §gt | Greater than (>) |
GreaterEqual | §ge | Greater or equal (>=) |
Arithmetic Gates
| Class | Instance Type | Description |
|---|
Adder | §add | Adder (+) |
Subtractor | §sub | Subtractor (-) |
Multiplier | §mul | Multiplier (*) |
Divider | §div | Divider (/) |
Modulo | §mod | Modulo (%) |
Exponentiator | §pow | Exponentiation (**) |
Multiplexer / Demultiplexer
| Class | Instance Type | Description |
|---|
Multiplexer | §mux | N-to-1 MUX (configurable via bit_width parameter: 2^bit_width inputs) |
Demultiplexer | §demux | 1-to-N DEMUX (configurable via bit_width parameter) |
Storage Elements (Flip-Flops & Latches)
| Class | Instance Type | Description |
|---|
DFF | §dff | D flip-flop (CLK, D, Q) |
ADFF | §adff | DFF with async reset (CLK, D, RST, Q) |
DFFE | §dffe | DFF with enable (CLK, EN, D, Q) |
ADFFE | §adffe | DFF with async reset + enable |
SDFF | §sdff | DFF with sync reset (CLK, RST, D, Q) |
SDFFCE | §sdffce | DFF with sync reset + enable |
SDFFE | §sdffe | DFF with sync reset + enable (alt.) |
ALDFF | §aldff | DFF with async load (CLK, LD, D, Q) |
ALDFFE | §aldffe | DFF with async load + enable |
DFFSR | §dffsr | DFF with set/reset (CLK, S, R, Q) |
DFFSRE | §dffsre | DFF with set/reset + enable |
DLatch | §dlatch | D latch (EN, D, Q) |
Scan Flip-Flops
| Class | Instance Type | Description |
|---|
ScanDFF | §scan_dff | Scan-capable DFF |
ScanADFF | §scan_adff | Scan DFF with async reset |
ScanDFFE | §scan_dffe | Scan DFF with enable |
ScanADFFE | §scan_adffe | Scan DFF with async reset + enable |
Mixins (combine with gate classes)
Gate classes inherit from mixins for additional functionality:
ClkMixin — clock port (inherited by DFF and variants)
RstMixin — reset port
EnMixin — enable port
ScanMixin — scan chain support
SRMixin — set/reset ports
LoadMixin — async load port
Use them directly as interface definitions for create_instance():
from netlist_carpentry.utils.gate_lib import DFF, AndGate, Multiplexer
from netlist_carpentry.utils.gate_mixins import ClkMixin, RstMixin
class MyDFF(ClkMixin, RstMixin, DFF):
pass
inst = module.create_instance(MyDFF, "u_ff")
mux = module.create_instance(Multiplexer, "u_mux")
mux.parameters.BIT_WIDTH = 2
mux.parameters.WIDTH = 8
ModuleGraph — NetworkX Integration
Every module has an associated ModuleGraph (a networkx.MultiDiGraph) for graph algorithms:
from netlist_carpentry import ModuleGraph
graph = module.graph()
graph.node_type("u_and")
graph.node_subtype("u_and")
all_edges = graph.all_edges("u_and")
import networkx as nx
paths = nx.all_simple_paths(graph, source="input_port", target="output_port")
cycles = nx.simple_cycles(graph)
Pattern Matching & Replacement
Define a pattern as a module and find/replace it in the circuit:
from netlist_carpentry import Pattern, Module
pattern_module = Module("pattern")
replacement_module = Module("replacement")
pattern = Pattern(pattern_module, replacement_module)
matches = pattern.match(target_module)
print(f"Found {matches.count} occurrences")
pattern.replace(target_module)
Constraints can filter matches:
from netlist_carpentry.core.graph.constraint import CASCADING_OR_CONSTRAINT
Built-in Routines
Analysis (read-only checks)
from netlist_carpentry.routines.check import fanout, find_comb_loops, has_comb_loops
if has_comb_loops(module):
loops = find_comb_loops(module)
fanout_counts = fanout(module)
Optimization
from netlist_carpentry.routines.opt import (
clean_circuit,
opt_constant,
opt_driverless,
opt_loadless,
opt_chains,
)
opt_constant(module)
opt_driverless(module)
clean_circuit(circuit)
Design-For-Test
from netlist_carpentry.routines import dft
Equivalence Checking
NetlistCarpentry integrates with eqy (via yosys-eqy) for formal equivalence checking:
from netlist_carpentry import run_eqy, run_equiv, run_equiv_miter
result = run_eqy(original_netlist, modified_netlist, top_module="my_design")
miter = run_equiv_miter(circuit_a, circuit_b, "top")
Writing Output
Export the modified circuit back to Verilog:
from netlist_carpentry import write
write(circuit, "output_design.v", overwrite=True)
Signal Model
Digital signals use a 4-value logic system:
from netlist_carpentry import Signal
Signal.LOW
Signal.HIGH
Signal.UNDEFINED
Signal.FLOATING
Signal.get('0')
Signal.get(True)
Signal.get(1)
Signal.parsable('x')
Signal Handling & Evaluation
Key Principle: Read vs. Write
Reading signals is always direct attribute access. Writing signals requires calling set_signal() — direct assignment like port[0].signal = Signal.HIGH does NOT work.
seg_signal = port[0].signal
wire_signal = wire[3].signal
port_all_signals = port.signal
port_array = port.signal_array
wire_array = wire.signal_array
port[0].set_signal(Signal.HIGH)
port[0].set_signal('1')
port[0].set_signal(1)
wire[3].set_signal(Signal.LOW)
port.set_signal(Signal.HIGH, index=0)
port.set_signal(Signal.LOW, index=1)
port.set_signals(0b1010)
port.set_signals("1010")
port.set_signals({0: '1', 2: '1'})
wire.set_signal(Signal.HIGH, index=0)
wire.set_signals(0b1100)
circuit.set_signal("top.in1", Signal.HIGH)
circuit.set_signal("top.in1.0", Signal.LOW)
circuit.set_signal("top.wire_a.3", 'z')
port[0].signal = Signal.HIGH
wire[3].signal = Signal.LOW
Tying Signals to Constants
Use tie_signal() to permanently tie a port segment to a constant value (0, 1, x, z). This is different from set_signal() — tied signals cannot be overwritten during evaluation:
port[0].tie_signal('0')
port[1].tie_signal('1')
port[2].tie_signal('z')
port[3].tie_signal('x')
port.tie_signal('1', index=0)
circuit.set_signal("top.in1", Signal.HIGH)
When to use set_signal() vs tie_signal():
set_signal() — temporary signal setting, overwritten during evaluation if driven by a wire
tie_signal() — permanent constant binding, survives evaluation (used for constant propagation)
Signal Evaluation Process
The evaluation process propagates signals through the circuit in breadth-first order, starting from input ports:
from netlist_carpentry import read
circuit = read("design.v")
module = circuit["my_module"]
module.ports['clk'].set_signal(Signal.HIGH)
module.ports['data_in'].set_signal(Signal.LOW)
module.ports['data_in'].set_signal(Signal.HIGH, index=1)
module.evaluate()
assert module.ports['out'].signal == Signal.LOW
assert module.wires['internal'].signal_array[0] == Signal.HIGH
assert module.get_instance('u_and').ports['Y'].signal == Signal.LOW
Evaluation flow:
- Starts from input ports and constant-driven instances
- Evaluates wire segments (propagates driver signal to loads)
- Evaluates instances (computes gate logic, propagates to output ports)
- Recursively evaluates downstream wires and instances
- For hierarchical modules: evaluates submodules in-place
For sequential elements (DFF, etc.): Evaluation updates outputs on clock edges based on input states. Call evaluate() multiple times for multi-cycle simulation.
Multi-Bit Signal Handling with SignalArray
from netlist_carpentry import SignalArray
arr = SignalArray.from_int(0b1010, fixed_width=4)
arr = SignalArray.from_bin("1010")
arr = SignalArray.create([Signal.HIGH, Signal.LOW, Signal.UNDEFINED])
arr = SignalArray.create({0: '1', 2: '1'})
value = int(arr)
str(arr)
arr.is_defined
arr.is_undefined
Practical Evaluation Examples
Example 1: Simulating a combinational circuit
circuit = read("alu.v")
alu = circuit["alu"]
alu.ports['a'].set_signals(0b0011)
alu.ports['b'].set_signals(0b1100)
alu.ports['op'].set_signal('0', index=0)
alu.evaluate()
result = int(alu.ports['result'].signal_array)
Example 2: Simulating a sequential circuit (DFF)
circuit = read("counter.v")
cnt = circuit["counter"]
cnt.ports['clk'].set_signal(Signal.LOW)
cnt.ports['data'].set_signal(Signal.HIGH)
cnt.evaluate()
cnt.ports['clk'].set_signal(Signal.HIGH)
cnt.evaluate()
cnt.ports['clk'].set_signal(Signal.LOW)
cnt.evaluate()
cnt.ports['data'].set_signal(Signal.LOW)
cnt.ports['clk'].set_signal(Signal.HIGH)
cnt.evaluate()
Example 3: Using Circuit.set_signal for quick input setting
circuit = read("design.v")
circuit.set_signal("top.in_a", Signal.HIGH)
circuit.set_signal("top.in_b.0", Signal.LOW)
circuit.set_signal("top.in_b.1", Signal.HIGH)
circuit.evaluate()
output = circuit.get_from_path("top.out")
Example 4: Constant propagation optimization
from netlist_carpentry.routines.opt import opt_constant
circuit = read("design.v")
module = circuit["top"]
module.ports['mode'].tie_signal('1')
module.ports['enable'].tie_signal('0')
opt_constant(module)
Typical Workflow
from netlist_carpentry import read, write, Circuit
from netlist_carpentry.routines.opt import opt_constant, clean_circuit
from netlist_carpentry.routines.check import has_comb_loops
circuit = read("design.v")
circuit.set_top("design")
if has_comb_loops(circuit):
loops = find_comb_loops(circuit)
print(f"Combinational loops: {loops}")
top = circuit.top
for inst_name, inst in top.instances.items():
print(f" {inst_name}: type={inst.instance_type}, ports={list(inst.ports.keys())}")
new_inst = top.create_instance(AndGate, "u_new_and")
top.connect(top.wires["net_a"][0], new_inst.ports["A"][0])
top.connect(top.wires["net_b"][0], new_inst.ports["B"][0])
top.connect(new_inst.ports["Y"][0], top.wires["net_out"][0])
opt_constant(top)
clean_circuit(circuit)
write(circuit, "design_modified.v", overwrite=True)
Where It's Helpful
- RTL analysis: Navigate complex designs, understand signal flow, find combinational loops, analyze fanout
- Automated optimization: Constant propagation, dead logic removal, chain optimization
- Pattern-based refactoring: Find and replace circuit patterns (e.g., replace multi-level logic with LUTs)
- Design-for-test: Automatic scan chain insertion
- Circuit transformation: Modify netlists programmatically (add wrappers, insert debug logic, restructure)
- Equivalence checking: Verify that modifications preserve functionality
- Research & education: Experiment with synthesis/optimization algorithms in Python
- Pre/post-synthesis comparison: Load synthesized netlists and compare with RTL descriptions
Key Files Reference
| File | Purpose |
|---|
src/netlist_carpentry/__init__.py | Public API exports (read, write, Circuit, Module, etc.) |
src/netlist_carpentry/core/circuit.py | Circuit class — root container |
src/netlist_carpentry/core/netlist_elements/module.py | Module class — design unit |
src/netlist_carpentry/core/netlist_elements/instance.py | Instance class — gate/submodule |
src/netlist_carpentry/core/netlist_elements/port.py | Port class — I/O ports |
src/netlist_carpentry/core/netlist_elements/wire.py | Wire class — internal nets |
src/netlist_carpentry/core/graph/module_graph.py | ModuleGraph — NetworkX wrapper |
src/netlist_carpentry/core/graph/pattern.py | Pattern — pattern matching |
src/netlist_carpentry/utils/gate_lib.py | Built-in primitive gate library |
src/netlist_carpentry/io/read/read_utils.py | read(), read_json() entry points |
src/netlist_carpentry/io/write/write_utils.py | write() — export to Verilog |
src/netlist_carpentry/routines/opt/ | Optimization routines |
src/netlist_carpentry/routines/check/ | Analysis/checking routines |
src/netlist_carpentry/routines/dft/ | Design-for-test routines |
Configuration
from netlist_carpentry import CFG
CFG.id_internal
CFG.id_external
CFG.log_level
CFG.print_source_module
CFG.allow_detached_segments
CFG.yosys_executable
Notes for AI Agents
- Loading circuits: Use
Circuit.read(config) or the module-level read() function to load Verilog, VHDL, or JSON. Circuits can also be built from scratch via Circuit(name="...") and circuit.create_module("name").
- Setting top module: Use
circuit.set_top("module_name") after loading if the top isn't auto-detected. Access it via circuit.top or circuit.top_name.
- Path strings use dot-separated hierarchical notation (e.g.,
"top.u_adder.sum.4"), NOT bracket/Verilog-style paths. Use typed ElementPath objects for robustness.
- Connections require matching widths — multi-bit connections need segment-by-segment wiring or equal-width ports.
- Elements can be locked — check
element.locked before modifying; some operations lock elements to prevent inconsistent state.
- Gate library uses
§ prefix — primitive gates have instance types like "§and", "§dff", "§or", etc. (see gate table above for full mappings). Configure with CFG.id_internal.
- Signal propagation:
module.evaluate() performs breadth-first evaluation from input ports through the entire module. Call iteratively for multi-level or recursive circuits.
- Pattern matching:
Pattern constructor takes a ModuleGraph (not a Module). Use module.graph() to build the graph, then pattern.find_matches(circuit_graph) to search. Use Pattern.get_mapping(pattern_module, replacement_module) to map ports between modules.
- Modifications are tracked internally — the library maintains consistency across changes.
module.graph() is a method call, not a property — always use parentheses: module.graph(). Returns a ModuleGraph (NetworkX MultiDiGraph).
- Signal access: Use
seg.signal on PortSegment/WireSegment to get the current signal value. There is NO seg.raw property.
- Wire objects DO have
set_signal() and set_signals() methods — both Wire and Port support signal setting. PortSegment and WireSegment both have .driver() and .loads() methods.
- SignalArray.create() accepts list, dict, int, or str to create signal arrays.
- DFT routines (
netlist_carpentry.routines.dft) have no public exports (__all__ is empty) — scan chain insertion is under development.
Circuit.read() is a class method; the module-level read() function accepts strings, paths, or ReadConfig.
Circuit.prove_equivalence(gold_design, out_dir) exists on Circuit for equivalence checking via Yosys EQY. Accepts either a list of gold Verilog file paths or another Circuit object.
Module.check() returns a CheckReport with structural validation results.
Circuit.check() also exists for circuit-level validation.
Circuit.optimize() and Module.optimize() both exist for circuit/module optimization.
Circuit.evaluate() performs evaluation across the entire circuit hierarchy.
- ModuleGraph access: Use
module.graph() (method call). ModuleGraph has .nodes and .edges attributes.
- Instance access: Use
inst.ports["clk"] (CustomDict subscripting), NOT inst.port["clk"].
- BFS/DFS traversal: Use
module.bfs_paths_between() and module.dfs_paths_between() — there are NO bfs_instances() or dfs_instances() methods.
- Module properties:
module.submodules, module.instances_by_types, and module.primitives are all available as properties.
module.show() renders the module graph with optional interactive=True or figpath parameters.