Skip to content

ASIC Flow

The ASIC-flow package materialises deterministic Yosys, OpenROAD, OpenSTA, KLayout, Magic, and Netgen inputs for Sky130, GF180MCU, commercial-template, and custom process configurations. It does not execute those external tools or claim physical area, power, timing, DRC, LVS, or GDSII closure.

Architecture

The historical sc_neurocore.asic_flow.asic_flow import remains available, but implementation ownership is split by responsibility:

Module Responsibility
pdk PDK presets, path resolution, installation checks, and validation
design physical-design and stochastic-synthesis parameters
decks Yosys, OpenROAD, SDC, and GDSII scripts
signoff STA/DRC/LVS scripts, PVT corners, OCV, and summaries
constraints CDC, IR-drop, IO-placement, and equivalence scripts
estimation deterministic pre-synthesis screening estimates
flow complete deck generation, bundle writes, and evidence manifests
hierarchy per-block synthesis and hard-macro top integration
readiness evidence-derived tape-out checklist state

The modules form an acyclic import graph. All 38 historical definitions retain their original qualified names and pickle paths. The package root intentionally keeps the narrower one-command API:

Python
from sc_neurocore.asic_flow import ASICFlowBundle, generate_asic_flow_bundle

Generate a bundle

Python
from sc_neurocore.asic_flow.asic_flow import DesignParams, generate_asic_flow_bundle

bundle = generate_asic_flow_bundle(
    "build/asic/sky130_demo",
    pdk_type="sky130",
    design=DesignParams(
        top_module="sc_neurocore_top",
        rtl_files=["rtl/top.sv"],
    ),
    pdk_root="/opt/pdks",
    require_pdk_files=True,
    n_neurons=32,
    n_synapses=512,
    bitstream_width=256,
    n_aer_ports=8,
    formal_evidence_artifacts=[
        "formal/sc_neurocore_top.sby",
        "formal/report.json",
    ],
)

print(bundle.manifest_path)
print(bundle.pdk_resolution.usable_for_synthesis)

The bundle contains nine generated flow files and asic_flow_manifest.json. When require_pdk_files=True, missing Liberty, cell-LEF, technology-LEF, setup, DRC, and LVS inputs are recorded rather than hidden. Formal evidence is complete for a claim only when at least one proof source (.sby, .sv, or .sva) and one report (.json, .txt, or .log) are attached.

The manifest always reports external_eda_executed: false and physical_ppa_claim_allowed: false. A downstream run must attach exact tool, container, PDK revision, command, and signoff artefacts before physical claims are made.

Polyglot boundary

Deck construction is typed text and filesystem orchestration, not a numerical kernel. Earlier Rust, Go, Julia, and Mojo files named for asic_flow were nonfunctional generated mirrors and have been removed. The maintained execution boundary is the external EDA toolchain; no language-speed comparison is claimed for removed code.

Verification and benchmark evidence

The focused suite contains 100 tests and covers all 580 statements and 66 branches across the 11 package files. It locks the historical facade, exclusive symbol ownership, the real import DAG, pickle paths, package API, responsibility size limits, removed false mirrors, and live benchmark hashes.

benchmarks/bench_asic_flow.py compares the pre-refactor source archive with the modular candidate over 30 interleaved cold processes. Both variants emit the same 10,465-byte canonical payload at SHA-256 ae901f9b10bdc61f0997964d6143568994625bdf89080f02cd58efbc83099653. The committed capture used CPU affinity without exclusive isolation while the workstation was under load, so its timing medians are local regression context, not publishable throughput evidence.

API reference

sc_neurocore.asic_flow.asic_flow

Preserve the original ASIC-flow API over responsibility modules.

New code may import the focused modules directly. Historical imports and pickle-qualified names remain stable through this facade.

CDCCheckGenerator

Generates clock-domain crossing lint scripts.

Source code in src/sc_neurocore/asic_flow/constraints.py
Python
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class CDCCheckGenerator:
    """Generates clock-domain crossing lint scripts."""

    @staticmethod
    def generate(design: DesignParams, clock_domains: Optional[List[str]] = None) -> str:
        """Render CDC checks for explicit domains or the design clock."""
        if clock_domains is None:
            clock_domains = [design.clock_name]
        domain_defs = "\n".join(f"create_clock -name {c} [get_ports {c}]" for c in clock_domains)
        return textwrap.dedent(f"""\
# SC-NeuroCore CDC Check
# Domains: {", ".join(clock_domains)}

{domain_defs}

# Report all clock-domain crossings
report_cdc -from [all_clocks] -to [all_clocks]
report_cdc -type async_reset
report_cdc -type reconvergence

# Check for missing synchronisers
check_cdc -severity error
""")

generate(design, clock_domains=None) staticmethod

Render CDC checks for explicit domains or the design clock.

Source code in src/sc_neurocore/asic_flow/constraints.py
Python
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
    @staticmethod
    def generate(design: DesignParams, clock_domains: Optional[List[str]] = None) -> str:
        """Render CDC checks for explicit domains or the design clock."""
        if clock_domains is None:
            clock_domains = [design.clock_name]
        domain_defs = "\n".join(f"create_clock -name {c} [get_ports {c}]" for c in clock_domains)
        return textwrap.dedent(f"""\
# SC-NeuroCore CDC Check
# Domains: {", ".join(clock_domains)}

{domain_defs}

# Report all clock-domain crossings
report_cdc -from [all_clocks] -to [all_clocks]
report_cdc -type async_reset
report_cdc -type reconvergence

# Check for missing synchronisers
check_cdc -severity error
""")

IOConstraintGenerator dataclass

Generates IO placement constraint files.

Source code in src/sc_neurocore/asic_flow/constraints.py
Python
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
@dataclass
class IOConstraintGenerator:
    """Generates IO placement constraint files."""

    @staticmethod
    def generate(pins: List[IOPin], design: DesignParams) -> str:
        """Render one OpenROAD ``place_pin`` command per supplied IO pin."""
        lines = [f"# IO Constraints for {design.top_module}"]
        for pin in pins:
            lines.append(
                f"place_pin -pin_name {pin.name} -layer {pin.layer} "
                f"-location {{{pin.offset_um} 0}} -side {pin.side}"
            )
        return "\n".join(lines) + "\n"

    @staticmethod
    def auto_assign(signal_names: List[str], sides: str = "NSEW") -> List[IOPin]:
        """Auto-assign pins to die edges round-robin."""
        pins = []
        for i, name in enumerate(signal_names):
            side = sides[i % len(sides)]
            pins.append(IOPin(name=name, direction="input", side=side, offset_um=float(i * 10)))
        return pins

generate(pins, design) staticmethod

Render one OpenROAD place_pin command per supplied IO pin.

Source code in src/sc_neurocore/asic_flow/constraints.py
Python
90
91
92
93
94
95
96
97
98
99
@staticmethod
def generate(pins: List[IOPin], design: DesignParams) -> str:
    """Render one OpenROAD ``place_pin`` command per supplied IO pin."""
    lines = [f"# IO Constraints for {design.top_module}"]
    for pin in pins:
        lines.append(
            f"place_pin -pin_name {pin.name} -layer {pin.layer} "
            f"-location {{{pin.offset_um} 0}} -side {pin.side}"
        )
    return "\n".join(lines) + "\n"

auto_assign(signal_names, sides='NSEW') staticmethod

Auto-assign pins to die edges round-robin.

Source code in src/sc_neurocore/asic_flow/constraints.py
Python
101
102
103
104
105
106
107
108
@staticmethod
def auto_assign(signal_names: List[str], sides: str = "NSEW") -> List[IOPin]:
    """Auto-assign pins to die edges round-robin."""
    pins = []
    for i, name in enumerate(signal_names):
        side = sides[i % len(sides)]
        pins.append(IOPin(name=name, direction="input", side=side, offset_um=float(i * 10)))
    return pins

IOPin dataclass

Specification for one IO pad.

Source code in src/sc_neurocore/asic_flow/constraints.py
Python
75
76
77
78
79
80
81
82
83
@dataclass
class IOPin:
    """Specification for one IO pad."""

    name: str
    direction: str  # "input", "output", "inout"
    side: str = "N"  # N, S, E, W
    offset_um: float = 0.0
    layer: str = "met3"

IRDropGenerator

Generates IR drop analysis scripts for OpenROAD.

Source code in src/sc_neurocore/asic_flow/constraints.py
Python
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
class IRDropGenerator:
    """Generates IR drop analysis scripts for OpenROAD."""

    @staticmethod
    def generate(pdk: PDKConfig, design: DesignParams, toggle_rate: float = 0.1) -> str:
        """Render OpenROAD power-grid analysis at an input toggle fraction."""
        return textwrap.dedent(f"""\
# SC-NeuroCore IR Drop Analysis — OpenROAD
# Toggle rate: {toggle_rate:.2f}

# Read design
read_lef {pdk.tech_lef}
read_lef {pdk.lef_file}
read_def {design.top_module}_final.def
read_liberty {pdk.liberty_file}
read_sdc constraints_{design.top_module}.sdc

# Set activity
set_power_activity -input -activity {toggle_rate:.3f}

# Analyze IR drop
analyze_power_grid -net {design.power_nets[0]}
analyze_power_grid -net {design.power_nets[1]}

# Report
report_power_grid -net {design.power_nets[0]} -corner tt
""")

generate(pdk, design, toggle_rate=0.1) staticmethod

Render OpenROAD power-grid analysis at an input toggle fraction.

Source code in src/sc_neurocore/asic_flow/constraints.py
Python
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
    @staticmethod
    def generate(pdk: PDKConfig, design: DesignParams, toggle_rate: float = 0.1) -> str:
        """Render OpenROAD power-grid analysis at an input toggle fraction."""
        return textwrap.dedent(f"""\
# SC-NeuroCore IR Drop Analysis — OpenROAD
# Toggle rate: {toggle_rate:.2f}

# Read design
read_lef {pdk.tech_lef}
read_lef {pdk.lef_file}
read_def {design.top_module}_final.def
read_liberty {pdk.liberty_file}
read_sdc constraints_{design.top_module}.sdc

# Set activity
set_power_activity -input -activity {toggle_rate:.3f}

# Analyze IR drop
analyze_power_grid -net {design.power_nets[0]}
analyze_power_grid -net {design.power_nets[1]}

# Report
report_power_grid -net {design.power_nets[0]} -corner tt
""")

LECGenerator

Generates Logic Equivalence Checking scripts.

Source code in src/sc_neurocore/asic_flow/constraints.py
Python
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
class LECGenerator:
    """Generates Logic Equivalence Checking scripts."""

    @staticmethod
    def generate(design: DesignParams) -> str:
        """Render a Yosys equivalence proof between synthesis and routed RTL."""
        return textwrap.dedent(f"""\
# SC-NeuroCore LEC — Yosys equivalence check
# Golden: synth_{design.top_module}.v
# Revised: {design.top_module}_final.v

read_verilog synth_{design.top_module}.v
prep -top {design.top_module}
design -stash golden

read_verilog {design.top_module}_final.v
prep -top {design.top_module}
design -stash revised

design -copy-from golden -as golden {design.top_module}
design -copy-from revised -as revised {design.top_module}

equiv_make golden revised equiv
equiv_simple
equiv_induct
equiv_status -assert
""")

generate(design) staticmethod

Render a Yosys equivalence proof between synthesis and routed RTL.

Source code in src/sc_neurocore/asic_flow/constraints.py
Python
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
    @staticmethod
    def generate(design: DesignParams) -> str:
        """Render a Yosys equivalence proof between synthesis and routed RTL."""
        return textwrap.dedent(f"""\
# SC-NeuroCore LEC — Yosys equivalence check
# Golden: synth_{design.top_module}.v
# Revised: {design.top_module}_final.v

read_verilog synth_{design.top_module}.v
prep -top {design.top_module}
design -stash golden

read_verilog {design.top_module}_final.v
prep -top {design.top_module}
design -stash revised

design -copy-from golden -as golden {design.top_module}
design -copy-from revised -as revised {design.top_module}

equiv_make golden revised equiv
equiv_simple
equiv_induct
equiv_status -assert
""")

