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

186 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-17 19:08 +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 info, 

33 warning, 

34 get_log_level, 

35 LogLevel 

36) 

37from klayout_pex.tech_info import TechInfo 

38 

39from klayout_pex.rcx25.c.geometry_restorer import GeometryRestorer 

40from klayout_pex.rcx25.extraction_results import * 

41from klayout_pex.rcx25.extraction_reporter import ExtractionReporter 

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

43from klayout_pex.rcx25.types import EdgeInterval, EdgeNeighborhood 

44from klayout_pex_protobuf.kpex.tech.process_parasitics_pb2 import CapacitanceInfo 

45 

46 

47class SidewallAndFringeExtractor: 

48 def __init__(self, 

49 all_layer_names: List[LayerName], 

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

51 dbu: float, 

52 scale_ratio_to_fit_halo: bool, 

53 tech_info: TechInfo, 

54 results: CellExtractionResults, 

55 report: ExtractionReporter): 

56 self.all_layer_names = all_layer_names 

57 self.layer_regions_by_name = layer_regions_by_name 

58 self.dbu = dbu 

59 self.scale_ratio_to_fit_halo = scale_ratio_to_fit_halo 

60 self.tech_info = tech_info 

61 self.results = results 

62 self.report = report 

63 

64 self.all_layer_regions = layer_regions_by_name.values() 

65 

66 def extract(self): 

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

68 other_layer_regions = [ 

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

70 if ln != layer_name 

71 ] 

72 

73 en_visitor = self.PEXEdgeNeighborhoodVisitor( 

74 all_layer_names=self.all_layer_names, 

75 inside_layer_index=idx, 

76 dbu=self.dbu, 

77 scale_ratio_to_fit_halo=self.scale_ratio_to_fit_halo, 

78 tech_info=self.tech_info, 

79 results=self.results, 

80 report=self.report 

81 ) 

82 

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

84 for r in self.all_layer_regions] 

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

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

87 

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

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

90 

91 en_node = kdb.CompoundRegionOperationNode.new_edge_neighborhood( 

92 children=en_children, 

93 visitor=en_visitor, 

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

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

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

97 dout=side_halo_dbu # dout 

98 ) 

99 

100 layer_region.complex_op(en_node) 

101 

102 # ------------------------------------------------------------------------ 

103 

104 class PEXEdgeNeighborhoodVisitor(kdb.EdgeNeighborhoodVisitor): 

105 def __init__(self, 

106 all_layer_names: List[LayerName], 

107 inside_layer_index: int, 

108 dbu: float, 

109 tech_info: TechInfo, 

110 scale_ratio_to_fit_halo: bool, 

111 results: CellExtractionResults, 

112 report: ExtractionReporter): 

113 super().__init__() 

114 

115 self.all_layer_names = all_layer_names 

116 self.inside_layer_index = inside_layer_index 

117 self.dbu = dbu 

118 self.tech_info = tech_info 

119 self.scale_ratio_to_fit_halo = scale_ratio_to_fit_halo 

120 self.results = results 

121 self.report = report 

122 

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

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

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

126 self.layer_above_indices = range(inside_layer_index, 

127 len(all_layer_names) - inside_layer_index) 

128 

129 @cached_property 

130 def inside_layer_name(self) -> LayerName: 

131 return self.all_layer_names[self.inside_layer_index] 

132 

133 def begin_polygon(self, 

134 layout: kdb.Layout, 

135 cell: kdb.Cell, 

136 polygon: kdb.Polygon): 

137 pass 

138 

139 def end_polygon(self): 

140 pass 

141 

142 @cached_property 

143 def side_halo(self) -> float: 

144 return self.tech_info.tech.process_parasitics.side_halo 

145 

146 def on_edge(self, 

147 layout: kdb.Layout, 

148 cell: kdb.Cell, 

149 edge: kdb.EdgeWithProperties, 

150 neighborhood: EdgeNeighborhood): 

151 # 

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

153 # going from 0 to edge.length 

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

155 # 

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

157 

158 if get_log_level() == LogLevel.DEBUG: 

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

160 all_layer_names=self.all_layer_names, 

161 edge=edge, 

162 neighborhood=neighborhood, 

163 geometry_restorer=geometry_restorer) 

164 

165 for edge_interval, polygons_by_child in neighborhood: 

166 if not polygons_by_child: 

167 continue 

168 

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

170 if edge_interval_length <= 1: 

171 warning(f"Short edge interval {edge_interval} " 

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

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

174 continue 

175 

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

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

178 if child_index < len(self.all_layer_names): 

179 layer_fringe_shields[child_index].insert(polygons) 

180 

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

182 # - sidewall (other net) 

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

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

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

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

187 

188 nearest_distance: Optional[float] = None 

189 nearest_lateral_edge: Optional[kdb.EdgeWithProperties] = None 

190 

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

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

193 distance, nearby_polygon = find_polygon_with_nearest_edge(polygons_on_same_layer=polygons) 

194 

195 if nearest_distance is None or \ 

196 distance < nearest_distance: 

197 nearest_distance = distance 

198 nearest_lateral_edge = nearest_edge(nearby_polygon) 

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

200 # NOTE: use only the nearest polygon, 

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

202 distance, nearby_polygon = find_polygon_with_nearest_edge(polygons_on_same_layer=polygons) 

203 

204 if nearest_distance is None or \ 

205 distance < nearest_distance: 

206 nearest_distance = distance 

207 nearest_lateral_edge = nearest_edge(nearby_polygon) 

208 

209 self.emit_sidewall( 

210 layer_name=self.inside_layer_name, 

211 edge=edge, 

212 edge_interval=edge_interval, 

213 polygon=nearby_polygon, 

214 geometry_restorer=geometry_restorer 

215 ) 

216 

217 lateral_shield: Optional[kdb.Polygon] = None 

218 if nearest_lateral_edge is not None: 

219 lateral_shield = kdb.Polygon([ 

220 nearest_lateral_edge.p2, 

221 nearest_lateral_edge.p1, 

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

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

224 ]) 

225 

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

227 if self.inside_layer_index == child_index: 

228 continue # already handled above 

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

230 fringe_shield = kdb.Region() 

231 if lateral_shield is not None: 

232 fringe_shield.insert(lateral_shield) 

233 if child_index < self.inside_layer_index: 

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

235 for idx in r: 

236 fringe_shield += layer_fringe_shields[idx] 

237 elif self.inside_layer_index < child_index: 

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

239 for idx in r: 

240 fringe_shield += layer_fringe_shields[idx] 

241 

242 # NOTE: 

243 # polygons can have different nets 

244 # polygons can be segmented after shield is applied 

245 

246 self.emit_fringe( 

247 inside_layer_name=self.inside_layer_name, 

248 outside_layer_name=self.all_layer_names[child_index], 

249 edge=edge, 

250 edge_interval=edge_interval, 

251 outside_polygons=polygons, 

252 shield=fringe_shield, 

253 lateral_shield=lateral_shield, 

254 geometry_restorer=geometry_restorer) 

255 

256 def emit_sidewall(self, 

257 layer_name: LayerName, 

258 edge: kdb.EdgeWithProperties, 

259 edge_interval: EdgeInterval, 

260 polygon: kdb.PolygonWithProperties, 

261 geometry_restorer: GeometryRestorer): 

262 net1 = edge.property('net') 

263 net2 = polygon.property('net') 

264 

265 if net1 == net2: 

266 return 

267 

268 sidewall_cap_spec = self.tech_info.sidewall_cap_by_layer_name[layer_name] 

269 

270 # TODO! 

271 

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

273 # polygons have 4 points. 

274 # Polygons points are sorted clockwise, so the edge 

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

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

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

278 

279 # C = Csidewall * l * t / s 

280 # C = Csidewall * l / s 

281 

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

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

284 

285 outside_edge = nearest_edge(polygon) 

286 

287 length_um = avg_length * self.dbu 

288 distance_um = avg_distance * self.dbu 

289 

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

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

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

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

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

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

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

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

298 # lives here, see 

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

300 cap_femto = ((length_um * sidewall_cap_spec.capacitance) 

301 / (distance_um + sidewall_cap_spec.offset) 

302 / 2.0 # non-bidirectional (half) 

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

304 / 1000.0) # aF -> fF 

305 

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

307 

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

309 sw_cap = SidewallCap(key=swk, 

310 cap_value=cap_femto, 

311 distance=distance_um, 

312 length=length_um, 

313 tech_spec=sidewall_cap_spec) 

314 self.results.add_sidewall_cap(sw_cap) 

315 

316 self.report.output_sidewall( 

317 sidewall_cap=sw_cap, 

318 inside_edge=geometry_restorer.restore_edge_interval(edge_interval), 

319 outside_edge=geometry_restorer.restore_edge(outside_edge) 

320 ) 

321 

322 def fringe_cap(self, 

323 edge_interval_length: float, 

324 distance_near: float, 

325 distance_far: float, 

326 overlap_cap_spec: CapacitanceInfo.OverlapCapacitance, 

327 sideoverlap_cap_spec: CapacitanceInfo.SideOverlapCapacitance) -> float: 

328 distance_near_um = distance_near * self.dbu 

329 distance_far_um = distance_far * self.dbu 

330 edge_interval_length_um = edge_interval_length * self.dbu 

331 

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

333 alpha_scale_factor = 0.02 * 0.01 * 0.5 * 200.0 

334 alpha_c = overlap_cap_spec.capacitance * alpha_scale_factor 

335 

336 # see Magic ExtCouple.c L1164 

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

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

339 

340 if self.scale_ratio_to_fit_halo: 

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

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

343 # but it is smaller, so we compensate 

344 if full_halo_ratio < 1.0: 

345 cnear /= full_halo_ratio 

346 cfar /= full_halo_ratio 

347 

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

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

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

351 cfrac = cfar - cnear 

352 

353 cap_femto = (cfrac * edge_interval_length_um * 

354 sideoverlap_cap_spec.capacitance / 1000.0) 

355 

356 return cap_femto 

357 

358 def emit_fringe(self, 

359 inside_layer_name: LayerName, 

360 outside_layer_name: LayerName, 

361 edge: kdb.EdgeWithProperties, 

362 edge_interval: EdgeInterval, 

363 outside_polygons: List[kdb.PolygonWithProperties], 

364 shield: kdb.Region, 

365 lateral_shield: kdb.Polygon, 

366 geometry_restorer: GeometryRestorer): 

367 inside_net_name = self.tech_info.internal_substrate_layer_name \ 

368 if inside_layer_name == self.tech_info.internal_substrate_layer_name \ 

369 else edge.property('net') 

370 

371 # NOTE: each polygon in outside_polygons 

372 # - could have a different net 

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

374 # each with different near/far regions 

375 

376 outside_net_names = [ 

377 self.tech_info.internal_substrate_layer_name \ 

378 if outside_layer_name == self.tech_info.internal_substrate_layer_name \ 

379 else p.property('net') 

380 for p in outside_polygons 

381 ] 

382 

383 same_net_markers = [ 

384 inside_net_name == outside_net_name 

385 for outside_net_name in outside_net_names 

386 ] 

387 

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

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

390 None) 

