from __future__ import annotations
from collections import defaultdict
from typing import TYPE_CHECKING, Any, Literal
import gdstk
from pydantic import BaseModel, Field
from rich.console import Console
from rich.table import Table
import gdsfactory as gf
from gdsfactory.cell import cell
from gdsfactory.component import Component
if TYPE_CHECKING:
from gdsfactory.technology import LayerViews
[docs]
class LayerLevel(BaseModel):
"""Level for 3D LayerStack.
Parameters:
name: layer name.
layer: (GDSII Layer number, GDSII datatype).
thickness: layer thickness in um.
thickness_tolerance: layer thickness tolerance in um.
zmin: height position where material starts in um.
zmin_tolerance: layer height tolerance in um.
material: material name.
sidewall_angle: in degrees with respect to normal.
sidewall_angle_tolerance: in degrees.
width_to_z: if sidewall_angle, relative z-position (0 --> zmin, 1 --> zmin + thickness).
z_to_bias: parametrizes shrinking/expansion of the design GDS layer \
when extruding from zmin (0) to zmin + thickness (1).\
Defaults no buffering [[0, 1], [0, 0]].
mesh_order: lower mesh order (1) will have priority over higher \
mesh order (2) in the regions where materials overlap.
layer_type: grow, etch, implant, or background.
mode: octagon, taper, round. https://gdsfactory.github.io/klayout_pyxs/DocGrow.html
into: etch into another layer. https://gdsfactory.github.io/klayout_pyxs/DocGrow.html
resistivity: for metals.
bias: in um for the etch. Can be a single number or 2 numbers (bias_x, bias_y)
derived_layer: Optional derived layer, used for layer_type='etch' to define the slab.
info: simulation_info and other types of metadata.
background_doping_concentration: uniform base doping level in the material (cm-3)
background_doping_ion: uniform base doping ion in the material
orientation: of the wafer (Miller indices of the plane)
"""
name: str | None = None
layer: tuple[int, int] | None = None
thickness: float | None = None
thickness_tolerance: float | None = None
zmin: float | None = None
zmin_tolerance: float | None = None
material: str | 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
mesh_order: int = 3
layer_type: Literal["grow", "etch", "doping", "background"] = "grow"
mode: Literal["octagon", "taper", "round"] | None = None
into: list[str] | None = None
resistivity: float | None = None
bias: tuple[float, float] | float | None = None
derived_layer: tuple[int, int] | None = None
info: dict[str, Any] = Field(default_factory=dict)
background_doping_concentration: float | None = None
background_doping_ion: str | None = None
orientation: str | None = "100"
@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.
"""
return tuple(sorted([self.zmin, self.zmin + self.thickness]))
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) -> LayerStack:
"""Returns a copy of the LayerStack."""
return LayerStack.model_validate_json(self.model_dump_json())
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 __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 get_layer_to_thickness(self) -> dict[tuple[int, int], float]:
"""Returns layer tuple to thickness (um)."""
layer_to_thickness = {}
for level in self.layers.values():
layer = level.layer
if layer and level.thickness:
layer_to_thickness[layer] = level.thickness
elif hasattr(level, "operator"):
layer_to_thickness[level.layer] = level.thickness
return layer_to_thickness
def get_component_with_derived_layers(self, component, **kwargs):
"""Returns component with derived layers."""
return get_component_with_derived_layers(
component=component, layer_stack=self, **kwargs
)
def get_layer_to_zmin(self) -> dict[tuple[int, int], 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[tuple[int, int], str]:
"""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[tuple[int, int], str]:
"""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[tuple[int, int], dict]:
"""Returns layer tuple to info dict."""
return {level.layer: level.info for level in self.layers.values()}
def get_layer_to_layername(self) -> dict[tuple[int, int], str]:
"""Returns layer tuple to layername."""
d = 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) -> 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.
"""
layers = self.layers or {}
unetched_layers = [
layer_name
for layer_name, level in layers.items()
if level.layer and level.layer_type == "grow"
]
etch_layers = [
layer_name
for layer_name, level in layers.items()
if level.layer and level.layer_type == "etch"
]
# remove all etched layers from the grown layers
unetched_layers_dict = defaultdict(list)
for layer_name in etch_layers:
level = layers[layer_name]
into = level.into or []
for layer_name_etched in into:
unetched_layers_dict[layer_name_etched].append(layer_name)
if layer_name_etched in unetched_layers:
unetched_layers.remove(layer_name_etched)
# define layers
out = "\n".join(
[
f"{layer_name} = input({level.layer[0]}, {level.layer[1]})"
for layer_name, level in layers.items()
if level.layer
]
)
out += "\n"
out += "\n"
# define unetched layers
for layer_name_etched, etching_layers in unetched_layers_dict.items():
etching_layers = " - ".join(etching_layers)
out += f"unetched_{layer_name_etched} = {layer_name_etched} - {etching_layers}\n"
out += "\n"
# define slabs
for layer_name, level in layers.items():
if level.layer_type == "etch":
into = level.into or []
for i, layer1 in enumerate(into):
out += f"slab_{layer1}_{layer_name}_{i} = {layer1} & {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)
if layer is None:
continue
elif level.layer_type == "etch":
name = f"{layer_name}: {level.material}"
into = level.into or []
for i, layer1 in enumerate(into):
unetched_level = layers[layer1]
unetched_zmin = unetched_level.zmin
unetched_zmax = unetched_zmin + unetched_level.thickness
# slab
slab_layer_name = f"slab_{layer1}_{layer_name}_{i}"
slab_zmin = unetched_level.zmin
slab_zmax = unetched_zmax - level.thickness
name = f"{slab_layer_name}: {level.material} {layer[0]}/{layer[1]}"
txt = (
f"z("
f"{slab_layer_name}, "
f"zstart: {slab_zmin}, "
f"zstop: {slab_zmax}, "
f"name: '{name}'"
)
if layer_views:
txt += ", "
props = layer_views.get_from_tuple(layer)
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:
name = f"{layer_name}: {level.material} {layer[0]}/{layer[1]}"
txt = (
f"z("
f"{layer_name}, "
f"zstart: {zmin}, "
f"zstop: {zmax}, "
f"name: '{name}'"
)
if layer_views:
txt += ", "
props = layer_views.get_from_tuple(layer)
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"
out += "\n"
for layer_name in unetched_layers_dict:
unetched_level = self.layers[layer_name]
layer = unetched_level.layer
unetched_zmin = unetched_level.zmin
unetched_zmax = unetched_zmin + unetched_level.thickness
name = f"{slab_layer_name}: {unetched_level.material}"
unetched_layer_name = f"unetched_{layer_name}"
name = f"{unetched_layer_name}: {unetched_level.material} {layer[0]}/{layer[1]}"
txt = (
f"z("
f"{unetched_layer_name}, "
f"zstart: {unetched_zmin}, "
f"zstop: {unetched_zmax}, "
f"name: '{name}'"
)
if layer_views:
txt += ", "
props = layer_views.get_from_tuple(layer)
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"
return out
def filtered(self, layers) -> 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) -> 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
@cell
def get_component_with_derived_layers(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.
"""
unetched_layers = [
layer_name
for layer_name, level in layer_stack.layers.items()
if level.layer and level.layer_type == "grow"
]
etch_layers = [
layer_name
for layer_name, level in layer_stack.layers.items()
if level.layer and level.layer_type == "etch"
]
# remove all etched layers from the grown layers
unetched_layers_dict = defaultdict(list)
for layer_name in etch_layers:
level = layer_stack.layers[layer_name]
into = level.into or []
for layer_name_etched in into:
unetched_layers_dict[layer_name_etched].append(layer_name)
if layer_name_etched in unetched_layers:
unetched_layers.remove(layer_name_etched)
component_layers = component.get_layers()
# Define pure grown layers
unetched_layer_numbers = [
layer_stack.layers[layer_name].layer
for layer_name in unetched_layers
if layer_stack.layers[layer_name].layer in component_layers
]
layer_to_polygons = component.get_polygons(by_spec=True)
# component_derived = component.extract(unetched_layer_numbers)
component_derived = gf.Component()
for layer in unetched_layer_numbers:
polygons = layer_to_polygons[layer]
unetched_polys = gdstk.boolean(
operand1=polygons,
operand2=[],
operation="or",
layer=layer[0],
datatype=layer[1],
)
component_derived.add(unetched_polys)
# Define unetched layers
polygons_to_remove = []
for unetched_layer_name, unetched_layers in unetched_layers_dict.items():
layer = layer_stack.layers[unetched_layer_name].layer
polygons = component.get_polygons(by_spec=layer)
# Add all the etching layers (OR)
for etching_layers in unetched_layers:
layer = layer_stack.layers[etching_layers].layer
B_polys = component.get_polygons(by_spec=layer)
polygons_to_remove = gdstk.boolean(
operand1=polygons_to_remove,
operand2=B_polys,
operation="or",
layer=layer[0],
datatype=layer[1],
)
derived_layer = layer_stack.layers[etching_layers].derived_layer
if derived_layer:
slab_polygons = gdstk.boolean(
operand1=polygons,
operand2=B_polys,
operation="and",
layer=derived_layer[0],
datatype=derived_layer[1],
)
component_derived.add(slab_polygons)
# Remove all etching layers
layer = layer_stack.layers[unetched_layer_name].layer
unetched_polys = gdstk.boolean(
operand1=polygons,
operand2=polygons_to_remove,
operation="not",
layer=layer[0],
datatype=layer[1],
)
component_derived.add(unetched_polys)
component_derived.add_ports(component.ports)
return component_derived
if __name__ == "__main__":
from gdsfactory.generic_tech import get_generic_pdk
PDK = get_generic_pdk()
PDK.activate()
from gdsfactory.generic_tech import LAYER_STACK
ls = LAYER_STACK
# ls.pprint()
c = gf.components.straight_heater_metal(length=30)
c.show()
s = c.to_3d()
s.show()
# import gdsfactory as gf
# from gdsfactory.generic_tech import LAYER_STACK
# component = c = gf.components.grating_coupler_elliptical_trenches()
# component = c = gf.components.taper_strip_to_ridge_trenches()
# script = LAYER_STACK.get_klayout_3d_script()
# print(script)
# ls = layer_stack = LAYER_STACK
# layer_to_thickness = layer_stack.get_layer_to_thickness()
# c = layer_stack.get_component_with_derived_layers(component)
# c.show(show_ports=True)
# import pathlib
# filepath = pathlib.Path(
# "/home/jmatres/gdslib/sp/temp/write_sparameters_meep_mpi.json"
# )
# ls_json = filepath.read_bytes()
# ls2 = LayerStack.parse_raw(ls_json)
# from gdsfactory.generic_tech import LAYER_STACK
# from gdsfactory.technology.klayout_tech import KLayoutTechnology
# lyp = LayerViews.from_lyp(str(PATH.klayout_lyp))
# # str_xml = open(PATH.klayout_tech / "tech.lyt").read()
# # new_tech = db.Technology.technology_from_xml(str_xml)
# # generic_tech = KLayoutTechnology(layer_views=lyp)
# connectivity = [("M1", "VIA1", "M2"), ("M2", "VIA2", "M3")]
# c = generic_tech = KLayoutTechnology(
# name="generic_tech", layer_views=lyp, connectivity=connectivity
# )
# tech_dir = PATH.klayout_tech
# # tech_dir = pathlib.Path("/home/jmatres/.klayout/salt/gdsfactory/tech/")
# tech_dir.mkdir(exist_ok=True, parents=True)
# generic_tech.write_tech(tech_dir=tech_dir, layer_stack=LAYER_STACK)
# yaml_test()