Coverage for klayout_pex/rcx25/exclusion.py: 99%

606 statements  

« prev     ^ index     » next       coverage.py v7.17.2, created at 2026-11-06 13:14 +0000

1# 

2# -------------------------------------------------------------------------------- 

3# SPDX-FileCopyrightText: 2024-2026 Martin Jan Köhler and Harald Pretl 

4# Johannes Kepler University, Institute for Integrated Circuits. 

5# 

6# This file is part of KPEX 

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

8# 

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

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

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

12# (at your option) any later version. 

13# 

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

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

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

17# GNU General Public License for more details. 

18# 

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

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

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

22# -------------------------------------------------------------------------------- 

23# 

24 

25""" 

26What the 2.5D extraction leaves to a model of the layout it stands for (e.g. an EM model of an inductor), 

27by exclusion markers (#401) 

28 

29NOTE: the conductors within a marker stay in the layout the capacitances are extracted from, as they shield 

30 the rest of the layout, whose capacitances neither the netlist nor the model would have without them. 

31 So only the extracted parasitics are filtered, each by the plates it is between: 

32 - both within one marker, or one within a marker and the other the substrate: left out, the model has them 

33 - both outside: unchanged 

34 - one within a marker, the other outside: kept (on the net of the plate within the marker, or on the 

35 substrate if nothing connects that net to the netlist), as the model doesn't know the conductor outside 

36 The resistance network has no resistance within a marker (see RExtractor.split_at_exclusions) 

37 

38NOTE: with ports at the cuts, the parts of a net that only a model joins are apart in the netlist, 

39 so their capacitances to each other count, unlike the ones within a net otherwise (see NetPiece) 

40 

41NOTE: a field solver only tells the capacitances between its conductors, not where they end on them, 

42 so each net is a conductor per set of blocks it is within (see ConductorPart), and the matrix filtered by these 

43""" 

44 

45from __future__ import annotations 

46 

47from collections import Counter, defaultdict 

48from dataclasses import dataclass, field 

49from enum import StrEnum 

50from fnmatch import fnmatchcase 

51import re 

52from typing import * 

53 

54import klayout.db as kdb 

55 

56from ..common.capacitance_matrix import CapacitanceMatrix 

57from ..types import GDSPair, LayerName, NetName 

58 

59import klayout_pex_protobuf.kpex.tech.tech_pb2 as tech_pb2 

60 

61 

62Interval = Tuple[float, float] # a part of an edge, by the parameter t ∈ [0, 1] along it 

63 

64 

65class ExclusionError(Exception): 

66 """ 

67 What is to be left out can't be honored 

68 """ 

69 pass 

70 

71 

72class ExclusionMarkerMode(StrEnum): 

73 """ 

74 What the 2.5D engine does with the exclusion markers of the tech info (--exclusion-markers) 

75 """ 

76 IGNORE = 'ignore' 

77 """extract what is within them, like the rest of the layout""" 

78 TIE = 'tie' 

79 """leave out the parasitics within them, each net stays connected across them, without resistance""" 

80 PORTS = 'ports' 

81 """leave out the parasitics within them, where a net enters them is a port, for the model""" 

82 DEFAULT = 'ignore' 

83 

84 

85def merged_intervals(intervals: Iterable[Interval]) -> List[Interval]: 

86 merged: List[Interval] = [] 

87 for t0, t1 in sorted(intervals): 

88 if merged and t0 <= merged[-1][1]: 

89 merged[-1] = (merged[-1][0], max(merged[-1][1], t1)) 

90 else: 

91 merged.append((t0, t1)) 

92 return merged 

93 

94 

95def intersected_intervals(intervals_a: List[Interval], intervals_b: List[Interval]) -> List[Interval]: 

96 """ 

97 :param intervals_a: merged intervals 

98 :param intervals_b: merged intervals 

99 """ 

100 intersected: List[Interval] = [] 

101 i, j = 0, 0 

102 while i < len(intervals_a) and j < len(intervals_b): 

103 t0, t1 = max(intervals_a[i][0], intervals_b[j][0]), min(intervals_a[i][1], intervals_b[j][1]) 

104 if t0 < t1: 

105 intersected.append((t0, t1)) 

106 if intervals_a[i][1] <= intervals_b[j][1]: 

107 i += 1 

108 else: 

109 j += 1 

110 return intersected 

111 

112 

113def complemented_intervals(intervals: List[Interval]) -> List[Interval]: 

114 """ 

115 :param intervals: merged intervals 

116 :return: the rest of [0, 1] 

117 """ 

118 complemented: List[Interval] = [] 

119 t = 0.0 

120 for t0, t1 in intervals: 

121 if t0 > t: 

122 complemented.append((t, t0)) 

123 t = t1 

124 if t < 1.0: 

125 complemented.append((t, 1.0)) 

126 return complemented 

127 

128 

129def intervals_within(edge: kdb.Edge, region: kdb.Region) -> List[Interval]: 

130 """ 

131 :return: the merged parts of the edge within the region (also on its boundary) 

132 """ 

133 dx, dy = edge.dx(), edge.dy() 

134 squared_length = dx * dx + dy * dy 

135 if squared_length == 0: 

136 return [] 

137 

138 def parameter(x: int, y: int) -> float: 

139 return ((x - edge.x1) * dx + (y - edge.y1) * dy) / squared_length 

140 

141 return merged_intervals(sorted((parameter(part.x1, part.y1), parameter(part.x2, part.y2))) 

142 for part in (kdb.Edges([edge]) & region).each()) 

143 

144 

145def edge_part(edge: kdb.Edge, interval: Interval) -> kdb.Edge: 

146 """ 

147 :return: the part of the edge of the interval, rounded to the grid 

148 """ 

149 def point(t: float) -> kdb.Point: 

150 return kdb.Point(round(edge.x1 + t * edge.dx()), round(edge.y1 + t * edge.dy())) 

151 return kdb.Edge(point(interval[0]), point(interval[1])) 

152 

153 

154def listed_names(names: Sequence[str], listed_count: int = 20) -> str: 

155 """ 

156 :return: the names for a message, of many only the first ones 

157 """ 

158 more = f", and {len(names) - listed_count} more" if len(names) > listed_count else '' 

159 return ', '.join(names[:listed_count]) + more 

160 

161 

162class RegionIndex: 

163 """ 

164 The polygons of regions by location: a boolean operation goes through all polygons of its operands, 

165 while a capacitance or a net only needs those near it, also of a hard block of many shapes 

166 """ 

167 

168 def __init__(self): 

169 # NOTE: with the region, as its ID would go to another one otherwise 

170 self._shapes_by_id: Dict[int, Tuple[kdb.Region, kdb.Shapes]] = {} 

171 

172 def near(self, region: kdb.Region, box: kdb.Box) -> kdb.Region: 

173 """ 

174 :param region: which must not change anymore 

175 :return: the merged polygons of the region that touch the box 

176 """ 

177 entry = self._shapes_by_id.get(id(region), None) 

178 if entry is None: 

179 shapes = kdb.Shapes() 

180 shapes.insert(region.merged()) 

181 entry = (region, shapes) 

182 self._shapes_by_id[id(region)] = entry 

183 polygons = kdb.Region() 

184 for shape in entry[1].each_touching(kdb.Shapes.SRegions, box): 

185 polygons.insert(shape.polygon) 

186 return polygons 

187 

188 

189@dataclass 

190class NetPiece: 

191 """ 

192 A part of a net with ports at exclusion markers, which its other parts are only joined to through a model: 

193 the metal outside the markers that resistors join, or the metal within one marker. 

194 

195 NOTE: by location, as a polygon can have several of them, e.g. a coil with its leads 

196 """ 

197 name: str # NOTE: stands for the net in the capacitance tables, until they go to the nodes of the piece 

198 region_by_layer: Dict[LayerName, kdb.Region] 

199 

200 

201@dataclass(frozen=True) 

202class ConductorPart: 

203 """ 

204 A part of a net that a field solver has as a conductor of its own 

205 """ 

206 net_name: NetName 

207 block_indexes: FrozenSet[int] = frozenset() # of the blocks it is within, as these can overlap 

208 layer_name: Optional[LayerName] = None # its layer, within a block that only leaves out some capacitances 

209 is_substrate: bool = False # the substrate below everything, as the net of the substrate can have metal as well 

210 

211 

212@dataclass(frozen=True) 

213class MarkerLayer: 

214 """ 

215 A layer of the layout that marks what the extraction leaves out 

216 """ 

217 name: str # NOTE: for messages 

218 gds_pair: GDSPair 

219 layer_names: FrozenSet[LayerName] = frozenset() # the layers of the stack it applies to, all if empty 

220 is_capacitance_pair: bool = False # it only leaves out the capacitances between its two layers 

221 

222 @classmethod 

223 def parse(cls, 

224 specification: str, 

225 tech_markers: Mapping[str, MarkerLayer], 

226 stack_layer_names: Collection[LayerName]) -> MarkerLayer: 

227 """ 

228 :param specification: the name of a marker of the tech info, or a GDS pair with the optional layers 

229 of the stack it applies to, e.g. 235/5 or 235/5:met2,met3 

230 :param tech_markers: the markers of the tech info by name 

231 :param stack_layer_names: the layers of the stack 

232 """ 

233 if specification in tech_markers: 

234 return tech_markers[specification] 

235 match = re.fullmatch(r'(\d+)/(\d+)(?::(.+))?', specification) 

236 if match is None: 

237 raise ExclusionError(f"The exclusion marker layer {specification} is neither L/D or L/D:layer,layer " 

238 f"nor a marker of the tech info: {', '.join(tech_markers) or 'it has none'}") 

239 layer_names = frozenset(match.group(3).split(',')) if match.group(3) else frozenset() 

240 unknown_layer_names = sorted(layer_names - set(stack_layer_names)) 

241 if unknown_layer_names: 

242 raise ExclusionError(f"The exclusion marker layer {specification} names layers the stack doesn't have: " 

243 f"{', '.join(unknown_layer_names)}, it has: {', '.join(sorted(stack_layer_names))}") 

244 return cls(name=specification, gds_pair=(int(match.group(1)), int(match.group(2))), layer_names=layer_names) 

245 

246 

247def selected_marker_layers(tech_markers: Iterable[Any], 

248 stack_layer_names: Collection[LayerName], 

249 specifications: Optional[Sequence[str]]) -> List[MarkerLayer]: 

250 """ 

251 :param tech_markers: the markers of the tech info (ExclusionMarker) 

252 :param stack_layer_names: the layers of the stack 

253 :param specifications: of the marker layers (see MarkerLayer.parse), by default the markers on by default 

254 :raises ExclusionError: if one is invalid 

255 """ 

256 markers = list(tech_markers) 

257 marker_layers = {m.name: MarkerLayer(name=m.name, 

258 gds_pair=(m.gds_pair.layer, m.gds_pair.datatype), 

259 layer_names=frozenset(m.layer_names), 

260 is_capacitance_pair=m.is_capacitance_pair) 

261 for m in markers} 

262 if not specifications: 

263 return [marker_layers[m.name] for m in markers if not m.on_request] 

264 return [MarkerLayer.parse(specification, marker_layers, stack_layer_names) for specification in specifications] 

265 

266 

267@dataclass(frozen=True) 

268class CutRequest: 

269 """ 

270 The instances of the cells to leave out of the extraction, for models of them (kpex --cut) 

271 """ 

272 cell_pattern: str # a name of a cell, or a glob pattern of names 

273 prefix: Optional[str] = None # of the names of the ports, by default the name of the cell and a number 

274 

275 @property 

276 def specification(self) -> str: 

277 return self.cell_pattern if self.prefix is None else f"{self.cell_pattern}={self.prefix}" 

278 

279 @classmethod 

280 def parse(cls, specification: str) -> CutRequest: 

281 """ 

282 :param specification: CELL or CELL=PREFIX 

283 """ 

284 cell_pattern, separator, prefix = specification.partition('=') 

285 if not cell_pattern or (separator and not prefix): 

286 raise ExclusionError(f"--cut {specification}: expected CELL or CELL=PREFIX") 

287 if prefix and re.fullmatch(r'[A-Za-z0-9_]+', prefix) is None: 

288 raise ExclusionError(f"--cut {specification}: the prefix names ports of the netlist, " 

289 f"so it may only have letters, digits and _") 

290 return cls(cell_pattern, prefix or None) 

291 

292 

293def parsed_cut_requests(specifications: Iterable[str]) -> List[CutRequest]: 

294 """ 

295 :raises ExclusionError: if one is invalid, or two have the same prefix, whose ports would be one node 

296 """ 

297 requests = [CutRequest.parse(specification) for specification in specifications] 

298 prefixes = Counter(request.prefix for request in requests if request.prefix is not None) 

299 duplicates = sorted(prefix for prefix, count in prefixes.items() if count > 1) 

300 if duplicates: 

301 raise ExclusionError(f"--cut: each one needs a prefix of its own, given several times: {', '.join(duplicates)}") 

302 return requests 

303 

304 

305@dataclass(frozen=True) 

306class CutLayers: 

307 """ 

308 Which shapes of a cell are on which layers of the stack 

309 """ 

310 # the drawn layers of the conductors with their fill, by layer of the stack: the own one (metal, wells), 

311 # or the diffusion for what LVS computes from it (e.g. the source/drain), as its implants overhang 

312 gds_pairs_by_layer: Mapping[LayerName, Collection[GDSPair]] 

313 # the labels of the metal, by layer of the stack 

314 label_gds_pairs: Mapping[LayerName, GDSPair] 

315 diffusion_gds_pairs: Collection[GDSPair] 

316 

317 @classmethod 

318 def of_tech(cls, tech_info: Any) -> CutLayers: 

319 """ 

320 :param tech_info: the TechInfo 

321 """ 

322 def pair(gds_pair: Any) -> GDSPair: 

323 return gds_pair.layer, gds_pair.datatype 

324 

325 def drawn_gds_pairs(layer: Any) -> Tuple[GDSPair, ...]: 

326 return (pair(layer.drw_gds_pair), pair(layer.fill_gds_pair)) if layer.HasField('fill_gds_pair') \ 

327 else (pair(layer.drw_gds_pair),) 

328 

329 LP = tech_pb2.LayerInfo.Purpose 

330 layers = list(tech_info.tech.layers) 

331 diffusion_gds_pairs = {gds_pair for layer in layers 

332 if layer.purpose in (LP.PURPOSE_DIFF, LP.PURPOSE_NTAP_OR_PTAP) 

333 for gds_pair in drawn_gds_pairs(layer)} 

