Tuesday, May 20, 2008

Evolution of a Python programmer

It is interesting to look back at how one evolves as a Python programmer. I ran across a blog which discussed this in general terms here:

http://www.vetta.org/2008/05/scipy-the-embarrassing-way-to-code/

Now I have some actual examples of this evolution, from basically programming a direct copy from FORTRAN, to being truly pythonic. Over time it evolved into true python and here is how it happened:


So to start we have a direct translation of some FORTRAN Code for doing composition flash calculations. Solve for equilibrium composition by root bisection where the main calculation is built into subroutine


def cFlash2(X, K, nC):
EPS = 1.0e-8
TOL = 1.0e-6
MAXITER = 100
La = 0.0
Lb = 1.0
f0 = 0
for i in range(nC):
f0+=(K[i]-1)*X[i]/K[i]
f1 = 0
for i in range(nC):
f1+=(K[i]-1)*X[i]
if (f0>0) or (f1<0):
print "No VLE"
return -1
print f0, f1
for i in range(MAXITER):
L=(La+Lb)/2.
if abs(La-Lb) return L
f = 0.0
for j in range(nC):
kk =(K[j]-1)*X[j]/(L+K[j]*(1-L))
f=f+kk
if (f>EPS):
Lb=L
elif (f<-EPS):
La=L
else:
return L


So we get a bit pythonic. Define a separate function, arrays know their size, tuples for multiple definitions. Return None if we can't find a solution



def f(L, X, K):
fx=0
for j in range(len(X)):
fx+=(K[j]-1)*X[j]/(L+K[j]*(1-L))
return fx

def cFlash(X, K):
EPS, TOL = 1.0e-6, 1.0e-8
MAXITER = 100
La, Lb = 0.0,1.0
f0,f1 = f(0,X,K), f(1,X,K)
if (f0>0) or (f1<0):
print "No VLE"
return None
for i in range(MAXITER):
L=(La+Lb)/2.
if abs(La-Lb) return L
fx = f(L,X,K)
if (fx>EPS):
Lb=L
elif (fx<-EPS):
La=L
else:
return L


Now define the function locally so we don't contaminate the global namespace and can access the function parameters. Use enumerate to access list index. Raise an exception if root can't be found.


def cFlash(X, K):
def f(L):
fx=0
for j,k in enumerate(K):
fx+=(k-1)*X[j]/(L+k*(1-L))
return fx
EPS, TOL = 1.0e-6, 1.0e-8
MAXITER = 100
La, Lb = 0.0,1.0
f0,f1 = f(0), f(1)
if (f0>0) or (f1<0):
raise VLEError
for i in range(MAXITER):
L=(La+Lb)/2.
if abs(La-Lb) return L
fx = f(L)
if (fx>EPS):
Lb=L
elif (fx<-EPS):
La=L
else:
return L


Now we are using a canned Brent solver routine, which is much faster and more robust. Error handling is all in the canned routine


def cFlash(X, K):
def f(L):
fx=0
for j,k in enumerate(K):
fx+=(k-1)*X[j]/(L+k*(1-L))
return fx
return solver.zbrent(f,0,1)


List comprehensions, zip, sum. Reduce FP operations in inner product term from 6 to 4 by rearrangement



def cFlash(X, K):
def f(L):
return sum([x/(k/(k-1)-L) for x,k in zip(X,K) if k!=1.])
return solver.zbrent(f,0,1)



Now going *too* far: functional version using lambda


def cFlash(X, K):
return solver.zbrent(lambda L:sum([x/(k/(k-1)-L) for x,k in zip(X,K) if k!=1.]),0,1)



In fact, why bother with a function, just inline the call! Actually, the last version is reaching the point of being obscure and offers no efficiency gains over the previous version. The lambda is just syntactic sugar (GVR wants to get rid of it, and I probably agree, though it is useful for some callback situations where the function call is trivial and defining the function makes the code messier than just the lambda, and I do use it in Tkinter code a lot)

Final production version: precompute the ratios k/(k-1) reducing the FP count to two


def cFlash1(X, K):
XK1 = [(x, k/(k-1.)) for x,k in zip(X,K) if k!=1]
def f(L):
return sum([x/(k1-L) for x,k1 in XK1])
return solver.zbrent(f,0,1)



Anyway that sequence basically shows the progression that I have made in using python. In doing so, there have been a number of optimizations made that are transparent when you write in the functional form. The code is perfectly adequate for the present purpose, but if I needed additional speed, I would simply rewrite the core function using NumPy for the array manipulations. In that case the overall speed would approach that of compiled c++, combined with the transparency of python.

But agreed, looking back at ones old code can be embarassing!

2 comments:

Norman J. Harman Jr. said...

Awesome

Max said...

Great post. It takes a true pyguy to come up with that.