From 8803426aaa773dd04b23dc28a7f2c475ebb93b4d Mon Sep 17 00:00:00 2001 From: Dean Hazineh Date: Sat, 31 Aug 2024 14:23:33 -0400 Subject: [PATCH] Fix broken GDSPY after update version --- dflat/GDSII/__init__.py | 4 +- dflat/GDSII/assemble.py | 204 ++++++++++++++++++++++++------------- dflat/GDSII/gds_utils.py | 7 +- tests/test_gds_assembly.py | 67 ++++++++++++ 4 files changed, 208 insertions(+), 74 deletions(-) create mode 100644 tests/test_gds_assembly.py diff --git a/dflat/GDSII/__init__.py b/dflat/GDSII/__init__.py index 4a47506..eaba3ea 100644 --- a/dflat/GDSII/__init__.py +++ b/dflat/GDSII/__init__.py @@ -1,5 +1,5 @@ from .assemble import ( - assemble_nanocylinder_gds, + assemble_cylinder_gds, assemble_ellipse_gds, - asseble_nanofin_gds, + assemble_fin_gds, ) diff --git a/dflat/GDSII/assemble.py b/dflat/GDSII/assemble.py index 6a932a5..8bebd4b 100644 --- a/dflat/GDSII/assemble.py +++ b/dflat/GDSII/assemble.py @@ -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, @@ -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, @@ -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, @@ -90,7 +97,7 @@ def assemble_ellipse_gds( return -def asseble_nanofin_gds( +def assemble_fin_gds( params, mask, cell_size, @@ -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, @@ -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, + ) diff --git a/dflat/GDSII/gds_utils.py b/dflat/GDSII/gds_utils.py index 69f93ee..994f0be 100644 --- a/dflat/GDSII/gds_utils.py +++ b/dflat/GDSII/gds_utils.py @@ -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) @@ -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 diff --git a/tests/test_gds_assembly.py b/tests/test_gds_assembly.py new file mode 100644 index 0000000..70c67b3 --- /dev/null +++ b/tests/test_gds_assembly.py @@ -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}")