seqeyes-python 0.2.8__tar.gz → 0.2.10__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.8 → seqeyes_python-0.2.10}/PKG-INFO +1 -1
  2. {seqeyes_python-0.2.8 → seqeyes_python-0.2.10}/pyproject.toml +1 -1
  3. {seqeyes_python-0.2.8 → seqeyes_python-0.2.10}/src/seqeyes/__init__.py +1 -1
  4. {seqeyes_python-0.2.8 → seqeyes_python-0.2.10}/src/seqeyes/resources/pulseq-bundle.js +148 -25
  5. seqeyes_python-0.2.10/src/seqeyes/resources/webview-bundle.js +2667 -0
  6. {seqeyes_python-0.2.8 → seqeyes_python-0.2.10}/.gitignore +0 -0
  7. {seqeyes_python-0.2.8 → seqeyes_python-0.2.10}/README.md +0 -0
  8. {seqeyes_python-0.2.8 → seqeyes_python-0.2.10}/examples/demo.ipynb +0 -0
  9. {seqeyes_python-0.2.8 → seqeyes_python-0.2.10}/examples/demo.py +0 -0
  10. {seqeyes_python-0.2.8 → seqeyes_python-0.2.10}/src/seqeyes/_plot.py +0 -0
  11. {seqeyes_python-0.2.8 → seqeyes_python-0.2.10}/src/seqeyes/_renderer.py +0 -0
  12. {seqeyes_python-0.2.8 → seqeyes_python-0.2.10}/src/seqeyes/resources/viewer.html +0 -0
  13. {seqeyes_python-0.2.8 → seqeyes_python-0.2.10}/tests/conftest.py +0 -0
  14. {seqeyes_python-0.2.8 → seqeyes_python-0.2.10}/tests/debug_full.html +0 -0
  15. {seqeyes_python-0.2.8 → seqeyes_python-0.2.10}/tests/debug_minimal.html +0 -0
  16. {seqeyes_python-0.2.8 → seqeyes_python-0.2.10}/tests/debug_viewer.html +0 -0
  17. {seqeyes_python-0.2.8 → seqeyes_python-0.2.10}/tests/debug_viewer.py +0 -0
  18. {seqeyes_python-0.2.8 → seqeyes_python-0.2.10}/tests/integration_test.py +0 -0
  19. {seqeyes_python-0.2.8 → seqeyes_python-0.2.10}/tests/test_plot.py +0 -0
  20. {seqeyes_python-0.2.8 → seqeyes_python-0.2.10}/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.8
3
+ Version: 0.2.10
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.8"
7
+ version = "0.2.10"
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.8"
32
+ __version__ = "0.2.9"
@@ -53,7 +53,7 @@ var Pulseq = (() => {
53
53
  });
54
54
 
55
55
  // package.json
56
- var version = "0.2.8";
56
+ var version = "0.2.9";
57
57
 
58
58
  // src/pulseq/decompressor.ts
