Skip to content

Commit c3ac3be

Browse files
authored
Add Split-Operator in Rust (algorithm-archivists#686)
* Add Split-Operator in Rust * Remove comments The small for-loops are fine as they are.
1 parent 9cb06db commit c3ac3be

File tree

2 files changed

+205
-0
lines changed

2 files changed

+205
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
extern crate num;
2+
extern crate rustfft;
3+
4+
use num::complex::Complex;
5+
use rustfft::FFTplanner;
6+
use std::f64::consts::PI;
7+
use std::fs::File;
8+
use std::io::Write;
9+
use std::path::Path;
10+
11+
// This implementation is based on the C and C++ implementations.
12+
13+
#[derive(Clone)]
14+
struct Parameters {
15+
xmax: f64,
16+
res: usize,
17+
dt: f64,
18+
timesteps: usize,
19+
dx: f64,
20+
x: Vec<f64>,
21+
dk: f64,
22+
k: Vec<f64>,
23+
im_time: bool,
24+
}
25+
26+
impl Parameters {
27+
pub fn new(xmax: f64, res: usize, dt: f64, timesteps: usize, im_time: bool) -> Parameters {
28+
let dx = 2.0_f64 * xmax / (res as f64);
29+
let mut x: Vec<f64> = Vec::with_capacity(res);
30+
let dk = PI / xmax;
31+
let mut k: Vec<f64> = Vec::with_capacity(res);
32+
for i in 0..res {
33+
x.push(xmax / (res as f64) - xmax + (i as f64) * dx);
34+
match i {
35+
i if (i < res / 2) => k.push((i as f64) * PI / xmax),
36+
_ => k.push(((i as f64) - (res as f64)) * PI / xmax),
37+
}
38+
}
39+
Parameters {
40+
xmax,
41+
res,
42+
dt,
43+
timesteps,
44+
im_time,
45+
dx,
46+
x,
47+
dk,
48+
k,
49+
}
50+
}
51+
}
52+
53+
struct Operators {
54+
v: Vec<Complex<f64>>,
55+
pe: Vec<Complex<f64>>,
56+
ke: Vec<Complex<f64>>,
57+
wfc: Vec<Complex<f64>>,
58+
}
59+
60+
impl Operators {
61+
pub fn new(par: &Parameters, v_offset: f64, wfc_offset: f64) -> Operators {
62+
let mut v: Vec<Complex<f64>> = Vec::with_capacity(par.res);
63+
let mut pe: Vec<Complex<f64>> = Vec::with_capacity(par.res);
64+
let mut ke: Vec<Complex<f64>> = Vec::with_capacity(par.res);
65+
let mut wfc: Vec<Complex<f64>> = Vec::with_capacity(par.res);
66+
67+
for i in 0..par.res {
68+
v.push(Complex::new(
69+
0.5_f64 * (par.x[i] - v_offset).powi(2),
70+
0.0_f64,
71+
));
72+
wfc.push(Complex::new(
73+
(-((par.x[i] - wfc_offset).powi(2)) / 2.0_f64).exp(),
74+
0.0_f64,
75+
));
76+
if par.im_time {
77+
ke.push(Complex::new(
78+
(-0.5_f64 * par.dt * par.k[i].powi(2)).exp(),
79+
0.0_f64,
80+
));
81+
pe.push(Complex::new((-0.5_f64 * par.dt * v[i].re).exp(), 0.0_f64));
82+
} else {
83+
ke.push(Complex::new(
84+
0.0_f64,
85+
(-0.5_f64 * par.dt * par.k[i].powi(2)).exp(),
86+
));
87+
pe.push(Complex::new(0.0_f64, (-0.5_f64 * par.dt * v[i].re).exp()));
88+
}
89+
}
90+
Operators { v, pe, ke, wfc }
91+
}
92+
}
93+
94+
fn fft(x: &mut Vec<Complex<f64>>, inverse: bool) {
95+
let mut y = vec![Complex::new(0.0_f64, 0.0_f64); x.len()];
96+
let mut p = FFTplanner::new(inverse);
97+
let fft = p.plan_fft(x.len());
98+
fft.process(x, &mut y);
99+
100+
for i in 0..x.len() {
101+
x[i] = y[i] / (x.len() as f64).sqrt();
102+
}
103+
}
104+
105+
fn split_op(par: &Parameters, opr: &mut Operators) {
106+
let mut density: Vec<f64>;
107+
108+
for i in 0..par.timesteps {
109+
for j in 0..par.res {
110+
opr.wfc[j] *= opr.pe[j];
111+
}
112+
113+
fft(&mut opr.wfc, false);
114+
115+
for j in 0..par.res {
116+
opr.wfc[j] *= opr.ke[j];
117+
}
118+
119+
fft(&mut opr.wfc, true);
120+
121+
for j in 0..par.res {
122+
opr.wfc[j] *= opr.pe[j];
123+
}
124+
125+
density = opr.wfc.iter().map(|x| x.norm().powi(2)).collect();
126+
127+
if par.im_time {
128+
let sum = density.iter().sum::<f64>() * par.dx;
129+
130+
for j in 0..par.res {
131+
opr.wfc[j] /= sum.sqrt();
132+
}
133+
}
134+
135+
// Writing data into a file in the format of:
136+
// index, density, real potential.
137+
let path_name = format!("output{}.dat", i);
138+
let path = Path::new(&path_name);
139+
let display = path.display();
140+
141+
let mut file = match File::create(&path) {
142+
Err(why) => panic!("Couldn't create {}: {}", display, why),
143+
Ok(good) => good,
144+
};
145+
146+
for j in 0..par.res {
147+
if let Err(why) = writeln!(file, "{}\t{}\t{}", j, density[j], opr.v[j].re) {
148+
panic!("Couldn't write to {}: {}", display, why)
149+
}
150+
if let Err(why) = file.flush() {
151+
panic!("Couldn't flush {}: {}", display, why)
152+
}
153+
}
154+
}
155+
}
156+
157+
fn calculate_energy(par: &Parameters, opr: &Operators) -> f64 {
158+
let wfc_r = opr.wfc.clone();
159+
let mut wfc_k = opr.wfc.clone();
160+
let mut wfc_c = vec![Complex::new(0.0_f64, 0.0_f64); par.res];
161+
162+
fft(&mut wfc_k, false);
163+
164+
for i in 0..par.res {
165+
wfc_c[i] = wfc_r[i].conj();
166+
}
167+
168+
let mut energy_k = vec![Complex::new(0.0_f64, 0.0_f64); par.res];
169+
let mut energy_r = vec![Complex::new(0.0_f64, 0.0_f64); par.res];
170+
171+
for i in 0..par.res {
172+
energy_k[i] = wfc_k[i] * Complex::new(par.k[i], 0.0_f64).powi(2);
173+
}
174+
175+
fft(&mut energy_k, true);
176+
177+
for i in 0..par.res {
178+
energy_k[i] *= wfc_c[i].scale(0.5_f64);
179+
energy_r[i] = wfc_c[i] * opr.v[i] * wfc_r[i];
180+
}
181+
182+
let energy_final = energy_k
183+
.into_iter()
184+
.zip(energy_r.into_iter())
185+
.fold(0.0_f64, |acc, x| acc + (x.0 + x.1).re);
186+
187+
energy_final * par.dx
188+
}
189+
190+
fn main() {
191+
let par = Parameters::new(5.0, 256, 0.05, 100, true);
192+
let mut opr = Operators::new(&par, 0.0, -1.0);
193+
194+
split_op(&par, &mut opr);
195+
196+
println!("The energy is {}", calculate_energy(&par, &opr));
197+
}

contents/split-operator_method/split-operator_method.md

+8
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ Regardless, we first need to set all the initial parameters, including the initi
108108
[import:11-30, lang:"python"](code/python/split_op.py)
109109
{% sample lang="hs" %}
110110
[import:17-47, lang:"haskell"](code/haskell/splitOp.hs)
111+
{% sample lang="rs" %}
112+
[import:14-51, lang:"rust"](code/rust/split_op.rs)
111113
{% endmethod %}
112114

113115
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`.
@@ -129,6 +131,8 @@ Afterwards, we turn them into operators:
129131
[import:33-54, lang:"python"](code/python/split_op.py)
130132
{% sample lang="hs" %}
131133
[import:49-66, lang:"haskell"](code/haskell/splitOp.hs)
134+
{% sample lang="rs" %}
135+
[import:53-92, lang:"rust"](code/rust/split_op.rs)
132136
{% endmethod %}
133137

134138
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.
@@ -150,6 +154,8 @@ The final step is to do the iteration, itself.
150154
[import:57-95, lang:"python"](code/python/split_op.py)
151155
{% sample lang="hs" %}
152156
[import:68-73, lang:"haskell"](code/haskell/splitOp.hs)
157+
{% sample lang="rs" %}
158+
[import:105-155, lang:"rust"](code/rust/split_op.rs)
153159
{% endmethod %}
154160

155161
And that's it.
@@ -185,6 +191,8 @@ Checking to make sure your code can output the correct energy for a harmonic tra
185191
[import:5-127, lang:"python"](code/python/split_op.py)
186192
{% sample lang="hs" %}
187193
[import, lang:"haskell"](code/haskell/splitOp.hs)
194+
{% sample lang="rs" %}
195+
[import, lang:"rust"](code/rust/split_op.rs)
188196
{% endmethod %}
189197

190198
<script>

0 commit comments

Comments
 (0)