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.
Files changed (151) hide show
  1. pulserver/__init__.py +25 -0
  2. pulserver/_accelerators.py +68 -0
  3. pulserver/_cli.py +59 -0
  4. pulserver/_ext.cp313-win_amd64.pyd +0 -0
  5. pulserver/_labels.py +184 -0
  6. pulserver/_logs.py +44 -0
  7. pulserver/_memory.py +21 -0
  8. pulserver/_plugins.py +79 -0
  9. pulserver/_zoo/__init__.py +35 -0
  10. pulserver/_zoo/_evaluation.py +325 -0
  11. pulserver/_zoo/_inversion.py +110 -0
  12. pulserver/_zoo/_pmc.py +26 -0
  13. pulserver/_zoo/_saturation.py +118 -0
  14. pulserver/_zoo/_slab.py +37 -0
  15. pulserver/_zoo/_user.py +193 -0
  16. pulserver/_zoo/recon/epi.py +3 -0
  17. pulserver/_zoo/recon/nufft.py +3 -0
  18. pulserver/_zoo/recon/nufft_train.py +3 -0
  19. pulserver/_zoo/recon/pics.py +3 -0
  20. pulserver/_zoo/recon/pics_train.py +3 -0
  21. pulserver/_zoo/recon/pmc.py +3 -0
  22. pulserver/_zoo/sequences/bssfp2d.py +180 -0
  23. pulserver/_zoo/sequences/bssfp3d.py +90 -0
  24. pulserver/_zoo/sequences/epi2d.py +163 -0
  25. pulserver/_zoo/sequences/epi3d.py +206 -0
  26. pulserver/_zoo/sequences/fse3d.py +214 -0
  27. pulserver/_zoo/sequences/gre2d.py +75 -0
  28. pulserver/_zoo/sequences/gre3d.py +77 -0
  29. pulserver/_zoo/sequences/gre_multiecho2d.py +69 -0
  30. pulserver/_zoo/sequences/gre_multiecho3d.py +82 -0
  31. pulserver/_zoo/sequences/gre_propeller2d.py +56 -0
  32. pulserver/_zoo/sequences/gre_radial2d.py +55 -0
  33. pulserver/_zoo/sequences/gre_spiral2d.py +73 -0
  34. pulserver/_zoo/sequences/gre_stack_of_blades3d.py +61 -0
  35. pulserver/_zoo/sequences/gre_stack_of_spirals3d.py +81 -0
  36. pulserver/_zoo/sequences/gre_stack_of_stars3d.py +60 -0
  37. pulserver/_zoo/sequences/mprage3d.py +134 -0
  38. pulserver/_zoo/sequences/mprage_stack_of_spirals3d.py +97 -0
  39. pulserver/_zoo/sequences/mprage_stack_of_stars3d.py +71 -0
  40. pulserver/_zoo/sequences/se2d.py +71 -0
  41. pulserver/_zoo/sequences/se3d.py +72 -0
  42. pulserver/_zoo/sequences/se_epi_propeller2d.py +121 -0
  43. pulserver/_zoo/sequences/se_propeller2d.py +60 -0
  44. pulserver/_zoo/sequences/se_radial2d.py +59 -0
  45. pulserver/_zoo/sequences/se_spiral2d.py +77 -0
  46. pulserver/_zoo/sequences/se_stack_of_blades3d.py +65 -0
  47. pulserver/_zoo/sequences/se_stack_of_spirals3d.py +85 -0
  48. pulserver/_zoo/sequences/se_stack_of_stars3d.py +64 -0
  49. pulserver/_zoo/sequences/zte3d.py +90 -0
  50. pulserver/design/__init__.py +52 -0
  51. pulserver/design/_entries.py +643 -0
  52. pulserver/design/_options.py +160 -0
  53. pulserver/design/_plugin.py +711 -0
  54. pulserver/design/_rf.py +111 -0
  55. pulserver/host/__init__.py +14 -0
  56. pulserver/host/__main__.py +8 -0
  57. pulserver/host/_blocks.py +116 -0
  58. pulserver/host/_command.py +280 -0
  59. pulserver/host/_limits.py +328 -0
  60. pulserver/host/_push.py +66 -0
  61. pulserver/host/_server.py +262 -0
  62. pulserver/host/_service.py +625 -0
  63. pulserver/host/_store.py +398 -0
  64. pulserver/ir/__init__.py +48 -0
  65. pulserver/ir/_checks.py +519 -0
  66. pulserver/ir/_convert.py +655 -0
  67. pulserver/ir/_playout.py +174 -0
  68. pulserver/ir/_source.py +847 -0
  69. pulserver/ir/_vendor.py +113 -0
  70. pulserver/ir/_waves.py +210 -0
  71. pulserver/mrd/__init__.py +38 -0
  72. pulserver/mrd/_acquisitions.py +380 -0
  73. pulserver/mrd/_header.py +269 -0
  74. pulserver/mrd/_images.py +113 -0
  75. pulserver/mrd/_metadata.py +229 -0
  76. pulserver/mrd/_sequence.py +480 -0
  77. pulserver/protocol/__init__.py +89 -0
  78. pulserver/protocol/_keys.py +485 -0
  79. pulserver/protocol/_prescription.py +67 -0
  80. pulserver/protocol/_schema.py +141 -0
  81. pulserver/protocol/_wire.py +593 -0
  82. pulserver/proxy/__init__.py +27 -0
  83. pulserver/proxy/__main__.py +170 -0
  84. pulserver/proxy/_designs.py +162 -0
  85. pulserver/proxy/_enrich.py +799 -0
  86. pulserver/proxy/_held.py +154 -0
  87. pulserver/proxy/_intake.py +162 -0
  88. pulserver/proxy/_local.py +103 -0
  89. pulserver/proxy/_motion.py +292 -0
  90. pulserver/proxy/_proxy.py +1240 -0
  91. pulserver/proxy/_queue.py +71 -0
  92. pulserver/proxy/_seqdesc.py +351 -0
  93. pulserver/proxy/_workers.py +206 -0
  94. pulserver/recon/__init__.py +53 -0
  95. pulserver/recon/__main__.py +106 -0
  96. pulserver/recon/_buffers.py +765 -0
  97. pulserver/recon/_calibration.py +481 -0
  98. pulserver/recon/_correct.py +123 -0
  99. pulserver/recon/_exam_image.py +107 -0
  100. pulserver/recon/_figure.py +31 -0
  101. pulserver/recon/_loader.py +35 -0
  102. pulserver/recon/_replay.py +75 -0
  103. pulserver/recon/_runtime/__init__.py +1 -0
  104. pulserver/recon/_runtime/application.py +322 -0
  105. pulserver/recon/_runtime/concurrency.py +252 -0
  106. pulserver/recon/_runtime/connection.py +574 -0
  107. pulserver/recon/_runtime/constants.py +63 -0
  108. pulserver/recon/_runtime/exam.py +316 -0
  109. pulserver/recon/_runtime/mrd2dicom.py +1068 -0
  110. pulserver/recon/_runtime/offline.py +171 -0
  111. pulserver/recon/_runtime/readers.py +261 -0
  112. pulserver/recon/_runtime/writers.py +213 -0
  113. pulserver/recon/_units.py +115 -0
  114. pulserver/recon/gadgets.py +270 -0
  115. pulserver/recon/handlers/__init__.py +1 -0
  116. pulserver/recon/handlers/epi.py +211 -0
  117. pulserver/recon/handlers/nufft.py +260 -0
  118. pulserver/recon/handlers/nufft_train.py +34 -0
  119. pulserver/recon/handlers/pics.py +295 -0
  120. pulserver/recon/handlers/pics_train.py +37 -0
  121. pulserver/recon/handlers/pmc.py +128 -0
  122. pulserver/recon/plugin.py +1126 -0
  123. pulserver/validate/__init__.py +15 -0
  124. pulserver/validate/_command.py +105 -0
  125. pulserver/validate/_compare.py +274 -0
  126. pulserver/validate/_validate.py +270 -0
  127. pulserver/validate/_xml.py +162 -0
  128. pulserver/virtual/__init__.py +50 -0
  129. pulserver/virtual/_brainweb.py +381 -0
  130. pulserver/virtual/_client.py +178 -0
  131. pulserver/virtual/_coils.py +341 -0
  132. pulserver/virtual/_command.py +443 -0
  133. pulserver/virtual/_console.py +787 -0
  134. pulserver/virtual/_export.py +212 -0
  135. pulserver/virtual/_fourier.py +1977 -0
  136. pulserver/virtual/_girf.py +85 -0
  137. pulserver/virtual/_localizer.py +100 -0
  138. pulserver/virtual/_motion.py +191 -0
  139. pulserver/virtual/_phantom.py +293 -0
  140. pulserver/virtual/_stream.py +514 -0
  141. pulserver/virtual/_timeline.py +899 -0
  142. pulserver/virtual/_tissue.py +72 -0
  143. pulserver-0.4.4.dist-info/DELVEWHEEL +2 -0
  144. pulserver-0.4.4.dist-info/METADATA +244 -0
  145. pulserver-0.4.4.dist-info/RECORD +151 -0
  146. pulserver-0.4.4.dist-info/WHEEL +5 -0
  147. pulserver-0.4.4.dist-info/entry_points.txt +3 -0
  148. pulserver-0.4.4.dist-info/licenses/LICENSE +21 -0
  149. pulserver-0.4.4.dist-info/licenses/LICENSES/gadgetron-python-MIT.txt +21 -0
  150. pulserver-0.4.4.dist-info/licenses/LICENSES/python-ismrmrd-server-MIT.txt +21 -0
  151. 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
+ }