Skip to content

Core Components

Sequence

Sequence(
    building_blocks: List[SequenceBaseBlock],
    system_specs: SystemSpec,
    snap_to_raster: bool = False,
    copy: bool = False,
)

Container for MRI sequence building blocks.

A sequence stores mutable SequenceBaseBlock instances and validates them against a shared SystemSpec. Conceptual usage examples live in the sequence model guide.

Parameters:

Name Type Description Default
building_blocks List[SequenceBaseBlock]

Building blocks to include in the sequence.

required
system_specs SystemSpec

System limits used for validation and rasterization.

required
snap_to_raster bool

If True, snap all blocks to the corresponding system rasters before validation.

False
copy bool

If True, copy the supplied blocks before storing them.

False
Source code in cmrseq/core/_sequence.py
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
def __init__(self, building_blocks: List[SequenceBaseBlock], system_specs: SystemSpec,
             snap_to_raster: bool = False, copy: bool = False):

    self._system_specs = system_specs
    if copy:
        self._blocks = [b.copy() for b in building_blocks]
    else:
        self._blocks = building_blocks

    if snap_to_raster:
        [b.snap_to_raster(self._system_specs) for b in self._blocks]

    self._block_lookup = {}
    self._reverse_block_lookup = {}
    self._block_slug_cache = {}
    for block in self._blocks:
        self._add_unique_block_name(block)
    self._orientation_matrices_grad = {}
    self._orientation_matrices_rf = {}

    self._global_orientation_matrix = None
    self.validate()

duration property

duration: Quantity

Time difference of earliest start and latest end of all blocks contained in the sequence

start_time property

start_time

Returns temporal minimum of all contained block definitions

end_time property

end_time

Returns temporal maximum of all contained block definitions

gradients property

gradients: List[Tuple[Quantity, Quantity]]

Returns the gradient definitions (t, wf) of all Gradient-type blocks that are contained in the sequence. If an OMatrix is registered the gradient channels are rotated accordingly by applying the OMatrix object

rf property

rf: List[Tuple[Quantity, Quantity]]

Returns the rf definitions (t, amplitude) of RFPulse-type blocks that are contained in the sequence. If an OMatrix is registered the frequency offset is adjusted accordingly by applying the OMatrix object

rf_events property

rf_events: List[Tuple[Quantity, Quantity]]

Returns the rf events (rf-center, flip-angle) of RFPulse-type blocks that are contained in the sequence.

adc_centers property

adc_centers: List[Quantity]

Returns the centers of all adc_blocks in the sequence.

blocks property

blocks: List[str]

Returns a tuple containing the names of all blocks contained in the sequence object, where temporal ordering is assumed

items

items()

Returns a generator yielding (unique_block_name, block) tuples

Source code in cmrseq/core/_sequence.py
173
174
175
176
def items(self):
    r"""Returns a generator yielding (unique_block_name, block) tuples"""
    names_and_times = self._create_sorted_block_list(reversed=False)
    return ((k, self._block_lookup[k]) for (k, _) in names_and_times)

validate

validate() -> None

Calls the validation function of each block with self._system_specs

Raises:

Type Description
ValueError

If any contained block fails to validate with own system specs

ValueError

If any combination of contained acquisition blocks have temporal overlap.

:raise ValueError: If all combined gradient definitions exceed system limits (max amplitude

and slew-rate)

Source code in cmrseq/core/_sequence.py
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
def validate(self) -> None:
    r"""Calls the validation function of each block with self._system_specs

    Raises
    ------
    ValueError
        If any contained block fails to validate with own system specs
    ValueError
        If any combination of contained acquisition blocks have temporal overlap.
    :raise ValueError: If all combined gradient definitions exceed system limits (max amplitude
                       and slew-rate)
    """
    for block in self._blocks:
        try:
            block.validate(system_specs=self._system_specs)
        except ValueError as err:
            unique_name = self._reverse_block_lookup[id(block)]
            err.args = (f'While validation of bock {unique_name}: \n\t' + err.args[0],)
            raise err

    self._validate_combined_gradient_limits()
    self._validate_overlap(ADC, self._system_specs.adc_dead_time)
    self._validate_overlap(RFPulse, self._system_specs.rf_dead_time)
    self._validate_overlapping_rf_adc()

add_block

add_block(
    block: SequenceBaseBlock, copy: bool = True
) -> None

Add the instance of block to the internal List of sequence blocks.

Note: The internal definition of blocks is mutable, therefore if the new block is not copied, subsequent alterations can have unwanted side-effects inside the sequence.

Parameters:

Name Type Description Default
block SequenceBaseBlock

Sequence block to be added to the sequence

required
copy bool

Determines if the block is copied before adding it to the sequence

True

Raises:

Type Description
ValueError

If block.validate() fails to validate using the system specs of self

TypeError

If block is an instance of class SequenceBaseBlock

Source code in cmrseq/core/_sequence.py
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
def add_block(self, block: SequenceBaseBlock, copy: bool = True) -> None:
    r"""Add the instance of block to the internal List of sequence blocks.

    **Note**: The internal definition of blocks is mutable, therefore if the new block is not
    copied, subsequent alterations can have unwanted side-effects inside the sequence.

    Parameters
    ----------
    block
        Sequence block to be added to the sequence
    copy
        Determines if the block is copied before adding it to the sequence

    Raises
    ------
    ValueError
        If block.validate() fails to validate using the system specs of self
    TypeError
        If block is an instance of class SequenceBaseBlock
    """

    if not isinstance(block, SequenceBaseBlock):
        raise NotImplementedError("Method only defined for instances of SequenceBaseBlocks."
                                  f"Got {type(block)}")
    try:
        block.validate(self._system_specs)
    except ValueError as err:
        raise ValueError("New block does not validate against sequence system specifications."
                         f"Resulting in following ValueError: {err}") from err
    if copy:
        block = deepcopy(block)
    self._blocks.append(block)
    self._add_unique_block_name(block)

rename_blocks

rename_blocks(old_names: List[str], new_names: List[str])

Renames blocks and updates block lookup map

Source code in cmrseq/core/_sequence.py
360
361
362
363
364
365
366
367
def rename_blocks(self, old_names: List[str], new_names: List[str]):
    r"""Renames blocks and updates block lookup map"""
    for old, new in zip(old_names, new_names):
        bl = self._block_lookup[old]
        bl.name = new
    self._block_lookup = {}
    for block in self._blocks:
        self._add_unique_block_name(block)

remove_block

remove_block(block_name: str)

Removes block from internal lookup

Source code in cmrseq/core/_sequence.py
369
370
371
372
373
374
375
376
377
378
379
380
381
382
def remove_block(self, block_name: str):
    r"""Removes block from internal lookup """
    block = self.get_block(block_name)
    if block is None:
        raise ValueError(f"Tried to remove non-existing block; \n "
                         f"'{block_name}' not in {self.blocks}")
    block_index = [block is b for b in self._blocks].index(True)
    del self._blocks[block_index]
    del self._block_lookup[block_name]
    del self._reverse_block_lookup[id(block)]
    if self._orientation_matrices_grad.get(id(block), None) is not None:
        del self._orientation_matrices_grad[id(block)]
    if self._orientation_matrices_rf.get(id(block), None) is not None:
        del self._orientation_matrices_rf[id(block)]

append

append(
    other: Union[Sequence, SequenceBaseBlock],
    copy: bool = True,
    end_time: Quantity = None,
) -> None

If both system specifications match, copies all blocks from other, shifts them by the current end time of this sequence intance (plus an additional delay according to ADC/RF - dead times and RF-ring-down time) and adds the blocks to itself.

Parameters:

Name Type Description Default
other Union[Sequence, SequenceBaseBlock]

Sequence or block to be added to the sequence

required
copy bool

if true copies the other sequence object

True
end_time Quantity
None

Raises:

Type Description
ValueError

If other fails to validate using the system specs of self

Source code in cmrseq/core/_sequence.py
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
def append(self, other: Union['Sequence', SequenceBaseBlock],
           copy: bool = True, end_time: Quantity = None) -> None:
    r"""If both system specifications match, copies all blocks from `other`, shifts them by the
    current end time of this sequence intance (plus an additional delay according to ADC/RF -
    dead times and RF-ring-down time) and adds the blocks to itself.

    Parameters
    ----------
    other
        Sequence or block to be added to the sequence
    copy
        if true copies the other sequence object
    end_time

    Raises
    ------
    ValueError
        If other fails to validate using the system specs of self
    """
    if isinstance(other, SequenceBaseBlock):
        try:
            other.validate(self._system_specs)
        except ValueError as err:
            raise ValueError(
                "New block does not validate against sequence system specifications."
                f"Resulting in following ValueError: {err}") from err
        block_copies = [other, ]
    elif isinstance(other, Sequence):
        self._check_sys_compatibility(other._system_specs)  # pylint: disable=W0212
        block_copies = [other.get_block(block_name) for block_name in other.blocks]
        ids = [id(block) for block in block_copies] # list of IDs for transfering o-matrices
        self.update_global_omatrix(other._global_orientation_matrix, warn_assign=True)
    else:
        raise NotImplementedError(f"Cannot append object of type {type(other)} to Sequence")

    if copy:
        block_copies = [deepcopy(block) for block in block_copies]

    if end_time is None:
        if not self._blocks:
            end_time = Quantity(0., "ms")
        else:
            end_time = self._get_append_delay(other)

    for block in block_copies:
        block.shift(Quantity(end_time, "ms"))

    self._blocks.extend(block_copies)
    if isinstance(other, Sequence):
        for block,blid in zip(block_copies,ids):
            self._add_unique_block_name(block)
            # Update omatrix dicts
            if blid in other._orientation_matrices_grad:
                self._orientation_matrices_grad[id(block)] = other._orientation_matrices_grad[blid]
            if blid in other._orientation_matrices_rf:
                self._orientation_matrices_rf[id(block)] = other._orientation_matrices_rf[blid]
    else:
        for block in block_copies:
            self._add_unique_block_name(block)

fast_extend

fast_extend(other, copy: bool = True)

