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
« 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#
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)
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)
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)
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"""
45from __future__ import annotations
47from collections import Counter, defaultdict
48from dataclasses import dataclass, field
49from enum import StrEnum
50from fnmatch import fnmatchcase
51import re
52from typing import *
54import klayout.db as kdb
56from ..common.capacitance_matrix import CapacitanceMatrix
57from ..types import GDSPair, LayerName, NetName
59import klayout_pex_protobuf.kpex.tech.tech_pb2 as tech_pb2
62Interval = Tuple[float, float] # a part of an edge, by the parameter t ∈ [0, 1] along it
65class ExclusionError(Exception):
66 """
67 What is to be left out can't be honored
68 """
69 pass
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'
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
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
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
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 []
138 def parameter(x: int, y: int) -> float:
139 return ((x - edge.x1) * dx + (y - edge.y1) * dy) / squared_length
141 return merged_intervals(sorted((parameter(part.x1, part.y1), parameter(part.x2, part.y2)))
142 for part in (kdb.Edges([edge]) & region).each())
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]))
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
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 """
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]] = {}
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
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.
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]
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
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
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)
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]
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
275 @property
276 def specification(self) -> str:
277 return self.cell_pattern if self.prefix is None else f"{self.cell_pattern}={self.prefix}"
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)
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
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]
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
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),)
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)
347@dataclass(frozen=True)
348class BlockLabel:
349 text: str
350 position: kdb.Point
351 layer_name: LayerName
354class BlockKind(StrEnum):
355 MARKER = 'marker'
356 CUT = 'cut'
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
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)
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
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)
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()
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
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
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
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)
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}
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
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())
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)
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()
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
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
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)
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
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)
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
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")
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)
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()
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
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
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
656class Exclusions:
657 """
658 What the extraction leaves out of a layout, as blocks that models stand for
659 """
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()
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}))
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()
682 # the nets within the blocks that nothing connects to the netlist (see net_of_capacitance)
683 self.floating_net_names: Set[NetName] = set()
685 # the pieces of the nets with ports at the blocks, by the resistance extraction
686 self.pieces_by_net: Dict[NetName, List[NetPiece]] = {}
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] = {}
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()
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()
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] = {}
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)
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
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
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
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()
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()
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
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
843 return self.parts_within_blocks(shapes, net_name, part_within)
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
865 return self.parts_within_blocks(shapes, net_name, part_within)
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
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]
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)
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)
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]
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)
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
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
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
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)
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)})
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)
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()
1011 return next(index for index in self.block_indexes_touching(box) if touches(self.blocks[index]))
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()
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()
1025 return [index for index in self.block_indexes_touching(box) if overlaps(self.blocks[index])]
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
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(','))
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}
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
1064 def has_pieces(self, net_name: NetName) -> bool:
1065 return net_name in self.pieces_by_net
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
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)
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')]
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()
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)
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)]
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)
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))