from dataclasses import dataclass
from typing import Optional, Union
import numpy as np
from ..device import Device
from ..foundry import AIR, CommonLayer, SILICON
from ..parametric import cubic_taper, cubic_taper_fn, dc_path, grating_arc, link, loopback, straight, trombone, turn
from ..pattern import Box, Pattern, Port
from ..typing import Float2, Int2
from ..utils import fix_dataclass_init_docs
[docs]@fix_dataclass_init_docs
@dataclass
class DC(Pattern):
"""Directional coupler
A directional coupler is a `symmetric` component (across x and y dimensions) that contains two waveguides that
interact and couple light by adiabatically bending the waveguides towards each other, interacting over some
interaction length :code:`interaction_l`, and then adiabatically bending out to the original interport distance.
An MMI can actually be created if the gap_w is set to be negative.
Attributes:
waveguide_w: Waveguide width at the inputs and outputs.
radius: The bend radius of the directional coupler
interport_distance: Distance between the ports of the directional coupler.
gap_w: Gap between the waveguides in the interaction region.
interaction_l: Interaction length for the interaction region.
euler: The euler parameter for the directional coupler bend.
end_l: End length for the coupler (before and after the bends).
coupler_waveguide_w: Coupling waveguide width
"""
waveguide_w: float
radius: float
interport_distance: float
gap_w: float
interaction_l: float
euler: float = 0.2
end_l: float = 0
coupler_waveguide_w: Optional[float] = None
def __post_init__(self):
self.coupler_waveguide_w = self.waveguide_w if self.coupler_waveguide_w is None else self.coupler_waveguide_w
cw = self.coupler_waveguide_w
w = self.waveguide_w
radius, dy = (self.radius, (self.interport_distance - self.gap_w - self.coupler_waveguide_w) / 2)
width = (w, cubic_taper_fn(w, cw), cw, cubic_taper_fn(cw, w), w) if cw != w else w
lower_path = link(self.end_l, dc_path(radius, dy, self.interaction_l, self.euler), self.end_l).path(width)
upper_path = link(self.end_l, dc_path(radius, -dy, self.interaction_l, self.euler), self.end_l).path(width)
upper_path.translate(dx=0, dy=self.interport_distance)
super(DC, self).__init__(lower_path, upper_path)
self.lower_path, self.upper_path = lower_path, upper_path
self.port['a0'] = Port(0, 0, -180, w=self.waveguide_w)
self.port['a1'] = Port(0, self.interport_distance, -180, w=self.waveguide_w)
self.port['b0'] = Port(self.size[0], 0, w=self.waveguide_w)
self.port['b1'] = Port(self.size[0], self.interport_distance, w=self.waveguide_w)
self.lower_path.port = {'a0': self.port['a0'].copy, 'b0': self.port['b0'].copy}
self.upper_path.port = {'a0': self.port['a1'].copy, 'b0': self.port['b1'].copy}
self.refs.extend([self.lower_path, upper_path])
@property
def interaction_points(self) -> np.ndarray:
bl = np.asarray(self.center) - np.asarray((self.interaction_l, self.waveguide_w + self.gap_w)) / 2
tl = bl + np.asarray((0, self.waveguide_w + self.gap_w))
br = bl + np.asarray((self.interaction_l, 0))
tr = tl + np.asarray((self.interaction_l, 0))
return np.vstack((bl, tl, br, tr))
@property
def path_array(self):
return np.array([self.polygons[:3], self.polygons[3:]])
[docs] def device(self, layer: str = CommonLayer.RIDGE_SI):
device = Device('dc', [(self, layer)])
device.port = self.port_copy
device.lower_path = self.lower_path
device.upper_path = self.upper_path
return device
[docs]@fix_dataclass_init_docs
@dataclass
class Cross(Pattern):
"""Cross
Attributes:
waveguide: waveguide to form the crossing (used to implement tapering)
"""
waveguide: Pattern
def __post_init__(self):
horizontal = self.waveguide
vertical = self.waveguide.copy.rotate(90, self.waveguide.center)
super().__init__(horizontal, vertical)
self.port['a0'] = horizontal.port['a0']
self.port['a1'] = vertical.port['a0']
self.port['b0'] = horizontal.port['b0']
self.port['b1'] = vertical.port['b0']
[docs]@fix_dataclass_init_docs
@dataclass
class Array(Pattern):
"""Array of boxes or ellipses for 2D photonic crystals.
This class can generate large circle arrays which may be used for photonic crystal designs or for slow
light applications.
Attributes:
unit: The pattern to repeat in the array
grid_shape: Number of rows and columns
pitch: The distance between the circles in the Hole array
"""
unit: Pattern
grid_shape: Int2
pitch: Optional[Union[float, Float2]] = None
def __post_init__(self):
self.pitch = np.array(self.unit.size) * 2 if self.pitch is None else self.pitch
self.pitch = (self.pitch, self.pitch) if np.isscalar(self.pitch) else self.pitch
super().__init__([self.unit.copy.translate(i * self.pitch[0], j * self.pitch[1])
for i in range(self.grid_shape[0]) for j in range(self.grid_shape[1])
])
[docs]@fix_dataclass_init_docs
@dataclass
class Escalator(Device):
"""Escalator device implemented using tapers facing each other
Attributes:
bottom_waveguide: The bottom waveguide of escalator represented as a :code:`Waveguide` object
to allow features such as tapering and coupling.
top_waveguide: The top waveguide of escalator represented as a :code:`Waveguide` object
to allow features such as tapering and coupling.
bottom: Bottom layer of escalator.
top: Top layer of escalator.
name: The device name for the escalator.
"""
bottom_waveguide: Pattern
top_waveguide: Optional[Pattern] = None
bottom: str = CommonLayer.RIDGE_SI
top: str = CommonLayer.RIDGE_SIN
name: str = "escalator"
def __post_init__(self):
pattern_to_layer = [(self.bottom_waveguide, self.bottom)]
pattern_to_layer += [(self.top_waveguide, self.top)] if self.top_waveguide is not None else []
super().__init__(self.name, pattern_to_layer)
self.port = {'a0': self.bottom_waveguide.port['a0'].copy, 'b0': self.bottom_waveguide.port['b0'].copy}
[docs]@fix_dataclass_init_docs
@dataclass
class RibDevice(Device):
"""Waveguide cross section allowing specification of ridge and slab waveguides.
Attributes:
ridge_waveguide: The ridge waveguide (the thick section of the rib), represented as a :code:`Waveguide` object
to allow features such as tapering and coupling. Generally this should be smaller than
:code:`slab_waveguide`. The port of this device is defined using the port of the ridge waveguide.
slab_waveguide: The slab waveguide (the thin section of the rib), represented as a :code:`Waveguide` object
to allow features such as tapering and coupling. Generally this should be larger than
:code:`ridge_waveguide`. If not specified, this merely implements a waveguide pattern.
ridge: Ridge layer.
slab: Slab layer.
name: The device name.
"""
ridge_waveguide: Pattern
slab_waveguide: Optional[Pattern] = None
ridge: str = CommonLayer.RIDGE_SI
slab: str = CommonLayer.RIB_SI
name: str = "rib_wg"
def __post_init__(self):
pattern_to_layer = [(self.ridge_waveguide, self.ridge)]
pattern_to_layer += [(self.slab_waveguide, self.slab)] if self.slab_waveguide is not None else []
super().__init__(self.name, pattern_to_layer)
self.port = {'a0': self.ridge_waveguide.port['a0'].copy, 'b0': self.ridge_waveguide.port['b0'].copy}
[docs]@fix_dataclass_init_docs
@dataclass
class StraightGrating(Device):
"""Straight (non-focusing) grating with partial etch.
Attributes:
extent: Dimension of the extent of the grating.
waveguide: The waveguide to connect to the grating structure (can be tapered if desired)
pitch: The pitch between the grating teeth.
duty_cycle: The fill factor for the grating.
rib_grow: Offset the rib / slab layer in size (usually positive).
num_periods: The number of periods (uses maximum given extent and pitch if not specified).
name: Name of the device.
ridge: The ridge layer for the partial etch.
slab: The slab layer for the partial etch.
"""
extent: Float2
waveguide: Pattern
pitch: float
duty_cycle: float = 0.5
rib_grow: float = 0
num_periods: Optional[int] = None
name: str = 'straight_grating'
ridge: CommonLayer = CommonLayer.RIDGE_SI
slab: CommonLayer = CommonLayer.RIB_SI
def __post_init__(self):
self.stripe_w = self.pitch * (1 - self.duty_cycle)
slab = (Box(self.extent).hstack(self.waveguide).buffer(self.rib_grow), self.slab)
grating = (Box(self.extent).hstack(self.waveguide).striped(self.stripe_w, (self.pitch, 0)), self.ridge)
super().__init__(self.name, [slab, grating, (self.waveguide, self.ridge)])
self.port['a0'] = self.waveguide.port['a0'].copy
[docs]@fix_dataclass_init_docs
@dataclass
class FocusingGrating(Device):
"""Focusing grating with partial etch.
Attributes:
angle: The opening angle for the focusing grating.
waveguide_w: The waveguide width.
wavelength: wavelength accepted by the grating.
duty_cycle: duty cycle for the grating
n_clad: clad material index of refraction (assume oxide by default).
n_core: core material index of refraction (assume silicon by default).
fiber_angle: angle of the fiber in degrees from horizontal (not NOT vertical).
num_periods: number of grating periods
resolution: Number of evaluations for the curve.
grating_frac: The fraction of the distance radiating from the center occupied by the grating (otherwise ridge).
rib_grow: Offset the rib / slab layer in size (usually positive).
waveguide_extra_l: The extra length of the waveguide
name: Name of the device.
ridge: The ridge layer for the partial etch.
slab: The slab layer for the partial etch.
"""
waveguide_w: float = 0.5
angle: float = 22.5
n_env: int = AIR.n
n_core: int = SILICON.n
min_period: int = 40
num_periods: int = 30
wavelength: float = 1.55
fiber_angle: float = 82
duty_cycle: float = 0.5
grating_frac: float = 1
resolution: int = 99
rib_grow: float = 1
waveguide_extra_l: float = 0
name: str = 'focusing_grating'
ridge: CommonLayer = CommonLayer.RIDGE_SI
slab: CommonLayer = CommonLayer.RIB_SI
def __post_init__(self):
grating_arcs = [grating_arc(self.angle, self.duty_cycle, self.n_core, self.n_env,
self.fiber_angle, self.wavelength, m, resolution=self.resolution)
for m in range(self.min_period, self.min_period + self.num_periods)]
sector = Pattern(np.hstack((np.zeros((2, 1)), grating_arcs[0].curve.geoms[0])))
grating = Pattern(grating_arcs, sector)
min_waveguide_l = np.abs(self.waveguide_w / np.tan(np.radians(self.angle)))
self.waveguide = RibDevice(straight(self.waveguide_extra_l + min_waveguide_l).path(self.waveguide_w),
slab=self.slab, ridge=self.ridge)
self.waveguide.halign(min_waveguide_l, left=False)
super().__init__(self.name,
[(grating.buffer(self.rib_grow), self.slab),
(grating, self.ridge), self.waveguide])
self.port['a0'] = self.waveguide.port['a0'].copy
self.translate(*(-self.port['a0'].xy)) # put the a0 port at 0, 0
[docs]@fix_dataclass_init_docs
@dataclass
class TapDC(Pattern):
"""Tap directional coupler
Attributes:
dc: the directional coupler that acts as a tap coupler
grating_pad: the grating pad for the tap
turn_radius: The turn radius for the tap DC
"""
dc: DC
radius: float
angle: float = 90
euler: float = 0
ridge: str = CommonLayer.RIDGE_SI
name: str = 'tap'
def __post_init__(self):
tap_turns = [turn(self.radius, -self.angle, self.euler).path(self.dc.waveguide_w).to(self.dc.port['a1']),
turn(self.radius, self.angle, self.euler).path(self.dc.waveguide_w).to(self.dc.port['b1'])]
super().__init__(self.dc, tap_turns)
self.port['a0'] = self.dc.port['a0']
self.port['b0'] = self.dc.port['b0']
self.port['a1'] = tap_turns[0].port['b0']
self.port['b1'] = tap_turns[1].port['b0']
self.wg_path = self.dc.lower_path
[docs] def with_gratings(self, grating: Union[StraightGrating, FocusingGrating]):
device = Device('tap_dc_with_grating', [(self, grating.ridge)])
device.port = self.port_copy
device.place(grating, self.port['a1'])
device.place(grating, self.port['b1'])
return device
[docs]@fix_dataclass_init_docs
@dataclass
class Interposer(Pattern):
"""Pitch-changing array of waveguides with path length correction.
Attributes:
waveguide_w: The waveguide width.
n: The number of I/O (waveguides) for interposer.
init_pitch: The initial pitch (distance between successive waveguides) entering the interposer.
radius: The radius of bends for the interposer.
trombone_radius: The trombone bend radius for path equalization.
final_pitch: The final pitch (distance between successive waveguides) for the interposer.
self_coupling_extension_extent: The self coupling for alignment, which is useful since a major use case of
the interposer is for fiber array coupling.
additional_x: The additional horizontal distance (useful in fiber array coupling for wirebond clearance).
num_trombones: The number of trombones for path equalization.
trombone_at_end: Whether to use a path-equalizing trombone near the waveguides spaced at :code:`final_period`.
"""
waveguide_w: float
n: int
init_pitch: float
final_pitch: float
radius: Optional[float] = None
euler: float = 0
trombone_radius: float = 5
self_coupling_final: bool = True
self_coupling_init: bool = False
self_coupling_radius: float = None
self_coupling_extension: float = 0
additional_x: float = 0
num_trombones: int = 1
trombone_at_end: bool = True
def __post_init__(self):
w = self.waveguide_w
pitch_diff = self.final_pitch - self.init_pitch
self.radius = np.abs(pitch_diff) / 2 if self.radius is None else self.radius
r = self.radius
paths = []
init_pos = np.zeros((2, self.n))
init_pos[1] = self.init_pitch * np.arange(self.n)
init_pos = init_pos.T
final_pos = np.zeros_like(init_pos)
if np.abs(1 - np.abs(pitch_diff) / 4 / r) > 1:
raise ValueError(f"Radius {r} needs to be at least abs(pitch_diff) / 2 = {np.abs(pitch_diff) / 2}.")
angle_r = np.sign(pitch_diff) * np.arccos(1 - np.abs(pitch_diff) / 4 / r)
angled_length = np.abs(pitch_diff / np.sin(angle_r))
x_length = np.abs(pitch_diff / np.tan(angle_r))
mid = int(np.ceil(self.n / 2))
angle = float(np.degrees(angle_r))
for idx in range(self.n):
init_pos[idx] = np.asarray((0, self.init_pitch * idx))
length_diff = (angled_length - x_length) * idx if idx < mid else (angled_length - x_length) * (self.n - 1 - idx)
segments = []
trombone_section = [trombone(self.trombone_radius,
length_diff / 2 / self.num_trombones, self.euler)] * self.num_trombones
if not self.trombone_at_end:
segments += trombone_section
segments.append(self.additional_x)
if idx < mid:
segments += [turn(r, -angle, self.euler), angled_length * (mid - idx - 1),
turn(r, angle, self.euler), x_length * (idx + 1)]
else:
segments += [turn(r, angle, self.euler), angled_length * (mid - self.n + idx),
turn(r, -angle, self.euler), x_length * (self.n - idx)]
if self.trombone_at_end:
segments += trombone_section
paths.append(link(*segments).path(w).to(init_pos[idx]))
final_pos[idx] = paths[-1].port['b0'].xy
if self.self_coupling_final:
scr = self.final_pitch / 4 if self.self_coupling_radius is None else self.self_coupling_radius
dx, dy = final_pos[0, 0], final_pos[0, 1]
p = self.final_pitch
port = Port(dx, dy - p, -180)
extension = (self.self_coupling_extension, p * (self.n + 1) - 6 * scr)
paths.append(loopback(scr, self.euler, extension).path(w).to(port))
if self.self_coupling_init:
scr = self.init_pitch / 4 if self.self_coupling_radius is None else self.self_coupling_radius
dx, dy = init_pos[0, 0], init_pos[0, 1]
p = self.init_pitch
port = Port(dx, dy - p)
extension = (self.self_coupling_extension, p * (self.n + 1) - 6 * scr)
paths.append(loopback(scr, self.euler, extension).path(w).to(port))
port = {**{f'a{idx}': Port(*init_pos[idx], -180, w=self.waveguide_w) for idx in range(self.n)},
**{f'b{idx}': Port(*final_pos[idx], w=self.waveguide_w) for idx in range(self.n)},
'l0': paths[-1].port['a0'], 'l1': paths[-1].port['b0']}
super().__init__(*paths)
self.self_coupling_path = None if self.self_coupling_extension is None else paths[-1]
self.paths = paths
self.init_pos = init_pos
self.final_pos = final_pos
self.port = port
[docs] def device(self, layer: str = CommonLayer.RIDGE_SI):
return Device('interposer', [(self, layer)]).set_port(self.port_copy)
[docs] def with_gratings(self, grating: FocusingGrating, layer: str = CommonLayer.RIDGE_SI):
interposer = self.device(layer)
interposer.port = self.port
for idx in range(6):
interposer.place(grating, self.port[f'b{idx}'], from_port=grating.port['a0'])
interposer.place(grating, self.port['l0'], from_port=grating.port['a0'])
interposer.place(grating, self.port['l1'], from_port=grating.port['a0'])
return interposer
[docs]@fix_dataclass_init_docs
@dataclass
class TSplitter(Pattern):
"""Pitch-changing array of waveguides with path length correction.
Args:
waveguide_w: The waveguide width.
splitter_l: The splitter length (ignoring the turns).
radius: The radius.
splitter_mmi_w: Splitter MMI width (use twice the waveguide width by default).
input_l: The input extension length.
output_l: The output extension length.
"""
waveguide_w: float
splitter_l: float
radius: float
splitter_mmi_w: Optional[float] = None
input_l: float = 0
output_l: float = 0
def __post_init__(self):
self.splitter_mmi_w = 2 * self.waveguide_w if self.splitter_mmi_w is None else self.splitter_mmi_w
splitter_taper = cubic_taper(init_w=self.waveguide_w,
change_w=self.splitter_mmi_w - self.waveguide_w,
length=self.input_l + self.splitter_l,
taper_length=self.splitter_l,
symmetric=False,
taper_first=False)
turn_shift = self.splitter_mmi_w / 2 - self.waveguide_w / 2
upturn = link(turn(self.radius), self.output_l).path(self.waveguide_w)
downturn = link(turn(self.radius, -90), self.output_l).path(self.waveguide_w)
super(TSplitter, self).__init__(splitter_taper,
upturn.to(splitter_taper.port['b0']).translate(dy=turn_shift),
downturn.to(splitter_taper.port['b0']).translate(dy=-turn_shift))
self.port = {'a0': splitter_taper.port['a0'], 'b0': upturn.port['b0'], 'b1': downturn.port['b0']}