Skip to content

Building Blocks

Gradient

Gradient(
    t: Quantity,
    grads: Quantity,
    system_specs: SystemSpec,
    name: str,
    snap_to_raster: bool,
)

Bases: SequenceBaseBlock

Base class for three-channel gradient waveform blocks.

Gradient waveforms are defined by time points and three-channel amplitudes. Linear interpolation is assumed between definition points. Longer examples for addition, splitting, and validation live in the gradients guide.

Source code in cmrseq/core/bausteine/_gradients.py
26
27
28
29
def __init__(self, t: Quantity, grads: Quantity, system_specs: 'cmrseq.SystemSpec',
             name: str, snap_to_raster: bool):
    self.gradients = (t, grads)
    super().__init__(system_specs=system_specs, name=name, snap_to_raster=snap_to_raster)

gradients instance-attribute

gradients: Tuple[Quantity, Quantity] = (t, grads)

tmin property

tmin: Quantity

Returns the time of the start of the gradient.

tmax property

tmax: Quantity

Returns the time of the end of the gradient.

split

split(t: Quantity) -> (Quantity, Quantity)

Splits the gradient waveform at given time and returns to new definining tuples that both include the split point. This output is meant to yield the original waveform when calling the add functions on the result

Parameters:

Name Type Description Default
t Quantity
required

Returns:

Type Description
object
Source code in cmrseq/core/bausteine/_gradients.py
 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
def split(self, t: Quantity) -> (Quantity, Quantity):
    r"""Splits the gradient waveform at given time and returns to new definining tuples
    that both include the split point.
    This output is meant to yield the original waveform when calling the __add__ functions on
    the result

    Parameters
    ----------
    t

    Returns
    -------
    object
    """
    split_is_on_def = np.isclose(t.to("ms"), self.gradients[0].to("ms"))
    tself, wfself = self.gradients

    if np.any(split_is_on_def):
        split_index, = np.where(split_is_on_def)
        split_index = split_index[0]
        return (tself[:split_index + 1], wfself[:, :split_index + 1]), \
            (tself[split_index:], wfself[:, split_index:])
    tself, wfself, t = tself.to("ms"), wfself.to("mT/m"), t.to("ms")
    insertion_index = np.searchsorted(tself, t)
    insertion_val = np.stack([np.interp(t, tself, wfself[i]) for i in range(3)])
    wfself = Quantity(np.insert(wfself, insertion_index, insertion_val, axis=1), "mT/m")
    tself = Quantity(np.insert(tself, insertion_index, t), "ms")
    split_index = insertion_index
    return (tself[:split_index + 1], wfself[:, :split_index + 1]), \
        (tself[split_index:], wfself[:, split_index:])

scale_gradients

scale_gradients(factor: float) -> None

Scales gradients by given factor if gradients are defined.

Parameters:

Name Type Description Default
factor float

factor to globally scale the amplitude of gradient definition.

required
Source code in cmrseq/core/bausteine/_gradients.py
120
121
122
123
124
125
126
127
128
129
130
def scale_gradients(self, factor: float) -> None:
    r"""Scales gradients by given factor if gradients are defined.

    Parameters
    ----------
    factor
        factor to globally scale the amplitude of gradient definition.
    """
    t, grads = self.gradients
    scaled_grads: Quantity = grads * factor
    self.gradients = (t, scaled_grads)

rotate_gradients

rotate_gradients(rotation_matrix: ndarray) -> None

Rotates gradients to according to the gradient axes transformation:

[[1, 0, 0], [0, 1, 0], [0, 0, 1]].T -> rotation matrix

Parameters:

Name Type Description Default
rotation_matrix ndarray

(3, 3) rotation matrix containing the new column basis vectors (meaning in [:, i], i indexes the new orientation of MPS). Vectors are normalized along axis=0 to ensure same magnitude

required

Raises:

Type Description
ValueError

If rotation_matrix is not orthogonal.

Source code in cmrseq/core/bausteine/_gradients.py
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
def rotate_gradients(self, rotation_matrix: np.ndarray) -> None:
    r"""Rotates gradients to according to the gradient axes transformation:

    ``[[1, 0, 0], [0, 1, 0], [0, 0, 1]].T`` -> rotation matrix

    Parameters
    ----------
    rotation_matrix
        (3, 3) rotation matrix containing the new column basis vectors (meaning in [:, i], i indexes the new orientation of MPS). Vectors are normalized along axis=0 to ensure same magnitude

    Raises
    ------
    ValueError
        If `rotation_matrix` is not orthogonal.
    """
    valid_rotation = np.all(np.isclose((np.matmul(rotation_matrix, rotation_matrix.T)),
                                       np.identity(3), rtol=1e-10))
    if not valid_rotation:
        raise ValueError(f"Rotation matrix is not valid\n "
                         f"{np.matmul(rotation_matrix, rotation_matrix.T)} \n"
                         f"should be identity")

    t, wf = self.gradients
    vector_norms = np.linalg.norm(rotation_matrix, axis=0, keepdims=True)
    rotation_matrix = rotation_matrix / vector_norms
    wf_rot = np.einsum("it, ij -> jt", wf.m_as("mT/m"), rotation_matrix)
    self.gradients = (t, Quantity(wf_rot, "mT/m"))

validate

validate(system_specs: SystemSpec) -> None

Validates if the contained gradient_definition is valid for the given system- specifications.

Source code in cmrseq/core/bausteine/_gradients.py
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
def validate(self, system_specs: SystemSpec) -> None:
    r"""Validates if the contained gradient_definition is valid for the given system-
    specifications.
    """
    t = self.gradients[0].m_as("ms")
    g = self.gradients[1].m_as("mT/m")
    max_grad_in_specs = np.all(np.abs(g) <= system_specs.max_grad.m_as("mT/m") + 1e-6)
    grad_slew = (np.diff(g, axis=1) / np.diff(t, axis=0)[np.newaxis])
    grad_slew_in_specs = np.all(np.abs(np.around(grad_slew, decimals=6))
                                <= system_specs.max_slew.m_as("mT/m/ms"))
    tgridded = t / system_specs.grad_raster_time.m_as("ms")
    grad_on_grid = np.allclose(tgridded, np.around(tgridded), rtol=1e-6)
    if not all([max_grad_in_specs, grad_slew_in_specs, grad_on_grid]):
        raise ValueError(f"Gradient definition of {self.name} invalid:\n"
                         f"\t- max grad: {max_grad_in_specs} "
                         f"[{np.max(np.abs(self.gradients[1].m_as('mT/m')))}"
                         f" {'<' if max_grad_in_specs else '<'} "
                         f"{system_specs.max_grad.m_as('mT/m')}]\n"
                         f"\t- max slew: {grad_slew_in_specs} [{np.max(grad_slew)} "
                         f"{'<' if grad_slew_in_specs else '<'} "
                         f"{system_specs.max_slew.m_as('mT/m/ms')}] \n"
                         f"\t- definition on grid: {grad_on_grid}")

snap_to_raster

snap_to_raster(system_specs: SystemSpec)

Rounds the time-points and waveform to the nearest raster point. Warning: When calling snap_to_raster the waveform points are simply rounded to their nearest neighbour if the difference is below the relative tolerance. Therefore this is not guaranteed to be precise anymore

Source code in cmrseq/core/bausteine/_gradients.py
195
196
197
198
199
200
201
202
203
204
205
206
207
208
def snap_to_raster(self, system_specs: SystemSpec):
    r"""Rounds the time-points and waveform to the nearest raster point.
    Warning: When calling snap_to_raster the waveform points are simply rounded to their nearest neighbour if the difference is below the relative tolerance.
    Therefore this is not guaranteed to be precise anymore

    """

    warn("Gradient.snap_to_raster Warning: When calling snap_to_raster the waveform "
         "points are simply rounded to their nearest neighbour if the difference is below the"
         " relative tolerance. Therefore this is not guaranteed to be precise anymore")
    time_ndt = np.around(self.gradients[0].m_as("ms") /
                         system_specs.grad_raster_time.m_as("ms"), decimals=0)
    time_ndt = time_ndt * system_specs.grad_raster_time.to("ms")
    self.gradients = (time_ndt.to("ms"), self.gradients[1].to("mT/m"))

shift

shift(time_shift: Quantity)

Adds the time-shift to all gradient definition points

Source code in cmrseq/core/bausteine/_gradients.py
210
211
212
def shift(self, time_shift: Quantity):
    r"""Adds the time-shift to all gradient definition points"""
    self.gradients = (self.gradients[0] + time_shift.to("ms"), self.gradients[1])

flip

flip(time_flip: Quantity = None)

Time reverses block by flipping about a given time point. If no time is specified, the center of this gradient block is choosen.

Source code in cmrseq/core/bausteine/_gradients.py
214
215
216
217
218
219
220
def flip(self, time_flip: Quantity = None):
    r"""Time reverses block by flipping about a given time point. If no
    time is specified, the center of this gradient block is choosen."""
    if time_flip is None:
        time_flip = self.tmin + (self.tmax - self.tmin) / 2
    self.gradients = (np.flip(time_flip.to("ms") - self.gradients[0], axis=0),
                      np.flip(self.gradients[1], axis=1))

TrapezoidalGradient

TrapezoidalGradient(
    system_specs: SystemSpec,
    orientation: ndarray,
    amplitude: Quantity,
    flat_duration: Quantity,
    rise_time: Quantity,
    fall_time: Quantity = None,
    delay: Quantity = Quantity(0.0, "ms"),
    name: str = "trapezoidal",
    snap_to_raster: bool = False,
)

Bases: Gradient

Module implementing a trapezoidal gradient pulse, from specified parameters

Define a trapezoidal gradient pulse.

Parameters:

Name Type Description Default
system_specs SystemSpec

System limits used for validation.

required
orientation ndarray

Gradient orientation vector with shape (3,); normalized internally.

required
amplitude Quantity

Desired gradient amplitude.

required
flat_duration Quantity

Duration of the gradient plateau.

required
rise_time Quantity

Duration of the rising slope.

required
fall_time Quantity

Duration of the falling slope. If omitted, a symmetric rise/fall time is used.

None
delay Quantity

Leading time without gradients.

Quantity(0.0, 'ms')
name str

Block name.

'trapezoidal'
snap_to_raster bool

If True, snap timing to the gradient raster before validation.

False
Source code in cmrseq/core/bausteine/_gradients.py
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 __init__(self,
             system_specs: SystemSpec,
             orientation: np.ndarray,
             amplitude: Quantity,
             flat_duration: Quantity,
             rise_time: Quantity,
             fall_time: Quantity = None,
             delay: Quantity = Quantity(0., "ms"),
             name: str = "trapezoidal",
             snap_to_raster: bool = False):
    r"""Define a trapezoidal gradient pulse.

    Parameters
    ----------
    system_specs
        System limits used for validation.
    orientation
        Gradient orientation vector with shape `(3,)`; normalized internally.
    amplitude
        Desired gradient amplitude.
    flat_duration
        Duration of the gradient plateau.
    rise_time
        Duration of the rising slope.
    fall_time
        Duration of the falling slope. If omitted, a symmetric rise/fall time is used.
    delay
        Leading time without gradients.
    name
        Block name.
    snap_to_raster
        If `True`, snap timing to the gradient raster before validation.
    """
    norm = np.linalg.norm(orientation)
    if norm > 0:
        orientation /= norm

    if fall_time is None:
        fall_time = rise_time
    rise_time = rise_time.m_as("ms")
    fall_time = fall_time.m_as("ms")
    flat_duration = flat_duration.m_as("ms")

    time_points = np.around(np.stack([
        0, rise_time,
        rise_time + flat_duration,
        fall_time + rise_time + flat_duration]
    ), decimals=6)

    time_points = Quantity(time_points, "ms") + delay
    grads_amp = np.stack([0., amplitude, amplitude, 0.])
    grads = grads_amp[np.newaxis] * orientation[:, np.newaxis]
    super().__init__(time_points, grads, system_specs=system_specs,
                     name=name, snap_to_raster=snap_to_raster)

rise_time property

rise_time: Quantity

Duration of the first trapezoidal gradient slope

fall_time property

fall_time: Quantity

Duration of the second trapezoidal gradient slope

flat_duration property

flat_duration: Quantity

Duration of the trapezoidal gradient plateau

amplitude property

amplitude: Quantity

Amplitude of the trapezoidal gradient plateau in mT/m

magnitude property

magnitude: Quantity

Magnitude (norm over spatial dimensions) of the trapezoidal gradient plateau in mT/m

signed_amplitude property

signed_amplitude: Quantity

Signed amplitude the amplitude per gradient channel

area property

area: Quantity

Area of the trapezoidal gradient: ((rise_time + fall_time + flat_duration) * amplitude)

from_area classmethod

from_area(
    system_specs: SystemSpec,
    orientation: ndarray,
    area: Quantity,
    delay: Quantity = Quantity(0.0, "ms"),
    name: str = "trapezoidal",
) -> TrapezoidalGradient

Constructs the shortest Trapezoidal or triangular gradient pulse with specified area given the system limits:

Parameters:

Name Type Description Default
system_specs SystemSpec

System-Limit context (SystemSpec instance)

required
area Quantity

Quantity[Tesla/Length*Time] Desired first moment of the Gradient Pulse

required
orientation ndarray

np.array of shape (3, ). Vector defining the gradient orientation in (gx, gy, gz) channels. Is normalized internally

required
delay Quantity

Quantity[Time] Leading time without gradients, defaults to 0. ms

Quantity(0.0, 'ms')
name str
'trapezoidal'

Returns:

Type Description
TrapezoidalGradient object

Raises:

Type Description
AssertionError

If area < 0

Source code in cmrseq/core/bausteine/_gradients.py
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
@classmethod
def from_area(cls, system_specs: SystemSpec, orientation: np.ndarray, area: Quantity,
              delay: Quantity = Quantity(0., "ms"),
              name: str = "trapezoidal") -> 'TrapezoidalGradient':
    r"""Constructs the shortest Trapezoidal or triangular gradient pulse with specified area
     given the system limits:

    Parameters
    ----------
    system_specs
        System-Limit context (SystemSpec instance)
    area
        Quantity[Tesla/Length*Time] Desired first moment of the Gradient Pulse
    orientation
        np.array of shape (3, ). Vector defining the gradient orientation in (gx, gy, gz) channels. Is normalized internally
    delay
        Quantity[Time] Leading time without gradients, defaults to 0. ms
    name

    Returns
    -------
    TrapezoidalGradient object

    Raises
    ------
    AssertionError
        If area < 0
     """
    assert area.m >= 0
    amplitude, rise_time, flat_time = system_specs.get_shortest_gradient(area.to("mT/m*ms"))
    return TrapezoidalGradient(system_specs=system_specs, orientation=orientation,
                               amplitude=amplitude.to("mT/m"),
                               flat_duration=flat_time.to("ms"), rise_time=rise_time.to("ms"),
                               delay=delay, name=name)

from_dur_area classmethod

from_dur_area(
    system_specs: SystemSpec,
    orientation: ndarray,
    duration: Quantity,
    area: Quantity,
    delay: Quantity = Quantity(0.0, "ms"),
    name: str = "trapezoidal",
) -> TrapezoidalGradient

Construct a trapezoidal or triangular gradient with specified area and duration.

Ramp time is calculated assuming the maximum slew rate. The derivation is documented in the gradients guide.

Parameters:

Name Type Description Default
system_specs SystemSpec

System limits used for validation.

required
orientation ndarray

Gradient orientation vector with shape (3,).

required
duration Quantity

Total gradient duration.

required
area Quantity

Desired gradient area.

required
delay Quantity

Leading time without gradients.

Quantity(0.0, 'ms')
name str

Block name.

'trapezoidal'

Returns:

Type Description
TrapezoidalGradient

Gradient block matching the requested area and duration.

Raises:

Type Description
ValueError

If the duration is not on the gradient raster.

AssertionError

If area < 0.

Source code in cmrseq/core/bausteine/_gradients.py
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
@classmethod
# pylint: disable=W1401
def from_dur_area(cls, system_specs: SystemSpec, orientation: np.ndarray, duration: Quantity,
                  area: Quantity, delay: Quantity = Quantity(0., "ms"),
                  name: str = "trapezoidal") -> 'TrapezoidalGradient':
    r"""Construct a trapezoidal or triangular gradient with specified area and duration.

    Ramp time is calculated assuming the maximum slew rate. The derivation is documented in
    the gradients guide.

    Parameters
    ----------
    system_specs
        System limits used for validation.
    orientation
        Gradient orientation vector with shape `(3,)`.
    duration
        Total gradient duration.
    area
        Desired gradient area.
    delay
        Leading time without gradients.
    name
        Block name.

    Returns
    -------
    TrapezoidalGradient
        Gradient block matching the requested area and duration.

    Raises
    ------
    ValueError
        If the duration is not on the gradient raster.
    AssertionError
        If `area < 0`.
    """
    assert area.m >= 0
    amplitude, rise_time, flat_time = system_specs.get_shortest_gradient(area)
    min_duration = rise_time * 2 + flat_time
    if duration.m_as("ms") < min_duration.m_as("ms") - 1e-6:
        message = f"Infeasible with given system limits, minimal value: {min_duration} > {duration}"
        raise cmrseq.err.BuildingBlockArgumentError(argument='duration', message=message,
                                                    class_name='TrapezoidalGradient')

    duration_raster = system_specs.time_to_raster(duration)
    if not np.isclose((duration - duration_raster).m_as("ms"), 0., rtol=1e-6):
        raise ValueError(f"Specified duration not on raster: {duration.m_as('ms'):1.6f}/"
                         f"{system_specs.grad_raster_time.m_as('ms'):1.6f} is not an integer")

    p_half = duration_raster / 2.
    q = area / system_specs.max_slew  # pylint: disable=C0103

    radicant = (p_half ** 2 - q).to("ms**2")
    if np.isclose(radicant.m, 0., atol=1e-5):
        radicant = Quantity(0, "ms**2")

    rise_time_p = system_specs.time_to_raster(np.abs(p_half + np.sqrt(radicant)), raster="grad")
    rise_time_m = system_specs.time_to_raster(np.abs(p_half - np.sqrt(radicant)), raster="grad")
    if 2 * rise_time_p > duration_raster:
        rise_time = rise_time_m
    else:
        rise_time = rise_time_p

    flat_duration = duration_raster - 2 * rise_time
    amplitude = area / (flat_duration + rise_time)

    return TrapezoidalGradient(system_specs=system_specs, orientation=orientation,
                               amplitude=amplitude,
                               flat_duration=flat_duration,
                               rise_time=rise_time, delay=delay, name=name)

from_fdur_area classmethod

from_fdur_area(
    system_specs: SystemSpec,
    orientation: ndarray,
    flat_duration: Quantity,
    area: Quantity,
    delay: Quantity = Quantity(0.0, "ms"),
    name: str = "trapezoidal",
)

Construct a gradient with specified area and flat duration.

Ramp time is calculated assuming the maximum slew rate. The derivation is documented in the gradients guide.

Parameters:

Name Type Description Default
system_specs SystemSpec

System limits used for validation.

required
orientation ndarray

Gradient orientation vector with shape (3,).

required
flat_duration Quantity

Duration of the gradient plateau.

required
area Quantity

Desired gradient area.

required
delay Quantity

Leading time without gradients.

Quantity(0.0, 'ms')
name str

Block name.

'trapezoidal'

Returns:

Type Description
TrapezoidalGradient

Gradient block matching the requested area and flat duration.

Raises:

Type Description
ValueError

If the flat duration is not on the gradient raster or the area is infeasible.

AssertionError

If area < 0.

Source code in cmrseq/core/bausteine/_gradients.py
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
@classmethod
# pylint: disable=W1401
def from_fdur_area(cls, system_specs: SystemSpec, orientation: np.ndarray,
                   flat_duration: Quantity, area: Quantity,
                   delay: Quantity = Quantity(0., "ms"), name: str = "trapezoidal"):
    r"""Construct a gradient with specified area and flat duration.

    Ramp time is calculated assuming the maximum slew rate. The derivation is documented in
    the gradients guide.

    Parameters
    ----------
    system_specs
        System limits used for validation.
    orientation
        Gradient orientation vector with shape `(3,)`.
    flat_duration
        Duration of the gradient plateau.
    area
        Desired gradient area.
    delay
        Leading time without gradients.
    name
        Block name.

    Returns
    -------
    TrapezoidalGradient
        Gradient block matching the requested area and flat duration.

    Raises
    ------
    ValueError
        If the flat duration is not on the gradient raster or the area is infeasible.
    AssertionError
        If `area < 0`.
    """
    assert area.m >= 0
    amplitude, rise_time, flat_time = system_specs.get_shortest_gradient(area.to("mT/m*ms"))
    if flat_duration < flat_time:
        message = ("Infeasible with given system limits, minimal value:"
                   f" {flat_time} > {flat_duration}")
        raise cmrseq.err.BuildingBlockArgumentError(argument='flat_duration', message=message,
                                                    class_name='TrapezoidalGradient')

    duration_raster = system_specs.time_to_raster(flat_duration, raster="grad")
    if not np.isclose(flat_duration.m_as("ms") - duration_raster.m_as("ms"), 0., rtol=1e-6):
        message = (f"Specified duration not on raster: {flat_duration.m_as('ms'):1.6f}/"
                   f"{system_specs.grad_raster_time.m_as('ms'):1.6f} is not an integer")
        raise cmrseq.err.BuildingBlockArgumentError(argument='flat_duration', message=message,
                                                    class_name='TrapezoidalGradient')

    p_half = flat_duration / 2.
    q = - area / system_specs.max_slew
    radicant = (p_half ** 2 - q).to("ms**2")
    if np.isclose(radicant.m, 0., atol=1e-5):
        rise_time = system_specs.time_to_raster(np.abs(p_half), raster="grad")
    else:
        rise_time_p = system_specs.time_to_raster(np.abs(p_half + np.sqrt(radicant)),
                                                  raster="grad")
        rise_time_m = system_specs.time_to_raster(np.abs(p_half - np.sqrt(radicant)),
                                                  raster="grad")
        rise_time = min(rise_time_m, rise_time_p)
    amplitude = area / (flat_duration + rise_time)
    return TrapezoidalGradient(system_specs=system_specs, orientation=orientation,
                               amplitude=amplitude.to("mT/m"),
                               flat_duration=flat_duration.to("ms"),
                               rise_time=rise_time.to("ms"), delay=delay, name=name)

from_dur_amp classmethod

from_dur_amp(
    system_specs: SystemSpec,
    orientation: ndarray,
    duration: Quantity,
    amplitude: Quantity,
    delay: Quantity = Quantity(0.0, "ms"),
    name: str = "trapezoidal",
)

Constructs the Trapezoidal or triangular (fdur=0) gradient pulse with specified duration and amplitude, given the system limits. Ramp time is calculated under the assumption of using maximal slew rate.

Raises:

Type Description
ValueError

If duration is not on grid & If amplitude is not reachable within specified duration / 2 with given system limits

Source code in cmrseq/core/bausteine/_gradients.py
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
@classmethod
def from_dur_amp(cls, system_specs: SystemSpec, orientation: np.ndarray, duration: Quantity,
                 amplitude: Quantity, delay: Quantity = Quantity(0., "ms"),
                 name: str = "trapezoidal"):
    r"""Constructs the Trapezoidal or triangular (fdur=0) gradient pulse with specified duration
            and amplitude, given the system limits. Ramp time is calculated under the
            assumption of using maximal slew rate.

    Raises
    ------
    ValueError
        If duration is not on grid & If amplitude is not reachable within specified duration / 2 with given system limits
    """
    duration_raster = system_specs.time_to_raster(duration)
    if not np.isclose(duration.m_as("ms") - duration_raster.m_as("ms"), 0., rtol=1e-6):
        raise ValueError(f"Specified duration not on raster: {duration.m_as('ms'):1.6f}/"
                         f"{system_specs.grad_raster_time.m_as('ms'):1.6f} is not an integer")

    if duration / 2 * system_specs.max_slew < amplitude:
        raise ValueError("Specified amplitude not reachable with given slewrate and duration")

    rise_time = system_specs.get_shortest_rise_time(amplitude)
    if rise_time > duration_raster / 2:
        msg = ("Necessary rounding to gradient-raster results in invalid gradient definition"
               f"where minimal rise_time (={rise_time}) to reach specified amplitude is"
               f"larger than half duration {duration_raster / 2} for "
               f"raster-time ({system_specs.grad_raster_time})")
        raise BuildingBlockArgumentError(msg, argument="duration/amplitude",
                                         class_name="TrapezoidalGradient")

    flat_duration = duration - 2 * rise_time
    return TrapezoidalGradient(system_specs=system_specs, orientation=orientation,
                               amplitude=amplitude.to("mT/m"),
                               flat_duration=flat_duration.to("ms"),
                               rise_time=rise_time.to("ms"), delay=delay, name=name)

from_fdur_amp classmethod

from_fdur_amp(
    system_specs: SystemSpec,
    orientation: ndarray,
    flat_duration: Quantity,
    amplitude: Quantity,
    delay: Quantity = Quantity(0.0, "ms"),
    name: str = "trapezoidal",
)

Constructs the Trapezoidal or triangular (fdur=0) gradient pulse with specified flat duration and amplitude, given the system limits. Ramp time is calculated under the assumption of using maximal slew rate.

Raises:

Type Description
ValueError

If flat_duration is not on grid

Source code in cmrseq/core/bausteine/_gradients.py
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
@classmethod
def from_fdur_amp(cls, system_specs: SystemSpec, orientation: np.ndarray,
                  flat_duration: Quantity, amplitude: Quantity,
                  delay: Quantity = Quantity(0., "ms"), name: str = "trapezoidal"):
    r"""Constructs the Trapezoidal or triangular (fdur=0) gradient pulse with specified flat
    duration and amplitude, given the system limits. Ramp time is calculated under the
    assumption of using maximal slew rate.

    Raises
    ------
    ValueError
        If flat_duration is not on grid
    """
    flat_duration_raster = system_specs.time_to_raster(flat_duration)
    if not np.isclose(flat_duration.m_as("ms") - flat_duration_raster.m_as("ms"),
                      0., rtol=1e-6):
        raise ValueError(f"Specified duration not on raster: {flat_duration.m_as('ms'):1.6f}/"
                         f"{system_specs.grad_raster_time.m_as('ms'):1.6f} is not an integer")

    rise_time = system_specs.get_shortest_rise_time(amplitude)
    return TrapezoidalGradient(system_specs=system_specs, orientation=orientation,
                               amplitude=amplitude.to("mT/m"),
                               flat_duration=flat_duration.to("ms"),
                               rise_time=rise_time.to("ms"), delay=delay, name=name)

from_fdur_farea classmethod

from_fdur_farea(
    system_specs: SystemSpec,
    orientation: ndarray,
    flat_duration: Quantity,
    flat_area: Quantity,
    delay: Quantity = Quantity(0.0, "ms"),
    name: str = "trapezoidal",
)

Constructs the Trapezoidal or triangular (fdur=0) gradient pulse with specified flat duration and flat_area, given the system limits. Ramp time is calculated under the assumption of using maximal slew rate.

Raises:

Type Description
ValueError

If flat_duration is not on grid

Source code in cmrseq/core/bausteine/_gradients.py
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
@classmethod
def from_fdur_farea(cls, system_specs: SystemSpec, orientation: np.ndarray,
                    flat_duration: Quantity, flat_area: Quantity,
                    delay: Quantity = Quantity(0., "ms"), name: str = "trapezoidal"):
    r"""Constructs the Trapezoidal or triangular (fdur=0) gradient pulse with specified flat
    duration and flat_area, given the system limits. Ramp time is calculated under the
    assumption of using maximal slew rate.

    Raises
    ------
    ValueError
        If flat_duration is not on grid
    """
    assert flat_area.m >= 0
    flat_duration_raster = system_specs.time_to_raster(flat_duration)
    if not np.isclose(flat_duration.m_as("ms") - flat_duration_raster.m_as("ms"),
                      0., rtol=1e-6):
        raise ValueError(f"Specified duration not on raster: {flat_duration.m_as('ms'):1.6f}/"
                         f"{system_specs.grad_raster_time.m_as('ms'):1.6f} is not an integer")

    amplitude = flat_area / flat_duration
    rise_time = system_specs.get_shortest_rise_time(amplitude)
    return TrapezoidalGradient(system_specs=system_specs, orientation=orientation,
                               amplitude=amplitude.to("mT/m"),
                               flat_duration=flat_duration.to("ms"),
                               rise_time=rise_time.to("ms"), delay=delay, name=name)

ArbitraryGradient

ArbitraryGradient(
    system_specs: SystemSpec,
    time_points: Quantity,
    waveform: Quantity,
    delay: Quantity = Quantity(0, "ms"),
    name: str = "name",
    snap_to_raster: bool = False,
)

Bases: Gradient

Wraps a definition of an arbitrary waveform defined as numpy arrays.

Parameters:

Name Type Description Default
system_specs SystemSpec
required
time_points Quantity

