segresnet-mlx 0.2.1__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 segresnet-mlx might be problematic. Click here for more details.
- segresnet_mlx/__init__.py +67 -0
- segresnet_mlx/_arrays.py +36 -0
- segresnet_mlx/_nifti.py +63 -0
- segresnet_mlx/_records.py +78 -0
- segresnet_mlx/_runtime.py +22 -0
- segresnet_mlx/_types.py +58 -0
- segresnet_mlx/inference.py +37 -0
- segresnet_mlx/medmlx.py +669 -0
- segresnet_mlx/network.py +195 -0
- segresnet_mlx/predictor.py +303 -0
- segresnet_mlx/py.typed +0 -0
- segresnet_mlx/telemetry.py +166 -0
- segresnet_mlx/topology.py +49 -0
- segresnet_mlx/transforms.py +669 -0
- segresnet_mlx/weights.py +183 -0
- segresnet_mlx-0.2.1.dist-info/METADATA +94 -0
- segresnet_mlx-0.2.1.dist-info/RECORD +21 -0
- segresnet_mlx-0.2.1.dist-info/WHEEL +4 -0
- segresnet_mlx-0.2.1.dist-info/entry_points.txt +2 -0
- segresnet_mlx-0.2.1.dist-info/licenses/LICENSE +202 -0
- segresnet_mlx-0.2.1.dist-info/licenses/THIRD_PARTY_NOTICES.md +21 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Explicit MLX port of MONAI SegResNet inference.
|
|
2
|
+
|
|
3
|
+
Public names load on first access, so importing the package (or the medmlx
|
|
4
|
+
declaration in ``segresnet_mlx.medmlx``) does not import MLX.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from importlib import import_module
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from segresnet_mlx.inference import forward_patch as forward_patch
|
|
12
|
+
from segresnet_mlx.network import SegResNet as SegResNet
|
|
13
|
+
from segresnet_mlx.predictor import BUNDLE_CONFIGS as BUNDLE_CONFIGS
|
|
14
|
+
from segresnet_mlx.predictor import BundlePredictor as BundlePredictor
|
|
15
|
+
from segresnet_mlx.predictor import SlidingWindowConfig as SlidingWindowConfig
|
|
16
|
+
from segresnet_mlx.predictor import predict_logits as predict_logits
|
|
17
|
+
from segresnet_mlx.telemetry import WindowTelemetry as WindowTelemetry
|
|
18
|
+
from segresnet_mlx.topology import SegResNetSpec as SegResNetSpec
|
|
19
|
+
from segresnet_mlx.weights import convert_checkpoint as convert_checkpoint
|
|
20
|
+
from segresnet_mlx.weights import convert_state_dict as convert_state_dict
|
|
21
|
+
from segresnet_mlx.weights import load_model as load_model
|
|
22
|
+
from segresnet_mlx.weights import required_shapes as required_shapes
|
|
23
|
+
|
|
24
|
+
__version__ = "0.2.1"
|
|
25
|
+
|
|
26
|
+
_EXPORTS = {
|
|
27
|
+
"BUNDLE_CONFIGS": "segresnet_mlx.predictor",
|
|
28
|
+
"BundlePredictor": "segresnet_mlx.predictor",
|
|
29
|
+
"SegResNet": "segresnet_mlx.network",
|
|
30
|
+
"SegResNetSpec": "segresnet_mlx.topology",
|
|
31
|
+
"SlidingWindowConfig": "segresnet_mlx.predictor",
|
|
32
|
+
"WindowTelemetry": "segresnet_mlx.telemetry",
|
|
33
|
+
"convert_checkpoint": "segresnet_mlx.weights",
|
|
34
|
+
"convert_state_dict": "segresnet_mlx.weights",
|
|
35
|
+
"forward_patch": "segresnet_mlx.inference",
|
|
36
|
+
"load_model": "segresnet_mlx.weights",
|
|
37
|
+
"predict_logits": "segresnet_mlx.predictor",
|
|
38
|
+
"required_shapes": "segresnet_mlx.weights",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"BUNDLE_CONFIGS",
|
|
43
|
+
"BundlePredictor",
|
|
44
|
+
"SegResNet",
|
|
45
|
+
"SegResNetSpec",
|
|
46
|
+
"SlidingWindowConfig",
|
|
47
|
+
"WindowTelemetry",
|
|
48
|
+
"convert_checkpoint",
|
|
49
|
+
"convert_state_dict",
|
|
50
|
+
"forward_patch",
|
|
51
|
+
"load_model",
|
|
52
|
+
"predict_logits",
|
|
53
|
+
"required_shapes",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def __getattr__(name: str) -> object:
|
|
58
|
+
module = _EXPORTS.get(name)
|
|
59
|
+
if module is None:
|
|
60
|
+
raise AttributeError(f"module 'segresnet_mlx' has no attribute {name!r}")
|
|
61
|
+
value = getattr(import_module(module), name)
|
|
62
|
+
globals()[name] = value
|
|
63
|
+
return value
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def __dir__() -> list[str]:
|
|
67
|
+
return sorted({*globals(), *_EXPORTS})
|
segresnet_mlx/_arrays.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Host array dtypes at model, checkpoint and image boundaries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import TYPE_CHECKING, cast
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from segresnet_mlx._types import Array
|
|
12
|
+
|
|
13
|
+
type HostArray = np.ndarray[tuple[int, ...], np.dtype[np.generic]]
|
|
14
|
+
type Float32Array = np.ndarray[tuple[int, ...], np.dtype[np.float32]]
|
|
15
|
+
type Float64Array = np.ndarray[tuple[int, ...], np.dtype[np.float64]]
|
|
16
|
+
type Int64Array = np.ndarray[tuple[int, ...], np.dtype[np.int64]]
|
|
17
|
+
type MaskArray = Float32Array | Int64Array
|
|
18
|
+
|
|
19
|
+
type NumericScalar = (
|
|
20
|
+
np.bool_
|
|
21
|
+
| np.int8
|
|
22
|
+
| np.int16
|
|
23
|
+
| np.int32
|
|
24
|
+
| np.int64
|
|
25
|
+
| np.uint8
|
|
26
|
+
| np.uint16
|
|
27
|
+
| np.uint32
|
|
28
|
+
| np.uint64
|
|
29
|
+
| np.float16
|
|
30
|
+
| np.float32
|
|
31
|
+
| np.float64
|
|
32
|
+
| np.complex64
|
|
33
|
+
| np.complex128
|
|
34
|
+
)
|
|
35
|
+
type NumericArray = np.ndarray[tuple[int, ...], np.dtype[NumericScalar]]
|
|
36
|
+
as_numpy = cast("Callable[[Array], NumericArray]", np.array)
|
segresnet_mlx/_nifti.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Typed protocols for the NiBabel NIfTI, header and orientation calls used here."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from os import PathLike
|
|
7
|
+
from typing import Protocol, cast
|
|
8
|
+
|
|
9
|
+
import nibabel
|
|
10
|
+
import numpy as np
|
|
11
|
+
from nibabel.filebasedimages import ImageFileError as ImageFileError
|
|
12
|
+
|
|
13
|
+
from segresnet_mlx._arrays import Float64Array, HostArray
|
|
14
|
+
|
|
15
|
+
type PathArg = str | PathLike[str]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ArrayBuffer(Protocol):
|
|
19
|
+
def __array__(
|
|
20
|
+
self, dtype: np.dtype[np.generic] | None = None, copy: bool | None = None
|
|
21
|
+
) -> HostArray: ...
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class NiftiHeader(Protocol):
|
|
25
|
+
def get_zooms(self) -> tuple[float, ...]: ...
|
|
26
|
+
def set_zooms(self, zooms: Sequence[float] | Float64Array) -> None: ...
|
|
27
|
+
def __getitem__(self, key: str) -> HostArray: ...
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class NiftiImage(Protocol):
|
|
31
|
+
shape: tuple[int, ...]
|
|
32
|
+
affine: Float64Array
|
|
33
|
+
dataobj: ArrayBuffer | HostArray
|
|
34
|
+
header: NiftiHeader
|
|
35
|
+
|
|
36
|
+
def __init__(self, dataobj: HostArray, affine: HostArray) -> None: ...
|
|
37
|
+
def get_data_dtype(self) -> np.dtype[np.generic]: ...
|
|
38
|
+
def get_sform(self) -> Float64Array: ...
|
|
39
|
+
def get_qform(self) -> Float64Array: ...
|
|
40
|
+
def set_sform(self, affine: Float64Array, code: int | None = None) -> None: ...
|
|
41
|
+
def set_qform(self, affine: Float64Array, code: int | None = None) -> None: ...
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Orientations(Protocol):
|
|
45
|
+
def axcodes2ornt(self, axcodes: str, labels: tuple[tuple[str, str], ...]) -> Float64Array: ...
|
|
46
|
+
def ornt_transform(self, start_ornt: Float64Array, end_ornt: Float64Array) -> Float64Array: ...
|
|
47
|
+
def inv_ornt_aff(self, ornt: Float64Array, shape: Sequence[int]) -> Float64Array: ...
|
|
48
|
+
def aff2axcodes(
|
|
49
|
+
self, aff: Float64Array, labels: tuple[tuple[str, str], ...]
|
|
50
|
+
) -> tuple[str | None, ...]: ...
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Nibabel(Protocol):
|
|
54
|
+
Nifti1Image: type[NiftiImage]
|
|
55
|
+
orientations: Orientations
|
|
56
|
+
|
|
57
|
+
def load(self, filename: PathArg) -> NiftiImage: ...
|
|
58
|
+
def save(self, img: NiftiImage, filename: PathArg) -> None: ...
|
|
59
|
+
def io_orientation(self, affine: Float64Array) -> Float64Array: ...
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# Bound to the real module; this runner admits only NIfTI images.
|
|
63
|
+
nib = cast(Nibabel, nibabel)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""JSON schemas for the public runner and window telemetry."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import NotRequired, TypedDict
|
|
7
|
+
|
|
8
|
+
from segresnet_mlx.topology import ArithmeticProfile
|
|
9
|
+
|
|
10
|
+
type Json = bool | int | float | str | list[Json] | dict[str, Json] | None
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class InfererConfig(TypedDict):
|
|
14
|
+
roi_size: Sequence[int]
|
|
15
|
+
sw_batch_size: NotRequired[int]
|
|
16
|
+
overlap: NotRequired[float]
|
|
17
|
+
mode: NotRequired[str]
|
|
18
|
+
padding_mode: NotRequired[str]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BundleConfig(TypedDict):
|
|
22
|
+
inferer: InfererConfig
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Runtime(TypedDict):
|
|
26
|
+
"""MLX device string and peak MLX memory in bytes (from ``mx.get_peak_memory``)."""
|
|
27
|
+
|
|
28
|
+
device: str
|
|
29
|
+
peak_bytes: int | None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SlidingWindow(TypedDict):
|
|
33
|
+
roi_size: list[int]
|
|
34
|
+
sw_batch_size: int
|
|
35
|
+
overlap: float
|
|
36
|
+
mode: str
|
|
37
|
+
padding_mode: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Seconds(TypedDict):
|
|
41
|
+
"""Wall-clock seconds per runner stage, rounded to four decimals."""
|
|
42
|
+
|
|
43
|
+
load_inputs_and_weights: float
|
|
44
|
+
preprocess: float
|
|
45
|
+
inference: float
|
|
46
|
+
postprocess_and_write: float
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Details(TypedDict):
|
|
50
|
+
"""The ``details`` block of ``run``'s result.
|
|
51
|
+
|
|
52
|
+
``labels`` maps each label value (as a string) to its name and ``labels_present``
|
|
53
|
+
lists the values found in the written mask. ``input_shape`` is the first input's
|
|
54
|
+
voxel shape (equal to the mask shape); ``network_input_shape`` is the spatial shape
|
|
55
|
+
after preprocessing, which the sliding window sees. ``output_dtype`` is the NIfTI
|
|
56
|
+
dtype of the mask. ``peak_mlx_bytes`` is the peak MLX memory in bytes.
|
|
57
|
+
``requires_human_review`` is always true: outputs are research results.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
bundle: str
|
|
61
|
+
arithmetic_profile: ArithmeticProfile
|
|
62
|
+
labels: dict[str, str]
|
|
63
|
+
labels_present: list[int]
|
|
64
|
+
input_shape: list[int]
|
|
65
|
+
network_input_shape: list[int]
|
|
66
|
+
output_dtype: str
|
|
67
|
+
sliding_window: SlidingWindow
|
|
68
|
+
mlx_device: str
|
|
69
|
+
peak_mlx_bytes: int | None
|
|
70
|
+
seconds: Seconds
|
|
71
|
+
requires_human_review: bool
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class RunResult(TypedDict):
|
|
75
|
+
"""Result of ``segresnet_mlx.medmlx.run``: ``outputs["mask"]`` is the mask path."""
|
|
76
|
+
|
|
77
|
+
outputs: dict[str, str]
|
|
78
|
+
details: Details
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Typed native import after the shared runtime's existing admission checks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, cast
|
|
6
|
+
|
|
7
|
+
from medmlx_core.runtime import import_mlx as _import_mlx
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from segresnet_mlx._types import MLX
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def import_mlx() -> MLX:
|
|
14
|
+
"""Return ``mlx.core`` with the Metal GPU as the default device.
|
|
15
|
+
|
|
16
|
+
The host checks and device selection are done by ``medmlx_core``; on an
|
|
17
|
+
unsupported host it raises ``MissingDependencyError`` and there is no CPU fallback.
|
|
18
|
+
"""
|
|
19
|
+
_import_mlx()
|
|
20
|
+
import mlx.core as mx
|
|
21
|
+
|
|
22
|
+
return cast("MLX", mx)
|
segresnet_mlx/_types.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Static native MLX contracts; runtime imports remain lazy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
9
|
+
from typing import Literal, Protocol
|
|
10
|
+
|
|
11
|
+
import mlx.core as mx
|
|
12
|
+
from medmlx_core.typing import MlxRuntime
|
|
13
|
+
|
|
14
|
+
from segresnet_mlx._arrays import Float32Array, HostArray
|
|
15
|
+
|
|
16
|
+
type Array = mx.array
|
|
17
|
+
type ArrayValue = bool | int | float | complex | list[ArrayValue] | tuple[ArrayValue, ...]
|
|
18
|
+
|
|
19
|
+
class ArrayConstructor(Protocol):
|
|
20
|
+
def __call__(
|
|
21
|
+
self, value: HostArray | Array | ArrayValue, dtype: mx.Dtype | None = None
|
|
22
|
+
) -> Array: ...
|
|
23
|
+
|
|
24
|
+
class MLX(MlxRuntime, Protocol):
|
|
25
|
+
all = staticmethod(mx.all)
|
|
26
|
+
isfinite = staticmethod(mx.isfinite)
|
|
27
|
+
full = staticmethod(mx.full)
|
|
28
|
+
sum = staticmethod(mx.sum)
|
|
29
|
+
ones = staticmethod(mx.ones)
|
|
30
|
+
synchronize = staticmethod(mx.synchronize)
|
|
31
|
+
clear_cache = staticmethod(mx.clear_cache)
|
|
32
|
+
load = staticmethod(mx.load)
|
|
33
|
+
|
|
34
|
+
def eval(self, *args: Array | Mapping[str, Array] | Sequence[Array]) -> None: ...
|
|
35
|
+
|
|
36
|
+
def save_safetensors(self, file: str, arrays: dict[str, Array]) -> None: ...
|
|
37
|
+
|
|
38
|
+
class SlidingWindowInference(Protocol):
|
|
39
|
+
def __call__(
|
|
40
|
+
self,
|
|
41
|
+
inputs: Float32Array,
|
|
42
|
+
roi_size: Sequence[int],
|
|
43
|
+
sw_batch_size: int,
|
|
44
|
+
predictor: Callable[[Float32Array | Array], Float32Array]
|
|
45
|
+
| Callable[[Float32Array | Array, list[list[slice]]], Float32Array],
|
|
46
|
+
*,
|
|
47
|
+
overlap: float,
|
|
48
|
+
mode: str,
|
|
49
|
+
padding_mode: str,
|
|
50
|
+
with_coord: bool,
|
|
51
|
+
predictor_input: Literal["numpy"],
|
|
52
|
+
) -> Float32Array: ...
|
|
53
|
+
|
|
54
|
+
class CoreSlidingWindow(Protocol):
|
|
55
|
+
sliding_window_inference: SlidingWindowInference
|
|
56
|
+
|
|
57
|
+
class Metal(Protocol):
|
|
58
|
+
def is_available(self) -> bool: ...
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Synchronized single-patch inference with NumPy inputs and raw-logit outputs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import cast
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from medmlx_core.errors import InvalidInputError
|
|
9
|
+
from medmlx_core.typing import ArrayFactory
|
|
10
|
+
|
|
11
|
+
from segresnet_mlx._arrays import Float32Array, HostArray
|
|
12
|
+
from segresnet_mlx._runtime import import_mlx
|
|
13
|
+
from segresnet_mlx.network import SegResNet
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def forward_patch(network: SegResNet, values: HostArray) -> Float32Array:
|
|
17
|
+
"""Run one finite float32 ``(N, C, D, H, W)`` patch without transforms or stitching.
|
|
18
|
+
|
|
19
|
+
Returns float32 NumPy logits ``(N, out_channels, D, H, W)``, synchronized and
|
|
20
|
+
checked for finiteness.
|
|
21
|
+
|
|
22
|
+
Raises:
|
|
23
|
+
InvalidInputError: If ``values`` is not finite float32.
|
|
24
|
+
RuntimeError: If the network produced non-finite logits.
|
|
25
|
+
"""
|
|
26
|
+
host = np.asarray(values)
|
|
27
|
+
if host.dtype != np.float32 or not np.isfinite(host).all():
|
|
28
|
+
raise InvalidInputError("SegResNet input must be finite float32")
|
|
29
|
+
mx = import_mlx()
|
|
30
|
+
mx.synchronize()
|
|
31
|
+
result = network(cast(ArrayFactory, mx.array)(host))
|
|
32
|
+
mx.eval(result)
|
|
33
|
+
mx.synchronize()
|
|
34
|
+
output = cast(Float32Array, np.array(result))
|
|
35
|
+
if not np.isfinite(output).all():
|
|
36
|
+
raise RuntimeError("SegResNet produced non-finite logits")
|
|
37
|
+
return output
|