Skip to content

Commit f69c197

Browse files
berquistleios
authored andcommitted
Add split-operator method in Python (#332)
* Implement split-operator method in Python. * Add Python split_op to book.
1 parent 1e31556 commit f69c197

File tree

2 files changed

+135
-0
lines changed

2 files changed

+135
-0
lines changed
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# pylint: disable=invalid-name
2+
# pylint: disable=too-many-instance-attributes
3+
# pylint: disable=too-few-public-methods
4+
# pylint: disable=too-many-arguments
5+
from math import pi
6+
from math import sqrt
7+
8+
import numpy as np
9+
10+
11+
class Param:
12+
"""Container for holding all simulation parameters."""
13+
def __init__(self,
14+
xmax: float,
15+
res: int,
16+
dt: float,
17+
timesteps: int,
18+
im_time: bool) -> None:
19+
20+
self.xmax = xmax
21+
self.res = res
22+
self.dt = dt
23+
self.timesteps = timesteps
24+
self.im_time = im_time
25+
26+
self.dx = 2 * xmax / res
27+
self.x = np.arange(-xmax + xmax / res, xmax, self.dx)
28+
self.dk = pi / xmax
29+
self.k = np.concatenate((np.arange(0, res / 2),
30+
np.arange(-res / 2, 0))) * self.dk
31+
32+
33+
class Operators:
34+
"""Container for holding operators and wavefunction coefficients."""
35+
def __init__(self, res: int) -> None:
36+
37+
self.V = np.empty(res, dtype=complex)
38+
self.R = np.empty(res, dtype=complex)
39+
self.K = np.empty(res, dtype=complex)
40+
self.wfc = np.empty(res, dtype=complex)
41+
42+
43+
def init(par: Param, voffset: float, wfcoffset: float) -> Operators:
44+
"""Initialize the wavefunction coefficients and the potential."""
45+
opr = Operators(len(par.x))
46+
opr.V = 0.5 * (par.x - voffset) ** 2
47+
opr.wfc = np.exp(-((par.x - wfcoffset) ** 2) / 2, dtype=complex)
48+
if par.im_time:
49+
opr.K = np.exp(-0.5 * (par.k ** 2) * par.dt)
50+
opr.R = np.exp(-0.5 * opr.V * par.dt)
51+
else:
52+
opr.K = np.exp(-0.5 * (par.k ** 2) * par.dt * 1j)
53+
opr.R = np.exp(-0.5 * opr.V * par.dt * 1j)
54+
return opr
55+
56+
57+
def split_op(par: Param, opr: Operators) -> None:
58+
59+
for i in range(par.timesteps):
60+
61+
# Half-step in real space
62+
opr.wfc *= opr.R
63+
64+
# FFT to momentum space
65+
opr.wfc = np.fft.fft(opr.wfc)
66+
67+
# Full step in momentum space
68+
opr.wfc *= opr.K
69+
70+
# iFFT back
71+
opr.wfc = np.fft.ifft(opr.wfc)
72+
73+
# Final half-step in real space
74+
opr.wfc *= opr.R
75+
76+
# Density for plotting and potential
77+
density = np.abs(opr.wfc) ** 2
78+
79+
# Renormalizing for imaginary time
80+
if par.im_time:
81+
renorm_factor = sum(density) * par.dx
82+
opr.wfc /= sqrt(renorm_factor)
83+
84+
# Outputting data to file. Plotting can also be done in a
85+
# similar way. This is set to output exactly 100 files, no
86+
# matter how many timesteps were specified.
87+
if i % (par.timesteps // 100) == 0:
88+
filename = "output{}.dat".format(str(i).rjust(5, str(0)))
89+
with open(filename, "w") as outfile:
90+
# Outputting for gnuplot. Any plotter will do.
91+
for j in range(len(density)):
92+
template = "{}\t{}\t{}\n".format
93+
line = template(par.x[j], density[j].real, opr.V[j].real)
94+
outfile.write(line)
95+
print("Outputting step: ", i + 1)
96+
97+
98+
def calculate_energy(par: Param, opr: Operators) -> float:
99+
"""Calculate the energy <Psi|H|Psi>."""
100+
# Creating real, momentum, and conjugate wavefunctions.
101+
wfc_r = opr.wfc
102+
wfc_k = np.fft.fft(wfc_r)
103+
wfc_c = np.conj(wfc_r)
104+
105+
# Finding the momentum and real-space energy terms
106+
energy_k = 0.5 * wfc_c * np.fft.ifft((par.k ** 2) * wfc_k)
107+
energy_r = wfc_c * opr.V * wfc_r
108+
109+
# Integrating over all space
110+
energy_final = sum(energy_k + energy_r).real
111+
112+
return energy_final * par.dx
113+
114+
115+
def main() -> None:
116+
par = Param(5.0, 256, 0.05, 100, True)
117+
118+
# Starting wavefunction slightly offset so we can see it change
119+
opr = init(par, 0.0, -1.00)
120+
split_op(par, opr)
121+
122+
energy = calculate_energy(par, opr)
123+
print("Energy is: ", energy)
124+
125+
126+
if __name__ == "__main__":
127+
main()

contents/split-operator_method/split-operator_method.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ Regardless, we first need to set all the initial parameters, including the initi
9999
{% method %}
100100
{% sample lang="jl" %}
101101
[import:9-32, lang:"julia"](code/julia/split_op.jl)
102+
{% sample lang="py" %}
103+
[import:11-30, lang:"python"](code/python/split_op.py)
102104
{% endmethod %}
103105

104106
As a note, when we generate our grid in momentum space `k`, we need to split the grid into two lines, one that is going from `0` to `-kmax` and is then discontinuous and goes from `kmax` to `0`.
@@ -111,6 +113,8 @@ Afterwards, we turn them into operators:
111113
{% method %}
112114
{% sample lang="jl" %}
113115
[import:34-60, lang:"julia"](code/julia/split_op.jl)
116+
{% sample lang="py" %}
117+
[import:33-54, lang:"python"](code/python/split_op.py)
114118
{% endmethod %}
115119

116120
Here, we use a standard harmonic potential for the atoms to sit in and a gaussian distribution for an initial guess for the probability distribution.
@@ -124,6 +128,8 @@ The final step is to do the iteration, itself.
124128
{% method %}
125129
{% sample lang="jl" %}
126130
[import:63-109, lang:"julia"](code/julia/split_op.jl)
131+
{% sample lang="py" %}
132+
[import:57-95, lang:"python"](code/python/split_op.py)
127133
{% endmethod %}
128134

129135
And that's it.
@@ -143,6 +149,8 @@ Checking to make sure your code can output the correct energy for a harmonic tra
143149
{% method %}
144150
{% sample lang="jl" %}
145151
[import, lang:"julia"](code/julia/split_op.jl)
152+
{% sample lang="py" %}
153+
[import:5-127, lang:"python"](code/python/split_op.py)
146154
{% endmethod %}
147155

148156
<script>

0 commit comments

Comments
 (0)