From 0a7cf3b067eb2f9a4fce6ab5d3dc7ba2652bf2f5 Mon Sep 17 00:00:00 2001 From: mole99 Date: Tue, 14 Jan 2025 17:05:01 +0100 Subject: [PATCH 1/4] Add default pcells to flatten in gds --- cace/common/cace_regenerate.py | 4 +++- cace/parameter/parameter_magic_antenna_check.py | 17 +++++++---------- cace/parameter/parameter_magic_drc.py | 14 ++++---------- 3 files changed, 14 insertions(+), 21 deletions(-) diff --git a/cace/common/cace_regenerate.py b/cace/common/cace_regenerate.py index 558b5ce..47939b3 100755 --- a/cace/common/cace_regenerate.py +++ b/cace/common/cace_regenerate.py @@ -355,7 +355,8 @@ def regenerate_netlist(datasheet, netlist_source, runtime_options, pex=False): magic_input += f'path search +{os.path.abspath(os.path.dirname(layout_filepath))}\n' magic_input += f'load {os.path.basename(layout_filepath)}\n' else: - # magic_input += 'gds flatglob *\n' + magic_input += 'gds flatglob guard_ring_gen*\n' + magic_input += 'gds flatglob vias_gen*\n' magic_input += f'gds read {layout_filepath}\n' magic_input += f'load {dname}\n' # Use readspice to get the port order @@ -366,6 +367,7 @@ def regenerate_netlist(datasheet, netlist_source, runtime_options, pex=False): if netlist_source == 'layout' or netlist_source == 'pex': magic_input += f'select top cell\n' magic_input += 'expand\n' + magic_input += 'extract path cace_extfiles\n' if netlist_source == 'layout': magic_input += 'extract no all\n' diff --git a/cace/parameter/parameter_magic_antenna_check.py b/cace/parameter/parameter_magic_antenna_check.py index 3b049ea..79f0f9e 100755 --- a/cace/parameter/parameter_magic_antenna_check.py +++ b/cace/parameter/parameter_magic_antenna_check.py @@ -56,6 +56,7 @@ def __init__( self.add_result(Result('antenna_violations')) self.add_argument(Argument('args', [], False)) + self.add_argument(Argument('gds_flatten', False, False)) def is_runnable(self): netlist_source = self.runtime_options['netlist_source'] @@ -106,17 +107,13 @@ def implementation(self): magic_input += f'path search +{os.path.abspath(os.path.dirname(layout_filepath))}\n' magic_input += f'load {os.path.basename(layout_filepath)}\n' else: + if self.get_argument('gds_flatten'): + magic_input += 'gds flatglob *\n' + else: + magic_input += 'gds flatglob guard_ring_gen*\n' + magic_input += 'gds flatglob vias_gen*\n' magic_input += f'gds read {os.path.abspath(layout_filepath)}\n' - magic_input += 'set toplist [cellname list top]\n' - magic_input += 'set numtop [llength $toplist]\n' - magic_input += 'if {$numtop > 1} {\n' - magic_input += ' foreach topcell $toplist {\n' - magic_input += ' if {$topcell != "(UNNAMED)"} {\n' - magic_input += ' load $topcell\n' - magic_input += ' break\n' - magic_input += ' }\n' - magic_input += ' }\n' - magic_input += '}\n' + magic_input += f'load {projname}\n' magic_input += 'select top cell\n' magic_input += 'expand\n' diff --git a/cace/parameter/parameter_magic_drc.py b/cace/parameter/parameter_magic_drc.py index c587728..2c1bfb1 100755 --- a/cace/parameter/parameter_magic_drc.py +++ b/cace/parameter/parameter_magic_drc.py @@ -102,17 +102,11 @@ def implementation(self): else: if self.get_argument('gds_flatten'): magic_input += 'gds flatglob *\n' + else: + magic_input += 'gds flatglob guard_ring_gen*\n' + magic_input += 'gds flatglob vias_gen*\n' magic_input += f'gds read {os.path.abspath(layout_filepath)}\n' - magic_input += 'set toplist [cellname list top]\n' - magic_input += 'set numtop [llength $toplist]\n' - magic_input += 'if {$numtop > 1} {\n' - magic_input += ' foreach topcell $toplist {\n' - magic_input += ' if {$topcell != "(UNNAMED)"} {\n' - magic_input += ' load $topcell\n' - magic_input += ' break\n' - magic_input += ' }\n' - magic_input += ' }\n' - magic_input += '}\n' + magic_input += f'load {projname}\n' magic_input += 'drc on\n' magic_input += 'catch {drc style drc(full)}\n' From ecaf8dda95a7512fbebc1c381047e90ab7291327 Mon Sep 17 00:00:00 2001 From: mole99 Date: Tue, 14 Jan 2025 18:04:00 +0100 Subject: [PATCH 2/4] Add KLayout LVS tool --- cace/parameter/__init__.py | 1 + cace/parameter/parameter_klayout_drc.py | 8 +- cace/parameter/parameter_klayout_lvs.py | 200 ++++++++++++++++++++++++ 3 files changed, 205 insertions(+), 4 deletions(-) create mode 100755 cace/parameter/parameter_klayout_lvs.py diff --git a/cace/parameter/__init__.py b/cace/parameter/__init__.py index 097bc9a..b263ef3 100755 --- a/cace/parameter/__init__.py +++ b/cace/parameter/__init__.py @@ -20,3 +20,4 @@ from .parameter_magic_antenna_check import ParameterMagicAntennaCheck from .parameter_ngspice import ParameterNgspice from .parameter_klayout_drc import ParameterKLayoutDRC +from .parameter_klayout_lvs import ParameterKLayoutLVS diff --git a/cace/parameter/parameter_klayout_drc.py b/cace/parameter/parameter_klayout_drc.py index 21d0c89..e9effdb 100755 --- a/cace/parameter/parameter_klayout_drc.py +++ b/cace/parameter/parameter_klayout_drc.py @@ -71,7 +71,7 @@ def implementation(self): projname = self.datasheet['name'] paths = self.datasheet['paths'] - info('Running KLayout to get layout DRC report.') + info('Running KLayout to get DRC report.') # Get the path to the layout, only GDS (layout_filepath, is_magic) = get_layout_path( @@ -93,13 +93,13 @@ def implementation(self): f'{self.datasheet["PDK"]}_mr.drc', ) - report_file_path = os.path.join(self.param_dir, 'report.xml') - if not os.path.exists(drc_script_path): err(f'DRC script {drc_script_path} does not exist!') self.result_type = ResultType.ERROR return + report_file_path = os.path.join(self.param_dir, 'report.xml') + arguments = [] # PDK specific arguments @@ -111,7 +111,7 @@ def implementation(self): '-rd', f'input={os.path.abspath(layout_filepath)}', '-rd', - f'topcell={projname}', + f'top_cell={projname}', '-rd', f'report={report_file_path}', '-rd', diff --git a/cace/parameter/parameter_klayout_lvs.py b/cace/parameter/parameter_klayout_lvs.py new file mode 100755 index 0000000..2cebaf2 --- /dev/null +++ b/cace/parameter/parameter_klayout_lvs.py @@ -0,0 +1,200 @@ +# Copyright 2024 Efabless Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import re +import sys +import math +import json + +from ..common.common import run_subprocess, get_pdk_root, get_layout_path + +from .parameter import Parameter, ResultType, Argument, Result +from .parameter_manager import register_parameter +from ..logging import ( + dbg, + verbose, + info, + subproc, + rule, + success, + warn, + err, +) + + +@register_parameter('klayout_lvs') +class ParameterKLayoutLVS(Parameter): + """ + Run LVS using KLayout + """ + + def __init__( + self, + *args, + **kwargs, + ): + super().__init__( + *args, + **kwargs, + ) + + self.add_result(Result('lvs_errors')) + + self.add_argument(Argument('args', [], False)) + self.add_argument(Argument('script', None, False)) + + def is_runnable(self): + netlist_source = self.runtime_options['netlist_source'] + + if netlist_source == 'schematic': + info('Netlist source is schematic capture. Not running LVS.') + self.result_type = ResultType.SKIPPED + return False + + return True + + def implementation(self): + + self.cancel_point() + + # Acquire a job from the global jobs semaphore + with self.jobs_sem: + + info('Running KLayout to get LVS report.') + + projname = self.datasheet['name'] + paths = self.datasheet['paths'] + root_path = self.paths['root'] + + # Make sure that schematic netlist exist, + schem_netlist = None + + if 'netlist' in paths: + schem_netlist_path = os.path.join( + paths['netlist'], 'schematic' + ) + schem_netlist = os.path.join( + schem_netlist_path, projname + '.spice' + ) + schem_netlist = os.path.abspath(schem_netlist) + + if not schem_netlist or not os.path.isfile(schem_netlist): + err( + 'Schematic-captured netlist does not exist. Cannot run LVS' + ) + self.result_type = ResultType.ERROR + return + + # Get the path to the layout, only GDS + (layout_filepath, is_magic) = get_layout_path( + projname, self.paths, check_magic=False + ) + + # Check if layout exists + if not os.path.isfile(layout_filepath): + err('No layout found!') + self.result_type = ResultType.ERROR + return + + if self.get_argument('script'): + lvs_script_path = os.path.abspath( + os.path.join(scriptspath, self.get_argument('script')) + ) + else: + lvs_script_path = os.path.join( + get_pdk_root(), + self.datasheet['PDK'], + 'libs.tech', + 'klayout', + 'lvs', + 'sky130.lvs', + ) + + if not os.path.exists(lvs_script_path): + err(f'LVS script {lvs_script_path} does not exist!') + self.result_type = ResultType.ERROR + return + + report_file_path = os.path.join(self.param_dir, f'{projname}.lvsdb') + + # PDK specific arguments + if self.datasheet['PDK'].startswith('sky130'): + arguments = [ + '-b', + '-r', + lvs_script_path, + '-rd', + f'input={os.path.abspath(layout_filepath)}', + '-rd', + f'top_cell={projname}', + '-rd', + f'schematic={schem_netlist}', + '-rd', + f'report={report_file_path}', + '-rd', + f'report={report_file_path}', + '-rd', + f'target_netlist={os.path.abspath(os.path.join(self.param_dir, projname + ".cir"))}', + '-rd', + f'thr={os.cpu_count()}', # TODO how to distribute cores? + ] + + returncode = self.run_subprocess( + 'klayout', + arguments + self.get_argument('args'), + cwd=self.param_dir, + ) + + if not os.path.isfile(report_file_path): + err('No output file generated by KLayout!') + err(f'Expected file: {report_file_path}') + self.result_type = ResultType.ERROR + return + + info( + f"KLayout LVS report at '[repr.filename][link=file://{os.path.abspath(report_file_path)}]{os.path.relpath(report_file_path)}[/link][/repr.filename]'…" + ) + + # Advance progress bar + if self.step_cb: + self.step_cb(self.param) + + # Match for errors in the .lvsdb file + lvsrex = re.compile(r"M\(E B\('.*'\)\)") + + # Get the result + try: + with open(report_file_path) as klayout_xml_report: + size = os.fstat(klayout_xml_report.fileno()).st_size + if size == 0: + err(f'File {report_file_path} is of size 0.') + self.result_type = ResultType.ERROR + return + lvs_content = klayout_xml_report.read() + lvs_count = len(lvsrex.findall(lvs_content)) + + self.result_type = ResultType.SUCCESS + self.get_result('lvs_errors').values = [lvs_count] + return + + # Catch reports not found + except FileNotFoundError as e: + err(f'Failed to generate {report_file_path}: {e}') + self.result_type = ResultType.ERROR + return + except (IOError, OSError) as e: + err(f'Failed to generate {report_file_path}: {e}') + self.result_type = ResultType.ERROR + return From 93eec81cb8248254141eafffe45ab57e28b0df87 Mon Sep 17 00:00:00 2001 From: mole99 Date: Tue, 14 Jan 2025 18:04:29 +0100 Subject: [PATCH 3/4] Add documentation for KLayout LVS --- docs/source/reference_manual/tools.md | 30 ++++++++++++++++++++------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/docs/source/reference_manual/tools.md b/docs/source/reference_manual/tools.md index 6570220..614b5cd 100644 --- a/docs/source/reference_manual/tools.md +++ b/docs/source/reference_manual/tools.md @@ -52,11 +52,29 @@ Perform the magic antenna check to find antenna violations in the layout. Arguments: - `args`: `` Additional args that are passed to magic. +- `gds_flatten`: `` Flatten the GDSII layout prior to running DRC. Results: - `antenna_violations`: `` The number of antenna violations. +## `netgen_lvs` + +```{note} +The `netgen_lvs` tool always compares the `schematic` netlist with the `layout` extracted netlist, independent of the selected netlist source. +``` + +Perform LVS (Layout VS Schematic) with netgen. + +Arguments: + +- `args`: `` Additional args that are passed to netgen. +- `script`: `` A custom LVS script relative to `scripts/`. + +Results: + +- `lvs_errors`: `` Number of LVS errors. + ## `klayout_drc` Perform DRC (Design Rule Check) with KLayout. @@ -69,18 +87,14 @@ Results: - `drc_errors`: `` Number of DRC errors. -## `netgen_lvs` - -```{note} -The `netgen_lvs` tool always compares the `schematic` netlist with the `layout` extracted netlist, independent of the selected netlist source. -``` +## `klayout_lvs` -Perform LVS (Layout VS Schematic) with netgen. +Perform LVS (Layout VS Schematic) with KLayout. Arguments: -- `args`: `` Additional args that are passed to netgen. -- `script`: `` A custom LVS script under `scripts/`. +- `args`: `` Additional args that are passed to KLayout. For example `['-rd', 'variable=value']`. +- `script`: `` A custom LVS script relative to `scripts/`. Results: From 64a26744c3fd0181fe46547801fa8303c109cf6c Mon Sep 17 00:00:00 2001 From: mole99 Date: Tue, 14 Jan 2025 18:07:33 +0100 Subject: [PATCH 4/4] Formatting --- cace/parameter/parameter_klayout_lvs.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cace/parameter/parameter_klayout_lvs.py b/cace/parameter/parameter_klayout_lvs.py index 2cebaf2..806e60f 100755 --- a/cace/parameter/parameter_klayout_lvs.py +++ b/cace/parameter/parameter_klayout_lvs.py @@ -109,9 +109,9 @@ def implementation(self): return if self.get_argument('script'): - lvs_script_path = os.path.abspath( - os.path.join(scriptspath, self.get_argument('script')) - ) + lvs_script_path = os.path.abspath( + os.path.join(scriptspath, self.get_argument('script')) + ) else: lvs_script_path = os.path.join( get_pdk_root(), @@ -127,7 +127,9 @@ def implementation(self): self.result_type = ResultType.ERROR return - report_file_path = os.path.join(self.param_dir, f'{projname}.lvsdb') + report_file_path = os.path.join( + self.param_dir, f'{projname}.lvsdb' + ) # PDK specific arguments if self.datasheet['PDK'].startswith('sky130'):