Quantity[Time] array of shape (#steps, ) containing the defining time-points of the gradient waveform

required
waveform Quantity

Quantity[Tesla/Length] array of shape (3, #steps) containing the gradient amplitudes corresponding to time_points

required
Source code in cmrseq/core/bausteine/_gradients.py
601
602
603
604
605
606
607
608
609
def __init__(self, system_specs: SystemSpec,
             time_points: Quantity,
             waveform: Quantity,
             delay: Quantity = Quantity(0, "ms"),
             name: str = "name",
             snap_to_raster: bool = False):
    super().__init__(time_points + delay, waveform,
                     system_specs=system_specs, name=name,
                     snap_to_raster=snap_to_raster)

from_kspace_trajectory classmethod

from_kspace_trajectory(
    system_specs: SystemSpec,
    kspace_traj: Quantity,
    delay: Quantity = Quantity(0, "ms"),
) -> ArbitraryGradient

Creates an ArbitraryGradient waveform block that follows the specified k-space trajectory with minimum duration.

Wraps sigpy.rf functionality: https://sigpy.readthedocs.io/en/latest/generated/sigpy.mri.rf .min_time_gradient.html#sigpy.mri.rf.min_time_gradient

Parameters:

Name Type Description Default
system_specs SystemSpec

SystemSpec instance

required
kspace_traj Quantity

(N, 3) k-space trajectory

required
delay Quantity

Leading time before the gradient starts

Quantity(0, 'ms')
Source code in cmrseq/core/bausteine/_gradients.py
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
@classmethod
def from_kspace_trajectory(cls, system_specs: SystemSpec, kspace_traj: Quantity,
                           delay: Quantity = Quantity(0, "ms")) -> 'ArbitraryGradient':
    r"""Creates an ArbitraryGradient waveform block that follows the specified k-space
    trajectory with minimum duration.

    Wraps sigpy.rf functionality:
    https://sigpy.readthedocs.io/en/latest/generated/sigpy.mri.rf
    .min_time_gradient.html#sigpy.mri.rf.min_time_gradient

    Parameters
    ----------
    system_specs
        SystemSpec instance
    kspace_traj
        (N, 3) k-space trajectory
    delay
        Leading time before the gradient starts
    """
    import sigpy.mri.rf as sigpy_rf

    gmax_gauss_cm = system_specs.max_grad.m_as("T/cm") * 10_000
    smax_gauss_cms = system_specs.max_slew.m_as("T/cm/ms") * 10_000 * 0.95
    dt = system_specs.grad_raster_time.m_as("ms")
    g0 = 0.
    gfin = 0
    curve = kspace_traj.m_as("1/cm")
    gamma = system_specs.gamma.m_as("kHz/T") / 10_000
    grad, _, _, time = sigpy_rf.min_time_gradient(curve, g0, gfin, gmax_gauss_cm,
                                                       smax_gauss_cms, dt, gamma)
    grad = Quantity(grad / 10_000, "T/cm").to("mT/m")
    time = Quantity(time, "ms")
    return ArbitraryGradient(system_specs, time_points=time, waveform=grad.T, delay=delay,
                             name="arbitrary_grad_from_kspace", snap_to_raster=False)

Delay

Delay(
    system_specs: SystemSpec,
    duration: Quantity,
    delay: Quantity = Quantity(0.0, "ms"),
    name: str = "delay",
)

Bases: Gradient

Defines a gradient with zero magnitude and given duration

Defines a gradient with zero magnitude and given duration. This block only makes sense to use when concatenating it to a sequence.

Parameters:

Name Type Description Default
system_specs SystemSpec
required
duration Quantity
required
delay Quantity

Quantity[time] Leading time before object definition

Quantity(0.0, 'ms')
Source code in cmrseq/core/bausteine/_delay.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def __init__(self, system_specs: SystemSpec,
             duration: Quantity,
             delay: Quantity = Quantity(0., "ms"),
             name: str = "delay"):
    r"""Defines a gradient with zero magnitude and given duration. This block only makes sense
    to use when concatenating it to a sequence.

    Parameters
    ----------
    system_specs
    duration
    delay
        Quantity[time] Leading time before object definition
    """
    time = Quantity(np.array([delay.m_as("ms"), (delay + duration).m_as("ms")]), "ms")
    dummy_gradient = Quantity(np.zeros([3, 2]), "mT/m")
    super(). __init__(time, dummy_gradient, system_specs, name, snap_to_raster=False)

RFPulse

RFPulse(
    system_specs: SystemSpec,
    name: str,
    time: Quantity,
    rf_waveform: Quantity,
    frequency_offset: Quantity,
    phase_offset: Quantity,
    bandwidth: Quantity,
    rf_events: Tuple[Quantity, Quantity],
    delay: Quantity = Quantity(0, "ms"),
    snap_to_raster: bool = False,
)

Bases: SequenceBaseBlock

Generic MRI-sequence radio-frequency building block

This class implements all functionality that should be provided by all subtypes of RF-pulses.

The waveform (assuming linear interpolation between the points) and the time-points have to be specified on construction of the RF object, where the waveform is assumed to be real-valued. It also is assumed, that all RF-pulse subclasses correctly calculate and provide the following quantities:

  1. Pulse bandwidth
  2. Frequency offset
  3. Phase offset

The phase offset and frequency offset attributes are used to compute the complex rf-waveform representation using the RFPulse.rf - property.

Parameters:

Name Type Description Default
system_specs SystemSpec

SytemSpecifications object

required
name str

string

required
time Quantity

(# points) time-points defining the waveform duration

required
rf_waveform Quantity

(#points) rf-amplitude

required
phase_offset Quantity

Offset in radians, that is added when computing the complex-valued RF-waveform in RFPulse.rf

required
frequency_offset Quantity

Linear phase contribution, that is added when computing the complex-valued RF-waveform in RFPulse.rf

required
bandwidth Quantity

RF pulse bandwidth in kilo Hertz. Used to calculate gradient strength

required
rf_events Tuple[Quantity, Quantity]

Tuple containing pairs of events defined as (center-time, flip-angle)

required
snap_to_raster bool

if True, all points in the rf definition are rounded to the nearest raster point.

False
Source code in cmrseq/core/bausteine/_rf.py
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
def __init__(self, system_specs: SystemSpec, name: str,
             time: Quantity, rf_waveform: Quantity,
             frequency_offset: Quantity, phase_offset: Quantity,
             bandwidth: Quantity,
             rf_events: Tuple[Quantity, Quantity],
             delay: Quantity = Quantity(0, "ms"),
             snap_to_raster: bool = False):

    # The RF definition will include the delay, but we will keep track of it
    self._rf = (time.to("ms") + delay.to("ms"), rf_waveform.to("uT"))
    self.rf_events = (rf_events[0].to("ms") + delay.to("ms"), rf_events[1].to("degree"))
    self._delay = delay.to("ms")
    self._use = "u"

    self.phase_offset = phase_offset.to("rad")
    self.frequency_offset = frequency_offset.to("Hz")
    self.bandwidth = bandwidth.to("kHz")
    self.label = Label()
    super().__init__(system_specs, name, snap_to_raster)

rf_events instance-attribute

rf_events: Tuple[Quantity, Quantity] = (
    to("ms") + to("ms"),
    to("degree"),
)

bandwidth instance-attribute

bandwidth: Quantity = to('kHz')

phase_offset instance-attribute

phase_offset: Quantity = to('rad')

frequency_offset instance-attribute

frequency_offset: Quantity = to('Hz')

label instance-attribute

label: Label = Label()

tmin property

tmin: Quantity

Returns the minimum time of the RF definition

tmin_block property

tmin_block: Quantity

Returns the minimum time of the RF definition including delay

tmax property

tmax: Quantity

Returns the maximum time of the RF definition

rf property writable

rf: (Quantity, Quantity)

Returns the complex RF-amplitude shifted/modulated by the phase/frequency offsets

isodelay property

isodelay: Quantity

Approximates isodelay as the time interval between peak RF energy and end of pulse, neglecting the (small) nonlinear dependence on flip angle. This is necessary to correctly compute the required gradient area in slice-selective excitation.

pulseq_waveform property

pulseq_waveform: (ndarray, Quantity, ndarray, Quantity)

Computes the normalized magnitude (scaled between 0, 1) and phase for use by pulseq. Note that the phase here does not include phase offset, as this is a property written in the pulseq file It also applies the required half-raster shift to lie on the pulseq grid.

validate

validate(system_specs: SystemSpec)

Validates if the contained rf-definition is valid for the given system- specifications

Source code in cmrseq/core/bausteine/_rf.py
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
def validate(self, system_specs: SystemSpec):
    r"""Validates if the contained rf-definition is valid for the given system-
            specifications"""
    t, wf = self._rf
    float_steps = t.m_as("ms") / system_specs.rf_raster_time.m_as("ms")
    n_steps = np.around(float_steps)
    ongrid = np.allclose(n_steps, float_steps, rtol=1e-6)
    if not all([ongrid]):
        raise cmrseq.err.BuildingBlockValidationError(f"RF definition invalid:\n"
                         f"\t - definition on grid: {ongrid}\n")

    if np.max(np.abs(wf)) > system_specs.rf_peak_power:
        raise cmrseq.err.BuildingBlockValidationError(f"RF definition invalid:\n"
                         f"\t - peak power exceeds system limits: {np.max(np.abs(wf))}\n")

    if not np.allclose([wf[0].m_as("uT"), wf[-1].m_as("uT")],
                       Quantity(0, "uT").m, atol=1e-3):
        start, end = [np.round(wf[i].m_as('uT'), decimals=3) for i in (0, -1)]
        raise cmrseq.err.BuildingBlockValidationError(f"RF definition invalid:\n",
                         f"\t - start/end of waveform != 0: {start}/{end}\n")

shift

shift(time_shift: Quantity) -> None

Adds the time-shift to all rf definition points and the rf-center

Source code in cmrseq/core/bausteine/_rf.py
180
181
182
183
184
def shift(self, time_shift: Quantity) -> None:
    r"""Adds the time-shift to all rf definition points and the rf-center"""
    time_shift =  time_shift.to("ms")
    self._rf = (self._rf[0] + time_shift, self._rf[1])
    self.rf_events = (self.rf_events[0] + time_shift, self.rf_events[1])

flip

flip(time_flip: Quantity = None)

Time reverses block by flipping about a given time point. If no time is specified, the rf center of this block is choosen.

Source code in cmrseq/core/bausteine/_rf.py
186
187
188
189
190
191
192
193
def flip(self, time_flip: Quantity = None):
    r"""Time reverses block by flipping about a given time point. If no
    time is specified, the rf center of this block is choosen."""
    if time_flip is None:
        time_flip = self.rf_events[0][0]
    self._rf = (np.flip(time_flip.to("ms") - self._rf[0], axis=0), np.flip(self._rf[1], axis=1))
    self.rf_events = (np.flip(time_flip.to("ms") - self.rf_events[0], axis=0),
                      np.flip(self.rf_events[1], axis=0))

scale_angle

scale_angle(factor: float)

Scales the contained waveform amplitude and corresponding rf_events by given factor. Resulting in scaled flip angles.

Source code in cmrseq/core/bausteine/_rf.py
195
196
197
198
199
200
def scale_angle(self, factor: float):
    r"""Scales the contained waveform amplitude and corresponding rf_events by
    given factor. Resulting in scaled flip angles.
    """
    self.rf_events = (self.rf_events[0].to("ms"), self.rf_events[1].to("degree") * factor)
    self._rf = (self._rf[0], self._rf[1] * factor)

snap_to_raster

snap_to_raster(system_specs: SystemSpec)

Rounds the time-points and waveform to the nearest raster point. Warning: When calling snap_to_raster the waveform points are simply rounded to their nearest neighbour if the difference is below the relative tolerance. Therefore this is not guaranteed to be precise anymore

Source code in cmrseq/core/bausteine/_rf.py
202
203
204
205
206
207
208
209
210
211
212
213
214
def snap_to_raster(self, system_specs: SystemSpec):
    r"""Rounds the time-points and waveform to the nearest raster point.
    Warning: When calling snap_to_raster the waveform points are simply rounded to their nearest neighbour if the difference is below the relative tolerance.
    Therefore this is not guaranteed to be precise anymore

    """

    warn("RF.snap_to_raster Warning: When calling snap_to_raster the waveform points are simply"
         "rounded to their nearest neighbour if the difference is below the relative tolerance."
         "Therefore this is not guaranteed to be precise anymore")

    t_rf = system_specs.time_to_raster(self._rf[0], "rf")
    self._rf = (t_rf.to("ms"), self._rf[1])

SincRFPulse

SincRFPulse(
    system_specs: SystemSpec,
    duration: Quantity,
    flip_angle: Quantity = Quantity(np.pi, "rad"),
    time_bandwidth_product: float = 3.0,
    center: float = 0.5,
    delay: Quantity = Quantity(0.0, "ms"),
    apodization: float = 0.5,
    frequency_offset: Quantity = Quantity(0.0, "Hz"),
    phase_offset: Quantity = Quantity(0.0, "rad"),
    name: str = "sinc_rf",
)

Bases: RFPulse

Defines a Sinc-RF pulse on a time grid with step length defined by system_specs. The window function used to temporally limit the waveform is given as:

.. math::

window = (1 - \beta) + \beta cos(2 \pi n /N)

where :math:\beta is the specified apodization argument. If set to 0.5 the used window is a Hanning window resulting in 0 start and end. using 0.46 results in the use of a Hamming window.

Parameters:

Name Type Description Default
flip_angle Quantity

Quantity[Angle] Desired Flip angle of the Sinc Pulse. For negative Values the flip-angle is stored as positive absolute plus a phase offset of 180°

Quantity(pi, 'rad')
duration Quantity

Quantity[Time] Total duration of the pulse

required
time_bandwidth_product float

float Used to calculate the pulse-bandwidth. For a Sinc-Pulse bw = time_bandwidth_product/duration corresponds to the half central-lobe-width

3.0
center float

float [0, 1] factor to compute the pulse center relative to duration

0.5
delay Quantity

Adds temporal offset to pulse

Quantity(0.0, 'ms')
apodization float

float from interval [0, 1] used to calculate cosine-apodization window

0.5
frequency_offset Quantity

Frequency offset in Hz in rotating frame ()

Quantity(0.0, 'Hz')
phase_offset Quantity

Phase offset in rad.

Quantity(0.0, 'rad')
name str

semantic label of the building block

'sinc_rf'

Defines a Sinc-RF pulse on a time grid with step length defined by system_specs.

Parameters:

Name Type Description Default
flip_angle Quantity

Quantity[Angle] Desired Flip angle of the Sinc Pulse. For negative Values the flip-angle is stored as positive absolute plus a phase offset of 180°

Quantity(pi, 'rad')
duration Quantity

Quantity[Time] Total duration of the pulse

required
time_bandwidth_product float

float Used to calculate the pulse-bandwidth. For a Sinc-Pulse bw = time_bandwidth_product/duration corresponds to the half central-lobe-width

3.0
center float

float [0, 1] factor to compute the pulse center relative to duration

0.5
delay Quantity
Quantity(0.0, 'ms')
apodization float

float from interval [0, 1] used to calculate cosine-apodization window

0.5
frequency_offset Quantity

Frequency offset in Hz in rotating frame ()

Quantity(0.0, 'Hz')
phase_offset Quantity

Phase offset in rad.

Quantity(0.0, 'rad')
name str
'sinc_rf'
Source code in cmrseq/core/bausteine/_rf.py
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
def __init__(self,
             system_specs: SystemSpec,
             duration: Quantity,
             flip_angle: Quantity = Quantity(np.pi, "rad"),
             time_bandwidth_product: float = 3.,
             center: float = 0.5,
             delay: Quantity = Quantity(0., "ms"),
             apodization: float = 0.5,
             frequency_offset: Quantity = Quantity(0., "Hz"),
             phase_offset: Quantity = Quantity(0., "rad"),
             name: str = "sinc_rf"):
    r"""Defines a Sinc-RF pulse on a time grid with step length defined by system_specs.

    Parameters
    ----------
    flip_angle
        Quantity[Angle] Desired Flip angle of the Sinc Pulse. For negative Values the flip-angle is stored as positive absolute plus a phase offset of 180°
    duration
        Quantity[Time] Total duration of the pulse
    time_bandwidth_product
        float Used to calculate the pulse-bandwidth. For a Sinc-Pulse bw = time_bandwidth_product/duration corresponds to the half central-lobe-width
    center
        float [0, 1] factor to compute the pulse center relative to duration
    delay
    apodization
        float from interval [0, 1] used to calculate cosine-apodization window
    frequency_offset
        Frequency offset in Hz in rotating frame ()
    phase_offset
        Phase offset in rad.
    name
    """

    if flip_angle < Quantity(0, "rad"):
        phase_offset += Quantity(np.pi, "rad")
        flip_angle = -flip_angle

    time_points, unit_wf = self.get_unit_waveform(
                                        raster_time=system_specs.rf_raster_time,
                                        time_bandwidth_product=time_bandwidth_product,
                                        duration=duration, apodization=apodization,
                                        center=center)

    # For Sinc-Pulse this t*bw/duration corresponds to half central lobe width
    bandwidth = Quantity(time_bandwidth_product / duration.to("ms"), "1/ms")

    unit_flip_angle = np.sum((unit_wf[1:] + unit_wf[:-1]) / 2) * system_specs.rf_raster_time.to("ms")\
                      * system_specs.gamma_rad.to("rad/mT/ms")

    amplitude = unit_wf * flip_angle.to("rad") / unit_flip_angle

    super().__init__(system_specs=system_specs, name=name,
                     time=time_points, rf_waveform=amplitude,
                     frequency_offset=frequency_offset, phase_offset=phase_offset,
                     rf_events=(center * duration, flip_angle),
                     bandwidth=bandwidth, delay = delay, snap_to_raster=False)

get_unit_waveform staticmethod

get_unit_waveform(
    raster_time: Quantity,
    time_bandwidth_product: float,
    duration: Quantity,
    apodization: float,
    center: float,
) -> Quantity

Constructs the sinc-pulse waveform according to:

.. math::

wf = (1 - \Gamma + \Gamma cos(2\pi / \Delta * t)) * sinc(tbw/\Delta t)

where

.. math:: \Gamma :& apodization (typically 0.46) \ \Delta :& Pulse duration \ tbw :& Time-bandwidth-product \ t :& time on raster where center defines 0.

Source code in cmrseq/core/bausteine/_rf.py
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
@staticmethod
def get_unit_waveform(raster_time: Quantity, time_bandwidth_product: float,
                      duration: Quantity, apodization: float, center: float) -> Quantity:
    r"""Constructs the sinc-pulse waveform according to:

    .. math::

        wf = (1 - \Gamma + \Gamma cos(2\pi / \Delta * t)) * sinc(tbw/\Delta t)

    where

    .. math::
        \Gamma     :& apodization (typically 0.46) \\
        \Delta     :& Pulse duration \\
        tbw        :& Time-bandwidth-product \\
        t          :& time on raster where center defines 0.


    """
    bandwidth = Quantity(time_bandwidth_product / duration.m_as("ms"), "1/ms")
    n_steps = np.around(duration.m_as("ms") / raster_time.m_as("ms"))
    time_points = Quantity(np.arange(0., n_steps+1, 1) * raster_time.m_as("ms"), "ms")
    time_rel_center = time_points.to("ms") - (center * duration.to("ms"))
    window = (1 - apodization) + apodization * np.cos(2 * np.pi * np.arange(-n_steps//2, n_steps//2+1, 1) / n_steps)
    unit_wf = np.sinc((bandwidth.to("1/ms") * time_rel_center).m_as("dimensionless")) * window
    unit_wf -= unit_wf[0]
    return time_points, unit_wf

from_shortest classmethod

from_shortest(
    system_specs: SystemSpec,
    flip_angle: Quantity,
    time_bandwidth_product: float = 3.0,
    center: float = 0.5,
    delay: Quantity = Quantity(0.0, "ms"),
    apodization: float = 0.5,
    frequency_offset: Quantity = Quantity(0.0, "Hz"),
    phase_offset: Quantity = Quantity(0.0, "rad"),
    name: str = "sinc_rf",
)

Creates the shortest Sinc RF pulse for specified arguments.

Parameters:

Name Type Description Default
flip_angle Quantity

Quantity[Angle] Desired Flip angle of the Sinc Pulse. For negative Values the flip-angle is stored as positive absolute plus a phase offset of 180°

required
time_bandwidth_product float

float Used to calculate the pulse-bandwidth. For a Sinc-Pulse bw = time_bandwidth_product/duration corresponds to the half central-lobe-width

3.0
center float

float [0, 1] factor to compute the pulse center relative to duration

0.5
delay Quantity
Quantity(0.0, 'ms')
apodization float

float from interval [0, 1] used to calculate cosine-apodization window

0.5
frequency_offset Quantity

Frequency offset in Hz in rotating frame ()

Quantity(0.0, 'Hz')
phase_offset Quantity

Phase offset in rad.

Quantity(0.0, 'rad')
name str
'sinc_rf'
Source code in cmrseq/core/bausteine/_rf.py
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
@classmethod
def from_shortest(cls, system_specs: SystemSpec, flip_angle: Quantity,
                  time_bandwidth_product: float = 3., center: float = 0.5,
                  delay: Quantity = Quantity(0., "ms"),
                  apodization: float = 0.5,
                  frequency_offset: Quantity = Quantity(0., "Hz"),
                  phase_offset: Quantity = Quantity(0., "rad"),
                  name: str = "sinc_rf"):
    r"""Creates the shortest Sinc RF pulse for specified arguments.

    Parameters
    ----------
    flip_angle
        Quantity[Angle] Desired Flip angle of the Sinc Pulse. For negative Values the flip-angle is stored as positive absolute plus a phase offset of 180°
    time_bandwidth_product
        float Used to calculate the pulse-bandwidth. For a Sinc-Pulse bw = time_bandwidth_product/duration corresponds to the half central-lobe-width
    center
        float [0, 1] factor to compute the pulse center relative to duration
    delay
    apodization
        float from interval [0, 1] used to calculate cosine-apodization window
    frequency_offset
        Frequency offset in Hz in rotating frame ()
    phase_offset
        Phase offset in rad.
    name
    """
    durations = Quantity(np.linspace(0.1, 1.5, 2), "ms")
    fas = []
    for dur in durations:
        _, unit_wf = cls.get_unit_waveform(raster_time=system_specs.rf_raster_time,
                                           time_bandwidth_product=time_bandwidth_product,
                                           duration=dur, apodization=apodization,
                                           center=center)
        max_wf =  unit_wf * system_specs.rf_peak_power.to("uT")
        fa = np.sum((max_wf[1:] + max_wf[:-1]) / 2 * system_specs.rf_raster_time.to("ms"))
        fa *= system_specs.gamma_rad.to("rad/mT/ms")
        fas.append(fa.m_as("degree"))
    slope = Quantity(np.diff(durations.m_as("ms")) / np.diff(fas), "ms/degree")[0]
    target_duration = system_specs.time_to_raster(np.abs(flip_angle) * slope, "rf")

    return cls(system_specs, duration=target_duration,
               flip_angle=flip_angle, time_bandwidth_product=time_bandwidth_product,
               center=center, delay=delay, apodization=apodization,
               frequency_offset=frequency_offset, phase_offset=phase_offset, name=name)

HardRFPulse

HardRFPulse(
    system_specs: SystemSpec,
    flip_angle: Quantity = Quantity(np.pi, "rad"),
    duration: Quantity = Quantity(1.0, "ms"),
    delay: Quantity = Quantity(0.0, "ms"),
    frequency_offset: Quantity = Quantity(0.0, "Hz"),
    phase_offset: Quantity = Quantity(0.0, "rad"),
    name: str = "hard_rf",
)

Bases: RFPulse

Defines a constant (hard) RF pulse on a time grid with step length defined by system_specs.

Parameters:

Name Type Description Default
flip_angle Quantity

Quantity[Angle] Desired Flip angle of the RF Pulse. For negative Values the flip-angle is stored as positive absolute plus a phase offset of 180°

Quantity(pi, 'rad')
duration Quantity

Quantity[Time] Total duration of the pulse

Quantity(1.0, 'ms')
delay Quantity

Leading time to RR start

Quantity(0.0, 'ms')
frequency_offset Quantity

Frequency offset in Hz in rotating frame ()

Quantity(0.0, 'Hz')
phase_offset Quantity

Phase offset in rad.

Quantity(0.0, 'rad')
name str

defaults to 'hard_rf'

'hard_rf'
Source code in cmrseq/core/bausteine/_rf.py
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
def __init__(self,
             system_specs: SystemSpec,
             flip_angle: Quantity = Quantity(np.pi, "rad"),
             duration: Quantity = Quantity(1., "ms"),
             delay: Quantity = Quantity(0., "ms"),
             frequency_offset: Quantity = Quantity(0., "Hz"),
             phase_offset: Quantity = Quantity(0., "rad"),
             name: str = "hard_rf"):

    if flip_angle < Quantity(0, "rad"):
        phase_offset += Quantity(np.pi, "rad")
        flip_angle = -flip_angle

    raster_time = system_specs.rf_raster_time.to("ms")

    # If duration is too short, we can not create a pulse due to raster time
    if duration<2*system_specs.rf_raster_time:
        duration = 2 *system_specs.rf_raster_time


    # estimate number of steps at the plateau (if any)
    n_steps = np.around((duration.m_as("ms")-2*system_specs.rf_raster_time.m_as("ms"))
                        / raster_time.m_as("ms"))

    # estimate amplitude
    amplitude = (flip_angle / system_specs.gamma_rad / (raster_time * (n_steps + 1))).to('mT')

    # First case, we are below max B1 and have triangular pulse
    if n_steps<1 and amplitude<=system_specs.rf_peak_power:
        time_points = Quantity(np.array([0,1,2]) * raster_time.m_as("ms"), "ms")
        amplitude = amplitude*np.array([0,1,0])
    # Second case, still below max B1 but now have trapezoidal pulse
    elif amplitude<=system_specs.rf_peak_power:
        time_points = Quantity(np.array([0,1,n_steps+1,n_steps+2]) * raster_time.m_as("ms"),
                               "ms")
        amplitude = amplitude * np.array([0, 1, 1, 0])
    # Third case, need to recalculate duration at max B1
    else:
        n_steps = np.ceil((flip_angle / system_specs.gamma_rad /
                           raster_time / system_specs.rf_peak_power-1).m_as("dimensionless"))
        time_points = Quantity(np.array([0, 1, n_steps + 1, n_steps + 2])
                               * raster_time.m_as("ms"), "ms")
        amplitude = (flip_angle / system_specs.gamma_rad /
                     (raster_time * (n_steps + 1))).to('mT') * np.array([0, 1, 1, 0])

    super().__init__(system_specs=system_specs, name=name,
                     time=time_points, rf_waveform=amplitude,
                     frequency_offset=frequency_offset, phase_offset=phase_offset,
                     rf_events=(duration/2, flip_angle),
                     bandwidth=0.5/duration, delay = delay, snap_to_raster=False)

GaussRFPulse

GaussRFPulse(
    system_specs: SystemSpec,
    duration: Quantity,
    flip_angle: Quantity = Quantity(np.pi, "rad"),
    time_bandwidth_product: float = 4.0,
    center: float = 0.5,
    delay: Quantity = Quantity(0.0, "ms"),
    apodization: float = 0.5,
    frequency_offset: Quantity = Quantity(0.0, "Hz"),
    phase_offset: Quantity = Quantity(0.0, "rad"),
    name: str = "gauss_rf",
)

Bases: RFPulse

Defines a Gauss-RF pulse on a time grid with step length defined by system_specs. The window function used to temporally limit the waveform is given as:

.. math::

window = (1 - \beta) + \beta cos(2 \pi n /N)

where :math:\beta is the specified apodization argument. If set to 0.5 the used window is a Hanning window resulting in 0 start and end. using 0.46 results in the use of a Hamming window.

Parameters:

Name Type Description Default
flip_angle Quantity

Quantity[Angle] Desired Flip angle of the Gauss Pulse. For negative Values the flip-angle is stored as positive absolute plus a phase offset of 180°

Quantity(pi, 'rad')
duration Quantity

Quantity[Time] Total duration of the pulse

required
time_bandwidth_product float

float Used to calculate the pulse-bandwidth. For a Gauss-Pulse bw = time_bandwidth_product/duration corresponds to the half central-lobe-width

4.0
center float

float [0, 1] factor to compute the pulse center relative to duration

0.5
delay Quantity

Adds temporal offset to pulse

Quantity(0.0, 'ms')
apodization float

float from interval [0, 1] used to calculate cosine-apodization window

0.5
frequency_offset Quantity

Frequency offset in Hz in rotating frame ()

Quantity(0.0, 'Hz')
phase_offset Quantity

Phase offset in rad.

Quantity(0.0, 'rad')
name str

semantic label of the building block

'gauss_rf'
Source code in cmrseq/core/bausteine/_rf.py
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
def __init__(self, system_specs: 'SystemSpec',
             duration: Quantity,
             flip_angle: Quantity = Quantity(np.pi, "rad"),
             time_bandwidth_product: float = 4.,
             center: float = 0.5,
             delay: Quantity = Quantity(0., "ms"),
             apodization: float = 0.5,
             frequency_offset: Quantity = Quantity(0., "Hz"),
             phase_offset: Quantity = Quantity(0., "rad"),
             name: str = "gauss_rf"):

    if flip_angle < Quantity(0, "rad"):
        phase_offset += Quantity(np.pi, "rad")
        flip_angle = -flip_angle

    time_points, unit_wf = self.get_unit_waveform(
                                        raster_time=system_specs.rf_raster_time,
                                        time_bandwidth_product=time_bandwidth_product,
                                        duration=duration, apodization=apodization,
                                        center=center)

    # For Sinc-Pulse this t*bw/duration corresponds to half central lobe width
    bandwidth = Quantity(time_bandwidth_product / duration.to("ms"), "1/ms")

    unit_flip_angle = (np.sum((unit_wf[1:] + unit_wf[:-1]) / 2)
                       * system_specs.rf_raster_time.to("ms")
                       * system_specs.gamma_rad.to("rad/mT/ms"))
    amplitude = unit_wf * flip_angle.to("rad") / unit_flip_angle

    super().__init__(system_specs=system_specs, name=name,
                     time=time_points, rf_waveform=amplitude,
                     frequency_offset=frequency_offset, phase_offset=phase_offset,
                     rf_events=(center * duration, flip_angle),
                     bandwidth=bandwidth, delay = delay, snap_to_raster=False)

get_unit_waveform staticmethod

get_unit_waveform(
    raster_time: Quantity,
    time_bandwidth_product: float,
    duration: Quantity,
    apodization: float,
    center: float,
) -> Quantity

Constructs a normalized Gaussian pulse waveform according to:

.. math::

wf = (1 - \Gamma + \Gamma cos(2\pi / \Delta * t)) * exp(-(tbw/\Delta t)^2)

where

.. math:: \Gamma :& apodization (typically 0.46) \ \Delta :& Pulse duration \ tbw :& Time-bandwidth-product \ t :& time on raster where center defines 0.

Source code in cmrseq/core/bausteine/_rf.py
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
@staticmethod
def get_unit_waveform(raster_time: Quantity, time_bandwidth_product: float,
                      duration: Quantity, apodization: float,
                      center: float) -> Quantity:
    r"""Constructs a normalized Gaussian pulse waveform according to:

    .. math::

        wf = (1 - \Gamma + \Gamma cos(2\pi / \Delta * t)) * exp(-(tbw/\Delta t)^2)

    where

    .. math::
        \Gamma     :& apodization (typically 0.46) \\
        \Delta     :& Pulse duration \\
        tbw        :& Time-bandwidth-product \\
        t          :& time on raster where center defines 0.


    """
    bandwidth = Quantity(time_bandwidth_product / duration.m_as("ms"), "1/ms")
    n_steps = np.around(duration.m_as("ms") / raster_time.m_as("ms"))
    time_points = Quantity(np.arange(0., n_steps + 1, 1)
                           * raster_time.m_as("ms"), "ms")
    time_rel_center = time_points.to("ms") - (center * duration.to("ms"))
    window = (1 - apodization) + apodization * np.cos(
        2 * np.pi * np.arange(-n_steps // 2, n_steps // 2 + 1, 1) / n_steps)
    unit_wf = np.exp(-(bandwidth.to("1/ms") * time_rel_center).m_as("dimensionless")**2)
    unit_wf *= window
    unit_wf -= unit_wf[0]
    return time_points, unit_wf

from_shortest classmethod

from_shortest(
    system_specs: SystemSpec,
    flip_angle: Quantity,
    time_bandwidth_product: float = 3.0,
    center: float = 0.5,
    delay: Quantity = Quantity(0.0, "ms"),
    apodization: float = 0.5,
    frequency_offset: Quantity = Quantity(0.0, "Hz"),
    phase_offset: Quantity = Quantity(0.0, "rad"),
    name: str = "sinc_rf",
)

Creates the shortest Gauss RF pulse for specified arguments.

Parameters:

Name Type Description Default
flip_angle Quantity

Quantity[Angle] Desired Flip angle of the Gauss Pulse. For negative values the flip-angle is stored as positive absolute plus a phase offset of 180°

required
time_bandwidth_product float

float Used to calculate the pulse-bandwidth. For a Sinc-Pulse bw = time_bandwidth_product/duration corresponds to the half central-lobe-width

3.0
center float

float [0, 1] factor to compute the pulse center relative to duration

0.5
delay Quantity
Quantity(0.0, 'ms')
apodization float

float from interval [0, 1] used to calculate cosine-apodization window

0.5
frequency_offset Quantity

Frequency offset in Hz in rotating frame ()

Quantity(0.0, 'Hz')
phase_offset Quantity

Phase offset in rad.

Quantity(0.0, 'rad')
name str
'sinc_rf'
Source code in cmrseq/core/bausteine/_rf.py
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
@classmethod
def from_shortest(cls, system_specs: SystemSpec, flip_angle: Quantity,
                  time_bandwidth_product: float = 3., center: float = 0.5,
                  delay: Quantity = Quantity(0., "ms"),
                  apodization: float = 0.5,
                  frequency_offset: Quantity = Quantity(0., "Hz"),
                  phase_offset: Quantity = Quantity(0., "rad"),
                  name: str = "sinc_rf"):
    r"""Creates the shortest Gauss RF pulse for specified arguments.

    Parameters
    ----------
    flip_angle
        Quantity[Angle] Desired Flip angle of the Gauss Pulse. For negative values the flip-angle is stored as positive absolute plus a phase offset of 180°
    time_bandwidth_product
        float Used to calculate the pulse-bandwidth. For a Sinc-Pulse bw = time_bandwidth_product/duration corresponds to the half central-lobe-width
    center
        float [0, 1] factor to compute the pulse center relative to duration
    delay
    apodization
        float from interval [0, 1] used to calculate cosine-apodization window
    frequency_offset
        Frequency offset in Hz in rotating frame ()
    phase_offset
        Phase offset in rad.
    name
    """
    durations = Quantity(np.linspace(0.1, 1.5, 2), "ms")
    fas = []
    for dur in durations:
        _, unit_wf = cls.get_unit_waveform(raster_time=system_specs.rf_raster_time,
                                           time_bandwidth_product=time_bandwidth_product,
                                           duration=dur, apodization=apodization,
                                           center=center)
        max_wf = unit_wf * system_specs.rf_peak_power.to("uT")
        fa = np.sum((max_wf[1:] + max_wf[:-1]) / 2 * system_specs.rf_raster_time.to("ms"))
        fa *= system_specs.gamma_rad.to("rad/mT/ms")
        fas.append(fa.m_as("degree"))
    slope = Quantity(np.diff(durations.m_as("ms")) / np.diff(fas), "ms/degree")[0]
    target_duration = system_specs.time_to_raster(np.abs(flip_angle) * slope, "rf")

    return cls(system_specs, duration=target_duration,
               flip_angle=flip_angle, time_bandwidth_product=time_bandwidth_product,
               center=center, delay=delay, apodization=apodization,
               frequency_offset=frequency_offset, phase_offset=phase_offset, name=name)

ArbitraryRFPulse

ArbitraryRFPulse(
    system_specs: SystemSpec,
    time_points: Quantity,
    waveform: Quantity,
    delay: Quantity = Quantity(0.0, "ms"),
    bandwidth: Quantity = None,
    frequency_offset: Quantity = Quantity(0.0, "Hz"),
    phase_offset: Quantity = Quantity(0.0, "rad"),
    snap_to_raster: bool = False,
    name: str = "arbitrary_rf",
)

Bases: RFPulse

Wrapper for arbitrary rf shapes, to adhere to building block concept. The gridding is assumed to be on raster time and not shifted by half a raster time. This shift (useful for simulations) can be incorporated when calling the gridding function of the sequence.

The waveform is assumed to start and end with values of 0 uT. If the given waveform does not adhere to that definition, the arrays are padded.

The rf-center (time-point of effective excitation) is estimated from pulse maximum.

If not specified, the bandwidth of the given waveform is estimated by using the full width at half maximum of the power-spectrum.

.. warning::

For very long pulses, the estimation of bandwidth might not be reasonable anymore, due to
relaxation.

Parameters:

Name Type Description Default
system_specs SystemSpec

SystemSpec instance

required
time_points Quantity

Shape (#steps)

required
waveform Quantity

Shape (#steps) in uT as complex array

required
bandwidth Quantity

In Hz. If not specified, the bandwidth is estimated from the spectrum as full-width-half-maximum.

None
frequency_offset Quantity

Linear phase evolution, which is added to the complex when calling the self.rf property

Quantity(0.0, 'Hz')
phase_offset Quantity

Phase offset, which is added to the complex waveform when calling the self.rf property

Quantity(0.0, 'rad')
snap_to_raster bool

If true waveform is rounded to raster time

False
name str

defaults to 'arbitrary_rf'

'arbitrary_rf'
Source code in cmrseq/core/bausteine/_rf.py
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
def __init__(self, system_specs: SystemSpec,
             time_points: Quantity,
             waveform: Quantity,
             delay: Quantity = Quantity(0., "ms"),
             bandwidth: Quantity = None,
             frequency_offset: Quantity = Quantity(0., "Hz"),
             phase_offset: Quantity = Quantity(0., "rad"),
             snap_to_raster: bool = False,
             name: str = "arbitrary_rf"):

    if not np.isclose(waveform[0].m_as("uT"), 0., atol=1e-3):
        time_points = np.concatenate([[time_points[0] - system_specs.rf_raster_time],
                                       time_points], axis=0)
        waveform = np.concatenate([[Quantity(0., "uT")], waveform], axis=0)

    if not np.isclose(waveform[-1].m_as("uT"), 0., atol=1e-3):
        time_points = np.concatenate([time_points,
                                      [time_points[-1] + system_specs.rf_raster_time]], axis=0)
        waveform = np.concatenate([waveform, [Quantity(0., "uT")]], axis=0)

    _, center_index = _calculate_rf_center(time=time_points.to("ms"),
                                                     rf_waveform=waveform)
    flip_angle = _calculate_flipangle(time=time_points, rf_waveform=waveform,
                                           gamma_rad=system_specs.gamma_rad)

    ## This is a weird case that can occur on loading other format definitions
    if np.allclose(waveform.m_as("uT"), 0., atol=1e-3):
        bandwidth = Quantity(0, "Hz")

    if bandwidth is None:
        _, _, bandwidth = _calculate_bandwidth(time=time_points, rf_waveform=waveform,
                                                    cut_off_percent=0.5,
                                                    min_frequency_resolution=Quantity(10, "Hz"))

    super().__init__(system_specs, name, frequency_offset=frequency_offset,
                     time=time_points.to("ms"), rf_waveform=waveform.to("mT"),
                     phase_offset=phase_offset, bandwidth=bandwidth,
                     rf_events=(time_points[center_index], flip_angle.to("rad")),
                     delay = delay, snap_to_raster=snap_to_raster)

AdiabaticRFPulse

AdiabaticRFPulse(
    system_specs: SystemSpec,
    name: str,
    time: Quantity,
    rf_waveform: Quantity,
    bandwidth: Quantity,
    rf_events: Tuple[Quantity, Quantity],
    phase_offset: Quantity = Quantity(0.0, "rad"),
    frequency_offset: Quantity = Quantity(0.0, "Hz"),
    phase_modulation: Quantity = None,
    frequency_modulation: Quantity = None,
    delay: Quantity = Quantity(0.0, "ms"),
    snap_to_raster: bool = False,
)

Bases: RFPulse

Class for implementation of adiabatic pulses, hence including amplitude and frequency modulation.

The phase offset and frequency offset attributes are used to compute the complex rf-waveform representation using the RFPulse.rf - property.

Parameters:

Name Type Description Default
system_specs SystemSpec

SystemSpecs object

required
name str

string to name the building block

required
time Quantity

(# points) time-points defining the waveform duration

required
rf_waveform Quantity

(#points) rf-amplitude

required
bandwidth Quantity

Effective inversion bandwidth in kilo Hertz. Used to calculate gradient strength

required
rf_events Tuple[Quantity, Quantity]

tuple containing (event, flip angle)

required
phase_offset Quantity

Phase in radians, Used to compute the complex rf-waveform

Quantity(0.0, 'rad')
frequency_offset Quantity

Used to compute the linear phase modulation due to a frequency offset of the complex rf-waveform

Quantity(0.0, 'Hz')
phase_modulation Quantity

Quantity containing a variable phase modulation for all points in the specified rf_waveform

None
frequency_modulation Quantity

Quantity containing a variable frequency modulation for all points. This is added to phase modulation

None
snap_to_raster bool

If true waveform is rounded to raster time

False
Source code in cmrseq/core/bausteine/_rf.py
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
def __init__(self, system_specs: 'cmrseq.SystemSpec',
             name: str, time: Quantity, rf_waveform: Quantity,
             bandwidth: Quantity,
             rf_events: Tuple[Quantity, Quantity],
             phase_offset: Quantity = Quantity(0., "rad"),
             frequency_offset: Quantity = Quantity(0., "Hz"),
             phase_modulation: Quantity = None,
             frequency_modulation: Quantity = None,
             delay: Quantity = Quantity(0., "ms"),
             snap_to_raster: bool = False):
    super().__init__(system_specs, name=name, time=time,
                     rf_waveform=rf_waveform,
                     frequency_offset=frequency_offset,
                     phase_offset=phase_offset, bandwidth=bandwidth,
                     rf_events=rf_events, delay=delay, snap_to_raster=snap_to_raster)
    self.phase_modulation = phase_modulation
    self.frequency_modulation = frequency_modulation

phase_modulation instance-attribute

phase_modulation: Quantity = phase_modulation

frequency_modulation instance-attribute

frequency_modulation: Quantity = frequency_modulation

rf property

rf: (Quantity, Quantity)

Returns the complex RF-amplitude shifted/modulated by the phase/frequency offsets

pulseq_waveform property

pulseq_waveform: (ndarray, Quantity, ndarray, Quantity)

Computes the normalized magnitude (scaled between 0, 1) and phase for use by pulseq. Note that the phase here does not include phase offset, as this is a property written in the pulseq file It also applies the required half-raster shift to lie on the pulseq grid.

from_bir4 classmethod

from_bir4(
    system_specs: SystemSpec,
    duration: Quantity,
    flip_angle: Quantity,
    beta: float,
    kappa: float,
    b1_amplitude: Quantity,
    phase_offset: Quantity = Quantity(0, "rad"),
    delay: Quantity = Quantity(0, "ms"),
    d0: float = 1,
) -> AdiabaticRFPulse

Construct a BIR-4 adiabatic RF pulse.

This wraps sigpy.mri.rf.adiabatic.bir4. Longer legacy figures and parameter plots live in the RF guide.

Parameters:

Name Type Description Default
system_specs SystemSpec

System limits used for validation.

required
duration Quantity

Total pulse duration.

required
flip_angle Quantity

Expected maximal flip angle.

required
beta float

Dimensionless AM constant controlling the adiabatic condition.

required
kappa float

Dimensionless FM constant controlling the adiabatic condition.

required
b1_amplitude Quantity

B1-max scaling.

required
phase_offset Quantity

Phase used to compute the complex RF waveform.

Quantity(0, 'rad')
delay Quantity

Shift of the pulse start.

Quantity(0, 'ms')
d0 float

Dimensionless frequency modulation scale.

1

Returns:

Type Description
AdiabaticRFPulse

Constructed BIR-4 pulse.

Raises:

Type Description
SequenceArgumentError

If duration is not a 4x multiple of the RF raster time.

Source code in cmrseq/core/bausteine/_rf.py
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
@classmethod
def from_bir4(cls, system_specs: 'cmrseq.SystemSpec',
              duration: Quantity,
              flip_angle: Quantity,
              beta: float,
              kappa: float,
              b1_amplitude: Quantity,
              phase_offset: Quantity = Quantity(0, "rad"),
              delay: Quantity = Quantity(0, "ms"),
              d0: float = 1) -> 'AdiabaticRFPulse':
    r"""Construct a BIR-4 adiabatic RF pulse.

    This wraps `sigpy.mri.rf.adiabatic.bir4`. Longer legacy figures and parameter plots live
    in the RF guide.

    Parameters
    ----------
    system_specs
        System limits used for validation.
    duration
        Total pulse duration.
    flip_angle
        Expected maximal flip angle.
    beta
        Dimensionless AM constant controlling the adiabatic condition.
    kappa
        Dimensionless FM constant controlling the adiabatic condition.
    b1_amplitude
        B1-max scaling.
    phase_offset
        Phase used to compute the complex RF waveform.
    delay
        Shift of the pulse start.
    d0
        Dimensionless frequency modulation scale.

    Returns
    -------
    AdiabaticRFPulse
        Constructed BIR-4 pulse.

    Raises
    ------
    SequenceArgumentError
        If duration is not a 4x multiple of the RF raster time.
    """
    import sigpy.mri.rf as sigpy_rf

    n_samples = (duration / system_specs.rf_raster_time).m_as("dimensionless") - 2
    if abs(int(n_samples) - n_samples) > 1e-6 or int(n_samples) % 4 != 0:
        raise cmrseq.err.SequenceArgumentError(message="Duration + 2 raster time not 4x multiple of RF-raster-time",
                                               argument='duration')
    n_samples = int(n_samples)

    dw0 = (d0 * np.pi / duration).m_as("1/ms")
    # Call sigpy and convert modulation into rf-waveform
    amp_modulation, freq_modulation = sigpy_rf.adiabatic.bir4(n_samples, beta, kappa,
                                                              flip_angle.m_as("rad"), dw0)
    rf_waveform = np.pad(amp_modulation, (1, 1)) * b1_amplitude
    frequency_modulation = Quantity(freq_modulation, "kHz")
    frequency_modulation = np.pad(frequency_modulation, (1, 1))
    time = np.arange(0, n_samples + 2) * system_specs.rf_raster_time
    rf_center, _ = _calculate_rf_center(time, rf_waveform)

    rf_events = (rf_center, flip_angle)
    obj = cls(system_specs, name="rf_adiabatic_bir4", time=time, rf_waveform=rf_waveform,
              phase_offset=phase_offset, frequency_offset=Quantity(0, "Hz"),
              bandwidth=Quantity(0, "Hz"), rf_events=rf_events,
              frequency_modulation=frequency_modulation,delay = delay)
    return obj

from_hyperbolic_secant classmethod

from_hyperbolic_secant(
    system_specs: SystemSpec,
    duration: Quantity,
    beta: Quantity,
    mu: float,
    flip_angle: Quantity = None,
    max_amplitude: Quantity = None,
    phase_offset: Quantity = Quantity(0, "rad"),
    frequency_offset: Quantity = Quantity(0, "Hz"),
    delay: Quantity = Quantity(0, "ms"),
) -> AdiabaticRFPulse

Construct an adiabatic hyperbolic secant pulse.

Exactly one of flip_angle or max_amplitude must be specified. Longer references and legacy plots live in the RF guide.

Parameters:

Name Type Description Default
system_specs SystemSpec

System limits used for validation.

required
duration Quantity

Total pulse duration.

required
beta Quantity

Modulation parameter in rad/s.

required
mu float

Frequency modulation scaling factor.

required
flip_angle Quantity

Target flip angle. If specified, peak amplitude is computed from the target.

None
max_amplitude Quantity

Peak RF amplitude.

None
phase_offset Quantity

Phase used to compute the complex RF waveform.

Quantity(0, 'rad')
frequency_offset Quantity

Linear phase contribution added when computing the complex RF waveform.

Quantity(0, 'Hz')
delay Quantity

Shift of the pulse start.

Quantity(0, 'ms')

Returns:

Type Description
AdiabaticRFPulse

Constructed hyperbolic secant pulse.

Raises:

Type Description
SequenceArgumentError

If duration is not on the RF raster time.

Source code in cmrseq/core/bausteine/_rf.py
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
958
959
@classmethod
def from_hyperbolic_secant(cls, system_specs: 'cmrseq.SystemSpec',
                           duration: Quantity,
                           beta: Quantity, mu: float,
                           flip_angle: Quantity = None,
                           max_amplitude: Quantity = None,
                           phase_offset: Quantity = Quantity(0, "rad"),
                           frequency_offset: Quantity = Quantity(0, "Hz"),
                           delay: Quantity = Quantity(0, "ms")) -> 'AdiabaticRFPulse':
    r"""Construct an adiabatic hyperbolic secant pulse.

    Exactly one of `flip_angle` or `max_amplitude` must be specified. Longer references and
    legacy plots live in the RF guide.

    Parameters
    ----------
    system_specs
        System limits used for validation.
    duration
        Total pulse duration.
    beta
        Modulation parameter in rad/s.
    mu
        Frequency modulation scaling factor.
    flip_angle
        Target flip angle. If specified, peak amplitude is computed from the target.
    max_amplitude
        Peak RF amplitude.
    phase_offset
        Phase used to compute the complex RF waveform.
    frequency_offset
        Linear phase contribution added when computing the complex RF waveform.
    delay
        Shift of the pulse start.

    Returns
    -------
    AdiabaticRFPulse
        Constructed hyperbolic secant pulse.

    Raises
    ------
    SequenceArgumentError
        If duration is not on the RF raster time.
    """

    if (flip_angle is None and max_amplitude is None or
            not (flip_angle is None or max_amplitude is None)):
        raise cmrseq.err.BuildingBlockArgumentError(
            message="Exactly one of the arguments mus be specified",
            argument='flip_angle/max_amplitude',
            class_name="AdiabaticRFPulse")

    if beta.units != Quantity(1, "rad/s").units:
        raise cmrseq.err.BuildingBlockArgumentError("Please explicitly specify in rad/s to "
                                                    "prevent conversion errors", argument='beta',
                                                    class_name="AdiabaticRFPulse")

    if max_amplitude is None:
        # Using eq. 17 of stated reference
        # Note: _term_2
        _alpha = flip_angle.m_as("rad")
        _term_0 = math.pi * mu / 2
        _term_1 = math.cos(_alpha) * math.cosh(_term_0) ** 2 + math.sinh(_term_0) ** 2
        _term_2 = (cmath.acos(_term_1) / math.pi) ** 2 + mu ** 2
        max_amplitude = (beta / system_specs.gamma_rad * math.sqrt(_term_2.real)).to("uT")
        del _term_0, _term_1, _term_2, _alpha

    n_samples = (duration / system_specs.rf_raster_time).m_as("dimensionless") - 2
    if abs(int(n_samples) - n_samples) > 1e-6:
        raise cmrseq.err.BuildingBlockArgumentError(message="Duration not on RF-raster-time",
                                                    argument='duration',
                                                    class_name="AdiabaticRFPulse")

    n_samples = int(n_samples)
    t = np.arange(- n_samples // 2, n_samples // 2) / n_samples * duration.to("ms")
    rf_waveform = max_amplitude / np.cosh(beta * t)
    frequency_modulation = - mu * beta * np.tanh(beta * t)

    rf_waveform = np.pad(rf_waveform, (1, 1))
    frequency_modulation = np.pad(frequency_modulation, (1, 1))
    time = np.arange(0, n_samples + 2) * system_specs.rf_raster_time
    rf_center, _ = _calculate_rf_center(time, rf_waveform)

    if flip_angle is None:
        bandwidth = (beta * mu).to("Hz")
        rf_events = (rf_center, Quantity(180, "degree"))
    else:
        # Using eq. 22 in stated reference
        _alpha = flip_angle.m_as("rad")
        _arg_term = math.sqrt(3 + math.cos(_alpha)**2) / 2
        _arg_num = math.cosh(np.pi * mu) * (math.cos(_alpha) -_arg_term) + math.cos(_alpha) - 1
        _arg_den = _arg_term - 1
        bandwidth = beta / np.pi**2 * math.acosh(_arg_num / _arg_den)
        rf_events = (rf_center, flip_angle.to("degree"))

    obj = cls(system_specs, name="rf_adiabatic_hypsec", time=time, rf_waveform=rf_waveform,
              phase_offset=phase_offset, frequency_offset=frequency_offset,
              bandwidth=bandwidth, rf_events=rf_events,
              frequency_modulation=frequency_modulation, delay = delay)
    return obj

SLRPulse

SLRPulse(
    system_specs: Quantity,
    flip_angle: Quantity,
    pulse_duration: Quantity,
    time_bandwidth_product: float,
    pulse_type: str,
    filter_type: str,
    passband_ripple: float = 0.01,
    stopband_ripple: float = 0.01,
    phase_offset: Quantity = Quantity(0, "rad"),
    frequency_offset: Quantity = Quantity(0, "Hz"),
    delay: Quantity = Quantity(0.0, "ms"),
    cancel_alpha_phs: bool = False,
)

Bases: RFPulse

Bundles the construction of RF pulses using the Shinnar-Le Roux as implemented by the sigpy package. For more details on suitable argument values, refer to the following publication:

Pauly, J., Le Roux, Patrick., Nishimura, D., and Macovski, A.(1991). ‘Parameter Relations for the Shinnar-LeRoux Selective Excitation Pulse Design Algorithm’. IEEE Transactions on Medical Imaging, Vol 10, No 1, 53-65.

https://sigpy.readthedocs.io/en/latest/generated/sigpy.mri.rf.slr.dzrf.html

Parameters:

Name Type Description Default
system_specs Quantity
required
flip_angle Quantity
required
pulse_duration Quantity
required
time_bandwidth_product float
required
pulse_type str

Allowed values ["small_tip", "excitation", "se_refocus", "inversion", "saturation"]

required
filter_type str

Allowed values ["sinc", "pm_equal_ripple", "min_phase", "max_phase", "least_squares"]

required
passband_ripple float

Allowed ripple amplitude inside the pass-band in percent (within slice profile)

0.01
stopband_ripple float

Allowed ripple amplitude outside the pass-band in percent (determines side-band excitation signal).

0.01
phase_offset Quantity

Offset in radians, that is added when computing the complex-valued RF-waveform in RFPulse.rf

Quantity(0, 'rad')
frequency_offset Quantity

Linear phase contribution, that is added when computing the complex-valued RF-waveform in RFPulse.rf

Quantity(0, 'Hz')
cancel_alpha_phs bool

For ‘excitation’ pulses, absorb the alpha phase profile from beta’s profile, so they cancel for a flatter total phase

False
Source code in cmrseq/core/bausteine/_rf.py
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
def __init__(self, system_specs: Quantity, flip_angle: Quantity, pulse_duration: Quantity,
             time_bandwidth_product: float, pulse_type: str, filter_type: str,
             passband_ripple: float = 0.01, stopband_ripple: float = 0.01,
             phase_offset: Quantity = Quantity(0, "rad"),
             frequency_offset: Quantity = Quantity(0, "Hz"),
             delay: Quantity = Quantity(0., "ms"),
             cancel_alpha_phs: bool = False):
    import sigpy.mri.rf as sigpy_rf

    pulse_type_lu = {"small_tip": "st", "excitation": "ex", "se_refocus":"se",
                      "inversion": "inv", "saturation":"sat"}
    if pulse_type not in pulse_type_lu.keys():
        raise cmrseq.err.BuildingBlockArgumentError(
            "Not in allowed values: " + '\n'.join(list(pulse_type_lu.keys())),
            argument='pulse_type', class_name='SLRPulse'
        )

    filter_type_lu = {"sinc": "ms", "pm_equal_ripple": "pm", "min_phase": "min",
                      "max_phase": "max", "least_squares": "ls"}
    if pulse_type not in pulse_type_lu.keys():
        raise cmrseq.err.BuildingBlockArgumentError(
            "Not in allowed values:" + '\n'.join(list(filter_type_lu.keys())),
            argument='filter_type', class_name='SLRPulse'
        )

    n_samples = (pulse_duration / system_specs.rf_raster_time).m_as("dimensionless")
    if abs(n_samples - int(n_samples)) > 1e-6:
        raise cmrseq.err.BuildingBlockArgumentError(
            "Not on RF raster-time", argument='pulse_duration', class_name='SLRPulse')
    n_samples = int(n_samples) - 2

    pulse = sigpy_rf.slr.dzrf(n_samples, time_bandwidth_product,
                              pulse_type_lu[pulse_type],
                              filter_type_lu[filter_type],
                              passband_ripple, stopband_ripple,
                              cancel_alpha_phs=cancel_alpha_phs)
    pulse = np.pad(pulse.real, (1, 1), mode="constant")

    if flip_angle is not None:
        flip_norm = Quantity(np.sum(pulse) * system_specs.rf_raster_time.m_as("s") * np.pi * 2,
                             'rad')
        rf_waveform = Quantity((pulse * flip_angle / flip_norm).m_as("dimensionless"),
                               "Hz") / system_specs.gamma
    else: # To get unscaled waveform
        rf_waveform = Quantity(pulse, "uT")
        flip_angle = Quantity(0, "degree")

    if np.max(rf_waveform.to("uT") > system_specs.rf_peak_power):
        raise cmrseq.err.BuildingBlockArgumentError(
                message="Too short for given peak power limit",
                argument="pulse_duration", class_name="SLRPulse")
    time = np.arange(0, n_samples+2) * system_specs.rf_raster_time
    rf_center, _ = _calculate_rf_center(time, rf_waveform)
    rf_events = (rf_center, flip_angle)

    super().__init__(system_specs, name="slr_rf_pulse", time=time,
                     rf_waveform=rf_waveform, frequency_offset=frequency_offset,
                     phase_offset=phase_offset,
                     bandwidth=time_bandwidth_product / pulse_duration,
                     rf_events=rf_events, delay = delay, snap_to_raster=False)

ADC

ADC(
    system_specs: SystemSpec,
    name: str,
    adc_timing: Quantity,
    adc_center: Quantity,
    frequency_offset: Quantity,
    phase_offset: Quantity,
    delay=Quantity(0, "ms"),
)

Bases: SequenceBaseBlock

ADC-specific extension to the SequenceBaseBlock, serves as base class for all ADC implementations.

Parameters:

Name Type Description Default
system_specs SystemSpec

System Limits specification object

required
name str
required
adc_timing Quantity

Quantity array of dimension time, containing all sampling event timings.

required
adc_center Quantity

Time point defining the center of the ADC object

required
phase_offset Quantity

Phase-offset for all adc-samples, added when computing the adc-phase.

required
frequency_offset Quantity

Frequency offset for all adc-samples, converted to an additional phase offset per sample

required
Source code in cmrseq/core/bausteine/_adc.py
51
52
53
54
55
56
57
58
59
60
def __init__(self, system_specs: SystemSpec, name: str,
             adc_timing: Quantity, adc_center: Quantity,
             frequency_offset: Quantity, phase_offset: Quantity, delay = Quantity(0, "ms")):
    self.adc_timing: Quantity = adc_timing.to("ms") + delay.to("ms")
    self.adc_center: Quantity = adc_center.to("ms") + delay.to("ms")
    self._delay = delay.to("ms")
    self.frequency_offset: Quantity = frequency_offset.to("Hz")
    self.phase_offset: Quantity = phase_offset.to("rad")
    self.label = Label()
    super().__init__(system_specs, name)

adc_timing instance-attribute

adc_timing: Quantity = to('ms') + to('ms')

adc_center instance-attribute

adc_center: Quantity = to('ms') + to('ms')

phase_offset instance-attribute

phase_offset: Quantity = to('rad')

frequency_offset instance-attribute

frequency_offset: Quantity = to('Hz')

label instance-attribute

label: Label = Label()

adc_phase property

adc_phase: Quantity

Returns the phase :math:\phi_s at each adc sample :math:s in radians given the phase offset :math:\phi_0 and frequency offset :math:\delta f according to the formular:

.. math::

\phi_s = \phi_0 + 2 * \pi * \delta f

tmin property

tmin: Quantity

Returns the time of the first sampling event.

tmin_block property

tmin_block: Quantity

Returns the time of the first sampling event minus the delay.

tmax property

tmax: Quantity

Returns the time of the last sampling event.

anchor_time property

anchor_time: Quantity

Reference time used for plotting and alignment.

Subclasses may override this when the ADC is explicitly anchored to a non-center boundary.

validate

validate(system_specs: SystemSpec)

Validates the dwell time against the system_specs, ensuring it sits on the ADC raster time.

Source code in cmrseq/core/bausteine/_adc.py
101
102
103
104
105
106
107
108
def validate(self, system_specs: SystemSpec):
    r"""Validates the dwell time against the system_specs, ensuring it sits on the ADC raster time."""
    unique_dwell_times = np.unique(np.round(np.diff(self.adc_timing.m_as("ms")), decimals=6))
    n_dwell =  np.round(unique_dwell_times/np.round(system_specs.adc_raster_time.m_as("ms"), decimals=6), decimals=6)
    dwell_remainder = np.mod(n_dwell, 1)
    if not np.allclose(dwell_remainder, 0., atol=1e-4):
        raise BuildingBlockValidationError(f"ADC dwell-time is not multiple of ADC-raster time"
                                           f"\n\t  {unique_dwell_times} \n\t {dwell_remainder}")

shift

shift(time_shift: Quantity) -> None

Adds the time-shift to all adc definition points and the adc-center

Source code in cmrseq/core/bausteine/_adc.py
110
111
112
113
114
def shift(self, time_shift: Quantity) -> None:
    r"""Adds the time-shift to all adc definition points and the adc-center"""
    time_shift = time_shift.to("ms")
    self.adc_timing += time_shift
    self.adc_center += time_shift

flip

flip(time_flip: Quantity = None)

Flips the adc-timing and adc-center around the given time point.

Source code in cmrseq/core/bausteine/_adc.py
116
117
118
119
120
121
def flip(self, time_flip: Quantity = None):
    r"""Flips the adc-timing and adc-center around the given time point."""
    if time_flip is None:
        time_flip = self.tmax
    self.adc_timing = np.flip(time_flip.to("ms") - self.adc_timing, axis=0)
    self.adc_center = np.flip(time_flip.to("ms") - self.adc_center, axis=0)

snap_to_raster

snap_to_raster(system_specs: SystemSpec) -> None
Source code in cmrseq/core/bausteine/_adc.py
123
124
def snap_to_raster(self, system_specs: SystemSpec) -> None:
    pass

SymmetricADC

SymmetricADC(
    system_specs: SystemSpec,
    num_samples: int,
    dwell: Quantity = None,
    duration: Quantity = None,
    delay: Quantity = None,
    frequency_offset: Quantity = Quantity(0.0, "Hz"),
    phase_offset: Quantity = Quantity(0.0, "rad"),
    name: str = "adc",
)

Bases: ADC

ADC with instantaneous encoding events at k-space positions.

Defines an ADC with sampling events uniformly distributed over the given duration. The central time point is always contained as sampling event.

Sample time always corresponds to the center of the sampling event.

Parameters:

Name Type Description Default
num_samples int

number of sampling events over duration

required
system_specs SystemSpec

cmrseq.SystemSpec object

required
dwell Quantity

Quantity[time] Interval length associated with 1 sampling event. Corresponds to kspace extend in readout-direction :math:(1/FOV_{kx}).

None
duration Quantity

Quantity[time] Total sampling duration corresponding to :math:(1 / \Delta k_x). Usually is the same as flat_duration of accompanying trapezoidal gradient.

None
delay Quantity

Quantity[time] Leading time without sampling events

None
frequency_offset Quantity

Adds a linearly increasing phase over the ADC duration, used for e.g. RF-spoiling or in-plane FOV shift.

Quantity(0.0, 'Hz')
phase_offset Quantity

Adds a constant phase offset to the adc, e.g. in RF spoiling

Quantity(0.0, 'rad')
Source code in cmrseq/core/bausteine/_adc.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
def __init__(self, system_specs: SystemSpec,
             num_samples: int,
             dwell: Quantity = None,
             duration: Quantity = None,
             delay: Quantity = None,
             frequency_offset: Quantity = Quantity(0., "Hz"),
             phase_offset: Quantity = Quantity(0., "rad"),
             name: str = "adc"):

    if (dwell is None and duration is None) or not (dwell is None or duration is None):
        raise ValueError("Either dwell or duration must be defined")

    if duration:
        dwell = duration / num_samples
    delay = Quantity(0, "ms") if delay is None else delay
    adc_timing = (np.arange(0, num_samples) + 0.5) * dwell

    frequency_offset = frequency_offset.to("Hz")
    phase_offset = phase_offset.to("rad")

    self._n_samples = int(num_samples)
    self._dwell = dwell
    adc_center = adc_timing[int(np.floor(num_samples / 2))]

    super().__init__(system_specs=system_specs, name=name,
                     adc_timing=adc_timing, adc_center=adc_center,
                     phase_offset=phase_offset,
                     frequency_offset=frequency_offset, delay = delay)

tmin property

tmin: Quantity

Returns the time of the first sampling event. Behavior varies for odd/even number of samples:

Returns the time of the first sampling event minus half a dwell time on gradient raster time.

In both cases this corresponds to the start of the plateau of a readout gradient

tmax property

tmax: Quantity

Returns the time of the last sampling event. Behavior varies for odd/even number of samples:

Returns the time of the last sampling event plus half a dwell time.

In both cases this corresponds to the end of the plateau of a readout gradient

anchor_time property

anchor_time: Quantity

Time of the guaranteed anchor sample (the center sample).

For SymmetricADC, the anchor is the sampling event guaranteed to be at the center of the ADC block (as constructed). This is identical to adc_center.

from_centered_valid classmethod

from_centered_valid(
    system_specs: SystemSpec,
    num_samples: int,
    duration: Quantity,
    delay: Quantity = Quantity(0.0, "ms"),
    frequency_offset: Quantity = Quantity(0.0, "Hz"),
    phase_offset: Quantity = Quantity(0.0, "rad"),
    name="adc",
    suppress_warnings=False,
) -> SymmetricADC

Creates an ADC block with valid duration (dwell time on raster) where the stated duration is the upper bound (altered by at max num_samples * adc_raster_time). The difference in duration is padded around at the start and end of the block to maintain the center.

Guarantees to have a sample at the exact half duration of the ADC block.

Parameters:

Name Type Description Default
num_samples int

number of sampling events over duration

required
system_specs SystemSpec

cmrseq.SystemSpec object

required
duration Quantity

target duration that is modified such that the resulting dwell time is on the adc raster

required
delay Quantity

Quantity[time] Leading time without sampling events

Quantity(0.0, 'ms')
frequency_offset Quantity

Adds a linearly increasing phase over the ADC duration, used for e.g. RF-spoiling or in-plane FOV shift.

Quantity(0.0, 'Hz')
phase_offset Quantity

Adds a constant phase offset to the adc, e.g. in RF spoiling

Quantity(0.0, 'rad')
Source code in cmrseq/core/bausteine/_adc.py
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
@classmethod
def from_centered_valid(cls, system_specs: SystemSpec, num_samples: int, duration: Quantity,
                        delay: Quantity = Quantity(0.,'ms'), frequency_offset: Quantity = Quantity(0., "Hz"),
                        phase_offset: Quantity = Quantity(0., "rad"), name="adc", suppress_warnings=False
                        ) -> 'SymmetricADC':
    r"""Creates an ADC block with valid duration (dwell time on raster) where the stated duration
    is the upper bound (altered by at max num_samples * adc_raster_time). The difference in
    duration is padded around at the start and end of the block to maintain the center.

    Guarantees to have a sample at the exact half duration of the ADC block.

    Parameters
    ----------
    num_samples
        number of sampling events over duration
    system_specs
        cmrseq.SystemSpec object
    duration
        target duration that is modified such that the resulting dwell time is on the adc raster
    delay
        Quantity[time] Leading time without sampling events
    frequency_offset
        Adds a linearly increasing phase over the ADC duration, used for e.g. RF-spoiling or in-plane FOV shift.
    phase_offset
        Adds a constant phase offset to the adc, e.g. in RF spoiling

    """
    dwell = duration / num_samples
    if not system_specs.is_on_raster(dwell, "adc")[0]:
        valid_dwell = (system_specs.time_to_raster(duration / num_samples, "adc")
                       - system_specs.adc_raster_time)
        if valid_dwell<system_specs.adc_raster_time:
            raise ValueError("ADC dwell time is smaller than raster. Either increase ADC duration or decrease number of samples.")

        valid_duration = np.round((num_samples * valid_dwell).to("ms"), decimals=6)
        duration_diff = np.round((duration - valid_duration).to("ms"), decimals=6)

        if not system_specs.is_on_raster(duration_diff / 2, "adc")[0]:
            valid_dwell -= system_specs.adc_raster_time
            valid_duration = np.round((num_samples * valid_dwell).to("ms"), decimals=6)
            duration_diff = np.round((duration - valid_duration).to("ms"), decimals=6)
        delay = delay + system_specs.time_to_raster(duration_diff.to("ns") / 2, "adc")
        if not suppress_warnings:
            warnings.warn(f"In SymmetricADC.from_centered_valid() modified duration to get"
                        f" a valid dwell time:\n\t\t{duration=}\n\t\t{valid_duration=}.\n\t"
                        f"+ To avoid this, make sure duration/num_samples is a multiple"
                        f" of system_specs.adc_raster_time", AutomaticOptimizationWarning)
    else:
        valid_duration = duration
    return cls(system_specs, num_samples, duration=valid_duration, delay=delay,
               frequency_offset=frequency_offset, phase_offset=phase_offset, name=name)

GridSamplingADC

GridSamplingADC(
    system_specs: SystemSpec,
    duration: Quantity,
    delay: Quantity = Quantity(0, "ms"),
    frequency_offset: Quantity = Quantity(0.0, "Hz"),
    phase_offset: Quantity = Quantity(0.0, "rad"),
    name: str = "adc",
)

Bases: ADC

Defines an oversampling adc-block on system adc_raster_time.

Parameters:

Name Type Description Default
system_specs SystemSpec

SystemSpec instance

required
duration Quantity

Duration over which the ADC is active on raster time. Is assumed to be on adc-raster-time

required
delay Quantity

Leading time before the ADC block starts. Is assumed to be on adc-raster-time

Quantity(0, 'ms')
frequency_offset Quantity

Linear phase evolution that is added to the demodulation over the ADC duration

Quantity(0.0, 'Hz')
phase_offset Quantity

Phase offset that is added to the demodulation

Quantity(0.0, 'rad')
name str

defaults to 'adc'

'adc'
Source code in cmrseq/core/bausteine/_adc.py
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
def __init__(self, system_specs: SystemSpec,
             duration: Quantity,
             delay: Quantity = Quantity(0, "ms"),
             frequency_offset: Quantity = Quantity(0., "Hz"),
             phase_offset: Quantity = Quantity(0., "rad"),
             name: str = "adc"):
    rounded_raster_time = decimal.Decimal(
        str(float(np.round(system_specs.adc_raster_time.m_as("ms"), decimals=6))))
    delay_dec = decimal.Decimal(str(float(np.round(delay.m_as("ms"), decimals=6))))
    duration_dec = decimal.Decimal(str(float(np.round(duration.m_as("ms"), decimals=6))))
    if delay_dec % rounded_raster_time != decimal.Decimal("0.0"):
        raise BuildingBlockArgumentError(f"Specified delay {delay:1.6} is not"
                                         f" on adc_raster_time", argument="delay",
                                         class_name="GridSamplingADC")
    if duration_dec % rounded_raster_time != decimal.Decimal("0.0"):
        raise BuildingBlockArgumentError(f"Specified duration {duration:1.6} is not"
                                         f" on adc_raster_time", argument="duration",
                                         class_name="GridSamplingADC")
    n_steps = math.ceil(duration / system_specs.adc_raster_time)
    time_grid = np.arange(0, n_steps + 1, 1) * system_specs.adc_raster_time.m_as("ms")

    super().__init__(system_specs=system_specs, name=name,
                     adc_timing=Quantity(time_grid, "ms"),
                     adc_center=system_specs.time_to_raster(duration / 2, "adc"),
                     frequency_offset=frequency_offset,
                     phase_offset=phase_offset,
                     delay=delay)

tmin property

tmin: Quantity

Returns the time of the first sampling event.

tmax property

tmax: Quantity

Returns the time of the last sampling event.