napari-tmidas 0.2.2__py3-none-any.whl → 0.2.4__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.
Files changed (54) hide show
  1. napari_tmidas/__init__.py +35 -5
  2. napari_tmidas/_crop_anything.py +1520 -609
  3. napari_tmidas/_env_manager.py +76 -0
  4. napari_tmidas/_file_conversion.py +1646 -1131
  5. napari_tmidas/_file_selector.py +1455 -216
  6. napari_tmidas/_label_inspection.py +83 -8
  7. napari_tmidas/_processing_worker.py +309 -0
  8. napari_tmidas/_reader.py +6 -10
  9. napari_tmidas/_registry.py +2 -2
  10. napari_tmidas/_roi_colocalization.py +1221 -84
  11. napari_tmidas/_tests/test_crop_anything.py +123 -0
  12. napari_tmidas/_tests/test_env_manager.py +89 -0
  13. napari_tmidas/_tests/test_grid_view_overlay.py +193 -0
  14. napari_tmidas/_tests/test_init.py +98 -0
  15. napari_tmidas/_tests/test_intensity_label_filter.py +222 -0
  16. napari_tmidas/_tests/test_label_inspection.py +86 -0
  17. napari_tmidas/_tests/test_processing_basic.py +500 -0
  18. napari_tmidas/_tests/test_processing_worker.py +142 -0
  19. napari_tmidas/_tests/test_regionprops_analysis.py +547 -0
  20. napari_tmidas/_tests/test_registry.py +70 -2
  21. napari_tmidas/_tests/test_scipy_filters.py +168 -0
  22. napari_tmidas/_tests/test_skimage_filters.py +259 -0
  23. napari_tmidas/_tests/test_split_channels.py +217 -0
  24. napari_tmidas/_tests/test_spotiflow.py +87 -0
  25. napari_tmidas/_tests/test_tyx_display_fix.py +142 -0
  26. napari_tmidas/_tests/test_ui_utils.py +68 -0
  27. napari_tmidas/_tests/test_widget.py +30 -0
  28. napari_tmidas/_tests/test_windows_basic.py +66 -0
  29. napari_tmidas/_ui_utils.py +57 -0
  30. napari_tmidas/_version.py +16 -3
  31. napari_tmidas/_widget.py +41 -4
  32. napari_tmidas/processing_functions/basic.py +557 -20
  33. napari_tmidas/processing_functions/careamics_env_manager.py +72 -99
  34. napari_tmidas/processing_functions/cellpose_env_manager.py +415 -112
  35. napari_tmidas/processing_functions/cellpose_segmentation.py +132 -191
  36. napari_tmidas/processing_functions/colocalization.py +513 -56
  37. napari_tmidas/processing_functions/grid_view_overlay.py +703 -0
  38. napari_tmidas/processing_functions/intensity_label_filter.py +422 -0
  39. napari_tmidas/processing_functions/regionprops_analysis.py +1280 -0
  40. napari_tmidas/processing_functions/sam2_env_manager.py +53 -69
  41. napari_tmidas/processing_functions/sam2_mp4.py +274 -195
  42. napari_tmidas/processing_functions/scipy_filters.py +403 -8
  43. napari_tmidas/processing_functions/skimage_filters.py +424 -212
  44. napari_tmidas/processing_functions/spotiflow_detection.py +949 -0
  45. napari_tmidas/processing_functions/spotiflow_env_manager.py +591 -0
  46. napari_tmidas/processing_functions/timepoint_merger.py +334 -86
  47. {napari_tmidas-0.2.2.dist-info → napari_tmidas-0.2.4.dist-info}/METADATA +70 -30
  48. napari_tmidas-0.2.4.dist-info/RECORD +63 -0
  49. napari_tmidas/_tests/__init__.py +0 -0
  50. napari_tmidas-0.2.2.dist-info/RECORD +0 -40
  51. {napari_tmidas-0.2.2.dist-info → napari_tmidas-0.2.4.dist-info}/WHEEL +0 -0
  52. {napari_tmidas-0.2.2.dist-info → napari_tmidas-0.2.4.dist-info}/entry_points.txt +0 -0
  53. {napari_tmidas-0.2.2.dist-info → napari_tmidas-0.2.4.dist-info}/licenses/LICENSE +0 -0
  54. {napari_tmidas-0.2.2.dist-info → napari_tmidas-0.2.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,547 @@
1
+ """
2
+ Tests for regionprops_analysis processing function
3
+ """
4
+
5
+ import os
6
+ import tempfile
7
+ from pathlib import Path
8
+
9
+ import numpy as np
10
+ import pandas as pd
11
+ import pytest
12
+
13
+ from napari_tmidas.processing_functions.regionprops_analysis import (
14
+ analyze_folder_regionprops,
15
+ extract_regionprops_folder,
16
+ extract_regionprops_recursive,
17
+ find_label_images,
18
+ load_label_image,
19
+ parse_dimensions_from_shape,
20
+ reset_regionprops_cache,
21
+ )
22
+
23
+
24
+ @pytest.fixture
25
+ def temp_label_folder():
26
+ """Create a temporary folder with test label images."""
27
+ with tempfile.TemporaryDirectory() as tmpdir:
28
+ folder = Path(tmpdir) / "labels"
29
+ folder.mkdir()
30
+
31
+ # Create 2D label image
32
+ label_2d = np.zeros((100, 100), dtype=np.uint16)
33
+ label_2d[20:40, 20:40] = 1
34
+ label_2d[60:80, 60:80] = 2
35
+ np.save(folder / "image_2d.npy", label_2d)
36
+
37
+ # Create 3D label image (ZYX)
38
+ label_3d = np.zeros((10, 100, 100), dtype=np.uint16)
39
+ label_3d[2:5, 20:40, 20:40] = 1
40
+ label_3d[5:8, 60:80, 60:80] = 2
41
+ np.save(folder / "image_3d.npy", label_3d)
42
+
43
+ # Create 4D label image (TZYX) - 3 timepoints
44
+ label_4d = np.zeros((3, 10, 100, 100), dtype=np.uint16)
45
+ for t in range(3):
46
+ label_4d[t, 2:5, 20:40, 20:40] = 1
47
+ label_4d[t, 5:8, 60:80, 60:80] = 2
48
+ np.save(folder / "image_4d.npy", label_4d)
49
+
50
+ yield folder
51
+
52
+
53
+ def test_parse_dimensions_from_shape():
54
+ """Test dimension parsing from image shape."""
55
+ # Test 2D
56
+ dims = parse_dimensions_from_shape((100, 100), 2)
57
+ assert "Y" in dims and "X" in dims
58
+ assert dims["Y"] == 100 and dims["X"] == 100
59
+
60
+ # Test 3D
61
+ dims = parse_dimensions_from_shape((10, 100, 100), 3)
62
+ assert "Z" in dims and "Y" in dims and "X" in dims
63
+ assert dims["Z"] == 10
64
+
65
+ # Test 4D
66
+ dims = parse_dimensions_from_shape((5, 10, 100, 100), 4)
67
+ assert "T" in dims and "Z" in dims
68
+ assert dims["T"] == 5 and dims["Z"] == 10
69
+
70
+
71
+ def test_find_label_images(temp_label_folder):
72
+ """Test finding label images in a folder."""
73
+ files = find_label_images(str(temp_label_folder))
74
+ assert len(files) == 3
75
+ assert all(f.endswith(".npy") for f in files)
76
+
77
+
78
+ def test_load_label_image(temp_label_folder):
79
+ """Test loading label images."""
80
+ files = find_label_images(str(temp_label_folder))
81
+
82
+ for filepath in files:
83
+ img = load_label_image(filepath)
84
+ assert isinstance(img, np.ndarray)
85
+ assert img.dtype in [np.uint16, np.int32, np.int64]
86
+
87
+
88
+ def test_extract_regionprops_recursive_2d(temp_label_folder):
89
+ """Test extracting regionprops from 2D label image."""
90
+ files = find_label_images(str(temp_label_folder))
91
+ file_2d = [f for f in files if "2d" in f][0]
92
+
93
+ img = load_label_image(file_2d)
94
+ results = extract_regionprops_recursive(
95
+ img,
96
+ prefix_dims={"filename": os.path.basename(file_2d)},
97
+ max_spatial_dims=3,
98
+ )
99
+
100
+ # Should have 2 regions
101
+ assert len(results) == 2
102
+
103
+ # Check that results have expected keys
104
+ for result in results:
105
+ assert "filename" in result
106
+ assert "label" in result
107
+ assert "size" in result # area is renamed to size
108
+ assert "centroid_y" in result
109
+ assert "centroid_x" in result
110
+ assert "bbox_min_y" in result
111
+ assert "bbox_max_x" in result
112
+
113
+ # Check areas (20x20 = 400 pixels each)
114
+ assert results[0]["size"] == 400
115
+ assert results[1]["size"] == 400
116
+
117
+
118
+ def test_extract_regionprops_recursive_3d(temp_label_folder):
119
+ """Test extracting regionprops from 3D label image."""
120
+ files = find_label_images(str(temp_label_folder))
121
+ file_3d = [f for f in files if "3d" in f][0]
122
+
123
+ img = load_label_image(file_3d)
124
+ results = extract_regionprops_recursive(
125
+ img,
126
+ prefix_dims={"filename": os.path.basename(file_3d)},
127
+ max_spatial_dims=3,
128
+ )
129
+
130
+ # Should have 2 regions
131
+ assert len(results) == 2
132
+
133
+ # Check that results have expected keys including Z coordinates
134
+ for result in results:
135
+ assert "filename" in result
136
+ assert "label" in result
137
+ assert "size" in result # area is renamed to size
138
+ assert "centroid_z" in result
139
+ assert "centroid_y" in result
140
+ assert "centroid_x" in result
141
+ assert "bbox_min_z" in result
142
+ assert "bbox_max_z" in result
143
+
144
+ # Check volumes (3 z-slices x 20x20 = 1200 voxels each)
145
+ assert results[0]["size"] == 1200
146
+ assert results[1]["size"] == 1200
147
+
148
+
149
+ def test_extract_regionprops_recursive_4d(temp_label_folder):
150
+ """Test extracting regionprops from 4D label image with time dimension."""
151
+ files = find_label_images(str(temp_label_folder))
152
+ file_4d = [f for f in files if "4d" in f][0]
153
+
154
+ img = load_label_image(file_4d)
155
+ results = extract_regionprops_recursive(
156
+ img,
157
+ prefix_dims={"filename": os.path.basename(file_4d)},
158
+ max_spatial_dims=3,
159
+ )
160
+
161
+ # Should have 2 regions x 3 timepoints = 6 results
162
+ assert len(results) == 6
163
+
164
+ # Check that results have time dimension
165
+ for result in results:
166
+ assert "T" in result
167
+ assert "label" in result
168
+ assert result["T"] in [0, 1, 2]
169
+
170
+ # Check that we have 2 labels per timepoint
171
+ for t in range(3):
172
+ t_results = [r for r in results if r["T"] == t]
173
+ assert len(t_results) == 2
174
+ labels = [r["label"] for r in t_results]
175
+ assert sorted(labels) == [1, 2]
176
+
177
+
178
+ def test_analyze_folder_regionprops(temp_label_folder):
179
+ """Test analyzing entire folder and saving to CSV."""
180
+ output_csv = temp_label_folder.parent / "test_regionprops.csv"
181
+
182
+ df = analyze_folder_regionprops(
183
+ folder_path=str(temp_label_folder),
184
+ output_csv=str(output_csv),
185
+ max_spatial_dims=3,
186
+ dimension_order="Auto",
187
+ )
188
+
189
+ # Check that CSV was created
190
+ assert output_csv.exists()
191
+
192
+ # Check DataFrame structure
193
+ assert isinstance(df, pd.DataFrame)
194
+ assert len(df) > 0
195
+ assert "filename" in df.columns
196
+ assert "label" in df.columns
197
+ assert "size" in df.columns # area is renamed to size
198
+
199
+ # Check that we have results from all files
200
+ filenames = df["filename"].unique()
201
+ assert len(filenames) == 3
202
+
203
+ # Load CSV and verify it matches the DataFrame
204
+ df_loaded = pd.read_csv(output_csv)
205
+ assert len(df_loaded) == len(df)
206
+
207
+
208
+ def test_extract_regionprops_folder_integration(temp_label_folder):
209
+ """Test the full batch processing function."""
210
+ # Reset cache first
211
+ reset_regionprops_cache()
212
+
213
+ # Load one of the images
214
+ files = find_label_images(str(temp_label_folder))
215
+ img = load_label_image(files[0])
216
+
217
+ # Mock the filepath in the call stack by calling from a function
218
+ # that has filepath in its locals
219
+ def mock_process(filepath, image):
220
+ return extract_regionprops_folder(
221
+ image,
222
+ max_spatial_dims=3,
223
+ dimension_order="Auto",
224
+ overwrite_existing=True,
225
+ )
226
+
227
+ result = mock_process(files[0], img)
228
+
229
+ # Function should return None (only generates CSV)
230
+ assert result is None
231
+
232
+ # Check that CSV was created
233
+ folder_name = temp_label_folder.name
234
+ parent_dir = temp_label_folder.parent
235
+ output_csv = parent_dir / f"{folder_name}_regionprops.csv"
236
+ assert output_csv.exists()
237
+
238
+ # Load and verify CSV
239
+ df = pd.read_csv(output_csv)
240
+ assert len(df) > 0
241
+ assert "filename" in df.columns
242
+
243
+
244
+ def test_extract_regionprops_folder_no_overwrite(temp_label_folder):
245
+ """Test that function respects overwrite_existing flag."""
246
+ reset_regionprops_cache()
247
+
248
+ # Create a CSV file first
249
+ folder_name = temp_label_folder.name
250
+ parent_dir = temp_label_folder.parent
251
+ output_csv = parent_dir / f"{folder_name}_regionprops.csv"
252
+
253
+ # Create dummy CSV
254
+ pd.DataFrame({"test": [1, 2, 3]}).to_csv(output_csv, index=False)
255
+ initial_mtime = output_csv.stat().st_mtime
256
+
257
+ # Load image
258
+ files = find_label_images(str(temp_label_folder))
259
+ img = load_label_image(files[0])
260
+
261
+ # Process with overwrite_existing=False
262
+ def mock_process(filepath, image):
263
+ return extract_regionprops_folder(
264
+ image,
265
+ max_spatial_dims=3,
266
+ dimension_order="Auto",
267
+ overwrite_existing=False,
268
+ )
269
+
270
+ _ = mock_process(files[0], img) # result unused, just checking behavior
271
+
272
+ # File should not have been modified
273
+ assert output_csv.stat().st_mtime == initial_mtime
274
+
275
+ # Now test with overwrite=True
276
+ reset_regionprops_cache()
277
+
278
+ # Create a mock process that properly sets filepath in locals
279
+ def mock_process_with_overwrite(filepath, image):
280
+ return extract_regionprops_folder(
281
+ image,
282
+ max_spatial_dims=3,
283
+ dimension_order="Auto",
284
+ overwrite_existing=True,
285
+ )
286
+
287
+ _ = mock_process_with_overwrite(files[0], img) # result unused
288
+
289
+ # Verify the CSV was updated properly
290
+ if output_csv.exists():
291
+ df = pd.read_csv(output_csv)
292
+ # Either it should have the proper structure, or it should have been skipped
293
+ # (because we can't determine filepath in test context)
294
+ if len(df.columns) > 1:
295
+ assert "filename" in df.columns
296
+
297
+
298
+ def test_reset_cache():
299
+ """Test that cache reset works."""
300
+ from napari_tmidas.processing_functions.regionprops_analysis import (
301
+ _REGIONPROPS_CSV_FILES,
302
+ )
303
+
304
+ # First clear any existing cache
305
+ reset_regionprops_cache()
306
+ assert len(_REGIONPROPS_CSV_FILES) == 0
307
+
308
+ # Add a CSV file to the cache
309
+ _REGIONPROPS_CSV_FILES["/test/folder/test_regionprops.csv"] = True
310
+ assert len(_REGIONPROPS_CSV_FILES) == 1
311
+
312
+ # Reset cache
313
+ reset_regionprops_cache()
314
+ assert len(_REGIONPROPS_CSV_FILES) == 0
315
+
316
+
317
+ def test_empty_label_image():
318
+ """Test handling of empty label images."""
319
+ empty_img = np.zeros((100, 100), dtype=np.uint16)
320
+
321
+ results = extract_regionprops_recursive(
322
+ empty_img,
323
+ prefix_dims={"filename": "empty.npy"},
324
+ max_spatial_dims=3,
325
+ )
326
+
327
+ # Should return empty list
328
+ assert len(results) == 0
329
+
330
+
331
+ def test_single_region():
332
+ """Test handling of label image with single region."""
333
+ img = np.zeros((50, 50), dtype=np.uint16)
334
+ img[10:30, 10:30] = 5 # Single region with label 5
335
+
336
+ results = extract_regionprops_recursive(
337
+ img,
338
+ prefix_dims={"filename": "single.npy"},
339
+ max_spatial_dims=3,
340
+ )
341
+
342
+ assert len(results) == 1
343
+ assert results[0]["label"] == 5
344
+ assert results[0]["size"] == 400 # 20x20
345
+
346
+
347
+ def test_dimension_order_tzyx():
348
+ """Test that dimension_order correctly identifies T and Z dimensions."""
349
+ # Create TZYX image (3 timepoints, 5 z-slices, 50x50)
350
+ label_tzyx = np.zeros((3, 5, 50, 50), dtype=np.uint16)
351
+ for t in range(3):
352
+ label_tzyx[t, 1:4, 10:20, 10:20] = 1
353
+ label_tzyx[t, 2:4, 30:40, 30:40] = 2
354
+
355
+ # Extract with explicit dimension order
356
+ results = extract_regionprops_recursive(
357
+ label_tzyx,
358
+ prefix_dims={"filename": "test_tzyx.npy"},
359
+ current_dim=0,
360
+ max_spatial_dims=3,
361
+ dimension_order="TZYX",
362
+ )
363
+
364
+ # Should have 2 regions x 3 timepoints = 6 results
365
+ assert len(results) == 6
366
+
367
+ # Check that T dimension is properly identified
368
+ for result in results:
369
+ assert "T" in result
370
+ assert result["T"] in [0, 1, 2]
371
+ assert "filename" in result
372
+ assert "label" in result
373
+
374
+
375
+ def test_dimension_order_czyx():
376
+ """Test that dimension_order correctly identifies C and Z dimensions."""
377
+ # Create CZYX image (2 channels, 5 z-slices, 50x50)
378
+ label_czyx = np.zeros((2, 5, 50, 50), dtype=np.uint16)
379
+ for c in range(2):
380
+ label_czyx[c, 1:4, 10:20, 10:20] = 1
381
+ label_czyx[c, 2:4, 30:40, 30:40] = 2
382
+
383
+ # Extract with explicit dimension order
384
+ results = extract_regionprops_recursive(
385
+ label_czyx,
386
+ prefix_dims={"filename": "test_czyx.npy"},
387
+ current_dim=0,
388
+ max_spatial_dims=3,
389
+ dimension_order="CZYX",
390
+ )
391
+
392
+ # Should have 2 regions x 2 channels = 4 results
393
+ assert len(results) == 4
394
+
395
+ # Check that C dimension is properly identified
396
+ for result in results:
397
+ assert "C" in result
398
+ assert result["C"] in [0, 1]
399
+ assert "filename" in result
400
+ assert "label" in result
401
+
402
+
403
+ def test_dimension_order_tczyx():
404
+ """Test that dimension_order correctly identifies T, C, and Z dimensions."""
405
+ # Create TCZYX image (2 timepoints, 2 channels, 3 z-slices, 30x30)
406
+ label_tczyx = np.zeros((2, 2, 3, 30, 30), dtype=np.uint16)
407
+ for t in range(2):
408
+ for c in range(2):
409
+ label_tczyx[t, c, 1:3, 5:15, 5:15] = 1
410
+ label_tczyx[t, c, 1:2, 20:25, 20:25] = 2
411
+
412
+ # Extract with explicit dimension order
413
+ results = extract_regionprops_recursive(
414
+ label_tczyx,
415
+ prefix_dims={"filename": "test_tczyx.npy"},
416
+ current_dim=0,
417
+ max_spatial_dims=3,
418
+ dimension_order="TCZYX",
419
+ )
420
+
421
+ # Should have 2 regions x 2 timepoints x 2 channels = 8 results
422
+ assert len(results) == 8
423
+
424
+ # Check that T and C dimensions are properly identified
425
+ for result in results:
426
+ assert "T" in result
427
+ assert result["T"] in [0, 1]
428
+ assert "C" in result
429
+ assert result["C"] in [0, 1]
430
+ assert "filename" in result
431
+ assert "label" in result
432
+
433
+
434
+ def test_extract_regionprops_summary_basic():
435
+ """Test summary statistics extraction without grouping by dimensions."""
436
+ # Create a simple 2D label image
437
+ label_2d = np.zeros((100, 100), dtype=np.uint16)
438
+ label_2d[10:20, 10:20] = 1 # 100 pixels
439
+ label_2d[30:50, 30:50] = 2 # 400 pixels
440
+ label_2d[60:70, 60:80] = 3 # 200 pixels
441
+
442
+ # Create intensity image
443
+ intensity_2d = np.random.rand(100, 100) * 100
444
+
445
+ # Extract regionprops first to get individual values
446
+ results = extract_regionprops_recursive(
447
+ label_2d,
448
+ intensity_image=intensity_2d,
449
+ prefix_dims={"filename": "test.npy"},
450
+ max_spatial_dims=2,
451
+ properties=[
452
+ "label",
453
+ "area",
454
+ "mean_intensity",
455
+ "median_intensity",
456
+ "std_intensity",
457
+ ],
458
+ )
459
+
460
+ # Convert to DataFrame
461
+ df = pd.DataFrame(results)
462
+
463
+ # Calculate expected summary statistics (note: 'area' is renamed to 'size')
464
+ expected_count = len(df)
465
+ expected_size_sum = df["size"].sum()
466
+ expected_size_mean = df["size"].mean()
467
+ expected_size_median = df["size"].median()
468
+
469
+ # Verify we have 3 labels
470
+ assert expected_count == 3
471
+ assert expected_size_sum == 700 # 100 + 400 + 200
472
+ assert expected_size_mean == pytest.approx(233.333, rel=0.01)
473
+ assert expected_size_median == 200
474
+
475
+
476
+ def test_extract_regionprops_summary_with_grouping():
477
+ """Test summary statistics extraction with grouping by dimensions."""
478
+ # Create 4D label image (T=2, Z=3, Y=50, X=50)
479
+ label_4d = np.zeros((2, 3, 50, 50), dtype=np.uint16)
480
+
481
+ # T=0: 2 labels
482
+ label_4d[0, 1:3, 10:20, 10:20] = 1 # 200 pixels
483
+ label_4d[0, 1:2, 30:40, 30:40] = 2 # 100 pixels
484
+
485
+ # T=1: 3 labels
486
+ label_4d[1, 1:3, 10:15, 10:15] = 1 # 50 pixels
487
+ label_4d[1, 1:2, 20:30, 20:30] = 2 # 100 pixels
488
+ label_4d[1, 1:2, 35:45, 35:45] = 3 # 100 pixels
489
+
490
+ # Extract with dimension order
491
+ results = extract_regionprops_recursive(
492
+ label_4d,
493
+ intensity_image=None,
494
+ prefix_dims={"filename": "test_4d.npy"},
495
+ current_dim=0,
496
+ max_spatial_dims=3,
497
+ dimension_order="TZYX",
498
+ properties=["label", "area"],
499
+ )
500
+
501
+ # Convert to DataFrame
502
+ df = pd.DataFrame(results)
503
+
504
+ # Group by T dimension (note: 'area' is renamed to 'size')
505
+ summary_stats = []
506
+ for t, group in df.groupby("T"):
507
+ summary_stats.append(
508
+ {
509
+ "T": t,
510
+ "label_count": len(group),
511
+ "size_sum": group["size"].sum(),
512
+ "size_mean": group["size"].mean(),
513
+ }
514
+ )
515
+
516
+ # Verify T=0 has 2 labels
517
+ t0_stats = [s for s in summary_stats if s["T"] == 0][0]
518
+ assert t0_stats["label_count"] == 2
519
+ assert t0_stats["size_sum"] == 300 # 200 + 100
520
+
521
+ # Verify T=1 has 3 labels
522
+ t1_stats = [s for s in summary_stats if s["T"] == 1][0]
523
+ assert t1_stats["label_count"] == 3
524
+ assert t1_stats["size_sum"] == 250 # 50 + 100 + 100
525
+
526
+
527
+ def test_summary_statistics_calculations():
528
+ """Test that summary statistics are calculated correctly."""
529
+ # Simple test data
530
+ data = np.array([10, 20, 30, 40, 50])
531
+
532
+ # Expected values
533
+ expected_sum = 150
534
+ expected_mean = 30.0
535
+ expected_median = 30.0
536
+ expected_std = np.std(data, ddof=1) # pandas uses ddof=1
537
+
538
+ # Verify using pandas
539
+ df = pd.DataFrame({"values": data})
540
+ assert df["values"].sum() == expected_sum
541
+ assert df["values"].mean() == expected_mean
542
+ assert df["values"].median() == expected_median
543
+ assert df["values"].std() == pytest.approx(expected_std, rel=0.01)
544
+
545
+
546
+ if __name__ == "__main__":
547
+ pytest.main([__file__, "-v"])
@@ -63,5 +63,73 @@ class TestBatchProcessingRegistry:
63
63
  for t in threads:
64
64
  t.join()
65
65
 
66
- assert len(results) == 10
67
- assert len(BatchProcessingRegistry.list_functions()) == 10
66
+ def test_get_function_info_nonexistent(self):
67
+ """Test getting info for non-existent function"""
68
+ info = BatchProcessingRegistry.get_function_info("NonExistent")
69
+ assert info is None
70
+
71
+ def test_register_with_none_parameters(self):
72
+ """Test registering function with None parameters (should convert to empty dict)"""
73
+
74
+ @BatchProcessingRegistry.register(
75
+ name="None Params Function",
76
+ suffix="_none",
77
+ description="Function with None parameters",
78
+ )
79
+ def none_params_func(image):
80
+ return image
81
+
82
+ info = BatchProcessingRegistry.get_function_info(
83
+ "None Params Function"
84
+ )
85
+ assert info["parameters"] == {}
86
+ assert info["suffix"] == "_none"
87
+ assert info["description"] == "Function with None parameters"
88
+
89
+ def test_register_minimal(self):
90
+ """Test registering function with minimal parameters"""
91
+
92
+ @BatchProcessingRegistry.register(name="Minimal Function")
93
+ def minimal_func(image):
94
+ return image
95
+
96
+ info = BatchProcessingRegistry.get_function_info("Minimal Function")
97
+ assert info is not None
98
+ assert callable(info["func"])
99
+
100
+ def test_register_with_complex_parameters(self):
101
+ """Test registering function with complex parameter metadata"""
102
+
103
+ @BatchProcessingRegistry.register(
104
+ name="Complex Params Function",
105
+ suffix="_complex",
106
+ description="Function with complex parameters",
107
+ parameters={
108
+ "param1": {
109
+ "type": int,
110
+ "default": 5,
111
+ "min": 1,
112
+ "max": 10,
113
+ "description": "Parameter description",
114
+ },
115
+ "param2": {
116
+ "type": str,
117
+ "default": "default_value",
118
+ "description": "String parameter",
119
+ },
120
+ },
121
+ )
122
+ def complex_func(image, param1=5, param2="default"):
123
+ return image
124
+
125
+ info = BatchProcessingRegistry.get_function_info(
126
+ "Complex Params Function"
127
+ )
128
+ assert info["parameters"]["param1"]["type"] is int
129
+ assert info["parameters"]["param1"]["default"] == 5
130
+ assert info["parameters"]["param1"]["min"] == 1
131
+ assert info["parameters"]["param1"]["max"] == 10
132
+ assert (
133
+ info["parameters"]["param1"]["description"]
134
+ == "Parameter description"
135
+ )