Coverage for cclib/method/nuclear.py : 87%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2#
3# Copyright (c) 2020, the cclib development team
4#
5# This file is part of cclib (http://cclib.github.io) and is distributed under
6# the terms of the BSD 3-Clause License.
8"""Calculate properties of nuclei based on data parsed by cclib."""
10import logging
12import numpy as np
14from cclib.method.calculationmethod import Method
15from cclib.parser.utils import PeriodicTable
16from cclib.parser.utils import find_package
18_found_periodictable = find_package("periodictable")
19if _found_periodictable:
20 import periodictable as pt
22_found_scipy = find_package("scipy")
23if _found_scipy:
24 import scipy.constants
27def _check_periodictable(found_periodictable):
28 if not _found_periodictable:
29 raise ImportError("You must install `periodictable` to use this function")
32def _check_scipy(found_scipy):
33 if not _found_scipy:
34 raise ImportError("You must install `scipy` to use this function")
37def get_most_abundant_isotope(element):
38 """Given a `periodictable` element, return the most abundant
39 isotope.
40 """
41 most_abundant_isotope = element.isotopes[0]
42 abundance = 0
43 for iso in element:
44 if iso.abundance > abundance:
45 most_abundant_isotope = iso
46 abundance = iso.abundance
47 return most_abundant_isotope
50def get_isotopic_masses(charges):
51 """Return the masses for the given nuclei, respresented by their
52 nuclear charges.
53 """
54 _check_periodictable(_found_periodictable)
55 masses = []
56 for charge in charges:
57 el = pt.elements[charge]
58 isotope = get_most_abundant_isotope(el)
59 mass = isotope.mass
60 masses.append(mass)
61 return np.array(masses)
64class Nuclear(Method):
65 """A container for methods pertaining to atomic nuclei."""
67 def __init__(self, data, progress=None, loglevel=logging.INFO, logname="Log"):
69 self.required_attrs = ('natom','atomcoords','atomnos','charge')
71 super(Nuclear, self).__init__(data, progress, loglevel, logname)
73 def __str__(self):
74 """Return a string representation of the object."""
75 return "Nuclear"
77 def __repr__(self):
78 """Return a representation of the object."""
79 return "Nuclear"
81 def stoichiometry(self):
82 """Return the stoichemistry of the object according to the Hill system"""
83 cclib_pt = PeriodicTable()
84 elements = [cclib_pt.element[ano] for ano in self.data.atomnos]
85 counts = {el: elements.count(el) for el in set(elements)}
87 formula = ""
88 elcount = lambda el, c: "%s%i" % (el, c) if c > 1 else el
89 if 'C' in elements:
90 formula += elcount('C', counts['C'])
91 counts.pop('C')
92 if 'H' in elements:
93 formula += elcount('H', counts['H'])
94 counts.pop('H')
95 for el, c in sorted(counts.items()):
96 formula += elcount(el, c)
98 if getattr(self.data, 'charge', 0):
99 magnitude = abs(self.data.charge)
100 sign = "+" if self.data.charge > 0 else "-"
101 formula += "(%s%i)" % (sign, magnitude)
102 return formula
104 def repulsion_energy(self, atomcoords_index=-1):
105 """Return the nuclear repulsion energy."""
106 nre = 0.0
107 for i in range(self.data.natom):
108 ri = self.data.atomcoords[atomcoords_index][i]
109 zi = self.data.atomnos[i]
110 for j in range(i+1, self.data.natom):
111 rj = self.data.atomcoords[0][j]
112 zj = self.data.atomnos[j]
113 d = np.linalg.norm(ri-rj)
114 nre += zi*zj/d
115 return nre
117 def center_of_mass(self, atomcoords_index=-1):
118 """Return the center of mass."""
119 charges = self.data.atomnos
120 coords = self.data.atomcoords[atomcoords_index]
121 masses = get_isotopic_masses(charges)
123 mwc = coords * masses[:, np.newaxis]
124 numerator = np.sum(mwc, axis=0)
125 denominator = np.sum(masses)
127 return numerator / denominator
129 def moment_of_inertia_tensor(self, atomcoords_index=-1):
130 """Return the moment of inertia tensor."""
131 charges = self.data.atomnos
132 coords = self.data.atomcoords[atomcoords_index]
133 masses = get_isotopic_masses(charges)
135 moi_tensor = np.empty((3, 3))
137 moi_tensor[0][0] = np.sum(masses * (coords[:, 1]**2 + coords[:, 2]**2))
138 moi_tensor[1][1] = np.sum(masses * (coords[:, 0]**2 + coords[:, 2]**2))
139 moi_tensor[2][2] = np.sum(masses * (coords[:, 0]**2 + coords[:, 1]**2))
141 moi_tensor[0][1] = np.sum(masses * coords[:, 0] * coords[:, 1])
142 moi_tensor[0][2] = np.sum(masses * coords[:, 0] * coords[:, 2])
143 moi_tensor[1][2] = np.sum(masses * coords[:, 1] * coords[:, 2])
145 moi_tensor[1][0] = moi_tensor[0][1]
146 moi_tensor[2][0] = moi_tensor[0][2]
147 moi_tensor[2][1] = moi_tensor[1][2]
149 return moi_tensor
151 def principal_moments_of_inertia(self, units='amu_bohr_2'):
152 """Return the principal moments of inertia in 3 kinds of units:
153 1. [amu][bohr]^2
154 2. [amu][angstrom]^2
155 3. [g][cm]^2
156 and the principal axes.
157 """
158 choices = ('amu_bohr_2', 'amu_angstrom_2', 'g_cm_2')
159 units = units.lower()
160 if units not in choices:
161 raise ValueError("Invalid units, pick one of {}".format(choices))
162 moi_tensor = self.moment_of_inertia_tensor()
163 principal_moments, principal_axes = np.linalg.eigh(moi_tensor)
164 if units == "amu_bohr_2":
165 _check_scipy(_found_scipy)
166 bohr2ang = scipy.constants.value("atomic unit of length") / scipy.constants.angstrom
167 conv = 1 / bohr2ang ** 2
168 elif units == "amu_angstrom_2":
169 conv = 1
170 elif units == "g_cm_2":
171 _check_scipy(_found_scipy)
172 amu2g = scipy.constants.value("unified atomic mass unit") * scipy.constants.kilo
173 conv = amu2g * (scipy.constants.angstrom / scipy.constants.centi) ** 2
174 return conv * principal_moments, principal_axes
176 def rotational_constants(self, units='ghz'):
177 """Compute the rotational constants in 1/cm or GHz."""
178 choices = ('invcm', 'ghz')
179 units = units.lower()
180 if units not in choices:
181 raise ValueError("Invalid units, pick one of {}".format(choices))
182 principal_moments = self.principal_moments_of_inertia("amu_angstrom_2")[0]
183 _check_scipy(_found_scipy)
184 bohr2ang = scipy.constants.value('atomic unit of length') / scipy.constants.angstrom
185 xfamu = 1 / scipy.constants.value('electron mass in u')
186 xthz = scipy.constants.value('hartree-hertz relationship')
187 rotghz = xthz * (bohr2ang ** 2) / (2 * xfamu * scipy.constants.giga)
188 if units == 'ghz':
189 conv = rotghz
190 if units == 'invcm':
191 ghz2invcm = scipy.constants.giga * scipy.constants.centi / scipy.constants.c
192 conv = rotghz * ghz2invcm
193 return conv / principal_moments
196del find_package