## Polite Python shrub

April 24th, 2009A 100-pyramid Pythonized polite shrub — 500 took too long, and crashed Maya.

Gory details (including code) follow:

Right, so I made a Python version of the polite shrub by tweaking the Pymel polite worm, and tested it against the MEL version. It’s slow, as I suspected. Real slow.

I’m learning things, also slowly. In Pymel, variable1 = variable2 doesn’t just set values; it turns variable1 into an auto-updating pointer to variable2. Why you’d want this, when you could just use variable2, I’m not sure, but knowing this has eliminated at least one embarrassing structural flaw in the code.

On the other hand, the MEL eats memory, and I smell a leak somewhere — Maya’s usage climbs steadily through the running, far more than I would have guessed necessary to store 500 strings, and if you set the number of iterations too high, it runs out of ram and bails. The Python is better-behaved, though my fears of long running times were well-founded: for 500 pyramids, the MEL took a minute 10 seconds; the Pymel took 36 minutes, finished, and crashed.

It’s not so bad for shorter runs, but the time increases exponentially: it can do 50 in 1 minute, 100 in 3, 200 in 9… etc. Toward the end of a 500-run, it can take upwards of 15 seconds to place a new cone, if it’s in a dense area.

So, yeah. It might just be the Pymel, but I suspect something’s still not quite on. Suspiciously, the Pymel also makes a more tightly-packed structure, resulting in more lithop-type near-intersections, as though the MEL’s collision detection is comparatively over-sensitive. This may be related to the longer running time of the Python: for higher iteration values, if everything’s more tightly-packed there are more pyramids to check for every growth.

Anyway. Possibly-broken code follows, set at 100 iterations, which takes about 3 minutes on my machine.

# start: semi-polite python worm from pymel import * import pymel.core.datatypes as dt import random startTime=timerX() # find the center of a face - aka the "centroid" def centerOfFace(face): # get locations of all the face's vertices pos = face.getPoints(space="world") returnVec = [0, 0, 0] # add them all together for vec in pos: returnVec += vec # divide by # of vectors for average returnVec /= len(pos) return returnVec # distange between two vectors def distanceBetween(a, b): a = dt.Vector(a) b = dt.Vector(b) return length(a-b) # return length([a[0]-b[0], a[1]-b[1], a[2]-b[2]]) def getNormal(someFace): fc = centerOfFace(someFace) # returns normal relative to face at origin norm = someFace.getNormal(space='world') returnVec = [fc[0]+norm[0], fc[1]+norm[1], fc[2]+norm[2]] return returnVec # detect sidedness of a point relative to a face def isInFront(point, face): faceNorm = getNormal(face) facePos = centerOfFace(face) if (getVectorAngle(point, faceNorm, facePos) <= 90): return 1 else: return 0 # get angle between two vectors def getVectorAngle(aLoc, bLoc, orig): aLoc = dt.Vector(aLoc) bLoc = dt.Vector(bLoc) orig = dt.Vector(orig) cLoc = aLoc-bLoc cLocFloor = round(cLoc, 4) if (cLocFloor[0]==0 and cLocFloor[1]==0 and cLocFloor[2]==0): return 0 else: # normalize locations to the face center aNorm = aLoc-orig bNorm = bLoc-orig returnAngle = degrees(angle(aNorm, bNorm)) # round answer down to two decimal places returnAngle = round(returnAngle, 2) return returnAngle # get angle between two vertices with respect to a third point def getVertexAngle(aVert, bVert, orig): aVec = dt.Vector(pointPosition(aVert)) bVec = dt.Vector(pointPosition(bVert)) return getVectorAngle(aVec, bVec, orig) # find the centroid of a cone # 1/4 of the way from the base to the peak def centerOfCone(cone): coneLoc = cone.t.get() coneLoc = datatypes.Vector(coneLoc) topLoc = pointPosition(cone.vtx[3]) # 3 = peak topLoc = datatypes.Vector(topLoc) center = ((topLoc-coneLoc)/4)+coneLoc return center # find nearby cones def checkProximity(conePos): ids = [] global coneList for i in coneList: iPos = centerOfCone(i) iDist = distanceBetween(iPos, conePos) # detection radius: 2r = 2 if (iDist < 0.70710678118654752440084436210485): # 1/2*sqrt(2) : definite intersection return "fail" elif (iDist < 2.1213203435596425732025330863145): # 2/3*sqrt(2) # possible intersection, add index to list of suspects ids.append(i) return ids def alignPyramids(pyrA, pyrB, fc): # pick a vertex on each pyramid newVertex = pyrA+".vtx[0]" # 0 is on the base oldVertex = pyrB+".vtx[3]" # 3 is the tip # get angle between two vertices angle = getVertexAngle(newVertex, oldVertex, fc) # if not already aligned if (angle > 0): # rotate new to align the corners xform(pyrA, r=1, os=1, ro=[0, angle, 0]) # check the result angle2 = getVertexAngle(newVertex, oldVertex, fc) # if still not aligned, go the other way twice if (angle2 > 0): angle3 = angle * -2 xform(pyrA, r=1, os=1, ro=[0, angle3, 0]) # do pyramids intersect? def pyramidsIntersect(pyrA, pyrB): # get all vertexes in pyrA pos = pyrA.getPoints(space="world") for i in pos: # for each point in pyrA if isInPyramid(i, pyrB): return 1 # not yet? try the other way around pos = pyrB.getPoints(space="world") for i in pos: if isInPyramid(i, pyrA): return 1 # made it this far? then fail return 0 def isInPyramid(point, pyramid): for face in pyramid.faces: if (isInFront(point, face)): return 0 # it's outside # if it's behind all four faces, it's inside return 1 def copyCone(old): # make a list of faces on old that aren't the base faces = [1, 2, 3] new = '' while (len(faces) > 0): # pick a random face r = random.choice(faces) # strike face off the list faces.remove(r) selectedFace = old.f[r] fc = centerOfFace(selectedFace) # copy cone and move to selected face dupe = duplicate(old) new = dupe[0] xform(new, a=1, t=[fc[0], fc[1], fc[2]]) # aim new at face with a temporary normal constraint tmpConst = normalConstraint(selectedFace, new, aim=(0, 1, 0)) delete(tmpConst) alignPyramids(new, old, fc) # collision detection - is area in new occupied? # reference coneList global coneList # check for other cones in search radius conePos = centerOfCone(new) if (len(coneList) > 1): suspects = checkProximity(conePos) if (suspects == "fail"): delete(new) # start over new = "fail" else: for i in suspects: if (pyramidsIntersect(new, i) == 1): # intersection found, try another face delete(new) new = "fail" break # no intersections? copy is good, clear facelist if (new == 'fail'): continue else: faces = [] else: faces = [] # facelist empty? return new coneList = [] def doit(): # make a tetrahedron new = polyCone(r=1, h=1.414214, sx=3)[0] # move its pivot point to the bottom face xform(rp=(0, -0.707107, 0)) # move to origin and freeze xforms xform(t=(0, 0.707107, 0)) makeIdentity(apply=True, t=1) # initialize cone list global coneList coneList = [new] growList = [new] totalCones = 100 # main creation loop for i in range(totalCones-1): while (1 == 1): # pick a random cone in growList growCone = random.choice(growList) # attempt to grow a new cone new = copyCone(growCone) # if no growth spaces open on that cone if (new == "fail"): # remove it from the coneList growList.remove(str(growCone)) else: # success! add new cone to list coneList.append(new) growList.append(new) setKeyframe(new, attribute = "visibility", v = 0, t = 1) setKeyframe(new, attribute = "visibility", v = 1, t = i+2) break # delete all bottom faces to lighten the load for i in coneList: delete(i.f[0]) del coneList del growList doit() select(None) totalTime = timerX(startTime=startTime) print("Total Time: "+str(totalTime)) # end« previously: Anchored | Home | next: Subprime »

April 25th, 2009 at 1:40 pm

variable1 = variable2 doesnâ€™t just set values; it turns variable1 into an auto-updating pointer to variable2.In Python this is usually the difference between a mutable type and an immutable type. Python doesn’t have variables so much as it has labels for values. Literals are often implemented as singletons. So, if you have two variables which are the letter ‘a’ for example, you may not worry about if they are the same ‘a’ – why would you?

>>> one = 'a'

>>> two = 'a'

>>> one is two

True

This can vary depending on how you get the value.

>>> one = 'definitely'

>>> two = 'definitely not'

>>> three = two.split()[0]

>>> one

'definitely'

>>> two

'definitely not'

>>> three

'definitely'

>>> one is three

False

Ontologically the difference is between A is B and A == B. The first tests identity and the second tests equivalence.

The primary gotcha for most people with this is lists – they are mutable.

>>> a = range(4)

>>> b = a

>>> a

[0, 1, 2, 3]

>>> b

[0, 1, 2, 3]

>>> a[0] = 'alphabet'

>>> a

['alphabet', 1, 2, 3]

>>> b

['alphabet', 1, 2, 3]

* http://docs.python.org/reference/datamodel.html

* http://en.wikipedia.org/wiki/Singleton_pattern

April 26th, 2009 at 12:29 am

Thanks for the breakdown; my gotcha was indeed with a list. I was trying to make a copy of it, for to branch therefrom. I’m still not sure what the benefit is of essentially allowing multiple names for the same variable, but I’m sure I’ll come around.