Example #1
0
    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")]))
Example #2
0
    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)
Example #3
0
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.")
Example #4
0
    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
Example #5
0
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
Example #6
0
    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
Example #7
0
   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;
Example #8
0
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