Coverage for klayout_pex/rcx25/c/sidewall_and_fringe_extractor.py: 95%

195 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-02 23:32 +0000

1#! /usr/bin/env python3 

2# 

3# -------------------------------------------------------------------------------- 

4# SPDX-FileCopyrightText: 2024-2025 Martin Jan Köhler and Harald Pretl 

5# Johannes Kepler University, Institute for Integrated Circuits. 

6# 

7# This file is part of KPEX  

8# (see https://github.com/iic-jku/klayout-pex). 

9# 

10# This program is free software: you can redistribute it and/or modify 

11# it under the terms of the GNU General Public License as published by 

12# the Free Software Foundation, either version 3 of the License, or 

13# (at your option) any later version. 

14# 

15# This program is distributed in the hope that it will be useful, 

16# but WITHOUT ANY WARRANTY; without even the implied warranty of 

17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

18# GNU General Public License for more details. 

19# 

20# You should have received a copy of the GNU General Public License 

21# along with this program. If not, see <http://www.gnu.org/licenses/>. 

22# SPDX-License-Identifier: GPL-3.0-or-later 

23# -------------------------------------------------------------------------------- 

24# 

25 

26from functools import cached_property 

27import math 

28 

29import klayout.db as kdb 

30 

31from klayout_pex.log import ( 

32 debug, 

33 info, 

34 warning, 

35 get_log_level, 

36 LogLevel 

37) 

38from klayout_pex.tech_info import TechInfo 

39 

40from klayout_pex.rcx25.c.device_capacitor_plates import DeviceCapacitorPlates 

41from klayout_pex.rcx25.c.geometry_restorer import GeometryRestorer 

42from klayout_pex.rcx25.extraction_results import * 

43from klayout_pex.rcx25.extraction_reporter import ExtractionReporter 

44from klayout_pex.rcx25.c.polygon_utils import find_polygon_with_nearest_edge, nearest_edge 

45from klayout_pex.rcx25.types import EdgeInterval, EdgeNeighborhood 

46from klayout_pex_protobuf.kpex.tech.process_parasitics_pb2 import CapacitanceInfo 

47 

48 

49class SidewallAndFringeExtractor: 

50 def __init__(self, 

51 all_layer_names: List[LayerName], 

52 layer_regions_by_name: Dict[LayerName, kdb.Region], 

53 dbu: float, 

54 scale_ratio_to_fit_halo: bool, 

55 tech_info: TechInfo, 

56 device_capacitor_plates: DeviceCapacitorPlates, 

57 results: CellExtractionResults, 

58 report: ExtractionReporter): 

59 self.all_layer_names = all_layer_names 

60 self.layer_regions_by_name = layer_regions_by_name 

61 self.dbu = dbu 

62 self.scale_ratio_to_fit_halo = scale_ratio_to_fit_halo 

63 self.tech_info = tech_info 

64 self.device_capacitor_plates = device_capacitor_plates 

65 self.results = results 

66 self.report = report 

67 

68 self.all_layer_regions = layer_regions_by_name.values() 

69 

70 def extract(self): 

71 for idx, (layer_name, layer_region) in enumerate(self.layer_regions_by_name.items()): 

72 # NOTE: the substrate (with its wells) and the diffusion are the bottom plates of the capacitances 

73 # of the layers above, whose fringe ends at them, while their own edges have none 

74 # (like in the magic techs): those between them are the devices' (e.g. the junctions) 

75 if layer_name == self.tech_info.internal_substrate_layer_name or \ 

76 layer_name in self.tech_info.process_diffusion_layer_names: 

77 continue 

78 

79 other_layer_regions = [ 

80 r for ln, r in self.layer_regions_by_name.items() 

81 if ln != layer_name 

82 ] 

83 

84 en_visitor = self.PEXEdgeNeighborhoodVisitor( 

85 all_layer_names=self.all_layer_names, 

86 inside_layer_index=idx, 

87 dbu=self.dbu, 

88 scale_ratio_to_fit_halo=self.scale_ratio_to_fit_halo, 

89 tech_info=self.tech_info, 

90 device_capacitor_plates=self.device_capacitor_plates, 

91 results=self.results, 

92 report=self.report 

93 ) 

94 

95 en_children = [kdb.CompoundRegionOperationNode.new_secondary(r) 

96 for r in self.all_layer_regions] 

97 en_children[idx] = kdb.CompoundRegionOperationNode.new_foreign() # sidewall of other nets on the same layer 

98 en_children.append(kdb.CompoundRegionOperationNode.new_primary()) # opposing structures of the same polygon 

99 

100 side_halo_um = self.tech_info.tech.process_parasitics.side_halo 

101 side_halo_dbu = int(side_halo_um / self.dbu) + 1 # add 1 nm to halo 

102 

103 en_node = kdb.CompoundRegionOperationNode.new_edge_neighborhood( 

104 children=en_children, 

105 visitor=en_visitor, 

106 bext=-1, # NOTE: -1 dbu, suppresses quasi-empty contributions (will also suppress 90° edges) 

107 eext=-1, # NOTE: -1 dbu, suppresses quasi-empty contributions (will also suppress 90° edges) 

108 din=-1, # NOTE: -1 dbu, suppresses the edge itself appearing as a pseudo-polygon in new_primary() 

109 dout=side_halo_dbu # dout 

110 ) 

111 

112 layer_region.complex_op(en_node) 

113 

114 # ------------------------------------------------------------------------ 

115 

116 class PEXEdgeNeighborhoodVisitor(kdb.EdgeNeighborhoodVisitor): 

117 def __init__(self, 

118 all_layer_names: List[LayerName], 

119 inside_layer_index: int, 

120 dbu: float, 

121 tech_info: TechInfo, 

122 scale_ratio_to_fit_halo: bool, 

123 device_capacitor_plates: DeviceCapacitorPlates, 

124 results: CellExtractionResults, 

125 report: ExtractionReporter): 

126 super().__init__() 

127 

128 self.all_layer_names = all_layer_names 

129 self.inside_layer_index = inside_layer_index 

130 self.dbu = dbu 

131 self.tech_info = tech_info 

132 self.scale_ratio_to_fit_halo = scale_ratio_to_fit_halo 

133 self.device_capacitor_plates = device_capacitor_plates 

134 self.results = results 

135 self.report = report 

136 

137 # NOTE: prepare layers below and layers above the "inside" layer, 

138 # each prepared for iteration that allows iterativly growing a shield region 

139 self.layer_below_indices = reversed(range(0, inside_layer_index)) 

140 self.layer_above_indices = range(inside_layer_index, 

141 len(all_layer_names) - inside_layer_index) 

142 

143 @cached_property 

144 def inside_layer_name(self) -> LayerName: 

145 return self.all_layer_names[self.inside_layer_index] 

146 

147 def begin_polygon(self, 

148 layout: kdb.Layout, 

149 cell: kdb.Cell, 

150 polygon: kdb.Polygon): 

151 pass 

152 

153 def end_polygon(self): 

154 pass 

155 

156 @cached_property 

157 def side_halo(self) -> float: 

158 return self.tech_info.tech.process_parasitics.side_halo 

159 

160 def on_edge(self, 

161 layout: kdb.Layout, 

162 cell: kdb.Cell, 

163 edge: kdb.EdgeWithProperties, 

164 neighborhood: EdgeNeighborhood): 

165 # 

166 # NOTE: this complex operation will automatically rotate every edge to be on the x-axis 

167 # going from 0 to edge.length 

168 # so we only have to consider the y-axis to get the near and far distances 

169 # 

170 geometry_restorer = GeometryRestorer(self.to_original_trans(edge)) 

171 

172 if get_log_level() == LogLevel.DEBUG: 

173 self.report.output_edge_neighborhood(inside_layer=self.inside_layer_name, 

174 all_layer_names=self.all_layer_names, 

175 edge=edge, 

176 neighborhood=neighborhood, 

177 geometry_restorer=geometry_restorer) 

178 

179 for edge_interval, polygons_by_child in neighborhood: 

180 if not polygons_by_child: 

181 continue 

182 

183 edge_interval_length = edge_interval[1] - edge_interval[0] 

184 if edge_interval_length <= 1: 

185 # NOTE: expected, so no warning, which would train to ignore warnings 

186 debug(f"Short edge interval {edge_interval} " 

187 f"(length {edge_interval_length * self.dbu * 1000} nm), " 

188 f"expected to be dropped due to bext/eext parameters, skipping…") 

189 continue 

190 

191 layer_fringe_shields = [kdb.Region() for _ in self.all_layer_names] 

192 for child_index, polygons in polygons_by_child.items(): 

193 if child_index < len(self.all_layer_names): 

194 layer_fringe_shields[child_index].insert(polygons) 

195 

196 # NOTE: lateral fringe shielding, can be caused by 

197 # - sidewall (other net) 

198 # - same net "sidewall" (other polygons) 

199 # - even opposing edges of the same polygon of the same net! 

200 # fringe to shapes on other layers will be limited by this distance 

201 # (i.e., fringe is shielded beyond this distance) 

202 

203 nearest_distance: Optional[float] = None 

204 nearest_lateral_edge: Optional[kdb.EdgeWithProperties] = None 

205 

206 for child_index, polygons in polygons_by_child.items(): 

207 if child_index == len(self.all_layer_names): # TODO, fix index, same layer, same polygon 

208 distance, nearby_polygon = find_polygon_with_nearest_edge(polygons_on_same_layer=polygons) 

209 

210 if nearest_distance is None or \ 

211 distance < nearest_distance: 

212 nearest_distance = distance 

213 nearest_lateral_edge = nearest_edge(nearby_polygon) 

214 elif self.inside_layer_index == child_index: # SIDEWALL! 

215 # NOTE: use only the nearest polygon, 

216 # as the others are laterally shielded by the nearer ones 

217 distance, nearby_polygon = find_polygon_with_nearest_edge(polygons_on_same_layer=polygons) 

218 

219 if nearest_distance is None or \ 

220 distance < nearest_distance: 

221 nearest_distance = distance 

222 nearest_lateral_edge = nearest_edge(nearby_polygon) 

223 

224 self.emit_sidewall( 

225 layer_name=self.inside_layer_name, 

226 edge=edge, 

227 edge_interval=edge_interval, 

228 polygon=nearby_polygon, 

229 geometry_restorer=geometry_restorer 

230 ) 

231 

232 lateral_shield: Optional[kdb.Polygon] = None 

233 if nearest_lateral_edge is not None: 

234 lateral_shield = kdb.Polygon([ 

235 nearest_lateral_edge.p2, 

236 nearest_lateral_edge.p1, 

237 kdb.Point(nearest_lateral_edge.p1.x, (self.side_halo + 10) / self.dbu), 

238 kdb.Point(nearest_lateral_edge.p2.x, (self.side_halo + 10) / self.dbu), 

239 ]) 

240 

241 for child_index, polygons in polygons_by_child.items(): 

242 if self.inside_layer_index == child_index: 

243 continue # already handled above 

244 elif child_index < len(self.all_layer_names): # FRINGE! 

245 fringe_shield = kdb.Region() 

246 if lateral_shield is not None: 

247 fringe_shield.insert(lateral_shield) 

248 if child_index < self.inside_layer_index: 

249 r = range(child_index + 1, self.inside_layer_index) 

250 for idx in r: 

251 fringe_shield += layer_fringe_shields[idx] 

252 elif self.inside_layer_index < child_index: 

253 r = range(self.inside_layer_index + 1, child_index) 

254 for idx in r: 

255 fringe_shield += layer_fringe_shields[idx] 

256 

257 # NOTE: 

258 # polygons can have different nets 

259 # polygons can be segmented after shield is applied 

260 

261 self.emit_fringe( 

262 inside_layer_name=self.inside_layer_name, 

263 outside_layer_name=self.all_layer_names[child_index], 

264 edge=edge, 

265 edge_interval=edge_interval, 

266 outside_polygons=polygons, 

267 shield=fringe_shield, 

268 lateral_shield=lateral_shield, 

269 geometry_restorer=geometry_restorer) 

270 

271 def emit_sidewall(self, 

272 layer_name: LayerName, 

273 edge: kdb.EdgeWithProperties, 

274 edge_interval: EdgeInterval, 

275 polygon: kdb.PolygonWithProperties, 

276 geometry_restorer: GeometryRestorer): 

277 net1 = edge.property('net') 

278 net2 = polygon.property('net') 

279 

280 if net1 == net2: 

281 return 

282 

283 # NOTE: the model of a device has the capacitance between its plates 

284 if self.device_capacitor_plates.are_plates_of_a_device(edge, net1, polygon, net2): 

285 return 

286 

287 sidewall_cap_spec = self.tech_info.sidewall_cap_by_layer_name[layer_name] 

288 

289 # TODO! 

290 

291 # NOTE: this method is always called for a single nearest edge (line), so the 

292 # polygons have 4 points. 

293 # Polygons points are sorted clockwise, so the edge 

294 # that goes from right-to-left is the nearest edge 

295 # nearby_opposing_edge = [e for e in nearest_lateral_shape[1].each_edge() if e.d().x < 0][-1] 

296 # nearby_opposing_edge_trans = geometry_restorer.restore_edge(edge) * nearby_opposing_edge 

297 

298 # C = Csidewall * l * t / s 

299 # C = Csidewall * l / s 

300 

301 avg_length = edge_interval[1] - edge_interval[0] 

302 avg_distance = min(polygon.bbox().p1.y, polygon.bbox().p2.y) 

303 

304 outside_edge = nearest_edge(polygon) 

305 

306 length_um = avg_length * self.dbu 

307 distance_um = avg_distance * self.dbu 

308 

309 # NOTE: dividing by 2 (like MAGIC this not bidirectional), 

310 # but we count 2 sidewall contributions (one for each side of the cap). 

311 # The second half accounts for the tech file coefficient itself: 

312 # it holds the full value of the field solver, while a 

313 # per-edge value has to be half of that. MAGIC counted the 

314 # full value on every edge until 8.3.679, which redefined the 

315 # tech file value rather than changing every PDK — the 

316 # coefficients stay as the PDK states them, and the factor 

317 # lives here, see 

318 # https://github.com/martinjankoehler/magic/issues/6#issuecomment-5371056429 

319 cap_femto = ((length_um * sidewall_cap_spec.capacitance) 

320 / (distance_um + sidewall_cap_spec.offset) 

321 / 2.0 # non-bidirectional (half) 

322 / 2.0 # the coefficient is the value for both edges 

323 / 1000.0) # aF -> fF 

324 

325 # info(f"(Sidewall) layer {layer_name}: Nets {net1} <-> {net2}: {round(cap_femto, 5)} fF") 

326 

327 swk = SidewallKey(layer=layer_name, net1=net1, net2=net2) 

328 sw_cap = SidewallCap(key=swk, 

329 cap_value=cap_femto, 

330 distance=distance_um, 

331 length=length_um, 

332 tech_spec=sidewall_cap_spec) 

333 self.results.add_sidewall_cap(sw_cap) 

334 

335 self.report.output_sidewall( 

336 sidewall_cap=sw_cap, 

337 inside_edge=geometry_restorer.restore_edge_interval(edge_interval), 

338 outside_edge=geometry_restorer.restore_edge(outside_edge) 

339 ) 

340 

341 def fringe_cap(self, 

342 edge_interval_length: float, 

343 distance_near: float, 

344 distance_far: float, 

345 overlap_cap_spec: CapacitanceInfo.OverlapCapacitance, 

346 sideoverlap_cap_spec: CapacitanceInfo.SideOverlapCapacitance) -> float: 

347 distance_near_um = distance_near * self.dbu 

348 distance_far_um = distance_far * self.dbu 

349 edge_interval_length_um = edge_interval_length * self.dbu 

350 

351 # NOTE: overlap scaling is 1/50 (see MAGIC ExtTech) 

352 alpha_scale_factor = 0.02 * 0.01 * 0.5 * 200.0 

353 alpha_c = overlap_cap_spec.capacitance * alpha_scale_factor 

354 

355 # see Magic ExtCouple.c L1164 

356 cnear = (2.0 / math.pi) * math.atan(alpha_c * distance_near_um) 

357 cfar = (2.0 / math.pi) * math.atan(alpha_c * distance_far_um) 

358 

359 if self.scale_ratio_to_fit_halo: 

360 full_halo_ratio = (2.0 / math.pi) * math.atan(alpha_c * self.side_halo) 

361 # NOTE: for a large enough halo, full_halo would be 1, 

362 # but it is smaller, so we compensate 

363 if full_halo_ratio < 1.0: 

364 cnear /= full_halo_ratio 

365 cfar /= full_halo_ratio 

366 

367 # "cfrac" is the fractional portion of the fringe cap seen 

368 # by tile tp along its length. This is independent of the 

369 # portion of the boundary length that tile tp occupies. 

370 cfrac = cfar - cnear 

371 

372 cap_femto = (cfrac * edge_interval_length_um * 

373 sideoverlap_cap_spec.capacitance / 1000.0) 

374 

375 return cap_femto 

376 

377 def emit_fringe(self, 

378 inside_layer_name: LayerName, 

379 outside_layer_name: LayerName, 

380 edge: kdb.EdgeWithProperties, 

381 edge_interval: EdgeInterval, 

382 outside_polygons: List[kdb.PolygonWithProperties], 

383 shield: kdb.Region, 

384 lateral_shield: kdb.Polygon, 

385 geometry_restorer: GeometryRestorer): 

386 # NOTE: e.g. the fringe of the gate poly to the source/drain, which the transistor model has 

387 if self.tech_info.is_device_capacitance(inside_layer_name, outside_layer_name): 

388 return 

389 

390 inside_net_name = edge.property('net') 

391 

392 # NOTE: each polygon in outside_polygons 

393 # - could have a different net (e.g. the wells of the substrate and the rest, VSUBS) 

