|
8 | 8 | from ndtiff import Dataset |
9 | 9 | from tifffile import TiffFile |
10 | 10 |
|
11 | | -from iohub.convert import TIFFConverter, _adjust_chunks_for_divisibility |
| 11 | +from iohub.convert import TIFFConverter, _clamp_chunks_to_shape |
12 | 12 | from iohub.ngff import Position, open_ome_zarr |
13 | 13 | from iohub.reader import MMStack, NDTiffDataset |
14 | 14 | from tests.conftest import ( |
@@ -54,7 +54,7 @@ def _check_chunks(position: Position, chunks: Literal["XY", "XYZ"] | tuple[int] |
54 | 54 | case "XYZ" | None: |
55 | 55 | assert img.chunks == (1,) * 2 + img.shape[-3:] |
56 | 56 | case tuple(): |
57 | | - expected = _adjust_chunks_for_divisibility(img.shape, chunks) |
| 57 | + expected = _clamp_chunks_to_shape(img.shape, chunks) |
58 | 58 | assert img.chunks == expected |
59 | 59 | case _: |
60 | 60 | raise AssertionError() |
@@ -264,34 +264,41 @@ class MockReader: |
264 | 264 | @pytest.mark.parametrize( |
265 | 265 | ("z_dim", "input_chunk", "expected_z"), |
266 | 266 | [ |
267 | | - (15, 10, 5), # 10 doesn't divide 15, adjust to 5 |
268 | | - (7, 5, 1), # prime - falls to 1 |
269 | | - (16, 8, 8), # divides evenly, no change |
270 | | - (100, 50, 50), # divides evenly |
271 | | - (9, 4, 3), # odd non-prime, adjust to 3 |
| 267 | + (15, 10, 10), # 10 < 15, no clamping needed |
| 268 | + (7, 5, 5), # 5 < 7, no clamping needed |
| 269 | + (16, 8, 8), # 8 < 16, no clamping needed |
| 270 | + (100, 50, 50), # 50 < 100, no clamping needed |
| 271 | + (9, 4, 4), # 4 < 9, no clamping needed |
| 272 | + (5, 10, 5), # 10 > 5, clamped to 5 |
272 | 273 | ], |
273 | 274 | ) |
274 | | - def test_gen_chunks_divisibility(self, make_mock_converter, z_dim, input_chunk, expected_z): |
275 | | - """Test chunks are adjusted to divide evenly into dimensions.""" |
| 275 | + def test_gen_chunks_clamping(self, make_mock_converter, z_dim, input_chunk, expected_z): |
| 276 | + """Test chunks are clamped to dimension size but not reduced for divisibility.""" |
276 | 277 | converter = make_mock_converter(z=z_dim) |
277 | 278 | chunks = converter._gen_chunks((1, 1, input_chunk, 256, 256)) |
278 | 279 | assert chunks[2] == expected_z |
279 | | - assert z_dim % chunks[2] == 0 |
| 280 | + assert chunks[2] <= z_dim |
280 | 281 |
|
281 | | - def test_gen_chunks_max_chunk_size_then_divisibility(self, make_mock_converter): |
282 | | - """Test divisibility check happens AFTER MAX_CHUNK_SIZE adjustment.""" |
283 | | - # Large chunks that trigger MAX_CHUNK_SIZE, then need divisibility fix |
284 | | - # Z=15, large XY to trigger size limit, chunk should end up dividing 15 |
| 282 | + def test_gen_chunks_max_chunk_size_then_clamp(self, make_mock_converter): |
| 283 | + """Test clamping happens AFTER MAX_CHUNK_SIZE adjustment.""" |
285 | 284 | converter = make_mock_converter(z=15, y=2048, x=2048) |
286 | 285 | chunks = converter._gen_chunks((1, 1, 15, 2048, 2048)) |
287 | | - assert 15 % chunks[2] == 0 |
| 286 | + assert chunks[2] <= 15 |
288 | 287 |
|
289 | 288 | @pytest.mark.parametrize("z_dim", [7, 11, 13, 17, 19, 23]) # prime numbers |
290 | 289 | def test_gen_chunks_prime_dimensions(self, make_mock_converter, z_dim): |
291 | | - """Test handling of prime dimension sizes - should fall to 1.""" |
| 290 | + """Test prime dimensions do NOT reduce chunk to 1.""" |
292 | 291 | converter = make_mock_converter(z=z_dim) |
293 | 292 | chunks = converter._gen_chunks((1, 1, z_dim - 1, 256, 256)) |
294 | | - assert z_dim % chunks[2] == 0 |
| 293 | + assert chunks[2] == z_dim - 1 # should stay as-is (< dim) |
| 294 | + assert chunks[2] > 1 |
| 295 | + |
| 296 | + def test_gen_chunks_prime_z_1187(self, make_mock_converter): |
| 297 | + """Regression: Z=1187 (prime) should not reduce chunk to 1.""" |
| 298 | + converter = make_mock_converter(z=1187, y=2048, x=2048) |
| 299 | + chunks = converter._gen_chunks("XYZ") |
| 300 | + assert chunks[2] > 1 |
| 301 | + assert chunks[2] <= 1187 |
295 | 302 |
|
296 | 303 | @pytest.mark.parametrize("input_chunks", ["XY", "XYZ", None]) |
297 | 304 | def test_gen_chunks_string_inputs(self, make_mock_converter, input_chunks): |
@@ -342,6 +349,43 @@ def test_rechunk_xy_to_xyz_preserves_data(shape, target_chunks, tmpdir): |
342 | 349 | np.testing.assert_array_equal(data, written, err_msg="Data lost during XY to XYZ rechunking") |
343 | 350 |
|
344 | 351 |
|
| 352 | +@pytest.mark.parametrize( |
| 353 | + ("shape", "target_chunks"), |
| 354 | + [ |
| 355 | + # Non-divisible Z: 1187 is prime, chunk 512 doesn't divide evenly |
| 356 | + ((1, 1, 1187, 256, 256), (1, 1, 512, 256, 256)), |
| 357 | + # Another non-divisible case |
| 358 | + ((1, 1, 50, 512, 512), (1, 1, 32, 512, 512)), |
| 359 | + ], |
| 360 | +) |
| 361 | +def test_rechunk_non_divisible_z_preserves_data(shape, target_chunks, tmpdir): |
| 362 | + """Test that rechunking with non-divisible Z chunks preserves all data. |
| 363 | +
|
| 364 | + Regression test for https://github.com/czbiohub-sf/iohub/issues/374 |
| 365 | + """ |
| 366 | + import dask |
| 367 | + import dask.array as da |
| 368 | + |
| 369 | + data = np.arange(np.prod(shape), dtype=np.uint16).reshape(shape) |
| 370 | + |
| 371 | + source_chunks = (1, 1, 1, shape[3], shape[4]) |
| 372 | + dask_data = da.from_array(data, chunks=source_chunks) |
| 373 | + |
| 374 | + output = tmpdir / "test_rechunk_nondiv.zarr" |
| 375 | + with open_ome_zarr(output, layout="hcs", mode="w-", channel_names=["test"], version="0.5") as writer: |
| 376 | + pos = writer.create_position("0", "0", "0") |
| 377 | + zarr_img = pos.create_zeros(name="0", shape=shape, dtype=np.uint16, chunks=target_chunks) |
| 378 | + |
| 379 | + chunk_size_bytes = int(np.prod(target_chunks) * 2) |
| 380 | + with dask.config.set({"array.chunk-size": chunk_size_bytes}): |
| 381 | + zarr_img.write_from_dask(dask_data.rechunk(target_chunks)) |
| 382 | + |
| 383 | + with open_ome_zarr(output, layout="hcs", mode="r") as reader: |
| 384 | + written = reader["0/0/0"]["0"][:] |
| 385 | + |
| 386 | + np.testing.assert_array_equal(data, written, err_msg="Data lost during non-divisible Z rechunking") |
| 387 | + |
| 388 | + |
345 | 389 | class TestVersionParameter: |
346 | 390 | """Tests for the OME-NGFF version parameter on TIFFConverter.""" |
347 | 391 |
|
|
0 commit comments