diff --git a/examples/tutorial/README.md b/examples/tutorial/README.md index 7d7bd59..7210a93 100644 --- a/examples/tutorial/README.md +++ b/examples/tutorial/README.md @@ -1 +1,39 @@ -TODO write tutorial readme +masque Tutorial +=============== + +Contents +-------- + +- [basic_shapes](basic_shapes.py): + * Draw basic geometry + * Export to GDS +- [devices](devices.py) + * Reference other patterns + * Add ports to a pattern + * Snap ports together to build a circuit + * Check for dangling references +- [library](library.py) + * Create a `LazyLibrary`, which loads / generates patterns only when they are first used + * Explore alternate ways of specifying a pattern for `.plug()` and `.place()` + * Design a pattern which is meant to plug into an existing pattern (via `.interface()`) +- [pather](pather.py) + * Use `Pather` to route individual wires and wire bundles + * Use `BasicTool` to generate paths + * Use `BasicTool` to automatically transition between path types +- [renderpather](rendpather.py) + * Use `RenderPather` and `PathTool` to build a layout similar to the one in [pather](pather.py), + but using `Path` shapes instead of `Polygon`s. + + +Additionaly, [pcgen](pcgen.py) is a utility module for generating photonic crystal lattices. + + +Running +------- + +Run from inside the examples directory: +```bash +cd examples/tutorial +python3 basic_shapes.py +klayout -e basic_shapes.gds +``` diff --git a/examples/tutorial/devices.py b/examples/tutorial/devices.py index 6211024..5e36ee3 100644 --- a/examples/tutorial/devices.py +++ b/examples/tutorial/devices.py @@ -31,7 +31,7 @@ def ports_to_data(pat: Pattern) -> Pattern: def data_to_ports(lib: Mapping[str, Pattern], name: str, pat: Pattern) -> Pattern: """ - Scans the Pattern to determine port locations. Same port format as `ports_to_data` + Scan the Pattern to determine port locations. Same port format as `ports_to_data` """ return ports2data.data_to_ports(layers=[(3, 0)], library=lib, pattern=pat, name=name) @@ -246,13 +246,14 @@ 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 -- useful for getting abstracts + # Turn our dict of devices into a Library. + # This provides some convenience functions in the future! lib = Library(devices) # # Build a circuit # - # Create a builder, and add the circuit to our library as "my_circuit" + # Create a `Builder`, and add the circuit to our library as "my_circuit". circ = Builder(library=lib, name='my_circuit') # Start by placing a waveguide. Call its ports "in" and "signal". @@ -263,6 +264,14 @@ def main(interactive: bool = True) -> None: # are attaching (wg10), it automatically inherits the name "signal". circ.plug('wg10', {'signal': 'left'}) + # We could have done the following instead: + # circ_pat = Pattern() + # 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. + # 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 diff --git a/examples/tutorial/library.py b/examples/tutorial/library.py index 5c8dfd0..f871f35 100644 --- a/examples/tutorial/library.py +++ b/examples/tutorial/library.py @@ -60,14 +60,17 @@ def main() -> None: circ2 = Builder(library=lib, ports='tri_l3cav') # First way to get abstracts is `lib.abstract(name)` + # We can use this syntax directly with `Pattern.plug()` and `Pattern.place()` as well as through `Builder`. circ2.plug(lib.abstract('wg10'), {'input': 'right'}) # Second way to get abstracts is to use an AbstractView + # This also works directly with `Pattern.plug()` / `Pattern.place()`. abstracts = lib.abstract_view() circ2.plug(abstracts['wg10'], {'output': 'left'}) # Third way to specify an abstract works by automatically getting - # it from the library already within the Builder object: + # it from the library already within the Builder object. + # This wouldn't work if we only had a `Pattern` (not a `Builder`). # Just pass the pattern name! circ2.plug('tri_wg10', {'input': 'right'}) circ2.plug('tri_wg10', {'output': 'left'}) diff --git a/examples/tutorial/pather.py b/examples/tutorial/pather.py index 7555e47..5aec53c 100644 --- a/examples/tutorial/pather.py +++ b/examples/tutorial/pather.py @@ -1,5 +1,5 @@ """ -Manual wire routing tutorial +Manual wire routing tutorial: Pather and BasicTool """ from typing import Callable from numpy import pi @@ -7,6 +7,7 @@ from masque import Pather, RenderPather, Library, Pattern, Port, layer_t, map_la from masque.builder.tools import BasicTool, PathTool from masque.file.gdsii import writefile +from basic_shapes import GDS_OPTS # # Define some basic wire widths, in nanometers @@ -97,10 +98,25 @@ def make_straight_wire(layer: layer_t, width: float, ptype: str, length: float) return pat +def map_layer(layer: layer_t) -> layer_t: + """ + Map from a strings to GDS layer numbers + """ + layer_mapping = { + 'M1': (10, 0), + 'M2': (20, 0), + 'V1': (30, 0), + } + return layer_mapping.get(layer, layer) + + # # Now we can start building up our library (collection of static cells) and pathing tools. # - +# If any of the operations below are confusing, you can cross-reference against the `RenderPather` +# tutorial, which handles some things more explicitly (e.g. via placement) and simplifies others +# (e.g. geometry definition). +# def main() -> None: # Build some patterns (static cells) using the above functions and store them in a library library = Library() @@ -181,6 +197,7 @@ def main() -> None: # Place two pads, and define their ports as 'VCC' and 'GND' pather.place('pad', offset=(18_000, 30_000), port_map={'wire_port': 'VCC'}) pather.place('pad', offset=(18_000, 60_000), port_map={'wire_port': 'GND'}) + # Add some labels to make the pads easier to distinguish pather.pattern.label(layer='M2', string='VCC', offset=(18e3, 30e3)) pather.pattern.label(layer='M2', string='GND', offset=(18e3, 60e3)) @@ -251,56 +268,9 @@ def main() -> None: # Save the pather's pattern into our library library['Pather_and_BasicTool'] = pather.pattern - - - M1_ptool = PathTool(layer='M1', width=M1_WIDTH, ptype='m1wire') - M2_ptool = PathTool(layer='M2', width=M2_WIDTH, ptype='m2wire') - rpather = RenderPather(tools=M2_ptool, library=library).add_port_pair() - - rpather.place('pad', offset=(18_000, 30_000), port_map={'wire_port': 'VCC'}) - rpather.place('pad', offset=(18_000, 60_000), port_map={'wire_port': 'GND'}) - rpather.pattern.label(layer='M2', string='VCC', offset=(18e3, 30e3)) - rpather.pattern.label(layer='M2', string='GND', offset=(18e3, 60e3)) - - rpather.path('VCC', ccw=False, length=6_000) - rpather.path_to('VCC', ccw=None, x=0) - rpather.path('GND', 0, 5_000) - rpather.path_to('GND', None, x=rpather['VCC'].offset[0]) - - rpather.plug('v1_via', {'GND': 'top'}) - rpather.retool(M1_ptool, keys=['GND']) - rpather.mpath(['GND', 'VCC'], ccw=True, xmax=-10_000, spacing=5_000) - - rpather.plug('v1_via', {'VCC': 'top'}) - rpather.retool(M1_ptool) - rpather.mpath(['GND', 'VCC'], ccw=True, emax=50_000, spacing=1_200) - rpather.mpath(['GND', 'VCC'], ccw=False, emin=1_000, spacing=1_200) - rpather.mpath(['GND', 'VCC'], ccw=False, emin=2_000, spacing=4_500) - - rpather.plug('v1_via', {'VCC': 'bottom'}) - rpather.retool(M2_ptool) - rpather.mpath(['GND', 'VCC'], None, xmin=-28_000) - via_size = abs( - library['v1_via'].ports['top'].offset[0] - - library['v1_via'].ports['bottom'].offset[0] - ) - rpather.path_to('VCC', None, -50_000 + via_size) #, out_ptype='m1wire') - rpather.plug('v1_via', {'VCC': 'top'}) - - rpather.render() - library['RenderPather_and_PathTool'] = rpather.pattern - - # Convert from text-based layers to numeric layers for GDS, and output the file - def map_layer(layer: layer_t) -> layer_t: - layer_mapping = { - 'M1': (10, 0), - 'M2': (20, 0), - 'V1': (30, 0), - } - return layer_mapping.get(layer, layer) library.map_layers(map_layer) - writefile(library, 'pathers.gds', 1e-9) + writefile(library, 'pather.gds', **GDS_OPTS) if __name__ == '__main__': diff --git a/examples/tutorial/renderpather.py b/examples/tutorial/renderpather.py new file mode 100644 index 0000000..a7963b8 --- /dev/null +++ b/examples/tutorial/renderpather.py @@ -0,0 +1,96 @@ +""" +Manual wire routing tutorial: RenderPather an PathTool +""" +from typing import Callable +from masque import RenderPather, Library, Pattern, Port, layer_t, map_layers +from masque.builder.tools import PathTool +from masque.file.gdsii import writefile + +from basic_shapes import GDS_OPTS +from pather import M1_WIDTH, V1_WIDTH, M2_WIDTH, map_layer, make_pad, make_via + + +def main() -> None: + # + # To illustrate the advantages of using `RenderPather`, we use `PathTool` instead + # of `BasicTool`. `PathTool` lacks some sophistication (e.g. no automatic transitions) + # but when used with `RenderPather`, it can consolidate multiple routing steps into + # a single `Path` shape. + # + # We'll try to nearly replicate the layout from the `Pather` tutorial; see `pather.py` + # for more detailed descriptions of the individual pathing steps. + # + + # First, we make a library and generate some of the same patterns as in the pather tutorial + library = Library() + library['pad'] = make_pad() + library['v1_via'] = make_via( + layer_top='M2', + layer_via='V1', + layer_bot='M1', + width_top=M2_WIDTH, + width_via=V1_WIDTH, + width_bot=M1_WIDTH, + ptype_bot='m1wire', + ptype_top='m2wire', + ) + + # `PathTool` is more limited than `BasicTool`. It only generates one type of shape + # (`Path`), so it only needs to know what layer to draw on, what width to draw with, + # and what port type to present. + M1_ptool = PathTool(layer='M1', width=M1_WIDTH, ptype='m1wire') + M2_ptool = PathTool(layer='M2', width=M2_WIDTH, ptype='m2wire') + rpather = RenderPather(tools=M2_ptool, library=library) + + # As in the pather tutorial, we make soem pads and labels... + rpather.place('pad', offset=(18_000, 30_000), port_map={'wire_port': 'VCC'}) + rpather.place('pad', offset=(18_000, 60_000), port_map={'wire_port': 'GND'}) + rpather.pattern.label(layer='M2', string='VCC', offset=(18e3, 30e3)) + rpather.pattern.label(layer='M2', string='GND', offset=(18e3, 60e3)) + + # ...and start routing the signals. + rpather.path('VCC', ccw=False, length=6_000) + rpather.path_to('VCC', ccw=None, x=0) + rpather.path('GND', 0, 5_000) + rpather.path_to('GND', None, x=rpather['VCC'].offset[0]) + + # `PathTool` doesn't know how to transition betwen metal layers, so we have to + # `plug` the via into the GND wire ourselves. + rpather.plug('v1_via', {'GND': 'top'}) + rpather.retool(M1_ptool, keys=['GND']) + rpather.mpath(['GND', 'VCC'], ccw=True, xmax=-10_000, spacing=5_000) + + # Same thing on the VCC wire when it goes down to M1. + rpather.plug('v1_via', {'VCC': 'top'}) + rpather.retool(M1_ptool) + rpather.mpath(['GND', 'VCC'], ccw=True, emax=50_000, spacing=1_200) + rpather.mpath(['GND', 'VCC'], ccw=False, emin=1_000, spacing=1_200) + rpather.mpath(['GND', 'VCC'], ccw=False, emin=2_000, spacing=4_500) + + # And again when VCC goes back up to M2. + rpather.plug('v1_via', {'VCC': 'bottom'}) + rpather.retool(M2_ptool) + rpather.mpath(['GND', 'VCC'], None, xmin=-28_000) + + # Finally, since PathTool has no conception of transitions, we can't + # just ask it to transition to an 'm1wire' port at the end of the final VCC segment. + # Instead, we have to calculate the via size ourselves, and adjust the final position + # to account for it. + via_size = abs( + library['v1_via'].ports['top'].offset[0] + - library['v1_via'].ports['bottom'].offset[0] + ) + rpather.path_to('VCC', None, -50_000 + via_size) + rpather.plug('v1_via', {'VCC': 'top'}) + + rpather.render() + library['RenderPather_and_PathTool'] = rpather.pattern + + + # Convert from text-based layers to numeric layers for GDS, and output the file + library.map_layers(map_layer) + writefile(library, 'render_pather.gds', **GDS_OPTS) + + +if __name__ == '__main__': + main()