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

213 statements  

« prev     ^ index     » next       coverage.py v7.17.2, created at 2026-11-06 13:14 +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 mos_channels: kdb.Region, 

58 results: CellExtractionResults, 

59 report: ExtractionReporter): 

60 """ 

61 :param mos_channels: the channels of the transistors, whose gate edges along them have no fringe 

62 to the substrate and wells (see emit_fringe) 

63 """ 

64 self.all_layer_names = all_layer_names 

65 self.layer_regions_by_name = layer_regions_by_name 

66 self.dbu = dbu 

67 self.scale_ratio_to_fit_halo = scale_ratio_to_fit_halo 

68 self.tech_info = tech_info 

69 self.device_capacitor_plates = device_capacitor_plates 

70 self.results = results 

71 self.report = report 

72 

73 # NOTE: found by their boxes, as each gate edge looks them up 

74 self.mos_channels = kdb.Shapes() 

75 for channel in mos_channels.each(): 

76 self.mos_channels.insert(channel) 

77 

78 self.all_layer_regions = layer_regions_by_name.values() 

79 

80 def extract(self): 

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

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

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

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

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

86 layer_name in self.tech_info.process_diffusion_layer_names: 

87 continue 

88 

89 other_layer_regions = [ 

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

91 if ln != layer_name 

92 ] 

93 

94 en_visitor = self.PEXEdgeNeighborhoodVisitor( 

95 all_layer_names=self.all_layer_names, 

96 inside_layer_index=idx, 

97 dbu=self.dbu, 

98 scale_ratio_to_fit_halo=self.scale_ratio_to_fit_halo, 

99 tech_info=self.tech_info, 

100 device_capacitor_plates=self.device_capacitor_plates, 

101 mos_channels=self.mos_channels, 

102 results=self.results, 

103 report=self.report 

104 ) 

105 

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

107 for r in self.all_layer_regions] 

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

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

110 

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

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

113 

114 en_node = kdb.CompoundRegionOperationNode.new_edge_neighborhood( 

115 children=en_children, 

116 visitor=en_visitor, 

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

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

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

120 dout=side_halo_dbu # dout 

121 ) 

122 

123 layer_region.complex_op(en_node) 

124 

125 # ------------------------------------------------------------------------ 

126 

127 class PEXEdgeNeighborhoodVisitor(kdb.EdgeNeighborhoodVisitor): 

128 def __init__(self, 

129 all_layer_names: List[LayerName], 

130 inside_layer_index: int, 

131 dbu: float, 

132 tech_info: TechInfo, 

133 scale_ratio_to_fit_halo: bool, 

134 device_capacitor_plates: DeviceCapacitorPlates, 

135 mos_channels: kdb.Shapes, 

136 results: CellExtractionResults, 

137 report: ExtractionReporter): 

138 super().__init__() 

139 

140 self.all_layer_names = all_layer_names 

141 self.inside_layer_index = inside_layer_index 

142 self.dbu = dbu 

143 self.tech_info = tech_info 

144 self.scale_ratio_to_fit_halo = scale_ratio_to_fit_halo 

145 self.device_capacitor_plates = device_capacitor_plates 

146 self.mos_channels = mos_channels 

147 self.results = results 

148 self.report = report 

149 

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

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

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

153 self.layer_above_indices = range(inside_layer_index, 

154 len(all_layer_names) - inside_layer_index) 

155 

156 @cached_property 

157 def inside_layer_name(self) -> LayerName: 

158 return self.all_layer_names[self.inside_layer_index] 

159 

160 def begin_polygon(self, 

161 layout: kdb.Layout, 

162 cell: kdb.Cell, 

163 polygon: kdb.Polygon): 

164 pass 

165 

166 def end_polygon(self): 

167 pass 

168 

169 @cached_property 

170 def side_halo(self) -> float: 

171 return self.tech_info.tech.process_parasitics.side_halo 

172 

173 def on_edge(self, 

174 layout: kdb.Layout, 

175 cell: kdb.Cell, 

176 edge: kdb.EdgeWithProperties, 

177 neighborhood: EdgeNeighborhood): 

178 # 

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

180 # going from 0 to edge.length 

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

182 # 

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

184 

185 if get_log_level() == LogLevel.DEBUG: 

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

