Introduction

With HQS NMR you can calculate and analyze NMR spectra for molecules or other spin systems. HQS NMR offers different solvers to obtain NMR spectra. The tool is a Python-based library with a cloud frontend and python API and uses RUST and C++ components for performance critical calculations.

If you are interested in the theory behind NMR or the math of how to calculate NMR spectra, take a look at the sections NMR and Math.

Getting started

If you want to get started right away, you can use the cloud service by visiting the HQSpectrum page on the HQStage website or interface the HQS NMR package directly via Python. To use the python API follow the installation steps outlined here and check out the examples, which will give you a quick introduction to the package. You can find information on how to download and run the examples in the basic usage section.

Background

This chapter provides an overview of the fundamentals behind the HQS NMR tool.

In section NMR we describe the basics of nuclear magnetic resonance (NMR) from experimental and theoretical perspective. We discuss for example the different relevant parameters entering an NMR calculation such as the shifts or couplings between nuclear spins and introduce the relevant Hamiltonian of a molecule for NMR.

In Math we describe in greater detail the math behind calculation of spectra in the HQS NMR Tool.

NMR

Nuclear magnetic resonance (NMR) spectroscopy is one of the most important analytical techniques in chemistry and related fields. It is widely used to identify molecules, but also to obtain information about their structure, dynamics, and chemical environment. Some fundamental aspects of NMR are summarized below.

NMR spectrometers place the sample into a strong, but constant magnetic field and use a weak, oscillating magnetic field to perturb the nuclei. At or near resonance, when the oscillation frequency matches the intrinsic frequency of a nucleus, the system responds by producing an electromagnetic signal with a frequency characteristic of the magnetic field at the respective nucleus.

Zeeman interaction

For the simulation of NMR spectra for molecules, the most important part is the Zeeman effect describing the interaction of the nuclear spin with the external magnetic field . The corresponding Hamiltonian can be written as

where is the gyromagnetic ratio, the ratio of a system's magnetic moment to its angular momentum, and is the total spin operator. Assuming that the strong (and constant) magnetic field is in -direction only, meaning , the Hamiltonian simplifies to

where is the so-called Larmor frequency, the angular frequency corresponding to the precession of the spin magnetization around the magnetic field at the position of the nucleus.

As an example, consider the simplest nucleus, H, consisting of only one proton, for which the gyromagnetic ratio is MHz T, meaning that a 500 MHz NMR spectrometer has a static magnetic field of about 11.7 Tesla. The energy of radiation of the Larmor frequency MHz ( J) is several orders of magnitude smaller than the average thermal energy of a molecule at a temperature of K ( J). Therefore, the occupations of the spin states are almost equal at room temperature, only a small surplus is responsible for the sample magnetization.

Chemical shift

Perhaps the most important aspect for NMR spectroscopy in chemistry is that the nuclei in molecules are shielded against the external magnetic field by the electrons surrounding them. This can be expressed by adding a correction term to the Hamiltonian as

where is referred to as the shielding tensor quantifying the change in the local magnetic field experienced by the nucleus in the molecule relative to a bare nucleus in vacuum. However, if the molecules of interest are in solution, or in liquid phase in general, they can rotate freely and only the isotropic chemical shift is of interest,

In practice, chemical shifts are normally used instead of chemical shieldings: instead of invoking the Larmor frequency of a nucleus in vacuum, shifts are defined with respect to the resonance frequency of a reference compound:

The standard reference for H-NMR is the Larmor frequency of the protons in TMS [tetramethylsilane, Si(CH)]. Chemical shifts are normally reported on a scale of ppm (parts per million): most H chemical shifts are observed in the range between 0 and 10 ppm, and most C chemical shifts between 0 and 200 ppm. Since the scale of chemical shieldings is so small in absolute terms , for practical intents and purposes the chemical shift can be substituted directly into the Hamiltonian:

Spin-spin coupling

Up to this point, the nuclear spins have been regarded to be isolated from each other. However, their magnetic moments have an effect on neighboring spins. The interaction of the nuclear spins can happen through two different mechanisms. The first one is the direct (or through space) spin-spin coupling, where the interaction strength depends on the distance of the two nuclei and the angle of their distance vector relative to the external field. As it comes from the direct interaction of two magnetic dipoles, it is also referred to as dipolar coupling. However, the effect is generally not observable in liquid phase since the free rotation of the molecules averages over all orientations and thus results in a vanishing average coupling.

An effect observable in the NMR spectrum is indirect spin-spin coupling, which is mediated by the electrons of a chemical bond. Due to the Pauli principle, the electrons of a covalent bond always have an anti-parallel spin orientation, and one electron will be closer to one nucleus than to the other, preferring an anti-parallel orientation with the nearby nucleus. Depending on the number of electrons involved in the transmission of the interaction, either a parallel or an anti-parallel orientation of two nuclei may result in a lower energy. Importantly, this interaction does not average out in solution since it mainly depends on the electron density at the position of the nucleus and not on the orientation of the distance vector relative to the field, which is why it is also referred to as scalar coupling. Since only s-orbitals have a finite electron density at the nucleus, the coupling depends on the electron density in those orbitals alone.

The interaction Hamiltonian in the case of homonuclear coupling is given by

where . In heteronuclear coupling, where the difference in Larmor frequencies is much larger in magnitude than the corresponding coupling constant, , the Hamiltonian can be written in terms of the -components only,

It should be noted that the -coupling tensor is a real matrix that depends on the molecular orientation, but in liquid phase only its isotropic part is observed due to motional averaging. Typical -coupling strengths between protons in H-NMR amount to a few Hz.

NMR spin Hamiltonian for molecules in liquid phase

The spin Hamiltonian in a static magnetic field in frequency units (rad s) is given by

where the sum runs over all nuclear spins of interest.

There are several interactions that have not been taken into account here. As already mentioned, the direct dipolar spin-spin interaction vanishes in liquids due to motional averaging. Beyond dipolar coupling, such as quadrupolar interactions, for instance, are relevant only for nuclei with spin quantum number . Furthermore, interactions with unpaired electrons need a special treatment as well. While most organic compounds are diamagnetic (closed-shell), paramagnetic NMR also exists.

Math

The hamiltonian we are using for the NMR systems is of the form

where are the gyromagnetic factors and the chemical shifts of nuclear spin , and denotes the coupling between spins and , and , with being the usual spin operators.

Within NMR we have a strong magnetic field in the -direction, and electromagnetic pulses / oscillating fields are applied to flip the spins into the plane. Since is typically of the order of 500Mhz, the pulses of 10kHz bandwidth, and the required resolution is sub 1Hz, we refrain from modelling the explicit time dependence of the pulses. Instead we model the pulses directly by calculating the spectral function, i.e., time-dependent correlations between the corresponding operators.

The spectrum measured in an NMR experiment corresponds to the spectral function calculated in the HQS NMR Tool.

Calculation of the spectral function

We calculate the spectral function as the imaginary part of the spin-spin correlation function

the operators, , contain the gyromagnetic factors for convenience, and is the real time dependence.

The contribution of an individual nuclear spin to the full NMR spectrum is obtained via

while the full NMR signale is the sum of individual contributions.

Temperature

For a typical NMR setup temperature is very large compared to the energy scales in the NMR Hamiltonian. Thus, we have to take finite or even infinite temperature into account. We define the partition sum

with the inverse temperature and arrive at

Here, the last equation is a Lehmann type spectral representation, which is suitable if the complete spectrum is known. are the eigenenergies of the spin NMR-Hamiltonian.

Resolvent formulation

In order to avoid having to determine the complete spectrum, we write the Green's function directly in operator form, where we have to introduce a convergence ensuring factor (broadening) and taking care of causality

The broadening is formally necessary to ensure the convergence of the Fourier integral. In practice it corresponds to the unknown or neglected noise, being intrinsic or the resolution limitation of the spectrometer.

Energy rediscretization

One problem that arises in the resolvent formulation is that the NMR peaks are usually very sharp. Discretizing the frequency axis with a very fine grid is computationally ineffective. We therefore perform several rounds of computing the spectral function. We start with a linear grid in frequency space and a rather large artificial broadening . We then iteratively rediscretize the frequency space in equal weight partitions of the total spectral function while reducing in each step until we reach the desired broadening. To this end we obtain a non-linear adaptive grid with accumulation points at the spectral function peaks.

Program components

This chapter provides an overview of the different components of the HQS NMR Tool python library. Molecule input is used to define parameters for NMR calculations, while different Solvers are available to do the actual calculation. All the data created can be stored in different datatypes and a convenience function is provided for quick and easy spectra calcualtions.

Input of molecular NMR parameters

Text input via a YAML file

NMR parameters for molecules can be provided in a YAML file. A brief summary of relevant YAML features is provided before proceeding to more detailed explanations and example.

  • Dictionaries in YAML are defined as key: value pairs. Most commonly, a dictionary contains one key/value pair per line:
    key 1: value 1
    key 2: value 2
    
  • value 1 is interpreted as a string, 1 is interpreted as an integer and 1.0 is interpreted as a floating-point number. To avoid problems with special characters (e.g. square brackets), strings may be enclosed in single or double quotes (they have different meanings, and single quotes should be preferred for a literal interpretation of the string).
  • Lists can be defined over multiple lines as:
    - item 1
    - item 2
    
    Lists can also be enclosed in square brackets: [item 1, item 2, item 3]. The nested list [[1, 2, 3], [4, 5, 6]] is equivalent with:
    - [1, 2, 3]
    - [4, 5, 6]
    
  • Indentation is part of the syntax: key/value pairs or list entries over multiple lines need to have the same number of leading spaces (no tabs).
  • Comments start with a hash, #.

Definition of the molecular structure

Definition using SMILES

A molecular structure needs to be provided along with its NMR parameters. The simplest way to define a structure in the input file is through its SMILES representation. This is done using the key smiles, followed by a representation of the molecule:

# Acetic acid defined using SMILES.
smiles: CC(=O)O

SMILES strings often contain square brackets [...]. In such cases, the string should be enclosed within quotes, '...', to avoid problems with the YAML parser.

Definition using a Molfile

Manual definition of increasingly large molecules using SMILES can be cumbersome. For example, the string representation of penicillin V would be:

smiles: CC1(C)S[C@@H]2[C@H](NC(=O)COc3ccccc3)C(=O)N2[C@H]1C(=O)O

Instead, it is easier to draw a graphical representation such as the one below using one of many available proprietary or open source packages.

Such structural 2D representations are commonly stored in "Molfiles". A molecule can be read from a Molfile by specifying the file name after the key molfile:

molfile: penicillin_v.mol

Note that the YAML input needs to contain either a Molfile or SMILES, but it is not possible to specify both at the same time. Both the V2000 and V3000 variants of the Molfile specification are supported in the input.

Hydrogens in the molecular structure

Common representations of molecular structure, whether as skeletal formulas or as SMILES, tend to omit hydrogens. Instead, the number of hydrogen atoms is inferred from the atomic valencies, especially those of the carbons. Any of the following three structures can be provided as a Molfile:

Where hydrogens are suppressed (not drawn out as separate atoms with a bond), their NMR parameters are specified through assignment to the respective skeletal atom. In the leftmost of the three structures shown above, it would be not possible to assign different parameters to the two protons in the CH2 group. Instead, one of the two other structures shown above could be used to specify different parameters for those protons.

The only restriction with regards to hydrogens is that any skeletal atom can be connected either to suppressed or to stand-alone hydrogens, but not to a mixture of both. Thus, the following two structures would be rejected during input parsing:

The structure to the left mixes a non-suppressed hydrogen with a suppressed "implicit" hydrogen (CH) on the carbon atom; the structure to the right mixes a non-suppressed hydrogen with a suppressed "explicit" hydrogen (NH) on the nitrogen atom.

