Skip to main content

Pulseq-zero

Pulseq-zero allows to define MRI sequences with the Python1 port of Pulseq2: PyPulseq3, and use them within MR-zero4. This way they are deeply integrated in a differentiable digital twin, enabling not only the simulation of the defined sequence but also the efficient optimization of any sequence parameter and any loss function, using the power of PyTorch5 and gradient-descent with backpropagation.

Pulseq-zero uses PDG6, a fast, analytical and physically exact simulation model that calculates signals that are comparable to in-vivo measurements within seconds. At the same time, the required changes to the sequence code are minimal; Pulseq-zero exports the optimized sequence works by simply using the installed PyPulseq without any interference.

Table of contents

  1. General Information
  2. Usage
  3. Development
  4. API
  5. References

1. General Information

Pulseq-zero can be cloned from this repository but is also hosted on PyPI, install it locally with:

pip install pulseqzero

[!NOTE] Pulseq-zero does not declare any runtime dependencies, but it expects pypulseq, torch, MRzeroCore, numpy, and matplotlib to already be in the environment. Pulseq-zero 1.0 is compatible with PyPulseq 1.4.x and 1.5.x. Version-specific kwargs (freq_ppm, no_signal_scaling, use_block_cache, etc.) are detected at import time via inspect.signature, so the same pulseq-zero installation adapts automatically to whichever PyPulseq version is in the environment.

Migration from 0.x: the mode-switching facade has been removed. Replace import pulseqzero; pp = pulseqzero.pp_impl with import pulseqzero as pp, and drop any with pulseqzero.mr0_mode(): wrappers — seq.to_mr0() and seq.write() both work unconditionally now.

Pulseq-zero was displayed at ESMRMB 2024! You can view the abstract here: abstract/abstract.md. This project is affiliated with MR-zero and PDG but none of the other technologies. It relies on the following amazing projects:

  • Python is the programming language used for Pulseq-zero
  • Pulseq is a vendor-agnostic library and file format for sequence definition and transfer to real systems
  • PyPulseq is the port of Pulseq to Python
  • MR-zero is a digital twin of the full measurement and reconstruction pipeline for sequence optimization and discovery
  • PyTorch is an ecosystem of tools for efficient tensor math with GPU accelleration, autograd through backpropagation and a wide variety of optimizers
  • PDG (short for Phase Distribution Graphs) is a state-of-the-art Bloch simulation that produces accurate MRI signals for any sequence, orders of magnitude faster than other approaches

2. Usage

Pulseq-zero is a drop-in replacement for PyPulseq: any existing PyPulseq script runs under pulseq-zero by swapping the import — or, if you don't want to touch the script at all, by installing a two-line sys.modules hijack in the driver (see §2 · Reuse an unmodified PyPulseq script). The same script then writes .seq files and is consumed differentiably by MR-zero — no context managers, no mode flags.

import pulseqzero as pp

# Build the sequence exactly like a PyPulseq script.
seq = pp.Sequence()
seq.add_block(pp.make_delay(10e-3))

Define the sequence as a function

Wrap the sequence code in a function so the same definition can drive both .seq export and MR-zero simulation / optimization:

def my_gre_seq(TR, TE):
    seq = pp.Sequence()
    # ... create your sequence ...
    seq.add_block(pp.make_delay(TR - 3e-3))
    # ... more sequence creation ...
    return seq

Reuse an unmodified PyPulseq script

You don't have to swap the import at all. If a sequence script still reads import pypulseq as pp, a two-line hijack in the driver redirects every pypulseq attribute lookup to pulseq-zero:

import sys
import pulseqzero
sys.modules["pypulseq"] = pulseqzero

from my_existing_pypulseq_script import main  # its `import pypulseq as pp` now resolves to pulseqzero
seq = main()
seq.to_mr0()   # differentiable — no edits to the sequence script

The demo/ workspace uses exactly this pattern: demo/write_tse.py is a near-verbatim copy of the PyPulseq 1.5 upstream TSE example and still imports pypulseq; demo/main.py installs the hijack before importing it and gets a differentiable sequence for free.

Caveats. The hijack must run before the downstream script is first imported. from pypulseq.submod import X (submodule access, e.g. pypulseq.convert) will fail unless pulseq-zero mirrors submod — top-level import pypulseq and from pypulseq import foo both work. tests/redirect.py has a ~40-line import finder that serves those too, if you need one (write_haste.py imports that way). Calls to entry points pulseq-zero deliberately doesn't wrap (adiabatic pulses, sigpy, SLR, etc.) still raise NotImplementedError with a named workaround (see §4).

Application

  • Export a .seq file and plot (goes through PyPulseq under the hood; a one-off translation warning is emitted so you notice if it fires inside a hot loop):
    seq = my_gre_seq(14e-3, 5e-3)
    seq.plot()
    seq.write("tse.seq")
    
  • Simulate with MR-zero:
    import MRzeroCore as mr0
    
    seq = my_gre_seq(14e-3, 5e-3).to_mr0()
    graph = mr0.compute_graph(seq, sim_data)
    signal = mr0.execute_graph(graph, seq, sim_data)
    reco = mr0.reco_adjoint(signal, seq.get_kspace())
    
  • Optimize sequence parameters with PyTorch:
    TR = torch.tensor(14e-3, requires_grad=True)
    TE = torch.tensor(5e-3, requires_grad=True)
    optimizer = torch.optim.Adam([TR, TE], lr=0.001)
    
    for _ in range(100):
        optimizer.zero_grad()
        seq = my_gre_seq(TR, TE).to_mr0()
        loss = my_loss(seq)
        loss.backward()
        optimizer.step()
    
    # After optimization: export using the same script.
    my_gre_seq(TR, TE).write("tse_optim.seq")
    

3. Development

The recommended dev toolchain is uv. Install it once (install instructions), then run the demos straight from the repo root:

uv run demo/main.py        # end-to-end optimization demo (needs demo/brain.npz)
uv run demo/write_tse.py   # build a TSE sequence, plot it, and emit tse_pypulseq.seq

uv run resolves the demo workspace member defined in pyproject.toml, installs the pinned demo deps (PyPulseq 1.5.0.post1, torch, MRzeroCore, matplotlib) into .venv/, and picks up the editable pulseqzero checkout — no manual pip install -e . step.

Good to know:

  • Tests. uv run --group dev pytest runs the suite in tests/ — under a minute, nothing downloaded. It asks the three questions a user of the adapter has: the .seq file written through pulseq-zero is byte-identical to the one PyPulseq writes, seq.to_mr0() simulates the same signal as writing that file and importing it into MR-zero, and derivatives reach every parameter documented as differentiable and agree with a finite difference of the same loss. It runs over ten sequences, six of them unmodified upstream PyPulseq examples (write_gre, write_haste, write_radial_gre, write_ute, write_epi_se_rs, write_tse) executed against either module through the sys.modules redirect. See tests/README.md.
  • Adapter completeness probe. demo/test_pypulseq_sequence_all_funcs.py exercises every entry point in the coverage table below and prints a side-by-side support matrix. It answers "is it there", the test suite answers "is it right".
  • CI (.github/workflows/tests.yml) runs both on every push and pull request. No linter.
  • The demos are the remaining acceptance gate. demo/main.py must complete 30 Adam iterations with non-NaN data loss and monotonically-decreasing SAR.
  • PyTorch CUDA pin. demo/pyproject.toml references the CUDA 12.6 wheel index (download.pytorch.org/whl/cu126). Swap that index URL (or remove it) if you're on CPU-only or a different CUDA version.
  • Headless plotting. seq.plot() forwards to pypulseq's plot and expects an interactive matplotlib backend. Export MPLBACKEND=Agg to run headless (Agg will render but not show — useful for CI-style runs).
  • Falling back to plain pip. If you'd rather skip uv, pip install --editable . from the root still works — but the root declares no runtime deps, so you need pypulseq==1.5.0.post1, torch, MRzeroCore, numpy, and matplotlib already in the env (a venv created with --system-site-packages and an existing MR-zero install is the path of least resistance).
  • Warnings are single-fire. seq.to_pypulseq() (and seq.write() / seq.plot() / other forwarders) emit a UserWarning the first time they run from a given call site, then stay quiet — Python's default warning filter dedupes by (message, module, lineno). If the warning fires inside an optimization loop, move the call out of the loop.

4. API

Pulseq-zero provides the whole pypulseq 1.5 API, with some notable exceptions: Some functions are not differentiable (like plotting) and just re-exports. A few methods are not provided (like sigpy pulse optimization) as they are not compatible with the approach taken by pulseq- zero.

The full API can be found in TOC.md.

Differentiable rounding

PyPulseq aligns many events to the block / gradient / ADC raster, which requires rounding — and rounding kills gradients. Pulseq-zero ships pp.round / pp.ceil / pp.floor that match PyTorch semantics but act like the identity function on the backward pass:

my_param = torch.tensor(1.5, requires_grad=True)
some_calc = pp.round(torch.sin(my_param))
some_calc.backward()

assert some_calc == 1
assert my_param.grad == torch.cos(my_param)

Use these whenever you round a timing (or any sequence quantity) that flows from an optimization parameter. For plain numeric rounding outside optimization, np.round / torch.round are fine.

See MATH.md for the full set of differentiable math helpers, including round_half_up and the internal interp used for gradient waveform resampling.

seq.to_mr0() and seq.write()

Every pulseqzero.Sequence supports both paths unconditionally:

  • mr0_seq = seq.to_mr0() — build an MRzeroCore.Sequence for PDG simulation / optimization.
  • seq.write("out.seq") — translate the internal event graph through PyPulseq and emit a .seq file. A one-time warnings.warn is raised per call so you notice if it fires inside a hot loop (move it out of the optimizer).

If you need a native PyPulseq Sequence for a one-off exotic call, seq.to_pypulseq() is the explicit escape hatch.

System (Opts) is configured once, on the Sequence

Pulseq-zero applies one system to the whole sequence: the system= you pass to pp.Sequence(system=...) (or Opts.default when you pass none). That system is authoritative — it is the one used for every event at to_mr0(), write(), and to_pypulseq().