334 gds_pairs_by_layer: Dict[LayerName, Collection[GDSPair]] = { 

335 layer_name: diffusion_gds_pairs for layer_name in tech_info.process_diffusion_layer_names 

336 } 

337 gds_pairs_by_layer.update({layer.name: drawn_gds_pairs(layer) for layer in layers 

338 if layer.purpose in (LP.PURPOSE_METAL, LP.PURPOSE_MIM_CAP, LP.PURPOSE_NWELL, 

339 LP.PURPOSE_PWELL, LP.PURPOSE_DNWELL)}) 

340 return cls(gds_pairs_by_layer=gds_pairs_by_layer, 

341 label_gds_pairs={layer.name: pair(layer.label_gds_pair) for layer in layers 

342 if layer.purpose in (LP.PURPOSE_METAL, LP.PURPOSE_MIM_CAP) 

343 and layer.HasField('label_gds_pair')}, 

344 diffusion_gds_pairs=diffusion_gds_pairs) 

345 

346 

347@dataclass(frozen=True) 

348class BlockLabel: 

349 text: str 

350 position: kdb.Point 

351 layer_name: LayerName 

352 

353 

354class BlockKind(StrEnum): 

355 MARKER = 'marker' 

356 CUT = 'cut' 

357 

358 

359@dataclass(frozen=True) 

360class CutCellInstance: 

361 """ 

362 An instance of a cut cell in the layout, which tells who draws what is at a place of it 

363 """ 

364 layout: kdb.Layout 

365 top_cell: kdb.Cell 

366 cell: kdb.Cell 

367 top_trans: kdb.ICplxTrans # of the top cell, into the coordinates of the blocks 

368 cell_trans: kdb.ICplxTrans # of the instance, into the coordinates of the blocks 

369 

370 @staticmethod 

371 def _shapes_of(cell: kdb.Cell, trans: kdb.ICplxTrans, layer_index: int, box: kdb.Box) -> kdb.Region: 

372 return kdb.Region(cell.begin_shapes_rec_touching(layer_index, trans.inverted() * box)).transformed(trans) 

373 

374 def shapes_of_layout(self, gds_pairs: Iterable[GDSPair], box: kdb.Box) -> kdb.Region: 

375 """ 

376 :param box: in the coordinates of the blocks 

377 :return: all shapes of the layout on the layers that touch the box 

378 """ 

379 shapes = kdb.Region() 

380 for gds_pair in gds_pairs: 

381 layer_index = self.layout.find_layer(*gds_pair) 

382 if layer_index is not None: 

383 shapes += self._shapes_of(self.top_cell, self.top_trans, layer_index, box) 

384 return shapes 

385 

386 def draws_device(self, gds_pairs: Iterable[GDSPair], terminals: Sequence[kdb.Region]) -> bool: 

387 """ 

388 :param gds_pairs: the layers that LVS makes the device from (see TechInfo.device_gds_pairs) 

389 :param terminals: the shapes of its terminals, in the coordinates of the blocks 

390 :return: whether the cell draws the device, on each of these layers that is at its terminals: a polygon at all 

391 of them, unless only the rest of the layout has one, or it has one over more of them (e.g. a tap 

392 that overlaps the one of a neighbor), as a terminal can be on a wire that goes on to another device 

393 (a diffusion can also go on beyond the cell), or else at each of them, if nothing is at all of them 

394 (e.g. the diffusions of a BJT) 

395 

396 NOTE: a polygon of the cell is apart from one of the rest of the layout that it abuts (e.g. the taps at a cell, 

397 the fingers of a transistor). At a terminal are the polygons that overlap it, or else those that touch it, 

398 as the terminals of a resistor abut its marker 

399 """ 

400 box = kdb.Box() 

401 for shapes in terminals: 

402 box += shapes.bbox() 

403 

404 def polygons_at(shapes: kdb.Region) -> Tuple[kdb.Region, kdb.Region]: 

405 cell_at, rest_at = cell_polygons.overlapping(shapes), rest_polygons.overlapping(shapes) 

406 if cell_at.is_empty() and rest_at.is_empty(): 

407 return cell_polygons.interacting(shapes), rest_polygons.interacting(shapes) 

408 return cell_at, rest_at 

409 

410 draws_any = False 

411 for gds_pair in gds_pairs: 

412 layer_index = self.layout.find_layer(*gds_pair) 

413 if layer_index is None: 

414 continue 

415 cell_polygons = self._shapes_of(self.cell, self.cell_trans, layer_index, box).merged() 

416 rest_polygons = (self._shapes_of(self.top_cell, self.top_trans, layer_index, box) - cell_polygons).merged() 

417 at_terminals = [(shapes, *polygons_at(shapes)) for shapes in terminals] 

418 at_terminals = [at for at in at_terminals if not (at[1].is_empty() and at[2].is_empty())] 

419 if not at_terminals: 

420 continue 

421 draws_any = True 

422 cell_at_all, rest_at_all = at_terminals[0][1:] 

423 for _, cell_at, rest_at in at_terminals[1:]: 

424 cell_at_all, rest_at_all = cell_at_all & cell_at, rest_at_all & rest_at 

425 if cell_at_all.is_empty(): 

426 if not rest_at_all.is_empty() or any(cell_at.is_empty() for _, cell_at, _ in at_terminals): 

427 return False 

428 elif sum((rest_at_all & shapes).area() for shapes, _, _ in at_terminals) > \ 

429 sum((cell_at_all & shapes).area() for shapes, _, _ in at_terminals): 

430 return False 

431 return draws_any 

432 

433 

434@dataclass 

435class ExcludedBlock: 

436 """ 

437 What one model stands for: a capacitance between plates within different blocks stays, as neither model has it 

438 """ 

439 name: str # NOTE: for the names of ports 

440 region: kdb.Region # NOTE: all of its shapes, or the same on each layer if it has no layer_regions 

441 layer_regions: Optional[Dict[LayerName, kdb.Region]] = None # its shapes by layer, it isn't on other layers 

442 capacitance_pair: Optional[FrozenSet[LayerName]] = None # it only leaves out the capacitances between these layers 

443 ports: bool = False # where a net enters it is a port, otherwise it stays connected across it 

444 kind: BlockKind = BlockKind.MARKER 

445 labels: List[BlockLabel] = field(default_factory=list) # of a cut cell, which name the ports 

446 cell_instance: Optional[CutCellInstance] = None # of a cut cell, which tells its devices 

447 

448 def region_of(self, layer_name: LayerName) -> Optional[kdb.Region]: 

449 return self.region if self.layer_regions is None else self.layer_regions.get(layer_name, None) 

450 

451 def applies_between(self, layer_name_a: LayerName, layer_name_b: LayerName) -> bool: 

452 return self.capacitance_pair is None or self.capacitance_pair == {layer_name_a, layer_name_b} 

453 

454 

455def polygons_by_center_inside(shapes: kdb.Region, mask: kdb.Region) -> kdb.Region: 

456 """ 

457 :return: the merged polygons of the shapes with the center of their bounding boxes strictly inside the mask 

458 """ 

459 polygons_by_center_box: Dict[kdb.Box, List[kdb.Polygon]] = defaultdict(list) 

460 center_boxes = kdb.Region() 

461 center_boxes.merged_semantics = False # NOTE: each box on its own 

462 for polygon in shapes.merged().each(): 

463 center = polygon.bbox().center() 

464 box = kdb.Box(center, center).enlarged(1, 1) 

465 polygons_by_center_box[box].append(polygon.downcast()) 

