Skip to content

Commit 939501c

Browse files
Merge pull request #7 from mdcourse/make-code-2D-compatible
start adapting the code to 2D
2 parents 4c05a3b + 13132ae commit 939501c

File tree

6 files changed

+62
-27
lines changed

6 files changed

+62
-27
lines changed

docs/source/chapters/chapter1.rst

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -66,33 +66,51 @@ between atoms. Although more complicated options exist, potentials are usually
6666
defined as functions of the distance :math:`r` between two atoms.
6767

6868
Within a dedicated folder, create the first file named *potentials.py*. This
69-
file will contain a function named *LJ_potential* for the Lennard-Jones
70-
potential (LJ). Copy the following into *potentials.py*:
69+
file will contain a function called *potentials*. Two types of potential can
70+
be returned by this function: the Lennard-Jones potential (LJ), and the
71+
hard-sphere potential.
72+
73+
Copy the following lines into *potentials.py*:
7174

7275
.. label:: start_potentials_class
7376

7477
.. code-block:: python
7578
76-
def LJ_potential(epsilon, sigma, r, derivative = False):
77-
if derivative:
78-
return 48*epsilon*((sigma/r)**12-0.5*(sigma/r)**6)/r
79+
def potentials(potential_type, epsilon, sigma, r, derivative=False):
80+
if potential_type == "Lennard-Jones":
81+
if derivative:
82+
return 48 * epsilon * ((sigma / r) ** 12 - 0.5 * (sigma / r) ** 6) / r
83+
else:
84+
return 4 * epsilon * ((sigma / r) ** 12 - (sigma / r) ** 6)
85+
elif potential_type == "Hard-Sphere":
86+
if derivative:
87+
raise ValueError("Derivative is not defined for Hard-Sphere potential.")
88+
else:
89+
return 1000 if r < sigma else 0
7990
else:
80-
return 4*epsilon*((sigma/r)**12-(sigma/r)**6)
91+
raise ValueError(f"Unknown potential type: {potential_type}")
8192
8293
.. label:: end_potentials_class
8394

84-
Depending on the value of the optional argument *derivative*, which can be
85-
either *False* or *True*, the *LJ_potential* function will return the force:
95+
The hard-sphere potential either returns a value of 0 when the distance between
96+
the two particles is larger than the parameter, :math:`r > \sigma`, or 1000 when
97+
:math:`r < \sigma`. The value of *1000* was chosen to be large enough to ensure
98+
that any Monte Carlo move that would lead to the two particles to overlap will
99+
be rejected.
100+
101+
In the case of the LJ potential, depending on the value of the optional
102+
argument *derivative*, which can be either *False* or *True*, the *LJ_potential*
103+
function will return the force:
86104

87105
.. math::
88106
89-
F_\text{LJ} = 48 \dfrac{\epsilon}{r} \left[ \left( \frac{\sigma}{r} \right)^{12}- \frac{1}{2} \left( \frac{\sigma}{r} \right)^6 \right],
107+
F_\text{LJ} = 48 \dfrac{\epsilon}{r} \left[ \left( \frac{\sigma}{r} \right)^{12} - \frac{1}{2} \left( \frac{\sigma}{r} \right)^6 \right],
90108
91109
or the potential energy:
92110

93111
.. math::
94112
95-
U_\text{LJ} = 4 \epsilon \left[ \left( \frac{\sigma}{r} \right)^{12}- \left( \frac{\sigma}{r} \right)^6 \right].
113+
U_\text{LJ} = 4 \epsilon \left[ \left( \frac{\sigma}{r} \right)^{12} - \left( \frac{\sigma}{r} \right)^6 \right].
96114
97115
Create the Classes
98116
------------------
@@ -124,7 +142,7 @@ copy the following lines:
124142

125143
.. code-block:: python
126144
127-
from potentials import LJ_potential
145+
from potentials import potentials
128146
129147
130148
class Utilities:

docs/source/chapters/chapter2.rst

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,14 @@ Modify the *Prepare* class as follows:
9999
epsilon=[0.1], # List - Kcal/mol
100100
sigma=[3], # List - Angstrom
101101
atom_mass=[10], # List - g/mol
102+
potential_type="Lennard-Jones",
102103
*args,
103104
**kwargs):
104105
self.number_atoms = number_atoms
105106
self.epsilon = epsilon
106107
self.sigma = sigma
107108
self.atom_mass = atom_mass
109+
self.potential_type = potential_type
108110
super().__init__(*args, **kwargs)
109111
110112
.. label:: end_Prepare_class
@@ -114,7 +116,10 @@ Here, the four lists *number_atoms* :math:`N`, *epsilon* :math:`\epsilon`,
114116
:math:`10`, :math:`0.1~\text{[Kcal/mol]}`, :math:`3~\text{[Å]}`, and
115117
:math:`10~\text{[g/mol]}`, respectively.
116118

117-
All four parameters are assigned to *self*, allowing other methods to access
119+
The type of potential is also specified, with Lennard-Jones being closen as
120+
the default option.
121+
122+
All the parameters are assigned to *self*, allowing other methods to access
118123
them. The *args* and *kwargs* parameters are used to accept an arbitrary number
119124
of positional and keyword arguments, respectively.
120125

@@ -130,7 +135,7 @@ unit system to the *LJ* unit system:
130135
.. code-block:: python
131136
132137
def calculate_LJunits_prefactors(self):
133-
"""Calculate the Lennard-Jones units prefacors."""
138+
"""Calculate the Lennard-Jones units prefactors."""
134139
# Define the reference distance, energy, and mass
135140
self.reference_distance = self.sigma[0] # Angstrom
136141
self.reference_energy = self.epsilon[0] # kcal/mol

docs/source/chapters/chapter3.rst

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ Let us improve the previously created *InitializeSimulation* class:
3535
):
3636
super().__init__(*args, **kwargs)
3737
self.box_dimensions = box_dimensions
38-
self.dimensions = len(box_dimensions)
38+
# If a box dimension was entered as 0, dimensions will be 2
39+
self.dimensions = len(list(filter(lambda x: x > 0, box_dimensions)))
3940
self.seed = seed
4041
self.initial_positions = initial_positions
4142
self.thermo_period = thermo_period
@@ -137,9 +138,14 @@ method to the *InitializeSimulation* class:
137138
.. code-block:: python
138139
139140
def define_box(self):
140-
box_boundaries = np.zeros((self.dimensions, 2))
141-
for dim, L in zip(range(self.dimensions), self.box_dimensions):
141+
"""Define the simulation box.
142+
For 2D simulations, the third dimensions only contains 0.
143+
"""
144+
box_boundaries = np.zeros((3, 2))
145+
dim = 0
146+
for L in self.box_dimensions:
142147
box_boundaries[dim] = -L/2, L/2
148+
dim += 1
143149
self.box_boundaries = box_boundaries
144150
box_size = np.diff(self.box_boundaries).reshape(3)
145151
box_geometry = np.array([90, 90, 90])
@@ -183,9 +189,8 @@ case, the array must be of size 'number of atoms' times 'number of dimensions'.
183189
184190
def populate_box(self):
185191
if self.initial_positions is None:
186-
atoms_positions = np.zeros((self.total_number_atoms,
187-
self.dimensions))
188-
for dim in np.arange(self.dimensions):
192+
atoms_positions = np.zeros((self.total_number_atoms, 3))
193+
for dim in np.arange(3):
189194
diff_box = np.diff(self.box_boundaries[dim])
190195
random_pos = np.random.random(self.total_number_atoms)
191196
atoms_positions[:, dim] = random_pos*diff_box-diff_box/2

docs/source/chapters/chapter4.rst

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,13 +291,16 @@ class.
291291
sigma_ij = self.sigma_ij_list[Ni]
292292
epsilon_ij = self.epsilon_ij_list[Ni]
293293
# Measure distances
294-
rij_xyz = (np.remainder(position_i - positions_j + half_box_size, box_size) - half_box_size)
294+
# Measure distances
295+
# The nan_to_num is crutial in 2D to avoid nan value along third dimension
296+
rij_xyz = np.nan_to_num(np.remainder(position_i - positions_j
297+
+ half_box_size, box_size) - half_box_size)
295298
rij = np.linalg.norm(rij_xyz, axis=1)
296299
# Measure potential
297300
if output == "potential":
298-
energy_potential += np.sum(LJ_potential(epsilon_ij, sigma_ij, rij))
301+
energy_potential += np.sum(potentials(self.potential_type, epsilon_ij, sigma_ij, rij))
299302
else:
300-
derivative_potential = LJ_potential(epsilon_ij, sigma_ij, rij, derivative = True)
303+
derivative_potential = potentials(self.potential_type, epsilon_ij, sigma_ij, rij, derivative = True)
301304
if output == "force-vector":
302305
forces[Ni] += np.sum((derivative_potential*rij_xyz.T/rij).T, axis=0)
303306
forces[neighbor_of_i] -= (derivative_potential*rij_xyz.T/rij).T

docs/source/chapters/chapter6.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,10 @@ Let us add a method named *monte_carlo_move* to the *MonteCarlo* class:
5757
atom_id = np.random.randint(self.total_number_atoms)
5858
# Move the chosen atom in a random direction
5959
# The maximum displacement is set by self.displace_mc
60-
move = (np.random.random(self.dimensions)-0.5)*self.displace_mc
60+
if self.dimensions == 3:
61+
move = (np.random.random(3)-0.5)*self.displace_mc
62+
elif self.dimensions == 2: # the third value will be 0
63+
move = np.append((np.random.random(2) - 0.5) * self.displace_mc, 0)
6164
self.atoms_positions[atom_id] += move
6265
# Measure the optential energy of the new configuration
6366
trial_Epot = self.compute_potential(output="potential")

docs/source/chapters/chapter7.rst

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,16 @@ Let us add the following method to the *Utilities* class.
3939
def calculate_pressure(self):
4040
"""Evaluate p based on the Virial equation (Eq. 4.4.2 in Frenkel-Smit,
4141
Understanding molecular simulation: from algorithms to applications, 2002)"""
42-
# Ideal contribution
42+
# Compute the ideal contribution
4343
Ndof = self.dimensions*self.total_number_atoms-self.dimensions
44-
volume = np.prod(self.box_size[:3])
44+
volume = np.prod(self.box_size[:self.dimensions])
4545
try:
4646
self.calculate_temperature() # this is for later on, when velocities are computed
4747
temperature = self.temperature
4848
except:
4949
temperature = self.desired_temperature # for MC, simply use the desired temperature
5050
p_ideal = Ndof*temperature/(volume*self.dimensions)
51-
# Non-ideal contribution
51+
# Compute the non-ideal contribution
5252
distances_forces = np.sum(self.compute_potential(output="force-matrix")*self.evaluate_rij_matrix())
5353
p_nonideal = distances_forces/(volume*self.dimensions)
5454
# Final pressure
@@ -73,7 +73,8 @@ To evaluate all the vectors between all the particles, let us also add the
7373
position_i = self.atoms_positions[Ni]
7474
rij_xyz = (np.remainder(position_i - positions_j + half_box_size, box_size) - half_box_size)
7575
rij_matrix[Ni] = rij_xyz
76-
return rij_matrix
76+
# use nan_to_num to avoid "nan" values in 2D
77+
return np.nan_to_num(rij_matrix)
7778
7879
.. label:: end_Utilities_class
7980

0 commit comments

Comments
 (0)