3D Example: Two particles with Coulomb interaction

This example demonstrates how to use the FewBodyToolkit.jl package to compute bound states for two particles in 3D. Here we use the Coulomb interaction, since it has analytic solutions. In relative coordinates, this system is equivalent to a single particle in a potential. It is governed by the following Schrödinger equation: \[ -\frac{1}{2} \frac{1}{r}\frac{d^2}{dr^2}\left( r\psi \right) + V(r)\psi = E\psi \] with the Colomb potential \[ V(r) = -\frac{Z}{r}. \]

Setup

using Printf, Interpolations, Plots, Antique, FewBodyToolkit#.GEM2B

Input parameters

Physical parameters

mass_arr = [1.0, Inf] # array of masses of particles [m1,m2]
mur = 1 / (1/mass_arr[1] + 1/mass_arr[2])
Z = 1.0

function v_coulomb(r)
    return -Z/r
end
v_coulomb (generic function with 1 method)

We define the physical parameters as a NamedTuple which carries the information about the Hamiltonian.

phys_params = make_phys_params2B(;vint_arr=[v_coulomb])
(hbar = 1.0, mur = 1.0, vint_arr = [Main.v_coulomb], lmin = 0, lmax = 0, dim = 3)

By leaving out the optional parameters, we use the defaults:

  • mur = 1.0: reduced mass
  • dim = 3: dimension of the problem
  • lmin = lmax = 0: minimum and maximum angular momentum (in 1D this corresponds to even states)
  • hbar = 1.0: when working in dimensionless units

Numerical parameters

nmax=10 # number of Gaussian basis functions
r1=0.1;rnmax=30.0; # r1 and rnmax defining the widths of the basis functions
gem_params = (;nmax,r1,rnmax);

We define the numerical parameters as a NamedTuple:

num_params = make_num_params2B(;gem_params)
(gem_params = (nmax = 10, r1 = 0.1, rnmax = 30.0), theta_csm = 0.0, omega_cr = 0.9, threshold = 1.0e-8)

1. Numerical solution

We solve the two-body system by calling GEM2B_solve.

energies = GEM2B.GEM2B_solve(phys_params,num_params)
10-element Vector{Float64}:
  -0.4998757180240628
  -0.12454305401429006
  -0.05443710110158085
  -0.02864355774575035
   0.007342103389141706
   0.6031764481212917
   3.674495968760052
  16.18142428223919
  64.2068555797109
 251.37534725527286

Number of bound states to consider:

simax = min(lastindex(energies),6); # max state index

The Coulomb potential has infinitely many bound states, whose energies can be found exaclty. We can use the package Antique.jl to provide these energies.

CTB = Antique.CoulombTwoBody(m₁=mass_arr[1], m₂=mass_arr[2])
energies_exact = [Antique.E(CTB,n=i) for i=1:40]

println("1. Numerical solution of the 3D problem:")
comparison(energies,energies_exact,simax)
1. Numerical solution of the 3D problem:
Index   Numerical       Reference       Difference
1       -0.499876       -0.500000       -0.000124
2       -0.124543       -0.125000       -0.000457
3       -0.054437       -0.055556       -0.001118
4       -0.028644       -0.031250       -0.002606
5       0.007342        -0.020000       -0.027342
6       0.603176        -0.013889       -0.617065

The numerical solutions are good only for the few lowest state. Also, we only find 5 bound states.

2. Optimization of basis parameters

We can optimize the basis parameters for a specific state indicated by stateindex using GEM_Optim_2B.

stateindex = 6
params_opt = GEM2B.GEM_Optim_2B(phys_params, num_params, stateindex)
gem_params_opt = (;nmax, r1 = params_opt[1], rnmax = params_opt[2])
num_params_opt = make_num_params2B(;gem_params=gem_params_opt)
energies_opt = GEM2B.GEM2B_solve(phys_params,num_params_opt)

println("\n2. Optimization of GEM parameters for E2[$stateindex]:")
@printf("%-15s %-15s %-15s %-15s %-15s\n", "r1", "rnmax", "E2[$(stateindex-1)]", "E2[$stateindex]", "E2[$(stateindex+1)]")

println("Before optimization:")
@printf("%-15.6f %-15.6f %-15.6f %-15.6f %-15.6f\n", gem_params.r1, gem_params.rnmax, energies[stateindex-1], energies[stateindex], energies[stateindex+1])

println("after optimization:")
@printf("%-15.6f %-15.6f %-15.6f %-15.6f %-15.6f\n", params_opt[1], params_opt[2], energies_opt[stateindex-1], energies_opt[stateindex], energies_opt[stateindex+1])

2. Optimization of GEM parameters for E2[6]:
r1              rnmax           E2[5]           E2[6]           E2[7]
Before optimization:
0.100000        30.000000       0.007342        0.603176        3.674496
after optimization:
0.871259        45.664907       -0.019847       -0.013823       0.025770

Optimizing the parameters for the 6th excited states finds more bound states, while loosing some accuracy for the lower states. Here, only more basis functions would help.

comparison(energies_opt,energies_exact,simax; s1="Optimized")
Index   Optimized       Reference       Difference
1       -0.489956       -0.500000       -0.010044
2       -0.123714       -0.125000       -0.001286
3       -0.055167       -0.055556       -0.000388
4       -0.031063       -0.031250       -0.000187
5       -0.019847       -0.020000       -0.000153
6       -0.013823       -0.013889       -0.000066

3. Example with many basis functions

Highly accurate results can indeed be obtained by using a larger basis. For a two-body system this comes only at a moderate computational cost. Here, we reproduce the table 2 of Ref. [1] with the following numerical parameters:

println("\n3. Highly accurate solution using many basis functions:")
np = make_num_params2B(;gem_params=(;nmax=80,r1=0.015,rnmax=2000.0),omega_cr=1.5,threshold=10^-11)
@time energies_accurate = GEM2B.GEM2B_solve(phys_params,np;cr_bool=1) # ~3s on an average laptop
nlist=[1,2,3,4,5,10,14,18,22,26,30,32,34,36,38,40]; # states shown in the article
comparison(energies_accurate,energies_exact,lastindex(nlist); s1="Numerical", s2="Exact",indexlist=nlist)

3. Highly accurate solution using many basis functions:
  1.660127 seconds (50.23 M allocations: 1.104 GiB, 5.22% gc time, 28.85% compilation time)
Index   Numerical       Exact           Difference
1       -0.500000       -0.500000       -0.000000
2       -0.125000       -0.125000       -0.000000
3       -0.055556       -0.055556       -0.000000
4       -0.031250       -0.031250       -0.000000
5       -0.020000       -0.020000       -0.000000
10      -0.005000       -0.005000       -0.000000
14      -0.002551       -0.002551       -0.000000
18      -0.001543       -0.001543       -0.000000
22      -0.001033       -0.001033       -0.000000
26      -0.000740       -0.000740       0.000000
30      -0.000556       -0.000556       0.000000
32      -0.000488       -0.000488       -0.000000
34      -0.000432       -0.000433       -0.000000
36      -0.000386       -0.000386       0.000000
38      -0.000347       -0.000346       0.000001
40      -0.000311       -0.000313       -0.000001

4. Using an interpolated interaction

We can also create a potential from interpolated data. Since the Coulomb potential diverges at the origin, a relatively fine grid is required.

r_arr = 0.001:0.01:50.001
v_arr = v_coulomb.(r_arr)
v_interpol = cubic_spline_interpolation(r_arr,v_arr,extrapolation_bc=Line())
v_int(r) = v_interpol(r); # we have to transform the interaction to an object of type "function"

As input to the solver we need to define new physical parameters with the interpolated interaction. Moreover, we use the optimized numerical parameters from the previous step.

phys_params_i = make_phys_params2B(;mur,vint_arr=[v_int],dim=3)

println("\n4. Numerical solution using an interpolated interaction:")
energies_interpol = GEM2B.GEM2B_solve(phys_params_i,num_params_opt)
comparison(energies_interpol, energies_opt, simax;s1="Interpolated", s2="Optimized")

4. Numerical solution using an interpolated interaction:
Index   Interpolated    Optimized       Difference
1       -0.489984       -0.489956       0.000028
2       -0.123717       -0.123714       0.000004
3       -0.055168       -0.055167       0.000001
4       -0.031063       -0.031063       -0.000000
5       -0.019783       -0.019847       -0.000064
6       -0.012595       -0.013823       -0.001228

5. Coupled-channel problem

The package also supports coupled-channel problems via GEM2B_solveCC. In this case the interaction is not provided via phys_params

phys_paramsCC = make_phys_params2B(;vint_arr=[r->0.0])
(hbar = 1.0, mur = 1.0, vint_arr = [Main.var"#3#4"()], lmin = 0, lmax = 0, dim = 3)

but via extra arguments WCC: wfun on the diagonal; wfun2 for off-diagonal couplings, and DCC: derivative terms of order dor, and radial prefactors dfun (diagonal) and dfun2 (off-diagonal).

wfun(r) = v_coulomb(r);wfun2(r) = 0.05*exp(-r^2)
dfun(r) = 0.0; dfun2(r) = 0.0
WCC = [wfun wfun2; wfun2 wfun]
dor = 1; #derivative-order
DCC = reshape([ [dor, dfun], [dor, dfun2], [dor, dfun2], [dor, dfun] ], 2, 2)
2×2 Matrix{Vector{Any}}:
 [1, dfun]   [1, dfun2]
 [1, dfun2]  [1, dfun]

As a test-case we use the coulomb interaction on the diagonal, and a weak repulsion on the off-diagonal. We don't consider any extra derivatives. Since the coupling is weak, we get approximately twofold degenerate eigenvalues, splitted around the original Coulomb results.

energiesCC = GEM2B.GEM2B_solveCC(phys_paramsCC, num_params_opt, WCC, DCC; diff_bool=0)

energies_exactCC = repeat(energies_exact, inner=(2,))

println("\n5. Coupled channel calculation:")
comparison(energiesCC, energies_exactCC, simax; s1="Coupled-Channel")

5. Coupled channel calculation:
Index   Coupled-Channel Reference       Difference
1       -0.503324       -0.500000       0.003324
2       -0.476991       -0.500000       -0.023009
3       -0.125008       -0.125000       0.000008
4       -0.122410       -0.125000       -0.002590
5       -0.055536       -0.055556       -0.000020
6       -0.054793       -0.055556       -0.000762

6. Calculation of the wave function

Adding the optional argument wf_bool=1 to GEM2B_solve also computes and returns a matrix of eigenvectors (in each column). These eigenvectors contain the weights of the basis functions.

energiesw,wfs = GEM2B.GEM2B_solve(phys_params,num_params_opt;wf_bool=1,cr_bool=0);

We can use the functions GEM2B.wavefun_arr and GEM2B.wavefun_point to compute the wave function at a set of points or at a specific point, respectively. The information on the basis functions is provided via the optimized numerical parameters num_params_opt, the vector of Gaussian widths. We compare the numerical wave function with the exact solutions provided by Antique.jl and find very good agreement.

dr = 0.1
r_arr = 0.0:dr:50.0
redind = vcat(1:2:30,31:5:50,51:10:lastindex(r_arr)) # We evaluate the analytical solutions at a coarser grid to avoid overloading the plot.

wfA(r,n) = Antique.R(CTB, r; n, l=0) # Exact wave function for the n-th state

p = plot(xlabel="\$ r \$", ylabel="\$ r^2\\,|\\psi(r)|^2 \$", title="Two-body radial s-wave densities\n for a 3D Coulomb system", guidefont=18,legendfont=10)
density = zeros(length(r_arr),4)
density_exact = zeros(length(r_arr),4)
markers = [:circ, :square, :utriangle, :star]
for si = 1:4
    wf = wfs[:,si]
    psi_arr = GEM2B.wavefun_arr(r_arr,phys_params,num_params_opt,wf;cr_bool=0)

    density[:,si] .= abs2.(psi_arr).*r_arr.^2
    density_exact[:,si] = abs2.(wfA.(r_arr,si)).*r_arr.^2

    scatter!(r_arr[redind],density_exact[redind,si],label="n=$(si), exact", lw=2,color=si, marker=markers[si],markersize=3)
    plot!(r_arr, density[:,si], label="n=$(si), num", lw=2, color=si)
end

p
Example block output

The normalization of the wave function can be checked by integrating the density:

norms = density[:,1:4]'*fill(dr,lastindex(r_arr)) # A simple Riemann sum is sufficient here
println("\n6. Norms of the wave functions:")
comparison(norms, ones(4), 4; s1="Norm", s2="Exact")

6. Norms of the wave functions:
Index   Norm            Exact           Difference
1       0.999999        1.000000        0.000001
2       0.999999        1.000000        0.000001
3       0.999997        1.000000        0.000003
4       0.998430        1.000000        0.001570

This page was generated using Literate.jl.