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.
Files changed (25) hide show
  1. {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/PKG-INFO +2 -2
  2. {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/README.md +1 -1
  3. {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/pyproject.toml +1 -1
  4. {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/__init__.py +2 -1
  5. {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/_group_norm.py +1 -0
  6. {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/_nax_conv.py +1 -0
  7. lesiondiffusion_mlx-0.2.4/src/lesiondiffusion_mlx/_nifti.py +111 -0
  8. {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/_reference.py +1 -0
  9. {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/_schedules.py +1 -0
  10. {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/_source_half.py +2 -0
  11. lesiondiffusion_mlx-0.2.4/src/lesiondiffusion_mlx/_typing.py +178 -0
  12. {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/medmlx.py +13 -0
  13. {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/mlx_model.py +9 -0
  14. lesiondiffusion_mlx-0.2.3/src/lesiondiffusion_mlx/_nifti.py +0 -73
  15. lesiondiffusion_mlx-0.2.3/src/lesiondiffusion_mlx/_typing.py +0 -122
  16. {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/.gitignore +0 -0
  17. {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/LICENSE +0 -0
  18. {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/THIRD_PARTY_NOTICES.md +0 -0
  19. {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/_arrays.py +0 -0
  20. {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/_attention.py +0 -0
  21. {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/_spatial.py +0 -0
  22. {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/mlx_pipeline.py +0 -0
  23. {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/mlx_sampling.py +0 -0
  24. {lesiondiffusion_mlx-0.2.3 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/py.typed +0 -0
  25. {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
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.3"
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.3"
18
+ uv pip install "lesiondiffusion-mlx>=0.2.4"
19
19
  ```
20
20
 
21
21
  ```python
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "lesiondiffusion-mlx"
7
- version = "0.2.3"
7
+ version = "0.2.4"
8
8
  description = "Native MLX implementation of LesionDiffusion"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13,<3.15"
@@ -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.3"
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 = (
@@ -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",
@@ -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)
@@ -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
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
 
6
6
  def _is_runtime_integer(value: object) -> bool:
7
+ """Recognize integer inputs while excluding booleans."""
7
8
  return isinstance(value, int) and not isinstance(value, bool)
8
9
 
9
10
 
@@ -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()
@@ -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