2. A scanner sequence and its protocol

2. A scanner sequence and its protocol#

A scanner sequence binds a pypulseqpp sequence function to the entries of the scanner protocol. The operator edits the entries; pulserver converts their values into the function’s arguments, evaluates the protocol under the scanner limits, and returns the protocol the design achieves. The previous lesson used the shipped gre2d; this lesson writes one like it.

Learning objectives

  • Declare a SequencePlugin: the sequence function and the entries that bind its arguments, with their units and presets.

  • Write an evaluation that designs one repetition of the scan and reads back the values the design achieves, the scan time and the RF of one TR.

  • Validate requests, including a preset and a prescription the design refuses, and write the design.

The next lesson converts a design into the representation the scanner plays.

Entries#

Each entry of protocol maps a protocol key, which names a parameter of the scanner UI, to a keyword argument of the sequence function. A TimeParam is exchanged in integer microseconds and given to the function in seconds; its Minimum preset passes None, for which gre2d designs its shortest time. A FloatParam carries a unit and a scale, here mm on the UI and m for the function. Entries a protocol leaves out keep the function’s defaults.

evaluate() answers every edit, so it designs one repetition, a single phase-encoding line without dummy scans, rather than the scan. The echo time and the repetition time are the TE and TR definitions the design records; the scan time is the repetition time times the lines and dummy scans the scan plays. The RF layout is the RF of that one TR, its excitation’s amplitude proportional to the flip angle.

import inspect

import pypulseqpp as pp
from pypulseqpp.sequences.sequence.gre2D_sequence import gre2d

from pulserver.design import (
    Evaluation,
    FloatParam,
    IntParam,
    RfLayout,
    SequencePlugin,
    TimeParam,
)
from pulserver.protocol import TEPreset, TRPreset, UIParam, format_listing


class Gre2D(SequencePlugin):
    app = gre2d
    protocol = {
        UIParam.FLIP: FloatParam(
            "flip_angle_deg", unit="deg", range_min=1.0, range_max=90.0
        ),
        UIParam.TE: TimeParam(
            "te", range_min=1000, range_max=80000, presets={TEPreset.MINIMUM: None}
        ),
        UIParam.TR: TimeParam(
            "tr", range_min=1000, range_max=5_000_000, presets={TRPreset.MINIMUM: None}
        ),
        UIParam.FOV: FloatParam(
            "fov_x", unit="mm", scale=1e-3, range_min=50.0, range_max=500.0
        ),
        UIParam.NX: IntParam("n_x", range_min=32, range_max=512, range_incr=2),
        UIParam.NY: IntParam("n_y", range_min=32, range_max=512, range_incr=2),
    }

    def evaluate(self, system, protocol):
        bound = inspect.signature(self.app).bind_partial(system, **protocol.arguments)
        bound.apply_defaults()
        a = bound.arguments
        one = self.app(
            system,
            **(protocol.arguments | {"n_dummy": 0, "ry": a["n_y"], "n_acs_y": 0}),
        )
        tr = one.definitions["TR"][0]
        achieved = {UIParam.TE: one.definitions["TE"][0], UIParam.TR: tr}
        return Evaluation(
            protocol.replace(achieved),
            duration=tr * (a["n_dummy"] + a["n_y"]),
            rf_layout=RfLayout.of(one, UIParam.FLIP, period=tr),
        )


plugin = Gre2D()
print(format_listing(plugin.listing()), end="")
[Protocol]
flip: float|typein|12.0|1.0|90.0|1.0|deg
TE: int|dropdown|8000|1000|80000|1|us|-2
TR: int|dropdown|250000|1000|5000000|1|us|-1
fov: float|typein|220.0|50.0|500.0|1.0|mm
nx: int|typein|128|32|512|2|
ny: int|typein|128|32|512|2|
fov_offset_x: float|off|0.0|-1000.0|1000.0|0.1|mm
fov_offset_y: float|off|0.0|-1000.0|1000.0|0.1|mm
fov_offset_z: float|off|0.0|-1000.0|1000.0|0.1|mm
fov_rotation_11: float|off|1.0|-1.0|1.0|1e-06|
fov_rotation_12: float|off|0.0|-1.0|1.0|1e-06|
fov_rotation_13: float|off|0.0|-1.0|1.0|1e-06|
fov_rotation_21: float|off|0.0|-1.0|1.0|1e-06|
fov_rotation_22: float|off|1.0|-1.0|1.0|1e-06|
fov_rotation_23: float|off|0.0|-1.0|1.0|1e-06|
fov_rotation_31: float|off|0.0|-1.0|1.0|1e-06|
fov_rotation_32: float|off|0.0|-1.0|1.0|1e-06|
fov_rotation_33: float|off|1.0|-1.0|1.0|1e-06|
[Protocol End]

Validation#

A request is a mapping of protocol keys to wire values; the entries it leaves out take their initial values. Here the echo and repetition times request the Minimum preset, sent as its negative value, and the reply holds the times the design achieved.

system = pp.Opts(max_grad=40, grad_unit="mT/m", max_slew=150, slew_unit="T/m/s")

shortest = plugin.validate(
    system,
    {
        UIParam.TE: TEPreset.MINIMUM.value,
        UIParam.TR: TRPreset.MINIMUM.value,
        UIParam.NY: 64,
    },
)
print(f"valid: {shortest.valid}, scan time {shortest.duration:.2f} s")
print(f"TE {shortest.values[UIParam.TE]} us, TR {shortest.values[UIParam.TR]} us")
valid: True, scan time 0.62 s
TE 3400 us, TR 7720 us

The RF layout states the definitions and the instances of one TR, from which a scanner estimates the RF of a prescription without designing it.

layout = shortest.rf_layout
print(
    f"{len(layout.instances.definitions)} RF definition, {len(layout.control)} instance per TR of {layout.period * 1e3:.2f} ms, controlled by {layout.control}"
)
1 RF definition, 1 instance per TR of 7.72 ms, controlled by (<FloatKey.FLIP: 'flip'>,)

A prescription the sequence function cannot realize is invalid. The reply carries the request unchanged and, as its note, the error the design raised.

refused = plugin.validate(system, {UIParam.TE: 1000})
print(f"valid: {refused.valid}")
print(refused.info)
valid: False
the requested TE of 1.000 ms is shorter than the 3.400 ms this readout can achieve

Design#

design() evaluates a request and writes the sequence of the requested protocol as signed binary Pulseq, designing the whole scan with generate(). The design calls of the previous lesson do the same, then check and convert the design and store it.

import tempfile
from pathlib import Path

directory = Path(tempfile.mkdtemp())
validation, paths = plugin.design(system, shortest.values, directory)
print([Path(path).name for path in paths])
written = pp.Sequence()
written.read(paths[0])
print(f"{written.num_blocks} blocks")
['sequence.seq']
400 blocks

Total running time of the script: (0 minutes 0.064 seconds)

Gallery generated by Sphinx-Gallery