seqeyes-python 0.2.1__py3-none-any.whl → 0.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
seqeyes/__init__.py CHANGED
@@ -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.1"
32
+ __version__ = "0.2.2"
seqeyes/_plot.py CHANGED
@@ -20,6 +20,9 @@ from typing import Any, Optional, Tuple, Union
20
20
 
21
21
  _defaults: dict[str, Any] = {}
22
22
  _original_plot = None
23
+ _original_repr_html = None
24
+ _had_original_repr_html = False
25
+ _patched_repr_html = None
23
26
  _patched = False
24
27
 
25
28
 
@@ -53,7 +56,7 @@ def reset() -> None:
53
56
 
54
57
  Call ``seqeyes.set()`` again to re‑enable at any time.
55
58
  """
56
- global _patched
59
+ global _patched, _patched_repr_html
57
60
  _defaults.clear()
58
61
  _patched = False
59
62
 
@@ -64,6 +67,12 @@ def reset() -> None:
64
67
 
65
68
  if _original_plot is not None:
66
69
  _Seq.plot = _original_plot # type: ignore[attr-defined]
70
+ if _patched_repr_html is not None and getattr(_Seq, "_repr_html_", None) is _patched_repr_html:
71
+ if _had_original_repr_html:
72
+ _Seq._repr_html_ = _original_repr_html # type: ignore[attr-defined]
73
+ else:
74
+ delattr(_Seq, "_repr_html_")
75
+ _patched_repr_html = None
67
76
 
68
77
 
69
78
  # ── Internal helpers ──────────────────────────────────────────────────────
@@ -87,7 +96,7 @@ def _store_defaults(
87
96
 
88
97
 
89
98
  def _ensure_patched() -> None:
90
- global _patched, _original_plot
99
+ global _patched, _original_plot, _original_repr_html, _had_original_repr_html, _patched_repr_html
91
100
 
92
101
  if _patched:
93
102
  return
@@ -100,6 +109,9 @@ def _ensure_patched() -> None:
100
109
 
101
110
  if _original_plot is None:
102
111
  _original_plot = getattr(_Seq, "plot", None)
112
+ if _patched_repr_html is None:
113
+ _had_original_repr_html = hasattr(_Seq, "_repr_html_")
114
+ _original_repr_html = getattr(_Seq, "_repr_html_", None)
103
115
 
104
116
  # ── Replacement .plot() ──────────────────────────────────────────
105
117
  def _seqeyes_plot(
@@ -108,7 +120,7 @@ def _ensure_patched() -> None:
108
120
  show_blocks: bool = False,
109
121
  time_range: Any = (0, float("inf")),
110
122
  time_disp: str = "s",
111
- grad_disp: str = "kHz/m",
123
+ grad_disp: str = "Hz/m",
112
124
  **kwargs: Any,
113
125
  ) -> None:
114
126
  # Merge globals — per‑call args take precedence
@@ -147,6 +159,7 @@ def _ensure_patched() -> None:
147
159
  return SeqEyesViewer(_seq_to_text(self))._repr_html_()
148
160
 
149
161
  _Seq._repr_html_ = _seq_repr_html_ # type: ignore[attr-defined]
162
+ _patched_repr_html = _seq_repr_html_
150
163
 
151
164
 
152
165
  def _in_jupyter() -> bool:
seqeyes/_renderer.py CHANGED
@@ -26,6 +26,7 @@ _BUNDLE_CANDIDATES = [
26
26
  Path(__file__).resolve().parent.parent.parent.parent / "web" / "pulseq-bundle.js",
27
27
  Path.cwd() / "web" / "pulseq-bundle.js",
28
28
  ]
29
+ _VALID_GRAD_UNITS = {"Hz/m", "mT/m", "G/cm"}
29
30
 
30
31
 
31
32
  def _find_bundle() -> str:
@@ -53,6 +54,11 @@ def _read_viewer_template() -> str:
53
54
  )
54
55
 
55
56
 
57
+ def _normalize_grad_disp(unit: str) -> str:
58
+ """Return a viewer-supported gradient unit."""
59
+ return unit if unit in _VALID_GRAD_UNITS else "Hz/m"
60
+
61
+
56
62
  def _build_html(
57
63
  seq_text: str,
58
64
  *,
@@ -62,7 +68,7 @@ def _build_html(
62
68
  show_blocks: bool = False,
63
69
  time_range: tuple = (0, float("inf")),
64
70
  time_disp: str = "s",
65
- grad_disp: str = "kHz/m",
71
+ grad_disp: str = "Hz/m",
66
72
  ) -> str:
67
73
  """Assemble the complete viewer HTML.
68
74
 
