Coverage for klayout_pex/rcx25/r/r_extractor.py: 9%

230 statements  

« prev     ^ index     » next       coverage.py v7.10.2, created at 2025-08-08 18:54 +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/martinjankoehler/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 

25from collections import defaultdict 

26from typing import * 

27 

28from klayout_pex.log import ( 

29 warning, 

30 subproc, 

31) 

32 

33from ..types import NetName 

34 

35from klayout_pex.klayout.shapes_pb2_converter import ShapesConverter 

36from klayout_pex.klayout.lvsdb_extractor import KLayoutExtractionContext 

37from klayout_pex.klayout.rex_core import klayout_r_extractor_tech 

38 

39import klayout_pex_protobuf.kpex.layout.device_pb2 as device_pb2 

40import klayout_pex_protobuf.kpex.layout.location_pb2 as location_pb2 

41from klayout_pex_protobuf.kpex.klayout.r_extractor_tech_pb2 import RExtractorTech as pb_RExtractorTech 

42import klayout_pex_protobuf.kpex.tech.tech_pb2 as tech_pb2 

43import klayout_pex_protobuf.kpex.r.r_network_pb2 as r_network_pb2 

44import klayout_pex_protobuf.kpex.request.pex_request_pb2 as pex_request_pb2 

45import klayout_pex_protobuf.kpex.result.pex_result_pb2 as pex_result_pb2 

46 

47import klayout.db as kdb 

48import klayout.pex as klp 

49 

50 

51class RExtractor: 

52 def __init__(self, 

53 pex_context: KLayoutExtractionContext, 

54 substrate_algorithm: pb_RExtractorTech.Algorithm, 

55 wire_algorithm: pb_RExtractorTech.Algorithm, 

56 delaunay_b: float, 

57 delaunay_amax: float, 

58 via_merge_distance: float, 

59 skip_simplify: bool): 

60 """ 

61 :param pex_context: KLayout PEX extraction context 

62 :param substrate_algorithm: The KLayout PEXCore Algorithm for decomposing polygons. 

63 Either SquareCounting or Tesselation (recommended) 

64 :param wire_algorithm: The KLayout PEXCore Algorithm for decomposing polygons. 

65 Either SquareCounting (recommended) or Tesselation 

66 :param delaunay_b: The "b" parameter for the Delaunay triangulation, 

67 a ratio of shortest triangle edge to circle radius 

68 :param delaunay_amax: The "max_area" specifies the maximum area of the triangles 

69 produced in square micrometers. 

70 :param via_merge_distance: Maximum distance where close vias are merged together 

71 :param skip_simplify: skip simplification of resistor network 

72 """ 

73 self.pex_context = pex_context 

74 self.substrate_algorithm = substrate_algorithm 

75 self.wire_algorithm = wire_algorithm 

76 self.delaunay_b = delaunay_b 

77 self.delaunay_amax = delaunay_amax 

78 self.via_merge_distance = via_merge_distance 

79 self.skip_simplify = skip_simplify 

80 

81 self.shapes_converter = ShapesConverter(dbu=self.pex_context.dbu) 

82 

83 def prepare_r_extractor_tech_pb(self, 

84 rex_tech: pb_RExtractorTech): 

85 """ 

86 Prepare KLayout PEXCore Technology Description based on the KPEX Tech Info data 

87 :param rex_tech: RExtractorTech protobuffer message 

88 """ 

89 

90 rex_tech.skip_simplify = self.skip_simplify 

91 

92 tech = self.pex_context.tech 

93 

94 for gds_pair, li in self.pex_context.extracted_layers.items(): 

95 computed_layer_info = tech.computed_layer_info_by_gds_pair.get(gds_pair, None) 

96 if computed_layer_info is None: 

97 warning(f"ignoring layer {gds_pair}, no computed layer info found in tech info") 

98 continue 

99 

100 canonical_layer_name = tech.canonical_layer_name_by_gds_pair[gds_pair] 

101 

102 LP = tech_pb2.LayerInfo.Purpose 

103 

104 if computed_layer_info.kind != tech_pb2.ComputedLayerInfo.Kind.KIND_PIN: 

105 match computed_layer_info.layer_info.purpose: 

106 case LP.PURPOSE_NWELL: 

107 pass # TODO! 

108 

109 case LP.PURPOSE_N_IMPLANT | LP.PURPOSE_P_IMPLANT: 

110 # device terminals 

111 # - source/drain (e.g. sky130A: nsdm, psdm) 