FloorplanGenerator

Generates OpenROAD floorplan TCL scripts.

Source code in src/sc_neurocore/asic_flow/decks.py
Python
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
class FloorplanGenerator:
    """Generates OpenROAD floorplan TCL scripts."""

    @staticmethod
    def generate(pdk: PDKConfig, design: DesignParams) -> str:
        """Render the OpenROAD floorplan and optional two-net power grid."""
        die = design.die_area_um
        core = design.core_area_um
        power = design.power_nets

        power_ring = ""
        if len(power) >= 2:
            power_ring = textwrap.dedent(f"""\
# Power grid
add_global_connection -net {power[0]} -pin_pattern "VPWR|VDD|vdd" -power
add_global_connection -net {power[1]} -pin_pattern "VGND|VSS|vss" -ground

set_voltage_domain -name CORE -power {power[0]} -ground {power[1]}

define_pdn_grid -name core_grid -pins {{{power[0]} {power[1]}}} \\
    -voltage_domains CORE

add_pdn_stripe -grid core_grid -layer met1 -width 0.48 -pitch 5.44 \\
    -offset 0 -followpins
add_pdn_stripe -grid core_grid -layer met4 -width 1.6 -pitch 27.14 \\
    -offset 13.57
add_pdn_stripe -grid core_grid -layer met5 -width 1.6 -pitch 27.2 \\
    -offset 13.6

add_pdn_connect -grid core_grid -layers {{met1 met4}}
add_pdn_connect -grid core_grid -layers {{met4 met5}}
""")

        return textwrap.dedent(f"""\
# SC-NeuroCore ASIC Floorplan — OpenROAD
# Die: {die[2] - die[0]}×{die[3] - die[1]} µm

# Read technology
read_lef {pdk.tech_lef}
read_lef {pdk.lef_file}

# Read synthesized netlist
read_verilog synth_{design.top_module}.v
link_design {design.top_module}

# Read timing constraints
read_sdc constraints_{design.top_module}.sdc

# Initialize floorplan
initialize_floorplan \\
    -die_area {{{die[0]} {die[1]} {die[2]} {die[3]}}} \\
    -core_area {{{core[0]} {core[1]} {core[2]} {core[3]}}} \\
    -site unithd

# IO placement
place_pins -hor_layers met3 -ver_layers met2

{power_ring}
""")

generate(pdk, design) staticmethod

Render the OpenROAD floorplan and optional two-net power grid.

Source code in src/sc_neurocore/asic_flow/decks.py
Python
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
    @staticmethod
    def generate(pdk: PDKConfig, design: DesignParams) -> str:
        """Render the OpenROAD floorplan and optional two-net power grid."""
        die = design.die_area_um
        core = design.core_area_um
        power = design.power_nets

        power_ring = ""
        if len(power) >= 2:
            power_ring = textwrap.dedent(f"""\
# Power grid
add_global_connection -net {power[0]} -pin_pattern "VPWR|VDD|vdd" -power
add_global_connection -net {power[1]} -pin_pattern "VGND|VSS|vss" -ground

set_voltage_domain -name CORE -power {power[0]} -ground {power[1]}

define_pdn_grid -name core_grid -pins {{{power[0]} {power[1]}}} \\
    -voltage_domains CORE

add_pdn_stripe -grid core_grid -layer met1 -width 0.48 -pitch 5.44 \\
    -offset 0 -followpins
add_pdn_stripe -grid core_grid -layer met4 -width 1.6 -pitch 27.14 \\
    -offset 13.57
add_pdn_stripe -grid core_grid -layer met5 -width 1.6 -pitch 27.2 \\
    -offset 13.6

add_pdn_connect -grid core_grid -layers {{met1 met4}}
add_pdn_connect -grid core_grid -layers {{met4 met5}}
""")

        return textwrap.dedent(f"""\
# SC-NeuroCore ASIC Floorplan — OpenROAD
# Die: {die[2] - die[0]}×{die[3] - die[1]} µm

# Read technology
read_lef {pdk.tech_lef}
read_lef {pdk.lef_file}

# Read synthesized netlist
read_verilog synth_{design.top_module}.v
link_design {design.top_module}

# Read timing constraints
read_sdc constraints_{design.top_module}.sdc

# Initialize floorplan
initialize_floorplan \\
    -die_area {{{die[0]} {die[1]} {die[2]} {die[3]}}} \\
    -core_area {{{core[0]} {core[1]} {core[2]} {core[3]}}} \\
    -site unithd

# IO placement
place_pins -hor_layers met3 -ver_layers met2

{power_ring}
""")

GDSIIExporter

Generates GDSII stream-out scripts.

Source code in src/sc_neurocore/asic_flow/decks.py
Python
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
class GDSIIExporter:
    """Generates GDSII stream-out scripts."""

    @staticmethod
    def generate(pdk: PDKConfig, design: DesignParams) -> str:
        """Render open-PDK stream-out commands or a vendor-tool boundary."""
        if pdk.is_open_source:
            return textwrap.dedent(f"""\
# SC-NeuroCore GDSII Export — KLayout/Magic
# Merge standard cell GDS with routed DEF

# Option 1: KLayout
klayout -zz -rd design_name={design.top_module} \\
    -rd in_def={design.top_module}_final.def \\
    -rd in_gds="$PDK_ROOT/{pdk.pdk_type.value}/libs.ref/*/gds/*.gds" \\
    -rd seal_gds="" \\
    -rd out_gds={design.top_module}.gds \\
    -rm $OPENLANE_ROOT/scripts/klayout/def2gds.py

# Option 2: Magic
magic -dnull -noconsole << EOF
lef read {pdk.tech_lef}
lef read {pdk.lef_file}
def read {design.top_module}_final.def
load {design.top_module}
select top cell
expand
gds write {design.top_module}.gds
quit
EOF
""")
        return f"# GDSII export for {pdk.pdk_type.value}: use vendor stream-out\n"

generate(pdk, design) staticmethod

Render open-PDK stream-out commands or a vendor-tool boundary.

Source code in src/sc_neurocore/asic_flow/decks.py
Python
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
    @staticmethod
    def generate(pdk: PDKConfig, design: DesignParams) -> str:
        """Render open-PDK stream-out commands or a vendor-tool boundary."""
        if pdk.is_open_source:
            return textwrap.dedent(f"""\
# SC-NeuroCore GDSII Export — KLayout/Magic
# Merge standard cell GDS with routed DEF

# Option 1: KLayout
klayout -zz -rd design_name={design.top_module} \\
    -rd in_def={design.top_module}_final.def \\
    -rd in_gds="$PDK_ROOT/{pdk.pdk_type.value}/libs.ref/*/gds/*.gds" \\
    -rd seal_gds="" \\
    -rd out_gds={design.top_module}.gds \\
    -rm $OPENLANE_ROOT/scripts/klayout/def2gds.py

# Option 2: Magic
magic -dnull -noconsole << EOF
lef read {pdk.tech_lef}
lef read {pdk.lef_file}
def read {design.top_module}_final.def
load {design.top_module}
select top cell
expand
gds write {design.top_module}.gds
quit
EOF
""")
        return f"# GDSII export for {pdk.pdk_type.value}: use vendor stream-out\n"

PlaceRouteGenerator

Generates OpenROAD place-and-route TCL scripts.

Source code in src/sc_neurocore/asic_flow/decks.py
Python
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
class PlaceRouteGenerator:
    """Generates OpenROAD place-and-route TCL scripts."""

    @staticmethod
    def generate(pdk: PDKConfig, design: DesignParams) -> str:
        """Render the OpenROAD placement, clock-tree, and routing script."""
        return textwrap.dedent(f"""\
# SC-NeuroCore ASIC Place & Route — OpenROAD
# Target: {design.top_module}

# Global placement
global_placement -density {design.utilisation:.2f} -pad_left 2 -pad_right 2

# Clock tree synthesis
clock_tree_synthesis -root_buf {pdk.cell_prefix}buf_2 \\
    -buf_list {{{pdk.cell_prefix}buf_4 {pdk.cell_prefix}buf_8}} \\
    -wire_unit 10

# Repair hold violations
estimate_parasitics -placement
repair_timing -hold

# Detailed placement
detailed_placement

# Check placement
check_placement

# Filler cell insertion
filler_placement {pdk.cell_prefix}fill_1 {pdk.cell_prefix}fill_2

# Global routing
global_route -guide_file route_{design.top_module}.guide \\
    -congestion_iterations 30

# Detailed routing
detailed_route -output_drc route_drc_{design.top_module}.rpt \\
    -output_maze route_maze_{design.top_module}.log

# Write outputs
write_def {design.top_module}_final.def
write_verilog {design.top_module}_final.v
""")

generate(pdk, design) staticmethod

Render the OpenROAD placement, clock-tree, and routing script.

Source code in src/sc_neurocore/asic_flow/decks.py
Python
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
    @staticmethod
    def generate(pdk: PDKConfig, design: DesignParams) -> str:
        """Render the OpenROAD placement, clock-tree, and routing script."""
        return textwrap.dedent(f"""\
# SC-NeuroCore ASIC Place & Route — OpenROAD
# Target: {design.top_module}

# Global placement
global_placement -density {design.utilisation:.2f} -pad_left 2 -pad_right 2

# Clock tree synthesis
clock_tree_synthesis -root_buf {pdk.cell_prefix}buf_2 \\
    -buf_list {{{pdk.cell_prefix}buf_4 {pdk.cell_prefix}buf_8}} \\
    -wire_unit 10

# Repair hold violations
estimate_parasitics -placement
repair_timing -hold

# Detailed placement
detailed_placement

# Check placement
check_placement

# Filler cell insertion
filler_placement {pdk.cell_prefix}fill_1 {pdk.cell_prefix}fill_2

# Global routing
global_route -guide_file route_{design.top_module}.guide \\
    -congestion_iterations 30

# Detailed routing
detailed_route -output_drc route_drc_{design.top_module}.rpt \\
    -output_maze route_maze_{design.top_module}.log

# Write outputs
write_def {design.top_module}_final.def
write_verilog {design.top_module}_final.v
""")

SDCGenerator

Generates Synopsys Design Constraints (SDC) for STA.

Source code in src/sc_neurocore/asic_flow/decks.py
Python
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
class SDCGenerator:
    """Generates Synopsys Design Constraints (SDC) for STA."""

    @staticmethod
    def generate(pdk: PDKConfig, design: DesignParams) -> str:
        """Render clock, IO-delay, reset, fanout, and load constraints."""
        period = design.clock_period_ns
        return textwrap.dedent(f"""\
# SC-NeuroCore ASIC Constraints — SDC
# Clock: {design.clock_name} @ {design.target_frequency_mhz} MHz

create_clock [get_ports {design.clock_name}] \\
    -name {design.clock_name} \\
    -period {period:.3f}

# Clock uncertainty (10% of period)
set_clock_uncertainty {period * 0.1:.3f} [get_clocks {design.clock_name}]

# Input/output delays (25% of period)
set_input_delay {period * 0.25:.3f} -clock {design.clock_name} [all_inputs]
set_output_delay {period * 0.25:.3f} -clock {design.clock_name} [all_outputs]

# Reset is constant during operation
set_false_path -from [get_ports {design.reset_name}]

# Don't touch clock/reset nets
set_dont_touch_network [get_ports {design.clock_name}]

# Max transition / fanout
set_max_transition {period * 0.15:.3f} [current_design]
set_max_fanout {design.sc_optimisation.max_fanout} [current_design]

# Driving cell
set_driving_cell -lib_cell {pdk.cell_prefix}buf_2 [all_inputs]

# Load
set_load 0.05 [all_outputs]
""")

generate(pdk, design) staticmethod

Render clock, IO-delay, reset, fanout, and load constraints.

Source code in src/sc_neurocore/asic_flow/decks.py
Python
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
    @staticmethod
    def generate(pdk: PDKConfig, design: DesignParams) -> str:
        """Render clock, IO-delay, reset, fanout, and load constraints."""
        period = design.clock_period_ns
        return textwrap.dedent(f"""\
# SC-NeuroCore ASIC Constraints — SDC
# Clock: {design.clock_name} @ {design.target_frequency_mhz} MHz

create_clock [get_ports {design.clock_name}] \\
    -name {design.clock_name} \\
    -period {period:.3f}

# Clock uncertainty (10% of period)
set_clock_uncertainty {period * 0.1:.3f} [get_clocks {design.clock_name}]

# Input/output delays (25% of period)
set_input_delay {period * 0.25:.3f} -clock {design.clock_name} [all_inputs]
set_output_delay {period * 0.25:.3f} -clock {design.clock_name} [all_outputs]

# Reset is constant during operation
set_false_path -from [get_ports {design.reset_name}]

# Don't touch clock/reset nets
set_dont_touch_network [get_ports {design.clock_name}]

# Max transition / fanout
set_max_transition {period * 0.15:.3f} [current_design]
set_max_fanout {design.sc_optimisation.max_fanout} [current_design]

# Driving cell
set_driving_cell -lib_cell {pdk.cell_prefix}buf_2 [all_inputs]

# Load
set_load 0.05 [all_outputs]
""")

SynthesisGenerator

Generates Yosys synthesis TCL scripts.

Source code in src/sc_neurocore/asic_flow/decks.py
Python
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
class SynthesisGenerator:
    """Generates Yosys synthesis TCL scripts."""

    @staticmethod
    def generate(pdk: PDKConfig, design: DesignParams) -> str:
        """Render the Yosys synthesis script for ``design`` and ``pdk``."""
        rtl_reads = "\n".join(f"read_verilog {f}" for f in design.rtl_files)
        if not rtl_reads:
            rtl_reads = f"read_verilog {design.top_module}.v"
        sc_passes = "\n".join(design.sc_optimisation.yosys_passes())
        if design.sc_optimisation.preserve_lfsr_hierarchy:
            sc_passes = (
                "# Preserve deterministic SC seed generators for gate-level debug\n"
                "setattr -mod -pattern *lfsr* keep_hierarchy 1\n"
                f"{sc_passes}"
            )
        abc_delay_ps = design.clock_period_ns * 1000.0 * design.sc_optimisation.abc_delay_margin

        return textwrap.dedent(f"""\
# SC-NeuroCore ASIC Synthesis — Yosys Script
# PDK: {pdk.pdk_type.value}
# Target: {design.top_module} @ {design.target_frequency_mhz} MHz

# Read RTL
{rtl_reads}

# Hierarchy check
hierarchy -check -top {design.top_module}

# High-level synthesis
proc; opt; fsm; opt; memory; opt

# Technology mapping
synth -top {design.top_module}

# SC-aware optimisation
{sc_passes}

# Map to standard cells
dfflibmap -liberty {pdk.liberty_file}
abc -liberty {pdk.liberty_file} -D {abc_delay_ps:.0f}

# Clean up
opt_clean -purge

# Write outputs
write_verilog -noattr synth_{design.top_module}.v
write_json synth_{design.top_module}.json

# Statistics
stat -liberty {pdk.liberty_file}
""")

generate(pdk, design) staticmethod

Render the Yosys synthesis script for design and pdk.

Source code in src/sc_neurocore/asic_flow/decks.py
Python
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
    @staticmethod
    def generate(pdk: PDKConfig, design: DesignParams) -> str:
        """Render the Yosys synthesis script for ``design`` and ``pdk``."""
        rtl_reads = "\n".join(f"read_verilog {f}" for f in design.rtl_files)
        if not rtl_reads:
            rtl_reads = f"read_verilog {design.top_module}.v"
        sc_passes = "\n".join(design.sc_optimisation.yosys_passes())
        if design.sc_optimisation.preserve_lfsr_hierarchy:
            sc_passes = (
                "# Preserve deterministic SC seed generators for gate-level debug\n"
                "setattr -mod -pattern *lfsr* keep_hierarchy 1\n"
                f"{sc_passes}"
            )
        abc_delay_ps = design.clock_period_ns * 1000.0 * design.sc_optimisation.abc_delay_margin

        return textwrap.dedent(f"""\
# SC-NeuroCore ASIC Synthesis — Yosys Script
# PDK: {pdk.pdk_type.value}
# Target: {design.top_module} @ {design.target_frequency_mhz} MHz

# Read RTL
{rtl_reads}

# Hierarchy check
hierarchy -check -top {design.top_module}

# High-level synthesis
proc; opt; fsm; opt; memory; opt

# Technology mapping
synth -top {design.top_module}

# SC-aware optimisation
{sc_passes}

# Map to standard cells
dfflibmap -liberty {pdk.liberty_file}
abc -liberty {pdk.liberty_file} -D {abc_delay_ps:.0f}

# Clean up
opt_clean -purge

# Write outputs
write_verilog -noattr synth_{design.top_module}.v
write_json synth_{design.top_module}.json

# Statistics
stat -liberty {pdk.liberty_file}
""")

DesignParams dataclass

ASIC design parameters.

Source code in src/sc_neurocore/asic_flow/design.py
Python
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
@dataclass
class DesignParams:
    """ASIC design parameters."""

    top_module: str = "sc_neurocore_top"
    clock_name: str = "clk"
    reset_name: str = "rst_n"
    reset_active_low: bool = True
    target_frequency_mhz: float = 100.0
    die_area_um: Tuple[float, float, float, float] = (0, 0, 500, 500)
    core_area_um: Tuple[float, float, float, float] = (20, 20, 480, 480)
    utilisation: float = 0.5
    aspect_ratio: float = 1.0
    io_margin_um: float = 20.0
    power_nets: List[str] = field(default_factory=lambda: ["VDD", "VSS"])
    rtl_files: List[str] = field(default_factory=list)
    sc_optimisation: SCASICOptimisationConfig = field(default_factory=SCASICOptimisationConfig)

    @property
    def clock_period_ns(self) -> float:
        """Return the target clock period in nanoseconds."""
        return 1000.0 / self.target_frequency_mhz

    @property
    def die_width_um(self) -> float:
        """Return the die width in micrometres."""
        return self.die_area_um[2] - self.die_area_um[0]

    @property
    def die_height_um(self) -> float:
        """Return the die height in micrometres."""
        return self.die_area_um[3] - self.die_area_um[1]

    @property
    def core_area_mm2(self) -> float:
        """Return the rectangular core area in square millimetres."""
        w = self.core_area_um[2] - self.core_area_um[0]
        h = self.core_area_um[3] - self.core_area_um[1]
        return (w * h) / 1e6

clock_period_ns property

Return the target clock period in nanoseconds.

die_width_um property

Return the die width in micrometres.

die_height_um property

Return the die height in micrometres.

core_area_mm2 property

Return the rectangular core area in square millimetres.

SCASICOptimisationConfig dataclass

SC-specific synthesis settings for stochastic neuromorphic datapaths.

Source code in src/sc_neurocore/asic_flow/design.py
Python
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@dataclass(frozen=True)
class SCASICOptimisationConfig:
    """SC-specific synthesis settings for stochastic neuromorphic datapaths."""

    share_stochastic_counters: bool = True
    reduce_constant_widths: bool = True
    preserve_lfsr_hierarchy: bool = True
    max_fanout: int = 16
    abc_delay_margin: float = 0.90

    def yosys_passes(self) -> List[str]:
        """Return the ordered Yosys passes selected for the SC datapath."""
        passes: List[str] = []
        if self.reduce_constant_widths:
            passes.append("wreduce")
        if self.share_stochastic_counters:
            passes.extend(["share", "opt_share"])
        passes.append("opt_clean -purge")
        return passes

yosys_passes()

Return the ordered Yosys passes selected for the SC datapath.

Source code in src/sc_neurocore/asic_flow/design.py
Python
27
28
29
30
31
32
33
34
35
def yosys_passes(self) -> List[str]:
    """Return the ordered Yosys passes selected for the SC datapath."""
    passes: List[str] = []
    if self.reduce_constant_widths:
        passes.append("wreduce")
    if self.share_stochastic_counters:
        passes.extend(["share", "opt_share"])
    passes.append("opt_clean -purge")
    return passes

DesignEstimate dataclass

Uncalibrated pre-synthesis screening estimate for an SC module.

Source code in src/sc_neurocore/asic_flow/estimation.py
Python
18
19
20
21
22
23
24
25
26
27
28
@dataclass
class DesignEstimate:
    """Uncalibrated pre-synthesis screening estimate for an SC module."""

    module_name: str
    gate_count: int
    area_um2: float
    dynamic_power_mw: float
    leakage_power_mw: float
    critical_path_ns: float
    max_frequency_mhz: float

PreSynthEstimator

Compute deterministic screening values before synthesis.

The legacy coefficients are architectural scaling assumptions, not foundry-characterised PPA models:

  • Bitstream ops: ~10 gates/bit
  • LIF neuron: ~500 gates
  • STDP synapse: ~200 gates
  • AER router: ~100 gates/port

Outputs support relative design screening only. They are not physical evidence and must not be presented as post-synthesis or signoff results.

Source code in src/sc_neurocore/asic_flow/estimation.py
Python
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
class PreSynthEstimator:
    """Compute deterministic screening values before synthesis.

    The legacy coefficients are architectural scaling assumptions, not
    foundry-characterised PPA models:

    - Bitstream ops: ~10 gates/bit
    - LIF neuron: ~500 gates
    - STDP synapse: ~200 gates
    - AER router: ~100 gates/port

    Outputs support relative design screening only. They are not physical
    evidence and must not be presented as post-synthesis or signoff results.
    """

    GATES_PER_BIT = 10
    GATES_PER_LIF = 500
    GATES_PER_SYNAPSE = 200
    GATES_PER_AER_PORT = 100

    @classmethod
    def estimate(
        cls,
        n_neurons: int,
        n_synapses: int,
        bitstream_width: int,
        n_aer_ports: int,
        pdk: PDKConfig,
    ) -> DesignEstimate:
        """Estimate design metrics from architectural parameters."""
        gates = (
            n_neurons * cls.GATES_PER_LIF
            + n_synapses * cls.GATES_PER_SYNAPSE
            + bitstream_width * cls.GATES_PER_BIT
            + n_aer_ports * cls.GATES_PER_AER_PORT
        )

        # Legacy screening scale: 1 µm²/gate at 130 nm, quadratic by feature size.
        scale = (pdk.min_feature_nm / 130.0) ** 2
        area = gates * 1.0 * scale

        # Legacy screening scales: 1 µW/gate dynamic and 0.01 µW/gate leakage.
        freq_scale = 100.0 / (1000.0 / pdk.clock_period_ns)
        v_scale = (pdk.voltage_v / 1.8) ** 2
        dynamic = gates * 1e-3 * freq_scale * v_scale  # mW
        leakage = gates * 1e-5 * scale  # mW

        # Legacy critical-path screening scale, normalised to the 130 nm preset.
        cp = max(1.0, 10 + 0.01 * n_neurons) * (pdk.min_feature_nm / 130.0)
        max_freq = 1000.0 / cp

        return DesignEstimate(
            module_name="sc_neurocore_top",
            gate_count=gates,
            area_um2=area,
            dynamic_power_mw=dynamic,
            leakage_power_mw=leakage,
            critical_path_ns=cp,
            max_frequency_mhz=max_freq,
        )

estimate(n_neurons, n_synapses, bitstream_width, n_aer_ports, pdk) classmethod

Estimate design metrics from architectural parameters.

Source code in src/sc_neurocore/asic_flow/estimation.py
Python
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
@classmethod
def estimate(
    cls,
    n_neurons: int,
    n_synapses: int,
    bitstream_width: int,
    n_aer_ports: int,
    pdk: PDKConfig,
) -> DesignEstimate:
    """Estimate design metrics from architectural parameters."""
    gates = (
        n_neurons * cls.GATES_PER_LIF
        + n_synapses * cls.GATES_PER_SYNAPSE
        + bitstream_width * cls.GATES_PER_BIT
        + n_aer_ports * cls.GATES_PER_AER_PORT
    )

    # Legacy screening scale: 1 µm²/gate at 130 nm, quadratic by feature size.
    scale = (pdk.min_feature_nm / 130.0) ** 2
    area = gates * 1.0 * scale

    # Legacy screening scales: 1 µW/gate dynamic and 0.01 µW/gate leakage.
    freq_scale = 100.0 / (1000.0 / pdk.clock_period_ns)
    v_scale = (pdk.voltage_v / 1.8) ** 2
    dynamic = gates * 1e-3 * freq_scale * v_scale  # mW
    leakage = gates * 1e-5 * scale  # mW

    # Legacy critical-path screening scale, normalised to the 130 nm preset.
    cp = max(1.0, 10 + 0.01 * n_neurons) * (pdk.min_feature_nm / 130.0)
    max_freq = 1000.0 / cp

    return DesignEstimate(
        module_name="sc_neurocore_top",
        gate_count=gates,
        area_um2=area,
        dynamic_power_mw=dynamic,
        leakage_power_mw=leakage,
        critical_path_ns=cp,
        max_frequency_mhz=max_freq,
    )

ASICFlowBundle dataclass

Generated ASIC flow files plus the evidence manifest path.

Source code in src/sc_neurocore/asic_flow/flow.py
Python
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
@dataclass(frozen=True)
class ASICFlowBundle:
    """Generated ASIC flow files plus the evidence manifest path."""

    output_dir: str
    manifest_path: str
    file_paths: Dict[str, str]
    pdk_resolution: PDKResolution
    estimate: DesignEstimate

    def to_dict(self) -> Dict[str, Any]:
        """Serialise bundle paths, PDK resolution, and screening estimate."""
        return {
            "output_dir": self.output_dir,
            "manifest_path": self.manifest_path,
            "file_paths": dict(self.file_paths),
            "pdk_resolution": {
                "pdk": _pdk_to_manifest(self.pdk_resolution.pdk),
                "files": asdict(self.pdk_resolution.files),
                "missing_required": list(self.pdk_resolution.missing_required),
                "missing_optional": list(self.pdk_resolution.missing_optional),
                "usable_for_synthesis": self.pdk_resolution.usable_for_synthesis,
                "usable_for_signoff": self.pdk_resolution.usable_for_signoff,
            },
            "estimate": asdict(self.estimate),
        }

to_dict()

Serialise bundle paths, PDK resolution, and screening estimate.

Source code in src/sc_neurocore/asic_flow/flow.py
Python
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
def to_dict(self) -> Dict[str, Any]:
    """Serialise bundle paths, PDK resolution, and screening estimate."""
    return {
        "output_dir": self.output_dir,
        "manifest_path": self.manifest_path,
        "file_paths": dict(self.file_paths),
        "pdk_resolution": {
            "pdk": _pdk_to_manifest(self.pdk_resolution.pdk),
            "files": asdict(self.pdk_resolution.files),
            "missing_required": list(self.pdk_resolution.missing_required),
            "missing_optional": list(self.pdk_resolution.missing_optional),
            "usable_for_synthesis": self.pdk_resolution.usable_for_synthesis,
            "usable_for_signoff": self.pdk_resolution.usable_for_signoff,
        },
        "estimate": asdict(self.estimate),
    }

ASICFlowGenerator

Top-level generator for the complete ASIC tape-out pipeline.

Source code in src/sc_neurocore/asic_flow/flow.py
Python
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
class ASICFlowGenerator:
    """Top-level generator for the complete ASIC tape-out pipeline."""

    def generate(
        self,
        pdk: PDKConfig,
        design: DesignParams,
    ) -> ASICFlowOutput:
        """Generate all deterministic decks for one PDK/design pair.

        Parameters
        ----------
        pdk:
            Resolved process configuration used by every deck.
        design:
            Logical and physical design parameters.

        Returns
        -------
        ASICFlowOutput
            Nine generated files without executing external EDA tools.
        """
        synth = SynthesisGenerator.generate(pdk, design)
        sdc = SDCGenerator.generate(pdk, design)
        fp = FloorplanGenerator.generate(pdk, design)
        pnr = PlaceRouteGenerator.generate(pdk, design)
        sta = SignoffGenerator.generate_sta_script(pdk, design)
        drc = SignoffGenerator.generate_drc_script(pdk, design)
        lvs = SignoffGenerator.generate_lvs_script(pdk, design)
        gdsii = GDSIIExporter.generate(pdk, design)
        makefile = self._generate_makefile(design)

        filelist = list(
            ASICFlowOutput(synth, sdc, fp, pnr, sta, drc, lvs, gdsii, makefile, []).to_dict().keys()
        )

        return ASICFlowOutput(synth, sdc, fp, pnr, sta, drc, lvs, gdsii, makefile, filelist)

    def _generate_makefile(self, design: DesignParams) -> str:
        return textwrap.dedent(f"""\
# SC-NeuroCore ASIC Flow — Makefile
# Usage: make all

TOP = {design.top_module}

.PHONY: all synth floorplan pnr sta drc lvs gdsii clean

all: synth floorplan pnr sta drc lvs gdsii

synth:
\tyosys -c synth.tcl 2>&1 | tee logs/synth.log

floorplan: synth
\topenroad -exit floorplan.tcl 2>&1 | tee logs/floorplan.log

pnr: floorplan
\topenroad -exit pnr.tcl 2>&1 | tee logs/pnr.log

sta: pnr
\tsta sta.tcl 2>&1 | tee logs/sta.log

drc: gdsii
\tpython3 drc_check.py 2>&1 | tee logs/drc.log

lvs: pnr
\tbash lvs_check.sh 2>&1 | tee logs/lvs.log

gdsii: pnr
\tbash gdsii_export.sh 2>&1 | tee logs/gdsii.log

clean:
\trm -rf synth_$(TOP).* $(TOP)_final.* $(TOP).gds logs/
""")

generate(pdk, design)

Generate all deterministic decks for one PDK/design pair.

Parameters

pdk: Resolved process configuration used by every deck. design: Logical and physical design parameters.

Returns

ASICFlowOutput Nine generated files without executing external EDA tools.

Source code in src/sc_neurocore/asic_flow/flow.py
Python
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
def generate(
    self,
    pdk: PDKConfig,
    design: DesignParams,
) -> ASICFlowOutput:
    """Generate all deterministic decks for one PDK/design pair.

    Parameters
    ----------
    pdk:
        Resolved process configuration used by every deck.
    design:
        Logical and physical design parameters.

    Returns
    -------
    ASICFlowOutput
        Nine generated files without executing external EDA tools.
    """
    synth = SynthesisGenerator.generate(pdk, design)
    sdc = SDCGenerator.generate(pdk, design)
    fp = FloorplanGenerator.generate(pdk, design)
    pnr = PlaceRouteGenerator.generate(pdk, design)
    sta = SignoffGenerator.generate_sta_script(pdk, design)
    drc = SignoffGenerator.generate_drc_script(pdk, design)
    lvs = SignoffGenerator.generate_lvs_script(pdk, design)
    gdsii = GDSIIExporter.generate(pdk, design)
    makefile = self._generate_makefile(design)

    filelist = list(
        ASICFlowOutput(synth, sdc, fp, pnr, sta, drc, lvs, gdsii, makefile, []).to_dict().keys()
    )

    return ASICFlowOutput(synth, sdc, fp, pnr, sta, drc, lvs, gdsii, makefile, filelist)

ASICFlowOutput dataclass

Complete output of the ASIC tape-out flow.

Source code in src/sc_neurocore/asic_flow/flow.py
Python
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@dataclass
class ASICFlowOutput:
    """Complete output of the ASIC tape-out flow."""

    synth_tcl: str
    sdc: str
    floorplan_tcl: str
    pnr_tcl: str
    sta_tcl: str
    drc_script: str
    lvs_script: str
    gdsii_script: str
    makefile: str
    filelist: List[str]

    def to_dict(self) -> Dict[str, str]:
        """Map canonical bundle filenames to their generated contents."""
        return {
            "synth.tcl": self.synth_tcl,
            "constraints.sdc": self.sdc,
            "floorplan.tcl": self.floorplan_tcl,
            "pnr.tcl": self.pnr_tcl,
            "sta.tcl": self.sta_tcl,
            "drc_check.py": self.drc_script,
            "lvs_check.sh": self.lvs_script,
            "gdsii_export.sh": self.gdsii_script,
            "Makefile": self.makefile,
        }

to_dict()

Map canonical bundle filenames to their generated contents.

Source code in src/sc_neurocore/asic_flow/flow.py
Python
52
53
54
55
56
57
58
59
60
61
62
63
64
def to_dict(self) -> Dict[str, str]:
    """Map canonical bundle filenames to their generated contents."""
    return {
        "synth.tcl": self.synth_tcl,
        "constraints.sdc": self.sdc,
        "floorplan.tcl": self.floorplan_tcl,
        "pnr.tcl": self.pnr_tcl,
        "sta.tcl": self.sta_tcl,
        "drc_check.py": self.drc_script,
        "lvs_check.sh": self.lvs_script,
        "gdsii_export.sh": self.gdsii_script,
        "Makefile": self.makefile,
    }

BlockConfig dataclass

One block in a hierarchical ASIC flow.

Source code in src/sc_neurocore/asic_flow/hierarchy.py
Python
21
22
23
24
25
26
27
28
@dataclass
class BlockConfig:
    """One block in a hierarchical ASIC flow."""

    name: str
    design: DesignParams
    is_hard_macro: bool = False
    abstract_lef: str = ""

HierarchicalFlow dataclass

Multi-block ASIC flow with per-block synthesis + top integration.

Source code in src/sc_neurocore/asic_flow/hierarchy.py
Python
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@dataclass
class HierarchicalFlow:
    """Multi-block ASIC flow with per-block synthesis + top integration."""

    top_design: DesignParams
    blocks: List[BlockConfig] = field(default_factory=list)

    def add_block(self, block: BlockConfig) -> None:
        """Append one logical or hard-macro block to the flow."""
        self.blocks.append(block)

    def block_names(self) -> List[str]:
        """Return block names in deterministic insertion order."""
        return [b.name for b in self.blocks]

    def generate_block_scripts(self, pdk: PDKConfig) -> Dict[str, str]:
        """Generate one Yosys synthesis script per configured block."""
        gen = ASICFlowGenerator()
        result = {}
        for block in self.blocks:
            output = gen.generate(pdk, block.design)
            result[block.name] = output.synth_tcl
        return result

    def generate_top_integration(self, pdk: PDKConfig) -> str:
        """Render top-level netlist linkage and hard-macro LEF reads."""
        lines = [f"# Hierarchical integration for {self.top_design.top_module}"]
        for block in self.blocks:
            if block.is_hard_macro and block.abstract_lef:
                lines.append(f"read_lef {block.abstract_lef}  ;# macro: {block.name}")
        lines.append(f"read_verilog synth_{self.top_design.top_module}.v")
        lines.append(f"link_design {self.top_design.top_module}")
        return "\n".join(lines) + "\n"

