mlarray 0.0.47__tar.gz → 0.0.49__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 (46) hide show
  1. {mlarray-0.0.47 → mlarray-0.0.49}/PKG-INFO +1 -1
  2. mlarray-0.0.49/examples/example_compressed_vs_uncompressed.py +42 -0
  3. {mlarray-0.0.47 → mlarray-0.0.49}/mlarray/meta.py +66 -1
  4. {mlarray-0.0.47 → mlarray-0.0.49}/mlarray/mlarray.py +303 -73
  5. {mlarray-0.0.47 → mlarray-0.0.49}/mlarray.egg-info/PKG-INFO +1 -1
  6. {mlarray-0.0.47 → mlarray-0.0.49}/mlarray.egg-info/SOURCES.txt +1 -0
  7. {mlarray-0.0.47 → mlarray-0.0.49}/tests/test_asarray.py +16 -0
  8. {mlarray-0.0.47 → mlarray-0.0.49}/tests/test_usage.py +48 -0
  9. {mlarray-0.0.47 → mlarray-0.0.49}/.github/workflows/workflow.yml +0 -0
  10. {mlarray-0.0.47 → mlarray-0.0.49}/.gitignore +0 -0
  11. {mlarray-0.0.47 → mlarray-0.0.49}/LICENSE +0 -0
  12. {mlarray-0.0.47 → mlarray-0.0.49}/MANIFEST.in +0 -0
  13. {mlarray-0.0.47 → mlarray-0.0.49}/README.md +0 -0
  14. {mlarray-0.0.47 → mlarray-0.0.49}/assets/banner.png +0 -0
  15. {mlarray-0.0.47 → mlarray-0.0.49}/assets/banner.png~ +0 -0
  16. {mlarray-0.0.47 → mlarray-0.0.49}/docs/api.md +0 -0
  17. {mlarray-0.0.47 → mlarray-0.0.49}/docs/cli.md +0 -0
  18. {mlarray-0.0.47 → mlarray-0.0.49}/docs/index.md +0 -0
  19. {mlarray-0.0.47 → mlarray-0.0.49}/docs/optimization.md +0 -0
  20. {mlarray-0.0.47 → mlarray-0.0.49}/docs/schema.md +0 -0
  21. {mlarray-0.0.47 → mlarray-0.0.49}/docs/usage.md +0 -0
  22. {mlarray-0.0.47 → mlarray-0.0.49}/docs/why.md +0 -0
  23. {mlarray-0.0.47 → mlarray-0.0.49}/examples/example_asarray.py +0 -0
  24. {mlarray-0.0.47 → mlarray-0.0.49}/examples/example_bboxes_only.py +0 -0
  25. {mlarray-0.0.47 → mlarray-0.0.49}/examples/example_channel.py +0 -0
  26. {mlarray-0.0.47 → mlarray-0.0.49}/examples/example_in_memory_constructors.py +0 -0
  27. {mlarray-0.0.47 → mlarray-0.0.49}/examples/example_metadata_only.py +0 -0
  28. {mlarray-0.0.47 → mlarray-0.0.49}/examples/example_non_spatial.py +0 -0
  29. {mlarray-0.0.47 → mlarray-0.0.49}/examples/example_open.py +0 -0
  30. {mlarray-0.0.47 → mlarray-0.0.49}/examples/example_save_load.py +0 -0
  31. {mlarray-0.0.47 → mlarray-0.0.49}/mkdocs.yml +0 -0
  32. {mlarray-0.0.47 → mlarray-0.0.49}/mlarray/__init__.py +0 -0
  33. {mlarray-0.0.47 → mlarray-0.0.49}/mlarray/cli.py +0 -0
  34. {mlarray-0.0.47 → mlarray-0.0.49}/mlarray/utils.py +0 -0
  35. {mlarray-0.0.47 → mlarray-0.0.49}/mlarray.egg-info/dependency_links.txt +0 -0
  36. {mlarray-0.0.47 → mlarray-0.0.49}/mlarray.egg-info/entry_points.txt +0 -0
  37. {mlarray-0.0.47 → mlarray-0.0.49}/mlarray.egg-info/requires.txt +0 -0
  38. {mlarray-0.0.47 → mlarray-0.0.49}/mlarray.egg-info/top_level.txt +0 -0
  39. {mlarray-0.0.47 → mlarray-0.0.49}/pyproject.toml +0 -0
  40. {mlarray-0.0.47 → mlarray-0.0.49}/setup.cfg +0 -0
  41. {mlarray-0.0.47 → mlarray-0.0.49}/tests/test_bboxes.py +0 -0
  42. {mlarray-0.0.47 → mlarray-0.0.49}/tests/test_constructors.py +0 -0
  43. {mlarray-0.0.47 → mlarray-0.0.49}/tests/test_create.py +0 -0
  44. {mlarray-0.0.47 → mlarray-0.0.49}/tests/test_metadata.py +0 -0
  45. {mlarray-0.0.47 → mlarray-0.0.49}/tests/test_open.py +0 -0
  46. {mlarray-0.0.47 → mlarray-0.0.49}/tests/test_optimization.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mlarray
3
- Version: 0.0.47
3
+ Version: 0.0.49
4
4
  Summary: Array format specialized for Machine Learning with Blosc2 backend and standardized metadata.
5
5
  Author-email: Karol Gotkowski <karol.gotkowski@dkfz.de>
6
6
  License: MIT
@@ -0,0 +1,42 @@
1
+ from pathlib import Path
2
+
3
+ import numpy as np
4
+
5
+ from mlarray import MLArray
6
+
7
+
8
+ def main():
9
+ array = np.arange(2 * 4 * 4, dtype=np.float32).reshape(2, 4, 4)
10
+
11
+ compressed_img = MLArray.asarray(
12
+ array,
13
+ compressed=True,
14
+ patch_size=None,
15
+ chunk_size=(1, 4, 4),
16
+ block_size=(1, 2, 2),
17
+ )
18
+ uncompressed_img = MLArray.asarray(array, compressed=False)
19
+
20
+ compressed_path = Path("example_compressed_output.mla")
21
+ uncompressed_path = Path("example_uncompressed_output.mla")
22
+
23
+ print("compressed in-memory backend (before save):", type(compressed_img._store))
24
+ print(
25
+ "uncompressed in-memory backend (before save):",
26
+ type(uncompressed_img._store),
27
+ )
28
+
29
+ compressed_img.save(compressed_path)
30
+ uncompressed_img.save(uncompressed_path)
31
+
32
+ print("compressed in-memory backend (after save):", type(compressed_img._store))
33
+ print(
34
+ "uncompressed in-memory backend (after save):",
35
+ type(uncompressed_img._store),
36
+ )
37
+ print("saved compressed file:", compressed_path)
38
+ print("saved uncompressed->compressed file:", uncompressed_path)
39
+
40
+
41
+ if __name__ == "__main__":
42
+ main()
@@ -393,6 +393,44 @@ def _cast_to_list(value: Any, label: str):
393
393
  return out
394
394
 
395
395
 
396
+ def _to_jsonable(value: Any) -> Any:
397
+ """Recursively convert values to JSON-serializable plain Python objects."""
398
+ if isinstance(value, Enum):
399
+ return value.value
400
+
401
+ if isinstance(value, Mapping):
402
+ return {str(k): _to_jsonable(v) for k, v in value.items()}
403
+
404
+ if isinstance(value, (list, tuple)):
405
+ return [_to_jsonable(v) for v in value]
406
+
407
+ if isinstance(value, np.generic):
408
+ return value.item()
409
+
410
+ return value
411
+
412
+
413
+ def _cast_to_jsonable_mapping(value: Any, label: str) -> dict[str, Any]:
414
+ """Cast a value to a JSON-serializable mapping.
415
+
416
+ Accepts mappings directly or objects exposing ``__dict__`` (for example
417
+ Blosc2 ``CParams`` / ``DParams`` objects).
418
+ """
419
+ if isinstance(value, Mapping):
420
+ out = dict(value)
421
+ elif hasattr(value, "__dict__"):
422
+ out = dict(vars(value))
423
+ else:
424
+ raise TypeError(f"{label} must be a mapping or object with __dict__")
425
+
426
+ out = _to_jsonable(out)
427
+ if not isinstance(out, dict):
428
+ raise TypeError(f"{label} could not be converted to a mapping")
429
+ if not is_serializable(out):
430
+ raise TypeError(f"{label} is not JSON-serializable")
431
+ return out
432
+
433
+
396
434
  def _validate_int(value: Any, label: str) -> None:
397
435
  """Validate that value is an int.
398
436
 
@@ -565,10 +603,14 @@ class MetaBlosc2(BaseMeta):
565
603
  chunk_size: List of per-dimension chunk sizes. Length must match ndims.
566
604
  block_size: List of per-dimension block sizes. Length must match ndims.
567
605
  patch_size: List of per-dimension patch sizes. Length must match spatial ndims.
606
+ cparams: Blosc2 compression parameters as a JSON-serializable dict.
607
+ dparams: Blosc2 decompression parameters as a JSON-serializable dict.
568
608
  """
569
609
  chunk_size: Optional[list] = None
570
610
  block_size: Optional[list] = None
571
611
  patch_size: Optional[list] = None
612
+ cparams: Optional[dict[str, Any]] = None
613
+ dparams: Optional[dict[str, Any]] = None
572
614
 
573
615
  def _validate_and_cast(self, *, ndims: Optional[int] = None, spatial_ndims: Optional[int] = None, **_: Any) -> None:
574
616
  """Validate and normalize tiling sizes.
@@ -591,6 +633,12 @@ class MetaBlosc2(BaseMeta):
591
633
  self.patch_size = _cast_to_list(self.patch_size, "meta.blosc2.patch_size")
592
634
  _validate_float_int_list(self.patch_size, "meta.blosc2.patch_size", spatial_ndims)
593
635
 
636
+ if self.cparams is not None:
637
+ self.cparams = _cast_to_jsonable_mapping(self.cparams, "meta.blosc2.cparams")
638
+
639
+ if self.dparams is not None:
640
+ self.dparams = _cast_to_jsonable_mapping(self.dparams, "meta.blosc2.dparams")
641
+
594
642
 
595
643
  class AxisLabelEnum(str, Enum):
596
644
  """Axis label/role identifiers used for spatial metadata.
@@ -628,6 +676,7 @@ class MetaSpatial(BaseMeta):
628
676
  spacing: Per-dimension spacing values. Length must match ndims.
629
677
  origin: Per-dimension origin values. Length must match ndims.
630
678
  direction: Direction cosine matrix of shape [ndims, ndims].
679
+ affine: Homogeneous affine matrix of shape [ndims + 1, ndims + 1].
631
680
  shape: Array shape. Length must match (spatial + non-spatial) ndims.
632
681
  axis_labels: Per-axis labels or roles. Length must match ndims.
633
682
  axis_units: Per-axis units. Length must match ndims.
@@ -638,6 +687,7 @@ class MetaSpatial(BaseMeta):
638
687
  spacing: Optional[list[Union[int,float]]] = None
639
688
  origin: Optional[list[Union[int,float]]] = None
640
689
  direction: Optional[list[list[Union[int,float]]]] = None
690
+ affine: Optional[list[list[Union[int,float]]]] = None
641
691
  shape: Optional[list[int]] = None
642
692
  axis_labels: Optional[list[Union[str,AxisLabel]]] = None
643
693
  axis_units: Optional[list[str]] = None
@@ -668,6 +718,21 @@ class MetaSpatial(BaseMeta):
668
718
  self.direction = _cast_to_list(self.direction, "meta.spatial.direction")
669
719
  _validate_float_int_matrix(self.direction, "meta.spatial.direction", spatial_ndims)
670
720
 
721
+ if self.affine is not None:
722
+ self.affine = _cast_to_list(self.affine, "meta.spatial.affine")
723
+ if spatial_ndims is not None:
724
+ _validate_float_int_matrix(
725
+ self.affine,
726
+ "meta.spatial.affine",
727
+ spatial_ndims + 1,
728
+ )
729
+ else:
730
+ _validate_float_int_matrix(self.affine, "meta.spatial.affine")
731
+ n_rows = len(self.affine)
732
+ for row in self.affine:
733
+ if len(row) != n_rows:
734
+ raise ValueError("meta.spatial.affine must be a square matrix")
735
+
671
736
  if self.shape is not None:
672
737
  self.shape = _cast_to_list(self.shape, "meta.spatial.shape")
673
738
  _validate_float_int_list(self.shape, "meta.spatial.shape", ndims)
@@ -867,7 +932,7 @@ class Meta(BaseMeta):
867
932
  Attributes:
868
933
  source: Source metadata from the original image source (JSON-serializable dict).
869
934
  extra: Additional metadata (JSON-serializable dict).
870
- spatial: Spatial metadata (spacing, origin, direction, shape).
935
+ spatial: Spatial metadata (spacing, origin, direction, affine, shape).
871
936
  stats: Summary statistics.
872
937
  bbox: Bounding boxes.
873
938
  is_seg: Segmentation flag.
@@ -2,11 +2,13 @@ from copy import deepcopy
2
2
  import numpy as np
3
3
  import blosc2
4
4
  import math
5
- from typing import Dict, Optional, Union, List, Tuple
5
+ from typing import Any, Dict, Optional, Union, List, Tuple
6
6
  from pathlib import Path
7
7
  import os
8
8
  from mlarray.meta import Meta, MetaBlosc2, AxisLabel, _spatial_axis_mask
9
9
  from mlarray.utils import is_serializable
10
+ import pickle
11
+ import gzip
10
12
 
11
13
  MLARRAY_SUFFIX = "mla"
12
14
  MLARRAY_VERSION = "v0"
@@ -20,6 +22,7 @@ class MLArray:
20
22
  spacing: Optional[Union[List, Tuple, np.ndarray]] = None,
21
23
  origin: Optional[Union[List, Tuple, np.ndarray]] = None,
22
24
  direction: Optional[Union[List, Tuple, np.ndarray]] = None,
25
+ affine: Optional[Union[List, Tuple, np.ndarray]] = None,
23
26
  meta: Optional[Union[Dict, Meta]] = None,
24
27
  axis_labels: Optional[List[Union[str, AxisLabel]]] = None,
25
28
  copy: Optional['MLArray'] = None,
@@ -28,6 +31,7 @@ class MLArray:
28
31
  block_size: Optional[Union[int, List, Tuple]] = None,
29
32
  cparams: Optional[Union[Dict, blosc2.CParams]] = None,
30
33
  dparams: Optional[Union[Dict, blosc2.DParams]] = None,
34
+ compressed: bool = True,
31
35
  ) -> None:
32
36
  """Initializes a MLArray instance.
33
37
 
@@ -48,6 +52,9 @@ class MLArray:
48
52
  direction (Optional[Union[List, Tuple, np.ndarray]]): Direction
49
53
  cosine matrix. Provide a 2D list/tuple/ndarray with shape
50
54
  (ndims, ndims) for spatial dimensions.
55
+ affine (Optional[Union[List, Tuple, np.ndarray]]): Homogeneous
56
+ affine matrix. Provide a 2D list/tuple/ndarray with shape
57
+ (spatial_ndims + 1, spatial_ndims + 1).
51
58
  meta (Optional[Dict | Meta]): Free-form metadata dictionary or Meta
52
59
  instance. Must be JSON-serializable when saving.
53
60
  If meta is passed as a Dict, it is internally converted into a
@@ -75,10 +82,12 @@ class MLArray:
75
82
  self.mmap_mode = None
76
83
  self.meta = None
77
84
  self._store = None
85
+ self._backend = None
78
86
  if isinstance(array, (str, Path)) and (
79
87
  spacing is not None
80
88
  or origin is not None
81
89
  or direction is not None
90
+ or affine is not None
82
91
  or meta is not None
83
92
  or axis_labels is not None
84
93
  or copy is not None
@@ -89,14 +98,23 @@ class MLArray:
89
98
  or dparams is not None
90
99
  ):
91
100
  raise RuntimeError(
92
- "Spacing, origin, direction, meta, axis_labels, copy, patch_size, "
101
+ "Spacing, origin, direction, affine, meta, axis_labels, copy, patch_size, "
93
102
  "chunk_size, block_size, cparams or dparams cannot be set when "
94
103
  "array is a filepath."
95
104
  )
96
105
  if isinstance(array, (str, Path)):
97
- self._load(array)
106
+ self._load(array, compressed=compressed)
98
107
  else:
99
- self._validate_and_add_meta(meta, spacing, origin, direction, axis_labels, False, validate=False)
108
+ self._validate_and_add_meta(
109
+ meta,
110
+ spacing=spacing,
111
+ origin=origin,
112
+ direction=direction,
113
+ affine=affine,
114
+ axis_labels=axis_labels,
115
+ has_array=False,
116
+ validate=False,
117
+ )
100
118
  if array is not None:
101
119
  self._asarray(
102
120
  array,
@@ -106,10 +124,16 @@ class MLArray:
106
124
  block_size=block_size,
107
125
  cparams=cparams,
108
126
  dparams=dparams,
127
+ compressed=compressed,
109
128
  )
110
129
  has_array = True
111
130
  else:
112
- self._store = blosc2.empty((0,))
131
+ if compressed:
132
+ self._store = blosc2.empty((0,))
133
+ self._backend = "blosc2"
134
+ else:
135
+ self._store = np.empty((0,))
136
+ self._backend = "numpy"
113
137
  has_array = False
114
138
  if copy is not None:
115
139
  self.meta.copy_from(copy.meta)
@@ -224,6 +248,7 @@ class MLArray:
224
248
  cls,
225
249
  filepath: Union[str, Path],
226
250
  dparams: Optional[Union[Dict, blosc2.DParams]] = None,
251
+ compressed: bool = True,
227
252
  ):
228
253
  """Loads a MLArray file as a whole. Does not use memory-mapping. Both MLArray ('.mla') and Blosc2 ('.b2nd') files are supported.
229
254
 
@@ -243,7 +268,7 @@ class MLArray:
243
268
  RuntimeError: If the file extension is not ".b2nd" or ".mla".
244
269
  """
245
270
  class_instance = cls()
246
- class_instance._load(filepath, dparams)
271
+ class_instance._load(filepath, dparams, compressed=compressed)
247
272
  return class_instance
248
273
 
249
274
  @classmethod
@@ -257,6 +282,7 @@ class MLArray:
257
282
  block_size: Optional[Union[int, List, Tuple]] = None,
258
283
  cparams: Optional[Union[Dict, blosc2.CParams]] = None,
259
284
  dparams: Optional[Union[Dict, blosc2.DParams]] = None,
285
+ compressed: bool = True,
260
286
  ):
261
287
  """Create an in-memory MLArray with uninitialized values.
262
288
 
@@ -284,6 +310,7 @@ class MLArray:
284
310
  """
285
311
  class_instance = cls()
286
312
  class_instance._construct_in_memory(
313
+ constructor="empty",
287
314
  shape=shape,
288
315
  dtype=dtype,
289
316
  meta=meta,
@@ -292,7 +319,7 @@ class MLArray:
292
319
  block_size=block_size,
293
320
  cparams=cparams,
294
321
  dparams=dparams,
295
- store_builder=lambda **kwargs: blosc2.empty(**kwargs),
322
+ compressed=compressed,
296
323
  )
297
324
  return class_instance
298
325
 
@@ -307,6 +334,7 @@ class MLArray:
307
334
  block_size: Optional[Union[int, List, Tuple]] = None,
308
335
  cparams: Optional[Union[Dict, blosc2.CParams]] = None,
309
336
  dparams: Optional[Union[Dict, blosc2.DParams]] = None,
337
+ compressed: bool = True,
310
338
  ):
311
339
  """Create an in-memory MLArray filled with zeros.
312
340
 
@@ -332,6 +360,7 @@ class MLArray:
332
360
  """
333
361
  class_instance = cls()
334
362
  class_instance._construct_in_memory(
363
+ constructor="zeros",
335
364
  shape=shape,
336
365
  dtype=dtype,
337
366
  meta=meta,
@@ -340,7 +369,7 @@ class MLArray:
340
369
  block_size=block_size,
341
370
  cparams=cparams,
342
371
  dparams=dparams,
343
- store_builder=lambda **kwargs: blosc2.zeros(**kwargs),
372
+ compressed=compressed,
344
373
  )
345
374
  return class_instance
346
375
 
@@ -355,6 +384,7 @@ class MLArray:
355
384
  block_size: Optional[Union[int, List, Tuple]] = None,
356
385
  cparams: Optional[Union[Dict, blosc2.CParams]] = None,
357
386
  dparams: Optional[Union[Dict, blosc2.DParams]] = None,
387
+ compressed: bool = True,
358
388
  ):
359
389
  """Create an in-memory MLArray filled with ones.
360
390
 
@@ -382,6 +412,7 @@ class MLArray:
382
412
  dtype = blosc2.DEFAULT_FLOAT if dtype is None else dtype
383
413
  class_instance = cls()
384
414
  class_instance._construct_in_memory(
415
+ constructor="ones",
385
416
  shape=shape,
386
417
  dtype=dtype,
387
418
  meta=meta,
@@ -390,7 +421,7 @@ class MLArray:
390
421
  block_size=block_size,
391
422
  cparams=cparams,
392
423
  dparams=dparams,
393
- store_builder=lambda **kwargs: blosc2.ones(**kwargs),
424
+ compressed=compressed,
394
425
  )
395
426
  return class_instance
396
427
 
@@ -406,6 +437,7 @@ class MLArray:
406
437
  block_size: Optional[Union[int, List, Tuple]] = None,
407
438
  cparams: Optional[Union[Dict, blosc2.CParams]] = None,
408
439
  dparams: Optional[Union[Dict, blosc2.DParams]] = None,
440
+ compressed: bool = True,
409
441
  ):
410
442
  """Create an in-memory MLArray filled with ``fill_value``.
411
443
 
@@ -439,6 +471,7 @@ class MLArray:
439
471
  dtype = np.dtype(type(fill_value))
440
472
  class_instance = cls()
441
473
  class_instance._construct_in_memory(
474
+ constructor="full",
442
475
  shape=shape,
443
476
  dtype=dtype,
444
477
  meta=meta,
@@ -447,7 +480,8 @@ class MLArray:
447
480
  block_size=block_size,
448
481
  cparams=cparams,
449
482
  dparams=dparams,
450
- store_builder=lambda **kwargs: blosc2.full(fill_value=fill_value, **kwargs),
483
+ compressed=compressed,
484
+ constructor_kwargs={"fill_value": fill_value},
451
485
  )
452
486
  return class_instance
453
487
 
@@ -466,6 +500,7 @@ class MLArray:
466
500
  block_size: Optional[Union[int, List, Tuple]] = None,
467
501
  cparams: Optional[Union[Dict, blosc2.CParams]] = None,
468
502
  dparams: Optional[Union[Dict, blosc2.DParams]] = None,
503
+ compressed: bool = True,
469
504
  ):
470
505
  """Create an in-memory MLArray with evenly spaced values.