@@ -83,10 +89,10 @@ def _build_html(
83
89
  time_disp : str
84
90
  Initial time display unit (``"s"``, ``"ms"``, ``"us"``).
85
91
  grad_disp : str
86
- Initial gradient display unit (``"Hz/m"``, ``"kHz/m"``,
87
- ``"mT/m"``, ``"G/cm"``).
92
+ Initial gradient display unit (``"Hz/m"``, ``"mT/m"``, ``"G/cm"``).
88
93
  """
89
94
  template = _read_viewer_template()
95
+ grad_disp = _normalize_grad_disp(grad_disp)
90
96
  seq_b64 = base64.b64encode(seq_text.encode("utf-8")).decode("ascii")
91
97
 
92
98
  # 1. Inject the pulseq-bundle.js
@@ -150,7 +156,7 @@ class SeqEyesViewer:
150
156
  time_disp : str
151
157
  Time axis unit — ``"s"``, ``"ms"``, or ``"us"``.
152
158
  grad_disp : str
153
- Gradient axis unit — ``"Hz/m"``, ``"kHz/m"``, ``"mT/m"``, ``"G/cm"``.
159
+ Gradient axis unit — ``"Hz/m"``, ``"mT/m"``, or ``"G/cm"``.
154
160
  theme : str or None
155
161
  Colour theme.
156
162
  width : str
@@ -167,7 +173,7 @@ class SeqEyesViewer:
167
173
  show_blocks: bool = False,
168
174
  time_range: tuple = (0, float("inf")),
169
175
  time_disp: str = "s",
170
- grad_disp: str = "kHz/m",
176
+ grad_disp: str = "Hz/m",
171
177
  theme: Optional[str] = None,
172
178
  width: str = "100%",
173
179
  height: str = "550px",
@@ -177,7 +183,7 @@ class SeqEyesViewer:
177
183
  self._show_blocks = show_blocks
178
184
  self._time_range = time_range
179
185
  self._time_disp = time_disp
180
- self._grad_disp = grad_disp
186
+ self._grad_disp = _normalize_grad_disp(grad_disp)
181
187
  self._theme = theme
182
188
  self._width = width
183
189
  self._height = height
@@ -4,6 +4,7 @@ var Pulseq = (() => {
4
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
7
8
  var __export = (target, all) => {
8
9
  for (var name in all)
9
10
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -17,6 +18,7 @@ var Pulseq = (() => {
17
18
  return to;
18
19
  };
19
20
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
21
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
20
22
 
21
23
  // web/pulseq-browser.ts
22
24
  var pulseq_browser_exports = {};
@@ -36,7 +38,7 @@ var Pulseq = (() => {
36
38
  });
37
39
 
38
40
  // package.json
39
- var version = "0.1.17";
41
+ var version = "0.2.2";
40
42
 
41
43
  // src/pulseq/decompressor.ts
42
44
  function decompressShape(compressed, numSamples) {
@@ -103,23 +105,27 @@ var Pulseq = (() => {
103
105
 
104
106
  // src/pulseq/reader.ts
105
107
  function parseSequenceText(text) {
106
- const lines = text.split(/\r?\n/);
107
108
  const seq = createEmptySequence();
108
109
  const seenSections = /* @__PURE__ */ new Set();
110
+ const shapeParser = new ShapeSectionParser(seq);
109
111
  let sectionName = null;
110
112
  let sectionLines = [];
111
- for (const line of lines) {
113
+ forEachLine(text, (line) => {
112
114
  const m = line.match(/^\[(\w+)\]$/);
113
115
  if (m) {
114
- if (sectionName) dispatchSection(seq, sectionName, sectionLines);
116
+ if (sectionName === "SHAPES") shapeParser.finish();
117
+ else if (sectionName) dispatchSection(seq, sectionName, sectionLines);
115
118
  sectionName = m[1];
116
119
  seenSections.add(sectionName);
117
120
  sectionLines = [];
121
+ } else if (sectionName === "SHAPES") {
122
+ shapeParser.consume(line);
118
123
  } else {
119
124
  sectionLines.push(line);
120
125
  }
121
- }
122
- if (sectionName) dispatchSection(seq, sectionName, sectionLines);
126
+ });
127
+ if (sectionName === "SHAPES") shapeParser.finish();
128
+ else if (sectionName) dispatchSection(seq, sectionName, sectionLines);
123
129
  seq.versionCombined = makeVersionCombined(
124
130
  seq.version.major,
125
131
  seq.version.minor,
@@ -129,7 +135,22 @@ var Pulseq = (() => {
129
135
  validateSequence(seq, seenSections);
130
136
  return seq;
131
137
  }
138
+ function forEachLine(text, visit) {
139
+ let start = 0;
140
+ while (start <= text.length) {
141
+ let end = text.indexOf("\n", start);
142
+ if (end < 0) end = text.length;
143
+ const contentEnd = end > start && text.charCodeAt(end - 1) === 13 ? end - 1 : end;
144
+ visit(text.slice(start, contentEnd));
145
+ if (end === text.length) break;
146
+ start = end + 1;
147
+ }
148
+ }
132
149
  function dispatchSection(seq, name, lines) {
150
+ if (name === "SHAPES") {
151
+ parseShapes(seq, lines);
152
+ return;
153
+ }
133
154
  const valid = lines.filter((l) => {
134
155
  const t = l.trim();
135
156
  return t && !t.startsWith("#");
@@ -159,9 +180,6 @@ var Pulseq = (() => {
159
180
  case "EXTENSIONS":
160
181
  parseExtensions(seq, valid);
161
182
  break;
162
- case "SHAPES":
163
- parseShapes(seq, lines);
164
- break;
165
183
  }
166
184
  }
167
185
  function createEmptySequence() {
@@ -656,58 +674,71 @@ var Pulseq = (() => {
656
674
  }
657
675
  }
658
676
  function parseShapes(seq, lines) {
659
- let i = 0;
660
- while (i < lines.length) {
661
- const t = lines[i].trim();
662
- if (!t || t.startsWith("#") || t.startsWith("[")) {
663
- i++;
664
- continue;
677
+ const parser = new ShapeSectionParser(seq);
678
+ for (const line of lines) parser.consume(line);
679
+ parser.finish();
680
+ }
681
+ var ShapeSectionParser = class {
682
+ constructor(seq) {
683
+ __publicField(this, "seq", seq);
684
+ __publicField(this, "shapeId", 0);
685
+ __publicField(this, "numSamples", 0);
686
+ __publicField(this, "raw", new Float64Array());
687
+ __publicField(this, "rawCount", 0);
688
+ }
689
+ consume(line) {
690
+ const trimmed = line.trim();
691
+ if (!trimmed || trimmed.startsWith("#")) return;
692
+ const shapeMatch = /^shape_id\s+(\d+)/.exec(trimmed);
693
+ if (shapeMatch) {
694
+ this.storeCurrent();
695
+ this.shapeId = Number(shapeMatch[1]);
696
+ return;
665
697
  }
666
- const m = t.match(/^shape_id\s+(\d+)/);
667
- if (!m) {
668
- i++;
669
- continue;
698
+ const countMatch = /^num_samples\s+(\d+)/.exec(trimmed);
699
+ if (countMatch) {
700
+ this.numSamples = Number(countMatch[1]);
701
+ this.raw = new Float64Array(Math.min(this.numSamples, 1024));
702
+ this.rawCount = 0;
703
+ return;
670
704
  }
671
- const shapeId = +m[1];
672
- i++;
673
- let numSamples = 0;
674
- while (i < lines.length) {
675
- const l = lines[i].trim();
676
- if (!l || l.startsWith("#")) {
677
- i++;
678
- continue;
679
- }
680
- const nm = l.match(/^num_samples\s+(\d+)/);
681
- if (nm) {
682
- numSamples = +nm[1];
683
- i++;
684
- break;
685
- }
686
- if (l.match(/^shape_id\s+\d+/) || l.startsWith("[")) break;
687
- i++;
705
+ if (this.shapeId <= 0 || this.numSamples <= 0 || this.rawCount >= this.numSamples) return;
706
+ if (!/\s/.test(trimmed)) {
707
+ this.appendRawValue(trimmed);
708
+ return;
688
709
  }
689
- if (numSamples <= 0) continue;
690
- const vals = [];
691
- while (i < lines.length && vals.length < numSamples) {
692
- const l = lines[i].trim();
693
- if (l.match(/^shape_id\s+\d+/) || l.startsWith("[")) break;
694
- if (!l || l.startsWith("#")) {
695
- i++;
696
- continue;
697
- }
698
- for (const n of l.split(/\s+/).map(Number).filter((x) => !isNaN(x))) {
699
- if (vals.length < numSamples) vals.push(n);
700
- }
701
- i++;
710
+ for (const field of trimmed.split(/\s+/)) {
711
+ this.appendRawValue(field);
712
+ if (this.rawCount >= this.numSamples) break;
702
713
  }
703
- if (vals.length === 0) continue;
704
- storeShape(seq, shapeId, numSamples, vals);
705
714
  }
706
- }
707
- function storeShape(seq, id, num, raw) {
708
- const decompressed = raw.length === num ? new Float64Array(raw) : decompressShape(raw, num);
709
- seq.shapes.set(id, { numSamples: num, samples: decompressed });
710
- }
715
+ finish() {
716
+ this.storeCurrent();
717
+ }
718
+ ensureRawCapacity() {
719
+ if (this.rawCount < this.raw.length) return;
720
+ const nextLength = Math.min(this.numSamples, Math.max(1, this.raw.length * 2));
721
+ const expanded = new Float64Array(nextLength);
722
+ expanded.set(this.raw);
723
+ this.raw = expanded;
724
+ }
725
+ appendRawValue(field) {
726
+ const value = Number(field);
727
+ if (!Number.isFinite(value)) return;
728
+ this.ensureRawCapacity();
729
+ this.raw[this.rawCount++] = value;
730
+ }
731
+ storeCurrent() {
732
+ if (this.shapeId > 0 && this.numSamples > 0 && this.rawCount > 0) {
733
+ const samples = this.rawCount === this.numSamples ? this.raw : decompressShape(this.raw.subarray(0, this.rawCount), this.numSamples);
734
+ this.seq.shapes.set(this.shapeId, { numSamples: this.numSamples, samples });
735
+ }
736
+ this.shapeId = 0;
737
+ this.numSamples = 0;
738
+ this.raw = new Float64Array();
739
+ this.rawCount = 0;
740
+ }
741
+ };
711
742
  function extractRasterTimes(seq) {
712
743
  const set = (key, field) => {
713
744
  const v = seq.definitions.get(key);
@@ -1499,40 +1530,26 @@ var Pulseq = (() => {
1499
1530
  return mode === "observationTime" ? "observationTime" : "rfCenter";
1500
1531
  }
1501
1532
  function collectGradientSeries(blocks, channel) {
1502
- const time = [];
1503
- const value = [];
1533
+ const series = { time: [], value: [] };
1504
1534
  for (const block of blocks) {
1505
1535
  const grad = block[channel];
1506
1536
  if (!grad?.timePoints || !grad.waveform) continue;
1507
1537
  const n = Math.min(grad.timePoints.length, grad.waveform.length);
1508
1538
  for (let i = 0; i < n; i++) {
1509
- time.push(grad.timePoints[i]);
1510
- value.push(grad.waveform[i]);
1539
+ appendGradientPoint(series, grad.timePoints[i], grad.waveform[i]);
1511
1540
  }
1512
1541
  }
1513
- return sanitizeGradientSeries(time, value);
1542
+ return series;
1514
1543
  }
1515
- function sanitizeGradientSeries(time, value) {
1516
- const pairs = [];
1517
- const n = Math.min(time.length, value.length);
1518
- for (let i = 0; i < n; i++) {
1519
- const t = time[i];
1520
- const v = value[i];
1521
- if (Number.isFinite(t) && Number.isFinite(v)) pairs.push([t, v]);
1522
- }
1523
- pairs.sort((a, b) => a[0] - b[0]);
1524
- const outT = [];
1525
- const outV = [];
1526
- for (const [t, v] of pairs) {
1527
- const last = outT.length - 1;
1528
- if (last >= 0 && Math.abs(t - outT[last]) <= TIME_EPS) {
1529
- outV[last] = 0.5 * (outV[last] + v);
1530
- continue;
1531
- }
1532
- outT.push(t);
1533
- outV.push(v);
1544
+ function appendGradientPoint(series, t, value) {
1545
+ if (!Number.isFinite(t) || !Number.isFinite(value)) return;
1546
+ const last = series.time.length - 1;
1547
+ if (last >= 0 && Math.abs(t - series.time[last]) <= TIME_EPS) {
1548
+ series.value[last] = 0.5 * (series.value[last] + value);
1549
+ } else if (last < 0 || t > series.time[last]) {
1550
+ series.time.push(t);
1551
+ series.value.push(value);
1534
1552
  }
1535
- return { time: outT, value: outV };
1536
1553
  }
1537
1554
  function collectRfEvents(blocks, warnings) {
1538
1555
  const events = [];
@@ -1589,6 +1606,26 @@ var Pulseq = (() => {
1589
1606
  let currentT = tReset;
1590
1607
  let unsignedM0 = 0;
1591
1608
  let unsignedM1 = 0;
1609
+ let gradientIndex = -1;
1610
+ const seekGradient = (t) => {
1611
+ while (gradientIndex + 1 < gradient.time.length && gradient.time[gradientIndex + 1] <= t + TIME_EPS) {
1612
+ gradientIndex++;
1613
+ }
1614
+ };
1615
+ const sampleGradient = (t) => {
1616
+ const n = gradient.time.length;
1617
+ if (n === 0 || t < gradient.time[0] - TIME_EPS || t > gradient.time[n - 1] + TIME_EPS) return 0;
1618
+ seekGradient(t);
1619
+ if (gradientIndex < 0) return 0;
1620
+ if (gradientIndex >= n - 1 || Math.abs(t - gradient.time[gradientIndex]) <= TIME_EPS) {
1621
+ return gradient.value[gradientIndex];
1622
+ }
1623
+ const t0 = gradient.time[gradientIndex];
1624
+ const t1 = gradient.time[gradientIndex + 1];
1625
+ if (!(t1 > t0)) return gradient.value[gradientIndex];
1626
+ const alpha = (t - t0) / (t1 - t0);
1627
+ return gradient.value[gradientIndex] + alpha * (gradient.value[gradientIndex + 1] - gradient.value[gradientIndex]);
1628
+ };
1592
1629
  const reportedM1At = (t) => {
1593
1630
  if (referenceMode === "observationTime") return sign * (unsignedM1 - (t - tReset) * unsignedM0);
1594
1631
  return sign * unsignedM1;
@@ -1596,10 +1633,11 @@ var Pulseq = (() => {
1596
1633
  const advanceTo = (targetT) => {
1597
1634
  if (!(targetT > currentT + TIME_EPS)) return;
1598
1635
  while (currentT < targetT - TIME_EPS) {
1599
- let nextT = nextGradientBreakpoint(gradient.time, currentT, targetT);
1636
+ seekGradient(currentT);
1637
+ let nextT = gradientIndex + 1 < gradient.time.length ? Math.min(targetT, gradient.time[gradientIndex + 1]) : targetT;
1600
1638
  if (!(nextT > currentT)) nextT = targetT;
1601
- const ga = sampleGradientAt(gradient, currentT);
1602
- const gb = sampleGradientAt(gradient, nextT);
1639
+ const ga = sampleGradient(currentT);
1640
+ const gb = sampleGradient(nextT);
1603
1641
  const [m0Seg, m1Seg] = integrateLinearSegment(currentT, nextT, tReset, ga, gb);
1604
1642
  unsignedM0 += m0Seg;
1605
1643
  unsignedM1 += m1Seg;
@@ -1641,36 +1679,6 @@ var Pulseq = (() => {
1641
1679
  }
1642
1680
  return { t: outT, m1: outM1 };
1643
1681
  }
1644
- function sampleGradientAt(gradient, t) {
1645
- const n = gradient.time.length;
1646
- if (n <= 0 || t < gradient.time[0] || t > gradient.time[n - 1]) return 0;
1647
- if (n === 1 || t <= gradient.time[0]) return gradient.value[0];
1648
- if (t >= gradient.time[n - 1]) return gradient.value[n - 1];
1649
- let lo = 0;
1650
- let hi = n - 1;
1651
- while (hi - lo > 1) {
1652
- const mid = lo + hi >> 1;
1653
- if (gradient.time[mid] <= t) lo = mid;
1654
- else hi = mid;
1655
- }
1656
- const t0 = gradient.time[lo];
1657
- const t1 = gradient.time[hi];
1658
- if (!(t1 > t0)) return gradient.value[lo];
1659
- const alpha = (t - t0) / (t1 - t0);
1660
- return gradient.value[lo] + alpha * (gradient.value[hi] - gradient.value[lo]);
1661
- }
1662
- function nextGradientBreakpoint(times, t, target) {
1663
- if (times.length <= 1 || t >= times[times.length - 1]) return target;
1664
- let lo = 0;
1665
- let hi = times.length;
1666
- const threshold = t + TIME_EPS;
1667
- while (lo < hi) {
1668
- const mid = lo + hi >> 1;
1669
- if (times[mid] <= threshold) lo = mid + 1;
1670
- else hi = mid;
1671
- }
1672
- return lo < times.length ? Math.min(target, times[lo]) : target;
1673
- }
1674
1682
  function integrateLinearSegment(a, b, tRef, ga, gb) {
1675
1683
  const h = b - a;
1676
1684
  if (!(h > 0)) return [0, 0];
@@ -1757,17 +1765,6 @@ var Pulseq = (() => {
1757
1765
  if (ntMax < ntMin) return invalidPns("Unable to build regular PNS raster.");
1758
1766
  const nSamples = Math.floor(ntMax - ntMin + 1);
1759
1767
  if (nSamples < 2) return invalidPns("Too few samples for PNS computation.");
1760
- const tAxis = new Float64Array(nSamples);
1761
- const gxTpm = new Float64Array(nSamples);
1762
- const gyTpm = new Float64Array(nSamples);
1763
- const gzTpm = new Float64Array(nSamples);
1764
- for (let i = 0; i < nSamples; i++) {
1765
- const tSec = (ntMin + i) * dtSec;
1766
- tAxis[i] = tSec;
1767
- gxTpm[i] = interpLinearZero(waves[0], tSec) / gammaHzPerT;
1768
- gyTpm[i] = interpLinearZero(waves[1], tSec) / gammaHzPerT;
1769
- gzTpm[i] = interpLinearZero(waves[2], tSec) / gammaHzPerT;
1770
- }
1771
1768
  const longestTauMs = Math.max(
1772
1769
  hardware.x.tau1Ms,
1773
1770
  hardware.x.tau2Ms,
@@ -1782,68 +1779,78 @@ var Pulseq = (() => {
1782
1779
  const zptSec = longestTauMs * 4 / 1e3;
1783
1780
  const preCount = Math.max(0, Math.round(zptSec / (4 * dtSec)));
1784
1781
  const postCount = Math.max(0, Math.round(zptSec / dtSec));
1785
- const gxPadded = padSamples(gxTpm, preCount, postCount);
1786
- const gyPadded = padSamples(gyTpm, preCount, postCount);
1787
- const gzPadded = padSamples(gzTpm, preCount, postCount);
1788
- const stimX = safePnsModel(diff(gxPadded, dtSec), dtSec, hardware.x);
1789
- const stimY = safePnsModel(diff(gyPadded, dtSec), dtSec, hardware.y);
1790
- const stimZ = safePnsModel(diff(gzPadded, dtSec), dtSec, hardware.z);
1782
+ const stimX = calculatePnsAxis(waves[0], ntMin, nSamples, preCount, postCount, dtSec, gammaHzPerT, hardware.x);
1783
+ const stimY = calculatePnsAxis(waves[1], ntMin, nSamples, preCount, postCount, dtSec, gammaHzPerT, hardware.y);
1784
+ const stimZ = calculatePnsAxis(waves[2], ntMin, nSamples, preCount, postCount, dtSec, gammaHzPerT, hardware.z);
1791
1785
  const hasAnyNonTrap = blocks.some((block) => block.gx?.type === "arb" || block.gy?.type === "arb" || block.gz?.type === "arb");
1792
1786
  const hasAnyLabelExt = blocks.some((block) => !!(block.labelSets?.length || block.labelIncs?.length));
1793
1787
  const shift = hasAnyNonTrap || hasAnyLabelExt ? 1 : 0;
1794
- const selectedX = [];
1795
- const selectedY = [];
1796
- const selectedZ = [];
1797
- const selectedT = [];
1788
+ let selectedCount = 0;
1798
1789
  for (let origIdx = 0; origIdx < nSamples; origIdx++) {
1799
1790
  const paddedIdx = preCount + origIdx;
1800
1791
  let stimIdx = paddedIdx - shift;
1801
- if (shift > 0 && hasAnyLabelExt && origIdx === tAxis.length - 1) {
1792
+ if (shift > 0 && hasAnyLabelExt && origIdx === nSamples - 1) {
1802
1793
  stimIdx = Math.min(paddedIdx, stimX.length - 1);
1803
1794
  }
1804
1795
  if (stimIdx < 0 || stimIdx >= stimX.length || stimIdx >= stimY.length || stimIdx >= stimZ.length) continue;
1805
- selectedX.push(stimX[stimIdx]);
1806
- selectedY.push(stimY[stimIdx]);
1807
- selectedZ.push(stimZ[stimIdx]);
1808
- selectedT.push(tAxis[origIdx]);
1809
- }
1810
- const timeSec = new Float64Array(selectedX.length);
1811
- const pnsX = new Float64Array(selectedX.length);
1812
- const pnsY = new Float64Array(selectedX.length);
1813
- const pnsZ = new Float64Array(selectedX.length);
1814
- const pnsNorm = new Float64Array(selectedX.length);
1796
+ selectedCount++;
1797
+ }
1798
+ const timeSec = new Float64Array(selectedCount);
1799
+ const pnsX = new Float64Array(selectedCount);
1800
+ const pnsY = new Float64Array(selectedCount);
1801
+ const pnsZ = new Float64Array(selectedCount);
1802
+ const pnsNorm = new Float64Array(selectedCount);
1815
1803
  let ok = true;
1816
- for (let i = 0; i < selectedX.length; i++) {
1817
- const xNorm = 0.01 * selectedX[i];
1818
- const yNorm = 0.01 * selectedY[i];
1819
- const zNorm = 0.01 * selectedZ[i];
1804
+ let selectedIndex = 0;
1805
+ for (let origIdx = 0; origIdx < nSamples; origIdx++) {
1806
+ const paddedIdx = preCount + origIdx;
1807
+ let stimIdx = paddedIdx - shift;
1808
+ if (shift > 0 && hasAnyLabelExt && origIdx === nSamples - 1) {
1809
+ stimIdx = Math.min(paddedIdx, stimX.length - 1);
1810
+ }
1811
+ if (stimIdx < 0 || stimIdx >= stimX.length || stimIdx >= stimY.length || stimIdx >= stimZ.length) continue;
1812
+ const xNorm = 0.01 * stimX[stimIdx];
1813
+ const yNorm = 0.01 * stimY[stimIdx];
1814
+ const zNorm = 0.01 * stimZ[stimIdx];
1820
1815
  const norm = Math.sqrt(xNorm * xNorm + yNorm * yNorm + zNorm * zNorm);
1821
- timeSec[i] = selectedT[i];
1822
- pnsX[i] = xNorm;
1823
- pnsY[i] = yNorm;
1824
- pnsZ[i] = zNorm;
1825
- pnsNorm[i] = norm;
1816
+ timeSec[selectedIndex] = (ntMin + origIdx) * dtSec;
1817
+ pnsX[selectedIndex] = xNorm;
1818
+ pnsY[selectedIndex] = yNorm;
1819
+ pnsZ[selectedIndex] = zNorm;
1820
+ pnsNorm[selectedIndex] = norm;
1826
1821
  if (norm >= 1) ok = false;
1822
+ selectedIndex++;
1827
1823
  }
1828
1824
  return { valid: true, ok, timeSec, pnsX, pnsY, pnsZ, pnsNorm };
1829
1825
  }
1830
1826
  function safePnsModel(dgdt, dtSec, hw) {
1831
- const absDgdt = new Float64Array(dgdt.length);
1832
- for (let i = 0; i < dgdt.length; i++) absDgdt[i] = Math.abs(dgdt[i]);
1827
+ return runPnsModel(dgdt.length, (index) => dgdt[index], dtSec, hw);
1828
+ }
1829
+ function runPnsModel(length, derivativeAt, dtSec, hw) {
1833
1830
  const dtMs = dtSec * 1e3;
1834
- const lp1 = lowpassTau(dgdt, hw.tau1Ms, dtMs);
1835
- const lp2 = lowpassTau(absDgdt, hw.tau2Ms, dtMs);
1836
- const lp3 = lowpassTau(dgdt, hw.tau3Ms, dtMs);
1837
- const stim = new Float64Array(dgdt.length);
1831
+ const alpha1 = lowpassAlpha(hw.tau1Ms, dtMs);
1832
+ const alpha2 = lowpassAlpha(hw.tau2Ms, dtMs);
1833
+ const alpha3 = lowpassAlpha(hw.tau3Ms, dtMs);
1834
+ const stim = new Float64Array(length);
1838
1835
  const denom = hw.stimLimit > 0 ? hw.stimLimit : 1;
1839
- for (let i = 0; i < dgdt.length; i++) {
1840
- const s1 = hw.a1 * Math.abs(lp1[i]);
1841
- const s2 = hw.a2 * lp2[i];
1842
- const s3 = hw.a3 * Math.abs(lp3[i]);
1836
+ let lp1 = 0;
1837
+ let lp2 = 0;
1838
+ let lp3 = 0;
1839
+ for (let i = 0; i < length; i++) {
1840
+ const derivative = derivativeAt(i);
1841
+ lp1 = alpha1 * derivative + (1 - alpha1) * lp1;
1842
+ lp2 = alpha2 * Math.abs(derivative) + (1 - alpha2) * lp2;
1843
+ lp3 = alpha3 * derivative + (1 - alpha3) * lp3;
1844
+ const s1 = hw.a1 * Math.abs(lp1);
1845
+ const s2 = hw.a2 * lp2;
1846
+ const s3 = hw.a3 * Math.abs(lp3);
1843
1847
  stim[i] = (s1 + s2 + s3) / denom * hw.gScale * 100;
1844
1848
  }
1845
1849
  return stim;
1846
1850
  }
1851
+ function lowpassAlpha(tauMs, dtMs) {
1852
+ return tauMs <= 0 || dtMs <= 0 ? 1 : dtMs / (tauMs + dtMs);
1853
+ }
1847
1854
  function invalidPns(error) {
1848
1855
  return {
1849
1856
  valid: false,
@@ -1939,79 +1946,58 @@ var Pulseq = (() => {
1939
1946
  function hasValidWeights(hw) {
1940
1947
  return Math.abs(hw.a1 + hw.a2 + hw.a3 - 1) <= 0.01 && hw.stimLimit > 0;
1941
1948
  }
1942
- function lowpassTau(input, tauMs, dtMs) {
1943
- const out = new Float64Array(input.length);
1944
- if (!input.length) return out;
1945
- if (tauMs <= 0 || dtMs <= 0) {
1946
- out.set(input);
1947
- return out;
1948
- }
1949
- const alpha = dtMs / (tauMs + dtMs);
1950
- out[0] = alpha * input[0];
1951
- for (let i = 1; i < input.length; i++) out[i] = alpha * input[i] + (1 - alpha) * out[i - 1];
1952
- return out;
1953
- }
1954
1949
  function collectGradientSeries2(blocks, channel) {
1955
- const time = [];
1956
- const value = [];
1950
+ const series = { time: [], value: [] };
1957
1951
  for (const block of blocks) {
1958
1952
  const grad = block[channel];
1959
1953
  if (!grad?.timePoints || !grad.waveform) continue;
1960
1954
  const n = Math.min(grad.timePoints.length, grad.waveform.length);
1961
1955
  for (let i = 0; i < n; i++) {
1962
- time.push(grad.timePoints[i]);
1963
- value.push(grad.waveform[i]);
1956
+ appendGradientPoint2(series, grad.timePoints[i], grad.waveform[i]);
1964
1957
  }
1965
1958
  }
1966
- return sanitizeGradientSeries2(time, value);
1967
- }
1968
- function sanitizeGradientSeries2(time, value) {
1969
- const pairs = [];
1970
- const n = Math.min(time.length, value.length);
1971
- for (let i = 0; i < n; i++) {
1972
- if (Number.isFinite(time[i]) && Number.isFinite(value[i])) pairs.push([time[i], value[i]]);
1973
- }
1974
- pairs.sort((a, b) => a[0] - b[0]);
1975
- const outT = [];
1976
- const outV = [];
1977
- for (const [t, v] of pairs) {
1978
- const last = outT.length - 1;
1979
- if (last >= 0 && Math.abs(t - outT[last]) <= TIME_EPS2) {
1980
- outV[last] = 0.5 * (outV[last] + v);
1981
- continue;
1982
- }
1983
- outT.push(t);
1984
- outV.push(v);
1985
- }
1986
- return { time: outT, value: outV };
1987
- }
1988
- function interpLinearZero(series, t) {
1989
- const n = series.time.length;
1990
- if (!n || t < series.time[0] || t > series.time[n - 1]) return 0;
1991
- if (n === 1 || t <= series.time[0]) return series.value[0];
1992
- if (t >= series.time[n - 1]) return series.value[n - 1];
1993
- let lo = 0;
1994
- let hi = n - 1;
1995
- while (hi - lo > 1) {
1996
- const mid = lo + hi >> 1;
1997
- if (series.time[mid] <= t) lo = mid;
1998
- else hi = mid;
1999
- }
2000
- const t0 = series.time[lo];
2001
- const t1 = series.time[hi];
2002
- if (!(t1 > t0)) return series.value[lo];
2003
- const alpha = (t - t0) / (t1 - t0);
2004
- return series.value[lo] + alpha * (series.value[hi] - series.value[lo]);
2005
- }
2006
- function padSamples(input, preCount, postCount) {
2007
- const out = new Float64Array(preCount + input.length + postCount);
2008
- out.set(input, preCount);
2009
- return out;
2010
- }
2011
- function diff(input, dtSec) {
2012
- const out = new Float64Array(Math.max(0, input.length - 1));
2013
- for (let i = 0; i < out.length; i++) out[i] = (input[i + 1] - input[i]) / dtSec;
2014
- return out;
1959
+ return series;
1960
+ }
1961
+ function appendGradientPoint2(series, t, value) {
1962
+ if (!Number.isFinite(t) || !Number.isFinite(value)) return;
1963
+ const last = series.time.length - 1;
1964
+ if (last >= 0 && Math.abs(t - series.time[last]) <= TIME_EPS2) {
1965
+ series.value[last] = 0.5 * (series.value[last] + value);
1966
+ } else if (last < 0 || t > series.time[last]) {
1967
+ series.time.push(t);
1968
+ series.value.push(value);
1969
+ }
1970
+ }
1971
+ function calculatePnsAxis(series, ntMin, nSamples, preCount, postCount, dtSec, gammaHzPerT, hardware) {
1972
+ const sampleGradient = createGradientSampler(series);
1973
+ const totalSamples = preCount + nSamples + postCount;
1974
+ const paddedValue = (index) => {
1975
+ if (index < preCount || index >= preCount + nSamples) return 0;
1976
+ const rasterIndex = index - preCount;
1977
+ return sampleGradient((ntMin + rasterIndex) * dtSec) / gammaHzPerT;
1978
+ };
1979
+ let previous = paddedValue(0);
1980
+ return runPnsModel(Math.max(0, totalSamples - 1), (index) => {
1981
+ const current = paddedValue(index + 1);
1982
+ const derivative = (current - previous) / dtSec;
1983
+ previous = current;
1984
+ return derivative;
1985
+ }, dtSec, hardware);
1986
+ }
1987
+ function createGradientSampler(series) {
1988
+ let index = -1;
1989
+ return (t) => {
1990
+ const n = series.time.length;
1991
+ 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++;
1993
+ if (index < 0) return 0;
1994
+ if (index >= n - 1 || t <= series.time[index] + TIME_EPS2) return series.value[index];
1995
+ const t0 = series.time[index];
1996
+ const t1 = series.time[index + 1];
1997
+ if (!(t1 > t0)) return series.value[index];
1998
+ const alpha = (t - t0) / (t1 - t0);
1999
+ return series.value[index] + alpha * (series.value[index + 1] - series.value[index]);
2000
+ };
2015
2001
  }
2016
2002
 
2017
2003
  // src/pulseq/trdetect.ts
@@ -2190,11 +2176,7 @@ var Pulseq = (() => {
2190
2176
  if (intervals.length === 0) return 0;
2191
2177
  intervals.sort((a, b) => a - b);
2192
2178
  const median = intervals[Math.floor(intervals.length / 2)];
2193
- const niceMs = niceRound(median * 1e3, 10);
2194
- return niceMs * 1e-3;
2195
- }
2196
- function niceRound(value, base) {
2197
- return Math.round(value / base) * base;
2179
+ return Math.round(median * 1e6) / 1e6;
2198
2180
  }
2199
2181
 
2200
2182
  // src/pulseq/kspaceExportArtifacts.ts
@@ -163,6 +163,7 @@ function scheduleViewerDraw(includeMinimap){
163
163
  // ── Unit conversion ──
164
164
  function gradConv(v){if(gradUnit==='mT/m')return v/GAMMA;if(gradUnit==='G/cm')return v/(GAMMA*10);return v;}
165
165
  function gradUnitStr(){return gradUnit;}
166
+ function normalizeGradUnit(unit){return(unit==='mT/m'||unit==='G/cm'||unit==='Hz/m')?unit:'Hz/m';}
166
167
  function timeConv(t){if(timeUnit==='ms')return t*1e3;if(timeUnit==='us')return t*1e6;return t;}
167
168
  function timeUnitStr(){return timeUnit;}
168
169
 
@@ -1519,7 +1520,7 @@ function loadSequenceText(rawText) {
1519
1520
  // ── Apply display options from Python host ──
1520
1521
  if (window.SEQEYES_SHOW_BLOCKS) { showBB = true; document.getElementById('bbc').checked = true; }
1521
1522
  if (window.SEQEYES_TIME_DISP) { timeUnit = window.SEQEYES_TIME_DISP; tuSel.value = timeUnit; }
1522
- if (window.SEQEYES_GRAD_DISP) { gradUnit = window.SEQEYES_GRAD_DISP; guSel.value = gradUnit; }
1523
+ if (window.SEQEYES_GRAD_DISP) { gradUnit = normalizeGradUnit(window.SEQEYES_GRAD_DISP); guSel.value = gradUnit; }
1523
1524
  // Apply time_range if specified
1524
1525
  if (window.SEQEYES_TIME_RANGE && window.SEQEYES_TIME_RANGE.length === 2) {
1525
1526
  var tr0 = window.SEQEYES_TIME_RANGE[0], tr1 = window.SEQEYES_TIME_RANGE[1];
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: seqeyes-python
3
- Version: 0.2.1
3
+ Version: 0.2.2
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
@@ -0,0 +1,8 @@
1
+ seqeyes/__init__.py,sha256=gLG6iHqBTpn2e8O8Xo-S4maZZO7yvGlbavEKMH86bQo,944
2
+ seqeyes/_plot.py,sha256=v5cPAn3mO017lQlNR4bthC0QgIBK1YxjygJTAPFUz1E,8114
3
+ seqeyes/_renderer.py,sha256=da7cK65iqRt9vMkrVE80d9VFL05a0BAzFvW7oXlbT_s,8441
4
+ seqeyes/resources/pulseq-bundle.js,sha256=Np9sE0OWxgLHlZ43TOpzWzwuYUGmCtBluXwdxZs8XpA,83126
5
+ seqeyes/resources/viewer.html,sha256=EUiBO49YWrrLu6cB6wA8SFA6EXG0WwGnbUk6CbkgLfs,86852
6
+ seqeyes_python-0.2.2.dist-info/METADATA,sha256=MryZDrpaLUgWKvNLCIPRgX15jl1Pfu3VxZoTHFMdfcA,4179
7
+ seqeyes_python-0.2.2.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
8
+ seqeyes_python-0.2.2.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- seqeyes/__init__.py,sha256=VKds1tbL8Bhb8kdEvgKgi9evTeDwV7QXyQCDTuxPgTg,944
2
- seqeyes/_plot.py,sha256=au0R_gzCWqrSkGUuTtD3a8azG91oLIrNnoIr1ekOEag,7438
3
- seqeyes/_renderer.py,sha256=f3xfV2t_lSQaTpDvVj7Hfy_lVRffuYu_m12VtVVQTFU,8205
4
- seqeyes/resources/pulseq-bundle.js,sha256=1yGGUCm28Pmb0r7ZkIDjtpfQEMwARqfzU78P9x2SDMQ,82228
5
- seqeyes/resources/viewer.html,sha256=po8dc7dya0pyAxMU0ckXFoicyL43I6FU8kDsPIMOVbM,86734
6
- seqeyes_python-0.2.1.dist-info/METADATA,sha256=XnrUBC1qtDlOErKN-0zTxipgY8SnrLY3-1jCyl-Q33I,4179
7
- seqeyes_python-0.2.1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
8
- seqeyes_python-0.2.1.dist-info/RECORD,,