Optimize GHPython random 2D points component for faster population?

Hi,

I’ve ported the Population method from the Vector GHa lib, mentioned by @DavidRutten in this thread to GHPython. Now I’m looking for advice, on how to optimise the code in order to make it run faster, since I need it to produce about 10 000 to 20 000 points (tuples with 3 coordinates) for me.

Here are the current speeds of the Python component:

Population Time Percentage
10 6 ms 28 %
100 853 ms 85 %
1000 1.3 min 100 %
10 000 60 min >100 %

This is what the code looks like! The most interesting section to optimise might be the populate() method of the Population class.

import Rhino.Geometry as rg
import random
import math
import sys


class Population:
    """A population of random points within a region.
    
    To use:
    >>> ppl = Population(10, (25,25))
    >>> ppl.populate()
    
    >>> ppl.get_coordinates()
    [(x0, y0, z0), (x1, y1, y2), ..., (x10, y10, z10)]
    
    >>> ppl.get_points()
    [Rhino.Geometry.Point3d, Rhino.Geometry.Point3d, ..., Rhino.Geometry.Point3d]
    """
    
    
    def __init__(self, count, region):
        self.count = int(count)
        self.region = region
        self.population = []
        
    
    def distance_to(self, ptA, ptB):
        """Calculate the distance between two 3-dimensional points.

        Args:
          ptA: 3-dimensional point A (Rhino.Geometry.Point3d, tuple or list)
          ptB: 3-dimensional point B (Rhino.Geometry.Point3d, tuple or list)
        Returns:
          The distance between point A and point B.
        """
        dist = math.sqrt((ptA[0] -  ptB[0])**2 + (ptA[1] -  ptB[1])**2 + (ptA[2] -  ptB[2])**2)
        return dist
    

    def next_point(self, domain):
        """Constructs a random 2-dimensional point coordinate.
        
        Args:
          domain: Tuple of 2 maximum values in x and y. 
        Returns:
          Returns a tuple of random 2D point coordinates.
        """
        maxX = domain[0]
        minX = -maxX
        maxY = domain[1]
        minY = -maxY
        return (random.uniform(minX, maxX), random.uniform(minY, maxY), 0)


    def populate(self):
        """Populates a 2-dimensional region with random 2-dimensional points."""
        distance_threshold = sys.maxint
    
        for i in range(self.count):
            if (len(self.population) == 0): # first point
                pt = self.next_point(self.region)
                self.population.append(pt)
            
            else: # other points
                attempts = int(max(50, math.sqrt(i)))
                max_dist = -sys.maxint
                max_pt = 0
            
                for k in range(attempts):
                    if (k > 100) and (max_dist > distance_threshold * 0.9):
                        break
                    elif (k > 50) and (max_dist > distance_threshold * 0.92):
                        break
                    elif (k > 25) and (max_dist > distance_threshold * 0.95):
                        break
                
                    pt = self.next_point(self.region)
                    dist = sorted(self.distance_to(pt, prev_pt) for prev_pt in self.population)[0]
                    if dist == sys.maxint:
                        break
                    if dist > max_dist:
                        max_dist = dist
                        max_pt = pt
                        if (max_dist > distance_threshold * 1.25):
                            break
        
                distance_threshold = max_dist
                self.population.append(max_pt)
    
    
    def get_coordinates(self):
        """Returns a list of random 2-dimensional point coordinates witihn a region."""
        return self.population
    
    
    def get_points(self):
        """Returns a list of Rhino.Geometry.Point3d witin a redion."""
        pts = [rg.Point3d(c[0], c[1], c[2]) for c in self.population]
        return pts


# Variables
count = 10 # number of random points to produce
domain = (5.0, 5.0) # max. extends of the region in x and y

# Initialise class
ppl = Population(count, domain)
ppl.populate()

# Output
a = ppl.get_points()

(To test the script, just copy and paste the python code into a GHPython component and run it.)

Any advice is welcome and appreciated!

:wink:

Hi how about disabling redraw?

import rhinoscriptsyntax as rs
rs.EnableRedraw(False)
...code here...
rs.EnableRedraw(True)

Could you refactor the code to use generators instead of creating a massive list of points? You could keep a list of guid if you need to keep track of them

1 Like

Thanks @Dancergraham!

Could you explain a little, what you mean by generators? Would keeping a list of GUIDs be less expensive than keeping a list op tuples with 3 float values each?

Generators let you generate objects one by one rather than storing them all in memory. They are defined much like functions but use yield rather than return:

2 Likes

No saving in using the guid rather than the tuple

import rhinoscriptsyntax as rs
import sys
import Rhino.Geometry as rg


ptcoords = (1000.,1000.,1000.)
pt = rs.AddPoint(ptcoords)
rhpt = rg.Point3d(*ptcoords)

print (ptcoords, sys.getsizeof(ptcoords))
print (pt, sys.getsizeof(pt))
print (rhpt, sys.getsizeof(rhpt))

((1000.0, 1000.0, 1000.0), 16)
(<System.Guid object at 0x0000000000000034 [7f7452f7-9235-406d-ad57-85c8fb5a4684]>, 16)
(<Rhino.Geometry.Point3d object at 0x0000000000000035 [1000,1000,1000]>, 16)

1 Like

Thanks, great explanation!