seqeyes-python 0.2.11__tar.gz → 0.2.12__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.
Files changed (20) hide show
  1. {seqeyes_python-0.2.11 → seqeyes_python-0.2.12}/PKG-INFO +1 -1
  2. {seqeyes_python-0.2.11 → seqeyes_python-0.2.12}/pyproject.toml +1 -1
  3. {seqeyes_python-0.2.11 → seqeyes_python-0.2.12}/src/seqeyes/__init__.py +1 -1
  4. {seqeyes_python-0.2.11 → seqeyes_python-0.2.12}/src/seqeyes/resources/pulseq-bundle.js +115 -75
  5. {seqeyes_python-0.2.11 → seqeyes_python-0.2.12}/src/seqeyes/resources/webview-bundle.js +12 -9
  6. {seqeyes_python-0.2.11 → seqeyes_python-0.2.12}/.gitignore +0 -0
  7. {seqeyes_python-0.2.11 → seqeyes_python-0.2.12}/README.md +0 -0
  8. {seqeyes_python-0.2.11 → seqeyes_python-0.2.12}/examples/demo.ipynb +0 -0
  9. {seqeyes_python-0.2.11 → seqeyes_python-0.2.12}/examples/demo.py +0 -0
  10. {seqeyes_python-0.2.11 → seqeyes_python-0.2.12}/src/seqeyes/_plot.py +0 -0
  11. {seqeyes_python-0.2.11 → seqeyes_python-0.2.12}/src/seqeyes/_renderer.py +0 -0
  12. {seqeyes_python-0.2.11 → seqeyes_python-0.2.12}/src/seqeyes/resources/viewer.html +0 -0
  13. {seqeyes_python-0.2.11 → seqeyes_python-0.2.12}/tests/conftest.py +0 -0
  14. {seqeyes_python-0.2.11 → seqeyes_python-0.2.12}/tests/debug_full.html +0 -0
  15. {seqeyes_python-0.2.11 → seqeyes_python-0.2.12}/tests/debug_minimal.html +0 -0
  16. {seqeyes_python-0.2.11 → seqeyes_python-0.2.12}/tests/debug_viewer.html +0 -0
  17. {seqeyes_python-0.2.11 → seqeyes_python-0.2.12}/tests/debug_viewer.py +0 -0
  18. {seqeyes_python-0.2.11 → seqeyes_python-0.2.12}/tests/integration_test.py +0 -0
  19. {seqeyes_python-0.2.11 → seqeyes_python-0.2.12}/tests/test_plot.py +0 -0
  20. {seqeyes_python-0.2.11 → seqeyes_python-0.2.12}/tests/test_renderer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: seqeyes-python
3
- Version: 0.2.11
3
+ Version: 0.2.12
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.11"
7
+ version = "0.2.12"
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"}
@@ -29,4 +29,4 @@ from seqeyes._plot import set, reset
29
29
  from seqeyes._renderer import SeqEyesViewer
30
30
 
31
31
  __all__ = ["set", "reset", "SeqEyesViewer"]
32
- __version__ = "0.2.11"
32
+ __version__ = "0.2.12"
@@ -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.11";
57
+ var version = "0.2.12";
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/decoder.ts
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 DEFAULT_B0_T;
1506
+ return DEFAULT_B0_T2;
1415
1507
  }
