"""Component references.
Adapted from PHIDL https://github.com/amccaugh/phidl/ by Adam McCaughan
"""
from __future__ import annotations
import typing
import warnings
from typing import Any, cast
import gdstk
import numpy as np
from numpy import cos, mod, ndarray, pi, sin
from gdsfactory.component_layout import (
Polygon,
_GeometryHelper,
get_polygons,
pprint_ports,
)
from gdsfactory.config import CONF
from gdsfactory.port import (
Port,
map_ports_layer_to_orientation,
map_ports_to_orientation_ccw,
map_ports_to_orientation_cw,
select_ports,
)
from gdsfactory.snap import snap_to_grid
if typing.TYPE_CHECKING:
import shapely
from gdsfactory.component import Component, Coordinate, Coordinates, LayerSpec
class SizeInfo:
def __init__(self, bbox: ndarray) -> None:
"""Initialize this object."""
self.west = bbox[0, 0]
self.east = bbox[1, 0]
self.south = bbox[0, 1]
self.north = bbox[1, 1]
self.width = self.east - self.west
self.height = self.north - self.south
xc = 0.5 * (self.east + self.west)
yc = 0.5 * (self.north + self.south)
self.sw = np.array([self.west, self.south])
self.se = np.array([self.east, self.south])
self.nw = np.array([self.west, self.north])
self.ne = np.array([self.east, self.north])
self.cw = np.array([self.west, yc])
self.ce = np.array([self.east, yc])
self.nc = np.array([xc, self.north])
self.sc = np.array([xc, self.south])
self.cc = self.center = np.array([xc, yc])
def get_rect(
self, padding=0, padding_w=None, padding_e=None, padding_n=None, padding_s=None
) -> tuple[Coordinate, Coordinate, Coordinate, Coordinate]:
w, e, s, n = self.west, self.east, self.south, self.north
padding_n = padding if padding_n is None else padding_n
padding_e = padding if padding_e is None else padding_e
padding_w = padding if padding_w is None else padding_w
padding_s = padding if padding_s is None else padding_s
w = w - padding_w
e = e + padding_e
s = s - padding_s
n = n + padding_n
return ((w, s), (e, s), (e, n), (w, n))
@property
def rect(self) -> tuple[Coordinate, Coordinate]:
return self.get_rect()
def __str__(self) -> str:
"""Return a string representation of the object."""
return f"w: {self.west}\ne: {self.east}\ns: {self.south}\nn: {self.north}\n"
def _rotate_points(
points: Coordinates,
angle: float = 45.0,
center: Coordinate = (
0.0,
0.0,
),
) -> ndarray:
"""Rotates points around a center point.
accepts single points [1,2] or array-like[N][2], and will return in kind
Args:
points: rotate points around center point.
angle: in degrees.
center: x, y.
"""
# First check for common, easy values of angle
p_arr = np.asarray(points)
if angle == 0:
return p_arr
c0 = np.asarray(center)
displacement = p_arr - c0
if angle == 180:
return c0 - displacement
if p_arr.ndim == 2:
perpendicular = displacement[:, ::-1]
elif p_arr.ndim == 1:
perpendicular = displacement[::-1]
# Fall back to trigonometry
angle = angle * pi / 180
ca = cos(angle)
sa = sin(angle)
sa = np.array((-sa, sa))
return displacement * ca + perpendicular * sa + c0
[docs]
class ComponentReference(_GeometryHelper):
"""Pointer to a Component with x, y, rotation, mirror.
Args:
component: Component The referenced Component.
columns: Number of columns in the array.
rows: Number of rows in the array.
spacing: Distances between adjacent columns and adjacent rows.
origin: array-like[2] of int or float
Position where the cell is inserted.
rotation : int or float
Angle of rotation of the reference (in `degrees`).
magnification : int or float
Magnification factor for the reference.
x_reflection : bool
If True, the reference is reflected parallel to the x direction
before being rotated.
name : str (optional)
A name for the reference (if provided).
"""
[docs]
def __init__(
self,
component: Component,
origin: Coordinate = (0, 0),
rotation: float = 0,
magnification: float = 1,
x_reflection: bool = False,
visual_label: str = "",
columns: int = 1,
rows: int = 1,
spacing=None,
name: str | None = None,
v1: tuple[float, float] | None = None,
v2: tuple[float, float] | None = None,
) -> None:
"""Initialize the ComponentReference object."""
self._reference = gdstk.Reference(
cell=component._cell,
origin=origin,
rotation=np.deg2rad(rotation),
magnification=magnification,
x_reflection=x_reflection,
columns=columns,
rows=rows,
spacing=spacing,
)
if v1 or v2:
self._reference.repetition = gdstk.Repetition(
columns=columns, rows=rows, v1=v1, v2=v2
)
self._ref_cell = component
self._owner = None
self._name = name
# The ports of a ComponentReference have their own unique id (uid),
# since two ComponentReferences of the same parent Component can be
# in different locations and thus do not represent the same port
self._local_ports = {
name: port._copy() for name, port in component.ports.items()
}
self.visual_label = visual_label
# self.uid = str(uuid.uuid4())[:8]
@property
def v1(self) -> tuple[float, float] | None:
return self._reference.repetition.v1
@property
def v2(self) -> tuple[float, float] | None:
return self._reference.repetition.v2
@property
def rows(self) -> int:
return self._reference.repetition.rows or 1
@property
def columns(self) -> int:
return self._reference.repetition.columns or 1
@property
def spacing(self) -> tuple[float, float] | None:
return self._reference.repetition.spacing
@property
def ref_cell(self):
return self._ref_cell
@property
def parent(self):
return self._ref_cell
@property
def origin(self):
return self._reference.origin
@origin.setter
def origin(self, value) -> None:
self._reference.origin = value
@property
def magnification(self) -> float:
return self._reference.magnification
@magnification.setter
def magnification(self, value) -> None:
self._reference.magnification = value
@property
def rotation(self) -> float:
return np.rad2deg(self._reference.rotation)
@rotation.setter
def rotation(self, value) -> None:
self._reference.rotation = np.deg2rad(value)
@property
def x_reflection(self) -> bool:
return self._reference.x_reflection
@x_reflection.setter
def x_reflection(self, value) -> None:
self._reference.x_reflection = value
def _set_ref_cell(self, value) -> None:
self._ref_cell = value
self._reference.cell = value._cell
@ref_cell.setter
def ref_cell(self, value) -> None:
self._set_ref_cell(value)
@parent.setter
def parent(self, value) -> None:
self._set_ref_cell(value)
def get_polygon_enclosure(self) -> shapely.Polygon:
import shapely
return shapely.Polygon(self._reference.convex_hull())
def get_polygon_bbox(
self,
default: float = 0.0,
top: float | None = None,
bottom: float | None = None,
right: float | None = None,
left: float | None = None,
) -> shapely.Polygon:
"""Returns shapely Polygon with padding.
Args:
default: default padding in um.
top: north padding in um.
bottom: south padding in um.
right: east padding in um.
left: west padding in um.
"""
import shapely
(xmin, ymin), (xmax, ymax) = self.bbox
top = top if top is not None else default
bottom = bottom if bottom is not None else default
right = right if right is not None else default
left = left if left is not None else default
points = [
[xmin - left, ymin - bottom],
[xmax + right, ymin - bottom],
[xmax + right, ymax + top],
[xmin - left, ymax + top],
]
return shapely.Polygon(points)
def get_polygons(
self,
by_spec: bool = False,
depth: int | None = None,
include_paths: bool = True,
as_array: bool = True,
as_shapely: bool = False,
as_shapely_merged: bool = False,
) -> list[Polygon] | dict[tuple[int, int], list[Polygon]]:
"""Return the list of polygons created by this reference.
Args:
by_spec : bool or tuple
If True, the return value is a dictionary with the
polygons of each individual pair (layer, datatype).
If set to a tuple of (layer, datatype), only polygons
with that specification are returned.
depth : integer or None
If not None, defines from how many reference levels to
retrieve polygons. References below this level will result
in a bounding box. If `by_spec` is True the key will be the
name of the referenced cell.
include_paths: If True, polygonal representation of paths are also included in the result.
as_array: when as_array=false, return the Polygon objects instead.
polygon objects have more information (especially when by_spec=False)
and are faster to retrieve.
as_shapely: returns shapely polygons.
as_shapely_merged: returns a shapely polygonize.
Returns
out : list of array-like[N][2] or dictionary
List containing the coordinates of the vertices of each
polygon, or dictionary with the list of polygons (if
`by_spec` is True).
Note:
Instances of `FlexPath` and `RobustPath` are also included in
the result by computing their polygonal boundary.
"""
return get_polygons(
instance=self,
by_spec=by_spec,
depth=depth,
include_paths=include_paths,
as_array=as_array,
as_shapely=as_shapely,
as_shapely_merged=as_shapely_merged,
)
def get_labels(self, depth: int | None = None, set_transform: bool = True):
"""Return the list of labels created by this reference.
Args:
depth: If not None, defines from how many reference levels to retrieve labels from.
set_transform: If True, labels will include the transformations from the reference.
Returns:
List containing the labels in this cell and its references.
"""
if set_transform:
return self._reference.get_labels(depth=depth)
else:
return self.parent.get_labels(depth=depth)
def get_bounding_box(self):
return self._reference.bounding_box()
@property
def layers(self) -> set[tuple[int, int]]:
return self.parent.layers
@property
def settings(self):
return self.parent.settings
def get_paths(self, depth: int | None = None):
"""Return the list of paths created by this reference.
Args:
depth: If not None, defines from how many reference levels to retrieve paths from.
Returns:
List containing the paths in this cell and its references.
"""
return self._reference.get_paths(depth=depth)
def translate(self, dx, dy) -> ComponentReference:
x0, y0 = self._reference.origin
self.origin = (x0 + dx, y0 + dy)
return self
def area(self, layer: LayerSpec | None = None) -> float:
"""Returns the area of the component.
Args:
layer: if None returns the area of the component.
If layer is specified returns the area of the component in that layer.
"""
if not layer:
return self._reference.area(False)
from gdsfactory.pdk import get_layer
layer = get_layer(layer)
layer_to_area = self._cell.area(True)
return layer_to_area[layer]
@property
def owner(self):
return self._owner
@owner.setter
def owner(self, value):
if self.owner is None or value is None:
self._owner = value
elif value != self._owner:
raise ValueError(
f"Cannot reset owner of a reference once it has already been set!"
f" Reference: {self}. Current owner: {self._owner}. "
f"Attempting to re-assign to {value!r}"
)
@property
def name(self):
return self._name
@name.setter
def name(self, value: str):
if value != self._name:
if self.owner and value in self.owner.named_references:
raise ValueError(
f"This reference's owner already has a reference with name {value!r}. Please choose another name."
)
if self.owner:
self.owner._named_references.pop(self._name, None)
self.owner._named_references[value] = self
self._name = value
def __repr__(self) -> str:
"""Return a string representation of the object."""
return f'ComponentReference (parent Component "{self.parent.name}", ports {list(self.ports.keys())}, origin {self.origin}, rotation {self.rotation}, x_reflection {self.x_reflection})'
def to_dict(self):
d = self.parent.to_dict()
d.update(
origin=self.origin,
rotation=self.rotation,
magnification=self.magnification,
x_reflection=self.x_reflection,
)
return d
@property
def bbox(self):
"""Return the bounding box of the ComponentReference.
it snaps to 3 decimals in um (0.001um = 1nm precision)
"""
bbox = self.get_bounding_box()
if bbox is None:
bbox = ((0, 0), (0, 0))
return snap_to_grid(np.array(bbox))
@classmethod
def __get_validators__(cls):
"""Get validators."""
yield cls.validate
@classmethod
def validate(cls, v, _info):
"""Check with pydantic ComponentReference valid type."""
assert isinstance(
v, ComponentReference
), f"TypeError, Got {type(v)}, expecting ComponentReference"
return v
def __getitem__(self, key: str | int):
"""Access reference ports."""
if isinstance(key, int):
key = list(self.ports.keys())[key]
if key not in self.ports:
ports = list(self.ports.keys())
raise ValueError(f"{key!r} not in {ports}")
return self.ports[key]
@property
def ports(self) -> dict[str, Port]:
"""This property allows you to access myref.ports, and receive a copy.
of the ports dict which is correctly rotated and translated.
"""
for name, port in self.parent.ports.items():
port = self.parent.ports[name]
new_center, new_orientation = self._transform_port(
port.center,
port.orientation,
self.origin,
self.rotation,
self.x_reflection,
)
if name not in self._local_ports:
self._local_ports[name] = port.copy()
self._local_ports[name].center = new_center
self._local_ports[name].orientation = (
mod(new_orientation, 360) if new_orientation else new_orientation
)
self._local_ports[name].parent = self
# Remove any ports that no longer exist in the reference's parent
parent_names = self.parent.ports.keys()
local_names = list(self._local_ports.keys())
for name in local_names:
if name not in parent_names:
self._local_ports.pop(name)
for k in list(self._local_ports):
self._local_ports[k].reference = self
return self._local_ports
@property
def info(self) -> dict[str, Any]:
return self.parent.info
@property
def metadata_child(self) -> dict[str, Any]:
return self.parent.metadata_child
@property
def size_info(self) -> SizeInfo:
return SizeInfo(self.bbox)
def pprint_ports(self) -> None:
"""Pretty print component ports."""
pprint_ports(self.ports)
def _transform_port(
self,
point: ndarray,
orientation: float,
origin: Coordinate = (0, 0),
rotation: int | None = None,
x_reflection: bool = False,
) -> tuple[ndarray, float]:
"""Apply GDS-type transformation to a port (x_ref)."""
new_point = np.array(point)
new_orientation = orientation
if x_reflection:
new_point[1] = -new_point[1]
new_orientation = None if orientation is None else -orientation
if rotation is not None:
new_point = _rotate_points(new_point, angle=rotation, center=[0, 0])
if orientation is not None:
new_orientation += rotation
if origin is not None:
new_point = new_point + np.array(origin)
if orientation is not None:
new_orientation = mod(new_orientation, 360)
return new_point, new_orientation
def _transform_point(
self,
point: ndarray,
origin: Coordinate = (0, 0),
rotation: int | None = None,
x_reflection: bool = False,
) -> ndarray:
"""Apply GDS-type transformation to a point."""
new_point = np.array(point)
if x_reflection:
new_point[1] = -new_point[1]
if rotation is not None:
new_point = _rotate_points(new_point, angle=rotation, center=[0, 0])
if origin is not None:
new_point = new_point + np.array(origin)
return new_point
def move(
self,
origin: Port | Coordinate | str = (0, 0),
destination: Port | Coordinate | str | None = None,
axis: str | None = None,
) -> ComponentReference:
"""Move the ComponentReference from origin point to destination.
Both origin and destination can be 1x2 array-like, Port, or a key
corresponding to one of the Ports in this device_ref.
Args:
origin: Port, port_name or Coordinate.
destination: Port, port_name or Coordinate.
axis: for the movement.
Returns:
ComponentReference.
"""
# If only one set of coordinates is defined, make sure it's used to move things
if destination is None:
destination = origin
origin = (0, 0)
if isinstance(origin, str):
if origin not in self.ports:
raise ValueError(f"{origin} not in {self.ports.keys()}")
origin = self.ports[origin]
origin = cast(Port, origin)
o = origin.center
elif hasattr(origin, "center"):
origin = cast(Port, origin)
o = origin.center
elif np.array(origin).size == 2:
o = origin
else:
raise ValueError(
f"move(origin={origin})\n"
f"Invalid origin = {origin!r} needs to be"
f"a coordinate, port or port name {list(self.ports.keys())}"
)
if isinstance(destination, str):
if destination not in self.ports:
raise ValueError(f"{destination} not in {self.ports.keys()}")
destination = self.ports[destination]
destination = cast(Port, destination)
d = destination.center
if hasattr(destination, "center"):
destination = cast(Port, destination)
d = destination.center
elif np.array(destination).size == 2:
d = destination
else:
raise ValueError(
f"{self.parent.name}.move(destination={destination!r}) \n"
f"Invalid destination = {destination!r} needs to be"
f"a coordinate, a port, or a valid port name {list(self.ports.keys())}"
)
# Lock one axis if necessary
if axis == "x":
d = (d[0], o[1])
if axis == "y":
d = (o[0], d[1])
# This needs to be done in two steps otherwise floating point errors can accrue
dxdy = np.array(d) - np.array(o)
self.origin = np.array(self.origin) + dxdy
self._bb_valid = False
return self
def rotate(
self,
angle: float = 45,
center: Coordinate | str | int = (0.0, 0.0),
) -> ComponentReference:
"""Return rotated ComponentReference.
Args:
angle: in degrees.
center: x, y.
"""
if angle == 0:
return self
if isinstance(center, int | str):
center = self.ports[center].center
if isinstance(center, Port):
center = center.center
self.rotation += angle
self.rotation %= 360
self.origin = _rotate_points(self.origin, angle, center)
self._bb_valid = False
return self
def mirror_x(
self, port_name: str | None = None, x0: float | None = None
) -> ComponentReference:
"""Perform horizontal mirror using x0 or port as axis (default, x0=0).
This is the default for mirror along X=x0 axis
"""
if port_name is None and x0 is None:
x0 = -self.x
if port_name is not None:
position = self.ports[port_name]
x0 = position.x
self.mirror((x0, 1), (x0, 0))
return self
def mirror_y(
self, port_name: str | None = None, y0: float | None = None
) -> ComponentReference:
"""Perform vertical mirror using y0 as axis (default, y0=0)."""
if port_name is None and y0 is None:
y0 = 0.0
if port_name is not None:
position = self.ports[port_name]
y0 = position.y
self.mirror((1, y0), (0, y0))
return self
def mirror(
self,
p1: Coordinate = (0.0, 1.0),
p2: Coordinate = (0.0, 0.0),
) -> ComponentReference:
"""Mirrors.
Args:
p1: point 1.
p2: point 2.
"""
if isinstance(p1, Port):
p1 = p1.center
if isinstance(p2, Port):
p2 = p2.center
p1 = np.array(p1)
p2 = np.array(p2)
# Translate so reflection axis passes through origin
self.origin = self.origin - p1
# Rotate so reflection axis aligns with x-axis
angle = np.arctan2((p2[1] - p1[1]), (p2[0] - p1[0])) * 180 / pi
self.origin = _rotate_points(self.origin, angle=-angle, center=(0, 0))
self.rotation -= angle
# Reflect across x-axis
self.x_reflection = not self.x_reflection
self.origin = (self.origin[0], -1 * self.origin[1])
self.rotation = -1 * self.rotation
# Un-rotate and un-translate
self.origin = _rotate_points(self.origin, angle=angle, center=(0, 0))
self.rotation += angle
self.rotation = self.rotation % 360
self.origin = self.origin + p1
self._bb_valid = False
return self
def connect(
self,
port: str | Port,
destination: Port,
overlap: float = 0.0,
preserve_orientation: bool = False,
allow_width_mismatch: bool = False,
allow_layer_mismatch: bool = False,
allow_type_mismatch: bool = False,
) -> ComponentReference:
"""Return ComponentReference where port connects to a destination.
Args:
port: origin (port, or port name) to connect.
destination: destination port.
overlap: how deep does the port go inside.
preserve_orientation: True, does not rotate the reference to align port
orientation and reference keep its orientation pre-connection.
allow_width_mismatch: if True, does not check if port width matches destination.
allow_layer_mismatch: if True, does not check if port layer matches destination.
allow_type_mismatch: if True, does not check if port type matches destination.
Returns:
ComponentReference: with correct rotation to connect to destination.
"""
from gdsfactory.pdk import get_active_pdk
pdk = get_active_pdk()
# port can either be a string with the name, port index, or an actual Port
if port in self.ports:
p = self.ports[port]
elif isinstance(port, Port):
p = port
else:
ports = list(self.ports.keys())
raise ValueError(
f"port = {port!r} not in {self.parent.name!r} ports {ports}"
)
enforce_width_mismatch = p.layer in pdk.enforce_width_mismatch_layers
allow_width_mismatch = allow_width_mismatch or not enforce_width_mismatch
if (
destination.orientation is not None
and p.orientation is not None
and not preserve_orientation
):
angle = 180 + destination.orientation - p.orientation
angle = angle % 360
self.rotate(angle=angle, center=p.center)
self.move(origin=p, destination=destination)
if destination.orientation is not None:
self.move(
-overlap
* np.array(
[
cos(destination.orientation * pi / 180),
sin(destination.orientation * pi / 180),
]
)
)
if not np.isclose(p.width, destination.width) and not allow_width_mismatch:
message = (
f"Port width mismatch: {p.width} != {destination.width} in {self.parent.name} on layer {p.layer}. "
"Use allow_width_mismatch=True to ignore"
)
if CONF.on_width_missmatch == "error":
raise ValueError(message)
elif CONF.on_width_missmatch == "warn":
warnings.warn(message)
if p.layer != destination.layer and not allow_layer_mismatch:
message = (
f"Port layer mismatch: {p.layer} != {destination.layer} in {self.parent.name}. "
"Use allow_layer_mismatch=True to ignore"
)
if CONF.on_layer_missmatch == "error":
raise ValueError(message)
elif CONF.on_layer_missmatch == "warn":
warnings.warn(message)
if p.port_type != destination.port_type and not allow_type_mismatch:
message = (
f"Port type mismatch: {p.port_type} != {destination.port_type} in {self.parent.name}. "
"Use allow_type_mismatch=True to ignore"
)
if CONF.on_type_missmatch == "error":
raise ValueError(message)
elif CONF.on_type_missmatch == "warn":
warnings.warn(message)
return self
def get_ports_list(self, **kwargs) -> list[Port]:
"""Return a list of ports.
Keyword Args:
layer: port GDS layer.
prefix: port name prefix.
orientation: in degrees.
width: port width.
layers_excluded: List of layers to exclude.
port_type: optical, electrical, ...
clockwise: if True, sort ports clockwise, False: counter-clockwise.
"""
return list(select_ports(self.ports, **kwargs).values())
def get_ports_dict(self, **kwargs) -> dict[str, Port]:
"""Return a dict of ports.
Keyword Args:
layer: port GDS layer.
prefix: port name prefix.
orientation: in degrees.
width: port width.
layers_excluded: List of layers to exclude.
port_type: optical, electrical, ...
clockwise: if True, sort ports clockwise, False: counter-clockwise.
"""
return select_ports(self.ports, **kwargs)
@property
def ports_layer(self) -> dict[str, str]:
"""Return a mapping from layer0_layer1_E0: portName."""
return map_ports_layer_to_orientation(self.ports)
def port_by_orientation_cw(self, key: str, **kwargs):
"""Return port by indexing them clockwise."""
m = map_ports_to_orientation_cw(self.ports, **kwargs)
if key not in m:
raise KeyError(f"{key} not in {list(m.keys())}")
key2 = m[key]
return self.ports[key2]
def port_by_orientation_ccw(self, key: str, **kwargs):
"""Return port by indexing them clockwise."""
m = map_ports_to_orientation_ccw(self.ports, **kwargs)
if key not in m:
raise KeyError(f"{key} not in {list(m.keys())}")
key2 = m[key]
return self.ports[key2]
def get_ports_xsize(self, **kwargs) -> float:
"""Return xdistance from east to west ports.
Keyword Args:
kwargs: orientation, port_type, layer.
"""
ports_cw = self.get_ports_list(clockwise=True, **kwargs)
ports_ccw = self.get_ports_list(clockwise=False, **kwargs)
return ports_ccw[0].x - ports_cw[0].x
def get_ports_ysize(self, **kwargs) -> float:
"""Returns ydistance from east to west ports."""
ports_cw = self.get_ports_list(clockwise=True, **kwargs)
ports_ccw = self.get_ports_list(clockwise=False, **kwargs)
return ports_ccw[0].y - ports_cw[0].y
if __name__ == "__main__":
# test_get_polygons_ref()
# test_get_polygons()
import gdsfactory as gf
c = gf.Component("parent")
wg1 = c << gf.components.straight(width=0.5, layer=(1, 0))
wg2 = c << gf.components.straight(width=0.5, layer=(2, 0))
wg2.connect("o1", wg1.ports["o2"], allow_layer_mismatch=True)
c.show(show_ports=True)
# p = ref.get_polygons(by_spec=(1, 0), as_array=False)
# c = gf.Component("parent")
# c2 = gf.Component("child")
# length = 10
# width = 0.5
# layer = (1, 0)
# c2.add_polygon([(0, 0), (length, 0), (length, width), (0, width)], layer=layer)
# c << c2
# c = gf.c.dbr()
# c.show()
# import gdsfactory as gf
# c = gf.Component()
# mzi = c.add_ref(gf.components.mzi())
# bend = c.add_ref(gf.components.bend_euler())
# bend.move("o1", mzi.ports["o2"])
# bend.move("o1", "o2")