from __future__ import annotations
from collections import defaultdict
from collections.abc import Mapping, Sequence
from typing import TYPE_CHECKING, Any, Literal, TypeAlias, TypeVar
import kfactory as kf
from kfactory.layer import LayerEnum
from pydantic import BaseModel, Field, field_validator
from rich.console import Console
from rich.table import Table
import gdsfactory as gf
from gdsfactory.technology.layer_views import LayerViews
from gdsfactory.typings import LayerSpec
if TYPE_CHECKING:
from gdsfactory.component import Component
T = TypeVar("T", bound="AbstractLayer")
[docs]
class AbstractLayer(BaseModel):
"""Generic design layer.
Attributes:
sizings_xoffsets: sequence of xoffset sizings to apply to this Logical or Derived layer.
sizings_yoffsets: sequence of yoffset sizings to apply to this Logical or Derived layer.
sizings_modes: sequence of sizing modes to apply to this Logical or Derived layer.
"""
sizings_xoffsets: Sequence[int] = (0,)
sizings_yoffsets: Sequence[int] = (0,)
sizings_modes: Sequence[int] = (2,)
def _perform_operation(
self, other: AbstractLayer, operation: Literal["and", "or", "xor", "not"]
) -> DerivedLayer:
if isinstance(other, DerivedLayer | LogicalLayer) and isinstance(
self, DerivedLayer | LogicalLayer
):
return DerivedLayer(layer1=self, layer2=other, operation=operation)
raise ValueError(f"{other} is not a DerivedLayer or LogicalLayer")
# Boolean AND (&)
def __and__(self, other: AbstractLayer) -> DerivedLayer:
"""Represents boolean AND (&) operation between two layers.
Args:
other (AbstractLayer): Another Layer object to perform AND operation.
Returns:
A new DerivedLayer with the AND operation logged.
"""
return self._perform_operation(other, "and")
# Boolean OR (|, +)
def __or__(self, other: AbstractLayer) -> DerivedLayer:
"""Represents boolean OR (|) operation between two layers.
Args:
other (AbstractLayer): Another Layer object to perform OR operation.
Returns:
A new DerivedLayer with the OR operation logged.
"""
return self._perform_operation(other, "or")
def __add__(self, other: AbstractLayer) -> DerivedLayer:
"""Represents boolean OR (+) operation between two derived layers.
Args:
other (AbstractLayer): Another Layer object to perform OR operation.
Returns:
A new DerivedLayer with the AND operation logged.
"""
return self._perform_operation(other, "or")
# Boolean XOR (^)
def __xor__(self, other: AbstractLayer) -> DerivedLayer:
"""Represents boolean XOR (^) operation between two derived layers.
Args:
other (AbstractLayer): Another Layer object to perform XOR operation.
Returns:
A new DerivedLayer with the XOR operation logged.
"""
return self._perform_operation(other, "xor")
# Boolean NOT (-)
def __sub__(self, other: AbstractLayer) -> DerivedLayer:
"""Represents boolean NOT (-) operation on a derived layer.
Args:
other (AbstractLayer): Another Layer object to perform NOT operation.
Returns:
A new DerivedLayer with the NOT operation logged.
"""
return self._perform_operation(other, "not")
def sized(
self: T,
xoffset: int | tuple[int, ...],
yoffset: int | tuple[int, ...] | None = None,
mode: int | tuple[int, ...] | None = None,
) -> T:
"""Accumulates a list of sizing operations for the layer by the provided offset (in dbu).
Args:
xoffset (int | tuple): number of dbu units to buffer by. Can be a tuple for sequential sizing operations.
yoffset (int | tuple): number of dbu units to buffer by in the y-direction. If not specified, uses xfactor. Can be a tuple for sequential sizing operations.
mode (int | tuple): mode of the sizing operation(s). Can be a tuple for sequential sizing operations.
"""
# Validate inputs
xoffset_list: list[int]
if isinstance(xoffset, int):
xoffset_list = [xoffset]
else:
xoffset_list = list(xoffset)
yoffset_list: list[int]
if isinstance(yoffset, tuple):
if len(yoffset) != len(xoffset_list):
raise ValueError(
"If yoffset is provided as a tuple, length must be equal to xoffset!"
)
else:
yoffset_list = list(yoffset)
elif yoffset is None:
yoffset_list = xoffset_list
else:
yoffset_list = [yoffset] * len(xoffset_list)
mode_list: list[int]
if isinstance(mode, tuple):
if len(mode) != len(xoffset_list):
raise ValueError(
"If mode is provided as a tuple, length must be equal to xoffset!"
)
else:
mode_list = list(mode)
elif mode is None:
mode_list = [2] * len(xoffset_list)
else:
mode_list = [mode] * len(xoffset_list)
# Accumulate
sizings_xoffsets = list(self.sizings_xoffsets) + xoffset_list
sizings_yoffsets = list(self.sizings_yoffsets) + yoffset_list
sizings_modes = list(self.sizings_modes) + mode_list
# Return a copy of the layer with updated sizings
current_layer_attributes = self.__dict__.copy()
current_layer_attributes["sizings_xoffsets"] = sizings_xoffsets
current_layer_attributes["sizings_yoffsets"] = sizings_yoffsets
current_layer_attributes["sizings_modes"] = sizings_modes
return self.__class__(**current_layer_attributes)
[docs]
class LogicalLayer(AbstractLayer):
"""GDS design layer."""
layer: LayerSpec
def __eq__(self, other: object) -> bool:
"""Check if two LogicalLayer instances are equal.
This method compares the 'layer' attribute of the two LogicalLayer instances.
Args:
other (LogicalLayer): The other LogicalLayer instance to compare with.
Returns:
bool: True if the 'layer' attributes are equal, False otherwise.
Raises:
NotImplementedError: If 'other' is not an instance of LogicalLayer.
"""
if not isinstance(other, type(self)):
raise NotImplementedError(f"{other} is not a {type(self)}")
return self.layer == other.layer
def __hash__(self) -> int:
"""Generates a hash value for a LogicalLayer instance.
This method allows LogicalLayer instances to be used in hash-based data structures such as sets and dictionaries.
Returns:
int: The hash value of the layer attribute.
"""
return hash(self.layer)
def get_shapes(self, component: "Component") -> kf.kdb.Region:
"""Return the shapes of the component argument corresponding to this layer.
Arguments:
component: Component from which to extract shapes on this layer.
Returns:
kf.kdb.Region: A region of polygons on this layer.
"""
from gdsfactory.pdk import get_layer
polygons_per_layer = component.get_polygons()
layer_index = get_layer(self.layer)
polygons = (
polygons_per_layer[layer_index] if layer_index in polygons_per_layer else []
)
region = kf.kdb.Region(polygons)
if not (
all(v == 0 for v in self.sizings_xoffsets)
and all(v == 0 for v in self.sizings_yoffsets)
):
for xoffset, yoffset, mode in zip(
self.sizings_xoffsets, self.sizings_yoffsets, self.sizings_modes
):
region = region.sized(xoffset, yoffset, mode)
return region
def __repr__(self) -> str:
"""Print text representation."""
return f"{self.layer}"
__str__ = __repr__
[docs]
class DerivedLayer(AbstractLayer):
"""Physical "derived layer", resulting from a combination of GDS design layers. Can be used by renderers and simulators.
Overloads operators for simpler expressions.
Attributes:
input_layer1: primary layer comprising the derived layer. Can be a GDS design layer (kf.kcell.LayerEnum , tuple[int, int]), or another derived layer.
input_layer2: secondary layer comprising the derived layer. Can be a GDS design layer (kf.kcell.LayerEnum , tuple[int, int]), or another derived layer.
operation: operation to perform between layer1 and layer2. One of "and", "or", "xor", or "not" or associated symbols.
"""
layer1: DerivedLayer | LogicalLayer
layer2: DerivedLayer | LogicalLayer
operation: Literal["and", "&", "or", "|", "xor", "^", "not", "-"]
def __hash__(self) -> int:
"""Generates a hash value for a LogicalLayer instance.
This method allows LogicalLayer instances to be used in hash-based data structures such as sets and dictionaries.
Returns:
int: The hash value of the layer attribute.
"""
return hash((self.layer1.__hash__(), self.layer2.__hash__(), self.operation))
@property
def keyword_to_symbol(self) -> dict[str, str]:
return {
"and": "&",
"or": "|",
"xor": "^",
"not": "-",
}
@property
def symbol_to_keyword(self) -> dict[str, str]:
return {
"&": "and",
"|": "or",
"^": "xor",
"-": "not",
}
def get_symbol(self) -> str:
if self.operation in self.keyword_to_symbol:
return self.keyword_to_symbol[self.operation]
else:
return self.operation
def get_shapes(self, component: "Component") -> kf.kdb.Region:
"""Return the shapes of the component argument corresponding to this layer.
Arguments:
component: Component from which to extract shapes on this layer.
Returns:
kf.kdb.Region: A region of polygons on this layer.
"""
from gdsfactory.component import boolean_operations
r1 = self.layer1.get_shapes(component)
r2 = self.layer2.get_shapes(component)
region = boolean_operations[self.operation](r1, r2)
if not (
all(v == 0 for v in self.sizings_xoffsets)
and all(v == 0 for v in self.sizings_yoffsets)
):
for xoffset, yoffset, mode in zip(
self.sizings_xoffsets, self.sizings_yoffsets, self.sizings_modes
):
region = region.sized(xoffset, yoffset, mode)
return region
def __repr__(self) -> str:
"""Print text representation."""
return f"({self.layer1} {self.get_symbol()} {self.layer2})"
__str__ = __repr__
BroadLayer: TypeAlias = (
LogicalLayer | DerivedLayer | int | str | tuple[int, int] | LayerEnum
)
[docs]
class LayerLevel(BaseModel):
"""Level for 3D LayerStack.
Parameters:
name: str
layer: LogicalLayer or DerivedLayer. DerivedLayers can be composed of operations consisting of multiple other GDSLayers or other DerivedLayers.
derived_layer: if the layer is derived, LogicalLayer to assign to the derived layer.
thickness: layer thickness in um.
thickness_tolerance: layer thickness tolerance in um.
width_tolerance: layer width tolerance in um.
zmin: height position where material starts in um.
zmin_tolerance: layer height tolerance in um.
sidewall_angle: in degrees with respect to normal.
sidewall_angle_tolerance: in degrees.
width_to_z: if sidewall_angle, reference z-position (0 --> zmin, 1 --> zmin + thickness, 0.5 in the middle).
bias: shrink/grow of the level compared to the mask
z_to_bias: most generic way to specify an extrusion.\
Two tuples of the same length specifying the shrink/grow (float) to apply between zmin (0) and zmin + thickness (1)\
I.e. [[z1, z2, ..., zN], [bias1, bias2, ..., biasN]]\
Defaults no buffering [[0, 1], [0, 0]].
NOTE: A dict might be more expressive.
mesh_order: lower mesh order (e.g. 1) will have priority over higher mesh order (e.g. 2) in the regions where materials overlap.
material: used in the klayout script
info: all other rendering and simulation metadata should go here.
"""
# ID
name: str | None = None
layer: BroadLayer
derived_layer: LogicalLayer | None = None
# Extrusion rules
thickness: float
thickness_tolerance: float | None = None
width_tolerance: float | None = None
zmin: float
zmin_tolerance: float | None = None
sidewall_angle: float = 0.0
sidewall_angle_tolerance: float | None = None
width_to_z: float = 0.0
z_to_bias: tuple[list[float], list[float]] | None = None
bias: tuple[float, float] | float | None = None
# Rendering
mesh_order: int = 3
material: str | None = None
# Other
info: dict[str, Any] = Field(default_factory=dict)
@field_validator("layer")
@classmethod
def check_layer(
cls, layer: BroadLayer | int | str | tuple[int, int] | LayerEnum
) -> LogicalLayer | DerivedLayer:
if isinstance(layer, int | str | tuple | LayerEnum):
layer = gf.get_layer(layer)
return LogicalLayer(layer=layer)
return layer
@property
def bounds(self) -> tuple[float, float]:
"""Calculates and returns the bounds of the layer level in the z-direction.
Returns:
tuple: A tuple containing the minimum and maximum z-values of the layer level.
"""
z_values = [self.zmin, self.zmin + self.thickness]
z_values.sort()
return z_values[0], z_values[1]
[docs]
class LayerStack(BaseModel):
"""For simulation and 3D rendering. Captures design intent of the chip layers after fabrication.
Parameters:
layers: dict of layer_levels.
"""
layers: dict[str, LayerLevel] = Field(
default_factory=dict,
description="dict of layer_levels",
)
def model_copy(
self, *, update: Mapping[str, Any] | None = None, deep: bool = False
) -> LayerStack:
"""Returns a copy of the LayerStack."""
return super().model_copy(update=update, deep=True)
[docs]
def __init__(self, **data: Any) -> None:
"""Add LayerLevels automatically for subclassed LayerStacks."""
super().__init__(**data)
for field in self.model_dump():
val = getattr(self, field)
if isinstance(val, LayerLevel):
self.layers[field] = val
def pprint(self) -> None:
console = Console()
table = Table(show_header=True, header_style="bold")
keys = ["layer", "thickness", "material", "sidewall_angle"]
for key in ["name", *keys]:
table.add_column(key)
for layer_name, layer in self.layers.items():
port_dict = dict(layer)
row = [layer_name] + [str(port_dict.get(key, "")) for key in keys]
table.add_row(*row)
console.print(table)
def get_layer_to_thickness(self) -> dict[BroadLayer, float]:
"""Returns layer tuple to thickness (um)."""
layer_to_thickness: dict[BroadLayer, float] = {}
for level in self.layers.values():
layer = level.layer
if (layer and level.thickness) or hasattr(level, "operator"):
layer_to_thickness[layer] = level.thickness
return layer_to_thickness
def get_component_with_derived_layers(
self, component: "Component", **kwargs: Any
) -> "Component":
"""Returns component with derived layers."""
return get_component_with_derived_layers(
component=component, layer_stack=self, **kwargs
)
def get_layer_to_zmin(self) -> dict[BroadLayer, float]:
"""Returns layer tuple to z min position (um)."""
return {
level.layer: level.zmin for level in self.layers.values() if level.thickness
}
def get_layer_to_material(self) -> dict[BroadLayer, str | None]:
"""Returns layer tuple to material name."""
return {
level.layer: level.material
for level in self.layers.values()
if level.thickness
}
def get_layer_to_sidewall_angle(self) -> dict[BroadLayer, float]:
"""Returns layer tuple to material name."""
return {
level.layer: level.sidewall_angle
for level in self.layers.values()
if level.thickness
}
def get_layer_to_info(self) -> dict[BroadLayer, dict[str, Any]]:
"""Returns layer tuple to info dict."""
return {level.layer: level.info for level in self.layers.values()}
def get_layer_to_layername(self) -> dict[BroadLayer, list[str]]:
"""Returns layer tuple to layername."""
d: dict[BroadLayer, list[str]] = defaultdict(list)
for level_name, level in self.layers.items():
d[level.layer].append(level_name)
return d
def to_dict(self) -> dict[str, dict[str, Any]]:
return {level_name: dict(level) for level_name, level in self.layers.items()}
def __getitem__(self, key: str) -> LayerLevel:
"""Access layer stack elements."""
if key not in self.layers:
layers = list(self.layers.keys())
raise ValueError(f"{key!r} not in {layers}")
return self.layers[key]
def get_klayout_3d_script(
self,
layer_views: LayerViews | None = None,
dbu: float | None = 0.001,
) -> str:
"""Returns script for 2.5D view in KLayout.
You can include this information in your tech.lyt
Args:
layer_views: optional layer_views.
dbu: Optional database unit. Defaults to 1nm.
"""
from gdsfactory.pdk import get_layer_views
layers = self.layers or {}
layer_views = layer_views or get_layer_views()
# Collect etch layers and unetched layers
etch_layers = [
layer_name
for layer_name, level in layers.items()
if isinstance(level.layer, DerivedLayer)
]
unetched_layers = [
layer_name
for layer_name, level in layers.items()
if isinstance(level.layer, LogicalLayer)
]
from gdsfactory.pdk import get_layer, get_layer_tuple
# Define input layers
out = "\n".join(
[
f"{layer_name} = input({(__layer := get_layer_tuple(level.derived_layer.layer))[0]}, {__layer[1]})"
for layer_name, level in layers.items()
if level.derived_layer
]
)
out += "\n\n"
# Remove all etched layers from the grown layers
unetched_layers_dict: dict[BroadLayer, list[str]] = defaultdict(list)
for layer_name in etch_layers:
level = layers[layer_name]
if level.derived_layer:
unetched_layers_dict[level.derived_layer.layer].append(layer_name)
if level.derived_layer.layer in unetched_layers:
unetched_layers.remove(level.derived_layer.layer) # type: ignore[arg-type]
# Define layers
out += "\n".join(
[
f"{layer_name} = input({level.layer.layer[0]}, {level.layer.layer[1]})" # type: ignore[index,union-attr]
for layer_name, level in layers.items()
if hasattr(level.layer, "layer")
]
)
out += "\n\n"
# Define unetched layers
for layer_name_etched, etching_layers in unetched_layers_dict.items():
etching_layers_str = " - ".join(etching_layers)
out += f"unetched_{layer_name_etched} = {layer_name_etched} - {etching_layers_str}\n"
out += "\n"
# Define slabs
for layer_name, level in layers.items():
if level.derived_layer:
_layer_from_derived = get_layer(level.derived_layer.layer)
out += f"slab_{_layer_from_derived}_{layer_name} = {_layer_from_derived} & {layer_name}\n"
out += "\n"
for layer_name, level in layers.items():
layer = level.layer
zmin = level.zmin
zmax = zmin + level.thickness
if dbu:
rnd_pl = len(str(dbu).split(".")[-1])
zmin = round(zmin, rnd_pl)
zmax = round(zmax, rnd_pl)
elif level.derived_layer:
derived_layer = level.derived_layer.layer
derived_layer_layer = get_layer_tuple(derived_layer)
slab_layer_name = f"slab_{layer}_{layer_name}"
slab_zmin = zmin
slab_zmax = zmax - level.thickness
name = f"{slab_layer_name}: {level.material} {derived_layer_layer[0]}/{derived_layer_layer[1]}"
txt = (
f"z("
f"{slab_layer_name}, "
f"zstart: {slab_zmin}, "
f"zstop: {slab_zmax}, "
f"name: '{name}'"
)
if layer_views:
txt += ", "
if layer in layer_views: # type: ignore[operator]
props = layer_views.get_from_tuple(layer) # type: ignore[arg-type]
if hasattr(props, "color"):
if props.color.fill == props.color.frame:
txt += f"color: {props.color.fill}"
else:
txt += (
f"fill: {props.color.fill}, "
f"frame: {props.color.frame}"
)
txt += ")"
out += f"{txt}\n"
elif layer_name in unetched_layers:
# TODO: Reimplement this
layer_tuple = get_layer_tuple(layer.layer) # type: ignore[union-attr]
name = (
f"{layer_name}: {level.material} {layer_tuple[0]}/{layer_tuple[1]}"
)
txt = f"z({layer_name}, zstart: {zmin}, zstop: {zmax}, name: '{name}'"
if layer_views:
txt += ", "
props = layer_views.get_from_tuple(get_layer_tuple(layer_tuple))
if hasattr(props, "color"):
if props.color.fill == props.color.frame:
txt += f"color: {props.color.fill}"
else:
txt += (
f"fill: {props.color.fill}, frame: {props.color.frame}"
)
txt += ")"
out += f"{txt}\n"
return out
def filtered(self, layers: list[str]) -> LayerStack:
"""Returns filtered layerstack, given layer specs."""
return LayerStack(
layers={k: self.layers[k] for k in layers if k in self.layers}
)
def z_offset(self, dz: float) -> LayerStack:
"""Translates the z-coordinates of the layerstack."""
layers = self.layers or {}
for layer in layers.values():
layer.zmin += dz
return self
def invert_zaxis(self) -> LayerStack:
"""Flips the zmin values about the origin."""
layers = self.layers or {}
for layer in layers.values():
layer.zmin *= -1
return self
def get_component_with_derived_layers(
component: "Component", layer_stack: LayerStack
) -> "Component":
"""Returns a component with derived layers.
Args:
component: Component to get derived layers for.
layer_stack: Layer stack to get derived layers from.
"""
from gdsfactory.component import Component
from gdsfactory.pdk import get_layer
component_derived = Component()
for level in layer_stack.layers.values():
if level.derived_layer is None:
if isinstance(level.layer, LogicalLayer):
derived_layer_index = get_layer(level.layer.layer)
else:
raise ValueError(
"If derived_layer is not provided, the LayerLevel layer must be a LogicalLayer"
)
else:
derived_layer_index = get_layer(level.derived_layer.layer)
if isinstance(level.layer, AbstractLayer):
shapes = level.layer.get_shapes(component=component)
component_derived.shapes(derived_layer_index).insert(shapes)
component_derived.add_ports(component.ports)
return component_derived
if __name__ == "__main__":
# For now, make regular layers trivial DerivedLayers
# This might be automatable during LayerStack instantiation, or we could modify the Layer object in LayerMap too
layer1 = LogicalLayer(layer=(1, 0))
layer2 = LogicalLayer(layer=(2, 0))
layer1_sized = LogicalLayer(layer=(1, 0)).sized(10000)
layer1_sized_asymmetric = LogicalLayer(layer=(1, 0)).sized(0, 50000)
layer3 = LogicalLayer(layer=(3, 0))
layer3_sequence = LogicalLayer(layer=(3, 0)).sized(2000, 2000).sized(-1000, -1000)
layer3_sequence_list = LogicalLayer(layer=(3, 0)).sized((2000, 2000))
layer3_sequence_lists = LogicalLayer(layer=(3, 0)).sized((0, 0), (5000, 1000))
ls = LayerStack(
layers={
"layerlevel_layer1": LayerLevel(layer=layer1, thickness=10, zmin=0),
"layerlevel_layer1_sized": LayerLevel(
layer=layer1_sized, thickness=10, zmin=0
),
"layerlevel_layer1_asymmetric": LayerLevel(
layer=layer1_sized_asymmetric, thickness=10, zmin=0
),
"layerlevel_layer1_to_layer2_derived": LayerLevel(
layer=layer1_sized, thickness=10, zmin=0, derived_layer=layer2
),
"layerlevel_layer3": LayerLevel(layer=layer3, thickness=10, zmin=0),
"layer3_sequence": LayerLevel(
layer=layer3_sequence,
thickness=10,
zmin=0,
derived_layer=LogicalLayer(layer=(4, 0)),
),
"layer3_sequence_list": LayerLevel(
layer=layer3_sequence_list,
thickness=10,
zmin=0,
derived_layer=LogicalLayer(layer=(5, 0)),
),
"layer3_sequence_lists": LayerLevel(
layer=layer3_sequence_lists,
thickness=10,
zmin=0,
derived_layer=LogicalLayer(layer=(6, 0)),
),
}
)
# Test with simple component
import gdsfactory as gf
c = gf.Component()
rect1 = c << gf.components.rectangle(size=(10, 10), layer=(1, 0))
rect2 = c << gf.components.rectangle(size=(10, 10), layer=(3, 0))
rect2.move((30, 30))
# c.show()
# import gdsfactory as gf
# c = gf.Component()
# rect1 = c << gf.components.rectangle(size=(10, 10), layer=(1, 0))
# rect2 = c << gf.components.rectangle(size=(10, 10), layer=(2, 0))
# rect2.move((5, 5))
# c.show()
c = get_component_with_derived_layers(c, ls)
c.show()
# s = ls.get_klayout_3d_script()
# print(s)
res = ls.get_klayout_3d_script()