471
506
 
@@ -525,6 +560,7 @@ class MLArray:
525
560
 
526
561
  class_instance = cls()
527
562
  class_instance._construct_in_memory(
563
+ constructor="arange",
528
564
  shape=shape,
529
565
  dtype=dtype,
530
566
  meta=meta,
@@ -533,13 +569,13 @@ class MLArray:
533
569
  block_size=block_size,
534
570
  cparams=cparams,
535
571
  dparams=dparams,
536
- store_builder=lambda **kwargs: blosc2.arange(
537
- start=start,
538
- stop=stop,
539
- step=step,
540
- c_order=c_order,
541
- **kwargs,
542
- ),
572
+ compressed=compressed,
573
+ constructor_kwargs={
574
+ "start": start,
575
+ "stop": stop,
576
+ "step": step,
577
+ "c_order": c_order,
578
+ },
543
579
  )
544
580
  return class_instance
545
581
 
@@ -559,6 +595,7 @@ class MLArray:
559
595
  block_size: Optional[Union[int, List, Tuple]] = None,
560
596
  cparams: Optional[Union[Dict, blosc2.CParams]] = None,
561
597
  dparams: Optional[Union[Dict, blosc2.DParams]] = None,
598
+ compressed: bool = True,
562
599
  ):
563
600
  """Create an in-memory MLArray with evenly spaced samples.
564
601
 
@@ -617,6 +654,7 @@ class MLArray:
617
654
 
618
655
  class_instance = cls()
619
656
  class_instance._construct_in_memory(
657
+ constructor="linspace",
620
658
  shape=shape,
621
659
  dtype=dtype,
622
660
  meta=meta,
@@ -625,14 +663,14 @@ class MLArray:
625
663
  block_size=block_size,
626
664
  cparams=cparams,
627
665
  dparams=dparams,
628
- store_builder=lambda **kwargs: blosc2.linspace(
629
- start=start,
630
- stop=stop,
631
- num=num,
632
- endpoint=endpoint,
633
- c_order=c_order,
634
- **kwargs,
635
- ),
666
+ compressed=compressed,
667
+ constructor_kwargs={
668
+ "start": start,
669
+ "stop": stop,
670
+ "num": num,
671
+ "endpoint": endpoint,
672
+ "c_order": c_order,
673
+ },
636
674
  )
637
675
  return class_instance
638
676
 
@@ -645,7 +683,8 @@ class MLArray:
645
683
  chunk_size: Optional[Union[int, List, Tuple]]= None,
646
684
  block_size: Optional[Union[int, List, Tuple]] = None,
647
685
  cparams: Optional[Union[Dict, blosc2.CParams]] = None,
648
- dparams: Optional[Union[Dict, blosc2.DParams]] = None
686
+ dparams: Optional[Union[Dict, blosc2.DParams]] = None,
687
+ compressed: bool = True,
649
688
  ):
650
689
  """Convert a NumPy array into an in-memory Blosc2-backed MLArray.
651
690
 
@@ -688,7 +727,16 @@ class MLArray:
688
727
  implemented for the provided dimensionality.
689
728
  """
690
729
  class_instance = cls()
691
- class_instance._asarray(array, meta, patch_size, chunk_size, block_size, cparams, dparams)
730
+ class_instance._asarray(
731
+ array,
732
+ meta,
733
+ patch_size,
734
+ chunk_size,
735
+ block_size,
736
+ cparams,
737
+ dparams,
738
+ compressed=compressed,
739
+ )
692
740
  return class_instance
693
741
 
694
742
  @classmethod
@@ -702,6 +750,7 @@ class MLArray:
702
750
  block_size: Optional[Union[int, List, Tuple]] = None,
703
751
  cparams: Optional[Union[Dict, blosc2.CParams]] = None,
704
752
  dparams: Optional[Union[Dict, blosc2.DParams]] = None,
753
+ compressed: bool = True,
705
754
  ):
706
755
  """Create an in-memory MLArray with the same shape as ``x``.
707
756
 
@@ -730,6 +779,7 @@ class MLArray:
730
779
  class_instance = cls()
731
780
  shape, dtype, meta = class_instance._resolve_like_input(x, dtype, meta)
732
781
  class_instance._construct_in_memory(
782
+ constructor="empty",
733
783
  shape=shape,
734
784
  dtype=dtype,
735
785
  meta=meta,
@@ -738,7 +788,7 @@ class MLArray:
738
788
  block_size=block_size,
739
789
  cparams=cparams,
740
790
  dparams=dparams,
741
- store_builder=lambda **kwargs: blosc2.empty(**kwargs),
791
+ compressed=compressed,
742
792
  )
743
793
  return class_instance
744
794
 
@@ -753,6 +803,7 @@ class MLArray:
753
803
  block_size: Optional[Union[int, List, Tuple]] = None,
754
804
  cparams: Optional[Union[Dict, blosc2.CParams]] = None,
755
805
  dparams: Optional[Union[Dict, blosc2.DParams]] = None,
806
+ compressed: bool = True,
756
807
  ):
757
808
  """Create an in-memory MLArray of zeros with the same shape as ``x``.
758
809
 
@@ -781,6 +832,7 @@ class MLArray:
781
832
  class_instance = cls()
782
833
  shape, dtype, meta = class_instance._resolve_like_input(x, dtype, meta)
783
834
  class_instance._construct_in_memory(
835
+ constructor="zeros",
784
836
  shape=shape,
785
837
  dtype=dtype,
786
838
  meta=meta,
@@ -789,7 +841,7 @@ class MLArray:
789
841
  block_size=block_size,
790
842
  cparams=cparams,
791
843
  dparams=dparams,
792
- store_builder=lambda **kwargs: blosc2.zeros(**kwargs),
844
+ compressed=compressed,
793
845
  )
794
846
  return class_instance
795
847
 
@@ -804,6 +856,7 @@ class MLArray:
804
856
  block_size: Optional[Union[int, List, Tuple]] = None,
805
857
  cparams: Optional[Union[Dict, blosc2.CParams]] = None,
806
858
  dparams: Optional[Union[Dict, blosc2.DParams]] = None,
859
+ compressed: bool = True,
807
860
  ):
808
861
  """Create an in-memory MLArray of ones with the same shape as ``x``.
809
862
 
@@ -832,6 +885,7 @@ class MLArray:
832
885
  class_instance = cls()
833
886
  shape, dtype, meta = class_instance._resolve_like_input(x, dtype, meta)
834
887
  class_instance._construct_in_memory(
888
+ constructor="ones",
835
889
  shape=shape,
836
890
  dtype=dtype,
837
891
  meta=meta,
@@ -840,7 +894,7 @@ class MLArray:
840
894
  block_size=block_size,
841
895
  cparams=cparams,
842
896
  dparams=dparams,
843
- store_builder=lambda **kwargs: blosc2.ones(**kwargs),
897
+ compressed=compressed,
844
898
  )
845
899
  return class_instance
846
900
 
@@ -856,6 +910,7 @@ class MLArray:
856
910
  block_size: Optional[Union[int, List, Tuple]] = None,