112 # - bulk (e.g. nwell) 

113 # 

114 # we will consider this only as a pin end-point, there are no wires at all on this layer, 

115 # so the resistance does not matter for PEX 

116 for source_layer in li.source_layers: 

117 cond = rex_tech.conductors.add() 

118 

119 cond.layer.id = self.pex_context.annotated_layout.layer(*source_layer.gds_pair) 

120 cond.layer.canonical_layer_name = canonical_layer_name 

121 cond.layer.lvs_layer_name = source_layer.lvs_layer_name 

122 

123 cond.triangulation_min_b = self.delaunay_b 

124 cond.triangulation_max_area = self.delaunay_amax 

125 

126 cond.algorithm = self.substrate_algorithm 

127 cond.resistance = 0 # see comment above 

128 

129 case LP.PURPOSE_METAL: 

130 if computed_layer_info.kind == tech_pb2.ComputedLayerInfo.Kind.KIND_PIN: 

131 continue 

132 

133 layer_resistance = tech.layer_resistance_by_layer_name.get(canonical_layer_name, None) 

134 for source_layer in li.source_layers: 

135 cond = rex_tech.conductors.add() 

136 

137 cond.layer.id = self.pex_context.annotated_layout.layer(*source_layer.gds_pair) 

138 cond.layer.canonical_layer_name = canonical_layer_name 

139 cond.layer.lvs_layer_name = source_layer.lvs_layer_name 

140 

141 cond.triangulation_min_b = self.delaunay_b 

142 cond.triangulation_max_area = self.delaunay_amax 

143 

144 if canonical_layer_name == tech.internal_substrate_layer_name: 

145 cond.algorithm = self.substrate_algorithm 

146 else: 

147 cond.algorithm = self.wire_algorithm 

148 cond.resistance = self.pex_context.tech.milliohm_to_ohm(layer_resistance.resistance) 

149 

150 case LP.PURPOSE_CONTACT: 

151 for source_layer in li.source_layers: 

152 contact = tech.contact_by_contact_lvs_layer_name.get(source_layer.lvs_layer_name, None) 

153 if contact is None: 

154 warning( 

155 f"ignoring LVS layer {source_layer.lvs_layer_name} (layer {canonical_layer_name}), " 

156 f"no contact found in tech info") 

157 continue 

158 

159 contact_resistance = tech.contact_resistance_by_device_layer_name.get(contact.layer_below, 

160 None) 

161 if contact_resistance is None: 

162 warning( 

163 f"ignoring LVS layer {source_layer.lvs_layer_name} (layer {canonical_layer_name}), " 

164 f"no contact resistance found in tech info") 

165 continue 

166 

167 via = rex_tech.vias.add() 

168 

169 bot_gds_pair = tech.gds_pair(contact.layer_below) 

170 top_gds_pair = tech.gds_pair(contact.metal_above) 

171 

172 via.layer.id = self.pex_context.annotated_layout.layer(*source_layer.gds_pair) 

173 via.layer.canonical_layer_name = canonical_layer_name 

174 via.layer.lvs_layer_name = source_layer.lvs_layer_name 

175 

176 via.bottom_conductor.id = self.pex_context.annotated_layout.layer(*bot_gds_pair) 

177 via.top_conductor.id = self.pex_context.annotated_layout.layer(*top_gds_pair) 

178 

179 via.resistance = self.pex_context.tech.milliohm_by_cnt_to_ohm_by_square_for_contact( 

180 contact=contact, 

181 contact_resistance=contact_resistance 

182 ) 

183 via.merge_distance = self.via_merge_distance 

184 

185 case LP.PURPOSE_VIA: 

186 via_resistance = tech.via_resistance_by_layer_name.get(canonical_layer_name, None) 

187 if via_resistance is None: 

188 warning(f"ignoring layer {canonical_layer_name}, no via resistance found in tech info") 

189 continue 

190 for source_layer in li.source_layers: 

191 via = rex_tech.vias.add() 

192 bot_top = tech.bottom_and_top_layer_name_by_via_computed_layer_name.get( 

193 source_layer.lvs_layer_name, None) 

194 if bot_top is None: 

195 warning(f"ignoring layer {canonical_layer_name} (LVS {source_layer.lvs_layer_name}), no bottom/top layers found in tech info") 

196 continue 

197 

198 (bot, top) = bot_top 

199 bot_gds_pair = tech.gds_pair(bot) 

200 top_gds_pair = tech.gds_pair(top) 

201 

202 via.layer.id = self.pex_context.annotated_layout.layer(*source_layer.gds_pair) 

203 via.layer.canonical_layer_name = canonical_layer_name 

204 via.layer.lvs_layer_name = source_layer.lvs_layer_name 

205 

206 via.bottom_conductor.id = self.pex_context.annotated_layout.layer(*bot_gds_pair) 

207 via.top_conductor.id = self.pex_context.annotated_layout.layer(*top_gds_pair) 

208 

209 contact = self.pex_context.tech.contact_by_contact_lvs_layer_name[ 

210 source_layer.lvs_layer_name] 

211 

212 via.resistance = self.pex_context.tech.milliohm_by_cnt_to_ohm_by_square_for_via( 

213 contact=contact, 

214 via_resistance=via_resistance 

215 ) 

216 

217 via.merge_distance = self.via_merge_distance 

218 

219 return rex_tech 

220 

221 def prepare_request(self) -> pex_request_pb2.RExtractionRequest: 

222 rex_request = pex_request_pb2.RExtractionRequest() 

223 

224 # prepare tech info 

225 self.prepare_r_extractor_tech_pb(rex_tech=rex_request.tech) 

226 

227 # prepare devices 

228 devices_by_name = self.pex_context.devices_by_name 

229 rex_request.devices.MergeFrom(devices_by_name.values()) 

230 

231 # prepare pins 

232 for pin_list in self.pex_context.pins_pb2_by_layer.values(): 

233 rex_request.pins.MergeFrom(pin_list) 

234 

235 net_request_by_name: Dict[NetName, pex_request_pb2.RNetExtractionRequest] = {} 

236 def get_or_create_net_request(net_name: str): 

237 v = net_request_by_name.get(net_name, None) 

238 if not v: 

239 v = rex_request.net_extraction_requests.add() 

240 v.net_name = net_name 

241 net_request_by_name[net_name] = v 

242 return v 

243 

244 for pin in rex_request.pins: 

245 get_or_create_net_request(pin.net_name).pins.add().CopyFrom(pin) 

246 

247 for device in rex_request.devices: 

248 for terminal in device.terminals: 

249 get_or_create_net_request(terminal.net_name).device_terminals.add().CopyFrom(terminal) 

250 

251 netlist = self.pex_context.lvsdb.netlist() 

252 circuit = netlist.circuit_by_name(self.pex_context.annotated_top_cell.name) 

253 # https://www.klayout.de/doc-qt5/code/class_Circuit.html 

254 if not circuit: 

255 circuits = [c.name for c in netlist.each_circuit()] 

256 raise Exception(f"Expected circuit called {self.pex_context.annotated_top_cell.name} in extracted netlist, " 

257 f"only available circuits are: {circuits}") 

258 LK = tech_pb2.ComputedLayerInfo.Kind 

259 for net in circuit.each_net(): 

260 for lvs_gds_pair, lyr_info in self.pex_context.extracted_layers.items(): 

261 for lyr in lyr_info.source_layers: 

262 li = self.pex_context.tech.computed_layer_info_by_gds_pair[lyr.gds_pair] 

263 match li.kind: 

264 case LK.KIND_PIN: 

265 continue # skip 

266 case LK.KIND_REGULAR | LK.KIND_DEVICE_CAPACITOR | LK.KIND_DEVICE_RESISTOR: 

267 r = self.pex_context.shapes_of_net(lyr.gds_pair, net) 

268 if not r: 

269 continue 

270 l2r = get_or_create_net_request(net.name).region_by_layer.add() 

271 l2r.layer.id = self.pex_context.annotated_layout.layer(*lvs_gds_pair) 

272 l2r.layer.canonical_layer_name = self.pex_context.tech.canonical_layer_name_by_gds_pair[lvs_gds_pair] 

273 l2r.layer.lvs_layer_name = lyr.lvs_layer_name 

274 self.shapes_converter.klayout_region_to_pb(r, l2r.region) 

275 

276 return rex_request 

277 

278 def extract(self, rex_request: pex_request_pb2.RExtractionRequest) -> pex_result_pb2.RExtractionResult: 

279 rex_result = pex_result_pb2.RExtractionResult() 

280 

281 rex_tech_kly = klayout_r_extractor_tech(rex_request.tech) 

282 

283 Label = str 

284 LayerName = str 

285 NetName = str 

286 DeviceID = int 

287 TerminalID = int 

288 

289 # dicts keyed by id / klayout_index 

290 layer_names: Dict[int, LayerName] = {} 

