From c2e0b80e4c8d6f08801004871571b74237d54c31 Mon Sep 17 00:00:00 2001 From: Divinesoumyadip Date: Fri, 6 Mar 2026 13:25:29 +0530 Subject: [PATCH 1/6] esim: add eSim-ORFS integration skeleton with PDK validator Signed-off-by: Divinesoumyadip --- src/orfs_integration/__init__.py | 0 src/orfs_integration/orfs_config_gen.py | 0 src/orfs_integration/pdk_validator.py | 16 ++++++++++++++++ 3 files changed, 16 insertions(+) create mode 100644 src/orfs_integration/__init__.py create mode 100644 src/orfs_integration/orfs_config_gen.py create mode 100644 src/orfs_integration/pdk_validator.py diff --git a/src/orfs_integration/__init__.py b/src/orfs_integration/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/orfs_integration/orfs_config_gen.py b/src/orfs_integration/orfs_config_gen.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/orfs_integration/pdk_validator.py b/src/orfs_integration/pdk_validator.py new file mode 100644 index 000000000..70a5e6672 --- /dev/null +++ b/src/orfs_integration/pdk_validator.py @@ -0,0 +1,16 @@ +# PDK-aware netlist validator for eSim-ORFS integration +# Validates .cir.out netlist against target PDK standard cells + +class PDKValidator: + def __init__(self, pdk_path): + self.pdk_path = pdk_path + self.standard_cells = [] + + def load_pdk_cells(self): + pass + + def validate_netlist(self, cir_file): + pass + + def report_unmappable(self): + pass From 6780fe505f6b2f6f1a7729e083c5b0541d927520 Mon Sep 17 00:00:00 2001 From: Divinesoumyadip Date: Fri, 6 Mar 2026 16:13:33 +0530 Subject: [PATCH 2/6] esim: implement full PDK validator and ORFS config generator --- src/orfs_integration/pdk_validator.py | 114 ++++++++++++++++++++++---- 1 file changed, 98 insertions(+), 16 deletions(-) diff --git a/src/orfs_integration/pdk_validator.py b/src/orfs_integration/pdk_validator.py index 70a5e6672..3370bb124 100644 --- a/src/orfs_integration/pdk_validator.py +++ b/src/orfs_integration/pdk_validator.py @@ -1,16 +1,98 @@ -# PDK-aware netlist validator for eSim-ORFS integration -# Validates .cir.out netlist against target PDK standard cells - -class PDKValidator: - def __init__(self, pdk_path): - self.pdk_path = pdk_path - self.standard_cells = [] - - def load_pdk_cells(self): - pass - - def validate_netlist(self, cir_file): - pass - - def report_unmappable(self): - pass +""" +orfs_config_gen.py +Auto-generates ORFS config.mk from a validated eSim .cir.out netlist. +""" + +import sys +from pathlib import Path +from datetime import datetime +from pdk_validator import validate_netlist, PDKValidator + +PDK_DEFAULTS = { + "sky130": { + "PLATFORM": "sky130hd", + "SCL": "sky130_fd_sc_hd", + "CORNER": "TYP", + "TARGET_DENSITY": "0.65", + "CLOCK_PERIOD": "10.0", + "CORE_UTILIZATION": "40", + "CORE_ASPECT_RATIO": "1", + "CORE_MARGIN": "2", + } +} + + +def generate_config(cir_out_path, design_name=None, pdk="sky130", + clock_port="clk", clock_period_ns=None, output_dir="."): + print(f"[orfs_config_gen] Validating {cir_out_path} against {pdk} PDK...") + report = validate_netlist(cir_out_path, pdk) + PDKValidator.print_report(report) + + if report["summary"] == "FAIL": + raise RuntimeError( + f"Validation FAILED - {len(report['unmappable'])} unmappable primitive(s). " + f"Fix the netlist before generating ORFS config." + ) + + if design_name is None: + stem = Path(cir_out_path).stem + design_name = stem.replace(".cir", "").replace(".", "_") + + defaults = PDK_DEFAULTS.get(pdk, PDK_DEFAULTS["sky130"]) + clk_period = str(clock_period_ns) if clock_period_ns else defaults["CLOCK_PERIOD"] + + lines = [ + f"# ORFS config.mk - auto-generated by eSim-ORFS integration", + f"# Source netlist : {Path(cir_out_path).resolve()}", + f"# Generated : {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", + f"# PDK : {pdk}", + f"", + f"export DESIGN_NAME = {design_name}", + f"export PLATFORM = {defaults['PLATFORM']}", + f"", + f"export VERILOG_FILES = $(DESIGN_HOME)/src/$(DESIGN_NAME)/$(DESIGN_NAME).v", + f"export SDC_FILE = $(DESIGN_HOME)/src/$(DESIGN_NAME)/constraint.sdc", + f"export ABC_AREA = 0", + f"", + f"export CORE_UTILIZATION = {defaults['CORE_UTILIZATION']}", + f"export CORE_ASPECT_RATIO = {defaults['CORE_ASPECT_RATIO']}", + f"export CORE_MARGIN = {defaults['CORE_MARGIN']}", + f"", + f"export CLOCK_PORT = {clock_port}", + f"export CLOCK_PERIOD = {clk_period}", + f"", + f"export TARGET_DENSITY = {defaults['TARGET_DENSITY']}", + f"", + f"# Mapped cells ({len(report['valid_cells'])}):", + ] + for cell in report["valid_cells"]: + lines.append(f"# {cell['name']} -> {cell['cell']}") + + if report["analog_primitives"]: + lines.append(f"# Skipped analog primitives ({len(report['analog_primitives'])}):") + for a in report["analog_primitives"]: + lines.append(f"# {a['name']} ({a['type'].upper()})") + + out_path = Path(output_dir) / "config.mk" + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_text("\n".join(lines) + "\n") + print(f"[orfs_config_gen] config.mk written to: {out_path.resolve()}") + return str(out_path) + + +if __name__ == "__main__": + import argparse + parser = argparse.ArgumentParser() + parser.add_argument("netlist") + parser.add_argument("--design") + parser.add_argument("--pdk", default="sky130") + parser.add_argument("--clock-port", default="clk") + parser.add_argument("--clock-period", type=float) + parser.add_argument("--output-dir", default=".") + args = parser.parse_args() + try: + generate_config(args.netlist, args.design, args.pdk, + args.clock_port, args.clock_period, args.output_dir) + except RuntimeError as e: + print(f"\n[ERROR] {e}") + sys.exit(1) \ No newline at end of file From 2357041b417020360b24ff34e426501f393bcc2f Mon Sep 17 00:00:00 2001 From: Divinesoumyadip Date: Fri, 6 Mar 2026 16:23:25 +0530 Subject: [PATCH 3/6] esim: implement full PDK validator and ORFS config generator --- src/orfs_integration/pdk_validator.py | 231 +++++++++++++++++--------- 1 file changed, 148 insertions(+), 83 deletions(-) diff --git a/src/orfs_integration/pdk_validator.py b/src/orfs_integration/pdk_validator.py index 3370bb124..4d7d132f2 100644 --- a/src/orfs_integration/pdk_validator.py +++ b/src/orfs_integration/pdk_validator.py @@ -1,98 +1,163 @@ -""" -orfs_config_gen.py -Auto-generates ORFS config.mk from a validated eSim .cir.out netlist. -""" - import sys from pathlib import Path -from datetime import datetime -from pdk_validator import validate_netlist, PDKValidator -PDK_DEFAULTS = { - "sky130": { - "PLATFORM": "sky130hd", - "SCL": "sky130_fd_sc_hd", - "CORNER": "TYP", - "TARGET_DENSITY": "0.65", - "CLOCK_PERIOD": "10.0", - "CORE_UTILIZATION": "40", - "CORE_ASPECT_RATIO": "1", - "CORE_MARGIN": "2", - } +SKY130_HD_CELLS = { + "sky130_fd_sc_hd__inv_1", "sky130_fd_sc_hd__inv_2", "sky130_fd_sc_hd__inv_4", + "sky130_fd_sc_hd__inv_6", "sky130_fd_sc_hd__inv_8", "sky130_fd_sc_hd__inv_12", + "sky130_fd_sc_hd__inv_16", "sky130_fd_sc_hd__buf_1", "sky130_fd_sc_hd__buf_2", + "sky130_fd_sc_hd__buf_4", "sky130_fd_sc_hd__buf_6", "sky130_fd_sc_hd__buf_8", + "sky130_fd_sc_hd__buf_12", "sky130_fd_sc_hd__buf_16", "sky130_fd_sc_hd__nand2_1", + "sky130_fd_sc_hd__nand2_2", "sky130_fd_sc_hd__nand2_4", "sky130_fd_sc_hd__nand2_8", + "sky130_fd_sc_hd__nand3_1", "sky130_fd_sc_hd__nand3_2", "sky130_fd_sc_hd__nand3_4", + "sky130_fd_sc_hd__nand4_1", "sky130_fd_sc_hd__nand4_2", "sky130_fd_sc_hd__nor2_1", + "sky130_fd_sc_hd__nor2_2", "sky130_fd_sc_hd__nor2_4", "sky130_fd_sc_hd__nor2_8", + "sky130_fd_sc_hd__nor3_1", "sky130_fd_sc_hd__nor3_2", "sky130_fd_sc_hd__nor3_4", + "sky130_fd_sc_hd__nor4_1", "sky130_fd_sc_hd__nor4_2", "sky130_fd_sc_hd__and2_0", + "sky130_fd_sc_hd__and2_2", "sky130_fd_sc_hd__and2_4", "sky130_fd_sc_hd__and3_1", + "sky130_fd_sc_hd__and3_2", "sky130_fd_sc_hd__and3_4", "sky130_fd_sc_hd__and4_1", + "sky130_fd_sc_hd__and4_2", "sky130_fd_sc_hd__or2_0", "sky130_fd_sc_hd__or2_2", + "sky130_fd_sc_hd__or2_4", "sky130_fd_sc_hd__or3_1", "sky130_fd_sc_hd__or3_2", + "sky130_fd_sc_hd__or3_4", "sky130_fd_sc_hd__or4_1", "sky130_fd_sc_hd__or4_2", + "sky130_fd_sc_hd__xor2_1", "sky130_fd_sc_hd__xor2_2", "sky130_fd_sc_hd__xnor2_1", + "sky130_fd_sc_hd__xnor2_2", "sky130_fd_sc_hd__xor3_1", "sky130_fd_sc_hd__mux2_1", + "sky130_fd_sc_hd__mux2_2", "sky130_fd_sc_hd__mux2_4", "sky130_fd_sc_hd__mux4_1", + "sky130_fd_sc_hd__mux4_2", "sky130_fd_sc_hd__dfxtp_1", "sky130_fd_sc_hd__dfxtp_2", + "sky130_fd_sc_hd__dfxtp_4", "sky130_fd_sc_hd__dfxbp_1", "sky130_fd_sc_hd__dfxbp_2", + "sky130_fd_sc_hd__dfrtp_1", "sky130_fd_sc_hd__dfrtp_2", "sky130_fd_sc_hd__dfrtp_4", + "sky130_fd_sc_hd__dfsbp_1", "sky130_fd_sc_hd__dfsbp_2", "sky130_fd_sc_hd__dfstp_1", + "sky130_fd_sc_hd__dfstp_2", "sky130_fd_sc_hd__dfstp_4", "sky130_fd_sc_hd__dlxtp_1", + "sky130_fd_sc_hd__dlxbp_1", "sky130_fd_sc_hd__dlrtp_1", "sky130_fd_sc_hd__dlrtp_2", + "sky130_fd_sc_hd__dlrtp_4", "sky130_fd_sc_hd__a21o_1", "sky130_fd_sc_hd__a21o_2", + "sky130_fd_sc_hd__a21oi_1", "sky130_fd_sc_hd__a21oi_2", "sky130_fd_sc_hd__a22o_1", + "sky130_fd_sc_hd__a22oi_1", "sky130_fd_sc_hd__a31o_1", "sky130_fd_sc_hd__a31oi_1", + "sky130_fd_sc_hd__o21a_1", "sky130_fd_sc_hd__o21ai_1", "sky130_fd_sc_hd__o22a_1", + "sky130_fd_sc_hd__o22ai_1", "sky130_fd_sc_hd__conb_1", "sky130_fd_sc_hd__clkbuf_1", + "sky130_fd_sc_hd__clkbuf_2", "sky130_fd_sc_hd__clkbuf_4", "sky130_fd_sc_hd__clkbuf_8", + "sky130_fd_sc_hd__clkbuf_16", "sky130_fd_sc_hd__clkinv_1", "sky130_fd_sc_hd__clkinv_2", + "sky130_fd_sc_hd__clkinv_4", "sky130_fd_sc_hd__clkinv_8", "sky130_fd_sc_hd__clkinv_16", } +PDK_CELL_LIBS = {"sky130": SKY130_HD_CELLS} +ANALOG_PRIMITIVES = {"r", "c", "l", "v", "i", "d", "q", "m", "e", "f", "g", "h"} -def generate_config(cir_out_path, design_name=None, pdk="sky130", - clock_port="clk", clock_period_ns=None, output_dir="."): - print(f"[orfs_config_gen] Validating {cir_out_path} against {pdk} PDK...") - report = validate_netlist(cir_out_path, pdk) - PDKValidator.print_report(report) - if report["summary"] == "FAIL": - raise RuntimeError( - f"Validation FAILED - {len(report['unmappable'])} unmappable primitive(s). " - f"Fix the netlist before generating ORFS config." - ) +class SpiceNetlist: + def __init__(self, path): + self.path = Path(path) + self.title = "" + self.subcircuits = [] + self.top_instances = [] + self._parse() + + def _parse(self): + with open(self.path, "r", errors="replace") as f: + lines = f.readlines() + joined = self._join_continuations(lines) + current_subckt = None + for line in joined: + stripped = line.strip() + if not stripped or stripped.startswith("*"): + continue + lower = stripped.lower() + if not self.title and not lower.startswith("."): + self.title = stripped + continue + if lower.startswith(".subckt"): + parts = stripped.split() + current_subckt = {"name": parts[1] if len(parts) > 1 else "unnamed", "ports": parts[2:], "instances": []} + self.subcircuits.append(current_subckt) + elif lower.startswith(".ends"): + current_subckt = None + elif lower.startswith(".end"): + break + elif not lower.startswith("."): + inst = self._parse_instance(stripped) + if inst: + if current_subckt is not None: + current_subckt["instances"].append(inst) + else: + self.top_instances.append(inst) + + @staticmethod + def _join_continuations(lines): + result = [] + for line in lines: + if line.startswith("+") and result: + result[-1] = result[-1].rstrip() + " " + line[1:].strip() + else: + result.append(line) + return result - if design_name is None: - stem = Path(cir_out_path).stem - design_name = stem.replace(".cir", "").replace(".", "_") + @staticmethod + def _parse_instance(line): + parts = line.split() + if not parts: + return None + name = parts[0] + positional = [p for p in parts[1:] if "=" not in p] + cell_name = positional[-1] if positional else "" + return {"name": name, "type": name[0].lower(), "cell": cell_name, "raw": line} - defaults = PDK_DEFAULTS.get(pdk, PDK_DEFAULTS["sky130"]) - clk_period = str(clock_period_ns) if clock_period_ns else defaults["CLOCK_PERIOD"] - lines = [ - f"# ORFS config.mk - auto-generated by eSim-ORFS integration", - f"# Source netlist : {Path(cir_out_path).resolve()}", - f"# Generated : {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", - f"# PDK : {pdk}", - f"", - f"export DESIGN_NAME = {design_name}", - f"export PLATFORM = {defaults['PLATFORM']}", - f"", - f"export VERILOG_FILES = $(DESIGN_HOME)/src/$(DESIGN_NAME)/$(DESIGN_NAME).v", - f"export SDC_FILE = $(DESIGN_HOME)/src/$(DESIGN_NAME)/constraint.sdc", - f"export ABC_AREA = 0", - f"", - f"export CORE_UTILIZATION = {defaults['CORE_UTILIZATION']}", - f"export CORE_ASPECT_RATIO = {defaults['CORE_ASPECT_RATIO']}", - f"export CORE_MARGIN = {defaults['CORE_MARGIN']}", - f"", - f"export CLOCK_PORT = {clock_port}", - f"export CLOCK_PERIOD = {clk_period}", - f"", - f"export TARGET_DENSITY = {defaults['TARGET_DENSITY']}", - f"", - f"# Mapped cells ({len(report['valid_cells'])}):", - ] - for cell in report["valid_cells"]: - lines.append(f"# {cell['name']} -> {cell['cell']}") +class PDKValidator: + def __init__(self, pdk="sky130"): + if pdk not in PDK_CELL_LIBS: + raise ValueError(f"Unknown PDK '{pdk}'.") + self.pdk = pdk + self.known_cells = PDK_CELL_LIBS[pdk] - if report["analog_primitives"]: - lines.append(f"# Skipped analog primitives ({len(report['analog_primitives'])}):") - for a in report["analog_primitives"]: - lines.append(f"# {a['name']} ({a['type'].upper()})") + def validate(self, netlist): + valid_cells, unmappable, analog_found = [], [], [] + all_instances = list(netlist.top_instances) + for subckt in netlist.subcircuits: + all_instances.extend(subckt["instances"]) + for inst in all_instances: + ctype = inst["type"] + cell = inst["cell"] + if ctype == "x": + if cell in self.known_cells: + valid_cells.append({"name": inst["name"], "cell": cell}) + else: + unmappable.append({"name": inst["name"], "cell": cell, "reason": f"Not found in {self.pdk} standard cell library"}) + elif ctype in ANALOG_PRIMITIVES: + analog_found.append({"name": inst["name"], "type": ctype}) + else: + unmappable.append({"name": inst["name"], "cell": cell, "reason": f"Unknown component type '{ctype}'"}) + return { + "netlist": str(netlist.path), "pdk": self.pdk, + "valid_cells": valid_cells, "unmappable": unmappable, + "analog_primitives": analog_found, + "summary": "PASS" if len(unmappable) == 0 else "FAIL", + } - out_path = Path(output_dir) / "config.mk" - out_path.parent.mkdir(parents=True, exist_ok=True) - out_path.write_text("\n".join(lines) + "\n") - print(f"[orfs_config_gen] config.mk written to: {out_path.resolve()}") - return str(out_path) + @staticmethod + def print_report(report): + print(f"\n{'='*60}\nPDK Validation Report") + print(f" Netlist : {report['netlist']}\n PDK : {report['pdk']}\n Result : {report['summary']}") + print(f"{'='*60}") + if report["valid_cells"]: + print(f"\n[OK] Mapped cells ({len(report['valid_cells'])}):") + for c in report["valid_cells"]: + print(f" {c['name']:30s} -> {c['cell']}") + if report["analog_primitives"]: + print(f"\n[SKIP] Analog primitives ({len(report['analog_primitives'])}):") + for a in report["analog_primitives"]: + print(f" {a['name']:30s} ({a['type'].upper()})") + if report["unmappable"]: + print(f"\n[FAIL] Unmappable ({len(report['unmappable'])}):") + for u in report["unmappable"]: + print(f" {u['name']:30s} -> {u['reason']}") + print() + + +def validate_netlist(cir_out_path, pdk="sky130"): + return PDKValidator(pdk).validate(SpiceNetlist(cir_out_path)) if __name__ == "__main__": - import argparse - parser = argparse.ArgumentParser() - parser.add_argument("netlist") - parser.add_argument("--design") - parser.add_argument("--pdk", default="sky130") - parser.add_argument("--clock-port", default="clk") - parser.add_argument("--clock-period", type=float) - parser.add_argument("--output-dir", default=".") - args = parser.parse_args() - try: - generate_config(args.netlist, args.design, args.pdk, - args.clock_port, args.clock_period, args.output_dir) - except RuntimeError as e: - print(f"\n[ERROR] {e}") - sys.exit(1) \ No newline at end of file + if len(sys.argv) < 2: + print("Usage: python pdk_validator.py [pdk]") + sys.exit(1) + report = validate_netlist(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else "sky130") + PDKValidator.print_report(report) + sys.exit(0 if report["summary"] == "PASS" else 1) From 88a259fa7d54987a67667a744dcc90254fba528b Mon Sep 17 00:00:00 2001 From: Divinesoumyadip Date: Fri, 6 Mar 2026 16:25:36 +0530 Subject: [PATCH 4/6] esim: add ORFS config.mk auto-generator --- src/orfs_integration/orfs_config_gen.py | 73 +++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/orfs_integration/orfs_config_gen.py b/src/orfs_integration/orfs_config_gen.py index e69de29bb..8eab8643e 100644 --- a/src/orfs_integration/orfs_config_gen.py +++ b/src/orfs_integration/orfs_config_gen.py @@ -0,0 +1,73 @@ +import sys +from pathlib import Path +from datetime import datetime +from pdk_validator import validate_netlist, PDKValidator + +PDK_DEFAULTS = { + "sky130": { + "PLATFORM": "sky130hd", + "SCL": "sky130_fd_sc_hd", + "TARGET_DENSITY": "0.65", + "CLOCK_PERIOD": "10.0", + "CORE_UTILIZATION": "40", + "CORE_ASPECT_RATIO": "1", + "CORE_MARGIN": "2", + } +} + +def generate_config(cir_out_path, design_name=None, pdk="sky130", + clock_port="clk", clock_period_ns=None, output_dir="."): + print(f"[orfs_config_gen] Validating {cir_out_path} against {pdk} PDK...") + report = validate_netlist(cir_out_path, pdk) + PDKValidator.print_report(report) + if report["summary"] == "FAIL": + raise RuntimeError(f"Validation FAILED - {len(report['unmappable'])} unmappable primitive(s).") + if design_name is None: + design_name = Path(cir_out_path).stem.replace(".cir", "").replace(".", "_") + defaults = PDK_DEFAULTS.get(pdk, PDK_DEFAULTS["sky130"]) + clk_period = str(clock_period_ns) if clock_period_ns else defaults["CLOCK_PERIOD"] + lines = [ + f"# ORFS config.mk - auto-generated by eSim-ORFS integration", + f"# Source : {Path(cir_out_path).resolve()}", + f"# Generated : {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", + f"# PDK : {pdk}", f"", + f"export DESIGN_NAME = {design_name}", + f"export PLATFORM = {defaults['PLATFORM']}", f"", + f"export VERILOG_FILES = $(DESIGN_HOME)/src/$(DESIGN_NAME)/$(DESIGN_NAME).v", + f"export SDC_FILE = $(DESIGN_HOME)/src/$(DESIGN_NAME)/constraint.sdc", + f"export ABC_AREA = 0", f"", + f"export CORE_UTILIZATION = {defaults['CORE_UTILIZATION']}", + f"export CORE_ASPECT_RATIO = {defaults['CORE_ASPECT_RATIO']}", + f"export CORE_MARGIN = {defaults['CORE_MARGIN']}", f"", + f"export CLOCK_PORT = {clock_port}", + f"export CLOCK_PERIOD = {clk_period}", f"", + f"export TARGET_DENSITY = {defaults['TARGET_DENSITY']}", f"", + f"# Mapped cells ({len(report['valid_cells'])}):", + ] + for cell in report["valid_cells"]: + lines.append(f"# {cell['name']} -> {cell['cell']}") + if report["analog_primitives"]: + lines.append(f"# Skipped analog ({len(report['analog_primitives'])}):") + for a in report["analog_primitives"]: + lines.append(f"# {a['name']} ({a['type'].upper()})") + out_path = Path(output_dir) / "config.mk" + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_text("\n".join(lines) + "\n") + print(f"[orfs_config_gen] config.mk written to: {out_path.resolve()}") + return str(out_path) + +if __name__ == "__main__": + import argparse + parser = argparse.ArgumentParser() + parser.add_argument("netlist") + parser.add_argument("--design") + parser.add_argument("--pdk", default="sky130") + parser.add_argument("--clock-port", default="clk") + parser.add_argument("--clock-period", type=float) + parser.add_argument("--output-dir", default=".") + args = parser.parse_args() + try: + generate_config(args.netlist, args.design, args.pdk, args.clock_port, args.clock_period, args.output_dir) + except RuntimeError as e: + print(f"\n[ERROR] {e}") + sys.exit(1) From 0f655ac3c1e741c91beb4f4fa5ed7fdf2064fc2b Mon Sep 17 00:00:00 2001 From: Divinesoumyadip Date: Fri, 6 Mar 2026 18:00:03 +0530 Subject: [PATCH 5/6] esim: add test netlists and fix hierarchical subcircuit handling --- src/orfs_integration/pdk_validator.py | 23 ++++++++++++++++--- .../tests/test_basic_gates.cir.out | 11 +++++++++ .../tests/test_hierarchical.cir.out | 14 +++++++++++ .../tests/test_parameterized.cir.out | 10 ++++++++ 4 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 src/orfs_integration/tests/test_basic_gates.cir.out create mode 100644 src/orfs_integration/tests/test_hierarchical.cir.out create mode 100644 src/orfs_integration/tests/test_parameterized.cir.out diff --git a/src/orfs_integration/pdk_validator.py b/src/orfs_integration/pdk_validator.py index 4d7d132f2..23845aaee 100644 --- a/src/orfs_integration/pdk_validator.py +++ b/src/orfs_integration/pdk_validator.py @@ -47,20 +47,31 @@ def __init__(self, path): self.title = "" self.subcircuits = [] self.top_instances = [] + self.internal_subckt_names = set() self._parse() def _parse(self): with open(self.path, "r", errors="replace") as f: lines = f.readlines() joined = self._join_continuations(lines) + # First pass: collect all internal subcircuit names + for line in joined: + stripped = line.strip() + if stripped.lower().startswith(".subckt"): + parts = stripped.split() + if len(parts) > 1: + self.internal_subckt_names.add(parts[1].lower()) + # Second pass: parse instances current_subckt = None + title_set = False for line in joined: stripped = line.strip() if not stripped or stripped.startswith("*"): continue lower = stripped.lower() - if not self.title and not lower.startswith("."): + if not title_set and not lower.startswith("."): self.title = stripped + title_set = True continue if lower.startswith(".subckt"): parts = stripped.split() @@ -107,7 +118,7 @@ def __init__(self, pdk="sky130"): self.known_cells = PDK_CELL_LIBS[pdk] def validate(self, netlist): - valid_cells, unmappable, analog_found = [], [], [] + valid_cells, unmappable, analog_found, internal_calls = [], [], [], [] all_instances = list(netlist.top_instances) for subckt in netlist.subcircuits: all_instances.extend(subckt["instances"]) @@ -117,6 +128,8 @@ def validate(self, netlist): if ctype == "x": if cell in self.known_cells: valid_cells.append({"name": inst["name"], "cell": cell}) + elif cell.lower() in netlist.internal_subckt_names: + internal_calls.append({"name": inst["name"], "cell": cell}) else: unmappable.append({"name": inst["name"], "cell": cell, "reason": f"Not found in {self.pdk} standard cell library"}) elif ctype in ANALOG_PRIMITIVES: @@ -126,7 +139,7 @@ def validate(self, netlist): return { "netlist": str(netlist.path), "pdk": self.pdk, "valid_cells": valid_cells, "unmappable": unmappable, - "analog_primitives": analog_found, + "analog_primitives": analog_found, "internal_calls": internal_calls, "summary": "PASS" if len(unmappable) == 0 else "FAIL", } @@ -139,6 +152,10 @@ def print_report(report): print(f"\n[OK] Mapped cells ({len(report['valid_cells'])}):") for c in report["valid_cells"]: print(f" {c['name']:30s} -> {c['cell']}") + if report.get("internal_calls"): + print(f"\n[HIER] Internal subcircuit calls ({len(report['internal_calls'])}):") + for c in report["internal_calls"]: + print(f" {c['name']:30s} -> {c['cell']} (user-defined)") if report["analog_primitives"]: print(f"\n[SKIP] Analog primitives ({len(report['analog_primitives'])}):") for a in report["analog_primitives"]: diff --git a/src/orfs_integration/tests/test_basic_gates.cir.out b/src/orfs_integration/tests/test_basic_gates.cir.out new file mode 100644 index 000000000..d19c05516 --- /dev/null +++ b/src/orfs_integration/tests/test_basic_gates.cir.out @@ -0,0 +1,11 @@ +* Basic logic gates test +.subckt basic_gates A B C OUT VDD GND +Xinv1 A net1 VDD GND sky130_fd_sc_hd__inv_1 +Xnand1 A B net2 VDD GND sky130_fd_sc_hd__nand2_1 +Xnor1 A B net3 VDD GND sky130_fd_sc_hd__nor2_1 +Xand1 net1 net2 net4 VDD GND sky130_fd_sc_hd__and2_2 +Xor1 net3 net4 OUT VDD GND sky130_fd_sc_hd__or2_2 +.ends basic_gates +Vvdd VDD GND 1.8 +Vgnd GND 0 0 +.end diff --git a/src/orfs_integration/tests/test_hierarchical.cir.out b/src/orfs_integration/tests/test_hierarchical.cir.out new file mode 100644 index 000000000..bc134e167 --- /dev/null +++ b/src/orfs_integration/tests/test_hierarchical.cir.out @@ -0,0 +1,14 @@ +* Hierarchical subcircuit test +.subckt half_adder A B Sum Cout VDD GND +Xxor1 A B Sum VDD GND sky130_fd_sc_hd__xor2_1 +Xand1 A B Cout VDD GND sky130_fd_sc_hd__and2_2 +.ends half_adder + +.subckt full_adder A B Cin Sum Cout VDD GND +Xha1 A B s1 c1 VDD GND half_adder +Xha2 s1 Cin Sum c2 VDD GND half_adder +Xor1 c1 c2 Cout VDD GND sky130_fd_sc_hd__or2_2 +.ends full_adder +Vvdd VDD GND 1.8 +Vgnd GND 0 0 +.end diff --git a/src/orfs_integration/tests/test_parameterized.cir.out b/src/orfs_integration/tests/test_parameterized.cir.out new file mode 100644 index 000000000..08af82ec6 --- /dev/null +++ b/src/orfs_integration/tests/test_parameterized.cir.out @@ -0,0 +1,10 @@ +* Parameterized components edge case test +.subckt param_test IN OUT VDD GND +Xinv1 IN net1 VDD GND sky130_fd_sc_hd__inv_1 +Xbuf1 net1 OUT VDD GND sky130_fd_sc_hd__buf_4 +R1 IN net1 R=1k +C1 net1 GND C=10f +M1 OUT IN GND GND nmos W=0.5u L=0.18u +.ends param_test +Vvdd VDD GND 1.8 +.end From c0f349a68ea7a0ceb4b93ea691728a6540d9c493 Mon Sep 17 00:00:00 2001 From: Divinesoumyadip Date: Fri, 6 Mar 2026 21:12:42 +0530 Subject: [PATCH 6/6] esim: add netlist-to-verilog converter with hierarchical subcircuit support --- src/orfs_integration/netlist_to_verilog.py | 214 ++++++++++++++++++ .../tests/test_basic_gates.cir.v | 24 ++ .../tests/test_hierarchical.cir.v | 46 ++++ 3 files changed, 284 insertions(+) create mode 100644 src/orfs_integration/netlist_to_verilog.py create mode 100644 src/orfs_integration/tests/test_basic_gates.cir.v create mode 100644 src/orfs_integration/tests/test_hierarchical.cir.v diff --git a/src/orfs_integration/netlist_to_verilog.py b/src/orfs_integration/netlist_to_verilog.py new file mode 100644 index 000000000..8ed4f7d24 --- /dev/null +++ b/src/orfs_integration/netlist_to_verilog.py @@ -0,0 +1,214 @@ +""" +netlist_to_verilog.py + +Converts a validated eSim .cir.out netlist into synthesizable Verilog. +Works on top of pdk_validator to reuse the parsed netlist structure. +""" + +import sys +from pathlib import Path +from pdk_validator import SpiceNetlist, ANALOG_PRIMITIVES + + +# Maps sky130 cell names to their Verilog primitive equivalents +# Format: cell_name -> (verilog_primitive, output_port, input_ports) +CELL_TO_VERILOG = { + # Inverters + "sky130_fd_sc_hd__inv_1": ("not", "Y", ["A"]), + "sky130_fd_sc_hd__inv_2": ("not", "Y", ["A"]), + "sky130_fd_sc_hd__inv_4": ("not", "Y", ["A"]), + "sky130_fd_sc_hd__inv_8": ("not", "Y", ["A"]), + # Buffers + "sky130_fd_sc_hd__buf_1": ("buf", "X", ["A"]), + "sky130_fd_sc_hd__buf_2": ("buf", "X", ["A"]), + "sky130_fd_sc_hd__buf_4": ("buf", "X", ["A"]), + "sky130_fd_sc_hd__buf_8": ("buf", "X", ["A"]), + # NAND + "sky130_fd_sc_hd__nand2_1": ("nand", "Y", ["A", "B"]), + "sky130_fd_sc_hd__nand2_2": ("nand", "Y", ["A", "B"]), + "sky130_fd_sc_hd__nand2_4": ("nand", "Y", ["A", "B"]), + "sky130_fd_sc_hd__nand3_1": ("nand", "Y", ["A", "B", "C"]), + "sky130_fd_sc_hd__nand3_2": ("nand", "Y", ["A", "B", "C"]), + "sky130_fd_sc_hd__nand4_1": ("nand", "Y", ["A", "B", "C", "D"]), + # NOR + "sky130_fd_sc_hd__nor2_1": ("nor", "Y", ["A", "B"]), + "sky130_fd_sc_hd__nor2_2": ("nor", "Y", ["A", "B"]), + "sky130_fd_sc_hd__nor3_1": ("nor", "Y", ["A", "B", "C"]), + "sky130_fd_sc_hd__nor4_1": ("nor", "Y", ["A", "B", "C", "D"]), + # AND + "sky130_fd_sc_hd__and2_0": ("and", "X", ["A", "B"]), + "sky130_fd_sc_hd__and2_2": ("and", "X", ["A", "B"]), + "sky130_fd_sc_hd__and2_4": ("and", "X", ["A", "B"]), + "sky130_fd_sc_hd__and3_1": ("and", "X", ["A", "B", "C"]), + "sky130_fd_sc_hd__and4_1": ("and", "X", ["A", "B", "C", "D"]), + # OR + "sky130_fd_sc_hd__or2_0": ("or", "X", ["A", "B"]), + "sky130_fd_sc_hd__or2_2": ("or", "X", ["A", "B"]), + "sky130_fd_sc_hd__or2_4": ("or", "X", ["A", "B"]), + "sky130_fd_sc_hd__or3_1": ("or", "X", ["A", "B", "C"]), + "sky130_fd_sc_hd__or4_1": ("or", "X", ["A", "B", "C", "D"]), + # XOR + "sky130_fd_sc_hd__xor2_1": ("xor", "X", ["A", "B"]), + "sky130_fd_sc_hd__xor2_2": ("xor", "X", ["A", "B"]), + "sky130_fd_sc_hd__xnor2_1": ("xnor", "Y", ["A", "B"]), + "sky130_fd_sc_hd__xnor2_2": ("xnor", "Y", ["A", "B"]), + # Flip-flops (D type, positive edge) + "sky130_fd_sc_hd__dfxtp_1": ("dff", "Q", ["D", "CLK"]), + "sky130_fd_sc_hd__dfxtp_2": ("dff", "Q", ["D", "CLK"]), + "sky130_fd_sc_hd__dfxtp_4": ("dff", "Q", ["D", "CLK"]), + # Clock buffers (treated as regular buffers in Verilog) + "sky130_fd_sc_hd__clkbuf_1": ("buf", "X", ["A"]), + "sky130_fd_sc_hd__clkbuf_2": ("buf", "X", ["A"]), + "sky130_fd_sc_hd__clkbuf_4": ("buf", "X", ["A"]), + "sky130_fd_sc_hd__clkbuf_8": ("buf", "X", ["A"]), +} + + +def get_digital_ports(subckt_ports): + # Filter out power/ground pins — not needed in synthesizable Verilog + skip = {"vdd", "vcc", "gnd", "vss", "vpwr", "vgnd"} + return [p for p in subckt_ports if p.lower() not in skip] + + +def infer_port_direction(port, instances): + # If a port drives an instance input it's an input, if it comes from an output it's output + # Simple heuristic: assume first ports are inputs, last is output + # A proper implementation would do connectivity analysis + driven_nets = set() + for inst in instances: + parts = inst["raw"].split() + positional = [p for p in parts[1:] if "=" not in p] + if len(positional) >= 2: + # Last positional before power pins is the cell — nets are everything else + cell = positional[-1] + nets = positional[:-1] + # First net after instance name is usually output for most gates + if nets: + driven_nets.add(nets[0]) + return driven_nets + + +def convert_subckt_to_verilog(subckt, internal_subckt_names): + name = subckt["name"] + ports = get_digital_ports(subckt["ports"]) + instances = subckt["instances"] + + # Collect all internal wire names + all_nets = set() + for inst in instances: + parts = inst["raw"].split() + positional = [p for p in parts[1:] if "=" not in p] + # exclude cell name and power pins + skip = {"vdd", "vcc", "gnd", "vss", "vpwr", "vgnd"} + nets = [p for p in positional[:-1] if p.lower() not in skip] + all_nets.update(nets) + + internal_wires = [n for n in all_nets if n not in ports] + + lines = [] + lines.append(f"module {name} (") + lines.append(f" {', '.join(ports)}") + lines.append(f");") + lines.append("") + + # Port declarations — simple heuristic: last port is output, rest are inputs + if ports: + for p in ports[:-1]: + lines.append(f" input wire {p};") + lines.append(f" output wire {ports[-1]};") + + if internal_wires: + lines.append("") + for w in sorted(internal_wires): + lines.append(f" wire {w};") + + lines.append("") + + # Instance declarations + for inst in instances: + ctype = inst["type"] + cell = inst["cell"] + iname = inst["name"] + + # Skip analog primitives + if ctype in ANALOG_PRIMITIVES: + continue + + # Skip power sources + if ctype == "v": + continue + + parts = inst["raw"].split() + positional = [p for p in parts[1:] if "=" not in p] + skip_pins = {"vdd", "vcc", "gnd", "vss", "vpwr", "vgnd"} + nets = [p for p in positional[:-1] if p.lower() not in skip_pins] + + if cell in internal_subckt_names: + # Hierarchical instance — emit as module instantiation + lines.append(f" {cell} {iname} (") + port_connects = [f" .port{i}({n})" for i, n in enumerate(nets)] + lines.append(",\n".join(port_connects)) + lines.append(f" );") + elif cell in CELL_TO_VERILOG: + primitive, out_port, in_ports = CELL_TO_VERILOG[cell] + + if primitive == "dff": + # Emit always block for flip-flop + if len(nets) >= 2: + q_net, d_net, clk_net = nets[0], nets[1], nets[2] if len(nets) > 2 else "clk" + lines.append(f" // {iname} : D flip-flop") + lines.append(f" reg {q_net}_reg;") + lines.append(f" always @(posedge {clk_net}) {q_net}_reg <= {d_net};") + lines.append(f" assign {q_net} = {q_net}_reg;") + else: + # Gate primitive + if nets: + out_net = nets[0] + in_nets = nets[1:] if len(nets) > 1 else nets + lines.append(f" {primitive} {iname} ({out_net}, {', '.join(in_nets)});") + else: + lines.append(f" // WARNING: {iname} uses unmapped cell '{cell}' - skipped") + + lines.append("") + lines.append("endmodule") + lines.append("") + return "\n".join(lines) + + +def convert(cir_out_path, output_path=None): + netlist = SpiceNetlist(cir_out_path) + + if not netlist.subcircuits: + print("[netlist_to_verilog] No subcircuits found in netlist.") + return None + + if output_path is None: + output_path = str(Path(cir_out_path).with_suffix(".v")) + + verilog_blocks = [] + verilog_blocks.append(f"// Auto-generated Verilog from eSim netlist") + verilog_blocks.append(f"// Source: {Path(cir_out_path).name}") + verilog_blocks.append(f"// Tool : eSim-ORFS netlist_to_verilog.py") + verilog_blocks.append("") + + for subckt in netlist.subcircuits: + print(f"[netlist_to_verilog] Converting subcircuit: {subckt['name']}") + verilog_blocks.append( + convert_subckt_to_verilog(subckt, netlist.internal_subckt_names) + ) + + verilog_output = "\n".join(verilog_blocks) + Path(output_path).write_text(verilog_output) + print(f"[netlist_to_verilog] Verilog written to: {output_path}") + return output_path + + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: python netlist_to_verilog.py [output.v]") + sys.exit(1) + out = convert(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else None) + if out: + print("\n--- Generated Verilog ---") + print(Path(out).read_text()) + diff --git a/src/orfs_integration/tests/test_basic_gates.cir.v b/src/orfs_integration/tests/test_basic_gates.cir.v new file mode 100644 index 000000000..de3d92908 --- /dev/null +++ b/src/orfs_integration/tests/test_basic_gates.cir.v @@ -0,0 +1,24 @@ +// Auto-generated Verilog from eSim netlist +// Source: test_basic_gates.cir.out +// Tool : eSim-ORFS netlist_to_verilog.py + +module basic_gates ( + A, B, C, OUT +); + + input wire A; + input wire B; + input wire C; + output wire OUT; + + wire net1; + wire net2; + wire net3; + wire net4; + + nand Xnand1 (A, B, net2); + nor Xnor1 (A, B, net3); + and Xand1 (net1, net2, net4); + or Xor1 (net3, net4, OUT); + +endmodule diff --git a/src/orfs_integration/tests/test_hierarchical.cir.v b/src/orfs_integration/tests/test_hierarchical.cir.v new file mode 100644 index 000000000..0f0f5b8e8 --- /dev/null +++ b/src/orfs_integration/tests/test_hierarchical.cir.v @@ -0,0 +1,46 @@ +// Auto-generated Verilog from eSim netlist +// Source: test_hierarchical.cir.out +// Tool : eSim-ORFS netlist_to_verilog.py + +module half_adder ( + A, B, Sum, Cout +); + + input wire A; + input wire B; + input wire Sum; + output wire Cout; + + and Xand1 (A, B, Cout); + +endmodule + +module full_adder ( + A, B, Cin, Sum, Cout +); + + input wire A; + input wire B; + input wire Cin; + input wire Sum; + output wire Cout; + + wire c1; + wire c2; + wire s1; + + half_adder Xha1 ( + .port0(A), + .port1(B), + .port2(s1), + .port3(c1) + ); + half_adder Xha2 ( + .port0(s1), + .port1(Cin), + .port2(Sum), + .port3(c2) + ); + or Xor1 (c1, c2, Cout); + +endmodule