Faster alternative to extend() that: - preserves the original extend() semantics (other is a sequence of Sequence or individual SequenceBaseBlock items), - keeps a tqdm progress bar, - avoids calling self.append() per-sequence (which causes growing costs), - does a single bulk extend of self._blocks and a single bulk update of lookups and o-matrix dictionaries.

NOTE: This directly manipulates internals (_blocks, _block_lookup, etc.) for speed.

Source code in cmrseq/core/_sequence.py
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
def fast_extend(self, other, copy: bool = True):
    """
    Faster alternative to extend() that:
    - preserves the original extend() semantics (other is a sequence of Sequence
        or individual SequenceBaseBlock items),
    - keeps a tqdm progress bar,
    - avoids calling self.append() per-sequence (which causes growing costs),
    - does a single bulk extend of self._blocks and a single bulk update of lookups
        and o-matrix dictionaries.

    NOTE: This directly manipulates internals (_blocks, _block_lookup, etc.) for speed.
    """
    if not other:
        return self

    # 1) Compute the same per-element delays as original extend()
    delays = [self._get_append_delay(other[0]).m_as("ms")]
    for bl, br in zip(other[:-1], other[1:]):
        if isinstance(bl, Sequence):
            delays.append(bl._get_append_delay(br).m_as("ms"))
        else:
            # Only create a tiny Sequence wrapper when needed (rare)
            tmp = Sequence([bl], self._system_specs)
            delays.append(tmp._get_append_delay(br).m_as("ms"))

    delays = np.round(delays, 4)  # 100 ns rounding
    end_times = Quantity(np.round(np.cumsum(delays), 4), "ms")

    # 2) Collect all new blocks (copied or not), shift them by their per-sequence end_time,
    #    and remember original->new mapping for orientation transfer.
    new_blocks = []
    orig_to_new = {}  # original block id -> new block object (copied or same)
    # We'll also gather orientation updates and the order we added blocks (sequence order)
    omat_grad_updates = {}
    omat_rf_updates = {}

    # iterate with progress bar per-element (keeps tqdm as requested)
    for idx, elem in enumerate(tqdm(other, desc="Extending Sequence", unit="sequence")):
        t_end = end_times[idx]  # Quantity

        # unify element to list-of-blocks
        if isinstance(elem, Sequence):
            src_blocks = elem._blocks
        else:
            src_blocks = [elem]

        # create copies (or reuse) of the source blocks
        if copy:
            copied_blocks = [deepcopy(b) for b in src_blocks]
        else:
            copied_blocks = list(src_blocks)

        # map original -> new (needed for transferring omatrix rf trap-block pointers)
        for orig_b, new_b in zip(src_blocks, copied_blocks):
            orig_to_new[id(orig_b)] = new_b

        # shift each copied block by the computed end_time (same semantics as append())
        for new_b in copied_blocks:
            # append() used block.shift(Quantity(end_time,"ms")) -> keep same call
            new_b.shift(t_end)

        # extend our collector
        new_blocks.extend(copied_blocks)

        # transfer orientation matrices (if present in the source Sequence)
        if isinstance(elem, Sequence):
            self.update_global_omatrix(elem._global_orientation_matrix, warn_assign=True)
            # gradients
            for orig_b in src_blocks:
                if id(orig_b) in elem._orientation_matrices_grad:
                    new_b = orig_to_new[id(orig_b)]
                    omat_grad_updates[id(new_b)] = elem._orientation_matrices_grad[id(orig_b)]

            # rf mappings: stored as (omat, trap_block) in the source Sequence
            for orig_b in src_blocks:
                if id(orig_b) in elem._orientation_matrices_rf:
                    omat, trap_block = elem._orientation_matrices_rf[id(orig_b)]
                    new_b = orig_to_new[id(orig_b)]
                    # trap_block must be mapped to the copied instance (if it exists in this sequence)
                    new_trap = orig_to_new.get(id(trap_block), trap_block)
                    omat_rf_updates[id(new_b)] = (omat, new_trap)

    # 3) Bulk-extend internal blocks list
    self._blocks.extend(new_blocks)

    # 4) Bulk-update name lookups: build a fast "next index" per base-name,
    #    avoiding repeated while-loops that get slow when many duplicates exist.
    #    We must parse existing augmented names like "<name>_<idx>".
    next_index = collections.defaultdict(int)
    for augmented in self._block_lookup.keys():
        parts = augmented.rsplit("_", 1)
        if len(parts) == 2 and parts[1].isdigit():
            base, idxnum = parts[0], int(parts[1])
            next_index[base] = max(next_index[base], idxnum + 1)
        else:
            # no explicit numeric suffix found, treat as base with next index at least 1
            base = augmented
            next_index[base] = max(next_index[base], 1)

    # assign augmented names for all new_blocks in insertion order
    for nb in new_blocks:
        base = nb.name
        i = next_index[base]
        augmented = f"{base}_{i}"
        next_index[base] = i + 1
        self._block_lookup[augmented] = nb
        self._reverse_block_lookup[id(nb)] = augmented

    # 5) Merge orientation-matrix dicts (these are keyed by new block ids)
    self._orientation_matrices_grad.update(omat_grad_updates)
    self._orientation_matrices_rf.update(omat_rf_updates)

    # done
    return self

extend

extend(
    other: Sequence[Union[Sequence, SequenceBaseBlock]],
    copy: bool = True,
) -> None

If both system specifications match, copies all blocks from other shifts them by own tmax and adds the blocks to own collection

Parameters:

Name Type Description Default
other Sequence[Union[Sequence, SequenceBaseBlock]]

ListSequence or block to be added to the sequence

required
copy bool

if true copies the other sequence object

True

Raises:

Type Description
ValueError

If other fails to validate using the system specs of self

Source code in cmrseq/core/_sequence.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
587
def extend(self, other: typing_Sequence[Union['Sequence', SequenceBaseBlock]],
           copy: bool = True) -> None:
    r"""If both system specifications match, copies all blocks from `other` shifts them by own
    tmax and adds the blocks to own collection

    Parameters
    ----------
    other
        ListSequence or block to be added to the sequence
    copy
        if true copies the other sequence object

    Raises
    ------
    ValueError
        If other fails to validate using the system specs of self
    """
    end_times = [self._get_append_delay(other[0]).m_as("ms"), ]
    end_times.extend([bl._get_append_delay(br).m_as("ms") if isinstance(bl, Sequence)
                      else Sequence([bl], self._system_specs)._get_append_delay(br).m_as("ms")
                      for bl, br in zip(other[:-1], other[1:])])
    end_times = np.round(end_times,4) # 100 ns rounding
    end_times = Quantity(np.round(np.cumsum(end_times),4), "ms")
    if isinstance(other, Sequence):
        self.update_global_omatrix(other._global_orientation_matrix, warn_assign=True)
    for idx, other_it in enumerate(tqdm(other, desc="Extending Sequence")):
        self.append(other_it, copy, end_time=end_times[idx])

get_block

get_block(
    block_name: Union[str, Iterable[str]] = None,
    partial_string_match: Union[str, Iterable[str]] = None,
    regular_expression: Union[str, Iterable[str]] = None,
    typedef=None,
    invert_pattern: bool = False,
    sort_by: str = None,
) -> Union[SequenceBaseBlock, List[SequenceBaseBlock]]

Returns reference to the block whose member name matches the specified argument. If no block with given name is present in the sequence, it returns None

.. note::

Checks which keyword argument to use from left to right as specified in the signature.
If multiple are specified uses only the first one.

Parameters:

Name Type Description Default
block_name Union[str, Iterable[str]]

String or iterable of strings exactly matching a set of blocks contained in the sequence

None
partial_string_match Union[str, Iterable[str]]

str or iterable of strings that specify partial string matches. All blocks partially matching at least one are returned.

None
regular_expression Union[str, Iterable[str]]

str or iterable of strings containing regular expressions that are matched against the block-names. All blocks, matching at least one of the given expressions are returned.

None
typedef

type defintion (e.g. cmrseq.bausteine.ADC)

None
invert_pattern bool

if True, all blocks except of the pattern-matched names are returned

False
sort_by str

from [None, start, end] returns the list of blocks sorted according to their start or end time, is ignored if blocks are retrieved by name

None

Returns:

Type Description
SequenceBaseBlock or List of SequenceBaseBlocks depending on the specified argument

Raises:

Type Description
ValueError

If no keyword argument is specified.

Source code in cmrseq/core/_sequence.py
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
def get_block(self, block_name: Union[str, Iterable[str]] = None,
              partial_string_match: Union[str, Iterable[str]] = None,
              regular_expression: Union[str, Iterable[str]] = None,
              typedef=None,
              invert_pattern: bool = False,
              sort_by: str = None) \
        -> Union[SequenceBaseBlock, List[SequenceBaseBlock]]:
    r"""Returns reference to the block whose member `name` matches the specified argument.
    If no block with given name is present in the sequence, it returns None

    .. note::

        Checks which keyword argument to use from left to right as specified in the signature.
        If multiple are specified uses only the first one.

    Parameters
    ----------
    block_name
        String or iterable of strings exactly matching a set of blocks contained in the sequence
    partial_string_match
        str or iterable of strings that specify partial string matches. All blocks partially matching at least one are returned.
    regular_expression
        str or iterable of strings containing regular expressions that are matched against the block-names. All blocks, matching at least one of the given expressions are returned.
    typedef
        type defintion (e.g. cmrseq.bausteine.ADC)
    invert_pattern
        if True, all blocks except of the pattern-matched names are returned
    sort_by
        from [None, start, end] returns the list of blocks sorted according to their start or end time, is ignored if blocks are retrieved by name

    Returns
    -------
    SequenceBaseBlock or List of SequenceBaseBlocks depending on the specified argument

    Raises
    ------
    ValueError
        If no keyword argument is specified.
    """
    if block_name is not None:
        if isinstance(block_name, str):
            return self._block_lookup.get(block_name, None)
        return [self._block_lookup[bn] for bn in block_name]

    elif partial_string_match is not None:
        if isinstance(partial_string_match, str):
            partial_string_match = [partial_string_match, ]
        partial_string_match = "|".join([f"(?:.*{p}.*)" for p in partial_string_match])
        # The condition inside the list-comprehension corresponds to a XOR operation
        # (is_match XOR invert), to determine if matched blocks are included or skipped
        matched_blocks = [block for name, block in self._block_lookup.items()
                          if ((re.match(partial_string_match, name) is not None)
                              ^ invert_pattern)]
    elif regular_expression is not None:
        if isinstance(regular_expression, str):
            regular_expression = [regular_expression, ]
        regular_expression = "|".join([f"(?:{p})" for p in regular_expression])
        # The condition inside the list-comprehension corresponds to a XOR operation
        # (is_match XOR invert), to determine if matched blocks are included or skipped
        matched_blocks = [block for name, block in self._block_lookup.items()
                          if ((re.match(regular_expression, name) is not None)
                              ^ invert_pattern)]

    elif typedef is not None:
        if not isinstance(typedef, (list, tuple)):
            typedef = (typedef,)
        typedef = tuple(typedef)
        matched_blocks = [block for name, block in self._block_lookup.items()
                          if (isinstance(block, typedef) ^ invert_pattern)]
    else:
        raise ValueError("At least one on the keyword arguments must be specified")

    if sort_by is not None:
        match sort_by:
            case "start":
                tmins = np.array([b.tmin.m_as("ms") for b in matched_blocks])
                indices = np.argsort(tmins)
                matched_blocks = [matched_blocks[i] for i in indices]
            case "end":
                tmax = np.array([b.tmax.m_as("ms") for b in matched_blocks])
                indices = np.argsort(tmax)
                matched_blocks = [matched_blocks[i] for i in indices]
            case _:
                raise NotImplementedError(f"Specified sorting ({sort_by}) is not implemented")

    return matched_blocks

register_omatrix

register_omatrix(
    matrix: OMatrix,
    gradients: list[Union[str, Gradient]] = None,
    rf_pulses: list[
        tuple[
            Union[str, RFPulse],
            Union[str, TrapezoidalGradient],
        ]
    ] = None,
)

Updates the mapping of orientation matrix objects for given Gradient blocks and rf_pulses associated with a slice-selection gradients

Parameters:

Name Type Description Default
matrix OMatrix

cmrseq.OMatrix object

required
gradients list[Union[str, Gradient]]

List of unique block names or instances of type Gradient to be registered with the given o-matrix

None
rf_pulses list[tuple[Union[str, RFPulse], Union[str, TrapezoidalGradient]]]

List of tuples containing block-names or instances of (RF-pulse, TrapezoidalGradients), to be registered with the orientation matrix

None
Source code in cmrseq/core/_sequence.py
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
def register_omatrix(self, matrix: 'OMatrix', gradients: list[Union[str, Gradient]] = None,
                     rf_pulses: list[
                         tuple[Union[str, RFPulse], Union[str, TrapezoidalGradient]]] = None):
    r"""Updates the mapping of orientation matrix objects for given Gradient blocks and
    rf_pulses associated with a slice-selection gradients

    Parameters
    ----------
    matrix
        cmrseq.OMatrix object
    gradients
        List of unique block names or instances of type Gradient to be registered with the given o-matrix
    rf_pulses
        List of tuples containing block-names or instances of (RF-pulse, TrapezoidalGradients), to be registered with the orientation matrix
    """
    if gradients is not None:
        for bn in gradients:
            if isinstance(bn, str):
                bn: SequenceBaseBlock = self._block_lookup[bn]
            else:
                assert self._reverse_block_lookup.get(id(bn), None) is not None
            self._orientation_matrices_grad[id(bn)] = matrix

    if rf_pulses is not None:
        for rf_block, trap_block in rf_pulses:
            if isinstance(rf_block, str):
                rf_block: SequenceBaseBlock = self._block_lookup[rf_block]
            else:
                assert self._reverse_block_lookup.get(id(rf_block), None) is not None
            if isinstance(trap_block, str):
                trap_block: SequenceBaseBlock = self._block_lookup[trap_block]
            else:
                assert self._reverse_block_lookup.get(id(trap_block), None) is not None

            assert isinstance(rf_block, RFPulse) and isinstance(trap_block, TrapezoidalGradient)
            self._orientation_matrices_rf[id(rf_block)] = (matrix, trap_block)

update_global_omatrix

update_global_omatrix(
    matrix: OMatrix,
    overwrite: bool = False,
    warn_assign: bool = False,
) -> None

Update the sequence-level orientation matrix.

Global orientation matrices must not contain a positional shift.

Parameters:

Name Type Description Default
matrix OMatrix

Orientation matrix to assign, or None.

required
overwrite bool

If True, replace the existing global orientation matrix.

False
warn_assign bool

If True, warn when a global matrix is inherited from a composed sequence.

False
Source code in cmrseq/core/_sequence.py
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
def update_global_omatrix(self, matrix: 'OMatrix', overwrite: bool = False,
                          warn_assign: bool = False) -> None:
    r"""Update the sequence-level orientation matrix.

    Global orientation matrices must not contain a positional shift.

    Parameters
    ----------
    matrix
        Orientation matrix to assign, or ``None``.
    overwrite
        If ``True``, replace the existing global orientation matrix.
    warn_assign
        If ``True``, warn when a global matrix is inherited from a composed sequence.
    """
    if matrix is not None:
        if not np.isclose(matrix._position.m_as("m"), 0.):
            raise ValueError("Global orientation matrices must NOT have a positional shift.")

    if overwrite:
        self._global_orientation_matrix = matrix
        return

    # Update global orientation
    if matrix is not None and self._global_orientation_matrix is not None:
        # Two defined global orientation matrices, not valid unless they are the same
        if not np.array_equal(matrix._tmatrix, self._global_orientation_matrix._tmatrix):
            raise ValueError("Global orientation matrix already set, cannot be changed.")
        else:
            # Arrays are the same, so no need to update
            return

    if matrix is not None:
        # If only the other sequence has a global orientation matrix, use it
        self._global_orientation_matrix = matrix
        if warn_assign:
            warn("Global orientation matrix set according to other sequence")
    elif self._global_orientation_matrix is not None:
        if warn_assign:
            warn("Global orientation matrix set according to original sequence")

    return

shift_in_time

shift_in_time(shift: Quantity) -> None

Shifts all blocks contained in the sequence object by the specified time

Parameters:

Name Type Description Default
shift Quantity

Quantity of dimesion time

required
Source code in cmrseq/core/_sequence.py
864
865
866
867
868
869
870
871
872
873
def shift_in_time(self, shift: Quantity) -> None:
    r"""Shifts all blocks contained in the sequence object by the specified time

    Parameters
    ----------
    shift
        Quantity of dimesion time
    """
    for block in self._blocks:
        block.shift(time_shift=shift)

time_reverse

time_reverse() -> None

Reverses the sequence in time

Source code in cmrseq/core/_sequence.py
875
876
877
878
879
880
881
def time_reverse(self) -> None:
    r"""Reverses the sequence in time
    """
    # flip about end of sequence
    time_flip_point = self.duration
    for block in self._blocks:
        block.flip(time_flip_point)

copy

copy() -> Sequence
Source code in cmrseq/core/_sequence.py
1001
1002
def copy(self) -> 'Sequence':
    return deepcopy(self)

partial_sequence

partial_sequence(
    copy_blocks: bool,
    partial_string_match: Union[str, Iterable[str]] = None,
    regular_expression: Union[str, Iterable[str]] = None,
    invert_pattern: bool = False,
    **kwargs,
) -> Sequence

Returns a cmrseq.Sequence object containing references or deep-copies of all blocks matched either with partial-string-match or regular expressions specified as keyword argument.

Parameters:

Name Type Description Default
copy_blocks bool

if True, creates deep-copies of matched blocks.

required
partial_string_match Union[str, Iterable[str]]

str or iterable of strings that specify partial string matches. All blocks partially matching at least one are returned.

None
regular_expression Union[str, Iterable[str]]

str or iterable of strings containing regular expressions that are matched against the block-names. All blocks, matching at least one of the given expressions are returned.

None
invert_pattern bool

if True, all blocks except of the pattern-matched names are returned

False

Returns:

Type Description
Sequence object
Source code in cmrseq/core/_sequence.py
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
def partial_sequence(self, copy_blocks: bool,
                     partial_string_match: Union[str, Iterable[str]] = None,
                     regular_expression: Union[str, Iterable[str]] = None,
                     invert_pattern: bool = False, **kwargs) -> 'Sequence':
    r"""Returns a cmrseq.Sequence object containing references or deep-copies of all blocks
    matched either with partial-string-match or regular expressions specified as keyword argument.

    Parameters
    ----------
    copy_blocks
        if True, creates deep-copies of matched blocks.
    partial_string_match
        str or iterable of strings that specify partial string matches. All blocks partially matching at least one are returned.
    regular_expression
        str or iterable of strings containing regular expressions that are matched against the block-names. All blocks, matching at least one of the given expressions are returned.
    invert_pattern
        if True, all blocks except of the pattern-matched names are returned

    Returns
    -------
    Sequence object

    """
    matched_blocks = self.get_block(block_name=None, partial_string_match=partial_string_match,
                                    regular_expression=regular_expression,
                                    invert_pattern=invert_pattern)
    return Sequence(building_blocks=matched_blocks, system_specs=self._system_specs,
                    copy=copy_blocks, **kwargs)

gradients_to_grid

gradients_to_grid(
    start_time: Quantity = None,
) -> Tuple[np.ndarray, np.ndarray]

Grids gradient definitions of all blocks contained in the sequence, on a joint time grid from the minimal to maximal value in single time-points definitions with a step-length defined in system_specs.grad_raster_time. If gradients occur at the same time on the same channel, they are added.

Returns:

