add_gradients.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. from copy import deepcopy
  2. from types import SimpleNamespace
  3. from typing import Iterable
  4. import numpy as np
  5. from seqgen.pypulseq import eps
  6. from seqgen.pypulseq.calc_duration import calc_duration
  7. from seqgen.pypulseq.make_arbitrary_grad import make_arbitrary_grad
  8. from seqgen.pypulseq.make_extended_trapezoid import make_extended_trapezoid
  9. from seqgen.pypulseq.opts import Opts
  10. from seqgen.pypulseq.points_to_waveform import points_to_waveform
  11. def add_gradients(
  12. grads: Iterable[SimpleNamespace],
  13. max_grad: int = 0,
  14. max_slew: int = 0,
  15. system=Opts(),
  16. ) -> SimpleNamespace:
  17. """
  18. Returns the superposition of several gradients.
  19. Parameters
  20. ----------
  21. grads : [SimpleNamespace, ...]
  22. Gradient events.
  23. system : Opts, default=Opts()
  24. System limits.
  25. max_grad : float, default=0
  26. Maximum gradient amplitude.
  27. max_slew : float, default=0
  28. Maximum slew rate.
  29. Returns
  30. -------
  31. grad : SimpleNamespace
  32. Superimposition of gradient events from `grads`.
  33. """
  34. # copy() to emulate pass-by-value; otherwise passed grad events are modified
  35. grads = deepcopy(grads)
  36. if max_grad <= 0:
  37. max_grad = system.max_grad
  38. if max_slew <= 0:
  39. max_slew = system.max_slew
  40. if len(grads) < 2:
  41. raise ValueError("Cannot add less than two gradients")
  42. # First gradient defines channel
  43. channel = grads[0].channel
  44. # Find out the general delay of all gradients and other statistics
  45. delays, firsts, lasts, durs, is_trap, is_arb = [], [], [], [], [], []
  46. for ii in range(len(grads)):
  47. if grads[ii].channel != channel:
  48. raise ValueError("Cannot add gradients on different channels.")
  49. delays.append(grads[ii].delay)
  50. firsts.append(grads[ii].first)
  51. lasts.append(grads[ii].last)
  52. durs.append(calc_duration(grads[ii]))
  53. is_trap.append(grads[ii].type == "trap")
  54. if is_trap[-1]:
  55. is_arb.append(False)
  56. else:
  57. tt_rast = grads[ii].tt / system.grad_raster_time + 0.5
  58. is_arb.append(np.all(np.abs(tt_rast - np.arange(len(tt_rast)))) < eps)
  59. # Convert to numpy.ndarray for fancy-indexing later on
  60. firsts, lasts = np.array(firsts), np.array(lasts)
  61. common_delay = np.min(delays)
  62. total_duration = np.max(durs)
  63. # Check if we have a set of traps with the same timing
  64. if np.all(is_trap):
  65. cond1 = 1 == len(np.unique([g.delay for g in grads]))
  66. cond2 = 1 == len(np.unique([g.rise_time for g in grads]))
  67. cond3 = 1 == len(np.unique([g.flat_time for g in grads]))
  68. cond4 = 1 == len(np.unique([g.fall_time for g in grads]))
  69. if cond1 and cond2 and cond3 and cond4:
  70. grad = grads[0]
  71. grad.amplitude = np.sum([g.amplitude for g in grads])
  72. grad.area = np.sum([g.area for g in grads])
  73. grad.flat_area = np.sum([g.flat_area for g in grads])
  74. return grad
  75. # Check if we only have arbitrary grads on irregular time samplings, optionally mixed with trapezoids
  76. if np.all(np.logical_or(is_trap, np.logical_not(is_arb))):
  77. # Keep shapes still rather simple
  78. times = []
  79. for ii in range(len(grads)):
  80. g = grads[ii]
  81. if g.type == "trap":
  82. times.extend(
  83. np.cumsum([g.delay, g.rise_time, g.flat_time, g.fall_time])
  84. )
  85. else:
  86. times.extend(g.delay + g.tt)
  87. times = np.sort(np.unique(times))
  88. dt = times[1:] - times[:-1]
  89. ieps = dt < eps
  90. if np.any(ieps):
  91. dtx = [times[0], *dt]
  92. dtx[ieps] = (
  93. dtx[ieps] + dtx[ieps + 1]
  94. ) # Assumes that no more than two too similar values can occur
  95. dtx[ieps + 1] = []
  96. times = np.cumsum(dtx)
  97. amplitudes = np.zeros_like(times)
  98. for ii in range(len(grads)):
  99. g = grads[ii]
  100. if g.type == "trap":
  101. if g.flat_time > 0: # Trapezoid or triangle
  102. g.tt = np.cumsum([0, g.rise_time, g.flat_time, g.fall_time])
  103. g.waveform = [0, g.amplitude, g.amplitude, 0]
  104. else:
  105. g.tt = np.cumsum([0, g.rise_time, g.fall_time])
  106. g.waveform = [0, g.amplitude, 0]
  107. tt = g.delay + g.tt
  108. # Fix rounding for the first and last time points
  109. i_min = np.argmin(np.abs(tt[0] - times))
  110. t_min = (np.abs(tt[0] - times))[i_min]
  111. if t_min < eps:
  112. tt[0] = times[i_min]
  113. i_min = np.argmin(np.abs(tt[-1] - times))
  114. t_min = (np.abs(tt[-1] - times))[i_min]
  115. if t_min < eps:
  116. tt[-1] = times[i_min]
  117. if np.abs(g.waveform[0]) > eps and tt[0] > eps:
  118. tt[0] += eps
  119. amplitudes += np.interp(xp=tt, fp=g.waveform, x=times)
  120. grad = make_extended_trapezoid(
  121. channel=channel, amplitudes=amplitudes, times=times, system=system
  122. )
  123. return grad
  124. # Convert everything to a regularly-sampled waveform
  125. waveforms = dict()
  126. max_length = 0
  127. for ii in range(len(grads)):
  128. g = grads[ii]
  129. if g.type == "grad":
  130. if is_arb[ii]:
  131. waveforms[ii] = g.waveform
  132. else:
  133. waveforms[ii] = points_to_waveform(
  134. amplitudes=g.waveform,
  135. times=g.tt,
  136. grad_raster_time=system.grad_raster_time,
  137. )
  138. elif g.type == "trap":
  139. if g.flat_time > 0: # Triangle or trapezoid
  140. times = np.array(
  141. [
  142. g.delay - common_delay,
  143. g.delay - common_delay + g.rise_time,
  144. g.delay - common_delay + g.rise_time + g.flat_time,
  145. g.delay
  146. - common_delay
  147. + g.rise_time
  148. + g.flat_time
  149. + g.fall_time,
  150. ]
  151. )
  152. amplitudes = np.array([0, g.amplitude, g.amplitude, 0])
  153. else:
  154. times = np.array(
  155. [
  156. g.delay - common_delay,
  157. g.delay - common_delay + g.rise_time,
  158. g.delay - common_delay + g.rise_time + g.fall_time,
  159. ]
  160. )
  161. amplitudes = np.array([0, g.amplitude, 0])
  162. waveforms[ii] = points_to_waveform(
  163. amplitudes=amplitudes,
  164. times=times,
  165. grad_raster_time=system.grad_raster_time,
  166. )
  167. else:
  168. raise ValueError("Unknown gradient type")
  169. if g.delay - common_delay > 0:
  170. # Stop for numpy.arange is not g.delay - common_delay - system.grad_raster_time like in Matlab
  171. # so as to include the endpoint
  172. t_delay = np.arange(0, g.delay - common_delay, step=system.grad_raster_time)
  173. waveforms[ii] = np.insert(waveforms[ii], 0, t_delay)
  174. num_points = len(waveforms[ii])
  175. max_length = num_points if num_points > max_length else max_length
  176. w = np.zeros(max_length)
  177. for ii in range(len(grads)):
  178. wt = np.zeros(max_length)
  179. wt[0 : len(waveforms[ii])] = waveforms[ii]
  180. w += wt
  181. grad = make_arbitrary_grad(
  182. channel=channel,
  183. waveform=w,
  184. system=system,
  185. max_slew=max_slew,
  186. max_grad=max_grad,
  187. delay=common_delay,
  188. )
  189. # Fix the first and the last values
  190. # First is defined by the sum of firsts with the minimal delay (common_delay)
  191. # Last is defined by the sum of lasts with the maximum duration (total_duration)
  192. grad.first = np.sum(firsts[np.array(delays) == common_delay])
  193. grad.last = np.sum(lasts[np.where(durs == total_duration)])
  194. return grad