[docs / examples] Update docs and examples
This commit is contained in:
parent
8100d8095a
commit
fd2698c503
7 changed files with 132 additions and 107 deletions
|
|
@ -1,3 +1,11 @@
|
|||
"""
|
||||
Tutorial: building hierarchical devices with `Pattern`, `Port`, and `Builder`.
|
||||
|
||||
This file uses photonic-crystal components as the concrete example, so some of
|
||||
the geometry-generation code is domain-specific. The tutorial value is in the
|
||||
Masque patterns around it: creating reusable cells, annotating ports, composing
|
||||
hierarchy with references, and snapping ports together to build a larger circuit.
|
||||
"""
|
||||
from collections.abc import Sequence, Mapping
|
||||
|
||||
import numpy
|
||||
|
|
@ -64,9 +72,9 @@ def perturbed_l3(
|
|||
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
|
||||
perturbed_radius: radius of holes perturbed to form an upwards-directed beam
|
||||
(multiplicative factor). Default 1.1.
|
||||
trench width: Width of the undercut trenches. Default 1200.
|
||||
trench_width: Width of the undercut trenches. Default 1200.
|
||||
|
||||
Returns:
|
||||
`Pattern` object representing the L3 design.
|
||||
|
|
@ -79,14 +87,15 @@ def perturbed_l3(
|
|||
shifts_a=shifts_a,
|
||||
shifts_r=shifts_r)
|
||||
|
||||
# Build L3 cavity, using references to the provided hole pattern
|
||||
# Build the cavity by instancing the supplied `hole` pattern many times.
|
||||
# Using references keeps the pattern compact even though it contains many holes.
|
||||
pat = Pattern()
|
||||
pat.refs[hole] += [
|
||||
Ref(scale=r, offset=(lattice_constant * x,
|
||||
lattice_constant * y))
|
||||
for x, y, r in xyr]
|
||||
|
||||
# Add rectangular undercut aids
|
||||
# Add rectangular undercut aids based on the referenced hole extents.
|
||||
min_xy, max_xy = pat.get_bounds_nonempty(hole_lib)
|
||||
trench_dx = max_xy[0] - min_xy[0]
|
||||
|
||||
|
|
@ -95,7 +104,7 @@ def perturbed_l3(
|
|||
Polygon.rect(ymax=min_xy[1], xmin=min_xy[0], lx=trench_dx, ly=trench_width),
|
||||
]
|
||||
|
||||
# Ports are at outer extents of the device (with y=0)
|
||||
# Define the interface in Masque terms: two ports at the left/right extents.
|
||||
extent = lattice_constant * xy_size[0]
|
||||
pat.ports = dict(
|
||||
input=Port((-extent, 0), rotation=0, ptype='pcwg'),
|
||||
|
|
@ -125,17 +134,17 @@ def waveguide(
|
|||
Returns:
|
||||
`Pattern` object representing the waveguide.
|
||||
"""
|
||||
# Generate hole locations
|
||||
# Generate the normalized lattice locations for the line defect.
|
||||
xy = pcgen.waveguide(length=length, num_mirror=mirror_periods)
|
||||
|
||||
# Build the pattern
|
||||
# Build the pattern by placing repeated references to the same hole cell.
|
||||
pat = Pattern()
|
||||
pat.refs[hole] += [
|
||||
Ref(offset=(lattice_constant * x,
|
||||
lattice_constant * y))
|
||||
for x, y in xy]
|
||||
|
||||
# Ports are at outer edges, with y=0
|
||||
# Publish the device interface as two ports at the outer edges.
|
||||
extent = lattice_constant * length / 2
|
||||
pat.ports = dict(
|
||||
left=Port((-extent, 0), rotation=0, ptype='pcwg'),
|
||||
|
|
@ -164,17 +173,17 @@ def bend(
|
|||
`Pattern` object representing the waveguide bend.
|
||||
Ports are named 'left' (input) and 'right' (output).
|
||||
"""
|
||||
# Generate hole locations
|
||||
# Generate the normalized lattice locations for the bend.
|
||||
xy = pcgen.wgbend(num_mirror=mirror_periods)
|
||||
|
||||
# Build the pattern
|
||||
pat= Pattern()
|
||||
# Build the pattern by instancing the shared hole cell.
|
||||
pat = Pattern()
|
||||
pat.refs[hole] += [
|
||||
Ref(offset=(lattice_constant * x,
|
||||
lattice_constant * y))
|
||||
for x, y in xy]
|
||||
|
||||
# Figure out port locations.
|
||||
# Publish the bend interface as two ports.
|
||||
extent = lattice_constant * mirror_periods
|
||||
pat.ports = dict(
|
||||
left=Port((-extent, 0), rotation=0, ptype='pcwg'),
|
||||
|
|
@ -203,17 +212,17 @@ def y_splitter(
|
|||
`Pattern` object representing the y-splitter.
|
||||
Ports are named 'in', 'top', and 'bottom'.
|
||||
"""
|
||||
# Generate hole locations
|
||||
# Generate the normalized lattice locations for the splitter.
|
||||
xy = pcgen.y_splitter(num_mirror=mirror_periods)
|
||||
|
||||
# Build pattern
|
||||
# Build the pattern by instancing the shared hole cell.
|
||||
pat = Pattern()
|
||||
pat.refs[hole] += [
|
||||
Ref(offset=(lattice_constant * x,
|
||||
lattice_constant * y))
|
||||
for x, y in xy]
|
||||
|
||||
# Determine port locations
|
||||
# Publish the splitter interface as one input and two outputs.
|
||||
extent = lattice_constant * mirror_periods
|
||||
pat.ports = {
|
||||
'in': Port((-extent, 0), rotation=0, ptype='pcwg'),
|
||||
|
|
@ -227,13 +236,13 @@ def y_splitter(
|
|||
|
||||
|
||||
def main(interactive: bool = True) -> None:
|
||||
# Generate some basic hole patterns
|
||||
# First make a couple of reusable primitive cells.
|
||||
shape_lib = {
|
||||
'smile': basic_shapes.smile(RADIUS),
|
||||
'hole': basic_shapes.hole(RADIUS),
|
||||
}
|
||||
|
||||
# Build some devices
|
||||
# Then build a small library of higher-level devices from those primitives.
|
||||
a = LATTICE_CONSTANT
|
||||
|
||||
devices = {}
|
||||
|
|
@ -245,22 +254,23 @@ def main(interactive: bool = True) -> None:
|
|||
devices['ysplit'] = y_splitter(lattice_constant=a, hole='hole', mirror_periods=5)
|
||||
devices['l3cav'] = perturbed_l3(lattice_constant=a, hole='smile', hole_lib=shape_lib, xy_size=(4, 10)) # uses smile :)
|
||||
|
||||
# Turn our dict of devices into a Library.
|
||||
# This provides some convenience functions in the future!
|
||||
# Turn the device mapping into a `Library`.
|
||||
# That gives us convenience helpers for hierarchy inspection and abstract views.
|
||||
lib = Library(devices)
|
||||
|
||||
#
|
||||
# Build a circuit
|
||||
#
|
||||
# Create a `Builder`, and add the circuit to our library as "my_circuit".
|
||||
# Create a `Builder`, and register the resulting top cell as "my_circuit".
|
||||
circ = Builder(library=lib, name='my_circuit')
|
||||
|
||||
# Start by placing a waveguide. Call its ports "in" and "signal".
|
||||
# Start by placing a waveguide and renaming its ports to match the circuit-level
|
||||
# names we want to use while assembling the design.
|
||||
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".
|
||||
# Extend the signal path by attaching another waveguide.
|
||||
# Because `wg10` only has one unattached port left after the plug, Masque can
|
||||
# infer that it should keep the name `signal`.
|
||||
circ.plug('wg10', {'signal': 'left'})
|
||||
|
||||
# We could have done the following instead:
|
||||
|
|
@ -268,8 +278,8 @@ def main(interactive: bool = True) -> None:
|
|||
# lib['my_circuit'] = circ_pat
|
||||
# circ_pat.place(lib.abstract('wg10'), ...)
|
||||
# circ_pat.plug(lib.abstract('wg10'), ...)
|
||||
# but `Builder` lets us omit some of the repetition of `lib.abstract(...)`, and uses similar
|
||||
# syntax to `Pather` and `RenderPather`, which add wire/waveguide routing functionality.
|
||||
# but `Builder` removes some repeated `lib.abstract(...)` boilerplate and keeps
|
||||
# the assembly code focused on port-level intent.
|
||||
|
||||
# Attach a y-splitter to the signal path.
|
||||
# Since the y-splitter has 3 ports total, we can't auto-inherit the
|
||||
|
|
@ -281,13 +291,10 @@ def main(interactive: bool = True) -> None:
|
|||
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.
|
||||
# Add a bend to both branches.
|
||||
# Our bend primitive is defined with a specific orientation, so choosing which
|
||||
# port to plug determines whether the path turns clockwise or counterclockwise.
|
||||
# We could also mirror one instance instead of using opposite ports.
|
||||
circ.plug('bend0', {'signal1': 'right'})
|
||||
circ.plug('bend0', {'signal2': 'left'})
|
||||
|
||||
|
|
@ -296,29 +303,26 @@ def main(interactive: bool = True) -> None:
|
|||
circ.plug('l3cav', {'signal1': 'input'})
|
||||
circ.plug('wg10', {'signal1': 'left'})
|
||||
|
||||
# "signal2" just gets a single of equivalent length
|
||||
# `signal2` gets a single waveguide of equivalent overall length.
|
||||
circ.plug('wg28', {'signal2': 'left'})
|
||||
|
||||
# Now we bend both waveguides back towards each other
|
||||
# Now bend both branches 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).
|
||||
# To join the branches, attach a second y-junction.
|
||||
# This succeeds only if both chosen ports agree on the same translation and
|
||||
# rotation for the inserted device; otherwise Masque raises an exception.
|
||||
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 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.
|
||||
# Bake the top-level port metadata into labels so it survives GDS export.
|
||||
# These labels appear on the circuit cell; individual child devices keep their
|
||||
# own port labels in their own cells.
|
||||
ports_to_data(circ.pattern)
|
||||
|
||||
# Check if we forgot to include any patterns... ooops!
|
||||
|
|
@ -330,12 +334,12 @@ def main(interactive: bool = True) -> None:
|
|||
lib.add(shape_lib)
|
||||
assert not lib.dangling_refs()
|
||||
|
||||
# We can visualize the design. Usually it's easier to just view the GDS.
|
||||
# We can visualize the design directly, though opening the written GDS is often easier.
|
||||
if interactive:
|
||||
print('Visualizing... this step may be slow')
|
||||
circ.pattern.visualize(lib)
|
||||
|
||||
#Write out to GDS, only keeping patterns referenced by our circuit (including itself)
|
||||
# Write out only the subtree reachable from our top cell.
|
||||
subtree = lib.subtree('my_circuit') # don't include wg90, which we don't use
|
||||
check_valid_names(subtree.keys())
|
||||
writefile(subtree, 'circuit.gds', **GDS_OPTS)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue