NMR Pulse Programme Semantic Annotation System
This annotation system is in early alpha development (currently v0.0.2). The schema, syntax, and functionality are subject to significant changes. Use with caution in production environments.
Motivation
Modern NMR pulse programmes are cryptic text files that encode complex experimental procedures, but they lack semantic information about what they actually measure. Analysis software must guess at the meaning of parameters like VALIST or VPLIST, leading to errors and requiring manual interpretation.
This annotation system embeds structured semantic metadata directly within pulse programme files as comments, enabling:
- Automated Analysis: Software can automatically understand experimental structure
- Reproducible Research: Clear provenance and citation information
- Knowledge Preservation: Experimental design captured alongside implementation
- Cross-Platform Compatibility: Vendor-agnostic semantic layer
- Collaborative Development: Track contributors and development status
Basic annotation parsing is implemented in NMRTools.jl. The system is under active development with ongoing refinement of the schema and parsing capabilities.
Design Principles
Human and Machine Readable
Annotations use simple comment syntax that NMR spectroscopists can read naturally whilst being structured enough for automated parsing.
Embedded and Immutable
Metadata travels with the pulse programme file and cannot drift out of sync with the code.
Modular and Extensible
Experiments are described as combinations of core types and features, allowing new variations without redefining the entire taxonomy.
Version Controlled
Both individual pulse programmes and the annotation system itself are versioned for reproducibility and compatibility.
Annotation Syntax
Basic Format
Annotations are extensions of Bruker comment lines, marked with ;@. Within these lines, annotations are written in YAML format. The initial ;@ will be stripped when parsing, permitting multi-line entries.
;@ parameter: text value
;@ parameter1: 0.123
;@ parameter2: [list, of, items]
;@ parameter3: {associative: array, key: value}
;@ parameter4: |
;@ This is
;@ a multi-line
;@ entry.Parameter References
Parameter and list variable names are referenced directly without special syntax:
;@ r1rho:
;@ power: VALIST # List variable
;@ duration: VPLIST # List variable
;@ offset: cnst28 # Constant parameterThe context distinguishes parameter names (letters) from literal values (numbers). NMRTools automatically resolves these references to actual values from the acqus file when loading data.
Dimension References
Dimensions use dotted path notation that references into experiment-specific parameter blocks:
;@ dimensions: [r1rho.power, r1rho.duration, f1]
;@ r1rho:
;@ power: VALIST
;@ duration: VPLISTThis creates an explicit connection: r1rho.power refers to the power field in the r1rho block, which is controlled by the VALIST parameter.
Using Annotations in NMRTools.jl
The complete annotation schema is defined in the pulse programmes repository. See the schema documentation and controlled vocabulary for details.
In NMRTools.jl, parsed annotation data is accessible via the :annotations metadata field or the annotations() convenience function.
Accessing annotation data
Annotations can be accessed using the annotations() function, which provides convenient nested access to annotation data. Dotted notation is automatically expanded:
# Load a spectrum with annotations
spec = loadnmr("path/to/annotated/experiment")
# Access all annotations
all_annotations = annotations(spec)
# Access specific annotation fields
experiment_type = annotations(spec, "experiment_type")
# Access nested dictionary fields - these are equivalent:
r1rho_duration = annotations(spec, "r1rho", "duration")
r1rho_duration = annotations(spec, "r1rho.duration")
# Access array elements by index
first_dimension = annotations(spec, "dimensions", 1)
# Deep nesting with dotted notation
calibration_start = annotations(spec, "calibration.duration.start")The annotations() function accepts both string and symbol keys, and returns nothing if the requested field does not exist.
Helper Functions
referencepulse(spec, nucleus)
Get the reference pulse calibration for a given nucleus:
# Get 19F reference pulse (returns tuple of pulse length and power)
p1, pl1 = referencepulse(spec, "19F")
# Get 1H reference pulse
p3, pl2 = referencepulse(spec, :1H)Returns nothing if no reference pulse is found for the specified nucleus.
Example: 19F R1ρ on-resonance experiment
This example experiment is annotated as follows (schema v0.0.2):
;@ schema_version: "0.0.2"
;@ sequence_version: "0.1.0"
;@ title: 19F on-resonance R1rho relaxation dispersion
;@ authors:
;@ - Chris Waudby <c.waudby@ucl.ac.uk>
;@ - Jan Overbeck
;@ created: 2020-01-01
;@ last_modified: 2025-11-15
;@ repository: github.com/waudbygroup/pulseprograms
;@ status: beta
;@ experiment_type: [r1rho, 1d]
;@ features: [on_resonance, temperature_compensation]
;@ typical_nuclei: [19F, 1H]
;@ citation:
;@ - Overbeck (2020)
;@ dimensions: [r1rho.power, r1rho.duration, f1]
;@ acquisition_order: [f1, r1rho.duration, r1rho.power]
;@ reference_pulse:
;@ - {channel: f1, pulse: p1, power: pl1}
;@ - {channel: f2, pulse: p3, power: pl2}
;@ r1rho:
;@ channel: f1
;@ power: VALIST
;@ duration: VPLIST
;@ offset: 0
;@ alignment: hard_pulseUsing annotated data, we can access the experiment metadata:
using NMRTools
# Load the annotated experiment
spec = loadnmr("path/to/experiment")
# Access experiment metadata
annotations(spec, "title") # "19F on-resonance R1rho relaxation dispersion"
annotations(spec, "experiment_type") # ["r1rho", "1d"]
annotations(spec, "features") # ["on_resonance", "temperature_compensation"]
# Access nested r1rho parameters (dotted notation works)
annotations(spec, "r1rho.power") # Vector of power values resolved from VALIST
annotations(spec, "r1rho.duration") # Vector of duration values resolved from VPLIST
annotations(spec, "r1rho", "channel") # "f1"
annotations(spec, "r1rho", "offset") # 0 (on-resonance)
# Access dimension information
annotations(spec, "dimensions") # ["r1rho.power", "r1rho.duration", "f1"]
annotations(spec, "dimensions", 1) # "r1rho.power"
# Get reference pulse calibration
p1, pl1 = referencepulse(spec, "19F")Additional Examples
CEST Experiment
;@ schema_version: "0.0.2"
;@ sequence_version: "1.0.0"
;@ title: 19F CEST
;@ experiment_type: [cest, 1d]
;@ typical_nuclei: [19F]
;@ dimensions: [cest.offset, f1]
;@ acquisition_order: [f1, cest.offset]
;@ reference_pulse:
;@ - {channel: f1, pulse: p1, power: pl1}
;@ cest:
;@ channel: f1
;@ power: pl8
;@ duration: d18
;@ offset: FQ1LISTDiffusion Experiment
;@ schema_version: "0.0.2"
;@ sequence_version: "2.0.1"
;@ title: 1H STE diffusion
;@ experiment_type: [diffusion, 1d]
;@ features: [ste, watergate]
;@ typical_nuclei: [1H]
;@ dimensions: [diffusion.gradient_strength, f1]
;@ acquisition_order: [f1, diffusion.gradient_strength]
;@ reference_pulse:
;@ - {channel: f1, pulse: p1, power: pl1}
;@ diffusion:
;@ type: bipolar
;@ coherence: [f1, 1]
;@ big_delta: d20
;@ little_delta: p31
;@ tau: d17
;@ gradient_strength: {type: linear, start: cnst1, end: cnst2, scale: gpz6}
;@ gradient_shape: gpnam6Resources
- Pulse Programme Repository
- Schema Documentation
- Controlled Vocabulary (VOCABULARY.md)
- Decision Log - Design rationale and schema evolution