1416
1508
  function effFreqOff(freqOffset, freqPPM, b0) {
1417
- return freqOffset + freqPPM * 1e-6 * GAMMA_HZ_T * b0;
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 * GAMMA_HZ_T * b0;
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 = getB0(seq);
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
- let use = rf.use || "";
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 = getB0(seq);
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 to 100 TRs or fewer for an automatic detailed calculation.`
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 = classifyRfUse(block.rf.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 classifyRfUse(raw) {
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 to 100 TRs or fewer for an automatic detailed calculation.`
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,
@@ -3259,6 +3344,12 @@ var Pulseq = (() => {
3259
3344
  kspaceGridCandidates: 18e6,
3260
3345
  derivedRasterSamples: 2e6
3261
3346
  });
3347
+ function derivedDetailViewLimitSec(gradientRaster, trTimeSec, maxRasterSamples = INTERACTIVE_COMPUTE_LIMITS.derivedRasterSamples) {
3348
+ if (!(gradientRaster > 0) || !(maxRasterSamples > 0)) return 0;
3349
+ const sampleLimitedDuration = maxRasterSamples * gradientRaster / 2;
3350
+ if (!(trTimeSec > 0)) return sampleLimitedDuration;
3351
+ return Math.min(sampleLimitedDuration, trTimeSec * 100.5);
3352
+ }
3262
3353
  function estimateKspaceCost(blocks, gradientRaster, totalDuration) {
3263
3354
  let adcSamples = 0;
3264
3355
  let gradientSupportPoints = 0;
@@ -3328,11 +3419,9 @@ var Pulseq = (() => {
3328
3419
  }
3329
3420
 
3330
3421
  // src/pulseq/trdetect.ts
3331
- var GAMMA_HZ_T2 = 42576e3;
3332
- var DEFAULT_B0_T2 = 3;
3333
3422
  function detectSequenceTiming(seq) {
3334
- const b0 = getB02(seq);
3335
3423
  const supportsRfUse = seq.versionCombined >= 1005e3;
3424
+ const classifiedRfUses = classifyRfUses(seq);
3336
3425
  let teTimeSec = 0;
3337
3426
  let hasExplicitTE = false;
3338
3427
  const teDef = seq.definitions.get("EchoTime") ?? seq.definitions.get("TE");
@@ -3362,7 +3451,7 @@ var Pulseq = (() => {
3362
3451
  rfUsePerBlock.push(0);
3363
3452
  continue;
3364
3453
  }
3365
- const useChar = classifyRfUse2(rf, seq, supportsRfUse, b0);
3454
+ const useChar = classifiedRfUses[i] || "u";
3366
3455
  const useCode = useChar.charCodeAt(0);
3367
3456
  rfUsePerBlock.push(useCode);
3368
3457
  if (useChar === "e") {
@@ -3415,11 +3504,6 @@ var Pulseq = (() => {
3415
3504
  rfUsePerBlock
3416
3505
  };
3417
3506
  }
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
3507
  function computeCumulativeTimes(seq) {
3424
3508
  const times = [];
3425
3509
  let cum = 0;
@@ -3433,40 +3517,6 @@ var Pulseq = (() => {
3433
3517
  if (seq.versionCombined < VER_PRE_14) return block.dur * 1e-6;
3434
3518
  return block.dur * seq.rasterTimes.blockDurationRaster;
3435
3519
  }
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
3520
  function estimateRfCenter(rf, _seq) {
3471
3521
  const magShape = _seq.shapes.get(rf.magShapeId);
3472
3522
  if (!magShape || magShape.numSamples <= 0) return 0;
@@ -3483,16 +3533,6 @@ var Pulseq = (() => {
3483
3533
  const timeShape = rf.timeShapeId > 0 ? _seq.shapes.get(rf.timeShapeId)?.samples : void 0;
3484
3534
  return timeShape ? (timeShape[peakIdx] ?? 0) * raster : (peakIdx + 0.5) * raster;
3485
3535
  }
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
3536
  function estimateTRFromExcitations(excTimesSec) {
3497
3537
  if (excTimesSec.length < 2) return 0;
3498
3538
  const intervals = [];
@@ -97,7 +97,7 @@ function scheduleViewerDraw(includeMinimap){
97
97
  }
98
98
 
99
99
  /* Timing metadata (TR/TE info for minimap tooltip) */
100
- var seqTiming=null; // {trTimeSec,trCount,hasExplicitTR,teTimeSec,hasExplicitTE,rfUseGuessed}
100
+ var seqTiming=null; // {trTimeSec,trCount,hasExplicitTR,teTimeSec,hasExplicitTE,rfUseGuessed,derivedDetailMaxViewSec}
101
101
 
102
102
  /* Block positions for minimap: [{i,s,d}, ...] */
103
103
  var blockPos=[];
@@ -304,7 +304,7 @@ window.addEventListener('message',function(e){
304
304
  }else chVis[m1RequestedChannel]=true;
305
305
  computeGlobalMax();buildLegend();draw();
306
306
  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 to 100 TRs or fewer for automatic detail.':null);
307
+ setViewerNotice('m1Coarse',m.m1.coarse?'Large sequence: showing bounded full-sequence M1. Zoom in for automatic detail.':null);
308
308
  }else{
309
309
  setViewerNotice('m1','M1 calculation failed: '+((m.m1&&m.m1.error)||'unknown error')+' Zoom in to inspect waveform detail.');
310
310
  }
@@ -324,7 +324,7 @@ window.addEventListener('message',function(e){
324
324
  pnsWindowData=null;pnsWindowPending=null;chVis[7]=true;
325
325
  computeGlobalMax();buildLegend();draw();
326
326
  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 to 100 TRs or fewer for automatic detail.':null);
327
+ setViewerNotice('pnsCoarse',m.pns.coarse?'Large sequence: showing bounded full-sequence PNS. Zoom in for automatic detail.':null);
328
328
  }else{
329
329
  setViewerNotice('pns','PNS calculation failed: '+((m.pns&&m.pns.error)||'unknown error')+' Zoom in to inspect waveform detail.');
330
330
  }
@@ -396,10 +396,14 @@ function createM1SeriesPayload(m1){
396
396
  };
397
397
  }
398
398
 
399
+ function shouldRequestFineDerivedWindow(vs,ve){
400
+ var dur=ve-vs,maxView=seqTiming&&Number(seqTiming.derivedDetailMaxViewSec);
401
+ return !!(dur>0&&isFinite(maxView)&&maxView>0&&dur<=maxView+1e-12);
402
+ }
403
+
399
404
  function shouldRequestFineM1Window(vs,ve){
400
405
  if(!vscApi||!m1Data||!m1Data.coarse||!(chVis[8]||chVis[9]||chVis[10]))return false;
401
- var dur=ve-vs;
402
- return !!(seqTiming&&seqTiming.trTimeSec>0&&dur<=seqTiming.trTimeSec*100.5);
406
+ return shouldRequestFineDerivedWindow(vs,ve);
403
407
  }
404
408
 
405
409
  function derivedWindowCovers(data,vs,ve){
@@ -421,14 +425,13 @@ function m1SeriesForView(vs,ve){
421
425
  requestM1Window(vs,ve);
422
426
  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
427
  }
424
- if(m1Data.coarse)setViewerNotice('m1Coarse','Large sequence: showing bounded full-sequence M1. Zoom to 100 TRs or fewer for automatic detail.');
428
+ if(m1Data.coarse)setViewerNotice('m1Coarse','Large sequence: showing bounded full-sequence M1. Zoom in for automatic detail.');
425
429
  return m1Data;
426
430
  }
427
431
 
428
432
  function shouldRequestFinePnsWindow(vs,ve){
429
433
  if(!vscApi||!pnsData||!pnsData.coarse||!chVis[7])return false;
430
- var dur=ve-vs;
431
- return !!(seqTiming&&seqTiming.trTimeSec>0&&dur<=seqTiming.trTimeSec*100.5);
434
+ return shouldRequestFineDerivedWindow(vs,ve);
432
435
  }
433
436
 
434
437
  function pnsWindowCovers(data,vs,ve){
@@ -450,7 +453,7 @@ function pnsSeriesForView(vs,ve){
450
453
  requestPnsWindow(vs,ve);
451
454
  if(pnsWindowCovers(pnsWindowData,vs,ve)){setViewerNotice('pnsCoarse','Large sequence: full sequence uses bounded PNS; the current view is detailed.');return pnsWindowData;}
452
455
  }
453
- if(pnsData.coarse)setViewerNotice('pnsCoarse','Large sequence: showing bounded full-sequence PNS. Zoom to 100 TRs or fewer for automatic detail.');
456
+ if(pnsData.coarse)setViewerNotice('pnsCoarse','Large sequence: showing bounded full-sequence PNS. Zoom in for automatic detail.');
454
457
  return pnsData;
455
458
  }
456
459