| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703 |
- import itertools
- import math
- from collections import OrderedDict
- from copy import copy
- from itertools import chain
- from types import SimpleNamespace
- from typing import Tuple, List, Iterable
- from typing import Union
- from warnings import warn
- import matplotlib as mpl
- import numpy as np
- from matplotlib import pyplot as plt
- from seqgen.pypulseq import eps
- from seqgen.pypulseq.Sequence import block, parula
- from seqgen.pypulseq.Sequence.ext_test_report import ext_test_report
- from seqgen.pypulseq.Sequence.read_seq import read
- from seqgen.pypulseq.Sequence.write_seq import write as write_seq
- from seqgen.pypulseq.calc_rf_center import calc_rf_center
- from seqgen.pypulseq.check_timing import check_timing as ext_check_timing
- from seqgen.pypulseq.decompress_shape import decompress_shape
- from seqgen.pypulseq.event_lib import EventLibrary
- from seqgen.pypulseq.opts import Opts
- from seqgen.pypulseq.supported_labels_rf_use import get_supported_labels
- from seqgen.MRI_sequences.version import major, minor, revision
- class Sequence:
- """
- Generate sequences and read/write sequence files. This class defines properties and methods to define a complete MR
- sequence including RF pulses, gradients, ADC events, etc. The class provides an implementation of the open MR
- sequence format defined by the Pulseq project. See http://pulseq.github.io/.
- See also `demo_read.py`, `demo_write.py`.
- """
- version_major = major
- version_minor = minor
- version_revision = revision
- def __init__(self, system=Opts()):
- # =========
- # EVENT LIBRARIES
- # =========
- self.adc_library = EventLibrary() # Library of ADC events
- self.delay_library = EventLibrary() # Library of delay events
- # Library of extension events. Extension events form single-linked zero-terminated lists
- self.extensions_library = EventLibrary()
- self.grad_library = EventLibrary() # Library of gradient events
- self.label_inc_library = (
- EventLibrary()
- ) # Library of Label(inc) events (reference from the extensions library)
- self.label_set_library = (
- EventLibrary()
- ) # Library of Label(set) events (reference from the extensions library)
- self.rf_library = EventLibrary() # Library of RF events
- self.shape_library = EventLibrary() # Library of compressed shapes
- self.trigger_library = EventLibrary() # Library of trigger events
- # =========
- # OTHER
- # =========
- self.system = system
- self.block_events = OrderedDict() # Event table
- self.definitions = dict() # Optional sequence definitions
- self.rf_raster_time = (
- self.system.rf_raster_time
- ) # RF raster time (system dependent)
- self.grad_raster_time = (
- self.system.grad_raster_time
- ) # Gradient raster time (system dependent)
- self.adc_raster_time = (
- self.system.adc_raster_time
- ) # ADC raster time (system dependent)
- self.block_duration_raster = self.system.block_duration_raster
- self.set_definition("AdcRasterTime", self.adc_raster_time)
- self.set_definition("BlockDurationRaster", self.block_duration_raster)
- self.set_definition("GradientRasterTime", self.grad_raster_time)
- self.set_definition("RadiofrequencyRasterTime", self.rf_raster_time)
- self.signature_type = ""
- self.signature_file = ""
- self.signature_value = ""
- self.block_durations = [] # Cache of block durations
- self.extension_numeric_idx = [] # numeric IDs of the used extensions
- self.extension_string_idx = [] # string IDs of the used extensions
- def __str__(self) -> str:
- s = "Sequence:"
- s += "\nshape_library: " + str(self.shape_library)
- s += "\nrf_library: " + str(self.rf_library)
- s += "\ngrad_library: " + str(self.grad_library)
- s += "\nadc_library: " + str(self.adc_library)
- s += "\ndelay_library: " + str(self.delay_library)
- s += "\nextensions_library: " + str(
- self.extensions_library
- ) # inserted for trigger support by mveldmann
- s += "\nrf_raster_time: " + str(self.rf_raster_time)
- s += "\ngrad_raster_time: " + str(self.grad_raster_time)
- s += "\nblock_events: " + str(len(self.block_events))
- return s
- def add_block(self, *args: SimpleNamespace) -> None:
- """
- Add a new block/multiple events to the sequence. Adds a sequence block with provided as a block structure
- See also:
- - `pypulseq.Sequence.sequence.Sequence.set_block()`
- - `pypulseq.make_adc.make_adc()`
- - `pypulseq.make_trapezoid.make_trapezoid()`
- - `pypulseq.make_sinc_pulse.make_sinc_pulse()`
- Parameters
- ----------
- args : SimpleNamespace
- Block structure or events to be added as a block to `Sequence`.
- """
- block.set_block(self, len(self.block_events) + 1, *args)
- def calculate_kspace(
- self, trajectory_delay: int = 0
- ) -> Tuple[np.array, np.array, np.array, np.array, np.array]:
- """
- Calculates the k-space trajectory of the entire pulse sequence.
- Parameters
- ----------
- trajectory_delay : int, default=0
- Compensation factor in seconds (s) to align ADC and gradients in the reconstruction.
- Returns
- -------
- k_traj_adc : numpy.array
- K-space trajectory sampled at `t_adc` timepoints.
- k_traj : numpy.array
- K-space trajectory of the entire pulse sequence.
- t_excitation : numpy.array
- Excitation timepoints.
- t_refocusing : numpy.array
- Refocusing timepoints.
- t_adc : numpy.array
- Sampling timepoints.
- """
- if np.any(np.abs(trajectory_delay) > 100e-6):
- raise Warning(
- f"Trajectory delay of {trajectory_delay * 1e6} us is suspiciously high"
- )
- # Initialise the counters and accumulator objects
- count_excitation = 0
- count_refocusing = 0
- count_adc_samples = 0
- # Loop through the blocks to prepare preallocations
- for block_counter in range(len(self.block_events)):
- block = self.get_block(block_counter + 1)
- if block.rf is not None:
- if (
- not hasattr(block.rf, "use")
- or block.rf.use == "excitation"
- or block.rf.use == "undefined"
- ):
- count_excitation += 1
- elif block.rf.use == "refocusing":
- count_refocusing += 1
- if block.adc is not None:
- count_adc_samples += int(block.adc.num_samples)
- t_excitation = np.zeros(count_excitation)
- t_refocusing = np.zeros(count_refocusing)
- k_time = np.zeros(count_adc_samples)
- current_duration = 0
- count_excitation = 0
- count_refocusing = 0
- kc_outer = 0
- traj_recon_delay = trajectory_delay
- # Go through the blocks and collect RF and ADC timing data
- for block_counter in range(len(self.block_events)):
- block = self.get_block(block_counter + 1)
- if block.rf is not None:
- rf = block.rf
- rf_center, _ = calc_rf_center(rf)
- t = rf.delay + rf_center
- if (
- not hasattr(block.rf, "use")
- or block.rf.use == "excitation"
- or block.rf.use == "undefined"
- ):
- t_excitation[count_excitation] = current_duration + t
- count_excitation += 1
- elif block.rf.use == "refocusing":
- t_refocusing[count_refocusing] = current_duration + t
- count_refocusing += 1
- if block.adc is not None:
- _k_time = np.arange(block.adc.num_samples) + 0.5
- _k_time = (
- _k_time * block.adc.dwell
- + block.adc.delay
- + current_duration
- + traj_recon_delay
- )
- k_time[kc_outer : kc_outer + block.adc.num_samples] = _k_time
- kc_outer += block.adc.num_samples
- current_duration += self.block_durations[block_counter]
- # Now calculate the actual k-space trajectory based on the gradient waveforms
- gw = self.gradient_waveforms()
- i_excitation = np.round(t_excitation / self.grad_raster_time)
- i_refocusing = np.round(t_refocusing / self.grad_raster_time)
- i_periods = np.sort(
- [1, *(i_excitation + 1), *(i_refocusing + 1), gw.shape[1] + 1]
- ).astype(np.int32)
- # i_periods -= 1 # Python is 0-indexed
- ii_next_excitation = np.min((len(i_excitation), 1))
- ii_next_refocusing = np.min((len(i_refocusing), 1))
- k_traj = np.zeros_like(gw)
- k = np.zeros((3, 1))
- for i in range(len(i_periods) - 1):
- i_period_end = i_periods[i + 1] - 1
- k_period = np.concatenate(
- (k, gw[:, i_periods[i] - 1 : i_period_end] * self.grad_raster_time),
- axis=1,
- )
- k_period = np.cumsum(k_period, axis=1)
- k_traj[:, i_periods[i] - 1 : i_period_end] = k_period[:, 1:]
- k = k_period[:, -1]
- if (
- ii_next_excitation > 0
- and i_excitation[ii_next_excitation - 1] == i_period_end
- ):
- k[:] = 0
- k_traj[:, i_period_end - 1] = np.nan
- ii_next_excitation = min(len(i_excitation), ii_next_excitation + 1)
- if (
- ii_next_refocusing > 0
- and i_refocusing[ii_next_refocusing - 1] == i_period_end
- ):
- k = -k
- ii_next_refocusing = min(len(i_refocusing), ii_next_refocusing + 1)
- k = k.reshape((-1, 1)) # To be compatible with np.concatenate
- k_traj_adc = []
- for _k_traj_row in k_traj:
- result = np.interp(
- xp=np.array(range(1, k_traj.shape[1] + 1)) * self.grad_raster_time,
- fp=_k_traj_row,
- x=k_time,
- )
- k_traj_adc.append(result)
- k_traj_adc = np.stack(k_traj_adc)
- t_adc = k_time
- return k_traj_adc, k_traj, t_excitation, t_refocusing, t_adc
- def calculate_kspacePP(
- self,
- trajectory_delay: Union[int, float, np.ndarray] = 0,
- gradient_offset: int = 0,
- ) -> Tuple[np.array, np.array, np.array, np.array, np.array]:
- """
- Calculates the k-space trajectory of the entire pulse sequence.
- Parameters
- ----------
- trajectory_delay : int, default=0
- Compensation factor in seconds (s) to align ADC and gradients in the reconstruction.
- Returns
- -------
- k_traj_adc : numpy.array
- K-space trajectory sampled at `t_adc` timepoints.
- k_traj : numpy.array
- K-space trajectory of the entire pulse sequence.
- t_excitation : numpy.array
- Excitation timepoints.
- t_refocusing : numpy.array
- Refocusing timepoints.
- t_adc : numpy.array
- Sampling timepoints.
- """
- if np.any(np.abs(trajectory_delay) > 100e-6):
- raise Warning(
- f"Trajectory delay of {trajectory_delay * 1e6} us is suspiciously high"
- )
- total_duration = np.sum(self.block_durations)
- gw_data, tfp_excitation, tfp_refocusing, t_adc, _ = self.waveforms_and_times()
- ng = len(gw_data)
- # Gradient delay handling
- if isinstance(trajectory_delay, (int, float)):
- gradient_delays = [trajectory_delay] * ng
- else:
- assert (
- len(trajectory_delay) == ng
- ) # Need to have same number of gradient channels
- gradient_delays = [trajectory_delay] * ng
- # Gradient offset handling
- if isinstance(gradient_offset, (int, float)):
- gradient_offset = [gradient_offset] * ng
- else:
- assert (
- len(gradient_offset) == ng
- ) # Need to have same number of gradient channels
- # Convert data to piecewise polynomials
- gw_pp = []
- gw_pp_MATLAB = []
- for j in range(ng):
- wave_cnt = gw_data[j].shape[1]
- if wave_cnt == 0:
- if np.abs(gradient_offset[j]) <= eps:
- continue
- else:
- gw = np.array(([0, total_duration], [0, 0]))
- else:
- gw = gw_data[j]
- # Now gw contains the waveform from the current axis
- if np.abs(gradient_delays[j]) > eps:
- gw[1] = gw[1] - gradient_delays[j] # Anisotropic gradient delay support
- if not np.all(np.isfinite(gw)):
- raise Warning("Not all elements of the generated waveform are finite.")
- teps = 1e-12
- if gw[0, 0] > 0 and gw[1, -1] < total_duration:
- # teps terms to avoid integration errors over extended periods of time
- _temp1 = np.array(([-teps, gw[0, 0] - teps], [0, 0]))
- _temp2 = np.array(([gw[0, -1] + teps, total_duration + teps], [0, 0]))
- gw = np.hstack((_temp1, gw, _temp2))
- elif gw[0, 0] > 0:
- _temp = np.array(([-teps, gw[0, 0] - teps], [0, 0]))
- gw = np.hstack((_temp, gw))
- elif gw[0, -1] < total_duration:
- _temp = np.array(([gw[0, -1] + teps, total_duration + teps], [0, 0]))
- gw = np.hstack((gw, _temp))
- if np.abs(gradient_offset[j]) > eps:
- gw[1:] += gradient_offset[j]
- gw[1][gw[1] == -0.0] = 0.0
- # Specify window to be same as domain prevent numpy from remapping to [-1, 1]
- polyfit = [
- np.polynomial.Polynomial.fit(
- gw[0, i : (i + 2)],
- gw[1, i : (i + 2)],
- deg=1,
- window=gw[0, i : (i + 2)],
- )
- for i in range(len(gw[0]) - 1)
- ]
- polyfit = np.stack(polyfit)
- gw_pp.append(polyfit)
- ###
- """
- Fix coefs for consistency with MATLAB:
- 1. MATLAB returns coefficients in descending order whereas Numpy returns coefficients in ascending order.
- 2. MATLAB returns local coefficients that will NOT match Numpy's outputs. Refer to the equation under the
- "output arguments" section of `mkpp` MATLAB docs to convert and match coefficient outputs.
- 3. Finally, MATLAB seems to not store any -1 < x < 1 coefficients, so we zero them out.
- """
- polyfit_MATLAB = []
- for i in range(len(polyfit)):
- polyfit_MATLAB_i = copy(polyfit[i])
- lower = polyfit_MATLAB_i.domain[0]
- co = polyfit_MATLAB_i.coef
- co = co[::-1] # Reverse
- a = co[0]
- b = co[1] + (a * lower)
- if -1 < a < 1: # to investigate
- a = 0
- if -1 < b < 1:
- b = 0
- # co = [b, a] # Un-reverse for Numpy
- co = [a, b]
- polyfit_MATLAB_i.coef = co
- polyfit_MATLAB.append(polyfit_MATLAB_i)
- gw_pp_MATLAB.append(polyfit_MATLAB)
- ###
- # Calculate slice positions. For now we entirely rely on the excitation -- ignoring complicated interleaved
- # refocused sequences
- if len(tfp_excitation) > 0:
- # Position in x, y, z
- slice_pos = np.zeros((len(gw_data), tfp_excitation.shape[1]))
- for j in range(len(gw_data)):
- if gw_pp[j] is None:
- slice_pos[j] = np.empty_like((1, slice_pos.shape[1]))
- else:
- slice_pos[j] = np.divide(
- tfp_excitation[1], self.ppval_numpy(gw_pp[j], tfp_excitation[0])
- )
- slice_pos[~np.isfinite(slice_pos)] = 0 # Reset undefined to 0
- t_slice_pos = tfp_excitation[0]
- else:
- slice_pos = []
- t_slice_pos = []
- # FNINT
- def fnint(arr_poly):
- pieces = len(arr_poly)
- breaks = np.stack([pp.domain for pp in arr_poly])
- breaks = np.append(breaks[:, 0], breaks[-1, 1])
- coefs = np.stack([pp.coef for pp in arr_poly])
- order = len(arr_poly[1].coef)
- dim = 1
- coefs = coefs / np.tile(range(order, 0, -1), (dim * pieces, 1))
- xs = np.diff(breaks[:-1])
- index = np.arange(pieces - 1)
- vv = xs * coefs[index, 0]
- for i in range(1, order):
- vv = xs * (vv + coefs[index, i])
- last = np.cumsum(np.insert(vv, 0, 0)).reshape((-1, 1))
- coefs = np.hstack((coefs[:, :order], last))
- arr_poly_integ = []
- for i in range(pieces):
- arr_poly_integ.append(
- np.polynomial.Polynomial(
- coefs[i],
- domain=[breaks[i], breaks[i + 1]],
- window=[breaks[i], breaks[i + 1]],
- )
- )
- return arr_poly_integ, coefs, breaks
- # =========
- # Integrate waveforms as PPs to produce gradient moments
- gm_pp = []
- tc = []
- for i in range(ng):
- if gw_pp[i] is None:
- continue
- res_fnint, res_coefs, res_breaks = fnint(gw_pp_MATLAB[i])
- gm_pp.append(res_fnint)
- tc.append(res_breaks)
- # "Sample" ramps for display purposes otherwise piecewise-linear display (plot) fails
- ii = np.nonzero(np.abs(res_coefs[:, 0]) > eps)[0]
- if len(ii) > 0:
- tca = []
- for j in range(len(ii)):
- res = (
- np.arange(
- np.floor(float(res_breaks[ii[j]] / self.grad_raster_time)),
- np.ceil(
- (res_breaks[ii[j] + 1] / self.grad_raster_time) + 1
- ),
- )
- * self.grad_raster_time
- )
- tca.extend(res)
- tc.append(tca)
- tc = np.array(list(chain(*tc)))
- if len(tfp_excitation) == 0:
- t_excitation = np.array([])
- else:
- t_excitation = tfp_excitation[0]
- if len(tfp_refocusing) == 0:
- t_refocusing = np.array([])
- else:
- t_refocusing = tfp_refocusing[0]
- t_acc = 1e-10 # Temporal accuracy
- t_acc_inv = 1 / t_acc
- # tc = self.__flatten_jagged_arr(tc)
- t_ktraj = t_acc * np.unique(
- np.round(
- t_acc_inv
- * np.array(
- [
- *tc,
- 0,
- *np.array(t_excitation) * 2 - self.rf_raster_time,
- *np.array(t_excitation) - self.rf_raster_time,
- *t_excitation,
- *np.array(t_refocusing) - self.rf_raster_time,
- *t_refocusing,
- *t_adc,
- total_duration,
- ]
- )
- )
- )
- i_excitation = np.isin(t_ktraj, t_acc * np.round(t_acc_inv * t_excitation))
- i_excitation = np.nonzero(i_excitation)[0] # Convert boolean array into indices
- i_refocusing = np.isin(t_ktraj, t_acc * np.round(t_acc_inv * t_refocusing))
- i_refocusing = np.nonzero(i_refocusing)[0] # Convert boolean array into indices
- i_adc = np.isin(t_ktraj, t_acc * np.round(t_acc_inv * t_adc))
- i_adc = np.nonzero(i_adc)[0] # Convert boolean array into indices
- i_periods = np.unique([1, *i_excitation, *i_refocusing, len(t_ktraj) - 1])
- if len(i_excitation) > 0:
- ii_next_excitation = 1
- else:
- ii_next_excitation = 0
- if len(i_refocusing) > 0:
- ii_next_refocusing = 1
- else:
- ii_next_refocusing = 0
- k_traj = np.zeros((3, len(t_ktraj)))
- for i in range(ng):
- if gw_pp_MATLAB[i] is None:
- continue
- it = np.logical_and(
- t_ktraj >= t_acc * np.round(t_acc_inv * res_breaks[0]),
- t_ktraj <= t_acc * np.round(t_acc_inv * res_breaks[-1]),
- )
- k_traj[i, it] = self.ppval_MATLAB(gm_pp[i], t_ktraj[it])
- if t_ktraj[it[-1]] < t_ktraj[-1]:
- k_traj[i, it[-1] + 1 :] = k_traj[i, it[-1]]
- # Convert gradient moments to kspace
- dk = -k_traj[:, 0]
- for i in range(len(i_periods) - 1):
- i_period = i_periods[i]
- i_period_end = i_periods[i + 1]
- if ii_next_excitation > 0 and i_excitation[ii_next_excitation] == i_period:
- if np.abs(t_ktraj[i_period] - t_excitation[ii_next_excitation]) > t_acc:
- raise Warning(
- f"np.abs(t_ktraj[i_period]-t_excitation[ii_next_excitation]) < {t_acc} failed for ii_next_excitation={ii_next_excitation} error={t_ktraj(i_period) - t_excitation(ii_next_excitation)}"
- )
- dk = -k_traj[:, i_period]
- if i_period > 1:
- # Use nans to mark the excitation points since they interrupt the plots
- k_traj[:, i_period - 1] = np.NaN
- # -1 on len(i_excitation) for 0-based indexing
- ii_next_excitation = np.minimum(
- len(i_excitation) - 1, ii_next_excitation + 1
- )
- elif (
- ii_next_refocusing > 0 and i_refocusing[ii_next_refocusing] == i_period
- ):
- dk = -k_traj[:, i_period]
- dk = -2 * k_traj[:, i_period] - dk
- # -1 on len(i_excitation) for 0-based indexing
- ii_next_refocusing = np.minimum(
- len(i_refocusing) - 1, ii_next_refocusing + 1
- )
- k_traj[:, i_period:i_period_end] = (
- k_traj[:, i_period:i_period_end] + dk[:, None]
- )
- k_traj[:, i_period_end] = k_traj[:, i_period_end] + dk
- k_traj_adc = k_traj[:, i_adc]
- return k_traj_adc, k_traj, t_excitation, t_refocusing, t_adc
- def check_timing(self) -> Tuple[bool, List[str]]:
- """
- Checks timing of all blocks and objects in the sequence optionally returns the detailed error log. This
- function also modifies the sequence object by adding the field "TotalDuration" to sequence definitions.
- Returns
- -------
- is_ok : bool
- Boolean flag indicating timing errors.
- error_report : str
- Error report in case of timing errors.
- """
- error_report = []
- is_ok = True
- num_blocks = len(self.block_events)
- total_duration = 0
- for block_counter in range(num_blocks):
- block = self.get_block(block_counter + 1)
- events = [e for e in vars(block).values() if e is not None]
- res, rep, duration = ext_check_timing(self.system, *events)
- is_ok = is_ok and res
- # Check the stored total block duration
- if np.abs(duration - self.block_durations[block_counter]) > eps:
- rep += "Inconsistency between the stored block duration and the duration of the block content"
- is_ok = False
- duration = self.block_durations[block_counter]
- # Check RF dead times
- if block.rf is not None:
- if block.rf.delay - block.rf.dead_time < -eps:
- rep += (
- f"Delay of {block.rf.delay * 1e6} us is smaller than the RF dead time "
- f"{block.rf.dead_time * 1e6} us"
- )
- is_ok = False
- if (
- block.rf.delay + block.rf.t[-1] + block.rf.ringdown_time - duration
- > eps
- ):
- rep += (
- f"Time between the end of the RF pulse at {block.rf.delay + block.rf.t[-1]} and the end "
- f"of the block at {duration * 1e6} us is shorter than rf_ringdown_time"
- )
- is_ok = False
- # Check ADC dead times
- if block.adc is not None:
- if block.adc.delay - self.system.adc_dead_time < -eps:
- rep += "adc.delay < system.adc_dead_time"
- is_ok = False
- if (
- block.adc.delay
- + block.adc.num_samples * block.adc.dwell
- + self.system.adc_dead_time
- - duration
- > eps
- ):
- rep += "adc: system.adc_dead_time (post-ADC) violation"
- is_ok = False
- # Update report
- if len(rep) != 0:
- error_report.append(f"Event: {block_counter} - {rep}\n")
- total_duration += duration
- # Check if all the gradients in the last block are ramped down properly
- if len(events) != 0 and all([isinstance(e, SimpleNamespace) for e in events]):
- for e in range(len(events)):
- if not isinstance(events[e], list) and events[e].type == "grad":
- if events[e].last != 0:
- error_report.append(
- f"Event {num_blocks - 1} gradients do not ramp to 0 at the end of the sequence"
- )
- self.set_definition("TotalDuration", total_duration)
- return is_ok, error_report
- def duration(self) -> Tuple[int, int, np.ndarray]:
- """
- Returns the total duration of this sequence, and the total count of blocks and events.
- Returns
- -------
- duration : int
- Duration of this sequence in seconds (s).
- num_blocks : int
- Number of blocks in this sequence.
- event_count : np.ndarray
- Number of events in this sequence.
- """
- num_blocks = len(self.block_events)
- event_count = np.zeros(len(self.block_events[1]))
- duration = 0
- for block_counter in range(num_blocks):
- event_count += self.block_events[block_counter + 1] > 0
- duration += self.block_durations[block_counter]
- return duration, num_blocks, event_count
- def __flatten_jagged_arr(self, arr: np.array) -> np.array:
- # Sanity check: we don't need to do anything if we have a flat array
- def __flat_check(arr: np.array) -> bool:
- return all([not isinstance(x, Iterable) for x in arr])
- if __flat_check(arr):
- return arr
- # Flatten the array simply -- 1 level deep
- arr_flattened = list(itertools.chain(*arr))
- # Not flat yet?
- if __flat_check(arr_flattened):
- return arr_flattened
- # Flatten the array -- 2 levels deep
- any_ragged = [isinstance(x, Iterable) for x in arr_flattened]
- if np.any(any_ragged):
- idx_ragged = np.array(np.where(any_ragged)[0])
- for i in range(len(idx_ragged)):
- ii = idx_ragged[i]
- # If we are not at the end of the list, we need to update the indices of the remaining elements
- # Because once we expand and insert this list element, the indices of the remaining elements
- # will be shifted by len(this element)
- if i != len(idx_ragged) - 1:
- idx_ragged[i + 1 :] += len(arr_flattened[ii])
- arr_flattened = np.insert(arr_flattened, ii, arr_flattened[ii])
- return arr_flattened
- def flip_grad_axis(self, axis: str) -> None:
- """
- Invert all gradients along the corresponding axis/channel. The function acts on all gradient objects already
- added to the sequence object.
- Parameters
- ----------
- axis : str
- Gradients to invert or scale. Must be one of 'x', 'y' or 'z'.
- """
- self.mod_grad_axis(axis, modifier=-1)
- def get_block(self, block_index: int) -> SimpleNamespace:
- """
- Return a block of the sequence specified by the index. The block is created from the sequence data with all
- events and shapes decompressed.
- See also:
- - `pypulseq.Sequence.sequence.Sequence.set_block()`.
- - `pypulseq.Sequence.sequence.Sequence.add_block()`.
- Parameters
- ----------
- block_index : int
- Index of block to be retrieved from `Sequence`.
- Returns
- -------
- SimpleNamespace
- Event identified by `block_index`.
- """
- return block.get_block(self, block_index)
- def get_definition(self, key: str) -> str:
- """
- Return value of the definition specified by the key. These definitions can be added manually or read from the
- header of a sequence file defined in the sequence header. An empty array is returned if the key is not defined.
- See also `pypulseq.Sequence.sequence.Sequence.set_definition()`.
- Parameters
- ----------
- key : str
- Key of definition to retrieve.
- Returns
- -------
- str
- Definition identified by `key` if found, else returns ''.
- """
- if key in self.definitions:
- return self.definitions[key]
- else:
- return ""
- def get_extension_type_ID(self, extension_string: str) -> int:
- """
- Get numeric extension ID for `extension_string`. Will automatically create a new ID if unknown.
- Parameters
- ----------
- extension_string : str
- Given string extension ID.
- Returns
- -------
- extension_id : int
- Numeric ID for given string extension ID.
- """
- if extension_string not in self.extension_string_idx:
- if len(self.extension_numeric_idx) == 0:
- extension_id = 1
- else:
- extension_id = 1 + max(self.extension_numeric_idx)
- self.extension_numeric_idx.append(extension_id)
- self.extension_string_idx.append(extension_string)
- assert len(self.extension_numeric_idx) == len(self.extension_string_idx)
- else:
- num = self.extension_string_idx.index(extension_string)
- extension_id = self.extension_numeric_idx[num]
- return extension_id
- def get_extension_type_string(self, extension_id: int) -> str:
- """
- Get string extension ID for `extension_id`.
- Parameters
- ----------
- extension_id : int
- Given numeric extension ID.
- Returns
- -------
- extension_str : str
- String ID for the given numeric extension ID.
- Raises
- ------
- ValueError
- If given numeric extension ID is unknown.
- """
- if extension_id in self.extension_numeric_idx:
- num = self.extension_numeric_idx.index(extension_id)
- else:
- raise ValueError(
- f"Extension for the given ID - {extension_id} - is unknown."
- )
- extension_str = self.extension_string_idx[num]
- return extension_str
- def mod_grad_axis(self, axis: str, modifier: int) -> None:
- """
- Invert or scale all gradients along the corresponding axis/channel. The function acts on all gradient objects
- already added to the sequence object.
- Parameters
- ----------
- axis : str
- Gradients to invert or scale. Must be one of 'x', 'y' or 'z'.
- modifier : int
- Scaling value.
- Raises
- ------
- ValueError
- If invalid `axis` is passed. Must be one of 'x', 'y','z'.
- RuntimeError
- If same gradient event is used on multiple axes.
- """
- if axis not in ["x", "y", "z"]:
- raise ValueError(
- f"Invalid axis. Must be one of 'x', 'y','z'. Passed: {axis}"
- )
- channel_num = ["x", "y", "z"].index(axis)
- other_channels = [0, 1, 2]
- other_channels.remove(channel_num)
- # Go through all event table entries and list gradient objects in the library
- all_grad_events = np.array(list(self.block_events.values()))
- all_grad_events = all_grad_events[:, 2:5]
- selected_events = np.unique(all_grad_events[:, channel_num])
- selected_events = selected_events[selected_events != 0]
- other_events = np.unique(all_grad_events[:, other_channels])
- if len(np.intersect1d(selected_events, other_events)) > 0:
- raise RuntimeError(
- "mod_grad_axis does not yet support the same gradient event used on multiple axes."
- )
- for i in range(len(selected_events)):
- self.grad_library.data[selected_events[i]][0] *= modifier
- if (
- self.grad_library.type[selected_events[i]] == "g"
- and self.grad_library.lengths[selected_events[i]] == 5
- ):
- # Need to update first and last fields
- self.grad_library.data[selected_events[i]][3] *= modifier
- self.grad_library.data[selected_events[i]][4] *= modifier
- def plot(
- self,
- label: str = str(),
- show_blocks: bool = False,
- save: bool = False,
- time_range=(0, np.inf),
- time_disp: str = "s",
- grad_disp: str = "kHz/m",
- plot_now: bool = True
- ) -> None:
- """
- Plot `Sequence`.
- Parameters
- ----------
- label : str, defualt=str()
- Plot label values for ADC events: in this example for LIN and REP labels; other valid labes are accepted as
- a comma-separated list.
- save : bool, default=False
- Boolean flag indicating if plots should be saved. The two figures will be saved as JPG with numerical
- suffixes to the filename 'seq_plot'.
- show_blocks : bool, default=False
- Boolean flag to indicate if grid and tick labels at the block boundaries are to be plotted.
- time_range : iterable, default=(0, np.inf)
- Time range (x-axis limits) for plotting the sequence. Default is 0 to infinity (entire sequence).
- time_disp : str, default='s'
- Time display type, must be one of `s`, `ms` or `us`.
- grad_disp : str, default='s'
- Gradient display unit, must be one of `kHz/m` or `mT/m`.
- plot_now : bool, default=True
- If true, function immediately shows the plots, blocking the rest of the code until plots are exited.
- If false, plots are shown when plt.show() is called. Useful if plots are to be modified.
- plot_type : str, default='Gradient'
- Gradients display type, must be one of either 'Gradient' or 'Kspace'.
- """
- mpl.rcParams["lines.linewidth"] = 0.75 # Set default Matplotlib linewidth
- valid_time_units = ["s", "ms", "us"]
- valid_grad_units = ["kHz/m", "mT/m"]
- valid_labels = get_supported_labels()
- if (
- not all([isinstance(x, (int, float)) for x in time_range])
- or len(time_range) != 2
- ):
- raise ValueError("Invalid time range")
- if time_disp not in valid_time_units:
- raise ValueError("Unsupported time unit")
- if grad_disp not in valid_grad_units:
- raise ValueError(
- "Unsupported gradient unit. Supported gradient units are: "
- + str(valid_grad_units)
- )
- fig1, fig2 = plt.figure(1), plt.figure(2)
- sp11 = fig1.add_subplot(311)
- sp12 = fig1.add_subplot(312, sharex=sp11)
- sp13 = fig1.add_subplot(313, sharex=sp11)
- fig2_subplots = [
- fig2.add_subplot(311, sharex=sp11),
- fig2.add_subplot(312, sharex=sp11),
- fig2.add_subplot(313, sharex=sp11),
- ]
- t_factor_list = [1, 1e3, 1e6]
- t_factor = t_factor_list[valid_time_units.index(time_disp)]
- g_factor_list = [1e-3, 1e3 / self.system.gamma]
- g_factor = g_factor_list[valid_grad_units.index(grad_disp)]
- t0 = 0
- label_defined = False
- label_idx_to_plot = []
- label_legend_to_plot = []
- label_store = dict()
- for i in range(len(valid_labels)):
- label_store[valid_labels[i]] = 0
- if valid_labels[i] in label.upper():
- label_idx_to_plot.append(i)
- label_legend_to_plot.append(valid_labels[i])
- if len(label_idx_to_plot) != 0:
- p = parula.main(len(label_idx_to_plot) + 1)
- label_colors_to_plot = p(np.arange(len(label_idx_to_plot)))
- label_colors_to_plot = np.roll(label_colors_to_plot, -1, axis=0).tolist()
- # Block timings
- block_edges = np.cumsum([0, *self.block_durations])
- block_edges_in_range = block_edges[
- (block_edges >= time_range[0]) * (block_edges <= time_range[1])
- ]
- if show_blocks:
- for sp in [sp11, sp12, sp13, *fig2_subplots]:
- sp.set_xticks(t_factor * block_edges_in_range)
- sp.set_xticklabels(rotation=90)
- for block_counter in range(len(self.block_events)):
- block = self.get_block(block_counter + 1)
- is_valid = time_range[0] <= t0 <= time_range[1]
- if is_valid:
- if getattr(block, "label", None) is not None:
- for i in range(len(block.label)):
- if block.label[i].type == "labelinc":
- label_store[block.label[i].label] += block.label[i].value
- else:
- label_store[block.label[i].label] = block.label[i].value
- label_defined = True
- if getattr(block, "adc", None) is not None: # ADC
- adc = block.adc
- # From Pulseq: According to the information from Klaus Scheffler and indirectly from Siemens this
- # is the present convention - the samples are shifted by 0.5 dwell
- t = adc.delay + (np.arange(int(adc.num_samples)) + 0.5) * adc.dwell
- sp11.plot(t_factor * (t0 + t), np.zeros(len(t)), "rx")
- sp13.plot(
- t_factor * (t0 + t),
- np.angle(
- np.exp(1j * adc.phase_offset)
- * np.exp(1j * 2 * np.pi * t * adc.freq_offset)
- ),
- "b.",
- markersize=0.25,
- )
- if label_defined and len(label_idx_to_plot) != 0:
- cycler = mpl.cycler(color=label_colors_to_plot)
- sp11.set_prop_cycle(cycler)
- label_colors_to_plot = np.roll(
- label_colors_to_plot, -1, axis=0
- ).tolist()
- arr_label_store = list(label_store.values())
- lbl_vals = np.take(arr_label_store, label_idx_to_plot)
- t = t0 + adc.delay + (adc.num_samples - 1) / 2 * adc.dwell
- _t = [t_factor * t] * len(lbl_vals)
- # Plot each label individually to retrieve each corresponding Line2D object
- p = itertools.chain.from_iterable(
- [
- sp11.plot(__t, _lbl_vals, ".")
- for __t, _lbl_vals in zip(_t, lbl_vals)
- ]
- )
- if len(label_legend_to_plot) != 0:
- sp11.legend(p, label_legend_to_plot, loc="upper left")
- label_legend_to_plot = []
- if getattr(block, "rf", None) is not None: # RF
- rf = block.rf
- tc, ic = calc_rf_center(rf)
- time = rf.t
- signal = rf.signal
- if np.abs(signal[0]) != 0:
- signal = np.insert(signal, obj=0, values=0)
- time = np.insert(time, obj=0, values=time[0])
- ic += 1
- if np.abs(signal[-1]) != 0:
- signal = np.append(signal, 0)
- time = np.append(time, time[-1])
- sp12.plot(t_factor * (t0 + time + rf.delay), np.abs(signal))
- sp13.plot(
- t_factor * (t0 + time + rf.delay),
- np.angle(
- signal
- * np.exp(1j * rf.phase_offset)
- * np.exp(1j * 2 * math.pi * time * rf.freq_offset)
- ),
- t_factor * (t0 + tc + rf.delay),
- np.angle(
- signal[ic]
- * np.exp(1j * rf.phase_offset)
- * np.exp(1j * 2 * math.pi * time[ic] * rf.freq_offset)
- ),
- "xb",
- )
- grad_channels = ["gx", "gy", "gz"]
- for x in range(len(grad_channels)): # Gradients
- if getattr(block, grad_channels[x], None) is not None:
- grad = getattr(block, grad_channels[x])
- if grad.type == "grad":
- # We extend the shape by adding the first and the last points in an effort of making the
- # display a bit less confusing...
- time = grad.delay + [0, *grad.tt, grad.shape_dur]
- waveform = g_factor * np.array(
- (grad.first, *grad.waveform, grad.last)
- )
- else:
- time = np.cumsum(
- [
- 0,
- grad.delay,
- grad.rise_time,
- grad.flat_time,
- grad.fall_time,
- ]
- )
- waveform = (
- g_factor * grad.amplitude * np.array([0, 0, 1, 1, 0])
- )
- fig2_subplots[x].plot(t_factor * (t0 + time), waveform)
- t0 += self.block_durations[block_counter]
- grad_plot_labels = ["x", "y", "z"]
- sp11.set_ylabel("ADC")
- sp12.set_ylabel("RF mag (Hz)")
- sp13.set_ylabel("RF/ADC phase (rad)")
- sp13.set_xlabel(f"t ({time_disp})")
- for x in range(3):
- _label = grad_plot_labels[x]
- fig2_subplots[x].set_ylabel(f"G{_label} ({grad_disp})")
- fig2_subplots[-1].set_xlabel(f"t ({time_disp})")
- # Setting display limits
- disp_range = t_factor * np.array([time_range[0], min(t0, time_range[1])])
- [x.set_xlim(disp_range) for x in [sp11, sp12, sp13, *fig2_subplots]]
- # Grid on
- for sp in [sp11, sp12, sp13, *fig2_subplots]:
- sp.grid()
- fig1.tight_layout()
- fig2.tight_layout()
- if save:
- fig1.savefig("seq_plot1.jpg")
- fig2.savefig("seq_plot2.jpg")
- if plot_now:
- plt.show()
- def ppval_numpy(self, arr_np_poly: np.ndarray, xq: np.ndarray):
- """
- Perform piece-wise polynomial evaluation on Numpy's polyfit objects.
- Parameters
- ==========
- arr_np_poly : Iterable[np.poly1d]
- xq : np.array
- Returns
- =======
- """
- result = []
- result2 = []
- breaks = np.array([p.domain for p in arr_np_poly])
- breaks = np.array([*breaks[::2].flatten(), breaks[-1, -1]])
- last_match = 0
- for x in xq:
- # Evaluate polynomial only when we find x's corresponding pp window
- for i in range(last_match, len(arr_np_poly)):
- corresponding_pp = x > arr_np_poly[i].domain
- if corresponding_pp.tolist() == [True, False]:
- value = np.polynomial.polynomial.polyval(x, arr_np_poly[i].coef)
- result.append(value)
- last_match = i
- break
- corresponding_pp2 = np.nonzero((x < breaks))[0][0]
- value2 = np.polynomial.polynomial.polyval(
- x, arr_np_poly[corresponding_pp2].coef
- )
- result2.append(value2)
- return result
- def ppval_MATLAB(self, arr_MATLAB_poly: np.ndarray, xq: np.ndarray) -> np.array:
- """
- Perform piece-wise polynomial evaluation on MATLAB's polyfit objects.
- Returns
- =======
- """
- def __ppval(c, x, b, i, n):
- return c[n] * (x - b) ** i + __ppval(c, x, b, i + 1, n - 1) if n >= 0 else 0
- coefs = np.array([p.coef for p in arr_MATLAB_poly])
- breaks = np.array([p.domain for p in arr_MATLAB_poly])
- breaks = np.array([*breaks[::2].flatten(), breaks[-1, -1]])
- result = []
- for x in xq:
- corresponding_pp = np.nonzero((x < breaks))[0][0]
- c = coefs[corresponding_pp - 1]
- b = breaks[corresponding_pp - 1]
- # c = coefs[2]
- # res = c[0] * (x - breaks[2]) ** 2 + c[1] * (x - breaks[2]) + c[2]
- res = __ppval(c, x, b, i=0, n=len(c) - 1)
- result.append(res)
- return np.array(result)
- def read(self, file_path: str, detect_rf_use: bool = False) -> None:
- """
- Read `.seq` file from `file_path`.
- Parameters
- ----------
- detect_rf_use
- file_path : str
- Path to `.seq` file to be read.
- """
- read(self, path=file_path, detect_rf_use=detect_rf_use)
- def register_adc_event(self, event: EventLibrary) -> int:
- return block.register_adc_event(self, event)
- def register_grad_event(
- self, event: SimpleNamespace
- ) -> Union[int, Tuple[int, int]]:
- return block.register_grad_event(self, event)
- def register_label_event(self, event: SimpleNamespace) -> int:
- return block.register_label_event(self, event)
- def register_rf_event(self, event: SimpleNamespace) -> Tuple[int, List[int]]:
- return block.register_rf_event(self, event)
- def rf_from_lib_data(self, lib_data: list, use: str = str()) -> SimpleNamespace:
- """
- Construct RF object from `lib_data`.
- Parameters
- ----------
- lib_data : list
- RF envelope.
- use : str, default=str()
- RF event use.
- Returns
- -------
- rf : SimpleNamespace
- RF object constructed from `lib_data`.
- """
- rf = SimpleNamespace()
- rf.type = "rf"
- amplitude, mag_shape, phase_shape = lib_data[0], lib_data[1], lib_data[2]
- shape_data = self.shape_library.data[mag_shape]
- compressed = SimpleNamespace()
- compressed.num_samples = shape_data[0]
- compressed.data = shape_data[1:]
- mag = decompress_shape(compressed)
- shape_data = self.shape_library.data[phase_shape]
- compressed.num_samples = shape_data[0]
- compressed.data = shape_data[1:]
- phase = decompress_shape(compressed)
- rf.signal = amplitude * mag * np.exp(1j * 2 * np.pi * phase)
- time_shape = lib_data[3]
- if time_shape > 0:
- shape_data = self.shape_library.data[time_shape]
- compressed.num_samples = shape_data[0]
- compressed.data = shape_data[1:]
- rf.t = decompress_shape(compressed) * self.rf_raster_time
- rf.shape_dur = (
- np.ceil((rf.t[-1] - eps) / self.rf_raster_time) * self.rf_raster_time
- )
- else: # Generate default time raster on the fly
- rf.t = (np.arange(1, len(rf.signal) + 1) - 0.5) * self.rf_raster_time
- rf.shape_dur = len(rf.signal) * self.rf_raster_time
- rf.delay = lib_data[4]
- rf.freq_offset = lib_data[5]
- rf.phase_offset = lib_data[6]
- rf.dead_time = self.system.rf_dead_time
- rf.ringdown_time = self.system.rf_ringdown_time
- if use != "":
- use_cases = {
- "e": "excitation",
- "r": "refocusing",
- "i": "inversion",
- "s": "saturation",
- "p": "preparation",
- }
- rf.use = use_cases[use] if use in use_cases else "undefined"
- return rf
- def set_block(self, block_index: int, *args: SimpleNamespace) -> None:
- """
- Replace block at index with new block provided as block structure, add sequence block, or create a new block
- from events and store at position specified by index. The block or events are provided in uncompressed form and
- will be stored in the compressed, non-redundant internal libraries.
- See also:
- - `pypulseq.Sequence.sequence.Sequence.get_block()`
- - `pypulseq.Sequence.sequence.Sequence.add_block()`
- Parameters
- ----------
- block_index : int
- Index at which block is replaced.
- args : SimpleNamespace
- Block or events to be replaced/added or created at `block_index`.
- """
- block.set_block(self, block_index, *args)
- def set_definition(
- self, key: str, value: Union[float, int, list, np.ndarray, str, tuple]
- ) -> None:
- """
- Modify a custom definition of the sequence. Set the user definition 'key' to value 'value'. If the definition
- does not exist it will be created.
- See also `pypulseq.Sequence.sequence.Sequence.get_definition()`.
- Parameters
- ----------
- key : str
- Definition key.
- value : int, list, np.ndarray, str or tuple
- Definition value.
- """
- if key == "FOV":
- if np.max(value) > 1:
- text = "Definition FOV uses values exceeding 1 m. "
- text += "New Pulseq interpreters expect values in units of meters."
- warn(text)
- self.definitions[key] = value
- def set_extension_string_ID(self, extension_str: str, extension_id: int) -> None:
- """
- Set numeric ID for the given string extension ID.
- Parameters
- ----------
- extension_str : str
- Given string extension ID.
- extension_id : int
- Given numeric extension ID.
- Raises
- ------
- ValueError
- If given numeric or string extension ID is not unique.
- """
- if (
- extension_str in self.extension_string_idx
- or extension_id in self.extension_numeric_idx
- ):
- raise ValueError("Numeric or string ID is not unique")
- self.extension_numeric_idx.append(extension_id)
- self.extension_string_idx.append(extension_str)
- assert len(self.extension_numeric_idx) == len(self.extension_string_idx)
- def test_report(self) -> str:
- """
- Analyze the sequence and return a text report.
- """
- return ext_test_report(self)
- def waveforms_and_times(
- self, append_RF: bool = False
- ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
- """
- Decompress the entire gradient waveform. Returns gradient waveforms as a tuple of `np.ndarray` of
- `gradient_axes` (typically 3) dimensions. Each `np.ndarray` contains timepoints and the corresponding
- gradient amplitude values. Additional return values are time points of excitations, refocusings and ADC
- sampling points.
- Parameters
- ----------
- append_RF : bool, default=False
- Boolean flag to indicate if RF wave shapes are to be appended after the gradients.
- Returns
- -------
- wave_data : np.ndarray
- tfp_excitation : np.ndarray
- Contains time moments, frequency and phase offsets of the excitation RF pulses (similar for `
- tfp_refocusing`).
- tfp_refocusing : np.ndarray
- t_adc: np.ndarray
- Contains times of all ADC sample points.
- fp_adc : np.ndarray
- Contains frequency and phase offsets of each ADC object (not samples).
- """
- grad_channels = ["gx", "gy", "gz"]
- num_blocks = len(self.block_events)
- # Collect shape pieces
- if append_RF:
- shape_channels = len(grad_channels) + 1 # Last 'channel' is RF
- else:
- shape_channels = len(grad_channels)
- shape_pieces = np.empty((shape_channels, num_blocks), dtype="object")
- # Also collect RF and ADC timing data
- # t_excitation, t_refocusing, t_adc
- tfp_excitation = []
- tfp_refocusing = []
- t_adc = []
- fp_adc = []
- curr_dur = 0
- out_len = np.zeros(shape_channels) # Last 'channel' is RF
- for block_counter in range(num_blocks):
- block = self.get_block(block_counter + 1)
- for j in range(len(grad_channels)):
- grad = getattr(block, grad_channels[j])
- if grad is not None: # Gradients
- if grad.type == "grad":
- # Check if we have an extended trapezoid or an arbitrary gradient on a regular raster
- tt_rast = grad.tt / self.grad_raster_time + 0.5
- if np.all(
- np.abs(tt_rast - np.arange(1, len(tt_rast) + 1)) < eps
- ): # Arbitrary gradient
- """
- Arbitrary gradient: restore & recompress shape - if we had a trapezoid converted to shape we
- have to find the "corners" and we can eliminate internal samples on the straight segments
- but first we have to restore samples on the edges of the gradient raster intervals for that
- we need the first sample.
- """
- max_abs = np.max(np.abs(grad.waveform))
- odd_step1 = np.array([grad.first, *(2 * grad.waveform)])
- odd_step2 = odd_step1 * (
- np.mod(np.arange(1, len(odd_step1) + 1), 2) * 2 - 1
- )
- waveform_odd_rest = np.cumsum(odd_step2) * (
- np.mod(np.arange(1, len(odd_step2)), 2) * 2 - 1
- )
- else: # Extended trapezoid
- out_len[j] += len(grad.tt)
- shape_pieces[j, block_counter] = np.array(
- [
- curr_dur + grad.delay + grad.tt,
- grad.waveform,
- ]
- )
- else:
- if np.abs(grad.flat_time) > eps:
- out_len[j] += 4
- _temp = np.vstack(
- (
- curr_dur
- + grad.delay
- + np.cumsum(
- [
- 0,
- grad.rise_time,
- grad.flat_time,
- grad.fall_time,
- ]
- ),
- grad.amplitude * np.array([0, 1, 1, 0]),
- )
- )
- shape_pieces[j, block_counter] = _temp
- else:
- out_len[j] += 3
- _temp = np.vstack(
- (
- curr_dur
- + grad.delay
- + np.cumsum([0, grad.rise_time, grad.fall_time]),
- grad.amplitude * np.array([0, 1, 0]),
- )
- )
- shape_pieces[j, block_counter] = _temp
- if block.rf is not None: # RF
- rf = block.rf
- t = rf.delay + calc_rf_center(rf)[0]
- if not hasattr(rf, "use") or block.rf.use in [
- "excitation",
- "undefined",
- ]:
- tfp_excitation.append(
- [curr_dur + t, block.rf.freq_offset, block.rf.phase_offset]
- )
- elif block.rf.use == "refocusing":
- tfp_refocusing.append(
- [curr_dur + t, block.rf.freq_offset, block.rf.phase_offset]
- )
- if append_RF:
- rf_piece = np.array(
- [
- curr_dur + rf.delay + rf.t,
- rf.signal
- * np.exp(
- 1j
- * (rf.phase_offset + 2 * np.pi * rf.freq_offset * rf.t)
- ),
- ]
- )
- out_len[-1] += len(rf.t)
- if np.abs(rf.signal[0]) > 0:
- pre = np.array([[curr_dur + rf.delay + rf.t[0] - eps], [0]])
- rf_piece = np.hstack((pre, rf_piece))
- out_len[-1] += pre.shape[1]
- if np.abs(rf.signal[-1]) > 0:
- post = np.array([[curr_dur + rf.delay + rf.t[-1] + eps], [0]])
- rf_piece = np.hstack((rf_piece, post))
- out_len[-1] += post.shape[1]
- shape_pieces[-1, block_counter] = rf_piece
- if block.adc is not None: # ADC
- t_adc.extend(
- np.arange(block.adc.num_samples) * block.adc.dwell
- + 0.5 * block.adc.dwell
- + block.adc.delay
- + curr_dur
- )
- fp_adc.append([block.adc.freq_offset, block.adc.phase_offset])
- curr_dur += self.block_durations[block_counter]
- # Collect wave data
- wave_data = np.empty(shape_channels, dtype="object")
- for j in range(shape_channels):
- wave_data[j] = np.zeros((2, int(out_len[j])))
- # TODO: This is weird, and needs to be fixed. Time points are also complex this way, and spends 4 times more memory than necessary.
- if append_RF:
- wave_data[j] = np.zeros((2, int(out_len[j])), dtype=np.complex128)
- wave_cnt = np.zeros(shape_channels, dtype=int)
- for block_counter in range(num_blocks):
- for j in range(shape_channels):
- if shape_pieces[j, block_counter] is not None:
- wave_data_local = shape_pieces[j, block_counter]
- length = wave_data_local.shape[1]
- if (
- wave_cnt[j] == 0
- or wave_data[j][0, wave_cnt[j] - 1] != wave_data_local[0, 0]
- ):
- wave_data[j][
- :, wave_cnt[j] + np.arange(length)
- ] = wave_data_local
- wave_cnt[j] += length
- else:
- wave_data[j][
- :, wave_cnt[j] + np.arange(length - 1)
- ] = wave_data_local[:, 1:]
- wave_cnt[j] += length - 1
- if wave_cnt[j] != len(np.unique(wave_data[j][0, : wave_cnt[j]])):
- raise Warning(
- "Not all elements of the generated time vector are unique."
- )
- # Trim output data
- for j in range(shape_channels):
- if wave_cnt[j] < wave_data[j].shape[1]:
- wave_data[j] = wave_data[j][:, : wave_cnt[j]]
- tfp_excitation = np.array(tfp_excitation).transpose()
- tfp_refocusing = np.array(tfp_refocusing)
- t_adc = np.array(t_adc)
- fp_adc = np.array(fp_adc)
- return wave_data, tfp_excitation, tfp_refocusing, t_adc, fp_adc
- def waveforms_export(self, time_range=(0, np.inf)) -> dict:
- """
- Plot `Sequence`.
- Parameters
- ----------
- time_range : iterable, default=(0, np.inf)
- Time range (x-axis limits) for all waveforms. Default is 0 to infinity (entire sequence).
- Returns
- -------
- all_waveforms: dict
- Dictionary containing the following sequence waveforms and time array(s):
- - `t_adc` - ADC timing array [seconds]
- - `t_rf` - RF timing array [seconds]
- - `t_rf_centers`: `rf_t_centers`,
- - `t_gx`: x gradient timing array,
- - `t_gy`: y gradient timing array,
- - `t_gz`: z gradient timing array,
- - `adc` - ADC complex signal (amplitude=1, phase=adc phase) [a.u.]
- - `rf` - RF complex signal
- - `rf_centers`: RF centers array,
- - `gx` - x gradient
- - `gy` - y gradient
- - `gz` - z gradient
- - `grad_unit`: [kHz/m],
- - `rf_unit`: [Hz],
- - `time_unit`: [seconds],
- """
- # Check time range validity
- if (
- not all([isinstance(x, (int, float)) for x in time_range])
- or len(time_range) != 2
- ):
- raise ValueError("Invalid time range")
- t0 = 0
- adc_t_all = np.array([])
- adc_signal_all = np.array([], dtype=complex)
- rf_t_all = np.array([])
- rf_signal_all = np.array([], dtype=complex)
- rf_t_centers = np.array([])
- rf_signal_centers = np.array([], dtype=complex)
- gx_t_all = np.array([])
- gy_t_all = np.array([])
- gz_t_all = np.array([])
- gx_all = np.array([])
- gy_all = np.array([])
- gz_all = np.array([])
- for block_counter in range(len(self.dict_block_events)): # For each block
- block = self.get_block(block_counter + 1) # Retrieve it
- is_valid = (
- time_range[0] <= t0 <= time_range[1]
- ) # Check if "current time" is within requested range.
- if is_valid:
- # Case 1: ADC
- if hasattr(block, "adc"):
- adc = block.adc # Get adc info
- # From Pulseq: According to the information from Klaus Scheffler and indirectly from Siemens this
- # is the present convention - the samples are shifted by 0.5 dwell
- t = adc.delay + (np.arange(int(adc.num_samples)) + 0.5) * adc.dwell
- adc_t = t0 + t
- adc_signal = np.exp(1j * adc.phase_offset) * np.exp(
- 1j * 2 * np.pi * t * adc.freq_offset
- )
- adc_t_all = np.append(adc_t_all, adc_t)
- adc_signal_all = np.append(adc_signal_all, adc_signal)
- if hasattr(block, "rf"):
- rf = block.rf
- tc, ic = calc_rf_center(rf)
- t = rf.t + rf.delay
- tc = tc + rf.delay
- # Debug - visualize
- # sp12.plot(t_factor * (t0 + t), np.abs(rf.signal))
- # sp13.plot(t_factor * (t0 + t), np.angle(rf.signal * np.exp(1j * rf.phase_offset)
- # * np.exp(1j * 2 * math.pi * rf.t * rf.freq_offset)),
- # t_factor * (t0 + tc), np.angle(rf.signal[ic] * np.exp(1j * rf.phase_offset)
- # * np.exp(1j * 2 * math.pi * rf.t[ic] * rf.freq_offset)),
- # 'xb')
- rf_t = t0 + t
- rf = (
- rf.signal
- * np.exp(1j * rf.phase_offset)
- * np.exp(1j * 2 * math.pi * rf.t * rf.freq_offset)
- )
- rf_t_all = np.append(rf_t_all, rf_t)
- rf_signal_all = np.append(rf_signal_all, rf)
- rf_t_centers = np.append(rf_t_centers, rf_t[ic])
- rf_signal_centers = np.append(rf_signal_centers, rf[ic])
- grad_channels = ["gx", "gy", "gz"]
- for x in range(
- len(grad_channels)
- ): # Check each gradient channel: x, y, and z
- if hasattr(
- block, grad_channels[x]
- ): # If this channel is on in current block
- grad = getattr(block, grad_channels[x])
- if grad.type == "grad": # Arbitrary gradient option
- # In place unpacking of grad.t with the starred expression
- g_t = (
- t0
- + grad.delay
- + [
- 0,
- *(grad.t + (grad.t[1] - grad.t[0]) / 2),
- grad.t[-1] + grad.t[1] - grad.t[0],
- ]
- )
- g = 1e-3 * np.array((grad.first, *grad.waveform, grad.last))
- else: # Trapezoid gradient option
- g_t = t0 + np.cumsum(
- [
- 0,
- grad.delay,
- grad.rise_time,
- grad.flat_time,
- grad.fall_time,
- ]
- )
- g = 1e-3 * grad.amplitude * np.array([0, 0, 1, 1, 0])
- if grad.channel == "x":
- gx_t_all = np.append(gx_t_all, g_t)
- gx_all = np.append(gx_all, g)
- elif grad.channel == "y":
- gy_t_all = np.append(gy_t_all, g_t)
- gy_all = np.append(gy_all, g)
- elif grad.channel == "z":
- gz_t_all = np.append(gz_t_all, g_t)
- gz_all = np.append(gz_all, g)
- t0 += self.arr_block_durations[
- block_counter
- ] # "Current time" gets updated to end of block just examined
- all_waveforms = {
- "t_adc": adc_t_all,
- "t_rf": rf_t_all,
- "t_rf_centers": rf_t_centers,
- "t_gx": gx_t_all,
- "t_gy": gy_t_all,
- "t_gz": gz_t_all,
- "adc": adc_signal_all,
- "rf": rf_signal_all,
- "rf_centers": rf_signal_centers,
- "gx": gx_all,
- "gy": gy_all,
- "gz": gz_all,
- "grad_unit": "[kHz/m]",
- "rf_unit": "[Hz]",
- "time_unit": "[seconds]",
- }
- return all_waveforms
- def write(self, name: str, create_signature: bool = True) -> None:
- """
- Write the sequence data to the given filename using the open file format for MR sequences.
- See also `pypulseq.Sequence.read_seq.read()`.
- Parameters
- ----------
- name : str
- Filename of `.seq` file to be written to disk.
- create_signature : bool, default=True
- Boolean flag to indicate if the file has to be signed.
- """
- write_seq(self, name, create_signature)
|