Higher Order Functions

I had the opportunity to listen to Professor Antonio Leitao's presentation at the recent CAADRIA conference. I have been following his work with interest since meeting him at ACADIA in 2011.  At that time, Professor Leitao and Jose Lopes presented Rosetta - a multi-language (Racket, Javascript, Python) programming environment that targets different CAD platforms (AutoCAD, Rhino). Rosetta's appeal lay in its 'write once run everywhere' potential. However the topic of Professor Leitao's recent talk wasn't about Rosetta. Instead he was advocating the use of higher order programming for generative design.

For example, we can use functions that accept and return other functions. These are known as higher-order functions. In their paper 'Programming Languages for Generative Design: A Comparative Study' [1] Professor Leitao, Luis Santos and Jose Lopes provide an example that generates a spiral tower. Though written in VisualScheme for AutoCAD, we can recreate the program in Python for Grasshopper. As always, the definition can be downloaded at the bottom of this post.

Screenshot of the spiral tower program in the Rhinoceros environment

Python component containing the Spiral example code

"""
   This is based on the spiral tower example by Antonio Leitao, Luis Santos and Jose Lopes
"""

from math import sin, pi,cos
from Rhino.Geometry import Point3d
from Rhino.Geometry.Curve import CreateInterpolatedCurve
from Rhino.Geometry.Brep import CreatePipe
from itertools import chain

def linear(a,b):
    return lambda t: (a + (t* (b - a)))

def sinusoidal(a, b, d, f):
	return lambda t: (a + (t * ( b - a)) + (d * sin ( 2 * pi * t * f)))

def variation (f,n):
	return map(f, [_n/n for _n in range(n)])

def cylindrical(r, phi, z):
	return (r*cos(phi), r*sin(phi), z)

def spiral_points(r0, r1, phi0, phi1, h, n, a, f):
	return map(cylindrical, 
    variation(sinusoidal(r0, r1, a, f),n),
    variation(linear(phi0,phi1), n),
    variation(linear(0,h), n))
 
def spirals_points(r0, r1, h, amp, freq, spirals, turns, points):
	return map(lambda phi: spiral_points(r0, r1, phi,(phi + 2*pi*turns)), h, points, amp, freq),
    variation (linear (0, 2*pi), spirals))

def spiral_mesh(r0, r1, h, amp, freq, spirals, turns, points):
	return (spirals_points(r0, r1, h, amp, freq, spirals, turns, points) + 
    spirals_points(r0, r1, h, amp, freq, spirals, -turns, points))

def pipe_from_points(radius, coords, blend = True, cap = 0):
	vertices = [Point3d(*c) for c in coords]
	crv = CreateInterpolatedCurve(vertices, 3)
	return CreatePipe(crv, radius, blend, cap, True, 0.001, 0.001)

def spiral_mesh_pipe(r0, r1, h, amp, freq, spirals, turns, points, pipe_radius):
	return map(lambda coords: pipe_from_points(pipe_radius, coords), 
    spiral_mesh(r0, r1, h, amp, freq, spirals, turns, points))

pipes = spiral_mesh_pipe(base_radius, top_radius, height, amplitude, frequency, num_spirals, num_turns, num_points, tube_radius)

a = list(chain(*pipes))

Let's look at the code inside the Python component. 

variation is a higher order function. In this example it takes sinusoidal  or linear as inputs. Note that we use the Python built-in function map to apply the input function to every item in the desired domain. We get a list of computed values as a result. 

spiral_points is an important function that returns a list of (x,y,z) coordinates for a spiral. It does this by mapping the cylindrical function over three variations (sinusoidal, linear, linear). Think of each variation as supplying the inputs radius, phi and z height to cylindrical, which is analogous to the 'cylindrical coordinates' component in Grasshopper. Note the lambda keyword used, this means that we are using an anonymous function (not bound to a name).

After this, spirals_points just creates all the coordinates for multiple spirals, while spiral_mesh creates another set of spirals that turn in the opposite direction. The last important function is spiral_mesh_pipe which is responsible for creating the previewed geometry. It applies/maps pipe_from_points to the list of coordinates returned by a call to spiral_mesh. Of the functions, only pipe_from_points uses the Rhino.Geometry library. This can theoretically be substituted with other libraries' geometry methods (assuming we want to make this non Rhino-specific). Finally, we just use chain from itertools to flatten the nested list so that the Python component previews the piped breps. Phew. 

I just want to mention again that this example was provided by Antonio Leitao, Luis Santos and Jose Lopes who did all the intellectual heavy lifting. I have always been using Python in an imperative style so this was a good first experience with a functional approach. I am surprised how concise the code is at the end (<50 lines) though it is probably debatable whether this impacts its readability.

Oh before I forget here's the grasshopper file.

 

Reference

  1. Leitao, Antonio,  Luis Santos and Jose Lopes (2012) 'Programming Languages For Generative Design: A Comparative Study', International Journal of  Architectural Computing, 10(1): 139-162.
/