seqeyes-python 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- seqeyes/__init__.py +32 -0
- seqeyes/_plot.py +221 -0
- seqeyes/_renderer.py +236 -0
- seqeyes/resources/pulseq-bundle.js +2296 -0
- seqeyes/resources/viewer.html +1589 -0
- seqeyes_python-0.2.0.dist-info/METADATA +124 -0
- seqeyes_python-0.2.0.dist-info/RECORD +8 -0
- seqeyes_python-0.2.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,2296 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var Pulseq = (() => {
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// web/pulseq-browser.ts
|
|
22
|
+
var pulseq_browser_exports = {};
|
|
23
|
+
__export(pulseq_browser_exports, {
|
|
24
|
+
PACKAGE_VERSION: () => PACKAGE_VERSION,
|
|
25
|
+
calculateKspace: () => calculateKspace,
|
|
26
|
+
calculateM1: () => calculateM1,
|
|
27
|
+
calculatePns: () => calculatePns,
|
|
28
|
+
decodeAllBlocks: () => decodeAllBlocks,
|
|
29
|
+
detectSequenceTiming: () => detectSequenceTiming,
|
|
30
|
+
exportKspaceArtifacts: () => exportKspaceArtifacts,
|
|
31
|
+
formatTrajectoryText: () => formatTrajectoryText,
|
|
32
|
+
getTotalDuration: () => getTotalDuration,
|
|
33
|
+
parsePnsHardwareAsc: () => parsePnsHardwareAsc,
|
|
34
|
+
parseSequenceText: () => parseSequenceText,
|
|
35
|
+
safePnsModel: () => safePnsModel
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// package.json
|
|
39
|
+
var version = "0.1.17";
|
|
40
|
+
|
|
41
|
+
// src/pulseq/decompressor.ts
|
|
42
|
+
function decompressShape(compressed, numSamples) {
|
|
43
|
+
const packedLen = compressed.length;
|
|
44
|
+
if (!Number.isInteger(numSamples) || numSamples <= 0) {
|
|
45
|
+
throw new Error(`Invalid shape sample count: ${numSamples}`);
|
|
46
|
+
}
|
|
47
|
+
if (packedLen === numSamples) {
|
|
48
|
+
return new Float64Array(compressed);
|
|
49
|
+
}
|
|
50
|
+
const result = new Float64Array(numSamples);
|
|
51
|
+
let iPacked = 0;
|
|
52
|
+
let iUnpacked = 0;
|
|
53
|
+
while (iPacked < packedLen && iUnpacked < numSamples) {
|
|
54
|
+
if (iPacked + 1 >= packedLen) {
|
|
55
|
+
result[iUnpacked] = compressed[iPacked];
|
|
56
|
+
iPacked++;
|
|
57
|
+
iUnpacked++;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
if (compressed[iPacked] !== compressed[iPacked + 1]) {
|
|
61
|
+
result[iUnpacked] = compressed[iPacked];
|
|
62
|
+
iPacked++;
|
|
63
|
+
iUnpacked++;
|
|
64
|
+
} else {
|
|
65
|
+
if (iPacked + 2 >= packedLen) {
|
|
66
|
+
throw new Error("Malformed compressed shape: repeat marker is missing its count");
|
|
67
|
+
}
|
|
68
|
+
const value = compressed[iPacked];
|
|
69
|
+
const rawRepeat = compressed[iPacked + 2];
|
|
70
|
+
const repeatCount = Math.round(rawRepeat) + 2;
|
|
71
|
+
if (Math.abs(rawRepeat + 2 - repeatCount) > 1e-6 || repeatCount < 2) {
|
|
72
|
+
throw new Error(`Malformed compressed shape: invalid repeat count ${rawRepeat}`);
|
|
73
|
+
}
|
|
74
|
+
if (iUnpacked + repeatCount > numSamples) {
|
|
75
|
+
throw new Error("Malformed compressed shape: repeat block exceeds expected sample count");
|
|
76
|
+
}
|
|
77
|
+
iPacked += 3;
|
|
78
|
+
const end = iUnpacked + repeatCount;
|
|
79
|
+
while (iUnpacked < end) {
|
|
80
|
+
result[iUnpacked] = value;
|
|
81
|
+
iUnpacked++;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (iUnpacked !== numSamples) {
|
|
86
|
+
throw new Error(`Malformed compressed shape: expected ${numSamples} samples, decoded ${iUnpacked}`);
|
|
87
|
+
}
|
|
88
|
+
let cumSum = 0;
|
|
89
|
+
for (let i = 0; i < numSamples; i++) {
|
|
90
|
+
cumSum += result[i];
|
|
91
|
+
result[i] = cumSum;
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/pulseq/types.ts
|
|
97
|
+
var VER_PRE_14 = 1004e3;
|
|
98
|
+
var VER_V15 = 1005e3;
|
|
99
|
+
var VER_V15001 = 1005001;
|
|
100
|
+
function makeVersionCombined(major, minor, revision) {
|
|
101
|
+
return major * 1e6 + minor * 1e3 + revision;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/pulseq/reader.ts
|
|
105
|
+
function parseSequenceText(text) {
|
|
106
|
+
const lines = text.split(/\r?\n/);
|
|
107
|
+
const seq = createEmptySequence();
|
|
108
|
+
const seenSections = /* @__PURE__ */ new Set();
|
|
109
|
+
let sectionName = null;
|
|
110
|
+
let sectionLines = [];
|
|
111
|
+
for (const line of lines) {
|
|
112
|
+
const m = line.match(/^\[(\w+)\]$/);
|
|
113
|
+
if (m) {
|
|
114
|
+
if (sectionName) dispatchSection(seq, sectionName, sectionLines);
|
|
115
|
+
sectionName = m[1];
|
|
116
|
+
seenSections.add(sectionName);
|
|
117
|
+
sectionLines = [];
|
|
118
|
+
} else {
|
|
119
|
+
sectionLines.push(line);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (sectionName) dispatchSection(seq, sectionName, sectionLines);
|
|
123
|
+
seq.versionCombined = makeVersionCombined(
|
|
124
|
+
seq.version.major,
|
|
125
|
+
seq.version.minor,
|
|
126
|
+
seq.version.revision
|
|
127
|
+
);
|
|
128
|
+
extractRasterTimes(seq);
|
|
129
|
+
validateSequence(seq, seenSections);
|
|
130
|
+
return seq;
|
|
131
|
+
}
|
|
132
|
+
function dispatchSection(seq, name, lines) {
|
|
133
|
+
const valid = lines.filter((l) => {
|
|
134
|
+
const t = l.trim();
|
|
135
|
+
return t && !t.startsWith("#");
|
|
136
|
+
});
|
|
137
|
+
switch (name) {
|
|
138
|
+
case "VERSION":
|
|
139
|
+
parseVersion(seq, valid);
|
|
140
|
+
break;
|
|
141
|
+
case "DEFINITIONS":
|
|
142
|
+
parseDefinitions(seq, valid);
|
|
143
|
+
break;
|
|
144
|
+
case "BLOCKS":
|
|
145
|
+
parseBlocks(seq, valid);
|
|
146
|
+
break;
|
|
147
|
+
case "RF":
|
|
148
|
+
parseRF(seq, valid);
|
|
149
|
+
break;
|
|
150
|
+
case "GRADIENTS":
|
|
151
|
+
parseArbitraryGrads(seq, valid);
|
|
152
|
+
break;
|
|
153
|
+
case "TRAP":
|
|
154
|
+
parseTrapGrads(seq, valid);
|
|
155
|
+
break;
|
|
156
|
+
case "ADC":
|
|
157
|
+
parseADC(seq, valid);
|
|
158
|
+
break;
|
|
159
|
+
case "EXTENSIONS":
|
|
160
|
+
parseExtensions(seq, valid);
|
|
161
|
+
break;
|
|
162
|
+
case "SHAPES":
|
|
163
|
+
parseShapes(seq, lines);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function createEmptySequence() {
|
|
168
|
+
return {
|
|
169
|
+
version: { major: 1, minor: 0, revision: 0 },
|
|
170
|
+
versionCombined: 0,
|
|
171
|
+
definitions: /* @__PURE__ */ new Map(),
|
|
172
|
+
definitionsRaw: /* @__PURE__ */ new Map(),
|
|
173
|
+
blocks: [],
|
|
174
|
+
rfs: /* @__PURE__ */ new Map(),
|
|
175
|
+
arbitraryGrads: /* @__PURE__ */ new Map(),
|
|
176
|
+
trapGrads: /* @__PURE__ */ new Map(),
|
|
177
|
+
adcs: /* @__PURE__ */ new Map(),
|
|
178
|
+
extensions: /* @__PURE__ */ new Map(),
|
|
179
|
+
extensionNames: /* @__PURE__ */ new Map(),
|
|
180
|
+
extensionTypes: /* @__PURE__ */ new Map(),
|
|
181
|
+
triggers: [],
|
|
182
|
+
ncos: [],
|
|
183
|
+
rotations: [],
|
|
184
|
+
labelSets: [],
|
|
185
|
+
labelIncs: [],
|
|
186
|
+
softDelays: [],
|
|
187
|
+
rfShims: [],
|
|
188
|
+
shapes: /* @__PURE__ */ new Map(),
|
|
189
|
+
rasterTimes: { blockDurationRaster: 1e-5, gradientRaster: 1e-5, rfRaster: 1e-6, adcRaster: 1e-7 }
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function ver(seq) {
|
|
193
|
+
if (seq.versionCombined > 0) return seq.versionCombined;
|
|
194
|
+
return makeVersionCombined(seq.version.major, seq.version.minor, seq.version.revision);
|
|
195
|
+
}
|
|
196
|
+
function parseError(message) {
|
|
197
|
+
throw new Error(`Pulseq parse error: ${message}`);
|
|
198
|
+
}
|
|
199
|
+
function requireFieldCount(section, line, count, allowed) {
|
|
200
|
+
const allowedCounts = Array.isArray(allowed) ? allowed : [allowed];
|
|
201
|
+
if (!allowedCounts.includes(count)) {
|
|
202
|
+
parseError(`${section} row has ${count} fields, expected ${allowedCounts.join(" or ")}: ${line}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function toNumber(value, section, line) {
|
|
206
|
+
const n = Number(value);
|
|
207
|
+
if (!Number.isFinite(n)) parseError(`${section} row contains a non-numeric field '${value}': ${line}`);
|
|
208
|
+
return n;
|
|
209
|
+
}
|
|
210
|
+
function toInt(value, section, line) {
|
|
211
|
+
const n = toNumber(value, section, line);
|
|
212
|
+
if (!Number.isInteger(n)) parseError(`${section} row contains a non-integer field '${value}': ${line}`);
|
|
213
|
+
return n;
|
|
214
|
+
}
|
|
215
|
+
function splitFields(line) {
|
|
216
|
+
return line.trim().split(/\s+/);
|
|
217
|
+
}
|
|
218
|
+
function parseVersion(seq, lines) {
|
|
219
|
+
for (const line of lines) {
|
|
220
|
+
const p = splitFields(line);
|
|
221
|
+
requireFieldCount("VERSION", line, p.length, 2);
|
|
222
|
+
const [k, v] = p;
|
|
223
|
+
const n = toInt(v, "VERSION", line);
|
|
224
|
+
if (k === "major") seq.version.major = n;
|
|
225
|
+
else if (k === "minor") seq.version.minor = n;
|
|
226
|
+
else if (k === "revision") seq.version.revision = n;
|
|
227
|
+
}
|
|
228
|
+
seq.versionCombined = makeVersionCombined(
|
|
229
|
+
seq.version.major,
|
|
230
|
+
seq.version.minor,
|
|
231
|
+
seq.version.revision
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
function parseDefinitions(seq, lines) {
|
|
235
|
+
for (const line of lines) {
|
|
236
|
+
const idx = line.search(/\s/);
|
|
237
|
+
if (idx < 0) {
|
|
238
|
+
seq.definitions.set(line.trim(), []);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const key = line.substring(0, idx);
|
|
242
|
+
const vals = line.substring(idx + 1).trim().split(/\s+/).map(Number).filter((n) => !isNaN(n));
|
|
243
|
+
seq.definitions.set(key, vals);
|
|
244
|
+
seq.definitionsRaw.set(key, line.substring(idx + 1).trim());
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function parseBlocks(seq, lines) {
|
|
248
|
+
const vc = ver(seq);
|
|
249
|
+
for (const line of lines) {
|
|
250
|
+
const p = splitFields(line);
|
|
251
|
+
requireFieldCount("BLOCKS", line, p.length, [7, 8]);
|
|
252
|
+
const num = toInt(p[0], "BLOCKS", line);
|
|
253
|
+
const extId = p.length === 8 ? toInt(p[7], "BLOCKS", line) : 0;
|
|
254
|
+
if (vc < VER_PRE_14) {
|
|
255
|
+
seq.blocks.push({
|
|
256
|
+
num,
|
|
257
|
+
dur: toNumber(p[1], "BLOCKS", line),
|
|
258
|
+
rfId: toInt(p[2], "BLOCKS", line),
|
|
259
|
+
gxId: toInt(p[3], "BLOCKS", line),
|
|
260
|
+
gyId: toInt(p[4], "BLOCKS", line),
|
|
261
|
+
gzId: toInt(p[5], "BLOCKS", line),
|
|
262
|
+
adcId: toInt(p[6], "BLOCKS", line),
|
|
263
|
+
extId
|
|
264
|
+
});
|
|
265
|
+
} else {
|
|
266
|
+
seq.blocks.push({
|
|
267
|
+
num,
|
|
268
|
+
dur: toNumber(p[1], "BLOCKS", line),
|
|
269
|
+
rfId: toInt(p[2], "BLOCKS", line),
|
|
270
|
+
gxId: toInt(p[3], "BLOCKS", line),
|
|
271
|
+
gyId: toInt(p[4], "BLOCKS", line),
|
|
272
|
+
gzId: toInt(p[5], "BLOCKS", line),
|
|
273
|
+
adcId: toInt(p[6], "BLOCKS", line),
|
|
274
|
+
extId
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function parseRF(seq, lines) {
|
|
280
|
+
const vc = ver(seq);
|
|
281
|
+
for (const line of lines) {
|
|
282
|
+
const parts = splitFields(line);
|
|
283
|
+
const id = toInt(parts[0], "RF", line);
|
|
284
|
+
const amp = toNumber(parts[1], "RF", line);
|
|
285
|
+
const magId = toInt(parts[2], "RF", line);
|
|
286
|
+
const phId = toInt(parts[3], "RF", line);
|
|
287
|
+
if (vc >= VER_V15) {
|
|
288
|
+
requireFieldCount("RF", line, parts.length, 12);
|
|
289
|
+
const use = parts[11].toLowerCase();
|
|
290
|
+
if (!/^[erisu]$/.test(use)) parseError(`RF row has invalid use flag '${parts[11]}': ${line}`);
|
|
291
|
+
seq.rfs.set(id, {
|
|
292
|
+
id,
|
|
293
|
+
amplitude: amp,
|
|
294
|
+
magShapeId: magId,
|
|
295
|
+
phaseShapeId: phId,
|
|
296
|
+
timeShapeId: toInt(parts[4], "RF", line),
|
|
297
|
+
center: toNumber(parts[5], "RF", line),
|
|
298
|
+
delay: toNumber(parts[6], "RF", line),
|
|
299
|
+
freqPPM: toNumber(parts[7], "RF", line),
|
|
300
|
+
phasePPM: toNumber(parts[8], "RF", line),
|
|
301
|
+
freqOffset: toNumber(parts[9], "RF", line),
|
|
302
|
+
phaseOffset: toNumber(parts[10], "RF", line),
|
|
303
|
+
phaseModShapeId: 0,
|
|
304
|
+
use
|
|
305
|
+
});
|
|
306
|
+
} else if (vc >= VER_PRE_14) {
|
|
307
|
+
requireFieldCount("RF", line, parts.length, 8);
|
|
308
|
+
seq.rfs.set(id, {
|
|
309
|
+
id,
|
|
310
|
+
amplitude: amp,
|
|
311
|
+
magShapeId: magId,
|
|
312
|
+
phaseShapeId: phId,
|
|
313
|
+
timeShapeId: toInt(parts[4], "RF", line),
|
|
314
|
+
center: -1,
|
|
315
|
+
// not in v1.4.x
|
|
316
|
+
delay: toNumber(parts[5], "RF", line),
|
|
317
|
+
freqPPM: 0,
|
|
318
|
+
phasePPM: 0,
|
|
319
|
+
freqOffset: toNumber(parts[6], "RF", line),
|
|
320
|
+
phaseOffset: toNumber(parts[7], "RF", line),
|
|
321
|
+
phaseModShapeId: 0,
|
|
322
|
+
use: "u"
|
|
323
|
+
});
|
|
324
|
+
} else {
|
|
325
|
+
requireFieldCount("RF", line, parts.length, 7);
|
|
326
|
+
seq.rfs.set(id, {
|
|
327
|
+
id,
|
|
328
|
+
amplitude: amp,
|
|
329
|
+
magShapeId: magId,
|
|
330
|
+
phaseShapeId: phId,
|
|
331
|
+
timeShapeId: 0,
|
|
332
|
+
center: -1,
|
|
333
|
+
delay: toNumber(parts[4], "RF", line),
|
|
334
|
+
freqPPM: 0,
|
|
335
|
+
phasePPM: 0,
|
|
336
|
+
freqOffset: toNumber(parts[5], "RF", line),
|
|
337
|
+
phaseOffset: toNumber(parts[6], "RF", line),
|
|
338
|
+
phaseModShapeId: 0,
|
|
339
|
+
use: "u"
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function parseArbitraryGrads(seq, lines) {
|
|
345
|
+
const vc = ver(seq);
|
|
346
|
+
for (const line of lines) {
|
|
347
|
+
const p = splitFields(line);
|
|
348
|
+
const id = toInt(p[0], "GRADIENTS", line);
|
|
349
|
+
if (vc >= VER_V15) {
|
|
350
|
+
requireFieldCount("GRADIENTS", line, p.length, 7);
|
|
351
|
+
seq.arbitraryGrads.set(id, {
|
|
352
|
+
id,
|
|
353
|
+
amplitude: toNumber(p[1], "GRADIENTS", line),
|
|
354
|
+
first: toNumber(p[2], "GRADIENTS", line),
|
|
355
|
+
last: toNumber(p[3], "GRADIENTS", line),
|
|
356
|
+
shapeId: toInt(p[4], "GRADIENTS", line),
|
|
357
|
+
timeId: toInt(p[5], "GRADIENTS", line),
|
|
358
|
+
delay: toNumber(p[6], "GRADIENTS", line)
|
|
359
|
+
});
|
|
360
|
+
} else if (vc >= VER_PRE_14) {
|
|
361
|
+
requireFieldCount("GRADIENTS", line, p.length, 5);
|
|
362
|
+
seq.arbitraryGrads.set(id, {
|
|
363
|
+
id,
|
|
364
|
+
amplitude: toNumber(p[1], "GRADIENTS", line),
|
|
365
|
+
first: NaN,
|
|
366
|
+
last: NaN,
|
|
367
|
+
shapeId: toInt(p[2], "GRADIENTS", line),
|
|
368
|
+
timeId: toInt(p[3], "GRADIENTS", line),
|
|
369
|
+
delay: toNumber(p[4], "GRADIENTS", line)
|
|
370
|
+
});
|
|
371
|
+
} else {
|
|
372
|
+
requireFieldCount("GRADIENTS", line, p.length, 4);
|
|
373
|
+
seq.arbitraryGrads.set(id, {
|
|
374
|
+
id,
|
|
375
|
+
amplitude: toNumber(p[1], "GRADIENTS", line),
|
|
376
|
+
first: NaN,
|
|
377
|
+
last: NaN,
|
|
378
|
+
shapeId: toInt(p[2], "GRADIENTS", line),
|
|
379
|
+
timeId: 0,
|
|
380
|
+
delay: toNumber(p[3], "GRADIENTS", line)
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
function parseTrapGrads(seq, lines) {
|
|
386
|
+
for (const line of lines) {
|
|
387
|
+
const p = splitFields(line);
|
|
388
|
+
requireFieldCount("TRAP", line, p.length, 6);
|
|
389
|
+
const id = toInt(p[0], "TRAP", line);
|
|
390
|
+
seq.trapGrads.set(id, {
|
|
391
|
+
id,
|
|
392
|
+
amplitude: toNumber(p[1], "TRAP", line),
|
|
393
|
+
rise: toNumber(p[2], "TRAP", line),
|
|
394
|
+
flat: toNumber(p[3], "TRAP", line),
|
|
395
|
+
fall: toNumber(p[4], "TRAP", line),
|
|
396
|
+
delay: toNumber(p[5], "TRAP", line)
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
function parseADC(seq, lines) {
|
|
401
|
+
const vc = ver(seq);
|
|
402
|
+
for (const line of lines) {
|
|
403
|
+
const p = splitFields(line);
|
|
404
|
+
const id = toInt(p[0], "ADC", line);
|
|
405
|
+
if (vc >= VER_V15) {
|
|
406
|
+
requireFieldCount("ADC", line, p.length, 9);
|
|
407
|
+
seq.adcs.set(id, {
|
|
408
|
+
id,
|
|
409
|
+
numSamples: toInt(p[1], "ADC", line),
|
|
410
|
+
dwell: toNumber(p[2], "ADC", line),
|
|
411
|
+
delay: toNumber(p[3], "ADC", line),
|
|
412
|
+
freqPPM: toNumber(p[4], "ADC", line),
|
|
413
|
+
phasePPM: toNumber(p[5], "ADC", line),
|
|
414
|
+
freqOffset: toNumber(p[6], "ADC", line),
|
|
415
|
+
phaseOffset: toNumber(p[7], "ADC", line),
|
|
416
|
+
deadTime: 0,
|
|
417
|
+
discardPre: 0,
|
|
418
|
+
discardPost: 0,
|
|
419
|
+
phaseModShapeId: toInt(p[8], "ADC", line)
|
|
420
|
+
});
|
|
421
|
+
} else {
|
|
422
|
+
requireFieldCount("ADC", line, p.length, 6);
|
|
423
|
+
seq.adcs.set(id, {
|
|
424
|
+
id,
|
|
425
|
+
numSamples: toInt(p[1], "ADC", line),
|
|
426
|
+
dwell: toNumber(p[2], "ADC", line),
|
|
427
|
+
delay: toNumber(p[3], "ADC", line),
|
|
428
|
+
freqPPM: 0,
|
|
429
|
+
phasePPM: 0,
|
|
430
|
+
freqOffset: toNumber(p[4], "ADC", line),
|
|
431
|
+
phaseOffset: toNumber(p[5], "ADC", line),
|
|
432
|
+
deadTime: 0,
|
|
433
|
+
discardPre: 0,
|
|
434
|
+
discardPost: 0,
|
|
435
|
+
phaseModShapeId: 0
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
function parseExtensions(seq, valid) {
|
|
441
|
+
const vc = ver(seq);
|
|
442
|
+
_unknownLabelCounter = 0;
|
|
443
|
+
_unknownLabels.clear();
|
|
444
|
+
let i = 0;
|
|
445
|
+
while (i < valid.length) {
|
|
446
|
+
const line = valid[i].trim();
|
|
447
|
+
if (line.startsWith("extension ")) break;
|
|
448
|
+
const p = splitFields(line);
|
|
449
|
+
requireFieldCount("EXTENSIONS", line, p.length, 4);
|
|
450
|
+
const id = toInt(p[0], "EXTENSIONS", line);
|
|
451
|
+
seq.extensions.set(id, {
|
|
452
|
+
id,
|
|
453
|
+
type: toInt(p[1], "EXTENSIONS", line),
|
|
454
|
+
ref: toInt(p[2], "EXTENSIONS", line),
|
|
455
|
+
nextId: toInt(p[3], "EXTENSIONS", line)
|
|
456
|
+
});
|
|
457
|
+
i++;
|
|
458
|
+
}
|
|
459
|
+
while (i < valid.length) {
|
|
460
|
+
const line = valid[i].trim();
|
|
461
|
+
const extM = line.match(/^extension\s+(\w+)\s+(\d+)/i);
|
|
462
|
+
if (!extM) {
|
|
463
|
+
i++;
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
const extName = extM[1].toUpperCase();
|
|
467
|
+
const extId = +extM[2];
|
|
468
|
+
seq.extensionNames.set(extId, extName);
|
|
469
|
+
seq.extensionTypes.set(extId, extensionNameToType(extName));
|
|
470
|
+
i++;
|
|
471
|
+
const dataLines = [];
|
|
472
|
+
while (i < valid.length && !valid[i].trim().startsWith("extension ")) {
|
|
473
|
+
dataLines.push(valid[i].trim());
|
|
474
|
+
i++;
|
|
475
|
+
}
|
|
476
|
+
switch (extName) {
|
|
477
|
+
case "TRIGGERS":
|
|
478
|
+
parseTriggerSpecs(seq, dataLines);
|
|
479
|
+
break;
|
|
480
|
+
case "NCO":
|
|
481
|
+
parseNCOSpecs(seq, dataLines);
|
|
482
|
+
break;
|
|
483
|
+
case "ROTATIONS":
|
|
484
|
+
parseRotationSpecs(seq, dataLines, vc);
|
|
485
|
+
break;
|
|
486
|
+
case "LABELSET":
|
|
487
|
+
parseLabelSpecs(seq, dataLines, true);
|
|
488
|
+
break;
|
|
489
|
+
case "LABELINC":
|
|
490
|
+
parseLabelSpecs(seq, dataLines, false);
|
|
491
|
+
break;
|
|
492
|
+
case "DELAYS":
|
|
493
|
+
parseSoftDelaySpecs(seq, dataLines);
|
|
494
|
+
break;
|
|
495
|
+
case "RF_SHIMS":
|
|
496
|
+
parseRFShimSpecs(seq, dataLines);
|
|
497
|
+
break;
|
|
498
|
+
default:
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
function extensionNameToType(name) {
|
|
504
|
+
switch (name.toUpperCase()) {
|
|
505
|
+
case "TRIGGERS":
|
|
506
|
+
return 1 /* EXT_TRIGGER */;
|
|
507
|
+
case "ROTATIONS":
|
|
508
|
+
return 2 /* EXT_ROTATION */;
|
|
509
|
+
case "LABELSET":
|
|
510
|
+
return 3 /* EXT_LABELSET */;
|
|
511
|
+
case "LABELINC":
|
|
512
|
+
return 4 /* EXT_LABELINC */;
|
|
513
|
+
case "DELAYS":
|
|
514
|
+
return 5 /* EXT_DELAY */;
|
|
515
|
+
case "RF_SHIMS":
|
|
516
|
+
return 6 /* EXT_RF_SHIM */;
|
|
517
|
+
case "NCO":
|
|
518
|
+
return 100 /* EXT_NCO */;
|
|
519
|
+
default:
|
|
520
|
+
return 999 /* EXT_UNKNOWN */;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
function parseTriggerSpecs(seq, lines) {
|
|
524
|
+
for (const line of lines) {
|
|
525
|
+
const p = splitFields(line);
|
|
526
|
+
requireFieldCount("TRIGGERS", line, p.length, 5);
|
|
527
|
+
seq.triggers.push({
|
|
528
|
+
id: toInt(p[0], "TRIGGERS", line),
|
|
529
|
+
triggerType: toInt(p[1], "TRIGGERS", line),
|
|
530
|
+
channel: toInt(p[2], "TRIGGERS", line),
|
|
531
|
+
delay: toNumber(p[3], "TRIGGERS", line),
|
|
532
|
+
duration: toNumber(p[4], "TRIGGERS", line)
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
function parseNCOSpecs(seq, lines) {
|
|
537
|
+
for (const line of lines) {
|
|
538
|
+
const p = splitFields(line);
|
|
539
|
+
requireFieldCount("NCO", line, p.length, 6);
|
|
540
|
+
seq.ncos.push({
|
|
541
|
+
id: toInt(p[0], "NCO", line),
|
|
542
|
+
channel: toInt(p[1], "NCO", line),
|
|
543
|
+
frequency: toNumber(p[2], "NCO", line),
|
|
544
|
+
phase: toNumber(p[3], "NCO", line),
|
|
545
|
+
delay: toNumber(p[4], "NCO", line),
|
|
546
|
+
duration: toNumber(p[5], "NCO", line)
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
function parseRotationSpecs(seq, lines, vc) {
|
|
551
|
+
for (const line of lines) {
|
|
552
|
+
const p = splitFields(line);
|
|
553
|
+
if (vc >= VER_V15) {
|
|
554
|
+
requireFieldCount("ROTATIONS", line, p.length, 5);
|
|
555
|
+
const [q0, q1, q2, q3] = [
|
|
556
|
+
toNumber(p[1], "ROTATIONS", line),
|
|
557
|
+
toNumber(p[2], "ROTATIONS", line),
|
|
558
|
+
toNumber(p[3], "ROTATIONS", line),
|
|
559
|
+
toNumber(p[4], "ROTATIONS", line)
|
|
560
|
+
];
|
|
561
|
+
const norm = Math.sqrt(q0 * q0 + q1 * q1 + q2 * q2 + q3 * q3);
|
|
562
|
+
if (Math.abs(norm - 1) > 1e-3 || norm === 0) {
|
|
563
|
+
parseError(`ROTATIONS row has a non-normalized quaternion: ${line}`);
|
|
564
|
+
}
|
|
565
|
+
seq.rotations.push({
|
|
566
|
+
id: toInt(p[0], "ROTATIONS", line),
|
|
567
|
+
values: [q0 / norm, q1 / norm, q2 / norm, q3 / norm]
|
|
568
|
+
});
|
|
569
|
+
} else {
|
|
570
|
+
requireFieldCount("ROTATIONS", line, p.length, 10);
|
|
571
|
+
seq.rotations.push({
|
|
572
|
+
id: toInt(p[0], "ROTATIONS", line),
|
|
573
|
+
values: p.slice(1, 10).map((v) => toNumber(v, "ROTATIONS", line))
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
var KNOWN_LABELS = {
|
|
579
|
+
"SLC": { labelId: 0, flagId: 0 },
|
|
580
|
+
"SEG": { labelId: 1, flagId: 0 },
|
|
581
|
+
"REP": { labelId: 2, flagId: 0 },
|
|
582
|
+
"AVG": { labelId: 3, flagId: 0 },
|
|
583
|
+
"ECO": { labelId: 4, flagId: 0 },
|
|
584
|
+
"PHS": { labelId: 5, flagId: 0 },
|
|
585
|
+
"SET": { labelId: 6, flagId: 0 },
|
|
586
|
+
"ACQ": { labelId: 7, flagId: 0 },
|
|
587
|
+
"LIN": { labelId: 8, flagId: 0 },
|
|
588
|
+
"PAR": { labelId: 9, flagId: 0 },
|
|
589
|
+
"ONCE": { labelId: 10, flagId: 0 },
|
|
590
|
+
"NAV": { labelId: 0, flagId: 1 },
|
|
591
|
+
"REV": { labelId: 0, flagId: 2 },
|
|
592
|
+
"SMS": { labelId: 0, flagId: 4 },
|
|
593
|
+
"REF": { labelId: 0, flagId: 8 },
|
|
594
|
+
"IMA": { labelId: 0, flagId: 16 },
|
|
595
|
+
"OFF": { labelId: 0, flagId: 32 },
|
|
596
|
+
"NOISE": { labelId: 0, flagId: 64 },
|
|
597
|
+
"PMC": { labelId: 0, flagId: 128 },
|
|
598
|
+
"NOPOS": { labelId: 0, flagId: 256 },
|
|
599
|
+
"NOROT": { labelId: 0, flagId: 512 },
|
|
600
|
+
"NOSCL": { labelId: 0, flagId: 1024 }
|
|
601
|
+
};
|
|
602
|
+
var _unknownLabelCounter = 0;
|
|
603
|
+
var _unknownLabels = /* @__PURE__ */ new Map();
|
|
604
|
+
function decodeLabel(name) {
|
|
605
|
+
const known = KNOWN_LABELS[name];
|
|
606
|
+
if (known) return known;
|
|
607
|
+
let id = _unknownLabels.get(name);
|
|
608
|
+
if (id === void 0) {
|
|
609
|
+
id = 1e3 + _unknownLabelCounter++;
|
|
610
|
+
_unknownLabels.set(name, id);
|
|
611
|
+
}
|
|
612
|
+
return { labelId: id, flagId: 0 };
|
|
613
|
+
}
|
|
614
|
+
function parseLabelSpecs(seq, lines, isSet) {
|
|
615
|
+
for (const line of lines) {
|
|
616
|
+
const p = splitFields(line);
|
|
617
|
+
requireFieldCount(isSet ? "LABELSET" : "LABELINC", line, p.length, 3);
|
|
618
|
+
const { labelId, flagId } = decodeLabel(p[2]);
|
|
619
|
+
const spec = {
|
|
620
|
+
id: toInt(p[0], isSet ? "LABELSET" : "LABELINC", line),
|
|
621
|
+
value: toNumber(p[1], isSet ? "LABELSET" : "LABELINC", line),
|
|
622
|
+
labelId,
|
|
623
|
+
flagId
|
|
624
|
+
};
|
|
625
|
+
if (isSet) seq.labelSets.push(spec);
|
|
626
|
+
else seq.labelIncs.push(spec);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
function parseSoftDelaySpecs(seq, lines) {
|
|
630
|
+
for (const line of lines) {
|
|
631
|
+
const p = splitFields(line);
|
|
632
|
+
if (p.length < 4) parseError(`DELAYS row has ${p.length} fields, expected at least 4: ${line}`);
|
|
633
|
+
const hintMatch = line.match(/^\s*\S+\s+\S+\s+\S+\s+\S+\s*(.*)$/);
|
|
634
|
+
seq.softDelays.push({
|
|
635
|
+
id: toInt(p[0], "DELAYS", line),
|
|
636
|
+
numId: toInt(p[1], "DELAYS", line),
|
|
637
|
+
offset: toNumber(p[2], "DELAYS", line),
|
|
638
|
+
factor: toNumber(p[3], "DELAYS", line),
|
|
639
|
+
hint: hintMatch ? hintMatch[1].trim() : ""
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
function parseRFShimSpecs(seq, lines) {
|
|
644
|
+
for (const line of lines) {
|
|
645
|
+
const p = splitFields(line);
|
|
646
|
+
if (p.length < 2) parseError(`RF_SHIMS row has ${p.length} fields, expected at least 2: ${line}`);
|
|
647
|
+
const nChan = toInt(p[1], "RF_SHIMS", line);
|
|
648
|
+
requireFieldCount("RF_SHIMS", line, p.length, 2 + nChan * 2);
|
|
649
|
+
const amps = [];
|
|
650
|
+
const phases = [];
|
|
651
|
+
for (let c = 0; c < nChan; c++) {
|
|
652
|
+
amps.push(toNumber(p[2 + c * 2], "RF_SHIMS", line));
|
|
653
|
+
phases.push(toNumber(p[2 + c * 2 + 1], "RF_SHIMS", line));
|
|
654
|
+
}
|
|
655
|
+
seq.rfShims.push({ id: toInt(p[0], "RF_SHIMS", line), nChannels: nChan, amplitudes: amps, phases });
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
function parseShapes(seq, lines) {
|
|
659
|
+
let i = 0;
|
|
660
|
+
while (i < lines.length) {
|
|
661
|
+
const t = lines[i].trim();
|
|
662
|
+
if (!t || t.startsWith("#") || t.startsWith("[")) {
|
|
663
|
+
i++;
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
const m = t.match(/^shape_id\s+(\d+)/);
|
|
667
|
+
if (!m) {
|
|
668
|
+
i++;
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
const shapeId = +m[1];
|
|
672
|
+
i++;
|
|
673
|
+
let numSamples = 0;
|
|
674
|
+
while (i < lines.length) {
|
|
675
|
+
const l = lines[i].trim();
|
|
676
|
+
if (!l || l.startsWith("#")) {
|
|
677
|
+
i++;
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
const nm = l.match(/^num_samples\s+(\d+)/);
|
|
681
|
+
if (nm) {
|
|
682
|
+
numSamples = +nm[1];
|
|
683
|
+
i++;
|
|
684
|
+
break;
|
|
685
|
+
}
|
|
686
|
+
if (l.match(/^shape_id\s+\d+/) || l.startsWith("[")) break;
|
|
687
|
+
i++;
|
|
688
|
+
}
|
|
689
|
+
if (numSamples <= 0) continue;
|
|
690
|
+
const vals = [];
|
|
691
|
+
while (i < lines.length && vals.length < numSamples) {
|
|
692
|
+
const l = lines[i].trim();
|
|
693
|
+
if (l.match(/^shape_id\s+\d+/) || l.startsWith("[")) break;
|
|
694
|
+
if (!l || l.startsWith("#")) {
|
|
695
|
+
i++;
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
for (const n of l.split(/\s+/).map(Number).filter((x) => !isNaN(x))) {
|
|
699
|
+
if (vals.length < numSamples) vals.push(n);
|
|
700
|
+
}
|
|
701
|
+
i++;
|
|
702
|
+
}
|
|
703
|
+
if (vals.length === 0) continue;
|
|
704
|
+
storeShape(seq, shapeId, numSamples, vals);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
function storeShape(seq, id, num, raw) {
|
|
708
|
+
const decompressed = raw.length === num ? new Float64Array(raw) : decompressShape(raw, num);
|
|
709
|
+
seq.shapes.set(id, { numSamples: num, samples: decompressed });
|
|
710
|
+
}
|
|
711
|
+
function extractRasterTimes(seq) {
|
|
712
|
+
const set = (key, field) => {
|
|
713
|
+
const v = seq.definitions.get(key);
|
|
714
|
+
if (v?.length) seq.rasterTimes[field] = v[0];
|
|
715
|
+
};
|
|
716
|
+
set("BlockDurationRaster", "blockDurationRaster");
|
|
717
|
+
set("GradientRasterTime", "gradientRaster");
|
|
718
|
+
set("RadiofrequencyRasterTime", "rfRaster");
|
|
719
|
+
set("AdcRasterTime", "adcRaster");
|
|
720
|
+
}
|
|
721
|
+
function validateSequence(seq, seenSections) {
|
|
722
|
+
if (!seenSections.has("VERSION")) parseError("Required [VERSION] section is missing");
|
|
723
|
+
if (seq.version.major !== 1 || seq.version.minor > 5) {
|
|
724
|
+
parseError(`Unsupported Pulseq version ${seq.version.major}.${seq.version.minor}.${seq.version.revision}`);
|
|
725
|
+
}
|
|
726
|
+
const vc = ver(seq);
|
|
727
|
+
if (vc >= VER_PRE_14) {
|
|
728
|
+
requireNumericDefinition(seq, "AdcRasterTime");
|
|
729
|
+
requireNumericDefinition(seq, "GradientRasterTime");
|
|
730
|
+
requireNumericDefinition(seq, "RadiofrequencyRasterTime");
|
|
731
|
+
requireNumericDefinition(seq, "BlockDurationRaster");
|
|
732
|
+
}
|
|
733
|
+
if (vc >= VER_V15001) {
|
|
734
|
+
const required = seq.definitionsRaw.get("RequiredExtensions")?.split(/\s+/).filter(Boolean) ?? [];
|
|
735
|
+
for (const name of required) {
|
|
736
|
+
if (extensionNameToType(name) === 999 /* EXT_UNKNOWN */) {
|
|
737
|
+
parseError(`Unknown required extension '${name}'`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
if (!seenSections.has("BLOCKS")) parseError("Required [BLOCKS] section is missing");
|
|
742
|
+
for (const block of seq.blocks) {
|
|
743
|
+
if (block.rfId > 0 && !seq.rfs.has(block.rfId)) {
|
|
744
|
+
parseError(`Block ${block.num} references undefined RF event ${block.rfId}`);
|
|
745
|
+
}
|
|
746
|
+
for (const [channel, gradId] of [["GX", block.gxId], ["GY", block.gyId], ["GZ", block.gzId]]) {
|
|
747
|
+
if (gradId > 0 && !seq.arbitraryGrads.has(gradId) && !seq.trapGrads.has(gradId)) {
|
|
748
|
+
parseError(`Block ${block.num} references undefined ${channel} gradient event ${gradId}`);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
if (block.adcId > 0 && !seq.adcs.has(block.adcId)) {
|
|
752
|
+
parseError(`Block ${block.num} references undefined ADC event ${block.adcId}`);
|
|
753
|
+
}
|
|
754
|
+
if (block.extId > 0 && !seq.extensions.has(block.extId)) {
|
|
755
|
+
parseError(`Block ${block.num} references undefined extension list ${block.extId}`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
for (const ext of seq.extensions.values()) {
|
|
759
|
+
if (ext.nextId > 0 && !seq.extensions.has(ext.nextId)) {
|
|
760
|
+
parseError(`Extension list ${ext.id} references undefined next extension ${ext.nextId}`);
|
|
761
|
+
}
|
|
762
|
+
const type = seq.extensionTypes.get(ext.type) ?? 999 /* EXT_UNKNOWN */;
|
|
763
|
+
if (type === 999 /* EXT_UNKNOWN */) continue;
|
|
764
|
+
if (!extensionPayloadExists(seq, type, ext.ref)) {
|
|
765
|
+
const name = seq.extensionNames.get(ext.type) ?? `type ${ext.type}`;
|
|
766
|
+
parseError(`Extension list ${ext.id} references undefined ${name} payload ${ext.ref}`);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
function requireNumericDefinition(seq, name) {
|
|
771
|
+
const value = seq.definitions.get(name);
|
|
772
|
+
if (!value || value.length === 0 || !Number.isFinite(value[0])) {
|
|
773
|
+
parseError(`Required definition ${name} is not present in the file`);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
function extensionPayloadExists(seq, type, ref) {
|
|
777
|
+
switch (type) {
|
|
778
|
+
case 1 /* EXT_TRIGGER */:
|
|
779
|
+
return seq.triggers.some((v) => v.id === ref);
|
|
780
|
+
case 2 /* EXT_ROTATION */:
|
|
781
|
+
return seq.rotations.some((v) => v.id === ref);
|
|
782
|
+
case 3 /* EXT_LABELSET */:
|
|
783
|
+
return seq.labelSets.some((v) => v.id === ref);
|
|
784
|
+
case 4 /* EXT_LABELINC */:
|
|
785
|
+
return seq.labelIncs.some((v) => v.id === ref);
|
|
786
|
+
case 5 /* EXT_DELAY */:
|
|
787
|
+
return seq.softDelays.some((v) => v.id === ref);
|
|
788
|
+
case 6 /* EXT_RF_SHIM */:
|
|
789
|
+
return seq.rfShims.some((v) => v.id === ref);
|
|
790
|
+
case 100 /* EXT_NCO */:
|
|
791
|
+
return seq.ncos.some((v) => v.id === ref);
|
|
792
|
+
default:
|
|
793
|
+
return false;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// src/pulseq/decoder.ts
|
|
798
|
+
var GAMMA_HZ_T = 42576e3;
|
|
799
|
+
var DEFAULT_B0_T = 3;
|
|
800
|
+
function getB0(seq) {
|
|
801
|
+
const raw = seq.definitions.get("B0");
|
|
802
|
+
if (raw && Array.isArray(raw) && raw.length > 0) return +raw[0];
|
|
803
|
+
const raw2 = seq.definitions.get("b0") ?? seq.definitions.get("b_0");
|
|
804
|
+
if (raw2 && Array.isArray(raw2) && raw2.length > 0) return +raw2[0];
|
|
805
|
+
return DEFAULT_B0_T;
|
|
806
|
+
}
|
|
807
|
+
function effFreqOff(freqOffset, freqPPM, b0) {
|
|
808
|
+
return freqOffset + freqPPM * 1e-6 * GAMMA_HZ_T * b0;
|
|
809
|
+
}
|
|
810
|
+
function effPhaseOff(phaseOffset, phasePPM, b0) {
|
|
811
|
+
return phaseOffset + phasePPM * 1e-6 * GAMMA_HZ_T * b0;
|
|
812
|
+
}
|
|
813
|
+
function decodeAllBlocks(seq) {
|
|
814
|
+
return decodeBlockRange(seq, 0, seq.blocks.length);
|
|
815
|
+
}
|
|
816
|
+
function decodeBlockRange(seq, startBlockIdx, endBlockIdx) {
|
|
817
|
+
_trigCache.clear();
|
|
818
|
+
_ncoCache.clear();
|
|
819
|
+
const totalBlocks = seq.blocks.length;
|
|
820
|
+
const s = Math.max(0, Math.min(startBlockIdx, totalBlocks));
|
|
821
|
+
const e = Math.max(s, Math.min(endBlockIdx, totalBlocks));
|
|
822
|
+
if (s >= e) return [];
|
|
823
|
+
let cumulative = 0;
|
|
824
|
+
for (let i = 0; i < Math.min(s, totalBlocks); i++) {
|
|
825
|
+
cumulative += blockDurationSeconds(seq, seq.blocks[i]);
|
|
826
|
+
}
|
|
827
|
+
const decoded = [];
|
|
828
|
+
for (let i = s; i < e; i++) {
|
|
829
|
+
const block = seq.blocks[i];
|
|
830
|
+
const dur = blockDurationSeconds(seq, block);
|
|
831
|
+
const db = { index: block.num, duration: dur, startTime: cumulative };
|
|
832
|
+
if (block.rfId > 0) {
|
|
833
|
+
const rf = seq.rfs.get(block.rfId);
|
|
834
|
+
if (rf) db.rf = decodeRF(seq, rf, cumulative, dur);
|
|
835
|
+
}
|
|
836
|
+
db.gx = decodeGradient(seq, block.gxId, cumulative, dur, "gx");
|
|
837
|
+
db.gy = decodeGradient(seq, block.gyId, cumulative, dur, "gy");
|
|
838
|
+
db.gz = decodeGradient(seq, block.gzId, cumulative, dur, "gz");
|
|
839
|
+
if (block.adcId > 0) {
|
|
840
|
+
const adc = seq.adcs.get(block.adcId);
|
|
841
|
+
if (adc) db.adc = decodeADC(adc, cumulative, seq);
|
|
842
|
+
}
|
|
843
|
+
if (block.extId > 0) {
|
|
844
|
+
const ext = seq.extensions.get(block.extId);
|
|
845
|
+
if (ext) decodeExtensions(seq, ext, db, cumulative);
|
|
846
|
+
}
|
|
847
|
+
decoded.push(db);
|
|
848
|
+
cumulative += dur;
|
|
849
|
+
}
|
|
850
|
+
return decoded;
|
|
851
|
+
}
|
|
852
|
+
function getTotalDuration(seq) {
|
|
853
|
+
let total = 0;
|
|
854
|
+
for (const block of seq.blocks) {
|
|
855
|
+
total += blockDurationSeconds(seq, block);
|
|
856
|
+
}
|
|
857
|
+
return total;
|
|
858
|
+
}
|
|
859
|
+
function blockDurationSeconds(seq, block) {
|
|
860
|
+
if (seq.versionCombined < VER_PRE_14) return block.dur * 1e-6;
|
|
861
|
+
return block.dur * seq.rasterTimes.blockDurationRaster;
|
|
862
|
+
}
|
|
863
|
+
function decodeRF(seq, rf, blockStart, _blockDur) {
|
|
864
|
+
const raster = seq.rasterTimes.rfRaster;
|
|
865
|
+
const rfDelay = rf.delay * 1e-6;
|
|
866
|
+
const rfStart = blockStart + rfDelay;
|
|
867
|
+
const b0 = getB0(seq);
|
|
868
|
+
const freqFull = effFreqOff(rf.freqOffset, rf.freqPPM, b0);
|
|
869
|
+
const phaseFull = effPhaseOff(rf.phaseOffset, rf.phasePPM, b0);
|
|
870
|
+
const magShape = seq.shapes.get(rf.magShapeId);
|
|
871
|
+
const nSamples = magShape?.numSamples ?? Math.max(2, Math.round(_blockDur / raster));
|
|
872
|
+
const mag = magShape ? new Float64Array(magShape.samples) : makeConstant(nSamples, 1);
|
|
873
|
+
const phShape = seq.shapes.get(rf.phaseShapeId);
|
|
874
|
+
const ph = phShape ? new Float64Array(phShape.samples) : new Float64Array(mag.length);
|
|
875
|
+
const timeShape = rf.timeShapeId > 0 ? seq.shapes.get(rf.timeShapeId)?.samples ?? null : null;
|
|
876
|
+
const n = Math.min(mag.length, ph.length);
|
|
877
|
+
const t = new Float64Array(n);
|
|
878
|
+
const amp = new Float64Array(n);
|
|
879
|
+
const phase = new Float64Array(n);
|
|
880
|
+
for (let i = 0; i < n; i++) {
|
|
881
|
+
t[i] = timeShape ? rfStart + timeShape[i] * raster : rfStart + (i + 0.5) * raster;
|
|
882
|
+
amp[i] = rf.amplitude * mag[i];
|
|
883
|
+
const dt = t[i] - rfStart;
|
|
884
|
+
phase[i] = 2 * Math.PI * ph[i] + phaseFull + 2 * Math.PI * freqFull * dt;
|
|
885
|
+
}
|
|
886
|
+
const duration = n > 0 ? t[n - 1] - rfStart + raster : 0;
|
|
887
|
+
const centerTime = rf.center >= 0 ? blockStart + rfDelay + rf.center * 1e-6 : estimateRfPeakTime(t, amp, rfStart, duration);
|
|
888
|
+
let use = rf.use || "";
|
|
889
|
+
if (!use || use === "u") {
|
|
890
|
+
let faDeg = 0;
|
|
891
|
+
for (let i = 1; i < n; i++) {
|
|
892
|
+
const dt = t[i] - t[i - 1];
|
|
893
|
+
faDeg += 360 * (amp[i] + amp[i - 1]) * 0.5 * dt;
|
|
894
|
+
}
|
|
895
|
+
use = faDeg >= 120 ? "r" : "e";
|
|
896
|
+
}
|
|
897
|
+
return {
|
|
898
|
+
blockIndex: rf.id,
|
|
899
|
+
startTime: rfStart,
|
|
900
|
+
centerTime,
|
|
901
|
+
duration,
|
|
902
|
+
timePoints: t,
|
|
903
|
+
magnitude: amp,
|
|
904
|
+
phase,
|
|
905
|
+
amplitude: rf.amplitude,
|
|
906
|
+
freqOffset: freqFull,
|
|
907
|
+
phaseOffset: phaseFull,
|
|
908
|
+
use
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
function decodeGradient(seq, gradId, blockStart, blockDur, channel) {
|
|
912
|
+
if (gradId <= 0) return zeroGradient(blockStart, blockDur, channel);
|
|
913
|
+
const trap = seq.trapGrads.get(gradId);
|
|
914
|
+
if (trap) return decodeTrap(trap, blockStart, channel);
|
|
915
|
+
const arb = seq.arbitraryGrads.get(gradId);
|
|
916
|
+
if (arb) return decodeArb(seq, arb, blockStart, channel);
|
|
917
|
+
return zeroGradient(blockStart, blockDur, channel);
|
|
918
|
+
}
|
|
919
|
+
function zeroGradient(t0, dur, ch) {
|
|
920
|
+
return {
|
|
921
|
+
blockIndex: 0,
|
|
922
|
+
startTime: t0,
|
|
923
|
+
duration: dur,
|
|
924
|
+
timePoints: new Float64Array([t0, t0 + dur]),
|
|
925
|
+
waveform: new Float64Array([0, 0]),
|
|
926
|
+
amplitude: 0,
|
|
927
|
+
type: "none",
|
|
928
|
+
channel: ch
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
function decodeTrap(trap, blockStart, ch) {
|
|
932
|
+
const rise = trap.rise * 1e-6;
|
|
933
|
+
const flat = trap.flat * 1e-6;
|
|
934
|
+
const fall = trap.fall * 1e-6;
|
|
935
|
+
const delay = trap.delay * 1e-6;
|
|
936
|
+
const gradStart = blockStart + delay;
|
|
937
|
+
const tRel = [0, rise, rise + flat, rise + flat + fall];
|
|
938
|
+
const wfRel = [0, trap.amplitude, trap.amplitude, 0];
|
|
939
|
+
if (delay > 0) {
|
|
940
|
+
const tp2 = new Float64Array(5);
|
|
941
|
+
const wf2 = new Float64Array(5);
|
|
942
|
+
tp2[0] = blockStart;
|
|
943
|
+
wf2[0] = 0;
|
|
944
|
+
for (let i = 0; i < 4; i++) {
|
|
945
|
+
tp2[i + 1] = gradStart + tRel[i];
|
|
946
|
+
wf2[i + 1] = wfRel[i];
|
|
947
|
+
}
|
|
948
|
+
return {
|
|
949
|
+
blockIndex: trap.id,
|
|
950
|
+
startTime: blockStart,
|
|
951
|
+
duration: delay + rise + flat + fall,
|
|
952
|
+
timePoints: tp2,
|
|
953
|
+
waveform: wf2,
|
|
954
|
+
amplitude: trap.amplitude,
|
|
955
|
+
type: "trap",
|
|
956
|
+
channel: ch
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
const tp = new Float64Array(4);
|
|
960
|
+
const wf = new Float64Array(4);
|
|
961
|
+
for (let i = 0; i < 4; i++) {
|
|
962
|
+
tp[i] = gradStart + tRel[i];
|
|
963
|
+
wf[i] = wfRel[i];
|
|
964
|
+
}
|
|
965
|
+
return {
|
|
966
|
+
blockIndex: trap.id,
|
|
967
|
+
startTime: blockStart,
|
|
968
|
+
duration: rise + flat + fall,
|
|
969
|
+
timePoints: tp,
|
|
970
|
+
waveform: wf,
|
|
971
|
+
amplitude: trap.amplitude,
|
|
972
|
+
type: "trap",
|
|
973
|
+
channel: ch
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
function decodeArb(seq, arb, blockStart, ch) {
|
|
977
|
+
const shape = seq.shapes.get(arb.shapeId);
|
|
978
|
+
if (!shape) return zeroGradient(blockStart, 0, ch);
|
|
979
|
+
const raster = seq.rasterTimes.gradientRaster;
|
|
980
|
+
const delay = arb.delay * 1e-6;
|
|
981
|
+
const gradStart = blockStart + delay;
|
|
982
|
+
const n = shape.numSamples;
|
|
983
|
+
const oversampled = arb.timeId === -1;
|
|
984
|
+
const timeShape = arb.timeId > 0 ? seq.shapes.get(arb.timeId)?.samples ?? null : null;
|
|
985
|
+
if (timeShape) {
|
|
986
|
+
const tp2 = new Float64Array(n);
|
|
987
|
+
const wf2 = new Float64Array(n);
|
|
988
|
+
for (let i = 0; i < n; i++) {
|
|
989
|
+
tp2[i] = gradStart + timeShape[i] * raster;
|
|
990
|
+
wf2[i] = arb.amplitude * shape.samples[i];
|
|
991
|
+
}
|
|
992
|
+
const dur2 = n > 0 ? tp2[n - 1] - blockStart + raster : delay;
|
|
993
|
+
return {
|
|
994
|
+
blockIndex: arb.id,
|
|
995
|
+
startTime: blockStart,
|
|
996
|
+
duration: dur2,
|
|
997
|
+
timePoints: tp2,
|
|
998
|
+
waveform: wf2,
|
|
999
|
+
amplitude: arb.amplitude,
|
|
1000
|
+
type: "arb",
|
|
1001
|
+
channel: ch
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
const tp = new Float64Array(n + 2);
|
|
1005
|
+
const wf = new Float64Array(n + 2);
|
|
1006
|
+
tp[0] = gradStart;
|
|
1007
|
+
wf[0] = edgeAmplitude(arb.first, arb.amplitude, shape.samples, true);
|
|
1008
|
+
if (oversampled) {
|
|
1009
|
+
const dt = raster * 0.5;
|
|
1010
|
+
for (let i = 0; i < n; i++) {
|
|
1011
|
+
tp[i + 1] = gradStart + (i + 1) * dt;
|
|
1012
|
+
wf[i + 1] = arb.amplitude * shape.samples[i];
|
|
1013
|
+
}
|
|
1014
|
+
tp[n + 1] = gradStart + (n + 1) * dt;
|
|
1015
|
+
} else {
|
|
1016
|
+
for (let i = 0; i < n; i++) {
|
|
1017
|
+
tp[i + 1] = gradStart + (i + 0.5) * raster;
|
|
1018
|
+
wf[i + 1] = arb.amplitude * shape.samples[i];
|
|
1019
|
+
}
|
|
1020
|
+
tp[n + 1] = gradStart + n * raster;
|
|
1021
|
+
}
|
|
1022
|
+
wf[wf.length - 1] = edgeAmplitude(arb.last, arb.amplitude, shape.samples, false);
|
|
1023
|
+
const dur = tp[tp.length - 1] - blockStart;
|
|
1024
|
+
return {
|
|
1025
|
+
blockIndex: arb.id,
|
|
1026
|
+
startTime: blockStart,
|
|
1027
|
+
duration: dur,
|
|
1028
|
+
timePoints: tp,
|
|
1029
|
+
waveform: wf,
|
|
1030
|
+
amplitude: arb.amplitude,
|
|
1031
|
+
type: "arb",
|
|
1032
|
+
channel: ch
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
function edgeAmplitude(stored, amplitude, samples, first) {
|
|
1036
|
+
let value;
|
|
1037
|
+
if (Number.isFinite(stored)) {
|
|
1038
|
+
value = stored;
|
|
1039
|
+
if (Math.abs(value) > 1 + 1e-6 && Math.abs(amplitude) > 0) value /= amplitude;
|
|
1040
|
+
} else if (samples.length === 0) {
|
|
1041
|
+
value = 0;
|
|
1042
|
+
} else if (samples.length === 1) {
|
|
1043
|
+
value = samples[0];
|
|
1044
|
+
} else if (first) {
|
|
1045
|
+
value = 0.5 * (3 * samples[0] - samples[1]);
|
|
1046
|
+
} else {
|
|
1047
|
+
value = 0.5 * (3 * samples[samples.length - 1] - samples[samples.length - 2]);
|
|
1048
|
+
}
|
|
1049
|
+
return value * amplitude;
|
|
1050
|
+
}
|
|
1051
|
+
function decodeADC(adc, blockStart, seq) {
|
|
1052
|
+
const b0 = getB0(seq);
|
|
1053
|
+
const freqFull = effFreqOff(adc.freqOffset, adc.freqPPM, b0);
|
|
1054
|
+
const phaseFull = effPhaseOff(adc.phaseOffset, adc.phasePPM, b0);
|
|
1055
|
+
return {
|
|
1056
|
+
blockIndex: adc.id,
|
|
1057
|
+
startTime: blockStart,
|
|
1058
|
+
numSamples: adc.numSamples,
|
|
1059
|
+
dwell: adc.dwell * 1e-9,
|
|
1060
|
+
// ns → s
|
|
1061
|
+
delay: adc.delay * 1e-6,
|
|
1062
|
+
// µs → s
|
|
1063
|
+
freqOffset: freqFull,
|
|
1064
|
+
phaseOffset: phaseFull
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
var _trigCache = /* @__PURE__ */ new Map();
|
|
1068
|
+
var _ncoCache = /* @__PURE__ */ new Map();
|
|
1069
|
+
function decodeExtensions(seq, ext, db, blockStart) {
|
|
1070
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1071
|
+
let cur = ext;
|
|
1072
|
+
while (cur && !visited.has(cur.id)) {
|
|
1073
|
+
visited.add(cur.id);
|
|
1074
|
+
const type = seq.extensionTypes.get(cur.type) ?? 999 /* EXT_UNKNOWN */;
|
|
1075
|
+
if (type === 1 /* EXT_TRIGGER */) {
|
|
1076
|
+
let cached = _trigCache.get(cur.id);
|
|
1077
|
+
if (!cached) {
|
|
1078
|
+
const trigger = findById(seq.triggers, cur.ref);
|
|
1079
|
+
if (trigger) {
|
|
1080
|
+
cached = {
|
|
1081
|
+
blockIndex: trigger.id,
|
|
1082
|
+
startTime: 0,
|
|
1083
|
+
channel: trigger.channel,
|
|
1084
|
+
delay: trigger.delay * 1e-6,
|
|
1085
|
+
duration: trigger.duration * 1e-6
|
|
1086
|
+
};
|
|
1087
|
+
_trigCache.set(cur.id, cached);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
if (cached) {
|
|
1091
|
+
if (!db.triggers) db.triggers = [];
|
|
1092
|
+
db.triggers.push({ ...cached, startTime: blockStart });
|
|
1093
|
+
}
|
|
1094
|
+
} else if (type === 100 /* EXT_NCO */) {
|
|
1095
|
+
let cached = _ncoCache.get(cur.id);
|
|
1096
|
+
if (!cached) {
|
|
1097
|
+
const nco = findById(seq.ncos, cur.ref);
|
|
1098
|
+
if (nco) {
|
|
1099
|
+
cached = {
|
|
1100
|
+
blockIndex: nco.id,
|
|
1101
|
+
startTime: 0,
|
|
1102
|
+
channel: nco.channel,
|
|
1103
|
+
frequency: nco.frequency,
|
|
1104
|
+
phase: nco.phase,
|
|
1105
|
+
delay: nco.delay * 1e-6,
|
|
1106
|
+
duration: nco.duration * 1e-6
|
|
1107
|
+
};
|
|
1108
|
+
_ncoCache.set(cur.id, cached);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
if (cached) {
|
|
1112
|
+
if (!db.nco) db.nco = [];
|
|
1113
|
+
db.nco.push({ ...cached, startTime: blockStart });
|
|
1114
|
+
}
|
|
1115
|
+
} else if (type === 2 /* EXT_ROTATION */) {
|
|
1116
|
+
const rotation = findById(seq.rotations, cur.ref);
|
|
1117
|
+
if (rotation) db.rotation = { id: rotation.id, values: [...rotation.values] };
|
|
1118
|
+
} else if (type === 3 /* EXT_LABELSET */) {
|
|
1119
|
+
const label = findById(seq.labelSets, cur.ref);
|
|
1120
|
+
if (label) {
|
|
1121
|
+
if (!db.labelSets) db.labelSets = [];
|
|
1122
|
+
db.labelSets.push({ ...label });
|
|
1123
|
+
}
|
|
1124
|
+
} else if (type === 4 /* EXT_LABELINC */) {
|
|
1125
|
+
const label = findById(seq.labelIncs, cur.ref);
|
|
1126
|
+
if (label) {
|
|
1127
|
+
if (!db.labelIncs) db.labelIncs = [];
|
|
1128
|
+
db.labelIncs.push({ ...label });
|
|
1129
|
+
}
|
|
1130
|
+
} else if (type === 5 /* EXT_DELAY */) {
|
|
1131
|
+
const delay = findById(seq.softDelays, cur.ref);
|
|
1132
|
+
if (delay) db.softDelay = { ...delay };
|
|
1133
|
+
} else if (type === 6 /* EXT_RF_SHIM */) {
|
|
1134
|
+
const shim = findById(seq.rfShims, cur.ref);
|
|
1135
|
+
if (shim) {
|
|
1136
|
+
db.rfShim = {
|
|
1137
|
+
id: shim.id,
|
|
1138
|
+
nChannels: shim.nChannels,
|
|
1139
|
+
amplitudes: [...shim.amplitudes],
|
|
1140
|
+
phases: [...shim.phases]
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
cur = cur.nextId > 0 ? seq.extensions.get(cur.nextId) : void 0;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
function makeConstant(n, value) {
|
|
1148
|
+
const a = new Float64Array(Math.max(n, 2));
|
|
1149
|
+
a.fill(value);
|
|
1150
|
+
return a;
|
|
1151
|
+
}
|
|
1152
|
+
function estimateRfPeakTime(timePoints, magnitude, startTime, duration) {
|
|
1153
|
+
if (!timePoints.length || !magnitude.length) return startTime + duration * 0.5;
|
|
1154
|
+
let peak = Math.abs(magnitude[0]);
|
|
1155
|
+
for (let i = 1; i < magnitude.length; i++) {
|
|
1156
|
+
const v = Math.abs(magnitude[i]);
|
|
1157
|
+
if (v > peak) peak = v;
|
|
1158
|
+
}
|
|
1159
|
+
const threshold = Math.abs(peak) * 0.99999;
|
|
1160
|
+
let firstPeak = -1;
|
|
1161
|
+
let lastPeak = -1;
|
|
1162
|
+
for (let i = 0; i < magnitude.length; i++) {
|
|
1163
|
+
if (Math.abs(magnitude[i]) >= threshold) {
|
|
1164
|
+
if (firstPeak < 0) firstPeak = i;
|
|
1165
|
+
lastPeak = i;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
if (firstPeak < 0 || lastPeak < 0) return startTime + duration * 0.5;
|
|
1169
|
+
return 0.5 * (timePoints[Math.min(firstPeak, timePoints.length - 1)] + timePoints[Math.min(lastPeak, timePoints.length - 1)]);
|
|
1170
|
+
}
|
|
1171
|
+
function findById(items, id) {
|
|
1172
|
+
return items.find((item) => item.id === id);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// src/pulseq/kspace.ts
|
|
1176
|
+
function calculateKspace(blocks, gradientRaster, totalDuration, trajectoryDelay = 0, _options) {
|
|
1177
|
+
if (!blocks.length || !gradientRaster || gradientRaster <= 0) return null;
|
|
1178
|
+
const GR = gradientRaster;
|
|
1179
|
+
const RF = _options?.rfRaster && _options.rfRaster > 0 ? _options.rfRaster : 1e-6;
|
|
1180
|
+
const tacc = 1e-10;
|
|
1181
|
+
const gradientSupport = _options?.gradientSupport ?? "endpoints";
|
|
1182
|
+
const excT = [], refT = [];
|
|
1183
|
+
const gradTimes = [];
|
|
1184
|
+
let totalAdcSamples = 0;
|
|
1185
|
+
for (const b of blocks) {
|
|
1186
|
+
if (b.adc) totalAdcSamples += b.adc.numSamples;
|
|
1187
|
+
}
|
|
1188
|
+
const adcT = new Float64Array(totalAdcSamples);
|
|
1189
|
+
let adcIdx = 0;
|
|
1190
|
+
for (const b of blocks) {
|
|
1191
|
+
collectGradientSupport(b.gx, gradTimes, gradientSupport);
|
|
1192
|
+
collectGradientSupport(b.gy, gradTimes, gradientSupport);
|
|
1193
|
+
collectGradientSupport(b.gz, gradTimes, gradientSupport);
|
|
1194
|
+
if (b.rf) {
|
|
1195
|
+
const iso = Number.isFinite(b.rf.centerTime) ? b.rf.centerTime : b.rf.startTime + b.rf.duration * 0.5;
|
|
1196
|
+
const u = b.rf.use || "";
|
|
1197
|
+
if (u === "e" || u === "" || u === "u") excT.push(iso);
|
|
1198
|
+
else if (u === "r") refT.push(iso);
|
|
1199
|
+
}
|
|
1200
|
+
if (b.adc) {
|
|
1201
|
+
const t0 = b.adc.startTime + b.adc.delay;
|
|
1202
|
+
const dwell = b.adc.dwell;
|
|
1203
|
+
const nSamp = b.adc.numSamples;
|
|
1204
|
+
for (let s = 0; s < nSamp; s++)
|
|
1205
|
+
adcT[adcIdx++] = t0 + (s + 0.5) * dwell + trajectoryDelay;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
const cand = [];
|
|
1209
|
+
const pushC = (t) => {
|
|
1210
|
+
if (isFinite(t) && t >= -tacc) cand.push(Math.max(0, tacc * Math.round(t / tacc)));
|
|
1211
|
+
};
|
|
1212
|
+
for (const t of gradTimes) pushC(t);
|
|
1213
|
+
for (const t of excT) {
|
|
1214
|
+
pushC(t);
|
|
1215
|
+
pushC(t - RF);
|
|
1216
|
+
pushC(t - 2 * RF);
|
|
1217
|
+
}
|
|
1218
|
+
for (const t of refT) {
|
|
1219
|
+
pushC(t);
|
|
1220
|
+
pushC(t - RF);
|
|
1221
|
+
}
|
|
1222
|
+
for (const t of adcT) pushC(t);
|
|
1223
|
+
pushC(0);
|
|
1224
|
+
pushC(totalDuration);
|
|
1225
|
+
if (totalDuration > 0) {
|
|
1226
|
+
const nS = Math.max(1, Math.round(totalDuration / GR));
|
|
1227
|
+
for (let i = 0; i <= nS; i++) pushC(i * GR);
|
|
1228
|
+
}
|
|
1229
|
+
if (cand.length === 0) return null;
|
|
1230
|
+
cand.sort((a, b) => a - b);
|
|
1231
|
+
const grid = [];
|
|
1232
|
+
for (let i = 0; i < cand.length; i++) {
|
|
1233
|
+
if (i === 0 || cand[i] - cand[i - 1] > tacc * 0.5) grid.push(cand[i]);
|
|
1234
|
+
}
|
|
1235
|
+
const N = grid.length;
|
|
1236
|
+
if (N < 2) return null;
|
|
1237
|
+
if (_options?.maxGridPoints && N > _options.maxGridPoints) return null;
|
|
1238
|
+
const gx = new Float64Array(N), gy = new Float64Array(N), gz = new Float64Array(N);
|
|
1239
|
+
const edges = [0];
|
|
1240
|
+
let cum = 0;
|
|
1241
|
+
for (const b of blocks) {
|
|
1242
|
+
cum += b.duration;
|
|
1243
|
+
edges.push(cum);
|
|
1244
|
+
}
|
|
1245
|
+
for (let i = 0; i < N; i++) {
|
|
1246
|
+
const t = grid[i];
|
|
1247
|
+
const bi = blockIdx(t, edges);
|
|
1248
|
+
if (bi >= 0 && bi < blocks.length) {
|
|
1249
|
+
const block = blocks[bi];
|
|
1250
|
+
const localX = gradVal(block.gx, t);
|
|
1251
|
+
const localY = gradVal(block.gy, t);
|
|
1252
|
+
const localZ = gradVal(block.gz, t);
|
|
1253
|
+
const rotated = rotateGradient(block, localX, localY, localZ);
|
|
1254
|
+
gx[i] = rotated[0];
|
|
1255
|
+
gy[i] = rotated[1];
|
|
1256
|
+
gz[i] = rotated[2];
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
const kx = new Float64Array(N), ky = new Float64Array(N), kz = new Float64Array(N);
|
|
1260
|
+
for (let i = 1; i < N; i++) {
|
|
1261
|
+
const dt = grid[i] - grid[i - 1];
|
|
1262
|
+
if (dt <= 0) {
|
|
1263
|
+
kx[i] = kx[i - 1];
|
|
1264
|
+
ky[i] = ky[i - 1];
|
|
1265
|
+
kz[i] = kz[i - 1];
|
|
1266
|
+
continue;
|
|
1267
|
+
}
|
|
1268
|
+
const gxm = 0.5 * (gx[i - 1] + gx[i]), gym = 0.5 * (gy[i - 1] + gy[i]), gzm = 0.5 * (gz[i - 1] + gz[i]);
|
|
1269
|
+
kx[i] = kx[i - 1] + gxm * dt;
|
|
1270
|
+
ky[i] = ky[i - 1] + gym * dt;
|
|
1271
|
+
kz[i] = kz[i - 1] + gzm * dt;
|
|
1272
|
+
}
|
|
1273
|
+
const eIdx = [], rIdx = [];
|
|
1274
|
+
for (const t of excT) {
|
|
1275
|
+
const i = timeIdx(t, grid);
|
|
1276
|
+
if (i >= 0) eIdx.push(i);
|
|
1277
|
+
}
|
|
1278
|
+
for (const t of refT) {
|
|
1279
|
+
const i = timeIdx(t, grid);
|
|
1280
|
+
if (i >= 0) rIdx.push(i);
|
|
1281
|
+
}
|
|
1282
|
+
eIdx.sort((a, b) => a - b);
|
|
1283
|
+
rIdx.sort((a, b) => a - b);
|
|
1284
|
+
const bounds = [0];
|
|
1285
|
+
for (const i of eIdx) bounds.push(i);
|
|
1286
|
+
for (const i of rIdx) bounds.push(i);
|
|
1287
|
+
bounds.push(N - 1);
|
|
1288
|
+
bounds.sort((a, b) => a - b);
|
|
1289
|
+
const bUniq = [bounds[0]];
|
|
1290
|
+
for (let i = 1; i < bounds.length; i++) if (bounds[i] !== bUniq[bUniq.length - 1]) bUniq.push(bounds[i]);
|
|
1291
|
+
let dkX = -kx[0], dkY = -ky[0], dkZ = -kz[0];
|
|
1292
|
+
let pE = 0, pR = 0;
|
|
1293
|
+
for (let s = 0; s < bUniq.length - 1; s++) {
|
|
1294
|
+
const st = bUniq[s], en = bUniq[s + 1];
|
|
1295
|
+
if (pE < eIdx.length && eIdx[pE] === st) {
|
|
1296
|
+
dkX = -kx[st];
|
|
1297
|
+
dkY = -ky[st];
|
|
1298
|
+
dkZ = -kz[st];
|
|
1299
|
+
pE++;
|
|
1300
|
+
} else if (pR < rIdx.length && rIdx[pR] === st) {
|
|
1301
|
+
dkX = -2 * kx[st] - dkX;
|
|
1302
|
+
dkY = -2 * ky[st] - dkY;
|
|
1303
|
+
dkZ = -2 * kz[st] - dkZ;
|
|
1304
|
+
pR++;
|
|
1305
|
+
}
|
|
1306
|
+
for (let j = st; j < en; j++) {
|
|
1307
|
+
kx[j] += dkX;
|
|
1308
|
+
ky[j] += dkY;
|
|
1309
|
+
kz[j] += dkZ;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
kx[N - 1] += dkX;
|
|
1313
|
+
ky[N - 1] += dkY;
|
|
1314
|
+
kz[N - 1] += dkZ;
|
|
1315
|
+
const kxP = new Float64Array(kx), kyP = new Float64Array(ky), kzP = new Float64Array(kz);
|
|
1316
|
+
for (const i of eIdx) {
|
|
1317
|
+
if (i > 0) {
|
|
1318
|
+
kxP[i - 1] = NaN;
|
|
1319
|
+
kyP[i - 1] = NaN;
|
|
1320
|
+
kzP[i - 1] = NaN;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
const nA = adcT.length;
|
|
1324
|
+
const kxA = new Float64Array(nA), kyA = new Float64Array(nA), kzA = new Float64Array(nA);
|
|
1325
|
+
for (let a = 0; a < nA; a++) {
|
|
1326
|
+
kxA[a] = interp(kx, grid, adcT[a]);
|
|
1327
|
+
kyA[a] = interp(ky, grid, adcT[a]);
|
|
1328
|
+
kzA[a] = interp(kz, grid, adcT[a]);
|
|
1329
|
+
}
|
|
1330
|
+
return { ktraj: [kxP, kyP, kzP], t_ktraj: new Float64Array(grid), ktraj_adc: [kxA, kyA, kzA], t_adc: new Float64Array(adcT) };
|
|
1331
|
+
}
|
|
1332
|
+
function collectGradientSupport(g, support, mode) {
|
|
1333
|
+
if (!g || g.type === "none" || !g.timePoints || g.timePoints.length < 2) return;
|
|
1334
|
+
if (mode === "all") {
|
|
1335
|
+
for (let i = 0; i < g.timePoints.length; i++) support.push(g.timePoints[i]);
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
support.push(g.timePoints[0], g.timePoints[g.timePoints.length - 1]);
|
|
1339
|
+
}
|
|
1340
|
+
function gradVal(g, t) {
|
|
1341
|
+
if (!g || g.type === "none") return 0;
|
|
1342
|
+
const tp = g.timePoints, wf = g.waveform;
|
|
1343
|
+
if (!tp || tp.length < 2) return 0;
|
|
1344
|
+
if (t < tp[0] || t > tp[tp.length - 1]) return 0;
|
|
1345
|
+
let lo = 0, hi = tp.length - 1;
|
|
1346
|
+
while (hi - lo > 1) {
|
|
1347
|
+
const m = lo + hi >> 1;
|
|
1348
|
+
if (tp[m] <= t) lo = m;
|
|
1349
|
+
else hi = m;
|
|
1350
|
+
}
|
|
1351
|
+
const s = tp[hi] - tp[lo];
|
|
1352
|
+
if (s <= 0) return wf[lo];
|
|
1353
|
+
return wf[lo] + (wf[hi] - wf[lo]) * (t - tp[lo]) / s;
|
|
1354
|
+
}
|
|
1355
|
+
function blockIdx(t, edges) {
|
|
1356
|
+
let lo = 0, hi = edges.length - 1;
|
|
1357
|
+
while (lo < hi) {
|
|
1358
|
+
const m = lo + hi >> 1;
|
|
1359
|
+
if (edges[m] <= t + 1e-12) lo = m + 1;
|
|
1360
|
+
else hi = m;
|
|
1361
|
+
}
|
|
1362
|
+
return Math.max(0, lo - 1);
|
|
1363
|
+
}
|
|
1364
|
+
function timeIdx(t, g) {
|
|
1365
|
+
let lo = 0, hi = g.length;
|
|
1366
|
+
while (lo < hi) {
|
|
1367
|
+
const m = lo + hi >> 1;
|
|
1368
|
+
if (g[m] < t - 1e-12) lo = m + 1;
|
|
1369
|
+
else hi = m;
|
|
1370
|
+
}
|
|
1371
|
+
return lo < g.length ? lo : -1;
|
|
1372
|
+
}
|
|
1373
|
+
function interp(d, g, t) {
|
|
1374
|
+
const n = g.length;
|
|
1375
|
+
if (n === 0) return 0;
|
|
1376
|
+
let lo = 0, hi = n;
|
|
1377
|
+
while (lo < hi) {
|
|
1378
|
+
const m = lo + hi >> 1;
|
|
1379
|
+
if (g[m] < t) lo = m + 1;
|
|
1380
|
+
else hi = m;
|
|
1381
|
+
}
|
|
1382
|
+
if (lo === 0) return d[0];
|
|
1383
|
+
if (lo >= n) return d[n - 1];
|
|
1384
|
+
if (Math.abs(g[lo] - t) < 1e-12) return d[lo];
|
|
1385
|
+
const i0 = lo - 1, i1 = lo, dt = g[i1] - g[i0];
|
|
1386
|
+
if (dt <= 0) return d[i1];
|
|
1387
|
+
return d[i0] + (d[i1] - d[i0]) * (t - g[i0]) / dt;
|
|
1388
|
+
}
|
|
1389
|
+
function rotateGradient(block, gx, gy, gz) {
|
|
1390
|
+
const values = block.rotation?.values;
|
|
1391
|
+
if (!values) return [gx, gy, gz];
|
|
1392
|
+
if (values.length === 4) {
|
|
1393
|
+
const [w, x, y, z] = values;
|
|
1394
|
+
const r00 = 1 - 2 * y * y - 2 * z * z;
|
|
1395
|
+
const r01 = 2 * x * y - 2 * w * z;
|
|
1396
|
+
const r02 = 2 * x * z + 2 * w * y;
|
|
1397
|
+
const r10 = 2 * x * y + 2 * w * z;
|
|
1398
|
+
const r11 = 1 - 2 * x * x - 2 * z * z;
|
|
1399
|
+
const r12 = 2 * y * z - 2 * w * x;
|
|
1400
|
+
const r20 = 2 * x * z - 2 * w * y;
|
|
1401
|
+
const r21 = 2 * y * z + 2 * w * x;
|
|
1402
|
+
const r22 = 1 - 2 * x * x - 2 * y * y;
|
|
1403
|
+
return [
|
|
1404
|
+
r00 * gx + r01 * gy + r02 * gz,
|
|
1405
|
+
r10 * gx + r11 * gy + r12 * gz,
|
|
1406
|
+
r20 * gx + r21 * gy + r22 * gz
|
|
1407
|
+
];
|
|
1408
|
+
}
|
|
1409
|
+
if (values.length === 9) {
|
|
1410
|
+
return [
|
|
1411
|
+
values[0] * gx + values[1] * gy + values[2] * gz,
|
|
1412
|
+
values[3] * gx + values[4] * gy + values[5] * gz,
|
|
1413
|
+
values[6] * gx + values[7] * gy + values[8] * gz
|
|
1414
|
+
];
|
|
1415
|
+
}
|
|
1416
|
+
return [gx, gy, gz];
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// src/pulseq/m1.ts
|
|
1420
|
+
var TIME_EPS = 1e-15;
|
|
1421
|
+
function calculateM1(blocks, gradientRaster, options = {}) {
|
|
1422
|
+
const referenceMode = normalizeReferenceMode(options.referenceMode);
|
|
1423
|
+
if (!blocks.length) {
|
|
1424
|
+
return invalidM1("Empty or invalid block list.", referenceMode);
|
|
1425
|
+
}
|
|
1426
|
+
const gx = collectGradientSeries(blocks, "gx");
|
|
1427
|
+
const gy = collectGradientSeries(blocks, "gy");
|
|
1428
|
+
const gz = collectGradientSeries(blocks, "gz");
|
|
1429
|
+
const ranges = [gx, gy, gz].filter((series) => series.time.length > 0).map((series) => [series.time[0], series.time[series.time.length - 1]]);
|
|
1430
|
+
if (!ranges.length) {
|
|
1431
|
+
return invalidM1("No gradient waveform available for M1.", referenceMode);
|
|
1432
|
+
}
|
|
1433
|
+
const tMin = Math.min(...ranges.map((range) => range[0]));
|
|
1434
|
+
const tMax = Math.max(...ranges.map((range) => range[1]));
|
|
1435
|
+
if (!Number.isFinite(tMin) || !Number.isFinite(tMax) || tMax < tMin) {
|
|
1436
|
+
return invalidM1("Invalid gradient time range for M1.", referenceMode);
|
|
1437
|
+
}
|
|
1438
|
+
const warnings = [];
|
|
1439
|
+
const rfEvents = collectRfEvents(blocks, warnings);
|
|
1440
|
+
const excitationTimes = rfEvents.filter((rf) => rf.use === "e").map((rf) => rf.tSec);
|
|
1441
|
+
const refocusingTimes = rfEvents.filter((rf) => rf.use === "r").map((rf) => rf.tSec);
|
|
1442
|
+
const events = buildWalkerEvents(rfEvents);
|
|
1443
|
+
let recentExcCount = 0;
|
|
1444
|
+
let lastExcT = -1e9;
|
|
1445
|
+
for (const rf of rfEvents) {
|
|
1446
|
+
if (rf.use === "e") {
|
|
1447
|
+
if (rf.tSec - lastExcT < 0.1) recentExcCount++;
|
|
1448
|
+
lastExcT = rf.tSec;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
if (recentExcCount > 8) {
|
|
1452
|
+
warnings.push(
|
|
1453
|
+
`Sequence shows ${recentExcCount} closely-spaced (<100 ms) excitation events. This pattern is consistent with a steady-state sequence for which the simplified reset/flip bookkeeping does NOT model coherent pathway interference. Treat the M1 curve as advisory only.`
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1456
|
+
if (!excitationTimes.length) {
|
|
1457
|
+
warnings.push(`No excitation RF events found in sequence. M1 will be integrated from t=${tMin.toFixed(6)} s with no signal basis.`);
|
|
1458
|
+
}
|
|
1459
|
+
const rasterSec = gradientRaster > 0 ? gradientRaster : 1e-5;
|
|
1460
|
+
if (rasterSec <= 0) {
|
|
1461
|
+
return invalidM1("gradientRaster must be positive.", referenceMode);
|
|
1462
|
+
}
|
|
1463
|
+
const samples = buildSampleTimes(tMin, tMax, rasterSec);
|
|
1464
|
+
const x = walkM1(gx, samples, events, excitationTimes, tMin, referenceMode);
|
|
1465
|
+
const y = walkM1(gy, samples, events, excitationTimes, tMin, referenceMode);
|
|
1466
|
+
const z = walkM1(gz, samples, events, excitationTimes, tMin, referenceMode);
|
|
1467
|
+
if (x.t.length !== y.t.length || x.t.length !== z.t.length) {
|
|
1468
|
+
warnings.push(`Internal warning: per-axis M1 output sizes disagree (${x.t.length}, ${y.t.length}, ${z.t.length}). Plot may be inconsistent.`);
|
|
1469
|
+
}
|
|
1470
|
+
return {
|
|
1471
|
+
valid: true,
|
|
1472
|
+
ok: true,
|
|
1473
|
+
referenceMode,
|
|
1474
|
+
tSec: new Float64Array(x.t),
|
|
1475
|
+
m1x: new Float64Array(x.m1),
|
|
1476
|
+
m1y: new Float64Array(y.m1),
|
|
1477
|
+
m1z: new Float64Array(z.m1),
|
|
1478
|
+
warnings,
|
|
1479
|
+
excitationTimesSec: new Float64Array(excitationTimes),
|
|
1480
|
+
refocusingTimesSec: new Float64Array(refocusingTimes)
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
function invalidM1(error, referenceMode = "rfCenter") {
|
|
1484
|
+
return {
|
|
1485
|
+
valid: false,
|
|
1486
|
+
ok: false,
|
|
1487
|
+
referenceMode,
|
|
1488
|
+
error,
|
|
1489
|
+
tSec: new Float64Array(),
|
|
1490
|
+
m1x: new Float64Array(),
|
|
1491
|
+
m1y: new Float64Array(),
|
|
1492
|
+
m1z: new Float64Array(),
|
|
1493
|
+
warnings: [],
|
|
1494
|
+
excitationTimesSec: new Float64Array(),
|
|
1495
|
+
refocusingTimesSec: new Float64Array()
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
function normalizeReferenceMode(mode) {
|
|
1499
|
+
return mode === "observationTime" ? "observationTime" : "rfCenter";
|
|
1500
|
+
}
|
|
1501
|
+
function collectGradientSeries(blocks, channel) {
|
|
1502
|
+
const time = [];
|
|
1503
|
+
const value = [];
|
|
1504
|
+
for (const block of blocks) {
|
|
1505
|
+
const grad = block[channel];
|
|
1506
|
+
if (!grad?.timePoints || !grad.waveform) continue;
|
|
1507
|
+
const n = Math.min(grad.timePoints.length, grad.waveform.length);
|
|
1508
|
+
for (let i = 0; i < n; i++) {
|
|
1509
|
+
time.push(grad.timePoints[i]);
|
|
1510
|
+
value.push(grad.waveform[i]);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
return sanitizeGradientSeries(time, value);
|
|
1514
|
+
}
|
|
1515
|
+
function sanitizeGradientSeries(time, value) {
|
|
1516
|
+
const pairs = [];
|
|
1517
|
+
const n = Math.min(time.length, value.length);
|
|
1518
|
+
for (let i = 0; i < n; i++) {
|
|
1519
|
+
const t = time[i];
|
|
1520
|
+
const v = value[i];
|
|
1521
|
+
if (Number.isFinite(t) && Number.isFinite(v)) pairs.push([t, v]);
|
|
1522
|
+
}
|
|
1523
|
+
pairs.sort((a, b) => a[0] - b[0]);
|
|
1524
|
+
const outT = [];
|
|
1525
|
+
const outV = [];
|
|
1526
|
+
for (const [t, v] of pairs) {
|
|
1527
|
+
const last = outT.length - 1;
|
|
1528
|
+
if (last >= 0 && Math.abs(t - outT[last]) <= TIME_EPS) {
|
|
1529
|
+
outV[last] = 0.5 * (outV[last] + v);
|
|
1530
|
+
continue;
|
|
1531
|
+
}
|
|
1532
|
+
outT.push(t);
|
|
1533
|
+
outV.push(v);
|
|
1534
|
+
}
|
|
1535
|
+
return { time: outT, value: outV };
|
|
1536
|
+
}
|
|
1537
|
+
function collectRfEvents(blocks, warnings) {
|
|
1538
|
+
const events = [];
|
|
1539
|
+
for (const block of blocks) {
|
|
1540
|
+
if (!block.rf) continue;
|
|
1541
|
+
const use = classifyRfUse(block.rf.use);
|
|
1542
|
+
if (!use) continue;
|
|
1543
|
+
const rec = { tSec: block.rf.centerTime, use };
|
|
1544
|
+
events.push(rec);
|
|
1545
|
+
if (use === "u") {
|
|
1546
|
+
warnings.push(`Unknown RF use 'u' at t=${rec.tSec.toFixed(6)} s; M1 bookkeeping treats it as no-op.`);
|
|
1547
|
+
} else if (use === "p") {
|
|
1548
|
+
warnings.push(
|
|
1549
|
+
`Preparation module 'p' at t=${rec.tSec.toFixed(6)} s; treated as M1 reset (simplified handling; prep modules that preserve phase encoding will give wrong results).`
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
events.sort((a, b) => a.tSec - b.tSec);
|
|
1554
|
+
return events;
|
|
1555
|
+
}
|
|
1556
|
+
function classifyRfUse(raw) {
|
|
1557
|
+
const c = (raw || "u").toLowerCase();
|
|
1558
|
+
if (c === "e" || c === "r" || c === "s" || c === "i" || c === "p") return c;
|
|
1559
|
+
return "u";
|
|
1560
|
+
}
|
|
1561
|
+
function buildWalkerEvents(rfs) {
|
|
1562
|
+
const events = [];
|
|
1563
|
+
for (const rf of rfs) {
|
|
1564
|
+
if (rf.use === "i" || rf.use === "u") continue;
|
|
1565
|
+
events.push({
|
|
1566
|
+
tSec: rf.tSec,
|
|
1567
|
+
kind: rf.use === "r" ? "flip" : "reset"
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
events.sort((a, b) => {
|
|
1571
|
+
if (a.tSec !== b.tSec) return a.tSec - b.tSec;
|
|
1572
|
+
return a.kind === "reset" && b.kind === "flip" ? -1 : 1;
|
|
1573
|
+
});
|
|
1574
|
+
return events;
|
|
1575
|
+
}
|
|
1576
|
+
function buildSampleTimes(tMin, tMax, rasterSec) {
|
|
1577
|
+
const samples = [];
|
|
1578
|
+
const nSamples = Math.floor((tMax - tMin) / rasterSec) + 1;
|
|
1579
|
+
for (let i = 0; i < nSamples; i++) samples.push(tMin + i * rasterSec);
|
|
1580
|
+
if (!samples.length || samples[samples.length - 1] < tMax - TIME_EPS) samples.push(tMax);
|
|
1581
|
+
return samples;
|
|
1582
|
+
}
|
|
1583
|
+
function walkM1(gradient, samples, events, excitationTimes, tMin, referenceMode) {
|
|
1584
|
+
const outT = [];
|
|
1585
|
+
const outM1 = [];
|
|
1586
|
+
let sign = 1;
|
|
1587
|
+
let tReset = excitationTimes.length ? excitationTimes[0] : tMin;
|
|
1588
|
+
if (samples.length && samples[0] < tReset) tReset = samples[0];
|
|
1589
|
+
let currentT = tReset;
|
|
1590
|
+
let unsignedM0 = 0;
|
|
1591
|
+
let unsignedM1 = 0;
|
|
1592
|
+
const reportedM1At = (t) => {
|
|
1593
|
+
if (referenceMode === "observationTime") return sign * (unsignedM1 - (t - tReset) * unsignedM0);
|
|
1594
|
+
return sign * unsignedM1;
|
|
1595
|
+
};
|
|
1596
|
+
const advanceTo = (targetT) => {
|
|
1597
|
+
if (!(targetT > currentT + TIME_EPS)) return;
|
|
1598
|
+
while (currentT < targetT - TIME_EPS) {
|
|
1599
|
+
let nextT = nextGradientBreakpoint(gradient.time, currentT, targetT);
|
|
1600
|
+
if (!(nextT > currentT)) nextT = targetT;
|
|
1601
|
+
const ga = sampleGradientAt(gradient, currentT);
|
|
1602
|
+
const gb = sampleGradientAt(gradient, nextT);
|
|
1603
|
+
const [m0Seg, m1Seg] = integrateLinearSegment(currentT, nextT, tReset, ga, gb);
|
|
1604
|
+
unsignedM0 += m0Seg;
|
|
1605
|
+
unsignedM1 += m1Seg;
|
|
1606
|
+
currentT = nextT;
|
|
1607
|
+
}
|
|
1608
|
+
};
|
|
1609
|
+
let ei = 0;
|
|
1610
|
+
let si = 0;
|
|
1611
|
+
while (ei < events.length || si < samples.length) {
|
|
1612
|
+
const nextEvtT = ei < events.length ? events[ei].tSec : Number.POSITIVE_INFINITY;
|
|
1613
|
+
const nextSampT = si < samples.length ? samples[si] : Number.POSITIVE_INFINITY;
|
|
1614
|
+
if (nextEvtT <= nextSampT) {
|
|
1615
|
+
advanceTo(nextEvtT);
|
|
1616
|
+
if (events[ei].kind === "reset") {
|
|
1617
|
+
if (!outT.length || outT[outT.length - 1] < nextEvtT - TIME_EPS) {
|
|
1618
|
+
outT.push(nextEvtT);
|
|
1619
|
+
outM1.push(0);
|
|
1620
|
+
} else {
|
|
1621
|
+
outT[outT.length - 1] = nextEvtT;
|
|
1622
|
+
outM1[outM1.length - 1] = 0;
|
|
1623
|
+
}
|
|
1624
|
+
sign = 1;
|
|
1625
|
+
tReset = nextEvtT;
|
|
1626
|
+
currentT = nextEvtT;
|
|
1627
|
+
unsignedM0 = 0;
|
|
1628
|
+
unsignedM1 = 0;
|
|
1629
|
+
} else {
|
|
1630
|
+
outT.push(nextEvtT);
|
|
1631
|
+
outM1.push(reportedM1At(nextEvtT));
|
|
1632
|
+
sign = -sign;
|
|
1633
|
+
}
|
|
1634
|
+
ei++;
|
|
1635
|
+
} else {
|
|
1636
|
+
advanceTo(nextSampT);
|
|
1637
|
+
outT.push(nextSampT);
|
|
1638
|
+
outM1.push(reportedM1At(nextSampT));
|
|
1639
|
+
si++;
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
return { t: outT, m1: outM1 };
|
|
1643
|
+
}
|
|
1644
|
+
function sampleGradientAt(gradient, t) {
|
|
1645
|
+
const n = gradient.time.length;
|
|
1646
|
+
if (n <= 0 || t < gradient.time[0] || t > gradient.time[n - 1]) return 0;
|
|
1647
|
+
if (n === 1 || t <= gradient.time[0]) return gradient.value[0];
|
|
1648
|
+
if (t >= gradient.time[n - 1]) return gradient.value[n - 1];
|
|
1649
|
+
let lo = 0;
|
|
1650
|
+
let hi = n - 1;
|
|
1651
|
+
while (hi - lo > 1) {
|
|
1652
|
+
const mid = lo + hi >> 1;
|
|
1653
|
+
if (gradient.time[mid] <= t) lo = mid;
|
|
1654
|
+
else hi = mid;
|
|
1655
|
+
}
|
|
1656
|
+
const t0 = gradient.time[lo];
|
|
1657
|
+
const t1 = gradient.time[hi];
|
|
1658
|
+
if (!(t1 > t0)) return gradient.value[lo];
|
|
1659
|
+
const alpha = (t - t0) / (t1 - t0);
|
|
1660
|
+
return gradient.value[lo] + alpha * (gradient.value[hi] - gradient.value[lo]);
|
|
1661
|
+
}
|
|
1662
|
+
function nextGradientBreakpoint(times, t, target) {
|
|
1663
|
+
if (times.length <= 1 || t >= times[times.length - 1]) return target;
|
|
1664
|
+
let lo = 0;
|
|
1665
|
+
let hi = times.length;
|
|
1666
|
+
const threshold = t + TIME_EPS;
|
|
1667
|
+
while (lo < hi) {
|
|
1668
|
+
const mid = lo + hi >> 1;
|
|
1669
|
+
if (times[mid] <= threshold) lo = mid + 1;
|
|
1670
|
+
else hi = mid;
|
|
1671
|
+
}
|
|
1672
|
+
return lo < times.length ? Math.min(target, times[lo]) : target;
|
|
1673
|
+
}
|
|
1674
|
+
function integrateLinearSegment(a, b, tRef, ga, gb) {
|
|
1675
|
+
const h = b - a;
|
|
1676
|
+
if (!(h > 0)) return [0, 0];
|
|
1677
|
+
const slope = (gb - ga) / h;
|
|
1678
|
+
const aRel = a - tRef;
|
|
1679
|
+
const m0 = ga * h + 0.5 * slope * h * h;
|
|
1680
|
+
const m1 = ga * (aRel * h + 0.5 * h * h) + slope * (0.5 * aRel * h * h + h * h * h / 3);
|
|
1681
|
+
return [m0, m1];
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
// src/pulseq/pns.ts
|
|
1685
|
+
var GAMMA_HZ_PER_T = 42576e3;
|
|
1686
|
+
var TIME_EPS2 = 1e-15;
|
|
1687
|
+
function parsePnsHardwareAsc(text) {
|
|
1688
|
+
const asc = parseAscText(text);
|
|
1689
|
+
const prefix = resolvePnsPrefix(asc);
|
|
1690
|
+
const x = getAxisHardware(
|
|
1691
|
+
asc,
|
|
1692
|
+
`${prefix}flGSWDTauX`,
|
|
1693
|
+
`${prefix}flGSWDAX`,
|
|
1694
|
+
`${prefix}flGSWDStimulationLimitX`,
|
|
1695
|
+
`${prefix}flGSWDStimulationThresholdX`,
|
|
1696
|
+
[
|
|
1697
|
+
"asGPAParameters[0].sGCParameters.flGScaleFactorX",
|
|
1698
|
+
"asGPAParameters.sGCParameters.flGScaleFactorX",
|
|
1699
|
+
"flGScaleFactorX",
|
|
1700
|
+
"flGCGScaleFactorX",
|
|
1701
|
+
"GScaleFactorX"
|
|
1702
|
+
]
|
|
1703
|
+
);
|
|
1704
|
+
const y = getAxisHardware(
|
|
1705
|
+
asc,
|
|
1706
|
+
`${prefix}flGSWDTauY`,
|
|
1707
|
+
`${prefix}flGSWDAY`,
|
|
1708
|
+
`${prefix}flGSWDStimulationLimitY`,
|
|
1709
|
+
`${prefix}flGSWDStimulationThresholdY`,
|
|
1710
|
+
[
|
|
1711
|
+
"asGPAParameters[0].sGCParameters.flGScaleFactorY",
|
|
1712
|
+
"asGPAParameters.sGCParameters.flGScaleFactorY",
|
|
1713
|
+
"flGScaleFactorY",
|
|
1714
|
+
"flGCGScaleFactorY",
|
|
1715
|
+
"GScaleFactorY"
|
|
1716
|
+
]
|
|
1717
|
+
);
|
|
1718
|
+
const z = getAxisHardware(
|
|
1719
|
+
asc,
|
|
1720
|
+
`${prefix}flGSWDTauZ`,
|
|
1721
|
+
`${prefix}flGSWDAZ`,
|
|
1722
|
+
`${prefix}flGSWDStimulationLimitZ`,
|
|
1723
|
+
`${prefix}flGSWDStimulationThresholdZ`,
|
|
1724
|
+
[
|
|
1725
|
+
"asGPAParameters[0].sGCParameters.flGScaleFactorZ",
|
|
1726
|
+
"asGPAParameters.sGCParameters.flGScaleFactorZ",
|
|
1727
|
+
"flGScaleFactorZ",
|
|
1728
|
+
"flGCGScaleFactorZ",
|
|
1729
|
+
"GScaleFactorZ"
|
|
1730
|
+
]
|
|
1731
|
+
);
|
|
1732
|
+
if (!hasValidWeights(x) || !hasValidWeights(y) || !hasValidWeights(z)) {
|
|
1733
|
+
throw new Error("ASC hardware coefficients are invalid (a1+a2+a3 or stim limit).");
|
|
1734
|
+
}
|
|
1735
|
+
return { x, y, z, valid: true };
|
|
1736
|
+
}
|
|
1737
|
+
function calculatePns(blocks, gradientRaster, hardware, gammaHzPerT = GAMMA_HZ_PER_T) {
|
|
1738
|
+
if (!hardware.valid) return invalidPns("PNS hardware is not initialized.");
|
|
1739
|
+
if (!blocks.length) return invalidPns("No sequence loaded.");
|
|
1740
|
+
if (gradientRaster <= 0 || gammaHzPerT <= 0) return invalidPns("Missing GradientRasterTime or gamma.");
|
|
1741
|
+
const dtSec = gradientRaster;
|
|
1742
|
+
const waves = [
|
|
1743
|
+
collectGradientSeries2(blocks, "gx"),
|
|
1744
|
+
collectGradientSeries2(blocks, "gy"),
|
|
1745
|
+
collectGradientSeries2(blocks, "gz")
|
|
1746
|
+
];
|
|
1747
|
+
const nonEmpty = waves.filter((wave) => wave.time.length > 0);
|
|
1748
|
+
if (!nonEmpty.length) return invalidPns("No gradient waveform available for PNS.");
|
|
1749
|
+
const tFirst = Math.min(...nonEmpty.map((wave) => wave.time[0]));
|
|
1750
|
+
const tLast = Math.max(...nonEmpty.map((wave) => wave.time[wave.time.length - 1]));
|
|
1751
|
+
if (!Number.isFinite(tFirst) || !Number.isFinite(tLast) || tLast <= tFirst) {
|
|
1752
|
+
return invalidPns("No gradient waveform available for PNS.");
|
|
1753
|
+
}
|
|
1754
|
+
let ntMin = Math.floor(tFirst / dtSec + Number.EPSILON) + 0.5;
|
|
1755
|
+
const ntMax = Math.ceil(tLast / dtSec - Number.EPSILON) - 0.5;
|
|
1756
|
+
if (ntMin < 0.5) ntMin = 0.5;
|
|
1757
|
+
if (ntMax < ntMin) return invalidPns("Unable to build regular PNS raster.");
|
|
1758
|
+
const nSamples = Math.floor(ntMax - ntMin + 1);
|
|
1759
|
+
if (nSamples < 2) return invalidPns("Too few samples for PNS computation.");
|
|
1760
|
+
const tAxis = new Float64Array(nSamples);
|
|
1761
|
+
const gxTpm = new Float64Array(nSamples);
|
|
1762
|
+
const gyTpm = new Float64Array(nSamples);
|
|
1763
|
+
const gzTpm = new Float64Array(nSamples);
|
|
1764
|
+
for (let i = 0; i < nSamples; i++) {
|
|
1765
|
+
const tSec = (ntMin + i) * dtSec;
|
|
1766
|
+
tAxis[i] = tSec;
|
|
1767
|
+
gxTpm[i] = interpLinearZero(waves[0], tSec) / gammaHzPerT;
|
|
1768
|
+
gyTpm[i] = interpLinearZero(waves[1], tSec) / gammaHzPerT;
|
|
1769
|
+
gzTpm[i] = interpLinearZero(waves[2], tSec) / gammaHzPerT;
|
|
1770
|
+
}
|
|
1771
|
+
const longestTauMs = Math.max(
|
|
1772
|
+
hardware.x.tau1Ms,
|
|
1773
|
+
hardware.x.tau2Ms,
|
|
1774
|
+
hardware.x.tau3Ms,
|
|
1775
|
+
hardware.y.tau1Ms,
|
|
1776
|
+
hardware.y.tau2Ms,
|
|
1777
|
+
hardware.y.tau3Ms,
|
|
1778
|
+
hardware.z.tau1Ms,
|
|
1779
|
+
hardware.z.tau2Ms,
|
|
1780
|
+
hardware.z.tau3Ms
|
|
1781
|
+
);
|
|
1782
|
+
const zptSec = longestTauMs * 4 / 1e3;
|
|
1783
|
+
const preCount = Math.max(0, Math.round(zptSec / (4 * dtSec)));
|
|
1784
|
+
const postCount = Math.max(0, Math.round(zptSec / dtSec));
|
|
1785
|
+
const gxPadded = padSamples(gxTpm, preCount, postCount);
|
|
1786
|
+
const gyPadded = padSamples(gyTpm, preCount, postCount);
|
|
1787
|
+
const gzPadded = padSamples(gzTpm, preCount, postCount);
|
|
1788
|
+
const stimX = safePnsModel(diff(gxPadded, dtSec), dtSec, hardware.x);
|
|
1789
|
+
const stimY = safePnsModel(diff(gyPadded, dtSec), dtSec, hardware.y);
|
|
1790
|
+
const stimZ = safePnsModel(diff(gzPadded, dtSec), dtSec, hardware.z);
|
|
1791
|
+
const hasAnyNonTrap = blocks.some((block) => block.gx?.type === "arb" || block.gy?.type === "arb" || block.gz?.type === "arb");
|
|
1792
|
+
const hasAnyLabelExt = blocks.some((block) => !!(block.labelSets?.length || block.labelIncs?.length));
|
|
1793
|
+
const shift = hasAnyNonTrap || hasAnyLabelExt ? 1 : 0;
|
|
1794
|
+
const selectedX = [];
|
|
1795
|
+
const selectedY = [];
|
|
1796
|
+
const selectedZ = [];
|
|
1797
|
+
const selectedT = [];
|
|
1798
|
+
for (let origIdx = 0; origIdx < nSamples; origIdx++) {
|
|
1799
|
+
const paddedIdx = preCount + origIdx;
|
|
1800
|
+
let stimIdx = paddedIdx - shift;
|
|
1801
|
+
if (shift > 0 && hasAnyLabelExt && origIdx === tAxis.length - 1) {
|
|
1802
|
+
stimIdx = Math.min(paddedIdx, stimX.length - 1);
|
|
1803
|
+
}
|
|
1804
|
+
if (stimIdx < 0 || stimIdx >= stimX.length || stimIdx >= stimY.length || stimIdx >= stimZ.length) continue;
|
|
1805
|
+
selectedX.push(stimX[stimIdx]);
|
|
1806
|
+
selectedY.push(stimY[stimIdx]);
|
|
1807
|
+
selectedZ.push(stimZ[stimIdx]);
|
|
1808
|
+
selectedT.push(tAxis[origIdx]);
|
|
1809
|
+
}
|
|
1810
|
+
const timeSec = new Float64Array(selectedX.length);
|
|
1811
|
+
const pnsX = new Float64Array(selectedX.length);
|
|
1812
|
+
const pnsY = new Float64Array(selectedX.length);
|
|
1813
|
+
const pnsZ = new Float64Array(selectedX.length);
|
|
1814
|
+
const pnsNorm = new Float64Array(selectedX.length);
|
|
1815
|
+
let ok = true;
|
|
1816
|
+
for (let i = 0; i < selectedX.length; i++) {
|
|
1817
|
+
const xNorm = 0.01 * selectedX[i];
|
|
1818
|
+
const yNorm = 0.01 * selectedY[i];
|
|
1819
|
+
const zNorm = 0.01 * selectedZ[i];
|
|
1820
|
+
const norm = Math.sqrt(xNorm * xNorm + yNorm * yNorm + zNorm * zNorm);
|
|
1821
|
+
timeSec[i] = selectedT[i];
|
|
1822
|
+
pnsX[i] = xNorm;
|
|
1823
|
+
pnsY[i] = yNorm;
|
|
1824
|
+
pnsZ[i] = zNorm;
|
|
1825
|
+
pnsNorm[i] = norm;
|
|
1826
|
+
if (norm >= 1) ok = false;
|
|
1827
|
+
}
|
|
1828
|
+
return { valid: true, ok, timeSec, pnsX, pnsY, pnsZ, pnsNorm };
|
|
1829
|
+
}
|
|
1830
|
+
function safePnsModel(dgdt, dtSec, hw) {
|
|
1831
|
+
const absDgdt = new Float64Array(dgdt.length);
|
|
1832
|
+
for (let i = 0; i < dgdt.length; i++) absDgdt[i] = Math.abs(dgdt[i]);
|
|
1833
|
+
const dtMs = dtSec * 1e3;
|
|
1834
|
+
const lp1 = lowpassTau(dgdt, hw.tau1Ms, dtMs);
|
|
1835
|
+
const lp2 = lowpassTau(absDgdt, hw.tau2Ms, dtMs);
|
|
1836
|
+
const lp3 = lowpassTau(dgdt, hw.tau3Ms, dtMs);
|
|
1837
|
+
const stim = new Float64Array(dgdt.length);
|
|
1838
|
+
const denom = hw.stimLimit > 0 ? hw.stimLimit : 1;
|
|
1839
|
+
for (let i = 0; i < dgdt.length; i++) {
|
|
1840
|
+
const s1 = hw.a1 * Math.abs(lp1[i]);
|
|
1841
|
+
const s2 = hw.a2 * lp2[i];
|
|
1842
|
+
const s3 = hw.a3 * Math.abs(lp3[i]);
|
|
1843
|
+
stim[i] = (s1 + s2 + s3) / denom * hw.gScale * 100;
|
|
1844
|
+
}
|
|
1845
|
+
return stim;
|
|
1846
|
+
}
|
|
1847
|
+
function invalidPns(error) {
|
|
1848
|
+
return {
|
|
1849
|
+
valid: false,
|
|
1850
|
+
ok: false,
|
|
1851
|
+
error,
|
|
1852
|
+
timeSec: new Float64Array(),
|
|
1853
|
+
pnsX: new Float64Array(),
|
|
1854
|
+
pnsY: new Float64Array(),
|
|
1855
|
+
pnsZ: new Float64Array(),
|
|
1856
|
+
pnsNorm: new Float64Array()
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
function parseAscText(text) {
|
|
1860
|
+
const scalar = /* @__PURE__ */ new Map();
|
|
1861
|
+
const array = /* @__PURE__ */ new Map();
|
|
1862
|
+
const re = /^\s*([A-Za-z0-9_.[\]]+?)(?:\[(\d+)])?\s*=\s*([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)\s*$/;
|
|
1863
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
1864
|
+
const line = rawLine.trim();
|
|
1865
|
+
if (!line || line.startsWith("#") || line.startsWith("###")) continue;
|
|
1866
|
+
if (/^\$include\b/i.test(line)) {
|
|
1867
|
+
throw new Error("ASC contains $include directives. Use a combined ASC profile in the web viewer, or open it through the VS Code extension so companion ASC files can be resolved.");
|
|
1868
|
+
}
|
|
1869
|
+
const match = re.exec(line);
|
|
1870
|
+
if (!match) continue;
|
|
1871
|
+
const key = match[1].trim();
|
|
1872
|
+
const index = match[2] === void 0 ? -1 : Number.parseInt(match[2], 10);
|
|
1873
|
+
const value = Number(match[3]);
|
|
1874
|
+
if (!Number.isFinite(value)) continue;
|
|
1875
|
+
if (index >= 0) {
|
|
1876
|
+
const values = array.get(key) ?? [];
|
|
1877
|
+
values[index] = value;
|
|
1878
|
+
array.set(key, values);
|
|
1879
|
+
} else {
|
|
1880
|
+
scalar.set(key, value);
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
return { scalar, array };
|
|
1884
|
+
}
|
|
1885
|
+
function resolvePnsPrefix(asc) {
|
|
1886
|
+
if (asc.array.has("flGSWDTauX")) return "";
|
|
1887
|
+
if (asc.array.has("GradPatSup.Phys.PNS.flGSWDTauX")) return "GradPatSup.Phys.PNS.";
|
|
1888
|
+
const candidates = [...asc.array.keys()].filter((key) => key.endsWith("flGSWDTauX") && !key.toLowerCase().includes(".carns.")).sort();
|
|
1889
|
+
if (candidates.length) return candidates[0].slice(0, -"flGSWDTauX".length);
|
|
1890
|
+
return "GradPatSup.Phys.PNS.";
|
|
1891
|
+
}
|
|
1892
|
+
function getAxisHardware(asc, tauKey, aKey, stimLimitKey, stimThreshKey, gScaleKeys) {
|
|
1893
|
+
const tau = findArray(asc, tauKey);
|
|
1894
|
+
const weights = findArray(asc, aKey);
|
|
1895
|
+
if (!tau || !weights) throw new Error(`Missing ASC arrays for ${tauKey} or ${aKey}`);
|
|
1896
|
+
if (tau.length < 3 || weights.length < 3) throw new Error(`ASC arrays ${tauKey}/${aKey} require at least 3 values`);
|
|
1897
|
+
const stimLimit = findScalar(asc, stimLimitKey);
|
|
1898
|
+
const stimThreshold = findScalar(asc, stimThreshKey);
|
|
1899
|
+
if (stimLimit === void 0 || stimThreshold === void 0) {
|
|
1900
|
+
throw new Error(`Missing ASC scalar ${stimLimitKey} or ${stimThreshKey}`);
|
|
1901
|
+
}
|
|
1902
|
+
let gScale;
|
|
1903
|
+
for (const key of gScaleKeys) {
|
|
1904
|
+
gScale = findScalar(asc, key);
|
|
1905
|
+
if (gScale !== void 0) break;
|
|
1906
|
+
}
|
|
1907
|
+
if (gScale === void 0) {
|
|
1908
|
+
throw new Error("ASC is missing g_scale factors (X/Y/Z). Select a full ASC (e.g. *_twoFilesCombined.asc).");
|
|
1909
|
+
}
|
|
1910
|
+
return {
|
|
1911
|
+
tau1Ms: tau[0],
|
|
1912
|
+
tau2Ms: tau[1],
|
|
1913
|
+
tau3Ms: tau[2],
|
|
1914
|
+
a1: weights[0],
|
|
1915
|
+
a2: weights[1],
|
|
1916
|
+
a3: weights[2],
|
|
1917
|
+
stimLimit,
|
|
1918
|
+
stimThreshold,
|
|
1919
|
+
gScale
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
function findArray(asc, key) {
|
|
1923
|
+
const exact = asc.array.get(key);
|
|
1924
|
+
if (exact) return exact;
|
|
1925
|
+
const keyNorm = normalizeAscKey(key);
|
|
1926
|
+
const chosen = [...asc.array.keys()].filter((candidate) => normalizeAscKey(candidate) === keyNorm && !candidate.toLowerCase().includes(".carns.")).sort()[0];
|
|
1927
|
+
return chosen ? asc.array.get(chosen) : void 0;
|
|
1928
|
+
}
|
|
1929
|
+
function findScalar(asc, key) {
|
|
1930
|
+
const exact = asc.scalar.get(key);
|
|
1931
|
+
if (exact !== void 0) return exact;
|
|
1932
|
+
const keyNorm = normalizeAscKey(key);
|
|
1933
|
+
const chosen = [...asc.scalar.keys()].filter((candidate) => normalizeAscKey(candidate) === keyNorm && !candidate.toLowerCase().includes(".carns.")).sort()[0];
|
|
1934
|
+
return chosen ? asc.scalar.get(chosen) : void 0;
|
|
1935
|
+
}
|
|
1936
|
+
function normalizeAscKey(key) {
|
|
1937
|
+
return key.trim().replace(/\[\d+]/g, "");
|
|
1938
|
+
}
|
|
1939
|
+
function hasValidWeights(hw) {
|
|
1940
|
+
return Math.abs(hw.a1 + hw.a2 + hw.a3 - 1) <= 0.01 && hw.stimLimit > 0;
|
|
1941
|
+
}
|
|
1942
|
+
function lowpassTau(input, tauMs, dtMs) {
|
|
1943
|
+
const out = new Float64Array(input.length);
|
|
1944
|
+
if (!input.length) return out;
|
|
1945
|
+
if (tauMs <= 0 || dtMs <= 0) {
|
|
1946
|
+
out.set(input);
|
|
1947
|
+
return out;
|
|
1948
|
+
}
|
|
1949
|
+
const alpha = dtMs / (tauMs + dtMs);
|
|
1950
|
+
out[0] = alpha * input[0];
|
|
1951
|
+
for (let i = 1; i < input.length; i++) out[i] = alpha * input[i] + (1 - alpha) * out[i - 1];
|
|
1952
|
+
return out;
|
|
1953
|
+
}
|
|
1954
|
+
function collectGradientSeries2(blocks, channel) {
|
|
1955
|
+
const time = [];
|
|
1956
|
+
const value = [];
|
|
1957
|
+
for (const block of blocks) {
|
|
1958
|
+
const grad = block[channel];
|
|
1959
|
+
if (!grad?.timePoints || !grad.waveform) continue;
|
|
1960
|
+
const n = Math.min(grad.timePoints.length, grad.waveform.length);
|
|
1961
|
+
for (let i = 0; i < n; i++) {
|
|
1962
|
+
time.push(grad.timePoints[i]);
|
|
1963
|
+
value.push(grad.waveform[i]);
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
return sanitizeGradientSeries2(time, value);
|
|
1967
|
+
}
|
|
1968
|
+
function sanitizeGradientSeries2(time, value) {
|
|
1969
|
+
const pairs = [];
|
|
1970
|
+
const n = Math.min(time.length, value.length);
|
|
1971
|
+
for (let i = 0; i < n; i++) {
|
|
1972
|
+
if (Number.isFinite(time[i]) && Number.isFinite(value[i])) pairs.push([time[i], value[i]]);
|
|
1973
|
+
}
|
|
1974
|
+
pairs.sort((a, b) => a[0] - b[0]);
|
|
1975
|
+
const outT = [];
|
|
1976
|
+
const outV = [];
|
|
1977
|
+
for (const [t, v] of pairs) {
|
|
1978
|
+
const last = outT.length - 1;
|
|
1979
|
+
if (last >= 0 && Math.abs(t - outT[last]) <= TIME_EPS2) {
|
|
1980
|
+
outV[last] = 0.5 * (outV[last] + v);
|
|
1981
|
+
continue;
|
|
1982
|
+
}
|
|
1983
|
+
outT.push(t);
|
|
1984
|
+
outV.push(v);
|
|
1985
|
+
}
|
|
1986
|
+
return { time: outT, value: outV };
|
|
1987
|
+
}
|
|
1988
|
+
function interpLinearZero(series, t) {
|
|
1989
|
+
const n = series.time.length;
|
|
1990
|
+
if (!n || t < series.time[0] || t > series.time[n - 1]) return 0;
|
|
1991
|
+
if (n === 1 || t <= series.time[0]) return series.value[0];
|
|
1992
|
+
if (t >= series.time[n - 1]) return series.value[n - 1];
|
|
1993
|
+
let lo = 0;
|
|
1994
|
+
let hi = n - 1;
|
|
1995
|
+
while (hi - lo > 1) {
|
|
1996
|
+
const mid = lo + hi >> 1;
|
|
1997
|
+
if (series.time[mid] <= t) lo = mid;
|
|
1998
|
+
else hi = mid;
|
|
1999
|
+
}
|
|
2000
|
+
const t0 = series.time[lo];
|
|
2001
|
+
const t1 = series.time[hi];
|
|
2002
|
+
if (!(t1 > t0)) return series.value[lo];
|
|
2003
|
+
const alpha = (t - t0) / (t1 - t0);
|
|
2004
|
+
return series.value[lo] + alpha * (series.value[hi] - series.value[lo]);
|
|
2005
|
+
}
|
|
2006
|
+
function padSamples(input, preCount, postCount) {
|
|
2007
|
+
const out = new Float64Array(preCount + input.length + postCount);
|
|
2008
|
+
out.set(input, preCount);
|
|
2009
|
+
return out;
|
|
2010
|
+
}
|
|
2011
|
+
function diff(input, dtSec) {
|
|
2012
|
+
const out = new Float64Array(Math.max(0, input.length - 1));
|
|
2013
|
+
for (let i = 0; i < out.length; i++) out[i] = (input[i + 1] - input[i]) / dtSec;
|
|
2014
|
+
return out;
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
// src/pulseq/trdetect.ts
|
|
2018
|
+
var GAMMA_HZ_T2 = 42576e3;
|
|
2019
|
+
var DEFAULT_B0_T2 = 3;
|
|
2020
|
+
function detectSequenceTiming(seq) {
|
|
2021
|
+
const b0 = getB02(seq);
|
|
2022
|
+
const supportsRfUse = seq.versionCombined >= 1005e3;
|
|
2023
|
+
let teTimeSec = 0;
|
|
2024
|
+
let hasExplicitTE = false;
|
|
2025
|
+
const teDef = seq.definitions.get("EchoTime") ?? seq.definitions.get("TE");
|
|
2026
|
+
if (teDef && teDef.length > 0) {
|
|
2027
|
+
teTimeSec = teDef[0];
|
|
2028
|
+
hasExplicitTE = true;
|
|
2029
|
+
}
|
|
2030
|
+
let trTimeSec = 0;
|
|
2031
|
+
let hasExplicitTR = false;
|
|
2032
|
+
const trDef = seq.definitions.get("RepetitionTime") ?? seq.definitions.get("TR");
|
|
2033
|
+
if (trDef && trDef.length > 0) {
|
|
2034
|
+
trTimeSec = trDef[0];
|
|
2035
|
+
hasExplicitTR = true;
|
|
2036
|
+
}
|
|
2037
|
+
const rfUsePerBlock = [];
|
|
2038
|
+
const excitationTimesSec = [];
|
|
2039
|
+
let rfUseGuessed = false;
|
|
2040
|
+
const blockStartTimes = computeCumulativeTimes(seq);
|
|
2041
|
+
for (let i = 0; i < seq.blocks.length; i++) {
|
|
2042
|
+
const blk = seq.blocks[i];
|
|
2043
|
+
if (blk.rfId <= 0) {
|
|
2044
|
+
rfUsePerBlock.push(0);
|
|
2045
|
+
continue;
|
|
2046
|
+
}
|
|
2047
|
+
const rf = seq.rfs.get(blk.rfId);
|
|
2048
|
+
if (!rf) {
|
|
2049
|
+
rfUsePerBlock.push(0);
|
|
2050
|
+
continue;
|
|
2051
|
+
}
|
|
2052
|
+
const useChar = classifyRfUse2(rf, seq, supportsRfUse, b0);
|
|
2053
|
+
const useCode = useChar.charCodeAt(0);
|
|
2054
|
+
rfUsePerBlock.push(useCode);
|
|
2055
|
+
if (useChar === "e") {
|
|
2056
|
+
const center = rf.center >= 0 ? rf.center * 1e-6 : estimateRfCenter(rf, seq);
|
|
2057
|
+
const excTime = blockStartTimes[i] + rf.delay * 1e-6 + center;
|
|
2058
|
+
excitationTimesSec.push(excTime);
|
|
2059
|
+
}
|
|
2060
|
+
if (!supportsRfUse && useChar !== "u") rfUseGuessed = true;
|
|
2061
|
+
}
|
|
2062
|
+
let trCount = 0;
|
|
2063
|
+
const trStartBlocks = [];
|
|
2064
|
+
if (!hasExplicitTR && excitationTimesSec.length >= 2) {
|
|
2065
|
+
trTimeSec = estimateTRFromExcitations(excitationTimesSec);
|
|
2066
|
+
hasExplicitTR = false;
|
|
2067
|
+
}
|
|
2068
|
+
if (trTimeSec > 0) {
|
|
2069
|
+
const totalDuration = blockStartTimes.length > 0 ? blockStartTimes[blockStartTimes.length - 1] + blockDurationSeconds2(seq, seq.blocks[seq.blocks.length - 1]) : 0;
|
|
2070
|
+
trCount = Math.max(1, Math.ceil(totalDuration / trTimeSec));
|
|
2071
|
+
const tol = trTimeSec * 0.3;
|
|
2072
|
+
let trIdx = 0;
|
|
2073
|
+
for (let i = 0; i < seq.blocks.length; i++) {
|
|
2074
|
+
const blkStart = blockStartTimes[i];
|
|
2075
|
+
const expected = trIdx * trTimeSec;
|
|
2076
|
+
if (blkStart >= expected - tol && trIdx < trCount) {
|
|
2077
|
+
trStartBlocks.push(i);
|
|
2078
|
+
trIdx++;
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
trStartBlocks.push(seq.blocks.length);
|
|
2082
|
+
trCount = trStartBlocks.length - 1;
|
|
2083
|
+
} else {
|
|
2084
|
+
trCount = 0;
|
|
2085
|
+
for (let i = 0; i < seq.blocks.length; i++) {
|
|
2086
|
+
if (seq.blocks[i].adcId > 0) {
|
|
2087
|
+
trStartBlocks.push(i);
|
|
2088
|
+
trCount++;
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
trStartBlocks.push(seq.blocks.length);
|
|
2092
|
+
}
|
|
2093
|
+
return {
|
|
2094
|
+
teTimeSec,
|
|
2095
|
+
hasExplicitTE,
|
|
2096
|
+
trTimeSec,
|
|
2097
|
+
hasExplicitTR,
|
|
2098
|
+
trCount,
|
|
2099
|
+
trStartBlocks,
|
|
2100
|
+
excitationTimesSec,
|
|
2101
|
+
rfUseGuessed,
|
|
2102
|
+
rfUsePerBlock
|
|
2103
|
+
};
|
|
2104
|
+
}
|
|
2105
|
+
function getB02(seq) {
|
|
2106
|
+
const raw = seq.definitions.get("B0") ?? seq.definitions.get("b0") ?? seq.definitions.get("b_0");
|
|
2107
|
+
if (raw && Array.isArray(raw) && raw.length > 0) return +raw[0];
|
|
2108
|
+
return DEFAULT_B0_T2;
|
|
2109
|
+
}
|
|
2110
|
+
function computeCumulativeTimes(seq) {
|
|
2111
|
+
const times = [];
|
|
2112
|
+
let cum = 0;
|
|
2113
|
+
for (const blk of seq.blocks) {
|
|
2114
|
+
times.push(cum);
|
|
2115
|
+
cum += blockDurationSeconds2(seq, blk);
|
|
2116
|
+
}
|
|
2117
|
+
return times;
|
|
2118
|
+
}
|
|
2119
|
+
function blockDurationSeconds2(seq, block) {
|
|
2120
|
+
if (seq.versionCombined < VER_PRE_14) return block.dur * 1e-6;
|
|
2121
|
+
return block.dur * seq.rasterTimes.blockDurationRaster;
|
|
2122
|
+
}
|
|
2123
|
+
function classifyRfUse2(rf, seq, supportsMetadata, b0Tesla) {
|
|
2124
|
+
if (supportsMetadata && rf.use && rf.use !== "u" && rf.use !== "U") {
|
|
2125
|
+
return rf.use.toLowerCase();
|
|
2126
|
+
}
|
|
2127
|
+
const faDeg = estimateFlipAngleDeg(rf, seq);
|
|
2128
|
+
if (faDeg < 90.01) return "e";
|
|
2129
|
+
const freqPPM = rf.freqPPM !== 0 ? rf.freqPPM : b0Tesla > 0 ? 1e6 * rf.freqOffset / (GAMMA_HZ_T2 * b0Tesla) : 0;
|
|
2130
|
+
const durEst = estimateRfDuration(rf, seq);
|
|
2131
|
+
if (durEst > 6e-3 && freqPPM >= -4.5 && freqPPM <= -3) return "s";
|
|
2132
|
+
return "r";
|
|
2133
|
+
}
|
|
2134
|
+
function estimateFlipAngleDeg(rf, seq) {
|
|
2135
|
+
const magShape = seq.shapes.get(rf.magShapeId);
|
|
2136
|
+
if (magShape && magShape.numSamples > 0) {
|
|
2137
|
+
const raster = seq.rasterTimes.rfRaster;
|
|
2138
|
+
const timeShape = rf.timeShapeId > 0 ? seq.shapes.get(rf.timeShapeId)?.samples : void 0;
|
|
2139
|
+
let area = 0;
|
|
2140
|
+
let prevT = timeShape ? timeShape[0] * raster : 0.5 * raster;
|
|
2141
|
+
let prevAmp = Math.abs(rf.amplitude * magShape.samples[0]);
|
|
2142
|
+
for (let i = 1; i < magShape.numSamples; i++) {
|
|
2143
|
+
const t = timeShape ? timeShape[i] * raster : (i + 0.5) * raster;
|
|
2144
|
+
const amp = Math.abs(rf.amplitude * magShape.samples[i]);
|
|
2145
|
+
const dt = t - prevT;
|
|
2146
|
+
if (dt > 0) area += 0.5 * (prevAmp + amp) * dt;
|
|
2147
|
+
prevT = t;
|
|
2148
|
+
prevAmp = amp;
|
|
2149
|
+
}
|
|
2150
|
+
return 360 * area;
|
|
2151
|
+
}
|
|
2152
|
+
const absAmp = Math.abs(rf.amplitude);
|
|
2153
|
+
if (absAmp > 3e3) return 180;
|
|
2154
|
+
if (absAmp > 1500) return 120;
|
|
2155
|
+
return 90;
|
|
2156
|
+
}
|
|
2157
|
+
function estimateRfCenter(rf, _seq) {
|
|
2158
|
+
const magShape = _seq.shapes.get(rf.magShapeId);
|
|
2159
|
+
if (!magShape || magShape.numSamples <= 0) return 0;
|
|
2160
|
+
let peakIdx = 0;
|
|
2161
|
+
let peak = Math.abs(magShape.samples[0]);
|
|
2162
|
+
for (let i = 1; i < magShape.numSamples; i++) {
|
|
2163
|
+
const v = Math.abs(magShape.samples[i]);
|
|
2164
|
+
if (v > peak) {
|
|
2165
|
+
peak = v;
|
|
2166
|
+
peakIdx = i;
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
const raster = _seq.rasterTimes.rfRaster;
|
|
2170
|
+
const timeShape = rf.timeShapeId > 0 ? _seq.shapes.get(rf.timeShapeId)?.samples : void 0;
|
|
2171
|
+
return timeShape ? (timeShape[peakIdx] ?? 0) * raster : (peakIdx + 0.5) * raster;
|
|
2172
|
+
}
|
|
2173
|
+
function estimateRfDuration(rf, seq) {
|
|
2174
|
+
const magShape = seq.shapes.get(rf.magShapeId);
|
|
2175
|
+
if (!magShape || magShape.numSamples <= 0) return 0;
|
|
2176
|
+
const raster = seq.rasterTimes.rfRaster;
|
|
2177
|
+
const timeShape = rf.timeShapeId > 0 ? seq.shapes.get(rf.timeShapeId)?.samples : void 0;
|
|
2178
|
+
if (timeShape && timeShape.length > 0) {
|
|
2179
|
+
return timeShape[timeShape.length - 1] * raster + raster;
|
|
2180
|
+
}
|
|
2181
|
+
return magShape.numSamples * raster;
|
|
2182
|
+
}
|
|
2183
|
+
function estimateTRFromExcitations(excTimesSec) {
|
|
2184
|
+
if (excTimesSec.length < 2) return 0;
|
|
2185
|
+
const intervals = [];
|
|
2186
|
+
for (let i = 1; i < excTimesSec.length; i++) {
|
|
2187
|
+
const dt = excTimesSec[i] - excTimesSec[i - 1];
|
|
2188
|
+
if (dt > 1e-9) intervals.push(dt);
|
|
2189
|
+
}
|
|
2190
|
+
if (intervals.length === 0) return 0;
|
|
2191
|
+
intervals.sort((a, b) => a - b);
|
|
2192
|
+
const median = intervals[Math.floor(intervals.length / 2)];
|
|
2193
|
+
const niceMs = niceRound(median * 1e3, 10);
|
|
2194
|
+
return niceMs * 1e-3;
|
|
2195
|
+
}
|
|
2196
|
+
function niceRound(value, base) {
|
|
2197
|
+
return Math.round(value / base) * base;
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
// src/pulseq/kspaceExportArtifacts.ts
|
|
2201
|
+
function exportKspaceArtifacts(sequenceText, sequenceName, options = {}) {
|
|
2202
|
+
const seq = parseSequenceText(sequenceText);
|
|
2203
|
+
const decoded = decodeAllBlocks(seq);
|
|
2204
|
+
const totalDuration = getTotalDuration(seq);
|
|
2205
|
+
const gradientSupport = options.gradientSupport ?? "all";
|
|
2206
|
+
const kspace = calculateKspace(
|
|
2207
|
+
decoded,
|
|
2208
|
+
seq.rasterTimes.gradientRaster,
|
|
2209
|
+
totalDuration,
|
|
2210
|
+
0,
|
|
2211
|
+
{ maxGridPoints: options.maxGridPoints, rfRaster: seq.rasterTimes.rfRaster, gradientSupport }
|
|
2212
|
+
);
|
|
2213
|
+
if (!kspace) {
|
|
2214
|
+
throw new Error("Unable to calculate k-space trajectory for sequence");
|
|
2215
|
+
}
|
|
2216
|
+
const metadata = createMetadata(
|
|
2217
|
+
seq,
|
|
2218
|
+
kspace,
|
|
2219
|
+
sequenceName,
|
|
2220
|
+
options.sequenceSha256 ?? "unknown",
|
|
2221
|
+
options.packageVersion ?? "unknown",
|
|
2222
|
+
!!options.includeFullTrajectory,
|
|
2223
|
+
totalDuration,
|
|
2224
|
+
gradientSupport
|
|
2225
|
+
);
|
|
2226
|
+
return {
|
|
2227
|
+
ktrajAdcText: formatTrajectoryText(kspace.ktraj_adc),
|
|
2228
|
+
ktrajText: options.includeFullTrajectory ? formatTrajectoryText(kspace.ktraj) : void 0,
|
|
2229
|
+
metadata
|
|
2230
|
+
};
|
|
2231
|
+
}
|
|
2232
|
+
function formatTrajectoryText(series) {
|
|
2233
|
+
assertThreeEqualLengthSeries(series);
|
|
2234
|
+
const n = series[0].length;
|
|
2235
|
+
if (n === 0) return "";
|
|
2236
|
+
const rows = [];
|
|
2237
|
+
for (let i = 0; i < n; i++) {
|
|
2238
|
+
rows.push(`${formatFloat(series[0][i])} ${formatFloat(series[1][i])} ${formatFloat(series[2][i])}`);
|
|
2239
|
+
}
|
|
2240
|
+
return `${rows.join("\n")}
|
|
2241
|
+
`;
|
|
2242
|
+
}
|
|
2243
|
+
function formatFloat(value) {
|
|
2244
|
+
if (Number.isNaN(value)) return "NaN";
|
|
2245
|
+
if (!Number.isFinite(value)) return value > 0 ? "Infinity" : "-Infinity";
|
|
2246
|
+
const normalized = Object.is(value, -0) ? 0 : value;
|
|
2247
|
+
return normalized.toExponential(12).replace(/e([+-])(\d+)$/, (_match, sign, exponent) => `e${sign}${exponent.padStart(2, "0")}`);
|
|
2248
|
+
}
|
|
2249
|
+
function createMetadata(seq, kspace, sequenceName, sequenceSha256, packageVersion, includeFullTrajectory, totalDurationSec, gradientSupport) {
|
|
2250
|
+
return {
|
|
2251
|
+
schemaVersion: 1,
|
|
2252
|
+
sequenceName,
|
|
2253
|
+
sequenceSha256,
|
|
2254
|
+
packageVersion,
|
|
2255
|
+
pulseqVersion: {
|
|
2256
|
+
major: seq.version.major,
|
|
2257
|
+
minor: seq.version.minor,
|
|
2258
|
+
revision: seq.version.revision,
|
|
2259
|
+
combined: seq.versionCombined
|
|
2260
|
+
},
|
|
2261
|
+
blockCount: seq.blocks.length,
|
|
2262
|
+
rasterTimes: {
|
|
2263
|
+
blockDuration: seq.rasterTimes.blockDurationRaster,
|
|
2264
|
+
gradient: seq.rasterTimes.gradientRaster,
|
|
2265
|
+
rf: seq.rasterTimes.rfRaster,
|
|
2266
|
+
adc: seq.rasterTimes.adcRaster
|
|
2267
|
+
},
|
|
2268
|
+
totalDurationSec,
|
|
2269
|
+
adcSampleCount: kspace.t_adc.length,
|
|
2270
|
+
trajectorySampleCount: kspace.t_ktraj.length,
|
|
2271
|
+
units: {
|
|
2272
|
+
trajectory: "1/m",
|
|
2273
|
+
time: "s",
|
|
2274
|
+
gradient: "Hz/m",
|
|
2275
|
+
convention: "Pulseq gradient integral without 2*pi factor"
|
|
2276
|
+
},
|
|
2277
|
+
calculation: {
|
|
2278
|
+
gradientSupport
|
|
2279
|
+
},
|
|
2280
|
+
files: includeFullTrajectory ? { ktrajAdc: "ktraj_adc.txt", ktraj: "ktraj.txt" } : { ktrajAdc: "ktraj_adc.txt" }
|
|
2281
|
+
};
|
|
2282
|
+
}
|
|
2283
|
+
function assertThreeEqualLengthSeries(series) {
|
|
2284
|
+
if (series.length !== 3) {
|
|
2285
|
+
throw new Error(`Expected three trajectory axes, received ${series.length}`);
|
|
2286
|
+
}
|
|
2287
|
+
const n = series[0].length;
|
|
2288
|
+
if (series[1].length !== n || series[2].length !== n) {
|
|
2289
|
+
throw new Error("Trajectory axes have mismatched sample counts");
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
// web/pulseq-browser.ts
|
|
2294
|
+
var PACKAGE_VERSION = version;
|
|
2295
|
+
return __toCommonJS(pulseq_browser_exports);
|
|
2296
|
+
})();
|