flexray-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 flexray-mlx might be problematic. Click here for more details.

@@ -0,0 +1,21 @@
1
+ """Native MLX implementation of FleXray."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from flexray_mlx.mlx_model import FleXrayMlxModel
9
+
10
+ __all__ = ["FleXrayMlxModel"]
11
+
12
+ __version__ = "0.2.1"
13
+
14
+
15
+ def __getattr__(name: str) -> object:
16
+ # MLX loads with the network, so importing the medmlx declaration stays light.
17
+ if name == "FleXrayMlxModel":
18
+ from flexray_mlx.mlx_model import FleXrayMlxModel
19
+
20
+ return FleXrayMlxModel
21
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,98 @@
1
+ """FP32 Metal fusions for the released FleXray decoder's NHWC tensors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from functools import lru_cache
6
+ from typing import TYPE_CHECKING, cast
7
+
8
+ from medmlx_core.typing import MetalKernel
9
+
10
+ from flexray_mlx._types import NativeRuntime
11
+
12
+ if TYPE_CHECKING:
13
+ from mlx.core import array
14
+
15
+
16
+ _RESIZE_CONCAT = """
17
+ // Preserve the original graph's separate multiply/add rounding.
18
+ #pragma clang fp contract(off)
19
+ uint i = thread_position_in_grid.x;
20
+ constexpr int C = CI + CS;
21
+ if (i >= N * OH * OW * C) return;
22
+ uint c = i % C;
23
+ uint ox = (i / C) % OW;
24
+ uint oy = (i / (C * OW)) % OH;
25
+ uint n = i / (C * OW * OH);
26
+ if (c >= CI) {
27
+ out[i] = skip[((n * OH + oy) * OW + ox) * CS + c - CI];
28
+ return;
29
+ }
30
+ // align_corners=True: match the FP32 source-to-destination endpoint ratio.
31
+ float iy = float(oy) * (float(H - 1) / float(OH - 1));
32
+ float ix = float(ox) * (float(W - 1) / float(OW - 1));
33
+ iy = metal::clamp(iy, 0.0f, float(H - 1));
34
+ ix = metal::clamp(ix, 0.0f, float(W - 1));
35
+ uint y0 = uint(metal::floor(iy)), y1 = uint(metal::ceil(iy));
36
+ uint x0 = uint(metal::floor(ix)), x1 = uint(metal::ceil(ix));
37
+ float fy = iy - float(y0), fx = ix - float(x0);
38
+ float a = inp[((n * H + y0) * W + x0) * CI + c];
39
+ float b = inp[((n * H + y0) * W + x1) * CI + c];
40
+ float d = inp[((n * H + y1) * W + x0) * CI + c];
41
+ float e = inp[((n * H + y1) * W + x1) * CI + c];
42
+ out[i] = ((1.0f - fy) * (1.0f - fx)) * a
43
+ + ((1.0f - fy) * fx) * b
44
+ + (fy * (1.0f - fx)) * d
45
+ + (fy * fx) * e;
46
+ """
47
+
48
+
49
+ @lru_cache(maxsize=1)
50
+ def _resize_concat_kernel(mx: NativeRuntime) -> MetalKernel:
51
+ return cast(
52
+ MetalKernel,
53
+ mx.fast.metal_kernel(
54
+ name="flexray_bilinear_concat",
55
+ input_names=["inp", "skip"],
56
+ output_names=["out"],
57
+ source=_RESIZE_CONCAT,
58
+ compile_options={"math_mode": "safe"},
59
+ ),
60
+ )
61
+
62
+
63
+ def upsample_concat(x: array, skip: array, *, mx: NativeRuntime) -> array:
64
+ """Join a 2x bilinear NHWC resize with ``skip``, keeping resized channels first.
65
+
66
+ Inputs must be FP32, with the same positive batch size and a skip grid
67
+ exactly twice ``x`` in both spatial dimensions. Corner-aligned sampling
68
+ matches the released decoder, including singleton source dimensions.
69
+ One kernel writes the joined tensor without materializing the four bilinear
70
+ sample tensors or the separate resized image. Noncontiguous inputs are
71
+ made contiguous by MLX's custom-kernel boundary.
72
+
73
+ Raises ``ValueError`` when the output exceeds signed 32-bit indexing, before
74
+ dispatch. The output is larger than either input, so this also bounds their
75
+ index products.
76
+ """
77
+ if x.ndim != 4 or skip.ndim != 4:
78
+ raise ValueError("FleXray decoder fusion requires NHWC tensors")
79
+ n, h, w, ci = x.shape
80
+ if min(n, h, w, ci, skip.shape[-1]) < 1 or skip.shape[:3] != (n, 2 * h, 2 * w):
81
+ raise ValueError("FleXray MLX decoder grid mismatch")
82
+ if x.dtype != mx.float32 or skip.dtype != mx.float32:
83
+ raise TypeError("FleXray decoder fusion requires float32 inputs")
84
+ oh, ow, cs = skip.shape[1:]
85
+ shape = (n, oh, ow, ci + cs)
86
+ count = n * oh * ow * (ci + cs)
87
+ if count > 2**31 - 1:
88
+ raise ValueError("FleXray decoder fusion exceeds signed 32-bit indexing")
89
+ return _resize_concat_kernel(mx)(
90
+ inputs=[x, skip],
91
+ template=[
92
+ ("N", n), ("H", h), ("W", w), ("CI", ci), ("CS", cs), ("OH", oh), ("OW", ow),
93
+ ],
94
+ grid=(count, 1, 1),
95
+ threadgroup=(256, 1, 1),
96
+ output_shapes=[shape],
97
+ output_dtypes=[mx.float32],
98
+ )[0]
flexray_mlx/_random.py ADDED
@@ -0,0 +1,91 @@
1
+ """Torch CPU draw order for the pinned FleXray float32 augmentation chain.
2
+
3
+ MT19937 uses its low 24 bits for float uniforms. Contiguous normal tensors use
4
+ 16-value Box-Muller blocks; short tensors use cached double-precision pairs.
5
+ Transcendental rounding can differ between CPU libraries, as it does in Torch.
6
+ Adapted from PyTorch's ATen/core and native/cpu distribution helpers (BSD;
7
+ see root THIRD_PARTY_NOTICES.md).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import math
13
+
14
+ import numpy as np
15
+
16
+ from flexray_mlx._types import FloatArray, UIntArray
17
+
18
+
19
+ class TorchRandom:
20
+ """An isolated stream that reproduces Torch CPU draws, without importing Torch.
21
+
22
+ Draws follow Torch's order of consumption for a given seed and call sequence;
23
+ the module docstring notes where float rounding can still differ. Values are
24
+ float32.
25
+ """
26
+
27
+ def __init__(self, seed: int | None) -> None:
28
+ """Seed from the low 32 bits of ``seed`` (negative values wrap); None is unseeded."""
29
+
30
+ self._engine = np.random.RandomState(None if seed is None else seed & 0xFFFFFFFF)
31
+ self._next_normal: float | None = None
32
+
33
+ def _words(self, count: int) -> UIntArray:
34
+ return self._engine.randint(0, 2**32, size=count, dtype=np.uint32)
35
+
36
+ def _units(self, count: int) -> FloatArray:
37
+ return (self._words(count) & np.uint32(0xFFFFFF)).astype(np.float32) * np.float32(2**-24)
38
+
39
+ def uniform(self, low: float = 0.0, high: float = 1.0) -> float:
40
+ """Draw one float32 uniform between ``low`` and ``high``, like Torch's ``uniform_``."""
41
+ # Deferred: flexray_mlx.medmlx imports this module (via tta) at load time.
42
+ from flexray_mlx.medmlx import fma32
43
+
44
+ lower, upper = np.float32(low), np.float32(high)
45
+ return float(fma32(self._units(1)[0], upper - lower, lower))
46
+
47
+ def chance(self, probability: float) -> np.bool_:
48
+ """Consume one uniform draw and return whether it is below ``probability``."""
49
+ return self.uniform() < np.float32(probability)
50
+
51
+ def _double_unit(self) -> float:
52
+ words = self._words(2).astype(np.uint64)
53
+ value = (int(words[0]) << 32) | int(words[1])
54
+ return (value & (2**53 - 1)) * 2**-53
55
+
56
+ def _short_normal(self) -> float:
57
+ if self._next_normal is not None:
58
+ value, self._next_normal = self._next_normal, None
59
+ return value
60
+ theta = 2.0 * math.pi * self._double_unit()
61
+ radius = math.sqrt(-2.0 * math.log1p(-self._double_unit()))
62
+ self._next_normal = radius * math.sin(theta)
63
+ return radius * math.cos(theta)
64
+
65
+ @staticmethod
66
+ def _normal_blocks(uniforms: FloatArray) -> FloatArray:
67
+ blocks = uniforms.reshape(-1, 16)
68
+ radius = np.sqrt(np.float32(-2.0) * np.log(np.float32(1.0) - blocks[:, :8]))
69
+ # The scalar CPU kernel multiplies by double pi before rounding theta.
70
+ theta = (2.0 * math.pi * blocks[:, 8:].astype(np.float64)).astype(np.float32)
71
+ return np.concatenate((radius * np.cos(theta), radius * np.sin(theta)), axis=1).ravel()
72
+
73
+ def normal(self, shape: tuple[int, ...]) -> FloatArray:
74
+ """Draw a float32 standard-normal array of ``shape``.
75
+
76
+ Fewer than 16 values use cached double-precision Box-Muller pairs; 16 or
77
+ more use float32 blocks of 16, as Torch's CPU kernel does.
78
+ """
79
+ count = math.prod(shape)
80
+ if count < 16:
81
+ return np.asarray([self._short_normal() for _ in range(count)], np.float32).reshape(
82
+ shape
83
+ )
84
+ uniforms = self._units(count)
85
+ complete = count // 16 * 16
86
+ values = np.empty(count, dtype=np.float32)
87
+ values[:complete] = self._normal_blocks(uniforms[:complete])
88
+ if count % 16:
89
+ # Torch redraws the final 16, including the overlapping completed block.
90
+ values[-16:] = self._normal_blocks(self._units(16))
91
+ return values.reshape(shape)
flexray_mlx/_types.py ADDED
@@ -0,0 +1,126 @@
1
+ """Array and namespace contracts for the pinned native and file boundaries."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from os import PathLike
7
+ from typing import TYPE_CHECKING, Literal, Protocol, TypedDict
8
+
9
+ import numpy as np
10
+ from medmlx_core.typing import FastRuntime
11
+ from numpy.typing import NDArray
12
+
13
+ if TYPE_CHECKING:
14
+ from mlx.core import Dtype, array
15
+
16
+ type HostArray = NDArray[np.generic]
17
+ type RealArray = NDArray[np.bool_ | np.integer | np.floating]
18
+ type FloatArray = NDArray[np.float32]
19
+ type DoubleArray = NDArray[np.float64]
20
+ type BoolArray = NDArray[np.bool_]
21
+ type IntArray = NDArray[np.int64]
22
+ type UIntArray = NDArray[np.uint32]
23
+ type JsonValue = bool | int | float | str | list[JsonValue] | dict[str, JsonValue] | None
24
+ type JsonObject = dict[str, JsonValue]
25
+
26
+
27
+ class NativeRuntime(Protocol):
28
+ fast: FastRuntime
29
+ float32: Dtype
30
+
31
+ # Constructor inputs here are the NumPy arrays prepared by this network.
32
+ def array(self, value: FloatArray) -> array: ...
33
+ def eval(self, *args: array) -> None: ...
34
+ def conv2d(self, input: array, weight: array, /, *, padding: int) -> array: ...
35
+ def mean(self, a: array, /, *, axis: Sequence[int], keepdims: bool) -> array: ...
36
+ def square(self, a: array, /) -> array: ...
37
+ def rsqrt(self, a: array, /) -> array: ...
38
+ def maximum(self, a: array, b: array, /) -> array: ...
39
+ def concatenate(self, arrays: list[array], *, axis: int) -> array: ...
40
+
41
+
42
+ class NativeOperation(Protocol):
43
+ def __call__(self, x: array) -> array: ...
44
+
45
+
46
+ class NeuralRuntime(Protocol):
47
+ def MaxPool2d(self, *, kernel_size: int, stride: int) -> NativeOperation: ...
48
+ def Upsample(self, *, scale_factor: int, mode: str, align_corners: bool) -> NativeOperation: ...
49
+
50
+
51
+ class TensorReader(Protocol):
52
+ def __enter__(self) -> TensorReader: ...
53
+ def __exit__(self, *args: object) -> None: ...
54
+ def keys(self) -> list[str]: ...
55
+ def get_tensor(self, name: str) -> HostArray: ...
56
+
57
+
58
+ class SafetensorsRuntime(Protocol):
59
+ def safe_open(self, filename: PathLike[str], *, framework: Literal["np"]) -> TensorReader: ...
60
+
61
+
62
+ class DicomDataset(Protocol):
63
+ pixel_array: RealArray
64
+
65
+ def __contains__(self, name: str) -> bool: ...
66
+
67
+
68
+ class DicomRuntime(Protocol):
69
+ def dcmread(self, fp: str, *, force: bool) -> DicomDataset: ...
70
+
71
+
72
+ class Normalization(TypedDict):
73
+ scheme: str
74
+ percentiles: tuple[float, float] | None
75
+ eps: float
76
+ mean: float | None
77
+ std: float | None
78
+
79
+
80
+ class EnsembleMember(TypedDict):
81
+ subfolder: str
82
+
83
+
84
+ class EnsembleManifest(TypedDict):
85
+ members: list[EnsembleMember]
86
+ flagship: str
87
+
88
+
89
+ class RunOutputs(TypedDict):
90
+ """POSIX paths of the files ``run`` writes to its output directory."""
91
+
92
+ mask: str
93
+ display_source: str
94
+ probabilities: str
95
+ prediction: str
96
+
97
+
98
+ class RunDetails(TypedDict):
99
+ """Settings and geometry of one ``run``.
100
+
101
+ ``source_size`` is ``[width, height]`` of the input; ``model_grid`` is
102
+ ``[height, width]``. ``content_box_tlbr`` is ``[top, left, bottom, right]``
103
+ in model-grid pixels with exclusive bottom/right.
104
+ """
105
+
106
+ selected_label: str
107
+ selected_channel: int
108
+ threshold: float
109
+ ensemble: bool
110
+ members: list[str]
111
+ tta_samples: int
112
+ tta_seed: int | None
113
+ normalization_scheme: str
114
+ source_size: list[int]
115
+ model_grid: list[int]
116
+ content_box_tlbr: list[int]
117
+ mask_pixels: int
118
+ probability_mode: str
119
+ backend: str
120
+
121
+
122
+ class RunResult(TypedDict):
123
+ """Return value of ``run``: output file paths and run details."""
124
+
125
+ outputs: RunOutputs
126
+ details: RunDetails