seqeyes-python 0.2.11__tar.gz → 0.2.13__tar.gz
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_python-0.2.11 → seqeyes_python-0.2.13}/PKG-INFO +2 -2
- {seqeyes_python-0.2.11 → seqeyes_python-0.2.13}/pyproject.toml +1 -1
- {seqeyes_python-0.2.11 → seqeyes_python-0.2.13}/src/seqeyes/__init__.py +1 -1
- {seqeyes_python-0.2.11 → seqeyes_python-0.2.13}/src/seqeyes/resources/pulseq-bundle.js +125 -76
- {seqeyes_python-0.2.11 → seqeyes_python-0.2.13}/src/seqeyes/resources/webview-bundle.js +95 -10
- {seqeyes_python-0.2.11 → seqeyes_python-0.2.13}/.gitignore +0 -0
- {seqeyes_python-0.2.11 → seqeyes_python-0.2.13}/README.md +0 -0
- {seqeyes_python-0.2.11 → seqeyes_python-0.2.13}/examples/demo.ipynb +0 -0
- {seqeyes_python-0.2.11 → seqeyes_python-0.2.13}/examples/demo.py +0 -0
- {seqeyes_python-0.2.11 → seqeyes_python-0.2.13}/src/seqeyes/_plot.py +0 -0
- {seqeyes_python-0.2.11 → seqeyes_python-0.2.13}/src/seqeyes/_renderer.py +0 -0
- {seqeyes_python-0.2.11 → seqeyes_python-0.2.13}/src/seqeyes/resources/viewer.html +0 -0
- {seqeyes_python-0.2.11 → seqeyes_python-0.2.13}/tests/conftest.py +0 -0
- {seqeyes_python-0.2.11 → seqeyes_python-0.2.13}/tests/debug_full.html +0 -0
- {seqeyes_python-0.2.11 → seqeyes_python-0.2.13}/tests/debug_minimal.html +0 -0
- {seqeyes_python-0.2.11 → seqeyes_python-0.2.13}/tests/debug_viewer.html +0 -0
- {seqeyes_python-0.2.11 → seqeyes_python-0.2.13}/tests/debug_viewer.py +0 -0
- {seqeyes_python-0.2.11 → seqeyes_python-0.2.13}/tests/integration_test.py +0 -0
- {seqeyes_python-0.2.11 → seqeyes_python-0.2.13}/tests/test_plot.py +0 -0
- {seqeyes_python-0.2.11 → seqeyes_python-0.2.13}/tests/test_renderer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: seqeyes-python
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.13
|
|
4
4
|
Summary: Interactive Pulseq MRI sequence viewer for Jupyter — lightweight, beautiful, replaces seq.plot() in pypulseq.
|
|
5
5
|
Project-URL: Homepage, https://github.com/bughht/seqeyes_plugin
|
|
6
6
|
Project-URL: Repository, https://github.com/bughht/seqeyes_plugin
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "seqeyes-python"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.13"
|
|
8
8
|
description = "Interactive Pulseq MRI sequence viewer for Jupyter — lightweight, beautiful, replaces seq.plot() in pypulseq."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -31,6 +31,7 @@ var Pulseq = (() => {
|
|
|
31
31
|
calculatePns: () => calculatePns,
|
|
32
32
|
calculatePnsCoarse: () => calculatePnsCoarse,
|
|
33
33
|
decodeAllBlocks: () => decodeAllBlocks,
|
|
34
|
+
derivedDetailViewLimitSec: () => derivedDetailViewLimitSec,
|
|
34
35
|
detectSequenceTiming: () => detectSequenceTiming,
|
|
35
36
|
estimateDerivedCost: () => estimateDerivedCost,
|
|
36
37
|
estimateKspaceCost: () => estimateKspaceCost,
|
|
@@ -53,7 +54,7 @@ var Pulseq = (() => {
|
|
|
53
54
|
});
|
|
54
55
|
|
|
55
56
|
// package.json
|
|
56
|
-
var version = "0.2.
|
|
57
|
+
var version = "0.2.13";
|
|
57
58
|
|
|
58
59
|
// src/pulseq/decompressor.ts
|
|
59
60
|
function decompressShape(compressed, numSamples) {
|
|
@@ -1403,21 +1404,112 @@ var Pulseq = (() => {
|
|
|
1403
1404
|
return parseSequenceText(text);
|
|
1404
1405
|
}
|
|
1405
1406
|
|
|
1406
|
-
// src/pulseq/
|
|
1407
|
+
// src/pulseq/rfClassification.ts
|
|
1407
1408
|
var GAMMA_HZ_T = 42576e3;
|
|
1408
1409
|
var DEFAULT_B0_T = 3;
|
|
1410
|
+
var sequenceUseCache = /* @__PURE__ */ new WeakMap();
|
|
1411
|
+
function classifyRfUse(rf, seq) {
|
|
1412
|
+
if (seq.versionCombined >= VER_V15 && rf.use && rf.use.toLowerCase() !== "u") {
|
|
1413
|
+
return rf.use.toLowerCase();
|
|
1414
|
+
}
|
|
1415
|
+
const flipAngleDeg = estimateRfFlipAngleDeg(rf, seq);
|
|
1416
|
+
if (isLegacyFatSaturation(rf, seq)) return "s";
|
|
1417
|
+
return flipAngleDeg >= 120 ? "r" : "e";
|
|
1418
|
+
}
|
|
1419
|
+
function classifyRfUses(seq) {
|
|
1420
|
+
const cachedUses = sequenceUseCache.get(seq);
|
|
1421
|
+
if (cachedUses) return cachedUses;
|
|
1422
|
+
const libraryUses = /* @__PURE__ */ new Map();
|
|
1423
|
+
const uses = seq.blocks.map((block) => {
|
|
1424
|
+
if (block.rfId <= 0) return "";
|
|
1425
|
+
const cached = libraryUses.get(block.rfId);
|
|
1426
|
+
if (cached !== void 0) return cached;
|
|
1427
|
+
const rf = seq.rfs.get(block.rfId);
|
|
1428
|
+
const use = rf ? classifyRfUse(rf, seq) : "";
|
|
1429
|
+
libraryUses.set(block.rfId, use);
|
|
1430
|
+
return use;
|
|
1431
|
+
});
|
|
1432
|
+
if (seq.versionCombined >= VER_V15 || uses.includes("e")) {
|
|
1433
|
+
sequenceUseCache.set(seq, uses);
|
|
1434
|
+
return uses;
|
|
1435
|
+
}
|
|
1436
|
+
const saturationBlocks = uses.map((use, index) => use === "s" ? index : -1).filter((index) => index >= 0);
|
|
1437
|
+
if (saturationBlocks.length < 2) {
|
|
1438
|
+
sequenceUseCache.set(seq, uses);
|
|
1439
|
+
return uses;
|
|
1440
|
+
}
|
|
1441
|
+
for (let anchor = 0; anchor < saturationBlocks.length; anchor++) {
|
|
1442
|
+
const start = saturationBlocks[anchor] + 1;
|
|
1443
|
+
const end = saturationBlocks[anchor + 1] ?? uses.length;
|
|
1444
|
+
for (let index = start; index < end; index++) {
|
|
1445
|
+
if (!uses[index] || uses[index] === "s") continue;
|
|
1446
|
+
uses[index] = "e";
|
|
1447
|
+
break;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
sequenceUseCache.set(seq, uses);
|
|
1451
|
+
return uses;
|
|
1452
|
+
}
|
|
1453
|
+
function estimateRfFlipAngleDeg(rf, seq) {
|
|
1454
|
+
const magShape = seq.shapes.get(rf.magShapeId);
|
|
1455
|
+
if (magShape && magShape.numSamples > 0) {
|
|
1456
|
+
const raster = seq.rasterTimes.rfRaster;
|
|
1457
|
+
const timeShape = rf.timeShapeId > 0 ? seq.shapes.get(rf.timeShapeId)?.samples : void 0;
|
|
1458
|
+
let area = 0;
|
|
1459
|
+
let previousTime = timeShape ? timeShape[0] * raster : 0.5 * raster;
|
|
1460
|
+
let previousAmplitude = Math.abs(rf.amplitude * magShape.samples[0]);
|
|
1461
|
+
for (let index = 1; index < magShape.numSamples; index++) {
|
|
1462
|
+
const time = timeShape ? timeShape[index] * raster : (index + 0.5) * raster;
|
|
1463
|
+
const amplitude = Math.abs(rf.amplitude * magShape.samples[index]);
|
|
1464
|
+
const duration = time - previousTime;
|
|
1465
|
+
if (duration > 0) area += 0.5 * (previousAmplitude + amplitude) * duration;
|
|
1466
|
+
previousTime = time;
|
|
1467
|
+
previousAmplitude = amplitude;
|
|
1468
|
+
}
|
|
1469
|
+
return 360 * area;
|
|
1470
|
+
}
|
|
1471
|
+
const absoluteAmplitude = Math.abs(rf.amplitude);
|
|
1472
|
+
if (absoluteAmplitude > 3e3) return 180;
|
|
1473
|
+
if (absoluteAmplitude > 1500) return 120;
|
|
1474
|
+
return 90;
|
|
1475
|
+
}
|
|
1476
|
+
function isLegacyFatSaturation(rf, seq) {
|
|
1477
|
+
const b0Tesla = getB0(seq);
|
|
1478
|
+
const frequencyPpm = rf.freqPPM !== 0 ? rf.freqPPM : b0Tesla > 0 ? 1e6 * rf.freqOffset / (GAMMA_HZ_T * b0Tesla) : 0;
|
|
1479
|
+
const durationSec = estimateRfDuration(rf, seq);
|
|
1480
|
+
return durationSec > 6e-3 && frequencyPpm >= -4.5 && frequencyPpm <= -3;
|
|
1481
|
+
}
|
|
1482
|
+
function estimateRfDuration(rf, seq) {
|
|
1483
|
+
const magShape = seq.shapes.get(rf.magShapeId);
|
|
1484
|
+
if (!magShape || magShape.numSamples <= 0) return 0;
|
|
1485
|
+
const raster = seq.rasterTimes.rfRaster;
|
|
1486
|
+
const timeShape = rf.timeShapeId > 0 ? seq.shapes.get(rf.timeShapeId)?.samples : void 0;
|
|
1487
|
+
if (timeShape && timeShape.length > 0) {
|
|
1488
|
+
return timeShape[timeShape.length - 1] * raster + raster;
|
|
1489
|
+
}
|
|
1490
|
+
return magShape.numSamples * raster;
|
|
1491
|
+
}
|
|
1409
1492
|
function getB0(seq) {
|
|
1493
|
+
const raw = seq.definitions.get("B0") ?? seq.definitions.get("b0") ?? seq.definitions.get("b_0");
|
|
1494
|
+
if (raw && raw.length > 0) return +raw[0];
|
|
1495
|
+
return DEFAULT_B0_T;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
// src/pulseq/decoder.ts
|
|
1499
|
+
var GAMMA_HZ_T2 = 42576e3;
|
|
1500
|
+
var DEFAULT_B0_T2 = 3;
|
|
1501
|
+
function getB02(seq) {
|
|
1410
1502
|
const raw = seq.definitions.get("B0");
|
|
1411
1503
|
if (raw && Array.isArray(raw) && raw.length > 0) return +raw[0];
|
|
1412
1504
|
const raw2 = seq.definitions.get("b0") ?? seq.definitions.get("b_0");
|
|
1413
1505
|
if (raw2 && Array.isArray(raw2) && raw2.length > 0) return +raw2[0];
|
|
1414
|
-
return
|
|
1506
|
+
return DEFAULT_B0_T2;
|
|
1415
1507
|
}
|
|
1416
1508
|
function effFreqOff(freqOffset, freqPPM, b0) {
|
|
1417
|
-
return freqOffset + freqPPM * 1e-6 *
|
|
1509
|
+
return freqOffset + freqPPM * 1e-6 * GAMMA_HZ_T2 * b0;
|
|
1418
1510
|
}
|
|
1419
1511
|
function effPhaseOff(phaseOffset, phasePPM, b0) {
|
|
1420
|
-
return phaseOffset + phasePPM * 1e-6 *
|
|
1512
|
+
return phaseOffset + phasePPM * 1e-6 * GAMMA_HZ_T2 * b0;
|
|
1421
1513
|
}
|
|
1422
1514
|
function decodeAllBlocks(seq) {
|
|
1423
1515
|
return decodeBlockRange(seq, 0, seq.blocks.length);
|
|
@@ -1434,13 +1526,14 @@ var Pulseq = (() => {
|
|
|
1434
1526
|
cumulative += blockDurationSeconds(seq, seq.blocks[i]);
|
|
1435
1527
|
}
|
|
1436
1528
|
const decoded = [];
|
|
1529
|
+
const classifiedRfUses = classifyRfUses(seq);
|
|
1437
1530
|
for (let i = s; i < e; i++) {
|
|
1438
1531
|
const block = seq.blocks[i];
|
|
1439
1532
|
const dur = blockDurationSeconds(seq, block);
|
|
1440
1533
|
const db = { index: block.num, duration: dur, startTime: cumulative };
|
|
1441
1534
|
if (block.rfId > 0) {
|
|
1442
1535
|
const rf = seq.rfs.get(block.rfId);
|
|
1443
|
-
if (rf) db.rf = decodeRF(seq, rf, cumulative, dur);
|
|
1536
|
+
if (rf) db.rf = decodeRF(seq, rf, cumulative, dur, classifiedRfUses[i]);
|
|
1444
1537
|
}
|
|
1445
1538
|
db.gx = decodeGradient(seq, block.gxId, cumulative, dur, "gx");
|
|
1446
1539
|
db.gy = decodeGradient(seq, block.gyId, cumulative, dur, "gy");
|
|
@@ -1469,11 +1562,11 @@ var Pulseq = (() => {
|
|
|
1469
1562
|
if (seq.versionCombined < VER_PRE_14) return block.dur * 1e-6;
|
|
1470
1563
|
return block.dur * seq.rasterTimes.blockDurationRaster;
|
|
1471
1564
|
}
|
|
1472
|
-
function decodeRF(seq, rf, blockStart, _blockDur) {
|
|
1565
|
+
function decodeRF(seq, rf, blockStart, _blockDur, classifiedUse) {
|
|
1473
1566
|
const raster = seq.rasterTimes.rfRaster;
|
|
1474
1567
|
const rfDelay = rf.delay * 1e-6;
|
|
1475
1568
|
const rfStart = blockStart + rfDelay;
|
|
1476
|
-
const b0 =
|
|
1569
|
+
const b0 = getB02(seq);
|
|
1477
1570
|
const freqFull = effFreqOff(rf.freqOffset, rf.freqPPM, b0);
|
|
1478
1571
|
const phaseFull = effPhaseOff(rf.phaseOffset, rf.phasePPM, b0);
|
|
1479
1572
|
const magShape = seq.shapes.get(rf.magShapeId);
|
|
@@ -1494,15 +1587,7 @@ var Pulseq = (() => {
|
|
|
1494
1587
|
}
|
|
1495
1588
|
const duration = n > 0 ? t[n - 1] - rfStart + raster : 0;
|
|
1496
1589
|
const centerTime = rf.center >= 0 ? blockStart + rfDelay + rf.center * 1e-6 : estimateRfPeakTime(t, amp, rfStart, duration);
|
|
1497
|
-
|
|
1498
|
-
if (!use || use === "u") {
|
|
1499
|
-
let faDeg = 0;
|
|
1500
|
-
for (let i = 1; i < n; i++) {
|
|
1501
|
-
const dt = t[i] - t[i - 1];
|
|
1502
|
-
faDeg += 360 * (amp[i] + amp[i - 1]) * 0.5 * dt;
|
|
1503
|
-
}
|
|
1504
|
-
use = faDeg >= 120 ? "r" : "e";
|
|
1505
|
-
}
|
|
1590
|
+
const use = classifiedUse || "u";
|
|
1506
1591
|
return {
|
|
1507
1592
|
blockIndex: rf.id,
|
|
1508
1593
|
startTime: rfStart,
|
|
@@ -1658,7 +1743,7 @@ var Pulseq = (() => {
|
|
|
1658
1743
|
return value * amplitude;
|
|
1659
1744
|
}
|
|
1660
1745
|
function decodeADC(adc, blockStart, seq) {
|
|
1661
|
-
const b0 =
|
|
1746
|
+
const b0 = getB02(seq);
|
|
1662
1747
|
const freqFull = effFreqOff(adc.freqOffset, adc.freqPPM, b0);
|
|
1663
1748
|
const phaseFull = effPhaseOff(adc.phaseOffset, adc.phasePPM, b0);
|
|
1664
1749
|
return {
|
|
@@ -2510,7 +2595,7 @@ var Pulseq = (() => {
|
|
|
2510
2595
|
}
|
|
2511
2596
|
}
|
|
2512
2597
|
warnings.push(
|
|
2513
|
-
`Showing a bounded full-sequence M1 envelope (at most ${maxBuckets.toLocaleString()} buckets per axis). Zoom
|
|
2598
|
+
`Showing a bounded full-sequence M1 envelope (at most ${maxBuckets.toLocaleString()} buckets per axis). Zoom in for an automatic detailed calculation; viewport sample limits also apply.`
|
|
2514
2599
|
);
|
|
2515
2600
|
return {
|
|
2516
2601
|
valid: true,
|
|
@@ -2611,7 +2696,7 @@ var Pulseq = (() => {
|
|
|
2611
2696
|
const events = [];
|
|
2612
2697
|
for (const block of blocks) {
|
|
2613
2698
|
if (!block.rf) continue;
|
|
2614
|
-
const use =
|
|
2699
|
+
const use = classifyRfUse2(block.rf.use);
|
|
2615
2700
|
if (!use) continue;
|
|
2616
2701
|
const rec = { tSec: block.rf.centerTime, use };
|
|
2617
2702
|
events.push(rec);
|
|
@@ -2626,7 +2711,7 @@ var Pulseq = (() => {
|
|
|
2626
2711
|
events.sort((a, b) => a.tSec - b.tSec);
|
|
2627
2712
|
return events;
|
|
2628
2713
|
}
|
|
2629
|
-
function
|
|
2714
|
+
function classifyRfUse2(raw) {
|
|
2630
2715
|
const c = (raw || "u").toLowerCase();
|
|
2631
2716
|
if (c === "e" || c === "r" || c === "s" || c === "i" || c === "p") return c;
|
|
2632
2717
|
return "u";
|
|
@@ -2986,7 +3071,7 @@ var Pulseq = (() => {
|
|
|
2986
3071
|
}
|
|
2987
3072
|
}
|
|
2988
3073
|
const warnings = [
|
|
2989
|
-
`Showing a bounded full-sequence PNS envelope (at most ${maxBuckets.toLocaleString()} buckets per curve). Zoom
|
|
3074
|
+
`Showing a bounded full-sequence PNS envelope (at most ${maxBuckets.toLocaleString()} buckets per curve). Zoom in for an automatic detailed calculation; viewport sample limits also apply.`
|
|
2990
3075
|
];
|
|
2991
3076
|
return {
|
|
2992
3077
|
valid: true,
|
|
@@ -3257,8 +3342,23 @@ var Pulseq = (() => {
|
|
|
3257
3342
|
kspaceRasterSamples: 12e6,
|
|
3258
3343
|
kspaceAdcSamples: 8e6,
|
|
3259
3344
|
kspaceGridCandidates: 18e6,
|
|
3260
|
-
derivedRasterSamples: 2e6
|
|
3345
|
+
derivedRasterSamples: 2e6,
|
|
3346
|
+
/**
|
|
3347
|
+
* Ceiling on the display samples one sequence load may carry to the VS Code
|
|
3348
|
+
* webview, counted as (time, amplitude) pairs across every RF and gradient
|
|
3349
|
+
* waveform. At 12 bytes per pair this bounds the shared binary buffers at
|
|
3350
|
+
* roughly 137 MiB in the extension host and again in the renderer.
|
|
3351
|
+
* Per-waveform detail is reduced uniformly once a sequence would exceed it,
|
|
3352
|
+
* because the alternative is an allocation neither process can satisfy.
|
|
3353
|
+
*/
|
|
3354
|
+
displayTransportSamples: 12e6
|
|
3261
3355
|
});
|
|
3356
|
+
function derivedDetailViewLimitSec(gradientRaster, trTimeSec, maxRasterSamples = INTERACTIVE_COMPUTE_LIMITS.derivedRasterSamples) {
|
|
3357
|
+
if (!(gradientRaster > 0) || !(maxRasterSamples > 0)) return 0;
|
|
3358
|
+
const sampleLimitedDuration = maxRasterSamples * gradientRaster / 2;
|
|
3359
|
+
if (!(trTimeSec > 0)) return sampleLimitedDuration;
|
|
3360
|
+
return Math.min(sampleLimitedDuration, trTimeSec * 100.5);
|
|
3361
|
+
}
|
|
3262
3362
|
function estimateKspaceCost(blocks, gradientRaster, totalDuration) {
|
|
3263
3363
|
let adcSamples = 0;
|
|
3264
3364
|
let gradientSupportPoints = 0;
|
|
@@ -3328,11 +3428,9 @@ var Pulseq = (() => {
|
|
|
3328
3428
|
}
|
|
3329
3429
|
|
|
3330
3430
|
// src/pulseq/trdetect.ts
|
|
3331
|
-
var GAMMA_HZ_T2 = 42576e3;
|
|
3332
|
-
var DEFAULT_B0_T2 = 3;
|
|
3333
3431
|
function detectSequenceTiming(seq) {
|
|
3334
|
-
const b0 = getB02(seq);
|
|
3335
3432
|
const supportsRfUse = seq.versionCombined >= 1005e3;
|
|
3433
|
+
const classifiedRfUses = classifyRfUses(seq);
|
|
3336
3434
|
let teTimeSec = 0;
|
|
3337
3435
|
let hasExplicitTE = false;
|
|
3338
3436
|
const teDef = seq.definitions.get("EchoTime") ?? seq.definitions.get("TE");
|
|
@@ -3362,7 +3460,7 @@ var Pulseq = (() => {
|
|
|
3362
3460
|
rfUsePerBlock.push(0);
|
|
3363
3461
|
continue;
|
|
3364
3462
|
}
|
|
3365
|
-
const useChar =
|
|
3463
|
+
const useChar = classifiedRfUses[i] || "u";
|
|
3366
3464
|
const useCode = useChar.charCodeAt(0);
|
|
3367
3465
|
rfUsePerBlock.push(useCode);
|
|
3368
3466
|
if (useChar === "e") {
|
|
@@ -3415,11 +3513,6 @@ var Pulseq = (() => {
|
|
|
3415
3513
|
rfUsePerBlock
|
|
3416
3514
|
};
|
|
3417
3515
|
}
|
|
3418
|
-
function getB02(seq) {
|
|
3419
|
-
const raw = seq.definitions.get("B0") ?? seq.definitions.get("b0") ?? seq.definitions.get("b_0");
|
|
3420
|
-
if (raw && Array.isArray(raw) && raw.length > 0) return +raw[0];
|
|
3421
|
-
return DEFAULT_B0_T2;
|
|
3422
|
-
}
|
|
3423
3516
|
function computeCumulativeTimes(seq) {
|
|
3424
3517
|
const times = [];
|
|
3425
3518
|
let cum = 0;
|
|
@@ -3433,40 +3526,6 @@ var Pulseq = (() => {
|
|
|
3433
3526
|
if (seq.versionCombined < VER_PRE_14) return block.dur * 1e-6;
|
|
3434
3527
|
return block.dur * seq.rasterTimes.blockDurationRaster;
|
|
3435
3528
|
}
|
|
3436
|
-
function classifyRfUse2(rf, seq, supportsMetadata, b0Tesla) {
|
|
3437
|
-
if (supportsMetadata && rf.use && rf.use !== "u" && rf.use !== "U") {
|
|
3438
|
-
return rf.use.toLowerCase();
|
|
3439
|
-
}
|
|
3440
|
-
const faDeg = estimateFlipAngleDeg(rf, seq);
|
|
3441
|
-
if (faDeg < 90.01) return "e";
|
|
3442
|
-
const freqPPM = rf.freqPPM !== 0 ? rf.freqPPM : b0Tesla > 0 ? 1e6 * rf.freqOffset / (GAMMA_HZ_T2 * b0Tesla) : 0;
|
|
3443
|
-
const durEst = estimateRfDuration(rf, seq);
|
|
3444
|
-
if (durEst > 6e-3 && freqPPM >= -4.5 && freqPPM <= -3) return "s";
|
|
3445
|
-
return "r";
|
|
3446
|
-
}
|
|
3447
|
-
function estimateFlipAngleDeg(rf, seq) {
|
|
3448
|
-
const magShape = seq.shapes.get(rf.magShapeId);
|
|
3449
|
-
if (magShape && magShape.numSamples > 0) {
|
|
3450
|
-
const raster = seq.rasterTimes.rfRaster;
|
|
3451
|
-
const timeShape = rf.timeShapeId > 0 ? seq.shapes.get(rf.timeShapeId)?.samples : void 0;
|
|
3452
|
-
let area = 0;
|
|
3453
|
-
let prevT = timeShape ? timeShape[0] * raster : 0.5 * raster;
|
|
3454
|
-
let prevAmp = Math.abs(rf.amplitude * magShape.samples[0]);
|
|
3455
|
-
for (let i = 1; i < magShape.numSamples; i++) {
|
|
3456
|
-
const t = timeShape ? timeShape[i] * raster : (i + 0.5) * raster;
|
|
3457
|
-
const amp = Math.abs(rf.amplitude * magShape.samples[i]);
|
|
3458
|
-
const dt = t - prevT;
|
|
3459
|
-
if (dt > 0) area += 0.5 * (prevAmp + amp) * dt;
|
|
3460
|
-
prevT = t;
|
|
3461
|
-
prevAmp = amp;
|
|
3462
|
-
}
|
|
3463
|
-
return 360 * area;
|
|
3464
|
-
}
|
|
3465
|
-
const absAmp = Math.abs(rf.amplitude);
|
|
3466
|
-
if (absAmp > 3e3) return 180;
|
|
3467
|
-
if (absAmp > 1500) return 120;
|
|
3468
|
-
return 90;
|
|
3469
|
-
}
|
|
3470
3529
|
function estimateRfCenter(rf, _seq) {
|
|
3471
3530
|
const magShape = _seq.shapes.get(rf.magShapeId);
|
|
3472
3531
|
if (!magShape || magShape.numSamples <= 0) return 0;
|
|
@@ -3483,16 +3542,6 @@ var Pulseq = (() => {
|
|
|
3483
3542
|
const timeShape = rf.timeShapeId > 0 ? _seq.shapes.get(rf.timeShapeId)?.samples : void 0;
|
|
3484
3543
|
return timeShape ? (timeShape[peakIdx] ?? 0) * raster : (peakIdx + 0.5) * raster;
|
|
3485
3544
|
}
|
|
3486
|
-
function estimateRfDuration(rf, seq) {
|
|
3487
|
-
const magShape = seq.shapes.get(rf.magShapeId);
|
|
3488
|
-
if (!magShape || magShape.numSamples <= 0) return 0;
|
|
3489
|
-
const raster = seq.rasterTimes.rfRaster;
|
|
3490
|
-
const timeShape = rf.timeShapeId > 0 ? seq.shapes.get(rf.timeShapeId)?.samples : void 0;
|
|
3491
|
-
if (timeShape && timeShape.length > 0) {
|
|
3492
|
-
return timeShape[timeShape.length - 1] * raster + raster;
|
|
3493
|
-
}
|
|
3494
|
-
return magShape.numSamples * raster;
|
|
3495
|
-
}
|
|
3496
3545
|
function estimateTRFromExcitations(excTimesSec) {
|
|
3497
3546
|
if (excTimesSec.length < 2) return 0;
|
|
3498
3547
|
const intervals = [];
|
|
@@ -1,5 +1,69 @@
|
|
|
1
1
|
/* SeqEyes WebView Bundle — auto-generated, do not edit */
|
|
2
2
|
"use strict";
|
|
3
|
+
/* ══ block-transport.js ══ */
|
|
4
|
+
/* ══════════════════════════════════════════════════════════════════════════
|
|
5
|
+
block-transport.js — rehydrate the packed block payload from the extension.
|
|
6
|
+
|
|
7
|
+
The standalone web app builds BL in this same heap, so its waveform arrays
|
|
8
|
+
cost one copy and nothing more. The VS Code extension has to cross a process
|
|
9
|
+
boundary, and VS Code serialises webview messages with JSON.stringify: every
|
|
10
|
+
sample becomes ~19 characters of text, the extension host and the renderer
|
|
11
|
+
each hold that string alongside the object graph, and V8 refuses to build a
|
|
12
|
+
string longer than 512 MiB at all. Dense arbitrary waveforms reach ~18 KB of
|
|
13
|
+
JSON per block, so a few tens of thousands of them cannot be delivered as
|
|
14
|
+
JSON at any amount of memory.
|
|
15
|
+
|
|
16
|
+
src/editor/blockTransport.ts therefore sends the samples as two shared binary
|
|
17
|
+
buffers — Float64 times, Float32 amplitudes — plus an envelope carrying each
|
|
18
|
+
waveform's offset and count. Restoring the arrays as typed-array views over
|
|
19
|
+
those buffers reproduces the exact block shape the renderer reads, without
|
|
20
|
+
copying a single sample: the renderer only ever indexes these arrays and asks
|
|
21
|
+
for their length, which a typed array answers identically.
|
|
22
|
+
══════════════════════════════════════════════════════════════════════════ */
|
|
23
|
+
|
|
24
|
+
/** Interpret a transferred buffer as `Ctor`, or null if it did not arrive. */
|
|
25
|
+
function asTypedView(buffer,Ctor){
|
|
26
|
+
if(buffer instanceof Ctor)return buffer;
|
|
27
|
+
if(buffer instanceof ArrayBuffer)return new Ctor(buffer);
|
|
28
|
+
// Some hosts hand the transfer back as a byte view rather than the buffer.
|
|
29
|
+
if(buffer&&buffer.buffer instanceof ArrayBuffer&&typeof buffer.byteLength==='number')
|
|
30
|
+
return new Ctor(buffer.buffer,buffer.byteOffset||0,Math.floor(buffer.byteLength/Ctor.BYTES_PER_ELEMENT));
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Attach typed-array views to the envelope in place and return it as BL.
|
|
36
|
+
* Throws rather than returning partial blocks: a viewer that silently draws
|
|
37
|
+
* half a sequence is worse than one that says why it cannot.
|
|
38
|
+
*/
|
|
39
|
+
function unpackSequenceBlocks(envelope,timeBuffer,valueBuffer,sampleCount){
|
|
40
|
+
if(!envelope||!envelope.length)return [];
|
|
41
|
+
var times=asTypedView(timeBuffer,Float64Array),values=asTypedView(valueBuffer,Float32Array);
|
|
42
|
+
if(!times||!values)throw new Error('the waveform sample buffers did not arrive as binary data');
|
|
43
|
+
var available=Math.min(times.length,values.length);
|
|
44
|
+
if(sampleCount>available)
|
|
45
|
+
throw new Error('the waveform sample buffers are short by '+(sampleCount-available)+' samples');
|
|
46
|
+
for(var i=0;i<envelope.length;i++){
|
|
47
|
+
var b=envelope[i];
|
|
48
|
+
if(b.rf){
|
|
49
|
+
b.rf.t=times.subarray(b.rf.o,b.rf.o+b.rf.n);
|
|
50
|
+
b.rf.m=values.subarray(b.rf.o,b.rf.o+b.rf.n);
|
|
51
|
+
b.rf.pt=times.subarray(b.rf.qo,b.rf.qo+b.rf.qn);
|
|
52
|
+
b.rf.p=values.subarray(b.rf.qo,b.rf.qo+b.rf.qn);
|
|
53
|
+
}
|
|
54
|
+
if(b.gx)attachGradientSamples(b.gx,times,values);
|
|
55
|
+
if(b.gy)attachGradientSamples(b.gy,times,values);
|
|
56
|
+
if(b.gz)attachGradientSamples(b.gz,times,values);
|
|
57
|
+
}
|
|
58
|
+
return envelope;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function attachGradientSamples(gradient,times,values){
|
|
62
|
+
gradient.t=times.subarray(gradient.o,gradient.o+gradient.n);
|
|
63
|
+
gradient.w=values.subarray(gradient.o,gradient.o+gradient.n);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
3
67
|
/* ══ state.js ══ */
|
|
4
68
|
/* ═══════════════════════════════════════════════════════════════════════
|
|
5
69
|
Application state & configuration
|
|
@@ -97,7 +161,7 @@ function scheduleViewerDraw(includeMinimap){
|
|
|
97
161
|
}
|
|
98
162
|
|
|
99
163
|
/* Timing metadata (TR/TE info for minimap tooltip) */
|
|
100
|
-
var seqTiming=null; // {trTimeSec,trCount,hasExplicitTR,teTimeSec,hasExplicitTE,rfUseGuessed}
|
|
164
|
+
var seqTiming=null; // {trTimeSec,trCount,hasExplicitTR,teTimeSec,hasExplicitTE,rfUseGuessed,derivedDetailMaxViewSec}
|
|
101
165
|
|
|
102
166
|
/* Block positions for minimap: [{i,s,d}, ...] */
|
|
103
167
|
var blockPos=[];
|
|
@@ -248,6 +312,16 @@ function applySerializedKspace(payload){
|
|
|
248
312
|
}
|
|
249
313
|
}
|
|
250
314
|
|
|
315
|
+
/* Report a load that did not arrive. Clearing the previous sequence matters as
|
|
316
|
+
much as the message: the progress overlay reaches "Ready" either way, so
|
|
317
|
+
leaving stale blocks on screen would read as a successful load. */
|
|
318
|
+
function showSequenceLoadFailure(message){
|
|
319
|
+
BL=[];waveformOverview=null;blockPos=[];mmCache=null;
|
|
320
|
+
setViewerNotice('sequence',message);
|
|
321
|
+
setExportButtonEnabled(false);
|
|
322
|
+
draw();drawMinimap();
|
|
323
|
+
}
|
|
324
|
+
|
|
251
325
|
/* ── Data reception ───────────────────────────────────────────────────── */
|
|
252
326
|
window.addEventListener('message',function(e){
|
|
253
327
|
var m=e.data;
|
|
@@ -270,7 +344,13 @@ window.addEventListener('message',function(e){
|
|
|
270
344
|
return;
|
|
271
345
|
}
|
|
272
346
|
if(m.type==='sequenceData'){
|
|
273
|
-
|
|
347
|
+
try{
|
|
348
|
+
BL=unpackSequenceBlocks(m.blocks,m.sampleTimes,m.sampleValues,m.sampleCount||0);
|
|
349
|
+
}catch(err){
|
|
350
|
+
showSequenceLoadFailure('Failed to load the sequence waveforms: '+(err&&err.message||err)+'.');
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
TD=m.totalDuration||0;GR=m.gradRaster||1e-5;
|
|
274
354
|
setViewerNotice('sequence',m.notices&&m.notices.length?m.notices.join(' '):null);
|
|
275
355
|
setViewerNotice('kspaceOverrideFailure',null);setKspaceSafetyWarning(m.kspaceSafety||null);
|
|
276
356
|
setViewerNotice('m1',null);setViewerNotice('pns',null);setViewerNotice('pnsThreshold',null);
|
|
@@ -288,6 +368,8 @@ window.addEventListener('message',function(e){
|
|
|
288
368
|
if(seqTiming&&seqTiming.trTimeSec>0){fitToFirstTR();}else{fit();}
|
|
289
369
|
draw();drawKs();drawMinimap();
|
|
290
370
|
setExportButtonEnabled(true);
|
|
371
|
+
}else if(m.type==='loadError'){
|
|
372
|
+
showSequenceLoadFailure(m.message||'The sequence could not be loaded.');
|
|
291
373
|
}else if(m.type==='kspaceData'){
|
|
292
374
|
applySerializedKspace(m.kspace);finishDangerousKspaceCalculation(null);drawKs();
|
|
293
375
|
if(typeof kOpen!=='undefined'&&!kOpen)document.getElementById('kbtn').click();
|
|
@@ -304,7 +386,7 @@ window.addEventListener('message',function(e){
|
|
|
304
386
|
}else chVis[m1RequestedChannel]=true;
|
|
305
387
|
computeGlobalMax();buildLegend();draw();
|
|
306
388
|
if(m.m1.warnings&&m.m1.warnings.length)console.warn('[SeqEyes M1]',m.m1.warnings.join('\n'));
|
|
307
|
-
setViewerNotice('m1Coarse',m.m1.coarse?'Large sequence: showing bounded full-sequence M1. Zoom
|
|
389
|
+
setViewerNotice('m1Coarse',m.m1.coarse?'Large sequence: showing bounded full-sequence M1. Zoom in for automatic detail.':null);
|
|
308
390
|
}else{
|
|
309
391
|
setViewerNotice('m1','M1 calculation failed: '+((m.m1&&m.m1.error)||'unknown error')+' Zoom in to inspect waveform detail.');
|
|
310
392
|
}
|
|
@@ -324,7 +406,7 @@ window.addEventListener('message',function(e){
|
|
|
324
406
|
pnsWindowData=null;pnsWindowPending=null;chVis[7]=true;
|
|
325
407
|
computeGlobalMax();buildLegend();draw();
|
|
326
408
|
setViewerNotice('pnsThreshold',m.pns.ok?null:'PNS warning: predicted level reaches or exceeds 100%.');
|
|
327
|
-
setViewerNotice('pnsCoarse',m.pns.coarse?'Large sequence: showing bounded full-sequence PNS. Zoom
|
|
409
|
+
setViewerNotice('pnsCoarse',m.pns.coarse?'Large sequence: showing bounded full-sequence PNS. Zoom in for automatic detail.':null);
|
|
328
410
|
}else{
|
|
329
411
|
setViewerNotice('pns','PNS calculation failed: '+((m.pns&&m.pns.error)||'unknown error')+' Zoom in to inspect waveform detail.');
|
|
330
412
|
}
|
|
@@ -396,10 +478,14 @@ function createM1SeriesPayload(m1){
|
|
|
396
478
|
};
|
|
397
479
|
}
|
|
398
480
|
|
|
481
|
+
function shouldRequestFineDerivedWindow(vs,ve){
|
|
482
|
+
var dur=ve-vs,maxView=seqTiming&&Number(seqTiming.derivedDetailMaxViewSec);
|
|
483
|
+
return !!(dur>0&&isFinite(maxView)&&maxView>0&&dur<=maxView+1e-12);
|
|
484
|
+
}
|
|
485
|
+
|
|
399
486
|
function shouldRequestFineM1Window(vs,ve){
|
|
400
487
|
if(!vscApi||!m1Data||!m1Data.coarse||!(chVis[8]||chVis[9]||chVis[10]))return false;
|
|
401
|
-
|
|
402
|
-
return !!(seqTiming&&seqTiming.trTimeSec>0&&dur<=seqTiming.trTimeSec*100.5);
|
|
488
|
+
return shouldRequestFineDerivedWindow(vs,ve);
|
|
403
489
|
}
|
|
404
490
|
|
|
405
491
|
function derivedWindowCovers(data,vs,ve){
|
|
@@ -421,14 +507,13 @@ function m1SeriesForView(vs,ve){
|
|
|
421
507
|
requestM1Window(vs,ve);
|
|
422
508
|
if(derivedWindowCovers(m1WindowData,vs,ve)){setViewerNotice('m1Detail',null);setViewerNotice('m1Coarse','Large sequence: full sequence uses bounded M1; the current view is detailed.');return m1WindowData;}
|
|
423
509
|
}
|
|
424
|
-
if(m1Data.coarse)setViewerNotice('m1Coarse','Large sequence: showing bounded full-sequence M1. Zoom
|
|
510
|
+
if(m1Data.coarse)setViewerNotice('m1Coarse','Large sequence: showing bounded full-sequence M1. Zoom in for automatic detail.');
|
|
425
511
|
return m1Data;
|
|
426
512
|
}
|
|
427
513
|
|
|
428
514
|
function shouldRequestFinePnsWindow(vs,ve){
|
|
429
515
|
if(!vscApi||!pnsData||!pnsData.coarse||!chVis[7])return false;
|
|
430
|
-
|
|
431
|
-
return !!(seqTiming&&seqTiming.trTimeSec>0&&dur<=seqTiming.trTimeSec*100.5);
|
|
516
|
+
return shouldRequestFineDerivedWindow(vs,ve);
|
|
432
517
|
}
|
|
433
518
|
|
|
434
519
|
function pnsWindowCovers(data,vs,ve){
|
|
@@ -450,7 +535,7 @@ function pnsSeriesForView(vs,ve){
|
|
|
450
535
|
requestPnsWindow(vs,ve);
|
|
451
536
|
if(pnsWindowCovers(pnsWindowData,vs,ve)){setViewerNotice('pnsCoarse','Large sequence: full sequence uses bounded PNS; the current view is detailed.');return pnsWindowData;}
|
|
452
537
|
}
|
|
453
|
-
if(pnsData.coarse)setViewerNotice('pnsCoarse','Large sequence: showing bounded full-sequence PNS. Zoom
|
|
538
|
+
if(pnsData.coarse)setViewerNotice('pnsCoarse','Large sequence: showing bounded full-sequence PNS. Zoom in for automatic detail.');
|
|
454
539
|
return pnsData;
|
|
455
540
|
}
|
|
456
541
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|