Note
Go to the end to download the full example code.
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)