From f4537a0feb965a8215ec58c934ed014b08860613 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 24 Jan 2023 23:25:10 -0800 Subject: [PATCH] Lots of progress on tutorials --- examples/phc.py | 174 --------------------- examples/tutorial/README.md | 1 + examples/tutorial/basic_shapes.py | 51 +++---- examples/tutorial/devices.py | 241 +++++++++++++++++------------- examples/tutorial/library.py | 98 ++++++------ examples/tutorial/pcgen.py | 7 +- masque/abstract.py | 222 +++++++++++++++++++++++---- masque/builder/builder.py | 30 +++- masque/builder/port_utils.py | 73 ++++----- masque/builder/utils.py | 7 +- masque/file/dxf.py | 7 +- masque/file/gdsii.py | 25 +++- masque/file/oasis.py | 2 +- masque/library.py | 72 ++++----- masque/pattern.py | 57 ++++--- masque/ports.py | 16 +- 16 files changed, 576 insertions(+), 507 deletions(-) delete mode 100644 examples/phc.py diff --git a/examples/phc.py b/examples/phc.py deleted file mode 100644 index 1f829b0..0000000 --- a/examples/phc.py +++ /dev/null @@ -1,174 +0,0 @@ -from typing import Tuple, Sequence - -import numpy # type: ignore -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.klamath import writefile - -import pcgen - - -HOLE_SCALE: float = 1000 -''' Radius for the 'hole' cell. Should be significantly bigger than - 1 (minimum database unit) in order to have enough precision to - reasonably represent a polygonized circle (for GDS) -''' - -def hole(layer: layer_t, - radius: float = HOLE_SCALE * 0.35, - ) -> Pattern: - """ - Generate a pattern containing a single circular hole. - - Args: - layer: Layer to draw the circle on. - radius: Circle radius. - - Returns: - Pattern, named `'hole'` - """ - pat = Pattern('hole', shapes=[ - Circle(radius=radius, offset=(0, 0), layer=layer) - ]) - return pat - - -def perturbed_l3(lattice_constant: float, - hole: Pattern, - 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_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 * lattice_constant / HOLE_SCALE) - for x, y, r in xyr] - - min_xy, max_xy = pat.get_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), - Polygon.rect(ymax=min_xy[1], xmin=min_xy[0], lx=trench_dx, ly=trench_width, layer=trench_layer), - ] - - ports = { - 'input': Port((-lattice_constant * xy_size[0], 0), rotation=0, ptype=1), - 'output': Port((lattice_constant * xy_size[0], 0), rotation=pi, ptype=1), - } - - return Device(pat, ports) - - -def waveguide(lattice_constant: float, - hole: Pattern, - length: int, - mirror_periods: int, - ) -> Device: - xy = pcgen.waveguide(length=length + 2, 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), scale=lattice_constant / HOLE_SCALE) - for x, y in xy] - - ports = { - 'left': Port((-lattice_constant * length / 2, 0), rotation=0, ptype=1), - 'right': Port((lattice_constant * length / 2, 0), rotation=pi, ptype=1), - } - return Device(pat, ports) - - -def bend(lattice_constant: float, - hole: Pattern, - mirror_periods: int, - ) -> Device: - xy = pcgen.wgbend(num_mirror=mirror_periods) - - pat_half = Pattern(f'_wgbend_half-a{lattice_constant:g}l{mirror_periods}') - pat_half.subpatterns += [SubPattern(hole, offset=(lattice_constant * x, - lattice_constant * y), scale=lattice_constant / HOLE_SCALE) - for x, y in xy] - - pat = Pattern(f'_wgbend-a{lattice_constant:g}l{mirror_periods}') - pat.addsp(pat_half, offset=(0, 0), rotation=0, mirrored=(False, False)) - pat.addsp(pat_half, offset=(0, 0), rotation=-2 * pi / 3, mirrored=(True, False)) - - - ports = { - 'left': Port((-lattice_constant * mirror_periods, 0), rotation=0, ptype=1), - 'right': Port((lattice_constant * mirror_periods / 2, - lattice_constant * mirror_periods * numpy.sqrt(3) / 2), rotation=pi * 4 / 3, ptype=1), - } - return Device(pat, ports) - - -def label_ports(device: Device, layer: layer_t = (3, 0)) -> Device: - for name, port in device.ports.items(): - angle_deg = numpy.rad2deg(port.rotation) - device.pattern.labels += [ - Label(string=f'{name} (angle {angle_deg:g})', layer=layer, offset=port.offset) - ] - return device - - -def main(): - hole_layer = (1, 2) - a = 512 - hole_pat = hole(layer=hole_layer) - wg0 = label_ports(waveguide(lattice_constant=a, hole=hole_pat, length=10, mirror_periods=5)) - wg1 = label_ports(waveguide(lattice_constant=a, hole=hole_pat, length=5, mirror_periods=5)) - bend0 = label_ports(bend(lattice_constant=a, hole=hole_pat, mirror_periods=5)) - l3cav = label_ports(perturbed_l3(lattice_constant=a, hole=hole_pat, xy_size=(4, 10))) - - dev = Device(name='my_bend', ports={}) - dev.place(wg0, offset=(0, 0), port_map={'left': 'in', 'right': 'signal'}) - dev.plug(wg0, {'signal': 'left'}) - dev.plug(bend0, {'signal': 'left'}) - dev.plug(wg1, {'signal': 'left'}) - dev.plug(bend0, {'signal': 'right'}) - dev.plug(wg0, {'signal': 'left'}) - dev.plug(l3cav, {'signal': 'input'}) - dev.plug(wg0, {'signal': 'left'}) - - writefile(dev.pattern, 'phc.gds', 1e-9, 1e-3) - dev.pattern.visualize() - - -if __name__ == '__main__': - main() diff --git a/examples/tutorial/README.md b/examples/tutorial/README.md index e69de29..7d7bd59 100644 --- a/examples/tutorial/README.md +++ b/examples/tutorial/README.md @@ -0,0 +1 @@ +TODO write tutorial readme diff --git a/examples/tutorial/basic_shapes.py b/examples/tutorial/basic_shapes.py index ccd89ff..4d896fc 100644 --- a/examples/tutorial/basic_shapes.py +++ b/examples/tutorial/basic_shapes.py @@ -3,19 +3,19 @@ from typing import Tuple, Sequence import numpy from numpy import pi -from masque import layer_t, Pattern, SubPattern, Label -from masque.shapes import Circle, Arc, Polygon -from masque.builder import Device, Port -from masque.library import Library, DeviceLibrary +from masque import ( + layer_t, Pattern, Label, Port, + Circle, Arc, Polygon, + ) import masque.file.gdsii # 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 -} +GDS_OPTS = dict( + meters_per_unit = 1e-9, # GDS database unit, 1 nanometer + logical_units_per_unit = 1e-3, # GDS display unit, 1 micron + ) def hole( @@ -30,10 +30,10 @@ def hole( layer: Layer to draw the circle on. Returns: - Pattern, named `'hole'` + Pattern containing a circle. """ - pat = Pattern('hole', shapes=[ - Circle(radius=radius, offset=(0, 0), layer=layer) + pat = Pattern(shapes=[ + Circle(radius=radius, offset=(0, 0), layer=layer), ]) return pat @@ -50,15 +50,15 @@ def triangle( layer: Layer to draw the circle on. Returns: - Pattern, named `'triangle'` + Pattern containing a 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 + ]) * radius - pat = Pattern('triangle', shapes=[ + pat = Pattern(shapes=[ Polygon(offset=(0, 0), layer=layer, vertices=vertices), ]) return pat @@ -78,37 +78,38 @@ def smile( secondary_layer: Layer to draw eyes and smile on. Returns: - Pattern, named `'smile'` + Pattern containing a smiley face """ # Make an empty pattern - pat = Pattern('smile') + pat = Pattern() # Add all the shapes we want pat.shapes += [ Circle(radius=radius, offset=(0, 0), layer=layer), # Outer circle Circle(radius=radius / 10, offset=(radius / 3, radius / 3), layer=secondary_layer), Circle(radius=radius / 10, offset=(-radius / 3, radius / 3), layer=secondary_layer), - Arc(radii=(radius * 2 / 3, radius * 2 / 3), # Underlying ellipse radii + Arc( + radii=(radius * 2 / 3, radius * 2 / 3), # Underlying ellipse radii angles=(7 / 6 * pi, 11 / 6 * pi), # Angles limiting the arc width=radius / 10, offset=(0, 0), - layer=secondary_layer), + layer=secondary_layer, + ), ] return pat def main() -> None: - hole_pat = hole(1000) - smile_pat = smile(1000) - tri_pat = triangle(1000) + lib = {} - units_per_meter = 1e-9 - units_per_display_unit = 1e-3 + lib['hole'] = hole(1000) + lib['smile'] = smile(1000) + lib['triangle'] = triangle(1000) - masque.file.gdsii.writefile([hole_pat, tri_pat, smile_pat], 'basic_shapes.gds', **GDS_OPTS) + masque.file.gdsii.writefile(lib, 'basic_shapes.gds', **GDS_OPTS) - smile_pat.visualize() + lib['triangle'].visualize() if __name__ == '__main__': diff --git a/examples/tutorial/devices.py b/examples/tutorial/devices.py index 762c17b..c9f8977 100644 --- a/examples/tutorial/devices.py +++ b/examples/tutorial/devices.py @@ -1,12 +1,15 @@ -from typing import Tuple, Sequence, Dict +# TODO update tutorials +from typing import Tuple, Sequence, Dict, Mapping 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, port_utils -from masque.file.gdsii import writefile +from masque import ( + layer_t, Pattern, Ref, Label, Builder, Port, Polygon, + WrapLibrary, Library, + ) +from masque.builder import port_utils +from masque.file.gdsii import writefile, check_valid_names import pcgen import basic_shapes @@ -17,38 +20,41 @@ LATTICE_CONSTANT = 512 RADIUS = LATTICE_CONSTANT / 2 * 0.75 -def dev2pat(dev: Device) -> Pattern: +def dev2pat(dev: Pattern) -> Pattern: """ - Bake port information into the device. + Bake port information into the pattern. This places a label at each port location on layer (3, 0) with text content 'name:ptype angle_deg' """ return port_utils.dev2pat(dev, layer=(3, 0)) -def pat2dev(pat: Pattern) -> Device: +def pat2dev(lib: Mapping[str, Pattern], name: str, pat: Pattern) -> Pattern: """ Scans the Pattern to determine port locations. Same format as `dev2pat` """ - return port_utils.pat2dev(pat, layers=[(3, 0)]) + return port_utils.pat2dev(layers=[(3, 0)], library=lib, pattern=pat, name=name) def perturbed_l3( lattice_constant: float, - hole: Pattern, + hole: str, + hole_lib: Mapping[str, Pattern], 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: + ) -> Pattern: """ - Generate a `Device` representing a perturbed L3 cavity. + Generate a `Pattern` representing a perturbed L3 cavity. Args: lattice_constant: Distance between nearest neighbor holes - hole: `Pattern` object containing a single hole + hole: name of a `Pattern` containing a single hole + hole_lib: Library which contains the `Pattern` object for hole. + Necessary because we need to know how big it is... 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 @@ -64,8 +70,10 @@ def perturbed_l3( trench width: Width of the undercut trenches. Default 1200. Returns: - `Device` object representing the L3 design. + `Pattern` object representing the L3 design. """ + print('Generating perturbed L3...') + # Get hole positions and radii xyr = pcgen.l3_shift_perturbed_defect(mirror_dims=xy_size, perturbed_radius=perturbed_radius, @@ -73,15 +81,14 @@ def perturbed_l3( 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)) + pat = Pattern() + pat.refs += [ + Ref(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() + min_xy, max_xy = pat.get_bounds_nonempty(hole_lib) trench_dx = max_xy[0] - min_xy[0] pat.shapes += [ @@ -91,168 +98,180 @@ def perturbed_l3( # 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'), - } + pat.ports = dict( + input=Port((-extent, 0), rotation=0, ptype='pcwg'), + output=Port((extent, 0), rotation=pi, ptype='pcwg'), + ) - return Device(pat, ports) + dev2pat(pat) + return pat def waveguide( lattice_constant: float, - hole: Pattern, + hole: str, length: int, mirror_periods: int, - ) -> Device: + ) -> Pattern: """ - Generate a `Device` representing a photonic crystal line-defect waveguide. + Generate a `Pattern` representing a photonic crystal line-defect waveguide. Args: lattice_constant: Distance between nearest neighbor holes - hole: `Pattern` object containing a single hole + hole: name of a `Pattern` 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. + `Pattern` 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] + pat = Pattern() + pat.refs += [ + Ref(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) + pat.ports = dict( + left=Port((-extent, 0), rotation=0, ptype='pcwg'), + right=Port((extent, 0), rotation=pi, ptype='pcwg'), + ) + pat2dev(pat) + print(pat) + return pat def bend( lattice_constant: float, - hole: Pattern, + hole: str, mirror_periods: int, - ) -> Device: + ) -> Pattern: """ - Generate a `Device` representing a 60-degree counterclockwise bend in a photonic crystal + Generate a `Pattern` 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 + hole: name of a `Pattern` 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. + `Pattern` 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)) + pat= Pattern() + pat.refs += [ + Ref(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) + pat.ports = dict( + left=Port((-extent, 0), rotation=0, ptype='pcwg'), + right=Port((extent / 2, + extent * numpy.sqrt(3) / 2), + rotation=pi * 4 / 3, ptype='pcwg'), + ) + pat2dev(pat) + return pat def y_splitter( lattice_constant: float, - hole: Pattern, + hole: str, mirror_periods: int, - ) -> Device: + ) -> Pattern: """ - Generate a `Device` representing a photonic crystal line-defect waveguide y-splitter. + Generate a `Pattern` representing a photonic crystal line-defect waveguide y-splitter. Args: lattice_constant: Distance between nearest neighbor holes - hole: `Pattern` object containing a single hole + hole: name of a `Pattern` 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. + `Pattern` 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)) + pat = Pattern() + pat.refs += [ + Ref(hole, offset=(lattice_constant * x, + lattice_constant * y)) for x, y in xy] # Determine port locations extent = lattice_constant * mirror_periods - ports = { + pat.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) + + pat2dev(pat) + return pat -def main(interactive: bool = True): +def main(interactive: bool = True) -> None: # Generate some basic hole patterns - smile = basic_shapes.smile(RADIUS) - hole = basic_shapes.hole(RADIUS) + shape_lib = { + '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) + devices = {} + devices['wg05'] = waveguide(lattice_constant=a, hole='hole', length=5, mirror_periods=5) + devices['wg10'] = waveguide(lattice_constant=a, hole='hole', length=10, mirror_periods=5) + devices['wg28'] = waveguide(lattice_constant=a, hole='hole', length=28, mirror_periods=5) + devices['wg90'] = waveguide(lattice_constant=a, hole='hole', length=90, mirror_periods=5) + devices['bend0'] = bend(lattice_constant=a, hole='hole', mirror_periods=5) + 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 + lib = WrapLibrary(devices) + abv = lib.abstract_view() # lets us use abv[cell] instead of lib.abstract(cell) # # Build a circuit # - circ = Device(name='my_circuit', ports={}) + circ = Builder(library=lib) # Start by placing a waveguide. Call its ports "in" and "signal". - circ.place(wg10, offset=(0, 0), port_map={'left': 'in', 'right': 'signal'}) + circ.place(abv['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'}) + circ.plug(abv['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'}) + circ.plug(abv['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'}) + circ.plug(abv['wg05'], {'signal1': 'left'}) + circ.plug(abv['wg05'], {'signal2': 'left'}) # Add a bend to both ports. # Our bend's ports "left" and "right" refer to the original counterclockwise @@ -261,22 +280,22 @@ def main(interactive: bool = True): # 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'}) + circ.plug(abv['bend0'], {'signal1': 'right'}) + circ.plug(abv['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'}) + circ.plug(abv['wg10'], {'signal1': 'left'}) + circ.plug(abv['l3cav'], {'signal1': 'input'}) + circ.plug(abv['wg10'], {'signal1': 'left'}) # "signal2" just gets a single of equivalent length - circ.plug(wg28, {'signal2': 'left'}) + circ.plug(abv['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'}) + circ.plug(abv['bend0'], {'signal1': 'right'}) + circ.plug(abv['bend0'], {'signal2': 'left'}) + circ.plug(abv['wg05'], {'signal1': 'left'}) + circ.plug(abv['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. @@ -284,23 +303,37 @@ def main(interactive: bool = True): # 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'}) + circ.plug(abv['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() + circ.plug(abv['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. - dev2pat(circ) + pat2dev(circ.pattern) - # Write out to GDS - writefile(circ.pattern, 'circuit.gds', **GDS_OPTS) + # Add the pattern into our library + lib['my_circuit'] = circ.pattern + + # Check if we forgot to include any patterns... ooops! + if dangling := lib.dangling_refs(): + print('Warning: The following patterns are referenced, but not present in the' + f'library! {dangling}') + print('We\'ll solve this by merging in shape_lib, which contains those shapes...') + + lib.add(shape_lib) + assert not lib.dangling_refs() + + # 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(lib) + + #Write out to GDS, only keeping patterns referenced by our circuit (including itself) + 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) if __name__ == '__main__': diff --git a/examples/tutorial/library.py b/examples/tutorial/library.py index 2dc2af6..25c8851 100644 --- a/examples/tutorial/library.py +++ b/examples/tutorial/library.py @@ -4,8 +4,7 @@ from pprint import pformat import numpy from numpy import pi -from masque.builder import Device -from masque.library import Library, LibDeviceLibrary +from masque import Pattern, Builder, WrapLibrary, LazyLibrary, Library from masque.file.gdsii import writefile, load_libraryfile import pcgen @@ -16,66 +15,62 @@ 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() + # Define a `LazyLibrary`, which provides lazy evaluation for generating + # patterns and lazy-loading of GDS contents. + lib = LazyLibrary() # # 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') + gds_lib, _properties = load_libraryfile('circuit.gds', postprocess=pat2dev) # 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=pat2dev) - - print('Devices loaded from GDS into library:\n' + pformat(list(device_lib.keys()))) + lib.add(gds_lib) + print('Patterns loaded from GDS into library:\n' + pformat(list(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: 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=dev2pat) + lib['triangle'] = lambda: basic_shapes.triangle(devices.RADIUS) + opts = dict( + lattice_constant = devices.LATTICE_CONSTANT, + hole = 'triangle', + ) # 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))) - + lib['tri_wg10'] = lambda: devices.waveguide(length=10, mirror_periods=5, **opts) + lib['tri_wg05'] = lambda: devices.waveguide(length=5, mirror_periods=5, **opts) + lib['tri_wg28'] = lambda: devices.waveguide(length=28, mirror_periods=5, **opts) + lib['tri_bend0'] = lambda: devices.bend(mirror_periods=5, **opts) + lib['tri_ysplit'] = lambda: devices.y_splitter(mirror_periods=5, **opts) + lib['tri_l3cav'] = lambda: devices.perturbed_l3(xy_size=(4, 10), **opts, hole_lib=lib) # # 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') + circ2 = Builder(library=lib, ports='tri_l3cav') - 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'}) + print(lib['wg10'].ports) + circ2.plug(lib.abstract('wg10'), {'input': 'right'}) + + abstracts = lib.abstract_view() # Alternate way to get abstracts + circ2.plug(abstracts['wg10'], {'output': 'left'}) + circ2.plug(abstracts['tri_wg10'], {'input': 'right'}) + circ2.plug(abstracts['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) + # `lib['mixed_wg_cav'] = lambda: circ2.pattern` + lib.set_const('mixed_wg_cav', circ2.pattern) # @@ -83,29 +78,26 @@ def main() -> None: # # 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'}) + circ3 = Builder.interface(source=circ2) - device_lib.set_const(circ3) + # ... that lets us continue from where we left off. + circ3.plug(abstracts['tri_bend0'], {'input': 'right'}) + circ3.plug(abstracts['tri_bend0'], {'input': 'left'}, mirrored=(True, False)) # mirror since no tri y-symmetry + circ3.plug(abstracts['tri_bend0'], {'input': 'right'}) + circ3.plug(abstracts['bend0'], {'output': 'left'}) + circ3.plug(abstracts['bend0'], {'output': 'left'}) + circ3.plug(abstracts['bend0'], {'output': 'left'}) + circ3.plug(abstracts['tri_wg10'], {'input': 'right'}) + circ3.plug(abstracts['tri_wg28'], {'input': 'right'}) + circ3.plug(abstracts['tri_wg10'], {'input': 'right', 'output': 'left'}) + + lib.set_const('loop_segment', circ3.pattern) # # 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) + print('Writing library to file...') + writefile(lib, 'library.gds', **GDS_OPTS) if __name__ == '__main__': @@ -116,14 +108,14 @@ if __name__ == '__main__': #class prout: # def place( # self, -# other: Device, +# other: Pattern, # label_layer: layer_t = 'WATLAYER', # *, # port_map: Optional[Dict[str, Optional[str]]] = None, # **kwargs, # ) -> 'prout': # -# Device.place(self, other, port_map=port_map, **kwargs) +# Pattern.place(self, other, port_map=port_map, **kwargs) # name: Optional[str] # for name in other.ports: # if port_map: diff --git a/examples/tutorial/pcgen.py b/examples/tutorial/pcgen.py index aebcc3f..0856ff9 100644 --- a/examples/tutorial/pcgen.py +++ b/examples/tutorial/pcgen.py @@ -29,8 +29,11 @@ def triangular_lattice( Returns: `[[x0, y0], [x1, 1], ...]` denoting lattice sites. """ - sx, sy = numpy.meshgrid(numpy.arange(dims[0], dtype=float), - numpy.arange(dims[1], dtype=float), indexing='ij') + sx, sy = numpy.meshgrid( + numpy.arange(dims[0], dtype=float), + numpy.arange(dims[1], dtype=float), + indexing='ij', + ) sx[sy % 2 == 1] += 0.5 sy *= numpy.sqrt(3) / 2 diff --git a/masque/abstract.py b/masque/abstract.py index f4916c4..04ce88b 100644 --- a/masque/abstract.py +++ b/masque/abstract.py @@ -1,27 +1,38 @@ -from typing import Dict, Union, Optional -from typing import MutableMapping, TYPE_CHECKING +from typing import Dict +#from typing import Union, Optional, MutableMapping, TYPE_CHECKING import copy import logging -from .pattern import Pattern +#from .pattern import Pattern from .ports import PortList, Port -if TYPE_CHECKING: - from .builder import Builder, Tool - from .library import MutableLibrary +#if TYPE_CHECKING: +# from .builder import Builder, Tool +# from .library import MutableLibrary logger = logging.getLogger(__name__) +AA = TypeVar('AA', bound='Abstract') + + class Abstract(PortList): - __slots__ = ('name', 'ports') + __slots__ = ('name', '_ports') name: str """ Name of the pattern this device references """ - ports: Dict[str, Port] - """ Uniquely-named ports which can be used to snap instances together""" + _ports: Dict[str, Port] + """ Uniquely-named ports which can be used to instances together""" + + @property + def ports(self) -> Dict[str, Port]: + return self._ports + + @ports.setter + def ports(self, value: Dict[str, Port]) -> None: + self._ports = value def __init__( self, @@ -31,22 +42,22 @@ class Abstract(PortList): self.name = name self.ports = copy.deepcopy(ports) - def build( - self, - library: 'MutableLibrary', - tools: Union[None, 'Tool', MutableMapping[Optional[str], 'Tool']] = None, - ) -> 'Builder': - """ - Begin building a new device around an instance of the current device - (rather than modifying the current device). - - Returns: - The new `Builder` object. - """ - pat = Pattern(ports=self.ports) - pat.ref(self.name) - new = Builder(library=library, pattern=pat, tools=tools) # TODO should Abstract have tools? - return new +# def build( +# self, +# library: 'MutableLibrary', +# tools: Union[None, 'Tool', MutableMapping[Optional[str], 'Tool']] = None, +# ) -> 'Builder': +# """ +# Begin building a new device around an instance of the current device +# (rather than modifying the current device). +# +# Returns: +# The new `Builder` object. +# """ +# pat = Pattern(ports=self.ports) +# pat.ref(self.name) +# new = Builder(library=library, pattern=pat, tools=tools) # TODO should Abstract have tools? +# return new # TODO do we want to store a Ref instead of just a name? then we can translate/rotate/mirror... @@ -56,3 +67,164 @@ class Abstract(PortList): s += f'\n\t{name}: {port}' s += ']>' return s + + def translate_ports(self: AA, offset: ArrayLike) -> AA: + """ + Translates all ports by the given offset. + + Args: + offset: (x, y) to translate by + + Returns: + self + """ + for port in self.ports.values(): + port.translate(offset) + return self + + def scale_by(self: AA, c: float) -> AA: + """ + Scale this Abstract by the given value + (all port offsets are scaled) + + Args: + c: factor to scale by + + Returns: + self + """ + for port in self.ports.values(): + port.offset *= c + return self + + def rotate_around(self: AA, pivot: ArrayLike, rotation: float) -> AA: + """ + Rotate the Abstract around the a location. + + Args: + pivot: (x, y) location to rotate around + rotation: Angle to rotate by (counter-clockwise, radians) + + Returns: + self + """ + pivot = numpy.array(pivot) + self.translate_ports(-pivot) + self.rotate_ports(rotation) + self.rotate_port_offsets(rotation) + self.translate_ports(+pivot) + return self + + def rotate_port_offsets(self: AA, rotation: float) -> AA: + """ + Rotate the offsets of all ports around (0, 0) + + Args: + rotation: Angle to rotate by (counter-clockwise, radians) + + Returns: + self + """ + for port in self.ports.values(): + port.offset = rotation_matrix_2d(rotation) @ port.offset + return self + + def rotate_ports(self: AA, rotation: float) -> AA: + """ + Rotate each port around its offset (i.e. in place) + + Args: + rotation: Angle to rotate by (counter-clockwise, radians) + + Returns: + self + """ + for port in self.ports.values(): + port.rotate(rotation) + return self + + def mirror_port_offsets(self: AA, across_axis: int) -> AA: + """ + Mirror the offsets of all shapes, labels, and refs across an axis + + Args: + across_axis: Axis to mirror across + (0: mirror across x axis, 1: mirror across y axis) + + Returns: + self + """ + for port in self.ports.values(): + port.offset[across_axis - 1] *= -1 + return self + + def mirror_ports(self: AA, across_axis: int) -> AA: + """ + Mirror each port's rotation across an axis, relative to its + offset + + Args: + across_axis: Axis to mirror across + (0: mirror across x axis, 1: mirror across y axis) + + Returns: + self + """ + for port in self.ports.values(): + port.mirror(across_axis) + return self + + def mirror(self: AA, across_axis: int) -> AA: + """ + Mirror the Pattern across an axis + + Args: + axis: Axis to mirror across + (0: mirror across x axis, 1: mirror across y axis) + + Returns: + self + """ + self.mirror_ports(across_axis) + self.mirror_port_offsets(across_axis) + return self + + def apply_ref_transform(self: AA, ref: Ref) -> AA: + """ + Apply the transform from a `Ref` to the ports of this `Abstract`. + This changes the port locations to where they would be in the Ref's parent pattern. + + Args: + ref: The ref whose transform should be applied. + + Returns: + self + """ + mirror_across_x, angle = normalize_mirror(ref.mirrored) + if mirrored_across_x: + self.mirror(across_axis=0) + self.rotate_ports(angle + ref.rotation) + self.rotate_port_offsets(angle + ref.rotation) + self.translate_ports(ref.offset) + return self + + def undo_ref_transform(self: AA, ref: Ref) -> AA: + """ + Apply the inverse transform from a `Ref` to the ports of this `Abstract`. + This changes the port locations to where they would be in the Ref's target (from the parent). + + Args: + ref: The ref whose (inverse) transform should be applied. + + Returns: + self + + # TODO test undo_ref_transform + """ + mirror_across_x, angle = normalize_mirror(ref.mirrored) + self.translate_ports(-ref.offset) + self.rotate_port_offsets(-angle - ref.rotation) + self.rotate_ports(-angle - ref.rotation) + if mirrored_across_x: + self.mirror(across_axis=0) + return self diff --git a/masque/builder/builder.py b/masque/builder/builder.py index 9bf2c48..e0b6c8f 100644 --- a/masque/builder/builder.py +++ b/masque/builder/builder.py @@ -97,19 +97,30 @@ class Builder(PortList): _dead: bool """ If True, plug()/place() are skipped (for debugging)""" + @property + def ports(self) -> Dict[str, Port]: + return self.pattern.ports + + @ports.setter + def ports(self, value: Dict[str, Port]) -> None: + self.pattern.ports = value + def __init__( self, library: MutableLibrary, *, pattern: Optional[Pattern] = None, - ports: Optional[Mapping[str, Port]] = None, + ports: Union[None, str, Mapping[str, Port]] = None, tools: Union[None, Tool, MutableMapping[Optional[str], Tool]] = None, ) -> None: """ - If `ports` is `None`, two default ports ('A' and 'B') are created. - Both are placed at (0, 0) and have default `ptype`, but 'A' has rotation 0 - (attached devices will be placed to the left) and 'B' has rotation - pi (attached devices will be placed to the right). + # TODO documentation for Builder() constructor + + # TODO MOVE THE BELOW DOCS to PortList + # If `ports` is `None`, two default ports ('A' and 'B') are created. + # Both are placed at (0, 0) and have default `ptype`, but 'A' has rotation 0 + # (attached devices will be placed to the left) and 'B' has rotation + # pi (attached devices will be placed to the right). """ self.library = library if pattern is not None: @@ -120,7 +131,10 @@ class Builder(PortList): if ports is not None: if self.pattern.ports: raise BuildError('Ports supplied for pattern with pre-existing ports!') - self.pattern.ports.update(copy.deepcopy(ports)) + if isinstance(ports, str): + ports = library.abstract(ports).ports + + self.pattern.ports.update(copy.deepcopy(dict(ports))) if tools is None: self.tools = {} @@ -509,7 +523,7 @@ class Builder(PortList): in_ptype = self.pattern[portspec].ptype pat = tool.path(ccw, length, in_ptype=in_ptype, port_names=tool_port_names, **kwargs) name = self.library.get_name(base_name) - self.library._set(name, pat) + self.library.set_const(name, pat) return self.plug(Abstract(name, pat.ports), {portspec: tool_port_names[0]}) def path_to( @@ -592,7 +606,7 @@ class Builder(PortList): for port_name, length in extensions.items(): bld.path(port_name, ccw, length, tool_port_names=tool_port_names) name = self.library.get_name(base_name) - self.library._set(name, bld.pattern) + self.library.set_const(name, bld.pattern) return self.plug(Abstract(name, bld.pattern.ports), {sp: 'in_' + sp for sp in ports.keys()}) # TODO safe to use 'in_'? # TODO def path_join() and def bus_join()? diff --git a/masque/builder/port_utils.py b/masque/builder/port_utils.py index 522caa7..75ad2b7 100644 --- a/masque/builder/port_utils.py +++ b/masque/builder/port_utils.py @@ -5,10 +5,11 @@ Functions for writing port data into a Pattern (`dev2pat`) and retrieving it (`p the port locations. This particular approach is just a sensible default; feel free to to write equivalent functions for your own format or alternate storage methods. """ -from typing import Sequence, Optional, Mapping +from typing import Sequence, Optional, Mapping, Tuple, Dict import logging import numpy +from numpy.typing import NDArray from ..pattern import Pattern from ..label import Label @@ -50,13 +51,16 @@ def dev2pat(pattern: Pattern, layer: layer_t) -> Pattern: def pat2dev( - library: Mapping[str, Pattern], - top: str, layers: Sequence[layer_t], + library: Mapping[str, Pattern], + pattern: Pattern, + name: Optional[str] = None, max_depth: int = 999_999, skip_subcells: bool = True, ) -> Pattern: """ + # TODO fixup documentation in port_utils + # TODO move port_utils to utils.file? Examine `pattern` for labels specifying port info, and use that info to fill out its `ports` attribute. @@ -64,8 +68,8 @@ def pat2dev( 'name:ptype angle_deg' Args: - pattern: Pattern object to scan for labels. layers: Search for labels on all the given layers. + pattern: Pattern object to scan for labels. max_depth: Maximum hierarcy depth to search. Default 999_999. Reduce this to 0 to avoid ever searching subcells. skip_subcells: If port labels are found at a given hierarcy level, @@ -73,52 +77,51 @@ def pat2dev( to contain their own port info without interfering with supercells' port data. Default True. + blacklist: If a cell name appears in the blacklist, do not ea Returns: The updated `pattern`. Port labels are not removed. """ + print(f'TODO pat2dev {name}') + if pattern.ports: + logger.warning(f'Pattern {name if name else pattern} already had ports, skipping pat2dev') + return pattern + if not isinstance(library, Library): library = WrapROLibrary(library) - ports = {} - annotated_cells = set() + pat2dev_flat(layers, pattern, name) + if (skip_subcells and pattern.ports) or max_depth == 0: + return pattern - def find_ports_each(pat, hierarchy, transform, memo) -> Pattern: - if len(hierarchy) > max_depth: - if max_depth >= 999_999: - logger.warning(f'pat2dev reached max depth ({max_depth})') - return pat + # Load ports for all subpatterns + for target in set(rr.target for rr in pat.refs): + pp = pat2dev( + layers=layers, + library=library, + pattern=library[target], + name=target, + max_depth=max_depth-1, + skip_subcells=skip_subcells, + blacklist=blacklist + {name}, + ) + found_ports |= bool(pp.ports) - if skip_subcells and any(parent in annotated_cells for parent in hierarchy): - return pat + for ref in pat.refs: + aa = library.abstract(ref.target) + if not aa.ports: + continue - cell_name = hierarchy[-1] - pat2dev_flat(pat, cell_name) + aa.apply_ref_transform(ref) - if skip_subcells: - annotated_cells.add(cell_name) - - mirr_factor = numpy.array((1, -1)) ** transform[3] - rot_matrix = rotation_matrix_2d(transform[2]) - for name, port in pat.ports.items(): - port.offset = transform[:2] + rot_matrix @ (port.offset * mirr_factor) - port.rotation = port.rotation * mirr_factor[0] * mirr_factor[1] + transform[2] - ports[name] = port - - return pat - - # update `ports` - library.dfs(top=top, visit_before=find_ports_each, transform=True) - - pattern = library[top] - pattern.check_ports(other_names=ports.keys()) - pattern.ports.update(ports) + pattern.check_ports(other_names=aa.ports.keys()) + pattern.ports.update(aa.ports) return pattern def pat2dev_flat( - pattern: Pattern, layers: Sequence[layer_t], + pattern: Pattern, cell_name: Optional[str] = None, ) -> Pattern: """ @@ -131,8 +134,8 @@ def pat2dev_flat( The pattern is assumed to be flat (have no `refs`) and have no pre-existing ports. Args: - pattern: Pattern object to scan for labels. layers: Search for labels on all the given layers. + pattern: Pattern object to scan for labels. cell_name: optional, used for warning message only Returns: diff --git a/masque/builder/utils.py b/masque/builder/utils.py index 949782a..d04caff 100644 --- a/masque/builder/utils.py +++ b/masque/builder/utils.py @@ -10,11 +10,11 @@ from ..utils import rotation_matrix_2d from ..error import BuildError if TYPE_CHECKING: - from ..ports import Port, PortList + from ..ports import Port def ell( - ports: Union[Mapping[str, 'Port'], 'PortList'], + ports: Mapping[str, 'Port'], ccw: Optional[bool], bound_type: str, bound: Union[float, ArrayLike], @@ -83,9 +83,6 @@ def ell( if not ports: raise BuildError('Empty port list passed to `ell()`') - if isinstance(ports, PortList): - ports = PortList.ports - if ccw is None: if spacing is not None and not numpy.isclose(spacing, 0): raise BuildError('Spacing must be 0 or None when ccw=None') diff --git a/masque/file/dxf.py b/masque/file/dxf.py index b70d712..16b9af4 100644 --- a/masque/file/dxf.py +++ b/masque/file/dxf.py @@ -6,11 +6,8 @@ Notes: * ezdxf sets creation time, write time, $VERSIONGUID, and $FINGERPRINTGUID to unique values, so byte-for-byte reproducibility is not achievable for now """ -from typing import List, Any, Dict, Tuple, Callable, Union, Iterable, Mapping, TextIO -import re +from typing import List, Any, Dict, Tuple, Callable, Union, Mapping, TextIO import io -import base64 -import struct import logging import pathlib import gzip @@ -60,7 +57,7 @@ def write( Other functions you may want to call: - `masque.file.oasis.check_valid_names(library.keys())` to check for invalid names - - `library.dangling_references()` to check for references to missing patterns + - `library.dangling_refs()` to check for references to missing patterns - `pattern.polygonize()` for any patterns with shapes other than `masque.shapes.Polygon` or `masque.shapes.Path` diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 9450498..aff2da6 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -19,8 +19,8 @@ Notes: * GDS creation/modification/access times are set to 1900-01-01 for reproducibility. * Gzip modification time is set to 0 (start of current epoch, usually 1970-01-01) """ -from typing import List, Any, Dict, Tuple, Callable, Union, Iterable -from typing import BinaryIO, Mapping, cast +from typing import List, Dict, Tuple, Callable, Union, Iterable, Mapping +from typing import BinaryIO, cast, Optional, Any import io import mmap import logging @@ -39,7 +39,7 @@ from .. import Pattern, Ref, PatternError, LibraryError, Label, Shape from ..shapes import Polygon, Path from ..repetition import Grid from ..utils import layer_t, normalize_mirror, annotations_t -from ..library import LazyLibrary, WrapLibrary, MutableLibrary +from ..library import LazyLibrary, WrapLibrary, MutableLibrary, Library logger = logging.getLogger(__name__) @@ -84,7 +84,7 @@ def write( Other functions you may want to call: - `masque.file.gdsii.check_valid_names(library.keys())` to check for invalid names - - `library.dangling_references()` to check for references to missing patterns + - `library.dangling_refs()` to check for references to missing patterns - `pattern.polygonize()` for any patterns with shapes other than `masque.shapes.Polygon` or `masque.shapes.Path` @@ -188,6 +188,7 @@ def read( raw_mode: bool = True, ) -> Tuple[Dict[str, Pattern], Dict[str, Any]]: """ + # TODO check GDSII file for cycles! Read a gdsii file and translate it into a dict of Pattern objects. GDSII structures are translated into Pattern objects; boundaries are translated into polygons, and srefs and arefs are translated into Ref objects. @@ -511,6 +512,7 @@ def load_library( stream: BinaryIO, *, full_load: bool = False, + postprocess: Optional[Callable[[Library, str, Pattern], Pattern]] = None ) -> Tuple[LazyLibrary, Dict[str, Any]]: """ Scan a GDSII stream to determine what structures are present, and create @@ -526,6 +528,8 @@ def load_library( full_load: If True, force all structures to be read immediately rather than as-needed. Since data is read sequentially from the file, this will be faster than using the resulting library's `precache` method. + postprocess: If given, this function is used to post-process each + pattern *upon first load only*. Returns: LazyLibrary object, allowing for deferred load of structures. @@ -548,9 +552,14 @@ def load_library( for name_bytes, pos in structs.items(): name = name_bytes.decode('ASCII') - def mkstruct(pos: int = pos) -> Pattern: + def mkstruct(pos: int = pos, name: str = name) -> Pattern: + logger.error(f'mkstruct {name} @ {pos:x}') stream.seek(pos) - return read_elements(stream, raw_mode=True) + pat = read_elements(stream, raw_mode=True) + if postprocess is not None: + pat = postprocess(lib, name, pat) + logger.error(f'mkstruct post {name} @ {pos:x}') + return pat lib[name] = mkstruct @@ -562,6 +571,7 @@ def load_libraryfile( *, use_mmap: bool = True, full_load: bool = False, + postprocess: Optional[Callable[[Library, str], Pattern]] = None ) -> Tuple[LazyLibrary, Dict[str, Any]]: """ Wrapper for `load_library()` that takes a filename or path instead of a stream. @@ -578,6 +588,7 @@ def load_libraryfile( is decompressed into a python `bytes` object in memory and reopened as an `io.BytesIO` stream. full_load: If `True`, immediately loads all data. See `load_library`. + postprocess: Passed to `load_library` Returns: LazyLibrary object, allowing for deferred load of structures. @@ -599,7 +610,7 @@ def load_libraryfile( stream = mmap.mmap(base_stream.fileno(), 0, access=mmap.ACCESS_READ) # type: ignore else: stream = open(path, mode='rb') - return load_library(stream, full_load=full_load) + return load_library(stream, full_load=full_load, postprocess=postprocess) def check_valid_names( diff --git a/masque/file/oasis.py b/masque/file/oasis.py index f7c9078..fbce662 100644 --- a/masque/file/oasis.py +++ b/masque/file/oasis.py @@ -78,7 +78,7 @@ def build( Other functions you may want to call: - `masque.file.oasis.check_valid_names(library.keys())` to check for invalid names - - `library.dangling_references()` to check for references to missing patterns + - `library.dangling_refs()` to check for references to missing patterns - `pattern.polygonize()` for any patterns with shapes other than `masque.shapes.Polygon`, `masque.shapes.Path`, or `masque.shapes.Circle` diff --git a/masque/library.py b/masque/library.py index 06f0673..b9ac8de 100644 --- a/masque/library.py +++ b/masque/library.py @@ -63,7 +63,7 @@ class Library(Mapping[str, 'Pattern'], metaclass=ABCMeta): def __repr__(self) -> str: return '' - def dangling_references( + def dangling_refs( self, tops: Union[None, str, Sequence[str]] = None, ) -> Set[Optional[str]]: @@ -304,11 +304,11 @@ class Library(Mapping[str, 'Pattern'], metaclass=ABCMeta): def dfs( self: L, - top: str, + pattern: 'Pattern', visit_before: Optional[visitor_function_t] = None, visit_after: Optional[visitor_function_t] = None, *, - hierarchy: Tuple[str, ...] = (), + hierarchy: Tuple[Optional[str], ...] = (None,), transform: Union[ArrayLike, bool, None] = False, memo: Optional[Dict] = None, ) -> L: @@ -317,23 +317,23 @@ class Library(Mapping[str, 'Pattern'], metaclass=ABCMeta): Performs a depth-first traversal of a pattern and its referenced patterns. At each pattern in the tree, the following sequence is called: ``` - hierarchy += (top,) current_pattern = visit_before(current_pattern, **vist_args) for sp in current_pattern.refs] self.dfs(sp.target, visit_before, visit_after, - hierarchy, updated_transform, memo) + hierarchy + (sp.target,), updated_transform, memo) current_pattern = visit_after(current_pattern, **visit_args) ``` where `visit_args` are - `hierarchy`: (top_pattern, L1_pattern, L2_pattern, ..., parent_pattern, current_pattern) - tuple of all parent-and-higher pattern names + `hierarchy`: (top_pattern_or_None, L1_pattern, L2_pattern, ..., parent_pattern) + tuple of all parent-and-higher pattern names. Top pattern name may be + `None` if not provided in first call to .dfs() `transform`: numpy.ndarray containing cumulative [x_offset, y_offset, rotation (rad), mirror_x (0 or 1)] for the instance being visited `memo`: Arbitrary dict (not altered except by `visit_before()` and `visit_after()`) Args: - top: Name of the pattern to start at (root node of the tree). + pattern: Pattern object to start at ("top"/root node of the tree). visit_before: Function to call before traversing refs. Should accept a `Pattern` and `**visit_args`, and return the (possibly modified) pattern. Default `None` (not called). @@ -345,8 +345,8 @@ class Library(Mapping[str, 'Pattern'], metaclass=ABCMeta): `True` or `None` is interpreted as `[0, 0, 0, 0]`. memo: Arbitrary dict for use by `visit_*()` functions. Default `None` (empty dict). hierarchy: Tuple of patterns specifying the hierarchy above the current pattern. - Appended to the start of the generated `visit_args['hierarchy']`. - Default is an empty tuple. + Default is (None,), which will be used as a placeholder for the top pattern's + name if not overridden. Returns: self @@ -359,16 +359,12 @@ class Library(Mapping[str, 'Pattern'], metaclass=ABCMeta): elif transform is not False: transform = numpy.array(transform) - if top in hierarchy: - raise LibraryError('.dfs() called on pattern with circular reference') + original_pattern = pattern - hierarchy += (top,) - - pat = self[top] if visit_before is not None: - pat = visit_before(pat, hierarchy=hierarchy, memo=memo, transform=transform) + pattern = visit_before(pattern, hierarchy=hierarchy, memo=memo, transform=transform) - for ref in pat.refs: + for ref in pattern.refs: if transform is not False: sign = numpy.ones(2) if transform[3]: @@ -376,32 +372,38 @@ class Library(Mapping[str, 'Pattern'], metaclass=ABCMeta): xy = numpy.dot(rotation_matrix_2d(transform[2]), ref.offset * sign) mirror_x, angle = normalize_mirror(ref.mirrored) angle += ref.rotation - sp_transform = transform + (xy[0], xy[1], angle, mirror_x) - sp_transform[3] %= 2 + ref_transform = transform + (xy[0], xy[1], angle, mirror_x) + ref_transform[3] %= 2 else: - sp_transform = False + ref_transform = False if ref.target is None: continue + if ref.target in hierarchy: + raise LibraryError(f'.dfs() called on pattern with circular reference to "{ref.target}"') self.dfs( - top=ref.target, + pattern=self[ref.target], visit_before=visit_before, visit_after=visit_after, - transform=sp_transform, + hierarchy=hierarchy + (ref.target,), + transform=ref_transform, memo=memo, - hierarchy=hierarchy, ) if visit_after is not None: - pat = visit_after(pat, hierarchy=hierarchy, memo=memo, transform=transform) + pattern = visit_after(pattern, hierarchy=hierarchy, memo=memo, transform=transform) - if self[top] is not pat: - if isinstance(self, MutableLibrary): - self._set(top, pat) - else: + if pattern is not original_pattern: + name = hierarchy[-1] + if not isintance(self, MutableLibrary): raise LibraryError('visit_* functions returned a new `Pattern` object' ' but the library is immutable') + if name is None: + raise LibraryError('visit_* functions returned a new `Pattern` object' + ' but no top-level name was provided in `hierarchy`') + + self.set_const(name, pattern) return self @@ -424,7 +426,7 @@ class MutableLibrary(Library, Generic[VVV], metaclass=ABCMeta): pass @abstractmethod - def _set(self, key: str, value: 'Pattern') -> None: + def set_const(self, key: str, value: 'Pattern') -> None: pass @abstractmethod @@ -564,7 +566,7 @@ class MutableLibrary(Library, Generic[VVV], metaclass=ABCMeta): del pat.shapes[i] for ll, pp in shape_pats.items(): - self._set(label2name(ll), pp) + self.set_const(label2name(ll), pp) return self @@ -599,7 +601,7 @@ class MutableLibrary(Library, Generic[VVV], metaclass=ABCMeta): continue name = name_func(pat, shape) - self._set(name, Pattern(shapes=[shape])) + self.set_const(name, Pattern(shapes=[shape])) pat.ref(name, repetition=shape.repetition) shape.repetition = None pat.shapes = new_shapes @@ -610,7 +612,7 @@ class MutableLibrary(Library, Generic[VVV], metaclass=ABCMeta): new_labels.append(label) continue name = name_func(pat, label) - self._set(name, Pattern(labels=[label])) + self.set_const(name, Pattern(labels=[label])) pat.ref(name, repetition=label.repetition) label.repetition = None pat.labels = new_labels @@ -692,7 +694,7 @@ class WrapLibrary(MutableLibrary): def __delitem__(self, key: str) -> None: del self.mapping[key] - def _set(self, key: str, value: 'Pattern') -> None: + def set_const(self, key: str, value: 'Pattern') -> None: self[key] = value def _merge(self, other: Mapping[str, 'Pattern'], key: str) -> None: @@ -744,7 +746,7 @@ class LazyLibrary(MutableLibrary): def __len__(self) -> int: return len(self.dict) - def _set(self, key: str, value: 'Pattern') -> None: + def set_const(self, key: str, value: 'Pattern') -> None: self[key] = lambda: value def _merge(self, other: Mapping[str, 'Pattern'], key: str) -> None: @@ -753,7 +755,7 @@ class LazyLibrary(MutableLibrary): if key in other.cache: self.cache[key] = other.cache[key] else: - self._set(key, other[key]) + self.set_const(key, other[key]) def __repr__(self) -> str: return '' diff --git a/masque/pattern.py b/masque/pattern.py index 6e29586..c557c06 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -30,9 +30,9 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots): (via Ref). Shapes are assumed to inherit from masque.shapes.Shape or provide equivalent functions. """ __slots__ = ( - 'shapes', 'labels', 'refs', 'ports', + 'shapes', 'labels', 'refs', '_ports', # inherited - '_offset', '_annotations' + '_offset', '_annotations', ) shapes: List[Shape] @@ -49,9 +49,17 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots): (i.e. multiple instances of the same object). """ - ports: Dict[str, Port] + _ports: Dict[str, Port] """ Uniquely-named ports which can be used to snap to other Pattern instances""" + @property + def ports(self) -> Dict[str, Port]: + return self._ports + + @ports.setter + def ports(self, value: Dict[str, Port]) -> None: + self._ports = value + def __init__( self, *, @@ -331,7 +339,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots): def translate_elements(self: P, offset: ArrayLike) -> P: """ - Translates all shapes, label, and refs by the given offset. + Translates all shapes, label, refs, and ports by the given offset. Args: offset: (x, y) to translate by @@ -339,7 +347,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots): Returns: self """ - for entry in chain(self.shapes, self.refs, self.labels, self.ports): + for entry in chain(self.shapes, self.refs, self.labels, self.ports.values()): cast(Positionable, entry).translate(offset) return self @@ -360,7 +368,8 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots): def scale_by(self: P, c: float) -> P: """ Scale this Pattern by the given value - (all shapes and refs and their offsets are scaled) + (all shapes and refs and their offsets are scaled, + as are all label and port offsets) Args: c: factor to scale by @@ -407,7 +416,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots): def rotate_element_centers(self: P, rotation: float) -> P: """ - Rotate the offsets of all shapes, labels, and refs around (0, 0) + Rotate the offsets of all shapes, labels, refs, and ports around (0, 0) Args: rotation: Angle to rotate by (counter-clockwise, radians) @@ -415,14 +424,14 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots): Returns: self """ - for entry in chain(self.shapes, self.refs, self.labels, self.ports): + for entry in chain(self.shapes, self.refs, self.labels, self.ports.values()): old_offset = cast(Positionable, entry).offset cast(Positionable, entry).offset = numpy.dot(rotation_matrix_2d(rotation), old_offset) return self def rotate_elements(self: P, rotation: float) -> P: """ - Rotate each shape and refs around its center (offset) + Rotate each shape, ref, and port around its origin (offset) Args: rotation: Angle to rotate by (counter-clockwise, radians) @@ -430,54 +439,54 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots): Returns: self """ - for entry in chain(self.shapes, self.refs): + for entry in chain(self.shapes, self.refs, self.ports.values()): cast(Rotatable, entry).rotate(rotation) return self - def mirror_element_centers(self: P, axis: int) -> P: + def mirror_element_centers(self: P, across_axis: int) -> P: """ Mirror the offsets of all shapes, labels, and refs across an axis Args: - axis: Axis to mirror across + across_axis: Axis to mirror across (0: mirror across x axis, 1: mirror across y axis) Returns: self """ - for entry in chain(self.shapes, self.refs, self.labels, self.ports): - cast(Positionable, entry).offset[axis - 1] *= -1 + for entry in chain(self.shapes, self.refs, self.labels, self.ports.values()): + cast(Positionable, entry).offset[across_axis - 1] *= -1 return self - def mirror_elements(self: P, axis: int) -> P: + def mirror_elements(self: P, across_axis: int) -> P: """ - Mirror each shape and refs across an axis, relative to its - offset + Mirror each shape, ref, and pattern across an axis, relative + to its offset Args: - axis: Axis to mirror across + across_axis: Axis to mirror across (0: mirror across x axis, 1: mirror across y axis) Returns: self """ - for entry in chain(self.shapes, self.refs): - cast(Mirrorable, entry).mirror(axis) + for entry in chain(self.shapes, self.refs, self.ports.values()): + cast(Mirrorable, entry).mirror(across_axis) return self - def mirror(self: P, axis: int) -> P: + def mirror(self: P, across_axis: int) -> P: """ Mirror the Pattern across an axis Args: - axis: Axis to mirror across + across_axis: Axis to mirror across (0: mirror across x axis, 1: mirror across y axis) Returns: self """ - self.mirror_elements(axis) - self.mirror_element_centers(axis) + self.mirror_elements(across_axis) + self.mirror_element_centers(across_axis) return self def copy(self: P) -> P: diff --git a/masque/ports.py b/masque/ports.py index a981457..000c866 100644 --- a/masque/ports.py +++ b/masque/ports.py @@ -4,7 +4,7 @@ import warnings import traceback import logging from collections import Counter -from abc import ABCMeta +from abc import ABCMeta, abstractmethod import numpy from numpy import pi @@ -103,10 +103,18 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable, met class PortList(metaclass=ABCMeta): - __slots__ = () # For use with AutoSlots + __slots__ = () # Allow subclasses to use __slots__ - ports: Dict[str, Port] - """ Uniquely-named ports which can be used to snap to other Device instances""" + @property + @abstractmethod + def ports(self) -> Dict[str, Port]: + """ Uniquely-named ports which can be used to snap to other Device instances""" + pass + + @ports.setter + @abstractmethod + def ports(self, value: Dict[str, Port]) -> None: + pass @overload def __getitem__(self, key: str) -> Port: