Skip to content

Parametric Definitions

Excitation

excitation

This module contains functions defining compositions of building blocks commonly used for excitation in MRI

sms_pulse

sms_pulse(
    system_specs: SystemSpec,
    single_pulse: RFPulse,
    n_slices: int,
    band_gap: Quantity,
    slice_thickness: Quantity,
    modulation_type: str = "quadrature",
) -> cmrseq.bausteine.RFPulse

Modulates the waveform of a given slice selective excitation pulse using the sigpy.mri.rf.dzrf_mb implementation.

Parameters:

Name Type Description Default
system_specs SystemSpec
required
single_pulse RFPulse

Instance of a cmrseq RFPulse or subclass

required
n_slices int

Number of slices to simultaneously excite

required
band_gap Quantity

Distance between the excited slices

required
slice_thickness Quantity

Thickness of the exited slices

required
modulation_type str

from [amplitude, phase, quadrature]

'quadrature'

Returns:

Type Description
New pulse object with multi-banded waveform
Source code in cmrseq/parametric_definitions/excitation.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
def sms_pulse(system_specs: cmrseq.SystemSpec,
              single_pulse: cmrseq.bausteine.RFPulse,
              n_slices: int,
              band_gap: Quantity,
              slice_thickness: Quantity,
              modulation_type: str = "quadrature") \
        -> cmrseq.bausteine.RFPulse:
    r"""Modulates the waveform of a given slice selective excitation pulse using
    the sigpy.mri.rf.dzrf_mb implementation.

    Parameters
    ----------
    system_specs
    single_pulse
        Instance of a cmrseq RFPulse or subclass
    n_slices
        Number of slices to simultaneously excite
    band_gap
        Distance between the excited slices
    slice_thickness
        Thickness of the exited slices
    modulation_type
        from [amplitude, phase, quadrature]

    Returns
    -------
    New pulse object with multi-banded waveform
    """

    import sigpy.mri.rf as sigpy_rf

    modulation_type_lu = {"amplitude": "amp_mod",
                          "phase": "phs_mod",
                          "quadrature": "quad_mod"}
    if modulation_type.lower() not in modulation_type_lu.keys():
        raise cmrseq.err.SequenceArgumentError(
            f"Specified value not in [{list(modulation_type_lu.keys()) + ['None', ]}]",
            argument='modulation_type')

    # convert magnitude from tesla to gauss
    time, rf_waveform = single_pulse._rf
    pulse = rf_waveform.m_as("T") * 10_000 / 2 / np.pi
    tb_product = single_pulse.bandwidth.m_as("kHz") * single_pulse.duration.m_as("ms")
    gap_factor = band_gap.m_as("m") / slice_thickness.m_as("m") * tb_product

    # Call sigpy multibanding
    mb_pulse = sigpy_rf.multiband.mb_rf(pulse.real, n_slices, gap_factor,
                                        modulation_type_lu.get(modulation_type, None))
    # Construct with RF pulse object
    mb_pulse = Quantity(mb_pulse / 10_000 * 2 * np.pi, "T").to("uT")
    pulse_block = cmrseq.bausteine.RFPulse(system_specs, f"sms_{single_pulse.name}",
                                        time=time, rf_waveform=mb_pulse,
                                        frequency_offset=single_pulse.frequency_offset,
                                        phase_offset=single_pulse.phase_offset,
                                        bandwidth=single_pulse.bandwidth,
                                        rf_events=single_pulse.rf_events)
    return pulse_block

slice_selective_sinc_pulse

slice_selective_sinc_pulse(
    system_specs: SystemSpec,
    slice_thickness: Quantity,
    flip_angle: Quantity,
    time_bandwidth_product: float = 4,
    pulse_duration: Quantity = None,
    delay: Quantity = Quantity(0.0, "ms"),
    slice_position_offset: Quantity = Quantity(0.0, "m"),
    slice_normal: ndarray = np.array([0.0, 0.0, 1.0]),
    phase_offset: Quantity = Quantity(0.0, "rad"),
) -> cmrseq.Sequence

Define slice-selective excitation using a sinc RF pulse and gradient.

Longer diagrams live in the excitation definitions guide.

Parameters:

Name Type Description Default
system_specs SystemSpec

System limits used for validation.

required
slice_thickness Quantity

Required slice thickness.

required
flip_angle Quantity

Required flip angle.

required
time_bandwidth_product float

Time-bandwidth product used to calculate RF bandwidth from duration.

4
pulse_duration Quantity

Total pulse duration. If omitted, the shortest possible duration within system limits is calculated.

None
delay Quantity

Added time offset.

Quantity(0.0, 'ms')
slice_position_offset Quantity

Positional offset in the slice-normal direction, defining the RF frequency offset.

Quantity(0.0, 'm')
slice_normal ndarray

Slice-normal direction with shape (3,).

array([0.0, 0.0, 1.0])
phase_offset Quantity

RF phase offset.

Quantity(0.0, 'rad')

Returns:

Type Description
Sequence

Sequence containing the RF pulse, slice-selection gradient, and rewinder.

Source code in cmrseq/parametric_definitions/excitation.py
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
def slice_selective_sinc_pulse(system_specs: cmrseq.SystemSpec,
                               slice_thickness: Quantity,
                               flip_angle: Quantity,
                               time_bandwidth_product: float = 4,
                               pulse_duration: Quantity = None,
                               delay: Quantity = Quantity(0., "ms"),
                               slice_position_offset: Quantity = Quantity(0., "m"),
                               slice_normal: np.ndarray = np.array([0., 0., 1.]),
                               phase_offset: Quantity = Quantity(0., "rad"),
                               ) -> cmrseq.Sequence:
    r"""Define slice-selective excitation using a sinc RF pulse and gradient.

    Longer diagrams live in the excitation definitions guide.

    Parameters
    ----------
    system_specs
        System limits used for validation.
    slice_thickness
        Required slice thickness.
    flip_angle
        Required flip angle.
    time_bandwidth_product
        Time-bandwidth product used to calculate RF bandwidth from duration.
    pulse_duration
        Total pulse duration. If omitted, the shortest possible duration within system limits is
        calculated.
    delay
        Added time offset.
    slice_position_offset
        Positional offset in the slice-normal direction, defining the RF frequency offset.
    slice_normal
        Slice-normal direction with shape `(3,)`.
    phase_offset
        RF phase offset.

    Returns
    -------
    cmrseq.Sequence
        Sequence containing the RF pulse, slice-selection gradient, and rewinder.
    """
    if pulse_duration is None:
        shortest_pulse = cmrseq.bausteine.SincRFPulse.from_shortest(system_specs=system_specs,
                                                    flip_angle=flip_angle,
                                                    time_bandwidth_product=time_bandwidth_product,
                                                    center=0.5, name="dummy", phase_offset=phase_offset)

        pulse_duration, gradient_amplitude = optimize_slice_selection(system_specs,
                                                     slice_thickness, time_bandwidth_product,
                                                     min_duration=shortest_pulse.duration)
    else:
        rf_bandwidth = time_bandwidth_product / pulse_duration.to("ms")
        gradient_amplitude = (rf_bandwidth / slice_thickness / system_specs.gamma).to("mT/m")

    frequency_offset = (system_specs.gamma.to("1/mT/ms") * slice_position_offset.to("m")
                        * gradient_amplitude.to("mT/m"))

    # Pulse is shifted by delay+rise-time after gradient definition
    rf_block = cmrseq.bausteine.SincRFPulse(system_specs=system_specs, flip_angle=flip_angle,
                                            duration=pulse_duration,
                                            time_bandwidth_product=time_bandwidth_product,
                                            frequency_offset=frequency_offset.to("Hz"),
                                            center=0.5, name="rf_excitation", phase_offset=phase_offset)
    ssgrad = cmrseq.bausteine.TrapezoidalGradient.from_fdur_amp(system_specs=system_specs,
                                                                orientation=slice_normal,
                                                                amplitude=gradient_amplitude,
                                                                flat_duration=pulse_duration,
                                                                name="slice_select")

    rf_block.shift(ssgrad.gradients[0][1])

    ssrefocus = cmrseq.bausteine.TrapezoidalGradient.from_area(
        system_specs=system_specs,
        orientation=-slice_normal,
        area=Quantity(np.abs(np.linalg.norm(ssgrad.area.m_as("mT/m*ms"), axis=-1) / 2), "mT/m*ms"),
        delay=ssgrad.gradients[0][-1],
        name="slice_select_rewind")
    seq = cmrseq.Sequence([rf_block, ssgrad, ssrefocus], system_specs=system_specs)
    if delay is not None:
        seq.shift_in_time(delay)
    return seq

optimize_slice_selection

optimize_slice_selection(
    system_specs: SystemSpec,
    slice_thickness: Quantity,
    time_bandwidth_product: float,
    min_duration: Quantity = None,
) -> (Quantity, Quantity)

Computes the shortest possible combination of RF-pulse and trapezoidal slice-selection gradient for the given time-bandwidth-product, flip angle and slice thickness.

Parameters:

Name Type Description Default
system_specs SystemSpec

SystemSpecifications

required
slice_thickness Quantity

Defines the necessary gradient amplitude

required
time_bandwidth_product float

Used to calculate the RF bandwidth from duration

required
min_duration Quantity

Manually set minimal duration (gradient flat duration) (e.g. shortest pulse duration according to peak RF power)

None

Returns:

Type Description
pulse_duration, gradient amplitude
Source code in cmrseq/parametric_definitions/excitation.py
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
def optimize_slice_selection(system_specs: cmrseq.SystemSpec,
                             slice_thickness: Quantity,
                             time_bandwidth_product: float,
                             min_duration: Quantity = None) \
                             -> (Quantity, Quantity):
    r"""Computes the shortest possible combination of RF-pulse and trapezoidal
    slice-selection gradient for the given time-bandwidth-product, flip angle and
    slice thickness.

    Parameters
    ----------
    system_specs
        SystemSpecifications
    slice_thickness
        Defines the necessary gradient amplitude
    time_bandwidth_product
        Used to calculate the RF bandwidth from duration
    min_duration
        Manually set minimal duration (gradient flat duration) (e.g. shortest pulse duration according to peak RF power)

    Returns
    -------
    pulse_duration, gradient amplitude
    """
    from scipy.optimize import minimize

    # x = [dur, grad_amp]
    # Minimize the duration of the slice selective excitation (without rewind)
    s = system_specs.max_slew.m_as("mT/m/ms")

    def _optim(x):
        rise_time = x[1] / s
        return (x[0] + 2 * rise_time)

    def _flat_dur(x):
        bandwidth = time_bandwidth_product / (x[0] + 1e-8)
        amp_for_rf = bandwidth / slice_thickness.m_as("m") / system_specs.gamma.m_as("1/mT/ms")
        return amp_for_rf - x[1]

    cons = ({'type': 'eq', 'fun': _flat_dur},
            {'type': 'ineq', 'fun': lambda x: x[0]},
            {'type': 'ineq', 'fun': lambda x: x[1]},
            {'type': 'ineq', 'fun': lambda x: -x[1] + system_specs.max_grad.m_as("mT/m")},
           )

    if min_duration is not None:
        cons = cons + ({'type': 'ineq', 'fun': lambda x: x[0] - min_duration.m_as("ms")}, )

    # initial guess
    shortest_pulse = cmrseq.bausteine.SincRFPulse.from_shortest(
                                                system_specs=system_specs,
                                                flip_angle=Quantity(45, "degree"),
                                                time_bandwidth_product=time_bandwidth_product,
                                                center=0.5, name="dummy")
    shortest_bw = shortest_pulse.bandwidth.m_as("1/ms")
    shortest_grad_amp = (Quantity(shortest_bw, "1/ms") / slice_thickness
                         / system_specs.gamma).m_as("mT/m")
    initial_guess = [shortest_bw, shortest_grad_amp]

    # Call optimization
    res = minimize(fun=_optim, x0=initial_guess, constraints=cons)
    pulse_duration = system_specs.time_to_raster(Quantity(res.x[0], "ms"))
    bandwidth = time_bandwidth_product / pulse_duration
    gradient_amplitude = (bandwidth / slice_thickness / system_specs.gamma).to("mT/m")
    return pulse_duration, gradient_amplitude

slice_selective_se_pulses

slice_selective_se_pulses(
    system_specs: SystemSpec,
    echo_time: Quantity,
    slice_thickness: Quantity,
    pulse_duration: Quantity,
    slice_orientation: ndarray,
    time_bandwidth_product: float = 4.0,
) -> cmrseq.Sequence

Define 90 and 180 degree sinc pulses with slice-selective gradients.

Parameters:

Name Type Description Default
system_specs SystemSpec

System limits used for validation.

required
echo_time Quantity

Echo time of the center of the echo.

required
slice_thickness Quantity

Required slice thickness.

required
pulse_duration Quantity

Total pulse duration.

required
slice_orientation ndarray

Slice-normal direction with shape (3,).

required
time_bandwidth_product float

Time-bandwidth product used to calculate RF bandwidth from duration.

4.0

Returns:

Type Description
Sequence

Sequence containing the excitation and refocusing pulses.

Source code in cmrseq/parametric_definitions/excitation.py
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
def slice_selective_se_pulses(system_specs: 'cmrseq.SystemSpec',
                              echo_time: Quantity,
                              slice_thickness: Quantity,
                              pulse_duration: Quantity,
                              slice_orientation: np.ndarray,
                              time_bandwidth_product: float = 4.) -> cmrseq.Sequence:
    r"""Define 90 and 180 degree sinc pulses with slice-selective gradients.

    Parameters
    ----------
    system_specs
        System limits used for validation.
    echo_time
        Echo time of the center of the echo.
    slice_thickness
        Required slice thickness.
    pulse_duration
        Total pulse duration.
    slice_orientation
        Slice-normal direction with shape `(3,)`.
    time_bandwidth_product
        Time-bandwidth product used to calculate RF bandwidth from duration.

    Returns
    -------
    cmrseq.Sequence
        Sequence containing the excitation and refocusing pulses.
    """
    excite = cmrseq.seqdefs.excitation.slice_selective_sinc_pulse(
        system_specs=system_specs,
        slice_thickness=slice_thickness,
        flip_angle=Quantity(np.pi / 2, "rad"),
        pulse_duration=pulse_duration,
        time_bandwidth_product=time_bandwidth_product,
        delay=Quantity(0., "ms"),
        slice_normal=slice_orientation)

    excitation_center_time = excite.rf_events[0][0]
    refocus_delay = excitation_center_time - pulse_duration / 2 + echo_time / 2
    refocus = cmrseq.bausteine.SincRFPulse(system_specs=system_specs,
                                           flip_angle=Quantity(np.pi, "rad"),
                                           duration=pulse_duration,
                                           time_bandwidth_product=time_bandwidth_product,
                                           center=0.5,
                                           delay=refocus_delay,
                                           name="rf_refocus")
    ss_grad = excite.get_block("slice_select_0")
    sliceselect_refocus = cmrseq.bausteine.TrapezoidalGradient(
                                                   system_specs, slice_orientation,
                                                   ss_grad.magnitude, pulse_duration,
                                                   ss_grad.rise_time,
                                                   delay=refocus.tmin - ss_grad.rise_time,
                                                   name="slice_select_refocus")

    seq = excite + cmrseq.Sequence([refocus, sliceselect_refocus], system_specs=system_specs)
    return seq

spectral_spatial_excitation

spectral_spatial_excitation(
    system_specs: SystemSpec,
    binomial_degree: int,
    total_flip_angle: Quantity,
    slice_thickness: Quantity,
    chemical_shift: float = 3.4,
    time_bandwidth_product=4.5,
) -> cmrseq.Sequence

Construct a spectral-spatial excitation sequence.

The sequence contains binomial sinc sub-pulses and trapezoidal slice-selection gradients. Legacy derivation notes and plots live in the excitation definitions guide.

Parameters:

Name Type Description Default
system_specs SystemSpec

System limits used for validation.

required
binomial_degree int

Number of sub-pulses. Degree 1 corresponds to 1-1, degree 2 to 1-2-1, and so on.

required
total_flip_angle Quantity

Total effective flip angle for on-resonant spins over all sub-pulses.

required
slice_thickness Quantity

Thickness of the spatial excitation slab.

required
chemical_shift float

Suppressed frequency as chemical shift in parts per million.

3.4
time_bandwidth_product

Time-bandwidth product used for all sinc sub-pulses.

4.5

Returns:

Type Description
Sequence

Sequence containing the spectral-spatial excitation.

Raises:

Type Description
ValueError

If the pulse composition is infeasible for the system limits and slice thickness.

Source code in cmrseq/parametric_definitions/excitation.py
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
def spectral_spatial_excitation(system_specs: cmrseq.SystemSpec, binomial_degree: int,
                                total_flip_angle: Quantity, slice_thickness: Quantity,
                                chemical_shift: float = 3.4,
                                time_bandwidth_product=4.5) -> cmrseq.Sequence:
    r"""Construct a spectral-spatial excitation sequence.

    The sequence contains binomial sinc sub-pulses and trapezoidal slice-selection gradients.
    Legacy derivation notes and plots live in the excitation definitions guide.

    Parameters
    ----------
    system_specs
        System limits used for validation.
    binomial_degree
        Number of sub-pulses. Degree 1 corresponds to 1-1, degree 2 to 1-2-1, and so on.
    total_flip_angle
        Total effective flip angle for on-resonant spins over all sub-pulses.
    slice_thickness
        Thickness of the spatial excitation slab.
    chemical_shift
        Suppressed frequency as chemical shift in parts per million.
    time_bandwidth_product
        Time-bandwidth product used for all sinc sub-pulses.

    Returns
    -------
    cmrseq.Sequence
        Sequence containing the spectral-spatial excitation.

    Raises
    ------
    ValueError
        If the pulse composition is infeasible for the system limits and slice thickness.

    """

    binomial_coeffs = np.array(
        [math.factorial(binomial_degree) // math.factorial(y) // math.factorial(binomial_degree - y)
         for y in range(binomial_degree + 1)])
    subpulse_flip_angles = total_flip_angle.to("degree") * binomial_coeffs / np.sum(binomial_coeffs,
                                                                                    keepdims=True)

    chemical_shift_freq = system_specs.gamma * system_specs.b0 * chemical_shift * 1e-6
    pulse_gap = system_specs.time_to_raster((1 / 2 / chemical_shift_freq.to("Hz")).to("ms"), "grad")

    # Check if pulses are feasible
    max_sub_fa = np.max(subpulse_flip_angles)
    max_pulse_seq = cmrseq.seqdefs.excitation.slice_selective_sinc_pulse(
                                                    system_specs,
                                                    slice_thickness=slice_thickness,
                                                    flip_angle=max_sub_fa,
                                                    time_bandwidth_product=time_bandwidth_product)

    if pulse_gap / 2 < max_pulse_seq.get_block("slice_select_0").duration:
        max_tbw = pulse_gap.to("ms") ** 2 * (
                    system_specs.gamma * slice_thickness * system_specs.max_slew).to("1/ms**2") / 32
        min_slth = (32 * time_bandwidth_product / (
                    pulse_gap ** 2 * system_specs.gamma * system_specs.max_slew)).m_as("mm")
        raise ValueError(
            f"Pulse not feasible for given system limits! Try increasing the slice"
            f" thickness > {min_slth: 1.3}mm or decreasing the "
            f"time-bandwidth-product < {max_tbw.m * 0.9: 1.4}.")

        # print(max_pulse_seq.get_block("rf_excitation_0").duration)
    # Solve quadratic equation from docstring to obtain ramptime and subsequently pulse duration
    a = 4
    b = - pulse_gap
    c = 2 * time_bandwidth_product / (system_specs.gamma * slice_thickness * system_specs.max_slew)
    radicant = b ** 2 - 4 * a * c
    ramp_dur_p = (- b + np.sqrt(radicant)) / (2 * a)
    ramp_dur_m = (- b - np.sqrt(radicant)) / (2 * a)
    ramp_dur = system_specs.time_to_raster(
        np.min(np.stack([r for r in (ramp_dur_p, ramp_dur_m) if r > Quantity(0, "ms")])), "grad")
    flat_dur = system_specs.time_to_raster((pulse_gap - 4 * ramp_dur) / 2, "grad")

    seqs = []
    for pulse_idx, fa in enumerate(subpulse_flip_angles):
        temp_seq = cmrseq.seqdefs.excitation.slice_selective_sinc_pulse(
                                                    system_specs,
                                                    slice_thickness=slice_thickness,
                                                    flip_angle=fa,
                                                    pulse_duration=flat_dur,
                                                    time_bandwidth_product=time_bandwidth_product)
        if pulse_idx < len(subpulse_flip_angles) - 1:
            temp_seq.remove_block("slice_select_rewind_0")
            temp_block = deepcopy(temp_seq.get_block("slice_select_0"))
            temp_block.scale_gradients(-1)
            temp_seq.append(temp_block)
        seqs.append(temp_seq)
    result_seq_obj = seqs[0]
    result_seq_obj.extend(seqs[1:])

    return result_seq_obj

Diffusion

diffusion

This module contains functions defining compositions of building blocks commonly used in diffusion MRI

bipolar

bipolar(
    system_specs: SystemSpec,
    dt: Quantity,
    Dt: Quantity,
    amplitude: Quantity,
    direction: ndarray,
    start_time: Quantity = Quantity(0.0, "ms"),
    rise_time: Quantity = None,
    flip_decoding: bool = False,
) -> Sequence

Define a bipolar M0-compensated diffusion gradient waveform.

Parameters:

Name Type Description Default
system_specs SystemSpec

System limits used for validation.

required
dt Quantity

Flat duration of the lobes.

required
Dt Quantity

Flat duration between lobes.

required
amplitude Quantity

Lobe amplitude.

required
direction ndarray

Gradient direction with shape (3,); normalized internally.

required
start_time Quantity

Time offset before the first lobe.

Quantity(0.0, 'ms')
rise_time Quantity

Rise and fall time. If omitted, the shortest valid system rise time is used.

None
flip_decoding bool

If True, invert the decoding lobe for spin-echo use.

False

Returns:

Type Description
Sequence

Sequence containing the bipolar gradient waveform.

Source code in cmrseq/parametric_definitions/diffusion.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
def bipolar(system_specs: SystemSpec,
            dt: Quantity,
            Dt: Quantity,
            amplitude: Quantity,
            direction: np.ndarray,
            start_time: Quantity = Quantity(0., "ms"),
            rise_time: Quantity = None,
            flip_decoding: bool = False) -> Sequence:
    r"""Define a bipolar M0-compensated diffusion gradient waveform.

    Parameters
    ----------
    system_specs
        System limits used for validation.
    dt
        Flat duration of the lobes.
    Dt
        Flat duration between lobes.
    amplitude
        Lobe amplitude.
    direction
        Gradient direction with shape `(3,)`; normalized internally.
    start_time
        Time offset before the first lobe.
    rise_time
        Rise and fall time. If omitted, the shortest valid system rise time is used.
    flip_decoding
        If `True`, invert the decoding lobe for spin-echo use.

    Returns
    -------
    Sequence
        Sequence containing the bipolar gradient waveform.
    """

    normed_direction = direction / np.linalg.norm(direction)
    if rise_time is None:
        rise_time = system_specs.get_shortest_rise_time(amplitude)

    lobe_1 = bausteine.TrapezoidalGradient(system_specs=system_specs, name="diffusion_encode",
                                           orientation=normed_direction, amplitude=amplitude,
                                           flat_duration=dt, delay=start_time, rise_time=rise_time)
    lobe_2 = bausteine.TrapezoidalGradient(system_specs=system_specs, name="diffusion_decode",
                                           orientation=-normed_direction,
                                           amplitude=amplitude, flat_duration=dt,
                                           delay=start_time+2*rise_time+dt+Dt, rise_time=rise_time)
    seq = Sequence([lobe_1, lobe_2], system_specs=system_specs)

    if flip_decoding:
        lobe_2.scale_gradients(-1)
    return seq

m012

m012(
    system_specs: SystemSpec,
    zeta: Quantity,
    lambda_: Quantity,
    direction: ndarray,
    amplitude: Quantity = None,
    bvalue: Quantity = None,
    start_time: Quantity = Quantity(0.0, "ms"),
    flip_decoding: bool = False,
) -> Union[Sequence, List[Sequence]]

Define an M012-compensated diffusion gradient waveform.

Implements the waveform described by Stoeck et al. (DOI: 10.1002/mrm.25784). Diagrams and legacy b-value plots live in the diffusion definitions guide.

Parameters:

Name Type Description Default
system_specs SystemSpec

System limits used for validation.

required
zeta Quantity

Rise time of ramps using the maximum slew rate.

required
lambda_ Quantity

Flat duration of the first trapezoidal lobe.

required
direction ndarray

Gradient direction with shape (3,) or (n, 3); normalized internally.

required
amplitude Quantity

Gradient amplitude. Specify exactly one of amplitude and bvalue.

None
bvalue Quantity

Target b-value. Specify exactly one of amplitude and bvalue.

None
start_time Quantity

Time offset before the first lobe.

Quantity(0.0, 'ms')
flip_decoding bool

If True, invert the decoding lobe for spin-echo use.

False

Returns:

Type Description
Sequence or list[Sequence]

Waveform sequence for each specified amplitude or b-value.

Raises:

Type Description
ValueError

If amplitude/b-value arguments are invalid, infeasible, or not broadcastable.

Source code in cmrseq/parametric_definitions/diffusion.py
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
def m012(system_specs: SystemSpec,
         zeta: Quantity, lambda_: Quantity,
         direction: np.ndarray,
         amplitude: Quantity = None, bvalue: Quantity = None,
         start_time: Quantity = Quantity(0., "ms"),
         flip_decoding: bool = False) -> Union[Sequence, List[Sequence]]:
    r"""Define an M012-compensated diffusion gradient waveform.

    Implements the waveform described by Stoeck et al. (DOI: 10.1002/mrm.25784). Diagrams and
    legacy b-value plots live in the diffusion definitions guide.

    Parameters
    ----------
    system_specs
        System limits used for validation.
    zeta
        Rise time of ramps using the maximum slew rate.
    lambda_
        Flat duration of the first trapezoidal lobe.
    direction
        Gradient direction with shape `(3,)` or `(n, 3)`; normalized internally.
    amplitude
        Gradient amplitude. Specify exactly one of `amplitude` and `bvalue`.
    bvalue
        Target b-value. Specify exactly one of `amplitude` and `bvalue`.
    start_time
        Time offset before the first lobe.
    flip_decoding
        If `True`, invert the decoding lobe for spin-echo use.

    Returns
    -------
    Sequence or list[Sequence]
        Waveform sequence for each specified amplitude or b-value.

    Raises
    ------
    ValueError
        If amplitude/b-value arguments are invalid, infeasible, or not broadcastable.
    """

    if (amplitude is None and bvalue is None) or (amplitude is not None and bvalue is not None):
        raise ValueError("Exactly one argument of amplitude/b-value must be specified. You "
                         f"specified neither or both!\n\t- amp:{amplitude}\n\t-bvalue:{bvalue}")

    if bvalue is not None:
        slew_ = Quantity(1., "mT/m") / zeta
        ref_bval = _m012_bval(zeta, lambda_, slew_, system_specs.gamma_rad, return_cumulative=False)
        factor = np.sqrt(bvalue.m_as("s/mm^2") / ref_bval.m_as("s/mm^2"))
        amplitude = Quantity(1., "mT/m") * factor

    amplitude = Quantity(np.array(amplitude.m).reshape(-1), amplitude.units)

    if len(direction.shape) == 1:
        direction = direction.reshape(1, 3)
    if len(direction) == 1 and len(amplitude) > 0:
        direction = np.repeat(direction, len(amplitude), 0)
    if len(direction) != len(amplitude):
        raise ValueError("Not broadcast dimensions of specified directions and amplitudes:"
                         f"\n\t\t amp: {amplitude.shape}  |  directions: {direction.shape}")

    flat1 = lambda_
    flat2 = 2*lambda_ + zeta
    flat3 = 2*zeta + lambda_
    rise = zeta
    delays = [0., 2*rise+flat1, 4*rise+flat1+flat2+flat3, 6*rise+flat1+flat2+flat3++flat2]

    seqlist = []
    for amp, direc in zip(amplitude, direction):
        lobe_kwargs = []
        for flat, delay in zip([flat1, flat2, flat2, flat1], delays):
            lobe_kwargs.append(dict(amplitude=amp, flat_duration=flat, rise_time=rise,
                                    delay=delay+start_time))
        normed_direction = direc / np.linalg.norm(direc)

        default_dir = np.array([1., 0., 0.])
        lobe_1 = bausteine.TrapezoidalGradient(system_specs=system_specs, name="diffusion_encode",
                                               orientation=default_dir, **lobe_kwargs[0])
        lobe_2 = bausteine.TrapezoidalGradient(system_specs=system_specs, name="diffusion_encode",
                                               orientation=-default_dir, **lobe_kwargs[1])
        lobe_3 = bausteine.TrapezoidalGradient(system_specs=system_specs, name="diffusion_decode",
                                               orientation=default_dir, **lobe_kwargs[2])
        lobe_4 = bausteine.TrapezoidalGradient(system_specs=system_specs, name="diffusion_decode",
                                               orientation=-default_dir, **lobe_kwargs[3])
        seq = Sequence([lobe_1, lobe_2, lobe_3, lobe_4], system_specs=system_specs)

        for lobe in [lobe_1, lobe_2, lobe_3, lobe_4]:
            lobe.gradients = (lobe.gradients[0],
                              np.einsum('n, i -> in', lobe.gradients[1][0], normed_direction))

        if flip_decoding:
            lobe_3.scale_gradients(-1)
            lobe_4.scale_gradients(-1)
        seqlist.append(seq)

    if len(amplitude) == 1:
        return seq
    else:
        return seqlist

shortest_m012

shortest_m012(
    system_specs: SystemSpec,
    direction: ndarray,
    bvalues: Quantity,
    start_time: Quantity = Quantity(0.0, "ms"),
    flip_decoding: bool = False,
) -> cmrseq.Sequence

Finds the shortest possible second order motion compensated diffusion weighting gradient waveform according to Stoeck et al. (DOI: 10.1002/mrm.25784).

Compare cmrseq.seqdefs.diffusion.m012 for more information

Parameters:

Name Type Description Default
system_specs SystemSpec
required
direction ndarray
required
bvalues Quantity
required
start_time Quantity
Quantity(0.0, 'ms')
flip_decoding bool
False

Returns:

Type Description
object
Source code in cmrseq/parametric_definitions/diffusion.py
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
def shortest_m012(system_specs: SystemSpec,
                  direction: np.ndarray, bvalues: Quantity,
                  start_time: Quantity = Quantity(0., "ms"),
                  flip_decoding: bool = False) -> cmrseq.Sequence:
    r"""Finds the shortest possible second order motion compensated diffusion weighting
    gradient waveform according to Stoeck et al. (DOI: 10.1002/mrm.25784).

    Compare cmrseq.seqdefs.diffusion.m012 for more information

    Parameters
    ----------
    system_specs
    direction
    bvalues
    start_time
    flip_decoding

    Returns
    -------
    object
    """
    max_b = np.max(bvalues.to("s/mm^2"))
    zeta, lambda_, actual_gmax, actual_smax, actual_b = _optimize_m012(system_specs, max_b)
    return m012(system_specs, zeta=zeta, lambda_=lambda_, direction=direction,
                bvalue=bvalues, start_time=start_time, flip_decoding=flip_decoding)

Velocity

velocity

This module contains functions defining compositions of building blocks commonly used in flow MRI

bipolar

bipolar(
    system_specs: SystemSpec,
    venc: Quantity,
    direction: ndarray,
    duration: Quantity = Quantity(0.0, "ms"),
    repetitions: int = 1,
    start_time: Quantity = Quantity(0.0, "ms"),
) -> cmrseq.Sequence

Define a bipolar M0-compensated velocity-encoding waveform.

Longer derivation notes and legacy plots live in the velocity definitions guide.

Parameters:

Name Type Description Default
system_specs SystemSpec

System limits used for validation.

required
venc Quantity

Velocity that corresponds to a phase accrual of 2 pi.

required
direction ndarray

Velocity encoding direction with shape (3,).

required
duration Quantity

Duration of the VENC gradients. If zero, use the shortest valid duration.

Quantity(0.0, 'ms')
repetitions int

Number of repetitions.

1
start_time Quantity

Time offset before the first gradient.

Quantity(0.0, 'ms')

Returns:

Type Description
Sequence

Sequence containing the velocity encoding waveform.

Source code in cmrseq/parametric_definitions/velocity.py
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
def bipolar(system_specs: cmrseq.SystemSpec,
            venc: Quantity,
            direction: np.ndarray,
            duration: Quantity = Quantity(0., 'ms'),
            repetitions: int = 1,
            start_time: Quantity = Quantity(0., "ms")) -> cmrseq.Sequence:
    r"""Define a bipolar M0-compensated velocity-encoding waveform.

    Longer derivation notes and legacy plots live in the velocity definitions guide.

    Parameters
    ----------
    system_specs
        System limits used for validation.
    venc
        Velocity that corresponds to a phase accrual of 2 pi.
    direction
        Velocity encoding direction with shape `(3,)`.
    duration
        Duration of the VENC gradients. If zero, use the shortest valid duration.
    repetitions
        Number of repetitions.
    start_time
        Time offset before the first gradient.

    Returns
    -------
    cmrseq.Sequence
        Sequence containing the velocity encoding waveform.
    """

    if venc == 0:
        delay = cmrseq.bausteine.Delay(system_specs=system_specs, duration=duration,
                                       name="velocity_encode_delay")
        return cmrseq.Sequence([delay], system_specs=system_specs)

    if venc < 0:
        venc = np.abs(venc)
        direction = -direction

    m1_desired = ((Quantity(np.pi, "rad") / system_specs.gamma_rad / venc).to('T*s**2/m')
                  / repetitions)

    # Start by solving equation to find flat duration for given m1 and max gradient specs
    a = system_specs.max_grad
    b = 3 * system_specs.max_grad ** 2 / system_specs.max_slew
    c = 2 * system_specs.max_grad ** 3 / system_specs.max_slew ** 2 - m1_desired

    flat_time = (-b + np.sqrt(b ** 2 - 4 * a * c)) / (2 * a)

    if flat_time < 0:  # if flat time is negative we have a triangular gradient
        flat_time = Quantity(0, 'ms')
        # Solve slightly modified equation for G (set T=0, eq 2)
        grad = ((m1_desired / 2 * system_specs.max_slew ** 2) ** (1 / 3)).to("mT/m")
        # Get rise time
        delta = grad / system_specs.max_slew
        # Round to gradient raster
        delta = round((delta / system_specs.grad_raster_time).to("dimensionless"))
        delta = delta * system_specs.grad_raster_time

        # If rounded to zero, allow one gradient raster rise time
        if delta == 0:
            delta = system_specs.grad_raster_time

        # Solve again with T=0 (eq 1)
        grad = m1_desired / 2 / delta ** 2

        # If required gradient is too strong, add extra raster time and recalculate
        if grad > system_specs.max_grad:
            delta = delta + system_specs.grad_raster_time
            grad = m1_desired / 2 / delta ** 2
        # If we now exceed slew rate, add another raster time and recalculate
        if grad / delta > system_specs.max_slew:
            delta = delta + system_specs.grad_raster_time
            grad = m1_desired / 2 / delta ** 2
        tend = 2 * delta

        # Check edge case in which adding single raster flat time is faster than triangular
        # This is due to the raster time rounding, since we require symmetric lobes
        # By adding a single raster flat time, we avoid needing to extend each lobe by 2 rasters

        delta_singflat = delta - system_specs.grad_raster_time
        flat_singflat = system_specs.grad_raster_time
        grad_singflat = delta_singflat * system_specs.max_slew

        m1_test = (grad_singflat*flat_singflat**2 +
                   3*delta_singflat*grad_singflat*flat_singflat +
                   2*grad_singflat*delta_singflat**2)

        if m1_test >= m1_desired:
            flat_time = flat_singflat
            delta = delta_singflat
            grad = grad_singflat * m1_desired / m1_test
            tend = 2 * delta + flat_time

    else:  # We have a trapezoidal gradient

        # Get flat time and rise time and round to grid
        flat_time = system_specs.time_to_raster(flat_time, raster="grad")
        delta = system_specs.get_shortest_rise_time(system_specs.max_grad)

        # Solve for first moment given flat time and max slew
        m1_cur = (3 * delta * flat_time * system_specs.max_grad
                  + system_specs.max_grad * flat_time ** 2
                  + 2 * system_specs.max_grad * delta ** 2)

        # If we are below required M1, increase flat time until we reach
        while m1_cur < m1_desired:
            flat_time = flat_time + system_specs.grad_raster_time
            m1_cur = (3 * delta * flat_time * system_specs.max_grad
                      + system_specs.max_grad * flat_time ** 2
                      + 2 * system_specs.max_grad * delta ** 2)

        # scale down gradient strength to match desired M1
        grad = system_specs.max_grad * m1_desired / m1_cur

        tend = 2 * delta + flat_time

    # User defined duration

    # Round lobe duration onto gradient raster time
    durlobe = system_specs.time_to_raster(duration / 2 / repetitions, raster="grad")
    tend = system_specs.time_to_raster(tend, raster="grad")
    # Check if duration is shorter than the previously generated the fastest gradients
    if tend > durlobe:
        if duration != 0:
            warn("Velocity Bipolar Gradient: Duration set too short")

    elif tend < durlobe:  # Duration is longer, we will generate a trapezoidal gradient

        # Solve quadratic equation to get number of max slew raster periods
        am = -durlobe * system_specs.max_slew * system_specs.grad_raster_time ** 2
        bm = durlobe ** 2 * system_specs.max_slew * system_specs.grad_raster_time
        cm = -m1_desired
        N = (-bm + np.sqrt(bm ** 2 - 4 * am * cm)) / (2 * am)

        # round up
        N = np.ceil(N)
        delta = N * system_specs.grad_raster_time
        flat_time = durlobe - 2 * delta
        grad = system_specs.max_slew * delta

        # Scale back gradient strength to match desired first moment
        m1_cur = 3 * delta * flat_time * grad + grad * flat_time ** 2 + 2 * grad * delta ** 2
        grad = grad * m1_desired / m1_cur

    rise_time = delta.to("ms")
    flat_time = flat_time.to("ms")
    amplitude = grad.to("mT/m")

    normed_direction = direction / np.linalg.norm(direction)
    lobe_1 = cmrseq.bausteine.TrapezoidalGradient(system_specs=system_specs,
                                                  orientation=-normed_direction,
                                                  amplitude=amplitude,
                                                  flat_duration=flat_time,
                                                  delay=start_time,
                                                  rise_time=rise_time,
                                                  name="velocity_encode")
    lobe_2 = cmrseq.bausteine.TrapezoidalGradient(system_specs=system_specs,
                                                  orientation=normed_direction,
                                                  amplitude=amplitude,
                                                  flat_duration=flat_time,
                                                  delay=start_time + (2 * rise_time + flat_time),
                                                  rise_time=rise_time,
                                                  name="velocity_encode")

    seq = cmrseq.Sequence([lobe_1, lobe_2], system_specs=system_specs)

    for _ in range(1, repetitions):
        lobe_1 = cmrseq.bausteine.TrapezoidalGradient(system_specs=system_specs,
                                                      orientation=-normed_direction,
                                                      amplitude=amplitude,
                                                      flat_duration=flat_time,
                                                      delay=Quantity(0., "ms"),
                                                      rise_time=rise_time,
                                                      name="velocity_encode")
        lobe_2 = cmrseq.bausteine.TrapezoidalGradient(system_specs=system_specs,
                                                      orientation=normed_direction,
                                                      amplitude=amplitude,
                                                      flat_duration=flat_time,
                                                      delay=(2 * rise_time + flat_time),
                                                      rise_time=rise_time,
                                                      name="velocity_encode")
        seq.extend([lobe_1, lobe_2], copy=False)
    time, gradient_waveform = seq.gradients_to_grid()
    return seq

flow_comp

flow_comp(
    system_specs: SystemSpec,
    venc_eff: Quantity,
    direction: ndarray,
    period: Quantity = Quantity(0.0, "ms"),
    repetitions: int = 1,
    start_time: Quantity = Quantity(0.0, "ms"),
) -> cmrseq.Sequence

Define concatenated trapezoids with flow compensation over the full duration.

Parameters:

Name Type Description Default
system_specs SystemSpec

System limits used for validation.

required
venc_eff Quantity

Effective velocity encoding target.

required
direction ndarray

Compensation direction with shape (3,).

required
period Quantity

Total period to compensate.

Quantity(0.0, 'ms')
repetitions int

Number of repetitions.

1
start_time Quantity

Time offset before the first gradient.

Quantity(0.0, 'ms')

Returns:

Type Description
Sequence

Sequence containing the flow-compensated waveform.

Source code in cmrseq/parametric_definitions/velocity.py
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
def flow_comp(system_specs: cmrseq.SystemSpec,
              venc_eff: Quantity,
              direction: np.ndarray,
              period: Quantity = Quantity(0., 'ms'),
              repetitions: int = 1,
              start_time: Quantity = Quantity(0., "ms")) -> cmrseq.Sequence:
    r"""Define concatenated trapezoids with flow compensation over the full duration.

    Parameters
    ----------
    system_specs
        System limits used for validation.
    venc_eff
        Effective velocity encoding target.
    direction
        Compensation direction with shape `(3,)`.
    period
        Total period to compensate.
    repetitions
        Number of repetitions.
    start_time
        Time offset before the first gradient.

    Returns
    -------
    cmrseq.Sequence
        Sequence containing the flow-compensated waveform.
    """

    if venc_eff == 0:
        delay = cmrseq.bausteine.Delay(system_specs=system_specs, duration=period*repetitions,
                                       name="flowcomp_encode_delay")
        return cmrseq.Sequence([delay],system_specs=system_specs)

    if venc_eff<0:
        venc_eff = np.abs(venc_eff)
        direction = -direction

    m1_eff = (Quantity(np.pi, "rad") / system_specs.gamma_rad / venc_eff).to('T*s**2/m')

    # Start by solving for the fastest possible flow compensated gradient

    # Solve quadratic equation to determine the flat time required for the given max first moment.
    # Assumes trapezoidal gradients
    a = system_specs.max_grad * (repetitions - 1 / 8)
    b = system_specs.max_grad ** 2 / system_specs.max_slew * (3 * repetitions)
    c = system_specs.max_grad ** 3 / system_specs.max_slew ** 2 * (2 * repetitions + 1 / 8) - m1_eff

    flat_time = (-b + np.sqrt(b ** 2 - 4 * a * c)) / (2 * a)

    if flat_time < 0:
        # Since the solution was negative, we will assume a fully triangular gradient

        # Solve for gradient strength with triangular gradients
        grad = ((m1_eff / (2 * repetitions - 1 + 3 / 4 * np.sqrt(2))
                 * system_specs.max_slew ** 2) ** (1 / 3)).to("mT/m")

        delta = grad / system_specs.max_slew #rise time

        delta = system_specs.time_to_raster(delta, raster="grad") #round rise time up to raster

        # recalculate gradient with rounded raster
        grad = (m1_eff / (delta ** 2 * (2 * repetitions - 1 + 3 / 4 * np.sqrt(2)))).to("mT/m")

        # Check if max gradient strength is exceeded somehow
        if grad > system_specs.max_grad:
            # add extra raster time to rise time to decrease gradient strength and recalculate
            delta = delta + system_specs.grad_raster_time
            grad = m1_eff / (delta ** 2 * (2 * repetitions - 1 + 3 / 4 * np.sqrt(2)))
        # If we now exceed slew rate, add another raster time and recalculate
        if grad / delta > system_specs.max_slew:
            delta = delta + system_specs.grad_raster_time
            grad = m1_eff / (delta ** 2 * (2 * repetitions - 1 + 3 / 4 * np.sqrt(2)))

        # round side lobe duration
        delta_side = system_specs.time_to_raster(delta / np.sqrt(2), raster="grad")

        # recalculate side lobe gradient strength
        grad_side = ((-m1_eff + grad * delta ** 2 * (np.sqrt(2) + 2 * repetitions - 1))
                     / delta_side ** 2).to("mT/m")

        flat_time = Quantity(0., "ms")
        flat_time_ends = Quantity(0., "ms")
        amplitude_ends = grad_side.to("mT/m")
        rise_time_ends = delta_side.to("ms")

    elif flat_time - system_specs.max_grad / system_specs.max_slew < 0:
        # trapezoidal, but first and last lobes are triangular

        # In this case the polynomial gains a square root term, making the analytic solution much
        # more involved. Instead we will ignore the root term to get a starting point and do a few
        # steps of Newton's method to get to a good approximation of the solution
        a = system_specs.max_grad * (repetitions - 1 / 2)
        b = system_specs.max_grad ** 2 / system_specs.max_slew * (3 * repetitions - 3 / 2)
        c = system_specs.max_grad ** 3 / system_specs.max_slew ** 2 * (2 * repetitions - 1) - m1_eff

        flat_time = (-b + np.sqrt(b ** 2 - 4 * a * c)) / (2 * a)

        delta = system_specs.get_shortest_rise_time(system_specs.max_grad)
        # now do a few iterative loops to converge on actual flat time
        for _ in range(3):
            # calculate maximum first moment
            m1_max = system_specs.max_grad * ((3 / 2 * (flat_time + delta)
                                               * np.sqrt((flat_time + delta) * delta / 2))
                                              + flat_time ** 2 * (repetitions - 1 / 2)
                                              + flat_time * delta * (3 * repetitions - 3 / 2)
                                              + delta ** 2 * (2 * repetitions - 1))
            # Calculate the gradient of the first moment with respect to the flat time
            dMdT = system_specs.max_grad * (9 / 4 * np.sqrt((flat_time + delta) * delta / 2)
                                            + flat_time * (2 * repetitions - 1)
                                            + delta * (3 * repetitions - 3 / 2))
            # Update flat time according to Newton's method
            flat_time = ((m1_eff - m1_max) / (dMdT)).to("ms") + flat_time

        # round onto raster time
        flat_time = system_specs.time_to_raster(flat_time, raster="grad")

        # calculate first moment
        m1_max = system_specs.max_grad * ((3 / 2 * (flat_time + delta)
                                           * np.sqrt((flat_time + delta) * delta / 2))
                                          + flat_time ** 2 * (repetitions - 1 / 2)
                                          + flat_time * delta * (3 * repetitions - 3 / 2)
                                          + delta ** 2 * (2 * repetitions - 1))

        # If we are below required M1, increase flat time by raster time until we reach desired M1
        while m1_max < m1_eff:
            flat_time = flat_time + system_specs.grad_raster_time
            m1_max = system_specs.max_grad * ((3 / 2 * (flat_time + delta)
                                               * np.sqrt((flat_time + delta) * delta / 2))
                                              + flat_time ** 2 * (repetitions - 1 / 2)
                                              + flat_time * delta * (3 * repetitions - 3 / 2)
                                              + delta ** 2 * (2 * repetitions - 1))

        # scale down gradient strength to match desired M1
        grad = system_specs.max_grad * m1_eff / m1_max

        # now need to check sidelobes

        # Get sidelobe duration, assuming triangular
        delta_side = np.sqrt((flat_time + delta) * delta / 2)
        # Round to raster
        delta_side = system_specs.time_to_raster(delta_side, raster="grad")

        # final check

        # Calculate current max M1
        m1_max = grad * (3 / 2 * (flat_time + delta) * delta_side +
                         flat_time ** 2 * (repetitions - 1 / 2) +
                         flat_time * delta * (3 * repetitions - 3 / 2) +
                         delta ** 2 * (2 * repetitions - 1))

        # Calculate final gradient strengths
        grad = grad * m1_eff / m1_max
        grad_side = grad / 2 * (flat_time + delta) / delta_side

        flat_time_ends = Quantity(0., "ms")
        amplitude_ends = grad_side.to("mT/m")
        rise_time_ends = delta_side.to("ms")

    else:
        # fully trapezoidal
        flat_time = system_specs.time_to_raster(flat_time, raster="grad")
        # Rise time is the fastest possible
        delta = system_specs.get_shortest_rise_time(system_specs.max_grad)

        # if (flat_time-delta)/2 is not on the grid, need to increase flat time by single raster
        if (round(((flat_time - delta) / system_specs.grad_raster_time).m_as("dimensionless")) % 2
                != 0):
            flat_time = flat_time + system_specs.grad_raster_time

        # Solve for first moment given flat time and max slew
        m1_max = system_specs.max_grad * ((repetitions - 1 / 8) * flat_time ** 2
                                          + 3 * repetitions * flat_time * delta
                                          + (2 * repetitions + 1 / 8) * delta ** 2)

        # If we are below required M1, increase flat time until we reach
        while m1_max < m1_eff:
            flat_time = flat_time + 2 * system_specs.grad_raster_time
            m1_max = system_specs.max_grad * ((repetitions - 1 / 8) * flat_time ** 2
                                              + 3 * repetitions * flat_time * delta
                                              + (2 * repetitions + 1 / 8) * delta ** 2)

        # scale down gradient strength to match desired M1
        grad = system_specs.max_grad * m1_eff / m1_max

        flat_time_ends = system_specs.time_to_raster((flat_time - delta) / 2, raster="grad")
        amplitude_ends = grad.to("mT/m")

        if ((flat_time - system_specs.max_grad / system_specs.max_slew) / 2
                < system_specs.grad_raster_time):
            # our final gradient is too short, increase flat time
            flat_time = flat_time + 2 * system_specs.grad_raster_time
            m1_max = system_specs.max_grad * ((repetitions - 1 / 8) * flat_time ** 2
                                              + 3 * repetitions * flat_time * delta +
                                              (2 * repetitions + 1 / 8) * delta ** 2)
            grad = grad * m1_eff / m1_max
            flat_time_ends = system_specs.time_to_raster((flat_time - delta) / 2, raster="grad")
            amplitude_ends = grad.to("mT/m")
        rise_time_ends = delta.to("ms")

    rise_time = delta.to("ms")
    flat_time = flat_time.to("ms")
    amplitude = grad.to("mT/m")

    lobe_time = system_specs.time_to_raster(period / 2, raster="grad")

    # If the user has defined a lobe time, check if it is possible
    if flat_time + 2 * rise_time >= lobe_time:
        # We use the previously computed fastest possible gradients and warn the user
        if period != 0:
            warn("Velocity Flow Compensated Gradient: Period set too short")
    else:
        # Specified period results in a longer gradient than the fastest possible.
        # This theoretically means that the gradient will be trapezoidal, however with raster\
        # gridding restrictions the analytic solution becomes very complicated.

        # Instead we will reformulate the M1 max equation to solve for N where N is the number of
        # raster rise times in the central lobes, with the duration of the lobes fixed according to
        # the user defined duration

        # But this is now a 3rd order polynomial... So we solve the general cubic equation :(
        # https://en.wikipedia.org/wiki/Cubic_equation
        A = (-3 / 8 * system_specs.max_slew * system_specs.grad_raster_time ** 3).m_as("ms**2*mT/m")
        B = ((1 / 2 - repetitions) * lobe_time * system_specs.max_slew
             * system_specs.grad_raster_time ** 2).m_as("ms**2*mT/m")
        C = ((repetitions - 1 / 8) * lobe_time ** 2 * system_specs.max_slew
             * system_specs.grad_raster_time).m_as("ms**2*mT/m")
        D = (- m1_eff.to("ms**2*mT/m")).m_as("ms**2*mT/m")

        # Difference of 0 and 1 resultants of the cubic and its derivatives
        Q = (2 * B ** 3 - 9 * A * B * C + 27 * A ** 2 * D) ** 2 - 4 * (B ** 2 - 3 * A * C) ** 3

        # Next we need the square root of  Q, but here we want an imaginary number if Q is negative
        if Q < 0:
            Q = 1j * (-Q) ** (1 / 2)
        else:
            Q = (Q) ** (1 / 2)

        # Some intermediate term. On the wiki this is called C
        P = (1 / 2 * (Q + 2 * B ** 3 - 9 * A * B * C + 27 * A ** 2 * D)) ** (1 / 3)

        # In our case, somehow we will only ever need this root, as it ends up being to only
        # non-negative, real root... hopefully
        root = (-B / (3 * A) + P / (6 * A) * (1 + 1j * 3 ** (1 / 2))
                + (B ** 2 - 3 * A * C) / (6 * A * P) * (1 - 1j * 3 ** (1 / 2)))

        # First root according to wikipedia.
        # r1 = -B/(3*A) - P/(3*A) - (B**2-3*A*C) / (3*A*P)

        # Another root
        # r3 = -B / (3 * A) + P / (6 * A) * (1 - 1j * 3 ** (1 / 2))
        #       + (B ** 2 - 3 * A * C) / (6 * A * P) * (
        #            1 + 1j * 3 ** (1 / 2))

        # Number of rist times needed, rounded up
        N = np.ceil(np.real(root))  # number of rise times needed

        # Calculate relevant timings
        delta = N * system_specs.grad_raster_time
        flat_time = lobe_time - 2 * delta
        grad = system_specs.max_slew * delta

        # Check if start and end lobes are trapezoidal
        if flat_time - delta < 0:
            # Start/end are triangular, but now its worse than before. So we revert to a fully
            # iterative method, increasing the number of rise times until we reach the desired
            # max M1

            # initalize times and gradient
            delta = system_specs.grad_raster_time
            flat_time = lobe_time - 2 * delta
            grad = system_specs.max_slew * delta

            # Calculate initial first moment
            m1_max = grad * ((3 / 2 * (flat_time + delta)
                              * np.sqrt((flat_time + delta) * delta / 2))
                             + flat_time ** 2 * (repetitions - 1 / 2)
                             + flat_time * delta * (3 * repetitions - 3 / 2)
                             + delta ** 2 * (2 * repetitions - 1))

            # increase rise time by raster time until we exceed desired moment
            while m1_max < m1_eff:
                delta = delta + system_specs.grad_raster_time
                flat_time = lobe_time - 2 * delta
                grad = system_specs.max_slew * delta

                m1_max = grad * ((3 / 2 * (flat_time + delta)
                                  * np.sqrt((flat_time + delta) * delta / 2))
                                 + flat_time ** 2 * (repetitions - 1 / 2)
                                 + flat_time * delta * (3 * repetitions - 3 / 2)
                                 + delta ** 2 * (2 * repetitions - 1))
            # scale back gradients to match moment
            grad = system_specs.max_grad * m1_eff / m1_max

            # Check side lobes
            delta_side = np.sqrt((flat_time + delta) * delta / 2)
            delta_side = system_specs.time_to_raster(delta_side, raster="grad")
            grad_side = grad / 2 * (flat_time + delta) / delta_side

            # final check
            m1_max = grad * (3 / 2 * (flat_time + delta) * delta_side +
                             flat_time ** 2 * (repetitions - 1 / 2) +
                             flat_time * delta * (3 * repetitions - 3 / 2) +
                             delta ** 2 * (2 * repetitions - 1))

            grad = grad * m1_eff / m1_max

            grad_side = grad / 2 * (flat_time + delta) / delta_side

            flat_time_ends = Quantity(0., "ms")
            rise_time_ends = delta_side.to("ms")
            amplitude_ends = grad_side.to("mT/m")
            flat_time = flat_time.to("ms")

        else:
            # Trapezoidal side lobes
            # if (flat_time-delta)/2 is not on the grid, need to increase flat time by single raster
            if round(((flat_time - delta) /
                      system_specs.grad_raster_time).m_as("dimensionless")) % 2 != 0:
                flat_time = flat_time + system_specs.grad_raster_time
            # calculate moment
            m1_max = grad * ((repetitions - 1 / 8) * flat_time ** 2
                             + 3 * repetitions * flat_time * delta
                             + (2 * repetitions + 1 / 8) * delta ** 2)
            # scale back gradient to match desired moment
            grad = grad * m1_eff / m1_max
            flat_time_ends = system_specs.time_to_raster((flat_time - delta) / 2, raster="grad")
            amplitude_ends = grad.to("mT/m")
            rise_time_ends = delta.to("ms")
            flat_time = flat_time.to("ms")

        rise_time = delta.to("ms")
        amplitude = grad.to("mT/m")

    # All timing calculation done, assemble gradients
    normed_direction = direction / np.linalg.norm(direction)

    # starting lobe
    lobe = cmrseq.bausteine.TrapezoidalGradient(system_specs=system_specs,
                                                orientation=-normed_direction,
                                                amplitude=amplitude_ends,
                                                flat_duration=flat_time_ends,
                                                delay=start_time,
                                                rise_time=rise_time_ends,
                                                name="flow_compensated")

    seq = cmrseq.Sequence([lobe], system_specs=system_specs)

    # iterate over middle lobes
    for di in range(0, 2 * repetitions - 1):
        lobe = cmrseq.bausteine.TrapezoidalGradient(system_specs=system_specs,
                                                    orientation=(-1) ** di * normed_direction,
                                                    amplitude=amplitude,
                                                    flat_duration=flat_time,
                                                    delay=Quantity(0., "ms"),
                                                    rise_time=rise_time,
                                                    name="flow_compensated")
        seq.append(cmrseq.Sequence([lobe], system_specs=system_specs))

    # final lobe
    lobe = cmrseq.bausteine.TrapezoidalGradient(system_specs=system_specs,
                                                orientation=-normed_direction,
                                                amplitude=amplitude_ends,
                                                flat_duration=flat_time_ends,
                                                delay=start_time,
                                                rise_time=rise_time_ends,
                                                name="flow_compensated")
    seq.append(cmrseq.Sequence([lobe], system_specs=system_specs))
    return seq

Preparation

_spinlock_prepulse

Definitions for spin-lock preparation pulses.

Each public function returns a :class:cmrseq.Sequence containing hard RF pulses and spin-lock RF blocks. Passing adc_samples > 0 preserves the historical debugging behavior of adding a centered ADC over the full preparation.

simple

simple(
    system_specs: SystemSpec,
    spin_lock_time: Quantity,
    spin_lock_frequency: Quantity,
    adc_samples: int = 0,
) -> cmrseq.Sequence

Generate a simple spin-lock preparation.

Sequence

90+x -> SL+y -> 90-x

Parameters:

Name Type Description Default
system_specs SystemSpec

System specifications used for RF rasterization and limits.

required
spin_lock_time Quantity

Total spin-lock duration.

required
spin_lock_frequency Quantity

Spin-lock frequency. Values in Hz are interpreted as cycles per second and converted to angular frequency before flip-angle calculation. Values in angular units such as rad/s are used directly.

required
adc_samples int

Number of ADC samples to add over the full preparation. If zero, no ADC block is added. Because this diagnostic ADC overlaps RF pulses, the returned sequence enables simultaneous transmit/receive validation on a copied system specification when needed.

0

Returns:

Type Description
Sequence

Spin-lock preparation sequence.

Source code in cmrseq/parametric_definitions/preparation/_spinlock_prepulse.py
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
def simple(system_specs: cmrseq.SystemSpec, spin_lock_time: Quantity,
           spin_lock_frequency: Quantity, adc_samples: int = 0) -> cmrseq.Sequence:
    r"""Generate a simple spin-lock preparation.

    Sequence
    --------
    ``90+x -> SL+y -> 90-x``

    Parameters
    ----------
    system_specs : cmrseq.SystemSpec
        System specifications used for RF rasterization and limits.
    spin_lock_time : Quantity
        Total spin-lock duration.
    spin_lock_frequency : Quantity
        Spin-lock frequency. Values in ``Hz`` are interpreted as cycles per
        second and converted to angular frequency before flip-angle calculation.
        Values in angular units such as ``rad/s`` are used directly.
    adc_samples : int, default=0
        Number of ADC samples to add over the full preparation. If zero, no ADC
        block is added. Because this diagnostic ADC overlaps RF pulses, the
        returned sequence enables simultaneous transmit/receive validation on a
        copied system specification when needed.

    Returns
    -------
    cmrseq.Sequence
        Spin-lock preparation sequence.
    """
    return _build_spinlock(system_specs, spin_lock_time, spin_lock_frequency,
                           _SPINLOCK_SEQUENCES["simple"], adc_samples)

rotary_echo

rotary_echo(
    system_specs: SystemSpec,
    spin_lock_time: Quantity,
    spin_lock_frequency: Quantity,
    adc_samples: int = 0,
) -> cmrseq.Sequence

Generate a rotary echo spin-lock preparation.

Sequence

90+x -> SL+y -> SL-y -> 90-x

Parameters:

Name Type Description Default
system_specs SystemSpec

System specifications used for RF rasterization and limits.

required
spin_lock_time Quantity

Total spin-lock duration.

required
spin_lock_frequency Quantity

Spin-lock frequency. Values in Hz are interpreted as cycles per second and converted to angular frequency before flip-angle calculation. Values in angular units such as rad/s are used directly.

required
adc_samples int

Number of ADC samples to add over the full preparation. If zero, no ADC block is added. Because this diagnostic ADC overlaps RF pulses, the returned sequence enables simultaneous transmit/receive validation on a copied system specification when needed.

0

Returns:

Type Description
Sequence

Spin-lock preparation sequence.

Source code in cmrseq/parametric_definitions/preparation/_spinlock_prepulse.py
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
def rotary_echo(system_specs: cmrseq.SystemSpec, spin_lock_time: Quantity,
                spin_lock_frequency: Quantity, adc_samples: int = 0) -> cmrseq.Sequence:
    r"""Generate a rotary echo spin-lock preparation.

    Sequence
    --------
    ``90+x -> SL+y -> SL-y -> 90-x``

    Parameters
    ----------
    system_specs : cmrseq.SystemSpec
        System specifications used for RF rasterization and limits.
    spin_lock_time : Quantity
        Total spin-lock duration.
    spin_lock_frequency : Quantity
        Spin-lock frequency. Values in ``Hz`` are interpreted as cycles per
        second and converted to angular frequency before flip-angle calculation.
        Values in angular units such as ``rad/s`` are used directly.
    adc_samples : int, default=0
        Number of ADC samples to add over the full preparation. If zero, no ADC
        block is added. Because this diagnostic ADC overlaps RF pulses, the
        returned sequence enables simultaneous transmit/receive validation on a
        copied system specification when needed.

    Returns
    -------
    cmrseq.Sequence
        Spin-lock preparation sequence.
    """
    return _build_spinlock(system_specs, spin_lock_time, spin_lock_frequency,
                           _SPINLOCK_SEQUENCES["rotary_echo"], adc_samples)

composite

composite(
    system_specs: SystemSpec,
    spin_lock_time: Quantity,
    spin_lock_frequency: Quantity,
    adc_samples: int = 0,
) -> cmrseq.Sequence

Generate a composite spin-lock preparation.

Sequence

90+x -> SL+y -> 180+y -> SL-y -> 90-x

Parameters:

Name Type Description Default
system_specs SystemSpec

System specifications used for RF rasterization and limits.

required
spin_lock_time Quantity

Total spin-lock duration.

required
spin_lock_frequency Quantity

Spin-lock frequency. Values in Hz are interpreted as cycles per second and converted to angular frequency before flip-angle calculation. Values in angular units such as rad/s are used directly.

required
adc_samples int

Number of ADC samples to add over the full preparation. If zero, no ADC block is added. Because this diagnostic ADC overlaps RF pulses, the returned sequence enables simultaneous transmit/receive validation on a copied system specification when needed.

0

Returns:

Type Description
Sequence

Spin-lock preparation sequence.

Source code in cmrseq/parametric_definitions/preparation/_spinlock_prepulse.py
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
def composite(system_specs: cmrseq.SystemSpec, spin_lock_time: Quantity,
              spin_lock_frequency: Quantity, adc_samples: int = 0) -> cmrseq.Sequence:
    r"""Generate a composite spin-lock preparation.

    Sequence
    --------
    ``90+x -> SL+y -> 180+y -> SL-y -> 90-x``

    Parameters
    ----------
    system_specs : cmrseq.SystemSpec
        System specifications used for RF rasterization and limits.
    spin_lock_time : Quantity
        Total spin-lock duration.
    spin_lock_frequency : Quantity
        Spin-lock frequency. Values in ``Hz`` are interpreted as cycles per
        second and converted to angular frequency before flip-angle calculation.
        Values in angular units such as ``rad/s`` are used directly.
    adc_samples : int, default=0
        Number of ADC samples to add over the full preparation. If zero, no ADC
        block is added. Because this diagnostic ADC overlaps RF pulses, the
        returned sequence enables simultaneous transmit/receive validation on a
        copied system specification when needed.

    Returns
    -------
    cmrseq.Sequence
        Spin-lock preparation sequence.
    """
    return _build_spinlock(system_specs, spin_lock_time, spin_lock_frequency,
                           _SPINLOCK_SEQUENCES["composite"], adc_samples)

balanced

balanced(
    system_specs: SystemSpec,
    spin_lock_time: Quantity,
    spin_lock_frequency: Quantity,
    adc_samples: int = 0,
) -> cmrseq.Sequence

Generate a balanced spin-lock preparation.

Sequence

90+x -> SL+y -> 180+y -> SL-y -> 180-y -> SL+y -> 90-x

Parameters:

Name Type Description Default
system_specs SystemSpec

System specifications used for RF rasterization and limits.

required
spin_lock_time Quantity

Total spin-lock duration.

required
spin_lock_frequency Quantity

Spin-lock frequency. Values in Hz are interpreted as cycles per second and converted to angular frequency before flip-angle calculation. Values in angular units such as rad/s are used directly.

required
adc_samples int

Number of ADC samples to add over the full preparation. If zero, no ADC block is added. Because this diagnostic ADC overlaps RF pulses, the returned sequence enables simultaneous transmit/receive validation on a copied system specification when needed.

0

Returns:

Type Description
Sequence

Spin-lock preparation sequence.

Source code in cmrseq/parametric_definitions/preparation/_spinlock_prepulse.py
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
def balanced(system_specs: cmrseq.SystemSpec, spin_lock_time: Quantity,
             spin_lock_frequency: Quantity, adc_samples: int = 0) -> cmrseq.Sequence:
    r"""Generate a balanced spin-lock preparation.

    Sequence
    --------
    ``90+x -> SL+y -> 180+y -> SL-y -> 180-y -> SL+y -> 90-x``

    Parameters
    ----------
    system_specs : cmrseq.SystemSpec
        System specifications used for RF rasterization and limits.
    spin_lock_time : Quantity
        Total spin-lock duration.
    spin_lock_frequency : Quantity
        Spin-lock frequency. Values in ``Hz`` are interpreted as cycles per
        second and converted to angular frequency before flip-angle calculation.
        Values in angular units such as ``rad/s`` are used directly.
    adc_samples : int, default=0
        Number of ADC samples to add over the full preparation. If zero, no ADC
        block is added. Because this diagnostic ADC overlaps RF pulses, the
        returned sequence enables simultaneous transmit/receive validation on a
        copied system specification when needed.

    Returns
    -------
    cmrseq.Sequence
        Spin-lock preparation sequence.
    """
    return _build_spinlock(system_specs, spin_lock_time, spin_lock_frequency,
                           _SPINLOCK_SEQUENCES["balanced"], adc_samples)

paired_self_compensated

paired_self_compensated(
    system_specs: SystemSpec,
    spin_lock_time: Quantity,
    spin_lock_frequency: Quantity,
    adc_samples: int = 0,
) -> cmrseq.Sequence

Generate a paired self-compensated spin-lock preparation.

Sequence

90+x -> SL+y -> SL-y -> 180+y -> SL+y -> SL-y -> 90-x

Parameters:

Name Type Description Default
system_specs SystemSpec

System specifications used for RF rasterization and limits.

required
spin_lock_time Quantity

Total spin-lock duration.

required
spin_lock_frequency Quantity

Spin-lock frequency. Values in Hz are interpreted as cycles per second and converted to angular frequency before flip-angle calculation. Values in angular units such as rad/s are used directly.

required
adc_samples int

Number of ADC samples to add over the full preparation. If zero, no ADC block is added. Because this diagnostic ADC overlaps RF pulses, the returned sequence enables simultaneous transmit/receive validation on a copied system specification when needed.

0

Returns:

Type Description
Sequence

Spin-lock preparation sequence.

Source code in cmrseq/parametric_definitions/preparation/_spinlock_prepulse.py
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
def paired_self_compensated(system_specs: cmrseq.SystemSpec, spin_lock_time: Quantity,
                            spin_lock_frequency: Quantity,
                            adc_samples: int = 0) -> cmrseq.Sequence:
    r"""Generate a paired self-compensated spin-lock preparation.

    Sequence
    --------
    ``90+x -> SL+y -> SL-y -> 180+y -> SL+y -> SL-y -> 90-x``

    Parameters
    ----------
    system_specs : cmrseq.SystemSpec
        System specifications used for RF rasterization and limits.
    spin_lock_time : Quantity
        Total spin-lock duration.
    spin_lock_frequency : Quantity
        Spin-lock frequency. Values in ``Hz`` are interpreted as cycles per
        second and converted to angular frequency before flip-angle calculation.
        Values in angular units such as ``rad/s`` are used directly.
    adc_samples : int, default=0
        Number of ADC samples to add over the full preparation. If zero, no ADC
        block is added. Because this diagnostic ADC overlaps RF pulses, the
        returned sequence enables simultaneous transmit/receive validation on a
        copied system specification when needed.

    Returns
    -------
    cmrseq.Sequence
        Spin-lock preparation sequence.
    """
    return _build_spinlock(system_specs, spin_lock_time, spin_lock_frequency,
                           _SPINLOCK_SEQUENCES["paired_self_compensated"], adc_samples)

totally_balanced

totally_balanced(
    system_specs: SystemSpec,
    spin_lock_time: Quantity,
    spin_lock_frequency: Quantity,
    adc_samples: int = 0,
) -> cmrseq.Sequence

Generate a totally balanced spin-lock preparation.

Sequence

90+x -> SL+y -> 180+y -> SL-y -> SL+y -> 180-y -> SL-y -> 90-x

Parameters:

Name Type Description Default
system_specs SystemSpec

System specifications used for RF rasterization and limits.

required
spin_lock_time Quantity

Total spin-lock duration.

required
spin_lock_frequency Quantity

Spin-lock frequency. Values in Hz are interpreted as cycles per second and converted to angular frequency before flip-angle calculation. Values in angular units such as rad/s are used directly.

required
adc_samples int

Number of ADC samples to add over the full preparation. If zero, no ADC block is added. Because this diagnostic ADC overlaps RF pulses, the returned sequence enables simultaneous transmit/receive validation on a copied system specification when needed.

0

Returns:

Type Description
Sequence

Spin-lock preparation sequence.

Source code in cmrseq/parametric_definitions/preparation/_spinlock_prepulse.py
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
def totally_balanced(system_specs: cmrseq.SystemSpec, spin_lock_time: Quantity,
                     spin_lock_frequency: Quantity, adc_samples: int = 0) -> cmrseq.Sequence:
    r"""Generate a totally balanced spin-lock preparation.

    Sequence
    --------
    ``90+x -> SL+y -> 180+y -> SL-y -> SL+y -> 180-y -> SL-y -> 90-x``

    Parameters
    ----------
    system_specs : cmrseq.SystemSpec
        System specifications used for RF rasterization and limits.
    spin_lock_time : Quantity
        Total spin-lock duration.
    spin_lock_frequency : Quantity
        Spin-lock frequency. Values in ``Hz`` are interpreted as cycles per
        second and converted to angular frequency before flip-angle calculation.
        Values in angular units such as ``rad/s`` are used directly.
    adc_samples : int, default=0
        Number of ADC samples to add over the full preparation. If zero, no ADC
        block is added. Because this diagnostic ADC overlaps RF pulses, the
        returned sequence enables simultaneous transmit/receive validation on a
        copied system specification when needed.

    Returns
    -------
    cmrseq.Sequence
        Spin-lock preparation sequence.
    """
    return _build_spinlock(system_specs, spin_lock_time, spin_lock_frequency,
                           _SPINLOCK_SEQUENCES["totally_balanced"], adc_samples)

totally_balanced_composite

totally_balanced_composite(
    system_specs: SystemSpec,
    spin_lock_time: Quantity,
    spin_lock_frequency: Quantity,
    adc_samples: int = 0,
) -> cmrseq.Sequence

Generate a totally balanced composite spin-lock preparation.

Sequence

90+x -> SL+y -> 90+x -> 180+y -> 90+x -> SL-y -> SL+y -> 90-x -> 180-y -> 90-x -> SL-y -> 90-x

Parameters:

Name Type Description Default
system_specs SystemSpec

System specifications used for RF rasterization and limits.

required
spin_lock_time Quantity

Total spin-lock duration.

required
spin_lock_frequency Quantity

Spin-lock frequency. Values in Hz are interpreted as cycles per second and converted to angular frequency before flip-angle calculation. Values in angular units such as rad/s are used directly.

required
adc_samples int

Number of ADC samples to add over the full preparation. If zero, no ADC block is added. Because this diagnostic ADC overlaps RF pulses, the returned sequence enables simultaneous transmit/receive validation on a copied system specification when needed.

0

Returns:

Type Description
Sequence

Spin-lock preparation sequence.

Source code in cmrseq/parametric_definitions/preparation/_spinlock_prepulse.py
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
def totally_balanced_composite(system_specs: cmrseq.SystemSpec, spin_lock_time: Quantity,
                               spin_lock_frequency: Quantity,
                               adc_samples: int = 0) -> cmrseq.Sequence:
    r"""Generate a totally balanced composite spin-lock preparation.

    Sequence
    --------
    ``90+x -> SL+y -> 90+x -> 180+y -> 90+x -> SL-y -> SL+y ->``
    ``90-x -> 180-y -> 90-x -> SL-y -> 90-x``

    Parameters
    ----------
    system_specs : cmrseq.SystemSpec
        System specifications used for RF rasterization and limits.
    spin_lock_time : Quantity
        Total spin-lock duration.
    spin_lock_frequency : Quantity
        Spin-lock frequency. Values in ``Hz`` are interpreted as cycles per
        second and converted to angular frequency before flip-angle calculation.
        Values in angular units such as ``rad/s`` are used directly.
    adc_samples : int, default=0
        Number of ADC samples to add over the full preparation. If zero, no ADC
        block is added. Because this diagnostic ADC overlaps RF pulses, the
        returned sequence enables simultaneous transmit/receive validation on a
        copied system specification when needed.

    Returns
    -------
    cmrseq.Sequence
        Spin-lock preparation sequence.
    """
    return _build_spinlock(system_specs, spin_lock_time, spin_lock_frequency,
                           _SPINLOCK_SEQUENCES["totally_balanced_composite"], adc_samples)

Readout

_cartesian_single_lines

