tutorial updates

This commit is contained in:
jan 2022-02-27 21:21:44 -08:00
parent 0471addd65
commit 47d655d270
4 changed files with 549 additions and 260 deletions

View File

@ -4,12 +4,18 @@ import numpy
from numpy import pi from numpy import pi
from masque import layer_t, Pattern, SubPattern, Label from masque import layer_t, Pattern, SubPattern, Label
from masque.shapes import Circle, Arc from masque.shapes import Circle, Arc, Polygon
from masque.builder import Device, Port from masque.builder import Device, Port
from masque.library import Library, DeviceLibrary from masque.library import Library, DeviceLibrary
import masque.file.gdsii import masque.file.gdsii
import pcgen
# Note that masque units are arbitrary, and are only given
# physical significance when writing to a file.
GDS_OPTS = {
'meters_per_unit': 1e-9, # GDS database unit, 1 nanometer
'logical_units_per_unit': 1e-3, # GDS display unit, 1 micron
}
def hole( def hole(
@ -20,8 +26,8 @@ def hole(
Generate a pattern containing a single circular hole. Generate a pattern containing a single circular hole.
Args: Args:
layer: Layer to draw the circle on.
radius: Circle radius. radius: Circle radius.
layer: Layer to draw the circle on.
Returns: Returns:
Pattern, named `'hole'` Pattern, named `'hole'`
@ -32,6 +38,32 @@ def hole(
return pat return pat
def triangle(
radius: float,
layer: layer_t = (1, 0),
) -> Pattern:
"""
Generate a pattern containing a single triangular hole.
Args:
radius: Radius of circumscribed circle.
layer: Layer to draw the circle on.
Returns:
Pattern, named `'triangle'`
"""
vertices = numpy.array([
(numpy.cos( pi / 2), numpy.sin( pi / 2)),
(numpy.cos(pi + pi / 6), numpy.sin(pi + pi / 6)),
(numpy.cos( - pi / 6), numpy.sin( - pi / 6)),
]) * radius
pat = Pattern('triangle', shapes=[
Polygon(offset=(0, 0), layer=layer, vertices=vertices),
])
return pat
def smile( def smile(
radius: float, radius: float,
layer: layer_t = (1, 0), layer: layer_t = (1, 0),
@ -66,14 +98,18 @@ def smile(
return pat return pat
def _main() -> None: def main() -> None:
hole_pat = hole(1000) hole_pat = hole(1000)
smile_pat = smile(1000) smile_pat = smile(1000)
tri_pat = triangle(1000)
masque.file.gdsii.writefile([hole_pat, smile_pat], 'basic.gds', 1e-9, 1e-3) units_per_meter = 1e-9
units_per_display_unit = 1e-3
masque.file.gdsii.writefile([hole_pat, tri_pat, smile_pat], 'basic_shapes.gds', **GDS_OPTS)
smile_pat.visualize() smile_pat.visualize()
if __name__ == '__main__': if __name__ == '__main__':
_main() main()

View File

@ -0,0 +1,368 @@
from typing import Tuple, Sequence, Dict
import numpy
from numpy import pi
from masque import layer_t, Pattern, SubPattern, Label
from masque.shapes import Polygon
from masque.builder import Device, Port
from masque.file.gdsii import writefile
from masque.utils import rotation_matrix_2d
import pcgen
import basic_shapes
from basic_shapes import GDS_OPTS
LATTICE_CONSTANT = 512
RADIUS = LATTICE_CONSTANT / 2 * 0.75
def perturbed_l3(
lattice_constant: float,
hole: Pattern,
trench_dose: float = 1.0,
trench_layer: layer_t = (1, 0),
shifts_a: Sequence[float] = (0.15, 0, 0.075),
shifts_r: Sequence[float] = (1.0, 1.0, 1.0),
xy_size: Tuple[int, int] = (10, 10),
perturbed_radius: float = 1.1,
trench_width: float = 1200,
) -> Device:
"""
Generate a `Device` representing a perturbed L3 cavity.
Args:
lattice_constant: Distance between nearest neighbor holes
hole: `Pattern` object containing a single hole
trench_dose: Dose for the trenches. Default 1.0. (Hole dose is 1.0.)
trench_layer: Layer for the trenches, default `(1, 0)`.
shifts_a: passed to `pcgen.l3_shift`; specifies lattice constant
(1 - multiplicative factor) for shifting holes adjacent to
the defect (same row). Default `(0.15, 0, 0.075)` for first,
second, third holes.
shifts_r: passed to `pcgen.l3_shift`; specifies radius for perturbing
holes adjacent to the defect (same row). Default 1.0 for all holes.
Provided sequence should have same length as `shifts_a`.
xy_size: `(x, y)` number of mirror periods in each direction; total size is
`2 * n + 1` holes in each direction. Default (10, 10).
perturbed_radius: radius of holes perturbed to form an upwards-driected beam
(multiplicative factor). Default 1.1.
trench width: Width of the undercut trenches. Default 1200.
Returns:
`Device` object representing the L3 design.
"""
# Get hole positions and radii
xyr = pcgen.l3_shift_perturbed_defect(mirror_dims=xy_size,
perturbed_radius=perturbed_radius,
shifts_a=shifts_a,
shifts_r=shifts_r)
# Build L3 cavity, using references to the provided hole pattern
pat = Pattern(f'L3p-a{lattice_constant:g}rp{perturbed_radius:g}')
pat.subpatterns += [
SubPattern(hole, scale=r,
offset=(lattice_constant * x,
lattice_constant * y))
for x, y, r in xyr]
# Add rectangular undercut aids
min_xy, max_xy = pat.get_bounds_nonempty()
trench_dx = max_xy[0] - min_xy[0]
pat.shapes += [
Polygon.rect(ymin=max_xy[1], xmin=min_xy[0], lx=trench_dx, ly=trench_width,
layer=trench_layer, dose=trench_dose),
Polygon.rect(ymax=min_xy[1], xmin=min_xy[0], lx=trench_dx, ly=trench_width,
layer=trench_layer, dose=trench_dose),
]
# Ports are at outer extents of the device (with y=0)
extent = lattice_constant * xy_size[0]
ports = {
'input': Port((-extent, 0), rotation=0, ptype='pcwg'),
'output': Port((extent, 0), rotation=pi, ptype='pcwg'),
}
return Device(pat, ports)
def waveguide(
lattice_constant: float,
hole: Pattern,
length: int,
mirror_periods: int,
) -> Device:
"""
Generate a `Device` representing a photonic crystal line-defect waveguide.
Args:
lattice_constant: Distance between nearest neighbor holes
hole: `Pattern` object containing a single hole
length: Distance (number of mirror periods) between the input and output ports.
Ports are placed at lattice sites.
mirror_periods: Number of hole rows on each side of the line defect
Returns:
`Device` object representing the waveguide.
"""
# Generate hole locations
xy = pcgen.waveguide(length=length, num_mirror=mirror_periods)
# Build the pattern
pat = Pattern(f'_wg-a{lattice_constant:g}l{length}')
pat.subpatterns += [SubPattern(hole, offset=(lattice_constant * x,
lattice_constant * y))
for x, y in xy]
# Ports are at outer edges, with y=0
extent = lattice_constant * length / 2
ports = {
'left': Port((-extent, 0), rotation=0, ptype='pcwg'),
'right': Port((extent, 0), rotation=pi, ptype='pcwg'),
}
return Device(pat, ports)
def bend(
lattice_constant: float,
hole: Pattern,
mirror_periods: int,
) -> Device:
"""
Generate a `Device` representing a 60-degree counterclockwise bend in a photonic crystal
line-defect waveguide.
Args:
lattice_constant: Distance between nearest neighbor holes
hole: `Pattern` object containing a single hole
mirror_periods: Minimum number of mirror periods on each side of the line defect.
Returns:
`Device` object representing the waveguide bend.
Ports are named 'left' (input) and 'right' (output).
"""
# Generate hole locations
xy = pcgen.wgbend(num_mirror=mirror_periods)
# Build the pattern
pat= Pattern(f'_wgbend-a{lattice_constant:g}l{mirror_periods}')
pat.subpatterns += [
SubPattern(hole, offset=(lattice_constant * x,
lattice_constant * y))
for x, y in xy]
# Figure out port locations.
extent = lattice_constant * mirror_periods
ports = {
'left': Port((-extent, 0), rotation=0, ptype='pcwg'),
'right': Port((extent / 2,
extent * numpy.sqrt(3) / 2),
rotation=pi * 4 / 3, ptype='pcwg'),
}
return Device(pat, ports)
def y_splitter(
lattice_constant: float,
hole: Pattern,
mirror_periods: int,
) -> Device:
"""
Generate a `Device` representing a photonic crystal line-defect waveguide y-splitter.
Args:
lattice_constant: Distance between nearest neighbor holes
hole: `Pattern` object containing a single hole
mirror_periods: Minimum number of mirror periods on each side of the line defect.
Returns:
`Device` object representing the y-splitter.
Ports are named 'in', 'top', and 'bottom'.
"""
# Generate hole locations
xy = pcgen.y_splitter(num_mirror=mirror_periods)
# Build pattern
pat = Pattern(f'_wgsplit_half-a{lattice_constant:g}l{mirror_periods}')
pat.subpatterns += [
SubPattern(hole, offset=(lattice_constant * x,
lattice_constant * y))
for x, y in xy]
# Determine port locations
extent = lattice_constant * mirror_periods
ports = {
'in': Port((-extent, 0), rotation=0, ptype='pcwg'),
'top': Port((extent / 2, extent * numpy.sqrt(3) / 2), rotation=pi * 4 / 3, ptype='pcwg'),
'bot': Port((extent / 2, -extent * numpy.sqrt(3) / 2), rotation=pi * 2 / 3, ptype='pcwg'),
}
return Device(pat, ports)
def dev2pat(device: Device, layer: layer_t = (3, 0)) -> Pattern:
"""
Place a text label at each port location, specifying the port data.
This can be used to debug port locations or to automatically generate ports
when reading in a GDS file.
NOTE that `device` is modified by this function, and `device.pattern` is returned.
Args:
device: The device which is to have its ports labeled. MODIFIED in-place.
layer: The layer on which the labels will be placed.
Returns:
`device.pattern`
"""
for name, port in device.ports.items():
if port.rotation is None:
angle_deg = numpy.inf
else:
angle_deg = numpy.rad2deg(port.rotation)
device.pattern.labels += [
Label(string=f'{name}:{port.ptype} {angle_deg:g}', layer=layer, offset=port.offset)
]
return device.pattern
def pat2dev(
pattern: Pattern,
layers: Sequence[layer_t] = ((3, 0),),
max_depth: int = 999_999,
skip_subcells: bool = True,
) -> Device:
ports = {} # Note: could do a list here, if they're not unique
annotated_cells = set()
def find_ports_each(pat, hierarchy, transform, memo) -> Pattern:
if len(hierarchy) > max_depth - 1:
return pat
if skip_subcells and any(parent in annotated_cells for parent in hierarchy):
return pat
labels = [ll for ll in pat.labels if ll.layer in layers]
if len(labels) == 0:
return pat
if skip_subcells:
annotated_cells.add(pat)
mirr_factor = numpy.array((1, -1)) ** transform[3]
rot_matrix = rotation_matrix_2d(transform[2])
for label in labels:
name, property_string = label.string.split(':')
properties = property_string.split(' ')
ptype = properties[0]
angle_deg = float(properties[1]) if len(ptype) else 0
xy_global = transform[:2] + rot_matrix @ (label.offset * mirr_factor)
angle = numpy.deg2rad(angle_deg) * mirr_factor[0] * mirr_factor[1] + transform[2]
if name in ports:
raise Exception('Duplicate port name in pattern!')
ports[name] = Port(offset=xy_global, rotation=angle, ptype=ptype)
return pat
pattern.dfs(visit_before=find_ports_each, transform=True)
return Device(pattern, ports)
def main(interactive: bool = True):
# Generate some basic hole patterns
smile = basic_shapes.smile(RADIUS)
hole = basic_shapes.hole(RADIUS)
# Build some devices
a = LATTICE_CONSTANT
wg10 = waveguide(lattice_constant=a, hole=hole, length=10, mirror_periods=5).rename('wg10')
wg05 = waveguide(lattice_constant=a, hole=hole, length=5, mirror_periods=5).rename('wg05')
wg28 = waveguide(lattice_constant=a, hole=hole, length=28, mirror_periods=5).rename('wg28')
bend0 = bend(lattice_constant=a, hole=hole, mirror_periods=5).rename('bend0')
ysplit = y_splitter(lattice_constant=a, hole=hole, mirror_periods=5).rename('ysplit')
l3cav = perturbed_l3(lattice_constant=a, hole=smile, xy_size=(4, 10)).rename('l3cav') # uses smile :)
# Autogenerate port labels so that GDS will also contain port data
for device in [wg10, wg05, wg28, l3cav, ysplit, bend0]:
dev2pat(device)
#
# Build a circuit
#
circ = Device(name='my_circuit', ports={})
# Start by placing a waveguide. Call its ports "in" and "signal".
circ.place(wg10, offset=(0, 0), port_map={'left': 'in', 'right': 'signal'})
# Extend the signal path by attaching the "left" port of a waveguide.
# Since there is only one other port ("right") on the waveguide we
# are attaching (wg10), it automatically inherits the name "signal".
circ.plug(wg10, {'signal': 'left'})
# Attach a y-splitter to the signal path.
# Since the y-splitter has 3 ports total, we can't auto-inherit the
# port name, so we have to specify what we want to name the unattached
# ports. We can call them "signal1" and "signal2".
circ.plug(ysplit, {'signal': 'in'}, {'top': 'signal1', 'bot': 'signal2'})
# Add a waveguide to both signal ports, inheriting their names.
circ.plug(wg05, {'signal1': 'left'})
circ.plug(wg05, {'signal2': 'left'})
# Add a bend to both ports.
# Our bend's ports "left" and "right" refer to the original counterclockwise
# orientation. We want the bends to turn in opposite directions, so we attach
# the "right" port to "signal1" to bend clockwise, and the "left" port
# to "signal2" to bend counterclockwise.
# We could also use `mirrored=(True, False)` to mirror one of the devices
# and then use same device port on both paths.
circ.plug(bend0, {'signal1': 'right'})
circ.plug(bend0, {'signal2': 'left'})
# We add some waveguides and a cavity to "signal1".
circ.plug(wg10, {'signal1': 'left'})
circ.plug(l3cav, {'signal1': 'input'})
circ.plug(wg10, {'signal1': 'left'})
# "signal2" just gets a single of equivalent length
circ.plug(wg28, {'signal2': 'left'})
# Now we bend both waveguides back towards each other
circ.plug(bend0, {'signal1': 'right'})
circ.plug(bend0, {'signal2': 'left'})
circ.plug(wg05, {'signal1': 'left'})
circ.plug(wg05, {'signal2': 'left'})
# To join the waveguides, we attach a second y-junction.
# We plug "signal1" into the "bot" port, and "signal2" into the "top" port.
# The remaining port gets named "signal_out".
# This operation would raise an exception if the ports did not line up
# correctly (i.e. they required different rotations or translations of the
# y-junction device).
circ.plug(ysplit, {'signal1': 'bot', 'signal2': 'top'}, {'in': 'signal_out'})
# Finally, add some more waveguide to "signal_out".
circ.plug(wg10, {'signal_out': 'left'})
# We can visualize the design. Usually it's easier to just view the GDS.
if interactive:
print('Visualizing... this step may be slow')
circ.pattern.visualize()
# We can also add text labels for our circuit's ports.
# They will appear at the uppermost hierarchy level, while the individual
# device ports will appear further down, in their respective cells.
dev2pat(circ)
# Write out to GDS
writefile(circ.pattern, 'circuit.gds', **GDS_OPTS)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,136 @@
from typing import Tuple, Sequence, Callable
from pprint import pformat
import numpy
from numpy import pi
from masque.builder import Device
from masque.library import Library, LibDeviceLibrary
from masque.file.gdsii import writefile, load_libraryfile
import pcgen
import basic_shapes
import devices
from basic_shapes import GDS_OPTS
def main() -> None:
# Define a `Library`-backed `DeviceLibrary`, which provides lazy evaluation
# for device generation code and lazy-loading of GDS contents.
device_lib = LibDeviceLibrary()
#
# Load some devices from a GDS file
#
# Scan circuit.gds and prepare to lazy-load its contents
pattern_lib, _properties = load_libraryfile('circuit.gds', tag='mycirc01')
# Add it into the device library by providing a way to read port info
# This maintains the lazy evaluation from above, so no patterns
# are actually read yet.
device_lib.add_library(pattern_lib, pat2dev=devices.pat2dev)
print('Devices loaded from GDS into library:\n' + pformat(list(device_lib.keys())))
#
# Add some new devices to the library, this time from python code rather than GDS
#
a = devices.LATTICE_CONSTANT
tri = basic_shapes.triangle(devices.RADIUS)
# Convenience function for adding devices
# This is roughly equivalent to
# `device_lib[name] = lambda: devices.dev2pat(fn())`
# but it also guarantees that the resulting pattern is named `name`.
def add(name: str, fn: Callable[[], Device]) -> None:
device_lib.add_device(name=name, fn=fn, dev2pat=devices.dev2pat)
# Triangle-based variants. These are defined here, but they won't run until they're
# retrieved from the library.
add('tri_wg10', lambda: devices.waveguide(lattice_constant=a, hole=tri, length=10, mirror_periods=5))
add('tri_wg05', lambda: devices.waveguide(lattice_constant=a, hole=tri, length=5, mirror_periods=5))
add('tri_wg28', lambda: devices.waveguide(lattice_constant=a, hole=tri, length=28, mirror_periods=5))
add('tri_bend0', lambda: devices.bend(lattice_constant=a, hole=tri, mirror_periods=5))
add('tri_ysplit', lambda: devices.y_splitter(lattice_constant=a, hole=tri, mirror_periods=5))
add('tri_l3cav', lambda: devices.perturbed_l3(lattice_constant=a, hole=tri, xy_size=(4, 10)))
#
# Build a mixed waveguide with an L3 cavity in the middle
#
# Immediately start building from an instance of the L3 cavity
circ2 = device_lib['tri_l3cav'].build('mixed_wg_cav')
print(device_lib['wg10'].ports)
circ2.plug(device_lib['wg10'], {'input': 'right'})
circ2.plug(device_lib['wg10'], {'output': 'left'})
circ2.plug(device_lib['tri_wg10'], {'input': 'right'})
circ2.plug(device_lib['tri_wg10'], {'output': 'left'})
# Add the circuit to the device library.
# It has already been generated, so we can use `set_const` as a shorthand for
# `device_lib['mixed_wg_cav'] = lambda: circ2`
device_lib.set_const(circ2)
#
# Build a device that could plug into our mixed_wg_cav and joins the two ports
#
# We'll be designing against an existing device's interface...
circ3 = circ2.as_interface('loop_segment')
# ... that lets us continue from where we left off.
circ3.plug(device_lib['tri_bend0'], {'input': 'right'})
circ3.plug(device_lib['tri_bend0'], {'input': 'left'}, mirrored=(True, False)) # mirror since no tri y-symmetry
circ3.plug(device_lib['tri_bend0'], {'input': 'right'})
circ3.plug(device_lib['bend0'], {'output': 'left'})
circ3.plug(device_lib['bend0'], {'output': 'left'})
circ3.plug(device_lib['bend0'], {'output': 'left'})
circ3.plug(device_lib['tri_wg10'], {'input': 'right'})
circ3.plug(device_lib['tri_wg28'], {'input': 'right'})
circ3.plug(device_lib['tri_wg10'], {'input': 'right', 'output': 'left'})
device_lib.set_const(circ3)
#
# Write all devices into a GDS file
#
# This line could be slow, since it generates or loads many of the devices
# since they were not all accessed above.
all_device_pats = [dev.pattern for dev in device_lib.values()]
writefile(all_device_pats, 'library.gds', **GDS_OPTS)
if __name__ == '__main__':
main()
#
#class prout:
# def place(
# self,
# other: Device,
# label_layer: layer_t = 'WATLAYER',
# *,
# port_map: Optional[Dict[str, Optional[str]]] = None,
# **kwargs,
# ) -> 'prout':
#
# Device.place(self, other, port_map=port_map, **kwargs)
# name: Optional[str]
# for name in other.ports:
# if port_map:
# assert(name is not None)
# name = port_map.get(name, name)
# if name is None:
# continue
# self.pattern.labels += [
# Label(string=name, offset=self.ports[name].offset, layer=layer)]
# return self
#

View File

@ -1,251 +0,0 @@
from typing import Tuple, Sequence
import numpy
from numpy import pi
from masque import layer_t, Pattern, SubPattern, Label
from masque.shapes import Polygon, Circle
from masque.builder import Device, Port
from masque.library import Library, DeviceLibrary
from masque.file.gdsii import writefile
import pcgen
import basic
def perturbed_l3(
lattice_constant: float,
hole: Pattern,
trench_dose: float = 1.0,
trench_layer: layer_t = (1, 0),
shifts_a: Sequence[float] = (0.15, 0, 0.075),
shifts_r: Sequence[float] = (1.0, 1.0, 1.0),
xy_size: Tuple[int, int] = (10, 10),
perturbed_radius: float = 1.1,
trench_width: float = 1200,
) -> Device:
"""
Generate a `Device` representing a perturbed L3 cavity.
Args:
lattice_constant: Distance between nearest neighbor holes
hole: `Pattern` object containing a single hole
trench_dose: Dose for the trenches. Default 1.0. (Hole dose is 1.0.)
trench_layer: Layer for the trenches, default `(1, 0)`.
shifts_a: passed to `pcgen.l3_shift`; specifies lattice constant
(1 - multiplicative factor) for shifting holes adjacent to
the defect (same row). Default `(0.15, 0, 0.075)` for first,
second, third holes.
shifts_r: passed to `pcgen.l3_shift`; specifies radius for perturbing
holes adjacent to the defect (same row). Default 1.0 for all holes.
Provided sequence should have same length as `shifts_a`.
xy_size: `(x, y)` number of mirror periods in each direction; total size is
`2 * n + 1` holes in each direction. Default (10, 10).
perturbed_radius: radius of holes perturbed to form an upwards-driected beam
(multiplicative factor). Default 1.1.
trench width: Width of the undercut trenches. Default 1200.
Returns:
`Device` object representing the L3 design.
"""
xyr = pcgen.l3_shift_perturbed_defect(mirror_dims=xy_size,
perturbed_radius=perturbed_radius,
shifts_a=shifts_a,
shifts_r=shifts_r)
pat = Pattern(f'L3p-a{lattice_constant:g}rp{perturbed_radius:g}')
pat.subpatterns += [SubPattern(hole,
offset=(lattice_constant * x,
lattice_constant * y),
scale=r)
for x, y, r in xyr]
bounds = pat.get_bounds()
assert(bounds is not None)
min_xy, max_xy = bounds
trench_dx = max_xy[0] - min_xy[0]
pat.shapes += [
Polygon.rect(ymin=max_xy[1], xmin=min_xy[0], lx=trench_dx, ly=trench_width,
layer=trench_layer, dose=trench_dose),
Polygon.rect(ymax=min_xy[1], xmin=min_xy[0], lx=trench_dx, ly=trench_width,
layer=trench_layer, dose=trench_dose),
]
extent = lattice_constant * xy_size[0]
ports = {
'input': Port((-extent, 0), rotation=0, ptype='pcwg'),
'output': Port((extent, 0), rotation=pi, ptype='pcwg'),
}
return Device(pat, ports)
def waveguide(
lattice_constant: float,
hole: Pattern,
length: int,
mirror_periods: int,
) -> Device:
"""
Generate a `Device` representing a photonic crystal line-defect waveguide.
Args:
lattice_constant: Distance between nearest neighbor holes
hole: `Pattern` object containing a single hole
length: Distance (number of mirror periods) between the input and output ports.
Ports are placed at lattice sites.
mirror_periods: Number of hole rows on each side of the line defect
Returns:
`Device` object representing the waveguide.
"""
xy = pcgen.waveguide(length=length, num_mirror=mirror_periods)
pat = Pattern(f'_wg-a{lattice_constant:g}l{length}')
pat.subpatterns += [SubPattern(hole, offset=(lattice_constant * x,
lattice_constant * y))
for x, y in xy]
extent = lattice_constant * length / 2
ports = {
'left': Port((-extent, 0), rotation=0, ptype='pcwg'),
'right': Port((extent, 0), rotation=pi, ptype='pcwg'),
}
return Device(pat, ports)
def bend(
lattice_constant: float,
hole: Pattern,
mirror_periods: int,
) -> Device:
"""
Generate a `Device` representing a 60-degree counterclockwise bend in a photonic crystal
line-defect waveguide.
Args:
lattice_constant: Distance between nearest neighbor holes
hole: `Pattern` object containing a single hole
mirror_periods: Minimum number of mirror periods on each side of the line defect.
Returns:
`Device` object representing the waveguide bend.
Ports are named 'left' (input) and 'right' (output).
"""
xy = pcgen.wgbend(num_mirror=mirror_periods)
pat= Pattern(f'_wgbend-a{lattice_constant:g}l{mirror_periods}')
pat.subpatterns += [SubPattern(hole, offset=(lattice_constant * x,
lattice_constant * y))
for x, y in xy]
extent = lattice_constant * mirror_periods
ports = {
'left': Port((-extent, 0), rotation=0, ptype='pcwg'),
'right': Port((extent / 2,
extent * numpy.sqrt(3) / 2), rotation=pi * 4 / 3, ptype='pcwg'),
}
return Device(pat, ports)
def y_splitter(
lattice_constant: float,
hole: Pattern,
mirror_periods: int,
) -> Device:
"""
Generate a `Device` representing a photonic crystal line-defect waveguide y-splitter.
Args:
lattice_constant: Distance between nearest neighbor holes
hole: `Pattern` object containing a single hole
mirror_periods: Minimum number of mirror periods on each side of the line defect.
Returns:
`Device` object representing the y-splitter.
Ports are named 'in', 'top', and 'bottom'.
"""
xy = pcgen.y_splitter(num_mirror=mirror_periods)
pat = Pattern(f'_wgsplit_half-a{lattice_constant:g}l{mirror_periods}')
pat.subpatterns += [SubPattern(hole, offset=(lattice_constant * x,
lattice_constant * y))
for x, y in xy]
extent = lattice_constant * mirror_periods
ports = {
'in': Port((-extent, 0), rotation=0, ptype='pcwg'),
'top': Port((extent / 2,
extent * numpy.sqrt(3) / 2), rotation=pi * 4 / 3, ptype='pcwg'),
'bot': Port((extent / 2,
-extent * numpy.sqrt(3) / 2), rotation=pi * 2 / 3, ptype='pcwg'),
}
return Device(pat, ports)
def label_ports(device: Device, layer: layer_t = (3, 0)) -> Device:
"""
Place a text label at each port location, specifying the port data.
This can be used to debug port locations or to automatically generate ports
when reading in a GDS file.
Args:
device: The device which is to have its ports labeled.
layer: The layer on which the labels will be placed.
Returns:
`device` is returned (and altered in-place)
"""
for name, port in device.ports.items():
angle_deg = numpy.rad2deg(port.rotation) if port.rotation is not None else numpy.inf
device.pattern.labels += [
Label(string=f'{name} (angle {angle_deg:g})', layer=layer, offset=port.offset)
]
return device
def main():
a = 512
radius = a / 2 * 0.75
smile = basic.smile(radius)
hole = basic.hole(radius)
wg10 = label_ports(waveguide(lattice_constant=a, hole=hole, length=10, mirror_periods=5))
wg05 = label_ports(waveguide(lattice_constant=a, hole=hole, length=5, mirror_periods=5))
wg28 = label_ports(waveguide(lattice_constant=a, hole=hole, length=28, mirror_periods=5))
bend0 = label_ports(bend(lattice_constant=a, hole=hole, mirror_periods=5))
l3cav = label_ports(perturbed_l3(lattice_constant=a, hole=smile, xy_size=(4, 10)))
ysplit = label_ports(y_splitter(lattice_constant=a, hole=hole, mirror_periods=5))
dev = Device(name='my_bend', ports={})
dev.place(wg10, offset=(0, 0), port_map={'left': 'in', 'right': 'signal'})
dev.plug(wg10, {'signal': 'left'})
dev.plug(ysplit, {'signal': 'in'}, {'top': 'signal1', 'bot': 'signal2'})
dev.plug(wg05, {'signal1': 'left'})
dev.plug(wg05, {'signal2': 'left'})
dev.plug(bend0, {'signal1': 'right'})
dev.plug(bend0, {'signal2': 'left'})
dev.plug(wg10, {'signal1': 'left'})
dev.plug(l3cav, {'signal1': 'input'})
dev.plug(wg10, {'signal1': 'left'})
dev.plug(wg28, {'signal2': 'left'})
dev.plug(bend0, {'signal1': 'right'})
dev.plug(bend0, {'signal2': 'left'})
dev.plug(wg05, {'signal1': 'left'})
dev.plug(wg05, {'signal2': 'left'})
dev.plug(ysplit, {'signal1': 'bot', 'signal2': 'top'}, {'in': 'signal_out'})
dev.plug(wg10, {'signal_out': 'left'})
writefile(dev.pattern, 'phc.gds', 1e-9, 1e-3)
dev.pattern.visualize()
if __name__ == '__main__':
main()