59
59
  function decompressShape(compressed, numSamples) {
@@ -1782,11 +1782,16 @@ var Pulseq = (() => {
1782
1782
  }
1783
1783
 
1784
1784
  // src/pulseq/kspace.ts
1785
+ var TRAJECTORY_TIME_ACCURACY_SEC = 1e-10;
1786
+ var GRADIENT_ENDPOINT_TOLERANCE_SEC = 1e-12;
1787
+ function canonicalTrajectoryTime(timeSec) {
1788
+ return TRAJECTORY_TIME_ACCURACY_SEC * Math.round(timeSec / TRAJECTORY_TIME_ACCURACY_SEC);
1789
+ }
1785
1790
  function calculateKspace(blocks, gradientRaster, totalDuration, trajectoryDelay = 0, _options) {
1786
1791
  if (!blocks.length || !gradientRaster || gradientRaster <= 0) return null;
1787
1792
  const GR = gradientRaster;
1788
1793
  const RF = _options?.rfRaster && _options.rfRaster > 0 ? _options.rfRaster : 1e-6;
1789
- const tacc = 1e-10;
1794
+ const tacc = TRAJECTORY_TIME_ACCURACY_SEC;
1790
1795
  const gradientSupport = _options?.gradientSupport ?? "endpoints";
1791
1796
  const excT = [], refT = [];
1792
1797
  const gradTimes = [];
@@ -1854,7 +1859,7 @@ var Pulseq = (() => {
1854
1859
  let cum = 0;
1855
1860
  for (const b of blocks) {
1856
1861
  cum += b.duration;
1857
- edges.push(cum);
1862
+ edges.push(canonicalTrajectoryTime(cum));
1858
1863
  }
1859
1864
  for (let i = 0; i < N; i++) {
1860
1865
  const t = grid[i];
@@ -1955,7 +1960,10 @@ var Pulseq = (() => {
1955
1960
  if (!g || g.type === "none") return 0;
1956
1961
  const tp = g.timePoints, wf = g.waveform;
1957
1962
  if (!tp || tp.length < 2) return 0;
1958
- if (t < tp[0] || t > tp[tp.length - 1]) return 0;
1963
+ const first = tp[0], last = tp[tp.length - 1];
1964
+ if (t < first - GRADIENT_ENDPOINT_TOLERANCE_SEC || t > last + GRADIENT_ENDPOINT_TOLERANCE_SEC) return 0;
1965
+ if (t <= first + GRADIENT_ENDPOINT_TOLERANCE_SEC) return wf[0];
1966
+ if (t >= last - GRADIENT_ENDPOINT_TOLERANCE_SEC) return wf[wf.length - 1];
1959
1967
  let lo = 0, hi = tp.length - 1;
1960
1968
  while (hi - lo > 1) {
1961
1969
  const m = lo + hi >> 1;
@@ -2044,11 +2052,34 @@ var Pulseq = (() => {
2044
2052
  }
2045
2053
  add(tSec, value) {
2046
2054
  if (!Number.isFinite(tSec) || !Number.isFinite(value)) return;
2055
+ this.addToBucket(this.bucketIndex(tSec), tSec, value);
2056
+ }
2057
+ /** Fill a known constant interval without visiting every source-raster sample. */
2058
+ addConstantRange(startSec, endSec, value) {
2059
+ if (!Number.isFinite(startSec) || !Number.isFinite(endSec) || !Number.isFinite(value)) return;
2060
+ const start = Math.max(this.startSec, Math.min(startSec, endSec));
2061
+ const end = Math.min(this.endSec, Math.max(startSec, endSec));
2062
+ if (end < start) return;
2063
+ const firstIndex = this.bucketIndex(start);
2064
+ const lastIndex = this.bucketIndex(end);
2065
+ for (let index = firstIndex; index <= lastIndex; index++) {
2066
+ const bucketStart = this.span > 0 ? this.startSec + this.span * index / this.bucketCount : start;
2067
+ const bucketEnd = this.span > 0 ? this.startSec + this.span * (index + 1) / this.bucketCount : end;
2068
+ const left = Math.max(start, bucketStart);
2069
+ const right = Math.min(end, bucketEnd);
2070
+ if (right < left) continue;
2071
+ this.addToBucket(index, left, value);
2072
+ this.addToBucket(index, right, value);
2073
+ }
2074
+ }
2075
+ bucketIndex(tSec) {
2047
2076
  const normalized = this.span > 0 ? (tSec - this.startSec) / this.span : 0;
2048
- const index = Math.max(0, Math.min(
2077
+ return Math.max(0, Math.min(
2049
2078
  this.bucketCount - 1,
2050
2079
  Math.floor(normalized * this.bucketCount)
2051
2080
  ));
2081
+ }
2082
+ addToBucket(index, tSec, value) {
2052
2083
  const bucket = this.buckets[index];
2053
2084
  if (!bucket) {
2054
2085
  this.buckets[index] = {
@@ -2198,19 +2229,45 @@ var Pulseq = (() => {
2198
2229
  if (x.t.length !== y.t.length || x.t.length !== z.t.length) {
2199
2230
  warnings.push(`Internal warning: per-axis M1 output sizes disagree (${x.t.length}, ${y.t.length}, ${z.t.length}). Plot may be inconsistent.`);
2200
2231
  }
2232
+ const output = referenceMode === "rfCenter" ? compactRfCenteredSamples(x.t, x.m1, y.m1, z.m1) : { t: x.t, x: x.m1, y: y.m1, z: z.m1 };
2201
2233
  return {
2202
2234
  valid: true,
2203
2235
  ok: true,
2204
2236
  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),
2237
+ tSec: new Float64Array(output.t),
2238
+ m1x: new Float64Array(output.x),
2239
+ m1y: new Float64Array(output.y),
2240
+ m1z: new Float64Array(output.z),
2209
2241
  warnings,
2210
2242
  excitationTimesSec: new Float64Array(excitationTimes),
2211
2243
  refocusingTimesSec: new Float64Array(refocusingTimes)
2212
2244
  };
2213
2245
  }
2246
+ function compactRfCenteredSamples(time, x, y, z) {
2247
+ const count = Math.min(time.length, x.length, y.length, z.length);
2248
+ if (count <= 2) return {
2249
+ t: time.slice(0, count),
2250
+ x: x.slice(0, count),
2251
+ y: y.slice(0, count),
2252
+ z: z.slice(0, count)
2253
+ };
2254
+ const out = { t: [time[0]], x: [x[0]], y: [y[0]], z: [z[0]] };
2255
+ const sameVector = (left, right) => x[left] === x[right] && y[left] === y[right] && z[left] === z[right];
2256
+ for (let index = 1; index < count - 1; index++) {
2257
+ const duplicateTime = time[index] <= time[index - 1] + TIME_EPS2 || time[index + 1] <= time[index] + TIME_EPS2;
2258
+ const insidePlateau = sameVector(index - 1, index) && sameVector(index, index + 1);
2259
+ if (!duplicateTime && insidePlateau) continue;
2260
+ out.t.push(time[index]);
2261
+ out.x.push(x[index]);
2262
+ out.y.push(y[index]);
2263
+ out.z.push(z[index]);
2264
+ }
2265
+ out.t.push(time[count - 1]);
2266
+ out.x.push(x[count - 1]);
2267
+ out.y.push(y[count - 1]);
2268
+ out.z.push(z[count - 1]);
2269
+ return out;
2270
+ }
2214
2271
  function appendM1AdvisoryWarnings(rfEvents, tMin, warnings) {
2215
2272
  let recentExcCount = 0;
2216
2273
  let lastExcT = -1e9;
@@ -2280,21 +2337,21 @@ var Pulseq = (() => {
2280
2337
  createDecodedGradientSampler(blocks, "gy"),
2281
2338
  createDecodedGradientSampler(blocks, "gz")
2282
2339
  ];
2283
- const unsignedM0 = [0, 0, 0];
2284
- const unsignedM1 = [0, 0, 0];
2340
+ const effectiveM0 = [0, 0, 0];
2341
+ const effectiveM1 = [0, 0, 0];
2285
2342
  let sign = 1;
2286
2343
  let tReset = excitationTimes.length ? excitationTimes[0] : range.first;
2287
2344
  if (range.first < tReset) tReset = range.first;
2288
2345
  let currentT = tReset;
2289
- const reported = (axis, tSec) => referenceMode === "observationTime" ? sign * (unsignedM1[axis] - (tSec - tReset) * unsignedM0[axis]) : sign * unsignedM1[axis];
2346
+ const reported = (axis, tSec) => referenceMode === "observationTime" ? effectiveM1[axis] - (tSec - tReset) * effectiveM0[axis] : effectiveM1[axis];
2290
2347
  const advanceTo = (targetT) => {
2291
2348
  if (!(targetT > currentT + TIME_EPS2)) return;
2292
2349
  for (let axis = 0; axis < 3; axis++) {
2293
2350
  const ga = samplers[axis](currentT);
2294
2351
  const gb = samplers[axis](targetT);
2295
2352
  const integrated = integrateLinearSegment(currentT, targetT, tReset, ga, gb);
2296
- unsignedM0[axis] += integrated[0];
2297
- unsignedM1[axis] += integrated[1];
2353
+ effectiveM0[axis] += sign * integrated[0];
2354
+ effectiveM1[axis] += sign * integrated[1];
2298
2355
  }
2299
2356
  currentT = targetT;
2300
2357
  };
@@ -2305,19 +2362,45 @@ var Pulseq = (() => {
2305
2362
  const regularLast = range.first + Math.max(0, regularCount - 1) * gradientRaster;
2306
2363
  const hasFinalSample = regularLast < range.last - TIME_EPS2;
2307
2364
  const totalSamples = regularCount + (hasFinalSample ? 1 : 0);
2365
+ const sampleTimeAt = (index) => index < regularCount ? range.first + index * gradientRaster : range.last;
2366
+ const gradientFreeIntervals = referenceMode === "rfCenter" ? collectGradientFreeIntervals(blocks) : [];
2367
+ let gradientFreeIndex = 0;
2308
2368
  let eventIndex = 0;
2309
2369
  let sampleIndex = 0;
2310
2370
  while (eventIndex < events.length || sampleIndex < totalSamples) {
2311
2371
  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;
2372
+ const sampleTime = sampleIndex < totalSamples ? sampleTimeAt(sampleIndex) : Number.POSITIVE_INFINITY;
2373
+ while (gradientFreeIndex < gradientFreeIntervals.length && gradientFreeIntervals[gradientFreeIndex].end <= currentT + TIME_EPS2) {
2374
+ gradientFreeIndex++;
2375
+ }
2376
+ const gap = gradientFreeIntervals[gradientFreeIndex];
2377
+ const ordinaryTarget = Math.min(eventTime, sampleTime);
2378
+ if (gap && currentT < gap.start - TIME_EPS2 && gap.start < ordinaryTarget - TIME_EPS2) {
2379
+ advanceTo(gap.start);
2380
+ addReported(gap.start);
2381
+ continue;
2382
+ }
2383
+ if (gap && currentT >= gap.start - TIME_EPS2 && currentT < gap.end - TIME_EPS2) {
2384
+ const jumpTarget = Math.min(gap.end, eventTime, endSec);
2385
+ if (jumpTarget > currentT + TIME_EPS2) {
2386
+ for (let axis = 0; axis < 3; axis++) {
2387
+ builders[axis].addConstantRange(currentT, jumpTarget, effectiveM1[axis]);
2388
+ }
2389
+ currentT = jumpTarget;
2390
+ while (sampleIndex < totalSamples && sampleTimeAt(sampleIndex) <= currentT + TIME_EPS2) {
2391
+ sampleIndex++;
2392
+ }
2393
+ continue;
2394
+ }
2395
+ }
2313
2396
  if (eventTime <= sampleTime) {
2314
2397
  advanceTo(eventTime);
2315
2398
  if (events[eventIndex].kind === "reset") {
2316
2399
  sign = 1;
2317
2400
  tReset = eventTime;
2318
2401
  currentT = eventTime;
2319
- unsignedM0.fill(0);
2320
- unsignedM1.fill(0);
2402
+ effectiveM0.fill(0);
2403
+ effectiveM1.fill(0);
2321
2404
  for (const builder of builders) builder.add(eventTime, 0);
2322
2405
  } else {
2323
2406
  addReported(eventTime);
@@ -2378,6 +2461,46 @@ var Pulseq = (() => {
2378
2461
  }
2379
2462
  return series;
2380
2463
  }
2464
+ function collectGradientFreeIntervals(blocks) {
2465
+ const gaps = [];
2466
+ const appendGap = (start, end) => {
2467
+ if (!(end > start + TIME_EPS2)) return;
2468
+ const previous = gaps[gaps.length - 1];
2469
+ if (previous && start <= previous.end + TIME_EPS2) {
2470
+ previous.end = Math.max(previous.end, end);
2471
+ } else {
2472
+ gaps.push({ start, end });
2473
+ }
2474
+ };
2475
+ for (const block of blocks) {
2476
+ const blockStart = block.startTime;
2477
+ const blockEnd = block.startTime + block.duration;
2478
+ if (!(blockEnd > blockStart + TIME_EPS2)) continue;
2479
+ const active = [];
2480
+ for (const gradient of [block.gx, block.gy, block.gz]) {
2481
+ if (!gradient?.timePoints.length || !gradient.waveform.length || gradient.type === "none") continue;
2482
+ let nonzero = false;
2483
+ for (const value of gradient.waveform) {
2484
+ if (value !== 0) {
2485
+ nonzero = true;
2486
+ break;
2487
+ }
2488
+ }
2489
+ if (!nonzero) continue;
2490
+ const first = Math.max(blockStart, gradient.timePoints[0]);
2491
+ const last = Math.min(blockEnd, gradient.timePoints[gradient.timePoints.length - 1]);
2492
+ if (last > first + TIME_EPS2) active.push({ start: first, end: last });
2493
+ }
2494
+ active.sort((left, right) => left.start - right.start);
2495
+ let cursor = blockStart;
2496
+ for (const interval of active) {
2497
+ if (interval.start > cursor + TIME_EPS2) appendGap(cursor, interval.start);
2498
+ cursor = Math.max(cursor, interval.end);
2499
+ }
2500
+ if (cursor < blockEnd - TIME_EPS2) appendGap(cursor, blockEnd);
2501
+ }
2502
+ return gaps;
2503
+ }
2381
2504
  function appendGradientPoint(series, t, value) {
2382
2505
  if (!Number.isFinite(t) || !Number.isFinite(value)) return;
2383
2506
  const last = series.time.length - 1;
@@ -2441,8 +2564,8 @@ var Pulseq = (() => {
2441
2564
  let tReset = excitationTimes.length ? excitationTimes[0] : tMin;
2442
2565
  if (samples.length && samples[0] < tReset) tReset = samples[0];
2443
2566
  let currentT = tReset;
2444
- let unsignedM0 = 0;
2445
- let unsignedM1 = 0;
2567
+ let effectiveM0 = 0;
2568
+ let effectiveM1 = 0;
2446
2569
  let gradientIndex = -1;
2447
2570
  const seekGradient = (t) => {
2448
2571
  while (gradientIndex + 1 < gradient.time.length && gradient.time[gradientIndex + 1] <= t + TIME_EPS2) {
@@ -2464,8 +2587,8 @@ var Pulseq = (() => {
2464
2587
  return gradient.value[gradientIndex] + alpha * (gradient.value[gradientIndex + 1] - gradient.value[gradientIndex]);
2465
2588
  };
2466
2589
  const reportedM1At = (t) => {
2467
- if (referenceMode === "observationTime") return sign * (unsignedM1 - (t - tReset) * unsignedM0);
2468
- return sign * unsignedM1;
2590
+ if (referenceMode === "observationTime") return effectiveM1 - (t - tReset) * effectiveM0;
2591
+ return effectiveM1;
2469
2592
  };
2470
2593
  const advanceTo = (targetT) => {
2471
2594
  if (!(targetT > currentT + TIME_EPS2)) return;
@@ -2476,8 +2599,8 @@ var Pulseq = (() => {
2476
2599
  const ga = sampleGradient(currentT);
2477
2600
  const gb = sampleGradient(nextT);
2478
2601
  const [m0Seg, m1Seg] = integrateLinearSegment(currentT, nextT, tReset, ga, gb);
2479
- unsignedM0 += m0Seg;
2480
- unsignedM1 += m1Seg;
2602
+ effectiveM0 += sign * m0Seg;
2603
+ effectiveM1 += sign * m1Seg;
2481
2604
  currentT = nextT;
2482
2605
  }
2483
2606
  };
@@ -2499,8 +2622,8 @@ var Pulseq = (() => {
2499
2622
  sign = 1;
2500
2623
  tReset = nextEvtT;
2501
2624
  currentT = nextEvtT;
2502
- unsignedM0 = 0;
2503
- unsignedM1 = 0;
2625
+ effectiveM0 = 0;
2626
+ effectiveM1 = 0;
2504
2627
  } else {
2505
2628
  outT.push(nextEvtT);
2506
2629
  outM1.push(reportedM1At(nextEvtT));