pymiox 0.1.0__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.
Potentially problematic release.
This version of pymiox might be problematic. Click here for more details.
- miox/__init__.py +79 -0
- miox/__main__.py +22 -0
- miox/audit.py +143 -0
- miox/cli.py +292 -0
- miox/data.py +1260 -0
- miox/matfile.py +94 -0
- miox/meta/__init__.py +22 -0
- miox/meta/keys.py +408 -0
- miox/meta/meta.py +61 -0
- miox/preview.py +454 -0
- miox/processing.py +766 -0
- miox/quicklook.py +1195 -0
- miox/quicklook_pages.py +1307 -0
- miox/readers/__init__.py +84 -0
- miox/readers/bart_cfl.py +113 -0
- miox/readers/base.py +380 -0
- miox/readers/basis_folder.py +110 -0
- miox/readers/biopac_acq.py +380 -0
- miox/readers/brainvoyager.py +485 -0
- miox/readers/brainvoyager_text.py +425 -0
- miox/readers/bruker.py +563 -0
- miox/readers/bruker_2dseq.py +150 -0
- miox/readers/bruker_raw.py +368 -0
- miox/readers/csdm.py +135 -0
- miox/readers/dicom_derived.py +1102 -0
- miox/readers/dicom_image.py +595 -0
- miox/readers/dicom_mrs.py +528 -0
- miox/readers/dmiwizard_map.py +66 -0
- miox/readers/dsi_studio.py +102 -0
- miox/readers/fastmri.py +103 -0
- miox/readers/fida_mat.py +109 -0
- miox/readers/fsl_mrs_basis.py +70 -0
- miox/readers/fsl_mrs_dynamic.py +107 -0
- miox/readers/fsl_vest.py +78 -0
- miox/readers/gadgetron_array.py +66 -0
- miox/readers/gava_midas_prior.py +90 -0
- miox/readers/ge_pfile.py +162 -0
- miox/readers/ge_pfile_raw.py +151 -0
- miox/readers/ge_rdb.py +429 -0
- miox/readers/ge_scanarchive.py +372 -0
- miox/readers/ge_signa.py +516 -0
- miox/readers/gipl.py +93 -0
- miox/readers/inspector_basis.py +72 -0
- miox/readers/ismrmrd.py +421 -0
- miox/readers/jcamp.py +324 -0
- miox/readers/jeol.py +242 -0
- miox/readers/jmrui.py +109 -0
- miox/readers/jnifti.py +244 -0
- miox/readers/lcmodel_basis.py +116 -0
- miox/readers/lcmodel_control.py +76 -0
- miox/readers/lcmodel_output.py +257 -0
- miox/readers/lcmodel_raw.py +201 -0
- miox/readers/loader.py +391 -0
- miox/readers/marss_basis.py +70 -0
- miox/readers/metaimage.py +172 -0
- miox/readers/mrd.py +514 -0
- miox/readers/mrs_sim_basis.py +87 -0
- miox/readers/mrtrix.py +205 -0
- miox/readers/nibabel_image.py +272 -0
- miox/readers/nifti_mrs.py +101 -0
- miox/readers/nifti_sidecar_mrs.py +111 -0
- miox/readers/nifti_zarr.py +80 -0
- miox/readers/nmrml.py +227 -0
- miox/readers/nmrpipe.py +164 -0
- miox/readers/nrrd.py +275 -0
- miox/readers/osprey_basis.py +79 -0
- miox/readers/osprey_container.py +81 -0
- miox/readers/philips_data_list.py +209 -0
- miox/readers/philips_dicom_raw.py +137 -0
- miox/readers/philips_examcard.py +194 -0
- miox/readers/philips_list_raw.py +150 -0
- miox/readers/philips_raw_lab.py +603 -0
- miox/readers/philips_spar_sdat.py +170 -0
- miox/readers/physio.py +453 -0
- miox/readers/profit_basis.py +72 -0
- miox/readers/pulseq.py +272 -0
- miox/readers/pyamares_prior.py +88 -0
- miox/readers/pyamares_result.py +57 -0
- miox/readers/rf_pulse.py +166 -0
- miox/readers/rnmrtk.py +82 -0
- miox/readers/rs2d.py +196 -0
- miox/readers/siemens_dicom_protocol.py +88 -0
- miox/readers/siemens_protocol.py +352 -0
- miox/readers/siemens_rda.py +84 -0
- miox/readers/siemens_twix.py +277 -0
- miox/readers/siemens_twix_raw.py +358 -0
- miox/readers/simpson.py +98 -0
- miox/readers/simulation.py +269 -0
- miox/readers/sivic_ddf.py +145 -0
- miox/readers/spant_rds.py +418 -0
- miox/readers/sparky.py +104 -0
- miox/readers/spinsolve.py +187 -0
- miox/readers/spinwizard_basis.py +87 -0
- miox/readers/spm_mat.py +110 -0
- miox/readers/stimulate.py +156 -0
- miox/readers/surfaces.py +262 -0
- miox/readers/tarquin_dpt.py +86 -0
- miox/readers/tarquin_output.py +182 -0
- miox/readers/tecmag.py +111 -0
- miox/readers/text_fid.py +66 -0
- miox/readers/toppe.py +187 -0
- miox/readers/topspin.py +306 -0
- miox/readers/tractograms.py +285 -0
- miox/readers/transforms.py +301 -0
- miox/readers/varian.py +170 -0
- miox/readers/varian_fdf.py +244 -0
- miox/readers/varian_raw.py +339 -0
- miox/readers/vasf.py +104 -0
- miox/readers/vespa_prior.py +77 -0
- miox/readers/vespa_viff.py +123 -0
- miox/readers/vista.py +138 -0
- miox/readers/vtk_legacy.py +294 -0
- miox/readers/vtk_xml.py +161 -0
- miox/readers/yarra.py +60 -0
- miox/sniff.py +2044 -0
- miox/viewer.py +990 -0
- miox/yardl.py +604 -0
- pymiox-0.1.0.dist-info/METADATA +87 -0
- pymiox-0.1.0.dist-info/RECORD +123 -0
- pymiox-0.1.0.dist-info/WHEEL +5 -0
- pymiox-0.1.0.dist-info/entry_points.txt +2 -0
- pymiox-0.1.0.dist-info/licenses/LICENSE +28 -0
- pymiox-0.1.0.dist-info/top_level.txt +1 -0
miox/__init__.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
####################################################################################################
|
|
2
|
+
# __init__.py #
|
|
3
|
+
####################################################################################################
|
|
4
|
+
# #
|
|
5
|
+
# Authors: J. P. Merkofer (j.p.merkofer@tue.nl) #
|
|
6
|
+
# #
|
|
7
|
+
# Created: 01/10/26 #
|
|
8
|
+
# #
|
|
9
|
+
# Purpose: Package entry: the version and the top-level API. #
|
|
10
|
+
# #
|
|
11
|
+
####################################################################################################
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
#*************#
|
|
15
|
+
# imports #
|
|
16
|
+
#*************#
|
|
17
|
+
|
|
18
|
+
# own
|
|
19
|
+
from miox.data import (
|
|
20
|
+
BasisSet,
|
|
21
|
+
FitResult,
|
|
22
|
+
MRImage,
|
|
23
|
+
MRSData,
|
|
24
|
+
NoiseCovariance,
|
|
25
|
+
Phantom,
|
|
26
|
+
PhysioData,
|
|
27
|
+
PriorSet,
|
|
28
|
+
Protocol,
|
|
29
|
+
Provenance,
|
|
30
|
+
PulseSequence,
|
|
31
|
+
RawData,
|
|
32
|
+
RelaxationData,
|
|
33
|
+
RFPulse,
|
|
34
|
+
Surface,
|
|
35
|
+
Tractogram,
|
|
36
|
+
Transform,
|
|
37
|
+
)
|
|
38
|
+
from miox.readers import IncompleteError, Loader, ReaderError, load
|
|
39
|
+
from miox.sniff import Format, Kind, Sniff, Sniffer, sniff
|
|
40
|
+
|
|
41
|
+
__version__ = "0.1.0"
|
|
42
|
+
__all__ = [
|
|
43
|
+
"BasisSet",
|
|
44
|
+
"FitResult",
|
|
45
|
+
"Format",
|
|
46
|
+
"IncompleteError",
|
|
47
|
+
"Kind",
|
|
48
|
+
"Loader",
|
|
49
|
+
"MRImage",
|
|
50
|
+
"MRSData",
|
|
51
|
+
"NoiseCovariance",
|
|
52
|
+
"Phantom",
|
|
53
|
+
"PhysioData",
|
|
54
|
+
"PriorSet",
|
|
55
|
+
"Protocol",
|
|
56
|
+
"Provenance",
|
|
57
|
+
"PulseSequence",
|
|
58
|
+
"RawData",
|
|
59
|
+
"ReaderError",
|
|
60
|
+
"RelaxationData",
|
|
61
|
+
"RFPulse",
|
|
62
|
+
"Sniff",
|
|
63
|
+
"Sniffer",
|
|
64
|
+
"Surface",
|
|
65
|
+
"Tractogram",
|
|
66
|
+
"Transform",
|
|
67
|
+
"__version__",
|
|
68
|
+
"load",
|
|
69
|
+
"sniff",
|
|
70
|
+
"view",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def __getattr__(name: str):
|
|
75
|
+
if name == "view": # on first use: matplotlib adds about 0.3 s to every import
|
|
76
|
+
from miox.viewer import view
|
|
77
|
+
|
|
78
|
+
return view
|
|
79
|
+
raise AttributeError(f"module 'miox' has no attribute {name!r}")
|
miox/__main__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
####################################################################################################
|
|
2
|
+
# __main__.py #
|
|
3
|
+
####################################################################################################
|
|
4
|
+
# #
|
|
5
|
+
# Authors: J. P. Merkofer (j.p.merkofer@tue.nl) #
|
|
6
|
+
# #
|
|
7
|
+
# Created: 12/10/26 #
|
|
8
|
+
# #
|
|
9
|
+
# Purpose: ``python -m miox``: the command line, as the ``miox`` console script. #
|
|
10
|
+
# #
|
|
11
|
+
####################################################################################################
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
#*************#
|
|
15
|
+
# imports #
|
|
16
|
+
#*************#
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
# own
|
|
20
|
+
from miox.cli import main
|
|
21
|
+
|
|
22
|
+
sys.exit(main())
|
miox/audit.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
####################################################################################################
|
|
2
|
+
# audit.py #
|
|
3
|
+
####################################################################################################
|
|
4
|
+
# #
|
|
5
|
+
# Authors: J. P. Merkofer (j.p.merkofer@tue.nl) #
|
|
6
|
+
# #
|
|
7
|
+
# Created: 01/10/26 #
|
|
8
|
+
# #
|
|
9
|
+
# Purpose: Coverage audit: every field of a file's vendor header, and whether a standard key, some #
|
|
10
|
+
# other key, or nothing is read from it. #
|
|
11
|
+
# #
|
|
12
|
+
####################################################################################################
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
#*************#
|
|
16
|
+
# imports #
|
|
17
|
+
#*************#
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from enum import StrEnum
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
# own
|
|
26
|
+
from miox.meta.keys import KeyTable
|
|
27
|
+
from miox.readers import Loader
|
|
28
|
+
from miox.sniff import Format
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
#**************************************************************************************************#
|
|
32
|
+
# Status #
|
|
33
|
+
#**************************************************************************************************#
|
|
34
|
+
# #
|
|
35
|
+
# What is read from a header field. #
|
|
36
|
+
# #
|
|
37
|
+
#**************************************************************************************************#
|
|
38
|
+
class Status(StrEnum):
|
|
39
|
+
STANDARD = "standard" # a key of the key table
|
|
40
|
+
OTHER = "other" # a key outside the key table, e.g. a user-defined NIfTI-MRS key
|
|
41
|
+
UNUSED = "unused" # nothing
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
#**************************************************************************************************#
|
|
45
|
+
# Field #
|
|
46
|
+
#**************************************************************************************************#
|
|
47
|
+
# #
|
|
48
|
+
# One field of an audited vendor header. #
|
|
49
|
+
# #
|
|
50
|
+
#**************************************************************************************************#
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class Field:
|
|
53
|
+
path: str
|
|
54
|
+
status: Status
|
|
55
|
+
key: str
|
|
56
|
+
"""The key read from this field, empty if none."""
|
|
57
|
+
value: str
|
|
58
|
+
"""Short preview of the value."""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
#**************************************************************************************************#
|
|
62
|
+
# Coverage #
|
|
63
|
+
#**************************************************************************************************#
|
|
64
|
+
# #
|
|
65
|
+
# The audit of one file: all fields of its vendor header, with counts and a Markdown table. #
|
|
66
|
+
# #
|
|
67
|
+
#**************************************************************************************************#
|
|
68
|
+
@dataclass(frozen=True)
|
|
69
|
+
class Coverage:
|
|
70
|
+
path: Path
|
|
71
|
+
format: Format
|
|
72
|
+
fields: tuple[Field, ...]
|
|
73
|
+
|
|
74
|
+
def summary(self) -> dict[Status, int]:
|
|
75
|
+
counts = dict.fromkeys(Status, 0)
|
|
76
|
+
for f in self.fields:
|
|
77
|
+
counts[f.status] += 1
|
|
78
|
+
return counts
|
|
79
|
+
|
|
80
|
+
def to_markdown(self) -> str:
|
|
81
|
+
s = self.summary()
|
|
82
|
+
lines = [
|
|
83
|
+
f"# Coverage: `{self.path.name}` ({self.format})",
|
|
84
|
+
"",
|
|
85
|
+
f"{len(self.fields)} header fields: {s[Status.STANDARD]} read into standard keys, "
|
|
86
|
+
f"{s[Status.OTHER]} into other keys, {s[Status.UNUSED]} unused.",
|
|
87
|
+
"",
|
|
88
|
+
"| field | status | key | value |",
|
|
89
|
+
"|---|---|---|---|",
|
|
90
|
+
]
|
|
91
|
+
for f in self.fields:
|
|
92
|
+
lines.append(f"| `{f.path}` | {f.status} | {f.key} | {f.value} |")
|
|
93
|
+
return "\n".join(lines) + "\n"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
#**************************************************************************************************#
|
|
97
|
+
# Auditor #
|
|
98
|
+
#**************************************************************************************************#
|
|
99
|
+
# #
|
|
100
|
+
# Loads a file and matches its vendor header against the fields its keys were read from. A key #
|
|
101
|
+
# read from a nested block covers every field inside it. #
|
|
102
|
+
# #
|
|
103
|
+
#**************************************************************************************************#
|
|
104
|
+
class Auditor:
|
|
105
|
+
PREVIEW_LIMIT = 60 # characters of a value shown in the table
|
|
106
|
+
|
|
107
|
+
def __init__(self):
|
|
108
|
+
self.loader = Loader()
|
|
109
|
+
self.keys = KeyTable()
|
|
110
|
+
|
|
111
|
+
def audit(self, path: str | Path) -> Coverage:
|
|
112
|
+
"""Audit the metadata coverage of one file (all datasets in it)."""
|
|
113
|
+
path = Path(path)
|
|
114
|
+
datasets = self.loader.load(path)
|
|
115
|
+
read_from = {} # vendor field -> key
|
|
116
|
+
for d in datasets:
|
|
117
|
+
read_from.update({field: key for key, field in d.meta.fields.items()})
|
|
118
|
+
fields = []
|
|
119
|
+
for field, value in datasets[0].meta.vendor_fields().items():
|
|
120
|
+
key = self._key(field, read_from)
|
|
121
|
+
if not key:
|
|
122
|
+
status = Status.UNUSED
|
|
123
|
+
elif key in self.keys:
|
|
124
|
+
status = Status.STANDARD
|
|
125
|
+
else:
|
|
126
|
+
status = Status.OTHER
|
|
127
|
+
fields.append(Field(field, status, key, self._preview(value)))
|
|
128
|
+
return Coverage(path, datasets[0].provenance.format, tuple(fields))
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def _key(field: str, read_from: dict[str, str]) -> str:
|
|
132
|
+
parts = field.split("/")
|
|
133
|
+
for n in range(len(parts), 0, -1):
|
|
134
|
+
key = read_from.get("/".join(parts[:n]))
|
|
135
|
+
if key:
|
|
136
|
+
return key
|
|
137
|
+
return ""
|
|
138
|
+
|
|
139
|
+
def _preview(self, value) -> str:
|
|
140
|
+
text = json.dumps(value, default=str)
|
|
141
|
+
if len(text) > self.PREVIEW_LIMIT:
|
|
142
|
+
text = text[: self.PREVIEW_LIMIT - 1] + "…"
|
|
143
|
+
return text.replace("|", "\\|")
|
miox/cli.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
####################################################################################################
|
|
2
|
+
# cli.py #
|
|
3
|
+
####################################################################################################
|
|
4
|
+
# #
|
|
5
|
+
# Authors: J. P. Merkofer (j.p.merkofer@tue.nl) #
|
|
6
|
+
# #
|
|
7
|
+
# Created: 01/10/26 #
|
|
8
|
+
# #
|
|
9
|
+
# Purpose: Console entry point for the ``miox`` command: ``info`` (what a file holds: data #
|
|
10
|
+
# shape, standard keys and their sources), ``sniff`` (format of files or directories), #
|
|
11
|
+
# ``audit`` (metadata coverage table of a file) and ``view`` (a quick look at what a #
|
|
12
|
+
# file holds, in a window or saved to a file). #
|
|
13
|
+
# #
|
|
14
|
+
####################################################################################################
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
#*************#
|
|
18
|
+
# imports #
|
|
19
|
+
#*************#
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import io
|
|
24
|
+
import json
|
|
25
|
+
import sys
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
# own
|
|
29
|
+
from miox import __version__
|
|
30
|
+
from miox.audit import Auditor
|
|
31
|
+
from miox.data import (
|
|
32
|
+
FitResult,
|
|
33
|
+
MRImage,
|
|
34
|
+
MRSData,
|
|
35
|
+
NoiseCovariance,
|
|
36
|
+
Phantom,
|
|
37
|
+
PhysioData,
|
|
38
|
+
PriorSet,
|
|
39
|
+
Protocol,
|
|
40
|
+
PulseSequence,
|
|
41
|
+
RawData,
|
|
42
|
+
RelaxationData,
|
|
43
|
+
RFPulse,
|
|
44
|
+
Surface,
|
|
45
|
+
Tractogram,
|
|
46
|
+
Transform,
|
|
47
|
+
)
|
|
48
|
+
from miox.readers import Loader, ReaderError
|
|
49
|
+
from miox.sniff import Format, Kind, Sniffer
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
#**************************************************************************************************#
|
|
53
|
+
# CLI #
|
|
54
|
+
#**************************************************************************************************#
|
|
55
|
+
# #
|
|
56
|
+
# The ``miox`` command line: one method per subcommand, each returning the exit status. #
|
|
57
|
+
# #
|
|
58
|
+
#**************************************************************************************************#
|
|
59
|
+
class CLI:
|
|
60
|
+
NOT_SPECTROSCOPY = (Kind.IMAGING, Kind.TRACTOGRAM, Kind.SURFACE, Kind.TRANSFORM, Kind.PHYSIO,
|
|
61
|
+
Kind.SEQUENCE, Kind.PROTOCOL, Kind.PHANTOM, Kind.REPORT)
|
|
62
|
+
def __init__(self):
|
|
63
|
+
self.parser = argparse.ArgumentParser(prog="miox", description="Open any MR file.")
|
|
64
|
+
self.parser.add_argument("--version", action="version", version=f"miox {__version__}")
|
|
65
|
+
commands = self.parser.add_subparsers(dest="command", required=True)
|
|
66
|
+
|
|
67
|
+
info = commands.add_parser("info", help="data shape, standard keys and their sources")
|
|
68
|
+
info.add_argument("paths", nargs="+", type=Path)
|
|
69
|
+
info.add_argument("--json", action="store_true", help="one JSON object per dataset")
|
|
70
|
+
info.set_defaults(func=self.info)
|
|
71
|
+
|
|
72
|
+
sniff = commands.add_parser("sniff", help="identify the format of files or directories")
|
|
73
|
+
sniff.add_argument("paths", nargs="+", type=Path)
|
|
74
|
+
sniff.add_argument("--json", action="store_true", help="one JSON object per line")
|
|
75
|
+
sniff.set_defaults(func=self.sniff)
|
|
76
|
+
|
|
77
|
+
audit = commands.add_parser("audit", help="metadata coverage table for a file (Markdown)")
|
|
78
|
+
audit.add_argument("path", type=Path)
|
|
79
|
+
audit.set_defaults(func=self.audit)
|
|
80
|
+
|
|
81
|
+
view = commands.add_parser("view", help="quick look in a window or saved to a file")
|
|
82
|
+
view.add_argument("paths", nargs="+", type=Path)
|
|
83
|
+
view.add_argument("--save", type=Path, help="save to this file instead of a window "
|
|
84
|
+
"(several datasets: name-1.png, name-2.png, ...)")
|
|
85
|
+
view.set_defaults(func=self.view)
|
|
86
|
+
|
|
87
|
+
preview = commands.add_parser("preview", help="one picture of a spectroscopy file; with "
|
|
88
|
+
"--to x.html the Quick Look page of a file of any kind")
|
|
89
|
+
preview.add_argument("path", type=Path)
|
|
90
|
+
preview.add_argument("--to", default=None, help="file to write (format by suffix: html "
|
|
91
|
+
"for the Quick Look page, pdf, png, svg), or - for PDF on standard "
|
|
92
|
+
"output; default: a window")
|
|
93
|
+
preview.set_defaults(func=self.preview)
|
|
94
|
+
|
|
95
|
+
def run(self, argv: list[str] | None = None) -> int:
|
|
96
|
+
args = self.parser.parse_args(argv)
|
|
97
|
+
return args.func(args)
|
|
98
|
+
|
|
99
|
+
def info(self, args: argparse.Namespace) -> int:
|
|
100
|
+
"""Summary of every dataset in every path; status 1 if any path cannot be read."""
|
|
101
|
+
loader = Loader()
|
|
102
|
+
status = 0
|
|
103
|
+
for path in args.paths:
|
|
104
|
+
try:
|
|
105
|
+
datasets = loader.load(path)
|
|
106
|
+
except ReaderError as e:
|
|
107
|
+
print(f"miox info: {e}", file=sys.stderr)
|
|
108
|
+
status = 1
|
|
109
|
+
continue
|
|
110
|
+
for d in datasets:
|
|
111
|
+
print(json.dumps(d.to_dict(), default=str) if args.json else self._describe(d))
|
|
112
|
+
return status
|
|
113
|
+
|
|
114
|
+
def _describe(self, d) -> str:
|
|
115
|
+
lines = [f"{d.provenance.paths[0]}: {d.provenance.format} ({d.provenance.reader})"]
|
|
116
|
+
if isinstance(d, RawData):
|
|
117
|
+
trajectory = ", with trajectory" if d.trajectory is not None else ""
|
|
118
|
+
waveforms = f", {len(d.waveforms)} waveforms" if d.waveforms else ""
|
|
119
|
+
lines.append(
|
|
120
|
+
f" {d.name}: {d.n_readouts} readouts x {d.n_coils} coils x "
|
|
121
|
+
f"{d.data.shape[-1]} samples, {d.data.dtype}{trajectory}{waveforms}"
|
|
122
|
+
)
|
|
123
|
+
elif isinstance(d, MRImage):
|
|
124
|
+
dims = " ".join(f"{name}={n}" for name, n in zip(d.dims, d.shape, strict=True))
|
|
125
|
+
sizes = " x ".join(f"{v:.4g}" for v in d.voxel_sizes)
|
|
126
|
+
lines.append(f" {d.name}: {dims}, {d.data.dtype}, voxels {sizes} mm")
|
|
127
|
+
elif isinstance(d, PriorSet):
|
|
128
|
+
lines.append(f" {d.name}: {len(d.metabolites)} metabolites, {len(d.lines)} lines")
|
|
129
|
+
elif isinstance(d, Protocol):
|
|
130
|
+
where = f" in {' / '.join(d.location)}" if d.location else ""
|
|
131
|
+
lines.append(f" protocol {d.name}{where}")
|
|
132
|
+
elif isinstance(d, Phantom):
|
|
133
|
+
lines.append(f" {d.name}: {d.n_spins} spins, {', '.join(d.properties)}")
|
|
134
|
+
elif isinstance(d, Tractogram):
|
|
135
|
+
lines.append(f" {d.name}: {d.n_streamlines} streamlines, {d.n_points} "
|
|
136
|
+
f"points ({d.space})")
|
|
137
|
+
elif isinstance(d, Surface):
|
|
138
|
+
faces = f", {d.faces.shape[0]} faces" if d.faces is not None else ""
|
|
139
|
+
data = ", ".join(list(d.vertex_data) + list(d.labels))
|
|
140
|
+
lines.append(f" {d.name}: {d.n_vertices} vertices{faces}{', ' + data if data else ''}")
|
|
141
|
+
elif isinstance(d, Transform):
|
|
142
|
+
lines.append(f" {d.name}: {d.kind} transform ({d.convention})")
|
|
143
|
+
elif isinstance(d, PhysioData):
|
|
144
|
+
rate = f" at {d.sampling_frequency:g} Hz" if d.sampling_frequency else ""
|
|
145
|
+
events = "".join(f", {len(v)} {k}" for k, v in d.events.items())
|
|
146
|
+
channels = ", ".join(d.channels)
|
|
147
|
+
lines.append(f" {d.name}: {d.n_samples} samples of {channels}{rate}{events}")
|
|
148
|
+
elif isinstance(d, RFPulse):
|
|
149
|
+
duration = f", {d.duration * 1e3:.4g} ms" if d.duration is not None else ""
|
|
150
|
+
lines.append(f" {d.name}: RF pulse, {d.n_samples} samples{duration}, "
|
|
151
|
+
f"amplitude in {d.amplitude_unit}")
|
|
152
|
+
elif isinstance(d, RelaxationData):
|
|
153
|
+
curves = f" x {d.n_curves} curves" if d.n_curves > 1 else ""
|
|
154
|
+
lines.append(f" {d.name}: {d.sequence} relaxation, {d.n_points} points{curves}")
|
|
155
|
+
elif isinstance(d, NoiseCovariance):
|
|
156
|
+
lines.append(
|
|
157
|
+
f" {d.name}: noise covariance of {d.n_coils} channels, {d.sample_count} samples"
|
|
158
|
+
)
|
|
159
|
+
elif isinstance(d, PulseSequence):
|
|
160
|
+
events = ", ".join(f"{len(t)} {n}" for n, t in d.events.items())
|
|
161
|
+
duration = f", {d.duration:.4g} s" if d.duration is not None else ""
|
|
162
|
+
lines.append(f" {d.name}: Pulseq {d.version}, {d.n_blocks} blocks{duration}; {events}")
|
|
163
|
+
elif isinstance(d, FitResult):
|
|
164
|
+
voxel = f", voxel {d.voxel}" if d.voxel else ""
|
|
165
|
+
curves = f", curves: {', '.join(d.curves)}" if d.curves else ""
|
|
166
|
+
lines.append(f" {d.name}: {len(d.rows)} metabolites{voxel}{curves}")
|
|
167
|
+
lines.extend(f" {name}: {value}" for name, value in d.diagnostics.items())
|
|
168
|
+
else:
|
|
169
|
+
dims = " ".join(f"{name}={n}" for name, n in zip(d.dims, d.shape, strict=True))
|
|
170
|
+
width = "" if d.spectral_width is None else f" ({d.spectral_width:.6g} Hz)"
|
|
171
|
+
lines += [f" {d.name}: {dims}", f" {d.acquisition()}{width}, {d.n_points} points"]
|
|
172
|
+
width = max((len(key) for key in d.meta), default=0)
|
|
173
|
+
for key, value in d.meta.items():
|
|
174
|
+
text = json.dumps(value, default=str)
|
|
175
|
+
if len(text) > 50:
|
|
176
|
+
text = text[:49] + "…"
|
|
177
|
+
lines.append(f" {key:<{width}} {text:<50} {d.meta.source[key]}")
|
|
178
|
+
lines.append(f" vendor header: {len(d.meta.vendor_fields())} fields")
|
|
179
|
+
lines.extend(f" note: {note}" for note in d.provenance.notes)
|
|
180
|
+
return "\n".join(lines)
|
|
181
|
+
|
|
182
|
+
def sniff(self, args: argparse.Namespace) -> int:
|
|
183
|
+
"""One line (or JSON object) per path; status 1 if any path is unknown."""
|
|
184
|
+
sniffer = Sniffer()
|
|
185
|
+
status = 0
|
|
186
|
+
for path in args.paths:
|
|
187
|
+
result = sniffer.sniff(path)
|
|
188
|
+
if result.format == Format.UNKNOWN:
|
|
189
|
+
status = 1
|
|
190
|
+
print(json.dumps(result.to_dict()) if args.json else result)
|
|
191
|
+
return status
|
|
192
|
+
|
|
193
|
+
def audit(self, args: argparse.Namespace) -> int:
|
|
194
|
+
"""Coverage table as Markdown; status 2 if the file cannot be read."""
|
|
195
|
+
try:
|
|
196
|
+
print(Auditor().audit(args.path).to_markdown(), end="")
|
|
197
|
+
except ReaderError as e:
|
|
198
|
+
print(f"miox audit: {e}", file=sys.stderr)
|
|
199
|
+
return 2
|
|
200
|
+
return 0
|
|
201
|
+
|
|
202
|
+
def view(self, args: argparse.Namespace) -> int:
|
|
203
|
+
"""Every dataset that has a view; status 1 if a path cannot be read or nothing is shown."""
|
|
204
|
+
from miox.viewer import VIEWS, show, view # matplotlib only when viewing
|
|
205
|
+
|
|
206
|
+
loader = Loader()
|
|
207
|
+
status, datasets = 0, []
|
|
208
|
+
for path in args.paths:
|
|
209
|
+
try:
|
|
210
|
+
loaded = loader.load(path)
|
|
211
|
+
except ReaderError as e:
|
|
212
|
+
print(f"miox view: {e}", file=sys.stderr)
|
|
213
|
+
status = 1
|
|
214
|
+
continue
|
|
215
|
+
for d in loaded:
|
|
216
|
+
if isinstance(d, tuple(VIEWS)):
|
|
217
|
+
datasets.append(d)
|
|
218
|
+
else:
|
|
219
|
+
print(f"miox view: no view for {type(d).__name__} yet ({d.name})",
|
|
220
|
+
file=sys.stderr)
|
|
221
|
+
if not datasets:
|
|
222
|
+
return 1
|
|
223
|
+
if args.save is None:
|
|
224
|
+
show(datasets)
|
|
225
|
+
elif len(datasets) == 1:
|
|
226
|
+
view(datasets[0], args.save)
|
|
227
|
+
else:
|
|
228
|
+
for n, d in enumerate(datasets, 1):
|
|
229
|
+
view(d, args.save.with_name(f"{args.save.stem}-{n}{args.save.suffix}"))
|
|
230
|
+
return status
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def preview(self, args: argparse.Namespace) -> int:
|
|
234
|
+
"""Status 2 when the file holds no spectroscopy data (files whose format cannot hold any
|
|
235
|
+
are declined before loading), 1 when it cannot be read."""
|
|
236
|
+
from miox.preview import Preview, preview # matplotlib only when previewing
|
|
237
|
+
|
|
238
|
+
sniffed = Sniffer().sniff(args.path)
|
|
239
|
+
page = bool(args.to) and str(args.to).endswith(".html") # the Quick Look page: every kind
|
|
240
|
+
declined = {Kind.REPORT} if page else set(self.NOT_SPECTROSCOPY)
|
|
241
|
+
if sniffed.format == Format.UNKNOWN or sniffed.kind in declined:
|
|
242
|
+
what = "has no page" if page else "is not spectroscopy"
|
|
243
|
+
print(f"miox preview: {args.path.name} {what} ({sniffed.format})", file=sys.stderr)
|
|
244
|
+
return 2
|
|
245
|
+
if page:
|
|
246
|
+
from miox.quicklook import load, write
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
datasets, notes = load(args.path)
|
|
250
|
+
write(datasets, args.path.name, args.to, source=args.path, notes=notes)
|
|
251
|
+
except (ReaderError, ValueError) as e:
|
|
252
|
+
print(f"miox preview: {e}", file=sys.stderr)
|
|
253
|
+
return 1 if isinstance(e, ReaderError) else 2
|
|
254
|
+
return 0
|
|
255
|
+
window = None
|
|
256
|
+
if args.to is None:
|
|
257
|
+
import matplotlib.pyplot as plt
|
|
258
|
+
|
|
259
|
+
window = plt.figure(figsize=Preview.SIZE)
|
|
260
|
+
try:
|
|
261
|
+
datasets = [d for d in Loader().load(args.path) if isinstance(d, MRSData)]
|
|
262
|
+
except ReaderError as e:
|
|
263
|
+
print(f"miox preview: {e}", file=sys.stderr)
|
|
264
|
+
return 1
|
|
265
|
+
if not datasets:
|
|
266
|
+
print(f"miox preview: {args.path.name} holds no spectroscopy data", file=sys.stderr)
|
|
267
|
+
return 2
|
|
268
|
+
figure = preview(datasets, title=args.path.name, figure=window)
|
|
269
|
+
if window is not None:
|
|
270
|
+
plt.show()
|
|
271
|
+
elif args.to == "-":
|
|
272
|
+
buffer = io.BytesIO()
|
|
273
|
+
figure.savefig(buffer, format="pdf")
|
|
274
|
+
sys.stdout.buffer.write(buffer.getvalue())
|
|
275
|
+
else:
|
|
276
|
+
figure.savefig(args.to, dpi=150)
|
|
277
|
+
return 0
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
#**************************************************************************************************#
|
|
281
|
+
# main #
|
|
282
|
+
#**************************************************************************************************#
|
|
283
|
+
# #
|
|
284
|
+
# Console-script entry point. #
|
|
285
|
+
# #
|
|
286
|
+
#**************************************************************************************************#
|
|
287
|
+
def main(argv: list[str] | None = None) -> int:
|
|
288
|
+
return CLI().run(argv)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
if __name__ == "__main__":
|
|
292
|
+
sys.exit(main())
|