466 center_boxes.insert(box) 

467 within = kdb.Region() 

468 for box in center_boxes.inside(mask).each(): 

469 for polygon in polygons_by_center_box[box.bbox()]: 

470 within.insert(polygon) 

471 return within 

472 

473 

474def cells_called_by(layout: kdb.Layout, cell: kdb.Cell) -> List[str]: 

475 """ 

476 :return: the names of the cells below the cell, sorted 

477 """ 

478 return sorted(layout.cell(index).name for index in cell.called_cells()) 

479 

480 

481def matches_name(name: str, pattern: str) -> bool: 

482 """ 

483 :return: whether the name is the pattern (e.g. data[1], whose brackets a pattern takes as a set), or matches it 

484 """ 

485 return name == pattern or fnmatchcase(name, pattern) 

486 

487 

488def instances_of_cut_cells(layout: kdb.Layout, 

489 top_cell: kdb.Cell, 

490 cut_requests: Sequence[CutRequest], 

491 label_gds_pairs: Iterable[GDSPair], 

492 scale: kdb.ICplxTrans) -> Tuple[Dict[CutRequest, List[Tuple[kdb.Cell, kdb.ICplxTrans]]], 

493 Set[Tuple[str, kdb.Point]]]: 

494 """ 

495 :param label_gds_pairs: of the labels of the metal 

496 :param scale: to the coordinates of the blocks 

497 :return: the instances of the cells of each request, sorted, but those within another one of them, 

498 and the labels of the rest of the layout on the metal, by their texts and positions 

499 :raises ExclusionError: if one isn't at a multiple of 90°, or something below it 

500 """ 

501 instances_by_request: Dict[CutRequest, List[Tuple[kdb.Cell, kdb.ICplxTrans]]] = {r: [] for r in cut_requests} 

502 label_layer_indexes = [layer_index for layer_index in (layout.find_layer(*gds_pair) for gds_pair in label_gds_pairs) 

503 if layer_index is not None] 

504 own_labels_by_cell: Dict[int, List[Tuple[str, kdb.Point]]] = {} 

505 labels_of_rest: Set[Tuple[str, kdb.Point]] = set() 

506 orientations_checked: Set[Tuple[int, float]] = set() 

507 

508 def any_angle_instance_at(cell: kdb.Cell, trans: kdb.ICplxTrans) -> Optional[Tuple[kdb.Cell, float]]: 

509 """ 

510 :return: the cell of an instance at or below this one that isn't at a multiple of 90°, with its angle 

511 """ 

512 if not trans.is_ortho(): 

513 return cell, trans.angle 

514 if (cell.cell_index(), trans.angle) not in orientations_checked: 

515 orientations_checked.add((cell.cell_index(), trans.angle)) 

516 for instance in cell.each_inst(): 

517 found = any_angle_instance_at(layout.cell(instance.cell_index), trans * instance.cplx_trans) 

518 if found is not None: 

519 return found 

520 return None 

521 

522 def own_labels(cell: kdb.Cell) -> List[Tuple[str, kdb.Point]]: 

523 labels = own_labels_by_cell.get(cell.cell_index(), None) 

524 if labels is None: 

525 labels = [(shape.text.string, kdb.Point(shape.text.x, shape.text.y)) 

526 for layer_index in label_layer_indexes 

527 for shape in cell.shapes(layer_index).each(kdb.Shapes.STexts)] 

528 own_labels_by_cell[cell.cell_index()] = labels 

529 return labels 

530 

531 def visit(cell: kdb.Cell, trans: kdb.ICplxTrans): 

532 labels_of_rest.update((text, trans * position) for text, position in own_labels(cell)) 

533 for instance in cell.each_inst(): 

534 child = layout.cell(instance.cell_index) 

535 request = next((r for r in cut_requests if matches_name(child.name, r.cell_pattern)), None) 

536 for instance_trans in instance.cell_inst.each_cplx_trans(): 

537 child_trans = trans * instance_trans 

538 if request is not None: 

539 # NOTE: of such an instance, LVS only has the bounding boxes of the shapes 

540 any_angle_instance = any_angle_instance_at(child, child_trans) 

541 if any_angle_instance is not None: 

542 raise ExclusionError(f"--cut {request.cell_pattern}: an instance of the cell " 

543 f"{any_angle_instance[0].name} has an angle of " 

544 f"{any_angle_instance[1]:.6g}°, not a multiple of 90°") 

545 instances_by_request[request].append((child, child_trans)) 

546 else: 

547 visit(child, child_trans) 

548 

549 visit(top_cell, scale) 

550 for instances in instances_by_request.values(): 

551 instances.sort(key=lambda i: (i[0].name, (i[1] * i[0].bbox()).bottom, (i[1] * i[0].bbox()).left)) 

552 return instances_by_request, labels_of_rest 

553 

554 

555def names_of_blocks_of_cut_cells(instances_by_request: Mapping[CutRequest, Sequence[Tuple[kdb.Cell, kdb.ICplxTrans]]]) \ 

556 -> Dict[CutRequest, List[str]]: 

557 """ 

558 :param instances_by_request: see instances_of_cut_cells 

559 :return: the names of the blocks of the instances, in their order 

560 :raises ExclusionError: if several instances would have one name 

561 """ 

562 names_by_request: Dict[CutRequest, List[str]] = {} 

563 instance_counts: Counter[str] = Counter() 

564 for request, instances in instances_by_request.items(): 

565 names_by_request[request] = [] 

566 for index, (cell, _) in enumerate(instances): 

567 if request.prefix is None: 

568 # NOTE: per cell, so that another cell of a pattern doesn't rename its ports 

569 safe_cell_name = re.sub(r'[^A-Za-z0-9_]', '_', cell.name) 

570 name = f"{safe_cell_name}_{instance_counts[safe_cell_name]}" 

571 instance_counts[safe_cell_name] += 1 

572 else: 

573 name = request.prefix if len(instances) == 1 else f"{request.prefix}_{index}" 

574 names_by_request[request].append(name) 

575 

576 names = Counter(name for request_names in names_by_request.values() for name in request_names) 

577 duplicates = sorted(name for name, count in names.items() if count > 1) 

578 if duplicates: 

579 raise ExclusionError(f"--cut: several instances would have these names, whose ports would be one node, " 

580 f"as the numbers of the instances of a cell follow its prefix: {', '.join(duplicates)}") 

581 return names_by_request 

582 

583 

584def check_instances_of_cut_cells(layout: kdb.Layout, top_cell: kdb.Cell, cut_requests: Sequence[CutRequest]): 

585 """ 

586 :raises ExclusionError: see instances_of_cut_cells, names_of_blocks_of_cut_cells, 

587 or if a request has no instances, as those of its cells are of another request, 

588 or within another cut cell 

589 """ 

590 instances_by_request, _ = instances_of_cut_cells(layout, top_cell, cut_requests, (), kdb.ICplxTrans()) 

591 names_of_blocks_of_cut_cells(instances_by_request) 

592 for request, instances in instances_by_request.items(): 

593 if not instances: 

594 raise ExclusionError(f"--cut {request.specification}: all instances of its cells are of another --cut " 

595 f"before it, or within a cell of one") 

596 

597 

598def blocks_of_cut_cells(layout: kdb.Layout, 

599 top_cell: kdb.Cell, 

600 cut_requests: Sequence[CutRequest], 

601 cut_layers: CutLayers, 

602 scale: kdb.ICplxTrans) -> Tuple[List[ExcludedBlock], Set[Tuple[str, kdb.Point]]]: 

603 """ 

604 :return: a block of each instance of the cells, but those within another one of them, 

605 and the labels of the rest of the layout on the metal, by their texts and positions 

606 """ 

607 instances_by_request, labels_of_rest = instances_of_cut_cells(layout, top_cell, cut_requests, 

608 cut_layers.label_gds_pairs.values(), scale) 

609 names_by_request = names_of_blocks_of_cut_cells(instances_by_request) 

610 

611 def shapes_of(cell: kdb.Cell, gds_pairs: Iterable[GDSPair]) -> kdb.Region: 

612 shapes = kdb.Region() 

613 for gds_pair in gds_pairs: 

614 layer_index = layout.find_layer(*gds_pair) 

615 if layer_index is not None: 

616 shapes += kdb.Region(cell.begin_shapes_rec(layer_index)) 

617 return shapes.merged() 

618 

619 def labels_of(cell: kdb.Cell) -> List[BlockLabel]: 

620 labels: List[BlockLabel] = [] 

621 for layer_name, gds_pair in cut_layers.label_gds_pairs.items(): 

622 layer_index = layout.find_layer(*gds_pair) 

623 if layer_index is None: 

624 continue 

625 shape_iterator = cell.begin_shapes_rec(layer_index) 

626 while not shape_iterator.at_end(): 

627 shape = shape_iterator.shape() 

628 if shape.is_text(): 

629 text = shape.text.transformed(shape_iterator.trans()) 

630 labels.append(BlockLabel(text.string, kdb.Point(text.x, text.y), layer_name)) 

631 shape_iterator.next() 

632 return labels 

633 

634 blocks: List[ExcludedBlock] = [] 

635 for request, instances in instances_by_request.items(): 

636 for name, (cell, trans) in zip(names_by_request[request], instances): 

637 # NOTE: one region for the layers of the same shapes, as a region of a layer goes by its identity 

638 regions_by_gds_pairs: Dict[FrozenSet[GDSPair], kdb.Region] = {} 

639 layer_regions: Dict[LayerName, kdb.Region] = {} 

640 for layer_name, gds_pairs in cut_layers.gds_pairs_by_layer.items(): 

641 key = frozenset(gds_pairs) 

642 if key not in regions_by_gds_pairs: 

643 regions_by_gds_pairs[key] = shapes_of(cell, gds_pairs).transformed(trans) 

644 layer_regions[layer_name] = regions_by_gds_pairs[key] 

645 region = kdb.Region() 

646 for layer_region in regions_by_gds_pairs.values(): 

647 region += layer_region 

648 

649 labels = [BlockLabel(label.text, trans * label.position, label.layer_name) for label in labels_of(cell)] 

650 blocks.append(ExcludedBlock(name=name, region=region.merged(), layer_regions=layer_regions, ports=True, 

651 kind=BlockKind.CUT, labels=labels, 

652 cell_instance=CutCellInstance(layout, top_cell, cell, scale, trans))) 

653 return blocks, labels_of_rest 

654 

655 

656class Exclusions: 

657 """ 

658 What the extraction leaves out of a layout, as blocks that models stand for 

659 """ 

660 

661 def __init__(self, blocks: List[ExcludedBlock], substrate_layer_name: LayerName = 'VSUBS'): 

662 """ 

663 :param blocks: in the coordinates of the top cell, in DBU of the extraction 

664 :param substrate_layer_name: of the capacitances, the model of a block has the substrate below all its plates 

665 """ 

666 self.blocks = blocks 

667 self.substrate_layer_name = substrate_layer_name 

668 self.bbox = kdb.Box() 

669 for block in blocks: 

670 self.bbox += block.region.bbox() 

671 

672 # NOTE: found by their boxes, as each capacitance looks them up 

673 self.block_boxes = kdb.Shapes() 

674 for index, block in enumerate(blocks): 

675 self.block_boxes.insert(kdb.BoxWithProperties(block.region.bbox(), {'block': index})) 

676 

677 self._masks: Dict[Tuple[LayerName, Optional[bool], Optional[BlockKind]], kdb.Region] = {} 

678 self._kept_shapes: Dict[LayerName, kdb.Region] = defaultdict(kdb.Region) # see without_shapes 

679 self._label_positions: Dict[Tuple[int, LayerName], kdb.Shapes] = {} # see labels_at 

680 self.regions = RegionIndex() 

681 

682 # the nets within the blocks that nothing connects to the netlist (see net_of_capacitance) 

683 self.floating_net_names: Set[NetName] = set() 

684 

685 # the pieces of the nets with ports at the blocks, by the resistance extraction 

686 self.pieces_by_net: Dict[NetName, List[NetPiece]] = {} 

687 

688 # the devices the netlist leaves out by their IDs, with the indexes of their blocks (of cut cells) 

689 self.removed_device_blocks: Dict[int, int] = {} 

690 

691 # the nets without resistance, and without capacitance to the substrate (kpex --exclude-net) 

692 # NOTE: no blocks, as all capacitances between nets stay, and a block of a power grid would cost each of them 

693 self.excluded_net_names: Set[NetName] = set() 

694 

695 # the labels of the layout on the metal, of the cut cells and outside them, by their texts and positions 

696 self.labels_of_cut_cells: Set[Tuple[str, kdb.Point]] = {(label.text, label.position) 

697 for block in blocks for label in block.labels} 

698 self.labels_of_rest: Set[Tuple[str, kdb.Point]] = set() 

699 

700 # the parts of the nets the field solvers have as conductors, by the names of the conductors 

701 self.conductor_parts: Dict[str, ConductorPart] = {} 

702 self.conductor_names: Dict[ConductorPart, str] = {} 

703 

704 @classmethod 

705 def from_region(cls, 

706 region: kdb.Region, 

707 ports: bool = False, 

708 substrate_layer_name: LayerName = 'VSUBS') -> Exclusions: 

709 """ 

710 :param region: shapes that mark all layers of the stack, each merged polygon a block 

711 """ 

712 return cls(cls.blocks_of_marker_region(region, MarkerLayer('', (0, 0)), ports, first_index=0), 

713 substrate_layer_name) 

714 

715 @staticmethod 

716 def blocks_of_marker_region(region: kdb.Region, 

717 marker_layer: MarkerLayer, 

718 ports: bool, 

719 first_index: int) -> List[ExcludedBlock]: 

720 """ 

721 :param region: the shapes of the marker layer 

722 :param first_index: of the first block, as the names of the blocks number them 

723 :return: a block of each merged polygon 

724 """ 

725 blocks: List[ExcludedBlock] = [] 

726 for index, polygon in enumerate(region.merged().each(), start=first_index): 

727 block_region = kdb.Region(polygon.downcast()) 

728 layer_regions = {layer_name: block_region for layer_name in marker_layer.layer_names} \ 

729 if marker_layer.layer_names else None 

730 blocks.append(ExcludedBlock(name=f"m{index}", 

731 region=block_region, 

732 layer_regions=layer_regions, 

733 capacitance_pair=marker_layer.layer_names 

734 if marker_layer.is_capacitance_pair else None, 

735 ports=ports and not marker_layer.is_capacitance_pair)) 

736 return blocks 

737 

738 @classmethod 

739 def from_layout(cls, 

740 layout: kdb.Layout, 

741 cell_name: str, 

742 marker_layers: Iterable[MarkerLayer], 

743 dbu: float, 

744 ports: bool = False, 

745 substrate_layer_name: LayerName = 'VSUBS', 

746 cut_requests: Sequence[CutRequest] = (), 

747 cut_layers: Optional[CutLayers] = None) -> Exclusions: 

748 """ 

749 :param layout: the layout the LVS database is of 

750 :param cell_name: the top cell of the extraction 

751 :param marker_layers: the marker layers 

752 :param dbu: the database unit of the extraction 

753 :param ports: where a net enters a marker is a port, otherwise it stays connected across the marker 

754 :param substrate_layer_name: see __init__ 

755 :param cut_requests: the cells to leave out, with ports where a net enters them 

756 :param cut_layers: which shapes of a cell are on which layers, for the cut requests 

757 """ 

758 cell = layout.cell(cell_name) 

759 scale = kdb.ICplxTrans(layout.dbu / dbu) 

760 blocks: List[ExcludedBlock] = [] 

761 for marker_layer in marker_layers: 

762 layer_index = layout.find_layer(*marker_layer.gds_pair) 

763 if layer_index is not None: 

764 region = kdb.Region(cell.begin_shapes_rec(layer_index)).transformed(scale) 

765 blocks.extend(cls.blocks_of_marker_region(region, marker_layer, ports, first_index=len(blocks))) 

766 labels_of_rest: Set[Tuple[str, kdb.Point]] = set() 

767 if cut_requests: 

768 blocks_of_cells, labels_of_rest = blocks_of_cut_cells(layout, cell, cut_requests, cut_layers, scale) 

769 blocks.extend(blocks_of_cells) 

770 exclusions = cls(blocks, substrate_layer_name) 

771 exclusions.labels_of_rest = labels_of_rest 

772 return exclusions 

773 

774 def without_shapes(self, shapes_by_layer: Mapping[LayerName, kdb.Region]): 

775 """ 

776 Takes shapes out of the cut cells (kpex --cut-keep-net), so that they are extracted like the rest of the layout 

777 

778 :param shapes_by_layer: the shapes by layer of the stack 

779 """ 

780 for layer_name, shapes in shapes_by_layer.items(): 

781 self._kept_shapes[layer_name] += shapes 

782 for block in self.blocks: 

783 if block.kind == BlockKind.CUT: 

784 for layer_name, shapes in shapes_by_layer.items(): 

785 region = block.region_of(layer_name) 

786 if region is not None: 

787 block.layer_regions[layer_name] = (region - shapes).merged() 

788 self._masks.clear() 

789 

790 def add_to_cut_cell(self, block_index: int, layer_name: LayerName, shapes: kdb.Region): 

791 """ 

792 :param shapes: what the cell of the block stands for beyond what it draws on the layer of the stack 

793 """ 

794 shapes = shapes - self._kept_shapes[layer_name] 

795 if shapes.is_empty(): 

796 return 

797 block = self.blocks[block_index] 

798 block.layer_regions[layer_name] = (block.layer_regions[layer_name] + shapes).merged() 

799 block.region = (block.region + shapes).merged() 

800 self.bbox += shapes.bbox() 

801 self.block_boxes.insert(kdb.BoxWithProperties(shapes.bbox(), {'block': block_index})) 

802 self._masks.clear() 

803 

804 def conductor_name(self, part: ConductorPart) -> str: 

805 """ 

806 :return: the name of the conductor of the part, the name of the net for its part outside of the blocks, 

807 unless that is the name of another conductor, as a label can have any text 

808 """ 

809 name = self.conductor_names.get(part, None) 

810 if name is not None: 

811 return name 

812 if part.is_substrate: 

813 name = f"{part.net_name}#substrate" 

814 elif not part.block_indexes: 

815 name = part.net_name 

816 else: 

817 block_indexes = '_'.join(str(index) for index in sorted(part.block_indexes)) 

818 name = f"{part.net_name}#x{block_indexes}" 

819 if part.layer_name is not None: 

820 name = f"{name}_{part.layer_name}" 

821 while name in self.conductor_parts: 

822 name = f"{name}#" 

823 self.conductor_parts[name] = part 

824 self.conductor_names[part] = name 

825 return name 

826 

827 def conductor_parts_of_shapes(self, 

828 shapes: kdb.Region, 

829 net_name: NetName, 

830 layer_name: LayerName) -> List[Tuple[kdb.Region, str]]: 

831 """ 

832 :param shapes: of the net on the conductor layer of the stack 

833 :return: the parts of the shapes with the names of their conductors 

834 """ 

835 def part_within(rest: kdb.Region, index: int) -> Optional[Tuple[kdb.Region, kdb.Region, Optional[LayerName]]]: 

836 block = self.blocks[index] 

837 region = block.region_of(layer_name) 

838 if region is None: 

839 return None 

840 near = self.regions.near(region, rest.bbox()) 

841 return rest & near, near, None if block.capacitance_pair is None else layer_name 

842 

843 return self.parts_within_blocks(shapes, net_name, part_within) 

844 

845 def conductor_parts_of_vias(self, 

846 shapes: kdb.Region, 

847 net_name: NetName, 

848 bottom_layer_name: LayerName, 

849 top_layer_name: LayerName) -> List[Tuple[kdb.Region, str]]: 

850 """ 

851 :param shapes: of the net on the via layer of the stack 

852 :return: the parts of the shapes with the names of their conductors: within a block by their centers, 

853 as for the resistances (see RExtractor.split_at_exclusions) 

854 """ 

855 def part_within(rest: kdb.Region, index: int) -> Optional[Tuple[kdb.Region, kdb.Region, Optional[LayerName]]]: 

856 block = self.blocks[index] 

857 bottom_region, top_region = block.region_of(bottom_layer_name), block.region_of(top_layer_name) 

858 if bottom_region is None or top_region is None or block.capacitance_pair is not None: 

859 return None 

860 box = rest.bbox() 

861 mask = self.regions.near(bottom_region, box) & self.regions.near(top_region, box) 

862 part = polygons_by_center_inside(rest, mask) 

863 return part, part, None 

864 

865 return self.parts_within_blocks(shapes, net_name, part_within) 

866 

867 def parts_within_blocks(self, 

868 shapes: kdb.Region, 

869 net_name: NetName, 

870 part_within: Callable[[kdb.Region, int], 

871 Optional[Tuple[kdb.Region, kdb.Region, Optional[LayerName]]]]) \ 

872 -> List[Tuple[kdb.Region, str]]: 

873 """ 

874 :param shapes: of the net 

875 :param part_within: what of some shapes is within the block of the index, what to take out of the shapes 

876 for what isn't, and the layer that the block needs a part to have its name 

877 :return: the parts of the shapes, each within other blocks, with the names of their conductors 

878 

879 NOTE: the rest goes without the mask rather than the part, as any-angle edges round differently, which leaves 

880 slivers along the cut otherwise, that join what is on both sides of a block 

881 """ 

882 parts: List[Tuple[kdb.Region, FrozenSet[int], Optional[LayerName]]] = [(shapes, frozenset(), None)] 

883 for index in self.block_indexes_touching(shapes.bbox()): 

884 split_parts: List[Tuple[kdb.Region, FrozenSet[int], Optional[LayerName]]] = [] 

885 for region, block_indexes, part_layer_name in parts: 

886 within = part_within(region, index) 

887 if within is None or within[0].is_empty(): 

