seqeyes-python 0.2.2__py3-none-any.whl → 0.2.6__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.
seqeyes/__init__.py CHANGED
@@ -29,4 +29,4 @@ from seqeyes._plot import set, reset
29
29
  from seqeyes._renderer import SeqEyesViewer
30
30
 
31
31
  __all__ = ["set", "reset", "SeqEyesViewer"]
32
- __version__ = "0.2.2"
32
+ __version__ = "0.2.6"
seqeyes/_renderer.py CHANGED
@@ -12,7 +12,7 @@ import base64
12
12
  import json
13
13
  import os
14
14
  from pathlib import Path
15
- from typing import Optional
15
+ from typing import Any, Optional, Union
16
16
 
17
17
 
18
18
  # ── Paths ─────────────────────────────────────────────────────────────────
@@ -27,6 +27,7 @@ _BUNDLE_CANDIDATES = [
27
27
  Path.cwd() / "web" / "pulseq-bundle.js",
28
28
  ]
29
29
  _VALID_GRAD_UNITS = {"Hz/m", "mT/m", "G/cm"}
30
+ SequenceSource = Union[str, bytes, bytearray, memoryview]
30
31
 
31
32
 
32
33
  def _find_bundle() -> str:
@@ -60,7 +61,7 @@ def _normalize_grad_disp(unit: str) -> str:
60
61
 
61
62
 
62
63
  def _build_html(
63
- seq_text: str,
64
+ sequence_source: SequenceSource,
64
65
  *,
65
66
  theme: Optional[str] = None,
66
67
  inject_bundle: bool = True,
@@ -74,8 +75,8 @@ def _build_html(
74
75
 
75
76
  Parameters
76
77
  ----------
77
- seq_text : str
78
- Raw .seq file content.
78
+ sequence_source : str or bytes-like
79
+ Raw text ``.seq`` content or binary ``.bseq`` bytes.
79
80
  theme : str or None
80
81
  CSS theme class to apply to ``<body>``.
81
82
  inject_bundle : bool
@@ -93,7 +94,9 @@ def _build_html(
93
94
  """
94
95
  template = _read_viewer_template()
95
96
  grad_disp = _normalize_grad_disp(grad_disp)
96
- seq_b64 = base64.b64encode(seq_text.encode("utf-8")).decode("ascii")
97
+ source_is_text = isinstance(sequence_source, str)
98
+ source_bytes = sequence_source.encode("utf-8") if source_is_text else bytes(sequence_source)
99
+ source_b64 = base64.b64encode(source_bytes).decode("ascii")
97
100
 
98
101
  # 1. Inject the pulseq-bundle.js
99
102
  bundle_placeholder = "<!-- PULSEQ_BUNDLE_PLACEHOLDER -->"
@@ -108,7 +111,9 @@ def _build_html(
108
111
 
109
112
  # 2. Build options injection block (sequence data + display opts)
110
113
  opts = [
111
- f"window.SEQEYES_RAW_B64 = {json.dumps(seq_b64)};",
114
+ f"window.SEQEYES_RAW_B64 = {json.dumps(source_b64)};",
115
+ f"window.SEQEYES_SOURCE_KIND = {json.dumps('text' if source_is_text else 'bytes')};",
116
+ f"window.SEQEYES_SOURCE_NAME = {json.dumps(label)};",
112
117
  f"window.SEQEYES_LABEL = {json.dumps(label)};",
113
118
  f"window.SEQEYES_SHOW_BLOCKS = {json.dumps(show_blocks)};",
114
119
  f"window.SEQEYES_TIME_RANGE = {json.dumps(list(time_range))};",
@@ -144,8 +149,8 @@ class SeqEyesViewer:
144
149
 
145
150
  Parameters
146
151
  ----------
147
- seq_text : str
148
- Raw .seq file content as a string.
152
+ sequence_source : str or bytes-like
153
+ Raw text ``.seq`` content or binary ``.bseq`` bytes.
149
154
  label : str
150
155
  Display label (not yet rendered on the viewer).
151
156
  show_blocks : bool
@@ -167,7 +172,7 @@ class SeqEyesViewer:
167
172
 
168
173
  def __init__(
169
174
  self,
170
- seq_text: str,
175
+ sequence_source: SequenceSource,
171
176
  *,
172
177
  label: str = "",
173
178
  show_blocks: bool = False,
@@ -178,7 +183,9 @@ class SeqEyesViewer:
178
183
  width: str = "100%",
179
184
  height: str = "550px",
180
185
  ) -> None:
181
- self._seq_text = seq_text
186
+ if not isinstance(sequence_source, (str, bytes, bytearray, memoryview)):
187
+ raise TypeError("sequence_source must be .seq text or .bseq bytes")
188
+ self._sequence_source = sequence_source
182
189
  self._label = label
183
190
  self._show_blocks = show_blocks
184
191
  self._time_range = time_range
@@ -188,12 +195,28 @@ class SeqEyesViewer:
188
195
  self._width = width
189
196
  self._height = height
190
197
 
198
+ @classmethod
199
+ def from_file(cls, path: Union[str, os.PathLike[str]], **kwargs: Any) -> "SeqEyesViewer":
200
+ """Create a viewer from a local ``.seq`` or ``.bseq`` file."""
201
+ source_path = Path(path)
202
+ if source_path.suffix.lower() not in {".seq", ".bseq"}:
203
+ raise ValueError("SeqEyesViewer.from_file() expects a .seq or .bseq file")
204
+ if not source_path.is_file():
205
+ raise FileNotFoundError(source_path)
206
+ kwargs.setdefault("label", source_path.name)
207
+ source: SequenceSource = (
208
+ source_path.read_text(encoding="utf-8")
209
+ if source_path.suffix.lower() == ".seq"
210
+ else source_path.read_bytes()
211
+ )
212
+ return cls(source, **kwargs)
213
+
191
214
  # ── Jupyter integration ───────────────────────────────────────────
192
215
 
193
216
  def _repr_html_(self) -> str:
194
217
  """Return an HTML iframe that Jupyter renders inline."""
195
218
  html = _build_html(
196
- self._seq_text,
219
+ self._sequence_source,
197
220
  theme=self._theme,
198
221
  label=self._label,
199
222
  show_blocks=self._show_blocks,
@@ -231,7 +254,7 @@ class SeqEyesViewer:
231
254
  def to_html(self, *, inject_bundle: bool = True) -> str:
232
255
  """Return the full standalone HTML string (for saving to a file)."""
233
256
  return _build_html(
234
- self._seq_text,
257
+ self._sequence_source,
235
258
  theme=self._theme,
236
259
  inject_bundle=inject_bundle,
237
260
  label=self._label,
@@ -30,15 +30,20 @@ var Pulseq = (() => {
30
30
  decodeAllBlocks: () => decodeAllBlocks,
31
31
  detectSequenceTiming: () => detectSequenceTiming,
32
32
  exportKspaceArtifacts: () => exportKspaceArtifacts,
33
+ exportKspaceArtifactsFromBytes: () => exportKspaceArtifactsFromBytes,
34
+ exportKspaceArtifactsFromSequence: () => exportKspaceArtifactsFromSequence,
33
35
  formatTrajectoryText: () => formatTrajectoryText,
34
36
  getTotalDuration: () => getTotalDuration,
37
+ hasPulseqBinaryMagic: () => hasPulseqBinaryMagic,
35
38
  parsePnsHardwareAsc: () => parsePnsHardwareAsc,
39
+ parseSequenceBinary: () => parseSequenceBinary,
40
+ parseSequenceBytes: () => parseSequenceBytes,
36
41
  parseSequenceText: () => parseSequenceText,
37
42
  safePnsModel: () => safePnsModel
38
43
  });
39
44
 
40
45
  // package.json
41
- var version = "0.2.2";
46
+ var version = "0.2.6";
42
47
 
43
48
  // src/pulseq/decompressor.ts
44
49
  function decompressShape(compressed, numSamples) {
@@ -103,6 +108,181 @@ var Pulseq = (() => {
103
108
  return major * 1e6 + minor * 1e3 + revision;
104
109
  }
105
110
 
111
+ // src/pulseq/readerShared.ts
112
+ function createEmptySequence() {
113
+ return {
114
+ version: { major: 1, minor: 0, revision: 0 },
115
+ versionCombined: 0,
116
+ definitions: /* @__PURE__ */ new Map(),
117
+ definitionsRaw: /* @__PURE__ */ new Map(),
118
+ blocks: [],
119
+ rfs: /* @__PURE__ */ new Map(),
120
+ arbitraryGrads: /* @__PURE__ */ new Map(),
121
+ trapGrads: /* @__PURE__ */ new Map(),
122
+ adcs: /* @__PURE__ */ new Map(),
123
+ extensions: /* @__PURE__ */ new Map(),
124
+ extensionNames: /* @__PURE__ */ new Map(),
125
+ extensionTypes: /* @__PURE__ */ new Map(),
126
+ triggers: [],
127
+ ncos: [],
128
+ rotations: [],
129
+ labelSets: [],
130
+ labelIncs: [],
131
+ softDelays: [],
132
+ rfShims: [],
133
+ shapes: /* @__PURE__ */ new Map(),
134
+ rasterTimes: { blockDurationRaster: 1e-5, gradientRaster: 1e-5, rfRaster: 1e-6, adcRaster: 1e-7 }
135
+ };
136
+ }
137
+ function parseError(message) {
138
+ throw new Error(`Pulseq parse error: ${message}`);
139
+ }
140
+ function extensionNameToType(name) {
141
+ switch (name.toUpperCase()) {
142
+ case "TRIGGERS":
143
+ return 1 /* EXT_TRIGGER */;
144
+ case "ROTATIONS":
145
+ return 2 /* EXT_ROTATION */;
146
+ case "LABELSET":
147
+ return 3 /* EXT_LABELSET */;
148
+ case "LABELINC":
149
+ return 4 /* EXT_LABELINC */;
150
+ case "DELAYS":
151
+ return 5 /* EXT_DELAY */;
152
+ case "RF_SHIMS":
153
+ return 6 /* EXT_RF_SHIM */;
154
+ case "NCO":
155
+ return 100 /* EXT_NCO */;
156
+ default:
157
+ return 999 /* EXT_UNKNOWN */;
158
+ }
159
+ }
160
+ var KNOWN_LABELS = {
161
+ "SLC": { labelId: 0, flagId: 0 },
162
+ "SEG": { labelId: 1, flagId: 0 },
163
+ "REP": { labelId: 2, flagId: 0 },
164
+ "AVG": { labelId: 3, flagId: 0 },
165
+ "ECO": { labelId: 4, flagId: 0 },
166
+ "PHS": { labelId: 5, flagId: 0 },
167
+ "SET": { labelId: 6, flagId: 0 },
168
+ "ACQ": { labelId: 7, flagId: 0 },
169
+ "LIN": { labelId: 8, flagId: 0 },
170
+ "PAR": { labelId: 9, flagId: 0 },
171
+ "ONCE": { labelId: 10, flagId: 0 },
172
+ "NAV": { labelId: 0, flagId: 1 },
173
+ "REV": { labelId: 0, flagId: 2 },
174
+ "SMS": { labelId: 0, flagId: 4 },
175
+ "REF": { labelId: 0, flagId: 8 },
176
+ "IMA": { labelId: 0, flagId: 16 },
177
+ "OFF": { labelId: 0, flagId: 32 },
178
+ "NOISE": { labelId: 0, flagId: 64 },
179
+ "PMC": { labelId: 0, flagId: 128 },
180
+ "NOPOS": { labelId: 0, flagId: 256 },
181
+ "NOROT": { labelId: 0, flagId: 512 },
182
+ "NOSCL": { labelId: 0, flagId: 1024 }
183
+ };
184
+ var unknownLabelCounter = 0;
185
+ var unknownLabels = /* @__PURE__ */ new Map();
186
+ function resetUnknownLabels() {
187
+ unknownLabelCounter = 0;
188
+ unknownLabels.clear();
189
+ }
190
+ function decodeLabel(name) {
191
+ const known = KNOWN_LABELS[name];
192
+ if (known) return known;
193
+ let id = unknownLabels.get(name);
194
+ if (id === void 0) {
195
+ id = 1e3 + unknownLabelCounter++;
196
+ unknownLabels.set(name, id);
197
+ }
198
+ return { labelId: id, flagId: 0 };
199
+ }
200
+ function extractRasterTimes(seq) {
201
+ const set = (key, field) => {
202
+ const value = seq.definitions.get(key);
203
+ if (value?.length) seq.rasterTimes[field] = value[0];
204
+ };
205
+ set("BlockDurationRaster", "blockDurationRaster");
206
+ set("GradientRasterTime", "gradientRaster");
207
+ set("RadiofrequencyRasterTime", "rfRaster");
208
+ set("AdcRasterTime", "adcRaster");
209
+ }
210
+ function validateSequence(seq, seenSections) {
211
+ if (!seenSections.has("VERSION")) parseError("Required [VERSION] section is missing");
212
+ if (seq.version.major !== 1 || seq.version.minor > 5) {
213
+ parseError(`Unsupported Pulseq version ${seq.version.major}.${seq.version.minor}.${seq.version.revision}`);
214
+ }
215
+ const version2 = seq.versionCombined > 0 ? seq.versionCombined : makeVersionCombined(seq.version.major, seq.version.minor, seq.version.revision);
216
+ if (version2 >= VER_PRE_14) {
217
+ requireNumericDefinition(seq, "AdcRasterTime");
218
+ requireNumericDefinition(seq, "GradientRasterTime");
219
+ requireNumericDefinition(seq, "RadiofrequencyRasterTime");
220
+ requireNumericDefinition(seq, "BlockDurationRaster");
221
+ }
222
+ if (version2 >= VER_V15001) {
223
+ const required = seq.definitionsRaw.get("RequiredExtensions")?.split(/\s+/).filter(Boolean) ?? [];
224
+ for (const name of required) {
225
+ if (extensionNameToType(name) === 999 /* EXT_UNKNOWN */) {
226
+ parseError(`Unknown required extension '${name}'`);
227
+ }
228
+ }
229
+ }
230
+ if (!seenSections.has("BLOCKS")) parseError("Required [BLOCKS] section is missing");
231
+ for (const block of seq.blocks) {
232
+ if (block.rfId > 0 && !seq.rfs.has(block.rfId)) {
233
+ parseError(`Block ${block.num} references undefined RF event ${block.rfId}`);
234
+ }
235
+ for (const [channel, gradId] of [["GX", block.gxId], ["GY", block.gyId], ["GZ", block.gzId]]) {
236
+ if (gradId > 0 && !seq.arbitraryGrads.has(gradId) && !seq.trapGrads.has(gradId)) {
237
+ parseError(`Block ${block.num} references undefined ${channel} gradient event ${gradId}`);
238
+ }
239
+ }
240
+ if (block.adcId > 0 && !seq.adcs.has(block.adcId)) {
241
+ parseError(`Block ${block.num} references undefined ADC event ${block.adcId}`);
242
+ }
243
+ if (block.extId > 0 && !seq.extensions.has(block.extId)) {
244
+ parseError(`Block ${block.num} references undefined extension list ${block.extId}`);
245
+ }
246
+ }
247
+ for (const ext of seq.extensions.values()) {
248
+ if (ext.nextId > 0 && !seq.extensions.has(ext.nextId)) {
249
+ parseError(`Extension list ${ext.id} references undefined next extension ${ext.nextId}`);
250
+ }
251
+ const type = seq.extensionTypes.get(ext.type) ?? 999 /* EXT_UNKNOWN */;
252
+ if (type === 999 /* EXT_UNKNOWN */) continue;
253
+ if (!extensionPayloadExists(seq, type, ext.ref)) {
254
+ const name = seq.extensionNames.get(ext.type) ?? `type ${ext.type}`;
255
+ parseError(`Extension list ${ext.id} references undefined ${name} payload ${ext.ref}`);
256
+ }
257
+ }
258
+ }
259
+ function requireNumericDefinition(seq, name) {
260
+ const value = seq.definitions.get(name);
261
+ if (!value || value.length === 0 || !Number.isFinite(value[0])) {
262
+ parseError(`Required definition ${name} is not present in the file`);
263
+ }
264
+ }
265
+ function extensionPayloadExists(seq, type, ref) {
266
+ switch (type) {
267
+ case 1 /* EXT_TRIGGER */:
268
+ return seq.triggers.some((value) => value.id === ref);
269
+ case 2 /* EXT_ROTATION */:
270
+ return seq.rotations.some((value) => value.id === ref);
271
+ case 3 /* EXT_LABELSET */:
272
+ return seq.labelSets.some((value) => value.id === ref);
273
+ case 4 /* EXT_LABELINC */:
274
+ return seq.labelIncs.some((value) => value.id === ref);
275
+ case 5 /* EXT_DELAY */:
276
+ return seq.softDelays.some((value) => value.id === ref);
277
+ case 6 /* EXT_RF_SHIM */:
278
+ return seq.rfShims.some((value) => value.id === ref);
279
+ case 100 /* EXT_NCO */:
280
+ return seq.ncos.some((value) => value.id === ref);
281
+ default:
282
+ return false;
283
+ }
284
+ }
285
+
106
286
  // src/pulseq/reader.ts
107
287
  function parseSequenceText(text) {
108
288
  const seq = createEmptySequence();
@@ -182,38 +362,10 @@ var Pulseq = (() => {
182
362
  break;
183
363
  }
184
364
  }
185
- function createEmptySequence() {
186
- return {
187
- version: { major: 1, minor: 0, revision: 0 },
188
- versionCombined: 0,
189
- definitions: /* @__PURE__ */ new Map(),
190
- definitionsRaw: /* @__PURE__ */ new Map(),
191
- blocks: [],
192
- rfs: /* @__PURE__ */ new Map(),
193
- arbitraryGrads: /* @__PURE__ */ new Map(),
194
- trapGrads: /* @__PURE__ */ new Map(),
195
- adcs: /* @__PURE__ */ new Map(),
196
- extensions: /* @__PURE__ */ new Map(),
197
- extensionNames: /* @__PURE__ */ new Map(),
198
- extensionTypes: /* @__PURE__ */ new Map(),
199
- triggers: [],
200
- ncos: [],
201
- rotations: [],
202
- labelSets: [],
203
- labelIncs: [],
204
- softDelays: [],
205
- rfShims: [],
206
- shapes: /* @__PURE__ */ new Map(),
207
- rasterTimes: { blockDurationRaster: 1e-5, gradientRaster: 1e-5, rfRaster: 1e-6, adcRaster: 1e-7 }
208
- };
209
- }
210
365
  function ver(seq) {
211
366
  if (seq.versionCombined > 0) return seq.versionCombined;
212
367
  return makeVersionCombined(seq.version.major, seq.version.minor, seq.version.revision);
213
368
  }
214
- function parseError(message) {
215
- throw new Error(`Pulseq parse error: ${message}`);
216
- }
217
369
  function requireFieldCount(section, line, count, allowed) {
218
370
  const allowedCounts = Array.isArray(allowed) ? allowed : [allowed];
219
371
  if (!allowedCounts.includes(count)) {
@@ -457,8 +609,7 @@ var Pulseq = (() => {
457
609
  }
458
610
  function parseExtensions(seq, valid) {
459
611
  const vc = ver(seq);
460
- _unknownLabelCounter = 0;
461
- _unknownLabels.clear();
612
+ resetUnknownLabels();
462
613
  let i = 0;
463
614
  while (i < valid.length) {
464
615
  const line = valid[i].trim();
@@ -518,26 +669,6 @@ var Pulseq = (() => {
518
669
  }
519
670
  }
520
671
  }
521
- function extensionNameToType(name) {
522
- switch (name.toUpperCase()) {
523
- case "TRIGGERS":
524
- return 1 /* EXT_TRIGGER */;
525
- case "ROTATIONS":
526
- return 2 /* EXT_ROTATION */;
527
- case "LABELSET":
528
- return 3 /* EXT_LABELSET */;
529
- case "LABELINC":
530
- return 4 /* EXT_LABELINC */;
531
- case "DELAYS":
532
- return 5 /* EXT_DELAY */;
533
- case "RF_SHIMS":
534
- return 6 /* EXT_RF_SHIM */;
535
- case "NCO":
536
- return 100 /* EXT_NCO */;
537
- default:
538
- return 999 /* EXT_UNKNOWN */;
539
- }
540
- }
541
672
  function parseTriggerSpecs(seq, lines) {
542
673
  for (const line of lines) {
543
674
  const p = splitFields(line);
@@ -593,42 +724,6 @@ var Pulseq = (() => {
593
724
  }
594
725
  }
595
726
  }
596
- var KNOWN_LABELS = {
597
- "SLC": { labelId: 0, flagId: 0 },
598
- "SEG": { labelId: 1, flagId: 0 },
599
- "REP": { labelId: 2, flagId: 0 },
600
- "AVG": { labelId: 3, flagId: 0 },
601
- "ECO": { labelId: 4, flagId: 0 },
602
- "PHS": { labelId: 5, flagId: 0 },
603
- "SET": { labelId: 6, flagId: 0 },
604
- "ACQ": { labelId: 7, flagId: 0 },
605
- "LIN": { labelId: 8, flagId: 0 },
606
- "PAR": { labelId: 9, flagId: 0 },
607
- "ONCE": { labelId: 10, flagId: 0 },
608
- "NAV": { labelId: 0, flagId: 1 },
609
- "REV": { labelId: 0, flagId: 2 },
610
- "SMS": { labelId: 0, flagId: 4 },
611
- "REF": { labelId: 0, flagId: 8 },
612
- "IMA": { labelId: 0, flagId: 16 },
613
- "OFF": { labelId: 0, flagId: 32 },
614
- "NOISE": { labelId: 0, flagId: 64 },
615
- "PMC": { labelId: 0, flagId: 128 },
616
- "NOPOS": { labelId: 0, flagId: 256 },
617
- "NOROT": { labelId: 0, flagId: 512 },
618
- "NOSCL": { labelId: 0, flagId: 1024 }
619
- };
620
- var _unknownLabelCounter = 0;
621
- var _unknownLabels = /* @__PURE__ */ new Map();
622
- function decodeLabel(name) {
623
- const known = KNOWN_LABELS[name];
624
- if (known) return known;
625
- let id = _unknownLabels.get(name);
626
- if (id === void 0) {
627
- id = 1e3 + _unknownLabelCounter++;
628
- _unknownLabels.set(name, id);
629
- }
630
- return { labelId: id, flagId: 0 };
631
- }
632
727
  function parseLabelSpecs(seq, lines, isSet) {
633
728
  for (const line of lines) {
634
729
  const p = splitFields(line);
@@ -739,90 +834,563 @@ var Pulseq = (() => {
739
834
  this.rawCount = 0;
740
835
  }
741
836
  };
742
- function extractRasterTimes(seq) {
743
- const set = (key, field) => {
744
- const v = seq.definitions.get(key);
745
- if (v?.length) seq.rasterTimes[field] = v[0];
746
- };
747
- set("BlockDurationRaster", "blockDurationRaster");
748
- set("GradientRasterTime", "gradientRaster");
749
- set("RadiofrequencyRasterTime", "rfRaster");
750
- set("AdcRasterTime", "adcRaster");
837
+
838
+ // src/pulseq/binaryReader.ts
839
+ var PULSEQ_BINARY_VERSION = Object.freeze({ major: 1, minor: 5, revision: 2 });
840
+ var MAGIC = new Uint8Array([1, 112, 117, 108, 115, 101, 113, 2]);
841
+ var SECTION_PREFIX = 0xffffffff00000000n;
842
+ var SECTION = Object.freeze({
843
+ definitions: SECTION_PREFIX | 1n,
844
+ blocks: SECTION_PREFIX | 2n,
845
+ rf: SECTION_PREFIX | 3n,
846
+ gradients: SECTION_PREFIX | 4n,
847
+ trapezoids: SECTION_PREFIX | 5n,
848
+ adc: SECTION_PREFIX | 6n,
849
+ legacyDelays: SECTION_PREFIX | 7n,
850
+ shapes: SECTION_PREFIX | 8n,
851
+ extensions: SECTION_PREFIX | 9n,
852
+ triggers: SECTION_PREFIX | 10n,
853
+ labelSet: SECTION_PREFIX | 11n,
854
+ labelInc: SECTION_PREFIX | 12n,
855
+ softDelays: SECTION_PREFIX | 13n,
856
+ rfShims: SECTION_PREFIX | 14n,
857
+ rotations: SECTION_PREFIX | 15n,
858
+ signature: SECTION_PREFIX | 0x00ffffffn
859
+ });
860
+ var MAX_RECORDS = 1e8;
861
+ var MAX_STRING_BYTES = 16 * 1024 * 1024;
862
+ var MAX_SHAPE_SAMPLES = 1e8;
863
+ var BINARY_LABELS = Object.freeze([
864
+ "SLC",
865
+ "SEG",
866
+ "REP",
867
+ "AVG",
868
+ "SET",
869
+ "ECO",
870
+ "PHS",
871
+ "LIN",
872
+ "PAR",
873
+ "ACQ",
874
+ "TRID",
875
+ "NAV",
876
+ "REV",
877
+ "SMS",
878
+ "REF",
879
+ "IMA",
880
+ "OFF",
881
+ "NOISE",
882
+ "PMC",
883
+ "NOROT",
884
+ "NOPOS",
885
+ "NOSCL",
886
+ "ONCE"
887
+ ]);
888
+ function hasPulseqBinaryMagic(bytes) {
889
+ if (bytes.byteLength < MAGIC.byteLength) return false;
890
+ for (let i = 0; i < MAGIC.byteLength; i++) {
891
+ if (bytes[i] !== MAGIC[i]) return false;
892
+ }
893
+ return true;
894
+ }
895
+ function parseSequenceBinary(bytes) {
896
+ const reader = new BinaryReader(bytes);
897
+ const magic = reader.bytes(MAGIC.byteLength, "file header");
898
+ if (!hasPulseqBinaryMagic(magic)) {
899
+ reader.fail("not a Pulseq binary file", 0);
900
+ }
901
+ const seq = createEmptySequence();
902
+ resetUnknownLabels();
903
+ seq.version.major = reader.safeInt64("version major");
904
+ seq.version.minor = reader.safeInt64("version minor");
905
+ seq.version.revision = reader.safeInt64("version revision");
906
+ seq.versionCombined = makeVersionCombined(
907
+ seq.version.major,
908
+ seq.version.minor,
909
+ seq.version.revision
910
+ );
911
+ assertSupportedVersion(seq, reader);
912
+ const seenSections = /* @__PURE__ */ new Set(["VERSION"]);
913
+ while (!reader.eof()) {
914
+ const sectionOffset = reader.position;
915
+ const section = reader.uint64("section code");
916
+ switch (section) {
917
+ case SECTION.definitions:
918
+ readDefinitions(reader, seq);
919
+ seenSections.add("DEFINITIONS");
920
+ break;
921
+ case SECTION.blocks:
922
+ readBlocks(reader, seq);
923
+ seenSections.add("BLOCKS");
924
+ break;
925
+ case SECTION.rf:
926
+ readRf(reader, seq);
927
+ seenSections.add("RF");
928
+ break;
929
+ case SECTION.gradients:
930
+ readGradients(reader, seq);
931
+ seenSections.add("GRADIENTS");
932
+ break;
933
+ case SECTION.trapezoids:
934
+ readTrapezoids(reader, seq);
935
+ seenSections.add("TRAP");
936
+ break;
937
+ case SECTION.adc:
938
+ readAdc(reader, seq);
939
+ seenSections.add("ADC");
940
+ break;
941
+ case SECTION.legacyDelays:
942
+ readLegacyDelays(reader);
943
+ break;
944
+ case SECTION.shapes:
945
+ readShapes(reader, seq);
946
+ seenSections.add("SHAPES");
947
+ break;
948
+ case SECTION.extensions:
949
+ readExtensions(reader, seq);
950
+ seenSections.add("EXTENSIONS");
951
+ break;
952
+ case SECTION.triggers:
953
+ readTriggers(reader, seq);
954
+ break;
955
+ case SECTION.labelSet:
956
+ readLabels(reader, seq, true);
957
+ break;
958
+ case SECTION.labelInc:
959
+ readLabels(reader, seq, false);
960
+ break;
961
+ case SECTION.softDelays:
962
+ readSoftDelays(reader, seq);
963
+ break;
964
+ case SECTION.rfShims:
965
+ readRfShims(reader, seq);
966
+ break;
967
+ case SECTION.rotations:
968
+ readRotations(reader, seq);
969
+ break;
970
+ case SECTION.signature:
971
+ readSignature(reader, seq, sectionOffset);
972
+ break;
973
+ default:
974
+ reader.fail(`unknown section code 0x${section.toString(16)}`, sectionOffset);
975
+ }
976
+ }
977
+ extractRasterTimes(seq);
978
+ validateSequence(seq, seenSections);
979
+ return seq;
751
980
  }
752
- function validateSequence(seq, seenSections) {
753
- if (!seenSections.has("VERSION")) parseError("Required [VERSION] section is missing");
754
- if (seq.version.major !== 1 || seq.version.minor > 5) {
755
- parseError(`Unsupported Pulseq version ${seq.version.major}.${seq.version.minor}.${seq.version.revision}`);
981
+ function assertSupportedVersion(seq, reader) {
982
+ const expected = PULSEQ_BINARY_VERSION;
983
+ if (seq.version.major !== expected.major || seq.version.minor !== expected.minor || seq.version.revision !== expected.revision) {
984
+ reader.fail(
985
+ `unsupported Pulseq binary version ${seq.version.major}.${seq.version.minor}.${seq.version.revision}; expected ${expected.major}.${expected.minor}.${expected.revision}`,
986
+ MAGIC.byteLength
987
+ );
756
988
  }
757
- const vc = ver(seq);
758
- if (vc >= VER_PRE_14) {
759
- requireNumericDefinition(seq, "AdcRasterTime");
760
- requireNumericDefinition(seq, "GradientRasterTime");
761
- requireNumericDefinition(seq, "RadiofrequencyRasterTime");
762
- requireNumericDefinition(seq, "BlockDurationRaster");
989
+ }
990
+ function readDefinitions(reader, seq) {
991
+ const count = reader.count64("DEFINITIONS count", 9);
992
+ for (let i = 0; i < count; i++) {
993
+ const keyLength = reader.length32("DEFINITIONS key length");
994
+ const key = reader.string(keyLength, "DEFINITIONS key");
995
+ const valueCount = reader.length32("DEFINITIONS value count", MAX_RECORDS);
996
+ const valueType = reader.char("DEFINITIONS value type");
997
+ if (valueType === "f") {
998
+ reader.requireArray(valueCount, 8, "DEFINITIONS float values");
999
+ const values = new Array(valueCount);
1000
+ for (let j = 0; j < valueCount; j++) values[j] = reader.float64("DEFINITIONS float value");
1001
+ seq.definitions.set(key, values);
1002
+ seq.definitionsRaw.set(key, values.join(" "));
1003
+ } else if (valueType === "i") {
1004
+ reader.requireArray(valueCount, 4, "DEFINITIONS integer values");
1005
+ const values = new Array(valueCount);
1006
+ for (let j = 0; j < valueCount; j++) values[j] = reader.int32("DEFINITIONS integer value");
1007
+ seq.definitions.set(key, values);
1008
+ seq.definitionsRaw.set(key, values.join(" "));
1009
+ } else if (valueType === "c") {
1010
+ const raw = reader.string(valueCount, "DEFINITIONS character value");
1011
+ const value = raw.endsWith("\0") ? raw.slice(0, -1) : raw;
1012
+ seq.definitions.set(key, []);
1013
+ seq.definitionsRaw.set(key, value);
1014
+ } else {
1015
+ reader.fail(`unknown definition value type '${valueType}'`);
1016
+ }
763
1017
  }
764
- if (vc >= VER_V15001) {
765
- const required = seq.definitionsRaw.get("RequiredExtensions")?.split(/\s+/).filter(Boolean) ?? [];
766
- for (const name of required) {
767
- if (extensionNameToType(name) === 999 /* EXT_UNKNOWN */) {
768
- parseError(`Unknown required extension '${name}'`);
769
- }
1018
+ }
1019
+ function readBlocks(reader, seq) {
1020
+ const count = reader.count64("BLOCKS count", 32);
1021
+ seq.blocks.length = 0;
1022
+ for (let i = 0; i < count; i++) {
1023
+ seq.blocks.push({
1024
+ num: i + 1,
1025
+ dur: reader.nonNegativeSafeInt64("BLOCKS duration"),
1026
+ rfId: reader.int32("BLOCKS RF id"),
1027
+ gxId: reader.int32("BLOCKS Gx id"),
1028
+ gyId: reader.int32("BLOCKS Gy id"),
1029
+ gzId: reader.int32("BLOCKS Gz id"),
1030
+ adcId: reader.int32("BLOCKS ADC id"),
1031
+ extId: reader.int32("BLOCKS extension id")
1032
+ });
1033
+ }
1034
+ }
1035
+ function readRf(reader, seq) {
1036
+ const count = reader.count64("RF count", 73);
1037
+ seq.rfs.clear();
1038
+ for (let i = 0; i < count; i++) {
1039
+ const id = reader.int32("RF id");
1040
+ const amplitude = reader.float64("RF amplitude");
1041
+ const magShapeId = reader.int32("RF magnitude shape id");
1042
+ const phaseShapeId = reader.int32("RF phase shape id");
1043
+ const timeShapeId = reader.int32("RF time shape id");
1044
+ const center = psToUs(reader.safeInt64("RF center"));
1045
+ const delay = psToUsRounded(reader.safeInt64("RF delay"));
1046
+ const freqPPM = reader.float64("RF frequency ppm");
1047
+ const phasePPM = reader.float64("RF phase ppm");
1048
+ const freqOffset = reader.float64("RF frequency offset");
1049
+ const phaseOffset = reader.float64("RF phase offset");
1050
+ const use = reader.char("RF use").toLowerCase();
1051
+ if (!/^[erisu]$/.test(use)) reader.fail(`invalid RF use flag '${use}'`);
1052
+ seq.rfs.set(id, {
1053
+ id,
1054
+ amplitude,
1055
+ magShapeId,
1056
+ phaseShapeId,
1057
+ timeShapeId,
1058
+ center,
1059
+ delay,
1060
+ freqPPM,
1061
+ phasePPM,
1062
+ freqOffset,
1063
+ phaseOffset,
1064
+ phaseModShapeId: 0,
1065
+ use
1066
+ });
1067
+ }
1068
+ }
1069
+ function readGradients(reader, seq) {
1070
+ const count = reader.count64("GRADIENTS count", 44);
1071
+ for (let i = 0; i < count; i++) {
1072
+ const id = reader.int32("GRADIENTS id");
1073
+ seq.arbitraryGrads.set(id, {
1074
+ id,
1075
+ amplitude: reader.float64("GRADIENTS amplitude"),
1076
+ first: reader.float64("GRADIENTS first"),
1077
+ last: reader.float64("GRADIENTS last"),
1078
+ shapeId: reader.int32("GRADIENTS shape id"),
1079
+ timeId: reader.int32("GRADIENTS time shape id"),
1080
+ delay: psToUsRounded(reader.safeInt64("GRADIENTS delay"))
1081
+ });
1082
+ }
1083
+ }
1084
+ function readTrapezoids(reader, seq) {
1085
+ const count = reader.count64("TRAP count", 44);
1086
+ for (let i = 0; i < count; i++) {
1087
+ const id = reader.int32("TRAP id");
1088
+ seq.trapGrads.set(id, {
1089
+ id,
1090
+ amplitude: reader.float64("TRAP amplitude"),
1091
+ rise: psToUsRounded(reader.safeInt64("TRAP rise")),
1092
+ flat: psToUsRounded(reader.safeInt64("TRAP flat")),
1093
+ fall: psToUsRounded(reader.safeInt64("TRAP fall")),
1094
+ delay: psToUsRounded(reader.safeInt64("TRAP delay"))
1095
+ });
1096
+ }
1097
+ }
1098
+ function readAdc(reader, seq) {
1099
+ const count = reader.count64("ADC count", 64);
1100
+ seq.adcs.clear();
1101
+ for (let i = 0; i < count; i++) {
1102
+ const id = reader.int32("ADC id");
1103
+ seq.adcs.set(id, {
1104
+ id,
1105
+ numSamples: reader.nonNegativeSafeInt64("ADC sample count"),
1106
+ dwell: psToNsRounded(reader.safeInt64("ADC dwell")),
1107
+ delay: psToUsRounded(reader.safeInt64("ADC delay")),
1108
+ freqPPM: reader.float64("ADC frequency ppm"),
1109
+ phasePPM: reader.float64("ADC phase ppm"),
1110
+ freqOffset: reader.float64("ADC frequency offset"),
1111
+ phaseOffset: reader.float64("ADC phase offset"),
1112
+ deadTime: 0,
1113
+ discardPre: 0,
1114
+ discardPost: 0,
1115
+ phaseModShapeId: reader.int32("ADC phase shape id")
1116
+ });
1117
+ }
1118
+ }
1119
+ function readLegacyDelays(reader) {
1120
+ const count = reader.count64("legacy DELAYS count", 12);
1121
+ for (let i = 0; i < count; i++) {
1122
+ reader.int32("legacy DELAYS id");
1123
+ reader.safeInt64("legacy DELAYS duration");
1124
+ }
1125
+ }
1126
+ function readShapes(reader, seq) {
1127
+ const count = reader.count64("SHAPES count", 20);
1128
+ seq.shapes.clear();
1129
+ for (let i = 0; i < count; i++) {
1130
+ const id = reader.int32("SHAPES id");
1131
+ const numSamples = reader.positiveSafeInt64("SHAPES uncompressed count", MAX_SHAPE_SAMPLES);
1132
+ const packedCount = reader.positiveSafeInt64("SHAPES compressed count", MAX_SHAPE_SAMPLES);
1133
+ reader.requireArray(packedCount, 4, "SHAPES compressed data");
1134
+ const packed = new Float64Array(packedCount);
1135
+ for (let j = 0; j < packedCount; j++) packed[j] = reader.float32("SHAPES sample");
1136
+ seq.shapes.set(id, { numSamples, samples: decompressShape(packed, numSamples) });
1137
+ }
1138
+ }
1139
+ function readExtensions(reader, seq) {
1140
+ const count = reader.count64("EXTENSIONS count", 16);
1141
+ seq.extensions.clear();
1142
+ for (let i = 0; i < count; i++) {
1143
+ const id = reader.int32("EXTENSIONS id");
1144
+ seq.extensions.set(id, {
1145
+ id,
1146
+ type: reader.int32("EXTENSIONS type"),
1147
+ ref: reader.int32("EXTENSIONS reference"),
1148
+ nextId: reader.int32("EXTENSIONS next id")
1149
+ });
1150
+ }
1151
+ }
1152
+ function registerExtension(seq, id, name) {
1153
+ seq.extensionNames.set(id, name);
1154
+ seq.extensionTypes.set(id, extensionNameToType(name));
1155
+ }
1156
+ function readTriggers(reader, seq) {
1157
+ const extensionId = reader.int32("TRIGGERS extension type id");
1158
+ registerExtension(seq, extensionId, "TRIGGERS");
1159
+ const count = reader.count64("TRIGGERS count", 28);
1160
+ seq.triggers.length = 0;
1161
+ for (let i = 0; i < count; i++) {
1162
+ seq.triggers.push({
1163
+ id: reader.int32("TRIGGERS id"),
1164
+ triggerType: reader.int32("TRIGGERS type"),
1165
+ channel: reader.int32("TRIGGERS channel"),
1166
+ delay: psToUsRounded(reader.safeInt64("TRIGGERS delay")),
1167
+ duration: psToUsRounded(reader.safeInt64("TRIGGERS duration"))
1168
+ });
1169
+ }
1170
+ }
1171
+ function readLabels(reader, seq, isSet) {
1172
+ const section = isSet ? "LABELSET" : "LABELINC";
1173
+ const extensionId = reader.int32(`${section} extension type id`);
1174
+ registerExtension(seq, extensionId, section);
1175
+ const count = reader.count64(`${section} count`, 12);
1176
+ const library = isSet ? seq.labelSets : seq.labelIncs;
1177
+ library.length = 0;
1178
+ for (let i = 0; i < count; i++) {
1179
+ const id = reader.int32(`${section} id`);
1180
+ const value = reader.int32(`${section} value`);
1181
+ const labelIndex = reader.int32(`${section} label index`);
1182
+ if (labelIndex < 1 || labelIndex > BINARY_LABELS.length) {
1183
+ reader.fail(`invalid binary label index ${labelIndex}`);
1184
+ }
1185
+ const { labelId, flagId } = decodeLabel(BINARY_LABELS[labelIndex - 1]);
1186
+ const spec = { id, value, labelId, flagId };
1187
+ library.push(spec);
1188
+ }
1189
+ }
1190
+ function readSoftDelays(reader, seq) {
1191
+ const extensionId = reader.int32("DELAYS extension type id");
1192
+ registerExtension(seq, extensionId, "DELAYS");
1193
+ const count = reader.count64("DELAYS count", 28);
1194
+ seq.softDelays.length = 0;
1195
+ for (let i = 0; i < count; i++) {
1196
+ const id = reader.int32("DELAYS id");
1197
+ const numId = reader.int32("DELAYS numeric id");
1198
+ const offset = psToUsRounded(reader.safeInt64("DELAYS offset"));
1199
+ const factor = reader.float64("DELAYS factor");
1200
+ const hintLength = reader.length32("DELAYS hint length");
1201
+ seq.softDelays.push({ id, numId, offset, factor, hint: reader.string(hintLength, "DELAYS hint") });
1202
+ }
1203
+ }
1204
+ function readRfShims(reader, seq) {
1205
+ const extensionId = reader.int32("RF_SHIMS extension type id");
1206
+ registerExtension(seq, extensionId, "RF_SHIMS");
1207
+ const count = reader.count64("RF_SHIMS count", 8);
1208
+ seq.rfShims.length = 0;
1209
+ for (let i = 0; i < count; i++) {
1210
+ const id = reader.int32("RF_SHIMS id");
1211
+ const nChannels = reader.length32("RF_SHIMS channel count", MAX_RECORDS / 2);
1212
+ reader.requireArray(nChannels * 2, 8, "RF_SHIMS channel data");
1213
+ const amplitudes = new Array(nChannels);
1214
+ const phases = new Array(nChannels);
1215
+ for (let channel = 0; channel < nChannels; channel++) {
1216
+ amplitudes[channel] = reader.float64("RF_SHIMS magnitude");
1217
+ phases[channel] = reader.float64("RF_SHIMS phase");
770
1218
  }
1219
+ seq.rfShims.push({ id, nChannels, amplitudes, phases });
1220
+ }
1221
+ }
1222
+ function readRotations(reader, seq) {
1223
+ const extensionId = reader.int32("ROTATIONS extension type id");
1224
+ registerExtension(seq, extensionId, "ROTATIONS");
1225
+ const count = reader.count64("ROTATIONS count", 36);
1226
+ seq.rotations.length = 0;
1227
+ for (let i = 0; i < count; i++) {
1228
+ const id = reader.int32("ROTATIONS id");
1229
+ const values = [
1230
+ reader.float64("ROTATIONS q0"),
1231
+ reader.float64("ROTATIONS qx"),
1232
+ reader.float64("ROTATIONS qy"),
1233
+ reader.float64("ROTATIONS qz")
1234
+ ];
1235
+ const norm = Math.hypot(...values);
1236
+ if (!Number.isFinite(norm) || norm <= 0) reader.fail("invalid zero or non-finite rotation quaternion");
1237
+ seq.rotations.push({ id, values: values.map((value) => value / norm) });
771
1238
  }
772
- if (!seenSections.has("BLOCKS")) parseError("Required [BLOCKS] section is missing");
773
- for (const block of seq.blocks) {
774
- if (block.rfId > 0 && !seq.rfs.has(block.rfId)) {
775
- parseError(`Block ${block.num} references undefined RF event ${block.rfId}`);
1239
+ }
1240
+ function readSignature(reader, seq, sectionOffset) {
1241
+ const typeLength = reader.length32("SIGNATURE type length");
1242
+ const type = reader.string(typeLength, "SIGNATURE type");
1243
+ const hashLength = reader.length32("SIGNATURE hash length");
1244
+ const hashBytes = reader.bytes(hashLength, "SIGNATURE hash");
1245
+ const originalSize = reader.nonNegativeSafeInt64("SIGNATURE original size");
1246
+ if (originalSize !== sectionOffset) {
1247
+ reader.fail(`SIGNATURE original size ${originalSize} does not match section offset ${sectionOffset}`);
1248
+ }
1249
+ let hash = "";
1250
+ for (const byte of hashBytes) hash += byte.toString(16).padStart(2, "0");
1251
+ seq.binarySignature = { type, hash, originalSize };
1252
+ }
1253
+ function psToUs(value) {
1254
+ return value / 1e6;
1255
+ }
1256
+ function psToUsRounded(value) {
1257
+ return value >= 0 ? Math.floor((value + 5e5) / 1e6) : Math.ceil((value - 5e5) / 1e6);
1258
+ }
1259
+ function psToNsRounded(value) {
1260
+ return value >= 0 ? Math.floor((value + 500) / 1e3) : Math.ceil((value - 500) / 1e3);
1261
+ }
1262
+ var BinaryReader = class {
1263
+ constructor(source) {
1264
+ __publicField(this, "source", source);
1265
+ __publicField(this, "view");
1266
+ __publicField(this, "offset", 0);
1267
+ this.view = new DataView(source.buffer, source.byteOffset, source.byteLength);
1268
+ }
1269
+ get position() {
1270
+ return this.offset;
1271
+ }
1272
+ get remaining() {
1273
+ return this.view.byteLength - this.offset;
1274
+ }
1275
+ eof() {
1276
+ return this.remaining === 0;
1277
+ }
1278
+ requireArray(count, width, context) {
1279
+ if (!Number.isSafeInteger(count) || count < 0 || count > MAX_RECORDS) {
1280
+ this.fail(`${context} has invalid count ${count}`);
776
1281
  }
777
- for (const [channel, gradId] of [["GX", block.gxId], ["GY", block.gyId], ["GZ", block.gzId]]) {
778
- if (gradId > 0 && !seq.arbitraryGrads.has(gradId) && !seq.trapGrads.has(gradId)) {
779
- parseError(`Block ${block.num} references undefined ${channel} gradient event ${gradId}`);
780
- }
1282
+ if (count > Math.floor(this.remaining / width)) {
1283
+ this.fail(`${context} exceeds remaining file data`);
781
1284
  }
782
- if (block.adcId > 0 && !seq.adcs.has(block.adcId)) {
783
- parseError(`Block ${block.num} references undefined ADC event ${block.adcId}`);
1285
+ }
1286
+ count64(context, minimumBytesPerEntry) {
1287
+ const count = this.nonNegativeSafeInt64(context);
1288
+ if (count > MAX_RECORDS) this.fail(`${context} exceeds limit ${MAX_RECORDS}`);
1289
+ if (minimumBytesPerEntry > 0 && count > Math.floor(this.remaining / minimumBytesPerEntry)) {
1290
+ this.fail(`${context} exceeds remaining file data`);
784
1291
  }
785
- if (block.extId > 0 && !seq.extensions.has(block.extId)) {
786
- parseError(`Block ${block.num} references undefined extension list ${block.extId}`);
1292
+ return count;
1293
+ }
1294
+ length32(context, limit = MAX_STRING_BYTES) {
1295
+ const value = this.int32(context);
1296
+ if (value < 0 || value > limit) this.fail(`${context} has invalid value ${value}`);
1297
+ if (value > this.remaining) this.fail(`${context} exceeds remaining file data`);
1298
+ return value;
1299
+ }
1300
+ positiveSafeInt64(context, limit) {
1301
+ const value = this.safeInt64(context);
1302
+ if (value <= 0 || value > limit) this.fail(`${context} has invalid value ${value}`);
1303
+ return value;
1304
+ }
1305
+ nonNegativeSafeInt64(context) {
1306
+ const value = this.safeInt64(context);
1307
+ if (value < 0) this.fail(`${context} must be non-negative`);
1308
+ return value;
1309
+ }
1310
+ safeInt64(context) {
1311
+ const value = this.int64(context);
1312
+ if (value > BigInt(Number.MAX_SAFE_INTEGER) || value < BigInt(Number.MIN_SAFE_INTEGER)) {
1313
+ this.fail(`${context} exceeds JavaScript safe integer range`);
787
1314
  }
1315
+ return Number(value);
1316
+ }
1317
+ int64(context) {
1318
+ this.require(8, context);
1319
+ const value = this.view.getBigInt64(this.offset, true);
1320
+ this.offset += 8;
1321
+ return value;
1322
+ }
1323
+ uint64(context) {
1324
+ this.require(8, context);
1325
+ const value = this.view.getBigUint64(this.offset, true);
1326
+ this.offset += 8;
1327
+ return value;
1328
+ }
1329
+ int32(context) {
1330
+ this.require(4, context);
1331
+ const value = this.view.getInt32(this.offset, true);
1332
+ this.offset += 4;
1333
+ return value;
1334
+ }
1335
+ float64(context) {
1336
+ this.require(8, context);
1337
+ const value = this.view.getFloat64(this.offset, true);
1338
+ this.offset += 8;
1339
+ if (!Number.isFinite(value)) this.fail(`${context} is not finite`, this.offset - 8);
1340
+ return value;
1341
+ }
1342
+ float32(context) {
1343
+ this.require(4, context);
1344
+ const value = this.view.getFloat32(this.offset, true);
1345
+ this.offset += 4;
1346
+ if (!Number.isFinite(value)) this.fail(`${context} is not finite`, this.offset - 4);
1347
+ return value;
1348
+ }
1349
+ char(context) {
1350
+ return this.string(1, context);
1351
+ }
1352
+ string(length, context) {
1353
+ const data = this.bytes(length, context);
1354
+ let result = "";
1355
+ const chunkSize = 8192;
1356
+ for (let start = 0; start < data.length; start += chunkSize) {
1357
+ const end = Math.min(data.length, start + chunkSize);
1358
+ result += String.fromCharCode(...data.subarray(start, end));
1359
+ }
1360
+ return result;
788
1361
  }
789
- for (const ext of seq.extensions.values()) {
790
- if (ext.nextId > 0 && !seq.extensions.has(ext.nextId)) {
791
- parseError(`Extension list ${ext.id} references undefined next extension ${ext.nextId}`);
1362
+ bytes(length, context) {
1363
+ if (!Number.isSafeInteger(length) || length < 0 || length > MAX_STRING_BYTES) {
1364
+ this.fail(`${context} has invalid byte length ${length}`);
792
1365
  }
793
- const type = seq.extensionTypes.get(ext.type) ?? 999 /* EXT_UNKNOWN */;
794
- if (type === 999 /* EXT_UNKNOWN */) continue;
795
- if (!extensionPayloadExists(seq, type, ext.ref)) {
796
- const name = seq.extensionNames.get(ext.type) ?? `type ${ext.type}`;
797
- parseError(`Extension list ${ext.id} references undefined ${name} payload ${ext.ref}`);
1366
+ this.require(length, context);
1367
+ const result = this.source.subarray(this.offset, this.offset + length);
1368
+ this.offset += length;
1369
+ return result;
1370
+ }
1371
+ fail(message, offset = this.offset) {
1372
+ throw new Error(`Pulseq binary parse error at byte ${offset}: ${message}`);
1373
+ }
1374
+ require(length, context) {
1375
+ if (length < 0 || length > this.remaining) {
1376
+ this.fail(`unexpected end of file while reading ${context}`);
798
1377
  }
799
1378
  }
800
- }
801
- function requireNumericDefinition(seq, name) {
802
- const value = seq.definitions.get(name);
803
- if (!value || value.length === 0 || !Number.isFinite(value[0])) {
804
- parseError(`Required definition ${name} is not present in the file`);
1379
+ };
1380
+
1381
+ // src/pulseq/sequenceReader.ts
1382
+ function parseSequenceBytes(bytes, fileName = "") {
1383
+ if (hasPulseqBinaryMagic(bytes)) return parseSequenceBinary(bytes);
1384
+ if (/\.bseq$/i.test(fileName)) {
1385
+ throw new Error("Pulseq binary parse error: .bseq file is missing the Pulseq binary header");
805
1386
  }
806
- }
807
- function extensionPayloadExists(seq, type, ref) {
808
- switch (type) {
809
- case 1 /* EXT_TRIGGER */:
810
- return seq.triggers.some((v) => v.id === ref);
811
- case 2 /* EXT_ROTATION */:
812
- return seq.rotations.some((v) => v.id === ref);
813
- case 3 /* EXT_LABELSET */:
814
- return seq.labelSets.some((v) => v.id === ref);
815
- case 4 /* EXT_LABELINC */:
816
- return seq.labelIncs.some((v) => v.id === ref);
817
- case 5 /* EXT_DELAY */:
818
- return seq.softDelays.some((v) => v.id === ref);
819
- case 6 /* EXT_RF_SHIM */:
820
- return seq.rfShims.some((v) => v.id === ref);
821
- case 100 /* EXT_NCO */:
822
- return seq.ncos.some((v) => v.id === ref);
823
- default:
824
- return false;
1387
+ let text;
1388
+ try {
1389
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
1390
+ } catch {
1391
+ throw new Error("Pulseq parse error: sequence text is not valid UTF-8");
825
1392
  }
1393
+ return parseSequenceText(text);
826
1394
  }
827
1395
 
828
1396
  // src/pulseq/decoder.ts
@@ -2182,6 +2750,13 @@ var Pulseq = (() => {
2182
2750
  // src/pulseq/kspaceExportArtifacts.ts
2183
2751
  function exportKspaceArtifacts(sequenceText, sequenceName, options = {}) {
2184
2752
  const seq = parseSequenceText(sequenceText);
2753
+ return exportKspaceArtifactsFromSequence(seq, sequenceName, options);
2754
+ }
2755
+ function exportKspaceArtifactsFromBytes(sequenceBytes, sequenceName, options = {}) {
2756
+ const seq = parseSequenceBytes(sequenceBytes, sequenceName);
2757
+ return exportKspaceArtifactsFromSequence(seq, sequenceName, options);
2758
+ }
2759
+ function exportKspaceArtifactsFromSequence(seq, sequenceName, options = {}) {
2185
2760
  const decoded = decodeAllBlocks(seq);
2186
2761
  const totalDuration = getTotalDuration(seq);
2187
2762
  const gradientSupport = options.gradientSupport ?? "all";
@@ -73,7 +73,7 @@ body.layout-vertical #rc{flex-direction:row;gap:4px;top:6px;left:4px}
73
73
  <body>
74
74
  <!-- ── Toolbar ── -->
75
75
  <div id="tb">
76
- <button id="openBtn" title="Open another .seq file">📂 Open</button>
76
+ <button id="openBtn" title="Open another .seq or .bseq file">📂 Open</button>
77
77
  <button id="zi" title="Zoom In (scroll wheel)">+</button>
78
78
  <button id="zo" title="Zoom Out">−</button>
79
79
  <button id="zf" title="Fit All">Fit</button>
@@ -107,7 +107,7 @@ body.layout-vertical #rc{flex-direction:row;gap:4px;top:6px;left:4px}
107
107
  <div id="right"><div class="handle" id="khandle"></div><canvas id="kg"></canvas><canvas id="kc"></canvas><div id="rc"><button id="krst" title="Reset view">↺</button><button id="kax" title="Toggle projection">Prj</button><button id="kunit" title="Toggle k-space unit (1/m ↔ rad/m)">Unit: 1/m</button><div style="display:flex;align-items:center;gap:2px;margin-top:4px"><span style="font-size:9px;color:var(--lb)">Size</span><input type="range" id="kdot" min="1" max="12" value="2" style="width:50px;accent-color:var(--adc)" title="ADC marker size"></div></div></div>
108
108
  </div>
109
109
  <!-- Hidden file inputs for standalone mode -->
110
- <input type="file" id="fileInput" accept=".seq" style="display:none">
110
+ <input type="file" id="fileInput" accept=".seq,.bseq" style="display:none">
111
111
  <input type="file" id="ascFileInput" accept=".asc" style="display:none">
112
112
 
113
113
  <!-- ═══════════════════════════════════════════════════════════════════════
@@ -1239,8 +1239,8 @@ document.getElementById('theme').onchange=function(){
1239
1239
  document.getElementById('fileInput').onchange=function(){
1240
1240
  var file=this.files[0];if(!file)return;
1241
1241
  var reader=new FileReader();
1242
- reader.onload=function(){loadSequenceText(reader.result);};
1243
- reader.readAsText(file);
1242
+ reader.onload=function(){loadSequenceBytes(new Uint8Array(reader.result),file.name);};
1243
+ reader.readAsArrayBuffer(file);
1244
1244
  };
1245
1245
 
1246
1246
  // ── Minimap ──
@@ -1322,7 +1322,7 @@ new MutationObserver(function(){mmCache=null;draw();drawKs();drawMinimap();}).ob
1322
1322
  ═══════════════════════════════════════════════════════════════════════ */
1323
1323
 
1324
1324
  /**
1325
- * Load raw .seq text, parse it, decode blocks, compute kspace, and render.
1325
+ * Load a parsed .seq/.bseq source, decode blocks, compute k-space, and render.
1326
1326
  * Called either:
1327
1327
  * - automatically on page load if window.SEQEYES_RAW_B64 is set
1328
1328
  * - when the user opens a file via the 📂 button
@@ -1396,10 +1396,10 @@ function convertBlock(blk) {
1396
1396
  return b;
1397
1397
  }
1398
1398
 
1399
- function loadSequenceText(rawText) {
1400
- if (!rawText || typeof rawText !== 'string') return;
1401
- if (typeof Pulseq === 'undefined' || typeof Pulseq.parseSequenceText !== 'function') {
1402
- console.error('[SeqEyes] Pulseq bundle not loaded — cannot parse .seq text.');
1399
+ function loadSequence(parseSource) {
1400
+ if (typeof parseSource !== 'function') return;
1401
+ if (typeof Pulseq === 'undefined') {
1402
+ console.error('[SeqEyes] Pulseq bundle not loaded — cannot parse sequence data.');
1403
1403
  return;
1404
1404
  }
1405
1405
 
@@ -1419,7 +1419,7 @@ function loadSequenceText(rawText) {
1419
1419
  fill.style.width = '20%'; pct.textContent = '20%';
1420
1420
 
1421
1421
  // Parse
1422
- var seq = Pulseq.parseSequenceText(rawText);
1422
+ var seq = parseSource();
1423
1423
  fill.style.width = '40%'; pct.textContent = '40%';
1424
1424
 
1425
1425
  // Detect timing
@@ -1551,14 +1551,40 @@ function loadSequenceText(rawText) {
1551
1551
  });
1552
1552
  }
1553
1553
 
1554
+ function loadSequenceText(rawText) {
1555
+ if (!rawText || typeof rawText !== 'string') return;
1556
+ if (typeof Pulseq === 'undefined' || typeof Pulseq.parseSequenceText !== 'function') {
1557
+ console.error('[SeqEyes] Pulseq bundle not loaded — cannot parse .seq text.');
1558
+ return;
1559
+ }
1560
+ loadSequence(function() { return Pulseq.parseSequenceText(rawText); });
1561
+ }
1562
+
1563
+ function loadSequenceBytes(rawBytes, fileName) {
1564
+ if (!(rawBytes instanceof Uint8Array) || rawBytes.length === 0) return;
1565
+ if (typeof Pulseq === 'undefined' || typeof Pulseq.parseSequenceBytes !== 'function') {
1566
+ console.error('[SeqEyes] Pulseq bundle not loaded — cannot parse sequence bytes.');
1567
+ return;
1568
+ }
1569
+ loadSequence(function() { return Pulseq.parseSequenceBytes(rawBytes, fileName || ''); });
1570
+ }
1571
+
1554
1572
  /* SEQEYES_DATA_PLACEHOLDER */
1555
1573
 
1556
1574
  // ── Auto‑load on page start ──
1557
1575
  // Check for data injected by the Python host via base64
1558
1576
  if (window.SEQEYES_RAW_B64) {
1559
1577
  try {
1560
- var rawText = atob(window.SEQEYES_RAW_B64);
1561
- loadSequenceText(rawText);
1578
+ var encodedSource = atob(window.SEQEYES_RAW_B64);
1579
+ var sourceBytes = new Uint8Array(encodedSource.length);
1580
+ for (var sourceIndex = 0; sourceIndex < encodedSource.length; sourceIndex++) {
1581
+ sourceBytes[sourceIndex] = encodedSource.charCodeAt(sourceIndex);
1582
+ }
1583
+ if (window.SEQEYES_SOURCE_KIND === 'bytes') {
1584
+ loadSequenceBytes(sourceBytes, window.SEQEYES_SOURCE_NAME || 'sequence.bseq');
1585
+ } else {
1586
+ loadSequenceText(new TextDecoder('utf-8').decode(sourceBytes));
1587
+ }
1562
1588
  } catch(e) {
1563
1589
  console.error('[SeqEyes] Failed to decode base64 sequence data:', e);
1564
1590
  }
@@ -1569,10 +1595,10 @@ document.addEventListener('dragover', function(e) { e.preventDefault(); });
1569
1595
  document.addEventListener('drop', function(e) {
1570
1596
  e.preventDefault();
1571
1597
  var file = e.dataTransfer.files[0];
1572
- if (!file || !file.name.endsWith('.seq')) return;
1598
+ if (!file || !/\.(?:seq|bseq)$/i.test(file.name)) return;
1573
1599
  var reader = new FileReader();
1574
- reader.onload = function() { loadSequenceText(reader.result); };
1575
- reader.readAsText(file);
1600
+ reader.onload = function() { loadSequenceBytes(new Uint8Array(reader.result), file.name); };
1601
+ reader.readAsArrayBuffer(file);
1576
1602
  });
1577
1603
 
1578
1604
  // ── Defer initial render until after browser layout is complete ──
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: seqeyes-python
3
- Version: 0.2.2
3
+ Version: 0.2.6
4
4
  Summary: Interactive Pulseq MRI sequence viewer for Jupyter — lightweight, beautiful, replaces seq.plot() in pypulseq.
5
5
  Project-URL: Homepage, https://github.com/bughht/seqeyes_plugin
6
6
  Project-URL: Repository, https://github.com/bughht/seqeyes_plugin
@@ -31,7 +31,7 @@ Description-Content-Type: text/markdown
31
31
  # SeqEyes — Interactive Pulseq MRI Sequence Viewer for Python
32
32
 
33
33
  **SeqEyes** is a lightweight Python package that provides interactive
34
- visualization of Pulseq (.seq) MRI sequences in Jupyter notebooks.
34
+ visualization of Pulseq text (`.seq`) and binary (`.bseq`) MRI sequences in Jupyter notebooks.
35
35
  It works as a drop‑in replacement for `pypulseq.Sequence.plot()`,
36
36
  rendering an interactive viewer directly in Jupyter notebook cell
37
37
  output — just like Plotly.
@@ -90,13 +90,23 @@ with open('my_sequence.seq') as f:
90
90
  viewer # renders inline in Jupyter
91
91
  ```
92
92
 
93
+ Open either supported file format directly:
94
+
95
+ ```python
96
+ from seqeyes import SeqEyesViewer
97
+
98
+ SeqEyesViewer.from_file("my_sequence.seq", theme="dark")
99
+ SeqEyesViewer.from_file("my_sequence.bseq", theme="dark")
100
+ ```
101
+
93
102
  ## API Reference
94
103
 
95
104
  | Function | Description |
96
105
  |---|---|
97
106
  | `seqeyes.set(**kwargs)` | Enable SeqEyes and set global defaults (`theme`, `show_blocks`, `time_disp`, `grad_disp`, `time_range`) |
98
107
  | `seqeyes.reset()` | Restore matplotlib `seq.plot()` and clear all defaults |
99
- | `SeqEyesViewer(seq_text, ...)` | Lowlevel viewer for raw `.seq` content (no pypulseq needed) |
108
+ | `SeqEyesViewer(source, ...)` | Low-level viewer for raw `.seq` text or `.bseq` bytes |
109
+ | `SeqEyesViewer.from_file(path, ...)` | Open a local `.seq` or `.bseq` file without pypulseq |
100
110
 
101
111
  ## Viewer Controls
102
112
 
@@ -0,0 +1,8 @@
1
+ seqeyes/__init__.py,sha256=GpvQX637Z8yqYHz6arELQmXasuQm8dC-sJu6M1IP0Fs,944
2
+ seqeyes/_plot.py,sha256=v5cPAn3mO017lQlNR4bthC0QgIBK1YxjygJTAPFUz1E,8114
3
+ seqeyes/_renderer.py,sha256=vIB0lBeuOyK6p3MyQ0qeBy-JoEUVGISxtfdny7JoqKE,9862
4
+ seqeyes/resources/pulseq-bundle.js,sha256=IJpZwkgi3-pftb3qVMuUNK1xMQwkVpbXecuj_jem2JE,104992
5
+ seqeyes/resources/viewer.html,sha256=AovT33ptVfkP2iEaIBklRQGZV1tag_OBQardKtxpru8,88066
6
+ seqeyes_python-0.2.6.dist-info/METADATA,sha256=9jH27I0J6qeaDP9gjHtvDdsjRHDifiwstn4GueZ-4Hw,4502
7
+ seqeyes_python-0.2.6.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
8
+ seqeyes_python-0.2.6.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- seqeyes/__init__.py,sha256=gLG6iHqBTpn2e8O8Xo-S4maZZO7yvGlbavEKMH86bQo,944
2
- seqeyes/_plot.py,sha256=v5cPAn3mO017lQlNR4bthC0QgIBK1YxjygJTAPFUz1E,8114
3
- seqeyes/_renderer.py,sha256=da7cK65iqRt9vMkrVE80d9VFL05a0BAzFvW7oXlbT_s,8441
4
- seqeyes/resources/pulseq-bundle.js,sha256=Np9sE0OWxgLHlZ43TOpzWzwuYUGmCtBluXwdxZs8XpA,83126
5
- seqeyes/resources/viewer.html,sha256=EUiBO49YWrrLu6cB6wA8SFA6EXG0WwGnbUk6CbkgLfs,86852
6
- seqeyes_python-0.2.2.dist-info/METADATA,sha256=MryZDrpaLUgWKvNLCIPRgX15jl1Pfu3VxZoTHFMdfcA,4179
7
- seqeyes_python-0.2.2.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
8
- seqeyes_python-0.2.2.dist-info/RECORD,,