Numbering of atoms

Atoms in the structural representation of the molecule are labelled with integers for the assignment of parameters. Indices can be counted starting from zero or from one. To avoid errors or misunderstandings, it is mandatory to specify a count from key in the input file, followed by either 0 or 1. The choice between those two options is entirely arbitrary. An example for atom counting starting from zero:

# Atom indices are 0, 1, 2, 3 in their order of appearance in the SMILES string.
smiles: CC(=O)O
count from: 0

An example for atom counting starting from one:

# Atom indices are 1, 2, 3, 4 in their order of appearance in the SMILES string.
smiles: CC(=O)O
count from: 1

The atoms in a molecule are indexed by their order of appearance in the Molfile. For example, acetamide may be represented by a Molfile with the following content:

     RDKit          2D

  4  3  0  0  0  0  0  0  0  0999 V2000
    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    1.2990    0.7500    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    1.2990    2.2500    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
    2.5981   -0.0000    0.0000 N   0  0  0  0  0  0  0  0  0  0  0  0
  1  2  1  0
  2  3  2  0
  2  4  1  0
M  END

Counting the atoms starting from 1 (count from: 1), the appropriate indices are represented in the image below.

Chemical shifts

Providing chemical shifts will be illustrated using the nitrobenzene molecule as an example. Its molecular structure is contained in the file PhNO2.mol and shown in the picture below, including a numbering of its atoms.

All chemical shifts are specified under the key shifts. Additionally, the values need to be nested under keys representing isotopes, which contain the atomic mass number and the element symbol (e.g. 1H or 13C). For each isotope, the chemical shifts are provided in pairs of an atomic index and the associated value in ppm (parts per million):

# Structure with suppressed protons.
molfile: PhNO2.mol
count from: 0
shifts:
  # chemical shifts for protons in ppm
  1H:
    1: 8.23
    2: 7.56
    3: 7.71
    4: 7.56
    5: 8.23
  # chemical shifts for carbon-13 nuclei in ppm
  13C:
    0: 148.5
    1: 123.7
    2: 129.4
    3: 134.3
    4: 129.4
    5: 123.7

To assign chemical shifts for suppressed protons (that are not provided explicitly in the skeletal structure), the indices of the respective non-hydrogen atoms are used instead. All suppressed protons connected to the same atom are assigned an identical shift value.

If a Molfile contains hydrogens as standalone atoms, the chemical shifts are assigned to those protons using their respective atom indices. This is illustrated using the file PhNO2_allH.mol. Its structure is shown below.

In this example, the 1H shifts need to be assigned to atoms 9-13. Assigning them to atoms 1-5, as in the previous example, would produce an error.

# Structure with suppressed protons.
molfile: PhNO2_allH.mol
count from: 0
shifts:
  # chemical shifts for protons in ppm
  1H:
    9: 8.23
    10: 7.56
    11: 7.71
    12: 7.56
    13: 8.23
  # chemical shifts for carbon-13 nuclei in ppm
  13C:
    0: 148.5
    1: 123.7
    2: 129.4
    3: 134.3
    4: 129.4
    5: 123.7

Indirect spin-spin coupling constants

Indirect spin-spin coupling constants are provided under the key J-couplings in the YAML file. Additionally, the coupling constant values need to be grouped together by isotopes. Keys for each combination of isotopes are combined as isotope1-isotope2: e.g., 1H-1H for coupling constants between two protons or 1H-13C for the associated heteronuclear coupling.

J-coupling constants in units of Hz for each combination of nuclei are provided as a list of lists with the following structure:

J-couplings:
  isotope1-isotope2:
    - [atom index 1, atom index 2, coupling constant in Hz]
    - [atom index 1, atom index 2, coupling constant in Hz]
    - [...]
  isotope1-isotope2:
    - [atom index 1, atom index 2, coupling constant in Hz]
    - [...]

The first atom index refers to the first isotope and the second atom index refers to the second isotope. As with shifts, values for suppressed hydrogens are assigned via the associated skeletal carbon or heteroatom. If multiple protons are connected to the same skeletal atom, they are assigned the same coupling constant. Inequivalent protons attached to the same skeletal atom, need to be specified explicitly as standalone atoms in the molecule definition, so that they can be referred to via their respective atom indices.

Examples

1H parameters for propane with SMILES input

smiles: CCC
count from: 0
shifts:
  1H:
    0: 0.9
    1: 1.3
    2: 0.9
J-couplings:
  1H-1H:
    - [0, 1, 7.26]
    - [1, 2, 7.26]

The protons at the terminal CH3 groups are assigned chemical shifts of 0.9 ppm each, and the protons of the central CH2 group a value of 1.3 ppm. J-couplings between all protons of the neighboring CH3 and CH2 groups are assigned as 7.26 Hz. While an indirect spin-spin coupling interaction exists between equivalent protons within the CH3 and CH2 groups, it is not observed in the spectrum and the associated values are left out.

1H parameters for acrylonitrile

The structure of acrylonitrile is provided as a Molfile in acrylonitrile.mol. Its depiction is shown below.

Since protons 4 and 5 are inequivalent, they are specified as standalone atoms with different parameters. In addition, hydrogen 6 is represented as a standalone atom, though suppressing it would be an equally valid choice.

molfile: acrylonitrile.mol
count from: 1
shifts:
  1H:
    4: 5.79  # H(trans)
    5: 5.97  # H(cis)
    6: 5.48  # H(gem)
J-couplings:
  1H-1H:
    - [4, 5, 0.9]
    - [4, 6, 11.8]
    - [5, 6, 17.9]

Combined 1H and 13C parameters for chloromethane

To illustrate the definition of heteronuclear coupling constants, the following example shows parameters for 13C-enriched chloromethane. The parameters include the shifts of the three protons and the 13C nucleus, as well as the coupling constants between these nuclei.

# CH3Cl with 13C, the hydrogens inside square brackets are implicit.
smiles: '[13CH3]Cl'
# C will have index 1 and Cl will have index 2
count from: 1
shifts:
  # Shifts of the three protons.
  1H:
    1: 3.05
  # Shift of carbon-13 in the molecule.
  13C:
    1: 25.6
J-couplings:
  # Coupling between the three protons (would normally not be observed).
  1H-1H:
    - [1, 1, -10.8]
  # Coupling between the three protons and the 13C atom.
  1H-13C:
    - [1, 1, 150.0]

Representation in Python data structures

In Python, a YAML file containing NMR parameters is parsed using the following function:

from hqs_nmr_parameters import read_parameters_yaml
parameters = read_parameters_yaml('input_file.yaml')

The read_parameters_yaml function can read the following keywords from a YAML file:

  • name: An optional name of the molecule.
  • shifts: Chemical shifts in format {isotope: {index: value}}.
  • j_couplings/J-couplings: J-coupling values in format {isotope1-isotope2: [[index1, index2, value], ...]}.
  • count from/count_from: Specifies whether to count atomic indices starting from zero or from one.
  • smiles: SMILES string of the molecule.
  • molfile: Path to a Molfile with the molecular structure.
  • molblock: Compressed Molfile content (not in clear text).
  • temperature: Temperature in K.
  • solvent: Name of the solvent.
  • description: Additional further description.

parameters is an instance of the Pydantic MolecularData class. It contains the following attributes:

  • name: The name of the molecule.
  • isotopes: List containing pairs of an atom index and the associated isotope.
  • shifts: List containing pairs of an atom index and the associated chemical shift.
  • j_couplings: List containing pairs of atomic indices and the associated J-coupling values. Note that atom index pairs are unique: if a value is provided for an atom pair (k, l), then no value is provided for pair (l, k).
  • structures: Contains the chemical structure representations. These can be from a SMILES string, a Molfile, or an XYZ file.
  • formula: The molecular formula of the molecule.
  • temperature: An optional temperature definition.
  • solvent: Name of the solvent. An empty string represents an unknown or undefined solvent, or the absence of a solvent.
  • description: Optional further information.
  • method_json: Stores a JSON serialization of computational method settings. An empty string indicates that the field is not applicable. Creating and interpreting the content is the responsibility of the user of the model.

For a NMR calculation we require a reduced set of parameters. The reduced set contains only the information required for a NMR calculation, so only the attributes isotopes, shifts, and j_couplings. This reduced set of parameters can be obtained using the spin_system function of MolecularData:

nmr_parameters = parameters.spin_system()

nmr_parameters is an instance of the Pydantic NMRParameters class. It is used as input for several functions to calculate an NMR spectrum.

type(parameters)      # MolecularData
type(nmr_parameters)  # NMRParameters
spectrum = calculate_spectrum(molecule_parms=nmr_parameters, frequency=400.0)

Example inputs

The hqs_nmr_parameters package comes with a set of example molecule definitions. They can be accessed via

from pprint import pprint
from hqs_nmr_parameters import examples
# a dictionary containing {molecule name: description} key-value pairs
pprint(examples.molecule_names)

will print

{'C10H7Br': '1H parameters for 2-bromonaphthalene.',
 'C10H8': '1H parameters for naphthalene.',
 'C2H3CN': '1H parameters for acrylonitrile.',
 'C2H5Cl': '1H parameters for chloroethane.',
 'C2H6': 'Fantasy parameters for ethane (to test behavior for 2 groups of 1H)',
 'C3H8': '1H parameters for propane.',
 'C6H5NO2': '1H parameters for nitrobenzene.',
 'C6H6': '1H parameters for benzene.',
 'CH3Cl': 'Methyl chloried: 1H parameters.',
 'CH3Cl_13C': 'Methyl chloride enriched with 13C: 1H and 13C parameters.',
 'CHCl3': 'Chloroform: 1H parameters.',
 'CHCl3_13C': 'Chloroform enriched with 13C: 1H and 13C parameters.'}

Full molecule definitions can be loaded via

from pprint import pprint
from hqs_nmr_parameters import examples
# obtain the parameters dictionary for acrylonitrile
parameters = examples.molecule_parameters['C2H3CN']
# print parameters
pprint(parameters.model_dump())
{'description': '',
 'formula': 'C3H3N',
 'isotopes': [(3, (1, 'H')), (4, (1, 'H')), (5, (1, 'H'))],
 'j_couplings': [((3, 4), 0.9), ((3, 5), 11.8), ((4, 5), 17.9)],
 'method_json': '',
 'name': 'C2H3CN',
 'shifts': [(3, 5.79), (4, 5.97), (5, 5.48)],
 'solvent': '',
 'structures': {'Molfile': {'atom_map': [0, 1, 2, 3, 4, 5, 6],
                            'charge': 0,
                            'content': '\n'
                                       'JME 2022-02-26 Wed Sep 07 15:54:28 '
                                       'GMT+200 2022\n'
                                       '\n'
                                       '  0  0  0  0  0  0  0  0  0  0999 '
                                       'V3000\n'
                                       'M  V30 BEGIN CTAB\n'
                                       'M  V30 COUNTS 7 6 0 0 0\n'
                                       'M  V30 BEGIN ATOM\n'
                                       'M  V30 1 C 2.4249 2.1000 0.0000 0\n'
                                       'M  V30 2 C 3.6373 1.4000 0.0000 0\n'
                                       'M  V30 3 C 1.2124 1.4000 0.0000 0\n'
                                       'M  V30 4 H 0.0000 2.1000 0.0000 0\n'
                                       'M  V30 5 H 1.2124 0.0000 0.0000 0\n'
                                       'M  V30 6 H 2.4249 3.5000 0.0000 0\n'
                                       'M  V30 7 N 4.8497 0.7000 0.0000 0\n'
                                       'M  V30 END ATOM\n'
                                       'M  V30 BEGIN BOND\n'
                                       'M  V30 1 1 1 2\n'
                                       'M  V30 2 2 1 3\n'
                                       'M  V30 3 1 3 4\n'
                                       'M  V30 4 1 3 5\n'
                                       'M  V30 5 1 1 6\n'
                                       'M  V30 6 3 2 7\n'
                                       'M  V30 END BOND\n'
                                       'M  V30 END CTAB\n'
                                       'M  END\n',
                            'representation': 'Molfile',
                            'symbols': ['C', 'C', 'C', 'H', 'H', 'H', 'N']}},
 'temperature': None}

To access the .yaml and .mol input files use the following command to see their location on the file system:

from pathlib import Path
from hqs_nmr_parameters import examples
acrylonitrile_yaml = Path(examples.__file__).parent / "parameters" / "C2H3CN.yaml"
print(acrylonitrile_yaml.read_text())

will print the .yaml input for acrylonitrile:

molfile: C2H3CN.mol
count from: 1
shifts:
  1H:
    4: 5.79  # H(trans)
    5: 5.97  # H(cis)
    6: 5.48  # H(gem)
J-couplings:
  1H-1H:
    - [4, 5, 0.9]
    - [4, 6, 11.8]
    - [5, 6, 17.9]

Datatypes

Datatypes in NMR

Molecular NMR parameters, spectra and calculation results are stored in python objects in the HQS NMR Tool. These data structures are as follows.

NMRParameters

The NMRParameters Pydantic datatype is defined in the hqs_nmr_parameters repository and holds the reduced set of molecular spin parametersrequired for a NMR calculation,

  • shifts (list[float]): List of chemical shifts in ppm for every nucleus in the system that has NMR parameters.
  • isotopes (list[Isotope]): List of isotopes for every nucleus, in the same ordering as shifts. The NamedTuple Isotope has the two attributes mass_number and symbol.
  • j_couplings (list[tuple[tuple[int, int], float]]): List containing pairs of atomic indices and the associated J-coupling values. The atomic indices refer to the ordering in the shifts and isotopes lists.

This data is accessed as attributes of the class instance:

from hqs_nmr_parameters import examples
nmr_parameters = examples.molecule_parameters["C10H7Br"].spin_system()
print(type(nmr_parameters))        # NMRParameters
print(nmr_parameters.shifts)
print(nmr_parameters.isotopes)
print(nmr_parameters.j_couplings)

NMRParameters objects are used as an input when calculating a spectrum using the calculate_spectrum method.

Spectrum

The Spectrum datatype holds a NMR spectrum comprising

  • half_width_ppm (float): Artificial broadening in ppm,

  • omegas (np.ndarray[float]): Frequencies in ppm,

  • values (np.ndarray[float]): Individual spin contributions to the spectrum. The values are a numpy array where each row holds the contribution of the corresponding spin, i.e.:

spectrum.values[0,:]

holds the contribution of the first spin (index 0). The total spectrum that would be measured experimentally can be calculated using

np.sum(spectrum.values, axis=0)

The Spectrum object is per default returned by the calculate_spectrum method used to calculate NMR spectra.

Greensfunction

The Greensfunction datatype holds the complex Green's function, of which the imaginary part corresponds to the NMR spectral function.

  • half_width_ppm (float): Artificial broadening in ppm,

  • omegas (np.ndarray[float]): Frequencies in ppm,

  • values (np.ndarray[complex]): Individual spin contributions to the Green's function. The values are a numpy array where each row holds the contribution of the corresponding spin, i.e.:

spectrum.values[0,:]

holds the contribution of the first spin (index 0). The total spectrum that would be measured experimentally can be calculated using

np.sum(spectrum.values, axis=0)

The Greensfunction object is returned by the calculate_spectrum method, if the flag calc_greens_function is set to True.

Serialization (Saving and Loading)

The datatypes described above have a common interface for data serialization. There exists a function called to_json common to all classes, that can take a class object and a file name (with or without the full path) to store the data as a JSON file. For example, if we have an object called spectrum of the data class Spectrum and want to save it as specrum_data.json:

from hqs_nmr.datatypes import to_json
to_json(spectrum, "spectrum_data.json")

There is also the inverse method from_json to load the data again. It needs as additional information the type of class the JSON file has stored, e.g.,

from hqs_nmr.datatypes import from_json, Spectrum
spectrum = from_json(Spectrum, "spectrum_data.json")

NMR spectra calculations

For most use cases the calculate_spectrum function should be sufficient to perform NMR spectra calculations. The function takes as input a NMRParameters object and the spectrometer frequency and determines automatically the spectral function. Per default it uses the spin_dependent_cluster_nmr_solver to evaluate the spectrum. It is exact up to the maximum cluster size (set per default to ten) and for larger systems still extremely accurate, while also being very fast. It returns the spectrum as a Spectrum object, which contains the broadening parameter eta, the frequencies omegas at which the spectral function was evaluated, as well as the spin dependent contributions to the spectral function stored in values.

Check out this [tutorial](link to tutorial 1) to get started right away.

There are further options that can be used to customize the function to your needs. Check out this second [tutorial](link to tutorial 2) to get an impression of some of the options listed below.

molecule_parms: datatypes.NMRParameters

The molecular isotopes, shifts and J-coupling values.

frequency: float

Spectrometer frequency for 1H. Proportional to the static magnetic field along z-axis:

gyromagnetic_ratios: Optional[dict[tuple[int, str], float]] = None

Dictionary of gyromagnetic ratios in radians per second per Tesla.

homoisotope: tuple[int, str] = (1, "H")

Symbol of the isotope to define the frequency w=gamma*field of the rotating frame. Defaults to (1, "H").

solver_str: str = "spin_dependent_cluster_nmr_solver"

Solver to be used for calculation. Options are:

  • "direct_nmr_solver"
  • "direct_nmr_solver (conserved)"
  • "cluster_nmr_solver"
  • "cluster_nmr_solver (symmetry)"
  • "spin_dependent_cluster_nmr_solver"
  • "spin_dependent_cluster_nmr_solver (symmetry)"
  • "automated_solver"

estimate_solver_str: Optional[str] = None

Solver to be used for inital spectrum estimation. If None, solver_str is used. Options: See solver_str.

sample_factor: float = 1.0

Factor to change the number of omega points. Defaults to 1.

frequency_window_ppm: Optional[tuple[int, int]] = None

Optional upper and lower bound of user defined freuency window in ppm.

solver_kwargs: Optional[Dict[str, Any]] = None

Optional arguments passed to the solver as **kwargs.

half_width: Optional[float] = None

The artificial broadening or intrinsic line width to use in ppm. If None the solver will choose a broadening based on the J-coupling values in the molecule.

calc_greens_function: bool = False

Flag that indicates whether the Green's function should be calculated rather than the spectral function.

Solver

The HQS NMR Tool comes with a variety of solvers to calculate the NMR spectrum where is the total spin weighted by the gyromagnetic factors, is the density matrix, and is a convergence factor for the Fourier transform. can be understood in terms of nuclear spins coupling to very small noise leading to a broadening of NMR peaks in the signal or as the intrinsic resolution of the spectrometer. See chapter Background for details on the general math behind all solvers and on NMR.

Typically though, one does not directly interface with a specific solver, but just use the convenience function calculate_spectrum to calculate NMR spectra.

All currently implemented solvers in the HQS NMR tool are frequency solvers and offer an identical base interface but differ by additional solver arguments.

Frequency solvers

Frequency solvers calculate the spectrum in the frequency domain and avoid the FFT that would be required for time solvers. These solvers calculate N correlation functions for given frequency points and return them as a numpy array where is the number of spins and the number of frequency points, Here, is the total spin creation operator scaled by the gyromagnetic factor . The full NMR signal is the sum of spin resolved signals,

Frequency solver interface

All frequency solvers take the following positional arguments plus additional solver-dependent options:

frequency_solver(
    calc_greens_function: bool,
    hamiltonian: SpinHamiltonianSystem,
    gyromagnetic_ratios: np.ndarray[tuple[Any,], np.dtype[np.floating]],
    omegas: np.ndarray[tuple[Any,], np.dtype[np.floating]],
    **kwargs
)

All frequency solvers return a numpy array where is the number of spins and the number of frequency points.

calc_greens_function: bool If set to True the Green's function rather than the spectrum is calculated. The spectrum can be obtained as the imaginary part of the Green's function.

hamiltonian: struqture_py.spins.SpinHamiltonianSystem The spin-Hamiltonian in the rotating frame given as a struqture.spins.SpinHamiltonianSystem. The HQS NMR Tool provides libraries to obtain the Hamiltonian from molecule inputs, see Molecule input. Arbitrary hamiltonians can be generated in python:

from struqture_py import spins

number_spins = 2
coupling = 1.0
shift = 1.0

hamiltonian = spins.SpinHamiltonianSystem(number_spins)
# Generate nearest neighbor xx + yy interaction
for i in range(number_spins - 1):
    hamiltonian.add_operator_product(spins.PauliProduct().x(i).x(i + 1), coupling)
    hamiltonian.add_operator_product(spins.PauliProduct().y(i).y(i + 1), coupling)
# Generate Zeeman terms
for i in range(number_spins ):
    hamiltonian.add_operator_product(spins.PauliProduct().z(i), shift)

gyromagnetic_ratios: np.ndarray List of gyromagnetic ratios for the spins included in the simulation. Used to obtain the operators

omegas: np.ndarray Numpy array of frequency points the spectrum should be evaluated at.

Available solvers

The HQS NMR tool comes with the following frequency solvers:

Direct solver

Introduction

Calculates transitions in a 1D NMR spectrum through a direct resolvent approach. The solver may be specified as "direct_nmr_solver" or "direct_nmr_solver (conserved)". Here, the second option allows the solver to exploit -conservation, which improves the runtime as well as the maximum system size that can be evaluated.

The method takes a struqture Hamiltonian (in rotating frame) and computes the spectral functions for the function, where corresponds to the spin operator including the gyromagnetic factors gamma, , .

The spectral function is evaluated via the resolvent representation detailed in chapter math. This is done via a brute force diagonalization of either the full Hamiltonian or each block associated with an quantum number in the conserved approach. It returns a spin-resolved array of spectral functions as a numpy array where is the number of spins and the number of frequency points.

Math

Extra Arguments

eta: float Artificial broadening.

The broadening is formally necessary to ensure the convergence of the Fourier integral. In practice it corresponds to the unknown or neglected noise, being intrinsic for the resolution limitation of the spectrometer. Note that the complete spectral function is positive, . However, the spin resolved spectral functions can be negative.

beta: float Inverse temperature

.

Due to the low energy of nuclear spin excitations the temperature in real measurements is practically infinite, i.e. . For theoretical studies or comparison to very low temperature NMR measurements one can apply finite temperatures.

threshold_matrix_elements: float

In the final evaluation a lot of matrix elements evalute to zero and hence do not need to be included, when performing the summation for each frequency. We therefore remove all matrix elements below this threshold beforehand.

Cluster solver

Introduction

Similar to the direct solver, the cluster solver calculates transitions in a 1D NMR spectrum through the resolvent approach. However, it can calculate the spectral function for much larger molecules, as it automatically divides the molecule into small clusters that are only weakly coupled and solves them as being independent. The solver can either be specified as "cluster_nmr_solver" or "cluster_nmr_solver (symmetry)". If the symmetry version is chosen, the solver is allowed to exploit local SU(2) symmetry. While this does allow to go to larger system sizes, it is not necessarily faster for small molecules, due to additional computational overhead in the evaluation.

Although this is the fastest solver for large molecules, it can give inaccurate results for spin contributions of spins at the boundary between two clusters. To improve accuracy check out the spin dependent cluster solver.

The setup is the same as for the direct solver. Meaning based on a struqture Hamiltonian input, the spectral function is computed from the correlation function , where corresponds to the spin operator including the gyromagnetic factors gamma, , .

Math

As for the direct solver the resolvent representation (see chapter math) is used. The correlation function is given as

However, it is only evaluated on each individual cluster. The clusters are specified in multiple steps:

Partitioning

First, a partitioning of the system into strictly independent parts (from now on called partitions) is performed. This is achieved by diagonalizing the Laplacian matrix based on the J-coupling matrix of the Hamiltonian. By counting the number of zero eigenvalues, the number of partitions in the system can be easily determined. To identify them explicitly, one can use an analysis of the Fiedler vector, which is normally defined as the eigenvector corresponding to the second lowest eigenvalue. In an ideal case a partition is then given by the zero entries in the vector leading to a bipartitioning of the system. By recursively applying this method on each of the two identified partitions, one can split the system into all of its individual parts. However, special care has to be taken as there can be no zeros in the Fiedler vector although the number of zero eigenvalues is larger than one. This can happen as the Fiedler vector is actually not well defined if the zero eigenvalues, which are also the lowest eigenvalues, are degenerate. In these cases a linear combination of two eigenvectors with zero eigenvalues can be created such that at least one entry of the resulting vector is zero. This vector can then be used as the Fiedler vector instead. After performing the partitioning, each partition is treated independently, however since these are truly non interacting parts of the system, no approximation was made here.

Symmetry Considerations

As a next stage, if the symmetry option was specified, symmetric groups are identified in each partition. A symmetric group is defined as a group of spins, where each individual spin has the same chemical shift and couples in the exact same way to the rest of the system. Identifying these groups is advantageous, as one can combine them into higher order spin representations. Exploiting the SU(2) symmetry then leads to a significant reduction in computational space. As an example consider propane, which has eight hydrogens. By identifying all symmetrically coupled groups in this molecule, the number of spins can be reduced to two. One being the CH2 group which has a combined spin representation of one and the second being the two methyl groups each representing a spin 3/2 and adding up to a total spin representation of spin 3.

Clustering

Once all of the groups in a partition have been identified, a final clustering of the partition into its weakly coupling parts can be performed. Note that up to here everything was still exact and approximations are only introduced through clustering. To perform the clustering lower triangular matrices are created, representing each partition as a weighted graph with the spins being the nodes and the edge weights being based on assumptions from perturbation theory:

Where are the entries in the J coupling matrix connecting sites x and y, the gyromagnetic ratio, the chemical shift and the magnetic field strength. After defining this graph, a Stör-Wagner splitting of the graph is used to obtain individual clusters. Note that the groups which were previously identified in the partition are always preserved by this splitting as they are treated as a single spin.

Extra Arguments

eta: float Artificial broadening.

The broadening is formally necessary to ensure the convergence of the Fourier integral. In practice it corresponds to the unknown or neglected noise, being intrinsic for the resolution limitation of the spectrometer. Note that the complete spectral function is positive, . However, the spin resolved spectral functions can be negative.

beta: float Inverse temperature

.

Due to the low energy of nuclear spin excitations the temperature in real measurements is practically infinite, i.e. . For theoretical studies or comparison to very low temperature NMR measurements one can apply finite temperatures.

Note: Currently only the case is implemented.

max_cluster_size: int Maximum cluster size.

This has to be set according to restraints imposed by the computational resources available. Note that symmetric groups are viewed as one site, with an effective number of sites corresponding to the spin representation of the group.

tolerance_couplings: float Tolerance for the J-coupling when identifying symmetry groups

When the symmetry groups are identified, the coupling to the outside does not have to be perfectly symmetric, as there might be small deviations in the input parameters. Hence this parameter allows to define a tolerance for the J-coupling until which spins are assumed to couple symmetric.

tolerance_shifts: float Tolerance for the chemical shifts when identifying symmetry groups

Similarly to the tolerance for the J-couplings the chemical shifts don't have to be perfectly identical. This parameter allows in the same way to define a tolerance until which they are assumed to be the same.

delta: float

Small parameter to avoid division by zero, when creating the weight matrix for the clustering.

threshold_matrix_elements: float

In the final evaluation a lot of matrix elements evalute to zero and hence do not need to be included, when performing the summation for each frequency. We therefore remove all matrix elements below this threshold beforehand.

verbose: int Verbosity of output

Specifically when set to one, the partitioning, as well as the identified groups and the clustering is printed. For further debug information it may be set higher.

Spin-dependent cluster solver

Introduction

The solver can either be specified as "spin_dependent_cluster_nmr_solver" or "spin_dependent_cluster_nmr_solver (symmetry)". If the symmetry version is used, the solver is allowed to exploit local SU(2) symmetry. We will refer to the cluster_nmr_solver for details, as the basic solver setup is very similar. The main difference is that the solver does not just split the molecule into independent parts, but identifies for each spin the spins that are most strongly coupling to it and groups them in one cluster, which is solved to find the individual spin contributions.

Although this is the most accurate approximate solver for large molecules, it has a slightly larger runtime as the cluster_nmr_solver. The gain in accuracy though typically justifies the small computational overhead, especially since often cluster sizes can be chosen smaller.

The setup is the same as for the direct solver. That means that based on a struqture Hamiltonian input, the spectral function is computed from the correlation function , where corresponds to the spin operator including the gyromagnetic factors gamma, , .

Math

As for the direct and cluster solver the resolvent representation (see chapter math) is used. The correlation function is given as

However, for each spin the evaluation is restricted to a spin-dependent cluster, which is identified in multiple steps:

Partitioning

First, a partitioning of the system into strictly independent parts (from now on called partitions) is performed. For details you may check here.

Symmetry Considerations

As a next stage, if the symmetry option was specified, symmetric groups are identified in each partition. Again details can be found here.

Clustering

To perform the clustering, we define as in the cluster solver lower triangular matrices for each partition representing a weighted graph with the spins being the nodes and the edge weights based on assumptions from perturbation theory:

