diff --git a/src/kfactory/checks.py b/src/kfactory/checks.py index 5dc024a0f..0006d990c 100644 --- a/src/kfactory/checks.py +++ b/src/kfactory/checks.py @@ -8,7 +8,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast from kfnetlist import PortCheck, check_connection @@ -16,6 +16,7 @@ from .layer import LayerEnum from .port import create_port_error, port_polygon from .ports import Ports +from .spatial import collect_instance_region, iter_overlapping_bbox_pairs if TYPE_CHECKING: from collections.abc import Callable, Iterable @@ -415,7 +416,9 @@ def emit_mismatch( if n == 1: if layer in cell_ports and coord in cell_ports[layer]: cell_port = cell_ports[layer][coord][0] - result = check_connection(cell_port, ports[0][0]) + result = check_connection( + cast("Any", cell_port), cast("Any", ports[0][0]) + ) emit_mismatch( result, lc, @@ -428,7 +431,9 @@ def emit_mismatch( ) # Dangling case is handled by dangling_ports_check. elif n == 2: - result = check_connection(ports[0][0], ports[1][0]) + result = check_connection( + cast("Any", ports[0][0]), cast("Any", ports[1][0]) + ) emit_mismatch( result, lc, @@ -707,42 +712,24 @@ def instance_overlap_check( for layer in _iter_check_layers(cell, layers): error_region = kdb.Region() - inst_regions: dict[int, kdb.Region] = {} - inst_region = kdb.Region() - for i, inst in enumerate(cell.insts): - inst_region_ = kdb.Region(inst.ibbox(layer)) - inst_shapes: kdb.Region | None = None - if not (inst_region & inst_region_).is_empty(): - if inst_shapes is None: - inst_shapes = kdb.Region() - shape_it = cell.begin_shapes_rec_overlapping( - layer, inst.bbox(layer) - ) - shape_it.select_cells([inst.cell.cell_index()]) - shape_it.min_depth = 1 - shape_it.shape_flags = kdb.Shapes.SRegions - for _it in shape_it.each(): - if _it.path()[0].inst() == inst.instance: - inst_shapes.insert( - _it.shape().polygon.transformed(_it.trans()) - ) - for j, _reg in inst_regions.items(): - if _reg & inst_region_: - reg_ = kdb.Region() - shape_it = cell.begin_shapes_rec_touching( - layer, (_reg & inst_region_).bbox() - ) - shape_it.select_cells([cell.insts[j].cell.cell_index()]) - shape_it.min_depth = 1 - shape_it.shape_flags = kdb.Shapes.SRegions - for _it in shape_it.each(): - if _it.path()[0].inst() == cell.insts[j].instance: - reg_.insert( - _it.shape().polygon.transformed(_it.trans()) - ) - error_region.insert(reg_ & inst_shapes) - inst_region += inst_region_ - inst_regions[i] = inst_region_ + inst_records: list[tuple[ProtoTInstance[Any], kdb.Box]] = [ + (inst, inst.ibbox(layer)) for inst in cell.insts + ] + inst_cache: dict[int, kdb.Region] = {} + for idx, other_idx in iter_overlapping_bbox_pairs( + [bbox for _, bbox in inst_records] + ): + inst, _ = inst_records[idx] + other_inst, _ = inst_records[other_idx] + inst_region = inst_cache.get(idx) + if inst_region is None: + inst_region = collect_instance_region(cell, layer, inst) + inst_cache[idx] = inst_region + other_region = inst_cache.get(other_idx) + if other_region is None: + other_region = collect_instance_region(cell, layer, other_inst) + inst_cache[other_idx] = other_region + error_region.insert(other_region & inst_region) if not error_region.is_empty(): sc = _get_or_create_subcategory(db_, layer_cat(layer), "InstanceOverlap") @@ -785,11 +772,17 @@ def shape_instance_overlap_check( for layer in _iter_check_layers(cell, layers): error_region = kdb.Region() reg = kdb.Region(cell.shapes(layer)) + if reg.is_empty(): + continue + reg_bbox = reg.bbox() + reg_bbox_region = kdb.Region(reg_bbox) for inst in cell.insts: inst_region_ = kdb.Region(inst.ibbox(layer)) - if (inst_region_ & reg).is_empty(): + if (inst_region_ & reg_bbox_region).is_empty(): continue - rec_it = cell.begin_shapes_rec_touching(layer, (inst_region_ & reg).bbox()) + rec_it = cell.begin_shapes_rec_touching( + layer, (inst_region_ & reg_bbox_region).bbox() + ) rec_it.min_depth = 1 error_region += kdb.Region(rec_it) & reg diff --git a/src/kfactory/conf.py b/src/kfactory/conf.py index 77151a456..0f27ebb21 100644 --- a/src/kfactory/conf.py +++ b/src/kfactory/conf.py @@ -87,6 +87,7 @@ def __call__( library_save_options: kdb.SaveLayoutOptions, technology: str | None = None, markers: list[tuple[DShapeLike, MarkerConfig]] | None = None, + name: str | None = None, ) -> None: ... diff --git a/src/kfactory/decorators.py b/src/kfactory/decorators.py index 2ba65f30d..74c5a9118 100644 --- a/src/kfactory/decorators.py +++ b/src/kfactory/decorators.py @@ -493,13 +493,14 @@ def wrapper_autocell( def wrapped_cell(**params: Any) -> KC: _params_to_original(params) + name_params = {k: v for k, v in params.items() if k not in drop_params} old_future_name: str | None = None if set_name: if basename is not None: - name = get_cell_name(basename, **params) + name = get_cell_name(basename, **name_params) else: - name = get_cell_name(self.name, **params) + name = get_cell_name(self.name, **name_params) old_future_name = kcl._future_cell_name kcl._future_cell_name = name if layout_cache: @@ -798,13 +799,14 @@ def wrapper_autocell( def wrapped_cell(**params: Any) -> VK: _params_to_original(params) + name_params = {k: v for k, v in params.items() if k not in drop_params} old_future_name: str | None = None if set_name: if basename is not None: - name = get_cell_name(basename, **params) + name = get_cell_name(basename, **name_params) else: - name = get_cell_name(self.name, **params) + name = get_cell_name(self.name, **name_params) old_future_name = kcl._future_cell_name kcl._future_cell_name = name logger.debug(f"Constructing {kcl._future_cell_name}") diff --git a/src/kfactory/enclosure.py b/src/kfactory/enclosure.py index d81446c5b..f1e2e030c 100644 --- a/src/kfactory/enclosure.py +++ b/src/kfactory/enclosure.py @@ -8,6 +8,7 @@ from __future__ import annotations import itertools +import math import sys from collections import defaultdict from enum import IntEnum @@ -77,6 +78,7 @@ class Direction(IntEnum): _min_size = -sys.maxsize - 1 +_EXTRUDE_PATH_ARRAY_THRESHOLD = 64 def is_callable_widths( @@ -94,6 +96,32 @@ def path_pts_to_polygon( return kdb.DPolygon(pts_top + pts_bot) +def _offset_point_from_angle( + point: kdb.DPoint, + offset: float, + angle: float, +) -> kdb.DPoint: + angle_rad = math.radians(angle) + return kdb.DPoint( + point.x - offset * math.sin(angle_rad), + point.y + offset * math.cos(angle_rad), + ) + + +def _offset_point_from_vector( + point: kdb.DPoint, + offset: float, + vector: kdb.DVector, +) -> kdb.DPoint: + length = math.hypot(vector.x, vector.y) + if length == 0: + return kdb.DPoint(point.x, point.y + offset) + return kdb.DPoint( + point.x - offset * vector.y / length, + point.y + offset * vector.x / length, + ) + + def _extrude_path_band_points( path: Sequence[kdb.DPoint], lo: float, @@ -118,6 +146,18 @@ def _extrude_path_band_points( end_angle: optionally specify a custom ending angle if `None` will be autocalculated from the last two elements """ + if len(path) >= _EXTRUDE_PATH_ARRAY_THRESHOLD: + return _extrude_path_band_points_array(path, lo, hi, start_angle, end_angle) + return _extrude_path_band_points_python(path, lo, hi, start_angle, end_angle) + + +def _extrude_path_band_points_python( + path: Sequence[kdb.DPoint], + lo: float, + hi: float, + start_angle: float | None = None, + end_angle: float | None = None, +) -> tuple[list[kdb.DPoint], list[kdb.DPoint]]: start = path[1] - path[0] end = path[-1] - path[-2] if start_angle is None: @@ -154,6 +194,77 @@ def _extrude_path_band_points( return [v.disp.to_p() for v in vector_top], [v.disp.to_p() for v in vector_bot] +def _extrude_path_band_points_array( + path: Sequence[kdb.DPoint], + lo: float, + hi: float, + start_angle: float | None = None, + end_angle: float | None = None, +) -> tuple[list[kdb.DPoint], list[kdb.DPoint]]: + n = len(path) + xs = np.fromiter((p.x for p in path), dtype=np.float64, count=n) + ys = np.fromiter((p.y for p in path), dtype=np.float64, count=n) + + top_x = np.empty(n, dtype=np.float64) + top_y = np.empty(n, dtype=np.float64) + bot_x = np.empty(n, dtype=np.float64) + bot_y = np.empty(n, dtype=np.float64) + + if start_angle is None: + start_vector = kdb.DVector(xs[1] - xs[0], ys[1] - ys[0]) + top_start = _offset_point_from_vector(path[0], hi, start_vector) + bot_start = _offset_point_from_vector(path[0], lo, start_vector) + else: + top_start = _offset_point_from_angle(path[0], hi, start_angle) + bot_start = _offset_point_from_angle(path[0], lo, start_angle) + top_x[0], top_y[0] = top_start.x, top_start.y + bot_x[0], bot_y[0] = bot_start.x, bot_start.y + + if end_angle is None: + end_dx = xs[-1] - xs[-2] + end_dy = ys[-1] - ys[-2] + end_angle = math.degrees(math.atan2(end_dy, end_dx)) + + mid_x = xs[1:-1] + mid_y = ys[1:-1] + dx = xs[2:] - xs[:-2] + dy = ys[2:] - ys[:-2] + length = np.hypot(dx, dy) + valid = length != 0 + + top_x_mid = mid_x.copy() + top_y_mid = mid_y + hi + bot_x_mid = mid_x.copy() + bot_y_mid = mid_y + lo + if np.any(valid): + inv_length = 1 / length[valid] + top_x_mid[valid] = mid_x[valid] - hi * dy[valid] * inv_length + top_y_mid[valid] = mid_y[valid] + hi * dx[valid] * inv_length + bot_x_mid[valid] = mid_x[valid] - lo * dy[valid] * inv_length + bot_y_mid[valid] = mid_y[valid] + lo * dx[valid] * inv_length + + top_x[1:-1] = top_x_mid + top_y[1:-1] = top_y_mid + bot_x[1:-1] = bot_x_mid + bot_y[1:-1] = bot_y_mid + + top_end = _offset_point_from_angle(path[-1], hi, end_angle) + bot_end = _offset_point_from_angle(path[-1], lo, end_angle) + top_x[-1], top_y[-1] = top_end.x, top_end.y + bot_x[-1], bot_y[-1] = bot_end.x, bot_end.y + + return ( + [ + kdb.DPoint(x, y) + for x, y in zip(top_x.tolist(), top_y.tolist(), strict=False) + ], + [ + kdb.DPoint(x, y) + for x, y in zip(bot_x.tolist(), bot_y.tolist(), strict=False) + ], + ) + + def extrude_path_points( path: Sequence[kdb.DPoint], width: float, @@ -594,6 +705,7 @@ class LayerEnclosure(BaseModel, arbitrary_types_allowed=True, frozen=True): layer_sections: dict[kdb.LayerInfo, LayerSection] _name: str | None = PrivateAttr() + _unnamed_key: str | None = PrivateAttr(default=None) main_layer: kdb.LayerInfo | None bbox_sections: dict[kdb.LayerInfo, int] @@ -726,6 +838,8 @@ def unnamed_key(self) -> str: named enclosures as well lets the registry alias the structural signature to a named enclosure. """ + if self._unnamed_key is not None: + return self._unnamed_key list_to_hash: list[tuple[str, ...]] = [(str(self.main_layer),)] for layer, layer_section in self.layer_sections.items(): list_to_hash.append((str(layer), str(layer_section.sections))) @@ -733,7 +847,9 @@ def unnamed_key(self) -> str: self.bbox_sections.items(), key=lambda kv: str(kv[0]) ): list_to_hash.append((str(layer), "bbox", str(offset))) - return sha1(str(list_to_hash).encode("UTF-8")).hexdigest()[-8:] # noqa: S324 + unnamed_key = sha1(str(list_to_hash).encode("UTF-8")).hexdigest()[-8:] # noqa: S324 + object.__setattr__(self, "_unnamed_key", unnamed_key) + return unnamed_key def minkowski_region( self, diff --git a/src/kfactory/factories/bezier.py b/src/kfactory/factories/bezier.py index c6aeb41ee..db0a82a30 100644 --- a/src/kfactory/factories/bezier.py +++ b/src/kfactory/factories/bezier.py @@ -74,15 +74,34 @@ def bezier_curve( control_points: Sequence[tuple[np.float64 | float, np.float64 | float]], ) -> list[kdb.DPoint]: """Calculates the backbone of a bezier bend.""" + n = len(control_points) - 1 + if n == 3: + p0, p1, p2, p3 = control_points + x0, y0 = p0 + x1, y1 = p1 + x2, y2 = p2 + x3, y3 = p3 + + xs = ( + (((x3 - 3 * x2 + 3 * x1 - x0) * t + 3 * (x2 - 2 * x1 + x0)) * t) + + 3 * (x1 - x0) + ) * t + x0 + ys = ( + (((y3 - 3 * y2 + 3 * y1 - y0) * t + 3 * (y2 - 2 * y1 + y0)) * t) + + 3 * (y1 - y0) + ) * t + y0 + return [ + kdb.DPoint(x, y) for x, y in zip(xs.tolist(), ys.tolist(), strict=False) + ] + xs = np.zeros(t.shape, dtype=np.float64) ys = np.zeros(t.shape, dtype=np.float64) - n = len(control_points) - 1 for k in range(n + 1): ank = binom(n, k) * (1 - t) ** (n - k) * t**k xs += ank * control_points[k][0] ys += ank * control_points[k][1] - return [kdb.DPoint(float(x), float(y)) for x, y in zip(xs, ys, strict=False)] + return [kdb.DPoint(x, y) for x, y in zip(xs.tolist(), ys.tolist(), strict=False)] @overload diff --git a/src/kfactory/factories/circular.py b/src/kfactory/factories/circular.py index f0381b276..1e427df6e 100644 --- a/src/kfactory/factories/circular.py +++ b/src/kfactory/factories/circular.py @@ -67,6 +67,16 @@ def __call__( ... +def _circular_backbone_points( + *, radius: um, angle: deg, angle_step: deg +) -> list[kdb.DPoint]: + points = max(int(angle // angle_step + 0.5), 1) + radians = np.linspace(0, np.deg2rad(angle), points, endpoint=True) + x = np.sin(radians) * radius + y = (-np.cos(radians) + 1) * radius + return [kdb.DPoint(_x, _y) for _x, _y in zip(x.tolist(), y.tolist(), strict=False)] + + @overload def bend_circular_factory( kcl: KCLayout, @@ -169,19 +179,9 @@ def _bend_circular( angle = -angle xs = kcl.get_base_cross_section(cross_section) - - backbone = [ - kdb.DPoint(x, y) - for x, y in [ - [ - np.sin(_angle / 180 * np.pi) * r, - (-np.cos(_angle / 180 * np.pi) + 1) * r, - ] - for _angle in np.linspace( - 0, angle, int(angle // angle_step + 0.5), endpoint=True - ) - ] - ] + backbone = _circular_backbone_points( + radius=r, angle=angle, angle_step=angle_step + ) extrude_path_cross_section(c, backbone, xs, start_angle=0, end_angle=angle) diff --git a/src/kfactory/factories/euler.py b/src/kfactory/factories/euler.py index a7411d2b4..b252b5e79 100644 --- a/src/kfactory/factories/euler.py +++ b/src/kfactory/factories/euler.py @@ -106,66 +106,54 @@ def __call__( ... -def euler_bend_points( +def _euler_bend_xy( angle_amount: deg = 90, radius: um = 100, resolution: float = 150 -) -> list[kdb.DPoint]: - """Base euler bend, no transformation, emerging from the origin.""" +) -> tuple[np.ndarray, np.ndarray]: if angle_amount < 0: raise ValueError(f"angle_amount should be positive. Got {angle_amount}") - # End angle - eth = angle_amount * np.pi / 180 - # If bend is trivial, return a trivial shape + eth = angle_amount * np.pi / 180 if eth == 0: - return [kdb.DPoint(0, 0)] + return np.array([0.0]), np.array([0.0]) - # Total displaced angle th = eth / 2 - - # Total length of curve total_length = 4 * radius * th - - # Compute curve ## a = np.sqrt(radius**2 * np.abs(th)) sq2pi = np.sqrt(2 * np.pi) - - # Function for computing curve coords - (fasin, facos) = fresnel(np.sqrt(2 / np.pi) * radius * th / a) - - def _xy(s: float) -> kdb.DPoint: - if th == 0: - return kdb.DPoint(0, 0) - if s <= total_length / 2: - (fsin, fcos) = fresnel(s / (sq2pi * a)) - x = sq2pi * a * fcos - y = sq2pi * a * fsin - else: - (fsin, fcos) = fresnel((total_length - s) / (sq2pi * a)) - x = ( - sq2pi - * a - * ( - facos - + np.cos(2 * th) * (facos - fcos) - + np.sin(2 * th) * (fasin - fsin) - ) - ) - y = ( - sq2pi - * a - * ( - fasin - - np.cos(2 * th) * (fasin - fsin) - + np.sin(2 * th) * (facos - fcos) - ) - ) - return kdb.DPoint(x, y) - - # Parametric step size + fasin, facos = fresnel(np.sqrt(2 / np.pi) * radius * th / a) step = total_length / max(int(th * resolution), 1) + s_vals = np.linspace(0.0, total_length, round(total_length / step) + 1) + left_mask = s_vals <= total_length / 2 + fresnel_arg = np.where(left_mask, s_vals, total_length - s_vals) / (sq2pi * a) + fsin, fcos = fresnel(fresnel_arg) + + x = np.where( + left_mask, + sq2pi * a * fcos, + sq2pi + * a + * (facos + np.cos(2 * th) * (facos - fcos) + np.sin(2 * th) * (fasin - fsin)), + ) + y = np.where( + left_mask, + sq2pi * a * fsin, + sq2pi + * a + * (fasin - np.cos(2 * th) * (fasin - fsin) + np.sin(2 * th) * (facos - fcos)), + ) + return x, y - # Generate points - return [_xy(i * step) for i in range(round(total_length / step) + 1)] + +def euler_bend_points( + angle_amount: deg = 90, radius: um = 100, resolution: float = 150 +) -> list[kdb.DPoint]: + """Base euler bend, no transformation, emerging from the origin.""" + x_vals, y_vals = _euler_bend_xy( + angle_amount=angle_amount, radius=radius, resolution=resolution + ) + return [ + kdb.DPoint(x, y) for x, y in zip(x_vals.tolist(), y_vals.tolist(), strict=False) + ] def euler_endpoint( @@ -218,21 +206,19 @@ def froot(th: float) -> float: angle = direction * 90.0 extra_y = -direction * fb - spoints = [] - right_point = [] - points_left_half = euler_bend_points(abs(angle), radius, resolution) - - # Second bend - for pts in points_left_half: - r_pt_x = 2 * points_left_half[-1].x - pts.x - r_pt_y = 2 * points_left_half[-1].y - pts.y + extra_y * direction - pts.y = pts.y * direction - r_pt_y = r_pt_y * direction - spoints.append(pts) - right_point.append(kdb.DPoint(r_pt_x, r_pt_y)) - spoints += right_point[::-1] - - return spoints + left_x, orig_y = _euler_bend_xy(abs(angle), radius, resolution) + left_y = orig_y * direction + right_x = 2 * left_x[-1] - left_x + right_y = (2 * orig_y[-1] - orig_y + extra_y * direction) * direction + + left_points = [ + kdb.DPoint(x, y) for x, y in zip(left_x.tolist(), left_y.tolist(), strict=False) + ] + right_points = [ + kdb.DPoint(x, y) + for x, y in zip(right_x.tolist(), right_y.tolist(), strict=False) + ] + return left_points + right_points[::-1] @overload diff --git a/src/kfactory/factories/straight.py b/src/kfactory/factories/straight.py index a1d481616..d2d012837 100644 --- a/src/kfactory/factories/straight.py +++ b/src/kfactory/factories/straight.py @@ -26,9 +26,9 @@ DCrossSectionSpecDict, ) from ..enclosure import LayerEnclosure, extrude_path_cross_section -from ..kcell import KCell +from ..kcell import KCell, ProtoTKCell from ..layout import CellKWargs, KCLayout -from ..port import rename_by_direction, rename_clockwise +from ..port import BasePort, rename_by_direction, rename_clockwise from ..settings import Info from ..typings import KC, KC_co, MetaData, dbu from .utils import ( @@ -46,6 +46,7 @@ def __call__( self, *, length: dbu, + routing_fast: bool = False, cross_section: str | AnyCrossSectionInput | CrossSectionSpecDict @@ -71,6 +72,46 @@ def __call__( ... +class _StraightFactoryWithRoutingFast[TKC: ProtoTKCell[Any]]: + __name__: str + __doc__: str | None + __module__: str + + def __init__( + self, + factory: StraightFactory[TKC], + routing_fast_factory: StraightFactory[KCell], + ) -> None: + self._factory = factory + self.routing_fast_factory = routing_fast_factory + self.__name__ = factory.__name__ + self.__doc__ = getattr(factory, "__doc__", None) + self.__module__ = getattr(factory, "__module__", __name__) + + def __call__( + self, + *, + length: dbu, + routing_fast: bool = False, + cross_section: str + | AnyCrossSectionInput + | CrossSectionSpecDict + | DCrossSectionSpecDict + | None = None, + width: dbu | None = None, + layer: kdb.LayerInfo | None = None, + enclosure: LayerEnclosure | None = None, + ) -> TKC: + return self._factory( + length=length, + routing_fast=routing_fast, + cross_section=cross_section, + width=width, + layer=layer, + enclosure=enclosure, + ) + + @overload def straight_dbu_factory( kcl: KCLayout, @@ -149,6 +190,9 @@ def additional_info_func( elif kcl.rename_function == rename_by_direction: cell_kwargs["ports"] = {"left": ["W0"], "right": ["E0"]} cell_kwargs.setdefault("basename", "straight") + cast("dict[str, Any]", cell_kwargs).setdefault( + "drop_params", ("self", "cls", "routing_fast") + ) basename = cell_kwargs["basename"] if output_type is not None: @@ -156,10 +200,10 @@ def additional_info_func( else: cell = kcl.cell(output_type=cast("type[KC]", KCell), **cell_kwargs) - @cell - def _straight( - cross_section: str | AnyCrossSectionInput, + def _straight_impl( + xs: Any, length: dbu, + routing_fast: bool = False, ) -> KCell: """Waveguide defined by a cross section.""" c = kcl.kcell() @@ -172,43 +216,75 @@ def _straight( ) length = -length - xs = kcl.get_base_cross_section(cross_section) - extrude_path_cross_section( c, [kdb.DPoint(0.0, 0.0), kdb.DPoint(kcl.to_um(length), 0.0)], xs ) - c.create_port( - name="o1", - trans=kdb.Trans(2, False, 0, 0), - cross_section=xs, - port_type=port_type, - ) - c.create_port( - name="o2", - trans=kdb.Trans(0, False, length, 0), - cross_section=xs, - port_type=port_type, - ) + if not routing_fast: + c.create_port( + name="o1", + trans=kdb.Trans(2, False, 0, 0), + cross_section=xs, + port_type=port_type, + ) + c.create_port( + name="o2", + trans=kdb.Trans(0, False, length, 0), + cross_section=xs, + port_type=port_type, + ) + + _info: dict[str, MetaData] = { + "width_um": kcl.to_um(xs.width), + "length_um": kcl.to_um(length), + "width_dbu": xs.width, + "length_dbu": length, + } + _info.update(_additional_info_func(cross_section=xs, length=length)) + _info.update(_additional_info) + c.info = Info(**_info) - _info: dict[str, MetaData] = { - "width_um": kcl.to_um(xs.width), - "length_um": kcl.to_um(length), - "width_dbu": xs.width, - "length_dbu": length, - } - _info.update(_additional_info_func(cross_section=xs, length=length)) - _info.update(_additional_info) - c.info = Info(**_info) - - c.boundary = kdb.DPolygon(c.dbbox()) - c.auto_rename_ports() + c.boundary = kdb.DPolygon(c.dbbox()) + c.auto_rename_ports() + else: + c._base.ports.extend( + [ + BasePort( + name="o1", + kcl=kcl, + cross_section=xs, + trans=kdb.Trans(2, False, 0, 0), + port_type=port_type, + ), + BasePort( + name="o2", + kcl=kcl, + cross_section=xs, + trans=kdb.Trans(0, False, length, 0), + port_type=port_type, + ), + ] + ) return c + @cell + def _straight( + cross_section: str | AnyCrossSectionInput, + length: dbu, + routing_fast: bool = False, + ) -> KCell: + """Waveguide defined by a cross section.""" + return _straight_impl( + kcl.get_base_cross_section(cross_section), + length, + routing_fast=routing_fast, + ) + @kcl.generic_factory(name=basename) def straight( *, length: dbu, + routing_fast: bool = False, cross_section: str | AnyCrossSectionInput | CrossSectionSpecDict @@ -226,6 +302,29 @@ def straight( xs = cross_section_from_width(kcl, width, layer, enclosure) else: xs = kcl.get_icross_section(cross_section) - return _straight(cross_section=xs, length=length) + return _straight(cross_section=xs, length=length, routing_fast=routing_fast) + + def routing_fast_straight( + *, + length: dbu, + routing_fast: bool = False, + cross_section: str + | AnyCrossSectionInput + | CrossSectionSpecDict + | DCrossSectionSpecDict + | None = None, + width: dbu | None = None, + layer: kdb.LayerInfo | None = None, + enclosure: LayerEnclosure | None = None, + ) -> KCell: + if cross_section is None: + if width is None or layer is None: + raise ValueError( + "Provide a cross_section, or width and layer (legacy call)." + ) + xs = cross_section_from_width(kcl, width, layer, enclosure) + else: + xs = kcl.get_icross_section(cross_section) + return _straight_impl(xs.base, length, routing_fast=True) - return straight + return _StraightFactoryWithRoutingFast[KC](straight, routing_fast_straight) diff --git a/src/kfactory/factories/utils.py b/src/kfactory/factories/utils.py index a2c838112..677a9de7d 100644 --- a/src/kfactory/factories/utils.py +++ b/src/kfactory/factories/utils.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, TypeGuard from .. import kdb -from ..cross_section import CrossSection, CrossSectionSpecDict +from ..cross_section import CrossSection from ..enclosure import ( LayerEnclosure, _extrude_path_band_points, @@ -70,14 +70,10 @@ def cross_section_from_width( enclosure: LayerEnclosure | None = None, ) -> CrossSection: """Build a (dbu) symmetric cross section from legacy width/layer/enclosure args.""" - return kcl.get_icross_section( - CrossSectionSpecDict( - layer=layer, - width=width, - unit="dbu", - sections=layer_enclosure_to_sections(enclosure), - ), - symmetrical=True, + return kcl.get_icross_section_from_width( + width=width, + layer=layer, + enclosure=enclosure, ) diff --git a/src/kfactory/factories/virtual/circular.py b/src/kfactory/factories/virtual/circular.py index b8676c59b..879b40aad 100644 --- a/src/kfactory/factories/virtual/circular.py +++ b/src/kfactory/factories/virtual/circular.py @@ -3,8 +3,6 @@ from collections.abc import Callable from typing import Any, Protocol -import numpy as np - from ... import kdb from ...conf import logger from ...cross_section import ( @@ -17,6 +15,7 @@ from ...layout import KCLayout from ...settings import Info from ...typings import MetaData, deg, um +from ..circular import _circular_backbone_points from ..utils import ( _is_additional_info_func, cross_section_from_width, @@ -125,18 +124,9 @@ def virtual_bend_circular( angle = -angle xs = kcl.get_base_cross_section(cross_section) - backbone = [ - kdb.DPoint(x, y) - for x, y in [ - [ - np.sin(_angle / 180 * np.pi) * radius, - (-np.cos(_angle / 180 * np.pi) + 1) * radius, - ] - for _angle in np.linspace( - 0, angle, int(angle // angle_step + 0.5), endpoint=True - ) - ] - ] + backbone = _circular_backbone_points( + radius=radius, angle=angle, angle_step=angle_step + ) extrude_backbone_cross_section( c, diff --git a/src/kfactory/instance.py b/src/kfactory/instance.py index 2cded7657..61ed80291 100644 --- a/src/kfactory/instance.py +++ b/src/kfactory/instance.py @@ -418,7 +418,9 @@ def connect( ) op = Port(base=other.ports[other_port_name].base) # ty:ignore[invalid-argument-type] if isinstance(port, ProtoPort): - p = Port(base=port.base.transformed(self.dcplx_trans.inverted())) + p = Port( + base=port.base.transformed(self.dcplx_trans.inverted(), copy_info=False) + ) else: p = Port(base=self.cell.ports[port].base) @@ -1096,7 +1098,9 @@ def connect( else: op = Port(base=other.base) if isinstance(port, ProtoPort): - p = port.copy(self.trans.inverted()).to_itype() + p = Port( + base=port.base.transformed(self.trans.inverted(), copy_info=False) + ).to_itype() else: p = self.cell.ports[port].to_itype() diff --git a/src/kfactory/instance_ports.py b/src/kfactory/instance_ports.py index d9066cc02..9eda38d70 100644 --- a/src/kfactory/instance_ports.py +++ b/src/kfactory/instance_ports.py @@ -317,7 +317,7 @@ def __init__(self, instance: Instance) -> None: @property def cell_ports(self) -> Ports: - return Ports(kcl=self.instance.cell.kcl, bases=self.instance.cell.ports.bases) + return cast("Ports", self.instance.cell.ports) def filter( self, @@ -338,7 +338,7 @@ def __getitem__( return Port(base=super().__getitem__(key).base) def __iter__(self) -> Iterator[Port]: - yield from (p.to_itype() for p in self.each_port()) + yield from cast("Iterator[Port]", self.each_port()) class DInstancePorts(ProtoTInstancePorts[float]): @@ -352,7 +352,7 @@ def __init__(self, instance: DInstance) -> None: @property def cell_ports(self) -> DPorts: - return DPorts(kcl=self.instance.cell.kcl, bases=self.instance.cell.ports.bases) + return cast("DPorts", self.instance.cell.ports) def filter( self, @@ -373,7 +373,7 @@ def __getitem__( return DPort(base=super().__getitem__(key).base) def __iter__(self) -> Iterator[DPort]: - yield from (p.to_dtype() for p in self.each_port()) + yield from cast("Iterator[DPort]", self.each_port()) class VInstancePorts(ProtoInstancePorts[float, VInstance]): @@ -402,9 +402,7 @@ def __init__(self, instance: VInstance) -> None: @property def cell_ports(self) -> DPorts: - return DPorts( - kcl=self.instance.cell.ports.kcl, bases=self.instance.cell.ports.bases - ) + return cast("DPorts", self.instance.cell.ports) def __len__(self) -> int: """Return Port count.""" diff --git a/src/kfactory/kcell.py b/src/kfactory/kcell.py index a3a61766f..b25746002 100644 --- a/src/kfactory/kcell.py +++ b/src/kfactory/kcell.py @@ -1580,7 +1580,7 @@ def read( if diff.diff_xor.cells() > 0 or diff.layout_meta_diff: diff_kcl = KCLayout(self.name + "_XOR") diff_kcl.layout.assign(diff.diff_xor) - show(diff_kcl) + show(diff_kcl, name=f"{self.name}_XOR") err_msg = ( f"Layout {self.name} cannot merge with layout " @@ -2148,7 +2148,7 @@ def l2n_elec( from kfnetlist.extract import l2n_elec as _kfnetlist_l2n_elec return _kfnetlist_l2n_elec( - self, + cast("Any", self), mark_port_types=mark_port_types, connectivity=connectivity, port_mapping=port_mapping, @@ -2172,8 +2172,8 @@ def netlist( from kfnetlist.extract import extract as _kfnetlist_extract return _kfnetlist_extract( - self, - wrap_kdb_instance=lambda i: Instance(kcl=self.kcl, instance=i), + cast("Any", self), + wrap_kdb_instance=cast("Any", lambda i: Instance(kcl=self.kcl, instance=i)), port_types=port_types, mark_port_types=mark_port_types, connectivity=connectivity, @@ -2192,7 +2192,7 @@ def get_optical_nets( from kfnetlist.extract import get_optical_nets as _kfnetlist_get_optical_nets return _kfnetlist_get_optical_nets( - self, + cast("Any", self), port_types=port_types, allow_width_mismatch=allow_width_mismatch, ) @@ -3620,6 +3620,7 @@ def show( set_technology: bool = True, file_format: Literal["oas", "gds"] = "oas", markers: list[tuple[DShapeLike, MarkerConfig]] | None = None, + name: str | None = None, ) -> None: """Show GDS in klayout. @@ -3647,23 +3648,24 @@ def show( library_save_options = save_layout_options() # Find the file that calls stack - try: - stk = inspect.getouterframes(inspect.currentframe()) - frame = stk[2] - frame_filename_stem = Path(frame.filename).stem - if frame_filename_stem.startswith(" None: # `layers` hasn't been materialized yet. with contextlib.suppress(AttributeError): del self.layers + self._dbu_cross_section_cache.clear() + self._dbu_cross_section_from_width_cache.clear() _ = self.layers # make sure the layers are computed elif hasattr(self.layout, name): self.layout.__setattr__(name, value) @@ -1710,6 +1718,8 @@ def clear(self, keep_layers: bool = True) -> None: c.locked = False self.layout.clear() self.tkcells = {} + self._dbu_cross_section_cache.clear() + self._dbu_cross_section_from_width_cache.clear() if keep_layers: with contextlib.suppress(AttributeError): @@ -1985,7 +1995,7 @@ def read( if diff.diff_xor.cells() > 0: diff_kcl = KCLayout(self.name + "_XOR") diff_kcl.layout.assign(diff.diff_xor) - show(diff_kcl) + show(diff_kcl, name=f"{self.name}_XOR") err_msg = ( f"Layout {self.name} cannot merge with layout " @@ -2010,7 +2020,6 @@ def read( raise MergeError(err_msg) - cells = set(self.cells("*")) saveopts = save_layout_options() saveopts.gds2_max_cellname_length = ( kdb.SaveLayoutOptions().gds2_max_cellname_length @@ -2025,6 +2034,9 @@ def read( for kdb_cell in locked_cells: kdb_cell.locked = True info, settings = self.get_meta_data() + existing_cells_by_name = { + c.name: c for c in self.layout.each_cell() if not c._destroyed() + } match update_kcl_meta_data: case "overwrite": @@ -2044,12 +2056,13 @@ def read( ", available strategies are 'overwrite', 'skip', or 'drop'" ) meta_format = settings.get("meta_format") or config.meta_format - load_cells = { - cell - for c in layout_b.cells("*") - if (cell := self.layout_cell(c.name)) is not None - } - new_cells = load_cells - cells + load_cells_by_name: dict[str, kdb.Cell] = {} + new_cells: list[kdb.Cell] = [] + for c in layout_b.each_cell(): + if c.name in existing_cells_by_name: + load_cells_by_name[c.name] = c + else: + new_cells.append(c) if register_cells: for c in sorted(new_cells, key=lambda _c: _c.hierarchy_levels()): @@ -2058,8 +2071,8 @@ def read( meta_format=meta_format, ) - for c in load_cells & cells: - kc = self.kcells[c.cell_index()] + for c in load_cells_by_name.values(): + kc = self.kcells[existing_cells_by_name[c.name].cell_index()] kc.get_meta_data(meta_format=meta_format) return lm @@ -2453,6 +2466,45 @@ def get_icross_section( return AsymmetricCrossSection(kcl=self, base=xs) return CrossSection(kcl=self, base=xs) + def get_icross_section_from_width( + self, + width: int, + layer: kdb.LayerInfo, + enclosure: LayerEnclosure | None = None, + ) -> CrossSection: + """Get a cached dbu cross section from legacy width/layer/enclosure args.""" + enclosure_key: tuple[str | None, str] | None + if enclosure is None: + enclosure_key = None + else: + enclosure_key = (enclosure._name, enclosure.unnamed_key) + cache_key = ( + layer.layer, + layer.datatype, + layer.name, + width, + enclosure_key, + ) + xs = self._dbu_cross_section_from_width_cache.get(cache_key) + if xs is not None: + return xs + spec: CrossSectionSpecDict = { + "layer": layer, + "width": width, + "unit": "dbu", + } + if enclosure is not None: + spec["sections"] = [ + (sec_layer, section.d_max) + if section.d_min is None + else (sec_layer, section.d_min, section.d_max) + for sec_layer, layer_section in enclosure.layer_sections.items() + for section in layer_section.sections + ] + xs = self.get_icross_section(spec, symmetrical=True) + self._dbu_cross_section_from_width_cache[cache_key] = xs + return xs + @overload def get_dcross_section( self, @@ -2614,7 +2666,6 @@ def wrapper(*args: Any, **kwargs: Any) -> ProtoTKCell[Any] | VKCell: TKCell.model_rebuild() TVCell.model_rebuild() BasePin.model_rebuild() -BasePort.model_rebuild() BaseKCell.model_rebuild() LayerEnclosureModel.model_rebuild() diff --git a/src/kfactory/merge.py b/src/kfactory/merge.py index 902188198..630cb6e5c 100644 --- a/src/kfactory/merge.py +++ b/src/kfactory/merge.py @@ -37,6 +37,12 @@ class MergeDiff: layout_meta_diff: dict[str, MetaData] = field(init=False) cells_meta_diff: dict[str, dict[str, MetaData]] = field(init=False) kdiff: kdb.LayoutDiff = field(init=False) + _regions_a: dict[int, tuple[tuple[kdb.LayerInfo, kdb.Region], ...]] = field( + init=False, default_factory=dict + ) + _regions_b: dict[int, tuple[tuple[kdb.LayerInfo, kdb.Region], ...]] = field( + init=False, default_factory=dict + ) loglevel: LogLevel | int = field(default=LogLevel.CRITICAL) """Log level at which to log polygon errors.""" @@ -88,39 +94,25 @@ def on_instance_in_a_only(self, instance: kdb.CellInstArray, propid: int) -> Non """Called when there is only an instance in the cell_a.""" if self.loglevel is not None: logger.log(self.loglevel, f"Found {instance=} in {self.name_a} only.") - cell = self.layout_a.cell(instance.cell_index) - - regions: list[kdb.Region] = [] - layers = list(cell.layout().layer_indexes()) - layer_infos = list(cell.layout().layer_infos()) - - for layer in layers: - r = kdb.Region() - r.insert(self.layout_a.cell(instance.cell_index).begin_shapes_rec(layer)) - regions.append(r) - - for trans in instance.each_cplx_trans(): - for li, r in zip(layer_infos, regions, strict=False): - self.cell_a.shapes(self.diff_a.layer(li)).insert(r.transformed(trans)) + _insert_transformed_instance_regions( + source_layout=self.layout_a, + target_layout=self.diff_a, + target_cell=self.cell_a, + instance=instance, + cache=self._regions_a, + ) def on_instance_in_b_only(self, instance: kdb.CellInstArray, propid: int) -> None: """Called when there is only an instance in the cell_b.""" if self.loglevel is not None: logger.log(self.loglevel, f"Found {instance=} in {self.name_b} only.") - cell = self.layout_b.cell(instance.cell_index) - - regions: list[kdb.Region] = [] - layers = list(cell.layout().layer_indexes()) - layer_infos = list(cell.layout().layer_infos()) - - for layer in layers: - r = kdb.Region() - r.insert(self.layout_b.cell(instance.cell_index).begin_shapes_rec(layer)) - regions.append(r) - - for trans in instance.each_cplx_trans(): - for li, r in zip(layer_infos, regions, strict=False): - self.cell_b.shapes(self.diff_b.layer(li)).insert(r.transformed(trans)) + _insert_transformed_instance_regions( + source_layout=self.layout_b, + target_layout=self.diff_b, + target_cell=self.cell_b, + instance=instance, + cache=self._regions_b, + ) def on_polygon_in_b_only(self, poly: kdb.Polygon, propid: int) -> None: """Called when there is only a polygon in the cell_b.""" @@ -190,3 +182,40 @@ def compare(self) -> bool: | kdb.LayoutDiff.IgnoreDuplicates | kdb.LayoutDiff.WithMetaInfo, ) + + +def _insert_transformed_instance_regions( + *, + source_layout: kdb.Layout, + target_layout: kdb.Layout, + target_cell: kdb.Cell, + instance: kdb.CellInstArray, + cache: dict[int, tuple[tuple[kdb.LayerInfo, kdb.Region], ...]], +) -> None: + regions = cache.get(instance.cell_index) + if regions is None: + regions = _collect_recursive_regions(source_layout, instance.cell_index) + cache[instance.cell_index] = regions + + for trans in instance.each_cplx_trans(): + for layer_info, region in regions: + target_cell.shapes(target_layout.layer(layer_info)).insert( + region.transformed(trans) + ) + + +def _collect_recursive_regions( + layout: kdb.Layout, + cell_index: int, +) -> tuple[tuple[kdb.LayerInfo, kdb.Region], ...]: + cell = layout.cell(cell_index) + regions: list[tuple[kdb.LayerInfo, kdb.Region]] = [] + for layer, layer_info in zip( + cell.layout().layer_indexes(), + cell.layout().layer_infos(), + strict=False, + ): + region = kdb.Region() + region.insert(cell.begin_shapes_rec(layer)) + regions.append((layer_info, region)) + return tuple(regions) diff --git a/src/kfactory/port.py b/src/kfactory/port.py index b3bbe8128..443c949f9 100644 --- a/src/kfactory/port.py +++ b/src/kfactory/port.py @@ -10,11 +10,6 @@ from enum import IntEnum, IntFlag, auto from typing import TYPE_CHECKING, Any, Literal, Self, overload -from pydantic import ( - BaseModel, - model_serializer, - model_validator, -) from typing_extensions import TypedDict from . import kdb, rdb @@ -35,7 +30,7 @@ from .utilities import pprint_ports if TYPE_CHECKING: - from collections.abc import Callable, Iterable + from collections.abc import Callable, Iterable, Mapping from .kcell import AnyTKCell, KCell from .layer import LayerEnum @@ -43,6 +38,12 @@ from .typings import Angle, TPort +def _new_info(info: dict[str, Any] | None) -> Info: + if not info: + return Info.model_construct() + return Info(**info) + + def create_port_error( p1: ProtoPort[Any], p2: ProtoPort[Any], @@ -134,7 +135,7 @@ class BasePortDict(TypedDict): port_type: str -class BasePort(BaseModel, arbitrary_types_allowed=True): +class BasePort: """Class representing the base port. This does not have any knowledge of units. Exactly one of @@ -142,16 +143,64 @@ class BasePort(BaseModel, arbitrary_types_allowed=True): must be set, mirroring the `trans` / `dcplx_trans` pattern. """ - name: str - kcl: KCLayout - cross_section: SymmetricalCrossSection | None = None - asymmetric_cross_section: AsymmetricalCrossSection | None = None - trans: kdb.Trans | None = None - dcplx_trans: kdb.DCplxTrans | None = None - info: Info = Info() - port_type: str + __slots__ = ( + "asymmetric_cross_section", + "cross_section", + "dcplx_trans", + "info", + "kcl", + "name", + "port_type", + "trans", + ) + + def __init__( + self, + *, + name: str, + kcl: KCLayout, + cross_section: SymmetricalCrossSection | None = None, + asymmetric_cross_section: AsymmetricalCrossSection | None = None, + trans: kdb.Trans | None = None, + dcplx_trans: kdb.DCplxTrans | None = None, + info: Info | dict[str, Any] | None = None, + port_type: str, + ) -> None: + self.name = name + self.kcl = kcl + self.cross_section = cross_section + self.asymmetric_cross_section = asymmetric_cross_section + self.trans = trans + self.dcplx_trans = dcplx_trans + self.info = info if isinstance(info, Info) else _new_info(info) + self.port_type = port_type + self.check_exclusivity() + + @classmethod + def _construct( + cls, + *, + name: str, + kcl: KCLayout, + cross_section: SymmetricalCrossSection | None = None, + asymmetric_cross_section: AsymmetricalCrossSection | None = None, + trans: kdb.Trans | None = None, + dcplx_trans: kdb.DCplxTrans | None = None, + info: Info | None = None, + port_type: str, + ) -> BasePort: + """Construct a port base after callers have normalized valid fields.""" + base = cls.__new__(cls) + base.name = name + base.kcl = kcl + base.cross_section = cross_section + base.asymmetric_cross_section = asymmetric_cross_section + base.trans = trans + base.dcplx_trans = dcplx_trans + base.info = info if info is not None else Info.model_construct() + base.port_type = port_type + return base - @model_validator(mode="after") def check_exclusivity(self) -> Self: """Check that exactly one trans and exactly one cross_section is set.""" if self.trans is None and self.dcplx_trans is None: @@ -182,32 +231,67 @@ def any_cross_section(self) -> SymmetricalCrossSection | AsymmetricalCrossSectio def __copy__(self) -> BasePort: """Copy the BasePort.""" - return BasePort( + return self._copy() + + def _copy( + self, + *, + update: Mapping[str, Any] | None = None, + copy_info: bool = True, + deep_info: bool = False, + ) -> BasePort: + info = self.info.model_copy(deep=deep_info) if copy_info else self.info + base = BasePort._construct( name=self.name, kcl=self.kcl, cross_section=self.cross_section, asymmetric_cross_section=self.asymmetric_cross_section, trans=self.trans.dup() if self.trans else None, dcplx_trans=self.dcplx_trans.dup() if self.dcplx_trans else None, - info=self.info.model_copy(), + info=info, port_type=self.port_type, ) + if update: + for key, value in update.items(): + setattr(base, key, value) + return base + + def model_copy( + self, *, update: Mapping[str, Any] | None = None, deep: bool = False + ) -> Self: + """Copy the BasePort with duplicated KLayout transforms. + + Pydantic's generic ``model_copy`` is optimized for Python object graphs, but + ports need explicit ``dup()`` calls for mutable KLayout transform objects. + Keeping this as the canonical copy path also makes high-volume routing + copies cheaper than going through pydantic internals. + """ + return self._copy(update=update, deep_info=deep) # ty:ignore[invalid-return-type] def transformed( self, trans: kdb.Trans | kdb.DCplxTrans = kdb.Trans.R0, post_trans: kdb.Trans | kdb.DCplxTrans = kdb.Trans.R0, + *, + copy_info: bool = True, ) -> BasePort: """Get a transformed copy of the BasePort.""" - base = self.__copy__() + info = self.info.model_copy() if copy_info else self.info if ( - base.trans is not None + self.trans is not None and isinstance(trans, kdb.Trans) and isinstance(post_trans, kdb.Trans) ): - base.trans = trans * base.trans * post_trans - base.dcplx_trans = None - return base + return BasePort._construct( + name=self.name, + kcl=self.kcl, + cross_section=self.cross_section, + asymmetric_cross_section=self.asymmetric_cross_section, + trans=trans * self.trans * post_trans, + dcplx_trans=None, + info=info, + port_type=self.port_type, + ) if isinstance(trans, kdb.Trans): trans = kdb.DCplxTrans(trans.to_dtype(self.kcl.dbu)) if isinstance(post_trans, kdb.Trans): @@ -216,9 +300,16 @@ def transformed( t=self.trans.to_dtype(self.kcl.dbu) # ty:ignore[unresolved-attribute] ) - base.trans = None - base.dcplx_trans = trans * dcplx_trans * post_trans - return base + return BasePort._construct( + name=self.name, + kcl=self.kcl, + cross_section=self.cross_section, + asymmetric_cross_section=self.asymmetric_cross_section, + trans=None, + dcplx_trans=trans * dcplx_trans * post_trans, + info=info, + port_type=self.port_type, + ) def transform( self, @@ -247,7 +338,6 @@ def transform( base.dcplx_trans = trans * dcplx_trans * post_trans return self - @model_serializer() def ser_model(self) -> BasePortDict: """Serialize the BasePort.""" trans = self.trans.dup() if self.trans is not None else None @@ -263,6 +353,20 @@ def ser_model(self) -> BasePortDict: port_type=self.port_type, ) + def model_dump(self, *, exclude_none: bool = False, **_: Any) -> dict[str, Any]: + """Serialize the BasePort using the subset of Pydantic's API we rely on.""" + data = dict(self.ser_model()) + if exclude_none: + return {key: value for key, value in data.items() if value is not None} + return data + + def __repr__(self) -> str: + trans = self.trans if self.trans is not None else self.dcplx_trans + return ( + f"BasePort(name={self.name!r}, trans={trans!r}, " + f"port_type={self.port_type!r})" + ) + def get_trans(self) -> kdb.Trans: """Get the transformation.""" if self.trans is not None: @@ -906,13 +1010,11 @@ def __init__( base: BasePort | None = None, ) -> None: """Create a port from dbu or um based units.""" - if info is None: - info = {} if base is not None: self._base = base return if port is not None: - self._base = port.base.__copy__() + self._base = port.base._copy() return if name is None: @@ -920,7 +1022,7 @@ def __init__( "Port must have a name. Only when passing another port or port base" " name can be None." ) - info_ = Info(**info) + info_ = _new_info(info) from .layout import get_default_kcl kcl_ = kcl or get_default_kcl() @@ -949,7 +1051,7 @@ def __init__( sym_xs = cross_section.base if trans is not None: trans_ = kdb.Trans.from_s(trans) if isinstance(trans, str) else trans.dup() - self._base = BasePort( + self._base = BasePort._construct( name=name, kcl=kcl_, cross_section=sym_xs, @@ -963,7 +1065,7 @@ def __init__( dcplx_trans_ = kdb.DCplxTrans.from_s(dcplx_trans) else: dcplx_trans_ = dcplx_trans.dup() - self._base = BasePort( + self._base = BasePort._construct( name=name, kcl=kcl_, cross_section=sym_xs, @@ -976,7 +1078,7 @@ def __init__( elif angle is not None: assert center is not None trans_ = kdb.Trans(angle, mirror_x, *center) - self._base = BasePort( + self._base = BasePort._construct( name=name, kcl=kcl_, cross_section=sym_xs, @@ -1273,13 +1375,11 @@ def __init__( base: BasePort | None = None, ) -> None: """Create a port from dbu or um based units.""" - if info is None: - info = {} if base is not None: self._base = base return if port is not None: - self._base = port.base.__copy__() + self._base = port.base._copy() return if name is None: @@ -1287,7 +1387,7 @@ def __init__( "DPort must have a name. Only when passing another port or port base" " name can be None." ) - info_ = Info(**info) + info_ = _new_info(info) from .layout import get_default_kcl @@ -1322,7 +1422,7 @@ def __init__( sym_xs = cross_section.base if trans is not None: trans_ = kdb.Trans.from_s(trans) if isinstance(trans, str) else trans.dup() - self._base = BasePort( + self._base = BasePort._construct( name=name, kcl=kcl_, cross_section=sym_xs, @@ -1336,7 +1436,7 @@ def __init__( dcplx_trans_ = kdb.DCplxTrans.from_s(dcplx_trans) else: dcplx_trans_ = dcplx_trans.dup() - self._base = BasePort( + self._base = BasePort._construct( name=name, kcl=kcl_, cross_section=sym_xs, @@ -1349,7 +1449,7 @@ def __init__( else: assert center is not None dcplx_trans_ = kdb.DCplxTrans.R0 - self._base = BasePort( + self._base = BasePort._construct( name=name, kcl=kcl_, cross_section=sym_xs, diff --git a/src/kfactory/ports.py b/src/kfactory/ports.py index 91be2c800..d7b9b062b 100644 --- a/src/kfactory/ports.py +++ b/src/kfactory/ports.py @@ -426,9 +426,20 @@ def create_port( layer_info = self.kcl.layout.get_info(layer) assert layer_info is not None try: - xs = self.kcl.get_icross_section( - CrossSectionSpecDict(layer=layer_info, width=width, unit="dbu") + cache_key = ( + layer_info.layer, + layer_info.datatype, + layer_info.name, + width, ) + cached_xs = self.kcl._dbu_cross_section_cache.get(cache_key) + if cached_xs is None: + xs = self.kcl.get_icross_section( + CrossSectionSpecDict(layer=layer_info, width=width, unit="dbu") + ) + self.kcl._dbu_cross_section_cache[cache_key] = xs + else: + xs = cached_xs except ValidationError as e: raise ValueError( "Port width needs to be even to snap to grid properly " @@ -751,7 +762,7 @@ def add_port( equivalent) to `False`. """ if port.kcl == self.kcl: - base = port.base.model_copy() + base = port.base._copy() if not keep_mirror: if base.trans is not None: base.trans.mirror = False @@ -765,7 +776,7 @@ def add_port( dcplx_trans = port.dcplx_trans.dup() if not keep_mirror: dcplx_trans.mirror = False - base = port.base.model_copy() + base = port.base._copy() base.trans = kdb.Trans.R0 base.dcplx_trans = None base.kcl = self.kcl @@ -815,7 +826,7 @@ def copy( self, rename_function: Callable[[Sequence[Port]], None] | None = None ) -> Self: """Get a copy of each port.""" - bases = [b.__copy__() for b in self._bases] + bases = [b._copy() for b in self._bases] if rename_function is not None: rename_function([Port(base=b) for b in bases]) return self.__class__(bases=bases, kcl=self.kcl) @@ -883,7 +894,7 @@ def add_port( equivalent) to `False`. """ if port.kcl == self.kcl: - base = port.base.model_copy() + base = port.base._copy() if not keep_mirror: if base.trans is not None: base.trans.mirror = False @@ -897,7 +908,7 @@ def add_port( dcplx_trans = port.dcplx_trans.dup() if not keep_mirror: dcplx_trans.mirror = False - base = port.base.model_copy() + base = port.base._copy() base.trans = kdb.Trans.R0 base.dcplx_trans = None base.kcl = self.kcl @@ -945,7 +956,7 @@ def copy( self, rename_function: Callable[[Sequence[DPort]], None] | None = None ) -> Self: """Get a copy of each port.""" - bases = [b.__copy__() for b in self._bases] + bases = [b._copy() for b in self._bases] if rename_function is not None: rename_function([DPort(base=b) for b in bases]) return self.__class__(bases=bases, kcl=self.kcl) diff --git a/src/kfactory/routing/electrical.py b/src/kfactory/routing/electrical.py index 3b527cd4c..5bb19a18d 100644 --- a/src/kfactory/routing/electrical.py +++ b/src/kfactory/routing/electrical.py @@ -221,8 +221,8 @@ def route_bundle( if bboxes is None: bboxes = [] - start_ports_ = [p.base.model_copy() for p in start_ports] - end_ports_ = [p.base.model_copy() for p in end_ports] + start_ports_ = [p.base._copy(copy_info=False) for p in start_ports] + end_ports_ = [p.base._copy(copy_info=False) for p in end_ports] if isinstance(c, KCell): try: diff --git a/src/kfactory/routing/generic.py b/src/kfactory/routing/generic.py index 58c93eb50..2435833c6 100644 --- a/src/kfactory/routing/generic.py +++ b/src/kfactory/routing/generic.py @@ -12,6 +12,7 @@ from ..conf import logger from ..instance import Instance # noqa: TC001 from ..port import BasePort, Port, ProtoPort +from ..spatial import collect_instance_region, iter_overlapping_bbox_pairs from ..typings import dbu # noqa: TC001 from .length_functions import LengthFunction, get_length_from_area from .manhattan import ( @@ -174,43 +175,29 @@ def layer_cat(layer_info: kdb.LayerInfo) -> rdb.RdbCategory: layer_ = c.kcl.layout.layer(layer_info) error_region_instances = kdb.Region() error_region_shapes = kdb.Region() + inst_records: list[tuple[Instance, kdb.Box]] = [ + (inst, inst.bbox(layer_)) for inst in insts + ] inst_regions: dict[int, kdb.Region] = {} - inst_region = kdb.Region() shape_region = kdb.Region() for r in shapes_regions: if not (shape_region & r).is_empty(): error_region_shapes.insert(shape_region & r) shape_region.insert(r) - for i, inst in enumerate(insts): - inst_region_ = kdb.Region(inst.bbox(layer_)) - if not (inst_region & inst_region_).is_empty(): - # if inst_shapes is None: - inst_shapes = kdb.Region() - shape_it = c.begin_shapes_rec_overlapping(layer_, inst.bbox(layer_)) - shape_it.select_cells([inst.cell.cell_index()]) - shape_it.min_depth = 1 - for _it in shape_it.each(): - if _it.path()[0].inst() == inst.instance: - inst_shapes.insert( - _it.shape().polygon.transformed(_it.trans()) - ) - for j, _reg in inst_regions.items(): - if _reg & inst_region_: - reg = kdb.Region() - shape_it = c.begin_shapes_rec_touching( - layer_, (_reg & inst_region_).bbox() - ) - shape_it.select_cells([insts[j].cell.cell_index()]) - shape_it.min_depth = 1 - for _it in shape_it.each(): - if _it.path()[0].inst() == insts[j].instance: - reg.insert( - _it.shape().polygon.transformed(_it.trans()) - ) - - error_region_instances.insert(reg & inst_shapes) - inst_region += inst_region_ - inst_regions[i] = inst_region_ + for idx, other_idx in iter_overlapping_bbox_pairs( + [bbox for _, bbox in inst_records] + ): + inst, _ = inst_records[idx] + other_inst, _ = inst_records[other_idx] + inst_region = inst_regions.get(idx) + if inst_region is None: + inst_region = collect_instance_region(c, layer_, inst) + inst_regions[idx] = inst_region + other_region = inst_regions.get(other_idx) + if other_region is None: + other_region = collect_instance_region(c, layer_, other_inst) + inst_regions[other_idx] = other_region + error_region_instances.insert(other_region & inst_region) if not error_region_shapes.is_empty(): any_layer_collision = True diff --git a/src/kfactory/routing/length_functions.py b/src/kfactory/routing/length_functions.py index 14408cdee..992cba63d 100644 --- a/src/kfactory/routing/length_functions.py +++ b/src/kfactory/routing/length_functions.py @@ -47,13 +47,16 @@ def get_length_from_area(layer: kdb.LayerInfo | None = None) -> LengthFunction: """ def get_length_(route: ManhattanRoute) -> float: - if not route.instances: + if not route.instances and not route.polygons: return 0 layer_ = layer or route.start_port.layer_info length: float = 0 width = route.start_port.width + for polygon in route.polygons.get(layer_, ()): + length += polygon.area() / width + for inst in route.instances: length += _get_area_from_layer( inst.cell.kcl.name, inst.cell.cell_index(), layer_, width diff --git a/src/kfactory/routing/manhattan.py b/src/kfactory/routing/manhattan.py index ea95b8478..752a4935f 100644 --- a/src/kfactory/routing/manhattan.py +++ b/src/kfactory/routing/manhattan.py @@ -17,7 +17,6 @@ ) import klayout.db as kdb -import numpy as np from ..conf import ( ANGLE_90, @@ -937,7 +936,7 @@ def route_smart( if bboxes: for box in bboxes: box_region.insert(box) - box_region.merge() + box_region.merge() if sort_ports: if bboxes is None: logger.warning( @@ -1304,6 +1303,7 @@ def route_smart( for r in all_routers ] _router_extra_bbox: list[kdb.Box | None] = [None] * len(all_routers) + _router_index = {id(router): i for i, router in enumerate(all_routers)} _max_overlap_retries = 5 for _retry_attempt in range(_max_overlap_retries): if _retry_attempt > 0: @@ -1665,22 +1665,28 @@ def route_smart( # extend the affected routers' router_bbox via _router_extra_bbox # so the next attempt will bundle them together. _bundle_regions: list[kdb.Region] = [] + _bundle_bboxes: list[kdb.Box] = [] for _bundle in bundled_routers: _region = kdb.Region() + _bbox = kdb.Box() for _router in _bundle: _pts = list(_router.start.pts) + list(reversed(_router.end.pts)) if len(_pts) >= 2: _path = kdb.Path(_pts, _router.width) _region.insert(_path.polygon()) + _bbox += _path.bbox() _bundle_regions.append(_region) + _bundle_bboxes.append(_bbox) _found_overlap = False for _bi in range(len(_bundle_regions)): for _bj in range(_bi + 1, len(_bundle_regions)): + if (_bundle_bboxes[_bi] & _bundle_bboxes[_bj]).empty(): + continue _inter = _bundle_regions[_bi] & _bundle_regions[_bj] if not _inter.is_empty(): _overlap_bbox = _inter.bbox() for _router in bundled_routers[_bi] + bundled_routers[_bj]: - _idx = all_routers.index(_router) + _idx = _router_index[id(_router)] _existing = _router_extra_bbox[_idx] if _existing is None: _router_extra_bbox[_idx] = _overlap_bbox.dup() @@ -2225,63 +2231,50 @@ def _route_to_side( def _sort_route(router: ManhattanRouterSide) -> int: y = (kdb.Trans(-router.t.angle, False, 0, 0) * router.t.disp).y - if clockwise: - return -y - return y + return -y if clockwise else y sorted_rs = sorted(routers, key=_sort_route) for rs in sorted_rs: + t = rs.t hw1 = rs.router.width // 2 hw2 = rs.router.width - hw1 - match rs.t.angle: + br = rs.router.bend90_radius + match t.angle: case 0: - s = ( - bbox.right - + hw1 - + separation - - rs.t.disp.x - - rs.router.bend90_radius - ) + s = bbox.right + hw1 + separation - t.disp.x - br case 1: - s = bbox.top + hw1 + separation - rs.t.disp.y - rs.router.bend90_radius + s = bbox.top + hw1 + separation - t.disp.y - br case 2: - s = ( - rs.t.disp.x - - (bbox.left - hw1 - separation) - - rs.router.bend90_radius - ) + s = t.disp.x - (bbox.left - hw1 - separation) - br case _: - s = ( - rs.t.disp.y - - (bbox.bottom - hw1 - separation) - - rs.router.bend90_radius - ) + s = t.disp.y - (bbox.bottom - hw1 - separation) - br rs.straight(s) tv = rs.tv + ta = rs.ta x = tv.x y = tv.y if clockwise: - match rs.ta: + match ta: case 3: - if x >= rs.router.bend90_radius: + if x >= br: rs.straight_nobend(x) - elif x > -rs.router.bend90_radius and not allow_sbends: - rs.straight(rs.router.bend90_radius + x) + elif x > -br and not allow_sbends: + rs.straight(br + x) case 0 if x > 0: rs.straight(x) - if not (y == 0 and rs.ta == ANGLE_180 and x > 0): + if not (y == 0 and ta == ANGLE_180 and x > 0): rs.left() bbox += rs.t * kdb.Point(0, -hw2) else: - match rs.ta: + match ta: case 1: - if x >= rs.router.bend90_radius: + if x >= br: rs.straight_nobend(x) - elif x > -rs.router.bend90_radius and not allow_sbends: - rs.straight(rs.router.bend90_radius + x) + elif x > -br and not allow_sbends: + rs.straight(br + x) case 0 if x > 0: rs.straight(x) - if not (y == 0 and rs.ta == ANGLE_180 and x > 0): + if not (y == 0 and ta == ANGLE_180 and x > 0): rs.right() bbox += rs.t * kdb.Point(0, hw2) @@ -2804,9 +2797,11 @@ def clean_points( v2 = p_n - p # ty:ignore[unsupported-operator] v1 = p - p_p # ty:ignore[unsupported-operator] - if ( - (np.sign(v1.x) == np.sign(v2.x)) and (np.sign(v1.y) == np.sign(v2.y)) - ) or v2.abs() == 0: + same_direction = (v1.x > 0) == (v2.x > 0) and (v1.x < 0) == (v2.x < 0) + same_direction = ( + same_direction and (v1.y > 0) == (v2.y > 0) and (v1.y < 0) == (v2.y < 0) + ) + if same_direction or v2.abs() == 0: del_points.append(i - 1) else: p_p = p diff --git a/src/kfactory/routing/optical.py b/src/kfactory/routing/optical.py index 3608175c6..063cd340b 100644 --- a/src/kfactory/routing/optical.py +++ b/src/kfactory/routing/optical.py @@ -2,9 +2,20 @@ from __future__ import annotations +import inspect from collections.abc import Sequence from enum import IntEnum -from typing import TYPE_CHECKING, Any, Literal, TypedDict, cast, overload +from functools import partial +from typing import ( + TYPE_CHECKING, + Any, + Literal, + Protocol, + TypedDict, + TypeGuard, + cast, + overload, +) from .. import kdb, rdb from ..conf import ( @@ -17,6 +28,7 @@ from ..instance import Instance, ProtoTInstance from ..instance_group import InstanceGroup, ProtoTInstanceGroup from ..kcell import DKCell, KCell, ProtoTKCell +from ..port import Port from .generic import ManhattanRoute, PlacerFunction, get_radius from .generic import ( route_bundle as route_bundle_generic, @@ -27,10 +39,21 @@ route_manhattan, route_smart, ) +from .route_ports import ( + RoutePort, + port_for_connect, + route_port, +) +from .route_ports import ( + instance_route_port as _instance_route_port, +) +from .route_ports import ( + instance_route_port_by_name as _instance_route_port_by_name, +) from .steps import Step, Straight if TYPE_CHECKING: - from collections.abc import Sequence + from collections.abc import Callable, Sequence from ..factories import ( SBendFactoryDBU, @@ -38,7 +61,7 @@ StraightFactoryDBU, StraightFactoryUM, ) - from ..port import DPort, Port + from ..port import BasePort, DPort, Port from ..schematic import Constraint from ..typings import dbu, um from .utils import RouteDebug @@ -52,6 +75,56 @@ "vec_angle", ] +_PENDING_POLYGON_REGIONS: dict[int, dict[kdb.LayerInfo, kdb.Region]] = {} +_PENDING_POLYGON_HOLES: dict[int, dict[kdb.LayerInfo, kdb.Region]] = {} + + +class _HasRoutingFastFactory(Protocol): + routing_fast_factory: StraightFactoryDBU + + +class _RoutingFastParameterFactoryDBU(Protocol): + def __call__( + self, width: int, length: int, routing_fast: bool = False + ) -> ProtoTKCell[Any]: ... + + +class _RoutingFastParameterFactoryUM(Protocol): + def __call__( + self, width: float, length: float, routing_fast: bool = False + ) -> ProtoTKCell[Any]: ... + + +class _RoutingStraightFactoryDBU(Protocol): + supports_routing_fast: bool + supports_polygon_materialization: bool + + def __call__( + self, width: int, length: int, routing_fast: bool = False + ) -> KCell: ... + + +class _CachedRoutingStraightFactoryDBU: + def __init__( + self, + make_cell: Callable[[int, int, bool], KCell], + *, + supports_routing_fast: bool, + supports_polygon_materialization: bool, + ) -> None: + self._make_cell = make_cell + self._cache: dict[tuple[int, int], KCell] = {} + self.supports_routing_fast = supports_routing_fast + self.supports_polygon_materialization = supports_polygon_materialization + + def __call__(self, width: int, length: int, routing_fast: bool = False) -> KCell: + key = (width, length) + straight_cell = self._cache.get(key) + if straight_cell is None: + straight_cell = self._make_cell(width, length, routing_fast) + self._cache[key] = straight_cell + return straight_cell + class LoopSide(IntEnum): left = -1 @@ -73,6 +146,62 @@ class PathLengthConfig[T: (int, float)](TypedDict, total=False): total_length: int +def _get_routing_fast_straight_factory( + straight_factory: object, +) -> StraightFactoryDBU | None: + if _has_routing_fast_factory(straight_factory): + return straight_factory.routing_fast_factory + if isinstance(straight_factory, partial) and _has_routing_fast_factory( + straight_factory.func + ): + return partial( + straight_factory.func.routing_fast_factory, + *straight_factory.args, + **(straight_factory.keywords or {}), + ) + return None + + +def _has_routing_fast_factory(factory: object) -> TypeGuard[_HasRoutingFastFactory]: + return hasattr(factory, "routing_fast_factory") + + +def _accepts_routing_fast_parameter_dbu( + factory: Callable[..., object], +) -> TypeGuard[_RoutingFastParameterFactoryDBU]: + try: + return "routing_fast" in inspect.signature(factory).parameters + except (TypeError, ValueError): + return False + + +def _accepts_routing_fast_parameter_um( + factory: Callable[..., object], +) -> TypeGuard[_RoutingFastParameterFactoryUM]: + try: + return "routing_fast" in inspect.signature(factory).parameters + except (TypeError, ValueError): + return False + + +def _supports_routing_fast_factory( + factory: StraightFactoryDBU, +) -> TypeGuard[_RoutingStraightFactoryDBU]: + return getattr(factory, "supports_routing_fast", False) is True + + +def _supports_polygon_materialization_factory( + factory: StraightFactoryDBU, +) -> TypeGuard[_RoutingStraightFactoryDBU]: + return getattr(factory, "supports_polygon_materialization", False) is True + + +def _expect_kcell(cell: ProtoTKCell[Any], context: str) -> KCell: + if not isinstance(cell, KCell): + raise TypeError(f"{context} must return a KCell, got {type(cell).__name__}") + return cell + + def path_length_match( routers: Sequence[ManhattanRouter], element: int = -1, @@ -441,8 +570,8 @@ def route_bundle( if bboxes is None: bboxes = [] bend90_radius = get_radius(bend90_cell.ports.filter(port_type=place_port_type)) - start_ports_ = [p.base.model_copy() for p in start_ports] - end_ports_ = [p.base.model_copy() for p in end_ports] + start_ports_ = [p.base._copy(copy_info=False) for p in start_ports] + end_ports_ = [p.base._copy(copy_info=False) for p in end_ports] if sbend_factory is None: placer: PlacerFunction = place_manhattan placer_kwargs: dict[str, Any] = { @@ -476,6 +605,52 @@ def route_bundle( "sbend_factory": sbend_factory, } if isinstance(c, KCell): + _raw_routing_fast_straight_factory = _get_routing_fast_straight_factory( + straight_factory + ) + _routing_fast_parameter_factory = ( + straight_factory + if _accepts_routing_fast_parameter_dbu(straight_factory) + else None + ) + + if ( + _raw_routing_fast_straight_factory is not None + or _routing_fast_parameter_factory is not None + ): + + def _make_straight_cell( + width: int, length: int, routing_fast: bool + ) -> KCell: + if _raw_routing_fast_straight_factory is not None: + return _expect_kcell( + _raw_routing_fast_straight_factory( + width=width, + length=length, + ), + "routing_fast_factory", + ) + if _routing_fast_parameter_factory is not None: + return _expect_kcell( + _routing_fast_parameter_factory( + width=width, + length=length, + routing_fast=True, + ), + "straight_factory(routing_fast=True)", + ) + return _expect_kcell( + straight_factory(width=width, length=length), "straight_factory" + ) + + placer_kwargs["straight_factory"] = _CachedRoutingStraightFactoryDBU( + _make_straight_cell, + supports_routing_fast=True, + supports_polygon_materialization=( + _raw_routing_fast_straight_factory is not None + ), + ) + try: return route_bundle_generic( c=c, @@ -589,12 +764,31 @@ def route_bundle( ends = [c.kcl.to_dbu(cast("int|float", end)) for end in ends] ends = cast("int | list[int] | list[Step] | list[list[Step]]", ends) - def _straight_factory(width: int, length: int) -> KCell: - dkc = cast("StraightFactoryUM", straight_factory)( - width=c.kcl.to_um(width), length=c.kcl.to_um(length) - ) + _routing_fast_parameter_factory_um = ( + straight_factory + if _accepts_routing_fast_parameter_um(straight_factory) + else None + ) + + def _make_straight_cell(width: int, length: int, routing_fast: bool) -> KCell: + if _routing_fast_parameter_factory_um is not None: + dkc = _routing_fast_parameter_factory_um( + width=c.kcl.to_um(width), + length=c.kcl.to_um(length), + routing_fast=True, + ) + else: + dkc = cast("StraightFactoryUM", straight_factory)( + width=c.kcl.to_um(width), length=c.kcl.to_um(length) + ) return c.kcl[dkc.cell_index()] + _straight_factory = _CachedRoutingStraightFactoryDBU( + _make_straight_cell, + supports_routing_fast=_routing_fast_parameter_factory_um is not None, + supports_polygon_materialization=False, + ) + bend90_cell = c.kcl[bend90_cell.cell_index()] if taper_cell is not None: taper_cell = c.kcl[taper_cell.cell_index()] @@ -736,46 +930,300 @@ def _place_straight( purpose: str | None, w: int, route: ManhattanRoute, - p1: Port, - p2: Port, + p1: Port | RoutePort, + p2: Port | RoutePort, route_width: int | None, *, port_type: str, allow_width_mismatch: bool, allow_layer_mismatch: bool, allow_type_mismatch: bool, -) -> tuple[Port, Port]: - length = int((p1.trans.disp.to_p() - p2.trans.disp.to_p()).length()) - wg = c << straight_factory(width=w, length=length) +) -> tuple[RoutePort, RoutePort]: + p1_route = route_port(p1) + p2_route = route_port(p2) + length = abs(p1_route.trans.disp.x - p2_route.trans.disp.x) + abs( + p1_route.trans.disp.y - p2_route.trans.disp.y + ) + if _supports_routing_fast_factory(straight_factory): + ports = _place_straight_polygon( + c=c, + straight_factory=straight_factory, + w=w, + length=length, + route=route, + p1=p1_route, + p2=p2_route, + port_type=port_type, + ) + if ports is not None: + route.length_straights += length + return ports + wg = c << straight_factory(width=w, length=length, routing_fast=True) + else: + wg = c << straight_factory(width=w, length=length) wg.purpose = purpose - wg_p1, _ = (v for v in wg.ports if v.port_type == port_type) - wg.connect( - wg_p1, - p1, - allow_width_mismatch=route_width is not None or allow_width_mismatch, + allow_width_mismatch = route_width is not None or allow_width_mismatch + _connect_straight_instance( + wg, + p1_route, + allow_width_mismatch=allow_width_mismatch, allow_layer_mismatch=allow_layer_mismatch, allow_type_mismatch=allow_type_mismatch, ) - wg_p1, wg_p2 = (v for v in wg.ports if v.port_type == port_type) + wg_p1 = _instance_route_port(wg, 0) + wg_p2 = _instance_route_port(wg, 1) + if wg_p1.port_type != port_type or wg_p2.port_type != port_type: + raise ValueError(f"straight_factory returned unexpected ports for {port_type=}") route.instances.append(wg) route.length_straights += length return wg_p1, wg_p2 +def _place_straight_polygon( + c: KCell, + straight_factory: StraightFactoryDBU, + w: int, + length: int, + route: ManhattanRoute, + p1: RoutePort, + p2: RoutePort, + *, + port_type: str, +) -> tuple[RoutePort, RoutePort] | None: + if not _supports_polygon_materialization_factory(straight_factory): + return None + if ( + not p1.is_dbu + or not p2.is_dbu + or p1.width != w + or p2.width != w + or p1.port_type != port_type + or p2.port_type != port_type + ): + return None + + if not _is_manhattan(p2.trans.disp - p1.trans.disp): + return None + + straight_cell = straight_factory(width=w, length=length) + xs = straight_cell._base.ports[0].cross_section + if xs is None: + return None + if not p1.base.any_cross_section.main_layer.is_equivalent( + xs.main_layer + ) or not p2.base.any_cross_section.main_layer.is_equivalent(xs.main_layer): + return None + + _queue_straight_cross_section_polygons(route, xs, p1, p2) + return ( + RoutePort(base=p1.base, trans=p1.trans * kdb.Trans.R180, dbu=True), + RoutePort(base=p2.base, trans=p2.trans * kdb.Trans.R180, dbu=True), + ) + + +def _queue_straight_cross_section_polygons( + route: ManhattanRoute, + xs: Any, + p1: RoutePort, + p2: RoutePort, +) -> None: + points = [p1.trans.disp.to_p(), p2.trans.disp.to_p()] + route_id = id(route) + pending_regions = _PENDING_POLYGON_REGIONS.setdefault(route_id, {}) + pending_holes = _PENDING_POLYGON_HOLES.setdefault(route_id, {}) + + _queue_path_region(pending_regions, xs.main_layer, points, xs.width) + for layer, layer_section in xs.enclosure.layer_sections.items(): + for section in layer_section.sections: + _queue_path_region( + pending_regions, + layer, + points, + xs.width + 2 * section.d_max, + ) + if section.d_min is not None: + inner_width = xs.width + 2 * section.d_min + if inner_width > 0: + _queue_path_region( + pending_holes, + layer, + points, + inner_width, + ) + + +def _queue_path_region( + regions: dict[kdb.LayerInfo, kdb.Region], + layer: kdb.LayerInfo, + points: list[kdb.Point], + width: int, +) -> None: + region = regions.get(layer) + if region is None: + region = kdb.Region() + regions[layer] = region + region.insert(kdb.Path(points, width)) + + +def _insert_route_polygons(c: KCell, route: ManhattanRoute) -> None: + route_id = id(route) + pending_regions = _PENDING_POLYGON_REGIONS.pop(route_id, None) + if not pending_regions: + return + pending_holes = _PENDING_POLYGON_HOLES.pop(route_id, {}) + for layer, pending_region in pending_regions.items(): + holes = pending_holes.get(layer) + region = pending_region + if holes is not None: + region -= holes + region.merge() + c.shapes(c.kcl.layer(layer)).insert(region) + route.polygons.setdefault(layer, []).extend(region.each()) + + +def _connect_straight_instance( + wg: Instance, + target: RoutePort, + *, + allow_width_mismatch: bool, + allow_layer_mismatch: bool, + allow_type_mismatch: bool, +) -> None: + local_base = wg.cell._base.ports[0] + target_base = target.base + + if _supports_direct_straight_connect( + local_base, target + ) and _ports_match_for_direct_connect( + local_base, + target_base, + allow_width_mismatch=allow_width_mismatch, + allow_layer_mismatch=allow_layer_mismatch, + allow_type_mismatch=allow_type_mismatch, + ): + _directly_connect_straight(wg, local_base, target) + return + + _connect_instance_with_checks( + wg, + local_base, + target, + allow_width_mismatch=allow_width_mismatch, + allow_layer_mismatch=allow_layer_mismatch, + allow_type_mismatch=allow_type_mismatch, + ) + + +def _supports_direct_straight_connect( + local_base: BasePort, + target: RoutePort, +) -> bool: + """Whether routing can apply the default `Instance.connect` transform directly.""" + target_base = target.base + return ( + config.connect_use_mirror + and config.connect_use_angle + and local_base.trans is not None + and target.is_dbu + and local_base.dcplx_trans is None + and target_base.dcplx_trans is None + and local_base.asymmetric_cross_section is None + and target_base.asymmetric_cross_section is None + ) + + +def _ports_match_for_direct_connect( + local_base: BasePort, + target_base: BasePort, + *, + allow_width_mismatch: bool, + allow_layer_mismatch: bool, + allow_type_mismatch: bool, +) -> bool: + """Mirror the cheap compatibility checks needed before direct connection.""" + local_xs = local_base.any_cross_section + target_xs = target_base.any_cross_section + return ( + (local_base.is_symmetric() == target_base.is_symmetric()) + and (local_xs.width == target_xs.width or allow_width_mismatch) + and ( + local_xs.main_layer.is_equivalent(target_xs.main_layer) + or allow_layer_mismatch + ) + and (local_base.port_type == target_base.port_type or allow_type_mismatch) + ) + + +def _directly_connect_straight( + wg: Instance, + local_base: BasePort, + target: RoutePort, +) -> None: + """Apply the same transform as `Instance.connect(..., mirror=False)`.""" + assert local_base.trans is not None + wg.trans = target.trans * kdb.Trans.R180 * local_base.trans.inverted() + + +def _connect_instance_with_checks( + wg: Instance, + local_base: BasePort, + target: Port | RoutePort, + *, + allow_width_mismatch: bool, + allow_layer_mismatch: bool, + allow_type_mismatch: bool, +) -> None: + wg.connect( + Port(base=local_base), + port_for_connect(target), + allow_width_mismatch=allow_width_mismatch, + allow_layer_mismatch=allow_layer_mismatch, + allow_type_mismatch=allow_type_mismatch, + ) + + +def _copy_port_for_placement( + port: Port | RoutePort, + post_trans: kdb.Trans = kdb.Trans.R0, +) -> Port: + return Port( + base=port_for_connect(port).base.transformed( + post_trans=post_trans, + copy_info=False, + ) + ) + + +def _copy_polar_for_placement( + port: Port | RoutePort, + d: int = 0, + d_orth: int = 0, + angle: int = 2, + mirror: bool = False, +) -> Port: + return _copy_port_for_placement(port, kdb.Trans(angle, mirror, d, d_orth)) + + +def _copy_route_endpoint(port: Port | RoutePort) -> Port: + if isinstance(port, RoutePort): + return port.to_port() + return Port(base=port.base.transformed(copy_info=False)) + + def _place_sbend( c: KCell, sbend_factory: SBendFactoryDBU, purpose: str | None, w: int, route: ManhattanRoute, - p1: Port, - p2: Port, + p1: Port | RoutePort, + p2: Port | RoutePort, *, allow_width_mismatch: bool, allow_layer_mismatch: bool, allow_type_mismatch: bool, ) -> tuple[Port, Port]: - p1_ = p1.copy() + p1_ = _copy_port_for_placement(p1) p1_.trans.mirror = False delta_p = p1_.trans.inverted() * p2.trans.disp.to_p() @@ -796,7 +1244,7 @@ def _place_sbend( ) sp1, sp2 = sbend_group.ports[0], sbend_group.ports[1] - sp1_ = sp1.copy_polar() + sp1_ = _copy_polar_for_placement(sp1) sp1_.trans.mirror = False sbg_delta_p = sp1_.trans.inverted() * sp2.trans.disp.to_p() if delta_p.y == sbg_delta_p.y: @@ -831,8 +1279,8 @@ def _place_tapered_straight( taper_cell: KCell, purpose: str | None, route: ManhattanRoute, - p1: Port, - p2: Port, + p1: Port | RoutePort, + p2: Port | RoutePort, route_width: int | None, taper_ports: tuple[Port, Port], *, @@ -840,14 +1288,16 @@ def _place_tapered_straight( allow_width_mismatch: bool, allow_layer_mismatch: bool, allow_type_mismatch: bool, -) -> tuple[Port, Port]: +) -> tuple[RoutePort, RoutePort]: taperp1, taperp2 = taper_ports - length = int((p1.trans.disp.to_p() - p2.trans.disp.to_p()).length()) + p1_route = route_port(p1) + p2_route = route_port(p2) + length = int((p1_route.trans.disp.to_p() - p2_route.trans.disp.to_p()).length()) t1 = c << taper_cell t1.purpose = purpose t1.connect( taperp1.name, - p1, + port_for_connect(p1), allow_width_mismatch=route_width is not None or allow_width_mismatch, allow_layer_mismatch=allow_layer_mismatch, allow_type_mismatch=allow_type_mismatch, @@ -857,7 +1307,7 @@ def _place_tapered_straight( t2.purpose = purpose t2.connect( taperp1.name, - p2, + port_for_connect(p2), allow_width_mismatch=route_width is not None or allow_width_mismatch, allow_layer_mismatch=allow_layer_mismatch, allow_type_mismatch=allow_type_mismatch, @@ -866,8 +1316,8 @@ def _place_tapered_straight( route.n_taper += 2 l_ = int(length - (taperp1.trans.disp - taperp2.trans.disp).length() * 2) if l_ != 0: - p1_ = t1.ports[taperp2.name] - p2_ = t2.ports[taperp2.name] + p1_ = _instance_route_port_by_name(t1, taperp2.name) + p2_ = _instance_route_port_by_name(t2, taperp2.name) _place_straight( c=c, straight_factory=straight_factory, @@ -883,7 +1333,10 @@ def _place_tapered_straight( allow_type_mismatch=allow_type_mismatch, ) - return t1.ports[taperp1.name], t2.ports[taperp1.name] + return ( + _instance_route_port_by_name(t1, taperp1.name), + _instance_route_port_by_name(t2, taperp1.name), + ) def _place_tapered_sbend_or_straight( @@ -892,8 +1345,8 @@ def _place_tapered_sbend_or_straight( taper_cell: KCell, purpose: str | None, route: ManhattanRoute, - p1: Port, - p2: Port, + p1: Port | RoutePort, + p2: Port | RoutePort, route_width: int | None, taper_ports: tuple[Port, Port], *, @@ -902,12 +1355,14 @@ def _place_tapered_sbend_or_straight( allow_type_mismatch: bool, ) -> tuple[Port, Port]: taperp1, taperp2 = taper_ports - length = int((p1.trans.disp.to_p() - p2.trans.disp.to_p()).length()) + p1_route = route_port(p1) + p2_route = route_port(p2) + length = int((p1_route.trans.disp.to_p() - p2_route.trans.disp.to_p()).length()) t1 = c << taper_cell t1.purpose = purpose t1.connect( taperp1.name, - p1, + port_for_connect(p1), allow_width_mismatch=route_width is not None or allow_width_mismatch, allow_layer_mismatch=allow_layer_mismatch, allow_type_mismatch=allow_type_mismatch, @@ -917,7 +1372,7 @@ def _place_tapered_sbend_or_straight( t2.purpose = purpose t2.connect( taperp1.name, - p2, + port_for_connect(p2), allow_width_mismatch=route_width is not None or allow_width_mismatch, allow_layer_mismatch=allow_layer_mismatch, allow_type_mismatch=allow_type_mismatch, @@ -984,8 +1439,8 @@ def place_manhattan( "place_manhattan needs to be passed a fixed bend90 cell with two optical" " ports which are 90° apart from each other with port_type 'port_type'." ) - route_start_port = p1.copy() - route_end_port = p2.copy() + route_start_port = _copy_port_for_placement(p1) + route_end_port = _copy_port_for_placement(p2) if p1.base.trans is None: logger.warning( f"{p1=} is not a manhattan port (either off-grid or angle not a multiple of" @@ -1065,7 +1520,7 @@ def place_manhattan( " the bend's ports" ) route = ManhattanRoute( - backbone=list(pts).copy(), + backbone=list(pts), start_port=route_start_port, end_port=route_end_port, instances=[], @@ -1074,7 +1529,7 @@ def place_manhattan( ) else: route = ManhattanRoute( - backbone=list(pts).copy(), + backbone=list(pts), start_port=route_start_port, end_port=route_end_port, instances=[], @@ -1101,8 +1556,8 @@ def place_manhattan( purpose=purpose, w=w, route=route, - p1=route.start_port.copy_polar(), - p2=route.end_port.copy_polar(), + p1=p1, + p2=p2, route_width=w, port_type=port_type, allow_width_mismatch=allow_width_mismatch, @@ -1116,8 +1571,8 @@ def place_manhattan( purpose=purpose, taper_ports=(taperp1, taperp2), route=route, - p1=route.start_port.copy_polar(), - p2=route.end_port.copy_polar(), + p1=p1, + p2=p2, route_width=w, port_type=port_type, allow_width_mismatch=allow_width_mismatch, @@ -1125,10 +1580,9 @@ def place_manhattan( allow_type_mismatch=allow_type_mismatch, taper_cell=taper_cell, ) - p1_.name = "route_start" - p2_.name = "route_end" route.start_port = p1 route.end_port = p2 + _insert_route_polygons(c, route) return route # in other cases, place the bend and then route @@ -1170,7 +1624,7 @@ def place_manhattan( f"The vector between manhattan points is not manhattan {old_pt}, {pt}" ) bend90.transform(kdb.Trans(ang, mirror, pt.x, pt.y) * b90c.inverted()) - new_bend_port = bend90.ports[b90p1.name] + new_bend_port = _instance_route_port_by_name(bend90, b90p1.name) length = int((new_bend_port.trans.disp - old_bend_port.trans.disp).length()) if length > 0: if ( @@ -1210,11 +1664,11 @@ def place_manhattan( allow_type_mismatch=allow_type_mismatch, ) if i == 1: - route.start_port = p1_ + route.start_port = _copy_route_endpoint(p1_) route.instances.append(bend90) old_pt = pt - old_bend_port = bend90.ports[b90p2.name] - length = int((bend90.ports[b90p2.name].trans.disp - p2.trans.disp).length()) + old_bend_port = _instance_route_port_by_name(bend90, b90p2.name) + length = int((old_bend_port.trans.disp - p2.trans.disp).length()) if length > 0: if ( taper_cell is None @@ -1252,11 +1706,12 @@ def place_manhattan( allow_layer_mismatch=allow_layer_mismatch, allow_type_mismatch=allow_type_mismatch, ) - route.end_port = p2_.copy() + route.end_port = _copy_route_endpoint(p2_) else: - route.end_port = old_bend_port.copy() + route.end_port = _copy_route_endpoint(old_bend_port) route.start_port.name = "route_start" route.end_port.name = "route_end" + _insert_route_polygons(c, route) return route @@ -1307,10 +1762,10 @@ def place_manhattan_with_sbends( raise ValueError( "place_manhattan_with_sbends needs to be passed a sbend_function." ) - route_start_port = p1.copy() + route_start_port = _copy_port_for_placement(p1) route_start_port.name = "route_start" route_start_port.trans.angle = (route_start_port.angle + 2) % 4 - route_end_port = p2.copy() + route_end_port = _copy_port_for_placement(p2) route_end_port.name = "route_end" route_end_port.trans.angle = (route_end_port.angle + 2) % 4 @@ -1373,7 +1828,7 @@ def place_manhattan_with_sbends( " the bend's ports" ) route = ManhattanRoute( - backbone=list(pts).copy(), + backbone=list(pts), start_port=route_start_port, end_port=route_end_port, instances=[], @@ -1382,7 +1837,7 @@ def place_manhattan_with_sbends( ) else: route = ManhattanRoute( - backbone=list(pts).copy(), + backbone=list(pts), start_port=route_start_port, end_port=route_end_port, instances=[], @@ -1406,7 +1861,9 @@ def place_manhattan_with_sbends( w=w, route=route, p1=old_bend_port, - p2=old_bend_port.copy_polar(d=sbend_vec.x, d_orth=sbend_vec.y, angle=2), + p2=_copy_polar_for_placement( + old_bend_port, d=sbend_vec.x, d_orth=sbend_vec.y, angle=2 + ), allow_width_mismatch=allow_width_mismatch, allow_layer_mismatch=allow_layer_mismatch, allow_type_mismatch=allow_type_mismatch, @@ -1425,8 +1882,8 @@ def place_manhattan_with_sbends( purpose=purpose, w=w, route=route, - p1=route.start_port.copy_polar(), - p2=route.end_port.copy_polar(), + p1=p1, + p2=p2, route_width=w, port_type=port_type, allow_width_mismatch=allow_width_mismatch, @@ -1440,8 +1897,8 @@ def place_manhattan_with_sbends( purpose=purpose, taper_ports=(taperp1, taperp2), route=route, - p1=route.start_port.copy_polar(), - p2=route.end_port.copy_polar(), + p1=p1, + p2=p2, route_width=w, port_type=port_type, allow_width_mismatch=allow_width_mismatch, @@ -1453,6 +1910,7 @@ def place_manhattan_with_sbends( p2.name = "route_end" route.start_port = p1 route.end_port = p2 + _insert_route_polygons(c, route) return route # in other cases, place the bend and then route @@ -1464,8 +1922,8 @@ def place_manhattan_with_sbends( vec = pt - old_pt if _is_sbend_vec(vec): sbend_vec = (kdb.Trans(-old_angle, False, 0, 0) * vec.to_p()).to_v() - bend_port = old_bend_port.copy_polar( - d=sbend_vec.x, d_orth=sbend_vec.y, angle=2 + bend_port = _copy_polar_for_placement( + old_bend_port, d=sbend_vec.x, d_orth=sbend_vec.y, angle=2 ) p1_, p2_ = _place_sbend( c=c, @@ -1482,13 +1940,13 @@ def place_manhattan_with_sbends( old_pt = pt old_bend_port = p2_ if i == 1: - route.start_port = p1_ + route.start_port = _copy_route_endpoint(p1_) continue vec_n = new_pt - pt if _is_sbend_vec(vec_n): - new_bend_port = old_bend_port.copy_polar(int(vec.length())) + new_bend_port = _copy_polar_for_placement(old_bend_port, int(vec.length())) length = int((new_bend_port.trans.disp - old_bend_port.trans.disp).length()) if length > 0: if ( @@ -1562,7 +2020,7 @@ def place_manhattan_with_sbends( f"The vector between manhattan points is not manhattan {old_pt}, {pt}" ) bend90.transform(kdb.Trans(ang, mirror, pt.x, pt.y) * b90c.inverted()) - new_bend_port = bend90.ports[b90p1.name] + new_bend_port = _instance_route_port_by_name(bend90, b90p1.name) length = int((new_bend_port.trans.disp - old_bend_port.trans.disp).length()) if length > 0: if ( @@ -1602,14 +2060,16 @@ def place_manhattan_with_sbends( allow_type_mismatch=allow_type_mismatch, ) if i == 1: - route.start_port = p1_ + route.start_port = _copy_route_endpoint(p1_) route.instances.append(bend90) old_pt = pt - old_bend_port = bend90.ports[b90p2.name] + old_bend_port = _instance_route_port_by_name(bend90, b90p2.name) vec = pts[-1] - pts[-2] if _is_sbend_vec(vec): sbend_vec = (old_bend_port.trans.inverted() * pts[-1]).to_v() - bend_port = old_bend_port.copy_polar(d=sbend_vec.x, d_orth=sbend_vec.y, angle=2) + bend_port = _copy_polar_for_placement( + old_bend_port, d=sbend_vec.x, d_orth=sbend_vec.y, angle=2 + ) _place_sbend( c=c, sbend_factory=sbend_factory, @@ -1662,11 +2122,12 @@ def place_manhattan_with_sbends( allow_layer_mismatch=allow_layer_mismatch, allow_type_mismatch=allow_type_mismatch, ) - route.end_port = p2_.copy() + route.end_port = _copy_route_endpoint(p2_) else: - route.end_port = old_bend_port.copy() + route.end_port = _copy_route_endpoint(old_bend_port) route.start_port.name = "route_start" route.end_port.name = "route_end" + _insert_route_polygons(c, route) return route diff --git a/src/kfactory/routing/route_ports.py b/src/kfactory/routing/route_ports.py new file mode 100644 index 000000000..c76ef6df4 --- /dev/null +++ b/src/kfactory/routing/route_ports.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from ..port import BasePort, Port + +if TYPE_CHECKING: + from .. import kdb + from ..instance import Instance + + +class RoutePort: + __slots__ = ("_dbu", "_materialized_base", "base", "trans") + + def __init__( + self, + *, + base: BasePort, + trans: kdb.Trans, + dbu: bool, + materialized_base: BasePort | None = None, + ) -> None: + self.base = base + self.trans = trans + self._dbu = dbu + self._materialized_base = materialized_base + + @classmethod + def from_port(cls, port: Port) -> RoutePort: + base = port.base + if base.trans is not None: + return cls(base=base, trans=base.trans, dbu=True) + return cls( + base=base, + trans=base.get_trans(), + dbu=False, + materialized_base=base, + ) + + @classmethod + def from_instance_port(cls, inst: Instance, base: BasePort) -> RoutePort: + if base.trans is not None: + return cls(base=base, trans=inst.trans * base.trans, dbu=True) + materialized_base = base.transformed(inst.trans, copy_info=False) + return cls( + base=materialized_base, + trans=materialized_base.get_trans(), + dbu=False, + materialized_base=materialized_base, + ) + + @property + def is_dbu(self) -> bool: + return self._dbu + + @property + def name(self) -> str | None: + return self.base.name + + @property + def port_type(self) -> str: + return self.base.port_type + + @property + def width(self) -> int: + return self.base.any_cross_section.width + + @property + def angle(self) -> int: + return self.trans.angle + + def to_port(self, *, name: str | None = None, copy_info: bool = False) -> Port: + if self._materialized_base is not None: + base = self._materialized_base._copy(copy_info=copy_info) + if name is not None: + base.name = name + return Port(base=base) + return Port( + base=BasePort._construct( + name=self.base.name if name is None else name, + kcl=self.base.kcl, + cross_section=self.base.cross_section, + asymmetric_cross_section=self.base.asymmetric_cross_section, + trans=self.trans.dup(), + info=self.base.info.model_copy() if copy_info else self.base.info, + port_type=self.base.port_type, + ) + ) + + +def route_port(port: Port | RoutePort) -> RoutePort: + if isinstance(port, RoutePort): + return port + return RoutePort.from_port(port) + + +def port_for_connect(port: Port | RoutePort) -> Port: + if isinstance(port, RoutePort): + return port.to_port() + return port + + +def instance_route_port(inst: Instance, port_index: int) -> RoutePort: + return RoutePort.from_instance_port(inst, inst.cell._base.ports[port_index]) + + +def instance_route_port_by_name(inst: Instance, name: str | None) -> RoutePort: + return RoutePort.from_instance_port(inst, inst.cell.ports[name].base) diff --git a/src/kfactory/schematic.py b/src/kfactory/schematic.py index 894b96a81..d7678e385 100644 --- a/src/kfactory/schematic.py +++ b/src/kfactory/schematic.py @@ -1621,9 +1621,24 @@ def create_cell( connections = self.connections - islands, instance_connections = _get_island_connections( - instances=self.instances, connections=connections - ) + dbu_schematic: Schematic | None = None + dbu_instance_connections: defaultdict[str, list[Connection[int]]] | None = None + um_schematic: DSchematic | None = None + um_instance_connections: defaultdict[str, list[Connection[float]]] | None = None + if _is_int_schematic(self): + dbu_schematic = self + islands, dbu_instance_connections = _get_island_connections( + instances=dbu_schematic.instances, + connections=dbu_schematic.connections, + ) + elif _is_um_schematic(self): + um_schematic = self + islands, um_instance_connections = _get_island_connections( + instances=um_schematic.instances, + connections=um_schematic.connections, + ) + else: + raise ValueError(f"Unsupported schematic unit {self.unit!r}") placed_insts: set[str] = set() placed_ports: set[str] = set() @@ -1651,23 +1666,43 @@ def create_cell( for i, island in enumerate(unique_islands): logger.debug("Placing island {} of schema {}, {}", i, self.name, island) if island not in placed_islands: - _place_island( - c, - schematic_island=island, - instances=instances, - connections=instance_connections, - schematic_instances=self.instances, # ty:ignore[invalid-argument-type] - placed_insts=placed_insts, - placed_ports=placed_ports, - schematic=self, # ty:ignore[invalid-argument-type] - cross_sections=cross_sections, - factories=factories, - place_unknown=place_unknown, - ) + if dbu_schematic is not None and dbu_instance_connections is not None: + _place_dbu_island( + c, + schematic_island=island, + instances=instances, + connections=dbu_instance_connections, + placed_insts=placed_insts, + placed_ports=placed_ports, + schematic=dbu_schematic, + cross_sections=cross_sections, + factories=factories, + place_unknown=place_unknown, + ) + elif um_schematic is not None and um_instance_connections is not None: + _place_um_island( + c, + schematic_island=island, + instances=instances, + connections=um_instance_connections, + placed_insts=placed_insts, + placed_ports=placed_ports, + schematic=um_schematic, + cross_sections=cross_sections, + factories=factories, + place_unknown=place_unknown, + ) + else: + raise ValueError(f"Unsupported schematic unit {self.unit!r}") placed_islands.append(island) placed_insts |= island nets_per_route = self.routes_nets() + constraints_by_route_name: dict[str, list[Constraint]] = defaultdict(list) + for ct in self.constraints: + for route_name in ct.route_names: + constraints_by_route_name[route_name].append(ct) + is_kcell_output = issubclass(output_type, KCell) # routes route_results: dict[str, list[Any]] = {} @@ -1692,22 +1727,20 @@ def create_cell( resolved_port_list.append(p) resolved_ports.append(tuple(resolved_port_list)) route_c = output_type(base=c.base) - relevant_constraints = [ - ct for ct in self.constraints if route.name in ct.route_names - ] + relevant_constraints = constraints_by_route_name.get(route.name, []) extra_kwargs: dict[str, Any] = ( {"constraints": relevant_constraints} if relevant_constraints else {} ) - if isinstance(route_c, KCell): + if is_kcell_output: result = routing_strategies[route.routing_strategy]( - output_type(base=c.base), + route_c, resolved_ports, **route.settings, **extra_kwargs, ) else: result = routing_strategies[route.routing_strategy]( - output_type(base=c.base), + route_c, [ tuple(DKCellPort(base=p.base) for p in net_ports) for net_ports in resolved_ports @@ -2446,12 +2479,12 @@ def connections(self) -> list[Connection[T]]: return [net for net in self.nets if isinstance(net, Connection)] -def _get_instance_orientation[T: (int, float)]( +def _get_instance_orientation( instance: str, - schematic: TSchematic[T], + schematic: TSchematic[Any], visited_instances: set[str], get_port_orientation_f: Callable[..., dict[str | None, float]], - instance_connections: defaultdict[str, list[Connection[T]]] | None = None, + instance_connections: defaultdict[str, list[Connection[Any]]] | None = None, instance_orientations: dict[str, float] | None = None, ) -> float | None: s_inst = schematic.instances[instance] @@ -2469,17 +2502,20 @@ def _get_instance_orientation[T: (int, float)]( get_port_orientation_f=get_port_orientation_f, ) return placement.orientation + connections_by_instance: defaultdict[str, list[Connection[Any]]] if instance_connections is None: - _, instance_connections = _get_island_connections( + _, connections_by_instance = _get_island_connections( schematic.instances, schematic.connections ) + else: + connections_by_instance = instance_connections visited_instances |= {instance} potential_instances: set[str] = set() s_inst_sign = -1 if s_inst.mirror else 1 - for connection in instance_connections[instance]: + for connection in connections_by_instance[instance]: if isinstance(connection.net[0], Port): if isinstance(connection.net[0].orientation, PortRef): continue @@ -2543,13 +2579,13 @@ def _get_instance_orientation[T: (int, float)]( continue orientation = _get_instance_orientation( inst, - schematic, # ty:ignore[invalid-argument-type] + schematic, visited_instances_, get_port_orientation_f=get_port_orientation_f, - instance_connections=instance_connections, # ty:ignore[invalid-argument-type] + instance_connections=connections_by_instance, ) if orientation is not None: - for connection in instance_connections[instance]: + for connection in connections_by_instance[instance]: if isinstance(connection.net[0], Port): continue if connection.net[0].instance == inst: @@ -2819,38 +2855,113 @@ def _is_int_schematic(s: TSchematic[Any]) -> TypeGuard[Schematic[int]]: return s.unit == "dbu" +def _is_um_schematic(s: TSchematic[Any]) -> TypeGuard[DSchematic]: + return s.unit == "um" + + +def _get_dbu_placement(schematic: Schematic, instance: str) -> Placement[int] | None: + placement = schematic.instances[instance].placement + if isinstance(placement, Placement): + return cast("Placement[int]", placement) + return None + + +def _get_um_placement(schematic: DSchematic, instance: str) -> Placement[float] | None: + placement = schematic.instances[instance].placement + if isinstance(placement, Placement): + return cast("Placement[float]", placement) + return None + + +type _SchematicCrossSections = Mapping[ + str, + CrossSection | DCrossSection | AsymmetricCrossSection | DAsymmetricCrossSection, +] +type _SchematicFactories = ( + Mapping[str, Callable[..., KCell] | Callable[..., DKCell] | Callable[..., VKCell]] + | None +) + + +def _place_dbu_island( + c: KCell, + schematic_island: set[str], + instances: dict[str, Instance | VInstance], + connections: dict[str, list[Connection[int]]], + placed_insts: set[str], + placed_ports: set[str], + schematic: Schematic, + cross_sections: _SchematicCrossSections, + factories: _SchematicFactories = None, + place_unknown: bool = False, +) -> set[str]: + return _place_island( + c, + schematic_island=schematic_island, + instances=instances, + connections=connections, + placed_insts=placed_insts, + placed_ports=placed_ports, + schematic=schematic, + cross_sections=cross_sections, + factories=factories, + place_unknown=place_unknown, + ) + + +def _place_um_island( + c: KCell, + schematic_island: set[str], + instances: dict[str, Instance | VInstance], + connections: dict[str, list[Connection[float]]], + placed_insts: set[str], + placed_ports: set[str], + schematic: DSchematic, + cross_sections: _SchematicCrossSections, + factories: _SchematicFactories = None, + place_unknown: bool = False, +) -> set[str]: + return _place_island( + c, + schematic_island=schematic_island, + instances=instances, + connections=connections, + placed_insts=placed_insts, + placed_ports=placed_ports, + schematic=schematic, + cross_sections=cross_sections, + factories=factories, + place_unknown=place_unknown, + ) + + def _place_island[T: (int, float)]( c: KCell, schematic_island: set[str], instances: dict[str, Instance | VInstance], connections: dict[str, list[Connection[T]]], - schematic_instances: dict[str, SchematicInstance[T]], placed_insts: set[str], placed_ports: set[str], schematic: TSchematic[T], - cross_sections: Mapping[ - str, - CrossSection | DCrossSection | AsymmetricCrossSection | DAsymmetricCrossSection, - ], - factories: Mapping[ - str, Callable[..., KCell] | Callable[..., DKCell] | Callable[..., VKCell] - ] - | None = None, + cross_sections: _SchematicCrossSections, + factories: _SchematicFactories = None, place_unknown: bool = False, ) -> set[str]: target_length = len(schematic_island) for inst in schematic_island: - schema_inst = schematic_instances[inst] + schema_inst = schematic.instances[inst] kinst = _create_kinst(c, schema_inst, factories=factories) instances[inst] = kinst p = schema_inst.placement if isinstance(p, Placement): - p = cast("Placement[int]", p) logger.debug("Placing {}", schema_inst.name) if p.is_placeable(placed_insts, placed_ports): if _is_int_schematic(schematic): + p = _get_dbu_placement(schematic, inst) + if p is None: + continue if isinstance(p.x, PortRef): x: float = KCellPort( base=instances[p.x.instance].ports[p.x.port].base @@ -2865,6 +2976,8 @@ def _place_island[T: (int, float)]( case _: x = bb.center().x else: + if not isinstance(p.x, int | float): + raise TypeError(f"Unsupported placement x value {p.x!r}") x = p.x if isinstance(p.y, PortRef): y: float = KCellPort( @@ -2880,6 +2993,8 @@ def _place_island[T: (int, float)]( case _: y = bb.center().y else: + if not isinstance(p.y, int | float): + raise TypeError(f"Unsupported placement y value {p.y!r}") y = p.y if isinstance(p.orientation, PortRef): rot: float = ( @@ -2934,36 +3049,53 @@ def _place_island[T: (int, float)]( * kdb.ICplxTrans(-kdb.Vector(_x, _y)) ) else: + if not _is_um_schematic(schematic): + raise ValueError( + f"Unsupported schematic unit {schematic.unit!r}" + ) + p = _get_um_placement(schematic, inst) + if p is None: + continue + x_d: float if isinstance(p.x, PortRef): - x = DKCellPort( - base=instances[p.x.instance].ports[p.x.port].base - ).x + x_d = float( + DKCellPort( + base=instances[p.x.instance].ports[p.x.port].base + ).x + ) elif isinstance(p.x, AnchorRefX): bb = instances[p.x.instance].dbbox() match p.x.x: case "left": - x = bb.left + x_d = float(bb.left) case "right": - x = bb.right + x_d = float(bb.right) case _: - x = bb.center().x + x_d = float(bb.center().x) else: - x = p.x + if not isinstance(p.x, int | float): + raise TypeError(f"Unsupported placement x value {p.x!r}") + x_d = float(p.x) + y_d: float if isinstance(p.y, PortRef): - y = DKCellPort( - base=instances[p.y.instance].ports[p.y.port].base - ).y + y_d = float( + DKCellPort( + base=instances[p.y.instance].ports[p.y.port].base + ).y + ) elif isinstance(p.y, AnchorRefY): bb = instances[p.y.instance].dbbox() match p.y.y: case "bottom": - y = bb.bottom + y_d = float(bb.bottom) case "top": - y = bb.top + y_d = float(bb.top) case _: - y = bb.center().y + y_d = float(bb.center().y) else: - y = p.y + if not isinstance(p.y, int | float): + raise TypeError(f"Unsupported placement y value {p.y!r}") + y_d = float(p.y) if isinstance(p.orientation, PortRef): drot: float = ( instances[p.orientation.instance] @@ -2972,13 +3104,15 @@ def _place_island[T: (int, float)]( ) else: drot = p.orientation + dx_d = float(p.dx) + dy_d = float(p.dy) if p.anchor is None: kinst.transform( kdb.DCplxTrans( mag=1, rot=drot, - x=x + p.dx, - y=y + p.dy, + x=x_d + dx_d, + y=y_d + dy_d, ) ) elif isinstance(p.anchor, PortAnchor): @@ -2986,8 +3120,8 @@ def _place_island[T: (int, float)]( kdb.DCplxTrans( mag=1, rot=drot, - x=x + p.dx, - y=y + p.dy, + x=x_d + dx_d, + y=y_d + dy_d, ) * kdb.DCplxTrans( -kinst.ports[p.anchor.port].dcplx_trans.disp @@ -3017,8 +3151,8 @@ def _place_island[T: (int, float)]( kdb.DCplxTrans( mag=1, rot=drot, - x=x + p.dx, - y=y + p.dy, + x=x_d + dx_d, + y=y_d + dy_d, ) * kdb.DCplxTrans(-kdb.DVector(_dx, _dy)) ) diff --git a/src/kfactory/spatial.py b/src/kfactory/spatial.py new file mode 100644 index 000000000..2e23fa4aa --- /dev/null +++ b/src/kfactory/spatial.py @@ -0,0 +1,48 @@ +"""Shared spatial-query helpers for bbox pruning and cached region extraction.""" + +from __future__ import annotations + +import heapq +from typing import TYPE_CHECKING, Any + +from . import kdb + +if TYPE_CHECKING: + from collections.abc import Iterator, Sequence + + +def collect_instance_region( + cell: Any, + layer: int, + inst: Any, +) -> kdb.Region: + """Collect the actual geometry region for one instance on one layer.""" + region = kdb.Region() + shape_it = cell.begin_shapes_rec_overlapping(layer, inst.bbox(layer)) + shape_it.select_cells([inst.cell.cell_index()]) + shape_it.min_depth = 1 + for _it in shape_it.each(): + if _it.path()[0].inst() == inst.instance: + region.insert(_it.shape().polygon.transformed(_it.trans())) + return region + + +def iter_overlapping_bbox_pairs( + boxes: Sequence[kdb.Box], +) -> Iterator[tuple[int, int]]: + """Yield index pairs whose bounding boxes overlap.""" + ordered = sorted(enumerate(boxes), key=lambda item: (item[1].left, item[1].bottom)) + active: dict[int, kdb.Box] = {} + active_rights: list[tuple[int, int]] = [] + + for idx, bbox in ordered: + while active_rights and active_rights[0][0] < bbox.left: + _, old_idx = heapq.heappop(active_rights) + active.pop(old_idx, None) + + for other_idx, other_bbox in active.items(): + if not (other_bbox & bbox).empty(): + yield idx, other_idx + + active[idx] = bbox + heapq.heappush(active_rights, (bbox.right, idx)) diff --git a/src/kfactory/technology/layer_map.py b/src/kfactory/technology/layer_map.py index 7f15a2be6..d8afcb146 100644 --- a/src/kfactory/technology/layer_map.py +++ b/src/kfactory/technology/layer_map.py @@ -151,8 +151,10 @@ def kl2lp(kl: lay.LayerPropertiesNodeRef) -> LayerPropertiesModel: layer=(kl.source_layer, kl.source_datatype), frame_color=Color(hex(kl.frame_color)) if kl.frame_color else None, fill_color=Color(hex(kl.fill_color)) if kl.fill_color else None, - dither_pattern=index2dither[kl.dither_pattern], # ty:ignore[invalid-argument-type] - line_style=index2line.get(kl.line_style, "solid"), # ty:ignore[invalid-argument-type] + dither_pattern=kl.dither_pattern, + line_style=kl.line_style + if kl.line_style in index2line + else line2index["solid"], visible=kl.visible, width=kl.width, xfill=kl.xfill, diff --git a/src/kfactory/utils/simplify.py b/src/kfactory/utils/simplify.py index 98c3f01fc..b7fe8a166 100644 --- a/src/kfactory/utils/simplify.py +++ b/src/kfactory/utils/simplify.py @@ -1,26 +1,73 @@ """Simplifying functions.""" +from typing import cast + import numpy as np from kfactory.conf import MIN_POINTS_FOR_SIMPLIFY from .. import kdb +_DSIMPLIFY_ARRAY_THRESHOLD = 256 -def simplify(points: list[kdb.Point], tolerance: float) -> list[kdb.Point]: - """Simplify a list of `klayout.db.Point` to a certain tolerance (in dbu). - Uses [Ramer-Douglas-Peucker algorithm](https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm) +def _simplify_from_arrays( + points: list[kdb.Point] | list[kdb.DPoint], + tolerance: float, +) -> list[kdb.Point] | list[kdb.DPoint]: + xs = np.fromiter((p.x for p in points), dtype=np.float64, count=len(points)) + ys = np.fromiter((p.y for p in points), dtype=np.float64, count=len(points)) + indices = _simplify_indices(xs, ys, 0, len(points) - 1, tolerance) + return cast("list[kdb.Point] | list[kdb.DPoint]", [points[i] for i in indices]) - Args: - points: list of points to simplify - tolerance: if two points are > tolerance (in dbu) apart, - delete most suitable points. - """ + +def _simplify_indices( + xs: np.ndarray, + ys: np.ndarray, + start: int, + end: int, + tolerance: float, +) -> list[int]: + if end - start + 1 < MIN_POINTS_FOR_SIMPLIFY: + return list(range(start, end + 1)) + + dx = xs[end] - xs[start] + dy = ys[end] - ys[start] + norm = np.hypot(dx, dy) + if norm == 0: + xs_ = xs[start : end + 1] + ys_ = ys[start : end + 1] + dists = np.hypot(xs_ - xs[start], ys_ - ys[start]) + ind_dist = start + int(np.argmax(dists)) + maxd = float(dists[ind_dist - start]) + return ( + [start, end] + if maxd <= tolerance + else _simplify_indices(xs, ys, start, ind_dist, tolerance) + + _simplify_indices(xs, ys, ind_dist, end, tolerance)[1:] + ) + + xs_ = xs[start : end + 1] + ys_ = ys[start : end + 1] + dists = np.abs(dy * xs_ - dx * ys_ + xs[end] * ys[start] - ys[end] * xs[start]) + ind_dist = start + int(np.argmax(dists)) + maxd = float(dists[ind_dist - start] / norm) + + return ( + [start, end] + if maxd <= tolerance + else _simplify_indices(xs, ys, start, ind_dist, tolerance) + + _simplify_indices(xs, ys, ind_dist, end, tolerance)[1:] + ) + + +def _dsimplify_with_edge( + points: list[kdb.DPoint], tolerance: float +) -> list[kdb.DPoint]: if len(points) < MIN_POINTS_FOR_SIMPLIFY: return points - e = kdb.Edge(points[0], points[-1]) + e = kdb.DEdge(points[0], points[-1]) dists = [e.distance_abs(p) for p in points] ind_dist = int(np.argmax(dists)) maxd = dists[ind_dist] @@ -29,12 +76,28 @@ def simplify(points: list[kdb.Point], tolerance: float) -> list[kdb.Point]: [points[0], points[-1]] if maxd <= tolerance else ( - simplify(points[: ind_dist + 1], tolerance) - + simplify(points[ind_dist:], tolerance)[1:] + _dsimplify_with_edge(points[: ind_dist + 1], tolerance) + + _dsimplify_with_edge(points[ind_dist:], tolerance)[1:] ) ) +def simplify(points: list[kdb.Point], tolerance: float) -> list[kdb.Point]: + """Simplify a list of `klayout.db.Point` to a certain tolerance (in dbu). + + Uses [Ramer-Douglas-Peucker algorithm](https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm) + + Args: + points: list of points to simplify + tolerance: if two points are > tolerance (in dbu) apart, + delete most suitable points. + """ + if len(points) < MIN_POINTS_FOR_SIMPLIFY: + return points + + return cast("list[kdb.Point]", _simplify_from_arrays(points, tolerance)) + + def dsimplify(points: list[kdb.DPoint], tolerance: float) -> list[kdb.DPoint]: """Simplify a list of um points to a certain tolerance (in um). @@ -48,16 +111,6 @@ def dsimplify(points: list[kdb.DPoint], tolerance: float) -> list[kdb.DPoint]: if len(points) < MIN_POINTS_FOR_SIMPLIFY: return points - e = kdb.DEdge(points[0], points[-1]) - dists = [e.distance_abs(p) for p in points] - ind_dist = int(np.argmax(dists)) - maxd = dists[ind_dist] - - return ( - [points[0], points[-1]] - if maxd <= tolerance - else ( - dsimplify(points[: ind_dist + 1], tolerance) - + dsimplify(points[ind_dist:], tolerance)[1:] - ) - ) + if len(points) < _DSIMPLIFY_ARRAY_THRESHOLD: + return _dsimplify_with_edge(points, tolerance) + return cast("list[kdb.DPoint]", _simplify_from_arrays(points, tolerance)) diff --git a/tests/conftest.py b/tests/conftest.py index eeea4cff9..08b719ed7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,7 @@ from functools import partial from pathlib import Path from threading import RLock -from typing import Any, Literal +from typing import Any, Literal, Protocol from warnings import warn import pytest @@ -16,6 +16,16 @@ pytest_plugins = ["pytest_regressions"] +class OASRegression(Protocol): + def __call__( + self, + c: kf.ProtoTKCell[Any], + tolerance: int = 0, + flatten: bool = False, + with_meta: bool = True, + ) -> None: ... + + class Layers(kf.LayerInfos): WG: kf.kdb.LayerInfo = kf.kdb.LayerInfo(1, 0) WGCLAD: kf.kdb.LayerInfo = kf.kdb.LayerInfo(111, 0) @@ -250,7 +260,7 @@ def unlink_merge_read_oas() -> Iterator[None]: @pytest.fixture def oas_regression( file_regression: FileRegressionFixture, -) -> Callable[[kf.ProtoTKCell[Any]], None]: +) -> OASRegression: saveopts = kf.save_layout_options() saveopts.format = "OASIS" @@ -264,6 +274,8 @@ def oas_regression( def _check( c: kf.ProtoTKCell[Any], tolerance: int = 0, + flatten: bool = False, + with_meta: bool = True, ) -> None: c.kcl.layout.clear_meta_info() @@ -271,7 +283,13 @@ def _check( c.write_bytes(saveopts, convert_external_cells=True), binary=True, extension=".oas", - check_fn=partial(_layout_xor, tolerance=tolerance, raises=raises), + check_fn=partial( + _layout_xor, + tolerance=tolerance, + raises=raises, + flatten=flatten, + with_meta=with_meta, + ), ) kf.config.write_kfactory_settings = write_settings @@ -303,6 +321,8 @@ def _layout_xor( path_b: Path, tolerance: int = 0, raises: Literal["error", "warning"] = "error", + flatten: bool = False, + with_meta: bool = True, ) -> None: diff = kf.kdb.LayoutDiff() ly_a = kf.kdb.Layout() @@ -310,11 +330,15 @@ def _layout_xor( ly_b = kf.kdb.Layout() ly_b.read(str(path_b)) - flags = ( - kf.kdb.LayoutDiff.Verbose - | kf.kdb.LayoutDiff.WithMetaInfo - | kf.kdb.LayoutDiff.NoLayerNames - ) + if flatten: + for ly in (ly_a, ly_b): + top_cell = ly.top_cell() + if top_cell is not None: + top_cell.flatten(True) + + flags = kf.kdb.LayoutDiff.Verbose | kf.kdb.LayoutDiff.NoLayerNames + if with_meta: + flags |= kf.kdb.LayoutDiff.WithMetaInfo if not diff.compare(ly_a, ly_b, flags=flags, tolerance=tolerance): match raises: diff --git a/tests/test_config.py b/tests/test_config.py index 72457abc4..e18fb8906 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -11,6 +11,7 @@ def test_custom_show() -> None: def show( layout: kf.KCLayout | AnyKCell | Path | str, + *, lyrdb: kf.rdb.ReportDatabase | Path | str | None = None, l2n: kf.kdb.LayoutToNetlist | Path | str | None = None, keep_position: bool = True, @@ -20,6 +21,7 @@ def show( technology: str | None = None, markers: list[tuple[kf.typings.DShapeLike, kf.typings.MarkerConfig]] | None = None, + name: str | None = None, ) -> None: nonlocal showed showed = True diff --git a/tests/test_enclosure.py b/tests/test_enclosure.py index 157aa451b..64ed06c36 100644 --- a/tests/test_enclosure.py +++ b/tests/test_enclosure.py @@ -235,3 +235,20 @@ def test_extrude_path_cross_section_asymmetric( assert kf.kdb.Region(c.shapes(kcl.layer(layers.WGCLAD))).bbox() == kf.kdb.Box( 0, -100, length_dbu, 900 ) + + +def test_extrude_path_points_long_path_matches_explicit_end_angle() -> None: + path = [kf.kdb.DPoint(float(i), 0.0) for i in range(63)] + [ + kf.kdb.DPoint(63.0, 1.0), + kf.kdb.DPoint(64.0, 2.0), + ] + width = 2.0 + end_angle = 45.0 + + implicit_top, implicit_bot = kf.enclosure.extrude_path_points(path, width) + explicit_top, explicit_bot = kf.enclosure.extrude_path_points( + path, width, end_angle=end_angle + ) + + assert implicit_top[-1] == explicit_top[-1] + assert implicit_bot[-1] == explicit_bot[-1] diff --git a/tests/test_layer_map.py b/tests/test_layer_map.py index 317559f43..3aea10637 100644 --- a/tests/test_layer_map.py +++ b/tests/test_layer_map.py @@ -48,29 +48,37 @@ def test_layer_properties_model_defaults() -> None: def test_layer_properties_model_dither_string() -> None: - lp = LayerPropertiesModel(name="WG", layer=(1, 0), dither_pattern="solid") # ty:ignore[invalid-argument-type] + lp = LayerPropertiesModel.model_validate( + {"name": "WG", "layer": (1, 0), "dither_pattern": "solid"} + ) assert lp.dither_pattern == dither2index["solid"] def test_layer_properties_model_line_style_string() -> None: - lp = LayerPropertiesModel(name="WG", layer=(1, 0), line_style="dotted") # ty:ignore[invalid-argument-type] + lp = LayerPropertiesModel.model_validate( + {"name": "WG", "layer": (1, 0), "line_style": "dotted"} + ) assert lp.line_style == line2index["dotted"] def test_layer_properties_model_color_to_frame_fill() -> None: - lp = LayerPropertiesModel(name="WG", layer=(1, 0), color="#ff0000") # ty:ignore[unknown-argument] + lp = LayerPropertiesModel.model_validate( + {"name": "WG", "layer": (1, 0), "color": "#ff0000"} + ) assert lp.frame_color is not None assert lp.fill_color is not None def test_layer_properties_model_color_overrides() -> None: # If explicit fill/frame are provided, the shorthand "color" doesn't override - lp = LayerPropertiesModel( - name="WG", - layer=(1, 0), - color="#ff0000", # ty:ignore[unknown-argument] - fill_color="#00ff00", # ty:ignore[invalid-argument-type] - frame_color="#0000ff", # ty:ignore[invalid-argument-type] + lp = LayerPropertiesModel.model_validate( + { + "name": "WG", + "layer": (1, 0), + "color": "#ff0000", + "fill_color": "#00ff00", + "frame_color": "#0000ff", + } ) assert lp.fill_color is not None assert lp.fill_color.as_hex().startswith("#0") @@ -123,11 +131,13 @@ def test_lp2kl_with_layer_to_name() -> None: def test_lp2kl_with_colors() -> None: - lp = LayerPropertiesModel( - name="WG", - layer=(1, 0), - frame_color="#abcdef", # ty:ignore[invalid-argument-type] - fill_color="#123456", # ty:ignore[invalid-argument-type] + lp = LayerPropertiesModel.model_validate( + { + "name": "WG", + "layer": (1, 0), + "frame_color": "#abcdef", + "fill_color": "#123456", + } ) kl = lp2kl(lp) # KLayout may store with alpha bits; compare only the low 24 bits @@ -137,11 +147,13 @@ def test_lp2kl_with_colors() -> None: def test_lp2kl_with_short_hex_colors() -> None: # Pydantic Color may normalize "#fff" -> a long form. Force short by using e.g. red. - lp = LayerPropertiesModel( - name="WG", - layer=(1, 0), - frame_color="red", # ty:ignore[invalid-argument-type] - fill_color="red", # ty:ignore[invalid-argument-type] + lp = LayerPropertiesModel.model_validate( + { + "name": "WG", + "layer": (1, 0), + "frame_color": "red", + "fill_color": "red", + } ) kl = lp2kl(lp) assert kl.frame_color > 0 diff --git a/tests/test_routing.py b/tests/test_routing.py index a33ae9d96..499c7f6da 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -7,7 +7,7 @@ import kfactory as kf from kfactory.routing.utils import RouteDebug -from tests.conftest import Layers +from tests.conftest import Layers, OASRegression smart_bundle_routing_params = [ (indirect, sort_ports, start_bbox, start_angle, m2, m1, z, p1, p2) @@ -32,7 +32,7 @@ def test_route_length_match( loop_side: int, bend90: kf.KCell, straight_factory_dbu: Callable[..., kf.KCell], - oas_regression: Callable[[kf.ProtoTKCell[Any]], None], + oas_regression: OASRegression, layers: Layers, kcl: kf.KCLayout, ) -> None: @@ -76,7 +76,7 @@ def test_route_length_match( ], route_name="path_length_matching", ) - oas_regression(c) + oas_regression(c, flatten=True, with_meta=False) def test_route_length_match_errors() -> None: @@ -96,7 +96,7 @@ def test_route_bundle( bend90_euler: kf.KCell, straight_factory_dbu: Callable[..., kf.KCell], kcl: kf.KCLayout, - oas_regression: Callable[[kf.ProtoTKCell[Any]], None], + oas_regression: OASRegression, ) -> None: c = kcl.kcell("TEST_ROUTE_BUNDLE") @@ -151,7 +151,7 @@ def test_route_bundle( assert np.isclose(route.length, length) c.auto_rename_ports() - oas_regression(c) + oas_regression(c, flatten=True, with_meta=False) def test_route_length_straight( @@ -160,7 +160,7 @@ def test_route_length_straight( straight_factory_dbu: Callable[..., kf.KCell], kcl: kf.KCLayout, layers: Layers, - oas_regression: Callable[[kf.ProtoTKCell[Any]], None], + oas_regression: OASRegression, ) -> None: c = kcl.kcell("TEST_ROUTE_BUNDLE_AREA_LENGTH") p1 = kf.Port(name="o1", width=1000, trans=kf.kdb.Trans.R0, layer_info=layers.WG) @@ -178,7 +178,7 @@ def test_route_length_straight( ) assert [r.length for r in routes] == [10_000] - oas_regression(c) + oas_regression(c, flatten=True, with_meta=False) def test_route_bundle_route_width( @@ -186,7 +186,7 @@ def test_route_bundle_route_width( bend90_euler_small: kf.KCell, straight_factory_dbu: Callable[..., kf.KCell], kcl: kf.KCLayout, - oas_regression: Callable[[kf.ProtoTKCell[Any]], None], + oas_regression: OASRegression, ) -> None: c = kcl.kcell("TEST_ROUTE_BUNDLE") @@ -229,7 +229,7 @@ def test_route_bundle_route_width( c.add_port(port=route.end_port) c.auto_rename_ports() - oas_regression(c) + oas_regression(c, flatten=True, with_meta=False) def test_route_length( @@ -237,7 +237,7 @@ def test_route_length( straight_factory_dbu: Callable[..., kf.KCell], optical_port: kf.Port, taper: kf.KCell, - oas_regression: Callable[[kf.ProtoTKCell[Any]], None], + oas_regression: OASRegression, kcl: kf.KCLayout, ) -> None: x, y, angle2 = (55000, 70000, 2) @@ -264,7 +264,7 @@ def test_route_length( assert route.length_straights == 30196 assert route.length_backbone == 125000 assert route.n_bend90 == 2 - oas_regression(c) + oas_regression(c, flatten=True, with_meta=False) _test_smart_routing_kcl = kf.KCLayout("TEST_SMART_ROUTING", infos=Layers) @@ -296,7 +296,7 @@ def test_smart_routing( z: bool, p1: bool, p2: bool, - oas_regression: Callable[[kf.ProtoTKCell[Any]], None], + oas_regression: OASRegression, kcl: kf.KCLayout, ) -> None: """Tests all possible smart routing configs.""" @@ -473,7 +473,7 @@ def test_smart_routing( c.show() case _: rf() - oas_regression(c) + oas_regression(c, flatten=True, with_meta=False) def test_custom_router( @@ -551,7 +551,7 @@ def test_route_smart_waypoints_trans_sort( bend90_small: kf.KCell, straight_factory_dbu: Callable[..., kf.KCell], layers: Layers, - oas_regression: Callable[[kf.ProtoTKCell[Any]], None], + oas_regression: OASRegression, kcl: kf.KCLayout, ) -> None: c = kcl.kcell(name="test_smart_route_waypoints_trans_sort") @@ -583,14 +583,14 @@ def test_route_smart_waypoints_trans_sort( waypoints=kf.kdb.Trans(250_000, 0), sort_ports=True, ) - oas_regression(c) + oas_regression(c, flatten=True, with_meta=False) def test_route_smart_waypoints_pts_sort( bend90_small: kf.KCell, straight_factory_dbu: Callable[..., kf.KCell], layers: Layers, - oas_regression: Callable[[kf.ProtoTKCell[Any]], None], + oas_regression: OASRegression, kcl: kf.KCLayout, ) -> None: c = kcl.kcell(name="test_smart_route_waypoints_pts_sort") @@ -622,7 +622,7 @@ def test_route_smart_waypoints_pts_sort( waypoints=[kf.kdb.Point(250_000, 0), kf.kdb.Point(250_000, 100_000)], sort_ports=True, ) - oas_regression(c) + oas_regression(c, flatten=True, with_meta=False) def test_route_waypoints_non_manhattan( @@ -677,7 +677,7 @@ def test_route_smart_waypoints_trans( bend90_small: kf.KCell, straight_factory_dbu: Callable[..., kf.KCell], layers: Layers, - oas_regression: Callable[[kf.ProtoTKCell[Any]], None], + oas_regression: OASRegression, kcl: kf.KCLayout, ) -> None: c = kcl.kcell(name="test_smart_route_waypoints_trans") @@ -709,14 +709,14 @@ def test_route_smart_waypoints_trans( bend90_cell=bend90_small, waypoints=kf.kdb.Trans(250_000, 0), ) - oas_regression(c) + oas_regression(c, flatten=True, with_meta=False) def test_route_smart_waypoints_pts( bend90_small: kf.KCell, straight_factory_dbu: Callable[..., kf.KCell], layers: Layers, - oas_regression: Callable[[kf.ProtoTKCell[Any]], None], + oas_regression: OASRegression, kcl: kf.KCLayout, ) -> None: c = kcl.kcell(name="test_smart_route_waypoints_pts") @@ -748,13 +748,13 @@ def test_route_smart_waypoints_pts( bend90_cell=bend90_small, waypoints=[kf.kdb.Point(250_000, 0), kf.kdb.Point(250_000, 100_000)], ) - oas_regression(c) + oas_regression(c, flatten=True, with_meta=False) def test_route_generic_reorient( bend90_small: kf.KCell, straight_factory_dbu: Callable[..., kf.KCell], - oas_regression: Callable[[kf.ProtoTKCell[Any]], None], + oas_regression: OASRegression, kcl: kf.KCLayout, ) -> None: c = kcl.kcell(name="test_route_generic_reorient") @@ -794,7 +794,7 @@ def test_route_generic_reorient( end_angles=end_angles, ) - oas_regression(c) + oas_regression(c, flatten=True, with_meta=False) def test_placer_error( @@ -1398,7 +1398,7 @@ def test_route_bundle_single_return( straight_factory_dbu: Callable[..., kf.KCell], optical_port: kf.Port, taper: kf.KCell, - oas_regression: Callable[[kf.ProtoTKCell[Any]], None], + oas_regression: OASRegression, kcl: kf.KCLayout, ) -> None: x, y, angle2 = (0, 7000, 0) @@ -1418,7 +1418,7 @@ def test_route_bundle_single_return( taper_cell=taper, allow_width_mismatch=True, )[0] - oas_regression(c) + oas_regression(c, flatten=True, with_meta=False) def test_route_bundle_multi_return( @@ -1426,7 +1426,7 @@ def test_route_bundle_multi_return( straight_factory_dbu: Callable[..., kf.KCell], optical_port: kf.Port, taper: kf.KCell, - oas_regression: Callable[[kf.ProtoTKCell[Any]], None], + oas_regression: OASRegression, kcl: kf.KCLayout, ) -> None: c = kcl.kcell() @@ -1473,7 +1473,7 @@ def test_route_bundle_multi_return( sort_ports=False, bboxes=[b1, b2], )[0] - oas_regression(c) + oas_regression(c, flatten=True, with_meta=False) def test_route_bundle_multi_return_opposite( @@ -1481,7 +1481,7 @@ def test_route_bundle_multi_return_opposite( straight_factory_dbu: Callable[..., kf.KCell], optical_port: kf.Port, taper: kf.KCell, - oas_regression: Callable[[kf.ProtoTKCell[Any]], None], + oas_regression: OASRegression, kcl: kf.KCLayout, ) -> None: c = kcl.kcell() @@ -1527,4 +1527,4 @@ def test_route_bundle_multi_return_opposite( sort_ports=False, bboxes=[b1, b2], )[0] - oas_regression(c) + oas_regression(c, flatten=True, with_meta=False) diff --git a/tests/test_simplify.py b/tests/test_simplify.py index 49595bfc2..bdfb1aa90 100644 --- a/tests/test_simplify.py +++ b/tests/test_simplify.py @@ -71,3 +71,17 @@ def test_simplify_single_point() -> None: points = [kdb.DPoint(0, 0)] simplified = dsimplify(points, 0.1) assert simplified == points + + +def test_simplify_closed_polyline_preserves_shape() -> None: + points = [ + kdb.Point(0, 0), + kdb.Point(10, 0), + kdb.Point(10, 10), + kdb.Point(0, 10), + kdb.Point(0, 0), + ] + + simplified = simplify(points, 0.1) + + assert simplified == points