PyPulseq: Pulseq in Python

repository·master·Indexed 42 days ago

https://github.com/imr-framework/pypulseq

A Python package for vendor-neutral MRI pulse sequence design, providing a translation of the original Matlab Pulseq framework. It allows researchers to design sequences and export them as .seq files compatible with Siemens, GE, Bruker, and Philips hardware. Version 1.5.0.post1 includes tools for pulse generation (sinc, gauss, adiabatic), gradient management, and ADC configuration, with built-in support for visualizing timing diagrams and reconstructing common sequences like GRE and TSE.

Tokens
16.9K
Snippets
55
Records
67
Agent score
53%

What's inside PyPulseq

  1. Introduction to PyPulseq

    master

    PyPulseq is a Python package designed for vendor-neutral MRI pulse sequence design. It allows researchers and designers to prototype sequences in Python and export them as .seq files. These files can be executed on Siemens, GE, or Bruker hardware by using their respective Pulseq interpreters.

    PyPulseq is a Python translation of the original Pulseq framework (originally written in Matlab).

  2. Overview of PyPulseq for MRI Pulse Sequence Design

    master

    PyPulseq is an open-source, vendor-neutral Python package designed for programming MRI pulse sequences. It allows researchers to design sequences in Python and export them as .seq files, which can then be deployed on Siemens, GE, or Bruker MRI scanners using vendor-specific interpreters (such as TOPPE for Siemens/GE or the Bruker interpreter).

    Key advantages include:

    • No compilation required: Unlike C++ frameworks, PyPulseq scripts run directly in Python.
    • No licensing fees: It is a free alternative to platforms like MATLAB.
    • Deep Learning Integration: Being Python-native, it integrates easily with modern deep learning libraries for intelligent acquisition and reconstruction.
    • Standardized Output: It uses the Pulseq .seq file format, which is human-readable, vendor-independent, and based on a system of uniquely identified events and shapes.
  3. Overview of pypulseq submodules

    master

    The pypulseq package is organized into several specialized submodules for MRI pulse sequence design. Key functional areas include:

    • Pulse Generation: Modules like make_sinc_pulse, make_gauss_pulse, make_adiabatic_pulse, make_trapezoid, and make_block_pulse for creating various RF and gradient waveforms.
    • Gradient & Ramp Management: add_gradients, add_ramps, scale_grad, and split_gradient for manipulating gradient waveforms.
    • Calculations & Utilities: calc_duration, calc_ramp, calc_rf_bandwidth, and calc_rf_center for computing physical parameters.
    • Sequence Manipulation: align for timing alignment, rotate for waveform rotation, and convert for format transformations.
    • ADC & Digital Output: make_adc and make_digital_output_pulse for configuring data acquisition and digital triggers.
    • Signal Processing & Conversion: traj_to_grad for converting trajectories to gradients and points_to_waveform for waveform generation.
  4. Understanding Pulseq concepts for PyPulseq design

    master
    Before designing pulse sequences with PyPulseq, it is strongly recommended to read the official Pulseq specification. The specification defines the fundamental concepts and primitives required to design valid pulse sequences using the PyPulseq API.
  5. Understanding the Pulseq `.seq` file format

    master

    The .seq file format is a plain-text representation of a pulse sequence designed to be human-readable and easily parsable. The hierarchy of a sequence is as follows:

    1. Events: The fundamental building blocks, which include radiofrequency (RF) pulses, magnetic field gradient waveforms, delays, or analog-to-digital converter (ADC) readout events.
    2. Shapes: Event envelopes defined by run-length encoded shapes. Each event and shape is identified by a unique integer.
    3. Blocks: A collection of one or more events occurring simultaneously.

    Custom pulse sequences are synthesized by concatenating these blocks.

  6. Handle Field of View (FOV) and k-Space steps

    master

    When defining sequences, handle the fov parameter based on dimensionality and use dimension-specific k-space step variables.

    Unpacking FOV

    For 2D sequences, fov can be a float (isotropic) or a tuple[float, float]. For 3D sequences, it can be a float or a tuple[float, float, float].

    # 2D sequences:
    fov_x, fov_y = (fov, fov) if isinstance(fov, (int, float)) else fov
    
    # 3D sequences:
    fov_x, fov_y, fov_z = (fov, fov, fov) if isinstance(fov, (int, float)) else fov

    k-Space Steps

    Always use explicit variables for k-space steps. Never use a single delta_k for multiple dimensions:

    delta_kx = 1 / fov_x
    delta_ky = 1 / fov_y
    delta_kz = 1 / fov_z  # 3D only

    Setting Definitions

    Use the unpacked values when calling seq.set_definition():

    • 2D: seq.set_definition(key='FOV', value=[fov_x, fov_y, slice_thickness])
    • 3D: seq.set_definition(key='FOV', value=[fov_x, fov_y, fov_z])
    # 2D sequences:
    fov_x, fov_y = (fov, fov) if isinstance(fov, (int, float)) else fov
    
    # 3D sequences:
    fov_x, fov_y, fov_z = (fov, fov, fov) if isinstance(fov, (int, float)) else fov
    
    delta_kx = 1 / fov_x
    delta_ky = 1 / fov_y
    
    seq.set_definition(key='FOV', value=[fov_x, fov_y, slice_thickness])
  7. Follow the PyPulseq Example Script Style Guide

    master

    When writing or refactoring PyPulseq example scripts, follow these core conventions to ensure consistency and readability:

    General Rules

    • Naming: Follow PEP 8 (lowercase_with_underscores). Use descriptive names (e.g., readout_duration instead of ro_dur).
    • Counts: Use the n_ prefix for counts (e.g., n_slices, n_echo).
    • Derived Variables: Start derived variables with the name of the parameter they relate to (e.g., te_delay, tr_min).
    • Conversions: Use np.deg2rad() for flip angle conversions; avoid manual math like * np.pi / 180.
    • Imports: Always import PyPulseq as import pypulseq as pp.

    Script Structure

    Every script should follow this specific order:

    1. Imports: Third-party imports first, then import pypulseq as pp.
    2. main() Function: Contains all sequence logic. Use positional arguments for control flags (plot, write_seq, etc.) and a bare * to force sequence parameters to be keyword-only.
    3. Docstrings: Use Numpy-style docstrings for the main function.
    4. Function Body Order:
      • Create system = pp.Opts(...).
      • Create seq = pp.Sequence(system).
      • Create events (RF, gradients, ADC, delays).
      • Calculate and round timing.
      • Construct sequence (loops over slices/phase encodes).
      • Check timing (seq.check_timing()).
      • Test report (seq.test_report()).
      • Visualization (seq.plot()).
      • Set definitions (seq.set_definition()).
      • Write file (seq.write()).
    5. if __name__ == '__main__': Block: To execute the main() function.
    import numpy as np
    import pypulseq as pp
    
    def main(
        plot: bool = False,
        test_report: bool = False,
        write_seq: bool = False,
        seq_filename: str = 'gre_pypulseq.seq',
        *,
        fov: float | tuple[float, float] = 256e-3,
        n_x: int = 64,
        n_y: int = 64,
        flip_angle_deg: float = 10,
        slice_thickness: float = 3e-3,
        tr: float = 12e-3,
        te: float = 5e-3,
    ):
        """Create a basic gradient echo (GRE) sequence.
    
        Parameters
        ----------
        plot : bool, optional
            Plot the sequence diagram. Default is False.
        ... (other params)
    
        Returns
        -------
        seq : pypulseq.Sequence
            The GRE sequence object.
        """
        # Sequence logic here
        ...
    
    if __name__ == '__main__':
        main(plot=True, write_seq=True)
  8. Install PyPulseq

    master

    You can install PyPulseq via pip or conda.

    To install the standard version:

    pip install pypulseq

    To enable sigpy functionality (required for make_sigpy_pulse.py):

    pip install pypulseq[sigpy]

    To install via conda:

    conda install conda-forge::pypulseq

    To install the latest development version from GitHub:

    pip install git+https://github.com/imr-framework/pypulseq@master
  9. Design custom pulse sequences with PyPulseq

    master

    Designing a pulse sequence in PyPulseq follows a five-step workflow:

    1. Define System Limits: Create an Opts object specifying hardware constraints like max_grad, grad_unit, max_slew, and slew_unit. Use this object to initialize a Sequence.
    2. Create Pulse Events: Use factory functions to generate RF, gradient, or ADC events. Common functions include make_sinc_pulse, make_trapezoid, and make_adc.
    3. Build the Sequence: Add events to the Sequence object using add_block(). To execute multiple events simultaneously (e.g., a gradient and an ADC readout), pass them as multiple arguments to a single add_block() call.
    4. Visualize: Call seq.plot() to see the pulse timing diagram.
    5. Export: Use seq.write('filename.seq') to generate a .seq file compatible with Pulseq interpreters for Siemens, GE, Bruker, and Philips hardware.
    import pypulseq as pp
    
    # 1. Define system limits and create sequence
    system = pp.Opts(max_grad=32, grad_unit='mT/m', max_slew=130, slew_unit='mT/m/ms')
    seq = pp.Sequence(system=system)
    
    # 2. Design events
    rf90 = pp.make_sinc_pulse(flip_angle=90, duration=2e-3, system=system, slice_thickness=5e-3, apodization=0.5, time_bw_product=4, use='excitation')
    gx = pp.make_trapezoid(channel='x', flat_area=256 * (220e-3 / 256), flat_time=6.4e-3, system=system)
    adc = pp.make_adc(num_samples=256, duration=gx.flat_time, delay=gx.rise_time, system=system)
    
    # 3. Add blocks (gx and adc are simultaneous)
    seq.add_block(rf90)
    seq.add_block(gx, adc)
    
    # 4. Visualize
    seq.plot()
    
    # 5. Export
    seq.write('demo.seq')
  10. Calculate and round timing durations correctly

    master

    When computing durations that must be rounded to the sequence's raster time, always split the calculation and the rounding into two separate steps. This prevents complex, error-prone single-line expressions.

    Correct Pattern:

    # Step 1: Calculate the raw delay
    te_delay = te - pp.calc_duration(gx_pre) - gz.fall_time - gz.flat_time / 2 - pp.calc_duration(gx) / 2
    
    # Step 2: Round to the raster time
    te_delay = np.ceil(te_delay / seq.grad_raster_time) * seq.grad_raster_time

    Avoid: Combining these into one expression makes the code harder to debug and read.

    # Good: two separate steps
    te_delay = te - pp.calc_duration(gx_pre) - gz.fall_time - gz.flat_time / 2 - pp.calc_duration(gx) / 2
    te_delay = np.ceil(te_delay / seq.grad_raster_time) * seq.grad_raster_time
  11. Construct and Add Blocks to a Sequence

    master

    A sequence is built by calling seq.add_block() repeatedly. You can pass a single block (like a delay) or multiple blocks (like an RF pulse and its slice-selection gradient) to be executed simultaneously.

    # Adding an RF pulse and its slice-selection gradient together
    seq.add_block(rf90, gz90)
    
    # Adding a gradient and an ADC readout together
    seq.add_block(gx, adc)
    
    # Adding a single delay block
    seq.add_block(delay1)