Lots of progress on tutorials

This commit is contained in:
Jan Petykiewicz 2023-01-24 23:25:10 -08:00
parent 34a5369a55
commit 22735125d5
16 changed files with 576 additions and 507 deletions

View File

@ -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()

View File

@ -0,0 +1 @@
TODO write tutorial readme

View File

@ -3,19 +3,19 @@ from typing import Tuple, Sequence
import numpy import numpy
from numpy import pi from numpy import pi
from masque import layer_t, Pattern, SubPattern, Label from masque import (
from masque.shapes import Circle, Arc, Polygon layer_t, Pattern, Label, Port,
from masque.builder import Device, Port Circle, Arc, Polygon,
from masque.library import Library, DeviceLibrary )
import masque.file.gdsii import masque.file.gdsii
# Note that masque units are arbitrary, and are only given # Note that masque units are arbitrary, and are only given
# physical significance when writing to a file. # physical significance when writing to a file.
GDS_OPTS = { GDS_OPTS = dict(
'meters_per_unit': 1e-9, # GDS database unit, 1 nanometer meters_per_unit = 1e-9, # GDS database unit, 1 nanometer
'logical_units_per_unit': 1e-3, # GDS display unit, 1 micron logical_units_per_unit = 1e-3, # GDS display unit, 1 micron
} )
def hole( def hole(
@ -30,10 +30,10 @@ def hole(
layer: Layer to draw the circle on. layer: Layer to draw the circle on.
Returns: Returns:
Pattern, named `'hole'` Pattern containing a circle.
""" """
pat = Pattern('hole', shapes=[ pat = Pattern(shapes=[
Circle(radius=radius, offset=(0, 0), layer=layer) Circle(radius=radius, offset=(0, 0), layer=layer),
]) ])
return pat return pat
@ -50,15 +50,15 @@ def triangle(
layer: Layer to draw the circle on. layer: Layer to draw the circle on.
Returns: Returns:
Pattern, named `'triangle'` Pattern containing a triangle
""" """
vertices = numpy.array([ vertices = numpy.array([
(numpy.cos( pi / 2), numpy.sin( pi / 2)), (numpy.cos( pi / 2), numpy.sin( pi / 2)),
(numpy.cos(pi + pi / 6), numpy.sin(pi + pi / 6)), (numpy.cos(pi + pi / 6), numpy.sin(pi + pi / 6)),
(numpy.cos( - pi / 6), numpy.sin( - 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), Polygon(offset=(0, 0), layer=layer, vertices=vertices),
]) ])
return pat return pat
@ -78,37 +78,38 @@ def smile(
secondary_layer: Layer to draw eyes and smile on. secondary_layer: Layer to draw eyes and smile on.
Returns: Returns:
Pattern, named `'smile'` Pattern containing a smiley face
""" """
# Make an empty pattern # Make an empty pattern
pat = Pattern('smile') pat = Pattern()
# Add all the shapes we want # Add all the shapes we want
pat.shapes += [ pat.shapes += [
Circle(radius=radius, offset=(0, 0), layer=layer), # Outer circle 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),
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 angles=(7 / 6 * pi, 11 / 6 * pi), # Angles limiting the arc
width=radius / 10, width=radius / 10,
offset=(0, 0), offset=(0, 0),
layer=secondary_layer), layer=secondary_layer,
),
] ]
return pat return pat
def main() -> None: def main() -> None:
hole_pat = hole(1000) lib = {}
smile_pat = smile(1000)
tri_pat = triangle(1000)
units_per_meter = 1e-9 lib['hole'] = hole(1000)
units_per_display_unit = 1e-3 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__': if __name__ == '__main__':

View File

@ -1,12 +1,15 @@
from typing import Tuple, Sequence, Dict # TODO update tutorials
from typing import Tuple, Sequence, Dict, Mapping
import numpy import numpy
from numpy import pi from numpy import pi
from masque import layer_t, Pattern, SubPattern, Label from masque import (
from masque.shapes import Polygon layer_t, Pattern, Ref, Label, Builder, Port, Polygon,
from masque.builder import Device, Port, port_utils WrapLibrary, Library,
from masque.file.gdsii import writefile )
from masque.builder import port_utils
from masque.file.gdsii import writefile, check_valid_names
import pcgen import pcgen
import basic_shapes import basic_shapes
@ -17,38 +20,41 @@ LATTICE_CONSTANT = 512
RADIUS = LATTICE_CONSTANT / 2 * 0.75 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 This places a label at each port location on layer (3, 0) with text content
'name:ptype angle_deg' 'name:ptype angle_deg'
""" """
return port_utils.dev2pat(dev, layer=(3, 0)) 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` 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( def perturbed_l3(
lattice_constant: float, lattice_constant: float,
hole: Pattern, hole: str,
hole_lib: Mapping[str, Pattern],
trench_layer: layer_t = (1, 0), trench_layer: layer_t = (1, 0),
shifts_a: Sequence[float] = (0.15, 0, 0.075), shifts_a: Sequence[float] = (0.15, 0, 0.075),
shifts_r: Sequence[float] = (1.0, 1.0, 1.0), shifts_r: Sequence[float] = (1.0, 1.0, 1.0),
xy_size: Tuple[int, int] = (10, 10), xy_size: Tuple[int, int] = (10, 10),
perturbed_radius: float = 1.1, perturbed_radius: float = 1.1,
trench_width: float = 1200, trench_width: float = 1200,
) -> Device: ) -> Pattern:
""" """
Generate a `Device` representing a perturbed L3 cavity. Generate a `Pattern` representing a perturbed L3 cavity.
Args: Args:
lattice_constant: Distance between nearest neighbor holes 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)`. trench_layer: Layer for the trenches, default `(1, 0)`.
shifts_a: passed to `pcgen.l3_shift`; specifies lattice constant shifts_a: passed to `pcgen.l3_shift`; specifies lattice constant
(1 - multiplicative factor) for shifting holes adjacent to (1 - multiplicative factor) for shifting holes adjacent to
@ -64,8 +70,10 @@ def perturbed_l3(
trench width: Width of the undercut trenches. Default 1200. trench width: Width of the undercut trenches. Default 1200.
Returns: Returns:
`Device` object representing the L3 design. `Pattern` object representing the L3 design.
""" """
print('Generating perturbed L3...')
# Get hole positions and radii # Get hole positions and radii
xyr = pcgen.l3_shift_perturbed_defect(mirror_dims=xy_size, xyr = pcgen.l3_shift_perturbed_defect(mirror_dims=xy_size,
perturbed_radius=perturbed_radius, perturbed_radius=perturbed_radius,
@ -73,15 +81,14 @@ def perturbed_l3(
shifts_r=shifts_r) shifts_r=shifts_r)
# Build L3 cavity, using references to the provided hole pattern # Build L3 cavity, using references to the provided hole pattern
pat = Pattern(f'L3p-a{lattice_constant:g}rp{perturbed_radius:g}') pat = Pattern()
pat.subpatterns += [ pat.refs += [
SubPattern(hole, scale=r, Ref(hole, scale=r, offset=(lattice_constant * x,
offset=(lattice_constant * x, lattice_constant * y))
lattice_constant * y))
for x, y, r in xyr] for x, y, r in xyr]
# Add rectangular undercut aids # 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] trench_dx = max_xy[0] - min_xy[0]
pat.shapes += [ pat.shapes += [
@ -91,168 +98,180 @@ def perturbed_l3(
# Ports are at outer extents of the device (with y=0) # Ports are at outer extents of the device (with y=0)
extent = lattice_constant * xy_size[0] extent = lattice_constant * xy_size[0]
ports = { pat.ports = dict(
'input': Port((-extent, 0), rotation=0, ptype='pcwg'), input=Port((-extent, 0), rotation=0, ptype='pcwg'),
'output': Port((extent, 0), rotation=pi, ptype='pcwg'), output=Port((extent, 0), rotation=pi, ptype='pcwg'),
} )
return Device(pat, ports) dev2pat(pat)
return pat
def waveguide( def waveguide(
lattice_constant: float, lattice_constant: float,
hole: Pattern, hole: str,
length: int, length: int,
mirror_periods: 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: Args:
lattice_constant: Distance between nearest neighbor holes 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. length: Distance (number of mirror periods) between the input and output ports.
Ports are placed at lattice sites. Ports are placed at lattice sites.
mirror_periods: Number of hole rows on each side of the line defect mirror_periods: Number of hole rows on each side of the line defect
Returns: Returns:
`Device` object representing the waveguide. `Pattern` object representing the waveguide.
""" """
# Generate hole locations # Generate hole locations
xy = pcgen.waveguide(length=length, num_mirror=mirror_periods) xy = pcgen.waveguide(length=length, num_mirror=mirror_periods)
# Build the pattern # Build the pattern
pat = Pattern(f'_wg-a{lattice_constant:g}l{length}') pat = Pattern()
pat.subpatterns += [SubPattern(hole, offset=(lattice_constant * x, pat.refs += [
lattice_constant * y)) Ref(hole, offset=(lattice_constant * x,
for x, y in xy] lattice_constant * y))
for x, y in xy]
# Ports are at outer edges, with y=0 # Ports are at outer edges, with y=0
extent = lattice_constant * length / 2 extent = lattice_constant * length / 2
ports = { pat.ports = dict(
'left': Port((-extent, 0), rotation=0, ptype='pcwg'), left=Port((-extent, 0), rotation=0, ptype='pcwg'),
'right': Port((extent, 0), rotation=pi, ptype='pcwg'), right=Port((extent, 0), rotation=pi, ptype='pcwg'),
} )
return Device(pat, ports) pat2dev(pat)
print(pat)
return pat
def bend( def bend(
lattice_constant: float, lattice_constant: float,
hole: Pattern, hole: str,
mirror_periods: int, 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. line-defect waveguide.
Args: Args:
lattice_constant: Distance between nearest neighbor holes 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. mirror_periods: Minimum number of mirror periods on each side of the line defect.
Returns: Returns:
`Device` object representing the waveguide bend. `Pattern` object representing the waveguide bend.
Ports are named 'left' (input) and 'right' (output). Ports are named 'left' (input) and 'right' (output).
""" """
# Generate hole locations # Generate hole locations
xy = pcgen.wgbend(num_mirror=mirror_periods) xy = pcgen.wgbend(num_mirror=mirror_periods)
# Build the pattern # Build the pattern
pat= Pattern(f'_wgbend-a{lattice_constant:g}l{mirror_periods}') pat= Pattern()
pat.subpatterns += [ pat.refs += [
SubPattern(hole, offset=(lattice_constant * x, Ref(hole, offset=(lattice_constant * x,
lattice_constant * y)) lattice_constant * y))
for x, y in xy] for x, y in xy]
# Figure out port locations. # Figure out port locations.
extent = lattice_constant * mirror_periods extent = lattice_constant * mirror_periods
ports = { pat.ports = dict(
'left': Port((-extent, 0), rotation=0, ptype='pcwg'), left=Port((-extent, 0), rotation=0, ptype='pcwg'),
'right': Port((extent / 2, right=Port((extent / 2,
extent * numpy.sqrt(3) / 2), extent * numpy.sqrt(3) / 2),
rotation=pi * 4 / 3, ptype='pcwg'), rotation=pi * 4 / 3, ptype='pcwg'),
} )
return Device(pat, ports) pat2dev(pat)
return pat
def y_splitter( def y_splitter(
lattice_constant: float, lattice_constant: float,
hole: Pattern, hole: str,
mirror_periods: int, 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: Args:
lattice_constant: Distance between nearest neighbor holes 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. mirror_periods: Minimum number of mirror periods on each side of the line defect.
Returns: Returns:
`Device` object representing the y-splitter. `Pattern` object representing the y-splitter.
Ports are named 'in', 'top', and 'bottom'. Ports are named 'in', 'top', and 'bottom'.
""" """
# Generate hole locations # Generate hole locations
xy = pcgen.y_splitter(num_mirror=mirror_periods) xy = pcgen.y_splitter(num_mirror=mirror_periods)
# Build pattern # Build pattern
pat = Pattern(f'_wgsplit_half-a{lattice_constant:g}l{mirror_periods}') pat = Pattern()
pat.subpatterns += [ pat.refs += [
SubPattern(hole, offset=(lattice_constant * x, Ref(hole, offset=(lattice_constant * x,
lattice_constant * y)) lattice_constant * y))
for x, y in xy] for x, y in xy]
# Determine port locations # Determine port locations
extent = lattice_constant * mirror_periods extent = lattice_constant * mirror_periods
ports = { pat.ports = {
'in': Port((-extent, 0), rotation=0, ptype='pcwg'), 'in': Port((-extent, 0), rotation=0, ptype='pcwg'),
'top': Port((extent / 2, extent * numpy.sqrt(3) / 2), rotation=pi * 4 / 3, 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'), '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 # Generate some basic hole patterns
smile = basic_shapes.smile(RADIUS) shape_lib = {
hole = basic_shapes.hole(RADIUS) 'smile': basic_shapes.smile(RADIUS),
'hole': basic_shapes.hole(RADIUS),
}
# Build some devices # Build some devices
a = LATTICE_CONSTANT 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 devices = {}
for device in [wg10, wg05, wg28, l3cav, ysplit, bend0]: devices['wg05'] = waveguide(lattice_constant=a, hole='hole', length=5, mirror_periods=5)
dev2pat(device) 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 # Build a circuit
# #
circ = Device(name='my_circuit', ports={}) circ = Builder(library=lib)
# Start by placing a waveguide. Call its ports "in" and "signal". # 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. # Extend the signal path by attaching the "left" port of a waveguide.
# Since there is only one other port ("right") on the waveguide we # Since there is only one other port ("right") on the waveguide we
# are attaching (wg10), it automatically inherits the name "signal". # 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. # Attach a y-splitter to the signal path.
# Since the y-splitter has 3 ports total, we can't auto-inherit the # 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 # port name, so we have to specify what we want to name the unattached
# ports. We can call them "signal1" and "signal2". # 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. # Add a waveguide to both signal ports, inheriting their names.
circ.plug(wg05, {'signal1': 'left'}) circ.plug(abv['wg05'], {'signal1': 'left'})
circ.plug(wg05, {'signal2': 'left'}) circ.plug(abv['wg05'], {'signal2': 'left'})
# Add a bend to both ports. # Add a bend to both ports.
# Our bend's ports "left" and "right" refer to the original counterclockwise # 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. # to "signal2" to bend counterclockwise.
# We could also use `mirrored=(True, False)` to mirror one of the devices # We could also use `mirrored=(True, False)` to mirror one of the devices
# and then use same device port on both paths. # and then use same device port on both paths.
circ.plug(bend0, {'signal1': 'right'}) circ.plug(abv['bend0'], {'signal1': 'right'})
circ.plug(bend0, {'signal2': 'left'}) circ.plug(abv['bend0'], {'signal2': 'left'})
# We add some waveguides and a cavity to "signal1". # We add some waveguides and a cavity to "signal1".
circ.plug(wg10, {'signal1': 'left'}) circ.plug(abv['wg10'], {'signal1': 'left'})
circ.plug(l3cav, {'signal1': 'input'}) circ.plug(abv['l3cav'], {'signal1': 'input'})
circ.plug(wg10, {'signal1': 'left'}) circ.plug(abv['wg10'], {'signal1': 'left'})
# "signal2" just gets a single of equivalent length # "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 # Now we bend both waveguides back towards each other
circ.plug(bend0, {'signal1': 'right'}) circ.plug(abv['bend0'], {'signal1': 'right'})
circ.plug(bend0, {'signal2': 'left'}) circ.plug(abv['bend0'], {'signal2': 'left'})
circ.plug(wg05, {'signal1': 'left'}) circ.plug(abv['wg05'], {'signal1': 'left'})
circ.plug(wg05, {'signal2': 'left'}) circ.plug(abv['wg05'], {'signal2': 'left'})
# To join the waveguides, we attach a second y-junction. # To join the waveguides, we attach a second y-junction.
# We plug "signal1" into the "bot" port, and "signal2" into the "top" port. # 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 # This operation would raise an exception if the ports did not line up
# correctly (i.e. they required different rotations or translations of the # correctly (i.e. they required different rotations or translations of the
# y-junction device). # 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". # Finally, add some more waveguide to "signal_out".
circ.plug(wg10, {'signal_out': 'left'}) circ.plug(abv['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. # We can also add text labels for our circuit's ports.
# They will appear at the uppermost hierarchy level, while the individual # They will appear at the uppermost hierarchy level, while the individual
# device ports will appear further down, in their respective cells. # device ports will appear further down, in their respective cells.
dev2pat(circ) pat2dev(circ.pattern)
# Write out to GDS # Add the pattern into our library
writefile(circ.pattern, 'circuit.gds', **GDS_OPTS) 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__': if __name__ == '__main__':

View File

@ -4,8 +4,7 @@ from pprint import pformat
import numpy import numpy
from numpy import pi from numpy import pi
from masque.builder import Device from masque import Pattern, Builder, WrapLibrary, LazyLibrary, Library
from masque.library import Library, LibDeviceLibrary
from masque.file.gdsii import writefile, load_libraryfile from masque.file.gdsii import writefile, load_libraryfile
import pcgen import pcgen
@ -16,66 +15,62 @@ from basic_shapes import GDS_OPTS
def main() -> None: def main() -> None:
# Define a `Library`-backed `DeviceLibrary`, which provides lazy evaluation # Define a `LazyLibrary`, which provides lazy evaluation for generating
# for device generation code and lazy-loading of GDS contents. # patterns and lazy-loading of GDS contents.
device_lib = LibDeviceLibrary() lib = LazyLibrary()
# #
# Load some devices from a GDS file # Load some devices from a GDS file
# #
# Scan circuit.gds and prepare to lazy-load its contents # 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 # Add it into the device library by providing a way to read port info
# This maintains the lazy evaluation from above, so no patterns # This maintains the lazy evaluation from above, so no patterns
# are actually read yet. # are actually read yet.
device_lib.add_library(pattern_lib, pat2dev=pat2dev) lib.add(gds_lib)
print('Devices loaded from GDS into library:\n' + pformat(list(device_lib.keys())))
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 # Add some new devices to the library, this time from python code rather than GDS
# #
a = devices.LATTICE_CONSTANT lib['triangle'] = lambda: basic_shapes.triangle(devices.RADIUS)
tri = basic_shapes.triangle(devices.RADIUS) opts = dict(
lattice_constant = devices.LATTICE_CONSTANT,
# Convenience function for adding devices hole = 'triangle',
# 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)
# Triangle-based variants. These are defined here, but they won't run until they're # Triangle-based variants. These are defined here, but they won't run until they're
# retrieved from the library. # retrieved from the library.
add('tri_wg10', lambda: devices.waveguide(lattice_constant=a, hole=tri, length=10, mirror_periods=5)) lib['tri_wg10'] = lambda: devices.waveguide(length=10, mirror_periods=5, **opts)
add('tri_wg05', lambda: devices.waveguide(lattice_constant=a, hole=tri, length=5, mirror_periods=5)) lib['tri_wg05'] = lambda: devices.waveguide(length=5, mirror_periods=5, **opts)
add('tri_wg28', lambda: devices.waveguide(lattice_constant=a, hole=tri, length=28, mirror_periods=5)) lib['tri_wg28'] = lambda: devices.waveguide(length=28, mirror_periods=5, **opts)
add('tri_bend0', lambda: devices.bend(lattice_constant=a, hole=tri, mirror_periods=5)) lib['tri_bend0'] = lambda: devices.bend(mirror_periods=5, **opts)
add('tri_ysplit', lambda: devices.y_splitter(lattice_constant=a, hole=tri, mirror_periods=5)) lib['tri_ysplit'] = lambda: devices.y_splitter(mirror_periods=5, **opts)
add('tri_l3cav', lambda: devices.perturbed_l3(lattice_constant=a, hole=tri, xy_size=(4, 10))) 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 # Build a mixed waveguide with an L3 cavity in the middle
# #
# Immediately start building from an instance of the L3 cavity # 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) print(lib['wg10'].ports)
circ2.plug(device_lib['wg10'], {'input': 'right'}) circ2.plug(lib.abstract('wg10'), {'input': 'right'})
circ2.plug(device_lib['wg10'], {'output': 'left'})
circ2.plug(device_lib['tri_wg10'], {'input': 'right'}) abstracts = lib.abstract_view() # Alternate way to get abstracts
circ2.plug(device_lib['tri_wg10'], {'output': 'left'}) 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. # Add the circuit to the device library.
# It has already been generated, so we can use `set_const` as a shorthand for # It has already been generated, so we can use `set_const` as a shorthand for
# `device_lib['mixed_wg_cav'] = lambda: circ2` # `lib['mixed_wg_cav'] = lambda: circ2.pattern`
device_lib.set_const(circ2) 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... # We'll be designing against an existing device's interface...
circ3 = circ2.as_interface('loop_segment') circ3 = Builder.interface(source=circ2)
# ... 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) # ... 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 # Write all devices into a GDS file
# #
print('Writing library to file...')
# This line could be slow, since it generates or loads many of the devices writefile(lib, 'library.gds', **GDS_OPTS)
# 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__': if __name__ == '__main__':
@ -116,14 +108,14 @@ if __name__ == '__main__':
#class prout: #class prout:
# def place( # def place(
# self, # self,
# other: Device, # other: Pattern,
# label_layer: layer_t = 'WATLAYER', # label_layer: layer_t = 'WATLAYER',
# *, # *,
# port_map: Optional[Dict[str, Optional[str]]] = None, # port_map: Optional[Dict[str, Optional[str]]] = None,
# **kwargs, # **kwargs,
# ) -> 'prout': # ) -> 'prout':
# #
# Device.place(self, other, port_map=port_map, **kwargs) # Pattern.place(self, other, port_map=port_map, **kwargs)
# name: Optional[str] # name: Optional[str]
# for name in other.ports: # for name in other.ports:
# if port_map: # if port_map:

View File

@ -29,8 +29,11 @@ def triangular_lattice(
Returns: Returns:
`[[x0, y0], [x1, 1], ...]` denoting lattice sites. `[[x0, y0], [x1, 1], ...]` denoting lattice sites.
""" """
sx, sy = numpy.meshgrid(numpy.arange(dims[0], dtype=float), sx, sy = numpy.meshgrid(
numpy.arange(dims[1], dtype=float), indexing='ij') numpy.arange(dims[0], dtype=float),
numpy.arange(dims[1], dtype=float),
indexing='ij',
)
sx[sy % 2 == 1] += 0.5 sx[sy % 2 == 1] += 0.5
sy *= numpy.sqrt(3) / 2 sy *= numpy.sqrt(3) / 2

View File

@ -1,27 +1,38 @@
from typing import Dict, Union, Optional from typing import Dict
from typing import MutableMapping, TYPE_CHECKING #from typing import Union, Optional, MutableMapping, TYPE_CHECKING
import copy import copy
import logging import logging
from .pattern import Pattern #from .pattern import Pattern
from .ports import PortList, Port from .ports import PortList, Port
if TYPE_CHECKING: #if TYPE_CHECKING:
from .builder import Builder, Tool # from .builder import Builder, Tool
from .library import MutableLibrary # from .library import MutableLibrary
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
AA = TypeVar('AA', bound='Abstract')
class Abstract(PortList): class Abstract(PortList):
__slots__ = ('name', 'ports') __slots__ = ('name', '_ports')
name: str name: str
""" Name of the pattern this device references """ """ Name of the pattern this device references """
ports: Dict[str, Port] _ports: Dict[str, Port]
""" Uniquely-named ports which can be used to snap instances together""" """ 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__( def __init__(
self, self,
@ -31,22 +42,22 @@ class Abstract(PortList):
self.name = name self.name = name
self.ports = copy.deepcopy(ports) self.ports = copy.deepcopy(ports)
def build( # def build(
self, # self,
library: 'MutableLibrary', # library: 'MutableLibrary',
tools: Union[None, 'Tool', MutableMapping[Optional[str], 'Tool']] = None, # tools: Union[None, 'Tool', MutableMapping[Optional[str], 'Tool']] = None,
) -> 'Builder': # ) -> 'Builder':
""" # """
Begin building a new device around an instance of the current device # Begin building a new device around an instance of the current device
(rather than modifying the current device). # (rather than modifying the current device).
#
Returns: # Returns:
The new `Builder` object. # The new `Builder` object.
""" # """
pat = Pattern(ports=self.ports) # pat = Pattern(ports=self.ports)
pat.ref(self.name) # pat.ref(self.name)
new = Builder(library=library, pattern=pat, tools=tools) # TODO should Abstract have tools? # new = Builder(library=library, pattern=pat, tools=tools) # TODO should Abstract have tools?
return new # return new
# TODO do we want to store a Ref instead of just a name? then we can translate/rotate/mirror... # 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 += f'\n\t{name}: {port}'
s += ']>' s += ']>'
return 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

View File

@ -97,19 +97,30 @@ class Builder(PortList):
_dead: bool _dead: bool
""" If True, plug()/place() are skipped (for debugging)""" """ 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__( def __init__(
self, self,
library: MutableLibrary, library: MutableLibrary,
*, *,
pattern: Optional[Pattern] = None, 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, tools: Union[None, Tool, MutableMapping[Optional[str], Tool]] = None,
) -> None: ) -> None:
""" """
If `ports` is `None`, two default ports ('A' and 'B') are created. # TODO documentation for Builder() constructor
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 # TODO MOVE THE BELOW DOCS to PortList
pi (attached devices will be placed to the right). # 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 self.library = library
if pattern is not None: if pattern is not None:
@ -120,7 +131,10 @@ class Builder(PortList):
if ports is not None: if ports is not None:
if self.pattern.ports: if self.pattern.ports:
raise BuildError('Ports supplied for pattern with pre-existing 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: if tools is None:
self.tools = {} self.tools = {}
@ -509,7 +523,7 @@ class Builder(PortList):
in_ptype = self.pattern[portspec].ptype in_ptype = self.pattern[portspec].ptype
pat = tool.path(ccw, length, in_ptype=in_ptype, port_names=tool_port_names, **kwargs) pat = tool.path(ccw, length, in_ptype=in_ptype, port_names=tool_port_names, **kwargs)
name = self.library.get_name(base_name) 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]}) return self.plug(Abstract(name, pat.ports), {portspec: tool_port_names[0]})
def path_to( def path_to(
@ -592,7 +606,7 @@ class Builder(PortList):
for port_name, length in extensions.items(): for port_name, length in extensions.items():
bld.path(port_name, ccw, length, tool_port_names=tool_port_names) bld.path(port_name, ccw, length, tool_port_names=tool_port_names)
name = self.library.get_name(base_name) 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_'? 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()? # TODO def path_join() and def bus_join()?

View File

@ -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 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. 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 logging
import numpy import numpy
from numpy.typing import NDArray
from ..pattern import Pattern from ..pattern import Pattern
from ..label import Label from ..label import Label
@ -50,13 +51,16 @@ def dev2pat(pattern: Pattern, layer: layer_t) -> Pattern:
def pat2dev( def pat2dev(
library: Mapping[str, Pattern],
top: str,
layers: Sequence[layer_t], layers: Sequence[layer_t],
library: Mapping[str, Pattern],
pattern: Pattern,
name: Optional[str] = None,
max_depth: int = 999_999, max_depth: int = 999_999,
skip_subcells: bool = True, skip_subcells: bool = True,
) -> Pattern: ) -> 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 Examine `pattern` for labels specifying port info, and use that info
to fill out its `ports` attribute. to fill out its `ports` attribute.
@ -64,8 +68,8 @@ def pat2dev(
'name:ptype angle_deg' 'name:ptype angle_deg'
Args: Args:
pattern: Pattern object to scan for labels.
layers: Search for labels on all the given layers. 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. max_depth: Maximum hierarcy depth to search. Default 999_999.
Reduce this to 0 to avoid ever searching subcells. Reduce this to 0 to avoid ever searching subcells.
skip_subcells: If port labels are found at a given hierarcy level, 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' to contain their own port info without interfering with supercells'
port data. port data.
Default True. Default True.
blacklist: If a cell name appears in the blacklist, do not ea
Returns: Returns:
The updated `pattern`. Port labels are not removed. 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): if not isinstance(library, Library):
library = WrapROLibrary(library) library = WrapROLibrary(library)
ports = {} pat2dev_flat(layers, pattern, name)
annotated_cells = set() if (skip_subcells and pattern.ports) or max_depth == 0:
return pattern
def find_ports_each(pat, hierarchy, transform, memo) -> Pattern: # Load ports for all subpatterns
if len(hierarchy) > max_depth: for target in set(rr.target for rr in pat.refs):
if max_depth >= 999_999: pp = pat2dev(
logger.warning(f'pat2dev reached max depth ({max_depth})') layers=layers,
return pat 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): for ref in pat.refs:
return pat aa = library.abstract(ref.target)
if not aa.ports:
continue
cell_name = hierarchy[-1] aa.apply_ref_transform(ref)
pat2dev_flat(pat, cell_name)
if skip_subcells: pattern.check_ports(other_names=aa.ports.keys())
annotated_cells.add(cell_name) pattern.ports.update(aa.ports)
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)
return pattern return pattern
def pat2dev_flat( def pat2dev_flat(
pattern: Pattern,
layers: Sequence[layer_t], layers: Sequence[layer_t],
pattern: Pattern,
cell_name: Optional[str] = None, cell_name: Optional[str] = None,
) -> Pattern: ) -> Pattern:
""" """
@ -131,8 +134,8 @@ def pat2dev_flat(
The pattern is assumed to be flat (have no `refs`) and have no pre-existing ports. The pattern is assumed to be flat (have no `refs`) and have no pre-existing ports.
Args: Args:
pattern: Pattern object to scan for labels.
layers: Search for labels on all the given layers. layers: Search for labels on all the given layers.
pattern: Pattern object to scan for labels.
cell_name: optional, used for warning message only cell_name: optional, used for warning message only
Returns: Returns:

View File

@ -10,11 +10,11 @@ from ..utils import rotation_matrix_2d
from ..error import BuildError from ..error import BuildError
if TYPE_CHECKING: if TYPE_CHECKING:
from ..ports import Port, PortList from ..ports import Port
def ell( def ell(
ports: Union[Mapping[str, 'Port'], 'PortList'], ports: Mapping[str, 'Port'],
ccw: Optional[bool], ccw: Optional[bool],
bound_type: str, bound_type: str,
bound: Union[float, ArrayLike], bound: Union[float, ArrayLike],
@ -83,9 +83,6 @@ def ell(
if not ports: if not ports:
raise BuildError('Empty port list passed to `ell()`') raise BuildError('Empty port list passed to `ell()`')
if isinstance(ports, PortList):
ports = PortList.ports
if ccw is None: if ccw is None:
if spacing is not None and not numpy.isclose(spacing, 0): if spacing is not None and not numpy.isclose(spacing, 0):
raise BuildError('Spacing must be 0 or None when ccw=None') raise BuildError('Spacing must be 0 or None when ccw=None')

View File

@ -6,11 +6,8 @@ Notes:
* ezdxf sets creation time, write time, $VERSIONGUID, and $FINGERPRINTGUID * ezdxf sets creation time, write time, $VERSIONGUID, and $FINGERPRINTGUID
to unique values, so byte-for-byte reproducibility is not achievable for now 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 from typing import List, Any, Dict, Tuple, Callable, Union, Mapping, TextIO
import re
import io import io
import base64
import struct
import logging import logging
import pathlib import pathlib
import gzip import gzip
@ -60,7 +57,7 @@ def write(
Other functions you may want to call: Other functions you may want to call:
- `masque.file.oasis.check_valid_names(library.keys())` to check for invalid names - `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 - `pattern.polygonize()` for any patterns with shapes other
than `masque.shapes.Polygon` or `masque.shapes.Path` than `masque.shapes.Polygon` or `masque.shapes.Path`

View File

@ -19,8 +19,8 @@ Notes:
* GDS creation/modification/access times are set to 1900-01-01 for reproducibility. * 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) * 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 List, Dict, Tuple, Callable, Union, Iterable, Mapping
from typing import BinaryIO, Mapping, cast from typing import BinaryIO, cast, Optional, Any
import io import io
import mmap import mmap
import logging import logging
@ -39,7 +39,7 @@ from .. import Pattern, Ref, PatternError, LibraryError, Label, Shape
from ..shapes import Polygon, Path from ..shapes import Polygon, Path
from ..repetition import Grid from ..repetition import Grid
from ..utils import layer_t, normalize_mirror, annotations_t 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__) logger = logging.getLogger(__name__)
@ -84,7 +84,7 @@ def write(
Other functions you may want to call: Other functions you may want to call:
- `masque.file.gdsii.check_valid_names(library.keys())` to check for invalid names - `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 - `pattern.polygonize()` for any patterns with shapes other
than `masque.shapes.Polygon` or `masque.shapes.Path` than `masque.shapes.Polygon` or `masque.shapes.Path`
@ -188,6 +188,7 @@ def read(
raw_mode: bool = True, raw_mode: bool = True,
) -> Tuple[Dict[str, Pattern], Dict[str, Any]]: ) -> 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 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 translated into Pattern objects; boundaries are translated into polygons, and srefs and arefs
are translated into Ref objects. are translated into Ref objects.
@ -511,6 +512,7 @@ def load_library(
stream: BinaryIO, stream: BinaryIO,
*, *,
full_load: bool = False, full_load: bool = False,
postprocess: Optional[Callable[[Library, str, Pattern], Pattern]] = None
) -> Tuple[LazyLibrary, Dict[str, Any]]: ) -> Tuple[LazyLibrary, Dict[str, Any]]:
""" """
Scan a GDSII stream to determine what structures are present, and create 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 full_load: If True, force all structures to be read immediately rather
than as-needed. Since data is read sequentially from the file, this than as-needed. Since data is read sequentially from the file, this
will be faster than using the resulting library's `precache` method. 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: Returns:
LazyLibrary object, allowing for deferred load of structures. LazyLibrary object, allowing for deferred load of structures.
@ -548,9 +552,14 @@ def load_library(
for name_bytes, pos in structs.items(): for name_bytes, pos in structs.items():
name = name_bytes.decode('ASCII') 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) 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 lib[name] = mkstruct
@ -562,6 +571,7 @@ def load_libraryfile(
*, *,
use_mmap: bool = True, use_mmap: bool = True,
full_load: bool = False, full_load: bool = False,
postprocess: Optional[Callable[[Library, str], Pattern]] = None
) -> Tuple[LazyLibrary, Dict[str, Any]]: ) -> Tuple[LazyLibrary, Dict[str, Any]]:
""" """
Wrapper for `load_library()` that takes a filename or path instead of a stream. 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 is decompressed into a python `bytes` object in memory
and reopened as an `io.BytesIO` stream. and reopened as an `io.BytesIO` stream.
full_load: If `True`, immediately loads all data. See `load_library`. full_load: If `True`, immediately loads all data. See `load_library`.
postprocess: Passed to `load_library`
Returns: Returns:
LazyLibrary object, allowing for deferred load of structures. 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 stream = mmap.mmap(base_stream.fileno(), 0, access=mmap.ACCESS_READ) # type: ignore
else: else:
stream = open(path, mode='rb') 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( def check_valid_names(

View File

@ -78,7 +78,7 @@ def build(
Other functions you may want to call: Other functions you may want to call:
- `masque.file.oasis.check_valid_names(library.keys())` to check for invalid names - `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 - `pattern.polygonize()` for any patterns with shapes other
than `masque.shapes.Polygon`, `masque.shapes.Path`, or `masque.shapes.Circle` than `masque.shapes.Polygon`, `masque.shapes.Path`, or `masque.shapes.Circle`

View File

@ -63,7 +63,7 @@ class Library(Mapping[str, 'Pattern'], metaclass=ABCMeta):
def __repr__(self) -> str: def __repr__(self) -> str:
return '<Library with keys\n' + pformat(list(self.keys())) + '>' return '<Library with keys\n' + pformat(list(self.keys())) + '>'
def dangling_references( def dangling_refs(
self, self,
tops: Union[None, str, Sequence[str]] = None, tops: Union[None, str, Sequence[str]] = None,
) -> Set[Optional[str]]: ) -> Set[Optional[str]]:
@ -304,11 +304,11 @@ class Library(Mapping[str, 'Pattern'], metaclass=ABCMeta):
def dfs( def dfs(
self: L, self: L,
top: str, pattern: 'Pattern',
visit_before: Optional[visitor_function_t] = None, visit_before: Optional[visitor_function_t] = None,
visit_after: 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, transform: Union[ArrayLike, bool, None] = False,
memo: Optional[Dict] = None, memo: Optional[Dict] = None,
) -> L: ) -> L:
@ -317,23 +317,23 @@ class Library(Mapping[str, 'Pattern'], metaclass=ABCMeta):
Performs a depth-first traversal of a pattern and its referenced patterns. Performs a depth-first traversal of a pattern and its referenced patterns.
At each pattern in the tree, the following sequence is called: At each pattern in the tree, the following sequence is called:
``` ```
hierarchy += (top,)
current_pattern = visit_before(current_pattern, **vist_args) current_pattern = visit_before(current_pattern, **vist_args)
for sp in current_pattern.refs] for sp in current_pattern.refs]
self.dfs(sp.target, visit_before, visit_after, 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) current_pattern = visit_after(current_pattern, **visit_args)
``` ```
where `visit_args` are where `visit_args` are
`hierarchy`: (top_pattern, L1_pattern, L2_pattern, ..., parent_pattern, current_pattern) `hierarchy`: (top_pattern_or_None, L1_pattern, L2_pattern, ..., parent_pattern)
tuple of all parent-and-higher pattern names 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 `transform`: numpy.ndarray containing cumulative
[x_offset, y_offset, rotation (rad), mirror_x (0 or 1)] [x_offset, y_offset, rotation (rad), mirror_x (0 or 1)]
for the instance being visited for the instance being visited
`memo`: Arbitrary dict (not altered except by `visit_before()` and `visit_after()`) `memo`: Arbitrary dict (not altered except by `visit_before()` and `visit_after()`)
Args: 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. visit_before: Function to call before traversing refs.
Should accept a `Pattern` and `**visit_args`, and return the (possibly modified) Should accept a `Pattern` and `**visit_args`, and return the (possibly modified)
pattern. Default `None` (not called). 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]`. `True` or `None` is interpreted as `[0, 0, 0, 0]`.
memo: Arbitrary dict for use by `visit_*()` functions. Default `None` (empty dict). memo: Arbitrary dict for use by `visit_*()` functions. Default `None` (empty dict).
hierarchy: Tuple of patterns specifying the hierarchy above the current pattern. hierarchy: Tuple of patterns specifying the hierarchy above the current pattern.
Appended to the start of the generated `visit_args['hierarchy']`. Default is (None,), which will be used as a placeholder for the top pattern's
Default is an empty tuple. name if not overridden.
Returns: Returns:
self self
@ -359,16 +359,12 @@ class Library(Mapping[str, 'Pattern'], metaclass=ABCMeta):
elif transform is not False: elif transform is not False:
transform = numpy.array(transform) transform = numpy.array(transform)
if top in hierarchy: original_pattern = pattern
raise LibraryError('.dfs() called on pattern with circular reference')
hierarchy += (top,)
pat = self[top]
if visit_before is not None: 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: if transform is not False:
sign = numpy.ones(2) sign = numpy.ones(2)
if transform[3]: 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) xy = numpy.dot(rotation_matrix_2d(transform[2]), ref.offset * sign)
mirror_x, angle = normalize_mirror(ref.mirrored) mirror_x, angle = normalize_mirror(ref.mirrored)
angle += ref.rotation angle += ref.rotation
sp_transform = transform + (xy[0], xy[1], angle, mirror_x) ref_transform = transform + (xy[0], xy[1], angle, mirror_x)
sp_transform[3] %= 2 ref_transform[3] %= 2
else: else:
sp_transform = False ref_transform = False
if ref.target is None: if ref.target is None:
continue continue
if ref.target in hierarchy:
raise LibraryError(f'.dfs() called on pattern with circular reference to "{ref.target}"')
self.dfs( self.dfs(
top=ref.target, pattern=self[ref.target],
visit_before=visit_before, visit_before=visit_before,
visit_after=visit_after, visit_after=visit_after,
transform=sp_transform, hierarchy=hierarchy + (ref.target,),
transform=ref_transform,
memo=memo, memo=memo,
hierarchy=hierarchy,
) )
if visit_after is not None: 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 pattern is not original_pattern:
if isinstance(self, MutableLibrary): name = hierarchy[-1]
self._set(top, pat) if not isintance(self, MutableLibrary):
else:
raise LibraryError('visit_* functions returned a new `Pattern` object' raise LibraryError('visit_* functions returned a new `Pattern` object'
' but the library is immutable') ' 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 return self
@ -424,7 +426,7 @@ class MutableLibrary(Library, Generic[VVV], metaclass=ABCMeta):
pass pass
@abstractmethod @abstractmethod
def _set(self, key: str, value: 'Pattern') -> None: def set_const(self, key: str, value: 'Pattern') -> None:
pass pass
@abstractmethod @abstractmethod
@ -564,7 +566,7 @@ class MutableLibrary(Library, Generic[VVV], metaclass=ABCMeta):
del pat.shapes[i] del pat.shapes[i]
for ll, pp in shape_pats.items(): for ll, pp in shape_pats.items():
self._set(label2name(ll), pp) self.set_const(label2name(ll), pp)
return self return self
@ -599,7 +601,7 @@ class MutableLibrary(Library, Generic[VVV], metaclass=ABCMeta):
continue continue
name = name_func(pat, shape) name = name_func(pat, shape)
self._set(name, Pattern(shapes=[shape])) self.set_const(name, Pattern(shapes=[shape]))
pat.ref(name, repetition=shape.repetition) pat.ref(name, repetition=shape.repetition)
shape.repetition = None shape.repetition = None
pat.shapes = new_shapes pat.shapes = new_shapes
@ -610,7 +612,7 @@ class MutableLibrary(Library, Generic[VVV], metaclass=ABCMeta):
new_labels.append(label) new_labels.append(label)
continue continue
name = name_func(pat, label) name = name_func(pat, label)
self._set(name, Pattern(labels=[label])) self.set_const(name, Pattern(labels=[label]))
pat.ref(name, repetition=label.repetition) pat.ref(name, repetition=label.repetition)
label.repetition = None label.repetition = None
pat.labels = new_labels pat.labels = new_labels
@ -692,7 +694,7 @@ class WrapLibrary(MutableLibrary):
def __delitem__(self, key: str) -> None: def __delitem__(self, key: str) -> None:
del self.mapping[key] del self.mapping[key]
def _set(self, key: str, value: 'Pattern') -> None: def set_const(self, key: str, value: 'Pattern') -> None:
self[key] = value self[key] = value
def _merge(self, other: Mapping[str, 'Pattern'], key: str) -> None: def _merge(self, other: Mapping[str, 'Pattern'], key: str) -> None:
@ -744,7 +746,7 @@ class LazyLibrary(MutableLibrary):
def __len__(self) -> int: def __len__(self) -> int:
return len(self.dict) 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 self[key] = lambda: value
def _merge(self, other: Mapping[str, 'Pattern'], key: str) -> None: def _merge(self, other: Mapping[str, 'Pattern'], key: str) -> None:
@ -753,7 +755,7 @@ class LazyLibrary(MutableLibrary):
if key in other.cache: if key in other.cache:
self.cache[key] = other.cache[key] self.cache[key] = other.cache[key]
else: else:
self._set(key, other[key]) self.set_const(key, other[key])
def __repr__(self) -> str: def __repr__(self) -> str:
return '<LazyLibrary with keys\n' + pformat(list(self.keys())) + '>' return '<LazyLibrary with keys\n' + pformat(list(self.keys())) + '>'

View File

@ -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. (via Ref). Shapes are assumed to inherit from masque.shapes.Shape or provide equivalent functions.
""" """
__slots__ = ( __slots__ = (
'shapes', 'labels', 'refs', 'ports', 'shapes', 'labels', 'refs', '_ports',
# inherited # inherited
'_offset', '_annotations' '_offset', '_annotations',
) )
shapes: List[Shape] shapes: List[Shape]
@ -49,9 +49,17 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
(i.e. multiple instances of the same object). (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""" """ 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__( def __init__(
self, self,
*, *,
@ -331,7 +339,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
def translate_elements(self: P, offset: ArrayLike) -> P: 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: Args:
offset: (x, y) to translate by offset: (x, y) to translate by
@ -339,7 +347,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
Returns: Returns:
self 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) cast(Positionable, entry).translate(offset)
return self return self
@ -360,7 +368,8 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
def scale_by(self: P, c: float) -> P: def scale_by(self: P, c: float) -> P:
""" """
Scale this Pattern by the given value 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: Args:
c: factor to scale by 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: 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: Args:
rotation: Angle to rotate by (counter-clockwise, radians) rotation: Angle to rotate by (counter-clockwise, radians)
@ -415,14 +424,14 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
Returns: Returns:
self 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 old_offset = cast(Positionable, entry).offset
cast(Positionable, entry).offset = numpy.dot(rotation_matrix_2d(rotation), old_offset) cast(Positionable, entry).offset = numpy.dot(rotation_matrix_2d(rotation), old_offset)
return self return self
def rotate_elements(self: P, rotation: float) -> P: 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: Args:
rotation: Angle to rotate by (counter-clockwise, radians) rotation: Angle to rotate by (counter-clockwise, radians)
@ -430,54 +439,54 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
Returns: Returns:
self 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) cast(Rotatable, entry).rotate(rotation)
return self 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 Mirror the offsets of all shapes, labels, and refs across an axis
Args: Args:
axis: Axis to mirror across across_axis: Axis to mirror across
(0: mirror across x axis, 1: mirror across y axis) (0: mirror across x axis, 1: mirror across y axis)
Returns: Returns:
self 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).offset[axis - 1] *= -1 cast(Positionable, entry).offset[across_axis - 1] *= -1
return self 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 Mirror each shape, ref, and pattern across an axis, relative
offset to its offset
Args: Args:
axis: Axis to mirror across across_axis: Axis to mirror across
(0: mirror across x axis, 1: mirror across y axis) (0: mirror across x axis, 1: mirror across y axis)
Returns: Returns:
self self
""" """
for entry in chain(self.shapes, self.refs): for entry in chain(self.shapes, self.refs, self.ports.values()):
cast(Mirrorable, entry).mirror(axis) cast(Mirrorable, entry).mirror(across_axis)
return self return self
def mirror(self: P, axis: int) -> P: def mirror(self: P, across_axis: int) -> P:
""" """
Mirror the Pattern across an axis Mirror the Pattern across an axis
Args: Args:
axis: Axis to mirror across across_axis: Axis to mirror across
(0: mirror across x axis, 1: mirror across y axis) (0: mirror across x axis, 1: mirror across y axis)
Returns: Returns:
self self
""" """
self.mirror_elements(axis) self.mirror_elements(across_axis)
self.mirror_element_centers(axis) self.mirror_element_centers(across_axis)
return self return self
def copy(self: P) -> P: def copy(self: P) -> P:

View File

@ -4,7 +4,7 @@ import warnings
import traceback import traceback
import logging import logging
from collections import Counter from collections import Counter
from abc import ABCMeta from abc import ABCMeta, abstractmethod
import numpy import numpy
from numpy import pi from numpy import pi
@ -103,10 +103,18 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable, met
class PortList(metaclass=ABCMeta): class PortList(metaclass=ABCMeta):
__slots__ = () # For use with AutoSlots __slots__ = () # Allow subclasses to use __slots__
ports: Dict[str, Port] @property
""" Uniquely-named ports which can be used to snap to other Device instances""" @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 @overload
def __getitem__(self, key: str) -> Port: def __getitem__(self, key: str) -> Port: