"""The `dnmr` module provides functions for calculating DNMR line shapes, and
classes to describe DNMR systems.
The dnmr module provides the following classes:
* `DnmrTwoSinglets`: a sumulation of the lineshape for two uncoupled nuclei
undergoing exchange.
* `DnmrAB`: a simulation of the lineshape for two coupled nuclei undergoing
exchange (i.e. an AB (or AX) pattern at the slow exchange limit).
The `dnmr` module provides the following functions:
* `dnmr_two_singlets`: for simulating the lineshape for two uncoupled nuclei
undergoing exchange [3]_.
* `dnmr_AB` : for simulating the lineshape for two coupled nuclei undergoing
exchange (i.e. an AB (or AX) pattern at the slow exchange limit) [4]_.
References
----------
.. [3] Sandström, J. Dynamic NMR Spectroscopy; Academic Press: New York, 1982.
.. [4] a) Brown, K.C.; Tyson, R.L.; Weil, J.A. J. Chem. Educ. 1998, 75, 1632.
b) an important math correction to the previous reference:
TODO: add reference to correction
"""
import numpy as np
from nmrsim._utils import is_number, is_decimal_fraction, is_tuple_of_two_numbers, is_positive, is_integer
def _dnmr_two_singlets_func(va, vb, ka, wa, wb, pa):
"""
Create a function that requires only frequency as an argurment, for
calculating the lineshape of a DNMR spectrum for two uncoupled spin-half
nuclei.
This allows the expressions that are independent of frequency to be
calculated only once, outside the returned function. The returned function
can then be applied to a list of frequency (x) coordinates (e.g. a numpy
linspace) to provide a list of the corresponding intensity (y) coordinates.
Parameters
----------
va : int or float
The frequency (Hz) of nucleus 'a' at the slow exchange limit. va > vb
vb : int or float
The frequency (Hz) of nucleus 'b' at the slow exchange limit. vb < va
ka : int or float
The rate constant (Hz) for state a--> state b
wa : int or float
The width at half height of the signal for nucleus a (at the slow
exchange limit).
wb : int or float
The width at half height of the signal for nucleus b (at the slow
exchange limit).
pa : float (0 <= pa <= 1)
The fraction of the population in state a.
Returns
-------
_maker: function
Notes
-----
The nmrsim.dnmr module gives a reference for the algorithm used here.
"""
pi = np.pi
pi_squared = pi ** 2
T2a = 1 / (pi * wa)
T2b = 1 / (pi * wb)
pb = 1 - pa
tau = pb / ka
dv = va - vb
Dv = (va + vb) / 2
P = tau * (1 / (T2a * T2b) + pi_squared * (dv ** 2)) + (pa / T2a + pb / T2b)
p = 1 + tau * ((pb / T2a) + (pa / T2b))
Q = tau * (- pi * dv * (pa - pb))
R = pi * dv * tau * ((1 / T2b) - (1 / T2a)) + pi * dv * (pa - pb)
r = 2 * pi * (1 + tau * ((1 / T2a) + (1 / T2b)))
def _maker(v):
"""Calculate the intensity (y coordinate) at a given frequency v(x coordinate)."""
# TODO: fix docstring, explain _P _Q etc correlate to P, Q etc in lit.
# FIXED: previous version of this function used
# nonlocal Dv, P, Q, R
# but apparently when function called repeatedly these values would
# become corrupted (turning into arrays?!)
# Solution: add underscores to create copies of any variables in
# outer scope whose values are changed in the inner scope.
_Dv = Dv - v
_P = P - tau * 4 * pi_squared * (_Dv ** 2)
_Q = Q + tau * 2 * pi * _Dv
_R = R + _Dv * r
return(_P * p + _Q * _R) / (_P ** 2 + _R ** 2)
return _maker
[docs]def dnmr_two_singlets(va, vb, ka, wa, wb, pa, limits=None, points=800):
"""
Create a the lineshape for a DNMR spectrum of two uncoupled spin-half nuclei.
Parameters
----------
va, vb : int or float
The frequencies (Hz) of nuclei 'a' and 'b' at the slow exchange limit.
ka : int or float
The rate constant (Hz) for state a--> state b
wa, wb : int or float
The peak widths at half height for the 'a' and 'b' singlets at the
slow-exchange limit.
pa : float (0 <= pa <= 1)
The fraction of the population in state a
limits : (int or float, int or float), optional
The minimum and maximum frequencies (in any order) for the simulation.
points : int
The length of the returned arrays (i.e. the number of points plotted).
Returns
-------
x, y : numpy.array, numpy.array
Arrays for the x (frequency) and y (intensity) lineshape data points.
See Also
--------
DnmrTwoSinglets : A class representation for this simulation.
References
----------
See the documentation for the nmrsim.dnmr module.
"""
if vb > va:
va, vb = vb, va
wa, wb = wb, wa
pa = 1 - pa
if limits:
l_limit = min(limits)
r_limit = max(limits)
else:
l_limit = vb - 50
r_limit = va + 50
x = np.linspace(l_limit, r_limit, points)
func = _dnmr_two_singlets_func(va, vb, ka, wa, wb, pa)
y = func(x)
return x, y
[docs]class DnmrTwoSinglets:
"""
A DNMR simulation for two uncoupled nuclei undergoing exchange.
Parameters
----------
va, vb : int or float
The frequencies (Hz) of nuclei 'a' and 'b' at the slow exchange limit.
k : int or float
The rate constant (Hz) for state a--> state b
wa, wb : int or float
The peak widths at half height for the 'a' and 'b' singlets at the
slow-exchange limit.
pa : float (0 <= pa <= 1)
The fraction of the population in state a
limits : (int or float, int or float), optional
The minimum and maximum frequencies (in any order) for the simulation.
points : int
The length of the returned arrays (i.e. the number of points plotted).
Attributes
----------
va
vb
k
wa
wb
pa
limits
points
Methods
-------
lineshape
Return the x, y (frequency, intensity) data for the lineshape
simulation.
See Also
--------
DnmrTwoSinglets : A class representation for this simulation
"""
def __init__(self, va=1, vb=0, k=0.01, wa=0.5, wb=0.5, pa=0.5,
limits=None, points=800):
# rethink default kwargs for v/k/w
self.va = va
self.vb = vb
self.k = k
self.wa = wa
self.wb = wb
self.pa = pa
if limits:
self.limits = limits
else:
self._vmin = min([va, vb]) - 50
self._vmax = max([va, vb]) + 50
self.points = points
@property
def va(self):
"""
The frequency of nucleus "a" (Hz) at the slow-exchange limit.
Returns
-------
int or float
"""
return self._va
@va.setter
def va(self, value):
self._va = is_number(value)
@property
def vb(self):
"""
The frequency of nucleus "b" (Hz) at the slow-exchange limit.
Returns
-------
int or float
"""
return self._vb
@vb.setter
def vb(self, value):
self._vb = is_number(value)
@property
def k(self):
"""
The rate constant (Hz) for state A--> state B (must be >0).
Returns
-------
int or float
"""
return self._k
@k.setter
def k(self, value):
self._k = is_positive(value)
@property
def wa(self):
"""
The peak width at half height (Hz) for the 'a' singlet at the
slow-exchange limit.
Returns
-------
int or float
"""
return self._wa
@wa.setter
def wa(self, value):
self._wa = is_number(value)
@property
def wb(self):
"""
The peak width at half height (Hz) for the 'b' singlet at the
slow-exchange limit.
Returns
-------
int or float
"""
return self._wb
@wb.setter
def wb(self, value):
self._wb = is_number(value)
@property
def pa(self):
"""
The fraction of the population in state a. Must be >=0 and <=1.
Returns
-------
float
"""
return self._pa
@pa.setter
def pa(self, value):
self._pa = is_decimal_fraction(value)
@property
def limits(self):
"""
The minimum and maximum frequencies for the simulated lineshape.
Returns
-------
(int or float, int or float)
"""
return self._vmin, self._vmax
@limits.setter
def limits(self, limits):
limits = is_tuple_of_two_numbers(limits)
self._vmin = min(limits)
self._vmax = max(limits)
@property
def points(self):
"""
The length of the returned arrays (i.e. the number of points plotted).
Returns
-------
int
"""
return self._points
@points.setter
def points(self, value):
self._points = is_integer(value)
[docs] def lineshape(self):
"""
Calculate and return the lineshape for the DNMR spectrum.
Returns
-------
x, y : numpy.array, numpy.array
Arrays for the x (frequency) and y (intensity) lineshape data
points.
"""
x = np.linspace(self._vmin, self._vmax, self.points)
x, y = dnmr_two_singlets(self.va, self.vb, self.k, self.wa, self.wb, self.pa,
limits=self.limits, points=self.points)
return x, y
def _dnmr_AB_func(v, v1, v2, J, k, w):
"""
Implement the equation from Weil et al for simulation of the DNMR lineshape
for two coupled nuclei undergoing exchange (AB or AX pattern at the
slow-exchange limit).
Parameters
----------
v : float or array-like
a frequency (x coordinate) or array of frequencies at which an
amplitude (y coordinate) is to be calculated.
v1, v2 : float
frequencies of a and b nuclei (at the slow exchange limit,
in the absence of coupling)
J : float
the coupling constant between the two nuclei.
k : float
rate constant for state A--> state B
w : float
peak widths at half height (slow exchange limit).
Returns
-------
float
amplitude at frequency `v`.
See Also
--------
DnmrAB : A class representation for this simulation.
References
----------
See the documentation for the nmrsim.dnmr module.
"""
pi = np.pi
vo = (v1 + v2) / 2
tau = 1 / k
tau2 = 1 / (pi * w)
a1_plus = 4 * pi ** 2 * (vo - v + J / 2) ** 2
a1_minus = 4 * pi ** 2 * (vo - v - J / 2) ** 2
a2 = - ((1 / tau) + (1 / tau2)) ** 2
a3 = - pi ** 2 * (v1 - v2) ** 2
a4 = - pi ** 2 * J ** 2 + (1 / tau ** 2)
a_plus = a1_plus + a2 + a3 + a4
a_minus = a1_minus + a2 + a3 + a4
b_plus = 4 * pi * (vo - v + J / 2) * (
(1 / tau) + (1 / tau2)) - 2 * pi * J / tau
b_minus = 4 * pi * (vo - v - J / 2) * (
(1 / tau) + (1 / tau2)) + 2 * pi * J / tau
r_plus = 2 * pi * (vo - v + J)
r_minus = 2 * pi * (vo - v - J)
s = (2 / tau) + (1 / tau2)
n1 = r_plus * b_plus - s * a_plus
d1 = a_plus ** 2 + b_plus ** 2
n2 = r_minus * b_minus - s * a_minus
d2 = a_minus ** 2 + b_minus ** 2
I = (n1 / d1) + (n2 / d2)
return I
[docs]def dnmr_AB(va, vb, J, k, w, limits=None, points=800):
"""
Simulate the DNMR lineshape for two coupled nuclei undergoing exchange
(AB or AX pattern at the slow-exchange limit).
Parameters
---------
va, vb : float
frequencies of a and b nuclei (at the slow exchange limit,
in the absence of coupling)
J : float
the coupling constant between the two nuclei.
k : float
rate constant for state A--> state B
w : float
peak widths at half height (at the slow-exchange limit).
limits : (int or float, int or float), optional
The minimum and maximum frequencies (in any order) for the simulation.
points : int
The length of the returned arrays (i.e. the number of points plotted).
Returns
-------
x, y : numpy.array, numpy.array
Arrays for the x (frequency) and y (intensity) lineshape data points.
See Also
--------
DnmrAB : A class representation for this simulation.
References
----------
See the documentation for the nmrsim.dnmr module.
"""
if limits:
l_limit = min(limits)
r_limit = max(limits)
else:
l_limit = min(va, vb) - 50
r_limit = max(va, vb) + 50
x = np.linspace(l_limit, r_limit, points)
y = _dnmr_AB_func(x, va, vb, J, k, w)
return x, y
[docs]class DnmrAB:
"""
Simulate the DNMR lineshape for two coupled nuclei undergoing exchange
(AB or AX pattern at the slow-exchange limit).
Parameters
----------
va, vb : int or float
frequencies of a and b nuclei (at the slow exchange limit,
in the absence of coupling)
J : int or float
the coupling constant between the two nuclei.
k : int or float
rate constant for state A--> state B
w : int or float
peak widths at half height (at the slow-exchange limit).
limits : (int or float, int or float), optional
The minimum and maximum frequencies (in any order) for the simulation.
points : int
The length of the returned arrays (i.e. the number of points plotted).
Attributes
----------
va
vb
J
k
w
limits
points
Methods
-------
lineshape
Return the x, y (frequency, intensity) data for the lineshape
simulation.
See Also
--------
DnmrAB : A class representation for this simulation.
References
----------
See the documentation for the nmrsim.dnmr module.
"""
def __init__(self, va=165.0, vb=135.0, J=12.0, k=12.0, w=0.5,
limits=None, points=800):
self.va = va
self.vb = vb
self.J = J
self.k = k
self.w = w
if limits:
self.limits = limits
else:
self._vmin = min([va, vb]) - 50
self._vmax = max([va, vb]) + 50
self.points = points
@property
def va(self):
"""
The frequency of nucleus "a" (Hz) at the slow-exchange limit, in the absence of coupling.
Returns
-------
int or float
"""
return self._va
@va.setter
def va(self, value):
self._va = is_number(value)
@property
def vb(self):
"""
The frequency of nucleus "b" (Hz) at the slow-exchange limit, in the absence of coupling.
Returns
-------
int or float
"""
return self._vb
@vb.setter
def vb(self, value):
self._vb = is_number(value)
@property
def J(self):
"""
The coupling constant (Hz) between the two nuclei.
Returns
-------
int or float
"""
return self._J
@J.setter
def J(self, value):
self._J = is_number(value)
@property
def k(self):
"""
The rate constant (Hz) for state A--> state B (must be >0).
Returns
-------
int or float
"""
return self._k
@k.setter
def k(self, value):
self._k = is_positive(value)
@property
def w(self):
"""
The peak width (Hz) at half height (at the slow-exchange limit).
Returns
-------
int or float
"""
return self._w
@w.setter
def w(self, value):
self._w = is_number(value)
@property
def limits(self):
"""
Give minimum and maximum frequencies for the simulated lineshape.
Returns
-------
(int or float, int or float)
"""
return self._vmin, self._vmax
@limits.setter
def limits(self, limits):
limits = is_tuple_of_two_numbers(limits)
self._vmin = min(limits)
self._vmax = max(limits)
@property
def points(self):
"""
Give the length of the returned arrays (i.e. the number of points plotted).
Returns
-------
int
"""
return self._points
@points.setter
def points(self, value):
self._points = is_integer(value)
[docs] def lineshape(self):
"""Return the x, y lineshape data for the simulation."""
x = np.linspace(self._vmin, self._vmax, self.points)
y = _dnmr_AB_func(x, self.va, self.vb, self.J, self.k, self.w)
return x, y