pulserver 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. pulserver/__init__.py +17 -0
  2. pulserver/_apps/SPGR2D.py +206 -0
  3. pulserver/_apps/SPGR3D.py +184 -0
  4. pulserver/_apps/bar.py +15 -0
  5. pulserver/_apps/foo.py +15 -0
  6. pulserver/_cli.py +70 -0
  7. pulserver/_core/__init__.py +12 -0
  8. pulserver/_core/_autoseg.py +128 -0
  9. pulserver/_core/_ceq.py +541 -0
  10. pulserver/_core/_sequence.py +313 -0
  11. pulserver/_server/__init__.py +6 -0
  12. pulserver/_server/_server.py +197 -0
  13. pulserver/_version.py +16 -0
  14. pulserver/blocks/__init__.py +16 -0
  15. pulserver/blocks/_grad/_make_spiral.py +201 -0
  16. pulserver/blocks/_misc/__init__.py +7 -0
  17. pulserver/blocks/_misc/_make_spoiler_gradient.py +64 -0
  18. pulserver/blocks/_phaseenc/__init__.py +7 -0
  19. pulserver/blocks/_phaseenc/_make_phase_blip.py +0 -0
  20. pulserver/blocks/_phaseenc/_make_phase_encoding.py +49 -0
  21. pulserver/blocks/_readout/__init__.py +9 -0
  22. pulserver/blocks/_readout/_make_line_readout.py +149 -0
  23. pulserver/blocks/_readout/_make_spiral_readout.py +105 -0
  24. pulserver/blocks/_readout/rosette_readout.py +6 -0
  25. pulserver/blocks/_rfpulse/__init__.py +221 -0
  26. pulserver/blocks/_rfpulse/_make_spsp_pulse.py +196 -0
  27. pulserver/blocks/_rfpulse/_plot.py +164 -0
  28. pulserver/blocks/_rfpulse/_spsp/__init__.py +11 -0
  29. pulserver/blocks/_rfpulse/_spsp/_slr.py +444 -0
  30. pulserver/blocks/_rfpulse/_spsp/_utils.py +99 -0
  31. pulserver/blocks/_rfpulse/_spsp/rf_ripple.py +75 -0
  32. pulserver/blocks/_rfpulse/_spsp/spec_interp.py +35 -0
  33. pulserver/blocks/_rfpulse/_spsp/ss_alias.py +316 -0
  34. pulserver/blocks/_rfpulse/_spsp/ss_design.py +493 -0
  35. pulserver/blocks/_rfpulse/_spsp/ss_ep.py +332 -0
  36. pulserver/blocks/_rfpulse/_spsp/ss_filters.py +2079 -0
  37. pulserver/blocks/_rfpulse/_spsp/ss_flyback.py +441 -0
  38. pulserver/blocks/_rfpulse/_spsp/ss_fourier.py +90 -0
  39. pulserver/blocks/_rfpulse/_spsp/ss_globals.py +43 -0
  40. pulserver/blocks/_rfpulse/_spsp/ss_grad.py +239 -0
  41. pulserver/blocks/_rfpulse/_spsp/ss_spect_correct.py +57 -0
  42. pulserver/blocks/_rfpulse/_spsp/ss_verse.py +166 -0
  43. pulserver/plan/__init__.py +32 -0
  44. pulserver/plan/_cartesian2D.py +151 -0
  45. pulserver/plan/_cartesian3D.py +157 -0
  46. pulserver/plan/_iterators/__init__.py +19 -0
  47. pulserver/plan/_iterators/_base.py +69 -0
  48. pulserver/plan/_iterators/_cartesian2D_iterator.py +173 -0
  49. pulserver/plan/_iterators/_cartesian3D_iterator.py +137 -0
  50. pulserver/plan/_iterators/_noncartesian2D_iterator.py +174 -0
  51. pulserver/plan/_iterators/_noncartesian3D_iterator.py +119 -0
  52. pulserver/plan/_iterators/_stack3D_iterator.py +173 -0
  53. pulserver/plan/_noncartesian2D.py +139 -0
  54. pulserver/plan/_noncartesian3D.py +94 -0
  55. pulserver/plan/_ordering.py +55 -0
  56. pulserver/plan/_phase_cycling.py +87 -0
  57. pulserver/plan/_sampling/__init__.py +14 -0
  58. pulserver/plan/_sampling/_grid_sampling.py +180 -0
  59. pulserver/plan/_sampling/_partial_fourier.py +52 -0
  60. pulserver/plan/_sampling/_poisson_sampling.py +275 -0
  61. pulserver/plan/_sampling/_tilt.py +48 -0
  62. pulserver/plan/_stack3D.py +136 -0
  63. pulserver/sequences/__init__.py +5 -0
  64. pulserver/sequences/_design_2D_spgr.py +211 -0
  65. pulserver/sequences/_design_3D_spgr.py +184 -0
  66. pulserver-0.1.0.dist-info/LICENSE.txt +21 -0
  67. pulserver-0.1.0.dist-info/METADATA +95 -0
  68. pulserver-0.1.0.dist-info/RECORD +70 -0
  69. pulserver-0.1.0.dist-info/WHEEL +5 -0
  70. pulserver-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,541 @@
1
+ """Ceq structure definition."""
2
+
3
+ __all__ = ["Ceq", "PulseqBlock", "SequenceParams"]
4
+
5
+ from dataclasses import dataclass
6
+ from dataclasses import asdict as _asdict
7
+ from types import SimpleNamespace
8
+ from typing import Optional
9
+
10
+ import struct
11
+ import numpy as np
12
+ import pypulseq as pp
13
+
14
+ from . import _autoseg
15
+
16
+ CHANNEL_ENUM = {"osc0": 0, "osc1": 1, "ext1": 2}
17
+ SEGMENT_RINGDOWN_TIME = 116 * 1e-6 # TODO: doesn't have to be hardcoded
18
+
19
+
20
+ @dataclass
21
+ class PulseqShapeArbitrary:
22
+ n_samples: int
23
+ raster: float
24
+ time: np.ndarray
25
+ magnitude: np.ndarray
26
+ phase: np.ndarray | None = None
27
+ amplitude: float | None = None
28
+
29
+ def __post_init__(self):
30
+ self.magnitude = np.asarray(self.magnitude, dtype=np.float32)
31
+ self.time = (
32
+ np.asarray(self.time, dtype=np.float32) if self.time is not None else None
33
+ )
34
+ self.phase = (
35
+ np.asarray(self.phase, dtype=np.float32) if self.phase is not None else None
36
+ )
37
+
38
+ # determine amplitude and normalize waveform
39
+ self.amplitude = abs(self.magnitude).max()
40
+ self.magnitude *= 32767 / self.amplitude
41
+
42
+ def to_bytes(self, endian=">") -> bytes:
43
+ _bytes = struct.pack(endian + "i", self.n_samples) + struct.pack(
44
+ endian + "f", self.raster
45
+ )
46
+
47
+ # add time
48
+ if self.time is not None:
49
+ _bytes += self.time.astype(endian + "f4").tobytes()
50
+
51
+ # add magnitude
52
+ _bytes += self.magnitude.astype(endian + "f4").tobytes()
53
+
54
+ # add phase
55
+ if self.phase is not None:
56
+ _bytes += self.phase.astype(endian + "f4").tobytes()
57
+
58
+ # add amplitude
59
+ _bytes += struct.pack(endian + "f", self.amplitude)
60
+
61
+ return _bytes
62
+
63
+
64
+ @dataclass
65
+ class PulseqShapeTrap:
66
+ amplitude: float
67
+ rise_time: float
68
+ flat_time: float
69
+ fall_time: float
70
+
71
+ def to_bytes(self, endian=">") -> bytes:
72
+ return (
73
+ struct.pack(endian + "f", self.amplitude)
74
+ + struct.pack(endian + "f", self.rise_time)
75
+ + struct.pack(endian + "f", self.flat_time)
76
+ + struct.pack(endian + "f", self.fall_time)
77
+ )
78
+
79
+
80
+ @dataclass
81
+ class PulseqRF:
82
+ type: int
83
+ wav: PulseqShapeArbitrary
84
+ duration: float
85
+ delay: float
86
+
87
+ def to_bytes(self, endian=">") -> bytes:
88
+ return (
89
+ struct.pack(endian + "h", self.type)
90
+ + self.wav.to_bytes(endian)
91
+ + struct.pack(endian + "f", self.duration)
92
+ + struct.pack(endian + "f", self.delay)
93
+ )
94
+
95
+ @classmethod
96
+ def from_struct(cls, data: SimpleNamespace) -> "PulseqRF":
97
+ n_samples = data.signal.shape[0]
98
+
99
+ # determine whether wave is arbitrary or extended trap
100
+ dt = np.unique(np.diff(np.round(data.t * 1e6))) / 1e6
101
+
102
+ if len(dt) == 1: # uniform raster -> arbitrary shape
103
+ type = 1
104
+ raster = dt.item()
105
+ time = None
106
+ else: # non-uniform raster -> extended trapezoid
107
+ type = 2
108
+ raster = 0.0
109
+ time = data.t
110
+
111
+ rho = np.abs(data.signal)
112
+ theta = np.angle(data.signal)
113
+
114
+ return cls(
115
+ type=type,
116
+ wav=PulseqShapeArbitrary(n_samples, raster, time, rho, theta),
117
+ duration=data.shape_dur,
118
+ delay=data.delay,
119
+ )
120
+
121
+
122
+ @dataclass
123
+ class PulseqGrad:
124
+ type: int
125
+ delay: float
126
+ shape: PulseqShapeArbitrary | PulseqShapeTrap
127
+
128
+ def to_bytes(self, endian=">") -> bytes:
129
+ return (
130
+ struct.pack(endian + "h", self.type)
131
+ + struct.pack(endian + "f", self.delay)
132
+ + self.shape.to_bytes(endian)
133
+ )
134
+
135
+ @classmethod
136
+ def from_struct(cls, data: SimpleNamespace) -> "PulseqGrad":
137
+ print(data)
138
+ if data.type == "trap":
139
+ type = 1
140
+ shape_obj = PulseqShapeTrap(
141
+ data.amplitude, data.rise_time, data.flat_time, data.fall_time
142
+ )
143
+ elif data.type == "grad":
144
+ n_samples = data.waveform.shape[0]
145
+
146
+ # determine whether wave is arbitrary or extended trap
147
+ dt = np.unique(np.diff(np.round(data.tt * 1e6))) / 1e6
148
+
149
+ if len(dt) == 1: # uniform raster -> arbitrary shape
150
+ print("arbitrary")
151
+ type = 2
152
+ raster = dt.item()
153
+ time = None
154
+ else: # non-uniform raster -> extended trapezoid
155
+ print("extended")
156
+ type = 3
157
+ raster = 0.0
158
+ time = data.tt
159
+
160
+ waveform = data.waveform
161
+ shape_obj = PulseqShapeArbitrary(n_samples, raster, time, waveform)
162
+
163
+ return cls(type=type, delay=data.delay, shape=shape_obj)
164
+
165
+
166
+ @dataclass
167
+ class PulseqADC:
168
+ type: int
169
+ num_samples: int
170
+ dwell: float
171
+ delay: float
172
+
173
+ def to_bytes(self, endian=">") -> bytes:
174
+ return (
175
+ struct.pack(endian + "h", self.type)
176
+ + struct.pack(endian + "i", self.num_samples)
177
+ + struct.pack(endian + "f", self.dwell)
178
+ + struct.pack(endian + "f", self.delay)
179
+ )
180
+
181
+ @classmethod
182
+ def from_struct(cls, data: SimpleNamespace) -> "PulseqADC":
183
+ return cls(
184
+ type=1,
185
+ num_samples=data.num_samples,
186
+ dwell=data.dwell,
187
+ delay=data.delay,
188
+ )
189
+
190
+
191
+ @dataclass
192
+ class PulseqTrig:
193
+ type: int
194
+ channel: int
195
+ delay: float
196
+ duration: float
197
+
198
+ def to_bytes(self, endian=">") -> bytes:
199
+ return (
200
+ struct.pack(endian + "h", self.type)
201
+ + struct.pack(endian + "i", self.channel)
202
+ + struct.pack(endian + "f", self.delay)
203
+ + struct.pack(endian + "f", self.duration)
204
+ )
205
+
206
+ @classmethod
207
+ def from_struct(cls, data: SimpleNamespace) -> "PulseqTrig":
208
+ return cls(
209
+ type=1,
210
+ channel=CHANNEL_ENUM[data.channel],
211
+ delay=data.delay,
212
+ duration=data.duration,
213
+ )
214
+
215
+
216
+ class PulseqBlock:
217
+ """Pulseq block structure."""
218
+
219
+ def __init__(
220
+ self,
221
+ ID: int,
222
+ rf: SimpleNamespace = None,
223
+ gx: SimpleNamespace = None,
224
+ gy: SimpleNamespace = None,
225
+ gz: SimpleNamespace = None,
226
+ adc: SimpleNamespace = None,
227
+ trig: SimpleNamespace = None,
228
+ delay: SimpleNamespace = None,
229
+ ) -> "PulseqBlock":
230
+ self.ID = ID
231
+ args = [rf, gx, gy, gz, adc, trig, delay]
232
+ args = [arg for arg in args if arg is not None]
233
+ self.duration = pp.calc_duration(*args)
234
+ self.rf = PulseqRF.from_struct(rf) if rf else None
235
+ self.gx = PulseqGrad.from_struct(gx) if gx else None
236
+ self.gy = PulseqGrad.from_struct(gy) if gy else None
237
+ self.gz = PulseqGrad.from_struct(gz) if gz else None
238
+ self.adc = PulseqADC.from_struct(adc) if adc else None
239
+ self.trig = PulseqTrig.from_struct(trig) if trig else None
240
+
241
+ def to_bytes(self, endian=">") -> bytes: # noqa
242
+ bytes_data = struct.pack(endian + "i", self.ID) + struct.pack(
243
+ endian + "f", self.duration
244
+ )
245
+
246
+ # RF Event
247
+ if self.rf:
248
+ bytes_data += self.rf.to_bytes(endian)
249
+ else:
250
+ bytes_data += struct.pack(endian + "h", 0)
251
+
252
+ # Gradient Events
253
+ for grad in [self.gx, self.gy, self.gz]:
254
+ if grad:
255
+ bytes_data += grad.to_bytes(endian)
256
+ else:
257
+ bytes_data += struct.pack(endian + "h", 0)
258
+
259
+ # ADC Event
260
+ if self.adc:
261
+ bytes_data += self.adc.to_bytes(endian)
262
+ else:
263
+ bytes_data += struct.pack(endian + "h", 0)
264
+
265
+ # Trigger Event
266
+ if self.trig:
267
+ bytes_data += self.trig.to_bytes(endian)
268
+ else:
269
+ bytes_data += struct.pack(endian + "h", 0)
270
+
271
+ return bytes_data
272
+
273
+
274
+ class Segment:
275
+ """Ceq segment."""
276
+
277
+ def __init__(self, segment_id: int, block_ids: list[int]):
278
+ self.segment_id = segment_id
279
+ self.n_blocks_in_segment = len(block_ids)
280
+ self.block_ids = np.asarray(block_ids, dtype=np.int16)
281
+
282
+ def to_bytes(self, endian=">") -> bytes:
283
+ return (
284
+ struct.pack(endian + "h", self.segment_id)
285
+ + struct.pack(endian + "h", self.n_blocks_in_segment)
286
+ + self.block_ids.astype(endian + "i2").tobytes()
287
+ )
288
+
289
+
290
+ class Ceq:
291
+ """CEQ structure."""
292
+
293
+ def __init__(
294
+ self,
295
+ parent_blocks: list[PulseqBlock],
296
+ loop: list[list],
297
+ sections_edges: list[list[int]],
298
+ ):
299
+ loop = np.asarray(loop, dtype=np.float32)
300
+ segments = _build_segments(loop, sections_edges)
301
+
302
+ # build CEQ structure
303
+ self.n_max = loop.shape[0]
304
+ self.n_parent_blocks = len(parent_blocks)
305
+ self.n_segments = len(segments)
306
+ self.parent_blocks = parent_blocks
307
+ self.segments = segments
308
+ self.n_columns_in_loop_array = loop.shape[1] - 2 # discard "hasrot", "hasadc"
309
+ self.loop = loop[:, :-2]
310
+ self.max_b1 = _find_b1_max(parent_blocks)
311
+ self.duration = _calc_duration(self.loop[:, 0], self.loop[:, 9])
312
+ self.n_readouts = int(np.sum(loop[:, -2]))
313
+
314
+ def to_bytes(self, endian=">") -> bytes: # noqa
315
+ bytes_data = (
316
+ struct.pack(endian + "i", self.n_max)
317
+ + struct.pack(endian + "h", self.n_parent_blocks)
318
+ + struct.pack(endian + "h", self.n_segments)
319
+ )
320
+ for block in self.parent_blocks:
321
+ bytes_data += block.to_bytes(endian)
322
+ for segment in self.segments:
323
+ bytes_data += segment.to_bytes(endian)
324
+ bytes_data += struct.pack(endian + "h", self.n_columns_in_loop_array)
325
+ bytes_data += self.loop.astype(endian + "f4").tobytes()
326
+ bytes_data += struct.pack(endian + "f", self.max_b1)
327
+ bytes_data += struct.pack(endian + "f", self.duration)
328
+ bytes_data += struct.pack(endian + "i", self.n_readouts)
329
+
330
+ return bytes_data
331
+
332
+ def export(self, dformat="file"): # noqa
333
+ if dformat == "bytes":
334
+ return self.to_bytes(endian=">")
335
+ elif dformat == "file":
336
+ return self.to_bytes(endian="<")
337
+
338
+
339
+ @dataclass
340
+ class SequenceParams:
341
+ """
342
+ Python representation of the C SequenceParams struct.
343
+
344
+ Attributes
345
+ ----------
346
+ FOVx : Optional[float]
347
+ Field of view in mm (x-direction).
348
+ FOVy : Optional[float]
349
+ Field of view in mm (y-direction).
350
+ Nx : Optional[int]
351
+ Matrix size (x-direction).
352
+ Ny : Optional[int]
353
+ Matrix size (y-direction).
354
+ Nslices : Optional[int]
355
+ Number of slices.
356
+ Nechoes : Optional[int]
357
+ Number of echoes.
358
+ Nphases : Optional[int]
359
+ Number of phases.
360
+ slice_thickness : Optional[float]
361
+ Thickness of each slice (mm).
362
+ slice_spacing : Optional[float]
363
+ Spacing between slices (mm).
364
+ Rplane : Optional[float]
365
+ In-plane undersampling factor.
366
+ Rplane2 : Optional[float]
367
+ Additional in-plane undersampling factor.
368
+ Rslice : Optional[float]
369
+ Through-plane undersampling factor.
370
+ PFplane : Optional[float]
371
+ In-plane partial fourier.
372
+ PFslice : Optional[float]
373
+ Through-plane partial fourier.
374
+ ETL : Optional[int]
375
+ Number of k-space shots per readout.
376
+ TE : Optional[float]
377
+ Echo time (ms).
378
+ TE0 : Optional[float]
379
+ First echo time (ms) for multiecho.
380
+ TR : Optional[float]
381
+ Repetition time (ms).
382
+ Tprep : Optional[float]
383
+ Preparation time (ms).
384
+ Trecovery : Optional[float]
385
+ Recovery time (ms).
386
+ flip : Optional[float]
387
+ Flip angle in degrees.
388
+ flip2 : Optional[float]
389
+ Second flip angle in degrees.
390
+ refoc_flip : Optional[float]
391
+ Refocusing flip angle in degrees.
392
+ freq_dir : Optional[int]
393
+ Frequency direction (0: A/P; 1: S/L).
394
+ freq_verse : Optional[int]
395
+ Frequency verse (1: normal; -1: swapped).
396
+ phase_verse : Optional[int]
397
+ Phase verse (1: normal; -1: swapped).
398
+ bipolar_echoes : Optional[int]
399
+ Bipolar echoes (0: false, 1: true).
400
+ dwell : Optional[float]
401
+ ADC dwell time (s).
402
+ raster : Optional[float]
403
+ Waveform raster time (s).
404
+ gmax : Optional[float]
405
+ Maximum gradient strength (mT/m).
406
+ smax : Optional[float]
407
+ Maximum gradient slew rate (T/m/s).
408
+ b1_max : Optional[float]
409
+ Maximum RF value (uT).
410
+ b0_field : Optional[float]
411
+ System field strength (T).
412
+ """
413
+
414
+ FOVx: float | None = None
415
+ FOVy: float | None = None
416
+ Nx: int | None = None
417
+ Ny: int | None = None
418
+ Nslices: int | None = None
419
+ Nechoes: int | None = None
420
+ Nphases: int | None = None
421
+ slice_thickness: float | None = None
422
+ slice_spacing: float | None = None
423
+ Rplane: float | None = None
424
+ Rplane2: float | None = None
425
+ Rslice: float | None = None
426
+ PFplane: float | None = None
427
+ PFslice: float | None = None
428
+ ETL: int | None = None
429
+ TE: float | None = None
430
+ TE0: float | None = None
431
+ TR: float | None = None
432
+ Tprep: float | None = None
433
+ Trecovery: float | None = None
434
+ flip: float | None = None
435
+ flip2: float | None = None
436
+ refoc_flip: float | None = None
437
+ freq_dir: int | None = None
438
+ freq_verse: int | None = None
439
+ phase_verse: int | None = None
440
+ bipolar_echoes: int | None = None
441
+ dwell: float | None = None
442
+ raster: float | None = None
443
+ gmax: float | None = None
444
+ smax: float | None = None
445
+ b1_max: float | None = None
446
+ b0_field: float | None = None
447
+
448
+ @classmethod
449
+ def from_bytes(cls, data: bytes) -> "SequenceParams":
450
+ """
451
+ Deserialize from a byte array into a SequenceParams object.
452
+
453
+ Parameters
454
+ ----------
455
+ data : bytes
456
+ A byte array representing a serialized SequenceParams object.
457
+
458
+ Returns
459
+ -------
460
+ SequenceParams
461
+ A SequenceParams instance with attributes filled.
462
+ """
463
+ format_string = "2f 5h 2f 5f h 5f 3f 3h h 6f"
464
+ unpacked = struct.unpack(format_string, data)
465
+
466
+ # Replace -1 values with None
467
+ unpacked = [None if x == -1 or x == -1.0 else x for x in unpacked]
468
+
469
+ return cls(*unpacked)
470
+
471
+ def asdict(self) -> dict:
472
+ """
473
+ Return a dictionary of the dataclass, excluding None values.
474
+
475
+ Returns
476
+ -------
477
+ dict
478
+ A dictionary of the dataclass fields, excluding None values.
479
+ """
480
+ return {k: v for k, v in _asdict(self).items() if v is not None}
481
+
482
+
483
+ # %% local subroutines
484
+ def _build_segments(loop, sections_edges):
485
+ hasrot = np.ascontiguousarray(loop[:, -1]).astype(int)
486
+ parent_block_id = np.ascontiguousarray(loop[:, 1]).astype(int) * hasrot
487
+
488
+ # build section edges
489
+ if not sections_edges:
490
+ sections_edges = [0]
491
+ sections_edges = np.stack((sections_edges, sections_edges[1:] + [-1]), axis=-1)
492
+
493
+ # loop over sections and find segment definitions
494
+ segment_id = np.zeros(loop.shape[0], dtype=np.float32)
495
+ seg_definitions = []
496
+
497
+ # fill sections from 0 to n-1
498
+ n_sections = len(sections_edges)
499
+ for n in range(n_sections - 1):
500
+ section_start, section_end = sections_edges[n]
501
+ _seg_definition = _autoseg.find_segment_definitions(
502
+ parent_block_id[section_start:section_end]
503
+ )
504
+ _seg_definition = _autoseg.split_rotated_segments(_seg_definition)
505
+ seg_definitions.extend(_seg_definition)
506
+
507
+ # fill last section
508
+ section_start = sections_edges[-1][0]
509
+ _seg_definition = _autoseg.find_segment_definitions(parent_block_id[section_start:])
510
+ _seg_definition = _autoseg.split_rotated_segments(_seg_definition)
511
+ seg_definitions.extend(_seg_definition)
512
+
513
+ # for each block, find the segment it belongs to
514
+ for n in range(len(seg_definitions)):
515
+ idx = _autoseg.find_segments(parent_block_id, seg_definitions[n])
516
+ segment_id[idx] = n
517
+ loop[:, 0] = segment_id
518
+
519
+ # now build segment fields
520
+ n_segments = len(seg_definitions)
521
+ segments = []
522
+ for n in range(n_segments):
523
+ segments.append(Segment(n, seg_definitions[n]))
524
+
525
+ return segments
526
+
527
+
528
+ def _find_b1_max(parent_blocks):
529
+ return np.max(
530
+ [block.rf.wav.amplitude for block in parent_blocks if block.rf is not None]
531
+ )
532
+
533
+
534
+ def _calc_duration(segment_id, block_duration):
535
+ block_duration = block_duration.sum()
536
+
537
+ # total segment ringdown
538
+ n_seg_boundaries = (np.diff(segment_id) != 0).sum()
539
+ seg_ringdown_duration = SEGMENT_RINGDOWN_TIME * n_seg_boundaries
540
+
541
+ return block_duration + seg_ringdown_duration