mlx-segment-ct 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.
@@ -0,0 +1,83 @@
1
+ """Native MLX implementation of NV-Segment-CT (VISTA-3D).
2
+
3
+ Public names load on first access, so importing the package (or the medmlx
4
+ declaration in ``mlx_segment_ct.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 mlx_segment_ct.correction import normalized_point_positions
12
+ from mlx_segment_ct.correction_inferer import (
13
+ point_based_window_inferer,
14
+ )
15
+ from mlx_segment_ct.interactive import (
16
+ interactive_labels,
17
+ interactive_logits,
18
+ transform_points,
19
+ )
20
+ from mlx_segment_ct.labels import InteractivePrompt
21
+ from mlx_segment_ct.mlx_network import AutomaticModel, FeatureModel, required_shapes
22
+ from mlx_segment_ct.mlx_point import PointModel, additional_shapes, correction_shapes
23
+ from mlx_segment_ct.mlx_point_window import PointWindowPredictor
24
+ from mlx_segment_ct.mlx_upsample import AutomaticUpsample
25
+ from mlx_segment_ct.mlx_worker import automatic_logits
26
+ from mlx_segment_ct.streaming import LabelReducer, bounded_labels, rolling_logits
27
+
28
+ __version__ = "0.2.1"
29
+
30
+ _EXPORTS = {
31
+ "AutomaticModel": "mlx_segment_ct.mlx_network",
32
+ "AutomaticUpsample": "mlx_segment_ct.mlx_upsample",
33
+ "FeatureModel": "mlx_segment_ct.mlx_network",
34
+ "InteractivePrompt": "mlx_segment_ct.labels",
35
+ "LabelReducer": "mlx_segment_ct.streaming",
36
+ "PointModel": "mlx_segment_ct.mlx_point",
37
+ "PointWindowPredictor": "mlx_segment_ct.mlx_point_window",
38
+ "additional_shapes": "mlx_segment_ct.mlx_point",
39
+ "automatic_logits": "mlx_segment_ct.mlx_worker",
40
+ "bounded_labels": "mlx_segment_ct.streaming",
41
+ "correction_shapes": "mlx_segment_ct.mlx_point",
42
+ "interactive_labels": "mlx_segment_ct.interactive",
43
+ "interactive_logits": "mlx_segment_ct.interactive",
44
+ "normalized_point_positions": "mlx_segment_ct.correction",
45
+ "point_based_window_inferer": "mlx_segment_ct.correction_inferer",
46
+ "required_shapes": "mlx_segment_ct.mlx_network",
47
+ "rolling_logits": "mlx_segment_ct.streaming",
48
+ "transform_points": "mlx_segment_ct.interactive",
49
+ }
50
+
51
+ __all__ = [
52
+ "AutomaticModel",
53
+ "AutomaticUpsample",
54
+ "FeatureModel",
55
+ "InteractivePrompt",
56
+ "LabelReducer",
57
+ "PointModel",
58
+ "PointWindowPredictor",
59
+ "additional_shapes",
60
+ "automatic_logits",
61
+ "bounded_labels",
62
+ "correction_shapes",
63
+ "interactive_labels",
64
+ "interactive_logits",
65
+ "normalized_point_positions",
66
+ "point_based_window_inferer",
67
+ "required_shapes",
68
+ "rolling_logits",
69
+ "transform_points",
70
+ ]
71
+
72
+
73
+ def __getattr__(name: str) -> object:
74
+ module = _EXPORTS.get(name)
75
+ if module is None:
76
+ raise AttributeError(f"module 'mlx_segment_ct' has no attribute {name!r}")
77
+ value = getattr(import_module(module), name)
78
+ globals()[name] = value
79
+ return value
80
+
81
+
82
+ def __dir__() -> list[str]:
83
+ return sorted({*globals(), *_EXPORTS})
@@ -0,0 +1,378 @@
1
+ """Consumed native MLX and host-orchestration contracts.
2
+
3
+ The native graph uses real MLX array/Dtype identities. The orchestration boundary
4
+ is generic because the existing CPU oracle tests inject NumPy arrays there.
5
+ These partial interfaces describe the MLX and medmlx-core versions pinned in uv.lock;
6
+ they do not replace either runtime or claim complete upstream API coverage.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Callable, Mapping, Sequence
12
+ from os import PathLike
13
+ from typing import TYPE_CHECKING, Literal, NotRequired, Protocol, Required, TypedDict
14
+
15
+ import numpy as np
16
+ from medmlx_core.typing import MlxRuntime
17
+ from numpy.typing import DTypeLike, NDArray
18
+
19
+ if TYPE_CHECKING:
20
+ from mlx.core import Dtype, array
21
+
22
+ type NumericScalar = (
23
+ np.bool_
24
+ | np.int8
25
+ | np.int16
26
+ | np.int32
27
+ | np.int64
28
+ | np.uint8
29
+ | np.uint16
30
+ | np.uint32
31
+ | np.uint64
32
+ | np.float16
33
+ | np.float32
34
+ | np.float64
35
+ )
36
+ HostArray = NDArray[NumericScalar]
37
+ FloatArray = NDArray[np.float32]
38
+ DoubleArray = NDArray[np.float64]
39
+ IntArray = NDArray[np.int64]
40
+ BoolArray = NDArray[np.bool_]
41
+ type ArrayValue = array | HostArray
42
+ type NumericInput = bool | int | float | ArrayValue | Sequence[NumericInput]
43
+ type ArrayTree = array | Sequence[ArrayTree] | Mapping[str, ArrayTree]
44
+ type PatchCoordinates = Sequence[Sequence[slice]]
45
+
46
+
47
+ class ArrayBoundary[ArrayT](Protocol):
48
+ @property
49
+ def array(self) -> Callable[..., ArrayT]: ...
50
+ def eval(self, *args: ArrayT | Sequence[ArrayT] | Mapping[str, ArrayT]) -> None: ...
51
+ def synchronize(self) -> None: ...
52
+
53
+
54
+ class CoreRuntime(MlxRuntime, Protocol):
55
+ def eval(self, *args: array | Mapping[str, array] | Sequence[array]) -> None: ...
56
+ def synchronize(self) -> None: ...
57
+ def async_eval(self, *args: ArrayTree) -> None: ...
58
+ def ones(self, shape: Sequence[int], dtype: Dtype | None = None) -> array: ...
59
+ def sin(self, a: array) -> array: ...
60
+ def cos(self, a: array) -> array: ...
61
+ def abs(self, a: array) -> array: ...
62
+ def sum(
63
+ self, a: array, axis: int | Sequence[int] | None = None, keepdims: bool = False
64
+ ) -> array: ...
65
+ def softmax(
66
+ self, a: array, axis: int | Sequence[int] | None = None, *, precise: bool = False
67
+ ) -> array: ...
68
+ def addmm(
69
+ self, c: array, a: array, b: array, alpha: float = 1.0, beta: float = 1.0
70
+ ) -> array: ...
71
+ def meshgrid(
72
+ self, *arrays: array, indexing: Literal["xy", "ij"] = "xy", sparse: bool = False
73
+ ) -> tuple[array, ...]: ...
74
+ def contiguous(self, a: array, /, *, allow_col_major: bool = False) -> array: ...
75
+ def load(self, file: str, /, format: str | None = None) -> array | dict[str, array]: ...
76
+ def save_safetensors(
77
+ self, file: str, arrays: Mapping[str, array], metadata: Mapping[str, str] | None = None
78
+ ) -> None: ...
79
+
80
+
81
+ class PointPredictor[ArrayT](Protocol):
82
+ def __call__(self, image: ArrayT, positions: ArrayT, codes: ArrayT, target: int) -> ArrayT: ...
83
+
84
+
85
+ class CoordinatePredictor[ArrayT](Protocol):
86
+ def __call__(self, image: ArrayT, coordinates: PatchCoordinates, /) -> ArrayT: ...
87
+
88
+
89
+ class PointOptions(TypedDict, total=False):
90
+ transpose: Required[bool]
91
+ sw_batch_size: int
92
+ with_coord: bool
93
+ mode: str
94
+ sw_device: object
95
+ device: object
96
+ overlap: float
97
+
98
+
99
+ class PointWindowCall[ArrayT](Protocol):
100
+ def __call__(
101
+ self,
102
+ image: ArrayT,
103
+ *,
104
+ point_coords: HostArray,
105
+ point_labels: HostArray,
106
+ class_vector: HostArray | None,
107
+ prompt_class: HostArray | None,
108
+ patch_coords: PatchCoordinates,
109
+ prev_mask: HostArray | None,
110
+ transpose: bool,
111
+ **kwargs: object,
112
+ ) -> HostArray: ...
113
+
114
+
115
+ class Metrics(TypedDict):
116
+ patch_seconds: list[float]
117
+ padded_shape: NotRequired[list[int]]
118
+ padding: NotRequired[list[int]]
119
+ streaming_axis: NotRequired[int]
120
+ ring_depth: NotRequired[int]
121
+ predicted_accumulator_bytes: NotRequired[int]
122
+ count_buffer_bytes: NotRequired[int]
123
+ label_buffer_bytes: NotRequired[int]
124
+ accumulator_dtype: NotRequired[str]
125
+
126
+
127
+ class OutputLabel(TypedDict):
128
+ value: int
129
+ name: str | None
130
+
131
+
132
+ class InteractiveParameters(TypedDict):
133
+ roi_size: list[int]
134
+ center_only: bool
135
+ padding_mode: str
136
+ overlap_reduction: str
137
+ threshold: int
138
+ uncovered_label: int
139
+ positive_components: bool
140
+
141
+
142
+ class Details(TypedDict, total=False):
143
+ mode: Required[str]
144
+ label_prompt: list[int]
145
+ label_names: list[str]
146
+ automatic_branch: bool
147
+ point_start: int
148
+ output_labels: Required[list[OutputLabel]]
149
+ points: list[list[float]]
150
+ point_labels: list[int]
151
+ effective_point_labels: list[int]
152
+ preprocessed_points: list[list[float]]
153
+ preprocessed_affine: list[list[float]]
154
+ preprocessed_shape: Required[list[int]]
155
+ output_shape: Required[list[int]]
156
+ output_dtype: Required[str]
157
+ algorithm: Required[str]
158
+ parameters: InteractiveParameters
159
+ patch_count: Required[int]
160
+ load_preprocess_seconds: Required[float]
161
+ inference_stitch_seconds: Required[float]
162
+ restore_save_seconds: Required[float]
163
+ class_ids: list[int]
164
+ roi_size: list[int]
165
+ overlap: float
166
+ blend_mode: str
167
+ padding_mode: str
168
+ padded_shape: list[int]
169
+ modality: str
170
+ checkpoint: str
171
+ restoration: str
172
+ backend: str
173
+ mlx_version: str
174
+ mlx_default_device: str
175
+ precision: str
176
+ source_revision: str
177
+ mlx_peak_memory_bytes: int | None
178
+ elapsed_seconds: float
179
+ warning: str
180
+
181
+
182
+ class PublishedDetails(TypedDict, total=False):
183
+ mode: Required[str]
184
+ label_prompt: list[int]
185
+ label_names: list[str]
186
+ automatic_branch: bool
187
+ point_start: int
188
+ output_labels: Required[list[OutputLabel]]
189
+ points: list[list[float]]
190
+ point_labels: list[int]
191
+ effective_point_labels: list[int]
192
+ preprocessed_points: list[list[float]]
193
+ preprocessed_affine: list[list[float]]
194
+ preprocessed_shape: Required[list[int]]
195
+ output_shape: Required[list[int]]
196
+ output_dtype: Required[str]
197
+ algorithm: Required[str]
198
+ parameters: InteractiveParameters
199
+ patch_count: Required[int]
200
+ load_preprocess_seconds: Required[float]
201
+ inference_stitch_seconds: Required[float]
202
+ restore_save_seconds: Required[float]
203
+ class_ids: list[int]
204
+ roi_size: list[int]
205
+ overlap: float
206
+ blend_mode: str
207
+ padding_mode: str
208
+ padded_shape: list[int]
209
+ modality: str
210
+ checkpoint: Required[str]
211
+ restoration: Required[str]
212
+ backend: Required[str]
213
+ mlx_version: Required[str]
214
+ mlx_default_device: Required[str]
215
+ precision: Required[str]
216
+ source_revision: Required[str]
217
+ mlx_peak_memory_bytes: Required[int | None]
218
+ elapsed_seconds: Required[float]
219
+ warning: Required[str]
220
+
221
+
222
+ class Outputs(TypedDict):
223
+ mask: str
224
+
225
+
226
+ class RunResult(TypedDict):
227
+ outputs: Outputs
228
+ details: PublishedDetails
229
+
230
+
231
+ class RunArguments(TypedDict, total=False):
232
+ image_path: str | PathLike[str]
233
+ weights_path: str | PathLike[str]
234
+ output_dir: str | PathLike[str]
235
+ segment_all: bool
236
+ prompt: str | Sequence[str] | None
237
+ label_prompt: Sequence[int] | None
238
+ previous_logits_path: str | PathLike[str] | None
239
+ points: Sequence[Sequence[float]] | None
240
+ point_labels: Sequence[int] | None
241
+ point_start: int
242
+ interactive: bool
243
+ overwrite: bool
244
+ checkpoint: str
245
+ modality: str | None
246
+ restoration: str
247
+
248
+
249
+ class NiftiHeader(Protocol):
250
+ @property
251
+ def binaryblock(self) -> bytes: ...
252
+ def __getitem__(self, key: str) -> HostArray: ...
253
+ def get(self, key: str) -> HostArray | None: ...
254
+ def get_zooms(self) -> tuple[float, ...]: ...
255
+ def set_zooms(self, zooms: Sequence[float] | HostArray) -> None: ...
256
+ def as_byteswapped(self, endianness: str | None = None) -> NiftiHeader: ...
257
+
258
+
259
+ class NiftiArrayProxy(Protocol):
260
+ """File-backed NIfTI data supports conversion without ndarray methods."""
261
+
262
+ def __array__(self, dtype: DTypeLike | None = None) -> NDArray[np.generic]: ...
263
+
264
+
265
+ class NiftiImage(Protocol):
266
+ @property
267
+ def shape(self) -> tuple[int, ...]: ...
268
+ def get_data_dtype(self) -> np.dtype[NumericScalar]: ...
269
+ @property
270
+ def header(self) -> NiftiHeader: ...
271
+ @property
272
+ def affine(self) -> DoubleArray: ...
273
+ @property
274
+ def dataobj(self) -> HostArray | NiftiArrayProxy: ...
275
+ def get_sform(self) -> DoubleArray: ...
276
+ def get_qform(self) -> DoubleArray: ...
277
+ def set_sform(self, affine: HostArray, code: int | None = None) -> None: ...
278
+ def set_qform(self, affine: HostArray, code: int | None = None) -> None: ...
279
+
280
+
281
+ class NiftiConstructor(Protocol):
282
+ def __call__(self, dataobj: HostArray, affine: HostArray) -> NiftiImage: ...
283
+
284
+
285
+ class NiftiRuntime(Protocol):
286
+ @property
287
+ def Nifti1Image(self) -> NiftiConstructor: ...
288
+ def load(self, filename: str) -> object: ...
289
+ def save(self, img: NiftiImage, filename: str) -> None: ...
290
+ def aff2axcodes(self, aff: HostArray) -> tuple[str | None, ...]: ...
291
+ def io_orientation(self, affine: HostArray) -> DoubleArray: ...
292
+
293
+
294
+ class OrientationRuntime(Protocol):
295
+ def axcodes2ornt(self, axcodes: tuple[str | None, ...], labels: None = None) -> DoubleArray: ...
296
+ def ornt_transform(self, start_ornt: DoubleArray, end_ornt: DoubleArray) -> DoubleArray: ...
297
+ def inv_ornt_aff(self, ornt: DoubleArray, shape: Sequence[int]) -> DoubleArray: ...
298
+
299
+
300
+ class RollingMetrics(TypedDict):
301
+ patch_seconds: list[float]
302
+ padded_shape: list[int]
303
+ padding: list[int]
304
+ streaming_axis: int
305
+ ring_depth: int
306
+ predicted_accumulator_bytes: int
307
+ count_buffer_bytes: int
308
+ label_buffer_bytes: int
309
+ accumulator_dtype: str
310
+
311
+
312
+ class SlidingWindowCall(Protocol):
313
+ def __call__(
314
+ self,
315
+ inputs: ArrayValue,
316
+ roi_size: Sequence[int],
317
+ sw_batch_size: int,
318
+ predictor: Callable[..., array],
319
+ *,
320
+ overlap: float,
321
+ mode: str,
322
+ padding_mode: str,
323
+ roi_weight_map: ArrayValue | None,
324
+ with_coord: bool,
325
+ **kwargs: object,
326
+ ) -> HostArray: ...
327
+
328
+
329
+ class SlidingWindowNamespace(Protocol):
330
+ @property
331
+ def sliding_window_inference(self) -> SlidingWindowCall: ...
332
+
333
+
334
+ class StandInRuntime[ArrayT, DtypeT](ArrayBoundary[ArrayT], Protocol):
335
+ @property
336
+ def float32(self) -> DtypeT: ...
337
+ def sum(self, a: ArrayT) -> ArrayT | NumericScalar: ...
338
+
339
+
340
+ type Float32Dtype = Dtype | type[np.float32]
341
+
342
+
343
+ class Float32Cast[ArrayT, DtypeT](Protocol):
344
+ def astype(self, dtype: DtypeT) -> ArrayT: ...
345
+
346
+
347
+ class RollingMetricsBuilder(TypedDict):
348
+ patch_seconds: NotRequired[list[float]]
349
+ padded_shape: NotRequired[list[int]]
350
+ padding: NotRequired[list[int]]
351
+ streaming_axis: NotRequired[int]
352
+ ring_depth: NotRequired[int]
353
+ predicted_accumulator_bytes: NotRequired[int]
354
+ count_buffer_bytes: NotRequired[int]
355
+ label_buffer_bytes: NotRequired[int]
356
+ accumulator_dtype: NotRequired[str]
357
+
358
+
359
+ class AutomaticDetails(TypedDict):
360
+ class_ids: list[int]
361
+ padded_shape: list[int]
362
+ label_names: list[str] | None
363
+
364
+
365
+ class InteractiveDetails(TypedDict):
366
+ automatic_branch: bool
367
+ effective_point_labels: list[int]
368
+ point_start: int
369
+
370
+
371
+ class NumericArrayArithmetic[ArrayT](Protocol):
372
+ """Native and NumPy arrays accept corresponding arrays and NumPy scalars.
373
+
374
+ MLX 0.32.3 accepts the NumPy reduction scalars used by host-injected point
375
+ fixtures; its bundled array operator declaration omits those scalar types.
376
+ """
377
+
378
+ def __add__(self, other: ArrayValue | NumericScalar | float, /) -> ArrayT: ...
@@ -0,0 +1,74 @@
1
+ """GELU with PyTorch 2.14.1's ARM float32 erf and exponential arithmetic."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, cast
6
+
7
+ from medmlx_core.typing import MetalKernel
8
+
9
+ from mlx_segment_ct._types import CoreRuntime
10
+
11
+ if TYPE_CHECKING:
12
+ from mlx.core import array
13
+
14
+ _GELU = """uint i = thread_position_in_grid.x;
15
+ if (i >= SIZE) return;
16
+ float x = X[i], z = x * 0.70710678118654752440f;
17
+ float t = 1.0f / fma(0.3275911f, abs(z), 1.0f);
18
+ float r = fma(1.061405429f, t, -1.453152027f);
19
+ r = fma(r, t, 1.421413741f);
20
+ r = fma(r, t, -0.284496736f);
21
+ r = fma(r, t, 0.254829592f);
22
+ float exponent = -(z * z), exponential;
23
+ if (exponent < -0x1.5d5e2ap+6f) {
24
+ exponential = exp(exponent);
25
+ } else {
26
+ // PyTorch's NEON exp_u20 range reduction and polynomial.
27
+ float n = round(exponent * 0x1.715476p+0f);
28
+ float reduced = fma(-n, 0x1.62e4p-1f, exponent);
29
+ reduced = fma(-n, 0x1.7f7d1cp-20f, reduced);
30
+ float scale = as_type<float>((uint(int(n)) << 23) + 0x3f800000u);
31
+ float square = reduced * reduced;
32
+ float p = fma(reduced, 0x1.0e4020p-7f, 0x1.573e2ep-5f);
33
+ float q = fma(reduced, 0x1.555e66p-3f, 0x1.fffdb6p-2f);
34
+ q = fma(p, square, q);
35
+ float polynomial = fma(q, square, 0x1.ffffecp-1f * reduced);
36
+ exponential = fma(polynomial, scale, scale);
37
+ }
38
+ float erf_value = fma(-t * exponential, r, 1.0f);
39
+ erf_value = as_type<float>(as_type<uint>(erf_value) ^ (as_type<uint>(z) & 0x80000000u));
40
+ Y[i] = (x * 0.5f) * (1.0f + erf_value);
41
+ """
42
+
43
+
44
+ class Gelu:
45
+ """Source-ordered ARM GELU on Metal; no reference-runtime dependency.
46
+
47
+ The kernel reproduces PyTorch's vectorized erf (a polynomial fit) and exponential
48
+ in the reference's operation order, with floating-point contraction disabled
49
+ except for the explicit ``fma`` calls.
50
+ """
51
+
52
+ def __init__(self, mx: CoreRuntime) -> None:
53
+ self.mx = mx
54
+ self.kernel = cast(
55
+ MetalKernel,
56
+ mx.fast.metal_kernel(
57
+ name="vista_arm_gelu",
58
+ input_names=["X"],
59
+ output_names=["Y"],
60
+ source=_GELU,
61
+ header="#pragma clang fp contract(off)\n",
62
+ ),
63
+ )
64
+
65
+ def __call__(self, values: array) -> array:
66
+ """Exact (erf-based) GELU of a float32 array; the result has the same shape."""
67
+ return self.kernel(
68
+ inputs=[values],
69
+ template=[("SIZE", values.size)],
70
+ grid=(values.size, 1, 1),
71
+ threadgroup=(256, 1, 1),
72
+ output_shapes=[tuple(values.shape)],
73
+ output_dtypes=[self.mx.float32],
74
+ )[0]