857
911
  cparams: Optional[Union[Dict, blosc2.CParams]] = None,
858
912
  dparams: Optional[Union[Dict, blosc2.DParams]] = None,
913
+ compressed: bool = True,
859
914
  ):
860
915
  """Create an in-memory MLArray filled with ``fill_value`` and shape of ``x``.
861
916
 
@@ -886,6 +941,7 @@ class MLArray:
886
941
  class_instance = cls()
887
942
  shape, dtype, meta = class_instance._resolve_like_input(x, dtype, meta)
888
943
  class_instance._construct_in_memory(
944
+ constructor="full",
889
945
  shape=shape,
890
946
  dtype=dtype,
891
947
  meta=meta,
@@ -894,7 +950,8 @@ class MLArray:
894
950
  block_size=block_size,
895
951
  cparams=cparams,
896
952
  dparams=dparams,
897
- store_builder=lambda **kwargs: blosc2.full(fill_value=fill_value, **kwargs),
953
+ compressed=compressed,
954
+ constructor_kwargs={"fill_value": fill_value},
898
955
  )
899
956
  return class_instance
900
957
 
@@ -923,7 +980,8 @@ class MLArray:
923
980
 
924
981
  if Path(filepath).is_file():
925
982
  os.remove(str(filepath))
926
-
983
+
984
+ self._ensure_blosc2_store()
927
985
  self._write_metadata(force=True)
928
986
  self._store.save(str(filepath))
929
987
  self._update_blosc2_meta()
@@ -937,6 +995,7 @@ class MLArray:
937
995
  """
938
996
  self._write_metadata()
939
997
  self._store = None
998
+ self._backend = None
940
999
  self.filepath = None
941
1000
  self.support_metadata = None
942
1001
  self.mode = None
@@ -1069,6 +1128,8 @@ class MLArray:
1069
1128
  """
1070
1129
  if self._store is None or self.meta._has_array.has_array == False:
1071
1130
  return None
1131
+ if self.meta.spatial.affine is not None:
1132
+ return self.meta.spatial.affine
1072
1133
  spacing = np.array(self.spacing) if self.spacing is not None else np.ones(self.spatial_ndim)
1073
1134
  origin = np.array(self.origin) if self.origin is not None else np.zeros(self.spatial_ndim)
1074
1135
  direction = np.array(self.direction) if self.direction is not None else np.eye(self.spatial_ndim)
@@ -1360,6 +1421,7 @@ class MLArray:
1360
1421
  dparams = MLArray._resolve_dparams(dparams)
1361
1422
 
1362
1423
  self._store = blosc2.open(urlpath=str(filepath), dparams=dparams, mode=mode, mmap_mode=mmap_mode)
1424
+ self._backend = "blosc2"
1363
1425
  self._read_meta()
1364
1426
  self._update_blosc2_meta()
1365
1427
  self.mode = mode
@@ -1439,15 +1501,13 @@ class MLArray:
1439
1501
 
1440
1502
  self._validate_and_add_meta(meta, has_array=True)
1441
1503
  spatial_axis_mask = [True] * len(shape) if self.meta.spatial.axis_labels is None else _spatial_axis_mask(self.meta.spatial.axis_labels)
1442
- self.meta.blosc2 = self._comp_and_validate_blosc2_meta(self.meta.blosc2, patch_size, chunk_size, block_size, shape, np.dtype(dtype).itemsize, spatial_axis_mask)
1504
+ self.meta.blosc2 = self._comp_and_validate_blosc2_meta(self.meta.blosc2, patch_size, chunk_size, block_size, shape, np.dtype(dtype).itemsize, spatial_axis_mask, cparams, dparams)
1443
1505
  self.meta._has_array.has_array = True
1444
1506
 
1445
1507
  self.support_metadata = str(filepath).endswith(f".{MLARRAY_SUFFIX}")
1446
-
1447
- cparams = MLArray._resolve_cparams(cparams)
1448
- dparams = MLArray._resolve_dparams(dparams)
1449
1508
 
1450
- self._store = blosc2.empty(shape=shape, dtype=np.dtype(dtype), urlpath=str(filepath), chunks=self.meta.blosc2.chunk_size, blocks=self.meta.blosc2.block_size, cparams=cparams, dparams=dparams, mmap_mode=mmap_mode)
1509
+ self._store = blosc2.empty(shape=shape, dtype=np.dtype(dtype), urlpath=str(filepath), chunks=self.meta.blosc2.chunk_size, blocks=self.meta.blosc2.block_size, cparams=MLArray._resolve_cparams(self.meta.blosc2.cparams), dparams=MLArray._resolve_dparams(self.meta.blosc2.dparams), mmap_mode=mmap_mode)
1510
+ self._backend = "blosc2"
1451
1511
  self._update_blosc2_meta()
1452
1512
  self.mode = mode
1453
1513
  self.mmap_mode = mmap_mode
@@ -1458,6 +1518,7 @@ class MLArray:
1458
1518
  self,
1459
1519
  filepath: Union[str, Path],
1460
1520
  dparams: Optional[Union[Dict, blosc2.DParams]] = None,
1521
+ compressed: bool = True,
1461
1522
  ):
1462
1523
  """Internal MLArray load method. Loads a MLArray file. Both MLArray ('.mla') and Blosc2 ('.b2nd') files are supported.
1463
1524
 
@@ -1484,10 +1545,14 @@ class MLArray:
1484
1545
  ondisk = blosc2.open(str(filepath), dparams=dparams, mode="r")
1485
1546
  cframe = ondisk.to_cframe()
1486
1547
  self._store = blosc2.ndarray_from_cframe(cframe, copy=True)
1548
+ self._backend = "blosc2"
1487
1549
  self.mode = None
1488
1550
  self.mmap_mode = None
1489
1551
  self._read_meta()
1490
1552
  self._update_blosc2_meta()
1553
+ if not compressed:
1554
+ self._store = np.asarray(self._store[...])
1555
+ self._backend = "numpy"
1491
1556
 
1492
1557
  def _asarray(
1493
1558
  self,
@@ -1497,7 +1562,8 @@ class MLArray:
1497
1562
  chunk_size: Optional[Union[int, List, Tuple]]= None,
1498
1563
  block_size: Optional[Union[int, List, Tuple]] = None,
1499
1564
  cparams: Optional[Union[Dict, blosc2.CParams]] = None,
1500
- dparams: Optional[Union[Dict, blosc2.DParams]] = None
1565
+ dparams: Optional[Union[Dict, blosc2.DParams]] = None,
1566
+ compressed: bool = True,
1501
1567
  ):
1502
1568
  """Internal MLArray asarray method.
1503
1569
 
@@ -1539,25 +1605,20 @@ class MLArray:
1539
1605
  if not isinstance(array, np.ndarray):
1540
1606
  raise TypeError("array must be a numpy.ndarray")
1541
1607
  self._construct_in_memory(
1542
- source_array=array,
1608
+ constructor="asarray",
1609
+ source_array=np.ascontiguousarray(array[...]),
1543
1610
  meta=meta,
1544
1611
  patch_size=patch_size,
1545
1612
  chunk_size=chunk_size,
1546
1613
  block_size=block_size,
1547
1614
  cparams=cparams,
1548
1615
  dparams=dparams,
1549
- store_builder=lambda **kwargs: blosc2.asarray(
1550
- kwargs["source_array"],
1551
- chunks=kwargs["chunks"],
1552
- blocks=kwargs["blocks"],
1553
- cparams=kwargs["cparams"],
1554
- dparams=kwargs["dparams"],
1555
- ),
1616
+ compressed=compressed,
1556
1617
  )
1557
1618
 
1558
1619
  def _construct_in_memory(
1559
1620
  self,
1560
- store_builder,
1621
+ constructor: str,
1561
1622
  shape: Optional[Union[int, List, Tuple, np.ndarray]] = None,
1562
1623
  dtype: Optional[np.dtype] = None,
1563
1624
  source_array: Optional[np.ndarray] = None,
@@ -1567,6 +1628,8 @@ class MLArray:
1567
1628
  block_size: Optional[Union[int, List, Tuple]] = None,
1568
1629
  cparams: Optional[Union[Dict, blosc2.CParams]] = None,
1569
1630
  dparams: Optional[Union[Dict, blosc2.DParams]] = None,
1631
+ compressed: bool = True,
1632
+ constructor_kwargs: Optional[dict[str, Any]] = None,
1570
1633
  ):
1571
1634
  """Internal generic constructor for in-memory Blosc2-backed MLArrays.
