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

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""" 

28 

29from __future__ import annotations 

30 

31from dataclasses import dataclass 

32import hashlib 

33import os 

34from pathlib import PurePath 

35import shutil 

36from typing import * 

37 

38import google.protobuf.json_format 

39 

40from klayout_pex_protobuf.kpex.klayout.lvs_input_fingerprint_pb2 import ( 

41 FileFingerprint, 

42 LVSDeckFingerprint, 

43 LVSInputFingerprint 

44) 

45 

46 

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 

50 

51# NOTE: an upgrade may change every file of a deck, only name a few of them 

52MAX_NAMED_DECK_FILES = 5 

53 

54 

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() 

61 

62 

63def file_fingerprint(path: str) -> FileFingerprint: 

64 return FileFingerprint(path=os.path.abspath(path), 

65 sha256=file_sha256(path)) 

66 

67 

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. 

72 

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 

87 

88 

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 

105 

106 

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})"] 

115 

116 differences: List[str] = [] 

117 

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'})") 

121 

122 compare('the PDK', cached.pdk, current.pdk) 

123 compare('the KLayout version', cached.klayout_version, current.klayout_version) 

124 

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 ''})") 

133 

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)})") 

141 

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") 

147 

148 return differences 

149 

150 

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 

160 

161 

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) 

169 

170 

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 

178 

179 @property 

180 def lvsdb_path(self) -> str: 

181 return os.path.join(self.dir_path, f"{self.cell_name}.lvsdb.gz") 

182 

183 @property 

184 def fingerprint_path(self) -> str: 

185 return os.path.join(self.dir_path, f"{self.cell_name}.lvs_fingerprint.pb.json") 

186 

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) 

197 

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)