391 if not overlap_cap_spec: 

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

393 

394 substrate_cap_spec = self.tech_info.substrate_cap_by_layer_name[inside_layer_name] 

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

396 outside_layer_name] 

397 

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

399 

400 for idx, p in enumerate(outside_polygons): 

401 outside_net = outside_net_names[idx] 

402 is_same_net = same_net_markers[idx] 

403 

404 if is_same_net: 

405 # TODO: log? 

406 continue 

407 

408 if shield.is_empty(): 

409 polygons_by_net[outside_net].append(p) 

410 else: 

411 unshielded_region = kdb.Region(p) 

412 unshielded_region.enable_properties() 

413 unshielded_region -= shield 

414 if unshielded_region.is_empty(): 

415 # TODO: log? 

416 continue 

417 

418 for up in unshielded_region.each(): 

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

420 polygons_by_net[outside_net].append(up) 

421 # if p != up: 

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

423 

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

425 for p in polygons: 

426 bbox = p.bbox() 

427 if not p.is_box(): 

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

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

430 

431 distance_near = bbox.p1.y # + 1 

432 if distance_near < 0: 

433 distance_near = 0 

434 distance_far = bbox.p2.y # - 2 

435 if distance_far < 0: 

436 distance_far = 0 

437 try: 

438 assert distance_near >= 0 

439 assert distance_far >= distance_near 

440 except AssertionError: 

441 print() 

442 raise 

443 

444 if distance_far == distance_near: 

445 return 

446 

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

448 edge_interval_length_um = edge_interval_length * self.dbu 

449 

450 cap_femto = self.fringe_cap(edge_interval_length=edge_interval_length, 

451 distance_near=distance_near, 

452 distance_far=distance_far, 

453 overlap_cap_spec=overlap_cap_spec, 

454 sideoverlap_cap_spec=sideoverlap_cap_spec) 

455 

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

457 # info(f"(Side Overlap) " 

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

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

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

461 

462 sok = SideOverlapKey(layer_inside=inside_layer_name, 

463 net_inside=inside_net_name, 

464 layer_outside=outside_layer_name, 

465 net_outside=outside_net_name) 

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

467 self.results.add_sideoverlap_cap(soc) 

468 

469 self.report.output_sideoverlap( 

470 sideoverlap_cap=soc, 

471 inside_edge=geometry_restorer.restore_edge_interval(edge_interval), 

472 outside_polygon=geometry_restorer.restore_polygon(p), 

473 lateral_shield=geometry_restorer.restore_polygon(lateral_shield) \ 

474 if lateral_shield is not None else None 

475 )