snapshot 2020-12-20 18:25:34.796266
This commit is contained in:
commit
fc4d68b9f2
298
examples/pcgen.py
Normal file
298
examples/pcgen.py
Normal file
@ -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
|
178
examples/phc.py
Normal file
178
examples/phc.py
Normal file
@ -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 +0,0 @@
|
|||||||
2.2
|
|
4
masque/VERSION.py
Normal file
4
masque/VERSION.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
""" VERSION defintion. THIS FILE IS MANUALLY PARSED BY setup.py and REQUIRES A SPECIFIC FORMAT """
|
||||||
|
__version__ = '''
|
||||||
|
2.4
|
||||||
|
'''
|
@ -2,9 +2,9 @@
|
|||||||
masque 2D CAD library
|
masque 2D CAD library
|
||||||
|
|
||||||
masque is an attempt to make a relatively small library for designing lithography
|
masque is an attempt to make a relatively small library for designing lithography
|
||||||
masks. The general idea is to implement something resembling the GDSII file-format, but
|
masks. The general idea is to implement something resembling the GDSII and OASIS file-formats,
|
||||||
with some vectorized element types (eg. circles, not just polygons), better support for
|
but with some additional vectorized element types (eg. ellipses, not just polygons), better
|
||||||
E-beam doses, and the ability to output to multiple formats.
|
support for E-beam doses, and the ability to interface with multiple file formats.
|
||||||
|
|
||||||
`Pattern` is a basic object containing a 2D lithography mask, composed of a list of `Shape`
|
`Pattern` is a basic object containing a 2D lithography mask, composed of a list of `Shape`
|
||||||
objects, a list of `Label` objects, and a list of references to other `Patterns` (using
|
objects, a list of `Label` objects, and a list of references to other `Patterns` (using
|
||||||
@ -15,9 +15,18 @@
|
|||||||
|
|
||||||
Note that the methods for these classes try to avoid copying wherever possible, so unless
|
Note that the methods for these classes try to avoid copying wherever possible, so unless
|
||||||
otherwise noted, assume that arguments are stored by-reference.
|
otherwise noted, assume that arguments are stored by-reference.
|
||||||
"""
|
|
||||||
|
|
||||||
import pathlib
|
|
||||||
|
NOTES ON INTERNALS
|
||||||
|
==========================
|
||||||
|
- Many of `masque`'s classes make use of `__slots__` to make them faster / smaller.
|
||||||
|
Since `__slots__` doesn't play well with multiple inheritance, the `masque.utils.AutoSlots`
|
||||||
|
metaclass is used to auto-generate slots based on superclass type annotations.
|
||||||
|
- File I/O submodules are imported by `masque.file` to avoid creating hard dependencies on
|
||||||
|
external file-format reader/writers
|
||||||
|
- Pattern locking/unlocking is quite slow for large hierarchies.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
from .error import PatternError, PatternLockedError
|
from .error import PatternError, PatternLockedError
|
||||||
from .shapes import Shape
|
from .shapes import Shape
|
||||||
@ -25,11 +34,10 @@ from .label import Label
|
|||||||
from .subpattern import SubPattern
|
from .subpattern import SubPattern
|
||||||
from .pattern import Pattern
|
from .pattern import Pattern
|
||||||
from .utils import layer_t, annotations_t
|
from .utils import layer_t, annotations_t
|
||||||
from .library import Library
|
from .library import Library, DeviceLibrary
|
||||||
|
|
||||||
|
|
||||||
__author__ = 'Jan Petykiewicz'
|
__author__ = 'Jan Petykiewicz'
|
||||||
|
|
||||||
with open(pathlib.Path(__file__).parent / 'VERSION', 'r') as f:
|
from .VERSION import __version__
|
||||||
__version__ = f.read().strip()
|
version = __version__ # legacy
|
||||||
version = __version__
|
|
||||||
|
2
masque/builder/__init__.py
Normal file
2
masque/builder/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
from .devices import Port, Device
|
||||||
|
from .utils import ell
|
726
masque/builder/devices.py
Normal file
726
masque/builder/devices.py
Normal file
@ -0,0 +1,726 @@
|
|||||||
|
from typing import Dict, Iterable, List, Tuple, Union, TypeVar, Any, Iterator, Optional, Sequence
|
||||||
|
import copy
|
||||||
|
import warnings
|
||||||
|
import logging
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
import numpy # type: ignore
|
||||||
|
from numpy import pi
|
||||||
|
|
||||||
|
from ..pattern import Pattern
|
||||||
|
from ..subpattern import SubPattern
|
||||||
|
from ..traits import PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable
|
||||||
|
from ..utils import AutoSlots, rotation_matrix_2d, vector2
|
||||||
|
from ..error import DeviceError
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
P = TypeVar('P', bound='Port')
|
||||||
|
D = TypeVar('D', bound='Device')
|
||||||
|
O = TypeVar('O', bound='Device')
|
||||||
|
|
||||||
|
|
||||||
|
class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable, metaclass=AutoSlots):
|
||||||
|
"""
|
||||||
|
A point at which a `Device` can be snapped to another `Device`.
|
||||||
|
|
||||||
|
Each port has an `offset` ((x, y) position) and may also have a
|
||||||
|
`rotation` (orientation) and a `ptype` (port type).
|
||||||
|
|
||||||
|
The `rotation` is an angle, in radians, measured counterclockwise
|
||||||
|
from the +x axis, pointing inwards into the device which owns the port.
|
||||||
|
The rotation may be set to `None`, indicating that any orientation is
|
||||||
|
allowed (e.g. for a DC electrical port). It is stored modulo 2pi.
|
||||||
|
|
||||||
|
The `ptype` is an arbitrary integer, default of `0`.
|
||||||
|
"""
|
||||||
|
__slots__ = ('ptype', '_rotation')
|
||||||
|
|
||||||
|
_rotation: Optional[float]
|
||||||
|
""" radians counterclockwise from +x, pointing into device body.
|
||||||
|
Can be `None` to signify undirected port """
|
||||||
|
|
||||||
|
ptype: int
|
||||||
|
""" Port types must match to be plugged together if both are non-zero """
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
offset: numpy.ndarray,
|
||||||
|
rotation: Optional[float],
|
||||||
|
ptype: int = 0,
|
||||||
|
) -> None:
|
||||||
|
self.offset = offset
|
||||||
|
self.rotation = rotation
|
||||||
|
self.ptype = ptype
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rotation(self) -> Optional[float]:
|
||||||
|
""" Rotation, radians counterclockwise, pointing into device body. Can be None. """
|
||||||
|
return self._rotation
|
||||||
|
|
||||||
|
@rotation.setter
|
||||||
|
def rotation(self, val: float):
|
||||||
|
if val is None:
|
||||||
|
self._rotation = None
|
||||||
|
else:
|
||||||
|
if not numpy.size(val) == 1:
|
||||||
|
raise DeviceError('Rotation must be a scalar')
|
||||||
|
self._rotation = val % (2 * pi)
|
||||||
|
|
||||||
|
def get_bounds(self):
|
||||||
|
return numpy.vstack((self.offset, self.offset))
|
||||||
|
|
||||||
|
def set_ptype(self: P, ptype: int) -> P:
|
||||||
|
""" Chainable setter for `ptype` """
|
||||||
|
self.ptype = ptype
|
||||||
|
return self
|
||||||
|
|
||||||
|
def mirror(self: P, axis: int) -> P:
|
||||||
|
self.offset[1 - axis] *= -1
|
||||||
|
if self.rotation is not None:
|
||||||
|
self.rotation *= -1
|
||||||
|
self.rotation += axis * pi
|
||||||
|
return self
|
||||||
|
|
||||||
|
def rotate(self: P, rotation: float) -> P:
|
||||||
|
if self.rotation is not None:
|
||||||
|
self.rotation += rotation
|
||||||
|
return self
|
||||||
|
|
||||||
|
def set_rotation(self: P, rotation: Optional[float]) -> P:
|
||||||
|
self.rotation = rotation
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
if self.rotation is None:
|
||||||
|
rot = 'any'
|
||||||
|
else:
|
||||||
|
rot = str(numpy.rad2deg(self.rotation))
|
||||||
|
return f'<{self.offset}, {rot}, [{self.ptype}]>'
|
||||||
|
|
||||||
|
|
||||||
|
class Device(Copyable, Mirrorable):
|
||||||
|
"""
|
||||||
|
A `Device` is a combination of a `Pattern` with a set of named `Port`s
|
||||||
|
which can be used to "snap" devices together to make complex layouts.
|
||||||
|
|
||||||
|
`Device`s can be as simple as one or two ports (e.g. an electrical pad
|
||||||
|
or wire), but can also be used to build and represent a large routed
|
||||||
|
layout (e.g. a logical block with multiple I/O connections or even a
|
||||||
|
full chip).
|
||||||
|
|
||||||
|
For convenience, ports can be read out using square brackets:
|
||||||
|
- `device['A'] == Port((0, 0), 0)`
|
||||||
|
- `device[['A', 'B']] == {'A': Port((0, 0), 0), 'B': Port((0, 0), pi)}`
|
||||||
|
|
||||||
|
Examples: Creating a Device
|
||||||
|
===========================
|
||||||
|
- `Device(pattern, ports={'A': port_a, 'C': port_c})` uses an existing
|
||||||
|
pattern and defines some ports.
|
||||||
|
|
||||||
|
- `Device(name='my_dev_name', ports=None)` makes a new empty pattern with
|
||||||
|
default ports ('A' and 'B', in opposite directions, at (0, 0)).
|
||||||
|
|
||||||
|
- `my_device.build('my_layout')` makes a new pattern and instantiates
|
||||||
|
`my_device` in it with offset (0, 0) as a base for further building.
|
||||||
|
|
||||||
|
- `my_device.as_interface('my_component', port_map=['A', 'B'])` makes a new
|
||||||
|
(empty) pattern, copies over ports 'A' and 'B' from `my_device`, and
|
||||||
|
creates additional ports 'in_A' and 'in_B' facing in the opposite
|
||||||
|
directions. This can be used to build a device which can plug into
|
||||||
|
`my_device` (using the 'in_*' ports) but which does not itself include
|
||||||
|
`my_device` as a subcomponent.
|
||||||
|
|
||||||
|
Examples: Adding to a Device
|
||||||
|
============================
|
||||||
|
- `my_device.plug(subdevice, {'A': 'C', 'B': 'B'}, map_out={'D': 'myport'})`
|
||||||
|
instantiates `subdevice` into `my_device`, plugging ports 'A' and 'B'
|
||||||
|
of `my_device` into ports 'C' and 'B' of `subdevice`. The connected ports
|
||||||
|
are removed and any unconnected ports from `subdevice` are added to
|
||||||
|
`my_device`. Port 'D' of `subdevice` (unconnected) is renamed to 'myport'.
|
||||||
|
|
||||||
|
- `my_device.plug(wire, {'myport': 'A'})` places port 'A' of `wire` at 'myport'
|
||||||
|
of `my_device`. If `wire` has only two ports (e.g. 'A' and 'B'), no `map_out`,
|
||||||
|
argument is provided, and the `inherit_name` argument is not explicitly
|
||||||
|
set to `False`, the unconnected port of `wire` is automatically renamed to
|
||||||
|
'myport'. This allows easy extension of existing ports without changing
|
||||||
|
their names or having to provide `map_out` each time `plug` is called.
|
||||||
|
|
||||||
|
- `my_device.place(pad, offset=(10, 10), rotation=pi / 2, port_map={'A': 'gnd'})`
|
||||||
|
instantiates `pad` at the specified (x, y) offset and with the specified
|
||||||
|
rotation, adding its ports to those of `my_device`. Port 'A' of `pad` is
|
||||||
|
renamed to 'gnd' so that further routing can use this signal or net name
|
||||||
|
rather than the port name on the original `pad` device.
|
||||||
|
"""
|
||||||
|
__slots__ = ('pattern', 'ports', '_dead')
|
||||||
|
|
||||||
|
pattern: Pattern
|
||||||
|
""" Layout of this device """
|
||||||
|
|
||||||
|
ports: Dict[str, Port]
|
||||||
|
""" Uniquely-named ports which can be used to snap to other Device instances"""
|
||||||
|
|
||||||
|
_dead: bool
|
||||||
|
""" If True, plug()/place() are skipped (for debugging)"""
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
pattern: Optional[Pattern] = None,
|
||||||
|
ports: Optional[Dict[str, Port]] = None,
|
||||||
|
*,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
If `ports` is `None`, two default ports ('A' and 'B') are created.
|
||||||
|
Both are placed at (0, 0) and have `ptype=0`, 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).
|
||||||
|
"""
|
||||||
|
if pattern is not None:
|
||||||
|
if name is not None:
|
||||||
|
raise DeviceError('Only one of `pattern` and `name` may be specified')
|
||||||
|
self.pattern = pattern
|
||||||
|
else:
|
||||||
|
if name is None:
|
||||||
|
raise DeviceError('Must specify either `pattern` or `name`')
|
||||||
|
self.pattern = Pattern(name=name)
|
||||||
|
|
||||||
|
if ports is None:
|
||||||
|
self.ports = {
|
||||||
|
'A': Port([0, 0], rotation=0, ptype=0),
|
||||||
|
'B': Port([0, 0], rotation=pi, ptype=0),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
self.ports = copy.deepcopy(ports)
|
||||||
|
|
||||||
|
self._dead = False
|
||||||
|
|
||||||
|
def __getitem__(self, key: Union[str, Iterable[str]]) -> numpy.ndarray:
|
||||||
|
"""
|
||||||
|
For convenience, ports can be read out using square brackets:
|
||||||
|
- `device['A'] == Port((0, 0), 0)`
|
||||||
|
- `device[['A', 'B']] == {'A': Port((0, 0), 0),
|
||||||
|
'B': Port((0, 0), pi)}`
|
||||||
|
"""
|
||||||
|
if isinstance(key, str):
|
||||||
|
return self.ports[key]
|
||||||
|
else:
|
||||||
|
return {k: self.ports[k] for k in key}
|
||||||
|
|
||||||
|
def rename_ports(self: D,
|
||||||
|
mapping: Dict[str, Optional[str]],
|
||||||
|
overwrite: bool = False,
|
||||||
|
) -> D:
|
||||||
|
"""
|
||||||
|
Renames ports as specified by `mapping`.
|
||||||
|
Ports can be explicitly deleted by mapping them to `None`.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mapping: Dict of `{'old_name': 'new_name'}` pairs. Names can be mapped
|
||||||
|
to `None` to perform an explicit deletion. `'new_name'` can also
|
||||||
|
overwrite an existing non-renamed port to implicitly delete it if
|
||||||
|
`overwrite` is set to `True`.
|
||||||
|
overwrite: Allows implicit deletion of ports if set to `True`; see `mapping`.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
"""
|
||||||
|
if not overwrite:
|
||||||
|
duplicates = (set(self.ports.keys()) - set(mapping.keys())) & set(mapping.values())
|
||||||
|
if duplicates:
|
||||||
|
raise DeviceError(f'Unrenamed ports would be overwritten: {duplicates}')
|
||||||
|
|
||||||
|
renamed = {mapping[k]: self.ports.pop(k) for k in mapping.keys()}
|
||||||
|
if None in renamed:
|
||||||
|
del renamed[None]
|
||||||
|
|
||||||
|
self.ports.update(renamed) # type: ignore
|
||||||
|
return self
|
||||||
|
|
||||||
|
def check_ports(self: D,
|
||||||
|
other_names: Iterable[str],
|
||||||
|
map_in: Optional[Dict[str, str]] = None,
|
||||||
|
map_out: Optional[Dict[str, Optional[str]]] = None,
|
||||||
|
) -> D:
|
||||||
|
"""
|
||||||
|
Given the provided port mappings, check that:
|
||||||
|
- All of the ports specified in the mappings exist
|
||||||
|
- There are no duplicate port names after all the mappings are performed
|
||||||
|
|
||||||
|
Args:
|
||||||
|
other_names: List of port names being considered for inclusion into
|
||||||
|
`self.ports` (before mapping)
|
||||||
|
map_in: Dict of `{'self_port': 'other_port'}` mappings, specifying
|
||||||
|
port connections between the two devices.
|
||||||
|
map_out: Dict of `{'old_name': 'new_name'}` mappings, specifying
|
||||||
|
new names for unconnected `other_names` ports.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
`DeviceError` if any ports specified in `map_in` or `map_out` do not
|
||||||
|
exist in `self.ports` or `other_names`.
|
||||||
|
`DeviceError` if there are any duplicate names after `map_in` and `map_out`
|
||||||
|
are applied.
|
||||||
|
"""
|
||||||
|
if map_in is None:
|
||||||
|
map_in = {}
|
||||||
|
|
||||||
|
if map_out is None:
|
||||||
|
map_out = {}
|
||||||
|
|
||||||
|
other = set(other_names)
|
||||||
|
|
||||||
|
missing_inkeys = set(map_in.keys()) - set(self.ports.keys())
|
||||||
|
if missing_inkeys:
|
||||||
|
raise DeviceError(f'`map_in` keys not present in device: {missing_inkeys}')
|
||||||
|
|
||||||
|
missing_invals = set(map_in.values()) - other
|
||||||
|
if missing_invals:
|
||||||
|
raise DeviceError(f'`map_in` values not present in other device: {missing_invals}')
|
||||||
|
|
||||||
|
missing_outkeys = set(map_out.keys()) - other
|
||||||
|
if missing_outkeys:
|
||||||
|
raise DeviceError(f'`map_out` keys not present in other device: {missing_outkeys}')
|
||||||
|
|
||||||
|
orig_remaining = set(self.ports.keys()) - set(map_in.keys())
|
||||||
|
other_remaining = other - set(map_out.keys()) - set(map_in.values())
|
||||||
|
mapped_vals = set(map_out.values())
|
||||||
|
mapped_vals.discard(None)
|
||||||
|
|
||||||
|
conflicts_final = orig_remaining & (other_remaining | mapped_vals)
|
||||||
|
if conflicts_final:
|
||||||
|
raise DeviceError(f'Device ports conflict with existing ports: {conflicts_final}')
|
||||||
|
|
||||||
|
conflicts_partial = other_remaining & mapped_vals
|
||||||
|
if conflicts_partial:
|
||||||
|
raise DeviceError(f'`map_out` targets conflict with non-mapped outputs: {conflicts_partial}')
|
||||||
|
|
||||||
|
map_out_counts = Counter(map_out.values())
|
||||||
|
map_out_counts[None] = 0
|
||||||
|
conflicts_out = {k for k, v in map_out_counts.items() if v > 1}
|
||||||
|
if conflicts_out:
|
||||||
|
raise DeviceError(f'Duplicate targets in `map_out`: {conflicts_out}')
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def build(self, name: str) -> 'Device':
|
||||||
|
"""
|
||||||
|
Begin building a new device around an instance of the current device
|
||||||
|
(rather than modifying the current device).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: A name for the new device
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The new `Device` object.
|
||||||
|
"""
|
||||||
|
pat = Pattern(name)
|
||||||
|
pat.addsp(self.pattern)
|
||||||
|
new = Device(pat, ports=self.ports)
|
||||||
|
return new
|
||||||
|
|
||||||
|
def as_interface(self,
|
||||||
|
name: str,
|
||||||
|
in_prefix: str = 'in_',
|
||||||
|
out_prefix: str = '',
|
||||||
|
port_map: Optional[Union[Dict[str, str], Sequence[str]]] = None
|
||||||
|
) -> 'Device':
|
||||||
|
"""
|
||||||
|
Begin building a new device based on all or some of the ports in the
|
||||||
|
current device. Do not include the current device; instead use it
|
||||||
|
to define ports (the "interface") for the new device.
|
||||||
|
|
||||||
|
The ports specified by `port_map` (default: all ports) are copied to
|
||||||
|
new device, and additional (input) ports are created facing in the
|
||||||
|
opposite directions. The specified `in_prefix` and `out_prefix` are
|
||||||
|
prepended to the port names to differentiate them.
|
||||||
|
|
||||||
|
By default, the flipped ports are given an 'in_' prefix and unflipped
|
||||||
|
ports keep their original names, enabling intuitive construction of
|
||||||
|
a device that will "plug into" the current device; the 'in_*' ports
|
||||||
|
are used for plugging the devices together while the original port
|
||||||
|
names are used for building the new device.
|
||||||
|
|
||||||
|
Another use-case could be to build the new device using the 'in_'
|
||||||
|
ports, creating a new device which could be used in place of the
|
||||||
|
current device.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Name for the new device
|
||||||
|
in_prefix: Prepended to port names for newly-created ports with
|
||||||
|
reversed directions compared to the current device.
|
||||||
|
out_prefix: Prepended to port names for ports which are directly
|
||||||
|
copied from the current device.
|
||||||
|
port_map: Specification for ports to copy into the new device:
|
||||||
|
- If `None`, all ports are copied.
|
||||||
|
- If a sequence, only the listed ports are copied
|
||||||
|
- If a mapping, the listed ports (keys) are copied and
|
||||||
|
renamed (to the values).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The new device, with an empty pattern and 2x as many ports as
|
||||||
|
listed in port_map.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
`DeviceError` if `port_map` contains port names not present in the
|
||||||
|
current device.
|
||||||
|
`DeviceError` if applying the prefixes results in duplicate port
|
||||||
|
names.
|
||||||
|
"""
|
||||||
|
if port_map:
|
||||||
|
if isinstance(port_map, dict):
|
||||||
|
missing_inkeys = set(port_map.keys()) - set(self.ports.keys())
|
||||||
|
orig_ports = {port_map[k]: v for k, v in self.ports.items() if k in port_map}
|
||||||
|
else:
|
||||||
|
port_set = set(port_map)
|
||||||
|
missing_inkeys = port_set - set(self.ports.keys())
|
||||||
|
orig_ports = {k: v for k, v in self.ports.items() if k in port_set}
|
||||||
|
|
||||||
|
if missing_inkeys:
|
||||||
|
raise DeviceError(f'`port_map` keys not present in device: {missing_inkeys}')
|
||||||
|
else:
|
||||||
|
orig_ports = self.ports
|
||||||
|
|
||||||
|
ports_in = {f'{in_prefix}{name}': port.deepcopy().rotate(pi)
|
||||||
|
for name, port in orig_ports.items()}
|
||||||
|
ports_out = {f'{out_prefix}{name}': port.deepcopy()
|
||||||
|
for name, port in orig_ports.items()}
|
||||||
|
|
||||||
|
duplicates = set(ports_out.keys()) & set(ports_in.keys())
|
||||||
|
if duplicates:
|
||||||
|
raise DeviceError(f'Duplicate keys after prefixing, try a different prefix: {duplicates}')
|
||||||
|
|
||||||
|
new = Device(name=name, ports={**ports_in, **ports_out})
|
||||||
|
return new
|
||||||
|
|
||||||
|
def plug(self: D,
|
||||||
|
other: O,
|
||||||
|
map_in: Dict[str, str],
|
||||||
|
map_out: Optional[Dict[str, Optional[str]]] = None,
|
||||||
|
*,
|
||||||
|
mirrored: Tuple[bool, bool] = (False, False),
|
||||||
|
inherit_name: bool = True,
|
||||||
|
set_rotation: Optional[bool] = None,
|
||||||
|
) -> D:
|
||||||
|
"""
|
||||||
|
Instantiate the device `other` into the current device, connecting
|
||||||
|
the ports specified by `map_in` and renaming the unconnected
|
||||||
|
ports specified by `map_out`.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
=========
|
||||||
|
- `my_device.plug(subdevice, {'A': 'C', 'B': 'B'}, map_out={'D': 'myport'})`
|
||||||
|
instantiates `subdevice` into `my_device`, plugging ports 'A' and 'B'
|
||||||
|
of `my_device` into ports 'C' and 'B' of `subdevice`. The connected ports
|
||||||
|
are removed and any unconnected ports from `subdevice` are added to
|
||||||
|
`my_device`. Port 'D' of `subdevice` (unconnected) is renamed to 'myport'.
|
||||||
|
|
||||||
|
- `my_device.plug(wire, {'myport': 'A'})` places port 'A' of `wire` at 'myport'
|
||||||
|
of `my_device`. If `wire` has only two ports (e.g. 'A' and 'B'), no `map_out`,
|
||||||
|
argument is provided, and the `inherit_name` argument is not explicitly
|
||||||
|
set to `False`, the unconnected port of `wire` is automatically renamed to
|
||||||
|
'myport'. This allows easy extension of existing ports without changing
|
||||||
|
their names or having to provide `map_out` each time `plug` is called.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
other: A device to instantiate into the current device.
|
||||||
|
map_in: Dict of `{'self_port': 'other_port'}` mappings, specifying
|
||||||
|
port connections between the two devices.
|
||||||
|
map_out: Dict of `{'old_name': 'new_name'}` mappings, specifying
|
||||||
|
new names for ports in `other`.
|
||||||
|
mirrored: Enables mirroring `other` across the x or y axes prior
|
||||||
|
to connecting any ports.
|
||||||
|
inherit_name: If `True`, and `map_in` specifies only a single port,
|
||||||
|
and `map_out` is `None`, and `other` has only two ports total,
|
||||||
|
then automatically renames the output port of `other` to the
|
||||||
|
name of the port from `self` that appears in `map_in`. This
|
||||||
|
makes it easy to extend a device with simple 2-port devices
|
||||||
|
(e.g. wires) without providing `map_out` each time `plug` is
|
||||||
|
called. See "Examples" above for more info. Default `True`.
|
||||||
|
set_rotation: If the necessary rotation cannot be determined from
|
||||||
|
the ports being connected (i.e. all pairs have at least one
|
||||||
|
port with `rotation=None`), `set_rotation` must be provided
|
||||||
|
to indicate how much `other` should be rotated. Otherwise,
|
||||||
|
`set_rotation` must remain `None`.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
`DeviceError` if any ports specified in `map_in` or `map_out` do not
|
||||||
|
exist in `self.ports` or `other_names`.
|
||||||
|
`DeviceError` if there are any duplicate names after `map_in` and `map_out`
|
||||||
|
are applied.
|
||||||
|
`DeviceError` if the specified port mapping is not achieveable (the ports
|
||||||
|
do not line up)
|
||||||
|
"""
|
||||||
|
if self._dead:
|
||||||
|
logger.error('Skipping plug() since device is dead')
|
||||||
|
return self
|
||||||
|
|
||||||
|
if (inherit_name
|
||||||
|
and not map_out
|
||||||
|
and len(map_in) == 1
|
||||||
|
and len(other.ports) == 2):
|
||||||
|
out_port_name = next(iter(set(other.ports.keys()) - set(map_in.values())))
|
||||||
|
map_out = {out_port_name: next(iter(map_in.keys()))}
|
||||||
|
|
||||||
|
if map_out is None:
|
||||||
|
map_out = {}
|
||||||
|
map_out = copy.deepcopy(map_out)
|
||||||
|
|
||||||
|
self.check_ports(other.ports.keys(), map_in, map_out)
|
||||||
|
translation, rotation, pivot = self.find_transform(other, map_in, mirrored=mirrored,
|
||||||
|
set_rotation=set_rotation)
|
||||||
|
|
||||||
|
# get rid of plugged ports
|
||||||
|
for ki, vi in map_in.items():
|
||||||
|
del self.ports[ki]
|
||||||
|
map_out[vi] = None
|
||||||
|
|
||||||
|
self.place(other, offset=translation, rotation=rotation, pivot=pivot,
|
||||||
|
mirrored=mirrored, port_map=map_out, skip_port_check=True)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def place(self: D,
|
||||||
|
other: O,
|
||||||
|
*,
|
||||||
|
offset: vector2 = (0, 0),
|
||||||
|
rotation: float = 0,
|
||||||
|
pivot: vector2 = (0, 0),
|
||||||
|
mirrored: Tuple[bool, bool] = (False, False),
|
||||||
|
port_map: Optional[Dict[str, Optional[str]]] = None,
|
||||||
|
skip_port_check: bool = False,
|
||||||
|
) -> D:
|
||||||
|
"""
|
||||||
|
Instantiate the device `other` into the current device, adding its
|
||||||
|
ports to those of the current device (but not connecting any ports).
|
||||||
|
|
||||||
|
Mirroring is applied before rotation; translation (`offset`) is applied last.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
=========
|
||||||
|
- `my_device.place(pad, offset=(10, 10), rotation=pi / 2, port_map={'A': 'gnd'})`
|
||||||
|
instantiates `pad` at the specified (x, y) offset and with the specified
|
||||||
|
rotation, adding its ports to those of `my_device`. Port 'A' of `pad` is
|
||||||
|
renamed to 'gnd' so that further routing can use this signal or net name
|
||||||
|
rather than the port name on the original `pad` device.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
other: A device to instantiate into the current device.
|
||||||
|
offset: Offset at which to place `other`. Default (0, 0).
|
||||||
|
rotation: Rotation applied to `other` before placement. Default 0.
|
||||||
|
pivot: Rotation is applied around this pivot point (default (0, 0)).
|
||||||
|
Rotation is applied prior to translation (`offset`).
|
||||||
|
mirrored: Whether `other` should be mirrored across the x and y axes.
|
||||||
|
Mirroring is applied before translation and rotation.
|
||||||
|
port_map: Dict of `{'old_name': 'new_name'}` mappings, specifying
|
||||||
|
new names for ports in `other`. New names can be `None`, which will
|
||||||
|
delete those ports.
|
||||||
|
skip_port_check: Can be used to skip the internal call to `check_ports`,
|
||||||
|
in case it has already been performed elsewhere.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
`DeviceError` if any ports specified in `map_in` or `map_out` do not
|
||||||
|
exist in `self.ports` or `other_names`.
|
||||||
|
`DeviceError` if there are any duplicate names after `map_in` and `map_out`
|
||||||
|
are applied.
|
||||||
|
"""
|
||||||
|
if self._dead:
|
||||||
|
logger.error('Skipping place() since device is dead')
|
||||||
|
return self
|
||||||
|
|
||||||
|
if port_map is None:
|
||||||
|
port_map = {}
|
||||||
|
|
||||||
|
if not skip_port_check:
|
||||||
|
self.check_ports(other.ports.keys(), map_in=None, map_out=port_map)
|
||||||
|
|
||||||
|
ports = {}
|
||||||
|
for name, port in other.ports.items():
|
||||||
|
new_name = port_map.get(name, name)
|
||||||
|
if new_name is None:
|
||||||
|
continue
|
||||||
|
ports[new_name] = port
|
||||||
|
|
||||||
|
for name, port in ports.items():
|
||||||
|
p = port.deepcopy()
|
||||||
|
p.mirror2d(mirrored)
|
||||||
|
p.rotate_around(pivot, rotation)
|
||||||
|
p.translate(offset)
|
||||||
|
self.ports[name] = p
|
||||||
|
|
||||||
|
sp = SubPattern(other.pattern, mirrored=mirrored)
|
||||||
|
sp.rotate_around(pivot, rotation)
|
||||||
|
sp.translate(offset)
|
||||||
|
self.pattern.subpatterns.append(sp)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def find_transform(self: D,
|
||||||
|
other: O,
|
||||||
|
map_in: Dict[str, str],
|
||||||
|
*,
|
||||||
|
mirrored: Tuple[bool, bool] = (False, False),
|
||||||
|
set_rotation: Optional[bool] = None,
|
||||||
|
) -> Tuple[numpy.ndarray, float, numpy.ndarray]:
|
||||||
|
"""
|
||||||
|
Given a device `other` and a mapping `map_in` specifying port connections,
|
||||||
|
find the transform which will correctly align the specified ports.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
other: a device
|
||||||
|
map_in: Dict of `{'self_port': 'other_port'}` mappings, specifying
|
||||||
|
port connections between the two devices.
|
||||||
|
mirrored: Mirrors `other` across the x or y axes prior to
|
||||||
|
connecting any ports.
|
||||||
|
set_rotation: If the necessary rotation cannot be determined from
|
||||||
|
the ports being connected (i.e. all pairs have at least one
|
||||||
|
port with `rotation=None`), `set_rotation` must be provided
|
||||||
|
to indicate how much `other` should be rotated. Otherwise,
|
||||||
|
`set_rotation` must remain `None`.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- The (x, y) translation (performed last)
|
||||||
|
- The rotation (radians, counterclockwise)
|
||||||
|
- The (x, y) pivot point for the rotation
|
||||||
|
|
||||||
|
The rotation should be performed before the translation.
|
||||||
|
"""
|
||||||
|
s_ports = self[map_in.keys()]
|
||||||
|
o_ports = other[map_in.values()]
|
||||||
|
|
||||||
|
s_offsets = numpy.array([p.offset for p in s_ports.values()])
|
||||||
|
o_offsets = numpy.array([p.offset for p in o_ports.values()])
|
||||||
|
s_types = numpy.array([p.ptype for p in s_ports.values()], dtype=int)
|
||||||
|
o_types = numpy.array([p.ptype for p in o_ports.values()], dtype=int)
|
||||||
|
|
||||||
|
s_rotations = numpy.array([p.rotation if p.rotation is not None else 0 for p in s_ports.values()])
|
||||||
|
o_rotations = numpy.array([p.rotation if p.rotation is not None else 0 for p in o_ports.values()])
|
||||||
|
s_has_rot = numpy.array([p.rotation is not None for p in s_ports.values()], dtype=bool)
|
||||||
|
o_has_rot = numpy.array([p.rotation is not None for p in o_ports.values()], dtype=bool)
|
||||||
|
has_rot = s_has_rot & o_has_rot
|
||||||
|
|
||||||
|
if mirrored[0]:
|
||||||
|
o_offsets[:, 1] *= -1
|
||||||
|
o_rotations *= -1
|
||||||
|
if mirrored[1]:
|
||||||
|
o_offsets[:, 0] *= -1
|
||||||
|
o_rotations *= -1
|
||||||
|
o_rotations += pi
|
||||||
|
|
||||||
|
type_conflicts = (s_types != o_types) & (s_types != 0) & (o_types != 0)
|
||||||
|
if type_conflicts.any():
|
||||||
|
ports = numpy.where(type_conflicts)
|
||||||
|
msg = 'Ports have conflicting types:\n'
|
||||||
|
for nn, (k, v) in enumerate(map_in.items()):
|
||||||
|
if type_conflicts[nn]:
|
||||||
|
msg += f'{k} | {s_types[nn]:g}:{o_types[nn]:g} | {v}\n'
|
||||||
|
warnings.warn(msg, stacklevel=2)
|
||||||
|
|
||||||
|
rotations = numpy.mod(s_rotations - o_rotations - pi, 2 * pi)
|
||||||
|
if not has_rot.any():
|
||||||
|
if set_rotation is None:
|
||||||
|
DeviceError('Must provide set_rotation if rotation is indeterminate')
|
||||||
|
rotations[:] = set_rotation
|
||||||
|
else:
|
||||||
|
rotations[~has_rot] = rotations[has_rot][0]
|
||||||
|
|
||||||
|
if not numpy.allclose(rotations[:1], rotations):
|
||||||
|
rot_deg = numpy.rad2deg(rotations)
|
||||||
|
msg = f'Port orientations do not match:\n'
|
||||||
|
for nn, (k, v) in enumerate(map_in.items()):
|
||||||
|
msg += f'{k} | {rot_deg[nn]:g} | {v}\n'
|
||||||
|
raise DeviceError(msg)
|
||||||
|
|
||||||
|
pivot = o_offsets[0].copy()
|
||||||
|
rotate_offsets_around(o_offsets, pivot, rotations[0])
|
||||||
|
translations = s_offsets - o_offsets
|
||||||
|
if not numpy.allclose(translations[:1], translations):
|
||||||
|
msg = f'Port translations do not match:\n'
|
||||||
|
for nn, (k, v) in enumerate(map_in.items()):
|
||||||
|
msg += f'{k} | {translations[nn]} | {v}\n'
|
||||||
|
raise DeviceError(msg)
|
||||||
|
|
||||||
|
return translations[0], rotations[0], o_offsets[0]
|
||||||
|
|
||||||
|
def translate(self: D, offset: vector2) -> D:
|
||||||
|
"""
|
||||||
|
Translate the pattern and all ports.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
offset: (x, y) distance to translate by
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
"""
|
||||||
|
self.pattern.translate_elements(offset)
|
||||||
|
for port in self.ports.values():
|
||||||
|
port.translate(offset)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def rotate_around(self: D, pivot: vector2, angle: float) -> D:
|
||||||
|
"""
|
||||||
|
Translate the pattern and all ports.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
offset: (x, y) distance to translate by
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
"""
|
||||||
|
self.pattern.rotate_around(pivot, angle)
|
||||||
|
for port in self.ports.values():
|
||||||
|
port.rotate_around(pivot, angle)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def mirror(self: D, axis: int) -> D:
|
||||||
|
"""
|
||||||
|
Translate the pattern and all ports across the specified axis.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
axis: Axis to mirror across (x=0, y=1)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
"""
|
||||||
|
self.pattern.mirror(axis)
|
||||||
|
for p in self.ports.values():
|
||||||
|
p.mirror(axis)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def set_dead(self: D) -> D:
|
||||||
|
"""
|
||||||
|
Disallows further changes through `plug()` or `place()`.
|
||||||
|
This is meant for debugging:
|
||||||
|
```
|
||||||
|
dev.plug(a, ...)
|
||||||
|
dev.set_dead() # added for debug purposes
|
||||||
|
dev.plug(b, ...) # usually raises an error, but now skipped
|
||||||
|
dev.plug(c, ...) # also skipped
|
||||||
|
dev.pattern.visualize() # shows the device as of the set_dead() call
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
"""
|
||||||
|
self._dead = True
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
s = f'<Device {self.pattern} ['
|
||||||
|
for name, port in self.ports.items():
|
||||||
|
s += f'\n\t{name}: {port}'
|
||||||
|
s += ']>'
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def rotate_offsets_around(offsets: numpy.ndarray, pivot: numpy.ndarray, angle: float) -> numpy.ndarray:
|
||||||
|
offsets -= pivot
|
||||||
|
offsets[:] = (rotation_matrix_2d(angle) @ offsets.T).T
|
||||||
|
offsets += pivot
|
||||||
|
return offsets
|
189
masque/builder/utils.py
Normal file
189
masque/builder/utils.py
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
from typing import Dict, Tuple, List, Optional, Union, Any, cast, Sequence
|
||||||
|
from pprint import pformat
|
||||||
|
|
||||||
|
import numpy # type: ignore
|
||||||
|
from numpy import pi
|
||||||
|
|
||||||
|
from .devices import Port
|
||||||
|
from ..utils import rotation_matrix_2d, vector2
|
||||||
|
from ..error import BuildError
|
||||||
|
|
||||||
|
|
||||||
|
def ell(ports: Dict[str, Port],
|
||||||
|
ccw: Optional[bool],
|
||||||
|
bound_type: str,
|
||||||
|
bound: Union[float, vector2],
|
||||||
|
*,
|
||||||
|
spacing: Optional[Union[float, numpy.ndarray]] = None,
|
||||||
|
set_rotation: Optional[float] = None,
|
||||||
|
) -> Dict[str, float]:
|
||||||
|
"""
|
||||||
|
Calculate extension for each port in order to build a 90-degree bend with the provided
|
||||||
|
channel spacing:
|
||||||
|
|
||||||
|
=A>---------------------------V turn direction: `ccw=False`
|
||||||
|
=B>-------------V |
|
||||||
|
=C>-----------------------V | |
|
||||||
|
=D=>----------------V | | |
|
||||||
|
|
||||||
|
|
||||||
|
x---x---x---x `spacing` (can be scalar or array)
|
||||||
|
|
||||||
|
<--------------> `bound_type='min_extension'`
|
||||||
|
<------> `'min_past_furthest'`
|
||||||
|
<--------------------------------> `'max_extension'`
|
||||||
|
x `'min_position'`
|
||||||
|
x `'max_position'`
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ports: `name: port` mapping. All ports should have the same rotation (or `None`). If
|
||||||
|
no port has a rotation specified, `set_rotation` must be provided.
|
||||||
|
ccw: Turn direction. `True` means counterclockwise, `False` means clockwise,
|
||||||
|
and `None` means no bend. If `None`, spacing must remain `None` or `0` (default),
|
||||||
|
Otherwise, spacing must be set to a non-`None` value.
|
||||||
|
bound_method: Method used for determining the travel distance; see diagram above.
|
||||||
|
Valid values are:
|
||||||
|
- 'min_extension' or 'emin':
|
||||||
|
The total extension value for the furthest-out port (B in the diagram).
|
||||||
|
- 'min_past_furthest':
|
||||||
|
The distance between furthest out-port (B) and the innermost bend (D's bend).
|
||||||
|
- 'max_extension' or 'emax':
|
||||||
|
The total extension value for the closest-in port (C in the diagram).
|
||||||
|
- 'min_position' or 'pmin':
|
||||||
|
The coordinate of the innermost bend (D's bend).
|
||||||
|
- 'max_position' or 'pmax':
|
||||||
|
The coordinate of the outermost bend (A's bend).
|
||||||
|
|
||||||
|
`bound` can also be a vector. If specifying an extension (e.g. 'min_extension',
|
||||||
|
'max_extension', 'min_past_furthest'), it sets independent limits along
|
||||||
|
the x- and y- axes. If specifying a position, it is projected onto
|
||||||
|
the extension direction.
|
||||||
|
|
||||||
|
bound_value: Value associated with `bound_type`, see above.
|
||||||
|
spacing: Distance between adjacent channels. Can be scalar, resulting in evenly
|
||||||
|
spaced channels, or a vector with length one less than `ports`, allowing
|
||||||
|
non-uniform spacing.
|
||||||
|
The ordering of the vector corresponds to the output order (DCBA in the
|
||||||
|
diagram above), *not* the order of `ports`.
|
||||||
|
set_rotation: If all ports have no specified rotation, this value is used
|
||||||
|
to set the extension direction. Otherwise it must remain `None`.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict of {port_name: distance_to_bend}
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
`BuildError` on bad inputs
|
||||||
|
`BuildError` if the requested bound is impossible
|
||||||
|
"""
|
||||||
|
if not ports:
|
||||||
|
raise BuildError('Empty port list passed to `ell()`')
|
||||||
|
|
||||||
|
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')
|
||||||
|
spacing = 0
|
||||||
|
elif spacing is None:
|
||||||
|
raise BuildError('Must provide spacing if a bend direction is specified')
|
||||||
|
|
||||||
|
has_rotation = numpy.array([p.rotation is not None for p in ports.values()], dtype=bool)
|
||||||
|
if has_rotation.any():
|
||||||
|
if set_rotation is not None:
|
||||||
|
raise BuildError('set_rotation must be None when ports have rotations!')
|
||||||
|
|
||||||
|
rotations = numpy.array([p.rotation if p.rotation is not None else 0
|
||||||
|
for p in ports.values()])
|
||||||
|
rotations[~has_rotation] = rotations[has_rotation][0]
|
||||||
|
|
||||||
|
if not numpy.allclose(rotations[0], rotations):
|
||||||
|
raise BuildError('Asked to find aggregation for ports that face in different directions:\n'
|
||||||
|
+ pformat({k: numpy.rad2deg(p.rotation) for k, p in ports.items()}))
|
||||||
|
else:
|
||||||
|
if set_rotation is not None:
|
||||||
|
raise BuildError('set_rotation must be specified if no ports have rotations!')
|
||||||
|
rotations = numpy.full_like(has_rotation, set_rotation, dtype=float)
|
||||||
|
|
||||||
|
direction = rotations[0] + pi # direction we want to travel in (+pi relative to port)
|
||||||
|
rot_matrix = rotation_matrix_2d(-direction)
|
||||||
|
|
||||||
|
# Rotate so are traveling in +x
|
||||||
|
orig_offsets = numpy.array([p.offset for p in ports.values()])
|
||||||
|
rot_offsets = (rot_matrix @ orig_offsets.T).T
|
||||||
|
|
||||||
|
y_order = ((-1 if ccw else 1) * rot_offsets[:, 1]).argsort()
|
||||||
|
y_ind = numpy.empty_like(y_order, dtype=int)
|
||||||
|
y_ind[y_order] = numpy.arange(y_ind.shape[0])
|
||||||
|
|
||||||
|
if spacing is None:
|
||||||
|
ch_offsets = numpy.zeros_like(y_order)
|
||||||
|
else:
|
||||||
|
steps = numpy.zeros_like(y_order)
|
||||||
|
steps[1:] = spacing
|
||||||
|
ch_offsets = numpy.cumsum(steps)[y_ind]
|
||||||
|
|
||||||
|
x_start = rot_offsets[:, 0]
|
||||||
|
|
||||||
|
# A---------| `d_to_align[0]`
|
||||||
|
# B `d_to_align[1]`
|
||||||
|
# C-------------| `d_to_align[2]`
|
||||||
|
# D-----------| `d_to_align[3]`
|
||||||
|
#
|
||||||
|
d_to_align = x_start.max() - x_start # distance to travel to align all
|
||||||
|
if bound_type == 'min_past_furthest':
|
||||||
|
# A------------------V `d_to_exit[0]`
|
||||||
|
# B-----V `d_to_exit[1]`
|
||||||
|
# C----------------V `d_to_exit[2]`
|
||||||
|
# D-----------V `d_to_exit[3]`
|
||||||
|
offsets = d_to_align + ch_offsets
|
||||||
|
else:
|
||||||
|
# A---------V `travel[0]` <-- Outermost port aligned to furthest-x port
|
||||||
|
# V--B `travel[1]` <-- Remaining ports follow spacing
|
||||||
|
# C-------V `travel[2]`
|
||||||
|
# D--V `travel[3]`
|
||||||
|
#
|
||||||
|
# A------------V `offsets[0]`
|
||||||
|
# B `offsets[1]` <-- Travels adjusted to be non-negative
|
||||||
|
# C----------V `offsets[2]`
|
||||||
|
# D-----V `offsets[3]`
|
||||||
|
travel = d_to_align - (ch_offsets.max() - ch_offsets)
|
||||||
|
offsets = travel - travel.min().clip(max=0)
|
||||||
|
|
||||||
|
if bound_type in ('emin', 'min_extension',
|
||||||
|
'emax', 'max_extension',
|
||||||
|
'min_past_furthest',):
|
||||||
|
if numpy.size(bound) == 2:
|
||||||
|
bound = cast(Sequence[float], bound)
|
||||||
|
rot_bound = (rot_matrix @ ((bound[0], 0),
|
||||||
|
(0, bound[1])))[0, :]
|
||||||
|
else:
|
||||||
|
bound = cast(float, bound)
|
||||||
|
rot_bound = numpy.array(bound)
|
||||||
|
|
||||||
|
if rot_bound < 0:
|
||||||
|
raise BuildError(f'Got negative bound for extension: {rot_bound}')
|
||||||
|
|
||||||
|
if bound_type in ('emin', 'min_extension', 'min_past_furthest'):
|
||||||
|
offsets += rot_bound.max()
|
||||||
|
elif bound_type in('emax', 'max_extension'):
|
||||||
|
offsets += rot_bound.min() - offsets.max()
|
||||||
|
else:
|
||||||
|
if numpy.size(bound) == 2:
|
||||||
|
bound = cast(Sequence[float], bound)
|
||||||
|
rot_bound = (rot_matrix @ bound)[0]
|
||||||
|
else:
|
||||||
|
bound = cast(float, bound)
|
||||||
|
neg = (direction + pi / 4) % (2 * pi) > pi
|
||||||
|
rot_bound = -bound if neg else bound
|
||||||
|
|
||||||
|
min_possible = x_start + offsets
|
||||||
|
if bound_type in ('pmax', 'max_position'):
|
||||||
|
extension = rot_bound - min_possible.max()
|
||||||
|
elif bound_type in ('pmin', 'min_position'):
|
||||||
|
extension = rot_bound - min_possible.min()
|
||||||
|
|
||||||
|
offsets += extension
|
||||||
|
if extension < 0:
|
||||||
|
raise BuildError(f'Position is too close by at least {-numpy.floor(extension)}. Total extensions would be'
|
||||||
|
+ '\n\t'.join(f'{key}: {off}' for key, off in zip(ports.keys(), offsets)))
|
||||||
|
|
||||||
|
result = dict(zip(ports.keys(), offsets))
|
||||||
|
return result
|
@ -1,13 +1,15 @@
|
|||||||
class PatternError(Exception):
|
class MasqueError(Exception):
|
||||||
"""
|
"""
|
||||||
Simple Exception for Pattern objects and their contents
|
Parent exception for all Masque-related Exceptions
|
||||||
"""
|
"""
|
||||||
def __init__(self, value):
|
pass
|
||||||
self.value = value
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return repr(self.value)
|
|
||||||
|
|
||||||
|
class PatternError(MasqueError):
|
||||||
|
"""
|
||||||
|
Exception for Pattern objects and their contents
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
class PatternLockedError(PatternError):
|
class PatternLockedError(PatternError):
|
||||||
"""
|
"""
|
||||||
@ -17,10 +19,22 @@ class PatternLockedError(PatternError):
|
|||||||
PatternError.__init__(self, 'Tried to modify a locked Pattern, subpattern, or shape')
|
PatternError.__init__(self, 'Tried to modify a locked Pattern, subpattern, or shape')
|
||||||
|
|
||||||
|
|
||||||
class LibraryError(Exception):
|
class LibraryError(MasqueError):
|
||||||
"""
|
"""
|
||||||
Exception raised by Library classes
|
Exception raised by Library classes
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceError(MasqueError):
|
||||||
|
"""
|
||||||
|
Exception raised by Device and Port objects
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BuildError(MasqueError):
|
||||||
|
"""
|
||||||
|
Exception raised by builder-related functions
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
@ -70,7 +70,7 @@ def write(pattern: Pattern,
|
|||||||
"""
|
"""
|
||||||
#TODO consider supporting DXF arcs?
|
#TODO consider supporting DXF arcs?
|
||||||
if disambiguate_func is None:
|
if disambiguate_func is None:
|
||||||
disambiguate_func = disambiguate_pattern_names
|
disambiguate_func = lambda pats: disambiguate_pattern_names(pats)
|
||||||
assert(disambiguate_func is not None)
|
assert(disambiguate_func is not None)
|
||||||
|
|
||||||
if not modify_originals:
|
if not modify_originals:
|
||||||
@ -349,7 +349,7 @@ def _mlayer2dxf(layer: layer_t) -> str:
|
|||||||
raise PatternError(f'Unknown layer type: {layer} ({type(layer)})')
|
raise PatternError(f'Unknown layer type: {layer} ({type(layer)})')
|
||||||
|
|
||||||
|
|
||||||
def disambiguate_pattern_names(patterns: Sequence[Pattern],
|
def disambiguate_pattern_names(patterns: Iterable[Pattern],
|
||||||
max_name_length: int = 32,
|
max_name_length: int = 32,
|
||||||
suffix_length: int = 6,
|
suffix_length: int = 6,
|
||||||
dup_warn_filter: Callable[[str], bool] = None, # If returns False, don't warn about this name
|
dup_warn_filter: Callable[[str], bool] = None, # If returns False, don't warn about this name
|
||||||
|
@ -188,6 +188,7 @@ def readfile(filename: Union[str, pathlib.Path],
|
|||||||
|
|
||||||
|
|
||||||
def read(stream: BinaryIO,
|
def read(stream: BinaryIO,
|
||||||
|
raw_mode: bool = True,
|
||||||
) -> Tuple[Dict[str, Pattern], Dict[str, Any]]:
|
) -> Tuple[Dict[str, Pattern], Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Read a gdsii file and translate it into a dict of Pattern objects. GDSII structures are
|
Read a gdsii file and translate it into a dict of Pattern objects. GDSII structures are
|
||||||
@ -202,12 +203,12 @@ def read(stream: BinaryIO,
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
stream: Stream to read from.
|
stream: Stream to read from.
|
||||||
|
raw_mode: If True, constructs shapes in raw mode, bypassing most data validation, Default True.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
- Dict of pattern_name:Patterns generated from GDSII structures
|
- Dict of pattern_name:Patterns generated from GDSII structures
|
||||||
- Dict of GDSII library info
|
- Dict of GDSII library info
|
||||||
"""
|
"""
|
||||||
raw_mode = True # Whether to construct shapes in raw mode (less error checking)
|
|
||||||
library_info = _read_header(stream)
|
library_info = _read_header(stream)
|
||||||
|
|
||||||
patterns = []
|
patterns = []
|
||||||
@ -253,7 +254,7 @@ def read_elements(stream: BinaryIO,
|
|||||||
stream: Seekable stream, positioned at a record boundary.
|
stream: Seekable stream, positioned at a record boundary.
|
||||||
Will be read until an ENDSTR record is consumed.
|
Will be read until an ENDSTR record is consumed.
|
||||||
name: Name of the resulting Pattern
|
name: Name of the resulting Pattern
|
||||||
raw_mode: If True, bypass per-shape consistency checking
|
raw_mode: If True, bypass per-shape data validation. Default True.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A pattern containing the elements that were read.
|
A pattern containing the elements that were read.
|
||||||
@ -551,6 +552,8 @@ def disambiguate_pattern_names(patterns: Sequence[Pattern],
|
|||||||
def load_library(stream: BinaryIO,
|
def load_library(stream: BinaryIO,
|
||||||
tag: str,
|
tag: str,
|
||||||
is_secondary: Optional[Callable[[str], bool]] = None,
|
is_secondary: Optional[Callable[[str], bool]] = None,
|
||||||
|
*,
|
||||||
|
full_load: bool = False,
|
||||||
) -> Tuple[Library, Dict[str, Any]]:
|
) -> Tuple[Library, Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Scan a GDSII stream to determine what structures are present, and create
|
Scan a GDSII stream to determine what structures are present, and create
|
||||||
@ -568,6 +571,10 @@ def load_library(stream: BinaryIO,
|
|||||||
True if the structure should only be used as a subcell
|
True if the structure should only be used as a subcell
|
||||||
and not appear in the main Library interface.
|
and not appear in the main Library interface.
|
||||||
Default always returns False.
|
Default always returns False.
|
||||||
|
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.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Library object, allowing for deferred load of structures.
|
Library object, allowing for deferred load of structures.
|
||||||
@ -576,12 +583,22 @@ def load_library(stream: BinaryIO,
|
|||||||
if is_secondary is None:
|
if is_secondary is None:
|
||||||
def is_secondary(k: str):
|
def is_secondary(k: str):
|
||||||
return False
|
return False
|
||||||
|
assert(is_secondary is not None)
|
||||||
|
|
||||||
stream.seek(0)
|
stream.seek(0)
|
||||||
|
lib = Library()
|
||||||
|
|
||||||
|
if full_load:
|
||||||
|
# Full load approach (immediately load everything)
|
||||||
|
patterns, library_info = read(stream)
|
||||||
|
for name, pattern in patterns.items():
|
||||||
|
lib.set_const(name, tag, pattern, secondary=is_secondary(name))
|
||||||
|
return lib, library_info
|
||||||
|
|
||||||
|
# Normal approach (scan and defer load)
|
||||||
library_info = _read_header(stream)
|
library_info = _read_header(stream)
|
||||||
structs = klamath.library.scan_structs(stream)
|
structs = klamath.library.scan_structs(stream)
|
||||||
|
|
||||||
lib = Library()
|
|
||||||
for name_bytes, pos in structs.items():
|
for name_bytes, pos in structs.items():
|
||||||
name = name_bytes.decode('ASCII')
|
name = name_bytes.decode('ASCII')
|
||||||
|
|
||||||
@ -597,7 +614,9 @@ def load_library(stream: BinaryIO,
|
|||||||
def load_libraryfile(filename: Union[str, pathlib.Path],
|
def load_libraryfile(filename: Union[str, pathlib.Path],
|
||||||
tag: str,
|
tag: str,
|
||||||
is_secondary: Optional[Callable[[str], bool]] = None,
|
is_secondary: Optional[Callable[[str], bool]] = None,
|
||||||
|
*,
|
||||||
use_mmap: bool = True,
|
use_mmap: bool = True,
|
||||||
|
full_load: bool = False,
|
||||||
) -> Tuple[Library, Dict[str, Any]]:
|
) -> Tuple[Library, Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Wrapper for `load_library()` that takes a filename or path instead of a stream.
|
Wrapper for `load_library()` that takes a filename or path instead of a stream.
|
||||||
@ -615,6 +634,7 @@ def load_libraryfile(filename: Union[str, pathlib.Path],
|
|||||||
of buffering. In the case of gzipped files, the file
|
of buffering. In the case of gzipped files, the file
|
||||||
is decompressed into a python `bytes` object in memory
|
is decompressed into a python `bytes` object in memory
|
||||||
and reopened as an `io.BytesIO` stream.
|
and reopened as an `io.BytesIO` stream.
|
||||||
|
full_load: If `True`, immediately loads all data. See `load_library`.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Library object, allowing for deferred load of structures.
|
Library object, allowing for deferred load of structures.
|
||||||
|
@ -674,7 +674,7 @@ def annotations_to_properties(annotations: annotations_t) -> List[fatrec.Propert
|
|||||||
for key, values in annotations.items():
|
for key, values in annotations.items():
|
||||||
vals = [AString(v) if isinstance(v, str) else v
|
vals = [AString(v) if isinstance(v, str) else v
|
||||||
for v in values]
|
for v in values]
|
||||||
properties.append(fatrec.Property(key, vals, is_standard=False))
|
properties.append(fatrec.Property(key, vals, is_standard=False)) # type: ignore
|
||||||
return properties
|
return properties
|
||||||
|
|
||||||
|
|
||||||
|
@ -58,11 +58,11 @@ class Label(PositionableImpl, LayerableImpl, LockableImpl, RepeatableImpl, Annot
|
|||||||
self.set_locked(locked)
|
self.set_locked(locked)
|
||||||
|
|
||||||
def __copy__(self: L) -> L:
|
def __copy__(self: L) -> L:
|
||||||
return Label(string=self.string,
|
return type(self)(string=self.string,
|
||||||
offset=self.offset.copy(),
|
offset=self.offset.copy(),
|
||||||
layer=self.layer,
|
layer=self.layer,
|
||||||
repetition=self.repetition,
|
repetition=self.repetition,
|
||||||
locked=self.locked)
|
locked=self.locked)
|
||||||
|
|
||||||
def __deepcopy__(self: L, memo: Dict = None) -> L:
|
def __deepcopy__(self: L, memo: Dict = None) -> L:
|
||||||
memo = {} if memo is None else memo
|
memo = {} if memo is None else memo
|
||||||
|
@ -1 +1,2 @@
|
|||||||
from .library import Library, PatternGenerator
|
from .library import Library, PatternGenerator
|
||||||
|
from .device_library import DeviceLibrary
|
||||||
|
105
masque/library/device_library.py
Normal file
105
masque/library/device_library.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
"""
|
||||||
|
DeviceLibrary class for managing unique name->device 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 ..error import LibraryError
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..builder import Device
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
L = TypeVar('L', bound='DeviceLibrary')
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceLibrary:
|
||||||
|
"""
|
||||||
|
This class is usually used to create a device library by mapping names to
|
||||||
|
functions which generate or load the relevant `Device` object as-needed.
|
||||||
|
|
||||||
|
The cache can be disabled by setting the `enable_cache` attribute to `False`.
|
||||||
|
"""
|
||||||
|
generators: Dict[str, Callable[[], 'Device']]
|
||||||
|
cache: Dict[Union[str, Tuple[str, str]], 'Device']
|
||||||
|
enable_cache: bool = True
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.generators = {}
|
||||||
|
self.cache = {}
|
||||||
|
|
||||||
|
def __setitem__(self, key: str, value: Callable[[], 'Device']) -> None:
|
||||||
|
self.generators[key] = value
|
||||||
|
if key in self.cache:
|
||||||
|
del self.cache[key]
|
||||||
|
|
||||||
|
def __delitem__(self, key: str) -> None:
|
||||||
|
del self.generators[key]
|
||||||
|
if key in self.cache:
|
||||||
|
del self.cache[key]
|
||||||
|
|
||||||
|
def __getitem__(self, key: str) -> 'Device':
|
||||||
|
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}')
|
||||||
|
dev = self.generators[key]()
|
||||||
|
self.cache[key] = dev
|
||||||
|
return dev
|
||||||
|
|
||||||
|
def __iter__(self) -> Iterator[str]:
|
||||||
|
return iter(self.keys())
|
||||||
|
|
||||||
|
def __contains__(self, key: str) -> bool:
|
||||||
|
return key in self.generators
|
||||||
|
|
||||||
|
def keys(self) -> Iterator[str]:
|
||||||
|
return iter(self.generators.keys())
|
||||||
|
|
||||||
|
def values(self) -> Iterator['Device']:
|
||||||
|
return iter(self[key] for key in self.keys())
|
||||||
|
|
||||||
|
def items(self) -> Iterator[Tuple[str, 'Device']]:
|
||||||
|
return iter((key, self[key]) for key in self.keys())
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return '<DeviceLibrary with keys ' + repr(list(self.generators.keys())) + '>'
|
||||||
|
|
||||||
|
def set_const(self, key: str, const: 'Device') -> None:
|
||||||
|
"""
|
||||||
|
Convenience function to avoid having to manually wrap
|
||||||
|
constant values into callables.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Lookup key, usually the device name
|
||||||
|
const: Device object to return
|
||||||
|
"""
|
||||||
|
self.generators[key] = lambda: const
|
||||||
|
|
||||||
|
def add(self: L, other: L) -> L:
|
||||||
|
"""
|
||||||
|
Add keys from another library into this one.
|
||||||
|
|
||||||
|
There must be no conflicting keys.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
other: The library to insert keys from
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
"""
|
||||||
|
conflicts = [key for key in other.generators
|
||||||
|
if key in self.generators]
|
||||||
|
if conflicts:
|
||||||
|
raise LibraryError('Duplicate keys encountered in library merge: ' + pformat(conflicts))
|
||||||
|
|
||||||
|
self.generators.update(other.generators)
|
||||||
|
self.cache.update(other.cache)
|
||||||
|
return self
|
@ -2,7 +2,7 @@
|
|||||||
Base object representing a lithography mask.
|
Base object representing a lithography mask.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import List, Callable, Tuple, Dict, Union, Set, Sequence, Optional, Type, overload
|
from typing import List, Callable, Tuple, Dict, Union, Set, Sequence, Optional, Type, overload, cast
|
||||||
from typing import MutableMapping, Iterable, TypeVar, Any
|
from typing import MutableMapping, Iterable, TypeVar, Any
|
||||||
import copy
|
import copy
|
||||||
import pickle
|
import pickle
|
||||||
@ -18,7 +18,8 @@ from .shapes import Shape, Polygon
|
|||||||
from .label import Label
|
from .label import Label
|
||||||
from .utils import rotation_matrix_2d, vector2, normalize_mirror, AutoSlots, annotations_t
|
from .utils import rotation_matrix_2d, vector2, normalize_mirror, AutoSlots, annotations_t
|
||||||
from .error import PatternError, PatternLockedError
|
from .error import PatternError, PatternLockedError
|
||||||
from .traits import LockableImpl, AnnotatableImpl, Scalable
|
from .traits import LockableImpl, AnnotatableImpl, Scalable, Mirrorable
|
||||||
|
from .traits import Rotatable, Positionable
|
||||||
|
|
||||||
|
|
||||||
visitor_function_t = Callable[['Pattern', Tuple['Pattern'], Dict, numpy.ndarray], 'Pattern']
|
visitor_function_t = Callable[['Pattern', Tuple['Pattern'], Dict, numpy.ndarray], 'Pattern']
|
||||||
@ -27,7 +28,7 @@ visitor_function_t = Callable[['Pattern', Tuple['Pattern'], Dict, numpy.ndarray]
|
|||||||
P = TypeVar('P', bound='Pattern')
|
P = TypeVar('P', bound='Pattern')
|
||||||
|
|
||||||
|
|
||||||
class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots):
|
class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
||||||
"""
|
"""
|
||||||
2D layout consisting of some set of shapes, labels, and references to other Pattern objects
|
2D layout consisting of some set of shapes, labels, and references to other Pattern objects
|
||||||
(via SubPattern). Shapes are assumed to inherit from masque.shapes.Shape or provide equivalent functions.
|
(via SubPattern). Shapes are assumed to inherit from masque.shapes.Shape or provide equivalent functions.
|
||||||
@ -91,7 +92,7 @@ class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots):
|
|||||||
self.name = name
|
self.name = name
|
||||||
self.set_locked(locked)
|
self.set_locked(locked)
|
||||||
|
|
||||||
def __copy__(self: P, memo: Dict = None) -> P:
|
def __copy__(self, memo: Dict = None) -> 'Pattern':
|
||||||
return Pattern(name=self.name,
|
return Pattern(name=self.name,
|
||||||
shapes=copy.deepcopy(self.shapes),
|
shapes=copy.deepcopy(self.shapes),
|
||||||
labels=copy.deepcopy(self.labels),
|
labels=copy.deepcopy(self.labels),
|
||||||
@ -99,7 +100,7 @@ class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots):
|
|||||||
annotations=copy.deepcopy(self.annotations),
|
annotations=copy.deepcopy(self.annotations),
|
||||||
locked=self.locked)
|
locked=self.locked)
|
||||||
|
|
||||||
def __deepcopy__(self: P, memo: Dict = None) -> P:
|
def __deepcopy__(self, memo: Dict = None) -> 'Pattern':
|
||||||
memo = {} if memo is None else memo
|
memo = {} if memo is None else memo
|
||||||
new = Pattern(
|
new = Pattern(
|
||||||
name=self.name,
|
name=self.name,
|
||||||
@ -130,12 +131,12 @@ class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots):
|
|||||||
self.labels += other_pattern.labels
|
self.labels += other_pattern.labels
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def subset(self: P,
|
def subset(self,
|
||||||
shapes_func: Callable[[Shape], bool] = None,
|
shapes_func: Callable[[Shape], bool] = None,
|
||||||
labels_func: Callable[[Label], bool] = None,
|
labels_func: Callable[[Label], bool] = None,
|
||||||
subpatterns_func: Callable[[SubPattern], bool] = None,
|
subpatterns_func: Callable[[SubPattern], bool] = None,
|
||||||
recursive: bool = False,
|
recursive: bool = False,
|
||||||
) -> P:
|
) -> 'Pattern':
|
||||||
"""
|
"""
|
||||||
Returns a Pattern containing only the entities (e.g. shapes) for which the
|
Returns a Pattern containing only the entities (e.g. shapes) for which the
|
||||||
given entity_func returns True.
|
given entity_func returns True.
|
||||||
@ -155,7 +156,7 @@ class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots):
|
|||||||
A Pattern containing all the shapes and subpatterns for which the parameter
|
A Pattern containing all the shapes and subpatterns for which the parameter
|
||||||
functions return True
|
functions return True
|
||||||
"""
|
"""
|
||||||
def do_subset(src: Optional[P]) -> Optional[P]:
|
def do_subset(src: Optional['Pattern']) -> Optional['Pattern']:
|
||||||
if src is None:
|
if src is None:
|
||||||
return None
|
return None
|
||||||
pat = Pattern(name=src.name)
|
pat = Pattern(name=src.name)
|
||||||
@ -175,10 +176,10 @@ class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots):
|
|||||||
assert(pat is not None)
|
assert(pat is not None)
|
||||||
return pat
|
return pat
|
||||||
|
|
||||||
def apply(self: P,
|
def apply(self,
|
||||||
func: Callable[[Optional[P]], Optional[P]],
|
func: Callable[[Optional['Pattern']], Optional['Pattern']],
|
||||||
memo: Optional[Dict[int, Optional[P]]] = None,
|
memo: Optional[Dict[int, Optional['Pattern']]] = None,
|
||||||
) -> Optional[P]:
|
) -> Optional['Pattern']:
|
||||||
"""
|
"""
|
||||||
Recursively apply func() to this pattern and any pattern it references.
|
Recursively apply func() to this pattern and any pattern it references.
|
||||||
func() is expected to take and return a Pattern.
|
func() is expected to take and return a Pattern.
|
||||||
@ -473,7 +474,7 @@ class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots):
|
|||||||
ids.update(pat.referenced_patterns_by_id())
|
ids.update(pat.referenced_patterns_by_id())
|
||||||
return ids
|
return ids
|
||||||
|
|
||||||
def referenced_patterns_by_name(self, **kwargs) -> List[Tuple[Optional[str], Optional['Pattern']]]:
|
def referenced_patterns_by_name(self, **kwargs: Any) -> List[Tuple[Optional[str], Optional['Pattern']]]:
|
||||||
"""
|
"""
|
||||||
Create a list of `(pat.name, pat)` tuples for all Pattern objects referenced by this
|
Create a list of `(pat.name, pat)` tuples for all Pattern objects referenced by this
|
||||||
Pattern (operates recursively on all referenced Patterns as well).
|
Pattern (operates recursively on all referenced Patterns as well).
|
||||||
@ -710,7 +711,7 @@ class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots):
|
|||||||
self
|
self
|
||||||
"""
|
"""
|
||||||
for entry in chain(self.shapes, self.subpatterns):
|
for entry in chain(self.shapes, self.subpatterns):
|
||||||
entry.rotate(rotation)
|
cast(Rotatable, entry).rotate(rotation)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def mirror_element_centers(self: P, axis: int) -> P:
|
def mirror_element_centers(self: P, axis: int) -> P:
|
||||||
@ -741,7 +742,7 @@ class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots):
|
|||||||
self
|
self
|
||||||
"""
|
"""
|
||||||
for entry in chain(self.shapes, self.subpatterns):
|
for entry in chain(self.shapes, self.subpatterns):
|
||||||
entry.mirror(axis)
|
cast(Mirrorable, entry).mirror(axis)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def mirror(self: P, axis: int) -> P:
|
def mirror(self: P, axis: int) -> P:
|
||||||
@ -803,14 +804,14 @@ class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots):
|
|||||||
and len(self.shapes) == 0
|
and len(self.shapes) == 0
|
||||||
and len(self.labels) == 0)
|
and len(self.labels) == 0)
|
||||||
|
|
||||||
def addsp(self, *args: Dict[str, Any], **kwargs: Dict[str, Any]):
|
def addsp(self: P, *args: Any, **kwargs: Any) -> P:
|
||||||
"""
|
"""
|
||||||
Convenience function which constructs a subpattern object and adds it
|
Convenience function which constructs a subpattern object and adds it
|
||||||
to this pattern.
|
to this pattern.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
*args: Passed to SubPattern()
|
*args: Passed to `SubPattern()`
|
||||||
**kwargs: Passed to SubPattern()
|
**kwargs: Passed to `SubPattern()`
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
@ -856,7 +857,7 @@ class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots):
|
|||||||
"""
|
"""
|
||||||
self.lock()
|
self.lock()
|
||||||
for ss in chain(self.shapes, self.labels):
|
for ss in chain(self.shapes, self.labels):
|
||||||
ss.lock()
|
ss.lock() # type: ignore # mypy struggles with multiple inheritance :(
|
||||||
for sp in self.subpatterns:
|
for sp in self.subpatterns:
|
||||||
sp.deeplock()
|
sp.deeplock()
|
||||||
return self
|
return self
|
||||||
@ -873,7 +874,7 @@ class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots):
|
|||||||
"""
|
"""
|
||||||
self.unlock()
|
self.unlock()
|
||||||
for ss in chain(self.shapes, self.labels):
|
for ss in chain(self.shapes, self.labels):
|
||||||
ss.unlock()
|
ss.unlock() # type: ignore # mypy struggles with multiple inheritance :(
|
||||||
for sp in self.subpatterns:
|
for sp in self.subpatterns:
|
||||||
sp.deepunlock()
|
sp.deepunlock()
|
||||||
return self
|
return self
|
||||||
|
@ -236,12 +236,16 @@ class Arc(Shape, metaclass=AutoSlots):
|
|||||||
if poly_max_arclen is not None:
|
if poly_max_arclen is not None:
|
||||||
n += [perimeter / poly_max_arclen]
|
n += [perimeter / poly_max_arclen]
|
||||||
num_points = int(round(max(n)))
|
num_points = int(round(max(n)))
|
||||||
thetas_inner = numpy.linspace(a_ranges[0][1], a_ranges[0][0], num_points, endpoint=True)
|
|
||||||
|
wh = self.width / 2.0
|
||||||
|
if wh == r0 and r0 == r1:
|
||||||
|
thetas_inner = [0] # Don't generate multiple vertices if we're at the origin
|
||||||
|
else:
|
||||||
|
thetas_inner = numpy.linspace(a_ranges[0][1], a_ranges[0][0], num_points, endpoint=True)
|
||||||
thetas_outer = numpy.linspace(a_ranges[1][0], a_ranges[1][1], num_points, endpoint=True)
|
thetas_outer = numpy.linspace(a_ranges[1][0], a_ranges[1][1], num_points, endpoint=True)
|
||||||
|
|
||||||
sin_th_i, cos_th_i = (numpy.sin(thetas_inner), numpy.cos(thetas_inner))
|
sin_th_i, cos_th_i = (numpy.sin(thetas_inner), numpy.cos(thetas_inner))
|
||||||
sin_th_o, cos_th_o = (numpy.sin(thetas_outer), numpy.cos(thetas_outer))
|
sin_th_o, cos_th_o = (numpy.sin(thetas_outer), numpy.cos(thetas_outer))
|
||||||
wh = self.width / 2.0
|
|
||||||
|
|
||||||
xs1 = (r0 + wh) * cos_th_o
|
xs1 = (r0 + wh) * cos_th_o
|
||||||
ys1 = (r1 + wh) * sin_th_o
|
ys1 = (r1 + wh) * sin_th_o
|
||||||
@ -326,6 +330,7 @@ class Arc(Shape, metaclass=AutoSlots):
|
|||||||
def mirror(self, axis: int) -> 'Arc':
|
def mirror(self, axis: int) -> 'Arc':
|
||||||
self.offset[axis - 1] *= -1
|
self.offset[axis - 1] *= -1
|
||||||
self.rotation *= -1
|
self.rotation *= -1
|
||||||
|
self.rotation += axis * pi
|
||||||
self.angles *= -1
|
self.angles *= -1
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
@ -183,6 +183,7 @@ class Ellipse(Shape, metaclass=AutoSlots):
|
|||||||
def mirror(self, axis: int) -> 'Ellipse':
|
def mirror(self, axis: int) -> 'Ellipse':
|
||||||
self.offset[axis - 1] *= -1
|
self.offset[axis - 1] *= -1
|
||||||
self.rotation *= -1
|
self.rotation *= -1
|
||||||
|
self.rotation += axis * pi
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def scale_by(self, c: float) -> 'Ellipse':
|
def scale_by(self, c: float) -> 'Ellipse':
|
||||||
|
@ -269,6 +269,64 @@ class Polygon(Shape, metaclass=AutoSlots):
|
|||||||
layer=layer, dose=dose)
|
layer=layer, dose=dose)
|
||||||
return poly
|
return poly
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def octagon(*,
|
||||||
|
side_length: Optional[float] = None,
|
||||||
|
inner_radius: Optional[float] = None,
|
||||||
|
regular: bool = True,
|
||||||
|
center: vector2 = (0.0, 0.0),
|
||||||
|
rotation: float = 0.0,
|
||||||
|
layer: layer_t = 0,
|
||||||
|
dose: float = 1.0,
|
||||||
|
) -> 'Polygon':
|
||||||
|
"""
|
||||||
|
Draw an octagon given one of (side length, inradius, circumradius).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
side_length: Length of one side. For an irregular octagon, this
|
||||||
|
specifies the length of the long sides.
|
||||||
|
inner_radius: Half of distance between opposite sides. For an irregular
|
||||||
|
octagon, this specifies the spacing between the long sides.
|
||||||
|
regular: If `True`, all sides have the same length. If `False`,
|
||||||
|
a "clipped square" with vertices (+-1, +-2) and (+-2, +-1)
|
||||||
|
is generated, avoiding irrational coordinate locations and
|
||||||
|
guaranteeing 45 degree edges.
|
||||||
|
center: Offset, default `(0, 0)`
|
||||||
|
rotation: Rotation counterclockwise, in radians.
|
||||||
|
`0` results in four axis-aligned sides (the long sides of the
|
||||||
|
irregular octagon).
|
||||||
|
layer: Layer, default `0`
|
||||||
|
dose: Dose, default `1.0`
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A Polygon object containing the requested octagon
|
||||||
|
"""
|
||||||
|
if regular:
|
||||||
|
s = 1 + numpy.sqrt(2)
|
||||||
|
else:
|
||||||
|
s = 2
|
||||||
|
|
||||||
|
norm_oct = numpy.array([
|
||||||
|
[-1, -s],
|
||||||
|
[-s, -1],
|
||||||
|
[-s, 1],
|
||||||
|
[-1, s],
|
||||||
|
[ 1, s],
|
||||||
|
[ s, 1],
|
||||||
|
[ s, -1],
|
||||||
|
[ 1, -s]], dtype=float)
|
||||||
|
|
||||||
|
if side_length is None:
|
||||||
|
if inner_radius is None:
|
||||||
|
raise PatternError('One of `side_length` or `inner_radius` must be specified.')
|
||||||
|
side_length = 2 * inner_radius / s
|
||||||
|
|
||||||
|
vertices = 0.5 * side_length * norm_oct
|
||||||
|
poly = Polygon(vertices, offset=center, layer=layer, dose=dose)
|
||||||
|
poly.rotate(rotation)
|
||||||
|
return poly
|
||||||
|
|
||||||
|
|
||||||
def to_polygons(self,
|
def to_polygons(self,
|
||||||
poly_num_points: int = None, # unused
|
poly_num_points: int = None, # unused
|
||||||
poly_max_arclen: float = None, # unused
|
poly_max_arclen: float = None, # unused
|
||||||
|
@ -121,7 +121,7 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots):
|
|||||||
# Move these polygons to the right of the previous letter
|
# Move these polygons to the right of the previous letter
|
||||||
for xys in raw_polys:
|
for xys in raw_polys:
|
||||||
poly = Polygon(xys, dose=self.dose, layer=self.layer)
|
poly = Polygon(xys, dose=self.dose, layer=self.layer)
|
||||||
[poly.mirror(ax) for ax, do in enumerate(self.mirrored) if do]
|
poly.mirror2d(self.mirrored)
|
||||||
poly.scale_by(self.height)
|
poly.scale_by(self.height)
|
||||||
poly.offset = self.offset + [total_advance, 0]
|
poly.offset = self.offset + [total_advance, 0]
|
||||||
poly.rotate_around(self.offset, self.rotation)
|
poly.rotate_around(self.offset, self.rotation)
|
||||||
|
@ -1,121 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
callback_t = Callable[[T, numpy.ndarray, Dict[str, int]], ...]
|
|
||||||
|
|
||||||
|
|
||||||
class Device:
|
|
||||||
pattern: Pattern
|
|
||||||
ports: numpy.ndarray
|
|
||||||
port_names: Dict[str, int]
|
|
||||||
callback: Optional[callback_t] = None
|
|
||||||
|
|
||||||
def __init__(self,
|
|
||||||
pattern: Optional[Pattern] = None,
|
|
||||||
ports: Optional[numpy.ndarray] = None,
|
|
||||||
names: Optional[Union[Dict[str, int], List[Optional[str]]] = None,
|
|
||||||
callback: Optional[callback_t]= None
|
|
||||||
) -> None:
|
|
||||||
if pattern is None:
|
|
||||||
self.pattern = Pattern()
|
|
||||||
|
|
||||||
if ports is None:
|
|
||||||
self.ports = {0: [0, 0, 0, 0],
|
|
||||||
1: [0, 0, pi, 0]}
|
|
||||||
else:
|
|
||||||
self.ports = numpy.array(ports)
|
|
||||||
|
|
||||||
if callback:
|
|
||||||
self.callback = callback
|
|
||||||
self.callback(self, self.ports.keys())
|
|
||||||
|
|
||||||
def __getitem__(self, key) -> numpy.ndarray:
|
|
||||||
if isinstance(key, str):
|
|
||||||
inds = [self.port_names[key]]
|
|
||||||
elif hasattr(key, '__iter__'):
|
|
||||||
inds = [self.port_names.get(k, k) for k in key]
|
|
||||||
else:
|
|
||||||
inds = [self.port_names.get(key, key)]
|
|
||||||
return self.ports[inds]
|
|
||||||
|
|
||||||
def build(self: T,
|
|
||||||
name: str,
|
|
||||||
other: Device,
|
|
||||||
map_in: Dict[port_t, port_t],
|
|
||||||
map_out: Dict[port_t, port_t],
|
|
||||||
mirror: bool = False,
|
|
||||||
) -> T:
|
|
||||||
translation, rotation = self.find_transform(other, map_in, map_out, mirror)
|
|
||||||
|
|
||||||
pat = Pattern(name)
|
|
||||||
pat.addsp(self.pattern)
|
|
||||||
new = Device(pat, ports=self.ports, port_names=self.port_names, callback=self.callback)
|
|
||||||
return new
|
|
||||||
|
|
||||||
|
|
||||||
def plug(self, other, map_in, map_out, mirror):
|
|
||||||
translation, rotation, pivot = self.find_transform(other, map_in, map_out, mirror)
|
|
||||||
|
|
||||||
sp = SubPattern(other.pattern, mirrored=mirror)
|
|
||||||
sp.rotate_around(pivot, rotation)
|
|
||||||
sp.translate(translation)
|
|
||||||
self.pat.subpatterns.append(sp)
|
|
||||||
|
|
||||||
# get rid of plugged ports
|
|
||||||
|
|
||||||
# insert remaining device ports into router port list
|
|
||||||
|
|
||||||
with numpy.errstate(invalid='ignore'):
|
|
||||||
self.ports[:, 2] %= 2 * pi
|
|
||||||
|
|
||||||
if self.port_callback:
|
|
||||||
self.port_callback(...)
|
|
||||||
|
|
||||||
def find_transform(self, other, map_in, map_out, mirror):
|
|
||||||
s_ports = self[map_in.keys()]
|
|
||||||
o_ports= other[map_in.values()]
|
|
||||||
|
|
||||||
if mirror[0]:
|
|
||||||
o_ports[:, 1] *= -1
|
|
||||||
o_ports[:, 2] += pi
|
|
||||||
if mirror[1]:
|
|
||||||
o_ports[:, 0] *= -1
|
|
||||||
o_ports[:, 2] += pi
|
|
||||||
|
|
||||||
s_offsets = s_ports[:, :2]
|
|
||||||
s_angles = s_ports[:, 2]
|
|
||||||
s_types = s_ports[:, 3]
|
|
||||||
o_offsets = o_ports[:, :2]
|
|
||||||
o_angles = o_ports[:, 2]
|
|
||||||
o_types = o_ports[:, 3]
|
|
||||||
|
|
||||||
if (r_types != d_types) & (r_types != 0) & (d_types != 0):
|
|
||||||
#TODO warn about types
|
|
||||||
|
|
||||||
rotations = numpy.mod(s_angles - o_angles - pi, 2 * pi)
|
|
||||||
|
|
||||||
if not numpy.allclose(rotations[:1], rotations):
|
|
||||||
# TODO warn about rotation
|
|
||||||
|
|
||||||
rot_ports = rotate_ports_around(o_ports, o_offsets[0], rotations[0]) #TODO also rotate unplugged device ports
|
|
||||||
|
|
||||||
translations = r_offsets - d_offsets
|
|
||||||
translation = translations[0]
|
|
||||||
|
|
||||||
if not numpy.allclose(translations[:1], translations):
|
|
||||||
|
|
||||||
return translations[0], rotations[0]
|
|
||||||
|
|
||||||
|
|
||||||
def as_pattern(self, name) -> Pattern:
|
|
||||||
return self.pat.copy().rename(name)
|
|
||||||
|
|
||||||
def as_device(self, name):
|
|
||||||
return Device(self.as_pattern, REMAINING_NON-NAN_PORTS) #TODO
|
|
||||||
|
|
||||||
|
|
||||||
def rotate_ports_around(ports: numpy.ndarray, pivot: numpy.ndarray, angle: float) -> numpy.ndarray:
|
|
||||||
ports[:, :2] -= pivot
|
|
||||||
ports[:, :2] = (rotation_matrix_2d(angle) @ ports[:, :2].T).T
|
|
||||||
ports[:, :2] += pivot
|
|
||||||
ports[:, 2] += angle
|
|
||||||
return ports
|
|
@ -79,13 +79,13 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
|
|||||||
self.dose = dose
|
self.dose = dose
|
||||||
self.scale = scale
|
self.scale = scale
|
||||||
if mirrored is None:
|
if mirrored is None:
|
||||||
mirrored = [False, False]
|
mirrored = (False, False)
|
||||||
self.mirrored = mirrored
|
self.mirrored = mirrored
|
||||||
self.repetition = repetition
|
self.repetition = repetition
|
||||||
self.annotations = annotations if annotations is not None else {}
|
self.annotations = annotations if annotations is not None else {}
|
||||||
self.set_locked(locked)
|
self.set_locked(locked)
|
||||||
|
|
||||||
def __copy__(self: S) -> S:
|
def __copy__(self) -> 'SubPattern':
|
||||||
new = SubPattern(pattern=self.pattern,
|
new = SubPattern(pattern=self.pattern,
|
||||||
offset=self.offset.copy(),
|
offset=self.offset.copy(),
|
||||||
rotation=self.rotation,
|
rotation=self.rotation,
|
||||||
@ -97,7 +97,7 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
|
|||||||
locked=self.locked)
|
locked=self.locked)
|
||||||
return new
|
return new
|
||||||
|
|
||||||
def __deepcopy__(self: S, memo: Dict = None) -> S:
|
def __deepcopy__(self, memo: Dict = None) -> 'SubPattern':
|
||||||
memo = {} if memo is None else memo
|
memo = {} if memo is None else memo
|
||||||
new = copy.copy(self).unlock()
|
new = copy.copy(self).unlock()
|
||||||
new.pattern = copy.deepcopy(self.pattern, memo)
|
new.pattern = copy.deepcopy(self.pattern, memo)
|
||||||
@ -138,7 +138,7 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
|
|||||||
assert(self.pattern is not None)
|
assert(self.pattern is not None)
|
||||||
pattern = self.pattern.deepcopy().deepunlock()
|
pattern = self.pattern.deepcopy().deepunlock()
|
||||||
pattern.scale_by(self.scale)
|
pattern.scale_by(self.scale)
|
||||||
[pattern.mirror(ax) for ax, do in enumerate(self.mirrored) if do]
|
pattern.mirror2d(self.mirrored)
|
||||||
pattern.rotate_around((0.0, 0.0), self.rotation)
|
pattern.rotate_around((0.0, 0.0), self.rotation)
|
||||||
pattern.translate_elements(self.offset)
|
pattern.translate_elements(self.offset)
|
||||||
pattern.scale_element_doses(self.dose)
|
pattern.scale_element_doses(self.dose)
|
||||||
|
@ -3,7 +3,7 @@ from typing import TypeVar
|
|||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
|
|
||||||
from ..utils import annotations_t
|
from ..utils import annotations_t
|
||||||
from ..error import PatternError
|
from ..error import MasqueError
|
||||||
|
|
||||||
|
|
||||||
T = TypeVar('T', bound='Annotatable')
|
T = TypeVar('T', bound='Annotatable')
|
||||||
@ -51,5 +51,5 @@ class AnnotatableImpl(Annotatable, metaclass=ABCMeta):
|
|||||||
@annotations.setter
|
@annotations.setter
|
||||||
def annotations(self, annotations: annotations_t):
|
def annotations(self, annotations: annotations_t):
|
||||||
if not isinstance(annotations, dict):
|
if not isinstance(annotations, dict):
|
||||||
raise PatternError(f'annotations expected dict, got {type(annotations)}')
|
raise MasqueError(f'annotations expected dict, got {type(annotations)}')
|
||||||
self._annotations = annotations
|
self._annotations = annotations
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
|
|
||||||
from ..error import PatternError
|
from ..error import MasqueError
|
||||||
|
|
||||||
|
|
||||||
T = TypeVar('T', bound='Doseable')
|
T = TypeVar('T', bound='Doseable')
|
||||||
@ -65,7 +65,7 @@ class DoseableImpl(Doseable, metaclass=ABCMeta):
|
|||||||
@dose.setter
|
@dose.setter
|
||||||
def dose(self, val: float):
|
def dose(self, val: float):
|
||||||
if not val >= 0:
|
if not val >= 0:
|
||||||
raise PatternError('Dose must be non-negative')
|
raise MasqueError('Dose must be non-negative')
|
||||||
self._dose = val
|
self._dose = val
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from typing import TypeVar
|
from typing import TypeVar, Tuple
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
|
|
||||||
|
|
||||||
@ -28,6 +28,22 @@ class Mirrorable(metaclass=ABCMeta):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def mirror2d(self: T, axes: Tuple[bool, bool]) -> T:
|
||||||
|
"""
|
||||||
|
Optionally mirror the entity across both axes
|
||||||
|
|
||||||
|
Args:
|
||||||
|
axes: (mirror_across_x, mirror_across_y)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
"""
|
||||||
|
if axes[0]:
|
||||||
|
self.mirror(0)
|
||||||
|
if axes[1]:
|
||||||
|
self.mirror(1)
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
#class MirrorableImpl(Mirrorable, metaclass=ABCMeta):
|
#class MirrorableImpl(Mirrorable, metaclass=ABCMeta):
|
||||||
# """
|
# """
|
||||||
@ -50,7 +66,7 @@ class Mirrorable(metaclass=ABCMeta):
|
|||||||
# @mirrored.setter
|
# @mirrored.setter
|
||||||
# def mirrored(self, val: Sequence[bool]):
|
# def mirrored(self, val: Sequence[bool]):
|
||||||
# if is_scalar(val):
|
# if is_scalar(val):
|
||||||
# raise PatternError('Mirrored must be a 2-element list of booleans')
|
# raise MasqueError('Mirrored must be a 2-element list of booleans')
|
||||||
# self._mirrored = numpy.array(val, dtype=bool, copy=True)
|
# self._mirrored = numpy.array(val, dtype=bool, copy=True)
|
||||||
#
|
#
|
||||||
# '''
|
# '''
|
||||||
|
@ -4,7 +4,7 @@ from typing import TypeVar
|
|||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
import numpy # type: ignore
|
import numpy # type: ignore
|
||||||
|
|
||||||
from ..error import PatternError
|
from ..error import MasqueError
|
||||||
from ..utils import vector2
|
from ..utils import vector2
|
||||||
|
|
||||||
|
|
||||||
@ -97,7 +97,7 @@ class PositionableImpl(Positionable, metaclass=ABCMeta):
|
|||||||
val = numpy.array(val, dtype=float)
|
val = numpy.array(val, dtype=float)
|
||||||
|
|
||||||
if val.size != 2:
|
if val.size != 2:
|
||||||
raise PatternError('Offset must be convertible to size-2 ndarray')
|
raise MasqueError('Offset must be convertible to size-2 ndarray')
|
||||||
self._offset = val.flatten()
|
self._offset = val.flatten()
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from typing import TypeVar, Optional, TYPE_CHECKING
|
from typing import TypeVar, Optional, TYPE_CHECKING
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
|
|
||||||
from ..error import PatternError
|
from ..error import MasqueError
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@ -71,7 +71,7 @@ class RepeatableImpl(Repeatable, metaclass=ABCMeta):
|
|||||||
def repetition(self, repetition: Optional['Repetition']):
|
def repetition(self, repetition: Optional['Repetition']):
|
||||||
from ..repetition import Repetition
|
from ..repetition import Repetition
|
||||||
if repetition is not None and not isinstance(repetition, Repetition):
|
if repetition is not None and not isinstance(repetition, Repetition):
|
||||||
raise PatternError(f'{repetition} is not a valid Repetition object!')
|
raise MasqueError(f'{repetition} is not a valid Repetition object!')
|
||||||
self._repetition = repetition
|
self._repetition = repetition
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
@ -5,7 +5,7 @@ import numpy # type: ignore
|
|||||||
from numpy import pi
|
from numpy import pi
|
||||||
|
|
||||||
#from .positionable import Positionable
|
#from .positionable import Positionable
|
||||||
from ..error import PatternError
|
from ..error import MasqueError
|
||||||
from ..utils import is_scalar, rotation_matrix_2d, vector2
|
from ..utils import is_scalar, rotation_matrix_2d, vector2
|
||||||
|
|
||||||
T = TypeVar('T', bound='Rotatable')
|
T = TypeVar('T', bound='Rotatable')
|
||||||
@ -24,12 +24,12 @@ class Rotatable(metaclass=ABCMeta):
|
|||||||
---- Abstract methods
|
---- Abstract methods
|
||||||
'''
|
'''
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def rotate(self: T, theta: float) -> T:
|
def rotate(self: T, val: float) -> T:
|
||||||
"""
|
"""
|
||||||
Rotate the shape around its origin (0, 0), ignoring its offset.
|
Rotate the shape around its origin (0, 0), ignoring its offset.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
theta: Angle to rotate by (counterclockwise, radians)
|
val: Angle to rotate by (counterclockwise, radians)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
@ -56,8 +56,8 @@ class RotatableImpl(Rotatable, metaclass=ABCMeta):
|
|||||||
|
|
||||||
@rotation.setter
|
@rotation.setter
|
||||||
def rotation(self, val: float):
|
def rotation(self, val: float):
|
||||||
if not is_scalar(val):
|
if not numpy.size(val) == 1:
|
||||||
raise PatternError('Rotation must be a scalar')
|
raise MasqueError('Rotation must be a scalar')
|
||||||
self._rotation = val % (2 * pi)
|
self._rotation = val % (2 * pi)
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
|
|
||||||
from ..error import PatternError
|
from ..error import MasqueError
|
||||||
from ..utils import is_scalar
|
from ..utils import is_scalar
|
||||||
|
|
||||||
|
|
||||||
@ -51,9 +51,9 @@ class ScalableImpl(Scalable, metaclass=ABCMeta):
|
|||||||
@scale.setter
|
@scale.setter
|
||||||
def scale(self, val: float):
|
def scale(self, val: float):
|
||||||
if not is_scalar(val):
|
if not is_scalar(val):
|
||||||
raise PatternError('Scale must be a scalar')
|
raise MasqueError('Scale must be a scalar')
|
||||||
if not val > 0:
|
if not val > 0:
|
||||||
raise PatternError('Scale must be positive')
|
raise MasqueError('Scale must be positive')
|
||||||
self._scale = val
|
self._scale = val
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
@ -120,12 +120,12 @@ def remove_colinear_vertices(vertices: numpy.ndarray, closed_path: bool = True)
|
|||||||
Returns:
|
Returns:
|
||||||
`vertices` with colinear (superflous) vertices removed.
|
`vertices` with colinear (superflous) vertices removed.
|
||||||
"""
|
"""
|
||||||
vertices = numpy.array(vertices)
|
vertices = remove_duplicate_vertices(vertices)
|
||||||
|
|
||||||
# Check for dx0/dy0 == dx1/dy1
|
# Check for dx0/dy0 == dx1/dy1
|
||||||
|
|
||||||
dv = numpy.roll(vertices, -1, axis=0) - vertices # [y1-y0, y2-y1, ...]
|
dv = numpy.roll(vertices, -1, axis=0) - vertices # [y1-y0, y2-y1, ...]
|
||||||
dxdy = dv * numpy.roll(dv, 1, axis=0)[:, ::-1] # [[dx0*(dy_-1), (dx_-1)*dy0], dx1*dy0, dy1*dy0]]
|
dxdy = dv * numpy.roll(dv, 1, axis=0)[:, ::-1] # [[dx0*(dy_-1), (dx_-1)*dy0], dx1*dy0, dy1*dx0]]
|
||||||
|
|
||||||
dxdy_diff = numpy.abs(numpy.diff(dxdy, axis=1))[:, 0]
|
dxdy_diff = numpy.abs(numpy.diff(dxdy, axis=1))[:, 0]
|
||||||
err_mult = 2 * numpy.abs(dxdy).sum(axis=1) + 1e-40
|
err_mult = 2 * numpy.abs(dxdy).sum(axis=1) + 1e-40
|
||||||
|
13
setup.py
13
setup.py
@ -3,11 +3,11 @@
|
|||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
|
||||||
with open('README.md', 'r') as f:
|
with open('README.md', 'rt') as f:
|
||||||
long_description = f.read()
|
long_description = f.read()
|
||||||
|
|
||||||
with open('masque/VERSION', 'r') as f:
|
with open('masque/VERSION.py', 'rt') as f:
|
||||||
version = f.read().strip()
|
version = f.readlines()[2].strip()
|
||||||
|
|
||||||
setup(name='masque',
|
setup(name='masque',
|
||||||
version=version,
|
version=version,
|
||||||
@ -19,16 +19,15 @@ setup(name='masque',
|
|||||||
url='https://mpxd.net/code/jan/masque',
|
url='https://mpxd.net/code/jan/masque',
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
package_data={
|
package_data={
|
||||||
'masque': ['VERSION',
|
'masque': ['py.typed',
|
||||||
'py.typed',
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'numpy',
|
'numpy',
|
||||||
],
|
],
|
||||||
extras_require={
|
extras_require={
|
||||||
'gdsii': ['python-gdsii'],
|
'gdsii': ['python-gdsii'],
|
||||||
'klamath': ['klamath'],
|
'klamath': ['klamath>=0.3'],
|
||||||
'oasis': ['fatamorgana>=0.7'],
|
'oasis': ['fatamorgana>=0.7'],
|
||||||
'dxf': ['ezdxf'],
|
'dxf': ['ezdxf'],
|
||||||
'svg': ['svgwrite'],
|
'svg': ['svgwrite'],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user