888 split_parts.append((region, block_indexes, part_layer_name)) 

889 continue 

890 split_parts.append((within[0], block_indexes | {index}, part_layer_name or within[2])) 

891 rest = region - within[1] 

892 if not rest.is_empty(): 

893 split_parts.append((rest, block_indexes, part_layer_name)) 

894 parts = split_parts 

895 return [(region, self.conductor_name(ConductorPart(net_name, block_indexes, part_layer_name))) 

896 for region, block_indexes, part_layer_name in parts] 

897 

898 def leaves_out_to_substrate(self, part: ConductorPart) -> bool: 

899 """ 

900 :return: whether the capacitance of the part to the substrate is left out 

901 """ 

902 if part.net_name in self.excluded_net_names: 

903 return True 

904 return any(self.blocks[index].applies_between(part.layer_name, self.substrate_layer_name) 

905 for index in part.block_indexes) 

906 

907 def leaves_out_between(self, part_a: ConductorPart, part_b: ConductorPart) -> bool: 

908 """ 

909 :return: whether the capacitance between the parts is left out 

910 """ 

911 if part_a.is_substrate or part_b.is_substrate: 

912 return self.leaves_out_to_substrate(part_b if part_a.is_substrate else part_a) 

913 return any(self.blocks[index].applies_between(part_a.layer_name, part_b.layer_name) 

914 for index in part_a.block_indexes & part_b.block_indexes) 

915 

916 def capacitance_matrix_of_nets(self, 

917 matrix: CapacitanceMatrix, 

918 conductor_names: List[str], 

919 substrate_net_name: NetName) -> CapacitanceMatrix: 

920 """ 

921 :param matrix: the Maxwell capacitance matrix of the conductors, whose capacitances to infinity 

922 go to the substrate, as in the netlist 

923 :param conductor_names: of the axes of the matrix, as in conductor_name 

924 :param substrate_net_name: the net of the capacitances to the substrate 

925 :return: the Maxwell capacitance matrix of the nets, by their names, without what is left out, 

926 the capacitances of the parts of a net summed up: with all at the same potential, 

927 the charge they induce adds up to the one of the net as one conductor 

928 """ 

929 parts = [self.conductor_parts[name] for name in conductor_names] 

930 nets_of_parts = [self.net_of_capacitance(part.net_name, substrate_net_name) for part in parts] 

931 net_names = list(dict.fromkeys(nets_of_parts)) 

932 index_of_net = {net_name: index for index, net_name in enumerate(net_names)} 

933 rows = [[0.0] * len(net_names) for _ in net_names] 

934 

935 for i, part in enumerate(parts): 

936 row, net_i = matrix[i], index_of_net[nets_of_parts[i]] 

937 if part.is_substrate or not self.leaves_out_to_substrate(part): 

938 rows[net_i][net_i] += sum(row) # NOTE: the capacitance to infinity, the rest of the row 

939 for j in range(i + 1, len(parts)): 

940 net_j = index_of_net[nets_of_parts[j]] 

941 if net_i != net_j and not self.leaves_out_between(part, parts[j]): 

942 rows[net_i][net_j] += row[j] 

943 rows[net_j][net_i] += row[j] 

944 rows[net_i][net_i] -= row[j] 

945 rows[net_j][net_j] -= row[j] 

946 return CapacitanceMatrix(conductor_names=net_names, rows=rows) 

947 

948 def leaves_out_capacitance_of_net(self, net_name: NetName, other_net_name: NetName) -> bool: 

949 """ 

950 :param net_name: the net of a plate 

951 :param other_net_name: the net of what the plate couples to 

952 :return: whether it's a capacitance of an excluded net to the substrate 

953 

954 NOTE: by the net, as a well of another net is on the layer of the substrate, which stays like any other net 

955 """ 

956 return net_name in self.excluded_net_names and other_net_name == self.substrate_layer_name 

957 

958 def mask(self, 

959 layer_name: LayerName, 

960 ports: Optional[bool] = None, 

961 kind: Optional[BlockKind] = None) -> kdb.Region: 

962 """ 

963 :param layer_name: a layer of the stack 

964 :param ports: only of the blocks with ports at their cuts, or without 

965 :param kind: only of the blocks of this kind 

966 :return: the shapes of the blocks on the layer, but of those that only leave out some capacitances 

967 """ 

968 key = (layer_name, ports, kind) 

969 mask = self._masks.get(key, None) 

970 if mask is None: 

971 mask = kdb.Region() 

972 for block in self.blocks: 

973 region = block.region_of(layer_name) 

974 if region is not None and block.capacitance_pair is None and ports in (None, block.ports) \ 

975 and kind in (None, block.kind): 

976 mask += region 

977 mask = mask.merged() 

978 self._masks[key] = mask 

979 return mask 

980 

981 def mask_near(self, 

982 box: kdb.Box, 

983 layer_name: LayerName, 

984 ports: Optional[bool] = None, 

985 kind: Optional[BlockKind] = None) -> kdb.Region: 

986 """ 

987 :return: the polygons of the mask (see mask) that touch the box 

988 """ 

989 return self.regions.near(self.mask(layer_name, ports, kind), box) 

990 

991 def block_indexes_touching(self, box: kdb.Box) -> List[int]: 

992 """ 

993 :return: the indexes of the blocks whose boxes touch the box, in their order 

994 """ 

995 return sorted({shape.property('block') for shape in self.block_boxes.each_touching(kdb.Shapes.SAll, box)}) 

996 

997 def block_index_with_ports_at(self, box: kdb.Box, layer_name: LayerName) -> int: 

998 """ 

999 :param box: of something at a block (e.g. a strip of metal beside it) 

1000 :param layer_name: of a layer the block is on 

1001 :return: the index of the first block with ports it touches (not only by the box of the block) 

1002 """ 

1003 box = box.enlarged(1, 1) 

1004 box_region = kdb.Region(box) 

1005 

1006 def touches(block: ExcludedBlock) -> bool: 

1007 region = block.region_of(layer_name) 

1008 return block.ports and region is not None and \ 

1009 not self.regions.near(region, box).interacting(box_region).is_empty() 

1010 

1011 return next(index for index in self.block_indexes_touching(box) if touches(self.blocks[index])) 

1012 

1013 def block_indexes_with_ports_within(self, polygon: kdb.Region, layer_name: LayerName) -> List[int]: 

1014 """ 

1015 :param polygon: of something within blocks 

1016 :param layer_name: its layer 

1017 :return: the indexes of the blocks with ports it is within, also only partly 

1018 """ 

1019 box = polygon.bbox() 

1020 

1021 def overlaps(block: ExcludedBlock) -> bool: 

1022 region = block.region_of(layer_name) 

1023 return block.ports and region is not None and not (polygon & self.regions.near(region, box)).is_empty() 

1024 

1025 return [index for index in self.block_indexes_touching(box) if overlaps(self.blocks[index])] 

1026 

1027 def is_only_label_of_cut_cell(self, text: str, position: kdb.Point) -> bool: 

1028 """ 

1029 :return: whether a cut cell has the label, and the rest of the layout not 

1030 """ 

1031 return (text, position) in self.labels_of_cut_cells and (text, position) not in self.labels_of_rest 

1032 

1033 def is_only_named_by_cut_cells(self, net_name: NetName) -> bool: 

1034 """ 

1035 :return: whether all labels that the net is named after (e.g. A,B) are labels of the cut cells only 

1036 """ 

