Skip to content

Bruker NMR Reader#200

Merged
FrederikLizakJohansen merged 22 commits into
mainfrom
bruker-reader
May 4, 2026
Merged

Bruker NMR Reader#200
FrederikLizakJohansen merged 22 commits into
mainfrom
bruker-reader

Conversation

@FrederikLizakJohansen

@FrederikLizakJohansen FrederikLizakJohansen commented Apr 11, 2026

Copy link
Copy Markdown
Contributor

Add Bruker TopSpin NMR reader

What this PR does

Adds a reader for Bruker TopSpin NMR experiment folders so that solution-NMR data can be loaded into ixdat.

from ixdat import Spectrum

spec = Spectrum.read("path/to/topspin_experiment_folder", reader="bruker")

spec.plot()                    # intensity vs. chemical shift (ppm)
print(spec.metadata["NS"])        # number of scans
print(spec.metadata["SOLVENT"])   # e.g. "Urine"

spec is an NMRSpectrum (a new technique class added in this PR) and behaves like any other ixdat Spectrum.

The "TopSpin experiment folder":

The folder produced by Bruker's TopSpin NMR software. Typically looks like this:

my_experiment/
├── acqus            # acquisition parameters (text, JCAMP format)
├── fid              # raw time-domain data (binary)
├── pulseprogram
└── pdata/
    └── 1/
        ├── procs    # processing parameters (text)
        ├── 1r       # processed real spectrum (binary)
        └── 1i       # processed imaginary spectrum (binary)

You point the reader at the top folder (my_experiment/) and it figures out the rest.

How it works

