diff --git a/docs/refactor/consolidation-20261009/01-INDEX.md b/docs/refactor/consolidation-20261009/01-INDEX.md index 6453c6dba3..c4337e708a 100644 --- a/docs/refactor/consolidation-20261009/01-INDEX.md +++ b/docs/refactor/consolidation-20261009/01-INDEX.md @@ -85,6 +85,7 @@ Total: about −16k production lines removed from `openhcs/`, plus up to −6k o | Q3 | Delete the GroupBy pickle migration (old saved pipelines then fail loudly)? | Yes | | Q4 | Delete validation logs under `docs/validation` that nothing links to? | Yes | | Q5 | `omero/` (a separate Django plugin) | Out of scope | +| G2-Q1 | Rewrite saved pipelines that set `ImagePlaneSource(channel=…)` or `SaveImagesSeriesAxis.TIMEPOINT/Z_INDEX` (both unread)? | No tool; such pipelines fail loudly on load | ## Surface files diff --git a/docs/refactor/consolidation-20261009/G2-derived-member-references.md b/docs/refactor/consolidation-20261009/G2-derived-member-references.md new file mode 100644 index 0000000000..508a7153a2 --- /dev/null +++ b/docs/refactor/consolidation-20261009/G2-derived-member-references.md @@ -0,0 +1,92 @@ +# G2: Member references derived from the axis family + +**Head audited:** `openhcs` `main` at `66a0b30a0` (#1182; G1, G3, G4, G6 merged). **Rules:** [00-RULES.md](00-RULES.md). **Step 3.** +**Architecture:** [04-ARCHITECTURE.md](04-ARCHITECTURE.md), "The axis family". *Uses:* G1 `Axis`/`AxisFamily`/roles, PolyStore `ZarrBatchAxis`. + +## What is wrong + +**Kernel modules still spell microscopy members by name: 134 references in 22 files name an axis (`well`, `channel`, …), its collection key (`wells`, …) or its label, as a string literal, an attribute or a class field (IDEN-4, MEMB-2).** + +G1 removed every member *class* reference from kernel modules (its guard keeps that at zero). The names themselves remain. Census at this head, an AST scan of `openhcs/` minus the domain modules of the G1 guard and `interop/`. Tokens are derived from the microscopy family: each axis's `name`, `metadata_collection_field`, and `label` when longer than one letter (`Z` and `T` coincide with ImageJ/NGFF dimension letters and `TypeVar("T")`). Docstrings are excluded. + +| File | Hits | What they are | +|---|---|---| +| `runtime/fiji_viewer_server.py` | 38 (attr 32, field 3, lit 3) | hyperstack coordinates named `channel` / `z_axis_coordinates` / `frame`; `name="channel"`; `"Ch{idx}"` label fallback | +| `agent/dto/plate.py` | 19 (attr 9, field 5, lit 5) | MCP `well=` file filter (3 DTOs) and synthetic-plate `wells` | +| `processing/backends/cellprofiler/color.py` | 16 (attr 13, field 2, lit 1) | `FixedImageType.channels`; ColorToGray `"channels"` value | +| `core/progress/types.py` | 14 (attr 3, field 10, lit 1) | progress-stream declarations name their stream `channel` | +| `mcp/dev_client_commands/plate.py` | 8 | `well=`/`wells` client arguments | +| `agent/services/plate_inspection_service.py` | 5 | `.well` filter | +| `agent/services/synthetic_plate_service.py` | 4 | `.wells` | +| `core/synthetic_plate_generation.py` | 4 | `SyntheticPlateGenerationParameters.wells`, `max_wells` reads | +| `formats/experimental_layout_rows.py` | 4 | `"well"`, `"wells"` workbook row labels (MetaXpress domain code outside the domain package) | +| `core/source_bindings.py` | 3 | `ImagePlaneSource.channel` | +| `formats/experimental_result_formats.py` | 3 | `"well"`, `"Well"` workbook columns (domain code) | +| `agent/services/plate_streaming_service.py` | 2 | `.well` filter | +| `core/plate_file_inventory.py` | 2 | `PlateFileInventoryQuery.well` | +| `core/plate_image_inventory.py` | 2 | `.well` filter | +| `processing/backends/cellprofiler/save_images.py` | 2 | `SaveImagesSeriesAxis` values `"timepoint"`, `"z_index"` | +| `runtime/zmq_compilation.py` | 2 | `ZMQCompilationRequest.wells` | +| `core/progress/runtime_tree.py` | 1 | `WellProgressTreeNode.node_kind = "well"` | +| `core/steps/function_io.py` | 1 | NGFF axis type `"channel"` restated beside `"c"` | +| `mcp/dev_client_renderers/plate.py` | 1 | `.wells` | +| `processing/backends/cellprofiler/display_modules.py` | 1 | `PlatemapData.well` (plus a private copy of the well row/column codec) | +| `processing/backends/cellprofiler/export_to_database.py` | 1 | `well_metadata="Well"` (CellProfiler's metadata tag, also restated as `"Metadata_Well"` in `display_modules.py`) | +| `runtime/viewer_display.py` | 1 | Fiji slot wire value `"channel"` | + +Already clean at this head (re-measured; done by G1/G4/G6): the `function_io` Zarr strategies are keyed by role; `config.py` viewer modes are per-role fields with slot-family defaults; source projections are one `SourceAxisProjection` over axis declarations; `required_variable_components` is `required_axis_roles`; analysis consolidation is a domain post-execute hook. Member *class* references in kernel modules: 0. + +Domain modules in the G1 allowlist that are not in the domain's package: `formats/experimental_analysis.py` sits apart from its two sibling modules (`experimental_layout_rows.py`, `experimental_result_formats.py`), which are not allowlisted and so count above. + +## Target (as built) + +- **Asked by role / derived:** the MCP and kernel plate-file filter `well` is `partition` (`PlateFileInventoryQuery`, the three plate-file DTOs, both services, the dev client `--partition`); `plate_image_inventory` already resolved it through `partition_axis()`. The synthetic-plate profile, request and result carry `partition_values` (`--partition-value`); `ZMQCompilationRequest.wells` is `partition_values`. The runtime tree's partition node is `PartitionProgressTreeNode` (`node_kind = "partition"`). +- **External vocabularies declared by their owners:** PolyStore 0.5.0 declares `NGFF_AXIS_TYPES` and derives each axis's NGFF type from its name, so `ZarrBatchAxis` takes no `axis_type` and `function_io` declares only `t`, `field`, `c`, `z` (lockstep PR OpenHCSDev/PolyStore#36; pin `polystore>=0.5.0,<0.6`). The four role-keyed Zarr leaves are named by role (`Time`, `Tile`, `Colour`, `Stack` `ZarrAxisProjection`), not by microscopy member. CellProfiler's `Plate`/`Well` metadata tags are declared once in `interop/cellprofiler/analyst_export.py` (`PLATE_METADATA_TAG`, `WELL_METADATA_TAG`); `CellProfilerDatabaseExportSettings` and `export_to_database` take their defaults from them. +- **Named for what they are:** Fiji hyperstack coordinates are ImageJ's `c`, `z`, `t` (`FijiDimensionStorage`, `FijiHyperstackCoordinateComponents`, `FijiHyperstackCoordinates`); `FijiHyperstackCoordinates.axes()` gives them in ImageJ order, and `dimensions`, `key`, `imagej_position`, `contains_axis_values` and the slice-label builder iterate it instead of restating the triple; slice labels and the channel-label fallback derive from the dimension name. Progress declarations name their stream `progress_channel`. ColorToGray's fixed colour planes are `fixed_channels`, InvertForPrinting's are `rgb_channels`, and the ColorToGray "Channels" type's value is `numbered_channels` (CellProfiler's spelling still matches by member name). `ImagePlaneSource.channel` is `colour_sample`. `SaveImagesSeriesAxis` members are CellProfiler's `TIME`/`SLICE`. `PlatemapData.well` is `well_name` (CellProfiler's DisplayPlatemap term). +- **Moved into the domain package:** `formats/experimental_analysis.py`, `experimental_layout_rows.py` and `experimental_result_formats.py` are `processing/backends/experimental_analysis/{analysis,layout_rows,result_formats}.py`. `openhcs/formats/` holds only `pattern/`; the G1 and G4 allowlists lose `formats/experimental_analysis.py`. +- **One external spelling kept:** `FijiSlots.HyperstackChannel.wire_value == "channel"` is ImageJ's hyperstack channel dimension, and saved configs spell the slot `FijiDimensionMode.CHANNEL` (G6 kept member names so saved configs load unchanged). The guard pins exactly this declaration. + +Production (Python under `openhcs/`): −294 +264. + +## Corrections made while executing + +- The CellProfiler plate map keeps its own well-name split: it parses CellProfiler's `Metadata_Well` measurement, not the partition axis, so binding it to `partition_axis().grid_coordinates` would couple CellProfiler's vocabulary to the active family. The private `_parse_well_name` copy of the well codec stays with the CellProfiler domain (owner: P1). +- The spelling scan excludes `openhcs/interop/` as well as the G1 domain allowlist: interop is the CellProfiler domain (04-ARCHITECTURE, P moves it to `domains/cellprofiler`) and owns CellProfiler's `Well` tag. The member-class scan of G1 still covers interop. +- The NGFF types could not be expressed as kernel declarations without restating `"channel"` in a kernel module; they belong to the NGFF writer, so they moved to PolyStore. + +## Persisted state + +| Store | Class | At cutover | +|---|---|---| +| Fiji/progress/runtime-tree wire values, compile requests, MCP plate requests and results | runtime | reset | +| Saved pipelines naming `ImagePlaneSource(channel=…)` or `SaveImagesSeriesAxis.TIMEPOINT/Z_INDEX` | durable | Only CellProfiler-imported pipelines that set a single-image channel or a non-default series axis spell these; neither parameter is read by any code path. Not converted (decision G2-Q1 in the index; default: no tool). | +| Saved configs (`FijiDimensionMode.*`) | durable | unchanged | + +## Guards + +`tests/unit/test_axis_family_guards.py`: +- `test_kernel_modules_spell_no_microscopy_member`: over every module outside the G1 domain allowlist and `interop/`, the set of (module, enclosing class, spelling) for string literals (docstrings aside, f-string parts included), attribute names and class-body fields equal to a microscopy spelling (each axis's `name`, `metadata_collection_field`, and `label` when longer than one letter) **equals** `EXTERNAL_SPELLINGS`, which holds the one Fiji declaration. Exact equality: a new spelling or a removed exemption both fail. +- `test_domain_allowlist_names_existing_domain_modules`: every allowlist entry exists. +- The allowlist shrinks by `formats/experimental_analysis.py` (also in `test_dataset_source_guards.py`). + +Census: 134 occurrences in 22 kernel files at `66a0b30a0`; 1 (the pinned Fiji declaration) after. + +## Tests + +- Witness (`tests/unit/test_axis_family_witness.py`): new `test_zarr_layout_follows_roles_and_ngff_types` (the remote-sensing family's Zarr layout follows its roles: tile → HCS image `field`, band → `c`, date → `t`, NGFF types from PolyStore). The end-to-end subprocess witness (no domain module loaded) passes unchanged. +- PolyStore: `test_array_axes_take_their_type_from_the_ngff_axis_name`; constructor calls lose the restated type. +- Touched behaviour tests take the new names; no assertion is weakened. + +## New-case experiments + +Before: a second domain meets `well`/`wells` in the MCP file filter, the synthetic-plate profile, the compile request and the runtime tree, and `function_io` restates the NGFF type beside each NGFF name. After: those read the partition axis, and the NGFF type comes from the writer. + +## Handoff (recorded for later surfaces) + +- **G7:** identifiers that contain a member name are runtime vocabulary and are not guarded yet: `WellFilterConfig`/`well_filter`/`well_filter_mode`/`WellFilterProcessor` (`core/config.py`, `core/utils.py`), `owned_wells`/`total_wells` (progress), `available_wells`, `_wells_for_execution` (`zmq_execution_server.py`), `plate_*` names. Once renamed, the guard's token match should extend to identifier substrings. +- **U1:** `pyqt_gui/widgets/shared/plate_view_widget.py` (`coord_to_well`, `wells_with_images`), the image browser's well decoding, and the MCP `component_filters` generalisation of the single `partition` filter. +- **P / P1:** `processing/backends/analysis/consolidate_analysis_results.py` and `processing/backends/experimental_analysis/` move into `openhcs/domains/microscopy`; `processing/backends/cellprofiler/` (with `display_modules._parse_well_name` and the `Metadata_Well*` defaults) into `domains/cellprofiler`. The G1 spelling scan then excludes only `domains/`. + +## Done when + +The guard finds zero kernel occurrences besides the pinned Fiji declaration; the witness, the touched tests, the parity check (baseline 29/30) and the `test_main.py` disk and zarr direct cases are green. diff --git a/external/PolyStore b/external/PolyStore index f6af43a9ba..473c3dafed 160000 --- a/external/PolyStore +++ b/external/PolyStore @@ -1 +1 @@ -Subproject commit f6af43a9ba17f605911033637e6e0a6a1e44f5dc +Subproject commit 473c3dafedff695afd7ba1c368cdcd57ba2d0f66 diff --git a/openhcs/agent/dto/plate.py b/openhcs/agent/dto/plate.py index 08669d99b0..ff9fc15037 100644 --- a/openhcs/agent/dto/plate.py +++ b/openhcs/agent/dto/plate.py @@ -149,7 +149,7 @@ class SelectedPlateFileFilterOptions(SelectedPlateTargetOptions): kind: PlateFileKind | None = PlateFileKind.IMAGE path_contains: str | None = None - well: str | None = None + partition: str | None = None limit: int = 1 @@ -320,7 +320,7 @@ class PlateFileQueryRequest: pattern_format: str | None = None kind: PlateFileKind | None = PlateFileKind.IMAGE path_contains: str | None = None - well: str | None = None + partition: str | None = None offset: int = 0 limit: int = 50 include_previews: bool = True @@ -337,7 +337,7 @@ def from_fields( pattern_format: str | None = None, kind: PlateFileKindSelection = PlateFileKind.IMAGE, path_contains: str | None = None, - well: str | None = None, + partition: str | None = None, offset: int = 0, limit: int = 50, include_previews: bool = True, @@ -351,7 +351,7 @@ def from_fields( pattern_format=pattern_format, kind=PlateFileInventoryQuery.kind_from_value(kind), path_contains=path_contains, - well=well, + partition=partition, offset=offset, limit=limit, include_previews=include_previews, @@ -367,7 +367,7 @@ def as_tool_arguments(self) -> dict[str, JsonValue]: "pattern_format": self.pattern_format, "kind": PlateFileInventoryQuery.kind_value(self.kind), "path_contains": self.path_contains, - "well": self.well, + "partition": self.partition, "offset": self.offset, "limit": self.limit, "include_previews": self.include_previews, @@ -388,7 +388,7 @@ class PlateFileStreamRequest: pattern_format: str | None = None kind: PlateFileKind | None = PlateFileKind.IMAGE path_contains: str | None = None - well: str | None = None + partition: str | None = None limit: int = 1 viewer_config_key: str = NapariViewer.config_key display_config: NapariDisplayConfig | None = None @@ -407,7 +407,7 @@ def from_fields( pattern_format: str | None = None, kind: PlateFileKindSelection = PlateFileKind.IMAGE, path_contains: str | None = None, - well: str | None = None, + partition: str | None = None, limit: int = 1, viewer_config_key: str = NapariViewer.config_key, display_config: NapariDisplayConfig | None = None, @@ -426,7 +426,7 @@ def from_fields( pattern_format=pattern_format, kind=PlateFileInventoryQuery.kind_from_value(kind), path_contains=path_contains, - well=well, + partition=partition, limit=limit, viewer_config_key=viewer_config_key, display_config=display_config, @@ -449,7 +449,7 @@ def as_tool_arguments(self) -> dict[str, JsonValue]: "pattern_format": self.pattern_format, "kind": PlateFileInventoryQuery.kind_value(self.kind), "path_contains": self.path_contains, - "well": self.well, + "partition": self.partition, "limit": self.limit, "viewer_config_key": self.viewer_config_key, "display_config": to_jsonable(self.display_config), @@ -545,7 +545,7 @@ def from_fields( kind: PlateFileKindSelection = PlateFileKind.IMAGE, target: str = SelectedPlateFileQueryTarget.SELECTED.value, path_contains: str | None = None, - well: str | None = None, + partition: str | None = None, offset: int = 0, limit: int = 50, include_previews: bool = True, @@ -558,7 +558,7 @@ def from_fields( kind=PlateFileInventoryQuery.kind_from_value(kind), target=cls.target_from_value(target), path_contains=path_contains, - well=well, + partition=partition, offset=offset, limit=limit, include_previews=include_previews, @@ -573,7 +573,7 @@ def as_tool_arguments(self) -> dict[str, JsonValue]: "kind": PlateFileInventoryQuery.kind_value(self.kind), "target": self.target.value, "path_contains": self.path_contains, - "well": self.well, + "partition": self.partition, "offset": self.offset, "limit": self.limit, "include_previews": self.include_previews, @@ -593,7 +593,7 @@ def to_plate_file_query_request( pattern_format=self.pattern_format, kind=self.kind, path_contains=self.path_contains, - well=self.well, + partition=self.partition, offset=self.offset, limit=self.limit, include_previews=self.include_previews, @@ -710,7 +710,7 @@ def from_fields( kind: PlateFileKindSelection = PlateFileKind.IMAGE, target: str = SelectedPlateFileQueryTarget.SELECTED.value, path_contains: str | None = None, - well: str | None = None, + partition: str | None = None, limit: int = 1, viewer_config_key: str = NapariViewer.config_key, host: str = "localhost", @@ -726,7 +726,7 @@ def from_fields( kind=PlateFileInventoryQuery.kind_from_value(kind), target=cls.target_from_value(target), path_contains=path_contains, - well=well, + partition=partition, limit=limit, viewer_config_key=viewer_config_key, connection=ExecutionConnectionSpec( @@ -746,7 +746,7 @@ def as_tool_arguments(self) -> dict[str, JsonValue]: "kind": PlateFileInventoryQuery.kind_value(self.kind), "target": self.target.value, "path_contains": self.path_contains, - "well": self.well, + "partition": self.partition, "limit": self.limit, "viewer_config_key": self.viewer_config_key, **self.connection.tool_arguments(), @@ -768,7 +768,7 @@ def to_plate_file_stream_request( pattern_format=self.pattern_format, kind=self.kind, path_contains=self.path_contains, - well=self.well, + partition=self.partition, limit=self.limit, viewer_config_key=self.viewer_config_key, connection=self.connection, @@ -799,7 +799,7 @@ class SyntheticPlateGenerationRequest: shared_cell_fraction: float = ( SYNTHETIC_PLATE_GENERATION_PROFILE.default_request.shared_cell_fraction ) - wells: tuple[str, ...] = SYNTHETIC_PLATE_GENERATION_PROFILE.default_request.wells + partition_values: tuple[str, ...] = SYNTHETIC_PLATE_GENERATION_PROFILE.default_request.partition_values format: SyntheticPlateFormat = ( SYNTHETIC_PLATE_GENERATION_PROFILE.default_request.format ) @@ -841,7 +841,7 @@ def from_fields( shared_cell_fraction: float = ( SYNTHETIC_PLATE_GENERATION_PROFILE.default_request.shared_cell_fraction ), - wells: list[str] | None = None, + partition_values: list[str] | None = None, format: str = (SYNTHETIC_PLATE_GENERATION_PROFILE.default_request.format.value), openhcs_format: bool = ( SYNTHETIC_PLATE_GENERATION_PROFILE.default_request.openhcs_format @@ -868,10 +868,10 @@ def from_fields( z_stack_levels=z_stack_levels, num_cells=num_cells, shared_cell_fraction=shared_cell_fraction, - wells=( - tuple(wells) - if wells is not None - else SYNTHETIC_PLATE_GENERATION_PROFILE.default_request.wells + partition_values=( + tuple(partition_values) + if partition_values is not None + else SYNTHETIC_PLATE_GENERATION_PROFILE.default_request.partition_values ), format=SyntheticPlateFormat(format), openhcs_format=openhcs_format, @@ -893,7 +893,7 @@ def as_tool_arguments(self) -> dict[str, JsonValue]: "z_stack_levels": self.z_stack_levels, "num_cells": self.num_cells, "shared_cell_fraction": self.shared_cell_fraction, - "wells": list(self.wells), + "partition_values": list(self.partition_values), "format": self.format.value, "openhcs_format": self.openhcs_format, "include_all_components": self.include_all_components, @@ -1038,7 +1038,7 @@ class SyntheticPlateGenerationResult(AgentResultEnvelope): tile_size: tuple[int, int] = () overlap_percent: int = 0 stage_error_px: int = 0 - wells: tuple[str, ...] = () + partition_values: tuple[str, ...] = () wavelengths: int = 0 z_stack_levels: int = 0 num_cells: int = 0 diff --git a/openhcs/agent/services/plate_inspection_service.py b/openhcs/agent/services/plate_inspection_service.py index 7eb1274898..0217733af1 100644 --- a/openhcs/agent/services/plate_inspection_service.py +++ b/openhcs/agent/services/plate_inspection_service.py @@ -1144,7 +1144,7 @@ def _query_files( query_kind is PlateFileKind.RESULT and request.microscope_type == PlateInspectionDefaults.MICROSCOPE_AUTO and request.pattern_format is None - and request.well is None + and request.partition is None ): result_only = self._result_only_query_files( request=request, @@ -1159,7 +1159,7 @@ def _query_files( query_kind is PlateFileKind.IMAGE and request.microscope_type == PlateInspectionDefaults.MICROSCOPE_AUTO and request.pattern_format is None - and request.well is None + and request.partition is None ): result_only = self._result_only_query_files( request=request, @@ -1206,7 +1206,7 @@ def _query_files( parser = self._parser( handler, warnings, - warn=query_kind is not PlateFileKind.RESULT or request.well is not None, + warn=query_kind is not PlateFileKind.RESULT or request.partition is not None, ) file_inventory = self._plate_file_inventory_for_query( handler, @@ -1232,7 +1232,7 @@ def _query_explicit_result_directory( plate_path: Path, ) -> PlateFileQueryResult: """Read persisted files without inventing a microscope/source identity.""" - if request.kind is not PlateFileKind.RESULT or request.well is not None: + if request.kind is not PlateFileKind.RESULT or request.partition is not None: return self._query_files_error( request, AgentError( @@ -1311,7 +1311,7 @@ def _query_files_from_inventory( PlateFileInventoryQuery( kinds=PlateFileInventoryQuery.kinds_for(request.kind), path_contains=request.path_contains, - well=request.well, + partition=request.partition, offset=request.offset, limit=request.limit, ) diff --git a/openhcs/agent/services/plate_streaming_service.py b/openhcs/agent/services/plate_streaming_service.py index 90ea1d0703..87646e45a1 100644 --- a/openhcs/agent/services/plate_streaming_service.py +++ b/openhcs/agent/services/plate_streaming_service.py @@ -160,7 +160,7 @@ def stream_files( ) result = replace(result, viewer_type=config.viewer_family.viewer_type(), connection=connection) if request.result_directory is not None: - if request.kind is not PlateFileKind.RESULT or request.well is not None: + if request.kind is not PlateFileKind.RESULT or request.partition is not None: raise ValueError( "Explicit result-directory streaming requires kind='result' " "and no acquisition-component filter." @@ -502,7 +502,7 @@ def _resolve_records( inventory_query = PlateFileInventoryQuery( kinds=kinds, path_contains=request.path_contains, - well=request.well, + partition=request.partition, offset=0, limit=requested_limit, ) diff --git a/openhcs/agent/services/synthetic_plate_service.py b/openhcs/agent/services/synthetic_plate_service.py index 0e5295b83c..21a6577a00 100644 --- a/openhcs/agent/services/synthetic_plate_service.py +++ b/openhcs/agent/services/synthetic_plate_service.py @@ -126,7 +126,7 @@ def generate( tile_size=(request.tile_width, request.tile_height), overlap_percent=request.overlap_percent, stage_error_px=request.stage_error_px, - wells=request.wells, + partition_values=request.partition_values, wavelengths=request.wavelengths, z_stack_levels=request.z_stack_levels, num_cells=request.num_cells, @@ -161,7 +161,7 @@ def _generate_dataset( z_stack_levels=request.z_stack_levels, num_cells=request.num_cells, shared_cell_fraction=request.shared_cell_fraction, - wells=list(request.wells), + wells=list(request.partition_values), format=self._profile.format_from_value(request.format).value, openhcs_format=request.openhcs_format, include_all_components=request.include_all_components, @@ -193,7 +193,7 @@ def _parameters_from_request( z_stack_levels=request.z_stack_levels, num_cells=request.num_cells, shared_cell_fraction=request.shared_cell_fraction, - wells=request.wells, + partition_values=request.partition_values, format=request.format, openhcs_format=request.openhcs_format, include_all_components=request.include_all_components, @@ -227,7 +227,7 @@ def _error_result( tile_size=(request.tile_width, request.tile_height), overlap_percent=request.overlap_percent, stage_error_px=request.stage_error_px, - wells=request.wells, + partition_values=request.partition_values, wavelengths=request.wavelengths, z_stack_levels=request.z_stack_levels, num_cells=request.num_cells, diff --git a/openhcs/core/plate_file_inventory.py b/openhcs/core/plate_file_inventory.py index 1bfbd9d3e6..e320b8b545 100644 --- a/openhcs/core/plate_file_inventory.py +++ b/openhcs/core/plate_file_inventory.py @@ -24,7 +24,7 @@ class PlateFileInventoryQuery: kinds: tuple[PlateFileKind, ...] = () path_contains: str | None = None - well: str | None = None + partition: str | None = None offset: int = 0 limit: int = 50 @@ -62,7 +62,7 @@ def normalized(self) -> "PlateFileInventoryQuery": return PlateFileInventoryQuery( kinds=self.kinds, path_contains=self.path_contains, - well=self.well, + partition=self.partition, offset=max(0, int(self.offset)), limit=max(0, int(self.limit)), ) diff --git a/openhcs/core/plate_image_inventory.py b/openhcs/core/plate_image_inventory.py index 3eb37db9aa..524255ed91 100644 --- a/openhcs/core/plate_image_inventory.py +++ b/openhcs/core/plate_image_inventory.py @@ -481,11 +481,11 @@ def matches(self, query: "PlateFileInventoryQuery") -> bool: ) if needle not in haystack: return False - if query.well is not None: + if query.partition is not None: partition_value = self.metadata.get( AxisFamily.active().partition_axis().name ) - if partition_value is None or str(partition_value) != query.well: + if partition_value is None or str(partition_value) != query.partition: return False return True diff --git a/openhcs/core/progress/runtime_tree.py b/openhcs/core/progress/runtime_tree.py index 965663eab6..da19a617bc 100644 --- a/openhcs/core/progress/runtime_tree.py +++ b/openhcs/core/progress/runtime_tree.py @@ -136,10 +136,10 @@ def label_for(cls, identity: RuntimeTreeNodeIdentity) -> str: return f"Worker {identity.worker_slot or identity.node_id}" -class WellProgressTreeNode( +class PartitionProgressTreeNode( ExplicitPercentRuntimeTreeNode, RuntimeTreeNodeDeclarationBase ): - node_kind = "well" + node_kind = "partition" sort_order = 20 @classmethod @@ -719,7 +719,7 @@ def make_well_progress_node( children: List[RuntimeTreeNode], ) -> RuntimeTreeNode: return self.make_runtime_node( - declaration=WellProgressTreeNode, + declaration=PartitionProgressTreeNode, identity=RuntimeTreeNodeIdentity(node_id=axis_id, axis_id=axis_id), status=status, info="", @@ -1126,6 +1126,6 @@ def _is_execution_mode( "RuntimeTreeProjectionBuilder", "RuntimeTreeStatusProjector", "StepProgressTreeNode", - "WellProgressTreeNode", + "PartitionProgressTreeNode", "WorkerProgressTreeNode", ) diff --git a/openhcs/core/progress/types.py b/openhcs/core/progress/types.py index e09cda0287..305931fbe5 100644 --- a/openhcs/core/progress/types.py +++ b/openhcs/core/progress/types.py @@ -99,20 +99,20 @@ def __str__(self): class ProgressChannelDeclarationBase(ABC, metaclass=AutoRegisterMeta): """Nominal semantic declaration for one progress channel.""" - __registry_key__ = "channel" + __registry_key__ = "progress_channel" __skip_if_no_key__ = True __registry__: ClassVar[ dict[ProgressChannel, type["ProgressChannelDeclarationBase"]] ] = {} - channel: ClassVar[ProgressChannel | None] = None + progress_channel: ClassVar[ProgressChannel | None] = None role: ClassVar[ProgressChannelRole] @classmethod def require_channel(cls) -> ProgressChannel: - if cls.channel is None: + if cls.progress_channel is None: raise TypeError(f"{cls.__name__} does not declare a progress channel.") - return cls.channel + return cls.progress_channel @classmethod def for_channel( @@ -135,19 +135,19 @@ class ExecutionProgressChannel: class InitProgressChannel(ControlProgressChannel, ProgressChannelDeclarationBase): - channel = ProgressChannel.INIT + progress_channel = ProgressChannel.INIT class CompileProgressChannel(ControlProgressChannel, ProgressChannelDeclarationBase): - channel = ProgressChannel.COMPILE + progress_channel = ProgressChannel.COMPILE class PipelineProgressChannel(ExecutionProgressChannel, ProgressChannelDeclarationBase): - channel = ProgressChannel.PIPELINE + progress_channel = ProgressChannel.PIPELINE class StepProgressChannel(ExecutionProgressChannel, ProgressChannelDeclarationBase): - channel = ProgressChannel.STEP + progress_channel = ProgressChannel.STEP class ProgressPhaseDeclarationBase(ABC, metaclass=AutoRegisterMeta): @@ -160,7 +160,7 @@ class ProgressPhaseDeclarationBase(ABC, metaclass=AutoRegisterMeta): ] = {} phase: ClassVar[ProgressPhase | None] = None - channel: ClassVar[type[ProgressChannelDeclarationBase]] + progress_channel: ClassVar[type[ProgressChannelDeclarationBase]] is_terminal: ClassVar[bool] = False is_failure: ClassVar[bool] = False is_success_terminal: ClassVar[bool] = False @@ -218,25 +218,25 @@ def is_success_terminal_for_status( class InitChannelProgressPhase: """Trait for progress phases carried on the init channel.""" - channel: ClassVar[type[ProgressChannelDeclarationBase]] = InitProgressChannel + progress_channel: ClassVar[type[ProgressChannelDeclarationBase]] = InitProgressChannel class CompileChannelProgressPhase: """Trait for progress phases carried on the compile channel.""" - channel: ClassVar[type[ProgressChannelDeclarationBase]] = CompileProgressChannel + progress_channel: ClassVar[type[ProgressChannelDeclarationBase]] = CompileProgressChannel class PipelineChannelProgressPhase: """Trait for progress phases carried on the pipeline execution channel.""" - channel: ClassVar[type[ProgressChannelDeclarationBase]] = PipelineProgressChannel + progress_channel: ClassVar[type[ProgressChannelDeclarationBase]] = PipelineProgressChannel class StepChannelProgressPhase: """Trait for progress phases carried on the step execution channel.""" - channel: ClassVar[type[ProgressChannelDeclarationBase]] = StepProgressChannel + progress_channel: ClassVar[type[ProgressChannelDeclarationBase]] = StepProgressChannel class InitProgressPhase(InitChannelProgressPhase, ProgressPhaseDeclarationBase): @@ -398,7 +398,7 @@ class QueuedProgressStatus(ProgressStatusDeclarationBase): def phase_channel(phase: ProgressPhase) -> ProgressChannel: """Classify phase to semantic channel.""" - return ProgressPhaseDeclarationBase.for_phase(phase).channel.require_channel() + return ProgressPhaseDeclarationBase.for_phase(phase).progress_channel.require_channel() def progress_channel_role(channel: ProgressChannel) -> ProgressChannelRole: diff --git a/openhcs/core/source_bindings.py b/openhcs/core/source_bindings.py index 3f55915083..8ff126a170 100644 --- a/openhcs/core/source_bindings.py +++ b/openhcs/core/source_bindings.py @@ -193,29 +193,20 @@ class ImagePlaneSource: index: str | None = None """Optional format-specific plane or image index within the selected series.""" - channel: str | None = None - """Optional source-channel identifier within the selected image or plane.""" + colour_sample: str | None = None + """Optional colour-sample identifier within the selected image or plane.""" def __post_init__(self) -> None: normalized_uri = self.uri.strip() if not normalized_uri: raise ValueError("ImagePlaneSource.uri cannot be empty.") object.__setattr__(self, "uri", normalized_uri) - object.__setattr__( - self, - "series", - _normalized_optional_source_text(self.series), - ) - object.__setattr__( - self, - "index", - _normalized_optional_source_text(self.index), - ) - object.__setattr__( - self, - "channel", - _normalized_optional_source_text(self.channel), - ) + for field_name in ("series", "index", "colour_sample"): + object.__setattr__( + self, + field_name, + _normalized_optional_source_text(getattr(self, field_name)), + ) def resolved(self, source_root: Path) -> ImagePlaneSource: """Return this source with its URI resolved to one verified local file.""" diff --git a/openhcs/core/steps/function_io.py b/openhcs/core/steps/function_io.py index 3a4da75d75..6cacb2ffed 100644 --- a/openhcs/core/steps/function_io.py +++ b/openhcs/core/steps/function_io.py @@ -106,7 +106,7 @@ class ZarrComponentAxisProjection(AxisRoleKeyedStrategyMixin, ABC): axis_order: ClassVar[int] axis_name: ClassVar[str] - axis_type: ClassVar[str] + """NGFF axis name; PolyStore derives its NGFF type.""" axis_role: ClassVar[ZarrBatchAxisRole] = ZarrBatchAxisRole.ARRAY @classmethod @@ -132,7 +132,6 @@ def batch_layout( axes = tuple( ZarrBatchAxis( name=axis_type.axis_name, - axis_type=axis_type.axis_type, values=tuple(dict.fromkeys(item_values)), role=axis_type.axis_role, ) @@ -198,41 +197,37 @@ def item_value( return str(value) -class TimepointZarrAxisProjection(ZarrComponentAxisProjection): +class TimeZarrAxisProjection(ZarrComponentAxisProjection): implements_role = TimeAxis axis_order = 0 axis_name = "t" - axis_type = "time" -class SiteZarrAxisProjection(ZarrComponentAxisProjection): +class TileZarrAxisProjection(ZarrComponentAxisProjection): implements_role = TileAxis axis_order = 1 axis_name = "field" - axis_type = "field" axis_role = ZarrBatchAxisRole.HCS_IMAGE -class ChannelZarrAxisProjection(ZarrComponentAxisProjection): +class ColourZarrAxisProjection(ZarrComponentAxisProjection): implements_role = ColourAxis axis_order = 2 axis_name = "c" - axis_type = "channel" @classmethod def item_value( cls, identity: ZarrBatchItemIdentity, component: type[Axis] ) -> str: - channel = super().item_value(identity, component) + value = super().item_value(identity, component) qualifier = identity.filename_qualifier - return channel if qualifier is None else f"{channel}:{qualifier}" + return value if qualifier is None else f"{value}:{qualifier}" -class ZIndexZarrAxisProjection(ZarrComponentAxisProjection): +class StackZarrAxisProjection(ZarrComponentAxisProjection): implements_role = StackAxis axis_order = 3 axis_name = "z" - axis_type = "space" def zarr_batch_layout( diff --git a/openhcs/core/synthetic_plate_generation.py b/openhcs/core/synthetic_plate_generation.py index ada0f28fec..8dff115ae4 100644 --- a/openhcs/core/synthetic_plate_generation.py +++ b/openhcs/core/synthetic_plate_generation.py @@ -27,7 +27,7 @@ class SyntheticPlateGenerationParameters: z_stack_levels: int = 1 num_cells: int = 80 shared_cell_fraction: float = 0.95 - wells: tuple[str, ...] = ("A01",) + partition_values: tuple[str, ...] = ("A01",) format: SyntheticPlateFormat = SyntheticPlateFormat.IMAGE_XPRESS openhcs_format: bool = False include_all_components: bool = True @@ -47,7 +47,7 @@ class SyntheticPlateGenerationBounds: max_stage_error_px: int = 128 max_wavelengths: int = 8 max_z_stack_levels: int = 16 - max_wells: int = 96 + max_partition_values: int = 96 max_num_cells: int = 10_000 max_sample_file_limit: int = 10_000 @@ -146,15 +146,15 @@ def validate(self, request: SyntheticPlateGenerationParameters) -> None: minimum=0, maximum=self.bounds.max_sample_file_limit, ) - if not request.wells: - raise ValueError("wells must contain at least one well.") - if len(request.wells) > self.bounds.max_wells: + if not request.partition_values: + raise ValueError("partition_values must contain at least one value.") + if len(request.partition_values) > self.bounds.max_partition_values: raise ValueError( - f"wells must not contain more than {self.bounds.max_wells} entries." + f"partition_values must not contain more than {self.bounds.max_partition_values} entries." ) - for well in request.wells: - if not well: - raise ValueError("wells must not contain empty values.") + for value in request.partition_values: + if not value: + raise ValueError("partition_values must not contain empty values.") self.format_from_value(request.format) @staticmethod diff --git a/openhcs/interop/cellprofiler/analyst_export.py b/openhcs/interop/cellprofiler/analyst_export.py index a4db419d1d..61facacee8 100644 --- a/openhcs/interop/cellprofiler/analyst_export.py +++ b/openhcs/interop/cellprofiler/analyst_export.py @@ -287,6 +287,12 @@ def _thumbnail_png_base64(pixels: np.ndarray, *, auto_scale: bool) -> str: return b64encode(output.getvalue()).decode() +PLATE_METADATA_TAG = "Plate" +"""CellProfiler's default metadata tag naming an image's plate.""" +WELL_METADATA_TAG = "Well" +"""CellProfiler's default metadata tag naming an image's well.""" + + @dataclass(frozen=True, slots=True) class CellProfilerDatabaseExportSettings: """Subset of CellProfiler ExportToDatabase settings needed for CPA projection.""" @@ -303,8 +309,8 @@ class CellProfilerDatabaseExportSettings: maximum_column_name_length: int = 64 location_object: str | None = None plate_type: str | None = None - plate_metadata: str = "Plate" - well_metadata: str = "Well" + plate_metadata: str = PLATE_METADATA_TAG + well_metadata: str = WELL_METADATA_TAG image_url_prepend: str = "" group_fields: tuple[tuple[str, str], ...] = () classification_type: Literal["object", "image"] = "object" diff --git a/openhcs/interop/cellprofiler/parser.py b/openhcs/interop/cellprofiler/parser.py index 9beee06540..81503e3a68 100644 --- a/openhcs/interop/cellprofiler/parser.py +++ b/openhcs/interop/cellprofiler/parser.py @@ -374,7 +374,7 @@ def _parse_image_plane_details( uri=uri, series=values.get("Series"), index=values.get("Index"), - channel=values.get("Channel"), + colour_sample=values.get("Channel"), ) ) if len(plane_sources) == expected_count: diff --git a/openhcs/mcp/dev_client_commands/plate.py b/openhcs/mcp/dev_client_commands/plate.py index a3b460a580..75dba016f6 100644 --- a/openhcs/mcp/dev_client_commands/plate.py +++ b/openhcs/mcp/dev_client_commands/plate.py @@ -101,10 +101,10 @@ def configure_parser(self, parser: argparse.ArgumentParser) -> None: "--shared-cell-fraction", ) parser.add_argument( - "--well", - dest="wells", + "--partition-value", + dest="partition_values", action="append", - help="Well ID to generate. Repeat for multiple wells.", + help="Partition value to generate. Repeat for several.", ) add_request_field_option( parser, @@ -162,7 +162,7 @@ def tool_arguments( z_stack_levels=args.z_stack_levels, num_cells=args.num_cells, shared_cell_fraction=args.shared_cell_fraction, - wells=args.wells, + partition_values=args.partition_values, format=args.format, openhcs_format=args.openhcs_format, include_all_components=args.include_all_components, @@ -271,7 +271,7 @@ def configure_parser(self, parser: argparse.ArgumentParser) -> None: "path_contains", "--path-contains", ) - add_request_field_option(parser, PlateFileQueryRequest, "well", "--well") + add_request_field_option(parser, PlateFileQueryRequest, "partition", "--partition") add_request_field_option(parser, PlateFileQueryRequest, "offset", "--offset") add_request_field_option(parser, PlateFileQueryRequest, "limit", "--limit") add_request_field_option( @@ -312,7 +312,7 @@ def tool_arguments( pattern_format=args.pattern_format, kind=args.kind, path_contains=args.path_contains, - well=args.well, + partition=args.partition, offset=args.offset, limit=args.limit, include_previews=args.include_previews, @@ -481,7 +481,7 @@ def configure_parser( help="Plate root to stream from the selected PlateManager row.", ) parser.add_argument("--path-contains") - parser.add_argument("--well") + parser.add_argument("--partition") add_request_field_option( parser, request_type, @@ -567,7 +567,7 @@ def tool_arguments( args.file_paths, ), path_contains=args.path_contains, - well=args.well, + partition=args.partition, limit=args.limit, viewer_config_key=args.viewer_config_key, host=args.viewer_host, @@ -695,8 +695,8 @@ def configure_parser(self, parser: argparse.ArgumentParser) -> None: add_request_field_option( parser, SelectedPlateFileQueryRequest, - "well", - "--well", + "partition", + "--partition", ) add_request_field_option( parser, @@ -749,7 +749,7 @@ def tool_arguments( kind=args.kind, target=args.target, path_contains=args.path_contains, - well=args.well, + partition=args.partition, offset=args.offset, limit=args.limit, include_previews=args.include_previews, @@ -912,7 +912,7 @@ def tool_arguments( ), target=args.target, path_contains=args.path_contains, - well=args.well, + partition=args.partition, limit=args.limit, viewer_config_key=args.viewer_config_key, host=args.viewer_host, diff --git a/openhcs/mcp/dev_client_renderers/plate.py b/openhcs/mcp/dev_client_renderers/plate.py index 834cb72da2..351d443a66 100644 --- a/openhcs/mcp/dev_client_renderers/plate.py +++ b/openhcs/mcp/dev_client_renderers/plate.py @@ -351,7 +351,7 @@ def render_payload( ), ( "Content: " - f"wells={cls.sequence_text(payload.wells)} " + f"partition_values={cls.sequence_text(payload.partition_values)} " f"channels={payload.wavelengths} " f"z={payload.z_stack_levels} " f"cells={payload.num_cells} " diff --git a/openhcs/mcp/installed_demo.py b/openhcs/mcp/installed_demo.py index be4ab90cb6..67aac2a0f8 100644 --- a/openhcs/mcp/installed_demo.py +++ b/openhcs/mcp/installed_demo.py @@ -390,7 +390,7 @@ def _generate_plate( "12", "--shared-cell-fraction", "0.95", - "--well", + "--partition-value", "A01", "--format", "ImageXpress", @@ -414,7 +414,7 @@ def _query_plate(client: McpDevClient, plate_path: Path) -> PlateFileQueryResult str(plate_path), "--kind", "image", - "--well", + "--partition", "A01", "--limit", "10", diff --git a/openhcs/processing/backends/cellprofiler/color.py b/openhcs/processing/backends/cellprofiler/color.py index 951ffd7a5e..959db70581 100644 --- a/openhcs/processing/backends/cellprofiler/color.py +++ b/openhcs/processing/backends/cellprofiler/color.py @@ -122,7 +122,7 @@ def __new__(cls, value: str, projection: type[ColorToGrayScalarOutputProjection] RGB = ("rgb", SelectedColorChannelProjection) HSV = ("hsv", DerivedColorChannelProjection) - CHANNELS = ("channels", SelectedColorChannelProjection) + CHANNELS = ("numbered_channels", SelectedColorChannelProjection) @property def scalar_output_projection(self) -> ColorToGrayScalarOutputProjection: @@ -162,11 +162,11 @@ class FixedChannel: @dataclass(frozen=True, slots=True) class FixedImageType: output_offset: int - channels: tuple["ColorToGrayModule.FixedChannel", ...] + fixed_channels: tuple["ColorToGrayModule.FixedChannel", ...] rgb_fixed_image_type = FixedImageType( output_offset=1, - channels=( + fixed_channels=( FixedChannel( "Convert red to gray?", "Red", @@ -186,13 +186,13 @@ class FixedImageType: ) hsv_fixed_image_type = FixedImageType( output_offset=4, - channels=( + fixed_channels=( FixedChannel("Convert hue to gray?", "Hue"), FixedChannel("Convert saturation to gray?", "Saturation"), FixedChannel("Convert value to gray?", "Value"), ), ) - default_channel_indices = tuple(range(len(rgb_fixed_image_type.channels))) + default_channel_indices = tuple(range(len(rgb_fixed_image_type.fixed_channels))) input_image_binding = SettingToKeywordBinding.input( input_image_setting, ImageArtifactType @@ -381,14 +381,14 @@ def _canonical_output_names( return tuple(f"{base_name}Channel{index + 1}" for index in channel_indices) declaration = cls.fixed_image_type(image_type) if any( - index < 0 or index >= len(declaration.channels) for index in channel_indices + index < 0 or index >= len(declaration.fixed_channels) for index in channel_indices ): raise ValueError( f"ColorToGray channel indices must address {image_type.value} " f"channels, got {channel_indices!r}." ) return tuple( - f"{base_name}{declaration.channels[index].output_suffix}" + f"{base_name}{declaration.fixed_channels[index].output_suffix}" for index in channel_indices ) @@ -400,7 +400,7 @@ def _compound_setting_names(cls) -> tuple[str, ...]: cls.channel_weight_setting, *( channel.weight_setting - for channel in cls.rgb_fixed_image_type.channels + for channel in cls.rgb_fixed_image_type.fixed_channels if channel.weight_setting is not None ), *( @@ -409,7 +409,7 @@ def _compound_setting_names(cls) -> tuple[str, ...]: cls.rgb_fixed_image_type, cls.hsv_fixed_image_type, ) - for channel in declaration.channels + for channel in declaration.fixed_channels ), ) @@ -489,7 +489,7 @@ def output_image_names( output_names = setting_values(module, cls.output_image_setting) output_flag_values = tuple( optional_setting_value(module, channel.output_flag) - for channel in declaration.channels + for channel in declaration.fixed_channels ) if all(value is None for value in output_flag_values): if not output_names: @@ -501,7 +501,7 @@ def output_image_names( enabled_indices = tuple( index for index, (channel, value) in enumerate( - zip(declaration.channels, output_flag_values, strict=True) + zip(declaration.fixed_channels, output_flag_values, strict=True) ) if value is not None and cls.flag_enabled(module, binder, channel.output_flag) @@ -511,7 +511,7 @@ def output_image_names( f"ColorToGray({module.module_num}) split mode must declare at least one enabled output channel." ) full_cellprofiler_row_count = declaration.output_offset + len( - declaration.channels + declaration.fixed_channels ) if len(output_names) >= full_cellprofiler_row_count: return tuple( @@ -568,7 +568,7 @@ def channel_indices( declaration = cls.fixed_image_type(image_type) return tuple( index - for index, channel in enumerate(declaration.channels) + for index, channel in enumerate(declaration.fixed_channels) if cls.flag_enabled(module, binder, channel.output_flag) ) @@ -590,7 +590,7 @@ def contributions( setting, required_setting_value(module, setting) ) ) - for channel in cls.rgb_fixed_image_type.channels + for channel in cls.rgb_fixed_image_type.fixed_channels if (setting := channel.weight_setting) is not None ) ) @@ -2310,7 +2310,7 @@ class Channel: "blue_output_name", ), ) - channels = (red_channel, green_channel, blue_channel) + rgb_channels = (red_channel, green_channel, blue_channel) color_input_binding = SettingToKeywordBinding.input( color_input_setting, ImageArtifactType, @@ -2321,9 +2321,9 @@ class Channel: "color_output_name", ) setting_bindings: ClassVar[tuple[SettingToKeywordBinding, ...]] = ( - *(channel.input_binding for channel in channels), + *(channel.input_binding for channel in rgb_channels), color_input_binding, - *(channel.output_binding for channel in channels), + *(channel.output_binding for channel in rgb_channels), color_output_binding, SettingToKeywordBinding( input_mode_setting, @@ -2337,7 +2337,7 @@ class Channel: parse_cellprofiler_bool, ) for channel, parameter_name in zip( - channels, + rgb_channels, ("use_red_input", "use_green_input", "use_blue_input"), strict=True, ) @@ -2354,7 +2354,7 @@ class Channel: parse_cellprofiler_bool, ) for channel, parameter_name in zip( - channels, + rgb_channels, ("output_red", "output_green", "output_blue"), strict=True, ) @@ -2388,7 +2388,7 @@ def active_artifact_bindings(cls, module=None, *, invocation_key=None): else: active_inputs = tuple( channel.input_binding - for channel in cls.channels + for channel in cls.rgb_channels if parse_cellprofiler_bool( required_setting_value(module, channel.input_flag_setting) ) @@ -2403,7 +2403,7 @@ def active_artifact_bindings(cls, module=None, *, invocation_key=None): else: active_outputs = tuple( channel.output_binding - for channel in cls.channels + for channel in cls.rgb_channels if parse_cellprofiler_bool( required_setting_value(module, channel.output_flag_setting) ) @@ -2456,7 +2456,7 @@ def resolve_function( parse_cellprofiler_bool( required_setting_value(module, channel.output_flag_setting) ) - for channel in cls.channels + for channel in cls.rgb_channels ) return cls.require_callable( cls.function_variants[0] diff --git a/openhcs/processing/backends/cellprofiler/display_modules.py b/openhcs/processing/backends/cellprofiler/display_modules.py index a6cec68fcf..1312c914a8 100644 --- a/openhcs/processing/backends/cellprofiler/display_modules.py +++ b/openhcs/processing/backends/cellprofiler/display_modules.py @@ -918,7 +918,7 @@ class PlatemapData: """Aggregated measurement data for plate map visualization.""" plate: str - well: str + well_name: str row: str column: str value: float @@ -1135,7 +1135,7 @@ def display_platemap( platemap_entries.append( PlatemapData( plate=plate, - well=well, + well_name=well, row=row, column=col, value=aggregated, diff --git a/openhcs/processing/backends/cellprofiler/export_to_database.py b/openhcs/processing/backends/cellprofiler/export_to_database.py index 437babf9a1..bff5f51f8e 100644 --- a/openhcs/processing/backends/cellprofiler/export_to_database.py +++ b/openhcs/processing/backends/cellprofiler/export_to_database.py @@ -37,6 +37,8 @@ CellProfilerAnalystProjectionBuilder, CellProfilerDatabaseExportSettings, CellProfilerObjectTableMode, + PLATE_METADATA_TAG, + WELL_METADATA_TAG, ) from openhcs.interop.cellprofiler.workspace_export import ( CPAWorkspacePanel, @@ -943,8 +945,8 @@ def export_to_database( thumbnail_image_names: tuple[str, ...] = (), auto_scale_thumbnail_intensities: bool = True, plate_type: str | None = None, - plate_metadata: str = "Plate", - well_metadata: str = "Well", + plate_metadata: str = PLATE_METADATA_TAG, + well_metadata: str = WELL_METADATA_TAG, wants_group_fields: bool = False, group_fields: tuple[tuple[str, str], ...] = (), phenotype_class_table: str = "", diff --git a/openhcs/processing/backends/cellprofiler/infrastructure.py b/openhcs/processing/backends/cellprofiler/infrastructure.py index 6da4215e7f..8fe2e848fd 100644 --- a/openhcs/processing/backends/cellprofiler/infrastructure.py +++ b/openhcs/processing/backends/cellprofiler/infrastructure.py @@ -1149,7 +1149,7 @@ def _image_plane_source(value: str) -> ImagePlaneSource: uri=uri, series=series or None, index=index or None, - channel=channel or None, + colour_sample=channel or None, ) diff --git a/openhcs/processing/backends/cellprofiler/save_images.py b/openhcs/processing/backends/cellprofiler/save_images.py index b6b366b32b..ea2b39558f 100644 --- a/openhcs/processing/backends/cellprofiler/save_images.py +++ b/openhcs/processing/backends/cellprofiler/save_images.py @@ -160,10 +160,10 @@ class SaveImagesWhen(str, Enum): class SaveImagesSeriesAxis(str, Enum): - """Source component used by sequential SaveImages filenames.""" + """CellProfiler movie dimension that numbers sequential SaveImages files.""" - TIMEPOINT = "timepoint" - Z_INDEX = "z_index" + TIME = "time" + SLICE = "slice" class SaveImagesOutputLocation(str, Enum): @@ -220,8 +220,8 @@ def relative_directory_from_literal(cls, value: str | None) -> str | None: SaveImagesWhen.EVERY_CYCLE.cellprofiler_literals = ("Every cycle",) SaveImagesWhen.FIRST_CYCLE.cellprofiler_literals = ("First cycle",) SaveImagesWhen.LAST_CYCLE.cellprofiler_literals = ("Last cycle",) -SaveImagesSeriesAxis.TIMEPOINT.cellprofiler_literals = ("T (Time)",) -SaveImagesSeriesAxis.Z_INDEX.cellprofiler_literals = ("Z (Slice)",) +SaveImagesSeriesAxis.TIME.cellprofiler_literals = ("T (Time)",) +SaveImagesSeriesAxis.SLICE.cellprofiler_literals = ("Z (Slice)",) SaveImagesOutputLocation.DEFAULT_OUTPUT_FOLDER.cellprofiler_literals = ( "Default Output Folder", ) @@ -996,7 +996,7 @@ def save_images( when_to_save: SaveImagesWhen = SaveImagesWhen.EVERY_CYCLE, create_subfolders: bool = False, base_image_folder: str | None = None, - series_axis: SaveImagesSeriesAxis = SaveImagesSeriesAxis.TIMEPOINT, + series_axis: SaveImagesSeriesAxis = SaveImagesSeriesAxis.TIME, lossless_compression: bool = True, context: ProcessingContext | None = None, ) -> tuple[RuntimeArrayData, RuntimeArrayData]: @@ -1053,7 +1053,7 @@ def save_images_with_measurements( when_to_save: SaveImagesWhen = SaveImagesWhen.EVERY_CYCLE, create_subfolders: bool = False, base_image_folder: str | None = None, - series_axis: SaveImagesSeriesAxis = SaveImagesSeriesAxis.TIMEPOINT, + series_axis: SaveImagesSeriesAxis = SaveImagesSeriesAxis.TIME, lossless_compression: bool = True, slice_index: int = 0, context: ProcessingContext | None = None, diff --git a/openhcs/processing/backends/experimental_analysis/__init__.py b/openhcs/processing/backends/experimental_analysis/__init__.py index 3b45e246f5..5827f5901b 100644 --- a/openhcs/processing/backends/experimental_analysis/__init__.py +++ b/openhcs/processing/backends/experimental_analysis/__init__.py @@ -5,7 +5,7 @@ data from the workbook-declared CX5 or MetaXpress result format. Result-format behavior is selected through the scope-keyed strategies in -``openhcs.formats.experimental_result_formats``. The workbook scope remains the +``result_formats``. The workbook scope remains the single declaration of its input format. """ diff --git a/openhcs/formats/experimental_analysis.py b/openhcs/processing/backends/experimental_analysis/analysis.py similarity index 99% rename from openhcs/formats/experimental_analysis.py rename to openhcs/processing/backends/experimental_analysis/analysis.py index bf080dc25e..ee871bb570 100755 --- a/openhcs/formats/experimental_analysis.py +++ b/openhcs/processing/backends/experimental_analysis/analysis.py @@ -22,11 +22,11 @@ from typing import Optional from openhcs.domains.microscopy.config import NormalizationMethod -from openhcs.formats.experimental_layout_rows import ( +from openhcs.processing.backends.experimental_analysis.layout_rows import ( ExperimentalAnalysisScope, ExperimentalLayoutRowRole, ) -from openhcs.formats.experimental_result_formats import ExperimentalResultFormatStrategy +from openhcs.processing.backends.experimental_analysis.result_formats import ExperimentalResultFormatStrategy @dataclass(frozen=True, slots=True) diff --git a/openhcs/formats/experimental_layout_rows.py b/openhcs/processing/backends/experimental_analysis/layout_rows.py similarity index 100% rename from openhcs/formats/experimental_layout_rows.py rename to openhcs/processing/backends/experimental_analysis/layout_rows.py diff --git a/openhcs/formats/experimental_result_formats.py b/openhcs/processing/backends/experimental_analysis/result_formats.py similarity index 98% rename from openhcs/formats/experimental_result_formats.py rename to openhcs/processing/backends/experimental_analysis/result_formats.py index 5e8ad7f97b..d3039f5574 100644 --- a/openhcs/formats/experimental_result_formats.py +++ b/openhcs/processing/backends/experimental_analysis/result_formats.py @@ -11,7 +11,7 @@ import pandas as pd from metaclass_registry.strategies import EnumKeyedStrategyMixin -from openhcs.formats.experimental_layout_rows import ExperimentalAnalysisScope +from openhcs.processing.backends.experimental_analysis.layout_rows import ExperimentalAnalysisScope PlateDictionary = dict[str, dict[str, dict[str, Any]]] diff --git a/openhcs/processing/backends/experimental_analysis/unified_analysis_engine.py b/openhcs/processing/backends/experimental_analysis/unified_analysis_engine.py index da170261d2..b1b2487cf7 100644 --- a/openhcs/processing/backends/experimental_analysis/unified_analysis_engine.py +++ b/openhcs/processing/backends/experimental_analysis/unified_analysis_engine.py @@ -4,8 +4,8 @@ from typing import Dict, List, Any, Optional, Tuple from openhcs.domains.microscopy.config import ExperimentalAnalysisConfig -from openhcs.formats.experimental_layout_rows import ExperimentalAnalysisScope -from openhcs.formats.experimental_result_formats import ( +from openhcs.processing.backends.experimental_analysis.layout_rows import ExperimentalAnalysisScope +from openhcs.processing.backends.experimental_analysis.result_formats import ( ExperimentalResultFormatStrategy, ) @@ -218,7 +218,7 @@ def _read_plate_layout( Returns: Tuple of (scope, plate_layout, conditions, ctrl_positions, excluded_positions, per_well_datapoints) """ - from openhcs.formats.experimental_analysis import read_plate_layout + from openhcs.processing.backends.experimental_analysis.analysis import read_plate_layout return read_plate_layout( config_path, @@ -235,7 +235,7 @@ def _load_plate_groups(self, config_path: str) -> Dict: Returns: Plate groups dictionary """ - from openhcs.formats.experimental_analysis import load_plate_groups + from openhcs.processing.backends.experimental_analysis.analysis import load_plate_groups return load_plate_groups( config_path, @@ -246,7 +246,7 @@ def _make_experiment_dict_locations( self, plate_groups: Dict, plate_layout: Dict, conditions: List ) -> Dict: """Create experiment location mapping.""" - from openhcs.formats.experimental_analysis import make_experiment_dict_locations + from openhcs.processing.backends.experimental_analysis.analysis import make_experiment_dict_locations return make_experiment_dict_locations(plate_groups, plate_layout, conditions) @@ -259,7 +259,7 @@ def _make_experiment_dict_values( per_well_datapoints: bool = False, ) -> Dict: """Map experimental design to measured values.""" - from openhcs.formats.experimental_analysis import make_experiment_dict_values + from openhcs.processing.backends.experimental_analysis.analysis import make_experiment_dict_values return make_experiment_dict_values( plates_dict, @@ -276,7 +276,7 @@ def _apply_exclusions( excluded_positions: Optional[Dict], ) -> None: """Apply the parser-owned exclusion projection to analysis locations.""" - from openhcs.formats.experimental_analysis import ( + from openhcs.processing.backends.experimental_analysis.analysis import ( apply_excluded_positions_to_control_positions, apply_excluded_positions_to_experiment_locations, ) @@ -299,7 +299,7 @@ def _normalize_experiment( plate_groups: Dict, ) -> Dict: """Apply normalization using control wells.""" - from openhcs.formats.experimental_analysis import normalize_experiment + from openhcs.processing.backends.experimental_analysis.analysis import normalize_experiment return normalize_experiment( experiment_dict_values, @@ -317,7 +317,7 @@ def _create_all_feature_tables( per_well_datapoints: bool = False, ) -> Dict: """Create feature tables for export.""" - from openhcs.formats.experimental_analysis import create_all_feature_tables + from openhcs.processing.backends.experimental_analysis.analysis import create_all_feature_tables return create_all_feature_tables( experiment_dict_values, features, per_well_datapoints @@ -325,7 +325,7 @@ def _create_all_feature_tables( def _export_results(self, feature_tables: Dict, output_path: str): """Export results to Excel file.""" - from openhcs.formats.experimental_analysis import feature_tables_to_excel + from openhcs.processing.backends.experimental_analysis.analysis import feature_tables_to_excel feature_tables_to_excel(feature_tables, output_path) @@ -338,7 +338,7 @@ def _export_heatmaps( output_path: str, ) -> None: """Export conditionally formatted plate grids after exclusions.""" - from openhcs.formats.experimental_analysis import ( + from openhcs.processing.backends.experimental_analysis.analysis import ( project_plates_without_excluded_positions, write_values_heat_map, ) diff --git a/openhcs/runtime/fiji_viewer_server.py b/openhcs/runtime/fiji_viewer_server.py index 5ae570cc1d..3658337c5e 100644 --- a/openhcs/runtime/fiji_viewer_server.py +++ b/openhcs/runtime/fiji_viewer_server.py @@ -230,9 +230,9 @@ def _require_active_route(self, route: str) -> None: class FijiDimensionStorage: """Stored ImageJ C/Z/T domains for one Fiji window.""" - channel: FijiDimensionValues - z_axis_values: FijiDimensionValues - frame: FijiDimensionValues + c: FijiDimensionValues + z: FijiDimensionValues + t: FijiDimensionValues @dataclass(frozen=True, slots=True) @@ -614,28 +614,15 @@ class FijiStackSliceLabelBuilder: coordinates: "FijiHyperstackCoordinates" def label_for(self, key: FijiCoordinateKey) -> str: - c_key, z_key, t_key = key - label_parts = [] - self._append_axis_label(label_parts, "C", c_key, self.coordinates.channel) - self._append_axis_label( - label_parts, "Z", z_key, self.coordinates.z_axis_coordinates - ) - self._append_axis_label(label_parts, "T", t_key, self.coordinates.frame) + label_parts = [ + f"{axis.name.upper()}{'_'.join(str(value) for value in axis_key)}" + for axis_key, axis in zip(key, self.coordinates.axes(), strict=True) + if axis.components + ] if label_parts: return "_".join(label_parts) return "slice" - @staticmethod - def _append_axis_label( - label_parts: list[str], - prefix: str, - axis_key: tuple, - axis: "FijiDimensionAxis", - ) -> None: - if not axis.components: - return - label_parts.append(f"{prefix}{'_'.join(str(value) for value in axis_key)}") - @dataclass(frozen=True, slots=True) class FijiImageStackBuilder: @@ -659,9 +646,9 @@ def build( label_builder = FijiStackSliceLabelBuilder(self.coordinates) pending_plane_count = 0 - for t_key in self.coordinates.frame.values: - for z_key in self.coordinates.z_axis_coordinates.values: - for c_key in self.coordinates.channel.values: + for t_key in self.coordinates.t.values: + for z_key in self.coordinates.z.values: + for c_key in self.coordinates.c.values: key = (c_key, z_key, t_key) if self.image_lookup.contains(key): processor = self._processor_for_key(key) @@ -862,25 +849,25 @@ def labels_for_position( class FijiHyperstackCoordinateComponents: """OpenHCS components assigned to ImageJ C/Z/T dimensions.""" - channel: FijiDimensionComponents - z_axis_components: FijiDimensionComponents - frame: FijiDimensionComponents + c: FijiDimensionComponents + z: FijiDimensionComponents + t: FijiDimensionComponents def collect(self, items: Sequence[FijiWireItem]) -> "FijiHyperstackCoordinates": return FijiHyperstackCoordinates( - channel=FijiDimensionAxis.collect( - name="channel", - components=self.channel, + c=FijiDimensionAxis.collect( + name="c", + components=self.c, items=items, ), - z_axis_coordinates=FijiDimensionAxis.collect( - name="slice", - components=self.z_axis_components, + z=FijiDimensionAxis.collect( + name="z", + components=self.z, items=items, ), - frame=FijiDimensionAxis.collect( - name="frame", - components=self.frame, + t=FijiDimensionAxis.collect( + name="t", + components=self.t, items=items, ), ) @@ -890,19 +877,19 @@ def collect_images( images: Sequence[FijiImagePayload], ) -> "FijiHyperstackCoordinates": return FijiHyperstackCoordinates( - channel=FijiDimensionAxis.collect_from_images( - name="channel", - components=self.channel, + c=FijiDimensionAxis.collect_from_images( + name="c", + components=self.c, images=images, ), - z_axis_coordinates=FijiDimensionAxis.collect_from_images( - name="z_axis", - components=self.z_axis_components, + z=FijiDimensionAxis.collect_from_images( + name="z", + components=self.z, images=images, ), - frame=FijiDimensionAxis.collect_from_images( - name="frame", - components=self.frame, + t=FijiDimensionAxis.collect_from_images( + name="t", + components=self.t, images=images, ), ) @@ -912,68 +899,57 @@ def collect_images( class FijiHyperstackCoordinates: """Complete ImageJ C/Z/T coordinate domain for one Fiji window.""" - channel: FijiDimensionAxis - z_axis_coordinates: FijiDimensionAxis - frame: FijiDimensionAxis + c: FijiDimensionAxis + z: FijiDimensionAxis + t: FijiDimensionAxis + + def axes(self) -> tuple[FijiDimensionAxis, FijiDimensionAxis, FijiDimensionAxis]: + """The hyperstack dimensions in ImageJ's C, Z, T order.""" + + return (self.c, self.z, self.t) def merge_storage( self, stored: FijiDimensionStorage, ) -> "FijiHyperstackCoordinates": return FijiHyperstackCoordinates( - channel=self.channel.merge_values(stored.channel), - z_axis_coordinates=self.z_axis_coordinates.merge_values( - stored.z_axis_values - ), - frame=self.frame.merge_values(stored.frame), + c=self.c.merge_values(stored.c), + z=self.z.merge_values(stored.z), + t=self.t.merge_values(stored.t), ) def storage(self) -> FijiDimensionStorage: return FijiDimensionStorage( - channel=self.channel.values, - z_axis_values=self.z_axis_coordinates.values, - frame=self.frame.values, + c=self.c.values, + z=self.z.values, + t=self.t.values, ) def dimensions(self) -> tuple[int, int, int]: - return ( - len(self.channel.values), - len(self.z_axis_coordinates.values), - len(self.frame.values), - ) + return tuple(len(axis.values) for axis in self.axes()) def key(self, metadata: Mapping[str, ComponentValue]) -> FijiCoordinateKey: - return ( - self.channel.value_tuple(metadata), - self.z_axis_coordinates.value_tuple(metadata), - self.frame.value_tuple(metadata), - ) + return tuple(axis.value_tuple(metadata) for axis in self.axes()) def imagej_position( self, metadata: Mapping[str, ComponentValue], ) -> tuple[int, int, int]: - return ( - self.channel.index(metadata) + 1, - self.z_axis_coordinates.index(metadata) + 1, - self.frame.index(metadata) + 1, - ) + return tuple(axis.index(metadata) + 1 for axis in self.axes()) def stack_index(self, key: FijiCoordinateKey) -> int: """Return the 1-based ImageJ stack position for a C/Z/T key.""" c_key, z_key, t_key = key - c_idx = self.channel.values.index(c_key) - z_idx = self.z_axis_coordinates.values.index(z_key) - t_idx = self.frame.values.index(t_key) + c_idx = self.c.values.index(c_key) + z_idx = self.z.values.index(z_key) + t_idx = self.t.values.index(t_key) n_channels, n_slices, _ = self.dimensions() return (t_idx * n_slices * n_channels) + (z_idx * n_channels) + c_idx + 1 def contains_axis_values(self, key: FijiCoordinateKey) -> bool: - c_key, z_key, t_key = key - return ( - c_key in self.channel.values - and z_key in self.z_axis_coordinates.values - and t_key in self.frame.values + return all( + axis_key in axis.values + for axis_key, axis in zip(key, self.axes(), strict=True) ) def label_text_for_position( @@ -988,19 +964,19 @@ def label_text_for_position( for component, value in fixed_labels ] labels.extend( - self.channel.labels_for_position( + self.c.labels_for_position( imp.getChannel(), component_names_metadata, ) ) labels.extend( - self.z_axis_coordinates.labels_for_position( + self.z.labels_for_position( imp.getSlice(), component_names_metadata, ) ) labels.extend( - self.frame.labels_for_position( + self.t.labels_for_position( imp.getFrame(), component_names_metadata, ) @@ -1025,10 +1001,10 @@ def expanded_dimensions(self) -> bool: if self.stored is None: return False return ( - len(self.coordinates.channel.values) > len(self.stored.channel) - or len(self.coordinates.z_axis_coordinates.values) - > len(self.stored.z_axis_values) - or len(self.coordinates.frame.values) > len(self.stored.frame) + len(self.coordinates.c.values) > len(self.stored.c) + or len(self.coordinates.z.values) + > len(self.stored.z) + or len(self.coordinates.t.values) > len(self.stored.t) ) @@ -1419,9 +1395,9 @@ def from_items( return cls( window_components=projection.window_components, coordinate_components=FijiHyperstackCoordinateComponents( - channel=layout.components_in(FijiSlots.HyperstackChannel), - z_axis_components=layout.components_in(FijiSlots.HyperstackSlice), - frame=layout.components_in(FijiSlots.HyperstackFrame), + c=layout.components_in(FijiSlots.HyperstackChannel), + z=layout.components_in(FijiSlots.HyperstackSlice), + t=layout.components_in(FijiSlots.HyperstackFrame), ), windows=projection.windows, fixed_window_labels=projection.fixed_window_labels, @@ -1437,9 +1413,9 @@ def log_summary(self) -> None: ) logger.info("🗂️ FIJI SERVER: Dimension mapping:") logger.info(" WINDOW: %s", self.window_components) - logger.info(" CHANNEL: %s", self.coordinate_components.channel) - logger.info(" SLICE: %s", self.coordinate_components.z_axis_components) - logger.info(" FRAME: %s", self.coordinate_components.frame) + logger.info(" CHANNEL: %s", self.coordinate_components.c) + logger.info(" SLICE: %s", self.coordinate_components.z) + logger.info(" FRAME: %s", self.coordinate_components.t) @dataclass(slots=True) @@ -1641,9 +1617,9 @@ def process_window_group_locked( if coordinate_resolution.expanded_dimensions(): logger.info( f"🔬 FIJI SERVER: Expanded dimensions for window '{request.window_key}': " - f"{len(stored.channel)}→{len(coordinates.channel.values)}C, " - f"{len(stored.z_axis_values)}→{len(coordinates.z_axis_coordinates.values)}Z, " - f"{len(stored.frame)}→{len(coordinates.frame.values)}T" + f"{len(stored.c)}→{len(coordinates.c.values)}C, " + f"{len(stored.z)}→{len(coordinates.z.values)}Z, " + f"{len(stored.t)}→{len(coordinates.t.values)}T" ) else: logger.info( @@ -1652,9 +1628,9 @@ def process_window_group_locked( else: logger.info( f"🔬 FIJI SERVER: First batch for window '{request.window_key}': " - f"{len(coordinates.channel.values)}C x " - f"{len(coordinates.z_axis_coordinates.values)}Z x " - f"{len(coordinates.frame.values)}T" + f"{len(coordinates.c.values)}C x " + f"{len(coordinates.z.values)}Z x " + f"{len(coordinates.t.values)}T" ) self.server.windows.store_fixed_labels(request.window_key, request.fixed_labels) @@ -1932,9 +1908,9 @@ def _add_slices_to_existing_hyperstack( # Collect dimension values from existing images existing_coordinates = FijiHyperstackCoordinateComponents( - channel=coordinates.channel.components, - z_axis_components=coordinates.z_axis_coordinates.components, - frame=coordinates.frame.components, + c=coordinates.c.components, + z=coordinates.z.components, + t=coordinates.t.components, ).collect_images(existing_images) # Process new images and check whether the coordinate domain changed. @@ -2511,27 +2487,27 @@ def _set_dimension_labels( """ try: logger.info( - f"🏷️ FIJI SERVER: _set_dimension_labels called with {len(coordinates.channel.values)} channels" + f"🏷️ FIJI SERVER: _set_dimension_labels called with {len(coordinates.c.values)} channels" ) logger.info( - f"🏷️ FIJI SERVER: channel axis components = {coordinates.channel.components}" + f"🏷️ FIJI SERVER: channel axis components = {coordinates.c.components}" ) logger.info( f"🏷️ FIJI SERVER: component_names_metadata = {component_names_metadata}" ) # Set channel labels - if coordinates.channel.components and coordinates.channel.values: + if coordinates.c.components and coordinates.c.values: logger.info( - f"🏷️ FIJI SERVER: Setting labels for {len(coordinates.channel.values)} channels" + f"🏷️ FIJI SERVER: Setting labels for {len(coordinates.c.values)} channels" ) for idx, channel_tuple in enumerate( - coordinates.channel.values, start=1 + coordinates.c.values, start=1 ): - label = coordinates.channel.label_for_value( + label = coordinates.c.label_for_value( channel_tuple, component_names_metadata, - fallback_label=f"Ch{idx}", + fallback_label=f"{coordinates.c.name.upper()}{idx}", ) imp.setProperty(f"Label{idx}", label) logger.info( @@ -2637,11 +2613,11 @@ def _build_new_hyperstack( # For single-channel images, add channel name to window title since no slider appears if ( nChannels == 1 - and coordinates.channel.components - and coordinates.channel.values + and coordinates.c.components + and coordinates.c.values ): - first_comp = coordinates.channel.components[0] - first_value_tuple = coordinates.channel.values[0] + first_comp = coordinates.c.components[0] + first_value_tuple = coordinates.c.values[0] channel_name = component_names_metadata.display_name( first_comp, first_value_tuple[0], @@ -2844,11 +2820,11 @@ def handle(self, request: FijiPayloadHandlerRequest) -> None: file_path = roi_item.path logger.info(f"🔬 FIJI SERVER: ROI metadata: {metadata}") - logger.info(f"🔬 FIJI SERVER: Channel axis: {request.coordinates.channel}") + logger.info(f"🔬 FIJI SERVER: Channel axis: {request.coordinates.c}") logger.info( - f"🔬 FIJI SERVER: Slice axis: {request.coordinates.z_axis_coordinates}" + f"🔬 FIJI SERVER: Slice axis: {request.coordinates.z}" ) - logger.info(f"🔬 FIJI SERVER: Frame axis: {request.coordinates.frame}") + logger.info(f"🔬 FIJI SERVER: Frame axis: {request.coordinates.t}") c_value, z_value, t_value = request.coordinates.imagej_position(metadata) diff --git a/openhcs/runtime/zmq_compilation.py b/openhcs/runtime/zmq_compilation.py index bbc9c52914..0f2b691597 100644 --- a/openhcs/runtime/zmq_compilation.py +++ b/openhcs/runtime/zmq_compilation.py @@ -76,7 +76,7 @@ class ZMQCompilationRequest: pipeline_steps: list[AbstractStep] orchestrator: "PipelineOrchestrator" resolved_config: GlobalPipelineConfig | None - wells: list[str] + partition_values: list[str] compile_artifact_id: str | None compilation_signature: str debug_replay_signature: str @@ -155,7 +155,7 @@ def compile_fresh(self) -> ZMQCompilationResult: ): execution_bundle = self.orchestrator.compile_pipelines( pipeline_definition=self.pipeline_steps, - well_filter=self.wells, + well_filter=self.partition_values, is_zmq_execution=True, debug_execution_policy=self.debug_execution_policy, resolved_config=self.resolved_config, diff --git a/openhcs/runtime/zmq_execution_server.py b/openhcs/runtime/zmq_execution_server.py index ca88cb702b..c9335ddd80 100644 --- a/openhcs/runtime/zmq_execution_server.py +++ b/openhcs/runtime/zmq_execution_server.py @@ -579,7 +579,7 @@ def _execute_with_orchestrator( plate_id=request_context.plate_id, ) - wells: list[str] | None = None + partition_values: list[str] | None = None compilation_resolved = False try: self._emit_compile_started( @@ -614,7 +614,7 @@ def _execute_with_orchestrator( execution_bundle=execution_bundle, ) self._raise_if_cancelled(request_context.execution_id, "initialization") - wells = self._wells_for_execution( + partition_values = self._wells_for_execution( auxiliary_params.axis_filter, orchestrator, debug_execution_policy, @@ -623,13 +623,13 @@ def _execute_with_orchestrator( self._emit_planned_init_started( progress_emitter, request_context.pipeline_steps, - wells, + partition_values, request_context.compile_artifact_id, ) compilation = self._resolve_compilation( request_context=request_context, orchestrator=orchestrator, - wells=wells, + partition_values=partition_values, debug_execution_config=debug_execution_config, debug_execution_policy=debug_execution_policy, progress_emitter=progress_emitter, @@ -652,7 +652,7 @@ def _execute_with_orchestrator( progress_emitter=progress_emitter, compile_artifact_id=request_context.compile_artifact_id, compilation_resolved=compilation_resolved, - wells=wells, + partition_values=partition_values, error=error, ) raise @@ -757,12 +757,12 @@ def _wells_for_execution( def _emit_planned_init_started( progress_emitter: ZMQProgressEmitter, pipeline_steps: Sequence[FunctionStep], - wells: list[str], + partition_values: list[str], compile_artifact_id: str | None, ) -> None: if compile_artifact_id is None: progress_emitter.planned_init_started( - wells=wells, + wells=partition_values, step_names=[step.name for step in pipeline_steps], ) @@ -771,7 +771,7 @@ def _resolve_compilation( *, request_context: ZMQExecutionContext, orchestrator, - wells: list[str], + partition_values: list[str], debug_execution_config, debug_execution_policy, progress_emitter: ZMQProgressEmitter, @@ -785,7 +785,7 @@ def _resolve_compilation( pipeline_steps=request_context.pipeline_steps, orchestrator=orchestrator, resolved_config=resolved_config, - wells=wells, + partition_values=partition_values, compile_artifact_id=request_context.compile_artifact_id, compilation_signature=request_context.compilation_signature, debug_replay_signature=request_context.debug_replay_signature, @@ -953,7 +953,7 @@ def _emit_compile_failure( progress_emitter: ZMQProgressEmitter, compile_artifact_id: str | None, compilation_resolved: bool, - wells: list[str] | None, + partition_values: list[str] | None, error: Exception, ) -> None: if compile_artifact_id is not None: @@ -961,7 +961,7 @@ def _emit_compile_failure( if compilation_resolved: return progress_emitter.compile_failed( - axis_ids=self._compile_failure_axis_ids(wells), + axis_ids=self._compile_failure_axis_ids(partition_values), error=str(error), ) @@ -979,10 +979,10 @@ def _ensure_request_global_config_context( ) @staticmethod - def _compile_failure_axis_ids(wells: list[str] | None) -> list[str]: - if wells is None: + def _compile_failure_axis_ids(partition_values: list[str] | None) -> list[str]: + if partition_values is None: return [] - return wells + return partition_values def _kill_worker_processes(self) -> int: """OpenHCS-specific worker cleanup (graceful cancellation + kill).""" diff --git a/pyproject.toml b/pyproject.toml index 9b2eebf19b..3c75da728e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,7 +105,7 @@ dependencies = [ "python-introspect>=0.2.5,<0.3", "metaclass-registry>=0.3.0,<0.4", "arraybridge>=0.3.8,<0.4", - "polystore>=0.4.0,<0.5", + "polystore>=0.5.0,<0.6", "pyqt-reactive>=0.3.30,<0.4", ] diff --git a/scripts/annotate_wells_with_experimental_metadata.py b/scripts/annotate_wells_with_experimental_metadata.py index 22b12a5265..102c30f959 100644 --- a/scripts/annotate_wells_with_experimental_metadata.py +++ b/scripts/annotate_wells_with_experimental_metadata.py @@ -23,7 +23,7 @@ # Add openhcs to path sys.path.insert(0, str(Path(__file__).parent.parent)) -from openhcs.formats.experimental_analysis import read_plate_layout, load_plate_groups +from openhcs.processing.backends.experimental_analysis.analysis import read_plate_layout, load_plate_groups from polystore.metadata_writer import AtomicMetadataWriter, get_metadata_path diff --git a/scripts/figures/generate_figure_overlays.py b/scripts/figures/generate_figure_overlays.py index 1cdb8b4832..012cb7e89c 100644 --- a/scripts/figures/generate_figure_overlays.py +++ b/scripts/figures/generate_figure_overlays.py @@ -34,7 +34,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent)) from openhcs.core.roi import load_rois_from_zip, PolygonShape -from openhcs.formats.experimental_analysis import read_plate_layout, load_plate_groups +from openhcs.processing.backends.experimental_analysis.analysis import read_plate_layout, load_plate_groups logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") logger = logging.getLogger(__name__) diff --git a/scripts/mcp_assay_showcase.py b/scripts/mcp_assay_showcase.py index 449b73e341..f13819cbcc 100644 --- a/scripts/mcp_assay_showcase.py +++ b/scripts/mcp_assay_showcase.py @@ -111,7 +111,7 @@ def generation_arguments(self, plate_path: Path) -> list[str]: str(self.num_cells), "--shared-cell-fraction", str(self.shared_cell_fraction), - "--well", + "--partition-value", WELL, "--format", "ImageXpress", diff --git a/tests/unit/agent/test_imagexpress_readonly_paths.py b/tests/unit/agent/test_imagexpress_readonly_paths.py index 24badec44e..a00f2b6ba8 100644 --- a/tests/unit/agent/test_imagexpress_readonly_paths.py +++ b/tests/unit/agent/test_imagexpress_readonly_paths.py @@ -129,7 +129,7 @@ def test_real_inventory_query_and_initialization_agree_on_physical_identity(tmp_ PlateFileQueryRequest.from_fields( plate_path=str(plate), microscope_type="imagexpress", - well="A01", + partition="A01", include_previews=False, limit=3, ) diff --git a/tests/unit/agent/test_mcp_dev_client_plate_renderers.py b/tests/unit/agent/test_mcp_dev_client_plate_renderers.py index a10815a1f3..c9d300f395 100644 --- a/tests/unit/agent/test_mcp_dev_client_plate_renderers.py +++ b/tests/unit/agent/test_mcp_dev_client_plate_renderers.py @@ -199,7 +199,7 @@ def test_every_plate_renderer_presents_its_declared_dto_and_failure(): SyntheticPlateGenerationResult( schema_version=SCHEMA_VERSION, output_dir="/plates/synthetic", requested_format=SyntheticPlateFormat.IMAGE_XPRESS, - grid_size=(2, 2), wells=("A01", "B02"), sampled_image_files=("a.tif",), + grid_size=(2, 2), partition_values=("A01", "B02"), sampled_image_files=("a.tif",), ), inspection(), query(), @@ -375,13 +375,13 @@ def test_synthetic_generation_suggests_inspection_and_query(): SyntheticPlateGenerationResult( schema_version=SCHEMA_VERSION, output_dir="/plates/synthetic", requested_format=SyntheticPlateFormat.IMAGE_XPRESS, - grid_size=(2, 2), tile_size=(64, 64), overlap_percent=10, wells=("A01", "B02"), + grid_size=(2, 2), tile_size=(64, 64), overlap_percent=10, partition_values=("A01", "B02"), wavelengths=2, image_count=8, sampled_image_files=tuple(f"{i}.tif" for i in range(14)), truncated_image_count=0, ) ) assert "Geometry: grid=2x2 tile=64x64 overlap=10% stage_error_px=0" in text - assert "Content: wells=A01,B02 channels=2" in text + assert "Content: partition_values=A01,B02 channels=2" in text assert "Files: images=8 sampled=14 truncated=0" in text assert "- 11.tif" in text and "- 12.tif" not in text assert text.endswith( diff --git a/tests/unit/agent/test_mcp_server.py b/tests/unit/agent/test_mcp_server.py index 6955397563..210a94e581 100644 --- a/tests/unit/agent/test_mcp_server.py +++ b/tests/unit/agent/test_mcp_server.py @@ -907,13 +907,13 @@ def test_mcp_tool_descriptions_expose_debugging_result_contracts(): assert "output_dir" in synthetic_plate_properties assert "overlap_percent" in synthetic_plate_properties assert "wavelengths" in synthetic_plate_properties - assert "wells" in synthetic_plate_properties + assert "partition_values" in synthetic_plate_properties assert "image/result file records" in descriptions["openhcs_query_plate_files"] query_plate_files_properties = schemas["openhcs_query_plate_files"]["properties"] assert "kind" in query_plate_files_properties assert "result_directory" in query_plate_files_properties assert "path_contains" in query_plate_files_properties - assert "well" in query_plate_files_properties + assert "partition" in query_plate_files_properties assert "include_previews" in query_plate_files_properties assert "virtual/source path" in descriptions["openhcs_sample_plate_image"] assert "bounded pixels" in descriptions["openhcs_sample_plate_image"] @@ -2076,7 +2076,7 @@ def generate(self, request): tile_size=(request.tile_width, request.tile_height), overlap_percent=request.overlap_percent, stage_error_px=request.stage_error_px, - wells=request.wells, + partition_values=request.partition_values, wavelengths=request.wavelengths, z_stack_levels=request.z_stack_levels, num_cells=request.num_cells, @@ -2110,7 +2110,7 @@ async def call_synthetic_plate_tool(): "tile_height": 48, "overlap_percent": 10, "wavelengths": 2, - "wells": ["A01"], + "partition_values": ["A01"], "random_seed": 11, }, ), @@ -2130,7 +2130,7 @@ async def call_synthetic_plate_tool(): assert synthetic_plate_service.request.tile_height == 48 assert synthetic_plate_service.request.overlap_percent == 10 assert synthetic_plate_service.request.wavelengths == 2 - assert synthetic_plate_service.request.wells == ("A01",) + assert synthetic_plate_service.request.partition_values == ("A01",) assert synthetic_plate_service.request.random_seed == 11 assert payload["schema_version"] == "openhcs.agent.v1" assert payload["output_dir"] == str(output_dir) @@ -4303,7 +4303,7 @@ def test_mcp_dev_client_query_plate_files_command_projects_tool_arguments(): "all", "--path-contains", "A01", - "--well", + "--partition", "A01", "--offset", "2", @@ -4326,7 +4326,7 @@ def test_mcp_dev_client_query_plate_files_command_projects_tool_arguments(): "pattern_format": None, "kind": "all", "path_contains": "A01", - "well": "A01", + "partition": "A01", "offset": 2, "limit": 3, "include_previews": True, @@ -4369,7 +4369,7 @@ def test_mcp_dev_client_generate_synthetic_plate_command_projects_tool_arguments "10", "--wavelengths", "2", - "--well", + "--partition-value", "A01", "--random-seed", "11", @@ -4393,7 +4393,7 @@ def test_mcp_dev_client_generate_synthetic_plate_command_projects_tool_arguments "z_stack_levels": 1, "num_cells": 80, "shared_cell_fraction": 0.95, - "wells": ["A01"], + "partition_values": ["A01"], "format": "ImageXpress", "openhcs_format": False, "include_all_components": True, @@ -4520,7 +4520,7 @@ def test_mcp_dev_client_stream_plate_files_command_projects_tool_arguments(): "pattern_format": None, "kind": "all", "path_contains": None, - "well": None, + "partition": None, "limit": 1, "viewer_config_key": "napari_streaming_config", "host": "localhost", @@ -4596,7 +4596,7 @@ def test_mcp_dev_client_selected_plate_files_command_projects_tool_arguments(): "output", "--path-contains", "A01", - "--well", + "--partition", "A01", "--limit", "3", @@ -4615,7 +4615,7 @@ def test_mcp_dev_client_selected_plate_files_command_projects_tool_arguments(): "kind": "all", "target": "output", "path_contains": "A01", - "well": "A01", + "partition": "A01", "offset": 0, "limit": 3, "include_previews": False, @@ -4775,7 +4775,7 @@ def test_mcp_dev_client_selected_plate_stream_command_projects_tool_arguments(): "kind": "all", "target": "output", "path_contains": None, - "well": None, + "partition": None, "limit": 1, "viewer_config_key": "napari_streaming_config", "host": "localhost", diff --git a/tests/unit/agent/test_plate_inspection_service.py b/tests/unit/agent/test_plate_inspection_service.py index 74d0f0d842..8e2ace59d0 100644 --- a/tests/unit/agent/test_plate_inspection_service.py +++ b/tests/unit/agent/test_plate_inspection_service.py @@ -149,7 +149,7 @@ def test_plate_request_dtos_own_mcp_tool_argument_projection(): ) synthetic_request = SyntheticPlateGenerationRequest.from_fields( output_dir="/tmp/synthetic", - wells=["A01"], + partition_values=["A01"], format="ImageXpress", ) @@ -159,7 +159,7 @@ def test_plate_request_dtos_own_mcp_tool_argument_projection(): assert sample_request.as_tool_arguments()["include_array_values"] is False assert sample_request.as_tool_arguments()["resolution_index"] == 0 assert sample_request.as_tool_arguments()["max_auto_resolution_size"] == 512 - assert synthetic_request.as_tool_arguments()["wells"] == ["A01"] + assert synthetic_request.as_tool_arguments()["partition_values"] == ["A01"] assert synthetic_request.as_tool_arguments()["format"] == "ImageXpress" @@ -509,7 +509,7 @@ def test_synthetic_plate_generation_service_writes_inspectable_plate(tmp_path: P tile_height=32, wavelengths=2, num_cells=4, - wells=("A01",), + partition_values=("A01",), random_seed=7, sample_file_limit=3, ) @@ -758,7 +758,7 @@ def test_plate_image_sample_resolves_openhcs_virtual_workspace( plate_path=str(plate), microscope_type="openhcsdata", kind=PlateFileKind.IMAGE, - well="A01", + partition="A01", limit=5, ) ) @@ -779,7 +779,7 @@ def test_plate_image_sample_resolves_openhcs_virtual_workspace( plate_path=str(plate), microscope_type="auto", kind=PlateFileKind.IMAGE, - well="A01", + partition="A01", limit=5, ) ) @@ -1044,7 +1044,7 @@ def test_explicit_result_directory_rejects_acquisition_selection( plate_path=str(tmp_path), result_directory=str(tmp_path), kind=kind, - well=well, + partition=well, ) ) assert result.errors[0].code == "plate_result_directory_selection_invalid" diff --git a/tests/unit/agent/test_plate_streaming_service.py b/tests/unit/agent/test_plate_streaming_service.py index 12f1baaddf..7e60c5cdae 100644 --- a/tests/unit/agent/test_plate_streaming_service.py +++ b/tests/unit/agent/test_plate_streaming_service.py @@ -981,7 +981,7 @@ def test_explicit_result_route_rejects_acquisition_selection_before_launch(monke plate_path="/actual_source", result_directory="/retained", kind="result", - well="A01", + partition="A01", ) ) assert result.errors diff --git a/tests/unit/agent/test_synthetic_plate_mcp_progress.py b/tests/unit/agent/test_synthetic_plate_mcp_progress.py index 55268e267d..8c5b2c3e39 100644 --- a/tests/unit/agent/test_synthetic_plate_mcp_progress.py +++ b/tests/unit/agent/test_synthetic_plate_mcp_progress.py @@ -77,7 +77,7 @@ def test_real_mcp_generation_survives_post_inspection_inactivity_window( "1", "--num-cells", "4", - "--well", + "--partition-value", "A01", "--random-seed", "7", diff --git a/tests/unit/agent/test_synthetic_plate_zero_geometry_journey.py b/tests/unit/agent/test_synthetic_plate_zero_geometry_journey.py index 439d49911b..e9b32ab738 100644 --- a/tests/unit/agent/test_synthetic_plate_zero_geometry_journey.py +++ b/tests/unit/agent/test_synthetic_plate_zero_geometry_journey.py @@ -48,7 +48,7 @@ def test_zero_geometry_real_mcp_journey(tmp_path, monkeypatch, format, native): "1", "--num-cells", "4", - "--well", + "--partition-value", "A01", "--random-seed", "7", diff --git a/tests/unit/test_axis_family_guards.py b/tests/unit/test_axis_family_guards.py index eb985b9f64..8368bce987 100644 --- a/tests/unit/test_axis_family_guards.py +++ b/tests/unit/test_axis_family_guards.py @@ -41,7 +41,6 @@ "openhcs/mcp/installed_demo.py", "openhcs/processing/backends/analysis/consolidate_analysis_results.py", "openhcs/processing/backends/experimental_analysis/", - "openhcs/formats/experimental_analysis.py", ) # The product package root is the microscopy distribution's entry point. DOMAIN_ENTRY_POINT = "openhcs/__init__.py" @@ -120,5 +119,99 @@ def test_kernel_modules_name_no_microscopy_member() -> None: assert offenders == {} +def _domain_member_spellings() -> frozenset[str]: + """Every spelling of a microscopy axis: name, collection key, multi-letter label. + + One-letter labels (``Z``, ``T``) are ImageJ and NGFF dimension letters too. + """ + + spellings: set[str] = set() + for axis in Microscopy.axes: + spellings |= {axis.name, axis.metadata_collection_field} + if len(axis.label) > 1: + spellings.add(axis.label) + return frozenset(spellings) + + +# CellProfiler's own vocabulary (its "Well"/"Plate" metadata tags) is declared +# in the CellProfiler domain package. P moves it under openhcs/domains/. +CELLPROFILER_DOMAIN_PREFIX = "openhcs/interop/" + +# ImageJ calls its hyperstack C dimension "channel", and saved configs spell +# the slot ``FijiDimensionMode.CHANNEL``. This is the one kernel declaration +# whose external spelling coincides with a microscopy axis name. +EXTERNAL_SPELLINGS = frozenset( + {("openhcs/runtime/viewer_display.py", "FijiSlots.HyperstackChannel", "channel")} +) + + +def _member_spellings_in(tree: ast.AST, spellings: frozenset[str]) -> set[tuple[str, str]]: + """(enclosing class qualname, spelling) for literals, attributes and class fields.""" + + docstrings = { + id(node.body[0].value) + for node in ast.walk(tree) + if isinstance(node, (ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)) + and node.body + and isinstance(node.body[0], ast.Expr) + and isinstance(node.body[0].value, ast.Constant) + } + found: set[tuple[str, str]] = set() + + def visit(node: ast.AST, scope: str) -> None: + for child in ast.iter_child_nodes(node): + child_scope = scope + if isinstance(child, ast.ClassDef): + child_scope = f"{scope}.{child.name}" if scope else child.name + for statement in child.body: + targets = ( + [statement.target] + if isinstance(statement, ast.AnnAssign) + else statement.targets + if isinstance(statement, ast.Assign) + else [] + ) + found.update( + (child_scope, f"field {target.id}") + for target in targets + if isinstance(target, ast.Name) and target.id in spellings + ) + elif ( + isinstance(child, ast.Constant) + and isinstance(child.value, str) + and child.value in spellings + and id(child) not in docstrings + ): + found.add((scope, child.value)) + elif isinstance(child, ast.Attribute) and child.attr in spellings: + found.add((scope, f".{child.attr}")) + visit(child, child_scope) + + visit(tree, "") + return found + + +def test_kernel_modules_spell_no_microscopy_member() -> None: + spellings = _domain_member_spellings() + found: set[tuple[str, str, str]] = set() + for path in _python_files("openhcs"): + relative = _relative(path) + if ( + relative.startswith((*DOMAIN_MODULE_PREFIXES, CELLPROFILER_DOMAIN_PREFIX)) + or relative == DOMAIN_ENTRY_POINT + ): + continue + found.update( + (relative, scope, spelling) + for scope, spelling in _member_spellings_in(ast.parse(path.read_text()), spellings) + ) + assert found == EXTERNAL_SPELLINGS + + +def test_domain_allowlist_names_existing_domain_modules() -> None: + for prefix in DOMAIN_MODULE_PREFIXES: + assert (REPO_ROOT / prefix).exists(), prefix + + def test_product_entry_point_activates_microscopy() -> None: assert AxisFamily.active() is Microscopy diff --git a/tests/unit/test_axis_family_witness.py b/tests/unit/test_axis_family_witness.py index 40428b68aa..90e787280b 100644 --- a/tests/unit/test_axis_family_witness.py +++ b/tests/unit/test_axis_family_witness.py @@ -186,6 +186,45 @@ def test_plane_addresses_spell_the_declared_tokens(remote_sensing) -> None: assert parsed.address.filename(".tif") == "S07_f002_b3_d014.tif" +def test_zarr_layout_follows_roles_and_ngff_types(remote_sensing) -> None: + from polystore.zarr_batch import ZarrBatchAxisRole + + from openhcs.core.components.parser_metaprogramming import FilenameParseResult + from openhcs.core.steps.function_io import ( + ZarrBatchItemIdentity, + ZarrComponentAxisProjection, + ) + + identities = [ + ZarrBatchItemIdentity( + component_values=FilenameParseResult( + ( + (RemoteSensing.Scene, "S01"), + (RemoteSensing.Tile, tile), + (RemoteSensing.Band, band), + (RemoteSensing.Date, "1"), + ), + extension=".tif", + ) + ) + for tile in ("1", "2") + for band in ("1", "2", "3") + ] + layout = ZarrComponentAxisProjection.batch_layout(identities) + + assert [(axis.name, axis.values, axis.role) for axis in layout.axes] == [ + ("t", ("1",), ZarrBatchAxisRole.ARRAY), + ("field", ("1", "2"), ZarrBatchAxisRole.HCS_IMAGE), + ("c", ("1", "2", "3"), ZarrBatchAxisRole.ARRAY), + ] + assert layout.ngff_axes == ( + {"name": "t", "type": "time"}, + {"name": "c", "type": "channel"}, + {"name": "y", "type": "space"}, + {"name": "x", "type": "space"}, + ) + + def test_step_validation_and_compiler_axis_resolution(remote_sensing) -> None: from openhcs.core.components.validation import GenericValidator from openhcs.core.pipeline.path_planner import ( diff --git a/tests/unit/test_cellprofiler_display_manual_projection_declarations.py b/tests/unit/test_cellprofiler_display_manual_projection_declarations.py index 3c69d8f79e..047f01f64a 100644 --- a/tests/unit/test_cellprofiler_display_manual_projection_declarations.py +++ b/tests/unit/test_cellprofiler_display_manual_projection_declarations.py @@ -550,14 +550,14 @@ def test_display_callables_resolve_values_from_declared_measurement_tables() -> assert density.row_mappings()[0]["num_points"] == 2 assert histogram.row_mappings()[0]["total_count"] == 2 assert scatter.row_mappings()[0]["point_count"] == 2 - assert {row["well"] for row in platemap.row_mappings() if "well" in row} == { + assert {row["well_name"] for row in platemap.row_mappings() if "well_name" in row} == { "A01", "A02", } object_well_values = { - row["well"]: row["value"] + row["well_name"]: row["value"] for row in object_platemap.row_mappings() - if "well" in row + if "well_name" in row } assert object_well_values == {"A01": 3.0, "A02": 6.0} diff --git a/tests/unit/test_cellprofiler_source_bindings.py b/tests/unit/test_cellprofiler_source_bindings.py index d29043784a..c33bb9c172 100644 --- a/tests/unit/test_cellprofiler_source_bindings.py +++ b/tests/unit/test_cellprofiler_source_bindings.py @@ -229,7 +229,7 @@ def test_complete_lazy_setup_facts_pycodify_and_reconstruct_in_fresh_process() - uri="/data/explicit.npy", series="0", index="2", - channel="1", + colour_sample="1", ), ), imported_metadata_tables=( @@ -353,7 +353,7 @@ def test_cppipe_parser_preserves_embedded_image_plane_rows(tmp_path: Path) -> No uri="file:/tmp/A_F.TIF", series="0", index="1", - channel="2", + colour_sample="2", ), ) assert "image_plane_sources" not in module.metadata @@ -764,7 +764,7 @@ def test_names_and_types_parses_declared_single_image_as_broadcast_member() -> N uri="file:/data/flatfield.tif", series="2", index="3", - channel="4", + colour_sample="4", ) assert config.image_plane_sources == (flatfield.explicit_source,) diff --git a/tests/unit/test_dataset_source_guards.py b/tests/unit/test_dataset_source_guards.py index 9032a0b37b..2693a55228 100644 --- a/tests/unit/test_dataset_source_guards.py +++ b/tests/unit/test_dataset_source_guards.py @@ -25,7 +25,6 @@ "openhcs/mcp/installed_demo.py", "openhcs/processing/backends/analysis/consolidate_analysis_results.py", "openhcs/processing/backends/experimental_analysis/", - "openhcs/formats/experimental_analysis.py", ) DOMAIN_ENTRY_POINT = "openhcs/__init__.py" DOMAIN_PACKAGES = ("openhcs.microscopes", "openhcs.domains") diff --git a/tests/unit/test_experimental_analysis_engine.py b/tests/unit/test_experimental_analysis_engine.py index 4cd08ed367..1109908af6 100644 --- a/tests/unit/test_experimental_analysis_engine.py +++ b/tests/unit/test_experimental_analysis_engine.py @@ -77,11 +77,11 @@ def test_desktop_workflow_does_not_copy_config_filenames_or_use_legacy_runner() assert defaults.compiled_results_file_name not in source assert defaults.raw_results_file_name not in source assert defaults.heatmap_file_name not in source - assert "formats.experimental_analysis import" not in source + assert "experimental_analysis.analysis import" not in source def test_engine_projects_declaration_owned_sheet_names(monkeypatch) -> None: - from openhcs.formats import experimental_analysis + from openhcs.processing.backends.experimental_analysis import analysis as experimental_analysis config = ExperimentalAnalysisConfig( design_sheet_name="design", @@ -110,7 +110,7 @@ def test_engine_projects_declaration_owned_sheet_names(monkeypatch) -> None: def test_engine_delegates_to_declared_normalization_method(monkeypatch) -> None: - from openhcs.formats import experimental_analysis + from openhcs.processing.backends.experimental_analysis import analysis as experimental_analysis engine = ExperimentalAnalysisEngine( ExperimentalAnalysisConfig(normalization_method=NormalizationMethod.Z_SCORE) diff --git a/tests/unit/test_experimental_layout_rows.py b/tests/unit/test_experimental_layout_rows.py index c3af626e03..b3303b676e 100644 --- a/tests/unit/test_experimental_layout_rows.py +++ b/tests/unit/test_experimental_layout_rows.py @@ -4,7 +4,7 @@ import pytest from openhcs.domains.microscopy.config import NormalizationMethod -from openhcs.formats.experimental_analysis import ( +from openhcs.processing.backends.experimental_analysis.analysis import ( average_wells, individual_wells, normalize_experiment, @@ -12,11 +12,11 @@ project_plates_without_excluded_positions, write_values_heat_map, ) -from openhcs.formats.experimental_layout_rows import ( +from openhcs.processing.backends.experimental_analysis.layout_rows import ( ExperimentalAnalysisScope, ExperimentalLayoutRowRole, ) -from openhcs.formats.experimental_result_formats import ( +from openhcs.processing.backends.experimental_analysis.result_formats import ( CX5ExperimentalResultFormat, ExperimentalResultFormatStrategy, MetaXpressExperimentalResultFormat, diff --git a/tests/unit/test_fiji_viewer_server.py b/tests/unit/test_fiji_viewer_server.py index 1ce37ffc8b..29f16608a4 100644 --- a/tests/unit/test_fiji_viewer_server.py +++ b/tests/unit/test_fiji_viewer_server.py @@ -536,9 +536,9 @@ def test_fiji_window_item_projection_preserves_nominal_items() -> None: window_items = next(iter(projection.windows.values())) assert window_items == items - assert projection.coordinate_components.channel == ("channel",) - assert projection.coordinate_components.z_axis_components == ("z_index",) - assert projection.coordinate_components.frame == tuple( + assert projection.coordinate_components.c == ("channel",) + assert projection.coordinate_components.z == ("z_index",) + assert projection.coordinate_components.t == tuple( component for component in AxisFamily.active().names() if component in {"site", "well", "timepoint"} @@ -593,7 +593,7 @@ def test_payload_local_site_axis_projects_exactly_across_fiji_and_napari() -> No assert all("plane_component_values" not in item.payload for item in projected_items) fiji_coordinates = fiji_projection.coordinate_components.collect(projected_items) - assert fiji_coordinates.frame.values == [ + assert fiji_coordinates.t.values == [ (1, 0, "A14"), (2, 0, "A14"), ] @@ -721,9 +721,9 @@ def __init__(self, width, height, pixels, color_model) -> None: channel_values = [(index,) for index in range(65)] coordinates = FijiHyperstackCoordinates( - channel=FijiDimensionAxis("channel", ["channel"], channel_values), - z_axis_coordinates=FijiDimensionAxis("z_axis", [], [()]), - frame=FijiDimensionAxis("frame", [], [()]), + c=FijiDimensionAxis("c", ["channel"], channel_values), + z=FijiDimensionAxis("z", [], [()]), + t=FijiDimensionAxis("t", [], [()]), ) image_lookup = FijiImagePlaneLookup( { @@ -802,9 +802,9 @@ def updateAndRepaintWindow(self) -> None: self.repaint_count += 1 coordinates = FijiHyperstackCoordinates( - channel=FijiDimensionAxis("channel", ["channel"], [(1,)]), - z_axis_coordinates=FijiDimensionAxis("z_axis", ["z_index"], [(0,)]), - frame=FijiDimensionAxis("frame", ["timepoint"], [(0,)]), + c=FijiDimensionAxis("c", ["channel"], [(1,)]), + z=FijiDimensionAxis("z", ["z_index"], [(0,)]), + t=FijiDimensionAxis("t", ["timepoint"], [(0,)]), ) existing_image = FijiImagePayload( data=np.zeros((2, 3), dtype=np.uint8), @@ -859,9 +859,9 @@ def test_fiji_image_intensity_range_uses_extracted_planes() -> None: def test_fiji_stack_slice_label_builder_uses_coordinate_axes() -> None: coordinates = FijiHyperstackCoordinates( - channel=FijiDimensionAxis("channel", ["channel"], [(1,)]), - z_axis_coordinates=FijiDimensionAxis("z_axis", ["z_index"], [(0,)]), - frame=FijiDimensionAxis("frame", ["site", "well"], [(2, "A14")]), + c=FijiDimensionAxis("c", ["channel"], [(1,)]), + z=FijiDimensionAxis("z", ["z_index"], [(0,)]), + t=FijiDimensionAxis("t", ["site", "well"], [(2, "A14")]), ) assert ( @@ -952,9 +952,9 @@ def convert_work_unit(encoded_rois, _scyjava): ) component_names = ViewerComponentNameMetadata.empty() coordinates = FijiHyperstackCoordinates( - channel=FijiDimensionAxis("channel", ["channel"], [(1,)]), - z_axis_coordinates=FijiDimensionAxis("z_axis", ["z_index"], [(0,)]), - frame=FijiDimensionAxis("frame", ["timepoint"], [(0,)]), + c=FijiDimensionAxis("c", ["channel"], [(1,)]), + z=FijiDimensionAxis("z", ["z_index"], [(0,)]), + t=FijiDimensionAxis("t", ["timepoint"], [(0,)]), ) item = FijiWireItem.from_payload( { diff --git a/tests/unit/test_mcp_assay_showcase.py b/tests/unit/test_mcp_assay_showcase.py index 8b2578b1cd..c87dee69a6 100644 --- a/tests/unit/test_mcp_assay_showcase.py +++ b/tests/unit/test_mcp_assay_showcase.py @@ -183,7 +183,7 @@ def test_showcase_sources_are_seven_bounded_distinct_pipeline_documents(tmp_path assert generation[generation.index("--z-stack-levels") + 1] == str( blueprint.z_stack_levels ) - assert generation.count("--well") == 1 + assert generation.count("--partition-value") == 1 if blueprint.scenario_id == "spatial_neighbors": _callable, streamed_kwargs = streamed_steps[0].func assert streamed_kwargs["retain_neighbor_count_image"] is True diff --git a/tests/unit/test_persisted_image_metadata.py b/tests/unit/test_persisted_image_metadata.py index 9e31045df6..0708befad3 100644 --- a/tests/unit/test_persisted_image_metadata.py +++ b/tests/unit/test_persisted_image_metadata.py @@ -610,7 +610,7 @@ def test_zarr_batch_header_uses_native_mapping_without_pixels(tmp_path, monkeypa np.arange(20, dtype=np.uint16).reshape(4, 5) + channel for channel in (1, 2) ] layout = ZarrBatchLayout( - axes=(ZarrBatchAxis("c", "channel", ("1", "2"), ZarrBatchAxisRole.ARRAY),), + axes=(ZarrBatchAxis("c", ("1", "2"), ZarrBatchAxisRole.ARRAY),), item_coordinates=((0,), (1,)), ) backend.save_batch( @@ -720,7 +720,7 @@ def test_mixed_zarr_batch_preserves_lineage_but_not_quantized_value_proofs(tmp_p row="A", col="01", batch_layout=ZarrBatchLayout( - axes=(ZarrBatchAxis("c", "channel", ("1", "2")),), + axes=(ZarrBatchAxis("c", ("1", "2")),), item_coordinates=((0,), (1,)), ), ) diff --git a/tests/unit/test_plane_store_sources.py b/tests/unit/test_plane_store_sources.py index bebbe001df..9aace848ae 100644 --- a/tests/unit/test_plane_store_sources.py +++ b/tests/unit/test_plane_store_sources.py @@ -242,15 +242,14 @@ def test_polystore_zarr_semantic_coordinates_round_trip_through_store_discovery( ] layout = ZarrBatchLayout( axes=( - ZarrBatchAxis("t", "time", ("2", "1")), + ZarrBatchAxis("t", ("2", "1")), ZarrBatchAxis( - "field", "field", ("3",), ZarrBatchAxisRole.HCS_IMAGE, ), - ZarrBatchAxis("c", "channel", ("2", "1")), - ZarrBatchAxis("z", "space", ("1",)), + ZarrBatchAxis("c", ("2", "1")), + ZarrBatchAxis("z", ("1",)), ), item_coordinates=( (0, 0, 0, 0), diff --git a/tests/unit/test_source_binding_workspace.py b/tests/unit/test_source_binding_workspace.py index 042072b2b8..02e42d4409 100644 --- a/tests/unit/test_source_binding_workspace.py +++ b/tests/unit/test_source_binding_workspace.py @@ -1747,7 +1747,7 @@ def test_source_binding_workspace_broadcasts_explicit_single_members(tmp_path): uri=str(flatfield), series="2", index="3", - channel="4", + colour_sample="4", ) projector = SourceBindingWorkspaceProjector( SourceBindingsConfig( diff --git a/tests/unit/test_zmq_compilation.py b/tests/unit/test_zmq_compilation.py index b0bff49b23..b84b1e7ff2 100644 --- a/tests/unit/test_zmq_compilation.py +++ b/tests/unit/test_zmq_compilation.py @@ -185,7 +185,7 @@ def test_reused_compile_artifact_reads_step_names_from_compiled_plans() -> None: pipeline_steps=[], orchestrator=None, resolved_config=None, - wells=["A01"], + partition_values=["A01"], compile_artifact_id="artifact-1", compilation_signature="signature", debug_replay_signature="debug-signature", @@ -226,7 +226,7 @@ def test_compile_fresh_emits_heartbeat_during_long_compilation() -> None: pipeline_steps=[_StrippedStepShell(), _StrippedStepShell()], orchestrator=orchestrator, resolved_config=GlobalPipelineConfig(), - wells=["A01"], + partition_values=["A01"], compile_artifact_id=None, compilation_signature="signature", debug_replay_signature="debug-signature", @@ -310,7 +310,7 @@ def test_rejected_reuse_preserves_artifact_for_a_valid_request(retain, invalid): pipeline_steps=[], orchestrator=None, resolved_config=GlobalPipelineConfig(), - wells=["A01"], + partition_values=["A01"], compile_artifact_id="artifact-1", compilation_signature="signature", debug_replay_signature="debug-signature", diff --git a/tests/unit/test_zmq_execution_server.py b/tests/unit/test_zmq_execution_server.py index 3b1ae7936d..b2d85f9603 100644 --- a/tests/unit/test_zmq_execution_server.py +++ b/tests/unit/test_zmq_execution_server.py @@ -837,5 +837,5 @@ def reject_resolution(*args, **kwargs): assert initialized[0]["execution_bundle"] is bundle assert initialized[0]["resolved_config"] is None assert compilations[0]["resolved_config"] is None - assert compilations[0]["wells"] == ["A01"] + assert compilations[0]["partition_values"] == ["A01"] assert get_current_global_config(GlobalPipelineConfig, use_live=False) is global_config