187 all_layer_names=self.all_layer_names, 

188 edge=edge, 

189 neighborhood=neighborhood, 

190 geometry_restorer=geometry_restorer) 

191 

192 for edge_interval, polygons_by_child in neighborhood: 

193 if not polygons_by_child: 

194 continue 

195 

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

197 if edge_interval_length <= 1: 

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

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

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

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

202 continue 

203 

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

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

206 if child_index < len(self.all_layer_names): 

207 layer_fringe_shields[child_index].insert(polygons) 

208 

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

210 # - sidewall (other net) 

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

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

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

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

215 

216 nearest_distance: Optional[float] = None 

217 nearest_lateral_edge: Optional[kdb.EdgeWithProperties] = None 

218 nearest_of_same_polygon: Optional[Tuple[float, kdb.PolygonWithProperties]] = None 

219 nearest_of_other_polygons: Optional[float] = None 

220 

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

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

223 distance, nearby_polygon = find_polygon_with_nearest_edge(polygons_on_same_layer=polygons) 

224 

225 if nearest_distance is None or \ 

226 distance < nearest_distance: 

227 nearest_distance = distance 

228 nearest_lateral_edge = nearest_edge(nearby_polygon) 

229 

230 nearest_of_same_polygon = distance, nearby_polygon 

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

232 # NOTE: use only the nearest polygon, 

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

234 distance, nearby_polygon = find_polygon_with_nearest_edge(polygons_on_same_layer=polygons) 

235 

236 if nearest_distance is None or \ 

237 distance < nearest_distance: 

238 nearest_distance = distance 

239 nearest_lateral_edge = nearest_edge(nearby_polygon) 

240 nearest_of_other_polygons = distance 

241 

242 self.emit_sidewall( 

243 layer_name=self.inside_layer_name, 

244 edge=edge, 

245 edge_interval=edge_interval, 

246 polygon=nearby_polygon, 

247 geometry_restorer=geometry_restorer 

248 ) 

249 

250 # NOTE: parts of a polygon can be apart in the netlist, e.g. the leads of a coil, 

251 # as far as no nearer polygon shields them from each other 

252 if nearest_of_same_polygon is not None and self.results.has_net_pieces(edge.property('net')) and \ 

253 (nearest_of_other_polygons is None or nearest_of_same_polygon[0] < nearest_of_other_polygons): 

254 self.emit_sidewall( 

255 layer_name=self.inside_layer_name, 

256 edge=edge, 

257 edge_interval=edge_interval, 

258 polygon=nearest_of_same_polygon[1], 

259 geometry_restorer=geometry_restorer 

260 ) 

261 

262 lateral_shield: Optional[kdb.Polygon] = None 

263 if nearest_lateral_edge is not None: 

264 lateral_shield = kdb.Polygon([ 

265 nearest_lateral_edge.p2, 

266 nearest_lateral_edge.p1, 

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

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

269 ]) 

270 

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

272 if self.inside_layer_index == child_index: 

273 continue # already handled above 

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

275 fringe_shield = kdb.Region() 

276 if lateral_shield is not None: 

277 fringe_shield.insert(lateral_shield) 

278 if child_index < self.inside_layer_index: 

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

280 for idx in r: 

281 fringe_shield += layer_fringe_shields[idx] 

282 elif self.inside_layer_index < child_index: 

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

284 for idx in r: 

285 fringe_shield += layer_fringe_shields[idx] 

286 

287 # NOTE: 

288 # polygons can have different nets 

289 # polygons can be segmented after shield is applied 

290 

291 self.emit_fringe( 

292 inside_layer_name=self.inside_layer_name, 

293 outside_layer_name=self.all_layer_names[child_index], 

294 edge=edge, 

295 edge_interval=edge_interval, 

296 outside_polygons=polygons, 

297 shield=fringe_shield, 

298 lateral_shield=lateral_shield, 

299 geometry_restorer=geometry_restorer) 

300 

301 def emit_sidewall(self, 

302 layer_name: LayerName, 

303 edge: kdb.EdgeWithProperties, 

304 edge_interval: EdgeInterval, 

305 polygon: kdb.PolygonWithProperties, 

306 geometry_restorer: GeometryRestorer): 

307 net1 = edge.property('net') 

308 net2 = polygon.property('net') 

309 

310 # NOTE: but for the parts of a net that are apart in the netlist 

311 if net1 == net2 and not self.results.has_net_pieces(net1): 

312 return 

313 

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

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

316 return 

317 

318 sidewall_cap_spec = self.tech_info.sidewall_cap_by_layer_name[layer_name] 

319 

320 # TODO! 

321 

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

323 # polygons have 4 points. 

324 # Polygons points are sorted clockwise, so the edge 

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

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

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

328 

329 # C = Csidewall * l * t / s 

330 # C = Csidewall * l / s 

331 

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

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

334 

335 outside_edge = nearest_edge(polygon) 

336 

337 length_um = avg_length * self.dbu 

338 distance_um = avg_distance * self.dbu 

339 

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

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

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

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

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

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

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

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

348 # lives here, see 

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

350 cap_femto = ((length_um * sidewall_cap_spec.capacitance) 

351 / (distance_um + sidewall_cap_spec.offset) 

352 / 2.0 # non-bidirectional (half) 

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

354 / 1000.0) # aF -> fF 

355 

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

357 

358 inside_edge = geometry_restorer.restore_edge_interval(edge_interval) 

359 

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

361 sw_cap = SidewallCap(key=swk, 

362 cap_value=cap_femto, 

363 distance=distance_um, 

364 length=length_um, 

365 tech_spec=sidewall_cap_spec, 

366 inside_edge=inside_edge, 

367 # NOTE: the part of the other net facing the edge interval 

368 outside_edge=geometry_restorer.restore_edge( 

369 kdb.Edge(kdb.Point(edge_interval[0], avg_distance), 

370 kdb.Point(edge_interval[1], avg_distance)) 

371 )) 

372 self.results.add_sidewall_cap(sw_cap) 

373 

374 self.report.output_sidewall( 

375 sidewall_cap=sw_cap, 

376 inside_edge=inside_edge, 

377 outside_edge=geometry_restorer.restore_edge(outside_edge) 

378 ) 

379 

380 def fringe_cap(self, 

381 edge_interval_length: float, 

382 distance_near: float, 

383 distance_far: float, 

384 overlap_cap_spec: CapacitanceInfo.OverlapCapacitance, 

385 sideoverlap_cap_spec: CapacitanceInfo.SideOverlapCapacitance) -> float: 

386 distance_near_um = distance_near * self.dbu 

387 distance_far_um = distance_far * self.dbu 

388 edge_interval_length_um = edge_interval_length * self.dbu 

389 

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

391 alpha_scale_factor = 0.02 * 0.01 * 0.5 * 200.0 

392 alpha_c = overlap_cap_spec.capacitance * alpha_scale_factor 

393 

394 # see Magic ExtCouple.c L1164 

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

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

397 

398 if self.scale_ratio_to_fit_halo: 

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

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

401 # but it is smaller, so we compensate 

402 if full_halo_ratio < 1.0: 

403 cnear /= full_halo_ratio 

404 cfar /= full_halo_ratio 

405 

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

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

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

409 cfrac = cfar - cnear 

410 

411 cap_femto = (cfrac * edge_interval_length_um * 

412 sideoverlap_cap_spec.capacitance / 1000.0) 

413 

414 return cap_femto 

415 

416 def is_gate_edge(self, edge: kdb.Edge) -> bool: 

417 """ 

418 Whether the edge runs along the channel of a transistor (its middle is on the channel's boundary) 

419 """ 

420 middle = edge.bbox().center() 

421 return any(shape.polygon.inside(middle) 

422 for shape in self.mos_channels.each_touching(kdb.Shapes.SAll, kdb.Box(middle, middle))) 

423 

424 def emit_fringe(self, 

425 inside_layer_name: LayerName, 

426 outside_layer_name: LayerName, 

427 edge: kdb.EdgeWithProperties, 

428 edge_interval: EdgeInterval, 

429 outside_polygons: List[kdb.PolygonWithProperties], 

430 shield: kdb.Region, 

431 lateral_shield: kdb.Polygon, 

432 geometry_restorer: GeometryRestorer): 

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

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

435 return 

436 

437 # NOTE: and the fringe of a gate edge along the channel, over the source/drain, 

438 # to the substrate or well beyond (#290) 

439 if outside_layer_name == self.tech_info.internal_substrate_layer_name and \ 

440 self.is_gate_edge(geometry_restorer.restore_edge_interval(edge_interval)): 

441 return 

442 

443 inside_net_name = edge.property('net') 

444 

445 # NOTE: each polygon in outside_polygons 

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

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

448 # each with different near/far regions 

449 

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

451 

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

453 # with the fringe from the edge of a plate to the other plate beyond it 

454 # NOTE: and the parts of a net that are apart in the netlist are like nets of their own 

455 same_net_markers = [ 

456 (inside_net_name == outside_net_name and not self.results.has_net_pieces(inside_net_name)) or 

457 self.device_capacitor_plates.is_plate_edge_of_a_device(edge, inside_net_name, outside_net_name) 

458 for outside_net_name in outside_net_names 

459 ] 

460 

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

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

463 None) 

464 if not overlap_cap_spec: 

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

466 

467 substrate_cap_spec = self.tech_info.substrate_cap_by_layer_name[inside_layer_name] 

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

469 outside_layer_name] 

470 

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

472 

473 for idx, p in enumerate(outside_polygons): 

474 outside_net = outside_net_names[idx] 

475 is_same_net = same_net_markers[idx] 

476 

477 if is_same_net: 

478 # TODO: log? 

479 continue 

480 

481 if shield.is_empty(): 

482 polygons_by_net[outside_net].append(p) 

483 else: 

484 unshielded_region = kdb.Region(p) 

485 unshielded_region.enable_properties() 

486 unshielded_region -= shield 

487 if unshielded_region.is_empty(): 

488 # TODO: log? 

489 continue 

490 

491 for up in unshielded_region.each(): 

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

493 polygons_by_net[outside_net].append(up) 

494 # if p != up: 

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

496 

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

498 for p in polygons: 

499 bbox = p.bbox() 

500 if not p.is_box(): 

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

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

503 

504 distance_near = bbox.p1.y # + 1 

505 if distance_near < 0: 

506 distance_near = 0 

507 distance_far = bbox.p2.y # - 2 

508 if distance_far < 0: 

509 distance_far = 0 

510 try: 

511 assert distance_near >= 0 

512 assert distance_far >= distance_near 

513 except AssertionError: 

514 print() 

515 raise 

516 

517 if distance_far == distance_near: 

518 return 

519 

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

521 edge_interval_length_um = edge_interval_length * self.dbu 

522 

523 cap_femto = self.fringe_cap(edge_interval_length=edge_interval_length, 

524 distance_near=distance_near, 

525 distance_far=distance_far, 

526 overlap_cap_spec=overlap_cap_spec, 

527 sideoverlap_cap_spec=sideoverlap_cap_spec) 

528 

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

530 # info(f"(Side Overlap) " 

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

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

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

534 

535 inside_edge = geometry_restorer.restore_edge_interval(edge_interval) 

536 # NOTE: the fringe ends on the polygon from its near to its far side, 

537 # so the middle of it along the edge interval stands for it 

538 distance_middle = (distance_near + distance_far) // 2 

539 

540 sok = SideOverlapKey(layer_inside=inside_layer_name, 

541 net_inside=inside_net_name, 

542 layer_outside=outside_layer_name, 

543 net_outside=outside_net_name) 

544 soc = SideOverlapCap(key=sok, 

545 cap_value=cap_femto, 

546 inside_edge=inside_edge, 

547 outside_edge=geometry_restorer.restore_edge( 

548 kdb.Edge(kdb.Point(edge_interval[0], distance_middle), 

549 kdb.Point(edge_interval[1], distance_middle)) 

550 ), 

551 near_edge=geometry_restorer.restore_edge( 

552 kdb.Edge(kdb.Point(edge_interval[0], distance_near), 

553 kdb.Point(edge_interval[1], distance_near)) 

554 )) 

555 self.results.add_sideoverlap_cap(soc) 

556 

557 self.report.output_sideoverlap( 

558 sideoverlap_cap=soc, 

559 inside_edge=inside_edge, 

560 outside_polygon=geometry_restorer.restore_polygon(p), 

561 lateral_shield=geometry_restorer.restore_polygon(lateral_shield) \ 

562 if lateral_shield is not None else None 

563 )