Python exercise

Hi guys.
To avoid checking each point against any other, I thought to use a grid of ‘quads’, that is a rectangular area with its list of points.
And then check distances inside each quad and also from the points into a quad to the points of adjacent quads.
… Don’t know if the logic makes sense, and also if the script follows this logic without errors ( I guess not … it has been written pretty quickly … )
The ‘split’ option is the number of quads used on the larger side of the bounding box of the points.
The script prints an alert message if the quads are too small and no distance gets calculated.
It also tries to use Rhino libraries for input/output only and not for data structure and calculations.

Anyway:

import Rhino
import math

def main():
  # input
  split = 10
  while True:
    gob = Rhino.Input.Custom.GetObject()
    gob.AcceptNothing( True )
    gob.SetCommandPrompt( 'Points ?' )
    gob.GeometryFilter = Rhino.DocObjects.ObjectType.Point
    spind, spopt = gob.AddOptionInteger( 'Split', 
        Rhino.Input.Custom.OptionInteger( split ) )
    gob.GetMultiple( 0, 0 )
    if gob.CommandResult() == Rhino.Commands.Result.Cancel:
      return
    res = gob.Result()
    if res == Rhino.Input.GetResult.Option:
      opind = gob.OptionIndex()
      if opind == spind:
        split = spopt.CurrentValue
    elif res == Rhino.Input.GetResult.Object:
      obrefs = gob.Objects()
      break
    else:
      return
  # turn points into lists [X,Y]
  plist = []
  for obref in obrefs:
    pt = obref.Point().Location
    plist.append( [ pt.X, pt.Y ] )
  print '%d points' % len( plist )
  # get bounding box: points min & max
  mi = plist[ 0 ][ : ]
  ma = plist[ 0 ][ : ]
  for pt in plist[ 1 : ]:
    if pt[ 0 ] < mi[ 0 ]:
      mi[ 0 ] = pt[ 0 ]
    if pt[ 0 ] > ma[ 0 ]:
      ma[ 0 ] = pt[ 0 ]
    if pt[ 1 ] < mi[ 1 ]:
      mi[ 1 ] = pt[ 1 ]
    if pt[ 1 ] > ma[ 1 ]:
      ma[ 1 ] = pt[ 1 ]
  # bbox size
  lenx = ma[ 0 ] - mi[ 0 ]
  leny = ma[ 1 ] - mi[ 1 ]
  # size of 'quads' containing point lists
  lenma = max( lenx, leny )
  stp = lenma / split
  xcnt = int( round( lenx / stp ) )
  ycnt = int( round( leny / stp ) )
  stx = lenx / xcnt
  sty = leny / ycnt
  # Rhino objects drawn for user info
  ids = []
  pla = Rhino.Geometry.Plane.WorldXY
  p0 = Rhino.Geometry.Point3d( mi[ 0 ], mi[ 1 ], 0 )
  p1 = Rhino.Geometry.Point3d( ma[ 0 ], ma[ 1 ], 0 )
  rec = Rhino.Geometry.Rectangle3d( pla, p0, p1 )
  ids.append( Rhino.RhinoDoc.ActiveDoc.Objects.AddCurve(
      rec.ToNurbsCurve() ) )
  Rhino.RhinoDoc.ActiveDoc.Views.Redraw()
  interx = Rhino.Geometry.Interval( 0, stx )
  intery = Rhino.Geometry.Interval( 0, sty )
# pla = Rhino.Geometry.Plane.WorldXY
# pla.OriginX = mi[ 0 ]
# for xind in range( xcnt ):
#   pla.OriginY = mi[ 1 ]
#   for yind in range( ycnt ):
#     rec = Rhino.Geometry.Rectangle3d( pla, interx, intery )
#     ids.append( Rhino.RhinoDoc.ActiveDoc.Objects.AddCurve(
#         rec.ToNurbsCurve() ) )
#     pla.OriginY += sty
#   pla.OriginX += stx
# Rhino.RhinoDoc.ActiveDoc.Views.Redraw()
  # initialize quads grid
  qad = [ [ [] for ind in range( ycnt ) ] for jnd in range( xcnt ) ]
  # storing point into 'quads'
  print '%d X %d = %d quads will be used' % ( xcnt * ycnt, xcnt, ycnt )
  for pt in plist:
    qx = int( ( pt[ 0 ] - mi[ 0 ] ) / stx - 0.001 )
    qy = int( ( pt[ 1 ] - mi[ 1 ] ) / sty - 0.001 )
    qad[ qx ][ qy ].append( pt )
  for ix in range( xcnt ):
    for iy in range( ycnt ):
      print '%d points in quad %d,%d' % ( len( qad[ ix ][ iy ] ), ix, iy )
#   tex = '%d,%d' % ( xind, yind )
#   pnt = Rhino.Geometry.Point3d( pt[ 0 ], pt[ 1 ], 0 )
#   ids.append( Rhino.RhinoDoc.ActiveDoc.Objects.AddTextDot( tex, pnt ) )
# Rhino.RhinoDoc.ActiveDoc.Views.Redraw()
  # initializing values
  mindis = lenx + leny
  pointa = []
  pointb = []
  gid = None
  pla = Rhino.Geometry.Plane.WorldXY
  # main loop on 'quads'
  for ix in range( xcnt ):
    for iy in range( ycnt ):
      pla.OriginX = mi[ 0 ] + stx * ix
      pla.OriginY = mi[ 1 ] + sty * iy
      # drawing 
      if gid:
        Rhino.RhinoDoc.ActiveDoc.Objects.Delete( gid, True )
      rec = Rhino.Geometry.Rectangle3d( pla, interx, intery )
      gid = Rhino.RhinoDoc.ActiveDoc.Objects.AddCurve(
          rec.ToNurbsCurve() )
      Rhino.RhinoDoc.ActiveDoc.Views.Redraw()
      # any point in this quad ?
      if not qad[ ix ][ iy ]:
        print 'skipped quad %d,%d' % ( ix, iy )
        continue
      # points in this quad
      pts = qad[ ix ][ iy ]
      # calc in locad quad
      print 'checking local points in quad %d,%d' % ( ix, iy )
      while len( pts ) > 1:
        p0 = pts.pop()
        for pt in pts:
          dx = abs( p0[ 0 ] - pt[ 0 ] )
          if dx > mindis:
            continue
          dy = abs( p0[ 1 ] - pt[ 1 ] )
          if dy > mindis:
            continue
          dis = math.sqrt( dx * dx + dy * dy )
          if dis < mindis:
            mindis = dis
            pointa = p0
            pointb = pt
      if pointa:
        print 'min. distance so far: %.4f' % mindis
      # indices of near quads to check
      x0 = ix
      x1 = ix + 1 if ix < ( xcnt -1 ) else ix
      y0 = iy
      y1 = iy + 1 if iy < ( ycnt -1 ) else iy
      # calc against near quads
      pts = qad[ ix ][ iy ]
      for qx in range( x0, x1 + 1 ):
        for qy in range( y0, y1 + 1 ):
          if ( ix == qx ) and ( iy == qy ):
            continue
          print 'checking quad %d,%d against quad %d,%d' % ( ix, iy, qx, qy )
          qpts = qad[ qx ][ qy ]
          if not qpts:
            continue
          for pt in pts:
            for qp in qpts:
              dx = abs( qp[ 0 ] - pt[ 0 ] )
              if dx > mindis:
                continue
              dy = abs( qp[ 1 ] - pt[ 1 ] )
              if dy > mindis:
                continue
              dis = math.sqrt( dx * dx + dy * dy )
              if dis < mindis:
                mindis = dis
                pointa = qp
                pointb = pt
          if pointa:
            print 'min. distance so far: %.4f' % mindis
  if gid:
    Rhino.RhinoDoc.ActiveDoc.Objects.Delete( gid, True )
  if ids:
    for gid in ids:
      Rhino.RhinoDoc.ActiveDoc.Objects.Delete( gid, True )
  if not pointa:
    print 'No distance calculated. Please try with a lower split factor'
    return
  Rhino.RhinoDoc.ActiveDoc.Objects.AddLine( 
      Rhino.Geometry.Point3d( pointa[ 0 ], pointa[ 1 ], 0 ),
      Rhino.Geometry.Point3d( pointb[ 0 ], pointb[ 1 ], 0 ) )
  tex = '%.4f' % mindis
  pnt = Rhino.Geometry.Point3d( 
      ( pointa[ 0 ] + pointb[ 0 ] ) / 2.0,
      ( pointa[ 1 ] + pointb[ 1 ] ) / 2.0,
      0 )
  Rhino.RhinoDoc.ActiveDoc.Objects.AddTextDot( tex, pnt )
  Rhino.RhinoDoc.ActiveDoc.Views.Redraw()



main()

Cheers

1 Like

Yes, I’ve done this in the past on huge projects where I had to find matching objects in a population of 10K objects or more. The “divide and conquer” strategy works really well for this sort of stuff, I was able to reduce run time from something like 8 hours for brute force to just minute or two… (you might find a post on the old newsgroup about this) But I was working on finding things that would always be in the same grid square and using a “crossing” window selection to make sure there were overlaps. This strategy works best if there is a generally even distribution of objects over the whole area.

In this case, I might try using a half-overlapping grid to make sure I got everything. It’s always a challenge to find the optimal sized grid, paradoxically, the more grid squares there are, the faster it runs - until you get to the point where the grids start to not have enough members…

All that said, I thought the ideal thing would be to use an rtree - this has been mentioned several times, and there’s even a sample - which I should really try to figure out how to use. It looks pretty simple actually. There are a few threads in Discourse about rtrees as well.

There’s a ton of info out there on this kind of stuff, but I’m feeling rather lazy… :sleeping: Will see if I have more initiative later on today or tomorrow, and if my brain can actually comprehend something of all that… --Mitch

Hi MItch

Yes, exacty.
That’s why the script also checks with points belonging to adjacent squares.

Actually I could call this ‘grid method’ a ‘poor man’s r-tree’ … :wink:
… And it is, in my understanding, much simpler to write down … but I may be wrong.
I never tried to write such a thing ( an r-tree)… maybe it’s time to try to understand it a little more … :smiley:

Thanks for the info !

I am just wondering where David is in all of this. Hasn’t he written the book on this? :smile:
(I don’t get the @ referral working on my phone )

Hey Mitch, we will see what evolves out of this exercise while seeking towards 10.000.000 points in 3D :smiley:

Hi guys, I just saw this thread and gave it a go.
Here is what my script returns:
Shortest distance is 0.4037 units. Generating 500000 points took 38.25 sec. Calculating distances took 41.66 sec.
Shortest distance is 2.5298 units. Generating 20000 points took 2.67 sec. Calculating distances took 1.61 sec.

As you can see the distance calculation is linear, as it should be.
I also added a percent calculator as I can’t stand staring at an empty screen.

Closest Points by Holo.py (1.6 KB)

@Holo
hey jorgen…

i think your algorithm needs a tune up :wink:

(i ran it with 10 points and it looks like 3 other sets are closer than the result given)




[edit] lol… my bad… they’re in 3 dimensions
sorry about that

This also happens using 2D points:

Oh, no… I see what I did wrong… :frowning:
To be continued…

Hmmmm … not sure about that.
Trees are fine when you have a single point and have to find the closest point to it choosing from many points.
But having to find the closest pair of points anywhere in a large cloud of points … :confused:
At least I have not been able to figure a way to do that. :frowning:

So I only tried to automatize a little my script, so that it finds by itsself a size for the little squares.

import Rhino
import math

def main():
  # input
  minstp = 1.0
  draw = False
  write = False
  debug = False
  while True:
    gob = Rhino.Input.Custom.GetObject()
    gob.AcceptNothing( True )
    gob.SetCommandPrompt( 'Points ?' )
    gob.GeometryFilter = Rhino.DocObjects.ObjectType.Point
    spind, spopt = gob.AddOptionDouble( 'MinStepSize', 
        Rhino.Input.Custom.OptionDouble( minstp ) )
    draind, draopt = gob.AddOptionToggle( 'Draw', 
        Rhino.Input.Custom.OptionToggle( draw, 'No', 'Yes' ) )
    priind, priopt = gob.AddOptionToggle( 'Print', 
        Rhino.Input.Custom.OptionToggle( write, 'No', 'Yes' ) )
    bugind, bugopt = gob.AddOptionToggle( 'Debug', 
        Rhino.Input.Custom.OptionToggle( debug, 'No', 'Yes' ) )
    gob.GetMultiple( 0, 0 )
    if gob.CommandResult() == Rhino.Commands.Result.Cancel:
      return
    res = gob.Result()
    if res == Rhino.Input.GetResult.Option:
      opind = gob.OptionIndex()
      if opind == spind:
        minstp = spopt.CurrentValue
      elif opind == draind:
        draw = draopt.CurrentValue
      elif opind == priind:
        write = priopt.CurrentValue
      elif opind == bugind:
        debug = bugopt.CurrentValue
    elif res == Rhino.Input.GetResult.Object:
      obrefs = gob.Objects()
      break
    else:
      return
  # turn points into lists [X,Y]
  Rhino.RhinoDoc.ActiveDoc.Views.Redraw()
  plist = []
  for obref in obrefs:
    pt = obref.Point().Location
    plist.append( ( pt.X, pt.Y ) )
  print '%d points' % len( plist )
  # get bounding box: points min & max
  mi = list( plist[ 0 ][ : ] )
  ma = list( plist[ 0 ][ : ] )
  for pt in plist[ 1 : ]:
    if pt[ 0 ] < mi[ 0 ]:
      mi[ 0 ] = pt[ 0 ]
    if pt[ 0 ] > ma[ 0 ]:
      ma[ 0 ] = pt[ 0 ]
    if pt[ 1 ] < mi[ 1 ]:
      mi[ 1 ] = pt[ 1 ]
    if pt[ 1 ] > ma[ 1 ]:
      ma[ 1 ] = pt[ 1 ]
  # bbox size
  lenx = ma[ 0 ] - mi[ 0 ]
  leny = ma[ 1 ] - mi[ 1 ]
  # first size of 'quads' containing point lists
  qarea = lenx * leny / len( plist )
  stp = math.sqrt( qarea )
  if stp < minstp:
    stp = minstp
  xcnt = int( lenx / stp + 0.1 ) + 1
  ycnt = int( leny / stp + 0.1 ) + 1
  # origin point of quads grid
  orgx = ( mi[ 0 ] + ma[ 0 ] ) / 2.0 - stp * xcnt / 2.0
  orgy = ( mi[ 1 ] + ma[ 1 ] ) / 2.0 - stp * ycnt / 2.0
  org = [ orgx, orgy ]
  # Rhino objects drawn for user info
  bus = []
  ids = []
  if draw:
    endx = orgx + stp * xcnt
    endy = orgy + stp * ycnt
    pla = Rhino.Geometry.Plane.WorldXY
    p0 = Rhino.Geometry.Point3d( orgx, orgy, 0 )
    p1 = Rhino.Geometry.Point3d( endx, endy, 0 )
    rec = Rhino.Geometry.Rectangle3d( pla, p0, p1 )
    ids.append( Rhino.RhinoDoc.ActiveDoc.Objects.AddCurve(
        rec.ToNurbsCurve() ) )
    Rhino.RhinoDoc.ActiveDoc.Views.Redraw()
  # initialize quad grid (and adjust quad size)
  while True:
    if bus:
      for gid in bus:
        Rhino.RhinoDoc.ActiveDoc.Objects.Delete( gid, True )
      Rhino.RhinoDoc.ActiveDoc.Views.Redraw()
    inter = Rhino.Geometry.Interval( 0, stp )
    qad = [ ( [ None ] * ycnt ) for ind in range( xcnt ) ]
    # storing point into 'quads'
    print 'using %d X %d = %d quads %.4f X %.4f' % ( 
        xcnt, ycnt, xcnt * ycnt, stp, stp )
    # debugging
    if debug:
      pla = Rhino.Geometry.Plane.WorldXY
      pla.OriginX = orgx
      for xind in range( xcnt ):
        pla.OriginY = orgy
        for yind in range( ycnt ):
          rec = Rhino.Geometry.Rectangle3d( pla, inter, inter )
          bus.append( Rhino.RhinoDoc.ActiveDoc.Objects.AddCurve(
              rec.ToNurbsCurve() ) )
          pla.OriginY += stp
        pla.OriginX += stp
      Rhino.RhinoDoc.ActiveDoc.Views.Redraw()
    redo = False
    if write:
      print 'loading quads: start'
    for pt in plist:
      qx = int( ( pt[ 0 ] - orgx ) / stp )
      qy = int( ( pt[ 1 ] - orgy ) / stp )
      if qad[ qx ][ qy ]:
        qad[ qx ][ qy ].append( pt )
        if  ( len( qad[ qx ][ qy ] ) == 5 ) and ( stp / 2.0 > minstp ):
          stp /= 2.0
          xcnt *= 2
          ycnt *= 2
          if write:
            print 'loading stopped: step reset to %.4f' % stp
          redo = True
          break
      else:
        qad[ qx ][ qy ] = [ pt ]
      # debugging
      if debug:
        tex = '%d,%d' % ( qx, qy )
        pnt = Rhino.Geometry.Point3d( pt[ 0 ], pt[ 1 ], 0 )
        bus.append( Rhino.RhinoDoc.ActiveDoc.Objects.AddTextDot( tex, pnt ) )
      Rhino.RhinoDoc.ActiveDoc.Views.Redraw()
    if write:
      print 'loading quads: end'
    if not redo:
      break
  # some info
  if write:
    for ix in range( xcnt ):
      for iy in range( ycnt ):
        if qad[ ix ][ iy ]:
          print '%d points in quad %d,%d' % ( len( qad[ ix ][ iy ] ), ix, iy )
  if debug:
    return
  # initializing values
  mindis = stp * 2.0
  pointa = []
  pointb = []
  gid = None
  pla = Rhino.Geometry.Plane.WorldXY
  # main loop on 'quads'
  for ix in range( xcnt ):
    for iy in range( ycnt ):
      pla.OriginX = orgx + stp * ix
      pla.OriginY = orgy + stp * iy
      # drawing 
      if gid:
        Rhino.RhinoDoc.ActiveDoc.Objects.Delete( gid, True )
      if draw:
        rec = Rhino.Geometry.Rectangle3d( pla, inter, inter )
        gid = Rhino.RhinoDoc.ActiveDoc.Objects.AddCurve(
            rec.ToNurbsCurve() )
        Rhino.RhinoDoc.ActiveDoc.Views.Redraw()
      # any point in this quad ?
      if not qad[ ix ][ iy ]:
        if write:
          print 'quad %d,%d skipped' % ( ix, iy )
        continue
      # points in this quad
      pts = qad[ ix ][ iy ]
      # calc in locad quad
      if write:
        print 'checking local points in quad %d,%d' % ( ix, iy )
      while len( pts ) > 1:
        p0 = pts.pop()
        for pt in pts:
          dx = abs( p0[ 0 ] - pt[ 0 ] )
          if dx > mindis:
            continue
          dy = abs( p0[ 1 ] - pt[ 1 ] )
          if dy > mindis:
            continue
          dis = math.sqrt( dx * dx + dy * dy )
          if dis < mindis:
            mindis = dis
            pointa = p0
            pointb = pt
      if pointa and write:
        print 'min. distance so far: %.4f' % mindis
      # indices of near quads to check
      x0 = ix
      x1 = ix + 1 if ix < ( xcnt -1 ) else ix
      y0 = iy
      y1 = iy + 1 if iy < ( ycnt -1 ) else iy
      # calc against near quads
      pts = qad[ ix ][ iy ]
      for qx in range( x0, x1 + 1 ):
        for qy in range( y0, y1 + 1 ):
          if ( ix == qx ) and ( iy == qy ):
            continue
          if write:
            print 'checking quad %d,%d against quad %d,%d' % ( ix, iy, qx, qy )
          qpts = qad[ qx ][ qy ]
          if not qpts:
            continue
          for pt in pts:
            for qp in qpts:
              dx = abs( qp[ 0 ] - pt[ 0 ] )
              if dx > mindis:
                continue
              dy = abs( qp[ 1 ] - pt[ 1 ] )
              if dy > mindis:
                continue
              dis = math.sqrt( dx * dx + dy * dy )
              if dis < mindis:
                mindis = dis
                pointa = qp
                pointb = pt
          if pointa and write:
            print 'min. distance so far: %.4f' % mindis
  if gid:
    Rhino.RhinoDoc.ActiveDoc.Objects.Delete( gid, True )
  if ids:
    for gid in ids:
      Rhino.RhinoDoc.ActiveDoc.Objects.Delete( gid, True )
  if not pointa:
    print 'No distance calculated. Please try with a higher step'
    return
  Rhino.RhinoDoc.ActiveDoc.Objects.AddLine( 
      Rhino.Geometry.Point3d( pointa[ 0 ], pointa[ 1 ], 0 ),
      Rhino.Geometry.Point3d( pointb[ 0 ], pointb[ 1 ], 0 ) )
  tex = '%.4f' % mindis
  pnt = Rhino.Geometry.Point3d( 
      ( pointa[ 0 ] + pointb[ 0 ] ) / 2.0,
      ( pointa[ 1 ] + pointb[ 1 ] ) / 2.0,
      0 )
  Rhino.RhinoDoc.ActiveDoc.Objects.AddTextDot( tex, pnt )
  Rhino.RhinoDoc.ActiveDoc.Views.Redraw()



main()

The script is by no means fast.
Trying it on your file with 2000 points, it takes a few seconds.

A strange thing I noticed is that it takes that time (on my slow pc) the first time I run it.
Then it becomes much slower until I restart Rhino …

I think Python/IronPython might not be the best tool for number crunching …
Sure I have a lot to learn about optimizing Python code for speed … :frowning:

I am working on my own “grid” version - just as a mental exercise - and it’s indeed straining my tiny brain… Disappointing to see that your script is not faster, means mine might not be either. Will try to finish it later though, but now need some air and sunshine… --Mitch

Why ?
No, no …
I hope yours will be much faster so that I be able to learn how to improve the performance of my scripts … :smile:

just out of curiosity–

if dealing with a ginormous amount of points, would it be worth it (or possible) to do a grid using all available processors?

I suppose. The problem with gridding in this particular case (closest 2 points) is you also need to consider the “edge case” where the two closest points fall in two different grids - one just inside each border. So there will need to be some overlap - even if you aren’t parallel processing - and this makes the task a bit more time consuming and complex.

I guess you could parallel process all the grid “buckets” and compare the result. If there are enough points, I suppose the extra overhead in dealing with the parallel processing would be more than offset.

–Mitch

how are you trying it?

my first idea (which probably isn’t the best idea :wink: )would be to do the grid, find the closest measurement within those squares, then use that measurement to determine how wide of an overlap strip to run over the seams…

(or- the overlap size could probably change depending on the data already collected)
?

Hi Jeff

Any idea about the way to obtain the points belonging to the overlapping strips ?
My script uses no overlap and still the code to choose the points belonging to the little squares takes most part of the time … :disappointed:
And I think the data structure containing the squares should be heavily optimized, and that would slow down the script even more …
… But obviously there may be a stupid error in my code …

not really other than sorting by coordinates but i’m thinking that’s going to take time…

maybe rs.WindowPick() could do something here?
i tried it but it crashes rhino on mac. :crying_cat_face:

OK, I’m just going to throw my current state of the thing out here…

  1. For large amounts of points, it’s much faster with the grids than my previous (fast) script - I’m doing 648,000 points in a little over 72 seconds. On my work computer Friday, 500,000 took 27.5 minutes with the other script. I’m not counting the time it takes to select and create the initial point arrays - actually quite long, around 35 sec. - it would be the same in any case.

  2. It’s actually slower for smaller amounts of points than my original script, like with 2000 points. Too much overhead.

  3. However, what I have not put in is the overlap part yet. So there is a possibility that it will occasionally get the wrong answer.

  4. There definitely seems to be a “sweet spot” for the number of grids. I did experiments with 25, 100, 400 and 900. The fastest were 100 and 400 (almost equal, 400 was 1 second faster.). So looks like 1500-2000 points per grid is good, I put in a grid size auto calculate for that.

  5. The grids are calculated manually, I could of course try some sort of window selection (I have done in the past) but for the moment I wanted to not rely on screen selection if possible. I am using a dictionary function to store the points in “bins” by x,y coordinates and retrieve them. It currently does not do a 3d grid, although the points can be in 3d.

Anyway, I have played around with it enough for now, I may look at again tomorrow…

–Mitch

FindClosestPointsByGrid.py (3.1 KB)

(from a phone-- haven’t tried your latest version)

it would probably be real easy to put a counter in there which tells how many distance measurements it makes… with 2000 points and the ‘naive’ approach, it’s doing 2million individual measurements… a counter would tell you how many less measurements are happening with the grid.

(and may or may not provide some insight/feedback towards narrowing down the sweet spot)

@Helvetosaur,

I haven’t got time to try this but i’m really curious to know if using a python Deque would speed things up in your script in a meaningful way. Since you are using lists and pops to iterate through the points maybe you could try implementing a deque instead of a list and see if it is faster (deque’s are apparently optimized for left / right pops and appends).

Just a thought! :wink: