SelDup equivalent in python

python
rhino5

#1

Pretty new to rhino python. Wonder if there is a way to eliminate duplicates.
I have a recursion that essentially copies an object along three directions, like a tripod. After each leg reaches a number of units, it starts a new branch. Problem is that there are many duplicates in this recursive branching. I used lists of 3 vectors as identifier of location and then oriented my object to every location. The 3 vector list help me with the individual posture of units. Python can’t tell if a list of 3 vectors is already in the larger list/set, to which I’m orienting my object in the end of the script.
Any suggestions to avoid duplicates? I learned that i could use rs.command(’_SelDup’) and rs.command(’_delete’) to manually clean up at the end but the script wastes so much time on placing those duplicate units.


#2

Sounds like your “larger list/set” should be a python set rather than a list - python will take care of no duplicates in a set.


#3

Thanks that’s something new for me.
However, python can’t tell the difference (or sameness) between two lists of 3 vectors. When I try set(mylist_with_dup), error comes up saying the list is unhashable.


#4

@Will_Wang,

you might try to cull duplicate vectors (or points) using rs.CullDuplicatePoints method.

c.


#5

True, you can’t hash lists. It sounds like your data would be better stored as tuples, if you always use 3 vectors per instance.

consider the following:

import rhinoscriptsyntax as rs

vectorOne = rs.VectorCreate((0,0,0),(3,4,5))
vectorTwo = rs.VectorCreate((1,1,1),(6,8,10))
vectorThree = rs.VectorCreate((2,2,2),(9,12,15))
vectorFour = rs.VectorCreate((2.0,2.0,2.0),(9.0,12.0,15.0))

tupleOne = (vectorOne, vectorTwo)
tupleTwo = (vectorOne, vectorThree)
tupleDup = (vectorOne, vectorTwo)
tupleDup2 = (vectorOne, vectorFour)


setOne = set([tupleOne, tupleTwo])
setTwo = set([tupleOne, tupleTwo, tupleDup, tupleDup2])

print(setOne == setTwo)

this evaluates to

True

This will probably be very quick & efficient compared to selDup.


#6

After I wrote that example, I started wondering about floating-point precision and how close vectors should be to count as duplicates. Since this is being handled in python rather than Rhino document precision doesn’t really come into play. Quick testing reveals that vectors within 1E-12 (very small) don’t count as duplicates, but 1E-16 differences are treated as equal.

import rhinoscriptsyntax as rs

vectorOne = rs.VectorCreate((0,0,0),(3,4,5))
vectorTwo = rs.VectorCreate((1,1,1),(6,8,10))
vectorThree = rs.VectorCreate((2,2,2),(9,12,15))
vectorFour = rs.VectorCreate((2.0,2.0,2.0),(9.0,12.0,15.0))

epsilon = 1E-12
vectorFive = rs.VectorCreate((2+epsilon,2,2),(9,12,15))

tinyEpsilon = 1E-16
vectorSix = rs.VectorCreate((2+tinyEpsilon,2,2),(9,12,15))

tupleOne = (vectorOne, vectorTwo)
tupleTwo = (vectorOne, vectorThree)
tupleDup = (vectorOne, vectorTwo)
tupleDup2 = (vectorOne, vectorFour)
tupleDupClose = (vectorOne, vectorFive)
tupleDupSuperClose = (vectorOne, vectorSix)

setOne = set([tupleOne, tupleTwo])
setTwo = set([tupleOne, tupleTwo, tupleDup, tupleDup2])
setThree = set([tupleOne, tupleTwo, tupleDup, tupleDup2, tupleDupClose])
setFour = set([tupleOne, tupleTwo, tupleDup, tupleDupSuperClose])

print(setOne == setTwo)     # I get True
print(setOne == setThree)   # I get False
print(setOne == setFour)    # I get True

#7

thanks Owen this seems a viable solution. I’ll have to try it. Meanwhile I wonder what’s a good way to deal with float numbers. my vectors come from series of transformations that involves trigonometry so their 3 number tuple representation will have a lot of floats. I read that python has the tendency to have slightly different representations of the same fraction number. How can I avoid it?


#8

Hi Will - here are a few answers. I got curious and had to do a little investigating…

  1. Don’t worry about it. All the numbers in Rhino are floats, that’s not a problem necessarily.

  2. Fractions are tricky for computers. This isn’t a python issue, anytime you are storing fractional value as a series of 1s and 0s it gets complicated. https://en.wikipedia.org/wiki/Floating-point_arithmetic is a good start. If you really want to get into it try: http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

  3. Rhino has to worry about this issue too - that’s why each document has an “absolute tolerance” defined. If you give Rhino two points (each created by a different crazy series of trig transformations) and ask Rhino if they are the same (say, with selDup) then Rhino uses the document tolerance to answer. Say you create one point at (1.0mm,2.0mm,3.0mm) and one point at (1.0mm,2.0mm,3.00001mm) - if your document tolerance is set to 0.1mm then selDup will classify them as coincident and select one. If your document tolerance is set to 0.0000000001mm then they will be seen as distinct.

  4. Rhinoscript has some tools for dealing with very close values - see, for example rs.IsVectorTiny(). A test of vector equality between vecA and vecB could be:
    rs.IsVectorTiny(rs.VectorCrossProduct(vecA, vecB)).

  5. It also depends on the scale of what you’re trying to do. rs.IsVectorTiny looks like it is hard coded to 1E-12, which for architectural applications is super small. Maybe you only need 1mm accuracy, or .1mm. In this case I would do all your transformations at full floating point accuracy, and then round the final value to your (lower) actual accuracy. Though technically imperfect(*) this approach is probably OK. Here’s a quick example:

     import rhinoscriptsyntax as rs
     import Rhino.Geometry
     
     #copy these 3 functions:
     def vector_round(vec, tolerance = 2):
         return Rhino.Geometry.Vector3d(
             round(vec.X,tolerance),
             round(vec.Y, tolerance),
             round(vec.Z, tolerance))
         
     def tuple_3_vectors_rounded(tup):
         return (vector_round(tup(0)), vector_round(tup(1)), vector_round(tup(2)))
         
     
     def append_rounded_tuple(workingSet, newTuple, tolerance = 2):
         workingSet.add( (vector_round(newTuple[0], tolerance),
                             vector_round(newTuple[1], tolerance),
                             vector_round(newTuple[2], tolerance) ) )
                             
     #these are placeholders for your vectors                        
     vectorOne = Rhino.Geometry.Vector3d(1,1,1)
     vectorTwo = Rhino.Geometry.Vector3d(2,2,2)
     vectorThree = Rhino.Geometry.Vector3d(1,0,0)
     
     #this is where you'll store your tuples of vectors
     output = set([])
     
     #do some stuff: this is a placeholder
     for i in range(6):
         vectorThree = rs.VectorRotate(vectorThree, 90, [0,0,1])  
    
         #once you do your operations, call this function to add the new output to the set
         append_rounded_tuple(output, (vectorOne, vectorTwo, vectorThree))
         print(i, ": ", len(output))
     
     #a few tests:
    
     vectorOne = rs.VectorCreate((1,1,1),(0,0,0))
     
     #vectorTwo is created by rotating vectorOne 360 degrees
     vectorTwo = rs.VectorRotate(vectorOne, 360, [0,0,1])
     
     #vectorThree is created by rotating vectorOne .1 degrees, 3600 times
     #introduces small rounding error
     vectorThree = vectorOne
     for i in range(3600):
         vectorThree = rs.VectorRotate(vectorThree, .1, [0,0,1])
     
     vectorFour = Rhino.Geometry.Vector3d(6,6,6)
     
     print(rs.VectorCrossProduct(vectorOne, vectorTwo))     #0,0,0
     print(rs.VectorCrossProduct(vectorOne, vectorThree))   #3 small numbers
     
     print(len(set([vectorOne, vectorTwo, vectorThree])))  # 2 (rounding error from 3)
     print(len(set([vector_round(vectorOne),vector_round(vectorTwo),vector_round(vectorThree)])))  # 1 (differences rounded off)
    

(*) typically you shouldn’t test for equality of rounded numbers, for example 1.499999 and 1.500001 are very close but may round away from another. In cases like this where you are rounding off many orders of magnitude (e.g. from 1e-12 to 1e-2) I feel like it’s probably fine.


Iterative Method - Faster - C#
#9

You typically calculate the difference between the two and check that this is less than some defined tolerance value, if it is they may be considered as being the “same”. Same for points, where you would calculate the distance between the two and check that this is less than a tolerance distance.

Edit: I forget exactly how it goes for vectors, but as I recall a quick way of checking equality by one value is to get the length of the cross product like so (don’t hold me to that though):

import Rhino as rc
vecDiff = rc.Geometry.Vector3d.CrossProduct(vecA,vecB).Length 

#10

thanks AndersDeleuran
I thought about scripting the comparison manually, too, instead of something like “if a in b”. Bust as my recursion grows, it becomes really slow to compare a set of three vectors against each element of a huge list of 3 vector sets (i dont necessary mean the set() type in python)


#11

Just as a side note: Recursion can be inherently slow in Python (depending in how you implement your algorithm). If possible, it is in my experience almost always faster to implement an iterative algorithm instead.

Edit: Attached a simple example demonstrating this and how to avoid it using memoization (using GHPython):

170330_GHPython_CostOfRecursion_00.gh (7.3 KB)


#12

So what if a script generates duplicates of breps? Is there a method in Python or C# that can be called to eliminate them within the script flow?


(Dale Fugier) #13

Other than keeping track of the duplicates and deleting them when you are finished? I’m not sure I fully understand the question…

– Dale


#14

Yes. I generated a bunch of Breps, some of which are duplicates of others. Now I need to feed these breps into another script to export CSV. It will be nice to automate this rather than the sequence of bake ->_SelDup -> Delete -> reassign unique breps back into GH

Also I tried this and it freezes up the GH canvas. I can still click on buttons on ribbon but can’t pan around canvas or edit components…


(Giulio Piacentino) #15

In Rhino WIP, there is now rs.CompareGeometry, which can be used to compare duplicate geometry.
If you have access to that, I can give an example. However, in general it is much better to avoid creating duplicate geometry in the first place.

I hope this helps, please let me know,

Giulio


Giulio Piacentino
for Robert McNeela & Assuociates
giulio@mcneel.com