Tutorial 5a - NMR Spin Systems: Using Soprano to generate NMR spin systems and interface with simulation software#
_
/|_|\
/ / \ \
/_/ \_\
\ \ / /
\ \_/ /
\|_|/
SOPRANO: a Python library for generation, manipulation and analysis of large batches of crystalline structures
Developed within the CCP-NC project. Copyright STFC 2022
This tutorial demonstrates how to use Soprano to generate nuclear spin systems and export them to simulation software such as MRSimulator and SIMPSON.
We don’t go into how to generate the rest of the input file or actually run the spin simulations here. For that, see the MRSimulator documentation or e.g. the SimPYson package.
# Basic imports
import os, sys
sys.path.insert(0, os.path.abspath('..')) # This to add the Soprano path to the PYTHONPATH
# so we can load it without installing it
# Other useful imports
import numpy as np
from ase import io as ase_io
from ase.visualize.plot import plot_atoms
from weas_widget import WeasWidget
import ipywidgets as widgets
%matplotlib inline
import matplotlib.pyplot as plt
plt.ion()
%reload_ext autoreload
%autoreload 2
1 - Preliminaries#
For extraction of NMR-related propertied from .magres files, see the 05-nmr.ipynb tutorial. This tutorial will focus on the generation of NMR spin systems using Soprano and interfacing with simulation software.
ethanol = ase_io.read('tutorial_data/ethanol.magres')
EDIZUM = ase_io.read('tutorial_data/EDIZUM.magres')
# We can do a nice trick with Soprano to connect
# molecules across the periodic boundary (this only really
# works for organic molecular crystals):
from soprano.scripts.cli_utils import reload_as_molecular_crystal
ethanol = reload_as_molecular_crystal(ethanol)
EDIZUM = reload_as_molecular_crystal(EDIZUM)
# Visualise the EDIZUM crystal (4 molecules per unit cell)
v = WeasWidget()
v.from_ase(EDIZUM)
v
2 - Selecting the subset#
Often when exporting from a magres file to a spin system suitable for simulation, we are only interested in a subset of the spins. This can be achieved by creating a soprano selection object and passing it to the get_spin_system function. You can find more details on the selection methods in Soprano in Tutorial 3.
We will begin with some examples of useful selection approaches for spin systems.
from soprano.selection import AtomSelection
2.1 - Selecting by element, label, or index#
If you only want a subset of spins based on a single element, you can use the from_element method. This will select all sites with the specified element.
from soprano.selection import AtomSelection
# Only the Carbon atoms:
selC = AtomSelection.from_element(atoms, 'C')
Alternatively, you can use the from_selection_string method to select multiple elements at once. This method uses a string with the element symbols separated by commas.
# Only the Carbon and Oxygen atoms:
selCO = AtomSelection.from_selection_string(atoms, 'C,O')
The from_selection_string method can also be used to select subsets based on their labels or indices. For example, to select the first and third carbon atoms:
# Only the first and third Carbon atoms:
selC_1_3 = AtomSelection.from_selection_string(atoms, 'C.1,C.3')
# Only the sites labelled C1, H1a, H1b, and H1c:
selLabels = AtomSelection.from_selection_string(atoms, 'C1,H1a,H1b,H1c')
Here’s are the different selections visualised:
# Only the first molecule
from soprano.properties.linkage import Molecules
molecules = Molecules.get(EDIZUM)
first_mol = molecules[0].subset(EDIZUM)
# Select the C1, C2, C3 and all H atoms:
sel = AtomSelection.from_selection_string(EDIZUM, 'N1,C1,C6,C7')
# Select all atoms except the N1, C1, C6, C7:
sel_compl = AtomSelection.all(EDIZUM) - sel
def make_viewer(atoms, label):
v = WeasWidget()
v.from_ase(atoms)
v.avr.show_bonded_atoms = True
title = widgets.HTML(f"<b style='font-size:13px'>{label}</b>")
return widgets.VBox([title, v])
grid = widgets.GridBox(
[
make_viewer(EDIZUM, "All atoms"),
make_viewer(first_mol, "First molecule"),
make_viewer(sel.subset(EDIZUM), "N1, C1, C6, C7"),
make_viewer(sel_compl.subset(EDIZUM), "All except {N1, C1, C6, C7}"),
],
layout=widgets.Layout(
grid_template_columns="repeat(2, 1fr)",
grid_gap="12px",
),
)
grid
Notice that there are four copies of that of the ones selected by the selection string ‘N1,C1,C6,C7’, corresponding to the four molecules in the unit cell.
2.2 - Selecting by distance#
You can also select sites based on their distance from a reference site. This can be useful for selecting sites within a certain range of a defect site, for example. The from_sphere method takes a reference site and a distance in Angstroms.
# All sites within 2.5 Angstroms of the first atom:
selDist = AtomSelection.from_sphere(atoms, atoms.positions[0], 2.5)
As an example, let’s select all atoms within 2.0 Angstroms of the N atom with index 138:
# One of the N atoms has index 138. Let's select all atoms within 2.0 Angstroms of it:
central_atom_index = 138
sphere_centre = EDIZUM.positions[central_atom_index]
sphere_radius = 2.0
selDisc = AtomSelection.from_sphere(EDIZUM, sphere_centre, sphere_radius)
# plot_atoms(selDisc.subset(EDIZUM))
# We can use weas_widget to show the full structure and the selected subset side by side.
# Left: full structure with a transparent sphere showing the selection region
v_full = WeasWidget()
v_full.from_ase(EDIZUM)
v_full.imp.settings = [{
"type": "sphere",
"opacity": 0.5,
"shape": {"radius": sphere_radius},
"instances": [{"position": sphere_centre, "color": "#fff70c"}],
}]
# Right: just the selected subset
v_sub = WeasWidget()
v_sub.from_ase(selDisc.subset(EDIZUM))
widgets.HBox([
widgets.VBox([widgets.HTML("<b>Full structure (sphere = selection region)</b>"), v_full]),
widgets.VBox([widgets.HTML(f"<b>Subset within {sphere_radius:.1f} Å of atom {central_atom_index}</b>"), v_sub]),
])
3 - Generating the spin system#
In Soprano, a SpinSystem is made up of lists of Site and Coupling objects. Sites represent the individual spins in the system, and Couplings represent the interactions between them. As with other parts of Soprano, there is an AtomsProperty class that can be used to generate a SpinSystem from ase.Atoms objects. However, let’s see how to create a SpinSystem from individual Site and Coupling objects first. See below for how to generate a SpinSystem from an Atoms object directly.
3.1 - Site#
A Site is a single spin in the system. It has a label, isotope/species and (optionally) magnetic shielding and electric field gradient tensors.
We can more conveniently create a list of site objects from an ase.Atoms object using the get_sites function. This function takes an ase.Atoms object and an AtomSelection object and returns a list of Site objects.
from soprano.properties.nmr import get_sites
# Example selections:
selC13 = AtomSelection.from_selection_string(EDIZUM, 'C13')
# Extract the sites
sites = get_sites(EDIZUM, selection=selC13)
print(f"Extracted {len(sites)} sites")
# Print the first site
print(sites[0])
Extracted 4 sites
Site: C13 (isotope: 13C, index = 0)
=============================================
Magnetic Shielding Tensor for C:
[[137.77746,-16.47803, 92.94306],
[-15.88779, 27.85678,-58.89048],
[ 87.90102,-59.16857, 50.16629]]
HaeberlenShielding:
sigma_iso (isotropy): 71.93351
zeta (reduced anisotropy): 134.42515
Delta (anisotropy): 201.63772
eta (asymmetry): 0.62167
Electric Field Gradient Tensor for 13C:
[[ 0.19059,-0.1067 , 0.26773],
[-0.1067 ,-0.1589 ,-0.1102 ],
[ 0.26773,-0.1102 ,-0.03169]]
Quadrupole moment: 0.0 barns
Nuclear spin: 0.5
This site is not quadrupole active.
Manually creating a Site object#
Sometimes we may want to create a Site object manually. This can be done by passing the label, isotope, and (optionally) the magnetic shielding and electric field gradient tensors. The tensors must be passed in as the relevant Soprano tensor object.
For example:
from soprano.nmr import Site, MagneticShielding, ElectricFieldGradient
# Data for the Site
isotope = '2H'
label = 'H1a'
index = 0
ms_data = np.array([
[30.29817962, 1.20510693, 3.67274493],
[ 1.96313295, 27.57652505, 2.57545224],
[ 4.21834132, 2.16271308, 30.90315252]])
efg_data = np.array([
[ 0.12793404, 0.05142987, 0.20226839],
[ 0.05142987, -0.13353175, 0.04145601],
[ 0.20226839, 0.04145601, 0.0055977 ]])
ms = MagneticShielding(ms_data, species = isotope)
efg = ElectricFieldGradient(efg_data, species=isotope)
# Create the Site object
site1 = Site(label=label, isotope=isotope, ms = ms, efg = efg, index=index)
# Printing a Site object will give a nice summary of the data
print(site1)
# We can also create a Site object from a dictionary:
site2 = Site.from_dict({
'label': label,
'isotope': isotope,
'index': index,
'ms': {
'data': ms_data,
'species': isotope
},
'efg': {
'data': efg_data,
'species': isotope
}
})
# The two Site objects are equal
print('Site1 == Site2:', site1 == site2)
Site: H1a (isotope: 2H, index = 0)
=============================================
Magnetic Shielding Tensor for 2H:
[[30.29818, 1.20511, 3.67274],
[ 1.96313,27.57653, 2.57545],
[ 4.21834, 2.16271,30.90315]]
HaeberlenShielding:
sigma_iso (isotropy): 29.59262
zeta (reduced anisotropy): 5.96101
Delta (anisotropy): 8.94151
eta (asymmetry): 0.14197
Electric Field Gradient Tensor for 2H:
[[ 0.12793, 0.05143, 0.20227],
[ 0.05143,-0.13353, 0.04146],
[ 0.20227, 0.04146, 0.0056 ]]
Quadrupole moment: 2.86 barns
Nuclear spin: 1.0
Quadrupolar constant: 193809.12016126476 Hz
Quadrupolar product: 193819.81577101676 Hz
Quadrupolar frequency: 290713.6802418971 Hz
Site1 == Site2: True
3.2 - Couplings#
A Coupling is an interaction between two sites. It has a label, a list of site indices it connects, and the coupling tensor.
There are two types of Coupling implemented in Soprano: DipolarCoupling and ISCoupling. The DipolarCoupling class represents the dipolar interaction between two sites, and the ISCoupling class represents the spin-spin or J-coupling.
3.2.1 - Dipolar Coupling#
As with the sites, we can generate a list of couplings from an ase.Atoms object using the get_dipolar_couplings function. This function takes an ase.Atoms object and (optionally) an AtomSelection object and returns a list of Coupling objects.
from soprano.properties.nmr import get_dipolar_couplings
# Example selections:
selC13 = AtomSelection.from_selection_string(EDIZUM, 'C13')
selC14 = AtomSelection.from_selection_string(EDIZUM, 'C14')
# Extract the dipolar couplings
dcouplings = get_dipolar_couplings(EDIZUM, sel_i = selC13, sel_j = selC14)
# Print the dipolar couplings
print(f"Extracted {len(dcouplings)} dipolar couplings")
print(" i - j\t dipolar couplings (Hz)")
for d in dcouplings:
print(f"{d.site_i} - {d.site_j}: {d.coupling_constant:10.2f}")
Extracted 16 dipolar couplings
i - j dipolar couplings (Hz)
124 - 130: -10.01
127 - 129: -10.01
126 - 130: -2912.52
125 - 131: -10.01
124 - 129: -118.62
125 - 128: -118.62
127 - 131: -2912.52
126 - 129: -13.89
127 - 128: -13.89
124 - 131: -13.89
126 - 128: -10.01
125 - 130: -13.89
124 - 128: -2912.52
126 - 131: -118.62
127 - 130: -118.62
125 - 129: -2912.52
Another example is if we have multiple molecules in the unit cell and want just intramolecule couplings. We can use the Molecules class to get the molecules and then use the get_dipolar_couplings function on the molecule subset. For example, to get the dipolar couplings for the first molecule in the unit cell:
# Selecting the atoms in the first molecule:
from soprano.properties.linkage import Molecules
molecules = Molecules.get(EDIZUM)
first_mol = molecules[0].subset(EDIZUM, use_cell_indices=True)
# Let's say we just want the coupling between H14 and C9:
sel_i = AtomSelection.from_selection_string(first_mol, 'H14')
sel_j = AtomSelection.from_selection_string(first_mol, 'C9')
# we can specify the isotopes for the atoms
isotopes = {'H': 2, 'C': 13}
first_mol_dipolar = get_dipolar_couplings(first_mol, sel_i, sel_j, isotopes=isotopes)
print(f"Number of couplings: {len(first_mol_dipolar)}")
print("Site_i Site_j Coupling (Hz)")
for d in first_mol_dipolar:
# print(d.site_i, d.site_j, d.coupling_constant)
# Format neatly i,j and the coupling constant
print(f"{d.site_i:5d} {d.site_j:5d} {d.coupling_constant:10.3f}")
Number of couplings: 1
Site_i Site_j Coupling (Hz)
5 13 -4932.655
Note
Note that the indices are always relative to the atoms object you’re dealing with, so in this case we created a new atoms object that was a subset of the original one, and the indices are relative to that new object.
Manually specifying dipolar couplings#
The get_dipolar_couplings function is a wrapper function around the AtomsProperty called DipolarCouplingList. You can use this directly, as with other AtomsProperties (see Tutorial 2 for information on how these work in Soprano).
If instead you work with data from other sources or need more control over the dipolar couplings, you can create the DipolarCoupling objects manually as in the below example.
from soprano.nmr.coupling import DipolarCoupling
from soprano.nmr import NMRTensor
dipolar_data = np.array([-2.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]).reshape(3, 3)
coupling = DipolarCoupling(
site_i=1,
site_j=2,
species1='1H',
species2='13C',
tensor=NMRTensor(dipolar_data, order='n'),
tag='test_tag',
)
# Show the representation of the coupling
coupling
DipolarCoupling(site_i=1, site_j=2, species1='1H', species2='13C', tensor=NMRTensor(data=[[-2. 0. 0.]
[ 0. 1. 0.]
[ 0. 0. 1.]]), type='D', tag='test_tag', gamma1=267522128.0, gamma2=67282840.0)
3.2.2 - J Coupling (Indirect Spin-Spin Coupling)#
from soprano.properties.nmr import get_j_couplings
# Example selections:
# - The first carbon atom in ethanol
sel_C1 = AtomSelection.from_selection_string(ethanol, 'C.1')
# - Atoms within a sphere around the first carbon atom with a radius of 1.5 Angstroms
# This will capture the CH3 group in ethanol (including the C).
center = ethanol.positions[sel_C1.indices[0]]
sel_sphere = AtomSelection.from_sphere(ethanol, center=center, r=1.5, periodic=True)
print(f"The selection sel_sphere contains {len(sel_sphere)} atoms.")
# Now we can extract the J couplings between the selected atoms:
jcouplings = get_j_couplings(ethanol, sel_i=sel_sphere, sel_j=sel_C1)
# Print the J couplings
print(f"Extracted {len(jcouplings)} J couplings")
print(" i - j\t\t J coupling (Hz)")
print(30 * '-')
for j in jcouplings:
print(f" {j.site_i} - {j.site_j}\t\t {j.coupling_constant:10.2f} Hz")
The selection sel_sphere contains 4 atoms.
Extracted 3 J couplings
i - j J coupling (Hz)
------------------------------
0 - 6 102.11 Hz
1 - 6 104.58 Hz
2 - 6 106.02 Hz
We can export the jcouplings to Simpson spinsys format like this:
for j in jcouplings:
print(j.to_simpson())
jcoupling 1 7 102.106331 -7.432942 0.011344 54.337641 53.157138 167.758511
jcoupling 2 7 104.581559 -7.462220 0.039285 3.823421 62.054964 258.325281
jcoupling 3 7 106.021611 -7.499649 0.016076 113.429825 54.888957 350.375301
As before, we can also create the JCoupling objects manually. The JCoupling class takes a label, a list of site indices, and the coupling tensor. The reduced coupling tensor must be passed in as a Soprano NMRTensor object.
Note that the reduced coupling tensor is that found in the magres files, in units of \(10^{19} T^2 J^{-1}\). By specifying the isotopes, the Jcoupling class will look up the gyromagnetic ratios and convert the tensor to the correct units (Hz) for the simulation software.
from soprano.nmr.coupling import JCoupling
from soprano.nmr import NMRTensor
# Note the K tensor is the reduced coupling tensor
# (i.e. the coupling tensor not scaled by the gyromagnetic ratios).
# It's in units of 10^19 T^2 J^-1
k = np.array([
[30.6826678444, -1.62975719737, -5.48015039636],
[-1.57659364137, 37.4487199089, -1.18603354744],
[-5.44203254428, -1.20427495723, 33.5914667613]
])
jcoupling = JCoupling(
site_i=0,
site_j=6,
species1='1H',
species2='13C',
tensor=NMRTensor(k),
tag='test_tag',
)
print("J-coupling in Simpson format:")
print(jcoupling.to_simpson())
print("\nJ-coupling in MRSimulator format:")
print(jcoupling.to_mrsimulator())
J-coupling in Simpson format:
jcoupling 1 7 102.437177 -11.744794 0.007619 131.108898 65.802389 57.760761
J-coupling in MRSimulator format:
{'site_index': (0, 6), 'isotropic_j': 102.4371770558714, 'j_symmetric': {'zeta': np.float64(-23.48958850190371), 'eta': np.float64(0.007619416428260916), 'alpha': np.float64(2.1334771911323362), 'beta': np.float64(1.1484683475271207), 'gamma': np.float64(0.853310697938118)}}
3.3 - SpinSystem#
Now that we’ve covered sites and couplings, we can finally construct a SpinSystem.
We again can use a utility function to generate a SpinSystem from an ase.Atoms object. The get_spin_system function takes an ASE Atoms object and (optionally) an AtomSelection object and returns a SpinSystem object. Or we can create a SpinSystem manually by passing lists of Site and Coupling objects - that way we have more control over which sites and couplings are included in the spin system.
from soprano.properties.nmr import get_spin_system
# Select first three H atoms - the methyl group:
references = {'H': 30.0, 'C': 170.0, 'O': 200.0}
isotopes = {'H': 1, 'C': 13, 'O': 17}
spin_system = get_spin_system(ethanol[:3], isotopes=isotopes, include_dipolar=True, include_j=True, references=references)
# Highlight the three atoms of ethanol (the methyl group used in the spin system)
v = WeasWidget()
v.from_ase(ethanol)
# Highlight the three atoms in the methyl group (indices 0, 1, 2) with a red sphere
v.avr.highlight.settings['methyl_group'] = {
"indices": [0, 1, 2],
"type": "sphere",
"color": [1.0, 1.0, 0.0],
"scale": 1.5,
"opacity": 0.4,
}
v
# Print all couplings between the first two atoms (one dipolar and one J coupling):
print("Couplings between the first two atoms:")
print(spin_system.get_couplings(0, 1))
Couplings between the first two atoms:
[DipolarCoupling(site_i=0, site_j=1, species1='1H', species2='1H', tensor=NMRTensor(data=[[ -1343.72451303 16356.76736598 -26455.59149643]
[ 16356.76736598 10060.85884521 18794.21382822]
[-26455.59149643 18794.21382822 -8717.13433217]]), type='D', tag=None, gamma1=267522128.0, gamma2=267522128.0), ISCoupling(site_i=0, site_j=1, species1='1H', species2='1H', tensor=NMRTensor(data=[[-0.54229077 0.17331152 0.37259644]
[ 0.21674253 -0.19654713 -0.21845368]
[ 0.3554969 -0.25711941 -0.67714406]]), type='J', tag='isc', gamma1=267522128.0, gamma2=267522128.0)]
Convert the whole spin system to the MRSimulator format:
spin_system.to_mrsimulator()
{'sites': [{'isotope': '1H',
'label': 'H',
'isotropic_chemical_shift': np.float64(0.40739316269489034),
'shielding_symmetric': {'zeta': np.float64(5.961009887515734),
'eta': np.float64(0.14196800078113322),
'alpha': np.float64(0.48803517894526216),
'beta': np.float64(0.7913150929573427),
'gamma': np.float64(0.4683872043686721)}},
{'isotope': '1H',
'label': 'H',
'isotropic_chemical_shift': np.float64(-0.25605869019403055),
'shielding_symmetric': {'zeta': np.float64(5.45900548692498),
'eta': np.float64(0.21082029526274973),
'alpha': np.float64(4.758887283431449),
'beta': np.float64(1.3503402102084974),
'gamma': np.float64(1.6270867160340883)}},
{'isotope': '1H',
'label': 'H',
'isotropic_chemical_shift': np.float64(-0.10275656683034946),
'shielding_symmetric': {'zeta': np.float64(4.858397691391922),
'eta': np.float64(0.06045150406399051),
'alpha': np.float64(3.177261605962247),
'beta': np.float64(0.6133956106610512),
'gamma': np.float64(2.9051980259777883)}}],
'couplings': [{'dipolar': {'D': np.float64(-21680.806312876222),
'alpha': np.float64(5.6655094655543925),
'beta': np.float64(0.8180657702868759),
'gamma': np.float64(0.0)},
'site_index': [0, 1],
'isotropic_j': -5.66959685090978,
'j_symmetric': {'zeta': np.float64(-7.378493555421018),
'eta': np.float64(0.2430290565233789),
'alpha': np.float64(2.645622399090312),
'beta': np.float64(0.7566432574831785),
'gamma': np.float64(1.9588018893350834)}},
{'dipolar': {'D': np.float64(-21358.409399716213),
'alpha': np.float64(0.21185518467520392),
'beta': np.float64(1.5593955813713107),
'gamma': np.float64(0.0)},
'site_index': [0, 2],
'isotropic_j': -4.6953651862477,
'j_symmetric': {'zeta': np.float64(-7.423596490349318),
'eta': np.float64(0.25291607692613527),
'alpha': np.float64(1.7723459031890676),
'beta': np.float64(1.5468393602573107),
'gamma': np.float64(2.7674259772176533)}},
{'dipolar': {'D': np.float64(-21554.100943486003),
'alpha': np.float64(4.163764907807882),
'beta': np.float64(0.8354223009650883),
'gamma': np.float64(0.0)},
'site_index': [1, 2],
'isotropic_j': -3.9423826685108607,
'j_symmetric': {'zeta': np.float64(-7.423777128127044),
'eta': np.float64(0.19341152013036295),
'alpha': np.float64(0.9557793770751726),
'beta': np.float64(0.7477431430281606),
'gamma': np.float64(1.9418890767187036)}}]}
Or to the Simpson format:
print(spin_system.to_simpson())
spinsys {
channels 1H
nuclei 1H 1H 1H
shift 1 0.40739316269489034p -5.961188723177429p 0.14196800078113322 153.16339001174353 45.33901509145815 152.03764399252458
shift 2 -0.25605869019403055p -5.459169262002839p 0.21082029526274973 86.7747982694457 77.3687949517553 267.33584348090034
shift 3 -0.10275656683034946p -4.858543447695352p 0.06045150406399051 13.544414461734629 35.144979662728076 357.95631956940525
dipole 1 2 -21680.806313 8.817606 46.871716 0.000000
dipole 1 3 -21358.409400 176.965866 89.346785 0.000000
dipole 2 3 -21554.100943 82.852394 47.866172 0.000000
jcoupling 1 2 -5.669597 -3.689247 0.243029 67.768919 43.352465 28.417002
jcoupling 1 3 -4.695365 -3.711798 0.252916 21.438171 88.627367 78.452060
jcoupling 2 3 -3.942383 -3.711889 0.193412 68.737952 42.842526 125.237876
}
Cross-terms (quadrupole × dipole / quadrupole × shift)#
When a site is quadrupolar (spin > ½), SIMPSON can include second-order cross-terms between the quadrupolar interaction and the dipolar or chemical-shift interaction. Soprano emits these automatically via quadrupole_x_dipole and quadrupole_x_shift blocks.
Here we use the ethanol methyl group again, but label the hydrogens as ²H (spin-1, quadrupolar). Because ²H has a non-zero EFG, the cross-terms appear in the output.
# Use the ethanol methyl group (first 3 atoms: C + 2H), but treat H as ²H (spin-1, quadrupolar).
# The EFG on each ²H site triggers quadrupole_x_dipole and quadrupole_x_shift cross-term blocks.
spin_system_2H = get_spin_system(
ethanol[:3],
isotopes={'H': 2, 'C': 13},
references={'H': 0.0, 'C': 170.0},
include_dipolar=True,
)
print(spin_system_2H.to_simpson())
spinsys {
channels 2H
nuclei 2H 2H 2H
shift 1 -29.59261905909999p -5.961009887515734p 0.14196800078113322 153.16339001174353 45.33901509145815 152.03764399252458
shift 2 -30.25605100843332p -5.45900548692498p 0.21082029526274973 86.7747982694457 77.3687949517553 267.33584348090034
shift 3 -30.102753484133345p -4.858397691391922p 0.06045150406399051 13.544414461734629 35.144979662728076 357.95631956940525
quadrupole 1 2 193809.12541952616 0.01819690682375444 145.3399331570065 53.86885309530375 168.96755681318012
quadrupole 2 2 191322.119193839 0.027063430326907292 92.75622320412285 58.60389462107387 258.62993699661143
quadrupole 3 2 195404.46463165167 0.01700845310705252 26.88348987089756 54.807490906364386 346.82054176593334
dipole 1 2 -510.889195 133.899793 46.871716 0.000000
dipole 1 3 -503.292194 86.965866 89.346785 0.000000
dipole 2 3 -507.903494 86.565094 47.866172 0.000000
quadrupole_x_dipole 1 2
quadrupole_x_dipole 2 1
quadrupole_x_dipole 1 3
quadrupole_x_dipole 3 1
quadrupole_x_dipole 2 3
quadrupole_x_dipole 3 2
quadrupole_x_shift 1
quadrupole_x_shift 2
quadrupole_x_shift 3
}