Type Description
(np.ndarray, np.ndarray) of shape (t, ) containing the time-grid and (3 [gx, gy, gz], t) containing the waveform definition in ms and mT/m returns (None, None) if no gradients are contained in the sequence
Source code in cmrseq/core/_sequence.py
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
def gradients_to_grid(self, start_time: Quantity = None) -> Tuple[np.ndarray, np.ndarray]:
    r"""Grids gradient definitions of all blocks contained in the sequence, on a joint time grid
    from the minimal to maximal value in single time-points definitions with a step-length
    defined in system_specs.grad_raster_time.
    If gradients occur at the same time on the same channel, they are added.

    Returns
    -------
    (np.ndarray, np.ndarray) of shape (t, ) containing the time-grid and (3 [gx, gy, gz], t) containing the waveform definition in ms and mT/m returns (None, None) if no gradients are contained in the sequence
    """

    gradients = self.gradients
    if not gradients:
        return None, None

    time_points = [g[0].m_as("ms") for g in gradients]
    wave_forms = [g[1].m_as("mT/m") for g in gradients]

    if start_time is None:
        start_time = self.start_time.m_as("ms")
    end_time = self.end_time.m_as("ms")

    dt = self._system_specs.grad_raster_time.m_as("ms")
    t_grid = np.arange(start_time, end_time + dt, dt)
    wf_grid = np.zeros((3, t_grid.shape[0]))

    for t, wf, bidx in zip(time_points, wave_forms, range(len(self._blocks))):
        t = np.array(t)
        tidx = np.around((t - start_time) / dt)
        if not np.allclose((t - start_time) / dt, tidx, rtol=1e-6):
            warn(
                f"Sequence.gradient_to_grid: Gradient definition of block {bidx} is not on gradient raster")
        start, end = int(tidx[0]), int(tidx[-1])
        interpolated_wfx = np.interp(t_grid[start:end], t, wf[0])
        interpolated_wfy = np.interp(t_grid[start:end], t, wf[1])
        interpolated_wfz = np.interp(t_grid[start:end], t, wf[2])
        wf_grid[:, start:end] += np.stack([interpolated_wfx,
                                           interpolated_wfy,
                                           interpolated_wfz])
    return t_grid, wf_grid

combined_gradients

combined_gradients() -> Tuple[np.ndarray, np.ndarray]

Combines the gradient definitions of all blocks contained in the sequence, into a joint single definition. The joint time-points are defined by the set of unique time-points of all combined blocks. If gradients occur at the same time on the same channel, they are added.

Returns:

Type Description
(np.ndarray, np.ndarray) of shape (t, ) containing the time-points and (3 [gx, gy, gz], t) containing the waveform definition in ms and mT/m returns (None, None) if no gradients are contained in the sequence
Source code in cmrseq/core/_sequence.py
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
def combined_gradients(self) -> Tuple[np.ndarray, np.ndarray]:
    r"""Combines the gradient definitions of all blocks contained in the sequence,
    into a joint single definition. The joint time-points are defined by the set of unique
    time-points of all combined blocks.
    If gradients occur at the same time on the same channel, they are added.

    Returns
    -------
    (np.ndarray, np.ndarray) of shape (t, ) containing the time-points and (3 [gx, gy, gz], t) containing the waveform definition in ms and mT/m returns (None, None) if no gradients are contained in the sequence
    """

    gradients = self.gradients
    if not gradients:
        return None, None

    time_points = [g[0].m_as("ms") for g in gradients]
    wave_forms = [g[1].m_as("mT/m") for g in gradients]

    t_grid = np.sort(np.unique(np.around(np.concatenate(time_points, axis=0), decimals=4)))
    t_grid = np.round(t_grid, decimals=4) # round to 4 decimals (100 ns)
    wf_grid = np.zeros((3, t_grid.shape[0]))

    t_idx = np.searchsorted(t_grid, np.concatenate(time_points)).tolist()

    for t, wf in zip(time_points, wave_forms):
        t = np.round(t, decimals=4) # round to 4 decimals (100 ns)
        t_idx_tmp = t_idx[:len(t)]
        del t_idx[:len(t)]
        start, end = int(t_idx_tmp[0]), int(t_idx_tmp[-1])
        interpolated_wfx = np.interp(t_grid[start:end], t, wf[0])
        interpolated_wfy = np.interp(t_grid[start:end], t, wf[1])
        interpolated_wfz = np.interp(t_grid[start:end], t, wf[2])
        wf_grid[:, start:end] += np.stack([interpolated_wfx,
                                           interpolated_wfy,
                                           interpolated_wfz])

    return t_grid, wf_grid

combined_rf

combined_rf() -> Tuple[np.ndarray, np.ndarray]

Combines the rf-definitions of all blocks contained in the sequence, into a joint single definition. The joint time-points are defined by the set of unique time-points of all combined blocks. If rf occur at the same time they are added.

Returns:

Type Description
(np.ndarray, np.ndarray) of shape (t, ) containing the time-points and (t, ) containing the complex RF-waveform definition in ms and uT
Source code in cmrseq/core/_sequence.py
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
def combined_rf(self) -> Tuple[np.ndarray, np.ndarray]:
    r"""Combines the rf-definitions of all blocks contained in the sequence,
    into a joint single definition. The joint time-points are defined by the set of unique
    time-points of all combined blocks.
    If rf occur at the same time they are added.

    Returns
    -------
    (np.ndarray, np.ndarray) of shape (t, ) containing the time-points and (t, ) containing the complex RF-waveform definition in ms and uT

    """
    rf_waveforms = self.rf
    if not rf_waveforms:
        return None, None

    time_points = [r[0].m_as("ms") for r in rf_waveforms]
    wave_forms = [np.stack([r[1].m_as("uT").real, r[1].m_as("uT").imag]) for r in rf_waveforms]

    t_grid = np.sort(np.unique(np.concatenate(time_points, axis=0)))
    wf_grid = np.zeros(t_grid.shape[0], dtype=np.complex128)
    t_idx = np.searchsorted(t_grid, np.concatenate(time_points)).tolist()

    for t, wf in zip(time_points, wave_forms):
        t_idx_tmp = t_idx[:len(t)]
        del t_idx[:len(t)]
        start, end = int(t_idx_tmp[0]), int(t_idx_tmp[-1])
        interpolated_wfreal = np.interp(t_grid[start:end], t, wf[0])
        interpolated_wfimag = np.interp(t_grid[start:end], t, wf[1])
        wf_grid[start:end] += interpolated_wfreal + 1j * interpolated_wfimag
    return t_grid, wf_grid

rf_to_grid

rf_to_grid() -> Tuple[np.ndarray, np.ndarray]

Grids RF-definitions of all blocks contained in the sequence, on a joint time grid from the minimal to maximal value in single time-points definitions with a step-length defined in system_specs.rf_raster_time.

If RF-pulses occur at the same time on the same channel, they are added.

Returns:

Type Description
(np.ndarray, np.ndarray) of shape (1, t) containing the time-grid and (1, t) containing the complex RF amplitude
Source code in cmrseq/core/_sequence.py
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
def rf_to_grid(self) -> Tuple[np.ndarray, np.ndarray]:
    r"""Grids RF-definitions of all blocks contained in the sequence, on a joint time grid
    from the minimal to maximal value in single time-points definitions with a step-length
    defined in system_specs.rf_raster_time.

    If RF-pulses occur at the same time on the same channel, they are added.

    Returns
    -------
    (np.ndarray, np.ndarray) of shape (1, t) containing the time-grid and (1, t) containing the complex RF amplitude
    """
    rf = self.rf
    if not rf:
        return None, None

    time_points = [r[0].m_as("ms") for r in rf]
    wave_forms = [r[1].m_as("mT") for r in rf]

    start_time = self.start_time.m_as("ms")
    end_time = self.end_time.m_as("ms")

    dt = self._system_specs.rf_raster_time.m_as("ms")
    t_grid = np.arange(start_time, end_time + dt, dt)
    rf_grid = np.zeros((t_grid.shape[0]), dtype=np.complex64)

    for t, complex_alpha, bidx in zip(time_points, wave_forms, range(len(rf))):
        t = np.array(t)
        tidx = np.around((t - start_time) / dt)
        if not np.allclose((t - start_time) / dt, tidx, atol=1e-6):
            warn(f"Sequence.rf_to_grid: RF definition of block {bidx} is not on RF raster")
        start, end = int(tidx[0]), int(tidx[-1])
        rf_grid[start:end] += np.interp(t_grid[start:end], t, complex_alpha)
    return t_grid, rf_grid

combined_adc

combined_adc() -> Tuple[np.ndarray, np.ndarray]

Combines all ADC-type blocks contained in the sequence, on a joint time grid.

Note: The binary event channel of the returned array is technically not needed but adheres to the signature of dense gridding

Returns:

Type Description
Array of shape (t, ) containing the time-points Array of shape (t, 2) containing binary event and phase
Source code in cmrseq/core/_sequence.py
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
def combined_adc(self) -> Tuple[np.ndarray, np.ndarray]:
    r"""Combines all ADC-type blocks contained in the sequence, on a joint time grid.

    **Note**: The binary event channel of the returned array is technically not needed but adheres to the signature
                of dense gridding

    Returns
    -------
    Array of shape (t, ) containing the time-points Array of shape (t, 2) containing binary event and phase
    """
    # First grid all individual blocks on adc_raster times
    adc_blocks = [block for block in self._blocks if isinstance(block, ADC)]
    t_combined = []
    adc_def_combined = []
    for block in adc_blocks:
        t_ = block.adc_timing.m_as("ms")
        on = np.ones_like(t_)
        phase = block.adc_phase
        t_combined.append(t_)
        adc_def_combined.append(np.stack([on, phase], axis=1))

    # insert into common array
    t_combined = np.concatenate(t_combined, axis=0)
    adc_def = np.concatenate(adc_def_combined, axis=0)
    sorting_indices = np.argsort(t_combined)
    return t_combined[sorting_indices], adc_def[sorting_indices, :]

adc_to_grid

adc_to_grid(
    force_raster: bool = False,
) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]

Grids the ADC-Events of all blocks contained in the sequence as boolean 1D mask along with the resulting time-grid. Additionally, the start and end points of the all adc-blocks are returned. The definition of start/end differ for force_gradient_raster True/False

Boolean mask explanation:

- *force_raster* == `False`
                    events that are not defined on the grid, are inserted into the
                    time-raster resulting in a non-uniform time definition.
                    The boolean values of the newly inserted points are set to 1.
- *force_raster* == `True`
                    for events that are not defined on the grid the boolean values
                    of the interval borders on gradient raster time are set to 1.
                    For events that are already on the grid, the corresponding single
                    index is set 1.

Start/End - definition:

- *force_raster* == `False`:
    the exact time of first/last event per block is returned.
- *force_raster* == `True`:
    The returned start/end times correspond to the beginning and end of the plateau
    of a trapezoidal gradient played out during the adc-events (addition of dwell-time).

Parameters:

Name Type Description Default
force_raster bool

bool defaults to True

False

Returns:

Type Description
Tuple(np.array, np.array, np.array) (t, ) containing time-values (t, ) containing values of 0 or 1, indicating where the adc is active (t, ) containing the adc_phase in radians (#adc_blocks, 2) where (:, 0) contains the indices of the start time of the adc-block and (:, 1) the end time correspondingly.
Source code in cmrseq/core/_sequence.py
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
def adc_to_grid(self, force_raster: bool = False) \
        -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
    r"""Grids the ADC-Events of all blocks contained in the sequence as boolean 1D mask along
    with the resulting time-grid. Additionally, the start and end points of the all adc-blocks
    are returned. The definition of start/end differ for force_gradient_raster True/False

    **Boolean mask explanation**:

        - *force_raster* == `False`
                            events that are not defined on the grid, are inserted into the
                            time-raster resulting in a non-uniform time definition.
                            The boolean values of the newly inserted points are set to 1.
        - *force_raster* == `True`
                            for events that are not defined on the grid the boolean values
                            of the interval borders on gradient raster time are set to 1.
                            For events that are already on the grid, the corresponding single
                            index is set 1.

    **Start/End - definition**:

        - *force_raster* == `False`:
            the exact time of first/last event per block is returned.
        - *force_raster* == `True`:
            The returned start/end times correspond to the beginning and end of the plateau
            of a trapezoidal gradient played out during the adc-events (addition of dwell-time).

    Parameters
    ----------
    force_raster
        bool defaults to True

    Returns
    -------
    Tuple(np.array, np.array, np.array) (t, ) containing time-values (t, ) containing values of 0 or 1, indicating where the adc is active (t, ) containing the adc_phase in radians (#adc_blocks, 2) where (:, 0) contains the indices of the start time of the adc-block and (:, 1) the end time correspondingly.

    """
    # First grid all individual blocks on adc_raster times
    adc_blocks = [block for block in self._blocks if isinstance(block, ADC)]
    gridded_adcs = []
    for block in adc_blocks:
        gridded_adcs.append(self._grid_single_adc_block(force_raster, block))

    # Secondly Insert the gridded adc-timings into the gradient raster
    gradient_raster = self.gradients_to_grid()[0]

    # Make sure that all gridded adc times are within the boundaries of gradient_raster because
    # Otherwise the insertion logic below will fail
    latest_adc_raster_time = np.max([np.max(t[0]) for t in gridded_adcs])
    first_adc_raster_time = np.min([np.min(t[0]) for t in gridded_adcs])
    if gradient_raster is None:
        gradient_raster = np.arange(first_adc_raster_time, latest_adc_raster_time,
                                    self._system_specs.grad_raster_time.m_as("ms"))
    if gradient_raster[-1] <= latest_adc_raster_time:
        gradient_raster = np.append(gradient_raster, latest_adc_raster_time)

    # Concatenate gridded adcs, sort the adcs according to their initial value of t
    gridded_adcs.sort(key=lambda v: v[0][0])
    adc_raster_time = np.around(np.concatenate([v[0] for v in gridded_adcs]), decimals=6)
    adc_on = np.concatenate([v[1] for v in gridded_adcs])
    adc_phase = np.concatenate([v[2] for v in gridded_adcs])
    if not np.all(np.diff(adc_raster_time) >= 0):
        raise ValueError("Currently gridding sequences with ADCs is only possible for "
                         "non-overlapping ADC-blocks")

    # Find positions to insert
    gradient_raster = np.around(gradient_raster, decimals=6)
    insertion_idx = np.searchsorted(gradient_raster, adc_raster_time, side="left")

    # Insert points into time raster and allocate the adc_on/phase arrays
    # while ignore points that are already on the gradient raster
    gradient_raster = np.insert(gradient_raster, insertion_idx, adc_raster_time)
    gradient_raster = np.unique(np.around(gradient_raster, decimals=6))
    adc_activation_raster = np.zeros_like(gradient_raster)
    adc_phase_raster = np.zeros_like(gradient_raster)

    # Recalculate indices to set values for phase and activation and set values accordingly
    setting_idx = np.searchsorted(gradient_raster, adc_raster_time, side="left")
    adc_activation_raster[setting_idx] = adc_on
    adc_phase_raster[np.where(adc_activation_raster)] = adc_phase
    start_end_per_event = []
    for time_raster, _, _ in gridded_adcs:
        s_e = np.searchsorted(gradient_raster, np.stack([time_raster[0], time_raster[-1]]))
        start_end_per_event.append(s_e)
    start_end_per_event = np.stack(start_end_per_event)

    return gradient_raster, adc_activation_raster, adc_phase_raster, start_end_per_event

calculate_kspace

calculate_kspace() -> Tuple[
    np.ndarray, np.ndarray, np.ndarray
]

Evaluates the k-space trajectory of the sequence.

Note: All RF-pulses with a smaller flip-angle other than 180° are assumed to be excitation pulses. 180° - Refocusing pulses result in a complex conjugation of the trajectory. Consecutive excitation pulses are handled by starting from k-space center again.

Returns:

Type Description
Tuple of arrays containing:
  • k-space trajectory on gradient rasters (-1, 3) in 1/m
  • k-space points at adc events (-1, 3) in 1/m
  • time at adc events (-1 ) in ms
Source code in cmrseq/core/_sequence.py
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
def calculate_kspace(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
    r"""Evaluates the k-space trajectory of the sequence.

    **Note**: All RF-pulses with a smaller flip-angle other than 180° are assumed to be
    excitation pulses. 180° - Refocusing pulses result in a complex conjugation of the
    trajectory. Consecutive excitation pulses are handled by starting from k-space center again.

    Returns
    -------
    Tuple of arrays containing:

            - k-space trajectory on gradient rasters (-1, 3) in 1/m
            - k-space points at adc events (-1, 3) in 1/m
            - time at adc events (-1 ) in ms
    """
    # Subdivide gradient waveforms in periods between rf events for integration
    rf_events = [block.rf_events for block in self._blocks if isinstance(block, RFPulse)]

    if rf_events:
        rf_factors = []
        for (t, fa) in rf_events:
            factor = -1. if np.isclose(fa, np.pi, rtol=np.pi / 50) else 0.
            rf_factors.append([t.m_as("ms"), factor])
        rf_factors = np.stack(rf_factors)
        rf_factors = rf_factors[np.argsort(rf_factors[:, 0])]
    else:
        rf_factors = None

    t_grid_global, gradient_waveform = self.gradients_to_grid()
    k_of_t = np.zeros([3, gradient_waveform.shape[1]])

    if rf_factors is not None:
        rf_event_tidx = np.searchsorted(np.round(t_grid_global,decimals=6), np.round(rf_factors[:, 0],decimals=6))
        rf_event_tidx = np.concatenate([rf_event_tidx, [-1, ]])
        for idx, factor in enumerate(rf_factors[:, 1]):
            start, end = rf_event_tidx[idx:idx + 2]
            dt = np.diff(t_grid_global[start:end]).reshape(1, -1)
            wf = gradient_waveform[:, start:end]
            delta_k = np.cumsum(dt * (wf[:, 1:] + wf[:, 0:-1]) / 2, axis=1)
            delta_k *= self._system_specs.gamma.m_as("MHz/T")  # 1/mT/ms
            k_of_t[:, start + 1:end] = factor * k_of_t[:, start - 1:start] + delta_k
    else:
        k_of_t[:, 1:] = np.cumsum(np.diff(t_grid_global).reshape(1, -1) *
                                  (gradient_waveform[:, 1:] + gradient_waveform[:, :-1]) / 2,
                                  axis=1) * self._system_specs.gamma.m_as("MHz/T")

    # Evaluate k-space position at adc-events
    all_adc_timings = [block.adc_timing.m_as("ms") for block in self._blocks
                       if isinstance(block, ADC)]
    if all_adc_timings:
        t_adc = np.around(np.concatenate(all_adc_timings, axis=0), decimals=6)
        k_adc = np.stack([np.interp(t_adc, t_grid_global, k) for k in k_of_t])
    else:
        k_adc = None
        t_adc = None

    return k_of_t, k_adc, t_adc

calculate_moment

calculate_moment(
    moment: int = 0,
    center_time: Quantity = Quantity(0.0, "ms"),
    end_time: Quantity = None,
    start_time: Quantity = None,
) -> Quantity

Calculates gradient moments about a given center point

Parameters:

Name Type Description Default
moment int

int of desired moment number

0
center_time Quantity

Quantity of center time to calculate moment about, defaults to t=0

Quantity(0.0, 'ms')
end_time Quantity

Time to calculate moment up to, default is end of sequence

None
start_time Quantity

Time to calculate moment from, default is start of sequence

None

Returns:

Type Description
Quantity[Mx, My, Mz]
Source code in cmrseq/core/_sequence.py
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
def calculate_moment(self, moment: int = 0, center_time: Quantity = Quantity(0., "ms"),
                     end_time: Quantity = None, start_time: Quantity = None) -> Quantity:
    r"""Calculates gradient moments about a given center point

    Parameters
    ----------
    moment
        int of desired moment number
    center_time
        Quantity of center time to calculate moment about, defaults to t=0
    end_time
        Time to calculate moment up to, default is end of sequence
    start_time
        Time to calculate moment from, default is start of sequence

    Returns
    -------
    Quantity [Mx, My, Mz]
    """

    # Get all gradient break points
    t, g = self.combined_gradients()

    if t is None or g is None:
        return Quantity([0., 0., 0.], 'mT/m*ms**' + str(moment + 1))

    if start_time is None:
        tstart = t[0]
    else:
        tstart = start_time.m_as("ms")

    if end_time is None:
        tend = t[-1]
    else:
        tend = end_time.m_as("ms")

    tcur = t
    gcur = g

    # If the start and end time are outside the range of the gradient definition
    # the gradient definition is cut to the range of the start and end time
    ind_end = 0
    if tend<t[-1]:
        ind_end = np.argwhere(t>tend)[0][0]
        tcur = tcur[:ind_end]
        gcur = gcur[:,:ind_end]

    ind_start = 0
    if tstart>t[0]:
        ind_start = np.argwhere(t>=tstart)[0][0]
        tcur = tcur[ind_start:]
        gcur = gcur[:,ind_start:]

    # Interpolate the gradient definition to the start and end time
    if tstart<t[ind_start]:
        gintstart = g[:,ind_start-1] + (g[:,ind_start]-g[:,ind_start-1])/(t[ind_start]-t[ind_start-1])*(tstart-t[ind_start-1])
        gcur = np.insert(gcur,0,gintstart,axis=1)
        tcur = np.insert(tcur,0,tstart)

    if tend>t[ind_end-1]:
        gintend = g[:,ind_end-1] +(g[:,ind_end]-g[:,ind_end-1])/(t[ind_end]-t[ind_end-1])*(tend-t[ind_end-1])
        gcur = np.append(gcur,gintend[:,np.newaxis],axis=1)
        tcur = np.append(tcur,tend)

    # Set center time
    tcur = tcur - center_time.m_as("ms")

    # Back to the original units
    t1 = Quantity(tcur[:-1],'ms')
    t2 = Quantity(tcur[1:],'ms')
    G1 = Quantity(gcur[:,:-1],'mT/m')
    G2 = Quantity(gcur[:,1:],'mT/m')

    # Use solution for arbitrary linear section, to nth order
    result = (G2-G1)*(t1**(moment+2) - t2**(moment+2))/((moment+2)*(t1-t2)) + (G1*t2-G2*t1)*(t1**(moment+1) - t2**(moment+1))/((moment+1)*(t1-t2))
    result = result.sum(axis=1)

    return result

SystemSpec

SystemSpec(
    gamma: Quantity = Quantity(42.576, "MHz/T"),
    grad_raster_time: Quantity = Quantity(0.01, "ms"),
    max_grad: Quantity = Quantity(40, "mT/m"),
    max_slew: Quantity = Quantity(120, "mT/m/ms"),
    rf_peak_power: Quantity = Quantity(30, "uT"),
    rf_raster_time: Quantity = Quantity(0.01, "ms"),
    rf_dead_time: Quantity = Quantity(0.0, "ms"),
    rf_ringdown_time: Quantity = Quantity(0.0, "ms"),
    rf_lead_time: Quantity = Quantity(0.0, "ms"),
    adc_raster_time: Quantity = Quantity(100, "ns"),
    adc_dead_time: Quantity = Quantity(0.0, "ms"),
    b0: Quantity = Quantity(1.5, "T"),
    enable_simulatenous_trasmit_receive: bool = False,
)

Bundles the system limit specifications, meant to be passed as object for creating sequences and building blocks.

In addition to store all relevant system specifications this class implements the methods to calculate quantities that depend on these limits (e.g. get_shortest_rise_time).

Parameters:

Name Type Description Default
gamma Quantity

Gyromagnetic Ratio of the target nucleus with dimensions equivalent to[MHz / T]

Quantity(42.576, 'MHz/T')
grad_raster_time Quantity

Raster time for gradient definitions with dimension [Time]

Quantity(0.01, 'ms')
max_grad Quantity

Maximal allowed gradient strength for combined gradient channels in dimension equivalent to [mT/m]

Quantity(40, 'mT/m')
max_slew Quantity

Maximal allow gradient slew-rate for combined gradient channels in dimensions equivalent to [mT/m/ms]

Quantity(120, 'mT/m/ms')
rf_peak_power Quantity

Maximal allowed peak rf power defined as B1 field strength with dimensions equivalent to [uT]

Quantity(30, 'uT')
rf_raster_time Quantity

Raster time for radio-frequency waveform definitions with dimension [Time]

Quantity(0.01, 'ms')
rf_dead_time Quantity

Minimum time between consecutive RF-pulses, due to switching delays in the transmit chain.

Quantity(0.0, 'ms')
rf_ringdown_time Quantity

Defines the minimum delay between a RF-pulse and and acquisition block. Corresponds to the time scale of self induced currents in the transmit coil, which could result in receive chain damages and sampling distortion.

Quantity(0.0, 'ms')
rf_lead_time Quantity

Defines the minimum delay between an acquisition block and a subsequent RF-pulse. This corresponds to the delay caused by switching from receive to transmit.

Quantity(0.0, 'ms')
adc_raster_time Quantity

Raster time for signal sampling definitions with dimension [Time]

Quantity(100, 'ns')
adc_dead_time Quantity

Minimum time between consecutive Sampling (ADC) blocks, due to switching delays in the receive chain.

Quantity(0.0, 'ms')
b0 Quantity

Static field strength in dimension of [T]

Quantity(1.5, 'T')
enable_simulatenous_trasmit_receive bool

System flag for sequence validation. If true, RF and ADC blocks are allowed to be occur simultaneously (ignoring) rf_ringdown_time in validation.

False
Source code in cmrseq/core/_system.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
109
110
111
112
113
114
115
116
def __init__(self,
             gamma: Quantity = Quantity(42.576, "MHz/T"),
             grad_raster_time: Quantity = Quantity(1e-2, "ms"),
             max_grad: Quantity = Quantity(40, "mT/m"),
             max_slew: Quantity = Quantity(120, "mT/m/ms"),
             rf_peak_power: Quantity = Quantity(30, "uT"),
             rf_raster_time: Quantity = Quantity(1e-2, "ms"),
             rf_dead_time: Quantity = Quantity(0., "ms"),
             rf_ringdown_time: Quantity = Quantity(0., "ms"),
             rf_lead_time: Quantity = Quantity(0., "ms"),
             adc_raster_time: Quantity = Quantity(100, "ns"),
             adc_dead_time: Quantity = Quantity(0., "ms"),
             b0: Quantity = Quantity(1.5, "T"),
             enable_simulatenous_trasmit_receive: bool = False):

    if max_grad.to_base_units().units == Quantity(1., "1/m/s").units:
        max_grad = (max_grad * gamma).to("mT/m")

    if max_slew.to_base_units().units == Quantity(1., "1/m/s**2").units:
        max_slew = (max_slew * gamma).to("mT/m/ms")

    self.rf_peak_power = rf_peak_power.to("uT")
    self.rf_dead_time = rf_dead_time.to("ms")
    self.rf_ringdown_time = rf_ringdown_time.to("ms")
    self.rf_lead_time = rf_lead_time.to("ms")
    self.adc_dead_time = adc_dead_time.to("ms")
    self.rf_raster_time = rf_raster_time.to("ms")
    self.grad_raster_time = grad_raster_time.to("ms")
    self.adc_raster_time = adc_raster_time.to("ms")
    self.gamma = gamma.to("MHz/T")
    self.gamma_rad = gamma.to("rad/T/s") * 2 * np.pi

    self.max_grad = max_grad
    self.max_slew = max_slew

    self.b0 = b0.to("T")
    self.enable_simulatenous_trasmit_receive = enable_simulatenous_trasmit_receive
    self._validate()

max_grad instance-attribute

max_grad: Quantity = max_grad

max_slew instance-attribute

max_slew: Quantity = max_slew

rf_peak_power instance-attribute

rf_peak_power: Quantity = to('uT')

rf_dead_time instance-attribute

rf_dead_time: Quantity = to('ms')

rf_ringdown_time instance-attribute

rf_ringdown_time: Quantity = to('ms')

rf_lead_time instance-attribute

rf_lead_time: Quantity = to('ms')

adc_dead_time instance-attribute

adc_dead_time: Quantity = to('ms')

rf_raster_time instance-attribute

rf_raster_time: Quantity = to('ms')

grad_raster_time instance-attribute

grad_raster_time: Quantity = to('ms')

adc_raster_time instance-attribute

adc_raster_time: Quantity = to('ms')

gamma instance-attribute

gamma: Quantity = to('MHz/T')

gamma_rad instance-attribute

gamma_rad: Quantity = to('rad/T/s') * 2 * pi

enable_simulatenous_trasmit_receive instance-attribute

enable_simulatenous_trasmit_receive: bool = (
    enable_simulatenous_trasmit_receive
)

b0 instance-attribute

b0 = to('T')

minmax_risetime property

minmax_risetime

Returns the minimum rise time to reach the maximum gradient amplitude

get_shortest_rise_time

get_shortest_rise_time(
    delta_amplitude: Quantity,
) -> Quantity

Calculates the shortest ramp duration for the specified amplitude difference.

Parameters:

Name Type Description Default
delta_amplitude Quantity

Quantity[mT/m]

required

Returns:

Type Description
delta t Quantity[ms] which is guaranteed to be a multiple of grad_raster_time
Source code in cmrseq/core/_system.py
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
def get_shortest_rise_time(self, delta_amplitude: Quantity) -> Quantity:
    r"""Calculates the shortest ramp duration for the specified amplitude difference.

    Parameters
    ----------
    delta_amplitude
        Quantity[mT/m]

    Returns
    -------
    delta t Quantity[ms] which is guaranteed to be a multiple of grad_raster_time
    """
    delta_amplitude = np.abs(delta_amplitude)
    shortest_ramp_dur = np.around((delta_amplitude / self.max_slew).to("ms"), decimals=6)
    return self.time_to_raster(shortest_ramp_dur, raster="grad")

get_shortest_gradient

get_shortest_gradient(
    area: Quantity,
) -> Tuple[Quantity, Quantity, Quantity]

Calculates the shortest gradient of a given area, obeying system limits

Parameters:

Name Type Description Default
area Quantity

Quantity[mT/m*s]

required

Returns:

Type Description
Tuple(amplitude, rise time, flat time)
Source code in cmrseq/core/_system.py
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
def get_shortest_gradient(self, area: Quantity) -> Tuple[Quantity, Quantity, Quantity]:
    r"""Calculates the shortest gradient of a given area, obeying system limits

    Parameters
    ----------
    area
        Quantity[mT/m*s]

    Returns
    -------
    Tuple(amplitude, rise time, flat time)
    """

    if not area.check("T/m*s"):
        raise ValueError("Unit of gradient area incorrect, must be mT/m*s or equivalent")

    fastest_ramp = self.get_shortest_rise_time(self.max_grad)

    if area == 0:
        return Quantity(0, 'mT/m'), Quantity(0, 'ms'), Quantity(0, 'ms')

    if fastest_ramp*self.max_grad > area:
        # Triangular
        ramp_time = np.sqrt(area / self.max_slew)
        ramp_time = self.time_to_raster(ramp_time, raster="grad")
        amplitude = area / ramp_time
        flat_time = Quantity(0., 'ms')
    else:
        # Trapezoid
        ramp_time = fastest_ramp
        flat_time = area / self.max_grad - fastest_ramp
        flat_time = self.time_to_raster(flat_time, raster="grad")
        amplitude = area / (fastest_ramp + flat_time)
    return amplitude, ramp_time, flat_time

get_fastest_kspace_traverse

get_fastest_kspace_traverse(
    k_space_vector: Quantity,
) -> Tuple[Quantity, Quantity, Quantity]

Computes the shortest gradient, resulting in a k-space traverse along the specified vector.

.. note:

This assumes the isotropic gradient limits, hence the norm of the gradient
vector adhering to the system limits.

Parameters:

Name Type Description Default
k_space_vector Quantity

(3, ) for X, Y, Z

required

Returns:

Type Description
Amplitude, ramp- and flat-duration of the resulting gradient pulse
Source code in cmrseq/core/_system.py
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
def get_fastest_kspace_traverse(self, k_space_vector: Quantity)\
        -> Tuple[Quantity, Quantity, Quantity]:
    r"""Computes the shortest gradient, resulting in a k-space traverse along the
    specified vector.

    .. note:

        This assumes the isotropic gradient limits, hence the norm of the gradient
        vector adhering to the system limits.

    Parameters
    ----------
    k_space_vector
        (3, ) for X, Y, Z

    Returns
    -------
    Amplitude, ramp- and flat-duration of the resulting gradient pulse
    """
    total_kspace_traverse = Quantity(np.linalg.norm(k_space_vector.m_as("1/m")), "1/m")
    combined_gradient_area = total_kspace_traverse / self.gamma.to("1/mT/ms")
    return self.get_shortest_gradient(combined_gradient_area)

time_to_raster

time_to_raster(
    time: Quantity, raster: str = "grad"
) -> Quantity

Calculates the time projected onto the either gradient or rf raster.

Parameters:

Name Type Description Default
time Quantity

Quantity[s]

required
raster str

from [grad, rd]

'grad'

Returns:

Type Description
Quantity[ms]
Source code in cmrseq/core/_system.py
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
def time_to_raster(self, time: Quantity, raster: str = "grad") -> Quantity:
    r"""Calculates the time projected onto the either gradient or rf raster.

    Parameters
    ----------
    time
        Quantity[s]
    raster
        from [grad, rd]

    Returns
    -------
    Quantity[ms]
    """
    if raster.lower() == "grad":
        raster = self.grad_raster_time.to("ms")
    elif raster.lower() == "rf":
        raster = self.rf_raster_time.to("ms")
    elif raster.lower() == "adc":
        raster = self.adc_raster_time.to("ms")
    else:
        raise ValueError(f"Invalid raster choice: {raster} not in [grad, rf, adc]")
    time = np.around(time.m_as("ms"), decimals=8)
    time_ndt = np.ceil(np.around(time / raster.m, decimals=8))
    time_ndt = time_ndt * raster
    return time_ndt

is_on_raster

is_on_raster(
    time: Quantity, raster: str
) -> (bool, Quantity)

Checks is given time is on raster. Returns a bool and the numerical difference to the next valid grid point.

Parameters:

Name Type Description Default
time Quantity
required
raster str
required

Returns:

Type Description
object
Source code in cmrseq/core/_system.py
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
def is_on_raster(self, time: Quantity, raster: str) -> (bool, Quantity):
    r"""Checks is given time is on raster. Returns a bool and the numerical difference to the
    next valid grid point.

    Parameters
    ----------
    time
    raster

    Returns
    -------
    object
    """
    gridded_time = self.time_to_raster(time, raster)
    difference = gridded_time - time
    return np.isclose(difference, 0., atol=1e-6), difference

modified_copy

modified_copy(**kwargs) -> SystemSpec

Copies system specsifications and modifies the specified attributes.

Parameters:

Name Type Description Default
kwargs

keyword argument according to instantiation, with the updated value

{}

Returns:

Type Description
SystemSpecs
Source code in cmrseq/core/_system.py
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
def modified_copy(self, **kwargs) -> 'SystemSpec':
    r"""Copies system specsifications and modifies the specified attributes.

    Parameters
    ----------
    kwargs
        keyword argument according to instantiation, with the updated value

    Returns
    -------
    SystemSpecs
    """
    tmp = deepcopy(self)
    for  k, v in kwargs.items():
        setattr(tmp, k, v)
    return tmp

OMatrix

OMatrix(
    position: Quantity,
    slice_normal: ndarray,
    readout_direction: ndarray,
    system_specs: SystemSpec,
)

Captures the transformation of a slice coordinate system (Readout, Phase encoding, Slice Normal) to XYZ scanner coordinates.

When applied to Gradient and corresponding RFPulse objects, a transformed definition of the waveforms is returned.

Parameters:

Name Type Description Default
position Quantity

Scalar length value for a positional offset along the slice-normal direction

required
slice_normal ndarray

(3, ) 3D vector containing the slice normal

required
readout_direction ndarray

(3, ) 3D vector containing the readout direction

required
system_specs SystemSpec
required

Raises:

Type Description
BuildingBlockArgumentError

if slice_normal and readout_direction are not orthogonal

Source code in cmrseq/core/_omatrix.py
51
52
53
54
55
56
57
58
59
60
61
def __init__(self, position: Quantity, slice_normal: np.ndarray,
               readout_direction: np.ndarray, system_specs: 'cmrseq.SystemSpec'):

    slice_normal = slice_normal / np.linalg.norm(slice_normal)
    readout_direction = readout_direction / np.linalg.norm(readout_direction)
    self._tmatrix = np.eye(4, 4)
    self._system_specs = system_specs
    self._position = position
    self._readout_dir = readout_direction
    self._slice_normal = slice_normal
    self._update_matrix()

pos_offset property writable

pos_offset

Scalar positional offset in 3D

tmatrix property

tmatrix

Returns the (4x4) transformation matrix

slice_normal property writable

slice_normal

Return read-only view of the slice-normal

readout_direction property writable

readout_direction

Return read-only view of the readout direction

apply

apply(
    block: Union[
        Gradient, Tuple[RFPulse, TrapezoidalGradient]
    ],
) -> Tuple[Quantity, Quantity]

Applies the spatial transformation from Slice-coordinates to XYZ-coordinates to the specified block.

For a Gradient block this means a rotation of the vector defined as the gradient channels [gx, gy, gz], where the total gradient area on all channels is preserved. The returned values are the time-points (t, ) and transformed gradient-waveform (3, t).

For a RFPulse the application of the OMatrix only makes sense in presence of a corresponding Trapezoidal gradient defining the slice-selective excitation. If given a tuple containing (RFPulse, TrapezoidalGradient), the frequency-modulated RF waveform is returned as time-points (t, ) and complex-wf (t, ).

Parameters:

Name Type Description Default
block Union[Gradient, Tuple[RFPulse, TrapezoidalGradient]]

Either an instance of a Gradient, or a tuple containing an RFPulse as well as a corresponding TrapezoidalGradient object defining the slice-selective excitation

required
Source code in cmrseq/core/_omatrix.py
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
def apply(self, block: Union[Gradient, Tuple[RFPulse, TrapezoidalGradient]]) \
         -> Tuple[Quantity, Quantity]:
    r"""Applies the spatial transformation from Slice-coordinates to XYZ-coordinates to the
    specified block.

    For a Gradient block this means a rotation of the vector defined as the gradient channels
    [gx, gy, gz], where the total gradient area on all channels is preserved.
    The returned values are the time-points (t, ) and transformed gradient-waveform (3, t).

    For a RFPulse the application of the OMatrix only makes sense in presence of a corresponding
    Trapezoidal gradient defining the slice-selective excitation. If given a tuple containing
    (RFPulse, TrapezoidalGradient), the frequency-modulated RF waveform is returned as
    time-points (t, ) and complex-wf (t, ).

    Parameters
    ----------
    block
        Either an instance of a Gradient, or a tuple containing an RFPulse as well as a corresponding TrapezoidalGradient object defining the slice-selective excitation
    """
    if isinstance(block, Gradient):
        return self._apply_gradient(block)
    elif (isinstance(block, (tuple, list)) and isinstance(block[0], RFPulse) and
          isinstance(block[1], TrapezoidalGradient)):
        return self._apply_rf(block[0], block[1])
    else:
        raise NotImplementedError(f"OMatrices can be applied to either instances of Gradient"
                                  f"or tuples of (RFPulse, TrapezoidalGradient) but received"
                                  f"{block}")

update

update(
    position: Quantity = None,
    slice_normal: ndarray = None,
    readout_direction: ndarray = None,
) -> None

Updates all specified properties of the OMatrix.

Parameters:

Name Type Description Default
position Quantity

Scalar length value for a positional offset along the slice-normal direction

None
slice_normal ndarray

(3, ) 3D vector containing the slice normal

None
readout_direction ndarray

(3, ) 3D vector containing the readout direction

None

Returns:

Type Description
object
Source code in cmrseq/core/_omatrix.py
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
def update(self, position: Quantity = None, slice_normal: np.ndarray = None,
           readout_direction: np.ndarray = None) -> None:
    r"""Updates all specified properties of the OMatrix.

    Parameters
    ----------
    position
        Scalar length value for a positional offset along the slice-normal direction
    slice_normal
        (3, ) 3D vector containing the slice normal
    readout_direction
        (3, ) 3D vector containing the readout direction

    Returns
    -------
    object
    """
    if slice_normal is not None:
        self._slice_normal[:] = slice_normal / np.linalg.norm(slice_normal)
    if readout_direction is not None:
        self._readout_dir[:] = readout_direction / np.linalg.norm(readout_direction)
    if position is not None:
        self._position = position.to("m")
    self._update_matrix()

merge_OMatrices staticmethod

merge_OMatrices(
    omatrices: List[OMatrix],
    allow_first_shift: bool = False,
) -> OMatrix

Combine a list of orientation matrices into one matrix.

The first entry is applied first. By default only the last matrix may contain a positional shift. Set allow_first_shift=True to instead allow only the first matrix to contain the positional shift.

Parameters:

Name Type Description Default
omatrices List[OMatrix]

Orientation matrices to merge.

required
allow_first_shift bool

If True, allow only the first matrix to have a positional shift. Otherwise, allow only the last matrix to have a positional shift.

False

Returns:

Type Description
OMatrix

Merged orientation matrix.

Source code in cmrseq/core/_omatrix.py
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
@staticmethod
def merge_OMatrices(omatrices: List['OMatrix'], allow_first_shift: bool = False) -> 'OMatrix':
    r"""Combine a list of orientation matrices into one matrix.

    The first entry is applied first. By default only the last matrix may contain a positional
    shift. Set ``allow_first_shift=True`` to instead allow only the first matrix to contain the
    positional shift.

    Parameters
    ----------
    omatrices
        Orientation matrices to merge.
    allow_first_shift
        If ``True``, allow only the first matrix to have a positional shift. Otherwise, allow
        only the last matrix to have a positional shift.

    Returns
    -------
    OMatrix
        Merged orientation matrix.
    """
    if not omatrices:
        raise ValueError("List of orientation matrices is empty")

    if len(omatrices) == 1:
        return deepcopy(omatrices[0])

    if allow_first_shift:
        if np.any([om._position.m_as("m") != 0 for om in omatrices[1:]]):
            raise ValueError("Only the first OMatrix can have a position shift.")
    else:
        if np.any([om._position.m_as("m") != 0 for om in omatrices[:-1]]):
            raise ValueError("Only the last OMatrix can have a position shift.")

    system_specs = omatrices[-1]._system_specs
    tmatrix_r = deepcopy(omatrices[0]._tmatrix)[:3, :3]

    for omatrix in omatrices[1:]:
        tmatrix_r = np.einsum("ij, jt -> it", omatrix._tmatrix[:3, :3], tmatrix_r)

    readout_direction = tmatrix_r[:, 0]
    slice_normal = tmatrix_r[:, 2]
    position = omatrices[0]._position if allow_first_shift else omatrices[-1]._position

    return OMatrix(position.to("m"), slice_normal, readout_direction, system_specs)

Label

Label(
    name: Union[str, List[str]] = None,
    value: Union[int, List[int]] = None,
)

Container for sequence labels and Pulseq extension metadata.

Labels are stored with ISMRMRD-style names and are attached to sequence blocks that support a label attribute, currently RF and ADC blocks. When a sequence is exported to Pulseq, standard counters are written as LABELSET/LABELINC extensions. The special labels triggerWait and triggerSend are written as Pulseq trigger extensions.

Parameters:

Name Type Description Default
name Union[str, List[str]]

Label name, or list of label names, to initialize.

None
value Union[int, List[int]]

Integer label value, or list of values matching name.

None
Source code in cmrseq/core/_labels.py
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
def __init__(self, name: Union[str, List[str]] = None, value: Union[int, List[int]] = None):
    # Initialize label dict
    self._labels = {"kspace_encode_step_1": None,
            "kspace_encode_step_2": None,
            "average": None,
            "slice": None,
            "contrast": None,
            "phase": None,
            "repetition": None,
            "set": None,
            "segment": None,
            "triggerWait": None,
            "triggerSend": None}
    if name is not None and value is not None:
        self.setLabels(name, value)

setLabels

setLabels(
    name: Union[str, List[str]],
    value: Union[int, List[int]],
)

Set one or more label values.

Parameters:

Name Type Description Default
name Union[str, List[str]]

Label name, or list of names.

required
value Union[int, List[int]]

Integer label value, or list of values matching name.

required

Raises:

Type Description
ValueError

If any label name is not supported.

Source code in cmrseq/core/_labels.py
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
def setLabels(self, name: Union[str, List[str]], value: Union[int, List[int]]):
    r"""Set one or more label values.

    Parameters
    ----------
    name
        Label name, or list of names.
    value
        Integer label value, or list of values matching ``name``.

    Raises
    ------
    ValueError
        If any label name is not supported.
    """
    if isinstance(name, str):
        name = [name]
        value = [value]

    for n,v in zip(name,value):
        if n not in self._labels:
            raise ValueError(f"Label {n} is not a valid label name")
        self._labels[n] = int(v)

clearLabels

clearLabels(name: Union[str, List[str]])

Clear one or more labels.

Parameters:

Name Type Description Default
name Union[str, List[str]]

Label name, list of names, or "all" to clear every label.

required

Raises:

Type Description
ValueError

If any label name is not supported.

Source code in cmrseq/core/_labels.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
def clearLabels(self, name: Union[str, List[str]]):
    r"""Clear one or more labels.

    Parameters
    ----------
    name
        Label name, list of names, or ``"all"`` to clear every label.

    Raises
    ------
    ValueError
        If any label name is not supported.
    """

    if isinstance(name, str):
        if name == "all":
            name = list(self._labels.keys())

    if isinstance(name, str):
        name = [name]

    for n in name:
        if n not in self._labels:
            raise ValueError(f"Label {n} is not a valid label name")
        self._labels[n] = None

getActiveLabels

getActiveLabels()

Return labels with non-None values.

Returns:

Type Description
dict

Mapping from active label name to integer value.

Source code in cmrseq/core/_labels.py
118
119
120
121
122
123
124
125
126
def getActiveLabels(self):
    r"""Return labels with non-``None`` values.

    Returns
    -------
    dict
        Mapping from active label name to integer value.
    """
    return {k:v for k,v in self._labels.items() if v is not None}

getValue

getValue(name: str)

Return the value for one label.

Parameters:

Name Type Description Default
name str

Label name.

required

Returns:

Type Description
int or None

Current label value. None means the label is inactive.

Raises:

Type Description
ValueError

If name is not supported.

Source code in cmrseq/core/_labels.py
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
def getValue(self, name: str):
    r"""Return the value for one label.

    Parameters
    ----------
    name
        Label name.

    Returns
    -------
    int or None
        Current label value. ``None`` means the label is inactive.

    Raises
    ------
    ValueError
        If ``name`` is not supported.
    """
    if name not in self._labels:
        raise ValueError(f"Label {name} is not a valid label name")
    return self._labels[name]

SequenceBaseBlock

SequenceBaseBlock(
    system_specs: SystemSpec,
    name: str,
    snap_to_raster: bool = False,
)

Bases: SimpleNamespace

Base class for all building blocks, defining the abstract interface for generic interaction with blocks.

All subclases must implent the abstract methods, and all general methods on blocks must only access the implemented methods when no sub-class type check is performed.

Furthermore, all subclasses must call the base-class constructor.

Parameters:

Name Type Description Default
system_specs SystemSpec
required
name str
required
snap_to_raster bool
False

Must be called as last line of subclass.init

Source code in cmrseq/core/bausteine/_base.py
34
35
36
37
38
39
40
def __init__(self, system_specs: SystemSpec, name: str, snap_to_raster: bool = False):
    r"""Must be called as last line of subclass.__init__"""
    super().__init__(name=name)
    if snap_to_raster:
        self.snap_to_raster(system_specs)
    self._clean()
    self.validate(system_specs)

name instance-attribute

name: str

tmin property

tmin: Quantity

Calculates the smallest time occuring in all contained definitions.

Returns:

Type Description
Quantity[time]

tmax property

tmax: Quantity

Calculates the largest time occuring in all contained definitions.

Returns:

Type Description
Quantity[time]

duration property

duration: Quantity

Calculates the duration of the block, which is defined as tmax - tmin.

copy

copy() -> SequenceBaseBlock

Returns a deep copied building block object

Source code in cmrseq/core/bausteine/_base.py
48
49
50
def copy(self) -> 'SequenceBaseBlock':
    r"""Returns a deep copied building block object"""
    return deepcopy(self)

snap_to_raster abstractmethod

snap_to_raster(system_specs: SystemSpec) -> None
Source code in cmrseq/core/bausteine/_base.py
52
53
54
@abstractmethod
def snap_to_raster(self, system_specs: SystemSpec) -> None:
    pass

validate abstractmethod

validate(system_specs: SystemSpec) -> None

Should raise a ValueError if subclass logic is not met by definition

Parameters:

Name Type Description Default
system_specs SystemSpec
required

Returns:

Type Description
object
Source code in cmrseq/core/bausteine/_base.py
59
60
61
62
63
64
65
66
67
68
69
70
@abstractmethod
def validate(self, system_specs: SystemSpec) -> None:
    r"""Should raise a ValueError if subclass logic is not met by definition
    Parameters
    ----------
    system_specs

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

flip abstractmethod

flip(time_flip: Quantity = None)

Flips block around specified time point

Source code in cmrseq/core/bausteine/_base.py
95
96
97
98
@abstractmethod
def flip(self, time_flip: Quantity = None):
    r"""Flips block around specified time point"""
    return

shift abstractmethod

shift(time_shift: Quantity) -> None

Shifts block in time

Source code in cmrseq/core/bausteine/_base.py
100
101
102
103
@abstractmethod
def shift(self, time_shift: Quantity) -> None:
    r"""Shifts block in time"""
    return