/* 
This code runs Monte Carlo simulations for Lennard Jones particles of a single type, in the canonical (NVT) ensemble.

The simulation cell is assumed to be cubic.

R. K. Lindsey (2023)
*/

// Project 1 - Lindsey Shereda

#include<iostream>
#include<iomanip>
#include<fstream>
#include<string>
#include<vector>
#include<cmath>

#include<random>

using namespace std;

double  nav     = 6.02*pow(10.0,23);        // Avagadro's number
double  cm2m    = 1.0/100.0;                // x cm * cm2m = m
double  A2m     = 1.0/pow(10.0,10);         // x Angstrom * A to m = m    
double  kB      = 1.380649*pow(10,-23.0);   // Units: J⋅K^−1 

struct xyz
{
    double x;
    double y;
    double z;

    // Added this operator so I can identify unequal vectors (to make identifying when atom positions are equal easier)
    bool operator != (const xyz&other) const{
        return x != other.x || y != other.y || z != other.z;
    }
};

double get_dist(const xyz & a1, const xyz & a2, const xyz & boxdim, xyz & rij_vec)
{

    double scalar_dist;
    
    // Updates rij_vec with a1 & a2 information
    rij_vec.x = a2.x-a1.x;
    rij_vec.y = a2.y-a1.y;
    rij_vec.z = a2.z-a1.z;

    // Calculates the scalar distance
    scalar_dist = sqrt(pow(rij_vec.x,2)+pow(rij_vec.y,2)+pow(rij_vec.z,2));
    return scalar_dist;
}

double update_max_displacement(double fraction_accepted, double boxdim, double max_displacement)
{
    // Calculates new max_displacement based on acceptance criteria
    max_displacement -= round((max_displacement/boxdim)*boxdim);

    // Returns the new max displacement
    return max_displacement;
}

void write_frame(ofstream & trajstream, vector<xyz> & coords, string atmtyp, xyz & boxdim, int mcstep)
{
    trajstream << "ITEM: TIMESTEP" << endl;
    trajstream << mcstep << endl;
    trajstream << "ITEM: NUMBER OF ATOMS" << endl;
    trajstream << coords.size() << endl;
    trajstream << "ITEM: BOX BOUNDS pp pp pp" << endl;
    trajstream << "0 " << boxdim.x << endl;
    trajstream << "0 " << boxdim.y << endl;
    trajstream << "0 " << boxdim.z << endl;
    trajstream << "ITEM: ATOMS id type element xu yu zu" << endl;
    
    for (int i=0; i<coords.size(); i++)
        trajstream << i << " 1 " << atmtyp << " " << coords[i].x << " "<< coords[i].y << " "  << coords[i].z << endl;
}

double get_LJ_eij(double sigma, double epsilon, double rcut, double rij)
{
    double LJ_energy;
    if(rij <= rcut) {
        LJ_energy = 4*epsilon*(pow((sigma / rij),12)-pow((sigma / rij),6));
    }
    return LJ_energy;
}

void get_LJ_fij(double sigma, double epsilon, double rcut, double rij, const xyz & rij_vec, xyz & fij)
{
    double LJ_force;
    if(rij <= rcut) {
        fij.x = 48*epsilon*(((pow(sigma,12))/(pow(rij_vec.x,13)))-(0.5*((pow(sigma,6))/(pow(rij_vec.x,7)))));
        fij.y = 48*epsilon*(((pow(sigma,12))/(pow(rij_vec.y,13)))-(0.5*((pow(sigma,6))/(pow(rij_vec.y,7)))));
        fij.z = 48*epsilon*(((pow(sigma,12))/(pow(rij_vec.z,13)))-(0.5*((pow(sigma,6))/(pow(rij_vec.z,7)))));
    }
}

void get_single_particle_LJ_contributions(double sigma, double epsilon, double rcut, const vector<xyz> & coords, int selected_atom, const xyz & selected_atom_coords, const xyz & boxdim, double & energy_selected, xyz & stress_selected)
{
    energy_selected   = 0;
    stress_selected.x = 0;
    stress_selected.y = 0;
    stress_selected.z = 0;
    xyz force;
    force.x = 0;
    force.y = 0;
    force.z = 0;
    
    static double rij;
    static xyz    rij_vec;
    
    /* Write code to determine the contributions to the total system energy, forces, and stresses due to the selected atom.
    Self interactions should not be included. */
    
    for(int i=0; i<coords.size(); i++){
        
        // If statement filters out self interactions
        if (coords[i] != selected_atom_coords){
        
            // Gets scalar distance and updates distance vector
            double scalar_distance = get_dist(coords[i],selected_atom_coords,boxdim,rij_vec);

            // Gets LJ energy and accumulates into energy selected
            energy_selected += get_LJ_eij(sigma,epsilon,rcut,rij);
            
            // Updates LJ force vector and adds to stress selected
            get_LJ_fij(sigma,epsilon,rcut,rij,rij_vec,force);
            stress_selected.x = stress_selected.x + force.x;
            stress_selected.y = stress_selected.y + force.y;
            stress_selected.z = stress_selected.z + force.z;
        } else {
            return;
        }
    }
    }


int main(int argc, char* argv[])
{
    
    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Set user-defined variables (Read in from input file at some point)
    ///////////////////////////////////////////////////////////////////////////////////////////////
    
    int     seed    = stoi(argv[1]);// Seed for random number generator - read from commandline 
    double  redden  = stod(argv[2]);// Reduced density - read from commandline
    
    //////// Static variables 
    
    mt19937 mt(time(nullptr)); // Seed the (psuedo) random number 
    
    double  sigma   = 3.4;          // Units: Angstrom
    double  epsilon = 120;          // Units: K/k_B
    double  rcut    = 4*sigma;      // Model outer cutoff
    
    int     natoms  = 500;          // Number of atoms in the system
    string  atmtyp  = "Ar";         // Atom type (atomic symbol)
    double  molmass = 39.948;       // Mass of an particle of atmtyp (Atomic mass in the case of an argon atom)
    double  density = redden / pow(A2m,3.0) * pow(cm2m,3.0) / nav * molmass / pow(sigma,3.0);     // System density; Units: g/cm^3
    double  numden;                 // System number density; Units: atoms/Ang^3 - will be calculated later on
    
    double  temp    = 1.2*epsilon;  // Temperature, in K
    double  nsteps  = 5e6;          // Number of MC steps
    int     iofrq   = 2e4;          // Frequency to output statistics and trajectory
    int     nequil  = 2e6;          // Equilibration period (chemical potential and heat capacity only collected after this many steps)
    
    xyz         boxdim;             // Simulation box x, y, and z lengths - will be determined later on
    vector<xyz> coords;             // Coordinates for all atoms in our system; coords[0].x gives the x coordinate of the 0th atom - will be generated later on
    
    //////// Variables used for file i/o
    
    ofstream    trajstream; // Trajectory file - uses the LAMMPS lammpstrj format
        
    //////// Print information for user
    
    cout << "# Number of atoms:       " << natoms     << endl;
    cout << "# Atom type:             " << atmtyp     << endl;
    cout << "# Molar Mass (g/mol):    " << molmass    << endl;
    cout << "# Density (g/cm^3):      " << density    << endl;
    cout << "# LJ sigma (Angstrom):   " << sigma      << endl;
    cout << "# LJ epsilon/kB (K):     " << epsilon    << endl;
    cout << "# LJ cutoff (Angstrom):  " << rcut       << endl;
    cout << "# LJ cutoff (sigma):     " << rcut/sigma << endl;
    cout << "# Temperature (K):       " << temp       << endl;
    cout << "# Number MC steps:       " << nsteps     << endl;
    cout << "# Output frequency:      " << iofrq      << endl;
    

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Generate the system coordinates
    // Generates an initial configuation of atom on a 3D lattice  with a user-specified number of 
    // atoms, at the user-specified density. 
    ///////////////////////////////////////////////////////////////////////////////////////////////
    
    // Determine the box length that will yield the user-specified density. Start by computing the target number density.

    numden = density / molmass * nav / pow(cm2m,3.0) * pow(A2m,3.0); // Density in atoms per Ang^3
    
    cout << "# Num. den. (atoms/Ang): " << numden << endl;
    
    boxdim.x = pow(natoms/numden, 1.0/3.0); 
    boxdim.y = boxdim.x;
    boxdim.z = boxdim.x;
    
    cout << "# Box length (x):        " << boxdim.x << endl;
    cout << "# Box length (y):        " << boxdim.y << endl;
    cout << "# Box length (z):        " << boxdim.z << endl;

    // Compute the number of gridpoints to use in each direction (ceiling of the cube root of number of atoms).
    // Set the spacing in each dimension based on the box length and the number of gridpoints+1 (to prevent overlap
    // across the periodic boundary. 
    
    int     ngridpoints = ceil(pow(natoms,1.0/3.0));
    double  spacing     = boxdim.x / (ngridpoints+1);
    
    cout << "# Init. spacing (Ang):   " << spacing << endl;

    xyz     tmp_atom;
    int     added_atoms = 0;
    
    for (int x=0; x<ngridpoints; x++)
    {
        for(int y=0; y<ngridpoints; y++)
        {
            for(int z=0; z<ngridpoints; z++)  
            {
                if(added_atoms >= natoms)
                    continue;
            
                tmp_atom.x = x * spacing;
                tmp_atom.y = y * spacing;
                tmp_atom.z = z * spacing;

                coords.push_back(tmp_atom);
            
                added_atoms++;
            }
        }
    }
            
    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Print information on the initial configuration 
    ///////////////////////////////////////////////////////////////////////////////////////////////
    
    // Print this initial configuration to a file
    
    trajstream.open("MC_traj.lammpstrj");
    write_frame(trajstream, coords, atmtyp, boxdim, -1);

    // Print some information for the user
    
    cout << "# reduc. density (rho*): " << redden << endl;
    cout << "# reduc. temp (T*):      " << kB*temp / epsilon << endl;
    
    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Determine initial system energy and pressure
    ///////////////////////////////////////////////////////////////////////////////////////////////
    
    double  rij;                // Scalar distance between atoms
    xyz     rij_vec;            // Distance vector between atoms
    
    double  energy = 0;         // Energy for the configuration
   
    double widom_factor = 0;    // The exponential term in the Widom chemical potential expression
    double widom_trials = 0;    // Number of Widom insertion attempts
    double widom_avg    = 0;    // Average value of the wWidom factor
        
    double Eavg = 0;            // Average energy
    double Esqavg = 0;          // Square of average energy

   
    xyz     force;              // Force on a given atom
    xyz     stensor;            // System stress tensor (diagonal components only, i.e., xx, yy, zz)
    stensor.x = 0;
    stensor.y = 0;
    stensor.z = 0;
    
    for (int i=0; i<natoms; i++)                                                                         
    {
        for (int j=i+1; j<natoms; j++)
        {
                      
            // Determine atom pair's contirbution to total system energy
            
            energy = get_LJ_eij(sigma,epsilon,rcut,rij);
            return energy;
            
            // Determines the atom pair's contribution to the total system pressure, updates stensor vector

            get_LJ_fij(sigma,epsilon,rcut,rij,rij_vec,force);
            stensor.x = force.x;
            stensor.y = force.y;
            stensor.z = force.z;

        }
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Begin the simulation
    ///////////////////////////////////////////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////////////////////////////////////
    
    
    double max_displacement = 0.5*boxdim.x;  // Start trial displacements at one angstrom

    int     selected_atom;
    
    double  eold_selected;  // Pre-trial-move energy of the selected atom
    double  enew_selected;  // Post-trial-move energy of the selected atom
    double  delta_energy;   // Difference between old and new (trial) energy
    xyz     sold_selected;  // Pre-trial-move stress tensor diagonal of the selected atom
    xyz     snew_selected;  // Post-trial-move stress tensor diagonal of the selected atom
    
    xyz     trial_displacement;    
    xyz     trial_position;
    
    int     naccepted_moves = 0;    // Number of accepted moves
    double  fraction_accepted;      // Fraction of attempted moves that have been accepted
    int     nrunningav_moves = 0;   // Move index for running averages (doesn't start until equilibration period has ended)
    
    double pressure;
    double Cv;

    double stat_avgE   = 0;
    double stat_avgEsq = 0;
    double stat_avgP   = 0;

    for (int i=0; i<nsteps; i++)
    {
        // Select a random particle. The syntax below shows how to use the random number generator. This generate a random integer between 0 and natoms-1
        
        selected_atom = int(mt()*natoms);
        
        // Determine contributions to the system's energy, force, and stress due to the selected atom 
        
        get_single_particle_LJ_contributions(sigma, epsilon, rcut, coords, selected_atom, coords[selected_atom], boxdim, eold_selected, sold_selected);
        
        // Attempt to randomly move a particle - this is a multistep process
        
        // 1. Generate the trial **displacement** in x, y, and z - the particle should be able to move in positive
        // and negative directions, i.e., +/- the maximum displacement

        trial_displacement.x = (1-2*rand())*max_displacement;
        trial_displacement.y = (1-2*rand())*max_displacement;
        trial_displacement.z = (1-2*rand())*max_displacement;
        
        // 3. Apply PBC if the particle has moved outside the box
        
        if(trial_displacement.x > max_displacement){
            trial_displacement.x -= round((trial_displacement.x / boxdim.x)*boxdim.x);
        }

        if(trial_displacement.y > max_displacement){
            trial_displacement.y -= round((trial_displacement.y / boxdim.y)*boxdim.y);
        }

        if(trial_displacement.z > max_displacement){
            trial_displacement.z -= round((trial_displacement.z / boxdim.z)*boxdim.z);
        }
        
        // 2. Generate the trial **position** in x, y, and z based on the *corrected* displacement
        
        trial_position.x = coords[selected_atom].x + trial_displacement.x;
        trial_position.y = coords[selected_atom].y + trial_displacement.y;
        trial_position.z = coords[selected_atom].z + trial_displacement.z;

        // 4. Determine the energy contribution of that particle with the system **in it's trial position**
    
        get_single_particle_LJ_contributions(sigma, epsilon, rcut, coords, selected_atom, trial_position, boxdim, enew_selected, snew_selected);
        
        if (i >= nequil) // Only do Widom tests for the equilibrated portion of the simulation
        {
            // 5. Do a widom insertion test
        
            double ewidom;
            xyz    swidom;
            xyz    widom_position;
        
            // 5.a Generate another position for a new ghost atom
            
            widom_position.x = (1-2*rand())*max_displacement;
            widom_position.y = (1-2*rand())*max_displacement;
            widom_position.z = (1-2*rand())*max_displacement;
        
            // 5.b Calculate change in system energy due to insertion of new ghost atom 
        
            get_single_particle_LJ_contributions(sigma, epsilon, rcut, coords, -1, widom_position, boxdim, ewidom, swidom);
        
            // 5.c Update the Widom factor
        
            widom_factor = exp(-ewidom/temp);
            widom_avg += widom_factor;
            widom_trials++;
        }
        else
        {
            // Needed to avoid a divide-by-zero during equilibration phase when these values aren't collected
            widom_avg    = 1;
            widom_trials = 1;
        }
        
        // 6. Accept or reject the move
        
        delta_energy = enew_selected - eold_selected;

        if (mt() < exp(-delta_energy/temp)) // Then accept
        {
            // Then the system energy has decreased **or** our random number is less than our probability to accept
            
            // Update the system position
            coords[selected_atom].x = trial_position.x;
            coords[selected_atom].y = trial_position.y;
            coords[selected_atom].z = trial_position.z;
            
            // Update the system energy
            energy += delta_energy;
           
            // Update the system stress tensors
            stensor.x += snew_selected.x - sold_selected.x;
            stensor.y += snew_selected.y - sold_selected.y;
            stensor.z += snew_selected.z - sold_selected.z;

            // Update the number of accepted moves
            naccepted_moves += naccepted_moves + 1;
            
        }
        
        // Update maximum diplacement to target a 50% acceptance rate
        
        fraction_accepted = float(naccepted_moves)/float(i+1);
        
        max_displacement = update_max_displacement(fraction_accepted, boxdim.x, max_displacement);
       
        // print statistics if ineeded - don't forget to convert the stress tensor to pressure 
        // Compute instantaneous properties
        
        // Gets Volume through density variable and converts cm^3 to m^3 in the equation
        pressure = ((density / molmass*nav)/(1*10^-6))*kB*temp + 1/3*((density / molmass*nav)/(10*10^-6)) * (stensor.x + stensor.y + stensor.z); 
        Cv       = 0;

        if (i >= nequil) // Compute values for running averages, only using the equilibrated portion of the trajectory
        {
            stat_avgE   += energy;
            stat_avgEsq += energy*energy;        
            stat_avgP   += pressure/epsilon*pow(sigma,3.0);
            nrunningav_moves++;
            
            double avgE   = stat_avgE /float(nrunningav_moves);
            double avgEsq = stat_avgEsq / float(nrunningav_moves);
            
            Cv = (stat_avgEsq - pow(stat_avgE,2)) / (kB * pow(temp,2));
        }
 
        
        if ( (i+1) % iofrq == 0)
        {
            write_frame(trajstream, coords, atmtyp, boxdim, i);
            
            cout << "Step:  " << setw(10) << left << i;
            cout << " NAcc:  " << setw(10) << left << setprecision(3) <<  naccepted_moves;
            cout << " fAcc:  " << setw(10) << left << fixed << setprecision(3) << fraction_accepted;
            cout << " Maxd:  " << setw(10) << left << fixed << setprecision(5) << max_displacement;
            cout << " E*/N:  " << setw(10) << left << fixed << setprecision(5) << energy/natoms/epsilon;
            cout << " P*:     " << setw(10) << left << fixed << setprecision(5) << pressure/epsilon;
            cout << " P*cold: " << setw(10) << left << fixed << setprecision(5) << ((density/molmass*nav)/(1*10^-6))*kB*temp + 1/3*((density/molmass*nav)/(10*10^-6)) * (stensor.x + stensor.y + stensor.z);
            cout << " Mu_xs: " << setw(10) << left << fixed << setprecision(5) << log(widom_factor) / (1/kB*temp); 
            cout << " Cv*/N_xs:  " << setw(15) << left << fixed << setprecision(5) << (stat_avgEsq - pow(stat_avgE,2)) / (kB * pow(temp,2));
            cout << " E(kJ/mol): " << setw(10) << left << fixed << setprecision(3) << energy * 0.003814; // KJ/mol per K 
            cout << " P(bar):    " << setw(10) << left << fixed << setprecision(3) << pressure * 0.003814 * 10.0e30 * 1000/(6.02*10.0e23)*1.0e-5; // KJ/mol/A^3 to bar
            cout << endl;
 
        }
    }

    trajstream.close();
    
    stat_avgE    /= float(nrunningav_moves);
    stat_avgEsq  /= float(nrunningav_moves);
    Cv            = (stat_avgEsq - pow(stat_avgE,2)) / (kB * pow(temp,2));
    stat_avgE    *= 1.0/natoms/epsilon;

    cout << "# Computed average properties: " << endl;
    cout << " # E*/N:  "      << setw(10) << left << fixed << setprecision(5) << stat_avgE << endl;
    cout << " # P*:     "     << setw(10) << left << fixed << setprecision(5) << stat_avgP / float(nrunningav_moves) << endl;
    cout << " # Cv*/N_xs:   " << setw(15) << left << fixed << setprecision(5) << Cv << endl;
    cout << " # Mu_xs:  "     << setw(10) << left << fixed << setprecision(5) << log(widom_factor) / (1/kB*temp) << endl;       
    
} 

C++ Online Compiler

Write, Run & Share C++ code online using OneCompiler's C++ online compiler for free. It's one of the robust, feature-rich online compilers for C++ language, running on the latest version 17. Getting started with the OneCompiler's C++ compiler is simple and pretty fast. The editor shows sample boilerplate code when you choose language as C++ and start coding!

Read inputs from stdin

OneCompiler's C++ online compiler supports stdin and users can give inputs to programs using the STDIN textbox under the I/O tab. Following is a sample program which takes name as input and print your name with hello.

#include <iostream>
#include <string>
using namespace std;

int main() 
{
    string name;
    cout << "Enter name:";
    getline (cin, name);
    cout << "Hello " << name;
    return 0;
}

About C++

C++ is a widely used middle-level programming language.

  • Supports different platforms like Windows, various Linux flavours, MacOS etc
  • C++ supports OOPS concepts like Inheritance, Polymorphism, Encapsulation and Abstraction.
  • Case-sensitive
  • C++ is a compiler based language
  • C++ supports structured programming language
  • C++ provides alot of inbuilt functions and also supports dynamic memory allocation.
  • Like C, C++ also allows you to play with memory using Pointers.

Syntax help

Loops

1. If-Else:

When ever you want to perform a set of operations based on a condition If-Else is used.

if(conditional-expression) {
   //code
}
else {
   //code
}

You can also use if-else for nested Ifs and If-Else-If ladder when multiple conditions are to be performed on a single variable.

2. Switch:

Switch is an alternative to If-Else-If ladder.

switch(conditional-expression){    
case value1:    
 // code    
 break;  // optional  
case value2:    
 // code    
 break;  // optional  
......    
    
default:     
 code to be executed when all the above cases are not matched;    
} 

3. For:

For loop is used to iterate a set of statements based on a condition.

for(Initialization; Condition; Increment/decrement){  
  //code  
} 

4. While:

While is also used to iterate a set of statements based on a condition. Usually while is preferred when number of iterations are not known in advance.

while (condition) {  
// code 
}  

5. Do-While:

Do-while is also used to iterate a set of statements based on a condition. It is mostly used when you need to execute the statements atleast once.

do {  
 // code 
} while (condition); 

Functions

Function is a sub-routine which contains set of statements. Usually functions are written when multiple calls are required to same set of statements which increases re-usuability and modularity. Function gets run only when it is called.

How to declare a Function:

return_type function_name(parameters);

How to call a Function:

function_name (parameters)

How to define a Function:

return_type function_name(parameters) {  
 // code
}