seqeyes-python 0.2.1__py3-none-any.whl → 0.2.6__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 +1 -1
- seqeyes/_plot.py +16 -3
- seqeyes/_renderer.py +47 -18
- seqeyes/resources/pulseq-bundle.js +944 -387
- seqeyes/resources/viewer.html +43 -16
- {seqeyes_python-0.2.1.dist-info → seqeyes_python-0.2.6.dist-info}/METADATA +13 -3
- seqeyes_python-0.2.6.dist-info/RECORD +8 -0
- seqeyes_python-0.2.1.dist-info/RECORD +0 -8
- {seqeyes_python-0.2.1.dist-info → seqeyes_python-0.2.6.dist-info}/WHEEL +0 -0
seqeyes/__init__.py
CHANGED
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 = "
|
|
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
|
@@ -12,7 +12,7 @@ import base64
|
|
|
12
12
|
import json
|
|
13
13
|
import os
|
|
14
14
|
from pathlib import Path
|
|
15
|
-
from typing import Optional
|
|
15
|
+
from typing import Any, Optional, Union
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
# ── Paths ─────────────────────────────────────────────────────────────────
|
|
@@ -26,6 +26,8 @@ _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"}
|
|
30
|
+
SequenceSource = Union[str, bytes, bytearray, memoryview]
|
|
29
31
|
|
|
30
32
|
|
|
31
33
|
def _find_bundle() -> str:
|
|
@@ -53,8 +55,13 @@ def _read_viewer_template() -> str:
|
|
|
53
55
|
)
|
|
54
56
|
|
|
55
57
|
|
|
58
|
+
def _normalize_grad_disp(unit: str) -> str:
|
|
59
|
+
"""Return a viewer-supported gradient unit."""
|
|
60
|
+
return unit if unit in _VALID_GRAD_UNITS else "Hz/m"
|
|
61
|
+
|
|
62
|
+
|
|
56
63
|
def _build_html(
|
|
57
|
-
|
|
64
|
+
sequence_source: SequenceSource,
|
|
58
65
|
*,
|
|
59
66
|
theme: Optional[str] = None,
|
|
60
67
|
inject_bundle: bool = True,
|
|
@@ -62,14 +69,14 @@ def _build_html(
|
|
|
62
69
|
show_blocks: bool = False,
|
|
63
70
|
time_range: tuple = (0, float("inf")),
|
|
64
71
|
time_disp: str = "s",
|
|
65
|
-
grad_disp: str = "
|
|
72
|
+
grad_disp: str = "Hz/m",
|
|
66
73
|
) -> str:
|
|
67
74
|
"""Assemble the complete viewer HTML.
|
|
68
75
|
|
|
69
76
|
Parameters
|
|
70
77
|
----------
|
|
71
|
-
|
|
72
|
-
Raw
|
|
78
|
+
sequence_source : str or bytes-like
|
|
79
|
+
Raw text ``.seq`` content or binary ``.bseq`` bytes.
|
|
73
80
|
theme : str or None
|
|
74
81
|
CSS theme class to apply to ``<body>``.
|
|
75
82
|
inject_bundle : bool
|
|
@@ -83,11 +90,13 @@ def _build_html(
|
|
|
83
90
|
time_disp : str
|
|
84
91
|
Initial time display unit (``"s"``, ``"ms"``, ``"us"``).
|
|
85
92
|
grad_disp : str
|
|
86
|
-
Initial gradient display unit (``"Hz/m"``, ``"
|
|
87
|
-
``"mT/m"``, ``"G/cm"``).
|
|
93
|
+
Initial gradient display unit (``"Hz/m"``, ``"mT/m"``, ``"G/cm"``).
|
|
88
94
|
"""
|
|
89
95
|
template = _read_viewer_template()
|
|
90
|
-
|
|
96
|
+
grad_disp = _normalize_grad_disp(grad_disp)
|
|
97
|
+
source_is_text = isinstance(sequence_source, str)
|
|
98
|
+
source_bytes = sequence_source.encode("utf-8") if source_is_text else bytes(sequence_source)
|
|
99
|
+
source_b64 = base64.b64encode(source_bytes).decode("ascii")
|
|
91
100
|
|
|
92
101
|
# 1. Inject the pulseq-bundle.js
|
|
93
102
|
bundle_placeholder = "<!-- PULSEQ_BUNDLE_PLACEHOLDER -->"
|
|
@@ -102,7 +111,9 @@ def _build_html(
|
|
|
102
111
|
|
|
103
112
|
# 2. Build options injection block (sequence data + display opts)
|
|
104
113
|
opts = [
|
|
105
|
-
f"window.SEQEYES_RAW_B64 = {json.dumps(
|
|
114
|
+
f"window.SEQEYES_RAW_B64 = {json.dumps(source_b64)};",
|
|
115
|
+
f"window.SEQEYES_SOURCE_KIND = {json.dumps('text' if source_is_text else 'bytes')};",
|
|
116
|
+
f"window.SEQEYES_SOURCE_NAME = {json.dumps(label)};",
|
|
106
117
|
f"window.SEQEYES_LABEL = {json.dumps(label)};",
|
|
107
118
|
f"window.SEQEYES_SHOW_BLOCKS = {json.dumps(show_blocks)};",
|
|
108
119
|
f"window.SEQEYES_TIME_RANGE = {json.dumps(list(time_range))};",
|
|
@@ -138,8 +149,8 @@ class SeqEyesViewer:
|
|
|
138
149
|
|
|
139
150
|
Parameters
|
|
140
151
|
----------
|
|
141
|
-
|
|
142
|
-
Raw
|
|
152
|
+
sequence_source : str or bytes-like
|
|
153
|
+
Raw text ``.seq`` content or binary ``.bseq`` bytes.
|
|
143
154
|
label : str
|
|
144
155
|
Display label (not yet rendered on the viewer).
|
|
145
156
|
show_blocks : bool
|
|
@@ -150,7 +161,7 @@ class SeqEyesViewer:
|
|
|
150
161
|
time_disp : str
|
|
151
162
|
Time axis unit — ``"s"``, ``"ms"``, or ``"us"``.
|
|
152
163
|
grad_disp : str
|
|
153
|
-
Gradient axis unit — ``"Hz/m"``, ``"
|
|
164
|
+
Gradient axis unit — ``"Hz/m"``, ``"mT/m"``, or ``"G/cm"``.
|
|
154
165
|
theme : str or None
|
|
155
166
|
Colour theme.
|
|
156
167
|
width : str
|
|
@@ -161,33 +172,51 @@ class SeqEyesViewer:
|
|
|
161
172
|
|
|
162
173
|
def __init__(
|
|
163
174
|
self,
|
|
164
|
-
|
|
175
|
+
sequence_source: SequenceSource,
|
|
165
176
|
*,
|
|
166
177
|
label: str = "",
|
|
167
178
|
show_blocks: bool = False,
|
|
168
179
|
time_range: tuple = (0, float("inf")),
|
|
169
180
|
time_disp: str = "s",
|
|
170
|
-
grad_disp: str = "
|
|
181
|
+
grad_disp: str = "Hz/m",
|
|
171
182
|
theme: Optional[str] = None,
|
|
172
183
|
width: str = "100%",
|
|
173
184
|
height: str = "550px",
|
|
174
185
|
) -> None:
|
|
175
|
-
|
|
186
|
+
if not isinstance(sequence_source, (str, bytes, bytearray, memoryview)):
|
|
187
|
+
raise TypeError("sequence_source must be .seq text or .bseq bytes")
|
|
188
|
+
self._sequence_source = sequence_source
|
|
176
189
|
self._label = label
|
|
177
190
|
self._show_blocks = show_blocks
|
|
178
191
|
self._time_range = time_range
|
|
179
192
|
self._time_disp = time_disp
|
|
180
|
-
self._grad_disp = grad_disp
|
|
193
|
+
self._grad_disp = _normalize_grad_disp(grad_disp)
|
|
181
194
|
self._theme = theme
|
|
182
195
|
self._width = width
|
|
183
196
|
self._height = height
|
|
184
197
|
|
|
198
|
+
@classmethod
|
|
199
|
+
def from_file(cls, path: Union[str, os.PathLike[str]], **kwargs: Any) -> "SeqEyesViewer":
|
|
200
|
+
"""Create a viewer from a local ``.seq`` or ``.bseq`` file."""
|
|
201
|
+
source_path = Path(path)
|
|
202
|
+
if source_path.suffix.lower() not in {".seq", ".bseq"}:
|
|
203
|
+
raise ValueError("SeqEyesViewer.from_file() expects a .seq or .bseq file")
|
|
204
|
+
if not source_path.is_file():
|
|
205
|
+
raise FileNotFoundError(source_path)
|
|
206
|
+
kwargs.setdefault("label", source_path.name)
|
|
207
|
+
source: SequenceSource = (
|
|
208
|
+
source_path.read_text(encoding="utf-8")
|
|
209
|
+
if source_path.suffix.lower() == ".seq"
|
|
210
|
+
else source_path.read_bytes()
|
|
211
|
+
)
|
|
212
|
+
return cls(source, **kwargs)
|
|
213
|
+
|
|
185
214
|
# ── Jupyter integration ───────────────────────────────────────────
|
|
186
215
|
|
|
187
216
|
def _repr_html_(self) -> str:
|
|
188
217
|
"""Return an HTML iframe that Jupyter renders inline."""
|
|
189
218
|
html = _build_html(
|
|
190
|
-
self.
|
|
219
|
+
self._sequence_source,
|
|
191
220
|
theme=self._theme,
|
|
192
221
|
label=self._label,
|
|
193
222
|
show_blocks=self._show_blocks,
|
|
@@ -225,7 +254,7 @@ class SeqEyesViewer:
|
|
|
225
254
|
def to_html(self, *, inject_bundle: bool = True) -> str:
|
|
226
255
|
"""Return the full standalone HTML string (for saving to a file)."""
|
|
227
256
|
return _build_html(
|
|
228
|
-
self.
|
|
257
|
+
self._sequence_source,
|
|
229
258
|
theme=self._theme,
|
|
230
259
|
inject_bundle=inject_bundle,
|
|
231
260
|
label=self._label,
|