dcccpy 0.3.0a3__tar.gz → 0.3.0a4__tar.gz
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.
- {dcccpy-0.3.0a3/src/dcccpy.egg-info → dcccpy-0.3.0a4}/PKG-INFO +42 -12
- {dcccpy-0.3.0a3 → dcccpy-0.3.0a4}/README.md +33 -3
- {dcccpy-0.3.0a3 → dcccpy-0.3.0a4}/pyproject.toml +9 -9
- {dcccpy-0.3.0a3 → dcccpy-0.3.0a4}/src/dcccpy/__init__.py +5 -0
- {dcccpy-0.3.0a3 → dcccpy-0.3.0a4}/src/dcccpy/core.py +42 -0
- {dcccpy-0.3.0a3 → dcccpy-0.3.0a4}/src/dcccpy/runtime.py +1 -1
- dcccpy-0.3.0a4/src/dcccpy/segmentation.py +123 -0
- {dcccpy-0.3.0a3 → dcccpy-0.3.0a4/src/dcccpy.egg-info}/PKG-INFO +42 -12
- {dcccpy-0.3.0a3 → dcccpy-0.3.0a4}/src/dcccpy.egg-info/SOURCES.txt +2 -0
- {dcccpy-0.3.0a3 → dcccpy-0.3.0a4}/src/dcccpy.egg-info/requires.txt +8 -8
- {dcccpy-0.3.0a3 → dcccpy-0.3.0a4}/tests/test_core.py +73 -7
- dcccpy-0.3.0a4/tests/test_seg_distribution.py +145 -0
- {dcccpy-0.3.0a3 → dcccpy-0.3.0a4}/MANIFEST.in +0 -0
- {dcccpy-0.3.0a3 → dcccpy-0.3.0a4}/setup.cfg +0 -0
- {dcccpy-0.3.0a3 → dcccpy-0.3.0a4}/setup.py +0 -0
- {dcccpy-0.3.0a3 → dcccpy-0.3.0a4}/src/dcccpy/cli.py +0 -0
- {dcccpy-0.3.0a3 → dcccpy-0.3.0a4}/src/dcccpy/py.typed +0 -0
- {dcccpy-0.3.0a3 → dcccpy-0.3.0a4}/src/dcccpy.egg-info/dependency_links.txt +0 -0
- {dcccpy-0.3.0a3 → dcccpy-0.3.0a4}/src/dcccpy.egg-info/entry_points.txt +0 -0
- {dcccpy-0.3.0a3 → dcccpy-0.3.0a4}/src/dcccpy.egg-info/top_level.txt +0 -0
- {dcccpy-0.3.0a3 → dcccpy-0.3.0a4}/tests/test_testpypi_centiloid.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dcccpy
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.0a4
|
|
4
4
|
Summary: Python wrapper for the DCCCcore PET biomarker command-line tool
|
|
5
5
|
Author: DCCCSlicer contributors
|
|
6
6
|
License-Expression: CC-BY-NC-ND-4.0
|
|
@@ -17,20 +17,50 @@ Requires-Python: >=3.9
|
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
Requires-Dist: nibabel>=5
|
|
19
19
|
Provides-Extra: linux-runtime
|
|
20
|
-
Requires-Dist: dcccpy-linux-runtime==0.2.
|
|
21
|
-
Requires-Dist: dcccpy-linux-arm64-runtime==0.2.
|
|
20
|
+
Requires-Dist: dcccpy-linux-runtime==0.2.0a3; (sys_platform == "linux" and platform_machine == "x86_64") and extra == "linux-runtime"
|
|
21
|
+
Requires-Dist: dcccpy-linux-arm64-runtime==0.2.0a3; (sys_platform == "linux" and platform_machine == "aarch64") and extra == "linux-runtime"
|
|
22
22
|
Provides-Extra: windows-runtime
|
|
23
|
-
Requires-Dist: dcccpy-windows-runtime==0.2.
|
|
23
|
+
Requires-Dist: dcccpy-windows-runtime==0.2.0a3; (sys_platform == "win32" and platform_machine == "AMD64") and extra == "windows-runtime"
|
|
24
24
|
Provides-Extra: macos-runtime
|
|
25
|
-
Requires-Dist: dcccpy-macos-runtime==0.2.
|
|
25
|
+
Requires-Dist: dcccpy-macos-runtime==0.2.0a3; (sys_platform == "darwin" and platform_machine == "arm64") and extra == "macos-runtime"
|
|
26
26
|
Provides-Extra: runtime
|
|
27
|
-
Requires-Dist: dcccpy-linux-runtime==0.2.
|
|
28
|
-
Requires-Dist: dcccpy-linux-arm64-runtime==0.2.
|
|
29
|
-
Requires-Dist: dcccpy-windows-runtime==0.2.
|
|
30
|
-
Requires-Dist: dcccpy-macos-runtime==0.2.
|
|
27
|
+
Requires-Dist: dcccpy-linux-runtime==0.2.0a3; (sys_platform == "linux" and platform_machine == "x86_64") and extra == "runtime"
|
|
28
|
+
Requires-Dist: dcccpy-linux-arm64-runtime==0.2.0a3; (sys_platform == "linux" and platform_machine == "aarch64") and extra == "runtime"
|
|
29
|
+
Requires-Dist: dcccpy-windows-runtime==0.2.0a3; (sys_platform == "win32" and platform_machine == "AMD64") and extra == "runtime"
|
|
30
|
+
Requires-Dist: dcccpy-macos-runtime==0.2.0a3; (sys_platform == "darwin" and platform_machine == "arm64") and extra == "runtime"
|
|
31
31
|
|
|
32
32
|
# dcccpy
|
|
33
33
|
|
|
34
|
+
### Native PET segmentation
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import dcccpy
|
|
38
|
+
result = dcccpy.seg("pet.nii.gz", "labels.nii.gz")
|
|
39
|
+
labels = result.load_output()
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Requires a DCCCcore build containing the `seg` command and its bundled model.
|
|
43
|
+
The signed integer output matches the input PET's native shape and geometry.
|
|
44
|
+
The default `mode="freesurfer"` returns raw DKT/aseg IDs; `mode="training"`
|
|
45
|
+
returns IDs 0–95. `mode="neurosegmenter"` provides partial regional compatibility:
|
|
46
|
+
brainstem/pons IDs 45/46 are unavailable, so pons SUVR is unsupported.
|
|
47
|
+
Mirror TTA is enabled by default; `tta=False` explicitly changes that protocol.
|
|
48
|
+
Use `threads=4` to select the CPU thread count, `bundle=...` to override the model
|
|
49
|
+
directory, and `debug=True` to save intermediate grids and a transform receipt.
|
|
50
|
+
The approximately 120 MB segmentation model is excluded from PyPI wheels.
|
|
51
|
+
With a slim runtime, the first `seg()` call downloads the separately published
|
|
52
|
+
`DCCCseg-4.6.0-alpha.zip` Release asset, verifies the model SHA-256 and caches it
|
|
53
|
+
under the dcccpy cache directory. A full native installation reuses its bundled
|
|
54
|
+
model. Existing `DCCCPY_AUTO_DOWNLOAD=0` disables implicit downloads; provision
|
|
55
|
+
offline use with `dcccpy.download_seg_model()` or pass `bundle=` pointing to
|
|
56
|
+
the extracted `seg` directory. `DCCCPY_SEG_MODEL_URL` selects a mirror while
|
|
57
|
+
retaining checksum verification.
|
|
58
|
+
An explicit `config=` delegates model selection to that configuration and
|
|
59
|
+
suppresses default-model provisioning. With a slim runtime and a configuration
|
|
60
|
+
that only changes other settings, also pass `bundle=dcccpy.download_seg_model()`.
|
|
61
|
+
An optional `rigid_header=...` must retain the exact native PET values and record
|
|
62
|
+
exactly two rigid steps. Existing output images are refused by the core.
|
|
63
|
+
|
|
34
64
|
`dcccpy` is a Python wrapper for the `DCCCcore` command-line tool from
|
|
35
65
|
DCCCSlicer. It keeps the native C++ core as the execution engine and provides a
|
|
36
66
|
small Python API for running common PET biomarker workflows.
|
|
@@ -212,7 +242,7 @@ the calculation again.
|
|
|
212
242
|
Release wheels should vendor the matching `DCCCcore` runtime tree before build:
|
|
213
243
|
|
|
214
244
|
```bash
|
|
215
|
-
python scripts/vendor_dccccore.py --version 4.
|
|
245
|
+
python scripts/vendor_dccccore.py --version 4.6.0-alpha --release-platform ubuntu-latest-x64
|
|
216
246
|
python -m build --wheel
|
|
217
247
|
```
|
|
218
248
|
|
|
@@ -220,7 +250,7 @@ The Linux ARM64 runtime package uses the corresponding release asset:
|
|
|
220
250
|
|
|
221
251
|
```bash
|
|
222
252
|
cd python/dcccpy-linux-arm64-runtime
|
|
223
|
-
python scripts/vendor_dccccore.py --version 4.
|
|
253
|
+
python scripts/vendor_dccccore.py --version 4.6.0-alpha --release-platform ubuntu-latest-arm64
|
|
224
254
|
python -m build --wheel
|
|
225
255
|
```
|
|
226
256
|
|
|
@@ -238,7 +268,7 @@ The preferred distribution layout is:
|
|
|
238
268
|
|
|
239
269
|
## Packaging note
|
|
240
270
|
|
|
241
|
-
The current optional runtime wheels use a PyPI-size profile for DCCCcore version 4.
|
|
271
|
+
The current optional runtime wheels use a PyPI-size profile for DCCCcore version 4.6.0-alpha. They omit the
|
|
242
272
|
`fast_and_acc` registration model/config and the ADAD decoupler ONNX ensemble,
|
|
243
273
|
while keeping the default spatial normalization model and assets needed by
|
|
244
274
|
common Centiloid/CenTauR/CenTauRz workflows.
|
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# dcccpy
|
|
2
2
|
|
|
3
|
+
### Native PET segmentation
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
import dcccpy
|
|
7
|
+
result = dcccpy.seg("pet.nii.gz", "labels.nii.gz")
|
|
8
|
+
labels = result.load_output()
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires a DCCCcore build containing the `seg` command and its bundled model.
|
|
12
|
+
The signed integer output matches the input PET's native shape and geometry.
|
|
13
|
+
The default `mode="freesurfer"` returns raw DKT/aseg IDs; `mode="training"`
|
|
14
|
+
returns IDs 0–95. `mode="neurosegmenter"` provides partial regional compatibility:
|
|
15
|
+
brainstem/pons IDs 45/46 are unavailable, so pons SUVR is unsupported.
|
|
16
|
+
Mirror TTA is enabled by default; `tta=False` explicitly changes that protocol.
|
|
17
|
+
Use `threads=4` to select the CPU thread count, `bundle=...` to override the model
|
|
18
|
+
directory, and `debug=True` to save intermediate grids and a transform receipt.
|
|
19
|
+
The approximately 120 MB segmentation model is excluded from PyPI wheels.
|
|
20
|
+
With a slim runtime, the first `seg()` call downloads the separately published
|
|
21
|
+
`DCCCseg-4.6.0-alpha.zip` Release asset, verifies the model SHA-256 and caches it
|
|
22
|
+
under the dcccpy cache directory. A full native installation reuses its bundled
|
|
23
|
+
model. Existing `DCCCPY_AUTO_DOWNLOAD=0` disables implicit downloads; provision
|
|
24
|
+
offline use with `dcccpy.download_seg_model()` or pass `bundle=` pointing to
|
|
25
|
+
the extracted `seg` directory. `DCCCPY_SEG_MODEL_URL` selects a mirror while
|
|
26
|
+
retaining checksum verification.
|
|
27
|
+
An explicit `config=` delegates model selection to that configuration and
|
|
28
|
+
suppresses default-model provisioning. With a slim runtime and a configuration
|
|
29
|
+
that only changes other settings, also pass `bundle=dcccpy.download_seg_model()`.
|
|
30
|
+
An optional `rigid_header=...` must retain the exact native PET values and record
|
|
31
|
+
exactly two rigid steps. Existing output images are refused by the core.
|
|
32
|
+
|
|
3
33
|
`dcccpy` is a Python wrapper for the `DCCCcore` command-line tool from
|
|
4
34
|
DCCCSlicer. It keeps the native C++ core as the execution engine and provides a
|
|
5
35
|
small Python API for running common PET biomarker workflows.
|
|
@@ -181,7 +211,7 @@ the calculation again.
|
|
|
181
211
|
Release wheels should vendor the matching `DCCCcore` runtime tree before build:
|
|
182
212
|
|
|
183
213
|
```bash
|
|
184
|
-
python scripts/vendor_dccccore.py --version 4.
|
|
214
|
+
python scripts/vendor_dccccore.py --version 4.6.0-alpha --release-platform ubuntu-latest-x64
|
|
185
215
|
python -m build --wheel
|
|
186
216
|
```
|
|
187
217
|
|
|
@@ -189,7 +219,7 @@ The Linux ARM64 runtime package uses the corresponding release asset:
|
|
|
189
219
|
|
|
190
220
|
```bash
|
|
191
221
|
cd python/dcccpy-linux-arm64-runtime
|
|
192
|
-
python scripts/vendor_dccccore.py --version 4.
|
|
222
|
+
python scripts/vendor_dccccore.py --version 4.6.0-alpha --release-platform ubuntu-latest-arm64
|
|
193
223
|
python -m build --wheel
|
|
194
224
|
```
|
|
195
225
|
|
|
@@ -207,7 +237,7 @@ The preferred distribution layout is:
|
|
|
207
237
|
|
|
208
238
|
## Packaging note
|
|
209
239
|
|
|
210
|
-
The current optional runtime wheels use a PyPI-size profile for DCCCcore version 4.
|
|
240
|
+
The current optional runtime wheels use a PyPI-size profile for DCCCcore version 4.6.0-alpha. They omit the
|
|
211
241
|
`fast_and_acc` registration model/config and the ADAD decoupler ONNX ensemble,
|
|
212
242
|
while keeping the default spatial normalization model and assets needed by
|
|
213
243
|
common Centiloid/CenTauR/CenTauRz workflows.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dcccpy"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.0a4"
|
|
8
8
|
description = "Python wrapper for the DCCCcore PET biomarker command-line tool"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -31,16 +31,16 @@ Issues = "https://github.com/tctco/DCCCSlicer/issues"
|
|
|
31
31
|
|
|
32
32
|
[project.optional-dependencies]
|
|
33
33
|
linux-runtime = [
|
|
34
|
-
"dcccpy-linux-runtime==0.2.
|
|
35
|
-
"dcccpy-linux-arm64-runtime==0.2.
|
|
34
|
+
"dcccpy-linux-runtime==0.2.0a3; sys_platform == 'linux' and platform_machine == 'x86_64'",
|
|
35
|
+
"dcccpy-linux-arm64-runtime==0.2.0a3; sys_platform == 'linux' and platform_machine == 'aarch64'",
|
|
36
36
|
]
|
|
37
|
-
windows-runtime = ["dcccpy-windows-runtime==0.2.
|
|
38
|
-
macos-runtime = ["dcccpy-macos-runtime==0.2.
|
|
37
|
+
windows-runtime = ["dcccpy-windows-runtime==0.2.0a3; sys_platform == 'win32' and platform_machine == 'AMD64'"]
|
|
38
|
+
macos-runtime = ["dcccpy-macos-runtime==0.2.0a3; sys_platform == 'darwin' and platform_machine == 'arm64'"]
|
|
39
39
|
runtime = [
|
|
40
|
-
"dcccpy-linux-runtime==0.2.
|
|
41
|
-
"dcccpy-linux-arm64-runtime==0.2.
|
|
42
|
-
"dcccpy-windows-runtime==0.2.
|
|
43
|
-
"dcccpy-macos-runtime==0.2.
|
|
40
|
+
"dcccpy-linux-runtime==0.2.0a3; sys_platform == 'linux' and platform_machine == 'x86_64'",
|
|
41
|
+
"dcccpy-linux-arm64-runtime==0.2.0a3; sys_platform == 'linux' and platform_machine == 'aarch64'",
|
|
42
|
+
"dcccpy-windows-runtime==0.2.0a3; sys_platform == 'win32' and platform_machine == 'AMD64'",
|
|
43
|
+
"dcccpy-macos-runtime==0.2.0a3; sys_platform == 'darwin' and platform_machine == 'arm64'",
|
|
44
44
|
]
|
|
45
45
|
|
|
46
46
|
[project.scripts]
|
|
@@ -15,12 +15,16 @@ from .core import (
|
|
|
15
15
|
parse_metrics,
|
|
16
16
|
pet_motion_correct,
|
|
17
17
|
rigid,
|
|
18
|
+
seg,
|
|
18
19
|
run,
|
|
19
20
|
suvr,
|
|
20
21
|
)
|
|
21
22
|
from .runtime import DCCCCORE_VERSION, DCCCcoreDownloadError, DCCCcoreNotFoundError, download_dccccore
|
|
23
|
+
from .segmentation import SegModelDownloadError, download_seg_model
|
|
22
24
|
|
|
23
25
|
__all__ = [
|
|
26
|
+
"SegModelDownloadError",
|
|
27
|
+
"download_seg_model",
|
|
24
28
|
"DCCCResult",
|
|
25
29
|
"DCCCCORE_VERSION",
|
|
26
30
|
"DCCCcoreDownloadError",
|
|
@@ -39,6 +43,7 @@ __all__ = [
|
|
|
39
43
|
"parse_metrics",
|
|
40
44
|
"pet_motion_correct",
|
|
41
45
|
"rigid",
|
|
46
|
+
"seg",
|
|
42
47
|
"run",
|
|
43
48
|
"suvr",
|
|
44
49
|
]
|
|
@@ -443,6 +443,48 @@ def rigid(
|
|
|
443
443
|
return _spatial_command("rigid", input, output, **kwargs)
|
|
444
444
|
|
|
445
445
|
|
|
446
|
+
def seg(
|
|
447
|
+
input: object,
|
|
448
|
+
output: str | os.PathLike[str] | None = None,
|
|
449
|
+
*,
|
|
450
|
+
mode: str = "freesurfer",
|
|
451
|
+
threads: int = 4,
|
|
452
|
+
tta: bool = True,
|
|
453
|
+
bundle: str | os.PathLike[str] | None = None,
|
|
454
|
+
rigid_header: str | os.PathLike[str] | None = None,
|
|
455
|
+
debug: bool = False,
|
|
456
|
+
config: str | os.PathLike[str] | None = None,
|
|
457
|
+
check: bool = True,
|
|
458
|
+
executable: str | os.PathLike[str] | None = None,
|
|
459
|
+
) -> DCCCResult:
|
|
460
|
+
"""Segment one 3-D PET into native-space categorical labels.
|
|
461
|
+
|
|
462
|
+
``freesurfer`` emits raw DKT/aseg IDs, ``training`` IDs 0–95, and
|
|
463
|
+
``neurosegmenter`` a partial aggregation without brainstem/pons IDs 45/46.
|
|
464
|
+
Mirror TTA is enabled by default. ``rigid_header``, when supplied, must
|
|
465
|
+
retain the input's voxel values after exactly two rigid predictions.
|
|
466
|
+
"""
|
|
467
|
+
if mode not in ("freesurfer", "training", "neurosegmenter"):
|
|
468
|
+
raise ValueError("mode must be freesurfer, training, or neurosegmenter")
|
|
469
|
+
if isinstance(threads, bool) or not isinstance(threads, int) or threads < 1:
|
|
470
|
+
raise ValueError("threads must be a positive integer")
|
|
471
|
+
if bundle is None and config is None:
|
|
472
|
+
from .segmentation import default_seg_bundle
|
|
473
|
+
executable = dccccore_path(executable=executable)
|
|
474
|
+
bundle = default_seg_bundle(executable)
|
|
475
|
+
args: list[object] = ["--mode", mode, "--threads", threads]
|
|
476
|
+
if not tta:
|
|
477
|
+
args.append("--no-tta")
|
|
478
|
+
if debug:
|
|
479
|
+
args.append("--debug")
|
|
480
|
+
for flag, path in (("--bundle", bundle), ("--rigid-header", rigid_header),
|
|
481
|
+
("--config", config)):
|
|
482
|
+
if path is not None:
|
|
483
|
+
args.extend([flag, _resolve_user_path(path)])
|
|
484
|
+
return _spatial_command("seg", input, output, extra_args=args,
|
|
485
|
+
check=check, executable=executable)
|
|
486
|
+
|
|
487
|
+
|
|
446
488
|
def pet_motion_correct(
|
|
447
489
|
input: object,
|
|
448
490
|
output: str | os.PathLike[str] | None = None,
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Optional DCCC Seg model delivery, kept outside PyPI wheels."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path, PurePosixPath
|
|
7
|
+
import re
|
|
8
|
+
import shutil
|
|
9
|
+
import stat
|
|
10
|
+
import tempfile
|
|
11
|
+
import urllib.request
|
|
12
|
+
import zipfile
|
|
13
|
+
|
|
14
|
+
from .runtime import DCCCCORE_VERSION, RELEASE_REPO, auto_download_enabled, cache_root
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
SEG_MODEL_SHA256 = "8ea46dc8eff44e7a6c55b93ec9a353eb676692741d56a47b04c755a3c956faf6"
|
|
18
|
+
MODEL_FILES = ("dccc_seg.onnx", "manifest.json", "grid.json", "plans.json", "dataset.json",
|
|
19
|
+
"labels.json", "neurosegmenter_mapping.json", "validation.json")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SegModelDownloadError(RuntimeError):
|
|
23
|
+
"""The optional segmentation model could not be provisioned."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def seg_model_dir(version: str = DCCCCORE_VERSION) -> Path:
|
|
27
|
+
if not re.fullmatch(r"\d+\.\d+\.\d+(?:-[A-Za-z0-9.]+)?", version):
|
|
28
|
+
raise ValueError("Invalid segmentation model version")
|
|
29
|
+
return cache_root() / "models" / "seg" / version
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _complete(directory: Path) -> bool:
|
|
33
|
+
return all((directory / name).is_file() for name in MODEL_FILES)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _verified(directory: Path) -> bool:
|
|
37
|
+
if not _complete(directory):
|
|
38
|
+
return False
|
|
39
|
+
digest = hashlib.sha256()
|
|
40
|
+
with (directory / "dccc_seg.onnx").open("rb") as source:
|
|
41
|
+
for chunk in iter(lambda: source.read(1024 * 1024), b""):
|
|
42
|
+
digest.update(chunk)
|
|
43
|
+
return digest.hexdigest() == SEG_MODEL_SHA256
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def download_seg_model(*, version: str = DCCCCORE_VERSION, force: bool = False) -> Path:
|
|
47
|
+
"""Download and verify the optional model; return its cached bundle directory.
|
|
48
|
+
|
|
49
|
+
An explicit call provisions offline use even with DCCCPY_AUTO_DOWNLOAD=0.
|
|
50
|
+
DCCCPY_SEG_MODEL_URL can select a mirrored archive; the model SHA-256 is
|
|
51
|
+
still enforced. force=True replaces this version's model cache only.
|
|
52
|
+
"""
|
|
53
|
+
destination = seg_model_dir(version)
|
|
54
|
+
if not force and _verified(destination):
|
|
55
|
+
return destination
|
|
56
|
+
if destination.exists() and not force:
|
|
57
|
+
raise SegModelDownloadError("Cached segmentation model is incomplete or corrupt; "
|
|
58
|
+
"call download_seg_model(force=True) to repair it")
|
|
59
|
+
repo = os.environ.get("DCCCPY_RELEASE_REPO", RELEASE_REPO)
|
|
60
|
+
url = os.environ.get("DCCCPY_SEG_MODEL_URL") or (
|
|
61
|
+
f"https://github.com/{repo}/releases/download/v{version}/DCCCseg-{version}.zip")
|
|
62
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
try:
|
|
64
|
+
with tempfile.TemporaryDirectory(prefix=".seg-download-", dir=destination.parent) as temporary:
|
|
65
|
+
temporary = Path(temporary)
|
|
66
|
+
archive = temporary / "model.zip"
|
|
67
|
+
with urllib.request.urlopen(url, timeout=120) as response, archive.open("wb") as output:
|
|
68
|
+
# Bound compressed downloads as well as extracted size.
|
|
69
|
+
total = 0
|
|
70
|
+
while True:
|
|
71
|
+
chunk = response.read(1024 * 1024)
|
|
72
|
+
if not chunk:
|
|
73
|
+
break
|
|
74
|
+
total += len(chunk)
|
|
75
|
+
if total > 200 * 1024 * 1024:
|
|
76
|
+
raise SegModelDownloadError("Segmentation model archive exceeds size limit")
|
|
77
|
+
output.write(chunk)
|
|
78
|
+
with zipfile.ZipFile(archive) as source:
|
|
79
|
+
total = 0
|
|
80
|
+
for member in source.infolist():
|
|
81
|
+
path = PurePosixPath(member.filename)
|
|
82
|
+
if (path.is_absolute() or ".." in path.parts or "\\" in member.filename
|
|
83
|
+
or not path.parts or path.parts[0] != "seg"
|
|
84
|
+
or stat.S_ISLNK(member.external_attr >> 16)):
|
|
85
|
+
raise SegModelDownloadError("Unsafe segmentation model archive path")
|
|
86
|
+
total += member.file_size
|
|
87
|
+
if total > 200 * 1024 * 1024:
|
|
88
|
+
raise SegModelDownloadError("Segmentation model expands beyond size limit")
|
|
89
|
+
for name in MODEL_FILES:
|
|
90
|
+
output = temporary / "seg" / name
|
|
91
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
92
|
+
with source.open("seg/" + name) as handle, output.open("xb") as target:
|
|
93
|
+
shutil.copyfileobj(handle, target)
|
|
94
|
+
staged = temporary / "seg"
|
|
95
|
+
if not _verified(staged):
|
|
96
|
+
raise SegModelDownloadError("Segmentation model SHA-256 mismatch")
|
|
97
|
+
if force and destination.exists():
|
|
98
|
+
shutil.rmtree(destination)
|
|
99
|
+
try:
|
|
100
|
+
staged.rename(destination)
|
|
101
|
+
except OSError:
|
|
102
|
+
# Another process may have finished the same version concurrently.
|
|
103
|
+
if not _verified(destination):
|
|
104
|
+
raise
|
|
105
|
+
return destination
|
|
106
|
+
except SegModelDownloadError:
|
|
107
|
+
raise
|
|
108
|
+
except Exception as exc:
|
|
109
|
+
raise SegModelDownloadError("Cannot download the DCCC Seg model; provide bundle= with "
|
|
110
|
+
"an extracted DCCCseg Release asset for offline use") from exc
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def default_seg_bundle(executable: Path) -> Path | None:
|
|
114
|
+
"""Return None for a full native installation, otherwise provision its model."""
|
|
115
|
+
if _complete(Path(executable).resolve().parent / "assets/models/seg"):
|
|
116
|
+
return None
|
|
117
|
+
destination = seg_model_dir()
|
|
118
|
+
if _verified(destination):
|
|
119
|
+
return destination
|
|
120
|
+
if not auto_download_enabled():
|
|
121
|
+
raise SegModelDownloadError("Segmentation model is absent and automatic download is disabled; "
|
|
122
|
+
"pass bundle= or call download_seg_model() explicitly")
|
|
123
|
+
return download_seg_model()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dcccpy
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.0a4
|
|
4
4
|
Summary: Python wrapper for the DCCCcore PET biomarker command-line tool
|
|
5
5
|
Author: DCCCSlicer contributors
|
|
6
6
|
License-Expression: CC-BY-NC-ND-4.0
|
|
@@ -17,20 +17,50 @@ Requires-Python: >=3.9
|
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
Requires-Dist: nibabel>=5
|
|
19
19
|
Provides-Extra: linux-runtime
|
|
20
|
-
Requires-Dist: dcccpy-linux-runtime==0.2.
|
|
21
|
-
Requires-Dist: dcccpy-linux-arm64-runtime==0.2.
|
|
20
|
+
Requires-Dist: dcccpy-linux-runtime==0.2.0a3; (sys_platform == "linux" and platform_machine == "x86_64") and extra == "linux-runtime"
|
|
21
|
+
Requires-Dist: dcccpy-linux-arm64-runtime==0.2.0a3; (sys_platform == "linux" and platform_machine == "aarch64") and extra == "linux-runtime"
|
|
22
22
|
Provides-Extra: windows-runtime
|
|
23
|
-
Requires-Dist: dcccpy-windows-runtime==0.2.
|
|
23
|
+
Requires-Dist: dcccpy-windows-runtime==0.2.0a3; (sys_platform == "win32" and platform_machine == "AMD64") and extra == "windows-runtime"
|
|
24
24
|
Provides-Extra: macos-runtime
|
|
25
|
-
Requires-Dist: dcccpy-macos-runtime==0.2.
|
|
25
|
+
Requires-Dist: dcccpy-macos-runtime==0.2.0a3; (sys_platform == "darwin" and platform_machine == "arm64") and extra == "macos-runtime"
|
|
26
26
|
Provides-Extra: runtime
|
|
27
|
-
Requires-Dist: dcccpy-linux-runtime==0.2.
|
|
28
|
-
Requires-Dist: dcccpy-linux-arm64-runtime==0.2.
|
|
29
|
-
Requires-Dist: dcccpy-windows-runtime==0.2.
|
|
30
|
-
Requires-Dist: dcccpy-macos-runtime==0.2.
|
|
27
|
+
Requires-Dist: dcccpy-linux-runtime==0.2.0a3; (sys_platform == "linux" and platform_machine == "x86_64") and extra == "runtime"
|
|
28
|
+
Requires-Dist: dcccpy-linux-arm64-runtime==0.2.0a3; (sys_platform == "linux" and platform_machine == "aarch64") and extra == "runtime"
|
|
29
|
+
Requires-Dist: dcccpy-windows-runtime==0.2.0a3; (sys_platform == "win32" and platform_machine == "AMD64") and extra == "runtime"
|
|
30
|
+
Requires-Dist: dcccpy-macos-runtime==0.2.0a3; (sys_platform == "darwin" and platform_machine == "arm64") and extra == "runtime"
|
|
31
31
|
|
|
32
32
|
# dcccpy
|
|
33
33
|
|
|
34
|
+
### Native PET segmentation
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import dcccpy
|
|
38
|
+
result = dcccpy.seg("pet.nii.gz", "labels.nii.gz")
|
|
39
|
+
labels = result.load_output()
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Requires a DCCCcore build containing the `seg` command and its bundled model.
|
|
43
|
+
The signed integer output matches the input PET's native shape and geometry.
|
|
44
|
+
The default `mode="freesurfer"` returns raw DKT/aseg IDs; `mode="training"`
|
|
45
|
+
returns IDs 0–95. `mode="neurosegmenter"` provides partial regional compatibility:
|
|
46
|
+
brainstem/pons IDs 45/46 are unavailable, so pons SUVR is unsupported.
|
|
47
|
+
Mirror TTA is enabled by default; `tta=False` explicitly changes that protocol.
|
|
48
|
+
Use `threads=4` to select the CPU thread count, `bundle=...` to override the model
|
|
49
|
+
directory, and `debug=True` to save intermediate grids and a transform receipt.
|
|
50
|
+
The approximately 120 MB segmentation model is excluded from PyPI wheels.
|
|
51
|
+
With a slim runtime, the first `seg()` call downloads the separately published
|
|
52
|
+
`DCCCseg-4.6.0-alpha.zip` Release asset, verifies the model SHA-256 and caches it
|
|
53
|
+
under the dcccpy cache directory. A full native installation reuses its bundled
|
|
54
|
+
model. Existing `DCCCPY_AUTO_DOWNLOAD=0` disables implicit downloads; provision
|
|
55
|
+
offline use with `dcccpy.download_seg_model()` or pass `bundle=` pointing to
|
|
56
|
+
the extracted `seg` directory. `DCCCPY_SEG_MODEL_URL` selects a mirror while
|
|
57
|
+
retaining checksum verification.
|
|
58
|
+
An explicit `config=` delegates model selection to that configuration and
|
|
59
|
+
suppresses default-model provisioning. With a slim runtime and a configuration
|
|
60
|
+
that only changes other settings, also pass `bundle=dcccpy.download_seg_model()`.
|
|
61
|
+
An optional `rigid_header=...` must retain the exact native PET values and record
|
|
62
|
+
exactly two rigid steps. Existing output images are refused by the core.
|
|
63
|
+
|
|
34
64
|
`dcccpy` is a Python wrapper for the `DCCCcore` command-line tool from
|
|
35
65
|
DCCCSlicer. It keeps the native C++ core as the execution engine and provides a
|
|
36
66
|
small Python API for running common PET biomarker workflows.
|
|
@@ -212,7 +242,7 @@ the calculation again.
|
|
|
212
242
|
Release wheels should vendor the matching `DCCCcore` runtime tree before build:
|
|
213
243
|
|
|
214
244
|
```bash
|
|
215
|
-
python scripts/vendor_dccccore.py --version 4.
|
|
245
|
+
python scripts/vendor_dccccore.py --version 4.6.0-alpha --release-platform ubuntu-latest-x64
|
|
216
246
|
python -m build --wheel
|
|
217
247
|
```
|
|
218
248
|
|
|
@@ -220,7 +250,7 @@ The Linux ARM64 runtime package uses the corresponding release asset:
|
|
|
220
250
|
|
|
221
251
|
```bash
|
|
222
252
|
cd python/dcccpy-linux-arm64-runtime
|
|
223
|
-
python scripts/vendor_dccccore.py --version 4.
|
|
253
|
+
python scripts/vendor_dccccore.py --version 4.6.0-alpha --release-platform ubuntu-latest-arm64
|
|
224
254
|
python -m build --wheel
|
|
225
255
|
```
|
|
226
256
|
|
|
@@ -238,7 +268,7 @@ The preferred distribution layout is:
|
|
|
238
268
|
|
|
239
269
|
## Packaging note
|
|
240
270
|
|
|
241
|
-
The current optional runtime wheels use a PyPI-size profile for DCCCcore version 4.
|
|
271
|
+
The current optional runtime wheels use a PyPI-size profile for DCCCcore version 4.6.0-alpha. They omit the
|
|
242
272
|
`fast_and_acc` registration model/config and the ADAD decoupler ONNX ensemble,
|
|
243
273
|
while keeping the default spatial normalization model and assets needed by
|
|
244
274
|
common Centiloid/CenTauR/CenTauRz workflows.
|
|
@@ -7,6 +7,7 @@ src/dcccpy/cli.py
|
|
|
7
7
|
src/dcccpy/core.py
|
|
8
8
|
src/dcccpy/py.typed
|
|
9
9
|
src/dcccpy/runtime.py
|
|
10
|
+
src/dcccpy/segmentation.py
|
|
10
11
|
src/dcccpy.egg-info/PKG-INFO
|
|
11
12
|
src/dcccpy.egg-info/SOURCES.txt
|
|
12
13
|
src/dcccpy.egg-info/dependency_links.txt
|
|
@@ -14,4 +15,5 @@ src/dcccpy.egg-info/entry_points.txt
|
|
|
14
15
|
src/dcccpy.egg-info/requires.txt
|
|
15
16
|
src/dcccpy.egg-info/top_level.txt
|
|
16
17
|
tests/test_core.py
|
|
18
|
+
tests/test_seg_distribution.py
|
|
17
19
|
tests/test_testpypi_centiloid.py
|
|
@@ -3,31 +3,31 @@ nibabel>=5
|
|
|
3
3
|
[linux-runtime]
|
|
4
4
|
|
|
5
5
|
[linux-runtime:sys_platform == "linux" and platform_machine == "aarch64"]
|
|
6
|
-
dcccpy-linux-arm64-runtime==0.2.
|
|
6
|
+
dcccpy-linux-arm64-runtime==0.2.0a3
|
|
7
7
|
|
|
8
8
|
[linux-runtime:sys_platform == "linux" and platform_machine == "x86_64"]
|
|
9
|
-
dcccpy-linux-runtime==0.2.
|
|
9
|
+
dcccpy-linux-runtime==0.2.0a3
|
|
10
10
|
|
|
11
11
|
[macos-runtime]
|
|
12
12
|
|
|
13
13
|
[macos-runtime:sys_platform == "darwin" and platform_machine == "arm64"]
|
|
14
|
-
dcccpy-macos-runtime==0.2.
|
|
14
|
+
dcccpy-macos-runtime==0.2.0a3
|
|
15
15
|
|
|
16
16
|
[runtime]
|
|
17
17
|
|
|
18
18
|
[runtime:sys_platform == "darwin" and platform_machine == "arm64"]
|
|
19
|
-
dcccpy-macos-runtime==0.2.
|
|
19
|
+
dcccpy-macos-runtime==0.2.0a3
|
|
20
20
|
|
|
21
21
|
[runtime:sys_platform == "linux" and platform_machine == "aarch64"]
|
|
22
|
-
dcccpy-linux-arm64-runtime==0.2.
|
|
22
|
+
dcccpy-linux-arm64-runtime==0.2.0a3
|
|
23
23
|
|
|
24
24
|
[runtime:sys_platform == "linux" and platform_machine == "x86_64"]
|
|
25
|
-
dcccpy-linux-runtime==0.2.
|
|
25
|
+
dcccpy-linux-runtime==0.2.0a3
|
|
26
26
|
|
|
27
27
|
[runtime:sys_platform == "win32" and platform_machine == "AMD64"]
|
|
28
|
-
dcccpy-windows-runtime==0.2.
|
|
28
|
+
dcccpy-windows-runtime==0.2.0a3
|
|
29
29
|
|
|
30
30
|
[windows-runtime]
|
|
31
31
|
|
|
32
32
|
[windows-runtime:sys_platform == "win32" and platform_machine == "AMD64"]
|
|
33
|
-
dcccpy-windows-runtime==0.2.
|
|
33
|
+
dcccpy-windows-runtime==0.2.0a3
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
import subprocess
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
import sys
|
|
6
7
|
import types
|
|
@@ -20,6 +21,11 @@ from dcccpy.runtime import (
|
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def make_fake_dccccore(tmp_path: Path) -> Path:
|
|
24
|
+
bundle = tmp_path / "assets/models/seg"
|
|
25
|
+
bundle.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
for name in ("dccc_seg.onnx", "manifest.json", "grid.json", "plans.json", "dataset.json",
|
|
27
|
+
"labels.json", "neurosegmenter_mapping.json", "validation.json"):
|
|
28
|
+
(bundle / name).write_bytes(b"test runtime resource")
|
|
23
29
|
exe = tmp_path / "DCCCcore"
|
|
24
30
|
exe.write_text(
|
|
25
31
|
"#!/usr/bin/env sh\n"
|
|
@@ -46,6 +52,66 @@ def test_parse_metrics() -> None:
|
|
|
46
52
|
assert metrics == {"fbp": 12.5, "SUVr": 1.23, "ADAD score": -0.2}
|
|
47
53
|
|
|
48
54
|
|
|
55
|
+
def test_seg_defaults_and_explicit_options(tmp_path, monkeypatch):
|
|
56
|
+
fake_args = tmp_path / "args.txt"
|
|
57
|
+
fake_exe = make_fake_dccccore(tmp_path)
|
|
58
|
+
monkeypatch.setenv("DCCCPY_FAKE_ARGS", str(fake_args))
|
|
59
|
+
result = dcccpy.seg("pet.nii", executable=fake_exe)
|
|
60
|
+
args = fake_args.read_text().splitlines()
|
|
61
|
+
assert args[0] == "seg"
|
|
62
|
+
assert args[args.index("--mode") + 1] == "freesurfer"
|
|
63
|
+
assert args[args.index("--threads") + 1] == "4"
|
|
64
|
+
assert "--no-tta" not in args
|
|
65
|
+
assert result.output.exists()
|
|
66
|
+
result = dcccpy.seg("pet.nii", tmp_path / "labels.nii.gz", mode="training",
|
|
67
|
+
threads=2, tta=False, bundle="model bundle",
|
|
68
|
+
rigid_header="rigid header.nii.gz", config="config.toml",
|
|
69
|
+
debug=True, executable=fake_exe)
|
|
70
|
+
args = fake_args.read_text().splitlines()
|
|
71
|
+
assert args[args.index("--mode") + 1] == "training"
|
|
72
|
+
assert args[args.index("--threads") + 1] == "2"
|
|
73
|
+
assert args[args.index("--bundle") + 1] == str(Path("model bundle").resolve())
|
|
74
|
+
assert args[args.index("--rigid-header") + 1] == str(Path("rigid header.nii.gz").resolve())
|
|
75
|
+
assert args[args.index("--config") + 1] == str(Path("config.toml").resolve())
|
|
76
|
+
assert "--debug" in args and "--no-tta" in args
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@pytest.mark.parametrize("kwargs", [{"mode": "bogus"}, {"threads": 0},
|
|
80
|
+
{"threads": 1.5}, {"threads": True}])
|
|
81
|
+
def test_seg_rejects_bad_options(kwargs):
|
|
82
|
+
with pytest.raises(ValueError):
|
|
83
|
+
dcccpy.seg("pet.nii", **kwargs)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_seg_preserves_native_failure(tmp_path):
|
|
87
|
+
exe = make_fake_dccccore(tmp_path)
|
|
88
|
+
exe.write_text("#!/bin/sh\nprintf 'invalid segmentation model' >&2\nexit 7\n")
|
|
89
|
+
exe.chmod(0o755)
|
|
90
|
+
result = dcccpy.seg("pet.nii", check=False, executable=exe)
|
|
91
|
+
assert result.returncode == 7
|
|
92
|
+
assert "invalid segmentation model" in result.stderr
|
|
93
|
+
assert not result.output.exists()
|
|
94
|
+
with pytest.raises(subprocess.CalledProcessError) as error:
|
|
95
|
+
dcccpy.seg("pet.nii", executable=exe)
|
|
96
|
+
assert error.value.returncode == 7
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_seg_custom_config_does_not_provision_or_override_model(tmp_path, monkeypatch):
|
|
100
|
+
exe = make_fake_dccccore(tmp_path)
|
|
101
|
+
(tmp_path / "assets/models/seg/dccc_seg.onnx").unlink()
|
|
102
|
+
args_file = tmp_path / "args.txt"
|
|
103
|
+
monkeypatch.setenv("DCCCPY_FAKE_ARGS", str(args_file))
|
|
104
|
+
monkeypatch.setenv("DCCCPY_AUTO_DOWNLOAD", "0")
|
|
105
|
+
monkeypatch.setenv("DCCCPY_CACHE_DIR", str(tmp_path / "empty-cache"))
|
|
106
|
+
config = tmp_path / "custom.toml"
|
|
107
|
+
config.write_text('[models]\nseg = "offline/custom/dccc_seg.onnx"\n')
|
|
108
|
+
result = dcccpy.seg("pet.nii", config=config, executable=exe)
|
|
109
|
+
assert result.returncode == 0
|
|
110
|
+
args = args_file.read_text().splitlines()
|
|
111
|
+
assert args[args.index("--config") + 1] == str(config)
|
|
112
|
+
assert "--bundle" not in args
|
|
113
|
+
|
|
114
|
+
|
|
49
115
|
def test_centiloid_omits_output_by_creating_temp_path(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
50
116
|
fake_args = tmp_path / "args.txt"
|
|
51
117
|
fake_exe = make_fake_dccccore(tmp_path)
|
|
@@ -142,7 +208,7 @@ def test_dccccore_path_can_disable_auto_download(tmp_path: Path, monkeypatch: py
|
|
|
142
208
|
|
|
143
209
|
|
|
144
210
|
def test_dccccore_path_auto_downloads_to_cache(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
145
|
-
asset_root = tmp_path / "asset" / "DCCCcore-4.
|
|
211
|
+
asset_root = tmp_path / "asset" / "DCCCcore-4.6.0-alpha-ubuntu-latest-x64"
|
|
146
212
|
asset_root.mkdir(parents=True)
|
|
147
213
|
exe = asset_root / "DCCCcore"
|
|
148
214
|
exe.write_text("#!/usr/bin/env sh\nprintf 'fake dccccore\\n'\n")
|
|
@@ -150,7 +216,7 @@ def test_dccccore_path_auto_downloads_to_cache(tmp_path: Path, monkeypatch: pyte
|
|
|
150
216
|
|
|
151
217
|
archive = tmp_path / "DCCCcore.zip"
|
|
152
218
|
with zipfile.ZipFile(archive, "w") as zf:
|
|
153
|
-
zf.write(exe, "DCCCcore-4.
|
|
219
|
+
zf.write(exe, "DCCCcore-4.6.0-alpha-ubuntu-latest-x64/DCCCcore")
|
|
154
220
|
|
|
155
221
|
monkeypatch.delenv("DCCCPY_DCCCCORE", raising=False)
|
|
156
222
|
monkeypatch.setenv("DCCCPY_AUTO_DOWNLOAD", "1")
|
|
@@ -175,7 +241,7 @@ def test_find_existing_dccccore_checks_macos_runtime_package(
|
|
|
175
241
|
exe.write_text("#!/usr/bin/env sh\n")
|
|
176
242
|
exe.chmod(0o755)
|
|
177
243
|
|
|
178
|
-
module = types.SimpleNamespace(DCCCCORE_VERSION="4.
|
|
244
|
+
module = types.SimpleNamespace(DCCCCORE_VERSION="4.6.0-alpha", dccccore_root=lambda: root)
|
|
179
245
|
monkeypatch.setitem(sys.modules, "dcccpy_macos_runtime", module)
|
|
180
246
|
monkeypatch.setenv("PATH", str(tmp_path / "empty-path"))
|
|
181
247
|
|
|
@@ -186,10 +252,10 @@ def test_linux_arm64_release_platform() -> None:
|
|
|
186
252
|
assert release_platform("linux-arm64") == "ubuntu-latest-arm64"
|
|
187
253
|
|
|
188
254
|
|
|
189
|
-
def
|
|
190
|
-
assert DCCCCORE_VERSION == "4.
|
|
255
|
+
def test_default_release_targets_dccccore_460_alpha() -> None:
|
|
256
|
+
assert DCCCCORE_VERSION == "4.6.0-alpha"
|
|
191
257
|
assert release_url(platform_name="ubuntu-latest-x64").endswith(
|
|
192
|
-
"/releases/download/v4.
|
|
258
|
+
"/releases/download/v4.6.0-alpha/DCCCcore-4.6.0-alpha-ubuntu-latest-x64.zip"
|
|
193
259
|
)
|
|
194
260
|
|
|
195
261
|
|
|
@@ -220,7 +286,7 @@ def test_find_existing_dccccore_checks_linux_arm64_runtime_package(
|
|
|
220
286
|
exe.write_text("#!/usr/bin/env sh\n")
|
|
221
287
|
exe.chmod(0o755)
|
|
222
288
|
|
|
223
|
-
module = types.SimpleNamespace(DCCCCORE_VERSION="4.
|
|
289
|
+
module = types.SimpleNamespace(DCCCCORE_VERSION="4.6.0-alpha", dccccore_root=lambda: root)
|
|
224
290
|
monkeypatch.setitem(sys.modules, "dcccpy_linux_arm64_runtime", module)
|
|
225
291
|
monkeypatch.setenv("PATH", str(tmp_path / "empty-path"))
|
|
226
292
|
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import importlib.util
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import zipfile
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
FILES = ("dccc_seg.onnx", "manifest.json", "grid.json", "plans.json", "dataset.json",
|
|
12
|
+
"labels.json", "neurosegmenter_mapping.json", "validation.json")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def import_script(path):
|
|
16
|
+
spec = importlib.util.spec_from_file_location("release_script", path)
|
|
17
|
+
module = importlib.util.module_from_spec(spec)
|
|
18
|
+
spec.loader.exec_module(module)
|
|
19
|
+
return module
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture
|
|
23
|
+
def distribution(tmp_path, monkeypatch):
|
|
24
|
+
import dcccpy.segmentation as model
|
|
25
|
+
archive = tmp_path / "model.zip"
|
|
26
|
+
with zipfile.ZipFile(archive, "w") as z:
|
|
27
|
+
for name in FILES:
|
|
28
|
+
z.writestr("seg/" + name, b"model" if name.endswith(".onnx") else b"{}")
|
|
29
|
+
monkeypatch.setenv("DCCCPY_CACHE_DIR", str(tmp_path / "cache"))
|
|
30
|
+
monkeypatch.setenv("DCCCPY_SEG_MODEL_URL", archive.as_uri())
|
|
31
|
+
monkeypatch.setattr(model, "SEG_MODEL_SHA256", hashlib.sha256(b"model").hexdigest())
|
|
32
|
+
return model, archive
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_model_download_is_cached_outside_package(distribution):
|
|
36
|
+
model, archive = distribution
|
|
37
|
+
directory = model.download_seg_model()
|
|
38
|
+
assert directory.name == "4.6.0-alpha"
|
|
39
|
+
assert (directory / "dccc_seg.onnx").read_bytes() == b"model"
|
|
40
|
+
archive.unlink()
|
|
41
|
+
assert model.download_seg_model() == directory
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.mark.parametrize("kind", ["digest", "traversal", "missing"])
|
|
45
|
+
def test_model_download_rejects_bad_assets(distribution, kind, tmp_path):
|
|
46
|
+
model, archive = distribution
|
|
47
|
+
with zipfile.ZipFile(archive, "w") as z:
|
|
48
|
+
for name in FILES:
|
|
49
|
+
if kind == "missing" and name == "labels.json":
|
|
50
|
+
continue
|
|
51
|
+
z.writestr("seg/" + name, b"wrong" if kind == "digest" else
|
|
52
|
+
(b"model" if name.endswith(".onnx") else b"{}"))
|
|
53
|
+
if kind == "traversal":
|
|
54
|
+
z.writestr("../escaped", b"bad")
|
|
55
|
+
with pytest.raises(model.SegModelDownloadError):
|
|
56
|
+
model.download_seg_model()
|
|
57
|
+
assert not (tmp_path / "escaped").exists()
|
|
58
|
+
assert not model.seg_model_dir().exists()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_bundled_model_and_download_opt_out(distribution, monkeypatch, tmp_path):
|
|
62
|
+
model, archive = distribution
|
|
63
|
+
exe = tmp_path / "native" / "DCCCcore"
|
|
64
|
+
bundle = exe.parent / "assets/models/seg"
|
|
65
|
+
bundle.mkdir(parents=True)
|
|
66
|
+
for name in FILES:
|
|
67
|
+
(bundle / name).write_bytes(b"model" if name.endswith(".onnx") else b"{}")
|
|
68
|
+
monkeypatch.setenv("DCCCPY_AUTO_DOWNLOAD", "0")
|
|
69
|
+
assert model.default_seg_bundle(exe) is None
|
|
70
|
+
(bundle / "dccc_seg.onnx").unlink()
|
|
71
|
+
with pytest.raises(model.SegModelDownloadError, match="download"):
|
|
72
|
+
model.default_seg_bundle(exe)
|
|
73
|
+
cached = model.download_seg_model() # Explicit provisioning honors user's call.
|
|
74
|
+
assert model.default_seg_bundle(exe) == cached
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_native_symlink_reuses_real_bundled_model(distribution, monkeypatch, tmp_path):
|
|
78
|
+
model, _ = distribution
|
|
79
|
+
native = tmp_path / "native/DCCCcore"
|
|
80
|
+
bundle = native.parent / "assets/models/seg"
|
|
81
|
+
bundle.mkdir(parents=True)
|
|
82
|
+
native.write_text("native executable")
|
|
83
|
+
for name in FILES:
|
|
84
|
+
(bundle / name).write_bytes(b"model" if name.endswith(".onnx") else b"{}")
|
|
85
|
+
link = tmp_path / "bin/DCCCcore"
|
|
86
|
+
link.parent.mkdir()
|
|
87
|
+
link.symlink_to(native)
|
|
88
|
+
monkeypatch.setenv("DCCCPY_AUTO_DOWNLOAD", "0")
|
|
89
|
+
assert model.default_seg_bundle(link) is None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@pytest.mark.parametrize("package", ["linux", "linux-arm64", "macos", "windows"])
|
|
93
|
+
def test_every_runtime_profile_excludes_seg(tmp_path, package):
|
|
94
|
+
root = Path(__file__).resolve().parents[3]
|
|
95
|
+
vendor = import_script(root / f"python/dcccpy-{package}-runtime/scripts/vendor_dccccore.py")
|
|
96
|
+
for profile in vendor.EXCLUDE_PROFILES:
|
|
97
|
+
dest = tmp_path / profile
|
|
98
|
+
model = dest / "assets/models/seg/dccc_seg.onnx"
|
|
99
|
+
rigid = dest / "assets/models/registration/rigid.onnx"
|
|
100
|
+
model.parent.mkdir(parents=True)
|
|
101
|
+
rigid.parent.mkdir(parents=True)
|
|
102
|
+
model.write_bytes(b"bulky optional segmentation")
|
|
103
|
+
rigid.write_bytes(b"required rigid")
|
|
104
|
+
vendor.prune_profile(dest, profile)
|
|
105
|
+
assert not model.exists()
|
|
106
|
+
assert rigid.exists()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_model_release_zip_contains_runtime_controls(tmp_path):
|
|
110
|
+
root = Path(__file__).resolve().parents[3]
|
|
111
|
+
packer = import_script(root / "scripts/package-seg-model.py")
|
|
112
|
+
bundle = tmp_path / "source"
|
|
113
|
+
bundle.mkdir()
|
|
114
|
+
for name in FILES:
|
|
115
|
+
(bundle / name).write_bytes(b"model" if name.endswith(".onnx") else b"{}")
|
|
116
|
+
archive = packer.package_model(bundle, tmp_path / "dist", "4.6.0-alpha")
|
|
117
|
+
assert archive.name == "DCCCseg-4.6.0-alpha.zip"
|
|
118
|
+
with zipfile.ZipFile(archive) as z:
|
|
119
|
+
assert set(z.namelist()) == {"seg/" + name for name in FILES}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_model_release_preserves_reference_entrypoint(tmp_path):
|
|
123
|
+
root = Path(__file__).resolve().parents[3]
|
|
124
|
+
packer = import_script(root / "scripts/package-seg-model.py")
|
|
125
|
+
bundle = tmp_path / "source"
|
|
126
|
+
bundle.mkdir()
|
|
127
|
+
for name in FILES:
|
|
128
|
+
(bundle / name).write_bytes(b"model" if name.endswith(".onnx") else b"{}")
|
|
129
|
+
(bundle / "README.md").write_text("See dccc_seg.py for reference inference")
|
|
130
|
+
(bundle / "dccc_seg.py").write_text("# shipped reference entry point\n")
|
|
131
|
+
archive = packer.package_model(bundle, tmp_path / "dist", "4.6.0-alpha")
|
|
132
|
+
with zipfile.ZipFile(archive) as z:
|
|
133
|
+
assert z.read("seg/dccc_seg.py") == b"# shipped reference entry point\n"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_release_version_must_match_source():
|
|
137
|
+
root = Path(__file__).resolve().parents[3]
|
|
138
|
+
metadata = import_script(root / "scripts/release-metadata.py")
|
|
139
|
+
cmake = 'project(DCCCcore VERSION 4.6.0)\nset(DCCCCORE_VERSION_SUFFIX "-alpha")\n'
|
|
140
|
+
assert metadata.release_version(cmake, requested="4.6.0-alpha") == "4.6.0-alpha"
|
|
141
|
+
assert metadata.release_version(cmake, tag="v4.6.0-alpha") == "4.6.0-alpha"
|
|
142
|
+
with pytest.raises(ValueError):
|
|
143
|
+
metadata.release_version(cmake, requested="4.5.1-alpha")
|
|
144
|
+
with pytest.raises(ValueError):
|
|
145
|
+
metadata.release_version(cmake, tag="v4.6.0")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|