From 59e468e0bca7f82fb26d59540b3a16f42f39d281 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Fri, 31 Jul 2020 11:12:24 -0400 Subject: [PATCH 01/14] Add decontracted molecular basis conversion --- iodata/basis.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/iodata/basis.py b/iodata/basis.py index c9aff450a..0376db59d 100644 --- a/iodata/basis.py +++ b/iodata/basis.py @@ -211,6 +211,24 @@ def get_segmented(self): # pylint: disable=no-member return attr.evolve(self, shells=shells) + def get_decontracted(self): + r""" + Get Decontracted Molecular Basis from a Molecular Basis. + + Decontracted Molecular basis is a Molecular basis where each contracted shell is a + primitive contracted shell (ie contracted shell with only one exponent and one kind). + + """ + shells = [] + for shell in self.shells: + for i, (angmom, kind) in enumerate(zip(shell.angmoms, shell.kinds)): + for exponent, coeff in zip(shell.exponents, shell.coeffs[:, i]): + shells.append( + Shell(shell.icenter, [angmom], [kind], np.array([exponent]), + coeff.reshape(-1, 1)) + ) + # pylint: disable=no-member + return self._replace(shells=shells) def convert_convention_shell(conv1: List[str], conv2: List[str], reverse=False) \ -> Tuple[np.ndarray, np.ndarray]: From ea423c5be888d8dd03bba7a0d34a460d9661936e Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Fri, 31 Jul 2020 11:13:15 -0400 Subject: [PATCH 02/14] Add test decontracted molecular basis conversion --- iodata/test/test_basis.py | 55 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/iodata/test/test_basis.py b/iodata/test/test_basis.py index 73140fcdf..5d859a1ad 100644 --- a/iodata/test/test_basis.py +++ b/iodata/test/test_basis.py @@ -228,6 +228,61 @@ def test_convert_convention_shell(): assert_equal(vec2[permutation] * signs, vec1) +def test_get_decontracted(): + obasis = MolecularBasis( + [Shell(0, [0], ['c'], [0.01], np.array([[3.]])), + Shell(1, [0, 1], ['c', 'c'], [0.03, 0.04], np.array([[3., 4.], [1., 2.]])), + Shell(0, [2, 1, 2], ['p', 'c', 'c'], [0.05], np.array([[5., 5., 6.]]))], + {(0, 'c'): ['s'], + (1, 'c'): ['x', 'z', '-y'], + (2, 'p'): ['dc0', 'dc1', '-ds1', 'dc2', '-ds2']}, + 'L2' + ) + obasis = obasis.get_decontracted() + + ncontshells = 8 # Number of contracted shells now is equal to the number of primitives. + assert_equal(len(obasis.shells), ncontshells) + # Assert each shell has length one exponents + for i in range(0, ncontshells): + assert_equal(len(obasis.shells[0].exponents), 1) + # Assert they have the right exponents. + assert_equal(obasis.shells[0].exponents, 0.01) + assert_equal(obasis.shells[1].exponents, 0.03) + assert_equal(obasis.shells[2].exponents, 0.04) + assert_equal(obasis.shells[3].exponents, 0.03) + assert_equal(obasis.shells[4].exponents, 0.04) + assert_equal(obasis.shells[5].exponents, 0.05) + assert_equal(obasis.shells[6].exponents, 0.05) + assert_equal(obasis.shells[7].exponents, 0.05) + # Assert they have the right coefficients. + assert_equal(obasis.shells[0].coeffs, 3.) + assert_equal(obasis.shells[1].coeffs, np.array([[3.]])) + assert_equal(obasis.shells[2].coeffs, np.array([[1.]])) + assert_equal(obasis.shells[3].coeffs, np.array([[4.]])) + assert_equal(obasis.shells[4].coeffs, np.array([[2.]])) + assert_equal(obasis.shells[5].coeffs, np.array([[5.]])) + assert_equal(obasis.shells[6].coeffs, np.array([[5.]])) + assert_equal(obasis.shells[7].coeffs, np.array([[6.]])) + # Assert right centers + assert_equal(obasis.shells[0].icenter, 0) + assert_equal(obasis.shells[1].icenter, 1) + assert_equal(obasis.shells[2].icenter, 1) + assert_equal(obasis.shells[3].icenter, 1) + assert_equal(obasis.shells[4].icenter, 1) + assert_equal(obasis.shells[5].icenter, 0) + assert_equal(obasis.shells[6].icenter, 0) + assert_equal(obasis.shells[7].icenter, 0) + # Assert right kind + assert_equal(obasis.shells[0].kinds, ['c']) + assert_equal(obasis.shells[1].kinds, ['c']) + assert_equal(obasis.shells[2].kinds, ['c']) + assert_equal(obasis.shells[3].kinds, ['c']) + assert_equal(obasis.shells[4].kinds, ['c']) + assert_equal(obasis.shells[5].kinds, ['p']) + assert_equal(obasis.shells[6].kinds, ['c']) + assert_equal(obasis.shells[7].kinds, ['c']) + + def test_convert_convention_obasis(): obasis = MolecularBasis( [Shell(0, [0], ['c'], np.zeros(3), np.zeros((3, 1))), From 86fcf0dd42ca407b99e929e94eeca2c118f99bd0 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Fri, 31 Jul 2020 15:14:05 -0400 Subject: [PATCH 03/14] Add conversion of vector wrt to basis sets Added the ability to convert the coefficients of a vector inside one basis to another basis of possible different size. --- iodata/overlap.py | 64 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/iodata/overlap.py b/iodata/overlap.py index 75a71de3c..112ca6f97 100644 --- a/iodata/overlap.py +++ b/iodata/overlap.py @@ -26,7 +26,9 @@ from .basis import convert_conventions, iter_cart_alphabet, MolecularBasis from .basis import HORTON2_CONVENTIONS as OVERLAP_CONVENTIONS -__all__ = ['OVERLAP_CONVENTIONS', 'compute_overlap', 'gob_cart_normalization'] +__all__ = [ + 'OVERLAP_CONVENTIONS', 'compute_overlap', 'gob_cart_normalization', "convert_vector_basis" +] # pylint: disable=too-many-nested-blocks,too-many-statements @@ -223,3 +225,63 @@ def gob_cart_normalization(alpha: np.ndarray, n: np.ndarray) -> np.ndarray: """ return np.sqrt((4 * alpha) ** sum(n) * (2 * alpha / np.pi) ** 1.5 / np.prod(factorial2(2 * n - 1))) + + +def convert_vector_basis( + coeffs1: np.ndarray, + basis2_overlap: np.ndarray, + basis21_overlap: np.ndarray +) -> np.ndarray: + r""" + Convert a vector from basis 1 of size M to another basis 2 of size N. + + Basis vectors are defined to be linearly independent wrt to one another + and need not be orthogonal. + + Parameters + ---------- + coeffs1: + Coefficients of the vector expanded in basis set 1. Shape is (M,). + basis2_overlap: + Symmetric matrix whose entries are the inner-product between basis vectors + inside basis set 2. Shape is (N, N). + basis21_overlap: + The overlap matrix between basis set 1 and basis set 2. Shape is (N, M). + + Returns + ------- + coeffs2 : + Coefficients of the vector expanded in basis set 2. Shape is (N,). + + Raises + ------ + ValueError : + If shapes of the matrices don't match the requirements in the docs. + LinAlgError : + If least squared solution does not converge. + + Notes + ----- + - `basis2_overlap` is the matrix with (i, j)th entries + :math:`\braket{\psi_i, \psi_j}` where :math:`\psi_i` are in basis set 2. + + - `basis21_overlap` is the matrix whose (i, j)th entries are + :math:`\braket{\psi_i, \phi_j}` where :math:`\phi_j` are in basis set 1 and + :math:`\psi_i` are in basis set 2. + + - If `basis2_overlap` is not full rank, then least-squared solution is solved instead. + + """ + if basis2_overlap.shape[0] != basis2_overlap.shape[1]: + raise ValueError("The `basis2_overlap` should be a square matrix.") + if np.any(np.abs(basis2_overlap.T - basis2_overlap) > 1e-10): + raise ValueError("The `basis2_overlap` should be a symmetric matrix.") + + b = basis21_overlap.dot(coeffs1) + try: + # Try solving exact solution if `basis12_overlap` is full rank. + coeffs2 = np.linalg.solve(basis2_overlap, b) + except np.linalg.LinAlgError: + # Try least-squared solution. + coeffs2, _, _, _ = np.linalg.lstsq(basis2_overlap, b) + return coeffs2 From 330fcc74c105f598c969d1c51d982c09ba4e6798 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Fri, 31 Jul 2020 19:12:31 -0400 Subject: [PATCH 04/14] Add tests for conversion vecs wrt basis sets --- iodata/test/test_overlap.py | 80 ++++++++++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 2 deletions(-) diff --git a/iodata/test/test_overlap.py b/iodata/test/test_overlap.py index 797d7de49..b5d5155ae 100644 --- a/iodata/test/test_overlap.py +++ b/iodata/test/test_overlap.py @@ -20,12 +20,13 @@ import attr import numpy as np -from numpy.testing import assert_allclose +from numpy.testing import assert_allclose, assert_equal from pytest import raises from ..api import load_one from ..basis import MolecularBasis, Shell -from ..overlap import compute_overlap, OVERLAP_CONVENTIONS +from ..overlap import compute_overlap, OVERLAP_CONVENTIONS, convert_vector_basis +from ..overlap_cartpure import tfs try: from importlib_resources import path @@ -85,3 +86,78 @@ def test_overlap_l1(): atcoords = np.zeros((1, 3)) with raises(ValueError): _ = compute_overlap(dbasis, atcoords) + + +def test_converting_between_orthonormal_basis_set(): + # Test converting from basis set of M Elements to N Elements where M <= N. + # All basis sets are assumed orthonormal and 1st basis set is assumed to be + # in the second basis set. + M = np.random.randint(1, 25) + N = np.random.randint(M, 50) + coeffs1 = np.random.random(M) + basis2_overlap = np.eye(N) # Since it is orthonormal, it is identity. + + basis21_overlap = np.zeros((N, M)) + for i in range(0, M): + basis21_overlap[i, i] = 1. # Since it is a subset. + + coeffs2 = convert_vector_basis(coeffs1, basis2_overlap, basis21_overlap) + assert_allclose(coeffs1, coeffs2[:M], rtol=1.e-5, atol=1.e-8) + assert_allclose(np.zeros(coeffs2[M:].shape), coeffs2[M:], rtol=1.e-5, atol=1.e-8) + + # Test converting in the reverse direction ie Large to small. + coeffs2 = np.random.random(N) + basis1_overlap = np.eye(M) + basis12_overlap = np.zeros((M, N)) + for i in range(0, M): + basis12_overlap[i, i] = 1. # Since it is a subset. + + coeffs1 = convert_vector_basis(coeffs2, basis1_overlap, basis12_overlap) + assert_equal(len(coeffs1), M) + assert_allclose(coeffs1, coeffs2[:M], rtol=1.e-5, atol=1.e-8) + + +def test_converting_from_cartesian_to_pure(): + # Test converting simple one coefficient, S-type. + overlap_cp = tfs[0] + coeff = np.array([5.]) + coeff2 = convert_vector_basis(coeff, np.eye(1), overlap_cp) + assert_allclose(coeff, coeff2, rtol=1.e-5, atol=1.e-8) + + # Test converting p-type. + overlap_cp = tfs[1] + coeff = np.array([1., 2., 3.]) + coeff2 = convert_vector_basis(coeff, np.eye(3), overlap_cp) + desired = np.array([3., 1., 2.]) + assert_allclose(coeff2, desired, rtol=1.e-5, atol=1.e-8) + + # Test converting d-type. + overlap_cp = tfs[2] + coeff = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]) + coeff2 = convert_vector_basis(coeff, np.eye(5), overlap_cp) + desired = np.array([-0.5 - 0.5 * 4.0 + 6.0, 3.0, 5.0, + 0.86602540378 * 1.0 - 0.86602540378 * 4.0, 2.0]) + assert_allclose(coeff2, desired, rtol=1.e-5, atol=1.e-8) + + +def test_converting_from_pure_to_cartesian(): + # Test converting S-type. + overlap_cp = tfs[0] + coeff = np.array([5.]) + coeff2 = convert_vector_basis(coeff, np.eye(1), overlap_cp.T) + assert_allclose(coeff, coeff2, rtol=1.e-5, atol=1.e-8) + + # Test converting P-type. + overlap_cp = tfs[1] + coeff = np.array([1., 2., 3.]) + coeff2 = convert_vector_basis(coeff, np.eye(3), overlap_cp.T) + desired = np.array([2., 3., 1.]) + assert_allclose(coeff2, desired, rtol=1.e-5, atol=1.e-8) + + # Test converting D-type. + overlap_cp = tfs[2] + coeff = np.array([1., 2., 3., 4., 5.]) + coeff2 = convert_vector_basis(coeff, np.eye(6), overlap_cp.T) + desired = np.array([np.sqrt(3.0) * 4.0 / 2.0 - 0.5, 5.0, 2.0, + -np.sqrt(3.0) * 4.0 / 2.0 - 0.5, 3.0, 1.0]) + assert_allclose(coeff2, desired, rtol=1.e-5, atol=1.e-8) From 6cb1f65e141c122f5f1576aed119cae2ef9b2d33 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Wed, 5 Aug 2020 08:47:19 -0400 Subject: [PATCH 05/14] Add conversion between primitive shells and kind The method "convert_kind" inside class MolecularBasis is added that converts all kind of a shell to either Cartesian or Pure. Note that the coefficients are averaged, since iodata assumes coefficients are the same throughtout a Primitive Shell. The function "convert_primitive_kind" allows the ability to convert one primitive shell to another. --- iodata/basis.py | 120 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/iodata/basis.py b/iodata/basis.py index 0376db59d..1e31c4a41 100644 --- a/iodata/basis.py +++ b/iodata/basis.py @@ -24,6 +24,7 @@ import attr import numpy as np +from .overlap_cartpure import tfs from .attrutils import validate_shape @@ -230,6 +231,125 @@ def get_decontracted(self): # pylint: disable=no-member return self._replace(shells=shells) + def convert_kind(self, to: str): + r""" + Convert all contractions from Pure (or Cartesian) kind to Cartesian (or Pure). + + Parameters + ---------- + to + Specifying which one to convert to. It is either "c" for Cartesian or "p" for Pure. + + Raises + ------ + ValueError : + If "to" is not recognized to be either "c" or "p". + + Notes + ----- + - Some of these conversion will cause the contraction coefficients to be + different across different shells. In such a scenario, + this function will average all coefficients so that the shell-type inside a contracted + shell will have the same coefficients. + + See Also + -------- + convert_primitive_kind : Converts individual groups of primitive shells. + + """ + if to != "c" and to != "p": + raise ValueError("The to string was not recognized: %s" % to) + + shells = [] + for shell in self.shells: + kind_new = [] + coeffs_new = [] + + for angmom, kind, coeffs in zip(shell.angmoms, shell.kinds, shell.coeffs.T): + # Need to convert the kind of shell. S-type doesn't need to change. + if kind != to and angmom >= 2: + if kind == "c": + b = np.repeat(coeffs[np.newaxis], tfs[angmom].shape[1], axis=0) + coeff = tfs[angmom].dot(b) + # Take the mean, since the coefficients vary between primitives. + coeff = np.mean(coeff, axis=0) + elif kind == "p": + b = np.repeat(coeffs[np.newaxis], tfs[angmom].shape[0], axis=0) + # Solve it using least-squared projection. + coeff = np.linalg.lstsq(tfs[angmom], b, rcond=None)[0] + # Take the mean, since the coefficients vary between primitives. + coeff = np.mean(coeff, axis=0) + kind_new.append(to) + coeffs_new.append(coeff) + # No need to convert. + else: + kind_new.append(to) + coeffs_new.append(coeffs) + + shells.append( + Shell(shell.icenter, angmom, kind_new, shell.exponents, np.array(coeffs_new).T) + ) + # pylint: disable=no-member + return self._replace(shells=shells) + + +def convert_primitive_kind(angmom: int, kind: str, coeffs: np.ndarray, to: str) -> np.ndarray: + r""" + Converts coefficients in Cartesian to Pure or vice-versa of a Primitive shell. + + Parameters + ---------- + angmom + Integer representing the total angular momentum number. + kind + String representing "p" for Pure primitives and "c" for Cartesian primitives. + coeffs + Coefficients of the contraction of type `kind`. Shape is (N,) where + N is the number of primitives with angular momentum `angmom` and type `kind`. + For example, angmom=2 and kind="c" implies N=6, + The order should match the conventions. + to + Specifying which one to convert to. It is either "c" for Cartesian or "p" for Pure. + + Returns + ------- + new_coeffs + Coefficients in the new basis (either Cartesian or Pure). + + Examples + -------- + Suppose one has the following contraction with angular momentum 2 and kind is Cartesian with + integers (2, 0, 0) and (1, 1, 0): + + .. math:: + c_1 x^2 y^0 z^0 e^{-\alpha r^2} + c_2 x y z^0 e^{-\alpha r^2} + + Then the coefficients of this in the right convention [xx, xy, xz, yy, yz, zz] is + + >> coeff = [c_1, c_2, 0, 0, 0, 0] + + The basis function conventions dictionary are documented in :ref:`basis_conventions and + in the MolecularBasis class documentation. + + To convert this Cartesian contraction to Pure type, one would do + + >> new_coeffs = convert_primitive_kind(1, "c", coeff, "p") + + """ + if to != "c" and to != "p": + raise ValueError("The to string was not recognized: %s" % to) + if kind != "c" and kind != "p": + raise ValueError("The kind string was not recognized: %s" % kind) + if to != kind: + if kind == "c": + coeff = tfs[angmom].dot(coeffs) + else: + # Solve it using least-squared projection. + coeff = np.linalg.lstsq(tfs[angmom], coeffs, rcond=None)[0] + return coeff + return coeffs + + def convert_convention_shell(conv1: List[str], conv2: List[str], reverse=False) \ -> Tuple[np.ndarray, np.ndarray]: """Return a permutation vector and sign changes to convert from 1 to 2. From d92ea87c836565caea68e29b033360bbd5f20bfb Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Wed, 5 Aug 2020 08:49:40 -0400 Subject: [PATCH 06/14] Add test for conversion between kind and primitive --- iodata/basis.py | 6 +-- iodata/test/test_basis.py | 89 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 90 insertions(+), 5 deletions(-) diff --git a/iodata/basis.py b/iodata/basis.py index 1e31c4a41..23990b889 100644 --- a/iodata/basis.py +++ b/iodata/basis.py @@ -229,7 +229,7 @@ def get_decontracted(self): coeff.reshape(-1, 1)) ) # pylint: disable=no-member - return self._replace(shells=shells) + return attr.evolve(self, shells=shells) def convert_kind(self, to: str): r""" @@ -287,10 +287,10 @@ def convert_kind(self, to: str): coeffs_new.append(coeffs) shells.append( - Shell(shell.icenter, angmom, kind_new, shell.exponents, np.array(coeffs_new).T) + Shell(shell.icenter, shell.angmoms, kind_new, shell.exponents, np.array(coeffs_new).T) ) # pylint: disable=no-member - return self._replace(shells=shells) + return attr.evolve(self, shells=shells) def convert_primitive_kind(angmom: int, kind: str, coeffs: np.ndarray, to: str) -> np.ndarray: diff --git a/iodata/test/test_basis.py b/iodata/test/test_basis.py index 5d859a1ad..997b1b58c 100644 --- a/iodata/test/test_basis.py +++ b/iodata/test/test_basis.py @@ -21,11 +21,11 @@ import attr import numpy as np -from numpy.testing import assert_equal +from numpy.testing import assert_equal, assert_allclose import pytest from ..basis import (angmom_sti, angmom_its, Shell, MolecularBasis, - convert_convention_shell, convert_conventions, + convert_convention_shell, convert_conventions, convert_primitive_kind, iter_cart_alphabet, HORTON2_CONVENTIONS, PSI4_CONVENTIONS) from ..formats.cp2klog import CONVENTIONS as CP2K_CONVENTIONS @@ -283,6 +283,91 @@ def test_get_decontracted(): assert_equal(obasis.shells[7].kinds, ['c']) +def test_convert_kind_to_cartesian(): + obasis = MolecularBasis( + [Shell(0, [0], ['c'], [0.01], np.array([[3.]])), + Shell(1, [0, 1], ['c', 'c'], [0.03, 0.04], np.array([[3., 4.], [1., 2.]])), + Shell(0, [2, 1], ['p', 'c'], [0.05], np.array([[5., 6.]]))], + {(0, 'c'): ['s'], + (1, 'c'): ['x', 'z', '-y'], + (2, 'p'): ['dc0', 'dc1', '-ds1', 'dc2', '-ds2']}, + 'L2' + ) + obasis = obasis.convert_kind("c") + # Assert number of shells is the same. + assert_equal(len(obasis.shells), 3) + # Assert the already cartesian are still cartesian. + assert_equal(obasis.shells[0].kinds, ["c"]) + assert_equal(obasis.shells[1].kinds, ["c", "c"]) + assert_equal(obasis.shells[2].kinds, ["c", "c"]) + # Assert the exponents should not change. + assert_equal(obasis.shells[0].exponents, [0.01]) + assert_equal(obasis.shells[1].exponents, [0.03, 0.04]) + assert_equal(obasis.shells[2].exponents, [0.05]) + # Assert that the coefficients expect for the pure is the same. + assert_equal(obasis.shells[0].coeffs, np.array([[3.]])) + assert_equal(obasis.shells[1].coeffs, np.array([[3., 4.], [1., 2.]])) + + # Assert that the coefficient of the pure-shell inside the third contraction changed to cart. + # First solve for the coefficients, this was done manually. + coeff_pure = np.array([[1.22008468], [5.], [5.], [-4.55341801], [5.], [3.33333333]]) + assert_allclose(obasis.shells[2].coeffs, np.array([[np.mean(coeff_pure), 6.0]])) + + # Convert Easier Example where nothing happens. + obasis = MolecularBasis( + [Shell(0, [1], ['p'], [0.05, 0.01], np.array([[5.], [10.]]))], None, 'L2' + ) + obasis = obasis.convert_kind("c") + assert_equal(len(obasis.shells), 1) + assert_equal(obasis.shells[0].kinds, ["c"]) + assert_equal(obasis.shells[0].exponents, [0.05, 0.01]) + assert_equal(obasis.shells[0].coeffs, np.array([[5.], [10.]])) + + +def test_convert_kind_to_pure(): + obasis = MolecularBasis( + [Shell(0, [0], ['c'], [0.01], np.array([[3.]])), + Shell(1, [1, 2], ['c', 'c'], [0.03, 0.04], np.array([[3., 4.], [1., 2.]]))], + {(0, 'c'): ['s'], + (1, 'c'): ['x', 'z', '-y'], + (2, 'p'): ['dc0', 'dc1', '-ds1', 'dc2', '-ds2']}, + 'L2' + ) + obasis = obasis.convert_kind("p") + assert_equal(len(obasis.shells), 2) + assert_equal(obasis.shells[0].coeffs, np.array([[3.]])) + new_coeff = np.mean(np.array([[0.], [4.], [4.], [0.], [4.]])) + new_coeff2 = np.mean(np.array([[0.], [2.], [2.], [0.], [2.]])) + desired = np.array([[3., new_coeff], [1., new_coeff2]]) + assert_equal(obasis.shells[1].coeffs, desired) + + +def test_convert_primitive_kind(): + # Test converting P-type from kind="c" to kind="p". + coeff = np.array([1., 2., 3.]) + coeff2 = convert_primitive_kind(1, "c", coeff, "p") + desired = np.array([3., 1., 2.]) + assert_equal(coeff2, desired) + + # Test converting D-type from kind="c" to kind="p". + coeff = np.array([1., 2., 3., 4., 5., 6.]) + coeff = convert_primitive_kind(2, "c", coeff, "p") + desired = np.array([3.5, 3., 5., -2.59807621, 2.]) + assert_allclose(coeff, desired) + + # Test converting P-type from kind="p" to kind="c". + coeff = np.array([1., 2., 3.]) + coeff2 = convert_primitive_kind(1, "p", coeff, "c") + desired = np.array([2., 3., 1.]) + assert_equal(coeff2, desired) + + # Test converting D-type from kind="p" to kind="c". + coeff = np.array([1., 2., 3., 4., 5.]) + ccoeff = convert_primitive_kind(2, "p", coeff, "c") + desired = np.array([1.97606774, 5., 2., -2.64273441, 3., 0.66666667]) + assert_allclose(ccoeff, desired) + + def test_convert_convention_obasis(): obasis = MolecularBasis( [Shell(0, [0], ['c'], np.zeros(3), np.zeros((3, 1))), From 7b7913bc67795d1e24bfffd811e7aec54dd8cc71 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Wed, 5 Aug 2020 08:52:13 -0400 Subject: [PATCH 07/14] Update doc for vector conversion in overlap --- iodata/overlap.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/iodata/overlap.py b/iodata/overlap.py index 112ca6f97..72e527657 100644 --- a/iodata/overlap.py +++ b/iodata/overlap.py @@ -235,9 +235,6 @@ def convert_vector_basis( r""" Convert a vector from basis 1 of size M to another basis 2 of size N. - Basis vectors are defined to be linearly independent wrt to one another - and need not be orthogonal. - Parameters ---------- coeffs1: @@ -262,14 +259,18 @@ def convert_vector_basis( Notes ----- + - Basis vectors are defined to be linearly independent wrt to one another + and need not be orthogonal. + - `basis2_overlap` is the matrix with (i, j)th entries - :math:`\braket{\psi_i, \psi_j}` where :math:`\psi_i` are in basis set 2. + :math:`\braket{\psi_i, \psi_j}` where :math:`\psi_i` are in basis set 2. - `basis21_overlap` is the matrix whose (i, j)th entries are - :math:`\braket{\psi_i, \phi_j}` where :math:`\phi_j` are in basis set 1 and - :math:`\psi_i` are in basis set 2. + :math:`\braket{\psi_i, \phi_j}` where :math:`\phi_j` are in basis set 1 and + :math:`\psi_i` are in basis set 2. - - If `basis2_overlap` is not full rank, then least-squared solution is solved instead. + - If `basis2_overlap` is not full rank, then least-squared solution is solved instead. This + is effectively solving :math:`||b - Ax||`. """ if basis2_overlap.shape[0] != basis2_overlap.shape[1]: From 3741b0cc671b7db2b1a8563456f00ddf2a50fea9 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Wed, 5 Aug 2020 08:52:52 -0400 Subject: [PATCH 08/14] Clean test for vector conversion overlap --- iodata/test/test_overlap.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/iodata/test/test_overlap.py b/iodata/test/test_overlap.py index b5d5155ae..b574a5bc2 100644 --- a/iodata/test/test_overlap.py +++ b/iodata/test/test_overlap.py @@ -135,8 +135,7 @@ def test_converting_from_cartesian_to_pure(): overlap_cp = tfs[2] coeff = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]) coeff2 = convert_vector_basis(coeff, np.eye(5), overlap_cp) - desired = np.array([-0.5 - 0.5 * 4.0 + 6.0, 3.0, 5.0, - 0.86602540378 * 1.0 - 0.86602540378 * 4.0, 2.0]) + desired = overlap_cp.dot(coeff) assert_allclose(coeff2, desired, rtol=1.e-5, atol=1.e-8) @@ -151,13 +150,12 @@ def test_converting_from_pure_to_cartesian(): overlap_cp = tfs[1] coeff = np.array([1., 2., 3.]) coeff2 = convert_vector_basis(coeff, np.eye(3), overlap_cp.T) - desired = np.array([2., 3., 1.]) + desired = np.linalg.lstsq(overlap_cp, coeff, rcond=None)[0] assert_allclose(coeff2, desired, rtol=1.e-5, atol=1.e-8) # Test converting D-type. overlap_cp = tfs[2] - coeff = np.array([1., 2., 3., 4., 5.]) - coeff2 = convert_vector_basis(coeff, np.eye(6), overlap_cp.T) - desired = np.array([np.sqrt(3.0) * 4.0 / 2.0 - 0.5, 5.0, 2.0, - -np.sqrt(3.0) * 4.0 / 2.0 - 0.5, 3.0, 1.0]) - assert_allclose(coeff2, desired, rtol=1.e-5, atol=1.e-8) + pcoeff = np.array([1., 2., 3., 4., 5.]) + ccoeff = convert_vector_basis(pcoeff, np.eye(6), np.linalg.pinv(overlap_cp)) + desired = np.linalg.lstsq(overlap_cp, pcoeff, rcond=None)[0] + assert_allclose(ccoeff, desired, rtol=1.e-5, atol=1.e-8) From 40f3ac66a8d32092ea6952ae971d93ca4e7961a6 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Wed, 5 Aug 2020 09:20:14 -0400 Subject: [PATCH 09/14] Update and Add shell documentations Added - COntracted Shell instead of Shell. - Primitive here means Primitive Shell. - Notes section detailing the definition and formulas. --- iodata/basis.py | 51 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/iodata/basis.py b/iodata/basis.py index 23990b889..36c17e342 100644 --- a/iodata/basis.py +++ b/iodata/basis.py @@ -89,7 +89,7 @@ def angmom_its(angmom: Union[int, List[int]]) -> Union[str, List[str]]: @attr.s(auto_attribs=True, slots=True, on_setattr=[attr.setters.validate, attr.setters.convert]) class Shell: - """A shell in a molecular basis representing (generalized) contractions with the same exponents. + """Contracted shell representing (generalized) contractions with the same exponents. Attributes ---------- @@ -103,7 +103,8 @@ class Shell: and 'p' for pure. Pure functions are only allowed for angmom>1. The length equals the number of contractions: len(angmoms)=ncon. exponents - The array containing the exponents of the primitives, with shape (nprim,). + The array containing the exponents of the primitives, with shape (nprim,), + where nprim is the number of primitive contracted shells. coeffs The array containing the coefficients of the normalized primitives in each contraction; shape = (nprim, ncon). @@ -111,6 +112,45 @@ class Shell: (densities) normalized, but contractions are not necessarily normalized. (This depends on the code which generated the contractions.) + Notes + ----- + Following definitions assumes all functions are centered at A specified by attribute `icenter`. + + - Cartesian Primitive Gaussian function for a given numbers (i, j, k) and exponent + :math:`\alpha`: + + .. math:: N() x_A^i y_A^j z_A^k e^{-\alpha_i r^2} + + - Pure Primitive Gaussian functions for a given integers (l, m) and exponent :math:`\alpha`: + + .. math:: N(\alpha, l) P(r, \theta, \phi) e^{-\alpha_i r^2} + \begin{align*} + C_{0m}(r, \theta, \phi) &:= r^l P^0_{l}(\cos(\theta)) & m = 0\\ + C_{lm}(r, \theta, \phi) &:= \sqrt{2} (-1)^m \sqrt{\frac{(l - m)!}{(l + m)!}} + r^l P^m_l(\cos(\theta)) \cos(m \phi) & m \in \{1, \cdots, l\} \\ + S_{lm}(r, \theta, \phi) &:= \sqrt{2} (-1)^m \sqrt{\frac{(l - m)!}{(l + m)!}} + r^l P^m_l(\cos(\theta)) \sin(m \phi) & m \in \{1, \cdots, l\} \\ + \end{align*} + + where :math:`P_l^m` is the associated Legrende function and :math:`N(\alpha, l)` is the + normalization constant. + + - Contraction of Primitive Gaussians is the linear combination of primitive Gaussian functions + of the same shell-type l, meant as a basis function. It has the form + + .. math:: P(\cdots) \sum^M_m c_m N e^{-\alpha_m r_A^2}, + + where N is the normalization constant and P is either the Cartesian polynomial for a fixed + (i, j, k) or the real regular solid harmonic for a fixed (l, m). The degree is the number + of primitive used. + + - Contracted Shell with exponents :math:`(\alpha_1, \cdots, \alpha_{M})` is the set of all + contractions whose primitives have the exponents + :math:`(\alpha_1, \cdots, \alpha_{M})` in that order. + + - Primitive contracted shell with exponent :math:`\alpha` is the set of all contractions + in a contracted shell that have the exponent :math:`\alpha`. + """ icenter: int @@ -134,12 +174,15 @@ def nbasis(self) -> int: # noqa: D401 @property def nprim(self) -> int: # noqa: D401 - """Number of primitives, also known as the contraction length.""" + """Number of primitive contracted shells, also known as the contraction length.""" return len(self.exponents) @property def ncon(self) -> int: # noqa: D401 - """Number of contractions. This is usually 1; e.g., it would be 2 for an SP shell.""" + """Number of contractions with distinct angular momentum numbers. + + This is usually 1; e.g., it would be 2 for an SP shell. + """ return len(self.angmoms) From 989c8b7f66845a4d3c84e124b33d994e38adbcdb Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Wed, 5 Aug 2020 09:33:19 -0400 Subject: [PATCH 10/14] Update and add doc to MolecularBasis Added - Emphasis on contracted shell. - Method section detailing the methods - Notes including the definitions of different types of molecular basis. --- iodata/basis.py | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/iodata/basis.py b/iodata/basis.py index 36c17e342..aeb691971 100644 --- a/iodata/basis.py +++ b/iodata/basis.py @@ -189,7 +189,7 @@ def ncon(self) -> int: # noqa: D401 @attr.s(auto_attribs=True, slots=True, on_setattr=[attr.setters.validate, attr.setters.convert]) class MolecularBasis: - """A complete molecular orbital or density basis set. + """A complete molecular orbital or density basis set as a collection of contracted shells. Attributes ---------- @@ -234,6 +234,30 @@ class MolecularBasis: The normalization convention of primitives, which can be 'L2' (orbitals) or 'L1' (densities) normalized. + Methods + ------- + get_segmented + Return molecular basis object that is segmented. + get_decontracted + Return molecular basis object that is decontracted. + convert_kind + Return molecular basis object whose kinds are the same type. + + Notes + ----- + - Primitive Contracted Shell with exponent :math:`\alpha`: + Set of all contractions in a contracted shell that have the exponent :math:`\alpha`. + Since there is a single exponent, then the degree of these contractions is one. + + - Segmented Molecular Basis: + Molecular basis where each contracted shell has contractions that all correspond to the + same total angular momentum number. There could only be the same shell-type inside a + contracted shell. There could only be one kind of shell-type inside a contracted shell. + + - Decontracted Molecular Basis: + Segmented molecular basis where each contracted shell is a primitive contracted shell with + its single exponent. + """ shells: List[Shell] @@ -246,7 +270,11 @@ def nbasis(self) -> int: # noqa: D401 return sum(shell.nbasis for shell in self.shells) def get_segmented(self): - """Unroll generalized contractions.""" + """Convert Generalized Molecular Basis to Segmented Molecular Basis. + + Segmented Molecular basis is a Molecular basis where all contracted shell in it share + a specific total angular momentum number. + """ shells = [] for shell in self.shells: for angmom, kind, coeffs in zip(shell.angmoms, shell.kinds, shell.coeffs.T): @@ -257,7 +285,7 @@ def get_segmented(self): def get_decontracted(self): r""" - Get Decontracted Molecular Basis from a Molecular Basis. + Convert to Decontracted Molecular Basis from a Molecular Basis. Decontracted Molecular basis is a Molecular basis where each contracted shell is a primitive contracted shell (ie contracted shell with only one exponent and one kind). @@ -297,7 +325,7 @@ def convert_kind(self, to: str): See Also -------- - convert_primitive_kind : Converts individual groups of primitive shells. + convert_primitive_kind : Converts primitive shells with no averaging. """ if to != "c" and to != "p": From 70968769953b29e1138ef96cf3c099aca33abbc6 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Wed, 5 Aug 2020 09:55:59 -0400 Subject: [PATCH 11/14] Fix rob issues in basis --- iodata/basis.py | 30 +++++++++++++++++------------- iodata/test/test_basis.py | 4 ++-- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/iodata/basis.py b/iodata/basis.py index aeb691971..da6aec99f 100644 --- a/iodata/basis.py +++ b/iodata/basis.py @@ -38,11 +38,13 @@ def _alsolist(f): """Wrap a function to accepts also list as first argument and then return list.""" + @wraps(f) def wrapper(firsts, *args, **kwargs): if isinstance(firsts, (Integral, str)): return f(firsts, *args, **kwargs) return [f(first, *args, **kwargs) for first in firsts] + return wrapper @@ -129,7 +131,7 @@ class Shell: C_{lm}(r, \theta, \phi) &:= \sqrt{2} (-1)^m \sqrt{\frac{(l - m)!}{(l + m)!}} r^l P^m_l(\cos(\theta)) \cos(m \phi) & m \in \{1, \cdots, l\} \\ S_{lm}(r, \theta, \phi) &:= \sqrt{2} (-1)^m \sqrt{\frac{(l - m)!}{(l + m)!}} - r^l P^m_l(\cos(\theta)) \sin(m \phi) & m \in \{1, \cdots, l\} \\ + r^l P^m_l(\cos(\theta)) \sin(m \phi) & m \in \{1, \cdots, l\}\\ \end{align*} where :math:`P_l^m` is the associated Legrende function and :math:`N(\alpha, l)` is the @@ -138,7 +140,7 @@ class Shell: - Contraction of Primitive Gaussians is the linear combination of primitive Gaussian functions of the same shell-type l, meant as a basis function. It has the form - .. math:: P(\cdots) \sum^M_m c_m N e^{-\alpha_m r_A^2}, + .. math:: P(\cdots) \sum^M_m c_m N e^{-\alpha_m r_A^2}, where N is the normalization constant and P is either the Cartesian polynomial for a fixed (i, j, k) or the real regular solid harmonic for a fixed (l, m). The degree is the number @@ -160,7 +162,7 @@ class Shell: coeffs: np.ndarray = attr.ib(validator=validate_shape(("exponents", 0), ("kinds", 0))) @property - def nbasis(self) -> int: # noqa: D401 + def nbasis(self) -> int: # noqa: D401 """Number of basis functions (e.g. 3 for a P shell and 4 for an SP shell).""" result = 0 for angmom, kind in zip(self.angmoms, self.kinds): @@ -173,16 +175,16 @@ def nbasis(self) -> int: # noqa: D401 return result @property - def nprim(self) -> int: # noqa: D401 + def nprim(self) -> int: # noqa: D401 """Number of primitive contracted shells, also known as the contraction length.""" return len(self.exponents) @property - def ncon(self) -> int: # noqa: D401 + def ncon(self) -> int: # noqa: D401 """Number of contractions with distinct angular momentum numbers. - This is usually 1; e.g., it would be 2 for an SP shell. - """ + This is usually 1; e.g., it would be 2 for an SP shell. + """ return len(self.angmoms) @@ -265,7 +267,7 @@ class MolecularBasis: primitive_normalization: str @property - def nbasis(self) -> int: # noqa: D401 + def nbasis(self) -> int: # noqa: D401 """Number of basis functions.""" return sum(shell.nbasis for shell in self.shells) @@ -328,7 +330,7 @@ def convert_kind(self, to: str): convert_primitive_kind : Converts primitive shells with no averaging. """ - if to != "c" and to != "p": + if to not in ("c", "p"): raise ValueError("The to string was not recognized: %s" % to) shells = [] @@ -358,7 +360,9 @@ def convert_kind(self, to: str): coeffs_new.append(coeffs) shells.append( - Shell(shell.icenter, shell.angmoms, kind_new, shell.exponents, np.array(coeffs_new).T) + Shell( + shell.icenter, shell.angmoms, kind_new, shell.exponents, np.array(coeffs_new).T + ) ) # pylint: disable=no-member return attr.evolve(self, shells=shells) @@ -366,7 +370,7 @@ def convert_kind(self, to: str): def convert_primitive_kind(angmom: int, kind: str, coeffs: np.ndarray, to: str) -> np.ndarray: r""" - Converts coefficients in Cartesian to Pure or vice-versa of a Primitive shell. + Convert coefficients in Cartesian to Pure or vice-versa of a Primitive shell. Parameters ---------- @@ -407,9 +411,9 @@ def convert_primitive_kind(angmom: int, kind: str, coeffs: np.ndarray, to: str) >> new_coeffs = convert_primitive_kind(1, "c", coeff, "p") """ - if to != "c" and to != "p": + if to not in ("p", "c"): raise ValueError("The to string was not recognized: %s" % to) - if kind != "c" and kind != "p": + if kind not in ("p", "c"): raise ValueError("The kind string was not recognized: %s" % kind) if to != kind: if kind == "c": diff --git a/iodata/test/test_basis.py b/iodata/test/test_basis.py index 997b1b58c..c4038099e 100644 --- a/iodata/test/test_basis.py +++ b/iodata/test/test_basis.py @@ -243,7 +243,7 @@ def test_get_decontracted(): ncontshells = 8 # Number of contracted shells now is equal to the number of primitives. assert_equal(len(obasis.shells), ncontshells) # Assert each shell has length one exponents - for i in range(0, ncontshells): + for _ in range(0, ncontshells): assert_equal(len(obasis.shells[0].exponents), 1) # Assert they have the right exponents. assert_equal(obasis.shells[0].exponents, 0.01) @@ -364,7 +364,7 @@ def test_convert_primitive_kind(): # Test converting D-type from kind="p" to kind="c". coeff = np.array([1., 2., 3., 4., 5.]) ccoeff = convert_primitive_kind(2, "p", coeff, "c") - desired = np.array([1.97606774, 5., 2., -2.64273441, 3., 0.66666667]) + desired = np.array([1.97606774, 5., 2., -2.64273441, 3., 0.66666667]) assert_allclose(ccoeff, desired) From 3aea62a0c25a5e2d3d2a61a39e3750902c806bc4 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Thu, 10 Sep 2020 14:09:05 -0400 Subject: [PATCH 12/14] Revert nprim code doc --- iodata/basis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iodata/basis.py b/iodata/basis.py index da6aec99f..1a707ef18 100644 --- a/iodata/basis.py +++ b/iodata/basis.py @@ -176,7 +176,7 @@ def nbasis(self) -> int: # noqa: D401 @property def nprim(self) -> int: # noqa: D401 - """Number of primitive contracted shells, also known as the contraction length.""" + """Number of primitive, also known as the contraction length.""" return len(self.exponents) @property From 5541301385672ecad84a3bf9faa575314e27c7b0 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Sun, 13 Sep 2020 09:16:55 -0400 Subject: [PATCH 13/14] Revert nprim code doc for brevity Based on Toon's suggestion. --- iodata/basis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iodata/basis.py b/iodata/basis.py index 1a707ef18..24df67578 100644 --- a/iodata/basis.py +++ b/iodata/basis.py @@ -176,7 +176,7 @@ def nbasis(self) -> int: # noqa: D401 @property def nprim(self) -> int: # noqa: D401 - """Number of primitive, also known as the contraction length.""" + """Number of primitives in the contracted shells, also known as the contraction length""" return len(self.exponents) @property From c5c906e1ebff8825027200a298e63d394480c614 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Mon, 14 Sep 2020 11:54:51 -0400 Subject: [PATCH 14/14] Revert code note on basis, mentioned to see doc Toon's suggestion to refer this to the doc instead. --- iodata/basis.py | 66 ++++--------------------------------------------- 1 file changed, 5 insertions(+), 61 deletions(-) diff --git a/iodata/basis.py b/iodata/basis.py index 24df67578..c8e81cb62 100644 --- a/iodata/basis.py +++ b/iodata/basis.py @@ -116,42 +116,7 @@ class Shell: Notes ----- - Following definitions assumes all functions are centered at A specified by attribute `icenter`. - - - Cartesian Primitive Gaussian function for a given numbers (i, j, k) and exponent - :math:`\alpha`: - - .. math:: N() x_A^i y_A^j z_A^k e^{-\alpha_i r^2} - - - Pure Primitive Gaussian functions for a given integers (l, m) and exponent :math:`\alpha`: - - .. math:: N(\alpha, l) P(r, \theta, \phi) e^{-\alpha_i r^2} - \begin{align*} - C_{0m}(r, \theta, \phi) &:= r^l P^0_{l}(\cos(\theta)) & m = 0\\ - C_{lm}(r, \theta, \phi) &:= \sqrt{2} (-1)^m \sqrt{\frac{(l - m)!}{(l + m)!}} - r^l P^m_l(\cos(\theta)) \cos(m \phi) & m \in \{1, \cdots, l\} \\ - S_{lm}(r, \theta, \phi) &:= \sqrt{2} (-1)^m \sqrt{\frac{(l - m)!}{(l + m)!}} - r^l P^m_l(\cos(\theta)) \sin(m \phi) & m \in \{1, \cdots, l\}\\ - \end{align*} - - where :math:`P_l^m` is the associated Legrende function and :math:`N(\alpha, l)` is the - normalization constant. - - - Contraction of Primitive Gaussians is the linear combination of primitive Gaussian functions - of the same shell-type l, meant as a basis function. It has the form - - .. math:: P(\cdots) \sum^M_m c_m N e^{-\alpha_m r_A^2}, - - where N is the normalization constant and P is either the Cartesian polynomial for a fixed - (i, j, k) or the real regular solid harmonic for a fixed (l, m). The degree is the number - of primitive used. - - - Contracted Shell with exponents :math:`(\alpha_1, \cdots, \alpha_{M})` is the set of all - contractions whose primitives have the exponents - :math:`(\alpha_1, \cdots, \alpha_{M})` in that order. - - - Primitive contracted shell with exponent :math:`\alpha` is the set of all contractions - in a contracted shell that have the exponent :math:`\alpha`. + Basis set conventions and terminology are documented in :ref:`basis_conventions`. """ @@ -247,18 +212,7 @@ class MolecularBasis: Notes ----- - - Primitive Contracted Shell with exponent :math:`\alpha`: - Set of all contractions in a contracted shell that have the exponent :math:`\alpha`. - Since there is a single exponent, then the degree of these contractions is one. - - - Segmented Molecular Basis: - Molecular basis where each contracted shell has contractions that all correspond to the - same total angular momentum number. There could only be the same shell-type inside a - contracted shell. There could only be one kind of shell-type inside a contracted shell. - - - Decontracted Molecular Basis: - Segmented molecular basis where each contracted shell is a primitive contracted shell with - its single exponent. + Basis set conventions and terminology are documented in :ref:`basis_conventions`. """ @@ -272,11 +226,7 @@ def nbasis(self) -> int: # noqa: D401 return sum(shell.nbasis for shell in self.shells) def get_segmented(self): - """Convert Generalized Molecular Basis to Segmented Molecular Basis. - - Segmented Molecular basis is a Molecular basis where all contracted shell in it share - a specific total angular momentum number. - """ + """Convert Generalized Molecular Basis to Segmented Molecular Basis.""" shells = [] for shell in self.shells: for angmom, kind, coeffs in zip(shell.angmoms, shell.kinds, shell.coeffs.T): @@ -286,13 +236,7 @@ def get_segmented(self): return attr.evolve(self, shells=shells) def get_decontracted(self): - r""" - Convert to Decontracted Molecular Basis from a Molecular Basis. - - Decontracted Molecular basis is a Molecular basis where each contracted shell is a - primitive contracted shell (ie contracted shell with only one exponent and one kind). - - """ + r"""Convert to Decontracted Molecular Basis from a Molecular Basis.""" shells = [] for shell in self.shells: for i, (angmom, kind) in enumerate(zip(shell.angmoms, shell.kinds)): @@ -327,7 +271,7 @@ def convert_kind(self, to: str): See Also -------- - convert_primitive_kind : Converts primitive shells with no averaging. + convert_primitive_kind : Converts primitive shells without averaging. """ if to not in ("c", "p"):