This module contains parametric definitions for cartesian readouts, as well as helper functions associated with cartesian sequence design.

multi_line_cartesian

multi_line_cartesian(
    system_specs: SystemSpec,
    fnc: callable,
    matrix_size: ndarray,
    inplane_resolution: Quantity,
    dummy_shots: int = None,
    **kwargs,
)

Creates a list of sequences, one for each k-space_line for a given single-line-definiton e.g. se_cartesian_line, gre_cartesian_line

Example: .. code-block: python

ro_blocks = cmrseq.seqdefs.readout.multi_line_cartesian(
                            system_specs=system_specs,
                            fnc=cmrseq.seqdefs.readout.gre_cartesian_line,
                            matrix_size=matrix_size,
                            inplane_resolution=inplane_resolution,
                            adc_duration=adc_duration,
                            prephaser_duration=ss_refocus.duration,
                            dummy_shots=dummy_shots)

Parameters:

Name Type Description Default
system_specs SystemSpec

SystemSpecification

required
fnc callable

callable

required
matrix_size ndarray

array of shape (2, )

required
inplane_resolution Quantity

Quantity[Length] of shape (2, )

required
dummy_shots int

number of shots without adc-events

None
kwargs

is forwared to call fnc. may not contain num_samples, k_readout, k_phase, prephaser_duration

{}

Returns:

Type Description
object
Source code in cmrseq/parametric_definitions/readout/_cartesian_single_lines.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
def multi_line_cartesian(system_specs: cmrseq.SystemSpec,
                         fnc: callable,
                         matrix_size: np.ndarray,
                         inplane_resolution: Quantity,
                         dummy_shots: int = None, **kwargs):
    r"""Creates a list of sequences, one for each k-space_line for a given single-line-definiton
    e.g. se_cartesian_line, gre_cartesian_line

    **Example:**
    .. code-block: python

        ro_blocks = cmrseq.seqdefs.readout.multi_line_cartesian(
                                    system_specs=system_specs,
                                    fnc=cmrseq.seqdefs.readout.gre_cartesian_line,
                                    matrix_size=matrix_size,
                                    inplane_resolution=inplane_resolution,
                                    adc_duration=adc_duration,
                                    prephaser_duration=ss_refocus.duration,
                                    dummy_shots=dummy_shots)

    Parameters
    ----------
    system_specs
        SystemSpecification
    fnc
        callable
    matrix_size
        array of shape (2, )
    inplane_resolution
        Quantity[Length] of shape (2, )
    dummy_shots
        number of shots without adc-events
    kwargs
        is forwared to call fnc. may not contain num_samples, k_readout, k_phase, prephaser_duration

    Returns
    -------
    object
    """
    # kro_max = 1 / inplane_resolution[0]
    # fov_pe = matrix_size[1] * inplane_resolution[1]
    # delta_kpe = 1 / fov_pe
    # if matrix_size[1] % 2 == 1:
    #     kpes = (np.arange(0, matrix_size[1], 1) - (matrix_size[1]) // 2) * delta_kpe
    # else:
    #     kpes = (np.arange(0, matrix_size[1], 1) - (matrix_size[1] + 1) // 2) * delta_kpe

    _, kpes, kro_max = matrix_to_kspace_2d(matrix_size, inplane_resolution)

    # Figure out prephaser shortest prephaser duration for maximal k-space traverse
    prephaser_duration = kwargs.get("prephaser_duration", None)
    if prephaser_duration is None:
        seq_max = fnc(system_specs, num_samples=matrix_size[0], k_phase=kpes[0], k_readout=kro_max,
                      **kwargs)
        prephaser_block = seq_max.get_block("ro_prephaser_0")
        prephaser_duration = system_specs.time_to_raster(prephaser_block.duration, "grad")
        kwargs["prephaser_duration"] = prephaser_duration

    sequence_list = []
    # Add dummy shots
    if dummy_shots is not None:
        # Temporary fix for bSSFP dummy mismatch
        dummy = fnc(system_specs, num_samples=matrix_size[0], k_readout=kro_max, k_phase=0 * kro_max, **kwargs)
        dummy.remove_block('adc_0')
        for _ in range(dummy_shots):
            sequence_list.append(deepcopy(dummy))

    for idx, kpe in enumerate(kpes):
        seq = fnc(system_specs, num_samples=matrix_size[0], k_readout=kro_max,
                  k_phase=kpe, **kwargs)
        sequence_list.append(seq)
    return sequence_list

matrix_to_kspace_2d

matrix_to_kspace_2d(
    matrix_size: ndarray, inplane_resolution: Quantity
) -> (np.ndarray, np.ndarray)

Calculates maximal k-space vector and phase encoding for each line for a bottom up filling.

The k-space center will allway be covered by a line, therefore:

- For an even number of k-space lines the first line at -kmax_pe  and
  the last line is at +kmax_pe - delta_kpe
- For and odd number the lines are symmetric around the center in pe direction

Parameters:

Name Type Description Default
matrix_size ndarray

(2, ) Integer array providing the inplane matrix size

required
inplane_resolution Quantity

(2, ) Quantity with length-dimension providing the inplane resolution

required

Returns:

Type Description
k_max (2, ), k-phase positions in phase encoding direction
Source code in cmrseq/parametric_definitions/readout/_cartesian_single_lines.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
def matrix_to_kspace_2d(matrix_size: np.ndarray, inplane_resolution: Quantity) -> (np.ndarray, np.ndarray):
    r"""Calculates maximal k-space vector and phase encoding for each line for a bottom up filling.

    The k-space center will allway be covered by a line, therefore:

        - For an even number of k-space lines the first line at -kmax_pe  and
          the last line is at +kmax_pe - delta_kpe
        - For and odd number the lines are symmetric around the center in pe direction

    Parameters
    ----------
    matrix_size
        (2, ) Integer array providing the inplane matrix size
    inplane_resolution
        (2, ) Quantity with length-dimension providing the inplane resolution

    Returns
    -------
    k_max (2, ), k-phase positions in phase encoding direction
    """
    kro_traverse = 1 / inplane_resolution[0]
    fov_pe = matrix_size[1] * inplane_resolution[1]
    delta_kpe = 1 / fov_pe
    if matrix_size[1] % 2 == 1:
        kpes = (np.arange(0, matrix_size[1], 1) - (matrix_size[1]) // 2) * delta_kpe
    else:
        kpes = (np.arange(0, matrix_size[1], 1) - (matrix_size[1] + 1) // 2) * delta_kpe

    delta_kro = 1 / (matrix_size[0] * inplane_resolution[0])
    kro_max = - ((matrix_size[1] + 1) // 2) * delta_kpe
    kpe_max = - ((matrix_size[1] + 1) // 2) * delta_kpe
    kmax = Quantity([kro_max.m_as("1/m"), kpe_max.m_as("1/m")], "1/m")
    return kmax, kpes, kro_traverse

get_shortest_adc_duration

get_shortest_adc_duration(
    system_specs: SystemSpec,
    num_samples: int,
    resolution: Quantity,
) -> (Quantity, Quantity, Quantity)

Computes the shortest possible single-line readout gradient (without prephaser) for the given resolution and matrix size in RO direction.

Assumes gradients are ramped with maximum slew-rate.

Parameters:

Name Type Description Default
system_specs SystemSpec
required
num_samples int
required
resolution Quantity
required

Returns:

Type Description
gradient object for the readout gradient and adc object
Source code in cmrseq/parametric_definitions/readout/_cartesian_single_lines.py
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
def get_shortest_adc_duration(system_specs: cmrseq.SystemSpec,
                              num_samples: int, resolution: Quantity) \
                              -> (Quantity, Quantity, Quantity):
    r"""Computes the shortest possible single-line readout gradient (without prephaser)
    for the given resolution and matrix size in RO direction.

    Assumes gradients are ramped with maximum slew-rate.

    Parameters
    ----------
    system_specs
    num_samples
    resolution

    Returns
    -------
    gradient object for the readout gradient and adc object
    """
    _, _, kro_traverse = cmrseq.seqdefs.readout.matrix_to_kspace_2d(np.array([num_samples, 1]),
                                                         Quantity([resolution.m_as("mm"), 1], "mm"))

    dk_M = kro_traverse/num_samples # Get kspace step
    farea_ro = (kro_traverse / system_specs.gamma).to("mT/m*ms")

    # The naive approach would be to assume maximum gradient strength, however this is not actually the shortest due to rise time
    # We already know the flat area of the gradient, so we need to minimize the rise area.
    # This also minimizes the prewinder, resulting in the fastest possible sequence

    # The duration is given by D = A/G + 2G/s, find G to minimize D (A=area of flat area, s=max slew, G=gradient strength)
    # Which has a minima at G = sqrt(A*S/2)

    g_opt = np.sqrt(farea_ro*system_specs.max_slew/2)

    if g_opt>system_specs.max_grad:
        g_opt = system_specs.max_grad

    # Solve for dwell time at optimal gradient strength
    min_dwell = (dk_M/system_specs.gamma/g_opt).to('ms')
    # round up to nearest adc_raster multiple
    dwell = system_specs.time_to_raster(min_dwell,raster='adc')

    adc_duration = dwell*num_samples

    # Generate ADC
    adc = cmrseq.bausteine.SymmetricADC.from_centered_valid(
            system_specs=system_specs,
            num_samples=num_samples,
            duration=adc_duration,
            delay=Quantity(0,'ms'))
    adc_dwell = adc._dwell
    # RO flat duration is set such that it includes all ADC samples + half a dwell time on either side, rounded up to gradient raster
    ro_flatdur = np.around(np.max(np.abs(adc.adc_timing-adc.adc_center)),decimals=8)*2+adc._dwell
    ro_flatdur = system_specs.time_to_raster(ro_flatdur, raster="grad")

    # RO amplitude is based on deltaK
    ro_amp = (dk_M / adc_dwell / system_specs.gamma).to("mT/m")

    ro = cmrseq.bausteine.TrapezoidalGradient.from_fdur_amp(system_specs=system_specs,
                                                orientation = np.array([1.,0.,0.]),
                                                amplitude = ro_amp, flat_duration = ro_flatdur)

    return ro, adc

get_longest_adc_duration

get_longest_adc_duration(
    system_specs: SystemSpec,
    total_duration: Quantity,
    num_samples: int,
    resolution: Quantity,
    balanced: bool = False,
    additional_kspace_traverse: Quantity = None,
    readout_prephaser_scaling: float = -0.5,
    max_iters: int = 10,
) -> (
    cmrseq.bausteine.TrapezoidalGradient,
    cmrseq.bausteine.TrapezoidalGradient,
)

Creates the readout-gradient and prephaser (and balancing rewinder) with maximum flat top duration of the readout gradient, for the specified flat top area (defined by the image resoultion) and a specified total duration.

Parameters:

Name Type Description Default
system_specs SystemSpec
required
total_duration Quantity

Total duration to fit the gradients into.

required
num_samples int

Number of samples (used to compute the required k-space traverse)

required
resolution Quantity

Resolution in RO direction (used to compute the required k-space traverse)

required
balanced bool

If true, the total duration includes the rewinder after the readout, otherwise not

False
additional_kspace_traverse Quantity

k-space vector that needs to be traversed during the prephaser, while adhering to the norm of the combined gradient channels being smaller than system_specs.max_grad. If None, no additional traverse is assumed, potentially resulting in higher prephaser amplitudes.

None
max_iters int

optional, maximum number of iteration loops for finding optimal gradients. Typically converges with less than 4

10

Returns:

Type Description
Two trapezoidal gradient objects, one for the prephaser and the other for the readout gradient, and one ADC object
Source code in cmrseq/parametric_definitions/readout/_cartesian_single_lines.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
def get_longest_adc_duration(system_specs: cmrseq.SystemSpec,
                             total_duration: Quantity,
                             num_samples: int,
                             resolution: Quantity,
                             balanced: bool = False,
                             additional_kspace_traverse: Quantity = None,
                             readout_prephaser_scaling: float = -0.5,
                             max_iters:int = 10) \
                             -> (cmrseq.bausteine.TrapezoidalGradient, cmrseq.bausteine.TrapezoidalGradient):
    r"""Creates the readout-gradient and prephaser (and balancing rewinder) with maximum flat top
    duration of the readout gradient, for the specified flat top area (defined by the
    image resoultion) and a specified total duration.

    Parameters
    ----------
    system_specs
    total_duration
        Total duration to fit the gradients into.
    num_samples
        Number of samples (used to compute the required k-space traverse)
    resolution
        Resolution in RO direction (used to compute the required k-space traverse)
    balanced
        If true, the total duration includes the rewinder after the readout, otherwise not
    additional_kspace_traverse
        k-space vector that needs to be traversed during the prephaser, while adhering to the norm of the combined gradient channels being smaller than system_specs.max_grad. If None, no additional traverse is assumed, potentially resulting in higher prephaser amplitudes.
    max_iters
        optional, maximum number of iteration loops for finding optimal gradients. Typically converges with less than 4

    Returns
    -------
    Two trapezoidal gradient objects, one for the prephaser and the other for the readout gradient, and one ADC object
    """

    kmax, _, kro_traverse = cmrseq.seqdefs.readout.matrix_to_kspace_2d(np.array([num_samples, 1]),
                                                         Quantity([resolution.m_as("mm"), 1], "mm"))
    area_ro = (kro_traverse / system_specs.gamma).m_as("mT/m*ms")

    # Only questions that needs to be solved is what is G_readout
    # since the area of the readout is set, then knowing G_readout gives us the flat time of the readout and its ramp times, basically the entire gradient

    # An initial guess for G_readout can be made by ignoring ramp times for the readout

    farea_ro = (kro_traverse / system_specs.gamma).to("mT/m*ms")

    # Get estimated area of prephaser as half readout + traverse
    # This ignores ramping for the readout, so the actual one will be larger...
    if additional_kspace_traverse is not None:
        k_y_area = np.abs((additional_kspace_traverse[0] / system_specs.gamma).to("mT/m*ms"))
        k_z_area = np.abs((additional_kspace_traverse[1] / system_specs.gamma).to("mT/m*ms"))
        area_prep_est = np.sqrt((readout_prephaser_scaling*farea_ro)**2 + k_y_area**2 + k_z_area**2)
    else:
        area_prep_est = np.abs(readout_prephaser_scaling*farea_ro)
        k_y_area = Quantity(0,'mT/m*ms')
        k_z_area = Quantity(0,'mT/m*ms')

    # If prep area is very small, we call a different solver.
    if area_prep_est.m_as('mT/m*ms') < 1e-8:
        ro, adc = get_longest_adc_duration_noprephaser(system_specs=system_specs,
                                                        total_duration = total_duration,
                                                        num_samples = num_samples,
                                                        resolution=resolution)
        return None, ro, adc


     # Otherwise, get the shortest possible duration for this prep
    _, fastest_prep_ramp, fastest_prep_flatdur = system_specs.get_shortest_gradient(area_prep_est)
    prep_duration_est = fastest_prep_flatdur + 2*fastest_prep_ramp

    if balanced:
        readout_duration_est = total_duration - 2*prep_duration_est
    else:
        readout_duration_est = total_duration - prep_duration_est
    # Solve for inital guess of readout gradient amplitude from given farea and duration
    a = 2/system_specs.max_slew
    b = -readout_duration_est
    c = farea_ro
    if b**2<(4*a*c):
        raise cmrseq.err.SequenceArgumentError(f"Total duration too short for desired kspace traverse",
                                        argument="total_duration")

    g = (-b - np.sqrt(b**2-4*a*c))/(2*a)

    # This is our first guess for the readout amplitude, and gives us a lower bound on the possible strength
    # This is because accounting for readout ramps in the prephaser will increase its duration, resulting in less time for readout and higher gradients
    # And then larger ramps, and more prephaser area and so on...
    # If this is larger than max, the sequence is not possible
    if g > system_specs.max_grad or readout_duration_est<0:
        raise cmrseq.err.SequenceArgumentError(f"Total duration too short for desired kspace traverse",
                                               argument="total_duration")

    # now we construct the actual sequence components
    # This uses a few steps of optimization

    for i in range(max_iters):
        # Step 1:
        # create readout gradient with these specs that abides by rasters
        ro = cmrseq.bausteine.TrapezoidalGradient.from_dur_amp(system_specs=system_specs,
                                                        orientation = np.array([1.,0.,0.]),
                                                        amplitude = g, duration=readout_duration_est)
        # Step 2:
        # ADC must be on raster, but to achieve the desired kspace step, we need to adjust the readout gradient strength to match adc dwell
        # Create an ADC object
        adc_duration = ro.flat_duration
        adc = cmrseq.bausteine.SymmetricADC.from_centered_valid(
                system_specs=system_specs,
                num_samples=num_samples,
                duration=adc_duration,
                delay=Quantity(0,'ms'), suppress_warnings=True)

        adc_dwell = adc._dwell
        # RO flat duration is set such that it includes all ADC samples + half a dwell time on either side, rounded up to gradient raster
        ro_flatdur = np.around(np.max(np.abs(adc.adc_timing-adc.adc_center)),decimals=8)*2+adc._dwell
        ro_flatdur = system_specs.time_to_raster(ro_flatdur, raster="grad")

        # RO amplitude is based on deltaK
        dk_M = kro_traverse/num_samples
        ro_amp = (dk_M / adc_dwell / system_specs.gamma).to("mT/m")

        # If this is larger than max, the sequence is not possible
        if ro_amp > system_specs.max_grad:
            raise cmrseq.err.SequenceArgumentError(f"Total duration too short for desired kspace traverse",
                                                    argument="total_duration")
        # Create updated readout gradient
        ro = cmrseq.bausteine.TrapezoidalGradient.from_fdur_amp(system_specs=system_specs,
                                                        orientation = np.array([1.,0.,0.]),
                                                        amplitude = ro_amp, flat_duration = ro_flatdur)

        # Create corresponding prephaser
        A_prep = np.sqrt((readout_prephaser_scaling*ro.area[0])**2 + k_y_area**2 + k_z_area**2)
        _, prep_ramp, prep_flatdur = system_specs.get_shortest_gradient(A_prep)
        prep_dur = prep_flatdur + 2*prep_ramp

        # Calculate final duration
        if balanced:
            final_duration = ro.duration + 2*prep_dur
        else:
            final_duration = ro.duration + prep_dur

        if final_duration > total_duration:
            # Adjust estimated readout duration
            readout_duration_est -= (final_duration-total_duration)
        else:
            break

    # Create final version of prep on proper axes
    prep_dir = np.array([(readout_prephaser_scaling*ro.area[0]).m_as('mT/m*ms'),k_y_area.m_as('mT/m*ms'),k_z_area.m_as('mT/m*ms')])
    prep_dir = prep_dir/np.linalg.norm(prep_dir)
    prep = cmrseq.bausteine.TrapezoidalGradient.from_area(system_specs=system_specs,
                                                    orientation = prep_dir,area=A_prep)
    return prep, ro, adc

get_longest_adc_duration_noprephaser

get_longest_adc_duration_noprephaser(
    system_specs: SystemSpec,
    total_duration: Quantity,
    num_samples: int,
    resolution: Quantity,
    iters: int = 3,
)
Source code in cmrseq/parametric_definitions/readout/_cartesian_single_lines.py
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
def get_longest_adc_duration_noprephaser(system_specs: cmrseq.SystemSpec,
                                        total_duration: Quantity,
                                        num_samples: int,
                                        resolution: Quantity,
                                        iters:int = 3):

    kmax, _, kro_traverse = cmrseq.seqdefs.readout.matrix_to_kspace_2d(np.array([num_samples, 1]),
                                                         Quantity([resolution.m_as("mm"), 1], "mm"))

    # Only questions that needs to be solved is what is G_readout
    # since the area of the readout is set, then knowing G_readout gives us the flat time of the readout and its ramp times, basically the entire gradient
    # An initial guess for G_readout can be made by ignoring ramp times for the readout

    farea = (kro_traverse / system_specs.gamma).to("mT/m*ms")

    # Solve for inital guess of readout gradient amplitude from given farea and duration
    a = 2/system_specs.max_slew
    b = -total_duration
    c = farea
    if b**2<(4*a*c):
        raise cmrseq.err.SequenceArgumentError(f"Total duration too short for desired kspace traverse",
                                        argument="total_duration")

    g = (-b - np.sqrt(b**2-4*a*c))/(2*a)

    # This is our first guess for the readout amplitude, and gives us a lower bound on the possible strength
    # This G assumes no rasters
    # As a result, rise time will be longer -> less flat time -> higher gradient strength needed
    # ADC will be on raster -> less dwell time -> higher gradient strength needed

    # If this is larger than max, the sequence is not possible
    if g > system_specs.max_grad:
        raise cmrseq.err.SequenceArgumentError(f"Total duration too short for desired kspace traverse",
                                               argument="total_duration")

    # now we construct the actual sequence components
    # This uses a few steps of optimization
    for i in range(iters):
        # Step 1:
        # create readout gradient with these specs that abides by rasters
        ro = cmrseq.bausteine.TrapezoidalGradient.from_dur_amp(system_specs=system_specs,
                                                        orientation = np.array([1.,0.,0.]),
                                                        amplitude = g, duration=total_duration)
        # Step 2:
        # ADC must be on raster, but to achieve the desired kspace step, we need to adjust the readout gradient strength to match adc dwell
        # Create an ADC object
        adc_duration = ro.flat_duration
        adc = cmrseq.bausteine.SymmetricADC.from_centered_valid(
                system_specs=system_specs,
                num_samples=num_samples,
                duration=adc_duration,
                delay=Quantity(0,'ms'), suppress_warnings=True)

        adc_dwell = adc._dwell
        # RO amplitude is based on deltaK
        dk_M = kro_traverse/num_samples
        ro_amp = (dk_M / adc_dwell / system_specs.gamma).to("mT/m")

        # Now we have an updated strength for the readout gradient.
        # If this is larger than max, the sequence is not possible
        if ro_amp > system_specs.max_grad:
            raise cmrseq.err.SequenceArgumentError(f"Total duration too short for desired kspace traverse",
                                                    argument="total_duration")

        # Early stopping if close enough
        if np.abs((ro_amp - g).m_as("mT/m")) < 1e-3:
            # Converged
            break

        # Update the readout amplitude
        g = ro_amp


    # Create final version of prep on proper axes
    return ro, adc

gre_cartesian_line

gre_cartesian_line(
    system_specs: SystemSpec,
    num_samples: int,
    k_readout: Quantity,
    k_phase: Quantity,
    adc_duration: Quantity,
    k_slice: Quantity = None,
    delay: Quantity = Quantity(0.0, "ms"),
    prephaser_duration: Quantity = None,
) -> cmrseq.Sequence

Generates a gradient sequence to apply phase encoding (0, 1.,0.) direction and a readout including adc-events for a single line in gradient direction (1., 0., 0.). Is designed to work for gradient-echo based readouts.

.. code-block:: python

. ADC: |||||| -> num_samples . . __ . . RO: _ / \ . . _/ . . ___ . . PE: __/ _____ . . . . | delay | | | . . adc_duration .

Parameters:

Name Type Description Default
system_specs SystemSpec

SystemSpecification

required
num_samples int

Number of samples acquired during frequency encoding

required
k_readout Quantity

Quantity[1/Length] :math:FOV_{kx} corresponds to :math:1/\Delta x s

required
k_phase Quantity

Quantity[1/Length] :math:n \Delta k_{y} phase encoding strength of current line

required
adc_duration Quantity

Quantity[time] Total duration of adc-sampling for a single TR

required
k_slice Quantity

Optional slice-direction phase encoding strength for 3D encoding. If specified, a slice-direction prephaser is added.

None
delay Quantity
Quantity(0.0, 'ms')
prephaser_duration Quantity

Optional if not specified the shortest possible duration for the RO/PE prephaser is calculated

None

Returns:

Type Description
Sequence object containing RO- & PE-gradients as well as ADC events
Source code in cmrseq/parametric_definitions/readout/_cartesian_single_lines.py
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
def gre_cartesian_line(system_specs: cmrseq.SystemSpec,
                       num_samples: int,
                       k_readout: Quantity,
                       k_phase: Quantity,
                       adc_duration: Quantity,
                       k_slice: Quantity = None,
                       delay: Quantity = Quantity(0., "ms"),
                       prephaser_duration: Quantity = None) -> cmrseq.Sequence:
    r"""Generates a gradient sequence to apply phase encoding (0, 1.,0.) direction and a readout
    including adc-events for a single line in gradient direction (1., 0., 0.). Is designed to work
    for gradient-echo based readouts.

    .. code-block:: python

       . ADC:                      ||||||     -> num_samples    .
       .                           ______                       .
       . RO:      ___________     /      \                      .
       .                     \___/                              .
       .                      ___                               .
       . PE:      ___________/   \________                      .
       .                                                        .
       .         | delay    |     |     |                       .
       .                        adc_duration                    .

    Parameters
    ----------
    system_specs
        SystemSpecification
    num_samples
        Number of samples acquired during frequency encoding
    k_readout
        Quantity[1/Length] :math:`FOV_{kx}` corresponds to :math:`1/\Delta x` s
    k_phase
        Quantity[1/Length] :math:`n \Delta k_{y}` phase encoding strength of current line
    adc_duration
        Quantity[time] Total duration of adc-sampling for a single TR
    k_slice
        Optional slice-direction phase encoding strength for 3D encoding. If specified, a
        slice-direction prephaser is added.
    delay
    prephaser_duration
        Optional if not specified the shortest possible duration for the RO/PE prephaser is calculated

    Returns
    -------
    Sequence object containing RO- & PE-gradients as well as ADC events
    """

    # First calculate ADC, and determine the actual dwell time
    if num_samples > 0:
        # Get raster-rounnded ADC
        adc = cmrseq.bausteine.SymmetricADC.from_centered_valid(
                system_specs=system_specs,
                num_samples=num_samples,
                duration=adc_duration,
                delay=Quantity(0,'ms'))
        adc_dwell = adc._dwell
        # RO flat duration is set such that it includes all ADC samples + half a dwell time on either side, rounded up to gradient raster
        ro_flatdur = np.around(np.max(np.abs(adc.adc_timing-adc.adc_center)),decimals=8)*2+adc._dwell
        ro_flatdur = system_specs.time_to_raster(ro_flatdur, raster="grad")

        # RO amplitude is based on deltaK
        dk_M = k_readout/num_samples
        ro_amp = (dk_M / adc_dwell / system_specs.gamma).to("mT/m")
    else:
        # Calculate based on only adc_duration and k_readout
        adc_duration = system_specs.time_to_raster(adc_duration, raster="grad")
        ro_amp = (k_readout / adc_duration / system_specs.gamma).to("mT/m")
        ro_flatdur = adc_duration

    readout_pulse = cmrseq.bausteine.TrapezoidalGradient.from_fdur_amp(
        system_specs=system_specs,
        orientation=np.array([1., 0., 0.]),
        flat_duration=ro_flatdur,
        amplitude=ro_amp, delay=Quantity(0., "ms"),
        name="trapezoidal_readout"
    )

    prephaser_ro_area = readout_pulse.area[0] / 2.

    if k_slice is None:
        k_slice = Quantity(0., "1/m")
        no_kz = True
    else:
        no_kz = False

    # Total gradient traverse is a combination of ro and pe directions.
    # Need to solve as single gradient to ensure slew and strength restrictions are met
    k_traverse_comb = Quantity([(prephaser_ro_area * system_specs.gamma).m_as("1/m"),
                                 k_phase.m_as("1/m"), k_slice.m_as("1/m")], "1/m")
    _, fastest_prep_ramp, fastest_prep_flatdur = system_specs.get_fastest_kspace_traverse(k_traverse_comb)

    # If prephaser duration was not specified use the fastest possible prephaser
    min_duration = fastest_prep_flatdur + 2 * fastest_prep_ramp
    if prephaser_duration is None:
        prephaser_duration = min_duration
    else:
        # Check if duration is sufficient for _combined_ prephaser gradients
        if prephaser_duration.m_as("ms") < min_duration.m_as("ms") - 1e-6:
            raise cmrseq.err.SequenceArgumentError(
                    f"Too short for combined PE+RO k-space traverse."
                    f" ({prephaser_duration} < {min_duration})",
                    argument="prephaser_duration")
    readout_pulse.shift(prephaser_duration + delay)

    total_kspace_traverse = Quantity(np.linalg.norm(k_traverse_comb.m_as("1/m")), "1/m")
    combined_gradient_area = total_kspace_traverse / system_specs.gamma.to("1/mT/ms")
    orientation = (k_traverse_comb/np.sqrt(np.sum(k_traverse_comb**2))).m_as('')
    prep_pulse = cmrseq.bausteine.TrapezoidalGradient.from_dur_area(system_specs=system_specs,
                                                                    orientation=orientation,
                                                                    duration=prephaser_duration,
                                                                    area=combined_gradient_area,
                                                                    delay=delay, name="ro_prephaser")


    ro_prep_pulse = cmrseq.bausteine.TrapezoidalGradient(system_specs=system_specs,
                                                        orientation=np.array([-1., 0., 0.]),
                                                        amplitude=prep_pulse.gradients[1][0,1],
                                                        flat_duration=prep_pulse.flat_duration,
                                                        rise_time=prep_pulse.rise_time,
                                                        delay=delay, name="ro_prephaser")

    pe_direction = np.array([0., 1., 0.])# * np.sign(k_phase)
    pe_prep_pulse = cmrseq.bausteine.TrapezoidalGradient(system_specs=system_specs,
                                                    orientation=pe_direction,
                                                    amplitude=prep_pulse.gradients[1][1,1],
                                                    flat_duration=prep_pulse.flat_duration,
                                                    rise_time=prep_pulse.rise_time,
                                                    delay=delay, name="pe_prephaser")

    # Use a list here to easily add the kz prephaser only if needed
    ksl = []
    # If the kslice was not defined, then we do not include the gradient.
    if not no_kz:
        slice_direction = np.array([0., 0., 1.])# * np.sign(k_slice)
        slice_prep_pulse = cmrseq.bausteine.TrapezoidalGradient(system_specs=system_specs,
                                                                orientation=slice_direction,
                                                                amplitude=prep_pulse.gradients[1][2,1],
                                                                flat_duration=prep_pulse.flat_duration,
                                                                rise_time=prep_pulse.rise_time,
                                                                delay=delay, name="kz_prephaser")
        ksl.append(slice_prep_pulse)

    if num_samples > 0:
        adc_delay = prephaser_duration + delay - adc.adc_center + readout_pulse.duration/2
        # ADC delay must be on ADC raster, otherwise sample edges will not be on raster
        adc_delay = system_specs.time_to_raster(adc_delay, raster="adc")
        adc.shift(adc_delay)
        return cmrseq.Sequence([ro_prep_pulse, pe_prep_pulse] + ksl + [readout_pulse, adc],
                               system_specs=system_specs)
    else:
        return cmrseq.Sequence([ro_prep_pulse, pe_prep_pulse] + ksl + [readout_pulse],
                               system_specs=system_specs)

balanced_gre_cartesian_line

balanced_gre_cartesian_line(
    system_specs: SystemSpec,
    num_samples: int,
    k_readout: Quantity,
    k_phase: Quantity,
    adc_duration: Quantity,
    k_slice: Quantity = None,
    delay: Quantity = Quantity(0.0, "ms"),
    prephaser_duration: Quantity = None,
) -> cmrseq.Sequence

Generates a gradient sequence to apply phase encoding (0, 1.,0.) direction and a readout including adc-events for a single line in gradient direction (1., 0., 0.). After readout prephasers are rewound. Is designed to work for gradient-echo based readouts.

.. code-block: python

. ADC: |||||| -> num_samples . . __ . . RO: _ / \ ______ . . _/ ___/ . . ___ ___ . . PE: __/ __/ __ . . . . | delay | | | . . adc_duration .

Parameters:

Name Type Description Default
system_specs SystemSpec

SystemSpecification

required
num_samples int

Number of samples acquired during frequency encoding

required
k_readout Quantity

Quantity[1/Length] :math:FOV_{kx} corresponds to :math:1/\Delta x s

required
k_phase Quantity

Quantity[1/Length] :math:n \Delta k_{y} phase encoding strength of current line

required
adc_duration Quantity

Quantity[time] Total duration of adc-sampling for a single TR

required
delay Quantity

Defaults to 0 ms

Quantity(0.0, 'ms')
prephaser_duration Quantity

Optional if not specified the shortest possible duration for the RO/PE prephaser is calculates

None

Returns:

Type Description
Sequence object containing RO- & PE-gradients plus rewinders as well as ADC events
Source code in cmrseq/parametric_definitions/readout/_cartesian_single_lines.py
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
def balanced_gre_cartesian_line(system_specs: cmrseq.SystemSpec,
                                num_samples: int,
                                k_readout: Quantity,
                                k_phase: Quantity,
                                adc_duration: Quantity,
                                k_slice: Quantity = None,
                                delay: Quantity = Quantity(0., "ms"),
                                prephaser_duration: Quantity = None) -> cmrseq.Sequence:
    r"""Generates a gradient sequence to apply phase encoding (0, 1.,0.) direction and a readout
    including adc-events for a single line in gradient direction (1., 0., 0.). After readout
    prephasers are rewound. Is designed to work for gradient-echo based readouts.

    .. code-block: python

       .        ADC:                      ||||||     -> num_samples        .
       .                                  ______                           .
       .        RO:      ___________     /      \     ______               .
       .                            \___/        \___/                     .
       .                             ___          ___                      .
       .        PE:      ___________/   \________/   \_____                .
       .                                                                   .
       .                | delay    |     |     |                           .
       .                              adc_duration                         .

    Parameters
    ----------
    system_specs
        SystemSpecification
    num_samples
        Number of samples acquired during frequency encoding
    k_readout
        Quantity[1/Length] :math:`FOV_{kx}` corresponds to :math:`1/\Delta x` s
    k_phase
        Quantity[1/Length] :math:`n \Delta k_{y}` phase encoding strength of current line
    adc_duration
        Quantity[time] Total duration of adc-sampling for a single TR
    delay
        Defaults to 0 ms
    prephaser_duration
        Optional if not specified the shortest possible duration for the RO/PE prephaser is calculates

    Returns
    -------
    Sequence object containing RO- & PE-gradients plus rewinders as well as ADC events
    """
    seq = gre_cartesian_line(system_specs=system_specs, num_samples=num_samples,
                             k_readout=k_readout, k_phase=k_phase, k_slice=k_slice,
                             adc_duration=adc_duration, delay=delay,
                             prephaser_duration=prephaser_duration)
    # Copy prephasers
    prep_ro_block = deepcopy(seq.get_block("ro_prephaser_0"))
    prep_pe_block = deepcopy(seq.get_block("pe_prephaser_0"))

    # Shift to end of readout
    ro_duration = seq["trapezoidal_readout_0"].duration
    prep_pe_block.shift(ro_duration + prep_pe_block.duration)
    prep_ro_block.shift(ro_duration + prep_ro_block.duration)

    # Invert amplidute
    prep_pe_block.scale_gradients(-1)

    prep_pe_block.name = "pe_prephaser_balance"
    prep_ro_block.name = "ro_prephaser_balance"

    # All this for kz again
    kzl = []
    if k_slice is not None:
        prep_kz_block = deepcopy(seq.get_block("kz_prephaser_0"))
        prep_kz_block.shift(ro_duration + prep_kz_block.duration)
        prep_kz_block.scale_gradients(-1)
        prep_kz_block.name = "kz_prephaser_balance"
        kzl.append(prep_kz_block)

    seq += cmrseq.Sequence([prep_ro_block, prep_pe_block] + kzl, system_specs=system_specs)
    return seq

se_cartesian_line

se_cartesian_line(
    system_specs: SystemSpec,
    num_samples: int,
    echo_time: Quantity,
    pulse_duration: Quantity,
    excitation_center_time: Quantity,
    k_readout: Quantity,
    k_phase: Quantity,
    adc_duration: Quantity,
    delay: Quantity = Quantity(0.0, "ms"),
    prephaser_duration: Quantity = None,
) -> cmrseq.Sequence

Generates a gradient sequence to apply phase encoding (0, 1.,0.) direction and a readout including adc-events for a single line in gradient direction (1., 0., 0.) for a spin-echo based readout.

.. code-block:: python

.                excitation center                                  .
.                   |                                               .
.                   |   TE/2 |   TE/2 |                             .
.   ADC:                           ||||||     -> num_samples        .
.                      ___         ______                           .
.   RO:           ____/   \_______/      \                          .
.                      ___                                          .
.   PE:           ____/   \_____________                            .
.           |   |                 |     |                           .
.           delay              adc_duration                         .
.               |    |                                              .
.           pulse_duration                                          .

Parameters:

Name Type Description Default
system_specs SystemSpec

SystemSpecification

required
num_samples int

Number of samples acquired during frequency encoding

required
echo_time Quantity
required
pulse_duration Quantity

total time of ss-gradient (including ramps)

required
excitation_center_time Quantity

Quantity[Time] Reference time-point to calculate TE from

required
k_readout Quantity

Quantity[1/Length] :math:FOV_{kx} corresponds to :math:1/\Delta x

required
k_phase Quantity

Quantity[1/Length] :math:n \Delta k_{y} phase encoding strength of current line

required
adc_duration Quantity

Quantity[time] Total duration of adc-sampling for a single TR

required
prephaser_duration Quantity

Optional if not specified the shortest possible duration for the RO/PE prephaser is calculates

None

Returns:

Type Description
Sequence containing the RO/PE prephaser, RO and adc events for a spin-echo read-out

Raises:

Type Description
ValueError

If phase/frequency encoding amplitude would exceed system limits

Source code in cmrseq/parametric_definitions/readout/_cartesian_single_lines.py
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
def se_cartesian_line(system_specs: cmrseq.SystemSpec,
                      num_samples: int,
                      echo_time: Quantity,
                      pulse_duration: Quantity,
                      excitation_center_time: Quantity,
                      k_readout: Quantity,
                      k_phase: Quantity,
                      adc_duration: Quantity,
                      delay: Quantity = Quantity(0., "ms"),
                      prephaser_duration: Quantity = None) -> cmrseq.Sequence:
    r"""Generates a gradient sequence to apply phase encoding (0, 1.,0.) direction and a readout
    including adc-events for a single line in gradient direction (1., 0., 0.) for a spin-echo based
    readout.

    .. code-block:: python

        .                excitation center                                  .
        .                   |                                               .
        .                   |   TE/2 |   TE/2 |                             .
        .   ADC:                           ||||||     -> num_samples        .
        .                      ___         ______                           .
        .   RO:           ____/   \_______/      \                          .
        .                      ___                                          .
        .   PE:           ____/   \_____________                            .
        .           |   |                 |     |                           .
        .           delay              adc_duration                         .
        .               |    |                                              .
        .           pulse_duration                                          .


    Parameters
    ----------
    system_specs
        SystemSpecification
    num_samples
        Number of samples acquired during frequency encoding
    echo_time
    pulse_duration
        total time of ss-gradient (including ramps)
    excitation_center_time
        Quantity[Time] Reference time-point to calculate TE from
    k_readout
        Quantity[1/Length] :math:`FOV_{kx}` corresponds to :math:`1/\Delta x`
    k_phase
        Quantity[1/Length] :math:`n \Delta k_{y}` phase encoding strength of current line
    adc_duration
        Quantity[time] Total duration of adc-sampling for a single TR
    prephaser_duration
        Optional if not specified the shortest possible duration for the RO/PE prephaser is calculates

    Returns
    -------
    Sequence containing the RO/PE prephaser, RO and adc events for a spin-echo read-out

    Raises
    ------
    ValueError
        If phase/frequency encoding amplitude would exceed system limits
    """

    # First calculate ADC, and determine the actual dwell time
    # Get raster-rounded ADC
    adc = cmrseq.bausteine.SymmetricADC.from_centered_valid(
            system_specs=system_specs,
            num_samples=num_samples,
            duration=adc_duration,
            delay=Quantity(0,'ms'))

    adc_dwell = adc._dwell
    # RO flat duration is set such that it includes all ADC samples + half a dwell time on either side, rounded up to gradient raster
    ro_flatdur = np.around(np.max(np.abs(adc.adc_timing-adc.adc_center)),decimals=8)*2+adc._dwell
    ro_flatdur = system_specs.time_to_raster(ro_flatdur, raster="grad")

    # RO amplitude is based on deltaK
    dk_M = k_readout/num_samples
    ro_amp = (dk_M / adc_dwell / system_specs.gamma).to("mT/m")

    # Check if viable ADC for SE
    rise_time = system_specs.get_shortest_rise_time(ro_amp)
    if ro_flatdur >= (echo_time / 2 - rise_time - pulse_duration / 2) * 2:
        raise ValueError("Specified ADC-duration is larger than available time from "
                         "end of refocusing pulse to Echo center")

    ro_delay = delay + excitation_center_time + echo_time - ro_flatdur / 2
    readout_pulse = cmrseq.bausteine.TrapezoidalGradient.from_fdur_amp(
        system_specs=system_specs,
        orientation=np.array([1., 0., 0.]),
        flat_duration=ro_flatdur,
        amplitude=ro_amp, delay=ro_delay,
        name="readout_grad")
    readout_pulse.shift(-readout_pulse.rise_time)
    prephaser_ro_area = readout_pulse.area[0] / 2.
    prephaser_pe_area = np.abs(k_phase / system_specs.gamma)

    # Total gradient traverse is a combination of ro and pe directions.
    # Need to solve as single gradient to ensure slew and strength restrictions are met
    combined_kspace_traverse = np.sqrt((prephaser_ro_area * system_specs.gamma) ** 2 + k_phase ** 2)
    [_, fastest_prep_ramp, fastest_prep_flatdur] = system_specs.get_shortest_gradient(
        combined_kspace_traverse / system_specs.gamma)

    # If prephaser duration was not specified use the fastest possible prephaser
    if prephaser_duration is None:
        prephaser_duration = fastest_prep_flatdur + 2 * fastest_prep_ramp
    else:
        if prephaser_duration < fastest_prep_flatdur + 2 * fastest_prep_ramp:
            raise ValueError("Prephaser duration is to short to for combined PE+RO "
                             "k-space traverse.")

    prephaser_delay = delay + echo_time / 2 - pulse_duration / 2 \
                      - prephaser_duration + excitation_center_time
    ro_prep_pulse = cmrseq.bausteine.TrapezoidalGradient.from_dur_area(
        system_specs=system_specs,
        orientation=np.array([1., 0., 0.]),
        duration=prephaser_duration,
        area=prephaser_ro_area,
        delay=prephaser_delay,
        name="ro_prephaser")

    pe_direction = np.array([0., -1., 0.]) * np.sign(k_phase)
    pe_prep_pulse = cmrseq.bausteine.TrapezoidalGradient.from_dur_area(system_specs=system_specs,
                                                                       orientation=pe_direction,
                                                                       duration=prephaser_duration,
                                                                       area=prephaser_pe_area,
                                                                       delay=prephaser_delay,
                                                                       name="pe_prephaser")
    adc_delay = readout_pulse.tmin + readout_pulse.rise_time + readout_pulse.flat_duration/2 - adc.adc_center
    adc.shift(adc_delay)
    return cmrseq.Sequence([ro_prep_pulse, pe_prep_pulse, readout_pulse, adc],
                           system_specs=system_specs)

_epi

This modules contains compositions of building blocks commonly used for in defining actual signal acqusition and spatial encoding

single_shot_epi

single_shot_epi(
    system_specs: SystemSpec,
    field_of_view: Quantity,
    matrix_size: ndarray,
    blip_direction: str = "up",
    partial_fourier_lines: int = 0,
    slope_sampling: bool = False,
    water_fat_shift: Union[str, float] = "minimum",
    max_total_duration: Quantity = None,
    delay: Quantity = Quantity(0, "ms"),
) -> cmrseq.Sequence

Define a single-shot EPI readout sequence from image configuration.

The prephaser is assumed to be as short as possible. Legacy diagrams and stale figures live in the readouts guide.

Parameters:

Name Type Description Default
system_specs SystemSpec

System limits used for validation.

required
field_of_view Quantity

Spatial extent in readout and phase-encoding directions.

required
matrix_size ndarray

Number of samples in readout and phase-encoding directions.

required
blip_direction str

Direction of phase encoding steps, either "up" or "down".

'up'
partial_fourier_lines int

Number of lines skipped before k-space center.

0
slope_sampling bool

If True, use ramp sampling; otherwise use flat-top sampling.

False
water_fat_shift Union[str, float]

Echo-spacing target: "minimum", "maximum", or a pixel shift as float.

'minimum'
max_total_duration Quantity

Maximum total readout duration. Required when water_fat_shift == "maximum".

None
delay Quantity

Time gap added before the sequence.

Quantity(0, 'ms')

Returns:

Type Description
Sequence

Sequence containing the EPI readout.

Raises:

Type Description
ValueError

If the shortest duration exceeds max_total_duration or the maximum water-fat shift target is requested without a maximum duration.

Source code in cmrseq/parametric_definitions/readout/_epi.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
def single_shot_epi(system_specs: cmrseq.SystemSpec, field_of_view: Quantity,
                    matrix_size: np.ndarray, blip_direction: str = "up",
                    partial_fourier_lines: int = 0, slope_sampling: bool = False,
                    water_fat_shift: Union[str, float] = "minimum",
                    max_total_duration: Quantity = None,
                    delay: Quantity = Quantity(0, "ms")) -> cmrseq.Sequence:
    r"""Define a single-shot EPI readout sequence from image configuration.

    The prephaser is assumed to be as short as possible. Legacy diagrams and stale figures live in
    the readouts guide.

    Parameters
    ----------
    system_specs
        System limits used for validation.
    field_of_view
        Spatial extent in readout and phase-encoding directions.
    matrix_size
        Number of samples in readout and phase-encoding directions.
    blip_direction
        Direction of phase encoding steps, either `"up"` or `"down"`.
    partial_fourier_lines
        Number of lines skipped before k-space center.
    slope_sampling
        If `True`, use ramp sampling; otherwise use flat-top sampling.
    water_fat_shift
        Echo-spacing target: `"minimum"`, `"maximum"`, or a pixel shift as float.
    max_total_duration
        Maximum total readout duration. Required when `water_fat_shift == "maximum"`.
    delay
        Time gap added before the sequence.

    Returns
    -------
    cmrseq.Sequence
        Sequence containing the EPI readout.

    Raises
    ------
    ValueError
        If the shortest duration exceeds `max_total_duration` or the maximum water-fat shift target
        is requested without a maximum duration.
    """
    # Calculate k-space definitions
    k_space_kwargs = _epi_fov_definition(field_of_view, matrix_size,
                                         blip_direction, partial_fourier_lines)

    # Create building blocks to assemble epi train
    if slope_sampling:
        _ = _epi_ramp_sampling(system_specs=system_specs, water_fat_shift=water_fat_shift,
                               max_total_duration=max_total_duration, **k_space_kwargs)
        ro_prephaser, pe_prephaser, readout_gradient, blip_gradient, adc_block = _
    else:
        _ = _epi_flat_sampling(system_specs=system_specs, water_fat_shift=water_fat_shift,
                               max_total_duration=max_total_duration, **k_space_kwargs)
        ro_prephaser, pe_prephaser, readout_gradient, blip_gradient, adc_block = _

    # Assemble single shot epi
    block_list = [ro_prephaser, pe_prephaser]
    for line_idx in range(matrix_size[1] - partial_fourier_lines):
        ro_block = deepcopy(readout_gradient)
        ro_block.shift(ro_prephaser.duration + line_idx * readout_gradient.duration)
        ro_block.scale_gradients((-1) ** line_idx)

        adc = deepcopy(adc_block)
        adc.shift(ro_prephaser.duration + line_idx * readout_gradient.duration)

        blip_block = deepcopy(blip_gradient)
        blip_block.shift(ro_prephaser.duration + (line_idx + 1) * readout_gradient.duration)
        block_list.extend([ro_block, adc, blip_block])

    seq = cmrseq.Sequence(block_list[:-1], system_specs)
    if delay is not None:
        seq.shift_in_time(delay)
    return seq

_radial

This module contains parametric definitions for generating radial readouts, as well as 3D radial ordering schemes.

radial_spoke

radial_spoke(
    system_specs: SystemSpec,
    num_samples: int,
    kr_max: Quantity,
    angle: Quantity,
    adc_duration: Quantity,
    delay: Quantity = Quantity(0.0, "ms"),
    prephaser_duration: Quantity = None,
    balanced: bool = False,
    readout_ramp_sampling: bool = False,
    sample_prephaser: bool = False,
    sample_balanced: bool = False,
    prephaser_ramp_sampling: bool = False,
    balanced_ramp_sampling: bool = False,
    match_prephaser_amp_to_readout: bool = False,
) -> cmrseq.Sequence

Generates a single 2D radial spoke that traverses from [-kr_max,kr_max] at a given angle.

The prephaser / rewinder ADC sampling is chosen such that the maximum k-space increment per sample satisfies

gamma * |G_pre/bal|max * dwell_pre/bal <= gamma * |G_ro|max * dwell_ro

i.e. the prephaser / rewinder never has a larger Δk per sample than the main readout. If the required dwell would be below system_specs.adc_raster_time, an error is raised.

Source code in cmrseq/parametric_definitions/readout/_radial.py
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
def radial_spoke(system_specs: cmrseq.SystemSpec,
                 num_samples: int,
                 kr_max: Quantity,
                 angle: Quantity,
                 adc_duration: Quantity,
                 delay: Quantity = Quantity(0., "ms"),
                 prephaser_duration: Quantity = None,
                 balanced: bool = False,
                 readout_ramp_sampling: bool = False,
                 sample_prephaser: bool = False,
                 sample_balanced: bool = False,             # These are sampled 2 dwell times longer
                 prephaser_ramp_sampling: bool = False,
                 balanced_ramp_sampling: bool = False,
                 match_prephaser_amp_to_readout: bool = False) -> cmrseq.Sequence:
    r"""Generates a single 2D radial spoke that traverses from [-kr_max,kr_max] at a given angle.

    The prephaser / rewinder ADC sampling is chosen such that the maximum k-space
    increment per sample satisfies

        gamma * |G_pre/bal|max * dwell_pre/bal <= gamma * |G_ro|max * dwell_ro

    i.e. the prephaser / rewinder never has a larger Δk per sample than the main readout.
    If the required dwell would be below system_specs.adc_raster_time, an error is raised.
    """

    # -------------------------------------------------------------------------
    # Main readout ADC + readout gradient
    # -------------------------------------------------------------------------
    if num_samples > 0:
        if num_samples % 2 != 0:
            raise ValueError("num_samples must be a multiple of 2 for Siemens ADC objects.")

        adc = cmrseq.bausteine.SymmetricADC.from_centered_valid(
            system_specs=system_specs,
            num_samples=num_samples,
            duration=adc_duration,
            delay=Quantity(0., 'ms')
        )
        adc_dwell = adc._dwell

        # RO flat duration includes all ADC samples + half dwell on either side, then grad-raster rounded
        ro_flatdur = np.around(np.max(np.abs(adc.adc_timing - adc.adc_center)), decimals=8) * 2 + adc._dwell
        ro_flatdur = system_specs.time_to_raster(ro_flatdur, raster="grad")

        dk_M = 2. * kr_max / num_samples
        ro_amp = (dk_M / adc_dwell / system_specs.gamma).to("mT/m")
    else:
        adc_duration = system_specs.time_to_raster(adc_duration, raster="grad")
        ro_amp = (2 * kr_max / adc_duration / system_specs.gamma).to("mT/m")
        ro_flatdur = adc_duration
        adc = None
        adc_dwell = None

    readout_pulse = cmrseq.bausteine.TrapezoidalGradient.from_fdur_amp(
        system_specs=system_specs,
        orientation=np.array([1., 0., 0.]),
        flat_duration=ro_flatdur,
        amplitude=ro_amp,
        delay=Quantity(0., "ms"),
        name="radial_readout"
    )

    adc_dead_shift = system_specs.time_to_raster(
        system_specs.adc_dead_time + system_specs.adc_raster_time,
        raster="grad"
    )

    # -------------------------------------------------------------------------
    # Readout ramp sampling
    # -------------------------------------------------------------------------
    if num_samples > 0 and readout_ramp_sampling:
        adc_dwell_ref = adc._dwell
        total_dur = system_specs.time_to_raster(
            readout_pulse.duration,
            raster="grad"
        ) - 2 * adc_dead_shift

        num_samples_total = int(np.floor((total_dur / adc_dwell_ref).m_as("dimensionless")))
        num_samples_total = (num_samples_total // 2) * 2
        num_samples_total = max(num_samples_total, num_samples)

        print(f"Ramp sampling: samples {num_samples} -> {num_samples_total}"
              f"(+{num_samples_total - num_samples})"
              f" with a dwell time of {adc_dwell_ref}")

        adc = cmrseq.bausteine.SymmetricADC.from_centered_valid(
            system_specs=system_specs,
            num_samples=num_samples_total,
            duration=total_dur,
            delay=Quantity(0., 'ms')
        )

        assert abs((adc._dwell - adc_dwell_ref).to("s").magnitude) < 1e-12, \
            f"ADC dwell changed with ramp sampling: {adc._dwell} vs {adc_dwell_ref}"

        adc_dwell = adc._dwell

    # Main-readout dk/sample target
    if num_samples > 0:
        dk_target = (system_specs.gamma * readout_pulse.magnitude * adc_dwell).to(kr_max.units)
    else:
        dk_target = (2 * kr_max).to(kr_max.units)

    # -------------------------------------------------------------------------
    # Initial sequence / optional adc_dead_delay
    # -------------------------------------------------------------------------
    ramp_os = Quantity(10, 'us')
    adc_os_shift = system_specs.time_to_raster(ramp_os, raster="grad")
    if sample_prephaser or sample_balanced:
        adc_dead_delay = cmrseq.bausteine.Delay(
            system_specs=system_specs,
            duration=adc_dead_shift,
            name="adc_dead_delay"
        )
        adc_os_delay = cmrseq.bausteine.Delay(
            system_specs=system_specs,
            duration=adc_os_shift,
            name="adc_os_delay"
        )
        seq = cmrseq.Sequence([adc_dead_delay], system_specs=system_specs)
        print('If Pulseq exporter is used - make sure the adc_dead_time is set to a multiple of the block_duration_raster')
        print('If adc_dead_time isnt 20us, the exporter to pulseq may not work!!!!!!')
    else:
        adc_dead_delay = cmrseq.bausteine.Delay(
            system_specs=system_specs,
            duration=Quantity(0., 'ms'),
            name="adc_dead_delay"
        )
        adc_os_delay = cmrseq.bausteine.Delay(
            system_specs=system_specs,
            duration=Quantity(0., 'ms'),
            name="adc_os_delay"
        )
        seq = cmrseq.Sequence([], system_specs=system_specs)

    # -------------------------------------------------------------------------
    # Prephaser
    # -------------------------------------------------------------------------
    if prephaser_duration != Quantity(0., "ms"):
        prephaser_area = readout_pulse.area[0] / 2.

        if not match_prephaser_amp_to_readout:
            [_, fastest_prep_ramp, fastest_prep_flatdur] = system_specs.get_shortest_gradient(prephaser_area)

            if prephaser_duration is None:
                prephaser_duration = fastest_prep_flatdur + 2 * fastest_prep_ramp
            else:
                if prephaser_duration < np.round(fastest_prep_flatdur + 2 * fastest_prep_ramp, 7):
                    raise ValueError("Prephaser duration is too short for combined PE+RO k-space traverse.")
        else:
            target_amp = readout_pulse.magnitude.to("mT/m")
            rise_time = system_specs.get_shortest_rise_time(target_amp).to("ms")

            flat_needed = (prephaser_area / target_amp - rise_time).to("ms")
            if flat_needed < Quantity(-1e-9, "ms"):
                amin = (target_amp * rise_time).to(prephaser_area.units)
                raise ValueError(
                    "match_prephaser_amp_to_readout=True infeasible: requested prephaser area "
                    f"{prephaser_area} smaller than minimal area {amin} achievable at |G|={target_amp}."
                )

            flat_duration = system_specs.time_to_raster(
                max(flat_needed, Quantity(0., "ms")),
                raster="grad"
            )
            prephaser_duration = flat_duration + 2 * rise_time

        readout_pulse.shift(prephaser_duration + delay + adc_dead_delay.duration + adc_os_delay.duration)

        prephaser_pulse = cmrseq.bausteine.TrapezoidalGradient.from_dur_area(
            system_specs=system_specs,
            orientation=np.array([-1., 0., 0.]),
            duration=prephaser_duration,
            area=prephaser_area,
            delay=delay + adc_dead_delay.duration + adc_os_delay.duration,
            name="radial_prephaser"
        )

        # -----------------------------
        # ADC on prephaser
        # -----------------------------
        if sample_prephaser:
            if num_samples <= 0:
                raise ValueError("sample_prephaser=True requires num_samples > 0 to define dk_target from the main readout.")

            if prephaser_ramp_sampling:
                adc_pre_start = prephaser_pulse.tmin - adc_os_shift
                adc_pre_end = prephaser_pulse.tmax - adc_dead_shift
            else:
                adc_pre_start = prephaser_pulse.tmin + prephaser_pulse.rise_time
                adc_pre_end = prephaser_pulse.tmax - max(adc_dead_shift, prephaser_pulse.fall_time)

            window_pre = adc_pre_end - adc_pre_start
            if window_pre <= Quantity(0., "ms"):
                raise ValueError("Prephaser ADC window has non-positive duration.")

            gpre_peak = prephaser_pulse.magnitude.to("mT/m")
            dwell_pre_max = (dk_target / (system_specs.gamma * gpre_peak)).to("s")

            if dwell_pre_max < system_specs.adc_raster_time.to("s"):
                raise ValueError(
                    f"Required prephaser ADC dwell {dwell_pre_max} is below adc_raster_time "
                    f"{system_specs.adc_raster_time.to('s')}."
                )

            num_samples_pre = int(np.ceil((window_pre / dwell_pre_max).m_as("dimensionless")))
            num_samples_pre = max(num_samples_pre, 2)
            num_samples_pre = (num_samples_pre) // 2 * 2  # round to even

            adc_pre = cmrseq.bausteine.EdgeAnchoredADC.from_window(
                system_specs=system_specs,
                num_samples=num_samples_pre,
                t_start=adc_pre_start,
                t_end=adc_pre_end,
                anchor="start",
                frequency_offset=Quantity(0., "Hz"),
                phase_offset=Quantity(0., "rad"),
                name="adc_prephaser"
            )

            dk_pre_max = (system_specs.gamma * gpre_peak * adc_pre._dwell).to(dk_target.units)
            if dk_pre_max > dk_target * (1 + 1e-12):
                raise ValueError(
                    f"Prephaser ADC violates dk constraint: dk_pre_max={dk_pre_max} > dk_target={dk_target}"
                )

            print(f"Prephaser sampling: samples {num_samples_pre}, dwell {adc_pre._dwell}, "
                  f"dk_max {dk_pre_max} <= target {dk_target}")

        # Shift readout ADC
        if num_samples > 0:
            adc_delay = prephaser_duration + delay + adc_dead_delay.duration + adc_os_delay.duration - adc.adc_center + readout_pulse.duration / 2
            print(f"ADC delay set to {adc_delay}")
            adc.shift(adc_delay)

            if sample_prephaser:
                seq += cmrseq.Sequence(
                    [prephaser_pulse, adc_pre, readout_pulse, adc],
                    system_specs=system_specs
                )
            else:
                seq += cmrseq.Sequence(
                    [prephaser_pulse, readout_pulse, adc],
                    system_specs=system_specs
                )
        else:
            if sample_prephaser:
                raise ValueError("sample_prephaser=True requires num_samples > 0.")
            seq += cmrseq.Sequence(
                [prephaser_pulse, readout_pulse],
                system_specs=system_specs
            )

    else:
        if num_samples > 0:
            adc_delay = delay - adc.adc_center + readout_pulse.duration / 2
            print(f"ADC delay set to {adc_delay}")
            adc.shift(adc_delay)
            seq += cmrseq.Sequence([readout_pulse, adc], system_specs=system_specs)
        else:
            seq += cmrseq.Sequence([readout_pulse], system_specs=system_specs)

    # -------------------------------------------------------------------------
    # Rewinder / balanced
    # -------------------------------------------------------------------------
    if balanced:
        try:
            rewind_block = deepcopy(seq.get_block("radial_prephaser_0"))
        except Exception as e:
            raise ValueError("balanced=True requires a prephaser block to be present.") from e

        ro_duration = seq.get_block("radial_readout_0").duration
        rewind_block.shift(ro_duration + rewind_block.duration)
        rewind_block.name = "radial_prephaser_balance"

        if sample_balanced:
            if num_samples <= 0:
                raise ValueError("sample_balanced=True requires num_samples > 0 to define dk_target from the main readout.")

            if balanced_ramp_sampling:
                adc_bal_start = rewind_block.tmin + adc_dead_shift
                adc_bal_end = rewind_block.tmax + adc_os_shift ## Add some more sampling at the end to make sure to not miss kspace center
            else:
                adc_bal_start = rewind_block.tmin + adc_dead_shift
                adc_bal_end = rewind_block.tmax - rewind_block.fall_time

            window_bal = adc_bal_end - adc_bal_start
            if window_bal <= Quantity(0., "ms"):
                raise ValueError("Balanced ADC window has non-positive duration.")

            gbal_peak = rewind_block.magnitude.to("mT/m")
            dwell_bal_max = (dk_target / (system_specs.gamma * gbal_peak)).to("s")

            if dwell_bal_max < system_specs.adc_raster_time.to("s"):
                raise ValueError(
                    f"Required balanced ADC dwell {dwell_bal_max} is below adc_raster_time "
                    f"{system_specs.adc_raster_time.to('s')}."
                )

            num_samples_bal = int(np.ceil((window_bal / dwell_bal_max).m_as("dimensionless")))
            num_samples_bal = max(num_samples_bal, 2)
            num_samples_bal = (num_samples_bal) // 2 * 2  # round to even

            adc_bal = cmrseq.bausteine.EdgeAnchoredADC.from_window(
                system_specs=system_specs,
                num_samples=num_samples_bal,
                t_start=adc_bal_start,
                t_end=adc_bal_end,
                anchor="end",
                frequency_offset=Quantity(0., "Hz"),
                phase_offset=Quantity(0., "rad"),
                name="adc_balanced"
            )

            dk_bal_max = (system_specs.gamma * gbal_peak * adc_bal._dwell).to(dk_target.units)
            if dk_bal_max > dk_target * (1 + 1e-12):
                raise ValueError(
                    f"Balanced ADC violates dk constraint: dk_bal_max={dk_bal_max} > dk_target={dk_target}"
                )

            print(f"Balanced sampling: samples {num_samples_bal}, dwell {adc_bal._dwell}, "
                  f"dk_max {dk_bal_max} <= target {dk_target}")

            seq += cmrseq.Sequence([rewind_block, adc_bal], system_specs=system_specs)
            seq.append
        else:
            seq += cmrseq.Sequence([rewind_block], system_specs=system_specs)

    # -------------------------------------------------------------------------
    # Final dead delay append if needed
    # -------------------------------------------------------------------------
    if sample_prephaser or sample_balanced:
        seq.append(adc_dead_delay)

    # -------------------------------------------------------------------------
    # Rotate spoke
    # -------------------------------------------------------------------------
    sa = np.sin(angle).m_as('dimensionless')
    ca = np.cos(angle).m_as('dimensionless')

    omatrix = cmrseq.OMatrix(
        system_specs=system_specs,
        position=Quantity(0, 'm'),
        slice_normal=np.array([0, 0, 1]),
        readout_direction=np.array([ca, sa, 0])
    )

    seq.register_omatrix(matrix=omatrix, gradients=seq.blocks)

    return seq

old_radial_spoke

old_radial_spoke(
    system_specs: SystemSpec,
    num_samples: int,
    kr_max: Quantity,
    angle: Quantity,
    adc_duration: Quantity,
    delay: Quantity = Quantity(0.0, "ms"),
    prephaser_duration: Quantity = None,
    balanced: bool = False,
    readout_ramp_sampling: bool = False,
    sample_prephaser: bool = False,
    sample_balanced: bool = False,
    prephaser_ramp_sampling: bool = False,
    balanced_ramp_sampling: bool = False,
    match_prephaser_amp_to_readout: bool = False,
) -> cmrseq.Sequence

Generate a single 2D radial spoke from -kr_max to kr_max.

Parameters:

Name Type Description Default
system_specs SystemSpec

System specifications used for rasterization and hardware limits.

required
num_samples int

Number of ADC samples from -kr_max to kr_max.

required
kr_max Quantity

Maximum k-space radius with units of inverse length.

required
angle Quantity

Spoke angle in the measurement/phase plane. An angle of zero corresponds to the readout direction.

required
adc_duration Quantity

Total ADC sampling duration.

required
delay Quantity

Delay before the spoke.

Quantity(0., "ms")
prephaser_duration Quantity

Prephaser duration. If omitted, the shortest feasible duration is used.

None
balanced bool

Add a rewinder gradient after the ADC to balance the zeroth moment.

False
readout_ramp_sampling bool

Sample during readout ramps while preserving the flat-top ADC dwell time.

False
sample_prephaser bool

Add ADC sampling during the prephaser.

False
sample_balanced bool

Add ADC sampling during the rewinder.

False
prephaser_ramp_sampling bool

Enable ramp sampling for the prephaser ADC.

False
balanced_ramp_sampling bool

Enable ramp sampling for the rewinder ADC.

False
match_prephaser_amp_to_readout bool

Match prephaser and rewinder amplitudes to the readout amplitude.

False

Returns:

Type Description
Sequence

Sequence containing the radial readout.

Source code in cmrseq/parametric_definitions/readout/_radial.py
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
def old_radial_spoke(system_specs: cmrseq.SystemSpec,
                 num_samples: int,
                 kr_max: Quantity,
                 angle: Quantity,
                 adc_duration: Quantity,
                 delay: Quantity = Quantity(0., "ms"),
                 prephaser_duration: Quantity = None,
                 balanced:bool = False,
                 readout_ramp_sampling:bool = False,
                 sample_prephaser:bool = False,
                 sample_balanced:bool = False,
                 prephaser_ramp_sampling:bool = False,
                 balanced_ramp_sampling:bool = False,
                 match_prephaser_amp_to_readout:bool = False) -> cmrseq.Sequence:                                                    #ToDO

    r"""Generate a single 2D radial spoke from ``-kr_max`` to ``kr_max``.

    Parameters
    ----------
    system_specs : cmrseq.SystemSpec
        System specifications used for rasterization and hardware limits.
    num_samples : int
        Number of ADC samples from ``-kr_max`` to ``kr_max``.
    kr_max : Quantity
        Maximum k-space radius with units of inverse length.
    angle : Quantity
        Spoke angle in the measurement/phase plane. An angle of zero corresponds
        to the readout direction.
    adc_duration : Quantity
        Total ADC sampling duration.
    delay : Quantity, default=Quantity(0., "ms")
        Delay before the spoke.
    prephaser_duration : Quantity, optional
        Prephaser duration. If omitted, the shortest feasible duration is used.
    balanced : bool, default=False
        Add a rewinder gradient after the ADC to balance the zeroth moment.
    readout_ramp_sampling : bool, default=False
        Sample during readout ramps while preserving the flat-top ADC dwell time.
    sample_prephaser : bool, default=False
        Add ADC sampling during the prephaser.
    sample_balanced : bool, default=False
        Add ADC sampling during the rewinder.
    prephaser_ramp_sampling : bool, default=False
        Enable ramp sampling for the prephaser ADC.
    balanced_ramp_sampling : bool, default=False
        Enable ramp sampling for the rewinder ADC.
    match_prephaser_amp_to_readout : bool, default=False
        Match prephaser and rewinder amplitudes to the readout amplitude.

    Returns
    -------
    cmrseq.Sequence
        Sequence containing the radial readout.
    """
        # First calculate ADC, and determine the actual dwell time
    if num_samples > 0:
        # Get raster-rounnded ADC
        adc = cmrseq.bausteine.SymmetricADC.from_centered_valid(
                system_specs=system_specs,
                num_samples=num_samples,
                duration=adc_duration,
                delay=Quantity(0,'ms'))
        adc_dwell = adc._dwell
        # RO flat duration is set such that it includes all ADC samples + half a dwell time on either side, rounded up to gradient raster
        ro_flatdur = np.around(np.max(np.abs(adc.adc_timing-adc.adc_center)),decimals=8)*2+adc._dwell
        ro_flatdur = system_specs.time_to_raster(ro_flatdur, raster="grad")

        # RO amplitude is based on deltaK
        dk_M = 2.*kr_max/num_samples
        ro_amp = (dk_M / adc_dwell / system_specs.gamma).to("mT/m")
    else:
        # Calculate based on only adc_duration and k_readout
        adc_duration = system_specs.time_to_raster(adc_duration, raster="grad")
        ro_amp = (2 * kr_max / adc_duration / system_specs.gamma).to("mT/m")
        ro_flatdur = adc_duration

    readout_pulse = cmrseq.bausteine.TrapezoidalGradient.from_fdur_amp(
        system_specs=system_specs,
        orientation=np.array([1., 0., 0.]),
        flat_duration=ro_flatdur,
        amplitude=ro_amp, delay=Quantity(0., "ms"),
        name="radial_readout"
    )

    if num_samples > 0 and readout_ramp_sampling:
        adc_dwell_ref = adc._dwell  # dwell from flat-only ADC (reference)
        total_dur = system_specs.time_to_raster(readout_pulse.duration, raster="grad")
        # keep sample spacing: increase sample count to cover full trapezoid. Minus 2 to remove one sample at each end of ramp.
        num_samples_total = int(np.floor((total_dur / adc_dwell_ref).m_as("dimensionless")))
        num_samples_total = max(num_samples_total, num_samples)

        print(f"Ramp sampling: samples {num_samples} -> {num_samples_total} "
            f"(+{num_samples_total - num_samples})")

        adc = cmrseq.bausteine.SymmetricADC.from_centered_valid(
            system_specs=system_specs,
            num_samples=num_samples_total,
            duration=total_dur,
            delay=Quantity(0,'ms'))

        # Assert dwell unchanged (use a tiny tolerance)
        assert abs((adc._dwell - adc_dwell_ref).to("s").magnitude) < 1e-12, \
            f"ADC dwell changed with ramp sampling: {adc._dwell} vs {adc_dwell_ref}"


    if prephaser_duration != Quantity(0., "ms"):
        prephaser_area = readout_pulse.area[0] / 2.
        [_, fastest_prep_ramp, fastest_prep_flatdur] = system_specs.get_shortest_gradient(prephaser_area)

        if prephaser_duration is None:
            prephaser_duration = fastest_prep_flatdur + 2 * fastest_prep_ramp
        else:
            # Check if duration is sufficient for _combined_ prephaser gradients
            if prephaser_duration < np.round(fastest_prep_flatdur + 2 * fastest_prep_ramp, 7):
                raise ValueError("Prephaser duration is to short for combined PE+RO k-space traverse.")

        readout_pulse.shift(prephaser_duration + delay)
        prephaser_pulse = cmrseq.bausteine.TrapezoidalGradient.from_dur_area(
            system_specs=system_specs,
            orientation=np.array([-1., 0., 0.]),
            duration=prephaser_duration,
            area=prephaser_area,
            delay=delay, name="radial_prephaser")

        # prephaser_pulse = cmrseq.bausteine.TrapezoidalGradient.from_fdur_area(
        #     system_specs=system_specs,
        #     orientation=np.array([-1., 0., 0.]),
        #     flat_duration=fastest_prep_flatdur,
        #     area=prephaser_area,
        #     delay=delay, name="radial_prephaser")

        if num_samples > 0:
            adc_delay = prephaser_duration + delay - adc.adc_center + readout_pulse.duration/2
            print(f"ADC delay set to {adc_delay}")
            adc.shift(adc_delay)
            seq = cmrseq.Sequence([prephaser_pulse, readout_pulse, adc],
                                system_specs=system_specs)
        else:
            seq = cmrseq.Sequence([prephaser_pulse, readout_pulse],
                                system_specs=system_specs)
    else:
        if num_samples > 0:
            adc_delay = delay - adc.adc_center + readout_pulse.duration/2
            print(f"ADC delay set to {adc_delay}")
            adc.shift(adc_delay)
            seq = cmrseq.Sequence([readout_pulse, adc],
                                system_specs=system_specs)
        else:
            seq = cmrseq.Sequence([readout_pulse],
                                system_specs=system_specs)


    if balanced:
        # Copy prephasers
        rewind_block = deepcopy(seq.get_block("radial_prephaser_0"))

        # Shift to end of readout
        ro_duration = seq.get_block("radial_readout_0").duration
        rewind_block.shift(ro_duration + rewind_block.duration)

        rewind_block.name = "radial_prephaser_balance"

        seq += cmrseq.Sequence([rewind_block], system_specs=system_specs)

    sa = np.sin(angle).m_as('dimensionless')
    ca = np.cos(angle).m_as('dimensionless')

    omatrix = cmrseq.OMatrix(system_specs=system_specs,
                             position=Quantity(0,'m'),
                             slice_normal=np.array([0,0,1]),
                             readout_direction = np.array([ca,sa,0]))

    seq.register_omatrix(matrix=omatrix, gradients=seq.blocks)

    return seq

radial_3D

radial_3D(
    system_specs: SystemSpec,
    spoke_directions: array,
    samples_per_spoke: int,
    kr_max: Quantity,
    adc_duration: Quantity,
    prephaser_duration: Quantity = None,
    balanced: bool = False,
    partial_fourier=None,
    readout_ramp_sampling: bool = False,
    sample_prephaser: bool = False,
    sample_balanced: bool = False,
    prephaser_ramp_sampling: bool = False,
    balanced_ramp_sampling: bool = False,
    match_prephaser_amp_to_readout: bool = False,
) -> list

Generate 3D radial spokes for the supplied readout directions.

Parameters:

Name Type Description Default
system_specs SystemSpec

System specifications used for rasterization and hardware limits.

required
spoke_directions ndarray

Array with shape (N, 3) containing readout directions in [X, Y, Z].

required
samples_per_spoke int

Number of ADC samples from -kr_max to kr_max per spoke.

required
kr_max Quantity

Maximum k-space radius with units of inverse length.

required
adc_duration Quantity

Total ADC sampling duration for a spoke.

required
prephaser_duration Quantity

Prephaser duration. If omitted, the shortest feasible duration is used.

None
balanced bool

Add a rewinder gradient after the ADC to balance the zeroth moment.

False
partial_fourier optional

Reserved for future partial Fourier support.

None
readout_ramp_sampling bool

Sample during readout ramps while preserving the flat-top ADC dwell time.

False
sample_prephaser bool

Add ADC sampling during the prephaser.

False
sample_balanced bool

Add ADC sampling during the rewinder.

False
prephaser_ramp_sampling bool

Enable ramp sampling for the prephaser ADC.

False
balanced_ramp_sampling bool

Enable ramp sampling for the rewinder ADC.

False
match_prephaser_amp_to_readout bool

Match prephaser and rewinder amplitudes to the readout amplitude.

False

Returns:

Type Description
list[Sequence]

One radial readout sequence for each spoke direction.

Source code in cmrseq/parametric_definitions/readout/_radial.py
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
def radial_3D(system_specs: cmrseq.SystemSpec,
              spoke_directions: np.array,
              samples_per_spoke: int,
              kr_max: Quantity,
              adc_duration: Quantity,
              prephaser_duration: Quantity = None,
              balanced:bool = False,
              partial_fourier = None,
              readout_ramp_sampling: bool = False,
              sample_prephaser: bool = False,
              sample_balanced: bool = False,
              prephaser_ramp_sampling: bool = False,
              balanced_ramp_sampling: bool = False,
              match_prephaser_amp_to_readout: bool = False) -> list:

    r"""Generate 3D radial spokes for the supplied readout directions.

    Parameters
    ----------
    system_specs : cmrseq.SystemSpec
        System specifications used for rasterization and hardware limits.
    spoke_directions : np.ndarray
        Array with shape ``(N, 3)`` containing readout directions in ``[X, Y, Z]``.
    samples_per_spoke : int
        Number of ADC samples from ``-kr_max`` to ``kr_max`` per spoke.
    kr_max : Quantity
        Maximum k-space radius with units of inverse length.
    adc_duration : Quantity
        Total ADC sampling duration for a spoke.
    prephaser_duration : Quantity, optional
        Prephaser duration. If omitted, the shortest feasible duration is used.
    balanced : bool, default=False
        Add a rewinder gradient after the ADC to balance the zeroth moment.
    partial_fourier : optional
        Reserved for future partial Fourier support.
    readout_ramp_sampling : bool, default=False
        Sample during readout ramps while preserving the flat-top ADC dwell time.
    sample_prephaser : bool, default=False
        Add ADC sampling during the prephaser.
    sample_balanced : bool, default=False
        Add ADC sampling during the rewinder.
    prephaser_ramp_sampling : bool, default=False
        Enable ramp sampling for the prephaser ADC.
    balanced_ramp_sampling : bool, default=False
        Enable ramp sampling for the rewinder ADC.
    match_prephaser_amp_to_readout : bool, default=False
        Match prephaser and rewinder amplitudes to the readout amplitude.

    Returns
    -------
    list[cmrseq.Sequence]
        One radial readout sequence for each spoke direction.
    """
    if partial_fourier is not None:
        print("partial_fourier parameter is not implemented yet. ")
    ref_spoke = radial_spoke(system_specs=system_specs,
                            angle=Quantity(0., 'rad'),
                            num_samples=samples_per_spoke,
                            kr_max=kr_max,
                            adc_duration=adc_duration,
                            prephaser_duration=prephaser_duration,
                            balanced=balanced,
                            readout_ramp_sampling=readout_ramp_sampling,
                            sample_prephaser=sample_prephaser,
                            sample_balanced=sample_balanced,
                            prephaser_ramp_sampling=prephaser_ramp_sampling,
                            balanced_ramp_sampling=balanced_ramp_sampling,
                            match_prephaser_amp_to_readout=match_prephaser_amp_to_readout)

    seq_list = []

    for readout_direction in spoke_directions:
        readout_direction = np.asarray(readout_direction, dtype=float)
        readout_direction = readout_direction / np.linalg.norm(readout_direction)

        # Choose a Cartesian axis least aligned with the readout,
        # then use a cross product to get an orthogonal direction.
        ref_axis = np.eye(3)[np.argmin(np.abs(readout_direction))]
        slice_direction = np.cross(readout_direction, ref_axis)
        slice_direction = slice_direction / np.linalg.norm(slice_direction)

        omatrix = cmrseq.OMatrix(system_specs=system_specs,
                                position=Quantity(0,'m'),
                                slice_normal=slice_direction,
                                readout_direction=readout_direction)

        seq = deepcopy(ref_spoke)
        seq.register_omatrix(omatrix,gradients=ref_spoke.blocks)
        seq_list.append(seq)

    return seq_list

radial_3D_spiral_WongRoos

radial_3D_spiral_WongRoos(
    num_interleaves: int,
    spokes_per_interleave: int,
    single_hemisphere: bool = False,
)

Generate 3D radial spoke ordering using the Wong-Roos scheme.

The ordering follows Wong STS, Roos MS. A strategy for sampling on a sphere applied to 3D selective RF pulse design. Magnetic Resonance in Medicine. 1994;32(6):778-784. doi:10.1002/mrm.1910320614

Parameters:

Name Type Description Default
num_interleaves int

Number of spiral interleaves.

required
spokes_per_interleave int

Number of radial spokes per interleave.

required
single_hemisphere bool

Restrict the spiral to a single hemisphere.

False

Returns:

Type Description
ndarray

Array with shape (N, 3) containing spoke directions in [X, Y, Z].

Source code in cmrseq/parametric_definitions/readout/_radial.py
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
def radial_3D_spiral_WongRoos(num_interleaves: int,
                          spokes_per_interleave: int,
                          single_hemisphere: bool = False):
    r"""Generate 3D radial spoke ordering using the Wong-Roos scheme.

    The ordering follows Wong STS, Roos MS. A strategy for sampling on a sphere
    applied to 3D selective RF pulse design. Magnetic Resonance in Medicine.
    1994;32(6):778-784. doi:10.1002/mrm.1910320614

    Parameters
    ----------
    num_interleaves : int
        Number of spiral interleaves.
    spokes_per_interleave : int
        Number of radial spokes per interleave.
    single_hemisphere : bool, default=False
        Restrict the spiral to a single hemisphere.

    Returns
    -------
    np.ndarray
        Array with shape ``(N, 3)`` containing spoke directions in ``[X, Y, Z]``.
    """

    spoke_directions = []

    if not single_hemisphere:
        spoke_directions.append([0,0,1])
    for interleave in range(num_interleaves):
        for spoke in range(spokes_per_interleave):

            if single_hemisphere:
                z = 1 - (spoke + 1) / spokes_per_interleave
            else:
                z = -(2 * (spoke + 1) - spokes_per_interleave - 1) / spokes_per_interleave * (-1) ** interleave

            x = np.cos(np.sqrt(spokes_per_interleave / num_interleaves * np.pi) * np.arcsin(
                z) + 2 * interleave * np.pi / num_interleaves) * np.sqrt(1 - z ** 2)
            y = np.sin(np.sqrt(spokes_per_interleave / num_interleaves * np.pi) * np.arcsin(
                z) + 2 * interleave * np.pi / num_interleaves) * np.sqrt(1 - z ** 2)

            spoke_directions.append([x,y,z])

    return np.array(spoke_directions)

radial_3D_spiral_phyllotaxis

radial_3D_spiral_phyllotaxis(
    num_interleaves: int,
    spokes_per_interleave: int,
    single_hemisphere: bool = False,
)

Generate 3D radial spoke ordering using spiral phyllotaxis.

The ordering follows Piccini D, Littmann A, Nielles-Vallespin S, Zenge MO. Spiral phyllotaxis: The natural way to construct a 3D radial trajectory in MRI. Magnetic Resonance in Medicine. 2011;66(4):1049-1056. doi:10.1002/mrm.22898

Parameters:

Name Type Description Default
num_interleaves int

Number of spiral interleaves.

required
spokes_per_interleave int

Number of radial spokes per interleave.

required
single_hemisphere bool

Restrict the spiral to a single hemisphere.

False

Returns:

Type Description
ndarray

Array with shape (N, 3) containing spoke directions in [X, Y, Z].

Source code in cmrseq/parametric_definitions/readout/_radial.py
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
def radial_3D_spiral_phyllotaxis(num_interleaves: int,
                                 spokes_per_interleave: int,
                                 single_hemisphere: bool = False):
    r"""Generate 3D radial spoke ordering using spiral phyllotaxis.

    The ordering follows Piccini D, Littmann A, Nielles-Vallespin S, Zenge MO.
    Spiral phyllotaxis: The natural way to construct a 3D radial trajectory in
    MRI. Magnetic Resonance in Medicine. 2011;66(4):1049-1056.
    doi:10.1002/mrm.22898

    Parameters
    ----------
    num_interleaves : int
        Number of spiral interleaves.
    spokes_per_interleave : int
        Number of radial spokes per interleave.
    single_hemisphere : bool, default=False
        Restrict the spiral to a single hemisphere.

    Returns
    -------
    np.ndarray
        Array with shape ``(N, 3)`` containing spoke directions in ``[X, Y, Z]``.
    """

    N_total = num_interleaves * spokes_per_interleave
    GA = 137.51 / 180 * np.pi

    if single_hemisphere:  # As per publication
        angles_az = (np.arange(0, N_total) * GA).reshape(spokes_per_interleave, num_interleaves).T
        angles_polar = (np.pi / 2 * np.sqrt(np.arange(0, N_total) / N_total)).reshape(spokes_per_interleave,
                                                                                      num_interleaves).T
    else:  # Modified to continue traverse into second hemisphere
        N_hem1 = np.ceil(N_total / 2)
        N_hem2 = np.floor(N_total / 2)

        # Angles increase with sqrt(n) until equator, then reverse same scaling in second hemisphere
        angles_polar_1 = np.pi / 2 * np.sqrt(np.arange(0, N_hem1) / N_hem1)
        angles_polar_2 = np.pi / 2 * (2 - np.sqrt((N_hem2 - np.arange(0, N_hem2)) / N_hem1))

        # Azimuthal angles follow GA
        angles_az = (np.arange(0, N_total) * GA).reshape(spokes_per_interleave, num_interleaves).T

        # Combine set of angles and reshape into array
        angles_polar = np.concatenate([angles_polar_1, angles_polar_2])
        angles_polar = angles_polar.reshape(spokes_per_interleave, num_interleaves).T

        # Reverse every second spiral
        angles_polar[1::2, :] = np.flip(angles_polar[1::2, :], axis=1)
        angles_az[1::2, :] = np.flip(angles_az[1::2, :], axis=1)

    spoke_directions = []

    for az_interleave, polar_interleave in zip(angles_az, angles_polar):
        interleave = []
        for az, polar in zip(az_interleave, polar_interleave):
            ca = np.cos(az)
            sa = np.sin(az)

            cp = np.cos(polar)
            sp = np.sin(polar)

            spoke_directions.append(np.array([sp * ca, sp * sa, cp]))

    return np.array(spoke_directions)

_spiral

This module contains parametric definitions for generating spiral readouts

spiral_pipezwart

spiral_pipezwart(
    system_specs: SystemSpec,
    interleaves: int,
    kr_max: Quantity,
    kr_delta: Quantity,
    spiral_type: str = "archimedean",
    gradient_rewind_type: str = "ramp down",
    undersampling_type: str = "none",
    undersampling_start: float = 1.0,
    undersampling_end: float = 1.0,
    undersampling_factor: float = 1.0,
    kz_max: Quantity = Quantity(1.0, "1/m"),
    kz_delta: Quantity = Quantity(1.0, "1/m"),
) -> cmrseq.bausteine.ArbitraryGradient

Generates spiral trajectory. Ported from C code provided along with: Pipe JG, Zwart NR. Spiral trajectory design: A flexible numerical algorithm and base analytical equations. Magn. Reson. Med. 2014;71:278–285 doi: 10.1002/mrm.24675.

Original C code can be found at https://www.ismrm.org/mri_unbound/sequence.htm

Some small changes to indexing when defining rewinder gradients and to address slew rate violations

:warning: If not rewound, gradient waveform does not end on 0 magnitude, therefore it is likely to violate subsequent sequence validation.

Parameters:

Name Type Description Default
system_specs SystemSpec

SystemSpecifications

required
interleaves int

number of interleaved spirals

required
kr_max Quantity

:math:FOV_{kr, max} corresponding to minimal radial resolution :math:1/\Delta r

required
kr_delta Quantity

k-space radial step-length

required
spiral_type str

str from ['Archimedean', 'spherical dst'] denoting the type of spiral

'archimedean'
gradient_rewind_type str

From [None, 'ramp down', 'rewind to center'] denoting the type of gradient rewind. If None is specified, the gradient waveform will not end on 0 magnitude, potentially violating subsequent sequence-validation

'ramp down'
undersampling_type str

str from ['linear', 'quadratic', 'hanning'] defining the type of undersampling during acquisition

'none'
undersampling_start float
1.0
undersampling_end float
1.0
undersampling_factor float
1.0
kz_max Quantity
Quantity(1.0, '1/m')
kz_delta Quantity
Quantity(1.0, '1/m')

Returns:

Type Description
Gradient block containing the spiral waveform
Source code in cmrseq/parametric_definitions/readout/_spiral.py
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
def spiral_pipezwart(system_specs: cmrseq.SystemSpec,
                     interleaves: int,
                     kr_max: Quantity,
                     kr_delta: Quantity,
                     spiral_type: str = "archimedean",
                     gradient_rewind_type: str = "ramp down",
                     undersampling_type: str = "none",
                     undersampling_start: float = 1.,
                     undersampling_end: float = 1.,
                     undersampling_factor: float = 1.,
                     kz_max: Quantity = Quantity(1., "1/m"),
                     kz_delta: Quantity = Quantity(1., "1/m")
                     ) -> cmrseq.bausteine.ArbitraryGradient:
    r"""Generates spiral trajectory. Ported from C code provided along with:
    Pipe JG, Zwart NR. Spiral trajectory design: A flexible numerical algorithm and base
    analytical equations. Magn. Reson. Med. 2014;71:278–285 doi: 10.1002/mrm.24675.

    Original C code can be found at https://www.ismrm.org/mri_unbound/sequence.htm

    Some small changes to indexing when defining rewinder gradients and to address slew rate
     violations

    :warning: If not rewound, gradient waveform does not end on 0 magnitude, therefore it is likely
                to violate subsequent sequence validation.

    Parameters
    ----------
    system_specs
        SystemSpecifications
    interleaves
        number of interleaved spirals
    kr_max
        :math:`FOV_{kr, max}` corresponding to minimal radial resolution :math:`1/\Delta r`
    kr_delta
        k-space radial step-length
    spiral_type
        str from ['Archimedean', 'spherical dst'] denoting the type of spiral
    gradient_rewind_type
        From [None, 'ramp down', 'rewind to center'] denoting the type of gradient rewind. If None is specified, the gradient waveform will not end on 0 magnitude, potentially violating subsequent sequence-validation
    undersampling_type
        str from ['linear', 'quadratic', 'hanning'] defining the type of undersampling during acquisition
    undersampling_start
    undersampling_end
    undersampling_factor
    kz_max
    kz_delta

    Returns
    -------
    Gradient block containing the spiral waveform
    """
    ## internal parameters
    raster_subdivision = 4

    max_array = int(100 / system_specs.grad_raster_time.m_as("ms"))  # Assuming maximum 100ms spiral

    internal_raster = system_specs.grad_raster_time / raster_subdivision

    nyquist = interleaves * kr_delta

    gamrast = internal_raster * system_specs.gamma
    dgc = internal_raster * system_specs.max_slew  # max gradient change per internal raster

    sub_gamrast = gamrast * raster_subdivision
    sub_dgc = dgc * raster_subdivision

    # initialize arrays
    gsign = np.ones(raster_subdivision * max_array)
    kx = Quantity(np.zeros(raster_subdivision * max_array), "1/m")
    ky = Quantity(np.zeros(raster_subdivision * max_array), "1/m")
    kz = Quantity(np.zeros(raster_subdivision * max_array), "1/m")
    gxarray = Quantity(np.zeros(max_array), "mT/m")
    gyarray = Quantity(np.zeros(max_array), "mT/m")
    gzarray = Quantity(np.zeros(max_array), "mT/m")

    # start out spiral going radially at max slew-rate for 2 time-points
    kr_lim = kr_max - kr_delta / 2

    kx[1] = gamrast * dgc
    kx[2] = 3 * gamrast * dgc

    if spiral_type.lower() == "spherical dst":
        kz[0] = kz_max
        kz[1] = np.sqrt(kz_max ** 2 * (1 - ((kx[1] ** 2 + ky[1] ** 2) / kr_max ** 2)))
        kz[2] = np.sqrt(kz_max ** 2 * (1 - ((kx[2] ** 2 + ky[2] ** 2) / kr_max ** 2)))

    i = 2
    kr = kx[2]

    # Main loop

    while (kr <= kr_lim) and (i < (raster_subdivision * max_array - 1)):

        # determine k position at i+0.5 given constant velocity
        kmx = 1.5 * kx[i] - 0.5 * kx[i - 1]  # kx[i] + 0.5*(kx[i] - kx[i-1])
        kmy = 1.5 * ky[i] - 0.5 * ky[i - 1]
        kmr = np.sqrt(kmx ** 2 + kmy ** 2)

        # Calculate radial spacing

        rnorm = kmr / kr_max  # normalized k-space radius on [0,1]

        if rnorm <= undersampling_start:
            rad_spacing = 1
        elif rnorm < undersampling_end:
            us_i = (rnorm - undersampling_start) / (undersampling_end - undersampling_start)
            if undersampling_type.lower() == "linear":
                # Linear
                rad_spacing = 1 + (undersampling_factor - 1) * us_i
            elif undersampling_type.lower() == "quadratic":
                # Quadratic
                rad_spacing = 1 + (undersampling_factor - 1) * us_i ** 2
            elif undersampling_type.lower() == "hanning":
                # Hanning
                rad_spacing = 1 + (undersampling_factor - 1) * 0.5 * (1 - np.cos(us_i * np.pi))
            else:
                rad_spacing = 1
        else:
            rad_spacing = undersampling_factor

        # Undersample spiral for Spherical-Distributed Spiral
        if spiral_type.lower() == "spherical dst":
            if rnorm < 1.:
                rad_spacing = min(kz_max / kz_delta, rad_spacing / np.sqrt(1.0 - rnorm ** 2))
            else:
                rad_spacing = kz_max / kz_delta
        # Fermat spiral for floret
        if spiral_type.lower() == "fermat:floret" and rnorm > 0:
            rad_spacing *= 1. / rnorm

        # Set up spiral

        alpha = np.arctan(2 * np.pi * kmr / (rad_spacing * nyquist))
        phi = np.arctan2(kmy, kmx)
        theta = phi + alpha

        ux = np.cos(theta)
        uy = np.sin(theta)
        uz = 0
        gz = 0

        # Spherical DST
        if spiral_type.lower() == "spherical dst":
            kmz = 1.5 * kz[i] - 0.5 * kz[i - 1]
            uz = -((ux * kmx + uy * kmy) / kr_max ** 2) * (kz_max ** 2 / kmz)
            umag = np.sqrt(ux ** 2 + uy ** 2 + uz ** 2)
            ux = ux / umag
            uy = uy / umag
            uz = uz / umag
            gz = (kz[i] - kz[i - 1]) / gamrast

        # Find largest gradient amplitude for max slew

        gx = (kx[i] - kx[i - 1]) / gamrast
        gy = (ky[i] - ky[i - 1]) / gamrast

        term = dgc ** 2 - (gx ** 2 + gy ** 2 + gz ** 2) + (ux * gx + uy * gy + uz * gz) ** 2

        if term >= 0:
            gm = min((ux * gx + uy * gy + uz * gz) + gsign[i] * np.sqrt(term),
                     system_specs.max_grad)
            gx = gm * ux
            gy = gm * uy

            kx[i + 1] = kx[i] + gx * gamrast
            ky[i + 1] = ky[i] + gy * gamrast

            if spiral_type.lower() == "spherical dst":
                kz[i + 1] = np.sqrt(kz_max ** 2
                                    * (1 - ((kx[i + 1] ** 2 + ky[i + 1] ** 2) / kr_max ** 2)))

            i += 1
        else:
            while i > 3 and gsign[i - 1] == -1:
                i -= 1
            gsign[i - 1] = -1
            i = i - 2

        kr = np.sqrt(kx[i] ** 2 + ky[i] ** 2)
    # End of main loop

    # Now work on rewinders

    i_end = i

    gxsum = 0
    gysum = 0
    gzsum = 0
    j = 0
    for j in range(1, int(np.floor(i_end / raster_subdivision))):
        i1 = j * raster_subdivision
        i0 = (j - 1) * raster_subdivision
        gxarray[j] = (kx[i1] - kx[i0]) / sub_gamrast
        gyarray[j] = (ky[i1] - ky[i0]) / sub_gamrast
        gzarray[j] = (kz[i1] - kz[i0]) / sub_gamrast
        gxsum += gxarray[j]
        gysum += gyarray[j]
        gzsum += gzarray[j]

    gm = np.sqrt(gxarray[j] ** 2 + gyarray[j] ** 2 + gzarray[j] ** 2)
    ux = gxarray[j] / gm
    uy = gyarray[j] / gm
    uz = gzarray[j] / gm

    # Ramp to zero gradient
    if gradient_rewind_type is not None and (gradient_rewind_type.lower() == "ramp down"
                                             or gradient_rewind_type.lower() == "rewind to center"):
        gz_sum_ramp = 0

        j += 1

        while gm > 0 and j < max_array - 1:
            gm = max(Quantity(0., "mT/m"), gm - sub_dgc)
            gxarray[j] = gm * ux
            gyarray[j] = gm * uy
            gzarray[j] = gm * uz
            gxsum += gxarray[j]
            gysum += gyarray[j]
            gzsum += gzarray[j]
            gz_sum_ramp += gzarray[j]
            j += 1
    rampdown_end = j

    # Return to k=0
    if gradient_rewind_type is not None and gradient_rewind_type.lower() == "rewind to center":
        # Get direction for rewinder
        gsum = np.sqrt(gxsum ** 2 + gysum ** 2 + gzsum ** 2)
        # Only rewind x and y for spherical DST
        if spiral_type.lower() == "spherical dst":
            gsum = np.sqrt(gxsum ** 2 + gysum ** 2 + gz_sum_ramp ** 2)
        gsum0 = gsum
        ux = -gxsum / gsum
        uy = -gysum / gsum
        uz = -gzsum / gsum
        if spiral_type.lower() == "spherical dst":
            uz = -gz_sum_ramp / gsum
        gsum_ramp = 0.5 * gm * (gm / sub_dgc)

        # Ramp up strength and hold until ramp down will take us just past the center
        while gsum_ramp < gsum and j < (max_array - 1):
            gm = min(system_specs.max_grad, gm + sub_dgc)
            gxarray[j] = gm * ux
            gyarray[j] = gm * uy
            gzarray[j] = gm * uz
            gsum -= gm
            j += 1
            gsum_ramp = 0.5 * gm * (gm / sub_dgc)

        # extra point to prevent slew rate issues
        gm = min(system_specs.max_grad, gm + sub_dgc)
        gxarray[j] = gm * ux
        gyarray[j] = gm * uy
        gzarray[j] = gm * uz
        gsum -= gm
        j += 1

        # ramp down (with some overshoot for now)
        while gm > 0 and j < max_array - 1:
            gm = max(Quantity(0., "mT/m"), gm - sub_dgc)
            gxarray[j] = gm * ux
            gyarray[j] = gm * uy
            gzarray[j] = gm * uz
            gsum -= gm
            j += 1
        rewind_end = j

        # Correct rewinder to take us exactly to k=0
        gradtweak = gsum0 / (gsum0 - gsum)

        if gradtweak > 1:
            raise ValueError("Something went wrong in rewinder calculation, slew rate exceeded")

        for j in range(rampdown_end, rewind_end):
            gxarray[j] *= gradtweak
            gyarray[j] *= gradtweak
            gzarray[j] *= gradtweak

    # end of gradient adjustments

    # Create a gradient objects

    time = system_specs.grad_raster_time * np.arange(0, j + 1)
    wf = np.transpose(
        np.concatenate((gxarray[0:(j + 1), np.newaxis],
                        gyarray[0:(j + 1), np.newaxis],
                        gzarray[0:(j + 1), np.newaxis]), axis=1))
    gradient = cmrseq.bausteine.ArbitraryGradient(system_specs=system_specs,
                                                  time_points=time, waveform=wf,
                                                  name="spiral_readout")
    return gradient

pipe_WHIRLED_PEAS

pipe_WHIRLED_PEAS(
    system_specs: SystemSpec,
    interleaves: int,
    fov: Quantity,
    kr_max: Quantity,
    freq_max: Quantity = Quantity(0, "Hz"),
)

Generates WHIRL trajectory from analytic equations from James Pipe.

Pipe JG. WHIRLED PEAS: Analytical Equations for Spiral Trajectories and Matching Gradient Waveforms. ISMRM Annual Meeting 2023 Original code can be found at https://github.com/jim-pipe/whirled-peas

Parameters:

Name Type Description Default
system_specs SystemSpec

SystemSpecifications

required
interleaves int

number of interleaved spirals

required
fov Quantity

field of view

required
kr_max Quantity

:math:FOV_{kr, max} corresponding to minimal radial resolution :math:1/\Delta r

required
freq_max Quantity

maximum frequency of spiral rotation, optional

Quantity(0, 'Hz')

Returns:

Type Description
object
Source code in cmrseq/parametric_definitions/readout/_spiral.py
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
def pipe_WHIRLED_PEAS(system_specs: cmrseq.SystemSpec,
                      interleaves: int,
                      fov: Quantity,
                      kr_max: Quantity,
                      freq_max: Quantity = Quantity(0,'Hz')):
    r"""Generates WHIRL trajectory from analytic equations from James Pipe.

     Pipe JG. WHIRLED PEAS: Analytical Equations for Spiral Trajectories and Matching Gradient Waveforms.
     ISMRM Annual Meeting 2023
     Original code can be found at https://github.com/jim-pipe/whirled-peas

    Parameters
    ----------
    system_specs
        SystemSpecifications
    interleaves
        number of interleaved spirals
    fov
        field of view
    kr_max
        :math:`FOV_{kr, max}` corresponding to minimal radial resolution :math:`1/\Delta r`
    freq_max
        maximum frequency of spiral rotation, optional

    Returns
    -------
    object
     """


    # convert units to match those of Pipe's code

    delta = (interleaves/(2*np.pi*fov)).m_as('1/m')

    gamma = system_specs.gamma.m_as('Hz/mT')
    m_slew = system_specs.max_slew.m_as('mT/m/s')
    m_grad = system_specs.max_grad.m_as('mT/m')
    grast = system_specs.grad_raster_time.m_as('s')
    krad_max = kr_max.m_as('1/m')
    m_omega = 2*np.pi*freq_max.m_as('Hz')

    # Code take from : https://github.com/jim-pipe/whirled-peas
    # Start of Pipe code with minor modifications:
    #   Remove k-space sample calculation
    #   change math to np
    #   change narms to interleaves
    #   Remove all used of GPI

    ######################################
    # Find compatible constraints, so each segment does not have "negative" duration
    ######################################
    omega1 = np.sqrt(2. * gamma * m_slew / (3. * delta))
    omega2 = 2. * gamma * m_grad / (3. * delta)
    if m_omega > 0:
        omega_max = min([m_omega, omega1, omega2])
    else:
        omega_max = min([omega1, omega2])

    slew1 = m_grad * omega_max
    slew2 = np.sqrt((omega_max ** 4.) * (krad_max * krad_max - delta * delta) / (gamma * gamma))
    slew_max = min([m_slew, slew1, slew2])

    grad1 = ((slew_max * slew_max) * (krad_max * krad_max - delta * delta) / (gamma * gamma)) ** 0.25
    grad_max = min([m_grad, grad1])

    ######################################
    # Find timings
    # segments start at tx0, end at tx1, with total time t_X
    ######################################
    # Arc
    ta0 = 0
    ta1 = (5 * np.pi + 1) / (6. * omega_max)
    t_arc = ta1 - ta0

    # Omega Constrained
    tw0 = 1. / omega_max
    tw1 = (gamma * slew_max) / (delta * omega_max ** 3.)
    t_omega = tw1 - tw0

    # Slew Constrained
    ts0 = (2. * gamma * slew_max) / (3. * delta * omega_max ** 3.)
    ts1 = (2. * gamma * grad_max ** 3.) / (3. * delta * slew_max * slew_max)
    t_slew = ts1 - ts0

    # Gradient Constrained
    tg0 = (gamma * grad_max ** 3.) / (2. * delta * slew_max * slew_max)
    tg1 = (krad_max * krad_max - delta * delta) / (2. * gamma * delta * grad_max)
    t_grad = tg1 - tg0

    # gradient rampdown is a Hanning Window
    # This may help a little with spiral-in (??)
    t_ramp = np.pi * grad_max / m_slew

    tau_total = t_arc + t_omega + t_slew + t_grad
    tgd_total = tau_total + t_ramp

    ##########################
    # Compute waveforms
    ##########################

    gpts = int(tau_total // grast)
    rpts = int(t_ramp // grast)

    # gradient waveforms
    grad_out = np.zeros((interleaves, gpts + rpts, 2))

    ##########################
    # Define some constants
    ##########################
    arc_ta = np.pi / (3. * omega_max)
    arc_tb = (1 + 2. * np.pi) / (6. * omega_max)

    cga = delta * omega_max / (3. * gamma)
    cgw = (delta * omega_max * omega_max / gamma)
    cgs = (3. * delta * slew_max * slew_max / (2. * gamma)) ** (1. / 3.)
    cgg = grad_max

    cta = omega_max / 3.
    ctw = omega_max
    cts = (9. * gamma * slew_max / (4. * delta)) ** (1. / 3.)
    ctg = np.sqrt(2. * gamma * grad_max / delta)

    csa = cga / grad_max
    csw = cgw / grad_max
    css = cgs / grad_max
    csg = cgg / grad_max

    #######################
    # Compute GRADIENT
    #######################
    for i in range(gpts):
        t = float(i) * grast

        # -----
        # ARC |
        # -----
        if t < ta1:
            if t < arc_ta:
                gmag = cga * (1 - np.cos(3. * omega_max * t))
                theta = cta * (t - (np.sin(3. * omega_max * t) / (3. * omega_max)))
                theta = theta + 1 - (0.5 * np.pi)
            elif t < (arc_ta + arc_tb):
                tt = t - arc_ta
                gmag = 2. * cga
                theta = cta * (arc_ta + 2. * tt)
                theta = theta + 1 - (0.5 * np.pi)
            else:
                tt = t - arc_ta - arc_tb
                gmag = cga * (3 - np.cos(3. * omega_max * tt))
                theta = cta * (
                            arc_ta + (2. * arc_tb) + 3. * tt - (np.sin(3. * omega_max * tt) / (3. * omega_max)))
                theta = theta + 1 - (0.5 * np.pi)
        else:
            t = t - ta1 + tw0

            # -----
            # FRQ |
            # -----
            if t < tw1:
                gmag = cgw * t
                theta = ctw * t
            else:
                t = t - tw1 + ts0

                # ------
                # SLEW |
                # ------
                if t < ts1:
                    gmag = cgs * (t ** (1. / 3.))
                    theta = cts * (t ** (2. / 3.))
                else:
                    t = t - ts1 + tg0

                    # ------
                    # GRAD |
                    # ------
                    if t < tg1:
                        gmag = cgg
                        theta = ctg * np.sqrt(t)

        grad_out[0, i, 0] = gmag * np.cos(theta)
        grad_out[0, i, 1] = gmag * np.sin(theta)

    #############################
    # Compute GRADIENT RAMPDOWN #
    #############################

    for i in range(gpts, gpts + rpts):
        t = float(i) * grast - ta1 + tw0 - tw1 + ts0 - ts1 + tg0
        theta = ctg * np.sqrt(t)
        t = float(i - gpts) * grast
        gmag = cgg * 0.5 * (1. + np.cos(np.pi * t / t_ramp))
        grad_out[0, i, 0] = gmag * np.cos(theta)
        grad_out[0, i, 1] = gmag * np.sin(theta)


    # End of Pipe code, now convert to CMRseq format
    time = system_specs.grad_raster_time * np.arange(0, gpts+rpts)
    wf = Quantity(np.transpose(
        np.concatenate((grad_out[0, :, :],
                        np.zeros((gpts+rpts, 1))), axis=1)), 'mT/m')
    gradient = cmrseq.bausteine.ArbitraryGradient(system_specs=system_specs,
                                                  time_points=time, waveform=wf,
                                                  name="WHIRL_readout")

    return cmrseq.Sequence([gradient], system_specs=system_specs)

Sequences

_gradient_echo

This module contains parametric definitions of complete multi-TR GRE-based sequences

flash

flash(
    system_specs: SystemSpec,
    matrix_size: ndarray,
    inplane_resolution: Quantity,
    slice_thickness: Quantity,
    adc_duration: Quantity,
    flip_angle: Quantity,
    pulse_duration: Quantity,
    repetition_time: Quantity,
    echo_time: Quantity,
    slice_position_offset: Quantity = Quantity(0.0, "m"),
    time_bandwidth_product: float = 4.0,
    dummy_shots: int = 0,
    fuse_slice_rewind_and_prephaser: bool = True,
    rf_spoil: bool = True,
    spoiler_strength: Quantity = None,
) -> List[cmrseq.Sequence]

Defines a 2D gradient echo sequence.

Parameters:

Name Type Description Default
system_specs SystemSpec

SystemSpecifications

required
matrix_size ndarray

array of shape (2, ) containing the resulting matrix dimensions

required
inplane_resolution Quantity

Quantity[Length] of shape (2, ) containing the in-plane voxel dimensions

required
slice_thickness Quantity

Quantity[Length] containing the required slice-thickness

required
adc_duration Quantity

Quantity[time] Total duration of adc-sampling for a single TR

required
repetition_time Quantity

Quantity[Time] containing the required repetition_time

required
echo_time Quantity

Quantity[Time] containing the required echo-time. If too short for given system specifications, it is increased to minimum and a warning is raised.

required
flip_angle Quantity

Quantity[Angle] containing the required flip_angle

required
pulse_duration Quantity

Quantity[Time] Total pulse duration (corresponds to flat_duration of the slice selection gradient)

required
slice_position_offset Quantity

Quantity[Length] positional offset in slice normal direction defining the frequency offset of the RF pulse

Quantity(0.0, 'm')
time_bandwidth_product float

float used to calculate the rf bandwidth from duration

4.0
dummy_shots int

number of dummy shots (TRs) without adc-events, with k-space center phase encoding

0
fuse_slice_rewind_and_prephaser bool

If True, the slice selection rewinder is recalculated to match the duration of the prephaser, resulting in the fastest possible 3D k-space traverse.

True
rf_spoil bool

If True, the RF phase is incremented for each TR to achieve spoiling, according to Zur et al (1991)

True

Returns:

Type Description
List of sequence objects, that each represent a single TR
Source code in cmrseq/parametric_definitions/sequences/_gradient_echo.py
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
def flash(system_specs: cmrseq.SystemSpec,
          matrix_size: np.ndarray,
          inplane_resolution: Quantity,
          slice_thickness: Quantity,
          adc_duration: Quantity,
          flip_angle: Quantity,
          pulse_duration: Quantity,
          repetition_time: Quantity,
          echo_time: Quantity,
          slice_position_offset: Quantity = Quantity(0., "m"),
          time_bandwidth_product: float = 4.,
          dummy_shots: int = 0,
          fuse_slice_rewind_and_prephaser: bool = True,
          rf_spoil: bool = True,
          spoiler_strength: Quantity = None) -> List[cmrseq.Sequence]:
    r"""Defines a 2D gradient echo sequence.

    Parameters
    ----------
    system_specs
        SystemSpecifications
    matrix_size
        array of shape (2, ) containing the resulting matrix dimensions
    inplane_resolution
        Quantity[Length] of shape (2, ) containing the in-plane voxel dimensions
    slice_thickness
        Quantity[Length] containing the required slice-thickness
    adc_duration
        Quantity[time] Total duration of adc-sampling for a single TR
    repetition_time
        Quantity[Time] containing the required repetition_time
    echo_time
        Quantity[Time] containing the required echo-time. If too short for given system specifications, it is increased to minimum and a warning is raised.
    flip_angle
        Quantity[Angle] containing the required flip_angle
    pulse_duration
        Quantity[Time] Total pulse duration (corresponds to flat_duration of the slice selection gradient)
    slice_position_offset
        Quantity[Length] positional offset in slice normal direction defining the frequency offset of the RF pulse
    time_bandwidth_product
        float used to calculate the rf bandwidth from duration
    dummy_shots
        number of dummy shots (TRs) without adc-events, with k-space center phase encoding
    fuse_slice_rewind_and_prephaser
        If True, the slice selection rewinder is recalculated to match the duration of the prephaser, resulting in the fastest possible 3D k-space traverse.
    rf_spoil
        If True, the RF phase is incremented for each TR to achieve spoiling, according to Zur et al (1991)

    Returns
    -------
    List of sequence objects, that each represent a single TR
    """

    # Step 0: Create a slice-selective excitation
    rf_seq = cmrseq.seqdefs.excitation.slice_selective_sinc_pulse(
                                    system_specs=system_specs,
                                    slice_thickness=slice_thickness,
                                    flip_angle=flip_angle,
                                    pulse_duration=pulse_duration,
                                    time_bandwidth_product=time_bandwidth_product,
                                    slice_position_offset=slice_position_offset,
                                    slice_normal=np.array([0., 0., 1.]))
    ss_refocus = rf_seq["slice_select_rewind_0"]

    # Step 1: Determine ADC-duration
    k_max_inplane, _, kro_traverse = cmrseq.seqdefs.readout.matrix_to_kspace_2d(matrix_size, inplane_resolution)

    if echo_time is not None: # Case 1: Echo time is defined

        if adc_duration is None: # ADC is not defined, so we try to find the longest possible
            # Time between TE and end of RF slice select gradient
            time_to_fill = echo_time - (rf_seq['slice_select_0'].duration - rf_seq['rf_excitation_0'].rf_events[0])

            if fuse_slice_rewind_and_prephaser:
                add_k_traverse = Quantity([k_max_inplane[1].m_as("1/m"),
                                        (ss_refocus.area[-1] * system_specs.gamma).m_as("1/m")],
                                        "1/m")
            else:
                # In this case, we leave the ss refocus gradient as is
                time_to_fill = time_to_fill - ss_refocus.duration
                add_k_traverse = Quantity([k_max_inplane[1].m_as("1/m"), 0.], "1/m")

            try:
                # We can use a trick here to ensure we get the right TE
                # We use double the available time to fill, and use the balanced variant
                # Since for balanced TE is at the center (symmetric) this solved for the optimal gradients up to TE
                prephaser, _, adc = cmrseq.seqdefs.readout.get_longest_adc_duration(
                                                    system_specs, 2*time_to_fill,
                                                    matrix_size[0], inplane_resolution[0],
                                                    balanced=True,
                                                    additional_kspace_traverse=add_k_traverse)
                internal_adc_duration = adc.duration
            except:
                # Something went wrong, likely the time to fill is not feasible, so we resort the same as case 2
                _, adc = cmrseq.seqdefs.readout.get_shortest_adc_duration(
                                            system_specs, matrix_size[0], inplane_resolution[0])
                internal_adc_duration = adc.duration
        else:
            internal_adc_duration = adc_duration

    else: # Case 2: Echo time is not defined, so we try to minimize it
        if adc_duration is None: # ADC is not defined
            # This is a trivial case, since we can just use the shortest possible ADC duration and TE is whatever we get
            _, adc = cmrseq.seqdefs.readout.get_shortest_adc_duration(system_specs, matrix_size[0], inplane_resolution[0])
            internal_adc_duration = adc.duration
        else:
            internal_adc_duration = adc_duration

    # Step 2: Compute prephaser duration

    # Get example readout gradient
    ro_dummy = cmrseq.seqdefs.readout.gre_cartesian_line(system_specs, matrix_size[0],
                                                    kro_traverse, k_max_inplane[1],
                                                    internal_adc_duration)
    ro_dummy_trap = ro_dummy["trapezoidal_readout_0"]
    ro_dummy_prephaser = ro_dummy["ro_prephaser_0"]

    if fuse_slice_rewind_and_prephaser:

        # Get fastest possible prephaser
        k_max_x = (ro_dummy_prephaser.area[0] * system_specs.gamma).m_as("1/m")
        k_max_y = k_max_inplane[1].m_as("1/m")
        kz_refocus = (ss_refocus.area * system_specs.gamma).m_as("1/m")
        total_kspace_traverse = Quantity([k_max_x, k_max_y, kz_refocus[-1]], "1/m")
        _, _rise, _flat = system_specs.get_fastest_kspace_traverse(total_kspace_traverse)

        if echo_time is not None:
            # Echo time is defined, so we use prephased to fill the gap
            max_prephaser_duration = echo_time - (ro_dummy_trap.duration/2 + rf_seq['slice_select_0'].duration - rf_seq['rf_excitation_0'].rf_events[0])
            max_prephaser_duration = system_specs.time_to_raster(max_prephaser_duration, "grad")

            if max_prephaser_duration < _flat + 2*_rise:
                raise ValueError("Echo time is too short or ADC duration is too long")

            # Get prephaser gradient
            combined_gradient_area = Quantity(np.linalg.norm(total_kspace_traverse.m_as("1/m")), "1/m") / system_specs.gamma.to("1/mT/ms")
            prephaser_total = cmrseq.bausteine.TrapezoidalGradient.from_dur_area(system_specs=system_specs,
                                                                                orientation=np.array([1., 0., 0.]),
                                                                                duration=max_prephaser_duration,
                                                                                area=combined_gradient_area)

            _flat = prephaser_total.flat_duration
            _rise = prephaser_total.rise_time
        prephaser_duration = _flat + 2*_rise
    else:
        # No fusing, but might need to add a delay
        prephaser_duration = ro_dummy_prephaser.duration
        if echo_time is not None:
            max_prephaser_duration = echo_time - (ro_dummy_trap.duration/2 + rf_seq['slice_select_0'].duration - rf_seq['rf_excitation_0'].rf_events[0])
            max_prephaser_duration = system_specs.time_to_raster(max_prephaser_duration, "grad")

            if ro_dummy_prephaser.duration + ss_refocus.duration > max_prephaser_duration:
                raise ValueError("Echo time is too short or ADC duration is too long. Try fuse_slice_rewind_and_prephaser=True ")

            prephaser_delay = max_prephaser_duration - prephaser_duration - ss_refocus.duration
            if np.isclose(prephaser_delay.m_as('ms'), 0., atol=1e-10):
                prephaser_delay = Quantity(0., 'ms')
        else:
            prephaser_delay = Quantity(0., 'ms')

    # Step 3: Create readout blocks
    ro_blocks = cmrseq.seqdefs.readout.multi_line_cartesian(system_specs=system_specs,
                                                            fnc=cmrseq.seqdefs.readout.gre_cartesian_line,
                                                            matrix_size=matrix_size,
                                                            inplane_resolution=inplane_resolution,
                                                            adc_duration=internal_adc_duration,
                                                            prephaser_duration=prephaser_duration,
                                                            dummy_shots=dummy_shots)

    # Step 3.5: Pre-calculate spoilers

    if spoiler_strength is not None:
        k_max_x = (ro_dummy_prephaser.area[0] * system_specs.gamma).m_as("1/m")
        k_max_y = k_max_inplane[1].m_as("1/m")
        kz_refocus = (ss_refocus.area[-1] * system_specs.gamma).m_as("1/m")

        spoiler_M = np.abs((spoiler_strength[0]/Quantity(2*np.pi,'rad')/inplane_resolution[0]).m_as('1/m')) + np.abs(k_max_x)
        spoiler_P = np.abs((spoiler_strength[1]/Quantity(2*np.pi,'rad')/inplane_resolution[1]).m_as('1/m')) + np.abs(k_max_y)
        spoiler_S = (spoiler_strength[2]/Quantity(2*np.pi,'rad')/slice_thickness).m_as('1/m') - np.abs(kz_refocus)

        total_kspace_traverse = Quantity([spoiler_M, spoiler_P, spoiler_S], "1/m")

        _, spoiler_rise, spoiler_flat = system_specs.get_fastest_kspace_traverse(total_kspace_traverse)

    # Step 4: Check TR and add delay if needed. If too short, override
    tr_delay = Quantity(0,'ms')
    if repetition_time is not None:
        if fuse_slice_rewind_and_prephaser:
            minimal_tr = ro_blocks[0].get_block("trapezoidal_readout_0").duration + rf_seq.duration - ss_refocus.duration + prephaser_duration
        else:
            minimal_tr = ro_blocks[0].get_block("trapezoidal_readout_0").duration + rf_seq.duration + prephaser_duration

        if spoiler_strength is not None:
            minimal_tr += Quantity(spoiler_flat + 2*spoiler_rise, "ms")

        if minimal_tr > system_specs.time_to_raster(repetition_time,"grad"):
            warn(f"Repetition time too short to be feasible, set TR to {minimal_tr}")
            repetition_time = minimal_tr

        tr_delay = system_specs.time_to_raster(repetition_time,"grad") - minimal_tr
        if np.isclose(tr_delay.m_as('ms'), 0., atol=1e-10):
            tr_delay = Quantity(0., 'ms')

    # Step 5: Re-generate SS rewind and readout prephaser if fusing
    if fuse_slice_rewind_and_prephaser:
        ss_rewind_amp = ss_refocus.area[-1]/(_flat + _rise)
        ss_rewind = cmrseq.bausteine.TrapezoidalGradient(system_specs,
                                                np.array([0., 0., -1.]),
                                                flat_duration=_flat,
                                                rise_time=_rise,
                                                amplitude=ss_rewind_amp,
                                                name="slice_select_rewind")

        ro_prephaser_amp = ro_blocks[0]['ro_prephaser_0'].area[0]/(_flat + _rise)
        ro_prephaser = cmrseq.bausteine.TrapezoidalGradient(system_specs,
                                                np.array([-1., 0., 0.]),
                                                flat_duration=_flat,
                                                rise_time=_rise,
                                                amplitude=ro_prephaser_amp,
                                                name="ro_prephaser")

    # Step 6: Build sequence
    seq_list = []

    phase_offset = Quantity(0, 'degree')
    pi_phase = Quantity(360, 'degree')
    # Zur et al (1991)
    rf_incr = Quantity(117, 'degree')

    for j, ro_b in enumerate(ro_blocks):

        seq = deepcopy(rf_seq)
        # RF spoiling
        if rf_spoil:
            seq['rf_excitation_0'].phase_offset = phase_offset
            if ro_b.get_block('adc_0') is not None:
                ro_b.get_block('adc_0').phase_offset = phase_offset

            phase_offset = (phase_offset + rf_incr) % pi_phase

        # Build spoiler if needed
        if spoiler_strength is not None:

            spoil_M = -ro_b['ro_prephaser_0'].area[0]
            spoil_P = - ro_b['pe_prephaser_0'].area[1] * np.sign(ro_b['pe_prephaser_0'].gradients[1][:,1][1])
            spoil_S = -ss_refocus.area[2]

            spoil_M += spoiler_strength[0]/system_specs.gamma_rad/inplane_resolution[0]
            spoil_P += spoiler_strength[1]/system_specs.gamma_rad/inplane_resolution[1]
            spoil_S += spoiler_strength[2]/system_specs.gamma_rad/slice_thickness

            total_spoil = np.sqrt(spoil_M**2 + spoil_P**2 + spoil_S**2)

            spoil_dir = (np.array([spoil_M.m_as('mT/m*ms'), spoil_P.m_as('mT/m*ms'), spoil_S.m_as('mT/m*ms')]) / total_spoil.m_as('mT/m*ms'))

            amp_spoil = total_spoil / (spoiler_flat + spoiler_rise)

            spoiler = cmrseq.bausteine.TrapezoidalGradient(system_specs,
                                        orientation = spoil_dir,
                                        flat_duration=spoiler_flat,
                                        rise_time=spoiler_rise,
                                        amplitude=amp_spoil,
                                        name="spoiler")


        if fuse_slice_rewind_and_prephaser:
            # Produce new pe prephaser and replace all prephasers
            pe_prephaser_amp = ro_b['pe_prephaser_0'].area[1]/(_flat + _rise)
            dir_norm = np.sqrt(np.sum(ro_b['pe_prephaser_0'].gradients[1][:,1]**2))
            if not dir_norm == 0:
                pedir = (ro_b['pe_prephaser_0'].gradients[1][:,1] / dir_norm).m_as("dimensionless")
            else:
                pedir = np.array([0., 1., 0.])
            pe_prephaser = cmrseq.bausteine.TrapezoidalGradient(system_specs,
                                                    pedir,
                                                    flat_duration=_flat,
                                                    rise_time=_rise,
                                                    amplitude=pe_prephaser_amp,
                                                    name="pe_prephaser")

            ro_b.remove_block("pe_prephaser_0")
            ro_b.remove_block("ro_prephaser_0")
            seq.remove_block("slice_select_rewind_0")

            ro_b.add_block(ro_prephaser)
            ro_b.add_block(pe_prephaser)
            ro_b.add_block(ss_rewind)
        else:
            # Add the prephaser delay to the sequence
            if prephaser_delay > 0:
                seq.append(cmrseq.bausteine.Delay(system_specs, prephaser_delay))

        seq.append(ro_b)
        if spoiler_strength is not None:
            seq.append(spoiler)
        # Add TR delay if needed
        if tr_delay > 0:
            seq.append(cmrseq.bausteine.Delay(system_specs, tr_delay))
        seq_list.append(seq)
    return seq_list

radial_flash

radial_flash(
    system_specs: SystemSpec,
    samples_per_spoke: int,
    inplane_resolution: Quantity,
    slice_thickness: Quantity,
    adc_duration: Quantity,
    flip_angle: Quantity,
    pulse_duration: Quantity,
    repetition_time: Quantity,
    echo_time: Quantity,
    spoke_angle_increment: Quantity = None,
    num_spokes: int = None,
    slice_position_offset: Quantity = Quantity(0.0, "m"),
    time_bandwidth_product: float = 4.0,
    dummy_shots: int = 0,
    fuse_slice_rewind_and_prephaser: bool = True,
) -> List[cmrseq.Sequence]

Defines a 2D radial FLASH sequence. Not as optimized as cartesian FLASH.

Parameters:

Name Type Description Default
system_specs SystemSpec

SystemSpecification

required
samples_per_spoke int

number of samples per spoke, i.e. number of adc-events per TR

required
inplane_resolution Quantity

Isotropic in-plane resolution, defines max kspace radius. Quantity[Length]

required
slice_thickness Quantity

Quantity[Length] containing the required slice-thickness

required
adc_duration Quantity

Quantity[time] Total duration of adc-sampling for a single TR

required
flip_angle Quantity

Quantity[Angle] containing the required flip_angle

required
pulse_duration Quantity

Quantity[Time] Total pulse duration (corresponds to flat_duration of the slice selection gradient)

required
repetition_time Quantity

Quantity[Time] containing the desired repetition time

required
echo_time Quantity

Quantity[Time] containing the desired echo time

required
spoke_angle_increment Quantity

Quantity[Angle] angle increment between spokes, if None, sets to uniformly fill 2pi

None
num_spokes int

number of spokes to acquire, if None, if None, defaults to satisfy nyquist

None
slice_position_offset Quantity

Quantity[Length] positional offset in slice normal direction defining the frequency offset of the RF pulse

Quantity(0.0, 'm')
time_bandwidth_product float

float used to calculate the rf bandwidth from duration

4.0
dummy_shots int

number of shots(TRs) without adc-events before starting the acquisition

0
fuse_slice_rewind_and_prephaser bool

If True, the slice selection rewinder is recalculated to match the duration of the prephaser, resulting in the fastest possible 3D k-space traverse.

True

Returns:

Type Description
List of length (n_dummy+matrix_size[1]) containting one Sequence object per TR
Source code in cmrseq/parametric_definitions/sequences/_gradient_echo.py
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
def radial_flash(system_specs: cmrseq.SystemSpec,
                 samples_per_spoke: int,
                 inplane_resolution: Quantity,
                 slice_thickness: Quantity,
                 adc_duration: Quantity,
                 flip_angle: Quantity,
                 pulse_duration: Quantity,
                 repetition_time: Quantity,
                 echo_time: Quantity,
                 spoke_angle_increment: Quantity = None,
                 num_spokes: int = None,
                 slice_position_offset: Quantity = Quantity(0., "m"),
                 time_bandwidth_product: float = 4.,
                 dummy_shots: int = 0,
                 fuse_slice_rewind_and_prephaser: bool = True) -> List[cmrseq.Sequence]:

    r"""Defines a 2D radial FLASH sequence. Not as optimized as cartesian FLASH.

    Parameters
    ----------
    system_specs
        SystemSpecification
    samples_per_spoke
        number of samples per spoke, i.e. number of adc-events per TR
    inplane_resolution
        Isotropic in-plane resolution, defines max kspace radius. Quantity[Length]
    slice_thickness
        Quantity[Length] containing the required slice-thickness
    adc_duration
        Quantity[time] Total duration of adc-sampling for a single TR
    flip_angle
        Quantity[Angle] containing the required flip_angle
    pulse_duration
        Quantity[Time] Total pulse duration (corresponds to flat_duration of the slice selection gradient)
    repetition_time
        Quantity[Time] containing the desired repetition time
    echo_time
        Quantity[Time] containing the desired echo time
    spoke_angle_increment
        Quantity[Angle] angle increment between spokes, if None, sets to uniformly fill 2pi
    num_spokes
        number of spokes to acquire, if None, if None, defaults to satisfy nyquist
    slice_position_offset
        Quantity[Length] positional offset in slice normal direction defining the frequency offset of the RF pulse
    time_bandwidth_product
        float used to calculate the rf bandwidth from duration
    dummy_shots
        number of shots(TRs) without adc-events before starting the acquisition
    fuse_slice_rewind_and_prephaser
        If True, the slice selection rewinder is recalculated to match the duration of the prephaser, resulting in the fastest possible 3D k-space traverse.

    Returns
    -------
    List of length (n_dummy+matrix_size[1]) containting one Sequence object per TR
    """

    rf_seq = cmrseq.seqdefs.excitation.slice_selective_sinc_pulse(
        system_specs=system_specs,
        slice_thickness=slice_thickness,
        flip_angle=flip_angle,
        pulse_duration=pulse_duration,
        time_bandwidth_product=time_bandwidth_product,
        slice_position_offset=slice_position_offset,
        slice_normal=np.array([0., 0., 1.]))
    ss_refocus = rf_seq.get_block("slice_select_rewind_0")

    kr_max = 1 / (2 * inplane_resolution.m_as("m"))

    if fuse_slice_rewind_and_prephaser:
        # Recalculate ss-gradient combined with ro prephaser

        kz_refocus = (ss_refocus.area * system_specs.gamma).m_as("1/m")

        total_kspace_traverse = Quantity(np.linalg.norm([kr_max, kz_refocus[-1]]), "1/m")
        combined_gradient_area = total_kspace_traverse / system_specs.gamma.to("1/mT/ms")
        prephaser_duration = cmrseq.bausteine.TrapezoidalGradient.from_area(
            system_specs, np.array([1., 0., 0]), combined_gradient_area).duration

        rf_seq.remove_block("slice_select_rewind_0")
        ss_refocus = cmrseq.bausteine.TrapezoidalGradient.from_dur_area(system_specs,
                                                                        np.array([0., 0., -1.]),
                                                                        prephaser_duration,
                                                                        ss_refocus.area[-1],
                                                                        delay=rf_seq.duration,
                                                                        name="slice_select_rewind")
        rf_seq.add_block(ss_refocus)
    else:
        prephaser_duration = None

    ro_ref = cmrseq.seqdefs.readout.radial_spoke(system_specs=system_specs, num_samples=samples_per_spoke,
                                                 kr_max=Quantity(kr_max,'1/m'), angle=Quantity(0, 'rad'),
                                                 adc_duration=adc_duration,
                                                 prephaser_duration=prephaser_duration)

    dummy_ref = cmrseq.seqdefs.readout.radial_spoke(system_specs=system_specs, num_samples=0,
                                                    kr_max=Quantity(kr_max,'1/m'), angle=Quantity(0, 'rad'),
                                                    adc_duration=adc_duration,
                                                    prephaser_duration=prephaser_duration)

    if prephaser_duration is None:
        prephaser_duration = ro_ref.get_block("radial_prephaser_0").duration

    readout_gradient_duration = ro_ref.get_block("radial_readout_0").duration
    max_ssref_prephaser = max(ss_refocus.duration, prephaser_duration)
    adc_center = system_specs.time_to_raster(ro_ref.get_block('adc_0').adc_center)

    if fuse_slice_rewind_and_prephaser:
        minimal_tr = readout_gradient_duration + max_ssref_prephaser + rf_seq.duration - ss_refocus.duration
        minimal_te = (rf_seq.duration - rf_seq.get_block("rf_excitation_0").rf_events[0] - ss_refocus.duration +
                      max_ssref_prephaser + adc_center - prephaser_duration)
    else:
        minimal_tr = ro_ref.duration + rf_seq.duration
        minimal_te = (rf_seq.duration - rf_seq.get_block("rf_excitation_0").rf_events[0] + adc_center)


    repetition_time = system_specs.time_to_raster(repetition_time)
    if repetition_time < minimal_tr:
        warn(f"Radial FLASH Sequence: Repetition time too short to be feasible, set TR to {minimal_tr}")
        repetition_time = minimal_tr

    maximum_te = repetition_time - (readout_gradient_duration - adc_center + prephaser_duration) \
                 - rf_seq.get_block("rf_excitation_0").rf_events[0]

    echo_time = system_specs.time_to_raster(echo_time)
    if echo_time < minimal_te:
        warn(f"Radial FLASH Sequence: Echo time too short to be feasible, set TE to {minimal_te}")
        echo_time = minimal_te

    if echo_time > maximum_te:
        warn(f"Radial FLASH Sequence: Echo time too long for given TR, set TE to {maximum_te}")
        echo_time = maximum_te

    te_shift = echo_time - minimal_te
    tr_delay = repetition_time - minimal_tr - te_shift

    # Concatenate readout blocks
    seq_list = []

    for _ in range(dummy_shots):
        cur_ro = deepcopy(dummy_ref)
        if fuse_slice_rewind_and_prephaser:
            cur_ro.shift_in_time(
                rf_seq.duration - min(ss_refocus.duration, prephaser_duration) + te_shift)
        else:
            cur_ro.shift_in_time(
                rf_seq.duration + te_shift)
        seq = rf_seq + cur_ro
        seq.append(cmrseq.bausteine.Delay(system_specs, tr_delay))
        seq_list.append(seq)

    if num_spokes is None:
        if spoke_angle_increment is not None:
            warn(f"Radial FLASH Sequence: Can not set spoke angle increment without "
                 f"setting number of spokes, defaulting to satisfy nyquist")

        num_spokes = np.ceil(samples_per_spoke*np.pi/2) # Nyquist criteria for radial sampling

        spoke_angles = np.linspace(0,np.pi,int(num_spokes),endpoint=False)
    else:
        if spoke_angle_increment is None:
            warn(f"Radial FLASH Sequence: Spoke angle not set while spoke count set,"
                 f" defaulting to even spacing of spokes")
            spoke_angles = np.linspace(0,np.pi,int(num_spokes),endpoint=False)
        else:
            spoke_angles = np.array(range(num_spokes))*spoke_angle_increment.to('rad').m_as('dimensionless')

    for angle in spoke_angles:
        cur_ro = deepcopy(ro_ref)

        sa = np.sin(angle)
        ca = np.cos(angle)
        omatrix = cmrseq.OMatrix(system_specs=system_specs,
                            position=Quantity(0,'m'),
                            slice_normal=np.array([0,0,1]),
                            readout_direction = np.array([ca,sa,0]))
        cur_ro.register_omatrix(matrix=omatrix, gradients=seq.blocks)

        if fuse_slice_rewind_and_prephaser:
            cur_ro.shift_in_time(
                rf_seq.duration - min(ss_refocus.duration, prephaser_duration) + te_shift)
        else:
            cur_ro.shift_in_time(
                rf_seq.duration + te_shift)
        seq = rf_seq + cur_ro
        seq.append(cmrseq.bausteine.Delay(system_specs, tr_delay))
        seq_list.append(seq)

    return seq_list

_spin_echo

This module contains parametric definitions of complete multi-TR SE-based sequences

single_line_cartesian2d

single_line_cartesian2d(
    system_specs: SystemSpec,
    echo_time: Quantity,
    repetition_time: Quantity,
    matrix_size: ndarray,
    inplane_resolution: Quantity,
    slice_thickness: Quantity,
    adc_duration: Quantity,
    pulse_duration: Quantity,
    time_bandwidth_product: float = 4.0,
) -> List[cmrseq.Sequence]

Constructs a basis spin echo single line acquisition scheme for a cartesian trajectory.

Parameters:

Name Type Description Default
system_specs SystemSpec

SystemSpecification

required
echo_time Quantity
required
repetition_time Quantity

Quantity[Time] containing the required repetition_time

required
matrix_size ndarray

array of shape (2, )

required
inplane_resolution Quantity

Quantity[Length] of shape (2, )

required
slice_thickness Quantity

Quantity[Length] containing the required slice-thickness

required
adc_duration Quantity

Quantity[time] Total duration of adc-sampling for a single TR

required
pulse_duration Quantity

Quantity[Time] Total pulse duration (corresponds to flat_duration of the slice selection gradient)

required
time_bandwidth_product float

float used to calculate the rf bandwidth from duration

4.0

Returns:

Type Description
List of length (matrix_size[1]) containting one Sequence object per TR

Raises:

Type Description
ValueError

if repetition time is smaller than the composite of elements within one TR

Source code in cmrseq/parametric_definitions/sequences/_spin_echo.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
def single_line_cartesian2d(system_specs: 'cmrseq.SystemSpec',
                            echo_time: Quantity,
                            repetition_time: Quantity,
                            matrix_size: np.ndarray,
                            inplane_resolution: Quantity,
                            slice_thickness: Quantity,
                            adc_duration: Quantity,
                            pulse_duration: Quantity,
                            time_bandwidth_product: float = 4.) -> List[cmrseq.Sequence]:
    r"""Constructs a basis spin echo single line acquisition scheme for a cartesian
    trajectory.

    Parameters
    ----------
    system_specs
        SystemSpecification
    echo_time
    repetition_time
        Quantity[Time] containing the required repetition_time
    matrix_size
        array of shape (2, )
    inplane_resolution
        Quantity[Length] of shape (2, )
    slice_thickness
        Quantity[Length] containing the required slice-thickness
    adc_duration
        Quantity[time] Total duration of adc-sampling for a single TR
    pulse_duration
        Quantity[Time] Total pulse duration (corresponds to flat_duration of the slice selection gradient)
    time_bandwidth_product
        float used to calculate the rf bandwidth from duration

    Returns
    -------
    List of length (matrix_size[1]) containting one Sequence object per TR

    Raises
    ------
    ValueError
        if repetition time is smaller than the composite of elements within one TR
    """

    rf_block = cmrseq.seqdefs.excitation.slice_selective_se_pulses(
                                                    system_specs, echo_time,
                                                    slice_thickness=slice_thickness,
                                                    pulse_duration=pulse_duration,
                                                    slice_orientation=np.array([0., 0., 1]),
                                                    time_bandwidth_product=time_bandwidth_product)
    ss_ramptime = (rf_block.gradients[0][0][1] - rf_block.gradients[0][0][0])
    ro_blocks = cmrseq.seqdefs.readout.multi_line_cartesian(
                                            system_specs=system_specs,
                                            fnc=cmrseq.seqdefs.readout.se_cartesian_line,
                                            matrix_size=matrix_size,
                                            inplane_resolution=inplane_resolution,
                                            echo_time=echo_time,
                                            pulse_duration=pulse_duration + 2 * ss_ramptime,
                                            excitation_center_time=pulse_duration / 2 + ss_ramptime,
                                            adc_duration=adc_duration)

    if repetition_time < (ro_blocks[0] + rf_block).duration:
        raise ValueError("Specified repetition time is too short")

    sequence_list = []
    for readout_block in ro_blocks:
        seq = readout_block + rf_block
        seq.append(cmrseq.bausteine.Delay(system_specs, repetition_time - seq.duration))
        sequence_list.append(seq)
    return sequence_list

se_ssepi

se_ssepi(
    system_specs: SystemSpec,
    field_of_view: Quantity,
    matrix_size: ndarray,
    echo_time: Quantity,
    slice_thickness: Quantity,
    slice_orientation: ndarray,
    pulse_duration: Quantity,
    epi_slope_sampling: bool = False,
    tbw_product: float = 4,
    max_epi_duration: Quantity = None,
    epi_water_fat_shift: Union[str, float, int] = "minimum",
    partial_fourier_lines: int = 0,
    blip_direction: str = "up",
)

Defines a single shot EPI sequence. If the specified echo time is too short, the shortest possible echo-time is used.

.. note::

The sequence object returned by this function contains the SimpleNamespace "additional_info"
as attribute, which contains the k-space center index, actually used echo-time and the
absolute value of the echo-formation time

Parameters:

Name Type Description Default
system_specs SystemSpec

SystemSpecification

required
field_of_view Quantity

spatial extend in readout and phase encoding direction; shape = (2, )

required
matrix_size ndarray

number of pixels in readout and phase encoding direction; shape = (2, )

required
echo_time Quantity

Time at which the central k-space line is placed

required
slice_thickness Quantity

Thickness of slice-selective excitation definitions

required
slice_orientation ndarray

Slice normal of excitation slice

required
pulse_duration Quantity

Duration of the excitation & refocusing pulses

required
epi_slope_sampling bool

If yes the epi readout uses slope sampling

False
tbw_product float

Time-bandwidth product of the inc-Pulses used for excitation and refocus

4
max_epi_duration Quantity

See documentation (cmrseq.seqdefs.readout.single_shot_epi)

None
epi_water_fat_shift Union[str, float, int]

See documentation (cmrseq.seqdefs.readout.single_shot_epi)

'minimum'
partial_fourier_lines int

number of lines to skip before k-space center, allowing shorter echo times

0
blip_direction str

from ["up", "down"] defining the direction of phase-encoding kspace travers

'up'

Returns:

Type Description
sequence object
Source code in cmrseq/parametric_definitions/sequences/_spin_echo.py
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
def se_ssepi(system_specs: cmrseq.SystemSpec,
             field_of_view: Quantity,
             matrix_size: np.ndarray,
             echo_time: Quantity,
             slice_thickness: Quantity,
             slice_orientation: np.ndarray,
             pulse_duration: Quantity,
             epi_slope_sampling: bool = False,
             tbw_product: float = 4,
             max_epi_duration: Quantity = None,
             epi_water_fat_shift: Union[str, float, int] = "minimum",
             partial_fourier_lines: int = 0,
             blip_direction: str = "up"):
    r"""Defines a single shot EPI sequence. If the specified echo time is too short, the shortest
    possible echo-time is used.

    .. note::

        The sequence object returned by this function contains the SimpleNamespace "additional_info"
        as attribute, which contains the k-space center index, actually used echo-time and the
        absolute value of the echo-formation time



    Parameters
    ----------
    system_specs
        SystemSpecification
    field_of_view
        spatial extend in readout and phase encoding direction; shape = (2, )
    matrix_size
        number of pixels in readout and phase encoding direction; shape = (2, )
    echo_time
        Time at which the central k-space line is placed
    slice_thickness
        Thickness of slice-selective excitation definitions
    slice_orientation
        Slice normal of excitation slice
    pulse_duration
        Duration of the excitation & refocusing pulses
    epi_slope_sampling
        If yes the epi readout uses slope sampling
    tbw_product
        Time-bandwidth product of the inc-Pulses used for excitation and refocus
    max_epi_duration
        See documentation (cmrseq.seqdefs.readout.single_shot_epi)
    epi_water_fat_shift
        See documentation (cmrseq.seqdefs.readout.single_shot_epi)
    partial_fourier_lines
        number of lines to skip before k-space center, allowing shorter echo times
    blip_direction
        from ["up", "down"] defining the direction of phase-encoding kspace travers

    Returns
    -------
    sequence object
    """
    # Define EPI-readout
    readout = cmrseq.seqdefs.readout.single_shot_epi(system_specs=system_specs,
                                                     field_of_view=field_of_view,
                                                     matrix_size=matrix_size,
                                                     blip_direction=blip_direction,
                                                     partial_fourier_lines=partial_fourier_lines,
                                                     slope_sampling=epi_slope_sampling,
                                                     water_fat_shift=epi_water_fat_shift,
                                                     max_total_duration=max_epi_duration)
    k_center_idx = int(np.floor(matrix_size[1] / 2) - partial_fourier_lines)

    # Construct the excitation blocks
    excitation = cmrseq.seqdefs.excitation.slice_selective_se_pulses(
                                                system_specs=system_specs,
                                                echo_time=echo_time,
                                                slice_thickness=slice_thickness,
                                                pulse_duration=pulse_duration,
                                                slice_orientation=slice_orientation,
                                                time_bandwidth_product=tbw_product)

    # check if minimal TE is smaller than the specified TE
    epi_duration_to_center = readout.adc_centers[k_center_idx]
    rf_post_duration = excitation.end_time - excitation.rf_events[-1][0]
    minimal_te = system_specs.time_to_raster((epi_duration_to_center + rf_post_duration)) * 2

    if minimal_te > echo_time:
        warn("SSEPI Sequence: TE is shorter than possible for the given readout and diffusion "
            f"weighting configuration. Setting the echo time to {minimal_te}")
        refocus = excitation.partial_sequence(partial_string_match=['rf_excitation_0', 'slice_select_refocus_0'],
                                              copy_blocks=False)
        refocus.shift_in_time((minimal_te - echo_time) / 2)
        echo_time = minimal_te

    ro_shift = system_specs.time_to_raster(
        echo_time - readout.adc_centers[k_center_idx] + excitation.rf_events[0][0], "grad")
    readout.shift_in_time(ro_shift)

    seq = excitation + readout
    seq.additional_info = SimpleNamespace(kcenter_idx=k_center_idx,
                                          echo_time=echo_time,
                                          echo_formation_time=readout.adc_centers[k_center_idx])
    return seq

_ssfp

This module contains parametric definitions of complete multi-TR balanced SSFP sequences

balanced_ssfp

balanced_ssfp(
    system_specs: SystemSpec,
    matrix_size: ndarray,
    inplane_resolution: Quantity,
    slice_thickness: Quantity,
    adc_duration: Quantity,
    flip_angle: Quantity,
    pulse_duration: Quantity,
    repetition_time: Quantity,
    slice_position_offset: Quantity = Quantity(0.0, "m"),
    time_bandwidth_product: float = 4.0,
    dummy_shots: int = None,
    dummy_scheme: str = "Linear",
    fuse_slice_rewind_and_prephaser: bool = True,
) -> List[cmrseq.Sequence]

Defines a balanced steady state free precession sequence with a/2-TR/2 preparation, with a cartesian readout.

Assumptions in temporal optimization for combinations of specified arguments:

  • Neither TR nor adc_duration: Timing is optimized to have minimal TR, hence also shortest possible ADC-duration
  • TR and adc_duration is provided: Padding around the readout gradient is applied to match TR if needed. If ADC is longer than possible, TR is set to minimal feasible value, marked by a warning.
  • TR specified, adc_duration is None: ADC-duration is maximized, according to given TR
  • TR is None, adc_duration is specified: TR is set to minimally possible value for given adc-duration

In all cases the gradient limits for combined k-space traverse during the prephaser is respected, both for fusing and not fusing the slice select rewinder with the phase and readout prephaser.

.. code-block::

.                 |                  TR                  |                 .
.                     |       TE         |                                 .
.                                                                          .
.            RF:     /\                                                    .
.            ADC   \/  \/      |||||||||||||||||||||                       .
.                  ______                                                  .
.            SS:  /      \    _______________________                      .
.                         \__/                       \__/                  .
.                              _____________________                       .
.            RO:  ________    /                     \                      .
.                         \__/                       \__/                  .
.                                                    __                    .
.            PE:  ________    ______________________/  \                   .
.                         \__/                                             .

Parameters:

Name Type Description Default
system_specs SystemSpec

SystemSpecification

required
matrix_size ndarray

array of shape (2, )

required
inplane_resolution Quantity

Quantity[Length] of shape (2, )

required
repetition_time Quantity

Quantity[Time] containing the required repetition_time If None or too short, the shortest possible time under system constraints is used.

required
slice_thickness Quantity

Quantity[Length] containing the required slice-thickness

required
adc_duration Quantity

Quantity[time] Total duration of adc-sampling for a single TR

required
flip_angle Quantity

Quantity[Angle] containing the required flip_angle

required
pulse_duration Quantity

Quantity[Time] Total pulse duration (corresponds to flat_duration of the slice selection gradient)

required
slice_position_offset Quantity

Quantity[Length] positional offset in slice normal direction defining the frequency offset of the RF pulse

Quantity(0.0, 'm')
time_bandwidth_product float

float used to calculate the rf bandwidth from duration

4.0
dummy_shots int

number of shots(TRs) without adc-events before starting the acquisition

None
dummy_scheme str

str scheme to use for dummy shots, options are "Linear" or "AlphaHalf"

'Linear'
fuse_slice_rewind_and_prephaser bool

If True, the slice selection rewinder is recalculated to match the duration of the prephaser, resulting in the fastest possible 3D k-space traverse.

True

Returns:

Type Description
List of length (n_dummy+matrix_size[1]) containting one Sequence object per TR
Source code in cmrseq/parametric_definitions/sequences/_ssfp.py
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
def balanced_ssfp(system_specs: cmrseq.SystemSpec,
                  matrix_size: np.ndarray,
                  inplane_resolution: Quantity,
                  slice_thickness: Quantity,
                  adc_duration: Quantity,
                  flip_angle: Quantity,
                  pulse_duration: Quantity,
                  repetition_time: Quantity,
                  slice_position_offset: Quantity = Quantity(0., "m"),
                  time_bandwidth_product: float = 4.,
                  dummy_shots: int = None,
                  dummy_scheme: str = "Linear",
                  fuse_slice_rewind_and_prephaser: bool = True) -> List[cmrseq.Sequence]:
    r"""Defines a balanced steady state free precession sequence with a/2-TR/2 preparation,
    with a cartesian readout.

    Assumptions in temporal optimization for combinations of specified arguments:

    - *Neither TR nor adc_duration*:
        Timing is optimized to have minimal TR, hence also shortest possible ADC-duration
    - *TR and adc_duration is provided*:
        Padding around the readout gradient is applied to match TR if needed. If ADC is longer
         than possible, TR is set to minimal feasible value, marked by a warning.
    - *TR specified, adc_duration is None*:
        ADC-duration is maximized, according to given TR
    - *TR is None, adc_duration is specified*:
        TR is set to minimally possible value for given adc-duration

    In all cases the gradient limits for combined k-space traverse during the prephaser
    is respected, both for fusing and not fusing the slice select rewinder with the
    phase and readout prephaser.

    .. code-block::

        .                 |                  TR                  |                 .
        .                     |       TE         |                                 .
        .                                                                          .
        .            RF:     /\                                                    .
        .            ADC   \/  \/      |||||||||||||||||||||                       .
        .                  ______                                                  .
        .            SS:  /      \    _______________________                      .
        .                         \__/                       \__/                  .
        .                              _____________________                       .
        .            RO:  ________    /                     \                      .
        .                         \__/                       \__/                  .
        .                                                    __                    .
        .            PE:  ________    ______________________/  \                   .
        .                         \__/                                             .


    Parameters
    ----------
    system_specs
        SystemSpecification
    matrix_size
        array of shape (2, )
    inplane_resolution
        Quantity[Length] of shape (2, )
    repetition_time
        Quantity[Time] containing the required repetition_time If None or too short, the shortest possible time under system constraints is used.
    slice_thickness
        Quantity[Length] containing the required slice-thickness
    adc_duration
        Quantity[time] Total duration of adc-sampling for a single TR
    flip_angle
        Quantity[Angle] containing the required flip_angle
    pulse_duration
        Quantity[Time] Total pulse duration (corresponds to flat_duration of the slice selection gradient)
    slice_position_offset
        Quantity[Length] positional offset in slice normal direction defining the frequency offset of the RF pulse
    time_bandwidth_product
        float used to calculate the rf bandwidth from duration
    dummy_shots
        number of shots(TRs) without adc-events before starting the acquisition
    dummy_scheme
        str scheme to use for dummy shots, options are "Linear" or "AlphaHalf"
    fuse_slice_rewind_and_prephaser
        If True, the slice selection rewinder is recalculated to match the duration of the prephaser, resulting in the fastest possible 3D k-space traverse.

    Returns
    -------
    List of length (n_dummy+matrix_size[1]) containting one Sequence object per TR
    """

    # Step 0: Create a slice-selective excitation
    rf_seq = cmrseq.seqdefs.excitation.slice_selective_sinc_pulse(
                                    system_specs=system_specs,
                                    slice_thickness=slice_thickness,
                                    flip_angle=flip_angle,
                                    pulse_duration=pulse_duration,
                                    time_bandwidth_product=time_bandwidth_product,
                                    slice_position_offset=slice_position_offset,
                                    slice_normal=np.array([0., 0., 1.]))
    ss_refocus = rf_seq["slice_select_rewind_0"]

    # Step 1: Determine ADC-duration depending on one of four cases:
    k_max_inplane, _, kro_traverse = cmrseq.seqdefs.readout.matrix_to_kspace_2d(matrix_size, inplane_resolution)
    prephaser = None

    # Case 1: Maximize sampling time for fixed TR
    if repetition_time is not None and adc_duration is None:
        if fuse_slice_rewind_and_prephaser:
            time_to_fill = repetition_time - rf_seq['slice_select_0'].duration
            add_k_traverse = Quantity([k_max_inplane[1].m_as("1/m"),
                                      (ss_refocus.area[-1] * system_specs.gamma).m_as("1/m")],
                                      "1/m")
        else:
            time_to_fill = repetition_time - rf_seq.duration -  ss_refocus.duration
            add_k_traverse = Quantity([k_max_inplane[1].m_as("1/m"), 0.], "1/m")
        try:
            prephaser, _, adc = cmrseq.seqdefs.readout.get_longest_adc_duration(
                                                system_specs, time_to_fill,
                                                matrix_size[0], inplane_resolution[0],
                                                balanced=True,
                                                additional_kspace_traverse=add_k_traverse)
            internal_adc_duration = adc.duration
        except:
            # Something went wrong, likely the time to fill is not feasible, so we resort the same as case 2
            _, adc = cmrseq.seqdefs.readout.get_shortest_adc_duration(
                                        system_specs, matrix_size[0], inplane_resolution[0])
            internal_adc_duration = adc.duration

    # Case 2: If tr is not set, set it to minimum.
    elif repetition_time is None and adc_duration is None:
        _, adc = cmrseq.seqdefs.readout.get_shortest_adc_duration(
                                        system_specs, matrix_size[0], inplane_resolution[0]
                                        )
        internal_adc_duration = adc.duration
    # Case 3: Specified adc_duration infeasible, therefore set it to minimum and
    # increase TR accordingly
    # Case 4: Both are specified, hence given value is used and TR is increased if
    # it is too short for given sampling duration
    else:
        _, adc = cmrseq.seqdefs.readout.get_shortest_adc_duration(
                                system_specs, matrix_size[0], inplane_resolution[0]
                                )
        if adc_duration < adc.duration:
            internal_adc_duration = adc.duration
            warn(f"ADC-duration set from {adc_duration} to {adc.duration}",
                cmrseq.err.AutomaticOptimizationWarning)
        else:
            internal_adc_duration = adc_duration

    # Step 2: Construct a dummy readout with prephaser with minimal duration
    ro_dummy = cmrseq.seqdefs.readout.balanced_gre_cartesian_line(system_specs, matrix_size[0],
                                                                    kro_traverse, k_max_inplane[1],
                                                                    internal_adc_duration)
    ro_dummy_prephaser = ro_dummy["ro_prephaser_0"]
    ro_dummy_trap = ro_dummy["trapezoidal_readout_0"]

    # Step 3 Compute shortest prephaser-duration
    if fuse_slice_rewind_and_prephaser:
        # prephaser duration has previously been defined to maximize adc while matching match the desired TR
        if repetition_time is not None and adc_duration is None and prephaser is not None:
            prephaser_duration = prephaser.duration
            _flat = prephaser.flat_duration
            _rise = prephaser.rise_time

        else: # We calculate the shortest possible prephaser duration
            k_max_x = (ro_dummy_prephaser.area[0] * system_specs.gamma).m_as("1/m")
            k_max_y = k_max_inplane[1].m_as("1/m")
            kz_refocus = (ss_refocus.area * system_specs.gamma).m_as("1/m")
            total_kspace_traverse = Quantity([k_max_x, k_max_y, kz_refocus[-1]], "1/m")
            _, _rise, _flat = system_specs.get_fastest_kspace_traverse(total_kspace_traverse)
            prephaser_duration = system_specs.time_to_raster(2 *_rise +  _flat, "grad")

            # In the case that we actually define an TR duration, we would rather have long prephasers than short + delay
            if repetition_time is not None:
                long_prephaser_duration = repetition_time - ro_dummy_trap.duration - rf_seq.duration + ss_refocus.duration
                # round down to nearest raster
                time = np.around((long_prephaser_duration/2).m_as("ms"), decimals=8)
                time_ndt = np.floor(np.around(time / system_specs.grad_raster_time.m_as("ms"), decimals=8))
                long_prephaser_duration = time_ndt * system_specs.grad_raster_time

                # If TR is set too short, the new duration will actually be too short
                # So we only update if the new duration is longer
                if long_prephaser_duration > prephaser_duration:
                    combined_gradient_area = Quantity(np.linalg.norm(total_kspace_traverse.m_as("1/m")), "1/m") / system_specs.gamma.to("1/mT/ms")
                    prephaser_total = cmrseq.bausteine.TrapezoidalGradient.from_dur_area(system_specs=system_specs,
                                                                                        orientation=np.array([1., 0., 0.]),
                                                                                        duration=long_prephaser_duration,
                                                                                        area=combined_gradient_area)
                    _flat = prephaser_total.flat_duration
                    _rise = prephaser_total.rise_time


        ss_rewind_amp = ss_refocus.area[-1]/(_flat + _rise)
        rf_seq.remove_block("slice_select_rewind_0")
        ss_rewind = cmrseq.bausteine.TrapezoidalGradient(system_specs,
                                                        np.array([0., 0., -1.]),
                                                        flat_duration=_flat,
                                                        rise_time=_rise,
                                                        amplitude=ss_rewind_amp,
                                                        name="slice_select_rewind")
        rf_seq.append(ss_rewind)
    else:
        prephaser_duration = ro_dummy_prephaser.duration

    # Step 4: Calculate padding for extra time in longer TRs
    if fuse_slice_rewind_and_prephaser:
        minimal_tr = (ro_dummy_trap.duration +
                      2 * prephaser_duration + rf_seq["slice_select_0"].duration)
    else:
        minimal_tr = (ro_dummy_trap.duration + 2 * prephaser_duration +
                      rf_seq.duration + rf_seq["slice_select_rewind_0"].duration)
    internal_repetition_time = repetition_time
    if repetition_time is None:
        internal_repetition_time = minimal_tr
    elif repetition_time.m_as("ms") < minimal_tr.m_as("ms") - 1e-6:
        warn(f"TR set from {repetition_time} to {minimal_tr}",
             cmrseq.err.AutomaticOptimizationWarning)
        internal_repetition_time = minimal_tr
    delay_dur = (internal_repetition_time - minimal_tr) /2

    # Step 5: Construct the readout and phase encoding gradients
    ro_blocks = cmrseq.seqdefs.readout.multi_line_cartesian(
                                    system_specs=system_specs,
                                    fnc=cmrseq.seqdefs.readout.balanced_gre_cartesian_line,
                                    matrix_size=matrix_size,
                                    inplane_resolution=inplane_resolution,
                                    adc_duration=internal_adc_duration,
                                    prephaser_duration=prephaser_duration,
                                    dummy_shots=dummy_shots)


    # Step 6: Create the slice selection compensation
    ss_compensate = deepcopy(rf_seq.get_block(partial_string_match="slice_select_rewind")[0])
    ss_compensate.name = "slice_select_balance"
    ss_compensate.shift(-ss_compensate.tmin)
    if fuse_slice_rewind_and_prephaser:
        ss_compensate.shift(-prephaser_duration)

    # Step 7: Adjust alternating phase offset for adc-events
    for ro_idx, ro_b in enumerate(ro_blocks):
        phase_offset = Quantity(np.mod(ro_idx, 2) * np.pi, "rad")
        adc_block = ro_b.get_block("adc_0")
        if adc_block is not None:
            adc_block.phase_offset = phase_offset

    # Step 8: Add delay to match TR/2 after the first exication, only for AlphaHalf
    if dummy_scheme == "AlphaHalf":
        catalyst_shot = deepcopy(rf_seq)
        catalyst_shot["rf_excitation_0"].scale_angle(0.5)
        catalyst_shot.append(cmrseq.bausteine.Delay(system_specs,
                         system_specs.time_to_raster(internal_repetition_time/2 - catalyst_shot.duration, "grad")))
        seq_list = [catalyst_shot]
    else:
        seq_list = []

    # Assemble blocks to list of sequences each representing one TR
    for tr_idx, ro_b in enumerate(ro_blocks):
        rf_phase = (tr_idx % 2)*Quantity(np.pi, 'rad')
        seq = deepcopy(rf_seq)
        seq["rf_excitation_0"].phase_offset = rf_phase

        if dummy_scheme == "Linear" and dummy_shots is not None:
            angle_scale = np.minimum((tr_idx+1)/(dummy_shots+1),1)
            seq["rf_excitation_0"].scale_angle(angle_scale)

        if fuse_slice_rewind_and_prephaser:
        # Adjust blocks to match the prephaser rise/flat
            for name in ['ro_prephaser_0','pe_prephaser_0','ro_prephaser_balance_0','pe_prephaser_balance_0']:
                dir_norm = np.sqrt(np.sum(ro_b[name].gradients[1][:,1]**2))
                if not dir_norm == 0:
                    new_dir = (ro_b[name].gradients[1][:,1] / np.sqrt(np.sum(ro_b[name].gradients[1][:,1]**2))).m_as("dimensionless")
                else:
                    new_dir = np.array([1., 0., 0.])
                new_area = np.sqrt(np.sum((ro_b[name].area)**2))
                if new_area == 0:
                    new_dir = np.array([1., 0., 0.])
                new_block = cmrseq.bausteine.TrapezoidalGradient(system_specs,
                                                                orientation=new_dir,
                                                                flat_duration=_flat,
                                                                rise_time=_rise,
                                                                amplitude=new_area/(_rise + _flat),
                                                                delay=ro_b[name].tmin,
                                                                name=name[:-2])

                ro_b.remove_block(name)
                ro_b.add_block(new_block)

            ro_b.shift_in_time(-prephaser_duration)

        seq.append(ro_b, copy=False)
        seq.append(ss_compensate, copy=True)

        # insert padding around the readout to match TR
        if delay_dur > 0:
            part1 = seq.partial_sequence(copy_blocks=False,
                                         partial_string_match=("readout", "adc"))
            part1.shift_in_time(delay_dur)

            part2 = seq.partial_sequence(copy_blocks=False,
                                        partial_string_match=("balance"))
            part2.shift_in_time(delay_dur*2)
        seq_list.append(seq)
    return seq_list

radial_balanced_ssfp

radial_balanced_ssfp(
    system_specs: SystemSpec,
    samples_per_spoke: int,
    inplane_resolution: Quantity,
    slice_thickness: Quantity,
    adc_duration: Quantity,
    flip_angle: Quantity,
    pulse_duration: Quantity,
    repetition_time: Quantity,
    spoke_angle_increment: Quantity = None,
    num_spokes: int = None,
    slice_position_offset: Quantity = Quantity(0.0, "m"),
    time_bandwidth_product: float = 4.0,
    dummy_shots: int = 0,
    dummy_scheme: str = "Linear",
    fuse_slice_rewind_and_prephaser: bool = True,
) -> List[cmrseq.Sequence]

Defines a 2D radial balanced steady state free precession sequence with a/2-TR/2 preparation Not as optimized as cartesian bSSFP.

Parameters:

Name Type Description Default
system_specs SystemSpec

SystemSpecification

required
samples_per_spoke int

number of samples per spoke, i.e. number of adc-events per TR

required
inplane_resolution Quantity

Isotropic in-plane resolution, defines max kspace radius. Quantity[Length]

required
slice_thickness Quantity

Quantity[Length] containing the required slice-thickness

required
adc_duration Quantity

Quantity[time] Total duration of adc-sampling for a single TR

required
flip_angle Quantity

Quantity[Angle] containing the required flip_angle

required
pulse_duration Quantity

Quantity[Time] Total pulse duration (corresponds to flat_duration of the slice selection gradient)

required
repetition_time Quantity

Quantity[Time] containing the desired repetition time

required
spoke_angle_increment Quantity

Quantity[Angle] angle increment between spokes, if None, sets to uniformly fill 2pi

None
num_spokes int

number of spokes to acquire, if None, if None, defaults to satisfy nyquist

None
slice_position_offset Quantity

Quantity[Length] positional offset in slice normal direction defining the frequency offset of the RF pulse

Quantity(0.0, 'm')
time_bandwidth_product float

float used to calculate the rf bandwidth from duration

4.0
dummy_shots int

number of shots(TRs) without adc-events before starting the acquisition

0
dummy_scheme str

str scheme to use for dummy shots, either "Linear" or "AlphaHalf"

'Linear'
fuse_slice_rewind_and_prephaser bool

If True, the slice selection rewinder is recalculated to match the duration of the prephaser, resulting in the fastest possible 3D k-space traverse.

True

Returns:

Type Description
List of length (n_dummy+matrix_size[1]) containting one Sequence object per TR
Source code in cmrseq/parametric_definitions/sequences/_ssfp.py
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
def radial_balanced_ssfp(system_specs: cmrseq.SystemSpec,
                         samples_per_spoke: int,
                         inplane_resolution: Quantity,
                         slice_thickness: Quantity,
                         adc_duration: Quantity,
                         flip_angle: Quantity,
                         pulse_duration: Quantity,
                         repetition_time: Quantity,
                         spoke_angle_increment: Quantity = None,
                         num_spokes: int = None,
                         slice_position_offset: Quantity = Quantity(0., "m"),
                         time_bandwidth_product: float = 4.,
                         dummy_shots: int = 0,
                         dummy_scheme: str = "Linear",
                         fuse_slice_rewind_and_prephaser: bool = True) -> List[cmrseq.Sequence]:
    r"""Defines a 2D radial balanced steady state free precession sequence with a/2-TR/2 preparation
    Not as optimized as cartesian bSSFP.

    Parameters
    ----------
    system_specs
        SystemSpecification
    samples_per_spoke
        number of samples per spoke, i.e. number of adc-events per TR
    inplane_resolution
        Isotropic in-plane resolution, defines max kspace radius. Quantity[Length]
    slice_thickness
        Quantity[Length] containing the required slice-thickness
    adc_duration
        Quantity[time] Total duration of adc-sampling for a single TR
    flip_angle
        Quantity[Angle] containing the required flip_angle
    pulse_duration
        Quantity[Time] Total pulse duration (corresponds to flat_duration of the slice selection gradient)
    repetition_time
        Quantity[Time] containing the desired repetition time
    spoke_angle_increment
        Quantity[Angle] angle increment between spokes, if None, sets to uniformly fill 2pi
    num_spokes
        number of spokes to acquire, if None, if None, defaults to satisfy nyquist
    slice_position_offset
        Quantity[Length] positional offset in slice normal direction defining the frequency offset of the RF pulse
    time_bandwidth_product
        float used to calculate the rf bandwidth from duration
    dummy_shots
        number of shots(TRs) without adc-events before starting the acquisition
    dummy_scheme
        str scheme to use for dummy shots, either "Linear" or "AlphaHalf"
    fuse_slice_rewind_and_prephaser
        If True, the slice selection rewinder is recalculated to match the duration of the prephaser, resulting in the fastest possible 3D k-space traverse.

    Returns
    -------
    List of length (n_dummy+matrix_size[1]) containting one Sequence object per TR
    """

    rf_seq = cmrseq.seqdefs.excitation.slice_selective_sinc_pulse(
        system_specs=system_specs,
        slice_thickness=slice_thickness,
        flip_angle=flip_angle,
        pulse_duration=pulse_duration,
        time_bandwidth_product=time_bandwidth_product,
        slice_position_offset=slice_position_offset,
        slice_normal=np.array([0., 0., 1.]))
    ss_refocus = rf_seq.get_block("slice_select_rewind_0")

    kr_max = 1 / (2 * inplane_resolution.m_as("m"))

    if fuse_slice_rewind_and_prephaser:
        # Recalculate ss-gradient combined with ro prephaser

        kz_refocus = (ss_refocus.area * system_specs.gamma).m_as("1/m")

        total_kspace_traverse = Quantity(np.linalg.norm([kr_max, kz_refocus[-1]]), "1/m")
        combined_gradient_area = total_kspace_traverse / system_specs.gamma.to("1/mT/ms")
        prephaser_duration = cmrseq.bausteine.TrapezoidalGradient.from_area(
            system_specs, np.array([1., 0., 0]), combined_gradient_area).duration

        rf_seq.remove_block("slice_select_rewind_0")
        ss_refocus = cmrseq.bausteine.TrapezoidalGradient.from_dur_area(system_specs,
                                                                        np.array([0., 0., -1.]),
                                                                        prephaser_duration,
                                                                        ss_refocus.area[-1],
                                                                        delay=rf_seq.duration,
                                                                        name="slice_select_rewind")
        rf_seq.add_block(ss_refocus)
    else:
        prephaser_duration = None

    ro_ref = cmrseq.seqdefs.readout.radial_spoke(system_specs=system_specs, num_samples=samples_per_spoke,
                                                          kr_max=Quantity(kr_max, '1/m'), angle=Quantity(0, 'rad'),
                                                          adc_duration=adc_duration,
                                                          prephaser_duration=prephaser_duration,
                                                          balanced=True)

    dummy_ref = cmrseq.seqdefs.readout.radial_spoke(system_specs=system_specs, num_samples=0,
                                                             kr_max=Quantity(kr_max, '1/m'), angle=Quantity(0, 'rad'),
                                                             adc_duration=adc_duration,
                                                             prephaser_duration=prephaser_duration,
                                                             balanced=True)

    if prephaser_duration is None:
        prephaser_duration = ro_ref.get_block("radial_prephaser_0").duration


    # Create the slice selection compensation
    ss_compensate = deepcopy(ss_refocus)
    ss_compensate.name = "slice_select_prewind"


    readout_gradient_duration = ro_ref.get_block("radial_readout_0").duration
    max_ssref_prephaser = max(ss_refocus.duration, prephaser_duration)

    if fuse_slice_rewind_and_prephaser:
        minimal_tr = readout_gradient_duration + 2 * max_ssref_prephaser + rf_seq.duration - ss_refocus.duration
    else:
        minimal_tr = ro_ref.duration + rf_seq.duration + ss_compensate.duration

    repetition_time = system_specs.time_to_raster(repetition_time)
    if repetition_time < minimal_tr:
        warn(f"Radial bSSFP Sequence: Repetition time too short to be feasible, set TR to {minimal_tr}")
        repetition_time = minimal_tr


    tr_delay_half = system_specs.time_to_raster((repetition_time - minimal_tr)/2)

    ss_compensate.shift(-ss_compensate.tmin+ repetition_time - ss_compensate.duration)

    if dummy_scheme == "AlphaHalf":
        # Generate catalyst with TR/2 duration
        rf_catalyst= deepcopy(rf_seq)
        rf_catalyst.append(cmrseq.bausteine.Delay(system_specs, repetition_time / 2 - rf_seq.duration))
        # Concatenate readout blocks
        seq_list = [rf_catalyst]
    else:
        seq_list = []

    # Start with dummy shots
    for idx in range(dummy_shots):
        # Alternating RF pulse phase
        rf_phase = (idx % 2)*Quantity(np.pi, 'rad')
        angle_scale = 1
        if dummy_scheme == "Linear":
            angle_scale = np.minimum((idx+1)/(dummy_shots+1),1)

        rf_seq = cmrseq.seqdefs.excitation.slice_selective_sinc_pulse(
                                                    system_specs=system_specs,
                                                    slice_thickness=slice_thickness,
                                                    flip_angle=flip_angle * angle_scale,
                                                    pulse_duration=pulse_duration,
                                                    slice_position_offset=slice_position_offset,
                                                    time_bandwidth_product=time_bandwidth_product,
                                                    slice_normal=np.array([0., 0., 1.]))
        rf_seq["rf_excitation_0"].phase_offset = rf_phase
        rf_seq.remove_block("slice_select_rewind_0")
        rf_seq.append(cmrseq.bausteine.TrapezoidalGradient.from_dur_area(system_specs,
                                                                         np.array([0., 0., -1.]),
                                                                         prephaser_duration,
                                                                         ss_refocus.area[-1],
                                                                         name="slice_select_rewind"))

        cur_ro = deepcopy(dummy_ref)
        if fuse_slice_rewind_and_prephaser:
            cur_ro.shift_in_time(
                rf_seq.duration - min(ss_refocus.duration, prephaser_duration)+tr_delay_half)
        else:
            cur_ro.shift_in_time(
                rf_seq.duration + tr_delay_half)
        seq = rf_seq + cur_ro + cmrseq.Sequence([ss_compensate, ], system_specs)
        seq_list.append(seq)

    # Calculate angle increment scheme
    if num_spokes is None:
        if spoke_angle_increment is not None:
            warn(f"Radial bSSFP Sequence: Cannot set spoke angle increment without"
                 f" setting number of spokes, defaulting to satisfy nyquist")

        num_spokes = np.ceil(samples_per_spoke*np.pi/2) # Nyquist criteria for radial sampling

        spoke_angles = np.linspace(0,np.pi,int(num_spokes), endpoint=False)
    else:
        if spoke_angle_increment is None:
            warn(f"Radial bSSFP Sequence: Spoke angle not set while spoke count set, "
                 f"defaulting to even spacing of spokes")
            spoke_angles = np.linspace(0,np.pi,int(num_spokes), endpoint=False)
        else:
            spoke_angles = np.array(range(num_spokes))*spoke_angle_increment.to('rad').m_as('dimensionless')

    # Readout shots
    for angle,idx in zip(spoke_angles,range(len(spoke_angles))):
        # Alternating RF pulse phase
        rf_phase = ((idx+dummy_shots) % 2)*Quantity(np.pi, 'rad')
        rf_seq = cmrseq.seqdefs.excitation.slice_selective_sinc_pulse(
            system_specs=system_specs,
            slice_thickness=slice_thickness,
            flip_angle=flip_angle,
            pulse_duration=pulse_duration,
            slice_position_offset=slice_position_offset,
            time_bandwidth_product=time_bandwidth_product,
            slice_normal=np.array([0., 0., 1.]))
        rf_seq["rf_excitation_0"].phase_offset = rf_phase
        if fuse_slice_rewind_and_prephaser:
            rf_seq.remove_block("slice_select_rewind_0")
            rf_seq.append(cmrseq.bausteine.TrapezoidalGradient.from_dur_area(system_specs,
                                                                             np.array([0., 0., -1.]),
                                                                             prephaser_duration,
                                                                             ss_refocus.area[-1],
                                                                             name="slice_select_rewind"))

        cur_ro = deepcopy(ro_ref)
        sa = np.sin(angle)
        ca = np.cos(angle)
        omatrix = cmrseq.OMatrix(system_specs=system_specs,
                            position=Quantity(0,'m'),
                            slice_normal=np.array([0,0,1]),
                            readout_direction = np.array([ca,sa,0]))
        cur_ro.register_omatrix(matrix=omatrix, gradients=cur_ro.blocks)

        if fuse_slice_rewind_and_prephaser:
            cur_ro.shift_in_time(
                rf_seq.duration - min(ss_refocus.duration, prephaser_duration)+tr_delay_half)
        else:
            cur_ro.shift_in_time(
                rf_seq.duration + tr_delay_half)

        # Adjust alternating phase offset for adc-events
        phase_offset = Quantity(np.mod(idx + dummy_shots + 1, 2) * np.pi, "rad")
        adc_block = cur_ro.get_block("adc_0")
        if adc_block is not None:
            adc_block.phase_offset = phase_offset

        seq = rf_seq + cur_ro + cmrseq.Sequence([ss_compensate, ], system_specs)
        seq_list.append(seq)

    return seq_list

balanced_ssfp_3d

balanced_ssfp_3d(
    system_specs: SystemSpec,
    k_phase: Quantity,
    k_z: Quantity,
    matrix_size: ndarray,
    inplane_resolution: Quantity,
    slice_thickness: Quantity,
    adc_duration: Quantity,
    flip_angle: Quantity,
    pulse_duration: Quantity,
    repetition_time: Quantity,
    slice_position_offset: Quantity,
    time_bandwidth_product: float = 4.0,
    dummy_shots: int = None,
    dummy_scheme: str = "Linear",
    fuse_slice_rewind_and_prephaser: bool = True,
) -> List[cmrseq.Sequence]

Define a 3D balanced steady-state free precession sequence.

The sequence uses alpha/2-TR/2 preparation and a Cartesian readout.

Assumptions in temporal optimization for combinations of specified arguments:

  • Neither TR nor adc_duration: Timing is optimized to have minimal TR, hence also shortest possible ADC-duration
  • TR and adc_duration is provided: Padding around the readout gradient is applied to match TR if needed. If ADC is longer than possible, TR is set to minimal feasible value, marked by a warning.
  • TR specified, adc_duration is None: ADC-duration is maximized, according to given TR
  • TR is None, adc_duration is specified: TR is set to minimally possible value for given adc-duration

In all cases the gradient limits for combined k-space traverse during the prephaser is respected, both for fusing and not fusing the slice select rewinder with the phase and readout prephaser.

.. code-block::

.                 |                  TR                  |                 .
.                     |       TE         |                                 .
.                                                                          .
.            RF:     /\                                                    .
.            ADC   \/  \/      |||||||||||||||||||||                       .
.                  ______                                                  .
.            SS:  /      \    _______________________                      .
.                         \__/                       \__/                  .
.                              _____________________                       .
.            RO:  ________    /                     \                      .
.                         \__/                       \__/                  .
.                                                    __                  .
.           PE_Y: ________    ______________________/  \                   .
.                         \__/                       __                    .                     .
.           PE_Z: ________    ______________________/  \                   .
.                         \__/

Parameters:

Name Type Description Default
system_specs SystemSpec

System specifications used for rasterization and hardware limits.

required
k_phase Quantity

Phase-encoding ky sampling points with units of inverse length.

required
k_z Quantity

Slice-encoding kz sampling points with units of inverse length.

required
matrix_size ndarray

Matrix size in readout, phase, and slice directions.

required
inplane_resolution Quantity

Resolution in readout, phase, and slice directions.

required
slice_thickness Quantity

Slice thickness.

required
adc_duration Quantity

Total ADC sampling duration for a single TR.

required
flip_angle Quantity

RF flip angle.

required
pulse_duration Quantity

Total RF pulse duration. This corresponds to the flat duration of the slice-selection gradient.

required
repetition_time Quantity

Requested repetition time. If None or too short, the shortest feasible time under system constraints is used.

required
slice_position_offset Quantity

Positional offset in the slice-normal direction used to define the RF pulse frequency offset.

required
time_bandwidth_product float

Time-bandwidth product used to calculate RF bandwidth from duration.

4.
dummy_shots int

Number of TRs without ADC events before starting acquisition.

None
dummy_scheme (Linear, AlphaHalf)

Dummy-shot preparation scheme.

"Linear"
fuse_slice_rewind_and_prephaser bool

Recalculate the slice-selection rewinder to match the prephaser duration, producing the fastest feasible 3D k-space traverse.

True

Returns:

Type Description
list[Sequence]

One sequence object per TR, including dummy shots.

Source code in cmrseq/parametric_definitions/sequences/_ssfp.py
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
def balanced_ssfp_3d(
                  system_specs: cmrseq.SystemSpec,
                  k_phase: Quantity,
                  k_z: Quantity,
                  matrix_size: np.ndarray,
                  inplane_resolution: Quantity,
                  slice_thickness: Quantity,
                  adc_duration: Quantity,
                  flip_angle: Quantity,
                  pulse_duration: Quantity,
                  repetition_time: Quantity,
                  slice_position_offset: Quantity,
                  time_bandwidth_product: float = 4.,
                  dummy_shots: int = None,
                  dummy_scheme: str = "Linear",
                  fuse_slice_rewind_and_prephaser: bool = True) -> List[cmrseq.Sequence]:

    r"""Define a 3D balanced steady-state free precession sequence.

    The sequence uses alpha/2-TR/2 preparation and a Cartesian readout.

    Assumptions in temporal optimization for combinations of specified arguments:

    - *Neither TR nor adc_duration*:
        Timing is optimized to have minimal TR, hence also shortest possible ADC-duration
    - *TR and adc_duration is provided*:
        Padding around the readout gradient is applied to match TR if needed. If ADC is longer
         than possible, TR is set to minimal feasible value, marked by a warning.
    - *TR specified, adc_duration is None*:
        ADC-duration is maximized, according to given TR
    - *TR is None, adc_duration is specified*:
        TR is set to minimally possible value for given adc-duration

    In all cases the gradient limits for combined k-space traverse during the prephaser
    is respected, both for fusing and not fusing the slice select rewinder with the
    phase and readout prephaser.

    .. code-block::

        .                 |                  TR                  |                 .
        .                     |       TE         |                                 .
        .                                                                          .
        .            RF:     /\                                                    .
        .            ADC   \/  \/      |||||||||||||||||||||                       .
        .                  ______                                                  .
        .            SS:  /      \    _______________________                      .
        .                         \__/                       \__/                  .
        .                              _____________________                       .
        .            RO:  ________    /                     \                      .
        .                         \__/                       \__/                  .
        .                                                    __                  .
        .           PE_Y: ________    ______________________/  \                   .
        .                         \__/                       __                    .                     .
        .           PE_Z: ________    ______________________/  \                   .
        .                         \__/

    Parameters
    ----------
    system_specs : cmrseq.SystemSpec
        System specifications used for rasterization and hardware limits.
    k_phase : Quantity
        Phase-encoding ``ky`` sampling points with units of inverse length.
    k_z : Quantity
        Slice-encoding ``kz`` sampling points with units of inverse length.
    matrix_size : np.ndarray
        Matrix size in readout, phase, and slice directions.
    inplane_resolution : Quantity
        Resolution in readout, phase, and slice directions.
    slice_thickness : Quantity
        Slice thickness.
    adc_duration : Quantity
        Total ADC sampling duration for a single TR.
    flip_angle : Quantity
        RF flip angle.
    pulse_duration : Quantity
        Total RF pulse duration. This corresponds to the flat duration of the
        slice-selection gradient.
    repetition_time : Quantity
        Requested repetition time. If ``None`` or too short, the shortest
        feasible time under system constraints is used.
    slice_position_offset : Quantity
        Positional offset in the slice-normal direction used to define the RF
        pulse frequency offset.
    time_bandwidth_product : float, default=4.
        Time-bandwidth product used to calculate RF bandwidth from duration.
    dummy_shots : int, optional
        Number of TRs without ADC events before starting acquisition.
    dummy_scheme : {"Linear", "AlphaHalf"}, default="Linear"
        Dummy-shot preparation scheme.
    fuse_slice_rewind_and_prephaser : bool, default=True
        Recalculate the slice-selection rewinder to match the prephaser duration,
        producing the fastest feasible 3D k-space traverse.

    Returns
    -------
    list[cmrseq.Sequence]
        One sequence object per TR, including dummy shots.
    """

    # initialize sequence list
    seq_list = []

    # Calculate k-space size
    kro_traverse = 1 / inplane_resolution[0]
    fov_ro       = matrix_size[0] * inplane_resolution[0]
    fov_pe       = matrix_size[1] * inplane_resolution[1]
    fov_z        = matrix_size[2] * inplane_resolution[2]
    delta_kro    = 1 / fov_ro
    delta_kpe    = 1 / fov_pe
    delta_kz     = 1 / fov_z

    # Define max k-space extents based on matrix size and resolution
    kro_max = - ((matrix_size[0] + 1) // 2) * delta_kro
    kpe_max = - ((matrix_size[1] + 1) // 2) * delta_kpe
    kz_max  = - ((matrix_size[2] + 1) // 2) * delta_kz

    # Check that k_z and k_phase are within the limits
    if np.max(np.abs(k_z.m_as("1/m"))) > np.abs(kz_max.m_as("1/m")):
        raise ValueError(f"k_z exceeds maximum k-space extent: {np.abs(kz_max)}")
    if np.max(np.abs(k_phase.m_as("1/m"))) > np.abs(kpe_max.m_as("1/m")):
        raise ValueError(f"k_phase exceeds maximum k-space extent: {np.abs(kpe_max)}")

    k_max_inplane = Quantity([kro_max.m_as("1/m"), kpe_max.m_as("1/m"), kz_max.m_as("1/m")], "1/m")

    # Step 0: Create a slice-selective excitation
    rf_seq = cmrseq.seqdefs.excitation.slice_selective_sinc_pulse(
                                    system_specs=system_specs,
                                    slice_thickness=slice_thickness,
                                    flip_angle=flip_angle,
                                    pulse_duration=pulse_duration,
                                    time_bandwidth_product=time_bandwidth_product,
                                    slice_position_offset= slice_position_offset,
                                    slice_normal=np.array([0., 0., 1.]))

    ss_refocus = rf_seq["slice_select_rewind_0"]

    # Step 1: Determine ADC-duration depending on one of four cases:
    prephaser = None

    # Case 1: Maximize sampling time for fixed TR
    if repetition_time is not None and adc_duration is None:
        if fuse_slice_rewind_and_prephaser:
            time_to_fill = repetition_time - rf_seq['slice_select_0'].duration
            # We need to take the worst case for refocus and prewinder.
            add_kz = (np.abs(ss_refocus.area[2]) * system_specs.gamma) + np.abs(k_max_inplane[2])

            add_k_traverse = Quantity([k_max_inplane[1].m_as("1/m"), -add_kz.m_as("1/m")], "1/m")
        else:
            time_to_fill = repetition_time - rf_seq.duration -  ss_refocus.duration
            add_k_traverse = Quantity([k_max_inplane[1].m_as("1/m"), k_max_inplane[2].m_as("1/m")], "1/m")
        try:
            prephaser, _, adc = cmrseq.seqdefs.readout.get_longest_adc_duration(
                                                system_specs, time_to_fill,
                                                matrix_size[0], inplane_resolution[0],
                                                balanced=True,
                                                additional_kspace_traverse=add_k_traverse)
            internal_adc_duration = adc.duration
        except:
            # Something went wrong, likely the time to fill is not feasible, so we resort the same as case 2
            _, adc = cmrseq.seqdefs.readout.get_shortest_adc_duration(
                                        system_specs, matrix_size[0], inplane_resolution[0])
            internal_adc_duration = adc.duration


    # Case 2: If tr is not set, set it to minimum.
    elif repetition_time is None and adc_duration is None:
        _, adc = cmrseq.seqdefs.readout.get_shortest_adc_duration(
                                        system_specs, matrix_size[0], inplane_resolution[0]
                                        )
        internal_adc_duration = adc.duration
    # Case 3: Specified adc_duration infeasible, therefore set it to minimum and
    # increase TR accordingly
    # Case 4: Both are specified, hence given value is used and TR is increased if
    # it is too short for given sampling duration
    else:
        _, adc = cmrseq.seqdefs.readout.get_shortest_adc_duration(
                                system_specs, matrix_size[0], inplane_resolution[0]
                                )
        if adc_duration < adc.duration:
            internal_adc_duration = adc.duration
            warn(f"ADC-duration set from {adc_duration} to {adc.duration}",
                cmrseq.err.AutomaticOptimizationWarning)
        else:
            internal_adc_duration = adc_duration

    # Step 2: Construct a dummy readout with prephaser with minimal duration
    ro_dummy = cmrseq.seqdefs.readout.balanced_gre_cartesian_line(system_specs=system_specs,
                                                                  num_samples = matrix_size[0],
                                                                  k_readout = kro_traverse,
                                                                  k_phase=k_max_inplane[1],
                                                                  adc_duration=internal_adc_duration,
                                                                  k_slice = k_max_inplane[2])


    ro_dummy_prephaser = ro_dummy["ro_prephaser_0"]
    ro_dummy_trap = ro_dummy["trapezoidal_readout_0"]

    # Step 3 Compute shortest prephaser-duration
    if fuse_slice_rewind_and_prephaser:
        # prephaser duration has previously been defined to maximize adc while matching match the desired TR
        if repetition_time is not None and adc_duration is None and prephaser is not None:
            prephaser_duration = prephaser.duration
            _flat = prephaser.flat_duration
            _rise = prephaser.rise_time

        else: # We calculate the shortest possible prephaser duration
            k_max_x = (ro_dummy_prephaser.area[0] * system_specs.gamma).m_as("1/m")
            k_max_y = k_max_inplane[1].m_as("1/m")
            k_max_z = k_max_inplane[2].m_as("1/m")
            kz_refocus = (ss_refocus.area[2] * system_specs.gamma).m_as("1/m")

            k_max_z = np.abs(k_max_z) + np.abs(kz_refocus)  # worst case for refocus and prewinder

            total_kspace_traverse = Quantity([k_max_x, k_max_y, -k_max_z], "1/m")
            _, _rise, _flat = system_specs.get_fastest_kspace_traverse(total_kspace_traverse)
            prephaser_duration = system_specs.time_to_raster(2 *_rise +  _flat, "grad")

            # In the case that we actually define an TR duration, we would rather have long prephasers than short + delay
            if repetition_time is not None:
                long_prephaser_duration = repetition_time - ro_dummy_trap.duration - rf_seq.duration + ss_refocus.duration
                # round down to nearest raster
                time = np.around((long_prephaser_duration/2).m_as("ms"), decimals=8)
                time_ndt = np.floor(np.around(time / system_specs.grad_raster_time.m_as("ms"), decimals=8))
                long_prephaser_duration = time_ndt * system_specs.grad_raster_time

                # If TR is set too short, the new duration will actually be too short
                # So we only update if the new duration is longer
                if long_prephaser_duration > prephaser_duration:
                    combined_gradient_area = Quantity(np.linalg.norm(total_kspace_traverse.m_as("1/m")), "1/m") / system_specs.gamma.to("1/mT/ms")
                    prephaser_total = cmrseq.bausteine.TrapezoidalGradient.from_dur_area(system_specs=system_specs,
                                                                                        orientation=np.array([1., 0., 0.]),
                                                                                        duration=long_prephaser_duration,
                                                                                        area=combined_gradient_area)
                    _flat = prephaser_total.flat_duration
                    _rise = prephaser_total.rise_time
                    prephaser_duration = system_specs.time_to_raster(2 *_rise +  _flat, "grad")

    else:
        prephaser_duration = ro_dummy_prephaser.duration

    # Step 4: Calculate padding for extra time in longer TRs
    if fuse_slice_rewind_and_prephaser:
        minimal_tr = (ro_dummy_trap.duration +
                      2 * prephaser_duration + rf_seq["slice_select_0"].duration)
    else:
        minimal_tr = (ro_dummy_trap.duration + 2 * prephaser_duration +
                      rf_seq.duration + rf_seq["slice_select_rewind_0"].duration)

    internal_repetition_time = repetition_time

    if repetition_time is None:
        internal_repetition_time = minimal_tr
    elif repetition_time.m_as("ms") < minimal_tr.m_as("ms") - 1e-6:
        warn(f"TR set from {repetition_time} to {minimal_tr}",
             cmrseq.err.AutomaticOptimizationWarning)
        internal_repetition_time = minimal_tr
    delay_dur = (internal_repetition_time - minimal_tr) /2

    # Step 5: Add delay to match TR/2 after the first exication, only for AlphaHalf
    if dummy_scheme == "AlphaHalf":
        catalyst_shot = deepcopy(rf_seq)
        catalyst_shot["rf_excitation_0"].scale_angle(-0.5) # - flip/2
        catalyst_shot.append(cmrseq.bausteine.Delay(system_specs,
                         system_specs.time_to_raster(internal_repetition_time/2 - catalyst_shot.duration, "grad")))
        seq_list = [catalyst_shot]
    else:
        seq_list = []

    # Step 6: Construct a dummy of final sequence structure
    kz_refocus = (ss_refocus.area[2] * system_specs.gamma)
    if fuse_slice_rewind_and_prephaser:
        kz_dummy = k_z[0] + kz_refocus
    else:
        kz_dummy = k_z[0]
    dummy = cmrseq.seqdefs.readout.balanced_gre_cartesian_line(system_specs=system_specs,
                                                                  num_samples = matrix_size[0],
                                                                  k_readout = kro_traverse,
                                                                  k_phase=k_phase[0],
                                                                  adc_duration=internal_adc_duration,
                                                                  k_slice = kz_dummy,
                                                                  prephaser_duration=prephaser_duration)
    dummy.remove_block("adc_0")

    if fuse_slice_rewind_and_prephaser:
        # Adjust blocks to match the prephaser rise/flat
        for name in ['ro_prephaser_0','pe_prephaser_0', 'kz_prephaser_0',
                        'ro_prephaser_balance_0','pe_prephaser_balance_0', 'kz_prephaser_balance_0']:
            dir_norm = np.sqrt(np.sum(dummy[name].gradients[1][:,1]**2))
            if not dir_norm == 0:
                new_dir = (dummy[name].gradients[1][:,1] / np.sqrt(np.sum(dummy[name].gradients[1][:,1]**2))).m_as("dimensionless")
            else:
                new_dir = np.array([1., 0., 0.])
            new_area = np.sqrt(np.sum((dummy[name].area)**2))
            if new_area == 0:
                new_dir = np.array([1., 0., 0.])
            new_block = cmrseq.bausteine.TrapezoidalGradient(system_specs,
                                                            orientation=new_dir,
                                                            flat_duration=_flat,
                                                            rise_time=_rise,
                                                            amplitude=new_area/(_rise + _flat),
                                                            delay=dummy[name].tmin,
                                                            name=name[:-2])

            dummy.remove_block(name)
            dummy.add_block(new_block)

    # Step 7: Create the slice selection compensation, only used when not fusing the slice rewind
    ss_compensate = deepcopy(rf_seq["slice_select_rewind_0"])
    ss_compensate.name = "slice_select_balance"
    ss_compensate.shift(-ss_compensate.tmin)

    # Step 8: Create dummy shots
    for dum in range(dummy_shots):
        flip_angle_phase = (-1) ** dum
        seq = deepcopy(rf_seq)
        seq["rf_excitation_0"].scale_angle(flip_angle_phase)

        if dummy_scheme == "Linear" and dummy_shots is not None:
            angle_scale = np.minimum((dum+1)/(dummy_shots+1),1)
            seq["rf_excitation_0"].scale_angle(angle_scale)

        if fuse_slice_rewind_and_prephaser:
            seq.remove_block("slice_select_rewind_0")
        seq.append(dummy, copy=True)
        if not fuse_slice_rewind_and_prephaser:
            seq.append(ss_compensate, copy=True)

        # insert padding around the readout to match TR
        if delay_dur > 0:
            part1 = seq.partial_sequence(copy_blocks=False,
                                         partial_string_match=("readout"))
            part1.shift_in_time(delay_dur)

            part2 = seq.partial_sequence(copy_blocks=False,
                                        partial_string_match=("balance"))
            part2.shift_in_time(delay_dur*2)

        seq_list.append(seq)

    # Step 9: Assemble the main sequence, similar to how the dummy shots were created
    n_trs = len(k_z)
    if n_trs != len(k_phase):
        raise ValueError("k_phase and k_z must have the same length, "
                         f"got {len(k_phase)} and {len(k_z)}")

    for tr_idx in range(n_trs):
        flip_angle_phase = (-1) ** (tr_idx+dummy_shots)
        seq = deepcopy(rf_seq)
        seq["rf_excitation_0"].scale_angle(flip_angle_phase)
        if fuse_slice_rewind_and_prephaser:
            seq.remove_block("slice_select_rewind_0")

        if fuse_slice_rewind_and_prephaser:
            kz_dummy = k_z[tr_idx] + kz_refocus
        else:
            kz_dummy = k_z[tr_idx]

        ro_seq = cmrseq.seqdefs.readout.balanced_gre_cartesian_line(system_specs=system_specs,
                                                                    num_samples = matrix_size[0],
                                                                    k_readout = kro_traverse,
                                                                    k_phase=k_phase[tr_idx],
                                                                    adc_duration=internal_adc_duration,
                                                                    k_slice = kz_dummy,
                                                                    prephaser_duration=prephaser_duration)

        phase_offset = Quantity(np.mod(tr_idx+dummy_shots, 2) * np.pi, "rad")
        adc_block = ro_seq.get_block("adc_0")
        if adc_block is not None:
            adc_block.phase_offset = phase_offset

        if fuse_slice_rewind_and_prephaser:
            # Adjust blocks to match the prephaser rise/flat
            for name in ['ro_prephaser_0','pe_prephaser_0', 'kz_prephaser_0',
                         'ro_prephaser_balance_0','pe_prephaser_balance_0', 'kz_prephaser_balance_0']:
                dir_norm = np.sqrt(np.sum(ro_seq[name].gradients[1][:,1]**2))
                if not dir_norm == 0:
                    new_dir = (ro_seq[name].gradients[1][:,1] / np.sqrt(np.sum(ro_seq[name].gradients[1][:,1]**2))).m_as("dimensionless")
                else:
                    new_dir = np.array([1., 0., 0.])
                new_area = np.sqrt(np.sum((ro_seq[name].area)**2))
                if new_area == 0:
                    new_dir = np.array([1., 0., 0.])
                new_block = cmrseq.bausteine.TrapezoidalGradient(system_specs,
                                                                orientation=new_dir,
                                                                flat_duration=_flat,
                                                                rise_time=_rise,
                                                                amplitude=new_area/(_rise + _flat),
                                                                delay=ro_seq[name].tmin,
                                                                name=name[:-2])

                ro_seq.remove_block(name)
                ro_seq.add_block(new_block)

        seq.append(ro_seq, copy=False)
        if not fuse_slice_rewind_and_prephaser:
            seq.append(ss_compensate, copy=True)

        # insert padding around the readout to match TR
        if delay_dur > 0:
            part1 = seq.partial_sequence(copy_blocks=False,
                                         partial_string_match=("readout", "adc"))
            part1.shift_in_time(delay_dur)

            part2 = seq.partial_sequence(copy_blocks=False,
                                        partial_string_match=("balance"))
            part2.shift_in_time(delay_dur*2)
        seq_list.append(seq)

    return seq_list

_b0

This module contains parametric definitions of complete B0 sequences

B0_map

B0_map(
    system_specs: SystemSpec,
    matrix_size: ndarray,
    inplane_resolution: Quantity,
    slice_thickness: Quantity,
    adc_duration: Quantity,
    flip_angle: Quantity,
    pulse_duration: Quantity,
    repetition_time: Quantity,
    echo_time: Quantity,
    echo_spacing: Quantity,
    num_echoes: int,
    positive_multiecho: bool = False,
    slice_position_offset: Quantity = Quantity(0.0, "m"),
    time_bandwidth_product: float = 4.0,
    dummy_shots: int = 0,
    fuse_slice_rewind_and_prephaser: bool = True,
    rf_spoil: bool = True,
    spoiler_strength: Quantity = None,
) -> List[cmrseq.Sequence]

Defines 2D multiecho gradient echo sequence.

Parameters:

Name Type Description Default
system_specs SystemSpec

SystemSpecifications

required
matrix_size ndarray

array of shape (2, ) containing the resulting matrix dimensions

required
inplane_resolution Quantity

Quantity[Length] of shape (2, ) containing the in-plane voxel dimensions

required
slice_thickness Quantity

Quantity[Length] containing the required slice-thickness

required
adc_duration Quantity

Quantity[time] Total duration of adc-sampling for a single TR

required
repetition_time Quantity

Quantity[Time] containing the required repetition_time

required
echo_time Quantity

Quantity[Time] containing the required echo-time. If too short for given system specifications, it is increased to minimum and a warning is raised.

required
flip_angle Quantity

Quantity[Angle] containing the required flip_angle

required
pulse_duration Quantity

Quantity[Time] Total pulse duration (corresponds to flat_duration of the slice selection gradient)

required
slice_position_offset Quantity

Quantity[Length] positional offset in slice normal direction defining the frequency offset of the RF pulse

Quantity(0.0, 'm')
time_bandwidth_product float

float used to calculate the rf bandwidth from duration

4.0
dummy_shots int

number of dummy shots (TRs) without adc-events, with k-space center phase encoding

0
fuse_slice_rewind_and_prephaser bool

If True, the slice selection rewinder is recalculated to match the duration of the prephaser, resulting in the fastest possible 3D k-space traverse.

True
rf_spoil bool

If True, the RF phase is incremented for each TR to achieve spoiling, according to Zur et al (1991)

True

Returns:

Type Description
List of sequence objects, that each represent a single TR
Source code in cmrseq/parametric_definitions/sequences/_b0.py
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
def B0_map(system_specs: cmrseq.SystemSpec,
          matrix_size: np.ndarray,
          inplane_resolution: Quantity,
          slice_thickness: Quantity,
          adc_duration: Quantity,
          flip_angle: Quantity,
          pulse_duration: Quantity,
          repetition_time: Quantity,
          echo_time: Quantity,
          echo_spacing: Quantity,
          num_echoes: int,
          positive_multiecho: bool = False,
          slice_position_offset: Quantity = Quantity(0., "m"),
          time_bandwidth_product: float = 4.,
          dummy_shots: int = 0,
          fuse_slice_rewind_and_prephaser: bool = True,
          rf_spoil: bool = True,
          spoiler_strength: Quantity = None) -> List[cmrseq.Sequence]:
    r"""Defines 2D multiecho gradient echo sequence.

    Parameters
    ----------
    system_specs
        SystemSpecifications
    matrix_size
        array of shape (2, ) containing the resulting matrix dimensions
    inplane_resolution
        Quantity[Length] of shape (2, ) containing the in-plane voxel dimensions
    slice_thickness
        Quantity[Length] containing the required slice-thickness
    adc_duration
        Quantity[time] Total duration of adc-sampling for a single TR
    repetition_time
        Quantity[Time] containing the required repetition_time
    echo_time
        Quantity[Time] containing the required echo-time. If too short for given system specifications, it is increased to minimum and a warning is raised.
    flip_angle
        Quantity[Angle] containing the required flip_angle
    pulse_duration
        Quantity[Time] Total pulse duration (corresponds to flat_duration of the slice selection gradient)
    slice_position_offset
        Quantity[Length] positional offset in slice normal direction defining the frequency offset of the RF pulse
    time_bandwidth_product
        float used to calculate the rf bandwidth from duration
    dummy_shots
        number of dummy shots (TRs) without adc-events, with k-space center phase encoding
    fuse_slice_rewind_and_prephaser
        If True, the slice selection rewinder is recalculated to match the duration of the prephaser, resulting in the fastest possible 3D k-space traverse.
    rf_spoil
        If True, the RF phase is incremented for each TR to achieve spoiling, according to Zur et al (1991)

    Returns
    -------
    List of sequence objects, that each represent a single TR
    """

    ###### Step 0: Create a slice-selective excitation
    rf_seq = cmrseq.seqdefs.excitation.slice_selective_sinc_pulse(
                                    system_specs=system_specs,
                                    slice_thickness=slice_thickness,
                                    flip_angle=flip_angle,
                                    pulse_duration=pulse_duration,
                                    time_bandwidth_product=time_bandwidth_product,
                                    slice_position_offset=slice_position_offset,
                                    slice_normal=np.array([0., 0., 1.]))
    ss_refocus = rf_seq["slice_select_rewind_0"]

    # General funtionality
    # If TE or dTE are set, we will not change them, and throw errors if they are not feasible
    # If ADC is set but not feasible due to given TE or dTE, we will treat it as undefined and maximize it later
    # If ADC is too short for system specs, we will use the shortest possible and warn
    # If ADC is not set, we try to maximize it
    # If TE or dTE are not set, we try to minimize them

    k_max_inplane, _, kro_traverse = cmrseq.seqdefs.readout.matrix_to_kspace_2d(matrix_size, inplane_resolution)

    ####### Step 1: Get some initial minimum times

    _, min_adc = cmrseq.seqdefs.readout.get_shortest_adc_duration(system_specs, matrix_size[0], inplane_resolution[0])

    min_ro = cmrseq.seqdefs.readout.gre_cartesian_line(system_specs, matrix_size[0],
                                                kro_traverse, k_max_inplane[1],
                                                min_adc.duration)

    min_ro_trap = min_ro["trapezoidal_readout_0"]
    min_ro_prephaser = min_ro["ro_prephaser_0"]
    if fuse_slice_rewind_and_prephaser:
        # Check combined prephaser duration
        k_max_x = (min_ro_prephaser.area[0] * system_specs.gamma).m_as("1/m")
        k_max_y = k_max_inplane[1].m_as("1/m")
        kz_refocus = (ss_refocus.area * system_specs.gamma).m_as("1/m")
        total_kspace_traverse = Quantity([k_max_x, k_max_y, kz_refocus[-1]], "1/m")
        _, _rise, _flat = system_specs.get_fastest_kspace_traverse(total_kspace_traverse)
        min_prephaser_duration = _flat + 2*_rise
    else:
        min_prephaser_duration = min_ro_prephaser.duration + ss_refocus.duration

    # We now have the fastest possible readout and prephaser durations
    # We can use these to determine minimal TE and dTE

    # Time of RF slice select after RF center
    ss_halfdur = rf_seq.duration - rf_seq.get_block("rf_excitation_0").rf_events[0] - ss_refocus.duration

    minimal_TE = ss_halfdur + min_prephaser_duration + min_ro_trap.duration/2
    minimal_dTE = min_ro_trap.duration
    if positive_multiecho:
        # We need to make a rephaser between each echo
        # Same area as the readout
        _, minadc_rise, minadc_flat = system_specs.get_shortest_gradient(min_ro_trap.area[0])
        minimal_dTE += minadc_flat + 2*minadc_rise

    # Check validity of given TE and dTE
    if echo_time is not None:
        if echo_time < minimal_TE:
            raise ValueError("Given echo time is not feasible within system specifications")
    if echo_spacing is not None:
        if echo_spacing < minimal_dTE:
            raise ValueError("Given echo spacing is not feasible within system specifications")


    ###### Step 2: If ADC is given, check validity
    # If it is not valid because it is shorter than the minimal ADC time, we use the minimum
    # If it is not valid due to TE or dTE, we will warn the user and set it to None to maximize later
    using_min_adc = False

    if adc_duration is not None:
        # First check if the ADC is valid at all?
        if adc_duration < min_adc.duration:
            # Warning, treat ADC as undefined
            warn("Given ADC duration is not feasible within system specifications, using shortest possible ADC duration")
            adc_duration = min_adc.duration
            using_min_adc = True
            # We also already checked validity of this wrt echo time and spacing for the minimum ADC

        else:
            # Check validity of user defined ADC with echo time and spacing
            ro_dummy = cmrseq.seqdefs.readout.gre_cartesian_line(system_specs, matrix_size[0],
                                                        kro_traverse, k_max_inplane[1],
                                                        adc_duration)

            ro_dummy_trap = ro_dummy["trapezoidal_readout_0"]
            ro_dummy_prephaser = ro_dummy["ro_prephaser_0"]

            # Calculate fastest prephaser duration for this readout
            if fuse_slice_rewind_and_prephaser:
                # Check combined prephaser duration
                k_max_x = (ro_dummy_prephaser.area[0] * system_specs.gamma).m_as("1/m")
                k_max_y = k_max_inplane[1].m_as("1/m")
                kz_refocus = (ss_refocus.area * system_specs.gamma).m_as("1/m")
                total_kspace_traverse = Quantity([k_max_x, k_max_y, kz_refocus[-1]], "1/m")
                _, _rise, _flat = system_specs.get_fastest_kspace_traverse(total_kspace_traverse)
                prephaser_duration = _flat + 2*_rise
            else:
                prephaser_duration = ro_dummy_prephaser.duration + ss_refocus.duration

            # Check initial echo time
            if echo_time is not None:

                # This is the minimal TE we can achieve with the given ADC
                cur_minimal_TE = ss_halfdur + prephaser_duration + ro_dummy_trap.duration/2

                if echo_time < cur_minimal_TE:
                    # The user has likely set the ADC longer than possible, so we will treat it as undefined
                    # This means it will be maximized later
                    warn("Given ADC duration is too long for desired echo time, using longest possible ADC")
                    adc_duration = None
            # If echo time is not defined, we can just use the ADC as is and minimize TE later

            # Check echo spacing
            # If ADC duration is already not feasible due to echo time (set to none), we do not need to check this
            if echo_spacing is not None and adc_duration is not None:
                # Simplest case, minimial echo spacing is just the readout gradient duration
                cur_minimal_dTE = ro_dummy_trap.duration
                if positive_multiecho:
                    # We need to make a rephaser between each echo
                    # Same area as the readout
                    _, _rise, _flat = system_specs.get_shortest_gradient(ro_dummy_trap.area[0])
                    cur_minimal_dTE += _flat + 2*_rise

                if echo_spacing < cur_minimal_dTE:
                    # Treat ADC as undefined
                    warn("Given ADC duration is too long for desired echo spacing, using longest possible ADC")
                    adc_duration = None

    # At this point we have checked ADC
    # If ADC is none, we can maximize it based on echo time and spacing
    # If ADC is not none, it must be valid, so we can just use it

    ###### Step 3: Calculate ADC duration if None

    # Possible cases:
    # 1: TE defined, dTE defined, ADC undefined -> Maximize ADC based on both constraints
    # 2: TE defined, dTE undefined, ADC undefined -> Maximize ADC based on TE, dTE is whatever we get
    # 3: TE undefined, dTE defined, ADC undefined -> Maximize ADC based on dTE, TE is whatever we get
    # 4: TE undefined, dTE undefined, ADC undefined -> Minimize TE and dTE with shortest ADC

    # if ADC is undefined, calculate it now
    if adc_duration is None:

        if echo_time is None and echo_spacing is None:
            # Case 4: Minimize TE and dTE with shortest ADC
            using_min_adc = True
            adc_duration = min_adc.duration

        else: # Cases 1-3 maximize ADC time based on given TE and dTE

            TEbound_adc_duration = Quantity(np.inf, 'ms')
            dTEbound_adc_duration = Quantity(np.inf, 'ms')

            if echo_time is not None:
                # This part is the same as for FLASH

                # Time between TE and end of RF slice select gradient
                time_to_fill = echo_time - ss_halfdur

                if fuse_slice_rewind_and_prephaser:
                    add_k_traverse = Quantity([k_max_inplane[1].m_as("1/m"),
                                            (ss_refocus.area[-1] * system_specs.gamma).m_as("1/m")],
                                            "1/m")
                else:
                    # In this case, we leave the ss refocus gradient as is
                    time_to_fill = time_to_fill - ss_refocus.duration
                    add_k_traverse = Quantity([k_max_inplane[1].m_as("1/m"), 0.], "1/m")

                try:
                    # We can use a trick here to ensure we get the right TE
                    # We use double the available time to fill, and use the balanced variant
                    # Since for balanced TE is at the center (symmetric) this solved for the optimal gradients up to TE
                    prephaser, _, adc = cmrseq.seqdefs.readout.get_longest_adc_duration(
                                                        system_specs, 2*time_to_fill,
                                                        matrix_size[0], inplane_resolution[0],
                                                        balanced=True,
                                                        additional_kspace_traverse=add_k_traverse)
                    TEbound_adc_duration = adc.duration
                except:
                    # Something went wrong, likely the time to fill is not feasible
                    TEbound_adc_duration = min_adc.duration
                if TEbound_adc_duration < min_adc.duration:
                    raise ValueError("Given echo time is not feasible within system specifications")

            if echo_spacing is not None:
                if not positive_multiecho:
                    # Simple case, due to symmetry the readout gradients must match the echo spacing
                    readout, adc = cmrseq.seqdefs.readout.get_longest_adc_duration_noprephaser(system_specs, echo_spacing,
                                                                                               matrix_size[0], inplane_resolution[0])
                    dTEbound_adc_duration = adc.duration
                else:
                    # We can play an interesting trick here
                    # The rephaser between echoes has the same area as the readout, so we can just optimize for
                    # a readout with a full area prephaser
                    reph, readout, adc = cmrseq.seqdefs.readout.get_longest_adc_duration(system_specs, echo_spacing,
                                                                                matrix_size[0], inplane_resolution[0],
                                                                                readout_prephaser_scaling = -1.0)
                    dTEbound_adc_duration = adc.duration
                if dTEbound_adc_duration < min_adc.duration:
                    raise ValueError("Given echo spacing is not feasible within system specifications")

            # Now we have the maximum ADC duration, subject to both constraints
            adc_duration = min(TEbound_adc_duration, dTEbound_adc_duration)

    # We now have a calculated ADC duration that should work for dTE and TE (if given)

    ###### Step 4: Calculate final timings

    # Now we have a fixed readout gradient structure!
    # We also already calculated it for the minimum ADC duration earlier
    if using_min_adc:
        ro = min_ro
    else:
        ro = cmrseq.seqdefs.readout.gre_cartesian_line(system_specs, matrix_size[0],
                                                        kro_traverse, k_max_inplane[1],
                                                        adc_duration)
    # Final, fixed readout gradient structure
    ro_trap = ro["trapezoidal_readout_0"]
    ro_prephaser = ro["ro_prephaser_0"]

    # Calculate time to fill for dTE (with rephaser if needed)
    if echo_spacing is not None:
        dTE_gap_duration = echo_spacing - ro_trap.duration
        # If this is small, we just use the fastest possible gap
        if np.isclose(dTE_gap_duration.m_as('ms'), 0., atol=1e-3):
            dTE_gap_duration = None
    else: # Otherwise, fast as possible
        dTE_gap_duration = None


    # Calculate time for prephaser time to achieve TE
    if echo_time is not None:
        prephaser_duration = echo_time - (ss_halfdur + ro_trap.duration/2)
        if not fuse_slice_rewind_and_prephaser:
            prephaser_duration -= ss_refocus.duration

        prephaser_duration = system_specs.time_to_raster(prephaser_duration, "grad")
    else:
        prephaser_duration = None
        # If echo time is not defined, we just use the fastest prephasers possible

    # At this point we have defined:
    # readout gradient structure (ro)
    # dTE gap duration
    # prephaser duration
    # This fully defines the sequence timing

    ###### Step 5: Calculate rephaser

    if positive_multiecho:
        # the dTE gap is filled with a rephaser
        if dTE_gap_duration is not None:
            rephaser = cmrseq.bausteine.TrapezoidalGradient.from_dur_area(system_specs=system_specs,
                                                                            orientation=np.array([-1., 0., 0.]),
                                                                            duration=dTE_gap_duration,
                                                                            area=ro_trap.area[0])
        else:
            rephaser = cmrseq.bausteine.TrapezoidalGradient.from_area(system_specs=system_specs,
                                                                    orientation=np.array([-1., 0., 0.]),
                                                                    area=ro_trap.area[0])
    else:
        if dTE_gap_duration is not None:
            rephaser = cmrseq.bausteine.Delay(system_specs, dTE_gap_duration)
        else:
            rephaser = None

    ###### Step 6: Create readout blocks

    ro_blocks = cmrseq.seqdefs.readout.multi_line_cartesian(system_specs=system_specs,
                                                            fnc=cmrseq.seqdefs.readout.gre_cartesian_line,
                                                            matrix_size=matrix_size,
                                                            inplane_resolution=inplane_resolution,
                                                            adc_duration=adc_duration,
                                                            prephaser_duration=prephaser_duration,
                                                            dummy_shots=dummy_shots)

    # Step 7: Pre-calculate spoiler timing

    if spoiler_strength is not None:
        k_max_x = (ro_prephaser.area[0] * system_specs.gamma).m_as("1/m")
        k_max_y = k_max_inplane[1].m_as("1/m")
        kz_refocus = (ss_refocus.area[-1] * system_specs.gamma).m_as("1/m")

        # Spoiler area - rewind area
        spoiler_M = np.abs((spoiler_strength[0]/Quantity(2*np.pi,'rad')/inplane_resolution[0]).m_as('1/m')) - np.abs(k_max_x)
        # P spoiler is +, to account for worst case, spoiler area + rewind area
        spoiler_P = np.abs((spoiler_strength[1]/Quantity(2*np.pi,'rad')/inplane_resolution[1]).m_as('1/m')) + np.abs(k_max_y)
        # Spoiler area - SS prewind area
        spoiler_S = np.abs((spoiler_strength[2]/Quantity(2*np.pi,'rad')/slice_thickness).m_as('1/m')) - np.abs(kz_refocus)

        total_kspace_traverse = Quantity([spoiler_M, spoiler_P, spoiler_S], "1/m")

        _, spoiler_rise, spoiler_flat = system_specs.get_fastest_kspace_traverse(total_kspace_traverse)

    # # Step 8: Check TR and add delay if needed. If too short, override
    # tr_delay = Quantity(0,'ms')
    # if repetition_time is not None:
    #     if fuse_slice_rewind_and_prephaser:
    #         minimal_tr = ro_blocks[0].get_block("trapezoidal_readout_0").duration + rf_seq.duration - ss_refocus.duration + prephaser_duration
    #     else:
    #         minimal_tr = ro_blocks[0].get_block("trapezoidal_readout_0").duration + rf_seq.duration + prephaser_duration

    #     if spoiler_strength is not None:
    #         minimal_tr += Quantity(spoiler_flat + 2*spoiler_rise, "ms")

    #     if minimal_tr > system_specs.time_to_raster(repetition_time,"grad"):
    #         warn(f"Repetition time too short to be feasible, set TR to {minimal_tr}")
    #         repetition_time = minimal_tr

    #     tr_delay = system_specs.time_to_raster(repetition_time,"grad") - minimal_tr
    #     if np.isclose(tr_delay.m_as('ms'), 0., atol=1e-10):
    #         tr_delay = Quantity(0., 'ms')

    # Step 9: Calculate prephaser timings
    if fuse_slice_rewind_and_prephaser:
        # Worst case prephaser
        k_max_x = (ro_prephaser.area[0] * system_specs.gamma).m_as("1/m")
        k_max_y = k_max_inplane[1].m_as("1/m")
        kz_refocus = (ss_refocus.area * system_specs.gamma).m_as("1/m")
        total_kspace_traverse = Quantity([k_max_x, k_max_y, kz_refocus[-1]], "1/m")

        if prephaser_duration is None:
            _, prep_rise, prep_flat = system_specs.get_fastest_kspace_traverse(total_kspace_traverse)
        else:
            # Defined duration
            combined_gradient_area = Quantity(np.linalg.norm(total_kspace_traverse.m_as("1/m")), "1/m") / system_specs.gamma.to("1/mT/ms")
            prephaser_total = cmrseq.bausteine.TrapezoidalGradient.from_dur_area(system_specs=system_specs,
                                                                                orientation=np.array([1., 0., 0.]),
                                                                                duration=prephaser_duration,
                                                                                area=combined_gradient_area)
            prep_flat = prephaser_total.flat_duration
            prep_rise = prephaser_total.rise_time

        ss_rewind_amp = ss_refocus.area[-1]/(prep_flat + prep_rise)
        ss_rewind = cmrseq.bausteine.TrapezoidalGradient(system_specs,
                                                np.array([0., 0., -1.]),
                                                flat_duration=prep_flat,
                                                rise_time=prep_rise,
                                                amplitude=ss_rewind_amp,
                                                name="slice_select_rewind")

        ro_prephaser_amp = ro_blocks[0]['ro_prephaser_0'].area[0]/(prep_flat + prep_rise)
        ro_prephaser = cmrseq.bausteine.TrapezoidalGradient(system_specs,
                                                np.array([-1., 0., 0.]),
                                                flat_duration=prep_flat,
                                                rise_time=prep_rise,
                                                amplitude=ro_prephaser_amp,
                                                name="ro_prephaser")

    else:
        # No fusing, but might need to add a delay
        prephaser_delay = prephaser_duration - ro_prephaser.duration
        if np.isclose(prephaser_delay.m_as('ms'), 0., atol=1e-10):
            prephaser_delay = None

    # Step 10: Build sequence
    seq_list = []

    phase_offset = Quantity(0, 'degree')
    pi_phase = Quantity(360, 'degree')
    # Zur et al (1991)
    rf_incr = Quantity(117, 'degree')

    for j, ro_b in enumerate(ro_blocks):

        seq = deepcopy(rf_seq)
        # RF spoiling
        if rf_spoil:
            seq['rf_excitation_0'].phase_offset = phase_offset
            if ro_b.get_block('adc_0') is not None:
                ro_b.get_block('adc_0').phase_offset = phase_offset

            phase_offset = (phase_offset + rf_incr) % pi_phase

        # Build spoiler if needed
        if spoiler_strength is not None:

            spoil_M = -ro_b['ro_prephaser_0'].area[0]
            spoil_P = - ro_b['pe_prephaser_0'].area[1] * np.sign(ro_b['pe_prephaser_0'].gradients[1][:,1][1])
            spoil_S = -ss_refocus.area[2]

            spoil_M += spoiler_strength[0]/system_specs.gamma_rad/inplane_resolution[0]
            spoil_P += spoiler_strength[1]/system_specs.gamma_rad/inplane_resolution[1]
            spoil_S += spoiler_strength[2]/system_specs.gamma_rad/slice_thickness

            if not positive_multiecho and np.mod(num_echoes,2) == 0:
                # For even echoes, the spoiler is inverted to account for the inverted readout
                spoil_M = - spoil_M

            total_spoil = np.sqrt(spoil_M**2 + spoil_P**2 + spoil_S**2)

            spoil_dir = (np.array([spoil_M.m_as('mT/m*ms'), spoil_P.m_as('mT/m*ms'), spoil_S.m_as('mT/m*ms')]) / total_spoil.m_as('mT/m*ms'))

            amp_spoil = total_spoil / (spoiler_flat + spoiler_rise)

            spoiler = cmrseq.bausteine.TrapezoidalGradient(system_specs,
                                        orientation = spoil_dir,
                                        flat_duration=spoiler_flat,
                                        rise_time=spoiler_rise,
                                        amplitude=amp_spoil,
                                        name="pe_prephaser")


        if fuse_slice_rewind_and_prephaser:
            # Produce new pe prephaser and replace all prephasers
            pe_prephaser_amp = ro_b['pe_prephaser_0'].area[1]/(prep_flat + prep_rise)
            dir_norm = np.sqrt(np.sum(ro_b['pe_prephaser_0'].gradients[1][:,1]**2))
            if not dir_norm == 0:
                pedir = (ro_b['pe_prephaser_0'].gradients[1][:,1] / dir_norm).m_as("dimensionless")
            else:
                pedir = np.array([0., 1., 0.])
            pe_prephaser = cmrseq.bausteine.TrapezoidalGradient(system_specs,
                                                    pedir,
                                                    flat_duration=prep_flat,
                                                    rise_time=prep_rise,
                                                    amplitude=pe_prephaser_amp,
                                                    name="pe_prephaser")

            ro_b.remove_block("pe_prephaser_0")
            ro_b.remove_block("ro_prephaser_0")
            seq.remove_block("slice_select_rewind_0")

            ro_b.add_block(ro_prephaser)
            ro_b.add_block(pe_prephaser)
            ro_b.add_block(ss_rewind)
        else:
            # Add the prephaser delay to the sequence
            if prephaser_delay is not None:
                seq.append(cmrseq.bausteine.Delay(system_specs, prephaser_delay))

        seq.append(ro_b)

        # Multi-echo handling

        if ro_b.get_block('adc_0') is not None:
            ro_only = cmrseq.Sequence([ro_b.get_block('adc_0'),
                                       ro_b.get_block('trapezoidal_readout_0')], system_specs=system_specs, copy=True)
        else:
            ro_only = cmrseq.Sequence([ro_b.get_block('trapezoidal_readout_0')], system_specs=system_specs, copy=True)
        ro_only.shift_in_time(-ro_only.start_time)

        for echo_idx in range(2, num_echoes+1):
            if rephaser is not None:
                seq.append(rephaser)


            cur_ro = deepcopy(ro_only)
            if not positive_multiecho and np.mod(echo_idx,2) == 0:
                # Invert readout gradient and adc for even echoes
                cur_ro.get_block('trapezoidal_readout_0').scale_gradients(-1)

            seq.append(cur_ro)

        if spoiler_strength is not None:
            seq.append(spoiler)
        # Add TR delay if needed
        # if tr_delay > 0:
        #     seq.append(cmrseq.bausteine.Delay(system_specs, tr_delay))
        seq_list.append(seq)
    return seq_list