WIP: make libraries and names first-class!
This commit is contained in:
parent
fff20b3da9
commit
42c3a2b1e1
28
examples/nested_poly_test.py
Normal file
28
examples/nested_poly_test.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import numpy
|
||||||
|
from pyclipper import (
|
||||||
|
Pyclipper, PT_CLIP, PT_SUBJECT, CT_UNION, CT_INTERSECTION, PFT_NONZERO,
|
||||||
|
scale_to_clipper, scale_from_clipper,
|
||||||
|
)
|
||||||
|
p = Pyclipper()
|
||||||
|
p.AddPaths([
|
||||||
|
[(-10, -10), (-10, 10), (-9, 10), (-9, -10)],
|
||||||
|
[(-10, 10), (10, 10), (10, 9), (-10, 9)],
|
||||||
|
[(10, 10), (10, -10), (9, -10), (9, 10)],
|
||||||
|
[(10, -10), (-10, -10), (-10, -9), (10, -9)],
|
||||||
|
], PT_SUBJECT, closed=True)
|
||||||
|
p.Execute2?
|
||||||
|
p.Execute?
|
||||||
|
p.Execute(PT_UNION, PT_NONZERO, PT_NONZERO)
|
||||||
|
p.Execute(CT_UNION, PT_NONZERO, PT_NONZERO)
|
||||||
|
p.Execute(CT_UNION, PFT_NONZERO, PFT_NONZERO)
|
||||||
|
p = Pyclipper()
|
||||||
|
p.AddPaths([
|
||||||
|
[(-10, -10), (-10, 10), (-9, 10), (-9, -10)],
|
||||||
|
[(-10, 10), (10, 10), (10, 9), (-10, 9)],
|
||||||
|
[(10, 10), (10, -10), (9, -10), (9, 10)],
|
||||||
|
[(10, -10), (-10, -10), (-10, -9), (10, -9)],
|
||||||
|
], PT_SUBJECT, closed=True)
|
||||||
|
r = p.Execute2(CT_UNION, PFT_NONZERO, PFT_NONZERO)
|
||||||
|
r
|
||||||
|
r.Childs
|
||||||
|
%history -f nested_poly_test.py
|
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()
|
@ -24,17 +24,17 @@
|
|||||||
metaclass is used to auto-generate slots based on superclass type annotations.
|
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
|
- File I/O submodules are imported by `masque.file` to avoid creating hard dependencies on
|
||||||
external file-format reader/writers
|
external file-format reader/writers
|
||||||
- Pattern locking/unlocking is quite slow for large hierarchies.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .error import PatternError, PatternLockedError
|
from .error import PatternError
|
||||||
from .shapes import Shape
|
from .shapes import Shape
|
||||||
from .label import Label
|
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, DeviceLibrary
|
from .library import Library, PatternGenerator
|
||||||
|
from .builder import DeviceLibrary, LibDeviceLibrary, Device, Port
|
||||||
|
|
||||||
|
|
||||||
__author__ = 'Jan Petykiewicz'
|
__author__ = 'Jan Petykiewicz'
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
from .devices import Port, Device
|
from .devices import Port, Device
|
||||||
from .utils import ell
|
from .utils import ell
|
||||||
from .tools import Tool
|
from .tools import Tool
|
||||||
|
from .device_library import DeviceLibrary, LibDeviceLibrary
|
||||||
|
@ -79,16 +79,6 @@ class DeviceLibrary:
|
|||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return '<DeviceLibrary with keys ' + repr(list(self.generators.keys())) + '>'
|
return '<DeviceLibrary with keys ' + repr(list(self.generators.keys())) + '>'
|
||||||
|
|
||||||
def set_const(self, const: Device) -> None:
|
|
||||||
"""
|
|
||||||
Convenience function to avoid having to manually wrap
|
|
||||||
already-generated Device objects into callables.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
const: Pre-generated device object
|
|
||||||
"""
|
|
||||||
self.generators[const.pattern.name] = lambda: const
|
|
||||||
|
|
||||||
def add(
|
def add(
|
||||||
self: D,
|
self: D,
|
||||||
other: D,
|
other: D,
|
||||||
@ -175,7 +165,6 @@ class DeviceLibrary:
|
|||||||
def build_dev() -> Device:
|
def build_dev() -> Device:
|
||||||
dev = fn()
|
dev = fn()
|
||||||
dev.pattern = dev2pat(dev)
|
dev.pattern = dev2pat(dev)
|
||||||
dev.pattern.rename(prefix + name)
|
|
||||||
return dev
|
return dev
|
||||||
|
|
||||||
self[prefix + name] = build_dev
|
self[prefix + name] = build_dev
|
||||||
@ -200,8 +189,8 @@ class DeviceLibrary:
|
|||||||
|
|
||||||
def build_wrapped_dev() -> Device:
|
def build_wrapped_dev() -> Device:
|
||||||
old_dev = self[old_name]
|
old_dev = self[old_name]
|
||||||
wrapper = Pattern(name=name)
|
wrapper = Pattern()
|
||||||
wrapper.addsp(old_dev.pattern)
|
wrapper.addsp(old_name)
|
||||||
return Device(wrapper, old_dev.ports)
|
return Device(wrapper, old_dev.ports)
|
||||||
|
|
||||||
self[name] = build_wrapped_dev
|
self[name] = build_wrapped_dev
|
@ -125,7 +125,7 @@ class Device(Copyable, Mirrorable):
|
|||||||
- `Device(pattern, ports={'A': port_a, 'C': port_c})` uses an existing
|
- `Device(pattern, ports={'A': port_a, 'C': port_c})` uses an existing
|
||||||
pattern and defines some ports.
|
pattern and defines some ports.
|
||||||
|
|
||||||
- `Device(name='my_dev_name', ports=None)` makes a new empty pattern with
|
- `Device(ports=None)` makes a new empty pattern with
|
||||||
default ports ('A' and 'B', in opposite directions, at (0, 0)).
|
default ports ('A' and 'B', in opposite directions, at (0, 0)).
|
||||||
|
|
||||||
- `my_device.build('my_layout')` makes a new pattern and instantiates
|
- `my_device.build('my_layout')` makes a new pattern and instantiates
|
||||||
@ -182,7 +182,6 @@ class Device(Copyable, Mirrorable):
|
|||||||
ports: Optional[Dict[str, Port]] = None,
|
ports: Optional[Dict[str, Port]] = None,
|
||||||
*,
|
*,
|
||||||
tools: Union[None, Tool, Dict[Optional[str], Tool]] = None,
|
tools: Union[None, Tool, Dict[Optional[str], Tool]] = None,
|
||||||
name: Optional[str] = None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
If `ports` is `None`, two default ports ('A' and 'B') are created.
|
If `ports` is `None`, two default ports ('A' and 'B') are created.
|
||||||
@ -190,14 +189,7 @@ class Device(Copyable, Mirrorable):
|
|||||||
(attached devices will be placed to the left) and 'B' has rotation
|
(attached devices will be placed to the left) and 'B' has rotation
|
||||||
pi (attached devices will be placed to the right).
|
pi (attached devices will be placed to the right).
|
||||||
"""
|
"""
|
||||||
if pattern is not None:
|
self.pattern = pattern or Pattern()
|
||||||
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:
|
if ports is None:
|
||||||
self.ports = {
|
self.ports = {
|
||||||
@ -336,25 +328,22 @@ class Device(Copyable, Mirrorable):
|
|||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def build(self, name: str) -> 'Device':
|
def build(self) -> 'Device':
|
||||||
"""
|
"""
|
||||||
Begin building a new device around an instance of the current device
|
Begin building a new device around an instance of the current device
|
||||||
(rather than modifying the current device).
|
(rather than modifying the current device).
|
||||||
|
|
||||||
Args:
|
|
||||||
name: A name for the new device
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The new `Device` object.
|
The new `Device` object.
|
||||||
"""
|
"""
|
||||||
pat = Pattern(name)
|
# TODO lib: this needs a name for self, rather than for the built thing
|
||||||
|
pat = Pattern()
|
||||||
pat.addsp(self.pattern)
|
pat.addsp(self.pattern)
|
||||||
new = Device(pat, ports=self.ports, tools=self.tools)
|
new = Device(pat, ports=self.ports, tools=self.tools)
|
||||||
return new
|
return new
|
||||||
|
|
||||||
def as_interface(
|
def as_interface(
|
||||||
self,
|
self,
|
||||||
name: str,
|
|
||||||
in_prefix: str = 'in_',
|
in_prefix: str = 'in_',
|
||||||
out_prefix: str = '',
|
out_prefix: str = '',
|
||||||
port_map: Optional[Union[Dict[str, str], Sequence[str]]] = None
|
port_map: Optional[Union[Dict[str, str], Sequence[str]]] = None
|
||||||
@ -380,7 +369,6 @@ class Device(Copyable, Mirrorable):
|
|||||||
current device.
|
current device.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name: Name for the new device
|
|
||||||
in_prefix: Prepended to port names for newly-created ports with
|
in_prefix: Prepended to port names for newly-created ports with
|
||||||
reversed directions compared to the current device.
|
reversed directions compared to the current device.
|
||||||
out_prefix: Prepended to port names for ports which are directly
|
out_prefix: Prepended to port names for ports which are directly
|
||||||
@ -424,12 +412,13 @@ class Device(Copyable, Mirrorable):
|
|||||||
if duplicates:
|
if duplicates:
|
||||||
raise DeviceError(f'Duplicate keys after prefixing, try a different prefix: {duplicates}')
|
raise DeviceError(f'Duplicate keys after prefixing, try a different prefix: {duplicates}')
|
||||||
|
|
||||||
new = Device(name=name, ports={**ports_in, **ports_out}, tools=self.tools)
|
new = Device(ports={**ports_in, **ports_out}, tools=self.tools)
|
||||||
return new
|
return new
|
||||||
|
|
||||||
def plug(
|
def plug(
|
||||||
self: D,
|
self: D,
|
||||||
other: O,
|
library: Mapping[str, 'Device'],
|
||||||
|
name: str,
|
||||||
map_in: Dict[str, str],
|
map_in: Dict[str, str],
|
||||||
map_out: Optional[Dict[str, Optional[str]]] = None,
|
map_out: Optional[Dict[str, Optional[str]]] = None,
|
||||||
*,
|
*,
|
||||||
@ -438,27 +427,29 @@ class Device(Copyable, Mirrorable):
|
|||||||
set_rotation: Optional[bool] = None,
|
set_rotation: Optional[bool] = None,
|
||||||
) -> D:
|
) -> D:
|
||||||
"""
|
"""
|
||||||
Instantiate the device `other` into the current device, connecting
|
Instantiate a device `library[name]` into the current device, connecting
|
||||||
the ports specified by `map_in` and renaming the unconnected
|
the ports specified by `map_in` and renaming the unconnected
|
||||||
ports specified by `map_out`.
|
ports specified by `map_out`.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
=========
|
=========
|
||||||
- `my_device.plug(subdevice, {'A': 'C', 'B': 'B'}, map_out={'D': 'myport'})`
|
- `my_device.plug(lib, 'subdevice', {'A': 'C', 'B': 'B'}, map_out={'D': 'myport'})`
|
||||||
instantiates `subdevice` into `my_device`, plugging ports 'A' and 'B'
|
instantiates `lib['subdevice']` into `my_device`, plugging ports 'A' and 'B'
|
||||||
of `my_device` into ports 'C' and 'B' of `subdevice`. The connected ports
|
of `my_device` into ports 'C' and 'B' of `subdevice`. The connected ports
|
||||||
are removed and any unconnected ports from `subdevice` are added to
|
are removed and any unconnected ports from `subdevice` are added to
|
||||||
`my_device`. Port 'D' of `subdevice` (unconnected) is renamed to 'myport'.
|
`my_device`. Port 'D' of `subdevice` (unconnected) is renamed to 'myport'.
|
||||||
|
|
||||||
- `my_device.plug(wire, {'myport': 'A'})` places port 'A' of `wire` at 'myport'
|
- `my_device.plug(lib, 'wire', {'myport': 'A'})` places port 'A' of `lib['wire']`
|
||||||
of `my_device`. If `wire` has only two ports (e.g. 'A' and 'B'), no `map_out`,
|
at 'myport' of `my_device`.
|
||||||
argument is provided, and the `inherit_name` argument is not explicitly
|
If `'wire'` has only two ports (e.g. 'A' and 'B'), no `map_out` argument is
|
||||||
set to `False`, the unconnected port of `wire` is automatically renamed to
|
provided, and the `inherit_name` argument is not explicitly set to `False`,
|
||||||
'myport'. This allows easy extension of existing ports without changing
|
the unconnected port of `wire` is automatically renamed to 'myport'. This
|
||||||
their names or having to provide `map_out` each time `plug` is called.
|
allows easy extension of existing ports without changing their names or
|
||||||
|
having to provide `map_out` each time `plug` is called.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
other: A device to instantiate into the current device.
|
library: A `DeviceLibrary` containing the device to be instatiated.
|
||||||
|
name: The name of the device to be instantiated (from `library`).
|
||||||
map_in: Dict of `{'self_port': 'other_port'}` mappings, specifying
|
map_in: Dict of `{'self_port': 'other_port'}` mappings, specifying
|
||||||
port connections between the two devices.
|
port connections between the two devices.
|
||||||
map_out: Dict of `{'old_name': 'new_name'}` mappings, specifying
|
map_out: Dict of `{'old_name': 'new_name'}` mappings, specifying
|
||||||
@ -513,13 +504,14 @@ class Device(Copyable, Mirrorable):
|
|||||||
del self.ports[ki]
|
del self.ports[ki]
|
||||||
map_out[vi] = None
|
map_out[vi] = None
|
||||||
|
|
||||||
self.place(other, offset=translation, rotation=rotation, pivot=pivot,
|
self.place(library, name, offset=translation, rotation=rotation, pivot=pivot,
|
||||||
mirrored=mirrored, port_map=map_out, skip_port_check=True)
|
mirrored=mirrored, port_map=map_out, skip_port_check=True)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def place(
|
def place(
|
||||||
self: D,
|
self: D,
|
||||||
other: O,
|
library: Mapping[str, 'Device'],
|
||||||
|
name: str,
|
||||||
*,
|
*,
|
||||||
offset: ArrayLike = (0, 0),
|
offset: ArrayLike = (0, 0),
|
||||||
rotation: float = 0,
|
rotation: float = 0,
|
||||||
@ -529,7 +521,7 @@ class Device(Copyable, Mirrorable):
|
|||||||
skip_port_check: bool = False,
|
skip_port_check: bool = False,
|
||||||
) -> D:
|
) -> D:
|
||||||
"""
|
"""
|
||||||
Instantiate the device `other` into the current device, adding its
|
Instantiate the device `library[name]` into the current device, adding its
|
||||||
ports to those of the current device (but not connecting any ports).
|
ports to those of the current device (but not connecting any ports).
|
||||||
|
|
||||||
Mirroring is applied before rotation; translation (`offset`) is applied last.
|
Mirroring is applied before rotation; translation (`offset`) is applied last.
|
||||||
@ -543,16 +535,17 @@ class Device(Copyable, Mirrorable):
|
|||||||
rather than the port name on the original `pad` device.
|
rather than the port name on the original `pad` device.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
other: A device to instantiate into the current device.
|
library: A `DeviceLibrary` containing the device to be instatiated.
|
||||||
offset: Offset at which to place `other`. Default (0, 0).
|
name: The name of the device to be instantiated (from `library`).
|
||||||
rotation: Rotation applied to `other` before placement. Default 0.
|
offset: Offset at which to place the instance. Default (0, 0).
|
||||||
|
rotation: Rotation applied to the instance before placement. Default 0.
|
||||||
pivot: Rotation is applied around this pivot point (default (0, 0)).
|
pivot: Rotation is applied around this pivot point (default (0, 0)).
|
||||||
Rotation is applied prior to translation (`offset`).
|
Rotation is applied prior to translation (`offset`).
|
||||||
mirrored: Whether `other` should be mirrored across the x and y axes.
|
mirrored: Whether theinstance should be mirrored across the x and y axes.
|
||||||
Mirroring is applied before translation and rotation.
|
Mirroring is applied before translation and rotation.
|
||||||
port_map: Dict of `{'old_name': 'new_name'}` mappings, specifying
|
port_map: Dict of `{'old_name': 'new_name'}` mappings, specifying
|
||||||
new names for ports in `other`. New names can be `None`, which will
|
new names for ports in the instantiated device. New names can be
|
||||||
delete those ports.
|
`None`, which will delete those ports.
|
||||||
skip_port_check: Can be used to skip the internal call to `check_ports`,
|
skip_port_check: Can be used to skip the internal call to `check_ports`,
|
||||||
in case it has already been performed elsewhere.
|
in case it has already been performed elsewhere.
|
||||||
|
|
||||||
@ -561,7 +554,7 @@ class Device(Copyable, Mirrorable):
|
|||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
`DeviceError` if any ports specified in `map_in` or `map_out` do not
|
`DeviceError` if any ports specified in `map_in` or `map_out` do not
|
||||||
exist in `self.ports` or `other_names`.
|
exist in `self.ports` or `library[name].ports`.
|
||||||
`DeviceError` if there are any duplicate names after `map_in` and `map_out`
|
`DeviceError` if there are any duplicate names after `map_in` and `map_out`
|
||||||
are applied.
|
are applied.
|
||||||
"""
|
"""
|
||||||
@ -572,6 +565,8 @@ class Device(Copyable, Mirrorable):
|
|||||||
if port_map is None:
|
if port_map is None:
|
||||||
port_map = {}
|
port_map = {}
|
||||||
|
|
||||||
|
other = library[name]
|
||||||
|
|
||||||
if not skip_port_check:
|
if not skip_port_check:
|
||||||
self.check_ports(other.ports.keys(), map_in=None, map_out=port_map)
|
self.check_ports(other.ports.keys(), map_in=None, map_out=port_map)
|
||||||
|
|
||||||
@ -589,7 +584,7 @@ class Device(Copyable, Mirrorable):
|
|||||||
p.translate(offset)
|
p.translate(offset)
|
||||||
self.ports[name] = p
|
self.ports[name] = p
|
||||||
|
|
||||||
sp = SubPattern(other.pattern, mirrored=mirrored)
|
sp = SubPattern(name, mirrored=mirrored) #TODO figure out how this should work?!
|
||||||
sp.rotate_around(pivot, rotation)
|
sp.rotate_around(pivot, rotation)
|
||||||
sp.translate(offset)
|
sp.translate(offset)
|
||||||
self.pattern.subpatterns.append(sp)
|
self.pattern.subpatterns.append(sp)
|
||||||
@ -748,19 +743,6 @@ class Device(Copyable, Mirrorable):
|
|||||||
self._dead = True
|
self._dead = True
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def rename(self: D, name: str) -> D:
|
|
||||||
"""
|
|
||||||
Renames the pattern and returns the device
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: The new name
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
self.pattern.name = name
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
s = f'<Device {self.pattern} ['
|
s = f'<Device {self.pattern} ['
|
||||||
for name, port in self.ports.items():
|
for name, port in self.ports.items():
|
||||||
@ -831,7 +813,7 @@ class Device(Copyable, Mirrorable):
|
|||||||
|
|
||||||
return self.path(portspec, ccw, length, tool_port_names=tool_port_names, **kwargs)
|
return self.path(portspec, ccw, length, tool_port_names=tool_port_names, **kwargs)
|
||||||
|
|
||||||
def busL(
|
def mpath(
|
||||||
self: D,
|
self: D,
|
||||||
portspec: Union[str, Sequence[str]],
|
portspec: Union[str, Sequence[str]],
|
||||||
ccw: Optional[bool],
|
ccw: Optional[bool],
|
||||||
@ -839,7 +821,6 @@ class Device(Copyable, Mirrorable):
|
|||||||
spacing: Optional[Union[float, ArrayLike]] = None,
|
spacing: Optional[Union[float, ArrayLike]] = None,
|
||||||
set_rotation: Optional[float] = None,
|
set_rotation: Optional[float] = None,
|
||||||
tool_port_names: Sequence[str] = ('A', 'B'),
|
tool_port_names: Sequence[str] = ('A', 'B'),
|
||||||
container_name: str = '_busL',
|
|
||||||
force_container: bool = False,
|
force_container: bool = False,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> D:
|
) -> D:
|
||||||
@ -873,7 +854,7 @@ class Device(Copyable, Mirrorable):
|
|||||||
port_name = tuple(portspec)[0]
|
port_name = tuple(portspec)[0]
|
||||||
return self.path(port_name, ccw, extensions[port_name], tool_port_names=tool_port_names)
|
return self.path(port_name, ccw, extensions[port_name], tool_port_names=tool_port_names)
|
||||||
else:
|
else:
|
||||||
dev = Device(name='', ports=ports, tools=self.tools).as_interface(container_name)
|
dev = Device(name='', ports=ports, tools=self.tools).as_interface()
|
||||||
for name, length in extensions.items():
|
for name, length in extensions.items():
|
||||||
dev.path(name, ccw, length, tool_port_names=tool_port_names)
|
dev.path(name, ccw, length, tool_port_names=tool_port_names)
|
||||||
return self.plug(dev, {sp: 'in_' + sp for sp in ports.keys()}) # TODO safe to use 'in_'?
|
return self.plug(dev, {sp: 'in_' + sp for sp in ports.keys()}) # TODO safe to use 'in_'?
|
||||||
|
@ -11,13 +11,6 @@ class PatternError(MasqueError):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class PatternLockedError(PatternError):
|
|
||||||
"""
|
|
||||||
Exception raised when trying to modify a locked pattern
|
|
||||||
"""
|
|
||||||
def __init__(self):
|
|
||||||
PatternError.__init__(self, 'Tried to modify a locked Pattern, subpattern, or shape')
|
|
||||||
|
|
||||||
|
|
||||||
class LibraryError(MasqueError):
|
class LibraryError(MasqueError):
|
||||||
"""
|
"""
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
DXF file format readers and writers
|
DXF file format readers and writers
|
||||||
"""
|
"""
|
||||||
from typing import List, Any, Dict, Tuple, Callable, Union, Sequence, Iterable
|
from typing import List, Any, Dict, Tuple, Callable, Union, Sequence, Iterable, Mapping
|
||||||
import re
|
import re
|
||||||
import io
|
import io
|
||||||
import base64
|
import base64
|
||||||
@ -10,7 +10,7 @@ import logging
|
|||||||
import pathlib
|
import pathlib
|
||||||
import gzip
|
import gzip
|
||||||
|
|
||||||
import numpy # type: ignore
|
import numpy
|
||||||
import ezdxf # type: ignore
|
import ezdxf # type: ignore
|
||||||
|
|
||||||
from .. import Pattern, SubPattern, PatternError, Label, Shape
|
from .. import Pattern, SubPattern, PatternError, Label, Shape
|
||||||
@ -29,12 +29,13 @@ DEFAULT_LAYER = 'DEFAULT'
|
|||||||
|
|
||||||
|
|
||||||
def write(
|
def write(
|
||||||
pattern: Pattern,
|
top_name: str,
|
||||||
|
library: Mapping[str, Pattern],
|
||||||
stream: io.TextIOBase,
|
stream: io.TextIOBase,
|
||||||
*,
|
*,
|
||||||
modify_originals: bool = False,
|
modify_originals: bool = False,
|
||||||
dxf_version='AC1024',
|
dxf_version='AC1024',
|
||||||
disambiguate_func: Callable[[Iterable[Pattern]], None] = None,
|
disambiguate_func: Callable[[Iterable[str]], List[str]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Write a `Pattern` to a DXF file, by first calling `.polygonize()` to change the shapes
|
Write a `Pattern` to a DXF file, by first calling `.polygonize()` to change the shapes
|
||||||
@ -60,10 +61,12 @@ def write(
|
|||||||
array with rotated instances must be manhattan _after_ having a compensating rotation applied.
|
array with rotated instances must be manhattan _after_ having a compensating rotation applied.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
patterns: A Pattern or list of patterns to write to the stream.
|
top_name: Name of the top-level pattern to write.
|
||||||
|
library: A {name: Pattern} mapping of patterns. Only `top_name` and patterns referenced
|
||||||
|
by it are written.
|
||||||
stream: Stream object to write to.
|
stream: Stream object to write to.
|
||||||
modify_original: If `True`, the original pattern is modified as part of the writing
|
modify_original: If `True`, the original pattern is modified as part of the writing
|
||||||
process. Otherwise, a copy is made and `deepunlock()`-ed.
|
process. Otherwise, a copy is made.
|
||||||
Default `False`.
|
Default `False`.
|
||||||
disambiguate_func: Function which takes a list of patterns and alters them
|
disambiguate_func: Function which takes a list of patterns and alters them
|
||||||
to make their names valid and unique. Default is `disambiguate_pattern_names`.
|
to make their names valid and unique. Default is `disambiguate_pattern_names`.
|
||||||
@ -75,11 +78,14 @@ def write(
|
|||||||
assert(disambiguate_func is not None)
|
assert(disambiguate_func is not None)
|
||||||
|
|
||||||
if not modify_originals:
|
if not modify_originals:
|
||||||
pattern = pattern.deepcopy().deepunlock()
|
library = library.deepcopy()
|
||||||
|
|
||||||
# Get a dict of id(pattern) -> pattern
|
pattern = library[top_name]
|
||||||
patterns_by_id = pattern.referenced_patterns_by_id()
|
|
||||||
disambiguate_func(patterns_by_id.values())
|
old_names = list(library.keys())
|
||||||
|
new_names = disambiguate_func(old_names)
|
||||||
|
renamed_lib = {new_name: library[old_name]
|
||||||
|
for old_name, new_name in zip(old_names, new_names)}
|
||||||
|
|
||||||
# Create library
|
# Create library
|
||||||
lib = ezdxf.new(dxf_version, setup=True)
|
lib = ezdxf.new(dxf_version, setup=True)
|
||||||
@ -89,9 +95,9 @@ def write(
|
|||||||
_subpatterns_to_refs(msp, pattern.subpatterns)
|
_subpatterns_to_refs(msp, pattern.subpatterns)
|
||||||
|
|
||||||
# Now create a block for each referenced pattern, and add in any shapes
|
# Now create a block for each referenced pattern, and add in any shapes
|
||||||
for pat in patterns_by_id.values():
|
for name, pat in renamed_lib.items():
|
||||||
assert(pat is not None)
|
assert(pat is not None)
|
||||||
block = lib.blocks.new(name=pat.name)
|
block = lib.blocks.new(name=name)
|
||||||
|
|
||||||
_shapes_to_elements(block, pat.shapes)
|
_shapes_to_elements(block, pat.shapes)
|
||||||
_labels_to_texts(block, pat.labels)
|
_labels_to_texts(block, pat.labels)
|
||||||
@ -101,7 +107,8 @@ def write(
|
|||||||
|
|
||||||
|
|
||||||
def writefile(
|
def writefile(
|
||||||
pattern: Pattern,
|
top_name: str,
|
||||||
|
library: Mapping[str, Pattern],
|
||||||
filename: Union[str, pathlib.Path],
|
filename: Union[str, pathlib.Path],
|
||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
@ -112,7 +119,9 @@ def writefile(
|
|||||||
Will automatically compress the file if it has a .gz suffix.
|
Will automatically compress the file if it has a .gz suffix.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
pattern: `Pattern` to save
|
top_name: Name of the top-level pattern to write.
|
||||||
|
library: A {name: Pattern} mapping of patterns. Only `top_name` and patterns referenced
|
||||||
|
by it are written.
|
||||||
filename: Filename to save to.
|
filename: Filename to save to.
|
||||||
*args: passed to `dxf.write`
|
*args: passed to `dxf.write`
|
||||||
**kwargs: passed to `dxf.write`
|
**kwargs: passed to `dxf.write`
|
||||||
@ -124,7 +133,7 @@ def writefile(
|
|||||||
open_func = open
|
open_func = open
|
||||||
|
|
||||||
with open_func(path, mode='wt') as stream:
|
with open_func(path, mode='wt') as stream:
|
||||||
write(pattern, stream, *args, **kwargs)
|
write(top_name, library, stream, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def readfile(
|
def readfile(
|
||||||
@ -156,7 +165,7 @@ def readfile(
|
|||||||
def read(
|
def read(
|
||||||
stream: io.TextIOBase,
|
stream: io.TextIOBase,
|
||||||
clean_vertices: bool = True,
|
clean_vertices: bool = True,
|
||||||
) -> Tuple[Pattern, Dict[str, Any]]:
|
) -> Tuple[Dict[str, Pattern], Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Read a dxf file and translate it into a dict of `Pattern` objects. DXF `Block`s are
|
Read a dxf file and translate it into a dict of `Pattern` objects. DXF `Block`s are
|
||||||
translated into `Pattern` objects; `LWPolyline`s are translated into polygons, and `Insert`s
|
translated into `Pattern` objects; `LWPolyline`s are translated into polygons, and `Insert`s
|
||||||
@ -176,26 +185,20 @@ def read(
|
|||||||
lib = ezdxf.read(stream)
|
lib = ezdxf.read(stream)
|
||||||
msp = lib.modelspace()
|
msp = lib.modelspace()
|
||||||
|
|
||||||
pat = _read_block(msp, clean_vertices)
|
npat = _read_block(msp, clean_vertices)
|
||||||
patterns = [pat] + [_read_block(bb, clean_vertices) for bb in lib.blocks if bb.name != '*Model_Space']
|
patterns_dict = dict([npat]
|
||||||
|
+ [_read_block(bb, clean_vertices) for bb in lib.blocks if bb.name != '*Model_Space'])
|
||||||
# Create a dict of {pattern.name: pattern, ...}, then fix up all subpattern.pattern entries
|
|
||||||
# according to the subpattern.identifier (which is deleted after use).
|
|
||||||
patterns_dict = dict(((p.name, p) for p in patterns))
|
|
||||||
for p in patterns_dict.values():
|
|
||||||
for sp in p.subpatterns:
|
|
||||||
sp.pattern = patterns_dict[sp.identifier[0]]
|
|
||||||
del sp.identifier
|
|
||||||
|
|
||||||
library_info = {
|
library_info = {
|
||||||
'layers': [ll.dxfattribs() for ll in lib.layers]
|
'layers': [ll.dxfattribs() for ll in lib.layers]
|
||||||
}
|
}
|
||||||
|
|
||||||
return pat, library_info
|
return patterns_dict, library_info
|
||||||
|
|
||||||
|
|
||||||
def _read_block(block, clean_vertices: bool) -> Pattern:
|
def _read_block(block, clean_vertices: bool) -> Tuple[str, Pattern]:
|
||||||
pat = Pattern(block.name)
|
name = block.name
|
||||||
|
pat = Pattern()
|
||||||
for element in block:
|
for element in block:
|
||||||
eltype = element.dxftype()
|
eltype = element.dxftype()
|
||||||
if eltype in ('POLYLINE', 'LWPOLYLINE'):
|
if eltype in ('POLYLINE', 'LWPOLYLINE'):
|
||||||
@ -258,12 +261,12 @@ def _read_block(block, clean_vertices: bool) -> Pattern:
|
|||||||
offset = numpy.array(attr.get('insert', (0, 0, 0)))[:2]
|
offset = numpy.array(attr.get('insert', (0, 0, 0)))[:2]
|
||||||
|
|
||||||
args = {
|
args = {
|
||||||
|
'target': (attr.get('name', None),),
|
||||||
'offset': offset,
|
'offset': offset,
|
||||||
'scale': scale,
|
'scale': scale,
|
||||||
'mirrored': mirrored,
|
'mirrored': mirrored,
|
||||||
'rotation': rotation,
|
'rotation': rotation,
|
||||||
'pattern': None,
|
'pattern': None,
|
||||||
'identifier': (attr.get('name', None),),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if 'column_count' in attr:
|
if 'column_count' in attr:
|
||||||
@ -274,7 +277,7 @@ def _read_block(block, clean_vertices: bool) -> Pattern:
|
|||||||
pat.subpatterns.append(SubPattern(**args))
|
pat.subpatterns.append(SubPattern(**args))
|
||||||
else:
|
else:
|
||||||
logger.warning(f'Ignoring DXF element {element.dxftype()} (not implemented).')
|
logger.warning(f'Ignoring DXF element {element.dxftype()} (not implemented).')
|
||||||
return pat
|
return name, pat
|
||||||
|
|
||||||
|
|
||||||
def _subpatterns_to_refs(
|
def _subpatterns_to_refs(
|
||||||
@ -282,9 +285,9 @@ def _subpatterns_to_refs(
|
|||||||
subpatterns: List[SubPattern],
|
subpatterns: List[SubPattern],
|
||||||
) -> None:
|
) -> None:
|
||||||
for subpat in subpatterns:
|
for subpat in subpatterns:
|
||||||
if subpat.pattern is None:
|
if subpat.target is None:
|
||||||
continue
|
continue
|
||||||
encoded_name = subpat.pattern.name
|
encoded_name = subpat.target
|
||||||
|
|
||||||
rotation = (subpat.rotation * 180 / numpy.pi) % 360
|
rotation = (subpat.rotation * 180 / numpy.pi) % 360
|
||||||
attribs = {
|
attribs = {
|
||||||
@ -360,18 +363,24 @@ def _mlayer2dxf(layer: layer_t) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def disambiguate_pattern_names(
|
def disambiguate_pattern_names(
|
||||||
patterns: Iterable[Pattern],
|
names: Iterable[str],
|
||||||
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
|
) -> List[str]:
|
||||||
) -> None:
|
"""
|
||||||
used_names = []
|
Args:
|
||||||
for pat in patterns:
|
names: List of pattern names to disambiguate
|
||||||
sanitized_name = re.compile(r'[^A-Za-z0-9_\?\$]').sub('_', pat.name)
|
max_name_length: Names longer than this will be truncated
|
||||||
|
suffix_length: Names which get truncated are truncated by this many extra characters. This is to
|
||||||
|
leave room for a suffix if one is necessary.
|
||||||
|
"""
|
||||||
|
new_names = []
|
||||||
|
for name in names:
|
||||||
|
sanitized_name = re.compile(r'[^A-Za-z0-9_\?\$]').sub('_', name)
|
||||||
|
|
||||||
i = 0
|
i = 0
|
||||||
suffixed_name = sanitized_name
|
suffixed_name = sanitized_name
|
||||||
while suffixed_name in used_names or suffixed_name == '':
|
while suffixed_name in new_names or suffixed_name == '':
|
||||||
suffix = base64.b64encode(struct.pack('>Q', i), b'$?').decode('ASCII')
|
suffix = base64.b64encode(struct.pack('>Q', i), b'$?').decode('ASCII')
|
||||||
|
|
||||||
suffixed_name = sanitized_name + '$' + suffix[:-1].lstrip('A')
|
suffixed_name = sanitized_name + '$' + suffix[:-1].lstrip('A')
|
||||||
@ -380,17 +389,16 @@ def disambiguate_pattern_names(
|
|||||||
if sanitized_name == '':
|
if sanitized_name == '':
|
||||||
logger.warning(f'Empty pattern name saved as "{suffixed_name}"')
|
logger.warning(f'Empty pattern name saved as "{suffixed_name}"')
|
||||||
elif suffixed_name != sanitized_name:
|
elif suffixed_name != sanitized_name:
|
||||||
if dup_warn_filter is None or dup_warn_filter(pat.name):
|
if dup_warn_filter is None or dup_warn_filter(name):
|
||||||
logger.warning(f'Pattern name "{pat.name}" ({sanitized_name}) appears multiple times;\n'
|
logger.warning(f'Pattern name "{name}" ({sanitized_name}) appears multiple times;\n'
|
||||||
+ f' renaming to "{suffixed_name}"')
|
+ f' renaming to "{suffixed_name}"')
|
||||||
|
|
||||||
if len(suffixed_name) == 0:
|
if len(suffixed_name) == 0:
|
||||||
# Should never happen since zero-length names are replaced
|
# Should never happen since zero-length names are replaced
|
||||||
raise PatternError(f'Zero-length name after sanitize,\n originally "{pat.name}"')
|
raise PatternError(f'Zero-length name after sanitize,\n originally "{name}"')
|
||||||
if len(suffixed_name) > max_name_length:
|
if len(suffixed_name) > max_name_length:
|
||||||
raise PatternError(f'Pattern name "{suffixed_name!r}" length > {max_name_length} after encode,\n'
|
raise PatternError(f'Pattern name "{suffixed_name!r}" length > {max_name_length} after encode,\n'
|
||||||
+ f' originally "{pat.name}"')
|
+ f' originally "{name}"')
|
||||||
|
|
||||||
pat.name = suffixed_name
|
|
||||||
used_names.append(suffixed_name)
|
|
||||||
|
|
||||||
|
new_names.append(suffixed_name)
|
||||||
|
return new_names
|
||||||
|
@ -53,18 +53,22 @@ path_cap_map = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def rint_cast(val: ArrayLike) -> NDArray[numpy.int32]:
|
||||||
|
return numpy.rint(val, dtype=numpy.int32, casting='unsafe')
|
||||||
|
|
||||||
|
|
||||||
def write(
|
def write(
|
||||||
patterns: Union[Pattern, Sequence[Pattern]],
|
library: Mapping[str, Pattern],
|
||||||
stream: BinaryIO,
|
stream: BinaryIO,
|
||||||
meters_per_unit: float,
|
meters_per_unit: float,
|
||||||
logical_units_per_unit: float = 1,
|
logical_units_per_unit: float = 1,
|
||||||
library_name: str = 'masque-klamath',
|
library_name: str = 'masque-klamath',
|
||||||
*,
|
*,
|
||||||
modify_originals: bool = False,
|
modify_originals: bool = False,
|
||||||
disambiguate_func: Callable[[Iterable[Pattern]], None] = None,
|
disambiguate_func: Callable[[Iterable[str]], List[str]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Convert a `Pattern` or list of patterns to a GDSII stream, and then mapping data as follows:
|
Convert a library to a GDSII stream, mapping data as follows:
|
||||||
Pattern -> GDSII structure
|
Pattern -> GDSII structure
|
||||||
SubPattern -> GDSII SREF or AREF
|
SubPattern -> GDSII SREF or AREF
|
||||||
Path -> GSDII path
|
Path -> GSDII path
|
||||||
@ -85,7 +89,7 @@ def write(
|
|||||||
prior to calling this function.
|
prior to calling this function.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
patterns: A Pattern or list of patterns to convert.
|
library: A {name: Pattern} mapping of patterns to write.
|
||||||
meters_per_unit: Written into the GDSII file, meters per (database) length unit.
|
meters_per_unit: Written into the GDSII file, meters per (database) length unit.
|
||||||
All distances are assumed to be an integer multiple of this unit, and are stored as such.
|
All distances are assumed to be an integer multiple of this unit, and are stored as such.
|
||||||
logical_units_per_unit: Written into the GDSII file. Allows the GDSII to specify a
|
logical_units_per_unit: Written into the GDSII file. Allows the GDSII to specify a
|
||||||
@ -94,52 +98,48 @@ def write(
|
|||||||
library_name: Library name written into the GDSII file.
|
library_name: Library name written into the GDSII file.
|
||||||
Default 'masque-klamath'.
|
Default 'masque-klamath'.
|
||||||
modify_originals: If `True`, the original pattern is modified as part of the writing
|
modify_originals: If `True`, the original pattern is modified as part of the writing
|
||||||
process. Otherwise, a copy is made and `deepunlock()`-ed.
|
process. Otherwise, a copy is made.
|
||||||
Default `False`.
|
Default `False`.
|
||||||
disambiguate_func: Function which takes a list of patterns and alters them
|
disambiguate_func: Function which takes a list of pattern names and returns a list of names
|
||||||
to make their names valid and unique. Default is `disambiguate_pattern_names`, which
|
altered to be valid and unique. Default is `disambiguate_pattern_names`, which
|
||||||
attempts to adhere to the GDSII standard as well as possible.
|
attempts to adhere to the GDSII standard reasonably well.
|
||||||
WARNING: No additional error checking is performed on the results.
|
WARNING: No additional error checking is performed on the results.
|
||||||
"""
|
"""
|
||||||
if isinstance(patterns, Pattern):
|
|
||||||
patterns = [patterns]
|
|
||||||
|
|
||||||
if disambiguate_func is None:
|
if disambiguate_func is None:
|
||||||
disambiguate_func = disambiguate_pattern_names # type: ignore
|
disambiguate_func = disambiguate_pattern_names
|
||||||
assert(disambiguate_func is not None) # placate mypy
|
|
||||||
|
|
||||||
if not modify_originals:
|
if not modify_originals:
|
||||||
patterns = [p.deepunlock() for p in copy.deepcopy(patterns)]
|
library = copy.deepcopy(library)
|
||||||
|
|
||||||
patterns = [p.wrap_repeated_shapes() for p in patterns]
|
for p in library.values():
|
||||||
|
library.add(p.wrap_repeated_shapes())
|
||||||
|
|
||||||
|
old_names = list(library.keys())
|
||||||
|
new_names = disambiguate_func(old_names)
|
||||||
|
renamed_lib = {new_name: library[old_name]
|
||||||
|
for old_name, new_name in zip(old_names, new_names)}
|
||||||
|
|
||||||
# Create library
|
# Create library
|
||||||
header = klamath.library.FileHeader(name=library_name.encode('ASCII'),
|
header = klamath.library.FileHeader(
|
||||||
|
name=library_name.encode('ASCII'),
|
||||||
user_units_per_db_unit=logical_units_per_unit,
|
user_units_per_db_unit=logical_units_per_unit,
|
||||||
meters_per_db_unit=meters_per_unit)
|
meters_per_db_unit=meters_per_unit,
|
||||||
|
)
|
||||||
header.write(stream)
|
header.write(stream)
|
||||||
|
|
||||||
# Get a dict of id(pattern) -> pattern
|
|
||||||
patterns_by_id = {id(pattern): pattern for pattern in patterns}
|
|
||||||
for pattern in patterns:
|
|
||||||
for i, p in pattern.referenced_patterns_by_id().items():
|
|
||||||
patterns_by_id[i] = p
|
|
||||||
|
|
||||||
disambiguate_func(patterns_by_id.values())
|
|
||||||
|
|
||||||
# Now create a structure for each pattern, and add in any Boundary and SREF elements
|
# Now create a structure for each pattern, and add in any Boundary and SREF elements
|
||||||
for pat in patterns_by_id.values():
|
for name, pat in renamed_lib.items():
|
||||||
elements: List[klamath.elements.Element] = []
|
elements: List[klamath.elements.Element] = []
|
||||||
elements += _shapes_to_elements(pat.shapes)
|
elements += _shapes_to_elements(pat.shapes)
|
||||||
elements += _labels_to_texts(pat.labels)
|
elements += _labels_to_texts(pat.labels)
|
||||||
elements += _subpatterns_to_refs(pat.subpatterns)
|
elements += _subpatterns_to_refs(pat.subpatterns)
|
||||||
|
|
||||||
klamath.library.write_struct(stream, name=pat.name.encode('ASCII'), elements=elements)
|
klamath.library.write_struct(stream, name=name.encode('ASCII'), elements=elements)
|
||||||
records.ENDLIB.write(stream, None)
|
records.ENDLIB.write(stream, None)
|
||||||
|
|
||||||
|
|
||||||
def writefile(
|
def writefile(
|
||||||
patterns: Union[Sequence[Pattern], Pattern],
|
library: Mapping[str, Pattern],
|
||||||
filename: Union[str, pathlib.Path],
|
filename: Union[str, pathlib.Path],
|
||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
@ -150,7 +150,7 @@ def writefile(
|
|||||||
Will automatically compress the file if it has a .gz suffix.
|
Will automatically compress the file if it has a .gz suffix.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
patterns: `Pattern` or list of patterns to save
|
library: {name: Pattern} pairs to save.
|
||||||
filename: Filename to save to.
|
filename: Filename to save to.
|
||||||
*args: passed to `write()`
|
*args: passed to `write()`
|
||||||
**kwargs: passed to `write()`
|
**kwargs: passed to `write()`
|
||||||
@ -216,22 +216,14 @@ def read(
|
|||||||
"""
|
"""
|
||||||
library_info = _read_header(stream)
|
library_info = _read_header(stream)
|
||||||
|
|
||||||
patterns = []
|
patterns_dict = {}
|
||||||
found_struct = records.BGNSTR.skip_past(stream)
|
found_struct = records.BGNSTR.skip_past(stream)
|
||||||
while found_struct:
|
while found_struct:
|
||||||
name = records.STRNAME.skip_and_read(stream)
|
name = records.STRNAME.skip_and_read(stream)
|
||||||
pat = read_elements(stream, name=name.decode('ASCII'), raw_mode=raw_mode)
|
pat = read_elements(stream, raw_mode=raw_mode)
|
||||||
patterns.append(pat)
|
patterns_dict[name.decode('ASCII')] = pat
|
||||||
found_struct = records.BGNSTR.skip_past(stream)
|
found_struct = records.BGNSTR.skip_past(stream)
|
||||||
|
|
||||||
# Create a dict of {pattern.name: pattern, ...}, then fix up all subpattern.pattern entries
|
|
||||||
# according to the subpattern.identifier (which is deleted after use).
|
|
||||||
patterns_dict = dict(((p.name, p) for p in patterns))
|
|
||||||
for p in patterns_dict.values():
|
|
||||||
for sp in p.subpatterns:
|
|
||||||
sp.pattern = patterns_dict[sp.identifier[0]]
|
|
||||||
del sp.identifier
|
|
||||||
|
|
||||||
return patterns_dict, library_info
|
return patterns_dict, library_info
|
||||||
|
|
||||||
|
|
||||||
@ -250,7 +242,6 @@ def _read_header(stream: BinaryIO) -> Dict[str, Any]:
|
|||||||
|
|
||||||
def read_elements(
|
def read_elements(
|
||||||
stream: BinaryIO,
|
stream: BinaryIO,
|
||||||
name: str,
|
|
||||||
raw_mode: bool = True,
|
raw_mode: bool = True,
|
||||||
) -> Pattern:
|
) -> Pattern:
|
||||||
"""
|
"""
|
||||||
@ -265,7 +256,7 @@ def read_elements(
|
|||||||
Returns:
|
Returns:
|
||||||
A pattern containing the elements that were read.
|
A pattern containing the elements that were read.
|
||||||
"""
|
"""
|
||||||
pat = Pattern(name)
|
pat = Pattern()
|
||||||
|
|
||||||
elements = klamath.library.read_elements(stream)
|
elements = klamath.library.read_elements(stream)
|
||||||
for element in elements:
|
for element in elements:
|
||||||
@ -276,10 +267,12 @@ def read_elements(
|
|||||||
path = _gpath_to_mpath(element, raw_mode)
|
path = _gpath_to_mpath(element, raw_mode)
|
||||||
pat.shapes.append(path)
|
pat.shapes.append(path)
|
||||||
elif isinstance(element, klamath.elements.Text):
|
elif isinstance(element, klamath.elements.Text):
|
||||||
label = Label(offset=element.xy.astype(float),
|
label = Label(
|
||||||
|
offset=element.xy.astype(float),
|
||||||
layer=element.layer,
|
layer=element.layer,
|
||||||
string=element.string.decode('ASCII'),
|
string=element.string.decode('ASCII'),
|
||||||
annotations=_properties_to_annotations(element.properties))
|
annotations=_properties_to_annotations(element.properties),
|
||||||
|
)
|
||||||
pat.labels.append(label)
|
pat.labels.append(label)
|
||||||
elif isinstance(element, klamath.elements.Reference):
|
elif isinstance(element, klamath.elements.Reference):
|
||||||
pat.subpatterns.append(_ref_to_subpat(element))
|
pat.subpatterns.append(_ref_to_subpat(element))
|
||||||
@ -304,8 +297,7 @@ def _mlayer2gds(mlayer: layer_t) -> Tuple[int, int]:
|
|||||||
|
|
||||||
def _ref_to_subpat(ref: klamath.library.Reference) -> SubPattern:
|
def _ref_to_subpat(ref: klamath.library.Reference) -> SubPattern:
|
||||||
"""
|
"""
|
||||||
Helper function to create a SubPattern from an SREF or AREF. Sets subpat.pattern to None
|
Helper function to create a SubPattern from an SREF or AREF. Sets subpat.target to struct_name.
|
||||||
and sets the instance .identifier to (struct_name,).
|
|
||||||
"""
|
"""
|
||||||
xy = ref.xy.astype(float)
|
xy = ref.xy.astype(float)
|
||||||
offset = xy[0]
|
offset = xy[0]
|
||||||
@ -317,14 +309,15 @@ def _ref_to_subpat(ref: klamath.library.Reference) -> SubPattern:
|
|||||||
repetition = Grid(a_vector=a_vector, b_vector=b_vector,
|
repetition = Grid(a_vector=a_vector, b_vector=b_vector,
|
||||||
a_count=a_count, b_count=b_count)
|
a_count=a_count, b_count=b_count)
|
||||||
|
|
||||||
subpat = SubPattern(pattern=None,
|
subpat = SubPattern(
|
||||||
|
pattern=ref.struct_name.decode('ASCII'),
|
||||||
offset=offset,
|
offset=offset,
|
||||||
rotation=numpy.deg2rad(ref.angle_deg),
|
rotation=numpy.deg2rad(ref.angle_deg),
|
||||||
scale=ref.mag,
|
scale=ref.mag,
|
||||||
mirrored=(ref.invert_y, False),
|
mirrored=(ref.invert_y, False),
|
||||||
annotations=_properties_to_annotations(ref.properties),
|
annotations=_properties_to_annotations(ref.properties),
|
||||||
repetition=repetition)
|
repetition=repetition,
|
||||||
subpat.identifier = (ref.struct_name.decode('ASCII'),)
|
)
|
||||||
return subpat
|
return subpat
|
||||||
|
|
||||||
|
|
||||||
@ -334,7 +327,8 @@ def _gpath_to_mpath(gpath: klamath.library.Path, raw_mode: bool) -> Path:
|
|||||||
else:
|
else:
|
||||||
raise PatternError(f'Unrecognized path type: {gpath.path_type}')
|
raise PatternError(f'Unrecognized path type: {gpath.path_type}')
|
||||||
|
|
||||||
mpath = Path(vertices=gpath.xy.astype(float),
|
mpath = Path(
|
||||||
|
vertices=gpath.xy.astype(float),
|
||||||
layer=gpath.layer,
|
layer=gpath.layer,
|
||||||
width=gpath.width,
|
width=gpath.width,
|
||||||
cap=cap,
|
cap=cap,
|
||||||
@ -348,7 +342,8 @@ def _gpath_to_mpath(gpath: klamath.library.Path, raw_mode: bool) -> Path:
|
|||||||
|
|
||||||
|
|
||||||
def _boundary_to_polygon(boundary: klamath.library.Boundary, raw_mode: bool) -> Polygon:
|
def _boundary_to_polygon(boundary: klamath.library.Boundary, raw_mode: bool) -> Polygon:
|
||||||
return Polygon(vertices=boundary.xy[:-1].astype(float),
|
return Polygon(
|
||||||
|
vertices=boundary.xy[:-1].astype(float),
|
||||||
layer=boundary.layer,
|
layer=boundary.layer,
|
||||||
offset=numpy.zeros(2),
|
offset=numpy.zeros(2),
|
||||||
annotations=_properties_to_annotations(boundary.properties),
|
annotations=_properties_to_annotations(boundary.properties),
|
||||||
@ -359,9 +354,9 @@ def _boundary_to_polygon(boundary: klamath.library.Boundary, raw_mode: bool) ->
|
|||||||
def _subpatterns_to_refs(subpatterns: List[SubPattern]) -> List[klamath.library.Reference]:
|
def _subpatterns_to_refs(subpatterns: List[SubPattern]) -> List[klamath.library.Reference]:
|
||||||
refs = []
|
refs = []
|
||||||
for subpat in subpatterns:
|
for subpat in subpatterns:
|
||||||
if subpat.pattern is None:
|
if subpat.target is None:
|
||||||
continue
|
continue
|
||||||
encoded_name = subpat.pattern.name.encode('ASCII')
|
encoded_name = subpat.target.encode('ASCII')
|
||||||
|
|
||||||
# Note: GDS mirrors first and rotates second
|
# Note: GDS mirrors first and rotates second
|
||||||
mirror_across_x, extra_angle = normalize_mirror(subpat.mirrored)
|
mirror_across_x, extra_angle = normalize_mirror(subpat.mirrored)
|
||||||
@ -377,31 +372,38 @@ def _subpatterns_to_refs(subpatterns: List[SubPattern]) -> List[klamath.library.
|
|||||||
rep.a_vector * rep.a_count,
|
rep.a_vector * rep.a_count,
|
||||||
b_vector * b_count,
|
b_vector * b_count,
|
||||||
]
|
]
|
||||||
aref = klamath.library.Reference(struct_name=encoded_name,
|
aref = klamath.library.Reference(
|
||||||
xy=numpy.round(xy).astype(int),
|
struct_name=encoded_name,
|
||||||
colrow=(numpy.round(rep.a_count), numpy.round(rep.b_count)),
|
xy=rint_cast(xy),
|
||||||
|
colrow=(numpy.rint(rep.a_count), numpy.rint(rep.b_count)),
|
||||||
angle_deg=angle_deg,
|
angle_deg=angle_deg,
|
||||||
invert_y=mirror_across_x,
|
invert_y=mirror_across_x,
|
||||||
mag=subpat.scale,
|
mag=subpat.scale,
|
||||||
properties=properties)
|
properties=properties,
|
||||||
|
)
|
||||||
refs.append(aref)
|
refs.append(aref)
|
||||||
elif rep is None:
|
elif rep is None:
|
||||||
ref = klamath.library.Reference(struct_name=encoded_name,
|
ref = klamath.library.Reference(
|
||||||
xy=numpy.round([subpat.offset]).astype(int),
|
struct_name=encoded_name,
|
||||||
|
xy=rint_cast([subpat.offset]),
|
||||||
colrow=None,
|
colrow=None,
|
||||||
angle_deg=angle_deg,
|
angle_deg=angle_deg,
|
||||||
invert_y=mirror_across_x,
|
invert_y=mirror_across_x,
|
||||||
mag=subpat.scale,
|
mag=subpat.scale,
|
||||||
properties=properties)
|
properties=properties,
|
||||||
|
)
|
||||||
refs.append(ref)
|
refs.append(ref)
|
||||||
else:
|
else:
|
||||||
new_srefs = [klamath.library.Reference(struct_name=encoded_name,
|
new_srefs = [
|
||||||
xy=numpy.round([subpat.offset + dd]).astype(int),
|
klamath.library.Reference(
|
||||||
|
struct_name=encoded_name,
|
||||||
|
xy=rint_cast([subpat.offset + dd]),
|
||||||
colrow=None,
|
colrow=None,
|
||||||
angle_deg=angle_deg,
|
angle_deg=angle_deg,
|
||||||
invert_y=mirror_across_x,
|
invert_y=mirror_across_x,
|
||||||
mag=subpat.scale,
|
mag=subpat.scale,
|
||||||
properties=properties)
|
properties=properties,
|
||||||
|
)
|
||||||
for dd in rep.displacements]
|
for dd in rep.displacements]
|
||||||
refs += new_srefs
|
refs += new_srefs
|
||||||
return refs
|
return refs
|
||||||
@ -443,8 +445,8 @@ def _shapes_to_elements(
|
|||||||
layer, data_type = _mlayer2gds(shape.layer)
|
layer, data_type = _mlayer2gds(shape.layer)
|
||||||
properties = _annotations_to_properties(shape.annotations, 128)
|
properties = _annotations_to_properties(shape.annotations, 128)
|
||||||
if isinstance(shape, Path) and not polygonize_paths:
|
if isinstance(shape, Path) and not polygonize_paths:
|
||||||
xy = numpy.round(shape.vertices + shape.offset).astype(int)
|
xy = rint_cast(shape.vertices + shape.offset)
|
||||||
width = numpy.round(shape.width).astype(int)
|
width = rint_cast(shape.width)
|
||||||
path_type = next(k for k, v in path_cap_map.items() if v == shape.cap) # reverse lookup
|
path_type = next(k for k, v in path_cap_map.items() if v == shape.cap) # reverse lookup
|
||||||
|
|
||||||
extension: Tuple[int, int]
|
extension: Tuple[int, int]
|
||||||
@ -453,30 +455,36 @@ def _shapes_to_elements(
|
|||||||
else:
|
else:
|
||||||
extension = (0, 0)
|
extension = (0, 0)
|
||||||
|
|
||||||
path = klamath.elements.Path(layer=(layer, data_type),
|
path = klamath.elements.Path(
|
||||||
|
layer=(layer, data_type),
|
||||||
xy=xy,
|
xy=xy,
|
||||||
path_type=path_type,
|
path_type=path_type,
|
||||||
width=width,
|
width=width,
|
||||||
extension=extension,
|
extension=extension,
|
||||||
properties=properties)
|
properties=properties,
|
||||||
|
)
|
||||||
elements.append(path)
|
elements.append(path)
|
||||||
elif isinstance(shape, Polygon):
|
elif isinstance(shape, Polygon):
|
||||||
polygon = shape
|
polygon = shape
|
||||||
xy_closed = numpy.empty((polygon.vertices.shape[0] + 1, 2), dtype=numpy.int32)
|
xy_closed = numpy.empty((polygon.vertices.shape[0] + 1, 2), dtype=numpy.int32)
|
||||||
numpy.rint(polygon.vertices + polygon.offset, out=xy_closed[:-1], casting='unsafe')
|
numpy.rint(polygon.vertices + polygon.offset, out=xy_closed[:-1], casting='unsafe')
|
||||||
xy_closed[-1] = xy_closed[0]
|
xy_closed[-1] = xy_closed[0]
|
||||||
boundary = klamath.elements.Boundary(layer=(layer, data_type),
|
boundary = klamath.elements.Boundary(
|
||||||
|
layer=(layer, data_type),
|
||||||
xy=xy_closed,
|
xy=xy_closed,
|
||||||
properties=properties)
|
properties=properties,
|
||||||
|
)
|
||||||
elements.append(boundary)
|
elements.append(boundary)
|
||||||
else:
|
else:
|
||||||
for polygon in shape.to_polygons():
|
for polygon in shape.to_polygons():
|
||||||
xy_closed = numpy.empty((polygon.vertices.shape[0] + 1, 2), dtype=numpy.int32)
|
xy_closed = numpy.empty((polygon.vertices.shape[0] + 1, 2), dtype=numpy.int32)
|
||||||
numpy.rint(polygon.vertices + polygon.offset, out=xy_closed[:-1], casting='unsafe')
|
numpy.rint(polygon.vertices + polygon.offset, out=xy_closed[:-1], casting='unsafe')
|
||||||
xy_closed[-1] = xy_closed[0]
|
xy_closed[-1] = xy_closed[0]
|
||||||
boundary = klamath.elements.Boundary(layer=(layer, data_type),
|
boundary = klamath.elements.Boundary(
|
||||||
|
layer=(layer, data_type),
|
||||||
xy=xy_closed,
|
xy=xy_closed,
|
||||||
properties=properties)
|
properties=properties,
|
||||||
|
)
|
||||||
elements.append(boundary)
|
elements.append(boundary)
|
||||||
return elements
|
return elements
|
||||||
|
|
||||||
@ -486,8 +494,9 @@ def _labels_to_texts(labels: List[Label]) -> List[klamath.elements.Text]:
|
|||||||
for label in labels:
|
for label in labels:
|
||||||
properties = _annotations_to_properties(label.annotations, 128)
|
properties = _annotations_to_properties(label.annotations, 128)
|
||||||
layer, text_type = _mlayer2gds(label.layer)
|
layer, text_type = _mlayer2gds(label.layer)
|
||||||
xy = numpy.round([label.offset]).astype(int)
|
xy = rint_cast([label.offset])
|
||||||
text = klamath.elements.Text(layer=(layer, text_type),
|
text = klamath.elements.Text(
|
||||||
|
layer=(layer, text_type),
|
||||||
xy=xy,
|
xy=xy,
|
||||||
string=label.string.encode('ASCII'),
|
string=label.string.encode('ASCII'),
|
||||||
properties=properties,
|
properties=properties,
|
||||||
@ -496,36 +505,33 @@ def _labels_to_texts(labels: List[Label]) -> List[klamath.elements.Text]:
|
|||||||
invert_y=False,
|
invert_y=False,
|
||||||
width=0,
|
width=0,
|
||||||
path_type=0,
|
path_type=0,
|
||||||
mag=1)
|
mag=1,
|
||||||
|
)
|
||||||
texts.append(text)
|
texts.append(text)
|
||||||
return texts
|
return texts
|
||||||
|
|
||||||
|
|
||||||
def disambiguate_pattern_names(
|
def disambiguate_pattern_names(
|
||||||
patterns: Sequence[Pattern],
|
names: Iterable[str],
|
||||||
max_name_length: int = 32,
|
max_name_length: int = 32,
|
||||||
suffix_length: int = 6,
|
suffix_length: int = 6,
|
||||||
dup_warn_filter: Optional[Callable[[str], bool]] = None,
|
) -> List[str]:
|
||||||
) -> None:
|
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
patterns: List of patterns to disambiguate
|
names: List of pattern names to disambiguate
|
||||||
max_name_length: Names longer than this will be truncated
|
max_name_length: Names longer than this will be truncated
|
||||||
suffix_length: Names which get truncated are truncated by this many extra characters. This is to
|
suffix_length: Names which get truncated are truncated by this many extra characters. This is to
|
||||||
leave room for a suffix if one is necessary.
|
leave room for a suffix if one is necessary.
|
||||||
dup_warn_filter: (optional) Function for suppressing warnings about cell names changing. Receives
|
|
||||||
the cell name and returns `False` if the warning should be suppressed and `True` if it should
|
|
||||||
be displayed. Default displays all warnings.
|
|
||||||
"""
|
"""
|
||||||
used_names = []
|
new_names = []
|
||||||
for pat in set(patterns):
|
for name in names:
|
||||||
# Shorten names which already exceed max-length
|
# Shorten names which already exceed max-length
|
||||||
if len(pat.name) > max_name_length:
|
if len(name) > max_name_length:
|
||||||
shortened_name = pat.name[:max_name_length - suffix_length]
|
shortened_name = name[:max_name_length - suffix_length]
|
||||||
logger.warning(f'Pattern name "{pat.name}" is too long ({len(pat.name)}/{max_name_length} chars),\n'
|
logger.warning(f'Pattern name "{name}" is too long ({len(name)}/{max_name_length} chars),\n'
|
||||||
+ f' shortening to "{shortened_name}" before generating suffix')
|
+ f' shortening to "{shortened_name}" before generating suffix')
|
||||||
else:
|
else:
|
||||||
shortened_name = pat.name
|
shortened_name = name
|
||||||
|
|
||||||
# Remove invalid characters
|
# Remove invalid characters
|
||||||
sanitized_name = re.compile(r'[^A-Za-z0-9_\?\$]').sub('_', shortened_name)
|
sanitized_name = re.compile(r'[^A-Za-z0-9_\?\$]').sub('_', shortened_name)
|
||||||
@ -533,7 +539,7 @@ def disambiguate_pattern_names(
|
|||||||
# Add a suffix that makes the name unique
|
# Add a suffix that makes the name unique
|
||||||
i = 0
|
i = 0
|
||||||
suffixed_name = sanitized_name
|
suffixed_name = sanitized_name
|
||||||
while suffixed_name in used_names or suffixed_name == '':
|
while suffixed_name in new_names or suffixed_name == '':
|
||||||
suffix = base64.b64encode(struct.pack('>Q', i), b'$?').decode('ASCII')
|
suffix = base64.b64encode(struct.pack('>Q', i), b'$?').decode('ASCII')
|
||||||
|
|
||||||
suffixed_name = sanitized_name + '$' + suffix[:-1].lstrip('A')
|
suffixed_name = sanitized_name + '$' + suffix[:-1].lstrip('A')
|
||||||
@ -542,27 +548,25 @@ def disambiguate_pattern_names(
|
|||||||
if sanitized_name == '':
|
if sanitized_name == '':
|
||||||
logger.warning(f'Empty pattern name saved as "{suffixed_name}"')
|
logger.warning(f'Empty pattern name saved as "{suffixed_name}"')
|
||||||
elif suffixed_name != sanitized_name:
|
elif suffixed_name != sanitized_name:
|
||||||
if dup_warn_filter is None or dup_warn_filter(pat.name):
|
if dup_warn_filter is None or dup_warn_filter(name):
|
||||||
logger.warning(f'Pattern name "{pat.name}" ({sanitized_name}) appears multiple times;\n'
|
logger.warning(f'Pattern name "{name}" ({sanitized_name}) appears multiple times;\n'
|
||||||
+ f' renaming to "{suffixed_name}"')
|
+ f' renaming to "{suffixed_name}"')
|
||||||
|
|
||||||
# Encode into a byte-string and perform some final checks
|
# Encode into a byte-string and perform some final checks
|
||||||
encoded_name = suffixed_name.encode('ASCII')
|
encoded_name = suffixed_name.encode('ASCII')
|
||||||
if len(encoded_name) == 0:
|
if len(encoded_name) == 0:
|
||||||
# Should never happen since zero-length names are replaced
|
# Should never happen since zero-length names are replaced
|
||||||
raise PatternError(f'Zero-length name after sanitize+encode,\n originally "{pat.name}"')
|
raise PatternError(f'Zero-length name after sanitize+encode,\n originally "{name}"')
|
||||||
if len(encoded_name) > max_name_length:
|
if len(encoded_name) > max_name_length:
|
||||||
raise PatternError(f'Pattern name "{encoded_name!r}" length > {max_name_length} after encode,\n'
|
raise PatternError(f'Pattern name "{encoded_name!r}" length > {max_name_length} after encode,\n'
|
||||||
+ f' originally "{pat.name}"')
|
+ f' originally "{name}"')
|
||||||
|
|
||||||
pat.name = suffixed_name
|
new_names.append(suffixed_name)
|
||||||
used_names.append(suffixed_name)
|
return new_names
|
||||||
|
|
||||||
|
|
||||||
def load_library(
|
def load_library(
|
||||||
stream: BinaryIO,
|
stream: BinaryIO,
|
||||||
tag: str,
|
|
||||||
is_secondary: Optional[Callable[[str], bool]] = None,
|
|
||||||
*,
|
*,
|
||||||
full_load: bool = False,
|
full_load: bool = False,
|
||||||
) -> Tuple[Library, Dict[str, Any]]:
|
) -> Tuple[Library, Dict[str, Any]]:
|
||||||
@ -577,25 +581,14 @@ def load_library(
|
|||||||
The caller should leave the stream open while the library
|
The caller should leave the stream open while the library
|
||||||
is still in use, since the library will need to access it
|
is still in use, since the library will need to access it
|
||||||
in order to read the structure contents.
|
in order to read the structure contents.
|
||||||
tag: Unique identifier that will be used to identify this data source
|
|
||||||
is_secondary: Function which takes a structure name and returns
|
|
||||||
True if the structure should only be used as a subcell
|
|
||||||
and not appear in the main Library interface.
|
|
||||||
Default always returns False.
|
|
||||||
full_load: If True, force all structures to be read immediately rather
|
full_load: If True, force all structures to be read immediately rather
|
||||||
than as-needed. Since data is read sequentially from the file,
|
than as-needed. Since data is read sequentially from the file, this
|
||||||
this will be faster than using the resulting library's
|
will be faster than using the resulting library's `precache` method.
|
||||||
`precache` method.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Library object, allowing for deferred load of structures.
|
Library object, allowing for deferred load of structures.
|
||||||
Additional library info (dict, same format as from `read`).
|
Additional library info (dict, same format as from `read`).
|
||||||
"""
|
"""
|
||||||
if is_secondary is None:
|
|
||||||
def is_secondary(k: str) -> bool:
|
|
||||||
return False
|
|
||||||
assert(is_secondary is not None)
|
|
||||||
|
|
||||||
stream.seek(0)
|
stream.seek(0)
|
||||||
lib = Library()
|
lib = Library()
|
||||||
|
|
||||||
@ -603,7 +596,7 @@ def load_library(
|
|||||||
# Full load approach (immediately load everything)
|
# Full load approach (immediately load everything)
|
||||||
patterns, library_info = read(stream)
|
patterns, library_info = read(stream)
|
||||||
for name, pattern in patterns.items():
|
for name, pattern in patterns.items():
|
||||||
lib.set_const(name, tag, pattern, secondary=is_secondary(name))
|
lib[name] = lambda: pattern
|
||||||
return lib, library_info
|
return lib, library_info
|
||||||
|
|
||||||
# Normal approach (scan and defer load)
|
# Normal approach (scan and defer load)
|
||||||
@ -613,19 +606,17 @@ def load_library(
|
|||||||
for name_bytes, pos in structs.items():
|
for name_bytes, pos in structs.items():
|
||||||
name = name_bytes.decode('ASCII')
|
name = name_bytes.decode('ASCII')
|
||||||
|
|
||||||
def mkstruct(pos: int = pos, name: str = name) -> Pattern:
|
def mkstruct(pos: int = pos) -> Pattern:
|
||||||
stream.seek(pos)
|
stream.seek(pos)
|
||||||
return read_elements(stream, name, raw_mode=True)
|
return read_elements(stream, raw_mode=True)
|
||||||
|
|
||||||
lib.set_value(name, tag, mkstruct, secondary=is_secondary(name))
|
lib[name] = mkstruct
|
||||||
|
|
||||||
return lib, library_info
|
return lib, library_info
|
||||||
|
|
||||||
|
|
||||||
def load_libraryfile(
|
def load_libraryfile(
|
||||||
filename: Union[str, pathlib.Path],
|
filename: Union[str, pathlib.Path],
|
||||||
tag: str,
|
|
||||||
is_secondary: Optional[Callable[[str], bool]] = None,
|
|
||||||
*,
|
*,
|
||||||
use_mmap: bool = True,
|
use_mmap: bool = True,
|
||||||
full_load: bool = False,
|
full_load: bool = False,
|
||||||
@ -640,8 +631,6 @@ def load_libraryfile(
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
path: filename or path to read from
|
path: filename or path to read from
|
||||||
tag: Unique identifier for library, see `load_library`
|
|
||||||
is_secondary: Function specifying subcess, see `load_library`
|
|
||||||
use_mmap: If `True`, will attempt to memory-map the file instead
|
use_mmap: If `True`, will attempt to memory-map the file instead
|
||||||
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
|
||||||
@ -667,4 +656,4 @@ def load_libraryfile(
|
|||||||
stream = mmap.mmap(base_stream.fileno(), 0, access=mmap.ACCESS_READ)
|
stream = mmap.mmap(base_stream.fileno(), 0, access=mmap.ACCESS_READ)
|
||||||
else:
|
else:
|
||||||
stream = io.BufferedReader(base_stream)
|
stream = io.BufferedReader(base_stream)
|
||||||
return load_library(stream, tag, is_secondary)
|
return load_library(stream, full_load=full_load)
|
||||||
|
@ -11,7 +11,7 @@ Note that OASIS references follow the same convention as `masque`,
|
|||||||
Scaling, rotation, and mirroring apply to individual instances, not grid
|
Scaling, rotation, and mirroring apply to individual instances, not grid
|
||||||
vectors or offsets.
|
vectors or offsets.
|
||||||
"""
|
"""
|
||||||
from typing import List, Any, Dict, Tuple, Callable, Union, Sequence, Iterable, Optional
|
from typing import List, Any, Dict, Tuple, Callable, Union, Sequence, Iterable, Mapping, Optional
|
||||||
import re
|
import re
|
||||||
import io
|
import io
|
||||||
import copy
|
import copy
|
||||||
@ -22,11 +22,12 @@ import pathlib
|
|||||||
import gzip
|
import gzip
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
|
from numpy.typing import ArrayLike, NDArray
|
||||||
import fatamorgana
|
import fatamorgana
|
||||||
import fatamorgana.records as fatrec
|
import fatamorgana.records as fatrec
|
||||||
from fatamorgana.basic import PathExtensionScheme, AString, NString, PropStringReference
|
from fatamorgana.basic import PathExtensionScheme, AString, NString, PropStringReference
|
||||||
|
|
||||||
from .utils import clean_pattern_vertices, is_gzipped
|
from .utils import is_gzipped
|
||||||
from .. import Pattern, SubPattern, PatternError, Label, Shape
|
from .. import Pattern, SubPattern, PatternError, Label, Shape
|
||||||
from ..shapes import Polygon, Path, Circle
|
from ..shapes import Polygon, Path, Circle
|
||||||
from ..repetition import Grid, Arbitrary, Repetition
|
from ..repetition import Grid, Arbitrary, Repetition
|
||||||
@ -47,19 +48,22 @@ path_cap_map = {
|
|||||||
|
|
||||||
#TODO implement more shape types?
|
#TODO implement more shape types?
|
||||||
|
|
||||||
|
def rint_cast(val: ArrayLike) -> NDArray[numpy.int64]:
|
||||||
|
return numpy.rint(val, dtype=numpy.int64, casting='unsafe')
|
||||||
|
|
||||||
|
|
||||||
def build(
|
def build(
|
||||||
patterns: Union[Pattern, Sequence[Pattern]],
|
library: Mapping[str, Pattern], # NOTE: Pattern here should be treated as immutable!
|
||||||
units_per_micron: int,
|
units_per_micron: int,
|
||||||
layer_map: Optional[Dict[str, Union[int, Tuple[int, int]]]] = None,
|
layer_map: Optional[Dict[str, Union[int, Tuple[int, int]]]] = None,
|
||||||
*,
|
*,
|
||||||
modify_originals: bool = False,
|
disambiguate_func: Optional[Callable[[Iterable[str]], List[str]]] = None,
|
||||||
disambiguate_func: Optional[Callable[[Iterable[Pattern]], None]] = None,
|
|
||||||
annotations: Optional[annotations_t] = None,
|
annotations: Optional[annotations_t] = None,
|
||||||
) -> fatamorgana.OasisLayout:
|
) -> fatamorgana.OasisLayout:
|
||||||
"""
|
"""
|
||||||
Convert a `Pattern` or list of patterns to an OASIS stream, writing patterns
|
Convert a collection of {name: Pattern} pairs to an OASIS stream, writing patterns
|
||||||
as OASIS cells, subpatterns as Placement records, and other shapes and labels
|
as OASIS cells, subpatterns as Placement records, and mapping other shapes and labels
|
||||||
mapped to equivalent record types (Polygon, Path, Circle, Text).
|
to equivalent record types (Polygon, Path, Circle, Text).
|
||||||
Other shape types may be converted to polygons if no equivalent
|
Other shape types may be converted to polygons if no equivalent
|
||||||
record type exists (or is not implemented here yet).
|
record type exists (or is not implemented here yet).
|
||||||
|
|
||||||
@ -75,7 +79,7 @@ def build(
|
|||||||
prior to calling this function.
|
prior to calling this function.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
patterns: A Pattern or list of patterns to convert.
|
library: A {name: Pattern} mapping of patterns to write.
|
||||||
units_per_micron: Written into the OASIS file, number of grid steps per micrometer.
|
units_per_micron: Written into the OASIS file, number of grid steps per micrometer.
|
||||||
All distances are assumed to be an integer multiple of the grid step, and are stored as such.
|
All distances are assumed to be an integer multiple of the grid step, and are stored as such.
|
||||||
layer_map: Dictionary which translates layer names into layer numbers. If this argument is
|
layer_map: Dictionary which translates layer names into layer numbers. If this argument is
|
||||||
@ -86,11 +90,8 @@ def build(
|
|||||||
into numbers, omit this argument, and manually generate the required
|
into numbers, omit this argument, and manually generate the required
|
||||||
`fatamorgana.records.LayerName` entries.
|
`fatamorgana.records.LayerName` entries.
|
||||||
Default is an empty dict (no names provided).
|
Default is an empty dict (no names provided).
|
||||||
modify_originals: If `True`, the original pattern is modified as part of the writing
|
disambiguate_func: Function which takes a list of pattern names and returns a list of names
|
||||||
process. Otherwise, a copy is made and `deepunlock()`-ed.
|
altered to be valid and unique. Default is `disambiguate_pattern_names`.
|
||||||
Default `False`.
|
|
||||||
disambiguate_func: Function which takes a list of patterns and alters them
|
|
||||||
to make their names valid and unique. Default is `disambiguate_pattern_names`.
|
|
||||||
annotations: dictionary of key-value pairs which are saved as library-level properties
|
annotations: dictionary of key-value pairs which are saved as library-level properties
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@ -108,9 +109,6 @@ def build(
|
|||||||
if annotations is None:
|
if annotations is None:
|
||||||
annotations = {}
|
annotations = {}
|
||||||
|
|
||||||
if not modify_originals:
|
|
||||||
patterns = [p.deepunlock() for p in copy.deepcopy(patterns)]
|
|
||||||
|
|
||||||
# Create library
|
# Create library
|
||||||
lib = fatamorgana.OasisLayout(unit=units_per_micron, validation=None)
|
lib = fatamorgana.OasisLayout(unit=units_per_micron, validation=None)
|
||||||
lib.properties = annotations_to_properties(annotations)
|
lib.properties = annotations_to_properties(annotations)
|
||||||
@ -119,10 +117,12 @@ def build(
|
|||||||
for name, layer_num in layer_map.items():
|
for name, layer_num in layer_map.items():
|
||||||
layer, data_type = _mlayer2oas(layer_num)
|
layer, data_type = _mlayer2oas(layer_num)
|
||||||
lib.layers += [
|
lib.layers += [
|
||||||
fatrec.LayerName(nstring=name,
|
fatrec.LayerName(
|
||||||
|
nstring=name,
|
||||||
layer_interval=(layer, layer),
|
layer_interval=(layer, layer),
|
||||||
type_interval=(data_type, data_type),
|
type_interval=(data_type, data_type),
|
||||||
is_textlayer=tt)
|
is_textlayer=tt,
|
||||||
|
)
|
||||||
for tt in (True, False)]
|
for tt in (True, False)]
|
||||||
|
|
||||||
def layer2oas(mlayer: layer_t) -> Tuple[int, int]:
|
def layer2oas(mlayer: layer_t) -> Tuple[int, int]:
|
||||||
@ -132,17 +132,14 @@ def build(
|
|||||||
else:
|
else:
|
||||||
layer2oas = _mlayer2oas
|
layer2oas = _mlayer2oas
|
||||||
|
|
||||||
# Get a dict of id(pattern) -> pattern
|
old_names = list(library.keys())
|
||||||
patterns_by_id = {id(pattern): pattern for pattern in patterns}
|
new_names = disambiguate_func(old_names)
|
||||||
for pattern in patterns:
|
renamed_lib = {new_name: library[old_name]
|
||||||
for i, p in pattern.referenced_patterns_by_id().items():
|
for old_name, new_name in zip(old_names, new_names)}
|
||||||
patterns_by_id[i] = p
|
|
||||||
|
|
||||||
disambiguate_func(patterns_by_id.values())
|
|
||||||
|
|
||||||
# Now create a structure for each pattern
|
# Now create a structure for each pattern
|
||||||
for pat in patterns_by_id.values():
|
for name, pat in renamed_lib.items():
|
||||||
structure = fatamorgana.Cell(name=pat.name)
|
structure = fatamorgana.Cell(name=name)
|
||||||
lib.cells.append(structure)
|
lib.cells.append(structure)
|
||||||
|
|
||||||
structure.properties += annotations_to_properties(pat.annotations)
|
structure.properties += annotations_to_properties(pat.annotations)
|
||||||
@ -229,7 +226,6 @@ def readfile(
|
|||||||
|
|
||||||
def read(
|
def read(
|
||||||
stream: io.BufferedIOBase,
|
stream: io.BufferedIOBase,
|
||||||
clean_vertices: bool = True,
|
|
||||||
) -> Tuple[Dict[str, Pattern], Dict[str, Any]]:
|
) -> Tuple[Dict[str, Pattern], Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Read a OASIS file and translate it into a dict of Pattern objects. OASIS cells are
|
Read a OASIS file and translate it into a dict of Pattern objects. OASIS cells are
|
||||||
@ -243,9 +239,6 @@ def read(
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
stream: Stream to read from.
|
stream: Stream to read from.
|
||||||
clean_vertices: If `True`, remove any redundant vertices when loading polygons.
|
|
||||||
The cleaning process removes any polygons with zero area or <3 vertices.
|
|
||||||
Default `True`.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
- Dict of `pattern_name`:`Pattern`s generated from OASIS cells
|
- Dict of `pattern_name`:`Pattern`s generated from OASIS cells
|
||||||
@ -264,14 +257,14 @@ def read(
|
|||||||
layer_map[str(layer_name.nstring)] = layer_name
|
layer_map[str(layer_name.nstring)] = layer_name
|
||||||
library_info['layer_map'] = layer_map
|
library_info['layer_map'] = layer_map
|
||||||
|
|
||||||
patterns = []
|
patterns_dict = {}
|
||||||
for cell in lib.cells:
|
for cell in lib.cells:
|
||||||
if isinstance(cell.name, int):
|
if isinstance(cell.name, int):
|
||||||
cell_name = lib.cellnames[cell.name].nstring.string
|
cell_name = lib.cellnames[cell.name].nstring.string
|
||||||
else:
|
else:
|
||||||
cell_name = cell.name.string
|
cell_name = cell.name.string
|
||||||
|
|
||||||
pat = Pattern(name=cell_name)
|
pat = Pattern()
|
||||||
for element in cell.geometry:
|
for element in cell.geometry:
|
||||||
if isinstance(element, fatrec.XElement):
|
if isinstance(element, fatrec.XElement):
|
||||||
logger.warning('Skipping XElement record')
|
logger.warning('Skipping XElement record')
|
||||||
@ -450,19 +443,7 @@ def read(
|
|||||||
for placement in cell.placements:
|
for placement in cell.placements:
|
||||||
pat.subpatterns.append(_placement_to_subpat(placement, lib))
|
pat.subpatterns.append(_placement_to_subpat(placement, lib))
|
||||||
|
|
||||||
if clean_vertices:
|
patterns_dict[name] = pat
|
||||||
clean_pattern_vertices(pat)
|
|
||||||
patterns.append(pat)
|
|
||||||
|
|
||||||
# Create a dict of {pattern.name: pattern, ...}, then fix up all subpattern.pattern entries
|
|
||||||
# according to the subpattern.identifier (which is deleted after use).
|
|
||||||
patterns_dict = dict(((p.name, p) for p in patterns))
|
|
||||||
for p in patterns_dict.values():
|
|
||||||
for sp in p.subpatterns:
|
|
||||||
ident = sp.identifier[0]
|
|
||||||
name = ident if isinstance(ident, str) else lib.cellnames[ident].nstring.string
|
|
||||||
sp.pattern = patterns_dict[name]
|
|
||||||
del sp.identifier
|
|
||||||
|
|
||||||
return patterns_dict, library_info
|
return patterns_dict, library_info
|
||||||
|
|
||||||
@ -486,8 +467,7 @@ def _mlayer2oas(mlayer: layer_t) -> Tuple[int, int]:
|
|||||||
|
|
||||||
def _placement_to_subpat(placement: fatrec.Placement, lib: fatamorgana.OasisLayout) -> SubPattern:
|
def _placement_to_subpat(placement: fatrec.Placement, lib: fatamorgana.OasisLayout) -> SubPattern:
|
||||||
"""
|
"""
|
||||||
Helper function to create a SubPattern from a placment. Sets subpat.pattern to None
|
Helper function to create a SubPattern from a placment. Sets subpat.target to the placemen name.
|
||||||
and sets the instance .identifier to (struct_name,).
|
|
||||||
"""
|
"""
|
||||||
assert(not isinstance(placement.repetition, fatamorgana.ReuseRepetition))
|
assert(not isinstance(placement.repetition, fatamorgana.ReuseRepetition))
|
||||||
xy = numpy.array((placement.x, placement.y))
|
xy = numpy.array((placement.x, placement.y))
|
||||||
@ -499,14 +479,15 @@ def _placement_to_subpat(placement: fatrec.Placement, lib: fatamorgana.OasisLayo
|
|||||||
rotation = 0
|
rotation = 0
|
||||||
else:
|
else:
|
||||||
rotation = numpy.deg2rad(float(placement.angle))
|
rotation = numpy.deg2rad(float(placement.angle))
|
||||||
subpat = SubPattern(offset=xy,
|
subpat = SubPattern(
|
||||||
pattern=None,
|
target=name,
|
||||||
|
offset=xy,
|
||||||
mirrored=(placement.flip, False),
|
mirrored=(placement.flip, False),
|
||||||
rotation=rotation,
|
rotation=rotation,
|
||||||
scale=float(mag),
|
scale=float(mag),
|
||||||
identifier=(name,),
|
|
||||||
repetition=repetition_fata2masq(placement.repetition),
|
repetition=repetition_fata2masq(placement.repetition),
|
||||||
annotations=annotations)
|
annotations=annotations,
|
||||||
|
)
|
||||||
return subpat
|
return subpat
|
||||||
|
|
||||||
|
|
||||||
@ -515,17 +496,17 @@ def _subpatterns_to_placements(
|
|||||||
) -> List[fatrec.Placement]:
|
) -> List[fatrec.Placement]:
|
||||||
refs = []
|
refs = []
|
||||||
for subpat in subpatterns:
|
for subpat in subpatterns:
|
||||||
if subpat.pattern is None:
|
if subpat.target is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Note: OASIS mirrors first and rotates second
|
# Note: OASIS mirrors first and rotates second
|
||||||
mirror_across_x, extra_angle = normalize_mirror(subpat.mirrored)
|
mirror_across_x, extra_angle = normalize_mirror(subpat.mirrored)
|
||||||
frep, rep_offset = repetition_masq2fata(subpat.repetition)
|
frep, rep_offset = repetition_masq2fata(subpat.repetition)
|
||||||
|
|
||||||
offset = numpy.round(subpat.offset + rep_offset).astype(int)
|
offset = rint_cast(subpat.offset + rep_offset)
|
||||||
angle = numpy.rad2deg(subpat.rotation + extra_angle) % 360
|
angle = numpy.rad2deg(subpat.rotation + extra_angle) % 360
|
||||||
ref = fatrec.Placement(
|
ref = fatrec.Placement(
|
||||||
name=subpat.pattern.name,
|
name=subpat.target,
|
||||||
flip=mirror_across_x,
|
flip=mirror_across_x,
|
||||||
angle=angle,
|
angle=angle,
|
||||||
magnification=subpat.scale,
|
magnification=subpat.scale,
|
||||||
@ -549,24 +530,27 @@ def _shapes_to_elements(
|
|||||||
repetition, rep_offset = repetition_masq2fata(shape.repetition)
|
repetition, rep_offset = repetition_masq2fata(shape.repetition)
|
||||||
properties = annotations_to_properties(shape.annotations)
|
properties = annotations_to_properties(shape.annotations)
|
||||||
if isinstance(shape, Circle):
|
if isinstance(shape, Circle):
|
||||||
offset = numpy.round(shape.offset + rep_offset).astype(int)
|
offset = rint_cast(shape.offset + rep_offset)
|
||||||
radius = numpy.round(shape.radius).astype(int)
|
radius = rint_cast(shape.radius)
|
||||||
circle = fatrec.Circle(layer=layer,
|
circle = fatrec.Circle(
|
||||||
|
layer=layer,
|
||||||
datatype=datatype,
|
datatype=datatype,
|
||||||
radius=radius,
|
radius=radius,
|
||||||
x=offset[0],
|
x=offset[0],
|
||||||
y=offset[1],
|
y=offset[1],
|
||||||
properties=properties,
|
properties=properties,
|
||||||
repetition=repetition)
|
repetition=repetition,
|
||||||
|
)
|
||||||
elements.append(circle)
|
elements.append(circle)
|
||||||
elif isinstance(shape, Path):
|
elif isinstance(shape, Path):
|
||||||
xy = numpy.round(shape.offset + shape.vertices[0] + rep_offset).astype(int)
|
xy = rint_cast(shape.offset + shape.vertices[0] + rep_offset)
|
||||||
deltas = numpy.round(numpy.diff(shape.vertices, axis=0)).astype(int)
|
deltas = rint_cast(numpy.diff(shape.vertices, axis=0))
|
||||||
half_width = numpy.round(shape.width / 2).astype(int)
|
half_width = rint_cast(shape.width / 2)
|
||||||
path_type = next(k for k, v in path_cap_map.items() if v == shape.cap) # reverse lookup
|
path_type = next(k for k, v in path_cap_map.items() if v == shape.cap) # reverse lookup
|
||||||
extension_start = (path_type, shape.cap_extensions[0] if shape.cap_extensions is not None else None)
|
extension_start = (path_type, shape.cap_extensions[0] if shape.cap_extensions is not None else None)
|
||||||
extension_end = (path_type, shape.cap_extensions[1] if shape.cap_extensions is not None else None)
|
extension_end = (path_type, shape.cap_extensions[1] if shape.cap_extensions is not None else None)
|
||||||
path = fatrec.Path(layer=layer,
|
path = fatrec.Path(
|
||||||
|
layer=layer,
|
||||||
datatype=datatype,
|
datatype=datatype,
|
||||||
point_list=deltas,
|
point_list=deltas,
|
||||||
half_width=half_width,
|
half_width=half_width,
|
||||||
@ -580,15 +564,17 @@ def _shapes_to_elements(
|
|||||||
elements.append(path)
|
elements.append(path)
|
||||||
else:
|
else:
|
||||||
for polygon in shape.to_polygons():
|
for polygon in shape.to_polygons():
|
||||||
xy = numpy.round(polygon.offset + polygon.vertices[0] + rep_offset).astype(int)
|
xy = rint_cast(polygon.offset + polygon.vertices[0] + rep_offset)
|
||||||
points = numpy.round(numpy.diff(polygon.vertices, axis=0)).astype(int)
|
points = rint_cast(numpy.diff(polygon.vertices, axis=0))
|
||||||
elements.append(fatrec.Polygon(layer=layer,
|
elements.append(fatrec.Polygon(
|
||||||
|
layer=layer,
|
||||||
datatype=datatype,
|
datatype=datatype,
|
||||||
x=xy[0],
|
x=xy[0],
|
||||||
y=xy[1],
|
y=xy[1],
|
||||||
point_list=points,
|
point_list=points,
|
||||||
properties=properties,
|
properties=properties,
|
||||||
repetition=repetition))
|
repetition=repetition,
|
||||||
|
))
|
||||||
return elements
|
return elements
|
||||||
|
|
||||||
|
|
||||||
@ -600,29 +586,31 @@ def _labels_to_texts(
|
|||||||
for label in labels:
|
for label in labels:
|
||||||
layer, datatype = layer2oas(label.layer)
|
layer, datatype = layer2oas(label.layer)
|
||||||
repetition, rep_offset = repetition_masq2fata(label.repetition)
|
repetition, rep_offset = repetition_masq2fata(label.repetition)
|
||||||
xy = numpy.round(label.offset + rep_offset).astype(int)
|
xy = rint_cast(label.offset + rep_offset)
|
||||||
properties = annotations_to_properties(label.annotations)
|
properties = annotations_to_properties(label.annotations)
|
||||||
texts.append(fatrec.Text(layer=layer,
|
texts.append(fatrec.Text(
|
||||||
|
layer=layer,
|
||||||
datatype=datatype,
|
datatype=datatype,
|
||||||
x=xy[0],
|
x=xy[0],
|
||||||
y=xy[1],
|
y=xy[1],
|
||||||
string=label.string,
|
string=label.string,
|
||||||
properties=properties,
|
properties=properties,
|
||||||
repetition=repetition))
|
repetition=repetition,
|
||||||
|
))
|
||||||
return texts
|
return texts
|
||||||
|
|
||||||
|
|
||||||
def disambiguate_pattern_names(
|
def disambiguate_pattern_names(
|
||||||
patterns,
|
names: Iterable[str],
|
||||||
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
|
||||||
) -> None:
|
) -> List[str]:
|
||||||
used_names = []
|
new_names = []
|
||||||
for pat in patterns:
|
for name in names:
|
||||||
sanitized_name = re.compile(r'[^A-Za-z0-9_\?\$]').sub('_', pat.name)
|
sanitized_name = re.compile(r'[^A-Za-z0-9_\?\$]').sub('_', name)
|
||||||
|
|
||||||
i = 0
|
i = 0
|
||||||
suffixed_name = sanitized_name
|
suffixed_name = sanitized_name
|
||||||
while suffixed_name in used_names or suffixed_name == '':
|
while suffixed_name in new_names or suffixed_name == '':
|
||||||
suffix = base64.b64encode(struct.pack('>Q', i), b'$?').decode('ASCII')
|
suffix = base64.b64encode(struct.pack('>Q', i), b'$?').decode('ASCII')
|
||||||
|
|
||||||
suffixed_name = sanitized_name + '$' + suffix[:-1].lstrip('A')
|
suffixed_name = sanitized_name + '$' + suffix[:-1].lstrip('A')
|
||||||
@ -631,16 +619,16 @@ def disambiguate_pattern_names(
|
|||||||
if sanitized_name == '':
|
if sanitized_name == '':
|
||||||
logger.warning(f'Empty pattern name saved as "{suffixed_name}"')
|
logger.warning(f'Empty pattern name saved as "{suffixed_name}"')
|
||||||
elif suffixed_name != sanitized_name:
|
elif suffixed_name != sanitized_name:
|
||||||
if dup_warn_filter is None or dup_warn_filter(pat.name):
|
if dup_warn_filter is None or dup_warn_filter(name):
|
||||||
logger.warning(f'Pattern name "{pat.name}" ({sanitized_name}) appears multiple times;\n'
|
logger.warning(f'Pattern name "{name}" ({sanitized_name}) appears multiple times;\n'
|
||||||
+ f' renaming to "{suffixed_name}"')
|
+ f' renaming to "{suffixed_name}"')
|
||||||
|
|
||||||
if len(suffixed_name) == 0:
|
if len(suffixed_name) == 0:
|
||||||
# Should never happen since zero-length names are replaced
|
# Should never happen since zero-length names are replaced
|
||||||
raise PatternError(f'Zero-length name after sanitize+encode,\n originally "{pat.name}"')
|
raise PatternError(f'Zero-length name after sanitize+encode,\n originally "{name}"')
|
||||||
|
|
||||||
pat.name = suffixed_name
|
new_names.append(suffixed_name)
|
||||||
used_names.append(suffixed_name)
|
return new_names
|
||||||
|
|
||||||
|
|
||||||
def repetition_fata2masq(
|
def repetition_fata2masq(
|
||||||
|
@ -18,7 +18,7 @@ Notes:
|
|||||||
* GDS does not support library- or structure-level annotations
|
* GDS does not support library- or structure-level annotations
|
||||||
"""
|
"""
|
||||||
from typing import List, Any, Dict, Tuple, Callable, Union, Iterable, Optional
|
from typing import List, Any, Dict, Tuple, Callable, Union, Iterable, Optional
|
||||||
from typing import Sequence
|
from typing import Sequence, Mapping
|
||||||
import re
|
import re
|
||||||
import io
|
import io
|
||||||
import copy
|
import copy
|
||||||
@ -59,13 +59,13 @@ def rint_cast(val: ArrayLike) -> NDArray[numpy.int32]:
|
|||||||
|
|
||||||
|
|
||||||
def build(
|
def build(
|
||||||
patterns: Union[Pattern, Sequence[Pattern]],
|
library: Mapping[str, Pattern],
|
||||||
meters_per_unit: float,
|
meters_per_unit: float,
|
||||||
logical_units_per_unit: float = 1,
|
logical_units_per_unit: float = 1,
|
||||||
library_name: str = 'masque-gdsii-write',
|
library_name: str = 'masque-gdsii-write',
|
||||||
*,
|
*,
|
||||||
modify_originals: bool = False,
|
modify_originals: bool = False,
|
||||||
disambiguate_func: Callable[[Iterable[Pattern]], None] = None,
|
disambiguate_func: Callable[[Iterable[str]], List[str]] = None,
|
||||||
) -> gdsii.library.Library:
|
) -> gdsii.library.Library:
|
||||||
"""
|
"""
|
||||||
Convert a `Pattern` or list of patterns to a GDSII stream, by first calling
|
Convert a `Pattern` or list of patterns to a GDSII stream, by first calling
|
||||||
@ -86,7 +86,7 @@ def build(
|
|||||||
prior to calling this function.
|
prior to calling this function.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
patterns: A Pattern or list of patterns to convert.
|
library: A {name: Pattern} mapping of patterns to write.
|
||||||
meters_per_unit: Written into the GDSII file, meters per (database) length unit.
|
meters_per_unit: Written into the GDSII file, meters per (database) length unit.
|
||||||
All distances are assumed to be an integer multiple of this unit, and are stored as such.
|
All distances are assumed to be an integer multiple of this unit, and are stored as such.
|
||||||
logical_units_per_unit: Written into the GDSII file. Allows the GDSII to specify a
|
logical_units_per_unit: Written into the GDSII file. Allows the GDSII to specify a
|
||||||
@ -95,27 +95,29 @@ def build(
|
|||||||
library_name: Library name written into the GDSII file.
|
library_name: Library name written into the GDSII file.
|
||||||
Default 'masque-gdsii-write'.
|
Default 'masque-gdsii-write'.
|
||||||
modify_originals: If `True`, the original pattern is modified as part of the writing
|
modify_originals: If `True`, the original pattern is modified as part of the writing
|
||||||
process. Otherwise, a copy is made and `deepunlock()`-ed.
|
process. Otherwise, a copy is made.
|
||||||
Default `False`.
|
Default `False`.
|
||||||
disambiguate_func: Function which takes a list of patterns and alters them
|
disambiguate_func: Function which takes a list of pattern names and returns a list of names
|
||||||
to make their names valid and unique. Default is `disambiguate_pattern_names`, which
|
altered to be valid and unique. Default is `disambiguate_pattern_names`, which
|
||||||
attempts to adhere to the GDSII standard as well as possible.
|
attempts to adhere to the GDSII standard reasonably well.
|
||||||
WARNING: No additional error checking is performed on the results.
|
WARNING: No additional error checking is performed on the results.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
`gdsii.library.Library`
|
`gdsii.library.Library`
|
||||||
"""
|
"""
|
||||||
if isinstance(patterns, Pattern):
|
|
||||||
patterns = [patterns]
|
|
||||||
|
|
||||||
if disambiguate_func is None:
|
if disambiguate_func is None:
|
||||||
disambiguate_func = disambiguate_pattern_names # type: ignore
|
disambiguate_func = disambiguate_pattern_names
|
||||||
assert(disambiguate_func is not None) # placate mypy
|
|
||||||
|
|
||||||
if not modify_originals:
|
if not modify_originals:
|
||||||
patterns = [p.deepunlock() for p in copy.deepcopy(patterns)]
|
library = copy.deepcopy(library)
|
||||||
|
|
||||||
patterns = [p.wrap_repeated_shapes() for p in patterns]
|
for p in library.values():
|
||||||
|
library.add(p.wrap_repeated_shapes())
|
||||||
|
|
||||||
|
old_names = list(library.keys())
|
||||||
|
new_names = disambiguate_func(old_names)
|
||||||
|
renamed_lib = {new_name: library[old_name]
|
||||||
|
for old_name, new_name in zip(old_names, new_names)}
|
||||||
|
|
||||||
# Create library
|
# Create library
|
||||||
lib = gdsii.library.Library(version=600,
|
lib = gdsii.library.Library(version=600,
|
||||||
@ -123,17 +125,9 @@ def build(
|
|||||||
logical_unit=logical_units_per_unit,
|
logical_unit=logical_units_per_unit,
|
||||||
physical_unit=meters_per_unit)
|
physical_unit=meters_per_unit)
|
||||||
|
|
||||||
# Get a dict of id(pattern) -> pattern
|
|
||||||
patterns_by_id = {id(pattern): pattern for pattern in patterns}
|
|
||||||
for pattern in patterns:
|
|
||||||
for i, p in pattern.referenced_patterns_by_id().items():
|
|
||||||
patterns_by_id[i] = p
|
|
||||||
|
|
||||||
disambiguate_func(patterns_by_id.values())
|
|
||||||
|
|
||||||
# Now create a structure for each pattern, and add in any Boundary and SREF elements
|
# Now create a structure for each pattern, and add in any Boundary and SREF elements
|
||||||
for pat in patterns_by_id.values():
|
for name, pat in renamed_lib.items():
|
||||||
structure = gdsii.structure.Structure(name=pat.name.encode('ASCII'))
|
structure = gdsii.structure.Structure(name=name.encode('ASCII'))
|
||||||
lib.append(structure)
|
lib.append(structure)
|
||||||
|
|
||||||
structure += _shapes_to_elements(pat.shapes)
|
structure += _shapes_to_elements(pat.shapes)
|
||||||
@ -144,7 +138,7 @@ def build(
|
|||||||
|
|
||||||
|
|
||||||
def write(
|
def write(
|
||||||
patterns: Union[Pattern, Sequence[Pattern]],
|
library: Mapping[str, Pattern],
|
||||||
stream: io.BufferedIOBase,
|
stream: io.BufferedIOBase,
|
||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
@ -154,31 +148,31 @@ def write(
|
|||||||
See `masque.file.gdsii.build()` for details.
|
See `masque.file.gdsii.build()` for details.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
patterns: A Pattern or list of patterns to write to file.
|
library: A {name: Pattern} mapping of patterns to write.
|
||||||
stream: Stream to write to.
|
stream: Stream to write to.
|
||||||
*args: passed to `masque.file.gdsii.build()`
|
*args: passed to `masque.file.gdsii.build()`
|
||||||
**kwargs: passed to `masque.file.gdsii.build()`
|
**kwargs: passed to `masque.file.gdsii.build()`
|
||||||
"""
|
"""
|
||||||
lib = build(patterns, *args, **kwargs)
|
lib = build(library, *args, **kwargs)
|
||||||
lib.save(stream)
|
lib.save(stream)
|
||||||
return
|
return
|
||||||
|
|
||||||
def writefile(
|
def writefile(
|
||||||
patterns: Union[Sequence[Pattern], Pattern],
|
library: Mapping[str, Pattern],
|
||||||
filename: Union[str, pathlib.Path],
|
filename: Union[str, pathlib.Path],
|
||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Wrapper for `masque.file.gdsii.write()` that takes a filename or path instead of a stream.
|
Wrapper for `write()` that takes a filename or path instead of a stream.
|
||||||
|
|
||||||
Will automatically compress the file if it has a .gz suffix.
|
Will automatically compress the file if it has a .gz suffix.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
patterns: `Pattern` or list of patterns to save
|
library: {name: Pattern} pairs to save.
|
||||||
filename: Filename to save to.
|
filename: Filename to save to.
|
||||||
*args: passed to `masque.file.gdsii.write`
|
*args: passed to `write()`
|
||||||
**kwargs: passed to `masque.file.gdsii.write`
|
**kwargs: passed to `write()`
|
||||||
"""
|
"""
|
||||||
path = pathlib.Path(filename)
|
path = pathlib.Path(filename)
|
||||||
if path.suffix == '.gz':
|
if path.suffix == '.gz':
|
||||||
@ -196,14 +190,14 @@ def readfile(
|
|||||||
**kwargs,
|
**kwargs,
|
||||||
) -> Tuple[Dict[str, Pattern], Dict[str, Any]]:
|
) -> Tuple[Dict[str, Pattern], Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Wrapper for `masque.file.gdsii.read()` that takes a filename or path instead of a stream.
|
Wrapper for `read()` that takes a filename or path instead of a stream.
|
||||||
|
|
||||||
Will automatically decompress gzipped files.
|
Will automatically decompress gzipped files.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
filename: Filename to save to.
|
filename: Filename to save to.
|
||||||
*args: passed to `masque.file.gdsii.read`
|
*args: passed to `read()`
|
||||||
**kwargs: passed to `masque.file.gdsii.read`
|
**kwargs: passed to `read()`
|
||||||
"""
|
"""
|
||||||
path = pathlib.Path(filename)
|
path = pathlib.Path(filename)
|
||||||
if is_gzipped(path):
|
if is_gzipped(path):
|
||||||
@ -251,9 +245,10 @@ def read(
|
|||||||
|
|
||||||
raw_mode = True # Whether to construct shapes in raw mode (less error checking)
|
raw_mode = True # Whether to construct shapes in raw mode (less error checking)
|
||||||
|
|
||||||
patterns = []
|
patterns_dict = {}
|
||||||
for structure in lib:
|
for structure in lib:
|
||||||
pat = Pattern(name=structure.name.decode('ASCII'))
|
pat = Pattern()
|
||||||
|
name=structure.name.decode('ASCII')
|
||||||
for element in structure:
|
for element in structure:
|
||||||
# Switch based on element type:
|
# Switch based on element type:
|
||||||
if isinstance(element, gdsii.elements.Boundary):
|
if isinstance(element, gdsii.elements.Boundary):
|
||||||
@ -275,15 +270,7 @@ def read(
|
|||||||
|
|
||||||
if clean_vertices:
|
if clean_vertices:
|
||||||
clean_pattern_vertices(pat)
|
clean_pattern_vertices(pat)
|
||||||
patterns.append(pat)
|
patterns_dict[name] = pat
|
||||||
|
|
||||||
# Create a dict of {pattern.name: pattern, ...}, then fix up all subpattern.pattern entries
|
|
||||||
# according to the subpattern.identifier (which is deleted after use).
|
|
||||||
patterns_dict = dict(((p.name, p) for p in patterns))
|
|
||||||
for p in patterns_dict.values():
|
|
||||||
for sp in p.subpatterns:
|
|
||||||
sp.pattern = patterns_dict[sp.identifier[0].decode('ASCII')]
|
|
||||||
del sp.identifier
|
|
||||||
|
|
||||||
return patterns_dict, library_info
|
return patterns_dict, library_info
|
||||||
|
|
||||||
@ -309,8 +296,7 @@ def _ref_to_subpat(
|
|||||||
gdsii.elements.ARef]
|
gdsii.elements.ARef]
|
||||||
) -> SubPattern:
|
) -> SubPattern:
|
||||||
"""
|
"""
|
||||||
Helper function to create a SubPattern from an SREF or AREF. Sets subpat.pattern to None
|
Helper function to create a SubPattern from an SREF or AREF. Sets subpat.target to struct_name.
|
||||||
and sets the instance .identifier to (struct_name,).
|
|
||||||
|
|
||||||
NOTE: "Absolute" means not affected by parent elements.
|
NOTE: "Absolute" means not affected by parent elements.
|
||||||
That's not currently supported by masque at all (and not planned).
|
That's not currently supported by masque at all (and not planned).
|
||||||
@ -351,7 +337,6 @@ def _ref_to_subpat(
|
|||||||
mirrored=(mirror_across_x, False),
|
mirrored=(mirror_across_x, False),
|
||||||
annotations=_properties_to_annotations(element.properties),
|
annotations=_properties_to_annotations(element.properties),
|
||||||
repetition=repetition)
|
repetition=repetition)
|
||||||
subpat.identifier = (element.struct_name,)
|
|
||||||
return subpat
|
return subpat
|
||||||
|
|
||||||
|
|
||||||
@ -395,9 +380,9 @@ def _subpatterns_to_refs(
|
|||||||
) -> List[Union[gdsii.elements.ARef, gdsii.elements.SRef]]:
|
) -> List[Union[gdsii.elements.ARef, gdsii.elements.SRef]]:
|
||||||
refs = []
|
refs = []
|
||||||
for subpat in subpatterns:
|
for subpat in subpatterns:
|
||||||
if subpat.pattern is None:
|
if subpat.target is None:
|
||||||
continue
|
continue
|
||||||
encoded_name = subpat.pattern.name.encode('ASCII')
|
encoded_name = subpat.target.encode('ASCII')
|
||||||
|
|
||||||
# Note: GDS mirrors first and rotates second
|
# Note: GDS mirrors first and rotates second
|
||||||
mirror_across_x, extra_angle = normalize_mirror(subpat.mirrored)
|
mirror_across_x, extra_angle = normalize_mirror(subpat.mirrored)
|
||||||
@ -523,14 +508,14 @@ def _labels_to_texts(labels: List[Label]) -> List[gdsii.elements.Text]:
|
|||||||
|
|
||||||
|
|
||||||
def disambiguate_pattern_names(
|
def disambiguate_pattern_names(
|
||||||
patterns: Sequence[Pattern],
|
names: Iterable[str],
|
||||||
max_name_length: int = 32,
|
max_name_length: int = 32,
|
||||||
suffix_length: int = 6,
|
suffix_length: int = 6,
|
||||||
dup_warn_filter: Optional[Callable[[str], bool]] = None,
|
dup_warn_filter: Optional[Callable[[str], bool]] = None,
|
||||||
) -> None:
|
) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
patterns: List of patterns to disambiguate
|
names: List of pattern names to disambiguate
|
||||||
max_name_length: Names longer than this will be truncated
|
max_name_length: Names longer than this will be truncated
|
||||||
suffix_length: Names which get truncated are truncated by this many extra characters. This is to
|
suffix_length: Names which get truncated are truncated by this many extra characters. This is to
|
||||||
leave room for a suffix if one is necessary.
|
leave room for a suffix if one is necessary.
|
||||||
@ -538,15 +523,15 @@ def disambiguate_pattern_names(
|
|||||||
the cell name and returns `False` if the warning should be suppressed and `True` if it should
|
the cell name and returns `False` if the warning should be suppressed and `True` if it should
|
||||||
be displayed. Default displays all warnings.
|
be displayed. Default displays all warnings.
|
||||||
"""
|
"""
|
||||||
used_names = []
|
new_names = []
|
||||||
for pat in set(patterns):
|
for name in names:
|
||||||
# Shorten names which already exceed max-length
|
# Shorten names which already exceed max-length
|
||||||
if len(pat.name) > max_name_length:
|
if len(name) > max_name_length:
|
||||||
shortened_name = pat.name[:max_name_length - suffix_length]
|
shortened_name = name[:max_name_length - suffix_length]
|
||||||
logger.warning(f'Pattern name "{pat.name}" is too long ({len(pat.name)}/{max_name_length} chars),\n'
|
logger.warning(f'Pattern name "{name}" is too long ({len(name)}/{max_name_length} chars),\n'
|
||||||
+ f' shortening to "{shortened_name}" before generating suffix')
|
+ f' shortening to "{shortened_name}" before generating suffix')
|
||||||
else:
|
else:
|
||||||
shortened_name = pat.name
|
shortened_name = name
|
||||||
|
|
||||||
# Remove invalid characters
|
# Remove invalid characters
|
||||||
sanitized_name = re.compile(r'[^A-Za-z0-9_\?\$]').sub('_', shortened_name)
|
sanitized_name = re.compile(r'[^A-Za-z0-9_\?\$]').sub('_', shortened_name)
|
||||||
@ -554,7 +539,7 @@ def disambiguate_pattern_names(
|
|||||||
# Add a suffix that makes the name unique
|
# Add a suffix that makes the name unique
|
||||||
i = 0
|
i = 0
|
||||||
suffixed_name = sanitized_name
|
suffixed_name = sanitized_name
|
||||||
while suffixed_name in used_names or suffixed_name == '':
|
while suffixed_name in new_names or suffixed_name == '':
|
||||||
suffix = base64.b64encode(struct.pack('>Q', i), b'$?').decode('ASCII')
|
suffix = base64.b64encode(struct.pack('>Q', i), b'$?').decode('ASCII')
|
||||||
|
|
||||||
suffixed_name = sanitized_name + '$' + suffix[:-1].lstrip('A')
|
suffixed_name = sanitized_name + '$' + suffix[:-1].lstrip('A')
|
||||||
@ -563,18 +548,19 @@ def disambiguate_pattern_names(
|
|||||||
if sanitized_name == '':
|
if sanitized_name == '':
|
||||||
logger.warning(f'Empty pattern name saved as "{suffixed_name}"')
|
logger.warning(f'Empty pattern name saved as "{suffixed_name}"')
|
||||||
elif suffixed_name != sanitized_name:
|
elif suffixed_name != sanitized_name:
|
||||||
if dup_warn_filter is None or dup_warn_filter(pat.name):
|
if dup_warn_filter is None or dup_warn_filter(name):
|
||||||
logger.warning(f'Pattern name "{pat.name}" ({sanitized_name}) appears multiple times;\n'
|
logger.warning(f'Pattern name "{name}" ({sanitized_name}) appears multiple times;\n'
|
||||||
+ f' renaming to "{suffixed_name}"')
|
+ f' renaming to "{suffixed_name}"')
|
||||||
|
|
||||||
# Encode into a byte-string and perform some final checks
|
# Encode into a byte-string and perform some final checks
|
||||||
encoded_name = suffixed_name.encode('ASCII')
|
encoded_name = suffixed_name.encode('ASCII')
|
||||||
if len(encoded_name) == 0:
|
if len(encoded_name) == 0:
|
||||||
# Should never happen since zero-length names are replaced
|
# Should never happen since zero-length names are replaced
|
||||||
raise PatternError(f'Zero-length name after sanitize+encode,\n originally "{pat.name}"')
|
raise PatternError(f'Zero-length name after sanitize+encode,\n originally "{name}"')
|
||||||
if len(encoded_name) > max_name_length:
|
if len(encoded_name) > max_name_length:
|
||||||
raise PatternError(f'Pattern name "{encoded_name!r}" length > {max_name_length} after encode,\n'
|
raise PatternError(f'Pattern name "{encoded_name!r}" length > {max_name_length} after encode,\n'
|
||||||
+ f' originally "{pat.name}"')
|
+ f' originally "{name}"')
|
||||||
|
|
||||||
|
new_names.append(suffixed_name)
|
||||||
|
return new_names
|
||||||
|
|
||||||
pat.name = suffixed_name
|
|
||||||
used_names.append(suffixed_name)
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
SVG file format readers and writers
|
SVG file format readers and writers
|
||||||
"""
|
"""
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional, Mapping
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
@ -13,7 +13,8 @@ from .. import Pattern
|
|||||||
|
|
||||||
|
|
||||||
def writefile(
|
def writefile(
|
||||||
pattern: Pattern,
|
library: Mapping[str, Pattern],
|
||||||
|
top: str,
|
||||||
filename: str,
|
filename: str,
|
||||||
custom_attributes: bool = False,
|
custom_attributes: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -41,11 +42,12 @@ def writefile(
|
|||||||
custom_attributes: Whether to write non-standard `pattern_layer` and
|
custom_attributes: Whether to write non-standard `pattern_layer` and
|
||||||
`pattern_dose` attributes to the SVG elements.
|
`pattern_dose` attributes to the SVG elements.
|
||||||
"""
|
"""
|
||||||
|
pattern = library[top]
|
||||||
|
|
||||||
# Polygonize pattern
|
# Polygonize pattern
|
||||||
pattern.polygonize()
|
pattern.polygonize()
|
||||||
|
|
||||||
bounds = pattern.get_bounds()
|
bounds = pattern.get_bounds(library=library)
|
||||||
if bounds is None:
|
if bounds is None:
|
||||||
bounds_min, bounds_max = numpy.array([[-1, -1], [1, 1]])
|
bounds_min, bounds_max = numpy.array([[-1, -1], [1, 1]])
|
||||||
warnings.warn('Pattern had no bounds (empty?); setting arbitrary viewbox')
|
warnings.warn('Pattern had no bounds (empty?); setting arbitrary viewbox')
|
||||||
@ -59,15 +61,10 @@ def writefile(
|
|||||||
svg = svgwrite.Drawing(filename, profile='full', viewBox=viewbox_string,
|
svg = svgwrite.Drawing(filename, profile='full', viewBox=viewbox_string,
|
||||||
debug=(not custom_attributes))
|
debug=(not custom_attributes))
|
||||||
|
|
||||||
# Get a dict of id(pattern) -> pattern
|
|
||||||
patterns_by_id = {**(pattern.referenced_patterns_by_id()), id(pattern): pattern} # type: Dict[int, Optional[Pattern]]
|
|
||||||
|
|
||||||
# Now create a group for each row in sd_table (ie, each pattern + dose combination)
|
# Now create a group for each row in sd_table (ie, each pattern + dose combination)
|
||||||
# and add in any Boundary and Use elements
|
# and add in any Boundary and Use elements
|
||||||
for pat in patterns_by_id.values():
|
for name, pat in library.items():
|
||||||
if pat is None:
|
svg_group = svg.g(id=mangle_name(name), fill='blue', stroke='red')
|
||||||
continue
|
|
||||||
svg_group = svg.g(id=mangle_name(pat), fill='blue', stroke='red')
|
|
||||||
|
|
||||||
for shape in pat.shapes:
|
for shape in pat.shapes:
|
||||||
for polygon in shape.to_polygons():
|
for polygon in shape.to_polygons():
|
||||||
@ -81,20 +78,24 @@ def writefile(
|
|||||||
svg_group.add(path)
|
svg_group.add(path)
|
||||||
|
|
||||||
for subpat in pat.subpatterns:
|
for subpat in pat.subpatterns:
|
||||||
if subpat.pattern is None:
|
if subpat.target is None:
|
||||||
continue
|
continue
|
||||||
transform = f'scale({subpat.scale:g}) rotate({subpat.rotation:g}) translate({subpat.offset[0]:g},{subpat.offset[1]:g})'
|
transform = f'scale({subpat.scale:g}) rotate({subpat.rotation:g}) translate({subpat.offset[0]:g},{subpat.offset[1]:g})'
|
||||||
use = svg.use(href='#' + mangle_name(subpat.pattern), transform=transform)
|
use = svg.use(href='#' + mangle_name(subpat.target), transform=transform)
|
||||||
if custom_attributes:
|
if custom_attributes:
|
||||||
use['pattern_dose'] = subpat.dose
|
use['pattern_dose'] = subpat.dose
|
||||||
svg_group.add(use)
|
svg_group.add(use)
|
||||||
|
|
||||||
svg.defs.add(svg_group)
|
svg.defs.add(svg_group)
|
||||||
svg.add(svg.use(href='#' + mangle_name(pattern)))
|
svg.add(svg.use(href='#' + mangle_name(top)))
|
||||||
svg.save()
|
svg.save()
|
||||||
|
|
||||||
|
|
||||||
def writefile_inverted(pattern: Pattern, filename: str):
|
def writefile_inverted(
|
||||||
|
library: Mapping[str, Pattern],
|
||||||
|
top: str,
|
||||||
|
filename: str,
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Write an inverted Pattern to an SVG file, by first calling `.polygonize()` and
|
Write an inverted Pattern to an SVG file, by first calling `.polygonize()` and
|
||||||
`.flatten()` on it to change the shapes into polygons, then drawing a bounding
|
`.flatten()` on it to change the shapes into polygons, then drawing a bounding
|
||||||
@ -110,10 +111,12 @@ def writefile_inverted(pattern: Pattern, filename: str):
|
|||||||
pattern: Pattern to write to file. Modified by this function.
|
pattern: Pattern to write to file. Modified by this function.
|
||||||
filename: Filename to write to.
|
filename: Filename to write to.
|
||||||
"""
|
"""
|
||||||
|
pattern = library[top]
|
||||||
|
|
||||||
# Polygonize and flatten pattern
|
# Polygonize and flatten pattern
|
||||||
pattern.polygonize().flatten()
|
pattern.polygonize().flatten()
|
||||||
|
|
||||||
bounds = pattern.get_bounds()
|
bounds = pattern.get_bounds(library=library)
|
||||||
if bounds is None:
|
if bounds is None:
|
||||||
bounds_min, bounds_max = numpy.array([[-1, -1], [1, 1]])
|
bounds_min, bounds_max = numpy.array([[-1, -1], [1, 1]])
|
||||||
warnings.warn('Pattern had no bounds (empty?); setting arbitrary viewbox')
|
warnings.warn('Pattern had no bounds (empty?); setting arbitrary viewbox')
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Helper functions for file reading and writing
|
Helper functions for file reading and writing
|
||||||
"""
|
"""
|
||||||
from typing import Set, Tuple, List
|
from typing import Set, Tuple, List, Iterable, Mapping
|
||||||
import re
|
import re
|
||||||
import copy
|
import copy
|
||||||
import pathlib
|
import pathlib
|
||||||
@ -10,19 +10,22 @@ from .. import Pattern, PatternError
|
|||||||
from ..shapes import Polygon, Path
|
from ..shapes import Polygon, Path
|
||||||
|
|
||||||
|
|
||||||
def mangle_name(pattern: Pattern, dose_multiplier: float = 1.0) -> str:
|
def mangle_name(name: str, dose_multiplier: float = 1.0) -> str:
|
||||||
"""
|
"""
|
||||||
Create a name using `pattern.name`, `id(pattern)`, and the dose multiplier.
|
Create a new name using `name` and the `dose_multiplier`.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
pattern: Pattern whose name we want to mangle.
|
name: Name we want to mangle.
|
||||||
dose_multiplier: Dose multiplier to mangle with.
|
dose_multiplier: Dose multiplier to mangle with.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Mangled name.
|
Mangled name.
|
||||||
"""
|
"""
|
||||||
|
if dose_multiplier == 1:
|
||||||
|
full_name = name
|
||||||
|
else:
|
||||||
|
full_name = f'{name}_dm{dose_multiplier}'
|
||||||
expression = re.compile(r'[^A-Za-z0-9_\?\$]')
|
expression = re.compile(r'[^A-Za-z0-9_\?\$]')
|
||||||
full_name = '{}_{}_{}'.format(pattern.name, dose_multiplier, id(pattern))
|
|
||||||
sanitized_name = expression.sub('_', full_name)
|
sanitized_name = expression.sub('_', full_name)
|
||||||
return sanitized_name
|
return sanitized_name
|
||||||
|
|
||||||
@ -51,25 +54,30 @@ def clean_pattern_vertices(pat: Pattern) -> Pattern:
|
|||||||
return pat
|
return pat
|
||||||
|
|
||||||
|
|
||||||
def make_dose_table(patterns: List[Pattern], dose_multiplier: float = 1.0) -> Set[Tuple[int, float]]:
|
def make_dose_table(
|
||||||
|
top_names: Iterable[str],
|
||||||
|
library: Mapping[str, Pattern],
|
||||||
|
dose_multiplier: float = 1.0,
|
||||||
|
) -> Set[Tuple[int, float]]:
|
||||||
"""
|
"""
|
||||||
Create a set containing `(id(pat), written_dose)` for each pattern (including subpatterns)
|
Create a set containing `(name, written_dose)` for each pattern (including subpatterns)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
top_names: Names of all topcells
|
||||||
pattern: Source Patterns.
|
pattern: Source Patterns.
|
||||||
dose_multiplier: Multiplier for all written_dose entries.
|
dose_multiplier: Multiplier for all written_dose entries.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
`{(id(subpat.pattern), written_dose), ...}`
|
`{(name, written_dose), ...}`
|
||||||
"""
|
"""
|
||||||
dose_table = {(id(pattern), dose_multiplier) for pattern in patterns}
|
dose_table = {(top_name, dose_multiplier) for top_name in top_names}
|
||||||
for pattern in patterns:
|
for name, pattern in library.items():
|
||||||
for subpat in pattern.subpatterns:
|
for subpat in pattern.subpatterns:
|
||||||
if subpat.pattern is None:
|
if subpat.target is None:
|
||||||
continue
|
continue
|
||||||
subpat_dose_entry = (id(subpat.pattern), subpat.dose * dose_multiplier)
|
subpat_dose_entry = (subpat.target, subpat.dose * dose_multiplier)
|
||||||
if subpat_dose_entry not in dose_table:
|
if subpat_dose_entry not in dose_table:
|
||||||
subpat_dose_table = make_dose_table([subpat.pattern], subpat.dose * dose_multiplier)
|
subpat_dose_table = make_dose_table(subpat.target, library, subpat.dose * dose_multiplier)
|
||||||
dose_table = dose_table.union(subpat_dose_table)
|
dose_table = dose_table.union(subpat_dose_table)
|
||||||
return dose_table
|
return dose_table
|
||||||
|
|
||||||
@ -96,7 +104,7 @@ def dtype2dose(pattern: Pattern) -> Pattern:
|
|||||||
|
|
||||||
|
|
||||||
def dose2dtype(
|
def dose2dtype(
|
||||||
patterns: List[Pattern],
|
library: List[Pattern],
|
||||||
) -> Tuple[List[Pattern], List[float]]:
|
) -> Tuple[List[Pattern], List[float]]:
|
||||||
"""
|
"""
|
||||||
For each shape in each pattern, set shape.layer to the tuple
|
For each shape in each pattern, set shape.layer to the tuple
|
||||||
@ -119,21 +127,16 @@ def dose2dtype(
|
|||||||
dose_list: A list of doses, providing a mapping between datatype (int, list index)
|
dose_list: A list of doses, providing a mapping between datatype (int, list index)
|
||||||
and dose (float, list entry).
|
and dose (float, list entry).
|
||||||
"""
|
"""
|
||||||
# Get a dict of id(pattern) -> pattern
|
logger.warning('TODO: dose2dtype() needs to be tested!')
|
||||||
patterns_by_id = {id(pattern): pattern for pattern in patterns}
|
|
||||||
for pattern in patterns:
|
|
||||||
for i, p in pattern.referenced_patterns_by_id().items():
|
|
||||||
patterns_by_id[i] = p
|
|
||||||
|
|
||||||
# Get a table of (id(pat), written_dose) for each pattern and subpattern
|
# Get a table of (id(pat), written_dose) for each pattern and subpattern
|
||||||
sd_table = make_dose_table(patterns)
|
sd_table = make_dose_table(library.find_topcells(), library)
|
||||||
|
|
||||||
# Figure out all the unique doses necessary to write this pattern
|
# Figure out all the unique doses necessary to write this pattern
|
||||||
# This means going through each row in sd_table and adding the dose values needed to write
|
# This means going through each row in sd_table and adding the dose values needed to write
|
||||||
# that subpattern at that dose level
|
# that subpattern at that dose level
|
||||||
dose_vals = set()
|
dose_vals = set()
|
||||||
for pat_id, pat_dose in sd_table:
|
for name, pat_dose in sd_table:
|
||||||
pat = patterns_by_id[pat_id]
|
pat = library[name]
|
||||||
for shape in pat.shapes:
|
for shape in pat.shapes:
|
||||||
dose_vals.add(shape.dose * pat_dose)
|
dose_vals.add(shape.dose * pat_dose)
|
||||||
|
|
||||||
@ -144,21 +147,22 @@ def dose2dtype(
|
|||||||
|
|
||||||
# Create a new pattern for each non-1-dose entry in the dose table
|
# Create a new pattern for each non-1-dose entry in the dose table
|
||||||
# and update the shapes to reflect their new dose
|
# and update the shapes to reflect their new dose
|
||||||
new_pats = {} # (id, dose) -> new_pattern mapping
|
new_names = {} # {(old name, dose): new name} mapping
|
||||||
for pat_id, pat_dose in sd_table:
|
new_lib = {} # {new_name: new_pattern} mapping
|
||||||
|
for name, pat_dose in sd_table:
|
||||||
|
mangled_name = mangle_name(name, pat_dose)
|
||||||
|
new_names[(name, pat_dose)] = mangled_name
|
||||||
|
|
||||||
|
old_pat = library[name]
|
||||||
|
|
||||||
if pat_dose == 1:
|
if pat_dose == 1:
|
||||||
new_pats[(pat_id, pat_dose)] = patterns_by_id[pat_id]
|
new_lib[mangled_name] = old_pat
|
||||||
continue
|
continue
|
||||||
|
|
||||||
old_pat = patterns_by_id[pat_id]
|
pat = old_pat.deepcopy()
|
||||||
pat = old_pat.copy() # keep old subpatterns
|
|
||||||
pat.shapes = copy.deepcopy(old_pat.shapes)
|
|
||||||
pat.labels = copy.deepcopy(old_pat.labels)
|
|
||||||
|
|
||||||
encoded_name = mangle_name(pat, pat_dose)
|
|
||||||
if len(encoded_name) == 0:
|
if len(encoded_name) == 0:
|
||||||
raise PatternError('Zero-length name after mangle+encode, originally "{}"'.format(pat.name))
|
raise PatternError('Zero-length name after mangle+encode, originally "{name}"'.format(pat.name))
|
||||||
pat.name = encoded_name
|
|
||||||
|
|
||||||
for shape in pat.shapes:
|
for shape in pat.shapes:
|
||||||
data_type = dose_vals_list.index(shape.dose * pat_dose)
|
data_type = dose_vals_list.index(shape.dose * pat_dose)
|
||||||
@ -169,15 +173,9 @@ def dose2dtype(
|
|||||||
else:
|
else:
|
||||||
raise PatternError(f'Invalid layer for gdsii: {shape.layer}')
|
raise PatternError(f'Invalid layer for gdsii: {shape.layer}')
|
||||||
|
|
||||||
new_pats[(pat_id, pat_dose)] = pat
|
new_lib[mangled_name] = pat
|
||||||
|
|
||||||
# Go back through all the dose-specific patterns and fix up their subpattern entries
|
return new_lib, dose_vals_list
|
||||||
for (pat_id, pat_dose), pat in new_pats.items():
|
|
||||||
for subpat in pat.subpatterns:
|
|
||||||
dose_mult = subpat.dose * pat_dose
|
|
||||||
subpat.pattern = new_pats[(id(subpat.pattern), dose_mult)]
|
|
||||||
|
|
||||||
return patterns, dose_vals_list
|
|
||||||
|
|
||||||
|
|
||||||
def is_gzipped(path: pathlib.Path) -> bool:
|
def is_gzipped(path: pathlib.Path) -> bool:
|
||||||
|
@ -6,14 +6,14 @@ from numpy.typing import ArrayLike, NDArray
|
|||||||
|
|
||||||
from .repetition import Repetition
|
from .repetition import Repetition
|
||||||
from .utils import rotation_matrix_2d, layer_t, AutoSlots, annotations_t
|
from .utils import rotation_matrix_2d, layer_t, AutoSlots, annotations_t
|
||||||
from .traits import PositionableImpl, LayerableImpl, Copyable, Pivotable, LockableImpl, RepeatableImpl
|
from .traits import PositionableImpl, LayerableImpl, Copyable, Pivotable, RepeatableImpl
|
||||||
from .traits import AnnotatableImpl
|
from .traits import AnnotatableImpl
|
||||||
|
|
||||||
|
|
||||||
L = TypeVar('L', bound='Label')
|
L = TypeVar('L', bound='Label')
|
||||||
|
|
||||||
|
|
||||||
class Label(PositionableImpl, LayerableImpl, LockableImpl, RepeatableImpl, AnnotatableImpl,
|
class Label(PositionableImpl, LayerableImpl, RepeatableImpl, AnnotatableImpl,
|
||||||
Pivotable, Copyable, metaclass=AutoSlots):
|
Pivotable, Copyable, metaclass=AutoSlots):
|
||||||
"""
|
"""
|
||||||
A text annotation with a position and layer (but no size; it is not drawn)
|
A text annotation with a position and layer (but no size; it is not drawn)
|
||||||
@ -49,32 +49,28 @@ class Label(PositionableImpl, LayerableImpl, LockableImpl, RepeatableImpl, Annot
|
|||||||
layer: layer_t = 0,
|
layer: layer_t = 0,
|
||||||
repetition: Optional[Repetition] = None,
|
repetition: Optional[Repetition] = None,
|
||||||
annotations: Optional[annotations_t] = None,
|
annotations: Optional[annotations_t] = None,
|
||||||
locked: bool = False,
|
|
||||||
identifier: Tuple = (),
|
identifier: Tuple = (),
|
||||||
) -> None:
|
) -> None:
|
||||||
LockableImpl.unlock(self)
|
|
||||||
self.identifier = identifier
|
self.identifier = identifier
|
||||||
self.string = string
|
self.string = string
|
||||||
self.offset = numpy.array(offset, dtype=float, copy=True)
|
self.offset = numpy.array(offset, dtype=float, copy=True)
|
||||||
self.layer = layer
|
self.layer = layer
|
||||||
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)
|
|
||||||
|
|
||||||
def __copy__(self: L) -> L:
|
def __copy__(self: L) -> L:
|
||||||
return type(self)(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,
|
identifier=self.identifier,
|
||||||
identifier=self.identifier)
|
)
|
||||||
|
|
||||||
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
|
||||||
new = copy.copy(self)
|
new = copy.copy(self)
|
||||||
LockableImpl.unlock(new)
|
|
||||||
new._offset = self._offset.copy()
|
new._offset = self._offset.copy()
|
||||||
new.set_locked(self.locked)
|
|
||||||
return new
|
return new
|
||||||
|
|
||||||
def rotate_around(self: L, pivot: ArrayLike, rotation: float) -> L:
|
def rotate_around(self: L, pivot: ArrayLike, rotation: float) -> L:
|
||||||
@ -106,17 +102,3 @@ class Label(PositionableImpl, LayerableImpl, LockableImpl, RepeatableImpl, Annot
|
|||||||
Bounds [[xmin, xmax], [ymin, ymax]]
|
Bounds [[xmin, xmax], [ymin, ymax]]
|
||||||
"""
|
"""
|
||||||
return numpy.array([self.offset, self.offset])
|
return numpy.array([self.offset, self.offset])
|
||||||
|
|
||||||
def lock(self: L) -> L:
|
|
||||||
PositionableImpl._lock(self)
|
|
||||||
LockableImpl.lock(self)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def unlock(self: L) -> L:
|
|
||||||
LockableImpl.unlock(self)
|
|
||||||
PositionableImpl._unlock(self)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
locked = ' L' if self.locked else ''
|
|
||||||
return f'<Label "{self.string}" l{self.layer} o{self.offset}{locked}>'
|
|
||||||
|
594
masque/library.py
Normal file
594
masque/library.py
Normal file
@ -0,0 +1,594 @@
|
|||||||
|
"""
|
||||||
|
Library class for managing unique name->pattern mappings and
|
||||||
|
deferred loading or creation.
|
||||||
|
"""
|
||||||
|
from typing import List, Dict, Callable, TypeVar, Type, TYPE_CHECKING
|
||||||
|
from typing import Any, Tuple, Union, Iterator, Mapping, MutableMapping, Set, Optional, Sequence
|
||||||
|
import logging
|
||||||
|
import copy
|
||||||
|
import base64
|
||||||
|
import struct
|
||||||
|
import re
|
||||||
|
from pprint import pformat
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
from numpy.typing import ArrayLike, NDArray, NDArray
|
||||||
|
|
||||||
|
from .error import LibraryError, PatternError
|
||||||
|
from .utils import rotation_matrix_2d, normalize_mirror
|
||||||
|
from .shapes import Shape, Polygon
|
||||||
|
from .label import Label
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .pattern import Pattern
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
visitor_function_t = Callable[['Pattern', Tuple['Pattern'], Dict, NDArray[numpy.float64]], 'Pattern']
|
||||||
|
L = TypeVar('L', bound='Library')
|
||||||
|
|
||||||
|
|
||||||
|
class Library:
|
||||||
|
"""
|
||||||
|
This class is usually used to create a library of Patterns by mapping names to
|
||||||
|
functions which generate or load the relevant `Pattern` object as-needed.
|
||||||
|
|
||||||
|
The cache can be disabled by setting the `enable_cache` attribute to `False`.
|
||||||
|
"""
|
||||||
|
dict: Dict[str, Callable[[], Pattern]]
|
||||||
|
cache: Dict[str, 'Pattern']
|
||||||
|
enable_cache: bool = True
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.dict = {}
|
||||||
|
self.cache = {}
|
||||||
|
|
||||||
|
def __setitem__(self, key: str, value: Callable[[], Pattern]) -> None:
|
||||||
|
self.dict[key] = value
|
||||||
|
if key in self.cache:
|
||||||
|
del self.cache[key]
|
||||||
|
|
||||||
|
def __delitem__(self, key: str) -> None:
|
||||||
|
del self.dict[key]
|
||||||
|
if key in self.cache:
|
||||||
|
del self.cache[key]
|
||||||
|
|
||||||
|
def __getitem__(self, key: str) -> 'Pattern':
|
||||||
|
logger.debug(f'loading {key}')
|
||||||
|
if self.enable_cache and key in self.cache:
|
||||||
|
logger.debug(f'found {key} in cache')
|
||||||
|
return self.cache[key]
|
||||||
|
|
||||||
|
func = self.dict[key]
|
||||||
|
pat = func()
|
||||||
|
self.cache[key] = pat
|
||||||
|
return pat
|
||||||
|
|
||||||
|
def __iter__(self) -> Iterator[str]:
|
||||||
|
return iter(self.keys())
|
||||||
|
|
||||||
|
def __contains__(self, key: str) -> bool:
|
||||||
|
return key in self.dict
|
||||||
|
|
||||||
|
def keys(self) -> Iterator[str]:
|
||||||
|
return iter(self.dict.keys())
|
||||||
|
|
||||||
|
def values(self) -> Iterator['Pattern']:
|
||||||
|
return iter(self[key] for key in self.keys())
|
||||||
|
|
||||||
|
def items(self) -> Iterator[Tuple[str, 'Pattern']]:
|
||||||
|
return iter((key, self[key]) for key in self.keys())
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return '<Library with keys ' + repr(list(self.dict.keys())) + '>'
|
||||||
|
|
||||||
|
def precache(self: L) -> L:
|
||||||
|
"""
|
||||||
|
Force all patterns into the cache
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
"""
|
||||||
|
for key in self.dict:
|
||||||
|
_ = self.dict.__getitem__(key)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add(
|
||||||
|
self: L,
|
||||||
|
other: L,
|
||||||
|
use_ours: Callable[[str], bool] = lambda name: False,
|
||||||
|
use_theirs: Callable[[str], bool] = lambda name: False,
|
||||||
|
) -> L:
|
||||||
|
"""
|
||||||
|
Add keys from another library into this one.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
other: The library to insert keys from
|
||||||
|
use_ours: Decision function for name conflicts, called with cell name.
|
||||||
|
Should return `True` if the value from `self` should be used.
|
||||||
|
use_theirs: Decision function for name conflicts. Same format as `use_ours`.
|
||||||
|
Should return `True` if the value from `other` should be used.
|
||||||
|
`use_ours` takes priority over `use_theirs`.
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
"""
|
||||||
|
duplicates = set(self.keys()) & set(other.keys())
|
||||||
|
keep_ours = set(name for name in duplicates if use_ours(name))
|
||||||
|
keep_theirs = set(name for name in duplicates - keep_ours if use_theirs(name))
|
||||||
|
conflicts = duplicates - keep_ours - keep_theirs
|
||||||
|
|
||||||
|
if conflicts:
|
||||||
|
raise LibraryError('Unresolved duplicate keys encountered in library merge: ' + pformat(conflicts))
|
||||||
|
|
||||||
|
for key in set(other.keys()) - keep_ours:
|
||||||
|
self.dict[key] = other.dict[key]
|
||||||
|
if key in other.cache:
|
||||||
|
self.cache[key] = other.cache[key]
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def clear_cache(self: L) -> L:
|
||||||
|
"""
|
||||||
|
Clear the cache of this library.
|
||||||
|
This is usually used before modifying or deleting cells, e.g. when merging
|
||||||
|
with another library.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
"""
|
||||||
|
self.cache.clear()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def referenced_patterns(
|
||||||
|
self,
|
||||||
|
tops: Union[str, Sequence[str]],
|
||||||
|
skip: Optional[Set[Optional[str]]] = None,
|
||||||
|
) -> Set[Optional[str]]:
|
||||||
|
"""
|
||||||
|
Get the set of all pattern names referenced by `top`. Recursively traverses into any subpatterns.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
top: Name of the top pattern(s) to check.
|
||||||
|
skip: Memo, set patterns which have already been traversed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Set of all referenced pattern names
|
||||||
|
"""
|
||||||
|
if skip is None:
|
||||||
|
skip = set([None])
|
||||||
|
|
||||||
|
if isinstance(tops, str):
|
||||||
|
tops = (tops,)
|
||||||
|
|
||||||
|
# Get referenced patterns for all tops
|
||||||
|
targets = set()
|
||||||
|
for top in set(tops):
|
||||||
|
targets |= self[top].referenced_patterns()
|
||||||
|
|
||||||
|
# Perform recursive lookups, but only once for each name
|
||||||
|
for target in targets - skip:
|
||||||
|
assert(target is not None)
|
||||||
|
self.referenced_patterns(target, skip)
|
||||||
|
skip.add(target)
|
||||||
|
|
||||||
|
return targets
|
||||||
|
|
||||||
|
def subtree(
|
||||||
|
self: L,
|
||||||
|
tops: Union[str, Sequence[str]],
|
||||||
|
) -> L:
|
||||||
|
"""
|
||||||
|
Return a new `Library`, containing only the specified patterns and the patterns they
|
||||||
|
reference (recursively).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tops: Name(s) of patterns to keep
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A `Library` containing only `tops` and the patterns they reference.
|
||||||
|
"""
|
||||||
|
keep: Set[str] = self.referenced_patterns(tops) - set((None,)) # type: ignore
|
||||||
|
|
||||||
|
new = type(self)()
|
||||||
|
for key in keep:
|
||||||
|
new.dict[key] = self.dict[key]
|
||||||
|
if key in self.cache:
|
||||||
|
new.cache[key] = self.cache[key]
|
||||||
|
|
||||||
|
return new
|
||||||
|
|
||||||
|
def dfs(
|
||||||
|
self: L,
|
||||||
|
top: str,
|
||||||
|
visit_before: visitor_function_t = None,
|
||||||
|
visit_after: visitor_function_t = None,
|
||||||
|
transform: Union[ArrayLike, bool, None] = False,
|
||||||
|
memo: Optional[Dict] = None,
|
||||||
|
hierarchy: Tuple[str, ...] = (),
|
||||||
|
) -> L:
|
||||||
|
"""
|
||||||
|
Convenience function.
|
||||||
|
Performs a depth-first traversal of a pattern and its subpatterns.
|
||||||
|
At each pattern in the tree, the following sequence is called:
|
||||||
|
```
|
||||||
|
current_pattern = visit_before(current_pattern, **vist_args)
|
||||||
|
for sp in current_pattern.subpatterns]
|
||||||
|
self.dfs(sp.target, visit_before, visit_after, updated_transform,
|
||||||
|
memo, (current_pattern,) + hierarchy)
|
||||||
|
current_pattern = visit_after(current_pattern, **visit_args)
|
||||||
|
```
|
||||||
|
where `visit_args` are
|
||||||
|
`hierarchy`: (top_pattern, L1_pattern, L2_pattern, ..., parent_pattern)
|
||||||
|
tuple of all parent-and-higher patterns
|
||||||
|
`transform`: numpy.ndarray containing cumulative
|
||||||
|
[x_offset, y_offset, rotation (rad), mirror_x (0 or 1)]
|
||||||
|
for the instance being visited
|
||||||
|
`memo`: Arbitrary dict (not altered except by `visit_before()` and `visit_after()`)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
top: Name of the pattern to start at (root node of the tree).
|
||||||
|
visit_before: Function to call before traversing subpatterns.
|
||||||
|
Should accept a `Pattern` and `**visit_args`, and return the (possibly modified)
|
||||||
|
pattern. Default `None` (not called).
|
||||||
|
visit_after: Function to call after traversing subpatterns.
|
||||||
|
Should accept a `Pattern` and `**visit_args`, and return the (possibly modified)
|
||||||
|
pattern. Default `None` (not called).
|
||||||
|
transform: Initial value for `visit_args['transform']`.
|
||||||
|
Can be `False`, in which case the transform is not calculated.
|
||||||
|
`True` or `None` is interpreted as `[0, 0, 0, 0]`.
|
||||||
|
memo: Arbitrary dict for use by `visit_*()` functions. Default `None` (empty dict).
|
||||||
|
hierarchy: Tuple of patterns specifying the hierarchy above the current pattern.
|
||||||
|
Appended to the start of the generated `visit_args['hierarchy']`.
|
||||||
|
Default is an empty tuple.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
"""
|
||||||
|
if memo is None:
|
||||||
|
memo = {}
|
||||||
|
|
||||||
|
if transform is None or transform is True:
|
||||||
|
transform = numpy.zeros(4)
|
||||||
|
elif transform is not False:
|
||||||
|
transform = numpy.array(transform)
|
||||||
|
|
||||||
|
if top in hierarchy:
|
||||||
|
raise PatternError('.dfs() called on pattern with circular reference')
|
||||||
|
|
||||||
|
pat = self[top]
|
||||||
|
if visit_before is not None:
|
||||||
|
pat = visit_before(pat, hierarchy=hierarchy, memo=memo, transform=transform) # type: ignore
|
||||||
|
|
||||||
|
for subpattern in pat.subpatterns:
|
||||||
|
if transform is not False:
|
||||||
|
sign = numpy.ones(2)
|
||||||
|
if transform[3]:
|
||||||
|
sign[1] = -1
|
||||||
|
xy = numpy.dot(rotation_matrix_2d(transform[2]), subpattern.offset * sign)
|
||||||
|
mirror_x, angle = normalize_mirror(subpattern.mirrored)
|
||||||
|
angle += subpattern.rotation
|
||||||
|
sp_transform = transform + (xy[0], xy[1], angle, mirror_x)
|
||||||
|
sp_transform[3] %= 2
|
||||||
|
else:
|
||||||
|
sp_transform = False
|
||||||
|
|
||||||
|
if subpattern.target is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.dfs(
|
||||||
|
top=subpattern.target,
|
||||||
|
visit_before=visit_before,
|
||||||
|
visit_after=visit_after,
|
||||||
|
transform=sp_transform,
|
||||||
|
memo=memo,
|
||||||
|
hierarchy=hierarchy + (top,),
|
||||||
|
)
|
||||||
|
|
||||||
|
if visit_after is not None:
|
||||||
|
pat = visit_after(pat, hierarchy=hierarchy, memo=memo, transform=transform) # type: ignore
|
||||||
|
|
||||||
|
self[top] = lambda: pat
|
||||||
|
return self
|
||||||
|
|
||||||
|
def polygonize(
|
||||||
|
self: L,
|
||||||
|
poly_num_points: Optional[int] = None,
|
||||||
|
poly_max_arclen: Optional[float] = None,
|
||||||
|
) -> L:
|
||||||
|
"""
|
||||||
|
Calls `.polygonize(...)` on each pattern in this library.
|
||||||
|
Arguments are passed on to `shape.to_polygons(...)`.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
poly_num_points: Number of points to use for each polygon. Can be overridden by
|
||||||
|
`poly_max_arclen` if that results in more points. Optional, defaults to shapes'
|
||||||
|
internal defaults.
|
||||||
|
poly_max_arclen: Maximum arclength which can be approximated by a single line
|
||||||
|
segment. Optional, defaults to shapes' internal defaults.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
"""
|
||||||
|
for pat in self.values():
|
||||||
|
pat.polygonize(poly_num_points, poly_max_arclen)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def manhattanize(
|
||||||
|
self: L,
|
||||||
|
grid_x: ArrayLike,
|
||||||
|
grid_y: ArrayLike,
|
||||||
|
) -> L:
|
||||||
|
"""
|
||||||
|
Calls `.manhattanize(grid_x, grid_y)` on each pattern in this library.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
grid_x: List of allowed x-coordinates for the Manhattanized polygon edges.
|
||||||
|
grid_y: List of allowed y-coordinates for the Manhattanized polygon edges.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
"""
|
||||||
|
for pat in self.values():
|
||||||
|
pat.manhattanize(grid_x, grid_y)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def subpatternize(
|
||||||
|
self: L,
|
||||||
|
norm_value: int = int(1e6),
|
||||||
|
exclude_types: Tuple[Type] = (Polygon,),
|
||||||
|
label2name: Optional[Callable[[Tuple], str]] = None,
|
||||||
|
threshold: int = 2,
|
||||||
|
) -> L:
|
||||||
|
"""
|
||||||
|
Iterates through all `Pattern`s. Within each `Pattern`, it iterates
|
||||||
|
over all shapes, calling `.normalized_form(norm_value)` on them to retrieve a scale-,
|
||||||
|
offset-, dose-, and rotation-independent form. Each shape whose normalized form appears
|
||||||
|
more than once is removed and re-added using subpattern objects referencing a newly-created
|
||||||
|
`Pattern` containing only the normalized form of the shape.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
The default norm_value was chosen to give a reasonable precision when using
|
||||||
|
integer values for coordinates.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
norm_value: Passed to `shape.normalized_form(norm_value)`. Default `1e6` (see function
|
||||||
|
note)
|
||||||
|
exclude_types: Shape types passed in this argument are always left untouched, for
|
||||||
|
speed or convenience. Default: `(shapes.Polygon,)`
|
||||||
|
label2name: Given a label tuple as returned by `shape.normalized_form(...)`, pick
|
||||||
|
a name for the generated pattern. Default `self.get_name('_shape')`.
|
||||||
|
threshold: Only replace shapes with subpatterns if there will be at least this many
|
||||||
|
instances.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
"""
|
||||||
|
# This currently simplifies globally (same shape in different patterns is
|
||||||
|
# merged into the same subpattern target.
|
||||||
|
|
||||||
|
if exclude_types is None:
|
||||||
|
exclude_types = ()
|
||||||
|
|
||||||
|
if label2name is None:
|
||||||
|
label2name = lambda label: self.get_name('_shape')
|
||||||
|
|
||||||
|
|
||||||
|
shape_counts: MutableMapping[Tuple, int] = defaultdict(int)
|
||||||
|
shape_funcs = {}
|
||||||
|
|
||||||
|
### First pass ###
|
||||||
|
# Using the label tuple from `.normalized_form()` as a key, check how many of each shape
|
||||||
|
# are present and store the shape function for each one
|
||||||
|
for pat in tuple(self.values()):
|
||||||
|
for i, shape in enumerate(pat.shapes):
|
||||||
|
if not any(isinstance(shape, t) for t in exclude_types):
|
||||||
|
label, _values, func = shape.normalized_form(norm_value)
|
||||||
|
shape_funcs[label] = func
|
||||||
|
shape_counts[label] += 1
|
||||||
|
|
||||||
|
shape_pats = {}
|
||||||
|
for label, count in shape_counts.items():
|
||||||
|
if count < threshold:
|
||||||
|
continue
|
||||||
|
|
||||||
|
shape_func = shape_funcs[label]
|
||||||
|
shape_pat = Pattern(shapes=[shape_func()])
|
||||||
|
shape_pats[label] = shape_pat
|
||||||
|
|
||||||
|
### Second pass ###
|
||||||
|
for pat in tuple(self.values()):
|
||||||
|
# Store `[(index_in_shapes, values_from_normalized_form), ...]` for all shapes which
|
||||||
|
# are to be replaced.
|
||||||
|
# The `values` are `(offset, scale, rotation, dose)`.
|
||||||
|
|
||||||
|
shape_table: MutableMapping[Tuple, List] = defaultdict(list)
|
||||||
|
for i, shape in enumerate(pat.shapes):
|
||||||
|
if any(isinstance(shape, t) for t in exclude_types):
|
||||||
|
continue
|
||||||
|
|
||||||
|
label, values, _func = shape.normalized_form(norm_value)
|
||||||
|
|
||||||
|
if label not in shape_pats:
|
||||||
|
continue
|
||||||
|
|
||||||
|
shape_table[label].append((i, values))
|
||||||
|
|
||||||
|
# For repeated shapes, create a `Pattern` holding a normalized shape object,
|
||||||
|
# and add `pat.subpatterns` entries for each occurrence in pat. Also, note down that
|
||||||
|
# we should delete the `pat.shapes` entries for which we made SubPatterns.
|
||||||
|
shapes_to_remove = []
|
||||||
|
for label in shape_table:
|
||||||
|
target = label2name(label)
|
||||||
|
for i, values in shape_table[label]:
|
||||||
|
offset, scale, rotation, mirror_x, dose = values
|
||||||
|
pat.addsp(target=target, offset=offset, scale=scale,
|
||||||
|
rotation=rotation, dose=dose, mirrored=(mirror_x, False))
|
||||||
|
shapes_to_remove.append(i)
|
||||||
|
|
||||||
|
# Remove any shapes for which we have created subpatterns.
|
||||||
|
for i in sorted(shapes_to_remove, reverse=True):
|
||||||
|
del pat.shapes[i]
|
||||||
|
|
||||||
|
for ll, pp in shape_pats.items():
|
||||||
|
self[label2name(ll)] = lambda: pp
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def wrap_repeated_shapes(
|
||||||
|
self: L,
|
||||||
|
name_func: Optional[Callable[['Pattern', Union[Shape, Label]], str]] = None,
|
||||||
|
) -> L:
|
||||||
|
"""
|
||||||
|
Wraps all shapes and labels with a non-`None` `repetition` attribute
|
||||||
|
into a `SubPattern`/`Pattern` combination, and applies the `repetition`
|
||||||
|
to each `SubPattern` instead of its contained shape.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name_func: Function f(this_pattern, shape) which generates a name for the
|
||||||
|
wrapping pattern. Default is `self.get_name('_rep')`.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
"""
|
||||||
|
if name_func is None:
|
||||||
|
name_func = lambda _pat, _shape: self.get_name('_rep')
|
||||||
|
|
||||||
|
for pat in tuple(self.values()):
|
||||||
|
new_shapes = []
|
||||||
|
for shape in pat.shapes:
|
||||||
|
if shape.repetition is None:
|
||||||
|
new_shapes.append(shape)
|
||||||
|
continue
|
||||||
|
|
||||||
|
name = name_func(pat, shape)
|
||||||
|
self[name] = lambda: Pattern(shapes=[shape])
|
||||||
|
pat.addsp(name, repetition=shape.repetition)
|
||||||
|
shape.repetition = None
|
||||||
|
pat.shapes = new_shapes
|
||||||
|
|
||||||
|
new_labels = []
|
||||||
|
for label in pat.labels:
|
||||||
|
if label.repetition is None:
|
||||||
|
new_labels.append(label)
|
||||||
|
continue
|
||||||
|
name = name_func(pat, label)
|
||||||
|
self[name] = lambda: Pattern(labels=[label])
|
||||||
|
pat.addsp(name, repetition=label.repetition)
|
||||||
|
label.repetition = None
|
||||||
|
pat.labels = new_labels
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def flatten(
|
||||||
|
self: L,
|
||||||
|
tops: Union[str, Sequence[str]],
|
||||||
|
) -> Dict[str, Pattern]:
|
||||||
|
"""
|
||||||
|
Removes all subpatterns and adds equivalent shapes.
|
||||||
|
Also flattens all subpatterns.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tops: The pattern(s) to flattern.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{name: flat_pattern} mapping for all flattened patterns.
|
||||||
|
"""
|
||||||
|
if isinstance(tops, str):
|
||||||
|
tops = (tops,)
|
||||||
|
|
||||||
|
flattened: Dict[str, Optional[Pattern]] = {}
|
||||||
|
|
||||||
|
def flatten_single(name) -> None:
|
||||||
|
flattened[name] = None
|
||||||
|
pat = self[name].deepcopy()
|
||||||
|
|
||||||
|
for subpat in pat.subpatterns:
|
||||||
|
target = subpat.target
|
||||||
|
if target is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if target not in flattened:
|
||||||
|
flatten_single(target)
|
||||||
|
if flattened[target] is None:
|
||||||
|
raise PatternError(f'Circular reference in {name} to {target}')
|
||||||
|
|
||||||
|
p = subpat.as_pattern(pattern=flattened[target])
|
||||||
|
pat.append(p)
|
||||||
|
|
||||||
|
pat.subpatterns.clear()
|
||||||
|
flattened[name] = pat
|
||||||
|
|
||||||
|
for top in tops:
|
||||||
|
flatten_single(top)
|
||||||
|
|
||||||
|
assert(None not in flattened.values())
|
||||||
|
return flattened # type: ignore
|
||||||
|
|
||||||
|
def get_name(
|
||||||
|
self,
|
||||||
|
name: str = '__',
|
||||||
|
sanitize: bool = True,
|
||||||
|
max_length: int = 32,
|
||||||
|
quiet: bool = False,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Find a unique name for the pattern.
|
||||||
|
|
||||||
|
This function may be overridden in a subclass or monkey-patched to fit the caller's requirements.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Preferred name for the pattern. Default '__'.
|
||||||
|
sanitize: Allows only alphanumeric charaters and _?$. Replaces invalid characters with underscores.
|
||||||
|
max_length: Names longer than this will be truncated.
|
||||||
|
quiet: If `True`, suppress log messages.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Unique name for this library.
|
||||||
|
"""
|
||||||
|
if sanitize:
|
||||||
|
# Remove invalid characters
|
||||||
|
sanitized_name = re.compile(r'[^A-Za-z0-9_\?\$]').sub('_', name)
|
||||||
|
else:
|
||||||
|
sanitized_name = name
|
||||||
|
|
||||||
|
ii = 0
|
||||||
|
suffixed_name = sanitized_name
|
||||||
|
while suffixed_name in self or suffixed_name == '':
|
||||||
|
suffix = base64.b64encode(struct.pack('>Q', ii), b'$?').decode('ASCII')
|
||||||
|
|
||||||
|
suffixed_name = sanitized_name + '$' + suffix[:-1].lstrip('A')
|
||||||
|
ii += 1
|
||||||
|
|
||||||
|
if len(suffixed_name) > max_length:
|
||||||
|
if name == '':
|
||||||
|
raise LibraryError(f'No valid pattern names remaining within the specified {max_length=}')
|
||||||
|
|
||||||
|
cropped_name = self.get_name(sanitized_name[:-1], sanitize=sanitize, max_length=max_length, quiet=True)
|
||||||
|
else:
|
||||||
|
cropped_name = suffixed_name
|
||||||
|
|
||||||
|
if not quiet:
|
||||||
|
logger.info(f'Requested name "{name}" changed to "{cropped_name}"')
|
||||||
|
|
||||||
|
return cropped_name
|
||||||
|
|
||||||
|
def find_toplevel(self) -> List[str]:
|
||||||
|
"""
|
||||||
|
Return the list of all patterns that are not referenced by any other pattern in the library.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of pattern names in which no pattern is referenced by any other pattern.
|
||||||
|
"""
|
||||||
|
names = set(self.keys())
|
||||||
|
not_toplevel: Set[Optional[str]] = set()
|
||||||
|
for name in names:
|
||||||
|
not_toplevel |= set(sp.target for sp in self[name].subpatterns)
|
||||||
|
|
||||||
|
toplevel = list(names - not_toplevel)
|
||||||
|
return toplevel
|
||||||
|
|
||||||
|
def __deepcopy__(self, memo: Dict = None) -> 'Library':
|
||||||
|
raise LibraryError('Libraries cannot be deepcopied (deepcopy doesn\'t descend into closures)')
|
@ -1,2 +0,0 @@
|
|||||||
from .library import Library, PatternGenerator
|
|
||||||
from .device_library import DeviceLibrary, LibDeviceLibrary
|
|
@ -1,355 +0,0 @@
|
|||||||
"""
|
|
||||||
Library class for managing unique name->pattern mappings and
|
|
||||||
deferred loading or creation.
|
|
||||||
"""
|
|
||||||
from typing import Dict, Callable, TypeVar, TYPE_CHECKING
|
|
||||||
from typing import Any, Tuple, Union, Iterator
|
|
||||||
import logging
|
|
||||||
from pprint import pformat
|
|
||||||
from dataclasses import dataclass
|
|
||||||
import copy
|
|
||||||
|
|
||||||
from ..error import LibraryError
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from ..pattern import Pattern
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class PatternGenerator:
|
|
||||||
__slots__ = ('tag', 'gen')
|
|
||||||
tag: str
|
|
||||||
""" Unique identifier for the source """
|
|
||||||
|
|
||||||
gen: Callable[[], 'Pattern']
|
|
||||||
""" Function which generates a pattern when called """
|
|
||||||
|
|
||||||
|
|
||||||
L = TypeVar('L', bound='Library')
|
|
||||||
|
|
||||||
|
|
||||||
class Library:
|
|
||||||
"""
|
|
||||||
This class is usually used to create a library of Patterns by mapping names to
|
|
||||||
functions which generate or load the relevant `Pattern` object as-needed.
|
|
||||||
|
|
||||||
Generated/loaded patterns can have "symbolic" references, where a SubPattern
|
|
||||||
object `sp` has a `None`-valued `sp.pattern` attribute, in which case the
|
|
||||||
Library expects `sp.identifier[0]` to contain a string which specifies the
|
|
||||||
referenced pattern's name.
|
|
||||||
|
|
||||||
Patterns can either be "primary" (default) or "secondary". Both get the
|
|
||||||
same deferred-load behavior, but "secondary" patterns may have conflicting
|
|
||||||
names and are not accessible through basic []-indexing. They are only used
|
|
||||||
to fill symbolic references in cases where there is no "primary" pattern
|
|
||||||
available, and only if both the referencing and referenced pattern-generators'
|
|
||||||
`tag` values match (i.e., only if they came from the same source).
|
|
||||||
|
|
||||||
Primary patterns can be turned into secondary patterns with the `demote`
|
|
||||||
method, `promote` performs the reverse (secondary -> primary) operation.
|
|
||||||
|
|
||||||
The `set_const` and `set_value` methods provide an easy way to transparently
|
|
||||||
construct PatternGenerator objects and directly set create "secondary"
|
|
||||||
patterns.
|
|
||||||
|
|
||||||
The cache can be disabled by setting the `enable_cache` attribute to `False`.
|
|
||||||
"""
|
|
||||||
primary: Dict[str, PatternGenerator]
|
|
||||||
secondary: Dict[Tuple[str, str], PatternGenerator]
|
|
||||||
cache: Dict[Union[str, Tuple[str, str]], 'Pattern']
|
|
||||||
enable_cache: bool = True
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.primary = {}
|
|
||||||
self.secondary = {}
|
|
||||||
self.cache = {}
|
|
||||||
|
|
||||||
def __setitem__(self, key: str, value: PatternGenerator) -> None:
|
|
||||||
self.primary[key] = value
|
|
||||||
if key in self.cache:
|
|
||||||
logger.warning(f'Replaced library item "{key}" & existing cache entry.'
|
|
||||||
' Previously-generated Pattern will *not* be updated!')
|
|
||||||
del self.cache[key]
|
|
||||||
|
|
||||||
def __delitem__(self, key: str) -> None:
|
|
||||||
if isinstance(key, str):
|
|
||||||
del self.primary[key]
|
|
||||||
elif isinstance(key, tuple):
|
|
||||||
del self.secondary[key]
|
|
||||||
|
|
||||||
if key in self.cache:
|
|
||||||
logger.warning(f'Deleting library item "{key}" & existing cache entry.'
|
|
||||||
' Previously-generated Pattern may remain in the wild!')
|
|
||||||
del self.cache[key]
|
|
||||||
|
|
||||||
def __getitem__(self, key: str) -> 'Pattern':
|
|
||||||
return self.get_primary(key)
|
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[str]:
|
|
||||||
return iter(self.keys())
|
|
||||||
|
|
||||||
def __contains__(self, key: str) -> bool:
|
|
||||||
return key in self.primary
|
|
||||||
|
|
||||||
def get_primary(self, key: str) -> 'Pattern':
|
|
||||||
if self.enable_cache and key in self.cache:
|
|
||||||
logger.debug(f'found {key} in cache')
|
|
||||||
return self.cache[key]
|
|
||||||
|
|
||||||
logger.debug(f'loading {key}')
|
|
||||||
pg = self.primary[key]
|
|
||||||
pat = pg.gen()
|
|
||||||
self.resolve_subpatterns(pat, pg.tag)
|
|
||||||
self.cache[key] = pat
|
|
||||||
return pat
|
|
||||||
|
|
||||||
def get_secondary(self, key: str, tag: str) -> 'Pattern':
|
|
||||||
logger.debug(f'get_secondary({key}, {tag})')
|
|
||||||
key2 = (key, tag)
|
|
||||||
if self.enable_cache and key2 in self.cache:
|
|
||||||
return self.cache[key2]
|
|
||||||
|
|
||||||
pg = self.secondary[key2]
|
|
||||||
pat = pg.gen()
|
|
||||||
self.resolve_subpatterns(pat, pg.tag)
|
|
||||||
self.cache[key2] = pat
|
|
||||||
return pat
|
|
||||||
|
|
||||||
def set_secondary(self, key: str, tag: str, value: PatternGenerator) -> None:
|
|
||||||
self.secondary[(key, tag)] = value
|
|
||||||
if (key, tag) in self.cache:
|
|
||||||
logger.warning(f'Replaced library item "{key}" & existing cache entry.'
|
|
||||||
' Previously-generated Pattern will *not* be updated!')
|
|
||||||
del self.cache[(key, tag)]
|
|
||||||
|
|
||||||
def resolve_subpatterns(self, pat: 'Pattern', tag: str) -> 'Pattern':
|
|
||||||
logger.debug(f'Resolving subpatterns in {pat.name}')
|
|
||||||
for sp in pat.subpatterns:
|
|
||||||
if sp.pattern is not None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
key = sp.identifier[0]
|
|
||||||
if key in self.primary:
|
|
||||||
sp.pattern = self.get_primary(key)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if (key, tag) in self.secondary:
|
|
||||||
sp.pattern = self.get_secondary(key, tag)
|
|
||||||
continue
|
|
||||||
|
|
||||||
raise LibraryError(f'Broken reference to {key} (tag {tag})')
|
|
||||||
return pat
|
|
||||||
|
|
||||||
def keys(self) -> Iterator[str]:
|
|
||||||
return iter(self.primary.keys())
|
|
||||||
|
|
||||||
def values(self) -> Iterator['Pattern']:
|
|
||||||
return iter(self[key] for key in self.keys())
|
|
||||||
|
|
||||||
def items(self) -> Iterator[Tuple[str, 'Pattern']]:
|
|
||||||
return iter((key, self[key]) for key in self.keys())
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return '<Library with keys ' + repr(list(self.primary.keys())) + '>'
|
|
||||||
|
|
||||||
def set_const(
|
|
||||||
self,
|
|
||||||
key: str,
|
|
||||||
tag: Any,
|
|
||||||
const: 'Pattern',
|
|
||||||
secondary: bool = False,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Convenience function to avoid having to manually wrap
|
|
||||||
constant values into callables.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Lookup key, usually the cell/pattern name
|
|
||||||
tag: Unique tag for the source, used to disambiguate secondary patterns
|
|
||||||
const: Pattern object to return
|
|
||||||
secondary: If True, this pattern is not accessible for normal lookup, and is
|
|
||||||
only used as a sub-component of other patterns if no non-secondary
|
|
||||||
equivalent is available.
|
|
||||||
"""
|
|
||||||
pg = PatternGenerator(tag=tag, gen=lambda: const)
|
|
||||||
if secondary:
|
|
||||||
self.secondary[(key, tag)] = pg
|
|
||||||
else:
|
|
||||||
self.primary[key] = pg
|
|
||||||
|
|
||||||
def set_value(
|
|
||||||
self,
|
|
||||||
key: str,
|
|
||||||
tag: str,
|
|
||||||
value: Callable[[], 'Pattern'],
|
|
||||||
secondary: bool = False,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Convenience function to automatically build a PatternGenerator.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Lookup key, usually the cell/pattern name
|
|
||||||
tag: Unique tag for the source, used to disambiguate secondary patterns
|
|
||||||
value: Callable which takes no arguments and generates the `Pattern` object
|
|
||||||
secondary: If True, this pattern is not accessible for normal lookup, and is
|
|
||||||
only used as a sub-component of other patterns if no non-secondary
|
|
||||||
equivalent is available.
|
|
||||||
"""
|
|
||||||
pg = PatternGenerator(tag=tag, gen=value)
|
|
||||||
if secondary:
|
|
||||||
self.secondary[(key, tag)] = pg
|
|
||||||
else:
|
|
||||||
self.primary[key] = pg
|
|
||||||
|
|
||||||
def precache(self: L) -> L:
|
|
||||||
"""
|
|
||||||
Force all patterns into the cache
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
for key in self.primary:
|
|
||||||
_ = self.get_primary(key)
|
|
||||||
for key2 in self.secondary:
|
|
||||||
_ = self.get_secondary(*key2)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def add(
|
|
||||||
self: L,
|
|
||||||
other: L,
|
|
||||||
use_ours: Callable[[Union[str, Tuple[str, str]]], bool] = lambda name: False,
|
|
||||||
use_theirs: Callable[[Union[str, Tuple[str, str]]], bool] = lambda name: False,
|
|
||||||
) -> L:
|
|
||||||
"""
|
|
||||||
Add keys from another library into this one.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
other: The library to insert keys from
|
|
||||||
use_ours: Decision function for name conflicts.
|
|
||||||
May be called with cell names and (name, tag) tuples for primary or
|
|
||||||
secondary cells, respectively.
|
|
||||||
Should return `True` if the value from `self` should be used.
|
|
||||||
use_theirs: Decision function for name conflicts. Same format as `use_ours`.
|
|
||||||
Should return `True` if the value from `other` should be used.
|
|
||||||
`use_ours` takes priority over `use_theirs`.
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
duplicates1 = set(self.primary.keys()) & set(other.primary.keys())
|
|
||||||
duplicates2 = set(self.secondary.keys()) & set(other.secondary.keys())
|
|
||||||
keep_ours1 = set(name for name in duplicates1 if use_ours(name))
|
|
||||||
keep_ours2 = set(name for name in duplicates2 if use_ours(name))
|
|
||||||
keep_theirs1 = set(name for name in duplicates1 - keep_ours1 if use_theirs(name))
|
|
||||||
keep_theirs2 = set(name for name in duplicates2 - keep_ours2 if use_theirs(name))
|
|
||||||
conflicts1 = duplicates1 - keep_ours1 - keep_theirs1
|
|
||||||
conflicts2 = duplicates2 - keep_ours2 - keep_theirs2
|
|
||||||
|
|
||||||
if conflicts1:
|
|
||||||
raise LibraryError('Unresolved duplicate keys encountered in library merge: ' + pformat(conflicts1))
|
|
||||||
|
|
||||||
if conflicts2:
|
|
||||||
raise LibraryError('Unresolved duplicate secondary keys encountered in library merge: ' + pformat(conflicts2))
|
|
||||||
|
|
||||||
for key1 in set(other.primary.keys()) - keep_ours1:
|
|
||||||
self[key1] = other.primary[key1]
|
|
||||||
if key1 in other.cache:
|
|
||||||
self.cache[key1] = other.cache[key1]
|
|
||||||
|
|
||||||
for key2 in set(other.secondary.keys()) - keep_ours2:
|
|
||||||
self.set_secondary(*key2, other.secondary[key2])
|
|
||||||
if key2 in other.cache:
|
|
||||||
self.cache[key2] = other.cache[key2]
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
def demote(self, key: str) -> None:
|
|
||||||
"""
|
|
||||||
Turn a primary pattern into a secondary one.
|
|
||||||
It will no longer be accessible through [] indexing and will only be used to
|
|
||||||
when referenced by other patterns from the same source, and only if no primary
|
|
||||||
pattern with the same name exists.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Lookup key, usually the cell/pattern name
|
|
||||||
"""
|
|
||||||
pg = self.primary[key]
|
|
||||||
key2 = (key, pg.tag)
|
|
||||||
self.secondary[key2] = pg
|
|
||||||
if key in self.cache:
|
|
||||||
self.cache[key2] = self.cache[key]
|
|
||||||
del self[key]
|
|
||||||
|
|
||||||
def promote(self, key: str, tag: str) -> None:
|
|
||||||
"""
|
|
||||||
Turn a secondary pattern into a primary one.
|
|
||||||
It will become accessible through [] indexing and will be used to satisfy any
|
|
||||||
reference to a pattern with its key, regardless of tag.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Lookup key, usually the cell/pattern name
|
|
||||||
tag: Unique tag for identifying the pattern's source, used to disambiguate
|
|
||||||
secondary patterns
|
|
||||||
"""
|
|
||||||
if key in self.primary:
|
|
||||||
raise LibraryError(f'Promoting ({key}, {tag}), but {key} already exists in primary!')
|
|
||||||
|
|
||||||
key2 = (key, tag)
|
|
||||||
pg = self.secondary[key2]
|
|
||||||
self.primary[key] = pg
|
|
||||||
if key2 in self.cache:
|
|
||||||
self.cache[key] = self.cache[key2]
|
|
||||||
del self.secondary[key2]
|
|
||||||
del self.cache[key2]
|
|
||||||
|
|
||||||
def copy(self, preserve_cache: bool = False) -> 'Library':
|
|
||||||
"""
|
|
||||||
Create a copy of this `Library`.
|
|
||||||
|
|
||||||
A shallow copy is made of the contained dicts.
|
|
||||||
Note that you should probably clear the cache (with `clear_cache()`) after copying.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A copy of self
|
|
||||||
"""
|
|
||||||
new = Library()
|
|
||||||
new.primary.update(self.primary)
|
|
||||||
new.secondary.update(self.secondary)
|
|
||||||
new.cache.update(self.cache)
|
|
||||||
return new
|
|
||||||
|
|
||||||
def clear_cache(self: L) -> L:
|
|
||||||
"""
|
|
||||||
Clear the cache of this library.
|
|
||||||
This is usually used before modifying or deleting cells, e.g. when merging
|
|
||||||
with another library.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
self.cache = {}
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
r"""
|
|
||||||
# Add a filter for names which aren't added
|
|
||||||
|
|
||||||
- Registration:
|
|
||||||
- scanned files (tag=filename, gen_fn[stream, {name: pos}])
|
|
||||||
- generator functions (tag='fn?', gen_fn[params])
|
|
||||||
- merge decision function (based on tag and cell name, can be "neither") ??? neither=keep both, load using same tag!
|
|
||||||
- Load process:
|
|
||||||
- file:
|
|
||||||
- read single cell
|
|
||||||
- check subpat identifiers, and load stuff recursively based on those. If not present, load from same file??
|
|
||||||
- function:
|
|
||||||
- generate cell
|
|
||||||
- traverse and check if we should load any subcells from elsewhere. replace if so.
|
|
||||||
* should fn generate subcells at all, or register those separately and have us control flow? maybe ask us and generate itself if not present?
|
|
||||||
|
|
||||||
- Scan all GDS files, save name -> (file, position). Keep the streams handy.
|
|
||||||
- Merge all names. This requires subcell merge because we don't know hierarchy.
|
|
||||||
- possibly include a "neither" option during merge, to deal with subcells. Means: just use parent's file.
|
|
||||||
"""
|
|
@ -3,9 +3,8 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import List, Callable, Tuple, Dict, Union, Set, Sequence, Optional, Type, overload, cast
|
from typing import List, Callable, Tuple, Dict, Union, Set, Sequence, Optional, Type, overload, cast
|
||||||
from typing import MutableMapping, Iterable, TypeVar, Any
|
from typing import Mapping, MutableMapping, Iterable, TypeVar, Any
|
||||||
import copy
|
import copy
|
||||||
import pickle
|
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
@ -18,23 +17,20 @@ from .subpattern import SubPattern
|
|||||||
from .shapes import Shape, Polygon
|
from .shapes import Shape, Polygon
|
||||||
from .label import Label
|
from .label import Label
|
||||||
from .utils import rotation_matrix_2d, normalize_mirror, AutoSlots, annotations_t
|
from .utils import rotation_matrix_2d, normalize_mirror, AutoSlots, annotations_t
|
||||||
from .error import PatternError, PatternLockedError
|
from .error import PatternError
|
||||||
from .traits import LockableImpl, AnnotatableImpl, Scalable, Mirrorable
|
from .traits import AnnotatableImpl, Scalable, Mirrorable
|
||||||
from .traits import Rotatable, Positionable
|
from .traits import Rotatable, Positionable
|
||||||
|
|
||||||
|
|
||||||
visitor_function_t = Callable[['Pattern', Tuple['Pattern'], Dict, NDArray[numpy.float64]], 'Pattern']
|
|
||||||
|
|
||||||
|
|
||||||
P = TypeVar('P', bound='Pattern')
|
P = TypeVar('P', bound='Pattern')
|
||||||
|
|
||||||
|
|
||||||
class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
class Pattern(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.
|
||||||
"""
|
"""
|
||||||
__slots__ = ('shapes', 'labels', 'subpatterns', 'name')
|
__slots__ = ('shapes', 'labels', 'subpatterns')
|
||||||
|
|
||||||
shapes: List[Shape]
|
shapes: List[Shape]
|
||||||
""" List of all shapes in this Pattern.
|
""" List of all shapes in this Pattern.
|
||||||
@ -50,18 +46,13 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
(i.e. multiple instances of the same object).
|
(i.e. multiple instances of the same object).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name: str
|
|
||||||
""" A name for this pattern """
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str = '',
|
|
||||||
*,
|
*,
|
||||||
shapes: Sequence[Shape] = (),
|
shapes: Sequence[Shape] = (),
|
||||||
labels: Sequence[Label] = (),
|
labels: Sequence[Label] = (),
|
||||||
subpatterns: Sequence[SubPattern] = (),
|
subpatterns: Sequence[SubPattern] = (),
|
||||||
annotations: Optional[annotations_t] = None,
|
annotations: Optional[annotations_t] = None,
|
||||||
locked: bool = False,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Basic init; arguments get assigned to member variables.
|
Basic init; arguments get assigned to member variables.
|
||||||
@ -71,10 +62,7 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
shapes: Initial shapes in the Pattern
|
shapes: Initial shapes in the Pattern
|
||||||
labels: Initial labels in the Pattern
|
labels: Initial labels in the Pattern
|
||||||
subpatterns: Initial subpatterns in the Pattern
|
subpatterns: Initial subpatterns in the Pattern
|
||||||
name: An identifier for the Pattern
|
|
||||||
locked: Whether to lock the pattern after construction
|
|
||||||
"""
|
"""
|
||||||
LockableImpl.unlock(self)
|
|
||||||
if isinstance(shapes, list):
|
if isinstance(shapes, list):
|
||||||
self.shapes = shapes
|
self.shapes = shapes
|
||||||
else:
|
else:
|
||||||
@ -91,41 +79,25 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
self.subpatterns = list(subpatterns)
|
self.subpatterns = list(subpatterns)
|
||||||
|
|
||||||
self.annotations = annotations if annotations is not None else {}
|
self.annotations = annotations if annotations is not None else {}
|
||||||
self.name = name
|
|
||||||
self.set_locked(locked)
|
|
||||||
|
|
||||||
def __copy__(self, memo: Dict = None) -> 'Pattern':
|
def __copy__(self, memo: Dict = None) -> 'Pattern':
|
||||||
return Pattern(name=self.name,
|
return Pattern(
|
||||||
shapes=copy.deepcopy(self.shapes),
|
shapes=copy.deepcopy(self.shapes),
|
||||||
labels=copy.deepcopy(self.labels),
|
labels=copy.deepcopy(self.labels),
|
||||||
subpatterns=[copy.copy(sp) for sp in self.subpatterns],
|
subpatterns=[copy.copy(sp) for sp in self.subpatterns],
|
||||||
annotations=copy.deepcopy(self.annotations),
|
annotations=copy.deepcopy(self.annotations),
|
||||||
locked=self.locked)
|
)
|
||||||
|
|
||||||
def __deepcopy__(self, memo: Dict = None) -> 'Pattern':
|
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,
|
|
||||||
shapes=copy.deepcopy(self.shapes, memo),
|
shapes=copy.deepcopy(self.shapes, memo),
|
||||||
labels=copy.deepcopy(self.labels, memo),
|
labels=copy.deepcopy(self.labels, memo),
|
||||||
subpatterns=copy.deepcopy(self.subpatterns, memo),
|
subpatterns=copy.deepcopy(self.subpatterns, memo),
|
||||||
annotations=copy.deepcopy(self.annotations, memo),
|
annotations=copy.deepcopy(self.annotations, memo),
|
||||||
locked=self.locked)
|
)
|
||||||
return new
|
return new
|
||||||
|
|
||||||
def rename(self: P, name: str) -> P:
|
|
||||||
"""
|
|
||||||
Chainable function for renaming the pattern.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: The new name
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
self.name = name
|
|
||||||
return self
|
|
||||||
|
|
||||||
def append(self: P, other_pattern: P) -> P:
|
def append(self: P, other_pattern: P) -> P:
|
||||||
"""
|
"""
|
||||||
Appends all shapes, labels and subpatterns from other_pattern to self's shapes,
|
Appends all shapes, labels and subpatterns from other_pattern to self's shapes,
|
||||||
@ -144,10 +116,9 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
|
|
||||||
def subset(
|
def subset(
|
||||||
self,
|
self,
|
||||||
shapes_func: Callable[[Shape], bool] = None,
|
shapes: Callable[[Shape], bool] = None,
|
||||||
labels_func: Callable[[Label], bool] = None,
|
labels: Callable[[Label], bool] = None,
|
||||||
subpatterns_func: Callable[[SubPattern], bool] = None,
|
subpatterns: Callable[[SubPattern], bool] = None,
|
||||||
recursive: bool = False,
|
|
||||||
) -> 'Pattern':
|
) -> '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
|
||||||
@ -155,169 +126,24 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
Self is _not_ altered, but shapes, labels, and subpatterns are _not_ copied.
|
Self is _not_ altered, but shapes, labels, and subpatterns are _not_ copied.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
shapes_func: Given a shape, returns a boolean denoting whether the shape is a member
|
shapes: Given a shape, returns a boolean denoting whether the shape is a member
|
||||||
of the subset. Default always returns False.
|
of the subset. Default always returns False.
|
||||||
labels_func: Given a label, returns a boolean denoting whether the label is a member
|
labels: Given a label, returns a boolean denoting whether the label is a member
|
||||||
of the subset. Default always returns False.
|
of the subset. Default always returns False.
|
||||||
subpatterns_func: Given a subpattern, returns a boolean denoting if it is a member
|
subpatterns: Given a subpattern, returns a boolean denoting if it is a member
|
||||||
of the subset. Default always returns False.
|
of the subset. Default always returns False.
|
||||||
recursive: If True, also calls .subset() recursively on patterns referenced by this
|
|
||||||
pattern.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
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['Pattern']) -> Optional['Pattern']:
|
pat = Pattern()
|
||||||
if src is None:
|
if shapes is not None:
|
||||||
return None
|
pat.shapes = [s for s in self.shapes if shapes(s)]
|
||||||
pat = Pattern(name=src.name)
|
if labels is not None:
|
||||||
if shapes_func is not None:
|
pat.labels = [s for s in self.labels if labels(s)]
|
||||||
pat.shapes = [s for s in src.shapes if shapes_func(s)]
|
if subpatterns is not None:
|
||||||
if labels_func is not None:
|
pat.subpatterns = [s for s in self.subpatterns if subpatterns(s)]
|
||||||
pat.labels = [s for s in src.labels if labels_func(s)]
|
|
||||||
if subpatterns_func is not None:
|
|
||||||
pat.subpatterns = [s for s in src.subpatterns if subpatterns_func(s)]
|
|
||||||
return pat
|
|
||||||
|
|
||||||
if recursive:
|
|
||||||
pat = self.apply(do_subset)
|
|
||||||
else:
|
|
||||||
pat = do_subset(self)
|
|
||||||
|
|
||||||
assert(pat is not None)
|
|
||||||
return pat
|
|
||||||
|
|
||||||
def apply(
|
|
||||||
self,
|
|
||||||
func: Callable[[Optional['Pattern']], Optional['Pattern']],
|
|
||||||
memo: Optional[Dict[int, Optional['Pattern']]] = None,
|
|
||||||
) -> Optional['Pattern']:
|
|
||||||
"""
|
|
||||||
Recursively apply func() to this pattern and any pattern it references.
|
|
||||||
func() is expected to take and return a Pattern.
|
|
||||||
func() is first applied to the pattern as a whole, then any referenced patterns.
|
|
||||||
It is only applied to any given pattern once, regardless of how many times it is
|
|
||||||
referenced.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
func: Function which accepts a Pattern, and returns a pattern.
|
|
||||||
memo: Dictionary used to avoid re-running on multiply-referenced patterns.
|
|
||||||
Stores `{id(pattern): func(pattern)}` for patterns which have already been processed.
|
|
||||||
Default `None` (no already-processed patterns).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The result of applying func() to this pattern and all subpatterns.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
PatternError if called on a pattern containing a circular reference.
|
|
||||||
"""
|
|
||||||
if memo is None:
|
|
||||||
memo = {}
|
|
||||||
|
|
||||||
pat_id = id(self)
|
|
||||||
if pat_id not in memo:
|
|
||||||
memo[pat_id] = None
|
|
||||||
pat = func(self)
|
|
||||||
if pat is not None:
|
|
||||||
for subpat in pat.subpatterns:
|
|
||||||
if subpat.pattern is None:
|
|
||||||
subpat.pattern = func(None)
|
|
||||||
else:
|
|
||||||
subpat.pattern = subpat.pattern.apply(func, memo)
|
|
||||||
memo[pat_id] = pat
|
|
||||||
elif memo[pat_id] is None:
|
|
||||||
raise PatternError('.apply() called on pattern with circular reference')
|
|
||||||
else:
|
|
||||||
pat = memo[pat_id]
|
|
||||||
return pat
|
|
||||||
|
|
||||||
def dfs(
|
|
||||||
self: P,
|
|
||||||
visit_before: visitor_function_t = None,
|
|
||||||
visit_after: visitor_function_t = None,
|
|
||||||
transform: Union[ArrayLike, bool, None] = False,
|
|
||||||
memo: Optional[Dict] = None,
|
|
||||||
hierarchy: Tuple[P, ...] = (),
|
|
||||||
) -> P:
|
|
||||||
"""
|
|
||||||
Convenience function.
|
|
||||||
Performs a depth-first traversal of this pattern and its subpatterns.
|
|
||||||
At each pattern in the tree, the following sequence is called:
|
|
||||||
```
|
|
||||||
current_pattern = visit_before(current_pattern, **vist_args)
|
|
||||||
for sp in current_pattern.subpatterns]
|
|
||||||
sp.pattern = sp.pattern.df(visit_before, visit_after, updated_transform,
|
|
||||||
memo, (current_pattern,) + hierarchy)
|
|
||||||
current_pattern = visit_after(current_pattern, **visit_args)
|
|
||||||
```
|
|
||||||
where `visit_args` are
|
|
||||||
`hierarchy`: (top_pattern, L1_pattern, L2_pattern, ..., parent_pattern)
|
|
||||||
tuple of all parent-and-higher patterns
|
|
||||||
`transform`: numpy.ndarray containing cumulative
|
|
||||||
[x_offset, y_offset, rotation (rad), mirror_x (0 or 1)]
|
|
||||||
for the instance being visited
|
|
||||||
`memo`: Arbitrary dict (not altered except by visit_*())
|
|
||||||
|
|
||||||
Args:
|
|
||||||
visit_before: Function to call before traversing subpatterns.
|
|
||||||
Should accept a `Pattern` and `**visit_args`, and return the (possibly modified)
|
|
||||||
pattern. Default `None` (not called).
|
|
||||||
visit_after: Function to call after traversing subpatterns.
|
|
||||||
Should accept a Pattern and **visit_args, and return the (possibly modified)
|
|
||||||
pattern. Default `None` (not called).
|
|
||||||
transform: Initial value for `visit_args['transform']`.
|
|
||||||
Can be `False`, in which case the transform is not calculated.
|
|
||||||
`True` or `None` is interpreted as `[0, 0, 0, 0]`.
|
|
||||||
memo: Arbitrary dict for use by `visit_*()` functions. Default `None` (empty dict).
|
|
||||||
hierarchy: Tuple of patterns specifying the hierarchy above the current pattern.
|
|
||||||
Appended to the start of the generated `visit_args['hierarchy']`.
|
|
||||||
Default is an empty tuple.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The result, including `visit_before(self, ...)` and `visit_after(self, ...)`.
|
|
||||||
Note that `self` may also be altered!
|
|
||||||
"""
|
|
||||||
if memo is None:
|
|
||||||
memo = {}
|
|
||||||
|
|
||||||
if transform is None or transform is True:
|
|
||||||
transform = numpy.zeros(4)
|
|
||||||
elif transform is not False:
|
|
||||||
transform = numpy.array(transform)
|
|
||||||
|
|
||||||
if self in hierarchy:
|
|
||||||
raise PatternError('.dfs() called on pattern with circular reference')
|
|
||||||
|
|
||||||
pat = self
|
|
||||||
if visit_before is not None:
|
|
||||||
pat = visit_before(pat, hierarchy=hierarchy, memo=memo, transform=transform) # type: ignore
|
|
||||||
|
|
||||||
for subpattern in self.subpatterns:
|
|
||||||
if transform is not False:
|
|
||||||
sign = numpy.ones(2)
|
|
||||||
if transform[3]:
|
|
||||||
sign[1] = -1
|
|
||||||
xy = numpy.dot(rotation_matrix_2d(transform[2]), subpattern.offset * sign)
|
|
||||||
mirror_x, angle = normalize_mirror(subpattern.mirrored)
|
|
||||||
angle += subpattern.rotation
|
|
||||||
sp_transform = transform + (xy[0], xy[1], angle, mirror_x)
|
|
||||||
sp_transform[3] %= 2
|
|
||||||
else:
|
|
||||||
sp_transform = False
|
|
||||||
|
|
||||||
if subpattern.pattern is not None:
|
|
||||||
result = subpattern.pattern.dfs(visit_before=visit_before,
|
|
||||||
visit_after=visit_after,
|
|
||||||
transform=sp_transform,
|
|
||||||
memo=memo,
|
|
||||||
hierarchy=hierarchy + (self,))
|
|
||||||
if result is not subpattern.pattern:
|
|
||||||
# skip assignment to avoid PatternLockedError unless modified
|
|
||||||
subpattern.pattern = result
|
|
||||||
|
|
||||||
if visit_after is not None:
|
|
||||||
pat = visit_after(pat, hierarchy=hierarchy, memo=memo, transform=transform) # type: ignore
|
|
||||||
return pat
|
return pat
|
||||||
|
|
||||||
def polygonize(
|
def polygonize(
|
||||||
@ -326,8 +152,7 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
poly_max_arclen: Optional[float] = None,
|
poly_max_arclen: Optional[float] = None,
|
||||||
) -> P:
|
) -> P:
|
||||||
"""
|
"""
|
||||||
Calls `.to_polygons(...)` on all the shapes in this Pattern and any referenced patterns,
|
Calls `.to_polygons(...)` on all the shapes in this Pattern, replacing them with the returned polygons.
|
||||||
replacing them with the returned polygons.
|
|
||||||
Arguments are passed directly to `shape.to_polygons(...)`.
|
Arguments are passed directly to `shape.to_polygons(...)`.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -341,12 +166,9 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
self
|
self
|
||||||
"""
|
"""
|
||||||
old_shapes = self.shapes
|
old_shapes = self.shapes
|
||||||
self.shapes = list(chain.from_iterable(
|
self.shapes = list(chain.from_iterable((
|
||||||
(shape.to_polygons(poly_num_points, poly_max_arclen)
|
shape.to_polygons(poly_num_points, poly_max_arclen)
|
||||||
for shape in old_shapes)))
|
for shape in old_shapes)))
|
||||||
for subpat in self.subpatterns:
|
|
||||||
if subpat.pattern is not None:
|
|
||||||
subpat.pattern.polygonize(poly_num_points, poly_max_arclen)
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def manhattanize(
|
def manhattanize(
|
||||||
@ -355,7 +177,7 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
grid_y: ArrayLike,
|
grid_y: ArrayLike,
|
||||||
) -> P:
|
) -> P:
|
||||||
"""
|
"""
|
||||||
Calls `.polygonize()` and `.flatten()` on the pattern, then calls `.manhattanize()` on all the
|
Calls `.polygonize()` on the pattern, then calls `.manhattanize()` on all the
|
||||||
resulting shapes, replacing them with the returned Manhattan polygons.
|
resulting shapes, replacing them with the returned Manhattan polygons.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -366,84 +188,13 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
self
|
self
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.polygonize().flatten()
|
self.polygonize()
|
||||||
old_shapes = self.shapes
|
old_shapes = self.shapes
|
||||||
self.shapes = list(chain.from_iterable(
|
self.shapes = list(chain.from_iterable(
|
||||||
(shape.manhattanize(grid_x, grid_y) for shape in old_shapes)))
|
(shape.manhattanize(grid_x, grid_y) for shape in old_shapes)))
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def subpatternize(
|
def as_polygons(self, library: Mapping[str, Pattern]) -> List[NDArray[numpy.float64]]:
|
||||||
self: P,
|
|
||||||
recursive: bool = True,
|
|
||||||
norm_value: int = int(1e6),
|
|
||||||
exclude_types: Tuple[Type] = (Polygon,)
|
|
||||||
) -> P:
|
|
||||||
"""
|
|
||||||
Iterates through this `Pattern` and all referenced `Pattern`s. Within each `Pattern`, it iterates
|
|
||||||
over all shapes, calling `.normalized_form(norm_value)` on them to retrieve a scale-,
|
|
||||||
offset-, dose-, and rotation-independent form. Each shape whose normalized form appears
|
|
||||||
more than once is removed and re-added using subpattern objects referencing a newly-created
|
|
||||||
`Pattern` containing only the normalized form of the shape.
|
|
||||||
|
|
||||||
Note:
|
|
||||||
The default norm_value was chosen to give a reasonable precision when converting
|
|
||||||
to GDSII, which uses integer values for pixel coordinates.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
recursive: Whether to call recursively on self's subpatterns. Default `True`.
|
|
||||||
norm_value: Passed to `shape.normalized_form(norm_value)`. Default `1e6` (see function
|
|
||||||
note about GDSII)
|
|
||||||
exclude_types: Shape types passed in this argument are always left untouched, for
|
|
||||||
speed or convenience. Default: `(shapes.Polygon,)`
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
|
|
||||||
if exclude_types is None:
|
|
||||||
exclude_types = ()
|
|
||||||
|
|
||||||
if recursive:
|
|
||||||
for subpat in self.subpatterns:
|
|
||||||
if subpat.pattern is None:
|
|
||||||
continue
|
|
||||||
subpat.pattern.subpatternize(recursive=True,
|
|
||||||
norm_value=norm_value,
|
|
||||||
exclude_types=exclude_types)
|
|
||||||
|
|
||||||
# Create a dict which uses the label tuple from `.normalized_form()` as a key, and which
|
|
||||||
# stores `(function_to_create_normalized_shape, [(index_in_shapes, values), ...])`, where
|
|
||||||
# values are the `(offset, scale, rotation, dose)` values as calculated by `.normalized_form()`
|
|
||||||
shape_table: MutableMapping[Tuple, List] = defaultdict(lambda: [None, list()])
|
|
||||||
for i, shape in enumerate(self.shapes):
|
|
||||||
if not any((isinstance(shape, t) for t in exclude_types)):
|
|
||||||
label, values, func = shape.normalized_form(norm_value)
|
|
||||||
shape_table[label][0] = func
|
|
||||||
shape_table[label][1].append((i, values))
|
|
||||||
|
|
||||||
# Iterate over the normalized shapes in the table. If any normalized shape occurs more than
|
|
||||||
# once, create a `Pattern` holding a normalized shape object, and add `self.subpatterns`
|
|
||||||
# entries for each occurrence in self. Also, note down that we should delete the
|
|
||||||
# `self.shapes` entries for which we made SubPatterns.
|
|
||||||
shapes_to_remove = []
|
|
||||||
for label in shape_table:
|
|
||||||
if len(shape_table[label][1]) > 1:
|
|
||||||
shape = shape_table[label][0]()
|
|
||||||
pat = Pattern(shapes=[shape])
|
|
||||||
|
|
||||||
for i, values in shape_table[label][1]:
|
|
||||||
(offset, scale, rotation, mirror_x, dose) = values
|
|
||||||
self.addsp(pattern=pat, offset=offset, scale=scale,
|
|
||||||
rotation=rotation, dose=dose, mirrored=(mirror_x, False))
|
|
||||||
shapes_to_remove.append(i)
|
|
||||||
|
|
||||||
# Remove any shapes for which we have created subpatterns.
|
|
||||||
for i in sorted(shapes_to_remove, reverse=True):
|
|
||||||
del self.shapes[i]
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
def as_polygons(self) -> List[NDArray[numpy.float64]]:
|
|
||||||
"""
|
"""
|
||||||
Represents the pattern as a list of polygons.
|
Represents the pattern as a list of polygons.
|
||||||
|
|
||||||
@ -454,95 +205,22 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
A list of `(Ni, 2)` `numpy.ndarray`s specifying vertices of the polygons. Each ndarray
|
A list of `(Ni, 2)` `numpy.ndarray`s specifying vertices of the polygons. Each ndarray
|
||||||
is of the form `[[x0, y0], [x1, y1],...]`.
|
is of the form `[[x0, y0], [x1, y1],...]`.
|
||||||
"""
|
"""
|
||||||
pat = self.deepcopy().deepunlock().polygonize().flatten()
|
pat = self.deepcopy().polygonize().flatten(library=library)
|
||||||
return [shape.vertices + shape.offset for shape in pat.shapes] # type: ignore # mypy can't figure out that shapes are all Polygons now
|
return [shape.vertices + shape.offset for shape in pat.shapes] # type: ignore # mypy can't figure out that shapes are all Polygons now
|
||||||
|
|
||||||
@overload
|
def referenced_patterns(self) -> Set[Optional[str]]:
|
||||||
def referenced_patterns_by_id(self) -> Dict[int, 'Pattern']:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def referenced_patterns_by_id(self, include_none: bool) -> Dict[int, Optional['Pattern']]:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def referenced_patterns_by_id(
|
|
||||||
self,
|
|
||||||
include_none: bool = False,
|
|
||||||
recursive: bool = True,
|
|
||||||
) -> Union[Dict[int, Optional['Pattern']],
|
|
||||||
Dict[int, 'Pattern']]:
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Create a dictionary with `{id(pat): pat}` for all Pattern objects referenced by this
|
Get all pattern namers referenced by this pattern. Non-recursive.
|
||||||
Pattern (by default, operates recursively on all referenced Patterns as well).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
include_none: If `True`, references to `None` will be included. Default `False`.
|
|
||||||
recursive: If `True`, operates recursively on all referenced patterns. Default `True`.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary with `{id(pat): pat}` for all referenced Pattern objects
|
A set of all pattern names referenced by this pattern.
|
||||||
"""
|
"""
|
||||||
ids: Dict[int, Optional['Pattern']] = {}
|
return set(sp.target for sp in self.subpatterns)
|
||||||
for subpat in self.subpatterns:
|
|
||||||
pat = subpat.pattern
|
|
||||||
if id(pat) in ids:
|
|
||||||
continue
|
|
||||||
if include_none or pat is not None:
|
|
||||||
ids[id(pat)] = pat
|
|
||||||
if recursive and pat is not None:
|
|
||||||
ids.update(pat.referenced_patterns_by_id())
|
|
||||||
return ids
|
|
||||||
|
|
||||||
def referenced_patterns_by_name(
|
def get_bounds(
|
||||||
self,
|
self,
|
||||||
**kwargs: Any,
|
library: Optional[Mapping[str, 'Pattern']] = None,
|
||||||
) -> List[Tuple[Optional[str], Optional['Pattern']]]:
|
) -> Optional[NDArray[numpy.float64]]:
|
||||||
"""
|
|
||||||
Create a list of `(pat.name, pat)` tuples for all Pattern objects referenced by this
|
|
||||||
Pattern (operates recursively on all referenced Patterns as well).
|
|
||||||
|
|
||||||
Note that names are not necessarily unique, so a list of tuples is returned
|
|
||||||
rather than a dict.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
**kwargs: passed to `referenced_patterns_by_id()`.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of `(pat.name, pat)` tuples for all referenced Pattern objects
|
|
||||||
"""
|
|
||||||
pats_by_id = self.referenced_patterns_by_id(**kwargs)
|
|
||||||
pat_list: List[Tuple[Optional[str], Optional['Pattern']]]
|
|
||||||
pat_list = [(p.name if p is not None else None, p) for p in pats_by_id.values()]
|
|
||||||
return pat_list
|
|
||||||
|
|
||||||
def subpatterns_by_id(
|
|
||||||
self,
|
|
||||||
include_none: bool = False,
|
|
||||||
recursive: bool = True,
|
|
||||||
) -> Dict[int, List[SubPattern]]:
|
|
||||||
"""
|
|
||||||
Create a dictionary which maps `{id(referenced_pattern): [subpattern0, ...]}`
|
|
||||||
for all SubPattern objects referenced by this Pattern (by default, operates
|
|
||||||
recursively on all referenced Patterns as well).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
include_none: If `True`, references to `None` will be included. Default `False`.
|
|
||||||
recursive: If `True`, operates recursively on all referenced patterns. Default `True`.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary mapping each pattern id to a list of subpattern objects referencing the pattern.
|
|
||||||
"""
|
|
||||||
ids: Dict[int, List[SubPattern]] = defaultdict(list)
|
|
||||||
for subpat in self.subpatterns:
|
|
||||||
pat = subpat.pattern
|
|
||||||
if include_none or pat is not None:
|
|
||||||
ids[id(pat)].append(subpat)
|
|
||||||
if recursive and pat is not None:
|
|
||||||
ids.update(pat.subpatterns_by_id(include_none=include_none))
|
|
||||||
return dict(ids)
|
|
||||||
|
|
||||||
def get_bounds(self) -> Union[NDArray[numpy.float64], None]:
|
|
||||||
"""
|
"""
|
||||||
Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the
|
Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the
|
||||||
extent of the Pattern's contents in each dimension.
|
extent of the Pattern's contents in each dimension.
|
||||||
@ -557,119 +235,42 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
min_bounds = numpy.array((+inf, +inf))
|
min_bounds = numpy.array((+inf, +inf))
|
||||||
max_bounds = numpy.array((-inf, -inf))
|
max_bounds = numpy.array((-inf, -inf))
|
||||||
|
|
||||||
for entry in chain(self.shapes, self.subpatterns, self.labels):
|
for entry in chain(self.shapes, self.labels):
|
||||||
bounds = entry.get_bounds()
|
bounds = entry.get_bounds()
|
||||||
if bounds is None:
|
if bounds is None:
|
||||||
continue
|
continue
|
||||||
min_bounds = numpy.minimum(min_bounds, bounds[0, :])
|
min_bounds = numpy.minimum(min_bounds, bounds[0, :])
|
||||||
max_bounds = numpy.maximum(max_bounds, bounds[1, :])
|
max_bounds = numpy.maximum(max_bounds, bounds[1, :])
|
||||||
|
|
||||||
|
if self.subpatterns and (library is None):
|
||||||
|
raise PatternError('Must provide a library to get_bounds() to resolve subpatterns')
|
||||||
|
|
||||||
|
for entry in self.subpatterns:
|
||||||
|
bounds = entry.get_bounds(library=library)
|
||||||
|
if bounds is None:
|
||||||
|
continue
|
||||||
|
min_bounds = numpy.minimum(min_bounds, bounds[0, :])
|
||||||
|
max_bounds = numpy.maximum(max_bounds, bounds[1, :])
|
||||||
|
|
||||||
if (max_bounds < min_bounds).any():
|
if (max_bounds < min_bounds).any():
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
return numpy.vstack((min_bounds, max_bounds))
|
return numpy.vstack((min_bounds, max_bounds))
|
||||||
|
|
||||||
def get_bounds_nonempty(self) -> NDArray[numpy.float64]:
|
def get_bounds_nonempty(
|
||||||
|
self,
|
||||||
|
library: Optional[Mapping[str, 'Pattern']] = None,
|
||||||
|
) -> NDArray[numpy.float64]:
|
||||||
"""
|
"""
|
||||||
Convenience wrapper for `get_bounds()` which asserts that the Pattern as non-None bounds.
|
Convenience wrapper for `get_bounds()` which asserts that the Pattern as non-None bounds.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
`[[x_min, y_min], [x_max, y_max]]`
|
`[[x_min, y_min], [x_max, y_max]]`
|
||||||
"""
|
"""
|
||||||
bounds = self.get_bounds()
|
bounds = self.get_bounds(library)
|
||||||
assert(bounds is not None)
|
assert(bounds is not None)
|
||||||
return bounds
|
return bounds
|
||||||
|
|
||||||
def flatten(self: P) -> P:
|
|
||||||
"""
|
|
||||||
Removes all subpatterns and adds equivalent shapes.
|
|
||||||
Also flattens all subpatterns.
|
|
||||||
Modifies patterns in-place.
|
|
||||||
|
|
||||||
Shape/label identifiers are changed to represent their original position in the
|
|
||||||
pattern hierarchy:
|
|
||||||
`(L1_sp_index (int), L2_sp_index (int), ..., sh_index (int), *original_shape_identifier)`
|
|
||||||
where the original shape can be accessed as e.g.
|
|
||||||
`self.subpatterns[L1_sp_index].pattern.subpatterns[L2_sp_index].shapes[L1_sh_index]`
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
def flatten_single(pat: P, flattened: Set[P]) -> P:
|
|
||||||
# Update identifiers so each shape has a unique one
|
|
||||||
for ss, shape in enumerate(pat.shapes):
|
|
||||||
shape.identifier = (ss,) + shape.identifier
|
|
||||||
for ll, label in enumerate(pat.labels):
|
|
||||||
label.identifier = (ll,) + label.identifier
|
|
||||||
|
|
||||||
for pp, subpat in enumerate(pat.subpatterns):
|
|
||||||
if subpat.pattern is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if subpat.pattern not in flattened:
|
|
||||||
flatten_single(subpat.pattern, flattened)
|
|
||||||
flattened.add(subpat.pattern)
|
|
||||||
|
|
||||||
p = subpat.as_pattern()
|
|
||||||
for item in chain(p.shapes, p.labels):
|
|
||||||
item.identifier = (pp,) + item.identifier
|
|
||||||
pat.append(p)
|
|
||||||
|
|
||||||
pat.subpatterns = []
|
|
||||||
return pat
|
|
||||||
|
|
||||||
flatten_single(self, set())
|
|
||||||
return self
|
|
||||||
|
|
||||||
def wrap_repeated_shapes(
|
|
||||||
self: P,
|
|
||||||
name_func: Callable[['Pattern', Union[Shape, Label]], str] = lambda p, s: '_repetition',
|
|
||||||
recursive: bool = True,
|
|
||||||
) -> P:
|
|
||||||
"""
|
|
||||||
Wraps all shapes and labels with a non-`None` `repetition` attribute
|
|
||||||
into a `SubPattern`/`Pattern` combination, and applies the `repetition`
|
|
||||||
to each `SubPattern` instead of its contained shape.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name_func: Function f(this_pattern, shape) which generates a name for the
|
|
||||||
wrapping pattern. Default always returns '_repetition'.
|
|
||||||
recursive: If `True`, this function is also applied to all referenced patterns
|
|
||||||
recursively. Default `True`.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
def do_wrap(pat: Optional[Pattern]) -> Optional[Pattern]:
|
|
||||||
if pat is None:
|
|
||||||
return pat
|
|
||||||
|
|
||||||
new_shapes = []
|
|
||||||
for shape in pat.shapes:
|
|
||||||
if shape.repetition is None:
|
|
||||||
new_shapes.append(shape)
|
|
||||||
continue
|
|
||||||
pat.addsp(Pattern(name_func(pat, shape), shapes=[shape]), repetition=shape.repetition)
|
|
||||||
shape.repetition = None
|
|
||||||
pat.shapes = new_shapes
|
|
||||||
|
|
||||||
new_labels = []
|
|
||||||
for label in pat.labels:
|
|
||||||
if label.repetition is None:
|
|
||||||
new_labels.append(label)
|
|
||||||
continue
|
|
||||||
pat.addsp(Pattern(name_func(pat, label), labels=[label]), repetition=label.repetition)
|
|
||||||
label.repetition = None
|
|
||||||
pat.labels = new_labels
|
|
||||||
|
|
||||||
return pat
|
|
||||||
|
|
||||||
if recursive:
|
|
||||||
self.apply(do_wrap)
|
|
||||||
else:
|
|
||||||
do_wrap(self)
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
def translate_elements(self: P, offset: ArrayLike) -> P:
|
def translate_elements(self: P, offset: ArrayLike) -> P:
|
||||||
"""
|
"""
|
||||||
Translates all shapes, label, and subpatterns by the given offset.
|
Translates all shapes, label, and subpatterns by the given offset.
|
||||||
@ -872,98 +473,51 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
self.subpatterns.append(SubPattern(*args, **kwargs))
|
self.subpatterns.append(SubPattern(*args, **kwargs))
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def lock(self: P) -> P:
|
def flatten(
|
||||||
|
self: P,
|
||||||
|
library: Mapping[str, P],
|
||||||
|
) -> 'Pattern':
|
||||||
"""
|
"""
|
||||||
Lock the pattern, raising an exception if it is modified.
|
Removes all subpatterns (recursively) and adds equivalent shapes.
|
||||||
Also see `deeplock()`.
|
Alters the current pattern in-place
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
if not self.locked:
|
|
||||||
self.shapes = tuple(self.shapes)
|
|
||||||
self.labels = tuple(self.labels)
|
|
||||||
self.subpatterns = tuple(self.subpatterns)
|
|
||||||
LockableImpl.lock(self)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def unlock(self: P) -> P:
|
|
||||||
"""
|
|
||||||
Unlock the pattern
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
if self.locked:
|
|
||||||
LockableImpl.unlock(self)
|
|
||||||
self.shapes = list(self.shapes)
|
|
||||||
self.labels = list(self.labels)
|
|
||||||
self.subpatterns = list(self.subpatterns)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def deeplock(self: P) -> P:
|
|
||||||
"""
|
|
||||||
Recursively lock the pattern, all referenced shapes, subpatterns, and labels.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
self.lock()
|
|
||||||
for ss in chain(self.shapes, self.labels):
|
|
||||||
ss.lock() # type: ignore # mypy struggles with multiple inheritance :(
|
|
||||||
for sp in self.subpatterns:
|
|
||||||
sp.deeplock()
|
|
||||||
return self
|
|
||||||
|
|
||||||
def deepunlock(self: P) -> P:
|
|
||||||
"""
|
|
||||||
Recursively unlock the pattern, all referenced shapes, subpatterns, and labels.
|
|
||||||
|
|
||||||
This is dangerous unless you have just performed a deepcopy, since anything
|
|
||||||
you change will be changed everywhere it is referenced!
|
|
||||||
|
|
||||||
Return:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
self.unlock()
|
|
||||||
for ss in chain(self.shapes, self.labels):
|
|
||||||
ss.unlock() # type: ignore # mypy struggles with multiple inheritance :(
|
|
||||||
for sp in self.subpatterns:
|
|
||||||
sp.deepunlock()
|
|
||||||
return self
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def load(filename: str) -> 'Pattern':
|
|
||||||
"""
|
|
||||||
Load a Pattern from a file using pickle
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
filename: Filename to load from
|
library: Source for referenced patterns.
|
||||||
|
|
||||||
Returns:
|
|
||||||
Loaded Pattern
|
|
||||||
"""
|
|
||||||
with open(filename, 'rb') as f:
|
|
||||||
pattern = pickle.load(f)
|
|
||||||
|
|
||||||
return pattern
|
|
||||||
|
|
||||||
def save(self, filename: str) -> 'Pattern':
|
|
||||||
"""
|
|
||||||
Save the Pattern to a file using pickle
|
|
||||||
|
|
||||||
Args:
|
|
||||||
filename: Filename to save to
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
"""
|
"""
|
||||||
with open(filename, 'wb') as f:
|
flattened: Dict[Optional[str], Optional[P]] = {}
|
||||||
pickle.dump(self, f, protocol=pickle.HIGHEST_PROTOCOL)
|
|
||||||
|
def flatten_single(name: Optional[str]) -> None:
|
||||||
|
if name is None:
|
||||||
|
pat = self
|
||||||
|
else:
|
||||||
|
pat = library[name].deepcopy()
|
||||||
|
flattened[name] = None
|
||||||
|
|
||||||
|
for subpat in pat.subpatterns:
|
||||||
|
target = subpat.target
|
||||||
|
if target is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if target not in flattened:
|
||||||
|
flatten_single(target)
|
||||||
|
if flattened[target] is None:
|
||||||
|
raise PatternError(f'Circular reference in {name} to {target}')
|
||||||
|
|
||||||
|
p = subpat.as_pattern(pattern=flattened[target])
|
||||||
|
pat.append(p)
|
||||||
|
|
||||||
|
pat.subpatterns.clear()
|
||||||
|
flattened[name] = pat
|
||||||
|
|
||||||
|
flatten_single(None)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def visualize(
|
def visualize(
|
||||||
self,
|
self: P,
|
||||||
|
library: Optional[Mapping[str, P]] = None,
|
||||||
offset: ArrayLike = (0., 0.),
|
offset: ArrayLike = (0., 0.),
|
||||||
line_color: str = 'k',
|
line_color: str = 'k',
|
||||||
fill_color: str = 'none',
|
fill_color: str = 'none',
|
||||||
@ -987,6 +541,9 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
from matplotlib import pyplot # type: ignore
|
from matplotlib import pyplot # type: ignore
|
||||||
import matplotlib.collections # type: ignore
|
import matplotlib.collections # type: ignore
|
||||||
|
|
||||||
|
if self.subpatterns and library is None:
|
||||||
|
raise PatternError('Must provide a library when visualizing a pattern with subpatterns')
|
||||||
|
|
||||||
offset = numpy.array(offset, dtype=float)
|
offset = numpy.array(offset, dtype=float)
|
||||||
|
|
||||||
if not overdraw:
|
if not overdraw:
|
||||||
@ -1001,50 +558,27 @@ class Pattern(LockableImpl, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
for shape in self.shapes:
|
for shape in self.shapes:
|
||||||
polygons += [offset + s.offset + s.vertices for s in shape.to_polygons()]
|
polygons += [offset + s.offset + s.vertices for s in shape.to_polygons()]
|
||||||
|
|
||||||
mpl_poly_collection = matplotlib.collections.PolyCollection(polygons,
|
mpl_poly_collection = matplotlib.collections.PolyCollection(
|
||||||
|
polygons,
|
||||||
facecolors=fill_color,
|
facecolors=fill_color,
|
||||||
edgecolors=line_color)
|
edgecolors=line_color,
|
||||||
|
)
|
||||||
axes.add_collection(mpl_poly_collection)
|
axes.add_collection(mpl_poly_collection)
|
||||||
pyplot.axis('equal')
|
pyplot.axis('equal')
|
||||||
|
|
||||||
for subpat in self.subpatterns:
|
for subpat in self.subpatterns:
|
||||||
subpat.as_pattern().visualize(offset=offset, overdraw=True,
|
subpat.as_pattern(library=library).visualize(
|
||||||
line_color=line_color, fill_color=fill_color)
|
library=library,
|
||||||
|
offset=offset,
|
||||||
|
overdraw=True,
|
||||||
|
line_color=line_color,
|
||||||
|
fill_color=fill_color,
|
||||||
|
)
|
||||||
|
|
||||||
if not overdraw:
|
if not overdraw:
|
||||||
pyplot.xlabel('x')
|
pyplot.xlabel('x')
|
||||||
pyplot.ylabel('y')
|
pyplot.ylabel('y')
|
||||||
pyplot.show()
|
pyplot.show()
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def find_toplevel(patterns: Iterable['Pattern']) -> List['Pattern']:
|
|
||||||
"""
|
|
||||||
Given a list of Pattern objects, return those that are not referenced by
|
|
||||||
any other pattern.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
patterns: A list of patterns to filter.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A filtered list in which no pattern is referenced by any other pattern.
|
|
||||||
"""
|
|
||||||
def get_children(pat: Pattern, memo: Set) -> Set:
|
|
||||||
children = set(sp.pattern for sp in pat.subpatterns if sp.pattern is not None)
|
|
||||||
new_children = children - memo
|
|
||||||
memo |= new_children
|
|
||||||
|
|
||||||
for child_pat in new_children:
|
|
||||||
memo |= get_children(child_pat, memo)
|
|
||||||
return memo
|
|
||||||
|
|
||||||
patterns = set(patterns)
|
|
||||||
not_toplevel: Set['Pattern'] = set()
|
|
||||||
for pattern in patterns:
|
|
||||||
not_toplevel |= get_children(pattern, not_toplevel)
|
|
||||||
|
|
||||||
toplevel = list(patterns - not_toplevel)
|
|
||||||
return toplevel
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
locked = ' L' if self.locked else ''
|
return (f'<Pattern: sh{len(self.shapes)} sp{len(self.subpatterns)} la{len(self.labels)}>')
|
||||||
return (f'<Pattern "{self.name}": sh{len(self.shapes)} sp{len(self.subpatterns)} la{len(self.labels)}{locked}>')
|
|
||||||
|
@ -12,7 +12,7 @@ from numpy.typing import ArrayLike, NDArray
|
|||||||
|
|
||||||
from .error import PatternError
|
from .error import PatternError
|
||||||
from .utils import rotation_matrix_2d, AutoSlots
|
from .utils import rotation_matrix_2d, AutoSlots
|
||||||
from .traits import LockableImpl, Copyable, Scalable, Rotatable, Mirrorable
|
from .traits import Copyable, Scalable, Rotatable, Mirrorable
|
||||||
|
|
||||||
|
|
||||||
class Repetition(Copyable, Rotatable, Mirrorable, Scalable, metaclass=ABCMeta):
|
class Repetition(Copyable, Rotatable, Mirrorable, Scalable, metaclass=ABCMeta):
|
||||||
@ -30,7 +30,7 @@ class Repetition(Copyable, Rotatable, Mirrorable, Scalable, metaclass=ABCMeta):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Grid(LockableImpl, Repetition, metaclass=AutoSlots):
|
class Grid(Repetition, metaclass=AutoSlots):
|
||||||
"""
|
"""
|
||||||
`Grid` describes a 2D grid formed by two basis vectors and two 'counts' (sizes).
|
`Grid` describes a 2D grid formed by two basis vectors and two 'counts' (sizes).
|
||||||
|
|
||||||
@ -67,7 +67,6 @@ class Grid(LockableImpl, Repetition, metaclass=AutoSlots):
|
|||||||
a_count: int,
|
a_count: int,
|
||||||
b_vector: Optional[ArrayLike] = None,
|
b_vector: Optional[ArrayLike] = None,
|
||||||
b_count: Optional[int] = 1,
|
b_count: Optional[int] = 1,
|
||||||
locked: bool = False,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
@ -79,7 +78,6 @@ class Grid(LockableImpl, Repetition, metaclass=AutoSlots):
|
|||||||
Can be omitted when specifying a 1D array.
|
Can be omitted when specifying a 1D array.
|
||||||
b_count: Number of elements in the `b_vector` direction.
|
b_count: Number of elements in the `b_vector` direction.
|
||||||
Should be omitted if `b_vector` was omitted.
|
Should be omitted if `b_vector` was omitted.
|
||||||
locked: Whether the `Grid` is locked after initialization.
|
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
PatternError if `b_*` inputs conflict with each other
|
PatternError if `b_*` inputs conflict with each other
|
||||||
@ -99,12 +97,10 @@ class Grid(LockableImpl, Repetition, metaclass=AutoSlots):
|
|||||||
if b_count < 1:
|
if b_count < 1:
|
||||||
raise PatternError(f'Repetition has too-small b_count: {b_count}')
|
raise PatternError(f'Repetition has too-small b_count: {b_count}')
|
||||||
|
|
||||||
object.__setattr__(self, 'locked', False)
|
|
||||||
self.a_vector = a_vector # type: ignore # setter handles type conversion
|
self.a_vector = a_vector # type: ignore # setter handles type conversion
|
||||||
self.b_vector = b_vector # type: ignore # setter handles type conversion
|
self.b_vector = b_vector # type: ignore # setter handles type conversion
|
||||||
self.a_count = a_count
|
self.a_count = a_count
|
||||||
self.b_count = b_count
|
self.b_count = b_count
|
||||||
self.locked = locked
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def aligned(
|
def aligned(
|
||||||
@ -129,18 +125,17 @@ class Grid(LockableImpl, Repetition, metaclass=AutoSlots):
|
|||||||
return cls(a_vector=(x, 0), b_vector=(0, y), a_count=x_count, b_count=y_count)
|
return cls(a_vector=(x, 0), b_vector=(0, y), a_count=x_count, b_count=y_count)
|
||||||
|
|
||||||
def __copy__(self) -> 'Grid':
|
def __copy__(self) -> 'Grid':
|
||||||
new = Grid(a_vector=self.a_vector.copy(),
|
new = Grid(
|
||||||
|
a_vector=self.a_vector.copy(),
|
||||||
b_vector=copy.copy(self.b_vector),
|
b_vector=copy.copy(self.b_vector),
|
||||||
a_count=self.a_count,
|
a_count=self.a_count,
|
||||||
b_count=self.b_count,
|
b_count=self.b_count,
|
||||||
locked=self.locked)
|
)
|
||||||
return new
|
return new
|
||||||
|
|
||||||
def __deepcopy__(self, memo: Dict = None) -> 'Grid':
|
def __deepcopy__(self, memo: Dict = None) -> 'Grid':
|
||||||
memo = {} if memo is None else memo
|
memo = {} if memo is None else memo
|
||||||
new = copy.copy(self)
|
new = copy.copy(self)
|
||||||
LocakbleImpl.unlock(new)
|
|
||||||
new.locked = self.locked
|
|
||||||
return new
|
return new
|
||||||
|
|
||||||
# a_vector property
|
# a_vector property
|
||||||
@ -264,36 +259,9 @@ class Grid(LockableImpl, Repetition, metaclass=AutoSlots):
|
|||||||
self.b_vector *= c
|
self.b_vector *= c
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def lock(self) -> 'Grid':
|
|
||||||
"""
|
|
||||||
Lock the `Grid`, disallowing changes.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
self.a_vector.flags.writeable = False
|
|
||||||
if self.b_vector is not None:
|
|
||||||
self.b_vector.flags.writeable = False
|
|
||||||
LockableImpl.lock(self)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def unlock(self) -> 'Grid':
|
|
||||||
"""
|
|
||||||
Unlock the `Grid`
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
self.a_vector.flags.writeable = True
|
|
||||||
if self.b_vector is not None:
|
|
||||||
self.b_vector.flags.writeable = True
|
|
||||||
LockableImpl.unlock(self)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
locked = ' L' if self.locked else ''
|
|
||||||
bv = f', {self.b_vector}' if self.b_vector is not None else ''
|
bv = f', {self.b_vector}' if self.b_vector is not None else ''
|
||||||
return (f'<Grid {self.a_count}x{self.b_count} ({self.a_vector}{bv}){locked}>')
|
return (f'<Grid {self.a_count}x{self.b_count} ({self.a_vector}{bv})>')
|
||||||
|
|
||||||
def __eq__(self, other: Any) -> bool:
|
def __eq__(self, other: Any) -> bool:
|
||||||
if not isinstance(other, type(self)):
|
if not isinstance(other, type(self)):
|
||||||
@ -308,12 +276,10 @@ class Grid(LockableImpl, Repetition, metaclass=AutoSlots):
|
|||||||
return False
|
return False
|
||||||
if any(self.b_vector[ii] != other.b_vector[ii] for ii in range(2)):
|
if any(self.b_vector[ii] != other.b_vector[ii] for ii in range(2)):
|
||||||
return False
|
return False
|
||||||
if self.locked != other.locked:
|
|
||||||
return False
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class Arbitrary(LockableImpl, Repetition, metaclass=AutoSlots):
|
class Arbitrary(Repetition, metaclass=AutoSlots):
|
||||||
"""
|
"""
|
||||||
`Arbitrary` is a simple list of (absolute) displacements for instances.
|
`Arbitrary` is a simple list of (absolute) displacements for instances.
|
||||||
|
|
||||||
@ -342,48 +308,19 @@ class Arbitrary(LockableImpl, Repetition, metaclass=AutoSlots):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
displacements: ArrayLike,
|
displacements: ArrayLike,
|
||||||
locked: bool = False,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
displacements: List of vectors (Nx2 ndarray) specifying displacements.
|
displacements: List of vectors (Nx2 ndarray) specifying displacements.
|
||||||
locked: Whether the object is locked after initialization.
|
|
||||||
"""
|
"""
|
||||||
object.__setattr__(self, 'locked', False)
|
|
||||||
self.displacements = displacements
|
self.displacements = displacements
|
||||||
self.locked = locked
|
|
||||||
|
|
||||||
def lock(self) -> 'Arbitrary':
|
|
||||||
"""
|
|
||||||
Lock the object, disallowing changes.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
self._displacements.flags.writeable = False
|
|
||||||
LockableImpl.lock(self)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def unlock(self) -> 'Arbitrary':
|
|
||||||
"""
|
|
||||||
Unlock the object
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
self._displacements.flags.writeable = True
|
|
||||||
LockableImpl.unlock(self)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
locked = ' L' if self.locked else ''
|
return (f'<Arbitrary {len(self.displacements)}pts >')
|
||||||
return (f'<Arbitrary {len(self.displacements)}pts {locked}>')
|
|
||||||
|
|
||||||
def __eq__(self, other: Any) -> bool:
|
def __eq__(self, other: Any) -> bool:
|
||||||
if not isinstance(other, type(self)):
|
if not isinstance(other, type(self)):
|
||||||
return False
|
return False
|
||||||
if self.locked != other.locked:
|
|
||||||
return False
|
|
||||||
return numpy.array_equal(self.displacements, other.displacements)
|
return numpy.array_equal(self.displacements, other.displacements)
|
||||||
|
|
||||||
def rotate(self, rotation: float) -> 'Arbitrary':
|
def rotate(self, rotation: float) -> 'Arbitrary':
|
||||||
|
@ -10,7 +10,6 @@ from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS
|
|||||||
from .. import PatternError
|
from .. import PatternError
|
||||||
from ..repetition import Repetition
|
from ..repetition import Repetition
|
||||||
from ..utils import is_scalar, layer_t, AutoSlots, annotations_t
|
from ..utils import is_scalar, layer_t, AutoSlots, annotations_t
|
||||||
from ..traits import LockableImpl
|
|
||||||
|
|
||||||
|
|
||||||
class Arc(Shape, metaclass=AutoSlots):
|
class Arc(Shape, metaclass=AutoSlots):
|
||||||
@ -166,10 +165,8 @@ class Arc(Shape, metaclass=AutoSlots):
|
|||||||
dose: float = 1.0,
|
dose: float = 1.0,
|
||||||
repetition: Optional[Repetition] = None,
|
repetition: Optional[Repetition] = None,
|
||||||
annotations: Optional[annotations_t] = None,
|
annotations: Optional[annotations_t] = None,
|
||||||
locked: bool = False,
|
|
||||||
raw: bool = False,
|
raw: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
LockableImpl.unlock(self)
|
|
||||||
self.identifier = ()
|
self.identifier = ()
|
||||||
if raw:
|
if raw:
|
||||||
assert(isinstance(radii, numpy.ndarray))
|
assert(isinstance(radii, numpy.ndarray))
|
||||||
@ -197,17 +194,14 @@ class Arc(Shape, metaclass=AutoSlots):
|
|||||||
self.poly_num_points = poly_num_points
|
self.poly_num_points = poly_num_points
|
||||||
self.poly_max_arclen = poly_max_arclen
|
self.poly_max_arclen = poly_max_arclen
|
||||||
[self.mirror(a) for a, do in enumerate(mirrored) if do]
|
[self.mirror(a) for a, do in enumerate(mirrored) if do]
|
||||||
self.set_locked(locked)
|
|
||||||
|
|
||||||
def __deepcopy__(self, memo: Dict = None) -> 'Arc':
|
def __deepcopy__(self, memo: Dict = None) -> 'Arc':
|
||||||
memo = {} if memo is None else memo
|
memo = {} if memo is None else memo
|
||||||
new = copy.copy(self)
|
new = copy.copy(self)
|
||||||
Shape.unlock(new)
|
|
||||||
new._offset = self._offset.copy()
|
new._offset = self._offset.copy()
|
||||||
new._radii = self._radii.copy()
|
new._radii = self._radii.copy()
|
||||||
new._angles = self._angles.copy()
|
new._angles = self._angles.copy()
|
||||||
new._annotations = copy.deepcopy(self._annotations)
|
new._annotations = copy.deepcopy(self._annotations)
|
||||||
new.set_locked(self.locked)
|
|
||||||
return new
|
return new
|
||||||
|
|
||||||
def to_polygons(
|
def to_polygons(
|
||||||
@ -429,21 +423,8 @@ class Arc(Shape, metaclass=AutoSlots):
|
|||||||
a.append((a0, a1))
|
a.append((a0, a1))
|
||||||
return numpy.array(a)
|
return numpy.array(a)
|
||||||
|
|
||||||
def lock(self) -> 'Arc':
|
|
||||||
self.radii.flags.writeable = False
|
|
||||||
self.angles.flags.writeable = False
|
|
||||||
Shape.lock(self)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def unlock(self) -> 'Arc':
|
|
||||||
Shape.unlock(self)
|
|
||||||
self.radii.flags.writeable = True
|
|
||||||
self.angles.flags.writeable = True
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
angles = f' a°{numpy.rad2deg(self.angles)}'
|
angles = f' a°{numpy.rad2deg(self.angles)}'
|
||||||
rotation = f' r°{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else ''
|
rotation = f' r°{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else ''
|
||||||
dose = f' d{self.dose:g}' if self.dose != 1 else ''
|
dose = f' d{self.dose:g}' if self.dose != 1 else ''
|
||||||
locked = ' L' if self.locked else ''
|
return f'<Arc l{self.layer} o{self.offset} r{self.radii}{angles} w{self.width:g}{rotation}{dose}>'
|
||||||
return f'<Arc l{self.layer} o{self.offset} r{self.radii}{angles} w{self.width:g}{rotation}{dose}{locked}>'
|
|
||||||
|
@ -9,7 +9,6 @@ from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS
|
|||||||
from .. import PatternError
|
from .. import PatternError
|
||||||
from ..repetition import Repetition
|
from ..repetition import Repetition
|
||||||
from ..utils import is_scalar, layer_t, AutoSlots, annotations_t
|
from ..utils import is_scalar, layer_t, AutoSlots, annotations_t
|
||||||
from ..traits import LockableImpl
|
|
||||||
|
|
||||||
|
|
||||||
class Circle(Shape, metaclass=AutoSlots):
|
class Circle(Shape, metaclass=AutoSlots):
|
||||||
@ -54,10 +53,8 @@ class Circle(Shape, metaclass=AutoSlots):
|
|||||||
dose: float = 1.0,
|
dose: float = 1.0,
|
||||||
repetition: Optional[Repetition] = None,
|
repetition: Optional[Repetition] = None,
|
||||||
annotations: Optional[annotations_t] = None,
|
annotations: Optional[annotations_t] = None,
|
||||||
locked: bool = False,
|
|
||||||
raw: bool = False,
|
raw: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
LockableImpl.unlock(self)
|
|
||||||
self.identifier = ()
|
self.identifier = ()
|
||||||
if raw:
|
if raw:
|
||||||
assert(isinstance(offset, numpy.ndarray))
|
assert(isinstance(offset, numpy.ndarray))
|
||||||
@ -76,15 +73,12 @@ class Circle(Shape, metaclass=AutoSlots):
|
|||||||
self.dose = dose
|
self.dose = dose
|
||||||
self.poly_num_points = poly_num_points
|
self.poly_num_points = poly_num_points
|
||||||
self.poly_max_arclen = poly_max_arclen
|
self.poly_max_arclen = poly_max_arclen
|
||||||
self.set_locked(locked)
|
|
||||||
|
|
||||||
def __deepcopy__(self, memo: Dict = None) -> 'Circle':
|
def __deepcopy__(self, memo: Dict = None) -> 'Circle':
|
||||||
memo = {} if memo is None else memo
|
memo = {} if memo is None else memo
|
||||||
new = copy.copy(self)
|
new = copy.copy(self)
|
||||||
Shape.unlock(new)
|
|
||||||
new._offset = self._offset.copy()
|
new._offset = self._offset.copy()
|
||||||
new._annotations = copy.deepcopy(self._annotations)
|
new._annotations = copy.deepcopy(self._annotations)
|
||||||
new.set_locked(self.locked)
|
|
||||||
return new
|
return new
|
||||||
|
|
||||||
def to_polygons(
|
def to_polygons(
|
||||||
@ -138,5 +132,4 @@ class Circle(Shape, metaclass=AutoSlots):
|
|||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
dose = f' d{self.dose:g}' if self.dose != 1 else ''
|
dose = f' d{self.dose:g}' if self.dose != 1 else ''
|
||||||
locked = ' L' if self.locked else ''
|
return f'<Circle l{self.layer} o{self.offset} r{self.radius:g}{dose}>'
|
||||||
return f'<Circle l{self.layer} o{self.offset} r{self.radius:g}{dose}{locked}>'
|
|
||||||
|
@ -10,7 +10,6 @@ from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS
|
|||||||
from .. import PatternError
|
from .. import PatternError
|
||||||
from ..repetition import Repetition
|
from ..repetition import Repetition
|
||||||
from ..utils import is_scalar, rotation_matrix_2d, layer_t, AutoSlots, annotations_t
|
from ..utils import is_scalar, rotation_matrix_2d, layer_t, AutoSlots, annotations_t
|
||||||
from ..traits import LockableImpl
|
|
||||||
|
|
||||||
|
|
||||||
class Ellipse(Shape, metaclass=AutoSlots):
|
class Ellipse(Shape, metaclass=AutoSlots):
|
||||||
@ -101,10 +100,8 @@ class Ellipse(Shape, metaclass=AutoSlots):
|
|||||||
dose: float = 1.0,
|
dose: float = 1.0,
|
||||||
repetition: Optional[Repetition] = None,
|
repetition: Optional[Repetition] = None,
|
||||||
annotations: Optional[annotations_t] = None,
|
annotations: Optional[annotations_t] = None,
|
||||||
locked: bool = False,
|
|
||||||
raw: bool = False,
|
raw: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
LockableImpl.unlock(self)
|
|
||||||
self.identifier = ()
|
self.identifier = ()
|
||||||
if raw:
|
if raw:
|
||||||
assert(isinstance(radii, numpy.ndarray))
|
assert(isinstance(radii, numpy.ndarray))
|
||||||
@ -127,16 +124,13 @@ class Ellipse(Shape, metaclass=AutoSlots):
|
|||||||
[self.mirror(a) for a, do in enumerate(mirrored) if do]
|
[self.mirror(a) for a, do in enumerate(mirrored) if do]
|
||||||
self.poly_num_points = poly_num_points
|
self.poly_num_points = poly_num_points
|
||||||
self.poly_max_arclen = poly_max_arclen
|
self.poly_max_arclen = poly_max_arclen
|
||||||
self.set_locked(locked)
|
|
||||||
|
|
||||||
def __deepcopy__(self, memo: Dict = None) -> 'Ellipse':
|
def __deepcopy__(self, memo: Dict = None) -> 'Ellipse':
|
||||||
memo = {} if memo is None else memo
|
memo = {} if memo is None else memo
|
||||||
new = copy.copy(self)
|
new = copy.copy(self)
|
||||||
Shape.unlock(new)
|
|
||||||
new._offset = self._offset.copy()
|
new._offset = self._offset.copy()
|
||||||
new._radii = self._radii.copy()
|
new._radii = self._radii.copy()
|
||||||
new._annotations = copy.deepcopy(self._annotations)
|
new._annotations = copy.deepcopy(self._annotations)
|
||||||
new.set_locked(self.locked)
|
|
||||||
return new
|
return new
|
||||||
|
|
||||||
def to_polygons(
|
def to_polygons(
|
||||||
@ -209,18 +203,7 @@ class Ellipse(Shape, metaclass=AutoSlots):
|
|||||||
(self.offset, scale / norm_value, angle, False, self.dose),
|
(self.offset, scale / norm_value, angle, False, self.dose),
|
||||||
lambda: Ellipse(radii=radii * norm_value, layer=self.layer))
|
lambda: Ellipse(radii=radii * norm_value, layer=self.layer))
|
||||||
|
|
||||||
def lock(self) -> 'Ellipse':
|
|
||||||
self.radii.flags.writeable = False
|
|
||||||
Shape.lock(self)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def unlock(self) -> 'Ellipse':
|
|
||||||
Shape.unlock(self)
|
|
||||||
self.radii.flags.writeable = True
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
rotation = f' r{self.rotation*180/pi:g}' if self.rotation != 0 else ''
|
rotation = f' r{self.rotation*180/pi:g}' if self.rotation != 0 else ''
|
||||||
dose = f' d{self.dose:g}' if self.dose != 1 else ''
|
dose = f' d{self.dose:g}' if self.dose != 1 else ''
|
||||||
locked = ' L' if self.locked else ''
|
return f'<Ellipse l{self.layer} o{self.offset} r{self.radii}{rotation}{dose}>'
|
||||||
return f'<Ellipse l{self.layer} o{self.offset} r{self.radii}{rotation}{dose}{locked}>'
|
|
||||||
|
@ -11,7 +11,6 @@ from .. import PatternError
|
|||||||
from ..repetition import Repetition
|
from ..repetition import Repetition
|
||||||
from ..utils import is_scalar, rotation_matrix_2d, layer_t, AutoSlots
|
from ..utils import is_scalar, rotation_matrix_2d, layer_t, AutoSlots
|
||||||
from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t
|
from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t
|
||||||
from ..traits import LockableImpl
|
|
||||||
|
|
||||||
|
|
||||||
class PathCap(Enum):
|
class PathCap(Enum):
|
||||||
@ -155,10 +154,8 @@ class Path(Shape, metaclass=AutoSlots):
|
|||||||
dose: float = 1.0,
|
dose: float = 1.0,
|
||||||
repetition: Optional[Repetition] = None,
|
repetition: Optional[Repetition] = None,
|
||||||
annotations: Optional[annotations_t] = None,
|
annotations: Optional[annotations_t] = None,
|
||||||
locked: bool = False,
|
|
||||||
raw: bool = False,
|
raw: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
LockableImpl.unlock(self)
|
|
||||||
self._cap_extensions = None # Since .cap setter might access it
|
self._cap_extensions = None # Since .cap setter might access it
|
||||||
|
|
||||||
self.identifier = ()
|
self.identifier = ()
|
||||||
@ -187,18 +184,15 @@ class Path(Shape, metaclass=AutoSlots):
|
|||||||
self.cap_extensions = cap_extensions
|
self.cap_extensions = cap_extensions
|
||||||
self.rotate(rotation)
|
self.rotate(rotation)
|
||||||
[self.mirror(a) for a, do in enumerate(mirrored) if do]
|
[self.mirror(a) for a, do in enumerate(mirrored) if do]
|
||||||
self.set_locked(locked)
|
|
||||||
|
|
||||||
def __deepcopy__(self, memo: Dict = None) -> 'Path':
|
def __deepcopy__(self, memo: Dict = None) -> 'Path':
|
||||||
memo = {} if memo is None else memo
|
memo = {} if memo is None else memo
|
||||||
new = copy.copy(self)
|
new = copy.copy(self)
|
||||||
Shape.unlock(new)
|
|
||||||
new._offset = self._offset.copy()
|
new._offset = self._offset.copy()
|
||||||
new._vertices = self._vertices.copy()
|
new._vertices = self._vertices.copy()
|
||||||
new._cap = copy.deepcopy(self._cap, memo)
|
new._cap = copy.deepcopy(self._cap, memo)
|
||||||
new._cap_extensions = copy.deepcopy(self._cap_extensions, memo)
|
new._cap_extensions = copy.deepcopy(self._cap_extensions, memo)
|
||||||
new._annotations = copy.deepcopy(self._annotations)
|
new._annotations = copy.deepcopy(self._annotations)
|
||||||
new.set_locked(self.locked)
|
|
||||||
return new
|
return new
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -424,22 +418,7 @@ class Path(Shape, metaclass=AutoSlots):
|
|||||||
extensions = numpy.zeros(2)
|
extensions = numpy.zeros(2)
|
||||||
return extensions
|
return extensions
|
||||||
|
|
||||||
def lock(self) -> 'Path':
|
|
||||||
self.vertices.flags.writeable = False
|
|
||||||
if self.cap_extensions is not None:
|
|
||||||
self.cap_extensions.flags.writeable = False
|
|
||||||
Shape.lock(self)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def unlock(self) -> 'Path':
|
|
||||||
Shape.unlock(self)
|
|
||||||
self.vertices.flags.writeable = True
|
|
||||||
if self.cap_extensions is not None:
|
|
||||||
self.cap_extensions.flags.writeable = True
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
centroid = self.offset + self.vertices.mean(axis=0)
|
centroid = self.offset + self.vertices.mean(axis=0)
|
||||||
dose = f' d{self.dose:g}' if self.dose != 1 else ''
|
dose = f' d{self.dose:g}' if self.dose != 1 else ''
|
||||||
locked = ' L' if self.locked else ''
|
return f'<Path l{self.layer} centroid {centroid} v{len(self.vertices)} w{self.width} c{self.cap}{dose}>'
|
||||||
return f'<Path l{self.layer} centroid {centroid} v{len(self.vertices)} w{self.width} c{self.cap}{dose}{locked}>'
|
|
||||||
|
@ -10,7 +10,6 @@ from .. import PatternError
|
|||||||
from ..repetition import Repetition
|
from ..repetition import Repetition
|
||||||
from ..utils import is_scalar, rotation_matrix_2d, layer_t, AutoSlots
|
from ..utils import is_scalar, rotation_matrix_2d, layer_t, AutoSlots
|
||||||
from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t
|
from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t
|
||||||
from ..traits import LockableImpl
|
|
||||||
|
|
||||||
|
|
||||||
class Polygon(Shape, metaclass=AutoSlots):
|
class Polygon(Shape, metaclass=AutoSlots):
|
||||||
@ -83,10 +82,8 @@ class Polygon(Shape, metaclass=AutoSlots):
|
|||||||
dose: float = 1.0,
|
dose: float = 1.0,
|
||||||
repetition: Optional[Repetition] = None,
|
repetition: Optional[Repetition] = None,
|
||||||
annotations: Optional[annotations_t] = None,
|
annotations: Optional[annotations_t] = None,
|
||||||
locked: bool = False,
|
|
||||||
raw: bool = False,
|
raw: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
LockableImpl.unlock(self)
|
|
||||||
self.identifier = ()
|
self.identifier = ()
|
||||||
if raw:
|
if raw:
|
||||||
assert(isinstance(vertices, numpy.ndarray))
|
assert(isinstance(vertices, numpy.ndarray))
|
||||||
@ -106,16 +103,13 @@ class Polygon(Shape, metaclass=AutoSlots):
|
|||||||
self.dose = dose
|
self.dose = dose
|
||||||
self.rotate(rotation)
|
self.rotate(rotation)
|
||||||
[self.mirror(a) for a, do in enumerate(mirrored) if do]
|
[self.mirror(a) for a, do in enumerate(mirrored) if do]
|
||||||
self.set_locked(locked)
|
|
||||||
|
|
||||||
def __deepcopy__(self, memo: Optional[Dict] = None) -> 'Polygon':
|
def __deepcopy__(self, memo: Optional[Dict] = None) -> 'Polygon':
|
||||||
memo = {} if memo is None else memo
|
memo = {} if memo is None else memo
|
||||||
new = copy.copy(self)
|
new = copy.copy(self)
|
||||||
Shape.unlock(new)
|
|
||||||
new._offset = self._offset.copy()
|
new._offset = self._offset.copy()
|
||||||
new._vertices = self._vertices.copy()
|
new._vertices = self._vertices.copy()
|
||||||
new._annotations = copy.deepcopy(self._annotations)
|
new._annotations = copy.deepcopy(self._annotations)
|
||||||
new.set_locked(self.locked)
|
|
||||||
return new
|
return new
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -430,18 +424,7 @@ class Polygon(Shape, metaclass=AutoSlots):
|
|||||||
self.vertices = remove_colinear_vertices(self.vertices, closed_path=True)
|
self.vertices = remove_colinear_vertices(self.vertices, closed_path=True)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def lock(self) -> 'Polygon':
|
|
||||||
self.vertices.flags.writeable = False
|
|
||||||
Shape.lock(self)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def unlock(self) -> 'Polygon':
|
|
||||||
Shape.unlock(self)
|
|
||||||
self.vertices.flags.writeable = True
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
centroid = self.offset + self.vertices.mean(axis=0)
|
centroid = self.offset + self.vertices.mean(axis=0)
|
||||||
dose = f' d{self.dose:g}' if self.dose != 1 else ''
|
dose = f' d{self.dose:g}' if self.dose != 1 else ''
|
||||||
locked = ' L' if self.locked else ''
|
return f'<Polygon l{self.layer} centroid {centroid} v{len(self.vertices)}{dose}>'
|
||||||
return f'<Polygon l{self.layer} centroid {centroid} v{len(self.vertices)}{dose}{locked}>'
|
|
||||||
|
@ -4,10 +4,11 @@ from abc import ABCMeta, abstractmethod
|
|||||||
import numpy
|
import numpy
|
||||||
from numpy.typing import NDArray, ArrayLike
|
from numpy.typing import NDArray, ArrayLike
|
||||||
|
|
||||||
from ..traits import (PositionableImpl, LayerableImpl, DoseableImpl,
|
from ..traits import (
|
||||||
|
PositionableImpl, LayerableImpl, DoseableImpl,
|
||||||
Rotatable, Mirrorable, Copyable, Scalable,
|
Rotatable, Mirrorable, Copyable, Scalable,
|
||||||
PivotableImpl, LockableImpl, RepeatableImpl,
|
PivotableImpl, RepeatableImpl, AnnotatableImpl,
|
||||||
AnnotatableImpl)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from . import Polygon
|
from . import Polygon
|
||||||
@ -27,7 +28,7 @@ T = TypeVar('T', bound='Shape')
|
|||||||
|
|
||||||
|
|
||||||
class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable, Copyable, Scalable,
|
class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable, Copyable, Scalable,
|
||||||
PivotableImpl, RepeatableImpl, LockableImpl, AnnotatableImpl, metaclass=ABCMeta):
|
PivotableImpl, RepeatableImpl, AnnotatableImpl, metaclass=ABCMeta):
|
||||||
"""
|
"""
|
||||||
Abstract class specifying functions common to all shapes.
|
Abstract class specifying functions common to all shapes.
|
||||||
"""
|
"""
|
||||||
@ -303,13 +304,3 @@ class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable
|
|||||||
dose=self.dose))
|
dose=self.dose))
|
||||||
|
|
||||||
return manhattan_polygons
|
return manhattan_polygons
|
||||||
|
|
||||||
def lock(self: T) -> T:
|
|
||||||
PositionableImpl._lock(self)
|
|
||||||
LockableImpl.lock(self)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def unlock(self: T) -> T:
|
|
||||||
LockableImpl.unlock(self)
|
|
||||||
PositionableImpl._unlock(self)
|
|
||||||
return self
|
|
||||||
|
@ -11,7 +11,6 @@ from ..repetition import Repetition
|
|||||||
from ..traits import RotatableImpl
|
from ..traits import RotatableImpl
|
||||||
from ..utils import is_scalar, get_bit, normalize_mirror, layer_t, AutoSlots
|
from ..utils import is_scalar, get_bit, normalize_mirror, layer_t, AutoSlots
|
||||||
from ..utils import annotations_t
|
from ..utils import annotations_t
|
||||||
from ..traits import LockableImpl
|
|
||||||
|
|
||||||
# Loaded on use:
|
# Loaded on use:
|
||||||
# from freetype import Face
|
# from freetype import Face
|
||||||
@ -74,10 +73,8 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots):
|
|||||||
dose: float = 1.0,
|
dose: float = 1.0,
|
||||||
repetition: Optional[Repetition] = None,
|
repetition: Optional[Repetition] = None,
|
||||||
annotations: Optional[annotations_t] = None,
|
annotations: Optional[annotations_t] = None,
|
||||||
locked: bool = False,
|
|
||||||
raw: bool = False,
|
raw: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
LockableImpl.unlock(self)
|
|
||||||
self.identifier = ()
|
self.identifier = ()
|
||||||
if raw:
|
if raw:
|
||||||
assert(isinstance(offset, numpy.ndarray))
|
assert(isinstance(offset, numpy.ndarray))
|
||||||
@ -102,16 +99,13 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots):
|
|||||||
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.font_path = font_path
|
self.font_path = font_path
|
||||||
self.set_locked(locked)
|
|
||||||
|
|
||||||
def __deepcopy__(self, memo: Dict = None) -> 'Text':
|
def __deepcopy__(self, memo: Dict = None) -> 'Text':
|
||||||
memo = {} if memo is None else memo
|
memo = {} if memo is None else memo
|
||||||
new = copy.copy(self)
|
new = copy.copy(self)
|
||||||
Shape.unlock(new)
|
|
||||||
new._offset = self._offset.copy()
|
new._offset = self._offset.copy()
|
||||||
new._mirrored = copy.deepcopy(self._mirrored, memo)
|
new._mirrored = copy.deepcopy(self._mirrored, memo)
|
||||||
new._annotations = copy.deepcopy(self._annotations)
|
new._annotations = copy.deepcopy(self._annotations)
|
||||||
new.set_locked(self.locked)
|
|
||||||
return new
|
return new
|
||||||
|
|
||||||
def to_polygons(
|
def to_polygons(
|
||||||
@ -259,19 +253,8 @@ def get_char_as_polygons(
|
|||||||
|
|
||||||
return polygons, advance
|
return polygons, advance
|
||||||
|
|
||||||
def lock(self) -> 'Text':
|
|
||||||
self.mirrored.flags.writeable = False
|
|
||||||
Shape.lock(self)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def unlock(self) -> 'Text':
|
|
||||||
Shape.unlock(self)
|
|
||||||
self.mirrored.flags.writeable = True
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
rotation = f' r°{self.rotation*180/pi:g}' if self.rotation != 0 else ''
|
rotation = f' r°{self.rotation*180/pi:g}' if self.rotation != 0 else ''
|
||||||
dose = f' d{self.dose:g}' if self.dose != 1 else ''
|
dose = f' d{self.dose:g}' if self.dose != 1 else ''
|
||||||
locked = ' L' if self.locked else ''
|
|
||||||
mirrored = ' m{:d}{:d}'.format(*self.mirrored) if self.mirrored.any() else ''
|
mirrored = ' m{:d}{:d}'.format(*self.mirrored) if self.mirrored.any() else ''
|
||||||
return f'<TextShape "{self.string}" l{self.layer} o{self.offset} h{self.height:g}{rotation}{mirrored}{dose}{locked}>'
|
return f'<TextShape "{self.string}" l{self.layer} o{self.offset} h{self.height:g}{rotation}{mirrored}{dose}>'
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
"""
|
"""
|
||||||
#TODO more top-level documentation
|
#TODO more top-level documentation
|
||||||
|
|
||||||
from typing import Dict, Tuple, Optional, Sequence, TYPE_CHECKING, Any, TypeVar
|
from typing import Dict, Tuple, Optional, Sequence, Mapping, TYPE_CHECKING, Any, TypeVar
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
@ -14,9 +14,10 @@ from numpy.typing import NDArray, ArrayLike
|
|||||||
from .error import PatternError
|
from .error import PatternError
|
||||||
from .utils import is_scalar, AutoSlots, annotations_t
|
from .utils import is_scalar, AutoSlots, annotations_t
|
||||||
from .repetition import Repetition
|
from .repetition import Repetition
|
||||||
from .traits import (PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl,
|
from .traits import (
|
||||||
Mirrorable, PivotableImpl, Copyable, LockableImpl, RepeatableImpl,
|
PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl,
|
||||||
AnnotatableImpl)
|
Mirrorable, PivotableImpl, Copyable, RepeatableImpl, AnnotatableImpl,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@ -27,19 +28,16 @@ S = TypeVar('S', bound='SubPattern')
|
|||||||
|
|
||||||
|
|
||||||
class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mirrorable,
|
class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mirrorable,
|
||||||
PivotableImpl, Copyable, RepeatableImpl, LockableImpl, AnnotatableImpl,
|
PivotableImpl, Copyable, RepeatableImpl, AnnotatableImpl,
|
||||||
metaclass=AutoSlots):
|
metaclass=AutoSlots):
|
||||||
"""
|
"""
|
||||||
SubPattern provides basic support for nesting Pattern objects within each other, by adding
|
SubPattern provides basic support for nesting Pattern objects within each other, by adding
|
||||||
offset, rotation, scaling, and associated methods.
|
offset, rotation, scaling, and associated methods.
|
||||||
"""
|
"""
|
||||||
__slots__ = ('_pattern',
|
__slots__ = ('_target', '_mirrored', 'identifier')
|
||||||
'_mirrored',
|
|
||||||
'identifier',
|
|
||||||
)
|
|
||||||
|
|
||||||
_pattern: Optional['Pattern']
|
_target: Optional[str]
|
||||||
""" The `Pattern` being instanced """
|
""" The name of the `Pattern` being instanced """
|
||||||
|
|
||||||
_mirrored: NDArray[numpy.bool_]
|
_mirrored: NDArray[numpy.bool_]
|
||||||
""" Whether to mirror the instance across the x and/or y axes. """
|
""" Whether to mirror the instance across the x and/or y axes. """
|
||||||
@ -49,7 +47,7 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
pattern: Optional['Pattern'],
|
target: Optional[str],
|
||||||
*,
|
*,
|
||||||
offset: ArrayLike = (0.0, 0.0),
|
offset: ArrayLike = (0.0, 0.0),
|
||||||
rotation: float = 0.0,
|
rotation: float = 0.0,
|
||||||
@ -58,24 +56,21 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
|
|||||||
scale: float = 1.0,
|
scale: float = 1.0,
|
||||||
repetition: Optional[Repetition] = None,
|
repetition: Optional[Repetition] = None,
|
||||||
annotations: Optional[annotations_t] = None,
|
annotations: Optional[annotations_t] = None,
|
||||||
locked: bool = False,
|
|
||||||
identifier: Tuple[Any, ...] = (),
|
identifier: Tuple[Any, ...] = (),
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
pattern: Pattern to reference.
|
target: Name of the Pattern to reference.
|
||||||
offset: (x, y) offset applied to the referenced pattern. Not affected by rotation etc.
|
offset: (x, y) offset applied to the referenced pattern. Not affected by rotation etc.
|
||||||
rotation: Rotation (radians, counterclockwise) relative to the referenced pattern's (0, 0).
|
rotation: Rotation (radians, counterclockwise) relative to the referenced pattern's (0, 0).
|
||||||
mirrored: Whether to mirror the referenced pattern across its x and y axes.
|
mirrored: Whether to mirror the referenced pattern across its x and y axes.
|
||||||
dose: Scaling factor applied to the dose.
|
dose: Scaling factor applied to the dose.
|
||||||
scale: Scaling factor applied to the pattern's geometry.
|
scale: Scaling factor applied to the pattern's geometry.
|
||||||
repetition: TODO
|
repetition: `Repetition` object, default `None`
|
||||||
locked: Whether the `SubPattern` is locked after initialization.
|
|
||||||
identifier: Arbitrary tuple, used internally by some `masque` functions.
|
identifier: Arbitrary tuple, used internally by some `masque` functions.
|
||||||
"""
|
"""
|
||||||
LockableImpl.unlock(self)
|
|
||||||
self.identifier = identifier
|
self.identifier = identifier
|
||||||
self.pattern = pattern
|
self.target = target
|
||||||
self.offset = offset
|
self.offset = offset
|
||||||
self.rotation = rotation
|
self.rotation = rotation
|
||||||
self.dose = dose
|
self.dose = dose
|
||||||
@ -85,10 +80,10 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
|
|||||||
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)
|
|
||||||
|
|
||||||
def __copy__(self) -> 'SubPattern':
|
def __copy__(self) -> 'SubPattern':
|
||||||
new = SubPattern(pattern=self.pattern,
|
new = SubPattern(
|
||||||
|
target=self.target,
|
||||||
offset=self.offset.copy(),
|
offset=self.offset.copy(),
|
||||||
rotation=self.rotation,
|
rotation=self.rotation,
|
||||||
dose=self.dose,
|
dose=self.dose,
|
||||||
@ -96,30 +91,26 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
|
|||||||
mirrored=self.mirrored.copy(),
|
mirrored=self.mirrored.copy(),
|
||||||
repetition=copy.deepcopy(self.repetition),
|
repetition=copy.deepcopy(self.repetition),
|
||||||
annotations=copy.deepcopy(self.annotations),
|
annotations=copy.deepcopy(self.annotations),
|
||||||
locked=self.locked)
|
)
|
||||||
return new
|
return new
|
||||||
|
|
||||||
def __deepcopy__(self, memo: Dict = None) -> 'SubPattern':
|
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)
|
new = copy.copy(self)
|
||||||
LockableImpl.unlock(new)
|
|
||||||
new.pattern = copy.deepcopy(self.pattern, memo)
|
|
||||||
new.repetition = copy.deepcopy(self.repetition, memo)
|
new.repetition = copy.deepcopy(self.repetition, memo)
|
||||||
new.annotations = copy.deepcopy(self.annotations, memo)
|
new.annotations = copy.deepcopy(self.annotations, memo)
|
||||||
new.set_locked(self.locked)
|
|
||||||
return new
|
return new
|
||||||
|
|
||||||
# pattern property
|
# target property
|
||||||
@property
|
@property
|
||||||
def pattern(self) -> Optional['Pattern']:
|
def target(self) -> Optional[str]:
|
||||||
return self._pattern
|
return self._target
|
||||||
|
|
||||||
@pattern.setter
|
@target.setter
|
||||||
def pattern(self, val: Optional['Pattern']) -> None:
|
def target(self, val: Optional[str]) -> None:
|
||||||
from .pattern import Pattern
|
if val is not None and not isinstance(val, str):
|
||||||
if val is not None and not isinstance(val, Pattern):
|
raise PatternError(f'Provided target {val} is not a str or None!')
|
||||||
raise PatternError(f'Provided pattern {val} is not a Pattern object or None!')
|
self._target = val
|
||||||
self._pattern = val
|
|
||||||
|
|
||||||
# Mirrored property
|
# Mirrored property
|
||||||
@property
|
@property
|
||||||
@ -132,14 +123,31 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
|
|||||||
raise PatternError('Mirrored must be a 2-element list of booleans')
|
raise PatternError('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)
|
||||||
|
|
||||||
def as_pattern(self) -> 'Pattern':
|
def as_pattern(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
pattern: Optional[Pattern] = None,
|
||||||
|
library: Optional[Mapping[str, Pattern]] = None,
|
||||||
|
) -> 'Pattern':
|
||||||
"""
|
"""
|
||||||
|
Args:
|
||||||
|
pattern: Pattern object to transform
|
||||||
|
library: A str->Pattern mapping, used instead of `pattern`. Must contain
|
||||||
|
`self.target`.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A copy of self.pattern which has been scaled, rotated, etc. according to this
|
A copy of the referenced Pattern which has been scaled, rotated, etc.
|
||||||
`SubPattern`'s properties.
|
according to this `SubPattern`'s properties.
|
||||||
"""
|
"""
|
||||||
assert(self.pattern is not None)
|
if pattern is None:
|
||||||
pattern = self.pattern.deepcopy().deepunlock()
|
if library is None:
|
||||||
|
raise PatternError('as_pattern() must be given a pattern or library.')
|
||||||
|
|
||||||
|
assert(self.target is not None)
|
||||||
|
pattern = library[self.target]
|
||||||
|
|
||||||
|
pattern = pattern.deepcopy()
|
||||||
|
|
||||||
if self.scale != 1:
|
if self.scale != 1:
|
||||||
pattern.scale_by(self.scale)
|
pattern.scale_by(self.scale)
|
||||||
if numpy.any(self.mirrored):
|
if numpy.any(self.mirrored):
|
||||||
@ -152,7 +160,7 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
|
|||||||
pattern.scale_element_doses(self.dose)
|
pattern.scale_element_doses(self.dose)
|
||||||
|
|
||||||
if self.repetition is not None:
|
if self.repetition is not None:
|
||||||
combined = type(pattern)(name='__repetition__')
|
combined = type(pattern)()
|
||||||
for dd in self.repetition.displacements:
|
for dd in self.repetition.displacements:
|
||||||
temp_pat = pattern.deepcopy()
|
temp_pat = pattern.deepcopy()
|
||||||
temp_pat.translate_elements(dd)
|
temp_pat.translate_elements(dd)
|
||||||
@ -174,75 +182,33 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
|
|||||||
self.repetition.mirror(axis)
|
self.repetition.mirror(axis)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def get_bounds(self) -> Optional[NDArray[numpy.float64]]:
|
def get_bounds(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
pattern: Optional[Pattern] = None,
|
||||||
|
library: Optional[Mapping[str, Pattern]] = None,
|
||||||
|
) -> Optional[NDArray[numpy.float64]]:
|
||||||
"""
|
"""
|
||||||
Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the
|
Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the
|
||||||
extent of the `SubPattern` in each dimension.
|
extent of the `SubPattern` in each dimension.
|
||||||
Returns `None` if the contained `Pattern` is empty.
|
Returns `None` if the contained `Pattern` is empty.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
library: Name-to-Pattern mapping for resul
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
`[[x_min, y_min], [x_max, y_max]]` or `None`
|
`[[x_min, y_min], [x_max, y_max]]` or `None`
|
||||||
"""
|
"""
|
||||||
if self.pattern is None:
|
if pattern is None and library is None:
|
||||||
|
raise PatternError('as_pattern() must be given a pattern or library.')
|
||||||
|
if pattern is None and self.target is None:
|
||||||
return None
|
return None
|
||||||
return self.as_pattern().get_bounds()
|
return self.as_pattern(pattern=pattern, library=library).get_bounds()
|
||||||
|
|
||||||
def lock(self: S) -> S:
|
|
||||||
"""
|
|
||||||
Lock the SubPattern, disallowing changes
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
self.mirrored.flags.writeable = False
|
|
||||||
PositionableImpl._lock(self)
|
|
||||||
LockableImpl.lock(self)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def unlock(self: S) -> S:
|
|
||||||
"""
|
|
||||||
Unlock the SubPattern
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
LockableImpl.unlock(self)
|
|
||||||
PositionableImpl._unlock(self)
|
|
||||||
self.mirrored.flags.writeable = True
|
|
||||||
return self
|
|
||||||
|
|
||||||
def deeplock(self: S) -> S:
|
|
||||||
"""
|
|
||||||
Recursively lock the SubPattern and its contained pattern
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
assert(self.pattern is not None)
|
|
||||||
self.lock()
|
|
||||||
self.pattern.deeplock()
|
|
||||||
return self
|
|
||||||
|
|
||||||
def deepunlock(self: S) -> S:
|
|
||||||
"""
|
|
||||||
Recursively unlock the SubPattern and its contained pattern
|
|
||||||
|
|
||||||
This is dangerous unless you have just performed a deepcopy, since
|
|
||||||
the subpattern and its components may be used in more than one once!
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
assert(self.pattern is not None)
|
|
||||||
self.unlock()
|
|
||||||
self.pattern.deepunlock()
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
name = self.pattern.name if self.pattern is not None else None
|
name = f'"{self.target}"' if self.target is not None else None
|
||||||
rotation = f' r{self.rotation*180/pi:g}' if self.rotation != 0 else ''
|
rotation = f' r{self.rotation*180/pi:g}' if self.rotation != 0 else ''
|
||||||
scale = f' d{self.scale:g}' if self.scale != 1 else ''
|
scale = f' d{self.scale:g}' if self.scale != 1 else ''
|
||||||
mirrored = ' m{:d}{:d}'.format(*self.mirrored) if self.mirrored.any() else ''
|
mirrored = ' m{:d}{:d}'.format(*self.mirrored) if self.mirrored.any() else ''
|
||||||
dose = f' d{self.dose:g}' if self.dose != 1 else ''
|
dose = f' d{self.dose:g}' if self.dose != 1 else ''
|
||||||
locked = ' L' if self.locked else ''
|
return f'<SubPattern {name} at {self.offset}{rotation}{scale}{mirrored}{dose}>'
|
||||||
return f'<SubPattern "{name}" at {self.offset}{rotation}{scale}{mirrored}{dose}{locked}>'
|
|
||||||
|
@ -9,5 +9,4 @@ from .repeatable import Repeatable, RepeatableImpl
|
|||||||
from .scalable import Scalable, ScalableImpl
|
from .scalable import Scalable, ScalableImpl
|
||||||
from .mirrorable import Mirrorable
|
from .mirrorable import Mirrorable
|
||||||
from .copyable import Copyable
|
from .copyable import Copyable
|
||||||
from .lockable import Lockable, LockableImpl
|
|
||||||
from .annotatable import Annotatable, AnnotatableImpl
|
from .annotatable import Annotatable, AnnotatableImpl
|
||||||
|
@ -44,9 +44,6 @@ class AnnotatableImpl(Annotatable, metaclass=ABCMeta):
|
|||||||
@property
|
@property
|
||||||
def annotations(self) -> annotations_t:
|
def annotations(self) -> annotations_t:
|
||||||
return self._annotations
|
return self._annotations
|
||||||
# # TODO: Find a way to make sure the subclass implements Lockable without dealing with diamond inheritance or this extra hasattr
|
|
||||||
# if hasattr(self, 'is_locked') and self.is_locked():
|
|
||||||
# return MappingProxyType(self._annotations)
|
|
||||||
|
|
||||||
@annotations.setter
|
@annotations.setter
|
||||||
def annotations(self, annotations: annotations_t):
|
def annotations(self, annotations: annotations_t):
|
||||||
|
@ -120,23 +120,3 @@ class PositionableImpl(Positionable, metaclass=ABCMeta):
|
|||||||
def translate(self: I, offset: ArrayLike) -> I:
|
def translate(self: I, offset: ArrayLike) -> I:
|
||||||
self._offset += offset # type: ignore # NDArray += ArrayLike should be fine??
|
self._offset += offset # type: ignore # NDArray += ArrayLike should be fine??
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def _lock(self: I) -> I:
|
|
||||||
"""
|
|
||||||
Lock the entity, disallowing further changes
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
self._offset.flags.writeable = False
|
|
||||||
return self
|
|
||||||
|
|
||||||
def _unlock(self: I) -> I:
|
|
||||||
"""
|
|
||||||
Unlock the entity
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
self._offset.flags.writeable = True
|
|
||||||
return self
|
|
||||||
|
@ -5,6 +5,7 @@ from .types import layer_t, annotations_t
|
|||||||
|
|
||||||
from .array import is_scalar
|
from .array import is_scalar
|
||||||
from .autoslots import AutoSlots
|
from .autoslots import AutoSlots
|
||||||
|
from .deferreddict import DeferredDict
|
||||||
|
|
||||||
from .bitwise import get_bit, set_bit
|
from .bitwise import get_bit, set_bit
|
||||||
from .vertices import (
|
from .vertices import (
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
2D bin-packing
|
2D bin-packing
|
||||||
"""
|
"""
|
||||||
from typing import Tuple, List, Set, Sequence, Callable
|
from typing import Tuple, List, Set, Sequence, Callable, Mapping
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from numpy.typing import NDArray, ArrayLike
|
from numpy.typing import NDArray, ArrayLike
|
||||||
@ -11,16 +11,18 @@ from ..pattern import Pattern
|
|||||||
from ..subpattern import SubPattern
|
from ..subpattern import SubPattern
|
||||||
|
|
||||||
|
|
||||||
def pack_patterns(patterns: Sequence[Pattern],
|
def pack_patterns(
|
||||||
|
library: Mapping[str, Pattern],
|
||||||
|
patterns: Sequence[str],
|
||||||
regions: numpy.ndarray,
|
regions: numpy.ndarray,
|
||||||
spacing: Tuple[float, float],
|
spacing: Tuple[float, float],
|
||||||
presort: bool = True,
|
presort: bool = True,
|
||||||
allow_rejects: bool = True,
|
allow_rejects: bool = True,
|
||||||
packer: Callable = maxrects_bssf,
|
packer: Callable = maxrects_bssf,
|
||||||
) -> Tuple[Pattern, List[Pattern]]:
|
) -> Tuple[Pattern, List[str]]:
|
||||||
half_spacing = numpy.array(spacing) / 2
|
half_spacing = numpy.array(spacing) / 2
|
||||||
|
|
||||||
bounds = [pp.get_bounds() for pp in patterns]
|
bounds = [library[pp].get_bounds() for pp in patterns]
|
||||||
sizes = [bb[1] - bb[0] + spacing if bb is not None else spacing for bb in bounds]
|
sizes = [bb[1] - bb[0] + spacing if bb is not None else spacing for bb in bounds]
|
||||||
offsets = [half_spacing - bb[0] if bb is not None else (0, 0) for bb in bounds]
|
offsets = [half_spacing - bb[0] if bb is not None else (0, 0) for bb in bounds]
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user