Skip to main content

Classes for reading and manipulating molecular quadrupole moments.

Project description

Quadrupole Analysis

Python functions for taking molecular quadrupole tensors and converting to forms for comparison to literature.

Inertia Tensor and Eigenvectors

The inertia tensor $\textbf{I}$ of a molecule with its center of mass at the origin is given as

    \textbf{I} = \sum_j \begin{bmatrix}
        m_j \left( y^2_j+z^2_j \right) & -m_j x_j y_j                   & -m_j x_j z_j \\
        -m_j y_j x_j                   & m_j \left( x^2_j+z^2_j \right) & -m_j y_j z_j \\
        -m_j z_j x_j                   & -m_j z_j y_j                   & m_j \left( x^2_j+y^2_j \right)
    \end{bmatrix}

with the index $j$ running over all atoms and $m$ being their mass. For samples with standard isotopic distributions the masses are the average atomic masses and can be accessed by the function get_atomic_mass(). Due to the transposition symmetry of the inertia tensor ($\textbf{I}_{\alpha\beta} = \textbf{I}_{\beta\alpha}$), one need only calculate the upper right (or lower left) triangular portion of the tensor, simplifying the calculations to

    \begin{align*}
        \textbf{I}_{xx} &= \sum_j^N m_j \left( y'^2_j+z'^2_j \right) \\
        \textbf{I}_{yy} &= \sum_j^N m_j \left( x'^2_j+z'^2_j \right) \\
        \textbf{I}_{zz} &= \sum_j^N m_j \left( x'^2_j+y'^2_j \right) \\
        \textbf{I}_{xy} &= \textbf{I}_{yx} = -\sum_j^N m_j x'_j y'_j \\
        \textbf{I}_{xz} &= \textbf{I}_{zx} = -\sum_j^N m_j x'_j z'_j \\
        \textbf{I}_{yz} &= \textbf{I}_{zy} = -\sum_j^N m_j y'_j z'_j \\
    \end{align*}

and for a system with a center of mass $\textbf{R}_\alpha = (\textbf{R}_x,\quad \textbf{R}_y,\quad \textbf{R}_z)$ given by

    \textbf{R}_\alpha = \frac{1}{M}\sum_{j} m_j * \textbf{r}_j;\quad M = \sum_{j} m_j

that is not at the origin, set $(x'_j,\quad y'_j,\quad z'_j) = (x_j-\textbf{R}_x,\quad y_j-\textbf{R}_y,\quad z_j-\textbf{R}_z)$.

Calculating the center of mass in Python requires a simple loop over all atoms. Here I have used the Geometry class which, when used as an iterator, yields individual members of the Atom class which have two attributes, the atomic symbol (atom.element) and the XYZ coordinates (atom.xyz), making our code into


center_of_mass = np.zeros(3, dtype=float)
total_mass = 0.
for atom in geometry:
    mass = get_atomic_mass(atom.element)
    center_of_mass += atom.xyz * mass
    total_mass += mass
center_of_mass = center_of_mass / total_mass

Then, initialize an array of zeros for the inertia matrix and begin iterating through the atoms in the geometry once again,


inertia_matrix = np.zeros((3, 3), dtype=float)

for atom in geometry:
    mass = get_atomic_mass(atom.element)
    x = atom.xyz[0] - center_of_mass[0]
    y = atom.xyz[1] - center_of_mass[1]
    z = atom.xyz[2] - center_of_mass[2]

    xx = mass * (y**2 + z**2)
    yy = mass * (x**2 + z**2)
    zz = mass * (x**2 + y**2)

    xy = mass * (x * y)
    xz = mass * (x * z)
    yz = mass * (y * z)

    inertia_matrix[0,0] += xx
    inertia_matrix[1,1] += yy
    inertia_matrix[2,2] += zz

    inertia_matrix[0,1] += -xy
    inertia_matrix[1,0] += -xy

    inertia_matrix[0,2] += -xz
    inertia_matrix[2,0] += -xz

    inertia_matrix[1,2] += -yz
    inertia_matrix[2,1] += -yz

Finally, to get the desired eigenvectors for our rotation matrix, use the NumPy function numpy.linalg.eig(), and return the output


eigenvalues, eigenvectors = np.linalg.eig(inertia_matrix)

return eigenvalues, eigenvectors

Currently the eigenvalues are not used further in the code, but are returned nonetheless to make the code more easily extensible.

Detracing Operation

The quadrupole tensor is a 3x3 matrix with transposition symmetry. These can be calculated as

    \Theta_{\alpha\beta} = \sum_i e_i\textbf{r}_{i_\alpha}\textbf{r}_{i_\beta}

There have been many papers arguing the form of this matrix, however due to the prevalence of the traceless quadrupole moment in experimental measurements of the quadrupole tensor, I have opted to include a method which can apply a detracing operation to an otherwise normal quadrupole matrix. The function detrace_quadrupole() performs the following operation

    \mathbb{A}_{traceless} = \frac{3}{2}\left( \mathbb{A} - \mathbb{I}\frac{tr(\mathbb{A})}{3} \right)

with the code


def detrace_quadrupole(quadrupole: npt.NDArray):
    return (3 / 2) * (quadrupole - (np.eye(3,3) * (np.trace(quadrupole) / 3)))

with the standard factor of 3/2 for the detracing operation. See the associated docstring for detrace_quadrupole() for links to papers discussing the traceless quadrupole and for notes regarding detracing in ORCA and Quantum ESPRESSO.

Comparing Quadrupoles

There is a significant challenge when it comes to comparing literature quadrupoles to calculated quadrupoles and that is the arbitrary alignment of the molecular coordinates. It is, in theory, possible to find the proper rotation of a molecule such that the quadrupole moment aligns with experiment, however as there is often little to no widespread agreement on how a molecule should be aligned this can be an arduous task. Similarly, multiple sources may use the same experimental data but alter the alignment of the molecule's quadrupole moment.

For example, consider the following diagram from a paper on the molecular Zeeman effect (Table 4, Journal page 246, PDF page 24):

Reference Axes Reference Data for Water

Clearly the authors have aligned the H2O molecule to be in the XY plane, with the oxygen pointing in the +X direction. If this alignment is used in a calculation (see water_aligned.out):

Alignment of water molecule in XY plane

the quadrupole moment (at the ωB97M-V/def2-QZVPPD level of theory) is [-0.15, 2.59, -2.44]. Clearly, when comparing this to the literature value of [-0.13, 2.63, -2.50], the product is a sensible difference of [-0.02, -0.04, 0.06]. However, if instead the calculation is run using a rotation that aligns the molecule in the XZ plane (see water_xz_plane.out):

Alignment of water molecule in XZ plane

the exact same method for acquiring the quadrupole moment would yield [2.59, -2.44, -0.15], which provides a difference of [2.72, -5.07, -2.35] when compared to the literature. One may attempt to align the largest inertial axis with the Z axis, as is occasionally suggested in the literature, however there are no rules for how one may align the remaining inertial axes. Therefore without visually inspecting the paper's diagram (should it exist), there is no a priori way to guarantee the alignment of the quadrupole moment.

It is for this reason that I have chosen to supply a function that can attempt to align the quadrupole from a calculation with the quadrupole from an experimental source. The algorithm is not guaranteed to produce the correct alignment, and could potentially introduce error into statistical analyses, however given the absence of a conclusive method for aligning the quadrupole moment it can potentially save a significant amount of time when attempting a wide-scale analysis of quadrupole moments.

The algorithm works by purely statistical reasoning, no deeper physics at play. It starts by comparing the sign of the molecular quadrupole moments, since in theory for molecules with low symmetry a rotation of 180 degrees could result in a sign flip, and if the sign of the calculated quadrupole does not match experiment it will flip the sign of the calculated quadrupole. It then takes the 6 possible permutations of the quadrupole tensor and checks which has the lowest difference from experiment. Given that there could be ties between two permutations, it takes the list and sorts by the standard deviation of each tensor, so the quadrupole that matches most closely to the experiment in both total deviation and in lowest standard deviation will be the returned values.

To demonstrate this, I ran a calculation with a water molecule arbitrarily oriented in space. The resulting diagonal components of the quadrupole tensor, after rotation into the inertial frame, were [2.59, -0.15, -2.44]. Using the function with the literature value,

>>> expt_quad = np.array([-0.13, 2.63, -2.50])
>>> calc_quad = np.array([2.59, -0.15, -2.44])
>>> best_quad, best_diff = compare_quadrupole(expt_quad, calc_quad)
>>> print(best_quad)
[-0.15, 2.59, -2.44]
>>> print(best_diff)
[-0.02, -0.04, 0.06]

and lo the quadrupole moment can indeed be matched to the expected value, albeit by statistical means rather than physical means.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

quadrupole-0.1.0.tar.gz (587.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

quadrupole-0.1.0-py3-none-any.whl (11.8 kB view details)

Uploaded Python 3

File details

Details for the file quadrupole-0.1.0.tar.gz.

File metadata

  • Download URL: quadrupole-0.1.0.tar.gz
  • Upload date:
  • Size: 587.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.9.17 {"installer":{"name":"uv","version":"0.9.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for quadrupole-0.1.0.tar.gz
Algorithm Hash digest
SHA256 55ee25fd731c216cf95a47360ade14db4de30faf9f6e03d325094a4ea8b6bf7a
MD5 bd341628d2e4dfa6b90d9cbd767d59fa
BLAKE2b-256 24d7833e321c34f5b5b579ecdb3a1f68e69efde4648fd38a9cd401479ce84e47

See more details on using hashes here.

File details

Details for the file quadrupole-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: quadrupole-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 11.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.9.17 {"installer":{"name":"uv","version":"0.9.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for quadrupole-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 dcc4c0665a279d2fed5a870822a8acdf87bd97c34417ab6e1d694543123ffab1
MD5 86b79fcfd7d8c15e3805b8a73c2c35bf
BLAKE2b-256 00ff7d7fa098ed7a8f4314beb515951df9f53d5fd2aca1f977ffbecdc442d0a5

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page