Use PyPulseq for MRI sequence definitions in MR-zero
Project description
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
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, andmatplotlibto 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 viainspect.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_implwithimport pulseqzero as pp, and drop anywith pulseqzero.mr0_mode():wrappers —seq.to_mr0()andseq.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. 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
.seqfile 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:
- No test suite, no linter, no CI for correctness. The two demo scripts are the acceptance gate:
demo/main.pymust complete 30 Adam iterations with non-NaN data loss and monotonically-decreasing SAR, anddemo/write_tse.pymust produce a.seqfile that round-trips byte-for-byte against the pypulseq reference (the unified adapter guarantees this; seeto_pypulseq()in adapter/sequence.py). - 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. ExportMPLBACKEND=Aggto 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 needpypulseq==1.5.0.post1,torch,MRzeroCore,numpy, andmatplotlibalready in the env (a venv created with--system-site-packagesand an existing MR-zero install is the path of least resistance). - Warnings are single-fire.
seq.to_pypulseq()(andseq.write()/seq.plot()/ other forwarders) emit aUserWarningthe 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.
seq.to_mr0() and seq.write()
Every pulseqzero.Sequence supports both paths unconditionally:
mr0_seq = seq.to_mr0()— build anMRzeroCore.Sequencefor PDG simulation / optimization.seq.write("out.seq")— translate the internal event graph through PyPulseq and emit a.seqfile. A one-timewarnings.warnis 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()):
- 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
The following are not differentiable today (they affect pulse shape, which is materialized eagerly via PyPulseq):
- Pulse shape parameters:
durationwhen used to shape the envelope,time_bw_product,apodization,center_pos,slice_thickness(as it feeds shape generation),dwellfor pulses - Gradient waveform samples for arbitrary gradients (the scale is differentiable, the samples aren't)
Optsfields (max_grad, rasters, dead times) — intentionally numeric
Pulse-shape autograd can be added back per-factory via an opt-in flag if it ever becomes load-bearing.
5. References
-
python programming language: https://www.python.org/ ↩
-
Layton K et al: Pulseq: A rapid and hardware-independent pulse sequence prototyping framework. MRM 2017, doi: 10.1002/mrm.26235 ↩
-
Keerthi SR et al: PyPulseq: A Python Package for MRI Pulse Sequence Design. JOSS 2019, doi: 10.21105/joss.01725 ↩
-
Loktyushin A et al: MRzero - Automated discovery of MRI sequences using supervised learning. MRM 2021, doi: 10.1002/mrm.28727 ↩
-
Paszke A et al: PyTorch: An Imperative Style, High-Performance Deep Learning Library. arxiv 2019, doi: 10.48550/arXiv.1912.01703 ↩
-
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 ↩
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pulseqzero-1.0.0.tar.gz.
File metadata
- Download URL: pulseqzero-1.0.0.tar.gz
- Upload date:
- Size: 51.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.25
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
49d1f5d8f704f0312516a2b45f8e5a98ddd1c986613e6ae5a875a682af450af9
|
|
| MD5 |
3db87222858e14fcf2096834860d0efa
|
|
| BLAKE2b-256 |
5fc4d71807e45349ddfbd69d19e822105ec9939599f8f424939211b5dba0f8ba
|
File details
Details for the file pulseqzero-1.0.0-py3-none-any.whl.
File metadata
- Download URL: pulseqzero-1.0.0-py3-none-any.whl
- Upload date:
- Size: 50.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.25
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a91103d07143c695bab13f63656a96c594c99ce704e8b703043ae4b781792bfa
|
|
| MD5 |
2b1ccdf72bd1be84dded270a6ae1da7b
|
|
| BLAKE2b-256 |
14d8f3b07d89dd27bba2af63598b42fa73df6b9da3185692148756bccc240c68
|