Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 34 additions & 41 deletions src/kfactory/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@

from __future__ import annotations

from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, cast

from kfnetlist import PortCheck, check_connection

from . import kdb, rdb
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
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions src/kfactory/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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: ...


Expand Down
10 changes: 6 additions & 4 deletions src/kfactory/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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}")
Expand Down
118 changes: 117 additions & 1 deletion src/kfactory/enclosure.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from __future__ import annotations

import itertools
import math
import sys
from collections import defaultdict
from enum import IntEnum
Expand Down Expand Up @@ -77,6 +78,7 @@ class Direction(IntEnum):


_min_size = -sys.maxsize - 1
_EXTRUDE_PATH_ARRAY_THRESHOLD = 64


def is_callable_widths(
Expand All @@ -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,
Expand All @@ -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:
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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]

Expand Down Expand Up @@ -726,14 +838,18 @@ 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)))
for layer, offset in sorted(
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,
Expand Down
23 changes: 21 additions & 2 deletions src/kfactory/factories/bezier.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading