# This code is part of KQCircuits
# Copyright (C) 2025 IQM Finland Oy
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
# License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with this program. If not, see
# https://www.gnu.org/licenses/gpl-3.0.html.
#
# The software distribution should follow IQM trademark policy for open-source software
# (meetiqm.com/iqm-open-source-trademark-policy). IQM welcomes contributions to the code.
# Please see our contribution agreements for individuals (meetiqm.com/iqm-individual-contributor-license-agreement)
# and organizations (meetiqm.com/iqm-organization-contributor-license-agreement).
from typing import List
from dataclasses import dataclass
from kqcircuits.defaults import default_faces, default_face_id
from kqcircuits.pya_resolver import pya
[docs]
@dataclass
class InstanceHierarchy:
"""Data structure holding the instance hierarchy of a single cell instance"""
instance: pya.Instance
trans: pya.DCplxTrans
parent_instances: List[pya.Instance]
top_cell: pya.Cell
[docs]
def get_cell_instance_hierarchy(layout: pya.Layout, cell_index: int) -> List[InstanceHierarchy]:
"""Find all instances of a cell and their transforms (DCplxTrans) in the global coordinate system.
Resolves the full cell hierarchy.
Args:
layout: Layout object
cell_index: Cell index of the cell to find instances of
Returns: list of ``InstanceHierarchy`` structrures describing the cell hierarchy of each instance
"""
to_do = [(cell_index, pya.DCplxTrans(), None, [])]
results = []
# Traverse the cell hierarchy upwards from the current cell, and accumulate the transformations at each hierarchy
# level until we reach the top cell, such that we have the global transform of each instance.
while len(to_do) > 0:
current_cell_index, input_trans, input_inst, parent_instances = to_do.pop(0)
cell = layout.cell(current_cell_index)
has_parents = False
for parent_inst in cell.each_parent_inst():
inst = parent_inst.child_inst()
if input_inst is None:
input_inst = inst
else:
parent_instances = parent_instances + [inst]
trans = inst.dcplx_trans
to_do.append((parent_inst.parent_cell_index(), trans * input_trans, input_inst, parent_instances))
has_parents = True
if not has_parents and input_inst is not None:
results.append(
InstanceHierarchy(
instance=input_inst, trans=input_trans, parent_instances=parent_instances, top_cell=cell
)
)
return results
def _get_instance_primary_face_id(instance: pya.Instance) -> str:
"""Return the primary face id for an instance, or the default KQC face."""
face_ids = instance.pcell_parameter("face_ids")
if isinstance(face_ids, str):
return face_ids
if face_ids is not None:
try:
if len(face_ids) > 0:
return face_ids[0]
except TypeError:
pass
return default_face_id
[docs]
def get_instance_marker_polygons(
instance: pya.Instance, trans: pya.DCplxTrans | None = None, marker_shape_layer: str = "ground_grid_avoidance"
) -> List[pya.DPolygon]:
"""Return ground-grid silhouette polygons for an instance in top-cell coordinates.
Args:
instance: Instance whose geometry should be highlighted.
trans: Optional instance transform in top-cell coordinates. Defaults to ``instance.dcplx_trans``.
marker_shape_layer: Name of the layer to use for marker shapes. Defaults to ``"ground_grid_avoidance"``.
Returns: DPolygons from the instance's primary face marker layer.
"""
layout = instance.layout()
instance_trans = instance.dcplx_trans if trans is None else trans
face_id = _get_instance_primary_face_id(instance)
face = default_faces[face_id]
marker_layer = layout.layer(face[marker_shape_layer])
polygons = []
for shape_iter in instance.cell.begin_shapes_rec(marker_layer):
polygon = shape_iter.shape().dpolygon
if polygon is not None:
polygons.append(instance_trans * shape_iter.dtrans() * polygon)
return polygons