Here, are the entries in the J-coupling matrix connecting sites x and y, the gyromagnetic ratio, the chemical shift and the magnetic field strength. We now use this graph to identify for each spin a cluster of the most strongly coupled spins. After having identified them for each spin we additionally check, whether multiple spins are associated with the same cluster and if so only evaluate such a cluster once.

Extra Arguments

eta: float Artificial broadening.

The broadening is formally necessary to ensure the convergence of the Fourier integral. In practice it corresponds to the unknown or neglected noise, being intrinsic for the resolution limitation of the spectrometer. Note that the complete spectral function is positive, . However, the spin resolved spectral functions can be negative.

beta: float Inverse temperature.

.

Due to the low energy of nuclear spin excitations the temperature in real measurements is practically infinite, i.e. . For theoretical studies or comparison to very low temperature NMR measurements one can apply finite temperatures.

Note: Currently only the case is implemented.

max_cluster_size: int Maximum cluster size.

This has to be set according to restraints imposed by the computational resources available. Note that symmetric groups are viewed as one site, with an effective number of sites corresponding to the spin representation of the group.

tolerance_couplings: float Tolerance for the J-coupling when identifying symmetry groups.

When the symmetry groups are identified, the coupling to the outside does not have to be perfectly symmetric, as there might be small deviations in the input parameters. Hence this parameter allows to define a tolerance for the J-coupling until which spins are assumed to couple symmetric.

tolerance_shifts: float Tolerance for the chemical shifts when identifying symmetry groups.

Similarly to the tolerance for the J-couplings the chemical shifts don't have to be perfectly identical. This parameter allows in the same way to define a tolerance until which they are assumed to be the same.

delta: float

Small parameter to avoid division by zero, when creating the weight matrix for the clustering.

threshold_matrix_elements: float

In the final evaluation a lot of matrix elements evaluate to zero and hence do not need to be included, when performing the summation for each frequency. We therefore remove all matrix elements below this threshold beforehand.

verbose: int Verbosity of output

Specifically when set to one, the partitioning, as well as the identified groups and the clustering is printed. For further debug information it may be set higher.

Automated solver

Introduction

The automated solver is based on the other frequency solvers, but operates in multiple steps trying to identify the most optimal route to evaluate a NMR spectrum under computational constraints. It can be used as a black box solver and should be used per default. Also, it allows the evaluation of the exact spectrum without any approximations by setting the flag calc_exact=True. The advantage is, that it still exploits local SU(2) symmetry if it gives an improvement in runtime (see below for details).

The setup is the same as for the direct solver. Meaning based on a struqture Hamiltonian input the spectral function is computed from the correlation function , where corresponds to the spin operator including the gyromagnetic factors gamma, , .

Math

As for all of the frequency solvers the resolvent representation (see chapter math) is used. The correlation function is given as

The solver first checks weather the number of spins in the molecule is smaller than some maximum number of spins. If so, it evaluates the spectrum using a direct approach. If not, it performs a partitioning and checks weather the individual partitions are smaller than the specified maximum number of spins. If so, it solves them again using the direct approach. If not, it identifies the groups in the partition and checks if it can solve the partition with a reasonable runtime exploiting local SU(2) symmetry. If also this does not work, it automatically performs a spin-dependent clustering. For each cluster we check additionally if we can gain any accuracy by exploiting the local SU2 symmetry and still have a reasonable runtime.

For details on the partitioning and exploitation of local SU(2) symmetry, check out the documentation of the cluster solver and for the spin-dependent clustering, check out that of the spin-dependent cluster solver.

Runtime estimation

When evaluating the spectral function, diagonalizations have to be performed for each block in the Hamiltonian. The difference between using the local SU(2) symmetry or not is just the number and sizes of the identified blocks. While identifying more blocks automatically means smaller block sizes, performing the diagonalization in each block leads to a computational overhead. Since each diagonalization scales roughly with , where is the dimension of the hamiltonian block, a cost factor can be estimated by iterating over all identified blocks and adding up the cost factors. Doing this for the approaches with and without local SU(2) symmetry allows us to check whether it should be exploited or not.

Extra Arguments

eta: float Artificial broadening.

The broadening is formally necessary to ensure the convergence of the Fourier integral. In practice it corresponds to the unknown or neglected noise, being intrinsic for the resolution limitation of the spectrometer. Note that the complete spectral function is positive, . However, the spin resolved spectral functions can be negative.

beta: float Inverse temperature.

.

Due to the low energy of nuclear spin excitations the temperature in real measurements is practically infinite, i.e. . For theoretical studies or comparison to very low temperature NMR measurements one can apply finite temperatures.

Note: Currently only the case is implemented.

max_cluster_size: int Maximum cluster size.

This has to be set according to restraints imposed by the computational resources available. Note that symmetric groups are viewed as one site, with an effective number of sites corresponding to the spin representation of the group.

tolerance_couplings: float Tolerance for the J-coupling when identifying symmetry groups.

When the symmetry groups are identified, the coupling to the outside does not have to be perfectly symmetric, as there might be small deviations in the input parameters. Hence this parameter allows to define a tolerance for the J-coupling until which spins are assumed to couple symmetric.

tolerance_shifts: float Tolerance for the chemical shifts when identifying symmetry groups.

Similarly to the tolerance for the J-couplings the chemical shifts don't have to be perfectly identical. This parameter allows in the same way to define a tolerance until which they are assumed to be the same.

delta: float

Small parameter to avoid division by zero, when creating the weight matrix for the clustering.

threshold_matrix_elements: float

In the final evaluation a lot of matrix elements evaluate to zero and hence do not need to be included, when performing the summation for each frequency. We therefore remove all matrix elements below this threshold beforehand.

calc_exact: bool

If you set to True the solver performs an exact calculation, but uses the direct approach, or the one exploiting the local SU2 symmetry depending on a comparision of the estimated runtime of both approaches.

verbose: int Verbosity of output.

Specifically when set to one, the partitioning, as well as the identified groups and the clustering is printed. For further debug information it may be set higher.