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
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,7 +50,7 @@ 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)),
@ -58,7 +58,7 @@ def triangle(
(numpy.cos( - pi / 6), numpy.sin( - pi / 6)),
]) * 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__':

View File

@ -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,
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,
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,
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,
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'),
}
return Device(pat, ports)
)
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,
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__':

View File

@ -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:

View File

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

View File

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

View File

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

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
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:

View File

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

View File

@ -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`

View File

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

View File

@ -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`

View File

@ -63,7 +63,7 @@ class Library(Mapping[str, 'Pattern'], metaclass=ABCMeta):
def __repr__(self) -> str:
return '<Library with keys\n' + pformat(list(self.keys())) + '>'
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 '<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.
"""
__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:

View File

@ -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]
@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: