Analysis of pure-shift NMR data for absolute metabolite quantification. Reference library and model mixture data provided as JCAMP-DX files. Goal: replicate results from paper's Table S5 and SI.
Original (incorrect) formula in quantify_all_metabolites_v3_ref20.py:
calc_conc = ref_conc * scale * scale_tsp # DOUBLE TSP CORRECTION - WRONGCorrect formula:
# For TSP-normalized spectra:
y_ref = spec_ref[mask] / tsp_area_ref
y_samp = spec_samp[mask] / tsp_area_samp
# Lorentzian fitting gives amplitudes from TSP-normalized spectra
samp_amp = result_samp.params['amplitude'].value
ref_amp = result_ref.params['amplitude'].value
scale = samp_amp / ref_amp # This IS Scale(M)/Scale(TSP)
calc_conc = ref_conc * scale # CORRECT - no additional * scale_tspMathematical justification:
samp_amp = (sample_metabolite_signal / sample_TSP_area)ref_amp = (ref_metabolite_signal / ref_TSP_area)scale = samp_amp / ref_amp = Scale(M) / Scale(TSP)- Therefore:
[M] = [M]_ref × Scale(M) = [M]_ref × scale × Scale(TSP) - But since
scale = Scale(M)/Scale(TSP), then[M] = [M]_ref × scaleautomatically!
Our calculated Scale(TSP) from magnitude spectra:
| File | Scale(TSP) | vs File 10 |
|---|---|---|
| File 10 | 1.000 | reference |
| File 20 | 2.188 | 2.2× |
| File 30 | 2.765 | 2.8× |
| File 40 | 7.703 | 7.7× |
| File 50 | 3.469 | 3.5× |
| File 60 | 3.470 | 3.5× |
| File 70 | 7.597 | 7.6× |
Table S5 from paper:
| File | Scale(TSP) |
|---|---|
| File 10 | 1.0000 |
| File 20 | 0.9932 |
| File 30 | 0.6607 |
| File 40 | 0.8964 |
| File 50 | 0.8992 |
| File 60 | 0.9546 |
| File 70 | 0.8852 |
Range comparison:
- Our data: 1.0× to 7.7× (factor of 7.7 variation)
- Table S5: 0.66 to 1.00 (factor of 1.5 variation)
This is the central unresolved issue.
- Location:
raw_data/Reference_Raw_Date_JCAMP-DX/Alanine-Reference/ - Naming:
{10,20,30,40,50,60,70}.dx(main files) - Also odd-numbered files exist (11, 21, etc.) - purpose unclear
- Alanine concentration: 40 mM
- TSP concentration: 1 mM (from SI PDF)
- Solvent: 90% H₂O / 10% D₂O
- Spectrometer: 600 MHz
- Temperature: 300K
- Sequence: NoesypresatSAPPHIRE
Data is stored as complex pairs (real, imaginary). Two methods attempted for ppm scale calculation:
Method 1 - CORRECT (uses $O1/$SWH):
sfo1 = float(dic['$SFO1'][0]) # Spectrometer frequency in MHz
o1_hz = float(dic['$O1'][0]) # Carrier frequency in Hz
sw_hz = float(dic['$SWH'][0]) # Spectral width in Hz
o1_ppm = o1_hz / sfo1
sw_ppm = sw_hz / sfo1
ppm = np.linspace(o1_ppm + sw_ppm/2, o1_ppm - sw_ppm/2, n_points)Method 2 - INCORRECT (uses $OFFSET):
# This method produces wrong ppm scale - TSP signal not found correctly
offset_hz = float(dic['$OFFSET'][0]) # Display offset, not spectral referenceConclusion: Method 1 is correct. $OFFSET in these files is a display parameter, not the chemical shift reference.
Each file contains $PHC0 and $PHC1 phase correction values, but:
- Applying them produces spectra with 79% negative values in "absorption" mode
- Phase-corrected absorption still has large dispersion component
- These parameters do NOT produce proper pure-absorption spectra
- Authors likely used automatic phase correction in TopSpin before data export
Current validated approach (in quantify_all_metabolites_v3_ref20.py):
- Read JCAMP-DX → complex spectrum (real + imag)
- Calculate magnitude →
sqrt(real² + imag²) - Find TSP position → max of magnitude in [-0.5, 0.5] ppm region
- Shift ppm axis → center TSP at exactly 0 ppm
- Integrate TSP area →
trapz(magnitude[tsp_region])over [-0.1, 0.1] ppm - Normalize spectrum →
spectrum / tsp_area - Fit Lorentzian peaks → using lmfit to TSP-normalized spectra
- Calculate scale →
sample_amplitude / reference_amplitude - Calculate concentration →
ref_conc × scale(NO additional TSP correction)
| Chemical Shift | Assignment | Proton Count | Expected Height (normalized to TSP=1) |
|---|---|---|---|
| 0.00 ppm | TSP singlet | 9H | 1.0 (by definition) |
| 1.47 ppm | Alanine CH₃ (doublet) | 3H | ~13.3× (40 mM / 1 mM × 3/9) |
| 3.78 ppm | Alanine CH (quartet) | 1H | ~4.4× (40 mM / 1 mM × 1/9) |
| 4.7 ppm | Water (suppressed) | - | Variable |
Observed heights match expectations (CH₃ ~17×, CH ~5×), confirming TSP is correctly identified despite its much lower concentration.
| Script | Purpose | Status |
|---|---|---|
quantify_all_metabolites_v3_ref20.py |
Main quantification with corrected formula | ✅ Working, ~100% recovery |
quantify_ref_vs_integration.py |
Compares [M]R vs [M]I methods | ✅ Working |
plot_tsp_comparison.py |
Original TSP comparison plot | ✅ Uses correct method |
plot_tsp_comparison_full_range.py |
Full spectrum (WRONG ppm method initially) | |
alanine_full_range_CORRECT.py |
Full spectrum with correct ppm scale | ✅ Correct method |
plot_tsp_region_only.py |
Individual TSP region plots | ✅ Shows raw variation |
compare_tsp_methods.py |
Compares $O1 vs $OFFSET ppm methods | ✅ Demonstrates correct method |
-
Why does our Scale(TSP) vary 7.7× while Table S5 shows only 1.5× variation?
- Possible causes: a. Different TopSpin processing parameters (different phase correction) b. Baseline correction applied by authors c. Different integration method (area vs peak height vs curve fitting) d. The JCAMP export used different scaling per file e. Water suppression effectiveness varies between files
-
How did the authors calculate Scale(TSP) in Table S5?
- Need to find the exact processing pipeline they used
- May need to re-export from TopSpin with consistent parameters
- Or find additional metadata in the raw Bruker files
-
Why do the odd-numbered files exist? (11, 21, 31, etc.)
- Are they duplicates, different acquisitions, or processed differently?
-
Does the TSP variation matter for final quantification?
- Our formula
calc_conc = ref_conc × scaleis internally consistent - It gives ~100% recovery for most metabolites
- But we cannot independently verify the absolute TSP correction
- Our formula
-
Phase correction:
- Current magnitude spectra are robust but not what authors used
- Real absorption spectra after proper phase correction might show different TSP ratios
- The $PHC0/$PHC1 values in headers don't produce proper absorption mode
quantify_all_metabolites_v3_ref20.py- Current best working scriptraw_data/Reference_Raw_Date_JCAMP-DX/Alanine-Reference/- Reference dataSI.pdf- Supporting Information with Table S52_Analytical Chemistry Data.xlsx- May contain additional metadata
The core issue is the TSP signal variation discrepancy:
- We have proven the formula is correct (no double TSP correction)
- We have proven the ppm scale calculation is correct (Method 1: $O1/$SWH)
- We have proven TSP is correctly identified in the spectra
- BUT our calculated Scale(TSP) values (1.0-7.7) don't match Table S5 (0.66-1.0)
Suggested investigation paths:
- Check if odd-numbered files (11, 21, etc.) are the "correct" processed versions
- Investigate if TopSpin processing parameters differ between files
- Try alternative TSP integration methods (peak height vs area vs Lorentzian fit)
- Check if baseline correction significantly affects TSP area
- Look at the
2_Analytical Chemistry Data.xlsxfile for additional metadata - Try to extract raw Bruker parameters if available