From 47d655d270f9bac114edaf4e159b8e96ae5202f5 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 27 Feb 2022 21:21:44 -0800 Subject: [PATCH] tutorial updates --- .../tutorial/{basic.py => basic_shapes.py} | 54 ++- examples/tutorial/devices.py | 368 ++++++++++++++++++ examples/tutorial/library.py | 136 +++++++ examples/tutorial/phc.py | 251 ------------ 4 files changed, 549 insertions(+), 260 deletions(-) rename examples/tutorial/{basic.py => basic_shapes.py} (57%) create mode 100644 examples/tutorial/devices.py create mode 100644 examples/tutorial/library.py delete mode 100644 examples/tutorial/phc.py diff --git a/examples/tutorial/basic.py b/examples/tutorial/basic_shapes.py similarity index 57% rename from examples/tutorial/basic.py rename to examples/tutorial/basic_shapes.py index 38aa002..ccd89ff 100644 --- a/examples/tutorial/basic.py +++ b/examples/tutorial/basic_shapes.py @@ -4,12 +4,18 @@ import numpy from numpy import pi 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.library import Library, DeviceLibrary 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( @@ -20,8 +26,8 @@ def hole( Generate a pattern containing a single circular hole. Args: - layer: Layer to draw the circle on. radius: Circle radius. + layer: Layer to draw the circle on. Returns: Pattern, named `'hole'` @@ -32,6 +38,32 @@ def hole( 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( radius: float, layer: layer_t = (1, 0), @@ -66,14 +98,18 @@ def smile( return pat -def _main() -> None: - hole_pat = hole(1000) - smile_pat = smile(1000) +def main() -> None: + hole_pat = hole(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 - smile_pat.visualize() + masque.file.gdsii.writefile([hole_pat, tri_pat, smile_pat], 'basic_shapes.gds', **GDS_OPTS) + + smile_pat.visualize() if __name__ == '__main__': - _main() + main() diff --git a/examples/tutorial/devices.py b/examples/tutorial/devices.py new file mode 100644 index 0000000..f5579c4 --- /dev/null +++ b/examples/tutorial/devices.py @@ -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() diff --git a/examples/tutorial/library.py b/examples/tutorial/library.py new file mode 100644 index 0000000..2154232 --- /dev/null +++ b/examples/tutorial/library.py @@ -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 +# diff --git a/examples/tutorial/phc.py b/examples/tutorial/phc.py deleted file mode 100644 index 1e5ea97..0000000 --- a/examples/tutorial/phc.py +++ /dev/null @@ -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()