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.

@@ -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})
@@ -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)
@@ -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)
@@ -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