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 +1 -1
- seqeyes/_renderer.py +35 -12
- seqeyes/resources/pulseq-bundle.js +731 -156
- seqeyes/resources/viewer.html +41 -15
- {seqeyes_python-0.2.2.dist-info → seqeyes_python-0.2.6.dist-info}/METADATA +13 -3
- seqeyes_python-0.2.6.dist-info/RECORD +8 -0
- seqeyes_python-0.2.2.dist-info/RECORD +0 -8
- {seqeyes_python-0.2.2.dist-info → seqeyes_python-0.2.6.dist-info}/WHEEL +0 -0
seqeyes/__init__.py
CHANGED
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
|
-
|
|
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
|
-
|
|
78
|
-
Raw
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
148
|
-
Raw
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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
|
|
753
|
-
|
|
754
|
-
if (seq.version.major !==
|
|
755
|
-
|
|
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
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
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
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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
|
-
|
|
778
|
-
|
|
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
|
-
|
|
783
|
-
|
|
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
|
-
|
|
786
|
-
|
|
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
|
-
|
|
790
|
-
if (
|
|
791
|
-
|
|
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
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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";
|
seqeyes/resources/viewer.html
CHANGED
|
@@ -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(){
|
|
1243
|
-
reader.
|
|
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
|
|
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
|
|
1400
|
-
if (
|
|
1401
|
-
if (typeof Pulseq === 'undefined'
|
|
1402
|
-
console.error('[SeqEyes] Pulseq bundle not loaded — cannot parse
|
|
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 =
|
|
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
|
|
1561
|
-
|
|
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 ||
|
|
1598
|
+
if (!file || !/\.(?:seq|bseq)$/i.test(file.name)) return;
|
|
1573
1599
|
var reader = new FileReader();
|
|
1574
|
-
reader.onload = function() {
|
|
1575
|
-
reader.
|
|
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.
|
|
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 (
|
|
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(
|
|
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,,
|
|
File without changes
|