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.
Files changed (41) hide show
  1. mnutils/GEExam.py +495 -0
  2. mnutils/GESeries.py +1578 -0
  3. mnutils/__init__.py +54 -0
  4. mnutils/_scripts.py +177 -0
  5. mnutils/fitting/AMARES.py +831 -0
  6. mnutils/fitting/__init__.py +5 -0
  7. mnutils/matlab/__init__.py +13 -0
  8. mnutils/matlab/setup_matlab.py +116 -0
  9. mnutils/plotting/__init__.py +38 -0
  10. mnutils/plotting/_widgets/__init__.py +5 -0
  11. mnutils/plotting/_widgets/_html.py +34 -0
  12. mnutils/plotting/_widgets/_shared/__init__.py +50 -0
  13. mnutils/plotting/_widgets/_shared/dom.js +124 -0
  14. mnutils/plotting/_widgets/_shared/theme.css +140 -0
  15. mnutils/plotting/_widgets/image_grid.css +202 -0
  16. mnutils/plotting/_widgets/image_grid.js +212 -0
  17. mnutils/plotting/_widgets/image_grid.py +86 -0
  18. mnutils/plotting/_widgets/mrsi_inspector.css +376 -0
  19. mnutils/plotting/_widgets/mrsi_inspector.js +681 -0
  20. mnutils/plotting/_widgets/mrsi_inspector.py +70 -0
  21. mnutils/plotting/_widgets/slice_viewer.css +38 -0
  22. mnutils/plotting/_widgets/slice_viewer.js +41 -0
  23. mnutils/plotting/_widgets/slice_viewer.py +35 -0
  24. mnutils/plotting/images.py +1399 -0
  25. mnutils/plotting/saving.py +138 -0
  26. mnutils/plotting/spectra.py +357 -0
  27. mnutils/py.typed +0 -0
  28. mnutils/testing/__init__.py +3 -0
  29. mnutils/testing/_spectra.py +338 -0
  30. mnutils/testing/synthetic.py +373 -0
  31. mnutils/utils/__init__.py +25 -0
  32. mnutils/utils/data_loaders.py +218 -0
  33. mnutils/utils/file_helpers.py +708 -0
  34. mnutils/utils/images.py +202 -0
  35. mnutils/utils/nifti.py +379 -0
  36. mnutils/utils/spectra.py +149 -0
  37. mnutils-1.2.0.dist-info/METADATA +161 -0
  38. mnutils-1.2.0.dist-info/RECORD +41 -0
  39. mnutils-1.2.0.dist-info/WHEEL +4 -0
  40. mnutils-1.2.0.dist-info/entry_points.txt +7 -0
  41. 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)