from enum import Enum
import numpy as np
from dataclasses import field, dataclass
from shapely.geometry import box, MultiPolygon, Polygon
from typing import List, Optional, Dict
from .typing import Float3, LayerLabel
from .utils import fix_dataclass_init_docs
[docs]@fix_dataclass_init_docs
@dataclass
class Material:
"""Helper class for materials.
Attributes:
name: Name of the material.
eps: Constant epsilon (relative permittivity) assigned for the material.
facecolor: Facecolor in red-green-blue (RGB) for drawings (default is black or :code:`(0, 0, 0)`).
alpha: transparency of the material for visualization
"""
name: str
eps: float = 1
color: Float3 = (0, 0, 0)
alpha: float = 1
def __str__(self):
return self.name
@property
def n(self):
return np.sqrt(self.eps)
SILICON = Material('si', 3.4784 ** 2, (0.3, 0.3, 0.3))
POLYSILICON = Material('poly_si', 3.4784 ** 2, (0.5, 0.5, 0.5))
AIR = Material('air')
N_SILICON = Material('n_si', color=(0.4, 0.3, 0), alpha=0.3)
P_SILICON = Material('p_si', color=(0, 0.3, 0.4), alpha=0.3)
NN_SILICON = Material('nn_si', color=(0.4, 0.3, 0), alpha=0.5)
PP_SILICON = Material('pp_si', color=(0, 0.3, 0.4), alpha=0.5)
NNN_SILICON = Material('nnn_si', color=(0.4, 0.3, 0), alpha=0.7)
PPP_SILICON = Material('ppp_si', color=(0, 0.3, 0.4), alpha=0.7)
OXIDE = Material('sio2', 1.4442 ** 2, (0.6, 0, 0))
NITRIDE = Material('si3n4', 1.996 ** 2, (0, 0, 0.7))
LS_NITRIDE = Material('ls_sin', color=(0, 0.4, 1))
LT_OXIDE = Material('lto', 1.4442 ** 2, (0.8, 0.2, 0.2))
COPPER = Material('cu', color=(1, 0.6, 0))
ALUMINUM = Material('al', color=(0, 0.5, 0)) # (1.5785 + 15.658 * 1j) ** 2,
ALCU = Material('alcu', color=(0.2, 0.4, 0))
ALUMINA = Material('al2o3', 1.75, (0.2, 0, 0.2))
HEATER = Material('tin', color=(0.8, 0.8, 0)) # (3.1477 + 5.8429 * 1j) ** 2,
ETCH = Material('etch')
DUMMY = Material('dummy', color=(0.7, 0.7, 0.7))
[docs]class ProcessOp(str, Enum):
"""Enumeration for process operations which describe what happens at each step in a foundry process.
Attributes:
ISO_ETCH: isotropic etch under a pattern stencil (not yet supported)
DRI_ETCH: directed-reactive ion etch (simply etch downward under a pattern stencil)
SAC_ETCH: sacrificial etch (only affects cladding material in a process)
GROW: grow over the previously deposited layer
DOPE: dopes the previously deposited layer
DUMMY: No process step associated with this
"""
ISO_ETCH = 'iso_etch'
DRI_ETCH = 'dri_etch'
SAC_ETCH = 'sac_etch'
GROW = 'grow'
DOPE = 'dope'
DUMMY = 'dummy'
[docs]class CommonLayer(str, Enum):
"""Common layers used in foundries. These are just strings representing common layers, no other functionality,
Attributes:
RIDGE_SI: Ridge silicon waveguide layer (waveguiding portion of the waveguide).
RIDGE_SI_2: Another ridge silicon waveguide layer.
RIB_SI: Rib silicon waveguide layer (slab portion of the waveguide).
RIB_SI_2: Another rib silicon waveguide layer.
PDOPED_SI: Lightly P-doped silicon (implants into the crystalline silicon layer).
NDOPED_SI: Lightly N-doped silicon (implants into the crystalline silicon layer).
PPDOPED_SI: Medium P-doped silicon (implants into the crystalline silicon layer).
NNDOPED_SI: Medium N-doped silicon (implants into the crystalline silicon layer).
PPPDOPED_SI: Highly P-doped silicon (implants into the crystalline silicon layer).
NNNDOPED_SI: Highly N-doped silicon (implants into the crystalline silicon layer).
RIDGE_SIN: Silicon nitride ridge layer (usually above silicon).
ALUMINA: Alumina layer (for etch stop and waveguides, usually done in post-processing).
POLY_SI_1: Polysilicon layer 1 (typically used in MEMS process).
POLY_SI_2: Polysilicon layer 2 (typically used in MEMS process).
POLY_SI_3: Polysilicon layer 3 (typically used in MEMS process).
VIA_SI_1: Via metal connection from :code:`si` to :code:`metal_1`.
METAL_1: Metal layer corresponding to an intermediate routing layer (1).
VIA_1_2: Via metal connection from :code:`metal_1` to :code:`metal_2`.
METAL_2: Metal layer corresponding to an intermediate routing layer (2).
VIA_2_PAD: Via metal connection from :code:`metal_2` to :code:`metal_pad`.
METAL_PAD: Metal layer corresponding to pads that can be wirebonded or solder-bump bonded from the chip surface.
HEATER: Heater layer (usually titanium nitride).
VIA_HEATER_2: Via metal connection from :code:`heater` to :code:`metal_2`.
CLAD: Cladding layer (usually oxide).
CLEAROUT: Clearout layer for a MEMS release process.
PHOTONIC_KEEPOUT: A layer specifying where photonics cannot be routed.
METAL_KEEPOUT: A layer specifying where metal cannot be routed.
BBOX: Layer for the bounding box of the design.
PORT: Default port layer (unless foundries specify port by layer)
"""
RIDGE_SI = 'ridge_si'
RIDGE_SI_2 = 'ridge_si_2'
RIB_SI = 'rib_si'
RIB_SI_2 = 'rib_si_2'
RIDGE_SIN = 'ridge_sin'
RIDGE_SIN_2 = 'ridge_sin_2'
RIB_SIN = 'rib_sin'
P_SI = 'p_si'
N_SI = 'n_si'
PP_SI = 'pp_si'
NN_SI = 'nn_si'
PPP_SI = 'pppdoped_si'
NNN_SI = 'nnndoped_si'
ALUMINA = 'alumina'
POLY_SI_1 = 'poly_si_1'
POLY_SI_2 = 'poly_si_2'
POLY_SI_3 = 'poly_si_3'
VIA_SI_1 = 'via_si_1'
METAL_1 = "metal_1"
VIA_1_2 = "via_1_2"
METAL_2 = "metal_2"
VIA_2_PAD = "via_2_pad"
PAD = "pad"
PAD_OPEN = "pad_open"
GERMANIUM_ANODE = "ge_anode"
GERMANIUM_CATHODE = "ge_cathode"
GERMANIUM_OPEN = "ge_open"
HEATER = "heater"
VIA_HEATER_2 = "via_heater_2"
CLAD = "clad"
OXIDE_OPEN = "oxide_open"
CLEAROUT = "clearout"
PHOTONIC_KEEPOUT = "photonic_keepout"
METAL_KEEPOUT = "metal_keepout"
BBOX = "bbox"
BBOX_SI = "bbox_si"
BBOX_SIN = "bbox_sin"
BBOX_SI_SIN = "bbox_si_sin"
BBOX_METAL = "bbox_metal"
BBOX_LABEL = "bbox_label"
BBOX_METAL_1 = "bbox_metal_1"
BBOX_METAL_2 = "bbox_metal_2"
BBOX_RIB_SI = "bbox_rib_si"
TRENCH = "trench"
VGROOVE = "vgroove"
PORT = "port"
[docs]@fix_dataclass_init_docs
@dataclass
class ProcessStep:
"""The :code:`ProcessStep` class is an object that stores all the information about a layer in a foundry process.
Attributes:
process_op: Process operation, specified in the enum for :code:`ProcessOp`
thickness: The thickness spec for the process step.
mat: Material (relevant to the non-etch :code:`ProcessOp.grow` and :code:`ProcessOp.dope` process ops)
layer: The device layer corresponding to the process step
(should NOT vary by foundry, use CommonLayer interface unless layers are hyper-specific).
gds_label: The GDS label used for GDS file creation of the device (SHOULD vary by foundry).
start_height: The starting height for the process step.
inverse: Whether to assign this process step as an inverse (data clear) mask
"""
process_op: ProcessOp
thickness: float
mat: Material
layer: str
gds_label: LayerLabel
start_height: Optional[float] = None
inverse: bool = False
[docs]@fix_dataclass_init_docs
@dataclass
class Foundry:
"""The :code:`Foundry` class defines the full stack of process steps.
For any step where the :code:`start_height` si not specified, the :code:`Foundry` class will assume a start height
that is directly above the previously deposited layer. Note this does not support conformal deposition as this
assumes all layers below are planarized.
Attributes:
stack: List of process steps that when applied sequentially lead to a full foundry stack
height: Overall height of the foundry stack.
cladding: The cladding material used by the foundry (usually OXIDE). This material will more-or-less
cover the entire
port_layers: A list of common layers for extracting the appropriate ports.
use_port_boxes: Whether the foundry uses boxes to indicate port widths (useful for loading PDKs).
"""
stack: List[ProcessStep]
height: float
cladding: Material = None
port_layers: List[CommonLayer] = field(default_factory=list)
use_port_boxes: bool = True
use_port_layers: bool = True # preserve the port layer given by the foundry when reading GDS
def __post_init__(self):
start_height = 0
self.port_fn = lambda d: {} if self.port_fn is None else self.port_fn
for step in self.stack:
step.start_height = start_height if step.start_height is None else step.start_height
start_height = step.start_height + step.thickness if step.process_op == ProcessOp.GROW else step.start_height
@property
def layer_to_gds_label(self):
return {step.layer: step.gds_label for step in self.stack}
@property
def gds_label_to_layer(self):
return {label: layer for layer, label in self.layer_to_gds_label.items()}
[docs] def color(self, layer: str):
for step in self.stack:
if step.layer == layer:
return step.mat.color
[docs] def fabricate(self, layer_to_geom: Dict[str, MultiPolygon], exclude_layer: Optional[List[CommonLayer]] = None):
"""Fabricate a device based on a layer-to-geometry dict, :code:`Foundry`, and initial device (type :code:`Scene`).
This method is fairly rudimentary and will not implement things like conformal deposition. At the moment,
you can implement things like rib etches which can be determined using 2d shape operations. Depositions in
layers above etched layers will just start from the maximum z extent of the previous layer. This is specified
by the :code:`Foundry` stack.
Note:
Custom fabricate methods may be built if this foundry class is extended.
However, the same template is recommended.
Args:
layer_to_geom: A dictionary mapping each layer to the `full` Shapely geometry for that layer.
exclude_layer: Exclude all layers in this list.
Returns:
The device :code:`Scene` to visualize.
"""
try:
import trimesh
from trimesh.creation import extrude_polygon
from trimesh.scene import Scene
except ImportError:
raise ImportError("Fabrication requires the triangle module to be compiled with trimesh")
def _shapely_to_mesh_from_step(_geom: MultiPolygon, _meshes: List["trimesh.Trimesh"], _step: ProcessStep):
for _poly in _geom.geoms:
if _poly.area > 1e-8:
_meshes.append(extrude_polygon(_poly, height=_step.thickness))
_mesh = trimesh.util.concatenate(_meshes) if _meshes else trimesh.Trimesh()
_mesh.visual.face_colors = _step.mat.color
return _mesh
device = Scene()
prev_mat: Optional[Material] = None
bound_list = np.array([p.bounds for _, p in layer_to_geom.items()]).T
xy_extent = np.min(bound_list[0]), np.min(bound_list[1]), np.max(bound_list[2]), np.max(bound_list[3])
clad_geometry = box(*xy_extent)
mesh = extrude_polygon(clad_geometry, height=self.height)
exclude_layer = [] if exclude_layer is None else exclude_layer
prev_si_geom = None
if 'clad' not in device.geometry and self.cladding is not None:
device.add_geometry(mesh, geom_name='clad')
mesh.visual.face_colors = (*self.cladding.color, 0.5)
for step in self.stack:
# move the pattern to the previous maximum z height (previous mesh) OR start_height if specified in step.
dz = step.start_height
meshes = []
layer = step.layer
if layer in exclude_layer:
continue
elif layer in layer_to_geom:
# only silicon (rib/ridge) and metal (via, pad, multilevel) generally have multiple layers
geom = layer_to_geom[layer]
mesh_name = f"{step.mat.name}_{layer}" if step.mat.name in {SILICON.name, ALUMINUM.name} else layer
if step.process_op == ProcessOp.GROW:
mesh = _shapely_to_mesh_from_step(geom, meshes, step)
device.add_geometry(mesh.apply_translation((0, 0, dz)), geom_name=mesh_name)
elif step.process_op == ProcessOp.DRI_ETCH:
# Directly etch device
# TODO: convert this to a 2D geometry function
raise NotImplementedError(f"Fabrication method not yet implemented for `{step.process_op.value}`")
elif step.process_op == ProcessOp.DOPE:
# only support to dope silicon at the moment
if prev_mat != SILICON:
raise ValueError("The previous material must be crystalline silicon for dopant implantation.")
geoms = []
for poly_si in prev_si_geom.geoms:
for poly in geom:
mat = poly.intersection(poly_si)
if isinstance(mat, Polygon) or isinstance(mat, MultiPolygon):
geoms.append(mat)
mesh = _shapely_to_mesh_from_step(MultiPolygon(geoms), meshes, step)
device.add_geometry(mesh.apply_translation((0, 0, dz - step.thickness)), geom_name=mesh_name)
elif step.process_op == ProcessOp.SAC_ETCH:
if 'clad' not in device.geometry:
raise ValueError(
"The cladding is not in the device geometry / not spec'd by the foundry object.")
clad_geometry -= geom
clad_geometry = MultiPolygon([clad_geometry]) if isinstance(clad_geometry,
Polygon) else clad_geometry
for poly in clad_geometry:
meshes.append(extrude_polygon(poly, height=step.thickness))
device.geometry['clad'] = trimesh.util.concatenate(meshes)
mesh.visual.face_colors = (*self.cladding.color, 0.5)
# raise NotImplementedError(f"Fabrication method not yet implemented for `{step.process_op.value}`")
# device.geometry['clad'] -= difference(device.geometry['clad'], mesh)
elif not step.process_op == ProcessOp.DUMMY:
raise NotImplementedError(f"Fabrication method not yet implemented for `{step.process_op.value}`")
if step.mat.name == SILICON.name:
prev_si_geom = geom
if step.process_op == ProcessOp.GROW:
prev_mat = step.mat
return device
# Foundries are generally secretive about their exact stack/gds labels,
# so the below is one example stack for demo purposes.
FABLESS = Foundry(
stack=[
# 1. First define the photonic stack
ProcessStep(ProcessOp.GROW, 0.2, SILICON, CommonLayer.RIDGE_SI, (100, 0), 2),
ProcessStep(ProcessOp.DOPE, 0.1, P_SILICON, CommonLayer.P_SI, (400, 0)),
ProcessStep(ProcessOp.DOPE, 0.1, N_SILICON, CommonLayer.N_SI, (401, 0)),
ProcessStep(ProcessOp.DOPE, 0.1, PP_SILICON, CommonLayer.PP_SI, (402, 0)),
ProcessStep(ProcessOp.DOPE, 0.1, NN_SILICON, CommonLayer.NN_SI, (403, 0)),
ProcessStep(ProcessOp.DOPE, 0.1, PPP_SILICON, CommonLayer.PPP_SI, (404, 0)),
ProcessStep(ProcessOp.DOPE, 0.1, NNN_SILICON, CommonLayer.NNN_SI, (405, 0)),
ProcessStep(ProcessOp.GROW, 0.1, SILICON, CommonLayer.RIB_SI, (101, 0), 2),
ProcessStep(ProcessOp.GROW, 0.2, NITRIDE, CommonLayer.RIDGE_SIN, (300, 0), 2.5),
ProcessStep(ProcessOp.GROW, 0.1, ALUMINA, CommonLayer.ALUMINA, (200, 0), 2.5),
# 2. Then define the metal connections (zranges).
ProcessStep(ProcessOp.GROW, 1, COPPER, CommonLayer.VIA_SI_1, (500, 0), 2.2),
ProcessStep(ProcessOp.GROW, 0.2, COPPER, CommonLayer.METAL_1, (501, 0)),
ProcessStep(ProcessOp.GROW, 0.5, COPPER, CommonLayer.VIA_1_2, (502, 0)),
ProcessStep(ProcessOp.GROW, 0.2, COPPER, CommonLayer.METAL_2, (503, 0)),
ProcessStep(ProcessOp.GROW, 0.5, ALUMINUM, CommonLayer.VIA_2_PAD, (504, 0)),
# Note: negative means grow downwards (below the ceiling of the device).
ProcessStep(ProcessOp.GROW, 0.3, ALUMINUM, CommonLayer.PAD, (600, 0)),
ProcessStep(ProcessOp.GROW, 0.2, HEATER, CommonLayer.HEATER, (700, 0), 3.2),
ProcessStep(ProcessOp.GROW, 0.5, COPPER, CommonLayer.VIA_HEATER_2, (505, 0)),
# 3. Finally specify the clearout (needed for MEMS).
ProcessStep(ProcessOp.SAC_ETCH, 4, ETCH, CommonLayer.CLEAROUT, (800, 0)),
ProcessStep(ProcessOp.DUMMY, 4, DUMMY, CommonLayer.TRENCH, (41, 0)),
ProcessStep(ProcessOp.DUMMY, 4, DUMMY, CommonLayer.PHOTONIC_KEEPOUT, (42, 0)),
ProcessStep(ProcessOp.DUMMY, 4, DUMMY, CommonLayer.METAL_KEEPOUT, (43, 0)),
ProcessStep(ProcessOp.DUMMY, 4, DUMMY, CommonLayer.BBOX, (44, 0)),
],
height=5
)
DEFAULT_FOUNDRY = FABLESS