Compare commits
No commits in common. "5e085794986b42f3115bf316457ea4f90e53ff2e" and "48f7569c1fb0817ce6a13c16e307a15fd2ec4acd" have entirely different histories.
5e08579498
...
48f7569c1f
2 changed files with 68 additions and 224 deletions
|
|
@ -120,10 +120,10 @@ def build(
|
||||||
layer, data_type = _mlayer2oas(layer_num)
|
layer, data_type = _mlayer2oas(layer_num)
|
||||||
lib.layers += [
|
lib.layers += [
|
||||||
fatrec.LayerName(
|
fatrec.LayerName(
|
||||||
nstring = name,
|
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)]
|
||||||
|
|
||||||
|
|
@ -286,11 +286,11 @@ def read(
|
||||||
|
|
||||||
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
|
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
|
||||||
pat.polygon(
|
pat.polygon(
|
||||||
vertices = vertices,
|
vertices=vertices,
|
||||||
layer = element.get_layer_tuple(),
|
layer=element.get_layer_tuple(),
|
||||||
offset = element.get_xy(),
|
offset=element.get_xy(),
|
||||||
annotations = annotations,
|
annotations=annotations,
|
||||||
repetition = repetition,
|
repetition=repetition,
|
||||||
)
|
)
|
||||||
elif isinstance(element, fatrec.Path):
|
elif isinstance(element, fatrec.Path):
|
||||||
vertices = numpy.cumsum(numpy.vstack(((0, 0), element.get_point_list())), axis=0)
|
vertices = numpy.cumsum(numpy.vstack(((0, 0), element.get_point_list())), axis=0)
|
||||||
|
|
@ -310,13 +310,13 @@ def read(
|
||||||
|
|
||||||
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
|
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
|
||||||
pat.path(
|
pat.path(
|
||||||
vertices = vertices,
|
vertices=vertices,
|
||||||
layer = element.get_layer_tuple(),
|
layer=element.get_layer_tuple(),
|
||||||
offset = element.get_xy(),
|
offset=element.get_xy(),
|
||||||
repetition = repetition,
|
repetition=repetition,
|
||||||
annotations = annotations,
|
annotations=annotations,
|
||||||
width = element.get_half_width() * 2,
|
width=element.get_half_width() * 2,
|
||||||
cap = cap,
|
cap=cap,
|
||||||
**path_args,
|
**path_args,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -325,11 +325,11 @@ def read(
|
||||||
height = element.get_height()
|
height = element.get_height()
|
||||||
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
|
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
|
||||||
pat.polygon(
|
pat.polygon(
|
||||||
layer = element.get_layer_tuple(),
|
layer=element.get_layer_tuple(),
|
||||||
offset = element.get_xy(),
|
offset=element.get_xy(),
|
||||||
repetition = repetition,
|
repetition=repetition,
|
||||||
vertices = numpy.array(((0, 0), (1, 0), (1, 1), (0, 1))) * (width, height),
|
vertices=numpy.array(((0, 0), (1, 0), (1, 1), (0, 1))) * (width, height),
|
||||||
annotations = annotations,
|
annotations=annotations,
|
||||||
)
|
)
|
||||||
|
|
||||||
elif isinstance(element, fatrec.Trapezoid):
|
elif isinstance(element, fatrec.Trapezoid):
|
||||||
|
|
@ -440,11 +440,11 @@ def read(
|
||||||
else:
|
else:
|
||||||
string = str_or_ref.string
|
string = str_or_ref.string
|
||||||
pat.label(
|
pat.label(
|
||||||
layer = element.get_layer_tuple(),
|
layer=element.get_layer_tuple(),
|
||||||
offset = element.get_xy(),
|
offset=element.get_xy(),
|
||||||
repetition = repetition,
|
repetition=repetition,
|
||||||
annotations = annotations,
|
annotations=annotations,
|
||||||
string = string,
|
string=string,
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
@ -549,13 +549,13 @@ def _shapes_to_elements(
|
||||||
offset = rint_cast(shape.offset + rep_offset)
|
offset = rint_cast(shape.offset + rep_offset)
|
||||||
radius = rint_cast(shape.radius)
|
radius = rint_cast(shape.radius)
|
||||||
circle = fatrec.Circle(
|
circle = fatrec.Circle(
|
||||||
layer = layer,
|
layer=layer,
|
||||||
datatype = datatype,
|
datatype=datatype,
|
||||||
radius = cast('int', radius),
|
radius=cast('int', 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):
|
||||||
|
|
@ -566,16 +566,16 @@ def _shapes_to_elements(
|
||||||
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(
|
path = fatrec.Path(
|
||||||
layer = layer,
|
layer=layer,
|
||||||
datatype = datatype,
|
datatype=datatype,
|
||||||
point_list = cast('Sequence[Sequence[int]]', deltas),
|
point_list=cast('Sequence[Sequence[int]]', deltas),
|
||||||
half_width = cast('int', half_width),
|
half_width=cast('int', half_width),
|
||||||
x = xy[0],
|
x=xy[0],
|
||||||
y = xy[1],
|
y=xy[1],
|
||||||
extension_start = extension_start, # TODO implement multiple cap types?
|
extension_start=extension_start, # TODO implement multiple cap types?
|
||||||
extension_end = extension_end,
|
extension_end=extension_end,
|
||||||
properties = properties,
|
properties=properties,
|
||||||
repetition = repetition,
|
repetition=repetition,
|
||||||
)
|
)
|
||||||
elements.append(path)
|
elements.append(path)
|
||||||
else:
|
else:
|
||||||
|
|
@ -583,13 +583,13 @@ def _shapes_to_elements(
|
||||||
xy = rint_cast(polygon.offset + polygon.vertices[0] + rep_offset)
|
xy = rint_cast(polygon.offset + polygon.vertices[0] + rep_offset)
|
||||||
points = rint_cast(numpy.diff(polygon.vertices, axis=0))
|
points = rint_cast(numpy.diff(polygon.vertices, axis=0))
|
||||||
elements.append(fatrec.Polygon(
|
elements.append(fatrec.Polygon(
|
||||||
layer = layer,
|
layer=layer,
|
||||||
datatype = datatype,
|
datatype=datatype,
|
||||||
x = xy[0],
|
x=xy[0],
|
||||||
y = xy[1],
|
y=xy[1],
|
||||||
point_list = cast('list[list[int]]', points),
|
point_list=cast('list[list[int]]', points),
|
||||||
properties = properties,
|
properties=properties,
|
||||||
repetition = repetition,
|
repetition=repetition,
|
||||||
))
|
))
|
||||||
return elements
|
return elements
|
||||||
|
|
||||||
|
|
@ -606,13 +606,13 @@ def _labels_to_texts(
|
||||||
xy = rint_cast(label.offset + rep_offset)
|
xy = rint_cast(label.offset + rep_offset)
|
||||||
properties = annotations_to_properties(label.annotations)
|
properties = annotations_to_properties(label.annotations)
|
||||||
texts.append(fatrec.Text(
|
texts.append(fatrec.Text(
|
||||||
layer = layer,
|
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
|
||||||
|
|
||||||
|
|
@ -622,12 +622,10 @@ def repetition_fata2masq(
|
||||||
) -> Repetition | None:
|
) -> Repetition | None:
|
||||||
mrep: Repetition | None
|
mrep: Repetition | None
|
||||||
if isinstance(rep, fatamorgana.GridRepetition):
|
if isinstance(rep, fatamorgana.GridRepetition):
|
||||||
mrep = Grid(
|
mrep = Grid(a_vector=rep.a_vector,
|
||||||
a_vector = rep.a_vector,
|
b_vector=rep.b_vector,
|
||||||
b_vector = rep.b_vector,
|
a_count=rep.a_count,
|
||||||
a_count = rep.a_count,
|
b_count=rep.b_count)
|
||||||
b_count = rep.b_count,
|
|
||||||
)
|
|
||||||
elif isinstance(rep, fatamorgana.ArbitraryRepetition):
|
elif isinstance(rep, fatamorgana.ArbitraryRepetition):
|
||||||
displacements = numpy.cumsum(numpy.column_stack((
|
displacements = numpy.cumsum(numpy.column_stack((
|
||||||
rep.x_displacements,
|
rep.x_displacements,
|
||||||
|
|
@ -649,19 +647,14 @@ def repetition_masq2fata(
|
||||||
frep: fatamorgana.GridRepetition | fatamorgana.ArbitraryRepetition | None
|
frep: fatamorgana.GridRepetition | fatamorgana.ArbitraryRepetition | None
|
||||||
if isinstance(rep, Grid):
|
if isinstance(rep, Grid):
|
||||||
a_vector = rint_cast(rep.a_vector)
|
a_vector = rint_cast(rep.a_vector)
|
||||||
a_count = int(rep.a_count)
|
b_vector = rint_cast(rep.b_vector) if rep.b_vector is not None else None
|
||||||
if rep.b_count > 1:
|
a_count = rint_cast(rep.a_count)
|
||||||
b_vector = rint_cast(rep.b_vector)
|
b_count = rint_cast(rep.b_count) if rep.b_count is not None else None
|
||||||
b_count = int(rep.b_count)
|
|
||||||
else:
|
|
||||||
b_vector = None
|
|
||||||
b_count = None
|
|
||||||
|
|
||||||
frep = fatamorgana.GridRepetition(
|
frep = fatamorgana.GridRepetition(
|
||||||
a_vector = a_vector,
|
a_vector=cast('list[int]', a_vector),
|
||||||
b_vector = b_vector,
|
b_vector=cast('list[int] | None', b_vector),
|
||||||
a_count = a_count,
|
a_count=cast('int', a_count),
|
||||||
b_count = b_count,
|
b_count=cast('int | None', b_count),
|
||||||
)
|
)
|
||||||
offset = (0, 0)
|
offset = (0, 0)
|
||||||
elif isinstance(rep, Arbitrary):
|
elif isinstance(rep, Arbitrary):
|
||||||
|
|
|
||||||
|
|
@ -1,149 +0,0 @@
|
||||||
from pathlib import Path
|
|
||||||
import pytest
|
|
||||||
from numpy.testing import assert_allclose
|
|
||||||
|
|
||||||
from ..pattern import Pattern
|
|
||||||
from ..library import Library
|
|
||||||
from ..file import gdsii, oasis
|
|
||||||
from ..shapes import Path as MPath, Circle, Polygon
|
|
||||||
from ..repetition import Grid, Arbitrary
|
|
||||||
|
|
||||||
def create_test_library(for_gds: bool = False) -> Library:
|
|
||||||
lib = Library()
|
|
||||||
|
|
||||||
# 1. Polygons
|
|
||||||
pat_poly = Pattern()
|
|
||||||
pat_poly.polygon((1, 0), vertices=[[0, 0], [10, 0], [5, 10]])
|
|
||||||
lib["polygons"] = pat_poly
|
|
||||||
|
|
||||||
# 2. Paths with different endcaps
|
|
||||||
pat_paths = Pattern()
|
|
||||||
# Flush
|
|
||||||
pat_paths.path((2, 0), vertices=[[0, 0], [20, 0]], width=2, cap=MPath.Cap.Flush)
|
|
||||||
# Square
|
|
||||||
pat_paths.path((2, 1), vertices=[[0, 10], [20, 10]], width=2, cap=MPath.Cap.Square)
|
|
||||||
# Circle (Only for GDS)
|
|
||||||
if for_gds:
|
|
||||||
pat_paths.path((2, 2), vertices=[[0, 20], [20, 20]], width=2, cap=MPath.Cap.Circle)
|
|
||||||
# SquareCustom
|
|
||||||
pat_paths.path((2, 3), vertices=[[0, 30], [20, 30]], width=2, cap=MPath.Cap.SquareCustom, cap_extensions=(1, 5))
|
|
||||||
lib["paths"] = pat_paths
|
|
||||||
|
|
||||||
# 3. Circles (only for OASIS or polygonized for GDS)
|
|
||||||
pat_circles = Pattern()
|
|
||||||
if for_gds:
|
|
||||||
# GDS writer calls to_polygons() for non-supported shapes,
|
|
||||||
# but we can also pre-polygonize
|
|
||||||
pat_circles.shapes[(3, 0)].append(Circle(radius=5, offset=(10, 10)).to_polygons()[0])
|
|
||||||
else:
|
|
||||||
pat_circles.shapes[(3, 0)].append(Circle(radius=5, offset=(10, 10)))
|
|
||||||
lib["circles"] = pat_circles
|
|
||||||
|
|
||||||
# 4. Refs with repetitions
|
|
||||||
pat_refs = Pattern()
|
|
||||||
# Simple Ref
|
|
||||||
pat_refs.ref("polygons", offset=(0, 0))
|
|
||||||
# Ref with Grid repetition
|
|
||||||
pat_refs.ref("polygons", offset=(100, 0), repetition=Grid(a_vector=(20, 0), a_count=3, b_vector=(0, 20), b_count=2))
|
|
||||||
# Ref with Arbitrary repetition
|
|
||||||
pat_refs.ref("polygons", offset=(0, 100), repetition=Arbitrary(displacements=[[0, 0], [10, 20], [30, -10]]))
|
|
||||||
lib["refs"] = pat_refs
|
|
||||||
|
|
||||||
# 5. Shapes with repetitions (OASIS only, must be wrapped for GDS)
|
|
||||||
pat_rep_shapes = Pattern()
|
|
||||||
poly_rep = Polygon(vertices=[[0, 0], [5, 0], [5, 5], [0, 5]], repetition=Grid(a_vector=(10, 0), a_count=5))
|
|
||||||
pat_rep_shapes.shapes[(4, 0)].append(poly_rep)
|
|
||||||
lib["rep_shapes"] = pat_rep_shapes
|
|
||||||
|
|
||||||
if for_gds:
|
|
||||||
lib.wrap_repeated_shapes()
|
|
||||||
|
|
||||||
return lib
|
|
||||||
|
|
||||||
def test_gdsii_full_roundtrip(tmp_path: Path) -> None:
|
|
||||||
lib = create_test_library(for_gds=True)
|
|
||||||
gds_file = tmp_path / "full_test.gds"
|
|
||||||
gdsii.writefile(lib, gds_file, meters_per_unit=1e-9)
|
|
||||||
|
|
||||||
read_lib, _ = gdsii.readfile(gds_file)
|
|
||||||
|
|
||||||
# Check existence
|
|
||||||
for name in lib:
|
|
||||||
assert name in read_lib
|
|
||||||
|
|
||||||
# Check Paths
|
|
||||||
read_paths = read_lib["paths"]
|
|
||||||
# Check caps (GDS stores them as path_type)
|
|
||||||
# Order might be different depending on how they were written,
|
|
||||||
# but here they should match the order they were added if dict order is preserved.
|
|
||||||
# Actually, they are grouped by layer.
|
|
||||||
p_flush = read_paths.shapes[(2, 0)][0]
|
|
||||||
assert p_flush.cap == MPath.Cap.Flush
|
|
||||||
|
|
||||||
p_square = read_paths.shapes[(2, 1)][0]
|
|
||||||
assert p_square.cap == MPath.Cap.Square
|
|
||||||
|
|
||||||
p_circle = read_paths.shapes[(2, 2)][0]
|
|
||||||
assert p_circle.cap == MPath.Cap.Circle
|
|
||||||
|
|
||||||
p_custom = read_paths.shapes[(2, 3)][0]
|
|
||||||
assert p_custom.cap == MPath.Cap.SquareCustom
|
|
||||||
assert_allclose(p_custom.cap_extensions, (1, 5))
|
|
||||||
|
|
||||||
# Check Refs with repetitions
|
|
||||||
read_refs = read_lib["refs"]
|
|
||||||
assert len(read_refs.refs["polygons"]) >= 3 # Simple, Grid (becomes 1 AREF), Arbitrary (becomes 3 SREFs)
|
|
||||||
|
|
||||||
# AREF check
|
|
||||||
arefs = [r for r in read_refs.refs["polygons"] if r.repetition is not None]
|
|
||||||
assert len(arefs) == 1
|
|
||||||
assert isinstance(arefs[0].repetition, Grid)
|
|
||||||
assert arefs[0].repetition.a_count == 3
|
|
||||||
assert arefs[0].repetition.b_count == 2
|
|
||||||
|
|
||||||
# Check wrapped shapes
|
|
||||||
# lib.wrap_repeated_shapes() created new patterns
|
|
||||||
# Original pattern "rep_shapes" now should have a Ref
|
|
||||||
assert len(read_lib["rep_shapes"].refs) > 0
|
|
||||||
|
|
||||||
def test_oasis_full_roundtrip(tmp_path: Path) -> None:
|
|
||||||
pytest.importorskip("fatamorgana")
|
|
||||||
lib = create_test_library(for_gds=False)
|
|
||||||
oas_file = tmp_path / "full_test.oas"
|
|
||||||
oasis.writefile(lib, oas_file, units_per_micron=1000)
|
|
||||||
|
|
||||||
read_lib, _ = oasis.readfile(oas_file)
|
|
||||||
|
|
||||||
# Check existence
|
|
||||||
for name in lib:
|
|
||||||
assert name in read_lib
|
|
||||||
|
|
||||||
# Check Circle
|
|
||||||
read_circles = read_lib["circles"]
|
|
||||||
assert isinstance(read_circles.shapes[(3, 0)][0], Circle)
|
|
||||||
assert read_circles.shapes[(3, 0)][0].radius == 5
|
|
||||||
|
|
||||||
# Check Path caps
|
|
||||||
read_paths = read_lib["paths"]
|
|
||||||
assert read_paths.shapes[(2, 0)][0].cap == MPath.Cap.Flush
|
|
||||||
assert read_paths.shapes[(2, 1)][0].cap == MPath.Cap.Square
|
|
||||||
# OASIS HalfWidth is Square. masque's Square is also HalfWidth extension.
|
|
||||||
# Wait, Circle cap in OASIS?
|
|
||||||
# masque/file/oasis.py:
|
|
||||||
# path_cap_map = {
|
|
||||||
# PathExtensionScheme.Flush: Path.Cap.Flush,
|
|
||||||
# PathExtensionScheme.HalfWidth: Path.Cap.Square,
|
|
||||||
# PathExtensionScheme.Arbitrary: Path.Cap.SquareCustom,
|
|
||||||
# }
|
|
||||||
# It seems Circle cap is NOT supported in OASIS by masque currently.
|
|
||||||
# Let's verify what happens with Circle cap in OASIS write.
|
|
||||||
# _shapes_to_elements in oasis.py:
|
|
||||||
# path_type = next(k for k, v in path_cap_map.items() if v == shape.cap)
|
|
||||||
# This will raise StopIteration if Circle is not in path_cap_map.
|
|
||||||
|
|
||||||
# Check Shape repetition
|
|
||||||
read_rep_shapes = read_lib["rep_shapes"]
|
|
||||||
poly = read_rep_shapes.shapes[(4, 0)][0]
|
|
||||||
assert poly.repetition is not None
|
|
||||||
assert isinstance(poly.repetition, Grid)
|
|
||||||
assert poly.repetition.a_count == 5
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue