def proj_ref_ao(mol, minao='minao', kpts=None, return_labels=False): """ Get a set of reference AO spanned by the calculation basis. Not orthogonalized. Args: return_labels: if True, return the labels as well. """ nao = mol.nao_nr() nkpts = len(kpts) pmol = iao.reference_mol(mol, minao) s1 = np.asarray(mol.pbc_intor('int1e_ovlp', hermi=1, kpts=kpts)) s2 = np.asarray(pmol.pbc_intor('int1e_ovlp', hermi=1, kpts=kpts)) s12 = np.asarray(pgto.cell.intor_cross('int1e_ovlp', mol, pmol, kpts=kpts)) s21 = np.swapaxes(s12, -1, -2).conj() C_ao_lo = np.zeros((nkpts, s1.shape[-1], s2.shape[-1]), dtype=np.complex128) for k in range(nkpts): s1cd_k = la.cho_factor(s1[k]) s2cd_k = la.cho_factor(s2[k]) C_ao_lo[k] = la.cho_solve(s1cd_k, s12[k]) if return_labels: labels = pmol.ao_labels() return C_ao_lo, labels else: return C_ao_lo
def PipekMezey(mol, orbocc, iaos=None, s=None, exponent=EXPONENT): ''' Note this localization is slightly different to Knizia's implementation. The localization here reserves orthogonormality during optimization. Orbitals are projected to IAO basis first and the Mulliken pop is calculated based on IAO basis (in function atomic_pops). A series of unitary matrices are generated and applied on the input orbitals. The intemdiate orbitals in the optimization and the finally localized orbitals are all orthogonormal. Examples: >>> from pyscf import gto, scf >>> from pyscf.lo import ibo >>> mol = gto.M(atom='H 0 0 0; F 0 0 1', >>> basis='unc-sto3g') >>> mf = scf.RHF(mol).run() >>> pm = ibo.PM(mol, mf.mo_coeff[:,mf.mo_occ>0]) >>> loc_orb = pm.kernel() ''' if hasattr(mol, 'pbc_intor'): # whether mol object is a cell if isinstance(orbocc, numpy.ndarray) and orbocc.ndim == 2: s = mol.pbc_intor('int1e_ovlp', hermi=1) else: raise NotImplementedError('k-points crystal orbitals') else: s = mol.intor_symmetric('int1e_ovlp') if iaos is None: iaos = iao.iao(mol, orbocc) # Different to Knizia's code, the reference IAOs are not neccessary # orthogonal. #iaos = orth.vec_lowdin(iaos, s) cs = numpy.dot(iaos.T.conj(), s) s_iao = numpy.dot(cs, iaos) iao_inv = numpy.linalg.solve(s_iao, cs) iao_mol = iao.reference_mol(mol) # Define the mulliken population of each atom based on IAO basis. # proj[i].trace is the mulliken population of atom i. def atomic_pops(mol, mo_coeff, method=None): nmo = mo_coeff.shape[1] proj = numpy.empty((mol.natm, nmo, nmo)) orb_in_iao = reduce(numpy.dot, (iao_inv, mo_coeff)) for i, (b0, b1, p0, p1) in enumerate(iao_mol.offset_nr_by_atom()): csc = reduce(numpy.dot, (orb_in_iao[p0:p1].T, s_iao[p0:p1], orb_in_iao)) proj[i] = (csc + csc.T) * .5 return proj pm = pipek.PM(mol, orbocc) pm.atomic_pops = atomic_pops pm.exponent = exponent return pm
def PipekMezey(mol, orbocc, iaos=None, s=None, exponent=EXPONENT): ''' Note this localization is slightly different to Knizia's implementation. The localization here reserves orthogonormality during optimization. Orbitals are projected to IAO basis first and the Mulliken pop is calculated based on IAO basis (in function atomic_pops). A series of unitary matrices are generated and applied on the input orbitals. The intemdiate orbitals in the optimization and the finally localized orbitals are all orthogonormal. Examples: >>> from pyscf import gto, scf >>> from pyscf.lo import ibo >>> mol = gto.M(atom='H 0 0 0; F 0 0 1', >>> basis='unc-sto3g') >>> mf = scf.RHF(mol).run() >>> pm = ibo.PM(mol, mf.mo_coeff[:,mf.mo_occ>0]) >>> loc_orb = pm.kernel() ''' if getattr(mol, 'pbc_intor', None): # whether mol object is a cell if isinstance(orbocc, numpy.ndarray) and orbocc.ndim == 2: s = mol.pbc_intor('int1e_ovlp', hermi=1) else: raise NotImplementedError('k-points crystal orbitals') else: s = mol.intor_symmetric('int1e_ovlp') if iaos is None: iaos = iao.iao(mol, orbocc) # Different to Knizia's code, the reference IAOs are not neccessary # orthogonal. #iaos = orth.vec_lowdin(iaos, s) cs = numpy.dot(iaos.T.conj(), s) s_iao = numpy.dot(cs, iaos) iao_inv = numpy.linalg.solve(s_iao, cs) iao_mol = iao.reference_mol(mol) # Define the mulliken population of each atom based on IAO basis. # proj[i].trace is the mulliken population of atom i. def atomic_pops(mol, mo_coeff, method=None): nmo = mo_coeff.shape[1] proj = numpy.empty((mol.natm,nmo,nmo)) orb_in_iao = reduce(numpy.dot, (iao_inv, mo_coeff)) for i, (b0, b1, p0, p1) in enumerate(iao_mol.offset_nr_by_atom()): csc = reduce(numpy.dot, (orb_in_iao[p0:p1].T, s_iao[p0:p1], orb_in_iao)) proj[i] = (csc + csc.T) * .5 return proj pm = pipek.PM(mol, orbocc) pm.atomic_pops = atomic_pops pm.exponent = exponent return pm
def _iao_complementary_orbitals(mol, iao_ref): """Get the IAOs for complementary space (virtual orbitals). Args: mol (pyscf.gto.Mole): The molecule to simulate. iao_ref (numpy.array): IAO in occupied space (float64). Returns: iao_comp (numpy.array): IAO in complementary space (float64). """ # Get the total number of AOs norbital_total = mol.nao_nr() # Calculate the Overlaps for total basis s1 = mol.intor_symmetric('int1e_ovlp') # Construct the complementary space AO number_iaos = iao_ref.shape[1] number_inactive = norbital_total - number_iaos iao_com_ref = _iao_complementary_space(iao_ref, s1, number_inactive) # Get a list of active orbitals min_mol = iao.reference_mol(mol) norbital_active, active_list = _iao_count_active(mol, min_mol) # Obtain the Overlap-like matrices s21 = s1[active_list, : ] s2 = s21[ : , active_list] s12 = s21.T # Calculate P_12 = S_1^-1 * S_12 using Cholesky decomposition s1_sqrt = scipy.linalg.cho_factor(s1) s2_sqrt = scipy.linalg.cho_factor(s2) p12 = scipy.linalg.cho_solve(s1_sqrt, s12) # C~ = orth ( second_half ( S_1^-1 * S_12 * first_half ( S_2^-1 * S_21 * C ) ) ) c_tilde = scipy.linalg.cho_solve(s2_sqrt, np.dot(s21, iao_com_ref)) c_tilde = scipy.linalg.cho_solve(s1_sqrt, np.dot(s12, c_tilde)) c_tilde = np.dot(c_tilde, orth.lowdin(reduce(np.dot, (c_tilde.T, s1, c_tilde)))) # Obtain C * C^T * S1 and C~ * C~^T * S1 ccs1 = reduce(np.dot, (iao_com_ref, iao_com_ref.conj().T, s1)) ctcts1 = reduce(np.dot, (c_tilde, c_tilde.conj().T, s1)) # Calculate A = ccs1 * ctcts1 * p12 + ( 1 - ccs1 ) * ( 1 - ctcts1 ) * p12 iao_comp = (p12 + reduce(np.dot, (ccs1, ctcts1, p12)) * 2 - np.dot(ccs1, p12) - np.dot(ctcts1, p12)) iao_comp = np.dot(iao_comp, orth.lowdin(reduce(np.dot, (iao_comp.T, s1, iao_comp)))) return iao_comp
def _iao_occupied_orbitals(mol, mf): """Get the IAOs for occupied space. Args: mol (pyscf.gto.Mole): The molecule to simulate. mf (pyscf.scf.RHF): The mean field of the molecule. Returns: iao_active (numpy.array): The localized orbitals for the occupied space (float64). """ # Get MO coefficient of occupied MOs occupied_orbitals = mf.mo_coeff[:, mf.mo_occ > 0.5] # Get mol data in minao basis min_mol = iao.reference_mol(mol) # Calculate the overlaps for total basis s1 = mol.intor_symmetric('int1e_ovlp') # ... for minao basis s2 = min_mol.intor_symmetric('int1e_ovlp') # ... between the two basis (and transpose) s12 = gto.mole.intor_cross('int1e_ovlp', mol, min_mol) s21 = s12.T # Calculate P_12 = S_1^-1 * S_12 using Cholesky decomposition s1_sqrt = scipy.linalg.cho_factor(s1) s2_sqrt = scipy.linalg.cho_factor(s2) p12 = scipy.linalg.cho_solve(s1_sqrt, s12) # C~ = second_half ( S_1^-1 * S_12 * first_half ( S_2^-1 * S_21 * C ) ) c_tilde = scipy.linalg.cho_solve(s2_sqrt, np.dot(s21, occupied_orbitals)) c_tilde = scipy.linalg.cho_solve(s1_sqrt, np.dot(s12, c_tilde)) c_tilde = np.dot(c_tilde, orth.lowdin(reduce(np.dot, (c_tilde.T, s1, c_tilde)))) # Obtain C * C^T * S1 and C~ * C~^T * S1 ccs1 = reduce(np.dot, (occupied_orbitals, occupied_orbitals.conj().T, s1)) ctcts1 = reduce(np.dot, (c_tilde, c_tilde.conj().T, s1)) # Calculate A = ccs1 * ctcts1 * p12 + ( 1 - ccs1 ) * ( 1 - ctcts1 ) * p12 iao_active = (p12 + reduce(np.dot, (ccs1, ctcts1, p12)) * 2 - np.dot(ccs1, p12) - np.dot(ctcts1, p12)) # Orthogonalize A iao_active = np.dot(iao_active, orth.lowdin(reduce(np.dot, (iao_active.T, s1, iao_active)))) return iao_active
def PipekMezey(mol, orbocc, iaos, s, exponent): ''' Note this localization is slightly different to Knizia's implementation. The localization here reserves orthogonormality during optimization. Orbitals are projected to IAO basis first and the Mulliken pop is calculated based on IAO basis (in function atomic_pops). A series of unitary matrices are generated and applied on the input orbitals. The intemdiate orbitals in the optimization and the finally localized orbitals are all orthogonormal. Examples: >>> from pyscf import gto, scf >>> from pyscf.lo import ibo >>> mol = gto.M(atom='H 0 0 0; F 0 0 1', >>> basis='unc-sto3g') >>> mf = scf.RHF(mol).run() >>> pm = ibo.PM(mol, mf.mo_coeff[:,mf.mo_occ>0]) >>> loc_orb = pm.kernel() ''' # Note: PM with Lowdin-orth IAOs is implemented in pipek.PM class # TODO: Merge the implemenation here to pipek.PM MINAO = getattr(__config__, 'lo_iao_minao', 'minao') cs = numpy.dot(iaos.T.conj(), s) s_iao = numpy.dot(cs, iaos) iao_inv = numpy.linalg.solve(s_iao, cs) iao_mol = iao.reference_mol(mol, minao=MINAO) # Define the mulliken population of each atom based on IAO basis. # proj[i].trace is the mulliken population of atom i. def atomic_pops(mol, mo_coeff, method=None): nmo = mo_coeff.shape[1] proj = numpy.empty((mol.natm, nmo, nmo)) orb_in_iao = reduce(numpy.dot, (iao_inv, mo_coeff)) for i, (b0, b1, p0, p1) in enumerate(iao_mol.offset_nr_by_atom()): csc = reduce(numpy.dot, (orb_in_iao[p0:p1].T, s_iao[p0:p1], orb_in_iao)) proj[i] = (csc + csc.T) * .5 return proj pm = pipek.PM(mol, orbocc) pm.atomic_pops = atomic_pops pm.exponent = exponent return pm
def ibo_loc(mol, orbocc, iaos, s, exponent, grad_tol, max_iter, minao=MINAO, verbose=logger.NOTE): '''Intrinsic Bonding Orbitals. [Ref. JCTC, 9, 4834] This implementation follows Knizia's implementation execept that the resultant IBOs are symmetrically orthogonalized. Note the IBOs of this implementation do not strictly maximize the IAO Mulliken charges. IBOs can also be generated by another implementation (see function pyscf.lo.ibo.PM). In that function, PySCF builtin Pipek-Mezey localization module was used to maximize the IAO Mulliken charges. Args: mol : the molecule or cell object orbocc : 2D array or a list of 2D array occupied molecular orbitals or crystal orbitals for each k-point Kwargs: iaos : 2D array the array of IAOs exponent : integer Localization power in PM scheme grad_tol : float convergence tolerance for norm of gradients Returns: IBOs in the big basis (the basis defined in mol object). ''' log = logger.new_logger(mol, verbose) assert(exponent in (2, 4)) # Symmetrically orthogonalization of the IAO orbitals as Knizia's # implementation. The IAO returned by iao.iao function is not orthogonal. iaos = orth.vec_lowdin(iaos, s) #static variables StartTime = logger.perf_counter() L = 0 # initialize a value of the localization function for safety #max_iter = 20000 #for some reason the convergence of solid is slower #fGradConv = 1e-10 #this ought to be pumped up to about 1e-8 but for testing purposes it's fine swapGradTolerance = 1e-12 #dynamic variables Converged = False # render Atoms list without ghost atoms iao_mol = iao.reference_mol(mol, minao=minao) Atoms = [iao_mol.atom_pure_symbol(i) for i in range(iao_mol.natm)] #generates the parameters we need about the atomic structure nAtoms = len(Atoms) AtomOffsets = MakeAtomIbOffsets(Atoms)[0] iAtSl = [slice(AtomOffsets[A],AtomOffsets[A+1]) for A in range(nAtoms)] #converts the occupied MOs to the IAO basis CIb = reduce(numpy.dot, (iaos.T, s , orbocc)) numOccOrbitals = CIb.shape[1] log.debug(" {0:^5s} {1:^14s} {2:^11s} {3:^8s}" .format("ITER.","LOC(Orbital)","GRADIENT", "TIME")) for it in range(max_iter): fGrad = 0.00 #calculate L for convergence checking L = 0. for A in range(nAtoms): for i in range(numOccOrbitals): CAi = CIb[iAtSl[A],i] L += numpy.dot(CAi,CAi)**exponent # loop over the occupied orbitals pairs i,j for i in range(numOccOrbitals): for j in range(i): # I eperimented with exponentially falling off random noise Aij = 0.0 #numpy.random.random() * numpy.exp(-1*it) Bij = 0.0 #numpy.random.random() * numpy.exp(-1*it) for k in range(nAtoms): CIbA = CIb[iAtSl[k],:] Cii = numpy.dot(CIbA[:,i], CIbA[:,i]) Cij = numpy.dot(CIbA[:,i], CIbA[:,j]) Cjj = numpy.dot(CIbA[:,j], CIbA[:,j]) #now I calculate Aij and Bij for the gradient search if exponent == 2: Aij += 4.*Cij**2 - (Cii - Cjj)**2 Bij += 4.*Cij*(Cii - Cjj) else: Bij += 4.*Cij*(Cii**3-Cjj**3) Aij += -Cii**4 - Cjj**4 + 6*(Cii**2 + Cjj**2)*Cij**2 + Cii**3 * Cjj + Cii*Cjj**3 if (Aij**2 + Bij**2 < swapGradTolerance) and False: continue #this saves us from replacing already fine orbitals else: #THE BELOW IS TAKEN DIRECLTY FROMG KNIZIA's FREE CODE # Calculate 2x2 rotation angle phi. # This correspond to [2] (12)-(15), re-arranged and simplified. phi = .25*numpy.arctan2(Bij,-Aij) fGrad += Bij**2 # ^- Bij is the actual gradient. Aij is effectively # the second derivative at phi=0. # 2x2 rotation form; that's what PM suggest. it works # fine, but I don't like the asymmetry. cs = numpy.cos(phi) ss = numpy.sin(phi) Ci = 1. * CIb[:,i] Cj = 1. * CIb[:,j] CIb[:,i] = cs * Ci + ss * Cj CIb[:,j] = -ss * Ci + cs * Cj fGrad = fGrad**.5 log.debug(" {0:5d} {1:12.8f} {2:11.2e} {3:8.2f}" .format(it+1, L**(1./exponent), fGrad, logger.perf_counter()-StartTime)) if fGrad < grad_tol: Converged = True break Note = "IB/P%i/2x2, %i iter; Final gradient %.2e" % (exponent, it+1, fGrad) if not Converged: log.note("\nWARNING: Iterative localization failed to converge!" "\n %s", Note) else: log.note(" Iterative localization: %s", Note) log.debug(" Localized orbitals deviation from orthogonality: %8.2e", numpy.linalg.norm(numpy.dot(CIb.T, CIb) - numpy.eye(numOccOrbitals))) # Note CIb is not unitary matrix (although very close to unitary matrix) # because the projection <IAO|OccOrb> does not give unitary matrix. return numpy.dot(iaos, (orth.vec_lowdin(CIb)))
def atomic_pops(mol, mo_coeff, method='meta_lowdin', mf=None): ''' Kwargs: method : string The atomic population projection scheme. It can be mulliken, lowdin, meta_lowdin, iao, or becke Returns: A 3-index tensor [A,i,j] indicates the population of any orbital-pair density |i><j| for each species (atom in this case). This tensor is used to construct the population and gradients etc. You can customize the PM localization wrt other population metric, such as the charge of a site, the charge of a fragment (a group of atoms) by overwriting this tensor. See also the example pyscf/examples/loc_orb/40-hubbard_model_PM_localization.py for the PM localization of site-based population for hubbard model. ''' method = method.lower().replace('_', '-') nmo = mo_coeff.shape[1] proj = numpy.empty((mol.natm,nmo,nmo)) if getattr(mol, 'pbc_intor', None): # whether mol object is a cell s = mol.pbc_intor('int1e_ovlp_sph', hermi=1) else: s = mol.intor_symmetric('int1e_ovlp') if method == 'becke': from pyscf.dft import gen_grid if not (getattr(mf, 'grids', None) and getattr(mf, '_numint', None)): # Call DFT to initialize grids and numint objects mf = mol.RKS() grids = mf.grids ni = mf._numint if not isinstance(grids, gen_grid.Grids): raise NotImplementedError('PM becke scheme for PBC systems') # The atom-wise Becke grids (without concatenated to a vector of grids) coords, weights = grids.get_partition(mol, concat=False) for i in range(mol.natm): ao = ni.eval_ao(mol, coords[i], deriv=0) aow = numpy.einsum('pi,p->pi', ao, weights[i]) charge_matrix = lib.dot(aow.conj().T, ao) proj[i] = reduce(lib.dot, (mo_coeff.conj().T, charge_matrix, mo_coeff)) elif method == 'mulliken': for i, (b0, b1, p0, p1) in enumerate(mol.offset_nr_by_atom()): csc = reduce(numpy.dot, (mo_coeff[p0:p1].conj().T, s[p0:p1], mo_coeff)) proj[i] = (csc + csc.conj().T) * .5 elif method in ('lowdin', 'meta-lowdin'): csc = reduce(lib.dot, (mo_coeff.conj().T, s, orth.orth_ao(mol, method, 'ANO', s=s))) for i, (b0, b1, p0, p1) in enumerate(mol.offset_nr_by_atom()): proj[i] = numpy.dot(csc[:,p0:p1], csc[:,p0:p1].conj().T) elif method in ('iao', 'ibo'): from pyscf.lo import iao assert mf is not None # FIXME: How to handle UHF/UKS object? orb_occ = mf.mo_coeff[:,mf.mo_occ>0] iao_coeff = iao.iao(mol, orb_occ) # # IAO is generally not orthogonalized. For simplicity, we take Lowdin # orthogonalization here. Other orthogonalization can be used. Results # should be very closed to the Lowdin-orth orbitals # # PM with Mulliken population of non-orth IAOs can be found in # ibo.PipekMezey function # iao_coeff = orth.vec_lowdin(iao_coeff, s) csc = reduce(lib.dot, (mo_coeff.conj().T, s, iao_coeff)) iao_mol = iao.reference_mol(mol) for i, (b0, b1, p0, p1) in enumerate(iao_mol.offset_nr_by_atom()): proj[i] = numpy.dot(csc[:,p0:p1], csc[:,p0:p1].conj().T) else: raise KeyError('method = %s' % method) return proj