WIP: make libraries and names first-class!
parent
f834ec6be5
commit
7aaf73cb37
@ -0,0 +1,28 @@
|
||||
import numpy
|
||||
from pyclipper import (
|
||||
Pyclipper, PT_CLIP, PT_SUBJECT, CT_UNION, CT_INTERSECTION, PFT_NONZERO,
|
||||
scale_to_clipper, scale_from_clipper,
|
||||
)
|
||||
p = Pyclipper()
|
||||
p.AddPaths([
|
||||
[(-10, -10), (-10, 10), (-9, 10), (-9, -10)],
|
||||
[(-10, 10), (10, 10), (10, 9), (-10, 9)],
|
||||
[(10, 10), (10, -10), (9, -10), (9, 10)],
|
||||
[(10, -10), (-10, -10), (-10, -9), (10, -9)],
|
||||
], PT_SUBJECT, closed=True)
|
||||
p.Execute2?
|
||||
p.Execute?
|
||||
p.Execute(PT_UNION, PT_NONZERO, PT_NONZERO)
|
||||
p.Execute(CT_UNION, PT_NONZERO, PT_NONZERO)
|
||||
p.Execute(CT_UNION, PFT_NONZERO, PFT_NONZERO)
|
||||
p = Pyclipper()
|
||||
p.AddPaths([
|
||||
[(-10, -10), (-10, 10), (-9, 10), (-9, -10)],
|
||||
[(-10, 10), (10, 10), (10, 9), (-10, 9)],
|
||||
[(10, 10), (10, -10), (9, -10), (9, 10)],
|
||||
[(10, -10), (-10, -10), (-10, -9), (10, -9)],
|
||||
], PT_SUBJECT, closed=True)
|
||||
r = p.Execute2(CT_UNION, PFT_NONZERO, PFT_NONZERO)
|
||||
r
|
||||
r.Childs
|
||||
%history -f nested_poly_test.py
|
@ -0,0 +1,298 @@
|
||||
"""
|
||||
Routines for creating normalized 2D lattices and common photonic crystal
|
||||
cavity designs.
|
||||
"""
|
||||
|
||||
from typing import Sequence, Tuple
|
||||
|
||||
import numpy # type: ignore
|
||||
|
||||
|
||||
def triangular_lattice(dims: Tuple[int, int],
|
||||
asymmetric: bool = False,
|
||||
origin: str = 'center',
|
||||
) -> numpy.ndarray:
|
||||
"""
|
||||
Return an ndarray of `[[x0, y0], [x1, y1], ...]` denoting lattice sites for
|
||||
a triangular lattice in 2D.
|
||||
|
||||
Args:
|
||||
dims: Number of lattice sites in the [x, y] directions.
|
||||
asymmetric: If true, each row will contain the same number of
|
||||
x-coord lattice sites. If false, every other row will be
|
||||
one site shorter (to make the structure symmetric).
|
||||
origin: If 'corner', the least-(x,y) lattice site is placed at (0, 0)
|
||||
If 'center', the center of the lattice (not necessarily a
|
||||
lattice site) is placed at (0, 0).
|
||||
|
||||
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 % 2 == 1] += 0.5
|
||||
sy *= numpy.sqrt(3) / 2
|
||||
|
||||
if not asymmetric:
|
||||
which = sx != sx.max()
|
||||
sx = sx[which]
|
||||
sy = sy[which]
|
||||
|
||||
xy = numpy.column_stack((sx.flat, sy.flat))
|
||||
|
||||
if origin == 'center':
|
||||
xy -= (xy.max(axis=0) - xy.min(axis=0)) / 2
|
||||
elif origin == 'corner':
|
||||
pass
|
||||
else:
|
||||
raise Exception(f'Invalid value for `origin`: {origin}')
|
||||
|
||||
return xy[xy[:, 0].argsort(), :]
|
||||
|
||||
|
||||
def square_lattice(dims: Tuple[int, int]) -> numpy.ndarray:
|
||||
"""
|
||||
Return an ndarray of `[[x0, y0], [x1, y1], ...]` denoting lattice sites for
|
||||
a square lattice in 2D. The lattice will be centered around (0, 0).
|
||||
|
||||
Args:
|
||||
dims: Number of lattice sites in the [x, y] directions.
|
||||
|
||||
Returns:
|
||||
`[[x0, y0], [x1, 1], ...]` denoting lattice sites.
|
||||
"""
|
||||
xs, ys = numpy.meshgrid(range(dims[0]), range(dims[1]), 'xy')
|
||||
xs -= dims[0]/2
|
||||
ys -= dims[1]/2
|
||||
xy = numpy.vstack((xs.flatten(), ys.flatten())).T
|
||||
return xy[xy[:, 0].argsort(), ]
|
||||
|
||||
|
||||
# ### Photonic crystal functions ###
|
||||
|
||||
|
||||
def nanobeam_holes(a_defect: float,
|
||||
num_defect_holes: int,
|
||||
num_mirror_holes: int
|
||||
) -> numpy.ndarray:
|
||||
"""
|
||||
Returns a list of `[[x0, r0], [x1, r1], ...]` of nanobeam hole positions and radii.
|
||||
Creates a region in which the lattice constant and radius are progressively
|
||||
(linearly) altered over num_defect_holes holes until they reach the value
|
||||
specified by a_defect, then symmetrically returned to a lattice constant and
|
||||
radius of 1, which is repeated num_mirror_holes times on each side.
|
||||
|
||||
Args:
|
||||
a_defect: Minimum lattice constant for the defect, as a fraction of the
|
||||
mirror lattice constant (ie., for no defect, a_defect = 1).
|
||||
num_defect_holes: How many holes form the defect (per-side)
|
||||
num_mirror_holes: How many holes form the mirror (per-side)
|
||||
|
||||
Returns:
|
||||
Ndarray `[[x0, r0], [x1, r1], ...]` of nanobeam hole positions and radii.
|
||||
"""
|
||||
a_values = numpy.linspace(a_defect, 1, num_defect_holes, endpoint=False)
|
||||
xs = a_values.cumsum() - (a_values[0] / 2) # Later mirroring makes center distance 2x as long
|
||||
mirror_xs = numpy.arange(1, num_mirror_holes + 1, dtype=float) + xs[-1]
|
||||
mirror_rs = numpy.ones_like(mirror_xs)
|
||||
return numpy.vstack((numpy.hstack((-mirror_xs[::-1], -xs[::-1], xs, mirror_xs)),
|
||||
numpy.hstack((mirror_rs[::-1], a_values[::-1], a_values, mirror_rs)))).T
|
||||
|
||||
|
||||
def waveguide(length: int, num_mirror: int) -> numpy.ndarray:
|
||||
"""
|
||||
Line defect waveguide in a triangular lattice.
|
||||
|
||||
Args:
|
||||
length: waveguide length (number of holes in x direction)
|
||||
num_mirror: Mirror length (number of holes per side; total size is
|
||||
`2 * n + 1` holes.
|
||||
|
||||
Returns:
|
||||
`[[x0, y0], [x1, y1], ...]` for all the holes
|
||||
"""
|
||||
p = triangular_lattice([length, 2 * num_mirror + 1])
|
||||
p_wg = p[p[:, 1] != 0, :]
|
||||
return p_wg
|
||||
|
||||
|
||||
def wgbend(num_mirror: int) -> numpy.ndarray:
|
||||
"""
|
||||
Line defect waveguide bend in a triangular lattice.
|
||||
|
||||
Args:
|
||||
num_mirror: Mirror length (number of holes per side; total size is
|
||||
approximately `2 * n + 1`
|
||||
|
||||
Returns:
|
||||
`[[x0, y0], [x1, y1], ...]` for all the holes
|
||||
"""
|
||||
p = triangular_lattice([2 * num_mirror, 2 * num_mirror + 1])
|
||||
left_horiz = (p[:, 1] == 0) & (p[:, 0] <= 0)
|
||||
p = p[~left_horiz, :]
|
||||
|
||||
right_diag = numpy.isclose(p[:, 1], p[:, 0] * numpy.sqrt(3)) & (p[:, 0] >= 0)
|
||||
p = p[~right_diag, :]
|
||||
return p
|
||||
|
||||
|
||||
def y_splitter(num_mirror: int) -> numpy.ndarray:
|
||||
"""
|
||||
Line defect waveguide y-splitter in a triangular lattice.
|
||||
|
||||
Args:
|
||||
num_mirror: Mirror length (number of holes per side; total size is
|
||||
approximately `2 * n + 1` holes.
|
||||
|
||||
Returns:
|
||||
`[[x0, y0], [x1, y1], ...]` for all the holes
|
||||
"""
|
||||
p = triangular_lattice([2 * num_mirror, 2 * num_mirror + 1])
|
||||
left_horiz = (p[:, 1] == 0) & (p[:, 0] <= 0)
|
||||
p = p[~left_horiz, :]
|
||||
|
||||
right_diag_up = numpy.isclose(p[:, 1], p[:, 0] * numpy.sqrt(3)) & (p[:, 0] >= 0)
|
||||
p = p[~right_diag_up, :]
|
||||
|
||||
right_diag_dn = numpy.isclose(p[:, 1], -p[:, 0] * numpy.sqrt(3)) & (p[:, 0] >= 0)
|
||||
p = p[~right_diag_dn, :]
|
||||
return p
|
||||
|
||||
|
||||
def ln_defect(mirror_dims: Tuple[int, int],
|
||||
defect_length: int,
|
||||
) -> numpy.ndarray:
|
||||
"""
|
||||
N-hole defect in a triangular lattice.
|
||||
|
||||
Args:
|
||||
mirror_dims: [x, y] mirror lengths (number of holes). Total number of holes
|
||||
is 2 * n + 1 in each direction.
|
||||
defect_length: Length of defect. Should be an odd number.
|
||||
|
||||
Returns:
|
||||
`[[x0, y0], [x1, y1], ...]` for all the holes
|
||||
"""
|
||||
if defect_length % 2 != 1:
|
||||
raise Exception('defect_length must be odd!')
|
||||
p = triangular_lattice([2 * d + 1 for d in mirror_dims])
|
||||
half_length = numpy.floor(defect_length / 2)
|
||||
hole_nums = numpy.arange(-half_length, half_length + 1)
|
||||
holes_to_keep = numpy.in1d(p[:, 0], hole_nums, invert=True)
|
||||
return p[numpy.logical_or(holes_to_keep, p[:, 1] != 0), ]
|
||||
|
||||
|
||||
def ln_shift_defect(mirror_dims: Tuple[int, int],
|
||||
defect_length: int,
|
||||
shifts_a: Sequence[float] = (0.15, 0, 0.075),
|
||||
shifts_r: Sequence[float] = (1, 1, 1)
|
||||
) -> numpy.ndarray:
|
||||
"""
|
||||
N-hole defect with shifted holes (intended to give the mode a gaussian profile
|
||||
in real- and k-space so as to improve both Q and confinement). Holes along the
|
||||
defect line are shifted and altered according to the shifts_* parameters.
|
||||
|
||||
Args:
|
||||
mirror_dims: [x, y] mirror lengths (number of holes). Total number of holes
|
||||
is `2 * n + 1` in each direction.
|
||||
defect_length: Length of defect. Should be an odd number.
|
||||
shifts_a: Percentage of a to shift (1st, 2nd, 3rd,...) holes along the defect line
|
||||
shifts_r: Factor to multiply the radius by. Should match length of shifts_a
|
||||
|
||||
Returns:
|
||||
`[[x0, y0, r0], [x1, y1, r1], ...]` for all the holes
|
||||
"""
|
||||
if not hasattr(shifts_a, "__len__") and shifts_a is not None:
|
||||
shifts_a = [shifts_a]
|
||||
if not hasattr(shifts_r, "__len__") and shifts_r is not None:
|
||||
shifts_r = [shifts_r]
|
||||
|
||||
xy = ln_defect(mirror_dims, defect_length)
|
||||
|
||||
# Add column for radius
|
||||
xyr = numpy.hstack((xy, numpy.ones((xy.shape[0], 1))))
|
||||
|
||||
# Shift holes
|
||||
# Expand shifts as necessary
|
||||
n_shifted = max(len(shifts_a), len(shifts_r))
|
||||
|
||||
tmp_a = numpy.array(shifts_a)
|
||||
shifts_a = numpy.ones((n_shifted, ))
|
||||
shifts_a[:len(tmp_a)] = tmp_a
|
||||
|
||||
tmp_r = numpy.array(shifts_r)
|
||||
shifts_r = numpy.ones((n_shifted, ))
|
||||
shifts_r[:len(tmp_r)] = tmp_r
|
||||
|
||||
x_removed = numpy.floor(defect_length / 2)
|
||||
|
||||
for ind in range(n_shifted):
|
||||
for sign in (-1, 1):
|
||||
x_val = sign * (x_removed + ind + 1)
|
||||
which = numpy.logical_and(xyr[:, 0] == x_val, xyr[:, 1] == 0)
|
||||
xyr[which, ] = (x_val + numpy.sign(x_val) * shifts_a[ind], 0, shifts_r[ind])
|
||||
|
||||
return xyr
|
||||
|
||||
|
||||
def r6_defect(mirror_dims: Tuple[int, int]) -> numpy.ndarray:
|
||||
"""
|
||||
R6 defect in a triangular lattice.
|
||||
|
||||
Args:
|
||||
mirror_dims: [x, y] mirror lengths (number of holes). Total number of holes
|
||||
is 2 * n + 1 in each direction.
|
||||
|
||||
Returns:
|
||||
`[[x0, y0], [x1, y1], ...]` specifying hole centers.
|
||||
"""
|
||||
xy = triangular_lattice([2 * d + 1 for d in mirror_dims])
|
||||
|
||||
rem_holes_plus = numpy.array([[1, 0],
|
||||
[0.5, +numpy.sqrt(3)/2],
|
||||
[0.5, -numpy.sqrt(3)/2]])
|
||||
rem_holes = numpy.vstack((rem_holes_plus, -rem_holes_plus))
|
||||
|
||||
for rem_xy in rem_holes:
|
||||
xy = xy[(xy != rem_xy).any(axis=1), ]
|
||||
|
||||
return xy
|
||||
|
||||
|
||||
def l3_shift_perturbed_defect(
|
||||
mirror_dims: Tuple[int, int],
|
||||
perturbed_radius: float = 1.1,
|
||||
shifts_a: Sequence[float] = (),
|
||||
shifts_r: Sequence[float] = ()
|
||||
) -> numpy.ndarray:
|
||||
"""
|
||||
3-hole defect with perturbed hole sizes intended to form an upwards-directed
|
||||
beam. Can also include shifted holes along the defect line, intended
|
||||
to give the mode a more gaussian profile to improve Q.
|
||||
|
||||
Args:
|
||||
mirror_dims: [x, y] mirror lengths (number of holes). Total number of holes
|
||||
is 2 * n + 1 in each direction.
|
||||
perturbed_radius: Amount to perturb the radius of the holes used for beam-forming
|
||||
shifts_a: Percentage of a to shift (1st, 2nd, 3rd,...) holes along the defect line
|
||||
shifts_r: Factor to multiply the radius by. Should match length of shifts_a
|
||||
|
||||
Returns:
|
||||
`[[x0, y0, r0], [x1, y1, r1], ...]` for all the holes
|
||||
"""
|
||||
xyr = ln_shift_defect(mirror_dims, 3, shifts_a, shifts_r)
|
||||
|
||||
abs_x, abs_y = (numpy.fabs(xyr[:, i]) for i in (0, 1))
|
||||
|
||||
# Sorted unique xs and ys
|
||||
# Ignore row y=0 because it might have shifted holes
|
||||
xs = numpy.unique(abs_x[abs_x != 0])
|
||||
ys = numpy.unique(abs_y)
|
||||
|
||||
# which holes should be perturbed? (xs[[3, 7]], ys[1]) and (xs[[2, 6]], ys[2])
|
||||
perturbed_holes = ((xs[a], ys[b]) for a, b in ((3, 1), (7, 1), (2, 2), (6, 2)))
|
||||
for row in xyr:
|
||||
if numpy.fabs(row) in perturbed_holes:
|
||||
row[2] = perturbed_radius
|
||||
return xyr
|
@ -0,0 +1,178 @@
|
||||
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, dose=1.0)
|
||||
])
|
||||
return pat
|
||||
|
||||
|
||||
def perturbed_l3(lattice_constant: float,
|
||||
hole: Pattern,
|
||||
trench_dose: float = 1.0,
|
||||
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_dose: Dose for the trenches. Default 1.0. (Hole dose is 1.0.)
|
||||
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, dose=trench_dose),
|
||||
Polygon.rect(ymax=min_xy[1], xmin=min_xy[0], lx=trench_dx, ly=trench_width,
|
||||
layer=trench_layer, dose=trench_dose),
|
||||
]
|
||||
|
||||
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()
|
@ -1,3 +1,4 @@
|
||||
from .devices import Port, Device
|
||||
from .utils import ell
|
||||
from .tools import Tool
|
||||
from .device_library import DeviceLibrary, LibDeviceLibrary
|
||||
|
@ -0,0 +1,594 @@
|
||||
"""
|
||||
Library class for managing unique name->pattern mappings and
|
||||
deferred loading or creation.
|
||||
"""
|
||||
from typing import List, Dict, Callable, TypeVar, Type, TYPE_CHECKING
|
||||
from typing import Any, Tuple, Union, Iterator, Mapping, MutableMapping, Set, Optional, Sequence
|
||||
import logging
|
||||
import copy
|
||||
import base64
|
||||
import struct
|
||||
import re
|
||||
from pprint import pformat
|
||||
from collections import defaultdict
|
||||
|
||||
import numpy
|
||||
from numpy.typing import ArrayLike, NDArray, NDArray
|
||||
|
||||
from .error import LibraryError, PatternError
|
||||
from .utils import rotation_matrix_2d, normalize_mirror
|
||||
from .shapes import Shape, Polygon
|
||||
from .label import Label
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .pattern import Pattern
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
visitor_function_t = Callable[['Pattern', Tuple['Pattern'], Dict, NDArray[numpy.float64]], 'Pattern']
|
||||
L = TypeVar('L', bound='Library')
|
||||
|
||||
|
||||
class Library:
|
||||
"""
|
||||
This class is usually used to create a library of Patterns by mapping names to
|
||||
functions which generate or load the relevant `Pattern` object as-needed.
|
||||
|
||||
The cache can be disabled by setting the `enable_cache` attribute to `False`.
|
||||
"""
|
||||
dict: Dict[str, Callable[[], Pattern]]
|
||||
cache: Dict[str, 'Pattern']
|
||||
enable_cache: bool = True
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.dict = {}
|
||||
self.cache = {}
|
||||
|
||||
def __setitem__(self, key: str, value: Callable[[], Pattern]) -> None:
|
||||
self.dict[key] = value
|
||||
if key in self.cache:
|
||||
del self.cache[key]
|
||||
|
||||
def __delitem__(self, key: str) -> None:
|
||||
del self.dict[key]
|
||||
if key in self.cache:
|
||||
del self.cache[key]
|
||||
|
||||
def __getitem__(self, key: str) -> 'Pattern':
|
||||
logger.debug(f'loading {key}')
|
||||
if self.enable_cache and key in self.cache:
|
||||
logger.debug(f'found {key} in cache')
|
||||
return self.cache[key]
|
||||
|
||||
func = self.dict[key]
|
||||
pat = func()
|
||||
self.cache[key] = pat
|
||||
return pat
|
||||
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
return iter(self.keys())
|
||||
|
||||
def __contains__(self, key: str) -> bool:
|
||||
return key in self.dict
|
||||
|
||||
def keys(self) -> Iterator[str]:
|
||||
return iter(self.dict.keys())
|
||||
|
||||
def values(self) -> Iterator['Pattern']:
|
||||
return iter(self[key] for key in self.keys())
|
||||
|
||||
def items(self) -> Iterator[Tuple[str, 'Pattern']]:
|
||||
return iter((key, self[key]) for key in self.keys())
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return '<Library with keys ' + repr(list(self.dict.keys())) + '>'
|
||||
|
||||
def precache(self: L) -> L:
|
||||
"""
|
||||
Force all patterns into the cache
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
for key in self.dict:
|
||||
_ = self.dict.__getitem__(key)
|
||||
return self
|
||||
|
||||
def add(
|
||||
self: L,
|
||||
other: L,
|
||||
use_ours: Callable[[str], bool] = lambda name: False,
|
||||
use_theirs: Callable[[str], bool] = lambda name: False,
|
||||
) -> L:
|
||||
"""
|
||||
Add keys from another library into this one.
|
||||
|
||||
Args:
|
||||
other: The library to insert keys from
|
||||
use_ours: Decision function for name conflicts, called with cell name.
|
||||
Should return `True` if the value from `self` should be used.
|
||||
use_theirs: Decision function for name conflicts. Same format as `use_ours`.
|
||||
Should return `True` if the value from `other` should be used.
|
||||
`use_ours` takes priority over `use_theirs`.
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
duplicates = set(self.keys()) & set(other.keys())
|
||||
keep_ours = set(name for name in duplicates if use_ours(name))
|
||||
keep_theirs = set(name for name in duplicates - keep_ours if use_theirs(name))
|
||||
conflicts = duplicates - keep_ours - keep_theirs
|
||||
|
||||
if conflicts:
|
||||
raise LibraryError('Unresolved duplicate keys encountered in library merge: ' + pformat(conflicts))
|
||||
|
||||
for key in set(other.keys()) - keep_ours:
|
||||
self.dict[key] = other.dict[key]
|
||||
if key in other.cache:
|
||||
self.cache[key] = other.cache[key]
|
||||
|
||||
return self
|
||||
|
||||
def clear_cache(self: L) -> L:
|
||||
"""
|
||||
Clear the cache of this library.
|
||||
This is usually used before modifying or deleting cells, e.g. when merging
|
||||
with another library.
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
self.cache.clear()
|
||||
return self
|
||||
|
||||
def referenced_patterns(
|
||||
self,
|
||||
tops: Union[str, Sequence[str]],
|
||||
skip: Optional[Set[Optional[str]]] = None,
|
||||
) -> Set[Optional[str]]:
|
||||
"""
|
||||
Get the set of all pattern names referenced by `top`. Recursively traverses into any subpatterns.
|
||||
|
||||
Args:
|
||||
top: Name of the top pattern(s) to check.
|
||||
skip: Memo, set patterns which have already been traversed.
|
||||
|
||||
Returns:
|
||||
Set of all referenced pattern names
|
||||
"""
|
||||
if skip is None:
|
||||
skip = set([None])
|
||||
|
||||
if isinstance(tops, str):
|
||||
tops = (tops,)
|
||||
|
||||
# Get referenced patterns for all tops
|
||||
targets = set()
|
||||
for top in set(tops):
|
||||
targets |= self[top].referenced_patterns()
|
||||
|
||||
# Perform recursive lookups, but only once for each name
|
||||
for target in targets - skip:
|
||||
assert(target is not None)
|
||||
self.referenced_patterns(target, skip)
|
||||
skip.add(target)
|
||||
|
||||
return targets
|
||||
|
||||
def subtree(
|
||||
self: L,
|
||||
tops: Union[str, Sequence[str]],
|
||||
) -> L:
|
||||
"""
|
||||
Return a new `Library`, containing only the specified patterns and the patterns they
|
||||
reference (recursively).
|
||||
|
||||
Args:
|
||||
tops: Name(s) of patterns to keep
|
||||
|
||||
Returns:
|
||||
A `Library` containing only `tops` and the patterns they reference.
|
||||
"""
|
||||
keep: Set[str] = self.referenced_patterns(tops) - set((None,)) # type: ignore
|
||||
|
||||
new = type(self)()
|
||||
for key in keep:
|
||||
new.dict[key] = self.dict[key]
|
||||
if key in self.cache:
|
||||
new.cache[key] = self.cache[key]
|
||||
|
||||
return new
|
||||
|
||||
def dfs(
|
||||
self: L,
|
||||
top: str,
|
||||
visit_before: visitor_function_t = None,
|
||||
visit_after: visitor_function_t = None,
|
||||
transform: Union[ArrayLike, bool, None] = False,
|
||||
memo: Optional[Dict] = None,
|
||||
hierarchy: Tuple[str, ...] = (),
|
||||
) -> L:
|
||||
"""
|
||||
Convenience function.
|
||||
Performs a depth-first traversal of a pattern and its subpatterns.
|
||||
At each pattern in the tree, the following sequence is called:
|
||||
```
|
||||
current_pattern = visit_before(current_pattern, **vist_args)
|
||||
for sp in current_pattern.subpatterns]
|
||||
self.dfs(sp.target, visit_before, visit_after, updated_transform,
|
||||
memo, (current_pattern,) + hierarchy)
|
||||
current_pattern = visit_after(current_pattern, **visit_args)
|
||||
```
|
||||
where `visit_args` are
|
||||
`hierarchy`: (top_pattern, L1_pattern, L2_pattern, ..., parent_pattern)
|
||||
tuple of all parent-and-higher patterns
|
||||
`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).
|
||||
visit_before: Function to call before traversing subpatterns.
|
||||
Should accept a `Pattern` and `**visit_args`, and return the (possibly modified)
|
||||
pattern. Default `None` (not called).
|
||||
visit_after: Function to call after traversing subpatterns.
|
||||
Should accept a `Pattern` and `**visit_args`, and return the (possibly modified)
|
||||
pattern. Default `None` (not called).
|
||||
transform: Initial value for `visit_args['transform']`.
|
||||
Can be `False`, in which case the transform is not calculated.
|
||||
`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.
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
if memo is None:
|
||||
memo = {}
|
||||
|
||||
if transform is None or transform is True:
|
||||
transform = numpy.zeros(4)
|
||||
elif transform is not False:
|
||||
transform = numpy.array(transform)
|
||||
|
||||
if top in hierarchy:
|
||||
raise PatternError('.dfs() called on pattern with circular reference')
|
||||
|
||||
pat = self[top]
|
||||
if visit_before is not None:
|
||||
pat = visit_before(pat, hierarchy=hierarchy, memo=memo, transform=transform) # type: ignore
|
||||
|
||||
for subpattern in pat.subpatterns:
|
||||
if transform is not False:
|
||||
sign = numpy.ones(2)
|
||||
if transform[3]:
|
||||
sign[1] = -1
|
||||
xy = numpy.dot(rotation_matrix_2d(transform[2]), subpattern.offset * sign)
|
||||
mirror_x, angle = normalize_mirror(subpattern.mirrored)
|
||||
angle += subpattern.rotation
|
||||
sp_transform = transform + (xy[0], xy[1], angle, mirror_x)
|
||||
sp_transform[3] %= 2
|
||||
else:
|
||||
sp_transform = False
|
||||
|
||||
if subpattern.target is None:
|
||||
continue
|
||||
|
||||
self.dfs(
|
||||
top=subpattern.target,
|
||||
visit_before=visit_before,
|
||||
visit_after=visit_after,
|
||||
transform=sp_transform,
|
||||
memo=memo,
|
||||
hierarchy=hierarchy + (top,),
|
||||
)
|
||||
|
||||
if visit_after is not None:
|
||||
pat = visit_after(pat, hierarchy=hierarchy, memo=memo, transform=transform) # type: ignore
|
||||
|
||||
self[top] = lambda: pat
|
||||
return self
|
||||
|
||||
def polygonize(
|
||||
self: L,
|
||||
poly_num_points: Optional[int] = None,
|
||||
poly_max_arclen: Optional[float] = None,
|
||||
) -> L:
|
||||
"""
|
||||
Calls `.polygonize(...)` on each pattern in this library.
|
||||
Arguments are passed on to `shape.to_polygons(...)`.
|
||||
|
||||
Args:
|
||||
poly_num_points: Number of points to use for each polygon. Can be overridden by
|
||||
`poly_max_arclen` if that results in more points. Optional, defaults to shapes'
|
||||
internal defaults.
|
||||
poly_max_arclen: Maximum arclength which can be approximated by a single line
|
||||
segment. Optional, defaults to shapes' internal defaults.
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
for pat in self.values():
|
||||
pat.polygonize(poly_num_points, poly_max_arclen)
|
||||
return self
|
||||
|
||||
def manhattanize(
|
||||
self: L,
|
||||
grid_x: ArrayLike,
|
||||
grid_y: ArrayLike,
|
||||
) -> L:
|
||||
"""
|
||||
Calls `.manhattanize(grid_x, grid_y)` on each pattern in this library.
|
||||
|
||||
Args:
|
||||
grid_x: List of allowed x-coordinates for the Manhattanized polygon edges.
|
||||
grid_y: List of allowed y-coordinates for the Manhattanized polygon edges.
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
for pat in self.values():
|
||||
pat.manhattanize(grid_x, grid_y)
|
||||
return self
|
||||
|
||||
def subpatternize(
|
||||
self: L,
|
||||
norm_value: int = int(1e6),
|
||||
exclude_types: Tuple[Type] = (Polygon,),
|
||||
label2name: Optional[Callable[[Tuple], str]] = None,
|
||||
threshold: int = 2,
|
||||
) -> L:
|
||||
"""
|
||||
Iterates through all `Pattern`s. Within each `Pattern`, it iterates
|
||||
over all shapes, calling `.normalized_form(norm_value)` on them to retrieve a scale-,
|
||||
offset-, dose-, and rotation-independent form. Each shape whose normalized form appears
|
||||
more than once is removed and re-added using subpattern objects referencing a newly-created
|
||||
`Pattern` containing only the normalized form of the shape.
|
||||
|
||||
Note:
|
||||
The default norm_value was chosen to give a reasonable precision when using
|
||||
integer values for coordinates.
|
||||
|
||||
Args:
|
||||
norm_value: Passed to `shape.normalized_form(norm_value)`. Default `1e6` (see function
|
||||
note)
|
||||
exclude_types: Shape types passed in this argument are always left untouched, for
|
||||
speed or convenience. Default: `(shapes.Polygon,)`
|
||||
label2name: Given a label tuple as returned by `shape.normalized_form(...)`, pick
|
||||
a name for the generated pattern. Default `self.get_name('_shape')`.
|
||||
threshold: Only replace shapes with subpatterns if there will be at least this many
|
||||
instances.
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
# This currently simplifies globally (same shape in different patterns is
|
||||
# merged into the same subpattern target.
|
||||
|
||||
if exclude_types is None:
|
||||
exclude_types = ()
|
||||
|
||||
if label2name is None:
|
||||
label2name = lambda label: self.get_name('_shape')
|
||||
|
||||
|
||||
shape_counts: MutableMapping[Tuple, int] = defaultdict(int)
|
||||
shape_funcs = {}
|
||||
|
||||
### First pass ###
|
||||
# Using the label tuple from `.normalized_form()` as a key, check how many of each shape
|
||||
# are present and store the shape function for each one
|
||||
for pat in tuple(self.values()):
|
||||
for i, shape in enumerate(pat.shapes):
|
||||
if not any(isinstance(shape, t) for t in exclude_types):
|
||||
label, _values, func = shape.normalized_form(norm_value)
|
||||
shape_funcs[label] = func
|
||||
shape_counts[label] += 1
|
||||
|
||||
shape_pats = {}
|
||||
for label, count in shape_counts.items():
|
||||
if count < threshold:
|
||||
continue
|
||||
|
||||
shape_func = shape_funcs[label]
|
||||
shape_pat = Pattern(shapes=[shape_func()])
|
||||
shape_pats[label] = shape_pat
|
||||
|
||||
### Second pass ###
|
||||
for pat in tuple(self.values()):
|
||||
# Store `[(index_in_shapes, values_from_normalized_form), ...]` for all shapes which
|
||||
# are to be replaced.
|
||||
# The `values` are `(offset, scale, rotation, dose)`.
|
||||
|
||||
shape_table: MutableMapping[Tuple, List] = defaultdict(list)
|
||||
for i, shape in enumerate(pat.shapes):
|
||||
if any(isinstance(shape, t) for t in exclude_types):
|
||||
continue
|
||||
|
||||
label, values, _func = shape.normalized_form(norm_value)
|
||||
|
||||
if label not in shape_pats:
|
||||
continue
|
||||
|
||||
shape_table[label].append((i, values))
|
||||
|
||||
# For repeated shapes, create a `Pattern` holding a normalized shape object,
|
||||
# and add `pat.subpatterns` entries for each occurrence in pat. Also, note down that
|
||||
# we should delete the `pat.shapes` entries for which we made SubPatterns.
|
||||
shapes_to_remove = []
|
||||
for label in shape_table:
|
||||
target = label2name(label)
|
||||
for i, values in shape_table[label]:
|
||||
offset, scale, rotation, mirror_x, dose = values
|
||||
pat.addsp(target=target, offset=offset, scale=scale,
|
||||
rotation=rotation, dose=dose, mirrored=(mirror_x, False))
|
||||
shapes_to_remove.append(i)
|
||||
|
||||
# Remove any shapes for which we have created subpatterns.
|
||||
for i in sorted(shapes_to_remove, reverse=True):
|
||||
del pat.shapes[i]
|
||||
|
||||
for ll, pp in shape_pats.items():
|
||||
self[label2name(ll)] = lambda: pp
|
||||
|
||||
return self
|
||||
|
||||
def wrap_repeated_shapes(
|
||||
self: L,
|
||||
name_func: Optional[Callable[['Pattern', Union[Shape, Label]], str]] = None,
|
||||
) -> L:
|
||||
"""
|
||||
Wraps all shapes and labels with a non-`None` `repetition` attribute
|
||||
into a `SubPattern`/`Pattern` combination, and applies the `repetition`
|
||||
to each `SubPattern` instead of its contained shape.
|
||||
|
||||
Args:
|
||||
name_func: Function f(this_pattern, shape) which generates a name for the
|
||||
wrapping pattern. Default is `self.get_name('_rep')`.
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
if name_func is None:
|
||||
name_func = lambda _pat, _shape: self.get_name('_rep')
|
||||
|
||||
for pat in tuple(self.values()):
|
||||
new_shapes = []
|
||||
for shape in pat.shapes:
|
||||
if shape.repetition is None:
|
||||
new_shapes.append(shape)
|
||||
continue
|
||||
|
||||
name = name_func(pat, shape)
|
||||
self[name] = lambda: Pattern(shapes=[shape])
|
||||
pat.addsp(name, repetition=shape.repetition)
|
||||
shape.repetition = None
|
||||
pat.shapes = new_shapes
|
||||
|
||||
new_labels = []
|
||||
for label in pat.labels:
|
||||
if label.repetition is None:
|
||||
new_labels.append(label)
|
||||
continue
|
||||
name = name_func(pat, label)
|
||||
self[name] = lambda: Pattern(labels=[label])
|
||||
pat.addsp(name, repetition=label.repetition)
|
||||
label.repetition = None
|
||||
pat.labels = new_labels
|
||||
|
||||
return self
|
||||
|
||||
def flatten(
|
||||
self: L,
|
||||
tops: Union[str, Sequence[str]],
|
||||
) -> Dict[str, Pattern]:
|
||||
"""
|
||||
Removes all subpatterns and adds equivalent shapes.
|
||||
Also flattens all subpatterns.
|
||||
|
||||
Args:
|
||||
tops: The pattern(s) to flattern.
|
||||
|
||||
Returns:
|
||||
{name: flat_pattern} mapping for all flattened patterns.
|
||||
"""
|
||||
if isinstance(tops, str):
|
||||
tops = (tops,)
|
||||
|
||||
flattened: Dict[str, Optional[Pattern]] = {}
|
||||
|
||||
def flatten_single(name) -> None:
|
||||
flattened[name] = None
|
||||
pat = self[name].deepcopy()
|
||||
|
||||
for subpat in pat.subpatterns:
|
||||
target = subpat.target
|
||||
if target is None:
|
||||
continue
|
||||
|
||||
if target not in flattened:
|
||||
flatten_single(target)
|
||||
if flattened[target] is None:
|
||||
raise PatternError(f'Circular reference in {name} to {target}')
|
||||
|
||||
p = subpat.as_pattern(pattern=flattened[target])
|
||||
pat.append(p)
|
||||
|
||||
pat.subpatterns.clear()
|
||||
flattened[name] = pat
|
||||
|
||||
for top in tops:
|
||||
flatten_single(top)
|
||||
|
||||
assert(None not in flattened.values())
|
||||
return flattened # type: ignore
|
||||
|
||||
def get_name(
|
||||
self,
|
||||
name: str = '__',
|
||||
sanitize: bool = True,
|
||||
max_length: int = 32,
|
||||
quiet: bool = False,
|
||||
) -> str:
|
||||
"""
|
||||
Find a unique name for the pattern.
|
||||
|
||||
This function may be overridden in a subclass or monkey-patched to fit the caller's requirements.
|
||||
|
||||
Args:
|
||||
name: Preferred name for the pattern. Default '__'.
|
||||
sanitize: Allows only alphanumeric charaters and _?$. Replaces invalid characters with underscores.
|
||||
max_length: Names longer than this will be truncated.
|
||||
quiet: If `True`, suppress log messages.
|
||||
|
||||
Returns:
|
||||
Unique name for this library.
|
||||
"""
|
||||
if sanitize:
|
||||
# Remove invalid characters
|
||||
sanitized_name = re.compile(r'[^A-Za-z0-9_\?\$]').sub('_', name)
|
||||
else:
|
||||
sanitized_name = name
|
||||
|
||||
ii = 0
|
||||
suffixed_name = sanitized_name
|
||||
while suffixed_name in self or suffixed_name == '':
|
||||
suffix = base64.b64encode(struct.pack('>Q', ii), b'$?').decode('ASCII')
|
||||
|
||||
suffixed_name = sanitized_name + '$' + suffix[:-1].lstrip('A')
|
||||
ii += 1
|
||||
|
||||
if len(suffixed_name) > max_length:
|
||||
if name == '':
|
||||
raise LibraryError(f'No valid pattern names remaining within the specified {max_length=}')
|
||||
|
||||
cropped_name = self.get_name(sanitized_name[:-1], sanitize=sanitize, max_length=max_length, quiet=True)
|
||||
else:
|
||||
cropped_name = suffixed_name
|
||||
|
||||
if not quiet:
|
||||
logger.info(f'Requested name "{name}" changed to "{cropped_name}"')
|
||||
|
||||
return cropped_name
|
||||
|
||||
def find_toplevel(self) -> List[str]:
|
||||
"""
|
||||
Return the list of all patterns that are not referenced by any other pattern in the library.
|
||||
|
||||
Returns:
|
||||
A list of pattern names in which no pattern is referenced by any other pattern.
|
||||
"""
|
||||
names = set(self.keys())
|
||||
not_toplevel: Set[Optional[str]] = set()
|
||||
for name in names:
|
||||
not_toplevel |= set(sp.target for sp in self[name].subpatterns)
|
||||
|
||||
toplevel = list(names - not_toplevel)
|
||||
return toplevel
|
||||
|
||||
def __deepcopy__(self, memo: Dict = None) -> 'Library':
|
||||
raise LibraryError('Libraries cannot be deepcopied (deepcopy doesn\'t descend into closures)')
|
@ -1,2 +0,0 @@
|
||||
from .library import Library, PatternGenerator
|
||||
from .device_library import DeviceLibrary, LibDeviceLibrary
|
@ -1,355 +0,0 @@
|
||||
"""
|
||||
Library class for managing unique name->pattern mappings and
|
||||
deferred loading or creation.
|
||||
"""
|
||||
from typing import Dict, Callable, TypeVar, TYPE_CHECKING
|
||||
from typing import Any, Tuple, Union, Iterator
|
||||
import logging
|
||||
from pprint import pformat
|
||||
from dataclasses import dataclass
|
||||
import copy
|
||||
|
||||
from ..error import LibraryError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..pattern import Pattern
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PatternGenerator:
|
||||
__slots__ = ('tag', 'gen')
|
||||
tag: str
|
||||
""" Unique identifier for the source """
|
||||
|
||||
gen: Callable[[], 'Pattern']
|
||||
""" Function which generates a pattern when called """
|
||||
|
||||
|
||||
L = TypeVar('L', bound='Library')
|
||||
|
||||
|
||||
class Library:
|
||||
"""
|
||||
This class is usually used to create a library of Patterns by mapping names to
|
||||
functions which generate or load the relevant `Pattern` object as-needed.
|
||||
|
||||
Generated/loaded patterns can have "symbolic" references, where a SubPattern
|
||||
object `sp` has a `None`-valued `sp.pattern` attribute, in which case the
|
||||
Library expects `sp.identifier[0]` to contain a string which specifies the
|
||||
referenced pattern's name.
|
||||
|
||||
Patterns can either be "primary" (default) or "secondary". Both get the
|
||||
same deferred-load behavior, but "secondary" patterns may have conflicting
|
||||
names and are not accessible through basic []-indexing. They are only used
|
||||
to fill symbolic references in cases where there is no "primary" pattern
|
||||
available, and only if both the referencing and referenced pattern-generators'
|
||||
`tag` values match (i.e., only if they came from the same source).
|
||||
|
||||
Primary patterns can be turned into secondary patterns with the `demote`
|
||||
method, `promote` performs the reverse (secondary -> primary) operation.
|
||||
|
||||
The `set_const` and `set_value` methods provide an easy way to transparently
|
||||
construct PatternGenerator objects and directly set create "secondary"
|
||||
patterns.
|
||||
|
||||
The cache can be disabled by setting the `enable_cache` attribute to `False`.
|
||||
"""
|
||||
primary: Dict[str, PatternGenerator]
|
||||
secondary: Dict[Tuple[str, str], PatternGenerator]
|
||||
cache: Dict[Union[str, Tuple[str, str]], 'Pattern']
|
||||
enable_cache: bool = True
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.primary = {}
|
||||
self.secondary = {}
|
||||
self.cache = {}
|
||||
|
||||
def __setitem__(self, key: str, value: PatternGenerator) -> None:
|
||||
self.primary[key] = value
|
||||
if key in self.cache:
|
||||
logger.warning(f'Replaced library item "{key}" & existing cache entry.'
|
||||
' Previously-generated Pattern will *not* be updated!')
|
||||
del self.cache[key]
|
||||
|
||||
def __delitem__(self, key: str) -> None:
|
||||
if isinstance(key, str):
|
||||
del self.primary[key]
|
||||
elif isinstance(key, tuple):
|
||||
del self.secondary[key]
|
||||
|
||||
if key in self.cache:
|
||||
logger.warning(f'Deleting library item "{key}" & existing cache entry.'
|
||||
' Previously-generated Pattern may remain in the wild!')
|
||||
del self.cache[key]
|
||||
|
||||
def __getitem__(self, key: str) -> 'Pattern':
|
||||
return self.get_primary(key)
|
||||
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
return iter(self.keys())
|
||||
|
||||
def __contains__(self, key: str) -> bool:
|
||||
return key in self.primary
|
||||
|
||||
def get_primary(self, key: str) -> 'Pattern':
|
||||
if self.enable_cache and key in self.cache:
|
||||
logger.debug(f'found {key} in cache')
|
||||
return self.cache[key]
|
||||
|
||||
logger.debug(f'loading {key}')
|
||||
pg = self.primary[key]
|
||||
pat = pg.gen()
|
||||
self.resolve_subpatterns(pat, pg.tag)
|
||||
self.cache[key] = pat
|
||||
return pat
|
||||
|
||||
def get_secondary(self, key: str, tag: str) -> 'Pattern':
|
||||
logger.debug(f'get_secondary({key}, {tag})')
|
||||
key2 = (key, tag)
|
||||
if self.enable_cache and key2 in self.cache:
|
||||
return self.cache[key2]
|
||||
|
||||
pg = self.secondary[key2]
|
||||
pat = pg.gen()
|
||||
self.resolve_subpatterns(pat, pg.tag)
|
||||
self.cache[key2] = pat
|
||||
return pat
|
||||
|
||||
def set_secondary(self, key: str, tag: str, value: PatternGenerator) -> None:
|
||||
self.secondary[(key, tag)] = value
|
||||
if (key, tag) in self.cache:
|
||||
logger.warning(f'Replaced library item "{key}" & existing cache entry.'
|
||||
' Previously-generated Pattern will *not* be updated!')
|
||||
del self.cache[(key, tag)]
|
||||
|
||||
def resolve_subpatterns(self, pat: 'Pattern', tag: str) -> 'Pattern':
|
||||
logger.debug(f'Resolving subpatterns in {pat.name}')
|
||||
for sp in pat.subpatterns:
|
||||
if sp.pattern is not None:
|
||||
continue
|
||||
|
||||
key = sp.identifier[0]
|
||||
if key in self.primary:
|
||||
sp.pattern = self.get_primary(key)
|
||||
continue
|
||||
|
||||
if (key, tag) in self.secondary:
|
||||
sp.pattern = self.get_secondary(key, tag)
|
||||
continue
|
||||
|
||||
raise LibraryError(f'Broken reference to {key} (tag {tag})')
|
||||
return pat
|
||||
|
||||
def keys(self) -> Iterator[str]:
|
||||
return iter(self.primary.keys())
|
||||
|
||||
def values(self) -> Iterator['Pattern']:
|
||||
return iter(self[key] for key in self.keys())
|
||||
|
||||
def items(self) -> Iterator[Tuple[str, 'Pattern']]:
|
||||
return iter((key, self[key]) for key in self.keys())
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return '<Library with keys ' + repr(list(self.primary.keys())) + '>'
|
||||
|
||||
def set_const(
|
||||
self,
|
||||
key: str,
|
||||
tag: Any,
|
||||
const: 'Pattern',
|
||||
secondary: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Convenience function to avoid having to manually wrap
|
||||
constant values into callables.
|
||||
|
||||
Args:
|
||||
key: Lookup key, usually the cell/pattern name
|
||||
tag: Unique tag for the source, used to disambiguate secondary patterns
|
||||
const: Pattern object to return
|
||||
secondary: If True, this pattern is not accessible for normal lookup, and is
|
||||
only used as a sub-component of other patterns if no non-secondary
|
||||
equivalent is available.
|
||||
"""
|
||||
pg = PatternGenerator(tag=tag, gen=lambda: const)
|
||||
if secondary:
|
||||
self.secondary[(key, tag)] = pg
|
||||
else:
|
||||
self.primary[key] = pg
|
||||
|
||||
def set_value(
|
||||
self,
|
||||
key: str,
|
||||
tag: str,
|
||||
value: Callable[[], 'Pattern'],
|
||||
secondary: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Convenience function to automatically build a PatternGenerator.
|
||||
|
||||
Args:
|
||||
key: Lookup key, usually the cell/pattern name
|
||||
tag: Unique tag for the source, used to disambiguate secondary patterns
|
||||
value: Callable which takes no arguments and generates the `Pattern` object
|
||||
secondary: If True, this pattern is not accessible for normal lookup, and is
|
||||
only used as a sub-component of other patterns if no non-secondary
|
||||
equivalent is available.
|
||||
"""
|
||||
pg = PatternGenerator(tag=tag, gen=value)
|
||||
if secondary:
|
||||
self.secondary[(key, tag)] = pg
|
||||
else:
|
||||
self.primary[key] = pg
|
||||
|
||||
def precache(self: L) -> L:
|
||||
"""
|
||||
Force all patterns into the cache
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
for key in self.primary:
|
||||
_ = self.get_primary(key)
|
||||
for key2 in self.secondary:
|
||||
_ = self.get_secondary(*key2)
|
||||
return self
|
||||
|
||||
def add(
|
||||
self: L,
|
||||
other: L,
|
||||
use_ours: Callable[[Union[str, Tuple[str, str]]], bool] = lambda name: False,
|
||||
use_theirs: Callable[[Union[str, Tuple[str, str]]], bool] = lambda name: False,
|
||||
) -> L:
|
||||
"""
|
||||
Add keys from another library into this one.
|
||||
|
||||
Args:
|
||||
other: The library to insert keys from
|
||||
use_ours: Decision function for name conflicts.
|
||||
May be called with cell names and (name, tag) tuples for primary or
|
||||
secondary cells, respectively.
|
||||
Should return `True` if the value from `self` should be used.
|
||||
use_theirs: Decision function for name conflicts. Same format as `use_ours`.
|
||||
Should return `True` if the value from `other` should be used.
|
||||
`use_ours` takes priority over `use_theirs`.
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
duplicates1 = set(self.primary.keys()) & set(other.primary.keys())
|
||||
duplicates2 = set(self.secondary.keys()) & set(other.secondary.keys())
|
||||
keep_ours1 = set(name for name in duplicates1 if use_ours(name))
|
||||
keep_ours2 = set(name for name in duplicates2 if use_ours(name))
|
||||
keep_theirs1 = set(name for name in duplicates1 - keep_ours1 if use_theirs(name))
|
||||
keep_theirs2 = set(name for name in duplicates2 - keep_ours2 if use_theirs(name))
|
||||
conflicts1 = duplicates1 - keep_ours1 - keep_theirs1
|
||||
conflicts2 = duplicates2 - keep_ours2 - keep_theirs2
|
||||
|
||||
if conflicts1:
|
||||
raise LibraryError('Unresolved duplicate keys encountered in library merge: ' + pformat(conflicts1))
|
||||
|
||||
if conflicts2:
|
||||
raise LibraryError('Unresolved duplicate secondary keys encountered in library merge: ' + pformat(conflicts2))
|
||||
|
||||
for key1 in set(other.primary.keys()) - keep_ours1:
|
||||
self[key1] = other.primary[key1]
|
||||
if key1 in other.cache:
|
||||
self.cache[key1] = other.cache[key1]
|
||||
|
||||
for key2 in set(other.secondary.keys()) - keep_ours2:
|
||||
self.set_secondary(*key2, other.secondary[key2])
|
||||
if key2 in other.cache:
|
||||
self.cache[key2] = other.cache[key2]
|
||||
|
||||
return self
|
||||
|
||||
def demote(self, key: str) -> None:
|
||||
"""
|
||||
Turn a primary pattern into a secondary one.
|
||||
It will no longer be accessible through [] indexing and will only be used to
|
||||
when referenced by other patterns from the same source, and only if no primary
|
||||
pattern with the same name exists.
|
||||
|
||||
Args:
|
||||
key: Lookup key, usually the cell/pattern name
|
||||
"""
|
||||
pg = self.primary[key]
|
||||
key2 = (key, pg.tag)
|
||||
self.secondary[key2] = pg
|
||||
if key in self.cache:
|
||||
self.cache[key2] = self.cache[key]
|
||||
del self[key]
|
||||
|
||||
def promote(self, key: str, tag: str) -> None:
|
||||
"""
|
||||
Turn a secondary pattern into a primary one.
|
||||
It will become accessible through [] indexing and will be used to satisfy any
|
||||
reference to a pattern with its key, regardless of tag.
|
||||
|
||||
Args:
|
||||
key: Lookup key, usually the cell/pattern name
|
||||
tag: Unique tag for identifying the pattern's source, used to disambiguate
|
||||
secondary patterns
|
||||
"""
|
||||
if key in self.primary:
|
||||
raise LibraryError(f'Promoting ({key}, {tag}), but {key} already exists in primary!')
|
||||
|
||||
key2 = (key, tag)
|
||||
pg = self.secondary[key2]
|
||||
self.primary[key] = pg
|
||||
if key2 in self.cache:
|
||||
self.cache[key] = self.cache[key2]
|
||||
del self.secondary[key2]
|
||||
del self.cache[key2]
|
||||
|
||||
def copy(self, preserve_cache: bool = False) -> 'Library':
|
||||
"""
|
||||
Create a copy of this `Library`.
|
||||
|
||||
A shallow copy is made of the contained dicts.
|
||||
Note that you should probably clear the cache (with `clear_cache()`) after copying.
|
||||
|
||||
Returns:
|
||||
A copy of self
|
||||
"""
|
||||
new = Library()
|
||||
new.primary.update(self.primary)
|
||||
new.secondary.update(self.secondary)
|
||||
new.cache.update(self.cache)
|
||||
return new
|
||||
|
||||
def clear_cache(self: L) -> L:
|
||||
"""
|
||||
Clear the cache of this library.
|
||||
This is usually used before modifying or deleting cells, e.g. when merging
|
||||
with another library.
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
self.cache = {}
|
||||
return self
|
||||
|
||||
|
||||
r"""
|
||||
# Add a filter for names which aren't added
|
||||
|
||||
- Registration:
|
||||
- scanned files (tag=filename, gen_fn[stream, {name: pos}])
|
||||
- generator functions (tag='fn?', gen_fn[params])
|
||||
- merge decision function (based on tag and cell name, can be "neither") ??? neither=keep both, load using same tag!
|
||||
- Load process:
|
||||
- file:
|
||||
- read single cell
|
||||
- check subpat identifiers, and load stuff recursively based on those. If not present, load from same file??
|
||||
- function:
|
||||
- generate cell
|
||||
- traverse and check if we should load any subcells from elsewhere. replace if so.
|
||||
* should fn generate subcells at all, or register those separately and have us control flow? maybe ask us and generate itself if not present?
|
||||
|
||||
- Scan all GDS files, save name -> (file, position). Keep the streams handy.
|
||||
- Merge all names. This requires subcell merge because we don't know hierarchy.
|
||||
- possibly include a "neither" option during merge, to deal with subcells. Means: just use parent's file.
|
||||
"""
|
Loading…
Reference in New Issue