check_timing.py 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. from types import SimpleNamespace
  2. from typing import Tuple
  3. import numpy as np
  4. from seqgen.pypulseq import eps
  5. from seqgen.pypulseq.calc_duration import calc_duration
  6. from seqgen.pypulseq.opts import Opts
  7. def check_timing(system: Opts, *events: SimpleNamespace) -> Tuple[bool, str, float]:
  8. """
  9. Checks if timings of `events` are aligned with the corresponding raster time.
  10. Parameters
  11. ----------
  12. system : Opts
  13. System limits object.
  14. events : SimpleNamespace
  15. Events.
  16. Returns
  17. -------
  18. is_ok : bool
  19. Boolean flag indicating if timing of events `events` are aligned with gradient raster time
  20. `system.grad_raster_time`.
  21. text_err : str
  22. Error string, if timings are not aligned.
  23. total_duration : float
  24. Total duration of events.
  25. Raises
  26. ------
  27. ValueError
  28. If incorrect data type is encountered in `events`.
  29. """
  30. if len(events) == 0:
  31. text_err = "Empty or damaged block detected"
  32. is_ok = False
  33. total_duration = 0.0
  34. return is_ok, text_err, total_duration
  35. total_duration = calc_duration(*events)
  36. is_ok = __div_check(total_duration, system.block_duration_raster)
  37. text_err = "" if is_ok else f"Total duration: {total_duration * 1e6} us"
  38. for e in events:
  39. if isinstance(e, (float, int)): # Special handling for block_duration
  40. continue
  41. elif not isinstance(e, (dict, SimpleNamespace)):
  42. raise ValueError(
  43. "Wrong data type of variable arguments, list[SimpleNamespace] expected."
  44. )
  45. ok = True
  46. if isinstance(e, list) and len(e) > 1:
  47. # For now this is only the case for arrays of extensions, but we cannot actually check extensions anyway...
  48. continue
  49. if hasattr(e, "type") and (e.type == "adc" or e.type == "rf"):
  50. raster = system.rf_raster_time
  51. else:
  52. raster = system.grad_raster_time
  53. if hasattr(e, "delay"):
  54. if e.delay < -eps:
  55. ok = False
  56. if not __div_check(e.delay, raster):
  57. ok = False
  58. if hasattr(e, "duration"):
  59. if not __div_check(e.duration, raster):
  60. ok = False
  61. if hasattr(e, "dwell"):
  62. if (
  63. e.dwell < system.adc_raster_time
  64. or np.abs(
  65. np.round(e.dwell / system.adc_raster_time) * system.adc_raster_time
  66. - e.dwell
  67. )
  68. > 1e-10
  69. ):
  70. ok = False
  71. if hasattr(e, "type") and e.type == "trap":
  72. if (
  73. not __div_check(e.rise_time, system.grad_raster_time)
  74. or not __div_check(e.flat_time, system.grad_raster_time)
  75. or not __div_check(e.fall_time, system.grad_raster_time)
  76. ):
  77. ok = False
  78. if not ok:
  79. is_ok = False
  80. text_err = "["
  81. if hasattr(e, "type"):
  82. text_err += f"type: {e.type} "
  83. if hasattr(e, "delay"):
  84. text_err += f"delay: {e.delay * 1e6} us "
  85. if hasattr(e, "duration"):
  86. text_err += f"duration: {e.duration * 1e6} us"
  87. if hasattr(e, "dwell"):
  88. text_err += f"dwell: {e.dwell * 1e9} ns"
  89. if hasattr(e, "type") and e.type == "trap":
  90. text_err += (
  91. f"rise time: {e.rise_time * 1e6} flat time: {e.flat_time * 1e6} "
  92. f"fall time: {e.fall_time * 1e6} us"
  93. )
  94. text_err += "]"
  95. return is_ok, text_err, total_duration
  96. def __div_check(a: float, b: float) -> bool:
  97. """
  98. Checks whether `a` can be divided by `b` to an accuracy of 1e-9.
  99. """
  100. c = a / b
  101. return abs(c - np.round(c)) < 1e-9