Skip to content

Commit 5b51dc8

Browse files
Reworked check_timing to provide a structured error report (#200)
* Removed trailing whitespace in a number of files * Corrected `system == None` checks to `system is None` and fixed `system` type hints * Added warnings where RF and ADC delays were silently increased to the dead time * Fixup: system type hint * Removed silent adjustment of ADC delay to ADC dead time (This should be caught by `make_adc`. If we get here, the user really wants to adjust the delay and just accept it and let `check_timing` report it) * Reworked `check_timing` to provide a structured error report - Added unit test for `check_timing` - The default signature of `seq.check_timing` remains the same, but can pretty-print the report with `print_errors=True` - `seq.write` now checks sequence timing by default - 'TotalDuration' calculation moved from `check_timing` to `seq.write` - Check whether gradients in the last block ramp down to zero removed from `check_timing` * Added optional tracing of where blocks/events were created - Can be enabled/disabled with `pp.enable_trace(limit)` and `pp.disable_trace()` - Added tracing to all blocks and all RF/ADC/gradient events - Added printing of the trace to `check_timing` when it is available * Fixed `check_timing` to not assume `rf_raster_time == adc_raster_time` * Updated expected output for `test_sequence` because it did not include `TotalDuration` in the sequence definitions * Fixed continuity checks for arbitrary gradients - `Sequence.write` now checks whether the last block ramps down to 0 - Fixed gradient continuity checks in `set_block` - Now handles arbitrary block orders properly - Now properly checks the last value of the previous block - Added gradient checks for blocks without gradients - Sequence object now remains valid after gradient errors - Added unit tests for gradient continuity in `test_block` * fix None checks fix some type hints some cleanup * refactor timing error report in test_report --------- Co-authored-by: Patrick Schuenke <[email protected]>
1 parent 2f09cf6 commit 5b51dc8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1723
-1225
lines changed

pypulseq/Sequence/block.py

Lines changed: 108 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44

55
import numpy as np
66

7+
from pypulseq import eps
78
from pypulseq.block_to_events import block_to_events
89
from pypulseq.compress_shape import compress_shape
910
from pypulseq.decompress_shape import decompress_shape
1011
from pypulseq.event_lib import EventLibrary
1112
from pypulseq.supported_labels_rf_use import get_supported_labels
13+
from pypulseq.utils.tracing import trace_enabled
1214

1315

1416
def set_block(self, block_index: int, *args: SimpleNamespace) -> None:
@@ -39,34 +41,42 @@ def set_block(self, block_index: int, *args: SimpleNamespace) -> None:
3941
If a gradient that doesn't end at zero is not aligned to the block boundary.
4042
"""
4143
events = block_to_events(*args)
42-
self.block_events[block_index] = np.zeros(7, dtype=np.int32)
44+
new_block = np.zeros(7, dtype=np.int32)
4345
duration = 0
4446

45-
check_g = {} # Key-value mapping of index and pairs of gradients/times
47+
check_g = {
48+
0: SimpleNamespace(idx=2, start=(0,0), stop=(0,0)),
49+
1: SimpleNamespace(idx=3, start=(0,0), stop=(0,0)),
50+
2: SimpleNamespace(idx=4, start=(0,0), stop=(0,0))
51+
} # Key-value mapping of index and pairs of gradients/times
4652
extensions = []
4753

4854
for event in events:
4955
if not isinstance(event, float): # If event is not a block duration
5056
if event.type == "rf":
51-
if self.block_events[block_index][1] != 0:
57+
if new_block[1] != 0:
5258
raise ValueError('Multiple RF events were specified in set_block')
53-
59+
5460
if hasattr(event, "id"):
5561
rf_id = event.id
5662
else:
5763
rf_id, _ = register_rf_event(self, event)
5864

59-
self.block_events[block_index][1] = rf_id
65+
new_block[1] = rf_id
6066
duration = max(
6167
duration, event.shape_dur + event.delay + event.ringdown_time
6268
)
69+
70+
if trace_enabled():
71+
if hasattr(event, 'trace'):
72+
setattr(self.block_trace[block_index], 'rf', event.trace)
6373
elif event.type == "grad":
6474
channel_num = ["x", "y", "z"].index(event.channel)
6575
idx = 2 + channel_num
6676

67-
if self.block_events[block_index][idx] != 0:
77+
if new_block[idx] != 0:
6878
raise ValueError(f'Multiple {event.channel.upper()} gradient events were specified in set_block')
69-
79+
7080
grad_start = (
7181
event.delay
7282
+ math.floor(event.tt[0] / self.grad_raster_time + 1e-10)
@@ -88,42 +98,54 @@ def set_block(self, block_index: int, *args: SimpleNamespace) -> None:
8898
else:
8999
grad_id, _ = register_grad_event(self, event)
90100

91-
self.block_events[block_index][idx] = grad_id
101+
new_block[idx] = grad_id
92102
duration = max(duration, grad_duration)
103+
104+
if trace_enabled():
105+
if hasattr(event, 'trace'):
106+
setattr(self.block_trace[block_index], 'g' + event.channel, event.trace)
93107
elif event.type == "trap":
94108
channel_num = ["x", "y", "z"].index(event.channel)
95109
idx = 2 + channel_num
96110

97-
if self.block_events[block_index][idx] != 0:
111+
if new_block[idx] != 0:
98112
raise ValueError(f'Multiple {event.channel.upper()} gradient events were specified in set_block')
99113

100114
if hasattr(event, "id"):
101115
trap_id = event.id
102116
else:
103117
trap_id = register_grad_event(self, event)
104118

105-
self.block_events[block_index][idx] = trap_id
119+
new_block[idx] = trap_id
106120
duration = max(
107121
duration,
108122
event.delay
109123
+ event.rise_time
110124
+ event.flat_time
111125
+ event.fall_time
112126
)
127+
128+
if trace_enabled():
129+
if hasattr(event, 'trace'):
130+
setattr(self.block_trace[block_index], 'g' + event.channel, event.trace)
113131
elif event.type == "adc":
114-
if self.block_events[block_index][5] != 0:
132+
if new_block[5] != 0:
115133
raise ValueError('Multiple ADC events were specified in set_block')
116-
134+
117135
if hasattr(event, "id"):
118136
adc_id = event.id
119137
else:
120138
adc_id = register_adc_event(self, event)
121139

122-
self.block_events[block_index][5] = adc_id
140+
new_block[5] = adc_id
123141
duration = max(
124142
duration,
125143
event.delay + event.num_samples * event.dwell + event.dead_time
126144
)
145+
146+
if trace_enabled():
147+
if hasattr(event, 'trace'):
148+
setattr(self.block_trace[block_index], 'adc', event.trace)
127149
elif event.type == "delay":
128150
duration = max(duration, event.delay)
129151
elif event.type in ["output", "trigger"]:
@@ -181,49 +203,86 @@ def set_block(self, block_index: int, *args: SimpleNamespace) -> None:
181203
self.extensions_library.insert(extension_id, data)
182204

183205
# Now we add the ID
184-
self.block_events[block_index][6] = extension_id
206+
new_block[6] = extension_id
185207

186208
# =========
187209
# PERFORM GRADIENT CHECKS
188210
# =========
189211
for grad_to_check in check_g.values():
190212

191-
if (
192-
abs(grad_to_check.start[1])
193-
> self.system.max_slew * self.system.grad_raster_time
194-
):
195-
if grad_to_check.start[0] != 0:
196-
raise ValueError(
213+
if (abs(grad_to_check.start[1]) > self.system.max_slew * self.system.grad_raster_time):
214+
if grad_to_check.start[0] > eps:
215+
raise RuntimeError(
197216
"No delay allowed for gradients which start with a non-zero amplitude"
198217
)
199218

200-
if block_index > 1:
201-
# TODO: block_index - 1 assumes linear ordering of blocks.
202-
# However, searching for the previous block index in block_events is a big performance hit
203-
# For newly inserted blocks (block_index not in self.block_events), we can just use the last block as previous
204-
prev_id = self.block_events[block_index - 1][grad_to_check.idx]
219+
# Check whether any blocks exist in the sequence
220+
if self.next_free_block_ID > 1:
221+
# Look up the previous block (and the next block in case of a set_block call)
222+
if block_index == self.next_free_block_ID:
223+
# New block inserted
224+
prev_block_index = next(reversed(self.block_events))
225+
next_block_index = None
226+
else:
227+
blocks = list(self.block_events)
228+
try:
229+
# Existing block overwritten
230+
idx = blocks.index(block_index)
231+
prev_block_index = blocks[idx - 1] if idx > 0 else None
232+
next_block_index = blocks[idx + 1] if idx < len(blocks)-1 else None
233+
except ValueError:
234+
# Inserting a new block with non-contiguous numbering
235+
prev_block_index = next(reversed(self.block_events))
236+
next_block_index = None
237+
238+
# Look up the last gradient value in the previous block
239+
last = 0
240+
if prev_block_index is not None:
241+
prev_id = self.block_events[prev_block_index][grad_to_check.idx]
205242
if prev_id != 0:
206243
prev_lib = self.grad_library.get(prev_id)
207-
prev_data = prev_lib["data"]
208-
prev_type = prev_lib["type"]
209-
if prev_type == "t":
210-
raise RuntimeError(
211-
"Two consecutive gradients need to have the same amplitude at the connection point"
212-
)
213-
elif prev_type == "g":
214-
last = prev_data[5]
215-
if (
216-
abs(last - grad_to_check.start[1])
217-
> self.system.max_slew * self.system.grad_raster_time
218-
):
219-
raise RuntimeError(
220-
"Two consecutive gradients need to have the same amplitude at the connection point"
221-
)
222-
else:
244+
prev_type = prev_lib['type']
245+
246+
if prev_type == 't':
247+
last = 0
248+
elif prev_type == 'g':
249+
last = prev_lib['data'][5]
250+
251+
# Check whether the difference between the last gradient value and
252+
# the first value of the new gradient is achievable with the
253+
# specified slew rate.
254+
if (abs(last - grad_to_check.start[1]) > self.system.max_slew * self.system.grad_raster_time):
223255
raise RuntimeError(
224-
"First gradient in the the first block has to start at 0."
256+
"Two consecutive gradients need to have the same amplitude at the connection point"
225257
)
226258

259+
# Look up the first gradient value in the next block
260+
# (this only happens when using set_block to patch a block)
261+
if next_block_index is not None:
262+
next_id = self.block_events[next_block_index][grad_to_check.idx]
263+
if next_id != 0:
264+
next_lib = self.grad_library.get(next_id)
265+
next_type = next_lib['type']
266+
267+
if next_type == 't':
268+
first = 0
269+
elif next_type == 'g':
270+
first = next_lib['data'][4]
271+
else:
272+
first = 0
273+
274+
# Check whether the difference between the first gradient value
275+
# in the next block and the last value of the new gradient is
276+
# achievable with the specified slew rate.
277+
if (abs(first - grad_to_check.stop[1]) > self.system.max_slew * self.system.grad_raster_time):
278+
raise RuntimeError(
279+
"Two consecutive gradients need to have the same amplitude at the connection point"
280+
)
281+
elif abs(grad_to_check.start[1]) > self.system.max_slew * self.system.grad_raster_time:
282+
raise RuntimeError(
283+
"First gradient in the the first block has to start at 0."
284+
)
285+
227286
if (
228287
grad_to_check.stop[1] > self.system.max_slew * self.system.grad_raster_time
229288
and abs(grad_to_check.stop[0] - duration) > 1e-7
@@ -232,6 +291,7 @@ def set_block(self, block_index: int, *args: SimpleNamespace) -> None:
232291
"A gradient that doesn't end at zero needs to be aligned to the block boundary."
233292
)
234293

294+
self.block_events[block_index] = new_block
235295
self.block_durations[block_index] = float(duration)
236296

237297

@@ -410,7 +470,7 @@ def get_block(self, block_index: int) -> SimpleNamespace:
410470
# Enter block into the block cache
411471
if self.use_block_cache:
412472
self.block_cache[block_index] = block
413-
473+
414474
return block
415475

416476

@@ -430,7 +490,7 @@ def register_adc_event(self, event: EventLibrary) -> int:
430490
data = (
431491
event.num_samples,
432492
event.dwell,
433-
max(event.delay, event.dead_time),
493+
event.delay,
434494
event.freq_offset,
435495
event.phase_offset,
436496
event.dead_time,
@@ -548,12 +608,12 @@ def register_grad_event(
548608
any_changed = any_changed or found
549609
else:
550610
grad_id = self.grad_library.insert(0, data, event.type[0])
551-
611+
552612
# Clear block cache because grad event or shapes were overwritten
553613
# TODO: Could find only the blocks that are affected by the changes
554614
if self.use_block_cache and any_changed:
555615
self.block_cache.clear()
556-
616+
557617
if event.type == "grad":
558618
return grad_id, shape_IDs
559619
elif event.type == "trap":
@@ -658,14 +718,14 @@ def register_rf_event(self, event: SimpleNamespace) -> Tuple[int, List[int]]:
658718

659719
if may_exist:
660720
rf_id, found = self.rf_library.find_or_insert(new_data=data, data_type=use)
661-
721+
662722
# Clear block cache because RF event was overwritten
663723
# TODO: Could find only the blocks that are affected by the changes
664724
if self.use_block_cache and found:
665725
self.block_cache.clear()
666726
else:
667727
rf_id = self.rf_library.insert(key_id=0, new_data=data, data_type=use)
668728

669-
729+
670730

671731
return rf_id, shape_IDs

0 commit comments

Comments
 (0)