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

254 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-02 17:21 +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# 

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 for source_layer in li.source_layers: 

96 computed_layer_info = tech.computed_layer_info_by_name.get(source_layer.lvs_layer_name, None) 

97 if computed_layer_info is None: 

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

99 continue 

100 

101 canonical_layer_name = tech.canonical_layer_name_by_gds_pair[gds_pair] 

102 

103 LP = tech_pb2.LayerInfo.Purpose 

104 

105 match computed_layer_info.kind: 

106 case tech_pb2.ComputedLayerInfo.Kind.KIND_PIN: 

107 continue 

108 

109 case tech_pb2.ComputedLayerInfo.Kind.KIND_LABEL: 

110 continue 

111 

112 case _: 

113 pass 

114 

115 match computed_layer_info.layer_info.purpose: 

116 case LP.PURPOSE_NWELL | LP.PURPOSE_PWELL: 

117 pass # TODO!? 

118 

119 case LP.PURPOSE_NTAP | LP.PURPOSE_PTAP: 

120 pass # TODO!? 

121 

122 case LP.PURPOSE_N_IMPLANT | LP.PURPOSE_P_IMPLANT: 

123 # device terminals 

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

125 # - bulk (e.g. nwell) 

126 # 

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

128 # so the resistance does not matter for PEX 

129 cond = rex_tech.conductors.add() 

130 

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

132 cond.layer.canonical_layer_name = canonical_layer_name 

133 cond.layer.lvs_layer_name = source_layer.lvs_layer_name 

134 

135 cond.triangulation_min_b = self.delaunay_b 

136 cond.triangulation_max_area = self.delaunay_amax 

137 

138 cond.algorithm = self.substrate_algorithm 

139 cond.resistance = 0 # see comment above 

140 

141 case LP.PURPOSE_METAL: 

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

143 

144 cond = rex_tech.conductors.add() 

145 

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

147 cond.layer.canonical_layer_name = canonical_layer_name 

148 cond.layer.lvs_layer_name = source_layer.lvs_layer_name 

149 

150 cond.triangulation_min_b = self.delaunay_b 

151 cond.triangulation_max_area = self.delaunay_amax 

152 

153 if canonical_layer_name == tech.internal_substrate_layer_name: 

154 cond.algorithm = self.substrate_algorithm 

155 else: 

156 cond.algorithm = self.wire_algorithm 

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

158 

159 case LP.PURPOSE_CONTACT: 

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

161 if contact is None: 

162 warning( 

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

164 f"no contact found in tech info") 

165 continue 

166 

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

168 None) 

169 if contact_resistance is None: 

170 warning( 

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

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

173 continue 

174 

175 via = rex_tech.vias.add() 

176 

177 bot_gds_pair = tech.gds_pair(contact.layer_below) 

178 top_gds_pair = tech.gds_pair(contact.metal_above) 

179 

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

181 via.layer.canonical_layer_name = canonical_layer_name 

182 via.layer.lvs_layer_name = source_layer.lvs_layer_name 

183 

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

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

186 

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

188 contact=contact, 

189 contact_resistance=contact_resistance 

190 ) 

191 via.merge_distance = self.via_merge_distance 

192 

193 case LP.PURPOSE_VIA: 

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

195 if via_resistance is None: 

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

197 continue 

198 bot_top = tech.bottom_and_top_layer_name_by_via_computed_layer_name.get( 

199 source_layer.lvs_layer_name, None) 

200 if bot_top is None: 

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

202 continue 

203 via = rex_tech.vias.add() 

204 

205 (bot, top) = bot_top 

206 bot_gds_pair = tech.gds_pair(bot) 

207 top_gds_pair = tech.gds_pair(top) 

208 

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

210 via.layer.canonical_layer_name = canonical_layer_name 

211 via.layer.lvs_layer_name = source_layer.lvs_layer_name 

212 

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

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

215 

216 contact = self.pex_context.tech.contact_by_contact_lvs_layer_name[ 

217 source_layer.lvs_layer_name] 

218 

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

220 contact=contact, 

221 via_resistance=via_resistance 

222 ) 

223 

224 via.merge_distance = self.via_merge_distance 

225 

226 case _: 

227 warning(f"prepare_r_extractor_tech_pb: Unhandled layer purpose " 

228 f"{LP.Name(computed_layer_info.layer_info.purpose)}" 

229 f"({computed_layer_info.layer_info.purpose}), " 

230 f"LVS computed layer is {source_layer.lvs_layer_name} ({source_layer.gds_pair}), " 

231 f"original layer is {canonical_layer_name}") 

232 

233 return rex_tech 

234 

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

236 rex_request = pex_request_pb2.RExtractionRequest() 

237 

238 # prepare tech info 

239 self.prepare_r_extractor_tech_pb(rex_tech=rex_request.tech) 

240 

241 # prepare devices 

242 devices_by_name = self.pex_context.devices_by_name 

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

244 

245 # prepare pins 

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

247 rex_request.pins.MergeFrom(pin_list) 

248 

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

250 def get_or_create_net_request(net_name: str): 

251 v = net_request_by_name.get(net_name, None) 

252 if not v: 

253 v = rex_request.net_extraction_requests.add() 

254 v.net_name = net_name 

255 net_request_by_name[net_name] = v 

256 return v 

257 

258 for pin in rex_request.pins: 

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

260 

261 for device in rex_request.devices: 

262 for terminal in device.terminals: 

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

264 

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

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

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

268 if not circuit: 

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

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

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

272 LK = tech_pb2.ComputedLayerInfo.Kind 

273 for net in circuit.each_net(): 

274 net_name = net.name or f"${net.cluster_id}" 

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

276 for lyr in lyr_info.source_layers: 

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

278 match li.kind: 

279 case LK.KIND_PIN: 

280 continue # skip 

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

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

283 if not r: 

284 continue 

285 l2r = get_or_create_net_request(net_name).region_by_layer.add() 

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

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

288 l2r.layer.lvs_layer_name = lyr.lvs_layer_name 

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

290 case _: 

291 raise NotImplementedError() 

292 

293 return rex_request 

294 

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

296 rex_result = pex_result_pb2.RExtractionResult() 

297 

298 rex_tech_kly = klayout_r_extractor_tech(rex_request.tech) 

299 

300 Label = str 

301 LayerName = str 

302 NetName = str 

303 DeviceID = int 

304 TerminalID = int 

305 

306 # dicts keyed by id / klayout_index 

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

308 

309 wire_layer_ids: Set[int] = set() 

310 via_layer_ids: Set[int] = set() 

311 

312 for c in rex_request.tech.conductors: 

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

314 wire_layer_ids.add(c.layer.id) 

315 

316 for v in rex_request.tech.vias: 

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

318 via_layer_ids.add(c.layer.id) 

319 

320 for net_extraction_request in rex_request.net_extraction_requests: 

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

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

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

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

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

326 

327 for t in net_extraction_request.device_terminals: 

328 for l2r in t.region_by_layer: 

329 for sh in l2r.region.shapes: 

330 sh_kly = self.shapes_converter.klayout_shape(sh) 

331 polygon_ports[l2r.layer.id].append(sh_kly) 

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

333 

334 for pin in net_extraction_request.pins: 

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

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

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

338 

339 for l2r in net_extraction_request.region_by_layer: 

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

341 

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

343 resistor_network = rex.extract(rex_tech_kly, 

344 regions, 

345 vertex_ports, 

346 polygon_ports) 

347 

348 result_network = rex_result.networks.add() 

349 result_network.net_name = net_extraction_request.net_name 

350 

351 for rn in resistor_network.each_node(): 

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

353 

354 loc = rn.location() 

355 layer_id = rn.layer() 

356 canonical_layer_name = layer_names[layer_id] 

357 

358 r_node = result_network.nodes.add() 

359 r_node.node_id = rn.object_id() 

360 r_node.node_name = rn.to_s() 

361 r_node.node_kind = r_network_pb2.RNode.Kind.KIND_UNSPECIFIED # TODO! 

362 r_node.layer_name = canonical_layer_name 

363 

364 match rn.type(): 

365 case klp.RNodeType.VertexPort: # pins! 

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

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

368 r_node.location.point.x = p.x 

369 r_node.location.point.y = p.y 

370 case klp.RNodeType.PolygonPort | klp.RNodeType.Internal: 

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

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

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

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

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

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

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

378 case _: 

379 raise NotImplementedError() 

380 

381 match rn.type(): 

382 case klp.RNodeType.VertexPort: 

383 r_node.node_kind = r_network_pb2.RNode.Kind.KIND_PIN 

384 port_idx = rn.port_index() 

385 r_node.node_name, r_node.net_name = vertex_port_pins[rn.layer()][port_idx][0:2] 

386 r_node.location.point.net = r_node.net_name 

387 

388 case klp.RNodeType.PolygonPort: 

389 r_node.node_kind = r_network_pb2.RNode.Kind.KIND_DEVICE_TERMINAL 

390 port_idx = rn.port_index() 

391 nn = polygon_port_device_terminals[rn.layer()][port_idx].net_name 

392 r_node.net_name = f"{result_network.net_name}.{r_node.node_name}" 

393 r_node.location.box.net = r_node.net_name 

394 case klp.RNodeType.Internal: 

395 if rn.layer() in via_layer_ids: 

396 r_node.node_kind = r_network_pb2.RNode.Kind.KIND_VIA_JUNCTION 

397 elif rn.layer() in wire_layer_ids: 

398 r_node.node_kind = r_network_pb2.RNode.Kind.KIND_WIRE_JUNCTION 

399 else: 

400 raise NotImplementedError() 

401 

402 # NOTE: network prefix, as node name is only unique per network 

403 r_node.net_name = f"{result_network.net_name}.{r_node.node_name}" 

404 r_node.location.box.net = r_node.net_name 

405 case _: 

406 raise NotImplementedError() 

407 

408 node_by_node_id[r_node.node_id] = r_node 

409 

410 for el in resistor_network.each_element(): 

411 r_element = result_network.elements.add() 

412 r_element.element_id = el.object_id() 

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

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

415 r_element.resistance = el.resistance() 

416 

417 return rex_result 

418