seqeyes-python 0.2.0__tar.gz → 0.2.2__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.0 → seqeyes_python-0.2.2}/PKG-INFO +1 -1
- {seqeyes_python-0.2.0 → seqeyes_python-0.2.2}/examples/demo.ipynb +6 -6
- {seqeyes_python-0.2.0 → seqeyes_python-0.2.2}/examples/demo.py +1 -1
- {seqeyes_python-0.2.0 → seqeyes_python-0.2.2}/pyproject.toml +1 -1
- {seqeyes_python-0.2.0 → seqeyes_python-0.2.2}/src/seqeyes/__init__.py +1 -1
- {seqeyes_python-0.2.0 → seqeyes_python-0.2.2}/src/seqeyes/_plot.py +16 -3
- {seqeyes_python-0.2.0 → seqeyes_python-0.2.2}/src/seqeyes/_renderer.py +12 -6
- {seqeyes_python-0.2.0 → seqeyes_python-0.2.2}/src/seqeyes/resources/pulseq-bundle.js +217 -235
- {seqeyes_python-0.2.0 → seqeyes_python-0.2.2}/src/seqeyes/resources/viewer.html +2 -1
- {seqeyes_python-0.2.0 → seqeyes_python-0.2.2}/tests/integration_test.py +1 -1
- {seqeyes_python-0.2.0 → seqeyes_python-0.2.2}/tests/test_plot.py +31 -0
- {seqeyes_python-0.2.0 → seqeyes_python-0.2.2}/.gitignore +0 -0
- {seqeyes_python-0.2.0 → seqeyes_python-0.2.2}/README.md +0 -0
- {seqeyes_python-0.2.0 → seqeyes_python-0.2.2}/tests/conftest.py +0 -0
- {seqeyes_python-0.2.0 → seqeyes_python-0.2.2}/tests/debug_full.html +0 -0
- {seqeyes_python-0.2.0 → seqeyes_python-0.2.2}/tests/debug_minimal.html +0 -0
- {seqeyes_python-0.2.0 → seqeyes_python-0.2.2}/tests/debug_viewer.html +0 -0
- {seqeyes_python-0.2.0 → seqeyes_python-0.2.2}/tests/debug_viewer.py +0 -0
- {seqeyes_python-0.2.0 → seqeyes_python-0.2.2}/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.
|
|
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
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"import pypulseq as pp\n",
|
|
54
54
|
"\n",
|
|
55
55
|
"# ── Enable SeqEyes (once per session) ──\n",
|
|
56
|
-
"seqeyes.set(time_disp=\"ms\", grad_disp=\"
|
|
56
|
+
"seqeyes.set(time_disp=\"ms\", grad_disp=\"Hz/m\")\n",
|
|
57
57
|
"print(f\"SeqEyes {seqeyes.__version__} — seq.plot() is now interactive!\")\n",
|
|
58
58
|
"\n",
|
|
59
59
|
"# ═══════════════════════════════════════════════════════════════════════\n",
|
|
@@ -225,7 +225,7 @@
|
|
|
225
225
|
}
|
|
226
226
|
],
|
|
227
227
|
"source": [
|
|
228
|
-
"seq_gre.plot(time_range=(0, tr_gre), time_disp=\"ms\", grad_disp=\"
|
|
228
|
+
"seq_gre.plot(time_range=(0, tr_gre), time_disp=\"ms\", grad_disp=\"Hz/m\", theme=\"dark\")"
|
|
229
229
|
]
|
|
230
230
|
},
|
|
231
231
|
{
|
|
@@ -259,7 +259,7 @@
|
|
|
259
259
|
}
|
|
260
260
|
],
|
|
261
261
|
"source": [
|
|
262
|
-
"seq_epi.plot(time_disp=\"ms\", grad_disp=\"
|
|
262
|
+
"seq_epi.plot(time_disp=\"ms\", grad_disp=\"Hz/m\", theme=\"dark\")"
|
|
263
263
|
]
|
|
264
264
|
},
|
|
265
265
|
{
|
|
@@ -293,7 +293,7 @@
|
|
|
293
293
|
}
|
|
294
294
|
],
|
|
295
295
|
"source": [
|
|
296
|
-
"seq_rad.plot(time_disp=\"ms\", grad_disp=\"
|
|
296
|
+
"seq_rad.plot(time_disp=\"ms\", grad_disp=\"Hz/m\", theme=\"dark\")"
|
|
297
297
|
]
|
|
298
298
|
},
|
|
299
299
|
{
|
|
@@ -311,7 +311,7 @@
|
|
|
311
311
|
"| `show_blocks` | `False` | Show block‑boundary lines |\n",
|
|
312
312
|
"| `time_range` | `(0, inf)` | Zoom to `(start_sec, end_sec)` |\n",
|
|
313
313
|
"| `time_disp` | `\"s\"` | Time unit: `\"s\"`, `\"ms\"`, `\"us\"` |\n",
|
|
314
|
-
"| `grad_disp` | `\"
|
|
314
|
+
"| `grad_disp` | `\"Hz/m\"` | Gradient unit: `\"Hz/m\"`, `\"Hz/m\"`, `\"mT/m\"`, `\"G/cm\"` |\n",
|
|
315
315
|
"| `theme` | `\"system\"` | `\"light\"`, `\"dark\"`, `\"dracula\"`, `\"nord\"`, ... |"
|
|
316
316
|
]
|
|
317
317
|
},
|
|
@@ -336,7 +336,7 @@
|
|
|
336
336
|
],
|
|
337
337
|
"source": [
|
|
338
338
|
"# Zoom to first TR with block boundaries + custom units\n",
|
|
339
|
-
"seqeyes.set(show_blocks=True, time_disp=\"ms\", grad_disp=\"
|
|
339
|
+
"seqeyes.set(show_blocks=True, time_disp=\"ms\", grad_disp=\"Hz/m\")\n",
|
|
340
340
|
"seq_gre.plot(time_range=(0, tr_gre), theme=\"dark\")\n",
|
|
341
341
|
"seqeyes.reset() # restore defaults"
|
|
342
342
|
]
|
|
@@ -10,7 +10,7 @@ import numpy as np
|
|
|
10
10
|
import pypulseq as pp
|
|
11
11
|
|
|
12
12
|
# ── Enable SeqEyes (once per session) ─────────────────────────────────
|
|
13
|
-
seqeyes.set(time_disp="ms", grad_disp="
|
|
13
|
+
seqeyes.set(time_disp="ms", grad_disp="Hz/m")
|
|
14
14
|
print(f"SeqEyes {seqeyes.__version__} — seq.plot() is now interactive!")
|
|
15
15
|
|
|
16
16
|
|
|
@@ -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.2"
|
|
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"}
|
|
@@ -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 = "
|
|
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:
|
|
@@ -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 = "
|
|
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"``, ``"
|
|
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"``, ``"
|
|
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 = "
|
|
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.
|
|
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
|
-
|
|
113
|
+
forEachLine(text, (line) => {
|
|
112
114
|
const m = line.match(/^\[(\w+)\]$/);
|
|
113
115
|
if (m) {
|
|
114
|
-
if (sectionName)
|
|
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)
|
|
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
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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
|
|
667
|
-
if (
|
|
668
|
-
|
|
669
|
-
|
|
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
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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
|
|
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
|
-
|
|
1510
|
-
value.push(grad.waveform[i]);
|
|
1539
|
+
appendGradientPoint(series, grad.timePoints[i], grad.waveform[i]);
|
|
1511
1540
|
}
|
|
1512
1541
|
}
|
|
1513
|
-
return
|
|
1542
|
+
return series;
|
|
1514
1543
|
}
|
|
1515
|
-
function
|
|
1516
|
-
|
|
1517
|
-
const
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
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
|
-
|
|
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 =
|
|
1602
|
-
const gb =
|
|
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
|
|
1786
|
-
const
|
|
1787
|
-
const
|
|
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
|
-
|
|
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 ===
|
|
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
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
const
|
|
1811
|
-
const
|
|
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
|
-
|
|
1817
|
-
|
|
1818
|
-
const
|
|
1819
|
-
|
|
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[
|
|
1822
|
-
pnsX[
|
|
1823
|
-
pnsY[
|
|
1824
|
-
pnsZ[
|
|
1825
|
-
pnsNorm[
|
|
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
|
-
|
|
1832
|
-
|
|
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
|
|
1835
|
-
const
|
|
1836
|
-
const
|
|
1837
|
-
const stim = new Float64Array(
|
|
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
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
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
|
|
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
|
-
|
|
1963
|
-
value.push(grad.waveform[i]);
|
|
1956
|
+
appendGradientPoint2(series, grad.timePoints[i], grad.waveform[i]);
|
|
1964
1957
|
}
|
|
1965
1958
|
}
|
|
1966
|
-
return
|
|
1967
|
-
}
|
|
1968
|
-
function
|
|
1969
|
-
|
|
1970
|
-
const
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
}
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
}
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
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
|
-
|
|
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];
|
|
@@ -57,7 +57,7 @@ try:
|
|
|
57
57
|
import seqeyes
|
|
58
58
|
|
|
59
59
|
# Test set()
|
|
60
|
-
seqeyes.set(theme="dracula", time_disp="ms", grad_disp="
|
|
60
|
+
seqeyes.set(theme="dracula", time_disp="ms", grad_disp="Hz/m")
|
|
61
61
|
seqeyes.set(show_blocks=True) # update a single key
|
|
62
62
|
print("OK seqeyes.set() works")
|
|
63
63
|
|
|
@@ -85,6 +85,17 @@ class TestSeqEyesViewer:
|
|
|
85
85
|
assert '"ms"' in raw
|
|
86
86
|
assert '"mT/m"' in raw
|
|
87
87
|
|
|
88
|
+
def test_viewer_default_gradient_unit_is_hz_per_m(self):
|
|
89
|
+
v = SeqEyesViewer(SAMPLE_SEQ_TEXT)
|
|
90
|
+
raw = v.to_html(inject_bundle=False)
|
|
91
|
+
assert 'window.SEQEYES_GRAD_DISP = "Hz/m";' in raw
|
|
92
|
+
|
|
93
|
+
def test_viewer_normalizes_unsupported_gradient_unit(self):
|
|
94
|
+
v = SeqEyesViewer(SAMPLE_SEQ_TEXT, grad_disp="kHz/m")
|
|
95
|
+
raw = v.to_html(inject_bundle=False)
|
|
96
|
+
assert 'window.SEQEYES_GRAD_DISP = "Hz/m";' in raw
|
|
97
|
+
assert "kHz/m" not in raw
|
|
98
|
+
|
|
88
99
|
|
|
89
100
|
class TestSetAndReset:
|
|
90
101
|
"""Tests for set() and reset()."""
|
|
@@ -132,6 +143,26 @@ class TestSetAndReset:
|
|
|
132
143
|
except ImportError:
|
|
133
144
|
pytest.skip("pypulseq not installed")
|
|
134
145
|
|
|
146
|
+
def test_reset_removes_seqeyes_repr_html(self):
|
|
147
|
+
"""reset() should restore bare Sequence display, not only plot()."""
|
|
148
|
+
try:
|
|
149
|
+
import seqeyes
|
|
150
|
+
from pypulseq import Sequence
|
|
151
|
+
|
|
152
|
+
had_original = hasattr(Sequence, "_repr_html_")
|
|
153
|
+
original_repr = getattr(Sequence, "_repr_html_", None)
|
|
154
|
+
|
|
155
|
+
seqeyes.set()
|
|
156
|
+
assert hasattr(Sequence, "_repr_html_")
|
|
157
|
+
|
|
158
|
+
seqeyes.reset()
|
|
159
|
+
if had_original:
|
|
160
|
+
assert getattr(Sequence, "_repr_html_", None) is original_repr
|
|
161
|
+
else:
|
|
162
|
+
assert not hasattr(Sequence, "_repr_html_")
|
|
163
|
+
except ImportError:
|
|
164
|
+
pytest.skip("pypulseq not installed")
|
|
165
|
+
|
|
135
166
|
def test_set_defaults_merge(self):
|
|
136
167
|
"""Global defaults should merge with per-call args."""
|
|
137
168
|
try:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|