Exemplo n.º 1
0
 def _Lx(self, X, deriv = 0, out = None, var = None):
     """
     basis evaluation function 
     
     Use recursion relations for generalized 
     Laguerre polynomials
 
     """    
     nd,nvar = dfun.ndnvar(deriv, var, self.nx)
     if out is None:
         base_shape = X.shape[1:]
         out = np.ndarray( (nd, self.nf) + base_shape, dtype = X.dtype)
     
     # Create adf object for theta
     x = dfun.X2adf(X,deriv,var)[0]
     
     # Calculate Laguerre polynomials
     # with generic algebra
     L = _laguerre_gen(x, self.alpha, self.nmax)
         
     # Convert adf objects to a single
     # derivative array
     dfun.adf2array(L, out)
     
     return out 
Exemplo n.º 2
0
 def _sin(self, X, deriv = 0, out = None, var = None):
     """ evaluation function """
     
     nd,nvar = dfun.ndnvar(deriv, var, self.nx)
     if out is None:
         base_shape = X.shape[1:]
         out = np.ndarray( (nd, self.nf) + base_shape, dtype = X.dtype)
         
     theta = X[0]
     kfact = 1.0 
     
     for k in range(nd):
         #
         # The k^th derivative order
         #
         if k % 4 == 0: 
             y =  np.sin(theta)
         elif k % 4 == 1:
             y =  np.cos(theta)
         elif k % 4 == 2:
             y = -np.sin(theta)
         else: # k % 4 == 3
             y = -np.cos(theta)   
             
         np.copyto(out[k,0:1], y / kfact)
         
         kfact *= (k + 1.0) 
         
     return out 
Exemplo n.º 3
0
 def _ftheta(self, X, deriv = 0, out = None, var = None):
     """
     basis evaluation function 
     
     Use recursion relations for associated Legendre
     polynomials.
 
     """    
     nd,nvar = dfun.ndnvar(deriv, var, self.nx)
     if out is None:
         base_shape = X.shape[1:]
         out = np.ndarray( (nd, self.nf) + base_shape, dtype = X.dtype)
     
     # Create adf object for theta
     theta = dfun.X2adf(X, deriv, var)[0]
     m = abs(self.m) # |m| 
     sinm = adf.powi(adf.sin(theta), m)
     cos = adf.cos(theta)
     
     # Calculate Legendre polynomials 
     # with generic algebra
     F = _leg_gen(sinm, cos, m, np.max(self.l))
         
     # Convert adf objects to a single
     # derivative array
     dfun.adf2array(F, out)
     
     return out 
Exemplo n.º 4
0
 def _fphi(self, X, deriv = 0, out = None, var = None):
     """ evaluation function """
     
     nd,nvar = dfun.ndnvar(deriv, var, self.nx)
     if out is None:
         base_shape = X.shape[1:]
         out = np.ndarray( (nd, self.nf) + base_shape, dtype = X.dtype)
         
     pi = np.pi
     norm = pi if self._rad else 180.0
     phi = X 
     
     kfact = 1.0 # Running value of k! 
     
     for k in range(nd):
         #
         # The k^th derivative order
         #
         for i in range(self.nf):
             m = self.m[i] # The m-index of the basis function
             if m < 0:
                 # 1/sqrt(pi) * sin(|m| * phi) 
                 
                 m = abs(m) if self._rad else abs(m) * pi/180.0 
                 
                 if k % 4 == 0:
                     y = +m**k * np.sin(m * phi)
                 elif k % 4 == 1:
                     y = +m**k * np.cos(m * phi)
                 elif k % 4 == 2:
                     y = -m**k * np.sin(m * phi)
                 else: # k % 4 == 3
                     y = -m**k * np.cos(m * phi)
                 np.copyto(out[k:(k+1),i],y / np.sqrt(norm))
                           
             elif m == 0:
                 # 1/sqrt(2*pi)
                 if k == 0: # Zeroth derivative
                     out[0:1,i].fill(1.0 / np.sqrt(2*norm))
                 if k > 0 : # All higher derivatives
                     out[k:(k+1),i].fill(0.0)
                     
             elif m > 0 :
                 # 1/sqrt(pi) * cos(m * phi) 
                 m = m if self._rad else m * pi/180.0 
                 if k % 4 == 0:
                     y = +m**k * np.cos(m * phi)
                 elif k % 4 == 1:
                     y = -m**k * np.sin(m * phi)
                 elif k % 4 == 2:
                     y = -m**k * np.cos(m * phi)
                 else: # k % 4 == 3
                     y = +m**k * np.sin(m * phi)
                 np.copyto(out[k:(k+1),i],y / np.sqrt(norm))
                 
         out[k:(k+1)] /= kfact
         kfact *= (k+1.0) # Update k! 
     
     return out
Exemplo n.º 5
0
 def _monomial(self, X, deriv = 0, out = None, var = None):
     """ evaluation function """
     
     nd,nvar = dfun.ndnvar(deriv, var, self.nx)
     if out is None:
         base_shape = X.shape[1:]
         out = np.ndarray( (nd, self.nf) + base_shape, dtype = X.dtype)
         
     x = dfun.X2adf(X, deriv, var)
     res = adf.powi(x[0], self.pows[0]) 
     for i in range(1, self.nx):
         res = res * adf.powi(x[i], self.pows[i])
         
     dfun.adf2array([res], out)
     return out 
Exemplo n.º 6
0
 def _jv(self, X, deriv = 0, out = None, var = None):
     """
     Bessel function of the first kind, Jv.
     Derivative array eval function.
     """
     # Setup
     nd,nvar = dfun.ndnvar(deriv, var, self.nx)
     if out is None:
         base_shape = X.shape[1:]
         out = np.ndarray( (nd, self.nf) + base_shape, dtype = X.dtype)
     
     # Make adf objects for theta and phi 
     x = X[0] 
     
     #
     # Calculate derivatives
     #
     # For now, we will just use the SciPy
     # Bessel function derivatives routine for 
     # each deriative order. This routine
     # makes use of the recursive definition of 
     # derivatives of Jv 
     #
     # (d/dx)^k J_v = (1/2)**k  * Sum_n=0^k  (-1)**n * (k choose n) * J_{v-k+2n}
     #
     # By calling it for each derivative order, 
     # we are calculating some Bessel functions
     # multiple times. But this is not that expensive anyway,
     # so don't worry about the efficiency issue.
     #
     kfact = 1.0 
     for k in range(nd):
         dk = scipy.special.jvp(self.v, x, n = k) / kfact
         np.copyto(out[k:(k+1),0], dk)
         kfact *= (k + 1.0) 
     
     return out 
Exemplo n.º 7
0
    def _Rr(self, X, deriv = 0, out = None, var = None):
        """
        basis evaluation function 
        
        Use generic algebra evaluation function
    
        """    
        nd,nvar = dfun.ndnvar(deriv, var, self.nx)
        if out is None:
            base_shape = X.shape[1:]
            out = np.ndarray( (nd, self.nf) + base_shape, dtype = X.dtype)
        
        # Create adf object for theta
        r = dfun.X2adf(X,deriv,var)[0]
        expar2 = adf.exp(-0.5 * self.alpha * (r*r))
        
        # Calculate radial wavefunctions
        R = _radialHO_gen(r, expar2, self.nmax, self.ell, self.d, self.alpha)
 
        # Convert adf objects to a single
        # derivative array
        dfun.adf2array(R, out)
        
        return out 
Exemplo n.º 8
0
 def _Philm(self, X, deriv = 0, out = None, var = None):
     
     # Setup
     nd,nvar = dfun.ndnvar(deriv, var, self.nx)
     if out is None:
         base_shape = X.shape[1:]
         out = np.ndarray( (nd, self.nf) + base_shape, dtype = X.dtype)
     
     # Make adf objects for theta and phi 
     x = dfun.X2adf(X, deriv, var)
     theta = x[0]
     phi = x[1] 
     
     #########################################
     #
     # Calculate F and f factors first
     #
     sinth = adf.sin(theta)
     costh = adf.cos(theta)
     lmax = np.max(self.l)
     # Calculate the associated Legendre factors
     Flm = [] 
     amuni = np.unique(abs(self.m)) # list of unique |m|
     for aM in amuni:
         # Order aM = |M|
         sinM = adf.powi(sinth, aM)
         # Calculate F^M up to l <= lmax
         Flm.append(_leg_gen(sinM, costh, aM, lmax))
     #Flm is now a nested list 
     
     # Calculate the phi factors 
     smuni = np.unique(self.m) # list of unique signed m
     fm = []
     for sM in smuni:
         if sM == 0:
             fm.append(adf.const_like(1/np.sqrt(2*np.pi), phi))
         elif sM < 0:
             fm.append(1/np.sqrt(np.pi) * adf.sin(abs(sM) * phi))
         else: #sM > 0
             fm.append(1/np.sqrt(np.pi) * adf.cos(sM * phi))
     #
     ##############################################
     
     ###########################################
     # Now calculate the list of real spherical harmonics 
     Phi = []
     for i in range(self.nf):
         l = self.l[i]
         m = self.m[i] # signed m
         
         # Gather the associated Legendre polynomial (theta factor)
         aM_idx = np.where(amuni == abs(m))[0][0]
         l_idx = l - abs(m)
         F_i = Flm[aM_idx][l_idx] 
         # Gather the sine/cosine (phi factor)
         sM_idx = np.where(smuni == m)[0][0]
         f_i = fm[sM_idx]
         
         # Add their product to the list of functions
         Phi.append(F_i * f_i) 
     #
     ###########################################
         
     # Convert the list to a single 
     # DFun derivative array
     dfun.adf2array(Phi, out)
     # and return
     return out 
Exemplo n.º 9
0
 def _chinell(self, X, deriv = 0, out = None, var = None):
     
     # Setup
     nd,nvar = dfun.ndnvar(deriv, var, self.nx)
     if out is None:
         base_shape = X.shape[1:]
         out = np.ndarray( (nd, self.nf) + base_shape, dtype = X.dtype)
     
     # Make adf objects for theta and phi 
     x = dfun.X2adf(X, deriv, var)
     r = x[0]        # Cylindrical radius
     phi = x[1] if self._rad else x[1] * np.pi/180.0      # Angular coordinate
     
     #########################################
     #
     # Calculate R and f factors first
     #        
     # Calculate the radial wavefunctions
     Rnell = [] 
     abs_ell_uni = np.unique(abs(self.ell)) # list of unique |ell|
     expar2 = adf.exp(-0.5 * self.alpha * (r*r))  # The exponential radial factor
     for aELL in abs_ell_uni:
         nmax = round(np.floor(self.vmax - aELL) / 2)
         Rnell.append(_radialHO_gen(r, expar2, nmax, aELL, 2, self.alpha))
     
     
     # Calculate the phi factors, f_ell(phi)
     sig_ell_uni = np.unique(self.ell) # list of unique signed ell
     fell = []
     if self._rad: # Normalization assuming radians
         for sELL in sig_ell_uni:
             if sELL == 0:
                 fell.append(adf.const_like(1/np.sqrt(2*np.pi), phi))
             elif sELL < 0:
                 fell.append(1/np.sqrt(np.pi) * adf.sin(abs(sELL) * phi))
             else: #sELL > 0
                 fell.append(1/np.sqrt(np.pi) * adf.cos(sELL * phi))
     else: # Normalization for degrees
         for sELL in sig_ell_uni:
             if sELL == 0:
                 fell.append(adf.const_like(1/np.sqrt(360.00), phi))
             elif sELL < 0:
                 fell.append(1/np.sqrt(180.0) * adf.sin(abs(sELL) * phi))
             else: #sELL > 0
                 fell.append(1/np.sqrt(180.0) * adf.cos(sELL * phi))
     #
     ##############################################
     
     ###########################################
     # Now calculate the list of real 2-D HO wavefunctions
     chi = []
     for i in range(self.nf):
         
         ell = self.ell[i] 
         n = self.n[i] 
         
         # Gather the radial factor
         abs_ELL_idx = np.where(abs_ell_uni == abs(ell))[0][0]
         R_i = Rnell[abs_ELL_idx][n] 
         
         # Gather the angular factor
         sig_ELL_idx = np.where(sig_ell_uni == ell)[0][0]
         f_i = fell[sig_ELL_idx]
         
         # Add their product to the list of functions
         chi.append(R_i * f_i)
     #
     ###########################################
         
     # Convert the list to a single 
     # DFun derivative array
     dfun.adf2array(chi, out)
     # and return
     return out 
Exemplo n.º 10
0
    def _f_cfour(self, X, deriv=0, out=None, var=None):
        """
        Evaluate CFOUR energy and derivatives
        """

        if var is None:
            var = [i for i in range(self.nx)]
        nd, nvar = dfun.ndnvar(deriv, var, self.nx)

        # X has shape (nx,) + base_shape
        base_shape = X.shape[1:]
        if out is None:
            out = np.ndarray((nd, self.nf) + base_shape, dtype=np.float64)

        # Loop over each input geometry in
        # serial fashion
        Xflat = np.reshape(X, (self.nx, -1))
        npts = Xflat.shape[1]  # The number of jobs

        # Intermediate storage:
        # Flatten the base_shape to one dimension
        #
        out_flat = np.zeros((nd, self.nf, npts))

        for i in range(npts):

            jobstr = f'job{i:05d}'
            jobdir = os.path.join(self.work_dir, jobstr)

            #
            # Create the work space
            try:
                os.makedirs(jobdir)
            except FileExistsError:
                # Remove previous job directory
                shutil.rmtree(jobdir)
                os.makedirs(jobdir)

            # Energy calculation only

            #
            # Create the ZMAT text file
            # for a single-point energy
            #
            with open(os.path.join(jobdir, 'ZMAT'), 'w') as file:
                file.write('nitrogen-cfour-interface\n')
                #
                # Write Cartesian coordinates
                for j in range(self.natoms):
                    file.write(self.atomic_symbols[j] + " ")
                    for k in range(3):
                        xval = Xflat[3 * j + k,
                                     i]  # Atom j, coordinate k = x,y,z
                        file.write(f'{xval:.15f} ')

                    file.write("\n")
                file.write("\n")

                # Write options
                file.write("*CFOUR(")

                first = True
                for keyword, value in self.params.items():
                    if not first:
                        file.write("\n")  # new line
                    else:
                        first = False
                    file.write(keyword + "=" + value)

                # Write the necessary keywords based
                # on deriv level
                #
                if deriv == 0:
                    file.write("\nDERIV_LEV=0")
                    file.write("\nPRINT=0")
                elif deriv == 1:
                    file.write("\nDERIV_LEV=1")
                    file.write("\nPRINT=1")
                elif deriv == 2:
                    file.write("\nDERIV_LEV=2")
                    file.write("\nPRINT=1")
                    file.write("\nVIB=ANALYTIC")

                file.write(")\n")

            # Save current word dir
            current_wd = os.getcwd()
            # Now change to the job directory
            os.chdir(jobdir)
            # Execute cfour
            os.system('xcfour > out')
            # and go back
            os.chdir(current_wd)

            ######################
            # Parse CFOUR output
            ######################
            if deriv >= 0:
                #
                # Find the energy in the string
                #
                #   "The final electronic energy is     XXXXXXXXXXXXXXXXX a.u."
                #
                found = False
                with open(os.path.join(jobdir, 'out'), 'r') as file:
                    for line in file:
                        if re.search('final electronic energy', line):
                            energy = float(
                                line.split()
                                [5])  # The sixth field is the energy, a.u.
                            found = True

                if not found:
                    raise RuntimeError(
                        f"Cannot find a CFOUR energy in {jobstr}")

                # Save energy
                # converting from hartree to cm**-1
                out_flat[0, 0, i] = energy * nitrogen.constants.Eh

            if deriv >= 1:
                #
                # Find the gradient
                #
                # This will be headed by
                #       reordered gradient in QCOM coords for ZMAT order
                # followed by a line for each atom
                # in the original ZMAT ordering we provided
                #
                # Note:
                #  "QCOMP" is the computation coordinates used by
                #  CFOUR for most of its work
                #  "QCOM" (no "P") are the original coordinates passed
                #  in ZMAT translated to the COM frame
                #
                #  Because energies and gradients are independent
                #  of total translations, we can use QCOM
                #  derivatives.
                #
                found = False
                with open(os.path.join(jobdir, 'out'), 'r') as file:
                    for line in file:
                        if re.search('reordered gradient', line):
                            # found it
                            found = True
                            break
                    if not found:
                        raise RuntimeError(
                            f"Cannot find a CFOUR gradient in {jobstr}.")

                    # Now parse gradient lines
                    grad_all = np.zeros((self.nx, ))
                    for j in range(self.natoms):
                        grad_str = file.readline().split()
                        for k in range(3):
                            # Save the x, y, z components
                            grad_all[j * 3 + k] = float(grad_str[k])

                # Now copy the requested derivatives to
                # the output buffer, per the `var` order
                #
                # The CFOUR printed values are in units
                # of hartree/bohr. Convert this to
                # cm**-1 / Angstrom
                #
                coeff = nitrogen.constants.Eh / nitrogen.constants.a0
                for k in range(len(var)):
                    # The derivative w.r.t. var[k]
                    out_flat[k + 1, 0, i] = grad_all[var[k]] * coeff

            if deriv >= 2:
                #
                # Parse the Hessian data
                #
                # VIB=ANALYTIC stores this in FCM
                #
                # As far as I can tell, FCM stores
                # in the "symmetrized" atomic order, while
                # FCMFINAL uses the original ZMAT order
                # Both appear to be in QCOMP coordinates, not
                # the ZMAT's original QCOM coordinates
                #
                try:
                    fcm_raw = np.loadtxt(os.path.join(jobdir, 'FCMFINAL'),
                                         skiprows=1)
                except:
                    raise RuntimeError(
                        f"Cannot find a CFOUR FCMFINAL file in {jobstr}.")
                if fcm_raw.shape != (3 * self.natoms**2, 3):
                    raise RuntimeError(f"Unexpected FCM shape in {jobstr}.")

                fcm_1d = np.zeros((self.nx**2, ))
                for j in range(3 * self.natoms**2):
                    for k in range(3):
                        fcm_1d[j * 3 + k] = fcm_raw[j, k]
                fcm_full = fcm_1d.reshape((self.nx, self.nx))

                #
                # Convert from Eh/a0**2 to cm**-1 / Angstrom**2
                fcm_full *= nitrogen.constants.Eh / (nitrogen.constants.a0**2)

                #
                # Now we need to parse the OMAT transformation
                # matrix between QCOMP and QCOM
                #
                # This starts 2 lines after
                #      Transformation matrix between QCOM and QCOMP (OMAT)
                # and is a 3 x 3 matrix
                #

                found = False
                with open(os.path.join(jobdir, 'out'), 'r') as file:
                    for line in file:
                        if re.search(
                                'Transformation matrix between QCOM and QCOMP',
                                line):
                            # found it
                            found = True
                            break
                    if not found:
                        raise RuntimeError(
                            f"Cannot find a CFOUR OMAT in {jobstr}.")

                    # Now parse gradient lines
                    # First, burn one line
                    file.readline()
                    #
                    # Store OMAT as printed
                    #
                    OMAT = np.zeros((3, 3))
                    for j in range(3):
                        omat_str = file.readline().split()
                        for k in range(3):
                            # Save the x, y, z components
                            OMAT[j, k] = float(omat_str[k])

                #
                # A 3x1 QCOMP atomic vector and a
                # 3x1 QCOM atomic vector appear to be
                # related as
                #
                #    QCOMP = OMAT @ QCOM
                #
                # This is the ***opposite*** of the printed description
                # in the CFOUR output file. I expect it is just
                # a mistake in the text.
                #

                # The Hessian I want is therefore
                #
                # O.T @ fcm_full @ O

                #
                # Build a block diagonal matrix with
                # OMAT on each diagonal block
                #
                Om = np.zeros((self.nx, self.nx))
                for a in range(self.natoms):
                    # Atomic block (a,a)
                    for j in range(3):
                        for k in range(3):
                            Om[a * 3 + j, a * 3 + k] = OMAT[j, k]

                fcm_full = Om.T @ fcm_full @ Om

                ###################
                # Copy to output buffer
                # We now take into account that
                # only the variables in `var` are requested
                #
                idx = len(var) + 1  # The starting index for second derivatives
                for k1 in range(len(var)):
                    for k2 in range(k1, len(var)):
                        v1 = var[k1]
                        v2 = var[k2]
                        # need the v1,v2 derivative
                        out_flat[idx, 0, i] = fcm_full[v1, v2]
                        if v1 == v2:
                            out_flat[idx, 0, i] *= 0.5
                            # derivative array format includes
                            # 1/2! factor of diagonal second derivative
                        idx += 1
                # done
                #########

            #
            # Remove job directory
            #
            if self.cleanup:
                shutil.rmtree(jobdir)

        # Reshape output data to correct base_shape
        # and copy to out buffer
        np.copyto(out, out_flat.reshape((nd, self.nf) + base_shape))

        return out