seqgen_TSE_NIRSII.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. # ---------------------------------------------------------------------
  2. # imports of the libraries
  3. # ---------------------------------------------------------------------
  4. import numpy as np
  5. import math
  6. # Важно: класс Sequence конфликтует по имени с подпакетом seqgen.pypulseq.Sequence,
  7. # что может приводить к ошибке "'module' object is not callable" при вызове Sequence(...)
  8. # Импортируем класс напрямую из файла определения, чтобы избежать коллизии с модулем.
  9. from seqgen.pypulseq.Sequence.sequence import Sequence
  10. from seqgen.pypulseq.calc_rf_center import calc_rf_center
  11. from seqgen.pypulseq.calc_duration import calc_duration
  12. from seqgen.pypulseq.make_adc import make_adc
  13. from seqgen.pypulseq.make_delay import make_delay
  14. from seqgen.pypulseq.make_sinc_pulse import make_sinc_pulse
  15. from seqgen.pypulseq.make_trapezoid import make_trapezoid
  16. from seqgen.pypulseq.opts import Opts
  17. from seqgen.utilities import phase_grad_utils as pgu
  18. from seqgen.utilities.magn_prep.magn_prep import magn_prep_calc, magn_prep_add
  19. from seqgen.utilities.TSE_k_space_fill import TSE_k_space_fill
  20. from seqgen.utilities.standart_RF import refocusing_grad
  21. from seqgen.utilities.standart_RF import readout_grad_x
  22. def seqgen_TSE_NIRSII(params):
  23. # --------------------------
  24. # Set system limits
  25. # --------------------------
  26. scanner_parameters = Opts(
  27. max_grad=params.G_amp_max, grad_unit="Hz/m",
  28. max_slew=params.G_slew_max, slew_unit="Hz/m/s",
  29. rf_ringdown_time=params.rf_ringdown_time,
  30. rf_dead_time=params.rf_dead_time,
  31. adc_dead_time=params.adc_dead_time,
  32. rf_raster_time=params.rf_raster_time,
  33. grad_raster_time=params.grad_raster_time,
  34. block_duration_raster=params.grad_raster_time,
  35. adc_raster_time=1 / (params.BW_pixel * params.Nf)
  36. )
  37. # Защита от редких коллизий: если по какой-то причине импортировалcя модуль, а не класс,
  38. # явно подтянем класс Sequence и используем его.
  39. SequenceClass = Sequence
  40. try:
  41. # type: ignore[attr-defined]
  42. _is_class = hasattr(SequenceClass, '__call__') and hasattr(SequenceClass, '__dict__')
  43. if not _is_class:
  44. from seqgen.pypulseq.Sequence.sequence import Sequence as _Seq
  45. SequenceClass = _Seq
  46. except Exception:
  47. from seqgen.pypulseq.Sequence.sequence import Sequence as _Seq
  48. SequenceClass = _Seq
  49. seq = SequenceClass(scanner_parameters)
  50. # --------------------
  51. # additional parameters
  52. # ---------------------
  53. united = True # splited of combined gradients
  54. if params.D_scans == 0:
  55. D_scans = False
  56. else:
  57. D_scans = True
  58. params.dixon_delta_te = 1.1e-3 # in ms #TODO add to GUI
  59. params.Dixon = True
  60. # --------------------
  61. # quantify timings
  62. # ---------------------
  63. readout_time = round(1 / params.BW_pixel, 8)
  64. t_exwd = params.t_ex + 2*scanner_parameters.rf_dead_time
  65. t_refwd = params.t_ref + 2*scanner_parameters.rf_dead_time
  66. t_sp = 0.5 * (params.ES - readout_time - t_refwd)
  67. t_sp = scanner_parameters.grad_raster_time * np.round(t_sp / scanner_parameters.grad_raster_time)
  68. f_sp = 0.75 # area factor for spoiler gradient
  69. t_spex = 0.5 * (params.ES - t_exwd - t_refwd)
  70. t_spex = scanner_parameters.grad_raster_time * np.round(t_spex / scanner_parameters.grad_raster_time)
  71. f_spex = f_sp - 0.5
  72. # =============================================================================
  73. # RF & SS Gradients
  74. # =============================================================================
  75. pulse_bandwidth = np.double(params.t_BW_product_ex) / np.double(params.t_ex) # In Hz
  76. gradient_slice_amp = np.double(pulse_bandwidth) / np.double(params.sl_thkn) # In Hz/m
  77. pulse_freq_offsets90 = (np.linspace(0.0, params.sl_nb - 1.0, np.int16(params.sl_nb)) - 0.5 * (
  78. np.double(params.sl_nb) - 1.0)) * (params.sl_thkn * (100.0 + params.sl_gap) / 100.0) * gradient_slice_amp
  79. # Common gradient parameters
  80. rf90_phase = np.pi / 2
  81. rf180_phase = 0
  82. flip90 = params.FA * np.pi / 180
  83. flip180 = params.RA * np.pi / 180 # TODO ad parameter to GUI
  84. rf90, gz_ex, _ = make_sinc_pulse(flip_angle=flip90, system=scanner_parameters, duration=params.t_ex,
  85. slice_thickness=params.sl_thkn, apodization=params.apodization,
  86. time_bw_product=round(params.t_BW_product_ex, 8), phase_offset=rf90_phase,
  87. return_gz=True, use="excitation", delay = scanner_parameters.rf_dead_time)
  88. rf90.delay = params.dG + scanner_parameters.rf_dead_time
  89. gz90 = make_trapezoid(channel="z", system=scanner_parameters, amplitude=gz_ex.amplitude,
  90. flat_time=t_exwd, rise_time=params.dG)
  91. gz_reph = make_trapezoid(channel="z", system=scanner_parameters, area=-gz90.area * 0.5,
  92. duration=t_spex, rise_time=params.dG)
  93. # spoil gradient G_sps
  94. gz_cr = make_trapezoid(channel='z', system=scanner_parameters, area=gz90.area * 4, rise_time=params.dG)
  95. #Refocusing_gradient
  96. rf180, gz_spoil1, gz180, gz_spoil2 = refocusing_grad(params, scanner_parameters,
  97. f_sp * gz90.area, flip180, rf180_phase, t_refwd,
  98. spoil_duration=t_sp, united=united)
  99. rf180.delay = scanner_parameters.rf_dead_time
  100. pulse_freq_offsets180 = (np.linspace(0.0, params.sl_nb - 1.0, np.int16(params.sl_nb)) - 0.5 * (np.double(params.sl_nb) - 1.0)) * (params.sl_thkn * (100.0 + params.sl_gap) / 100.0) * gz180.first
  101. gx_spoil1, gx, gx_spoil2, t_gx, gx_pre = readout_grad_x(params, scanner_parameters,
  102. spoil_duration=t_sp,
  103. rewind_duration=t_spex, united=united,
  104. phi_radial=0, phase_encoding_area=0,
  105. dixon_delta_te=0)
  106. adc = make_adc(num_samples=params.Nf, duration=t_gx,
  107. delay=scanner_parameters.adc_dead_time, system=scanner_parameters)
  108. # =============================================================================
  109. # k-space filling quantification
  110. # =============================================================================
  111. k_phase = np.double(params.Np) / np.double(params.FoV_p)
  112. k_steps_PE = pgu.create_k_steps_PI(k_phase, np.int16(params.Np),
  113. np.int16(params.PI_factor)) # list of phase encoding gradients
  114. zero_indices = np.where(k_steps_PE == 0.0)[0]
  115. if zero_indices.size > 0:
  116. N_center = zero_indices[0]
  117. else:
  118. N_center = np.argmin(np.abs(k_steps_PE))
  119. k_steps_PE = k_steps_PE[0:np.int16(np.round(len(k_steps_PE)))]
  120. n_ex = math.ceil(len(k_steps_PE) / params.ETL) # number of excitations with partial Fourier
  121. # k_space_order_filing = TSE_k_space_fill(n_ex, np.int32(params.ETL), np.int32(params.Np), np.int32(params.N_TE), 'non_linear') # number of excitations w/o partial Fourier
  122. k_space_order_filing = TSE_k_space_fill(n_ex, np.int32(params.ETL), len(k_steps_PE),
  123. np.int32(params.N_TE), N_center, 'non_linear',
  124. dummy=D_scans, N_dummy=params.D_scans)
  125. k_space_save = {'k_space_order': k_space_order_filing}
  126. magn_prep_objects, magn_prep_dur = magn_prep_calc(vars(params),
  127. scanner_parameters)
  128. # =============================================================================
  129. # block duration quant
  130. # =============================================================================
  131. # TODO:quantify block duration and TE together with d
  132. block_duration = 0
  133. block_duration += magn_prep_dur
  134. block_duration += calc_duration(gz90)
  135. block_duration += calc_duration(gz_reph)
  136. #block_duration += params.small_delta + 2 * params.dG
  137. #block_duration += calc_duration(delay_diff_empt)
  138. block_duration += gz_spoil1.shape_dur
  139. block_duration += gz180.shape_dur
  140. block_duration += gz_spoil2.shape_dur
  141. #block_duration += calc_duration(delay_diff_empt)
  142. #block_duration += params.small_delta + 2 * params.dG
  143. block_duration += gz_spoil1.shape_dur
  144. for i in range(np.int32(params.ETL) - 1):
  145. block_duration += gz180.shape_dur
  146. block_duration += gz_spoil2.shape_dur
  147. block_duration += gx.shape_dur
  148. block_duration += gz_spoil1.shape_dur
  149. block_duration += gz180.shape_dur
  150. block_duration += gz_spoil2.shape_dur
  151. block_duration += gx.shape_dur
  152. block_duration += calc_duration(gz_cr)
  153. # =============================================================================
  154. # Construct concatinations
  155. # =============================================================================
  156. eff_time = block_duration # equal to previous!
  157. max_slices_per_TR = np.floor(params.TR / eff_time)
  158. if max_slices_per_TR == 0: #TODO: check GUI
  159. max_slices_per_TR = 1
  160. # required_concats = np.int32(np.ceil(params.sl_nb / max_slices_per_TR)) #maximum possible
  161. required_concats = params.concats
  162. slice_list1 = list(range(0, np.int32(params.sl_nb), 2)) #TODO: linear and interleaved slices
  163. slice_list2 = list(range(1, np.int32(params.sl_nb), 2))
  164. slice_list = slice_list1 + slice_list2
  165. slice_list = [slice_list[x::required_concats] for x in range(required_concats)]
  166. # Calculate the TR fillers
  167. tr_pauses = [(params.TR / np.double(len(x))) - eff_time for x in slice_list]
  168. tr_pauses = [
  169. max(seq.grad_raster_time, seq.rf_raster_time) * np.floor(x / max(seq.grad_raster_time, seq.rf_raster_time)) for
  170. x in tr_pauses]
  171. # Generate the TR fillers
  172. tr_fillers = [make_delay(x) for x in tr_pauses]
  173. # -----------------------------------------------------------------------------
  174. # =============================================================================
  175. # Construct Sequence
  176. # =============================================================================
  177. timing_quant = 0
  178. for k in range(params.average): # Averages
  179. for curr_concat in range(required_concats):
  180. for phase_steps in k_space_order_filing: # instead of phase steps list of phase steps
  181. # for curr_slice in range(np.int32(params.sl_nb)): # Slices
  182. for curr_slice in slice_list[curr_concat]:
  183. seq = magn_prep_add(magn_prep_objects, seq, curr_slice,
  184. only_one='inv_prep') # magn prep block
  185. # Apply RF offsets
  186. n_echo_temp = 0
  187. rf90.freq_offset = pulse_freq_offsets90[curr_slice]
  188. rf180.freq_offset = pulse_freq_offsets180[curr_slice]
  189. rf90.phase_offset = (rf90_phase - 2 * np.pi * rf90.freq_offset * calc_rf_center(rf90)[0])
  190. rf180.phase_offset = (rf180_phase - 2 * np.pi * rf180.freq_offset * calc_rf_center(rf180)[0])
  191. seq.add_block(rf90, gz90)
  192. _ ,gz_spoil1_first, _, _ = refocusing_grad(params, scanner_parameters,
  193. f_spex * gz90.area, flip180,
  194. rf180_phase, t_refwd,
  195. spoil_duration=t_spex, united=united)
  196. seq.add_block(gz_spoil1_first, gx_pre)
  197. timing_quant += calc_duration(gx_pre)
  198. for phase_step in phase_steps:
  199. # print('phase step_' + str(phase_step))
  200. seq.add_block(gz180, rf180)
  201. timing_quant += gz180.shape_dur
  202. if phase_step == 'skip' or phase_step == 'dummy':
  203. gy_pre = make_trapezoid(channel='y', system=scanner_parameters,
  204. area=0, duration=calc_duration(gz_spoil2),
  205. rise_time=params.dG)
  206. else:
  207. gy_pre = make_trapezoid(channel='y', system=scanner_parameters,
  208. area=k_steps_PE[phase_step], duration=calc_duration(gz_spoil2),
  209. rise_time=params.dG)
  210. # print(k_steps_PE[phase_step])
  211. seq.add_block(gz_spoil2, gx_spoil1, gy_pre)
  212. timing_quant += calc_duration(gy_pre)
  213. if phase_step == 'skip' or phase_step == 'dummy':
  214. # seq.add_block(gx)
  215. timing_quant += gx.shape_dur
  216. else:
  217. seq.add_block(gx, adc)
  218. timing_quant += gx.shape_dur / 2
  219. timing_quant += gx.shape_dur / 2
  220. n_echo_temp += 1
  221. if phase_step == 'skip' or phase_step == 'dummy':
  222. gy_pre = make_trapezoid(channel='y', system=scanner_parameters,
  223. area=-0, duration=calc_duration(gz_spoil2),
  224. rise_time=params.dG)
  225. else:
  226. gy_pre = make_trapezoid(channel='y', system=scanner_parameters,
  227. area=-k_steps_PE[phase_step], duration=calc_duration(gz_spoil2),
  228. rise_time=params.dG)
  229. if n_echo_temp == np.int32(params.ETL):
  230. seq.add_block(gz_cr, gx_spoil2, gy_pre)
  231. timing_quant += calc_duration(gz_cr)
  232. else:
  233. seq.add_block(gz_spoil1, gx_spoil2, gy_pre)
  234. timing_quant += gx_spoil2.shape_dur
  235. seq.add_block(tr_fillers[curr_concat])
  236. timing_quant += calc_duration(tr_fillers[curr_concat])
  237. ok, error_report = seq.check_timing()
  238. if ok:
  239. print("Timing check passed successfully")
  240. else:
  241. print("Timing check failed. Error listing follows:")
  242. [print(e) for e in error_report]
  243. # ======
  244. # VISUALIZATION
  245. # ======
  246. return seq, k_space_save