napari-tmidas 0.2.6__py3-none-any.whl → 0.3.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.
@@ -4,17 +4,15 @@
4
4
  import numpy as np
5
5
  import pytest
6
6
 
7
- # Try importing the functions - they may not be available if sklearn-extra is not installed
8
- try:
9
- from napari_tmidas.processing_functions.intensity_label_filter import (
10
- _calculate_label_mean_intensities,
11
- _cluster_intensities,
12
- _filter_labels_by_threshold,
13
- )
14
-
15
- HAS_KMEDOIDS = True
16
- except ImportError:
17
- HAS_KMEDOIDS = False
7
+ # Import the module and check if k-medoids is available
8
+ from napari_tmidas.processing_functions.intensity_label_filter import (
9
+ _HAS_KMEDOIDS,
10
+ _calculate_label_mean_intensities,
11
+ _cluster_intensities,
12
+ _filter_labels_by_threshold,
13
+ )
14
+
15
+ HAS_KMEDOIDS = _HAS_KMEDOIDS
18
16
 
19
17
 
20
18
  @pytest.mark.skipif(
@@ -1,11 +1,17 @@
1
1
  # src/napari_tmidas/_tests/test_registry.py
2
2
  from napari_tmidas._registry import BatchProcessingRegistry
3
+ from napari_tmidas.processing_functions import discover_and_load_processing_functions
3
4
 
4
5
 
5
6
  class TestBatchProcessingRegistry:
6
7
  def setup_method(self):
7
8
  """Clear registry before each test"""
8
9
  BatchProcessingRegistry._processing_functions.clear()
10
+
11
+ def teardown_method(self):
12
+ """Restore registry after each test"""
13
+ BatchProcessingRegistry._processing_functions.clear()
14
+ discover_and_load_processing_functions(reload=True)
9
15
 
10
16
  def test_register_function(self):
11
17
  """Test registering a processing function"""
@@ -0,0 +1,138 @@
1
+ # Test VisCy virtual staining integration
2
+ """
3
+ Tests for VisCy virtual staining processing function.
4
+ """
5
+ import numpy as np
6
+ import pytest
7
+
8
+ from napari_tmidas._registry import BatchProcessingRegistry
9
+ from napari_tmidas.processing_functions import discover_and_load_processing_functions
10
+
11
+
12
+ def test_viscy_registered():
13
+ """Test that VisCy function is registered."""
14
+ # Ensure processing functions are loaded
15
+ discover_and_load_processing_functions()
16
+
17
+ functions = BatchProcessingRegistry.list_functions()
18
+ assert "VisCy Virtual Staining" in functions
19
+
20
+
21
+ def test_viscy_function_info():
22
+ """Test that VisCy function has correct metadata."""
23
+ # Ensure processing functions are loaded
24
+ discover_and_load_processing_functions()
25
+
26
+ info = BatchProcessingRegistry.get_function_info("VisCy Virtual Staining")
27
+
28
+ assert info is not None
29
+ assert "description" in info
30
+ assert "parameters" in info
31
+ assert info["suffix"] == "_virtual_stain"
32
+
33
+ # Check parameters
34
+ params = info["parameters"]
35
+ assert "dim_order" in params
36
+ assert "z_batch_size" in params
37
+ assert "output_channel" in params
38
+
39
+ # Check parameter defaults
40
+ assert params["dim_order"]["default"] == "ZYX"
41
+ assert params["z_batch_size"]["default"] == 15
42
+ assert params["output_channel"]["default"] == "both"
43
+
44
+
45
+ def test_viscy_import():
46
+ """Test that VisCy modules can be imported."""
47
+ from napari_tmidas.processing_functions import viscy_env_manager
48
+ from napari_tmidas.processing_functions import viscy_virtual_staining
49
+
50
+ assert hasattr(viscy_env_manager, "ViscyEnvironmentManager")
51
+ assert hasattr(viscy_virtual_staining, "viscy_virtual_staining")
52
+
53
+
54
+ def test_viscy_dimension_validation():
55
+ """Test that VisCy validates input dimensions."""
56
+ from napari_tmidas.processing_functions.viscy_virtual_staining import (
57
+ viscy_virtual_staining,
58
+ )
59
+
60
+ # Test with 2D image (should fail)
61
+ image_2d = np.random.rand(512, 512)
62
+ with pytest.raises(ValueError, match="requires 3D images with Z dimension"):
63
+ viscy_virtual_staining(image_2d, dim_order="YX")
64
+
65
+ # Test with insufficient Z slices (should fail)
66
+ image_3d_small = np.random.rand(10, 512, 512) # Only 10 slices
67
+ with pytest.raises(ValueError, match="at least 15 Z slices"):
68
+ viscy_virtual_staining(image_3d_small, dim_order="ZYX")
69
+
70
+
71
+ def test_viscy_transpose_dimensions():
72
+ """Test dimension transposition."""
73
+ from napari_tmidas.processing_functions.viscy_virtual_staining import (
74
+ transpose_dimensions,
75
+ )
76
+
77
+ # Test ZYX (no change needed)
78
+ img = np.random.rand(15, 100, 100)
79
+ transposed, new_order, has_time = transpose_dimensions(img, "ZYX")
80
+ assert transposed.shape == (15, 100, 100)
81
+ assert new_order == "ZYX"
82
+ assert has_time is False
83
+
84
+ # Test YXZ (should transpose)
85
+ img = np.random.rand(100, 100, 15)
86
+ transposed, new_order, has_time = transpose_dimensions(img, "YXZ")
87
+ assert transposed.shape == (15, 100, 100)
88
+ assert new_order == "ZYX"
89
+ assert has_time is False
90
+
91
+ # Test TZYX (no change needed)
92
+ img = np.random.rand(5, 15, 100, 100)
93
+ transposed, new_order, has_time = transpose_dimensions(img, "TZYX")
94
+ assert transposed.shape == (5, 15, 100, 100)
95
+ assert new_order == "TZYX"
96
+ assert has_time is True
97
+
98
+
99
+ def test_viscy_env_manager():
100
+ """Test VisCy environment manager."""
101
+ from napari_tmidas.processing_functions.viscy_env_manager import (
102
+ ViscyEnvironmentManager,
103
+ )
104
+
105
+ manager = ViscyEnvironmentManager()
106
+ assert manager.env_name == "viscy"
107
+ assert "viscy" in manager.env_dir
108
+ assert "models" in manager.model_dir
109
+
110
+
111
+ def test_viscy_output_channel_options():
112
+ """Test that output channel options are correctly defined."""
113
+ # Ensure processing functions are loaded
114
+ discover_and_load_processing_functions()
115
+
116
+ info = BatchProcessingRegistry.get_function_info("VisCy Virtual Staining")
117
+ params = info["parameters"]
118
+
119
+ assert "output_channel" in params
120
+ assert "options" in params["output_channel"]
121
+
122
+ options = params["output_channel"]["options"]
123
+ assert "both" in options
124
+ assert "nuclei" in options
125
+ assert "membrane" in options
126
+
127
+
128
+ if __name__ == "__main__":
129
+ # Run basic tests
130
+ test_viscy_registered()
131
+ test_viscy_function_info()
132
+ test_viscy_import()
133
+ test_viscy_dimension_validation()
134
+ test_viscy_transpose_dimensions()
135
+ test_viscy_env_manager()
136
+ test_viscy_output_channel_options()
137
+
138
+ print("✓ All tests passed!")
napari_tmidas/_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.2.6'
32
- __version_tuple__ = version_tuple = (0, 2, 6)
31
+ __version__ = version = '0.3.1'
32
+ __version_tuple__ = version_tuple = (0, 3, 1)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -5,16 +5,20 @@ Package for processing functions that can be registered with the batch processin
5
5
  import importlib
6
6
  import os
7
7
  import pkgutil
8
+ import sys
8
9
  from typing import Dict, List
9
10
 
10
11
  # Keep the registry global
11
12
  from napari_tmidas._registry import BatchProcessingRegistry
12
13
 
13
14
 
14
- def discover_and_load_processing_functions() -> List[str]:
15
+ def discover_and_load_processing_functions(reload: bool = False) -> List[str]:
15
16
  """
16
17
  Discover and load all processing functions from the processing_functions package.
17
18
 
19
+ Args:
20
+ reload: If True, force reload of modules even if already imported
21
+
18
22
  Returns:
19
23
  List of registered function names
20
24
  """
@@ -27,12 +31,27 @@ def discover_and_load_processing_functions() -> List[str]:
27
31
  ):
