Coverage for klayout_pex/klayout/lvs_cache.py: 100%
99 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-02 23:32 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-02 23:32 +0000
1#
2# --------------------------------------------------------------------------------
3# SPDX-FileCopyrightText: 2024-2026 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#
24"""
25Cached LVS databases, which are reused only while the inputs of the LVS run are unchanged
26(see protos/kpex/klayout/lvs_input_fingerprint.proto)
27"""
29from __future__ import annotations
31from dataclasses import dataclass
32import hashlib
33import os
34from pathlib import PurePath
35import shutil
36from typing import *
38import google.protobuf.json_format
40from klayout_pex_protobuf.kpex.klayout.lvs_input_fingerprint_pb2 import (
41 FileFingerprint,
42 LVSDeckFingerprint,
43 LVSInputFingerprint
44)
47# NOTE: increment whenever the fingerprint is computed differently,
48# so that the fingerprints written by an older KPEX never match
49FINGERPRINT_FORMAT_VERSION = 1
51# NOTE: an upgrade may change every file of a deck, only name a few of them
52MAX_NAMED_DECK_FILES = 5
55def file_sha256(path: str) -> str:
56 h = hashlib.sha256()
57 with open(path, 'rb') as f:
58 for chunk in iter(lambda: f.read(1 << 20), b''):
59 h.update(chunk)
60 return h.hexdigest()
63def file_fingerprint(path: str) -> FileFingerprint:
64 return FileFingerprint(path=os.path.abspath(path),
65 sha256=file_sha256(path))
68def deck_fingerprint(lvs_script_path: str) -> LVSDeckFingerprint:
69 """
70 Every file in the directory of the LVS script and below, as the script includes further files
71 (%include, require, load) in ways that are not practical to trace.
73 Hidden files (e.g. .DS_Store) are skipped, as are dangling symbolic links.
74 """
75 script_path = os.path.abspath(lvs_script_path)
76 deck_dir_path = os.path.dirname(script_path)
77 fingerprint = LVSDeckFingerprint(script_path=script_path)
78 for dir_path, dir_names, file_names in os.walk(deck_dir_path, followlinks=True):
79 dir_names[:] = [n for n in dir_names if not n.startswith('.')]
80 for file_name in file_names:
81 path = os.path.join(dir_path, file_name)
82 if file_name.startswith('.') or not os.path.isfile(path):
83 continue
84 relative_path = PurePath(os.path.relpath(path, deck_dir_path)).as_posix()
85 fingerprint.sha256_by_relative_path[relative_path] = file_sha256(path)
86 return fingerprint
89def lvs_input_fingerprint(pdk: str,
90 klayout_version: str,
91 lvs_script_path: str,
92 script_parameters: Dict[str, str],
93 gds_path: str,
94 cell_name: str,
95 schematic_path: str) -> LVSInputFingerprint:
96 fingerprint = LVSInputFingerprint(format_version=FINGERPRINT_FORMAT_VERSION,
97 pdk=pdk,
98 klayout_version=klayout_version,
99 cell_name=cell_name)
100 fingerprint.deck.CopyFrom(deck_fingerprint(lvs_script_path))
101 fingerprint.script_parameters.update(script_parameters)
102 fingerprint.layout.CopyFrom(file_fingerprint(gds_path))
103 fingerprint.schematic.CopyFrom(file_fingerprint(schematic_path))
104 return fingerprint
107def fingerprint_differences(cached: LVSInputFingerprint,
108 current: LVSInputFingerprint) -> List[str]:
109 """
110 What changed between the inputs of the cached LVS run and those of the current one,
111 empty if the cached LVS database can be reused. Paths are not compared.
112 """
113 if cached.format_version != current.format_version:
114 return [f"the fingerprint format changed ({cached.format_version} → {current.format_version})"]
116 differences: List[str] = []
118 def compare(what: str, cached_value: str, current_value: str):
119 if cached_value != current_value:
120 differences.append(f"{what} changed ({cached_value or 'unknown'} → {current_value or 'unknown'})")
122 compare('the PDK', cached.pdk, current.pdk)
123 compare('the KLayout version', cached.klayout_version, current.klayout_version)
125 cached_files = dict(cached.deck.sha256_by_relative_path)
126 current_files = dict(current.deck.sha256_by_relative_path)
127 changed_files = sorted(p for p in cached_files.keys() | current_files.keys()
128 if cached_files.get(p) != current_files.get(p))
129 if changed_files:
130 named = ', '.join(changed_files[:MAX_NAMED_DECK_FILES])
131 more = len(changed_files) - MAX_NAMED_DECK_FILES
132 differences.append(f"the LVS deck changed ({named}{f' and {more} more files' if more > 0 else ''})")
134 cached_parameters = dict(cached.script_parameters)
135 current_parameters = dict(current.script_parameters)
136 changed_parameters = [f"{n}: {cached_parameters.get(n, 'unset')} → {current_parameters.get(n, 'unset')}"
137 for n in sorted(cached_parameters.keys() | current_parameters.keys())
138 if cached_parameters.get(n) != current_parameters.get(n)]
139 if changed_parameters:
140 differences.append(f"the LVS script parameters changed ({', '.join(changed_parameters)})")
142 if cached.layout.sha256 != current.layout.sha256:
143 differences.append("the input layout changed")
144 compare('the cell', cached.cell_name, current.cell_name)
145 if cached.schematic.sha256 != current.schematic.sha256:
146 differences.append("the schematic changed")
148 return differences
151def read_fingerprint(path: str) -> Optional[LVSInputFingerprint]:
152 """
153 None if there is none, or it can't be read (e.g. written by an incompatible KPEX version)
154 """
155 try:
156 with open(path, 'r', encoding='utf-8') as f:
157 return google.protobuf.json_format.Parse(f.read(), LVSInputFingerprint())
158 except (OSError, google.protobuf.json_format.ParseError):
159 return None
162def write_fingerprint(path: str, fingerprint: LVSInputFingerprint):
163 json_str = google.protobuf.json_format.MessageToJson(fingerprint,
164 preserving_proto_field_name=True,
165 sort_keys=True,
166 ensure_ascii=False)
167 with open(path, 'w', encoding='utf-8') as f:
168 f.write(json_str)
171@dataclass(frozen=True)
172class LVSCacheEntry:
173 """
174 The cached LVS database of a cell, and the fingerprint of the LVS inputs it was created from
175 """
176 dir_path: str
177 cell_name: str
179 @property
180 def lvsdb_path(self) -> str:
181 return os.path.join(self.dir_path, f"{self.cell_name}.lvsdb.gz")
183 @property
184 def fingerprint_path(self) -> str:
185 return os.path.join(self.dir_path, f"{self.cell_name}.lvs_fingerprint.pb.json")
187 def cache_miss_reasons(self, fingerprint: LVSInputFingerprint) -> List[str]:
188 """
189 Why the cached LVS database can't be reused, empty if it can
190 """
191 if not os.path.isfile(self.lvsdb_path):
192 return ["there is no cached LVSDB"]
193 cached_fingerprint = read_fingerprint(self.fingerprint_path)
194 if cached_fingerprint is None:
195 return ["the cached LVSDB has no fingerprint of its LVS inputs"]
196 return fingerprint_differences(cached_fingerprint, fingerprint)
198 def store(self, lvsdb_path: str, fingerprint: LVSInputFingerprint):
199 os.makedirs(self.dir_path, exist_ok=True)
200 # NOTE: the fingerprint is written last, so an interrupted store leaves an entry without one,
201 # which is never reused
202 if os.path.exists(self.fingerprint_path):
203 os.remove(self.fingerprint_path)
204 shutil.copy(lvsdb_path, self.lvsdb_path)
205 write_fingerprint(self.fingerprint_path, fingerprint)