lesiondiffusion-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 lesiondiffusion-mlx might be problematic. Click here for more details.
- lesiondiffusion_mlx/__init__.py +66 -0
- lesiondiffusion_mlx/_arrays.py +24 -0
- lesiondiffusion_mlx/_nifti.py +73 -0
- lesiondiffusion_mlx/_schedules.py +38 -0
- lesiondiffusion_mlx/_source_half.py +541 -0
- lesiondiffusion_mlx/_spatial.py +57 -0
- lesiondiffusion_mlx/_typing.py +110 -0
- lesiondiffusion_mlx/medmlx.py +1764 -0
- lesiondiffusion_mlx/mlx_model.py +456 -0
- lesiondiffusion_mlx/mlx_pipeline.py +136 -0
- lesiondiffusion_mlx/mlx_sampling.py +132 -0
- lesiondiffusion_mlx/mlx_weights.py +216 -0
- lesiondiffusion_mlx/py.typed +0 -0
- lesiondiffusion_mlx/reference.json +141 -0
- lesiondiffusion_mlx-0.2.1.dist-info/METADATA +99 -0
- lesiondiffusion_mlx-0.2.1.dist-info/RECORD +20 -0
- lesiondiffusion_mlx-0.2.1.dist-info/WHEEL +4 -0
- lesiondiffusion_mlx-0.2.1.dist-info/entry_points.txt +2 -0
- lesiondiffusion_mlx-0.2.1.dist-info/licenses/LICENSE +202 -0
- lesiondiffusion_mlx-0.2.1.dist-info/licenses/THIRD_PARTY_NOTICES.md +147 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Native MLX implementation of LesionDiffusion, with lazy public imports."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib import import_module
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from lesiondiffusion_mlx._schedules import (
|
|
10
|
+
lesion_diffusion_timesteps as lesion_diffusion_timesteps,
|
|
11
|
+
)
|
|
12
|
+
from lesiondiffusion_mlx.mlx_model import (
|
|
13
|
+
LINET_CONFIG as LINET_CONFIG,
|
|
14
|
+
)
|
|
15
|
+
from lesiondiffusion_mlx.mlx_model import (
|
|
16
|
+
LMNET_CONFIG as LMNET_CONFIG,
|
|
17
|
+
)
|
|
18
|
+
from lesiondiffusion_mlx.mlx_model import (
|
|
19
|
+
BiomedTextEncoder as BiomedTextEncoder,
|
|
20
|
+
)
|
|
21
|
+
from lesiondiffusion_mlx.mlx_model import (
|
|
22
|
+
LdmUnet as LdmUnet,
|
|
23
|
+
)
|
|
24
|
+
from lesiondiffusion_mlx.mlx_model import (
|
|
25
|
+
SourceLayers as SourceLayers,
|
|
26
|
+
)
|
|
27
|
+
from lesiondiffusion_mlx.mlx_model import (
|
|
28
|
+
UnetConfig as UnetConfig,
|
|
29
|
+
)
|
|
30
|
+
from lesiondiffusion_mlx.mlx_model import (
|
|
31
|
+
VqAutoencoder as VqAutoencoder,
|
|
32
|
+
)
|
|
33
|
+
from lesiondiffusion_mlx.mlx_sampling import DdimSampler as DdimSampler
|
|
34
|
+
from lesiondiffusion_mlx.mlx_weights import write_conversion as write_conversion
|
|
35
|
+
|
|
36
|
+
__version__ = "0.2.1"
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"LINET_CONFIG",
|
|
40
|
+
"LMNET_CONFIG",
|
|
41
|
+
"BiomedTextEncoder",
|
|
42
|
+
"DdimSampler",
|
|
43
|
+
"LdmUnet",
|
|
44
|
+
"SourceLayers",
|
|
45
|
+
"UnetConfig",
|
|
46
|
+
"VqAutoencoder",
|
|
47
|
+
"lesion_diffusion_timesteps",
|
|
48
|
+
"write_conversion",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def __getattr__(name: str) -> object:
|
|
53
|
+
if name not in __all__:
|
|
54
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
55
|
+
module = (
|
|
56
|
+
"_schedules"
|
|
57
|
+
if name == "lesion_diffusion_timesteps"
|
|
58
|
+
else "mlx_weights"
|
|
59
|
+
if name == "write_conversion"
|
|
60
|
+
else "mlx_sampling"
|
|
61
|
+
if name == "DdimSampler"
|
|
62
|
+
else "mlx_model"
|
|
63
|
+
)
|
|
64
|
+
value = getattr(import_module(f"lesiondiffusion_mlx.{module}"), name)
|
|
65
|
+
globals()[name] = value
|
|
66
|
+
return value
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Host arrays shared by conversion, NIfTI I/O and reference tooling."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
type HostArray = np.ndarray[tuple[int, ...], np.dtype[np.generic]]
|
|
6
|
+
type FloatArray = np.ndarray[tuple[int, ...], np.dtype[np.float32]]
|
|
7
|
+
type BoolArray = np.ndarray[tuple[int, ...], np.dtype[np.bool_]]
|
|
8
|
+
type NumericScalar = (
|
|
9
|
+
np.bool_
|
|
10
|
+
| np.int8
|
|
11
|
+
| np.int16
|
|
12
|
+
| np.int32
|
|
13
|
+
| np.int64
|
|
14
|
+
| np.uint8
|
|
15
|
+
| np.uint16
|
|
16
|
+
| np.uint32
|
|
17
|
+
| np.uint64
|
|
18
|
+
| np.float16
|
|
19
|
+
| np.float32
|
|
20
|
+
| np.float64
|
|
21
|
+
| np.complex64
|
|
22
|
+
| np.complex128
|
|
23
|
+
)
|
|
24
|
+
type NumericArray = np.ndarray[tuple[int, ...], np.dtype[NumericScalar]]
|
|
@@ -0,0 +1,73 @@
|
|
|
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, ...]) -> HostArray: ...
|
|
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)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Pinned upstream uniform DDIM timestep schedule, with explicit invalid-index errors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _is_runtime_integer(value: object) -> bool:
|
|
7
|
+
return isinstance(value, int) and not isinstance(value, bool)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def lesion_diffusion_timesteps(steps: int, total: int = 1000) -> tuple[int, ...]:
|
|
11
|
+
"""Return the pinned upstream uniform DDIM timestep indices, validated.
|
|
12
|
+
|
|
13
|
+
The indices are ``value + 1`` for ``value`` in ``range(0, total, total // steps)``,
|
|
14
|
+
in ascending order; the sampler walks them in reverse. The tuple has at least
|
|
15
|
+
``steps`` entries and has more when ``steps`` does not divide ``total`` (7 entries
|
|
16
|
+
for ``steps=6``), so callers report ``len(...)`` as the actual step count. An
|
|
17
|
+
out-of-range index is rejected rather than clipped, to keep the schedule upstream's.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
steps: Requested number of DDIM steps, a positive integer (not a bool).
|
|
21
|
+
total: Length of the training noise schedule (``alphas_cumprod``).
|
|
22
|
+
|
|
23
|
+
Raises:
|
|
24
|
+
ValueError: If ``steps`` or ``total`` is not an integer, ``steps < 1``,
|
|
25
|
+
``total < steps``, or the last index would reach ``total`` (for example
|
|
26
|
+
``steps=3`` or ``steps=1000`` with ``total=1000``).
|
|
27
|
+
"""
|
|
28
|
+
if not _is_runtime_integer(steps) or steps < 1:
|
|
29
|
+
raise ValueError("LesionDiffusion DDIM steps must be a positive integer")
|
|
30
|
+
if not _is_runtime_integer(total) or total < steps:
|
|
31
|
+
raise ValueError("LesionDiffusion DDIM schedule must contain at least as many timesteps")
|
|
32
|
+
timesteps = tuple(value + 1 for value in range(0, total, total // steps))
|
|
33
|
+
if timesteps[-1] >= total:
|
|
34
|
+
raise ValueError(
|
|
35
|
+
f"LesionDiffusion DDIM steps {steps} produce out-of-bounds indices "
|
|
36
|
+
f"for a {total}-timestep schedule; choose another diffusion_steps value"
|
|
37
|
+
)
|
|
38
|
+
return timesteps
|