Skip to content

Commit da0b521

Browse files
Merge pull request #8 from mdcourse/simplify_compute_potential
Simplify potential functions
2 parents 939501c + ff91e21 commit da0b521

File tree

5 files changed

+89
-130
lines changed

5 files changed

+89
-130
lines changed

docs/source/chapters/chapter2.rst

Lines changed: 0 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -293,86 +293,6 @@ Let us call the *identify_atom_properties* from the *__init__()* method:
293293
294294
.. label:: end_Prepare_class
295295

296-
..
297-
Calculate cross coefficients
298-
----------------------------
299-
300-
Let us calculate all cross coefficients that are required to calculate the interactions
301-
between atom :math:`i` and atom :math:`j`. From the example described previously,
302-
where:
303-
304-
.. math::
305-
306-
\text{atoms_sigma} = [\sigma_{11}, \sigma_{11}, \sigma_{22}, \sigma_{22}, \sigma_{22}]
307-
308-
one expects all direct and cross coefficients to be:
309-
310-
.. math::
311-
312-
\text{array_sigma_ij} = \begin{bmatrix}
313-
\sigma_{11} & \sigma_{11} & \sigma_{12} & \sigma_{12} & \sigma_{12} \\
314-
\sigma_{11} & \sigma_{11} & \sigma_{12} & \sigma_{12} & \sigma_{12} \\
315-
\sigma_{12} & \sigma_{12} & \sigma_{22} & \sigma_{22} & \sigma_{22} \\
316-
\sigma_{12} & \sigma_{12} & \sigma_{22} & \sigma_{22} & \sigma_{22} \\
317-
\sigma_{12} & \sigma_{12} & \sigma_{22} & \sigma_{22} & \sigma_{22} \\
318-
\end{bmatrix}
319-
320-
321-
The matrix is symmetric, so the coefficients in the bottom left corner are
322-
the same as the coefficient in the top right corner. The first value in the top left corner of the matrix,
323-
:math:`\sigma_{11}`, indicates that the :math:`\sigma` value for the interaction
324-
between the atom 1 and itself is is :math:`\sigma_{11}`. A similar matrix can
325-
be written for epsilon_sigma_ij.
326-
327-
Here, the values of the cross coefficients :math:`\sigma_{12}` and :math:`\epsilon_{12}`
328-
are assumed to follow the arithmetic mean :
329-
330-
.. math::
331-
332-
\sigma_{12} = (\sigma_{11}+\sigma_{22})/2 \\
333-
\epsilon_{12} = (\epsilon_{11}+\epsilon_{22})/2
334-
335-
Create the following method called *calculate_cross_coefficients* within the
336-
*Prepare* class:
337-
338-
. . label:: start_Prepare_class
339-
340-
.. code-block:: python
341-
342-
def calculate_cross_coefficients(self):
343-
"""Calculate all the cross-coefficients for the LJ interations."""
344-
self.identify_atom_properties() # TOFIX: this was left because of GCMC. Remove? Move?
345-
matrix_epsilon_ij = []
346-
matrix_sigma_ij = []
347-
for i in range(self.total_number_atoms):
348-
matrix_epsilon_ij.append([])
349-
matrix_sigma_ij.append([])
350-
for j in range(self.total_number_atoms):
351-
matrix_epsilon_ij[-1].append((self.atoms_epsilon[i]+self.atoms_epsilon[j])/2)
352-
matrix_sigma_ij[-1].append((self.atoms_sigma[i]+self.atoms_sigma[j])/2)
353-
self.matrix_sigma_ij = matrix_sigma_ij
354-
self.matrix_epsilon_ij = matrix_epsilon_ij
355-
356-
. . label:: end_Prepare_class
357-
358-
After calling for the *identify_atom_properties()* method, a double loop
359-
is performed over all direct coefficients, and the cross coefficients
360-
are stored within *matrix_sigma_ij* and *matrix_epsilon_ij*, two matrices.
361-
362-
Finally, let us call the *calculate_cross_coefficients* method from the
363-
*__init__()* method.
364-
365-
. . label:: start_Prepare_class
366-
367-
.. code-block:: python
368-
369-
def __init__(self,
370-
(...)
371-
self.identify_atom_properties()
372-
self.calculate_cross_coefficients()
373-
374-
. . label:: end_Prepare_class
375-
376296
Test the code
377297
-------------
378298

docs/source/chapters/chapter4.rst

Lines changed: 85 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -146,25 +146,21 @@ following *run()* method to the *MinimizeEnergy* class:
146146
# First, meevaluate the initial energy and max force
147147
self.update_neighbor_lists() # Rebuild neighbor list, if necessary
148148
self.update_cross_coefficients() # Recalculate the cross coefficients, if necessary
149-
try: # try using the last saved Epot and MaxF, if it exists
150-
init_Epot = self.Epot
151-
init_MaxF = self.MaxF
152-
except: # If Epot/MaxF do not exists yet, calculate them both
153-
init_Epot = self.compute_potential(output="potential")
154-
forces = self.compute_potential(output="force-vector")
155-
init_MaxF = np.max(np.abs(forces))
149+
if self.step == 0: # At the first step, Epot/MaxF do not exists yet, calculate them both
150+
init_Epot = self.compute_potential()
151+
forces, init_MaxF = self.compute_force()
156152
# Save the current atom positions
157153
init_positions = copy.deepcopy(self.atoms_positions)
158154
# Move the atoms in the opposite direction of the maximum force
159155
self.atoms_positions = self.atoms_positions \
160156
+ forces/init_MaxF*self.displacement
161157
# Recalculate the energy
162-
trial_Epot = self.compute_potential(output="potential")
158+
trial_Epot = self.compute_potential()
163159
# Keep the more favorable energy
164160
if trial_Epot < init_Epot: # accept new position
165161
self.Epot = trial_Epot
166162
# calculate the new max force and save it
167-
forces = self.compute_potential(output="force-vector")
163+
forces, init_MaxF = self.compute_force()
168164
self.MaxF = np.max(np.abs(forces))
169165
self.wrap_in_box() # Wrap atoms in the box, if necessary
170166
self.displacement *= 1.2 # Multiply the displacement by a factor 1.2
@@ -268,56 +264,99 @@ Compute_potential
268264
Computing the potential energy of the system is central to the energy minimizer,
269265
as the value of the potential is used to decide if the trial is accepted or
270266
rejected. Add the following method called *compute_potential()* to the *Utilities*
271-
class.
267+
class:
272268

273269
.. label:: start_Utilities_class
274270

275271
.. code-block:: python
276272
277-
def compute_potential(self, output):
278-
if output == "force-vector":
279-
forces = np.zeros((self.total_number_atoms,3))
280-
elif output == "force-matrix":
281-
forces = np.zeros((self.total_number_atoms,self.total_number_atoms,3))
273+
def compute_potential(self):
274+
"""Compute the potential energy by summing up all pair contributions."""
282275
energy_potential = 0
283-
box_size = self.box_size[:3]
284-
half_box_size = self.box_size[:3]/2.0
285276
for Ni in np.arange(self.total_number_atoms-1):
286-
# Read information about atom i
287-
position_i = self.atoms_positions[Ni]
277+
# Read neighbor list
288278
neighbor_of_i = self.neighbor_lists[Ni]
289-
# Read information about neighbors j and cross coefficient
290-
positions_j = self.atoms_positions[neighbor_of_i]
279+
# Measure distance
280+
rij = self.compute_distance(self.atoms_positions[Ni],
281+
self.atoms_positions[neighbor_of_i],
282+
self.box_size[:3])
283+
# Measure potential using information about cross coefficients
291284
sigma_ij = self.sigma_ij_list[Ni]
292285
epsilon_ij = self.epsilon_ij_list[Ni]
293-
# Measure distances
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)
298-
rij = np.linalg.norm(rij_xyz, axis=1)
299-
# Measure potential
300-
if output == "potential":
301-
energy_potential += np.sum(potentials(self.potential_type, epsilon_ij, sigma_ij, rij))
302-
else:
303-
derivative_potential = potentials(self.potential_type, epsilon_ij, sigma_ij, rij, derivative = True)
304-
if output == "force-vector":
305-
forces[Ni] += np.sum((derivative_potential*rij_xyz.T/rij).T, axis=0)
306-
forces[neighbor_of_i] -= (derivative_potential*rij_xyz.T/rij).T
307-
elif output == "force-matrix":
308-
forces[Ni][neighbor_of_i] += (derivative_potential*rij_xyz.T/rij).T
309-
if output=="potential":
310-
return energy_potential
311-
elif (output == "force-vector") | (output == "force-matrix"):
312-
return forces
286+
energy_potential += np.sum(potentials(self.potential_type,
287+
epsilon_ij, sigma_ij, rij))
288+
return energy_potential
289+
290+
.. label:: end_Utilities_class
291+
292+
Measuring the distance is an important step of computing the potential. Let us
293+
do it using a dedicated method. Add the following method to the *Utilities*
294+
class as well:
295+
296+
.. label:: start_Utilities_class
297+
298+
.. code-block:: python
299+
300+
def compute_distance(self,position_i, positions_j, box_size, only_norm = True):
301+
"""
302+
Measure the distances between two particles.
303+
The nan_to_num is crutial in 2D to avoid nan value along third dimension.
304+
# TOFIX: Move as function instead of a method?
305+
"""
306+
rij_xyz = np.nan_to_num(np.remainder(position_i - positions_j
307+
+ box_size[:3]/2.0, box_size) - box_size[:3]/2.0)
308+
if only_norm:
309+
return np.linalg.norm(rij_xyz, axis=1)
310+
else:
311+
return np.linalg.norm(rij_xyz, axis=1), rij_xyz
312+
313+
.. label:: end_Utilities_class
313314

315+
Finally, the energy minimization requires the computation of the minimum
316+
force in the system. Although not very different from the potential measurement,
317+
let us create a new method that is dedicated solely to measuring forces:
318+
319+
.. label:: start_Utilities_class
320+
321+
.. code-block:: python
322+
323+
def compute_force(self, return_vector = True):
324+
if return_vector: # return a N-size vector
325+
force_vector = np.zeros((self.total_number_atoms,3))
326+
else: # return a N x N matrix
327+
force_matrix = np.zeros((self.total_number_atoms,
328+
self.total_number_atoms,3))
329+
for Ni in np.arange(self.total_number_atoms-1):
330+
# Read neighbor list
331+
neighbor_of_i = self.neighbor_lists[Ni]
332+
# Measure distance
333+
rij, rij_xyz = self.compute_distance(self.atoms_positions[Ni],
334+
self.atoms_positions[neighbor_of_i],
335+
self.box_size[:3], only_norm = False)
336+
# Measure force using information about cross coefficients
337+
sigma_ij = self.sigma_ij_list[Ni]
338+
epsilon_ij = self.epsilon_ij_list[Ni]
339+
fij_xyz = potentials(self.potential_type, epsilon_ij,
340+
sigma_ij, rij, derivative = True)
341+
if return_vector:
342+
# Add the contribution to both Ni and its neighbors
343+
force_vector[Ni] += np.sum((fij_xyz*rij_xyz.T/rij).T, axis=0)
344+
force_vector[neighbor_of_i] -= (fij_xyz*rij_xyz.T/rij).T
345+
else:
346+
# Add the contribution to the matrix
347+
force_matrix[Ni][neighbor_of_i] += (fij_xyz*rij_xyz.T/rij).T
348+
if return_vector:
349+
max_force = np.max(np.abs(force_vector))
350+
return force_vector, max_force
351+
else:
352+
return force_matrix
353+
314354
.. label:: end_Utilities_class
315355

316-
Here, the method is a little bit complicated, because three types of outputs can
317-
be requested by the user: *force-vector*, *force-matrix*, and *potential*. The last
318-
one, *potential*, simply returns the value of the potential energy for the entire system.
319-
If *force-vector* or *force-matrix* are selected instead, then the individual forces
320-
between atoms are returned.
356+
Here, two types of outputs can
357+
be requested by the user: *force-vector*, and *force-matrix*.
358+
The *force-matrix* option will be useful for pressure calculation, see
359+
:ref:`chapter7-label`.
321360

322361
Wrap in box
323362
-----------

docs/source/chapters/chapter5.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ All quantities are re-dimensionalized before getting outputed.
8484
if code.thermo_period is not None:
8585
if code.step % code.thermo_period == 0:
8686
if code.step == 0:
87-
Epot = code.compute_potential(output="potential") \
87+
Epot = code.compute_potential() \
8888
* code.reference_energy # kcal/mol
8989
else:
9090
Epot = code.Epot * code.reference_energy # kcal/mol

docs/source/chapters/chapter6.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ Let us add a method named *monte_carlo_move* to the *MonteCarlo* class:
5050
try: # try using the last saved Epot, if it exists
5151
initial_Epot = self.Epot
5252
except: # If self.Epot does not exists yet, calculate it
53-
initial_Epot = self.compute_potential(output="potential")
53+
initial_Epot = self.compute_potential()
5454
# Make a copy of the initial atoms positions
5555
initial_positions = copy.deepcopy(self.atoms_positions)
5656
# Pick an atom id randomly
@@ -63,7 +63,7 @@ Let us add a method named *monte_carlo_move* to the *MonteCarlo* class:
6363
move = np.append((np.random.random(2) - 0.5) * self.displace_mc, 0)
6464
self.atoms_positions[atom_id] += move
6565
# Measure the optential energy of the new configuration
66-
trial_Epot = self.compute_potential(output="potential")
66+
trial_Epot = self.compute_potential()
6767
# Evaluate whether the new configuration should be kept or not
6868
beta = 1/self.desired_temperature
6969
delta_E = trial_Epot-initial_Epot

docs/source/chapters/chapter7.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ Let us add the following method to the *Utilities* class.
4949
temperature = self.desired_temperature # for MC, simply use the desired temperature
5050
p_ideal = Ndof*temperature/(volume*self.dimensions)
5151
# Compute the non-ideal contribution
52-
distances_forces = np.sum(self.compute_potential(output="force-matrix")*self.evaluate_rij_matrix())
52+
distances_forces = np.sum(self.compute_force(return_vector = False)*self.evaluate_rij_matrix())
5353
p_nonideal = distances_forces/(volume*self.dimensions)
5454
# Final pressure
5555
self.pressure = p_ideal+p_nonideal

0 commit comments

Comments
 (0)