"""Contains the classes which deal with all the beads. Copyright (C) 2013, Joshua More and Michele Ceriotti This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Used for holding information about the beads, including their positions, masses momenta and kinetic energy. Has different objects for the position and normal mode representations, and has a special centroid atoms object for when the centroid coordinate is required. Classes: Beads: Class with methods dealing with all the beads. """ __all__ = ['Beads'] import numpy as np from ipi.utils.depend import * from ipi.engine.atoms import Atoms from ipi.utils import units class Beads(dobject): """Storage for the beads positions and velocities. Everything is stored as (nbeads,3*natoms) sized contiguous arrays, and a convenience-access to each replica of the system is provided through a list of Atoms objects. Contains arrays of both the normal mode representation and the position representation, and various sized arrays for the atom labels and masses. Also contains the potential and force between neighbouring replicas. Attributes: natoms: The number of atoms. nbeads: The number of beads. _blist: A list of Atoms objects for each replica of the system. Each replica is assumed to have the same mass and atom label. centroid: An atoms object giving the centroid coordinate of the beads. Depend objects: names: An array giving the atom names. m: An array giving the atom masses. m3: An array giving the mass associated with each degree of freedom. sm3: An array giving the square root of m3. q: An array giving all the bead positions. p: An array giving all the bead momenta. qc: An array giving the centroid positions. Depends on qnm. pc: An array giving the centroid momenta. Depends on pnm. vpath: The spring potential between the beads, divided by omegan**2. Depends on q. fpath: The spring force between the beads, divided by omegan**2. Depends on q. kins: A list of the kinetic energy of each replica. kin: The total kinetic energy of the system. Note that this is not the same as the estimate of the kinetic energy of the system, which is contained in the properties module. kstress: The total kinetic stress tensor for the system. rg: An array giving the radius of gyration of each atom. """ def __init__(self, natoms, nbeads): """Initialises Beads. Args: natoms: Number of atoms. nbeads: Number of beads. """ self.resize(natoms, nbeads) def resize(self, natoms, nbeads): """Creates all the data arrays needed in the simulation. Effectively initializes the whole Beads object, according to the specified number of atoms and beads. Is also used, as the name suggests, to resize the data to a new number of beads when this is necessary, for example in initialization from a simulation with a different number of beads. Also creates, or recreates, the dependency network, as this requires the data arrays to be created for it to work. Args: natoms: The number of atoms. nbeads: The number of beads. """ self.natoms = natoms self.nbeads = nbeads dset(self,"names", depend_array(name="names",value=np.zeros(natoms, np.dtype('|S6'))) ) # atom masses, and mass-related arrays dset(self,"m",depend_array(name="m",value=np.zeros(natoms, float)) ) # this is the prototype mass array (just one independent of bead n) dset(self,"m3", depend_array(name="m3",value=np.zeros((nbeads,3*natoms), float), # this is m conveniently replicated to be (nb,3*nat) func=self.mtom3, dependencies=[dget(self,"m")])) dset(self,"sm3", depend_array(name="sm3",value=np.zeros((nbeads,3*natoms), float), # this is just the square root of m3 func=self.m3tosm3, dependencies=[dget(self,"m3")])) # positions and momenta. bead representation, base storage used everywhere dset(self,"q", depend_array(name="q",value=np.zeros((nbeads,3*natoms), float)) ) dset(self,"p", depend_array(name="p",value=np.zeros((nbeads,3*natoms), float)) ) # position and momentum of the centroid dset(self,"qc", depend_array(name="qc",value=np.zeros(3*natoms, float), func=self.get_qc, dependencies=[dget(self,"q")] ) ) dset(self,"pc", depend_array(name="pc",value=np.zeros(3*natoms, float), func=self.get_pc, dependencies=[dget(self,"p")] ) ) # create proxies to access the centroid and the individual beads as Atoms objects self.centroid = Atoms(natoms, _prebind=(self.qc, self.pc, self.m, self.names)) self._blist = [Atoms(natoms, _prebind=( self.q[i,:], self.p[i,:], self.m, self.names )) for i in range(nbeads) ] # path springs potential and force dset(self,"vpath", depend_value(name="vpath", func=self.get_vpath, dependencies=[dget(self,"q")])) dset(self,"fpath", depend_array(name="fpath", value=np.zeros((nbeads,3*natoms), float), func=self.get_fpath, dependencies=[dget(self,"q")])) # kinetic energies of thhe beads, and total (classical) kinetic stress tensor dset(self,"kins", depend_array(name="kins",value=np.zeros(nbeads, float), func=self.kin_gather, dependencies=[dget(b,"kin") for b in self._blist])) dset(self,"kin", depend_value(name="kin", func=self.get_kin, dependencies=[dget(self,"kins")])) dset(self,"kstress", depend_array(name="kstress",value=np.zeros((3,3), float), func=self.get_kstress, dependencies=[dget(b,"kstress") for b in self._blist])) def copy(self): """Creates a new beads object from the original. Returns: A Beads object with the same q, p, m and names arrays as the original. """ newbd = Beads(self.natoms, self.nbeads) newbd.q[:] = self.q newbd.p[:] = self.p newbd.m[:] = self.m newbd.names[:] = self.names return newbd def m3tosm3(self): """Takes the mass array and returns the square rooted mass array.""" return np.sqrt(depstrip(self.m3)) def mtom3(self): """Takes the mass array for each bead and returns one with an element for each degree of freedom. Returns: An array of size (nbeads,3*natoms), with each element corresponding to the mass associated with the appropriate degree of freedom in q. """ m3 = np.zeros((self.nbeads,3*self.natoms),float) m3[:,0:3*self.natoms:3] = self.m m3[:,1:3*self.natoms:3] = m3[:,0:3*self.natoms:3] m3[:,2:3*self.natoms:3] = m3[:,0:3*self.natoms:3] return m3 def get_qc(self): """Gets the centroid coordinates.""" return np.dot(np.ones(self.nbeads,float),depstrip(self.q))/float(self.nbeads) def get_pc(self): """Gets the centroid momenta.""" return np.dot(np.ones(self.nbeads,float),depstrip(self.p))/float(self.nbeads) def kin_gather(self): """Gets the kinetic energy for all the replicas. Returns: A list of the kinetic energy for each system. """ return np.array([b.kin for b in self._blist]) def get_kin(self): """Gets the total kinetic energy of all the replicas. Note that this does not correspond to the total kinetic energy estimate for the system. Returns: The sum of the kinetic energy of each replica. """ return self.kins.sum() def get_kstress(self): """Calculates the total kinetic stress tensor of all the replicas. Note that this does not correspond to the quantum kinetic stress tensor estimate for the system. Returns: The sum of the kinetic stress tensor of each replica. """ ks = np.zeros((3,3),float) for b in range(self.nbeads): ks += self[b].kstress return ks def get_vpath(self): """Calculates the spring potential between the replicas. Note that this is actually the harmonic potential without being multiplied by the factor omegan**2, which is only available in the ensemble as the temperature is required to calculate it. """ epath = 0.0 q = depstrip(self.q) m = depstrip(self.m3)[0] for b in range(self.nbeads): if b > 0: dq = q[b,:] - q[b-1,:] else: dq = q[b,:] - q[self.nbeads-1,:] epath += np.dot(dq, m*dq) return epath*0.5 def get_fpath(self): """Calculates the spring force between the replicas. Note that this is actually the harmonic force without being multiplied by the factor omegan**2, which is only available in the ensemble as the temperature is required to calculate it. """ nbeads = self.nbeads natoms = self.natoms f = np.zeros((nbeads,3*natoms),float) q = depstrip(self.q) m = depstrip(self.m3)[0] for b in range(nbeads): if b > 0: dq = q[b,:] - q[b-1,:] else: dq = q[b,:] - q[self.nbeads-1,:] dq *= m f[b] -= dq if b > 0: f[b-1] += dq else: f[nbeads-1] += dq return f # A set of functions to access individual beads as Atoms objects def __len__(self): """Length function. This is called whenever the standard function len(beads) is used. Returns: The number of beads. """ return self.nbeads def __getitem__(self,index): """Overwrites standard getting function. This is called whenever the standard function beads[index] is used. Returns an Atoms object with the appropriate position and momenta arrays. Args: index: The index of the replica of the system to be accessed. Returns: The replica of the system given by the index. """ return self._blist[index] def __setitem__(self,index,value): """Overwrites standard setting function. This is called whenever the standard function beads[index]=value is used. Changes the position and momenta of the appropriate slice of the global position and momentum arrays to those given by value. Args: index: The replica of the system to be changed. value: The Atoms object that holds the new values. """ self._blist[index].p[:] = value.p self._blist[index].q[:] = value.q self._blist[index].m[:] = value.m self._blist[index].names[:] = value.names