heitzmann / gdstk

Gdstk (GDSII Tool Kit) is a C++/Python library for creation and manipulation of GDSII and OASIS files.
https://heitzmann.github.io/gdstk/
Boost Software License 1.0
354 stars 86 forks source link

GDS Hierarchy Traversing #264

Open Rishabhgoyal07 opened 4 months ago

Rishabhgoyal07 commented 4 months ago

Hii @heitzmann, can we### traverse hierarchy in gdstk and access cell present in that hierarchy. Example, I have 10 hierarchy in my GDS, can I access the bottom and top hierarchy and move from top to bottom or vice versa and access each cell present in that hierarchy.

heitzmann commented 4 months ago

Yes, just look at Cell.references. For each reference you can go to the corresponding cell with Reference.cell, and then traverse its references and so on. Note that you might traverse the same cell more than once, if it is referenced multiple times.

nmz787-intel commented 1 month ago

@Rishabhgoyal07 here's some code that given a topcell and a child that you want to get, but taking into account any offsets/rotations/repetitions the referencing did. NOTE I only implemented this for polygons and labels in the resulting "transformed cell". I have some comments for how to approach for FlexPath/etc... I think the polygon and label chunks of code should be easy to adapt to any other cell components.

the code:


def find_leaf_reference_offsets_and_rotations(cell, target_cell_name):
    """
    Recursively find the offsets and rotations applied to reach the target cell.

    Parameters:
    - cell: The current cell being traversed.
    - target_cell_name: The name of the target cell to find.
    - current_offset: The cumulative offset applied so far.
    - current_rotation: The cumulative rotation applied so far.

    Returns:
    - A list of lists of tuples containing the offset and rotation at each level of the hierarchy.
    """
    transformations = []
    current_cell_name = cell.name
    for ref in cell.references:
        if ref.cell.name == target_cell_name:
            # Found the target cell, add the current offset and rotation
            transformations.append((ref.origin, ref.rotation, ref.x_reflection, ref.magnification, target_cell_name))
        else:
            # Recursively search in the referenced cell
            result = find_leaf_reference_offsets_and_rotations(ref.cell, target_cell_name)
            if result:
                # If the target cell is found in the recursion, add the current level's offset and rotation
                for res in result:
                    transformations.append(((ref.origin, ref.rotation, ref.x_reflection, ref.magnification, ref.cell.name), res))
    return transformations

def rotate_point(point, angle, origin=(0, 0), round_to=3):
    """Rotate a point around a given origin by a given angle."""
    ox, oy = origin
    px, py = point

    qx = ox + math.cos(angle) * (px - ox) - math.sin(angle) * (py - oy)
    qy = oy + math.sin(angle) * (px - ox) + math.cos(angle) * (py - oy)
    return [round(qx, round_to), round(qy, round_to)]

def reflect_point(point, origin=(0, 0)):
    """Reflect a point across the x-axis around a given origin."""
    ox, oy = origin
    px, py = point

    qx = 2 * ox - px
    return [qx, py]

def apply_reps(cell_component_with_repetition, keep_reps_attr=False):
    reps_out = []
    if str(cell_component_with_repetition.repetition) != 'No repetition':
        rep_obj = cell_component_with_repetition.repetition
        reps = cell_component_with_repetition.apply_repetition()

        # ensure input is not in output, as I experienced once
        if isinstance(cell_component_with_repetition, gdstk.Polygon):
            polygon = cell_component_with_repetition
            reps_out += [rep for rep in reps if not (polygon.points == rep.points).all()]
        elif isinstance(cell_component_with_repetition, gdstk.Label):
            lbl = cell_component_with_repetition
            reps_out += [rep for rep in reps if (
                                                (lbl.origin != rep.origin) or
                                                (lbl.text != rep.text) or
                                                (lbl.texttype == rep.texttype) or
                                                (lbl.anchor == rep.anchor) or
                                                (lbl.rotation == rep.rotation)  or
                                                (lbl.magnification == rep.magnification) or
                                                (lbl.x_reflection == rep.x_reflection)
                                                 )]
        if keep_reps_attr:
            cell_component_with_repetition.repetition = rep_obj
    return reps_out

