For seek of nice drawing, I code a python code for that. Although the scipy package can solve differential equation, the way is breaking a 2nd order equation into two 1st order equations. So, I simply move the RK4 method into python. Also, the plotly package can draw a nice plot.
import numpy
import math
import plotly.graph_objects as go
import scipy.integrate
import scipy.special
import scipy.interpolate
class WoodsSaxon:
#grid setting
rStart = 0.0001
dr = 0.1
nStep = 300
#constant
mn = 939.56539 #MeV/c2
hbarc = 197.326979 #MeV.fm
#RK4 constants
parC = [0, 1./2, 1./2, 1.]
parD = [1./6, 2./6, 2./6, 1./6]
mu = 0
R0 = 0
RSO = 0
E = 0 #energy of the state
L = 0
J = 0
#inital condition
solu0 = 0.0
dsolu0 = 1.0
trancation = 100 #wavefunction will trancate if value < max/ 100
rpos = numpy.arange(rStart, rStart+nStep*dr, dr)
kpos = numpy.arange(0, 1000, 1) #momentum in MeV/c
kzpos = numpy.arange(0, 500, 1 ) #para-momenutem in MeV/c
SolU = []
maxSolU = 0.0
WF = numpy.empty([nStep], dtype=float) # radial wave function
maxWF = 0.0
WFk = numpy.empty([len(kpos)], dtype=float) #momentum wave function
WFkz = numpy.empty([len(kzpos)], dtype=float) #para-momentum dist function
minEigenE = -100 # the lowest eigen energy
eigenEnergy = numpy.empty([0,6], dtype=float) #energy, N, L, J, dEnergy, rms(raidus)
eigenWFList = numpy.empty([0,nStep], dtype=float) # list of eigen WF.
eigenWFkList = numpy.empty([0, len(kpos)], dtype=float) #list of eigen momentum wave function
eigenWFkzList = numpy.empty([0, len(kzpos)], dtype=float) # list of para-momentum dist function
#====================== Setting
# must declare at the initialization of the class
def __init__(self, A, V0, r0, a0, VSO, rSO, aSO):
self.A = A
self.V0 = V0
self.r0 = r0
self.a0 = a0
self.VSO = VSO
self.rSO = rSO
self.aSO = aSO
self.mu = self.mn * A/(A+1)
self.R0 = r0 * math.pow(A,1./3)
self.RSO = rSO * math.pow(A, 1./3)
#self.PrintParameters()
self.minEigenE = V0 + 5 #assume the 1st s-state is 5 MeV above the V0
# set energy, can be not eigen status
def SetE(self, E):
self.E = E
# set the mometum L and J
def SetLJ(self,L,J):
self.L = L
self.J = J
# set the range in fm
def SetRange(self, rStart, dr, nStep):
self.rStart = rStart
self.dr = dr
self.nStep = nStep
self.WF=numpy.empty([nStep], dtype=float)
rpos = numpy.arange(self.rStart, self.rStart+self.nStep*dr, self.dr)
# Woods-Saxon potential
def __WS(self, x):
return self.V0/(1 + math.exp((x-self.R0)/self.a0))
# spin-obital potential
def __SO(self, x):
return self.LS(self.L, self.J)*(self.VSO * math.exp((x-self.RSO)/self.aSO))/(self.aSO*math.pow(1+math.exp((x-self.RSO)/self.aSO),2))/x
# LS coupling factor
def LS(self, L, J) :
return (J*(J+1)-L*(L+1)-0.5*(1.5))/2.
# The G-function, u''[r] = G[r, u[r], u'[r]]
def __G(self, x, y, dy):
#return -2*x*dy -2*y # solution of gaussian
return 2*self.mu/math.pow(self.hbarc,2)*(self.__WS(x) - self.__SO(x) - self.E)*y + self.L*(1+self.L)/x/x*y
#============================== Radial wave function
# Plot potential and wavefunction for give energy E
def PlotAll(self, E, maxR = 20):
self.E = E
ypos = []
rSpinObi = []
SpinObi = []
rL = []
yL = []
rtot = []
ytot = []
yE = []
maxWS = abs(self.V0)
for i in range(self.nStep):
x = self.rStart + self.dr*i
if( x < maxR ):
central = self.__WS(x)
ypos.append(central)
yE.append(self.E)
so = self.__SO(x)
centrapetal = math.pow(self.hbarc,2)/2/self.mu*(self.L*(1+self.L))/x/x
tot = central-so + centrapetal
if abs(so) < maxWS :
rSpinObi.append(x)
SpinObi.append(so)
if abs(centrapetal) < maxWS :
rL.append(x)
yL.append(centrapetal)
if abs(tot) < maxWS :
rtot.append(x)
ytot.append(tot)
fig = go.Figure()
fig.add_trace(go.Scatter(x=self.rpos, y=ypos, name="Woods-Saxon"))
fig.add_trace(go.Scatter(x=rSpinObi, y=SpinObi, name="Spin-Orbit"))
fig.add_trace(go.Scatter(x=rL, y=yL, name="Centrapetal"))
fig.add_trace(go.Scatter(x=rtot, y=ytot, name="Total"))
if E != 0.0 :
fig.add_trace(go.Scatter(x=self.rpos, y=yE, name="Energy", marker=dict(color = 'black')))
self.SolveByRK4()
self.CovertSolUtoWF()
fig.add_trace(go.Scatter(x=self.rpos, y=(self.WF/self.maxWF*maxWS + self.E), name="WaveFunction"))
fig.update_xaxes(range=[0, maxR])
fig.update_layout(xaxis_title="radius [fm]")
fig.update_layout(yaxis_title="Energy [MeV]")
fig.show()
# plot the Eigen wave function of ID, will calculate Wave function
def PlotEigenWF(self, ID, maxR = 20):
if -1 < ID and ID < len(ws.eigenEnergy) :
self.SetLJ(self.eigenEnergy[ID, 2],self.eigenEnergy[ID, 3])
self.PlotAll(self.eigenEnergy[ID,0], maxR)
# plot all eigen wave function
def PlotAllEigenWFs(self, isSquare = False, maxR = 20):
fig = go.Figure()
for i in range(len(self.eigenEnergy)) :
haha = self.eigenEnergy[i]
wf = self.eigenWFList[i]
if isSquare == True :
fig.add_trace(go.Scatter(x=self.rpos, y=(wf*wf), name=self.FormatOrbital(haha)))
else:
fig.add_trace(go.Scatter(x=self.rpos, y=wf, name=self.FormatOrbital(haha)))
fig.update_xaxes(range=[0, maxR])
fig.update_layout(xaxis_title="radius [fm]")
fig.update_layout(yaxis_title=r"$\phi(r)$")
fig.show()
# Using Rungu-Kutta 4th method to solve u''[r] = G[r, u[r], u'[r]]
def SolveByRK4(self):
#initial condition
self.SolU = [self.solu0]
dSolU = [self.dsolu0]
dyy = numpy.array([1., 0., 0., 0., 0.])
dzz = numpy.array([1., 0., 0., 0., 0.])
self.maxSolU = 0.0
for i in range(self.nStep-1):
r = self.rStart + self.dr * i
y = self.SolU[i]
z = dSolU[i]
for j in range(4):
j = j+1
dyy[j] = self.dr * (z + self.parC[j-1]*dzz[j-1])
dzz[j] = self.dr * self.__G(r + self.parC[j-1]*self.dr, y + self.parC[j-1]*dyy[j-1], z + self.parC[j-1]*dzz[j-1])
#if( i < 10 ):
# print("%d, %f, (%f, %f ), %f, %f, %f" % (j, parC[j-1], dyy[j-1], dzz[j-1], dyy[j], dzz[j], G(r, y,z)))
#if( i < 10 ):
# print(dyy, dzz)
dy = 0
dz = 0
for j in range(4):
j = j+1
dy = dy + self.parD[j-1]*dyy[j]
dz = dz + self.parD[j-1]*dzz[j]
#if( i < 10 ):
# print("%7.5f, %8.4f, %8.4f, %8.4f" % (r, y, G(r,y,z), y +dy))
self.SolU.append(y + dy)
dSolU.append(z + dz)
if abs(y+dy) > self.maxSolU and r < self.R0*6:
self.maxSolU = abs(y+dy)
return self.SolU
# normalize SolU as int( SolU^2 r^2 ) =
def NormalizeSolU(self):
SolU2 = []
for i in range(self.nStep):
if( self.rpos[i] > 5* self.R0 and self.SolU[i] < self.maxSolU / self.trancation):
SolU2.append(0.0)
else:
SolU2.append(math.pow(self.SolU[i],2))
norm = math.sqrt(scipy.integrate.simps(SolU2, self.rpos))
for i in range(self.nStep):
self.SolU[i] = self.SolU[i]/norm
# convert the SolU = u[r] to radial wave function as R[r] = u[r]/r
def CovertSolUtoWF(self):
self.NormalizeSolU()
#convert to wave function
self.maxWF = 0.0
for i in range(self.nStep):
r = self.rStart + self.dr * i
self.WF[i] = self.SolU[i]/r
if( self.maxWF < abs(self.SolU[i]/r)):
self.maxWF = abs(self.SolU[i]/r)
if self.L == 0 :
self.WF[0] = self.WF[1]
# Find Eigen energies, should save the wave function one by one?
def FindEigenEnergis(self, isPrint=True):
Estep = 0.2 # this limit the weakly bound state
self.eigenEnergy = numpy.empty([0,6], dtype=float)
if self.A < 40 :
lmax = 3
else :
lmax = 6
for l in range(lmax+1):
if l == 0 :
jList = [1./2]
else :
jList = [l + 1./2, l - 1./2]
for j in jList:
self.SetLJ(l, j)
count = 0
eigenN = 0
EList = numpy.arange(self.minEigenE, 0, Estep) # assume there is no state with only 5 MeV above the V0
oldhaha = []
for E in EList:
self.E = E
Enow = E
haha = self.SolveByRK4()
if count > 0 :
# if the tail change sign, use bisection method to find the best E
repeat = 0
while oldhaha[-1] * haha[-1] < 0:
EMiddle = (Enow + Epast)/2.
if (Enow - Epast < 1e-4 and abs(oldhaha[-1] - haha[-1]) < self.maxSolU * 0.01 ) or repeat > 200:
if eigenN == 0 :
self.minEigenE = EMiddle #this set the starting energy for next j-value
self.eigenEnergy = numpy.append(self.eigenEnergy, [[EMiddle, eigenN, l, j, abs(Enow-Epast), 0]], axis=0)
eigenN = eigenN + 1
break
else:
self.E = EMiddle
hahaMiddle = self.SolveByRK4()
if oldhaha[-1] * hahaMiddle[-1] < 0 :
haha = hahaMiddle
Enow = EMiddle
else:
oldhaha = hahaMiddle
Epast = EMiddle
repeat = repeat + 1
oldhaha = haha
Epast = E
count = count + 1
#sorting energy energy with N, L, J
self.eigenEnergy = self.eigenEnergy[self.eigenEnergy[:,0].argsort()]
self.eigenWFList = numpy.empty([0,self.nStep], dtype=float) # list of eigen WF.
for i in range(len(self.eigenEnergy)):
haha = self.eigenEnergy[i]
self.E = haha[0]
self.SetLJ(haha[2], haha[3])
self.SolveByRK4()
self.NormalizeSolU()
self.CovertSolUtoWF()
self.eigenWFList = numpy.append(self.eigenWFList, [self.WF], axis=0)
self.eigenEnergy[i][5] = self.CalRMSRadius()
if isPrint :
self.PrintEigenEnergies()
# Get Eigen radial wave function
def GetEigenWF(self, ID):
return self.eigenWFList[ID]
# Calculate RMS radius for the WF stored in the momeory
def CalRMSRadius(self):
return math.sqrt(scipy.integrate.simps(self.WF*self.WF*numpy.power(self.rpos,4), self.rpos))
# Print Woods-Saxon parameters
def PrintParameters(self):
print("======================================================")
print("A = %d, mu = %f MeV/c2" % (self.A, self.mu))
print("V0 = %7.2f MeV, r0 = %6.3f(%6.3f) fm, a0 = %6.3f fm" % (self.V0, self.r0, self.R0, self.a0))
print("VSO = %7.2f MeV, rSO = %6.3f(%6.3f) fm, aSO = %6.3f fm" % (self.VSO, self.rSO, self.RSO, self.aSO))
print("Range = Start : %.6f fm, End : %.6f | stepSize : %.2f fm, num. Step : %d" % (self.rStart, self.rStart + self.dr*self.nStep, self.dr, self.nStep))
print("======================================================")
# Format te orbital symbol from N, L, J using the eigen energy array
def FormatOrbital(self, eigen):
if eigen[2] == 0.0 : Lsym = "s"
if eigen[2] == 1.0 : Lsym = "p"
if eigen[2] == 2.0 : Lsym = "d"
if eigen[2] == 3.0 : Lsym = "f"
if eigen[2] == 4.0 : Lsym = "g"
if eigen[2] == 5.0 : Lsym = "h"
if eigen[2] == 6.0 : Lsym = "i"
if eigen[2] == 7.0 : Lsym = "j"
output = "%.0f%s%.0f/2" % (eigen[1], Lsym, 2*eigen[3])
return output
# print eigen energies
def PrintEigenEnergies(self):
print("======================================================")
count = 0
for haha in self.eigenEnergy:
print("%2d | %4s %14.10f MeV +- %14.10e | rms(Radius): %10.6f fm" % (count, self.FormatOrbital(haha), haha[0], haha[4], haha[5]))
count = count + 1
print("======================================================")
#================================= Momentum
# Calculate momentum wave function. This use the WF in the memory.
def __CalMomentumWF(self, L):
for i in range(len(self.kpos)):
kfm = self.kpos[i] / self.hbarc
function = numpy.empty([self.nStep], dtype=float) # this is SpheicalBeseel(kr)*WF(r)*r^2
for j in range(self.nStep):
if( self.rpos[j] > 5 * self.R0 and self.WF[j] < self.maxWF / self.trancation) :
function[j] = 0.
else:
sb = scipy.special.spherical_jn(int(L) , kfm * self.rpos[j])
function[j] = sb * self.WF[j] * self.rpos[j] * self.rpos[j]
self.WFk[i] = scipy.integrate.simps(function, self.rpos)
maxWFk = numpy.sign(self.WFk[1])*max(abs(numpy.amax(self.WFk)), abs(numpy.amin(self.WFk)))
self.WFk = self.WFk/maxWFk
# Calculate eigen momentum wave function and store it
def CalEigenMomentumWF(self):
self.eigenWFkList = numpy.empty([0, len(self.kpos)], dtype=float)
for i in range(len(self.eigenWFList)) :
self.WF = self.eigenWFList[i]
self.__CalMomentumWF(self.eigenEnergy[i][2])
self.eigenWFkList = numpy.append(self.eigenWFkList, [self.WFk], axis=0)
# [Obsolete?] normaalize momentum Wave Function as int(wfk^2 k^2) = 1
def NormalizedMomentumWF(self):
if len(self.WFk) == len(self.kpos) :
norm = math.sqrt(scipy.integrate.simps((self.WFk*self.WFk*self.kpos*self.kpos), self.kpos))
self.WFk/norm
# return the eigen momentum wave function
def GetEigenMomentumWF(self, ID):
return self.eigenWFkList[ID]
# Plot all Eigne momentum wave functions, this will recalculate from the eigen energy
def PlotAllEigenMomentumWF(self, isSquare = False, maxk=500):
fig = go.Figure()
if len(self.eigenEnergy) != len(self.eigenWFkList) :
self.CalEigenMomentumWF()
for i in range(len(self.eigenWFkList)) :
haha = self.eigenEnergy[i]
if isSquare == False :
wfk = self.eigenWFkList[i]
else:
wfk = numpy.power(self.eigenWFkList[i],2)
fig.add_trace(go.Scatter(x=self.kpos, y=wfk, name=self.FormatOrbital(haha)))
fig.update_xaxes(range=[0, maxk])
fig.update_layout(xaxis_title="Momemtum [MeV/c]")
if isSquare == True :
fig.update_layout(yaxis_title=r"$\phi^2(k)$")
else:
fig.update_layout(yaxis_title=r"$\phi(k)$")
fig.show()
# calculate the parallel momentum distribution using the WFk in the memory
# the para-momentum distribution is f(kz) = int( WFk(sqrt(kz^2 + kr^2 ))^2 kr )
def __CalParaMomentumDist(self):
wfk = scipy.interpolate.interp1d(self.kpos, self.WFk)
krList = self.kzpos
for i in range(len(self.kzpos)):
kz = self.kzpos[i]
kList = numpy.sqrt(kz*kz + krList*krList)
func = wfk(kList)
self.WFkz[i] = scipy.integrate.simps(func * func * krList, krList )
maxWFkz = max(abs(numpy.amax(self.WFkz)), abs(numpy.amin(self.WFkz)))
self.WFkz= self.WFkz/maxWFkz
# Calculate eigen para-momentum distribution
def CalEigenParaMomentumDist(self):
self.eigenWFkzList = numpy.empty([0, len(self.kzpos)], dtype=float)
if len(self.eigenEnergy) != len(self.eigenWFkList) :
self.CalEigenMomentumWF()
for haha in self.eigenWFkList :
self.WFk = haha
self.__CalParaMomentumDist()
self.eigenWFkzList = numpy.append(self.eigenWFkzList, [self.WFkz], axis=0)
# Plot all parallel momentum distribution
def PlotAllParaMomentumDists(self, maxk = 500):
if len(self.eigenEnergy) != len(self.eigenWFkzList) :
self.CalEigenParaMomentumDist()
fig = go.Figure()
for i in range(len(self.eigenWFkzList)) :
haha = self.eigenEnergy[i]
wfkz = self.eigenWFkzList[i]
fig.add_trace(go.Scatter(x=self.kpos, y=wfkz, name=self.FormatOrbital(haha)))
fig.update_xaxes(range=[0, maxk])
fig.update_layout(xaxis_title="para-Momemtum [MeV/c]")
fig.show()
The usage is very straight forward.