1572
1635
 
@@ -1575,8 +1638,7 @@ class MLArray:
1575
1638
  array shape, required when ``source_array`` is None.
1576
1639
  dtype (Optional[np.dtype]): Target dtype, required when
1577
1640
  ``source_array`` is None.
1578
- store_builder (Callable): Callable receiving normalized kwargs and
1579
- returning a Blosc2 NDArray.
1641
+ constructor (str): Constructor operation name.
1580
1642
  source_array (Optional[np.ndarray]): Source array that should be
1581
1643
  converted into an in-memory Blosc2-backed store.
1582
1644
  meta (Optional[Union[Dict, Meta]]): Optional metadata attached to
@@ -1599,12 +1661,13 @@ class MLArray:
1599
1661
  Raises:
1600
1662
  ValueError: If constructor inputs are inconsistent.
1601
1663
  """
1664
+ constructor_kwargs = {} if constructor_kwargs is None else dict(constructor_kwargs)
1665
+
1602
1666
  if source_array is not None:
1603
1667
  if shape is not None or dtype is not None:
1604
1668
  raise ValueError(
1605
1669
  "shape/dtype must not be set when source_array is provided."
1606
1670
  )
1607
- source_array = np.ascontiguousarray(source_array[...])
1608
1671
  shape = self._normalize_shape(source_array.shape)
1609
1672
  dtype = np.dtype(source_array.dtype)
1610
1673
  else:
@@ -1621,6 +1684,7 @@ class MLArray:
1621
1684
  if self.meta.spatial.axis_labels is None
1622
1685
  else _spatial_axis_mask(self.meta.spatial.axis_labels)
1623
1686
  )
1687
+
1624
1688
  self.meta.blosc2 = self._comp_and_validate_blosc2_meta(
1625
1689
  self.meta.blosc2,
1626
1690
  patch_size,
@@ -1629,29 +1693,101 @@ class MLArray:
1629
1693
  shape,
1630
1694
  dtype.itemsize,
1631
1695
  spatial_axis_mask,
1696
+ cparams,
1697
+ dparams,
1632
1698
  )
1633
1699
  self.meta._has_array.has_array = True
1634
1700
 
1635
- cparams = MLArray._resolve_cparams(cparams)
1636
- dparams = MLArray._resolve_dparams(dparams)
1701
+ backend_lib = blosc2 if compressed else np
1702
+ blosc2_storage_kwargs = {}
1703
+ if compressed:
1704
+ blosc2_storage_kwargs = {
1705
+ "chunks": self.meta.blosc2.chunk_size,
1706
+ "blocks": self.meta.blosc2.block_size,
1707
+ "cparams": MLArray._resolve_cparams(self.meta.blosc2.cparams),
1708
+ "dparams": MLArray._resolve_dparams(self.meta.blosc2.dparams),
1709
+ }
1710
+
1711
+ if constructor == "asarray":
1712
+ if compressed:
1713
+ self._store = blosc2.asarray(source_array, **blosc2_storage_kwargs)
1714
+ else:
1715
+ self._store = np.asarray(source_array, dtype=dtype)
1716
+ else:
1717
+ call_kwargs = self._build_constructor_call_kwargs(
1718
+ constructor=constructor,
1719
+ shape=shape,
1720
+ dtype=dtype,
1721
+ constructor_kwargs=constructor_kwargs,
1722
+ )
1637
1723
 
1638
- builder_kwargs = dict(
1639
- shape=shape,
1640
- dtype=dtype,
1641
- chunks=self.meta.blosc2.chunk_size,
1642
- blocks=self.meta.blosc2.block_size,
1643
- cparams=cparams,
1644
- dparams=dparams,
1645
- )
1646
- if source_array is not None:
1647
- builder_kwargs["source_array"] = source_array
1724
+ if not compressed and constructor in ("arange", "linspace"):
1725
+ self._store = self._construct_numpy_range(constructor, call_kwargs)
1726
+ else:
1727
+ func = getattr(backend_lib, constructor, None)
1728
+ if func is None:
1729
+ raise ValueError(f"Unknown constructor '{constructor}'.")
1730
+ if compressed:
1731
+ call_kwargs.update(blosc2_storage_kwargs)
1732
+ self._store = func(**call_kwargs)
1733
+
1734
+ self._backend = "blosc2" if compressed else "numpy"
1648
1735
 
1649
- self._store = store_builder(**builder_kwargs)
1736
+ self.support_metadata = True
1650
1737
 
1651
1738
  self._update_blosc2_meta()
1652
1739
  self._validate_and_add_meta(self.meta)
1653
1740
 
1654
- def _comp_and_validate_blosc2_meta(self, meta_blosc2, patch_size, chunk_size, block_size, shape, dtype_itemsize, spatial_axis_mask):
1741
+ @staticmethod
1742
+ def _build_constructor_call_kwargs(
1743
+ constructor: str,
1744
+ shape: Tuple[int, ...],
1745
+ dtype: np.dtype,
1746
+ constructor_kwargs: dict[str, Any],
1747
+ ) -> dict[str, Any]:
1748
+ """Build constructor kwargs shared by NumPy and Blosc2 backends."""
1749
+ if constructor in ("empty", "zeros", "ones"):
1750
+ return {"shape": shape, "dtype": dtype}
1751
+ if constructor == "full":
1752
+ return {
1753
+ "shape": shape,
1754
+ "fill_value": constructor_kwargs["fill_value"],
1755
+ "dtype": dtype,
1756
+ }
1757
+ if constructor == "arange":
1758
+ return {
1759
+ "start": constructor_kwargs["start"],
1760
+ "stop": constructor_kwargs["stop"],
1761
+ "step": constructor_kwargs["step"],
1762
+ "dtype": dtype,
1763
+ "shape": shape,
1764
+ "c_order": constructor_kwargs.get("c_order", True),
1765
+ }
1766
+ if constructor == "linspace":
1767
+ return {
1768
+ "start": constructor_kwargs["start"],
1769
+ "stop": constructor_kwargs["stop"],
1770
+ "num": constructor_kwargs["num"],
1771
+ "dtype": dtype,
1772
+ "shape": shape,
1773
+ "endpoint": constructor_kwargs.get("endpoint", True),
1774
+ "c_order": constructor_kwargs.get("c_order", True),
1775
+ }
1776
+ raise ValueError(f"Unknown constructor '{constructor}'.")
1777
+
1778
+ @staticmethod
1779
+ def _construct_numpy_range(
1780
+ constructor: str,
1781
+ call_kwargs: dict[str, Any],
1782
+ ) -> np.ndarray:
1783
+ """Construct NumPy arange/linspace arrays and apply shape/order reshaping."""
1784
+ shape = call_kwargs.pop("shape")
1785
+ c_order = call_kwargs.pop("c_order", True)
1786
+ func = getattr(np, constructor)
1787
+ arr = func(**call_kwargs)
1788
+ return np.reshape(arr, shape, order="C" if c_order else "F")
1789
+
1790
+ def _comp_and_validate_blosc2_meta(self, meta_blosc2, patch_size, chunk_size, block_size, shape, dtype_itemsize, spatial_axis_mask, cparams, dparams):
1655
1791
  """Compute and validate Blosc2 chunk/block metadata.
