make_trapezoid.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. from types import SimpleNamespace
  2. import numpy as np
  3. from seqgen.pypulseq.opts import Opts
  4. def make_trapezoid(
  5. channel: str,
  6. amplitude: float = 0,
  7. area: float = None,
  8. delay: float = 0,
  9. duration: float = 0,
  10. fall_time: float = 0,
  11. flat_area: float = 0,
  12. flat_time: float = -1,
  13. max_grad: float = 0,
  14. max_slew: float = 0,
  15. rise_time: float = 0,
  16. system: Opts = Opts(),
  17. ) -> SimpleNamespace:
  18. """
  19. Create a trapezoidal gradient event.
  20. See also:
  21. - `pypulseq.Sequence.sequence.Sequence.add_block()`
  22. - `pypulseq.opts.Opts`
  23. Parameters
  24. ----------
  25. channel : str
  26. Orientation of trapezoidal gradient event. Must be one of `x`, `y` or `z`.
  27. amplitude : float, default=0
  28. Amplitude.
  29. area : float, default=None
  30. Area.
  31. delay : float, default=0
  32. Delay in seconds (s).
  33. duration : float, default=0
  34. Duration in seconds (s).
  35. flat_area : float, default=0
  36. Flat area.
  37. flat_time : float, default=-1
  38. Flat duration in seconds (s). Default is -1 to account for triangular pulses.
  39. max_grad : float, default=0
  40. Maximum gradient strength.
  41. max_slew : float, default=0
  42. Maximum slew rate.
  43. rise_time : float, default=0
  44. Rise time in seconds (s).
  45. system : Opts, default=Opts()
  46. System limits.
  47. Returns
  48. -------
  49. grad : SimpleNamespace
  50. Trapezoidal gradient event created based on the supplied parameters.
  51. Raises
  52. ------
  53. ValueError
  54. If none of `area`, `flat_area` and `amplitude` are passed
  55. If requested area is too large for this gradient
  56. If `flat_time`, `duration` and `area` are not supplied.
  57. Amplitude violation
  58. """
  59. if channel not in ["x", "y", "z"]:
  60. raise ValueError(
  61. f"Invalid channel. Must be one of `x`, `y` or `z`. Passed: {channel}"
  62. )
  63. if max_grad <= 0:
  64. max_grad = system.max_grad
  65. if max_slew <= 0:
  66. max_slew = system.max_slew
  67. if rise_time <= 0:
  68. rise_time = 0.0
  69. if fall_time > 0:
  70. if rise_time == 0:
  71. raise ValueError(
  72. "Invalid arguments. Must always supply `rise_time` if `fall_time` is specified explicitly."
  73. )
  74. else:
  75. fall_time = 0.0
  76. if area is None and flat_area == 0 and amplitude == 0:
  77. raise ValueError("Must supply either 'area', 'flat_area' or 'amplitude'.")
  78. if flat_time != -1:
  79. if amplitude != 0:
  80. amplitude2 = amplitude
  81. elif (area is not None) and (
  82. rise_time > 0
  83. ): # We have rise_time, flat_time and area.
  84. amplitude2 = area / (rise_time + flat_time)
  85. else:
  86. if flat_area == 0:
  87. raise ValueError(
  88. "When `flat_time` is provided, either `flat_area` or `amplitude` must be provided as well; you may "
  89. "consider providing `duration`, `area` and optionally ramp times instead."
  90. )
  91. amplitude2 = flat_area / flat_time
  92. if rise_time == 0:
  93. rise_time = np.abs(amplitude2) / max_slew
  94. rise_time = (
  95. np.ceil(rise_time / system.grad_raster_time) * system.grad_raster_time
  96. )
  97. if rise_time == 0:
  98. rise_time = system.grad_raster_time
  99. if fall_time == 0:
  100. fall_time = rise_time
  101. elif duration > 0:
  102. if amplitude == 0:
  103. if rise_time == 0:
  104. dC = 1 / np.abs(2 * max_slew) + 1 / np.abs(2 * max_slew)
  105. possible = duration**2 > 4 * np.abs(area) * dC
  106. assert possible, (
  107. f"Requested area is too large for this gradient. Minimum required duration is "
  108. f"{np.round(np.sqrt(4 * np.abs(area) * dC) * 1e6)} uss"
  109. )
  110. amplitude2 = (
  111. duration - np.sqrt(duration**2 - 4 * np.abs(area) * dC)
  112. ) / (2 * dC)
  113. else:
  114. if fall_time == 0:
  115. fall_time = rise_time
  116. amplitude2 = area / (duration - 0.5 * rise_time - 0.5 * fall_time)
  117. possible = (
  118. duration > (rise_time + fall_time) and np.abs(amplitude2) < max_grad
  119. )
  120. assert possible, (
  121. f"Requested area is too large for this gradient. Probably amplitude is violated "
  122. f"{np.round(np.abs(amplitude) / max_grad * 100)}"
  123. )
  124. if rise_time == 0:
  125. rise_time = (
  126. np.ceil(np.abs(amplitude2) / max_slew / system.grad_raster_time)
  127. * system.grad_raster_time
  128. )
  129. if rise_time == 0:
  130. rise_time = system.grad_raster_time
  131. if fall_time == 0:
  132. fall_time = rise_time
  133. flat_time = duration - rise_time - fall_time
  134. if amplitude == 0:
  135. # Adjust amplitude (after rounding) to match area
  136. amplitude2 = area / (rise_time / 2 + fall_time / 2 + flat_time)
  137. else:
  138. if area == 0:
  139. raise ValueError("Must supply area or duration.")
  140. else:
  141. # Find the shortest possible duration. First check if the area can be realized as a triangle.
  142. # If not, then it must be a trapezoid.
  143. rise_time = (
  144. np.ceil(np.sqrt(np.abs(area) / max_slew) / system.grad_raster_time)
  145. * system.grad_raster_time
  146. )
  147. if rise_time < system.grad_raster_time: # Area was almost 0 maybe
  148. rise_time = system.grad_raster_time
  149. amplitude2 = np.divide(area, rise_time) # To handle nan
  150. t_eff = rise_time
  151. if np.abs(amplitude2) > max_grad:
  152. t_eff = (
  153. np.ceil(np.abs(area) / max_grad / system.grad_raster_time)
  154. * system.grad_raster_time
  155. )
  156. amplitude2 = area / t_eff
  157. rise_time = (
  158. np.ceil(np.abs(amplitude2) / max_slew / system.grad_raster_time)
  159. * system.grad_raster_time
  160. )
  161. if rise_time == 0:
  162. rise_time = system.grad_raster_time
  163. flat_time = t_eff - rise_time
  164. fall_time = rise_time
  165. if np.abs(amplitude2) > max_grad:
  166. raise ValueError("Amplitude violation.")
  167. grad = SimpleNamespace()
  168. grad.type = "trap"
  169. grad.channel = channel
  170. grad.amplitude = amplitude2
  171. grad.rise_time = rise_time
  172. grad.flat_time = flat_time
  173. grad.fall_time = fall_time
  174. grad.area = amplitude2 * (flat_time + rise_time / 2 + fall_time / 2)
  175. grad.flat_area = amplitude2 * flat_time
  176. grad.delay = delay
  177. grad.first = 0
  178. grad.last = 0
  179. return grad