def apply_transformations_to_cell(cell, transform_list):
    """Apply a list of transformations to all elements in a cell and return a new cell."""
    new_cell = gdstk.Cell(f"{cell.name}_transformed")

    # Transform polygons
    input_polygons = cell.polygons
    while input_polygons:
        polygon = input_polygons.pop()
        input_polygons.extend(apply_reps(polygon, keep_reps_attr=True))

        new_points = [list(point) for point in polygon.points]
        origin_for_rotating_around = [0,0]
        for origin_transform, rotation_transform, x_reflection, magnification, parent_cell in transform_list:
            # Apply translation
            translated_points = [[point[0] + origin_transform[0], point[1] + origin_transform[1]] for point in new_points]
            origin_for_rotating_around[0] += origin_transform[0]
            origin_for_rotating_around[1] += origin_transform[1]
            # Apply rotation around the origin
            rotated_points = [rotate_point(point, rotation_transform, origin_for_rotating_around) for point in translated_points]

            # Apply x reflection
            reflected_points = rotated_points
            if x_reflection:
                #TODO add implementation
                reflected_points = [reflect_point(point, origin_for_rotating_around) for point in rotated_points]

            new_points = reflected_points

        new_polygon = gdstk.Polygon(new_points, datatype=polygon.datatype, layer=polygon.layer)
        new_cell.add(new_polygon)

    # Transform paths
    if cell.paths:
        raise NotImplementedError('implement me')
    # for path in cell.paths:
    #     new_points = [list(point) for point in path.points]
    #
    #     for origin_transform, rotation_transform, parent_cell in transform_list:
    #         # Apply translation
    #         new_points = [[point[0] + origin_transform[0], point[1] + origin_transform[1]] for point in new_points]
    #         # Apply rotation around the origin
    #         new_points = [rotate_point(point, rotation_transform, origin_transform) for point in new_points]
    #
    #     new_path = gdstk.FlexPath(new_points, path.widths, datatype=path.datatype, layer=path.layer)
    #     new_cell.add(new_path)

    # Transform labels
    input_labels = cell.labels
    while input_labels:
        label = input_labels.pop()
        input_labels.extend(apply_reps(label, keep_reps_attr=True))
        new_position = list(label.origin)
        origin_for_rotating_around=[0,0]
        for origin_transform, rotation_transform, x_reflection, magnification, parent_cell in transform_list:
            # Apply translation
            new_position[0] += origin_transform[0]
            new_position[1] += origin_transform[1]
            # Apply rotation around the origin
            origin_for_rotating_around[0] += origin_transform[0]
            origin_for_rotating_around[1] += origin_transform[1]

            new_position = rotate_point(new_position, rotation_transform, origin_for_rotating_around)

            # Apply x reflection
            if x_reflection:
                new_position = reflect_point(new_position, origin_for_rotating_around)

        new_label = gdstk.Label(label.text, new_position, layer=label.layer, texttype=label.texttype,
                                anchor=label.anchor, rotation=label.rotation, magnification=label.magnification, x_reflection=label.x_reflection)
        new_cell.add(new_label)

    return new_cell

and some tests:

def create_temp_output_dir(delete_first=True):
    temp_output_dir = f'{this_files_dir}/temp_output_data'
    if os.path.exists(temp_output_dir) and delete_first:
        shutil.rmtree(temp_output_dir)
    os.makedirs(temp_output_dir, exist_ok=True)
    os.chdir(temp_output_dir)
    return os.path.abspath(temp_output_dir)

def create_test_cells():
    # Create the library
    lib = gdstk.Library()

    # Create the leaf cell with a polygon
    leaf_cell = gdstk.Cell("LEAF")
    polygon = gdstk.rectangle((0, 0), (5, 10), datatype=99, layer=101)
    leaf_cell.add(polygon)
    lib.add(leaf_cell)

    # Create intermediary cells
    intermediate_cell_1 = gdstk.Cell("INTERMEDIATE_1")
    intermediate_cell_1.add(gdstk.Reference(leaf_cell))
    lib.add(intermediate_cell_1)

    intermediate_cell_2 = gdstk.Cell("INTERMEDIATE_2")
    intermediate_cell_2.add(gdstk.Reference(leaf_cell, origin=(5, 5), rotation=math.pi))
    # intermediate_cell_2.add(gdstk.Reference(leaf_cell, origin=(5, 5)))
    lib.add(intermediate_cell_2)

    # Create the top cell
    top_cell = gdstk.Cell("TOP")
    top_cell.add(gdstk.Reference(intermediate_cell_1))
    top_cell.add(gdstk.Reference(intermediate_cell_2, origin=(10, 10)))
    lib.add(top_cell)
    tmp_file_path = create_temp_output_dir()+'/temp.oas'
    print(tmp_file_path)
    lib.write_oas(tmp_file_path)
    return lib

def test_find_offsets_and_rotations():
    lib = create_test_cells()
    top_cell = lib.cells[-1]  # Assuming the last cell is the top cell
    assert top_cell.name == 'TOP'
    # Test case 1: No offset or rotation in intermediary cells
    result = find_leaf_reference_offsets_and_rotations(top_cell, "LEAF")
    assert len(result) == 2

    expected1 = (
        ((0, 0), 0, False, 1, 'INTERMEDIATE_1'),  # Top cell to INTERMEDIATE_1
        ((0, 0), 0, False, 1, 'LEAF'))  # INTERMEDIATE_1 to LEAF
    expected2 = (
        ((10, 10), 0, False, 1, 'INTERMEDIATE_2'),  # Top cell to INTERMEDIATE_1
        ((5, 5), math.pi, False, 1, 'LEAF')  # Final offset and rotation
    )
    # expected3 = (
    #     ((10, 10), 0, 'INTERMEDIATE_2'),  # Top cell to INTERMEDIATE_1
    #     ((5, 5), 0, 'LEAF')  # Final offset and rotation
    # )
    assert result[0] == expected1, f"Expected {expected1}, but got {result[0]}"
    assert result[1] == expected2, f"Expected {expected2}, but got {result[1]}"
    # assert result[2] == expected3, f"Expected {expected3}, but got {result[2]}"

    flat_cell = top_cell.flatten()
    flat_polys = flat_cell.get_polygons(datatype=99, layer=101)
    original_poly =  [cell for cell in lib.cells if cell.name=='LEAF']
    assert len(original_poly) == 1
    original_poly = original_poly[0]

    assert len(flat_polys) == 2
    flat_poly_bbs = []
    for poly in flat_polys:
        bb = poly.bounding_box()
        assert bb in [((0.,0.), (5.,10.)),
                      # ((15., 15.), (20., 25.)),
                      ((10.,5.), (15.,15.)), ]
        flat_poly_bbs.append(bb)

    original_poly_transformed = []
    op_bb = original_poly.bounding_box()
    for transform_list in result:
        new_bb = [list(op_bb[0]), list(op_bb[1])]

        for origin_transform, rotation_transform, x_reflection, magnification, parent_cell in transform_list:
            new_bb[0][0] += origin_transform[0]
            new_bb[1][0] += origin_transform[0]
            new_bb[0][1] += origin_transform[1]
            new_bb[1][1] += origin_transform[1]
            # Apply rotation around the origin
            ll = rotate_point(new_bb[0], rotation_transform, new_bb[0])
            ur = rotate_point(new_bb[1], rotation_transform, new_bb[0])
            # Apply rotation around the origin
            new_bb[0] = min(ll, ur)
            new_bb[1] = max(ll, ur)

        original_poly_transformed.append((tuple(new_bb[0]), tuple(new_bb[1])))
    assert original_poly_transformed == flat_poly_bbs

    # Apply transformations to the LEAF cell and create new transformed cells
    leaf_cell = [cell for cell in lib.cells if cell.name == 'LEAF'][0]
    transformed_cells = [apply_transformations_to_cell(leaf_cell, transform_list) for transform_list in result]

    # Check that the transformed cells match the flattened cell
    for transformed_cell in transformed_cells:
        transformed_polys = transformed_cell.get_polygons(datatype=99, layer=101)
        assert len(transformed_polys) == 1
        transformed_poly = transformed_polys[0]
        transformed_poly.bounding_box() in flat_poly_bbs