Skip to content

Commit 86f4f4b

Browse files
committed
manage units
1 parent f093aa6 commit 86f4f4b

4 files changed

Lines changed: 47 additions & 30 deletions

File tree

‎nmrdfrommd/NMR.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
# Released under the GNU Public Licence, v3 or any higher version
99
# SPDX-License-Identifier: GPL-3.0-or-later
1010

11+
1112
import numpy as np
1213
from scipy import constants as cst
1314
from scipy.interpolate import interp1d

‎nmrdfrommd/fourier.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
# SPDX-License-Identifier: GPL-3.0-or-later
1010

1111
import numpy as np
12-
from scipy import constants as cst
12+
from .units import PS_TO_S, HZ_TO_MHZ
1313

1414
# This Fourier transform implementation is numerically correct
1515
# but scientifically fragile due to implicit unit handling and
@@ -64,10 +64,10 @@ def fourier_transform(data, normalize=False):
6464
if not np.allclose(np.diff(data[:, 0]), dt_ps):
6565
raise ValueError("Non-uniform time spacing detected.")
6666

67-
dt = dt_ps * cst.pico # convert to seconds
67+
dt = dt_ps * PS_TO_S
6868

6969
n = len(data)
70-
freqs = np.fft.rfftfreq(n, dt) / cst.mega # in MHz
70+
freqs = np.fft.rfftfreq(n, dt) * HZ_TO_MHZ # in MHz
7171
fft_norm = 'ortho' if normalize else None
7272
spectrum = np.fft.rfft(data[:, 1], norm=fft_norm) * dt * 2
7373

‎nmrdfrommd/relaxation.py‎

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from scipy.interpolate import interp1d
1414

1515
from .utilities import find_nearest
16+
from .units import ANGSTROM_TO_M
1617

1718

1819
def compute_relaxation_rates(f, J, K, isotropic):
@@ -23,11 +24,12 @@ def compute_relaxation_rates(f, J, K, isotropic):
2324
Parameters
2425
----------
2526
f : np.ndarray
26-
Frequency array.
27+
Frequency array, in MHz.
2728
J : np.ndarray
28-
Spectral density, shape (dim, n_freqs); dim=1 if isotropic, else 3.
29+
Spectral density, shape (dim, n_freqs), in Angstrom^-6 * s;
30+
dim=1 if isotropic, else 3.
2931
K : float
30-
Dipolar relaxation prefactor.
32+
Dipolar relaxation prefactor, in m^6 * s^-2 (SI).
3133
isotropic : bool
3234
Whether to use the isotropic (dim=1) or anisotropic (dim=3) formula.
3335
@@ -36,21 +38,21 @@ def compute_relaxation_rates(f, J, K, isotropic):
3638
R1, R2 : np.ndarray
3739
Relaxation rates on the same frequency grid as f.
3840
"""
39-
prefactor = K / cst.angstrom ** 6
40-
idx0 = find_nearest(f, 0.0)
41-
J0 = J[0]
41+
prefactor = K / ANGSTROM_TO_M ** 6 # Angstrom^6 * s^-2 (K rescaled m^6 -> Angstrom^6 to match J)
42+
idx0 = find_nearest(f, 0.0) # index, dimensionless
43+
J0 = J[0] # Angstrom^-6 * s
4244

4345
if isotropic:
44-
J02 = interp1d(f, J0, fill_value="extrapolate")(2 * f)
45-
R1 = prefactor * (J0 + 4 * J02) / 6
46-
R2 = prefactor * (3 / 2 * J0[idx0] + 5 / 2 * J0 + J02) / 6
46+
J02 = interp1d(f, J0, fill_value="extrapolate")(2 * f) # Angstrom^-6 * s
47+
R1 = prefactor * (J0 + 4 * J02) / 6 # s^-1
48+
R2 = prefactor * (3 / 2 * J0[idx0] + 5 / 2 * J0 + J02) / 6 # s^-1
4749
else:
48-
J1 = J[1]
49-
J2 = interp1d(f, J[2], fill_value="extrapolate")(2 * f)
50-
R1 = prefactor * (J1 + J2)
51-
R2 = prefactor * (1 / 4) * (J0[idx0] + 10 * J1 + J2)
50+
J1 = J[1] # Angstrom^-6 * s
51+
J2 = interp1d(f, J[2], fill_value="extrapolate")(2 * f) # Angstrom^-6 * s
52+
R1 = prefactor * (J1 + J2) # s^-1
53+
R2 = prefactor * (1 / 4) * (J0[idx0] + 10 * J1 + J2) # s^-1
5254

53-
return R1, R2
55+
return R1, R2 # s^-1, s^-1
5456

5557
def compute_relaxation_times(f, R1, R2, target_frequency=None):
5658
"""Compute T1 and T2 relaxation times from relaxation rates R1, R2.
@@ -62,27 +64,27 @@ def compute_relaxation_times(f, R1, R2, target_frequency=None):
6264
Parameters
6365
----------
6466
f : np.ndarray
65-
Frequency array.
67+
Frequency array, in MHz.
6668
R1 : np.ndarray
67-
Longitudinal relaxation rate, same shape as f.
69+
Longitudinal relaxation rate, in s^-1, same shape as f.
6870
R2 : np.ndarray
69-
Transverse relaxation rate, same shape as f.
71+
Transverse relaxation rate, in s^-1, same shape as f.
7072
target_frequency : float, optional
71-
Frequency at which to evaluate T1/T2. If None, f=0 is used.
73+
Frequency at which to evaluate T1/T2, in MHz. If None, f=0 is used.
7274
7375
Returns
7476
-------
7577
T1, T2 : float
76-
Relaxation times. np.inf if the corresponding rate is ~0.
78+
Relaxation times, in s. np.inf if the corresponding rate is ~0.
7779
"""
78-
target = 0.0 if target_frequency is None else target_frequency
79-
idx = find_nearest(f, target)
80+
target = 0.0 if target_frequency is None else target_frequency # MHz
81+
idx = find_nearest(f, target) # index, dimensionless
8082

81-
R1_val = R1[idx]
82-
R2_val = R2[idx]
83+
R1_val = R1[idx] # s^-1
84+
R2_val = R2[idx] # s^-1
8385

84-
eps = 1e-20
85-
T1 = np.inf if np.isclose(R1_val, 0.0) else 1.0 / (R1_val + eps)
86-
T2 = np.inf if np.isclose(R2_val, 0.0) else 1.0 / (R2_val + eps)
86+
eps = 1e-20 # s^-1 (safety floor, sized for R1/R2 ~ 0.1-1e4 s^-1)
87+
T1 = np.inf if np.isclose(R1_val, 0.0) else 1.0 / (R1_val + eps) # s
88+
T2 = np.inf if np.isclose(R2_val, 0.0) else 1.0 / (R2_val + eps) # s
8789

88-
return T1, T2
90+
return T1, T2 # s, s

‎nmrdfrommd/units.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""Unit conventions and conversion factors for NMRDfromMD.
2+
3+
Internal convention (matches MDAnalysis and common NMR usage):
4+
length : Angstrom (Å) -- matches MDAnalysis .positions
5+
time : picosecond (ps) -- matches MDAnalysis .trajectory.dt
6+
frequency : MHz -- conventional for NMR relaxation
7+
Anything else (K, gamma, ...) is SI, converted to/from the above only
8+
at the points imported from here.
9+
"""
10+
from scipy import constants as cst
11+
12+
PS_TO_S = cst.pico
13+
HZ_TO_MHZ = 1 / cst.mega
14+
ANGSTROM_TO_M = cst.angstrom

0 commit comments

Comments
 (0)