Coverage for klayout_pex/klayout/lvs_runner.py: 100%
54 statements
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-06 13:14 +0000
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-06 13:14 +0000
1#
2# --------------------------------------------------------------------------------
3# SPDX-FileCopyrightText: 2024-2025 Martin Jan Köhler and Harald Pretl
4# Johannes Kepler University, Institute for Integrated Circuits.
5#
6# This file is part of KPEX
7# (see https://github.com/iic-jku/klayout-pex).
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
21# SPDX-License-Identifier: GPL-3.0-or-later
22# --------------------------------------------------------------------------------
23#
24from __future__ import annotations
26import os
27import re
28import subprocess
29import time
30from typing import *
32from ..log import (
33 debug,
34 info,
35 warning,
36 error,
37 subproc,
38 rule
39)
42class LVSError(Exception):
43 """
44 KLayout LVS did not produce the LVS database that extraction depends on.
45 """
46 pass
49class LVSRunner:
50 # Parameters passed to the LVS scripts (-rd name=value), besides the paths and the verbosity
51 SCRIPT_PARAMETERS: Dict[str, str] = {
52 'thr': '22',
53 'run_mode': 'deep',
54 'spice_net_names': 'true',
55 'spice_comments': 'false',
56 'scale': 'false',
57 'schematic_simplify': 'false',
58 'net_only': 'false',
59 'top_lvl_pins': 'true',
60 'combine': 'false',
61 'combine_devices': 'false', # IHP
62 'purge': 'false',
63 'purge_nets': 'false',
64 'no_simplify': 'true', # IHP
65 }
67 # Script parameters that can't change the LVS database:
68 # the thread count, and the options of the SPICE writer for the extracted netlist
69 #
70 # NOTE: any other parameter is assumed to change it, so a new one is part of
71 # the LVS cache fingerprint unless it is listed here
72 NEUTRAL_SCRIPT_PARAMETERS: FrozenSet[str] = frozenset({
73 'thr',
74 'spice_net_names',
75 'spice_comments',
76 })
78 @classmethod
79 def result_script_parameters(cls) -> Dict[str, str]:
80 """
81 The script parameters that can change the LVS database (see LVSInputFingerprint)
82 """
83 return {name: value
84 for name, value in cls.SCRIPT_PARAMETERS.items()
85 if name not in cls.NEUTRAL_SCRIPT_PARAMETERS}
87 @staticmethod
88 def write_dummy_schematic(path: str, cell_name: str):
89 """
90 The schematic of the PEX-LVS, empty: it compares the layout with a dummy schematic only,
91 as KLayout writes the LVS database after a comparison: the alignment flattens every circuit
92 of the layout into its top circuit, whose devices and nets the extraction takes, while a schematic
93 with subcircuits would keep their circuits (#330)
94 """
95 # NOTE: the same bytes on every platform (no CRLF on Windows),
96 # as the fingerprints of the LVS inputs hash it (LVS cache, testdata/lvsdb)
97 with open(path, 'w', encoding='utf-8', newline='\n') as f:
98 f.writelines([
99 f".subckt {cell_name} VDD VSS\n",
100 '.ends\n',
101 '.end\n'
102 ])
104 @staticmethod
105 def run_klayout_lvs(exe_path: str,
106 lvs_script: str,
107 gds_path: str,
108 schematic_path: str,
109 log_path: str,
110 lvsdb_path: str,
111 netlist_path: str,
112 verbose: bool):
113 # NOTE: target_netlist must always be passed, otherwise the LVS scripts fall back to
114 # a path relative to the (in batch mode empty) active cell view,
115 # i.e. next to the input layout or into the parent of the working directory
116 args = [
117 exe_path,
118 '-b',
119 '-r', lvs_script,
120 '-rd', f"input={os.path.abspath(gds_path)}",
121 '-rd', f"report={os.path.abspath(lvsdb_path)}",
122 '-rd', f"target_netlist={os.path.abspath(netlist_path)}",
123 '-rd', f"schematic={os.path.abspath(schematic_path)}",
124 ]
125 for name, value in LVSRunner.SCRIPT_PARAMETERS.items():
126 args += ['-rd', f"{name}={value}"]
127 args += ['-rd', f"verbose={'true' if verbose else 'false'}"]
128 rule('Calling KLayout LVS script')
129 subproc(' '.join(args))
130 subproc(log_path)
132 # A report left over from an earlier run in the same output directory
133 # must not be mistaken for the result of this one
134 if os.path.exists(lvsdb_path):
135 os.remove(lvsdb_path)
137 klayout_errors = []
138 start = time.time()
140 proc = subprocess.Popen(args,
141 stdin=subprocess.DEVNULL,
142 stdout=subprocess.PIPE,
143 stderr=subprocess.STDOUT,
144 universal_newlines=True,
145 text=True,
146 encoding='utf-8',
147 errors='replace')
148 with open(log_path, 'w', encoding='utf-8') as f:
149 while True:
150 line = proc.stdout.readline()
151 if not line:
152 break
153 subproc(line[:-1]) # remove newline
154 f.writelines([line])
155 # NOTE: the log entries behind an error of the LVS have its reason, e.g. "[device-extract] In cell ..."
156 if line.startswith('ERROR:') or re.match(r'\[[\w-]+] ', line):
157 klayout_errors.append(line.rstrip())
158 proc.wait()
160 duration = time.time() - start
162 rule()
164 # NOTE: the report is the criterion, not the status code:
165 # a script error (e.g. an unresolvable %include) aborts before the report is written
166 if not os.path.isfile(lvsdb_path):
167 msg = f"KLayout LVS exited with status code {proc.returncode} after {'%.4g' % duration}s " \
168 f"without writing the LVS database {lvsdb_path}"
169 if klayout_errors:
170 msg += "\nKLayout reported:\n" + '\n'.join(f" {e}" for e in klayout_errors)
171 msg += f"\nSee LVS log file for details: {log_path}"
172 raise LVSError(msg)
174 if proc.returncode == 0:
175 info(f"klayout LVS succeeded after {'%.4g' % duration}s")
176 else:
177 info(f"klayout LVS finished with status code {proc.returncode} after {'%.4g' % duration}s, "
178 f"see log file: {log_path}")