The following is a Rust implementation of polymer simulation where the PBC(periodic boundary condition) has been implemented using the Ewald Summation technique.
Can you review the code?
Is the PBC properly implemented using the Ewal Summation?
use rand::Rng;
use std::f64::consts::PI;
use std::f64::consts::E;
use plotters::prelude::*;
const N: usize = 9;
const SIGMA: f64 = 2.0;
const PERIODIC_BOUNDARY: f64 = 5.0;
const TEMPERATURE: f64 = 2.0;
const MIN_ATOM_DISTANCE: f64 = 1.0;
const MAX_ATOM_DISTANCE: f64 = 3.8;
const SIM_STEPS: usize = 10000;
const WRITE_STEPS: usize = 10;
const EWALD_ALPHA: f64 = 0.5;
type Point = [f64; 2];
struct Polymer {
chain_vec: Vec<Point>,
}
impl Polymer {
fn new() -> Self {
Polymer {
chain_vec: Vec::new(),
}
}
fn plot_energies(&self, energies: &[f64]) {
let root = BitMapBackend::new("energies.png", (800, 600)).into_drawing_area();
root.fill(&WHITE).unwrap();
let (min, max) = (0, energies.len());
let (x_min, x_max) = (min as f64, max as f64);
let (y_min, y_max) = (energies.iter().fold(f64::INFINITY, |a, &b| a.min(b)),
energies.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b)));
let mut chart = ChartBuilder::on(&root)
.x_label_area_size(40)
.y_label_area_size(40)
.margin(10)
.caption("Energies", ("sans-serif", 40.0).into_font())
.build_cartesian_2d(x_min..x_max, y_min..y_max)
.unwrap();
chart.configure_mesh().draw().unwrap();
chart.draw_series(LineSeries::new(
(min..max).map(|x| (x as f64, energies[x])),
&RED,
)).unwrap();
println!("Check the output in file 'energies.png'");
}
fn apply_boundary_condition(&self, coord: Point) -> Point {
let mut x = coord[0];
let mut y = coord[1];
if x > PERIODIC_BOUNDARY {
x -= PERIODIC_BOUNDARY;
} else if x < 0.0 {
x += PERIODIC_BOUNDARY;
}
if y > PERIODIC_BOUNDARY {
y -= PERIODIC_BOUNDARY;
} else if y < 0.0 {
y += PERIODIC_BOUNDARY;
}
[x, y]
}
fn get_distance(&self, coord1: Point, coord2: Point) -> f64 {
let dx = coord1[0] - coord2[0];
let dy = coord1[1] - coord2[1];
let dx = dx - PERIODIC_BOUNDARY * (dx / PERIODIC_BOUNDARY).round();
let dy = dy - PERIODIC_BOUNDARY * (dy / PERIODIC_BOUNDARY).round();
(dx * dx + dy * dy).sqrt()
}
fn get_point_at_radius(&self, coord: Point, radius: f64) -> Point {
let angle = rand::thread_rng().gen::<f64>() * 2.0 * PI;
let x = (angle.cos() * radius) + coord[0];
let y = (angle.sin() * radius) + coord[1];
self.apply_boundary_condition([x, y])
}
fn morse_potential_func(&self, r: f64) -> f64 {
(E.powf(-2.0 * SIGMA * (r - MIN_ATOM_DISTANCE)))
- (2.0 * E.powf(-SIGMA * (r - MIN_ATOM_DISTANCE)))
}
fn ewald_short_range_potential(&self, coord: Point) -> f64 {
let mut potential = 0.0;
for (_i, &other_loc) in self.chain_vec.iter().enumerate() {
if self.get_distance(coord, other_loc) < MIN_ATOM_DISTANCE {
potential += self.morse_potential_func(MIN_ATOM_DISTANCE);
}
}
potential
}
fn ewald_long_range_potential(&self, coord: Point) -> f64 {
let mut potential = 0.0;
for m in -5..=5 {
for n in -5..=5 {
let mut sum = 0.0;
for &other_loc in self.chain_vec.iter() {
let dx = coord[0] - other_loc[0] + (m as f64) * PERIODIC_BOUNDARY;
let dy = coord[1] - other_loc[1] + (n as f64) * PERIODIC_BOUNDARY;
let r = (dx * dx + dy * dy).sqrt();
sum += self.morse_potential_func(r);
}
potential += sum;
}
}
potential * PI / (EWALD_ALPHA * EWALD_ALPHA)
}
fn get_potential(&self, index: usize) -> f64 {
let current_loc = self.chain_vec[index];
let short_range_potential = self.ewald_short_range_potential(current_loc);
let long_range_potential = self.ewald_long_range_potential(current_loc);
short_range_potential + long_range_potential
}
fn get_total_potential(&self) -> f64 {
let mut potential = 0.0;
for i in 0..self.chain_vec.len() {
potential += self.get_potential(i);
}
potential
}
fn initialize_polymer(&mut self) {
let mut current_coord = [0.0, 0.0];
self.chain_vec.push(current_coord);
for _ in 0..N - 1 {
let new_loc = self.get_point_at_radius(current_coord, MAX_ATOM_DISTANCE);
self.chain_vec.push(new_loc);
current_coord = new_loc;
}
}
fn run_simulation(&mut self, steps: usize) {
for _ in 0..steps {
let rand_index = rand::thread_rng().gen_range(0..self.chain_vec.len());
let old_loc = self.chain_vec[rand_index];
let new_loc = self.get_point_at_radius(old_loc, MAX_ATOM_DISTANCE);
self.chain_vec[rand_index] = new_loc;
let old_potential = self.get_potential(rand_index);
let new_potential = self.get_potential(rand_index);
let delta_energy = new_potential - old_potential;
if delta_energy > 0.0 {
let acceptance_probability = (-delta_energy / TEMPERATURE).exp();
if rand::thread_rng().gen::<f64>() > acceptance_probability {
self.chain_vec[rand_index] = old_loc;
}
}
}
}
}
fn main() {
let mut polymer = Polymer::new();
polymer.initialize_polymer();
let mut energies = Vec::new();
for step in 0..SIM_STEPS {
polymer.run_simulation(WRITE_STEPS);
let total_potential = polymer.get_total_potential();
energies.push(total_potential);
if step % WRITE_STEPS == 0 {
println!("Step: {}, Energy: {}", step, total_potential);
}
}
polymer.plot_energies(&energies);
}