seqeyes-python 0.2.2__py3-none-any.whl → 0.2.7__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.
@@ -23,22 +23,37 @@ var Pulseq = (() => {
23
23
  // web/pulseq-browser.ts
24
24
  var pulseq_browser_exports = {};
25
25
  __export(pulseq_browser_exports, {
26
+ INTERACTIVE_COMPUTE_LIMITS: () => INTERACTIVE_COMPUTE_LIMITS,
26
27
  PACKAGE_VERSION: () => PACKAGE_VERSION,
27
28
  calculateKspace: () => calculateKspace,
28
29
  calculateM1: () => calculateM1,
30
+ calculateM1Coarse: () => calculateM1Coarse,
29
31
  calculatePns: () => calculatePns,
32
+ calculatePnsCoarse: () => calculatePnsCoarse,
30
33
  decodeAllBlocks: () => decodeAllBlocks,
31
34
  detectSequenceTiming: () => detectSequenceTiming,
35
+ estimateDerivedCost: () => estimateDerivedCost,
36
+ estimateKspaceCost: () => estimateKspaceCost,
37
+ estimateKspacePeakMemoryBytes: () => estimateKspacePeakMemoryBytes,
32
38
  exportKspaceArtifacts: () => exportKspaceArtifacts,
39
+ exportKspaceArtifactsFromBytes: () => exportKspaceArtifactsFromBytes,
40
+ exportKspaceArtifactsFromSequence: () => exportKspaceArtifactsFromSequence,
41
+ formatMemorySize: () => formatMemorySize,
42
+ formatSampleCount: () => formatSampleCount,
33
43
  formatTrajectoryText: () => formatTrajectoryText,
34
44
  getTotalDuration: () => getTotalDuration,
45
+ hasPulseqBinaryMagic: () => hasPulseqBinaryMagic,
35
46
  parsePnsHardwareAsc: () => parsePnsHardwareAsc,
47
+ parseSequenceBinary: () => parseSequenceBinary,
48
+ parseSequenceBytes: () => parseSequenceBytes,
36
49
  parseSequenceText: () => parseSequenceText,
37
- safePnsModel: () => safePnsModel
50
+ safePnsModel: () => safePnsModel,
51
+ selectM1WindowBlocks: () => selectM1WindowBlocks,
52
+ selectPnsWindowBlocks: () => selectPnsWindowBlocks
38
53
  });
39
54
 
40
55
  // package.json
41
- var version = "0.2.2";
56
+ var version = "0.2.7";
42
57
 
43
58
  // src/pulseq/decompressor.ts
44
59
  function decompressShape(compressed, numSamples) {
@@ -103,6 +118,181 @@ var Pulseq = (() => {
103
118
  return major * 1e6 + minor * 1e3 + revision;
104
119
  }
105
120
 
121
+ // src/pulseq/readerShared.ts
122
+ function createEmptySequence() {
123
+ return {
124
+ version: { major: 1, minor: 0, revision: 0 },
125
+ versionCombined: 0,
126
+ definitions: /* @__PURE__ */ new Map(),
127
+ definitionsRaw: /* @__PURE__ */ new Map(),
128
+ blocks: [],
129
+ rfs: /* @__PURE__ */ new Map(),
130
+ arbitraryGrads: /* @__PURE__ */ new Map(),
131
+ trapGrads: /* @__PURE__ */ new Map(),
132
+ adcs: /* @__PURE__ */ new Map(),
133
+ extensions: /* @__PURE__ */ new Map(),
134
+ extensionNames: /* @__PURE__ */ new Map(),
135
+ extensionTypes: /* @__PURE__ */ new Map(),
136
+ triggers: [],
137
+ ncos: [],
138
+ rotations: [],
139
+ labelSets: [],
140
+ labelIncs: [],
141
+ softDelays: [],
142
+ rfShims: [],
143
+ shapes: /* @__PURE__ */ new Map(),
144
+ rasterTimes: { blockDurationRaster: 1e-5, gradientRaster: 1e-5, rfRaster: 1e-6, adcRaster: 1e-7 }
145
+ };
146
+ }
147
+ function parseError(message) {
148
+ throw new Error(`Pulseq parse error: ${message}`);
149
+ }
150
+ function extensionNameToType(name) {
151
+ switch (name.toUpperCase()) {
152
+ case "TRIGGERS":
153
+ return 1 /* EXT_TRIGGER */;
154
+ case "ROTATIONS":
155
+ return 2 /* EXT_ROTATION */;
156
+ case "LABELSET":
157
+ return 3 /* EXT_LABELSET */;
158
+ case "LABELINC":
159
+ return 4 /* EXT_LABELINC */;
160
+ case "DELAYS":
161
+ return 5 /* EXT_DELAY */;
162
+ case "RF_SHIMS":
163
+ return 6 /* EXT_RF_SHIM */;
164
+ case "NCO":
165
+ return 100 /* EXT_NCO */;
166
+ default:
167
+ return 999 /* EXT_UNKNOWN */;
168
+ }
169
+ }
170
+ var KNOWN_LABELS = {
171
+ "SLC": { labelId: 0, flagId: 0 },
172
+ "SEG": { labelId: 1, flagId: 0 },
173
+ "REP": { labelId: 2, flagId: 0 },
174
+ "AVG": { labelId: 3, flagId: 0 },
175
+ "ECO": { labelId: 4, flagId: 0 },
176
+ "PHS": { labelId: 5, flagId: 0 },
177
+ "SET": { labelId: 6, flagId: 0 },
178
+ "ACQ": { labelId: 7, flagId: 0 },
179
+ "LIN": { labelId: 8, flagId: 0 },
180
+ "PAR": { labelId: 9, flagId: 0 },
181
+ "ONCE": { labelId: 10, flagId: 0 },
182
+ "NAV": { labelId: 0, flagId: 1 },
183
+ "REV": { labelId: 0, flagId: 2 },
184
+ "SMS": { labelId: 0, flagId: 4 },
185
+ "REF": { labelId: 0, flagId: 8 },
186
+ "IMA": { labelId: 0, flagId: 16 },
187
+ "OFF": { labelId: 0, flagId: 32 },
188
+ "NOISE": { labelId: 0, flagId: 64 },
189
+ "PMC": { labelId: 0, flagId: 128 },
190
+ "NOPOS": { labelId: 0, flagId: 256 },
191
+ "NOROT": { labelId: 0, flagId: 512 },
192
+ "NOSCL": { labelId: 0, flagId: 1024 }
193
+ };
194
+ var unknownLabelCounter = 0;
195
+ var unknownLabels = /* @__PURE__ */ new Map();
196
+ function resetUnknownLabels() {
197
+ unknownLabelCounter = 0;
198
+ unknownLabels.clear();
199
+ }
200
+ function decodeLabel(name) {
201
+ const known = KNOWN_LABELS[name];
202
+ if (known) return known;
203
+ let id = unknownLabels.get(name);
204
+ if (id === void 0) {
205
+ id = 1e3 + unknownLabelCounter++;
206
+ unknownLabels.set(name, id);
207
+ }
208
+ return { labelId: id, flagId: 0 };
209
+ }
210
+ function extractRasterTimes(seq) {
211
+ const set = (key, field) => {
212
+ const value = seq.definitions.get(key);
213
+ if (value?.length) seq.rasterTimes[field] = value[0];
214
+ };
215
+ set("BlockDurationRaster", "blockDurationRaster");
216
+ set("GradientRasterTime", "gradientRaster");
217
+ set("RadiofrequencyRasterTime", "rfRaster");
218
+ set("AdcRasterTime", "adcRaster");
219
+ }
220
+ function validateSequence(seq, seenSections) {
221
+ if (!seenSections.has("VERSION")) parseError("Required [VERSION] section is missing");
222
+ if (seq.version.major !== 1 || seq.version.minor > 5) {
223
+ parseError(`Unsupported Pulseq version ${seq.version.major}.${seq.version.minor}.${seq.version.revision}`);
224
+ }
225
+ const version2 = seq.versionCombined > 0 ? seq.versionCombined : makeVersionCombined(seq.version.major, seq.version.minor, seq.version.revision);
226
+ if (version2 >= VER_PRE_14) {
227
+ requireNumericDefinition(seq, "AdcRasterTime");
228
+ requireNumericDefinition(seq, "GradientRasterTime");
229
+ requireNumericDefinition(seq, "RadiofrequencyRasterTime");
230
+ requireNumericDefinition(seq, "BlockDurationRaster");
231
+ }
232
+ if (version2 >= VER_V15001) {
233
+ const required = seq.definitionsRaw.get("RequiredExtensions")?.split(/\s+/).filter(Boolean) ?? [];
234
+ for (const name of required) {
235
+ if (extensionNameToType(name) === 999 /* EXT_UNKNOWN */) {
236
+ parseError(`Unknown required extension '${name}'`);
237
+ }
238
+ }
239
+ }
240
+ if (!seenSections.has("BLOCKS")) parseError("Required [BLOCKS] section is missing");
241
+ for (const block of seq.blocks) {
242
+ if (block.rfId > 0 && !seq.rfs.has(block.rfId)) {
243
+ parseError(`Block ${block.num} references undefined RF event ${block.rfId}`);
244
+ }
245
+ for (const [channel, gradId] of [["GX", block.gxId], ["GY", block.gyId], ["GZ", block.gzId]]) {
246
+ if (gradId > 0 && !seq.arbitraryGrads.has(gradId) && !seq.trapGrads.has(gradId)) {
247
+ parseError(`Block ${block.num} references undefined ${channel} gradient event ${gradId}`);
248
+ }
249
+ }
250
+ if (block.adcId > 0 && !seq.adcs.has(block.adcId)) {
251
+ parseError(`Block ${block.num} references undefined ADC event ${block.adcId}`);
252
+ }
253
+ if (block.extId > 0 && !seq.extensions.has(block.extId)) {
254
+ parseError(`Block ${block.num} references undefined extension list ${block.extId}`);
255
+ }
256
+ }
257
+ for (const ext of seq.extensions.values()) {
258
+ if (ext.nextId > 0 && !seq.extensions.has(ext.nextId)) {
259
+ parseError(`Extension list ${ext.id} references undefined next extension ${ext.nextId}`);
260
+ }
261
+ const type = seq.extensionTypes.get(ext.type) ?? 999 /* EXT_UNKNOWN */;
262
+ if (type === 999 /* EXT_UNKNOWN */) continue;
263
+ if (!extensionPayloadExists(seq, type, ext.ref)) {
264
+ const name = seq.extensionNames.get(ext.type) ?? `type ${ext.type}`;
265
+ parseError(`Extension list ${ext.id} references undefined ${name} payload ${ext.ref}`);
266
+ }
267
+ }
268
+ }
269
+ function requireNumericDefinition(seq, name) {
270
+ const value = seq.definitions.get(name);
271
+ if (!value || value.length === 0 || !Number.isFinite(value[0])) {
272
+ parseError(`Required definition ${name} is not present in the file`);
273
+ }
274
+ }
275
+ function extensionPayloadExists(seq, type, ref) {
276
+ switch (type) {
277
+ case 1 /* EXT_TRIGGER */:
278
+ return seq.triggers.some((value) => value.id === ref);
279
+ case 2 /* EXT_ROTATION */:
280
+ return seq.rotations.some((value) => value.id === ref);
281
+ case 3 /* EXT_LABELSET */:
282
+ return seq.labelSets.some((value) => value.id === ref);
283
+ case 4 /* EXT_LABELINC */:
284
+ return seq.labelIncs.some((value) => value.id === ref);
285
+ case 5 /* EXT_DELAY */:
286
+ return seq.softDelays.some((value) => value.id === ref);
287
+ case 6 /* EXT_RF_SHIM */:
288
+ return seq.rfShims.some((value) => value.id === ref);
289
+ case 100 /* EXT_NCO */:
290
+ return seq.ncos.some((value) => value.id === ref);
291
+ default:
292
+ return false;
293
+ }
294
+ }
295
+
106
296
  // src/pulseq/reader.ts
107
297
  function parseSequenceText(text) {
108
298
  const seq = createEmptySequence();
@@ -182,38 +372,10 @@ var Pulseq = (() => {
182
372
  break;
183
373
  }
184
374
  }
185
- function createEmptySequence() {
186
- return {
187
- version: { major: 1, minor: 0, revision: 0 },
188
- versionCombined: 0,
189
- definitions: /* @__PURE__ */ new Map(),
190
- definitionsRaw: /* @__PURE__ */ new Map(),
191
- blocks: [],
192
- rfs: /* @__PURE__ */ new Map(),
193
- arbitraryGrads: /* @__PURE__ */ new Map(),
194
- trapGrads: /* @__PURE__ */ new Map(),
195
- adcs: /* @__PURE__ */ new Map(),
196
- extensions: /* @__PURE__ */ new Map(),
197
- extensionNames: /* @__PURE__ */ new Map(),
198
- extensionTypes: /* @__PURE__ */ new Map(),
199
- triggers: [],
200
- ncos: [],
201
- rotations: [],
202
- labelSets: [],
203
- labelIncs: [],
204
- softDelays: [],
205
- rfShims: [],
206
- shapes: /* @__PURE__ */ new Map(),
207
- rasterTimes: { blockDurationRaster: 1e-5, gradientRaster: 1e-5, rfRaster: 1e-6, adcRaster: 1e-7 }
208
- };
209
- }
210
375
  function ver(seq) {
211
376
  if (seq.versionCombined > 0) return seq.versionCombined;
212
377
  return makeVersionCombined(seq.version.major, seq.version.minor, seq.version.revision);
213
378
  }
214
- function parseError(message) {
215
- throw new Error(`Pulseq parse error: ${message}`);
216
- }
217
379
  function requireFieldCount(section, line, count, allowed) {
218
380
  const allowedCounts = Array.isArray(allowed) ? allowed : [allowed];
219
381
  if (!allowedCounts.includes(count)) {
@@ -457,8 +619,7 @@ var Pulseq = (() => {
457
619
  }
458
620
  function parseExtensions(seq, valid) {
459
621
  const vc = ver(seq);
460
- _unknownLabelCounter = 0;
461
- _unknownLabels.clear();
622
+ resetUnknownLabels();
462
623
  let i = 0;
463
624
  while (i < valid.length) {
464
625
  const line = valid[i].trim();
@@ -518,26 +679,6 @@ var Pulseq = (() => {
518
679
  }
519
680
  }
520
681
  }
521
- function extensionNameToType(name) {
522
- switch (name.toUpperCase()) {
523
- case "TRIGGERS":
524
- return 1 /* EXT_TRIGGER */;
525
- case "ROTATIONS":
526
- return 2 /* EXT_ROTATION */;
527
- case "LABELSET":
528
- return 3 /* EXT_LABELSET */;
529
- case "LABELINC":
530
- return 4 /* EXT_LABELINC */;
531
- case "DELAYS":
532
- return 5 /* EXT_DELAY */;
533
- case "RF_SHIMS":
534
- return 6 /* EXT_RF_SHIM */;
535
- case "NCO":
536
- return 100 /* EXT_NCO */;
537
- default:
538
- return 999 /* EXT_UNKNOWN */;
539
- }
540
- }
541
682
  function parseTriggerSpecs(seq, lines) {
542
683
  for (const line of lines) {
543
684
  const p = splitFields(line);
@@ -593,42 +734,6 @@ var Pulseq = (() => {
593
734
  }
594
735
  }
595
736
  }
596
- var KNOWN_LABELS = {
597
- "SLC": { labelId: 0, flagId: 0 },
598
- "SEG": { labelId: 1, flagId: 0 },
599
- "REP": { labelId: 2, flagId: 0 },
600
- "AVG": { labelId: 3, flagId: 0 },
601
- "ECO": { labelId: 4, flagId: 0 },
602
- "PHS": { labelId: 5, flagId: 0 },
603
- "SET": { labelId: 6, flagId: 0 },
604
- "ACQ": { labelId: 7, flagId: 0 },
605
- "LIN": { labelId: 8, flagId: 0 },
606
- "PAR": { labelId: 9, flagId: 0 },
607
- "ONCE": { labelId: 10, flagId: 0 },
608
- "NAV": { labelId: 0, flagId: 1 },
609
- "REV": { labelId: 0, flagId: 2 },
610
- "SMS": { labelId: 0, flagId: 4 },
611
- "REF": { labelId: 0, flagId: 8 },
612
- "IMA": { labelId: 0, flagId: 16 },
613
- "OFF": { labelId: 0, flagId: 32 },
614
- "NOISE": { labelId: 0, flagId: 64 },
615
- "PMC": { labelId: 0, flagId: 128 },
616
- "NOPOS": { labelId: 0, flagId: 256 },
617
- "NOROT": { labelId: 0, flagId: 512 },
618
- "NOSCL": { labelId: 0, flagId: 1024 }
619
- };
620
- var _unknownLabelCounter = 0;
621
- var _unknownLabels = /* @__PURE__ */ new Map();
622
- function decodeLabel(name) {
623
- const known = KNOWN_LABELS[name];
624
- if (known) return known;
625
- let id = _unknownLabels.get(name);
626
- if (id === void 0) {
627
- id = 1e3 + _unknownLabelCounter++;
628
- _unknownLabels.set(name, id);
629
- }
630
- return { labelId: id, flagId: 0 };
631
- }
632
737
  function parseLabelSpecs(seq, lines, isSet) {
633
738
  for (const line of lines) {
634
739
  const p = splitFields(line);
@@ -739,90 +844,563 @@ var Pulseq = (() => {
739
844
  this.rawCount = 0;
740
845
  }
741
846
  };
742
- function extractRasterTimes(seq) {
743
- const set = (key, field) => {
744
- const v = seq.definitions.get(key);
745
- if (v?.length) seq.rasterTimes[field] = v[0];
746
- };
747
- set("BlockDurationRaster", "blockDurationRaster");
748
- set("GradientRasterTime", "gradientRaster");
749
- set("RadiofrequencyRasterTime", "rfRaster");
750
- set("AdcRasterTime", "adcRaster");
847
+
848
+ // src/pulseq/binaryReader.ts
849
+ var PULSEQ_BINARY_VERSION = Object.freeze({ major: 1, minor: 5, revision: 2 });
850
+ var MAGIC = new Uint8Array([1, 112, 117, 108, 115, 101, 113, 2]);
851
+ var SECTION_PREFIX = 0xffffffff00000000n;
852
+ var SECTION = Object.freeze({
853
+ definitions: SECTION_PREFIX | 1n,
854
+ blocks: SECTION_PREFIX | 2n,
855
+ rf: SECTION_PREFIX | 3n,
856
+ gradients: SECTION_PREFIX | 4n,
857
+ trapezoids: SECTION_PREFIX | 5n,
858
+ adc: SECTION_PREFIX | 6n,
859
+ legacyDelays: SECTION_PREFIX | 7n,
860
+ shapes: SECTION_PREFIX | 8n,
861
+ extensions: SECTION_PREFIX | 9n,
862
+ triggers: SECTION_PREFIX | 10n,
863
+ labelSet: SECTION_PREFIX | 11n,
864
+ labelInc: SECTION_PREFIX | 12n,
865
+ softDelays: SECTION_PREFIX | 13n,
866
+ rfShims: SECTION_PREFIX | 14n,
867
+ rotations: SECTION_PREFIX | 15n,
868
+ signature: SECTION_PREFIX | 0x00ffffffn
869
+ });
870
+ var MAX_RECORDS = 1e8;
871
+ var MAX_STRING_BYTES = 16 * 1024 * 1024;
872
+ var MAX_SHAPE_SAMPLES = 1e8;
873
+ var BINARY_LABELS = Object.freeze([
874
+ "SLC",
875
+ "SEG",
876
+ "REP",
877
+ "AVG",
878
+ "SET",
879
+ "ECO",
880
+ "PHS",
881
+ "LIN",
882
+ "PAR",
883
+ "ACQ",
884
+ "TRID",
885
+ "NAV",
886
+ "REV",
887
+ "SMS",
888
+ "REF",
889
+ "IMA",
890
+ "OFF",
891
+ "NOISE",
892
+ "PMC",
893
+ "NOROT",
894
+ "NOPOS",
895
+ "NOSCL",
896
+ "ONCE"
897
+ ]);
898
+ function hasPulseqBinaryMagic(bytes) {
899
+ if (bytes.byteLength < MAGIC.byteLength) return false;
900
+ for (let i = 0; i < MAGIC.byteLength; i++) {
901
+ if (bytes[i] !== MAGIC[i]) return false;
902
+ }
903
+ return true;
904
+ }
905
+ function parseSequenceBinary(bytes) {
906
+ const reader = new BinaryReader(bytes);
907
+ const magic = reader.bytes(MAGIC.byteLength, "file header");
908
+ if (!hasPulseqBinaryMagic(magic)) {
909
+ reader.fail("not a Pulseq binary file", 0);
910
+ }
911
+ const seq = createEmptySequence();
912
+ resetUnknownLabels();
913
+ seq.version.major = reader.safeInt64("version major");
914
+ seq.version.minor = reader.safeInt64("version minor");
915
+ seq.version.revision = reader.safeInt64("version revision");
916
+ seq.versionCombined = makeVersionCombined(
917
+ seq.version.major,
918
+ seq.version.minor,
919
+ seq.version.revision
920
+ );
921
+ assertSupportedVersion(seq, reader);
922
+ const seenSections = /* @__PURE__ */ new Set(["VERSION"]);
923
+ while (!reader.eof()) {
924
+ const sectionOffset = reader.position;
925
+ const section = reader.uint64("section code");
926
+ switch (section) {
927
+ case SECTION.definitions:
928
+ readDefinitions(reader, seq);
929
+ seenSections.add("DEFINITIONS");
930
+ break;
931
+ case SECTION.blocks:
932
+ readBlocks(reader, seq);
933
+ seenSections.add("BLOCKS");
934
+ break;
935
+ case SECTION.rf:
936
+ readRf(reader, seq);
937
+ seenSections.add("RF");
938
+ break;
939
+ case SECTION.gradients:
940
+ readGradients(reader, seq);
941
+ seenSections.add("GRADIENTS");
942
+ break;
943
+ case SECTION.trapezoids:
944
+ readTrapezoids(reader, seq);
945
+ seenSections.add("TRAP");
946
+ break;
947
+ case SECTION.adc:
948
+ readAdc(reader, seq);
949
+ seenSections.add("ADC");
950
+ break;
951
+ case SECTION.legacyDelays:
952
+ readLegacyDelays(reader);
953
+ break;
954
+ case SECTION.shapes:
955
+ readShapes(reader, seq);
956
+ seenSections.add("SHAPES");
957
+ break;
958
+ case SECTION.extensions:
959
+ readExtensions(reader, seq);
960
+ seenSections.add("EXTENSIONS");
961
+ break;
962
+ case SECTION.triggers:
963
+ readTriggers(reader, seq);
964
+ break;
965
+ case SECTION.labelSet:
966
+ readLabels(reader, seq, true);
967
+ break;
968
+ case SECTION.labelInc:
969
+ readLabels(reader, seq, false);
970
+ break;
971
+ case SECTION.softDelays:
972
+ readSoftDelays(reader, seq);
973
+ break;
974
+ case SECTION.rfShims:
975
+ readRfShims(reader, seq);
976
+ break;
977
+ case SECTION.rotations:
978
+ readRotations(reader, seq);
979
+ break;
980
+ case SECTION.signature:
981
+ readSignature(reader, seq, sectionOffset);
982
+ break;
983
+ default:
984
+ reader.fail(`unknown section code 0x${section.toString(16)}`, sectionOffset);
985
+ }
986
+ }
987
+ extractRasterTimes(seq);
988
+ validateSequence(seq, seenSections);
989
+ return seq;
990
+ }
991
+ function assertSupportedVersion(seq, reader) {
992
+ const expected = PULSEQ_BINARY_VERSION;
993
+ if (seq.version.major !== expected.major || seq.version.minor !== expected.minor || seq.version.revision !== expected.revision) {
994
+ reader.fail(
995
+ `unsupported Pulseq binary version ${seq.version.major}.${seq.version.minor}.${seq.version.revision}; expected ${expected.major}.${expected.minor}.${expected.revision}`,
996
+ MAGIC.byteLength
997
+ );
998
+ }
999
+ }
1000
+ function readDefinitions(reader, seq) {
1001
+ const count = reader.count64("DEFINITIONS count", 9);
1002
+ for (let i = 0; i < count; i++) {
1003
+ const keyLength = reader.length32("DEFINITIONS key length");
1004
+ const key = reader.string(keyLength, "DEFINITIONS key");
1005
+ const valueCount = reader.length32("DEFINITIONS value count", MAX_RECORDS);
1006
+ const valueType = reader.char("DEFINITIONS value type");
1007
+ if (valueType === "f") {
1008
+ reader.requireArray(valueCount, 8, "DEFINITIONS float values");
1009
+ const values = new Array(valueCount);
1010
+ for (let j = 0; j < valueCount; j++) values[j] = reader.float64("DEFINITIONS float value");
1011
+ seq.definitions.set(key, values);
1012
+ seq.definitionsRaw.set(key, values.join(" "));
1013
+ } else if (valueType === "i") {
1014
+ reader.requireArray(valueCount, 4, "DEFINITIONS integer values");
1015
+ const values = new Array(valueCount);
1016
+ for (let j = 0; j < valueCount; j++) values[j] = reader.int32("DEFINITIONS integer value");
1017
+ seq.definitions.set(key, values);
1018
+ seq.definitionsRaw.set(key, values.join(" "));
1019
+ } else if (valueType === "c") {
1020
+ const raw = reader.string(valueCount, "DEFINITIONS character value");
1021
+ const value = raw.endsWith("\0") ? raw.slice(0, -1) : raw;
1022
+ seq.definitions.set(key, []);
1023
+ seq.definitionsRaw.set(key, value);
1024
+ } else {
1025
+ reader.fail(`unknown definition value type '${valueType}'`);
1026
+ }
1027
+ }
1028
+ }
1029
+ function readBlocks(reader, seq) {
1030
+ const count = reader.count64("BLOCKS count", 32);
1031
+ seq.blocks.length = 0;
1032
+ for (let i = 0; i < count; i++) {
1033
+ seq.blocks.push({
1034
+ num: i + 1,
1035
+ dur: reader.nonNegativeSafeInt64("BLOCKS duration"),
1036
+ rfId: reader.int32("BLOCKS RF id"),
1037
+ gxId: reader.int32("BLOCKS Gx id"),
1038
+ gyId: reader.int32("BLOCKS Gy id"),
1039
+ gzId: reader.int32("BLOCKS Gz id"),
1040
+ adcId: reader.int32("BLOCKS ADC id"),
1041
+ extId: reader.int32("BLOCKS extension id")
1042
+ });
1043
+ }
1044
+ }
1045
+ function readRf(reader, seq) {
1046
+ const count = reader.count64("RF count", 73);
1047
+ seq.rfs.clear();
1048
+ for (let i = 0; i < count; i++) {
1049
+ const id = reader.int32("RF id");
1050
+ const amplitude = reader.float64("RF amplitude");
1051
+ const magShapeId = reader.int32("RF magnitude shape id");
1052
+ const phaseShapeId = reader.int32("RF phase shape id");
1053
+ const timeShapeId = reader.int32("RF time shape id");
1054
+ const center = psToUs(reader.safeInt64("RF center"));
1055
+ const delay = psToUsRounded(reader.safeInt64("RF delay"));
1056
+ const freqPPM = reader.float64("RF frequency ppm");
1057
+ const phasePPM = reader.float64("RF phase ppm");
1058
+ const freqOffset = reader.float64("RF frequency offset");
1059
+ const phaseOffset = reader.float64("RF phase offset");
1060
+ const use = reader.char("RF use").toLowerCase();
1061
+ if (!/^[erisu]$/.test(use)) reader.fail(`invalid RF use flag '${use}'`);
1062
+ seq.rfs.set(id, {
1063
+ id,
1064
+ amplitude,
1065
+ magShapeId,
1066
+ phaseShapeId,
1067
+ timeShapeId,
1068
+ center,
1069
+ delay,
1070
+ freqPPM,
1071
+ phasePPM,
1072
+ freqOffset,
1073
+ phaseOffset,
1074
+ phaseModShapeId: 0,
1075
+ use
1076
+ });
1077
+ }
1078
+ }
1079
+ function readGradients(reader, seq) {
1080
+ const count = reader.count64("GRADIENTS count", 44);
1081
+ for (let i = 0; i < count; i++) {
1082
+ const id = reader.int32("GRADIENTS id");
1083
+ seq.arbitraryGrads.set(id, {
1084
+ id,
1085
+ amplitude: reader.float64("GRADIENTS amplitude"),
1086
+ first: reader.float64("GRADIENTS first"),
1087
+ last: reader.float64("GRADIENTS last"),
1088
+ shapeId: reader.int32("GRADIENTS shape id"),
1089
+ timeId: reader.int32("GRADIENTS time shape id"),
1090
+ delay: psToUsRounded(reader.safeInt64("GRADIENTS delay"))
1091
+ });
1092
+ }
1093
+ }
1094
+ function readTrapezoids(reader, seq) {
1095
+ const count = reader.count64("TRAP count", 44);
1096
+ for (let i = 0; i < count; i++) {
1097
+ const id = reader.int32("TRAP id");
1098
+ seq.trapGrads.set(id, {
1099
+ id,
1100
+ amplitude: reader.float64("TRAP amplitude"),
1101
+ rise: psToUsRounded(reader.safeInt64("TRAP rise")),
1102
+ flat: psToUsRounded(reader.safeInt64("TRAP flat")),
1103
+ fall: psToUsRounded(reader.safeInt64("TRAP fall")),
1104
+ delay: psToUsRounded(reader.safeInt64("TRAP delay"))
1105
+ });
1106
+ }
1107
+ }
1108
+ function readAdc(reader, seq) {
1109
+ const count = reader.count64("ADC count", 64);
1110
+ seq.adcs.clear();
1111
+ for (let i = 0; i < count; i++) {
1112
+ const id = reader.int32("ADC id");
1113
+ seq.adcs.set(id, {
1114
+ id,
1115
+ numSamples: reader.nonNegativeSafeInt64("ADC sample count"),
1116
+ dwell: psToNsRounded(reader.safeInt64("ADC dwell")),
1117
+ delay: psToUsRounded(reader.safeInt64("ADC delay")),
1118
+ freqPPM: reader.float64("ADC frequency ppm"),
1119
+ phasePPM: reader.float64("ADC phase ppm"),
1120
+ freqOffset: reader.float64("ADC frequency offset"),
1121
+ phaseOffset: reader.float64("ADC phase offset"),
1122
+ deadTime: 0,
1123
+ discardPre: 0,
1124
+ discardPost: 0,
1125
+ phaseModShapeId: reader.int32("ADC phase shape id")
1126
+ });
1127
+ }
1128
+ }
1129
+ function readLegacyDelays(reader) {
1130
+ const count = reader.count64("legacy DELAYS count", 12);
1131
+ for (let i = 0; i < count; i++) {
1132
+ reader.int32("legacy DELAYS id");
1133
+ reader.safeInt64("legacy DELAYS duration");
1134
+ }
1135
+ }
1136
+ function readShapes(reader, seq) {
1137
+ const count = reader.count64("SHAPES count", 20);
1138
+ seq.shapes.clear();
1139
+ for (let i = 0; i < count; i++) {
1140
+ const id = reader.int32("SHAPES id");
1141
+ const numSamples = reader.positiveSafeInt64("SHAPES uncompressed count", MAX_SHAPE_SAMPLES);
1142
+ const packedCount = reader.positiveSafeInt64("SHAPES compressed count", MAX_SHAPE_SAMPLES);
1143
+ reader.requireArray(packedCount, 4, "SHAPES compressed data");
1144
+ const packed = new Float64Array(packedCount);
1145
+ for (let j = 0; j < packedCount; j++) packed[j] = reader.float32("SHAPES sample");
1146
+ seq.shapes.set(id, { numSamples, samples: decompressShape(packed, numSamples) });
1147
+ }
1148
+ }
1149
+ function readExtensions(reader, seq) {
1150
+ const count = reader.count64("EXTENSIONS count", 16);
1151
+ seq.extensions.clear();
1152
+ for (let i = 0; i < count; i++) {
1153
+ const id = reader.int32("EXTENSIONS id");
1154
+ seq.extensions.set(id, {
1155
+ id,
1156
+ type: reader.int32("EXTENSIONS type"),
1157
+ ref: reader.int32("EXTENSIONS reference"),
1158
+ nextId: reader.int32("EXTENSIONS next id")
1159
+ });
1160
+ }
1161
+ }
1162
+ function registerExtension(seq, id, name) {
1163
+ seq.extensionNames.set(id, name);
1164
+ seq.extensionTypes.set(id, extensionNameToType(name));
1165
+ }
1166
+ function readTriggers(reader, seq) {
1167
+ const extensionId = reader.int32("TRIGGERS extension type id");
1168
+ registerExtension(seq, extensionId, "TRIGGERS");
1169
+ const count = reader.count64("TRIGGERS count", 28);
1170
+ seq.triggers.length = 0;
1171
+ for (let i = 0; i < count; i++) {
1172
+ seq.triggers.push({
1173
+ id: reader.int32("TRIGGERS id"),
1174
+ triggerType: reader.int32("TRIGGERS type"),
1175
+ channel: reader.int32("TRIGGERS channel"),
1176
+ delay: psToUsRounded(reader.safeInt64("TRIGGERS delay")),
1177
+ duration: psToUsRounded(reader.safeInt64("TRIGGERS duration"))
1178
+ });
1179
+ }
1180
+ }
1181
+ function readLabels(reader, seq, isSet) {
1182
+ const section = isSet ? "LABELSET" : "LABELINC";
1183
+ const extensionId = reader.int32(`${section} extension type id`);
1184
+ registerExtension(seq, extensionId, section);
1185
+ const count = reader.count64(`${section} count`, 12);
1186
+ const library = isSet ? seq.labelSets : seq.labelIncs;
1187
+ library.length = 0;
1188
+ for (let i = 0; i < count; i++) {
1189
+ const id = reader.int32(`${section} id`);
1190
+ const value = reader.int32(`${section} value`);
1191
+ const labelIndex = reader.int32(`${section} label index`);
1192
+ if (labelIndex < 1 || labelIndex > BINARY_LABELS.length) {
1193
+ reader.fail(`invalid binary label index ${labelIndex}`);
1194
+ }
1195
+ const { labelId, flagId } = decodeLabel(BINARY_LABELS[labelIndex - 1]);
1196
+ const spec = { id, value, labelId, flagId };
1197
+ library.push(spec);
1198
+ }
1199
+ }
1200
+ function readSoftDelays(reader, seq) {
1201
+ const extensionId = reader.int32("DELAYS extension type id");
1202
+ registerExtension(seq, extensionId, "DELAYS");
1203
+ const count = reader.count64("DELAYS count", 28);
1204
+ seq.softDelays.length = 0;
1205
+ for (let i = 0; i < count; i++) {
1206
+ const id = reader.int32("DELAYS id");
1207
+ const numId = reader.int32("DELAYS numeric id");
1208
+ const offset = psToUsRounded(reader.safeInt64("DELAYS offset"));
1209
+ const factor = reader.float64("DELAYS factor");
1210
+ const hintLength = reader.length32("DELAYS hint length");
1211
+ seq.softDelays.push({ id, numId, offset, factor, hint: reader.string(hintLength, "DELAYS hint") });
1212
+ }
1213
+ }
1214
+ function readRfShims(reader, seq) {
1215
+ const extensionId = reader.int32("RF_SHIMS extension type id");
1216
+ registerExtension(seq, extensionId, "RF_SHIMS");
1217
+ const count = reader.count64("RF_SHIMS count", 8);
1218
+ seq.rfShims.length = 0;
1219
+ for (let i = 0; i < count; i++) {
1220
+ const id = reader.int32("RF_SHIMS id");
1221
+ const nChannels = reader.length32("RF_SHIMS channel count", MAX_RECORDS / 2);
1222
+ reader.requireArray(nChannels * 2, 8, "RF_SHIMS channel data");
1223
+ const amplitudes = new Array(nChannels);
1224
+ const phases = new Array(nChannels);
1225
+ for (let channel = 0; channel < nChannels; channel++) {
1226
+ amplitudes[channel] = reader.float64("RF_SHIMS magnitude");
1227
+ phases[channel] = reader.float64("RF_SHIMS phase");
1228
+ }
1229
+ seq.rfShims.push({ id, nChannels, amplitudes, phases });
1230
+ }
1231
+ }
1232
+ function readRotations(reader, seq) {
1233
+ const extensionId = reader.int32("ROTATIONS extension type id");
1234
+ registerExtension(seq, extensionId, "ROTATIONS");
1235
+ const count = reader.count64("ROTATIONS count", 36);
1236
+ seq.rotations.length = 0;
1237
+ for (let i = 0; i < count; i++) {
1238
+ const id = reader.int32("ROTATIONS id");
1239
+ const values = [
1240
+ reader.float64("ROTATIONS q0"),
1241
+ reader.float64("ROTATIONS qx"),
1242
+ reader.float64("ROTATIONS qy"),
1243
+ reader.float64("ROTATIONS qz")
1244
+ ];
1245
+ const norm = Math.hypot(...values);
1246
+ if (!Number.isFinite(norm) || norm <= 0) reader.fail("invalid zero or non-finite rotation quaternion");
1247
+ seq.rotations.push({ id, values: values.map((value) => value / norm) });
1248
+ }
1249
+ }
1250
+ function readSignature(reader, seq, sectionOffset) {
1251
+ const typeLength = reader.length32("SIGNATURE type length");
1252
+ const type = reader.string(typeLength, "SIGNATURE type");
1253
+ const hashLength = reader.length32("SIGNATURE hash length");
1254
+ const hashBytes = reader.bytes(hashLength, "SIGNATURE hash");
1255
+ const originalSize = reader.nonNegativeSafeInt64("SIGNATURE original size");
1256
+ if (originalSize !== sectionOffset) {
1257
+ reader.fail(`SIGNATURE original size ${originalSize} does not match section offset ${sectionOffset}`);
1258
+ }
1259
+ let hash = "";
1260
+ for (const byte of hashBytes) hash += byte.toString(16).padStart(2, "0");
1261
+ seq.binarySignature = { type, hash, originalSize };
751
1262
  }
752
- function validateSequence(seq, seenSections) {
753
- if (!seenSections.has("VERSION")) parseError("Required [VERSION] section is missing");
754
- if (seq.version.major !== 1 || seq.version.minor > 5) {
755
- parseError(`Unsupported Pulseq version ${seq.version.major}.${seq.version.minor}.${seq.version.revision}`);
1263
+ function psToUs(value) {
1264
+ return value / 1e6;
1265
+ }
1266
+ function psToUsRounded(value) {
1267
+ return value >= 0 ? Math.floor((value + 5e5) / 1e6) : Math.ceil((value - 5e5) / 1e6);
1268
+ }
1269
+ function psToNsRounded(value) {
1270
+ return value >= 0 ? Math.floor((value + 500) / 1e3) : Math.ceil((value - 500) / 1e3);
1271
+ }
1272
+ var BinaryReader = class {
1273
+ constructor(source) {
1274
+ __publicField(this, "source", source);
1275
+ __publicField(this, "view");
1276
+ __publicField(this, "offset", 0);
1277
+ this.view = new DataView(source.buffer, source.byteOffset, source.byteLength);
756
1278
  }
757
- const vc = ver(seq);
758
- if (vc >= VER_PRE_14) {
759
- requireNumericDefinition(seq, "AdcRasterTime");
760
- requireNumericDefinition(seq, "GradientRasterTime");
761
- requireNumericDefinition(seq, "RadiofrequencyRasterTime");
762
- requireNumericDefinition(seq, "BlockDurationRaster");
1279
+ get position() {
1280
+ return this.offset;
763
1281
  }
764
- if (vc >= VER_V15001) {
765
- const required = seq.definitionsRaw.get("RequiredExtensions")?.split(/\s+/).filter(Boolean) ?? [];
766
- for (const name of required) {
767
- if (extensionNameToType(name) === 999 /* EXT_UNKNOWN */) {
768
- parseError(`Unknown required extension '${name}'`);
769
- }
770
- }
1282
+ get remaining() {
1283
+ return this.view.byteLength - this.offset;
771
1284
  }
772
- if (!seenSections.has("BLOCKS")) parseError("Required [BLOCKS] section is missing");
773
- for (const block of seq.blocks) {
774
- if (block.rfId > 0 && !seq.rfs.has(block.rfId)) {
775
- parseError(`Block ${block.num} references undefined RF event ${block.rfId}`);
1285
+ eof() {
1286
+ return this.remaining === 0;
1287
+ }
1288
+ requireArray(count, width, context) {
1289
+ if (!Number.isSafeInteger(count) || count < 0 || count > MAX_RECORDS) {
1290
+ this.fail(`${context} has invalid count ${count}`);
776
1291
  }
777
- for (const [channel, gradId] of [["GX", block.gxId], ["GY", block.gyId], ["GZ", block.gzId]]) {
778
- if (gradId > 0 && !seq.arbitraryGrads.has(gradId) && !seq.trapGrads.has(gradId)) {
779
- parseError(`Block ${block.num} references undefined ${channel} gradient event ${gradId}`);
780
- }
1292
+ if (count > Math.floor(this.remaining / width)) {
1293
+ this.fail(`${context} exceeds remaining file data`);
781
1294
  }
782
- if (block.adcId > 0 && !seq.adcs.has(block.adcId)) {
783
- parseError(`Block ${block.num} references undefined ADC event ${block.adcId}`);
1295
+ }
1296
+ count64(context, minimumBytesPerEntry) {
1297
+ const count = this.nonNegativeSafeInt64(context);
1298
+ if (count > MAX_RECORDS) this.fail(`${context} exceeds limit ${MAX_RECORDS}`);
1299
+ if (minimumBytesPerEntry > 0 && count > Math.floor(this.remaining / minimumBytesPerEntry)) {
1300
+ this.fail(`${context} exceeds remaining file data`);
784
1301
  }
785
- if (block.extId > 0 && !seq.extensions.has(block.extId)) {
786
- parseError(`Block ${block.num} references undefined extension list ${block.extId}`);
1302
+ return count;
1303
+ }
1304
+ length32(context, limit = MAX_STRING_BYTES) {
1305
+ const value = this.int32(context);
1306
+ if (value < 0 || value > limit) this.fail(`${context} has invalid value ${value}`);
1307
+ if (value > this.remaining) this.fail(`${context} exceeds remaining file data`);
1308
+ return value;
1309
+ }
1310
+ positiveSafeInt64(context, limit) {
1311
+ const value = this.safeInt64(context);
1312
+ if (value <= 0 || value > limit) this.fail(`${context} has invalid value ${value}`);
1313
+ return value;
1314
+ }
1315
+ nonNegativeSafeInt64(context) {
1316
+ const value = this.safeInt64(context);
1317
+ if (value < 0) this.fail(`${context} must be non-negative`);
1318
+ return value;
1319
+ }
1320
+ safeInt64(context) {
1321
+ const value = this.int64(context);
1322
+ if (value > BigInt(Number.MAX_SAFE_INTEGER) || value < BigInt(Number.MIN_SAFE_INTEGER)) {
1323
+ this.fail(`${context} exceeds JavaScript safe integer range`);
787
1324
  }
1325
+ return Number(value);
1326
+ }
1327
+ int64(context) {
1328
+ this.require(8, context);
1329
+ const value = this.view.getBigInt64(this.offset, true);
1330
+ this.offset += 8;
1331
+ return value;
1332
+ }
1333
+ uint64(context) {
1334
+ this.require(8, context);
1335
+ const value = this.view.getBigUint64(this.offset, true);
1336
+ this.offset += 8;
1337
+ return value;
1338
+ }
1339
+ int32(context) {
1340
+ this.require(4, context);
1341
+ const value = this.view.getInt32(this.offset, true);
1342
+ this.offset += 4;
1343
+ return value;
1344
+ }
1345
+ float64(context) {
1346
+ this.require(8, context);
1347
+ const value = this.view.getFloat64(this.offset, true);
1348
+ this.offset += 8;
1349
+ if (!Number.isFinite(value)) this.fail(`${context} is not finite`, this.offset - 8);
1350
+ return value;
1351
+ }
1352
+ float32(context) {
1353
+ this.require(4, context);
1354
+ const value = this.view.getFloat32(this.offset, true);
1355
+ this.offset += 4;
1356
+ if (!Number.isFinite(value)) this.fail(`${context} is not finite`, this.offset - 4);
1357
+ return value;
1358
+ }
1359
+ char(context) {
1360
+ return this.string(1, context);
1361
+ }
1362
+ string(length, context) {
1363
+ const data = this.bytes(length, context);
1364
+ let result = "";
1365
+ const chunkSize = 8192;
1366
+ for (let start = 0; start < data.length; start += chunkSize) {
1367
+ const end = Math.min(data.length, start + chunkSize);
1368
+ result += String.fromCharCode(...data.subarray(start, end));
1369
+ }
1370
+ return result;
788
1371
  }
789
- for (const ext of seq.extensions.values()) {
790
- if (ext.nextId > 0 && !seq.extensions.has(ext.nextId)) {
791
- parseError(`Extension list ${ext.id} references undefined next extension ${ext.nextId}`);
1372
+ bytes(length, context) {
1373
+ if (!Number.isSafeInteger(length) || length < 0 || length > MAX_STRING_BYTES) {
1374
+ this.fail(`${context} has invalid byte length ${length}`);
792
1375
  }
793
- const type = seq.extensionTypes.get(ext.type) ?? 999 /* EXT_UNKNOWN */;
794
- if (type === 999 /* EXT_UNKNOWN */) continue;
795
- if (!extensionPayloadExists(seq, type, ext.ref)) {
796
- const name = seq.extensionNames.get(ext.type) ?? `type ${ext.type}`;
797
- parseError(`Extension list ${ext.id} references undefined ${name} payload ${ext.ref}`);
1376
+ this.require(length, context);
1377
+ const result = this.source.subarray(this.offset, this.offset + length);
1378
+ this.offset += length;
1379
+ return result;
1380
+ }
1381
+ fail(message, offset = this.offset) {
1382
+ throw new Error(`Pulseq binary parse error at byte ${offset}: ${message}`);
1383
+ }
1384
+ require(length, context) {
1385
+ if (length < 0 || length > this.remaining) {
1386
+ this.fail(`unexpected end of file while reading ${context}`);
798
1387
  }
799
1388
  }
800
- }
801
- function requireNumericDefinition(seq, name) {
802
- const value = seq.definitions.get(name);
803
- if (!value || value.length === 0 || !Number.isFinite(value[0])) {
804
- parseError(`Required definition ${name} is not present in the file`);
1389
+ };
1390
+
1391
+ // src/pulseq/sequenceReader.ts
1392
+ function parseSequenceBytes(bytes, fileName = "") {
1393
+ if (hasPulseqBinaryMagic(bytes)) return parseSequenceBinary(bytes);
1394
+ if (/\.bseq$/i.test(fileName)) {
1395
+ throw new Error("Pulseq binary parse error: .bseq file is missing the Pulseq binary header");
805
1396
  }
806
- }
807
- function extensionPayloadExists(seq, type, ref) {
808
- switch (type) {
809
- case 1 /* EXT_TRIGGER */:
810
- return seq.triggers.some((v) => v.id === ref);
811
- case 2 /* EXT_ROTATION */:
812
- return seq.rotations.some((v) => v.id === ref);
813
- case 3 /* EXT_LABELSET */:
814
- return seq.labelSets.some((v) => v.id === ref);
815
- case 4 /* EXT_LABELINC */:
816
- return seq.labelIncs.some((v) => v.id === ref);
817
- case 5 /* EXT_DELAY */:
818
- return seq.softDelays.some((v) => v.id === ref);
819
- case 6 /* EXT_RF_SHIM */:
820
- return seq.rfShims.some((v) => v.id === ref);
821
- case 100 /* EXT_NCO */:
822
- return seq.ncos.some((v) => v.id === ref);
823
- default:
824
- return false;
1397
+ let text;
1398
+ try {
1399
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
1400
+ } catch {
1401
+ throw new Error("Pulseq parse error: sequence text is not valid UTF-8");
825
1402
  }
1403
+ return parseSequenceText(text);
826
1404
  }
827
1405
 
828
1406
  // src/pulseq/decoder.ts
@@ -1216,6 +1794,11 @@ var Pulseq = (() => {
1216
1794
  for (const b of blocks) {
1217
1795
  if (b.adc) totalAdcSamples += b.adc.numSamples;
1218
1796
  }
1797
+ if (_options?.maxAdcSamples && totalAdcSamples > _options.maxAdcSamples) return null;
1798
+ if (_options?.maxGridPoints && totalDuration > 0) {
1799
+ const rasterPointCount = Math.max(2, Math.round(totalDuration / GR) + 1);
1800
+ if (rasterPointCount + totalAdcSamples > _options.maxGridPoints) return null;
1801
+ }
1219
1802
  const adcT = new Float64Array(totalAdcSamples);
1220
1803
  let adcIdx = 0;
1221
1804
  for (const b of blocks) {
@@ -1447,8 +2030,140 @@ var Pulseq = (() => {
1447
2030
  return [gx, gy, gz];
1448
2031
  }
1449
2032
 
1450
- // src/pulseq/m1.ts
2033
+ // src/pulseq/boundedSeries.ts
2034
+ var BoundedSeriesBuilder = class {
2035
+ constructor(startSec, endSec, maxPoints) {
2036
+ __publicField(this, "startSec", startSec);
2037
+ __publicField(this, "endSec", endSec);
2038
+ __publicField(this, "buckets");
2039
+ __publicField(this, "bucketCount");
2040
+ __publicField(this, "span");
2041
+ this.bucketCount = Math.max(1, Math.floor(Math.max(4, maxPoints) / 4));
2042
+ this.buckets = new Array(this.bucketCount);
2043
+ this.span = Math.max(0, endSec - startSec);
2044
+ }
2045
+ add(tSec, value) {
2046
+ if (!Number.isFinite(tSec) || !Number.isFinite(value)) return;
2047
+ const normalized = this.span > 0 ? (tSec - this.startSec) / this.span : 0;
2048
+ const index = Math.max(0, Math.min(
2049
+ this.bucketCount - 1,
2050
+ Math.floor(normalized * this.bucketCount)
2051
+ ));
2052
+ const bucket = this.buckets[index];
2053
+ if (!bucket) {
2054
+ this.buckets[index] = {
2055
+ firstT: tSec,
2056
+ firstV: value,
2057
+ minV: value,
2058
+ maxV: value,
2059
+ lastT: tSec,
2060
+ lastV: value
2061
+ };
2062
+ return;
2063
+ }
2064
+ if (value < bucket.minV) {
2065
+ bucket.minV = value;
2066
+ }
2067
+ if (value > bucket.maxV) {
2068
+ bucket.maxV = value;
2069
+ }
2070
+ bucket.lastT = tSec;
2071
+ bucket.lastV = value;
2072
+ }
2073
+ finish() {
2074
+ const startTime = [];
2075
+ const endTime = [];
2076
+ const min = [];
2077
+ const max = [];
2078
+ const first = [];
2079
+ const last = [];
2080
+ for (const bucket of this.buckets) {
2081
+ if (!bucket) continue;
2082
+ startTime.push(bucket.firstT);
2083
+ endTime.push(bucket.lastT);
2084
+ min.push(bucket.minV);
2085
+ max.push(bucket.maxV);
2086
+ first.push(bucket.firstV);
2087
+ last.push(bucket.lastV);
2088
+ }
2089
+ return {
2090
+ startTime: new Float64Array(startTime),
2091
+ endTime: new Float64Array(endTime),
2092
+ min: new Float64Array(min),
2093
+ max: new Float64Array(max),
2094
+ first: new Float64Array(first),
2095
+ last: new Float64Array(last)
2096
+ };
2097
+ }
2098
+ };
2099
+
2100
+ // src/pulseq/gradientSampler.ts
1451
2101
  var TIME_EPS = 1e-15;
2102
+ function decodedGradientTimeRange(blocks) {
2103
+ let first = Number.POSITIVE_INFINITY;
2104
+ let last = Number.NEGATIVE_INFINITY;
2105
+ for (const block of blocks) {
2106
+ for (const gradient of [block.gx, block.gy, block.gz]) {
2107
+ if (!gradient?.timePoints.length) continue;
2108
+ const gradientFirst = gradient.timePoints[0];
2109
+ const gradientLast = gradient.timePoints[gradient.timePoints.length - 1];
2110
+ if (Number.isFinite(gradientFirst)) first = Math.min(first, gradientFirst);
2111
+ if (Number.isFinite(gradientLast)) last = Math.max(last, gradientLast);
2112
+ }
2113
+ }
2114
+ return Number.isFinite(first) && Number.isFinite(last) && last >= first ? { first, last } : void 0;
2115
+ }
2116
+ function createDecodedGradientSampler(blocks, channel) {
2117
+ const events = [];
2118
+ for (const block of blocks) {
2119
+ const gradient = block[channel];
2120
+ if (!gradient?.timePoints.length || !gradient.waveform.length) continue;
2121
+ const n = Math.min(gradient.timePoints.length, gradient.waveform.length);
2122
+ if (n < 1) continue;
2123
+ events.push({
2124
+ gradient,
2125
+ first: gradient.timePoints[0],
2126
+ last: gradient.timePoints[n - 1]
2127
+ });
2128
+ }
2129
+ events.sort((a, b) => a.first - b.first);
2130
+ let eventIndex = 0;
2131
+ let pointIndex = 0;
2132
+ let previousTime = Number.NEGATIVE_INFINITY;
2133
+ return (timeSec) => {
2134
+ if (timeSec < previousTime - TIME_EPS) {
2135
+ eventIndex = 0;
2136
+ pointIndex = 0;
2137
+ }
2138
+ previousTime = timeSec;
2139
+ while (eventIndex < events.length && events[eventIndex].last < timeSec - TIME_EPS) {
2140
+ eventIndex++;
2141
+ pointIndex = 0;
2142
+ }
2143
+ if (eventIndex >= events.length) return 0;
2144
+ const event = events[eventIndex];
2145
+ if (timeSec < event.first - TIME_EPS || timeSec > event.last + TIME_EPS) return 0;
2146
+ const times = event.gradient.timePoints;
2147
+ const values = event.gradient.waveform;
2148
+ const n = Math.min(times.length, values.length);
2149
+ if (Math.abs(timeSec - event.last) <= TIME_EPS && eventIndex + 1 < events.length) {
2150
+ const next = events[eventIndex + 1];
2151
+ if (Math.abs(next.first - timeSec) <= TIME_EPS && next.gradient.waveform.length) {
2152
+ return 0.5 * (values[n - 1] + next.gradient.waveform[0]);
2153
+ }
2154
+ }
2155
+ while (pointIndex + 1 < n && times[pointIndex + 1] <= timeSec + TIME_EPS) pointIndex++;
2156
+ if (pointIndex >= n - 1 || timeSec <= times[pointIndex] + TIME_EPS) return values[pointIndex];
2157
+ const t0 = times[pointIndex];
2158
+ const t1 = times[pointIndex + 1];
2159
+ if (!(t1 > t0)) return values[pointIndex];
2160
+ const alpha = (timeSec - t0) / (t1 - t0);
2161
+ return values[pointIndex] + alpha * (values[pointIndex + 1] - values[pointIndex]);
2162
+ };
2163
+ }
2164
+
2165
+ // src/pulseq/m1.ts
2166
+ var TIME_EPS2 = 1e-15;
1452
2167
  function calculateM1(blocks, gradientRaster, options = {}) {
1453
2168
  const referenceMode = normalizeReferenceMode(options.referenceMode);
1454
2169
  if (!blocks.length) {
@@ -1471,10 +2186,38 @@ var Pulseq = (() => {
1471
2186
  const excitationTimes = rfEvents.filter((rf) => rf.use === "e").map((rf) => rf.tSec);
1472
2187
  const refocusingTimes = rfEvents.filter((rf) => rf.use === "r").map((rf) => rf.tSec);
1473
2188
  const events = buildWalkerEvents(rfEvents);
2189
+ appendM1AdvisoryWarnings(rfEvents, tMin, warnings);
2190
+ const rasterSec = gradientRaster > 0 ? gradientRaster : 1e-5;
2191
+ if (rasterSec <= 0) {
2192
+ return invalidM1("gradientRaster must be positive.", referenceMode);
2193
+ }
2194
+ const samples = buildSampleTimes(tMin, tMax, rasterSec);
2195
+ const x = walkM1(gx, samples, events, excitationTimes, tMin, referenceMode);
2196
+ const y = walkM1(gy, samples, events, excitationTimes, tMin, referenceMode);
2197
+ const z = walkM1(gz, samples, events, excitationTimes, tMin, referenceMode);
2198
+ if (x.t.length !== y.t.length || x.t.length !== z.t.length) {
2199
+ warnings.push(`Internal warning: per-axis M1 output sizes disagree (${x.t.length}, ${y.t.length}, ${z.t.length}). Plot may be inconsistent.`);
2200
+ }
2201
+ return {
2202
+ valid: true,
2203
+ ok: true,
2204
+ referenceMode,
2205
+ tSec: new Float64Array(x.t),
2206
+ m1x: new Float64Array(x.m1),
2207
+ m1y: new Float64Array(y.m1),
2208
+ m1z: new Float64Array(z.m1),
2209
+ warnings,
2210
+ excitationTimesSec: new Float64Array(excitationTimes),
2211
+ refocusingTimesSec: new Float64Array(refocusingTimes)
2212
+ };
2213
+ }
2214
+ function appendM1AdvisoryWarnings(rfEvents, tMin, warnings) {
1474
2215
  let recentExcCount = 0;
1475
2216
  let lastExcT = -1e9;
2217
+ let excitationCount = 0;
1476
2218
  for (const rf of rfEvents) {
1477
2219
  if (rf.use === "e") {
2220
+ excitationCount++;
1478
2221
  if (rf.tSec - lastExcT < 0.1) recentExcCount++;
1479
2222
  lastExcT = rf.tSec;
1480
2223
  }
@@ -1484,28 +2227,122 @@ var Pulseq = (() => {
1484
2227
  `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.`
1485
2228
  );
1486
2229
  }
1487
- if (!excitationTimes.length) {
2230
+ if (!excitationCount) {
1488
2231
  warnings.push(`No excitation RF events found in sequence. M1 will be integrated from t=${tMin.toFixed(6)} s with no signal basis.`);
1489
2232
  }
1490
- const rasterSec = gradientRaster > 0 ? gradientRaster : 1e-5;
1491
- if (rasterSec <= 0) {
1492
- return invalidM1("gradientRaster must be positive.", referenceMode);
1493
- }
1494
- const samples = buildSampleTimes(tMin, tMax, rasterSec);
1495
- const x = walkM1(gx, samples, events, excitationTimes, tMin, referenceMode);
1496
- const y = walkM1(gy, samples, events, excitationTimes, tMin, referenceMode);
1497
- const z = walkM1(gz, samples, events, excitationTimes, tMin, referenceMode);
1498
- if (x.t.length !== y.t.length || x.t.length !== z.t.length) {
1499
- warnings.push(`Internal warning: per-axis M1 output sizes disagree (${x.t.length}, ${y.t.length}, ${z.t.length}). Plot may be inconsistent.`);
2233
+ }
2234
+ function calculateM1Coarse(blocks, gradientRaster, options = {}) {
2235
+ const referenceMode = normalizeReferenceMode(options.referenceMode);
2236
+ const emptySeries = () => ({
2237
+ startTime: new Float64Array(),
2238
+ endTime: new Float64Array(),
2239
+ min: new Float64Array(),
2240
+ max: new Float64Array(),
2241
+ first: new Float64Array(),
2242
+ last: new Float64Array()
2243
+ });
2244
+ const invalid = (error) => ({
2245
+ valid: false,
2246
+ ok: false,
2247
+ coarse: true,
2248
+ referenceMode,
2249
+ error,
2250
+ startSec: 0,
2251
+ endSec: 0,
2252
+ x: emptySeries(),
2253
+ y: emptySeries(),
2254
+ z: emptySeries(),
2255
+ warnings: [],
2256
+ excitationTimesSec: new Float64Array(),
2257
+ refocusingTimesSec: new Float64Array()
2258
+ });
2259
+ if (!blocks.length) return invalid("Empty or invalid block list.");
2260
+ if (!(gradientRaster > 0)) return invalid("gradientRaster must be positive.");
2261
+ const range = decodedGradientTimeRange(blocks);
2262
+ if (!range) return invalid("No gradient waveform available for M1.");
2263
+ const warnings = [];
2264
+ const rfEvents = collectRfEvents(blocks, warnings);
2265
+ appendM1AdvisoryWarnings(rfEvents, range.first, warnings);
2266
+ const excitationTimes = rfEvents.filter((rf) => rf.use === "e").map((rf) => rf.tSec);
2267
+ const refocusingTimes = rfEvents.filter((rf) => rf.use === "r").map((rf) => rf.tSec);
2268
+ const events = buildWalkerEvents(rfEvents);
2269
+ const startSec = Math.min(range.first, events[0]?.tSec ?? range.first);
2270
+ const endSec = Math.max(range.last, events[events.length - 1]?.tSec ?? range.last);
2271
+ const maxPoints = Math.max(1024, Math.min(12e4, options.maxPoints ?? 3e4));
2272
+ const maxBuckets = Math.floor(maxPoints / 4);
2273
+ const builders = [
2274
+ new BoundedSeriesBuilder(startSec, endSec, maxPoints),
2275
+ new BoundedSeriesBuilder(startSec, endSec, maxPoints),
2276
+ new BoundedSeriesBuilder(startSec, endSec, maxPoints)
2277
+ ];
2278
+ const samplers = [
2279
+ createDecodedGradientSampler(blocks, "gx"),
2280
+ createDecodedGradientSampler(blocks, "gy"),
2281
+ createDecodedGradientSampler(blocks, "gz")
2282
+ ];
2283
+ const unsignedM0 = [0, 0, 0];
2284
+ const unsignedM1 = [0, 0, 0];
2285
+ let sign = 1;
2286
+ let tReset = excitationTimes.length ? excitationTimes[0] : range.first;
2287
+ if (range.first < tReset) tReset = range.first;
2288
+ let currentT = tReset;
2289
+ const reported = (axis, tSec) => referenceMode === "observationTime" ? sign * (unsignedM1[axis] - (tSec - tReset) * unsignedM0[axis]) : sign * unsignedM1[axis];
2290
+ const advanceTo = (targetT) => {
2291
+ if (!(targetT > currentT + TIME_EPS2)) return;
2292
+ for (let axis = 0; axis < 3; axis++) {
2293
+ const ga = samplers[axis](currentT);
2294
+ const gb = samplers[axis](targetT);
2295
+ const integrated = integrateLinearSegment(currentT, targetT, tReset, ga, gb);
2296
+ unsignedM0[axis] += integrated[0];
2297
+ unsignedM1[axis] += integrated[1];
2298
+ }
2299
+ currentT = targetT;
2300
+ };
2301
+ const addReported = (tSec) => {
2302
+ for (let axis = 0; axis < 3; axis++) builders[axis].add(tSec, reported(axis, tSec));
2303
+ };
2304
+ const regularCount = Math.floor((range.last - range.first) / gradientRaster) + 1;
2305
+ const regularLast = range.first + Math.max(0, regularCount - 1) * gradientRaster;
2306
+ const hasFinalSample = regularLast < range.last - TIME_EPS2;
2307
+ const totalSamples = regularCount + (hasFinalSample ? 1 : 0);
2308
+ let eventIndex = 0;
2309
+ let sampleIndex = 0;
2310
+ while (eventIndex < events.length || sampleIndex < totalSamples) {
2311
+ const eventTime = eventIndex < events.length ? events[eventIndex].tSec : Number.POSITIVE_INFINITY;
2312
+ const sampleTime = sampleIndex < totalSamples ? sampleIndex < regularCount ? range.first + sampleIndex * gradientRaster : range.last : Number.POSITIVE_INFINITY;
2313
+ if (eventTime <= sampleTime) {
2314
+ advanceTo(eventTime);
2315
+ if (events[eventIndex].kind === "reset") {
2316
+ sign = 1;
2317
+ tReset = eventTime;
2318
+ currentT = eventTime;
2319
+ unsignedM0.fill(0);
2320
+ unsignedM1.fill(0);
2321
+ for (const builder of builders) builder.add(eventTime, 0);
2322
+ } else {
2323
+ addReported(eventTime);
2324
+ sign = -sign;
2325
+ }
2326
+ eventIndex++;
2327
+ } else {
2328
+ advanceTo(sampleTime);
2329
+ addReported(sampleTime);
2330
+ sampleIndex++;
2331
+ }
1500
2332
  }
2333
+ warnings.push(
2334
+ `Showing a bounded full-sequence M1 envelope (at most ${maxBuckets.toLocaleString()} buckets per axis). Zoom to 100 TRs or fewer for an automatic detailed calculation.`
2335
+ );
1501
2336
  return {
1502
2337
  valid: true,
1503
2338
  ok: true,
2339
+ coarse: true,
1504
2340
  referenceMode,
1505
- tSec: new Float64Array(x.t),
1506
- m1x: new Float64Array(x.m1),
1507
- m1y: new Float64Array(y.m1),
1508
- m1z: new Float64Array(z.m1),
2341
+ startSec,
2342
+ endSec,
2343
+ x: builders[0].finish(),
2344
+ y: builders[1].finish(),
2345
+ z: builders[2].finish(),
1509
2346
  warnings,
1510
2347
  excitationTimesSec: new Float64Array(excitationTimes),
1511
2348
  refocusingTimesSec: new Float64Array(refocusingTimes)
@@ -1544,7 +2381,7 @@ var Pulseq = (() => {
1544
2381
  function appendGradientPoint(series, t, value) {
1545
2382
  if (!Number.isFinite(t) || !Number.isFinite(value)) return;
1546
2383
  const last = series.time.length - 1;
1547
- if (last >= 0 && Math.abs(t - series.time[last]) <= TIME_EPS) {
2384
+ if (last >= 0 && Math.abs(t - series.time[last]) <= TIME_EPS2) {
1548
2385
  series.value[last] = 0.5 * (series.value[last] + value);
1549
2386
  } else if (last < 0 || t > series.time[last]) {
1550
2387
  series.time.push(t);
@@ -1594,7 +2431,7 @@ var Pulseq = (() => {
1594
2431
  const samples = [];
1595
2432
  const nSamples = Math.floor((tMax - tMin) / rasterSec) + 1;
1596
2433
  for (let i = 0; i < nSamples; i++) samples.push(tMin + i * rasterSec);
1597
- if (!samples.length || samples[samples.length - 1] < tMax - TIME_EPS) samples.push(tMax);
2434
+ if (!samples.length || samples[samples.length - 1] < tMax - TIME_EPS2) samples.push(tMax);
1598
2435
  return samples;
1599
2436
  }
1600
2437
  function walkM1(gradient, samples, events, excitationTimes, tMin, referenceMode) {
@@ -1608,16 +2445,16 @@ var Pulseq = (() => {
1608
2445
  let unsignedM1 = 0;
1609
2446
  let gradientIndex = -1;
1610
2447
  const seekGradient = (t) => {
1611
- while (gradientIndex + 1 < gradient.time.length && gradient.time[gradientIndex + 1] <= t + TIME_EPS) {
2448
+ while (gradientIndex + 1 < gradient.time.length && gradient.time[gradientIndex + 1] <= t + TIME_EPS2) {
1612
2449
  gradientIndex++;
1613
2450
  }
1614
2451
  };
1615
2452
  const sampleGradient = (t) => {
1616
2453
  const n = gradient.time.length;
1617
- if (n === 0 || t < gradient.time[0] - TIME_EPS || t > gradient.time[n - 1] + TIME_EPS) return 0;
2454
+ if (n === 0 || t < gradient.time[0] - TIME_EPS2 || t > gradient.time[n - 1] + TIME_EPS2) return 0;
1618
2455
  seekGradient(t);
1619
2456
  if (gradientIndex < 0) return 0;
1620
- if (gradientIndex >= n - 1 || Math.abs(t - gradient.time[gradientIndex]) <= TIME_EPS) {
2457
+ if (gradientIndex >= n - 1 || Math.abs(t - gradient.time[gradientIndex]) <= TIME_EPS2) {
1621
2458
  return gradient.value[gradientIndex];
1622
2459
  }
1623
2460
  const t0 = gradient.time[gradientIndex];
@@ -1631,8 +2468,8 @@ var Pulseq = (() => {
1631
2468
  return sign * unsignedM1;
1632
2469
  };
1633
2470
  const advanceTo = (targetT) => {
1634
- if (!(targetT > currentT + TIME_EPS)) return;
1635
- while (currentT < targetT - TIME_EPS) {
2471
+ if (!(targetT > currentT + TIME_EPS2)) return;
2472
+ while (currentT < targetT - TIME_EPS2) {
1636
2473
  seekGradient(currentT);
1637
2474
  let nextT = gradientIndex + 1 < gradient.time.length ? Math.min(targetT, gradient.time[gradientIndex + 1]) : targetT;
1638
2475
  if (!(nextT > currentT)) nextT = targetT;
@@ -1652,7 +2489,7 @@ var Pulseq = (() => {
1652
2489
  if (nextEvtT <= nextSampT) {
1653
2490
  advanceTo(nextEvtT);
1654
2491
  if (events[ei].kind === "reset") {
1655
- if (!outT.length || outT[outT.length - 1] < nextEvtT - TIME_EPS) {
2492
+ if (!outT.length || outT[outT.length - 1] < nextEvtT - TIME_EPS2) {
1656
2493
  outT.push(nextEvtT);
1657
2494
  outM1.push(0);
1658
2495
  } else {
@@ -1691,7 +2528,7 @@ var Pulseq = (() => {
1691
2528
 
1692
2529
  // src/pulseq/pns.ts
1693
2530
  var GAMMA_HZ_PER_T = 42576e3;
1694
- var TIME_EPS2 = 1e-15;
2531
+ var TIME_EPS3 = 1e-15;
1695
2532
  function parsePnsHardwareAsc(text) {
1696
2533
  const asc = parseAscText(text);
1697
2534
  const prefix = resolvePnsPrefix(asc);
@@ -1823,6 +2660,148 @@ var Pulseq = (() => {
1823
2660
  }
1824
2661
  return { valid: true, ok, timeSec, pnsX, pnsY, pnsZ, pnsNorm };
1825
2662
  }
2663
+ function calculatePnsCoarse(blocks, gradientRaster, hardware, options = {}) {
2664
+ const emptySeries = () => ({
2665
+ startTime: new Float64Array(),
2666
+ endTime: new Float64Array(),
2667
+ min: new Float64Array(),
2668
+ max: new Float64Array(),
2669
+ first: new Float64Array(),
2670
+ last: new Float64Array()
2671
+ });
2672
+ const invalid = (error) => ({
2673
+ valid: false,
2674
+ ok: false,
2675
+ coarse: true,
2676
+ error,
2677
+ startSec: 0,
2678
+ endSec: 0,
2679
+ x: emptySeries(),
2680
+ y: emptySeries(),
2681
+ z: emptySeries(),
2682
+ norm: emptySeries(),
2683
+ warnings: []
2684
+ });
2685
+ const gammaHzPerT = options.gammaHzPerT ?? GAMMA_HZ_PER_T;
2686
+ if (!hardware.valid) return invalid("PNS hardware is not initialized.");
2687
+ if (!blocks.length) return invalid("No sequence loaded.");
2688
+ if (!(gradientRaster > 0) || !(gammaHzPerT > 0)) return invalid("Missing GradientRasterTime or gamma.");
2689
+ const range = decodedGradientTimeRange(blocks);
2690
+ if (!range || !(range.last > range.first)) return invalid("No gradient waveform available for PNS.");
2691
+ const dtSec = gradientRaster;
2692
+ let ntMin = Math.floor(range.first / dtSec + Number.EPSILON) + 0.5;
2693
+ const ntMax = Math.ceil(range.last / dtSec - Number.EPSILON) - 0.5;
2694
+ if (ntMin < 0.5) ntMin = 0.5;
2695
+ if (ntMax < ntMin) return invalid("Unable to build regular PNS raster.");
2696
+ const nSamples = Math.floor(ntMax - ntMin + 1);
2697
+ if (nSamples < 2) return invalid("Too few samples for PNS computation.");
2698
+ const longestTauMs = Math.max(
2699
+ hardware.x.tau1Ms,
2700
+ hardware.x.tau2Ms,
2701
+ hardware.x.tau3Ms,
2702
+ hardware.y.tau1Ms,
2703
+ hardware.y.tau2Ms,
2704
+ hardware.y.tau3Ms,
2705
+ hardware.z.tau1Ms,
2706
+ hardware.z.tau2Ms,
2707
+ hardware.z.tau3Ms
2708
+ );
2709
+ const zptSec = longestTauMs * 4 / 1e3;
2710
+ const preCount = Math.max(0, Math.round(zptSec / (4 * dtSec)));
2711
+ const postCount = Math.max(0, Math.round(zptSec / dtSec));
2712
+ const totalSamples = preCount + nSamples + postCount;
2713
+ const hasAnyNonTrap = blocks.some((block) => block.gx?.type === "arb" || block.gy?.type === "arb" || block.gz?.type === "arb");
2714
+ const hasAnyLabelExt = blocks.some((block) => !!(block.labelSets?.length || block.labelIncs?.length));
2715
+ const shift = hasAnyNonTrap || hasAnyLabelExt ? 1 : 0;
2716
+ const stimLength = Math.max(0, totalSamples - 1);
2717
+ const desiredStimIndex = (origIndex) => {
2718
+ const paddedIndex = preCount + origIndex;
2719
+ if (shift > 0 && hasAnyLabelExt && origIndex === nSamples - 1) {
2720
+ return Math.min(paddedIndex, stimLength - 1);
2721
+ }
2722
+ return paddedIndex - shift;
2723
+ };
2724
+ const startSec = ntMin * dtSec;
2725
+ const endSec = (ntMin + nSamples - 1) * dtSec;
2726
+ const maxPoints = Math.max(1024, Math.min(12e4, options.maxPoints ?? 3e4));
2727
+ const maxBuckets = Math.floor(maxPoints / 4);
2728
+ const builders = [
2729
+ new BoundedSeriesBuilder(startSec, endSec, maxPoints),
2730
+ new BoundedSeriesBuilder(startSec, endSec, maxPoints),
2731
+ new BoundedSeriesBuilder(startSec, endSec, maxPoints),
2732
+ new BoundedSeriesBuilder(startSec, endSec, maxPoints)
2733
+ ];
2734
+ const samplers = [
2735
+ createDecodedGradientSampler(blocks, "gx"),
2736
+ createDecodedGradientSampler(blocks, "gy"),
2737
+ createDecodedGradientSampler(blocks, "gz")
2738
+ ];
2739
+ const hardwareAxes = [hardware.x, hardware.y, hardware.z];
2740
+ const filterStates = hardwareAxes.map((axis) => createPnsFilterState(axis, dtSec));
2741
+ const paddedValue = (axis, index) => {
2742
+ if (index < preCount || index >= preCount + nSamples) return 0;
2743
+ const rasterIndex = index - preCount;
2744
+ return samplers[axis]((ntMin + rasterIndex) * dtSec) / gammaHzPerT;
2745
+ };
2746
+ const previous = [paddedValue(0, 0), paddedValue(1, 0), paddedValue(2, 0)];
2747
+ let outputIndex = 0;
2748
+ let ok = true;
2749
+ for (let stimIndex = 0; stimIndex < stimLength; stimIndex++) {
2750
+ const normalized = [0, 0, 0];
2751
+ for (let axis = 0; axis < 3; axis++) {
2752
+ const current = paddedValue(axis, stimIndex + 1);
2753
+ const derivative = (current - previous[axis]) / dtSec;
2754
+ previous[axis] = current;
2755
+ normalized[axis] = updatePnsFilter(filterStates[axis], derivative);
2756
+ }
2757
+ while (outputIndex < nSamples && desiredStimIndex(outputIndex) < stimIndex) outputIndex++;
2758
+ if (outputIndex < nSamples && desiredStimIndex(outputIndex) === stimIndex) {
2759
+ const timeSec = (ntMin + outputIndex) * dtSec;
2760
+ const norm = Math.hypot(normalized[0], normalized[1], normalized[2]);
2761
+ builders[0].add(timeSec, normalized[0]);
2762
+ builders[1].add(timeSec, normalized[1]);
2763
+ builders[2].add(timeSec, normalized[2]);
2764
+ builders[3].add(timeSec, norm);
2765
+ if (norm >= 1) ok = false;
2766
+ outputIndex++;
2767
+ }
2768
+ }
2769
+ const warnings = [
2770
+ `Showing a bounded full-sequence PNS envelope (at most ${maxBuckets.toLocaleString()} buckets per curve). Zoom to 100 TRs or fewer for an automatic detailed calculation.`
2771
+ ];
2772
+ return {
2773
+ valid: true,
2774
+ ok,
2775
+ coarse: true,
2776
+ startSec,
2777
+ endSec,
2778
+ x: builders[0].finish(),
2779
+ y: builders[1].finish(),
2780
+ z: builders[2].finish(),
2781
+ norm: builders[3].finish(),
2782
+ warnings
2783
+ };
2784
+ }
2785
+ function createPnsFilterState(hardware, dtSec) {
2786
+ const dtMs = dtSec * 1e3;
2787
+ return {
2788
+ alpha1: lowpassAlpha(hardware.tau1Ms, dtMs),
2789
+ alpha2: lowpassAlpha(hardware.tau2Ms, dtMs),
2790
+ alpha3: lowpassAlpha(hardware.tau3Ms, dtMs),
2791
+ lp1: 0,
2792
+ lp2: 0,
2793
+ lp3: 0,
2794
+ hardware
2795
+ };
2796
+ }
2797
+ function updatePnsFilter(state, derivative) {
2798
+ state.lp1 = state.alpha1 * derivative + (1 - state.alpha1) * state.lp1;
2799
+ state.lp2 = state.alpha2 * Math.abs(derivative) + (1 - state.alpha2) * state.lp2;
2800
+ state.lp3 = state.alpha3 * derivative + (1 - state.alpha3) * state.lp3;
2801
+ const hw = state.hardware;
2802
+ const numerator = hw.a1 * Math.abs(state.lp1) + hw.a2 * state.lp2 + hw.a3 * Math.abs(state.lp3);
2803
+ return numerator / (hw.stimLimit > 0 ? hw.stimLimit : 1) * hw.gScale;
2804
+ }
1826
2805
  function safePnsModel(dgdt, dtSec, hw) {
1827
2806
  return runPnsModel(dgdt.length, (index) => dgdt[index], dtSec, hw);
1828
2807
  }
@@ -1961,7 +2940,7 @@ var Pulseq = (() => {
1961
2940
  function appendGradientPoint2(series, t, value) {
1962
2941
  if (!Number.isFinite(t) || !Number.isFinite(value)) return;
1963
2942
  const last = series.time.length - 1;
1964
- if (last >= 0 && Math.abs(t - series.time[last]) <= TIME_EPS2) {
2943
+ if (last >= 0 && Math.abs(t - series.time[last]) <= TIME_EPS3) {
1965
2944
  series.value[last] = 0.5 * (series.value[last] + value);
1966
2945
  } else if (last < 0 || t > series.time[last]) {
1967
2946
  series.time.push(t);
@@ -1989,9 +2968,9 @@ var Pulseq = (() => {
1989
2968
  return (t) => {
1990
2969
  const n = series.time.length;
1991
2970
  if (n === 0 || t < series.time[0] || t > series.time[n - 1]) return 0;
1992
- while (index + 1 < n && series.time[index + 1] <= t + TIME_EPS2) index++;
2971
+ while (index + 1 < n && series.time[index + 1] <= t + TIME_EPS3) index++;
1993
2972
  if (index < 0) return 0;
1994
- if (index >= n - 1 || t <= series.time[index] + TIME_EPS2) return series.value[index];
2973
+ if (index >= n - 1 || t <= series.time[index] + TIME_EPS3) return series.value[index];
1995
2974
  const t0 = series.time[index];
1996
2975
  const t1 = series.time[index + 1];
1997
2976
  if (!(t1 > t0)) return series.value[index];
@@ -2000,6 +2979,135 @@ var Pulseq = (() => {
2000
2979
  };
2001
2980
  }
2002
2981
 
2982
+ // src/pulseq/derivedWindow.ts
2983
+ function selectM1WindowBlocks(blocks, startSec, endSec) {
2984
+ const displayStartSec = finiteMin(startSec, endSec);
2985
+ const displayEndSec = finiteMax(startSec, endSec, displayStartSec);
2986
+ let calculationStartSec = displayStartSec;
2987
+ for (const block of blocks) {
2988
+ const center = block.rf?.centerTime;
2989
+ if (center === void 0 || center > displayStartSec) continue;
2990
+ const use = (block.rf?.use || "").toLowerCase();
2991
+ if (use === "e") calculationStartSec = Math.min(center, block.startTime);
2992
+ }
2993
+ return {
2994
+ blocks: overlappingBlocks(blocks, calculationStartSec, displayEndSec),
2995
+ calculationStartSec,
2996
+ displayStartSec,
2997
+ displayEndSec
2998
+ };
2999
+ }
3000
+ function selectPnsWindowBlocks(blocks, startSec, endSec, hardware) {
3001
+ const displayStartSec = finiteMin(startSec, endSec);
3002
+ const displayEndSec = finiteMax(startSec, endSec, displayStartSec);
3003
+ const longestTauMs = Math.max(
3004
+ hardware.x.tau1Ms,
3005
+ hardware.x.tau2Ms,
3006
+ hardware.x.tau3Ms,
3007
+ hardware.y.tau1Ms,
3008
+ hardware.y.tau2Ms,
3009
+ hardware.y.tau3Ms,
3010
+ hardware.z.tau1Ms,
3011
+ hardware.z.tau2Ms,
3012
+ hardware.z.tau3Ms
3013
+ );
3014
+ const calculationStartSec = Math.max(0, displayStartSec - longestTauMs * 4 / 1e3);
3015
+ return {
3016
+ blocks: overlappingBlocks(blocks, calculationStartSec, displayEndSec),
3017
+ calculationStartSec,
3018
+ displayStartSec,
3019
+ displayEndSec
3020
+ };
3021
+ }
3022
+ function overlappingBlocks(blocks, startSec, endSec) {
3023
+ return blocks.filter((block) => block.startTime + block.duration > startSec && block.startTime <= endSec);
3024
+ }
3025
+ function finiteMin(a, b) {
3026
+ const aa = Number.isFinite(a) ? a : 0;
3027
+ const bb = Number.isFinite(b) ? b : aa;
3028
+ return Math.max(0, Math.min(aa, bb));
3029
+ }
3030
+ function finiteMax(a, b, fallback) {
3031
+ const aa = Number.isFinite(a) ? a : fallback;
3032
+ const bb = Number.isFinite(b) ? b : aa;
3033
+ return Math.max(fallback, Math.max(aa, bb));
3034
+ }
3035
+
3036
+ // src/pulseq/computeBudget.ts
3037
+ var INTERACTIVE_COMPUTE_LIMITS = Object.freeze({
3038
+ kspaceRasterSamples: 12e6,
3039
+ kspaceAdcSamples: 8e6,
3040
+ kspaceGridCandidates: 18e6,
3041
+ derivedRasterSamples: 2e6
3042
+ });
3043
+ function estimateKspaceCost(blocks, gradientRaster, totalDuration) {
3044
+ let adcSamples = 0;
3045
+ let gradientSupportPoints = 0;
3046
+ let rfSupportPoints = 0;
3047
+ for (const block of blocks) {
3048
+ if (block.adc?.numSamples && block.adc.numSamples > 0) {
3049
+ adcSamples += block.adc.numSamples;
3050
+ }
3051
+ for (const gradient of [block.gx, block.gy, block.gz]) {
3052
+ if (gradient && gradient.type !== "none" && gradient.timePoints.length >= 2) {
3053
+ gradientSupportPoints += 2;
3054
+ }
3055
+ }
3056
+ if (block.rf) rfSupportPoints += block.rf.use === "r" ? 2 : 3;
3057
+ }
3058
+ const rasterSamples = gradientRaster > 0 && totalDuration > 0 ? Math.max(2, Math.round(totalDuration / gradientRaster) + 1) : 0;
3059
+ const gridCandidatePoints = rasterSamples + adcSamples + gradientSupportPoints + rfSupportPoints + 2;
3060
+ return { rasterSamples, adcSamples, gridCandidatePoints };
3061
+ }
3062
+ function estimateKspacePeakMemoryBytes(estimate) {
3063
+ const gridBytes = Math.max(0, estimate.gridCandidatePoints) * 96;
3064
+ const adcAndTransferBytes = Math.max(0, estimate.adcSamples) * 104;
3065
+ return Math.ceil(Math.min(Number.MAX_SAFE_INTEGER, (gridBytes + adcAndTransferBytes) * 1.25));
3066
+ }
3067
+ function estimateDerivedCost(blocks, gradientRaster) {
3068
+ let firstGradientTime = Infinity;
3069
+ let lastGradientTime = -Infinity;
3070
+ for (const block of blocks) {
3071
+ for (const gradient of [block.gx, block.gy, block.gz]) {
3072
+ const times = gradient?.timePoints;
3073
+ if (!times?.length) continue;
3074
+ const first = times[0];
3075
+ const last = times[times.length - 1];
3076
+ if (Number.isFinite(first) && first < firstGradientTime) firstGradientTime = first;
3077
+ if (Number.isFinite(last) && last > lastGradientTime) lastGradientTime = last;
3078
+ }
3079
+ }
3080
+ if (!Number.isFinite(firstGradientTime) || !Number.isFinite(lastGradientTime) || lastGradientTime < firstGradientTime || gradientRaster <= 0) {
3081
+ return { rasterSamples: 0, firstGradientTime: null, lastGradientTime: null };
3082
+ }
3083
+ const span = lastGradientTime - firstGradientTime;
3084
+ let rasterSamples = Math.max(1, Math.floor(span / gradientRaster) + 1);
3085
+ const finalRasterTime = firstGradientTime + (rasterSamples - 1) * gradientRaster;
3086
+ if (finalRasterTime < lastGradientTime - 1e-15) rasterSamples++;
3087
+ return { rasterSamples, firstGradientTime, lastGradientTime };
3088
+ }
3089
+ function formatSampleCount(value) {
3090
+ if (value >= 1e6) return `${(value / 1e6).toFixed(1)} million`;
3091
+ if (value >= 1e3) return `${(value / 1e3).toFixed(1)} thousand`;
3092
+ return String(value);
3093
+ }
3094
+ function formatMemorySize(bytes) {
3095
+ const safeBytes = Math.max(0, Number.isFinite(bytes) ? bytes : 0);
3096
+ const kib = 1024;
3097
+ const mib = kib * 1024;
3098
+ const gib = mib * 1024;
3099
+ if (safeBytes >= gib) {
3100
+ const value = safeBytes / gib;
3101
+ return `${value.toFixed(value >= 10 ? 0 : 1)} GiB`;
3102
+ }
3103
+ if (safeBytes >= mib) {
3104
+ const value = safeBytes / mib;
3105
+ return `${value.toFixed(value >= 10 ? 0 : 1)} MiB`;
3106
+ }
3107
+ if (safeBytes >= kib) return `${(safeBytes / kib).toFixed(1)} KiB`;
3108
+ return `${Math.round(safeBytes)} bytes`;
3109
+ }
3110
+
2003
3111
  // src/pulseq/trdetect.ts
2004
3112
  var GAMMA_HZ_T2 = 42576e3;
2005
3113
  var DEFAULT_B0_T2 = 3;
@@ -2182,6 +3290,13 @@ var Pulseq = (() => {
2182
3290
  // src/pulseq/kspaceExportArtifacts.ts
2183
3291
  function exportKspaceArtifacts(sequenceText, sequenceName, options = {}) {
2184
3292
  const seq = parseSequenceText(sequenceText);
3293
+ return exportKspaceArtifactsFromSequence(seq, sequenceName, options);
3294
+ }
3295
+ function exportKspaceArtifactsFromBytes(sequenceBytes, sequenceName, options = {}) {
3296
+ const seq = parseSequenceBytes(sequenceBytes, sequenceName);
3297
+ return exportKspaceArtifactsFromSequence(seq, sequenceName, options);
3298
+ }
3299
+ function exportKspaceArtifactsFromSequence(seq, sequenceName, options = {}) {
2185
3300
  const decoded = decodeAllBlocks(seq);
2186
3301
  const totalDuration = getTotalDuration(seq);
2187
3302
  const gradientSupport = options.gradientSupport ?? "all";