394 # - could be segmented by a shield into multiple polygons 

395 # each with different near/far regions 

396 

397 outside_net_names = [p.property('net') for p in outside_polygons] 

398 

399 # NOTE: the model of a device has the capacitance between its plates, so they are like one net 

400 same_net_markers = [ 

401 inside_net_name == outside_net_name or 

402 self.device_capacitor_plates.are_plates_of_a_device(edge, inside_net_name, p, outside_net_name) 

403 for p, outside_net_name in zip(outside_polygons, outside_net_names) 

404 ] 

405 

406 # NOTE: overlap_cap_by_layer_names is top/bot (dict is not symmetric) 

407 overlap_cap_spec = self.tech_info.overlap_cap_by_layer_names[inside_layer_name].get(outside_layer_name, 

408 None) 

409 if not overlap_cap_spec: 

410 overlap_cap_spec = self.tech_info.overlap_cap_by_layer_names[outside_layer_name][inside_layer_name] 

411 

412 substrate_cap_spec = self.tech_info.substrate_cap_by_layer_name[inside_layer_name] 

413 sideoverlap_cap_spec = self.tech_info.side_overlap_cap_by_layer_names[inside_layer_name][ 

414 outside_layer_name] 

415 

416 polygons_by_net: Dict[NetName, List[kdb.PolygonWithProperties]] = defaultdict(list) 

417 

418 for idx, p in enumerate(outside_polygons): 

419 outside_net = outside_net_names[idx] 

420 is_same_net = same_net_markers[idx] 

421 

422 if is_same_net: 

423 # TODO: log? 

424 continue 

425 

426 if shield.is_empty(): 

427 polygons_by_net[outside_net].append(p) 

428 else: 

429 unshielded_region = kdb.Region(p) 

430 unshielded_region.enable_properties() 

431 unshielded_region -= shield 

432 if unshielded_region.is_empty(): 

433 # TODO: log? 

434 continue 

435 

436 for up in unshielded_region.each(): 

437 up = kdb.PolygonWithProperties(up, {'net': outside_net}) 

438 polygons_by_net[outside_net].append(up) 

439 # if p != up: 

440 # print(f"Unshieleded polygon {up}, differs from original polygon {p}") 

441 

442 for outside_net_name, polygons in polygons_by_net.items(): 

443 for p in polygons: 

444 bbox = p.bbox() 

445 if not p.is_box(): 

446 warning(f"Side overlap, polygon {p} is not a box. " 

447 f"Currently, only boxes are supported, will be using bounding box {bbox}") 

448 

449 distance_near = bbox.p1.y # + 1 

450 if distance_near < 0: 

451 distance_near = 0 

452 distance_far = bbox.p2.y # - 2 

453 if distance_far < 0: 

454 distance_far = 0 

455 try: 

456 assert distance_near >= 0 

457 assert distance_far >= distance_near 

458 except AssertionError: 

459 print() 

460 raise 

461 

462 if distance_far == distance_near: 

463 return 

464 

465 edge_interval_length = edge_interval[1] - edge_interval[0] 

466 edge_interval_length_um = edge_interval_length * self.dbu 

467 

468 cap_femto = self.fringe_cap(edge_interval_length=edge_interval_length, 

469 distance_near=distance_near, 

470 distance_far=distance_far, 

471 overlap_cap_spec=overlap_cap_spec, 

472 sideoverlap_cap_spec=sideoverlap_cap_spec) 

473 

474 if cap_femto > 0.0001: # TODO: configurable threshold, but keeping accumulation might also be nice 

475 # info(f"(Side Overlap) " 

476 # f"{inside_layer_name}({inside_net_name})-{outside_layer_name}({outside_net_name}): " 

477 # f"{round(cap_femto, 5)} fF, " 

478 # f"edge interval length = {round(edge_interval_length_um, 2)} µm") 

479 

480 sok = SideOverlapKey(layer_inside=inside_layer_name, 

481 net_inside=inside_net_name, 

482 layer_outside=outside_layer_name, 

483 net_outside=outside_net_name) 

484 soc = SideOverlapCap(key=sok, cap_value=cap_femto) 

485 self.results.add_sideoverlap_cap(soc) 

486 

487 self.report.output_sideoverlap( 

488 sideoverlap_cap=soc, 

489 inside_edge=geometry_restorer.restore_edge_interval(edge_interval), 

490 outside_polygon=geometry_restorer.restore_polygon(p), 

491 lateral_shield=geometry_restorer.restore_polygon(lateral_shield) \ 

492 if lateral_shield is not None else None 

493 )