1037 texts_of_cut_cells = {text for text, _ in self.labels_of_cut_cells} 

1038 texts_of_rest = {text for text, _ in self.labels_of_rest} 

1039 return all(text in texts_of_cut_cells and text not in texts_of_rest for text in net_name.split(',')) 

1040 

1041 def labels_at(self, block_index: int, layer_name: LayerName, box: kdb.Box) -> Dict[int, BlockLabel]: 

1042 """ 

1043 :return: the labels of the cell of the block at the box, on the layer, by their indexes in its list 

1044 """ 

1045 block = self.blocks[block_index] 

1046 positions = self._label_positions.get((block_index, layer_name), None) 

1047 if positions is None: 

1048 positions = kdb.Shapes() 

1049 for index, label in enumerate(block.labels): 

1050 if label.layer_name == layer_name: 

1051 positions.insert(kdb.BoxWithProperties(kdb.Box(label.position, label.position), {'label': index})) 

1052 self._label_positions[(block_index, layer_name)] = positions 

1053 indexes = (shape.property('label') for shape in positions.each_touching(kdb.Shapes.SAll, box)) 

1054 return {index: block.labels[index] for index in indexes} 

1055 

1056 def net_of_capacitance(self, net_name: NetName, substrate_net_name: NetName) -> NetName: 

1057 """ 

1058 :return: the net a capacitance of a plate of the net goes to: 

1059 the substrate for the conductors within a block that nothing connects to the netlist, 

1060 like metal with a floating node, but which a model of the block doesn't know either 

1061 """ 

1062 return substrate_net_name if net_name in self.floating_net_names else net_name 

1063 

1064 def has_pieces(self, net_name: NetName) -> bool: 

1065 return net_name in self.pieces_by_net 

1066 

1067 def area_parts(self, 

1068 area: kdb.Region, 

1069 net_name: NetName, 

1070 layer_name: LayerName) -> List[Tuple[kdb.Region, NetName]]: 

1071 """ 

1072 :param area: of a plate of the net on the layer 

1073 :return: its parts by the piece of the net they are on, or by the net itself, off its pieces 

1074 (e.g. the substrate, or metal the resistance extraction has no nodes on) 

1075 """ 

1076 pieces = self.pieces_by_net.get(net_name, None) 

1077 if pieces is None: 

1078 return [(area, net_name)] 

1079 parts: List[Tuple[kdb.Region, NetName]] = [] 

1080 rest = area 

1081 for piece in pieces: 

1082 region = piece.region_by_layer.get(layer_name, None) 

1083 if region is None: 

1084 continue 

1085 part = rest & self.regions.near(region, rest.bbox()) 

1086 if not part.is_empty(): 

1087 parts.append((part, piece.name)) 

1088 rest = rest - part 

1089 if not rest.is_empty(): 

1090 parts.append((rest, net_name)) 

1091 return parts 

1092 

1093 def edge_parts(self, 

1094 edge: kdb.Edge, 

1095 metal_side: int, 

1096 net_name: NetName, 

1097 layer_name: LayerName) -> List[Tuple[Interval, NetName]]: 

1098 """ 

1099 :param edge: of a plate of the net on the layer 

1100 :param metal_side: where the metal is beside the edge, -1 on its right, 1 on its left 

1101 :return: its parts by the piece of the net they are on, or by the net itself, off its pieces 

1102 """ 

1103 pieces = self.pieces_by_net.get(net_name, None) 

1104 if pieces is None: 

1105 return [((0.0, 1.0), net_name)] 

1106 metal_edge = edge.shifted(metal_side) 

1107 parts: List[Tuple[Interval, NetName]] = [] 

1108 rest: List[Interval] = [(0.0, 1.0)] 

1109 for piece in pieces: 

1110 region = piece.region_by_layer.get(layer_name, None) 

1111 if region is not None: 

1112 # NOTE: each part of the edge on one piece only, as a node of a piece can touch a polygon of another 

1113 within = intervals_within(metal_edge, self.regions.near(region, metal_edge.bbox())) 

1114 parts.extend((interval, piece.name) for interval in intersected_intervals(within, rest)) 

1115 rest = intersected_intervals(rest, complemented_intervals(within)) 

1116 parts.extend((interval, net_name) for interval in rest) 

1117 return sorted(parts) 

1118 

1119 def blocks_touching(self, box: kdb.Box) -> Iterator[ExcludedBlock]: 

1120 for shape in self.block_boxes.each_touching(kdb.Shapes.SAll, box): 

1121 yield self.blocks[shape.property('block')] 

1122 

1123 def excluded_area(self, area: kdb.Region, layer_name_a: LayerName, layer_name_b: LayerName) -> kdb.Region: 

1124 """ 

1125 :param area: an overlap of two plates, of these layers, the second may be the substrate 

1126 :return: the part of it that a model has: both plates (or a plate and the substrate) within one block 

1127 """ 

1128 excluded = kdb.Region() 

1129 for block in self.blocks_touching(area.bbox()): 

1130 if not block.applies_between(layer_name_a, layer_name_b): 

1131 continue 

1132 part = area 

1133 for layer_name in (layer_name_a, layer_name_b): 

1134 if layer_name != self.substrate_layer_name: 

1135 region = block.region_of(layer_name) 

1136 part = kdb.Region() if region is None else part & self.regions.near(region, area.bbox()) 

1137 excluded += part 

1138 return excluded.merged() 

1139 

1140 def kept_edge_parts(self, 

1141 edge_a: kdb.Edge, 

1142 layer_name_a: LayerName, 

1143 edge_b: kdb.Edge, 

1144 layer_name_b: LayerName) -> List[Interval]: 

1145 """ 

1146 The capacitance between a plate and what its edge faces (e.g. a sidewall, a fringe) 

1147 

1148 :param edge_a: the edge of the plate (on its right), in a direction of its polygon 

1149 :param layer_name_a: the layer of the plate 

1150 :param edge_b: the edge of what it faces (same direction, on its left) 

1151 :param layer_name_b: the layer of what it faces, the substrate is below all plates of a model 

1152 :return: the parts of the edges (their parameters) that the model doesn't have 

1153 """ 

1154 if not edge_a.bbox().enlarged(1, 1).touches(self.bbox): 

1155 return [(0.0, 1.0)] 

1156 

1157 # NOTE: by the metal beside the edges rather than the edges, as they may be on the boundary of a marker, 

1158 # e.g. a plate within it up to there, or a wire outside that ends there 

1159 metal_edge_a = edge_a.shifted(-1) 

1160 metal_edge_b = edge_b.shifted(1) 

1161 

1162 excluded: List[Interval] = [] 

1163 for block in self.blocks_touching(metal_edge_a.bbox()): 

1164 if not block.applies_between(layer_name_a, layer_name_b): 

1165 continue 

1166 region_a = block.region_of(layer_name_a) 

1167 if region_a is None: 

1168 continue 

1169 parts = intervals_within(metal_edge_a, self.regions.near(region_a, metal_edge_a.bbox())) 

1170 if parts and layer_name_b != self.substrate_layer_name: 

1171 region_b = block.region_of(layer_name_b) 

1172 parts = [] if region_b is None else intersected_intervals( 

1173 parts, intervals_within(metal_edge_b, self.regions.near(region_b, metal_edge_b.bbox()))) 

1174 excluded.extend(parts) 

1175 return complemented_intervals(merged_intervals(excluded))