add_block(block)

Append one logical or hard-macro block to the flow.

Source code in src/sc_neurocore/asic_flow/hierarchy.py
Python
38
39
40
def add_block(self, block: BlockConfig) -> None:
    """Append one logical or hard-macro block to the flow."""
    self.blocks.append(block)

block_names()

Return block names in deterministic insertion order.

Source code in src/sc_neurocore/asic_flow/hierarchy.py
Python
42
43
44
def block_names(self) -> List[str]:
    """Return block names in deterministic insertion order."""
    return [b.name for b in self.blocks]

generate_block_scripts(pdk)

Generate one Yosys synthesis script per configured block.

Source code in src/sc_neurocore/asic_flow/hierarchy.py
Python
46
47
48
49
50
51
52
53
def generate_block_scripts(self, pdk: PDKConfig) -> Dict[str, str]:
    """Generate one Yosys synthesis script per configured block."""
    gen = ASICFlowGenerator()
    result = {}
    for block in self.blocks:
        output = gen.generate(pdk, block.design)
        result[block.name] = output.synth_tcl
    return result

generate_top_integration(pdk)

Render top-level netlist linkage and hard-macro LEF reads.

Source code in src/sc_neurocore/asic_flow/hierarchy.py
Python
55
56
57
58
59
60
61
62
63
def generate_top_integration(self, pdk: PDKConfig) -> str:
    """Render top-level netlist linkage and hard-macro LEF reads."""
    lines = [f"# Hierarchical integration for {self.top_design.top_module}"]
    for block in self.blocks:
        if block.is_hard_macro and block.abstract_lef:
            lines.append(f"read_lef {block.abstract_lef}  ;# macro: {block.name}")
    lines.append(f"read_verilog synth_{self.top_design.top_module}.v")
    lines.append(f"link_design {self.top_design.top_module}")
    return "\n".join(lines) + "\n"

OpenSourcePDKResolver

Resolve Sky130/GF180 file locations without requiring OpenLane at import time.

Source code in src/sc_neurocore/asic_flow/pdk.py
Python
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
class OpenSourcePDKResolver:
    """Resolve Sky130/GF180 file locations without requiring OpenLane at import time."""

    @staticmethod
    def resolve(
        pdk: PDKConfig,
        pdk_root: Optional[str] = None,
        require_existing: bool = False,
    ) -> PDKResolution:
        """Bind ``$PDK_ROOT`` and report missing PDK artefacts.

        Parameters
        ----------
        pdk:
            PDK preset or custom configuration.
        pdk_root:
            Explicit PDK root. If absent, ``PDK_ROOT`` then ``PDKPATH`` are used.
        require_existing:
            When true, missing required files are reported as blockers. When false,
            paths are still resolved so generated flow decks are deterministic.
        """
        root = pdk_root or os.environ.get("PDK_ROOT") or os.environ.get("PDKPATH") or "$PDK_ROOT"
        resolved_pdk = pdk.with_pdk_root(root)
        files = OpenSourcePDKResolver._file_manifest(resolved_pdk)

        missing_required: Tuple[str, ...] = ()
        missing_optional: Tuple[str, ...] = ()
        if require_existing:
            missing_required = tuple(
                name for name, path in files.required_paths().items() if not Path(path).exists()
            )
            missing_optional = tuple(
                name
                for name, path in files.optional_paths().items()
                if path and not Path(path).exists()
            )

        return PDKResolution(resolved_pdk, files, missing_required, missing_optional)

    @staticmethod
    def _file_manifest(pdk: PDKConfig) -> ResolvedPDKFiles:
        if pdk.pdk_type == PDKType.SKY130:
            root = OpenSourcePDKResolver._pdk_root_from_path(pdk.liberty_file, "sky130A")
            return ResolvedPDKFiles(
                liberty_file=pdk.liberty_file,
                lef_file=pdk.lef_file,
                tech_lef=pdk.tech_lef,
                setup_tcl=f"{root}/sky130A/libs.tech/netgen/sky130A_setup.tcl",
                drc_deck=f"{root}/sky130A/libs.tech/klayout/drc/sky130.lydrc",
                lvs_setup=f"{root}/sky130A/libs.tech/netgen/sky130A_setup.tcl",
            )
        if pdk.pdk_type == PDKType.GF180MCU:
            root = OpenSourcePDKResolver._pdk_root_from_path(pdk.liberty_file, "gf180mcuD")
            return ResolvedPDKFiles(
                liberty_file=pdk.liberty_file,
                lef_file=pdk.lef_file,
                tech_lef=pdk.tech_lef,
                setup_tcl=f"{root}/gf180mcuD/libs.tech/netgen/gf180mcuD_setup.tcl",
                drc_deck=f"{root}/gf180mcuD/libs.tech/klayout/drc/gf180mcu.drc",
                lvs_setup=f"{root}/gf180mcuD/libs.tech/netgen/gf180mcuD_setup.tcl",
            )
        return ResolvedPDKFiles(
            liberty_file=pdk.liberty_file,
            lef_file=pdk.lef_file,
            tech_lef=pdk.tech_lef,
        )

    @staticmethod
    def _pdk_root_from_path(path: str, marker: str) -> str:
        before, separator, _after = path.partition(f"/{marker}/")
        return before if separator else "$PDK_ROOT"

resolve(pdk, pdk_root=None, require_existing=False) staticmethod

Bind $PDK_ROOT and report missing PDK artefacts.

Parameters

pdk: PDK preset or custom configuration. pdk_root: Explicit PDK root. If absent, PDK_ROOT then PDKPATH are used. require_existing: When true, missing required files are reported as blockers. When false, paths are still resolved so generated flow decks are deterministic.

Source code in src/sc_neurocore/asic_flow/pdk.py
Python
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
@staticmethod
def resolve(
    pdk: PDKConfig,
    pdk_root: Optional[str] = None,
    require_existing: bool = False,
) -> PDKResolution:
    """Bind ``$PDK_ROOT`` and report missing PDK artefacts.

    Parameters
    ----------
    pdk:
        PDK preset or custom configuration.
    pdk_root:
        Explicit PDK root. If absent, ``PDK_ROOT`` then ``PDKPATH`` are used.
    require_existing:
        When true, missing required files are reported as blockers. When false,
        paths are still resolved so generated flow decks are deterministic.
    """
    root = pdk_root or os.environ.get("PDK_ROOT") or os.environ.get("PDKPATH") or "$PDK_ROOT"
    resolved_pdk = pdk.with_pdk_root(root)
    files = OpenSourcePDKResolver._file_manifest(resolved_pdk)

    missing_required: Tuple[str, ...] = ()
    missing_optional: Tuple[str, ...] = ()
    if require_existing:
        missing_required = tuple(
            name for name, path in files.required_paths().items() if not Path(path).exists()
        )
        missing_optional = tuple(
            name
            for name, path in files.optional_paths().items()
            if path and not Path(path).exists()
        )

    return PDKResolution(resolved_pdk, files, missing_required, missing_optional)

PDKConfig dataclass

Process Design Kit configuration.

Source code in src/sc_neurocore/asic_flow/pdk.py
Python
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
@dataclass
class PDKConfig:
    """Process Design Kit configuration."""

    pdk_type: PDKType = PDKType.SKY130
    liberty_file: str = ""
    lef_file: str = ""
    tech_lef: str = ""
    cell_prefix: str = "sky130_fd_sc_hd__"
    clock_period_ns: float = 10.0
    voltage_v: float = 1.8
    temperature_c: float = 25.0
    corner: str = "tt"
    metal_layers: int = 5
    min_feature_nm: int = 130

    @classmethod
    def from_pdk_type(cls, pdk: PDKType) -> PDKConfig:
        """Construct the maintained preset for a process family.

        Parameters
        ----------
        pdk:
            Process family whose nominal files, voltage, and geometry are
            required.

        Returns
        -------
        PDKConfig
            Configuration containing deterministic ``$PDK_ROOT`` templates.
        """
        presets: Dict[PDKType, Dict[str, Any]] = {
            PDKType.SKY130: dict(
                liberty_file="$PDK_ROOT/sky130A/libs.ref/sky130_fd_sc_hd/lib/sky130_fd_sc_hd__tt_025C_1v80.lib",
                lef_file="$PDK_ROOT/sky130A/libs.ref/sky130_fd_sc_hd/lef/sky130_fd_sc_hd.lef",
                tech_lef="$PDK_ROOT/sky130A/libs.ref/sky130_fd_sc_hd/techlef/sky130_fd_sc_hd__nom.tlef",
                cell_prefix="sky130_fd_sc_hd__",
                clock_period_ns=10.0,
                voltage_v=1.8,
                metal_layers=5,
                min_feature_nm=130,
            ),
            PDKType.GF180MCU: dict(
                liberty_file="$PDK_ROOT/gf180mcuD/libs.ref/gf180mcu_fd_sc_mcu7t5v0/lib/gf180mcu_fd_sc_mcu7t5v0__tt_025C_3v30.lib",
                lef_file="$PDK_ROOT/gf180mcuD/libs.ref/gf180mcu_fd_sc_mcu7t5v0/lef/gf180mcu_fd_sc_mcu7t5v0.lef",
                tech_lef="$PDK_ROOT/gf180mcuD/libs.ref/gf180mcu_fd_sc_mcu7t5v0/techlef/gf180mcu_fd_sc_mcu7t5v0__nom.tlef",
                cell_prefix="gf180mcu_fd_sc_mcu7t5v0__",
                clock_period_ns=15.0,
                voltage_v=3.3,
                metal_layers=6,
                min_feature_nm=180,
            ),
            PDKType.TSMC28: dict(
                liberty_file="$PDK_ROOT/tsmc28/tcbn28hpcplusbwp7t30p140_110a/TSMCHOME/digital/Front_End/timing_power_noise/NLDM/tcbn28hpcplusbwp7t30p140ssgnp0p81v125c.lib",
                lef_file="$PDK_ROOT/tsmc28/lef/tcbn28hpcplusbwp7t30p140.lef",
                tech_lef="$PDK_ROOT/tsmc28/lef/HiPe_M10.tlef",
                cell_prefix="TSMC_",
                clock_period_ns=2.0,
                voltage_v=0.9,
                metal_layers=10,
                min_feature_nm=28,
            ),
            PDKType.INTEL16: dict(
                liberty_file="$PDK_ROOT/intel16/lib/intel16_sc.lib",
                lef_file="$PDK_ROOT/intel16/lef/intel16_sc.lef",
                tech_lef="$PDK_ROOT/intel16/lef/intel16.tlef",
                cell_prefix="INTEL16_",
                clock_period_ns=1.5,
                voltage_v=0.8,
                metal_layers=12,
                min_feature_nm=16,
            ),
            PDKType.CUSTOM: dict(
                liberty_file="",
                lef_file="",
                tech_lef="",
                cell_prefix="",
                clock_period_ns=10.0,
                voltage_v=1.8,
                metal_layers=5,
                min_feature_nm=130,
            ),
        }
        return cls(pdk_type=pdk, **presets[pdk])

    @property
    def is_open_source(self) -> bool:
        """Return whether the process has a maintained open-source file map."""
        return self.pdk_type in (PDKType.SKY130, PDKType.GF180MCU)

    def with_pdk_root(self, pdk_root: str) -> PDKConfig:
        """Return a copy with ``$PDK_ROOT`` variables bound to ``pdk_root``."""
        root = str(Path(pdk_root).expanduser())
        return PDKConfig(
            pdk_type=self.pdk_type,
            liberty_file=self.liberty_file.replace("$PDK_ROOT", root),
            lef_file=self.lef_file.replace("$PDK_ROOT", root),
            tech_lef=self.tech_lef.replace("$PDK_ROOT", root),
            cell_prefix=self.cell_prefix,
            clock_period_ns=self.clock_period_ns,
            voltage_v=self.voltage_v,
            temperature_c=self.temperature_c,
            corner=self.corner,
            metal_layers=self.metal_layers,
            min_feature_nm=self.min_feature_nm,
        )

is_open_source property

Return whether the process has a maintained open-source file map.

from_pdk_type(pdk) classmethod

Construct the maintained preset for a process family.

Parameters

pdk: Process family whose nominal files, voltage, and geometry are required.

Returns

PDKConfig Configuration containing deterministic $PDK_ROOT templates.

Source code in src/sc_neurocore/asic_flow/pdk.py
Python
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
@classmethod
def from_pdk_type(cls, pdk: PDKType) -> PDKConfig:
    """Construct the maintained preset for a process family.

    Parameters
    ----------
    pdk:
        Process family whose nominal files, voltage, and geometry are
        required.

    Returns
    -------
    PDKConfig
        Configuration containing deterministic ``$PDK_ROOT`` templates.
    """
    presets: Dict[PDKType, Dict[str, Any]] = {
        PDKType.SKY130: dict(
            liberty_file="$PDK_ROOT/sky130A/libs.ref/sky130_fd_sc_hd/lib/sky130_fd_sc_hd__tt_025C_1v80.lib",
            lef_file="$PDK_ROOT/sky130A/libs.ref/sky130_fd_sc_hd/lef/sky130_fd_sc_hd.lef",
            tech_lef="$PDK_ROOT/sky130A/libs.ref/sky130_fd_sc_hd/techlef/sky130_fd_sc_hd__nom.tlef",
            cell_prefix="sky130_fd_sc_hd__",
            clock_period_ns=10.0,
            voltage_v=1.8,
            metal_layers=5,
            min_feature_nm=130,
        ),
        PDKType.GF180MCU: dict(
            liberty_file="$PDK_ROOT/gf180mcuD/libs.ref/gf180mcu_fd_sc_mcu7t5v0/lib/gf180mcu_fd_sc_mcu7t5v0__tt_025C_3v30.lib",
            lef_file="$PDK_ROOT/gf180mcuD/libs.ref/gf180mcu_fd_sc_mcu7t5v0/lef/gf180mcu_fd_sc_mcu7t5v0.lef",
            tech_lef="$PDK_ROOT/gf180mcuD/libs.ref/gf180mcu_fd_sc_mcu7t5v0/techlef/gf180mcu_fd_sc_mcu7t5v0__nom.tlef",
            cell_prefix="gf180mcu_fd_sc_mcu7t5v0__",
            clock_period_ns=15.0,
            voltage_v=3.3,
            metal_layers=6,
            min_feature_nm=180,
        ),
        PDKType.TSMC28: dict(
            liberty_file="$PDK_ROOT/tsmc28/tcbn28hpcplusbwp7t30p140_110a/TSMCHOME/digital/Front_End/timing_power_noise/NLDM/tcbn28hpcplusbwp7t30p140ssgnp0p81v125c.lib",
            lef_file="$PDK_ROOT/tsmc28/lef/tcbn28hpcplusbwp7t30p140.lef",
            tech_lef="$PDK_ROOT/tsmc28/lef/HiPe_M10.tlef",
            cell_prefix="TSMC_",
            clock_period_ns=2.0,
            voltage_v=0.9,
            metal_layers=10,
            min_feature_nm=28,
        ),
        PDKType.INTEL16: dict(
            liberty_file="$PDK_ROOT/intel16/lib/intel16_sc.lib",
            lef_file="$PDK_ROOT/intel16/lef/intel16_sc.lef",
            tech_lef="$PDK_ROOT/intel16/lef/intel16.tlef",
            cell_prefix="INTEL16_",
            clock_period_ns=1.5,
            voltage_v=0.8,
            metal_layers=12,
            min_feature_nm=16,
        ),
        PDKType.CUSTOM: dict(
            liberty_file="",
            lef_file="",
            tech_lef="",
            cell_prefix="",
            clock_period_ns=10.0,
            voltage_v=1.8,
            metal_layers=5,
            min_feature_nm=130,
        ),
    }
    return cls(pdk_type=pdk, **presets[pdk])

with_pdk_root(pdk_root)

Return a copy with $PDK_ROOT variables bound to pdk_root.

Source code in src/sc_neurocore/asic_flow/pdk.py
Python
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
def with_pdk_root(self, pdk_root: str) -> PDKConfig:
    """Return a copy with ``$PDK_ROOT`` variables bound to ``pdk_root``."""
    root = str(Path(pdk_root).expanduser())
    return PDKConfig(
        pdk_type=self.pdk_type,
        liberty_file=self.liberty_file.replace("$PDK_ROOT", root),
        lef_file=self.lef_file.replace("$PDK_ROOT", root),
        tech_lef=self.tech_lef.replace("$PDK_ROOT", root),
        cell_prefix=self.cell_prefix,
        clock_period_ns=self.clock_period_ns,
        voltage_v=self.voltage_v,
        temperature_c=self.temperature_c,
        corner=self.corner,
        metal_layers=self.metal_layers,
        min_feature_nm=self.min_feature_nm,
    )

PDKResolution dataclass

Outcome of resolving a PDK against the local filesystem.

Source code in src/sc_neurocore/asic_flow/pdk.py
Python
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
@dataclass(frozen=True)
class PDKResolution:
    """Outcome of resolving a PDK against the local filesystem."""

    pdk: PDKConfig
    files: ResolvedPDKFiles
    missing_required: Tuple[str, ...] = ()
    missing_optional: Tuple[str, ...] = ()

    @property
    def usable_for_synthesis(self) -> bool:
        """Return whether every synthesis-required PDK file was found."""
        return not self.missing_required

    @property
    def usable_for_signoff(self) -> bool:
        """Return whether required and optional signoff files were found."""
        return self.usable_for_synthesis and not self.missing_optional

usable_for_synthesis property

Return whether every synthesis-required PDK file was found.

usable_for_signoff property

Return whether required and optional signoff files were found.

PDKType

Bases: Enum

Process-design-kit families supported by the deck templates.

Source code in src/sc_neurocore/asic_flow/pdk.py
Python
20
21
22
23
24
25
26
27
class PDKType(Enum):
    """Process-design-kit families supported by the deck templates."""

    SKY130 = "sky130"
    GF180MCU = "gf180mcu"
    TSMC28 = "tsmc28"
    INTEL16 = "intel16"
    CUSTOM = "custom"

PDKValidationResult dataclass

Result of PDK sanity check.

Source code in src/sc_neurocore/asic_flow/pdk.py
Python
259
260
261
262
263
264
265
@dataclass
class PDKValidationResult:
    """Result of PDK sanity check."""

    valid: bool
    errors: List[str] = field(default_factory=list)
    warnings: List[str] = field(default_factory=list)

ResolvedPDKFiles dataclass

Resolved file paths required by the open-source ASIC flow.

Source code in src/sc_neurocore/asic_flow/pdk.py
Python
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
@dataclass(frozen=True)
class ResolvedPDKFiles:
    """Resolved file paths required by the open-source ASIC flow."""

    liberty_file: str
    lef_file: str
    tech_lef: str
    setup_tcl: str = ""
    drc_deck: str = ""
    lvs_setup: str = ""

    def required_paths(self) -> Dict[str, str]:
        """Return the Liberty, cell-LEF, and technology-LEF paths."""
        return {
            "liberty_file": self.liberty_file,
            "lef_file": self.lef_file,
            "tech_lef": self.tech_lef,
        }

    def optional_paths(self) -> Dict[str, str]:
        """Return optional setup, DRC-deck, and LVS-setup paths."""
        return {
            "setup_tcl": self.setup_tcl,
            "drc_deck": self.drc_deck,
            "lvs_setup": self.lvs_setup,
        }

required_paths()

Return the Liberty, cell-LEF, and technology-LEF paths.

Source code in src/sc_neurocore/asic_flow/pdk.py
Python
149
150
151
152
153
154
155
def required_paths(self) -> Dict[str, str]:
    """Return the Liberty, cell-LEF, and technology-LEF paths."""
    return {
        "liberty_file": self.liberty_file,
        "lef_file": self.lef_file,
        "tech_lef": self.tech_lef,
    }

optional_paths()

Return optional setup, DRC-deck, and LVS-setup paths.

Source code in src/sc_neurocore/asic_flow/pdk.py
Python
157
158
159
160
161
162
163
def optional_paths(self) -> Dict[str, str]:
    """Return optional setup, DRC-deck, and LVS-setup paths."""
    return {
        "setup_tcl": self.setup_tcl,
        "drc_deck": self.drc_deck,
        "lvs_setup": self.lvs_setup,
    }

TapeOutChecklist dataclass

Go/no-go checklist for ASIC tape-out.

Source code in src/sc_neurocore/asic_flow/readiness.py
Python
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
@dataclass
class TapeOutChecklist:
    """Go/no-go checklist for ASIC tape-out."""

    synthesis_clean: bool = False
    timing_met: bool = False
    power_within_budget: bool = False
    area_within_limit: bool = False
    drc_clean: bool = False
    lvs_clean: bool = False
    formal_equiv_pass: bool = False
    cdc_clean: bool = False
    ir_drop_ok: bool = False
    esd_reviewed: bool = False

    @property
    def readiness_score(self) -> float:
        """Return the fraction of the ten required checks that passed."""
        checks = [
            self.synthesis_clean,
            self.timing_met,
            self.power_within_budget,
            self.area_within_limit,
            self.drc_clean,
            self.lvs_clean,
            self.formal_equiv_pass,
            self.cdc_clean,
            self.ir_drop_ok,
            self.esd_reviewed,
        ]
        return sum(1 for c in checks if c) / len(checks)

    @property
    def is_tape_out_ready(self) -> bool:
        """Return whether all ten readiness checks passed."""
        return self.readiness_score == 1.0

    def failing_checks(self) -> List[str]:
        """Return stable field names for every incomplete readiness check."""
        names = [
            "synthesis_clean",
            "timing_met",
            "power_within_budget",
            "area_within_limit",
            "drc_clean",
            "lvs_clean",
            "formal_equiv_pass",
            "cdc_clean",
            "ir_drop_ok",
            "esd_reviewed",
        ]
        return [n for n in names if not getattr(self, n)]

    def from_signoff(self, summary: SignoffSummary) -> None:
        """Populate from a signoff summary."""
        self.timing_met = summary.timing.passed
        self.power_within_budget = summary.power.passed
        self.area_within_limit = summary.area.passed
        self.drc_clean = summary.drc_clean
        self.lvs_clean = summary.lvs_match

readiness_score property

Return the fraction of the ten required checks that passed.

is_tape_out_ready property

Return whether all ten readiness checks passed.

failing_checks()

Return stable field names for every incomplete readiness check.

Source code in src/sc_neurocore/asic_flow/readiness.py
Python
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
def failing_checks(self) -> List[str]:
    """Return stable field names for every incomplete readiness check."""
    names = [
        "synthesis_clean",
        "timing_met",
        "power_within_budget",
        "area_within_limit",
        "drc_clean",
        "lvs_clean",
        "formal_equiv_pass",
        "cdc_clean",
        "ir_drop_ok",
        "esd_reviewed",
    ]
    return [n for n in names if not getattr(self, n)]

from_signoff(summary)

Populate from a signoff summary.

Source code in src/sc_neurocore/asic_flow/readiness.py
Python
72
73
74
75
76
77
78
def from_signoff(self, summary: SignoffSummary) -> None:
    """Populate from a signoff summary."""
    self.timing_met = summary.timing.passed
    self.power_within_budget = summary.power.passed
    self.area_within_limit = summary.area.passed
    self.drc_clean = summary.drc_clean
    self.lvs_clean = summary.lvs_match

CornerType

Bases: Enum

Process-corner combinations used by multi-corner timing analysis.

Source code in src/sc_neurocore/asic_flow/signoff.py
Python
112
113
114
115
116
117
118
119
class CornerType(Enum):
    """Process-corner combinations used by multi-corner timing analysis."""

    TT = "tt"  # typical
    FF = "ff"  # fast-fast
    SS = "ss"  # slow-slow
    SF = "sf"  # slow-fast
    FS = "fs"

DRCViolation dataclass

One DRC rule violation.

Source code in src/sc_neurocore/asic_flow/signoff.py
Python
219
220
221
222
223
224
225
@dataclass
class DRCViolation:
    """One DRC rule violation."""

    rule_name: str
    count: int
    severity: str = "error"

MultiCornerAnalysis dataclass

Generates multi-corner STA scripts for all PVT corners.

Source code in src/sc_neurocore/asic_flow/signoff.py
Python
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
@dataclass
class MultiCornerAnalysis:
    """Generates multi-corner STA scripts for all PVT corners."""

    @staticmethod
    def generate(
        pdk: PDKConfig, design: DesignParams, corners: Optional[List[PVTCorner]] = None
    ) -> str:
        """Render one OpenSTA analysis section per selected PVT corner."""
        if corners is None:
            corners = DEFAULT_CORNERS
        lines = [f"# Multi-Corner STA for {design.top_module}"]
        for c in corners:
            lib = (
                pdk.liberty_file.replace("_tt_025C_1v80", c.liberty_suffix)
                if c.liberty_suffix
                else pdk.liberty_file
            )
            lines.append(f"\n# Corner: {c.label}")
            lines.append(f"read_liberty {lib}")
            lines.append(f"read_verilog {design.top_module}_final.v")
            lines.append(f"link_design {design.top_module}")
            lines.append(f"read_sdc constraints_{design.top_module}.sdc")
            lines.append("set_operating_conditions -analysis_type on_chip_variation")
            lines.append("report_checks -path_delay min_max -digits 4")
            lines.append("report_tns")
            lines.append("report_wns")
        return "\n".join(lines) + "\n"

    @staticmethod
    def worst_slack(per_corner_wns: Dict[str, float]) -> Tuple[str, float]:
        """Return the corner with the smallest worst negative slack."""
        if not per_corner_wns:
            return ("none", 0.0)
        worst = min(per_corner_wns.items(), key=lambda kv: kv[1])
        return worst

generate(pdk, design, corners=None) staticmethod

Render one OpenSTA analysis section per selected PVT corner.

Source code in src/sc_neurocore/asic_flow/signoff.py
Python
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
@staticmethod
def generate(
    pdk: PDKConfig, design: DesignParams, corners: Optional[List[PVTCorner]] = None
) -> str:
    """Render one OpenSTA analysis section per selected PVT corner."""
    if corners is None:
        corners = DEFAULT_CORNERS
    lines = [f"# Multi-Corner STA for {design.top_module}"]
    for c in corners:
        lib = (
            pdk.liberty_file.replace("_tt_025C_1v80", c.liberty_suffix)
            if c.liberty_suffix
            else pdk.liberty_file
        )
        lines.append(f"\n# Corner: {c.label}")
        lines.append(f"read_liberty {lib}")
        lines.append(f"read_verilog {design.top_module}_final.v")
        lines.append(f"link_design {design.top_module}")
        lines.append(f"read_sdc constraints_{design.top_module}.sdc")
        lines.append("set_operating_conditions -analysis_type on_chip_variation")
        lines.append("report_checks -path_delay min_max -digits 4")
        lines.append("report_tns")
        lines.append("report_wns")
    return "\n".join(lines) + "\n"

worst_slack(per_corner_wns) staticmethod

Return the corner with the smallest worst negative slack.

Source code in src/sc_neurocore/asic_flow/signoff.py
Python
176
177
178
179
180
181
182
@staticmethod
def worst_slack(per_corner_wns: Dict[str, float]) -> Tuple[str, float]:
    """Return the corner with the smallest worst negative slack."""
    if not per_corner_wns:
        return ("none", 0.0)
    worst = min(per_corner_wns.items(), key=lambda kv: kv[1])
    return worst

OCVConfig dataclass

On-Chip Variation derating factors.

Source code in src/sc_neurocore/asic_flow/signoff.py
Python
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
@dataclass
class OCVConfig:
    """On-Chip Variation derating factors."""

    data_cell_early: float = 0.95
    data_cell_late: float = 1.05
    data_net_early: float = 0.95
    data_net_late: float = 1.05
    clock_cell_early: float = 0.97
    clock_cell_late: float = 1.03

    def generate_sdc_fragment(self) -> str:
        """Render early and late cell/net derates as an SDC fragment."""
        return textwrap.dedent(f"""\
# OCV Derating
set_timing_derate -early {self.data_cell_early:.3f} -cell_delay [all_inputs]
set_timing_derate -late {self.data_cell_late:.3f} -cell_delay [all_inputs]
set_timing_derate -early {self.data_net_early:.3f} -net_delay [all_inputs]
set_timing_derate -late {self.data_net_late:.3f} -net_delay [all_inputs]
""")

    @classmethod
    def conservative(cls) -> OCVConfig:
        """Return wider early/late derates for screening runs."""
        return cls(
            data_cell_early=0.93,
            data_cell_late=1.07,
            data_net_early=0.93,
            data_net_late=1.07,
            clock_cell_early=0.95,
            clock_cell_late=1.05,
        )

generate_sdc_fragment()

Render early and late cell/net derates as an SDC fragment.

Source code in src/sc_neurocore/asic_flow/signoff.py
Python
196
197
198
199
200
201
202
203
204
    def generate_sdc_fragment(self) -> str:
        """Render early and late cell/net derates as an SDC fragment."""
        return textwrap.dedent(f"""\
# OCV Derating
set_timing_derate -early {self.data_cell_early:.3f} -cell_delay [all_inputs]
set_timing_derate -late {self.data_cell_late:.3f} -cell_delay [all_inputs]
set_timing_derate -early {self.data_net_early:.3f} -net_delay [all_inputs]
set_timing_derate -late {self.data_net_late:.3f} -net_delay [all_inputs]
""")

conservative() classmethod

Return wider early/late derates for screening runs.

Source code in src/sc_neurocore/asic_flow/signoff.py
Python
206
207
208
209
210
211
212
213
214
215
216
@classmethod
def conservative(cls) -> OCVConfig:
    """Return wider early/late derates for screening runs."""
    return cls(
        data_cell_early=0.93,
        data_cell_late=1.07,
        data_net_early=0.93,
        data_net_late=1.07,
        clock_cell_early=0.95,
        clock_cell_late=1.05,
    )

PVTCorner dataclass

Process-Voltage-Temperature corner definition.

Source code in src/sc_neurocore/asic_flow/signoff.py
Python
122
123
124
125
126
127
128
129
130
131
132
133
134
135
@dataclass
class PVTCorner:
    """Process-Voltage-Temperature corner definition."""

    corner: CornerType
    temperature_c: float
    voltage_v: float
    liberty_suffix: str = ""
    is_signoff: bool = True

    @property
    def label(self) -> str:
        """Return a stable corner-temperature-voltage label."""
        return f"{self.corner.value}_{self.temperature_c:.0f}C_{self.voltage_v:.2f}V"

label property

Return a stable corner-temperature-voltage label.

SignoffCheckResult dataclass

Result of one signoff check.

Source code in src/sc_neurocore/asic_flow/signoff.py
Python
22
23
24
25
26
27
28
29
@dataclass
class SignoffCheckResult:
    """Result of one signoff check."""

    check_name: str
    passed: bool
    details: str = ""
    metric: float = 0.0

SignoffGenerator

Generates signoff scripts and evaluates results.

Source code in src/sc_neurocore/asic_flow/signoff.py
Python
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
class SignoffGenerator:
    """Generates signoff scripts and evaluates results."""

    @staticmethod
    def generate_sta_script(pdk: PDKConfig, design: DesignParams) -> str:
        """Generate OpenSTA timing analysis script."""
        return textwrap.dedent(f"""\
# SC-NeuroCore STA Signoff — OpenSTA
read_liberty {pdk.liberty_file}
read_verilog {design.top_module}_final.v
link_design {design.top_module}
read_sdc constraints_{design.top_module}.sdc

report_checks -path_delay min_max -format full_clock_expanded \\
    -fields {{slew cap input_pins nets}} \\
    -digits 4

report_tns
report_wns
report_power
""")

    @staticmethod
    def generate_drc_script(pdk: PDKConfig, design: DesignParams) -> str:
        """Generate DRC check script (KLayout-based for open PDKs)."""
        if pdk.is_open_source:
            return textwrap.dedent(f"""\
# SC-NeuroCore DRC — KLayout (open-source PDK)
import klayout.db as db
import klayout.rdb as rdb

layout = db.Layout()
layout.read("{design.top_module}.gds")
# Run DRC deck for {pdk.pdk_type.value}
# drc_deck = "$PDK_ROOT/{pdk.pdk_type.value}/libs.tech/klayout/drc/{pdk.pdk_type.value}.lydrc"
""")
        return f"# DRC for {pdk.pdk_type.value}: use vendor-specific tool\n"

    @staticmethod
    def generate_lvs_script(pdk: PDKConfig, design: DesignParams) -> str:
        """Generate LVS check script."""
        if pdk.is_open_source:
            return textwrap.dedent(f"""\
# SC-NeuroCore LVS — Netgen (open-source PDK)
netgen -batch lvs \\
    "{design.top_module}.spice {design.top_module}" \\
    "{design.top_module}_final.v {design.top_module}" \\
    $PDK_ROOT/{pdk.pdk_type.value}/libs.tech/netgen/{pdk.pdk_type.value}_setup.tcl \\
    lvs_{design.top_module}.log
""")
        return f"# LVS for {pdk.pdk_type.value}: use vendor-specific tool\n"

    @staticmethod
    def evaluate_timing(wns: float, tns: float, clock_period_ns: float) -> SignoffCheckResult:
        """Evaluate timing signoff from worst/total negative slack."""
        passed = wns >= 0.0
        details = f"WNS={wns:.3f}ns TNS={tns:.3f}ns period={clock_period_ns:.3f}ns"
        return SignoffCheckResult("STA", passed, details, wns)

    @staticmethod
    def evaluate_power(
        dynamic_mw: float, leakage_mw: float, budget_mw: float
    ) -> SignoffCheckResult:
        """Compare dynamic plus leakage power against a milliwatt budget."""
        total = dynamic_mw + leakage_mw
        passed = total <= budget_mw
        details = f"dynamic={dynamic_mw:.3f}mW leakage={leakage_mw:.3f}mW total={total:.3f}mW budget={budget_mw:.3f}mW"
        return SignoffCheckResult("Power", passed, details, total)

    @staticmethod
    def evaluate_area(
        cell_count: int, used_area_um2: float, die_area_um2: float
    ) -> SignoffCheckResult:
        """Compare placed-cell area with the 85 percent utilisation limit."""
        util = used_area_um2 / die_area_um2 if die_area_um2 > 0 else 0
        passed = util <= 0.85
        details = f"cells={cell_count} util={util:.1%} used={used_area_um2:.0f}µm² die={die_area_um2:.0f}µm²"
        return SignoffCheckResult("Area", passed, details, util)

generate_sta_script(pdk, design) staticmethod

Generate OpenSTA timing analysis script.

Source code in src/sc_neurocore/asic_flow/signoff.py
Python
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
    @staticmethod
    def generate_sta_script(pdk: PDKConfig, design: DesignParams) -> str:
        """Generate OpenSTA timing analysis script."""
        return textwrap.dedent(f"""\
# SC-NeuroCore STA Signoff — OpenSTA
read_liberty {pdk.liberty_file}
read_verilog {design.top_module}_final.v
link_design {design.top_module}
read_sdc constraints_{design.top_module}.sdc

report_checks -path_delay min_max -format full_clock_expanded \\
    -fields {{slew cap input_pins nets}} \\
    -digits 4

report_tns
report_wns
report_power
""")

generate_drc_script(pdk, design) staticmethod

Generate DRC check script (KLayout-based for open PDKs).

Source code in src/sc_neurocore/asic_flow/signoff.py
Python
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
    @staticmethod
    def generate_drc_script(pdk: PDKConfig, design: DesignParams) -> str:
        """Generate DRC check script (KLayout-based for open PDKs)."""
        if pdk.is_open_source:
            return textwrap.dedent(f"""\
# SC-NeuroCore DRC — KLayout (open-source PDK)
import klayout.db as db
import klayout.rdb as rdb

layout = db.Layout()
layout.read("{design.top_module}.gds")
# Run DRC deck for {pdk.pdk_type.value}
# drc_deck = "$PDK_ROOT/{pdk.pdk_type.value}/libs.tech/klayout/drc/{pdk.pdk_type.value}.lydrc"
""")
        return f"# DRC for {pdk.pdk_type.value}: use vendor-specific tool\n"

generate_lvs_script(pdk, design) staticmethod

Generate LVS check script.

Source code in src/sc_neurocore/asic_flow/signoff.py
Python
70
71
72
73
74
75
76
77
78
79
80
81
82
    @staticmethod
    def generate_lvs_script(pdk: PDKConfig, design: DesignParams) -> str:
        """Generate LVS check script."""
        if pdk.is_open_source:
            return textwrap.dedent(f"""\
# SC-NeuroCore LVS — Netgen (open-source PDK)
netgen -batch lvs \\
    "{design.top_module}.spice {design.top_module}" \\
    "{design.top_module}_final.v {design.top_module}" \\
    $PDK_ROOT/{pdk.pdk_type.value}/libs.tech/netgen/{pdk.pdk_type.value}_setup.tcl \\
    lvs_{design.top_module}.log
""")
        return f"# LVS for {pdk.pdk_type.value}: use vendor-specific tool\n"

evaluate_timing(wns, tns, clock_period_ns) staticmethod

Evaluate timing signoff from worst/total negative slack.

Source code in src/sc_neurocore/asic_flow/signoff.py
Python
84
85
86
87
88
89
@staticmethod
def evaluate_timing(wns: float, tns: float, clock_period_ns: float) -> SignoffCheckResult:
    """Evaluate timing signoff from worst/total negative slack."""
    passed = wns >= 0.0
    details = f"WNS={wns:.3f}ns TNS={tns:.3f}ns period={clock_period_ns:.3f}ns"
    return SignoffCheckResult("STA", passed, details, wns)

evaluate_power(dynamic_mw, leakage_mw, budget_mw) staticmethod

Compare dynamic plus leakage power against a milliwatt budget.

Source code in src/sc_neurocore/asic_flow/signoff.py
Python
91
92
93
94
95
96
97
98
99
@staticmethod
def evaluate_power(
    dynamic_mw: float, leakage_mw: float, budget_mw: float
) -> SignoffCheckResult:
    """Compare dynamic plus leakage power against a milliwatt budget."""
    total = dynamic_mw + leakage_mw
    passed = total <= budget_mw
    details = f"dynamic={dynamic_mw:.3f}mW leakage={leakage_mw:.3f}mW total={total:.3f}mW budget={budget_mw:.3f}mW"
    return SignoffCheckResult("Power", passed, details, total)

evaluate_area(cell_count, used_area_um2, die_area_um2) staticmethod

Compare placed-cell area with the 85 percent utilisation limit.

Source code in src/sc_neurocore/asic_flow/signoff.py
Python
101
102
103
104
105
106
107
108
109
@staticmethod
def evaluate_area(
    cell_count: int, used_area_um2: float, die_area_um2: float
) -> SignoffCheckResult:
    """Compare placed-cell area with the 85 percent utilisation limit."""
    util = used_area_um2 / die_area_um2 if die_area_um2 > 0 else 0
    passed = util <= 0.85
    details = f"cells={cell_count} util={util:.1%} used={used_area_um2:.0f}µm² die={die_area_um2:.0f}µm²"
    return SignoffCheckResult("Area", passed, details, util)

SignoffSummary dataclass

Structured signoff summary with pass/fail per check.

Source code in src/sc_neurocore/asic_flow/signoff.py
Python
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
@dataclass
class SignoffSummary:
    """Structured signoff summary with pass/fail per check."""

    timing: SignoffCheckResult
    power: SignoffCheckResult
    area: SignoffCheckResult
    drc_violations: List[DRCViolation] = field(default_factory=list)
    lvs_match: bool = False

    @property
    def drc_clean(self) -> bool:
        """Return whether no counted error-severity DRC violation exists."""
        return not any(v.severity == "error" and v.count > 0 for v in self.drc_violations)

    @property
    def all_pass(self) -> bool:
        """Return whether timing, power, area, DRC, and LVS all pass."""
        return (
            self.timing.passed
            and self.power.passed
            and self.area.passed
            and self.drc_clean
            and self.lvs_match
        )

    def to_dict(self) -> Dict[str, Any]:
        """Serialise the signoff decision and counted DRC violations."""
        return {
            "timing": {"passed": self.timing.passed, "details": self.timing.details},
            "power": {"passed": self.power.passed, "details": self.power.details},
            "area": {"passed": self.area.passed, "details": self.area.details},
            "drc_clean": self.drc_clean,
            "drc_violations": [
                {"rule": v.rule_name, "count": v.count} for v in self.drc_violations
            ],
            "lvs_match": self.lvs_match,
            "all_pass": self.all_pass,
        }

drc_clean property

Return whether no counted error-severity DRC violation exists.

all_pass property

Return whether timing, power, area, DRC, and LVS all pass.

to_dict()

Serialise the signoff decision and counted DRC violations.

Source code in src/sc_neurocore/asic_flow/signoff.py
Python
254
255
256
257
258
259
260
261
262
263
264
265
266
def to_dict(self) -> Dict[str, Any]:
    """Serialise the signoff decision and counted DRC violations."""
    return {
        "timing": {"passed": self.timing.passed, "details": self.timing.details},
        "power": {"passed": self.power.passed, "details": self.power.details},
        "area": {"passed": self.area.passed, "details": self.area.details},
        "drc_clean": self.drc_clean,
        "drc_violations": [
            {"rule": v.rule_name, "count": v.count} for v in self.drc_violations
        ],
        "lvs_match": self.lvs_match,
        "all_pass": self.all_pass,
    }

generate_asic_flow_bundle(output_dir, *, pdk_type=PDKType.SKY130, design=None, pdk_root=None, require_pdk_files=False, n_neurons=16, n_synapses=256, bitstream_width=256, n_aer_ports=4, formal_evidence_artifacts=None)

Write a complete ASIC flow deck and evidence manifest in one call.

The helper deliberately does not run Yosys/OpenROAD. It materialises the scripts, resolves the requested PDK paths, records missing artefacts, and adds a pre-synthesis estimate so Python API users can inspect the bundle before launching external EDA tools.

Source code in src/sc_neurocore/asic_flow/flow.py
Python
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
def generate_asic_flow_bundle(
    output_dir: str | Path,
    *,
    pdk_type: PDKType | str = PDKType.SKY130,
    design: Optional[DesignParams] = None,
    pdk_root: Optional[str] = None,
    require_pdk_files: bool = False,
    n_neurons: int = 16,
    n_synapses: int = 256,
    bitstream_width: int = 256,
    n_aer_ports: int = 4,
    formal_evidence_artifacts: Optional[List[str]] = None,
) -> ASICFlowBundle:
    """Write a complete ASIC flow deck and evidence manifest in one call.

    The helper deliberately does not run Yosys/OpenROAD. It materialises the
    scripts, resolves the requested PDK paths, records missing artefacts, and
    adds a pre-synthesis estimate so Python API users can inspect the bundle
    before launching external EDA tools.
    """
    pdk_enum = _normalise_pdk_type(pdk_type)
    design = design or DesignParams()
    out = Path(output_dir).expanduser()
    out.mkdir(parents=True, exist_ok=True)

    pdk = PDKConfig.from_pdk_type(pdk_enum)
    resolution = OpenSourcePDKResolver.resolve(
        pdk,
        pdk_root=pdk_root,
        require_existing=require_pdk_files,
    )
    flow = ASICFlowGenerator().generate(resolution.pdk, design)

    file_paths: Dict[str, str] = {}
    for name, content in flow.to_dict().items():
        path = out / name
        path.write_text(content, encoding="utf-8")
        file_paths[name] = str(path)

    estimate = PreSynthEstimator.estimate(
        n_neurons=n_neurons,
        n_synapses=n_synapses,
        bitstream_width=bitstream_width,
        n_aer_ports=n_aer_ports,
        pdk=resolution.pdk,
    )
    manifest = _build_asic_flow_manifest(
        design=design,
        pdk_resolution=resolution,
        estimate=estimate,
        file_paths=file_paths,
        require_pdk_files=require_pdk_files,
        formal_evidence_artifacts=formal_evidence_artifacts or [],
    )
    manifest_path = out / "asic_flow_manifest.json"
    manifest_path.write_text(
        json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8"
    )

    return ASICFlowBundle(
        output_dir=str(out),
        manifest_path=str(manifest_path),
        file_paths=file_paths,
        pdk_resolution=resolution,
        estimate=estimate,
    )

validate_pdk(pdk)

Check PDK configuration for obvious errors.

Source code in src/sc_neurocore/asic_flow/pdk.py
Python
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
def validate_pdk(pdk: PDKConfig) -> PDKValidationResult:
    """Check PDK configuration for obvious errors."""
    errors = []
    warnings = []

    if pdk.pdk_type != PDKType.CUSTOM:
        if not pdk.liberty_file:
            errors.append("liberty_file is empty")
        if not pdk.lef_file:
            errors.append("lef_file is empty")
        if not pdk.tech_lef:
            errors.append("tech_lef is empty")

    if pdk.clock_period_ns <= 0:
        errors.append(f"clock_period_ns must be positive, got {pdk.clock_period_ns}")
    if pdk.voltage_v <= 0:
        errors.append(f"voltage_v must be positive, got {pdk.voltage_v}")
    if pdk.metal_layers < 3:
        warnings.append(f"only {pdk.metal_layers} metal layers — may limit routing")

    return PDKValidationResult(valid=len(errors) == 0, errors=errors, warnings=warnings)

validate_pdk_installation(pdk, pdk_root=None, require_signoff=False)

Check whether the resolved open-source PDK files are present locally.

Source code in src/sc_neurocore/asic_flow/pdk.py
Python
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
def validate_pdk_installation(
    pdk: PDKConfig,
    pdk_root: Optional[str] = None,
    require_signoff: bool = False,
) -> PDKValidationResult:
    """Check whether the resolved open-source PDK files are present locally."""
    base = validate_pdk(pdk)
    errors = list(base.errors)
    warnings = list(base.warnings)

    resolution = OpenSourcePDKResolver.resolve(pdk, pdk_root=pdk_root, require_existing=True)
    for name in resolution.missing_required:
        path = resolution.files.required_paths()[name]
        errors.append(f"{name} not found: {path}")
    for name in resolution.missing_optional:
        path = resolution.files.optional_paths()[name]
        message = f"{name} not found: {path}"
        if require_signoff:
            errors.append(message)
        else:
            warnings.append(message)

    return PDKValidationResult(valid=len(errors) == 0, errors=errors, warnings=warnings)