def lowdin_populations(self, atom_weights=None): r"""Return the Lowdin populations of the molecular orbitals in atomic orbital basis set. Lowdin population analysis is simply the Mulliken population analysis where the basis functions are symmeterically orthogonalized. Parameters ---------- atom_weights : np.ndarray(A, K, K) Weights of the atomic orbital pairs for the atoms. In other words, this weight controls the amount of electrons associated with an atomic orbital pair that will be attributed to an atom. `A` is the number of atoms and `K` is the number of atomic orbitals. Default is the Mulliken partitioning scheme where two orbitals that belong to the given atom is 1, only one orbital that belong to the given atoms is 0.5, and no orbitals is 0. Returns ------- population : np.ndarray(M,) Number of electrons associated with each atom. `M` is the number of atoms, which will be assumed to be the maximum index in `ab_atom_indices`. """ coeff_ab_oab = power_symmetric(self.olp_ab_ab, -0.5) new_orbpart = self.transform_orbitals(coeff_ab_oab, self.ab_atom_indices) return new_orbpart.mulliken_populations(atom_weights=atom_weights)
def project(olp_one_one, olp_one_two): r"""Project one basis set onto another basis set. .. math:: \mathrm{proj}_A \ket{b_i} &= \sum_{kl} \ket{a_k} (S_{A}^{-1})_{kl} \braket{a_l | b_i}\\ &= \sum_{kl} \ket{a_k} (S_{A}^{-1})_{kl} (S_{A,B})_{li}\\ &= \sum_k \ket{a_k} C_{ki} where :math:`(S_A)_{kl} = \braket{a_k | a_l}`, :math:`(S_{A,B})_{li} = \braket{a_l | b_i}`, and :math:`C_{ki} = \sum_l (S_{A}^{-1})_{kl} (S_{A,B})_{li}`. Parameters ---------- olp_one_one : np.ndarray(N, N) Overlap of the basis functions in set 1 with basis functions from set 1. olp_one_two : np.ndarray(N, M) Overlap of the basis functions in set 1 with basis functions from set 2. Returns ------- coeff : np.ndarray(N, M) Transformation matrix from basis functions in set 1 to the projection of baiss set 2 onto basis set 1. """ if not (isinstance(olp_one_one, np.ndarray) and olp_one_one.ndim == 2 and olp_one_one.shape[0] == olp_one_one.shape[1]): raise TypeError( "`olp_one_one` must be a two-dimensional square numpy array.") if not (isinstance(olp_one_two, np.ndarray) and olp_one_two.ndim == 2): raise TypeError("`olp_one_two` must be a two-dimensional numpy array.") if olp_one_one.shape[0] != olp_one_two.shape[0]: raise ValueError( "Number of rows/columns of `olp_one_one` must be equal to the number of rows in " "`olp_one_two`.") olp_one_one_inv = orth.power_symmetric(olp_one_one, -1) coeff_one_proj = olp_one_one_inv.dot(olp_one_two) # Remove zero columns coeff_one_proj = coeff_one_proj[:, np.any(coeff_one_proj, axis=0)] # Normalize olp_proj_proj = coeff_one_proj.T.dot(olp_one_one).dot(coeff_one_proj) normalizer = np.diag(olp_proj_proj)**(-0.5) coeff_one_proj *= normalizer # Check linear dependence rank = np.linalg.matrix_rank(coeff_one_proj) if rank < coeff_one_proj.shape[1]: print(( "Warning: There are {0} linearly dependent projections. The transformation matrix has a" " shape of {1} and rank of {2}".format( coeff_one_proj.shape[1] - rank, coeff_one_proj.shape, rank))) return coeff_one_proj
def test_lowdin_populations(): """Test orbstools.mulliken.lowdin_populations.""" with path("chemtools.data", "naclo4_coeff_ab_mo.npy") as fname: coeff_ab_mo = np.load(str(fname)) with path("chemtools.data", "naclo4_olp_ab_ab.npy") as fname: olp_ab_ab = np.load(str(fname)) with path("chemtools.data", "naclo4_occupations.npy") as fname: occupations = np.load(str(fname)) with path("chemtools.data", "naclo4_ab_atom_indices.npy") as fname: ab_atom_indices = np.load(str(fname)) coeff_ab_oab = power_symmetric(olp_ab_ab, -0.5) assert np.allclose( coeff_ab_oab.T.dot(olp_ab_ab).dot(coeff_ab_oab), np.identity(124)) assert np.allclose( mulliken_populations_newbasis(coeff_ab_mo, occupations, olp_ab_ab, 6, coeff_ab_oab, ab_atom_indices), lowdin_populations(coeff_ab_mo, occupations, olp_ab_ab, 6, ab_atom_indices), )
def test_lowdin_populations(): """Test orbstools.partition.OrbitalPartitionTools.lowdin_populations.""" with path("chemtools.data", "naclo4_coeff_ab_mo.npy") as fname: coeff_ab_mo = np.load(str(fname)) with path("chemtools.data", "naclo4_olp_ab_ab.npy") as fname: olp_ab_ab = np.load(str(fname)) with path("chemtools.data", "naclo4_occupations.npy") as fname: occupations = np.load(str(fname)) with path("chemtools.data", "naclo4_ab_atom_indices.npy") as fname: ab_atom_indices = np.load(str(fname)) coeff_ab_oab = power_symmetric(olp_ab_ab, -0.5) orbpart = OrbitalPartitionTools(coeff_ab_mo, occupations, olp_ab_ab, 6, ab_atom_indices) assert np.allclose( coeff_ab_oab.T.dot(olp_ab_ab).dot(coeff_ab_oab), np.identity(124)) assert np.allclose( orbpart.transform_orbitals(coeff_ab_oab, ab_atom_indices).mulliken_populations(), orbpart.lowdin_populations(), )
def test_power_symmetric(): """Test orbstools.orthogonalization.power_symmetric.""" matrix = np.random.rand(5, 5) # positive semidefinite matrix = matrix.dot(matrix.T) assert np.allclose(orth.power_symmetric(matrix, 2), matrix.dot(matrix)) assert np.allclose(orth.power_symmetric(matrix, -1).dot(matrix), np.identity(5)) power_matrix = orth.power_symmetric(matrix, 0.5) assert np.allclose(power_matrix.dot(power_matrix), matrix) power_matrix = orth.power_symmetric(matrix, 1.0 / 3.0) assert np.allclose(power_matrix.dot(power_matrix).dot(power_matrix), matrix) # not positive semidifinite (probably) matrix = matrix + matrix.T assert np.allclose(orth.power_symmetric(matrix, 2), matrix.dot(matrix)) assert np.allclose(orth.power_symmetric(matrix, -1).dot(matrix), np.identity(5)) # following crashes because it has negative eigenvalues matrix = np.random.rand(100, 100) matrix = matrix + matrix.T assert_raises(ValueError, orth.power_symmetric, matrix, 0.5)
def quao(olp_ab_ab, olp_aao_ab, olp_aao_aao, coeff_ab_mo, indices_span, dim=None): r"""Return transformation matrix from atomic basis functions to QUAO's. Parameters ---------- olp_ab_ab : np.ndarray(K, K) Overlaps of the atomic basis functions. :math:`K` is the number of atomic basis functions. olp_ab_aao : np.ndarray(K, L) Overlaps of the atomic basis functions with the reference basis functions (aao). Rows correspond to the atomic basis functions. :math:`K` is the number of atomic basis functions. Columns correspond to the reference basis functions. :math:`L` is the number of reference basis functions. olp_aao_aao : np.ndarray(L, L) Overlaps of the atomic basis functions with the reference basis functions (aao). Rows correspond to the atomic basis functions. :math:`K` is the number of atomic basis functions. Columns correspond to the reference basis functions. :math:`L` is the number of reference basis functions. coeff_ab_mo : np.ndarray(K, M) Transformation matrix from the atomic basis functions to molecular orbitals. The matrix is applied onto the right side. Rows correspond to the atomic basis functions. :math:`K` is the number of atomic basis functions. Columns correspond to the molecular basis functions. :math:`L` is the number of molecular basis functions. indices_span : np.ndarray(M) Molecular orbitals that will be spanned exactly by the QUAMBO's. Each entry is a boolean, where molecular orbitals that are exactly described have value `True`. dim : {int, None} Number of QUAMBO basis functions. Default is the number of reference basis functions. Returns ------- coeff_ab_quao Transformation matrix from atomic basis functions to QUAO's Notes ----- .. math:: \ket{\mathrm{QUAO}_k} &= \mathrm{proj}_{\mathrm{mMO}^\mathrm{QUAO}}\\ &= \sum_i \ket{ \mathrm{mMO}^\mathrm{QUAO}_i } (olp_{{\mathrm{mMO}^\mathrm{QUAO}}}^{-1})_{ij} \braket{ \mathrm{mMO}^\mathrm{QUAO}_j | \mathrm{AAO}_k }\\ &= \sum_j \ket{AB_j} (C^{\mathrm{AB},\mathrm{mMO}^\mathrm{QUAO}} olp_{{\mathrm{mMO}^\mathrm{QUAO}}}^{-1} olp_{\mathrm{mMO}^\mathrm{QUAO}, \mathrm{AAO}})_{ji}\\ References ---------- .. [1] West, A.C.; Schmidt, M.W. Gordon, M.S; Ruedenberg, K. A comprehensive analysis of molecule-intrinsic quasiatomic, bonding, and correlating orbitals. I. Hartree-Fock wave functions. J. Chem. Phys. 2013, 139, 234107. """ _check_input( olp_ab_ab=olp_ab_ab, olp_aao_ab=olp_aao_ab, olp_aao_aao=olp_aao_aao, coeff_ab_mo=coeff_ab_mo, indices_span=indices_span, ) # Orthogonalize AAOs olp_oaao_ab = orth.power_symmetric(olp_aao_aao, -0.5).dot(olp_aao_ab) # Get MMOs using the orthogonalized AAOs (MMO for QUAOs) coeff_ab_mmo = make_mmo(olp_oaao_ab, coeff_ab_mo, indices_span, dim_mmo=dim) # Find transformation for QUAOs olp_mmo_mmo = coeff_ab_mmo.T.dot(olp_ab_ab).dot(coeff_ab_mmo) olp_mmo_aao = (olp_aao_ab.dot(coeff_ab_mmo)).T coeff_mmo_proj = project(olp_mmo_mmo, olp_mmo_aao) # Normalize olp_proj_proj = coeff_mmo_proj.T.dot(olp_mmo_mmo).dot(coeff_mmo_proj) coeff_mmo_proj *= np.diag(olp_proj_proj)**(-0.5) return coeff_ab_mmo.dot(coeff_mmo_proj)
def lowdin_populations(coeff_ab_mo, occupations, olp_ab_ab, num_atoms, ab_atom_indices, atom_weights=None): r"""Return the Lowdin populations of the given molecular orbitals in atomic orbital basis set. Lowdin population analysis is simply the Mulliken population analysis where the basis functions are symmeterically orthogonalized. Parameters ---------- coeff_ab_mo : np.ndarray(K, M) Transformation matrix from the atomic basis to molecular orbitals. Rows correspond to the atomic basis. Columns correspond to the molecular orbitals. The transformation matrix is applied to the right: .. math:: \ket{\psi_i} = \sum_j \phi_i C_{ij} Data type must be float. `K` is the number of atomic orbitals and `M` is the number of molecular orbitals. occupations : np.ndarray(M,) Occupation numbers of each molecular orbital. Data type must be integers or floats. `M` is the number of molecular orbitals. olp_ab_ab : np.ndarray(K, K) Overlap between atomic basis functions. Data type must be floats. `K` is the number of atomic orbitals. num_atoms : int Number of atoms. Must be an integer. ab_atom_indices : np.ndarray(K,) Index of the atom to which each atomic basis function belongs. Data type must be integers. `K` is the number of atomic orbitals. atom_weights : np.ndarray(A, K, K) Weights of the atomic orbital pairs for the atoms. In other words, this weight controls the amount of electrons associated with an atomic orbital pair that will be attributed to an atom. `A` is the number of atoms and `K` is the number of atomic orbitals. Default is the Mulliken partitioning scheme where two orbitals that belong to the given atom is 1, only one orbital that belong to the given atoms is 0.5, and no orbitals is 0. Returns ------- population : np.ndarray(M,) Number of electrons associated with each atom. `M` is the number of atoms, which will be assumed to be the maximum index in `ab_atom_indices`. """ coeff_ab_oab = power_symmetric(olp_ab_ab, -0.5) return mulliken_populations_newbasis( coeff_ab_mo, occupations, olp_ab_ab, num_atoms, coeff_ab_oab, ab_atom_indices, new_atom_weights=atom_weights, )