Recursive collection of the points in a region or on a layer to calculate the centroid of layer

edited October 2023 in Ruby Scripting

I am using a few methods from the demos and help from chatgpt to collect the centroids of several shapes using a macro. The shapes are contained in a layer, and here is the functional code to get close to what I want. (posted below...)

The trouble I am encountering is how to iterate over shapes added to the region, as in the provided "layout.begin_shapes(cell,layer_indexes[idx])" loop. I am looking to get the corners or coordinates of the polygons, but I am unclear--nor able to find--how to get the coordinates such that I can manipulate them for the purpose of getting the centroid or center of mass of a shape.

Here is a snippet of the code in question, included here for verisimilitude.

def button_clicked(self, checked):
    """ Event handler: "Calculate area" button clicked """
    view = pya.Application.instance().main_window().current_view()
    layout = view.cellview(0).layout()
    cell = view.cellview(0).cell
    #create KLayout region object to use for grabbing shapes
    reg = pya.Region()
    total_area = 0
    com_y = 0
    com_x = 0

    #1/0, 2/0
    layer_input = self.layer_input.selectedItems()
    layer_input = [j.text for j in set(layer_input)]

    layer_indexes = layout.layer_indexes()
    for idx,lyr in enumerate(layout.layer_infos()):
        if str(lyr) in layer_input:
            #Use the Region insert function on a recursive shape iterator
            #that is a function of the layout. This handles all the 
            #complicated work of finding shapes in the current cell.
            reg.insert(layout.begin_shapes(cell,layer_indexes[idx]))

    total_area += reg.area()/1e6

    _points = self.convert_edges(str(reg.corners()))

    centroids = self.centroid_calc(_points)
    print(com_x, com_y)

    # get the area and place it in the qt dialog
    self.area.setText("Total Area: "+str(round(total_area, 2))+", Centroids (x, y): "+str(centroids))
def convert_edges(self, edges):
    import re
    # Use regular expression to extract tuples
    tuple_pattern = r'\(([^)]+)\);'
    tuples = re.findall(tuple_pattern, edges)

    # Initialize two lists to store the extracted values as tuples
    _tuple1 = []
    _tuple2 = []

    # Iterate through the extracted tuples and split them into values
    for tuple_str in tuples:
        values = tuple_str.split(';')
        x1, y1 = map(float, values[0].split(','))
        x2, y2 = map(float, values[1].split(','))
        _tuple1.append((x1, y1))
        _tuple2.append((x2, y2))
    # Print the parsed tuple variables
    print(edges)
    if not _tuple1: return [((0, 0), (0, 0)),]
    return list(zip(_tuple1, _tuple2)) + [(_tuple1[0], _tuple2[-1])]# Wrap around to the first point
def centroid_calc(self, points):
    # Initialize variables for the numerator of the centroid equations
    sum_x = 0.0
    sum_y = 0.0

    # Initialize variable for the area of the region
    area = 0.0

    # Iterate through the points to calculate the centroid
    for pair in points:
        x1, y1 = pair[0]
        x2, y2 = pair[1]            
        # Calculate the cross product of the two points
        cross_product = (x1 * y2) - (x2 * y1)

        # Update the area and the centroid numerator values
        area += cross_product
        sum_x += (x1 + x2) * cross_product
        sum_y += (y1 + y2) * cross_product
    # Calculate the area and centroid coordinates
    area /= 2.0
    centroid_x = sum_x / (6 * area)
    centroid_y = sum_y / (6 * area)

    return round(centroid_x, 2), round(centroid_y, 2)

Comments

  • edited October 2023

    Hi @double0darbo,

    I do not fully understand what you are trying to achieve. The code is overly complex and does not make much sense to me. E.g. why computing corners and then converting them to edges? Plus that path via strings is extremely inefficient.

    Centroids are usually defined for single polygons. The solution in that case was to iterate over the polygons using the recursive shape iterator and computing the centroids from those. The recursive shape iterator delivers polygons plus a transformation that transforms them into the top cell. You can apply this transformation to the centroid after computation. So basic code is this (Caution: not tested):

    # taken from https://en.wikipedia.org/wiki/Centroid - *NOT* ChatGPT
    def compute_centroid_from_polygon(polygon, dbu):
      a = polygon.area()
      cx = cy = 0.0
      for edge in polygon.each_edge():
        d = float(edge.p1.x * edge.p2.y - edge.p2.x * edge.p1.y)
        cx += float(edge.p1.x + edge.p2.x) * d
        cy += float(edge.p1.y + edge.p2.y) * d
      return pya.DPoint(dbu * cx / (6 * a), dbu * cy / (6 * a))
    
    ...
    
    centroids = []
    for iter in layout.begin_shapes(cell,layer_indexes[idx]):
      polygon = iter.shape().polygon    # NOTE: edited, was polygon() which is wrong
      if polygon is not None:
        centroid = iter.dtrans() * compute_centroid_from_polygon(polygon, layout.dbu)
        centroids.append(centroid)
    

    Matthias

  • This is exactly the guidance I needed. I can clean-up the code in the previous post so that it is more succinct if preferred, and I will attach the finished code using your snippets once I get it working.

    To summarize the previous issue: Since I am new to coding using the Klayout macros, I was unable to figure out how to use the begin_shapes method to iterate over multiple shapes contained in a layer to obtain the centroids of each shape in the layer.

  • I am getting the error, 'Polygon' object is not callable, so I had to correct to not expressedly call the polygon. Thank you for the support, as this is now working brilliantly.

    import pya
    from pandas import DataFrame as DF
    
    class AreaCalculator(pya.QDialog):
      """
      This class implements a dialog for calculating area of shapes
      in a layout, then computes their centroid. The calculator adds up shapes in the currently
      selected cell and below.
      """
      # taken from https://en.wikipedia.org/wiki/Centroid - *NOT* ChatGPT
      def compute_centroid_from_polygon(self, polygon, dbu):
        a = polygon.area()
        cx = cy = 0.0
        for edge in polygon.each_edge():
          d = float(edge.p1.x * edge.p2.y - edge.p2.x * edge.p1.y)
          cx += float(edge.p1.x + edge.p2.x) * d
          cy += float(edge.p1.y + edge.p2.y) * d
    
        _x, _y = dbu * cx / (6 * a), dbu * cy / (6 * a)
        return pya.DPoint(round(_x, 2), round(_y, 2))
      def calculate_center_of_mass(self, _centroids):
        _centdf = DF(_centroids)
        total_mass = _centdf[2].sum()
        wt_sum_x = sum(x*m for x, m in zip(_centdf.loc[:, 0], _centdf.loc[:, 2]))
        wt_sum_y = sum(y * m for y, m in zip(_centdf.loc[:, 1], _centdf.loc[:, 2]))
    
        com_x = wt_sum_x / total_mass
        com_y = wt_sum_y / total_mass
    
        return round(com_x, 2), round(com_y, 2)
    
      def button_clicked(self, checked):
        """ Event handler: "Calculate area" button clicked """
    
        view = pya.Application.instance().main_window().current_view()
        layout = view.cellview(0).layout()
        cell = view.cellview(0).cell
        #create KLayout region object to use for grabbing shapes
        reg = pya.Region()
        total_area = 0
        centroids = []
        #1/0, 2/0
        layer_input = self.layer_input.selectedItems()
        layer_input = [j.text for j in set(layer_input)]
    
        layer_indexes = layout.layer_indexes()
        for idx,lyr in enumerate(layout.layer_infos()):
          if str(lyr) in layer_input:
            #Use the Region insert function on a recursive shape iterator
            #that is a function of the layout. This handles all the 
            #complicated work of finding shapes in the current cell and
            #all child cells.
            reg.insert(layout.begin_shapes(cell, layer_indexes[idx]))
            for iter in layout.begin_shapes(cell, layer_indexes[idx]):
              polygon = iter.shape()
              if polygon is not None:
                centroid = iter.dtrans() * self.compute_centroid_from_polygon(polygon, layout.dbu)
                centroids.append(tuple([centroid.x, centroid.y, polygon.area()]))
                # [(_cent.x, _cent.y) for _cent in centroids] [(_cx, _cy, _ca) for _cx, _cy, _ca in centroids]))
    
        total_area += reg.area()/1e6
        print(self.calculate_center_of_mass(centroids))
        # get the area and place it in the qt dialog
        self.area.setText("Total Area: "+str(round(total_area, 2))+", Centroids (x, y): "+str( self.calculate_center_of_mass(centroids) ))
    
    
      def __init__(self, parent = None):
        """ Dialog constructor """
    
        super(AreaCalculator, self).__init__()
    
        self.setWindowTitle("Area Calculator")
    
        self.resize(400, 360)
    
        qt_layout = pya.QVBoxLayout(self)
        self.setLayout(qt_layout)
    
        self.layer_label = pya.QLabel("Select the layers", self)
        qt_layout.addWidget(self.layer_label)
    
        #original implementation using a textbox for getting the layers
        #self.layer_input = pya.QLineEdit('',self)
        #qt_layout.addWidget(self.layer_input)
    
        #second implementation where we pull layers from the file, and
        #display them in a dropdown menu (QListWidget)
        view = pya.Application.instance().main_window().current_view()
        layout = view.cellview(0).layout()
        layers = layout.layer_infos()
        layers = [str(j) for j in layers]
        self.layer_input = pya.QListWidget(self)
        self.layer_input.setSelectionMode(pya.QAbstractItemView.ExtendedSelection)
        self.layer_input.addItems(layers)
        qt_layout.addWidget(self.layer_input)
    
        self.area = pya.QLabel("Press the button to calculate the area", self)
        qt_layout.addWidget(self.area)
    
        button = pya.QPushButton('Calculate area', self)
        button.setFont(pya.QFont('Times', 18, pya.QFont.Bold))
        qt_layout.addWidget(button)
    
        # attach the event handler
        button.clicked(self.button_clicked)
    
    # Instantiate the dialog and make it visible initially.
    # Passing the main_window will make it stay on top of the main window.
    dialog = AreaCalculator(pya.Application.instance().main_window())
    dialog.show()
    
  • Very good. Thanks for sharing the code.

    I did not have the time to test the code above, so apologies for the typo. The line was supposed to be "iter.shape().polygon" (without brackets) and I'd advise you to change your code too. Otherwise it will not work if you have labels in your layout for example.

    Matthias

  • Good note. Thank you.

  • In a similar vein to your labels comment, I am running into an issue with the names of the layers disappearing when I close my .gds file. Is that somehow related to the labels you mentioned?

  • @double0darbo No, GDS cannot store layer names. So when you save to GDS, layer names will be gone. Saving to OASIS is an option to prevent that.

    By "labels" I meant text objects that may be present in your layout. These do not translate to polygons and the "polygon" property delivers "None". If that case is not handled, you will see an error message in that case.

    Matthias

Sign In or Register to comment.