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
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
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
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
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
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
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
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
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
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