The reader uses [nmrglue](https://nmrglue.readthedocs.io/) (Python package for NMR file I/O) to do the actual binary parsing. ixdat then wraps the result into its standard Spectrum data model:

  • Processed data: reads pdata/<procno>/, returns the real spectrum with a proper chemical-shift axis in ppm.
  • FID fallback: if no processed data is available (or you pass prefer_processed=False), the reader returns the magnitude of the raw FID against an index axis. Phasing/Fourier transform/referencing is left to the user.

nmrglue is not added to ixdat's required dependencies, it's only imported lazily inside BrukerNMRReader.read(). If a user tries to read a Bruker folder without it installed, they get a clear ReadError explaining how to install it (pip install nmrglue).

Metadata

All the key acquisition parameters from acqus are moved onto spec.metadata with their original Bruker names (PULPROG, SOLVENT, NS, DS, TE, BF1, SFO1, O1, SW, TD, NUC1, DATE, ...), and processing parameters from procs are exposed under proc_* prefixes (proc_SI, proc_SF, proc_OFFSET, proc_LB, proc_PHC0, ...). The full acqus and procs dictionaries are also preserved under spec.metadata["acqus"] / spec.metadata["procs"] so nothing is lost. Everything in metadata is JSON-serialisable.

The acquisition DATE is parsed into ixdat's standard unix timestamp (spec.tstamp) so NMR data can be co-plotted on a shared time axis with other techniques.

New technique class

Adds NMRSpectrum and NMRSpectrumSeries in ixdat.techniques.nmr, following the same pattern as the existing FTIRSpectrum / FTIRSpectrumSeries. Registered as "NMR" and "NMR_spectra" in TECHNIQUE_CLASSES.

Tests

A new tests/functional/test_bruker_reader.py is added.

The tests run against a real TopSpin 1D ¹H NMR dataset vendored under test_data/bruker/MTBLS1_ADG19007u_162_10/. It comes from MetaboLights study MTBLS1 (a publicly availiable human-urine 1H NMR dataset, 700 MHz, noesypr1d, 128 scans), MIT-licensed reference repository. I included full attribution in test_data/bruker/MTBLS1_ADG19007u_162_10/CREDITS.txt. Only the files strictly needed by the tests are vendored (~770 KB total).

Demo script

development_scripts/reader_demonstrators/demo_bruker_reader.py :

  1. Reads the test-data via Spectrum.read(..., reader="bruker").
  2. Prints the basics (class, technique, name, tstamp, ppm range, point count, intensity range).
  3. Prints the lifted acquisition parameters (PULPROG, SOLVENT, NUC1, BF1, NS, TE, ...) and processing parameters (SI, SF, OFFSET, LB, PHC0, ...).
  4. Plots intensity vs. chemical shift with the conventional inverted ppm axis, titled with the nucleus / pulse program / field / number-of-scans / solvent.

Files added / changed

New:

  • src/ixdat/readers/bruker.py: BrukerNMRReader implementation.
  • src/ixdat/techniques/nmr.py: NMRSpectrum / NMRSpectrumSeries.
  • tests/functional/test_bruker_reader.py
  • development_scripts/reader_demonstrators/demo_bruker_reader.py: demo
  • test_data/bruker/MTBLS1_ADG19007u_162_10/: test dataset

Modified:

  • src/ixdat/readers/__init__.py: registers "bruker" in SPECTRUM_READER_CLASSES.
  • src/ixdat/techniques/__init__.py: registers "NMR" and "NMR_spectra" in TECHNIQUE_CLASSES.
  • README.rst: adds an "NMR" row to the techniques/readers table. (EDIT: Accidentally removed the nordic-reader line and then added it back by mistake on main-branch; so we have patched it back here from main to not have merge-issues)
  • NEXT_CHANGES.rst: entries describing the new reader and the new technique classes.

Things out of scope for now:

  • 2D and higher-dimensional NMR experiments. This reader is scoped to 1D experiments for the time being. Support for 2D could be a future addition.
  • FT/phasing/referencing of raw FIDs. The FID fallback intentionally doesn't try to do this; users who need that should preprocess the data in TopSpin (or with nmrglue) and let ixdat read the processed spectrum. We could eventually incorporate this as something like a new Calculator

Adds BrukerReader (reader="bruker") for 1D Bruker TopSpin experiment
folders, using the optional nmrglue package. Reads the processed real
spectrum from pdata/<procno>/ with a chemical-shift (ppm) axis, or
falls back to the raw FID magnitude. Acquisition and processing
parameters are lifted into the spectrum's metadata, with the full
acqus/procs dictionaries preserved.

Also adds NMRSpectrum / NMRSpectrumSeries technique classes, a
demonstrator script, and a functional test suite backed by a small
real TopSpin 1H NMR dataset vendored under test_data/bruker/ (sourced
from the MIT-licensed nmrML repo, MetaboLights MTBLS1; see CREDITS.txt
in that folder for full attribution).
@FrederikLizakJohansen FrederikLizakJohansen changed the title Bruker reader Bruker NMR Reader Apr 15, 2026
@FrederikLizakJohansen

FrederikLizakJohansen commented Apr 27, 2026

Copy link
Copy Markdown
Contributor Author

Addressing this discussion preemptively since this reader also depends on an external package.

I think the lazy import at call-time is the right approach here rather than going through plugins/. The plugins system seems designed for packages that change how ixdat behaves globally. siq rewires mass spec quantification, cinfdata sets up a database connection. nmrglue is just a file parser that we need for one specific read() call. Having the user call something like plugins.activate_nmrglue() before reading a file seems like unnecessary setup.

@ScottSoren ScottSoren left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work!
Just a few questions and small changes requested here. The biggest is a request for an NMRPlotter that does the xasix inversion so the user doesn't have to (may as well), and a request to build a physically meaningful xseries based on the acquisition parameters for non-processed NMR (nice to have, not need to have).
I agree that the lazy import works fine in this case :)

Comment thread src/ixdat/readers/bruker.py Outdated
Comment thread development_scripts/reader_demonstrators/demo_bruker_reader.py Outdated
Comment thread src/ixdat/readers/bruker.py
Comment thread test_data/bruker/MTBLS1_ADG19007u_162_10/pdata/1/proc
Comment thread src/ixdat/readers/bruker.py Outdated
Comment thread src/ixdat/readers/bruker.py Outdated
Comment thread src/ixdat/readers/bruker.py
Comment thread src/ixdat/readers/bruker.py Outdated
Comment thread src/ixdat/readers/bruker.py Outdated
Comment thread src/ixdat/techniques/nmr.py Outdated

@ScottSoren ScottSoren left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great!
A trivial comment, otherwise ready to merge. I can see that there are merge conflicts - also look trivial. Can you merge it into main?

Comment thread src/ixdat/readers/bruker.py Outdated
@FrederikLizakJohansen FrederikLizakJohansen merged commit 8d37e5f into main May 4, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants