effusive 0.1.1__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.
- effusive/__init__.py +1 -0
- effusive/assets/__init__.py +28 -0
- effusive/assets/aperture.svg +1 -0
- effusive/assets/audio-waveform.svg +1 -0
- effusive/assets/circle-check.svg +4 -0
- effusive/assets/circle-x.svg +5 -0
- effusive/assets/cpu.svg +1 -0
- effusive/assets/hard-drive.svg +1 -0
- effusive/assets/layers.svg +1 -0
- effusive/assets/pause.svg +1 -0
- effusive/assets/play.svg +1 -0
- effusive/assets/sliders-horizontal.svg +21 -0
- effusive/assets/square.svg +1 -0
- effusive/assets/triangle-alert.svg +5 -0
- effusive/bids.py +390 -0
- effusive/cli.py +42 -0
- effusive/config.py +534 -0
- effusive/default_config.toml +63 -0
- effusive/matlab_worker.py +424 -0
- effusive/motor_controller.py +356 -0
- effusive/napari/__init__.py +1 -0
- effusive/napari/commands.py +592 -0
- effusive/napari/components.py +94 -0
- effusive/napari/console.py +191 -0
- effusive/napari/controls.py +545 -0
- effusive/napari/crop.py +379 -0
- effusive/napari/debug.py +365 -0
- effusive/napari/panels/__init__.py +31 -0
- effusive/napari/panels/common.py +158 -0
- effusive/napari/panels/data.py +153 -0
- effusive/napari/panels/metadata.py +582 -0
- effusive/napari/panels/processing.py +356 -0
- effusive/napari/panels/sequence.py +337 -0
- effusive/napari/panels/stack.py +261 -0
- effusive/napari/panels/system.py +497 -0
- effusive/napari/plugin.py +70 -0
- effusive/napari/runtime.py +462 -0
- effusive/napari/stack.py +1000 -0
- effusive/napari/theme.py +460 -0
- effusive/napari/udp_control.py +511 -0
- effusive/napari/widget.py +797 -0
- effusive/napari/worker.py +466 -0
- effusive/napari.yaml +10 -0
- effusive/shared_memory.py +584 -0
- effusive-0.1.1.dist-info/METADATA +68 -0
- effusive-0.1.1.dist-info/RECORD +48 -0
- effusive-0.1.1.dist-info/WHEEL +4 -0
- effusive-0.1.1.dist-info/entry_points.txt +6 -0
effusive/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Effusive python package for the napari viewer."""
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Effusive assets module.
|
|
2
|
+
|
|
3
|
+
Contains SVG icons and other static assets used by the application.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def load_svg(name: str) -> str:
|
|
10
|
+
"""Load an SVG icon by name.
|
|
11
|
+
|
|
12
|
+
Parameters
|
|
13
|
+
----------
|
|
14
|
+
name : str
|
|
15
|
+
Name of the SVG file (without .svg extension).
|
|
16
|
+
|
|
17
|
+
Returns
|
|
18
|
+
-------
|
|
19
|
+
str
|
|
20
|
+
Contents of the SVG file.
|
|
21
|
+
|
|
22
|
+
Raises
|
|
23
|
+
------
|
|
24
|
+
FileNotFoundError
|
|
25
|
+
If the SVG file does not exist.
|
|
26
|
+
"""
|
|
27
|
+
svg_path = Path(__file__).parent / f"{name}.svg"
|
|
28
|
+
return svg_path.read_text()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m14.31 8 5.74 9.94"/><path d="M9.69 8h11.48"/><path d="m7.38 12 5.74-9.94"/><path d="M9.69 16 3.95 6.06"/><path d="M14.31 16H2.83"/><path d="m16.62 12-5.74 9.94"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-audio-waveform-icon lucide-audio-waveform"><path d="M2 13a2 2 0 0 0 2-2V7a2 2 0 0 1 4 0v13a2 2 0 0 0 4 0V4a2 2 0 0 1 4 0v13a2 2 0 0 0 4 0v-4a2 2 0 0 1 2-2"/></svg>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
2
|
+
<circle cx="12" cy="12" r="10"/>
|
|
3
|
+
<path d="m15 9-6 6"/>
|
|
4
|
+
<path d="m9 9 6 6"/>
|
|
5
|
+
</svg>
|
effusive/assets/cpu.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-cpu-icon lucide-cpu"><path d="M12 20v2"/><path d="M12 2v2"/><path d="M17 20v2"/><path d="M17 2v2"/><path d="M2 12h2"/><path d="M2 17h2"/><path d="M2 7h2"/><path d="M20 12h2"/><path d="M20 17h2"/><path d="M20 7h2"/><path d="M7 20v2"/><path d="M7 2v2"/><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="8" y="8" width="8" height="8" rx="1"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01"/><path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/><path d="M21.946 12.013H2.054"/><path d="M6 16h.01"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z"/><path d="M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12"/><path d="M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="4" width="4" height="16" rx="1"/><rect x="6" y="4" width="4" height="16" rx="1"/></svg>
|
effusive/assets/play.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="6 3 20 12 6 21 6 3"/></svg>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<svg
|
|
2
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
3
|
+
width="24"
|
|
4
|
+
height="24"
|
|
5
|
+
viewBox="0 0 24 24"
|
|
6
|
+
fill="none"
|
|
7
|
+
stroke="currentColor"
|
|
8
|
+
stroke-width="2"
|
|
9
|
+
stroke-linecap="round"
|
|
10
|
+
stroke-linejoin="round"
|
|
11
|
+
>
|
|
12
|
+
<path d="M10 5H3" />
|
|
13
|
+
<path d="M12 19H3" />
|
|
14
|
+
<path d="M14 3v4" />
|
|
15
|
+
<path d="M16 17v4" />
|
|
16
|
+
<path d="M21 12h-9" />
|
|
17
|
+
<path d="M21 19h-5" />
|
|
18
|
+
<path d="M21 5h-7" />
|
|
19
|
+
<path d="M8 10v4" />
|
|
20
|
+
<path d="M8 12H3" />
|
|
21
|
+
</svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2"/></svg>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
2
|
+
<path d="m21.73 18-8-14a2 2 0 0 0-3.46 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/>
|
|
3
|
+
<path d="M12 9v4"/>
|
|
4
|
+
<path d="M12 17h.01"/>
|
|
5
|
+
</svg>
|
effusive/bids.py
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
"""Helpers for Effusive's BIDS-like storage mode.
|
|
2
|
+
|
|
3
|
+
This module centralises validation and filename construction for the planned
|
|
4
|
+
fUSI-BIDS layout so the napari UI and later MATLAB runtime code can share the
|
|
5
|
+
same naming rules.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import platformdirs
|
|
15
|
+
|
|
16
|
+
FUSI_DATATYPE = "fusi"
|
|
17
|
+
"""Datatype folder for regular recordings."""
|
|
18
|
+
|
|
19
|
+
ANGIO_DATATYPE = "angio"
|
|
20
|
+
"""Datatype folder for z-stack exports."""
|
|
21
|
+
|
|
22
|
+
FUSI_RECORDING_SUFFIXES = ("iq", "pwd", "rf", "rftimestamps", "seq.mat")
|
|
23
|
+
"""Basename suffixes reserved by one fUSI recording stem."""
|
|
24
|
+
|
|
25
|
+
VALID_BIDS_LABEL_RE = re.compile(r"^[A-Za-z0-9]+$")
|
|
26
|
+
"""Allowed entity-label pattern for the v1 BIDS-like mode."""
|
|
27
|
+
|
|
28
|
+
BIDS_META_SIDECAR_PATH = (
|
|
29
|
+
Path(platformdirs.user_cache_dir("effusive")) / "cf_bids_meta.json"
|
|
30
|
+
)
|
|
31
|
+
"""Per-recording metadata sidecar for live updates to the MATLAB worker.
|
|
32
|
+
|
|
33
|
+
Written by Python at record-start; read by `cfPrepareStorageSpecForSave` to pick up
|
|
34
|
+
subject/session/task/acq/proc/run values that may have changed since VSX launched.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def write_bids_meta_sidecar(
|
|
39
|
+
task: str,
|
|
40
|
+
acq: str,
|
|
41
|
+
proc: str,
|
|
42
|
+
run: int,
|
|
43
|
+
subject: str = "",
|
|
44
|
+
session: str = "",
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Write per-recording BIDS metadata to the shared sidecar file.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
task : str
|
|
51
|
+
Task label without the `task-` prefix.
|
|
52
|
+
acq : str
|
|
53
|
+
Acquisition label without the `acq-` prefix.
|
|
54
|
+
proc : str
|
|
55
|
+
Processing label without the `proc-` prefix.
|
|
56
|
+
run : int
|
|
57
|
+
Exact run index to use for the next recording.
|
|
58
|
+
subject : str, optional
|
|
59
|
+
Subject label without the `sub-` prefix.
|
|
60
|
+
session : str, optional
|
|
61
|
+
Session label without the `ses-` prefix.
|
|
62
|
+
"""
|
|
63
|
+
BIDS_META_SIDECAR_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
BIDS_META_SIDECAR_PATH.write_text(
|
|
65
|
+
json.dumps(
|
|
66
|
+
{
|
|
67
|
+
"task": task,
|
|
68
|
+
"acq": acq,
|
|
69
|
+
"proc": proc,
|
|
70
|
+
"run": int(run),
|
|
71
|
+
"subject": subject,
|
|
72
|
+
"session": session,
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def validate_bids_label(value: str, field_name: str, required: bool = False) -> str:
|
|
79
|
+
"""Validate one BIDS entity label.
|
|
80
|
+
|
|
81
|
+
Parameters
|
|
82
|
+
----------
|
|
83
|
+
value : str
|
|
84
|
+
Raw entity-label text.
|
|
85
|
+
field_name : str
|
|
86
|
+
Human-readable field name used in error messages.
|
|
87
|
+
required : bool, default: False
|
|
88
|
+
Whether an empty value should be rejected.
|
|
89
|
+
|
|
90
|
+
Returns
|
|
91
|
+
-------
|
|
92
|
+
str
|
|
93
|
+
Stripped label text when valid.
|
|
94
|
+
|
|
95
|
+
Raises
|
|
96
|
+
------
|
|
97
|
+
ValueError
|
|
98
|
+
Raised when the value is empty for a required field or contains
|
|
99
|
+
characters outside `[A-Za-z0-9]+`.
|
|
100
|
+
"""
|
|
101
|
+
cleaned = value.strip()
|
|
102
|
+
if not cleaned:
|
|
103
|
+
if required:
|
|
104
|
+
raise ValueError(f"{field_name} is required.")
|
|
105
|
+
return ""
|
|
106
|
+
if VALID_BIDS_LABEL_RE.fullmatch(cleaned) is None:
|
|
107
|
+
raise ValueError(f"{field_name} must match [A-Za-z0-9]+.")
|
|
108
|
+
return cleaned
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def required_bids_fields(datatype: str) -> tuple[str, ...]:
|
|
112
|
+
"""Return the required entity names for a datatype.
|
|
113
|
+
|
|
114
|
+
Parameters
|
|
115
|
+
----------
|
|
116
|
+
datatype : str
|
|
117
|
+
Recording datatype, typically `fusi` or `angio`.
|
|
118
|
+
|
|
119
|
+
Returns
|
|
120
|
+
-------
|
|
121
|
+
tuple of str
|
|
122
|
+
Required entity names for the requested datatype.
|
|
123
|
+
"""
|
|
124
|
+
if datatype == FUSI_DATATYPE:
|
|
125
|
+
return ("subject", "session", "task")
|
|
126
|
+
if datatype == ANGIO_DATATYPE:
|
|
127
|
+
return ("subject", "session")
|
|
128
|
+
raise ValueError(f"Unsupported BIDS datatype: {datatype}.")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def validate_bids_entities(
|
|
132
|
+
*,
|
|
133
|
+
datatype: str,
|
|
134
|
+
subject: str,
|
|
135
|
+
session: str,
|
|
136
|
+
task: str = "",
|
|
137
|
+
acq: str = "",
|
|
138
|
+
proc: str = "",
|
|
139
|
+
) -> dict[str, str]:
|
|
140
|
+
"""Validate a set of entity values for one datatype.
|
|
141
|
+
|
|
142
|
+
Parameters
|
|
143
|
+
----------
|
|
144
|
+
datatype : str
|
|
145
|
+
Recording datatype, typically `fusi` or `angio`.
|
|
146
|
+
subject : str
|
|
147
|
+
Subject label without the `sub-` prefix.
|
|
148
|
+
session : str
|
|
149
|
+
Session label without the `ses-` prefix.
|
|
150
|
+
task : str, optional
|
|
151
|
+
Task label without the `task-` prefix.
|
|
152
|
+
acq : str, optional
|
|
153
|
+
Acquisition label without the `acq-` prefix.
|
|
154
|
+
proc : str, optional
|
|
155
|
+
Processing label without the `proc-` prefix.
|
|
156
|
+
|
|
157
|
+
Returns
|
|
158
|
+
-------
|
|
159
|
+
dict of str to str
|
|
160
|
+
Validated entity values keyed by entity name.
|
|
161
|
+
|
|
162
|
+
Raises
|
|
163
|
+
------
|
|
164
|
+
ValueError
|
|
165
|
+
Raised when any required or optional label is invalid.
|
|
166
|
+
"""
|
|
167
|
+
required = set(required_bids_fields(datatype))
|
|
168
|
+
return {
|
|
169
|
+
"subject": validate_bids_label(subject, "Subject", "subject" in required),
|
|
170
|
+
"session": validate_bids_label(session, "Session", "session" in required),
|
|
171
|
+
"task": validate_bids_label(task, "Task", "task" in required),
|
|
172
|
+
"acq": validate_bids_label(acq, "Acq", False),
|
|
173
|
+
"proc": validate_bids_label(proc, "Proc", False),
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def format_run(run: int) -> str:
|
|
178
|
+
"""Format a run index for the BIDS-like entity string.
|
|
179
|
+
|
|
180
|
+
Parameters
|
|
181
|
+
----------
|
|
182
|
+
run : int
|
|
183
|
+
Positive run index.
|
|
184
|
+
|
|
185
|
+
Returns
|
|
186
|
+
-------
|
|
187
|
+
str
|
|
188
|
+
Zero-padded run label body such as `01`.
|
|
189
|
+
|
|
190
|
+
Raises
|
|
191
|
+
------
|
|
192
|
+
ValueError
|
|
193
|
+
Raised when `run` is not positive.
|
|
194
|
+
"""
|
|
195
|
+
if run < 1:
|
|
196
|
+
raise ValueError("Run must be >= 1.")
|
|
197
|
+
return f"{run:02d}"
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def build_bids_dir(root: Path, subject: str, session: str, datatype: str) -> Path:
|
|
201
|
+
"""Build the datatype directory for one BIDS-like recording.
|
|
202
|
+
|
|
203
|
+
Parameters
|
|
204
|
+
----------
|
|
205
|
+
root : Path
|
|
206
|
+
Storage root directory.
|
|
207
|
+
subject : str
|
|
208
|
+
Subject label without the `sub-` prefix.
|
|
209
|
+
session : str
|
|
210
|
+
Session label without the `ses-` prefix.
|
|
211
|
+
datatype : str
|
|
212
|
+
Datatype folder name, typically `fusi` or `angio`.
|
|
213
|
+
|
|
214
|
+
Returns
|
|
215
|
+
-------
|
|
216
|
+
Path
|
|
217
|
+
Datatype directory path below `root`.
|
|
218
|
+
"""
|
|
219
|
+
subject_label = validate_bids_label(subject, "Subject", required=True)
|
|
220
|
+
session_label = validate_bids_label(session, "Session", required=True)
|
|
221
|
+
return Path(root) / f"sub-{subject_label}" / f"ses-{session_label}" / datatype
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def build_bids_stem(
|
|
225
|
+
*,
|
|
226
|
+
subject: str,
|
|
227
|
+
session: str,
|
|
228
|
+
task: str = "",
|
|
229
|
+
acq: str = "",
|
|
230
|
+
run: int | None = None,
|
|
231
|
+
proc: str = "",
|
|
232
|
+
datatype: str = FUSI_DATATYPE,
|
|
233
|
+
) -> str:
|
|
234
|
+
"""Build a BIDS-like filename stem without suffix or extension.
|
|
235
|
+
|
|
236
|
+
Parameters
|
|
237
|
+
----------
|
|
238
|
+
subject : str
|
|
239
|
+
Subject label without the `sub-` prefix.
|
|
240
|
+
session : str
|
|
241
|
+
Session label without the `ses-` prefix.
|
|
242
|
+
task : str, optional
|
|
243
|
+
Task label without the `task-` prefix.
|
|
244
|
+
acq : str, optional
|
|
245
|
+
Acquisition label without the `acq-` prefix.
|
|
246
|
+
run : int, optional
|
|
247
|
+
Run index. When omitted, the stem is built without a `run-` entity.
|
|
248
|
+
proc : str, optional
|
|
249
|
+
Processing label without the `proc-` prefix.
|
|
250
|
+
datatype : str, default: `fusi`
|
|
251
|
+
Recording datatype, used only to validate required fields.
|
|
252
|
+
|
|
253
|
+
Returns
|
|
254
|
+
-------
|
|
255
|
+
str
|
|
256
|
+
Filename stem up to, but not including, the suffix.
|
|
257
|
+
"""
|
|
258
|
+
entities = validate_bids_entities(
|
|
259
|
+
datatype=datatype,
|
|
260
|
+
subject=subject,
|
|
261
|
+
session=session,
|
|
262
|
+
task=task,
|
|
263
|
+
acq=acq,
|
|
264
|
+
proc=proc,
|
|
265
|
+
)
|
|
266
|
+
parts = [f"sub-{entities['subject']}", f"ses-{entities['session']}"]
|
|
267
|
+
if entities["task"]:
|
|
268
|
+
parts.append(f"task-{entities['task']}")
|
|
269
|
+
if entities["acq"]:
|
|
270
|
+
parts.append(f"acq-{entities['acq']}")
|
|
271
|
+
if entities["proc"]:
|
|
272
|
+
parts.append(f"proc-{entities['proc']}")
|
|
273
|
+
if run is not None:
|
|
274
|
+
parts.append(f"run-{format_run(run)}")
|
|
275
|
+
return "_".join(parts)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def check_bids_run_collision(
|
|
279
|
+
root: Path,
|
|
280
|
+
*,
|
|
281
|
+
datatype: str,
|
|
282
|
+
subject: str,
|
|
283
|
+
session: str,
|
|
284
|
+
task: str = "",
|
|
285
|
+
acq: str = "",
|
|
286
|
+
run: int,
|
|
287
|
+
proc: str = "",
|
|
288
|
+
) -> bool:
|
|
289
|
+
"""Return True if any file with the expected BIDS stem already exists.
|
|
290
|
+
|
|
291
|
+
Parameters
|
|
292
|
+
----------
|
|
293
|
+
root : Path
|
|
294
|
+
Storage root directory.
|
|
295
|
+
datatype : str
|
|
296
|
+
Datatype folder name, typically `fusi` or `angio`.
|
|
297
|
+
subject : str
|
|
298
|
+
Subject label without the `sub-` prefix.
|
|
299
|
+
session : str
|
|
300
|
+
Session label without the `ses-` prefix.
|
|
301
|
+
task : str, optional
|
|
302
|
+
Task label without the `task-` prefix.
|
|
303
|
+
acq : str, optional
|
|
304
|
+
Acquisition label without the `acq-` prefix.
|
|
305
|
+
run : int
|
|
306
|
+
Manual run index to check for collision.
|
|
307
|
+
proc : str, optional
|
|
308
|
+
Processing label without the `proc-` prefix.
|
|
309
|
+
|
|
310
|
+
Returns
|
|
311
|
+
-------
|
|
312
|
+
bool
|
|
313
|
+
True when at least one file matching `<stem>_*` exists.
|
|
314
|
+
"""
|
|
315
|
+
bids_dir = build_bids_dir(root, subject, session, datatype)
|
|
316
|
+
stem = build_bids_stem(
|
|
317
|
+
subject=subject,
|
|
318
|
+
session=session,
|
|
319
|
+
task=task,
|
|
320
|
+
acq=acq,
|
|
321
|
+
run=run,
|
|
322
|
+
proc=proc,
|
|
323
|
+
datatype=datatype,
|
|
324
|
+
)
|
|
325
|
+
if not bids_dir.is_dir():
|
|
326
|
+
return False
|
|
327
|
+
|
|
328
|
+
if datatype == FUSI_DATATYPE:
|
|
329
|
+
return any(
|
|
330
|
+
(bids_dir / f"{stem}_{suffix}").exists()
|
|
331
|
+
for suffix in FUSI_RECORDING_SUFFIXES
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
return any(bids_dir.glob(f"{stem}_*"))
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def build_bids_preview_path(
|
|
338
|
+
root: Path,
|
|
339
|
+
*,
|
|
340
|
+
datatype: str,
|
|
341
|
+
subject: str,
|
|
342
|
+
session: str,
|
|
343
|
+
task: str = "",
|
|
344
|
+
acq: str = "",
|
|
345
|
+
run: int | None = None,
|
|
346
|
+
proc: str = "",
|
|
347
|
+
suffix: str = "pwd",
|
|
348
|
+
extension: str = ".ext",
|
|
349
|
+
) -> Path:
|
|
350
|
+
"""Build a full preview path for UI display.
|
|
351
|
+
|
|
352
|
+
Parameters
|
|
353
|
+
----------
|
|
354
|
+
root : Path
|
|
355
|
+
Storage root directory.
|
|
356
|
+
datatype : str
|
|
357
|
+
Datatype folder name, typically `fusi` or `angio`.
|
|
358
|
+
subject : str
|
|
359
|
+
Subject label without the `sub-` prefix.
|
|
360
|
+
session : str
|
|
361
|
+
Session label without the `ses-` prefix.
|
|
362
|
+
task : str, optional
|
|
363
|
+
Task label without the `task-` prefix.
|
|
364
|
+
acq : str, optional
|
|
365
|
+
Acquisition label without the `acq-` prefix.
|
|
366
|
+
run : int, optional
|
|
367
|
+
Run index for the preview stem.
|
|
368
|
+
proc : str, optional
|
|
369
|
+
Processing label without the `proc-` prefix.
|
|
370
|
+
suffix : str, default: `pwd`
|
|
371
|
+
Output suffix token without the leading underscore.
|
|
372
|
+
extension : str, default: `.ext`
|
|
373
|
+
Filename extension shown in the preview.
|
|
374
|
+
|
|
375
|
+
Returns
|
|
376
|
+
-------
|
|
377
|
+
Path
|
|
378
|
+
Full preview path including one representative filename.
|
|
379
|
+
"""
|
|
380
|
+
bids_dir = build_bids_dir(root, subject, session, datatype)
|
|
381
|
+
stem = build_bids_stem(
|
|
382
|
+
subject=subject,
|
|
383
|
+
session=session,
|
|
384
|
+
task=task,
|
|
385
|
+
acq=acq,
|
|
386
|
+
run=run,
|
|
387
|
+
proc=proc,
|
|
388
|
+
datatype=datatype,
|
|
389
|
+
)
|
|
390
|
+
return bids_dir / f"{stem}_{suffix}{extension}"
|
effusive/cli.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Effusive command-line entry point.
|
|
2
|
+
|
|
3
|
+
Opens a napari viewer with live B-mode and PDI layers and a control dock widget. The
|
|
4
|
+
MATLAB worker subprocess is not started automatically: click the play button in the
|
|
5
|
+
sidebar after configuring acquisition parameters.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
effusive
|
|
11
|
+
```
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import napari
|
|
17
|
+
from qtpy.QtWidgets import QApplication
|
|
18
|
+
|
|
19
|
+
from effusive import config as cf_config
|
|
20
|
+
from effusive.napari import worker as worker_module
|
|
21
|
+
from effusive.napari.plugin import make_main_widget
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main() -> None:
|
|
25
|
+
"""Run the Effusive napari viewer."""
|
|
26
|
+
viewer = napari.Viewer(title="Effusive")
|
|
27
|
+
widget = make_main_widget(viewer)
|
|
28
|
+
viewer.window.add_dock_widget(widget, name="Effusive", area="right")
|
|
29
|
+
|
|
30
|
+
# widget._config is kept live by command handlers and panel connections;
|
|
31
|
+
# saving it here requires no Qt object access so it works even after teardown.
|
|
32
|
+
app = QApplication.instance()
|
|
33
|
+
assert app is not None
|
|
34
|
+
app.aboutToQuit.connect(lambda: cf_config.save_config(widget._config))
|
|
35
|
+
|
|
36
|
+
napari.run()
|
|
37
|
+
|
|
38
|
+
worker_module.stop_acquisition(widget, from_close=True)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if __name__ == "__main__":
|
|
42
|
+
main()
|