1656
1792
 
1657
1793
  Args:
@@ -1693,11 +1829,30 @@ class MLArray:
1693
1829
  if patch_size is not None:
1694
1830
  chunk_size, block_size = MLArray.comp_blosc2_params(shape, patch_size, spatial_axis_mask, bytes_per_pixel=dtype_itemsize)
1695
1831
 
1696
- meta_blosc2 = MetaBlosc2(chunk_size, block_size, patch_size)
1832
+ cparams = MLArray._resolve_cparams(cparams)
1833
+ dparams = MLArray._resolve_dparams(dparams)
1834
+
1835
+ meta_blosc2 = MetaBlosc2(
1836
+ chunk_size=chunk_size,
1837
+ block_size=block_size,
1838
+ patch_size=patch_size,
1839
+ cparams=cparams,
1840
+ dparams=dparams,
1841
+ )
1697
1842
  meta_blosc2._validate_and_cast(ndims=len(shape), spatial_ndims=num_spatial_axes)
1698
1843
  return meta_blosc2
1699
1844
 
1700
- def _validate_and_add_meta(self, meta, spacing=None, origin=None, direction=None, axis_labels=None, has_array=None, validate=True):
1845
+ def _validate_and_add_meta(
1846
+ self,
1847
+ meta,
1848
+ spacing=None,
1849
+ origin=None,
1850
+ direction=None,
1851
+ affine=None,
1852
+ axis_labels=None,
1853
+ has_array=None,
1854
+ validate=True,
1855
+ ):
1701
1856
  """Validate and attach metadata to the MLArray instance.
1702
1857
 
1703
1858
  Args:
@@ -1709,6 +1864,8 @@ class MLArray:
1709
1864
  spatial axis.
1710
1865
  direction (Optional[Union[List, Tuple, np.ndarray]]): Direction
1711
1866
  cosine matrix with shape (ndims, ndims).
1867
+ affine (Optional[Union[List, Tuple, np.ndarray]]): Homogeneous
1868
+ affine matrix with shape (spatial_ndims + 1, spatial_ndims + 1).
1712
1869
  axis_labels (Optional[List[Union[str, AxisLabel]]]): Per-axis labels or roles. Length must match ndims.
1713
1870
  has_array (Optional[bool]): Explicitly set whether array data is
1714
1871
  present. When True, metadata is validated with array-dependent
@@ -1726,12 +1883,22 @@ class MLArray:
1726
1883
  meta = Meta()
1727
1884
  self.meta = meta
1728
1885
  self.meta._mlarray_version = MLARRAY_VERSION
1886
+
1887
+ if affine is not None and (
1888
+ spacing is not None or origin is not None or direction is not None
1889
+ ):
1890
+ raise ValueError(
1891
+ "affine cannot be provided together with spacing, origin, or direction."
1892
+ )
1893
+
1729
1894
  if spacing is not None:
1730
1895
  self.meta.spatial.spacing = spacing
1731
1896
  if origin is not None:
1732
1897
  self.meta.spatial.origin = origin
1733
1898
  if direction is not None:
1734
1899
  self.meta.spatial.direction = direction
1900
+ if affine is not None:
1901
+ self.meta.spatial.affine = affine
1735
1902
  if axis_labels is not None:
1736
1903
  self.meta.spatial.axis_labels = axis_labels
1737
1904
  if has_array == True:
@@ -1747,15 +1914,18 @@ class MLArray:
1747
1914
  Updates ``self.meta.blosc2`` from the underlying store when the array
1748
1915
  is present.
1749
1916
  """
1750
- if self.meta._has_array.has_array == True:
1917
+ if self._backend != "blosc2":
1918
+ return
1919
+ if self.support_metadata and self.meta._has_array.has_array == True:
1751
1920
  self.meta.blosc2.chunk_size = list(self._store.chunks)
1752
1921
  self.meta.blosc2.block_size = list(self._store.blocks)
1753
1922
 
1754
1923
  def _read_meta(self):
1755
1924
  """Read MLArray metadata from the underlying store, if available."""
1756
1925
  meta = Meta()
1757
- if self.support_metadata and isinstance(self._store, blosc2.ndarray.NDArray):
1926
+ if self.support_metadata and self._backend == "blosc2":
1758
1927
  meta = self._store.vlmeta["mlarray"]
1928
+ meta = pickle.loads(gzip.decompress(meta))
1759
1929
  meta = Meta.from_mapping(meta)
1760
1930
  self._validate_and_add_meta(meta)
1761
1931
 
@@ -1766,7 +1936,7 @@ class MLArray:
1766
1936
  force (bool): If True, write even when mmap_mode is read-only.
