numerov_solve.py

Go to the documentation of this file.
00001 """Compute Schrodinger equation eigenfunctions very quickly and efficiently using Numerov's method to solve the underlying differential equations"""
00002 _rcsid="$Id: numerov_solve.py,v 1.3 2003/10/03 14:37:38 mendenhall Exp $"
00003 
00004 import math
00005 import Numeric
00006 import sys
00007 
00008 try:
00009     from _numerov import _numerov
00010     def numerov_iter(Y, coefs):
00011         _numerov(Y,coefs)
00012 except:
00013     def numerov_iter(Y, coefs):
00014         npoints=len(Y)
00015         for i in range(1,npoints-1):
00016             yy=Y[i+1]=Y[i]*coefs[i]-Y[i-1]
00017             if abs(yy) > 1e20: #don't let exponential blow up
00018                 Y*=1e-20
00019 
00020     
00021 def bare_numerov(V, dx):
00022     """generate a solution to Schrodinger's eqn for a potential V=(2m/hbar**2)(V-E) using the Numerov method.
00023     Always integrate in from forbidden region to center of potential from both ends, and solve to match boundary conditions.  The initial conditions will fail if started in an allowed (V-E < 0) region"""
00024     npoints=len(V)
00025     Y=Numeric.zeros(npoints,Numeric.Float)
00026     dx2=dx*dx
00027     ypsifact=1.0/(1.0-(dx2/12.0)*V)
00028     coef=2.0+dx2*V*ypsifact
00029     
00030     Y[0]=1.0/ypsifact[0]
00031     Y[1]=math.exp(math.sqrt(V[0])*dx)/ypsifact[1]
00032     
00033     numerov_iter(Y,coef)
00034 
00035     return Y*ypsifact
00036 
00037 
00038 def zbrent(func, x1, x2, tol, itmax=100, trace=0):
00039     "find zeros using the van Wijngaarden-Dekker-Brent method a la Numeric Recipes"
00040     a=x1; b=x2
00041     fa=func(a); fb=func(b)
00042 
00043     assert fb*fa < 0.0, "Root must be bracketed in ZBRENT"
00044     fc=fb
00045     for iter in range(itmax):
00046         if trace: print iter, a, b, fa, fb
00047         if (fb*fc > 0.0):
00048             c=a
00049             fc=fa
00050             e=d=b-a
00051         if abs(fc) < abs(fb):
00052             a=b
00053             b=c
00054             c=a
00055             fa=fb
00056             fb=fc
00057             fc=fa
00058         tol1=2.0*1e-14*abs(b)+0.5*tol
00059         xm=0.5*(c-b)
00060         if abs(xm) <= tol1 or fb == 0.0: return b
00061         if abs(e) >= tol1 and abs(fa) > abs(fb):
00062             s=fb/fa
00063             if a==c:
00064                 p=2.0*xm*s
00065                 q=1.0-s
00066             else:
00067                 q=fa/fc
00068                 r=fb/fc
00069                 p=s*(2.0*xm*q*(q-r)-(b-a)*(r-1.0))
00070                 q=(q-1.0)*(r-1.0)*(s-1.0)
00071             if p>0.0: q = -q
00072             p=abs(p)
00073             min1=3.0*xm*q-abs(tol1*q)
00074             min2=abs(e*q)
00075             if 2.0*p < min(min1,min2):
00076                 e=d
00077                 d=p/q
00078             else:
00079                 d=xm
00080                 e=d
00081         else:
00082             d=xm; e=d
00083         a=b
00084         fa=fb
00085         if abs(d) > tol1:
00086             b += d
00087         elif xm > 0.0:
00088             b+=tol1
00089         else:
00090             b-=tol1 
00091         fb = func(b);
00092         
00093     assert 0, "Maximum number of iterations exceeded in ZBRENT"
00094 
00095 def single_well_numerov_match(V0, dx, parity=1, overlapsize=5):
00096     "generate a candidate eigenfunction with given potential, assuming the potential well has a single allowed region bounded on both sides"
00097     assert len(V0) & 1, "Must have a grid with an odd number of points"
00098     vminchan=Numeric.argmin(V0) #position of bottom of well
00099     right_turn=Numeric.searchsorted(V0[vminchan:], 0.0)+vminchan #position of right-hand turning point
00100 
00101     #integrate from left end to right-hand turning point, at which point numerov method becomes unstable
00102     leftpsi=bare_numerov(V0[:right_turn], dx)
00103     #iterate backwards on right part to turning point, and flatten to normal array
00104     rightpsi= Numeric.array(bare_numerov(V0[right_turn-overlapsize:][::-1], dx)[::-1]) 
00105     
00106     #remember that since Numeric handles slices without copying, leftend and rightend also scale here
00107     slopeweight=Numeric.array(range(overlapsize),Numeric.Float)-overlapsize/2.0
00108     leftend=leftpsi[-overlapsize:]
00109     rightend=rightpsi[:overlapsize]
00110     
00111     scale=abs(Numeric.sum(leftend*rightend)/Numeric.sum(leftend**2)) #'least-squares' scale estimate
00112     #note that since leftend and rightend are Numeric slice references, modifying psi also changes them!
00113     rightpsi*=float(parity)/scale   
00114     error=Numeric.sum((leftend-rightend)*slopeweight) #average derivative
00115     psi0=Numeric.concatenate((leftpsi, rightpsi[overlapsize:]))
00116 
00117     psi2=psi0*psi0
00118     integral=psi2[0]+psi2[-1]+4.0*Numeric.sum(psi2[1:-1:2])+2.0*Numeric.sum(psi2[2:-2:2]) #use simpson's rule
00119     psimag=1.0/math.sqrt(integral*dx/3.0)
00120     
00121     return psi0*psimag, error*psimag
00122 
00123 def single_well_numerov_solve(V0, dx, parity, emin, emax, overlapsize=5):
00124     "generate an eigenfunction with given potential with emin<(2m/hbar**2)E<emax, assuming the potential well has a single allowed region bounded on both sides"
00125     e=zbrent(lambda x: single_well_numerov_match(V0-x, dx, parity, overlapsize=overlapsize)[1], emin, emax, 1e-15, trace=0)
00126     psi, slope_error=single_well_numerov_match(V0-e, dx, parity, overlapsize)
00127     return e, psi
00128 
00129 def find_spectrum(V0, dx, levels, ground_state_estimate, state_separation_estimate=None, trace=0, subdivide=4):
00130     results=[]
00131     if state_separation_estimate is None:
00132         state_separation_estimate=ground_state_estimate
00133     #prime the bookkeeping to guess where next state lies and make it point to ground_state_estimate
00134     e0, e1= ground_state_estimate-2*state_separation_estimate, ground_state_estimate-state_separation_estimate
00135     for i in range(levels):
00136         if trace: print '***', i, e0, e1
00137         delta_e=(e1-e0)/subdivide
00138         if i & 1:
00139             parity=-1
00140         else:
00141             parity=1
00142         try:
00143             firstguess=e1+(e1-e0)*0.5 #1/2 to next estimated level
00144             psi, slope=single_well_numerov_match(V0-firstguess, dx, parity) #initial slope
00145             for s in range(1,10*subdivide):
00146             
00147                 e_test=firstguess+delta_e
00148                 psi, test_slope=single_well_numerov_match(V0-e_test, dx, parity) #initial slope
00149                 if trace:
00150                     print "%4d %4d %8.2e %8.2e %8.2e %8.2e" % (i, s, firstguess, slope, e_test, test_slope) 
00151                 if test_slope==0.0 or test_slope*slope < 0.0: #got a sign change
00152                     break
00153                 if s==(10*subdivide-1):
00154                     assert 0,"No root found" #bail out completely
00155                 firstguess=e_test
00156                 slope=test_slope
00157             e_n, psi_n=single_well_numerov_solve(V0, dx, parity, e1+(s-1)*delta_e, e_test) #must bracket root
00158             results.append((i,e_n, psi_n))
00159             e0, e1=e1, e_n #use last two energies to estimate search size
00160         except:
00161             print sys.exc_value
00162             break #stop if we can't find a level!       
00163     return results
00164         
00165 
00166 if __name__=='__main__':    
00167     potlen=101
00168     xmax=7.0
00169     dx=2.0*xmax/(potlen-1)
00170     xar=(Numeric.array(range(potlen),Numeric.Float)-(potlen-1)/2.0)*(xmax*2.0/(potlen-1))
00171     testpot=xar**2
00172     
00173     results=find_spectrum(testpot, dx, 10, 1.,  trace=0)
00174     for n, e_n, psi_n in results:
00175         print n, e_n

Generated on Wed Nov 21 10:18:31 2007 for analysis by  doxygen 1.5.4