"""Component is a canvas for geometry.
Adapted from PHIDL https://github.com/amccaugh/phidl/ by Adam McCaughan
"""
from __future__ import annotations
import datetime
import hashlib
import itertools
import math
import os
import pathlib
import uuid
import warnings
from collections import Counter
from collections.abc import Callable, Iterable, Iterator
from copy import deepcopy
from typing import TYPE_CHECKING, Any, Literal
import gdstk
import numpy as np
import orjson
import yaml
from omegaconf import DictConfig
from gdsfactory import snap
from gdsfactory.component_layout import (
CellSettings,
ComponentSpec,
Info,
Label,
_align,
_distribute,
_GeometryHelper,
_parse_layer,
get_polygons,
pprint_ports,
)
from gdsfactory.component_reference import ComponentReference, SizeInfo
from gdsfactory.config import CONF, GDSDIR_TEMP, logger
from gdsfactory.name import get_name_short
from gdsfactory.polygon import Polygon
from gdsfactory.port import (
Port,
auto_rename_ports,
auto_rename_ports_counter_clockwise,
auto_rename_ports_layer_orientation,
auto_rename_ports_orientation,
map_ports_layer_to_orientation,
map_ports_to_orientation_ccw,
map_ports_to_orientation_cw,
select_ports,
)
from gdsfactory.serialization import clean_dict
from gdsfactory.snap import snap_to_grid2x
if TYPE_CHECKING:
from gdsfactory.technology import LayerStack, LayerViews
from gdsfactory.typings import (
Coordinate,
CrossSection,
CrossSectionSpec,
Float2,
Layer,
Layers,
LayerSpec,
PathType,
Tuple,
)
valid_plotters = ["matplotlib", "klayout", "kweb"]
Axis = Literal["x", "y"]
os.environ["KWEB_FILESLOCATION"] = str(GDSDIR_TEMP)
class UncachedComponentWarning(UserWarning):
pass
class UncachedComponentError(ValueError):
pass
class MutabilityError(ValueError):
pass
def _get_dependencies(component, references_set) -> None:
for ref in component.references:
references_set.add(ref.ref_cell)
_get_dependencies(ref.ref_cell, references_set)
mutability_error_message = """
You cannot modify a Component after creation as it will affect all of its instances.
Create a new Component and add a reference to it.
For example:
# BAD
c = gf.components.bend_euler()
c.add_ref(gf.components.mzi())
# GOOD
c = gf.Component()
c.add_ref(gf.components.bend_euler())
c.add_ref(gf.components.mzi())
"""
move_error_message = """
You cannot move a Component. You can create a new Component, add a reference to the other Component and then move the reference.
For example:
# BAD
c = gf.components.straight()
c.xmin = 10
# GOOD
c = gf.Component()
ref = c.add_ref(gf.components.straight()) # or ref = c << gf.components.straight()
ref.xmin = 10
"""
_timestamp2019 = datetime.datetime.fromtimestamp(1572014192.8273)
# Global dictionary to hold counters for each name
name_counters = Counter()
valid_anchor_point_keywords = [
"ce",
"cw",
"nc",
"ne",
"nw",
"sc",
"se",
"sw",
"center",
"cc",
]
valid_anchor_value_keywords = [
"south",
"west",
"east",
"north",
]
# refer to a singular (x or y) value
valid_anchors = valid_anchor_point_keywords + valid_anchor_value_keywords
# full set of valid anchor keywords (either referring to points or values)
def _rnd(arr, precision=1e-4):
arr = np.ascontiguousarray(arr)
ndigits = round(-math.log10(precision))
return np.ascontiguousarray(arr.round(ndigits) / precision, dtype=np.int64)
[docs]
class Component(_GeometryHelper):
"""A Component is an empty canvas where you add polygons, references and ports \
(to connect to other components).
- stores settings that you use to build the component
- stores info that you want to use
- can return ports by type (optical, electrical ...)
- can return netlist for circuit simulation
- can write to GDS, OASIS
- can show in KLayout, matplotlib, 3D
- can return copy, mirror, flattened (no references)
Args:
name: component_name. Use @cell decorator for auto-naming.
with_uuid: adds unique identifier.
Properties:
info: dictionary that includes
- derived properties
- external metadata (test_protocol, docs, ...)
- simulation_settings
- function_name
- name: for the component
settings:
full: full settings passed to the function to create component.
changed: changed settings.
default: default component settings.
child: dict info from the children, if any.
"""
[docs]
def __init__(
self,
name: str = "Unnamed",
with_uuid: bool = False,
max_name_length: int | None = None,
) -> None:
"""Initialize the Component object.
Args:
name: component_name. Use @cell decorator for auto-naming.
with_uuid: adds unique identifier.
max_name_length: maximum number of characters for component name.
"""
self.uid = str(uuid.uuid4())[:8]
if with_uuid:
warnings.warn("with_uuid is deprecated. Use @cell decorator instead.")
name += f"_{self.uid}"
if name == "Unnamed":
name = f"Unnamed_{self.uid}"
name_counters[name] += 1
if name_counters[name] > 1:
name = f"{name}${name_counters[name]-1}"
self._cell = gdstk.Cell(name)
self.rename(name, max_name_length=max_name_length)
self.info: Info = Info()
self.settings: CellSettings = CellSettings()
self._locked = False
self._get_child_name = False
self._reference_names_counter = Counter()
self._reference_names_used: set[str] = set()
self._named_references: dict[str, ComponentReference] = {}
self._references: list[ComponentReference] = []
self.function_name = ""
self.module = ""
self.ports: dict[str, Port] = {}
self.child = None
def simplify(self, tolerance: float = 1e-3) -> Component:
"""Removes points from the polygon but does not change the polygon
shape by more than `tolerance` from the original. Uses the
Ramer-Douglas-Peucker algorithm.
Args:
tolerance: Tolerance value for the simplification algorithm. All points that
can be removed without changing the resulting polygon by more than
the value listed here will be removed. Also known as `epsilon` here
https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
"""
c = Component(f"{self.name}_simplified_{tolerance:.0e}")
polygons = self.get_polygons(by_spec=True, as_array=True)
for layer, points in polygons.items():
for p in points:
c.add_polygon(points=_simplify(p, tolerance=tolerance), layer=layer)
return c
@property
def references(self) -> list[ComponentReference]:
return self._references
@property
def polygons(self) -> list[Polygon]:
return self._cell.polygons
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._cell.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 labels(self) -> list[Label]:
return self._cell.labels
@property
def paths(self):
return self._cell.paths
@property
def name(self) -> str:
return self._cell.name
@name.setter
def name(self, name) -> None:
self.rename(name)
def rename(self, name: str, cache: bool = True, max_name_length: int | None = None):
from gdsfactory.cell import CACHE, remove_from_cache
if max_name_length is None:
max_name_length = CONF.max_name_length
if len(name) > max_name_length:
name_short = get_name_short(name, max_name_length=max_name_length)
warnings.warn(
f" {name} is too long. Max length is {max_name_length}. Renaming to {name_short}",
stacklevel=2,
)
name = name_short
if self.name != name:
# if this component is registered under its old name in the cache, remove it
old_name = self.name
if CACHE.get(old_name) is self:
remove_from_cache(self.name)
# cache the new name and add to counter if specified
if cache is True:
name_counters[name] += 1
if name_counters[name] > 1:
name = f"{name}${name_counters[name]-1}"
CACHE[name] = self
self._cell.name = name
def __iter__(self):
"""You can iterate over polygons, paths, labels and references."""
return itertools.chain(self.polygons, self.paths, self.labels, self.references)
def get_polygon_enclosure(self) -> Polygon:
"""Returns Polygon enclosure."""
import shapely
points = self._cell.convex_hull()
return shapely.Polygon(points)
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,
) -> Polygon:
"""Returns shapely Polygon with bounding box.
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 | tuple[int, int] = 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 a list of polygons in this cell.
Args:
by_spec: bool or layer
If True, the return value is a dictionary with the
polygons of each individual pair (layer, datatype), which
are used as keys. 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 this 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 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_dependencies(self, recursive: bool = False) -> list[Component]:
"""Return a list of Components referenced by this Component.
Args:
recursive: If True returns dependencies recursively.
"""
if not recursive:
return list({ref.parent for ref in self.references})
references_set = set()
_get_dependencies(self, references_set=references_set)
return list(references_set)
def __getitem__(self, key: str | int) -> Port:
"""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]
def __lshift__(self, element) -> ComponentReference:
"""Convenience operator equivalent to add_ref()."""
return self.add_ref(element)
def __setitem__(self, key, element):
"""Allow adding polygons and cell references.
like D['arc3'] = pg.arc()
Args:
key: Alias name.
element: Object that will be accessible by alias name.
"""
if isinstance(element, ComponentReference | Polygon):
self.named_references[key] = element
else:
raise ValueError(
f"Tried to assign alias {key!r} in Component {self.name!r},"
"but failed because the item was not a ComponentReference"
)
@classmethod
def __get_validators__(cls):
"""Get validators for the Component object."""
yield cls.validate
@classmethod
def validate(cls, v, _info):
"""Pydantic assumes component is valid if the following are true.
- name characters < pdk.cell_decorator_settings.max_name_length
- is not empty (has references or polygons)
"""
from gdsfactory.pdk import get_active_pdk
pdk = get_active_pdk()
max_name_length = pdk.cell_decorator_settings.max_name_length
assert isinstance(
v, Component
), f"TypeError, Got {type(v)}, expecting Component"
assert (
len(v.name) <= max_name_length
), f"name `{v.name}` {len(v.name)} > {max_name_length} "
return v
@property
def named_references(self) -> dict[str, ComponentReference]:
return self._named_references
def add_label(
self,
text: str = "hello",
position: tuple[float, float] = (0.0, 0.0),
magnification: float = 1.0,
rotation: float = 0,
anchor: str = "o",
layer: LayerSpec = "TEXT",
x_reflection: bool = False,
) -> Label:
"""Adds Label to the Component.
Args:
text: Label text.
position: x-, y-coordinates of the Label location.
magnification: Magnification factor for the Label text.
rotation: Angle rotation of the Label text.
anchor: {'n', 'e', 's', 'w', 'o', 'ne', 'nw', ...}
Position of the anchor relative to the text.
layer: Specific layer(s) to put Label on.
x_reflection: True reflects across the horizontal axis before rotation.
"""
from gdsfactory.pdk import get_layer
layer = get_layer(layer)
gds_layer, gds_datatype = layer
if not isinstance(text, str):
text = text
label = Label(
text=text,
origin=position,
anchor=anchor,
magnification=magnification,
rotation=rotation,
layer=gds_layer,
texttype=gds_datatype,
x_reflection=x_reflection,
)
self.add(label)
return label
@property
def bbox(self):
"""Returns the bounding box of the ComponentReference."""
bbox = self._cell.bounding_box()
if bbox is None:
bbox = ((0, 0), (0, 0))
return np.array(bbox)
@property
def ports_layer(self) -> dict[str, str]:
"""Returns a mapping from layer0_layer1_E0: portName."""
return map_ports_layer_to_orientation(self.ports)
def port_by_orientation_cw(self, key: str, **kwargs) -> Port:
"""Returns 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) -> Port:
"""Returns 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:
"""Returns xdistance from east to west ports.
Keyword Args:
layer: port GDS layer.
prefix: with in port name.
orientation: in degrees.
width: port width.
layers_excluded: List of layers to exclude.
port_type: optical, electrical, ...
"""
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.
Keyword Args:
layer: port GDS layer.
prefix: with in port name.
orientation: in degrees.
width: port width (um).
layers_excluded: List of layers to exclude.
port_type: optical, electrical, ...
"""
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
def plot_netlist(
self, with_labels: bool = True, font_weight: str = "normal", **kwargs
):
"""Plots a netlist graph with networkx.
Args:
with_labels: add label to each node.
font_weight: normal, bold.
**kwargs: keyword arguments for the get_netlist function
"""
import matplotlib.pyplot as plt
import networkx as nx
plt.figure()
netlist = self.get_netlist(**kwargs)
connections = netlist["connections"]
placements = netlist["placements"]
G = nx.Graph()
G.add_edges_from(
[
(",".join(k.split(",")[:-1]), ",".join(v.split(",")[:-1]))
for k, v in connections.items()
]
)
pos = {k: (v["x"], v["y"]) for k, v in placements.items()}
labels = {k: ",".join(k.split(",")[:1]) for k in placements.keys()}
nx.draw(
G,
with_labels=with_labels,
font_weight=font_weight,
labels=labels,
pos=pos,
)
return G
def plot_netlist_flat(
self, with_labels: bool = True, font_weight: str = "normal", **kwargs
):
"""Plots a netlist graph with networkx.
Args:
flat: if true, will plot the flat netlist
with_labels: add label to each node.
font_weight: normal, bold.
**kwargs: keyword arguments for the get_netlist function
"""
import matplotlib.pyplot as plt
import networkx as nx
plt.figure()
netlist = self.get_netlist_flat(**kwargs)
connections = netlist["connections"]
placements = netlist["placements"]
connections_list = []
for k, v_list in connections.items():
connections_list.extend(
(",".join(k.split(",")[:-1]), ",".join(v.split(",")[:-1]))
for v in v_list
)
G = nx.Graph()
G.add_edges_from(connections_list)
pos = {k: (v["x"], v["y"]) for k, v in placements.items()}
labels = {k: ",".join(k.split(",")[:1]) for k in placements.keys()}
nx.draw(
G,
with_labels=with_labels,
font_weight=font_weight,
labels=labels,
pos=pos,
)
return G
def to_yaml(self, **kwargs) -> str:
from gdsfactory.get_netlist import get_netlist_yaml
return get_netlist_yaml(self, **kwargs)
def write_netlist(self, filepath: str) -> None:
"""Write netlist in YAML."""
netlist = self.get_netlist()
netlist = clean_dict(netlist)
filepath = pathlib.Path(filepath)
filepath.write_text(yaml.dump(netlist))
def write_netlist_dot(self, filepath: str | None = None) -> None:
"""Write netlist graph in DOT format."""
from networkx.drawing.nx_agraph import write_dot
filepath = filepath or f"{self.name}.dot"
G = self.plot_netlist()
write_dot(G, filepath)
def get_netlist(self, **kwargs) -> dict[str, Any]:
"""From Component returns instances, connections and placements dict.
Keyword Args:
component: to extract netlist.
full_settings: True returns all, false changed settings.
tolerance: tolerance in nm to consider two ports connected.
exclude_port_types: optional list of port types to exclude from netlisting.
get_instance_name: function to get instance name.
allow_multiple: False to raise an error if more than two ports share the same connection.
if True, will return key: [value] pairs with [value] a list of all connected instances.
merge_info: True to merge info from the component and the netlist.
Returns:
Netlist dict (instances, connections, placements, ports)
instances: Dict of instance name and settings.
connections: Dict of Instance1Name,portName: Instance2Name,portName.
placements: Dict of instance names and placements (x, y, rotation).
ports: Dict portName: ComponentName,port.
name: name of component.
"""
from gdsfactory.get_netlist import get_netlist
return get_netlist(component=self, **kwargs)
def get_netlist_recursive(self, **kwargs) -> dict[str, DictConfig]:
"""Returns recursive netlist for a component and subcomponents.
Keyword Args:
component: to extract netlist.
component_suffix: suffix to append to each component name.
useful if to save and reload a back-annotated netlist.
get_netlist_func: function to extract individual netlists.
full_settings: True returns all, false changed settings.
tolerance: tolerance in nm to consider two ports connected.
exclude_port_types: optional list of port types to exclude from netlisting.
get_instance_name: function to get instance name.
allow_multiple: False to raise an error if more than two ports share the same connection.
if True, will return key: [value] pairs with [value] a list of all connected instances.
Returns:
Dictionary of netlists, keyed by the name of each component.
"""
from gdsfactory.get_netlist import get_netlist_recursive
return get_netlist_recursive(component=self, **kwargs)
def get_netlist_flat(self, **kwargs) -> dict[str, DictConfig]:
"""Returns a netlist where all subinstances are exposed and independently named.
Keyword Args:
component: to extract netlist.
component_suffix: suffix to append to each component name.
useful if to save and reload a back-annotated netlist.
get_netlist_func: function to extract individual netlists.
full_settings: True returns all, false changed settings.
tolerance: tolerance in nm to consider two ports connected.
exclude_port_types: optional list of port types to exclude from netlisting.
get_instance_name: function to get instance name.
allow_multiple: False to raise an error if more than two ports share the same connection.
if True, will return key: [value] pairs with [value] a list of all connected instances.
Returns:
Dictionary of netlists, keyed by the name of each component.
"""
from gdsfactory.get_netlist_flat import get_netlist_flat
return get_netlist_flat(component=self, **kwargs)
def assert_ports_on_grid(
self, grid_factor: int = 1, error_type: str = "error"
) -> None:
"""Asserts that all ports are on grid."""
for port in self.ports.values():
port.assert_on_grid(grid_factor=grid_factor, error_type=error_type)
def assert_ports_manhattan(self, error_type: str = "error") -> None:
"""Asserts that all ports are on manhattan angles (0, 90, 180, 270)."""
for port in self.ports.values():
port.assert_manhattan(error_type=error_type)
def get_ports(self, depth: int | None = 0) -> list[Port]:
"""Returns copies of all the ports of the Component, rotated and \
translated so that they're in their top-level position.
The Ports returned are copies of the originals, but each copy has the same
``uid`` as the original so that they can be traced back to the original if needed.
Args:
depth: If not None, defines from how many reference levels to retrieve Ports from.
Returns:
port_list : list of Port List of all Ports in the Component.
"""
port_list = [p._copy() for p in self.ports.values()]
if depth is None or depth > 0:
for r in self.references:
new_depth = None if depth is None else depth - 1
ref_ports = r.parent.get_ports(depth=new_depth)
# Transform ports that came from a reference
ref_ports_transformed = []
for rp in ref_ports:
new_port = rp._copy()
new_center, new_orientation = r._transform_port(
rp.center,
rp.orientation,
r.origin,
r.rotation,
r.x_reflection,
)
new_port.center = new_center
new_port.new_orientation = new_orientation
ref_ports_transformed.append(new_port)
port_list += ref_ports_transformed
return port_list
def get_ports_dict(self, **kwargs) -> dict[str, Port]:
"""Returns a dict of ports.
Keyword Args:
layer: port GDS layer.
prefix: select ports with prefix in port name.
suffix: select ports with port name suffix.
orientation: select ports with orientation in degrees.
width: select ports with port width.
layers_excluded: List of layers to exclude.
port_type: select ports with port_type (optical, electrical, vertical_te).
clockwise: if True, sort ports clockwise, False: counter-clockwise.
"""
return select_ports(self.ports, **kwargs)
def get_ports_list(self, **kwargs) -> list[Port]:
"""Returns list of ports.
Keyword Args:
layer: select ports with GDS layer.
prefix: select ports with prefix in port name.
suffix: select ports with port name suffix.
orientation: select ports with orientation in degrees.
orientation: select ports with orientation in degrees.
width: select ports with port width.
layers_excluded: List of layers to exclude.
port_type: select ports with port_type (optical, electrical, vertical_te).
clockwise: if True, sort ports clockwise, False: counter-clockwise.
"""
return list(select_ports(self.ports, **kwargs).values())
def get_ports_pandas(self):
import pandas as pd
col_spec = [
"name",
"width",
"center",
"orientation",
"layer",
"port_type",
"shear_angle",
]
return pd.DataFrame(
[port.to_dict() for port in self.get_ports_list()], columns=col_spec
)
def get_ports_polars(self):
import polars as pl
col_spec = {
"name": pl.Utf8,
"width": pl.Float64,
"center": pl.List(pl.Float64),
"orientation": pl.Float64,
"layer": pl.List(pl.UInt16),
"port_type": pl.Utf8,
"shear_angle": pl.Float64,
}
return pl.DataFrame(
[port.to_dict() for port in self.get_ports_list()], schema=col_spec
)
def ref(
self,
position: Coordinate = (0, 0),
port_id: str | None = None,
rotation: float = 0,
h_mirror: bool = False,
v_mirror: bool = False,
) -> ComponentReference:
"""Returns Component reference.
Args:
position: x, y position.
port_id: name of the port.
rotation: in degrees.
h_mirror: horizontal mirror using y axis (x, 1) (1, 0).
This is the most common mirror.
v_mirror: vertical mirror using x axis (1, y) (0, y).
"""
_ref = ComponentReference(self)
if port_id and port_id not in self.ports:
raise ValueError(f"port {port_id} not in {self.ports.keys()}")
origin = self.ports[port_id].center if port_id else (0, 0)
if h_mirror:
_ref.mirror_x(port_id)
if v_mirror:
_ref.mirror_y(port_id)
if rotation != 0:
_ref.rotate(rotation, origin)
_ref.move(origin, position)
return _ref
def ref_center(self, position=(0, 0)) -> ComponentReference:
"""Returns a reference of the component centered at (x=0, y=0)."""
si = self.size_info
yc = si.south + si.height / 2
xc = si.west + si.width / 2
center = (xc, yc)
_ref = ComponentReference(self)
_ref.move(center, position)
return _ref
def __repr__(self) -> str:
"""Return a string representation of the object."""
return (
f"{self.name}: uid {self.uid}, "
f"ports {list(self.ports.keys())}, "
f"references {list(self.named_references.keys())}, "
f"{len(self.polygons)} polygons"
)
def pprint(self) -> None:
"""Prints component info."""
try:
from rich import pretty
pretty.install()
pretty.pprint(self.to_dict())
except ImportError:
print(yaml.dump(self.to_dict()))
def pprint_ports(self, sort_by_name: bool = True, **kwargs) -> None:
"""Prints ports in a rich table.
Keyword Args:
layer: select ports with GDS layer.
prefix: select ports with prefix in port name.
suffix: select ports with port name suffix.
orientation: select ports with orientation in degrees.
orientation: select ports with orientation in degrees.
width: select ports with port width.
layers_excluded: List of layers to exclude.
port_type: select ports with port_type (optical, electrical, vertical_te).
clockwise: if True, sort ports clockwise, False: counter-clockwise.
"""
pprint_ports(self.get_ports_list(sort_by_name=sort_by_name, **kwargs))
def to_kfactory(self):
"""Converts the component to KLayout Component."""
from gdsfactory.export.to_kfactory import to_kfactory
return to_kfactory(self)
def add_port(
self,
name: str | object | None = None,
center: tuple[float, float] | None = None,
width: float | None = None,
orientation: float | None = None,
port: Port | None = None,
layer: LayerSpec | None = None,
port_type: str | None = None,
cross_section: CrossSectionSpec | None = None,
shear_angle: float | None = None,
info: Info | None = None,
) -> Port:
"""Add port to component.
You can copy an existing port like add_port(port = existing_port) or
create a new port add_port(myname, mycenter, mywidth, myorientation).
You can also copy an existing port
with a new name add_port(port = existing_port, name = new_name)
Args:
name: port name.
center: x, y.
width: in um.
orientation: in deg.
port: optional port.
layer: port layer.
port_type: optical, electrical, vertical_dc, vertical_te, vertical_tm. Defaults to optical.
cross_section: port cross_section.
shear_angle: an optional angle to shear port face in degrees.
info: contains arbitrary information about the port.
"""
from gdsfactory.pdk import get_cross_section, get_layer
layer = get_layer(layer)
if port:
if not isinstance(port, Port):
raise ValueError(f"add_port() needs a Port, got {type(port)}")
p = port.copy()
if name is not None:
p.name = name
if center is not None:
p.center = center
if width is not None:
p.width = width
if orientation is not None:
p.orientation = orientation
if port_type is not None:
p.port_type = port_type
if layer is not None:
p.layer = layer
if shear_angle is not None:
p.shear_angle = shear_angle
if cross_section is not None:
p.cross_section = cross_section
if info:
for k, v in dict(info).items():
p.info[k] = v
p.parent = self
elif isinstance(name, Port):
p = name.copy()
p.parent = self
name = p.name
elif center is None:
raise ValueError("Port needs center parameter (x, y) um.")
else:
p = Port(
name=name,
center=center,
width=width,
orientation=orientation,
parent=self,
layer=layer,
port_type=port_type or "optical",
cross_section=get_cross_section(cross_section)
if cross_section
else None,
shear_angle=shear_angle,
)
p.parent = self
if info is not None:
for k, v in dict(info).items():
p.info[k] = v
if name is not None:
p.name = name
if p.name in self.ports:
raise ValueError(f"add_port() Port name {p.name!r} exists in {self.name!r}")
self.ports[p.name] = p
return p
def add_ports(
self,
ports: Iterable[Port] | dict[str, Port],
prefix: str = "",
suffix: str = "",
**kwargs,
) -> None:
"""Add a list or dict of ports.
you can include a prefix to add to the new port names to avoid name conflicts.
Args:
ports: list or dict of ports.
prefix: to prepend to each port name.
suffix: to append to each port name.
"""
if hasattr(ports, "values"):
for port_name, port in ports.items():
name = f"{prefix}{port_name}{suffix}"
self.add_port(name=name, port=port, **kwargs)
else:
for port in ports:
name = f"{prefix}{port.name}{suffix}"
self.add_port(name=name, port=port, **kwargs)
def snap_ports_to_grid(self, grid_factor: int = 1) -> None:
for port in self.ports.values():
port.snap_to_grid(grid_factor=grid_factor)
def remove_layers(
self,
layers: list[LayerSpec],
include_labels: bool = True,
invert_selection: bool = False,
recursive: bool = True,
) -> Component:
"""Remove a list of layers and returns the same Component.
Args:
layers: list of layers to remove.
include_labels: remove labels on those layers.
invert_selection: removes all layers except layers specified.
recursive: operate on the cells included in this cell.
"""
from gdsfactory import get_layer
component = self.flatten() if recursive and self.references else self
layers = [get_layer(layer) for layer in layers]
should_remove = not invert_selection
component._cell.filter(
spec=layers,
remove=should_remove,
polygons=True,
paths=True,
labels=include_labels,
)
return component
def extract(
self,
layers: list[LayerSpec],
include_labels: bool = True,
recursive: bool = True,
) -> Component:
"""Extract polygons from a Component and returns a new Component.
Args:
layers: list of layers to extract.
include_labels: extract labels on those layers.
recursive: operate on the cells included in this cell.
"""
c = self.copy()
return c.remove_layers(
layers,
invert_selection=True,
recursive=recursive,
include_labels=include_labels,
)
def add_polygon(
self,
points,
layer: str | int | tuple[int, int] | np.nan = np.nan,
snap_to_grid: bool = False,
) -> Polygon:
"""Adds a Polygon to the Component.
Args:
points: Coordinates of the vertices of the Polygon.
layer: layer spec to add polygon on.
snap_to_grid: snap points to grid.
"""
from gdsfactory.pdk import get_layer
if layer is None:
return None
elif isinstance(layer, set):
polygons = [self.add_polygon(points, ly) for ly in layer]
return polygons[0]
layer = get_layer(layer)
if isinstance(points, gdstk.Polygon):
# if layer is unspecified or matches original polygon, just add it as-is
polygon = points
if layer is np.nan or (
isinstance(layer, tuple) and (polygon.layer, polygon.datatype) == layer
):
polygon = Polygon(polygon.points, (polygon.layer, polygon.datatype))
else:
layer, datatype = _parse_layer(layer)
polygon = Polygon(polygon.points, (layer, datatype))
if hasattr(points, "properties"):
polygon.properties = deepcopy(points.properties)
if polygon.area() > 0:
self._add_polygons(polygon)
return polygon
elif hasattr(points, "geoms"):
for geom in points.geoms:
polygon = self.add_polygon(geom, layer=layer)
return polygon
elif hasattr(points, "exterior"): # points is a shapely Polygon
return self._add_polygon_shapely(layer, points)
points = np.asarray(points)
if points.ndim == 1:
return [self.add_polygon(poly, layer=layer) for poly in points]
if layer is np.nan:
layer = 0
if points.ndim == 2:
# add single polygon from points
if len(points[0]) > 2:
# Convert to form [[1,2],[3,4],[5,6]]
points = np.column_stack(points)
points = snap.snap_to_grid2x(points) if snap_to_grid else points
layer, datatype = _parse_layer(layer)
polygon = Polygon(points, (layer, datatype))
if polygon.area() > 0:
self._add_polygons(polygon)
return polygon
elif points.ndim == 3:
layer, datatype = _parse_layer(layer)
polygons = []
for polygon_points in points:
polygon = Polygon(polygon_points, (layer, datatype))
if polygon.area() > 0:
polygons.append(polygon)
self._add_polygons(*polygons)
return polygons
else:
raise ValueError(f"Unable to add {points.ndim}-dimensional points object")
def _add_polygon_shapely(self, layer, points, snap_to_grid=False) -> Polygon:
layer, datatype = _parse_layer(layer)
points_exterior = points.exterior.coords
if snap_to_grid:
points_exterior = snap_to_grid2x(points_exterior)
polygon = Polygon(points_exterior, (layer, datatype))
if points.interiors:
return self._add_polygon_shapely_with_holes(
points, layer, datatype, polygon
)
self._add_polygons(polygon)
return polygon
def _add_polygon_shapely_with_holes(
self, points, layer, datatype, polygon, snap_to_grid=False
) -> Polygon:
from shapely import get_coordinates
points = get_coordinates(points.interiors)
if snap_to_grid:
points = np.round(points, 3)
polygon_interior = Polygon(points, (layer, datatype))
polygons = gdstk.boolean(
polygon,
polygon_interior,
operation="not",
layer=layer,
datatype=datatype,
)
for polygon in polygons:
if polygon.area() > 0:
self._add_polygons(polygon)
return polygon
def _add_polygons(self, *polygons: list[Polygon]) -> None:
self.is_unlocked()
self._cell.add(*polygons)
def copy(self, name: str | None = None) -> Component:
c = copy(self)
if name:
c.rename(name)
return c
def add_ref_container(self, component: Component) -> ComponentReference:
"""Add reference, ports and copy_child_info."""
ref = self << component
self.add_ports(ref.ports)
self.copy_child_info(component)
return ref
def copy_child_info(self, component: Component) -> None:
"""Copy and settings info from child component into parent.
Parent components can access child cells settings.
"""
if not isinstance(component, Component | ComponentReference):
raise ValueError(
f"{type(component)}" "is not a Component or ComponentReference"
)
self.child = component
for k, v in dict(component.info).items():
if k not in self.info:
self.info[k] = v
@property
def size_info(self) -> SizeInfo:
"""Size info of the component."""
return SizeInfo(self.bbox)
def is_unlocked(self) -> None:
"""Raises warning if Component is locked."""
if self._locked:
message = (
f"Component {self.name!r} is dangerous to modify as it's already "
"on cache and will change all of its references. "
+ mutability_error_message
)
if CONF.raise_error_on_mutation:
raise MutabilityError(message)
else:
warnings.warn(message)
def _add(self, element) -> None:
"""Add a new element or list of elements to this Component.
Args:
element: Polygon, ComponentReference or iterable
The element or iterable of elements to be inserted in this cell.
Raises:
MutabilityError: if component is locked.
"""
self.is_unlocked()
if isinstance(element, ComponentReference):
self._cell.add(element._reference)
self._references.append(element)
else:
self._cell.add(element)
def add(self, element) -> None:
"""Add a new element or list of elements to this Component.
Args:
element: Polygon, ComponentReference or iterable
The element or iterable of elements to be inserted in this cell.
Raises:
MutabilityError: if component is locked.
"""
if isinstance(element, ComponentReference):
self._register_reference(element)
self._add(element)
elif isinstance(element, Iterable):
for subelement in element:
self.add(subelement)
else:
self._add(element)
def add_array(
self,
component: Component,
columns: int = 2,
rows: int = 2,
spacing: tuple[float, float] = (100, 100),
alias: str | None = None,
) -> ComponentReference:
"""Creates a ComponentReference reference to a Component.
Args:
component: The referenced component.
columns: Number of columns in the array.
rows: Number of rows in the array.
spacing: array-like[2] of int or float.
Distance between adjacent columns and adjacent rows.
alias: str or None. Alias of the referenced Component.
Returns
a: ComponentReference containing references to the Component.
"""
if not isinstance(component, Component):
raise TypeError("add_array() needs a Component object.")
ref = ComponentReference(
component=component,
columns=int(round(columns)),
rows=int(round(rows)),
spacing=spacing,
)
ref.name = None
self._add(ref)
self._register_reference(reference=ref, alias=alias)
return ref
def distribute(
self,
elements: str = "all",
direction: str = "x",
spacing: float = 100.0,
separation: bool = True,
edge: str = "center",
) -> Component:
"""Distributes the specified elements in the Component.
Args:
elements: array-like of objects or 'all'. Elements to distribute.
direction: {'x', 'y'} Direction of distribution; either a line in the x-direction or y-direction.
spacing int or float. Distance between elements.
separation: bool. If True, guarantees elements are separated with a fixed spacing
between; if False, elements are spaced evenly along a grid.
edge: {'x', 'xmin', 'xmax', 'y', 'ymin', 'ymax'}
Which edge to perform the distribution along (unused if separation == True)
"""
if elements == "all":
elements = self.polygons + self.references
_distribute(
elements=elements,
direction=direction,
spacing=spacing,
separation=separation,
edge=edge,
)
return self
def align(self, elements="all", alignment: str = "ymax") -> Component:
"""Align elements in the Component.
Args:
elements : array-like of objects, or 'all'
Elements in the Component to align.
alignment : {'x', 'y', 'xmin', 'xmax', 'ymin', 'ymax'}
Which edge to align along (e.g. 'ymax' will move the elements such
that all of their topmost points are aligned).
"""
if elements == "all":
elements = self.polygons + self.references
_align(elements, alignment=alignment)
return self
def flatten(self, single_layer: LayerSpec | None = None) -> Component:
"""Returns a flattened copy of the component.
Flattens the hierarchy of the Component such that there are no longer
any references to other Components. All polygons and labels from
underlying references are copied and placed in the top-level Component.
If single_layer is specified, all polygons are moved to that layer.
Args:
single_layer: move all polygons are moved to the specified (optional).
"""
component_flat = Component()
_cell = self._cell.copy(name=component_flat.name)
_cell = _cell.flatten()
component_flat._cell = _cell
if single_layer is not None:
warnings.warn("flatten on single layer is deprecated")
component_flat.copy_child_info(self)
component_flat.add_ports(self.ports)
return component_flat
def flatten_reference(self, ref: ComponentReference) -> None:
"""From existing cell replaces reference with a flatten reference \
which has the transformations already applied.
Transformed reference keeps the original name.
Args:
ref: the reference to flatten into a new cell.
"""
from gdsfactory.functions import transformed
self.remove(ref)
new_component = transformed(ref)
self.add_ref(new_component, alias=ref.name)
def flatten_invalid_refs(self, *args, **kwargs) -> Component:
"""Flatten all invalid references."""
warnings.warn(
"flatten_invalid_refs is deprecated, use flatten_offgrid_references",
DeprecationWarning,
)
return self.flatten_offgrid_references(*args, **kwargs)
def flatten_offgrid_references(
self,
grid_size: float | None = None,
updated_components=None,
traversed_components=None,
keep_names: bool = False,
) -> Component:
"""Returns new component with flattened references so that they snap to grid.
Args:
grid_size: snap to grid size.
updated_components: set of updated components.
traversed_components: set of traversed components.
keep_names: True for writing to GDS, False for internal use.
"""
return flatten_offgrid_references_recursive(
self,
grid_size=grid_size,
updated_components=updated_components,
traversed_components=traversed_components,
keep_names=keep_names,
)
def add_ref(
self, component: Component, alias: str | None = None, **kwargs
) -> ComponentReference:
"""Add ComponentReference to the current Component.
Args:
component: Component.
alias: named_references.
Keyword Args:
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).
"""
if not isinstance(component, Component):
raise TypeError(f"type = {type(component)} needs to be a Component.")
ref = ComponentReference(component, **kwargs)
self._add(ref)
self._register_reference(reference=ref, alias=alias)
return ref
def _register_reference(
self, reference: ComponentReference, alias: str | None = None
) -> None:
component = reference.parent
reference.owner = self
if alias is None:
if reference.name is not None:
alias = reference.name
else:
prefix = (
component.function_name
if component.function_name
else component.name
)
self._reference_names_counter.update({prefix: 1})
alias = f"{prefix}_{self._reference_names_counter[prefix]}"
while alias in self._named_references:
self._reference_names_counter.update({prefix: 1})
alias = f"{prefix}_{self._reference_names_counter[prefix]}"
reference.name = alias
self._named_references[alias] = reference
@property
def layers(self) -> set[tuple[int, int]]:
"""Returns a set of the Layers in the Component."""
return self.get_layers()
def get_layers(self) -> set[tuple[int, int]]:
"""Return a set of (layer, datatype).
.. code ::
import gdsfactory as gf
gf.components.straight().get_layers() == {(1, 0), (111, 0)}
"""
polygons = self._cell.get_polygons(depth=None)
return {(polygon.layer, polygon.datatype) for polygon in polygons}
def get_layer_names(self) -> list[str]:
"""Return layer names used in the design.
.. code ::
import gdsfactory as gf
gf.components.straight().get_names() == ['WG']
"""
import gdsfactory as gf
PDK = gf.get_active_pdk()
LAYERS = PDK.layers
name_to_layer = dict(LAYERS)
layer_to_name = {v: k for k, v in name_to_layer.items()}
layer_names = []
for layer in self.layers:
if layer not in layer_to_name:
warnings.warn(f"{layer} not in LayerMap.", stacklevel=3)
else:
layer_names.append(layer_to_name[layer])
return layer_names
def _repr_html_(self):
"""Show geometry in KLayout and in matplotlib for Jupyter Notebooks."""
self.show()
fig = self.plot()
if fig and hasattr(fig, "_repr_html_"):
return fig._repr_html_()
def add_pins_triangle(
self,
port_marker_layer: Layer = (1, 10),
layer_label: Layer = (1, 10),
make_copy: bool = True,
) -> Component:
"""Returns component with triangular pins."""
from gdsfactory.add_pins import add_pins_triangle
if make_copy:
component = self.copy()
else:
component = self
add_pins_triangle(
component=component, layer=port_marker_layer, layer_label=layer_label
)
return component
def plot_klayout(
self,
show_ports: bool = True,
port_marker_layer: Layer = (1, 10),
show_labels: bool = False,
show_ruler: bool = True,
):
"""Returns klayout image.
If it fails to import klayout defaults to matplotlib.
Args:
show_ports: shows component with port markers and labels.
port_marker_layer: for the ports.
show_labels: shows labels.
show_ruler: shows ruler.
"""
if show_ports:
name = self.name
component = self.add_pins_triangle(port_marker_layer=port_marker_layer)
component.rename(name, cache=False)
else:
component = self
try:
from io import BytesIO
import klayout.db as db # noqa: F401
import klayout.lay as lay
import matplotlib.pyplot as plt
from gdsfactory.pdk import get_layer_views
gdspath = component.write_gds(logging=False)
lyp_path = gdspath.with_suffix(".lyp")
layer_views = get_layer_views()
layer_views.to_lyp(filepath=lyp_path)
layout_view = lay.LayoutView()
layout_view.load_layout(str(gdspath.absolute()))
layout_view.max_hier()
layout_view.load_layer_props(str(lyp_path))
layout_view.set_config("text-visible", "true" if show_labels else "false")
layout_view.set_config("grid-show-ruler", "true" if show_ruler else "false")
pixel_buffer = layout_view.get_pixels_with_options(800, 600)
png_data = pixel_buffer.to_png_data()
# Convert PNG data to NumPy array and display with matplotlib
with BytesIO(png_data) as f:
img_array = plt.imread(f)
# Compute the figure dimensions based on the image size and desired DPI
dpi = 80
fig_width = img_array.shape[1] / dpi
fig_height = img_array.shape[0] / dpi
fig, ax = plt.subplots(figsize=(fig_width, fig_height), dpi=dpi)
# Remove margins and display the image
ax.imshow(img_array)
ax.axis("off") # Hide axes
ax.set_position([0, 0, 1, 1]) # Set axes to occupy the full figure space
plt.subplots_adjust(
left=0, right=1, top=1, bottom=0, wspace=0, hspace=0
) # Remove any padding
plt.tight_layout(pad=0) # Ensure no space is wasted
return fig
except ImportError:
component.plot(plotter="matplotlib")
def plot_kweb(self):
"""Shows current gds in kweb."""
warnings.warn(
"Component.plot_kweb() is deprecated and will be removed in future versions of gdsfactory. "
"Use Component.plot() instead"
)
try:
import kweb.server_jupyter as kj
except Exception:
print("You need to install kweb with `pip install 'gdsfactory[cad]'`")
return self.plot_klayout()
from html import escape
from IPython.display import IFrame
from gdsfactory.pdk import get_layer_views
gdspath = self.write_gds(gdsdir=GDSDIR_TEMP, logging=False)
lyp_path = GDSDIR_TEMP / "layers.lyp"
layer_props = get_layer_views()
layer_props.to_lyp(filepath=lyp_path)
host = os.getenv("KWEB_HOST", "localhost")
port = (
kj.port
if hasattr(kj, "port") and kj.port
else int(os.getenv("KWEB_PORT", 8000))
)
src = f"http://{host}:{port}/gds/{escape(gdspath.stem+gdspath.suffix)}?layer_props={escape(str(lyp_path))}"
os.environ["KWEB_PORT"] = str(os.getenv("KWEB_PORT", port))
if not kj.jupyter_server:
port = port
while kj.is_port_in_use(port=port, host=host):
port += 1
os.environ["KWEB_PORT"] = str(port)
logger.debug(src)
kj.start()
if kj.jupyter_server:
return IFrame(
src=src,
width=1400,
height=600,
)
else:
return self.plot_klayout()
def plot_matplotlib(self, **kwargs) -> None:
"""Plot component using matplotlib.
Keyword Args:
show_ports: Sets whether ports are drawn.
show_subports: Sets whether subports (ports that belong to references) are drawn.
label_aliases: Sets whether aliases are labeled with a text name.
new_window: If True, each call to quickplot() will generate a separate window.
blocking: If True, calling quickplot() will pause execution of ("block") the
remainder of the python code until the quickplot() window is closed.
If False, the window will be opened and code will continue to run.
zoom_factor: Sets the scaling factor when zooming the quickplot window with the
mousewheel/trackpad.
interactive_zoom: Enables using mousewheel/trackpad to zoom.
fontsize: for labels.
layers_excluded: list of layers to exclude.
layer_views: layer_views colors loaded from Klayout.
min_aspect: minimum aspect ratio.
"""
from gdsfactory.quickplotter import quickplot
warnings.warn(
"Component.plot_matplotlib() is deprecated and will be removed in future versions of gdsfactory. "
"Use Component.plot() instead"
)
quickplot(self, **kwargs)
def plot(self, plotter: str | None = None, **kwargs):
"""Returns component plot using klayout, matplotlib, or kweb.
We recommend using klayout or kweb.
Klayout is good for images and kweb for responsive interactive plots.
Matplotlib is slow for rendering big layouts and is deprecated.
Args:
plotter: plot backends ('klayout').
"""
plotter = plotter or CONF.display_type
if plotter not in valid_plotters:
raise ValueError(f"{plotter!r} not in {valid_plotters}")
if plotter == "klayout":
self.plot_klayout(**kwargs)
return
elif plotter == "kweb":
return self.plot_kweb()
elif plotter == "matplotlib":
from gdsfactory.quickplotter import quickplot
quickplot(self, **kwargs)
return
def show(
self,
show_ports: bool = False,
show_subports: bool = False,
port_marker_layer: Layer = (1, 10),
**kwargs,
) -> None:
"""Show component in KLayout.
returns a copy of the Component, so the original component remains intact.
with pins markers on each port show_ports = True, and optionally also
the ports from the references (show_subports=True)
Args:
show_ports: shows component with port markers and labels.
show_subports: add ports markers and labels to references.
port_marker_layer: for the ports.
Keyword Args:
gdspath: GDS file path to write to.
gdsdir: directory for the GDS file. Defaults to /tmp/.
unit: unit size for objects in library. 1um by default.
precision: for object dimensions in the library (m). 1nm by default.
timestamp: Defaults to 2019-10-25. If None uses current time.
"""
from gdsfactory.add_pins import add_pins_triangle
from gdsfactory.show import show
component = (
self.add_pins_triangle(
port_marker_layer=port_marker_layer,
layer_label=port_marker_layer,
make_copy=True,
)
if show_ports
else self
)
if show_subports:
component = component.copy()
for reference in component.references:
if isinstance(component, ComponentReference):
add_pins_triangle(
component=component,
reference=reference,
layer=port_marker_layer,
layer_label=port_marker_layer,
)
component.rename(self.name, cache=False)
show(component, **kwargs)
def _write_library(
self,
gdspath: PathType | None = None,
gdsdir: PathType | None = None,
timestamp: datetime.datetime | None = _timestamp2019,
logging: bool = True,
with_oasis: bool = False,
with_metadata: bool = False,
with_metadata_json: bool = False,
with_netlist: bool = False,
netlist_function: Callable | None = None,
**kwargs,
) -> pathlib.Path:
"""Write component to GDS or OASIS and returns gdspath.
Args:
gdspath: GDS file path to write to.
gdsdir: directory for the GDS file. Defaults to /tmp/randomFile/gdsfactory.
timestamp: Defaults to 2019-10-25 for consistent hash.
If None uses current time.
logging: disable GDS path logging, for example for showing it in KLayout.
with_oasis: If True, file will be written to OASIS. Otherwise, file will be written to GDS.
with_metadata: writes metadata in YAML format.
with_metadata_json: writes metadata in JSON format.
with_netlist: writes netlist in JSON format.
netlist_function: The netlist function to use. You can compose a partial function with the `get_netlist` function for example with your parameters.
Keyword Args:
Keyword arguments will override the active PDK's default GdsWriteSettings and OasisWriteSettings.
Gds settings:
unit: unit size for objects in library. 1um by default.
precision: for dimensions in the library (m). 1nm by default.
on_duplicate_cell: specify how to resolve duplicate-named cells. Choose one of the following:
"warn" (default): overwrite all duplicate cells with one of the duplicates (arbitrarily).
"error": throw a ValueError when attempting to write a gds with duplicate cells.
"overwrite": overwrite all duplicate cells with one of the duplicates, without warning.
None: do not try to resolve (at your own risk!)
flatten_offgrid_references: flattens component references which have invalid transformations.
max_points: Maximal number of vertices per polygon. Polygons with more vertices that this are automatically fractured.
Oasis settings:
compression_level: Level of compression for cells (between 0 and 9).
Setting to 0 will disable cell compression, 1 gives the best speed and 9, the best compression.
detect_rectangles: Store rectangles in compressed format.
detect_trapezoids: Store trapezoids in compressed format.
circle_tolerance: Tolerance for detecting circles. If less or equal to 0, no detection is performed. Circles are stored in compressed format.
validation ("crc32", "checksum32", None): type of validation to include in the saved file.
standard_properties: Store standard OASIS properties in the file.
"""
from gdsfactory.decorators import has_valid_transformations
from gdsfactory.pdk import get_active_pdk
if gdspath and gdsdir:
warnings.warn(
"gdspath and gdsdir have both been specified. gdspath will take precedence and gdsdir will be ignored.",
stacklevel=3,
)
default_settings = get_active_pdk().gds_write_settings
default_oasis_settings = get_active_pdk().oasis_settings
explicit_gds_settings = {
k: v
for k, v in kwargs.items()
if v is not None and k in default_settings.model_dump()
}
explicit_oas_settings = {
k: v
for k, v in kwargs.items()
if v is not None and k in default_oasis_settings.model_dump()
}
# update the write settings with any settings explicitly passed
write_settings = default_settings.model_copy(update=explicit_gds_settings)
oasis_settings = default_oasis_settings.model_copy(update=explicit_oas_settings)
_check_uncached_components(
component=self, mode=write_settings.on_uncached_component
)
if write_settings.flatten_offgrid_references:
top_cell = flatten_offgrid_references_recursive(self, keep_names=True)
else:
top_cell = self
if not has_valid_transformations(self):
warnings.warn(
f"Component {self.name} has invalid transformations. "
"Try component.flatten_offgrid_references() first."
)
gdsdir = gdsdir or GDSDIR_TEMP
gdsdir = pathlib.Path(gdsdir)
if with_oasis:
gdspath = gdspath or gdsdir / f"{top_cell.name}.oas"
else:
gdspath = gdspath or gdsdir / f"{top_cell.name}.gds"
gdspath = pathlib.Path(gdspath)
gdsdir = gdspath.parent
gdsdir.mkdir(exist_ok=True, parents=True)
cells = top_cell.get_dependencies(recursive=True)
cell_names = [cell.name for cell in list(cells)]
cell_names_unique = set(cell_names)
if len(cell_names) != len(set(cell_names)):
for cell_name in cell_names_unique:
cell_names.remove(cell_name)
if write_settings.on_duplicate_cell == "error":
raise ValueError(
f"Duplicated cell names in {top_cell.name!r}: {cell_names!r}"
)
elif write_settings.on_duplicate_cell in {"warn", "overwrite"}:
if write_settings.on_duplicate_cell == "warn":
warnings.warn(
f"Duplicated cell names in {top_cell.name!r}: {cell_names}",
stacklevel=3,
)
cells_dict = {cell.name: cell._cell for cell in cells}
cells = cells_dict.values()
elif write_settings.on_duplicate_cell is not None:
raise ValueError(
f"on_duplicate_cell: {write_settings.on_duplicate_cell!r} not in (None, warn, error, overwrite)"
)
all_cells = [top_cell._cell] + sorted(cells, key=lambda cc: cc.name)
no_name_cells = [
cell.name for cell in all_cells if cell.name.startswith("Unnamed")
]
if no_name_cells:
warnings.warn(
f"Unnamed cells, {len(no_name_cells)} in {top_cell.name!r}",
stacklevel=3,
)
# for cell in all_cells:
# print(cell.name, type(cell))
lib = gdstk.Library(
name=write_settings.lib_name,
unit=write_settings.unit,
precision=write_settings.precision,
)
lib.add(top_cell._cell)
lib.add(*top_cell._cell.dependencies(True))
if with_oasis:
lib.write_oas(gdspath, **oasis_settings.dict())
else:
lib.write_gds(
gdspath, timestamp=timestamp, max_points=write_settings.max_points
)
if logging:
logger.info(f"Wrote to {str(gdspath)!r}")
if with_metadata:
metadata = gdspath.with_suffix(".yml")
metadata.write_text(self.to_dict_yaml(with_cells=True, with_ports=True))
logger.info(f"Write YAML metadata to {str(metadata)!r}")
if with_metadata_json:
metadata = gdspath.with_suffix(".json")
metadata.write_bytes(
orjson.dumps(self.to_dict(with_cells=True, with_ports=True))
)
logger.info(f"Write JSON metadata to {str(metadata)!r}")
if with_netlist:
"""
Saves the netlist_function output to a json file.
"""
import json
if netlist_function is None:
from gdsfactory.get_netlist import get_netlist
netlist_function = get_netlist
netlist_path = gdspath.with_suffix(".json")
netlist_dictionary = netlist_function(component=self, **kwargs)
netlist_path.write_text(json.dumps(netlist_dictionary, indent=2))
CONF.last_saved_files.append(gdspath)
return gdspath
def write_gds(
self,
gdspath: PathType | None = None,
gdsdir: PathType | None = None,
**kwargs,
) -> pathlib.Path:
"""Write component to GDS and returns gdspath.
Args:
gdspath: GDS file path to write to.
gdsdir: directory for the GDS file. Defaults to /tmp/randomFile/gdsfactory.
Keyword Args:
unit: unit size for objects in library. 1um by default.
precision: for dimensions in the library (m). 1nm by default.
logging: disable GDS path logging, for example for showing it in KLayout.
on_duplicate_cell: specify how to resolve duplicate-named cells. Choose one of the following:
"warn" (default): overwrite all duplicate cells with one of the duplicates (arbitrarily).
"error": throw a ValueError when attempting to write a gds with duplicate cells.
"overwrite": overwrite all duplicate cells with one of the duplicates, without warning.
on_uncached_component: Literal["warn", "error"] = "warn"
flatten_offgrid_references: flattens component references which have invalid transformations.
max_points: Maximal number of vertices per polygon.
Polygons with more vertices that this are automatically fractured.
with_metadata: writes metadata in YAML format.
with_netlist: writes a netlist in JSON format.
netlist_function: function to generate the netlist.
"""
return self._write_library(
gdspath=gdspath, gdsdir=gdsdir, with_oasis=False, **kwargs
)
def write_oas(
self,
gdspath: PathType | None = None,
gdsdir: PathType | None = None,
**kwargs,
) -> pathlib.Path:
"""Write component to GDS and returns gdspath.
Args:
gdspath: GDS file path to write to.
gdsdir: directory for the GDS file. Defaults to /tmp/randomFile/gdsfactory.
Keyword Args:
unit: unit size for objects in library. 1um by default.
precision: for dimensions in the library (m). 1nm by default.
logging: disable GDS path logging, for example for showing it in KLayout.
on_duplicate_cell: specify how to resolve duplicate-named cells. Choose one of the following:
"warn" (default): overwrite all duplicate cells with one of the duplicates (arbitrarily).
"error": throw a ValueError when attempting to write a gds with duplicate cells.
"overwrite": overwrite all duplicate cells with one of the duplicates, without warning.
None: do not try to resolve (at your own risk!)
on_uncached_component: Literal["warn", "error"] = "warn"
flatten_offgrid_references: flattens component references which have invalid transformations.
compression_level: Level of compression for cells (between 0 and 9).
Setting to 0 will disable cell compression, 1 gives the best speed and 9, the best compression.
detect_rectangles: Store rectangles in compressed format.
detect_trapezoids: Store trapezoids in compressed format.
circle_tolerance: Tolerance for detecting circles. If less or equal to 0, no detection is performed.
Circles are stored in compressed format.
validation ("crc32", "checksum32", None) – type of validation to include in the saved file.
standard_properties: Store standard OASIS properties in the file.
"""
return self._write_library(
gdspath=gdspath,
gdsdir=gdsdir,
with_oasis=True,
**kwargs,
)
def to_dict(
self,
ignore_components_prefix: list[str] | None = None,
ignore_functions_prefix: list[str] | None = None,
with_cells: bool = False,
with_ports: bool = False,
) -> dict[str, Any]:
"""Returns Dict representation of a component.
Args:
ignore_components_prefix: for components to ignore when exporting.
ignore_functions_prefix: for functions to ignore when exporting.
with_cells: write cell info recursively.
with_ports: write ports.
"""
d = self.get_component_spec().model_dump()
if with_ports:
ports = {port.name: port.to_dict() for port in self.get_ports_list()}
d["ports"] = ports
if with_cells:
cells = recurse_structures(
self,
ignore_functions_prefix=ignore_functions_prefix,
ignore_components_prefix=ignore_components_prefix,
)
d["cells"] = clean_dict(cells)
d["name"] = self.name
d["info"] = self.info.model_dump()
return d
def to_dict_yaml(self, **kwargs) -> str:
"""Write Dict representation of a component in YAML format.
Args:
ignore_components_prefix: for components to ignore when exporting.
ignore_functions_prefix: for functions to ignore when exporting.
with_cells: write cells recursively.
with_ports: write port information.
"""
return yaml.dump(clean_dict(self.to_dict(**kwargs)))
def auto_rename_ports(self, **kwargs) -> None:
"""Rename ports by orientation NSEW (north, south, east, west).
Keyword Args:
function: to rename ports.
select_ports_optical: to select optical ports.
select_ports_electrical: to select electrical ports.
prefix_optical: prefix.
prefix_electrical: prefix.
.. code::
3 4
_|__|_
2 -| |- 5
| |
1 -|______|- 6
| |
8 7
"""
auto_rename_ports(self, **kwargs)
def auto_rename_ports_counter_clockwise(self, **kwargs) -> None:
auto_rename_ports_counter_clockwise(self, **kwargs)
def auto_rename_ports_layer_orientation(self, **kwargs) -> None:
auto_rename_ports_layer_orientation(self, **kwargs)
def auto_rename_ports_orientation(self, **kwargs) -> None:
"""Rename ports by orientation NSEW (north, south, east, west).
Keyword Args:
function: to rename ports.
select_ports_optical: to select ports.
select_ports_electrical:
prefix_optical:
prefix_electrical:
.. code::
N0 N1
|___|_
W1 -| |- E1
| |
W0 -|______|- E0
| |
S0 S1
"""
auto_rename_ports_orientation(self, **kwargs)
def move(self, *args, **kwargs) -> Component:
"""Make a reference instead"""
raise ValueError(move_error_message)
def mirror(self, p1: Float2 = (0, 1), p2: Float2 = (0, 0), **kwargs) -> Component:
"""Returns new Component with a mirrored reference.
Args:
p1: first point to define mirror axis.
p2: second point to define mirror axis.
"""
from gdsfactory.functions import mirror
return mirror(component=self, p1=p1, p2=p2, **kwargs)
def rotate(self, angle: float = 90, **kwargs) -> Component:
"""Returns new component with a rotated reference to the original.
Args:
angle: in degrees.
"""
from gdsfactory.functions import rotate
return rotate(component=self, angle=angle, **kwargs)
def add_padding(self, **kwargs) -> Component:
"""Returns same component with padding.
Keyword Args:
component: for padding.
layers: list of layers.
suffix for name.
default: default padding (50um).
top: north padding.
bottom: south padding.
right: east padding.
left: west padding.
"""
from gdsfactory.add_padding import add_padding
return add_padding(component=self, **kwargs)
def absorb(self, reference) -> Component:
"""Absorbs polygons from ComponentReference into Component.
Destroys the reference in the process but keeping the polygon geometry.
Args:
reference: ComponentReference to be absorbed into the Component.
"""
if reference not in self.references:
raise ValueError(
"The reference you asked to absorb does not exist in this Component."
)
ref_polygons = reference.get_polygons(
by_spec=False, include_paths=False, as_array=False
)
self._add_polygons(*ref_polygons)
self.add(reference.get_labels())
self.add(reference.get_paths())
self.remove(reference)
return self
def remove(self, items):
"""Removes items from a Component, which can include Ports, PolygonSets \
CellReferences, ComponentReferences and Labels.
Args:
items: list of Items to be removed from the Component.
"""
if not hasattr(items, "__iter__"):
items = [items]
for item in items:
if isinstance(item, Port):
self.ports = {k: v for k, v in self.ports.items() if v != item}
elif isinstance(item, gdstk.Reference):
self._cell.remove(item)
item.owner = None
elif isinstance(item, ComponentReference):
self.references.remove(item)
self._cell.remove(item._reference)
item.owner = None
self._named_references.pop(item.name)
else:
self._cell.remove(item)
self._bb_valid = False
return self
def hash_geometry(self, precision: float = 1e-4) -> str:
"""Returns an SHA1 hash of the geometry in the Component.
For each layer, each polygon is individually hashed and then the polygon hashes
are sorted, to ensure the hash stays constant regardless of the ordering
the polygons. Similarly, the layers are sorted by (layer, datatype).
Args:
precision: Rounding precision for the the objects in the Component.
For instance, a precision of 1e-2 will round a point at
(0.124, 1.748) to (0.12, 1.75).
"""
polygons_by_spec = self.get_polygons(by_spec=True, as_array=False)
layers = np.array(list(polygons_by_spec.keys()))
final_hash = hashlib.sha1()
for layer in layers:
layer_hash = hashlib.sha1(layer.astype(np.int64)).digest()
polygons = polygons_by_spec[tuple(layer)]
polygons = [_rnd(p.points, precision) for p in polygons]
polygon_hashes = np.sort([hashlib.sha1(p).digest() for p in polygons])
final_hash.update(layer_hash)
for ph in polygon_hashes:
final_hash.update(ph)
return final_hash.hexdigest()
def get_labels(
self, apply_repetitions=True, depth: int | None = None, layer=None
) -> list[Label]:
"""Return labels.
Args:
apply_repetitions:.
depth: None returns all labels and 0 top level.
layer: layerspec.
"""
from gdsfactory.pdk import get_layer
if layer:
layer, texttype = get_layer(layer)
else:
texttype = None
return self._cell.get_labels(
apply_repetitions=apply_repetitions,
depth=depth,
layer=layer,
texttype=texttype,
)
def remove_labels(self) -> None:
"""Remove labels."""
self._cell.remove(*self.labels)
def remap_layers(self, layermap, new_copy: bool = True, **kwargs) -> Component:
"""Returns a copy of the component with remapped layers, unless `new_copy` is set to False, in which case it modifies the current Component in place. It's important to be aware that modifying the current Component can have side effects on any references to it.
Args:
layermap: Dictionary of values in format {layer_from: layer_to}.
new_copy: If True, returns a new Component. If False, modifies the current Component in place, potentially affecting references to it.
"""
if kwargs:
warnings.warn("{kwargs.keys} is deprecated.", DeprecationWarning)
component = self.copy() if new_copy else self
layermap = {_parse_layer(k): _parse_layer(v) for k, v in layermap.items()}
cells = list(component.get_dependencies(True))
cells.append(component)
for cell in cells:
cell._cell.remap(layermap)
return component
def to_3d(
self,
layer_views: LayerViews | None = None,
layer_stack: LayerStack | None = None,
exclude_layers: tuple[Layer, ...] | None = None,
):
"""Return Component 3D trimesh Scene.
Args:
component: to extrude in 3D.
layer_views: layer colors from Klayout Layer Properties file.
Defaults to active PDK.layer_views.
layer_stack: contains thickness and zmin for each layer.
Defaults to active PDK.layer_stack.
exclude_layers: layers to exclude.
"""
from gdsfactory.export.to_3d import to_3d
return to_3d(
self,
layer_views=layer_views,
layer_stack=layer_stack,
exclude_layers=exclude_layers,
)
def to_np(
self,
nm_per_pixel: int = 20,
layers: Layers = ((1, 0),),
values: tuple[float, ...] | None = None,
pad_width: int = 1,
) -> np.ndarray:
"""Returns a pixelated numpy array from Component polygons.
Args:
component: Component.
nm_per_pixel: you can go from 20 (coarse) to 4 (fine).
layers: to convert. Order matters (latter overwrite former).
values: associated to each layer (defaults to 1).
pad_width: padding pixels around the image.
"""
from gdsfactory.export.to_np import to_np
return to_np(
self,
nm_per_pixel=nm_per_pixel,
layers=layers,
values=values,
pad_width=pad_width,
)
def write_stl(
self,
filepath: str,
layer_stack: LayerStack | None = None,
exclude_layers: tuple[Layer, ...] | None = None,
use_layer_name: bool = False,
hull_invalid_polygons: bool = True,
scale: float | None = None,
) -> None:
"""Write a Component to STL for 3D printing.
Args:
filepath: to write STL to.
layer_stack: contains thickness and zmin for each layer.
exclude_layers: layers to exclude.
use_layer_name: If True, uses LayerLevel names in output filenames rather than gds_layer and gds_datatype.
hull_invalid_polygons: If True, replaces invalid polygons (determined by shapely.Polygon.is_valid) with its convex hull.
scale: Optional factor by which to scale meshes before writing.
"""
from gdsfactory.export.to_stl import to_stl
to_stl(
self,
filepath=filepath,
layer_stack=layer_stack,
exclude_layers=exclude_layers,
use_layer_name=use_layer_name,
hull_invalid_polygons=hull_invalid_polygons,
scale=scale,
)
def write_gerber(self, dirpath, layermap_to_gerber_layer, options) -> None:
"""
Args:
dirpath: directory to write gerber files to.
layermap_to_gerber_layer: dictionary of layermap to gerber layer.
options: dictionary of options for gerber export.
header: List[str] | None = None
mode: Literal["mm", "in"] = "mm"
resolution: float = 1e-6
int_size: int = 4
"""
from gdsfactory.export.to_gerber import to_gerber
to_gerber(
self,
dirpath=dirpath,
layermap_to_gerber_layer=layermap_to_gerber_layer,
options=options,
)
def to_gmsh(self, *args, **kwargs) -> None:
"""Deprecated. instead of.
mesh = component.to_gmsh(arguments)
Use:
from gplugins.gmsh.get_mesh import get_mesh
"""
raise ValueError(
"""component.to_gmsh() has been deprecated. Instead of:
mesh = component.to_gmsh(arguments)
Use:
from gplugins.gmsh.get_mesh import get_mesh
mesh = get_mesh(component, arguments)
"""
)
def offset(
self,
distance: float = 0.1,
use_union: bool = True,
precision: float = 1e-4,
join: str = "miter",
tolerance: int = 2,
layer: LayerSpec = "WG",
) -> Component:
"""Returns new Component with polygons eroded or dilated by an offset.
Args:
distance: Distance to offset polygons. Positive values expand, negative shrink.
use_union: If True, use union of all polygons to offset. If False, offset
precision: Desired precision for rounding vertex coordinates.
join: {'miter', 'bevel', 'round'} Type of join used to create polygon offset
tolerance: For miter joints, this number must be at least 2 represents the
maximal distance in multiples of offset between new vertices and their
original position before beveling to avoid spikes at acute joints. For
round joints, it indicates the curvature resolution in number of
points per full circle.
layer: Specific layer to put polygon geometry on.
"""
from gdsfactory.geometry.offset import offset
return offset(
self,
distance=distance,
use_union=use_union,
precision=precision,
join=join,
tolerance=tolerance,
layer=layer,
)
def add_route_info(
self,
cross_section: CrossSection | str,
length: float,
length_eff: float | None = None,
taper: bool = False,
**kwargs,
) -> None:
"""Adds route information to a component.
Args:
cross_section: CrossSection or name of the cross_section.
length: length of the route.
length_eff: effective length of the route.
taper: if True adds taper information.
**kwargs: extra information to add to the component.
"""
from gdsfactory.pdk import get_active_pdk
pdk = get_active_pdk()
length_eff = length_eff or length
length_eff = float(length_eff)
xs_name = (
cross_section
if isinstance(cross_section, str)
else pdk.get_cross_section_name(cross_section)
)
info = self.info
if taper:
info[f"route_info_{xs_name}_taper_length"] = length
info["route_info_type"] = xs_name
info["route_info_length"] = length_eff
info["route_info_weight"] = length_eff
info[f"route_info_{xs_name}_length"] = length_eff
for key, value in kwargs.items():
info[f"route_info_{key}"] = value
def get_component_spec(self) -> ComponentSpec:
return ComponentSpec(
function=self.function_name,
module=self.module,
settings=self.settings,
)
# Deprecated
@property
def metadata_child(self) -> dict:
"""Returns metadata from child if any, Otherwise returns component own.
metadata can access the children metadata at the bottom of the hierarchy.
"""
warnings.warn(
"metadata_child is deprecated and will be removed in future versions of gdsfactory"
)
settings = dict(self.settings)
while settings.get("child"):
settings = settings.get("child")
return dict(settings)
def get_info(self):
"""Gathers the .info dictionaries from every sub-Component and returns them in a list.
Args:
depth: int or None
If not None, defines from how many reference levels to
retrieve Ports from.
Returns:
list of dictionaries
List of the ".info" property dictionaries from all sub-Components
"""
warnings.warn(
"get_info is deprecated and will be removed in future versions of gdsfactory"
)
D_list = self.get_dependencies(recursive=True)
return [D.info.model_copy() for D in D_list]
def get_netlist_yaml(self, **kwargs) -> dict[str, Any]:
from gdsfactory.get_netlist import get_netlist_yaml
warnings.warn(
"get_netlist_yaml is deprecated and will be removed in future versions of gdsfactory"
"Use to_yaml instead"
)
return get_netlist_yaml(self, **kwargs)
def get_setting(self, setting: str) -> str | int | float:
warnings.warn(
"get_setting is deprecated and will be removed in future versions of gdsfactory"
)
return (
self.info.get(setting)
or self.settings.get(setting)
or self.metadata_child.get(setting)
)
def unlock(self) -> None:
"""Only do this if you know what you are doing."""
warnings.warn("we will remove unlock to discourage use")
self._locked = False
def lock(self) -> None:
"""Makes sure components can't add new elements or move existing ones.
Components lock automatically when going into the CACHE to
ensure one component does not change others
"""
warnings.warn(
f"we will remove lock to discourage use. Using it in {self.name!r}"
)
self._locked = True
@property
def metadata(self) -> dict:
warnings.warn(
"metadata is deprecated and will be removed in future versions of gdsfactory. "
"Use component.settings for accessing component settings or component.info for component info."
)
return dict(self.settings)
def __reduce__(self):
"""Gdstk Cells cannot be directly pickled. This method overrides binary serialization with GDS serialization."""
return deserialize_gds, serialize_gds(self)
# Component functions
def serialize_gds(component: Component) -> Tuple[PathType]:
"""Saves Component as GDS + YAML metadata in temporary files with unique name."""
gds_filepath = GDSDIR_TEMP / component.name
gds_filepath = gds_filepath.with_suffix(".gds")
component.write_gds(gds_filepath, with_metadata=True)
return (gds_filepath,)
def _line_distances(points, start, end):
if np.all(start == end):
return np.linalg.norm(points - start, axis=1)
vec = end - start
cross = np.cross(vec, start - points)
return np.divide(abs(cross), np.linalg.norm(vec))
def _simplify(points, tolerance=0):
"""Ramer–Douglas–Peucker algorithm for line simplification. Takes an
array of points of shape (N,2) and removes excess points in the line. The
remaining points form a identical line to within `tolerance` from the
original
"""
# From https://github.com/fhirschmann/rdp/issues/7
# originally written by Kirill Konevets https://github.com/kkonevets
M = np.asarray(points)
start, end = M[0], M[-1]
dists = _line_distances(M, start, end)
index = np.argmax(dists)
dmax = dists[index]
if dmax > tolerance:
result1 = _simplify(M[: index + 1], tolerance)
result2 = _simplify(M[index:], tolerance)
result = np.vstack((result1[:-1], result2))
else:
result = np.array([start, end])
return result
def deserialize_gds(gds_filepath: PathType) -> Component:
"""Loads Component as GDS + YAML metadata from temporary files, and deletes them."""
from gdsfactory.read import import_gds
c = import_gds(gds_filepath, read_metadata=True)
metadata_filepath = gds_filepath.with_suffix(".yml")
metadata_filepath.unlink()
gds_filepath.unlink()
return c
def copy(
D: Component,
references=None,
ports=None,
polygons=None,
paths=None,
name=None,
labels=None,
) -> Component:
"""Returns a Component copy.
Args:
D: component to copy.
references: references to copy.
ports: ports to copy.
polygons: polygons to copy.
paths: paths to copy.
name: name of the new component.
labels: labels to copy.
"""
c = Component()
c.settings = D.settings.model_copy()
c.info = D.info.model_copy()
c.child = D.child
c.function_name = D.function_name
c.module = D.module
for ref in references if references is not None else D.references:
c.add(copy_reference(ref))
for port in (ports if ports is not None else D.ports).values():
c.add_port(port=port)
for poly in polygons if polygons is not None else D.polygons:
c.add_polygon(poly)
for path in paths if paths is not None else D.paths:
c.add(path)
for label in labels if labels is not None else D.labels:
c.add_label(
text=label.text,
position=label.origin,
layer=(label.layer, label.texttype),
)
if name is not None:
c.name = name
return c
def copy_reference(
ref,
parent=None,
columns=None,
rows=None,
spacing=None,
origin=None,
rotation=None,
magnification=None,
x_reflection=None,
name=None,
v1=None,
v2=None,
) -> ComponentReference:
return ComponentReference(
component=parent or ref.parent,
columns=columns or ref.columns,
rows=rows or ref.rows,
spacing=spacing or ref.spacing,
origin=origin or ref.origin,
rotation=rotation or ref.rotation,
magnification=magnification or ref.magnification,
x_reflection=x_reflection or ref.x_reflection,
name=name or ref.name,
v1=v1 or ref.v1,
v2=v2 or ref.v2,
)
def _filter_polys(polygons, layers_excl):
return [
polygon
for polygon, layer, datatype in zip(
polygons.polygons, polygons.layers, polygons.datatypes
)
if (layer, datatype) not in layers_excl
]
def recurse_structures(
component: Component,
ignore_components_prefix: list[str] | None = None,
ignore_functions_prefix: list[str] | None = None,
) -> dict[str, Any]:
"""Recurse component and components references recursively.
Args:
component: component to recurse.
ignore_components_prefix: list of prefix to ignore.
ignore_functions_prefix: list of prefix to ignore.
"""
ignore_functions_prefix = ignore_functions_prefix or []
ignore_components_prefix = ignore_components_prefix or []
if component.function_name and component.function_name in ignore_functions_prefix:
return {}
if hasattr(component, "name") and any(
component.name.startswith(i) for i in ignore_components_prefix
):
return {}
output = {component.name: dict(component.settings)}
for reference in component.references:
if (
isinstance(reference, ComponentReference)
and reference.ref_cell.name not in output
):
output.update(recurse_structures(reference.ref_cell))
return output
def get_base_components(
component: gf.Component, allow_empty: bool = True
) -> Iterator[gf.Component]:
"""Generator function that yields base components of a given component.
Parameters:
component (gf.Component): The component whose base components are to be found.
allow_empty (bool): If True, allows yielding of components without polygons.
Yields:
gf.Component: The base components of the given component.
"""
if not component.references and (component.polygons or allow_empty):
yield component
for ref in component.references:
yield from get_base_components(ref.parent, allow_empty)
def flatten_offgrid_references_recursive(
component: Component,
grid_size: float | None = None,
updated_components=None,
traversed_components=None,
keep_names: bool = False,
) -> Component:
"""Recursively flattens component references which have invalid transformations
(i.e. non-90 deg rotations or sub-grid translations)
returns a copy if any subcells have been modified.
WARNING: this function will produce same-name copies of cells.
It is strictly meant to be used on write of the GDS file and
should not be mixed with other cells,
or you will likely experience issues with duplicate cells
Args:
component: the component to fix (in place).
grid_size: the GDS grid size, in um, defaults to active PDK.get_grid_size()
any translations with higher resolution than this are considered invalid.
updated_components: dict of components transformed.
Should always be None, except for recursive.
traversed_components: the set of component names which have been traversed.
Should always be None, except for recursive invocations.
keep_names: True for writing to GDS, False for internal use.
"""
from gdsfactory.decorators import is_invalid_ref
invalid_refs = []
refs = component.references
subcell_modified = False
updated_components = updated_components or {}
traversed_components = traversed_components or set()
for ref in refs:
# mark any invalid refs for flattening
# otherwise, check if there are any modified cells beneath (we need not do this if the ref will be flattened anyways)
if is_invalid_ref(ref, grid_size):
invalid_refs.append(ref.name)
else:
# otherwise, recursively flatten refs if the subcell has not already been traversed
if ref.parent.name not in traversed_components:
flatten_offgrid_references_recursive(
ref.parent,
grid_size=grid_size,
updated_components=updated_components,
traversed_components=traversed_components,
)
# now, if the ref's cell been modified, mark it as such
if ref.parent.name in updated_components:
subcell_modified = True
if invalid_refs or subcell_modified:
# if the cell or subcells need to have references flattened, create an uncached copy of this cell for export
new_component = component.copy()
if keep_names:
new_component.rename(component.name, cache=False)
else:
new_component.rename(f"{component.name}_offgrid")
# make sure all modified cells have their references updated
new_refs = new_component.references.copy()
for ref in new_refs:
if ref.name in invalid_refs:
new_component.flatten_reference(ref)
elif (
ref.parent.name in updated_components
and ref.parent is not updated_components[ref.parent.name]
):
ref.parent = updated_components[ref.parent.name]
component = new_component
updated_components[component.name] = new_component
traversed_components.add(component.name)
return component
def _check_uncached_components(component, mode):
valid_modes = ["warn", "error", "ignore"]
if mode == "ignore":
return
elif mode not in valid_modes:
raise ValueError(
f"{mode} is not a valid value for on_uncached_component. Try one of these: {valid_modes}."
)
for sub_component in component.get_dependencies(recursive=True):
if not sub_component._locked:
message = (
f"Component {sub_component.name!r} was NOT properly locked. "
"You need to write it into a function that has the @cell decorator."
)
if mode == "warn":
warnings.warn(message, UncachedComponentWarning, stacklevel=3)
elif mode == "error":
raise UncachedComponentError(message)
if __name__ == "__main__":
# from functools import partial
import gdsfactory as gf
# c = gf.c.mzi()
# c = c.simplify(200e-3)
c = gf.Component()
s = c << gf.c.straight()
b = c << gf.c.bend_circular(width=1)
b.connect("o1", s.ports["o2"])
c.show()
# c1 = gf.Component()
# c2 = gf.Component()
# print(c1.name)
# print(c2.name)
# c = gf.components.straight(length=1)
# cc = gf.routing.add_fiber_array(c)
# print(c.hash_geometry())
# c2 = c.flatten()
# c = gf.routing.add_fiber_single(c)
# c = gf.components.mzi(info=dict(hi=3))
# print(type(c.info))
# yaml_netlist = c.to_yaml()
# c2 = gf.read.from_yaml(yaml_netlist)
# c2.show()
# c = gf.Component()
# 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"])
# custom_padding = partial(gf.add_padding, layers=("WG",))
# c = gf.c.mzi(decorator=custom_padding)
# c = c.copy()
# c = c.remap_layers({(1, 0): (3, 0)})
# c._cell.remap({(1, 0): (3, 0)})
# lib = gdstk.Library()
# lib.add(c._cell)
# lib.remap({(1, 0): (2, 0)})
# c2 = lib[c.name]
# c._cell = c2
# c.show()
# gf.config.enable_offgrid_ports()
# c = gf.Component("bend")
# b = c << gf.components.bend_circular(angle=30)
# s = c << gf.components.straight(length=5)
# s.connect("o1", b.ports["o2"])
# p_shapely = c.get_polygons(as_shapely_merged=True)
# c2 = gf.Component("bend_fixed")
# c2.add_polygon(p_shapely, layer=(1, 0))
# c2.plot()
# c = gf.c.mzi(flatten=True, decorator=gf.routing.add_fiber_single)
# # print(c.name)
# c.show()
# c = gf.c.mzi()
# fig = c.plot_klayout()
# fig.savefig("mzi.png")
# c.pprint_ports()
# c = gf.Component("hi" * 200)
# print(c.name)
# c = gf.Component("hi" * 200)
# print(c.name)
# p = c.add_polygon(
# [(-8, 6, 7, 9), (-6, 8, 17, 5)], layer=(1, 0)
# ) # GDS layers are tuples of ints (but if we use only one number it assumes the other number is 0)
# c.write_gds("hi.gds")
# c.show()
# print(CONF.last_saved_files)