291 

292 for c in rex_request.tech.conductors: 

293 layer_names[c.layer.id] = c.layer.canonical_layer_name 

294 

295 for v in rex_request.tech.vias: 

296 layer_names[v.layer.id] = v.layer.canonical_layer_name 

297 

298 for net_extraction_request in rex_request.net_extraction_requests: 

299 

300 vertex_ports: Dict[int, List[kdb.Point]] = defaultdict(list) 

301 polygon_ports: Dict[int, List[kdb.Polygon]] = defaultdict(list) 

302 vertex_port_pins: Dict[int, List[Tuple[Label, NetName]]] = defaultdict(list) 

303 polygon_port_device_terminals: Dict[int, List[device_pb2.Device.Terminal]] = defaultdict(list) 

304 regions: Dict[int, kdb.Region] = defaultdict(kdb.Region) 

305 

306 for t in net_extraction_request.device_terminals: 

307 for l2r in t.region_by_layer: 

308 for p in l2r.region.polygons: 

309 p_kly = self.shapes_converter.klayout_polygon(p) 

310 polygon_ports[l2r.layer.id].append(p_kly) 

311 polygon_port_device_terminals[l2r.layer.id].append(t) 

312 

313 for pin in net_extraction_request.pins: 

314 p = self.shapes_converter.klayout_point(pin.label_point) 

315 vertex_ports[pin.layer.id].append(p) 

316 vertex_port_pins[pin.layer.id].append((pin.label, pin.net_name)) 

317 

318 for l2r in net_extraction_request.region_by_layer: 

319 regions[l2r.layer.id] = self.shapes_converter.klayout_region(l2r.region) 

320 

321 rex = klp.RNetExtractor(self.pex_context.dbu) 

322 resistor_network = rex.extract(rex_tech_kly, 

323 regions, 

324 vertex_ports, 

325 polygon_ports) 

326 

327 node_by_node_id: Dict[int, r_network_pb2.RNode] = {} 

328 

329 result_network = rex_result.networks.add() 

330 result_network.net_name = net_extraction_request.net_name 

331 

332 for rn in resistor_network.each_node(): 

333 loc = rn.location() 

334 layer_id = rn.layer() 

335 canonical_layer_name = layer_names[layer_id] 

336 

337 r_node = result_network.nodes.add() 

338 r_node.node_id = rn.object_id() 

339 r_node.node_name = rn.to_s() 

340 r_node.node_type = r_network_pb2.RNode.Kind.KIND_UNSPECIFIED # TODO! 

341 r_node.layer_name = canonical_layer_name 

342 

343 match rn.type(): 

344 case klp.RNodeType.VertexPort: 

345 r_node.location.kind = location_pb2.Location.Kind.LOCATION_KIND_POINT 

346 p = loc.center().to_itype(self.pex_context.dbu) 

347 r_node.location.point.x = p.x 

348 r_node.location.point.y = p.y 

349 case klp.RNodeType.PolygonPort | _: 

350 r_node.location.kind = location_pb2.Location.Kind.LOCATION_KIND_BOX 

351 p1 = loc.p1.to_itype(self.pex_context.dbu) 

352 p2 = loc.p2.to_itype(self.pex_context.dbu) 

353 r_node.location.box.lower_left.x = p1.x 

354 r_node.location.box.lower_left.y = p1.y 

355 r_node.location.box.upper_right.x = p2.x 

356 r_node.location.box.upper_right.y = p2.y 

357 

358 match rn.type(): 

359 case klp.RNodeType.VertexPort: 

360 port_idx = rn.port_index() 

361 r_node.net_name = vertex_port_pins[rn.layer()][port_idx][1] 

362 r_node.location.point.net = r_node.net_name 

363 case klp.RNodeType.PolygonPort: 

364 port_idx = rn.port_index() 

365 r_node.net_name = polygon_port_device_terminals[rn.layer()][port_idx].net_name 

366 r_node.location.box.net = r_node.net_name 

367 case _: 

368 r_node.net_name = r_node.node_name 

369 r_node.location.box.net = r_node.net_name 

370 

371 node_by_node_id[r_node.node_id] = r_node 

372 

373 for el in resistor_network.each_element(): 

374 r_element = result_network.elements.add() 

375 r_element.element_id = el.object_id() 

376 r_element.node_a.node_id = el.a().object_id() 

377 r_element.node_b.node_id = el.b().object_id() 

378 r_element.resistance = el.resistance() 

379 

380 return rex_result 

381