"""Pdk functions for layer and device management, mostly taken from gdsfactory"""
# can be removed in python 3.14, see https://peps.python.org/pep-0749/
from __future__ import annotations
from functools import partial
import phidl
from qnngds import Layer, LayerSet, CrossSection
from qnngds.typing import (
LayerSpec,
DeviceSpec,
DeviceSpecs,
CrossSectionSpec,
)
from .geometries import hyper_taper, default_cross_section, fine_to_coarse
from .utilities import get_outline_layers, outline
_ACTIVE_PDK: Pdk | None = None
[docs]class Pdk:
"""PDK class, stores layer information, cross sections, layer transistions, and devices"""
[docs] def __init__(
self,
name: str,
layers: LayerSet,
cross_sections: dict[str, CrossSection] = {},
layer_transitions: dict[
LayerSpec | tuple[LayerSpec, LayerSpec], DeviceSpec
] = {},
devices: DeviceSpecs = [],
) -> None:
"""Constructor
Args:
name (str): name of PDK
layers (LayerSet): LayerSet to use for PDK
cross_sections (dict[str, CrossSection]): map of named cross sections
to their instances.
layer_transitions (dict[LayerSpec | tuple[LayerSpec, LayerSpec], DeviceSpec]):
map of LayerSpec (or pair of LayerSpecs) to a DeviceSpec which
will transition between layers.
devices (DeviceSpecs): devices to register with PDK
Returns:
None
"""
self.name = name
self.layers = layers
self.cross_sections = cross_sections
self.layer_transitions = {}
for key, transition in layer_transitions.items():
raise_error = False
if isinstance(key, tuple):
if isinstance(key[0], tuple) | isinstance(key[0], str):
# transition between two layers
self.layer_transitions[
(self.get_layer(key[0]).tuple, self.get_layer(key[1]).tuple)
] = transition
elif isinstance(key[0], int):
# single layer
self.layer_transitions[self.get_layer(key).tuple] = transition
else:
raise_error = True
elif isinstance(key, str):
self.layer_transitions[self.get_layer(key).tuple] = transition
else:
raise_error = True
if raise_error:
raise ValueError(
f"Could not parse layer transition specification {key}, "
"expected LayerSpec | tuple[LayerSpec, LayerSpec]"
)
self.devices = devices
[docs] def activate(self) -> None:
"""Enable the PDK and allow it to be accessed globally"""
_set_active_pdk(self)
[docs] def get_layer(self, layer: LayerSpec) -> Layer:
"""Get a specific layer within the PDK
Args:
layer (LayerSpec): string, int, or tuple that identifies the desired layer
Returns:
(Layer): instance of layer matching the queried LayerSpec
"""
value_error_msg = (
f"Could not find layer {layer} in Pdk {self.name}. "
"Double-check for misspelled layer name or a Device "
"that uses phidl.Port instead of qnngds.Port for its ports"
)
if isinstance(layer, str):
if layer not in self.layers._layers:
raise ValueError(value_error_msg)
return self.layers[layer]
# convert to tuple
if isinstance(layer, int):
layer = (layer, 0)
elif isinstance(layer, phidl.Layer):
layer = (layer.gds_layer, layer.gds_datatype)
elif isinstance(layer, Layer):
layer = layer.tuple
if isinstance(layer, tuple):
for _layer in self.layers:
_layer = self.layers[_layer]
if layer == (_layer.gds_layer, _layer.gds_datatype):
return _layer
raise ValueError(value_error_msg)
[docs] def get_device(self, spec: DeviceSpec, **kwargs: dict) -> phidl.Device:
"""Get a specific device within the PDK
Args:
spec (DeviceSpec): device instance, name (string), or callable function that identifies desired device
kwargs (dict): keyword arguments to pass to `spec`.
Returns:
(Device): instance of device matching the queried DeviceSpec
"""
if callable(spec):
d = spec(**kwargs)
if not isinstance(d, phidl.Device):
# allow both phidl and qnngds Devices
# (qnngds device has layer-assigned ports)
raise ValueError(
f"Callable argument `spec` did not return a phidl.Device or Device, got {type(d)}"
)
return d
if isinstance(spec, phidl.Device):
if len(kwargs) > 0:
raise ValueError(
"`spec` was passed a phidl.Device, but kwargs were also passed. These kwargs will be ignored"
)
return spec
if isinstance(spec, str):
if spec not in self.devices:
approx_match = [d for d in self.devices if spec in d]
raise ValueError(
f"{spec} from PDK {self.name} not in self.devices: did you mean {approx_match}"
)
return self.devices[spec](**kwargs)
raise ValueError(
f"Argument `spec` is invalid, must be a device, function that returns a device, or string name for device registered with active PDK, got {type(spec)}"
)
[docs] def get_cross_section(self, spec: CrossSectionSpec) -> phidl.CrossSection:
"""Get a specific layer within the PDK
Args:
spec (CrossSectionSpec): device instance, name (string), or callable function that identifies desired device
Returns:
(CrossSection): instance of device matching the queried CrossSectionSpec
"""
if callable(spec):
d = spec()
if not isinstance(d, phidl.CrossSection):
# allow both phidl and qnngds CrossSections
# (qnngds device has layer-assigned ports)
raise ValueError(
f"Callable argument `spec` did not return a phidl.CrossSection, got {type(d)}"
)
return d
if isinstance(spec, phidl.CrossSection):
return spec
if isinstance(spec, str):
if spec not in self.cross_sections:
approx_match = [d for d in self.cross_sections if spec in d]
raise ValueError(
f"{spec} from PDK {self.name} not in self.cross_sections: did you mean {approx_match}"
)
# cross_sections[spec] could be a CrossSectionFactory (i.e. callable) or CrossSection.
# recurse once to get a CrossSection
return self.get_cross_section(self.cross_sections[spec])
raise ValueError(
f"Argument `spec` is invalid, must be a cross section, function that returns a cross section, or string name for cross section registered with active PDK, got {type(spec)}"
)
[docs]def get_active_pdk() -> Pdk:
"""Get the globally-activated PDK"""
global _ACTIVE_PDK
if _ACTIVE_PDK is None:
return get_generic_pdk()
return _ACTIVE_PDK
[docs]def get_generic_pdk() -> Pdk:
"""Get a generic PDK
Includes the following layers:
* EBEAM_FINE (1, 0), outline=0.1
* EBEAM_COARSE (2, 0), outline=10
* EBEAM_KEEPOUT (3, 0), keepout=(EBEAM_FINE,)
* PHOTO1 (10, 0)
* PHOTO2 (20, 0)
* PHOTO3 (30, 0)
* PHOTO4 (40, 0)
Includes the following cross-sections for routing:
* ebeam (EBEAM_COARSE)
* photo1 (PHOTO1)
* photo2 (PHOTO2)
* photo3 (PHOTO3)
* photo4 (PHOTO4)
Includes the following layer transitions:
* EBEAM_FINE <-> EBEAM_COARSE
* all transitions within the same layer
"""
layers = LayerSet()
layers.add_layer(Layer(name="EBEAM_FINE", gds_layer=1, outline=0.1))
layers.add_layer(Layer(name="EBEAM_COARSE", gds_layer=2, outline=10))
layers.add_layer(Layer(name="PHOTO1", gds_layer=10))
layers.add_layer(Layer(name="PHOTO2", gds_layer=20))
layers.add_layer(Layer(name="PHOTO3", gds_layer=30))
layers.add_layer(Layer(name="PHOTO4", gds_layer=40))
layers.add_layer(
Layer(
name="EBEAM_KEEPOUT",
gds_layer=3,
keepout=("EBEAM_FINE",),
)
)
cross_sections = dict(
photo1=partial(default_cross_section, layer="PHOTO1"),
photo2=partial(default_cross_section, layer="PHOTO2"),
photo3=partial(default_cross_section, layer="PHOTO3"),
photo4=partial(default_cross_section, layer="PHOTO4"),
ebeam=partial(default_cross_section, radius=40, layer="EBEAM_COARSE"),
)
layer_transitions = {
("EBEAM_FINE", "EBEAM_COARSE"): partial(
fine_to_coarse,
layer1="EBEAM_FINE",
layer2="EBEAM_COARSE",
)
}
layer_transitions |= layer_auto_transitions(layers)
return Pdk(
name="generic",
layers=layers,
cross_sections=cross_sections,
layer_transitions=layer_transitions,
devices=[],
)
def _set_active_pdk(pdk: Pdk):
"""Helper method for PDK activation"""
global _ACTIVE_PDK
_ACTIVE_PDK = pdk
[docs]def get_layer(layer: LayerSpec) -> Layer:
"""Get a specific layer within the globally-activated PDK
Args:
layer (LayerSpec): string, int, or tuple that identifies the desired layer
Returns:
(Layer): instance of layer matching the queried LayerSpec
"""
return get_active_pdk().get_layer(layer)
[docs]def get_device(spec: DeviceSpec, **kwargs: dict) -> phidl.Device:
"""Get a specific layer within the globally-activated PDK
Parameters:
spec (DeviceSpec): device instance, name (string), or callable function that identifies desired device
kwargs (dict): Keyword arguments to pass to `spec`.
Returns:
(Device): instance of device matching the queried DeviceSpec
"""
return get_active_pdk().get_device(spec, **kwargs)
[docs]def get_cross_section(cross_section: CrossSectionSpec) -> phidl.CrossSection:
"""Get a specific layer within the globally-activated PDK
Parameters:
cross_section (CrossSectionSpec): cross_section instance, name (string), or callable function that identifies desired cross_section
Returns:
(CrossSection): instance of cross_section matching the queried CrossSectionSpec
"""
return get_active_pdk().get_cross_section(cross_section)
[docs]def layer_auto_transitions(layer_set: LayerSet) -> dict[LayerSpec, DeviceSpec]:
"""Generate layer_transitions dictionary for auto tapers within the same layer
Parameters:
layer_set (LayerSet): layers in PDK for which the auto transitions should be generated
Returns:
(dict[Layer, DeviceSpec]): mapping the appropriate taper for each layer auto transitions
"""
outline_layers = get_outline_layers(layer_set)
auto_transitions = {
layer: lambda width1, width2, layer=layer: outline(
hyper_taper(
length=width2, start_width=width1, end_width=width2, layer=layer
),
outline_layers,
)
for layer in layer_set
}
return auto_transitions