lesiondiffusion-mlx 0.2.3__tar.gz → 0.2.4__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.
- {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/PKG-INFO +2 -2
- {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/README.md +1 -1
- {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/pyproject.toml +1 -1
- {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/__init__.py +2 -1
- {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/_group_norm.py +1 -0
- {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/_nax_conv.py +1 -0
- lesiondiffusion_mlx-0.2.4/src/lesiondiffusion_mlx/_nifti.py +111 -0
- {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/_reference.py +1 -0
- {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/_schedules.py +1 -0
- {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/_source_half.py +2 -0
- lesiondiffusion_mlx-0.2.4/src/lesiondiffusion_mlx/_typing.py +178 -0
- {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/medmlx.py +13 -0
- {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/mlx_model.py +9 -0
- lesiondiffusion_mlx-0.2.3/src/lesiondiffusion_mlx/_nifti.py +0 -73
- lesiondiffusion_mlx-0.2.3/src/lesiondiffusion_mlx/_typing.py +0 -122
- {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/.gitignore +0 -0
- {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/LICENSE +0 -0
- {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/THIRD_PARTY_NOTICES.md +0 -0
- {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/_arrays.py +0 -0
- {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/_attention.py +0 -0
- {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/_spatial.py +0 -0
- {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/mlx_pipeline.py +0 -0
- {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/mlx_sampling.py +0 -0
- {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/py.typed +0 -0
- {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/reference.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: lesiondiffusion-mlx
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Native MLX implementation of LesionDiffusion
|
|
5
5
|
Project-URL: Homepage, https://huggingface.co/MedMLX/lesiondiffusion-mlx
|
|
6
6
|
Author: Joseph Sandoval, Alfredo Sandoval Jr.
|
|
@@ -47,7 +47,7 @@ Add synthetic lesions to CT with native MLX, with controls for organ, size, plac
|
|
|
47
47
|
```bash
|
|
48
48
|
uv venv --python 3.13
|
|
49
49
|
source .venv/bin/activate
|
|
50
|
-
uv pip install "lesiondiffusion-mlx>=0.2.
|
|
50
|
+
uv pip install "lesiondiffusion-mlx>=0.2.4"
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
```python
|
|
@@ -15,7 +15,7 @@ Add synthetic lesions to CT with native MLX, with controls for organ, size, plac
|
|
|
15
15
|
```bash
|
|
16
16
|
uv venv --python 3.13
|
|
17
17
|
source .venv/bin/activate
|
|
18
|
-
uv pip install "lesiondiffusion-mlx>=0.2.
|
|
18
|
+
uv pip install "lesiondiffusion-mlx>=0.2.4"
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
```python
|
|
@@ -32,7 +32,7 @@ if TYPE_CHECKING:
|
|
|
32
32
|
)
|
|
33
33
|
from lesiondiffusion_mlx.mlx_sampling import DdimSampler as DdimSampler
|
|
34
34
|
|
|
35
|
-
__version__ = "0.2.
|
|
35
|
+
__version__ = "0.2.4"
|
|
36
36
|
|
|
37
37
|
__all__ = [
|
|
38
38
|
"LINET_CONFIG",
|
|
@@ -48,6 +48,7 @@ __all__ = [
|
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
def __getattr__(name: str) -> object:
|
|
51
|
+
"""Load a public export from its module on first access."""
|
|
51
52
|
if name not in __all__:
|
|
52
53
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
53
54
|
module = (
|
{lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/_group_norm.py
RENAMED
|
@@ -98,6 +98,7 @@ if (SILU) y = y / (1.0f + metal::precise::exp(-y));
|
|
|
98
98
|
|
|
99
99
|
@lru_cache(maxsize=1)
|
|
100
100
|
def _kernels(mx: Mlx) -> tuple[MetalKernel, MetalKernel, MetalKernel]:
|
|
101
|
+
"""Build or reuse the native group-normalization Metal kernels."""
|
|
101
102
|
return (
|
|
102
103
|
mx.fast.metal_kernel(
|
|
103
104
|
name="lesion_group_norm_partial",
|
{lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/_nax_conv.py
RENAMED
|
@@ -392,6 +392,7 @@ def pack_upsample_weight(weight: Array, mx: Mlx) -> Array:
|
|
|
392
392
|
# parity p reads padded source offset p + t. Plain FP32 additions, not an
|
|
393
393
|
# einsum, whose matmul could run in TF32 under MLX_ENABLE_TF32.
|
|
394
394
|
def fold(taps: Array) -> Array:
|
|
395
|
+
"""Fold one upsampling tap into packed convolution weights."""
|
|
395
396
|
k0, k1, k2 = taps[0], taps[1], taps[2]
|
|
396
397
|
return mx.stack([mx.stack([k0, k1 + k2]), mx.stack([k0 + k1, k2])])
|
|
397
398
|
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""The NIfTI surface used here, verified against NiBabel's image/header APIs."""
|
|
2
|
+
|
|
3
|
+
from os import PathLike
|
|
4
|
+
from typing import Literal, Protocol, cast
|
|
5
|
+
|
|
6
|
+
import nibabel
|
|
7
|
+
import numpy as np
|
|
8
|
+
from nibabel.filebasedimages import ImageFileError as ImageFileError
|
|
9
|
+
|
|
10
|
+
from lesiondiffusion_mlx._arrays import HostArray
|
|
11
|
+
|
|
12
|
+
type PathArg = str | PathLike[str]
|
|
13
|
+
type SpatialAffine = np.ndarray[tuple[int, ...], np.dtype[np.float64]]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ArrayBuffer(Protocol):
|
|
17
|
+
def __array__(
|
|
18
|
+
self, dtype: np.dtype[np.generic] | None = None, copy: bool | None = None
|
|
19
|
+
) -> HostArray:
|
|
20
|
+
"""Convert the image data buffer to a host array."""
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class NiftiHeader(Protocol):
|
|
25
|
+
def get_zooms(self) -> tuple[float, ...]:
|
|
26
|
+
"""Return voxel spacing from the header."""
|
|
27
|
+
...
|
|
28
|
+
def get_xyzt_units(self) -> tuple[str, str]:
|
|
29
|
+
"""Return spatial and temporal unit names."""
|
|
30
|
+
...
|
|
31
|
+
def set_xyzt_units(self, xyz: str, t: str) -> None:
|
|
32
|
+
"""Set spatial and temporal unit names."""
|
|
33
|
+
...
|
|
34
|
+
def set_data_dtype(self, dtype: type[np.generic]) -> None:
|
|
35
|
+
"""Set the stored image dtype."""
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class HeaderFactory(Protocol):
|
|
40
|
+
def from_header(self, header: NiftiHeader) -> NiftiHeader:
|
|
41
|
+
"""Construct a NIfTI header from an existing header."""
|
|
42
|
+
...
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class NiftiImage(Protocol):
|
|
46
|
+
shape: tuple[int, ...]
|
|
47
|
+
affine: SpatialAffine
|
|
48
|
+
dataobj: ArrayBuffer | HostArray
|
|
49
|
+
header: NiftiHeader
|
|
50
|
+
|
|
51
|
+
def get_qform(self, *, coded: Literal[True]) -> tuple[SpatialAffine | None, int]:
|
|
52
|
+
"""Return the qform affine and its code."""
|
|
53
|
+
...
|
|
54
|
+
def get_sform(self, *, coded: Literal[True]) -> tuple[SpatialAffine | None, int]:
|
|
55
|
+
"""Return the sform affine and its code."""
|
|
56
|
+
...
|
|
57
|
+
def set_qform(self, affine: HostArray, code: int) -> None:
|
|
58
|
+
"""Set the qform affine and code."""
|
|
59
|
+
...
|
|
60
|
+
def set_sform(self, affine: HostArray, code: int) -> None:
|
|
61
|
+
"""Set the sform affine and code."""
|
|
62
|
+
...
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ImageFactory(Protocol):
|
|
66
|
+
def __call__(
|
|
67
|
+
self, dataobj: HostArray, affine: HostArray, header: NiftiHeader | None = None
|
|
68
|
+
) -> NiftiImage:
|
|
69
|
+
"""Construct a NIfTI image from data, affine, and header."""
|
|
70
|
+
...
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class OrientationFunctions(Protocol):
|
|
74
|
+
"""The subset of ``nibabel.orientations`` used to resample to RAS 1 mm."""
|
|
75
|
+
|
|
76
|
+
def axcodes2ornt(self, axcodes: tuple[str, str, str]) -> HostArray:
|
|
77
|
+
"""Convert axis codes to an orientation transform."""
|
|
78
|
+
...
|
|
79
|
+
def ornt_transform(self, start_ornt: HostArray, end_ornt: HostArray) -> HostArray:
|
|
80
|
+
"""Compute the transform between two orientations."""
|
|
81
|
+
...
|
|
82
|
+
def apply_orientation(self, data: HostArray, ornt: HostArray) -> HostArray:
|
|
83
|
+
"""Apply an orientation transform to image data."""
|
|
84
|
+
...
|
|
85
|
+
def inv_ornt_aff(self, ornt: HostArray, shape: tuple[int, ...]) -> SpatialAffine:
|
|
86
|
+
"""Return the inverse voxel affine for an orientation and shape."""
|
|
87
|
+
...
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class Nibabel(Protocol):
|
|
91
|
+
Nifti1Header: HeaderFactory
|
|
92
|
+
Nifti1Image: ImageFactory
|
|
93
|
+
orientations: OrientationFunctions
|
|
94
|
+
|
|
95
|
+
def load(self, filename: PathArg) -> NiftiImage:
|
|
96
|
+
"""Load an image from a path."""
|
|
97
|
+
...
|
|
98
|
+
def save(self, image: NiftiImage, filename: PathArg) -> None:
|
|
99
|
+
"""Write a NIfTI image to a path."""
|
|
100
|
+
...
|
|
101
|
+
def aff2axcodes(self, affine: HostArray) -> tuple[str | None, ...]:
|
|
102
|
+
"""Derive axis codes from an affine."""
|
|
103
|
+
...
|
|
104
|
+
def io_orientation(self, affine: HostArray) -> HostArray:
|
|
105
|
+
"""Derive voxel-axis orientation from an affine."""
|
|
106
|
+
...
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# This adapter is used only for .nii/.nii.gz inputs and NIfTI output; the wider
|
|
110
|
+
# NiBabel loader supports unrelated image classes outside the runner contract.
|
|
111
|
+
nib = cast(Nibabel, nibabel)
|
{lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/_reference.py
RENAMED
|
@@ -25,6 +25,7 @@ class SourceReference(TypedDict):
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
def _string(value: object) -> str:
|
|
28
|
+
"""Require a string for a reference metadata field."""
|
|
28
29
|
if not isinstance(value, str):
|
|
29
30
|
raise ValueError("reference.json source names, revisions and labels must be strings")
|
|
30
31
|
return value
|
{lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/_source_half.py
RENAMED
|
@@ -342,9 +342,11 @@ def _norm_affine_kernels(mx: Mlx) -> tuple[MetalKernel, MetalKernel]:
|
|
|
342
342
|
|
|
343
343
|
@lru_cache(maxsize=1)
|
|
344
344
|
def _kernels(mx: Mlx) -> tuple[MetalKernel, MetalKernel, MetalKernel, MetalKernel, MetalKernel]:
|
|
345
|
+
"""Build or reuse the source-order half-precision Metal kernels."""
|
|
345
346
|
def kernel(
|
|
346
347
|
name: str, inputs: list[str], output: str, source: str, header: str = ""
|
|
347
348
|
) -> MetalKernel:
|
|
349
|
+
"""Construct one named source-order Metal kernel."""
|
|
348
350
|
return mx.fast.metal_kernel(
|
|
349
351
|
name="lesion_source_half_" + name,
|
|
350
352
|
input_names=inputs,
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Native array and dependency contracts used by the source implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Literal, Protocol
|
|
6
|
+
|
|
7
|
+
from tokenizers import Encoding
|
|
8
|
+
|
|
9
|
+
from lesiondiffusion_mlx._arrays import HostArray as HostArray
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from collections.abc import Callable, Sequence
|
|
13
|
+
|
|
14
|
+
import mlx.core as mx
|
|
15
|
+
from medmlx_core.typing import FastRuntime as CoreFastRuntime
|
|
16
|
+
from medmlx_core.typing import MlxRuntime
|
|
17
|
+
|
|
18
|
+
type Array = mx.array
|
|
19
|
+
type Dtype = mx.Dtype
|
|
20
|
+
type ArrayValue = bool | int | float | complex | list[ArrayValue] | tuple[ArrayValue, ...]
|
|
21
|
+
|
|
22
|
+
class ArrayConstructor(Protocol):
|
|
23
|
+
def __call__(
|
|
24
|
+
self,
|
|
25
|
+
value: HostArray | Array | ArrayValue,
|
|
26
|
+
/,
|
|
27
|
+
dtype: Dtype | None = None,
|
|
28
|
+
) -> Array:
|
|
29
|
+
"""Construct a native array from values and an optional dtype."""
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
class TokenizerContract(Protocol):
|
|
33
|
+
def enable_truncation(self, max_length: int) -> None:
|
|
34
|
+
"""Enable tokenizer truncation at the requested length."""
|
|
35
|
+
...
|
|
36
|
+
def enable_padding(self, *, length: int, pad_id: int, pad_token: str) -> None:
|
|
37
|
+
"""Configure tokenizer padding and its token ID."""
|
|
38
|
+
...
|
|
39
|
+
def encode_batch(self, inputs: Sequence[str]) -> list[Encoding]:
|
|
40
|
+
"""Encode a batch of input strings."""
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
class MetalKernel(Protocol):
|
|
44
|
+
def __call__(
|
|
45
|
+
self,
|
|
46
|
+
*,
|
|
47
|
+
inputs: Sequence[Array],
|
|
48
|
+
template: Sequence[tuple[str, int | bool | Dtype]],
|
|
49
|
+
grid: tuple[int, int, int],
|
|
50
|
+
threadgroup: tuple[int, int, int],
|
|
51
|
+
output_shapes: Sequence[Sequence[int]],
|
|
52
|
+
output_dtypes: Sequence[Dtype],
|
|
53
|
+
) -> list[Array]:
|
|
54
|
+
"""Launch a Metal kernel and return its output arrays."""
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
class Fast(CoreFastRuntime, Protocol):
|
|
58
|
+
def scaled_dot_product_attention(
|
|
59
|
+
self,
|
|
60
|
+
q: Array,
|
|
61
|
+
k: Array,
|
|
62
|
+
v: Array,
|
|
63
|
+
*,
|
|
64
|
+
scale: float,
|
|
65
|
+
mask: str | Array | None = None,
|
|
66
|
+
sinks: Array | None = None,
|
|
67
|
+
force_fused: bool = False,
|
|
68
|
+
) -> Array:
|
|
69
|
+
"""Compute native scaled dot-product attention."""
|
|
70
|
+
...
|
|
71
|
+
|
|
72
|
+
def metal_kernel(
|
|
73
|
+
self,
|
|
74
|
+
name: str,
|
|
75
|
+
input_names: Sequence[str],
|
|
76
|
+
output_names: Sequence[str],
|
|
77
|
+
source: str,
|
|
78
|
+
header: str = "",
|
|
79
|
+
ensure_row_contiguous: bool = True,
|
|
80
|
+
atomic_outputs: bool = False,
|
|
81
|
+
compile_options: object | None = None,
|
|
82
|
+
) -> MetalKernel:
|
|
83
|
+
"""Compile a named native Metal kernel."""
|
|
84
|
+
...
|
|
85
|
+
|
|
86
|
+
class Random(Protocol):
|
|
87
|
+
def seed(self, seed: int) -> None:
|
|
88
|
+
"""Set the native random generator seed."""
|
|
89
|
+
...
|
|
90
|
+
def normal(self, shape: Sequence[int]) -> Array:
|
|
91
|
+
"""Draw normally distributed native values."""
|
|
92
|
+
...
|
|
93
|
+
|
|
94
|
+
class Mlx(MlxRuntime, Protocol):
|
|
95
|
+
"""The MLX module surface consumed by this port, without importing it eagerly."""
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def fast(self) -> Fast:
|
|
99
|
+
"""Return the native fast operations boundary."""
|
|
100
|
+
...
|
|
101
|
+
|
|
102
|
+
random: Random
|
|
103
|
+
|
|
104
|
+
def addmm(self, c: Array, a: Array, b: Array, /) -> Array:
|
|
105
|
+
"""Multiply matrices and add the supplied bias."""
|
|
106
|
+
...
|
|
107
|
+
def all(self, a: Array, /) -> Array:
|
|
108
|
+
"""Reduce a native array with logical all."""
|
|
109
|
+
...
|
|
110
|
+
def argmin(self, a: Array, /, *, axis: int | None = None) -> Array:
|
|
111
|
+
"""Return indices of minimum values along an axis."""
|
|
112
|
+
...
|
|
113
|
+
def clear_cache(self) -> None:
|
|
114
|
+
"""Clear the native allocator cache."""
|
|
115
|
+
...
|
|
116
|
+
def compile[**P, R](self, fun: Callable[P, R], /) -> Callable[P, R]:
|
|
117
|
+
"""Compile a callable for native execution."""
|
|
118
|
+
...
|
|
119
|
+
def contiguous(self, a: Array, /) -> Array:
|
|
120
|
+
"""Return a contiguous native array."""
|
|
121
|
+
...
|
|
122
|
+
def cos(self, a: Array, /) -> Array:
|
|
123
|
+
"""Compute elementwise cosine."""
|
|
124
|
+
...
|
|
125
|
+
def device_info(self) -> dict[str, str | int]:
|
|
126
|
+
"""Return native device capability information."""
|
|
127
|
+
...
|
|
128
|
+
def erf(self, a: Array, /) -> Array:
|
|
129
|
+
"""Compute the elementwise error function."""
|
|
130
|
+
...
|
|
131
|
+
def exp(self, a: Array, /) -> Array:
|
|
132
|
+
"""Compute the elementwise exponential."""
|
|
133
|
+
...
|
|
134
|
+
def full(self, shape: Sequence[int], vals: object, *, dtype: Dtype) -> Array:
|
|
135
|
+
"""Create a native array filled with the supplied value."""
|
|
136
|
+
...
|
|
137
|
+
def isfinite(self, a: Array, /) -> Array:
|
|
138
|
+
"""Test native values for finiteness."""
|
|
139
|
+
...
|
|
140
|
+
def load(self, file: str, *, format: Literal["safetensors"]) -> dict[str, Array]:
|
|
141
|
+
"""Load named arrays from a safetensors file."""
|
|
142
|
+
...
|
|
143
|
+
def reciprocal(self, a: Array, /) -> Array:
|
|
144
|
+
"""Compute elementwise reciprocals."""
|
|
145
|
+
...
|
|
146
|
+
def sin(self, a: Array, /) -> Array:
|
|
147
|
+
"""Compute elementwise sine."""
|
|
148
|
+
...
|
|
149
|
+
def softmax(
|
|
150
|
+
self, a: Array, /, *, axis: int | None = None, precise: bool = False
|
|
151
|
+
) -> Array:
|
|
152
|
+
"""Normalize scores along the requested axis."""
|
|
153
|
+
...
|
|
154
|
+
def sqrt(self, a: Array | float, /) -> Array:
|
|
155
|
+
"""Compute elementwise square roots."""
|
|
156
|
+
...
|
|
157
|
+
def sum(self, a: Array, /, *, axis: int | None = None, keepdims: bool = False) -> Array:
|
|
158
|
+
"""Sum native values over an optional axis."""
|
|
159
|
+
...
|
|
160
|
+
else:
|
|
161
|
+
|
|
162
|
+
class ArrayConstructor(Protocol):
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
class TokenizerContract(Protocol):
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
class MetalKernel(Protocol):
|
|
169
|
+
pass
|
|
170
|
+
|
|
171
|
+
class Fast(Protocol):
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
class Random(Protocol):
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
class Mlx(Protocol):
|
|
178
|
+
pass
|
|
@@ -249,6 +249,7 @@ class LesionDiffusionOptions:
|
|
|
249
249
|
output_region: str = DEFAULT_OUTPUT_REGION
|
|
250
250
|
|
|
251
251
|
def __post_init__(self) -> None:
|
|
252
|
+
"""Validate inference, geometry, and lesion-generation options."""
|
|
252
253
|
if self.inference_mode not in {"conditioned", "source"}:
|
|
253
254
|
raise InvalidInputError("LesionDiffusion inference_mode must be conditioned or source")
|
|
254
255
|
if self.output_region not in {"full", "box"}:
|
|
@@ -495,6 +496,7 @@ def _run_paths(
|
|
|
495
496
|
output_region: str,
|
|
496
497
|
overwrite: bool,
|
|
497
498
|
) -> tuple[Path, Path, dict[str, Path]]:
|
|
499
|
+
"""Resolve input and output paths and prevent output collisions."""
|
|
498
500
|
if not isinstance(cast(object, overwrite), bool):
|
|
499
501
|
raise InvalidInputError("overwrite must be a boolean")
|
|
500
502
|
source = Path(image_path).expanduser().resolve()
|
|
@@ -542,6 +544,7 @@ def _execute_run(
|
|
|
542
544
|
directory: Path,
|
|
543
545
|
outputs: dict[str, Path],
|
|
544
546
|
) -> tuple[GenerationSpec, dict[str, dict[str, object]]]:
|
|
547
|
+
"""Run native generation and write its validated outputs."""
|
|
545
548
|
model_label, _ = source_organ_label(options.organ)
|
|
546
549
|
report = _structured_report(
|
|
547
550
|
reference=geometry.reference,
|
|
@@ -603,6 +606,7 @@ def _run_result(
|
|
|
603
606
|
started_at: datetime,
|
|
604
607
|
started: float,
|
|
605
608
|
) -> RunResult:
|
|
609
|
+
"""Build the output paths and generation metadata for a completed run."""
|
|
606
610
|
grid = [int(value) for value in geometry.output_reference.shape]
|
|
607
611
|
details: RunDetails = {
|
|
608
612
|
"model_id": MODEL_ID,
|
|
@@ -767,6 +771,7 @@ def run_native_pipeline(
|
|
|
767
771
|
def _linet_attributes(
|
|
768
772
|
attributes: Mapping[str, object], options: LesionDiffusionOptions
|
|
769
773
|
) -> dict[str, object]:
|
|
774
|
+
"""Build image-synthesis text attributes for the selected organ and mode."""
|
|
770
775
|
_, organ_name = source_organ_label(options.organ)
|
|
771
776
|
omitted = {"size"}
|
|
772
777
|
if options.inference_mode == "conditioned":
|
|
@@ -787,6 +792,7 @@ def _lmnet_attributes(
|
|
|
787
792
|
attributes: Mapping[str, object],
|
|
788
793
|
options: LesionDiffusionOptions,
|
|
789
794
|
) -> dict[str, object]:
|
|
795
|
+
"""Build mask-synthesis text attributes using the pinned source conventions."""
|
|
790
796
|
_, organ_name = source_organ_label(options.organ)
|
|
791
797
|
lm_attributes = dict(attributes)
|
|
792
798
|
lm_attributes["lesion location"] = organ_name
|
|
@@ -822,6 +828,7 @@ def _run_lmnet(
|
|
|
822
828
|
*,
|
|
823
829
|
anatomy: HostArray,
|
|
824
830
|
) -> tuple[BoolArray, dict[str, object]]:
|
|
831
|
+
"""Generate and validate a lesion mask within the selected crop."""
|
|
825
832
|
crop = source_crop(bbox, LMNET_CROP)
|
|
826
833
|
lm_started = time.perf_counter()
|
|
827
834
|
optimized = options.inference_mode == "conditioned"
|
|
@@ -890,6 +897,7 @@ def _run_linet(
|
|
|
890
897
|
tokenizer_path: Path,
|
|
891
898
|
mx: Mlx,
|
|
892
899
|
) -> tuple[HostArray, dict[str, object]]:
|
|
900
|
+
"""Generate the lesion image using the selected mask and attributes."""
|
|
893
901
|
lin_started = time.perf_counter()
|
|
894
902
|
source_arithmetic = options.inference_mode == "source"
|
|
895
903
|
linet = LdmUnet(
|
|
@@ -1156,6 +1164,7 @@ def load_stage(directory: Path, name: str, mx: Mlx, *, dtype: Dtype) -> dict[str
|
|
|
1156
1164
|
|
|
1157
1165
|
|
|
1158
1166
|
def _load_json_object(path: Path) -> dict[str, object]:
|
|
1167
|
+
"""Read a JSON object or report an invalid metadata file."""
|
|
1159
1168
|
try:
|
|
1160
1169
|
payload: object = json.loads(path.read_text(encoding="utf-8"))
|
|
1161
1170
|
except (UnicodeError, json.JSONDecodeError) as exc:
|
|
@@ -1188,6 +1197,7 @@ def _parse_options(
|
|
|
1188
1197
|
output_region: object = DEFAULT_OUTPUT_REGION,
|
|
1189
1198
|
lesion_mask_supplied: bool = False,
|
|
1190
1199
|
) -> LesionDiffusionOptions:
|
|
1200
|
+
"""Validate request values and construct generation options."""
|
|
1191
1201
|
for name, value in (
|
|
1192
1202
|
("organ", organ),
|
|
1193
1203
|
("lesion_type", lesion_type),
|
|
@@ -1274,6 +1284,7 @@ def _check_box_source(
|
|
|
1274
1284
|
|
|
1275
1285
|
|
|
1276
1286
|
def _is_integer(value: object) -> bool:
|
|
1287
|
+
"""Recognize integer inputs while excluding booleans."""
|
|
1277
1288
|
return isinstance(value, int) and not isinstance(value, bool)
|
|
1278
1289
|
|
|
1279
1290
|
|
|
@@ -2137,11 +2148,13 @@ def localize_output(
|
|
|
2137
2148
|
|
|
2138
2149
|
def _with_comma(value: str) -> str:
|
|
2139
2150
|
# Upstream report phrases are comma-terminated.
|
|
2151
|
+
"""Normalize an upstream report phrase to end with one comma."""
|
|
2140
2152
|
stripped = value.strip().rstrip(",")
|
|
2141
2153
|
return f"{stripped},"
|
|
2142
2154
|
|
|
2143
2155
|
|
|
2144
2156
|
def _json_safe(value: object) -> object:
|
|
2157
|
+
"""Convert nested mappings, sequences, and NumPy scalars to JSON-compatible values."""
|
|
2145
2158
|
if isinstance(value, Mapping):
|
|
2146
2159
|
return {
|
|
2147
2160
|
str(key): _json_safe(item) for key, item in cast(Mapping[object, object], value).items()
|
{lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/mlx_model.py
RENAMED
|
@@ -99,6 +99,7 @@ class SourceLayers:
|
|
|
99
99
|
source_arithmetic: bool = True,
|
|
100
100
|
native_kernels: bool | None = None,
|
|
101
101
|
):
|
|
102
|
+
"""Initialize source-keyed weights and the selected native arithmetic."""
|
|
102
103
|
self.weights = weights
|
|
103
104
|
self.mx = mx
|
|
104
105
|
self.source_arithmetic = source_arithmetic
|
|
@@ -188,6 +189,7 @@ class SourceLayers:
|
|
|
188
189
|
return linear(x, weight, bias, self.mx).astype(dtype)
|
|
189
190
|
|
|
190
191
|
def _affine_inputs(self, x: Array, key: str) -> tuple[Array, Array, Array | None]:
|
|
192
|
+
"""Promote half inputs and weights for source-compatible affine accumulation."""
|
|
191
193
|
weight = self.weights[key + ".weight"]
|
|
192
194
|
bias = self.weights.get(key + ".bias")
|
|
193
195
|
if x.dtype == self.mx.float16:
|
|
@@ -313,6 +315,7 @@ class SourceLayers:
|
|
|
313
315
|
return silu(x, self.mx)
|
|
314
316
|
|
|
315
317
|
def _native_norm(self, x: Array, key: str, *, groups: int, eps: float, silu: bool) -> Array:
|
|
318
|
+
"""Apply the native group-normalization kernel with optional SiLU."""
|
|
316
319
|
return native_group_norm(
|
|
317
320
|
x,
|
|
318
321
|
self.weights[key + ".weight"],
|
|
@@ -374,6 +377,7 @@ class LdmUnet(SourceLayers):
|
|
|
374
377
|
fused_attention: bool = False,
|
|
375
378
|
native_kernels: bool | None = None,
|
|
376
379
|
):
|
|
380
|
+
"""Initialize the source UNet and its attention and arithmetic policies."""
|
|
377
381
|
super().__init__(
|
|
378
382
|
weights, mx, source_arithmetic=source_arithmetic, native_kernels=native_kernels
|
|
379
383
|
)
|
|
@@ -383,10 +387,12 @@ class LdmUnet(SourceLayers):
|
|
|
383
387
|
def silu(self, x: Array) -> Array:
|
|
384
388
|
# Source nn.SiLU rounds once after the activation. VQ's separate
|
|
385
389
|
# sigmoid/product deliberately retains its two half rounding steps.
|
|
390
|
+
"""Apply SiLU with FP32 intermediate rounding and restore the input dtype."""
|
|
386
391
|
return silu(x.astype(self.mx.float32), self.mx).astype(x.dtype)
|
|
387
392
|
|
|
388
393
|
def softmax(self, x: Array) -> Array:
|
|
389
394
|
# Torch softmax accumulates in float32, then returns the input dtype.
|
|
395
|
+
"""Apply source-compatible precise softmax on the final axis."""
|
|
390
396
|
return self.mx.softmax(x, axis=-1, precise=True)
|
|
391
397
|
|
|
392
398
|
def residual(self, x: Array, emb: Array, key: str) -> Array:
|
|
@@ -432,6 +438,7 @@ class LdmUnet(SourceLayers):
|
|
|
432
438
|
return self.dense(result, key + ".to_out.0")
|
|
433
439
|
|
|
434
440
|
def split(tensor: Array) -> Array:
|
|
441
|
+
"""Split projected tokens into attention heads."""
|
|
435
442
|
return mx.transpose(
|
|
436
443
|
mx.reshape(tensor, (tensor.shape[0], tensor.shape[1], heads, depth)), (0, 2, 1, 3)
|
|
437
444
|
)
|
|
@@ -548,6 +555,7 @@ class VqAutoencoder(SourceLayers):
|
|
|
548
555
|
"""
|
|
549
556
|
|
|
550
557
|
def residual(self, x: Array, key: str) -> Array:
|
|
558
|
+
"""Apply one VQ autoencoder residual block."""
|
|
551
559
|
h = self.norm_silu_conv(x, key + ".norm1", key + ".conv1", groups=16, eps=1e-6)
|
|
552
560
|
if key + ".nin_shortcut.weight" in self.weights:
|
|
553
561
|
x = self.conv(x, key + ".nin_shortcut", padding=0)
|
|
@@ -653,6 +661,7 @@ class BiomedTextEncoder(SourceLayers):
|
|
|
653
661
|
)
|
|
654
662
|
|
|
655
663
|
def split(tensor: Array) -> Array:
|
|
664
|
+
"""Split text-attention projections into heads."""
|
|
656
665
|
return mx.transpose(
|
|
657
666
|
mx.reshape(tensor, (tensor.shape[0], tensor.shape[1], 12, 64)), (0, 2, 1, 3)
|
|
658
667
|
)
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
"""The NIfTI surface used here, verified against NiBabel's image/header APIs."""
|
|
2
|
-
|
|
3
|
-
from os import PathLike
|
|
4
|
-
from typing import Literal, Protocol, cast
|
|
5
|
-
|
|
6
|
-
import nibabel
|
|
7
|
-
import numpy as np
|
|
8
|
-
from nibabel.filebasedimages import ImageFileError as ImageFileError
|
|
9
|
-
|
|
10
|
-
from lesiondiffusion_mlx._arrays import HostArray
|
|
11
|
-
|
|
12
|
-
type PathArg = str | PathLike[str]
|
|
13
|
-
type SpatialAffine = np.ndarray[tuple[int, ...], np.dtype[np.float64]]
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class ArrayBuffer(Protocol):
|
|
17
|
-
def __array__(
|
|
18
|
-
self, dtype: np.dtype[np.generic] | None = None, copy: bool | None = None
|
|
19
|
-
) -> HostArray: ...
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class NiftiHeader(Protocol):
|
|
23
|
-
def get_zooms(self) -> tuple[float, ...]: ...
|
|
24
|
-
def get_xyzt_units(self) -> tuple[str, str]: ...
|
|
25
|
-
def set_xyzt_units(self, xyz: str, t: str) -> None: ...
|
|
26
|
-
def set_data_dtype(self, dtype: type[np.generic]) -> None: ...
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class HeaderFactory(Protocol):
|
|
30
|
-
def from_header(self, header: NiftiHeader) -> NiftiHeader: ...
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
class NiftiImage(Protocol):
|
|
34
|
-
shape: tuple[int, ...]
|
|
35
|
-
affine: SpatialAffine
|
|
36
|
-
dataobj: ArrayBuffer | HostArray
|
|
37
|
-
header: NiftiHeader
|
|
38
|
-
|
|
39
|
-
def get_qform(self, *, coded: Literal[True]) -> tuple[SpatialAffine | None, int]: ...
|
|
40
|
-
def get_sform(self, *, coded: Literal[True]) -> tuple[SpatialAffine | None, int]: ...
|
|
41
|
-
def set_qform(self, affine: HostArray, code: int) -> None: ...
|
|
42
|
-
def set_sform(self, affine: HostArray, code: int) -> None: ...
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
class ImageFactory(Protocol):
|
|
46
|
-
def __call__(
|
|
47
|
-
self, dataobj: HostArray, affine: HostArray, header: NiftiHeader | None = None
|
|
48
|
-
) -> NiftiImage: ...
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
class OrientationFunctions(Protocol):
|
|
52
|
-
"""The subset of ``nibabel.orientations`` used to resample to RAS 1 mm."""
|
|
53
|
-
|
|
54
|
-
def axcodes2ornt(self, axcodes: tuple[str, str, str]) -> HostArray: ...
|
|
55
|
-
def ornt_transform(self, start_ornt: HostArray, end_ornt: HostArray) -> HostArray: ...
|
|
56
|
-
def apply_orientation(self, data: HostArray, ornt: HostArray) -> HostArray: ...
|
|
57
|
-
def inv_ornt_aff(self, ornt: HostArray, shape: tuple[int, ...]) -> SpatialAffine: ...
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
class Nibabel(Protocol):
|
|
61
|
-
Nifti1Header: HeaderFactory
|
|
62
|
-
Nifti1Image: ImageFactory
|
|
63
|
-
orientations: OrientationFunctions
|
|
64
|
-
|
|
65
|
-
def load(self, filename: PathArg) -> NiftiImage: ...
|
|
66
|
-
def save(self, image: NiftiImage, filename: PathArg) -> None: ...
|
|
67
|
-
def aff2axcodes(self, affine: HostArray) -> tuple[str | None, ...]: ...
|
|
68
|
-
def io_orientation(self, affine: HostArray) -> HostArray: ...
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
# This adapter is used only for .nii/.nii.gz inputs and NIfTI output; the wider
|
|
72
|
-
# NiBabel loader supports unrelated image classes outside the runner contract.
|
|
73
|
-
nib = cast(Nibabel, nibabel)
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
"""Native array and dependency contracts used by the source implementation."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from typing import TYPE_CHECKING, Literal, Protocol
|
|
6
|
-
|
|
7
|
-
from tokenizers import Encoding
|
|
8
|
-
|
|
9
|
-
from lesiondiffusion_mlx._arrays import HostArray as HostArray
|
|
10
|
-
|
|
11
|
-
if TYPE_CHECKING:
|
|
12
|
-
from collections.abc import Callable, Sequence
|
|
13
|
-
|
|
14
|
-
import mlx.core as mx
|
|
15
|
-
from medmlx_core.typing import FastRuntime as CoreFastRuntime
|
|
16
|
-
from medmlx_core.typing import MlxRuntime
|
|
17
|
-
|
|
18
|
-
type Array = mx.array
|
|
19
|
-
type Dtype = mx.Dtype
|
|
20
|
-
type ArrayValue = bool | int | float | complex | list[ArrayValue] | tuple[ArrayValue, ...]
|
|
21
|
-
|
|
22
|
-
class ArrayConstructor(Protocol):
|
|
23
|
-
def __call__(
|
|
24
|
-
self,
|
|
25
|
-
value: HostArray | Array | ArrayValue,
|
|
26
|
-
/,
|
|
27
|
-
dtype: Dtype | None = None,
|
|
28
|
-
) -> Array: ...
|
|
29
|
-
|
|
30
|
-
class TokenizerContract(Protocol):
|
|
31
|
-
def enable_truncation(self, max_length: int) -> None: ...
|
|
32
|
-
def enable_padding(self, *, length: int, pad_id: int, pad_token: str) -> None: ...
|
|
33
|
-
def encode_batch(self, inputs: Sequence[str]) -> list[Encoding]: ...
|
|
34
|
-
|
|
35
|
-
class MetalKernel(Protocol):
|
|
36
|
-
def __call__(
|
|
37
|
-
self,
|
|
38
|
-
*,
|
|
39
|
-
inputs: Sequence[Array],
|
|
40
|
-
template: Sequence[tuple[str, int | bool | Dtype]],
|
|
41
|
-
grid: tuple[int, int, int],
|
|
42
|
-
threadgroup: tuple[int, int, int],
|
|
43
|
-
output_shapes: Sequence[Sequence[int]],
|
|
44
|
-
output_dtypes: Sequence[Dtype],
|
|
45
|
-
) -> list[Array]: ...
|
|
46
|
-
|
|
47
|
-
class Fast(CoreFastRuntime, Protocol):
|
|
48
|
-
def scaled_dot_product_attention(
|
|
49
|
-
self,
|
|
50
|
-
q: Array,
|
|
51
|
-
k: Array,
|
|
52
|
-
v: Array,
|
|
53
|
-
*,
|
|
54
|
-
scale: float,
|
|
55
|
-
mask: str | Array | None = None,
|
|
56
|
-
sinks: Array | None = None,
|
|
57
|
-
force_fused: bool = False,
|
|
58
|
-
) -> Array: ...
|
|
59
|
-
|
|
60
|
-
def metal_kernel(
|
|
61
|
-
self,
|
|
62
|
-
name: str,
|
|
63
|
-
input_names: Sequence[str],
|
|
64
|
-
output_names: Sequence[str],
|
|
65
|
-
source: str,
|
|
66
|
-
header: str = "",
|
|
67
|
-
ensure_row_contiguous: bool = True,
|
|
68
|
-
atomic_outputs: bool = False,
|
|
69
|
-
compile_options: object | None = None,
|
|
70
|
-
) -> MetalKernel: ...
|
|
71
|
-
|
|
72
|
-
class Random(Protocol):
|
|
73
|
-
def seed(self, seed: int) -> None: ...
|
|
74
|
-
def normal(self, shape: Sequence[int]) -> Array: ...
|
|
75
|
-
|
|
76
|
-
class Mlx(MlxRuntime, Protocol):
|
|
77
|
-
"""The MLX module surface consumed by this port, without importing it eagerly."""
|
|
78
|
-
|
|
79
|
-
@property
|
|
80
|
-
def fast(self) -> Fast: ...
|
|
81
|
-
|
|
82
|
-
random: Random
|
|
83
|
-
|
|
84
|
-
def addmm(self, c: Array, a: Array, b: Array, /) -> Array: ...
|
|
85
|
-
def all(self, a: Array, /) -> Array: ...
|
|
86
|
-
def argmin(self, a: Array, /, *, axis: int | None = None) -> Array: ...
|
|
87
|
-
def clear_cache(self) -> None: ...
|
|
88
|
-
def compile[**P, R](self, fun: Callable[P, R], /) -> Callable[P, R]: ...
|
|
89
|
-
def contiguous(self, a: Array, /) -> Array: ...
|
|
90
|
-
def cos(self, a: Array, /) -> Array: ...
|
|
91
|
-
def device_info(self) -> dict[str, str | int]: ...
|
|
92
|
-
def erf(self, a: Array, /) -> Array: ...
|
|
93
|
-
def exp(self, a: Array, /) -> Array: ...
|
|
94
|
-
def full(self, shape: Sequence[int], vals: object, *, dtype: Dtype) -> Array: ...
|
|
95
|
-
def isfinite(self, a: Array, /) -> Array: ...
|
|
96
|
-
def load(self, file: str, *, format: Literal["safetensors"]) -> dict[str, Array]: ...
|
|
97
|
-
def reciprocal(self, a: Array, /) -> Array: ...
|
|
98
|
-
def sin(self, a: Array, /) -> Array: ...
|
|
99
|
-
def softmax(
|
|
100
|
-
self, a: Array, /, *, axis: int | None = None, precise: bool = False
|
|
101
|
-
) -> Array: ...
|
|
102
|
-
def sqrt(self, a: Array | float, /) -> Array: ...
|
|
103
|
-
def sum(self, a: Array, /, *, axis: int | None = None, keepdims: bool = False) -> Array: ...
|
|
104
|
-
else:
|
|
105
|
-
|
|
106
|
-
class ArrayConstructor(Protocol):
|
|
107
|
-
pass
|
|
108
|
-
|
|
109
|
-
class TokenizerContract(Protocol):
|
|
110
|
-
pass
|
|
111
|
-
|
|
112
|
-
class MetalKernel(Protocol):
|
|
113
|
-
pass
|
|
114
|
-
|
|
115
|
-
class Fast(Protocol):
|
|
116
|
-
pass
|
|
117
|
-
|
|
118
|
-
class Random(Protocol):
|
|
119
|
-
pass
|
|
120
|
-
|
|
121
|
-
class Mlx(Protocol):
|
|
122
|
-
pass
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/_attention.py
RENAMED
|
File without changes
|
|
File without changes
|
{lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/mlx_pipeline.py
RENAMED
|
File without changes
|
{lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/mlx_sampling.py
RENAMED
|
File without changes
|
|
File without changes
|
{lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/reference.json
RENAMED
|
File without changes
|