A system= handed to an individual make_* call is honored only for that call's construction-time math (e.g. deriving a slew-limited rise_time); it is not stored on the event and not carried to conversion/export. Consequently, pulseq-zero does not support mutating the system between calls, nor using a different/derated Opts for individual events: at export the Sequence's system wins, so an event built under different limits can silently disagree with it (or fail PyPulseq's re-validation). If you genuinely need per-event limits, do that one-off through seq.to_pypulseq().

This matches the universal pulseq idiom — create one Opts at the top, pass it to the Sequence and (by convention) to each make_* call; since it is the same object, everything stays consistent. Every script in the PyPulseq example suite checked (write_gre, write_epi, write_tse, write_haste, write_radial_gre, write_ute, write_epi_se_rs) creates exactly one Opts and reuses it throughout.

Differentiability

Gradients flow through the following quantities end-to-end (set requires_grad=True and they thread through to seq.to_mr0()). One parameter of each class below is checked against a finite difference of the same loss in tests/test_differentiability.py, through sequence construction, conversion and PDG simulation:

  • RF flip_angle, phase_offset, freq_offset, delay
  • ADC phase_offset, freq_offset, delay, dwell
  • Gradient amplitude (trapezoidal and arbitrary), rise_time, flat_time, fall_time, delay
  • Block / repetition / TR / TE durations

RF pulse shapes are not differentiable. The envelope is generated eagerly by PyPulseq at construction time, so it arrives here as plain numbers; the flip angle is reattached to it as a scalar factor, computed as the grad-free ratio of the integral over the pulse to the integral over the whole shape. That is what splits the pulse in two: the flip angle carries a derivative, the shape it is applied to does not. Consequently these do not:

  • Pulse shape parameters: duration when used to shape the envelope, time_bw_product, apodization, center_pos, slice_thickness (as it feeds shape generation), dwell for pulses
  • Gradient waveform samples for arbitrary gradients (the scale is differentiable, the samples aren't)
  • Opts fields (max_grad, rasters, dead times) — intentionally numeric

This matters only if you construct and optimize pulse shapes dynamically. Sinc, Gauss, block and custom envelopes are handled correctly as long as they stay static, and pulse-shape autograd can be added back per-factory via an opt-in flag if it ever becomes load-bearing.

pTx shim_array (optional)

Every RF factory (make_block_pulse, make_gauss_pulse, make_sinc_pulse, make_arbitrary_rf) accepts an optional shim_array kwarg: a per-channel transmit shim, carried through unchanged to to_mr0() / to_pypulseq(). It is only meaningful together with Martin's pTx fork of pypulseq (which adds set_tx_mode); pulseq-zero re-exports set_tx_mode when that fork is detected at import time, and is a no-op for everyone else. Regular single-channel sequences can ignore this parameter entirely.

5. References

  1. python programming language: https://www.python.org/

  2. Layton K et al: Pulseq: A rapid and hardware-independent pulse sequence prototyping framework. MRM 2017, doi: 10.1002/mrm.26235

  3. Keerthi SR et al: PyPulseq: A Python Package for MRI Pulse Sequence Design. JOSS 2019, doi: 10.21105/joss.01725

  4. Loktyushin A et al: MRzero - Automated discovery of MRI sequences using supervised learning. MRM 2021, doi: 10.1002/mrm.28727

  5. Paszke A et al: PyTorch: An Imperative Style, High-Performance Deep Learning Library. arxiv 2019, doi: 10.48550/arXiv.1912.01703

  6. Endres J et al: Phase distribution graphs for fast, differentiable, and spatially encoded Bloch simulations of arbitrary MRI sequences. MRM 2024, doi: 10.1002/mrm.30055

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pulseqzero-1.0.4.tar.gz (58.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pulseqzero-1.0.4-py3-none-any.whl (53.3 kB view details)

Uploaded Python 3

File details

Details for the file pulseqzero-1.0.4.tar.gz.

File metadata

  • Download URL: pulseqzero-1.0.4.tar.gz
  • Upload date:
  • Size: 58.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.25

File hashes

Hashes for pulseqzero-1.0.4.tar.gz
Algorithm Hash digest
SHA256 f670eb02c292462ee814d21773170ba2b92ac1f6ad1d5acceb7e3a9ab17ea79c
MD5 cca07c3224cc23de108e24f67b4c7a32
BLAKE2b-256 e4548e883c7cce40c4f31e2ea7c0b5e1703566bbc5f208dfccc5f9845f308ed4

See more details on using hashes here.

File details

Details for the file pulseqzero-1.0.4-py3-none-any.whl.

File metadata

  • Download URL: pulseqzero-1.0.4-py3-none-any.whl
  • Upload date:
  • Size: 53.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.25

File hashes

Hashes for pulseqzero-1.0.4-py3-none-any.whl
Algorithm Hash digest
SHA256 99610a0cdb7cfd8b246a3ea0191985f40b20dd7ff619db105c7d38561f3a4f85
MD5 dcd0fdcd01a3e6e2fb49070624cab60f
BLAKE2b-256 96811b6535cc91accb2c3ffdf20cb6e448278445797737af5dd153c36a23c17f

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

1.0.4 This release

2 files

1.0.3

2 files

1.0.1

2 files

1.0.0

2 files

0.2.1

2 files

0.1.11

2 files

0.1.10

2 files

0.1.9

2 files

0.1.8

2 files

0.1.7

2 files

0.1.6

2 files

0.1.5

2 files

0.1.4

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page