1767
1937
  """
1768
1938
  is_writable = False
1769
- if self.support_metadata and isinstance(self._store, blosc2.ndarray.NDArray):
1939
+ if self.support_metadata:
1770
1940
  if self.mode in ('a', 'w') and self.mmap_mode is None:
1771
1941
  is_writable = True
1772
1942
  elif self.mmap_mode in ('r+', 'w+'):
@@ -1776,11 +1946,58 @@ class MLArray:
1776
1946
 
1777
1947
  if not is_writable:
1778
1948
  return
1779
-
1780
- metadata = self.meta.to_mapping()
1781
- if not is_serializable(metadata):
1949
+
1950
+ if self._backend != "blosc2":
1951
+ return
1952
+
1953
+ meta = self.meta.to_mapping()
1954
+ if not is_serializable(meta):
1782
1955
  raise RuntimeError("Metadata is not serializable.")
1783
- self._store.vlmeta["mlarray"] = metadata
1956
+
1957
+ meta = gzip.compress(pickle.dumps(meta, protocol=pickle.HIGHEST_PROTOCOL))
1958
+ self._store.vlmeta["mlarray"] = meta
1959
+
1960
+ def _ensure_blosc2_store(self):
1961
+ """Ensure underlying store is Blosc2, converting from NumPy when needed."""
1962
+ if self._store is None:
1963
+ self._store = blosc2.empty((0,))
1964
+ self._backend = "blosc2"
1965
+ return
1966
+
1967
+ if self._backend == "blosc2":
1968
+ return
1969
+
1970
+ array = np.asarray(self._store)
1971
+ if not self.meta._has_array.has_array:
1972
+ self._store = blosc2.empty((0,))
1973
+ self._backend = "blosc2"
1974
+ return
1975
+
1976
+ shape = self._normalize_shape(array.shape)
1977
+ spatial_axis_mask = (
1978
+ [True] * len(shape)
1979
+ if self.meta.spatial.axis_labels is None
1980
+ else _spatial_axis_mask(self.meta.spatial.axis_labels)
1981
+ )
1982
+ self.meta.blosc2 = self._comp_and_validate_blosc2_meta(
1983
+ self.meta.blosc2,
1984
+ patch_size="default",
1985
+ chunk_size=self.meta.blosc2.chunk_size,
1986
+ block_size=self.meta.blosc2.block_size,
1987
+ shape=shape,
1988
+ dtype_itemsize=np.dtype(array.dtype).itemsize,
1989
+ spatial_axis_mask=spatial_axis_mask,
1990
+ cparams=self.meta.blosc2.cparams,
1991
+ dparams=self.meta.blosc2.dparams,
1992
+ )
1993
+ self._store = blosc2.asarray(
1994
+ np.ascontiguousarray(array),
1995
+ chunks=self.meta.blosc2.chunk_size,
1996
+ blocks=self.meta.blosc2.block_size,
1997
+ cparams=MLArray._resolve_cparams(self.meta.blosc2.cparams),
1998
+ dparams=MLArray._resolve_dparams(self.meta.blosc2.dparams),
1999
+ )
2000
+ self._backend = "blosc2"
1784
2001
 
1785
2002
  @staticmethod
1786
2003
  def _normalize_shape(shape: Union[int, List, Tuple, np.ndarray]) -> Tuple[int, ...]:
@@ -1812,6 +2029,19 @@ class MLArray:
1812
2029
  """Resolve compression params with MLArray defaults."""
1813
2030
  if cparams is None:
1814
2031
  return {"codec": blosc2.Codec.LZ4HC, "clevel": 8}
2032
+ if isinstance(cparams, dict):
2033
+ cparams = dict(cparams)
2034
+ if "codec" in cparams and not isinstance(cparams["codec"], blosc2.Codec):
2035
+ cparams["codec"] = blosc2.Codec(cparams["codec"])
2036
+ if "splitmode" in cparams and not isinstance(cparams["splitmode"], blosc2.SplitMode):
2037
+ cparams["splitmode"] = blosc2.SplitMode(cparams["splitmode"])
2038
+ if "tuner" in cparams and not isinstance(cparams["tuner"], blosc2.Tuner):
2039
+ cparams["tuner"] = blosc2.Tuner(cparams["tuner"])
2040
+ if "filters" in cparams and isinstance(cparams["filters"], (list, tuple)):
2041
+ cparams["filters"] = [
2042
+ f if isinstance(f, blosc2.Filter) else blosc2.Filter(f)
2043
+ for f in cparams["filters"]
2044
+ ]
1815
2045
  return cparams
1816
2046
 
1817
2047
  @staticmethod
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mlarray
3
- Version: 0.0.47
3
+ Version: 0.0.49
4
4
  Summary: Array format specialized for Machine Learning with Blosc2 backend and standardized metadata.
5
5
  Author-email: Karol Gotkowski <karol.gotkowski@dkfz.de>
6
6
  License: MIT
@@ -22,6 +22,7 @@ docs/why.md
22
22
  examples/example_asarray.py
23
23
  examples/example_bboxes_only.py
24
24
  examples/example_channel.py
25
+ examples/example_compressed_vs_uncompressed.py
25
26
  examples/example_in_memory_constructors.py
26
27
  examples/example_metadata_only.py
27
28
  examples/example_non_spatial.py
@@ -64,6 +64,22 @@ class TestAsArray(unittest.TestCase):
64
64
  self.assertEqual(image.meta.source.to_plain(), {"patient_id": "p-001"})
65
65
  self.assertTrue(image.meta.is_seg)
66
66
 
67
+ def test_asarray_uncompressed_numpy_store_and_save(self):
68
+ with tempfile.TemporaryDirectory() as tmpdir:
69
+ array = _make_array(seed=3)
70
+ image = MLArray.asarray(array, meta={"case_id": "np"}, compressed=False)
71
+
72
+ self.assertTrue(isinstance(image._store, np.ndarray))
73
+ self.assertTrue(np.allclose(image.to_numpy(), array))
74
+ self.assertEqual(image.meta.source.to_plain(), {"case_id": "np"})
75
+
76
+ path = Path(tmpdir) / "asarray-numpy-save.mla"
77
+ image.save(path)
78
+ loaded = MLArray(path)
79
+ self.assertFalse(isinstance(loaded._store, np.ndarray))
80
+ self.assertTrue(np.allclose(loaded.to_numpy(), array))
81
+ self.assertEqual(loaded.meta.source.to_plain(), {"case_id": "np"})
82
+
67
83
 
68
84
  if __name__ == "__main__":
69
85
  unittest.main()
@@ -25,6 +25,18 @@ class TestUsage(unittest.TestCase):
25
25
  loaded = MLArray(path)
26
26
  self.assertEqual(loaded.shape, array.shape)
27
27
 
28
+ def test_default_usage_uncompressed_backend(self):
29
+ with tempfile.TemporaryDirectory() as tmpdir:
30
+ array = _make_array(seed=11)
31
+ image = MLArray(array, compressed=False)
32
+ self.assertTrue(isinstance(image._store, np.ndarray))
33
+
34
+ path = Path(tmpdir) / "sample-uncompressed.mla"
35
+ image.save(path)
36
+ loaded = MLArray(path)
37
+ self.assertEqual(loaded.shape, array.shape)
38
+ self.assertTrue(np.allclose(loaded.to_numpy(), array))
39
+
28
40
  def test_mmap_loading(self):
29
41
  with tempfile.TemporaryDirectory() as tmpdir:
30
42
  array = _make_array()
@@ -34,6 +46,16 @@ class TestUsage(unittest.TestCase):
34
46
  loaded = MLArray.open(path, mmap_mode="r")
35
47
  self.assertFalse(isinstance(loaded._store, np.ndarray))
36
48
 
49
+ def test_load_uncompressed_store(self):
50
+ with tempfile.TemporaryDirectory() as tmpdir:
51
+ array = _make_array()
52
+ path = Path(tmpdir) / "sample-load-uncompressed.mla"
53
+ MLArray(array).save(path)
54
+
55
+ loaded = MLArray.load(path, compressed=False)
56
+ self.assertTrue(isinstance(loaded._store, np.ndarray))
57
+ self.assertTrue(np.allclose(loaded.to_numpy(), array))
58
+
37
59
  def test_loading_and_saving(self):
38
60
  with tempfile.TemporaryDirectory() as tmpdir:
39
61
  array = _make_array()
@@ -66,6 +88,32 @@ class TestUsage(unittest.TestCase):
66
88
  self.assertEqual(loaded.origin, [10.0, 10.0, 30.0])
67
89
  self.assertEqual(loaded.meta.source["study_id"], "study-001")
68
90
 
91
+ def test_affine_stored_in_meta_spatial(self):
92
+ array = _make_array(shape=(8, 8, 8))
93
+ affine = [
94
+ [1.0, 0.0, 0.0, 10.0],
95
+ [0.0, 2.0, 0.0, 20.0],
96
+ [0.0, 0.0, 3.0, 30.0],
97
+ [0.0, 0.0, 0.0, 1.0],
98
+ ]
99
+ image = MLArray(array, affine=affine)
100
+
101
+ self.assertEqual(image.meta.spatial.affine, affine)
102
+ self.assertIsNone(image.meta.spatial.spacing)
103
+ self.assertIsNone(image.meta.spatial.origin)
104
+ self.assertIsNone(image.meta.spatial.direction)
105
+
106
+ def test_affine_and_spacing_origin_direction_mix_raises(self):
107
+ array = _make_array(shape=(8, 8, 8))
108
+ affine = [
109
+ [1.0, 0.0, 0.0, 10.0],
110
+ [0.0, 1.0, 0.0, 20.0],
111
+ [0.0, 0.0, 1.0, 30.0],
112
+ [0.0, 0.0, 0.0, 1.0],
113
+ ]
114
+ with self.assertRaises(ValueError):
115
+ MLArray(array, spacing=(1.0, 1.0, 1.0), affine=affine)
116
+
69
117
  def test_copy_metadata_with_override(self):
70
118
  with tempfile.TemporaryDirectory() as tmpdir:
71
119
  array = _make_array()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes