patchworks 2.6.11__tar.gz → 2.6.12__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 (108) hide show
  1. {patchworks-2.6.11 → patchworks-2.6.12}/PKG-INFO +1 -1
  2. {patchworks-2.6.11 → patchworks-2.6.12}/docs/guide/snakemake.md +46 -1
  3. {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/_chunks.py +14 -1
  4. {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/plugins/cellpose.py +27 -1
  5. {patchworks-2.6.11 → patchworks-2.6.12}/tests/test_allocation.py +29 -0
  6. {patchworks-2.6.11 → patchworks-2.6.12}/tests/test_cellpose.py +24 -0
  7. {patchworks-2.6.11 → patchworks-2.6.12}/tests/test_pw.py +41 -0
  8. {patchworks-2.6.11 → patchworks-2.6.12}/workflow/scripts/_pw.py +38 -6
  9. {patchworks-2.6.11 → patchworks-2.6.12}/workflow/scripts/prepare_tiles.py +12 -0
  10. {patchworks-2.6.11 → patchworks-2.6.12}/.github/workflows/docs.yml +0 -0
  11. {patchworks-2.6.11 → patchworks-2.6.12}/.github/workflows/lint.yml +0 -0
  12. {patchworks-2.6.11 → patchworks-2.6.12}/.github/workflows/release.yml +0 -0
  13. {patchworks-2.6.11 → patchworks-2.6.12}/.gitignore +0 -0
  14. {patchworks-2.6.11 → patchworks-2.6.12}/.markdownlint-cli2.yaml +0 -0
  15. {patchworks-2.6.11 → patchworks-2.6.12}/LICENSE +0 -0
  16. {patchworks-2.6.11 → patchworks-2.6.12}/README.md +0 -0
  17. {patchworks-2.6.11 → patchworks-2.6.12}/cliff.toml +0 -0
  18. {patchworks-2.6.11 → patchworks-2.6.12}/docs/api/chunks.md +0 -0
  19. {patchworks-2.6.11 → patchworks-2.6.12}/docs/api/cluster.md +0 -0
  20. {patchworks-2.6.11 → patchworks-2.6.12}/docs/api/io.md +0 -0
  21. {patchworks-2.6.11 → patchworks-2.6.12}/docs/api/merge_tile_labels.md +0 -0
  22. {patchworks-2.6.11 → patchworks-2.6.12}/docs/api/plugins/cellpose.md +0 -0
  23. {patchworks-2.6.11 → patchworks-2.6.12}/docs/api/plugins/dog.md +0 -0
  24. {patchworks-2.6.11 → patchworks-2.6.12}/docs/api/plugins/napari.md +0 -0
  25. {patchworks-2.6.11 → patchworks-2.6.12}/docs/api/plugins/ome_zarr.md +0 -0
  26. {patchworks-2.6.11 → patchworks-2.6.12}/docs/api/postprocess.md +0 -0
  27. {patchworks-2.6.11 → patchworks-2.6.12}/docs/api/relabel.md +0 -0
  28. {patchworks-2.6.11 → patchworks-2.6.12}/docs/api/tile_process.md +0 -0
  29. {patchworks-2.6.11 → patchworks-2.6.12}/docs/api/volume_filter.md +0 -0
  30. {patchworks-2.6.11 → patchworks-2.6.12}/docs/assets/logo.png +0 -0
  31. {patchworks-2.6.11 → patchworks-2.6.12}/docs/examples/cellpose_2d.md +0 -0
  32. {patchworks-2.6.11 → patchworks-2.6.12}/docs/examples/cellpose_2d.py +0 -0
  33. {patchworks-2.6.11 → patchworks-2.6.12}/docs/examples/cellpose_3d.md +0 -0
  34. {patchworks-2.6.11 → patchworks-2.6.12}/docs/examples/cellpose_3d.py +0 -0
  35. {patchworks-2.6.11 → patchworks-2.6.12}/docs/examples/custom.md +0 -0
  36. {patchworks-2.6.11 → patchworks-2.6.12}/docs/examples/custom_method.py +0 -0
  37. {patchworks-2.6.11 → patchworks-2.6.12}/docs/examples/dog.md +0 -0
  38. {patchworks-2.6.11 → patchworks-2.6.12}/docs/examples/dog.py +0 -0
  39. {patchworks-2.6.11 → patchworks-2.6.12}/docs/examples/standalone_merge.md +0 -0
  40. {patchworks-2.6.11 → patchworks-2.6.12}/docs/examples/stardist.md +0 -0
  41. {patchworks-2.6.11 → patchworks-2.6.12}/docs/examples/stardist_2d.py +0 -0
  42. {patchworks-2.6.11 → patchworks-2.6.12}/docs/getting_started.md +0 -0
  43. {patchworks-2.6.11 → patchworks-2.6.12}/docs/guide/custom_segmentation.md +0 -0
  44. {patchworks-2.6.11 → patchworks-2.6.12}/docs/guide/gpu_distributed.md +0 -0
  45. {patchworks-2.6.11 → patchworks-2.6.12}/docs/guide/label_relations.md +0 -0
  46. {patchworks-2.6.11 → patchworks-2.6.12}/docs/guide/measurements.md +0 -0
  47. {patchworks-2.6.11 → patchworks-2.6.12}/docs/guide/merging.md +0 -0
  48. {patchworks-2.6.11 → patchworks-2.6.12}/docs/guide/ome_zarr_napari.md +0 -0
  49. {patchworks-2.6.11 → patchworks-2.6.12}/docs/guide/performance.md +0 -0
  50. {patchworks-2.6.11 → patchworks-2.6.12}/docs/guide/pitfalls.md +0 -0
  51. {patchworks-2.6.11 → patchworks-2.6.12}/docs/guide/skip_empty.md +0 -0
  52. {patchworks-2.6.11 → patchworks-2.6.12}/docs/guide/tiling.md +0 -0
  53. {patchworks-2.6.11 → patchworks-2.6.12}/docs/index.md +0 -0
  54. {patchworks-2.6.11 → patchworks-2.6.12}/mkdocs.yml +0 -0
  55. {patchworks-2.6.11 → patchworks-2.6.12}/pyproject.toml +0 -0
  56. {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/__init__.py +0 -0
  57. {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/_cluster.py +0 -0
  58. {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/_core.py +0 -0
  59. {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/_distributed.py +0 -0
  60. {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/_gpu.py +0 -0
  61. {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/_io.py +0 -0
  62. {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/_merge.py +0 -0
  63. {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/_notify.py +0 -0
  64. {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/_occupancy.py +0 -0
  65. {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/_postprocess.py +0 -0
  66. {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/_progress.py +0 -0
  67. {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/_relabel.py +0 -0
  68. {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/_relations.py +0 -0
  69. {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/_volume_filter.py +0 -0
  70. {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/plugins/__init__.py +0 -0
  71. {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/plugins/dog.py +0 -0
  72. {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/plugins/napari.py +0 -0
  73. {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/plugins/ome_zarr.py +0 -0
  74. {patchworks-2.6.11 → patchworks-2.6.12}/tests/test_core.py +0 -0
  75. {patchworks-2.6.11 → patchworks-2.6.12}/tests/test_distributed.py +0 -0
  76. {patchworks-2.6.11 → patchworks-2.6.12}/tests/test_dog.py +0 -0
  77. {patchworks-2.6.11 → patchworks-2.6.12}/tests/test_gpu.py +0 -0
  78. {patchworks-2.6.11 → patchworks-2.6.12}/tests/test_napari.py +0 -0
  79. {patchworks-2.6.11 → patchworks-2.6.12}/tests/test_notify.py +0 -0
  80. {patchworks-2.6.11 → patchworks-2.6.12}/tests/test_occupancy.py +0 -0
  81. {patchworks-2.6.11 → patchworks-2.6.12}/tests/test_ome_zarr.py +0 -0
  82. {patchworks-2.6.11 → patchworks-2.6.12}/tests/test_postprocess.py +0 -0
  83. {patchworks-2.6.11 → patchworks-2.6.12}/tests/test_progress.py +0 -0
  84. {patchworks-2.6.11 → patchworks-2.6.12}/tests/test_relations.py +0 -0
  85. {patchworks-2.6.11 → patchworks-2.6.12}/tests/test_run_multi.py +0 -0
  86. {patchworks-2.6.11 → patchworks-2.6.12}/tests/test_volume_filter.py +0 -0
  87. {patchworks-2.6.11 → patchworks-2.6.12}/workflow/README.md +0 -0
  88. {patchworks-2.6.11 → patchworks-2.6.12}/workflow/Snakefile +0 -0
  89. {patchworks-2.6.11 → patchworks-2.6.12}/workflow/config/common.yaml +0 -0
  90. {patchworks-2.6.11 → patchworks-2.6.12}/workflow/config/config.yaml +0 -0
  91. {patchworks-2.6.11 → patchworks-2.6.12}/workflow/config/config_cilia.yaml +0 -0
  92. {patchworks-2.6.11 → patchworks-2.6.12}/workflow/config/config_cyto.yaml +0 -0
  93. {patchworks-2.6.11 → patchworks-2.6.12}/workflow/config/config_nuclei.yaml +0 -0
  94. {patchworks-2.6.11 → patchworks-2.6.12}/workflow/config/multi.yaml +0 -0
  95. {patchworks-2.6.11 → patchworks-2.6.12}/workflow/pixi.toml +0 -0
  96. {patchworks-2.6.11 → patchworks-2.6.12}/workflow/profile/slurm/config.yaml +0 -0
  97. {patchworks-2.6.11 → patchworks-2.6.12}/workflow/rules/common.smk +0 -0
  98. {patchworks-2.6.11 → patchworks-2.6.12}/workflow/rules/convert.smk +0 -0
  99. {patchworks-2.6.11 → patchworks-2.6.12}/workflow/rules/merge.smk +0 -0
  100. {patchworks-2.6.11 → patchworks-2.6.12}/workflow/rules/segment.smk +0 -0
  101. {patchworks-2.6.11 → patchworks-2.6.12}/workflow/scripts/build_occupancy.py +0 -0
  102. {patchworks-2.6.11 → patchworks-2.6.12}/workflow/scripts/convert.py +0 -0
  103. {patchworks-2.6.11 → patchworks-2.6.12}/workflow/scripts/fetch_model.py +0 -0
  104. {patchworks-2.6.11 → patchworks-2.6.12}/workflow/scripts/merge.py +0 -0
  105. {patchworks-2.6.11 → patchworks-2.6.12}/workflow/scripts/relate.py +0 -0
  106. {patchworks-2.6.11 → patchworks-2.6.12}/workflow/scripts/run_multi.py +0 -0
  107. {patchworks-2.6.11 → patchworks-2.6.12}/workflow/scripts/segment_tile.py +0 -0
  108. {patchworks-2.6.11 → patchworks-2.6.12}/workflow/scripts/view.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: patchworks
3
- Version: 2.6.11
3
+ Version: 2.6.12
4
4
  Summary: Tiled processing of arbitrarily large images with globally consistent labels
5
5
  Project-URL: Homepage, https://github.com/imcf/patchworks
6
6
  Project-URL: Issues, https://github.com/imcf/patchworks/issues
@@ -112,15 +112,60 @@ sequential_labels: true # renumber labels to a contiguous 1..N
112
112
  [Filtering by size after merge](merging.md#filtering-by-size-after-merge)
113
113
  for the equivalent direct-API call.
114
114
 
115
+ !!! warning "`model:` names are version-specific — check which Cellpose you have"
116
+ Cellpose 3 and 4 have **disjoint** model names: v3 has `cyto3`,
117
+ `nuclei`, …; v4 replaced them all with the `cpsam` family (`cpsam`,
118
+ `cpsam_v2`, `cpdino`, …). Neither version *raises* on a name it doesn't
119
+ know — v4 logs a warning and quietly loads its default (`cpsam_v2`)
120
+ instead. A `cyto3` left in a config against a v4 install therefore
121
+ segments every tile with a model you did not choose, and the only
122
+ evidence is one line in the job log.
123
+
124
+ patchworks now rejects an unavailable name in `prepare`, on the cheap
125
+ CPU job, rather than letting it through. Check what you have with
126
+ `python -c "import cellpose; print(cellpose.version)"`, then either pick
127
+ a name that install offers, point `model:` at a custom-trained model's
128
+ path, or install the version you want — the workflow ships `cellpose3`
129
+ and `cellpose4` pixi environments (`pixi install -e cellpose3`) for
130
+ exactly this.
131
+
115
132
  !!! tip "3-D anisotropy is derived automatically"
116
133
  Cellpose's `do_3D` assumes isotropic voxels unless told otherwise —
117
134
  without an `anisotropy`, a real (anisotropic) dataset gets objects
118
- fragmented or distorted across z. `segment` now derives it from
135
+ fragmented or distorted across z. `segment` derives it from
119
136
  `image.zarr`'s own calibration (`z` voxel size ÷ lateral voxel size)
120
137
  whenever `do_3D: true` and `cellpose.anisotropy` isn't set explicitly, so
121
138
  there's usually nothing to configure. Set `anisotropy:` yourself in the
122
139
  `cellpose:` block to override it.
123
140
 
141
+ Note it is not just a hint to the model: Cellpose *resizes* the tile to
142
+ `z × anisotropy` planes before the net runs. `tile_shape: "auto"`
143
+ budgets for that resized tile, so a 2.2× anisotropy buys a
144
+ correspondingly smaller tile rather than an out-of-memory job.
145
+
146
+ !!! warning "The physically-correct anisotropy is not always the best one"
147
+ Upsampling z by the true ratio can produce **ring artifacts** and costs
148
+ runtime proportional to the ratio. A contributor on
149
+ [cellpose#1408](https://github.com/MouseLand/cellpose/pull/1408) reports
150
+ that `anisotropy: 1` avoids the rings entirely and is much faster, at
151
+ the cost of boundaries being off by a few pixels in the top/bottom
152
+ z-planes, where a cell's cross-section changes fastest — and recommends
153
+ pairing it with light z-only flow smoothing:
154
+
155
+ ```yaml
156
+ cellpose:
157
+ do_3D: true
158
+ anisotropy: 1 # overrides the derived value
159
+ flow3D_smooth: [1, 0, 0] # z, y, x -- smooth z only
160
+ ```
161
+
162
+ This is a genuine trade-off, not a strictly better setting, and it is
163
+ worth testing both on your own data rather than taking either on faith.
164
+ Reach for it especially if 3-D results look ringed or fragmented along
165
+ z: that is the symptom this addresses. `flow3D_smooth` accepts a scalar
166
+ on older Cellpose and a `[z, y, x]` list from the version that merged
167
+ that PR onward.
168
+
124
169
  !!! tip "Tile size vs runtime"
125
170
  `tile_shape: "auto"` sizes each tile to your GPU's VRAM. Smaller tiles =
126
171
  more (faster) jobs; very large 3-D tiles are slow. Keep `do_3D: false` (2-D
@@ -380,6 +380,7 @@ def auto_tile_shape_cellpose(
380
380
  model_memory_bytes: int = 2 * 1024**3,
381
381
  cellpose_memory_factor: int = 20,
382
382
  n_channels: int = 1,
383
+ anisotropy: float | None = None,
383
384
  verbose: bool = False,
384
385
  ) -> tuple[int, ...]:
385
386
  """Cellpose-optimised tile shape.
@@ -426,6 +427,13 @@ def auto_tile_shape_cellpose(
426
427
  Channels each tile carries (default 1). Above 1 the per-voxel cost
427
428
  scales with it, so the tile shrinks accordingly -- e.g. the workflow's
428
429
  ``nuclei_channel`` hands Cellpose a cyto+nuclei pair.
430
+ anisotropy:
431
+ The ``anisotropy`` Cellpose will be given (``do_3D`` only). It is not
432
+ merely a hint to the model: Cellpose resizes the tile to
433
+ ``z * anisotropy`` planes before the net runs, so a 2.2x anisotropy
434
+ costs 2.2x the z it was handed. Budgeting against the unscaled ``z``
435
+ under-counts by exactly that factor. ``None`` (the default) assumes
436
+ isotropic, i.e. no resize.
429
437
  verbose:
430
438
  Log the chosen shape and memory estimates.
431
439
 
@@ -477,7 +485,12 @@ def auto_tile_shape_cellpose(
477
485
  chunk_spatial = [1, min(y, tile_side), min(x, tile_side)]
478
486
  else:
479
487
  z, y, x = shape[-3], shape[-2], shape[-1]
480
- max_pixels_per_slice = max(1, (max_raw_bytes // 3) // (z * itemsize))
488
+ # Cellpose resizes z by `anisotropy` before the net runs, so the tile
489
+ # it actually holds is that much deeper than the one handed to it.
490
+ effective_z = z * max(1.0, anisotropy or 1.0)
491
+ max_pixels_per_slice = max(
492
+ 1, int((max_raw_bytes // 3) // (effective_z * itemsize))
493
+ )
481
494
  tile_side = max(min_tile, int(max_pixels_per_slice**0.5))
482
495
  chunk_spatial = [z, min(y, tile_side), min(x, tile_side)]
483
496
 
@@ -253,6 +253,26 @@ def _make_config(
253
253
  }
254
254
 
255
255
 
256
+ def available_models() -> list[str]:
257
+ """Pretrained model names the installed Cellpose actually accepts.
258
+
259
+ The name set changed completely between major versions -- v3 ships
260
+ ``cyto3``/``nuclei``/… , v4 replaced them with ``cpsam``-family names --
261
+ and neither version raises on an unknown one. v4 falls back to its
262
+ default model with only a log line, so a v3 name in a v4 environment
263
+ segments everything with the wrong model and nothing fails. Checking the
264
+ name against this list turns that into a config error instead.
265
+
266
+ Returns
267
+ -------
268
+ list of str
269
+ Model names, or an empty list when Cellpose isn't installed or
270
+ doesn't publish them (in which case no name can be rejected).
271
+ """
272
+ names = getattr(_cellpose_models, "MODEL_NAMES", None)
273
+ return list(names) if names else []
274
+
275
+
256
276
  def _get_model(cellpose_dict: dict[str, Any]) -> Any:
257
277
  """Return a worker-local cached Cellpose model.
258
278
 
@@ -272,8 +292,14 @@ def _get_model(cellpose_dict: dict[str, Any]) -> Any:
272
292
  gpu = cellpose_dict.get("gpu", False)
273
293
  model_type = cellpose_dict["model"]
274
294
  if _CELLPOSE_V4:
295
+ # v4 renamed this: `model_type=` is accepted but explicitly
296
+ # ignored ("not used in v4.0.1+"), leaving pretrained_model at
297
+ # its "cpsam_v2" default -- so passing the configured name there
298
+ # silently segmented *every* config with the same default model,
299
+ # whatever `model:` said, with only a logger warning to show for
300
+ # it. `available_models()` rejects an unusable name up front.
275
301
  _model_cache[key] = _cellpose_models.CellposeModel(
276
- model_type=model_type, gpu=gpu
302
+ pretrained_model=model_type, gpu=gpu
277
303
  )
278
304
  else:
279
305
  _model_cache[key] = _cellpose_models.Cellpose(
@@ -117,3 +117,32 @@ def test_gpu_tile_sizing_is_bounded_by_the_host_allocation(monkeypatch):
117
117
  "the 1 GiB host grant must shrink the tile below what the same "
118
118
  "24 GiB GPU would otherwise allow"
119
119
  )
120
+
121
+
122
+ def test_auto_tile_shape_cellpose_budgets_for_the_anisotropy_resize():
123
+ """Cellpose resizes a do_3D tile to z * anisotropy planes before the net
124
+
125
+ runs, so budgeting against the unscaled z hands the GPU a tile that is
126
+ `anisotropy` times bigger than the estimate. Auto-deriving anisotropy
127
+ turned that from dormant into live, so the sizer has to know about it.
128
+ """
129
+ from patchworks import auto_tile_shape_cellpose
130
+
131
+ kwargs = dict(
132
+ shape=(64, 4096, 4096),
133
+ dtype="uint16",
134
+ do_3D=True,
135
+ use_gpu=True,
136
+ gpu_memory=8 * 1024**3,
137
+ available_memory=64 * 1024**3,
138
+ )
139
+ isotropic = auto_tile_shape_cellpose(**kwargs)
140
+ anisotropic = auto_tile_shape_cellpose(**kwargs, anisotropy=2.215)
141
+
142
+ # z is pinned to the full extent either way; the cost is paid in y/x.
143
+ assert anisotropic[0] == isotropic[0]
144
+ assert anisotropic[1] < isotropic[1]
145
+ assert anisotropic[2] < isotropic[2]
146
+ # An anisotropy at or below 1 cannot *grow* the budget.
147
+ assert auto_tile_shape_cellpose(**kwargs, anisotropy=1.0) == isotropic
148
+ assert auto_tile_shape_cellpose(**kwargs, anisotropy=0.5) == isotropic
@@ -47,3 +47,27 @@ def test_cellpose_fn_declares_a_voxel_size_parameter():
47
47
  # model.eval() argument, not a patchworks-specific one) -- only the raw
48
48
  # calibration is a named parameter.
49
49
  assert "anisotropy" not in params
50
+
51
+
52
+ def test_available_models_is_a_list():
53
+ """Empty when Cellpose isn't installed -- then no name can be rejected."""
54
+ from patchworks.plugins.cellpose import available_models
55
+
56
+ assert isinstance(available_models(), list)
57
+
58
+
59
+ def test_v4_gets_the_model_name_as_pretrained_model():
60
+ """v4 accepts `model_type=` and then ignores it ("not used in v4.0.1+"),
61
+
62
+ leaving pretrained_model at its default -- so passing the configured name
63
+ there segmented every config with the same default model whatever
64
+ `model:` said. The name has to reach `pretrained_model=` on v4.
65
+ """
66
+ import inspect
67
+
68
+ from patchworks.plugins import cellpose as cp
69
+
70
+ src = inspect.getsource(cp._get_model)
71
+ v4_branch = src.split("if _CELLPOSE_V4:")[1].split("else:")[0]
72
+ assert "pretrained_model=model_type" in v4_branch
73
+ assert "model_type=model_type" not in v4_branch
@@ -121,3 +121,44 @@ def test_validate_config_rejects_max_volume_at_or_below_min_volume():
121
121
  validate_config(
122
122
  {"method": "threshold", "min_volume": 500.0, "max_volume": 5.0}
123
123
  )
124
+
125
+
126
+ def test_validate_config_rejects_a_model_the_install_does_not_have(
127
+ monkeypatch,
128
+ ):
129
+ """Cellpose does not raise on an unknown model name -- it logs and loads
130
+
131
+ its default. A v3 name against a v4 install therefore segments every
132
+ tile with a model nobody chose, silently. That has to fail in prepare.
133
+ """
134
+ import pytest
135
+ from _pw import validate_config
136
+
137
+ import patchworks.plugins.cellpose as cp
138
+
139
+ monkeypatch.setattr(cp, "available_models", lambda: ["cpsam", "cpsam_v2"])
140
+ with pytest.raises(ValueError, match="cyto3"):
141
+ validate_config({"method": "cellpose", "cellpose": {"model": "cyto3"}})
142
+
143
+
144
+ def test_validate_config_accepts_a_model_the_install_has(monkeypatch):
145
+ from _pw import validate_config
146
+
147
+ import patchworks.plugins.cellpose as cp
148
+
149
+ monkeypatch.setattr(cp, "available_models", lambda: ["cpsam", "cpsam_v2"])
150
+ validate_config({"method": "cellpose", "cellpose": {"model": "cpsam"}})
151
+
152
+
153
+ def test_validate_config_leaves_a_custom_model_path_alone(
154
+ monkeypatch, tmp_path
155
+ ):
156
+ """A path is a custom-trained model; no name list can vouch for it."""
157
+ from _pw import validate_config
158
+
159
+ import patchworks.plugins.cellpose as cp
160
+
161
+ monkeypatch.setattr(cp, "available_models", lambda: ["cpsam"])
162
+ custom = tmp_path / "my_model.pth"
163
+ custom.write_text("")
164
+ validate_config({"method": "cellpose", "cellpose": {"model": str(custom)}})
@@ -322,29 +322,61 @@ def validate_config(cfg) -> None:
322
322
 
323
323
  def _cellpose_problems(cp: dict) -> list[str]:
324
324
  """Unknown keys in the ``cellpose:`` block, checked against model.eval."""
325
+ problems = _cellpose_model_problems(cp)
325
326
  known = ("model", "diameter", "do_3D", "gpu")
326
327
  extra = {k: v for k, v in cp.items() if k not in known}
327
328
  if not extra:
328
- return []
329
+ return problems
329
330
  try:
330
331
  from patchworks.plugins.cellpose import _get_model
331
332
  except ImportError:
332
- return []
333
+ return problems
333
334
  try:
334
335
  model = _get_model({"model": cp.get("model", "cyto3"), "gpu": False})
335
336
  except Exception:
336
337
  # Weights unavailable here (fetch_model runs separately); skip rather
337
338
  # than fail a config that may be perfectly fine.
338
- return []
339
+ return problems
339
340
  unknown = _unknown_kwargs(
340
341
  model.eval, extra, skip=("channels", "channel_axis")
341
342
  )
342
343
  if unknown:
343
- return [
344
+ problems.append(
344
345
  f"unknown cellpose: key(s) {unknown} -- these are forwarded to "
345
346
  "model.eval(), which does not accept them"
346
- ]
347
- return []
347
+ )
348
+ return problems
349
+
350
+
351
+ def _cellpose_model_problems(cp: dict) -> list[str]:
352
+ """Reject a model name the installed Cellpose would silently substitute.
353
+
354
+ Cellpose 4 renamed the pretrained models outright, and does not raise on
355
+ an unknown name -- it logs and loads its default instead. A v3 name
356
+ (``cyto3``) left in a config against a v4 install therefore segments
357
+ every tile with the wrong model, silently. A path is left alone: that is
358
+ a custom-trained model, which no name list can vouch for.
359
+ """
360
+ name = cp.get("model", "cyto3")
361
+ if not isinstance(name, str) or Path(name).exists():
362
+ return []
363
+ try:
364
+ from patchworks.plugins.cellpose import available_models
365
+ except ImportError:
366
+ return []
367
+ names = available_models()
368
+ if not names or name in names:
369
+ return []
370
+ listed = ", ".join(f'"{n}"' for n in names)
371
+ return [
372
+ f"cellpose.model {name!r} is not available in the installed Cellpose "
373
+ f"(it offers {listed}). Cellpose would fall back to its default "
374
+ "model and log a warning rather than fail, so every tile would be "
375
+ "segmented with a model you did not choose. Pick one of the above, "
376
+ "give a path to a custom-trained model, or install the Cellpose "
377
+ 'major version that has it (the workflow ships "cellpose3" and '
378
+ '"cellpose4" pixi environments).'
379
+ ]
348
380
 
349
381
 
350
382
  def _custom_problems(spec) -> list[str]:
@@ -50,6 +50,17 @@ if ts == "auto":
50
50
  n_channels = 2 if cfg.get("nuclei_channel") is not None else 1
51
51
  if method == "cellpose":
52
52
  cp = cfg["cellpose"]
53
+ # The same anisotropy segment will derive (or was given). Cellpose
54
+ # resizes the tile to z * anisotropy planes, so the sizer has to
55
+ # budget for the resized tile, not the one it hands over.
56
+ anisotropy = cp.get("anisotropy")
57
+ if anisotropy is None and cp.get("do_3D", False):
58
+ from patchworks.plugins.cellpose import cellpose_anisotropy
59
+ from patchworks.plugins.ome_zarr import read_pixel_size
60
+
61
+ anisotropy = cellpose_anisotropy(
62
+ read_pixel_size(str(Path(work_dir) / "image.zarr"))
63
+ )
53
64
  sizer = partial(
54
65
  auto_tile_shape_cellpose,
55
66
  do_3D=cp.get("do_3D", False),
@@ -57,6 +68,7 @@ if ts == "auto":
57
68
  diameter=cp.get("diameter"),
58
69
  gpu_memory=gpu_bytes,
59
70
  n_channels=n_channels,
71
+ anisotropy=anisotropy,
60
72
  )
61
73
  else:
62
74
  # cfg["cellpose"] used to be read unconditionally here, so a DoG or
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes