@@ -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
268264Computing the potential energy of the system is central to the energy minimizer,
269265as the value of the potential is used to decide if the trial is accepted or
270266rejected. 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
322361Wrap in box
323362-----------
0 commit comments