28
32
  if not is_pkg: # Only load non-package modules
29
33
  try:
30
- # Import the module
31
- importlib.import_module(f"{package}.{module_name}")
34
+ module_fullname = f"{package}.{module_name}"
35
+
36
+ # If reload requested and module already loaded, reload it
37
+ if reload and module_fullname in sys.modules:
38
+ importlib.reload(sys.modules[module_fullname])
39
+ else:
40
+ # Import the module
41
+ importlib.import_module(module_fullname)
42
+
32
43
  print(f"Loaded processing function module: {module_name}")
33
- except ImportError as e:
44
+ except (ImportError, ValueError) as e:
34
45
  # Log the error but continue with other modules
35
- print(f"Failed to import {module_name}: {e}")
46
+ # ValueError catches NumPy binary incompatibility issues
47
+ error_msg = str(e)
48
+ if "numpy.dtype size changed" in error_msg:
49
+ print(
50
+ f"Failed to import {module_name}: NumPy binary incompatibility. "
51
+ "Try: pip install --force-reinstall --no-cache-dir scikit-learn-extra"
52
+ )
53
+ else:
54
+ print(f"Failed to import {module_name}: {e}")
36
55
 
37
56
  # Return the list of registered functions
38
57
  return BatchProcessingRegistry.list_functions()
@@ -67,6 +67,28 @@ class CellposeEnvironmentManager(BaseEnvironmentManager):
67
67
  "Installing Cellpose 4 (Cellpose-SAM) in the dedicated environment..."
68
68
  )
69
69
 
70
+ # First, install PyTorch with CUDA 12.x support for sm_120 compatibility
71
+ # RTX PRO 4000 Blackwell has CUDA capability sm_120, which requires newer PyTorch
72
+ print("Installing PyTorch with CUDA 12.x support (for sm_120 compatibility)...")
73
+ try:
74
+ subprocess.check_call(
75
+ [
76
+ env_python,
77
+ "-m",
78
+ "pip",
79
+ "install",
80
+ "torch",
81
+ "torchvision",
82
+ "torchaudio",
83
+ "--index-url",
84
+ "https://download.pytorch.org/whl/cu124",
85
+ ]
86
+ )
87
+ print("✓ PyTorch with CUDA 12.4 installed successfully")
88
+ except subprocess.CalledProcessError as e:
89
+ print(f"✗ Failed to install PyTorch: {e}")
90
+ raise
91
+
70
92
  # Install packages one by one with error checking
71
93
  packages = ["cellpose", "zarr", "tifffile"]
72
94
  for package in packages:
@@ -47,12 +47,23 @@ try:
47
47
  from sklearn_extra.cluster import KMedoids
48
48
 
49
49
  _HAS_KMEDOIDS = True
50
- except ImportError:
50
+ except (ImportError, ValueError) as e:
51
+ # ImportError: package not installed
52
+ # ValueError: binary incompatibility (e.g., numpy version mismatch)
51
53
  KMedoids = None
52
54
  _HAS_KMEDOIDS = False
53
- print(
54
- "scikit-learn-extra not available. Install with: pip install scikit-learn-extra"
55
- )
55
+ error_msg = str(e)
56
+ if "numpy.dtype size changed" in error_msg:
57
+ print(
58
+ "scikit-learn-extra has a NumPy binary incompatibility. "
59
+ "This is typically resolved by reinstalling scikit-learn-extra. "
60
+ "Run: pip install --force-reinstall --no-cache-dir scikit-learn-extra"
61
+ )
62
+ else:
63
+ print(
64
+ f"scikit-learn-extra not available ({type(e).__name__}). "
65
+ "Install with: pip install scikit-learn-extra"
66
+ )
56
67
 
57
68
  try:
58
69
  import pandas as pd
@@ -2,6 +2,9 @@
2
2
  """
3
3
  Processing functions that depend on scikit-image.
4
4
  """
5
+ import concurrent.futures
6
+ import os
7
+
5
8
  import numpy as np
6
9
 
7
10
  try:
@@ -61,7 +64,7 @@ if SKIMAGE_AVAILABLE:
61
64
  Parameters
62
65
  ----------
63
66
  image : np.ndarray
64
- Input image
67
+ Input image (supports 2D, 3D, and 4D arrays like YX, ZYX, TYX, TZYX)
65
68
  clip_limit : float
66
69
  Clipping limit for contrast limiting (normalized to 0-1 range, e.g., 0.01 = 1%)
67
70
  Higher values give more contrast but may amplify noise
@@ -72,9 +75,18 @@ if SKIMAGE_AVAILABLE:
72
75
  -------
73
76
  np.ndarray
74
77
  CLAHE-enhanced image with same dtype as input
78
+
79
+ Notes
80
+ -----
81
+ For large multi-dimensional datasets (TZYX), processing is parallelized across
82
+ the first dimension to utilize multiple CPU cores effectively.
75
83
  """
76
84
  # Store original dtype to convert back later
77
85
  original_dtype = image.dtype
86
+
87
+ # Print diagnostic info for multi-dimensional data
88
+ if image.ndim > 2:
89
+ print(f"Applying CLAHE to {image.ndim}D image with shape {image.shape}")
78
90
 
79
91
  # Auto-calculate kernel size if not specified
80
92
  if kernel_size <= 0:
@@ -87,13 +99,64 @@ if SKIMAGE_AVAILABLE:
87
99
  # Ensure kernel_size is odd
88
100
  if kernel_size % 2 == 0:
89
101
  kernel_size += 1
90
-
91
- # Apply CLAHE using scikit-image's equalize_adapthist
92
- # Note: clip_limit in equalize_adapthist is already normalized (0-1 range)
93
- # This returns float64 in range [0, 1]
94
- result = skimage.exposure.equalize_adapthist(
95
- image, kernel_size=kernel_size, clip_limit=clip_limit
96
- )
102
+
103
+ if image.ndim > 2:
104
+ print(f"Using kernel_size={kernel_size}, clip_limit={clip_limit}")
105
+
106
+ # For 4D data (TZYX), parallelize across first dimension for better performance
107
+ if image.ndim == 4 and image.shape[0] > 1:
108
+ print(f"Parallelizing CLAHE across {image.shape[0]} timepoints/slices using {max(1, os.cpu_count() - 1)} workers...")
109
+
110
+ def process_slice(idx):
111
+ """Process a single slice along first dimension"""
112
+ result_slice = skimage.exposure.equalize_adapthist(
113
+ image[idx], kernel_size=kernel_size, clip_limit=clip_limit
114
+ )
115
+ if (idx + 1) % max(1, image.shape[0] // 10) == 0:
116
+ print(f" Processed {idx + 1}/{image.shape[0]} slices")
117
+ return result_slice
118
+
119
+ # Process in parallel
120
+ with concurrent.futures.ThreadPoolExecutor(
121
+ max_workers=max(1, os.cpu_count() - 1)
122
+ ) as executor:
123
+ futures = [executor.submit(process_slice, i) for i in range(image.shape[0])]
124
+ results = [future.result() for future in futures]
125
+
126
+ result = np.stack(results, axis=0)
127
+ print("CLAHE processing complete!")
128
+
129
+ elif image.ndim == 3 and image.shape[0] > 5:
130
+ # For 3D data with many slices, also parallelize
131
+ print(f"Parallelizing CLAHE across {image.shape[0]} slices using {max(1, os.cpu_count() - 1)} workers...")
132
+
133
+ def process_slice(idx):
134
+ """Process a single 2D slice"""
135
+ result_slice = skimage.exposure.equalize_adapthist(
136
+ image[idx], kernel_size=kernel_size, clip_limit=clip_limit
137
+ )
138
+ if (idx + 1) % max(1, image.shape[0] // 10) == 0:
139
+ print(f" Processed {idx + 1}/{image.shape[0]} slices")
140
+ return result_slice
141
+
142
+ # Process in parallel
143
+ with concurrent.futures.ThreadPoolExecutor(
144
+ max_workers=max(1, os.cpu_count() - 1)
145
+ ) as executor:
146
+ futures = [executor.submit(process_slice, i) for i in range(image.shape[0])]
147
+ results = [future.result() for future in futures]
148
+
149
+ result = np.stack(results, axis=0)
150
+ print("CLAHE processing complete!")
151
+ else:
152
+ # For 2D or small 3D data, use native implementation
153
+ if image.ndim > 2:
154
+ print("Processing...")
155
+ result = skimage.exposure.equalize_adapthist(
156
+ image, kernel_size=kernel_size, clip_limit=clip_limit
157
+ )
158
+ if image.ndim > 2:
159
+ print("CLAHE processing complete!")
97
160
 
98
161
  # Convert back to original dtype to preserve compatibility
99
162
  if np.issubdtype(original_dtype, np.integer):