pulserver 0.4.4__cp313-cp313-win_amd64.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.
- pulserver/__init__.py +25 -0
- pulserver/_accelerators.py +68 -0
- pulserver/_cli.py +59 -0
- pulserver/_ext.cp313-win_amd64.pyd +0 -0
- pulserver/_labels.py +184 -0
- pulserver/_logs.py +44 -0
- pulserver/_memory.py +21 -0
- pulserver/_plugins.py +79 -0
- pulserver/_zoo/__init__.py +35 -0
- pulserver/_zoo/_evaluation.py +325 -0
- pulserver/_zoo/_inversion.py +110 -0
- pulserver/_zoo/_pmc.py +26 -0
- pulserver/_zoo/_saturation.py +118 -0
- pulserver/_zoo/_slab.py +37 -0
- pulserver/_zoo/_user.py +193 -0
- pulserver/_zoo/recon/epi.py +3 -0
- pulserver/_zoo/recon/nufft.py +3 -0
- pulserver/_zoo/recon/nufft_train.py +3 -0
- pulserver/_zoo/recon/pics.py +3 -0
- pulserver/_zoo/recon/pics_train.py +3 -0
- pulserver/_zoo/recon/pmc.py +3 -0
- pulserver/_zoo/sequences/bssfp2d.py +180 -0
- pulserver/_zoo/sequences/bssfp3d.py +90 -0
- pulserver/_zoo/sequences/epi2d.py +163 -0
- pulserver/_zoo/sequences/epi3d.py +206 -0
- pulserver/_zoo/sequences/fse3d.py +214 -0
- pulserver/_zoo/sequences/gre2d.py +75 -0
- pulserver/_zoo/sequences/gre3d.py +77 -0
- pulserver/_zoo/sequences/gre_multiecho2d.py +69 -0
- pulserver/_zoo/sequences/gre_multiecho3d.py +82 -0
- pulserver/_zoo/sequences/gre_propeller2d.py +56 -0
- pulserver/_zoo/sequences/gre_radial2d.py +55 -0
- pulserver/_zoo/sequences/gre_spiral2d.py +73 -0
- pulserver/_zoo/sequences/gre_stack_of_blades3d.py +61 -0
- pulserver/_zoo/sequences/gre_stack_of_spirals3d.py +81 -0
- pulserver/_zoo/sequences/gre_stack_of_stars3d.py +60 -0
- pulserver/_zoo/sequences/mprage3d.py +134 -0
- pulserver/_zoo/sequences/mprage_stack_of_spirals3d.py +97 -0
- pulserver/_zoo/sequences/mprage_stack_of_stars3d.py +71 -0
- pulserver/_zoo/sequences/se2d.py +71 -0
- pulserver/_zoo/sequences/se3d.py +72 -0
- pulserver/_zoo/sequences/se_epi_propeller2d.py +121 -0
- pulserver/_zoo/sequences/se_propeller2d.py +60 -0
- pulserver/_zoo/sequences/se_radial2d.py +59 -0
- pulserver/_zoo/sequences/se_spiral2d.py +77 -0
- pulserver/_zoo/sequences/se_stack_of_blades3d.py +65 -0
- pulserver/_zoo/sequences/se_stack_of_spirals3d.py +85 -0
- pulserver/_zoo/sequences/se_stack_of_stars3d.py +64 -0
- pulserver/_zoo/sequences/zte3d.py +90 -0
- pulserver/design/__init__.py +52 -0
- pulserver/design/_entries.py +643 -0
- pulserver/design/_options.py +160 -0
- pulserver/design/_plugin.py +711 -0
- pulserver/design/_rf.py +111 -0
- pulserver/host/__init__.py +14 -0
- pulserver/host/__main__.py +8 -0
- pulserver/host/_blocks.py +116 -0
- pulserver/host/_command.py +280 -0
- pulserver/host/_limits.py +328 -0
- pulserver/host/_push.py +66 -0
- pulserver/host/_server.py +262 -0
- pulserver/host/_service.py +625 -0
- pulserver/host/_store.py +398 -0
- pulserver/ir/__init__.py +48 -0
- pulserver/ir/_checks.py +519 -0
- pulserver/ir/_convert.py +655 -0
- pulserver/ir/_playout.py +174 -0
- pulserver/ir/_source.py +847 -0
- pulserver/ir/_vendor.py +113 -0
- pulserver/ir/_waves.py +210 -0
- pulserver/mrd/__init__.py +38 -0
- pulserver/mrd/_acquisitions.py +380 -0
- pulserver/mrd/_header.py +269 -0
- pulserver/mrd/_images.py +113 -0
- pulserver/mrd/_metadata.py +229 -0
- pulserver/mrd/_sequence.py +480 -0
- pulserver/protocol/__init__.py +89 -0
- pulserver/protocol/_keys.py +485 -0
- pulserver/protocol/_prescription.py +67 -0
- pulserver/protocol/_schema.py +141 -0
- pulserver/protocol/_wire.py +593 -0
- pulserver/proxy/__init__.py +27 -0
- pulserver/proxy/__main__.py +170 -0
- pulserver/proxy/_designs.py +162 -0
- pulserver/proxy/_enrich.py +799 -0
- pulserver/proxy/_held.py +154 -0
- pulserver/proxy/_intake.py +162 -0
- pulserver/proxy/_local.py +103 -0
- pulserver/proxy/_motion.py +292 -0
- pulserver/proxy/_proxy.py +1240 -0
- pulserver/proxy/_queue.py +71 -0
- pulserver/proxy/_seqdesc.py +351 -0
- pulserver/proxy/_workers.py +206 -0
- pulserver/recon/__init__.py +53 -0
- pulserver/recon/__main__.py +106 -0
- pulserver/recon/_buffers.py +765 -0
- pulserver/recon/_calibration.py +481 -0
- pulserver/recon/_correct.py +123 -0
- pulserver/recon/_exam_image.py +107 -0
- pulserver/recon/_figure.py +31 -0
- pulserver/recon/_loader.py +35 -0
- pulserver/recon/_replay.py +75 -0
- pulserver/recon/_runtime/__init__.py +1 -0
- pulserver/recon/_runtime/application.py +322 -0
- pulserver/recon/_runtime/concurrency.py +252 -0
- pulserver/recon/_runtime/connection.py +574 -0
- pulserver/recon/_runtime/constants.py +63 -0
- pulserver/recon/_runtime/exam.py +316 -0
- pulserver/recon/_runtime/mrd2dicom.py +1068 -0
- pulserver/recon/_runtime/offline.py +171 -0
- pulserver/recon/_runtime/readers.py +261 -0
- pulserver/recon/_runtime/writers.py +213 -0
- pulserver/recon/_units.py +115 -0
- pulserver/recon/gadgets.py +270 -0
- pulserver/recon/handlers/__init__.py +1 -0
- pulserver/recon/handlers/epi.py +211 -0
- pulserver/recon/handlers/nufft.py +260 -0
- pulserver/recon/handlers/nufft_train.py +34 -0
- pulserver/recon/handlers/pics.py +295 -0
- pulserver/recon/handlers/pics_train.py +37 -0
- pulserver/recon/handlers/pmc.py +128 -0
- pulserver/recon/plugin.py +1126 -0
- pulserver/validate/__init__.py +15 -0
- pulserver/validate/_command.py +105 -0
- pulserver/validate/_compare.py +274 -0
- pulserver/validate/_validate.py +270 -0
- pulserver/validate/_xml.py +162 -0
- pulserver/virtual/__init__.py +50 -0
- pulserver/virtual/_brainweb.py +381 -0
- pulserver/virtual/_client.py +178 -0
- pulserver/virtual/_coils.py +341 -0
- pulserver/virtual/_command.py +443 -0
- pulserver/virtual/_console.py +787 -0
- pulserver/virtual/_export.py +212 -0
- pulserver/virtual/_fourier.py +1977 -0
- pulserver/virtual/_girf.py +85 -0
- pulserver/virtual/_localizer.py +100 -0
- pulserver/virtual/_motion.py +191 -0
- pulserver/virtual/_phantom.py +293 -0
- pulserver/virtual/_stream.py +514 -0
- pulserver/virtual/_timeline.py +899 -0
- pulserver/virtual/_tissue.py +72 -0
- pulserver-0.4.4.dist-info/DELVEWHEEL +2 -0
- pulserver-0.4.4.dist-info/METADATA +244 -0
- pulserver-0.4.4.dist-info/RECORD +151 -0
- pulserver-0.4.4.dist-info/WHEEL +5 -0
- pulserver-0.4.4.dist-info/entry_points.txt +3 -0
- pulserver-0.4.4.dist-info/licenses/LICENSE +21 -0
- pulserver-0.4.4.dist-info/licenses/LICENSES/gadgetron-python-MIT.txt +21 -0
- pulserver-0.4.4.dist-info/licenses/LICENSES/python-ismrmrd-server-MIT.txt +21 -0
- pulserver.libs/msvcp140-a4c2229bdc2a2a630acdc095b4d86008.dll +0 -0
pulserver/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Orchestrator for MR acquisitions: sequence design, scanner preparation and reconstruction."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# start delvewheel patch
|
|
7
|
+
def _delvewheel_patch_1_13_2():
|
|
8
|
+
import os
|
|
9
|
+
if os.path.isdir(libs_dir := os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, 'pulserver.libs'))):
|
|
10
|
+
os.add_dll_directory(libs_dir)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
_delvewheel_patch_1_13_2()
|
|
14
|
+
del _delvewheel_patch_1_13_2
|
|
15
|
+
# end delvewheel patch
|
|
16
|
+
|
|
17
|
+
from importlib.metadata import PackageNotFoundError
|
|
18
|
+
from importlib.metadata import version as _distribution_version
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
__version__ = _distribution_version(__name__)
|
|
22
|
+
except PackageNotFoundError: # a source tree that was never installed
|
|
23
|
+
__version__ = "0.0.0.dev0"
|
|
24
|
+
|
|
25
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Load the compiled extension shipped inside the wheel."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from importlib import import_module
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
__all__ = ["require"]
|
|
11
|
+
|
|
12
|
+
_MODULE = "pulserver._ext"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _installed_tags() -> list[str]:
|
|
16
|
+
"""Return the ABI tags of the extension builds present beside the package."""
|
|
17
|
+
suffix = ".pyd" if sys.platform == "win32" else ".so"
|
|
18
|
+
package = import_module("pulserver")
|
|
19
|
+
tags = set()
|
|
20
|
+
for directory in getattr(package, "__path__", ()):
|
|
21
|
+
for path in Path(directory).glob(f"_ext.*{suffix}"):
|
|
22
|
+
parts = path.name.split(".")
|
|
23
|
+
if len(parts) >= 3:
|
|
24
|
+
tags.add(parts[1])
|
|
25
|
+
return sorted(tags)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def require(attribute: str | None = None) -> Any:
|
|
29
|
+
"""Load the bundled extension, or raise naming the mismatch.
|
|
30
|
+
|
|
31
|
+
The wheel ships the extension for every supported interpreter, so a failed
|
|
32
|
+
import means a broken installation, never an optional feature.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
attribute : str, default=None
|
|
37
|
+
Symbol the caller needs, checked at load so a binary that predates it
|
|
38
|
+
fails here rather than at the call.
|
|
39
|
+
|
|
40
|
+
Returns
|
|
41
|
+
-------
|
|
42
|
+
module or object
|
|
43
|
+
The extension module, or the named attribute when one is given.
|
|
44
|
+
|
|
45
|
+
Raises
|
|
46
|
+
------
|
|
47
|
+
ImportError
|
|
48
|
+
The extension is absent, was built for another interpreter, or does not
|
|
49
|
+
provide ``attribute``.
|
|
50
|
+
"""
|
|
51
|
+
try:
|
|
52
|
+
module = import_module(_MODULE)
|
|
53
|
+
except ImportError as error:
|
|
54
|
+
installed = _installed_tags()
|
|
55
|
+
found = ", ".join(installed) if installed else "none"
|
|
56
|
+
raise ImportError(
|
|
57
|
+
f"the bundled extension did not load. Running "
|
|
58
|
+
f"{sys.implementation.cache_tag}, builds present: {found}."
|
|
59
|
+
) from error
|
|
60
|
+
if attribute is None:
|
|
61
|
+
return module
|
|
62
|
+
try:
|
|
63
|
+
return getattr(module, attribute)
|
|
64
|
+
except AttributeError as error:
|
|
65
|
+
raise ImportError(
|
|
66
|
+
f"the bundled extension does not provide {attribute!r}; "
|
|
67
|
+
f"the installed binary predates it."
|
|
68
|
+
) from error
|
pulserver/_cli.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""The ``pulserver`` command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
_USAGE = (
|
|
8
|
+
"usage: pulserver design {list,validate,generate,import,prune,push,serve} ...\n"
|
|
9
|
+
" pulserver scan (--seq FILE | --plugin NAME --plugins DIR) --limits FILE ...\n"
|
|
10
|
+
" pulserver console --plugins DIR --limits FILE --store DIR ...\n"
|
|
11
|
+
" pulserver validate SEQ [--vendor NAME] [--played FILE] ...\n"
|
|
12
|
+
" pulserver proxy --store DIR --port N (--plugins DIR | --forward HOST:PORT) ...\n"
|
|
13
|
+
" pulserver recon --plugins DIR --port N ...\n"
|
|
14
|
+
" pulserver replay SERIES --plugin FILE (--store DIR | --design SEQ) ...\n"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main(argv: list[str] | None = None) -> int:
|
|
19
|
+
"""Run ``pulserver <command> ...``; return its exit status."""
|
|
20
|
+
argv = sys.argv[1:] if argv is None else argv
|
|
21
|
+
from ._memory import without_huge_page_advice
|
|
22
|
+
|
|
23
|
+
without_huge_page_advice()
|
|
24
|
+
if argv and argv[0] == "design":
|
|
25
|
+
from .host._command import main as design
|
|
26
|
+
|
|
27
|
+
return design(argv[1:])
|
|
28
|
+
if argv and argv[0] == "scan":
|
|
29
|
+
from .virtual._command import main as scan
|
|
30
|
+
|
|
31
|
+
return scan(argv[1:])
|
|
32
|
+
if argv and argv[0] == "proxy":
|
|
33
|
+
from .proxy.__main__ import main as proxy
|
|
34
|
+
|
|
35
|
+
proxy(argv[1:])
|
|
36
|
+
return 0
|
|
37
|
+
if argv and argv[0] == "recon":
|
|
38
|
+
from .recon.__main__ import main as recon
|
|
39
|
+
|
|
40
|
+
recon(argv[1:])
|
|
41
|
+
return 0
|
|
42
|
+
if argv and argv[0] == "replay":
|
|
43
|
+
from .recon._replay import main as replay
|
|
44
|
+
|
|
45
|
+
return replay(argv[1:])
|
|
46
|
+
if argv and argv[0] == "validate":
|
|
47
|
+
from .validate._command import main as check
|
|
48
|
+
|
|
49
|
+
return check(argv[1:])
|
|
50
|
+
if argv and argv[0] == "console":
|
|
51
|
+
from .virtual._console import main as console
|
|
52
|
+
|
|
53
|
+
return console(argv[1:])
|
|
54
|
+
sys.stderr.write(_USAGE)
|
|
55
|
+
return 2
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if __name__ == "__main__":
|
|
59
|
+
sys.exit(main())
|
|
Binary file
|
pulserver/_labels.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Sequence label names and the ISMRMRD counters and flags they map to.
|
|
2
|
+
|
|
3
|
+
Canonical spellings are the ``.seq`` label names (``LIN``, ``SLC``,
|
|
4
|
+
``LASTSLC``). ISMRMRD and MRPro spellings are translated by
|
|
5
|
+
:func:`canonical_label`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"COUNTER_LABELS",
|
|
12
|
+
"ENCODING_COUNTERS",
|
|
13
|
+
"FLAG_LABELS",
|
|
14
|
+
"FRAME_COUNTERS",
|
|
15
|
+
"MRD_COUNTERS",
|
|
16
|
+
"MRD_FLAGS",
|
|
17
|
+
"SCANNER_FLAGS",
|
|
18
|
+
"canonical_label",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
#: Counter labels: one integer index per acquisition.
|
|
22
|
+
COUNTER_LABELS = (
|
|
23
|
+
"SLC",
|
|
24
|
+
"SEG",
|
|
25
|
+
"REP",
|
|
26
|
+
"AVG",
|
|
27
|
+
"SET",
|
|
28
|
+
"ECO",
|
|
29
|
+
"PHS",
|
|
30
|
+
"LIN",
|
|
31
|
+
"PAR",
|
|
32
|
+
"ACQ",
|
|
33
|
+
"USER0",
|
|
34
|
+
"USER1",
|
|
35
|
+
"USER2",
|
|
36
|
+
"USER3",
|
|
37
|
+
"USER4",
|
|
38
|
+
"USER5",
|
|
39
|
+
"USER6",
|
|
40
|
+
"USER7",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
#: Counters selecting the image an acquisition belongs to.
|
|
44
|
+
FRAME_COUNTERS = ("SLC", "ECO", "PHS", "REP", "AVG", "SET")
|
|
45
|
+
|
|
46
|
+
#: Counters locating an acquisition within its image.
|
|
47
|
+
ENCODING_COUNTERS = ("LIN", "PAR", "SEG")
|
|
48
|
+
|
|
49
|
+
#: Counter label to ISMRMRD ``EncodingCounters`` field. ``ACQ`` has no MRD
|
|
50
|
+
#: field. ``USERn`` is ``user_n`` in ``encodingLimits`` and ``idx.user[n]`` on
|
|
51
|
+
#: an acquisition.
|
|
52
|
+
MRD_COUNTERS = {
|
|
53
|
+
"LIN": "kspace_encode_step_1",
|
|
54
|
+
"PAR": "kspace_encode_step_2",
|
|
55
|
+
"AVG": "average",
|
|
56
|
+
"SLC": "slice",
|
|
57
|
+
"ECO": "contrast",
|
|
58
|
+
"PHS": "phase",
|
|
59
|
+
"REP": "repetition",
|
|
60
|
+
"SET": "set",
|
|
61
|
+
"SEG": "segment",
|
|
62
|
+
"USER0": "user_0",
|
|
63
|
+
"USER1": "user_1",
|
|
64
|
+
"USER2": "user_2",
|
|
65
|
+
"USER3": "user_3",
|
|
66
|
+
"USER4": "user_4",
|
|
67
|
+
"USER5": "user_5",
|
|
68
|
+
"USER6": "user_6",
|
|
69
|
+
"USER7": "user_7",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#: Flag label to ISMRMRD acquisition-flag constant, for every ISMRMRD flag.
|
|
73
|
+
#: Boundary flags follow from the counters and the acquisition order;
|
|
74
|
+
#: classifying flags come only from the sequence; transport flags come from
|
|
75
|
+
#: whatever packs the data.
|
|
76
|
+
MRD_FLAGS = {
|
|
77
|
+
# boundary
|
|
78
|
+
"FIRSTLIN": "ACQ_FIRST_IN_ENCODE_STEP1",
|
|
79
|
+
"LASTLIN": "ACQ_LAST_IN_ENCODE_STEP1",
|
|
80
|
+
"FIRSTPAR": "ACQ_FIRST_IN_ENCODE_STEP2",
|
|
81
|
+
"LASTPAR": "ACQ_LAST_IN_ENCODE_STEP2",
|
|
82
|
+
"FIRSTAVG": "ACQ_FIRST_IN_AVERAGE",
|
|
83
|
+
"LASTAVG": "ACQ_LAST_IN_AVERAGE",
|
|
84
|
+
"FIRSTSLC": "ACQ_FIRST_IN_SLICE",
|
|
85
|
+
"LASTSLC": "ACQ_LAST_IN_SLICE",
|
|
86
|
+
"FIRSTECO": "ACQ_FIRST_IN_CONTRAST",
|
|
87
|
+
"LASTECO": "ACQ_LAST_IN_CONTRAST",
|
|
88
|
+
"FIRSTPHS": "ACQ_FIRST_IN_PHASE",
|
|
89
|
+
"LASTPHS": "ACQ_LAST_IN_PHASE",
|
|
90
|
+
"FIRSTREP": "ACQ_FIRST_IN_REPETITION",
|
|
91
|
+
"LASTREP": "ACQ_LAST_IN_REPETITION",
|
|
92
|
+
"FIRSTSET": "ACQ_FIRST_IN_SET",
|
|
93
|
+
"LASTSET": "ACQ_LAST_IN_SET",
|
|
94
|
+
"FIRSTSEG": "ACQ_FIRST_IN_SEGMENT",
|
|
95
|
+
"LASTSEG": "ACQ_LAST_IN_SEGMENT",
|
|
96
|
+
"LASTSCAN": "ACQ_LAST_IN_MEASUREMENT",
|
|
97
|
+
# classifying
|
|
98
|
+
"NOISE": "ACQ_IS_NOISE_MEASUREMENT",
|
|
99
|
+
"REF": "ACQ_IS_PARALLEL_CALIBRATION",
|
|
100
|
+
"IMA": "ACQ_IS_PARALLEL_CALIBRATION_AND_IMAGING",
|
|
101
|
+
"REV": "ACQ_IS_REVERSE",
|
|
102
|
+
"NAV": "ACQ_IS_NAVIGATION_DATA",
|
|
103
|
+
"PHASECORR": "ACQ_IS_PHASECORR_DATA",
|
|
104
|
+
"HPFEEDBACK": "ACQ_IS_HPFEEDBACK_DATA",
|
|
105
|
+
"DUMMYSCAN": "ACQ_IS_DUMMYSCAN_DATA",
|
|
106
|
+
"RTFEEDBACK": "ACQ_IS_RTFEEDBACK_DATA",
|
|
107
|
+
"COILCORR": "ACQ_IS_SURFACECOILCORRECTIONSCAN_DATA",
|
|
108
|
+
"PHSTABREF": "ACQ_IS_PHASE_STABILIZATION_REFERENCE",
|
|
109
|
+
"PHSTAB": "ACQ_IS_PHASE_STABILIZATION",
|
|
110
|
+
# transport
|
|
111
|
+
"COMPRESS1": "ACQ_COMPRESSION1",
|
|
112
|
+
"COMPRESS2": "ACQ_COMPRESSION2",
|
|
113
|
+
"COMPRESS3": "ACQ_COMPRESSION3",
|
|
114
|
+
"COMPRESS4": "ACQ_COMPRESSION4",
|
|
115
|
+
# free
|
|
116
|
+
"USERFLAG1": "ACQ_USER1",
|
|
117
|
+
"USERFLAG2": "ACQ_USER2",
|
|
118
|
+
"USERFLAG3": "ACQ_USER3",
|
|
119
|
+
"USERFLAG4": "ACQ_USER4",
|
|
120
|
+
"USERFLAG5": "ACQ_USER5",
|
|
121
|
+
"USERFLAG6": "ACQ_USER6",
|
|
122
|
+
"USERFLAG7": "ACQ_USER7",
|
|
123
|
+
"USERFLAG8": "ACQ_USER8",
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#: Flag labels the scanner interpreter consumes; they have no MRD counterpart.
|
|
127
|
+
SCANNER_FLAGS = ("PMC", "NOROT", "NOPOS", "NOSCL", "OFF", "ONCE", "TRID")
|
|
128
|
+
|
|
129
|
+
#: Every flag label. ``SMS`` carries a multiband group index, for which ISMRMRD
|
|
130
|
+
#: has no field; a band index a reconstruction needs travels in a ``USER*``
|
|
131
|
+
#: counter.
|
|
132
|
+
FLAG_LABELS = (*MRD_FLAGS, "SMS", *SCANNER_FLAGS)
|
|
133
|
+
|
|
134
|
+
_CANONICAL = frozenset(COUNTER_LABELS) | frozenset(FLAG_LABELS)
|
|
135
|
+
|
|
136
|
+
#: Non-canonical spellings. ``K1`` and ``K2`` are MRPro's ``AcqIdx`` names.
|
|
137
|
+
_ALIASES = {
|
|
138
|
+
**{mrd.upper(): name for name, mrd in MRD_COUNTERS.items()},
|
|
139
|
+
**{mrd: name for name, mrd in MRD_FLAGS.items()},
|
|
140
|
+
**{mrd[len("ACQ_") :]: name for name, mrd in MRD_FLAGS.items()},
|
|
141
|
+
"K1": "LIN",
|
|
142
|
+
"K2": "PAR",
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
#: Names that read as labels but are none, with the reason they are refused.
|
|
146
|
+
_REFUSED = {
|
|
147
|
+
"K0": (
|
|
148
|
+
"k0 is the readout direction, the samples within one acquisition, and "
|
|
149
|
+
"no counter indexes it; the readout extent is "
|
|
150
|
+
"encodingLimits.kspace_encoding_step_0"
|
|
151
|
+
),
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def canonical_label(name: str) -> str:
|
|
156
|
+
"""Return the canonical spelling of a label name.
|
|
157
|
+
|
|
158
|
+
Accepts the canonical name, an ISMRMRD counter field or flag constant name
|
|
159
|
+
(with or without ``ACQ_``), or MRPro's ``k1``/``k2``, in any case. An
|
|
160
|
+
unrecognised name is returned unchanged, so custom labels pass through.
|
|
161
|
+
|
|
162
|
+
Raises
|
|
163
|
+
------
|
|
164
|
+
ValueError
|
|
165
|
+
If the name is ``k0``, which reads as a label but indexes nothing.
|
|
166
|
+
|
|
167
|
+
Examples
|
|
168
|
+
--------
|
|
169
|
+
>>> from pulserver._labels import canonical_label
|
|
170
|
+
>>> canonical_label("kspace_encode_step_1")
|
|
171
|
+
'LIN'
|
|
172
|
+
>>> canonical_label("k1"), canonical_label("k2")
|
|
173
|
+
('LIN', 'PAR')
|
|
174
|
+
>>> canonical_label("ACQ_LAST_IN_SLICE"), canonical_label("LAST_IN_SLICE")
|
|
175
|
+
('LASTSLC', 'LASTSLC')
|
|
176
|
+
>>> canonical_label("LIN"), canonical_label("BIN")
|
|
177
|
+
('LIN', 'BIN')
|
|
178
|
+
"""
|
|
179
|
+
upper = name.upper()
|
|
180
|
+
if upper in _CANONICAL:
|
|
181
|
+
return upper
|
|
182
|
+
if upper in _REFUSED:
|
|
183
|
+
raise ValueError(f"{name!r} is not a label: {_REFUSED[upper]}")
|
|
184
|
+
return _ALIASES.get(upper, name)
|
pulserver/_logs.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Log configuration shared by the services."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import faulthandler
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
FORMAT = "%(asctime)s %(process)d %(levelname)s %(name)s: %(message)s"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def configure(
|
|
15
|
+
level: int | str = logging.INFO, logfile: Path | str | None = None
|
|
16
|
+
) -> None:
|
|
17
|
+
"""Log at ``level`` to the standard error, and send that to ``logfile`` when given.
|
|
18
|
+
|
|
19
|
+
``logfile`` is appended to by redirecting the standard output and error
|
|
20
|
+
descriptors, so ``print``, warnings, native output, a crash's traceback
|
|
21
|
+
and every child process the caller starts afterwards land in it too.
|
|
22
|
+
Output is unbuffered in this process and in spawned children, so lines
|
|
23
|
+
from several processes interleave in the order they were written.
|
|
24
|
+
"""
|
|
25
|
+
if logfile is not None:
|
|
26
|
+
redirect(logfile)
|
|
27
|
+
if isinstance(level, str):
|
|
28
|
+
level = getattr(logging, level.upper(), logging.INFO)
|
|
29
|
+
logging.basicConfig(level=level, format=FORMAT, force=True)
|
|
30
|
+
logging.captureWarnings(True)
|
|
31
|
+
faulthandler.enable()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def redirect(logfile: Path | str) -> None:
|
|
35
|
+
"""Append the standard output and error of this process to ``logfile``."""
|
|
36
|
+
sys.stdout.flush()
|
|
37
|
+
sys.stderr.flush()
|
|
38
|
+
descriptor = os.open(logfile, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o666)
|
|
39
|
+
os.dup2(descriptor, 1)
|
|
40
|
+
os.dup2(descriptor, 2)
|
|
41
|
+
os.close(descriptor)
|
|
42
|
+
os.environ["PYTHONUNBUFFERED"] = "1"
|
|
43
|
+
for stream in (sys.stdout, sys.stderr):
|
|
44
|
+
stream.reconfigure(line_buffering=True)
|
pulserver/_memory.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Process-wide memory settings for the pulserver services."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def without_huge_page_advice() -> None:
|
|
10
|
+
"""Stop NumPy advising the kernel to back large arrays with huge pages.
|
|
11
|
+
|
|
12
|
+
Under ``transparent_hugepage/defrag = madvise`` the first touch of an
|
|
13
|
+
advised array compacts memory, which then dominates building the readout
|
|
14
|
+
tables. Sets ``NUMPY_MADVISE_HUGEPAGE=0`` unless the environment already
|
|
15
|
+
chooses, and applies it to a NumPy already imported.
|
|
16
|
+
"""
|
|
17
|
+
os.environ.setdefault("NUMPY_MADVISE_HUGEPAGE", "0")
|
|
18
|
+
numpy = sys.modules.get("numpy")
|
|
19
|
+
if numpy is None or os.environ["NUMPY_MADVISE_HUGEPAGE"] != "0":
|
|
20
|
+
return
|
|
21
|
+
numpy._core.multiarray._set_madvise_hugepage(False)
|
pulserver/_plugins.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Plugin files, found by name along an ordered list of directories."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
#: Directories searched for ``<name>.py``, in order; a single directory is a
|
|
10
|
+
#: search path of one.
|
|
11
|
+
PluginPath = Path | str | Sequence[Path | str]
|
|
12
|
+
|
|
13
|
+
NAME = re.compile(r"[A-Za-z0-9_\-]+")
|
|
14
|
+
|
|
15
|
+
#: The shipped scanner sequences and reconstructions, searched after the
|
|
16
|
+
#: directories a caller gives, so a file of the same name there replaces one.
|
|
17
|
+
SEQUENCES = Path(__file__).parent / "_zoo" / "sequences"
|
|
18
|
+
RECONSTRUCTIONS = Path(__file__).parent / "_zoo" / "recon"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def directories(plugins: PluginPath) -> tuple[Path, ...]:
|
|
22
|
+
"""Return the directories of a search path, in search order."""
|
|
23
|
+
if isinstance(plugins, (str, Path)):
|
|
24
|
+
return (Path(plugins),)
|
|
25
|
+
return tuple(Path(directory) for directory in plugins)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def find(plugins: PluginPath, name: str) -> Path:
|
|
29
|
+
"""Return ``<directory>/<name>.py`` of the first directory holding it, the shipped plugins last.
|
|
30
|
+
|
|
31
|
+
A symbolic link is followed, so a link names the file it points to.
|
|
32
|
+
|
|
33
|
+
Raises
|
|
34
|
+
------
|
|
35
|
+
ValueError
|
|
36
|
+
If ``name`` is not a plugin name.
|
|
37
|
+
FileNotFoundError
|
|
38
|
+
If no directory holds it.
|
|
39
|
+
"""
|
|
40
|
+
if not NAME.fullmatch(name):
|
|
41
|
+
raise ValueError(f"invalid plugin name {name!r}")
|
|
42
|
+
given = directories(plugins)
|
|
43
|
+
for directory in (*given, SEQUENCES, RECONSTRUCTIONS):
|
|
44
|
+
path = directory / f"{name}.py"
|
|
45
|
+
if path.is_file():
|
|
46
|
+
return path
|
|
47
|
+
raise FileNotFoundError(
|
|
48
|
+
f"no plugin {name!r} in {', '.join(str(d) for d in given)}"
|
|
49
|
+
" or among the shipped plugins"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def names(plugins: PluginPath) -> list[str]:
|
|
54
|
+
"""Return the name of each scanner sequence :func:`find` can return, once, sorted.
|
|
55
|
+
|
|
56
|
+
The shipped sequences are listed; the shipped reconstructions are not. A
|
|
57
|
+
directory that does not exist holds none.
|
|
58
|
+
"""
|
|
59
|
+
return _stems((*directories(plugins), SEQUENCES))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def recon_names(plugins: PluginPath = ()) -> list[str]:
|
|
63
|
+
"""Return the name of each reconstruction :func:`find` can return, once, sorted.
|
|
64
|
+
|
|
65
|
+
The shipped reconstructions are listed; the shipped sequences are not. A
|
|
66
|
+
directory that does not exist holds none.
|
|
67
|
+
"""
|
|
68
|
+
return _stems((*directories(plugins), RECONSTRUCTIONS))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _stems(search: Sequence[Path]) -> list[str]:
|
|
72
|
+
return sorted(
|
|
73
|
+
{
|
|
74
|
+
path.stem
|
|
75
|
+
for directory in search
|
|
76
|
+
for path in directory.glob("*.py")
|
|
77
|
+
if NAME.fullmatch(path.stem) and path.is_file()
|
|
78
|
+
}
|
|
79
|
+
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""The shipped scanner sequences and reconstructions, and the pairs a console reconstructs them with."""
|
|
2
|
+
|
|
3
|
+
#: The shipped reconstruction plugin a console reconstructs each shipped
|
|
4
|
+
#: scanner-sequence plugin with when a scan names none. The stored design and
|
|
5
|
+
#: the reconstruction proxy do not read it.
|
|
6
|
+
ZOO_PAIRS: dict[str, str] = {
|
|
7
|
+
"bssfp2d": "pics",
|
|
8
|
+
"bssfp3d": "pics",
|
|
9
|
+
"epi2d": "epi",
|
|
10
|
+
"epi3d": "epi",
|
|
11
|
+
"fse3d": "pmc",
|
|
12
|
+
"gre2d": "pics",
|
|
13
|
+
"gre3d": "pics",
|
|
14
|
+
"gre_multiecho2d": "pics",
|
|
15
|
+
"gre_multiecho3d": "pics",
|
|
16
|
+
"gre_propeller2d": "nufft",
|
|
17
|
+
"gre_radial2d": "nufft",
|
|
18
|
+
"gre_spiral2d": "nufft",
|
|
19
|
+
"gre_stack_of_blades3d": "nufft",
|
|
20
|
+
"gre_stack_of_spirals3d": "nufft",
|
|
21
|
+
"gre_stack_of_stars3d": "nufft",
|
|
22
|
+
"mprage3d": "pmc",
|
|
23
|
+
"mprage_stack_of_spirals3d": "nufft_train",
|
|
24
|
+
"mprage_stack_of_stars3d": "nufft_train",
|
|
25
|
+
"se2d": "pics",
|
|
26
|
+
"se3d": "pics",
|
|
27
|
+
"se_epi_propeller2d": "nufft",
|
|
28
|
+
"se_propeller2d": "nufft",
|
|
29
|
+
"se_radial2d": "nufft",
|
|
30
|
+
"se_spiral2d": "nufft",
|
|
31
|
+
"se_stack_of_blades3d": "nufft",
|
|
32
|
+
"se_stack_of_spirals3d": "nufft",
|
|
33
|
+
"se_stack_of_stars3d": "nufft",
|
|
34
|
+
"zte3d": "nufft",
|
|
35
|
+
}
|