MNutils 1.2.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.
- mnutils/GEExam.py +495 -0
- mnutils/GESeries.py +1578 -0
- mnutils/__init__.py +54 -0
- mnutils/_scripts.py +177 -0
- mnutils/fitting/AMARES.py +831 -0
- mnutils/fitting/__init__.py +5 -0
- mnutils/matlab/__init__.py +13 -0
- mnutils/matlab/setup_matlab.py +116 -0
- mnutils/plotting/__init__.py +38 -0
- mnutils/plotting/_widgets/__init__.py +5 -0
- mnutils/plotting/_widgets/_html.py +34 -0
- mnutils/plotting/_widgets/_shared/__init__.py +50 -0
- mnutils/plotting/_widgets/_shared/dom.js +124 -0
- mnutils/plotting/_widgets/_shared/theme.css +140 -0
- mnutils/plotting/_widgets/image_grid.css +202 -0
- mnutils/plotting/_widgets/image_grid.js +212 -0
- mnutils/plotting/_widgets/image_grid.py +86 -0
- mnutils/plotting/_widgets/mrsi_inspector.css +376 -0
- mnutils/plotting/_widgets/mrsi_inspector.js +681 -0
- mnutils/plotting/_widgets/mrsi_inspector.py +70 -0
- mnutils/plotting/_widgets/slice_viewer.css +38 -0
- mnutils/plotting/_widgets/slice_viewer.js +41 -0
- mnutils/plotting/_widgets/slice_viewer.py +35 -0
- mnutils/plotting/images.py +1399 -0
- mnutils/plotting/saving.py +138 -0
- mnutils/plotting/spectra.py +357 -0
- mnutils/py.typed +0 -0
- mnutils/testing/__init__.py +3 -0
- mnutils/testing/_spectra.py +338 -0
- mnutils/testing/synthetic.py +373 -0
- mnutils/utils/__init__.py +25 -0
- mnutils/utils/data_loaders.py +218 -0
- mnutils/utils/file_helpers.py +708 -0
- mnutils/utils/images.py +202 -0
- mnutils/utils/nifti.py +379 -0
- mnutils/utils/spectra.py +149 -0
- mnutils-1.2.0.dist-info/METADATA +161 -0
- mnutils-1.2.0.dist-info/RECORD +41 -0
- mnutils-1.2.0.dist-info/WHEEL +4 -0
- mnutils-1.2.0.dist-info/entry_points.txt +7 -0
- mnutils-1.2.0.dist-info/licenses/LICENSE +28 -0
mnutils/__init__.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Utilities for working with multi-nuclear MR data from GE scanners.
|
|
2
|
+
|
|
3
|
+
The public surface is re-exported here, so the common entry points are
|
|
4
|
+
reachable without knowing the internal module layout::
|
|
5
|
+
|
|
6
|
+
import mnutils
|
|
7
|
+
|
|
8
|
+
exam = mnutils.DMIExam("/data/exam")
|
|
9
|
+
mnutils.plotting.images.display_images(exam.anatomical[0].data)
|
|
10
|
+
|
|
11
|
+
`mnutils.matlab` is deliberately *not* imported here: it needs the optional
|
|
12
|
+
`matlabengine` dependency, so import it explicitly when you need it.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
16
|
+
|
|
17
|
+
from . import GEExam, GESeries, fitting, plotting, testing, utils
|
|
18
|
+
from .GEExam import DMIExam, DMIinjExam, ExamBase, MS_DMIExam
|
|
19
|
+
from .GESeries import (
|
|
20
|
+
MRISeries,
|
|
21
|
+
MRSISeries,
|
|
22
|
+
MRSSeries,
|
|
23
|
+
MRSWashinSeries,
|
|
24
|
+
NiiBase,
|
|
25
|
+
RawMRISeries,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
__version__ = version("mnutils")
|
|
30
|
+
except PackageNotFoundError: # pragma: no cover - package not installed
|
|
31
|
+
__version__ = "0.0.0"
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
# subpackages / modules
|
|
35
|
+
"GEExam",
|
|
36
|
+
"GESeries",
|
|
37
|
+
"fitting",
|
|
38
|
+
"plotting",
|
|
39
|
+
"testing",
|
|
40
|
+
"utils",
|
|
41
|
+
# exam classes
|
|
42
|
+
"ExamBase",
|
|
43
|
+
"DMIExam",
|
|
44
|
+
"MS_DMIExam",
|
|
45
|
+
"DMIinjExam",
|
|
46
|
+
# series classes
|
|
47
|
+
"NiiBase",
|
|
48
|
+
"MRISeries",
|
|
49
|
+
"RawMRISeries",
|
|
50
|
+
"MRSISeries",
|
|
51
|
+
"MRSSeries",
|
|
52
|
+
"MRSWashinSeries",
|
|
53
|
+
"__version__",
|
|
54
|
+
]
|
mnutils/_scripts.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Thin `uv run <name>` wrappers around the MyST/quartodoc docs toolchain.
|
|
2
|
+
|
|
3
|
+
Simpler than the equivalent script in the sibling `xmris` package: MNUtils has
|
|
4
|
+
no interactive-widget config classes to auto-document, so this is just
|
|
5
|
+
`quartodoc build` (which emits Quarto Markdown, `.qmd`) followed by a
|
|
6
|
+
translation pass into plain MyST Markdown (`.md`), then subprocess wrappers
|
|
7
|
+
around `myst build`/`myst start`.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _find_project_root() -> Path:
|
|
18
|
+
"""Return the root of the MNUtils project, or exit if not found."""
|
|
19
|
+
for parent in Path(__file__).resolve().parents:
|
|
20
|
+
if (parent / "pyproject.toml").exists():
|
|
21
|
+
return parent
|
|
22
|
+
msg = "Error: could not find MNUtils project root (no pyproject.toml in any parent)"
|
|
23
|
+
raise RuntimeError(msg)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _get_docs_dir() -> Path:
|
|
27
|
+
docs_dir = _find_project_root() / "docs"
|
|
28
|
+
|
|
29
|
+
if not docs_dir.exists():
|
|
30
|
+
print(f"Error: 'docs' directory not found at: {docs_dir!s}")
|
|
31
|
+
sys.exit(1)
|
|
32
|
+
|
|
33
|
+
return docs_dir
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _translate_qmd_to_myst(api_dir: Path) -> None:
|
|
37
|
+
"""Convert quartodoc's `.qmd` output into plain MyST `.md`.
|
|
38
|
+
|
|
39
|
+
quartodoc targets Quarto by default; mystmd needs MyST Markdown instead.
|
|
40
|
+
This rewrites Quarto's `{.doc-method #anchor}` attribute blocks into
|
|
41
|
+
explicit `(anchor)=` MyST targets, drops leftover Quarto CSS blocks, and
|
|
42
|
+
downgrades any local link whose target didn't survive rather than
|
|
43
|
+
shipping a silently dead link. That downgrade is expected and routine
|
|
44
|
+
for plain attributes -- quartodoc's summary table links every attribute
|
|
45
|
+
by name, but (unlike methods) an attribute never gets its own anchored
|
|
46
|
+
section to link to, documented or not -- so it's counted, not printed
|
|
47
|
+
per occurrence; see the summary line `docs_api()` prints at the end.
|
|
48
|
+
"""
|
|
49
|
+
downgraded = 0
|
|
50
|
+
for qmd_file in api_dir.rglob("*.qmd"):
|
|
51
|
+
content = qmd_file.read_text(encoding="utf-8")
|
|
52
|
+
|
|
53
|
+
# Fix internal file extensions to point to standard Markdown.
|
|
54
|
+
content = content.replace(".qmd", ".md")
|
|
55
|
+
|
|
56
|
+
# Converts: ### Heading {.doc-method #anchor} -> (anchor)= \n ### Heading
|
|
57
|
+
content = re.sub(
|
|
58
|
+
r"^(.*?)\s*\{[^\}]*?#([\w\.\-]+)[^\}]*\}\s*$",
|
|
59
|
+
r"(\2)=\n\1",
|
|
60
|
+
content,
|
|
61
|
+
flags=re.MULTILINE,
|
|
62
|
+
)
|
|
63
|
+
# Strip any leftover pure CSS blocks that didn't have an ID (e.g. {.doc-signature}).
|
|
64
|
+
content = re.sub(r"\s*\{\.[^\}]+\}", "", content)
|
|
65
|
+
|
|
66
|
+
valid_targets = set(re.findall(r"^\(([\w\.\-]+)\)=", content, flags=re.MULTILINE))
|
|
67
|
+
|
|
68
|
+
def link_replacer(match: re.Match) -> str:
|
|
69
|
+
nonlocal downgraded
|
|
70
|
+
text, anchor = match.group(1), match.group(2)
|
|
71
|
+
if anchor in valid_targets:
|
|
72
|
+
return match.group(0)
|
|
73
|
+
downgraded += 1
|
|
74
|
+
return f"`{text}`"
|
|
75
|
+
|
|
76
|
+
content = re.sub(r"\[([^\]]+)\]\(#([\w\.\-]+)\)", link_replacer, content)
|
|
77
|
+
|
|
78
|
+
md_file = qmd_file.with_suffix(".md")
|
|
79
|
+
md_file.write_text(content, encoding="utf-8")
|
|
80
|
+
qmd_file.unlink()
|
|
81
|
+
|
|
82
|
+
# Give the generated landing page a stable MyST target -- quartodoc emits
|
|
83
|
+
# a bare `# Function reference` with no anchor, so without this every
|
|
84
|
+
# link into the API chapter would resolve against a URL instead.
|
|
85
|
+
index_file = api_dir / "index.md"
|
|
86
|
+
if index_file.exists():
|
|
87
|
+
index_text = index_file.read_text(encoding="utf-8")
|
|
88
|
+
if not index_text.startswith("(api-home)="):
|
|
89
|
+
index_file.write_text(f"(api-home)=\n{index_text.lstrip()}", encoding="utf-8")
|
|
90
|
+
|
|
91
|
+
if downgraded:
|
|
92
|
+
print(
|
|
93
|
+
f"docs-api: downgraded {downgraded} dead attribute link(s) to plain text "
|
|
94
|
+
"(expected -- attributes never get their own anchor)"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def docs_api() -> None:
|
|
99
|
+
"""Regenerate the quartodoc API reference stubs under docs/api/."""
|
|
100
|
+
docs_dir = _get_docs_dir()
|
|
101
|
+
api_dir = docs_dir / "api"
|
|
102
|
+
|
|
103
|
+
if api_dir.exists():
|
|
104
|
+
shutil.rmtree(api_dir)
|
|
105
|
+
api_dir.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
|
|
107
|
+
subprocess.run(["quartodoc", "build", "--config", "quartodoc.yml"], cwd=docs_dir, check=True)
|
|
108
|
+
_translate_qmd_to_myst(api_dir)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def docs_notebooks() -> None:
|
|
112
|
+
"""Serve a live preview of the docs, executing notebooks. Blocking."""
|
|
113
|
+
docs_dir = _get_docs_dir()
|
|
114
|
+
subprocess.run(["myst", "start", "--execute"], cwd=docs_dir, check=True)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def docs_all() -> None:
|
|
118
|
+
"""Regenerate API stubs, then serve a live preview. Blocking."""
|
|
119
|
+
docs_api()
|
|
120
|
+
docs_notebooks()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# Docs chapters whose pages are executed as tests. One directory per sidebar
|
|
124
|
+
# chapter, mirrored into tests/autogen_notebooks/<chapter>/. Keep in sync with
|
|
125
|
+
# GENRES in .claude/skills/docs-page/check_docs.py -- that file decides which
|
|
126
|
+
# house rules a chapter's pages are held to, this one decides which get run.
|
|
127
|
+
TEST_CHAPTERS = ("basics", "data-model", "plotting", "fitting", "nifti", "matlab")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def generate_test_notebooks() -> None:
|
|
131
|
+
"""Convert every executable page under TEST_CHAPTERS into a test notebook.
|
|
132
|
+
|
|
133
|
+
Mirrors xmris's docs-are-the-tests pattern: a page only joins the suite
|
|
134
|
+
once it carries a jupytext kernelspec (frontmatter-less pages, like a bare
|
|
135
|
+
chapter `index.md`, are skipped rather than failing with no kernel to
|
|
136
|
+
start). Output goes under tests/autogen_notebooks/, which is gitignored --
|
|
137
|
+
regenerate it before running pytest, never commit it.
|
|
138
|
+
"""
|
|
139
|
+
project_root = _find_project_root()
|
|
140
|
+
docs_dir = project_root / "docs"
|
|
141
|
+
out_root = project_root / "tests" / "autogen_notebooks"
|
|
142
|
+
|
|
143
|
+
if out_root.exists():
|
|
144
|
+
shutil.rmtree(out_root)
|
|
145
|
+
|
|
146
|
+
for chapter in TEST_CHAPTERS:
|
|
147
|
+
chapter_dir = docs_dir / chapter
|
|
148
|
+
if not chapter_dir.is_dir():
|
|
149
|
+
continue
|
|
150
|
+
for md_file in sorted(chapter_dir.glob("*.md")):
|
|
151
|
+
if not _is_executable_page(md_file):
|
|
152
|
+
continue
|
|
153
|
+
out_dir = out_root / chapter
|
|
154
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
155
|
+
subprocess.run(
|
|
156
|
+
[
|
|
157
|
+
"jupytext",
|
|
158
|
+
"--to",
|
|
159
|
+
"notebook",
|
|
160
|
+
"--output",
|
|
161
|
+
str(out_dir / f"{md_file.stem}.ipynb"),
|
|
162
|
+
str(md_file),
|
|
163
|
+
],
|
|
164
|
+
check=True,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _is_executable_page(md_file: Path) -> bool:
|
|
169
|
+
"""A page joins the test suite only if it carries a jupytext kernelspec."""
|
|
170
|
+
head = md_file.read_text(encoding="utf-8")[:400]
|
|
171
|
+
return head.startswith("---") and "kernelspec:" in head
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def run_tests() -> None:
|
|
175
|
+
"""Regenerate the notebook tests from docs, then run the full pytest suite."""
|
|
176
|
+
generate_test_notebooks()
|
|
177
|
+
subprocess.run(["pytest"], check=True)
|