ssnmr
Overview
ssnmr covers three complementary NMR-observable predictions for IDP/IDR ensembles:
Sequence-based random-coil chemical shifts (1H, 13C, 15N), via
compute_random_coil_chemical_shifts(). Stateless — takes a sequence string, no trajectory needed.Structure-based scalar (J) couplings: ³J(HN, Hα) computed per frame per residue from the φ dihedral via the Karplus relation, via
compute_J3_HN_HA()(and the generic Karplus evaluatorkarplus()for arbitrary coefficients). Takes anSSProtein.NOE distances: per-frame inter-atom distances via
compute_NOE_distances(), collapsed to an \(\langle r^{-p}\rangle^{-1/p}\) ensemble average bynoe_ensemble_average().
Random coil chemical shifts
The primary function, compute_random_coil_chemical_shifts(), predicts sequence-corrected random coil 1H, 13C, and 15N chemical shifts for a given amino acid sequence. These are useful as a disordered-state reference baseline when interpreting experimental NMR spectra of intrinsically disordered proteins (IDPs) or unfolded proteins.
Corrections applied include:
Nearest-neighbour sequence effects — shifts are adjusted for the two residues on either side of each position, using the correction factors of Kjaergaard & Poulsen (2011) and Schwarzinger et al. (2001).
Temperature — linear corrections are applied relative to a 5 °C baseline.
pH — charged-state populations for Asp, Glu, His, and phosphorylated residues (pSer, pThr, pTyr) are accounted for via fractional deprotonation at the given pH.
Perdeuteration — optional corrections for fully deuterated protein samples.
Supported residue types. All 20 canonical amino acids are supported, along with three phosphorylated residues: phosphoserine (pSer / SEP / PS), phosphothreonine (pThr / PTHR / PT), and phosphotyrosine (pTyr / PTYR / PY). Phosphorylated residues cannot be combined with the perdeuteration corrections.
Output format. The function returns a list of per-residue dictionaries, one per position (excluding the two terminal padding residues), each containing keys Res, Index, CA, CB, CO, N, HN, and HA. Glycine lacks a Cβ (CB is "**.***") and proline lacks a backbone amide (N and HN are "*.***"). Shifts are returned as floats or three-decimal-place strings depending on the asFloat flag.
Example usage:
from soursop.ssnmr import compute_random_coil_chemical_shifts
sequence = "MAEQKLISEEDL"
shifts = compute_random_coil_chemical_shifts(sequence, temperature=25, pH=7.4)
for residue in shifts:
print(residue['Res'], residue['CA'], residue['N'])
Scalar (J) couplings
For each residue with a defined backbone φ dihedral, the three-bond ³J(HN, Hα) scalar coupling is well approximated by the Karplus relation
where A, B, C and φ₀ are empirical coefficients fitted to experimental data. ssnmr ships six literature parameterisations of these coefficients, ported from the biceps package (Voelz lab), which itself adapts MDTraj’s mdtraj/nmr/scalar_couplings.py (Beauchamp et al.):
Model |
A |
B |
C |
σ (Hz) |
|---|---|---|---|---|
|
8.40 |
-1.36 |
0.33 |
0.36 |
|
7.09 |
-1.42 |
1.55 |
0.39 |
|
7.90 |
-1.05 |
0.65 |
0.25 |
|
7.13 |
-1.31 |
1.56 |
0.34 |
|
6.51 |
-1.76 |
1.60 |
0.73 |
|
6.40 |
-1.40 |
1.90 |
0.76 |
All six models share φ₀ = −60° (the convention is to phase-shift the Karplus form by −60° so that θ = 0 corresponds to the ideal HN–Cα–N–C′ eclipsed geometry). The per-model σ is the RMSD of the parameterisation against its training experimental dataset and is a sensible forward-model uncertainty to use when feeding J-couplings into the BME or COPER reweighters.
Units. ssnmr takes φ in degrees (consistent with SSProtein.get_angles) and returns J in Hz.
Computing 3J(HN, Hα) from an ensemble:
from soursop.sstrajectory import SSTrajectory
from soursop.ssnmr import compute_J3_HN_HA
traj = SSTrajectory('traj.xtc', 'start.pdb')
protein = traj.proteinTrajectoryList[0]
# Per-frame, per-residue J-couplings (shape: n_frames x n_phi).
atoms, J = compute_J3_HN_HA(protein, model="Bax2007")
# Ensemble mean + the model's forward-model uncertainty in Hz.
atoms, J_mean, sigma = compute_J3_HN_HA(
protein, model="Bax2007", weights=False, return_uncertainty=True)
J_mean = J.mean(axis=0)
The (n_frames, n_phi) matrix is the natural input for the reweighters - so a typical workflow against an experimental J vector is:
from soursop.ssbme import BME, ExperimentalObservable
atoms, J_calc, sigma = compute_J3_HN_HA(protein, return_uncertainty=True)
# one experimental observable per residue with a defined phi
obs = [ExperimentalObservable(value=J_exp[k], uncertainty=sigma,
name=f"3J_HN_HA_res{k}")
for k in range(J_calc.shape[1])]
result = BME(obs, J_calc).fit(theta=2.0, auto_theta=False)
weights = result.weights
Other Karplus parameterisations. For any other Karplus-form coupling - ³J(Hα, C′), ³J(HN, Cβ), Bothner-By, Tvaroska, Aydin, … - call karplus() directly with the appropriate dihedral angle (in degrees) and your coefficients.
Citations. The HN-Hα coefficients are due to: Vögeli, B. et al. J. Am. Chem. Soc. 129, 9377-9385 (2007) (Bax2007); Hu, J.-S. & Bax, A. J. Am. Chem. Soc. 119, 6360-6368 (1997) (Bax1997); Schmidt, J. M. et al. J. Biomol. NMR 14, 1-12 (1999) (Ruterjans1999); Habeck, M., Rieping, W. & Nilges, M. J. Magn. Reson. 177, 160-165 (2005) (Habeck); Vuister, G. W. & Bax, A. J. Am. Chem. Soc. 115, 7772-7777 (1993) (Vuister); Pardi, A., Billeter, M. & Wüthrich, K. J. Mol. Biol. 180, 741-751 (1984) (Pardi).
NOE distances
The nuclear Overhauser effect (NOE) cross-peak between two protons depends on the inverse sixth power of the inter-proton distance averaged over the ensemble:
Two helpers cover the typical workflow:
compute_NOE_distances()returns the per-frame inter-atom distance matrix in Angstroms for a list of atom pairs — the natural per-frame structural primitive.noe_ensemble_average()collapses such a distance array via the NOE convention \((\sum_i w_i\,d_i^{-p})^{-1/p}\) (default \(p = 6\); some studies use \(p = 3\)). It honours the package-wideweightscontract:weights=False(default) gives the uniform mean, a per-frame weight vector gives a reweighted NOE distance.
Computing ensemble-averaged NOE distances:
import numpy as np
from soursop.ssnmr import compute_NOE_distances, noe_ensemble_average
pairs = np.array([[0, 10], [0, 20], [5, 15]]) # atom indices
d = compute_NOE_distances(protein, pairs) # (n_frames, 3) A
r_noe = noe_ensemble_average(d, power=6) # (3,) A
Feeding NOEs to BME / COPER. The linear-additive observable for reweighting is \(r^{-p}\), not \(r\) (because NOE intensity itself is the linear ensemble average of \(r^{-p}\)). So the BME-ready calculated_values matrix is d ** -p and the experimental observable is r_exp ** -p:
from soursop.ssbme import BME, ExperimentalObservable
calc = d ** -6 # (n_frames, n_pairs)
obs = [ExperimentalObservable((r_exp[k]) ** -6,
uncertainty=6.0 * (r_exp[k]) ** -7 * sigma_r[k],
name=f"NOE_pair{k}")
for k in range(len(r_exp))] # uncertainty propagated from r_exp
weights = BME(obs, calc).fit(theta=2.0, auto_theta=False).weights
(Note the experimental σ on the linear observable \(r^{-6}\) is obtained by error-propagation from σ on \(r\): σ(r^{-6}) ≈ 6·r^{-7}·σ(r).)
ssnmr - NMR observables for IDP/IDR ensembles.
This module covers two complementary NMR predictions:
Sequence-based random-coil chemical shifts — predicts random-coil backbone shifts (CA, CB, CO, N, HN, HA) for an arbitrary amino-acid sequence, corrected for temperature, pH and (optionally) perdeuteration, including phosphorylated Ser/Thr/Tyr. Implementation ports the Kjaergaard & Poulsen / Schwarzinger reference-shift and neighbour- correction tables. The public entry point is
compute_random_coil_chemical_shifts().Structure-based scalar (J) couplings — predicts the backbone ³J(HN, Hα) scalar coupling per residue per frame from the φ dihedral via the Karplus relation, using any of the six literature parameterisations stored in
KARPLUS_HN_HA_COEFFICIENTS(Bax2007, Bax1997, Ruterjans1999, Habeck, Vuister, Pardi). Public entry points arekarplus()(generic Karplus evaluator for arbitrary coefficients) andcompute_J3_HN_HA()(operates on anSSProtein). The returned(n_frames, n_phi)array is the natural input forsoursop.ssbme.BME/BMECustomandsoursop.sscoper.COPERreweighting.
The Karplus coefficient table is adapted from biceps (Voelz lab,
https://github.com/vvoelz/biceps), itself ported from MDTraj’s
mdtraj/nmr/scalar_couplings.py (Beauchamp / McGibbon / Lane).
Author(s): Alex Keeley (chemical shifts) and Alex Holehouse (J-couplings).
- soursop.ssnmr.compute_random_coil_chemical_shifts(protein_sequence, temperature=25, pH=7.4, use_ggxgg=True, use_perdeuteration=False, asFloat=True)[source]
Predict sequence-corrected random-coil chemical shifts.
For a user-provided amino-acid sequence, predicts the random-coil backbone chemical shifts (CA, CB, CO, N, HN, HA) and applies sequence-context (nearest-neighbour), temperature and pH corrections. Reference shifts and general sequence-correction factors are from Kjaergaard & Poulsen (J. Biomol. NMR 2011, 50:157-165); temperature and glycine corrections are from Kjaergaard, Brander & Poulsen (J. Biomol. NMR 2011, 49:139-149); the correction-factor methodology follows Schwarzinger et al. (JACS 2001, 123:2970-2978); and the perdeuteration corrections are from Cavanagh, Fairbrother, Palmer, Rance & Skelton, Protein NMR Spectroscopy, 2nd ed. (Academic Press, 2007). The implementation is a port of the JavaScript tool by Alex Maltsev (NIH); see https://www1.bio.ku.dk/english/research/bms/research/sbinlab/randomchemicalshifts/
The input may be a standard one-letter sequence; phospho-residues can additionally be supplied using parenthesised three-letter codes (e.g.
"AS(SEP)GA"for a phospho-serine). Glycine and proline produce masked placeholder values for atoms they lack (CB for glycine; N/HN for proline).- Parameters:
protein_sequence (str) – Amino-acid sequence to predict shifts for. One-letter codes, with optional parenthesised multi-letter codes for phospho-residues (
SEP/PS,TPO/PT,PTR/PY).temperature (float or int, optional) – Sample temperature in degrees Celsius, used for the temperature correction. Must be between 0 and 100. Default
25.pH (float or int, optional) – Sample pH, used for the pH (titratable-residue) correction. Must be between 0 and 14. Default
7.4.use_ggxgg (bool, optional) – Whether to apply the GGXGG-based neighbour correction for glycines. Default
True.use_perdeuteration (bool, optional) – Whether to apply perdeuterated correction factors. Cannot be combined with phospho-residues. Default
False.asFloat (bool, optional) – If
Truethe output chemical shifts are floats; ifFalsethey are formatted strings. DefaultTrue.
- Returns:
One dictionary per residue in the input sequence, each containing the residue abbreviation (
'Res'), its index ('Index') and the six predicted shifts ('CA','CB','CO','N','HN','HA'). Atoms absent for a residue type (glycine CB, proline N/HN, HA under perdeuteration) carry a masked placeholder.- Return type:
list of dict
- Raises:
soursop.ssexceptions.SSException – If
temperatureis outside 0-100 C, ifpHis outside 0-14, or ifuse_perdeuterationis requested for a sequence containing phosphorylated residues.
Example
>>> shifts = compute_random_coil_chemical_shifts('ASGAS', temperature=25, pH=7.4) >>> sorted(shifts[0].keys()) ['CA', 'CB', 'CO', 'HA', 'HN', 'Index', 'N', 'Res']
- soursop.ssnmr.karplus(angle, A, B, C, phi0=0.0)[source]
Generic Karplus relation
J = A cos^2(theta) + B cos(theta) + C.theta = angle + phi0with both quantities in degrees; the returned scalar coupling is in Hz. Vectorised — accepts a scalar, 1D array or higher-dimensional array ofangle.The shape of the input
angleis preserved in the output, so this function evaluates the Karplus form for any literature parameterisation (the protein ³J(HN,Hα) sets inKARPLUS_HN_HA_COEFFICIENTS, but also Bothner-By, Tvaroska, Aydin or any other Karplus-type relation with its ownA,B,Candphi0).- Parameters:
angle (float or numpy.ndarray) – Dihedral angle(s) in degrees.
A (float) – Karplus coefficients.
B (float) – Karplus coefficients.
C (float) – Karplus coefficients.
phi0 (float, optional) – Phase offset (degrees) added to
anglebefore evaluating the Karplus form. Default0.0.
- Returns:
A cos^2(angle + phi0) + B cos(angle + phi0) + C, same shape asangle, in Hz.- Return type:
float or numpy.ndarray
Examples
>>> from soursop.ssnmr import karplus, KARPLUS_HN_HA_COEFFICIENTS >>> round(karplus(60.0, **KARPLUS_HN_HA_COEFFICIENTS["Bax2007"]), 3) 7.37 >>> import numpy as np >>> phi = np.array([-60.0, 60.0, 180.0]) >>> np.round(karplus(phi, **KARPLUS_HN_HA_COEFFICIENTS["Bax2007"]), 2) array([3.11, 7.37, 3.11])
- soursop.ssnmr.compute_J3_HN_HA(protein, model='Bax2007', stride=1, weights=False, etol=1e-07, return_uncertainty=False)[source]
Compute 3J(HN, H_alpha) scalar couplings from an SSProtein.
Evaluates the Karplus relation on the per-frame φ dihedral angles (in degrees) returned by
get_angles()using the chosen literature parameterisation. The result is a(n_frames, n_phi)matrix (per frame, per residue with a defined φ) ready to be passed ascalculated_valuestosoursop.ssbme.BMEorsoursop.sscoper.COPER. The first residue has no φ, son_phi == n_residues - 1for a single-chain protein.- Parameters:
protein (soursop.ssprotein.SSProtein) – Protein chain whose ensemble we want J-couplings for.
model (str, optional) – Karplus parameterisation. Must be a key of
KARPLUS_HN_HA_COEFFICIENTS. DefaultKARPLUS_HN_HA_DEFAULT_MODEL("Bax2007").stride (int, optional) – Subsample the trajectory by taking every
stride-th frame before evaluation. Default1.weights (numpy.ndarray or False, optional) – Optional per-frame weight vector (length
n_frames) used to collapse the frame axis to a single per-residue ensemble mean, validated bysoursop.ssutils.validate_weights()(so the usual[0, 1], finite,sum(w) == 1contract applies). Whenstrideandweightsare both given the weight vector is first subsampled and re-normalised, matching the consistent package-wide reweighting behaviour. DefaultFalse(no weighted collapse — the full(n_frames, n_phi)matrix is returned).etol (float, optional) – Tolerance for the
sum(weights) == 1check. Default1e-7.return_uncertainty (bool, optional) – If True, additionally return the Karplus-model RMSD-vs-experiment from
KARPLUS_HN_HA_UNCERTAINTIES(scalar, Hz). This is a useful default forward-model uncertainty for use with BME/COPER. DefaultFalse.
- Returns:
atom_indices (list of list of mdtraj.Atom) – The four atoms (C_{i-1}, N_i, CA_i, C_i) defining each φ dihedral.
len(atom_indices) == n_phi.J (numpy.ndarray) – ³J(HN, Hα) in Hz. Shape
(n_frames, n_phi)by default; shape(n_phi,)whenweightsis supplied (frame axis collapsed to the weighted mean).sigma (float) – Karplus-model uncertainty in Hz. Returned only if
return_uncertainty=True.
- Raises:
SSException – If
modelis not a key ofKARPLUS_HN_HA_COEFFICIENTS, or ifweightsfailssoursop.ssutils.validate_weights().
Examples
>>> # standard per-frame matrix, ready for BME / COPER >>> atoms, J = compute_J3_HN_HA(protein, model="Bax2007") >>> J.shape (n_frames, n_phi) >>> # ensemble mean (uniform weights) with the model's uncertainty >>> import numpy as np >>> w = np.full(protein.n_frames, 1.0 / protein.n_frames) >>> atoms, J_mean, sigma = compute_J3_HN_HA( ... protein, weights=w, return_uncertainty=True)
- soursop.ssnmr.compute_NOE_distances(protein, atom_pairs, stride=1)[source]
Per-frame inter-atom distances for a set of NOE atom pairs.
Thin wrapper around
mdtraj.compute_distances()that returns distances in Angstroms (the soursop convention) and the shape thatBME/COPER/BMECustomconsume directly. The raw r-values are returned per frame; collapse to a single NOE ensemble distance withnoe_ensemble_average()(or taker**-pyourself if you want the linear-additive observable to feed to BME against an experimentalr_exp**-p).- Parameters:
protein (soursop.ssprotein.SSProtein)
atom_pairs (array_like, shape (n_pairs, 2)) – Zero-based atom indices into
protein.traj.topology. (Useprotein.traj.topology.select(...)ortopology.atom(...)to translate residue/atom names into indices.)stride (int, optional) – Subsample frames before the distance computation. Default 1.
- Returns:
Distances in Angstroms, shape
(n_frames, n_pairs)(n_framesafter striding).- Return type:
numpy.ndarray
- Raises:
SSException – If
atom_pairsdoes not have shape(n_pairs, 2).
Examples
>>> import numpy as np >>> from soursop.ssnmr import compute_NOE_distances, noe_ensemble_average >>> pairs = np.array([[0, 10], [0, 20], [5, 15]]) # atom indices >>> d = compute_NOE_distances(protein, pairs) # (n_frames, 3) Å >>> r_noe = noe_ensemble_average(d, power=6) # (3,) Å
- soursop.ssnmr.noe_ensemble_average(distances, power=6, weights=False, etol=1e-07, axis=0)[source]
NOE-averaged distance across the
axisof a distance array.Implements \(\big( \sum_i w_i\, d_i^{-p} \big)^{-1/p}\) along
axis: the standard NOE r^-p ensemble convention (defaultp=6; some studies usep=3). Honours the package-wide weights= contract (seesoursop.ssutils.validate_weights()):weights=False(default) gives the uniform mean, while a vector of per-frame weights (must be lengthdistances.shape[axis], in [0, 1], finite, summing to 1) gives the weighted NOE distance.- Parameters:
distances (numpy.ndarray) – Distance array (any shape) — typically
(n_frames, n_pairs)fromcompute_NOE_distances().power (float, optional) – NOE exponent. Default 6.
weights (numpy.ndarray or False, optional) – Per-frame weights, validated by
soursop.ssutils.validate_weights(). DefaultFalse-> uniform.etol (float, optional) – Tolerance on
sum(weights) == 1. Default1e-7.axis (int, optional) – Axis to collapse. Default 0 (frame axis).
- Returns:
NOE distance(s) (Å) with
axiscollapsed.- Return type:
numpy.ndarray
- Raises:
SSException – If
weightsfails validation, or if any distance alongaxisis non-positive (sincer^-pis undefined).