Skip to content

Commit

Permalink
Fix broken GDSPY after update version
Browse files Browse the repository at this point in the history
  • Loading branch information
DeanHazineh committed Aug 31, 2024
1 parent ef8b8d3 commit 8803426
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 74 deletions.
4 changes: 2 additions & 2 deletions dflat/GDSII/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from .assemble import (
assemble_nanocylinder_gds,
assemble_cylinder_gds,
assemble_ellipse_gds,
asseble_nanofin_gds,
assemble_fin_gds,
)
204 changes: 135 additions & 69 deletions dflat/GDSII/assemble.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
from tqdm.auto import tqdm
import gdspy
import time
import uuid

from .gds_utils import add_marker_tag, upsample_block


def assemble_nanocylinder_gds(
def assemble_cylinder_gds(
params,
mask,
cell_size,
Expand All @@ -17,22 +18,25 @@ def assemble_nanocylinder_gds(
marker_size=250e-6,
number_of_points=9,
):
"""Generate a GDS for nanocylinder metasurfaces.
"""Generate a GDS file for nanocylinder metasurfaces.
Args:
params (float): Nanocylinder radius across the lens of shape [H, W, 1].
mask (int): Boolean mask of whether to write a shape or skip it of shape [H, W].
cell_size (list): Cell sizes holding the nanocylinder of [dy, dx].
block_size (list): Block sizes to repeat the nanocylinders of [dy', dx']. resize function is applied.
savepath (str): Path to save the gds file (including .gds extension).
gds_unit (flaot, optional): gdspy units. Defaults to 1e-6.
gds_precision (float, optional): gdspy precision. Defaults to 1e-9.
marker_size (float, optional): size of alignment markers. Defaults to 250e-6.
number_of_points (int, optional): Number of points to represent the shape. Defaults to 9.
params (numpy.ndarray): Nanocylinder radii across the lens, shape [H, W, 1].
mask (numpy.ndarray): Boolean mask indicating whether to write a shape (True) or skip it (False), shape [H, W].
cell_size (list): Cell sizes holding the nanocylinder [dy, dx].
block_size (list): Block sizes to repeat the nanocylinders [dy', dx']. Resizing may be applied.
savepath (str): Path to save the GDS file (including .gds extension).
gds_unit (float, optional): GDSPY units. Defaults to 1e-6.
gds_precision (float, optional): GDSPY precision. Defaults to 1e-9.
marker_size (float, optional): Size of alignment markers. Defaults to 250e-6.
number_of_points (int, optional): Number of points to represent the circular shape. Defaults to 9.
Raises:
ValueError: If params.shape[-1] != 1.
"""
assert (
params.shape[-1] == 1
), "Shape dimension D encodes radius should be equal to 1."
if params.shape[-1] != 1:
raise ValueError("Shape dimension D encoding radius should be equal to 1.")

assemble_standard_shapes(
gdspy.Round,
params,
Expand All @@ -59,22 +63,25 @@ def assemble_ellipse_gds(
marker_size=250e-6,
number_of_points=9,
):
"""Generate a GDS for Nano-ellipse metasurfaces.
"""Generate a GDS file for nano-ellipse metasurfaces.
Args:
params (float): Nanocylinder radius across the lens of shape [H, W, 1].
mask (int): Boolean mask of whether to write a shape or skip it of shape [H, W].
cell_size (list): Cell sizes holding the nanocylinder of [dy, dx].
block_size (list): Block sizes to repeat the nanocylinders of [dy', dx']. resize function is applied.
savepath (str): Path to save the gds file (including .gds extension).
gds_unit (flaot, optional): gdspy units. Defaults to 1e-6.
gds_precision (float, optional): gdspy precision. Defaults to 1e-9.
marker_size (float, optional): size of alignment markers. Defaults to 250e-6.
number_of_points (int, optional): Number of points to represent the shape. Defaults to 9.
params (numpy.ndarray): Ellipse radii across the lens, shape [H, W, 2] where [:,:,0] is x-radius and [:,:,1] is y-radius.
mask (numpy.ndarray): Boolean mask indicating whether to write a shape (True) or skip it (False), shape [H, W].
cell_size (list): Cell sizes holding the nano-ellipse [dy, dx].
block_size (list): Block sizes to repeat the nano-ellipses [dy', dx']. Resizing may be applied.
savepath (str): Path to save the GDS file (including .gds extension).
gds_unit (float, optional): GDSPY units. Defaults to 1e-6.
gds_precision (float, optional): GDSPY precision. Defaults to 1e-9.
marker_size (float, optional): Size of alignment markers. Defaults to 250e-6.
number_of_points (int, optional): Number of points to represent the elliptical shape. Defaults to 9.
Raises:
ValueError: If params.shape[-1] != 2.
"""
assert (
params.shape[-1] == 2
), "Shape dimension D encodes radius (x,y) should be equal to 2."
if params.shape[-1] != 2:
raise ValueError("Shape dimension D encoding radii (x,y) should be equal to 2.")

assemble_standard_shapes(
gdspy.Round,
params,
Expand All @@ -90,7 +97,7 @@ def assemble_ellipse_gds(
return


def asseble_nanofin_gds(
def assemble_fin_gds(
params,
mask,
cell_size,
Expand All @@ -101,22 +108,26 @@ def asseble_nanofin_gds(
marker_size=250e-6,
number_of_points=9,
):
"""Generate a GDS for Nanofin metasurfaces.
"""Generate a GDS file for nanofin metasurfaces.
Args:
params (float): Nanocylinder radius across the lens of shape [H, W, 1].
mask (int): Boolean mask of whether to write a shape or skip it of shape [H, W].
cell_size (list): Cell sizes holding the nanocylinder of [dy, dx].
block_size (list): Block sizes to repeat the nanocylinders of [dy', dx']. resize function is applied.
savepath (str): Path to save the gds file (including .gds extension).
gds_unit (flaot, optional): gdspy units. Defaults to 1e-6.
gds_precision (float, optional): gdspy precision. Defaults to 1e-9.
marker_size (float, optional): size of alignment markers. Defaults to 250e-6.
number_of_points (int, optional): Number of points to represent the shape. Defaults to 9.
params (numpy.ndarray): Nanofin dimensions across the lens, shape [H, W, 2] where [:,:,0] is width and [:,:,1] is length.
mask (numpy.ndarray): Boolean mask indicating whether to write a shape (True) or skip it (False), shape [H, W].
cell_size (list): Cell sizes holding the nanofin [dy, dx].
block_size (list): Block sizes to repeat the nanofins [dy', dx']. Resizing may be applied.
savepath (str): Path to save the GDS file (including .gds extension).
gds_unit (float, optional): GDSPY units. Defaults to 1e-6.
gds_precision (float, optional): GDSPY precision. Defaults to 1e-9.
marker_size (float, optional): Size of alignment markers. Defaults to 250e-6.
Raises:
ValueError: If params.shape[-1] != 2.
"""
assert (
params.shape[-1] == 2
), "Shape dimension D encodes width should be equal to 1."
if params.shape[-1] != 2:
raise ValueError(
"Shape dimension D encoding width and length should be equal to 2."
)

assemble_standard_shapes(
gdspy.Rectangle,
params,
Expand Down Expand Up @@ -144,48 +155,103 @@ def assemble_standard_shapes(
marker_size=250e-6,
number_of_points=9,
):
assert len(cell_size) == 2
assert len(block_size) == 2
assert np.all(np.greater_equal(block_size, cell_size))
assert len(params.shape) == 3
assert len(mask.shape) == 2
assert mask.shape == params.shape[0:2]

# upsample the params to match the target blocks
"""
Assemble standard shapes for GDS files based on given parameters.
This function creates a GDS file containing a metasurface pattern of standard shapes
(e.g., circles, ellipses, rectangles) based on the provided parameters and mask.
Args:
cell_fun (callable): GDSPY function to create the shape (e.g., gdspy.Round, gdspy.Rectangle).
params (numpy.ndarray): Shape parameters across the lens, shape [H, W, D] where D depends on the shape type.
mask (numpy.ndarray): Boolean mask indicating whether to write a shape (True) or skip it (False), shape [H, W].
cell_size (list): Cell sizes holding the shape [dy, dx].
block_size (list): Block sizes to repeat the shapes [dy', dx']. Resizing may be applied.
savepath (str): Path to save the GDS file (including .gds extension).
gds_unit (float, optional): GDSPY units. Defaults to 1e-6.
gds_precision (float, optional): GDSPY precision. Defaults to 1e-9.
marker_size (float, optional): Size of alignment markers. Defaults to 250e-6.
number_of_points (int, optional): Number of points to represent curved shapes. Defaults to 9.
Raises:
ValueError: If input dimensions are incorrect or inconsistent.
Returns:
None
"""
# Input validation
if len(cell_size) != 2 or len(block_size) != 2:
raise ValueError("cell_size and block_size must be lists of length 2.")
if not np.all(np.greater_equal(block_size, cell_size)):
raise ValueError("block_size must be greater than or equal to cell_size.")
if len(params.shape) != 3 or len(mask.shape) != 2:
raise ValueError("params must be 3D and mask must be 2D.")
if mask.shape != params.shape[:2]:
raise ValueError("mask shape must match the first two dimensions of params.")

# Upsample the params to match the target blocks
params_, mask = upsample_block(params, mask, cell_size, block_size)
mask = mask.astype(int).astype(bool)
mask = mask.astype(bool)
pshape = params_.shape

# Write to GDS
unique_id = str(uuid.uuid4())[:8]
lib = gdspy.GdsLibrary(unit=gds_unit, precision=gds_precision)
cell = lib.new_cell("MAIN")
cell = lib.new_cell(f"MAIN_{unique_id}")
print("Writing metasurface shapes to GDS File")
start = time.time()
for yi in tqdm(range(pshape[0])):
for xi in range(pshape[1]):
if mask[yi, xi]:
xoffset = cell_size[1] * xi / gds_unit
yoffset = cell_size[0] * yi / gds_unit
radius = params_[yi, xi, 0] / gds_unit
shape = cell_fun((xoffset, yoffset), radius, number_of_points=9)
cell.add(shape)

### Add some lens markers

for yi, xi in np.ndindex(pshape[:2]):
if mask[yi, xi]:
xoffset = cell_size[1] * xi / gds_unit
yoffset = cell_size[0] * yi / gds_unit
shape_params = params_[yi, xi] / gds_unit
shape_params = shape_params.flatten()

## In new version of GDSPY, we can no longer specify rectangle widths (?)
## Now it corresponds to edge coordintes which is unfortunate
if cell_fun == gdspy.Round:
if len(shape_params) == 1:
shape_params = [shape_params[0], shape_params[0]]
shape = cell_fun((xoffset, yoffset), shape_params)
elif cell_fun == gdspy.Rectangle:
shape_params += [xoffset, yoffset]
shape = cell_fun((xoffset, yoffset), shape_params)
else:
raise ValueError
cell.add(shape)

# Add lens markers
hx = cell_size[1] * pshape[1] / gds_unit
hy = cell_size[0] * pshape[0] / gds_unit
ms = marker_size / gds_unit
cell_annot = lib.new_cell("TEXT")
cell_annot = lib.new_cell(f"TEXT_{unique_id}")
add_marker_tag(cell_annot, ms, hx, hy)

top_cell = lib.new_cell("TOP_CELL")
# Reference cell1 and cell2 in the top-level cell
ref_cell1 = gdspy.CellReference(cell)
ref_cell2 = gdspy.CellReference(cell_annot)
top_cell.add(ref_cell1)
top_cell.add(ref_cell2)
# Create top-level cell and add references
top_cell = lib.new_cell(f"TOP_CELL_{unique_id}")
top_cell.add(gdspy.CellReference(cell))
top_cell.add(gdspy.CellReference(cell_annot))

# Write GDS file
lib.write_gds(savepath)
end = time.time()
print("Completed writing and saving metasurface GDS File: Time: ", end - start)
print(
f"Completed writing and saving metasurface GDS File. Time: {end - start:.2f} seconds"
)

return


if __name__ == "__main__":
assemble_fin_gds(
np.random.rand(10, 10, 2) * 250e-9,
np.random.choice([True, False], size=(10, 10)),
[500e-9, 500e-9],
[1e-6, 1e-6],
"/home/deanhazineh/Research/DFlat/dflat/GDSII/out.gds",
gds_unit=1e-6,
gds_precision=1e-9,
marker_size=250e-6,
number_of_points=9,
)
7 changes: 4 additions & 3 deletions dflat/GDSII/gds_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@


def upsample_block(params, mask, cell_size, block_size):
# upsample the params to match the target blocks
H, W, C = params.shape
scale_factor = np.array(block_size) / np.array(cell_size)
Hnew = np.rint(H * scale_factor[0]).astype(int)
Expand All @@ -18,10 +17,12 @@ def upsample_block(params, mask, cell_size, block_size):
params = np.expand_dims(params, -1)

mask = cv2.resize(
np.expand_dims(mask, -1),
mask.astype(np.float16),
(Wnew, Hnew),
interpolation=cv2.INTER_LINEAR,
interpolation=cv2.INTER_NEAREST, # Use NEAREST for mask to preserve binary nature
)
mask = mask.astype(bool)

return params, mask


Expand Down
67 changes: 67 additions & 0 deletions tests/test_gds_assembly.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import pytest
import numpy as np
import os
import tempfile

from dflat.GDSII import (
assemble_cylinder_gds,
assemble_ellipse_gds,
assemble_fin_gds,
)


@pytest.fixture
def temp_gds_file():
with tempfile.NamedTemporaryFile(suffix=".gds", delete=False) as tmp_file:
yield tmp_file.name
os.unlink(tmp_file.name)


@pytest.fixture
def sample_data():
return {
"params_1d": np.random.rand(10, 10, 1),
"params_2d": np.random.rand(10, 10, 2),
"mask": np.random.choice([True, False], size=(10, 10)),
"cell_size": [1e-6, 1e-6],
"block_size": [2e-6, 2e-6],
}


def test_assemble_cylinder_gds(sample_data, temp_gds_file):
try:
assemble_cylinder_gds(
sample_data["params_1d"],
sample_data["mask"],
sample_data["cell_size"],
sample_data["block_size"],
temp_gds_file,
)
except Exception as e:
pytest.fail(f"assemble_nanocylinder_gds raised an exception: {e}")


def test_assemble_ellipse_gds(sample_data, temp_gds_file):
try:
assemble_ellipse_gds(
sample_data["params_2d"],
sample_data["mask"],
sample_data["cell_size"],
sample_data["block_size"],
temp_gds_file,
)
except Exception as e:
pytest.fail(f"assemble_ellipse_gds raised an exception: {e}")


def test_assemble_fin_gds(sample_data, temp_gds_file):
try:
assemble_fin_gds(
sample_data["params_2d"],
sample_data["mask"],
sample_data["cell_size"],
sample_data["block_size"],
temp_gds_file,
)
except Exception as e:
pytest.fail(f"assemble_nanofin_gds raised an exception: {e}")

0 comments on commit 8803426

Please sign in to comment.