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

188 statements  

« prev     ^ index     » next       coverage.py v7.10.2, created at 2025-08-08 18:54 +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/martinjankoehler/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 cap_femto = ((length_um * sidewall_cap_spec.capacitance) 

293 / (distance_um + sidewall_cap_spec.offset) 

294 / 2.0 # non-bidirectional (half) 

295 / 1000.0) # aF -> fF 

296 

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

298 

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

300 sw_cap = SidewallCap(key=swk, 

301 cap_value=cap_femto, 

302 distance=distance_um, 

303 length=length_um, 

304 tech_spec=sidewall_cap_spec) 

305 self.results.add_sidewall_cap(sw_cap) 

306 

307 self.report.output_sidewall( 

308 sidewall_cap=sw_cap, 

309 inside_edge=geometry_restorer.restore_edge_interval(edge_interval), 

310 outside_edge=geometry_restorer.restore_edge(outside_edge) 

311 ) 

312 

313 def fringe_cap(self, 

314 edge_interval_length: float, 

315 distance_near: float, 

316 distance_far: float, 

317 overlap_cap_spec: CapacitanceInfo.OverlapCapacitance, 

318 sideoverlap_cap_spec: CapacitanceInfo.SideOverlapCapacitance) -> float: 

319 distance_near_um = distance_near * self.dbu 

320 distance_far_um = distance_far * self.dbu 

321 edge_interval_length_um = edge_interval_length * self.dbu 

322 

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

324 alpha_scale_factor = 0.02 * 0.01 * 0.5 * 200.0 

325 alpha_c = overlap_cap_spec.capacitance * alpha_scale_factor 

326 

327 # see Magic ExtCouple.c L1164 

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

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

330 

331 if self.scale_ratio_to_fit_halo: 

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

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

334 # but it is smaller, so we compensate 

335 if full_halo_ratio < 1.0: 

336 cnear /= full_halo_ratio 

337 cfar /= full_halo_ratio 

338 

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

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

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

342 cfrac = cfar - cnear 

343 

344 cap_femto = (cfrac * edge_interval_length_um * 

345 sideoverlap_cap_spec.capacitance / 1000.0) 

346 

347 return cap_femto 

348 

349 def emit_fringe(self, 

350 inside_layer_name: LayerName, 

351 outside_layer_name: LayerName, 

352 edge: kdb.EdgeWithProperties, 

353 edge_interval: EdgeInterval, 

354 outside_polygons: List[kdb.PolygonWithProperties], 

355 shield: kdb.Region, 

356 lateral_shield: kdb.Polygon, 

357 geometry_restorer: GeometryRestorer): 

358 inside_net_name = self.tech_info.internal_substrate_layer_name \ 

359 if inside_layer_name == self.tech_info.internal_substrate_layer_name \ 

360 else edge.property('net') 

361 

362 # NOTE: each polygon in outside_polygons 

363 # - could have a different net 

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

365 # each with different near/far regions 

366 

367 outside_net_names = [ 

368 self.tech_info.internal_substrate_layer_name \ 

369 if outside_layer_name == self.tech_info.internal_substrate_layer_name \ 

370 else p.property('net') 

371 for p in outside_polygons 

372 ] 

373 

374 same_net_markers = [ 

375 inside_net_name == outside_net_name 

376 for outside_net_name in outside_net_names 

377 ] 

378 

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

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

381 None) 

382 if not overlap_cap_spec: 

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

384 

385 substrate_cap_spec = self.tech_info.substrate_cap_by_layer_name[inside_layer_name] 

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

387 outside_layer_name] 

388 

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

390 

391 for idx, p in enumerate(outside_polygons): 

392 outside_net = outside_net_names[idx] 

393 is_same_net = same_net_markers[idx] 

394 

395 if is_same_net: 

396 # TODO: log? 

397 continue 

398 

399 if shield.is_empty(): 

400 polygons_by_net[outside_net].append(p) 

401 else: 

402 unshielded_region = kdb.Region(p) 

403 unshielded_region.enable_properties() 

404 unshielded_region -= shield 

405 if unshielded_region.is_empty(): 

406 # TODO: log? 

407 continue 

408 

409 for up in unshielded_region.each(): 

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

411 polygons_by_net[outside_net].append(up) 

412 # if p != up: 

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

414 

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

416 for p in polygons: 

417 bbox = p.bbox() 

418 if not p.is_box(): 

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

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

421 

422 distance_near = bbox.p1.y # + 1 

423 if distance_near < 0: 

424 distance_near = 0 

425 distance_far = bbox.p2.y # - 2 

426 if distance_far < 0: 

427 distance_far = 0 

428 try: 

429 assert distance_near >= 0 

430 assert distance_far >= distance_near 

431 except AssertionError: 

432 print() 

433 raise 

434 

435 if distance_far == distance_near: 

436 return 

437 

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

439 edge_interval_length_um = edge_interval_length * self.dbu 

440 

441 cap_femto = self.fringe_cap(edge_interval_length=edge_interval_length, 

442 distance_near=distance_near, 

443 distance_far=distance_far, 

444 overlap_cap_spec=overlap_cap_spec, 

445 sideoverlap_cap_spec=sideoverlap_cap_spec) 

446 

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

448 info(f"(Side Overlap) " 

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

450 f"{round(cap_femto, 5)} fF, " 

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

452 

453 sok = SideOverlapKey(layer_inside=inside_layer_name, 

454 net_inside=inside_net_name, 

455 layer_outside=outside_layer_name, 

456 net_outside=outside_net_name) 

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

458 self.results.add_sideoverlap_cap(soc) 

459 

460 self.report.output_sideoverlap( 

461 sideoverlap_cap=soc, 

462 inside_edge=geometry_restorer.restore_edge_interval(edge_interval), 

463 outside_polygon=geometry_restorer.restore_polygon(p), 

464 lateral_shield=geometry_restorer.restore_polygon(lateral_shield) \ 

465 if lateral_shield is not None else None 

466 )