def bind(self, beads=None, atoms=None, pm=None, prng=None, fixdof=None): """Binds the appropriate degrees of freedom to the thermostat. This takes an object with degrees of freedom, and makes their momentum and mass vectors members of the thermostat. It also then creates the objects that will hold the data needed in the thermostat algorithms and the dependency network. Args: beads: An optional beads object to take the mass and momentum vectors from. atoms: An optional atoms object to take the mass and momentum vectors from. pm: An optional tuple containing a single momentum value and its conjugate mass. prng: An optional pseudo random number generator object. Defaults to Random(). fixdof: An optional integer which can specify the number of constraints applied to the system. Defaults to zero. Raises: TypeError: Raised if no appropriate degree of freedom or object containing a momentum vector is specified for the thermostat to couple to. """ if prng is None: warning("Initializing thermostat from standard random PRNG", verbosity.medium) self.prng = Random() else: self.prng = prng if not beads is None: dset(self, "p", beads.p.flatten()) dset(self, "m", beads.m3.flatten()) elif not atoms is None: dset(self, "p", dget(atoms, "p")) dset(self, "m", dget(atoms, "m3")) elif not pm is None: dset(self, "p", pm[0]) dset(self, "m", pm[1]) else: raise TypeError( "Thermostat.bind expects either Beads, Atoms, NormalModes, or a (p,m) tuple to bind to" ) if fixdof is None: self.ndof = len(self.p) else: self.ndof = float(len(self.p) - fixdof) dset( self, "sm", depend_array(name="sm", value=np.zeros(len(dget(self, "m"))), func=self.get_sm, dependencies=[dget(self, "m")]))
def __init__( self, prefix, natoms, iseed, npl, npts, stride, dt, temperature, estimators ): """ Initialises planetary model simulation. Args: prefix : identifies trajectory files natoms : number of atoms iseed : random number seed stride : number of planetary steps between centroid updates dt : planetary time step in fs temperature : temperature in K estimators : list of modules defining estimators in the required format (see ipi/tools/py/estmod_example.py) """ self.prefix = prefix self.natoms = natoms self.iseed = iseed self.npl = npl self.npts = npts self.stride = stride self.dt = unit_to_internal("time", "femtosecond", dt) self.temperature = unit_to_internal("temperature", "kelvin", temperature) self.estimators = estimators self.prng = Random(self.iseed) self.beta = 1.0 / self.temperature # Centroid self.qc = np.zeros(3 * self.natoms) self.pc = np.zeros(3 * self.natoms) # Fluctuation (planet - centroid) self.p = np.zeros((3 * self.natoms, self.npl)) self.q = np.zeros((3 * self.natoms, self.npl)) # Dimensionless fluctuation self.qtil = np.zeros((3 * self.natoms, self.npl)) self.ptil = np.zeros((3 * self.natoms, self.npl)) # Total (planet coorindates) self.psum = np.zeros((3 * self.natoms, self.npl)) self.qsum = np.zeros((3 * self.natoms, self.npl)) # Path-integral frequency self.omega2 = np.zeros((3 * self.natoms, 3 * self.natoms)) self.omega = np.zeros((3 * self.natoms, 3 * self.natoms)) self.omega_old = np.zeros((3 * self.natoms, 3 * self.natoms)) self.omega_interp = np.zeros((3 * self.natoms, 3 * self.natoms)) # omega2 eigensystem self.evals = np.zeros(3 * self.natoms) self.evals_sqrt = np.zeros(3 * self.natoms) self.evecs = np.zeros((3 * self.natoms, 3 * self.natoms)) # Position smearing matrix self.a = np.zeros((3 * self.natoms, 3 * self.natoms)) self.a_sqrt = np.zeros((3 * self.natoms, 3 * self.natoms)) self.a_inv = np.zeros((3 * self.natoms, 3 * self.natoms)) # Momentum smearing matrix self.b = np.zeros((3 * self.natoms, 3 * self.natoms)) self.b_sqrt = np.zeros((3 * self.natoms, 3 * self.natoms)) self.b_inv = np.zeros((3 * self.natoms, 3 * self.natoms)) # Boolean mask for frequencies close to 0 self.mask = np.zeros(3 * self.natoms, dtype=bool) self.fomega2 = open("{}.omega2".format(self.prefix), "r") self.fqc = open("{}.xc.xyz".format(self.prefix), "r") self.fpc = open("{}.pc.xyz".format(self.prefix), "r") for i, mod in enumerate(self.estimators): if not hasattr(mod, "corr"): mod.corr = simple_corr mod.TCF = np.zeros(self.npts) mod.TCF_c = np.zeros(self.npts) mod.Aarr = np.zeros((self.npts, self.npl) + mod.Ashape) mod.Aarr_c = np.zeros((self.npts, 1) + mod.Ashape) mod.Atemp = np.zeros(np.prod(mod.Ashape)) mod.Barr = np.zeros((self.npts, self.npl) + mod.Bshape) mod.Barr_c = np.zeros((self.npts, 1) + mod.Bshape)
class Planets(object): """ Stores relevant trajectory info for planetary model simulation, calculates planetary dynamics and evaluates estimators and TCFs. """ def __init__( self, prefix, natoms, iseed, npl, npts, stride, dt, temperature, estimators ): """ Initialises planetary model simulation. Args: prefix : identifies trajectory files natoms : number of atoms iseed : random number seed stride : number of planetary steps between centroid updates dt : planetary time step in fs temperature : temperature in K estimators : list of modules defining estimators in the required format (see ipi/tools/py/estmod_example.py) """ self.prefix = prefix self.natoms = natoms self.iseed = iseed self.npl = npl self.npts = npts self.stride = stride self.dt = unit_to_internal("time", "femtosecond", dt) self.temperature = unit_to_internal("temperature", "kelvin", temperature) self.estimators = estimators self.prng = Random(self.iseed) self.beta = 1.0 / self.temperature # Centroid self.qc = np.zeros(3 * self.natoms) self.pc = np.zeros(3 * self.natoms) # Fluctuation (planet - centroid) self.p = np.zeros((3 * self.natoms, self.npl)) self.q = np.zeros((3 * self.natoms, self.npl)) # Dimensionless fluctuation self.qtil = np.zeros((3 * self.natoms, self.npl)) self.ptil = np.zeros((3 * self.natoms, self.npl)) # Total (planet coorindates) self.psum = np.zeros((3 * self.natoms, self.npl)) self.qsum = np.zeros((3 * self.natoms, self.npl)) # Path-integral frequency self.omega2 = np.zeros((3 * self.natoms, 3 * self.natoms)) self.omega = np.zeros((3 * self.natoms, 3 * self.natoms)) self.omega_old = np.zeros((3 * self.natoms, 3 * self.natoms)) self.omega_interp = np.zeros((3 * self.natoms, 3 * self.natoms)) # omega2 eigensystem self.evals = np.zeros(3 * self.natoms) self.evals_sqrt = np.zeros(3 * self.natoms) self.evecs = np.zeros((3 * self.natoms, 3 * self.natoms)) # Position smearing matrix self.a = np.zeros((3 * self.natoms, 3 * self.natoms)) self.a_sqrt = np.zeros((3 * self.natoms, 3 * self.natoms)) self.a_inv = np.zeros((3 * self.natoms, 3 * self.natoms)) # Momentum smearing matrix self.b = np.zeros((3 * self.natoms, 3 * self.natoms)) self.b_sqrt = np.zeros((3 * self.natoms, 3 * self.natoms)) self.b_inv = np.zeros((3 * self.natoms, 3 * self.natoms)) # Boolean mask for frequencies close to 0 self.mask = np.zeros(3 * self.natoms, dtype=bool) self.fomega2 = open("{}.omega2".format(self.prefix), "r") self.fqc = open("{}.xc.xyz".format(self.prefix), "r") self.fpc = open("{}.pc.xyz".format(self.prefix), "r") for i, mod in enumerate(self.estimators): if not hasattr(mod, "corr"): mod.corr = simple_corr mod.TCF = np.zeros(self.npts) mod.TCF_c = np.zeros(self.npts) mod.Aarr = np.zeros((self.npts, self.npl) + mod.Ashape) mod.Aarr_c = np.zeros((self.npts, 1) + mod.Ashape) mod.Atemp = np.zeros(np.prod(mod.Ashape)) mod.Barr = np.zeros((self.npts, self.npl) + mod.Bshape) mod.Barr_c = np.zeros((self.npts, 1) + mod.Bshape) def sample(self): """ Sample planetary momenta in dimensionless normal mode coordinates """ self.qtil[:] = self.prng.gvec((3 * self.natoms, self.npl)) self.ptil[:] = self.prng.gvec((3 * self.natoms, self.npl)) def read_omega2(self): """ Read in next instance of path-integral frequency matrix from <prefix>.omega2 """ omega2 = sparse.load_npz(self.fomega2, loader=netstring_encoded_loadz) # Convert self.omega2 to double precision (need at least single precision for np.linalg to work) self.omega2[:] = omega2.toarray().astype(np.float64) def read_qcpc(self): """ Read in next instances of centroid positions and momenta """ ret = read_file("xyz", self.fqc) # , readcell=True) self.qpos = ret["atoms"] ret = read_file("xyz", self.fpc) # , readcell=True) self.ppos = ret["atoms"] self.qc[:] = self.qpos.q self.pc[:] = self.ppos.q def matrix_setup(self): """ Set up and store as attributes the various matrices that need to be manipulated """ self.omega[:] = 0.0 self.a[:] = 0.0 self.a_inv[:] = 0.0 self.a_sqrt[:] = 0.0 self.b[:] = 0.0 self.b_inv[:] = 0.0 self.b_sqrt[:] = 0.0 self.evals[:], self.evecs[:] = np.linalg.eigh(self.omega2, UPLO="L") self.mask[:] = self.beta ** 2 * self.evals < 1e-14 self.evals[self.mask] = 0.0 self.evals_sqrt[:] = np.sqrt(self.evals) np.fill_diagonal(self.omega, self.evals_sqrt) # Calculate approximate smearing matrices self.a[~self.mask, ~self.mask] = ( 0.5 * self.beta * self.evals_sqrt[~self.mask] / np.tanh(0.5 * self.beta * self.evals_sqrt[~self.mask]) # <=> *coth() - 1.0 ) / (self.beta * self.evals[~self.mask]) self.a[self.mask, self.mask] = self.beta / 12.0 np.fill_diagonal(self.a_inv, 1.0 / np.diag(self.a)) np.fill_diagonal(self.a_sqrt, np.sqrt(np.diag(self.a))) self.b[~self.mask, ~self.mask] = ( self.evals[~self.mask] * self.a[~self.mask, ~self.mask] ) # *self.m[~self.mask]**2 self.b_inv[~self.mask, ~self.mask] = 1.0 / self.b[~self.mask, ~self.mask] np.fill_diagonal(self.b_sqrt, np.sqrt(np.diag(self.b))) # Undiagonalize matrices inplace self.eigenbasis(self.omega, backward=True) self.eigenbasis(self.a, backward=True) self.eigenbasis(self.a_sqrt, backward=True) self.eigenbasis(self.a_inv, backward=True) self.eigenbasis(self.b, backward=True) self.eigenbasis(self.b_sqrt, backward=True) self.eigenbasis(self.b_inv, backward=True) # Ensure appropriate scaling by mass matrix - very important # to to this in the correct basis! self.a[:] = multi_dot([self.mmat_sqrt_inv, self.a, self.mmat_sqrt_inv]) self.a_sqrt[:] = multi_dot([self.mmat_qrt_inv, self.a_sqrt, self.mmat_qrt_inv]) self.a_inv[:] = multi_dot([self.mmat_sqrt, self.a_inv, self.mmat_sqrt]) self.b[:] = multi_dot([self.mmat_sqrt, self.b, self.mmat_sqrt]) self.b_sqrt[:] = multi_dot([self.mmat_qrt, self.b_sqrt, self.mmat_qrt]) self.b_inv[:] = multi_dot([self.mmat_sqrt_inv, self.b_inv, self.mmat_sqrt_inv]) def eigenbasis(self, mat, backward=False): """ Transfrom a given matrix in place to or from the eigenbasis of the path-integral frequency matrix. Args: mat : matrix with shape (3*natoms, 3*natoms) backward : boolean specifying whether to transform to or from eigenbasis """ if backward: mat[:] = multi_dot([self.evecs, mat, self.evecs.transpose()]) else: mat[:] = multi_dot([self.evecs.transpose(), mat, self.evecs]) def step(self): """ Evolve planets for time stride*dt """ self.omega_old[:] = self.omega.copy() self.read_omega2() self.read_qcpc() self.matrix_setup() for j in range(self.stride): # Linear interpolation of frequency matrix self.omega_interp[:] = ( j * self.omega + (self.stride - j) * self.omega_old ) / self.stride # Velocity verlet integrator self.ptil[:] -= 0.5 * self.dt * matmul(self.omega_interp, self.qtil) self.qtil[:] += self.dt * matmul(self.omega_interp, self.ptil) self.ptil[:] -= 0.5 * self.dt * matmul(self.omega_interp, self.qtil) self.q[:] = multi_dot( [self.mmat_qrt_inv, self.a_sqrt, self.mmat_qrt, self.qtil] ) self.p[:] = multi_dot( [self.mmat_qrt, self.b_sqrt, self.mmat_qrt_inv, self.ptil] ) self.qsum[:] = self.q + self.qc[:, np.newaxis] self.psum[:] = self.p + self.pc[:, np.newaxis] def estimate(self, i): """ Evaluate TCF estimators. Args: i : index specifying number of centroid samples elapsed """ for mod in self.estimators: if hasattr(mod, "Afunc0"): # Might as well calculate centroid TCF at little extra cost mod.Barr_c[i, :] += mod.Bfunc( self.qc[:, np.newaxis], self.pc[:, np.newaxis] ) mod.Aarr_c[i, :] += mod.Afunc0( self.qc[:, np.newaxis], self.pc[:, np.newaxis] ) # Deal with B(Q_0+q) first, this is easy mod.Barr[i, :] += mod.Bfunc(self.qsum, self.psum) # Now deal with f_A(Q0+q,p) if mod.method == "0thorder_re": # 0th order term (evaluate the function A at the planet position) mod.Aarr[i, :] += mod.Afunc0(self.qsum, self.psum) elif mod.method == "1storder_im": # 1st order term, with shape given by # ( npl, prod(mod.Ashape), 3*self.natoms) A1q = mod.Afunc1(self.qsum) for j in range(self.npl): mod.Atemp[:] = 0.0 for k, val in enumerate(A1q[j]): mod.Atemp[k] = multi_dot([self.p[:, j], self.b_inv, val]) mod.Aarr[i, j, :] += mod.Atemp.reshape(mod.Ashape) / 2.0 elif mod.method == "2ndorder_re": # 0th order term (evaluate the function A at the planet position) mod.Aarr[i, :] += mod.Afunc0(self.qsum, self.psum) # 2nd order term, with shape given by # ( npl, prod(mod.Ashape), 3*self.natoms, 3*self.natoms ) A2q = mod.Afunc2(self.qsum) for j in range(self.npl): mod.Atemp[:] = 0.0 for k, val in enumerate(A2q[j]): mod.Atemp[k] = np.trace(matmul(self.b_inv, val)) - multi_dot( [self.p[:, j], self.b_inv, val, self.b_inv, self.p[:, j]] ) mod.Aarr[i, j, :] += mod.Atemp.reshape(mod.Ashape) / 8.0 else: raise ValueError( "f_A approximation method '{}' not recognised".format(mod.method) ) def correlate(self): """ Run at the end to calculate the TCFs """ for mod in self.estimators: mod.TCF[:] = mod.corr(mod.Aarr, mod.Barr, **self.__dict__) mod.TCF_c[:] = mod.corr(mod.Aarr_c, mod.Barr_c, **self.__dict__) def write_tcfs(self): """ Run at the end to write the TCFs to .dat files """ tarr = np.arange(self.npts) * self.dt * self.stride for mod in self.estimators: if hasattr(mod, "Afunc0"): np.savetxt( "{}_ce_{}.dat".format(self.prefix, mod.name), np.transpose([tarr, mod.TCF_c]), ) print( "Saved {} (centroid) to {}_ce_{}.dat".format( mod.name, self.prefix, mod.name ) ) np.savetxt( "{}_pl_{}.dat".format(self.prefix, mod.name), np.transpose([tarr, mod.TCF]), ) print( "Saved {} (full) to {}_pl_{}.dat".format( mod.name, self.prefix, mod.name ) ) def shutdown(self): """ Prepare to end simulation by safely closing any open files and removing temporary files """ # os.remove("TEMP2_PLANETARY") self.fomega2.close() self.fqc.close() self.fpc.close() def get_masses(self): """ Deterine mass matrix based on identities of atoms """ m = dstrip(self.qpos.m) self.m = np.concatenate((m, m, m)) self.m[:] = self.m.reshape(3, -1).transpose().reshape(-1) self.mmat = np.diag(self.m) self.mmat_sqrt = np.sqrt(self.mmat) self.mmat_qrt = np.sqrt(self.mmat_sqrt) self.mmat_inv = np.linalg.inv(self.mmat) self.mmat_sqrt_inv = np.linalg.inv(self.mmat_sqrt) self.mmat_qrt_inv = np.linalg.inv(self.mmat_qrt) def simulation(self, correlate=True, write=True, shutdown=True): """ Execute this method to run the planetary simulation. Args: correlate : set to True to evaluate TCFs after full time elapsed write : set to True to write TCFs to .dat files shutdown : set to True to safely shutdown after simulation complete by calling self.shutdown() """ print("STARTING PLANETARY SIMULATION") self.read_omega2() self.read_qcpc() self.get_masses() self.matrix_setup() self.sample() self.q[:] = multi_dot( [self.mmat_qrt_inv, self.a_sqrt, self.mmat_qrt, self.qtil] ) self.p[:] = multi_dot( [self.mmat_qrt, self.b_sqrt, self.mmat_qrt_inv, self.ptil] ) self.qsum[:] = self.q + self.qc[:, np.newaxis] self.psum[:] = self.p + self.pc[:, np.newaxis] self.estimate(0) self.count = 0 for i in range(self.npts - 1): self.step() self.count += 1 # this might be pointless, obviously self.count = i self.estimate(i + 1) print("Completed step {:d} of {:d}".format(self.count, self.npts - 1)) sys.stdout.flush() if correlate: print("CALCULATING TCFS") self.correlate() if write: print("SAVING TCFS") self.write_tcfs() if shutdown: print("SIMULATION COMPLETE. SHUTTING DOWN.") self.shutdown() else: warning("Reached end of simulation but files may remain open.")
def bind(self, beads=None, atoms=None, pm=None, nm=None, prng=None, fixdof=None): """Binds the appropriate degrees of freedom to the thermostat. This takes an object with degrees of freedom, and makes their momentum and mass vectors members of the thermostat. It also then creates the objects that will hold the data needed in the thermostat algorithms and the dependency network. Actually, this specific thermostat requires being called on a beads object. Args: nm: An optional normal modes object to take the mass and momentum vectors from. prng: An optional pseudo random number generator object. Defaults to Random(). fixdof: An optional integer which can specify the number of constraints applied to the system. Defaults to zero. Raises: TypeError: Raised if no beads object is specified for the thermostat to couple to. """ dself = dd(self) if nm is None or not type(nm) is NormalModes: raise TypeError( "ThermoNMGLE.bind expects a NormalModes argument to bind to") if prng is None: self.prng = Random() else: self.prng = prng if nm.nbeads != self.nb: raise IndexError( "The parameters in nm_gle options correspond to a bead number " + str(self.nb) + " which does not match the number of beads in the path" + str(nm.nbeads)) # allocates, initializes or restarts an array of s's if self.s.shape != (self.nb, self.ns + 1, nm.natoms * 3): if len(self.s) > 0: warning( "Mismatch in GLE s array size on restart, will reinitialise to free particle.", verbosity.low, ) self.s = np.zeros((self.nb, self.ns + 1, nm.natoms * 3)) # Initializes the s vector in the free-particle limit info( " GLE additional DOFs initialised to the free-particle limit.", verbosity.low, ) for b in range(self.nb): SC = stab_cholesky(self.C[b] * Constants.kb) self.s[b] = np.dot(SC, self.prng.gvec(self.s[b].shape)) else: info("GLE additional DOFs initialised from input.", verbosity.medium) prev_ethermo = self.ethermo # creates a set of thermostats to be applied to individual normal modes self._thermos = [ ThermoGLE(temp=1, dt=1, A=self.A[b], C=self.C[b]) for b in range(self.nb) ] # must pipe all the dependencies in such a way that values for the nm # thermostats are automatically updated based on the "master" thermostat def make_Agetter(k): return lambda: self.A[k] def make_Cgetter(k): return lambda: self.C[k] it = 0 for t in self._thermos: t.s = self.s[it] # gets the s's as a slice of self.s t.bind( pm=(nm.pnm[it, :], nm.dynm3[it, :]), prng=self.prng) # bind thermostat t to the it-th normal mode # pipes temp and dt dpipe(dself.temp, dd(t).temp) dpipe(dself.dt, dd(t).dt) # here we pipe the A and C of individual NM to the "master" arrays dd(t).A.add_dependency(dself.A) dd(t).A._func = make_Agetter(it) dd(t).C.add_dependency(dself.C) dd(t).C._func = make_Cgetter(it) dself.ethermo.add_dependency(dd(t).ethermo) it += 1 # since the ethermo will be "delegated" to the normal modes thermostats, # one has to split # any previously-stored value between the sub-thermostats for t in self._thermos: t.ethermo = prev_ethermo / self.nb dself.ethermo._func = self.get_ethermo
class ThermoNMGLE(Thermostat): """Represents a 'normal-modes' generalized Langevin equation thermostat. An extension to the GLE thermostat which is applied in the normal modes representation, and which allows to use a different GLE for each normal mode Attributes: ns: The number of auxilliary degrees of freedom. nb: The number of beads. s: An array holding all the momenta, including the ones for the auxilliary degrees of freedom. Depend objects: A: Drift matrix giving the damping time scales for all the different degrees of freedom (must contain nb terms). C: Static covariance matrix. Satisfies A.C + C.transpose(A) = B.transpose(B), where B is the diffusion matrix, giving the strength of the coupling of the system with the heat bath, and thus the size of the stochastic contribution of the thermostat. """ def get_C(self): """Calculates C from temp (if C is not set explicitely).""" rv = np.ndarray((self.nb, self.ns + 1, self.ns + 1), float) for b in range(0, self.nb): rv[b] = np.identity(self.ns + 1, float) * self.temp return rv[:] def __init__(self, temp=1.0, dt=1.0, A=None, C=None, ethermo=0.0): """Initialises ThermoGLE. Args: temp: The simulation temperature. Defaults to 1.0. dt: The simulation time step. Defaults to 1.0. A: An optional matrix giving the drift matrix. Defaults to a single value of 1.0. C: An optional matrix giving the covariance matrix. Defaults to an identity matrix times temperature with the same dimensions as the total number of degrees of freedom in the system. ethermo: The initial heat energy transferred to the bath. Defaults to 0.0. Will be non-zero if the thermostat is initialised from a checkpoint file. """ super(ThermoNMGLE, self).__init__(temp, dt, ethermo) dself = dd(self) if A is None: A = np.identity(1, float) dself.A = depend_value(value=A.copy(), name="A") self.nb = len(self.A) self.ns = len(self.A[0]) - 1 # now, this is tricky. if C is taken from temp, then we want it to be # updated as a depend of temp. # Otherwise, we want it to be an independent beast. if C is None: dself.C = depend_value(name="C", func=self.get_C, dependencies=[dself.temp]) else: dself.C = depend_value(value=C.copy(), name="C") def bind(self, beads=None, atoms=None, pm=None, nm=None, prng=None, fixdof=None): """Binds the appropriate degrees of freedom to the thermostat. This takes an object with degrees of freedom, and makes their momentum and mass vectors members of the thermostat. It also then creates the objects that will hold the data needed in the thermostat algorithms and the dependency network. Actually, this specific thermostat requires being called on a beads object. Args: nm: An optional normal modes object to take the mass and momentum vectors from. prng: An optional pseudo random number generator object. Defaults to Random(). fixdof: An optional integer which can specify the number of constraints applied to the system. Defaults to zero. Raises: TypeError: Raised if no beads object is specified for the thermostat to couple to. """ dself = dd(self) if nm is None or not type(nm) is NormalModes: raise TypeError( "ThermoNMGLE.bind expects a NormalModes argument to bind to") if prng is None: self.prng = Random() else: self.prng = prng if nm.nbeads != self.nb: raise IndexError( "The parameters in nm_gle options correspond to a bead number " + str(self.nb) + " which does not match the number of beads in the path" + str(nm.nbeads)) # allocates, initializes or restarts an array of s's if self.s.shape != (self.nb, self.ns + 1, nm.natoms * 3): if len(self.s) > 0: warning( "Mismatch in GLE s array size on restart, will reinitialise to free particle.", verbosity.low, ) self.s = np.zeros((self.nb, self.ns + 1, nm.natoms * 3)) # Initializes the s vector in the free-particle limit info( " GLE additional DOFs initialised to the free-particle limit.", verbosity.low, ) for b in range(self.nb): SC = stab_cholesky(self.C[b] * Constants.kb) self.s[b] = np.dot(SC, self.prng.gvec(self.s[b].shape)) else: info("GLE additional DOFs initialised from input.", verbosity.medium) prev_ethermo = self.ethermo # creates a set of thermostats to be applied to individual normal modes self._thermos = [ ThermoGLE(temp=1, dt=1, A=self.A[b], C=self.C[b]) for b in range(self.nb) ] # must pipe all the dependencies in such a way that values for the nm # thermostats are automatically updated based on the "master" thermostat def make_Agetter(k): return lambda: self.A[k] def make_Cgetter(k): return lambda: self.C[k] it = 0 for t in self._thermos: t.s = self.s[it] # gets the s's as a slice of self.s t.bind( pm=(nm.pnm[it, :], nm.dynm3[it, :]), prng=self.prng) # bind thermostat t to the it-th normal mode # pipes temp and dt dpipe(dself.temp, dd(t).temp) dpipe(dself.dt, dd(t).dt) # here we pipe the A and C of individual NM to the "master" arrays dd(t).A.add_dependency(dself.A) dd(t).A._func = make_Agetter(it) dd(t).C.add_dependency(dself.C) dd(t).C._func = make_Cgetter(it) dself.ethermo.add_dependency(dd(t).ethermo) it += 1 # since the ethermo will be "delegated" to the normal modes thermostats, # one has to split # any previously-stored value between the sub-thermostats for t in self._thermos: t.ethermo = prev_ethermo / self.nb dself.ethermo._func = self.get_ethermo def step(self): """Updates the thermostat in NM representation by looping over the individual DOFs. """ for t in self._thermos: t.step() def get_ethermo(self): """Computes the total energy transferred to the heat bath for all the nm thermostats. """ et = 0.0 for t in self._thermos: et += t.ethermo return et
def bind( self, beads=None, atoms=None, pm=None, nm=None, prng=None, bindcentroid=True, fixdof=None, ): """Binds the appropriate degrees of freedom to the thermostat. This takes a beads object with degrees of freedom, and makes its momentum and mass vectors members of the thermostat. It also then creates the objects that will hold the data needed in the thermostat algorithms and the dependency network. Gives the interface for both the PILE_L and PILE_G thermostats, which only differ in their treatment of the centroid coordinate momenta. Args: nm: An optional normal mode object to take the mass and momentum vectors from. prng: An optional pseudo random number generator object. Defaults to Random(). bindcentroid: An optional boolean which decides whether a Langevin thermostat is attached to the centroid mode of each atom separately, or the total kinetic energy. Defaults to True, which gives a thermostat bound to each centroid momentum. fixdof: An optional integer which can specify the number of constraints applied to the system. Defaults to zero. Raises: TypeError: Raised if no appropriate degree of freedom or object containing a momentum vector is specified for the thermostat to couple to. """ dself = dd(self) if nm is None or not type(nm) is NormalModes: raise TypeError( "ThermoPILE_L.bind expects a NormalModes argument to bind to") if prng is None: self.prng = Random() else: self.prng = prng prev_ethermo = self.ethermo # creates a set of thermostats to be applied to individual normal modes self._thermos = [ ThermoLangevin(temp=1, dt=1, tau=1) for b in range(nm.nbeads) ] # optionally does not bind the centroid, so we can re-use all of this # in the PILE_G case if not bindcentroid: self._thermos[0] = None self.nm = nm dself.tauk = depend_array( name="tauk", value=np.zeros(nm.nbeads - 1, float), func=self.get_tauk, dependencies=[dself.pilescale, dd(nm).dynomegak], ) # must pipe all the dependencies in such a way that values for the nm thermostats # are automatically updated based on the "master" thermostat def make_taugetter(k): return lambda: self.tauk[k - 1] it = 0 nm.pnm.hold() for t in self._thermos: if t is None: it += 1 continue if it > 0: fixdof = None # only the centroid thermostat may have constraints # bind thermostat t to the it-th bead t.bind(pm=(nm.pnm[it, :], nm.dynm3[it, :]), prng=self.prng, fixdof=fixdof) # pipes temp and dt dpipe(dself.temp, dd(t).temp) dpipe(dself.dt, dd(t).dt) # for tau it is slightly more complex if it == 0: dpipe(dself.tau, dd(t).tau) else: # Here we manually connect _thermos[i].tau to tauk[i]. # Simple and clear. dd(t).tau.add_dependency(dself.tauk) dd(t).tau._func = make_taugetter(it) dself.ethermo.add_dependency(dd(t).ethermo) dself.ethermo.hold() # will manually update ethermo when needed! it += 1 # since the ethermo will be "delegated" to the normal modes thermostats, # one has to split # any previously-stored value between the sub-thermostats if bindcentroid: for t in self._thermos: t.ethermo = prev_ethermo / nm.nbeads dself.ethermo._func = self.get_ethermo
def bind(self, nm=None, prng=None, fixdof=None): """Binds the appropriate degrees of freedom to the thermostat. This takes an object with degrees of freedom, and makes their momentum and mass vectors members of the thermostat. It also then creates the objects that will hold the data needed in the thermostat algorithms and the dependency network. Actually, this specific thermostat requires being called on a beads object. Args: nm: An optional normal modes object to take the mass and momentum vectors from. prng: An optional pseudo random number generator object. Defaults to Random(). fixdof: An optional integer which can specify the number of constraints applied to the system. Defaults to zero. Raises: TypeError: Raised if no beads object is specified for the thermostat to couple to. """ if nm is None or not type(nm) is NormalModes: raise TypeError("ThermoNMGLE.bind expects a NormalModes argument to bind to") if prng is None: self.prng = Random() else: self.prng = prng if (nm.nbeads != self.nb): raise IndexError("The parameters in nm_gle options correspond to a bead number "+str(self.nb)+ " which does not match the number of beads in the path" + str(nm.nbeads) ) # allocates, initializes or restarts an array of s's if self.s.shape != (self.nb, self.ns + 1, nm.natoms *3) : if len(self.s) > 0: warning("Mismatch in GLE s array size on restart, will reinitialise to free particle.", verbosity.low) self.s = np.zeros((self.nb, self.ns + 1, nm.natoms*3)) # Initializes the s vector in the free-particle limit info(" GLE additional DOFs initialised to the free-particle limit.", verbosity.low) for b in range(self.nb): SC = stab_cholesky(self.C[b]*Constants.kb) self.s[b] = np.dot(SC, self.prng.gvec(self.s[b].shape)) else: info("GLE additional DOFs initialised from input.", verbosity.medium) prev_ethermo = self.ethermo # creates a set of thermostats to be applied to individual normal modes self._thermos = [ThermoGLE(temp=1, dt=1, A=self.A[b], C=self.C[b]) for b in range(self.nb)] # must pipe all the dependencies in such a way that values for the nm # thermostats are automatically updated based on the "master" thermostat def make_Agetter(k): return lambda: self.A[k] def make_Cgetter(k): return lambda: self.C[k] it = 0 for t in self._thermos: t.s = self.s[it] # gets the s's as a slice of self.s t.bind(pm=(nm.pnm[it,:],nm.dynm3[it,:]), prng=self.prng) # bind thermostat t to the it-th normal mode # pipes temp and dt deppipe(self,"temp", t, "temp") deppipe(self,"dt", t, "dt") # here we pipe the A and C of individual NM to the "master" arrays dget(t,"A").add_dependency(dget(self,"A")) dget(t,"A")._func = make_Agetter(it) dget(t,"C").add_dependency(dget(self,"C")) dget(t,"C")._func = make_Cgetter(it) dget(self,"ethermo").add_dependency(dget(t,"ethermo")) it += 1 # since the ethermo will be "delegated" to the normal modes thermostats, # one has to split # any previously-stored value between the sub-thermostats for t in self._thermos: t.ethermo = prev_ethermo/self.nb dget(self,"ethermo")._func = self.get_ethermo;
class ThermoNMGLE(Thermostat): """Represents a 'normal-modes' GLE thermostat. An extension to the GLE thermostat which is applied in the normal modes representation, and which allows to use a different GLE for each normal mode Attributes: ns: The number of auxilliary degrees of freedom. nb: The number of beads. s: An array holding all the momenta, including the ones for the auxilliary degrees of freedom. Depend objects: A: Drift matrix giving the damping time scales for all the different degrees of freedom (must contain nb terms). C: Static covariance matrix. Satisfies A.C + C.transpose(A) = B.transpose(B), where B is the diffusion matrix, giving the strength of the coupling of the system with the heat bath, and thus the size of the stochastic contribution of the thermostat. """ def get_C(self): """Calculates C from temp (if C is not set explicitly).""" rv = np.ndarray((self.nb, self.ns+1, self.ns+1), float) for b in range(0,self.nb): rv[b] = np.identity(self.ns + 1,float)*self.temp return rv[:] def __init__(self, temp = 1.0, dt = 1.0, A = None, C = None, ethermo=0.0): """Initialises ThermoGLE. Args: temp: The simulation temperature. Defaults to 1.0. dt: The simulation time step. Defaults to 1.0. A: An optional matrix giving the drift matrix. Defaults to a single value of 1.0. C: An optional matrix giving the covariance matrix. Defaults to an identity matrix times temperature with the same dimensions as the total number of degrees of freedom in the system. ethermo: The initial heat energy transferred to the bath. Defaults to 0.0. Will be non-zero if the thermostat is initialised from a checkpoint file. """ super(ThermoNMGLE,self).__init__(temp,dt,ethermo) if A is None: A = np.identity(1,float) dset(self,"A",depend_value(value=A.copy(),name='A')) self.nb = len(self.A) self.ns = len(self.A[0]) - 1; # now, this is tricky. if C is taken from temp, then we want it to be # updated as a depend of temp. # Otherwise, we want it to be an independent beast. if C is None: dset(self,"C",depend_value(name='C', func=self.get_C, dependencies=[dget(self,"temp")])) else: dset(self,"C",depend_value(value=C.copy(),name='C')) def bind(self, nm=None, prng=None, fixdof=None): """Binds the appropriate degrees of freedom to the thermostat. This takes an object with degrees of freedom, and makes their momentum and mass vectors members of the thermostat. It also then creates the objects that will hold the data needed in the thermostat algorithms and the dependency network. Actually, this specific thermostat requires being called on a beads object. Args: nm: An optional normal modes object to take the mass and momentum vectors from. prng: An optional pseudo random number generator object. Defaults to Random(). fixdof: An optional integer which can specify the number of constraints applied to the system. Defaults to zero. Raises: TypeError: Raised if no beads object is specified for the thermostat to couple to. """ if nm is None or not type(nm) is NormalModes: raise TypeError("ThermoNMGLE.bind expects a NormalModes argument to bind to") if prng is None: self.prng = Random() else: self.prng = prng if (nm.nbeads != self.nb): raise IndexError("The parameters in nm_gle options correspond to a bead number "+str(self.nb)+ " which does not match the number of beads in the path" + str(nm.nbeads) ) # allocates, initializes or restarts an array of s's if self.s.shape != (self.nb, self.ns + 1, nm.natoms *3) : if len(self.s) > 0: warning("Mismatch in GLE s array size on restart, will reinitialise to free particle.", verbosity.low) self.s = np.zeros((self.nb, self.ns + 1, nm.natoms*3)) # Initializes the s vector in the free-particle limit info(" GLE additional DOFs initialised to the free-particle limit.", verbosity.low) for b in range(self.nb): SC = stab_cholesky(self.C[b]*Constants.kb) self.s[b] = np.dot(SC, self.prng.gvec(self.s[b].shape)) else: info("GLE additional DOFs initialised from input.", verbosity.medium) prev_ethermo = self.ethermo # creates a set of thermostats to be applied to individual normal modes self._thermos = [ThermoGLE(temp=1, dt=1, A=self.A[b], C=self.C[b]) for b in range(self.nb)] # must pipe all the dependencies in such a way that values for the nm # thermostats are automatically updated based on the "master" thermostat def make_Agetter(k): return lambda: self.A[k] def make_Cgetter(k): return lambda: self.C[k] it = 0 for t in self._thermos: t.s = self.s[it] # gets the s's as a slice of self.s t.bind(pm=(nm.pnm[it,:],nm.dynm3[it,:]), prng=self.prng) # bind thermostat t to the it-th normal mode # pipes temp and dt deppipe(self,"temp", t, "temp") deppipe(self,"dt", t, "dt") # here we pipe the A and C of individual NM to the "master" arrays dget(t,"A").add_dependency(dget(self,"A")) dget(t,"A")._func = make_Agetter(it) dget(t,"C").add_dependency(dget(self,"C")) dget(t,"C")._func = make_Cgetter(it) dget(self,"ethermo").add_dependency(dget(t,"ethermo")) it += 1 # since the ethermo will be "delegated" to the normal modes thermostats, # one has to split # any previously-stored value between the sub-thermostats for t in self._thermos: t.ethermo = prev_ethermo/self.nb dget(self,"ethermo")._func = self.get_ethermo; def step(self): """Updates the thermostat in NM representation by looping over the individual DOFs. """ for t in self._thermos: t.step() def get_ethermo(self): """Computes the total energy transferred to the heat bath for all the nm thermostats. """ et = 0.0; for t in self._thermos: et += t.ethermo return et