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

187 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-03-31 19:36 +0000

1#! /usr/bin/env python3 

2# 

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

4# SPDX-FileCopyrightText: 2024 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) 

35from klayout_pex.tech_info import TechInfo 

36 

37from klayout_pex.rcx25.c.geometry_restorer import GeometryRestorer 

38from klayout_pex.rcx25.extraction_results import * 

39from klayout_pex.rcx25.extraction_reporter import ExtractionReporter 

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

41from klayout_pex.rcx25.types import EdgeInterval, EdgeNeighborhood 

42from process_parasitics_pb2 import CapacitanceInfo 

43 

44 

45class SidewallAndFringeExtractor: 

46 def __init__(self, 

47 all_layer_names: List[LayerName], 

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

49 dbu: float, 

50 scale_ratio_to_fit_halo: bool, 

51 tech_info: TechInfo, 

52 results: CellExtractionResults, 

53 report: ExtractionReporter): 

54 self.all_layer_names = all_layer_names 

55 self.layer_regions_by_name = layer_regions_by_name 

56 self.dbu = dbu 

57 self.scale_ratio_to_fit_halo = scale_ratio_to_fit_halo 

58 self.tech_info = tech_info 

59 self.results = results 

60 self.report = report 

61 

62 self.all_layer_regions = layer_regions_by_name.values() 

63 

64 def extract(self): 

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

66 other_layer_regions = [ 

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

68 if ln != layer_name 

69 ] 

70 

71 en_visitor = self.PEXEdgeNeighborhoodVisitor( 

72 all_layer_names=self.all_layer_names, 

73 inside_layer_index=idx, 

74 dbu=self.dbu, 

75 scale_ratio_to_fit_halo=self.scale_ratio_to_fit_halo, 

76 tech_info=self.tech_info, 

77 results=self.results, 

78 report=self.report 

79 ) 

80 

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

82 for r in self.all_layer_regions] 

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

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

85 

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

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

88 

89 en_node = kdb.CompoundRegionOperationNode.new_edge_neighborhood( 

90 children=en_children, 

91 visitor=en_visitor, 

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

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

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

95 dout=side_halo_dbu # dout 

96 ) 

97 

98 layer_region.complex_op(en_node) 

99 

100 # ------------------------------------------------------------------------ 

101 

102 class PEXEdgeNeighborhoodVisitor(kdb.EdgeNeighborhoodVisitor): 

103 def __init__(self, 

104 all_layer_names: List[LayerName], 

105 inside_layer_index: int, 

106 dbu: float, 

107 tech_info: TechInfo, 

108 scale_ratio_to_fit_halo: bool, 

109 results: CellExtractionResults, 

110 report: ExtractionReporter): 

111 super().__init__() 

112 

113 self.all_layer_names = all_layer_names 

114 self.inside_layer_index = inside_layer_index 

115 self.dbu = dbu 

116 self.tech_info = tech_info 

117 self.scale_ratio_to_fit_halo = scale_ratio_to_fit_halo 

118 self.results = results 

119 self.report = report 

120 

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

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

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

124 self.layer_above_indices = range(inside_layer_index, 

125 len(all_layer_names) - inside_layer_index) 

126 

127 @cached_property 

128 def inside_layer_name(self) -> LayerName: 

129 return self.all_layer_names[self.inside_layer_index] 

130 

131 def begin_polygon(self, 

132 layout: kdb.Layout, 

133 cell: kdb.Cell, 

134 polygon: kdb.Polygon): 

135 pass 

136 

137 def end_polygon(self): 

138 pass 

139 

140 @cached_property 

141 def side_halo(self) -> float: 

142 return self.tech_info.tech.process_parasitics.side_halo 

143 

144 def on_edge(self, 

145 layout: kdb.Layout, 

146 cell: kdb.Cell, 

147 edge: kdb.EdgeWithProperties, 

148 neighborhood: EdgeNeighborhood): 

149 # 

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

151 # going from 0 to edge.length 

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

153 # 

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

155 

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

157 all_layer_names=self.all_layer_names, 

158 edge=edge, 

159 neighborhood=neighborhood, 

160 geometry_restorer=geometry_restorer) 

161 

162 for edge_interval, polygons_by_child in neighborhood: 

163 if not polygons_by_child: 

164 continue 

165 

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

167 if edge_interval_length <= 1: 

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

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

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

171 continue 

172 

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

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

175 if child_index < len(self.all_layer_names): 

176 layer_fringe_shields[child_index].insert(polygons) 

177 

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

179 # - sidewall (other net) 

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

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

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

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

184 

185 nearest_distance: Optional[float] = None 

186 nearest_lateral_edge: Optional[kdb.EdgeWithProperties] = None 

187 

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

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

190 distance, nearby_polygon = find_polygon_with_nearest_edge(polygons_on_same_layer=polygons) 

191 

192 if nearest_distance is None or \ 

193 distance < nearest_distance: 

194 nearest_distance = distance 

195 nearest_lateral_edge = nearest_edge(nearby_polygon) 

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

197 # NOTE: use only the nearest polygon, 

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

199 distance, nearby_polygon = find_polygon_with_nearest_edge(polygons_on_same_layer=polygons) 

200 

201 if nearest_distance is None or \ 

202 distance < nearest_distance: 

203 nearest_distance = distance 

204 nearest_lateral_edge = nearest_edge(nearby_polygon) 

205 

206 self.emit_sidewall( 

207 layer_name=self.inside_layer_name, 

208 edge=edge, 

209 edge_interval=edge_interval, 

210 polygon=nearby_polygon, 

211 geometry_restorer=geometry_restorer 

212 ) 

213 

214 lateral_shield: Optional[kdb.Polygon] = None 

215 if nearest_lateral_edge is not None: 

216 lateral_shield = kdb.Polygon([ 

217 nearest_lateral_edge.p2, 

218 nearest_lateral_edge.p1, 

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

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

221 ]) 

222 

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

224 if self.inside_layer_index == child_index: 

225 continue # already handled above 

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

227 fringe_shield = kdb.Region() 

228 if lateral_shield is not None: 

229 fringe_shield.insert(lateral_shield) 

230 if child_index < self.inside_layer_index: 

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

232 for idx in r: 

233 fringe_shield += layer_fringe_shields[idx] 

234 elif self.inside_layer_index < child_index: 

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

236 for idx in r: 

237 fringe_shield += layer_fringe_shields[idx] 

238 

239 # NOTE: 

240 # polygons can have different nets 

241 # polygons can be segmented after shield is applied 

242 

243 self.emit_fringe( 

244 inside_layer_name=self.inside_layer_name, 

245 outside_layer_name=self.all_layer_names[child_index], 

246 edge=edge, 

247 edge_interval=edge_interval, 

248 outside_polygons=polygons, 

249 shield=fringe_shield, 

250 lateral_shield=lateral_shield, 

251 geometry_restorer=geometry_restorer) 

252 

253 def emit_sidewall(self, 

254 layer_name: LayerName, 

255 edge: kdb.EdgeWithProperties, 

256 edge_interval: EdgeInterval, 

257 polygon: kdb.PolygonWithProperties, 

258 geometry_restorer: GeometryRestorer): 

259 net1 = edge.property('net') 

260 net2 = polygon.property('net') 

261 

262 if net1 == net2: 

263 return 

264 

265 sidewall_cap_spec = self.tech_info.sidewall_cap_by_layer_name[layer_name] 

266 

267 # TODO! 

268 

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

270 # polygons have 4 points. 

271 # Polygons points are sorted clockwise, so the edge 

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

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

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

275 

276 # C = Csidewall * l * t / s 

277 # C = Csidewall * l / s 

278 

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

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

281 

282 outside_edge = nearest_edge(polygon) 

283 

284 length_um = avg_length * self.dbu 

285 distance_um = avg_distance * self.dbu 

286 

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

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

289 cap_femto = ((length_um * sidewall_cap_spec.capacitance) 

290 / (distance_um + sidewall_cap_spec.offset) 

291 / 2.0 # non-bidirectional (half) 

292 / 1000.0) # aF -> fF 

293 

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

295 

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

297 sw_cap = SidewallCap(key=swk, 

298 cap_value=cap_femto, 

299 distance=distance_um, 

300 length=length_um, 

301 tech_spec=sidewall_cap_spec) 

302 self.results.add_sidewall_cap(sw_cap) 

303 

304 self.report.output_sidewall( 

305 sidewall_cap=sw_cap, 

306 inside_edge=geometry_restorer.restore_edge_interval(edge_interval), 

307 outside_edge=geometry_restorer.restore_edge(outside_edge) 

308 ) 

309 

310 def fringe_cap(self, 

311 edge_interval_length: float, 

312 distance_near: float, 

313 distance_far: float, 

314 overlap_cap_spec: CapacitanceInfo.OverlapCapacitance, 

315 sideoverlap_cap_spec: CapacitanceInfo.SideOverlapCapacitance) -> float: 

316 distance_near_um = distance_near * self.dbu 

317 distance_far_um = distance_far * self.dbu 

318 edge_interval_length_um = edge_interval_length * self.dbu 

319 

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

321 alpha_scale_factor = 0.02 * 0.01 * 0.5 * 200.0 

322 alpha_c = overlap_cap_spec.capacitance * alpha_scale_factor 

323 

324 # see Magic ExtCouple.c L1164 

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

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

327 

328 if self.scale_ratio_to_fit_halo: 

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

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

331 # but it is smaller, so we compensate 

332 if full_halo_ratio < 1.0: 

333 cnear /= full_halo_ratio 

334 cfar /= full_halo_ratio 

335 

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

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

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

339 cfrac = cfar - cnear 

340 

341 cap_femto = (cfrac * edge_interval_length_um * 

342 sideoverlap_cap_spec.capacitance / 1000.0) 

343 

344 return cap_femto 

345 

346 def emit_fringe(self, 

347 inside_layer_name: LayerName, 

348 outside_layer_name: LayerName, 

349 edge: kdb.EdgeWithProperties, 

350 edge_interval: EdgeInterval, 

351 outside_polygons: List[kdb.PolygonWithProperties], 

352 shield: kdb.Region, 

353 lateral_shield: kdb.Polygon, 

354 geometry_restorer: GeometryRestorer): 

355 inside_net_name = self.tech_info.internal_substrate_layer_name \ 

356 if inside_layer_name == self.tech_info.internal_substrate_layer_name \ 

357 else edge.property('net') 

358 

359 # NOTE: each polygon in outside_polygons 

360 # - could have a different net 

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

362 # each with different near/far regions 

363 

364 outside_net_names = [ 

365 self.tech_info.internal_substrate_layer_name \ 

366 if outside_layer_name == self.tech_info.internal_substrate_layer_name \ 

367 else p.property('net') 

368 for p in outside_polygons 

369 ] 

370 

371 same_net_markers = [ 

372 inside_net_name == outside_net_name 

373 for outside_net_name in outside_net_names 

374 ] 

375 

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

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

378 None) 

379 if not overlap_cap_spec: 

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

381 

382 substrate_cap_spec = self.tech_info.substrate_cap_by_layer_name[inside_layer_name] 

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

384 outside_layer_name] 

385 

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

387 

388 for idx, p in enumerate(outside_polygons): 

389 outside_net = outside_net_names[idx] 

390 is_same_net = same_net_markers[idx] 

391 

392 if is_same_net: 

393 # TODO: log? 

394 continue 

395 

396 if shield.is_empty(): 

397 polygons_by_net[outside_net].append(p) 

398 else: 

399 unshielded_region = kdb.Region(p) 

400 unshielded_region.enable_properties() 

401 unshielded_region -= shield 

402 if unshielded_region.is_empty(): 

403 # TODO: log? 

404 continue 

405 

406 for up in unshielded_region.each(): 

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

408 polygons_by_net[outside_net].append(up) 

409 # if p != up: 

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

411 

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

413 for p in polygons: 

414 bbox = p.bbox() 

415 if not p.is_box(): 

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

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

418 

419 distance_near = bbox.p1.y # + 1 

420 if distance_near < 0: 

421 distance_near = 0 

422 distance_far = bbox.p2.y # - 2 

423 if distance_far < 0: 

424 distance_far = 0 

425 try: 

426 assert distance_near >= 0 

427 assert distance_far >= distance_near 

428 except AssertionError: 

429 print() 

430 raise 

431 

432 if distance_far == distance_near: 

433 return 

434 

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

436 edge_interval_length_um = edge_interval_length * self.dbu 

437 

438 cap_femto = self.fringe_cap(edge_interval_length=edge_interval_length, 

439 distance_near=distance_near, 

440 distance_far=distance_far, 

441 overlap_cap_spec=overlap_cap_spec, 

442 sideoverlap_cap_spec=sideoverlap_cap_spec) 

443 

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

445 info(f"(Side Overlap) " 

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

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

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

449 

450 sok = SideOverlapKey(layer_inside=inside_layer_name, 

451 net_inside=inside_net_name, 

452 layer_outside=outside_layer_name, 

453 net_outside=outside_net_name) 

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

455 self.results.add_sideoverlap_cap(soc) 

456 

457 self.report.output_sideoverlap( 

458 sideoverlap_cap=soc, 

459 inside_edge=geometry_restorer.restore_edge_interval(edge_interval), 

460 outside_polygon=geometry_restorer.restore_polygon(p), 

461 lateral_shield=geometry_restorer.restore_polygon(lateral_shield) \ 

462 if lateral_shield is not None else None 

463 )