event_lib.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. from types import SimpleNamespace
  2. from typing import Tuple, Union
  3. try:
  4. from typing import Self
  5. except ImportError:
  6. from typing import TypeVar
  7. Self = TypeVar('Self', bound='EventLibrary')
  8. import math
  9. import numpy as np
  10. class EventLibrary:
  11. """
  12. Defines an event library ot maintain a list of events. Provides methods to insert new data and find existing data.
  13. Sequence Properties:
  14. - data - A struct array with field 'array' to store data of varying lengths, remaining compatible with codegen.
  15. - type - Type to distinguish events in the same class (e.g. trapezoids and arbitrary gradients)
  16. Sequence Methods:
  17. - find - Find an event in the library
  18. - insert - Add a new event to the library
  19. See also `Sequence.py`.
  20. Attributes
  21. ----------
  22. data : dict{str: numpy.array}
  23. Key-value pairs of event keys and corresponding data.
  24. type : dict{str, str}
  25. Key-value pairs of event keys and corresponding event types.
  26. keymap : dict{str, int}
  27. Key-value pairs of data values and corresponding event keys.
  28. """
  29. def __init__(self, numpy_data=False):
  30. self.data = dict()
  31. self.type = dict()
  32. self.keymap = dict()
  33. self.next_free_ID = 1
  34. self.numpy_data = numpy_data
  35. def __str__(self) -> str:
  36. s = "EventLibrary:"
  37. s += "\ndata: " + str(len(self.data))
  38. s += "\ntype: " + str(len(self.type))
  39. return s
  40. def find(self, new_data: np.ndarray) -> Tuple[int, bool]:
  41. """
  42. Finds data `new_data` in event library.
  43. Parameters
  44. ----------
  45. new_data : numpy.ndarray
  46. Data to be found in event library.
  47. Returns
  48. -------
  49. key_id : int
  50. Key of `new_data` in event library, if found.
  51. found : bool
  52. If `new_data` was found in the event library or not.
  53. """
  54. if self.numpy_data:
  55. new_data = np.asarray(new_data)
  56. key = new_data.tobytes()
  57. else:
  58. key = tuple(new_data)
  59. if key in self.keymap:
  60. key_id = self.keymap[key]
  61. found = True
  62. else:
  63. key_id = self.next_free_ID
  64. found = False
  65. return key_id, found
  66. def find_or_insert(
  67. self, new_data: np.ndarray, data_type: str = str()
  68. ) -> Tuple[int, bool]:
  69. """
  70. Lookup a data structure in the given library and return the index of the data in the library. If the data does
  71. not exist in the library it is inserted right away. The data is a 1xN array with event-specific data.
  72. See also insert `pypulseq.Sequence.sequence.Sequence.add_block()`.
  73. Parameters
  74. ----------
  75. new_data : numpy.ndarray
  76. Data to be found (or added, if not found) in event library.
  77. data_type : str, default=str()
  78. Type of data.
  79. Returns
  80. -------
  81. key_id : int
  82. Key of `new_data` in event library, if found.
  83. found : bool
  84. If `new_data` was found in the event library or not.
  85. """
  86. if self.numpy_data:
  87. new_data = np.asarray(new_data)
  88. new_data.flags.writeable = False
  89. key = new_data.tobytes()
  90. else:
  91. key = tuple(new_data)
  92. if key in self.keymap:
  93. key_id = self.keymap[key]
  94. found = True
  95. else:
  96. key_id = self.next_free_ID
  97. found = False
  98. # Insert
  99. self.data[key_id] = new_data
  100. if data_type != str():
  101. self.type[key_id] = data_type
  102. self.keymap[key] = key_id
  103. self.next_free_ID = key_id + 1 # Update next_free_id
  104. return key_id, found
  105. def insert(self, key_id: int, new_data: np.ndarray, data_type: str = str()) -> int:
  106. """
  107. Add event to library.
  108. See also `pypulseq.event_library.EventLibrary.find()`.
  109. Parameters
  110. ----------
  111. key_id : int
  112. Key of `new_data`.
  113. new_data : numpy.ndarray
  114. Data to be inserted into event library.
  115. data_type : str, default=str()
  116. Data type of `new_data`.
  117. Returns
  118. -------
  119. key_id : int
  120. Key ID of inserted event.
  121. """
  122. if isinstance(key_id, float):
  123. key_id = int(key_id)
  124. if key_id == 0:
  125. key_id = self.next_free_ID
  126. if self.numpy_data:
  127. new_data = np.asarray(new_data)
  128. new_data.flags.writeable = False
  129. key = new_data.tobytes()
  130. else:
  131. key = tuple(new_data)
  132. self.data[key_id] = new_data
  133. if data_type != str():
  134. self.type[key_id] = data_type
  135. self.keymap[key] = key_id
  136. if key_id >= self.next_free_ID:
  137. self.next_free_ID = key_id + 1 # Update next_free_id
  138. return key_id
  139. def get(self, key_id: int) -> dict:
  140. """
  141. Parameters
  142. ----------
  143. key_id : int
  144. Returns
  145. -------
  146. dict
  147. """
  148. return {
  149. "key": key_id,
  150. "data": self.data[key_id],
  151. "type": self.type[key_id],
  152. }
  153. def out(self, key_id: int) -> SimpleNamespace:
  154. """
  155. Get element from library by key.
  156. See also `pypulseq.event_library.EventLibrary.find()`.
  157. Parameters
  158. ----------
  159. key_id : int
  160. Returns
  161. -------
  162. out : SimpleNamespace
  163. """
  164. out = SimpleNamespace()
  165. out.key = key_id
  166. out.data = self.data[key_id]
  167. out.type = self.type[key_id]
  168. return out
  169. def update(
  170. self,
  171. key_id: int,
  172. old_data: np.ndarray,
  173. new_data: np.ndarray,
  174. data_type: str = str(),
  175. ):
  176. """
  177. Parameters
  178. ----------
  179. key_id : int
  180. old_data : numpy.ndarray (Ignored!)
  181. new_data : numpy.ndarray
  182. data_type : str, default=str()
  183. """
  184. if key_id in self.data:
  185. if self.data[key_id] in self.keymap:
  186. del self.keymap[self.data[key_id]]
  187. self.insert(key_id, new_data, data_type)
  188. def update_data(
  189. self,
  190. key_id: int,
  191. old_data: np.ndarray,
  192. new_data: np.ndarray,
  193. data_type: str = str(),
  194. ):
  195. """
  196. Parameters
  197. ----------
  198. key_id : int
  199. old_data : np.ndarray (Ignored!)
  200. new_data : np.ndarray
  201. data_type : str
  202. """
  203. self.update(key_id, old_data, new_data, data_type)
  204. def remove_duplicates(self, digits: Union[int, Tuple[int]]) -> Tuple[Self, dict]:
  205. """
  206. Remove duplicate events from this event library by rounding the data
  207. according to the significant `digits` specification, and then removing
  208. duplicate events.
  209. Returns a new event library, leaving the current one intact.
  210. Parameters
  211. ----------
  212. digits : Union[int, List[int]]
  213. For libraries with `numpy_data == True`:
  214. A single number specifying the number of significant digits
  215. after rounding.
  216. Otherwise:
  217. A tuple of numbers specifying the number of significant digits
  218. after rounding for each entry in the event data tuple.
  219. Returns
  220. -------
  221. new_library : EventLibrary
  222. Event library with the duplicate events removed
  223. mapping : dict
  224. Dictionary containing a mapping of IDs in the old library to IDs
  225. in the new library.
  226. """
  227. def round_data(data: Tuple[float], digits: Tuple[int]) -> Tuple[float]:
  228. """
  229. Round the data tuple to a specified number of significant digits,
  230. specified by `digits`. Rounding behaviour is similar to the {.Ng}
  231. format specifier if N > 0, and similar to {.0f} otherwise.
  232. """
  233. return tuple(round(d, dig - int(math.ceil(math.log10(abs(d) + 1e-12))) if dig > 0 else -dig) for d, dig in
  234. zip(data, digits))
  235. def round_data_numpy(data: np.ndarray, digits: int) -> np.ndarray:
  236. """
  237. Round the data array to a specified number of significant digits,
  238. specified by `digits`. Rounding behaviour is similar to the {.Ng}
  239. format specifier if N > 0, and similar to {.0f} otherwise.
  240. """
  241. mags = 10 ** (digits - (np.ceil(np.log10(abs(data) + 1e-12))) if digits > 0 else -digits)
  242. result = np.round(data * mags) / mags
  243. result.flags.writeable = False
  244. return result
  245. # Round library data based on `digits` specification
  246. if self.numpy_data:
  247. rounded_data = {x: round_data_numpy(self.data[x], digits) for x in self.data}
  248. else:
  249. rounded_data = {x: round_data(self.data[x], digits) for x in self.data}
  250. # Initialize filtered library
  251. new_library = EventLibrary(numpy_data=self.numpy_data)
  252. # Initialize ID mapping. Always include 0:0 to allow the mapping dict
  253. # to be used for mapping block_events (which can contain 0, i.e. no
  254. # event)
  255. mapping = {0: 0}
  256. # Recreate library using rounded values
  257. for k, v in sorted(rounded_data.items()):
  258. mapping[k], _ = new_library.find_or_insert(v, self.type[k] if k in self.type else str())
  259. return new_library, mapping