diff --git a/benchmark/equivalence/object_label_measurements.py b/benchmark/equivalence/object_label_measurements.py index 5f8eca1621..541de3e5cf 100644 --- a/benchmark/equivalence/object_label_measurements.py +++ b/benchmark/equivalence/object_label_measurements.py @@ -1,6 +1,7 @@ """Object-label derived runtime measurement projection.""" from __future__ import annotations +from openhcs.core.runtime_object_label_domains import PayloadLabelScope, PlaneLabelScope from openhcs.core.equivalence.measurement_features import ( RuntimeMeasurementFactCounterMapping, @@ -190,7 +191,7 @@ def has_declared_object_domain(self) -> bool: def owns_payload_spatial_location(self) -> bool: """Return whether this payload is authoritative for dimensional locations.""" - if self.domain.scope is not ObjectLabelDomainScope.PAYLOAD: + if self.domain.scope.ids_per_plane: return False projections = ObjectLabelMeasurementProjectionStrategy.for_scope( self.domain.scope @@ -322,7 +323,7 @@ class ObjectLabelMeasurementProjectionStrategy(ABC, metaclass=AutoRegisterMeta): @classmethod def for_scope( cls, - scope: ObjectLabelDomainScope, + scope: type[ObjectLabelDomainScope], ) -> "ObjectLabelMeasurementProjectionStrategy": matches = tuple( strategy_type() @@ -367,7 +368,7 @@ def declared_identifier_domains( ) -> tuple[tuple[int, ...], ...]: """Return declared ObjectNumber domains aligned to measurement planes.""" domain = context.object_labels.domain - if domain.scope is ObjectLabelDomainScope.PAYLOAD: + if domain.scope.ids_span_payload: return context.dense_identity_domains() return domain.declared_object_id_domains @@ -377,8 +378,8 @@ class PayloadObjectLabelMeasurementProjectionStrategy( ): """Payload-scoped labels measure the dense object payload as one domain.""" - scope = ObjectLabelDomainScope.PAYLOAD - strategy_label = ObjectLabelDomainScope.PAYLOAD.value + scope = PayloadLabelScope + strategy_label = PayloadLabelScope.name def projections( self, @@ -401,8 +402,8 @@ class PlaneObjectLabelMeasurementProjectionStrategy( ): """Plane-scoped labels measure each dense XY plane independently.""" - scope = ObjectLabelDomainScope.PLANE - strategy_label = ObjectLabelDomainScope.PLANE.value + scope = PlaneLabelScope + strategy_label = PlaneLabelScope.name def projections( self, diff --git a/benchmark/results/numpy_allocation_scaling_receiving_20261008/diagnostics/replay-full.py b/benchmark/results/numpy_allocation_scaling_receiving_20261008/diagnostics/replay-full.py index f3ec079abe..5cab0d86a7 100644 --- a/benchmark/results/numpy_allocation_scaling_receiving_20261008/diagnostics/replay-full.py +++ b/benchmark/results/numpy_allocation_scaling_receiving_20261008/diagnostics/replay-full.py @@ -1,3 +1,4 @@ +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis import os,sys,time,json,resource,ctypes,gc from pathlib import Path from multiprocessing import get_context @@ -6,7 +7,6 @@ import numpy as np from PIL import Image from openhcs.core.runtime_image_values import ImageMetadataPayload,ImagePayloadMetadata,image_payload_data -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.aligned_image_payload import ImagePayloadSliceStack from openhcs.interop.cellprofiler.image_normalization import normalize_cellprofiler_image_payload from openhcs.processing.backends.cellprofiler.color import combine_color_to_gray @@ -15,12 +15,12 @@ os.sched_setaffinity(0,{0}) root=Path('/home/ts/.cache/openhcs/cellprofiler_examples/ExampleWoundHealing/images') data=np.stack([np.asarray(Image.open(root/name)) for name in ('DMSO_B5_t0.JPG','DMSO_B5_t24.JPG')]) -metadata=ImagePayloadMetadata(source_channel_axis=-1,plane_axis=RuntimePlaneAxis.RUNTIME_SLICE,source_plane_intensity_scales=(255,255),source_plane_dtypes=('uint8','uint8')) +metadata=ImagePayloadMetadata(source_channel_axis=-1,plane_axis=RuntimeSlicePlaneAxis,source_plane_intensity_scales=(255,255),source_plane_dtypes=('uint8','uint8')) payload=ImageMetadataPayload(data,metadata) def chain(): normalized=normalize_cellprofiler_image_payload(payload) gray=[combine_color_to_gray(normalized.metadata.for_leading_source_plane(i).payload_with(image_payload_data(normalized)[i]),(0,1,2),(1.,1.,1.)) for i in range(2)] - produced=ImagePayloadSliceStack.from_output_slices(gray,memory_type='numpy',plane_axis=RuntimePlaneAxis.RUNTIME_SLICE) + produced=ImagePayloadSliceStack.from_output_slices(gray,memory_type='numpy',plane_axis=RuntimeSlicePlaneAxis) composed=produced.compose() normalized_gray=normalize_cellprofiler_image_payload(composed) smoothed=[smooth_image(image_payload_data(normalized_gray)[i],auto_object_size=False,object_size=20.) for i in range(2)] @@ -44,7 +44,7 @@ def worker(slot,connection): barrier.wait(); total_start=time.perf_counter(); before=usage(); phases={} start=time.perf_counter(); normalized=normalize_cellprofiler_image_payload(payload); phases['normalize']=time.perf_counter()-start start=time.perf_counter(); gray=[combine_color_to_gray(normalized.metadata.for_leading_source_plane(i).payload_with(image_payload_data(normalized)[i]),(0,1,2),(1.,1.,1.)) for i in range(2)]; phases['color']=time.perf_counter()-start - start=time.perf_counter(); produced=ImagePayloadSliceStack.from_output_slices(gray,memory_type='numpy',plane_axis=RuntimePlaneAxis.RUNTIME_SLICE); composed=produced.compose(); normalized_gray=normalize_cellprofiler_image_payload(composed); phases['publication_normalize']=time.perf_counter()-start + start=time.perf_counter(); produced=ImagePayloadSliceStack.from_output_slices(gray,memory_type='numpy',plane_axis=RuntimeSlicePlaneAxis); composed=produced.compose(); normalized_gray=normalize_cellprofiler_image_payload(composed); phases['publication_normalize']=time.perf_counter()-start start=time.perf_counter(); smoothed=[smooth_image(image_payload_data(normalized_gray)[i],auto_object_size=False,object_size=20.) for i in range(2)]; phases['smooth']=time.perf_counter()-start digest=[float(image_payload_data(a).sum(dtype=np.float64)) for a in smoothed] del normalized,gray,smoothed,produced,composed,normalized_gray diff --git a/benchmark/results/numpy_allocation_scaling_receiving_20261008/diagnostics/replay.py b/benchmark/results/numpy_allocation_scaling_receiving_20261008/diagnostics/replay.py index 29117635df..27b6a00dcf 100644 --- a/benchmark/results/numpy_allocation_scaling_receiving_20261008/diagnostics/replay.py +++ b/benchmark/results/numpy_allocation_scaling_receiving_20261008/diagnostics/replay.py @@ -1,3 +1,4 @@ +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis import os,sys,time,json,resource,ctypes,gc from pathlib import Path from multiprocessing import get_context @@ -6,7 +7,6 @@ import numpy as np from PIL import Image from openhcs.core.runtime_image_values import ImageMetadataPayload,ImagePayloadMetadata,image_payload_data -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.interop.cellprofiler.image_normalization import normalize_cellprofiler_image_payload from openhcs.processing.backends.cellprofiler.color import combine_color_to_gray from openhcs.processing.backends.cellprofiler.smoothing import smooth_image @@ -14,7 +14,7 @@ os.sched_setaffinity(0,{0}) root=Path('/home/ts/.cache/openhcs/cellprofiler_examples/ExampleWoundHealing/images') data=np.stack([np.asarray(Image.open(root/name)) for name in ('DMSO_B5_t0.JPG','DMSO_B5_t24.JPG')]) -metadata=ImagePayloadMetadata(source_channel_axis=-1,plane_axis=RuntimePlaneAxis.RUNTIME_SLICE,source_plane_intensity_scales=(255,255),source_plane_dtypes=('uint8','uint8')) +metadata=ImagePayloadMetadata(source_channel_axis=-1,plane_axis=RuntimeSlicePlaneAxis,source_plane_intensity_scales=(255,255),source_plane_dtypes=('uint8','uint8')) payload=ImageMetadataPayload(data,metadata) def chain(): normalized=normalize_cellprofiler_image_payload(payload) diff --git a/benchmark/results/perf_payload_slice_projection_owner_20260930/recipes/refine_payload_slice_projection_owner_20260930.py b/benchmark/results/perf_payload_slice_projection_owner_20260930/recipes/refine_payload_slice_projection_owner_20260930.py index f95c860b86..6268feb909 100644 --- a/benchmark/results/perf_payload_slice_projection_owner_20260930/recipes/refine_payload_slice_projection_owner_20260930.py +++ b/benchmark/results/perf_payload_slice_projection_owner_20260930/recipes/refine_payload_slice_projection_owner_20260930.py @@ -14,7 +14,7 @@ slices: Sequence[RuntimeArrayData], ) -> list[RuntimeArrayData]: """Project each child once, keeping strict batch-mask cardinality.""" - if self.metadata.plane_axis is None: + if self.metadata.plane_axis.unstacked: if len(slices) != 1: raise ValueError( "Image payload produced multiple slices without a declared " diff --git a/benchmark/results/perf_payload_slice_projection_owner_20260930/recipes/retire_unreachable_mask_axis_guard_20260930.py b/benchmark/results/perf_payload_slice_projection_owner_20260930/recipes/retire_unreachable_mask_axis_guard_20260930.py index 6d2997970a..29ab8fefec 100644 --- a/benchmark/results/perf_payload_slice_projection_owner_20260930/recipes/retire_unreachable_mask_axis_guard_20260930.py +++ b/benchmark/results/perf_payload_slice_projection_owner_20260930/recipes/retire_unreachable_mask_axis_guard_20260930.py @@ -1,7 +1,7 @@ from pathlib import Path from nominal_refactor_advisor.codemod import CodemodPlanDocument, CodemodSourceSnapshot, PatchTargetOperation, RefactorRecipe, SourceRewriteTarget, SourceTextReplacement root=Path('/home/ts/code/projects/openhcs-compile-perf');path=root/'openhcs/core/runtime_image_values.py';old=path.read_text() -before=''' if self.metadata.plane_axis is None: +before=''' if self.metadata.plane_axis.unstacked: if plane_index != 0: raise ValueError( "Image payload without a plane axis cannot select nonzero " diff --git a/docs/refactor/consolidation-20261009/K1-plane-modes.md b/docs/refactor/consolidation-20261009/K1-plane-modes.md new file mode 100644 index 0000000000..fd8beb6167 --- /dev/null +++ b/docs/refactor/consolidation-20261009/K1-plane-modes.md @@ -0,0 +1,92 @@ +# K1: Image payloads and plane modes + +**Status:** most of K1 landed through G3; this file records that, and the remainder K1 built. **Head audited:** `openhcs` `main` at `d597aa224` (#1205), re-measured at `44980dfe2` (#1203, E1) and merged up to `625150118`. **Rules:** [00-RULES.md](00-RULES.md). **Step 4.** +**Shared abstractions** ([02-SHARED-ABSTRACTIONS.md](02-SHARED-ABSTRACTIONS.md)). *Builds:* the plane-mode families. *Uses:* A1 (built by G3), `AutoRegisterMeta`, python-introspect's registered-class codec. + +## Already done (G3) + +| Audit item | At `d597aa224` | +|---|---| +| Bare-array vs carrier switch at 513 call sites | accessors and `ImagePayloadMetadataCarrier` gone | +| Four-type switch `payload_slices_for_alignment` | deleted; `alignment_slices`/`map_slices` on the slice family | +| Two slice-projection mechanisms | `RuntimeSliceProjectionStrategy` deleted; `RuntimeSliceProjectableValue` only | +| Triple payload dispatch in `steps/function_runtime.py` | payload methods | + +## What was wrong (the remainder) + +**Plane modes were three behaviourless enums whose meaning every caller re-decided, and their absence was `None`.** + +- `RuntimePlaneAxis` (`RUNTIME_SLICE`/`SOURCE_BINDING`): 198 lines in 34 files, 93 member references, with an `EnumKeyedStrategyMixin` strategy family beside it. +- `ImagePayloadMetadataCompositionMode` (`STACK`/`BUNDLE`) restated `RuntimePlaneAxis` one to one (`for_plane_axis`): 64 lines in 10 files. +- `ObjectLabelDomainScope` (`PAYLOAD`/`PLANE`): 64 lines in 13 files, again with an enum-keyed strategy family (`ObjectLabelPlaneDomainStrategy`) beside it. +- `plane_axis is (not) None` at 69 lines in 24 files; `plane_index is (not) None` at 43 lines in 19 files (one projection class whose `None` index meant "whole axis"). +- `image_metadata_of` / `array_data_of` (K2 handoff): two helpers that `isinstance`-switched on `ImagePayload`, 34 lines in 12 files. +- G3's gate `test_cellprofiler_projection_enters_only_runtime_slice_projection` failed on two type branches (`invocation.py`, `measurement_execution_support.py`). + +## Target (as built) + +```python +# core/runtime_plane_projection.py +class RuntimePlaneAxis(ABC, metaclass=AutoRegisterMeta): # members are used as classes; `name` is the wire spelling + leads_data / unstacked / one_plane_per_runtime_slice / one_plane_per_source_binding / keeps_image_whole + plane_index(projector, aliases) / axis_size(projector, aliases) / projected_axis(count) + keeps_planes_of(axis) / axis_when_stacked() / compose_provenance(...) + require_runtime_slices(message) / require_source_bindings(message) + named(name) / leading_axes() / require_member(value) +class RuntimeSlicePlaneAxis; class SourceBindingPlaneAxis +class NoPlaneAxis # typed absence: replaces `plane_axis None` + +class RuntimePlaneAxisValueProjection(ABC) # preserve() / from_selected_plane() / for_plane() +class WholeAxisProjection; class SelectedPlaneProjection # only the selecting member has `plane_index` +class RuntimePlaneProjection(ABC) # stack() / selected() +class WholeStackProjection; class StackPlaneSelection + +class RuntimeSliceProjectableValue: # every owned runtime value + image_metadata / array_data / scalar_source_provenance() / slice_source_provenances() + +# core/runtime_object_label_domains.py +class ObjectLabelDomainScope(ABC, metaclass=AutoRegisterMeta) # owns the plane-domain behaviour +class PayloadLabelScope; class PlaneLabelScope # ids_span_payload / ids_per_plane +``` + +- **One plane-axis family.** The composition mode is the axis the members are composed along; `stack`/`bundle` provenance is `axis.compose_provenance`. Deleted: `ImagePayloadMetadataCompositionMode`, `RuntimePlaneAxisStrategy` and its two leaves. +- **Typed absence.** `plane_axis` is always a member; `NoPlaneAxis` answers each question (`axis_when_stacked()` is the runtime-slice axis, `projected_axis` of a consumed axis is `NoPlaneAxis`). A projection is a `WholeAxisProjection` or a `SelectedPlaneProjection`; an Optional index from a resolver becomes one of them once, in `for_plane`. +- **Scope family owns its strategy.** The enum-keyed `ObjectLabelPlaneDomainStrategy` family merged into `ObjectLabelDomainScope`; a domain validates its declared IDs through its scope. +- **Payload methods.** `image_metadata`, `array_data` and the per-slice source provenance are family methods; `as_runtime_value` wraps a foreign value once for layers that hold values of every kind. Deleted: `image_metadata_of`, `array_data_of`, `CellProfilerSourceIdentityMixin.scalar_source_provenance`. +- **Positive questions.** Each family question has a positive spelling or a `require_*` method; no caller negates a flag or compares with a member. + +## Persisted state + +| Store | Class | At cutover | +|---|---|---| +| Viewer image metadata, worker bundles, debug facts, CellProfiler profile fields | runtime | reset: `plane_axis` is always spelled (`"none"` where it was null); a plane-axis-less debug fact reads `declared_plane_axis='none'` | + +## Guards + +`tests/unit/test_k1_plane_mode_guards.py` (AST over `openhcs/`): the deleted names are gone; no comparison names a member of either family; no `*plane_axis` or `.plane_index` is compared with `None`; `plane_index` exists only on the selecting members and the two projection bases are abstract; no family flag is negated; every member answers the family questions (registry-driven). E1's table loses the three enums' 36 rows. G3's projection gate passes. + +## Tests + +- B5's G3 rows. Volume projection (4): three failed because `with_source_context_from` turned an absent scalar source record into an empty one (`{}` for `None`); fixed in production. The fourth asserted that one selected plane stays a `(1, Y, X)` stack, which #395 changed on purpose the next day (one selected plane is that source plane; `test_selected_plane_stream_materialization` pins it for both axes and the neurite, BaSiC and primary-object diagnostics depend on it); it now asserts #395's behaviour. Derived role (2): stale. The test wrote its main-flow file as a bare array, bypassing the runtime's declared TIFF write, so tifffile guessed RGB for a 4-plane stack; and it read the provenance planes in an old wire shape. It now writes through `ImageFileFormat.storage_write_batches` as the runtime does. +- Template crop (16): settled by K8 (#1208), not touched here. +- Deleted: four internal-order tests that mutated `plane_axis` to an invalid string after construction (a state the type no longer has). +- A test-only projection subclass now composes its capability with `WholeAxisProjection`. + +## New-case experiments + +- A new plane axis (say, one plane per time point): before, an enum member, a strategy leaf, a composition-mode member and a branch at each of ~90 comparisons. After: one `RuntimePlaneAxis` subclass. +- A new label scope: before, an enum member plus a strategy leaf plus each `is PAYLOAD` site. After: one `ObjectLabelDomainScope` subclass. + +## Reassigned + +| Item | Evidence | Belongs to | +|---|---|---| +| Plane resolvers answer `int \| None` | `RuntimePlaneAxisProjector.runtime_slice_plane_index()` and its CellProfiler adapter callers (`artifact_binding.py`, `adapter.py`): 6 `plane_index is None` reads of a resolver answer | K7 (execution protocol) | +| Optional record fields named `*plane_index` | `SpatialGraph.source_plane_index`, `RuntimeRelationshipPlaneRecord.plane_index`, `project_intensity_proof(plane_index)` | neurite module; K3 | +| `SourceBuildRequest.domain_scope: ... \| None` | "caller did not constrain the scope" in `runtime_object_label_building.py` | K5 (callable contract) | +| Undeclared 3- or 4-plane arrays are written as RGB | `TiffConfig()` leaves `photometric` unset and tifffile guesses | PolyStore | +| Names `composition_metadata_mode` / `metadata_mode` | now hold a plane axis | P2 (rename with the package move) | + +## Done when + +The three enums are families, absence is typed, the helper switch is gone, the guards, G3's gate and B5's volume-projection and derived-role rows pass, parity is 30/30, and the ratchet and nra-r1 show no regression. diff --git a/openhcs/agent/services/plate_streaming_service.py b/openhcs/agent/services/plate_streaming_service.py index 353cf5176f..57110b0496 100644 --- a/openhcs/agent/services/plate_streaming_service.py +++ b/openhcs/agent/services/plate_streaming_service.py @@ -1,6 +1,7 @@ """Agent-facing plate file streaming service.""" from __future__ import annotations +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis import json from dataclasses import replace @@ -39,7 +40,6 @@ PlateFileRecord, ) from openhcs.core.runtime_image_values import ImagePayloadMetadata -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.source_image_provenance import ( SourceImageProvenance, SourceImageProvenancePlanes, @@ -424,7 +424,7 @@ def _receipt_source_projection( ref=source_ref, source_metadata=first_components, image_metadata=ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_provenance=SourceImageProvenance( source_path=image_path, source_component_metadata=record.components, diff --git a/openhcs/core/aligned_image_payload.py b/openhcs/core/aligned_image_payload.py index 82e24c967d..9a7c809471 100644 --- a/openhcs/core/aligned_image_payload.py +++ b/openhcs/core/aligned_image_payload.py @@ -1,6 +1,8 @@ """Generic aligned image-payload composition for multi-source runtime inputs.""" from __future__ import annotations +from openhcs.core.runtime_plane_projection import NoPlaneAxis +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis from _thread import LockType from abc import ABC, abstractmethod @@ -30,7 +32,6 @@ ImagePayload, ImagePayloadMetadata, ImagePayloadSliceProjector, - ImagePayloadMetadataCompositionMode, ImageMaskDomain, project_image_mask_to_data_domain, preserved_image_plane_projection, @@ -46,6 +47,7 @@ RuntimePlaneAxisProjector, RuntimePlaneAxisValueProjection, ) +from openhcs.core.source_image_provenance import SourceImageProvenance from openhcs.core.source_spatial_domain import ( SourceSpatialDomain, SourceSpatialDomainAdapter, @@ -382,7 +384,7 @@ def composition_payloads(self) -> tuple[Any, ...]: ... @property @abstractmethod - def composition_metadata_mode(self) -> ImagePayloadMetadataCompositionMode: ... + def composition_metadata_mode(self) -> type[RuntimePlaneAxis]: ... @staticmethod def validate_main_flow_cohort( @@ -410,7 +412,7 @@ def from_loaded_images( declared_plane_axis=( producer_records[0].main_flow_plane_axis if producer_records and len(producer_records) == 1 - else None + else NoPlaneAxis ), ): main_data_stack = payloads[0].copied( @@ -418,32 +420,30 @@ def from_loaded_images( device_id=execution_plan.device_id_for(execution_plan.input_memory_type), ) else: - metadata_mode = ImagePayloadMetadataCompositionMode.STACK + metadata_mode = RuntimeSlicePlaneAxis if producer_records: declared_axis = producer_records[0].main_flow_plane_axis # Scalar occurrences of one declared output reconstruct its # runtime axis; distinct output contexts introduce a binding axis. metadata_mode = ( - ImagePayloadMetadataCompositionMode.BUNDLE - if declared_axis is None + SourceBindingPlaneAxis + if declared_axis.unstacked and len( {record.output_context for record in producer_records} ) > 1 - else ImagePayloadMetadataCompositionMode.for_plane_axis( - declared_axis or RuntimePlaneAxis.RUNTIME_SLICE - ) + else declared_axis.axis_when_stacked() ) if source_projection is not None and workspace_source_lookups: metadata_mode = source_projection.payload_composition_mode( workspace_source_lookups ) - if metadata_mode is ImagePayloadMetadataCompositionMode.STACK: + if metadata_mode.one_plane_per_runtime_slice: main_data_stack = stack_image_payloads( payloads, metadata_mode=metadata_mode, memory_type=execution_plan.input_memory_type, device_id=execution_plan.device_id_for(execution_plan.input_memory_type), ) - elif metadata_mode is ImagePayloadMetadataCompositionMode.BUNDLE: + elif metadata_mode.one_plane_per_source_binding: main_data_stack = ImagePayloadBundleContext.from_payloads( tuple(payloads), metadata_mode=metadata_mode, ).compose() @@ -455,7 +455,7 @@ def with_saved_output_context( payloads: Sequence[RuntimeArrayData], metadata: Sequence[ImagePayloadMetadata], *, - single_output_plane_axis: RuntimePlaneAxis | None, + single_output_plane_axis: type[RuntimePlaneAxis], ) -> RuntimeArrayData: """Attach saved member context without replacing the independent buffer.""" data = stack_payload.data @@ -474,19 +474,14 @@ def with_saved_output_context( return member # Cache the cohort the next step composes from this one saved member. return stack_image_payloads( - (member,), metadata_mode=ImagePayloadMetadataCompositionMode.STACK, + (member,), metadata_mode=RuntimeSlicePlaneAxis, ) if np.shape(data)[:1] != (len(payloads),): raise ValueError( "Output stack must match its declared output slice count: " f"stack shape {np.shape(data)!r}, slice count {len(payloads)}." ) - stack_plane_axis = stack_payload.metadata.plane_axis - mode = ( - ImagePayloadMetadataCompositionMode.STACK - if stack_plane_axis is None - else ImagePayloadMetadataCompositionMode.for_plane_axis(stack_plane_axis) - ) + mode = stack_payload.metadata.plane_axis.axis_when_stacked() output_metadata = ImagePayloadMetadata.compose( tuple(payloads), mode=mode, source_metadata=tuple( record.with_current_intensity_from(current_intensity, plane_index=index) @@ -566,7 +561,7 @@ class ImagePayloadStackContext(ImagePayloadStackComposition): """Explicit dense stack inputs; the ancestor owns composition.""" payloads: Sequence[ImagePayload] - metadata_mode: ImagePayloadMetadataCompositionMode + metadata_mode: type[RuntimePlaneAxis] def __post_init__(self) -> None: # Stack members arrive as payloads or as bare producer arrays. @@ -579,14 +574,14 @@ def composition_payloads(self) -> tuple[Any, ...]: return tuple(self.payloads) @property - def composition_metadata_mode(self) -> ImagePayloadMetadataCompositionMode: + def composition_metadata_mode(self) -> type[RuntimePlaneAxis]: return self.metadata_mode def stack_image_payloads( image_payloads: Sequence[Any], *, - metadata_mode: ImagePayloadMetadataCompositionMode, + metadata_mode: type[RuntimePlaneAxis], memory_type: str | None = None, device_id: int | None = None, ) -> Any: @@ -669,14 +664,14 @@ def unstack_image_payload_context( payload: Any, slices: Sequence[Any], *, - default_plane_axis: RuntimePlaneAxis | None = None, + default_plane_axis: type[RuntimePlaneAxis] = NoPlaneAxis, ) -> list[Any]: """Attach one source plane of payload context to each unstacked image slice.""" mask = payload.mask metadata = payload.metadata if mask is None and not metadata.has_values: return [metadata.payload_with(image_slice) for image_slice in slices] - if metadata.plane_axis is None and default_plane_axis is not None: + if metadata.plane_axis.unstacked and default_plane_axis.leads_data: metadata = replace(metadata, plane_axis=default_plane_axis) projector = ImagePayloadSliceProjector(mask=mask, metadata=metadata) return projector.payloads_for_slices(slices) @@ -690,7 +685,7 @@ class ImagePayloadComposition: execution_mode: type[ImagePayloadExecutionMode] @property - def plane_axis(self) -> RuntimePlaneAxis | None: + def plane_axis(self) -> type[RuntimePlaneAxis]: """Return the invocation axis, distinct from a bundle's inner source axis.""" return self.payload.invocation_plane_axis @@ -703,7 +698,7 @@ def preserved_plane_projection( """Return the complete projection owned by the composed payload axis.""" axis = self.plane_axis - if axis is None: + if axis.unstacked: return None return preserved_image_plane_projection( self.payload, @@ -716,8 +711,8 @@ def preserved_plane_projection( class ImagePayloadBundleContext(ImagePayloadStackContext): """Compose same-slice image bundle data, masks, and metadata together.""" - metadata_mode: ImagePayloadMetadataCompositionMode = ( - ImagePayloadMetadataCompositionMode.BUNDLE + metadata_mode: type[RuntimePlaneAxis] = ( + SourceBindingPlaneAxis ) def __post_init__(self) -> None: @@ -732,7 +727,7 @@ def __post_init__(self) -> None: for index, (payload, metadata) in enumerate( zip(self.payloads, self.source_metadata, strict=True) ) - if metadata.plane_axis is not None + if metadata.plane_axis.leads_data ) if declared_axes: raise ValueError( @@ -761,8 +756,8 @@ def from_payloads( cls, payloads: tuple[RuntimeArrayData, ...], *, - metadata_mode: ImagePayloadMetadataCompositionMode = ( - ImagePayloadMetadataCompositionMode.BUNDLE + metadata_mode: type[RuntimePlaneAxis] = ( + SourceBindingPlaneAxis ), ) -> "ImagePayloadBundleContext": return cls( @@ -1101,7 +1096,7 @@ def from_output_slices( slices: Sequence[Any], *, memory_type: str, - plane_axis: RuntimePlaneAxis, + plane_axis: type[RuntimePlaneAxis], ) -> "ProducedImageStack": """Wrap produced image slices as a stack without allocating its pixels.""" return ProducedImageStack( @@ -1163,7 +1158,7 @@ def composition_payloads(self) -> tuple[Any, ...]: return self.slices @property - def projected_output_composition_mode(self) -> ImagePayloadMetadataCompositionMode | None: + def projected_output_composition_mode(self) -> type[RuntimePlaneAxis]: """Declare the outer runtime axis retained by projected output members.""" return self.composition_metadata_mode @@ -1186,7 +1181,7 @@ def full_stack_value(self) -> Any: return self.compose() def value_for_slice(self, context: RuntimePlaneAxisValueProjection) -> Any: - if context.axis is self.composition_metadata_mode.plane_axis: + if context.axis is self.composition_metadata_mode: return self.aligned_slice( context.require_plane_index(), context.axis_size, @@ -1206,9 +1201,9 @@ def value_for_slice(self, context: RuntimePlaneAxisValueProjection) -> Any: def plane_axis_for_output_context( self, context: AlignedImageSliceContext, - ) -> RuntimePlaneAxis | None: + ) -> type[RuntimePlaneAxis]: """An explicitly aligned stack declares its outer runtime-slice domain.""" - return RuntimePlaneAxis.RUNTIME_SLICE + return RuntimeSlicePlaneAxis @property def owns_output_surfaces(self) -> bool: @@ -1302,7 +1297,7 @@ def copy_projected_output_stack( """Prepare an independent output buffer in this owner's declared domain.""" payloads = tuple(payload for payload, _context in projected_outputs) metadata_mode = self.projected_output_composition_mode - if metadata_mode is None: + if metadata_mode.unstacked: return payloads[0].copied(memory_type=memory_type, device_id=device_id) data = tuple(payload.data for payload in payloads) declared_axes = { @@ -1311,7 +1306,7 @@ def copy_projected_output_stack( } if len(declared_axes) != 1 or len({tuple(np.shape(item)) for item in data}) != 1: return None - if metadata_mode is ImagePayloadMetadataCompositionMode.BUNDLE: + if metadata_mode.one_plane_per_source_binding: return ImagePayloadBundleContext.from_payloads( payloads, metadata_mode=metadata_mode, ).compose() @@ -1402,13 +1397,41 @@ class AlignedImageStack(ImagePayloadSliceStack): """Per-runtime-slice bundles of separately bound image inputs or outputs.""" @property - def composition_metadata_mode(self) -> ImagePayloadMetadataCompositionMode: - return ImagePayloadMetadataCompositionMode.STACK + def composition_metadata_mode(self) -> type[RuntimePlaneAxis]: + return RuntimeSlicePlaneAxis @property - def invocation_plane_axis(self) -> RuntimePlaneAxis: + def invocation_plane_axis(self) -> type[RuntimePlaneAxis]: """An aligned stack is invoked once per outer runtime slice.""" - return RuntimePlaneAxis.RUNTIME_SLICE + return RuntimeSlicePlaneAxis + + def slice_source_provenances(self) -> tuple[SourceImageProvenance, ...]: + return tuple(value.scalar_source_provenance() for value in self.slices) + + def scalar_source_provenance(self) -> SourceImageProvenance: + """Collapse the inner image bundle's source topology into one slice's sources.""" + metadata = tuple(value.metadata for value in self.slices) + provenances = tuple( + ( + fields.source_provenance.for_source_plane(0) + if fields.plane_axis.unstacked + and fields.source_provenance.source_plane_count == 1 + else fields.source_provenance + ) + for fields in metadata + ) + provenance = SourceImageProvenance.bundle( + provenances, + scalar_sources=tuple(fields.source_provenance for fields in metadata), + preserve_single_topology=( + len(metadata) == 1 + and metadata[0].plane_axis.one_plane_per_source_binding + ), + ) + return ( + provenance.with_runtime_planes_as_contributors() + .with_common_scalar_identity_from_planes() + ) @property def keeps_own_source_context(self) -> bool: @@ -1454,7 +1477,7 @@ class ProducedImageStack(ImagePayloadSliceStack): """ memory_type: str - plane_axis: RuntimePlaneAxis + plane_axis: type[RuntimePlaneAxis] source_metadata: InitVar[ImagePayloadMetadata | None] = None _metadata: ImagePayloadMetadata = field(init=False, repr=False) _composed_payload: RuntimeArrayData | None = field(default=None, init=False, repr=False) @@ -1475,7 +1498,7 @@ def __post_init__(self, source_metadata: ImagePayloadMetadata | None) -> None: self._metadata = ( ImagePayloadMetadata.compose( self.slices, - mode=ImagePayloadMetadataCompositionMode.for_plane_axis(self.plane_axis), + mode=self.plane_axis, ) if source_metadata is None else source_metadata ) @@ -1503,8 +1526,8 @@ def metadata(self) -> ImagePayloadMetadata: return self._metadata @property - def composition_metadata_mode(self) -> ImagePayloadMetadataCompositionMode: - return ImagePayloadMetadataCompositionMode.for_plane_axis(self.plane_axis) + def composition_metadata_mode(self) -> type[RuntimePlaneAxis]: + return self.plane_axis @property def dtype(self) -> Any: @@ -1518,11 +1541,11 @@ def dtype(self) -> Any: def plane_axis_for_output_context( self, context: AlignedImageSliceContext, - ) -> RuntimePlaneAxis: + ) -> type[RuntimePlaneAxis]: return self.plane_axis def runtime_slice_count(self) -> int | None: - if self.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE: + if self.plane_axis.one_plane_per_runtime_slice: return len(self.slices) from openhcs.core.runtime_slice_projection import RuntimeSliceProjection @@ -1692,24 +1715,24 @@ def composition_payloads(self) -> tuple[Any, ...]: ) @property - def composition_metadata_mode(self) -> ImagePayloadMetadataCompositionMode: - return ImagePayloadMetadataCompositionMode.BUNDLE + def composition_metadata_mode(self) -> type[RuntimePlaneAxis]: + return SourceBindingPlaneAxis @property - def projected_output_composition_mode(self) -> ImagePayloadMetadataCompositionMode | None: + def projected_output_composition_mode(self) -> type[RuntimePlaneAxis]: """Flatten declared inner runtime planes, retaining other named image domains.""" if any( - payload.metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + payload.metadata.plane_axis.one_plane_per_runtime_slice for payload in self.slices ): - return ImagePayloadMetadataCompositionMode.STACK + return RuntimeSlicePlaneAxis if len(self.slices) == 1: - return None + return NoPlaneAxis return self.composition_metadata_mode def plane_axis_for_output_context( self, context: AlignedImageSliceContext, - ) -> RuntimePlaneAxis | None: + ) -> type[RuntimePlaneAxis]: """Resolve a named output's own inner plane axis before slice projection.""" payloads = tuple( payload for payload, declared_context in zip(self.slices, self.slice_contexts, strict=True) @@ -1741,7 +1764,7 @@ def runtime_slice_count(self) -> int | None: def value_for_slice(self, context: RuntimePlaneAxisValueProjection) -> Any: """Project each named output through its shared declared runtime axis.""" if ( - context.axis is RuntimePlaneAxis.RUNTIME_SLICE + context.axis.one_plane_per_runtime_slice and self.inner_runtime_slice_count() is None ): return self.aligned_slice(context.require_plane_index(), context.axis_size) @@ -1801,8 +1824,8 @@ def compose_aligned_image_payload( image_payloads: tuple[Any, ...], slice_contexts: Sequence[AlignedImageSliceContext] = (), stack_broadcast_source_indices: Sequence[int | None] = (), - metadata_mode: ImagePayloadMetadataCompositionMode = ( - ImagePayloadMetadataCompositionMode.BUNDLE + metadata_mode: type[RuntimePlaneAxis] = ( + SourceBindingPlaneAxis ), *, retain_single_input: bool = True, @@ -1970,7 +1993,7 @@ def compose_aligned_image_payload( RuntimeSliceProjection.value_for_slice( payload, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, plane_index=slice_index, axis_size=slice_count, ), @@ -2005,7 +2028,7 @@ def aligned_image_stack_kwargs( """Slice runtime-array kwargs alongside an aligned image stack.""" resolver = AlignedImageStackKwargResolver( projection_axis=RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, plane_index=slice_index, axis_size=slice_count, ), diff --git a/openhcs/core/artifacts/kinds.py b/openhcs/core/artifacts/kinds.py index 93cc3609fb..8e828c833d 100644 --- a/openhcs/core/artifacts/kinds.py +++ b/openhcs/core/artifacts/kinds.py @@ -7,6 +7,7 @@ """ from __future__ import annotations +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from abc import ABC, abstractmethod from collections.abc import Sequence @@ -26,7 +27,6 @@ from openhcs.core.axes import AxisFamily from openhcs.core.memory import detect_memory_type from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisValueProjection, ) from openhcs.core.runtime_slice_alignment import ( @@ -341,7 +341,7 @@ def contextualize_projected_output( ) plane_projection = RuntimePlaneAxisValueProjection.require_from_projector( plane_projector, - RuntimePlaneAxis.RUNTIME_SLICE, + RuntimeSlicePlaneAxis, ) return cls.contextualize_output( source_payload, @@ -633,9 +633,9 @@ def source_image_payload_from_runtime_value(cls, value: object) -> object: @classmethod def materialization_image_metadata(cls, value: RuntimeValue) -> ImagePayloadMetadata: - from openhcs.core.runtime_image_values import image_metadata_of + from openhcs.core.runtime_image_values import as_runtime_value - return image_metadata_of(cls.materialization_payload(value)) + return as_runtime_value(cls.materialization_payload(value)).image_metadata class ImageArtifactType(ImagePayloadArtifactKind): @@ -732,15 +732,14 @@ def contextualize_image( ): output_value = output_value.with_undeclared_plane_axis(plane_projection) if target.collapses_runtime_slices(): - if output_value.declared_plane_axis is RuntimePlaneAxis.RUNTIME_SLICE: + if output_value.declared_plane_axis.one_plane_per_runtime_slice: output_value = RuntimeSliceProjection.value_for_singleton_slice( output_value, source_description=f"Image output {target.describe()}", ) - if ( - RuntimeSliceProjection.declared_plane_axis(source_payload) - is RuntimePlaneAxis.RUNTIME_SLICE - ): + if RuntimeSliceProjection.declared_plane_axis( + source_payload + ).one_plane_per_runtime_slice: source_ref = target.source_context_source() if source_ref is None: raise ValueError( @@ -860,7 +859,6 @@ def _stack_runtime_values( keeps_memory_type: bool, ) -> object: from openhcs.core.aligned_image_payload import stack_image_payloads - from openhcs.core.runtime_image_values import ImagePayloadMetadataCompositionMode components = {value.key.scope.component for value in values} if None in components or len(components) != 1: @@ -869,9 +867,7 @@ def _stack_runtime_values( f"axis, got {components!r}." ) payloads = tuple(value.data for value in values) - metadata_mode = ImagePayloadMetadataCompositionMode.for_plane_axis( - RuntimePlaneAxis.RUNTIME_SLICE - ) + metadata_mode = RuntimeSlicePlaneAxis if not keeps_memory_type: return stack_image_payloads(payloads, metadata_mode=metadata_mode) return stack_image_payloads( @@ -1102,7 +1098,7 @@ def _aggregated(cls, name: str, labels: tuple[object, ...]) -> object: aggregated = ObjectLabelPure2DSliceAggregator.aggregate( labels, detect_memory_type(labels[0].labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) return cls._bind_compiled_name(name, aggregated) @@ -1225,12 +1221,12 @@ def contextualize_value( ) -> object: """Own returned rows as this output's table, in the source's context.""" - from openhcs.core.runtime_image_values import image_metadata_of + from openhcs.core.runtime_image_values import as_runtime_value from openhcs.core.runtime_measurements import MeasurementTable del plane_projection subject = target.require_measurement_subject() - source_provenance = image_metadata_of(source_payload).source_provenance + source_provenance = as_runtime_value(source_payload).image_metadata.source_provenance if cls.accepts_runtime_payload(output_value): cls._validate_table(output_value, target, subject) contextualized_provenance = ( diff --git a/openhcs/core/callable_contract.py b/openhcs/core/callable_contract.py index 866ef38d2f..2bad708b2b 100644 --- a/openhcs/core/callable_contract.py +++ b/openhcs/core/callable_contract.py @@ -1011,7 +1011,7 @@ def contextualize_returned_canonical_output( "outputs but returned a non-aligned payload without a " "compiled plane projection." ) - if projection.plane_index is not None: + if projection.selects_one_plane: raise RuntimeSliceProjectionDeclarationError( f"{function_name} declares {len(canonical_specs)} canonical " "outputs after the compiled plane projection already selected " @@ -1028,7 +1028,7 @@ def contextualize_returned_canonical_output( raise RuntimeSliceProjectionDeclarationError( f"{function_name} declares {len(canonical_specs)} canonical " "outputs but its returned payload does not declare the " - f"compiled {projection.axis.value!r} plane axis; got " + f"compiled {projection.axis.name!r} plane axis; got " f"{output_axis!r}." ) output_values = tuple( diff --git a/openhcs/core/debug.py b/openhcs/core/debug.py index 60c7a18433..06aaef03fb 100644 --- a/openhcs/core/debug.py +++ b/openhcs/core/debug.py @@ -33,7 +33,6 @@ from openhcs.core.debug_view_models import DebugViewModel from openhcs.core.function_patterns import CompiledFunctionInvocation from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisProjector, RuntimePlaneAxisValueProjection, RuntimePlaneProjection, @@ -528,7 +527,7 @@ def from_value( facts.extend( ( f"representation={value.representation.value!r}", - f"domain_scope={domain.scope.value!r}", + f"domain_scope={domain.scope.name!r}", f"declared_object_count={domain.declared_object_count!r}", f"declared_object_id_count={len(domain.declared_object_ids)}", "declared_object_plane_count=" @@ -541,7 +540,7 @@ def from_value( facts.extend( ( "declared_plane_axis=" - f"{None if value.plane_axis is None else value.plane_axis.value!r}", + f"{value.plane_axis.name!r}", "declared_cardinality=" f"{None if projection is None else projection.axis_size!r}", ) @@ -568,10 +567,10 @@ def from_value( f"{len(metadata.source_image_names)}", ) ) - if metadata.plane_axis is not None: + if metadata.plane_axis.leads_data: payload_shape = tuple(int(axis_size) for axis_size in value.shape) if plane_projector is not None and ( - metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + metadata.plane_axis.one_plane_per_runtime_slice or not isinstance(plane_projector, RuntimePlaneProjection) ): projection = RuntimePlaneAxisValueProjection.from_projector( @@ -580,20 +579,16 @@ def from_value( metadata.source_image_names, ) if projection is None: - facts.append(f"plane_axis={metadata.plane_axis.value!r}") - facts.extend( - ( - f"declared_plane_axis={metadata.plane_axis.value!r}", - f"declared_cardinality={payload_shape[0]}", - ) - ) + facts.append(f"plane_axis={metadata.plane_axis.name!r}") + declared_cardinality = payload_shape[0] else: - facts.extend( - ( - "declared_plane_axis=None", - "declared_cardinality=None", - ) + declared_cardinality = None + facts.extend( + ( + f"declared_plane_axis={metadata.plane_axis.name!r}", + f"declared_cardinality={declared_cardinality}", ) + ) elif isinstance(value, np.ndarray): facts.extend( ( @@ -630,11 +625,11 @@ def from_value( if projection is not None: plane_coordinate = ( "preserved" - if projection.plane_index is None + if projection.preserves_complete_axis else str(projection.plane_index) ) projection_facts = ( - f"plane_axis={projection.axis.value!r}", + f"plane_axis={projection.axis.name!r}", f"plane_index={plane_coordinate}", f"axis_size={projection.axis_size}", f"source_alias_count={len(projection.source_aliases)}", diff --git a/openhcs/core/image_file_serialization.py b/openhcs/core/image_file_serialization.py index 78fa510f4a..3e97cf52c0 100644 --- a/openhcs/core/image_file_serialization.py +++ b/openhcs/core/image_file_serialization.py @@ -516,7 +516,7 @@ def storage_config( metadata = payload.metadata if metadata.persists_whole_image(): axes = list("ZYX") - elif metadata.plane_axis is not None: + elif metadata.plane_axis.leads_data: # Q is a container frame, not a guessed physical Z/channel axis. # Its exact OpenHCS component domain remains in source projection. axes = list("QYX") @@ -822,10 +822,10 @@ def image_payload_as_uint8(payload: Any) -> np.ndarray: def require_single_image_payload(payload: Any) -> np.ndarray: """Return pixels only when no runtime plane axis remains to project.""" plane_axis = payload.metadata.plane_axis - if plane_axis is not None: + if plane_axis.leads_data: raise ValueError( "Single-image raster serialization requires a payload projected off " - f"its declared {plane_axis.value!r} plane axis." + f"its declared {plane_axis.name!r} plane axis." ) return np.asarray(payload.data) diff --git a/openhcs/core/image_payload_execution_mode.py b/openhcs/core/image_payload_execution_mode.py index 859666e5ff..cfc6f5be82 100644 --- a/openhcs/core/image_payload_execution_mode.py +++ b/openhcs/core/image_payload_execution_mode.py @@ -6,6 +6,7 @@ """ from __future__ import annotations +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from abc import ABC, abstractmethod from typing import TYPE_CHECKING, Any, ClassVar @@ -123,7 +124,9 @@ def batch_request(cls, request): AlignedImageStack, aligned_image_stack_kwargs, ) - from openhcs.core.runtime_plane_projection import RuntimePlaneAxis + from openhcs.core.runtime_plane_projection import ( + RuntimePlaneAxisValueProjection, + ) from openhcs.core.runtime_slice_projection import RuntimeSliceProjection if not isinstance(request.image, AlignedImageStack): @@ -131,13 +134,12 @@ def batch_request(cls, request): "Aligned runtime batch execution requires AlignedImageStack, got " f"{type(request.image).__name__}." ) - projection = request.plane_projection - if projection is None or projection.axis is not RuntimePlaneAxis.RUNTIME_SLICE: - raise ValueError( - "Aligned runtime batch execution requires a compiled runtime-slice " - "projection." - ) - if projection.plane_index is not None: + projection = RuntimePlaneAxisValueProjection.require_runtime_slice_projection( + request.plane_projection, + "Aligned runtime batch execution requires a compiled runtime-slice " + "projection.", + ) + if projection.selects_one_plane: raise ValueError( "Aligned runtime batch execution received an image after its " "runtime-slice projection was already selected." @@ -182,7 +184,6 @@ def execute(cls, contract, call, image, kwargs): Pure2DSliceBatchExecutor, RuntimePure2DSliceBatchRequest, ) - from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.runtime_slice_alignment import RuntimeSliceAlignedValues from openhcs.core.runtime_slice_projection import ( RuntimeSliceProjection, @@ -205,12 +206,12 @@ def execute(cls, contract, call, image, kwargs): f"{owner} requires a compiled runtime-slice projection for " "aligned multi-image stack execution." ) - if projection.axis is not RuntimePlaneAxis.RUNTIME_SLICE: - raise RuntimeSliceProjectionDeclarationError( - f"{owner} aligned image execution requires " - f"RuntimePlaneAxis.RUNTIME_SLICE, got {projection.axis!r}." - ) - if projection.plane_index is not None: + projection.axis.require_runtime_slices( + f"{owner} aligned image execution requires " + f"RuntimeSlicePlaneAxis, got {projection.axis!r}.", + error=RuntimeSliceProjectionDeclarationError, + ) + if projection.selects_one_plane: raise RuntimeSliceProjectionDeclarationError( f"{owner} received an AlignedImageStack after the compiled " "runtime-slice projection already selected plane " @@ -226,14 +227,14 @@ def execute(cls, contract, call, image, kwargs): slice_plane_axes = tuple( slice_payload.metadata.plane_axis for slice_payload in image.slices ) - if any( - plane_axis is not RuntimePlaneAxis.SOURCE_BINDING + if not all( + plane_axis.one_plane_per_source_binding for plane_axis in slice_plane_axes ): raise ValueError( f"{owner} with whole-stack contract {contract.key!r} requires " "every aligned image slice to declare " - f"RuntimePlaneAxis.SOURCE_BINDING; got {slice_plane_axes!r}." + f"SourceBindingPlaneAxis; got {slice_plane_axes!r}." ) def execute_aligned_stack_slice( @@ -284,7 +285,7 @@ def execute_aligned_stack_slice( aligned_main_output = Pure2DAuxiliaryOutputAggregator.aggregate( result_batch.main_outputs, memory_type, - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) if not isinstance(aligned_main_output, AlignedImageStack): raise TypeError( @@ -308,7 +309,7 @@ def execute_aligned_stack_slice( stacked_main_output = Pure2DAuxiliaryOutputAggregator.aggregate( result_batch.main_outputs, memory_type, - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) elif len(canonical_specs) == len(result_batch.main_outputs): stacked_main_output = AlignedImageStack( @@ -340,7 +341,7 @@ def execute_aligned_stack_slice( Pure2DAuxiliaryOutputAggregator.aggregate( values, memory_type, - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) for values in result_batch.auxiliary_groups ), diff --git a/openhcs/core/materialization/core.py b/openhcs/core/materialization/core.py index f2e6f5c4e5..82dd33f25d 100644 --- a/openhcs/core/materialization/core.py +++ b/openhcs/core/materialization/core.py @@ -4,6 +4,7 @@ """ from __future__ import annotations +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis import copy import csv @@ -46,7 +47,6 @@ from openhcs.core.runtime_array_values import runtime_array_operand from openhcs.core.runtime_image_values import ( ImagePayloadMetadata, - ImagePayloadMetadataCompositionMode, ) from openhcs.core.runtime_object_labels import ( ObjectLabelValue, @@ -107,7 +107,7 @@ from openhcs.core.axes import Axis from openhcs.core.measurement_dialect import MeasurementDialect from openhcs.core.runtime_image_values import ImagePayload -from openhcs.core.runtime_image_values import array_data_of, image_metadata_of +from openhcs.core.runtime_image_values import as_runtime_value if TYPE_CHECKING: from polystore.filemanager import FileManager @@ -1229,12 +1229,11 @@ class MaterializationInputItem(RuntimeProjectedPayloadItem): @property def data(self) -> MaterializationValue: - return runtime_array_operand(array_data_of(self.value)) + return runtime_array_operand(as_runtime_value(self.value).array_data) def has_leading_plane_axis(self) -> bool: """Whether the payload's first dimension is a declared plane axis.""" - plane_axis = self.metadata.plane_axis - return plane_axis is not None + return self.metadata.plane_axis.leads_data @property def runtime_plane(self) -> RuntimeProjectionPlaneMetadata | None: @@ -1287,7 +1286,7 @@ def from_payloads( plane_count_source for payload in payloads for plane_count_source in ( - image_metadata_of(payload).source_provenance.plane_count_sources + as_runtime_value(payload).image_metadata.source_provenance.plane_count_sources ) ) ) @@ -3692,7 +3691,7 @@ def metadata_for_target( return ImagePayloadMetadata.compose( (item.value,), - mode=ImagePayloadMetadataCompositionMode.STACK, + mode=RuntimeSlicePlaneAxis, source_metadata=(metadata,), ) diff --git a/openhcs/core/measurement_image_alignment.py b/openhcs/core/measurement_image_alignment.py index 674eb976a6..eee0e942f7 100644 --- a/openhcs/core/measurement_image_alignment.py +++ b/openhcs/core/measurement_image_alignment.py @@ -1,6 +1,7 @@ """Nominal measurement-image alignment contracts.""" from __future__ import annotations +from openhcs.core.runtime_plane_projection import SourceBindingPlaneAxis import logging from abc import ABC, abstractmethod @@ -20,10 +21,8 @@ object_label_project_plane, ) from openhcs.core.runtime_profile import RuntimeProfileLogger, RuntimeProfileTimer -from openhcs.core.runtime_object_label_domains import ObjectLabelDomainScope from openhcs.core.runtime_object_labels import ObjectLabelRepresentation from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisProjector, RuntimePlaneAxisValueProjection, ) @@ -143,10 +142,10 @@ def with_source_projected_image(self) -> "MeasurementImageLabelAlignmentRequest" """Return this request with only the measurement image source-projected.""" projection = RuntimePlaneAxisValueProjection.from_projector( self.plane_projector, - RuntimePlaneAxis.SOURCE_BINDING, + SourceBindingPlaneAxis, self.source_aliases, ) - if projection is None or projection.plane_index is None: + if projection is None or projection.preserves_complete_axis: return self image = self.image if isinstance(image, AlignedImageStack): @@ -179,10 +178,7 @@ def with_source_projected_labels(self) -> "MeasurementImageLabelAlignmentRequest """Project labels only through their declared axis and runtime context.""" if self.label_payload is None: return self - if ( - self.label_payload.object_label_domain().scope - is not ObjectLabelDomainScope.PLANE - ): + if self.label_payload.object_label_domain().scope.ids_span_payload: return self declared_projection = self.label_payload.declared_plane_projection() if declared_projection is None: @@ -196,7 +192,7 @@ def with_source_projected_labels(self) -> "MeasurementImageLabelAlignmentRequest ) if runtime_projection is None: return self - if runtime_projection.plane_index is None: + if runtime_projection.preserves_complete_axis: if declared_projection.axis_size != 1: return self projection = declared_projection.selected_plane(0) @@ -328,8 +324,7 @@ def _with_aligned_source(self) -> "MeasurementImageLabelAlignmentRequest": ) if ( label_payload is not None - and label_payload.object_label_domain().scope - is ObjectLabelDomainScope.PAYLOAD + and label_payload.object_label_domain().scope.ids_span_payload and image_runtime_projection is not None and image_runtime_projection.axis_size == 1 ): @@ -371,11 +366,7 @@ def object_label_reference_image(self) -> RuntimeArrayData | AlignedImageStack: return request.image return label_payload.measurement_reference_image() domain = label_payload.object_label_domain() - if domain.scope is not ObjectLabelDomainScope.PAYLOAD and not ( - domain.scope is ObjectLabelDomainScope.PLANE - and label_payload.plane_axis - in (RuntimePlaneAxis.RUNTIME_SLICE, RuntimePlaneAxis.SOURCE_BINDING) - ): + if domain.scope.ids_per_plane and label_payload.plane_axis.unstacked: raise ValueError( "Object-label reference alignment has no behavior declared for " f"scope={domain.scope!r}, plane_axis={label_payload.plane_axis!r}." diff --git a/openhcs/core/pipeline/function_contracts.py b/openhcs/core/pipeline/function_contracts.py index 5668ea78e6..46f584b80d 100644 --- a/openhcs/core/pipeline/function_contracts.py +++ b/openhcs/core/pipeline/function_contracts.py @@ -216,9 +216,8 @@ def semantic_label_payload(cls, source_projected_payload, completion_payload): @classmethod def image_execution_mode(cls, labels, default, *, runtime_slice_count=None): - from openhcs.core.runtime_object_label_domains import ObjectLabelDomainScope - if labels.object_label_domain().scope is ObjectLabelDomainScope.PAYLOAD: + if labels.object_label_domain().scope.ids_span_payload: return default.for_payload_scoped_labels(runtime_slice_count) return default.for_plane_scoped_labels(labels.declared_plane_projection()) diff --git a/openhcs/core/processing_contracts.py b/openhcs/core/processing_contracts.py index ed73f05acd..f041fe3465 100644 --- a/openhcs/core/processing_contracts.py +++ b/openhcs/core/processing_contracts.py @@ -8,6 +8,7 @@ """ from __future__ import annotations +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis import inspect from abc import ABC, abstractmethod @@ -42,7 +43,7 @@ ImagePayload, MaskedImagePayload, PlainImagePayload, - image_metadata_of, + as_runtime_value, owned_runtime_value, ) from openhcs.core.runtime_object_label_aggregation import ( @@ -420,7 +421,7 @@ class ImagePayloadPure2DInputSlicer(Pure2DInputSlicer): value_type = None def is_single_plane_value(self, value: Any) -> bool: - return value.metadata.plane_axis is None + return value.metadata.plane_axis.unstacked def slice_value(self, value: Any, memory_type: str) -> tuple[Any, ...]: data = value.data @@ -497,7 +498,7 @@ def aggregate( values: list[Any], memory_type: str, *, - plane_axis: RuntimePlaneAxis = RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis: type[RuntimePlaneAxis] = RuntimeSlicePlaneAxis, ) -> Any: if not values: raise ValueError("PURE_2D auxiliary aggregation requires output values.") @@ -548,7 +549,7 @@ def aggregate_values( values: list[Any], memory_type: str, *, - plane_axis: RuntimePlaneAxis, + plane_axis: type[RuntimePlaneAxis], ) -> Any: """Aggregate compatible per-slice auxiliary values.""" @@ -563,7 +564,7 @@ def aggregate_values( values: list[Any], memory_type: str, *, - plane_axis: RuntimePlaneAxis, + plane_axis: type[RuntimePlaneAxis], ) -> DirectedObjectRelationshipPayload: del memory_type, plane_axis return DirectedObjectRelationshipPayload( @@ -592,7 +593,7 @@ def aggregate_values( values: list[Any], memory_type: str, *, - plane_axis: RuntimePlaneAxis, + plane_axis: type[RuntimePlaneAxis], ) -> RuntimeSliceAlignedValues[SpatialGrid]: del memory_type, plane_axis return RuntimeSliceAlignedValues(tuple(values)) @@ -608,7 +609,7 @@ def aggregate_values( values: list[Any], memory_type: str, *, - plane_axis: RuntimePlaneAxis, + plane_axis: type[RuntimePlaneAxis], ) -> AlignedImageStack: aligned_values = tuple(values) first = aligned_values[0] @@ -662,7 +663,7 @@ def aggregate_values( values: list[Any], memory_type: str, *, - plane_axis: RuntimePlaneAxis, + plane_axis: type[RuntimePlaneAxis], ) -> Any: del values, memory_type, plane_axis raise NotImplementedError( @@ -694,7 +695,7 @@ def aggregate_values( values: list[Any], memory_type: str, *, - plane_axis: RuntimePlaneAxis, + plane_axis: type[RuntimePlaneAxis], ) -> Any: return ImagePayloadSliceStack.from_output_slices( values, memory_type=memory_type, plane_axis=plane_axis, @@ -733,7 +734,7 @@ def aggregate_values( values: list[Any], memory_type: str, *, - plane_axis: RuntimePlaneAxis, + plane_axis: type[RuntimePlaneAxis], ) -> Any: return ObjectLabelPure2DSliceAggregator.aggregate( values, @@ -760,7 +761,7 @@ def aggregate_values( values: list[Any], memory_type: str, *, - plane_axis: RuntimePlaneAxis, + plane_axis: type[RuntimePlaneAxis], ) -> Any: return ImagePayloadSliceStack.from_output_slices( values, memory_type=memory_type, plane_axis=plane_axis, @@ -777,7 +778,7 @@ def aggregate_values( values: list[Any], memory_type: str, *, - plane_axis: RuntimePlaneAxis, + plane_axis: type[RuntimePlaneAxis], ) -> ColumnarRows: del memory_type, plane_axis if len(values) == 1: @@ -824,7 +825,7 @@ def aggregate_values( values: list[Any], memory_type: str, *, - plane_axis: RuntimePlaneAxis, + plane_axis: type[RuntimePlaneAxis], ) -> Any: del memory_type, plane_axis flattened: list[Any] = [] @@ -857,7 +858,7 @@ class PlaneSplit: """A main-flow value split into the planes of its declared plane axis.""" slices: tuple[Any, ...] - plane_axis: RuntimePlaneAxis + plane_axis: type[RuntimePlaneAxis] memory_type: str kwargs: dict[str, Any] source_aliases: tuple[str, ...] = () @@ -1168,7 +1169,7 @@ def supports_measurement_image_batch( return ( not request.execution_mode.per_plane or request.plane_projection is None - or request.plane_projection.plane_index is not None + or request.plane_projection.selects_one_plane ) @classmethod @@ -1293,10 +1294,10 @@ def explains_probe(cls, probe: ContractProbe) -> bool: @classmethod def main_flow_output_source_payload(cls, source_payload: Any) -> Any: """Consume the declared leading plane axis while preserving provenance.""" - metadata = image_metadata_of(source_payload) + metadata = as_runtime_value(source_payload).image_metadata if not metadata.has_values: return source_payload - if metadata.plane_axis is None: + if metadata.plane_axis.unstacked: raise ValueError( "VOLUMETRIC_TO_SLICE output requires a declared input plane axis." ) @@ -1402,7 +1403,7 @@ def plane_split(self, image: Any, kwargs: Mapping[str, Any]) -> PlaneSplit | Non return None input_metadata = image.metadata plane_axis = input_metadata.plane_axis - if plane_axis is None: + if plane_axis.unstacked: raise ValueError( "PURE_2D multi-plane execution requires the input payload to " "declare its runtime plane axis." diff --git a/openhcs/core/runtime_artifact_queries.py b/openhcs/core/runtime_artifact_queries.py index 5d2e9e2edb..5dc2f7a92e 100644 --- a/openhcs/core/runtime_artifact_queries.py +++ b/openhcs/core/runtime_artifact_queries.py @@ -38,7 +38,6 @@ ObjectLabelMeasurementValues, ) from openhcs.core.runtime_object_label_domains import ( - ObjectLabelPlaneDomainStrategy, dense_object_label_id_domain, ) from openhcs.core.runtime_plane_projection import ( @@ -291,9 +290,7 @@ def for_labels( plane_projector: RuntimePlaneAxisProjector, ) -> "MeasurementLabelSliceAxisSelection": """Bind label-domain semantics to the invocation's exact runtime axis.""" - domain_strategy = ObjectLabelPlaneDomainStrategy.for_enum_member( - labels.object_label_domain().scope - ) + domain_strategy = labels.object_label_domain().scope projection = domain_strategy.measurement_projection(labels, plane_projector) return cls( row_axis=row_axis, @@ -328,7 +325,7 @@ def validate_observed_axis_values( ) ) full_axis_values = tuple(range(self.plane_projection.axis_size)) - selects_one_plane = self.plane_projection.plane_index is not None + selects_one_plane = self.plane_projection.selects_one_plane missing_values = tuple( value for value in required_values if value not in observed_values ) @@ -442,9 +439,7 @@ def label_planes( "Object-label measurement lookup requires ObjectLabelValue, got " f"{type(labels).__name__}." ) - domain_strategy = ObjectLabelPlaneDomainStrategy.for_enum_member( - labels.object_label_domain().scope - ) + domain_strategy = labels.object_label_domain().scope return domain_strategy.measurement_planes( labels, domain_strategy.measurement_projection(labels, self.plane_projector), diff --git a/openhcs/core/runtime_image_loading.py b/openhcs/core/runtime_image_loading.py index 6b20627473..950a955b6b 100644 --- a/openhcs/core/runtime_image_loading.py +++ b/openhcs/core/runtime_image_loading.py @@ -1,6 +1,8 @@ """Source-file metadata construction for runtime image payloads.""" from __future__ import annotations +from openhcs.core.runtime_plane_projection import NoPlaneAxis +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from dataclasses import dataclass from pathlib import Path @@ -18,7 +20,6 @@ ImagePayloadMetadata, ImagePayload, ) -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.source_image_provenance import ( SourceImageIdentity, SourceImageProvenancePlanes, @@ -89,13 +90,13 @@ def metadata( source_identity_path = self.source_path source_component_metadata = self.source_identity.component_metadata source_image_provenance_planes = SourceImageProvenancePlanes() - plane_axis = None + plane_axis = NoPlaneAxis if source_component_metadata is not None: indexed_metadata = SourcePlaneIndexedMetadata.from_declared_source_metadata( source_component_metadata ) if indexed_metadata is not None: - plane_axis = RuntimePlaneAxis.RUNTIME_SLICE + plane_axis = RuntimeSlicePlaneAxis source_image_provenance_planes = ( SourceImageProvenancePlanes.from_components( paths=(source_identity_path,) diff --git a/openhcs/core/runtime_image_values.py b/openhcs/core/runtime_image_values.py index 7facd0d90b..a36e286d7b 100644 --- a/openhcs/core/runtime_image_values.py +++ b/openhcs/core/runtime_image_values.py @@ -1,6 +1,8 @@ """Nominal runtime image payload values and metadata.""" from __future__ import annotations +from openhcs.core.runtime_plane_projection import NoPlaneAxis +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis from abc import ABC, abstractmethod from collections.abc import ( @@ -9,7 +11,6 @@ Sequence, ) from dataclasses import dataclass, field, fields -from enum import Enum from typing import Any, TypeVar import numpy as np @@ -22,7 +23,6 @@ ) from collections.abc import Mapping -from openhcs.core.alias_property import AliasProperty from openhcs.core.payload_axes import ( AxisSpec, PayloadAxes, @@ -43,6 +43,7 @@ RuntimePlaneAxis, RuntimePlaneAxisProjector, RuntimePlaneAxisValueProjection, + RuntimeSliceInvariantValue, RuntimeSliceProjectableValue, ) from openhcs.core.source_image_provenance import ( @@ -348,7 +349,7 @@ def axes(self) -> PayloadAxes: ... @property @abstractmethod - def plane_axis(self) -> RuntimePlaneAxis | None: ... + def plane_axis(self) -> type[RuntimePlaneAxis]: ... @property @abstractmethod @@ -356,14 +357,14 @@ def source_spatial_domain(self) -> SourceSpatialDomain: ... def require_scalar_source_plane(self) -> None: """Require source metadata for one scalar grayscale image plane.""" - if self.plane_axis is not None: + if self.plane_axis.leads_data: raise ValueError("Exported source planes require scalar image metadata.") if self.axes.has_values: raise ValueError("Exported Z planes cannot carry declared non-spatial axes.") def require_leading_plane_axis(self, message: str) -> None: """Require axis presence before later ordered projection validation.""" - if self.plane_axis is None: + if self.plane_axis.unstacked: raise ValueError(message) def axis_position(self, role: type[AxisRole]) -> int | None: @@ -418,10 +419,10 @@ def payload_axes(self, data: Any) -> tuple[AxisSpec, ...]: """Return one declared dimension per axis of ``data``.""" ndim = array_geometry(data).ndim resolved: dict[int, AxisSpec] = dict(self.axes.indices(ndim)) - if self.plane_axis is not None: + if self.plane_axis.leads_data: if 0 in resolved: raise ValueError("Image metadata declares its leading plane axis twice.") - resolved[0] = RuntimePlaneAxisSpec(self.plane_axis.value) + resolved[0] = RuntimePlaneAxisSpec(self.plane_axis.name) candidates = tuple(axis for axis in range(ndim) if axis not in resolved) names = self.source_spatial_domain.axis_names spatial = candidates[len(candidates) - len(names):] if names else () @@ -432,11 +433,11 @@ def payload_axes(self, data: Any) -> tuple[AxisSpec, ...]: def declares_colour_samples_plane(self, data: Any) -> bool: """Return whether this payload declares one colour-sampled image plane.""" - return self.axis_index(ColourAxis, data) is not None and self.plane_axis is None + return self.axis_index(ColourAxis, data) is not None and self.plane_axis.unstacked def declares_colour_samples_stack(self, data: Any) -> bool: """Return whether this payload declares a plane stack of colour-sampled images.""" - return self.axis_index(ColourAxis, data) is not None and self.plane_axis is not None + return self.axis_index(ColourAxis, data) is not None and self.plane_axis.leads_data def spatial_shape_yx(self, data: Any) -> tuple[int, int] | None: """Return the planar Y/X shape from the declared spatial axes.""" @@ -449,7 +450,7 @@ def spatial_shape_yx(self, data: Any) -> tuple[int, int] | None: @property def has_leading_intensity_axis(self) -> bool: """Bind intensity projection to the existing image-axis declaration.""" - return self.plane_axis is not None + return self.plane_axis.leads_data def require_leading_intensity_axis(self) -> None: """Keep numerical normalization inside the existing image-axis domain.""" @@ -474,8 +475,8 @@ class ImagePayloadMetadata( axes: PayloadAxes = field( default_factory=PayloadAxes, metadata={ViewerWireField.IMAGE_METADATA: True} ) - plane_axis: RuntimePlaneAxis | None = field( - default=None, metadata={ViewerWireField.IMAGE_METADATA: True} + plane_axis: type[RuntimePlaneAxis] = field( + default=NoPlaneAxis, metadata={ViewerWireField.IMAGE_METADATA: True} ) @classmethod @@ -498,7 +499,7 @@ def require_source_image_pixels(self, data: Any) -> None: def singleton_plane_projection(self) -> RuntimePlaneAxisValueProjection | None: """Select a declared leading axis only with one exact runtime source plane.""" plane_count = self.source_provenance.source_plane_count - if self.plane_axis is None or plane_count != 1: + if self.plane_axis.unstacked or plane_count != 1: return None return RuntimePlaneAxisValueProjection.from_selected_plane( axis=self.plane_axis, @@ -554,7 +555,7 @@ def retained_plane_component_values( axis; artifact storage/grouping axes do not declare that image domain. """ - if self.plane_axis is None: + if self.plane_axis.unstacked: return {} return self.source_provenance.varying_plane_component_values( AxisFamily.active().axes @@ -613,9 +614,7 @@ def normalize_metadata_fields(self) -> None: self.normalize_source_voxel_spacing_fields() if not isinstance(self.axes, PayloadAxes): raise TypeError("ImagePayloadMetadata.axes must be PayloadAxes.") - if self.plane_axis is not None: - self.plane_axis = RuntimePlaneAxis(self.plane_axis) - + self.plane_axis = RuntimePlaneAxis.require_member(self.plane_axis) @property def has_values(self) -> bool: @@ -636,7 +635,7 @@ def has_values(self) -> bool: self.mask_defines_border is not None, bool(self.source_image_names), self.axes.has_values, - self.plane_axis is not None, + self.plane_axis.leads_data, ) ) @@ -697,33 +696,37 @@ def _derived_metadata( else source_metadata.plane_axis ) if plane_projection is None and ( - declared_output_axis is not None or source_metadata.plane_axis is not None + declared_output_axis.leads_data or source_metadata.plane_axis.leads_data ): - declared_axis = declared_output_axis or source_metadata.plane_axis + declared_axis = ( + declared_output_axis + if declared_output_axis.leads_data + else source_metadata.plane_axis + ) raise ValueError( "Derived image payload carries a declared plane axis but the " "invocation supplied no plane projection: " - f"{declared_axis.value!r}." + f"{declared_axis.name!r}." ) - if plane_projection is not None and plane_projection.plane_index is None: + if plane_projection is not None and plane_projection.preserves_complete_axis: for owner_name, metadata in ( ("source", source_metadata), ("output", output_metadata), ): if ( - metadata.plane_axis is not None + metadata.plane_axis.leads_data and metadata.plane_axis is not plane_projection.axis ): raise ValueError( f"Derived image {owner_name} metadata axis conflicts with " "the invocation projection: " - f"{metadata.plane_axis.value!r} != " - f"{plane_projection.axis.value!r}." + f"{metadata.plane_axis.name!r} != " + f"{plane_projection.axis.name!r}." ) source_metadata = source_metadata.with_indexed_source_plane_provenance( plane_projection.axis_size ) - if declared_output_axis is not None: + if declared_output_axis.leads_data: plane_projection.validate_shape( data.geometry.shape, value_name="Derived image payload", @@ -736,15 +739,15 @@ def _derived_metadata( ("source", source_metadata), ("output", output_metadata), ): - if metadata.plane_axis is not None: + if metadata.plane_axis.leads_data: raise ValueError( f"Derived image {owner_name} metadata retains an unconsumed " - f"{metadata.plane_axis.value!r} axis after the invocation " + f"{metadata.plane_axis.name!r} axis after the invocation " "selected one plane." ) # Read current source fields before deriving the output. This private # snapshot owns its stripped axes; no intermediate image is published. - source_context = source_metadata.replace_fields(plane_axis=None) + source_context = source_metadata.replace_fields(plane_axis=NoPlaneAxis) if not data.inherits_source_axes: source_context.axes = PayloadAxes() # Take current output calibration and geometry before replacing stale @@ -817,12 +820,12 @@ def has_complete_source_identity( planes = self.source_image_provenance_planes if ( plane_projection is not None - and plane_projection.plane_index is None + and plane_projection.preserves_complete_axis and plane_projection.axis_size > 1 - and self.plane_axis is None + and self.plane_axis.unstacked ): return False - if plane_projection is not None and self.plane_axis is not None: + if plane_projection is not None and self.plane_axis.leads_data: if self.plane_axis is not plane_projection.axis: raise ValueError( "Image payload plane declaration conflicts with the execution " @@ -843,16 +846,13 @@ def compose( cls, payloads: Sequence["ImagePayload"], *, - mode: "ImagePayloadMetadataCompositionMode | None" = None, + mode: type[RuntimePlaneAxis] = RuntimeSlicePlaneAxis, source_metadata: Sequence["ImagePayloadMetadata"] | None = None, ) -> "ImagePayloadMetadata": """Compose metadata for payloads assembled on a new leading axis.""" - resolved_mode = ( - ImagePayloadMetadataCompositionMode.STACK if mode is None else mode - ) return _ImagePayloadMetadataComposer( payloads=tuple(payloads), - mode=resolved_mode, + mode=mode, source_metadata_override=source_metadata, metadata_type=cls, ).compose() @@ -877,7 +877,7 @@ def persists_whole_image(self) -> bool: """Keep intrinsic pixels whole, excluding a distinct image-binding axis.""" return ( self.source_spatial_domain.persists_whole_image() - and self.plane_axis is not RuntimePlaneAxis.SOURCE_BINDING + and self.plane_axis.keeps_image_whole ) def for_leading_source_plane(self, plane_index: int) -> "ImagePayloadMetadata": @@ -897,11 +897,11 @@ def without_leading_plane_axis( projected = projection.project_source_provenance( self, self.source_provenance.with_runtime_planes_as_contributors() ) - if self.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE: + if self.plane_axis.one_plane_per_runtime_slice: projected.source_spatial_domain = ( projected.source_spatial_domain.for_intrinsic_plane() ) - projected.plane_axis = None + projected.plane_axis = NoPlaneAxis projected.axes = axes projected.source_plane_intensity_scales = () projected.source_plane_dtypes = () @@ -1046,7 +1046,7 @@ def project_source_planes( if not source_plane_selection: return self.attach_to(payload) complete_source_axis = tuple(range(self.source_provenance.source_plane_count)) - if self.plane_axis is None: + if self.plane_axis.unstacked: if source_plane_selection == complete_source_axis: return self.attach_to(payload) channel_axis = self.axis_index(ColourAxis, payload) @@ -1067,14 +1067,6 @@ def project_source_planes( "Declared source-image payload projection requires a declared " "plane or channel axis." ) - if self.plane_axis not in ( - RuntimePlaneAxis.RUNTIME_SLICE, - RuntimePlaneAxis.SOURCE_BINDING, - ): - raise ValueError( - "Source-image identity projection requires a runtime-slice, " - "source-binding, or channel axis." - ) axis_projection = RuntimePlaneAxisValueProjection.preserve( axis=self.plane_axis, axis_size=self.source_provenance.source_plane_count, @@ -1097,9 +1089,7 @@ def project_source_planes( return projected_planes[0] return stack_image_payloads( projected_planes, - metadata_mode=ImagePayloadMetadataCompositionMode.for_plane_axis( - self.plane_axis - ), + metadata_mode=self.plane_axis, ) def with_indexed_source_plane_provenance( @@ -1200,13 +1190,13 @@ def with_source_context_from( varying = self.source_provenance.varying_plane_component_values( AxisFamily.active().axes ) - if varying: + fallback_scalar = fallback_provenance.source_component_metadata + if varying and fallback_scalar is not None: + # A source without scalar component metadata keeps declaring none. fallback_provenance = fallback_provenance.with_source_component_metadata( { key: value - for key, value in ( - fallback_provenance.source_component_metadata or {} - ).items() + for key, value in fallback_scalar.items() if key not in varying } ) @@ -1215,12 +1205,12 @@ def with_source_context_from( ) axes = self.axes if self.axes.has_values else source.axes plane_axis = self.plane_axis - if plane_axis is None: + if plane_axis.unstacked: plane_axis = source.plane_axis - elif source.plane_axis is not None and source.plane_axis is not plane_axis: + elif source.plane_axis.leads_data and source.plane_axis is not plane_axis: raise ValueError( "Cannot combine image metadata with conflicting plane axes: " - f"{plane_axis.value!r} != {source.plane_axis.value!r}." + f"{plane_axis.name!r} != {source.plane_axis.name!r}." ) return self.replace_fields( source_provenance=source_provenance, @@ -1370,6 +1360,14 @@ def data(self) -> Any: def metadata(self) -> ImagePayloadMetadata: """Return the image metadata attached to this payload.""" + @property + def image_metadata(self) -> ImagePayloadMetadata: + return self.metadata + + @property + def array_data(self) -> Any: + return self.data + @property def mask(self) -> Any | None: """Return the validity mask this payload carries, if any.""" @@ -1396,11 +1394,11 @@ def inherits_source_axes(self) -> bool: return False @property - def declared_plane_axis(self) -> RuntimePlaneAxis | None: + def declared_plane_axis(self) -> type[RuntimePlaneAxis]: return self.metadata.plane_axis @property - def invocation_plane_axis(self) -> RuntimePlaneAxis | None: + def invocation_plane_axis(self) -> type[RuntimePlaneAxis]: """Return the plane axis a callable invocation iterates over.""" return self.metadata.plane_axis @@ -1475,16 +1473,15 @@ def slice_payload( data: Any, plane_index: int, *, - plane_axis: RuntimePlaneAxis | None = None, + plane_axis: type[RuntimePlaneAxis] = NoPlaneAxis, ) -> "ImagePayload": """Attach one source plane of this payload's context to slice pixels.""" metadata = self.metadata - if plane_axis is not None: - plane_axis = RuntimePlaneAxis(plane_axis) - if metadata.plane_axis is not None and metadata.plane_axis is not plane_axis: + if plane_axis.leads_data: + if metadata.plane_axis.leads_data and metadata.plane_axis is not plane_axis: raise ValueError( "Image slice projection axis conflicts with payload metadata: " - f"{plane_axis.value!r} != {metadata.plane_axis.value!r}." + f"{plane_axis.name!r} != {metadata.plane_axis.name!r}." ) if metadata.plane_axis is not plane_axis: metadata = metadata.replace_fields(plane_axis=plane_axis) @@ -1517,10 +1514,10 @@ def project_declared_source(self, source_image_name: str) -> "ImagePayload": @property def declares_whole_image_output(self) -> bool: """Whether a main-flow output publishes this payload as one whole image.""" - return self.metadata.plane_axis is None or self.metadata.persists_whole_image() + return self.metadata.plane_axis.unstacked or self.metadata.persists_whole_image() def keeps_whole_as_single_member( - self, *, declared_plane_axis: RuntimePlaneAxis | None, + self, *, declared_plane_axis: type[RuntimePlaneAxis], ) -> bool: """Whether this payload, alone in a cohort, is passed on without a new leading axis. @@ -1531,7 +1528,7 @@ def keeps_whole_as_single_member( rank a callable receives never depends on how many files matched. """ return self.metadata.persists_whole_image() or ( - declared_plane_axis is not None + declared_plane_axis.leads_data and declared_plane_axis is self.metadata.plane_axis ) @@ -1540,7 +1537,7 @@ def owns_output_surfaces(self) -> bool: """Whether this output names its own output surfaces (filename qualifiers).""" return False - def plane_axis_for_output_context(self, context: Any) -> RuntimePlaneAxis | None: + def plane_axis_for_output_context(self, context: Any) -> type[RuntimePlaneAxis]: """Return the plane axis one published output slice was taken along.""" del context return self.metadata.plane_axis @@ -1569,7 +1566,7 @@ def main_output_slices( for payload in unstack_image_payload_context( self, unstack(self.data), - default_plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + default_plane_axis=RuntimeSlicePlaneAxis, ) ) @@ -1616,7 +1613,7 @@ def with_undeclared_plane_axis( """Declare ``plane_projection``'s axis when this dense stack carries it undeclared.""" metadata = self.metadata if ( - self.declared_plane_axis is None + self.declared_plane_axis.unstacked and metadata.source_provenance.source_plane_count == plane_projection.axis_size and plane_projection.dense_shape_carries_axis(self.geometry.shape) ): @@ -1676,7 +1673,7 @@ def alignment_slices(self) -> tuple[Any, ...]: if slice_count is None: return (self,) projection = RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, axis_size=slice_count, + axis=RuntimeSlicePlaneAxis, axis_size=slice_count, ) return tuple( self.value_for_slice(projection.selected_plane(index)) @@ -1686,8 +1683,11 @@ def alignment_slices(self) -> tuple[Any, ...]: # -- RuntimeSliceProjectableValue ------------------------------------- def runtime_slice_count(self) -> int | None: - if self.metadata.plane_axis is not RuntimePlaneAxis.RUNTIME_SLICE: - return None + if self.metadata.plane_axis.one_plane_per_runtime_slice: + return self._leading_plane_count() + return None + + def _leading_plane_count(self) -> int: geometry = self.geometry if geometry.ndim <= AxisFamily.active().payload_spatial_rank: from openhcs.core.runtime_slice_projection import ( @@ -1727,22 +1727,27 @@ def owned_runtime_value(value: Any) -> Any: return PlainImagePayload(value) -def image_metadata_of(value: Any) -> "ImagePayloadMetadata": - """Image metadata of a runtime value of any artifact kind. +@dataclass(frozen=True, slots=True) +class UnownedRuntimeValue(RuntimeSliceInvariantValue): + """A value no runtime family owns (a mapping, a list of rows, a scalar).""" + + value: Any + + @property + def array_data(self) -> Any: + return self.value - Kind-generic layers (projection items, materialization, output recording) - hold values of every artifact kind. Only image payloads carry image - metadata; every other kind carries none. This is the one place that - decision is made until K2 gives each artifact kind its own metadata. - """ - value = owned_runtime_value(value) - return value.metadata if isinstance(value, ImagePayload) else ImagePayloadMetadata() +def as_runtime_value(value: Any) -> RuntimeSliceProjectableValue: + """Return any value as a member of the runtime-value family. -def array_data_of(value: Any) -> Any: - """Array of an image payload, or a value of another artifact kind unchanged.""" + Layers that hold values of every artifact kind call this once and then ask + the member (``image_metadata``, ``array_data``). + """ value = owned_runtime_value(value) - return value.data if isinstance(value, ImagePayload) else value + if isinstance(value, RuntimeSliceProjectableValue): + return value + return UnownedRuntimeValue(value) @dataclass(frozen=True, slots=True) @@ -1771,9 +1776,9 @@ def declares_whole_image_output(self) -> bool: """An undeclared main-flow output stacks runtime slices on its leading axis.""" return False - def plane_axis_for_output_context(self, context: Any) -> RuntimePlaneAxis: + def plane_axis_for_output_context(self, context: Any) -> type[RuntimePlaneAxis]: del context - return RuntimePlaneAxis.RUNTIME_SLICE + return RuntimeSlicePlaneAxis def with_data(self, data: Any) -> "PlainImagePayload": return type(self)(data) @@ -1886,9 +1891,9 @@ def preserved_image_plane_projection( from openhcs.core.runtime_slice_projection import RuntimeSliceProjection plane_axis = RuntimeSliceProjection.declared_plane_axis(value) - if plane_axis is None: + if plane_axis.unstacked: return RuntimeSliceProjection.preserved_context_for_value(value) - if plane_axis is RuntimePlaneAxis.RUNTIME_SLICE: + if plane_axis.one_plane_per_runtime_slice: invocation_projection = RuntimePlaneAxisValueProjection.from_projector( projector, plane_axis, @@ -1896,7 +1901,7 @@ def preserved_image_plane_projection( ) if ( invocation_projection is not None - and invocation_projection.plane_index is not None + and invocation_projection.selects_one_plane ): return None payload_projection = RuntimeSliceProjection.preserved_context_for_value(value) @@ -1914,8 +1919,8 @@ def preserved_image_plane_projection( f"the leading dimension, got shape {shape!r}." ) axis_size = shape[0] - return RuntimePlaneAxisValueProjection( - axis=RuntimePlaneAxis.SOURCE_BINDING, + return RuntimePlaneAxisValueProjection.for_plane( + axis=SourceBindingPlaneAxis, source_aliases=source_aliases, plane_index=value.metadata.source_provenance.source_alias_plane_index( source_aliases, @@ -1935,16 +1940,16 @@ def preserve_declared_image_payload_axis( from openhcs.core.runtime_slice_projection import RuntimeSliceProjection - def declared_axis(value: Any) -> RuntimePlaneAxis | None: + def declared_axis(value: Any) -> type[RuntimePlaneAxis]: plane_axis = RuntimeSliceProjection.declared_plane_axis(value) - if plane_axis is not None: + if plane_axis.leads_data: return plane_axis projection = RuntimeSliceProjection.preserved_context_for_value(value) - return None if projection is None else projection.axis + return NoPlaneAxis if projection is None else projection.axis - if declared_axis(output_value) is not None: + if declared_axis(output_value).leads_data: owner = output_value - elif declared_axis(source_payload) is not None: + elif declared_axis(source_payload).leads_data: owner = source_payload else: return None @@ -1964,7 +1969,7 @@ def project_image_mask_to_data_domain( if mask is None: return None if metadata is None: - metadata = image_metadata_of(data) + metadata = as_runtime_value(data).image_metadata data_array = runtime_array_operand(data) candidate = mask_array(mask) target = MemoryType(detect_memory_type(data_array)) @@ -2084,7 +2089,7 @@ def payloads_for_slices( slices: Sequence[RuntimeArrayData], ) -> list[RuntimeArrayData]: """Project each child once, keeping strict batch-mask cardinality.""" - if self.metadata.plane_axis is None: + if self.metadata.plane_axis.unstacked: if len(slices) != 1: raise ValueError( "Image payload produced multiple slices without a declared " @@ -2154,7 +2159,7 @@ def _mask_for_projected_slice( return None mask_array = np.asarray(self.mask) if ( - self.metadata.plane_axis is RuntimePlaneAxis.SOURCE_BINDING + self.metadata.plane_axis.one_plane_per_source_binding and slice_metadata.mask_domain(data_slice).accepts(mask_array.shape) ): candidate = mask_array @@ -2176,56 +2181,12 @@ def _mask_for_projected_slice( ) -class ImagePayloadMetadataCompositionMode(Enum): - """Source-provenance topology for composed image metadata.""" - - def __new__( - cls, - value: str, - plane_axis: RuntimePlaneAxis, - ): - member = object.__new__(cls) - member._value_ = value - member._plane_axis = plane_axis - return member - - STACK = ("stack", RuntimePlaneAxis.RUNTIME_SLICE) - BUNDLE = ("bundle", RuntimePlaneAxis.SOURCE_BINDING) - plane_axis = AliasProperty[RuntimePlaneAxis]("_plane_axis") - - @classmethod - def for_plane_axis( - cls, - plane_axis: RuntimePlaneAxis, - ) -> "ImagePayloadMetadataCompositionMode": - """Return the unique composition operation that creates an axis.""" - - matches = tuple(mode for mode in cls if mode.plane_axis is plane_axis) - if len(matches) != 1: - raise ValueError( - f"No unique image composition mode owns {plane_axis.value!r}." - ) - return matches[0] - - def preserves_plane_topology( - self, - plane_axis: RuntimePlaneAxis | None, - ) -> bool: - """Return whether composition retains an existing runtime-plane axis.""" - - return plane_axis is self.plane_axis or ( - plane_axis is None and self.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE - ) - - @dataclass(slots=True) class _ImagePayloadMetadataComposer: """Stateful implementation for composing image metadata on a leading axis.""" payloads: tuple["ImagePayload", ...] - mode: ImagePayloadMetadataCompositionMode = ( - ImagePayloadMetadataCompositionMode.STACK - ) + mode: type[RuntimePlaneAxis] = RuntimeSlicePlaneAxis source_metadata_override: Sequence[ImagePayloadMetadata] | None = None metadata_type: type[ImagePayloadMetadata] = ImagePayloadMetadata @@ -2252,7 +2213,7 @@ def source_plane_metadata_for_payload( metadata: ImagePayloadMetadata, ) -> ImagePayloadMetadata: """Return metadata for one payload on the newly composed leading axis.""" - if metadata.plane_axis is not None: + if metadata.plane_axis.leads_data: return metadata if metadata.source_provenance.source_plane_count == 1: return metadata.for_source_plane(0) @@ -2261,17 +2222,12 @@ def source_plane_metadata_for_payload( def compose(self) -> ImagePayloadMetadata: metadata_by_payload = self.source_metadata if not any(metadata.has_values for metadata in metadata_by_payload): - return self.metadata_type(plane_axis=self.mode.plane_axis) + return self.metadata_type(plane_axis=self.mode) source_metadata_by_payload = tuple( self.source_plane_metadata_for_payload(metadata) for metadata in metadata_by_payload ) - compose_provenance = ( - SourceImageProvenance.stack - if self.mode is ImagePayloadMetadataCompositionMode.STACK - else SourceImageProvenance.bundle - ) - source_provenance = compose_provenance( + source_provenance = self.mode.compose_provenance( tuple( metadata.source_provenance for metadata in source_metadata_by_payload ), @@ -2280,7 +2236,7 @@ def compose(self) -> ImagePayloadMetadata: ), preserve_single_topology=( len(source_metadata_by_payload) == 1 - and self.mode.preserves_plane_topology( + and self.mode.keeps_planes_of( source_metadata_by_payload[0].plane_axis ) ), @@ -2319,7 +2275,7 @@ def compose(self) -> ImagePayloadMetadata: self.payloads, metadata_by_payload, strict=True, ) ), - plane_axis=self.mode.plane_axis, + plane_axis=self.mode, ) @staticmethod @@ -2390,7 +2346,7 @@ class ImageMaskDomain: data_shape: tuple[int, ...] declared_axes: tuple[int, ...] = () - plane_axis: RuntimePlaneAxis | None = None + plane_axis: type[RuntimePlaneAxis] = NoPlaneAxis placement_axes: tuple[int, ...] | None = None def __post_init__(self) -> None: @@ -2463,11 +2419,11 @@ def projected_channel_mask( def shared_spatial_mask_shape(self) -> tuple[int, ...] | None: """Return a mask domain shared by declared source-binding planes.""" if ( - self.plane_axis is not RuntimePlaneAxis.SOURCE_BINDING - or self.placement_axes is None + self.plane_axis.one_plane_per_source_binding + and self.placement_axes is not None ): - return None - return tuple(self.data_shape[axis] for axis in self.placement_axes) + return tuple(self.data_shape[axis] for axis in self.placement_axes) + return None @property def undeclared_shape(self) -> tuple[int, ...] | None: diff --git a/openhcs/core/runtime_measurements.py b/openhcs/core/runtime_measurements.py index e9c1687e7c..9cf9211970 100644 --- a/openhcs/core/runtime_measurements.py +++ b/openhcs/core/runtime_measurements.py @@ -135,12 +135,12 @@ def runtime_slice_count(self) -> int | None: def value_for_slice(self, context: Any) -> "MeasurementTable": """Select the rows of one runtime slice.""" - from openhcs.core.runtime_plane_projection import RuntimePlaneAxis - if ( - context.axis is not RuntimePlaneAxis.RUNTIME_SLICE - or self.runtime_slice_row_domain() is None - ): + selects_slice_rows = ( + context.axis.one_plane_per_runtime_slice + and self.runtime_slice_row_domain() is not None + ) + if not selects_slice_rows: return self from openhcs.core.runtime_artifact_queries import ( MeasurementTableAxisProjection, diff --git a/openhcs/core/runtime_object_label_aggregation.py b/openhcs/core/runtime_object_label_aggregation.py index e3ed2e107d..50387b38c6 100644 --- a/openhcs/core/runtime_object_label_aggregation.py +++ b/openhcs/core/runtime_object_label_aggregation.py @@ -1,6 +1,8 @@ """Object-label reduction and PURE_2D slice aggregation.""" from __future__ import annotations +from openhcs.core.runtime_object_label_domains import PlaneLabelScope +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from collections.abc import Sequence from dataclasses import dataclass @@ -19,7 +21,6 @@ from openhcs.core.runtime_object_label_domains import ( ObjectLabelDomain, ObjectLabelDomainScope, - ObjectLabelPlaneDomainStrategy, ) from openhcs.core.runtime_object_labels import ( FinalLabels, @@ -90,7 +91,7 @@ def __init__( values: Sequence[ObjectLabelValue], memory_type: str, *, - plane_axis: RuntimePlaneAxis, + plane_axis: type[RuntimePlaneAxis], ) -> None: self.values: tuple[ObjectLabelValue, ...] = tuple(values) self.memory_type = memory_type @@ -102,7 +103,7 @@ def aggregate( values: Sequence[ObjectLabelValue], memory_type: str, *, - plane_axis: RuntimePlaneAxis, + plane_axis: type[RuntimePlaneAxis], ) -> ObjectLabelValue: values_tuple = tuple(values) if not values_tuple: @@ -146,16 +147,14 @@ def value_plane_id_domains( ) -> tuple[tuple[int, ...], ...]: """Return the object-id domain represented by one PURE_2D output slice.""" domain = value.object_label_domain() - return ObjectLabelPlaneDomainStrategy.for_enum_member( - domain.scope - ).identity_domains( + return domain.scope.identity_domains( value, domain=domain, ) @property - def domain_scope(self) -> ObjectLabelDomainScope: - return ObjectLabelDomainScope.PLANE + def domain_scope(self) -> type[ObjectLabelDomainScope]: + return PlaneLabelScope @property def domain(self) -> ObjectLabelDomain: @@ -165,7 +164,7 @@ def domain(self) -> ObjectLabelDomain: ) @property - def plane_axis(self) -> RuntimePlaneAxis: + def plane_axis(self) -> type[RuntimePlaneAxis]: return self.requested_plane_axis @property @@ -262,7 +261,7 @@ def output_value( """Build the aggregated object-label value.""" metadata = runtime_image_values.ImagePayloadMetadata.compose( self.values, - mode=runtime_image_values.ImagePayloadMetadataCompositionMode.STACK, + mode=RuntimeSlicePlaneAxis, ) return self.first.with_variants( variants, diff --git a/openhcs/core/runtime_object_label_building.py b/openhcs/core/runtime_object_label_building.py index 1095e7fcab..3c3acf7d04 100644 --- a/openhcs/core/runtime_object_label_building.py +++ b/openhcs/core/runtime_object_label_building.py @@ -1,6 +1,8 @@ """Construction of object-label values from source-image context.""" from __future__ import annotations +from openhcs.core.runtime_object_label_domains import PlaneLabelScope +from openhcs.core.runtime_plane_projection import NoPlaneAxis from dataclasses import dataclass, field from types import MappingProxyType @@ -32,7 +34,7 @@ class SourceImageObjectLabelBuildRequest: image: runtime_image_values.ImagePayload labels: object - domain_scope: ObjectLabelDomainScope | None = None + domain_scope: type[ObjectLabelDomainScope] | None = None plane_projection: RuntimePlaneAxisValueProjection | None = None declared_object_count: int | None = None declared_object_ids: tuple[int, ...] = () @@ -76,7 +78,7 @@ def payload( self.image, self.labels, ) - plane_axis = None + plane_axis = NoPlaneAxis else: if self.declared_object_count is not None or self.declared_object_ids: raise ValueError( @@ -84,7 +86,7 @@ def payload( "domains; payload-wide object counts or IDs are invalid." ) label_domain = PresentObjectLabelIdsDomainDeclaration( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, plane_projection=semantics, ).declared_domain(self.image, self.labels) plane_axis = semantics.axis @@ -125,24 +127,17 @@ def plane_semantics(self) -> RuntimePlaneAxisValueProjection | None: declare whether object IDs identify a whole volume or separate planes. Saved label-image admission supplies its projection at the ingress owner. """ - if self.domain_scope is ObjectLabelDomainScope.PAYLOAD: + scope = self.domain_scope + if scope is not None and scope.ids_span_payload: if self.plane_projection is not None: raise ValueError( "Payload-scoped object-label output cannot declare a plane " "projection." ) return None - if ( - self.domain_scope is not None - and self.domain_scope is not ObjectLabelDomainScope.PLANE - ): - raise TypeError( - "Source-image object-label domain_scope must be payload, plane, " - f"or None; got {self.domain_scope!r}." - ) projection = self.plane_projection if projection is None: - if self.domain_scope is ObjectLabelDomainScope.PLANE: + if scope is not None: raise ValueError( "Plane-scoped object-label output requires an exact plane " "projection." diff --git a/openhcs/core/runtime_object_label_domains.py b/openhcs/core/runtime_object_label_domains.py index bdf1c4100a..beac0a3c69 100644 --- a/openhcs/core/runtime_object_label_domains.py +++ b/openhcs/core/runtime_object_label_domains.py @@ -1,15 +1,15 @@ """Nominal runtime object label domains semantics.""" from __future__ import annotations +from openhcs.core.runtime_plane_projection import NoPlaneAxis +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from abc import ABC from abc import abstractmethod from collections.abc import Iterable from dataclasses import dataclass -from enum import Enum from metaclass_registry import AutoRegisterMeta from metaclass_registry import RegistryFamily from metaclass_registry import RegistryKeyAttribute -from metaclass_registry.strategies import EnumKeyedStrategyMixin from metaclass_registry.strategies import NominalTypeKeyedStrategyMixin from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.runtime_plane_projection import RuntimePlaneAxisProjector @@ -27,25 +27,35 @@ DeclaredObjectIds = tuple[int, ...] | list[int] | None -class ObjectLabelDomainScope(str, Enum): - """How declared object-label IDs apply across dense label planes.""" +class ObjectLabelDomainScope(ABC, metaclass=AutoRegisterMeta): + """How declared object-label IDs apply across dense label planes. - PAYLOAD = "payload" - PLANE = "plane" + Each scope is a subclass, used as a class; ``name`` is its spelling in + profiles and error text. A scope owns how its labels project onto + measurement planes. + """ - @classmethod - def common(cls, scopes: Any) -> "ObjectLabelDomainScope": + __registry_key__ = "name" + __skip_if_no_key__ = True + __registry__: ClassVar[dict[str, type[ObjectLabelDomainScope]]] = {} + + name: ClassVar[str | None] = None + ids_per_plane: ClassVar[bool] + """Each label plane declares its own object IDs.""" + ids_span_payload: ClassVar[bool] + """One set of object IDs covers the whole label value.""" + + @staticmethod + def require_member(value: object) -> type[ObjectLabelDomainScope]: + """Return ``value`` when it is an object-label domain scope.""" + if isinstance(value, type) and issubclass(value, ObjectLabelDomainScope): + return value + raise ValueError(f"{value!r} is not an object-label domain scope.") + + @staticmethod + def common(scopes: Any) -> type[ObjectLabelDomainScope]: """Return the one scope declared by every merged label value.""" - unique_scopes = tuple( - dict.fromkeys( - ( - cls( - scope, - ) - for scope in scopes - ) - ) - ) + unique_scopes = tuple(dict.fromkeys(scopes)) if len(unique_scopes) == 1: return unique_scopes[0] raise ValueError( @@ -53,606 +63,338 @@ def common(cls, scopes: Any) -> "ObjectLabelDomainScope": f"{unique_scopes!r}." ) + @classmethod + @abstractmethod + def validate_declared_ids( + cls, + *, + object_count: int | None, + object_ids: tuple[int, ...], + plane_id_domains: tuple[tuple[int, ...], ...], + ) -> None: + """Reject declared IDs that do not fit this scope.""" -@dataclass(frozen=True, slots=True) -class ObjectLabelDomain: - """Declared object-label identity domain metadata.""" - - declared_object_count: int | None = None - declared_object_ids: tuple[int, ...] = () - declared_object_id_domains: tuple[tuple[int, ...], ...] = () - scope: ObjectLabelDomainScope = ObjectLabelDomainScope.PAYLOAD + @classmethod + @abstractmethod + def plane_domains( + cls, + labels: Any, + *, + domain: ObjectLabelDomain, + ) -> tuple[tuple[int, ...], ...]: + """Return the object-id domain attached to each dense measurement plane.""" - def __post_init__(self) -> None: - if self.declared_object_count is not None: - count = int(self.declared_object_count) - if count < 0: - raise ValueError( - "ObjectLabelDomain.declared_object_count cannot be negative." - ) - object.__setattr__(self, "declared_object_count", count) - object.__setattr__( - self, - "declared_object_ids", - self._normalize_ids(self.declared_object_ids, "declared_object_ids"), - ) - object.__setattr__( - self, - "declared_object_id_domains", - tuple( - ( - self._normalize_ids(domain, "declared_object_id_domains") - for domain in self.declared_object_id_domains - ) - ), + @classmethod + def identity_domains( + cls, + labels: Any, + *, + domain: ObjectLabelDomain, + ) -> tuple[tuple[int, ...], ...]: + """Return object-id domains for identity rows represented by the payload.""" + return cls.plane_domains( + labels, + domain=domain, ) - object.__setattr__( - self, - "scope", - ObjectLabelDomainScope( - self.scope, - ), + + @classmethod + @abstractmethod + def measurement_object_row_identity( + cls, + declared_identity: MeasurementObjectRowIdentity, + ) -> MeasurementObjectRowIdentity: + """Return object-row identity after applying label-domain semantics.""" + + @classmethod + @abstractmethod + def present_domain( + cls, + labels: Any, + *, + plane_projection: RuntimePlaneAxisValueProjection | None, + ) -> ObjectLabelDomain: + """Declare the material IDs produced in an already-selected domain scope.""" + + @classmethod + @abstractmethod + def measurement_projection( + cls, + labels: "ObjectLabelValue", + projector: RuntimePlaneAxisProjector, + ) -> RuntimePlaneAxisValueProjection: + """Resolve the runtime row projection represented by label semantics.""" + + @classmethod + @abstractmethod + def measurement_axis_values( + cls, + labels: "ObjectLabelValue", + projection: RuntimePlaneAxisValueProjection, + ) -> tuple[int, ...]: + """Return exact measurement-row axis values represented by labels.""" + + @classmethod + def required_measurement_axis_values( + cls, + labels: "ObjectLabelValue", + projection: RuntimePlaneAxisValueProjection, + ) -> tuple[int, ...]: + """Return row-axis values required by nonempty declared label planes.""" + + axis_values = cls.measurement_axis_values(labels, projection) + plane_domains = cls.plane_domains( + labels, + domain=labels.object_label_domain(), ) - if self.scope is ObjectLabelDomainScope.PAYLOAD: - if self.declared_object_id_domains: - raise ValueError( - "Payload-scoped object-label domains cannot declare per-plane " - "object-ID domains." - ) - return - if not self.declared_object_id_domains: + if len(axis_values) != len(plane_domains): raise ValueError( - "Plane-scoped object-label domains require one explicit object-ID " - "domain per plane." + "Object-label measurement axis and declared plane domains have " + f"different cardinalities: {len(axis_values)} != " + f"{len(plane_domains)}." ) - if self.declared_object_count is not None or self.declared_object_ids: - raise ValueError( - "Plane-scoped object-label domains cannot also declare a payload-wide " - "object count or object-ID domain." + return tuple( + axis_value + for axis_value, object_ids in zip( + axis_values, + plane_domains, + strict=True, ) + if object_ids + ) - @staticmethod - def _normalize_ids( - ids: tuple[int, ...] | list[int], field_name: str + @classmethod + @abstractmethod + def measurement_planes( + cls, + labels: "ObjectLabelValue", + projection: RuntimePlaneAxisValueProjection | None, + ) -> tuple["ObjectLabelValue", ...]: + """Return label values in the same order as measurement axis values.""" + + @classmethod + def declared_measurement_planes( + cls, + labels: "ObjectLabelValue", + ) -> tuple["ObjectLabelValue", ...]: + """Project labels through their own declared plane-domain semantics.""" + return cls.measurement_planes(labels, cls.value_projection(labels)) + + @classmethod + def declared_measurement_axis_values( + cls, + labels: "ObjectLabelValue", ) -> tuple[int, ...]: - normalized = tuple((int(object_id) for object_id in ids)) - if any((object_id <= 0 for object_id in normalized)): - raise ValueError(f"ObjectLabelDomain.{field_name} IDs must be positive.") - return tuple(sorted(dict.fromkeys(normalized))) + """Return local row-axis values for every declared measurement plane.""" + return tuple(range(len(cls.declared_measurement_planes(labels)))) @classmethod - def declared( + @abstractmethod + def measurement_reference_source_provenance( cls, - *, - scope: ObjectLabelDomainScope = ObjectLabelDomainScope.PAYLOAD, - declared_object_count: int | None = None, - declared_object_ids: DeclaredObjectIds = (), - declared_object_id_domains: tuple[tuple[int, ...], ...] = (), - ) -> "ObjectLabelDomain": - """Return a normalized object-label domain declaration.""" - return cls( - declared_object_count=declared_object_count, - declared_object_ids=( - tuple(declared_object_ids) if declared_object_ids is not None else () - ), - declared_object_id_domains=declared_object_id_domains, - scope=scope, - ) + labels: "ObjectLabelValue", + ) -> "SourceImageProvenance": + """Return source lineage in this domain's image-reference topology.""" - def explicit_id_domain(self) -> tuple[int, ...] | None: - """Return this declaration as IDs, or ``None`` if it is undeclared.""" - if self.declared_object_ids: - return self.declared_object_ids - if self.declared_object_count is not None: - return tuple(range(1, self.declared_object_count + 1)) - return None + @classmethod + @abstractmethod + def value_plane_axis( + cls, + plane_axis: type[RuntimePlaneAxis], + ) -> type[RuntimePlaneAxis]: + """Return the plane axis valid for this declared domain scope.""" - def require_explicit_id_domain(self, *, context: str) -> tuple[int, ...]: - """Return declared IDs or reject an undeclared semantic domain.""" - object_ids = self.explicit_id_domain() - if object_ids is None: + @classmethod + @abstractmethod + def value_projection( + cls, + labels: "ObjectLabelValue", + ) -> RuntimePlaneAxisValueProjection | None: + """Return the exact runtime plane projection declared by a label value.""" + + +class PayloadLabelScope(ObjectLabelDomainScope): + """Declared IDs describe one indivisible label value.""" + + name = "payload" + ids_per_plane = False + ids_span_payload = True + + @classmethod + def validate_declared_ids( + cls, + *, + object_count: int | None, + object_ids: tuple[int, ...], + plane_id_domains: tuple[tuple[int, ...], ...], + ) -> None: + del object_count, object_ids + if plane_id_domains: raise ValueError( - f"{context} requires an explicit object-ID domain; label contents " - "cannot declare semantic identity." + "Payload-scoped object-label domains cannot declare per-plane " + "object-ID domains." ) - return object_ids - def with_scope(self, scope: ObjectLabelDomainScope) -> "ObjectLabelDomain": - """Return this object-label declaration with the requested domain scope.""" - normalized_scope = ObjectLabelDomainScope( - scope, - ) - if ( - normalized_scope is ObjectLabelDomainScope.PAYLOAD - and self.scope is ObjectLabelDomainScope.PLANE - and self.declared_object_id_domains - ): - declared_object_ids = tuple( - sorted( - { - object_id - for domain in self.declared_object_id_domains - for object_id in domain - } - ) + @classmethod + def plane_domains( + cls, + labels: Any, + *, + domain: ObjectLabelDomain, + ) -> tuple[tuple[int, ...], ...]: + if domain.scope is not cls: + raise ValueError( + "Payload object-label domain strategy requires a payload-scoped " + f"declaration, got {domain.scope.name!r}." ) - return ObjectLabelDomain( - declared_object_count=self.declared_object_count, - declared_object_ids=declared_object_ids, - scope=normalized_scope, + if domain.declared_object_id_domains: + raise ValueError( + "Payload-scoped object labels cannot declare per-plane object-ID " + "domains." ) - return ObjectLabelDomain( - declared_object_count=self.declared_object_count, - declared_object_ids=self.declared_object_ids, - declared_object_id_domains=self.declared_object_id_domains, - scope=normalized_scope, + object_ids = domain.require_explicit_id_domain( + context="Payload-scoped object labels" ) + return (object_ids,) - def project_slice(self, slice_index: int, slice_count: int) -> "ObjectLabelDomain": - """Return the object-label domain carried by one PURE_2D slice.""" - normalized_index = int(slice_index) - normalized_count = int(slice_count) - if normalized_count <= 0: - raise ValueError("Object-label slice_count must be positive.") - if normalized_index < 0 or normalized_index >= normalized_count: - raise ValueError( - f"Object-label slice_index {normalized_index} is outside slice_count {normalized_count}." - ) - if self.scope is not ObjectLabelDomainScope.PLANE: - raise ValueError( - "Runtime-slice projection requires a plane-scoped object-label " - f"domain, got {self.scope.value!r}." - ) - if not self.declared_object_id_domains: - raise ValueError( - "Plane-scoped object-label projection requires one declared " - "object-ID domain per runtime slice." - ) - if len(self.declared_object_id_domains) != normalized_count: - raise ValueError( - f"Plane-scoped object-label domains must match PURE_2D slice count: {len(self.declared_object_id_domains)} domains for {normalized_count} slices." - ) - return self.project_planes((normalized_index,)) + @classmethod + def measurement_object_row_identity( + cls, + declared_identity: MeasurementObjectRowIdentity, + ) -> MeasurementObjectRowIdentity: + """Keep the row identity declared for one indivisible payload.""" + return declared_identity - def project_planes(self, plane_indices: Iterable[int]) -> "ObjectLabelDomain": - """Return the object-label domain carried by selected plane indexes.""" - normalized_indices = tuple((int(index) for index in plane_indices)) - if self.scope is not ObjectLabelDomainScope.PLANE: - raise ValueError( - "Object-label plane projection requires a plane-scoped domain." - ) - if not self.declared_object_id_domains: - raise ValueError( - "Object-label plane projection requires declared per-plane " - "object-ID domains." - ) - if not normalized_indices: - domains: tuple[tuple[int, ...], ...] = () - elif any( - ( - index < 0 or index >= len(self.declared_object_id_domains) - for index in normalized_indices - ) - ): + @classmethod + def present_domain( + cls, + labels: Any, + *, + plane_projection: RuntimePlaneAxisValueProjection | None, + ) -> ObjectLabelDomain: + if plane_projection is not None: raise ValueError( - f"Object-label plane projection index is outside declared domain count {len(self.declared_object_id_domains)}: {normalized_indices!r}." - ) - else: - domains = tuple( - (self.declared_object_id_domains[index] for index in normalized_indices) - ) - if len(domains) == 1: - return ObjectLabelDomain.declared( - scope=ObjectLabelDomainScope.PAYLOAD, - declared_object_count=0 if not domains[0] else None, - declared_object_ids=domains[0], + "Payload-scoped object-label declaration cannot consume a plane " + "projection." ) + object_ids = ObjectLabelIdDomainStrategy.for_value(labels).present_ids(labels) return ObjectLabelDomain.declared( - scope=ObjectLabelDomainScope.PLANE, declared_object_id_domains=domains + scope=cls, + declared_object_count=0 if not object_ids else None, + declared_object_ids=object_ids, ) - -class ObjectLabelDomainMetadata(ABC): - """Nominal provider for object-label ID domain metadata.""" - - @abstractmethod - def object_label_domain(self) -> ObjectLabelDomain: - """Return the declared object-label identity domain.""" - - -class ObjectLabelDomainMetadataStrategy( - NominalTypeKeyedStrategyMixin, ABC, metaclass=AutoRegisterMeta -): - """Registered extractor for nominal object-label domain metadata.""" - - value_type: ClassVar[type[object] | tuple[type[object], ...] | None] = None - value_type_label: ClassVar[str | None] = None - __registry_family__ = RegistryFamily(RegistryKeyAttribute.VALUE_TYPE_LABEL) - @classmethod - def for_value(cls, value: object) -> "ObjectLabelDomainMetadataStrategy": - return cls.require_nominal_value( - value, - context="Object-label domain metadata", + def measurement_projection( + cls, + labels: "ObjectLabelValue", + projector: RuntimePlaneAxisProjector, + ) -> RuntimePlaneAxisValueProjection: + projection = RuntimePlaneAxisValueProjection.require_from_projector( + projector, + RuntimeSlicePlaneAxis, ) - - @abstractmethod - def object_label_domain(self, value: object) -> ObjectLabelDomain: - """Return the declared object-label identity domain for ``value``.""" - - -class NominalObjectLabelDomainMetadataStrategy(ObjectLabelDomainMetadataStrategy): - """Use the domain declared by a nominal object-label domain provider.""" - - value_type = ObjectLabelDomainMetadata - - def object_label_domain(self, value: object) -> ObjectLabelDomain: - if not isinstance(value, ObjectLabelDomainMetadata): - raise TypeError( - f"NominalObjectLabelDomainMetadataStrategy requires ObjectLabelDomainMetadata, got {type(value).__name__}." + if projection.preserves_complete_axis and projection.axis_size != 1: + raise ValueError( + "Payload-scoped object-label measurement lookup requires one " + "selected runtime slice, but the invocation preserves " + f"{projection.axis_size} {projection.axis.name!r} plane(s)." ) - return value.object_label_domain() - - -class ObjectLabelIdDomainStrategy( - NominalTypeKeyedStrategyMixin, ABC, metaclass=AutoRegisterMeta -): - """Registered extractor for materially present positive object-label IDs.""" - - value_type: ClassVar[type[object] | None] = None - value_type_label: ClassVar[str | None] = None - __registry_family__ = RegistryFamily(RegistryKeyAttribute.VALUE_TYPE_LABEL) + return projection @classmethod - def for_value(cls, labels: Any) -> "ObjectLabelIdDomainStrategy": - strategy = cls.for_nominal_value(labels) - if strategy is None: - raise TypeError( - "Object-label ID extraction requires a registered nominal payload " - f"type, got {type(labels).__name__}." - ) - return strategy - - @abstractmethod - def present_ids(self, labels: Any) -> tuple[int, ...]: - """Return positive object IDs materially present in the label payload.""" - - def max_present_id(self, labels: Any) -> int: - """Return the largest materially present positive object ID.""" - ids = self.present_ids(labels) - return max(ids) if ids else 0 - - @staticmethod - def positive_ids_from_array(labels: Any) -> tuple[int, ...]: - """Return positive IDs from one numeric dense object-label array.""" - import numpy as np - - label_array = np.asarray(labels) - if not label_array.size or not ( - np.issubdtype(label_array.dtype, np.number) - or np.issubdtype(label_array.dtype, np.bool_) - ): - return () - dense_integer_domain = DenseIntegerObjectLabelIdDomain.from_array(label_array) - if dense_integer_domain is not None: - return dense_integer_domain.present_ids() - return tuple( - (int(object_id) for object_id in np.unique(label_array) if object_id > 0) - ) - + def measurement_axis_values( + cls, + labels: "ObjectLabelValue", + projection: RuntimePlaneAxisValueProjection, + ) -> tuple[int, ...]: + if projection.selects_one_plane: + return projection.selected_planes() + return (0,) -@dataclass(frozen=True, slots=True) -class DenseIntegerObjectLabelIdDomain: - """Exact present-ID extractor for bounded nonnegative dense integer labels.""" + @classmethod + def required_measurement_axis_values( + cls, + labels: "ObjectLabelValue", + projection: RuntimePlaneAxisValueProjection, + ) -> tuple[int, ...]: + """Payload-scoped measurements need no per-plane row coordinate.""" - labels: np.ndarray - max_label: int + del labels, projection + return () @classmethod - def from_array(cls, labels: np.ndarray) -> "DenseIntegerObjectLabelIdDomain | None": - if not ( - np.issubdtype(labels.dtype, np.integer) - or np.issubdtype(labels.dtype, np.bool_) - ): - return None - if labels.size == 0: - return None - min_label = int(labels.min()) - if min_label < 0: - return None - max_label = int(labels.max()) - if max_label > labels.size: - return None - return cls(labels=labels, max_label=max_label) + def measurement_planes( + cls, + labels: "ObjectLabelValue", + projection: RuntimePlaneAxisValueProjection | None, + ) -> tuple["ObjectLabelValue", ...]: + del projection + return (labels,) - def present_ids(self) -> tuple[int, ...]: - if self.max_label <= 0: - return () - counts = np.bincount( - np.asarray(self.labels, dtype=np.int64).ravel(), - minlength=self.max_label + 1, - ) - present_ids = np.flatnonzero(counts) - return tuple((int(label_id) for label_id in present_ids if label_id > 0)) + @classmethod + def measurement_reference_source_provenance( + cls, + labels: "ObjectLabelValue", + ) -> "SourceImageProvenance": + """Keep volume planes as contributors to one payload-scoped image.""" + return labels.source_provenance.with_runtime_planes_as_contributors() -class DenseArrayObjectLabelIdDomainStrategy(ObjectLabelIdDomainStrategy): - """Extract present object IDs from dense NumPy label arrays.""" + @classmethod + def value_plane_axis( + cls, + plane_axis: type[RuntimePlaneAxis], + ) -> type[RuntimePlaneAxis]: + del plane_axis + return NoPlaneAxis - value_type = np.ndarray + @classmethod + def value_projection( + cls, + labels: "ObjectLabelValue", + ) -> None: + del labels + return None - def present_ids(self, labels: Any) -> tuple[int, ...]: - return self.positive_ids_from_array(labels) +class PlaneLabelScope(ObjectLabelDomainScope): + """Declared IDs map exactly to the declared label-plane stack.""" -class ObjectLabelPlaneDomainStrategy( - EnumKeyedStrategyMixin[ObjectLabelDomainScope], ABC, metaclass=AutoRegisterMeta -): - """Projection strategy from object-label domain metadata to measurement planes.""" + name = "plane" + ids_per_plane = True + ids_span_payload = False - __registry_family__ = RegistryFamily(RegistryKeyAttribute.STRATEGY_LABEL) - __enum_member_attr__ = "scope" - scope: ClassVar[ObjectLabelDomainScope] - strategy_label: ClassVar[str | None] = None + @classmethod + def validate_declared_ids( + cls, + *, + object_count: int | None, + object_ids: tuple[int, ...], + plane_id_domains: tuple[tuple[int, ...], ...], + ) -> None: + if not plane_id_domains: + raise ValueError( + "Plane-scoped object-label domains require one explicit object-ID " + "domain per plane." + ) + if object_count is not None or object_ids: + raise ValueError( + "Plane-scoped object-label domains cannot also declare a payload-wide " + "object count or object-ID domain." + ) - @abstractmethod + @classmethod def plane_domains( - self, + cls, labels: Any, *, domain: ObjectLabelDomain, ) -> tuple[tuple[int, ...], ...]: - """Return the object-id domain attached to each dense measurement plane.""" - - def identity_domains( - self, - labels: Any, - *, - domain: ObjectLabelDomain, - ) -> tuple[tuple[int, ...], ...]: - """Return object-id domains for identity rows represented by the payload.""" - return self.plane_domains( - labels, - domain=domain, - ) - - @abstractmethod - def measurement_object_row_identity( - self, - declared_identity: MeasurementObjectRowIdentity, - ) -> MeasurementObjectRowIdentity: - """Return object-row identity after applying label-domain semantics.""" - - @abstractmethod - def present_domain( - self, - labels: Any, - *, - plane_projection: RuntimePlaneAxisValueProjection | None, - ) -> ObjectLabelDomain: - """Declare the material IDs produced in an already-selected domain scope.""" - - @abstractmethod - def measurement_projection( - self, - labels: "ObjectLabelValue", - projector: RuntimePlaneAxisProjector, - ) -> RuntimePlaneAxisValueProjection: - """Resolve the runtime row projection represented by label semantics.""" - - @abstractmethod - def measurement_axis_values( - self, - labels: "ObjectLabelValue", - projection: RuntimePlaneAxisValueProjection, - ) -> tuple[int, ...]: - """Return exact measurement-row axis values represented by labels.""" - - def required_measurement_axis_values( - self, - labels: "ObjectLabelValue", - projection: RuntimePlaneAxisValueProjection, - ) -> tuple[int, ...]: - """Return row-axis values required by nonempty declared label planes.""" - - axis_values = self.measurement_axis_values(labels, projection) - plane_domains = self.plane_domains( - labels, - domain=labels.object_label_domain(), - ) - if len(axis_values) != len(plane_domains): - raise ValueError( - "Object-label measurement axis and declared plane domains have " - f"different cardinalities: {len(axis_values)} != " - f"{len(plane_domains)}." - ) - return tuple( - axis_value - for axis_value, object_ids in zip( - axis_values, - plane_domains, - strict=True, - ) - if object_ids - ) - - @abstractmethod - def measurement_planes( - self, - labels: "ObjectLabelValue", - projection: RuntimePlaneAxisValueProjection | None, - ) -> tuple["ObjectLabelValue", ...]: - """Return label values in the same order as measurement axis values.""" - - def declared_measurement_planes( - self, - labels: "ObjectLabelValue", - ) -> tuple["ObjectLabelValue", ...]: - """Project labels through their own declared plane-domain semantics.""" - return self.measurement_planes(labels, self.value_projection(labels)) - - def declared_measurement_axis_values( - self, - labels: "ObjectLabelValue", - ) -> tuple[int, ...]: - """Return local row-axis values for every declared measurement plane.""" - return tuple(range(len(self.declared_measurement_planes(labels)))) - - @abstractmethod - def measurement_reference_source_provenance( - self, - labels: "ObjectLabelValue", - ) -> "SourceImageProvenance": - """Return source lineage in this domain's image-reference topology.""" - - @abstractmethod - def value_plane_axis( - self, - plane_axis: RuntimePlaneAxis | None, - ) -> RuntimePlaneAxis | None: - """Return the plane axis valid for this declared domain scope.""" - - @abstractmethod - def value_projection( - self, - labels: "ObjectLabelValue", - ) -> RuntimePlaneAxisValueProjection | None: - """Return the exact runtime plane projection declared by a label value.""" - - -class PayloadObjectLabelPlaneDomainStrategy(ObjectLabelPlaneDomainStrategy): - """Payload-scope declarations describe one indivisible label value.""" - - scope = ObjectLabelDomainScope.PAYLOAD - - def plane_domains( - self, - labels: Any, - *, - domain: ObjectLabelDomain, - ) -> tuple[tuple[int, ...], ...]: - if domain.scope is not self.scope: - raise ValueError( - "Payload object-label domain strategy requires a payload-scoped " - f"declaration, got {domain.scope.value!r}." - ) - if domain.declared_object_id_domains: - raise ValueError( - "Payload-scoped object labels cannot declare per-plane object-ID " - "domains." - ) - object_ids = domain.require_explicit_id_domain( - context="Payload-scoped object labels" - ) - return (object_ids,) - - def measurement_object_row_identity( - self, - declared_identity: MeasurementObjectRowIdentity, - ) -> MeasurementObjectRowIdentity: - """Keep the row identity declared for one indivisible payload.""" - return declared_identity - - def present_domain( - self, - labels: Any, - *, - plane_projection: RuntimePlaneAxisValueProjection | None, - ) -> ObjectLabelDomain: - if plane_projection is not None: - raise ValueError( - "Payload-scoped object-label declaration cannot consume a plane " - "projection." - ) - object_ids = ObjectLabelIdDomainStrategy.for_value(labels).present_ids(labels) - return ObjectLabelDomain.declared( - scope=self.scope, - declared_object_count=0 if not object_ids else None, - declared_object_ids=object_ids, - ) - - def measurement_projection( - self, - labels: "ObjectLabelValue", - projector: RuntimePlaneAxisProjector, - ) -> RuntimePlaneAxisValueProjection: - projection = RuntimePlaneAxisValueProjection.require_from_projector( - projector, - RuntimePlaneAxis.RUNTIME_SLICE, - ) - if projection.plane_index is None and projection.axis_size != 1: - raise ValueError( - "Payload-scoped object-label measurement lookup requires one " - "selected runtime slice, but the invocation preserves " - f"{projection.axis_size} {projection.axis.value!r} plane(s)." - ) - return projection - - def measurement_axis_values( - self, - labels: "ObjectLabelValue", - projection: RuntimePlaneAxisValueProjection, - ) -> tuple[int, ...]: - plane_index = projection.plane_index - return (0,) if plane_index is None else (plane_index,) - - def required_measurement_axis_values( - self, - labels: "ObjectLabelValue", - projection: RuntimePlaneAxisValueProjection, - ) -> tuple[int, ...]: - """Payload-scoped measurements need no per-plane row coordinate.""" - - del labels, projection - return () - - def measurement_planes( - self, - labels: "ObjectLabelValue", - projection: RuntimePlaneAxisValueProjection | None, - ) -> tuple["ObjectLabelValue", ...]: - del projection - return (labels,) - - def measurement_reference_source_provenance( - self, - labels: "ObjectLabelValue", - ) -> "SourceImageProvenance": - """Keep volume planes as contributors to one payload-scoped image.""" - - return labels.source_provenance.with_runtime_planes_as_contributors() - - def value_plane_axis( - self, - plane_axis: RuntimePlaneAxis | None, - ) -> None: - del plane_axis - return None - - def value_projection( - self, - labels: "ObjectLabelValue", - ) -> None: - del labels - return None - -class PlaneObjectLabelPlaneDomainStrategy(ObjectLabelPlaneDomainStrategy): - """Plane-scope declarations map exactly to the declared label-plane stack.""" - - scope = ObjectLabelDomainScope.PLANE - - def plane_domains( - self, - labels: Any, - *, - domain: ObjectLabelDomain, - ) -> tuple[tuple[int, ...], ...]: - - if domain.scope is not self.scope: + if domain.scope is not cls: raise ValueError( "Plane object-label domain strategy requires a plane-scoped " - f"declaration, got {domain.scope.value!r}." + f"declaration, got {domain.scope.name!r}." ) if domain.declared_object_count is not None or domain.declared_object_ids: raise ValueError( @@ -673,16 +415,18 @@ def plane_domains( ) return domain.declared_object_id_domains + @classmethod def measurement_object_row_identity( - self, + cls, declared_identity: MeasurementObjectRowIdentity, ) -> MeasurementObjectRowIdentity: """Use labels within the runtime-slice/object identity product.""" del declared_identity return MeasurementObjectRowIdentity.LABEL_ID + @classmethod def present_domain( - self, + cls, labels: Any, *, plane_projection: RuntimePlaneAxisValueProjection | None, @@ -703,7 +447,7 @@ def present_domain( context="Object-label present-domain declaration", ) return ObjectLabelDomain.declared( - scope=self.scope, + scope=cls, declared_object_id_domains=tuple( ObjectLabelIdDomainStrategy.for_value(plane).present_ids(plane) for plane in ( @@ -717,29 +461,29 @@ def present_domain( ), ) + @classmethod def measurement_projection( - self, + cls, labels: "ObjectLabelValue", projector: RuntimePlaneAxisProjector, ) -> RuntimePlaneAxisValueProjection: return RuntimePlaneAxisValueProjection.require_from_projector( projector, - self.value_plane_axis(labels.plane_axis), + cls.value_plane_axis(labels.plane_axis), labels.source_aliases, ) + @classmethod def measurement_axis_values( - self, + cls, labels: "ObjectLabelValue", projection: RuntimePlaneAxisValueProjection, ) -> tuple[int, ...]: - plane_index = projection.plane_index - if plane_index is not None: - return (plane_index,) - return tuple(range(projection.axis_size)) + return projection.selected_planes() + @classmethod def measurement_planes( - self, + cls, labels: "ObjectLabelValue", projection: RuntimePlaneAxisValueProjection | None, ) -> tuple["ObjectLabelValue", ...]: @@ -752,9 +496,8 @@ def measurement_planes( "Plane-scoped object-label measurement requires an exact runtime " "plane projection." ) - plane_index = projection.plane_index plane_count = labels.declared_plane_count() - if plane_index is not None: + if projection.selects_one_plane: if plane_count is not None: raise ValueError( "Grouped plane-scoped object labels must already be projected " @@ -784,29 +527,32 @@ def measurement_planes( for index in range(projection.axis_size) ) + @classmethod def measurement_reference_source_provenance( - self, + cls, labels: "ObjectLabelValue", ) -> "SourceImageProvenance": """Preserve the projectable source planes declared by plane scope.""" return labels.source_provenance + @classmethod def value_plane_axis( - self, - plane_axis: RuntimePlaneAxis | None, - ) -> RuntimePlaneAxis: - if plane_axis is None: + cls, + plane_axis: type[RuntimePlaneAxis], + ) -> type[RuntimePlaneAxis]: + if plane_axis.unstacked: raise ValueError( "Plane-scoped object labels require a declared runtime plane axis." ) return plane_axis + @classmethod def value_projection( - self, + cls, labels: "ObjectLabelValue", ) -> RuntimePlaneAxisValueProjection: - plane_axis = self.value_plane_axis(labels.plane_axis) + plane_axis = cls.value_plane_axis(labels.plane_axis) return RuntimePlaneAxisValueProjection.preserve( axis=plane_axis, axis_size=len(labels.object_label_domain().declared_object_id_domains), @@ -814,6 +560,316 @@ def value_projection( ) +@dataclass(frozen=True, slots=True) +class ObjectLabelDomain: + """Declared object-label identity domain metadata.""" + + declared_object_count: int | None = None + declared_object_ids: tuple[int, ...] = () + declared_object_id_domains: tuple[tuple[int, ...], ...] = () + scope: type[ObjectLabelDomainScope] = PayloadLabelScope + + def __post_init__(self) -> None: + if self.declared_object_count is not None: + count = int(self.declared_object_count) + if count < 0: + raise ValueError( + "ObjectLabelDomain.declared_object_count cannot be negative." + ) + object.__setattr__(self, "declared_object_count", count) + object.__setattr__( + self, + "declared_object_ids", + self._normalize_ids(self.declared_object_ids, "declared_object_ids"), + ) + object.__setattr__( + self, + "declared_object_id_domains", + tuple( + ( + self._normalize_ids(domain, "declared_object_id_domains") + for domain in self.declared_object_id_domains + ) + ), + ) + ObjectLabelDomainScope.require_member(self.scope).validate_declared_ids( + object_count=self.declared_object_count, + object_ids=self.declared_object_ids, + plane_id_domains=self.declared_object_id_domains, + ) + + @staticmethod + def _normalize_ids( + ids: tuple[int, ...] | list[int], field_name: str + ) -> tuple[int, ...]: + normalized = tuple((int(object_id) for object_id in ids)) + if any((object_id <= 0 for object_id in normalized)): + raise ValueError(f"ObjectLabelDomain.{field_name} IDs must be positive.") + return tuple(sorted(dict.fromkeys(normalized))) + + @classmethod + def declared( + cls, + *, + scope: type[ObjectLabelDomainScope] = PayloadLabelScope, + declared_object_count: int | None = None, + declared_object_ids: DeclaredObjectIds = (), + declared_object_id_domains: tuple[tuple[int, ...], ...] = (), + ) -> "ObjectLabelDomain": + """Return a normalized object-label domain declaration.""" + return cls( + declared_object_count=declared_object_count, + declared_object_ids=( + tuple(declared_object_ids) if declared_object_ids is not None else () + ), + declared_object_id_domains=declared_object_id_domains, + scope=scope, + ) + + def explicit_id_domain(self) -> tuple[int, ...] | None: + """Return this declaration as IDs, or ``None`` if it is undeclared.""" + if self.declared_object_ids: + return self.declared_object_ids + if self.declared_object_count is not None: + return tuple(range(1, self.declared_object_count + 1)) + return None + + def require_explicit_id_domain(self, *, context: str) -> tuple[int, ...]: + """Return declared IDs or reject an undeclared semantic domain.""" + object_ids = self.explicit_id_domain() + if object_ids is None: + raise ValueError( + f"{context} requires an explicit object-ID domain; label contents " + "cannot declare semantic identity." + ) + return object_ids + + def with_scope(self, scope: type[ObjectLabelDomainScope]) -> "ObjectLabelDomain": + """Return this object-label declaration with the requested domain scope.""" + normalized_scope = ObjectLabelDomainScope.require_member(scope) + if ( + normalized_scope.ids_span_payload + and self.scope.ids_per_plane + and self.declared_object_id_domains + ): + declared_object_ids = tuple( + sorted( + { + object_id + for domain in self.declared_object_id_domains + for object_id in domain + } + ) + ) + return ObjectLabelDomain( + declared_object_count=self.declared_object_count, + declared_object_ids=declared_object_ids, + scope=normalized_scope, + ) + return ObjectLabelDomain( + declared_object_count=self.declared_object_count, + declared_object_ids=self.declared_object_ids, + declared_object_id_domains=self.declared_object_id_domains, + scope=normalized_scope, + ) + + def project_slice(self, slice_index: int, slice_count: int) -> "ObjectLabelDomain": + """Return the object-label domain carried by one PURE_2D slice.""" + normalized_index = int(slice_index) + normalized_count = int(slice_count) + if normalized_count <= 0: + raise ValueError("Object-label slice_count must be positive.") + if normalized_index < 0 or normalized_index >= normalized_count: + raise ValueError( + f"Object-label slice_index {normalized_index} is outside slice_count {normalized_count}." + ) + if self.scope.ids_span_payload: + raise ValueError( + "Runtime-slice projection requires a plane-scoped object-label " + f"domain, got {self.scope.name!r}." + ) + if not self.declared_object_id_domains: + raise ValueError( + "Plane-scoped object-label projection requires one declared " + "object-ID domain per runtime slice." + ) + if len(self.declared_object_id_domains) != normalized_count: + raise ValueError( + f"Plane-scoped object-label domains must match PURE_2D slice count: {len(self.declared_object_id_domains)} domains for {normalized_count} slices." + ) + return self.project_planes((normalized_index,)) + + def project_planes(self, plane_indices: Iterable[int]) -> "ObjectLabelDomain": + """Return the object-label domain carried by selected plane indexes.""" + normalized_indices = tuple((int(index) for index in plane_indices)) + if self.scope.ids_span_payload: + raise ValueError( + "Object-label plane projection requires a plane-scoped domain." + ) + if not self.declared_object_id_domains: + raise ValueError( + "Object-label plane projection requires declared per-plane " + "object-ID domains." + ) + if not normalized_indices: + domains: tuple[tuple[int, ...], ...] = () + elif any( + ( + index < 0 or index >= len(self.declared_object_id_domains) + for index in normalized_indices + ) + ): + raise ValueError( + f"Object-label plane projection index is outside declared domain count {len(self.declared_object_id_domains)}: {normalized_indices!r}." + ) + else: + domains = tuple( + (self.declared_object_id_domains[index] for index in normalized_indices) + ) + if len(domains) == 1: + return ObjectLabelDomain.declared( + scope=PayloadLabelScope, + declared_object_count=0 if not domains[0] else None, + declared_object_ids=domains[0], + ) + return ObjectLabelDomain.declared( + scope=PlaneLabelScope, declared_object_id_domains=domains + ) + + +class ObjectLabelDomainMetadata(ABC): + """Nominal provider for object-label ID domain metadata.""" + + @abstractmethod + def object_label_domain(self) -> ObjectLabelDomain: + """Return the declared object-label identity domain.""" + + +class ObjectLabelDomainMetadataStrategy( + NominalTypeKeyedStrategyMixin, ABC, metaclass=AutoRegisterMeta +): + """Registered extractor for nominal object-label domain metadata.""" + + value_type: ClassVar[type[object] | tuple[type[object], ...] | None] = None + value_type_label: ClassVar[str | None] = None + __registry_family__ = RegistryFamily(RegistryKeyAttribute.VALUE_TYPE_LABEL) + + @classmethod + def for_value(cls, value: object) -> "ObjectLabelDomainMetadataStrategy": + return cls.require_nominal_value( + value, + context="Object-label domain metadata", + ) + + @abstractmethod + def object_label_domain(self, value: object) -> ObjectLabelDomain: + """Return the declared object-label identity domain for ``value``.""" + + +class NominalObjectLabelDomainMetadataStrategy(ObjectLabelDomainMetadataStrategy): + """Use the domain declared by a nominal object-label domain provider.""" + + value_type = ObjectLabelDomainMetadata + + def object_label_domain(self, value: object) -> ObjectLabelDomain: + if not isinstance(value, ObjectLabelDomainMetadata): + raise TypeError( + f"NominalObjectLabelDomainMetadataStrategy requires ObjectLabelDomainMetadata, got {type(value).__name__}." + ) + return value.object_label_domain() + + +class ObjectLabelIdDomainStrategy( + NominalTypeKeyedStrategyMixin, ABC, metaclass=AutoRegisterMeta +): + """Registered extractor for materially present positive object-label IDs.""" + + value_type: ClassVar[type[object] | None] = None + value_type_label: ClassVar[str | None] = None + __registry_family__ = RegistryFamily(RegistryKeyAttribute.VALUE_TYPE_LABEL) + + @classmethod + def for_value(cls, labels: Any) -> "ObjectLabelIdDomainStrategy": + strategy = cls.for_nominal_value(labels) + if strategy is None: + raise TypeError( + "Object-label ID extraction requires a registered nominal payload " + f"type, got {type(labels).__name__}." + ) + return strategy + + @abstractmethod + def present_ids(self, labels: Any) -> tuple[int, ...]: + """Return positive object IDs materially present in the label payload.""" + + def max_present_id(self, labels: Any) -> int: + """Return the largest materially present positive object ID.""" + ids = self.present_ids(labels) + return max(ids) if ids else 0 + + @staticmethod + def positive_ids_from_array(labels: Any) -> tuple[int, ...]: + """Return positive IDs from one numeric dense object-label array.""" + import numpy as np + + label_array = np.asarray(labels) + if not label_array.size or not ( + np.issubdtype(label_array.dtype, np.number) + or np.issubdtype(label_array.dtype, np.bool_) + ): + return () + dense_integer_domain = DenseIntegerObjectLabelIdDomain.from_array(label_array) + if dense_integer_domain is not None: + return dense_integer_domain.present_ids() + return tuple( + (int(object_id) for object_id in np.unique(label_array) if object_id > 0) + ) + + +@dataclass(frozen=True, slots=True) +class DenseIntegerObjectLabelIdDomain: + """Exact present-ID extractor for bounded nonnegative dense integer labels.""" + + labels: np.ndarray + max_label: int + + @classmethod + def from_array(cls, labels: np.ndarray) -> "DenseIntegerObjectLabelIdDomain | None": + if not ( + np.issubdtype(labels.dtype, np.integer) + or np.issubdtype(labels.dtype, np.bool_) + ): + return None + if labels.size == 0: + return None + min_label = int(labels.min()) + if min_label < 0: + return None + max_label = int(labels.max()) + if max_label > labels.size: + return None + return cls(labels=labels, max_label=max_label) + + def present_ids(self) -> tuple[int, ...]: + if self.max_label <= 0: + return () + counts = np.bincount( + np.asarray(self.labels, dtype=np.int64).ravel(), + minlength=self.max_label + 1, + ) + present_ids = np.flatnonzero(counts) + return tuple((int(label_id) for label_id in present_ids if label_id > 0)) + + +class DenseArrayObjectLabelIdDomainStrategy(ObjectLabelIdDomainStrategy): + """Extract present object IDs from dense NumPy label arrays.""" + + value_type = np.ndarray + + def present_ids(self, labels: Any) -> tuple[int, ...]: + return self.positive_ids_from_array(labels) + + @dataclass(frozen=True, slots=True) class ConsecutiveObjectLabelIdProjection: """Projection from arbitrary positive object IDs to consecutive IDs.""" @@ -992,14 +1048,12 @@ def declared_domain(self, source: Any, labels: Any) -> ObjectLabelDomain: class PresentObjectLabelIdsDomainDeclaration(ObjectLabelDomainDeclaration): """Declare only object IDs materially present in transformed labels.""" - scope: ObjectLabelDomainScope = ObjectLabelDomainScope.PAYLOAD + scope: type[ObjectLabelDomainScope] = PayloadLabelScope plane_projection: RuntimePlaneAxisValueProjection | None = None def declared_domain(self, source: Any, labels: Any) -> ObjectLabelDomain: del source - return ObjectLabelPlaneDomainStrategy.for_enum_member( - self.scope - ).present_domain( + return self.scope.present_domain( labels, plane_projection=self.plane_projection, ) @@ -1025,9 +1079,7 @@ def dense_object_label_plane_id_domains( payload_domain = ObjectLabelDomainMetadataStrategy.for_value( labels ).object_label_domain(labels) - return ObjectLabelPlaneDomainStrategy.for_enum_member( - payload_domain.scope - ).plane_domains( + return payload_domain.scope.plane_domains( labels, domain=payload_domain, ) @@ -1040,9 +1092,7 @@ def dense_object_label_identity_domains( payload_domain = ObjectLabelDomainMetadataStrategy.for_value( labels ).object_label_domain(labels) - return ObjectLabelPlaneDomainStrategy.for_enum_member( - payload_domain.scope - ).identity_domains( + return payload_domain.scope.identity_domains( labels, domain=payload_domain, ) diff --git a/openhcs/core/runtime_object_labels.py b/openhcs/core/runtime_object_labels.py index a7f1c898d0..1637e5cad0 100644 --- a/openhcs/core/runtime_object_labels.py +++ b/openhcs/core/runtime_object_labels.py @@ -1,6 +1,9 @@ """Nominal runtime object-label values and transformations.""" from __future__ import annotations +from openhcs.core.runtime_object_label_domains import PayloadLabelScope +from openhcs.core.runtime_plane_projection import NoPlaneAxis +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from abc import ABC, abstractmethod from collections.abc import ( @@ -36,14 +39,11 @@ ObjectLabelDomainDeclaration, ObjectLabelDomainMetadata, ObjectLabelDomainMetadataStrategy, - ObjectLabelDomainScope, ObjectLabelIdDomainStrategy, - ObjectLabelPlaneDomainStrategy, PreserveSourceObjectLabelDomainDeclaration, ) from openhcs.core.runtime_plane_projection import ( RuntimePlaneAxis, - RuntimePlaneAxisStrategy, RuntimePlaneAxisValueProjection, RuntimeSliceIdentityProjectableValue, ) @@ -291,7 +291,7 @@ def project_runtime_slice( ) -> "ObjectLabelVariantData": """Project every present variant onto one runtime slice.""" return self.project_plane( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, plane_index=slice_index, plane_count=slice_count, ) @@ -299,7 +299,7 @@ def project_runtime_slice( def project_plane( self, *, - plane_axis: RuntimePlaneAxis, + plane_axis: type[RuntimePlaneAxis], plane_index: int, plane_count: int, ) -> "ObjectLabelVariantData": @@ -327,7 +327,7 @@ def project_planes( def project_label_data_plane( labels: ObjectLabelData, *, - plane_axis: RuntimePlaneAxis, + plane_axis: type[RuntimePlaneAxis], plane_index: int, plane_count: int, ) -> ObjectLabelData: @@ -442,7 +442,7 @@ def in_representation(self, representation: ObjectLabelRepresentation) -> Object return super().in_representation(representation) def project_plane( - self, *, plane_axis: RuntimePlaneAxis, plane_index: int, plane_count: int, + self, *, plane_axis: type[RuntimePlaneAxis], plane_index: int, plane_count: int, ) -> ObjectLabelVariantData: self.validate_plane_count(plane_count, "Object-label data plane projection") if plane_index < 0 or plane_index >= plane_count: @@ -559,7 +559,7 @@ class ObjectLabelValue( variant_data: ObjectLabelVariantData representation: ObjectLabelRepresentation domain: ObjectLabelDomain - plane_axis: RuntimePlaneAxis | None + plane_axis: type[RuntimePlaneAxis] parent_image_source_voxel_spacing: SourceVoxelSpacing def object_label_domain(self) -> ObjectLabelDomain: @@ -570,9 +570,7 @@ def measurement_object_row_identity( declared_identity: MeasurementObjectRowIdentity, ) -> MeasurementObjectRowIdentity: """Project declared row identity through this label value's domain scope.""" - return ObjectLabelPlaneDomainStrategy.for_enum_member( - self.domain.scope - ).measurement_object_row_identity(declared_identity) + return self.domain.scope.measurement_object_row_identity(declared_identity) def validate_object_label_variants(self) -> None: if not isinstance(self.variant_data, ObjectLabelVariantData): @@ -637,9 +635,7 @@ def measurement_reference_image(self) -> runtime_array_values.RuntimeArrayData: """Return an image payload in this value's exact object-label domain.""" labels = object_label_dense_array(self) - domain_strategy = ObjectLabelPlaneDomainStrategy.for_enum_member( - self.object_label_domain().scope - ) + domain_strategy = self.object_label_domain().scope metadata = self.metadata.with_source_provenance( domain_strategy.measurement_reference_source_provenance(self) ) @@ -650,38 +646,47 @@ def measurement_reference_image(self) -> runtime_array_values.RuntimeArrayData: def declared_plane_projection(self) -> RuntimePlaneAxisValueProjection | None: """Return the exact runtime plane axis declared by this label value.""" domain = self.object_label_domain() - return ObjectLabelPlaneDomainStrategy.for_enum_member( - domain.scope - ).value_projection(self) + return domain.scope.value_projection(self) def measurement_planes(self) -> tuple["ObjectLabelValue", ...]: """Return values projected through the declared label-plane domain.""" - return ObjectLabelPlaneDomainStrategy.for_enum_member( - self.object_label_domain().scope - ).declared_measurement_planes(self) + return self.object_label_domain().scope.declared_measurement_planes(self) def measurement_plane_domains(self) -> tuple[tuple[int, ...], ...]: """Return object-ID domains in declared measurement-plane order.""" domain = self.object_label_domain() - return ObjectLabelPlaneDomainStrategy.for_enum_member( - domain.scope - ).plane_domains( + return domain.scope.plane_domains( self, domain=domain, ) + def runtime_slice_projection(self) -> RuntimePlaneAxisValueProjection | None: + """Return the declared plane projection when its planes are runtime slices.""" + projection = self.declared_plane_projection() + if projection is not None and projection.axis.one_plane_per_runtime_slice: + return projection + return None + def runtime_slice_plane_count(self) -> int | None: """Return the declared runtime-slice plane count, when present.""" - projection = self.declared_plane_projection() - if projection is None or projection.axis is not RuntimePlaneAxis.RUNTIME_SLICE: - return None - return projection.axis_size + projection = self.runtime_slice_projection() + return None if projection is None else projection.axis_size + + def for_runtime_slice(self, slice_index: int) -> "ObjectLabelValue": + """Return the labels of one runtime slice; other label values are unchanged.""" + projection = self.runtime_slice_projection() + if projection is None: + return self + return self.value_for_slice(projection.selected_plane(slice_index)) # -- RuntimeSliceProjectableValue ------------------------------------- def runtime_slice_count(self) -> int | None: - if self.plane_axis is not RuntimePlaneAxis.RUNTIME_SLICE: - return None + if self.plane_axis.one_plane_per_runtime_slice: + return self._declared_runtime_slice_count() + return None + + def _declared_runtime_slice_count(self) -> int: plane_count = self.declared_plane_count() if plane_count is None: from openhcs.core.runtime_slice_projection import ( @@ -706,7 +711,7 @@ def value_for_slice( raise RuntimeSliceProjectionDeclarationError( "Object labels have no nominal plane-stack contract for their " - f"declared {context.axis.value!r} axis." + f"declared {context.axis.name!r} axis." ) if plane_count != context.axis_size: raise ValueError( @@ -764,8 +769,8 @@ def requires_output_plane_contextualization( del output return ( plane_projection is not None - and plane_projection.plane_index is None - and self.metadata.plane_axis is None + and plane_projection.preserves_complete_axis + and self.metadata.plane_axis.unstacked and not self.metadata.persists_whole_image() and np.ndim(object_label_dense_array(self)) >= 3 ) @@ -779,7 +784,7 @@ def contextualize_image_output( source_metadata = self.metadata if source_metadata.persists_whole_image(): return source_metadata.derive_payload(self, output, plane_projection=None) - if plane_projection is None or plane_projection.plane_index is not None: + if plane_projection is None or plane_projection.selects_one_plane: return source_metadata.derive_payload( self, output, plane_projection=plane_projection, ) @@ -815,7 +820,7 @@ def alignment_slices(self) -> tuple[Any, ...]: if slice_count is None: return (self,) projection = RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, axis_size=slice_count, + axis=RuntimeSlicePlaneAxis, axis_size=slice_count, ) return tuple( self.value_for_slice(projection.selected_plane(index)) @@ -824,7 +829,7 @@ def alignment_slices(self) -> tuple[Any, ...]: def declared_plane_count(self) -> int | None: """Return this value's validated plane-scoped label cardinality.""" - if self.domain.scope is not ObjectLabelDomainScope.PLANE: + if self.domain.scope.ids_span_payload: return None declared_domains = self.domain.declared_object_id_domains if not declared_domains: @@ -838,9 +843,9 @@ def declared_plane_count(self) -> int | None: def validate_source_alignment(self, label_name: str) -> None: """Validate source-addressable provenance for every declared label plane.""" - if self.domain.scope is ObjectLabelDomainScope.PAYLOAD: + if self.domain.scope.ids_span_payload: return - if self.domain.scope is not ObjectLabelDomainScope.PLANE: + if self.domain.scope.ids_span_payload: raise TypeError( f"Unsupported object-label domain scope {self.domain.scope!r}." ) @@ -894,7 +899,7 @@ def with_variants( source_provenance: SourceImageProvenance | None = None, source_spatial_domain: SourceSpatialDomain | None = None, parent_image_source_voxel_spacing: SourceVoxelSpacing | None = None, - plane_axis: RuntimePlaneAxis | None | object = _PRESERVE_PLANE_AXIS, + plane_axis: type[RuntimePlaneAxis] | object = _PRESERVE_PLANE_AXIS, ) -> Self: """Return this nominal carrier with exact replacement label semantics.""" selected_representation = ( @@ -902,9 +907,7 @@ def with_variants( ) selected_domain = self.domain if domain is None else domain selected_plane_axis = ( - ObjectLabelPlaneDomainStrategy.for_enum_member( - selected_domain.scope - ).value_plane_axis(self.plane_axis) + selected_domain.scope.value_plane_axis(self.plane_axis) if plane_axis is _PRESERVE_PLANE_AXIS else plane_axis ) @@ -968,13 +971,13 @@ def with_projected_plane( ObjectLabelVariantData(labels, variants), domain=object_label_domain_for_projected_label_plane(self, plane_index), source_provenance=self.source_provenance.for_source_plane(plane_index), - plane_axis=None, + plane_axis=NoPlaneAxis, ) def with_measurement_labels(self, labels: ObjectLabelData) -> Self: """Return measurement-time labels in this value's declared domain.""" variants = ObjectLabelVariantData.compatible_replacement(self, labels) - if self.domain.scope is ObjectLabelDomainScope.PLANE: + if self.domain.scope.ids_per_plane: plane_count = self.declared_plane_count() if plane_count is None: raise ValueError( @@ -1016,7 +1019,7 @@ def project_source_plane( variants, domain=object_label_domain_for_projected_label_plane(self, plane_index), source_provenance=self.source_provenance.for_source_plane(plane_index), - plane_axis=None, + plane_axis=NoPlaneAxis, ) def with_runtime_slice_projection( @@ -1028,7 +1031,7 @@ def with_runtime_slice_projection( source_plane_indices: tuple[int, ...] | None, ) -> "ObjectLabelValue": """Return this carrier projected onto one runtime slice.""" - if self.plane_axis is None: + if self.plane_axis.unstacked: return self source_variants = self.variant_data variants = ( @@ -1056,12 +1059,10 @@ def with_runtime_slice_projection( ) projected_plane_count = ( len(projected_domain.declared_object_id_domains) - if projected_domain.scope is ObjectLabelDomainScope.PLANE + if projected_domain.scope.ids_per_plane else 1 ) - projected_axis = RuntimePlaneAxisStrategy.for_enum_member( - self.plane_axis - ).projected_axis(projected_plane_count) + projected_axis = self.plane_axis.projected_axis(projected_plane_count) slice_metadata = self.metadata.for_grouped_source_plane_projection( source_plane_indices=source_plane_indices, runtime_plane_index=slice_index, @@ -1131,17 +1132,13 @@ def normalize_object_label_metadata( self.representation = ObjectLabelRepresentation( self.representation, ) - if self.plane_axis is not None: - self.plane_axis = RuntimePlaneAxis( - self.plane_axis, - ) - if self.domain.scope is ObjectLabelDomainScope.PAYLOAD: - if self.plane_axis is not None: + if self.domain.scope.ids_span_payload: + if self.plane_axis.leads_data: raise ValueError( f"{value_label} payload-scoped labels cannot declare a plane axis." ) - elif self.domain.scope is ObjectLabelDomainScope.PLANE: - if self.plane_axis is None: + elif self.domain.scope.ids_per_plane: + if self.plane_axis.unstacked: raise ValueError( f"{value_label} plane-scoped labels require a declared plane axis." ) @@ -1274,7 +1271,7 @@ def object_label_source_context_provenance( """Merge image provenance into labels without reviving stale stack axes.""" label_provenance = label.source_provenance image_provenance = image.metadata.source_provenance - if label.domain.scope is ObjectLabelDomainScope.PLANE: + if label.domain.scope.ids_per_plane: plane_count = label.declared_plane_count() if plane_count is None: raise ValueError( @@ -1286,7 +1283,7 @@ def object_label_source_context_provenance( image_provenance = runtime_image_values.ImagePayloadMetadata.compose( (image,), mode=( - runtime_image_values.ImagePayloadMetadataCompositionMode.STACK + RuntimeSlicePlaneAxis ), ).source_provenance else: @@ -1317,7 +1314,7 @@ class ObjectLabelPayload(ObjectLabelValue): representation: ObjectLabelRepresentation = ObjectLabelRepresentation.DENSE_LABELS domain: ObjectLabelDomain = field(default_factory=ObjectLabelDomain) - plane_axis: RuntimePlaneAxis | None = None + plane_axis: type[RuntimePlaneAxis] = NoPlaneAxis parent_image_source_voxel_spacing: SourceVoxelSpacing = field( default_factory=SourceVoxelSpacing ) @@ -1356,7 +1353,7 @@ class ObjectLabelSet(ObjectLabelValue, NamedArtifactPayload): source_image_name: str | None = None representation: ObjectLabelRepresentation = ObjectLabelRepresentation.DENSE_LABELS domain: ObjectLabelDomain = field(default_factory=ObjectLabelDomain) - plane_axis: RuntimePlaneAxis | None = None + plane_axis: type[RuntimePlaneAxis] = NoPlaneAxis parent_image_source_voxel_spacing: SourceVoxelSpacing = field( default_factory=SourceVoxelSpacing ) @@ -1405,9 +1402,7 @@ def from_payload( metadata.object_label_source_spatial_domain() ) ) - plane_axis = ObjectLabelPlaneDomainStrategy.for_enum_member( - payload.domain.scope - ).value_plane_axis(payload.plane_axis) + plane_axis = payload.domain.scope.value_plane_axis(payload.plane_axis) variants = variants.in_representation(payload.representation) if isinstance(payload, ObjectLabelSet): payload.require_name() @@ -1456,11 +1451,28 @@ def source_alias_plane_index( axis_size: int, ) -> int | None: """Resolve source aliases only when this label set declares that axis.""" - if self.plane_axis is not RuntimePlaneAxis.SOURCE_BINDING: - return None - return self.source_provenance.source_alias_plane_index( - source_aliases, - axis_size, + if self.plane_axis.one_plane_per_source_binding: + return self.source_provenance.source_alias_plane_index( + source_aliases, + axis_size, + ) + return None + + def source_aliases_select_one_plane(self) -> bool: + """Return whether source-binding labels resolve their own aliases to one plane.""" + aliases = self.source_aliases + if not aliases: + raise ValueError( + "Source-binding object labels require explicit source aliases." + ) + declared_projection = self.declared_plane_projection() + if declared_projection is None: + raise ValueError( + "Source-binding object labels require a declared plane projection." + ) + return ( + self.source_alias_plane_index(aliases, declared_projection.axis_size) + is not None ) def object_label_semantic_identity(self) -> tuple[tuple[str, Hashable], ...]: @@ -2263,11 +2275,11 @@ def object_label_domain_for_projected_label_plane( ) -> ObjectLabelDomain: """Return the payload-domain object IDs carried by one selected plane.""" domain = source.object_label_domain() - if domain.scope is ObjectLabelDomainScope.PAYLOAD: + if domain.scope.ids_span_payload: return domain plane_domain = domain.project_planes((plane_index,)) return ObjectLabelDomain.declared( - scope=ObjectLabelDomainScope.PAYLOAD, + scope=PayloadLabelScope, declared_object_count=plane_domain.declared_object_count, declared_object_ids=plane_domain.declared_object_ids, ) diff --git a/openhcs/core/runtime_plane_projection.py b/openhcs/core/runtime_plane_projection.py index a97f886fe4..8518797b90 100644 --- a/openhcs/core/runtime_plane_projection.py +++ b/openhcs/core/runtime_plane_projection.py @@ -4,9 +4,7 @@ from abc import ABC from abc import abstractmethod from dataclasses import dataclass -from enum import Enum from metaclass_registry import AutoRegisterMeta -from metaclass_registry.strategies import EnumKeyedStrategyMixin from collections.abc import Callable, Sequence from typing import Any, ClassVar from typing import Self @@ -16,6 +14,7 @@ if TYPE_CHECKING: from openhcs.core.aligned_image_payload import AlignedImageStackKwargResolver + from openhcs.core.runtime_image_values import ImagePayloadMetadata from openhcs.core.source_image_provenance import SourceImageProvenance @@ -31,9 +30,41 @@ def runtime_slice_count(self) -> int | None: return None @property - def declared_plane_axis(self) -> "RuntimePlaneAxis | None": - """Return the leading plane axis this value declares, if any.""" - return None + def declared_plane_axis(self) -> "type[RuntimePlaneAxis]": + """Return the leading plane axis this value declares.""" + return NoPlaneAxis + + @property + def image_metadata(self) -> "ImagePayloadMetadata": + """Return the image metadata this value carries; only images carry any.""" + from openhcs.core.runtime_image_values import ImagePayloadMetadata + + return ImagePayloadMetadata() + + @property + def array_data(self) -> object: + """Return the array a writer stores for this value; only images unwrap.""" + return self + + def scalar_source_provenance(self) -> "SourceImageProvenance": + """Return the source provenance of this value read as one runtime slice.""" + return self.image_metadata.source_provenance + + def slice_source_provenances(self) -> "tuple[SourceImageProvenance, ...]": + """Return the source provenance of each runtime slice, without reading pixels.""" + from openhcs.core.axes import ColourAxis + + count = self.runtime_slice_count() + metadata = self.image_metadata + provenance = metadata.source_provenance + if count is None: + return (provenance,) + if metadata.axis_position(ColourAxis) == 0: + raise ValueError( + "Image metadata cannot declare the same leading axis as both " + "plane and channel." + ) + return tuple(provenance.for_source_plane(index) for index in range(count)) @abstractmethod def value_for_slice( @@ -105,9 +136,9 @@ def project_runtime_slice(self, slice_index: int) -> object: """Return the value represented by one runtime-slice index.""" def value_for_slice(self, context: "RuntimePlaneAxisValueProjection") -> object: - if context.axis is not RuntimePlaneAxis.RUNTIME_SLICE: - return self - return self.project_runtime_slice(context.require_plane_index()) + if context.axis.one_plane_per_runtime_slice: + return self.project_runtime_slice(context.require_plane_index()) + return self def aligned_value(self, resolver: "AlignedImageStackKwargResolver") -> object: return self.value_for_slice(resolver.projection_axis) @@ -137,108 +168,269 @@ def value_for_slice(self, context: "RuntimePlaneAxisValueProjection") -> object: def identity_projected_value( self, context: "RuntimePlaneAxisValueProjection" ) -> object: - if context.axis is not RuntimePlaneAxis.RUNTIME_SLICE: - return self - return self.with_runtime_slice_identity( - slice_index=context.require_plane_index(), - slice_count=context.axis_size, - ) + if context.axis.one_plane_per_runtime_slice: + return self.with_runtime_slice_identity( + slice_index=context.require_plane_index(), + slice_count=context.axis_size, + ) + return self + +class RuntimePlaneAxis(ABC, metaclass=AutoRegisterMeta): + """What the leading plane axis of a runtime array stack counts. + + Each kind of axis is a subclass, used as a class; ``name`` is its spelling + in viewer messages, saved metadata and error text. + """ -class RuntimePlaneAxis(str, Enum): - """Semantic meaning of the leading plane axis on runtime array stacks.""" + __registry_key__ = "name" + __skip_if_no_key__ = True + __registry__: ClassVar[dict[str, type[RuntimePlaneAxis]]] = {} + + name: ClassVar[str | None] = None + leads_data: ClassVar[bool] = True + """The data has a leading dimension that counts planes of this axis.""" + unstacked: ClassVar[bool] = False + """The data has no leading plane dimension (the opposite of ``leads_data``).""" + one_plane_per_runtime_slice: ClassVar[bool] = False + """Each plane is one runtime slice of the invocation.""" + one_plane_per_source_binding: ClassVar[bool] = False + """Each plane is one bound source image.""" + keeps_image_whole: ClassVar[bool] = True + """An image with this axis can still be one whole image of its spatial domain.""" + + @classmethod + def require_runtime_slices( + cls, message: str, *, error: type[Exception] = ValueError + ) -> None: + """Raise ``error(message)`` unless each plane is one runtime slice.""" + if cls.one_plane_per_runtime_slice: + return + raise error(message) - RUNTIME_SLICE = "runtime_slice" - SOURCE_BINDING = "source_binding" + @classmethod + def require_source_bindings(cls, message: str) -> None: + """Raise ``ValueError(message)`` unless each plane is one bound source image.""" + if cls.one_plane_per_source_binding: + return + raise ValueError(message) + @classmethod + def leading_axes(cls) -> tuple[type[RuntimePlaneAxis], ...]: + """Return every axis that puts a leading plane dimension on the data.""" + return tuple(axis for axis in cls.__registry__.values() if axis.leads_data) -class RuntimePlaneAxisStrategy( - EnumKeyedStrategyMixin[RuntimePlaneAxis], ABC, metaclass=AutoRegisterMeta -): - """Own all behavior for one nominal runtime plane axis.""" + @classmethod + def require_member(cls, value: object) -> type[RuntimePlaneAxis]: + """Return ``value`` when it is one of this family's axes.""" + if isinstance(value, type) and issubclass(value, RuntimePlaneAxis): + return value + raise ValueError(f"{value!r} is not a runtime plane axis.") - __enum_member_attr__ = "axis" - axis: ClassVar[RuntimePlaneAxis] - strategy_label: ClassVar[str | None] = None + @classmethod + def named(cls, name: object) -> type[RuntimePlaneAxis]: + """Return the axis a viewer message or saved record spells as ``name``.""" + try: + return cls.__registry__[name] + except (KeyError, TypeError): + raise ValueError(f"Unknown runtime plane axis {name!r}.") from None + @classmethod @abstractmethod def plane_index( - self, + cls, projector: "RuntimePlaneAxisProjector", source_aliases: tuple[str, ...], ) -> int | None: """Resolve this axis against an execution-local projector.""" + @classmethod @abstractmethod def axis_size( - self, + cls, projector: "RuntimePlaneAxisProjector", source_aliases: tuple[str, ...], ) -> int | None: """Resolve this axis cardinality against an execution-local projector.""" + @classmethod @abstractmethod - def projected_axis(self, projected_plane_count: int) -> RuntimePlaneAxis | None: + def projected_axis(cls, projected_plane_count: int) -> type[RuntimePlaneAxis]: """Return the axis retained after an exact runtime projection.""" + @classmethod + def keeps_planes_of(cls, plane_axis: type[RuntimePlaneAxis]) -> bool: + """Whether composing along this axis keeps a member's existing plane axis.""" + return plane_axis is cls + + @classmethod + def axis_when_stacked(cls) -> type[RuntimePlaneAxis]: + """Return the axis that members declaring this axis are stacked along.""" + return cls + + @classmethod + @abstractmethod + def compose_provenance( + cls, + provenances: "Sequence[SourceImageProvenance]", + *, + scalar_sources: "Sequence[SourceImageProvenance]", + **topology: bool, + ) -> "SourceImageProvenance": + """Return the source provenance of members composed along this axis. + + ``topology`` forwards ``preserve_single_topology``; each axis has its + own default for a single member. + """ + -class RuntimeSlicePlaneAxisStrategy(RuntimePlaneAxisStrategy): - """Runtime-slice axis behavior.""" +class RuntimeSlicePlaneAxis(RuntimePlaneAxis): + """One plane per runtime slice; composing along it stacks the slices.""" - axis = RuntimePlaneAxis.RUNTIME_SLICE + name = "runtime_slice" + one_plane_per_runtime_slice = True + @classmethod def plane_index( - self, + cls, projector: "RuntimePlaneAxisProjector", source_aliases: tuple[str, ...], ) -> int | None: del source_aliases return projector.runtime_slice_plane_index() + @classmethod def axis_size( - self, + cls, projector: "RuntimePlaneAxisProjector", source_aliases: tuple[str, ...], ) -> int | None: del source_aliases return projector.runtime_slice_axis_size() - def projected_axis(self, projected_plane_count: int) -> RuntimePlaneAxis | None: + @classmethod + def projected_axis(cls, projected_plane_count: int) -> type[RuntimePlaneAxis]: if projected_plane_count != 1: raise ValueError( "Runtime-slice object-label projection must select exactly one " f"plane, got {projected_plane_count}." ) - return None + return NoPlaneAxis + + @classmethod + def keeps_planes_of(cls, plane_axis: type[RuntimePlaneAxis]) -> bool: + return plane_axis is cls or plane_axis.unstacked + + @classmethod + def compose_provenance( + cls, + provenances: "Sequence[SourceImageProvenance]", + *, + scalar_sources: "Sequence[SourceImageProvenance]", + **topology: bool, + ) -> "SourceImageProvenance": + from openhcs.core.source_image_provenance import SourceImageProvenance + + return SourceImageProvenance.stack( + provenances, scalar_sources=scalar_sources, **topology + ) -class SourceBindingPlaneAxisStrategy(RuntimePlaneAxisStrategy): - """Source-binding axis behavior.""" +class SourceBindingPlaneAxis(RuntimePlaneAxis): + """One plane per bound source image; composing along it bundles the sources.""" - axis = RuntimePlaneAxis.SOURCE_BINDING + name = "source_binding" + one_plane_per_source_binding = True + keeps_image_whole = False + @classmethod def plane_index( - self, + cls, projector: "RuntimePlaneAxisProjector", source_aliases: tuple[str, ...], ) -> int | None: return projector.source_binding_axis_plane_index(source_aliases) + @classmethod def axis_size( - self, + cls, projector: "RuntimePlaneAxisProjector", source_aliases: tuple[str, ...], ) -> int | None: return projector.source_binding_axis_size(source_aliases) - def projected_axis(self, projected_plane_count: int) -> RuntimePlaneAxis | None: + @classmethod + def projected_axis(cls, projected_plane_count: int) -> type[RuntimePlaneAxis]: if projected_plane_count <= 0: raise ValueError( "Source-binding object-label projection must select at least one plane." ) if projected_plane_count == 1: - return None - return RuntimePlaneAxis.SOURCE_BINDING + return NoPlaneAxis + return cls + + @classmethod + def compose_provenance( + cls, + provenances: "Sequence[SourceImageProvenance]", + *, + scalar_sources: "Sequence[SourceImageProvenance]", + **topology: bool, + ) -> "SourceImageProvenance": + from openhcs.core.source_image_provenance import SourceImageProvenance + + return SourceImageProvenance.bundle( + provenances, scalar_sources=scalar_sources, **topology + ) + + +class NoPlaneAxis(RuntimePlaneAxis): + """The data has no leading plane dimension: it is one plane or one whole image.""" + + name = "none" + leads_data = False + unstacked = True + + @classmethod + def plane_index( + cls, + projector: "RuntimePlaneAxisProjector", + source_aliases: tuple[str, ...], + ) -> int | None: + del projector, source_aliases + return None + + @classmethod + def axis_size( + cls, + projector: "RuntimePlaneAxisProjector", + source_aliases: tuple[str, ...], + ) -> int | None: + del projector, source_aliases + return None + + @classmethod + def projected_axis(cls, projected_plane_count: int) -> type[RuntimePlaneAxis]: + raise ValueError("Data without a plane axis has no planes to project.") + + @classmethod + def keeps_planes_of(cls, plane_axis: type[RuntimePlaneAxis]) -> bool: + del plane_axis + return False + + @classmethod + def axis_when_stacked(cls) -> type[RuntimePlaneAxis]: + return RuntimeSlicePlaneAxis + + @classmethod + def compose_provenance( + cls, + provenances: "Sequence[SourceImageProvenance]", + *, + scalar_sources: "Sequence[SourceImageProvenance]", + **topology: bool, + ) -> "SourceImageProvenance": + raise ValueError("Members are composed along a declared plane axis.") class RuntimePlaneAxisProjector(ABC): @@ -266,49 +458,41 @@ def source_binding_axis_size(self, source_aliases: tuple[str, ...]) -> int | Non @dataclass(frozen=True, slots=True) -class RuntimePlaneProjection(RuntimePlaneAxisProjector): - """Preserve a runtime stack or select one explicitly proven plane.""" +class RuntimePlaneProjection(RuntimePlaneAxisProjector, ABC): + """Which planes of a runtime stack an execution scope works on. + + ``WholeStackProjection`` keeps the stack; ``StackPlaneSelection`` selects + one explicitly proven plane. + """ - plane_index: int | None = None plane_count: int | None = None + selects_one_plane: ClassVar[bool] + def __post_init__(self) -> None: if self.plane_count is None: - plane_count = None - else: - plane_count = int(self.plane_count) - if plane_count <= 0: - raise ValueError( - "Runtime-plane projection plane_count must be positive." - ) - object.__setattr__(self, "plane_count", plane_count) - if self.plane_index is None: return - plane_index = int(self.plane_index) - if plane_index < 0: - raise ValueError("Runtime-plane projection plane_index cannot be negative.") - if plane_count is not None and plane_index >= plane_count: - raise ValueError( - "Runtime-plane projection plane_index must be within plane_count: " - f"index {plane_index}, count {plane_count}." - ) - object.__setattr__(self, "plane_index", plane_index) + plane_count = int(self.plane_count) + if plane_count <= 0: + raise ValueError("Runtime-plane projection plane_count must be positive.") + object.__setattr__(self, "plane_count", plane_count) - @classmethod - def stack(cls, plane_count: int | None = None) -> "RuntimePlaneProjection": + @staticmethod + def stack(plane_count: int | None = None) -> "WholeStackProjection": """Preserve runtime-slice stacks for stack-scoped execution.""" - return cls(plane_count=plane_count) + return WholeStackProjection(plane_count=plane_count) - @classmethod + @staticmethod def selected( - cls, plane_index: int, plane_count: int | None = None - ) -> "RuntimePlaneProjection": + plane_index: int, plane_count: int | None = None + ) -> "StackPlaneSelection": """Select one runtime-slice plane from an explicit projection proof.""" - return cls(plane_index=plane_index, plane_count=plane_count) + return StackPlaneSelection(plane_count=plane_count, plane_index=plane_index) - def runtime_slice_plane_index(self) -> int | None: - """Return selected runtime-slice plane, or None when stacks are preserved.""" - return self.plane_index + @property + def preserves_complete_axis(self) -> bool: + """Whether this projection keeps every plane instead of selecting one.""" + return not self.selects_one_plane def runtime_slice_axis_size(self) -> int | None: """Return the grouped runtime-slice axis size when known.""" @@ -316,18 +500,60 @@ def runtime_slice_axis_size(self) -> int | None: @dataclass(frozen=True, slots=True) -class RuntimePlaneAxisValueProjection(RuntimeSliceIndexedValue): - """Projection of values that explicitly carry a declared runtime plane axis.""" +class WholeStackProjection(RuntimePlaneProjection): + """Keep every plane of the runtime stack.""" + + selects_one_plane = False + + def runtime_slice_plane_index(self) -> None: + return None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class StackPlaneSelection(RuntimePlaneProjection): + """Select one plane of the runtime stack.""" + + plane_index: int + + selects_one_plane = True + + def __post_init__(self) -> None: + RuntimePlaneProjection.__post_init__(self) + plane_index = int(self.plane_index) + if plane_index < 0: + raise ValueError("Runtime-plane projection plane_index cannot be negative.") + if self.plane_count is not None and plane_index >= self.plane_count: + raise ValueError( + "Runtime-plane projection plane_index must be within plane_count: " + f"index {plane_index}, count {self.plane_count}." + ) + object.__setattr__(self, "plane_index", plane_index) + + def runtime_slice_plane_index(self) -> int: + return self.plane_index + - axis: RuntimePlaneAxis +@dataclass(frozen=True, slots=True) +class RuntimePlaneAxisValueProjection(RuntimeSliceIndexedValue, ABC): + """Projection of values that explicitly carry a declared runtime plane axis. + + ``WholeAxisProjection`` keeps every plane of the axis; + ``SelectedPlaneProjection`` selects one. + """ + + axis: type[RuntimePlaneAxis] source_aliases: tuple[str, ...] - plane_index: int | None axis_size: int + preserves_complete_axis: ClassVar[bool] + """Whether this projection keeps every plane instead of selecting one.""" + def __post_init__(self) -> None: - axis = RuntimePlaneAxis( - self.axis, - ) + axis = RuntimePlaneAxis.require_member(self.axis) + if axis.unstacked: + raise ValueError( + "RuntimePlaneAxisValueProjection requires an axis that leads the data." + ) source_aliases = tuple(self.source_aliases) if any(not isinstance(alias, str) or not alias for alias in source_aliases): raise ValueError( @@ -339,23 +565,45 @@ def __post_init__(self) -> None: raise ValueError( "RuntimePlaneAxisValueProjection.axis_size must be positive." ) - plane_index = self.plane_index - if plane_index is not None: - plane_index = int(plane_index) - if plane_index < 0 or plane_index >= axis_size: - raise ValueError( - "RuntimePlaneAxisValueProjection.plane_index must be within " - f"axis_size: index {plane_index}, size {axis_size}." - ) object.__setattr__(self, "axis", axis) object.__setattr__(self, "source_aliases", source_aliases) object.__setattr__(self, "axis_size", axis_size) - object.__setattr__(self, "plane_index", plane_index) + + @property + def selects_one_plane(self) -> bool: + """Whether this projection selects one plane of its axis.""" + return not self.preserves_complete_axis + + @abstractmethod + def selected_planes(self) -> tuple[int, ...]: + """Return the plane indices of the axis this projection keeps.""" + + def selects_same_planes(self, other: "RuntimePlaneAxisValueProjection") -> bool: + """Whether ``other`` keeps or selects the same planes the same way.""" + return ( + self.preserves_complete_axis == other.preserves_complete_axis + and self.selected_planes() == other.selected_planes() + ) + + @staticmethod + def for_plane( + *, + axis: type[RuntimePlaneAxis], + plane_index: int | None, + axis_size: int, + source_aliases: tuple[str, ...] = (), + ) -> "RuntimePlaneAxisValueProjection": + """Build from a resolver's answer: an index selects, ``None`` keeps the axis.""" + if plane_index is None: + return WholeAxisProjection(axis, tuple(source_aliases), axis_size) + return SelectedPlaneProjection( + axis, tuple(source_aliases), axis_size, plane_index + ) @classmethod def from_source_declaration( cls, - axis: RuntimePlaneAxis | None, + axis: type[RuntimePlaneAxis], source: "SourceImageProvenance", ) -> "RuntimePlaneAxisValueProjection | None": """Construct the retained axis declared by an exact source-image value. @@ -364,7 +612,7 @@ def from_source_declaration( multiple contributors. Cardinality and aliases come from that source source declaration; array rank never supplies spatial meaning. """ - if axis is None: + if axis.unstacked: return None return cls.preserve( axis=axis, @@ -376,7 +624,7 @@ def from_source_declaration( def from_projector( cls, projector: RuntimePlaneAxisProjector | None, - axis: RuntimePlaneAxis, + axis: type[RuntimePlaneAxis], source_aliases: tuple[str, ...], ) -> "RuntimePlaneAxisValueProjection | None": """Return the runtime-axis projection declared by a runtime projector.""" @@ -386,24 +634,22 @@ def from_projector( raise TypeError( f"Runtime plane-axis projection requires RuntimePlaneAxisProjector, got {type(projector).__name__}." ) - axis = RuntimePlaneAxis(axis) source_aliases = tuple(source_aliases) - strategy = RuntimePlaneAxisStrategy.for_enum_member(axis) - axis_size = strategy.axis_size(projector, source_aliases) + axis_size = axis.axis_size(projector, source_aliases) if axis_size is None: return None - return cls( + return cls.for_plane( axis=axis, source_aliases=source_aliases, - plane_index=strategy.plane_index(projector, source_aliases), + plane_index=axis.plane_index(projector, source_aliases), axis_size=axis_size, ) def validate_source_declaration( - self, axis: RuntimePlaneAxis | None, source: "SourceImageProvenance", + self, axis: type[RuntimePlaneAxis], source: "SourceImageProvenance", ) -> None: """Check an explicit projection against retained acquisition declarations.""" - if axis is None: + if axis.unstacked: return declared_count = source.source_plane_count if self.axis is not axis or ( @@ -418,7 +664,7 @@ def validate_source_declaration( def require_from_projector( cls, projector: RuntimePlaneAxisProjector, - axis: RuntimePlaneAxis, + axis: type[RuntimePlaneAxis], source_aliases: tuple[str, ...] = (), ) -> "RuntimePlaneAxisValueProjection": """Return the complete projection declared for one runtime image axis.""" @@ -426,7 +672,7 @@ def require_from_projector( projection = cls.from_projector(projector, axis, source_aliases) if projection is None: raise ValueError( - f"Declared {axis.value!r} image axis has no runtime cardinality." + f"Declared {axis.name!r} image axis has no runtime cardinality." ) return projection @@ -434,40 +680,32 @@ def require_from_projector( def from_selected_plane( cls, *, - axis: RuntimePlaneAxis, + axis: type[RuntimePlaneAxis], plane_index: int, axis_size: int, source_aliases: tuple[str, ...] = (), ) -> "RuntimePlaneAxisValueProjection": """Return a projection whose explicit plane proof is already resolved.""" - return cls( - axis=axis, - source_aliases=tuple(source_aliases), - plane_index=plane_index, - axis_size=axis_size, + return SelectedPlaneProjection( + axis, tuple(source_aliases), axis_size, plane_index ) @classmethod def preserve( cls, *, - axis: RuntimePlaneAxis, + axis: type[RuntimePlaneAxis], axis_size: int, source_aliases: tuple[str, ...] = (), ) -> "RuntimePlaneAxisValueProjection": """Declare a complete runtime axis without selecting one plane.""" - return cls( - axis=axis, - source_aliases=tuple(source_aliases), - plane_index=None, - axis_size=axis_size, - ) + return WholeAxisProjection(axis, tuple(source_aliases), axis_size) def selected_plane(self, plane_index: int) -> "RuntimePlaneAxisValueProjection": """Select one plane while preserving this declaration's exact axis.""" - return type(self).from_selected_plane( + return self.from_selected_plane( axis=self.axis, source_aliases=self.source_aliases, plane_index=plane_index, @@ -480,11 +718,6 @@ def project_runtime_slice( """Select the projection carried into one runtime-slice invocation.""" return self.selected_plane(slice_index) - @property - def preserves_complete_axis(self) -> bool: - """Whether this projection keeps every plane instead of selecting one.""" - return self.plane_index is None - @staticmethod def require_axis_size( projection: "RuntimePlaneAxisValueProjection | None", @@ -500,6 +733,16 @@ def require_axis_size( ) return projection + @staticmethod + def require_runtime_slice_projection( + projection: "RuntimePlaneAxisValueProjection | None", message: str + ) -> "RuntimePlaneAxisValueProjection": + """Return ``projection`` when it is declared on the runtime-slice axis.""" + if projection is None: + raise ValueError(message) + projection.axis.require_runtime_slices(message) + return projection + @staticmethod def require_runtime_slice_axis( projection: "RuntimePlaneAxisValueProjection | None", @@ -508,10 +751,9 @@ def require_runtime_slice_axis( value_name: str, ) -> "RuntimePlaneAxisValueProjection": """Require the runtime-slice axis, declared with ``slice_count`` planes.""" - if projection is None or projection.axis is not RuntimePlaneAxis.RUNTIME_SLICE: - raise ValueError( - f"{value_name} requires an exact runtime slice projection." - ) + projection = RuntimePlaneAxisValueProjection.require_runtime_slice_projection( + projection, f"{value_name} requires an exact runtime slice projection." + ) if projection.axis_size != slice_count: raise ValueError( f"{value_name} count must exactly match the declared runtime plane " @@ -519,22 +761,14 @@ def require_runtime_slice_axis( ) return projection + @abstractmethod def require_plane_index(self) -> int: """Return the selected plane index required by a projected invocation.""" - if self.plane_index is None: - raise ValueError( - "Runtime plane projection requires an explicitly selected plane." - ) - return self.plane_index - + @abstractmethod def require_complete_axis(self, *, value_name: str) -> Self: """Require that this declaration preserves every input plane.""" - if self.plane_index is not None: - raise ValueError(f"{value_name} requires a complete input stack projection.") - return self - @classmethod def require_complete_projection( cls, @@ -560,7 +794,7 @@ def validate_shape(self, shape: Sequence[int], *, value_name: str) -> None: shape = tuple(int(size) for size in shape) if not self.dense_shape_carries_axis(shape): raise ValueError( - f"{value_name} does not match its declared {self.axis.value!r} " + f"{value_name} does not match its declared {self.axis.name!r} " f"axis of size {self.axis_size}: shape {shape!r}." ) @@ -571,3 +805,60 @@ def validate_plane_index(plane_index: int, shape: tuple[int, ...]) -> None: raise RuntimeError( f"Runtime plane-axis projection produced an out-of-range plane index {plane_index} for shape {shape!r}." ) + + +@dataclass(frozen=True, slots=True) +class WholeAxisProjection(RuntimePlaneAxisValueProjection): + """Keep every plane of the declared axis.""" + + preserves_complete_axis = True + + @classmethod + def preserve( + cls, + *, + axis: type[RuntimePlaneAxis], + axis_size: int, + source_aliases: tuple[str, ...] = (), + ) -> Self: + return cls(axis, tuple(source_aliases), axis_size) + + def selected_planes(self) -> tuple[int, ...]: + return tuple(range(self.axis_size)) + + def require_plane_index(self) -> int: + raise ValueError( + "Runtime plane projection requires an explicitly selected plane." + ) + + def require_complete_axis(self, *, value_name: str) -> Self: + del value_name + return self + + +@dataclass(frozen=True, slots=True) +class SelectedPlaneProjection(RuntimePlaneAxisValueProjection): + """Select one plane of the declared axis.""" + + plane_index: int + + preserves_complete_axis = False + + def __post_init__(self) -> None: + RuntimePlaneAxisValueProjection.__post_init__(self) + plane_index = int(self.plane_index) + if plane_index < 0 or plane_index >= self.axis_size: + raise ValueError( + "RuntimePlaneAxisValueProjection.plane_index must be within " + f"axis_size: index {plane_index}, size {self.axis_size}." + ) + object.__setattr__(self, "plane_index", plane_index) + + def selected_planes(self) -> tuple[int, ...]: + return (self.plane_index,) + + def require_plane_index(self) -> int: + return self.plane_index + + def require_complete_axis(self, *, value_name: str) -> Self: + raise ValueError(f"{value_name} requires a complete input stack projection.") diff --git a/openhcs/core/runtime_relationships.py b/openhcs/core/runtime_relationships.py index 076fe4f82d..79d3e5fb09 100644 --- a/openhcs/core/runtime_relationships.py +++ b/openhcs/core/runtime_relationships.py @@ -749,10 +749,9 @@ def relationship_slice_count( for labels in (context.parent_labels, context.child_labels) ) ) - if ( - ObjectLabelDomainScope.common((domain.scope for domain in domains)) - is not ObjectLabelDomainScope.PLANE - ): + if ObjectLabelDomainScope.common( + domain.scope for domain in domains + ).ids_span_payload: return None plane_counts = tuple( labels.declared_plane_count() diff --git a/openhcs/core/runtime_slice_alignment.py b/openhcs/core/runtime_slice_alignment.py index c291ec1b2c..1bb92c6e6e 100644 --- a/openhcs/core/runtime_slice_alignment.py +++ b/openhcs/core/runtime_slice_alignment.py @@ -1,6 +1,7 @@ """Non-image values aligned to runtime slices.""" from __future__ import annotations +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from abc import abstractmethod from collections.abc import Callable @@ -10,7 +11,6 @@ import numpy as np from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisValueProjection, RuntimeSliceProjectableValue, ) @@ -102,11 +102,11 @@ def runtime_slice_count(self) -> int | None: return self.slice_count def value_for_slice(self, context: RuntimePlaneAxisValueProjection) -> Any: - if context.axis is not RuntimePlaneAxis.RUNTIME_SLICE: - return self - return self.value_for_aligned_slice( - context.require_plane_index(), context.axis_size, - ) + if context.axis.one_plane_per_runtime_slice: + return self.value_for_aligned_slice( + context.require_plane_index(), context.axis_size, + ) + return self def aligned_value(self, resolver: "AlignedImageStackKwargResolver") -> Any: return self.value_for_aligned_slice( @@ -117,6 +117,9 @@ def aligned_value(self, resolver: "AlignedImageStackKwargResolver") -> Any: def alignment_slices(self) -> tuple[SliceValueT, ...]: return self.values + def slice_source_provenances(self) -> tuple[Any, ...]: + return tuple(value.scalar_source_provenance() for value in self.values) + def project_declared_source( self, source_image_name: str, ) -> "RuntimeSliceAlignedValues[Any]": @@ -161,9 +164,6 @@ def contextualize_image_output( value_name="Runtime-slice-aligned image output", ) from openhcs.core.aligned_image_payload import stack_image_payloads - from openhcs.core.runtime_image_values import ( - ImagePayloadMetadataCompositionMode, - ) return stack_image_payloads( self.map_indexed_slices( @@ -174,7 +174,7 @@ def contextualize_image_output( None, ) ).values, - metadata_mode=ImagePayloadMetadataCompositionMode.STACK, + metadata_mode=RuntimeSlicePlaneAxis, ) def object_label_output( diff --git a/openhcs/core/runtime_slice_projection.py b/openhcs/core/runtime_slice_projection.py index 1f36c1f886..121230c668 100644 --- a/openhcs/core/runtime_slice_projection.py +++ b/openhcs/core/runtime_slice_projection.py @@ -1,6 +1,8 @@ """Nominal runtime-slice projection contracts.""" from __future__ import annotations +from openhcs.core.runtime_plane_projection import NoPlaneAxis +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from abc import ABC, abstractmethod from collections.abc import Callable, Iterable, Mapping, Sequence @@ -21,7 +23,7 @@ RuntimeSliceProjectableValue, ) from openhcs.core.source_image_provenance import SourceComponentMetadata -from openhcs.core.runtime_image_values import array_data_of, image_metadata_of +from openhcs.core.runtime_image_values import as_runtime_value if TYPE_CHECKING: from openhcs.core.aligned_image_payload import AlignedImageStackKwargResolver @@ -66,7 +68,7 @@ def runtime_slice_count(self) -> int | None: return None raise RuntimeSliceProjectionDeclarationError( "Variable-component runtime projection requires a nominal payload " - "with RuntimePlaneAxis.RUNTIME_SLICE; source provenance and ndarray " + "with RuntimeSlicePlaneAxis; source provenance and ndarray " "shape cannot declare the runtime axis." ) @@ -81,9 +83,9 @@ def source_plane_indices( context: RuntimePlaneAxisValueProjection, ) -> tuple[int, ...] | None: """Return exact provenance projection for the request-declared stack axis.""" - source_plane_count = image_metadata_of( + source_plane_count = as_runtime_value( self.value - ).source_provenance.source_plane_count + ).image_metadata.source_provenance.source_plane_count if source_plane_count == 0: return None if source_plane_count != context.axis_size: @@ -192,7 +194,7 @@ def project_payload_items( source_description=request.source_description, ) projection = request.plane_projection or RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=slice_count, ) items: list[RuntimeProjectedPayloadItem] = [] @@ -250,12 +252,12 @@ class RuntimeProjectedPayloadItem: @property def data(self) -> RuntimeProjectionData: - return array_data_of(self.value) + return as_runtime_value(self.value).array_data @property def metadata(self) -> ImagePayloadMetadata: """Image metadata of the item; items of non-image kinds carry none.""" - return image_metadata_of(self.value) + return as_runtime_value(self.value).image_metadata @property def source_component_metadata(self) -> SourceComponentMetadata | None: @@ -357,11 +359,11 @@ def aligned_value( return value @classmethod - def declared_plane_axis(cls, value: RuntimeProjectionData) -> RuntimePlaneAxis | None: + def declared_plane_axis(cls, value: RuntimeProjectionData) -> type[RuntimePlaneAxis]: """Return the leading plane axis a value declares; foreign values declare none.""" if isinstance(value, RuntimeSliceProjectableValue): return value.declared_plane_axis - return None + return NoPlaneAxis @classmethod def alignment_slices(cls, value: RuntimeProjectionData) -> tuple[Any, ...]: @@ -421,7 +423,7 @@ def preserved_context_for_value( if slice_count is None: return None return RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=slice_count, ) @@ -462,7 +464,7 @@ def context_for_value( if effective_slice_count is None: return None return RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, plane_index=slice_index, axis_size=effective_slice_count, ) diff --git a/openhcs/core/runtime_sparse_labels.py b/openhcs/core/runtime_sparse_labels.py index 5e81890169..a0b50f8931 100644 --- a/openhcs/core/runtime_sparse_labels.py +++ b/openhcs/core/runtime_sparse_labels.py @@ -12,7 +12,6 @@ from openhcs.core.runtime_tabular_values import FieldSpec from openhcs.core.runtime_tabular_values import ColumnarRows from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisValueProjection, ) from openhcs.core.source_spatial_domain import SourceSpatialDomain, SpatialShapeYX @@ -93,9 +92,9 @@ def runtime_slice_count(self) -> int | None: def value_for_slice( self, context: RuntimePlaneAxisValueProjection ) -> "SparseIJVLabelRows": - if context.axis is not RuntimePlaneAxis.RUNTIME_SLICE: - return self - return self.slice(context.require_plane_index()) + if context.axis.one_plane_per_runtime_slice: + return self.slice(context.require_plane_index()) + return self @property def has_slice_index(self) -> bool: diff --git a/openhcs/core/runtime_stores.py b/openhcs/core/runtime_stores.py index 8a78d2445a..b7df494b02 100644 --- a/openhcs/core/runtime_stores.py +++ b/openhcs/core/runtime_stores.py @@ -1,6 +1,7 @@ """Runtime stores for typed artifact values.""" from __future__ import annotations +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from collections import OrderedDict from collections.abc import Mapping @@ -33,7 +34,6 @@ RuntimeValue, ) from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisValueProjection, ) from openhcs.core.runtime_slice_projection import RuntimeSliceProjection @@ -613,7 +613,7 @@ def _axis_value(self, value: RuntimeValue) -> RuntimeValue: data=RuntimeSliceProjection.value_for_slice( payload, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, plane_index=matching_indices[0], axis_size=slice_count, ), diff --git a/openhcs/core/runtime_tabular_values.py b/openhcs/core/runtime_tabular_values.py index c4c971fbc1..c4936ecb11 100644 --- a/openhcs/core/runtime_tabular_values.py +++ b/openhcs/core/runtime_tabular_values.py @@ -19,7 +19,6 @@ import openhcs.core.numpy_jsonable # noqa: F401 (numpy values in to_jsonable) from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisValueProjection, RuntimeSliceProjectableValue, ) @@ -32,15 +31,15 @@ class ColumnarRows(RuntimeSliceProjectableValue): def value_for_slice(self, context: RuntimePlaneAxisValueProjection) -> Any: """Select the rows of one runtime slice through the rows' declared axis.""" - if context.axis is not RuntimePlaneAxis.RUNTIME_SLICE: - return self - from openhcs.core.measurement_row_materialization import ( - MeasurementRowsAxisProjection, - ) + if context.axis.one_plane_per_runtime_slice: + from openhcs.core.measurement_row_materialization import ( + MeasurementRowsAxisProjection, + ) - return MeasurementRowsAxisProjection.from_rows(self).project_runtime_slice_index( - context.require_plane_index() - ) + return MeasurementRowsAxisProjection.from_rows( + self + ).project_runtime_slice_index(context.require_plane_index()) + return self @staticmethod def column_array(values: Sequence[object]) -> np.ndarray: diff --git a/openhcs/core/source_binding_selection.py b/openhcs/core/source_binding_selection.py index dc35becb70..bd1859ad3c 100644 --- a/openhcs/core/source_binding_selection.py +++ b/openhcs/core/source_binding_selection.py @@ -1,6 +1,7 @@ """Shared source-binding candidate selection for planning and runtime.""" from __future__ import annotations +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from abc import ABC, abstractmethod from dataclasses import dataclass, field, replace @@ -60,14 +61,12 @@ VirtualWorkspaceSourceProjection, ) from openhcs.core.aligned_image_payload import stack_image_payloads -from openhcs.core.runtime_image_values import ImagePayloadMetadataCompositionMode -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.steps.function_io import get_all_image_paths from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.runtime_image_loading import ImagePayloadSourceMetadataContext from openhcs.core.compiled_step_plan import CompiledStepPlan from openhcs.core.runtime_image_values import ImagePayload -from openhcs.core.runtime_image_values import image_metadata_of +from openhcs.core.runtime_image_values import as_runtime_value if TYPE_CHECKING: from openhcs.core.runtime_adapters import RuntimeAdapterRequest @@ -1628,7 +1627,7 @@ def source_artifact_payload( source_provenance = ( None if source_payload is None - else image_metadata_of(source_payload).source_provenance + else as_runtime_value(source_payload).image_metadata.source_provenance ) if source_provenance is not None and not source_provenance.has_values: raise ValueError( @@ -1689,9 +1688,7 @@ def source_artifact_payload( and projected_payloads[0].metadata.persists_whole_image() else stack_image_payloads( projected_payloads, - metadata_mode=ImagePayloadMetadataCompositionMode.for_plane_axis( - RuntimePlaneAxis.RUNTIME_SLICE - ), + metadata_mode=RuntimeSlicePlaneAxis, ) ) return cls.admit_source_artifact_cohort( @@ -1714,16 +1711,14 @@ def admit_source_artifact_cohort( whole-image source retains its intrinsic spatial dimensions. """ metadata = payload.metadata - if not metadata.persists_whole_image() and metadata.plane_axis is None: + if not metadata.persists_whole_image() and metadata.plane_axis.unstacked: if member_count != 1: raise ValueError( "A scalar source payload must declare exactly one cohort member." ) payload = stack_image_payloads( (payload,), - metadata_mode=ImagePayloadMetadataCompositionMode.for_plane_axis( - RuntimePlaneAxis.RUNTIME_SLICE - ), + metadata_mode=RuntimeSlicePlaneAxis, ) metadata = payload.metadata domain = source_binding_plan.source_spatial_domain.with_source_cohort( diff --git a/openhcs/core/source_workspace_projection.py b/openhcs/core/source_workspace_projection.py index dede1af47d..6b2481bfcc 100644 --- a/openhcs/core/source_workspace_projection.py +++ b/openhcs/core/source_workspace_projection.py @@ -1,6 +1,8 @@ """Project OpenHCS virtual-workspace metadata for runtime source binding.""" from __future__ import annotations +from openhcs.core.runtime_plane_projection import RuntimePlaneAxis +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis from collections.abc import Mapping, Sequence from dataclasses import dataclass, field @@ -13,7 +15,6 @@ from openhcs.constants import Backend from openhcs.core.runtime_image_values import ( ImagePayloadMetadata, - ImagePayloadMetadataCompositionMode, ) from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.source_bindings import ( @@ -240,7 +241,7 @@ def require_source_projection_for( def payload_composition_mode( self, lookups: tuple[VirtualWorkspacePathLookup, ...], - ) -> ImagePayloadMetadataCompositionMode: + ) -> type[RuntimePlaneAxis]: """Return the leading-axis topology declared by workspace projections.""" projections = tuple( self.require_source_projection_for(lookup) for lookup in lookups @@ -255,8 +256,8 @@ def payload_composition_mode( and len(source_aliases) > 1 and len(set(source_aliases)) == len(source_aliases) ): - return ImagePayloadMetadataCompositionMode.BUNDLE - return ImagePayloadMetadataCompositionMode.STACK + return SourceBindingPlaneAxis + return RuntimeSlicePlaneAxis def project_payload( self, diff --git a/openhcs/core/steps/function_artifact_materialization.py b/openhcs/core/steps/function_artifact_materialization.py index 0893ba8b90..30ddf74961 100644 --- a/openhcs/core/steps/function_artifact_materialization.py +++ b/openhcs/core/steps/function_artifact_materialization.py @@ -63,7 +63,7 @@ prepare_materialization, ) from openhcs.core.axes import Axis, AxisFamily, PartitionAxis -from openhcs.core.runtime_image_values import image_metadata_of +from openhcs.core.runtime_image_values import as_runtime_value if TYPE_CHECKING: from polystore.filemanager import FileManager @@ -532,7 +532,7 @@ def stream_source_metadata_items( def payload_source_identity( data: MaterializationValue, ) -> SourceImageIdentity | None: - metadata = image_metadata_of(data) + metadata = as_runtime_value(data).image_metadata source_identity = metadata.source_provenance.scalar_source_identity if source_identity.addressable: return source_identity @@ -703,7 +703,7 @@ def record_payload_metadata( return ImagePayloadMetadata( source_provenance=record.data.source_provenance, ) - return image_metadata_of(record.data) + return as_runtime_value(record.data).image_metadata @classmethod def record_metadata_with_runtime_scope( diff --git a/openhcs/core/steps/function_output_manifest.py b/openhcs/core/steps/function_output_manifest.py index 9295802dd4..d7f6cb850f 100644 --- a/openhcs/core/steps/function_output_manifest.py +++ b/openhcs/core/steps/function_output_manifest.py @@ -1,6 +1,7 @@ """Execution-local main-flow output lineage for FunctionStep execution.""" from __future__ import annotations +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from dataclasses import dataclass, field, replace from pathlib import Path @@ -60,7 +61,7 @@ class ProducedOutputSemantics(FunctionOutputIdentity): output_path: str relative_output_path: str image_metadata: ImagePayloadMetadata | None = None - main_flow_plane_axis: RuntimePlaneAxis | None = RuntimePlaneAxis.RUNTIME_SLICE + main_flow_plane_axis: type[RuntimePlaneAxis] = RuntimeSlicePlaneAxis def published(self) -> "ProducedOutputSemantics": """Fix the saved slot while retaining live provenance and storage aliases.""" @@ -212,7 +213,7 @@ def from_output( output_identity: FunctionOutputIdentity, output_context: AlignedImageSliceContext | None = None, image_metadata: ImagePayloadMetadata | None = None, - main_flow_plane_axis: RuntimePlaneAxis | None = RuntimePlaneAxis.RUNTIME_SLICE, + main_flow_plane_axis: type[RuntimePlaneAxis] = RuntimeSlicePlaneAxis, ) -> "ProducedOutputSemantics": output_path_text = str(output_path) if output_context is None: diff --git a/openhcs/core/steps/function_runtime.py b/openhcs/core/steps/function_runtime.py index be3de32214..daab77bb25 100644 --- a/openhcs/core/steps/function_runtime.py +++ b/openhcs/core/steps/function_runtime.py @@ -4,6 +4,8 @@ execution. FunctionStep remains responsible for step-level orchestration. """ +from openhcs.core.runtime_plane_projection import NoPlaneAxis +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis import logging import time from dataclasses import dataclass, replace @@ -123,7 +125,6 @@ ) from openhcs.core.runtime_tabular_values import ColumnarRows from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisProjector, RuntimePlaneAxisValueProjection, RuntimePlaneProjection, @@ -141,7 +142,7 @@ from openhcs.core.compiled_step_plan import CompiledStepPlan from openhcs.core.axes import Axis, AxisFamily, PartitionAxis from openhcs.core.runtime_image_values import owned_runtime_value -from openhcs.core.runtime_image_values import image_metadata_of +from openhcs.core.runtime_image_values import as_runtime_value from openhcs.core.debug import ( AfterInvocationDebugBoundary, BeforeInvocationDebugBoundary, @@ -235,7 +236,7 @@ def active_main_flow_source_binding_plan( plan = self.unscoped_main_flow_source_binding_plan represented_names = frozenset( - image_metadata_of(payload).source_provenance.represented_source_image_names + as_runtime_value(payload).image_metadata.source_provenance.represented_source_image_names ) if represented_names: variable_components = ComponentSet.of( @@ -1249,7 +1250,7 @@ def _save_outputs( single_output_plane_axis=( processed_stack.plane_axis_for_output_context(output_contexts[0]) if len(output_payloads) == 1 - else None + else NoPlaneAxis ), ) context.runtime_image_stack_cache.store( @@ -1703,7 +1704,7 @@ def loaded_source_artifact_payload( metadata = payload.metadata if ( not metadata.persists_whole_image() - and metadata.plane_axis not in (None, RuntimePlaneAxis.RUNTIME_SLICE) + and metadata.plane_axis.one_plane_per_source_binding ): # A remaining binding axis does not prove this source's ordered # physical members. Resolve it through its declared origin owner. @@ -1715,9 +1716,9 @@ def loaded_source_artifact_payload( member_count=member_count if member_count is not None else 1, ) projection = RuntimePlaneAxisValueProjection.from_projector( - request.plane_projection, RuntimePlaneAxis.RUNTIME_SLICE, (), + request.plane_projection, RuntimeSlicePlaneAxis, (), ) - if projection is not None and projection.plane_index is not None: + if projection is not None and projection.selects_one_plane: payload = RuntimeSliceProjection.value_for_slice(payload, projection) return payload diff --git a/openhcs/core/steps/stream_component_semantics.py b/openhcs/core/steps/stream_component_semantics.py index 6aadcc3fc3..f86234c060 100644 --- a/openhcs/core/steps/stream_component_semantics.py +++ b/openhcs/core/steps/stream_component_semantics.py @@ -185,10 +185,10 @@ def _item_fields( colour_axis_position = metadata.axis_position(ColourAxis) if colour_axis_position is not None: item_fields[ViewerWireField.SOURCE_CHANNEL_AXIS.value] = colour_axis_position - if metadata.plane_axis is None: + if metadata.plane_axis.unstacked: return item_fields - item_fields[ViewerWireField.PLANE_AXIS.value] = metadata.plane_axis.value + item_fields[ViewerWireField.PLANE_AXIS.value] = metadata.plane_axis.name plane_component_values = metadata.retained_plane_component_values() if ( not plane_component_values @@ -442,7 +442,7 @@ def from_image_metadata( provenance.for_source_plane(index).scalar_source_identity for index in range(provenance.source_plane_count) ) - if metadata.plane_axis is not None and provenance.source_plane_count + if metadata.plane_axis.leads_data and provenance.source_plane_count else (provenance.scalar_source_identity,) ) return cls.from_source_identities( diff --git a/openhcs/core/viewer_streaming_service.py b/openhcs/core/viewer_streaming_service.py index 0b82362121..641cb446cc 100644 --- a/openhcs/core/viewer_streaming_service.py +++ b/openhcs/core/viewer_streaming_service.py @@ -492,7 +492,7 @@ def require_projected_image_window( shape = tuple(image.data.shape) if ( len(shape) != 3 - or metadata.plane_axis is None + or metadata.plane_axis.unstacked or shape[0] != metadata.source_plane_metadata_count or metadata.source_spatial_domain.origin_yx != (0, 0) or tuple(shape[-2:]) != metadata.source_spatial_domain.source_shape_yx @@ -516,7 +516,7 @@ def image_source_metadata_items( ) ) metadata = None if projection is None else projection.image_metadata - if metadata is not None and metadata.plane_axis is not None: + if metadata is not None and metadata.plane_axis.leads_data: values.extend( StreamSourceComponentMetadataItems.from_image_metadata( metadata, diff --git a/openhcs/interop/cellprofiler/module_artifact_declarations.py b/openhcs/interop/cellprofiler/module_artifact_declarations.py index 9fcd6a617d..2a82729330 100644 --- a/openhcs/interop/cellprofiler/module_artifact_declarations.py +++ b/openhcs/interop/cellprofiler/module_artifact_declarations.py @@ -430,11 +430,11 @@ def measurement_record_rows( ) row_batches.append(super().measurement_record_rows(request)) rows = ConcatenatedColumnarRows(tuple(row_batches)) - plane_index = request.adapter.request.plane_projection.plane_index - if plane_index is not None: + plane_projection = request.adapter.request.plane_projection + if plane_projection.selects_one_plane: rows = MeasurementRowsAxisProjection.from_rows( rows - ).project_runtime_slice_index(plane_index) + ).project_runtime_slice_index(plane_projection.plane_index) return rows @classmethod diff --git a/openhcs/interop/cellprofiler/runtime/adapter.py b/openhcs/interop/cellprofiler/runtime/adapter.py index 982ef80e5c..15e8a7a6b4 100644 --- a/openhcs/interop/cellprofiler/runtime/adapter.py +++ b/openhcs/interop/cellprofiler/runtime/adapter.py @@ -1,6 +1,7 @@ """Thin CellProfiler-style view over OpenHCS runtime artifacts.""" from __future__ import annotations +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis import time from collections.abc import Sequence @@ -42,7 +43,6 @@ ObjectRelationship, ) from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisProjector, RuntimePlaneAxisValueProjection, ) @@ -292,13 +292,13 @@ def get_image( data = edge.composed_value(records) projected: RuntimeArrayData = data source_metadata = projected.metadata - if source_metadata.plane_axis is RuntimePlaneAxis.SOURCE_BINDING: + if source_metadata.plane_axis.one_plane_per_source_binding: source_projection = preserved_image_plane_projection( projected, self, source_metadata.source_image_names, ) - if source_projection.plane_index is not None: + if source_projection.selects_one_plane: projected = cast( RuntimeArrayData, RuntimeSliceProjection.value_for_slice( @@ -311,10 +311,10 @@ def get_image( ) runtime_projection = RuntimePlaneAxisValueProjection.from_projector( self, - RuntimePlaneAxis.RUNTIME_SLICE, + RuntimeSlicePlaneAxis, (), ) - if runtime_projection is None or runtime_projection.plane_index is None: + if runtime_projection is None or runtime_projection.preserves_complete_axis: return cast( RuntimeArrayData, RuntimeSliceProjection.full_stack_value(projected), ) @@ -385,12 +385,12 @@ def get_objects( ObjectLabelsArtifactType, ).composed_value(records), ) - if objects.plane_axis is RuntimePlaneAxis.SOURCE_BINDING: + if objects.plane_axis.one_plane_per_source_binding: declared_projection = objects.declared_plane_projection() if declared_projection is None: raise ValueError( "Current-source object-label selection requires a declared " - "RuntimePlaneAxis.SOURCE_BINDING payload." + "SourceBindingPlaneAxis payload." ) source_plane_index = objects.source_alias_plane_index( objects.source_image_names, diff --git a/openhcs/interop/cellprofiler/runtime/artifact_binding.py b/openhcs/interop/cellprofiler/runtime/artifact_binding.py index 571b2de270..685e7e3456 100644 --- a/openhcs/interop/cellprofiler/runtime/artifact_binding.py +++ b/openhcs/interop/cellprofiler/runtime/artifact_binding.py @@ -1,6 +1,7 @@ """Runtime artifact binding authorities for CellProfiler modules.""" from __future__ import annotations +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from collections.abc import Callable, Sequence from dataclasses import dataclass, replace @@ -43,8 +44,6 @@ MeasurementRowAxisField, ) from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, - RuntimePlaneAxisStrategy, RuntimePlaneAxisValueProjection, ) from openhcs.core.variable_component_stack_requirement import ( @@ -393,16 +392,14 @@ def current_plane_label_payload( payload: ObjectLabelValue, ) -> ObjectLabelValue: """Project loaded object labels to the invocation's current plane.""" - if payload.plane_axis is None: + if payload.plane_axis.unstacked: return payload plane_count = payload.declared_plane_count() if plane_count is None: raise ValueError( "Slice-aligned object labels require a declared plane count." ) - plane_index = RuntimePlaneAxisStrategy.for_enum_member( - payload.plane_axis - ).plane_index( + plane_index = payload.plane_axis.plane_index( self.adapter, payload.source_aliases, ) @@ -483,27 +480,11 @@ def relationship_runtime_slice_index(self) -> int | None: def object_inputs_projected_by_source_binding_axis(self) -> bool: """Return whether object inputs are already scoped by source binding.""" - for spec in self.object_inputs: - labels = self.label_payload_for(spec) - if labels.plane_axis is not RuntimePlaneAxis.SOURCE_BINDING: - continue - aliases = labels.source_aliases - if not aliases: - raise ValueError( - "Source-binding object labels require explicit source aliases." - ) - declared_projection = labels.declared_plane_projection() - if declared_projection is None: - raise ValueError( - "Source-binding object labels require a declared plane projection." - ) - source_plane_index = labels.source_alias_plane_index( - aliases, - declared_projection.axis_size, - ) - if source_plane_index is not None: - return True - return False + return any( + labels.plane_axis.one_plane_per_source_binding + and labels.source_aliases_select_one_plane() + for labels in map(self.label_payload_for, self.object_inputs) + ) def runtime_value( self, @@ -536,12 +517,12 @@ def runtime_value( return value runtime_projection = RuntimePlaneAxisValueProjection.from_projector( self.adapter, - RuntimePlaneAxis.RUNTIME_SLICE, + RuntimeSlicePlaneAxis, (), ) if ( runtime_projection is not None - and runtime_projection.plane_index is not None + and runtime_projection.selects_one_plane and runtime_projection.axis_size == slice_count ): return cast( @@ -584,7 +565,7 @@ def runtime_value( RuntimeSliceProjection.value_for_slice( value, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, plane_index=selected_indices[0], axis_size=slice_count, ), diff --git a/openhcs/interop/cellprofiler/runtime/function_contract_execution.py b/openhcs/interop/cellprofiler/runtime/function_contract_execution.py index f1a405b973..7bdc805e43 100644 --- a/openhcs/interop/cellprofiler/runtime/function_contract_execution.py +++ b/openhcs/interop/cellprofiler/runtime/function_contract_execution.py @@ -162,7 +162,7 @@ def plane_split(self, image, kwargs): raise RuntimeSliceProjectionDeclarationError( f"{self._owner} with a per-plane contract has an image payload " "plane axis that conflicts with the compiled projection: " - f"{declared_plane_axis!r} != {projection.axis.value!r}." + f"{declared_plane_axis!r} != {projection.axis.name!r}." ) prepare_started_at = time.perf_counter() slices = tuple( diff --git a/openhcs/interop/cellprofiler/runtime/invocation.py b/openhcs/interop/cellprofiler/runtime/invocation.py index 8f63960d0f..70364d5d1c 100644 --- a/openhcs/interop/cellprofiler/runtime/invocation.py +++ b/openhcs/interop/cellprofiler/runtime/invocation.py @@ -1,6 +1,8 @@ """Typed CellProfiler invocation records and execution-mode utilities.""" from __future__ import annotations +from openhcs.core.runtime_plane_projection import RuntimePlaneAxis +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis from collections.abc import Mapping from dataclasses import dataclass, field, replace @@ -24,7 +26,6 @@ ) from openhcs.core.runtime_image_values import ( ImagePayloadMetadata, - ImagePayloadMetadataCompositionMode, ) from openhcs.core.runtime_object_labels import ( ObjectLabelMeasurementSource, @@ -35,15 +36,13 @@ ) from openhcs.core.runtime_slice_alignment import ( RuntimeSliceAlignedValues, - RuntimeSliceAlignedValueSet, ) from openhcs.core.runtime_slice_projection import RuntimeSliceProjection from openhcs.core.source_image_provenance import SourceImageProvenance from openhcs.core.source_spatial_domain import CommonRuntimeValue from openhcs.core.steps.function_runtime import RuntimeCallableArgument from openhcs.core.runtime_array_values import array_geometry -from openhcs.core.axes import ColourAxis -from openhcs.core.runtime_image_values import image_metadata_of +from openhcs.core.runtime_image_values import as_runtime_value from openhcs.core.runtime_image_values import owned_runtime_value @@ -142,7 +141,7 @@ def composed_source_metadata( cls, sources: tuple["CellProfilerSourceIdentityMixin", ...], *, - mode: ImagePayloadMetadataCompositionMode | None = None, + mode: type[RuntimePlaneAxis] | None = None, ) -> ImagePayloadMetadata | None: """Return source metadata composed in runtime source order.""" if not sources: @@ -165,13 +164,9 @@ def composed_source_metadata( for provenance in scalar_sources ) if not any(provenance.has_values for provenance in provenances): - return ImagePayloadMetadata(plane_axis=mode.plane_axis) - provenance = ( - SourceImageProvenance.stack(provenances, scalar_sources=scalar_sources) - if mode is ImagePayloadMetadataCompositionMode.STACK - else SourceImageProvenance.bundle( - provenances, scalar_sources=scalar_sources - ) + return ImagePayloadMetadata(plane_axis=mode) + provenance = mode.compose_provenance( + provenances, scalar_sources=scalar_sources ) return cls.metadata_for_source_provenance(provenance, mode=mode) @@ -179,12 +174,12 @@ def composed_source_metadata( def metadata_for_source_provenance( provenance: SourceImageProvenance, *, - mode: ImagePayloadMetadataCompositionMode, + mode: type[RuntimePlaneAxis], ) -> ImagePayloadMetadata: """Carry a composed source axis without projecting its image pixels.""" return ImagePayloadMetadata( source_provenance=provenance, - plane_axis=mode.plane_axis, + plane_axis=mode, ) @classmethod @@ -216,7 +211,7 @@ def aligned_source_metadata( ) return cls.metadata_for_source_provenance( provenance, - mode=ImagePayloadMetadataCompositionMode.STACK, + mode=RuntimeSlicePlaneAxis, ) @classmethod @@ -224,64 +219,16 @@ def source_provenances( cls, payload: RuntimeCallableArgument ) -> tuple[SourceImageProvenance, ...]: """Read the declared runtime source roster without projecting image pixels.""" - if isinstance(payload, AlignedImageStack): - return tuple( - cls.scalar_source_provenance(value) for value in payload.slices - ) - if isinstance(payload, RuntimeSliceAlignedValueSet): - return tuple(cls.scalar_source_provenance(value) for value in payload.values) - count = RuntimeSliceProjection.slice_count_from_values((payload,)) - metadata = image_metadata_of(payload) - provenance = metadata.source_provenance - if count is None: - return (provenance,) - if metadata.axis_position(ColourAxis) == 0: - raise ValueError( - "Image metadata cannot declare the same leading axis as both " - "plane and channel." - ) - return tuple(provenance.for_source_plane(index) for index in range(count)) - - @staticmethod - def scalar_source_provenance( - payload: RuntimeCallableArgument, - ) -> SourceImageProvenance: - """Collapse an inner image bundle's source topology at its actual read epoch.""" - if not isinstance(payload, AlignedImageStack): - return payload.metadata.source_provenance - metadata = tuple(value.metadata for value in payload.slices) - provenances = tuple( - ( - fields.source_provenance.for_source_plane(0) - if fields.plane_axis is None - and fields.source_provenance.source_plane_count == 1 - else fields.source_provenance - ) - for fields in metadata - ) - preserve_topology = ( - len(metadata) == 1 - and metadata[0].plane_axis - is ImagePayloadMetadataCompositionMode.BUNDLE.plane_axis - ) - scalar_sources = tuple(fields.source_provenance for fields in metadata) - provenance = SourceImageProvenance.bundle( - provenances, - scalar_sources=scalar_sources, - preserve_single_topology=preserve_topology, - ) - return ( - provenance.with_runtime_planes_as_contributors().with_common_scalar_identity_from_planes() - ) + return as_runtime_value(payload).slice_source_provenances() @classmethod def source_metadata_composition_mode( cls, sources: tuple["CellProfilerSourceIdentityMixin", ...], - ) -> ImagePayloadMetadataCompositionMode: + ) -> type[RuntimePlaneAxis]: """Return the source-axis topology owned by this source class.""" del sources - return ImagePayloadMetadataCompositionMode.STACK + return RuntimeSlicePlaneAxis @dataclass(frozen=True, slots=True, kw_only=True) @@ -471,11 +418,11 @@ def __post_init__(self) -> None: def source_metadata_composition_mode( cls, sources: tuple["CellProfilerMeasurementImage", ...], - ) -> ImagePayloadMetadataCompositionMode: + ) -> type[RuntimePlaneAxis]: """Return source metadata topology declared by measurement reference domains.""" if all(source.reference_domain.bundles_source_metadata for source in sources): - return ImagePayloadMetadataCompositionMode.BUNDLE - return ImagePayloadMetadataCompositionMode.STACK + return SourceBindingPlaneAxis + return RuntimeSlicePlaneAxis @property def alignment_image(self) -> RuntimeCallableArgument: @@ -515,7 +462,7 @@ def with_alignment_image( if ( aligned_projection is None or aligned_projection.axis is not plane_projection.axis - or aligned_projection.plane_index != plane_projection.plane_index + or not aligned_projection.selects_same_planes(plane_projection) or aligned_projection.axis_size != plane_projection.axis_size ): raise ValueError( @@ -525,13 +472,13 @@ def with_alignment_image( ) else: image_plane_axis = image.metadata.plane_axis - if image_plane_axis is None: + if image_plane_axis.unstacked: plane_projection = None elif image_plane_axis is not plane_projection.axis: raise ValueError( "Aligned measurement image plane axis conflicts with its " - f"execution projection: {image_plane_axis.value!r} != " - f"{plane_projection.axis.value!r}." + f"execution projection: {image_plane_axis.name!r} != " + f"{plane_projection.axis.name!r}." ) else: plane_projection.validate_shape( @@ -551,6 +498,11 @@ def prepare_object_labels( plane_projector: RuntimePlaneAxisProjector | None = None, ) -> PreparedMeasurementObjectLabels: """Prepare object labels according to this measurement image contract.""" + if not isinstance(label_payload, ObjectLabelValue): + raise TypeError( + "CellProfiler object measurement requires ObjectLabelValue labels, " + f"got {type(label_payload).__name__}." + ) return PreparedMeasurementObjectLabels.from_request( self.object_label_alignment_request( label_payload, diff --git a/openhcs/interop/cellprofiler/runtime/main_flow.py b/openhcs/interop/cellprofiler/runtime/main_flow.py index 413243d9be..38644d2f0b 100644 --- a/openhcs/interop/cellprofiler/runtime/main_flow.py +++ b/openhcs/interop/cellprofiler/runtime/main_flow.py @@ -16,7 +16,7 @@ def cellprofiler_main_flow_output( """Preserve the declared OpenHCS plane axis on a CP output image.""" output_image = owned_runtime_value(output_image) output_axis = output_image.metadata.plane_axis - if output_axis is None: + if output_axis.unstacked: return output_image if plane_projection is None: raise ValueError( @@ -26,20 +26,20 @@ def cellprofiler_main_flow_output( if output_axis is not plane_projection.axis: raise ValueError( "CellProfiler main-flow output axis conflicts with the invocation " - f"projection: {output_axis.value!r} != " - f"{plane_projection.axis.value!r}." + f"projection: {output_axis.name!r} != " + f"{plane_projection.axis.name!r}." ) plane_projection.validate_shape( array_geometry(output_image).shape, value_name="CellProfiler main-flow output", ) input_axis = input_image.metadata.plane_axis - if input_axis is not None: + if input_axis.leads_data: if input_axis is not plane_projection.axis: raise ValueError( "CellProfiler main-flow input axis conflicts with the output " - f"projection: {input_axis.value!r} != " - f"{plane_projection.axis.value!r}." + f"projection: {input_axis.name!r} != " + f"{plane_projection.axis.name!r}." ) plane_projection.validate_shape( array_geometry(input_image).shape, diff --git a/openhcs/interop/cellprofiler/runtime/measurement_execution_support.py b/openhcs/interop/cellprofiler/runtime/measurement_execution_support.py index facb6b6e72..272d1f7d42 100644 --- a/openhcs/interop/cellprofiler/runtime/measurement_execution_support.py +++ b/openhcs/interop/cellprofiler/runtime/measurement_execution_support.py @@ -88,11 +88,6 @@ def object_measurement_runtime_inputs( profile_enabled = CellProfilerRuntimeProfileLogger.enabled() profile_events: list[CellProfilerRuntimeProfileEvent] = [] label_payload_timer = RuntimeProfileTimer.start() - if not isinstance(label_payload, ObjectLabelValue): - raise TypeError( - "CellProfiler object measurement requires ObjectLabelValue labels, " - f"got {type(label_payload).__name__}." - ) prepared_labels = measurement_image.prepare_object_labels( label_payload, plane_projector=adapter, diff --git a/openhcs/interop/cellprofiler/runtime/measurement_recording.py b/openhcs/interop/cellprofiler/runtime/measurement_recording.py index 364adfedc2..df0b9a24f6 100644 --- a/openhcs/interop/cellprofiler/runtime/measurement_recording.py +++ b/openhcs/interop/cellprofiler/runtime/measurement_recording.py @@ -44,7 +44,7 @@ from openhcs.core.runtime_measurements import MeasurementTable from openhcs.core.runtime_tabular_values import ColumnarRows from openhcs.core.source_image_provenance import SourceImageProvenance -from openhcs.core.runtime_image_values import image_metadata_of +from openhcs.core.runtime_image_values import as_runtime_value if TYPE_CHECKING: from openhcs.core.artifacts.specs import ArtifactSpec @@ -266,7 +266,7 @@ def measurement_record_source_metadata( del cls, rows return request.source.composed_source_metadata( (request.source,) - ) or image_metadata_of(request.source.payload) + ) or as_runtime_value(request.source.payload).image_metadata @staticmethod def rows_only_declare_object_name(rows: ColumnarRows) -> bool: @@ -656,7 +656,7 @@ def measurement_record_source_metadata( cls, request: CellProfilerOutputRecordRequest, rows: ColumnarRows ) -> ImagePayloadMetadata: del cls, rows - return image_metadata_of(request.source.payload) + return as_runtime_value(request.source.payload).image_metadata @classmethod def clear_source_when_rows_declare_object_name(cls) -> bool: diff --git a/openhcs/interop/cellprofiler/runtime/measurement_rows.py b/openhcs/interop/cellprofiler/runtime/measurement_rows.py index 93dee0a153..27b8874228 100644 --- a/openhcs/interop/cellprofiler/runtime/measurement_rows.py +++ b/openhcs/interop/cellprofiler/runtime/measurement_rows.py @@ -84,10 +84,10 @@ def measurement_source_image_name_for_slice( ) if plane_projection is None: - if source_metadata.plane_axis is not None: + if source_metadata.plane_axis.leads_data: raise ValueError( "Measurement source metadata declares plane axis " - f"{source_metadata.plane_axis.value!r} without a runtime projection." + f"{source_metadata.plane_axis.name!r} without a runtime projection." ) if slice_index != 0: raise ValueError( @@ -102,7 +102,7 @@ def measurement_source_image_name_for_slice( "RuntimePlaneAxisValueProjection or None, got " f"{type(plane_projection).__name__}." ) - if plane_projection.plane_index is not None: + if plane_projection.selects_one_plane: raise ValueError( "Measurement row source projection requires a preserved runtime " "plane axis, not a preselected plane." @@ -412,7 +412,7 @@ class CellProfilerObjectLocationMeasurementRows(ABC): label_payload: ObjectLabelValue object_name: str - domain_scope: ObjectLabelDomainScope | None = None + domain_scope: type[ObjectLabelDomainScope] | None = None @abstractmethod def coordinate_is_measured( @@ -430,8 +430,8 @@ def __post_init__(self) -> None: if self.domain_scope is not None and self.domain_scope is not declared_scope: raise ValueError( "ObjectLocationMeasurementRows domain_scope must match the " - f"object-label declaration; got {self.domain_scope.value!r}, " - f"expected {declared_scope.value!r}." + f"object-label declaration; got {self.domain_scope.name!r}, " + f"expected {declared_scope.name!r}." ) def rows(self) -> MeasurementProjectedColumnarRows: @@ -475,7 +475,7 @@ def rows(self) -> MeasurementProjectedColumnarRows: def label_plane_domains(self) -> tuple[tuple[object, tuple[int, ...]], ...]: domain = self.label_payload.object_label_domain() - if domain.scope is ObjectLabelDomainScope.PAYLOAD: + if domain.scope.ids_span_payload: object_ids = domain.explicit_id_domain() if object_ids is None: raise ValueError( diff --git a/openhcs/interop/cellprofiler/runtime/module_execution.py b/openhcs/interop/cellprofiler/runtime/module_execution.py index c0df403d92..baac7d5b50 100644 --- a/openhcs/interop/cellprofiler/runtime/module_execution.py +++ b/openhcs/interop/cellprofiler/runtime/module_execution.py @@ -1,6 +1,7 @@ """Execution bridge from generated CellProfiler modules to OpenHCS runtime state.""" from __future__ import annotations +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis import logging import time @@ -51,7 +52,6 @@ MeasurementRowAxisField, ) from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisValueProjection, ) from openhcs.core.runtime_slice_projection import RuntimeSliceProjection @@ -705,7 +705,7 @@ def _resolved_measurement_image( ) plane_projection = ( preserved_image_plane_projection(payload, adapter, source_aliases) - if payload.metadata.plane_axis is not None + if payload.metadata.plane_axis.leads_data else None ) return CellProfilerMeasurementImage( @@ -798,7 +798,7 @@ def _run_per_object_measurement( ) runtime_projection = RuntimePlaneAxisValueProjection.from_projector( adapter, - RuntimePlaneAxis.RUNTIME_SLICE, + RuntimeSlicePlaneAxis, (), ) projected_binding = ( @@ -811,7 +811,7 @@ def _run_per_object_measurement( ), ) if runtime_projection is not None - and runtime_projection.plane_index is not None + and runtime_projection.selects_one_plane else None ) source_names = [] @@ -1268,13 +1268,13 @@ def _image_request( ) runtime_projection = RuntimePlaneAxisValueProjection.from_projector( adapter, - RuntimePlaneAxis.RUNTIME_SLICE, + RuntimeSlicePlaneAxis, (), ) current_runtime_payload = current_image if ( runtime_projection is not None - and runtime_projection.plane_index is not None + and runtime_projection.selects_one_plane ): current_runtime_payload = cast( RuntimeCallableArgument, @@ -1290,7 +1290,7 @@ def _image_request( ) if ( runtime_projection is not None - and runtime_projection.plane_index is not None + and runtime_projection.selects_one_plane and current_image_payload.metadata.plane_axis is runtime_projection.axis ): @@ -1480,12 +1480,12 @@ def _invocation_request( phase_started_at = time.perf_counter() runtime_projection = RuntimePlaneAxisValueProjection.from_projector( adapter, - RuntimePlaneAxis.RUNTIME_SLICE, + RuntimeSlicePlaneAxis, (), ) if ( runtime_projection is not None - and runtime_projection.plane_index is not None + and runtime_projection.selects_one_plane ): runtime_kwargs = cast( dict[str, RuntimeCallableArgument], diff --git a/openhcs/interop/cellprofiler/runtime/object_measurement_row_completion.py b/openhcs/interop/cellprofiler/runtime/object_measurement_row_completion.py index 344474e863..89289c8b4f 100644 --- a/openhcs/interop/cellprofiler/runtime/object_measurement_row_completion.py +++ b/openhcs/interop/cellprofiler/runtime/object_measurement_row_completion.py @@ -31,13 +31,6 @@ from openhcs.core.runtime_measurements import ( MeasurementRowAxisField, ) -from openhcs.core.runtime_object_label_domains import ( - ObjectLabelDomainScope, - ObjectLabelPlaneDomainStrategy, -) -from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, -) from openhcs.core.runtime_object_labels import ( ObjectLabelValue, ) @@ -211,7 +204,7 @@ def positive_label_extent(label_payload: RuntimeCallableArgument) -> int: f"got {type(label_payload).__name__}." ) domain = label_payload.object_label_domain() - if domain.scope is not ObjectLabelDomainScope.PAYLOAD: + if domain.scope.ids_per_plane: raise ValueError( "Missing object-measurement values require a projected payload " "object-ID domain." @@ -381,9 +374,7 @@ def axis_keys_for_label_payload( "Empty object-measurement rows can only be completed when the sole " f"declared axis is {slice_axis!r}; got {self.axis_fields!r}." ) - axis_values = ObjectLabelPlaneDomainStrategy.for_enum_member( - label_payload.object_label_domain().scope - ).declared_measurement_axis_values(label_payload) + axis_values = label_payload.object_label_domain().scope.declared_measurement_axis_values(label_payload) return tuple((axis_value,) for axis_value in axis_values) def object_ids_for_axis( @@ -457,13 +448,12 @@ def label_domain_axis_key( ) -> tuple[RuntimeCallableArgument, ...]: """Return the axis subset that changes the label-id domain.""" label_domain = label_payload.object_label_domain() - if label_domain.scope is ObjectLabelDomainScope.PAYLOAD: + if label_domain.scope.ids_span_payload: return () slice_axis_name = MeasurementRowAxisField.SLICE_INDEX.value - if label_payload.plane_axis is not RuntimePlaneAxis.RUNTIME_SLICE: - raise ValueError( - "Plane-scoped measurement domains require the runtime-slice axis." - ) + label_payload.plane_axis.require_runtime_slices( + "Plane-scoped measurement domains require the runtime-slice axis." + ) if slice_axis_name not in self.axis_fields: raise ValueError("Plane-scoped measurement rows must declare slice_index.") slice_axis_position = self.axis_fields.index(slice_axis_name) diff --git a/openhcs/interop/cellprofiler/runtime/object_measurement_vectors.py b/openhcs/interop/cellprofiler/runtime/object_measurement_vectors.py index bf08ee5d5e..91d1736dfa 100644 --- a/openhcs/interop/cellprofiler/runtime/object_measurement_vectors.py +++ b/openhcs/interop/cellprofiler/runtime/object_measurement_vectors.py @@ -82,7 +82,7 @@ def slice_aligned_value(self) -> CellProfilerSliceAlignedValues: def runtime_value(self) -> RuntimeCallableArgument: if ( self.plane_projection is None - or self.plane_projection.plane_index is not None + or self.plane_projection.selects_one_plane ): if len(self.slices) != 1: raise ValueError( diff --git a/openhcs/interop/cellprofiler/runtime/output_record_request.py b/openhcs/interop/cellprofiler/runtime/output_record_request.py index 24ceec7f9b..bcb4c00747 100644 --- a/openhcs/interop/cellprofiler/runtime/output_record_request.py +++ b/openhcs/interop/cellprofiler/runtime/output_record_request.py @@ -1,6 +1,7 @@ """Output-record request authority for CellProfiler runtime artifacts.""" from __future__ import annotations +from openhcs.core.runtime_object_label_domains import PayloadLabelScope from collections.abc import Mapping from dataclasses import dataclass, field @@ -279,13 +280,13 @@ def materialization_source_metadata(self) -> ImagePayloadMetadata | None: self.adapter.request.require_artifact_input_edge(source_ref) )).metadata - def object_label_output_domain_scope(self) -> ObjectLabelDomainScope | None: + def object_label_output_domain_scope(self) -> type[ObjectLabelDomainScope] | None: """Return the declared object-label output domain for this invocation.""" if ( self.spec.source_context_sources() and not self.spec.preserves_source_stack_scope() ): - return ObjectLabelDomainScope.PAYLOAD + return PayloadLabelScope return None def single_output_object_name(self) -> str: diff --git a/openhcs/interop/cellprofiler/runtime/output_recording.py b/openhcs/interop/cellprofiler/runtime/output_recording.py index ce59e454a4..f745222c83 100644 --- a/openhcs/interop/cellprofiler/runtime/output_recording.py +++ b/openhcs/interop/cellprofiler/runtime/output_recording.py @@ -64,7 +64,7 @@ from openhcs.interop.cellprofiler.runtime.runtime_profile import ( CellProfilerRuntimeProfileLogger, ) -from openhcs.core.runtime_image_values import image_metadata_of +from openhcs.core.runtime_image_values import as_runtime_value from openhcs.core.runtime_image_values import ImagePayload @@ -544,7 +544,7 @@ def record(self, request: CellProfilerOutputRecordRequest) -> None: f"ObjectRelationshipDeclaration, got {len(relations)}." ) _relationship_spec, declaration = relations[0] - source_metadata = image_metadata_of(request.source.payload) + source_metadata = as_runtime_value(request.source.payload).image_metadata request.adapter.add_relationship( ObjectRelationship.from_payload( name=request.spec.name, diff --git a/openhcs/interop/cellprofiler/runtime/profile_fields.py b/openhcs/interop/cellprofiler/runtime/profile_fields.py index 3fa18fee15..6de1c71eb9 100644 --- a/openhcs/interop/cellprofiler/runtime/profile_fields.py +++ b/openhcs/interop/cellprofiler/runtime/profile_fields.py @@ -116,8 +116,8 @@ def object_label_artifact_profile_fields( "declared_object_count": domain.declared_object_count, "declared_object_ids": len(domain.declared_object_ids), "declared_object_id_domains": len(domain.declared_object_id_domains), - "domain_scope": domain.scope.value, - "plane_axis": None if value.plane_axis is None else value.plane_axis.value, + "domain_scope": domain.scope.name, + "plane_axis": value.plane_axis.name, "source_path": value.source_path, "source_component_metadata": source_component_metadata, "source_image_provenance_plane_count": ( diff --git a/openhcs/interop/cellprofiler/runtime/relationship_measurement_rows.py b/openhcs/interop/cellprofiler/runtime/relationship_measurement_rows.py index 184690008b..138afaddee 100644 --- a/openhcs/interop/cellprofiler/runtime/relationship_measurement_rows.py +++ b/openhcs/interop/cellprofiler/runtime/relationship_measurement_rows.py @@ -16,9 +16,6 @@ from openhcs.core.runtime_measurements import ( MeasurementRowAxisField, ) -from openhcs.core.runtime_object_label_domains import ( - ObjectLabelDomainScope, -) from openhcs.core.runtime_relationships import ( DirectedObjectRelationshipPayload, ) @@ -43,7 +40,7 @@ from openhcs.interop.cellprofiler.runtime.runtime_profile import ( CellProfilerRuntimeProfileLogger, ) -from openhcs.core.runtime_image_values import image_metadata_of +from openhcs.core.runtime_image_values import as_runtime_value if TYPE_CHECKING: from openhcs.interop.cellprofiler.runtime.output_record_request import ( @@ -163,9 +160,9 @@ def canonical_relationships( name=spec.name, declaration=declaration, payload=payload, - source_provenance=image_metadata_of( + source_provenance=as_runtime_value( self.request.source.payload - ).source_provenance, + ).image_metadata.source_provenance, ), ) case RuntimeSliceAlignedValues(slices=slices): @@ -493,7 +490,7 @@ def object_numbers_by_label_id( f"got {type(labels).__name__}." ) domain = labels.object_label_domain() - if domain.scope is not ObjectLabelDomainScope.PAYLOAD: + if domain.scope.ids_per_plane: raise ValueError( f"Relationship endpoint {spec.name!r} must be projected to one " "payload-scoped object-ID domain before row materialization." diff --git a/openhcs/processing/backends/cellprofiler/alignment.py b/openhcs/processing/backends/cellprofiler/alignment.py index dc8fb5b04c..26d5089256 100644 --- a/openhcs/processing/backends/cellprofiler/alignment.py +++ b/openhcs/processing/backends/cellprofiler/alignment.py @@ -763,7 +763,7 @@ def execute( """Return aligned image payloads followed by shift measurements.""" input_data = np.asarray(self.image.data) input_metadata = self.image.metadata - if input_metadata.plane_axis is None: + if input_metadata.plane_axis.unstacked: raise ValueError("Align requires a declared input image plane axis.") plane_count = input_metadata.source_provenance.source_plane_count if plane_count < 2: diff --git a/openhcs/processing/backends/cellprofiler/area_occupied.py b/openhcs/processing/backends/cellprofiler/area_occupied.py index 98e54d8347..c955ac19dc 100644 --- a/openhcs/processing/backends/cellprofiler/area_occupied.py +++ b/openhcs/processing/backends/cellprofiler/area_occupied.py @@ -4,6 +4,7 @@ """ from __future__ import annotations +from openhcs.core.runtime_plane_projection import SourceBindingPlaneAxis from dataclasses import dataclass, replace from enum import Enum @@ -39,7 +40,6 @@ object_label_dense_array, ) from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisValueProjection, RuntimeSliceInvariantValue, ) @@ -862,19 +862,17 @@ def _binary_images_from_payload( if not binary_image_names: return () metadata = image.metadata - if metadata.plane_axis is None: + if metadata.plane_axis.unstacked: if len(binary_image_names) != 1: raise ValueError( "MeasureImageAreaOccupied requires a declared source-binding " "axis for multiple binary-image rows." ) return (np.asarray(image.data),) - if metadata.plane_axis is not RuntimePlaneAxis.SOURCE_BINDING: - raise ValueError( - "MeasureImageAreaOccupied binary-image composition requires a " - "source-binding image axis, got " - f"{metadata.plane_axis.value!r}." - ) + metadata.plane_axis.require_source_bindings( + "MeasureImageAreaOccupied binary-image composition requires a " + f"source-binding image axis, got {metadata.plane_axis.name!r}." + ) source_aliases = metadata.source_image_names if source_aliases != binary_image_names: raise ValueError( @@ -882,7 +880,7 @@ def _binary_images_from_payload( f"declared source aliases: {binary_image_names!r} != {source_aliases!r}." ) projection = RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.SOURCE_BINDING, + axis=SourceBindingPlaneAxis, axis_size=len(source_aliases), source_aliases=source_aliases, ) @@ -895,7 +893,7 @@ def _binary_images_from_payload( (RuntimeSliceProjection.value_for_slice( image, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.SOURCE_BINDING, + axis=SourceBindingPlaneAxis, plane_index=index, axis_size=projection.axis_size, source_aliases=projection.source_aliases, diff --git a/openhcs/processing/backends/cellprofiler/colocalization.py b/openhcs/processing/backends/cellprofiler/colocalization.py index 83f085077c..1cea5b180d 100644 --- a/openhcs/processing/backends/cellprofiler/colocalization.py +++ b/openhcs/processing/backends/cellprofiler/colocalization.py @@ -61,11 +61,9 @@ RuntimeMeasurementFeatureRelation, ) from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimeSliceInvariantValue, RuntimeSliceIndexedValue, ) -from openhcs.core.runtime_slice_projection import RuntimeSliceProjection from openhcs.core.runtime_tabular_values import ColumnarRows from openhcs.interop.cellprofiler.cellprofiler_literals import ( cellprofiler_enum_from_literal, @@ -233,14 +231,9 @@ def project_runtime_slice( """Project the optional object-label domain through its declared axis.""" if self.object_labels is None: return self - projection = self.object_labels.declared_plane_projection() - if projection is None or projection.axis is not RuntimePlaneAxis.RUNTIME_SLICE: - return self - projected = RuntimeSliceProjection.value_for_slice( - self.object_labels, - projection.selected_plane(slice_index), + return replace( + self, object_labels=self.object_labels.for_runtime_slice(slice_index) ) - return replace(self, object_labels=projected) class _ColocalizationThresholdMaskOutputsRuntimeParameter(KeywordRuntimeParameter): @@ -1663,7 +1656,7 @@ def valid_mask( return np.isfinite(first_pixels) & np.isfinite(second_pixels) valid = np.isfinite(first_pixels) & np.isfinite(second_pixels) metadata = image.metadata - if metadata.plane_axis is None: + if metadata.plane_axis.unstacked: raise ValueError( "MeasureColocalization masked image pairs require a declared " "image plane axis." @@ -2984,18 +2977,17 @@ def _colocalization_source_plane_output( """Attach one declared composed-image source plane to a derived output.""" metadata = image.metadata - if metadata.plane_axis is None: + if metadata.plane_axis.unstacked: return metadata.project_channel_payload( image, image_data, source_channel_index, channel_data=output_data, ) - if metadata.plane_axis is not RuntimePlaneAxis.SOURCE_BINDING: - raise ValueError( - "MeasureColocalization source channels require the declared " - f"source-binding plane axis, got {metadata.plane_axis.value!r}." - ) + metadata.plane_axis.require_source_bindings( + "MeasureColocalization source channels require the declared " + f"source-binding plane axis, got {metadata.plane_axis.name!r}." + ) source_plane = image.slice_payload(image_data[source_channel_index], source_channel_index,) return source_plane.with_pixels(output_data) diff --git a/openhcs/processing/backends/cellprofiler/color.py b/openhcs/processing/backends/cellprofiler/color.py index f7debb1b69..9823adbe7a 100644 --- a/openhcs/processing/backends/cellprofiler/color.py +++ b/openhcs/processing/backends/cellprofiler/color.py @@ -1,6 +1,7 @@ """Shared CellProfiler color literal semantics.""" from __future__ import annotations +from openhcs.core.runtime_plane_projection import SourceBindingPlaneAxis import re from abc import ABC, abstractmethod @@ -41,7 +42,6 @@ ) from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisValueProjection, ) from openhcs.core.runtime_slice_projection import RuntimeSliceProjection @@ -1600,12 +1600,11 @@ def gray_to_color( ) output = GrayToColorSchemeRunner.for_scheme(scheme).run(request) metadata = image.metadata - if metadata.plane_axis is not None: - if metadata.plane_axis is not RuntimePlaneAxis.SOURCE_BINDING: - raise ValueError( - "GrayToColor image metadata requires a source-binding plane " - f"axis, got {metadata.plane_axis!r}." - ) + if metadata.plane_axis.leads_data: + metadata.plane_axis.require_source_bindings( + "GrayToColor image metadata requires a source-binding plane " + f"axis, got {metadata.plane_axis!r}." + ) source_plane_count = metadata.source_provenance.source_plane_count if source_plane_count < 1: raise ValueError( @@ -1614,7 +1613,7 @@ def gray_to_color( parent_image = RuntimeSliceProjection.value_for_slice( image, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.SOURCE_BINDING, + axis=SourceBindingPlaneAxis, plane_index=0, axis_size=source_plane_count, source_aliases=metadata.source_image_names, @@ -1624,7 +1623,7 @@ def gray_to_color( parent_image = image output_metadata = ( metadata.without_leading_plane_axis() - if metadata.plane_axis is not None + if metadata.plane_axis.leads_data else metadata ) return output_metadata.with_axis(ColourSampleAxisSpec(), -1).payload_with( diff --git a/openhcs/processing/backends/cellprofiler/illumination.py b/openhcs/processing/backends/cellprofiler/illumination.py index 079e43bded..9b28e4566a 100644 --- a/openhcs/processing/backends/cellprofiler/illumination.py +++ b/openhcs/processing/backends/cellprofiler/illumination.py @@ -887,7 +887,7 @@ def calculate_payload( output_metadata = ( metadata.collapse_leading_plane_axis() if self.calculation_scope.uses_all_images - and metadata.plane_axis is not None + and metadata.plane_axis.leads_data else metadata ) return self.payload_result( diff --git a/openhcs/processing/backends/cellprofiler/image_geometry.py b/openhcs/processing/backends/cellprofiler/image_geometry.py index e511e22eba..c8e5cb23ef 100644 --- a/openhcs/processing/backends/cellprofiler/image_geometry.py +++ b/openhcs/processing/backends/cellprofiler/image_geometry.py @@ -907,10 +907,10 @@ def from_image_plane(cls, image: Any) -> "CellProfilerPlaneGeometry": image = ImagePayload.of(image) image_array = np.asarray(image.data) metadata = image.metadata - if metadata.plane_axis is not None: + if metadata.plane_axis.leads_data: raise ValueError( "CellProfiler plane geometry requires an already-projected image; " - f"got declared {metadata.plane_axis.value!r} plane axis." + f"got declared {metadata.plane_axis.name!r} plane axis." ) channel_axis = metadata.axis_index(ColourAxis, image_array) expected_rank = 2 if channel_axis is None else 3 @@ -1254,7 +1254,7 @@ def tile( for image_index in range(num_images): put_tile(image_data[image_index], output_pixels, image_index, geometry) metadata = image.metadata - if metadata.plane_axis is not None: + if metadata.plane_axis.leads_data: metadata = metadata.collapse_leading_plane_axis() return ( metadata.replace_fields( diff --git a/openhcs/processing/backends/cellprofiler/image_math.py b/openhcs/processing/backends/cellprofiler/image_math.py index f4957d3182..418c706678 100644 --- a/openhcs/processing/backends/cellprofiler/image_math.py +++ b/openhcs/processing/backends/cellprofiler/image_math.py @@ -1,6 +1,7 @@ """ImageMath operation semantics for CellProfiler-compatible processing.""" from __future__ import annotations +from openhcs.core.runtime_plane_projection import SourceBindingPlaneAxis from abc import ABC, abstractmethod from dataclasses import dataclass @@ -25,7 +26,6 @@ project_image_mask_to_data_domain, ) from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisValueProjection, ) from openhcs.core.runtime_slice_projection import RuntimeSliceProjection @@ -827,30 +827,30 @@ def _source_payloads( image: RuntimeArrayData, ) -> tuple[RuntimeArrayData, ...]: metadata = image.metadata - if metadata.plane_axis is not RuntimePlaneAxis.SOURCE_BINDING: - return (image,) - axis_size = metadata.source_provenance.source_plane_count - if axis_size <= 0: - raise ValueError( - "ImageMath SOURCE_BINDING operand payload has no declared source " - "plane provenance." + if metadata.plane_axis.one_plane_per_source_binding: + axis_size = metadata.source_provenance.source_plane_count + if axis_size <= 0: + raise ValueError( + "ImageMath SOURCE_BINDING operand payload has no declared " + "source plane provenance." + ) + projection = RuntimePlaneAxisValueProjection.preserve( + axis=SourceBindingPlaneAxis, + axis_size=axis_size, + source_aliases=metadata.source_image_names, ) - projection = RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.SOURCE_BINDING, - axis_size=axis_size, - source_aliases=metadata.source_image_names, - ) - projection.validate_shape( - array_geometry(image).shape, - value_name="ImageMath operand payload", - ) - return tuple( - RuntimeSliceProjection.value_for_slice( - image, - projection.selected_plane(index), + projection.validate_shape( + array_geometry(image).shape, + value_name="ImageMath operand payload", ) - for index in range(axis_size) - ) + return tuple( + RuntimeSliceProjection.value_for_slice( + image, + projection.selected_plane(index), + ) + for index in range(axis_size) + ) + return (image,) def operand_pixel(self, index: int) -> np.ndarray: """Return one logical operand without forcing stacked copies.""" @@ -877,7 +877,7 @@ def output_metadata(self) -> ImagePayloadMetadata: metadata = self.source_image.metadata if metadata.unit_interval_intensity is not None: metadata = metadata.without_unit_interval_intensity_scale() - if metadata.plane_axis is RuntimePlaneAxis.SOURCE_BINDING: + if metadata.plane_axis.one_plane_per_source_binding: return metadata.collapse_leading_plane_axis() return metadata diff --git a/openhcs/processing/backends/cellprofiler/intensity.py b/openhcs/processing/backends/cellprofiler/intensity.py index afe8271324..b052b24ff3 100644 --- a/openhcs/processing/backends/cellprofiler/intensity.py +++ b/openhcs/processing/backends/cellprofiler/intensity.py @@ -296,9 +296,6 @@ def active_artifact_bindings( RuntimeMeasurementFeatureRelationDeclaration, RuntimeMeasurementFeatureSemanticMarker, ) -from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, -) from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.runtime_image_values import ( ImageMetadataPayload, @@ -1497,7 +1494,7 @@ def _object_intensity_prepared_labels_for_batch_group( labels = first_request.completion_label_payload if not isinstance(labels, ObjectLabelValue): return None - if labels.plane_axis is RuntimePlaneAxis.SOURCE_BINDING: + if labels.plane_axis.one_plane_per_source_binding: return None return ObjectIntensityPreparedLabels.from_measurement( image=first_request.image.data, diff --git a/openhcs/processing/backends/cellprofiler/intensity_distribution.py b/openhcs/processing/backends/cellprofiler/intensity_distribution.py index 8876b2c70e..b533685681 100644 --- a/openhcs/processing/backends/cellprofiler/intensity_distribution.py +++ b/openhcs/processing/backends/cellprofiler/intensity_distribution.py @@ -66,10 +66,8 @@ dense_object_label_id_domain, ) from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimeSliceIndexedValue, ) -from openhcs.core.runtime_slice_projection import RuntimeSliceProjection from openhcs.core.runtime_tabular_values import ColumnarRows from openhcs.interop.cellprofiler.parser import ModuleBlock, ModuleSetting from openhcs.interop.cellprofiler.setting_names import ( @@ -279,14 +277,9 @@ def project_runtime_slice( self, slice_index: int ) -> "IntensityDistributionHeatmapRuntimeOutput": """Project the heatmap object-label domain through its declared axis.""" - projection = self.object_labels.declared_plane_projection() - if projection is None or projection.axis is not RuntimePlaneAxis.RUNTIME_SLICE: - return self - projected = RuntimeSliceProjection.value_for_slice( - self.object_labels, - projection.selected_plane(slice_index), - ) - return replace(self, object_labels=projected) + return replace( + self, object_labels=self.object_labels.for_runtime_slice(slice_index) + ) class _IntensityDistributionHeatmapOutputsRuntimeParameter(KeywordRuntimeParameter): diff --git a/openhcs/processing/backends/cellprofiler/measurement_math.py b/openhcs/processing/backends/cellprofiler/measurement_math.py index 24b094d3cc..5e60be2f26 100644 --- a/openhcs/processing/backends/cellprofiler/measurement_math.py +++ b/openhcs/processing/backends/cellprofiler/measurement_math.py @@ -1,6 +1,7 @@ """CalculateMath measurement semantics for CellProfiler-compatible processing.""" from __future__ import annotations +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from openhcs.core.artifacts.plans import ArtifactInputPlan from abc import ABC, abstractmethod @@ -42,16 +43,12 @@ MeasurementRowAxisField, MeasurementRowValueField, ) -from openhcs.core.runtime_object_label_domains import ( - ObjectLabelPlaneDomainStrategy, -) from openhcs.core.runtime_slice_alignment import ( RuntimeSliceAlignedValueSet, RuntimeSliceAlignedValues, ) from openhcs.core.runtime_slice_projection import RuntimeSliceProjection from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisValueProjection, ) from openhcs.core.runtime_tabular_values import ( @@ -344,9 +341,7 @@ def operand_value( if count_object_name is not None: labels = request.adapter.get_objects(count_object_name) domain = labels.object_label_domain() - object_id_domains = ObjectLabelPlaneDomainStrategy.for_enum_member( - domain.scope - ).plane_domains( + object_id_domains = domain.scope.plane_domains( labels, domain=domain, ) @@ -1113,7 +1108,7 @@ def aligned_operands(self) -> tuple[tuple[int, Any, Any], ...] | None: for slice_index in range(slice_count) for projection in ( RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, plane_index=slice_index, axis_size=slice_count, ), diff --git a/openhcs/processing/backends/cellprofiler/outlines.py b/openhcs/processing/backends/cellprofiler/outlines.py index 2fe6497b7b..2ae7b165bb 100644 --- a/openhcs/processing/backends/cellprofiler/outlines.py +++ b/openhcs/processing/backends/cellprofiler/outlines.py @@ -43,7 +43,6 @@ ObjectLabelStorageStrategy, object_label_dense_array, ) -from openhcs.core.runtime_object_label_domains import ObjectLabelDomainScope from openhcs.interop.cellprofiler.settings_binder import coerce_cellprofiler_enum from openhcs.processing.backends.cellprofiler._backend import ( CellProfilerBackendStrategyMixin, @@ -141,7 +140,7 @@ def execution_mode( labels = kwargs[cls.input_objects_binding.require_runtime_parameter_name()] if ( isinstance(labels, ObjectLabelValue) - and labels.object_label_domain().scope is ObjectLabelDomainScope.PAYLOAD + and labels.object_label_domain().scope.ids_span_payload ): return FullStackExecution return default diff --git a/openhcs/processing/backends/cellprofiler/relationships.py b/openhcs/processing/backends/cellprofiler/relationships.py index f51439f740..86f7649941 100644 --- a/openhcs/processing/backends/cellprofiler/relationships.py +++ b/openhcs/processing/backends/cellprofiler/relationships.py @@ -847,9 +847,6 @@ def aggregate_child_feature_name( measurement_axis_integer_domain, measurement_axis_integer_value, ) -from openhcs.core.runtime_object_label_domains import ( - ObjectLabelDomainScope, -) from openhcs.core.runtime_tabular_values import ColumnarRows from openhcs.core.runtime_object_labels import ( dense_label_centers_2d_numba, @@ -1602,7 +1599,7 @@ def upstream_child_feature_indexes( plane_domains = core_rows.label_plane_domains() payload_scoped = ( - child_labels.object_label_domain().scope is ObjectLabelDomainScope.PAYLOAD + child_labels.object_label_domain().scope.ids_span_payload ) for slice_index, _plane_domain in enumerate(plane_domains): object_numbers = self.object_numbers_by_label_id( diff --git a/openhcs/processing/backends/cellprofiler/worms.py b/openhcs/processing/backends/cellprofiler/worms.py index 74a6e41d72..d01251bd52 100644 --- a/openhcs/processing/backends/cellprofiler/worms.py +++ b/openhcs/processing/backends/cellprofiler/worms.py @@ -2138,7 +2138,7 @@ def straighten_worms( image_data = np.asarray(image.data) image_stack = image_data[np.newaxis, :, :] if image_data.ndim == 2 else image_data source_metadata = image.metadata - if image_stack.shape[0] > 1 and source_metadata.plane_axis is None: + if image_stack.shape[0] > 1 and source_metadata.plane_axis.unstacked: raise ValueError( "StraightenWorms requires a declared leading plane axis for " "multiple input images." @@ -2190,7 +2190,7 @@ def straighten_worms( ).execute(placements) output_metadata = ( source_metadata.for_leading_source_plane(slice_index) - if source_metadata.plane_axis is not None + if source_metadata.plane_axis.leads_data else source_metadata ) output_spatial_shape = output_metadata.spatial_shape_yx(slice_image) diff --git a/openhcs/runtime/fiji_viewer_server.py b/openhcs/runtime/fiji_viewer_server.py index 7c45f24003..c4ff774efa 100644 --- a/openhcs/runtime/fiji_viewer_server.py +++ b/openhcs/runtime/fiji_viewer_server.py @@ -443,14 +443,14 @@ def _plane_component_domain( ) @staticmethod - def _require_plane_axis(item: FijiWireItem) -> RuntimePlaneAxis: + def _require_plane_axis(item: FijiWireItem) -> type[RuntimePlaneAxis]: raw_axis = item.payload.get(ViewerWireField.PLANE_AXIS.value) if raw_axis is None: raise ValueError( "Fiji payload-local plane component declaration requires plane_axis." ) try: - return RuntimePlaneAxis(raw_axis) + return RuntimePlaneAxis.named(raw_axis) except (TypeError, ValueError) as error: raise ValueError( f"Unsupported Fiji payload-local plane axis {raw_axis!r}." diff --git a/openhcs/runtime/napari_viewer_server.py b/openhcs/runtime/napari_viewer_server.py index ef4dcdf378..9562256b44 100644 --- a/openhcs/runtime/napari_viewer_server.py +++ b/openhcs/runtime/napari_viewer_server.py @@ -6,6 +6,7 @@ """ from __future__ import annotations +from openhcs.core.runtime_plane_projection import NoPlaneAxis, RuntimePlaneAxis import logging import os @@ -84,7 +85,6 @@ NapariDisplaySettings, NapariSlots, ) -from metaclass_registry.strategies import EnumKeyedStrategyMixin from openhcs.core.runtime_image_values import ( ImagePayloadMetadata, ) @@ -657,7 +657,12 @@ def from_payload_map( if data_type_value is None: data_type_value = DEFAULT_IMAGE_DATA_TYPE source_channel_axis = payload.optional(ViewerWireField.SOURCE_CHANNEL_AXIS) - plane_axis = payload.optional(ViewerWireField.PLANE_AXIS) + plane_axis_name = payload.optional(ViewerWireField.PLANE_AXIS) + plane_axis = ( + NoPlaneAxis + if plane_axis_name is None + else RuntimePlaneAxis.named(plane_axis_name) + ) return cls( entries=layer_axis_projection_semantics.entries, layout=layer_axis_projection_semantics.layout, @@ -857,7 +862,7 @@ def for_stream_layer_context( return None metadata = stream_layer_context.image_metadata has_channel_axis = metadata.axis_position(ColourAxis) is not None - has_plane_axis = metadata.plane_axis is not None + has_plane_axis = metadata.plane_axis.leads_data if has_channel_axis: return cls.COLOR_STACK if has_plane_axis else cls.COLOR_PLANE return cls.SCALAR_STACK if has_plane_axis else cls.SCALAR_PLANE diff --git a/tests/integration/test_cellprofiler_generated_pipeline.py b/tests/integration/test_cellprofiler_generated_pipeline.py index ba9ad53108..ec319242a8 100644 --- a/tests/integration/test_cellprofiler_generated_pipeline.py +++ b/tests/integration/test_cellprofiler_generated_pipeline.py @@ -1,4 +1,5 @@ from __future__ import annotations +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from openhcs.core.pipeline_document import PipelineDocumentCodec import ast @@ -64,7 +65,6 @@ from openhcs.core.runtime_relationships import ( ObjectRelationship, ) -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.runtime_tabular_values import measurement_row_mapping from openhcs.core.runtime_stores import StoredRuntimeValue from openhcs.core.source_binding_workspace import ( @@ -847,7 +847,7 @@ def test_official_colocalization_preserves_relationships_and_configured_export( Microscopy.Channel, "1", (2, 1040, 1392), - RuntimePlaneAxis.RUNTIME_SLICE, + RuntimeSlicePlaneAxis, ), ), "Objects2": ( @@ -855,7 +855,7 @@ def test_official_colocalization_preserves_relationships_and_configured_export( Microscopy.Channel, "2", (2, 1040, 1392), - RuntimePlaneAxis.RUNTIME_SLICE, + RuntimeSlicePlaneAxis, ), ), "ColocalizedRegion": ( @@ -863,7 +863,7 @@ def test_official_colocalization_preserves_relationships_and_configured_export( Microscopy.Channel, "1", (2, 1040, 1392), - RuntimePlaneAxis.RUNTIME_SLICE, + RuntimeSlicePlaneAxis, ), ), } diff --git a/tests/integration/test_neurite_outgrowth_synthetic_plate.py b/tests/integration/test_neurite_outgrowth_synthetic_plate.py index c11cefc9ab..75ce69b60b 100644 --- a/tests/integration/test_neurite_outgrowth_synthetic_plate.py +++ b/tests/integration/test_neurite_outgrowth_synthetic_plate.py @@ -1,5 +1,6 @@ """Synthetic-plate integration coverage for 2D neurite outgrowth.""" +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from openhcs.core.progress import ProgressExecutionContext import csv import io @@ -57,7 +58,6 @@ from openhcs.core.progress.live_measurements import LiveMeasurementProgressPayload from openhcs.core.steps import FunctionStep from openhcs.core.source_metadata import SourceVoxelSpacing -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.source_workspace_projection import ( VirtualWorkspacePathLookup, VirtualWorkspaceSourceProjection, @@ -362,7 +362,7 @@ def observe_dense_output(output, components): assert projection.execution_scope is not None assert projection.image_metadata is not None assert ( - projection.image_metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + projection.image_metadata.plane_axis is RuntimeSlicePlaneAxis ) assert projection.image_metadata.source_provenance.source_plane_count == 2 diff --git a/tests/unit/agent/test_callable_artifact_reference.py b/tests/unit/agent/test_callable_artifact_reference.py index f9a5a29d67..525b5d85dc 100644 --- a/tests/unit/agent/test_callable_artifact_reference.py +++ b/tests/unit/agent/test_callable_artifact_reference.py @@ -1,5 +1,8 @@ """Execute the canonical public usage code, rather than a copied example.""" +from openhcs.core.runtime_object_label_domains import PayloadLabelScope, PlaneLabelScope +from openhcs.core.runtime_plane_projection import NoPlaneAxis +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis import re import runpy import sys @@ -33,14 +36,12 @@ PIPELINE_INPUT_ARTIFACT, ) from openhcs.core.runtime_image_values import ImageMetadataPayload, ImagePayloadMetadata -from openhcs.core.runtime_object_label_domains import ObjectLabelDomainScope from openhcs.core.runtime_object_labels import ( ObjectLabelPayload, ObjectLabelValue, ObjectLabelVariantData, ) from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisValueProjection, ) @@ -330,7 +331,7 @@ def fixture_storage(_name, *, create): assert returned[2].row_mappings()[0]["pixel_count"] == 16 stack = ImageMetadataPayload( data=np.stack((fixture, fixture)), - metadata=ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE), + metadata=ImagePayloadMetadata(plane_axis=RuntimeSlicePlaneAxis), ) stacked = metadata.func(stack) assert stacked[2].row_mappings() == ( @@ -453,7 +454,7 @@ def test_reference_retains_plane_domain_until_raw_numpy_invocation( metadata=ImagePayloadMetadata( source_dtype="uint16", source_path="/synthetic/label-fixture.tif", - plane_axis=None if plane_count is None else RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=NoPlaneAxis if plane_count is None else RuntimeSlicePlaneAxis, ), ) call_argument = contract.main_flow_call_argument(stack) @@ -466,27 +467,26 @@ def test_reference_retains_plane_domain_until_raw_numpy_invocation( for plane in range(1 if plane_count is None else plane_count) ) projection = ( - None if plane_count is None else RuntimePlaneAxisValueProjection( - axis=RuntimePlaneAxis.RUNTIME_SLICE, source_aliases=(), - plane_index=None, axis_size=plane_count, + None if plane_count is None else RuntimePlaneAxisValueProjection.preserve( + axis=RuntimeSlicePlaneAxis, axis_size=plane_count, ) ) label_payload = ObjectLabelsArtifactType.contextualize_output(stack, labels, MAIN_FLOW, projection) assert isinstance(label_payload, ObjectLabelPayload) np.testing.assert_array_equal(label_payload.labels, stack.data) assert label_payload.plane_axis is ( - None if plane_count is None else RuntimePlaneAxis.RUNTIME_SLICE + NoPlaneAxis if plane_count is None else RuntimeSlicePlaneAxis ) assert label_payload.source_provenance == stack.metadata.source_provenance assert label_payload.source_spatial_domain.source_shape_yx == (8, 8) assert label_payload.domain.scope is ( - ObjectLabelDomainScope.PAYLOAD - if plane_count is None else ObjectLabelDomainScope.PLANE + PayloadLabelScope + if plane_count is None else PlaneLabelScope ) if plane_count is not None: assert label_payload.domain.declared_object_id_domains == ((1,),) * plane_count wrong_spatial_shape = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(stack.data[:, :-1, :]) with pytest.raises(ValueError, match="Object-label spatial shape"): ObjectLabelsArtifactType.contextualize_output(stack, wrong_spatial_shape, MAIN_FLOW, projection) diff --git a/tests/unit/agent/test_plate_streaming_service.py b/tests/unit/agent/test_plate_streaming_service.py index 0a4347af7e..cf81778b05 100644 --- a/tests/unit/agent/test_plate_streaming_service.py +++ b/tests/unit/agent/test_plate_streaming_service.py @@ -147,7 +147,7 @@ def test_saved_site_free_image_inventory_and_loading_preserve_original_scope(tmp np.testing.assert_array_equal(actual.data, pixels) actual_metadata = actual.metadata assert "site" not in actual_metadata.source_component_metadata - assert actual_metadata.plane_axis is None + assert actual_metadata.plane_axis.unstacked assert actual_metadata.source_voxel_spacing.values_zyx == (0.25, 0.5) assert actual_metadata.source_provenance.source_image_provenance_planes == metadata.source_provenance.source_image_provenance_planes diff --git a/tests/unit/agent/test_selected_plane_reopen_stream.py b/tests/unit/agent/test_selected_plane_reopen_stream.py index 333e000a04..73f3af8cba 100644 --- a/tests/unit/agent/test_selected_plane_reopen_stream.py +++ b/tests/unit/agent/test_selected_plane_reopen_stream.py @@ -1,5 +1,6 @@ """Ordinary saved-image streaming must reach the strict receiver with owned axes.""" +from openhcs.core.runtime_plane_projection import NoPlaneAxis from dataclasses import replace from hashlib import sha256 from multiprocessing.shared_memory import SharedMemory @@ -114,7 +115,7 @@ def receive(_manager, data, paths, backend, **fields): SharedMemory(name=name) -@pytest.mark.parametrize("axis", (None, *RuntimePlaneAxis)) +@pytest.mark.parametrize("axis", (NoPlaneAxis, *RuntimePlaneAxis.leading_axes())) def test_public_declared_saved_image_reaches_strict_receiver( tmp_path, receiver_transport, axis ): @@ -126,7 +127,7 @@ def test_public_declared_saved_image_reaches_strict_receiver( plane_axis=axis, source_image_provenance_planes=( SourceImageProvenancePlanes() - if axis is None + if axis.unstacked else SourceImageProvenancePlanes.from_components( paths=(projection.image_metadata.source_path,), component_metadata=( @@ -136,7 +137,7 @@ def test_public_declared_saved_image_reaches_strict_receiver( ), ) image_format = ImageFileFormat.require_path(path) - pixels = expected if axis is None else expected[None] + pixels = expected if axis.unstacked else expected[None] image = metadata.payload_with(pixels) image_format.write(path, image) projection = replace( @@ -178,7 +179,7 @@ def test_public_declared_saved_image_reaches_strict_receiver( assert item.image_metadata.source_voxel_spacing == SourceVoxelSpacing( (1.3556, 1.3556) ) - assert item.image_metadata.plane_axis is None + assert item.image_metadata.plane_axis.unstacked assert before == {p: sha256(p.read_bytes()).hexdigest() for p in before} @@ -211,7 +212,7 @@ def test_plain_persisted_tiff_without_saved_image_metadata_streams( assert len(received) == 1 for transmitted, item, bindings in received: np.testing.assert_array_equal(transmitted, expected) - assert item.image_metadata.plane_axis is None + assert item.image_metadata.plane_axis.unstacked assert bindings.payload_axes == () assert item.image_metadata.source_voxel_spacing == SourceVoxelSpacing( (0.5, 0.5) diff --git a/tests/unit/test_aggregate_layer_materialization_515.py b/tests/unit/test_aggregate_layer_materialization_515.py index 1d62431c7c..c839e1d810 100644 --- a/tests/unit/test_aggregate_layer_materialization_515.py +++ b/tests/unit/test_aggregate_layer_materialization_515.py @@ -4,6 +4,8 @@ transport is intercepted; no viewer or native process is started. """ +from openhcs.core.runtime_object_label_domains import PlaneLabelScope +from openhcs.core.runtime_plane_projection import SourceBindingPlaneAxis from dataclasses import replace import numpy as np @@ -16,9 +18,9 @@ ) from openhcs.core.runtime_artifact_values import RuntimeValue from openhcs.core.runtime_image_values import ImagePayloadMetadata -from openhcs.core.runtime_object_label_domains import ObjectLabelDomain, ObjectLabelDomainScope +from openhcs.core.runtime_object_label_domains import ObjectLabelDomain from openhcs.core.runtime_object_labels import ObjectLabelPayload, ObjectLabelVariantData -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis, RuntimePlaneAxisValueProjection +from openhcs.core.runtime_plane_projection import RuntimePlaneAxisValueProjection from openhcs.core.runtime_slice_projection import ( RuntimeProjectionSourceIdentityRequest, RequiredSourceComponentMetadata, @@ -90,9 +92,9 @@ def _source_bound_labels(component): ) labels = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=pixels), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, domain=ObjectLabelDomain( - declared_object_id_domains=((1,), (2,)), scope=ObjectLabelDomainScope.PLANE, + declared_object_id_domains=((1,), (2,)), scope=PlaneLabelScope, ), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=paths, component_metadata=source_fields, @@ -157,7 +159,7 @@ def save_batch(contents, paths, backend, **kwargs): np.testing.assert_array_equal(persisted_pixels, pixels) assert persisted_pixels.dtype == pixels.dtype metadata = disk_outputs[0].metadata - assert metadata.plane_axis is RuntimePlaneAxis.SOURCE_BINDING + assert metadata.plane_axis is SourceBindingPlaneAxis assert metadata.source_provenance.source_plane_count == 2 assert metadata.source_voxel_spacing.values_zyx == (0.75, 0.75) assert [metadata.source_provenance.for_source_plane(index).source_component_metadata[component.name] @@ -246,7 +248,7 @@ def test_automatic_aggregate_label_tiff_preserves_stack(component, viewer_ack_re item = batches[0].batch_images[0] assert tuple(item["shape"]) == pixels.shape assert item["dtype"] == str(pixels.dtype) - assert item["plane_axis"] == RuntimePlaneAxis.SOURCE_BINDING.value + assert item["plane_axis"] == SourceBindingPlaneAxis.name assert item["plane_component_values"] == {component.name: ["1", "2"]} decoded = ImagePayloadMetadata.from_viewer_image_metadata(item["image_metadata"]) assert decoded.source_voxel_spacing.values_zyx == (0.75, 0.75) @@ -308,9 +310,9 @@ def test_single_output_kwargs_cannot_hide_projected_pixels( for index, item in enumerate(projected): np.testing.assert_array_equal(item.content, pixels[index]) assert np.shares_memory(item.content, pixels) - assert item.metadata.plane_axis is None + assert item.metadata.plane_axis.unstacked assert item.source_component_metadata["channel"] == index + 1 - assert metadata.plane_axis is RuntimePlaneAxis.SOURCE_BINDING + assert metadata.plane_axis is SourceBindingPlaneAxis np.testing.assert_array_equal(output.content, pixels[:plane_count]) @@ -372,14 +374,14 @@ def test_existing_projection_owns_source_binding_label_planes(component): value=payload, source_description="automatic label TIFF", variable_components=(component,), plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.SOURCE_BINDING, axis_size=2, + axis=SourceBindingPlaneAxis, axis_size=2, ), ) ) assert len(items) == 2 for index, item in enumerate(items): np.testing.assert_array_equal(item.data, pixels[index]) - assert item.metadata.plane_axis is None + assert item.metadata.plane_axis.unstacked assert item.require_source_component_metadata()[component.name] == index + 1 assert item.metadata.source_voxel_spacing.values_zyx == (0.75, 0.75) assert np.shares_memory(item.data, labels.data) @@ -406,7 +408,7 @@ def test_existing_projection_rejects_label_source_cardinality_mismatch(): source_description="automatic label TIFF", variable_components=(Microscopy.Channel,), plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.SOURCE_BINDING, axis_size=3, + axis=SourceBindingPlaneAxis, axis_size=3, ), ) ) diff --git a/tests/unit/test_aggregate_source_scope.py b/tests/unit/test_aggregate_source_scope.py index 7855206b26..2edded41a4 100644 --- a/tests/unit/test_aggregate_source_scope.py +++ b/tests/unit/test_aggregate_source_scope.py @@ -1,5 +1,6 @@ """Contributor scope and saved producer addresses survive their real consumers.""" +from openhcs.core.runtime_plane_projection import SourceBindingPlaneAxis from dataclasses import dataclass, replace from pathlib import Path import pickle @@ -21,7 +22,7 @@ ) from openhcs.core.runtime_image_values import ImagePayloadMetadata from openhcs.core.runtime_measurements import MeasurementScope, MeasurementSubject, MeasurementTable -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis, RuntimePlaneAxisValueProjection +from openhcs.core.runtime_plane_projection import RuntimePlaneAxisValueProjection from openhcs.core.runtime_stores import ( RuntimeArtifactAddress, RuntimeArtifactLocation, RuntimeValueStore, ) @@ -43,7 +44,7 @@ def _source(sites): return ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_voxel_spacing=SourceVoxelSpacing((0.5, 0.5)), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=tuple(f"/input/field_{i}.tif" for i in range(len(sites))), @@ -84,7 +85,7 @@ def test_mixed_acquired_and_reduced_batch_uses_distinct_original_display_scopes( declaration = DeclaredReducedField(np.full((3, 4), 720, dtype=np.uint16), []) reduced = ImageArtifactType.contextualize_output( source, declaration, MAIN_FLOW, - RuntimePlaneAxisValueProjection.preserve(axis=RuntimePlaneAxis.SOURCE_BINDING, axis_size=2), + RuntimePlaneAxisValueProjection.preserve(axis=SourceBindingPlaneAxis, axis_size=2), ) assert declaration.calls == ["before", "after"] acquired_metadata = source.metadata.for_leading_source_plane(0) diff --git a/tests/unit/test_aligned_image_payload_device.py b/tests/unit/test_aligned_image_payload_device.py index b638e1b139..00654371eb 100644 --- a/tests/unit/test_aligned_image_payload_device.py +++ b/tests/unit/test_aligned_image_payload_device.py @@ -1,3 +1,4 @@ +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis import numpy as np import pytest import sys @@ -11,7 +12,6 @@ ) from openhcs.core.runtime_image_values import ( ImagePayloadMetadata, - ImagePayloadMetadataCompositionMode, ImageUnitIntervalIntensityMetadata, ) from openhcs.core.source_spatial_domain import SourceSpatialDomain @@ -98,8 +98,8 @@ def upload(values): @pytest.mark.parametrize('composition,mode', ( - (ImagePayloadStackContext, ImagePayloadMetadataCompositionMode.STACK), - (ImagePayloadBundleContext, ImagePayloadMetadataCompositionMode.BUNDLE), + (ImagePayloadStackContext, RuntimeSlicePlaneAxis), + (ImagePayloadBundleContext, SourceBindingPlaneAxis), )) @pytest.mark.parametrize('raw_first', (False, True)) @pytest.mark.parametrize('destination', ('implicit', 'numpy', 'cupy')) diff --git a/tests/unit/test_aligned_image_payload_registry.py b/tests/unit/test_aligned_image_payload_registry.py index 20de9626d1..901eed74f2 100644 --- a/tests/unit/test_aligned_image_payload_registry.py +++ b/tests/unit/test_aligned_image_payload_registry.py @@ -1,4 +1,6 @@ from __future__ import annotations +from openhcs.core.runtime_object_label_domains import PlaneLabelScope +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis import numpy as np @@ -14,9 +16,8 @@ ImagePayloadMetadata, ) from openhcs.core.runtime_object_labels import ObjectLabelSet, ObjectLabelVariantData -from openhcs.core.runtime_object_label_domains import ObjectLabelDomain, ObjectLabelDomainScope +from openhcs.core.runtime_object_label_domains import ObjectLabelDomain from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisValueProjection, ) from openhcs.core.runtime_slice_alignment import RuntimeSliceAlignedValues @@ -26,7 +27,7 @@ ) -def _resolver(*, axis=RuntimePlaneAxis.RUNTIME_SLICE, size=2, index=1): +def _resolver(*, axis=RuntimeSlicePlaneAxis, size=2, index=1): return AlignedImageStackKwargResolver( RuntimePlaneAxisValueProjection.from_selected_plane( axis=axis, plane_index=index, axis_size=size, @@ -57,7 +58,7 @@ class Unknown: def test_image_aligned_operation_does_not_remove_runtime_plane_axis(): - value = ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE).payload_with( + value = ImagePayloadMetadata(plane_axis=RuntimeSlicePlaneAxis).payload_with( np.arange(24, dtype=np.float32).reshape(2, 3, 4), ) resolver = _resolver() @@ -67,21 +68,21 @@ def test_image_aligned_operation_does_not_remove_runtime_plane_axis(): assert ordinary.data.shape == (3, 4) -@pytest.mark.parametrize("axis", (RuntimePlaneAxis.RUNTIME_SLICE, RuntimePlaneAxis.SOURCE_BINDING)) +@pytest.mark.parametrize("axis", (RuntimeSlicePlaneAxis, SourceBindingPlaneAxis)) def test_produced_literal_stack_uses_image_aligned_operation_without_selecting_a_plane(axis): value = ProducedImageStack( tuple(np.full((3, 4), index, dtype=np.float32) for index in (1, 2, 3)), - memory_type="numpy", plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + memory_type="numpy", plane_axis=RuntimeSlicePlaneAxis, ) assert _resolver(axis=axis).resolve(value) is value assert value._composed_payload is None @pytest.mark.parametrize("inner_count", (1, 2, 3)) -@pytest.mark.parametrize("axis", (RuntimePlaneAxis.RUNTIME_SLICE, RuntimePlaneAxis.SOURCE_BINDING)) +@pytest.mark.parametrize("axis", (RuntimeSlicePlaneAxis, SourceBindingPlaneAxis)) def test_image_output_bundle_aligned_operation_selects_outer_member_then_recurses(inner_count, axis): first, second = tuple( - ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE).payload_with( + ImagePayloadMetadata(plane_axis=RuntimeSlicePlaneAxis).payload_with( np.full((inner_count, 3, 4), index, dtype=np.float32), ) for index in (1, 2) ) @@ -100,7 +101,7 @@ def test_image_output_bundle_aligned_operation_selects_outer_member_then_recurse def test_image_output_bundle_aligned_operation_checks_outer_cardinality_before_inner_planes(): - value = ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE).payload_with( + value = ImagePayloadMetadata(plane_axis=RuntimeSlicePlaneAxis).payload_with( np.ones((3, 3, 4), dtype=np.float32), ) bundle = ImageOutputBundle( @@ -113,7 +114,7 @@ def test_image_output_bundle_aligned_operation_checks_outer_cardinality_before_i def test_image_output_bundle_aligned_operation_recurses_through_selected_bundle(): values = tuple( - ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE).payload_with( + ImagePayloadMetadata(plane_axis=RuntimeSlicePlaneAxis).payload_with( np.full((3, 3, 4), index, dtype=np.float32), ) for index in (1, 2) ) @@ -125,13 +126,13 @@ def test_image_output_bundle_aligned_operation_recurses_through_selected_bundle( assert _resolver().resolve(outer) is values[1] -@pytest.mark.parametrize("axis", (RuntimePlaneAxis.RUNTIME_SLICE, RuntimePlaneAxis.SOURCE_BINDING)) +@pytest.mark.parametrize("axis", (RuntimeSlicePlaneAxis, SourceBindingPlaneAxis)) def test_aligned_capability_owns_outer_count_for_both_declared_axes(axis): first, second = object(), object() value = RuntimeSliceAlignedValues((first, second)) resolver = _resolver(axis=axis) assert resolver.resolve(value) is second - if axis is RuntimePlaneAxis.SOURCE_BINDING: + if axis is SourceBindingPlaneAxis: assert RuntimeSliceProjection.value_for_slice(value, resolver.projection_axis) is value with pytest.raises(ValueError, match="count must exactly match"): _resolver(axis=axis, size=3).resolve(value) @@ -140,10 +141,10 @@ def test_aligned_capability_owns_outer_count_for_both_declared_axes(axis): def test_object_label_count_is_checked_before_projection_and_spatial_alignment(monkeypatch): labels = ObjectLabelSet( name="Labels", variant_data=ObjectLabelVariantData(labels=np.ones((2, 3, 4), dtype=np.int32)), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (1,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) monkeypatch.setattr(RuntimeSliceProjection, "value_for_slice", lambda *_args: pytest.fail("cardinality comes first")) diff --git a/tests/unit/test_aligned_output_projection.py b/tests/unit/test_aligned_output_projection.py index bd704339a8..322fb455db 100644 --- a/tests/unit/test_aligned_output_projection.py +++ b/tests/unit/test_aligned_output_projection.py @@ -1,3 +1,4 @@ +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis import numpy as np import pytest @@ -12,7 +13,6 @@ ) from openhcs.core.memory import MemoryType from openhcs.core.runtime_image_values import ImagePayload, ImagePayloadMetadata -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.steps.function_runtime import PatternGroupExecutionRequest from tests.context_support import ( executing_context, @@ -25,10 +25,10 @@ def test_runtime_projects_mixed_output_planes_once_with_their_contexts(monkeypat mask = first > 3 payloads = ( ImagePayloadMetadata( - source_path="first.tif", plane_axis=RuntimePlaneAxis.RUNTIME_SLICE + source_path="first.tif", plane_axis=RuntimeSlicePlaneAxis ).payload_with(first, mask), ImagePayloadMetadata( - source_path="second.tif", plane_axis=RuntimePlaneAxis.RUNTIME_SLICE + source_path="second.tif", plane_axis=RuntimeSlicePlaneAxis ).payload_with(second, None), ) contexts = tuple(AlignedImageSliceContext.main_flow(name) for name in ("A", "B")) @@ -69,13 +69,13 @@ def counted(payload): np.testing.assert_array_equal(payload.mask, mask[plane]) else: assert payload.mask is None - assert payload.metadata.plane_axis is None + assert payload.metadata.plane_axis.unstacked def test_shared_projection_retains_nesting_and_fresh_metadata_snapshots(): array = np.ones((2, 2, 2), dtype=np.float32) metadata = ImagePayloadMetadata( - source_path="first.tif", plane_axis=RuntimePlaneAxis.RUNTIME_SLICE + source_path="first.tif", plane_axis=RuntimeSlicePlaneAxis ) payload = metadata.payload_with(array, None) stack = AlignedImageStack((payload,)) diff --git a/tests/unit/test_automatic_image_role_materialization_435.py b/tests/unit/test_automatic_image_role_materialization_435.py index 97f84d7bfc..46267133e9 100644 --- a/tests/unit/test_automatic_image_role_materialization_435.py +++ b/tests/unit/test_automatic_image_role_materialization_435.py @@ -1,5 +1,6 @@ """Default retained image artifacts keep their declared output role identity.""" +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from openhcs.core.materialization.options import NameByArtifactName, NameBySourceIdentity from dataclasses import replace from pathlib import Path @@ -22,7 +23,7 @@ from openhcs.core.compiled_step_plan import RuntimeArtifactMaterializationPlan from openhcs.core.pipeline.artifact_planning import ArtifactOutputMaterializationPlanner from openhcs.core.runtime_artifact_values import RuntimeValue -from openhcs.core.runtime_image_values import ImageMetadataPayload, ImagePayloadMetadata, ImagePayloadMetadataCompositionMode +from openhcs.core.runtime_image_values import ImageMetadataPayload, ImagePayloadMetadata from openhcs.core.runtime_stores import RuntimeValueStore from openhcs.core.source_metadata import SourceVoxelSpacing from openhcs.core.source_projection import SourceProjectionMetadataSerializer @@ -95,7 +96,7 @@ def test_automatic_two_role_images_publish_their_actual_saved_occurrences(tmp_pa ) value = ( payloads[0] if plane_count == 1 else ImagePayloadBundleContext.from_payloads( - payloads, metadata_mode=ImagePayloadMetadataCompositionMode.STACK, + payloads, metadata_mode=RuntimeSlicePlaneAxis, ).compose() ) context.runtime_value_store.record( @@ -187,7 +188,7 @@ def test_retained_role_policy_preserves_authored_and_unnamed_image_paths(tmp_pat for index in (1, 2) ) value = ImagePayloadBundleContext.from_payloads( - payloads, metadata_mode=ImagePayloadMetadataCompositionMode.STACK, + payloads, metadata_mode=RuntimeSlicePlaneAxis, ).compose() filemanager = FileManager({Backend.MEMORY.value: MemoryStorageBackend()}) context = context_stub(filemanager, parser=SourceSchemaFilenameParser()) @@ -222,7 +223,7 @@ def test_authored_duplicate_template_still_fails_before_any_save(tmp_path): source_component_metadata={"well": "A01", "site": "1", "channel": "2", "z_index": str(index), "timepoint": "1", "extension": ".tif"}, )) for index in (1, 2) ) - value = ImagePayloadBundleContext.from_payloads(payloads, metadata_mode=ImagePayloadMetadataCompositionMode.STACK).compose() + value = ImagePayloadBundleContext.from_payloads(payloads, metadata_mode=RuntimeSlicePlaneAxis).compose() manager = FileManager({Backend.MEMORY.value: MemoryStorageBackend()}) with pytest.raises(ValueError, match="produced duplicate paths"): materialization_outputs(spec, value, str(tmp_path / "role"), manager, @@ -252,7 +253,7 @@ def test_retained_image_qualifier_respects_complete_writer_suffix(tmp_path, suff )) for index in range(1, plane_count + 1) ) value = payloads[0] if plane_count == 1 else ImagePayloadBundleContext.from_payloads( - payloads, metadata_mode=ImagePayloadMetadataCompositionMode.STACK, + payloads, metadata_mode=RuntimeSlicePlaneAxis, ).compose() store = RuntimeValueStore() record = store.record(RuntimeValue.normalize(output_plan, value, axis_id="A01"), path=output_plan.path, backend=Backend.MEMORY.value) diff --git a/tests/unit/test_axis_family_witness.py b/tests/unit/test_axis_family_witness.py index ad13de24d3..ac36a446f9 100644 --- a/tests/unit/test_axis_family_witness.py +++ b/tests/unit/test_axis_family_witness.py @@ -11,6 +11,7 @@ """ from __future__ import annotations +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis import json import os @@ -468,19 +469,17 @@ def test_time_series_stack_slices_and_projects_without_kernel_edits(telemetry) - from openhcs.core.payload_axes import FamilyAxisSpec, RuntimePlaneAxisSpec from openhcs.core.runtime_image_values import ( ImagePayloadMetadata, - ImagePayloadMetadataCompositionMode, MaskedImagePayload, ) - from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.runtime_slice_projection import RuntimeSliceProjection signals = (_signal(0.0), _signal(100.0)) stack = stack_image_payloads( - signals, metadata_mode=ImagePayloadMetadataCompositionMode.STACK + signals, metadata_mode=RuntimeSlicePlaneAxis ) assert stack.geometry.shape == (2, 3, 4) assert stack.axes == ( - RuntimePlaneAxisSpec(RuntimePlaneAxis.RUNTIME_SLICE.value), + RuntimePlaneAxisSpec(RuntimeSlicePlaneAxis.name), FamilyAxisSpec(Telemetry.Sensor), FamilyAxisSpec(Telemetry.Time), ) @@ -491,7 +490,7 @@ def test_time_series_stack_slices_and_projects_without_kernel_edits(telemetry) - for original, projected in zip(signals, slices, strict=True): np.testing.assert_array_equal(np.asarray(projected.data), original.data) assert projected.axes == original.axes - assert projected.metadata.plane_axis is None + assert projected.metadata.plane_axis.unstacked restored = ImagePayloadMetadata.from_mapping( {"axes": stack.metadata.axes.to_mapping(), "source_dtype": "float32"} diff --git a/tests/unit/test_calibrated_gaussian_spatial_axes.py b/tests/unit/test_calibrated_gaussian_spatial_axes.py index 77eeaa6af4..2b88fda035 100644 --- a/tests/unit/test_calibrated_gaussian_spatial_axes.py +++ b/tests/unit/test_calibrated_gaussian_spatial_axes.py @@ -1,5 +1,7 @@ """Gaussian consumes declared physical axes, not assembled payload rank.""" +from openhcs.core.runtime_plane_projection import NoPlaneAxis +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from dataclasses import replace import numpy as np @@ -11,7 +13,6 @@ ImagePayloadAxisFields, ImagePayloadMetadata, ) -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.source_metadata import SourceVoxelSpacing from openhcs.core.source_spatial_domain import SourceSpatialDomain, VolumeSourceSpatialDomain from openhcs.interop.cellprofiler.runtime.adapter import CellProfilerRuntimeAdapter @@ -41,7 +42,7 @@ def _source(pixels, *, domain=None, spacing=(0.5, 0.75), channel_axis=None, coho source_voxel_spacing=SourceVoxelSpacing(values_zyx=spacing), source_spatial_domain=SourceSpatialDomain() if domain is None else domain, axes=PayloadAxes.colour_samples(channel_axis), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE if cohort else None, + plane_axis=RuntimeSlicePlaneAxis if cohort else NoPlaneAxis, ) return metadata.payload_with(pixels, np.ones(pixels.shape, dtype=bool)) @@ -156,7 +157,7 @@ def test_axis_capability_is_inherited_without_metadata_overrides(): assert getattr(ImagePayloadMetadata, name) is getattr(ImagePayloadAxisFields, name) metadata = ImagePayloadMetadata() assert metadata.axis_position(ColourAxis) is None - assert metadata.plane_axis is None + assert metadata.plane_axis.unstacked assert metadata.undeclared_axes(np.zeros((3, 7, 9))) == (0, 1, 2) @@ -190,7 +191,7 @@ def test_original_cohort_composition_keeps_every_source_plane(physical_volume): for index in range(3) ) metadata = ImagePayloadMetadata.compose(planes).replace_fields( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) if physical_volume: metadata = metadata.replace_fields( diff --git a/tests/unit/test_callable_abi_preparation.py b/tests/unit/test_callable_abi_preparation.py index 817090f8b0..32b05ae80b 100644 --- a/tests/unit/test_callable_abi_preparation.py +++ b/tests/unit/test_callable_abi_preparation.py @@ -1,5 +1,6 @@ """Prepared canonical signatures survive compilation and worker transport.""" +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis import inspect import pickle from types import SimpleNamespace @@ -24,7 +25,6 @@ from openhcs.core.function_reference import ImportableFunctionReference from openhcs.core.processing_preparation import CallablePreparation, PreparationCacheBatch from openhcs.core.runtime_image_values import ImagePayloadMetadata -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.pipeline.function_contracts import composed_image_payload from openhcs.processing.backends.lib_registry.registry_service import RegistryService from openhcs.core.runtime_batch_contracts import ( @@ -71,7 +71,7 @@ def raw(image): mask = np.ones((1, 4, 5), dtype=bool) mask[0, 0, 0] = False source = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_image_names=("FITC",), ).payload_with(pixels, mask) expected = source if retains_payload else pixels @@ -80,7 +80,7 @@ def raw(image): assert result is expected assert received[0] is expected - assert source.metadata.plane_axis is RuntimePlaneAxis.SOURCE_BINDING + assert source.metadata.plane_axis is SourceBindingPlaneAxis np.testing.assert_array_equal(source.mask, mask) @@ -99,7 +99,7 @@ def forbidden(*args, **kwargs): monkeypatch.setattr(contract_module, "get_type_hints", forbidden) monkeypatch.setattr(inspect, "signature", forbidden) payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_image_names=("FITC",), ).payload_with(np.ones((1, 4, 5)), np.ones((1, 4, 5), dtype=bool)) for _ in range(40): @@ -453,7 +453,7 @@ def test_compiled_input_reuses_produced_stack_realization_without_copy(): source = ProducedImageStack( tuple(np.full((3, 4), value, dtype=np.float32) for value in (1, 2)), - memory_type="numpy", plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + memory_type="numpy", plane_axis=RuntimeSlicePlaneAxis, ) invocation = compile_function_pattern( admitted_graph(gaussian_blur), diff --git a/tests/unit/test_cellprofiler_additional_prepared_payload_abi.py b/tests/unit/test_cellprofiler_additional_prepared_payload_abi.py index 2457eba342..d670d9b214 100644 --- a/tests/unit/test_cellprofiler_additional_prepared_payload_abi.py +++ b/tests/unit/test_cellprofiler_additional_prepared_payload_abi.py @@ -1,4 +1,5 @@ """Prepared raw arguments retain context consumed by nominal CP request owners.""" +from openhcs.core.runtime_plane_projection import SourceBindingPlaneAxis from dataclasses import replace import numpy as np @@ -6,7 +7,6 @@ from openhcs.core.callable_contract import CallableContract from openhcs.core.runtime_image_values import ImagePayloadMetadata -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.source_image_provenance import SourceImageProvenancePlanes from openhcs.core.source_spatial_domain import SourceSpatialDomain from openhcs.interop.cellprofiler.runtime.adapter import CellProfilerRuntimeAdapter @@ -146,7 +146,7 @@ def test_prepared_align_preserves_named_plane_axis_masks_and_source_identity(col paths=("/input/first.tif", "/input/second.tif"), component_metadata=({"well": "A01", "channel": "1"}, {"well": "A01", "channel": "2"}), ), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, axes=PayloadAxes.colour_samples(3 if color else None), source_spatial_domain=SourceSpatialDomain( origin_yx=(2, 3), source_shape_yx=(14, 17) diff --git a/tests/unit/test_cellprofiler_backend_leaf_wrapper_deletion.py b/tests/unit/test_cellprofiler_backend_leaf_wrapper_deletion.py index 2ddf7c7148..9615bf50bf 100644 --- a/tests/unit/test_cellprofiler_backend_leaf_wrapper_deletion.py +++ b/tests/unit/test_cellprofiler_backend_leaf_wrapper_deletion.py @@ -1,11 +1,11 @@ """Behavior gates retained after backend-leaf wrapper deletion.""" from __future__ import annotations +from openhcs.core.runtime_plane_projection import SourceBindingPlaneAxis import numpy as np import pytest -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.runtime_image_values import ( ImagePayloadMetadata, ) @@ -43,7 +43,7 @@ def test_align_execution_retains_input_and_additional_mode_invariants( ) ) ), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, ).payload_with(image, None), method="Mutual Information", crop_mode="Keep size", diff --git a/tests/unit/test_cellprofiler_extended_relationship_topology.py b/tests/unit/test_cellprofiler_extended_relationship_topology.py index 9adbe66522..2aa6362945 100644 --- a/tests/unit/test_cellprofiler_extended_relationship_topology.py +++ b/tests/unit/test_cellprofiler_extended_relationship_topology.py @@ -1,4 +1,6 @@ from __future__ import annotations +from openhcs.core.runtime_object_label_domains import PlaneLabelScope +from openhcs.core.runtime_plane_projection import SourceBindingPlaneAxis from inspect import unwrap from types import SimpleNamespace @@ -28,11 +30,9 @@ ) from openhcs.core.runtime_relationships import ObjectRelationshipDeclaration from openhcs.core.runtime_object_label_domains import ( - ObjectLabelDomainScope, PresentObjectLabelIdsDomainDeclaration, ) from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisValueProjection, ) from openhcs.interop.cellprofiler.parser import CPPipeParser, ModuleBlock, ModuleSetting @@ -434,14 +434,14 @@ def test_track_objects_unsupported_methods_fail_before_contract_emission( def _timepoint_labels(labels: np.ndarray) -> ObjectLabelPayload: projection = RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.SOURCE_BINDING, + axis=SourceBindingPlaneAxis, axis_size=labels.shape[0], ) return ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, domain=PresentObjectLabelIdsDomainDeclaration( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, plane_projection=projection, ).declared_domain(None, labels), ) diff --git a/tests/unit/test_cellprofiler_full_stack_materialization.py b/tests/unit/test_cellprofiler_full_stack_materialization.py index 4114496fbb..c408e05f57 100644 --- a/tests/unit/test_cellprofiler_full_stack_materialization.py +++ b/tests/unit/test_cellprofiler_full_stack_materialization.py @@ -1,5 +1,6 @@ """Dense full-stack ABI must consume aligned source/artifact values correctly.""" +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis from dataclasses import replace import numpy as np @@ -18,7 +19,6 @@ from openhcs.core.runtime_object_labels import ObjectLabelPayload, ObjectLabelVariantData from openhcs.core.runtime_image_values import ImagePayloadMetadata from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisValueProjection, ) from openhcs.core.runtime_slice_alignment import RuntimeSliceAlignedValues @@ -47,7 +47,7 @@ def test_real_rescale_full_stack_materializes_composed_runtime_sources(): data = np.arange(24, dtype=np.float32).reshape(2, 3, 4) source = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE + plane_axis=RuntimeSlicePlaneAxis ).payload_with(data, None) composition = compose_aligned_image_payload( "synthetic source inputs", (source, source) @@ -72,7 +72,7 @@ def test_literal_volume_keeps_dense_contract_semantics_without_bundle_classifica mask = np.ones(pixels.shape, dtype=bool) mask[:, 0, 0] = False metadata = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_voxel_spacing=SourceVoxelSpacing( (1.25, 2.5, 3.0), SourceVoxelSpacingUnit.MICROMETERS, ), @@ -84,17 +84,17 @@ def test_literal_volume_keeps_dense_contract_semantics_without_bundle_classifica ) dense = metadata.payload_with(pixels, mask) projection = RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, axis_size=60, + axis=RuntimeSlicePlaneAxis, axis_size=60, ) literal = ImagePayloadSliceStack.from_output_slices( tuple(RuntimeSliceProjection.value_for_slice(dense, projection.selected_plane(index)) for index in range(60)), - memory_type="numpy", plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + memory_type="numpy", plane_axis=RuntimeSlicePlaneAxis, ) assert not isinstance(literal, AlignedImageStack) composition = compose_aligned_image_payload("literal volume", (literal,)) assert composition.execution_mode is NaturalExecution - assert composition.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert composition.plane_axis is RuntimeSlicePlaneAxis assert literal._composed_payload is None contract = CallableContract.from_callable(function) contract = replace(contract, artifact_outputs=(ArtifactSpec.output("Filled", ImageArtifactType),)) @@ -173,7 +173,7 @@ def test_aligned_materialization_retains_masks_calibration_and_exact_contributor paths = tuple(f"/synthetic/A01_s{site}_w{channel}.tif" for site in (1, 2)) expected_paths.extend(paths) metadata = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_names=(alias,), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=paths, @@ -199,7 +199,7 @@ def test_aligned_materialization_retains_masks_calibration_and_exact_contributor dense = RuntimeSliceProjection.full_stack_value(composed) assert dense.shape == (2, 2, 3, 4) metadata = dense.metadata - assert metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert metadata.plane_axis is RuntimeSlicePlaneAxis assert metadata.source_voxel_spacing == spacing assert metadata.source_spatial_shape_yx == (3, 4) assert set(metadata.source_provenance.represented_source_image_names) == { @@ -223,7 +223,7 @@ def test_aligned_materialization_retains_masks_calibration_and_exact_contributor projected = RuntimeSliceProjection.value_for_slice( dense, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=index, axis_size=2 + axis=RuntimeSlicePlaneAxis, plane_index=index, axis_size=2 ), ) np.testing.assert_array_equal( @@ -246,7 +246,7 @@ def test_named_output_bundle_materializes_its_own_source_binding_axis(): ), ) dense = RuntimeSliceProjection.full_stack_value(bundle) - assert dense.metadata.plane_axis is RuntimePlaneAxis.SOURCE_BINDING + assert dense.metadata.plane_axis is SourceBindingPlaneAxis assert dense.metadata.source_provenance.represented_source_image_names == ("red", "green") np.testing.assert_array_equal(dense.data, np.stack(planes)) assert all( @@ -260,7 +260,7 @@ def test_named_bundle_preserves_outer_aliases_with_real_inner_plane_provenance() payloads = [] for channel, alias in enumerate(("red", "green"), start=1): metadata = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_names=(alias,), source_voxel_spacing=spacing, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( @@ -277,7 +277,7 @@ def test_named_bundle_preserves_outer_aliases_with_real_inner_plane_provenance() ) dense = RuntimeSliceProjection.full_stack_value(bundle) metadata = dense.metadata - assert metadata.plane_axis is RuntimePlaneAxis.SOURCE_BINDING + assert metadata.plane_axis is SourceBindingPlaneAxis assert metadata.source_image_names == ("red", "green") assert metadata.source_provenance.source_plane_count == 2 assert metadata.source_voxel_spacing == spacing @@ -285,7 +285,7 @@ def test_named_bundle_preserves_outer_aliases_with_real_inner_plane_provenance() selected = RuntimeSliceProjection.value_for_slice( dense, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.SOURCE_BINDING, plane_index=index, axis_size=2 + axis=SourceBindingPlaneAxis, plane_index=index, axis_size=2 ), ) np.testing.assert_array_equal( @@ -328,7 +328,7 @@ def composition_payload_metadata(self, metadata): ) np.testing.assert_allclose(result.data, data / 23.0) metadata = result.metadata - assert metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert metadata.plane_axis is RuntimeSlicePlaneAxis assert metadata.source_voxel_spacing == spacing assert { identity.path diff --git a/tests/unit/test_cellprofiler_function_contract_execution.py b/tests/unit/test_cellprofiler_function_contract_execution.py index 45e843396b..69205f6707 100644 --- a/tests/unit/test_cellprofiler_function_contract_execution.py +++ b/tests/unit/test_cellprofiler_function_contract_execution.py @@ -1,6 +1,7 @@ """Focused tests for the CellProfiler mode/processing-contract boundary.""" from __future__ import annotations +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis from collections.abc import Callable, Sequence from dataclasses import dataclass, replace @@ -24,7 +25,6 @@ callable_request, ) from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisValueProjection, ) from openhcs.core.runtime_slice_alignment import RuntimeSliceAlignedValues @@ -115,7 +115,7 @@ def test_registered_morph_distance_projects_raw_abi_and_retains_source_context() pixels[:, 2:5, 2:5] = 1 mask = np.ones_like(pixels, dtype=bool) source = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_names=("SavedLabels",), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("/tmp/saved-labels.tif",), @@ -133,14 +133,14 @@ def test_registered_morph_distance_projects_raw_abi_and_retains_source_context() {"operation": MorphOperation.DISTANCE}, execution_mode=NaturalExecution, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, axis_size=1, + axis=RuntimeSlicePlaneAxis, axis_size=1, ), ) np.testing.assert_array_equal(result.data, expected[None]) np.testing.assert_array_equal(result.mask, mask) metadata = result.metadata - assert metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert metadata.plane_axis is RuntimeSlicePlaneAxis assert metadata.source_image_names == ("SavedLabels",) assert metadata.source_image_provenance_planes.paths == ("/tmp/saved-labels.tif",) assert metadata.source_image_provenance_planes.component_metadata == ( @@ -177,7 +177,7 @@ def payload_raw(image: ArrayPayload, *, labels: np.ndarray) -> ArrayPayload: ) selected_labels = np.full((3, 4), 5, dtype=np.float32) projection = RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, axis_size=2, + axis=RuntimeSlicePlaneAxis, axis_size=2, ) assert contract.runtime_main_flow_call_argument(source) is source @@ -205,7 +205,7 @@ def bad_shape(image: np.ndarray) -> np.ndarray: np.ones((3, 4)), np.ones((3, 4), dtype=bool), ) projection = RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, axis_size=1, + axis=RuntimeSlicePlaneAxis, axis_size=1, ) with pytest.raises(ValueError, match="[Mm]ask.*shape|shape.*[Mm]ask"): _execute_one_slice(contract, bad_shape, projection, source, {}, 0, 1) @@ -310,7 +310,7 @@ def inplace(image: np.ndarray) -> np.ndarray: pixels, None, ) projection = RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, axis_size=1, + axis=RuntimeSlicePlaneAxis, axis_size=1, ) result = _execute_one_slice(contract, inplace, projection, source, {}, 0, 1) @@ -332,10 +332,10 @@ def raw(image: np.ndarray) -> np.ndarray: artifact_outputs=(ArtifactSpec.output("Result", ImageArtifactType),), ) source = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, source_image_names=("DNA",), + plane_axis=RuntimeSlicePlaneAxis, source_image_names=("DNA",), ).payload_with(np.ones((12, 3, 4)), None) projection = RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, axis_size=12, + axis=RuntimeSlicePlaneAxis, axis_size=12, ) executor = CellProfilerFunctionContractExecutor() hints = [] @@ -391,10 +391,10 @@ def reject_premature_annotations(self): execution_mode=AlignedStackExecution, ) projection = RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, axis_size=2, + axis=RuntimeSlicePlaneAxis, axis_size=2, ) mismatched_image = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, ).payload_with(np.zeros((2, 3, 4)), None) with pytest.raises(RuntimeSliceProjectionDeclarationError, match="conflicts"): CellProfilerFunctionContractExecutor().execute( @@ -416,13 +416,13 @@ def raw(image: np.ndarray) -> np.ndarray: raw, Pure2DContract, artifact_outputs=(ArtifactSpec.output("Result", ImageArtifactType),), ) - source = ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE).payload_with( + source = ImagePayloadMetadata(plane_axis=RuntimeSlicePlaneAxis).payload_with( np.ones((2, 3, 4)), None, ) result = CellProfilerFunctionContractExecutor().execute( contract, raw, source, {}, execution_mode=NaturalExecution, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, axis_size=2, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), ) @@ -449,9 +449,9 @@ def raw(image: np.ndarray, scale: int = 2) -> np.ndarray: artifact_outputs=(ArtifactSpec.output("Result",ImageArtifactType),), ) source = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE,source_image_names=("DNA",), + plane_axis=RuntimeSlicePlaneAxis,source_image_names=("DNA",), ).payload_with(np.ones((12,3,4)),None) - projection = RuntimePlaneAxisValueProjection.preserve(axis=RuntimePlaneAxis.RUNTIME_SLICE,axis_size=12) + projection = RuntimePlaneAxisValueProjection.preserve(axis=RuntimeSlicePlaneAxis,axis_size=12) def forbidden(*args, **kwargs): raise AssertionError("Prepared execution introspected a live callable") monkeypatch.setattr(inspect,"signature",forbidden) @@ -480,8 +480,8 @@ def wrapper(*args, **kwargs): monkeypatch.setattr(CallablePreparation,"prepare",lambda self:None) contract = CallableContract.from_prepared_callable(wrapper) contract = replace(contract,processing_contract=Pure2DContract, artifact_outputs=(ArtifactSpec.output("Result",ImageArtifactType),)) - source = ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE,source_image_names=("DNA",)).payload_with(np.ones((12,3,4)),None) - projection = RuntimePlaneAxisValueProjection.preserve(axis=RuntimePlaneAxis.RUNTIME_SLICE,axis_size=12) + source = ImagePayloadMetadata(plane_axis=RuntimeSlicePlaneAxis,source_image_names=("DNA",)).payload_with(np.ones((12,3,4)),None) + projection = RuntimePlaneAxisValueProjection.preserve(axis=RuntimeSlicePlaneAxis,axis_size=12) def forbidden(*args, **kwargs): raise AssertionError("Prepared authored raw target queried a live signature") monkeypatch.setattr(inspect,"signature",forbidden) @@ -533,7 +533,7 @@ def reject_reconstruction(*args: object, **kwargs: object) -> CallableContract: monkeypatch.setattr(CallableContract, "resolve_raw_runtime_callable", resolve_once) monkeypatch.setattr(CallableContract, "from_callable", reject_reconstruction) image = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE + plane_axis=RuntimeSlicePlaneAxis ).payload_with(np.ones((3, 4, 5), dtype=np.float32), None) result = CellProfilerFunctionContractExecutor().execute( @@ -543,7 +543,7 @@ def reject_reconstruction(*args: object, **kwargs: object) -> CallableContract: {"scale": 4, "adapter_control": 17}, execution_mode=NaturalExecution, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=3, ), ) @@ -582,7 +582,7 @@ def decorated(*args: object, **kwargs: object) -> object: ) contract = _compiled_contract(decorated, FlexibleContract) image = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE + plane_axis=RuntimeSlicePlaneAxis ).payload_with(np.ones((3, 4, 5), dtype=np.float32), None) CellProfilerFunctionContractExecutor().execute( @@ -592,7 +592,7 @@ def decorated(*args: object, **kwargs: object) -> object: {}, execution_mode=NaturalExecution, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=3, ), ) @@ -637,7 +637,7 @@ def dispatch_probe(image: np.ndarray) -> np.ndarray: if image_mode is AlignedStackExecution else ( ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE + plane_axis=RuntimeSlicePlaneAxis ).payload_with(np.zeros((2, 2, 3), dtype=np.float32), None) if processing_contract is VolumetricToSliceContract else np.zeros((2, 3), dtype=np.float32) @@ -652,7 +652,7 @@ def dispatch_probe(image: np.ndarray) -> np.ndarray: ValueError, match=( "DispatchProbeModule.*dispatch_probe.*" - "pure_3d.*RuntimePlaneAxis.SOURCE_BINDING" + "pure_3d.*SourceBindingPlaneAxis" ), ): CellProfilerFunctionContractExecutor().execute( @@ -662,7 +662,7 @@ def dispatch_probe(image: np.ndarray) -> np.ndarray: {}, execution_mode=image_mode, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), ) @@ -677,7 +677,7 @@ def dispatch_probe(image: np.ndarray) -> np.ndarray: execution_mode=image_mode, plane_projection=( RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ) if image_mode is AlignedStackExecution @@ -702,7 +702,7 @@ def dispatch_probe(image: np.ndarray) -> np.ndarray: artifact_inputs=(image_spec,), ) source_stack = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("/tmp/site-1.tif", "/tmp/site-2.tif"), component_metadata=(({"site": "1"}), ({"site": "2"})), @@ -728,7 +728,7 @@ def dispatch_probe(image: np.ndarray) -> np.ndarray: {}, execution_mode=composition.execution_mode, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), ) @@ -748,7 +748,7 @@ def dispatch_probe(image: object) -> np.ndarray: spatial_domain = SourceSpatialDomain(source_shape_yx=(2, 3)) source_stack = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_spatial_domain=spatial_domain, source_image_names=("Raw", "Raw"), ).payload_with( @@ -783,7 +783,7 @@ def dispatch_probe(image: object) -> np.ndarray: artifact_outputs=(output_spec,), ) projection = RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, source_aliases=("Raw", "Illum"), ) @@ -823,7 +823,7 @@ def dispatch_probe(image: np.ndarray) -> np.ndarray: return image[0] pair = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_image_names=("Orig", "Illum"), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("/tmp/orig.tif", "/tmp/illum.pkl"), @@ -841,7 +841,7 @@ def dispatch_probe(image: np.ndarray) -> np.ndarray: {}, execution_mode=AlignedStackExecution, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), ) @@ -873,7 +873,7 @@ def dispatch_probe(image: np.ndarray) -> np.ndarray: {}, execution_mode=AlignedStackExecution, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), ) @@ -923,7 +923,7 @@ def dispatch_probe(image: np.ndarray) -> np.ndarray: {}, execution_mode=AlignedStackExecution, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=3, ), ) @@ -947,7 +947,7 @@ def dispatch_probe( Pure2DContract, ) preserved = RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ) @@ -990,7 +990,7 @@ def dispatch_probe( outputs = np.stack((np.asarray(image) + 1, np.asarray(image) + 2)) return ( ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE + plane_axis=RuntimeSlicePlaneAxis ).payload_with(outputs, None), trailing, ) @@ -1013,7 +1013,7 @@ def dispatch_probe( {}, execution_mode=execution_mode, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), ) @@ -1054,7 +1054,7 @@ def dispatch_probe(image: np.ndarray) -> np.ndarray: {}, execution_mode=FullStackExecution, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), ) @@ -1066,7 +1066,7 @@ def dispatch_probe(image: np.ndarray) -> object: tuple(np.asarray(image) + output_index for output_index in range(3)) ) return ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE + plane_axis=RuntimeSlicePlaneAxis ).payload_with(outputs, None) callable_contract = _compiled_contract( @@ -1089,7 +1089,7 @@ def dispatch_probe(image: np.ndarray) -> object: {}, execution_mode=FullStackExecution, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=3, ), ) @@ -1140,7 +1140,7 @@ def dispatch_probe( {}, execution_mode=AlignedStackExecution, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=runtime_slice_count, ), ) @@ -1179,7 +1179,7 @@ def dispatch_probe(image: np.ndarray) -> np.ndarray: {}, execution_mode=AlignedStackExecution, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), ) @@ -1209,7 +1209,7 @@ def dispatch_probe(image: np.ndarray) -> np.ndarray: {}, execution_mode=AlignedStackExecution, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=1, ), ) @@ -1242,7 +1242,7 @@ def dispatch_probe(image: np.ndarray) -> AlignedImageStack: {}, execution_mode=AlignedStackExecution, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), ) @@ -1278,7 +1278,7 @@ def dispatch_probe(image: np.ndarray) -> AlignedImageStack: {}, execution_mode=AlignedStackExecution, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=1, ), ) diff --git a/tests/unit/test_cellprofiler_geometry_prepared_payload_abi.py b/tests/unit/test_cellprofiler_geometry_prepared_payload_abi.py index d13cb5c52a..423e0c69ea 100644 --- a/tests/unit/test_cellprofiler_geometry_prepared_payload_abi.py +++ b/tests/unit/test_cellprofiler_geometry_prepared_payload_abi.py @@ -1,5 +1,7 @@ """Geometry declarations preserve the payload their raw implementations consume.""" +from openhcs.core.runtime_plane_projection import NoPlaneAxis +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from dataclasses import replace from typing import get_type_hints @@ -9,7 +11,6 @@ from openhcs.core.callable_contract import CallableContract from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.runtime_image_values import ImagePayloadMetadata -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.source_spatial_domain import ( SourceSpatialDomain, VolumeSourceSpatialDomain, @@ -48,7 +49,7 @@ def test_prepared_resize_preserves_typed_primary_and_bare_array(func): assert contract.raw_main_flow_call_argument(pixels).data is pixels -@pytest.mark.parametrize("axis", (None, RuntimePlaneAxis.RUNTIME_SLICE)) +@pytest.mark.parametrize("axis", (NoPlaneAxis, RuntimeSlicePlaneAxis)) @pytest.mark.parametrize("z_factor", (1.0, 2.0)) @pytest.mark.parametrize( "domain_type", (SourceSpatialDomain, VolumeSourceSpatialDomain) diff --git a/tests/unit/test_cellprofiler_image_geometry_cardinality.py b/tests/unit/test_cellprofiler_image_geometry_cardinality.py index 83094de334..3e80c04d83 100644 --- a/tests/unit/test_cellprofiler_image_geometry_cardinality.py +++ b/tests/unit/test_cellprofiler_image_geometry_cardinality.py @@ -1,12 +1,12 @@ +from openhcs.core.runtime_object_label_domains import PlaneLabelScope +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis import numpy as np import pytest from openhcs.core.aligned_image_payload import AlignedImageStack from openhcs.core.runtime_object_label_domains import ( ObjectLabelDomain, - ObjectLabelDomainScope, ) -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.runtime_object_labels import ( ObjectLabelVariantData, ObjectLabelSet, @@ -37,10 +37,10 @@ def test_object_label_runtime_axis_owns_exact_mask_plane_selection() -> None: objects = ObjectLabelSet( name="Nuclei", variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) diff --git a/tests/unit/test_cellprofiler_image_math_operand_axes.py b/tests/unit/test_cellprofiler_image_math_operand_axes.py index b60436cafb..8c65e29185 100644 --- a/tests/unit/test_cellprofiler_image_math_operand_axes.py +++ b/tests/unit/test_cellprofiler_image_math_operand_axes.py @@ -1,7 +1,7 @@ +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis import numpy as np from openhcs.core.aligned_image_payload import ImagePayloadBundleContext -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.runtime_image_values import ImagePayloadMetadata from openhcs.processing.backends.cellprofiler.image_math import ( ImageMathOperation, @@ -25,12 +25,12 @@ def test_image_math_splits_only_declared_source_binding_operand_axis() -> None: ) np.testing.assert_allclose(result.data, 0.5) - assert result.metadata.plane_axis is None + assert result.metadata.plane_axis.unstacked def test_image_math_preserves_declared_runtime_slice_as_one_operand() -> None: image = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with( np.array( ( @@ -46,7 +46,7 @@ def test_image_math_preserves_declared_runtime_slice_as_one_operand() -> None: np.testing.assert_allclose( result.data, 1.0 - image.data ) - assert result.metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert result.metadata.plane_axis is RuntimeSlicePlaneAxis def test_image_math_does_not_infer_operand_axis_from_array_rank() -> None: diff --git a/tests/unit/test_cellprofiler_image_output_metadata.py b/tests/unit/test_cellprofiler_image_output_metadata.py index e8754434b2..3195b7fc90 100644 --- a/tests/unit/test_cellprofiler_image_output_metadata.py +++ b/tests/unit/test_cellprofiler_image_output_metadata.py @@ -1,4 +1,5 @@ from __future__ import annotations +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis import inspect @@ -63,7 +64,7 @@ def _source_binding_payload( {"channel": str(index)} for index in range(plane_count) ), ), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, ).payload_with(data, None) @@ -97,7 +98,7 @@ def test_colocalization_saved_mask_projects_its_declared_source_plane() -> None: metadata = output.metadata assert output.data.shape == rna.shape - assert metadata.plane_axis is None + assert metadata.plane_axis.unstacked assert metadata.axis_position(ColourAxis) is None assert metadata.source_image_paths == ("/input/rna.tif",) @@ -120,7 +121,7 @@ def test_image_math_collapses_all_source_binding_contributors() -> None: ) metadata = output.metadata - assert metadata.plane_axis is None + assert metadata.plane_axis.unstacked assert metadata.source_path == "/input/shared.tif" assert len(metadata.source_provenance.represented_source_identities) == 2 @@ -144,7 +145,7 @@ def test_all_image_illumination_collapses_all_source_plane_contributors() -> Non metadata = output.metadata assert output.data.shape == (4, 5) - assert metadata.plane_axis is None + assert metadata.plane_axis.unstacked assert metadata.source_path == "/input/shared.tif" assert len(metadata.source_provenance.represented_source_identities) == 2 @@ -170,7 +171,7 @@ def test_skeleton_branchpoint_image_declares_rgb_channel_axis() -> None: metadata = branchpoints.metadata assert branchpoints.data.shape == (9, 9, 3) assert metadata.axis_position(ColourAxis) == -1 - assert metadata.plane_axis is None + assert metadata.plane_axis.unstacked def test_worm_outline_images_declare_rgb_and_scalar_channel_semantics() -> None: @@ -200,10 +201,10 @@ def test_worm_outline_images_declare_rgb_and_scalar_channel_semantics() -> None: @pytest.mark.parametrize( "plane_axis", - (RuntimePlaneAxis.SOURCE_BINDING, RuntimePlaneAxis.RUNTIME_SLICE), + (SourceBindingPlaneAxis, RuntimeSlicePlaneAxis), ) def test_straighten_worms_projects_sources_into_warped_spatial_domain( - plane_axis: RuntimePlaneAxis, + plane_axis: type[RuntimePlaneAxis], ) -> None: source_payloads = tuple( ImagePayloadMetadata( @@ -218,9 +219,9 @@ def test_straighten_worms_projects_sources_into_warped_spatial_domain( ) image = ( ImagePayloadBundleContext.from_payloads(source_payloads).compose() - if plane_axis is RuntimePlaneAxis.SOURCE_BINDING + if plane_axis is SourceBindingPlaneAxis else ImagePayloadMetadata.compose(source_payloads) - .replace_fields(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE) + .replace_fields(plane_axis=RuntimeSlicePlaneAxis) .payload_with( np.stack(tuple(payload.data for payload in source_payloads)), None, @@ -242,7 +243,7 @@ def test_straighten_worms_projects_sources_into_warped_spatial_domain( assert len(output.slices) == 2 for index, warped in enumerate(output.slices, start=1): metadata = warped.metadata - assert metadata.plane_axis is None + assert metadata.plane_axis.unstacked assert metadata.source_image_paths == (f"/input/channel_{index}.tif",) assert metadata.source_spatial_domain.origin_yx == (0, 0) assert metadata.source_spatial_domain.source_shape_yx == ( diff --git a/tests/unit/test_cellprofiler_image_quality.py b/tests/unit/test_cellprofiler_image_quality.py index 3db1fd429b..a6789a33d8 100644 --- a/tests/unit/test_cellprofiler_image_quality.py +++ b/tests/unit/test_cellprofiler_image_quality.py @@ -1,4 +1,5 @@ from __future__ import annotations +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis from dataclasses import replace from types import SimpleNamespace @@ -39,7 +40,6 @@ MeasurementSubject, ) from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisValueProjection, ) from openhcs.core.runtime_tabular_values import ColumnarRows @@ -310,7 +310,7 @@ def test_measure_image_quality_pure_2d_contract_slices_3d_site_batch() -> None: ) ) image = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(planes, None) contract = _compiled_image_quality_contract() @@ -327,7 +327,7 @@ def test_measure_image_quality_pure_2d_contract_slices_3d_site_batch() -> None: }, execution_mode=NaturalExecution, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), ) @@ -344,7 +344,7 @@ def test_measure_image_quality_projects_named_source_axis_from_runtime_metadata( source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=tuple(f"/plate/A01_{name}.tif" for name in source_names), ), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, ) image = source_metadata.payload_with( np.stack( @@ -355,7 +355,7 @@ def test_measure_image_quality_projects_named_source_axis_from_runtime_metadata( ) ) projection = RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.SOURCE_BINDING, + axis=SourceBindingPlaneAxis, axis_size=len(source_names), source_aliases=source_names, ) @@ -414,7 +414,7 @@ def test_measure_image_quality_table_uses_row_qualified_source_inputs() -> None: source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=tuple(f"/plate/A01_{name}.tif" for name in source_names), ), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, ) image = source_metadata.payload_with( np.stack( @@ -425,7 +425,7 @@ def test_measure_image_quality_table_uses_row_qualified_source_inputs() -> None: ) ) projection = RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.SOURCE_BINDING, + axis=SourceBindingPlaneAxis, axis_size=len(source_names), source_aliases=source_names, ) diff --git a/tests/unit/test_cellprofiler_library_loading.py b/tests/unit/test_cellprofiler_library_loading.py index 19af8406fa..a8c468d557 100644 --- a/tests/unit/test_cellprofiler_library_loading.py +++ b/tests/unit/test_cellprofiler_library_loading.py @@ -1,3 +1,5 @@ +from openhcs.core.runtime_object_label_domains import PayloadLabelScope, PlaneLabelScope +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis import importlib from openhcs.interop.cellprofiler.object_label_variants import ( SmallRemovedLabels, @@ -160,10 +162,8 @@ ) from openhcs.core.runtime_object_label_domains import ( ObjectLabelDomain, - ObjectLabelDomainScope, ) from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisValueProjection, ) from openhcs.core.source_image_provenance import SourceImageProvenancePlanes @@ -703,7 +703,7 @@ def test_overlay_objects_preserves_payload_scoped_volume() -> None: label_data[1, 1:3, 2:4] = 1 labels = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=label_data), - domain=ObjectLabelDomain(scope=ObjectLabelDomainScope.PAYLOAD), + domain=ObjectLabelDomain(scope=PayloadLabelScope), ) raw_overlay_objects = overlay_objects while hasattr(raw_overlay_objects, "__wrapped__"): @@ -1317,7 +1317,7 @@ def test_runtime_batch_projects_singleton_aligned_axis_before_colocalization() - }, execution_mode=AlignedStackExecution, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=1, ), batch_index=0, @@ -1451,7 +1451,7 @@ def test_colocalization_batches_preserve_step_thresholds_and_live_source_epoch( payload = MaskedImagePayload( data=image, mask=mask, - metadata=ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.SOURCE_BINDING), + metadata=ImagePayloadMetadata(plane_axis=SourceBindingPlaneAxis), ) label_sets = tuple( ObjectLabelPayload( @@ -1761,7 +1761,7 @@ def test_measure_colocalization_respects_masked_payload_pixels(): data=image, mask=np.stack((mask, mask)), metadata=ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, ), ), do_costes=False, @@ -2686,9 +2686,9 @@ def test_measure_texture_objects_rejects_unprojected_plane_domain_stack(): ) ), domain=ObjectLabelDomain( - declared_object_id_domains=((1,), (2,)), scope=ObjectLabelDomainScope.PLANE + declared_object_id_domains=((1,), (2,)), scope=PlaneLabelScope ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) with pytest.raises(ValueError, match="runtime-projected 2-D"): measure_texture_objects(image, labels, scale=1, dtype_config=DtypeConfig()) @@ -2748,16 +2748,16 @@ def test_measure_texture_objects_preserves_runtime_projected_label_domain(): ), domain=ObjectLabelDomain( declared_object_id_domains=((1, 2), (3, 4)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) results = [] for slice_index in range(2): projected = RuntimeSliceProjection.value_for_slice( labels, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, plane_index=slice_index, axis_size=2, ), @@ -3618,7 +3618,7 @@ def test_correct_illumination_apply_projects_runtime_slice_artifact_stack() -> N np.full((2, 2), 0.6, dtype=np.float32), ) ), - metadata=ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE), + metadata=ImagePayloadMetadata(plane_axis=RuntimeSlicePlaneAxis), ) illumination = ImageMetadataPayload( data=np.stack( @@ -3627,7 +3627,7 @@ def test_correct_illumination_apply_projects_runtime_slice_artifact_stack() -> N np.full((2, 2), 0.3, dtype=np.float32), ) ), - metadata=ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE), + metadata=ImagePayloadMetadata(plane_axis=RuntimeSlicePlaneAxis), ) result = correct_illumination_apply( image, @@ -3660,7 +3660,7 @@ def test_measure_colocalization_uses_payload_metadata_for_costes_scale() -> None mask=np.ones(image.shape, dtype=bool), metadata=ImagePayloadMetadata( source_plane_intensity_scales=(65535.0, 65535.0), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, ), ) _, metadata_measurements = measure_colocalization( @@ -4325,7 +4325,7 @@ def test_pure_2d_contract_wrapper_aggregates_illumination_outputs_per_slice(): ImageMetadataPayload( data=image, metadata=ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ), ), dtype_config=DtypeConfig(), @@ -4661,7 +4661,7 @@ def test_mask_image_uses_aligned_mask_stack_planes(): RuntimeSliceProjection.value_for_slice( image, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.SOURCE_BINDING, + axis=SourceBindingPlaneAxis, plane_index=index, axis_size=2, ), @@ -4669,7 +4669,7 @@ def test_mask_image_uses_aligned_mask_stack_planes(): RuntimeSliceProjection.value_for_slice( mask, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.SOURCE_BINDING, + axis=SourceBindingPlaneAxis, plane_index=index, axis_size=2, ), @@ -4692,9 +4692,9 @@ def test_mask_image_rejects_unprojected_object_label_stack(): variant_data=ObjectLabelVariantData(labels=mask), domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) with pytest.raises(ValueError, match="cardinalities must exactly match"): mask_image( @@ -4714,9 +4714,9 @@ def test_relate_objects_rejects_mixed_object_label_domain_scopes(): ), domain=ObjectLabelDomain( declared_object_id_domains=((1, 2), (1, 2)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) child_payload = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=child_plane) @@ -4753,7 +4753,7 @@ def test_relate_objects_preserves_child_object_label_domain_metadata(): ) assert isinstance(output, ObjectLabelPayload) assert output.domain.declared_object_count == 4 - assert output.domain.scope is ObjectLabelDomainScope.PAYLOAD + assert output.domain.scope.ids_span_payload np.testing.assert_array_equal(output.labels, child_plane) @@ -4766,9 +4766,9 @@ def test_relate_objects_uses_explicitly_projected_plane_identity(): ), domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) child_stack = ObjectLabelPayload( variant_data=ObjectLabelVariantData( @@ -4778,14 +4778,14 @@ def test_relate_objects_uses_explicitly_projected_plane_identity(): ), domain=ObjectLabelDomain( declared_object_id_domains=((1,), (1,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) relationships = [] for slice_index in range(2): axis = RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=slice_index, axis_size=2 + axis=RuntimeSlicePlaneAxis, plane_index=slice_index, axis_size=2 ) _output, relationship, _reverse_relationship, _measurements = ( relate_objects.__wrapped__( @@ -4861,7 +4861,7 @@ def test_align_returns_two_registered_images_and_shift_measurements(): paths=("/input/first.tif", "/input/second.tif") ) ), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, ).payload_with(image, None), crop_mode=AlignModule.CropMode.KEEP_SIZE, dtype_config=DtypeConfig(), @@ -4915,7 +4915,7 @@ def test_align_applies_similar_shift_to_additional_images(): ) ) ), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, ).payload_with(image, None), crop_mode=AlignModule.CropMode.KEEP_SIZE, additional_alignment_modes=(AlignModule.AdditionalMode.SIMILARLY,), @@ -5024,9 +5024,9 @@ def test_overlay_outlines_rejects_unprojected_object_label_stack(): variant_data=ObjectLabelVariantData(labels=labels), domain=ObjectLabelDomain( declared_object_id_domains=((1,), (1,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) with pytest.raises(ValueError, match="runtime-projected 2-D"): @@ -5046,14 +5046,14 @@ def test_overlay_outlines_renders_exact_projected_empty_label_plane(): variant_data=ObjectLabelVariantData(labels=labels), domain=ObjectLabelDomain( declared_object_id_domains=((), ()), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) projected = RuntimeSliceProjection.value_for_slice( payload, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=2, ), @@ -5089,7 +5089,7 @@ def test_measurement_batch_admission_uses_declared_processing_domain( kwargs={"slice_by_slice": slice_by_slice}, execution_mode=mode, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), batch_index=0, diff --git a/tests/unit/test_cellprofiler_library_untangleworms.py b/tests/unit/test_cellprofiler_library_untangleworms.py index f6e4856282..717836b660 100644 --- a/tests/unit/test_cellprofiler_library_untangleworms.py +++ b/tests/unit/test_cellprofiler_library_untangleworms.py @@ -1,4 +1,6 @@ from __future__ import annotations +from openhcs.core.runtime_object_label_domains import PlaneLabelScope +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from dataclasses import replace from unittest.mock import patch @@ -44,10 +46,8 @@ ) from openhcs.core.runtime_object_label_domains import ( ObjectLabelDomain, - ObjectLabelDomainScope, ) from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneProjection, ) from openhcs.core.runtime_artifact_values import RuntimeValue @@ -278,10 +278,10 @@ def test_straighten_worms_policy_consumes_declared_singleton_label_plane() -> No variant_data=ObjectLabelVariantData( labels=np.array([[[0, 1], [0, 0]]], dtype=np.int32) ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,),), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) request = _straighten_worms_binding_request(label_stack, plane_index=0) @@ -293,8 +293,8 @@ def test_straighten_worms_policy_consumes_declared_singleton_label_plane() -> No projected_labels.labels, np.array([[0, 1], [0, 0]], dtype=np.int32), ) - assert projected_labels.plane_axis is None - assert projected_labels.domain.scope is ObjectLabelDomainScope.PAYLOAD + assert projected_labels.plane_axis.unstacked + assert projected_labels.domain.scope.ids_span_payload def test_straighten_worms_callable_rejects_unselected_label_stack() -> None: @@ -308,10 +308,10 @@ def test_straighten_worms_callable_rejects_unselected_label_stack() -> None: dtype=np.int32, ) ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) request = _straighten_worms_binding_request( diff --git a/tests/unit/test_cellprofiler_measurement_rows.py b/tests/unit/test_cellprofiler_measurement_rows.py index 21ff0d08bc..1b6c3885c6 100644 --- a/tests/unit/test_cellprofiler_measurement_rows.py +++ b/tests/unit/test_cellprofiler_measurement_rows.py @@ -1,3 +1,4 @@ +from openhcs.core.runtime_plane_projection import SourceBindingPlaneAxis import ast import inspect import textwrap @@ -33,7 +34,6 @@ ObjectLabelVariantData, ) from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisValueProjection, ) from openhcs.core.source_image_provenance import SourceImageProvenancePlanes @@ -127,10 +127,10 @@ def test_measurement_source_name_projects_exact_runtime_metadata_plane() -> None source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=tuple(f"/plate/A01_{name}.tif" for name in source_names), ), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, ) projection = RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.SOURCE_BINDING, + axis=SourceBindingPlaneAxis, axis_size=len(source_names), source_aliases=source_names, ) @@ -150,10 +150,10 @@ def test_measurement_source_name_does_not_fall_back_to_projection_alias_order() source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=tuple(f"/plate/A01_{name}.tif" for name in source_aliases), ), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, ) projection = RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.SOURCE_BINDING, + axis=SourceBindingPlaneAxis, axis_size=len(source_aliases), source_aliases=source_aliases, ) diff --git a/tests/unit/test_cellprofiler_module_execution.py b/tests/unit/test_cellprofiler_module_execution.py index 6c58f8623d..38ca65af1b 100644 --- a/tests/unit/test_cellprofiler_module_execution.py +++ b/tests/unit/test_cellprofiler_module_execution.py @@ -1,3 +1,7 @@ +from openhcs.core.runtime_plane_projection import NoPlaneAxis +from openhcs.core.runtime_object_label_domains import PayloadLabelScope, PlaneLabelScope +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis +from openhcs.core.runtime_plane_projection import SourceBindingPlaneAxis from dataclasses import dataclass, replace import sqlite3 from tests.artifact_graphs import admitted_graph @@ -78,7 +82,6 @@ ImagePayloadSliceProjector, ImageMetadataPayload, ImagePayloadMetadata, - ImagePayloadMetadataCompositionMode, MaskedImagePayload, ) from openhcs.core.runtime_array_values import RuntimeArrayData @@ -111,7 +114,6 @@ ) from openhcs.core.runtime_object_label_domains import ( ObjectLabelDomain, - ObjectLabelDomainScope, ) from openhcs.core.runtime_object_labels import ( ObjectLabelRepresentation, @@ -641,7 +643,7 @@ def test_output_recording_preserves_complete_callable_return_abi() -> None: ) executor = _module_executor(contract) projection = RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ) first_value = ImagePayloadMetadata(source_image_names=("Corrected1",)).payload_with( @@ -1011,10 +1013,10 @@ def consume_guiding_labels(image, guiding_labels: ObjectLabelValue): objects = ObjectLabelSet( name="Guides", variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) guides_spec = ArtifactSpec.input( @@ -1048,7 +1050,7 @@ def consume_guiding_labels(image, guiding_labels: ObjectLabelValue): RuntimeSliceProjection.kwargs_for_slice( bound, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=1, axis_size=2 + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=2 ), )["guiding_labels"] ), @@ -1074,10 +1076,10 @@ def consume_guiding_labels(image, guiding_labels: ObjectLabelValue): objects = ObjectLabelSet( name="Guides", variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) guides_spec = ArtifactSpec.input( @@ -1111,7 +1113,7 @@ def consume_guiding_labels(image, guiding_labels: ObjectLabelValue): RuntimeSliceProjection.kwargs_for_slice( bound, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=0, axis_size=2 + axis=RuntimeSlicePlaneAxis, plane_index=0, axis_size=2 ), )["guiding_labels"] ), @@ -1122,7 +1124,7 @@ def consume_guiding_labels(image, guiding_labels: ObjectLabelValue): RuntimeSliceProjection.kwargs_for_slice( bound, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=1, axis_size=2 + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=2 ), )["guiding_labels"] ), @@ -1146,10 +1148,10 @@ def consume_guiding_labels(image, guiding_labels: ObjectLabelValue): objects = ObjectLabelSet( name="Guides", variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1, 2), (3, 4)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("/input/site-1.tif", "/input/site-2.tif"), @@ -1184,13 +1186,13 @@ def consume_guiding_labels(image, guiding_labels: ObjectLabelValue): first_slice = RuntimeSliceProjection.kwargs_for_slice( bound, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=0, axis_size=2 + axis=RuntimeSlicePlaneAxis, plane_index=0, axis_size=2 ), )["guiding_labels"] second_slice = RuntimeSliceProjection.kwargs_for_slice( bound, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=1, axis_size=2 + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=2 ), )["guiding_labels"] assert isinstance(first_slice, ObjectLabelValue) @@ -1263,10 +1265,10 @@ def test_track_objects_special_input_binds_full_stack_labels_for_pure_3d() -> No objects = ObjectLabelSet( name="Embryos", variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) embryos_spec = ArtifactSpec.input( @@ -1286,7 +1288,7 @@ def test_track_objects_special_input_binds_full_stack_labels_for_pure_3d() -> No embryos_spec, ), ), - plane_projection=RuntimePlaneProjection(plane_index=1, plane_count=2), + plane_projection=RuntimePlaneProjection.selected(1, 2), ), kwargs={}, current_image=np.zeros((2, 2, 2), dtype=np.float32), @@ -1447,7 +1449,7 @@ def test_main_flow_image_artifact_selects_declared_alias_plane() -> None: source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("stain1.tif", "stain2.tif"), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with( np.stack( ( @@ -1497,7 +1499,7 @@ def test_single_main_flow_image_artifact_selects_declared_source_binding_plane() source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("stain1.tif", "stain2.tif"), ), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, ).payload_with( np.stack( ( @@ -1542,7 +1544,7 @@ def test_main_flow_image_artifact_projects_named_provenance_plane() -> None: source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("stain1.tif", "stain2.tif"), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) metadata = metadata.with_source_provenance( metadata.source_provenance.with_derived_source_image_names( @@ -1593,7 +1595,7 @@ def test_main_flow_image_artifact_preserves_declared_runtime_slice_stack() -> No source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("stain1-1.tif", "stain1-2.tif"), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with( np.stack( ( @@ -1659,7 +1661,7 @@ def test_main_flow_projection_binds_singleton_broadcast_artifact_as_2d( paths=source_paths, component_metadata=source_metadata, ), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, ).payload_with(np.ones((2, 4, 5), dtype=np.float32), None) illumination = ImagePayloadMetadata( source_image_names=("IllumStain1",), @@ -1667,7 +1669,7 @@ def test_main_flow_projection_binds_singleton_broadcast_artifact_as_2d( paths=(source_paths[0],), component_metadata=(source_metadata[0],), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.ones((1, 4, 5), dtype=np.float32), None) runtime = _FakeCellProfilerRuntime( {illumination_spec.name: illumination}, @@ -1792,7 +1794,7 @@ def test_repeated_broadcast_inputs_consume_their_declared_source_group_axes() -> paths=source_paths, component_metadata=source_metadata, ), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, ).payload_with(np.ones((2, 4, 5), dtype=np.float32), None) illumination_payloads = tuple( ImagePayloadMetadata( @@ -1803,7 +1805,7 @@ def test_repeated_broadcast_inputs_consume_their_declared_source_group_axes() -> component_metadata=(source_metadata[index],), ) ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with( np.full((1, 4, 5), index + 1, dtype=np.float32), None, @@ -1868,7 +1870,7 @@ def test_single_main_flow_image_preserves_runtime_slice_axis() -> None: source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("crop-red.tif",), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.ones((1, 4, 5), dtype=np.float32), None) payload = CellProfilerOutputRecorder.for_artifact_type( @@ -1891,7 +1893,7 @@ def test_single_main_flow_image_preserves_runtime_slice_axis() -> None: ) assert payload.data.shape == (1, 4, 5) - assert payload.metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert payload.metadata.plane_axis is RuntimeSlicePlaneAxis assert payload.metadata.source_image_names == ("CropRed",) @@ -1925,10 +1927,10 @@ def test_object_input_does_not_replace_preserved_canonical_main_flow_image() -> variant_data=ObjectLabelVariantData( labels=np.asarray((((0, 1), (2, 0)),), dtype=np.int32) ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1, 2),), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) runtime = _FakeCellProfilerRuntime( @@ -1941,7 +1943,7 @@ def test_object_input_does_not_replace_preserved_canonical_main_flow_image() -> _activate_runtime_contract(executor.callable_contract, runtime) current_image = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.full((1, 2, 2), 7, dtype=np.float32)) image_request = executor._image_request( current_image, @@ -1951,7 +1953,7 @@ def test_object_input_does_not_replace_preserved_canonical_main_flow_image() -> ) assert image_request.plane_projection == RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=1, ) np.testing.assert_array_equal( @@ -1971,10 +1973,10 @@ def test_object_input_does_not_replace_selected_canonical_main_flow_plane() -> N variant_data=ObjectLabelVariantData( labels=np.asarray((((0, 1), (2, 0)),), dtype=np.int32) ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1, 2),), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) runtime = _FakeCellProfilerRuntime( @@ -1987,7 +1989,7 @@ def test_object_input_does_not_replace_selected_canonical_main_flow_plane() -> N _activate_runtime_contract(executor.callable_contract, runtime) current_image = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.full((1, 2, 2), 9, dtype=np.float32)) image_request = executor._image_request( current_image, @@ -2002,7 +2004,7 @@ def test_object_input_does_not_replace_selected_canonical_main_flow_plane() -> N image_request.payload.data, np.full((2, 2), 9, dtype=np.float32), ) - assert image_request.payload.metadata.plane_axis is None + assert image_request.payload.metadata.plane_axis.unstacked def test_singleton_object_measurement_collapses_singleton_plane_context() -> None: @@ -2016,10 +2018,10 @@ def test_singleton_object_measurement_collapses_singleton_plane_context() -> Non variant_data=ObjectLabelVariantData( labels=np.asarray((((0, 1), (2, 0)),), dtype=np.int32) ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1, 2),), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) runtime = _FakeCellProfilerRuntime( @@ -2109,10 +2111,10 @@ def test_special_object_label_payload_uses_runtime_plane_selection() -> None: objects = ObjectLabelSet( name="Nuclei", variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=( @@ -2135,7 +2137,7 @@ def test_special_object_label_payload_uses_runtime_plane_selection() -> None: callable_contract=contract, objects={spec.name: objects}, artifact_input_edges=(_artifact_input_edge_for_test(spec),), - plane_projection=RuntimePlaneProjection(plane_index=1, plane_count=2), + plane_projection=RuntimePlaneProjection.selected(1, 2), ) current_image = np.zeros((4, 4), dtype=np.float32) request = RuntimeInputBindingRequest( @@ -2166,10 +2168,10 @@ def test_measurement_object_label_preparation_projects_runtime_slice_payload() - objects = ObjectLabelSet( name="Objects", variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) measurement_image = CellProfilerMeasurementImage( @@ -2211,7 +2213,7 @@ def test_measure_object_neighbors_derives_same_object_topology_from_artifact_ide callable_contract=contract, objects={cells_spec.name: objects}, artifact_input_edges=(_artifact_input_edge_for_test(cells_spec),), - plane_projection=RuntimePlaneProjection(plane_index=0, plane_count=1), + plane_projection=RuntimePlaneProjection.selected(0, 1), ), kwargs={}, current_image=np.zeros((1, 1), dtype=np.float32), @@ -2233,10 +2235,10 @@ def test_relateobjects_special_inputs_project_object_labels_to_current_plane() - objects = ObjectLabelSet( name="Objects", variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) input_specs = ( @@ -2267,7 +2269,7 @@ def test_relateobjects_special_inputs_project_object_labels_to_current_plane() - for spec in input_specs ) ), - plane_projection=RuntimePlaneProjection(plane_index=1, plane_count=2), + plane_projection=RuntimePlaneProjection.selected(1, 2), ), kwargs={}, current_image=np.zeros((2, 2), dtype=np.float32), @@ -2292,10 +2294,10 @@ def test_relateobjects_special_inputs_leave_runtime_slice_index_to_executor() -> objects = ObjectLabelSet( name="Objects", variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) input_specs = ( @@ -2326,7 +2328,7 @@ def test_relateobjects_special_inputs_leave_runtime_slice_index_to_executor() -> for spec in input_specs ) ), - plane_projection=RuntimePlaneProjection(plane_index=1, plane_count=2), + plane_projection=RuntimePlaneProjection.selected(1, 2), ), kwargs={}, current_image=np.zeros((2, 2), dtype=np.float32), @@ -2357,11 +2359,11 @@ def source_binding_axis_plane_index( objects = ObjectLabelSet( name="Objects", variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_image_names=("Site1", "Site2"), domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) input_specs = ( @@ -2424,11 +2426,11 @@ def source_binding_axis_plane_index( objects = ObjectLabelSet( name="Objects", variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_names=("Stain1",), domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) input_specs = ( @@ -2459,7 +2461,7 @@ def source_binding_axis_plane_index( for spec in input_specs ) ), - plane_projection=RuntimePlaneProjection(plane_index=1, plane_count=2), + plane_projection=RuntimePlaneProjection.selected(1, 2), ), kwargs={}, current_image=np.zeros((2, 2), dtype=np.float32), @@ -2481,11 +2483,11 @@ def test_relateobjects_scalar_source_name_does_not_create_source_axis() -> None: objects = ObjectLabelSet( name="Objects", variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_name="OrigStain1__OrigStain2", domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) input_specs = ( @@ -2516,7 +2518,7 @@ def test_relateobjects_scalar_source_name_does_not_create_source_axis() -> None: for spec in input_specs ) ), - plane_projection=RuntimePlaneProjection(plane_index=1, plane_count=2), + plane_projection=RuntimePlaneProjection.selected(1, 2), ), kwargs={}, current_image=np.zeros((2, 2), dtype=np.float32), @@ -2542,10 +2544,10 @@ def test_track_objects_retained_image_uses_tracked_object_source_payload() -> No tracked_objects = ObjectLabelSet( name="Embryos", variant_data=ObjectLabelVariantData(labels=np.zeros((3, 5, 6), dtype=np.int32)), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((), (), ()), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("gray0.tif", "gray1.tif", "gray2.tif"), @@ -2681,22 +2683,22 @@ def test_contextual_object_label_output_applies_declared_payload_scope() -> None variant_data=ObjectLabelVariantData(labels=np.zeros((3, 4, 5), dtype=np.int32)), domain=ObjectLabelDomain( declared_object_id_domains=((), (), ()), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) contextual = labels.with_source_image_context(source_image) payload = contextual.with_variants( contextual.variant_data, domain=contextual.object_label_domain().with_scope( - ObjectLabelDomainScope.PAYLOAD + PayloadLabelScope ), - plane_axis=None, + plane_axis=NoPlaneAxis, ) assert isinstance(payload, ObjectLabelPayload) - assert payload.object_label_domain().scope is ObjectLabelDomainScope.PAYLOAD + assert payload.object_label_domain().scope.ids_span_payload assert payload.source_image_provenance_planes.paths == ( "/input/A01_s001_w1_z001_t001.TIF", "/input/A01_s001_w1_z002_t001.TIF", @@ -2716,9 +2718,9 @@ def test_object_label_output_source_preserves_matching_input_object_plane_contex variant_data=ObjectLabelVariantData(labels=np.zeros((2, 4, 5), dtype=np.int32)), domain=ObjectLabelDomain( declared_object_id_domains=((), ()), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=source_paths, component_metadata=({"site": "001"}, {"site": "002"}), @@ -2747,7 +2749,7 @@ def test_object_label_output_source_preserves_matching_input_object_plane_contex paths=source_paths, component_metadata=({"site": "001"}, {"site": "002"}), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.zeros((2, 4, 5), dtype=np.float32), None) output = _output_from_input("GridObjects", "Guides") request = _cellprofiler_output_record_request( @@ -3985,7 +3987,7 @@ def test_payload_scoped_object_measurement_completion_ignores_slice_axis() -> No variant_data=ObjectLabelVariantData(labels=labels), domain=ObjectLabelDomain( declared_object_count=5, - scope=ObjectLabelDomainScope.PAYLOAD, + scope=PayloadLabelScope, ), ) rows = MeasurementSparseColumnarRows.from_rows( @@ -4449,7 +4451,7 @@ def test_object_label_output_domain_scope_uses_declared_group_lineage() -> None: kwargs={}, ) - assert request.object_label_output_domain_scope() is ObjectLabelDomainScope.PAYLOAD + assert request.object_label_output_domain_scope().ids_span_payload class _CalculateMathObjectOperandAdapter(_FakeCellProfilerRuntime): @@ -4513,10 +4515,10 @@ def __init__(self, contract: CallableContract, labels: np.ndarray) -> None: "Nuclei": ObjectLabelSet( name="Nuclei", variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1, 2), (1, 2)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ), }, @@ -4789,7 +4791,7 @@ def test_runtime_slice_projection_projects_cellprofiler_slice_aligned_values() - RuntimeSliceProjection.value_for_slice( values, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=1, axis_size=2 + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=2 ), ), np.asarray([3.0]), @@ -4800,7 +4802,7 @@ def test_cellprofiler_measurement_vector_preserves_singleton_slice_alignment() - value = CellProfilerMeasurementVector( (np.asarray([1.0, 2.0]),), plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=1, ), ).runtime_value @@ -4822,7 +4824,7 @@ def test_cellprofiler_measurement_vector_consumes_selected_plane() -> None: value = CellProfilerMeasurementVector( (np.asarray([1.0, 2.0]),), plane_projection=RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, plane_index=0, axis_size=1, ), @@ -4843,10 +4845,10 @@ def test_single_object_input_policy_preserves_native_label_contract() -> None: dtype=np.int32, ) ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) object_spec = ArtifactSpec.input( @@ -4889,10 +4891,10 @@ def test_object_row_binding_projects_the_current_runtime_plane() -> None: dtype=np.int32, ) ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) object_spec = ArtifactSpec.input("Cells", ObjectLabelsArtifactType) @@ -4926,14 +4928,14 @@ def test_object_row_binding_projects_the_current_runtime_plane() -> None: pytest.param( RuntimePlaneProjection.stack(1), (1, 2, 2), - RuntimePlaneAxis.RUNTIME_SLICE, + RuntimeSlicePlaneAxis, True, id="preserved-singleton-domain", ), pytest.param( RuntimePlaneProjection.selected(0, 1), (2, 2), - None, + NoPlaneAxis, False, id="explicit-selection", ), @@ -4942,7 +4944,7 @@ def test_object_row_binding_projects_the_current_runtime_plane() -> None: def test_object_row_binding_honors_authoritative_singleton_runtime_projection( plane_projection: RuntimePlaneProjection, expected_shape: tuple[int, ...], - expected_plane_axis: RuntimePlaneAxis | None, + expected_plane_axis: type[RuntimePlaneAxis], preserved: bool, ) -> None: label_array = np.asarray( @@ -4952,10 +4954,10 @@ def test_object_row_binding_honors_authoritative_singleton_runtime_projection( labels = ObjectLabelSet( name="Cells", variant_data=ObjectLabelVariantData(labels=label_array), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,),), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) object_spec = ArtifactSpec.input("Cells", ObjectLabelsArtifactType) @@ -4988,7 +4990,7 @@ def test_object_row_binding_honors_authoritative_singleton_runtime_projection( if preserved: image = bound_labels.measurement_reference_image() projection = bound_labels.declared_plane_projection() - assert projection is not None and projection.plane_index is None + assert projection is not None and projection.preserves_complete_axis measurement_values = RuntimeSliceAlignedValues((np.asarray([0.25]),)) projected = RuntimeSliceProjection.kwargs_for_slice( {"object_labels": (bound_labels,), "measurement_values": measurement_values}, @@ -5053,10 +5055,10 @@ def test_adapter_object_record_uses_nominal_full_stack_binding() -> None: "Cells", ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=label_array), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,),), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ), ) @@ -5109,10 +5111,10 @@ def test_image_stack_requirement_does_not_control_scalar_object_input_projection labels = ObjectLabelSet( name="Cells", variant_data=ObjectLabelVariantData(labels=label_array), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,),), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) object_spec = ArtifactSpec.input("Cells", ObjectLabelsArtifactType) @@ -5147,21 +5149,21 @@ def consume_labels( bound_labels = request.label_argument_for(object_spec, "labels") assert isinstance(bound_labels, ObjectLabelValue) - assert bound_labels.plane_axis is None + assert bound_labels.plane_axis.unstacked assert object_label_dense_array(bound_labels).shape == (2, 2) @pytest.mark.parametrize( ("slice_by_slice", "expected_shape", "expected_plane_axis"), ( - (True, (2, 2), None), - (False, (1, 2, 2), RuntimePlaneAxis.RUNTIME_SLICE), + (True, (2, 2), NoPlaneAxis), + (False, (1, 2, 2), RuntimeSlicePlaneAxis), ), ) def test_match_image_stack_object_input_follows_declared_image_execution( slice_by_slice: bool, expected_shape: tuple[int, ...], - expected_plane_axis: RuntimePlaneAxis | None, + expected_plane_axis: type[RuntimePlaneAxis], ) -> None: label_array = np.asarray( (((0, 1), (0, 0)),), @@ -5170,10 +5172,10 @@ def test_match_image_stack_object_input_follows_declared_image_execution( labels = ObjectLabelSet( name="Cells", variant_data=ObjectLabelVariantData(labels=label_array), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,),), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) plane_projection = ( @@ -5226,10 +5228,10 @@ def test_pure_3d_executor_preserves_nominal_singleton_object_label_stack() -> No labels = ObjectLabelSet( name="Cells", variant_data=ObjectLabelVariantData(labels=label_array), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,),), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) seen_label_shape: list[tuple[int, ...]] = [] @@ -5272,10 +5274,10 @@ def test_object_row_binding_returns_current_runtime_plane_relationship() -> None dtype=np.int32, ) ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) declaration = ObjectRelationshipDeclaration.parent_child( @@ -5361,7 +5363,7 @@ def add_one(image: np.ndarray) -> np.ndarray: ) stack = ImageMetadataPayload( np.zeros((2, 4, 5), dtype=np.uint16), - ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE), + ImagePayloadMetadata(plane_axis=RuntimeSlicePlaneAxis), ) result = CellProfilerFunctionContractExecutor().execute( @@ -5370,7 +5372,7 @@ def add_one(image: np.ndarray) -> np.ndarray: stack, {}, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), execution_mode=NaturalExecution, @@ -5433,7 +5435,7 @@ def split_first_channel(image: np.ndarray) -> np.ndarray: np.zeros((2, 4, 5, 2), dtype=np.float32), ImagePayloadMetadata( axes=PayloadAxes.colour_samples(-1), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( component_metadata=( {"site": "1"}, @@ -5451,7 +5453,7 @@ def split_first_channel(image: np.ndarray) -> np.ndarray: stack, {}, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), execution_mode=NaturalExecution, @@ -5485,15 +5487,15 @@ def add_labels(image: np.ndarray, *, labels: np.ndarray) -> np.ndarray: ) image = ImageMetadataPayload( np.zeros((2, 3, 4, 5), dtype=np.float32), - ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE), + ImagePayloadMetadata(plane_axis=RuntimeSlicePlaneAxis), ) label_data = np.arange(2, dtype=np.int32).reshape((2, 1, 1, 1)) label_data = np.broadcast_to(label_data, image.shape) labels = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=label_data), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((), (1,)), ), ) @@ -5504,7 +5506,7 @@ def add_labels(image: np.ndarray, *, labels: np.ndarray) -> np.ndarray: image, {"labels": labels}, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), execution_mode=NaturalExecution, @@ -5537,9 +5539,9 @@ def keep_labels( label_data = np.broadcast_to(label_data, (3, 2, 4, 5)) labels = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=label_data), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((), (1,), (2,)), ), ) @@ -5585,7 +5587,7 @@ def keep_labels( ) image = ImageMetadataPayload( np.zeros((3, 4, 5), dtype=np.float32), - ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE), + ImagePayloadMetadata(plane_axis=RuntimeSlicePlaneAxis), ) label_data = np.zeros((3, 4, 5), dtype=np.int32) label_data[0, 1:3, 1:3] = 1 @@ -5593,9 +5595,9 @@ def keep_labels( label_data[2, 1:4, 1:4] = 3 labels = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=label_data), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,), (2,), (3,)), ), ) @@ -5606,7 +5608,7 @@ def keep_labels( image, {"labels": labels}, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=3, ), execution_mode=NaturalExecution, @@ -5628,7 +5630,7 @@ def add_singleton_plane(image: np.ndarray) -> np.ndarray: ) stack = ImageMetadataPayload( np.zeros((2, 4, 5), dtype=np.uint16), - ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE), + ImagePayloadMetadata(plane_axis=RuntimeSlicePlaneAxis), ) result = CellProfilerFunctionContractExecutor().execute( @@ -5637,7 +5639,7 @@ def add_singleton_plane(image: np.ndarray) -> np.ndarray: stack, {}, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), execution_mode=NaturalExecution, @@ -5662,7 +5664,7 @@ def add_singleton_color_plane(image: np.ndarray) -> np.ndarray: ) stack = ImageMetadataPayload( np.zeros((2, 4, 5), dtype=np.uint16), - ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE), + ImagePayloadMetadata(plane_axis=RuntimeSlicePlaneAxis), ) result = CellProfilerFunctionContractExecutor().execute( @@ -5671,7 +5673,7 @@ def add_singleton_color_plane(image: np.ndarray) -> np.ndarray: stack, {}, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), execution_mode=NaturalExecution, @@ -5696,7 +5698,7 @@ def add_singleton_volume(image: np.ndarray) -> np.ndarray: ) stack = ImageMetadataPayload( np.zeros((3, 4, 5), dtype=np.uint16), - ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE), + ImagePayloadMetadata(plane_axis=RuntimeSlicePlaneAxis), ) result = CellProfilerFunctionContractExecutor().execute( @@ -5705,7 +5707,7 @@ def add_singleton_volume(image: np.ndarray) -> np.ndarray: stack, {}, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=3, ), execution_mode=NaturalExecution, @@ -5780,10 +5782,10 @@ def test_complete_object_measurement_rows_preserves_sliced_object_label_set_doma variant_data=ObjectLabelVariantData(labels=labels), source_image_name="BF_image", domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1, 2, 3), (1, 2, 3)), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) rows = complete_object_measurement_rows( @@ -6053,9 +6055,9 @@ def test_measure_object_intensity_direct_slice_requires_projected_label_plane() ), domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) with pytest.raises(ValueError, match="explicit object-ID domain"): @@ -6090,14 +6092,14 @@ def test_measure_object_size_shape_consumes_runtime_projected_label_plane() -> N ), domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) projected_labels = RuntimeSliceProjection.value_for_slice( labels, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=1, axis_size=2 + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=2 ), ) @@ -6126,7 +6128,7 @@ def records_slice_index(image, *, slice_index: int = 0): CallableContract.from_callable(records_slice_index), records_slice_index, RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=3, ), ) @@ -6149,10 +6151,10 @@ def test_complete_object_measurement_rows_uses_declared_slice_local_label_domain payload = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1, 3), (1, 2)), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) row_policy = MeasureObjectIntensityModule.runtime_object_measurement_row_policy() @@ -6414,17 +6416,17 @@ def test_measure_texture_uses_exact_projected_plane_domain_for_missing_rows() -> labels[0, 0, 0] = 5 payload = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_names=("BF_image", "MorphBf"), domain=ObjectLabelDomain( declared_object_id_domains=((1, 2, 3, 4, 5), (1,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) projected_payload = RuntimeSliceProjection.value_for_slice( payload, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=0, axis_size=2 + axis=RuntimeSlicePlaneAxis, plane_index=0, axis_size=2 ), ) row_policy = MeasureTextureModule.runtime_object_measurement_row_policy() @@ -6472,10 +6474,10 @@ def test_measure_object_intensity_missing_rows_use_declared_label_extent() -> No ) ), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1, 2, 3), (1, 2, 3)), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) row_policy = MeasureObjectIntensityModule.runtime_object_measurement_row_policy() schema = ObjectMeasurementRowCompletionSchema.for_completion_fields( @@ -6699,10 +6701,10 @@ def test_object_only_measurement_preserves_runtime_slice_carrier_stack() -> None "Nuclei": ObjectLabelSet( name="Nuclei", variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (1,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) }, @@ -6750,7 +6752,7 @@ def test_resolved_measurement_image_preserves_runtime_slice_projection() -> None ) executor = _module_executor(contract) image = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE + plane_axis=RuntimeSlicePlaneAxis ).payload_with(np.zeros((21, 5, 5), dtype=np.float32), None) runtime = _FakeCellProfilerRuntime( {"DNA": image}, @@ -6769,7 +6771,7 @@ def test_resolved_measurement_image_preserves_runtime_slice_projection() -> None assert measurement_image.plane_projection == ( RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=21, source_aliases=("DNA",), ) @@ -6787,7 +6789,7 @@ def test_multi_image_photometry_consumes_each_named_current_source(aliases): images["Process"][1, 2] = 221 images["Nuclear"][2, 3] = 255 payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_names=aliases, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=tuple(f"/synthetic/{name}.tif" for name in aliases), @@ -6842,7 +6844,7 @@ def test_registered_object_photometry_retains_each_source_at_each_slice(aliases, source_image_names=(name,), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=paths, component_metadata=fields), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE if slice_count > 1 else None, + plane_axis=RuntimeSlicePlaneAxis if slice_count > 1 else NoPlaneAxis, ) shape = (slice_count, 4, 5) if slice_count > 1 else (4, 5) images[name] = metadata.payload_with(np.full(shape, value, dtype=np.uint16)) @@ -6850,9 +6852,9 @@ def test_registered_object_photometry_retains_each_source_at_each_slice(aliases, labels = ObjectLabelSet( name="Cells", variant_data=ObjectLabelVariantData(labels=np.ones(label_shape, dtype=np.int32)), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE if slice_count > 1 else None, + plane_axis=RuntimeSlicePlaneAxis if slice_count > 1 else NoPlaneAxis, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE if slice_count > 1 else ObjectLabelDomainScope.PAYLOAD, + scope=PlaneLabelScope if slice_count > 1 else PayloadLabelScope, declared_object_id_domains=((1,),) * slice_count if slice_count > 1 else (), declared_object_count=None if slice_count > 1 else 1, ), @@ -6930,10 +6932,10 @@ def test_object_intensity_measurement_image_batch_preserves_request_labels() -> variant_data=ObjectLabelVariantData( labels=np.asarray([[[1]], [[2]]], dtype=np.int32) ), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_image_names=("ImageA", "ImageB"), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,), (2,)), ), ) @@ -6942,10 +6944,10 @@ def test_object_intensity_measurement_image_batch_preserves_request_labels() -> variant_data=ObjectLabelVariantData( labels=np.asarray([[[10]], [[20]]], dtype=np.int32) ), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_image_names=("ImageA", "ImageB"), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((10,), (20,)), ), ) @@ -6993,9 +6995,9 @@ def execute_request(_func, request): variant_data=ObjectLabelVariantData( labels=np.asarray([[[1]], [[2]]], dtype=np.int32) ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,), (2,)), ), ) @@ -7049,10 +7051,10 @@ def test_object_intensity_measurement_image_batch_delegates_natural_prepared_req variant_data=ObjectLabelVariantData( labels=np.asarray([[[1, 0]], [[2, 3]]], dtype=np.int32) ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2, 3)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) object_spec = ArtifactSpec.output("Cells", ObjectLabelsArtifactType) @@ -7580,10 +7582,10 @@ def test_measure_object_size_shape_preserves_sparse_plane_storage() -> None: ) ), representation=ObjectLabelRepresentation.SPARSE_IJV, - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) @@ -7685,7 +7687,7 @@ def colorize(image: np.ndarray) -> np.ndarray: ) stack = ImageMetadataPayload( np.zeros((2, 4, 5), dtype=np.float32), - ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE), + ImagePayloadMetadata(plane_axis=RuntimeSlicePlaneAxis), ) result = CellProfilerFunctionContractExecutor().execute( @@ -7695,7 +7697,7 @@ def colorize(image: np.ndarray) -> np.ndarray: {}, execution_mode=NaturalExecution, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), ) @@ -7734,7 +7736,7 @@ def test_gray_to_color_inherits_first_declared_input_mask() -> None: first_mask[1, 2] = False second_mask = np.ones((4, 5), dtype=bool) image = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("/input/red.tif", "/input/green.tif"), component_metadata=( @@ -7762,7 +7764,7 @@ def test_gray_to_color_inherits_first_declared_input_mask() -> None: assert result.data.shape == (4, 5, 3) np.testing.assert_array_equal(result.mask, first_mask) metadata = result.metadata - assert metadata.plane_axis is None + assert metadata.plane_axis.unstacked assert metadata.axis_position(ColourAxis) == -1 assert metadata.source_image_provenance_planes.paths == ( "/input/red.tif", @@ -7775,7 +7777,7 @@ def test_color_to_gray_combines_openhcs_color_stack() -> None: np.zeros((2, 4, 5, 3), dtype=np.float32), ImagePayloadMetadata( axes=PayloadAxes.colour_samples(-1), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ), ) image.data[..., 0] = 2.0 @@ -7816,7 +7818,7 @@ def test_color_to_gray_combine_removes_declared_source_channel_axis() -> None: source_path="/input/A01_s001.tif", source_component_metadata={"site": "1"}, axes=PayloadAxes.colour_samples(-1), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ), ) image.data[..., 0] = 2.0 @@ -8099,7 +8101,7 @@ def test_cellprofiler_image_aggregation_uses_nominal_image_payload_type() -> Non aggregated = RuntimeSliceProjection.full_stack_value(aggregated) assert aggregated.data.shape == (1, 4, 5) assert aggregated.metadata.plane_axis is ( - RuntimePlaneAxis.RUNTIME_SLICE + RuntimeSlicePlaneAxis ) @@ -8150,7 +8152,7 @@ class SliceMeasurement(MeasurementFeatureRecord): Pure2DAuxiliaryOutputAggregator.aggregate( [SliceMeasurement(0, 1.0), SliceMeasurement(1, 2.0)], MemoryType.NUMPY.value, - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) @@ -8164,11 +8166,11 @@ def test_pure_2d_declared_object_label_output_preserves_single_slice_axis() -> N aggregated = Pure2DAuxiliaryOutputAggregator.aggregate( [label_payload], MemoryType.NUMPY.value, - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) assert isinstance(aggregated, ObjectLabelPayload) - assert aggregated.domain.scope is ObjectLabelDomainScope.PLANE + assert aggregated.domain.scope.ids_per_plane np.testing.assert_array_equal( object_label_dense_array(aggregated), labels[np.newaxis, ...], @@ -8224,7 +8226,7 @@ def test_full_stack_pure_2d_implicit_main_output_aligns_declared_object_labels() label_payload = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), domain=ObjectLabelDomain.declared( - scope=ObjectLabelDomainScope.PAYLOAD, + scope=PayloadLabelScope, declared_object_ids=(1,), ), ) @@ -8260,7 +8262,7 @@ def watershed_like( assert result_image is image assert result_measurements == {"count": 1} assert isinstance(result_labels, ObjectLabelPayload) - assert result_labels.domain.scope is ObjectLabelDomainScope.PAYLOAD + assert result_labels.domain.scope.ids_span_payload np.testing.assert_array_equal(object_label_dense_array(result_labels), labels) @@ -8270,7 +8272,7 @@ def test_full_stack_pure_2d_non_flow_main_keeps_relationship_output_alignment() variant_data=ObjectLabelVariantData( labels=np.asarray(((0, 1), (0, 0)), dtype=np.int32) ), - domain=ObjectLabelDomain(scope=ObjectLabelDomainScope.PAYLOAD), + domain=ObjectLabelDomain(scope=PayloadLabelScope), ) relationship = DirectedObjectRelationshipPayload(source_ids=(1,), target_ids=(1,)) parent_spec = ArtifactSpec.input("Parent", ObjectLabelsArtifactType) @@ -8334,7 +8336,7 @@ def object_transform_like( assert result_measurements == {"count": 1} assert result_relationship is relationship assert result_labels is labels - assert result_labels.domain.scope is ObjectLabelDomainScope.PAYLOAD + assert result_labels.domain.scope.ids_span_payload def test_pure_2d_aggregation_uses_generic_columnar_row_owner() -> None: @@ -8431,7 +8433,7 @@ def test_illumination_apply_projects_broadcast_input_to_selected_primary_site( source_image_names=("OrigHoechst",), ).payload_with(original_pixels, None) illumination_payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=( "/prepared/IllumHoechst/A01_s001_w5_z001_t001.npy", @@ -8595,21 +8597,21 @@ def test_illumination_apply_image_output_uses_original_input_source_payload() -> @pytest.mark.parametrize( ("output_variable_components", "plane_projection", "expected_plane_axis"), ( - ((), None, None), + ((), None, NoPlaneAxis), ( (Microscopy.Channel,), RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), - RuntimePlaneAxis.RUNTIME_SLICE, + RuntimeSlicePlaneAxis, ), ), ) def test_image_output_projection_uses_exact_invocation_projection( output_variable_components: tuple[type[Axis], ...], plane_projection: RuntimePlaneAxisValueProjection | None, - expected_plane_axis: RuntimePlaneAxis | None, + expected_plane_axis: type[RuntimePlaneAxis], ) -> None: source_spec = ArtifactSpec.input("SourceImage", ImageArtifactType) output_spec = ArtifactSpec.output( @@ -8628,7 +8630,7 @@ def test_image_output_projection_uses_exact_invocation_projection( "/plate/A01_s001_w2_z001_t001.tif", ) source_payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_names=(source_spec.name,), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=source_paths, @@ -8690,7 +8692,7 @@ def test_image_output_projection_uses_exact_invocation_projection( metadata = recorded.metadata assert metadata.plane_axis is expected_plane_axis assert metadata.source_image_names == (output_spec.name,) - if expected_plane_axis is None: + if expected_plane_axis.unstacked: assert dict(metadata.source_component_metadata or {}) == { "well": "A01", "site": "1", @@ -8724,10 +8726,10 @@ def test_declared_output_source_uses_projected_object_input_value() -> None: dtype=np.int32, ) ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,),), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) output_plan = replace( @@ -8771,14 +8773,14 @@ def test_declared_output_source_uses_projected_object_input_value() -> None: projected_source = MeasureObjectNeighborsModule.source_payload(request) assert isinstance(projected_source, ObjectLabelSet) - assert projected_source.plane_axis is None + assert projected_source.plane_axis.unstacked assert object_label_dense_array(projected_source).shape == (2, 3) CellProfilerOutputRecorder.for_artifact_type(ImageArtifactType).record(request) recorded = runtime.images[output_spec.name] assert recorded.data.shape == (2, 3, 3) - assert recorded.metadata.plane_axis is None + assert recorded.metadata.plane_axis.unstacked def test_image_output_recording_projects_masked_singleton_rgb_payload() -> None: @@ -8802,7 +8804,7 @@ def test_image_output_recording_projects_masked_singleton_rgb_payload() -> None: source_payload = ImagePayloadMetadata( source_spatial_domain=spatial_domain, source_image_names=(source_spec.name,), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with( np.zeros((1, 4, 5), dtype=np.float32), mask[np.newaxis, ...], @@ -8810,7 +8812,7 @@ def test_image_output_recording_projects_masked_singleton_rgb_payload() -> None: output_payload = ImagePayloadMetadata( source_spatial_domain=spatial_domain, axes=PayloadAxes.colour_samples(3), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with( np.zeros((1, 4, 5, 3), dtype=np.float32), mask[np.newaxis, ...], @@ -8857,7 +8859,7 @@ def test_image_output_recording_projects_masked_singleton_rgb_payload() -> None: assert recorded.data.shape == (4, 5, 3) np.testing.assert_array_equal(recorded.mask, mask) metadata = recorded.metadata - assert metadata.plane_axis is None + assert metadata.plane_axis.unstacked assert metadata.axis_index(ColourAxis, recorded) == 2 @@ -9031,15 +9033,15 @@ def keep_labels(image: np.ndarray, *, labels: ObjectLabelValue): ) stack = ImageMetadataPayload( np.zeros((2, 4, 5), dtype=np.uint16), - ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE), + ImagePayloadMetadata(plane_axis=RuntimeSlicePlaneAxis), ) labels = ObjectLabelPayload( variant_data=ObjectLabelVariantData( labels=np.ones(stack.shape, dtype=np.int32) ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,), (1,)), ), ) @@ -9051,7 +9053,7 @@ def keep_labels(image: np.ndarray, *, labels: ObjectLabelValue): {"labels": labels}, execution_mode=NaturalExecution, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), ) @@ -9092,7 +9094,7 @@ def keep_payload(image: np.ndarray): np.ones((4, 5), dtype=np.uint16), ) ), - ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE), + ImagePayloadMetadata(plane_axis=RuntimeSlicePlaneAxis), ) result_image, result_payload = CellProfilerFunctionContractExecutor().execute( @@ -9102,7 +9104,7 @@ def keep_payload(image: np.ndarray): {}, execution_mode=NaturalExecution, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), ) @@ -9149,7 +9151,7 @@ def segment(image: object): ), ) image = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=( "/input/A01_s001_w1_z001_t001.TIF", @@ -9169,14 +9171,14 @@ def segment(image: object): {}, execution_mode=NaturalExecution, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), ) assert result_image.data.shape == (2, 4, 5) assert isinstance(result_payload, ObjectLabelPayload) - assert result_payload.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert result_payload.plane_axis is RuntimeSlicePlaneAxis assert result_payload.source_image_provenance_planes.paths == ( "/input/A01_s001_w1_z001_t001.TIF", "/input/A01_s002_w1_z001_t001.TIF", @@ -9242,14 +9244,14 @@ def segment(image: object): {}, execution_mode=AlignedStackExecution, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), ) assert result_image.data.shape == (2, 4, 5) assert isinstance(result_payload, ObjectLabelSet) - assert result_payload.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert result_payload.plane_axis is RuntimeSlicePlaneAxis assert result_payload.source_image_names == ("OrigBlue", "OrigBlue") np.testing.assert_array_equal(result_payload.labels[0], np.full((4, 5), 1)) np.testing.assert_array_equal(result_payload.labels[1], np.full((4, 5), 2)) @@ -9475,7 +9477,7 @@ def source_payload(alias: str, channel: int) -> AlignedImageStack: ) assert metadata is not None - assert metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert metadata.plane_axis is RuntimeSlicePlaneAxis assert metadata.source_image_provenance_planes.component_metadata[:2] == ( {"well": "A01", "site": "1", "channel": "1"}, {"well": "A01", "site": "2", "channel": "1"}, @@ -9485,12 +9487,12 @@ def source_payload(alias: str, channel: int) -> AlignedImageStack: @pytest.mark.parametrize( ("incoming_axis", "mode", "expected_planes"), ( - (None, ImagePayloadMetadataCompositionMode.STACK, 2), - (None, ImagePayloadMetadataCompositionMode.BUNDLE, 1), - (RuntimePlaneAxis.RUNTIME_SLICE, ImagePayloadMetadataCompositionMode.STACK, 2), - (RuntimePlaneAxis.RUNTIME_SLICE, ImagePayloadMetadataCompositionMode.BUNDLE, 1), - (RuntimePlaneAxis.SOURCE_BINDING, ImagePayloadMetadataCompositionMode.STACK, 1), - (RuntimePlaneAxis.SOURCE_BINDING, ImagePayloadMetadataCompositionMode.BUNDLE, 2), + (NoPlaneAxis, RuntimeSlicePlaneAxis, 2), + (NoPlaneAxis, SourceBindingPlaneAxis, 1), + (RuntimeSlicePlaneAxis, RuntimeSlicePlaneAxis, 2), + (RuntimeSlicePlaneAxis, SourceBindingPlaneAxis, 1), + (SourceBindingPlaneAxis, RuntimeSlicePlaneAxis, 1), + (SourceBindingPlaneAxis, SourceBindingPlaneAxis, 2), ), ) def test_image_source_composition_preserves_only_compatible_incoming_topology( @@ -9540,7 +9542,7 @@ def provenance(channel, alias): metadata = ImagePayloadMetadata( source_provenance=provenance(2, "OrigBlue"), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) source = CellProfilerMeasurementImage( source_image_name="OrigBlue", @@ -9853,7 +9855,7 @@ def test_invocation_request_keeps_original_aliases_when_labels_change_image_doma def test_cellprofiler_main_flow_output_preserves_input_source_planes(): input_image = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=( "/input/A01_s001_w3_z001_t001.TIF", @@ -9866,14 +9868,14 @@ def test_cellprofiler_main_flow_output_preserves_input_source_planes(): ), ).payload_with(np.zeros((2, 4, 5), dtype=np.float32), None) output_image = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.ones((2, 4, 5), dtype=np.float32), None) result = cellprofiler_main_flow_output( input_image, output_image, RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), ) @@ -9895,7 +9897,7 @@ def test_cellprofiler_main_flow_output_preserves_input_source_planes(): def test_cellprofiler_main_flow_output_replaces_scalar_output_source_on_stack(): input_image = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=( "/input/A01_s001_w3_z001_t001.TIF", @@ -9908,7 +9910,7 @@ def test_cellprofiler_main_flow_output_replaces_scalar_output_source_on_stack(): ), ).payload_with(np.zeros((2, 4, 5), dtype=np.float32), None) output_image = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_component_metadata={"well": "A01", "site": "1", "channel": "3"}, ).payload_with(np.ones((2, 4, 5), dtype=np.float32), None) @@ -9916,7 +9918,7 @@ def test_cellprofiler_main_flow_output_replaces_scalar_output_source_on_stack(): input_image, output_image, RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), ) @@ -9970,7 +9972,7 @@ def relate(image: np.ndarray): ) stack = ImageMetadataPayload( np.zeros((2, 4, 5), dtype=np.float32), - ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE), + ImagePayloadMetadata(plane_axis=RuntimeSlicePlaneAxis), ) result_image, relationship = CellProfilerFunctionContractExecutor().execute( @@ -9980,7 +9982,7 @@ def relate(image: np.ndarray): {}, execution_mode=NaturalExecution, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), ) @@ -10018,7 +10020,7 @@ def keep_volume_labels(image: np.ndarray): np.full((3, 4, 5), 2.0, dtype=np.float32), ) ), - ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE), + ImagePayloadMetadata(plane_axis=RuntimeSlicePlaneAxis), ) result_image, result_labels = CellProfilerFunctionContractExecutor().execute( @@ -10028,7 +10030,7 @@ def keep_volume_labels(image: np.ndarray): {}, execution_mode=NaturalExecution, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), ) @@ -10187,7 +10189,7 @@ def add_label_values(image: np.ndarray, *, labels: np.ndarray): np.full((4, 5), 20, dtype=np.uint16), ) ), - ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE), + ImagePayloadMetadata(plane_axis=RuntimeSlicePlaneAxis), ) labels = np.ones((4, 5), dtype=np.uint16) @@ -10198,7 +10200,7 @@ def add_label_values(image: np.ndarray, *, labels: np.ndarray): {"labels": labels}, execution_mode=NaturalExecution, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), ) @@ -10582,7 +10584,7 @@ def test_pure_2d_object_label_payload_aggregation_expands_varying_crop_domains() (1, 2, 3), (1, 2, 3, 4, 5), ) - assert aggregated.domain.scope is ObjectLabelDomainScope.PLANE + assert aggregated.domain.scope.ids_per_plane assert aggregated.spatial_origin_yx is None assert aggregated.source_spatial_shape_yx == (4, 5) assert aggregated.labels[0, 1, 2] == 1 @@ -10614,7 +10616,7 @@ def test_pure_2d_object_label_payload_aggregation_uses_declared_slice_domains(): assert isinstance(aggregated, ObjectLabelPayload) assert aggregated.domain.declared_object_id_domains == ((1, 2), (3,)) - assert aggregated.domain.scope is ObjectLabelDomainScope.PLANE + assert aggregated.domain.scope.ids_per_plane def test_pure_2d_object_label_payload_slice_projects_plane_domain() -> None: @@ -10630,15 +10632,15 @@ def test_pure_2d_object_label_payload_slice_projects_plane_domain() -> None: ), domain=ObjectLabelDomain( declared_object_id_domains=((1, 2), (1, 2, 3, 4)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) sliced = RuntimeSliceProjection.value_for_slice( payload, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=1, axis_size=2 + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=2 ), ) @@ -10646,7 +10648,7 @@ def test_pure_2d_object_label_payload_slice_projects_plane_domain() -> None: assert sliced.labels.shape == (2, 2) assert sliced.domain.declared_object_ids == (1, 2, 3, 4) assert sliced.domain.declared_object_id_domains == () - assert sliced.domain.scope is ObjectLabelDomainScope.PAYLOAD + assert sliced.domain.scope.ids_span_payload def test_object_label_endpoint_uses_declared_runtime_slice_axis() -> None: @@ -10662,9 +10664,9 @@ def test_object_label_endpoint_uses_declared_runtime_slice_axis() -> None: ), domain=ObjectLabelDomain( declared_object_id_domains=((1, 2), (3, 4)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) sliced = RuntimeSliceProjection.object_label_endpoint( @@ -10709,16 +10711,16 @@ def test_object_label_endpoint_rejects_singleton_axis_cardinality_mismatch() -> ), domain=ObjectLabelDomain( declared_object_id_domains=((1, 2),), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) with pytest.raises(ValueError, match="cardinality mismatch"): RuntimeSliceProjection.object_label_endpoint( payload, context=RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=1, axis_size=3 + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=3 ), ) @@ -10737,15 +10739,15 @@ def test_pure_2d_object_label_set_slice_projects_plane_domain() -> None: ), domain=ObjectLabelDomain( declared_object_id_domains=((1, 2), (1, 2, 3, 4)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) sliced = RuntimeSliceProjection.value_for_slice( label_set, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=0, axis_size=2 + axis=RuntimeSlicePlaneAxis, plane_index=0, axis_size=2 ), ) @@ -10753,7 +10755,7 @@ def test_pure_2d_object_label_set_slice_projects_plane_domain() -> None: assert sliced.labels.shape == (2, 2) assert sliced.domain.declared_object_ids == (1, 2) assert sliced.domain.declared_object_id_domains == () - assert sliced.domain.scope is ObjectLabelDomainScope.PAYLOAD + assert sliced.domain.scope.ids_span_payload def test_pure_2d_object_label_payload_slice_projects_grouped_plane_domains() -> None: @@ -10763,15 +10765,15 @@ def test_pure_2d_object_label_payload_slice_projects_grouped_plane_domains() -> ), domain=ObjectLabelDomain( declared_object_id_domains=((1, 2, 3), (4, 5, 6)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) sliced = RuntimeSliceProjection.value_for_slice( payload, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=1, axis_size=2 + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=2 ), ) @@ -10779,7 +10781,7 @@ def test_pure_2d_object_label_payload_slice_projects_grouped_plane_domains() -> assert sliced.labels.shape == (3, 4, 5) assert sliced.domain.declared_object_ids == (4, 5, 6) assert sliced.domain.declared_object_id_domains == () - assert sliced.domain.scope is ObjectLabelDomainScope.PAYLOAD + assert sliced.domain.scope.ids_span_payload def test_grouped_object_label_planes_project_source_provenance_independently() -> None: @@ -10789,9 +10791,9 @@ def test_grouped_object_label_planes_project_source_provenance_independently() - ), domain=ObjectLabelDomain( declared_object_id_domains=((1, 2), (3, 4)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("/input/site-1.tif", "/input/site-2.tif"), component_metadata=({"site": "1"}, {"site": "2"}), @@ -10801,7 +10803,7 @@ def test_grouped_object_label_planes_project_source_provenance_independently() - sliced = RuntimeSliceProjection.value_for_slice( payload, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=1, axis_size=2 + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=2 ), ) @@ -10819,17 +10821,17 @@ def test_runtime_slice_count_allows_grouped_object_label_planes() -> None: ), domain=ObjectLabelDomain( declared_object_id_domains=((1, 2, 3), (4, 5, 6)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) child = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=np.zeros((2, 4, 5), dtype=np.int32)), domain=ObjectLabelDomain( declared_object_id_domains=((1, 2, 3), (4, 5, 6)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) assert RuntimeSliceProjection.slice_count_from_values((parent, child)) == 2 @@ -10840,17 +10842,17 @@ def test_runtime_slice_count_uses_sequence_value_declarations() -> None: variant_data=ObjectLabelVariantData(labels=np.zeros((2, 4, 5), dtype=np.int32)), domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) second = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=np.zeros((2, 4, 5), dtype=np.int32)), domain=ObjectLabelDomain( declared_object_id_domains=((3,), (4,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) assert RuntimeSliceProjection.slice_count_from_values(((first, second),)) == 2 @@ -10904,7 +10906,7 @@ def test_runtime_slice_projection_does_not_offset_repeated_scalar_tables() -> No sliced = RuntimeSliceProjection.kwargs_for_slice( {"measurement_tables": tables}, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=1, axis_size=2 + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=2 ), )["measurement_tables"] assert tuple(table.rows.row_count() for table in sliced) == (0, 0) @@ -10968,10 +10970,10 @@ def test_identify_tertiary_preserves_exact_runtime_projected_plane_domains(): primary = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=np.zeros((2, 3, 3), dtype=np.int32)), source_image_names=("rawDNA", "rawActin"), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((), ()), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) secondary = ObjectLabelPayload( @@ -10985,17 +10987,17 @@ def test_identify_tertiary_preserves_exact_runtime_projected_plane_domains(): ) ), source_image_names=("rawDNA", "rawActin"), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((2,), (5,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) outputs = [] for slice_index in range(2): projection_axis = RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=slice_index, axis_size=2 + axis=RuntimeSlicePlaneAxis, plane_index=slice_index, axis_size=2 ) *_, output = ito.identify_tertiary_objects.__wrapped__( np.zeros((3, 3), dtype=np.float32), @@ -11038,17 +11040,17 @@ def test_tertiary_projected_plane_keeps_dense_local_label_domain(): source_shape_yx=(10, 11), ), source_image_names=("rawDNA", "rawActin"), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((2,), (5,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) labels = np.asarray(((0, 0), (5, 0)), dtype=np.int32) projected_source = RuntimeSliceProjection.value_for_slice( source, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=1, axis_size=2 + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=2 ), ) @@ -11275,7 +11277,7 @@ def test_compose_image_payload_rejects_unowned_singleton_broadcast(): spatial_domain = SourceSpatialDomain(source_shape_yx=(4, 5)) raw_stack = ImagePayloadMetadata( source_spatial_domain=spatial_domain, - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with( np.stack( ( @@ -11300,7 +11302,7 @@ def test_compose_image_payload_broadcasts_only_from_declared_stack_owner(): spatial_domain = SourceSpatialDomain(source_shape_yx=(4, 5)) raw_stack = ImagePayloadMetadata( source_spatial_domain=spatial_domain, - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with( np.stack( ( @@ -11381,7 +11383,7 @@ def test_compose_image_bundle_rejects_shape_selected_singleton_collapse(): spatial_domain = SourceSpatialDomain(source_shape_yx=(4, 5)) singleton = ImagePayloadMetadata( source_spatial_domain=spatial_domain, - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.full((1, 4, 5), 3, dtype=np.float32), None) plane = ImagePayloadMetadata(source_spatial_domain=spatial_domain).payload_with( np.full((4, 5), 7, dtype=np.float32), None @@ -11525,7 +11527,7 @@ def test_tile_aligned_multi_image_stack_tiles_each_runtime_slice() -> None: {"rows": 1, "columns": 2}, execution_mode=AlignedStackExecution, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), ) @@ -11574,7 +11576,7 @@ def subtract_illumination(image: np.ndarray) -> np.ndarray: {}, execution_mode=AlignedStackExecution, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), ) @@ -11610,7 +11612,7 @@ def keep_volume(image: np.ndarray) -> np.ndarray: {}, execution_mode=AlignedStackExecution, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), ) @@ -12068,7 +12070,7 @@ def test_object_label_output_recorder_uses_output_label_domain() -> None: np.testing.assert_array_equal(recorded_payload.labels, output_labels) -@pytest.mark.parametrize("carrier_axis", tuple(RuntimePlaneAxis)) +@pytest.mark.parametrize("carrier_axis", RuntimePlaneAxis.leading_axes()) @pytest.mark.parametrize("module_name", ( "ExpandOrShrinkObjects", "DilateObjects", "ShrinkToObjectCenters", "ClassifyObjectsSingleMeasurement", @@ -12084,9 +12086,9 @@ def test_object_only_module_uses_label_planes_instead_of_carrier_axis( variant_data=ObjectLabelVariantData(labels=labels), domain=ObjectLabelDomain( declared_object_id_domains=((1,), (4,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("/input/site-1.tif", "/input/site-2.tif"), component_metadata=({"site": "1"}, {"site": "2"}), @@ -12146,7 +12148,7 @@ def test_object_only_module_uses_label_planes_instead_of_carrier_axis( assert classified == ((0, 1, 1, 0), (1, 4, 0, 1)) return recorded = runtime.objects[0][1] - assert recorded.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert recorded.plane_axis is RuntimeSlicePlaneAxis assert recorded.domain.declared_object_id_domains == ((1,), (4,)) assert ( recorded.source_image_provenance_planes @@ -12434,10 +12436,10 @@ def classify_like(image: np.ndarray) -> tuple[np.ndarray, ClassificationResult]: labels=np.array([[[1, 2], [3, 0]]], dtype=np.int32) ), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1, 2, 3),), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=( SourceImageProvenancePlanes.from_components( paths=("/input/Nuclei.tif",), @@ -12718,7 +12720,7 @@ def test_literal_measurement_stack_preserves_pixels_in_object_reference_domain( ) stack = ProducedImageStack( (metadata.payload_with(pixels, mask),), - memory_type="numpy", plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + memory_type="numpy", plane_axis=RuntimeSlicePlaneAxis, ) labels = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=np.ones(pixels.shape, dtype=np.int32)), @@ -12735,7 +12737,7 @@ def test_literal_measurement_stack_preserves_pixels_in_object_reference_domain( np.testing.assert_array_equal(prepared.measurement_labels, labels.labels) -@pytest.mark.parametrize("carrier_axis", tuple(RuntimePlaneAxis)) +@pytest.mark.parametrize("carrier_axis", RuntimePlaneAxis.leading_axes()) def test_filterobjects_binds_selection_measurement_values_to_label_slices( carrier_axis, ) -> None: @@ -12779,10 +12781,10 @@ def test_filterobjects_binds_selection_measurement_values_to_label_slices( ) ), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1, 2), (1, 2)), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ), "Tiles": ObjectLabelSet( name="Tiles", @@ -12797,10 +12799,10 @@ def test_filterobjects_binds_selection_measurement_values_to_label_slices( ) ), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,), (1,)), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ), }, measurement_tables={"Cells": (measurements,)}, @@ -12893,9 +12895,9 @@ def test_filterobjects_binds_selection_measurement_values_to_label_slices( @pytest.mark.parametrize("carrier_axis, binary_operand", ( - (RuntimePlaneAxis.SOURCE_BINDING, False), - (RuntimePlaneAxis.RUNTIME_SLICE, False), - (RuntimePlaneAxis.RUNTIME_SLICE, True), + (SourceBindingPlaneAxis, False), + (RuntimeSlicePlaneAxis, False), + (RuntimeSlicePlaneAxis, True), )) def test_area_occupied_rows_preserve_the_selected_domain(carrier_axis, binary_operand) -> None: from openhcs.processing.backends.cellprofiler.area_occupied import OperandChoice @@ -12914,9 +12916,9 @@ def test_area_occupied_rows_preserve_the_selected_domain(carrier_axis, binary_op variant_data=ObjectLabelVariantData(labels=labels), domain=ObjectLabelDomain( declared_object_id_domains=((1,), (1,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) measurement_spec = ArtifactSpec.output( "AreaMeasurements", MeasurementsArtifactType, @@ -12983,19 +12985,19 @@ def test_relationship_measurements_preserve_pure_2d_slice_indices() -> None: name="Parents", variant_data=ObjectLabelVariantData(labels=parent_labels), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,), (2,)), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ), "Children": ObjectLabelSet( name="Children", variant_data=ObjectLabelVariantData(labels=child_labels), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,), (2,)), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ), }, ) @@ -13085,19 +13087,19 @@ def reject_ref_reconstruction(*_args: object, **_kwargs: object) -> None: name="Parents", variant_data=ObjectLabelVariantData(labels=parent_labels), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,), (2,)), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ), "Children": ObjectLabelSet( name="Children", variant_data=ObjectLabelVariantData(labels=child_labels), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,), (2,)), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ), }, artifact_input_edges=tuple( @@ -13208,7 +13210,7 @@ def test_parent_child_relationship_payload_slices_with_pure_2d_kwargs() -> None: sliced = RuntimeSliceProjection.value_for_slice( payload, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=1, axis_size=2 + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=2 ), ) @@ -13230,10 +13232,10 @@ def test_relationship_rows_project_temporal_endpoints_to_their_declared_slices() name="Embryos", variant_data=ObjectLabelVariantData(labels=labels), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1, 2), (1,)), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) embryos_input = ArtifactSpec.input("Embryos", ObjectLabelsArtifactType) declaration = ObjectRelationshipDeclaration( @@ -13321,7 +13323,7 @@ def test_object_relationship_slices_with_pure_2d_kwargs() -> None: sliced = RuntimeSliceProjection.value_for_slice( relationship, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=1, axis_size=2 + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=2 ), ) @@ -13343,19 +13345,19 @@ def test_relationship_measurements_reject_singleton_label_domain_broadcast() -> name="Parents", variant_data=ObjectLabelVariantData(labels=parent_labels), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,), (1,)), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ), "Children": ObjectLabelSet( name="Children", variant_data=ObjectLabelVariantData(labels=child_labels), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,),), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ), }, ) @@ -14139,7 +14141,7 @@ def test_relateobjects_parent_means_align_scoped_child_tables_by_source_plane( child_labels[1, 1:3, 1:3] = 1 child_labels[1, 3:5, 3:5] = 2 object_domain = ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1, 2), (1, 2)), ) colocalization_feature = "Correlation_Correlation_First_Second" @@ -14222,7 +14224,7 @@ def test_relateobjects_parent_means_align_scoped_child_tables_by_source_plane( name="Parents", variant_data=ObjectLabelVariantData(labels=parent_labels), domain=object_domain, - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=( SourceImageProvenancePlanes.from_components( paths=source_paths, @@ -14234,7 +14236,7 @@ def test_relateobjects_parent_means_align_scoped_child_tables_by_source_plane( name="Children", variant_data=ObjectLabelVariantData(labels=child_labels), domain=object_domain, - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=( SourceImageProvenancePlanes.from_components( paths=source_paths, @@ -14378,19 +14380,19 @@ def test_relateobjects_relationship_rows_project_distances_from_slice_measuremen name="Parents", variant_data=ObjectLabelVariantData(labels=parent_labels), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,), (1,)), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ), "Children": ObjectLabelSet( name="Children", variant_data=ObjectLabelVariantData(labels=child_labels), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,), (1,)), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ), }, ) @@ -14536,7 +14538,7 @@ def test_spatial_grid_output_recorder_accepts_pure_2d_grid_sequence() -> None: relations=(SourceStackLineageSourceRelation(source=source_spec.ref()),), ) source_payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_names=(source_spec.name,), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("/input/site1.tif", "/input/site2.tif"), @@ -14555,7 +14557,7 @@ def test_spatial_grid_output_recorder_accepts_pure_2d_grid_sequence() -> None: source_image_name=source_spec.name, payload=source_payload, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, axis_size=2 + axis=RuntimeSlicePlaneAxis, axis_size=2 ), ), output_plans=tuple(_artifact_output_plan(item) for item in ((grid_spec,))), @@ -14828,7 +14830,7 @@ def test_identify_objects_in_grid_location_rows_preserve_empty_grid_slots() -> N name="Guides", variant_data=ObjectLabelVariantData(labels=guide_labels), domain=ObjectLabelDomain.declared( - scope=ObjectLabelDomainScope.PAYLOAD, + scope=PayloadLabelScope, declared_object_ids=(2,), ), source_image_provenance_planes=( @@ -14958,10 +14960,10 @@ def test_identify_objects_in_grid_location_rows_use_slice_aligned_grid() -> None variant_data=ObjectLabelVariantData( labels=np.stack((first_guides, second_guides), axis=0) ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((2, 3), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), source_image_provenance_planes=( SourceImageProvenancePlanes.from_components( @@ -15081,10 +15083,10 @@ def test_object_location_measurements_preserve_declared_empty_grid_cells() -> No payload = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1, 2, 3), (1, 2, 3)), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) rows = ObjectLocationMeasurementRows( @@ -15189,9 +15191,9 @@ def test_object_location_measurements_preserve_sparse_plane_storage() -> None: representation=ObjectLabelRepresentation.SPARSE_IJV, domain=ObjectLabelDomain( declared_object_id_domains=((1, 2), (3,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) rows = ObjectLocationMeasurementRows( @@ -15235,7 +15237,7 @@ def test_object_location_measurements_use_payload_domain_for_full_stack_labels() rows = ObjectLocationMeasurementRows( payload, object_name="Nuclei", - domain_scope=ObjectLabelDomainScope.PAYLOAD, + domain_scope=PayloadLabelScope, ).rows() by_key = { @@ -15264,9 +15266,9 @@ def test_object_location_measurements_preserve_declared_diagonal_planes() -> Non variant_data=ObjectLabelVariantData(labels=labels), domain=ObjectLabelDomain( declared_object_id_domains=((1, 2), (1, 2)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) rows = ObjectLocationMeasurementRows( @@ -15298,9 +15300,9 @@ def test_object_location_measurements_preserve_repeated_homogeneous_planes() -> variant_data=ObjectLabelVariantData(labels=np.stack((plane, plane), axis=0)), domain=ObjectLabelDomain( declared_object_id_domains=((1, 2), (1, 2)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) rows = ObjectLocationMeasurementRows( @@ -15354,7 +15356,7 @@ def test_sparse_object_label_aggregation_preserves_declared_domain() -> None: (1, 2, 3, 4), (1, 2, 3, 4), ) - assert aggregated.domain.scope is ObjectLabelDomainScope.PLANE + assert aggregated.domain.scope.ids_per_plane assert isinstance(aggregated.labels, SparseIJVLabelRows) @@ -15515,7 +15517,7 @@ def test_image_payload_pure_2d_slicer_preserves_declared_volume_planes() -> None pairwise[1, 1] = 4.0 payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(pairwise, None) slices = Pure2DInputSlicer.strategy_for_value(payload).slice_value( payload, @@ -15540,7 +15542,7 @@ def test_image_payload_pure_2d_slicer_projects_declared_single_volume_mask() -> mask = np.ones((1, 3, 4, 5), dtype=bool) mask[0, 1] = False payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(data, mask) slices = Pure2DInputSlicer.strategy_for_value(payload).slice_value( @@ -15558,7 +15560,7 @@ def test_image_payload_pure_2d_slicer_projects_declared_volume_masks() -> None: mask = np.ones((2, 3, 4, 5), dtype=bool) mask[1, 2] = False payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(data, mask) slices = Pure2DInputSlicer.strategy_for_value(payload).slice_value( @@ -15580,7 +15582,7 @@ def test_image_payload_slice_projector_rejects_out_of_range_mask_axis() -> None: ImagePayloadSliceProjector( mask=mask, metadata=ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ), ).payload_for_slice(data_slice, 2) @@ -15668,9 +15670,9 @@ def source_binding_axis_size( ) labels = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=label_planes), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,), (2,)), ), ) @@ -15679,7 +15681,7 @@ def source_binding_axis_size( source_image_name=None, source_aliases=("origMemb",), payload=ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_image_names=("origDNA", "origMemb"), source_image_provenance_planes=( SourceImageProvenancePlanes.from_components( @@ -15746,7 +15748,7 @@ def source_binding_axis_size( mask=mask, metadata=ImagePayloadMetadata( source_image_names=("origDNA", "origMemb"), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_image_provenance_planes=( SourceImageProvenancePlanes.from_components( paths=("/input/origDNA.tif", "/input/origMemb.tif"), @@ -15805,7 +15807,7 @@ def source_binding_axis_size( labels = ObjectLabelSet( name="Nuclei", source_image_name="rawGFP", - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, variant_data=ObjectLabelVariantData( labels=np.stack( ( @@ -15815,7 +15817,7 @@ def source_binding_axis_size( ) ), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,), (2,)), ), ) @@ -15856,9 +15858,9 @@ def runtime_slice_axis_size(self) -> int | None: ) labels = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=label_planes), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((3,), (7,)), ), ) @@ -15875,7 +15877,7 @@ def runtime_slice_axis_size(self) -> int | None: assert isinstance(prepared.source_projected_payload, ObjectLabelPayload) assert ( - prepared.source_projected_payload.domain.scope is ObjectLabelDomainScope.PAYLOAD + prepared.source_projected_payload.domain.scope.ids_span_payload ) assert ( prepared.source_projected_payload.object_label_domain().declared_object_ids @@ -15906,9 +15908,9 @@ def test_prepared_measurement_labels_reject_implicit_image_source_identity_proje ) labels = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=label_planes), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((3,), (7,)), ), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( @@ -15943,9 +15945,9 @@ def test_prepared_measurement_labels_reject_implicit_image_set_identity_projecti ) labels = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=label_planes), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((3,), (7,)), ), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( @@ -16004,10 +16006,10 @@ def test_measurement_label_alignment_preserves_runtime_slice_payload_for_aligned ) label_payload = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) @@ -16029,10 +16031,10 @@ def test_measurement_label_alignment_preserves_runtime_slice_payload_for_dense_s labels = np.stack((label_plane, label_plane)) label_payload = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((9,), (9,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) @@ -16063,11 +16065,11 @@ def test_aligned_stack_kwargs_projects_runtime_slice_labels_to_reference_slice_d ) label_payload = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_spatial_domain=SourceSpatialDomain(source_shape_yx=(4, 4)), domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) @@ -16083,9 +16085,9 @@ def test_aligned_stack_kwargs_projects_runtime_slice_labels_to_reference_slice_d resolved["labels"].labels, np.full((2, 2), 2, dtype=np.int32), ) - assert resolved["labels"].domain.scope is ObjectLabelDomainScope.PAYLOAD + assert resolved["labels"].domain.scope.ids_span_payload assert resolved["labels"].domain.declared_object_ids == (2,) - assert resolved["labels"].plane_axis is None + assert resolved["labels"].plane_axis.unstacked assert resolved["labels"].source_spatial_domain.origin_yx == (2, 0) assert resolved["labels"].source_spatial_domain.source_shape_yx == (4, 4) @@ -16101,10 +16103,10 @@ def test_aligned_stack_kwargs_projects_runtime_slice_labels_without_source_metad ) label_payload = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) @@ -16120,9 +16122,9 @@ def test_aligned_stack_kwargs_projects_runtime_slice_labels_without_source_metad resolved["labels"].labels, np.full((4, 4), 2, dtype=np.int32), ) - assert resolved["labels"].domain.scope is ObjectLabelDomainScope.PAYLOAD + assert resolved["labels"].domain.scope.ids_span_payload assert resolved["labels"].domain.declared_object_ids == (2,) - assert resolved["labels"].plane_axis is None + assert resolved["labels"].plane_axis.unstacked def test_measurement_labels_do_not_infer_broadcast_from_equal_planes() -> None: @@ -16187,9 +16189,9 @@ def test_measurement_labels_select_source_named_plane_from_composed_image() -> N name="Nuclei", variant_data=ObjectLabelVariantData(labels=np.stack((dna_labels, gfp_labels))), source_image_name="rawDNA", - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((3,), (7,)), ), ) @@ -16197,7 +16199,7 @@ def test_measurement_labels_select_source_named_plane_from_composed_image() -> N source_image_name="rawDNA__rawGFP", source_aliases=("rawDNA", "rawGFP"), payload=ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_image_names=("rawDNA", "rawGFP"), source_image_provenance_planes=( SourceImageProvenancePlanes.from_components( @@ -16233,9 +16235,9 @@ def test_measurement_labels_select_current_source_binding_plane() -> None: name="Nuclei", variant_data=ObjectLabelVariantData(labels=label_planes), source_image_name="rawDNA", - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,), (2,), (3,)), ), ) @@ -16243,7 +16245,7 @@ def test_measurement_labels_select_current_source_binding_plane() -> None: source_image_name="rawDAPI__rawGFP__rawDNA", source_aliases=("rawDAPI", "rawGFP", "rawDNA"), payload=ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_image_names=("rawDAPI", "rawGFP", "rawDNA"), source_image_provenance_planes=( SourceImageProvenancePlanes.from_components( @@ -16282,9 +16284,9 @@ def test_measurement_labels_reject_source_binding_cardinality_mismatch() -> None name="Nuclei", variant_data=ObjectLabelVariantData(labels=label_planes), source_image_name="rawFarRed", - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,), (2,)), ), ) @@ -16292,7 +16294,7 @@ def test_measurement_labels_reject_source_binding_cardinality_mismatch() -> None source_image_name="rawDNA__rawGFP__rawFarRed", source_aliases=("rawDNA", "rawGFP", "rawFarRed"), payload=ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_image_names=("rawDNA", "rawGFP"), source_image_provenance_planes=( SourceImageProvenancePlanes.from_components( @@ -16330,9 +16332,9 @@ def test_measurement_labels_select_measurement_source_binding_over_label_origin( name="ExpandedCells", variant_data=ObjectLabelVariantData(labels=label_planes), source_image_name="rawDNA", - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((11,), (22,)), ), ) @@ -16340,7 +16342,7 @@ def test_measurement_labels_select_measurement_source_binding_over_label_origin( source_image_name="rawGFP", source_aliases=("rawGFP",), payload=ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_image_names=("rawDNA", "rawGFP"), source_image_provenance_planes=( SourceImageProvenancePlanes.from_components( @@ -16422,10 +16424,10 @@ def source_binding_axis_plane_index( name="Cells", variant_data=ObjectLabelVariantData(labels=label_planes), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,), (2,), (3,)), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) measurement_image = CellProfilerMeasurementImage( source_image_name="rawGFP", @@ -16467,9 +16469,9 @@ def source_binding_axis_plane_index( name="Cells", variant_data=ObjectLabelVariantData(labels=label_planes), source_image_name="BF_image", - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((10,), (20,)), ), ) @@ -16520,9 +16522,9 @@ def source_binding_axis_size( name="Nuclei", variant_data=ObjectLabelVariantData(labels=label_planes), source_image_name="CropBlue", - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((10,), (20,), (30,)), ), ) @@ -16533,7 +16535,7 @@ def source_binding_axis_size( payload=AlignedImageStack( tuple( ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_image_names=("CropBlue", "CropGreen"), source_image_provenance_planes=( SourceImageProvenancePlanes.from_components( @@ -16573,9 +16575,9 @@ def test_measurement_labels_project_runtime_slice_stack_for_single_plane_source( name="Cells", variant_data=ObjectLabelVariantData(labels=label_planes), source_image_name="BF_image", - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((10,), (20,)), ), ) @@ -16618,9 +16620,9 @@ def source_binding_axis_plane_index( labels = ObjectLabelSet( name="Cells", variant_data=ObjectLabelVariantData(labels=label_planes), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((10,), (20,)), ), ) @@ -16648,9 +16650,9 @@ def test_measurement_labels_project_runtime_slice_stack_for_object_domain() -> N labels = ObjectLabelSet( name="Cells", variant_data=ObjectLabelVariantData(labels=label_planes), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((10,), (20,)), ), ) @@ -16684,9 +16686,9 @@ def test_measurement_labels_reject_runtime_slice_stack_without_projection() -> N labels = ObjectLabelSet( name="Cells", variant_data=ObjectLabelVariantData(labels=label_planes), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((10,), (20,)), ), ) @@ -16717,11 +16719,11 @@ def test_measurement_labels_project_runtime_slice_stack_for_unrelated_source_ima labels = ObjectLabelSet( name="Cells", variant_data=ObjectLabelVariantData(labels=label_planes), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_name="BF_image", source_image_names=("BF_image", "MorphBf"), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((10,), (20,)), ), ) @@ -16755,10 +16757,10 @@ def test_measurement_labels_project_source_binding_stack_for_object_domain() -> labels = ObjectLabelSet( name="Cells", variant_data=ObjectLabelVariantData(labels=label_planes), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_image_names=("BF_image", "MorphBf"), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((10,), (20,)), ), ) @@ -16790,10 +16792,10 @@ def test_measurement_labels_project_repeated_source_binding_stack_for_object_dom labels = ObjectLabelSet( name="Cells", variant_data=ObjectLabelVariantData(labels=label_planes), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_image_names=("BF_image", "MorphBf"), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((10,), (10,)), ), ) @@ -16828,10 +16830,10 @@ def test_measurement_labels_reject_undeclared_nested_runtime_stack_axis() -> Non labels = ObjectLabelSet( name="Cells", variant_data=ObjectLabelVariantData(labels=grouped_stack), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_names=("BF_image", "MorphBf"), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((10, 20), (10, 20)), ), ) @@ -16877,7 +16879,7 @@ def source_binding_axis_plane_index( variant_data=ObjectLabelVariantData(labels=label_volume), source_image_name="MembFinal", domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PAYLOAD, + scope=PayloadLabelScope, ), ) measurement_image = CellProfilerMeasurementImage( @@ -16954,10 +16956,10 @@ def test_runtime_object_label_payload_ignores_measurement_image_as_selector() -> source_image_provenance_planes=SourceImageProvenancePlanes.from_components( component_metadata=({"well": "A01"}, {"well": "A01"}) ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) runtime = _FakeCellProfilerRuntime( @@ -17008,10 +17010,10 @@ def test_full_stack_object_measurement_resolves_complete_label_artifact() -> Non ) ) ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) runtime = _FakeCellProfilerRuntime( @@ -17985,10 +17987,10 @@ def test_object_measurement_execution_policy_uses_full_stack_for_source_bound_vo variant_data=ObjectLabelVariantData( labels=np.zeros((2, 3, 5, 5), dtype=np.int32) ), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) @@ -18006,7 +18008,7 @@ def test_object_measurement_execution_policy_keeps_payload_domain_labels_full_st policy = FullStackLabels labels = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=np.zeros((3, 5, 5), dtype=np.int32)), - domain=ObjectLabelDomain(scope=ObjectLabelDomainScope.PAYLOAD), + domain=ObjectLabelDomain(scope=PayloadLabelScope), ) mode = policy.image_execution_mode( @@ -18022,7 +18024,7 @@ def test_slice_aligned_measurement_preserves_singleton_aligned_image_owner() -> policy = SliceAlignedLabels labels = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=np.zeros((5, 5), dtype=np.int32)), - domain=ObjectLabelDomain(scope=ObjectLabelDomainScope.PAYLOAD), + domain=ObjectLabelDomain(scope=PayloadLabelScope), ) mode = policy.image_execution_mode( @@ -18040,10 +18042,10 @@ def test_object_measurement_execution_policy_keeps_declared_full_stack_for_plane policy = FullStackLabels labels = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=np.zeros((3, 5, 5), dtype=np.int32)), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((), (), ()), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) @@ -18119,7 +18121,7 @@ def test_convert_objects_to_image_contract_preserves_volume_label_payload() -> N labels[:, 1:4, 2:5] = 1 label_payload = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - domain=ObjectLabelDomain(scope=ObjectLabelDomainScope.PAYLOAD), + domain=ObjectLabelDomain(scope=PayloadLabelScope), ) assert ( @@ -18179,12 +18181,12 @@ def test_convert_objects_to_image_uses_declared_label_source_for_runtime_plane_d ) label_payload = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - domain=ObjectLabelDomain(scope=ObjectLabelDomainScope.PAYLOAD), + domain=ObjectLabelDomain(scope=PayloadLabelScope), source_image_provenance_planes=_convert_objects_source_planes("2"), source_spatial_domain=label_spatial_domain, ) primary_image = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=_convert_objects_source_planes("0"), source_spatial_domain=SourceSpatialDomain( origin_yx=(0, 0), @@ -18213,7 +18215,7 @@ def test_convert_objects_to_image_uses_declared_label_source_for_runtime_plane_d ) metadata = result.metadata - assert metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert metadata.plane_axis is RuntimeSlicePlaneAxis assert metadata.source_provenance == label_payload.source_provenance assert metadata.source_spatial_domain == label_spatial_domain assert StreamImagePayloadMetadataProjector.item_fields( @@ -18226,7 +18228,7 @@ def test_object_label_image_output_rejects_source_plane_count_drift() -> None: labels = np.zeros((3, 5, 7), dtype=np.int32) label_payload = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - domain=ObjectLabelDomain(scope=ObjectLabelDomainScope.PAYLOAD), + domain=ObjectLabelDomain(scope=PayloadLabelScope), source_image_provenance_planes=_convert_objects_source_planes("2"), ) source_spec = ArtifactSpec.input("Nuclei", ObjectLabelsArtifactType) @@ -18255,7 +18257,7 @@ def test_object_label_singleton_volume_output_declares_runtime_plane_axis() -> N labels = np.zeros((1, 5, 7), dtype=np.int32) label_payload = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - domain=ObjectLabelDomain(scope=ObjectLabelDomainScope.PAYLOAD), + domain=ObjectLabelDomain(scope=PayloadLabelScope), source_image_provenance_planes=_convert_objects_source_planes("2", 1), ) raw_result = convert_objects_to_image( @@ -18280,14 +18282,14 @@ def test_object_label_singleton_volume_output_declares_runtime_plane_axis() -> N ) assert result.data.shape == (1, 5, 7) - assert result.metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert result.metadata.plane_axis is RuntimeSlicePlaneAxis def test_object_label_scalar_image_output_does_not_invent_runtime_plane_axis() -> None: labels = np.zeros((5, 7), dtype=np.int32) label_payload = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - domain=ObjectLabelDomain(scope=ObjectLabelDomainScope.PAYLOAD), + domain=ObjectLabelDomain(scope=PayloadLabelScope), source_image_provenance_planes=_convert_objects_source_planes("2", 1), ) raw_result = convert_objects_to_image( @@ -18312,7 +18314,7 @@ def test_object_label_scalar_image_output_does_not_invent_runtime_plane_axis() - ) assert result.data.shape == (5, 7) - assert result.metadata.plane_axis is None + assert result.metadata.plane_axis.unstacked def test_object_measurement_execution_policy_uses_full_stack_for_single_runtime_slice_volume() -> ( @@ -18322,7 +18324,7 @@ def test_object_measurement_execution_policy_uses_full_stack_for_single_runtime_ labels = ObjectLabelSet( name="Nuclei", variant_data=ObjectLabelVariantData(labels=np.zeros((3, 5, 5), dtype=np.int32)), - domain=ObjectLabelDomain(scope=ObjectLabelDomainScope.PAYLOAD), + domain=ObjectLabelDomain(scope=PayloadLabelScope), ) mode = policy.image_execution_mode( @@ -18342,10 +18344,10 @@ def test_measure_object_size_shape_payload_runtime_slice_stack_rows_are_per_slic labels[1, 0:3, 0:3] = 1 payload = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (1,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) @@ -18374,10 +18376,10 @@ def test_measure_object_size_shape_rejects_projected_plane_with_stack_metadata() ): ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (1,), (1,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) @@ -18393,10 +18395,10 @@ def test_measure_object_size_shape_rejects_mismatched_plane_metadata() -> None: ): ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (1,), (1,), (1,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) @@ -18406,10 +18408,10 @@ def test_measure_object_size_shape_plane_scoped_singleton_uses_2d_schema() -> No labels[0, 1:4, 1:4] = 1 payload = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,),), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) @@ -18438,10 +18440,10 @@ def test_measure_object_size_shape_preserves_declared_repeated_plane_domains() - labels[1, 1] = plane payload = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (1,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) @@ -18467,7 +18469,7 @@ def test_measure_object_size_shape_payload_scoped_volume_rows_are_3d() -> None: variant_data=ObjectLabelVariantData(labels=labels), domain=ObjectLabelDomain( declared_object_ids=(1,), - scope=ObjectLabelDomainScope.PAYLOAD, + scope=PayloadLabelScope, ), ) label_set = ObjectLabelSet( @@ -18475,7 +18477,7 @@ def test_measure_object_size_shape_payload_scoped_volume_rows_are_3d() -> None: variant_data=ObjectLabelVariantData(labels=labels), domain=ObjectLabelDomain( declared_object_ids=(1,), - scope=ObjectLabelDomainScope.PAYLOAD, + scope=PayloadLabelScope, ), ) @@ -18513,10 +18515,10 @@ def test_measure_object_size_shape_plane_scoped_volume_rows_are_3d() -> None: labels[1, :, 0:3, 0:3] = 1 payload = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (1,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) @@ -18691,7 +18693,6 @@ def table_name(self, measurement_images, source_image_name): def test_natural_measurement_fallback_preserves_selected_main_flow_plane(monkeypatch): - from openhcs.core.runtime_image_values import ImagePayloadMetadataCompositionMode image_spec = ArtifactSpec.input("DNA", ImageArtifactType) object_spec = ArtifactSpec.input("Cells", ObjectLabelsArtifactType) @@ -18702,14 +18703,14 @@ def test_natural_measurement_fallback_preserves_selected_main_flow_plane(monkeyp ) for name in ("DNA", "ER") ) image = ImagePayloadMetadata.compose( - planes, mode=ImagePayloadMetadataCompositionMode.STACK + planes, mode=RuntimeSlicePlaneAxis ).payload_with(np.ones((2, 3, 4), dtype=np.float32)) labels = ObjectLabelSet( name="Cells", variant_data=ObjectLabelVariantData(labels=np.ones((2, 3, 4), dtype=np.int32)), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,), (1,)), ), ) @@ -18723,7 +18724,7 @@ def test_natural_measurement_fallback_preserves_selected_main_flow_plane(monkeyp _artifact_input_edge_for_test(object_spec), ), artifact_output_bindings=((output_spec, _artifact_output_plan(output_spec)),), - plane_projection=RuntimePlaneProjection(plane_index=0, plane_count=2), + plane_projection=RuntimePlaneProjection.selected(0, 2), ) executor = _module_executor(_compiled_callable_contract( MeasureObjectIntensityModule.require_callable(), diff --git a/tests/unit/test_cellprofiler_module_execution_semantics.py b/tests/unit/test_cellprofiler_module_execution_semantics.py index f1bfb482c6..829f6ba662 100644 --- a/tests/unit/test_cellprofiler_module_execution_semantics.py +++ b/tests/unit/test_cellprofiler_module_execution_semantics.py @@ -1,6 +1,8 @@ """Focused regressions for declaration-owned CellProfiler execution semantics.""" from __future__ import annotations +from openhcs.core.runtime_object_label_domains import PlaneLabelScope +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis import ast import inspect @@ -28,14 +30,12 @@ from openhcs.core.runtime_image_values import ImagePayloadMetadata from openhcs.core.runtime_object_label_domains import ( ObjectLabelDomain, - ObjectLabelDomainScope, ) from openhcs.core.runtime_object_labels import ( ObjectLabelPayload, ObjectLabelVariantData, object_label_dense_array, ) -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.runtime_stores import RuntimeValueStore from openhcs.core.pipeline.function_contracts import object_label_input_execution_mode_from_callable from openhcs.interop.cellprofiler.runtime.module_execution import ( @@ -248,9 +248,9 @@ def test_slice_aligned_object_measurement_consumes_declared_singleton_plane() -> variant_data=ObjectLabelVariantData( labels=np.full((1, 4, 5), 7, dtype=np.int32), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((7,),), ), ) @@ -260,9 +260,9 @@ def test_slice_aligned_object_measurement_consumes_declared_singleton_plane() -> assert isinstance(projected, ObjectLabelPayload) assert object_label_dense_array(projected).shape == (4, 5) - assert projected.object_label_domain().scope is ObjectLabelDomainScope.PAYLOAD + assert projected.object_label_domain().scope.ids_span_payload assert projected.object_label_domain().declared_object_ids == (7,) - assert projected.plane_axis is None + assert projected.plane_axis.unstacked assert ( policy.image_execution_mode( projected, @@ -277,9 +277,9 @@ def test_slice_aligned_object_measurement_rejects_full_stack_label_domain() -> N variant_data=ObjectLabelVariantData( labels=np.ones((2, 4, 5), dtype=np.int32), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,), (1,)), ), ) diff --git a/tests/unit/test_cellprofiler_object_label_payload_inputs.py b/tests/unit/test_cellprofiler_object_label_payload_inputs.py index 87c19a6aff..e8bf684e08 100644 --- a/tests/unit/test_cellprofiler_object_label_payload_inputs.py +++ b/tests/unit/test_cellprofiler_object_label_payload_inputs.py @@ -20,7 +20,7 @@ ObjectLabelVariantData, ObjectLabelPayload, ) -from openhcs.core.runtime_image_values import array_data_of +from openhcs.core.runtime_image_values import as_runtime_value def test_area_occupied_accepts_object_label_payload_input() -> None: @@ -84,7 +84,7 @@ def test_convert_objects_to_image_accepts_object_label_payload_input() -> None: ) # The rendered image carries the labels' image context, so it is a payload. - pixels = array_data_of(converted) + pixels = as_runtime_value(converted).array_data assert pixels.dtype == np.float32 assert pixels.sum() == 1.0 diff --git a/tests/unit/test_cellprofiler_output_recording.py b/tests/unit/test_cellprofiler_output_recording.py index bbfdef5476..81f20e1e7f 100644 --- a/tests/unit/test_cellprofiler_output_recording.py +++ b/tests/unit/test_cellprofiler_output_recording.py @@ -1,3 +1,6 @@ +from openhcs.core.runtime_plane_projection import NoPlaneAxis +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis +from openhcs.core.runtime_plane_projection import SourceBindingPlaneAxis from types import SimpleNamespace from unittest.mock import Mock @@ -168,22 +171,22 @@ def test_relationship_recording_uses_exact_artifact_relation() -> None: @pytest.mark.parametrize( ("plane_projection", "output_shape", "expected_plane_axis"), ( - (None, (4, 5, 3), None), + (None, (4, 5, 3), NoPlaneAxis), ( RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=1, ), (1, 4, 5, 3), - RuntimePlaneAxis.RUNTIME_SLICE, + RuntimeSlicePlaneAxis, ), ( RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.SOURCE_BINDING, + axis=SourceBindingPlaneAxis, axis_size=3, ), (3, 4, 5, 3), - RuntimePlaneAxis.RUNTIME_SLICE, + RuntimeSlicePlaneAxis, ), ), ) @@ -191,7 +194,7 @@ def test_image_output_recording_uses_exact_invocation_projection_for_rgb( monkeypatch: pytest.MonkeyPatch, plane_projection: RuntimePlaneAxisValueProjection | None, output_shape: tuple[int, ...], - expected_plane_axis: RuntimePlaneAxis | None, + expected_plane_axis: type[RuntimePlaneAxis], ) -> None: measured_objects = ArtifactSpec.input("Cells", ObjectLabelsArtifactType) output = ArtifactSpec.output( @@ -268,7 +271,7 @@ def test_output_recording_carries_exact_invocation_plane_projection( adapter.request = SimpleNamespace(artifact_outputs={output_plan.ref(): output_plan}) image = np.zeros((2, 2), dtype=np.float32) invocation_projection = RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ) recorder = Mock() diff --git a/tests/unit/test_cellprofiler_runtime_adapter.py b/tests/unit/test_cellprofiler_runtime_adapter.py index f6575b5e8b..4673958403 100644 --- a/tests/unit/test_cellprofiler_runtime_adapter.py +++ b/tests/unit/test_cellprofiler_runtime_adapter.py @@ -1,3 +1,6 @@ +from openhcs.core.runtime_object_label_domains import PayloadLabelScope, PlaneLabelScope +from openhcs.core.runtime_plane_projection import NoPlaneAxis +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis import importlib from collections.abc import Callable, Mapping from dataclasses import dataclass, replace @@ -90,7 +93,6 @@ ) from openhcs.core.runtime_object_label_domains import ( ObjectLabelDomain, - ObjectLabelDomainScope, ) from openhcs.core.runtime_object_labels import ( ObjectLabelRepresentation, @@ -405,7 +407,7 @@ def measurement_values_for_label_slices( labels: object, *, domain: ObjectLabelDomain, - plane_axis: RuntimePlaneAxis | None, + plane_axis: type[RuntimePlaneAxis], plane_projector: RuntimePlaneProjection, object_name: str | None = None, row_axis: MeasurementRowAxisField = MeasurementRowAxisField.SLICE_INDEX, @@ -435,7 +437,7 @@ def adapter_label_measurement_values( labels: object, *, domain: ObjectLabelDomain, - plane_axis: RuntimePlaneAxis | None, + plane_axis: type[RuntimePlaneAxis], slice_index: int | None = None, ) -> tuple[object, ...]: return ObjectLabelMeasurementSliceRequest( @@ -769,7 +771,7 @@ def _source_bound_image_stack(images): paths=tuple(f"/src/{alias}.tif" for alias in aliases), ), source_image_names=aliases, - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, ).payload_with(np.stack(tuple(images.values())), None) @@ -1145,10 +1147,10 @@ def test_cellprofiler_adapter_contextualizes_single_source_aligned_label_plane() labels = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=np.ones((1, 5, 6), dtype=np.int32)), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,),), ), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, ) record = adapter.add_objects( @@ -1215,15 +1217,15 @@ def test_cellprofiler_adapter_rejects_unaddressable_source_aligned_label_stack() source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=(None, None) ), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, ).payload_with(np.zeros((2, 5, 6), dtype=np.float32), None) labels = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=np.ones((2, 5, 6), dtype=np.int32)), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,), (1,)), ), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, ) with pytest.raises(ValueError, match="neither source_path"): @@ -1989,7 +1991,7 @@ def test_cellprofiler_adapter_projects_source_bound_runtime_image_to_group_plane source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=source_paths, component_metadata=source_metadata ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with( np.stack( ( @@ -2090,7 +2092,7 @@ def test_cellprofiler_adapter_deduplicates_grouped_runtime_image_input_locations source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=source_paths, component_metadata=source_metadata ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with( np.stack( ( @@ -2455,7 +2457,7 @@ def test_cellprofiler_adapter_projects_stack_without_replacing_artifact_provenan {"site": "2", "channel": "1"}, ), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with( np.stack( ( @@ -2773,10 +2775,10 @@ def test_cellprofiler_adapter_does_not_project_stacked_objects_from_source_metad ), source_image_names=(DNA_IMAGE,), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,), (2,)), ), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, ), ) ImagePayloadMetadata( @@ -3138,7 +3140,7 @@ def test_cellprofiler_adapter_discovers_single_realized_dynamic_grouped_input(): objects = consumer.get_objects(NUCLEI) np.testing.assert_array_equal(objects.labels, realized_labels[None, ...]) - assert objects.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert objects.plane_axis is RuntimeSlicePlaneAxis assert objects.domain.declared_object_id_domains == ((1,),) # A dynamic component never grants access to another compiled producer path. @@ -3457,10 +3459,10 @@ def test_cellprofiler_adapter_preserves_ungrouped_runtime_slice_output_stack(): ) ) ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) @@ -4838,10 +4840,10 @@ def test_cellprofiler_adapter_aligns_multiplane_measurements_across_groups(): "Intensity_MeanIntensity_rawGFP", labels, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,), (1,)), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) np.testing.assert_allclose(values[0], [5.0]) @@ -4930,10 +4932,10 @@ def test_cellprofiler_adapter_rejects_single_slice_measurements_for_repeated_lab "Intensity_MeanIntensity_rawGFP", labels, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,), (1,), (1,)), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) @@ -5265,10 +5267,10 @@ def test_cellprofiler_adapter_multiplane_measurement_alignment_is_feature_scoped "Intensity_MeanIntensity_rawGFP", np.array([[[1]], [[1]]], dtype=np.int32), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,), (1,)), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) np.testing.assert_allclose(values[0], [5.0]) @@ -5305,7 +5307,7 @@ def test_cellprofiler_adapter_measurement_query_cache_is_store_scoped(): ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), domain=ObjectLabelDomain(declared_object_ids=(1, 2)), - plane_axis=None, + plane_axis=NoPlaneAxis, ), ) producer.add_measurements( @@ -5376,10 +5378,10 @@ def test_cellprofiler_adapter_measurement_query_cache_is_store_scoped(): "AreaShape_Area", labels[np.newaxis, ...], domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1, 2),), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, )[0], [20.0, 80.0], ) @@ -5390,10 +5392,10 @@ def test_cellprofiler_adapter_measurement_query_cache_is_store_scoped(): "AreaShape_Area", labels[np.newaxis, ...], domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1, 2),), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, )[0], [4.0, 12.0], ) @@ -5434,7 +5436,7 @@ def test_label_measurement_cache_distinguishes_single_plane_from_stack() -> None labels=np.array([[1, 2]], dtype=np.int32) ), domain=ObjectLabelDomain(declared_object_ids=(1, 2)), - plane_axis=None, + plane_axis=NoPlaneAxis, ), ) producer.add_measurements( @@ -5583,10 +5585,10 @@ def test_label_measurement_cache_distinguishes_single_plane_from_stack() -> None "AreaShape_Area", first_plane, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1, 2),), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) stack_values = adapter_label_measurement_values( stack_adapter, @@ -5594,10 +5596,10 @@ def test_label_measurement_cache_distinguishes_single_plane_from_stack() -> None "AreaShape_Area", label_stack, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1, 2), (1,)), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) np.testing.assert_allclose(single_plane_values[0], [10.0, 20.0]) @@ -5692,10 +5694,10 @@ def test_cellprofiler_adapter_projects_duplicate_object_labels_to_current_runtim "AreaShape_Area", labels, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PAYLOAD, + scope=PayloadLabelScope, declared_object_ids=(1, 2), ), - plane_axis=None, + plane_axis=NoPlaneAxis, ) assert len(value_slices) == 1 @@ -6202,17 +6204,17 @@ def test_cellprofiler_object_only_pure_2d_module_executes_label_runtime_slices( labels = ObjectLabelSet( name=NUCLEI, variant_data=ObjectLabelVariantData(labels=label_planes), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("/src/Nuclei_t0.tif", "/src/Nuclei_t1.tif"), ), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,), (2,)), ), ) image = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.zeros((2, 20, 21), dtype=np.float32), None) seen: list[tuple[tuple[int, ...], int]] = [] adapter.add_objects(NUCLEI, labels) @@ -6298,14 +6300,14 @@ def test_cellprofiler_object_only_full_stack_measurement_preserves_label_runtime labels = ObjectLabelSet( name=NUCLEI, variant_data=ObjectLabelVariantData(labels=label_planes), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,), (2,)), ), ) image = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.zeros((2, 20, 21), dtype=np.float32), None) seen: list[tuple[int, ...]] = [] adapter.add_objects(NUCLEI, labels) @@ -6839,10 +6841,10 @@ def test_measurement_lookup_aligns_values_to_label_slices(): dtype=np.int32, ), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((10, 20), (30,)), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, plane_projector=RuntimePlaneProjection.stack(2), object_name=NUCLEI, dialect=CELLPROFILER_MEASUREMENT_DIALECT, @@ -6900,10 +6902,10 @@ def test_measurement_lookup_does_not_stop_on_broad_cellprofiler_alias(): "Intensity_MaxIntensity_OrigGreen", np.array([[[1, 2]]], dtype=np.int32), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1, 2),), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, plane_projector=RuntimePlaneProjection.stack(1), object_name=NUCLEI, ) @@ -7180,10 +7182,10 @@ def test_adapter_feature_lookup_rejects_undeclared_measurement_output(): name=NUCLEI, variant_data=ObjectLabelVariantData(labels=labels), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1, 2),), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ), ) adapter.add_measurements( @@ -7230,10 +7232,10 @@ def test_adapter_feature_lookup_rejects_undeclared_measurement_output(): "Intensity_MaxIntensity_OrigGreen", labels, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1, 2),), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) @@ -7262,10 +7264,10 @@ def test_adapter_measurement_vector_scope_uses_feature_bearing_axis_projection() name=NUCLEI, variant_data=ObjectLabelVariantData(labels=labels), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1, 2),), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ), ) producer.add_measurements( @@ -7332,10 +7334,10 @@ def test_adapter_measurement_vector_scope_uses_feature_bearing_axis_projection() "Intensity_MaxIntensity_OrigGreen", labels, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1, 2),), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, slice_index=99, ) @@ -7373,10 +7375,10 @@ def test_adapter_multiplane_label_lookup_prefers_runtime_slice_axis(): name=NUCLEI, variant_data=ObjectLabelVariantData(labels=labels), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,), (2,)), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ), ) producer.add_measurements( @@ -7443,10 +7445,10 @@ def test_adapter_multiplane_label_lookup_prefers_runtime_slice_axis(): "Intensity_MaxIntensity_OrigGreen", labels, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,), (2,)), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, slice_index=99, ) @@ -7488,10 +7490,10 @@ def test_adapter_measurement_vector_scope_rejects_undeclared_axis_table(): name=NUCLEI, variant_data=ObjectLabelVariantData(labels=labels), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1, 2),), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ), ) producer.add_measurements( @@ -7535,10 +7537,10 @@ def test_adapter_measurement_vector_scope_rejects_undeclared_axis_table(): "Intensity_MaxIntensity_OrigGreen", labels, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1, 2),), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) @@ -7794,10 +7796,10 @@ def test_measurement_lookup_allows_empty_multiplane_label_planes(): "Intensity_MeanIntensity_rawGFP", labels, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((), (1, 2)), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, plane_projector=RuntimePlaneProjection.stack(2), object_name=NUCLEI, dialect=CELLPROFILER_MEASUREMENT_DIALECT, @@ -7846,10 +7848,10 @@ def test_measurement_lookup_rejects_singleton_columnar_slice_for_label_stack(): dtype=np.int32, ), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,), (2,)), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, plane_projector=RuntimePlaneProjection.stack(2), object_name=NUCLEI, dialect=CELLPROFILER_MEASUREMENT_DIALECT, @@ -7900,10 +7902,10 @@ def test_measurement_lookup_rejects_singleton_indexed_slice_for_label_stack(): dtype=np.int32, ), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,), (2,)), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, plane_projector=RuntimePlaneProjection.stack(2), object_name=NUCLEI, dialect=CELLPROFILER_MEASUREMENT_DIALECT, @@ -7954,10 +7956,10 @@ def test_measurement_lookup_rejects_shifted_slice_domain_for_local_label_stack() dtype=np.int32, ), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,), (2,)), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, plane_projector=RuntimePlaneProjection.stack(2), object_name=NUCLEI, dialect=CELLPROFILER_MEASUREMENT_DIALECT, @@ -8003,10 +8005,10 @@ def test_measurement_lookup_rejects_smaller_axis_domain_for_label_stack(): dtype=np.int32, ), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,), (1,), (1,), (1,)), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, plane_projector=RuntimePlaneProjection.stack(4), object_name=NUCLEI, dialect=CELLPROFILER_MEASUREMENT_DIALECT, @@ -8019,10 +8021,10 @@ def test_measurement_lookup_returns_empty_slices_for_empty_objects(): "AreaShape_FormFactor", np.zeros((2, 3, 4), dtype=np.int32), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((), ()), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, plane_projector=RuntimePlaneProjection.stack(2), object_name=NUCLEI, ) @@ -8038,10 +8040,10 @@ def test_measurement_lookup_rejects_undeclared_row_axis_for_nonempty_objects(): "AreaShape_FormFactor", np.array([[[1, 0], [0, 0]]], dtype=np.int32), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,),), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, plane_projector=RuntimePlaneProjection.stack(1), object_name=NUCLEI, ) @@ -8083,10 +8085,10 @@ def test_calculate_math_records_object_indexed_measurements(): name=NUCLEI, variant_data=ObjectLabelVariantData(labels=labels), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1, 2),), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("/src/Nuclei.tif",), ), @@ -8216,10 +8218,10 @@ def test_calculate_math_pads_missing_same_object_operand_values(): name=NUCLEI, variant_data=ObjectLabelVariantData(labels=labels), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1, 2),), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("/src/Nuclei.tif",), ), @@ -8529,10 +8531,10 @@ def test_classify_objects_binds_runtime_measurement_values(): name=NUCLEI, variant_data=ObjectLabelVariantData(labels=labels), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1, 2),), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("/src/Nuclei.tif",), ), @@ -8590,7 +8592,7 @@ def test_classify_objects_binds_runtime_measurement_values(): result = executor( ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.zeros((1, 2, 2), dtype=np.float32), None), cellprofiler_runtime=adapter, measurement_feature="Math_Ratio", @@ -8653,10 +8655,10 @@ def test_classify_objects_uses_declared_area_shape_measurements(): name=NUCLEI, variant_data=ObjectLabelVariantData(labels=labels), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1, 2),), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("/src/Nuclei.tif",), ), @@ -8714,7 +8716,7 @@ def test_classify_objects_uses_declared_area_shape_measurements(): executor( ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.zeros((1, 2, 2), dtype=np.float32), None), cellprofiler_runtime=adapter, measurement_feature="AreaShape_Area", @@ -8764,10 +8766,10 @@ def test_classify_objects_binds_custom_threshold_and_named_low_high_bins(): name=NUCLEI, variant_data=ObjectLabelVariantData(labels=labels), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1, 2, 3),), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("/src/Nuclei.tif",), ), @@ -8832,7 +8834,7 @@ def test_classify_objects_binds_custom_threshold_and_named_low_high_bins(): executor( ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.zeros((1, 2, 2), dtype=np.float32), None), cellprofiler_runtime=adapter, measurement_feature="Intensity_MaxIntensity_OrigGreen", @@ -8900,10 +8902,10 @@ def test_classify_objects_binds_repeated_single_measurement_rules(): name=NUCLEI, variant_data=ObjectLabelVariantData(labels=labels), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1, 2),), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("/src/Nuclei.tif",), ), @@ -8975,7 +8977,7 @@ def test_classify_objects_binds_repeated_single_measurement_rules(): executor( ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.zeros((1, 2, 2), dtype=np.float32), None), cellprofiler_runtime=adapter, classification_rules=( @@ -9068,9 +9070,9 @@ def test_classify_objects_slices_runtime_measurements_with_label_stack(): variant_data=ObjectLabelVariantData(labels=labels), domain=ObjectLabelDomain( declared_object_id_domains=((1, 2), (3, 4)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("/src/Nuclei_t0.tif", "/src/Nuclei_t1.tif"), ), @@ -9120,7 +9122,7 @@ def test_classify_objects_slices_runtime_measurements_with_label_stack(): result = executor( ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.zeros((2, 2, 2), dtype=np.float32), None), cellprofiler_runtime=adapter, measurement_feature="AreaShape_Area", diff --git a/tests/unit/test_cellprofiler_runtime_cache_authorities.py b/tests/unit/test_cellprofiler_runtime_cache_authorities.py index f4d88b649b..3e33294b48 100644 --- a/tests/unit/test_cellprofiler_runtime_cache_authorities.py +++ b/tests/unit/test_cellprofiler_runtime_cache_authorities.py @@ -1,6 +1,7 @@ """Focused tests for retained CellProfiler runtime cache authorities.""" from __future__ import annotations +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from dataclasses import replace from types import SimpleNamespace @@ -39,9 +40,6 @@ ) from openhcs.core.runtime_measurements import MeasurementScope, MeasurementSubject from openhcs.core.runtime_object_label_domains import ObjectLabelDomain -from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, -) from openhcs.core.runtime_relationships import DirectedObjectRelationshipPayload from openhcs.core.runtime_tabular_values import FieldSpec from openhcs.core.runtime_stores import RuntimeValueStore @@ -96,7 +94,7 @@ def test_runtime_adapter_recomposes_images_from_runtime_value_store() -> None: ) producer_pixels = np.full((2, 2), 1.0, dtype=np.float32) first_payload = AlignedImageStack.from_output_slices( - (producer_pixels,), memory_type="numpy", plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + (producer_pixels,), memory_type="numpy", plane_axis=RuntimeSlicePlaneAxis, ) first_value = RuntimeValue.normalize( output_plan, @@ -119,7 +117,7 @@ def test_runtime_adapter_recomposes_images_from_runtime_value_store() -> None: ) replacement_payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE + plane_axis=RuntimeSlicePlaneAxis ).payload_with(np.full((1, 2, 2), 2.0, dtype=np.float32), None) replacement_value = RuntimeValue.normalize( output_plan, diff --git a/tests/unit/test_cellprofiler_runtime_input_binding_contract.py b/tests/unit/test_cellprofiler_runtime_input_binding_contract.py index 6bad72b555..cf548b606e 100644 --- a/tests/unit/test_cellprofiler_runtime_input_binding_contract.py +++ b/tests/unit/test_cellprofiler_runtime_input_binding_contract.py @@ -1,6 +1,7 @@ """Focused gates for the CellProfiler runtime input-binding boundary.""" from __future__ import annotations +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from dataclasses import replace from pathlib import Path @@ -28,7 +29,6 @@ from openhcs.core.runtime_artifact_values import RuntimeValue from openhcs.core.runtime_image_values import ImagePayloadMetadata from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneProjection, ) from openhcs.core.runtime_stores import RuntimeValueStore @@ -257,7 +257,7 @@ def consume(image: np.ndarray, mask: np.ndarray) -> np.ndarray: mask_metadata = ImagePayloadMetadata( source_image_names=(mask.name,), source_image_provenance_planes=mask_provenance, - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) mask_payload = mask_metadata.payload_with( np.stack( @@ -291,7 +291,7 @@ def consume(image: np.ndarray, mask: np.ndarray) -> np.ndarray: paths=tuple(f"/plate/A01_s1_w0_z{index + 1:03}.tif" for index in range(3)), component_metadata=source_plane_metadata, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.zeros((3, 2, 2), dtype=np.float32), None) source_binding_plan = CompiledSourceBindingPlan( source_stack_components=( @@ -417,7 +417,7 @@ def consume(image: np.ndarray, mask: np.ndarray) -> np.ndarray: }, ), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.ones((1, 2, 2), dtype=np.float32), None) store = RuntimeValueStore() store.replace( @@ -445,7 +445,7 @@ def consume(image: np.ndarray, mask: np.ndarray) -> np.ndarray: paths=tuple(f"/plate/A01_s1_w0_z{index + 1:03}.tif" for index in range(3)), component_metadata=source_plane_metadata, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.zeros((3, 2, 2), dtype=np.float32), None) adapter = cellprofiler_runtime_adapter_for_test( runtime_value_store=store, diff --git a/tests/unit/test_cellprofiler_save_images.py b/tests/unit/test_cellprofiler_save_images.py index 9ee9a1561b..07267c2e7e 100644 --- a/tests/unit/test_cellprofiler_save_images.py +++ b/tests/unit/test_cellprofiler_save_images.py @@ -1,6 +1,7 @@ """Focused contracts for executable CellProfiler SaveImages support.""" from __future__ import annotations +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis import inspect from pathlib import PurePosixPath @@ -39,7 +40,6 @@ ) from openhcs.core.runtime_slice_alignment import RuntimeSliceAlignedValues from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisValueProjection, ) from openhcs.core.source_bindings import ( @@ -513,7 +513,7 @@ def test_save_images_projects_declared_singleton_runtime_slice() -> None: assert isinstance(materialization, MaterializationSpec) payload = ImageMetadataPayload( np.ones((1, 3, 4), dtype=np.uint8), - ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE), + ImagePayloadMetadata(plane_axis=RuntimeSlicePlaneAxis), ) filemanager = FileManager({"memory": MemoryStorageBackend()}) @@ -964,7 +964,7 @@ def test_image_output_context_projects_declared_group_lineage_source() -> None: np.zeros((3, 2, 2, 3), dtype=np.uint8), output_plan or MAIN_FLOW, RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=3, ), ) @@ -1002,7 +1002,7 @@ def test_declared_group_lineage_source_overrides_complete_output_identity() -> N ) complete_output = ImagePayloadMetadata( axes=PayloadAxes.colour_samples(-1), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_names=tuple(alias for _site in range(1, 4) for alias in aliases), source_image_provenance_planes=( SourceImageProvenancePlanes.from_components( @@ -1039,7 +1039,7 @@ def test_declared_group_lineage_source_overrides_complete_output_identity() -> N complete_output, output_plan or MAIN_FLOW, RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=3, ), ) @@ -1082,13 +1082,13 @@ def test_planned_image_output_restores_axis_without_duplicate_provenance() -> No source_payload, output_plan or MAIN_FLOW, RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=plane_count, ), ) contextualized_metadata = contextualized.metadata - assert contextualized_metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert contextualized_metadata.plane_axis is RuntimeSlicePlaneAxis assert contextualized_metadata.source_image_provenance_planes.paths == ( metadata.source_image_provenance_planes.paths ) @@ -1101,7 +1101,7 @@ def test_planned_image_output_restores_axis_without_duplicate_provenance() -> No def test_image_output_context_removes_selected_source_runtime_axis() -> None: source_payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_names=("OrigRed", "OrigGreen", "OrigBlue"), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("/source/red.tif", "/source/green.tif", "/source/blue.tif"), @@ -1130,13 +1130,13 @@ def test_image_output_context_removes_selected_source_runtime_axis() -> None: np.zeros((2, 2, 3), dtype=np.uint8), output_plan or MAIN_FLOW, RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=3, ), ) metadata = contextualized.metadata - assert metadata.plane_axis is None + assert metadata.plane_axis.unstacked assert metadata.source_path == "/source/blue.tif" @@ -1167,7 +1167,7 @@ def test_image_output_context_preserves_repeated_declared_source_planes() -> Non np.zeros((2, 2, 2), dtype=np.uint8), output_plan or MAIN_FLOW, RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), ) diff --git a/tests/unit/test_cellprofiler_strategy_registries.py b/tests/unit/test_cellprofiler_strategy_registries.py index b928115d0e..64a4b25ba9 100644 --- a/tests/unit/test_cellprofiler_strategy_registries.py +++ b/tests/unit/test_cellprofiler_strategy_registries.py @@ -1,5 +1,7 @@ """Regression tests for JSON-safe CellProfiler compatibility registries.""" +from openhcs.core.runtime_object_label_domains import PayloadLabelScope, PlaneLabelScope +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis import numpy as np from openhcs.processing.backends.cellprofiler.alignment import AlignCropModeStrategy @@ -54,9 +56,7 @@ ) from openhcs.core.runtime_object_label_domains import ( ObjectLabelDomain, - ObjectLabelDomainScope, ) -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.runtime_object_labels import ( ObjectLabelVariantData, ObjectLabelPayload, @@ -171,10 +171,10 @@ def test_measurement_execution_mode_follows_callable_declaration(): labels = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=np.zeros((3, 8, 8), dtype=np.int32)), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((), (), ()), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) @@ -198,10 +198,10 @@ def test_measurement_execution_mode_follows_callable_declaration(): variant_data=ObjectLabelVariantData( labels=np.zeros((3, 8, 8), dtype=np.int32) ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((), (), ()), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ), NaturalExecution, @@ -215,10 +215,10 @@ def test_measurement_execution_mode_follows_callable_declaration(): variant_data=ObjectLabelVariantData( labels=np.zeros((3, 8, 8), dtype=np.int32) ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((), (), ()), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ), FullStackExecution, @@ -232,10 +232,10 @@ def test_measurement_execution_mode_follows_callable_declaration(): variant_data=ObjectLabelVariantData( labels=np.zeros((3, 8, 8), dtype=np.int32) ), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((), (), ()), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ), FullStackExecution, @@ -270,7 +270,7 @@ def test_slice_aligned_measurement_preserves_payload_scoped_volume() -> None: policy = SliceAlignedLabels labels = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=np.zeros((3, 8, 8), dtype=np.int32)), - domain=ObjectLabelDomain(scope=ObjectLabelDomainScope.PAYLOAD), + domain=ObjectLabelDomain(scope=PayloadLabelScope), ) assert ( diff --git a/tests/unit/test_cellprofiler_trackobjects.py b/tests/unit/test_cellprofiler_trackobjects.py index 2a2dd26b64..ef2a186d12 100644 --- a/tests/unit/test_cellprofiler_trackobjects.py +++ b/tests/unit/test_cellprofiler_trackobjects.py @@ -1,3 +1,5 @@ +from openhcs.core.runtime_object_label_domains import PlaneLabelScope +from openhcs.core.runtime_plane_projection import SourceBindingPlaneAxis from openhcs.core.artifacts.plans import ArtifactInputPlan, ArtifactOutputPlan from openhcs.core.artifacts.specs import ArtifactSpec, ArtifactSpecCollection from openhcs.core.artifacts.kinds import ImageArtifactType, ObjectLabelsArtifactType @@ -13,11 +15,9 @@ MeasurementRowValueField, ) from openhcs.core.runtime_object_label_domains import ( - ObjectLabelDomainScope, PresentObjectLabelIdsDomainDeclaration, ) from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisValueProjection, ) from openhcs.core.runtime_object_labels import ( @@ -71,14 +71,14 @@ def _projected_measurement_rows(result, *, object_name="Cells"): def _timepoint_labels(labels: np.ndarray, *, axis_size: int) -> ObjectLabelPayload: projection = RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.SOURCE_BINDING, + axis=SourceBindingPlaneAxis, axis_size=axis_size, ) return ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, domain=PresentObjectLabelIdsDomainDeclaration( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, plane_projection=projection, ).declared_domain(None, labels), ) @@ -633,7 +633,7 @@ def test_retained_tracking_image_uses_track_palette_and_preserves_measurements() metadata = retained.output_image.metadata assert pixels.shape == (2, 32, 40, 3) assert pixels.dtype == np.float32 - assert metadata.plane_axis is RuntimePlaneAxis.SOURCE_BINDING + assert metadata.plane_axis is SourceBindingPlaneAxis assert metadata.axis_position(ColourAxis) == -1 assert metadata.unit_interval_intensity_scale == 255 np.testing.assert_array_equal(pixels[:, 0, 0], np.zeros((2, 3))) diff --git a/tests/unit/test_cellprofiler_uncovered_object_output_contracts.py b/tests/unit/test_cellprofiler_uncovered_object_output_contracts.py index 7100c6236d..0d196a99dc 100644 --- a/tests/unit/test_cellprofiler_uncovered_object_output_contracts.py +++ b/tests/unit/test_cellprofiler_uncovered_object_output_contracts.py @@ -1,6 +1,7 @@ """Focused contracts for uncovered CellProfiler object-producing leaves.""" from __future__ import annotations +from openhcs.core.runtime_plane_projection import RuntimePlaneProjection import inspect from pathlib import Path @@ -316,7 +317,7 @@ def test_identify_dead_worms_projects_exact_native_measurement_features() -> Non object_label_output_domain_scope=lambda: None, adapter=SimpleNamespace( request=SimpleNamespace( - plane_projection=SimpleNamespace(plane_index=None) + plane_projection=RuntimePlaneProjection.stack() ) ), measurement_source_metadata=lambda _specs: label_payload.metadata, diff --git a/tests/unit/test_cellprofiler_watershed_3d.py b/tests/unit/test_cellprofiler_watershed_3d.py index 1b3375b793..3b397a1007 100644 --- a/tests/unit/test_cellprofiler_watershed_3d.py +++ b/tests/unit/test_cellprofiler_watershed_3d.py @@ -4,7 +4,6 @@ import pytest from openhcs.core.runtime_object_labels import object_label_dense_array -from openhcs.core.runtime_object_label_domains import ObjectLabelDomainScope from openhcs.processing.backends.cellprofiler.watershed import ( WatershedMethod, watershed_cellprofiler4, @@ -29,7 +28,7 @@ def test_marker_watershed_3d_defaults_to_thresholded_image_domain() -> None: np.testing.assert_array_equal(labels > 0, image) assert np.count_nonzero(labels[~image]) == 0 assert np.unique(labels).tolist() == [0, 1, 2] - assert label_value.domain.scope is ObjectLabelDomainScope.PAYLOAD + assert label_value.domain.scope.ids_span_payload assert label_value.domain.declared_object_ids == (1, 2) (stats_row,) = stats.row_mappings() assert stats_row["object_count"] == 2 diff --git a/tests/unit/test_cellprofiler_watershed_residual_hotpath.py b/tests/unit/test_cellprofiler_watershed_residual_hotpath.py index f32485ec8e..f8cfb549c7 100644 --- a/tests/unit/test_cellprofiler_watershed_residual_hotpath.py +++ b/tests/unit/test_cellprofiler_watershed_residual_hotpath.py @@ -8,7 +8,6 @@ from openhcs.processing.backends.cellprofiler.morphology import MorphologyBackendStrategy from openhcs.core.runtime_image_values import ImageMetadataPayload, ImagePayloadMetadata -from openhcs.core.runtime_object_label_domains import ObjectLabelDomainScope from openhcs.core.runtime_object_labels import object_label_dense_array from openhcs.processing.backends.cellprofiler.structuring_elements import ( StructuringElement, @@ -54,7 +53,7 @@ def fail_external_call(*_args: object, **_kwargs: object) -> None: assert labels.shape == image.shape assert labels.dtype == np.int32 assert not np.any(labels) - assert label_value.domain.scope is ObjectLabelDomainScope.PAYLOAD + assert label_value.domain.scope.ids_span_payload assert label_value.domain.declared_object_count == 0 assert label_value.domain.declared_object_ids == () assert label_value.source_provenance == metadata.source_provenance @@ -147,7 +146,7 @@ def test_nonempty_sparse_high_markers_preserve_connectivity_reference( object_count = int(expected_labels.max(initial=0)) np.testing.assert_array_equal(public_labels, expected_labels) assert public_labels.dtype == np.int32 - assert label_value.domain.scope is ObjectLabelDomainScope.PAYLOAD + assert label_value.domain.scope.ids_span_payload assert label_value.domain.explicit_id_domain() == tuple(range(1, object_count + 1)) assert rows.row_mappings() == ( { diff --git a/tests/unit/test_child_measurement_producer_selection.py b/tests/unit/test_child_measurement_producer_selection.py index 1bbc93e550..b46c6255f7 100644 --- a/tests/unit/test_child_measurement_producer_selection.py +++ b/tests/unit/test_child_measurement_producer_selection.py @@ -1,5 +1,7 @@ """Prior child measurements select their producer domain, not the child channel.""" +from openhcs.core.runtime_object_label_domains import PlaneLabelScope +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from dataclasses import replace import pytest @@ -317,10 +319,8 @@ def _upstream_rows(*, sites=("1", "2"), well="A01", time="1"): ) from openhcs.core.runtime_object_label_domains import ( ObjectLabelDomain, - ObjectLabelDomainScope, ) from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneProjection, ) from openhcs.core.function_patterns import InvocationArtifactInputEdgePlan @@ -361,10 +361,10 @@ def _upstream_rows(*, sites=("1", "2"), well="A01", time="1"): labels=np.array([[[1, 2], [0, 0]], [[1, 2], [0, 0]]], dtype=np.int32) ), domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1, 2), (1, 2)), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=child_planes, ) storage = ArtifactInputPlan( @@ -451,7 +451,7 @@ def _upstream_rows(*, sites=("1", "2"), well="A01", time="1"): backend="memory", ) image = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=child_planes, ).payload_with(np.ones((2, 2, 2), dtype=np.float32)) bindings = CompiledSourceBindingPlan( diff --git a/tests/unit/test_collapsed_persisted_occurrence_435.py b/tests/unit/test_collapsed_persisted_occurrence_435.py index aeea7e02a3..31df40d4e8 100644 --- a/tests/unit/test_collapsed_persisted_occurrence_435.py +++ b/tests/unit/test_collapsed_persisted_occurrence_435.py @@ -320,7 +320,7 @@ def test_collapsed_same_path_mirror_retains_filename_and_semantic_views(tmp_path assert "site" not in metadata.source_component_metadata assert len(metadata.source_provenance.represented_source_identities) == 2 assert metadata.source_voxel_spacing == SourceVoxelSpacing((0.5, 0.5)) - assert metadata.plane_axis is None + assert metadata.plane_axis.unstacked target.write(context, produced_plan=plan) document = json.loads((tmp_path / "openhcs_metadata.json").read_text()) reopened = VirtualWorkspaceSourceProjection.from_openhcs_metadata( diff --git a/tests/unit/test_debug_runtime.py b/tests/unit/test_debug_runtime.py index 3e6f1f8670..41925cc15d 100644 --- a/tests/unit/test_debug_runtime.py +++ b/tests/unit/test_debug_runtime.py @@ -1,3 +1,4 @@ +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis import ast import json import threading @@ -91,9 +92,6 @@ ObjectRelationship, ObjectRelationshipDeclaration, ) -from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, -) from openhcs.core.runtime_slice_alignment import RuntimeSliceAlignedValues from openhcs.core.runtime_object_labels import ( ObjectLabelPayload, @@ -717,7 +715,7 @@ def test_before_invocation_observes_exact_bound_call_boundary(): context = DebugExecutionContextStub(debug_event_sink=sink) main_image = ImagePayloadMetadata( source_image_names=("Orig",), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.arange(6, dtype=np.uint16).reshape(1, 2, 3), None) auxiliary_output = ArtifactOutputPlan( name="auxiliary", @@ -1066,7 +1064,7 @@ def test_projection_exception_contains_complete_invocation_context_without_debug ) image = ImagePayloadMetadata( source_image_names=("OrigStain1", "OrigStain2"), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.zeros((2, 4, 5), dtype=np.float32), None) try: DebugRuntimeFixture.execute_function_chain( diff --git a/tests/unit/test_declared_singleton_composition_437.py b/tests/unit/test_declared_singleton_composition_437.py index b3adbd438c..81c64fa40a 100644 --- a/tests/unit/test_declared_singleton_composition_437.py +++ b/tests/unit/test_declared_singleton_composition_437.py @@ -1,5 +1,6 @@ """Declaration-owned admission to the unchanged source-bundle algorithm.""" +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis from dataclasses import replace import numpy as np @@ -22,7 +23,6 @@ from openhcs.core.artifacts.kinds import ImageArtifactType from openhcs.core.runtime_image_values import ImagePayloadMetadata from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisValueProjection, RuntimePlaneProjection, ) @@ -66,11 +66,11 @@ @numpy_contract(contract=Pure3DContract) def independently_declared_source_echo(image): """A new source consumer, not a GrayToColor alias or special-case branch.""" - assert image.metadata.plane_axis is RuntimePlaneAxis.SOURCE_BINDING + assert image.metadata.plane_axis is SourceBindingPlaneAxis return RuntimeSliceProjection.value_for_slice( image, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.SOURCE_BINDING, + axis=SourceBindingPlaneAxis, plane_index=0, axis_size=1, source_aliases=("FITC",), @@ -90,7 +90,7 @@ def _runtime_source(slice_count): masks = np.ones_like(pixels, dtype=bool) masks[:, 0, 0] = False metadata = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_names=("FITC",), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=tuple( @@ -116,7 +116,7 @@ def test_new_declaration_reuses_composition_and_executor(slice_count, aligned): original_pixels = source.data.copy() original_masks = source.mask.copy() projection = RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=slice_count, ) planes = tuple( @@ -191,7 +191,7 @@ def test_new_declaration_reuses_composition_and_executor(slice_count, aligned): assert payload.slice_contexts == (contexts if aligned else ()) for index, plane in enumerate(payload.slices): metadata = plane.metadata - assert metadata.plane_axis is RuntimePlaneAxis.SOURCE_BINDING + assert metadata.plane_axis is SourceBindingPlaneAxis assert metadata.source_image_names == ("FITC",) assert metadata.source_component_metadata["channel"] == "2" assert metadata.source_image_paths == (f"/synthetic/A01_s{index + 1}_w2.tif",) @@ -219,7 +219,7 @@ def test_new_declaration_reuses_composition_and_executor(slice_count, aligned): result_plane.mask, original_masks[index] ) metadata = result_plane.metadata - assert metadata.plane_axis is None + assert metadata.plane_axis.unstacked assert metadata.axis_position(ColourAxis) is None assert ( metadata.source_voxel_spacing @@ -238,7 +238,7 @@ def test_declared_composed_scalar_introduces_one_source_axis(): scalar = RuntimeSliceProjection.value_for_slice( source, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, plane_index=0, axis_size=1, ), @@ -252,7 +252,7 @@ def test_declared_composed_scalar_introduces_one_source_axis(): assert composition.execution_mode is FullStackExecution assert ( composition.payload.metadata.plane_axis - is RuntimePlaneAxis.SOURCE_BINDING + is SourceBindingPlaneAxis ) contract = CallableContract.from_callable(independently_declared_source_echo) result = CellProfilerFunctionContractExecutor().execute( @@ -299,7 +299,7 @@ def test_natural_declaration_retains_exact_singleton(aligned): result.mask, source.mask ) assert ( - result.metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + result.metadata.plane_axis is RuntimeSlicePlaneAxis ) diff --git a/tests/unit/test_derived_role_publication_435.py b/tests/unit/test_derived_role_publication_435.py index c38331ee57..ff8d3401fc 100644 --- a/tests/unit/test_derived_role_publication_435.py +++ b/tests/unit/test_derived_role_publication_435.py @@ -1,5 +1,7 @@ """Issue435: real saved image occurrences projected through two existing owners.""" +from openhcs.core.image_file_serialization import ImageFileFormat +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from openhcs.core.materialization.options import NameByArtifactName, NameBySourceIdentity from dataclasses import asdict, replace import json @@ -23,7 +25,7 @@ from openhcs.core.compiled_step_plan import RuntimeArtifactMaterializationPlan from openhcs.core.runtime_artifact_values import RuntimeValue from openhcs.core.runtime_image_values import ( - ImageMetadataPayload, ImagePayloadMetadata, ImagePayloadMetadataCompositionMode, + ImageMetadataPayload, ImagePayloadMetadata, ) from openhcs.core.runtime_stores import RuntimeValueStore from openhcs.core.runtime_object_label_building import SourceImageObjectLabelBuildRequest @@ -39,7 +41,7 @@ ) from openhcs.core.steps.function_output_identity import FunctionOutputIdentity -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis, RuntimePlaneAxisValueProjection +from openhcs.core.runtime_plane_projection import RuntimePlaneAxisValueProjection from python_introspect import to_jsonable from openhcs.core.materialization import ( ImageFileOptions, MaterializationSpec, @@ -109,7 +111,7 @@ def test_saved_roles_publish_once_per_persisted_occurrence(tmp_path, scenario, a source_voxel_spacing=SourceVoxelSpacing((2.0, 0.65, 0.65)), ).payload_with(filemanager.load(str(path), Backend.DISK.value))) source_stack = stack_image_payloads( - source_planes, metadata_mode=ImagePayloadMetadataCompositionMode.STACK, + source_planes, metadata_mode=RuntimeSlicePlaneAxis, ) roles = (("fixture_image_494", 1.0),) for role, scale in roles: @@ -181,7 +183,7 @@ def test_saved_roles_publish_once_per_persisted_occurrence(tmp_path, scenario, a if aggregate: payload = ImageArtifactType.contextualize_output( source_stack, pixels.copy(), output_plan, - RuntimePlaneAxisValueProjection(RuntimePlaneAxis.RUNTIME_SLICE, (), None, 4), + RuntimePlaneAxisValueProjection.preserve(axis=RuntimeSlicePlaneAxis, axis_size=4), ) if aggregate: execution_scope = RuntimeExecutionAxisScope.from_raw( @@ -256,7 +258,14 @@ def test_saved_roles_publish_once_per_persisted_occurrence(tmp_path, scenario, a destination = plan.output_dir / filename filemanager.save(payload, str(destination), Backend.MEMORY.value) filemanager.ensure_directory(plan.output_dir, Backend.DISK.value) - filemanager.save(payload.data, str(destination), Backend.DISK.value) + # Write the file as the runtime does: with the payload's declared TIFF axes. + ((_indices, tiff_config),) = ImageFileFormat.storage_write_batches( + (payload,), (destination,), None, + ) + filemanager.save( + payload.data, str(destination), Backend.DISK.value, + **({} if tiff_config is None else {"tiff_config": tiff_config}), + ) record_output_path( context, plan, str(destination), image_metadata=payload.metadata, output_context=AlignedImageSliceContext.main_flow( @@ -339,9 +348,9 @@ def test_saved_roles_publish_once_per_persisted_occurrence(tmp_path, scenario, a } provenance = metadata["source_provenance"] assert "z_index" not in provenance["source_component_metadata"] - assert [plane["component_metadata"]["z_index"] for plane in - provenance["source_image_provenance_planes"]] == ["0", "1", "2", "3"] - assert [plane["path"] for plane in provenance["source_image_provenance_planes"]] == [ + planes = provenance["source_image_provenance_planes"]["identities"] + assert [plane["component_metadata"]["z_index"] for plane in planes] == ["0", "1", "2", "3"] + assert [plane["path"] for plane in planes] == [ str(source_dir / f"A01_s001_w1_z{z_index:03d}_t001.tif") for z_index in range(4) ] assert metadata["source_spatial_domain"]["spatial_dimensions"] == 2 diff --git a/tests/unit/test_derived_runtime_image_provenance.py b/tests/unit/test_derived_runtime_image_provenance.py index d60a7e7917..813c8dc819 100644 --- a/tests/unit/test_derived_runtime_image_provenance.py +++ b/tests/unit/test_derived_runtime_image_provenance.py @@ -1,4 +1,5 @@ from __future__ import annotations +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis import numpy as np import pytest @@ -7,7 +8,6 @@ from openhcs.core.artifacts.kinds import ImageArtifactType from openhcs.core.runtime_artifact_values import RuntimeValue from openhcs.core.runtime_image_values import ImagePayloadMetadata -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.runtime_slice_alignment import RuntimeSliceAlignedValues from openhcs.core.source_image_provenance import ( RuntimeSourceImageProvenancePlane, @@ -137,7 +137,7 @@ def test_derived_singleton_runtime_plane_projects_by_output_name() -> None: metadata = ImagePayloadMetadata( source_image_provenance_planes=SourceImageProvenancePlanes((_source_plane(1),)), source_image_names=("OrigGreen",), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) derived = metadata.with_source_provenance( @@ -155,7 +155,7 @@ def test_derived_singleton_runtime_plane_projects_by_output_name() -> None: assert derived.source_image_provenance_planes.runtime_source_image_names == ( "OrigGreenOverlay", ) - assert projected.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert projected.plane_axis is RuntimeSlicePlaneAxis assert projected_payload.data.shape == (1, 4, 5) assert projected.source_image_names == ("OrigGreenOverlay",) assert projected.source_provenance.represented_source_image_names == ( @@ -169,7 +169,7 @@ def test_declared_singleton_name_owns_projection_over_nested_source_name() -> No metadata = ImagePayloadMetadata( source_image_provenance_planes=SourceImageProvenancePlanes((_source_plane(1),)), source_image_names=("OrigGreenOverlay",), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) payload = metadata.payload_with(np.zeros((1, 4, 5), dtype=np.float32)) @@ -179,7 +179,7 @@ def test_declared_singleton_name_owns_projection_over_nested_source_name() -> No ) projected = projected_payload.metadata - assert projected.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert projected.plane_axis is RuntimeSlicePlaneAxis assert projected_payload.data.shape == (1, 4, 5) assert projected.source_image_names == ("OrigGreenOverlay",) assert projected.source_provenance.represented_source_image_names == ( @@ -194,7 +194,7 @@ def test_one_derived_name_applies_to_every_declared_runtime_plane() -> None: (_source_plane(1), _source_plane(2)) ), source_image_names=("OrigGreen", "OrigGreen"), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) derived = metadata.source_provenance.with_derived_source_image_names( @@ -259,7 +259,7 @@ def test_source_name_projects_one_exact_named_runtime_slice() -> None: (_source_plane(1), _source_plane(2)) ), source_image_names=("First", "Second"), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) payload = metadata.payload_with(np.zeros((2, 4, 5), dtype=np.float32)) @@ -267,7 +267,7 @@ def test_source_name_projects_one_exact_named_runtime_slice() -> None: projected = projected_payload.metadata assert projected_payload.data.shape == (4, 5) - assert projected.plane_axis is None + assert projected.plane_axis.unstacked assert projected.source_path == "/input/A01_s2_w1.tif" assert projected.source_image_names == ("OrigGreen",) @@ -288,7 +288,7 @@ def test_derived_plane_names_require_exact_declared_cardinality() -> None: source_image_provenance_planes=SourceImageProvenancePlanes( (_source_plane(1), _source_plane(2), _source_plane(3)) ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) with pytest.raises(ValueError, match="one shared name or exactly one name"): @@ -312,12 +312,12 @@ def test_scalar_contributors_survive_runtime_plane_projection() -> None: ) ), source_image_names=("OrigOverlay",), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) projected = metadata.for_leading_source_plane(0) - assert projected.plane_axis is None + assert projected.plane_axis.unstacked assert projected.source_image_names == ("OrigOverlay",) assert projected.source_provenance.represented_source_image_names == ( "OrigOverlay", diff --git a/tests/unit/test_e1_enum_dispatch_guards.py b/tests/unit/test_e1_enum_dispatch_guards.py index 4b9b06f7a9..97fec42d32 100644 --- a/tests/unit/test_e1_enum_dispatch_guards.py +++ b/tests/unit/test_e1_enum_dispatch_guards.py @@ -59,9 +59,6 @@ ('ImagePayloadConsumption', 'openhcs/core/steps/function_execution.py'): 'K5 callable contract (#1202 merged, still an enum); needs an owner', ('ImagePayloadConsumption', 'openhcs/interop/cellprofiler/module_artifact_declarations.py'): 'K5 callable contract (#1202 merged, still an enum); needs an owner', ('ImagePayloadConsumption', 'openhcs/interop/cellprofiler/runtime/module_execution.py'): 'K5 callable contract (#1202 merged, still an enum); needs an owner', - ('ImagePayloadMetadataCompositionMode', 'openhcs/core/aligned_image_payload.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', - ('ImagePayloadMetadataCompositionMode', 'openhcs/core/runtime_image_values.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', - ('ImagePayloadMetadataCompositionMode', 'openhcs/interop/cellprofiler/runtime/invocation.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', ('MainFlowInputProjection', 'openhcs/core/steps/function_runtime.py'): 'K5 callable contract (#1202 merged, still an enum); needs an owner', ('MainFlowInputProjection', 'openhcs/interop/cellprofiler/runtime/artifact_binding.py'): 'K5 callable contract (#1202 merged, still an enum); needs an owner', ('McpWireContentType', 'openhcs/mcp/dev_client_core.py'): 'MCP content-type names', @@ -112,17 +109,6 @@ ('MissingObjectMeasurementValuePolicy', 'openhcs/interop/cellprofiler/runtime/object_measurement_row_completion.py'): 'K3 measurement vocabulary (#1199 merged, still an enum); needs an owner', ('MultiprocessingStartMethod', 'openhcs/core/orchestrator/worker_execution.py'): 'Python multiprocessing start-method names; a class-valued top-level GlobalPipelineConfig field does not reach lazy PipelineConfig (ObjectState, L6)', ('MultiprocessingStartMethod', 'openhcs/core/worker_start_policy.py'): 'Python multiprocessing start-method names; a class-valued top-level GlobalPipelineConfig field does not reach lazy PipelineConfig (ObjectState, L6)', - ('ObjectLabelDomainScope', 'openhcs/core/measurement_image_alignment.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', - ('ObjectLabelDomainScope', 'openhcs/core/pipeline/function_contracts.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', - ('ObjectLabelDomainScope', 'openhcs/core/runtime_object_label_building.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', - ('ObjectLabelDomainScope', 'openhcs/core/runtime_object_label_domains.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', - ('ObjectLabelDomainScope', 'openhcs/core/runtime_object_labels.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', - ('ObjectLabelDomainScope', 'openhcs/core/runtime_relationships.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', - ('ObjectLabelDomainScope', 'openhcs/interop/cellprofiler/runtime/measurement_rows.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', - ('ObjectLabelDomainScope', 'openhcs/interop/cellprofiler/runtime/object_measurement_row_completion.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', - ('ObjectLabelDomainScope', 'openhcs/interop/cellprofiler/runtime/relationship_measurement_rows.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', - ('ObjectLabelDomainScope', 'openhcs/processing/backends/cellprofiler/outlines.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', - ('ObjectLabelDomainScope', 'openhcs/processing/backends/cellprofiler/relationships.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', ('ObjectLabelRepresentation', 'openhcs/core/runtime_object_label_aggregation.py'): 'K3 measurement vocabulary (#1199 merged, still an enum); needs an owner', ('ObjectLabelRepresentation', 'openhcs/core/runtime_object_labels.py'): 'K3 measurement vocabulary (#1199 merged, still an enum); needs an owner', ('ObjectLabelRepresentation', 'openhcs/processing/backends/cellprofiler/shape.py'): 'K3 measurement vocabulary (#1199 merged, still an enum); needs an owner', @@ -140,28 +126,6 @@ ('RuntimeInvocationDomain', 'openhcs/core/steps/function_execution.py'): 'K5 callable contract (#1202 merged, still an enum); needs an owner', ('RuntimeInvocationDomain', 'openhcs/core/steps/function_runtime.py'): 'K5 callable contract (#1202 merged, still an enum); needs an owner', ('RuntimeMeasurementFeatureNameMode', 'openhcs/core/equivalence/keys.py'): 'K3 measurement vocabulary (#1199 merged, still an enum); needs an owner', - ('RuntimePlaneAxis', 'openhcs/core/aligned_image_payload.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', - ('RuntimePlaneAxis', 'openhcs/core/artifacts/kinds.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', - ('RuntimePlaneAxis', 'openhcs/core/debug.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', - ('RuntimePlaneAxis', 'openhcs/core/image_payload_execution_mode.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', - ('RuntimePlaneAxis', 'openhcs/core/measurement_image_alignment.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', - ('RuntimePlaneAxis', 'openhcs/core/runtime_image_values.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', - ('RuntimePlaneAxis', 'openhcs/core/runtime_measurements.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', - ('RuntimePlaneAxis', 'openhcs/core/runtime_object_labels.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', - ('RuntimePlaneAxis', 'openhcs/core/runtime_plane_projection.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', - ('RuntimePlaneAxis', 'openhcs/core/runtime_slice_alignment.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', - ('RuntimePlaneAxis', 'openhcs/core/runtime_sparse_labels.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', - ('RuntimePlaneAxis', 'openhcs/core/runtime_tabular_values.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', - ('RuntimePlaneAxis', 'openhcs/core/steps/function_runtime.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', - ('RuntimePlaneAxis', 'openhcs/interop/cellprofiler/runtime/adapter.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', - ('RuntimePlaneAxis', 'openhcs/interop/cellprofiler/runtime/artifact_binding.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', - ('RuntimePlaneAxis', 'openhcs/interop/cellprofiler/runtime/object_measurement_row_completion.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', - ('RuntimePlaneAxis', 'openhcs/processing/backends/cellprofiler/area_occupied.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', - ('RuntimePlaneAxis', 'openhcs/processing/backends/cellprofiler/colocalization.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', - ('RuntimePlaneAxis', 'openhcs/processing/backends/cellprofiler/color.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', - ('RuntimePlaneAxis', 'openhcs/processing/backends/cellprofiler/image_math.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', - ('RuntimePlaneAxis', 'openhcs/processing/backends/cellprofiler/intensity.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', - ('RuntimePlaneAxis', 'openhcs/processing/backends/cellprofiler/intensity_distribution.py'): 'plane-mode family (G3: unowned); now unblocked by K2 and K3', ('ViewerControlField', 'openhcs/agent/services/viewer_window_service.py'): 'L7/G6: viewer reply keys; typed reply records replace the dicts', ('ViewerControlField', 'openhcs/runtime/napari_viewer_server.py'): 'L7/G6: viewer reply keys; typed reply records replace the dicts', ('ViewerControlField', 'openhcs/runtime/viewer_control_actions.py'): 'L7/G6: viewer reply keys; typed reply records replace the dicts', diff --git a/tests/unit/test_fiji_viewer_server.py b/tests/unit/test_fiji_viewer_server.py index 82b2c798d9..13766fcb99 100644 --- a/tests/unit/test_fiji_viewer_server.py +++ b/tests/unit/test_fiji_viewer_server.py @@ -1,4 +1,5 @@ from __future__ import annotations +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis import sys from types import ModuleType, SimpleNamespace @@ -15,7 +16,6 @@ FijiStreamingConfig, NapariDisplayConfig, ) -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.streaming_config_factory import ViewerProcessLaunchConfig from openhcs.runtime import fiji_viewer_server as fiji_viewer_server_module from openhcs.runtime.fiji_macro_runtime import ( @@ -561,7 +561,7 @@ def test_payload_local_site_axis_projects_exactly_across_fiji_and_napari() -> No }, "path": "aggregate.tif", "data": data, - "plane_axis": RuntimePlaneAxis.RUNTIME_SLICE.value, + "plane_axis": RuntimeSlicePlaneAxis.name, "plane_component_values": {"site": [1, 2]}, } component_domain = _component_value_domain( @@ -627,7 +627,7 @@ def test_fiji_payload_local_plane_axis_rejects_coordinate_count_mismatch() -> No "timepoint": 0, }, "data": np.zeros((3, 4, 5), dtype=np.uint8), - "plane_axis": RuntimePlaneAxis.RUNTIME_SLICE.value, + "plane_axis": RuntimeSlicePlaneAxis.name, "plane_component_values": {"site": [1, 2]}, } ) @@ -646,7 +646,7 @@ def test_fiji_payload_local_plane_axis_rejects_malformed_declarations() -> None: "timepoint": 0, }, "data": np.zeros((2, 4, 5), dtype=np.uint8), - "plane_axis": RuntimePlaneAxis.RUNTIME_SLICE.value, + "plane_axis": RuntimeSlicePlaneAxis.name, "plane_component_values": { "site": [1, 2], "channel": [1, 2], diff --git a/tests/unit/test_file_materialization_options.py b/tests/unit/test_file_materialization_options.py index 487e498313..9fece0e2ea 100644 --- a/tests/unit/test_file_materialization_options.py +++ b/tests/unit/test_file_materialization_options.py @@ -1,6 +1,7 @@ """Generic image-file and file-bundle materialization contracts.""" from __future__ import annotations +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from pathlib import Path @@ -11,7 +12,6 @@ from polystore.memory import MemoryStorageBackend from openhcs.core.runtime_image_values import ImagePayloadMetadata -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.source_image_provenance import SourceImageProvenancePlanes from openhcs.core.materialization import options as materialization_options from openhcs.core.materialization.constants import WriteMode @@ -62,7 +62,7 @@ def test_source_identity_image_file_projects_addressable_stack_planes() -> None: image_options, _bundle_options = _option_types() planes = tuple(np.full((4, 5, 3), value, dtype=np.uint8) for value in (1, 2, 3)) payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=tuple(f"/input/site{index}.tif" for index in range(1, 4)), component_metadata=tuple({"site": str(index)} for index in range(1, 4)), diff --git a/tests/unit/test_fitted_illumination_fields.py b/tests/unit/test_fitted_illumination_fields.py index df92b35f2d..ca7b6ee964 100644 --- a/tests/unit/test_fitted_illumination_fields.py +++ b/tests/unit/test_fitted_illumination_fields.py @@ -1,11 +1,11 @@ """Real source-owner projection of one field fitted from all observations.""" +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis import numpy as np import pytest from openhcs.core.runtime_image_values import ImagePayloadMetadata from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisValueProjection, ) from openhcs.core.source_image_provenance import SourceImageProvenancePlanes @@ -15,7 +15,7 @@ def observation_source(components): count = len(components) metadata = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, intensity_scale=65535.0, source_dtype="uint16", source_image_provenance_planes=SourceImageProvenancePlanes.from_components( @@ -41,13 +41,13 @@ def test_field_collapses_pixels_not_contributor_history(): result = field.resolve_source_context( source, RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=3, ), ) metadata = result.metadata assert result.shape == (8, 9) - assert metadata.plane_axis is None + assert metadata.plane_axis.unstacked assert len(metadata.source_image_paths) == 3 assert metadata.source_image_provenance_planes.contributor_count == 3 assert metadata.source_component_metadata == { @@ -91,7 +91,7 @@ def test_z_only_stack_is_not_an_observation_ensemble(): [ None, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, plane_index=0, axis_size=3, ), @@ -114,7 +114,7 @@ def test_field_keeps_observation_count_through_real_array_conversion(): def test_field_rejects_wrong_grid_or_observation_count(): source = cross_site_source() projection = RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=3, ) with pytest.raises(ValueError, match="spatial grid"): diff --git a/tests/unit/test_function_artifact_materialization.py b/tests/unit/test_function_artifact_materialization.py index 59b3979451..d686e62058 100644 --- a/tests/unit/test_function_artifact_materialization.py +++ b/tests/unit/test_function_artifact_materialization.py @@ -1,3 +1,6 @@ +from openhcs.core.runtime_plane_projection import NoPlaneAxis +from openhcs.core.runtime_object_label_domains import PayloadLabelScope, PlaneLabelScope +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis import importlib import json import pickle @@ -94,7 +97,6 @@ from openhcs.core.roi_source_metadata import ROIArchiveSourceMetadata from openhcs.core.runtime_object_label_domains import ( ObjectLabelDomain, - ObjectLabelDomainScope, ) from openhcs.core.runtime_object_labels import ( ObjectLabelPayload, @@ -102,7 +104,6 @@ ObjectLabelVariantData, ) from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisValueProjection, RuntimePlaneProjection, ) @@ -666,7 +667,7 @@ def test_slice_aligned_object_label_arrays_preserve_source_slice_metadata(): artifact_type=ObjectLabelsArtifactType, ) source = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=( "/input/A02_s001_w1_z001_t001.tif", @@ -691,7 +692,7 @@ def test_slice_aligned_object_label_arrays_preserve_source_slice_metadata(): label_slices, output_plan or MAIN_FLOW, RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), ) @@ -710,9 +711,9 @@ def test_slice_aligned_object_label_arrays_preserve_source_slice_metadata(): ) assert runtime_value.data.domain == ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ) - assert runtime_value.data.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert runtime_value.data.plane_axis is RuntimeSlicePlaneAxis assert runtime_value.data.source_spatial_domain == SourceSpatialDomain( origin_yx=(0, 0), source_shape_yx=(8, 8), @@ -770,7 +771,7 @@ def test_image_outputs_merge_source_provenance_when_output_already_has_metadata( output, output_plan or MAIN_FLOW, RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), ) @@ -826,7 +827,7 @@ def test_object_label_payload_stack_preserves_source_slice_metadata(): labels, output_plan or MAIN_FLOW, RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), ) @@ -968,7 +969,7 @@ def test_materialize_artifact_outputs_uses_output_plan_axes_for_source_named_run variable_components=(Microscopy.Site,), ) payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_component_metadata={ "well": "A01", "site": "1", @@ -1230,7 +1231,7 @@ def test_runtime_artifact_plan_materializes_3d_point_measurements(source_z_origi # The measurement table has no pixel-plane axis. Its rendered point archive # nevertheless owns every represented Z plane, not only its scalar address. - assert result.metadata.plane_axis is None + assert result.metadata.plane_axis.unstacked domain = materialization.stream_source_metadata_items((result,), None) assert [item["z_index"] for item in domain.values] == list( range(source_z_origin, source_z_origin + 4) @@ -1542,10 +1543,10 @@ def test_multi_plane_roi_aggregate_defers_source_filenames_to_plane_writer(monke labels_array[:, 2:6, 3:7] = 1 labels = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels_array), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,),) * 4, - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=tuple( @@ -2983,10 +2984,10 @@ def test_projected_artifact_uses_filename_identity_without_restoring_collapsed_s ) labels = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=np.ones((2, 4, 4), dtype=np.int32)), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((), ()), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), source_image_provenance_planes=SourceImageProvenancePlanes( tuple( @@ -3612,7 +3613,7 @@ def fake_materialize( @pytest.mark.parametrize( "domain_scope", - (ObjectLabelDomainScope.PLANE, ObjectLabelDomainScope.PAYLOAD), + (PlaneLabelScope, PayloadLabelScope), ) def test_materialize_artifact_outputs_uses_variable_components_for_streaming_identity( monkeypatch, @@ -3628,17 +3629,17 @@ def test_materialize_artifact_outputs_uses_variable_components_for_streaming_ide labels = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=np.ones((2, 2, 2), dtype=np.int32)), plane_axis=( - RuntimePlaneAxis.RUNTIME_SLICE - if domain_scope is ObjectLabelDomainScope.PLANE - else None + RuntimeSlicePlaneAxis + if domain_scope.ids_per_plane + else NoPlaneAxis ), domain=( ObjectLabelDomain( declared_object_id_domains=((1,), (1,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ) - if domain_scope is ObjectLabelDomainScope.PLANE - else ObjectLabelDomain(scope=ObjectLabelDomainScope.PAYLOAD) + if domain_scope.ids_per_plane + else ObjectLabelDomain(scope=PayloadLabelScope) ), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=( @@ -3793,10 +3794,10 @@ def test_materialize_artifact_outputs_streams_source_binding_roi_plane_metadata( streaming_config = streaming_config_stub() labels = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=np.ones((2, 2, 2), dtype=np.int32)), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (1,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=( @@ -4055,7 +4056,7 @@ def test_materialization_identity_replaces_provenance_not_payload_layout() -> No }, source_image_names=("OrigDNA",), axes=PayloadAxes.colour_samples(0), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, ) value = RuntimeValue.normalize( output_plan, @@ -4063,7 +4064,7 @@ def test_materialization_identity_replaces_provenance_not_payload_layout() -> No source_path="/derived/nuclei-rgb.tif", source_image_names=("NucleiImage",), axes=PayloadAxes.colour_samples(3), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with( np.ones((2, 5, 7, 3), dtype=np.uint16), None, @@ -4080,7 +4081,7 @@ def test_materialization_identity_replaces_provenance_not_payload_layout() -> No assert metadata.source_path == "/input/A01_s001_w2_z001_t001.tif" assert metadata.source_image_names == ("OrigDNA",) assert metadata.axis_position(ColourAxis) == 3 - assert metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert metadata.plane_axis is RuntimeSlicePlaneAxis def test_compiled_z_axis_reaches_source_named_image_materialization() -> None: @@ -4120,7 +4121,7 @@ def test_compiled_z_axis_reaches_source_named_image_materialization() -> None: saved_payload = (ImageArtifactType if output_plan is None else output_plan.artifact_type).contextualize_output_from_projector( selected_payload, ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with( np.ones((2, 5, 7), dtype=np.uint16), None, @@ -4181,7 +4182,7 @@ def test_compiled_z_axis_reaches_source_named_image_materialization() -> None: ) assert saved_payload.metadata.plane_axis is ( - RuntimePlaneAxis.RUNTIME_SLICE + RuntimeSlicePlaneAxis ) assert tuple( Path(output.path) diff --git a/tests/unit/test_function_artifact_outputs.py b/tests/unit/test_function_artifact_outputs.py index c303595294..5a424fe51f 100644 --- a/tests/unit/test_function_artifact_outputs.py +++ b/tests/unit/test_function_artifact_outputs.py @@ -1,3 +1,6 @@ +from openhcs.core.runtime_plane_projection import NoPlaneAxis +from openhcs.core.runtime_object_label_domains import PayloadLabelScope, PlaneLabelScope +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis from openhcs.core.steps.function_runtime import PatternGroupExecutionRequest from collections.abc import Callable, Mapping from dataclasses import dataclass, replace @@ -72,7 +75,6 @@ ) from openhcs.core.runtime_measurements import MeasurementScope, MeasurementSubject from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisValueProjection, RuntimePlaneProjection, ) @@ -92,7 +94,6 @@ unstack_image_payload_context, ) from openhcs.core.runtime_image_values import ( - ImagePayloadMetadataCompositionMode, ImagePayloadMetadata, ) from openhcs.core.runtime_object_labels import ( @@ -104,7 +105,6 @@ ) from openhcs.core.runtime_object_label_domains import ( ObjectLabelDomain, - ObjectLabelDomainScope, ) from openhcs.core.source_image_provenance import ( SourceImageProvenancePlanes, @@ -276,7 +276,7 @@ def test_function_core_passes_payload_data_to_array_callable_and_restores_contex tiles = np.arange(2 * 4 * 4, dtype=np.uint16).reshape(2, 4, 4) payload = ImagePayloadMetadata( source_path="/input/A01_s001_w1_z001_t001.tif", - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(tiles, None) positions = np.array([[0, 0], [4, 0]], dtype=np.float32) @@ -311,7 +311,7 @@ def test_function_core_preserves_output_declared_runtime_slice_axis(): def produce_runtime_slice_stack(_image): return ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.ones((1, 4, 5), dtype=np.float32), None) result = _execute_function_core( @@ -326,7 +326,7 @@ def produce_runtime_slice_stack(_image): ) assert result.data.shape == (1, 4, 5) - assert result.metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert result.metadata.plane_axis is RuntimeSlicePlaneAxis def test_image_output_context_preserves_stack_after_exact_source_projection(): @@ -334,10 +334,10 @@ def test_image_output_context_preserves_stack_after_exact_source_projection(): source = ImagePayloadMetadata( source_path="/input/A01_s001_w1_z001_t001.tif", source_image_names=(source_spec.name,), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.ones((1, 4, 5), dtype=np.float32), None) output = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.ones((1, 4, 5), dtype=np.float32), None) output_plan = ArtifactOutputPlan( name="CorrGray", @@ -352,13 +352,13 @@ def test_image_output_context_preserves_stack_after_exact_source_projection(): output, output_plan or MAIN_FLOW, RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=1, ), ) assert result.data.shape == (1, 4, 5) - assert result.metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert result.metadata.plane_axis is RuntimeSlicePlaneAxis assert result.metadata.source_image_names == (source_spec.name,) @@ -405,7 +405,7 @@ def test_image_output_context_does_not_infer_axis_for_unmarked_payload(): ) metadata = result.metadata - assert metadata.plane_axis is None + assert metadata.plane_axis.unstacked assert metadata.source_image_provenance_planes.paths == ( "/input/A01_s001_w2_z001_t001.tif", "/input/A01_s001_w2_z002_t001.tif", @@ -415,7 +415,7 @@ def test_image_output_context_does_not_infer_axis_for_unmarked_payload(): def test_image_output_context_rejects_declared_output_axis_shape_drift(): source_spec = ArtifactSpec.input("NucleiImage", ImageArtifactType) output = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.ones((1, 4, 5), dtype=np.uint16), None) output_plan = ArtifactOutputPlan( name="SavedNuclei", @@ -434,7 +434,7 @@ def test_image_output_context_rejects_declared_output_axis_shape_drift(): output, output_plan or MAIN_FLOW, RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), ) @@ -472,7 +472,7 @@ def test_image_output_context_preserves_complete_scalar_rgb_identity() -> None: assert result.data.shape == (4, 5, 3) metadata = result.metadata - assert metadata.plane_axis is None + assert metadata.plane_axis.unstacked assert metadata.axis_index(ColourAxis, result) == 2 assert metadata.source_path == "/input/A01_s001_w1_z001_t001.tif" @@ -487,7 +487,7 @@ def test_image_output_context_preserves_target_axis_across_source_rank_change() output = ImagePayloadMetadata( source_path="/input/A01_s001_color.tif", axes=PayloadAxes.colour_samples(4), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.ones((1, 2, 4, 5, 3), dtype=np.float32), None) result = ImageArtifactType.contextualize_output( @@ -499,7 +499,7 @@ def test_image_output_context_preserves_target_axis_across_source_rank_change() metadata = result.metadata assert metadata.axis_index(ColourAxis, result) == 4 - assert metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert metadata.plane_axis is RuntimeSlicePlaneAxis assert dict(metadata.source_component_metadata or {}) == { "well": "A01", "site": "1", @@ -536,7 +536,7 @@ def test_image_output_context_projects_complete_multi_plane_identity() -> None: ) source = metadata.payload_with(np.ones((2, 4, 5), dtype=np.float32), None) output = metadata.replace_fields( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with( np.ones((2, 4, 5), dtype=np.uint16), None, @@ -557,13 +557,13 @@ def test_image_output_context_projects_complete_multi_plane_identity() -> None: ) result_metadata = result.metadata - assert result_metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert result_metadata.plane_axis is RuntimeSlicePlaneAxis assert result_metadata.source_image_provenance_planes.count == 2 def test_named_image_outputs_bypass_unrelated_source_axis_projection(): source = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("/input/red.tif", "/input/green.tif"), component_metadata=( @@ -614,7 +614,7 @@ def test_image_output_context_projector_proves_source_ownership_once( {"well": "A01", "z_index": "2"}, ), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.zeros((2, 4, 5), dtype=np.uint16), None) output = np.ones((2, 4, 5), dtype=np.uint8) ownership_proofs = [] @@ -647,7 +647,7 @@ def track_ownership_proof(*args): def test_composed_function_output_owns_collapsed_source_identity(): source = ImagePayloadMetadata( source_path="/input/A01_s001_w1_z001_t001.tif", - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.ones((1, 4, 5), dtype=np.float32), None) output = ImagePayloadMetadata( source_path="/input/A01_s001_w1_z001_t001.tif", @@ -670,7 +670,7 @@ def compose_channels(_image): ) assert result.data.shape == (4, 5, 3) - assert result.metadata.plane_axis is None + assert result.metadata.plane_axis.unstacked assert result.metadata.axis_position(ColourAxis) == -1 @@ -688,7 +688,7 @@ def test_unstack_payload_context_slices_volume_stack_mask_with_volume_data(): [slice_payload] = unstack_image_payload_context( payload, [data[0]], - default_plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + default_plane_axis=RuntimeSlicePlaneAxis, ) assert slice_payload.mask.shape == data[0].shape @@ -764,13 +764,13 @@ def test_unstack_payload_context_preserves_declared_axis_over_default(): source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("/input/dna.tif", "/input/actin.tif"), ), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, ).payload_with(data, None) slices = unstack_image_payload_context( payload, list(data), - default_plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + default_plane_axis=RuntimeSlicePlaneAxis, ) assert [ @@ -790,7 +790,7 @@ def test_stack_payload_context_projects_single_payload_mask_to_stack_domain(): stacked = stack_image_payloads( (payload,), - metadata_mode=ImagePayloadMetadataCompositionMode.STACK, + metadata_mode=RuntimeSlicePlaneAxis, ) assert stacked.mask.shape == stack.shape @@ -808,7 +808,7 @@ def test_stack_image_payloads_uses_payload_memory_and_preserves_masks(): stacked = stack_image_payloads( payloads, - metadata_mode=ImagePayloadMetadataCompositionMode.STACK, + metadata_mode=RuntimeSlicePlaneAxis, ) np.testing.assert_array_equal( @@ -826,11 +826,11 @@ def test_stack_image_payloads_preserves_declared_singleton_stack_axis(): stacked = stack_image_payloads( (payload,), - metadata_mode=ImagePayloadMetadataCompositionMode.STACK, + metadata_mode=RuntimeSlicePlaneAxis, ) assert stacked.data.shape == (1, 4, 5) - assert stacked.metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert stacked.metadata.plane_axis is RuntimeSlicePlaneAxis np.testing.assert_array_equal(stacked.data[0], payload) @@ -850,7 +850,7 @@ def test_stack_payload_context_preserves_single_volumetric_payload_identity(): stacked_payload = stack_image_payloads( (payload,), - metadata_mode=ImagePayloadMetadataCompositionMode.STACK, + metadata_mode=RuntimeSlicePlaneAxis, ) stacked_metadata = stacked_payload.metadata @@ -877,12 +877,12 @@ def test_stack_payload_context_nests_incompatible_singleton_plane_topology(): paths=("/input/A01_w1.tif", "/input/A01_w2.tif"), component_metadata=plane_metadata, ), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, ).payload_with(data, None) stacked_payload = stack_image_payloads( (payload,), - metadata_mode=ImagePayloadMetadataCompositionMode.STACK, + metadata_mode=RuntimeSlicePlaneAxis, ) provenance_planes = stacked_payload.metadata.source_image_provenance_planes @@ -901,12 +901,12 @@ def test_stack_payload_context_preserves_singleton_stack_payload_mask_domain(): data = np.ones((1, 4, 5), dtype=np.float32) mask = np.ones_like(data, dtype=bool) payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(data, mask) stacked_payload = stack_image_payloads( (payload,), - metadata_mode=ImagePayloadMetadataCompositionMode.STACK, + metadata_mode=RuntimeSlicePlaneAxis, ) np.testing.assert_array_equal( @@ -923,7 +923,7 @@ def test_stack_payload_context_composes_single_image_slice_mask_axis(): stacked_payload = stack_image_payloads( (payload,), - metadata_mode=ImagePayloadMetadataCompositionMode.STACK, + metadata_mode=RuntimeSlicePlaneAxis, ) np.testing.assert_array_equal( @@ -1397,7 +1397,7 @@ def test_saved_canonical_source_projection_survives_main_flow_return(): pixels = np.arange(12, dtype=np.uint16).reshape(2, 2, 3) paths = ("/source/A01_s001_w2_z001_t001.tif", "/source/A01_s002_w2_z001_t001.tif") source = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=paths, component_metadata=tuple( @@ -1422,7 +1422,7 @@ def select_second_plane(image): for payload in (result, stored[0].data): np.testing.assert_array_equal(payload.data, pixels[1]) metadata = payload.metadata - assert metadata.plane_axis is None + assert metadata.plane_axis.unstacked assert metadata.source_provenance.source_plane_count == 0 assert metadata.source_path == paths[1] assert metadata.source_component_metadata["site"] == "2" @@ -1654,7 +1654,7 @@ def test_managed_runtime_adapter_output_preserves_authoritative_source_metadata( }, ), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.zeros((1, 2, 3), dtype=np.uint16), None) adapter_output = ImagePayloadMetadata( source_path="/input/A01_s1_w3.tif", @@ -1702,7 +1702,7 @@ def enhance(image, *, runtime): metadata = result.metadata assert result.data.shape == (2, 3, 3) - assert metadata.plane_axis is None + assert metadata.plane_axis.unstacked assert metadata.axis_position(ColourAxis) == -1 assert metadata.source_path == "/input/A01_s1_w3.tif" assert dict(metadata.source_component_metadata) == { @@ -1777,13 +1777,13 @@ def rgb_output(image): device_id=None,) assert stack_payload.data.shape == (2, 3, 3) stack_metadata = stack_payload.metadata - assert stack_metadata.plane_axis is None + assert stack_metadata.plane_axis.unstacked assert stack_metadata.axis_position(ColourAxis) == -1 def test_pattern_group_runtime_uses_declared_output_slice_cardinality(): declared_output = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.ones((1, 4, 5), dtype=np.float32), None) runtime = PatternGroupExecutionRequest( @@ -1815,7 +1815,7 @@ def test_pattern_group_runtime_uses_declared_output_slice_cardinality(): assert output[0][0].data.shape == (4, 5) assert ( declared_output.metadata.plane_axis - is RuntimePlaneAxis.RUNTIME_SLICE + is RuntimeSlicePlaneAxis ) @@ -1830,10 +1830,10 @@ def test_pattern_group_runtime_projects_nominal_object_label_stack(): declared_output = ObjectLabelSet( name="SavedChildren", variant_data=ObjectLabelVariantData(labels=label_planes), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) @@ -1864,10 +1864,10 @@ def test_pattern_group_runtime_projects_nominal_object_label_stack(): assert len(output) == 2 assert all(isinstance(value, ObjectLabelSet) for value in output) assert [value.name for value in output] == ["SavedChildren"] * 2 - assert [value.plane_axis for value in output] == [None, None] + assert [value.plane_axis for value in output] == [NoPlaneAxis, NoPlaneAxis] assert [value.domain.scope for value in output] == [ - ObjectLabelDomainScope.PAYLOAD, - ObjectLabelDomainScope.PAYLOAD, + PayloadLabelScope, + PayloadLabelScope, ] assert [value.domain.declared_object_ids for value in output] == [ (1,), @@ -2313,7 +2313,7 @@ def segment(image): label_set = stored.data assert isinstance(label_set, ObjectLabelSet) assert label_set.name == output_spec.name - assert label_set.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert label_set.plane_axis is RuntimeSlicePlaneAxis assert label_set.declared_plane_count() == 2 np.testing.assert_array_equal( label_set.labels, diff --git a/tests/unit/test_function_outputs.py b/tests/unit/test_function_outputs.py index 20f35e2b54..ac95f6b678 100644 --- a/tests/unit/test_function_outputs.py +++ b/tests/unit/test_function_outputs.py @@ -1,3 +1,4 @@ +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis import json from pathlib import Path from dataclasses import replace @@ -44,7 +45,6 @@ ImageMetadataPayload, ImagePayloadMetadata, ) -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.source_image_provenance import ( SourceImageIdentity, SourceImageProvenance, @@ -776,7 +776,7 @@ def test_stream_outputs_scope_viewer_layout_to_collapsed_source_components(): }, ) ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) collapsed_metadata = stack_metadata.collapse_leading_plane_axis() payload = collapsed_metadata.payload_with( @@ -1055,7 +1055,7 @@ def test_stream_outputs_projects_declared_channel_stack_axis(): path = "/tmp/output/A01_s1_w1.tif" pixels = np.ones((2, 5, 6), dtype=np.uint16) payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=(path, path), component_metadata=( @@ -1109,7 +1109,7 @@ def test_stream_outputs_projects_stack_planes_with_item_source_paths(): second_path = "/tmp/source/A01_s1_w2.tif" pixels = np.ones((2, 5, 6), dtype=np.uint16) payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=(first_path, second_path), component_metadata=( @@ -1160,7 +1160,7 @@ def test_stream_outputs_projects_stack_planes_with_item_source_paths(): def test_stream_outputs_rejects_unaddressed_stack_payload_metadata(): path = "/tmp/output/A01_s1_w1.tif" payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_component_metadata=complete_component_metadata( { "well": "A01", @@ -1189,7 +1189,7 @@ def test_stream_outputs_projects_semantic_image_stack_before_viewer_backend(): ) payload = ImagePayloadMetadata( axes=PayloadAxes.colour_samples(-1), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=( SourceImageProvenancePlanes.from_components( component_metadata=(first_metadata, second_metadata) @@ -1352,7 +1352,7 @@ def test_stream_outputs_partitions_one_producer_by_image_axis_fields(): def test_stream_outputs_rejects_unidentified_stack_without_per_slice_metadata(): path = "/tmp/output/A01_s1_w1.tif" payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_component_metadata=complete_component_metadata( {"well": "A01", "site": "1", "channel": "1"} ), @@ -1836,7 +1836,7 @@ def test_runtime_multiplane_label_artifact_projects_persisted_source_binding( ) ), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) output = Output.from_metadata( path=str(output_path), @@ -1924,7 +1924,7 @@ def test_runtime_multiplane_label_artifact_projects_persisted_source_binding( assert restored.execution_scope == materialization.record.key.scope assert restored.artifact_kind is ObjectLabelsArtifactType assert restored.image_metadata is not None - assert restored.image_metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert restored.image_metadata.plane_axis is RuntimeSlicePlaneAxis assert restored.image_metadata.source_provenance.source_plane_count == 2 @@ -2750,7 +2750,7 @@ def test_whole_volume_checkpoint_publication_retains_exact_producer_scope( ) metadata = ImagePayloadMetadata( source_image_names=("DNA",), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_spatial_domain=VolumeSourceSpatialDomain( source_depth=5, origin_yx=(0, 0), diff --git a/tests/unit/test_function_runtime_source_projection.py b/tests/unit/test_function_runtime_source_projection.py index 2d3a369fd1..2f411448de 100644 --- a/tests/unit/test_function_runtime_source_projection.py +++ b/tests/unit/test_function_runtime_source_projection.py @@ -1,3 +1,4 @@ +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis from openhcs.core.steps.abstract import StepExecutionObservation from tests.artifact_graphs import admitted_graph from openhcs.processing.backends.cellprofiler.crop import CropMask @@ -63,9 +64,7 @@ from openhcs.core.runtime_adapters import runtime_adapter from openhcs.core.runtime_image_values import ( ImagePayloadMetadata, - ImagePayloadMetadataCompositionMode, ) -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.runtime_stack_cache import RuntimeImageStackCache from openhcs.core.source_binding_selection import SourcePatternResolutionContext from openhcs.core.runtime_source_binding_cache import RuntimeSourceBindingContextCache @@ -444,7 +443,7 @@ def test_workspace_source_projection_carries_exact_aliases_into_stack_provenance ) assert stack.metadata.source_image_names == ("Raw", "Illum") - assert stack.metadata.plane_axis is RuntimePlaneAxis.SOURCE_BINDING + assert stack.metadata.plane_axis is SourceBindingPlaneAxis def test_workspace_replay_preserves_collapsed_semantic_identity( @@ -468,7 +467,7 @@ def test_workspace_replay_preserves_collapsed_semantic_identity( for site in (1, 2) ), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).collapse_leading_plane_axis() projection_set = SourceProjectionSet( ( @@ -852,7 +851,7 @@ def test_stack_payload_context_promotes_single_channel_slice_metadata() -> None: payload = stack_image_payloads( (first, second), - metadata_mode=ImagePayloadMetadataCompositionMode.STACK, + metadata_mode=RuntimeSlicePlaneAxis, ) metadata = payload.metadata @@ -4153,7 +4152,7 @@ def resolve_address(backend_address, backend, *, base_path): assert data.shape == (2, 4, 5, 3) np.testing.assert_array_equal(data[1], np.full((4, 5, 3), 7, dtype=np.float32)) assert metadata.source_image_names == aliases - assert metadata.plane_axis is RuntimePlaneAxis.SOURCE_BINDING + assert metadata.plane_axis is SourceBindingPlaneAxis assert metadata.axis_position(ColourAxis) == 3 assert metadata.source_component_metadata["specimen"] == "sample" @@ -5298,7 +5297,7 @@ def test_collapsed_output_identity_uses_retained_source_contributors( }, ), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) collapsed_metadata = stack_metadata.collapse_leading_plane_axis() payload = collapsed_metadata.payload_with( @@ -5362,7 +5361,7 @@ def test_composite_then_z_collapse_uses_current_scalar_identity( ), ) ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) composite_payloads.append( channel_stack_metadata.collapse_leading_plane_axis().payload_with( @@ -6007,7 +6006,7 @@ def test_whole_volume_checkpoint_load_preserves_depth_and_independent_buffers(): pixels = np.arange(3 * 4 * 5, dtype=np.float32).reshape(3, 4, 5) mask = pixels % 2 == 0 metadata = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_spatial_domain=VolumeSourceSpatialDomain(source_depth=3), ) source = metadata.payload_with(pixels, mask) diff --git a/tests/unit/test_graph_roi_source_roundtrip_134.py b/tests/unit/test_graph_roi_source_roundtrip_134.py index d648dcd2c9..a068b9815b 100644 --- a/tests/unit/test_graph_roi_source_roundtrip_134.py +++ b/tests/unit/test_graph_roi_source_roundtrip_134.py @@ -1,5 +1,6 @@ """Receiving #134: actual graph context/writer/ZIP, without a native process.""" +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from openhcs.core.artifacts.kinds import ImageArtifactType from pathlib import Path @@ -23,7 +24,6 @@ from openhcs.core.config import NapariStreamingConfig from openhcs.core.roi_source_metadata import ROIArchiveSourceMetadata from openhcs.core.runtime_image_values import ImagePayloadMetadata -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.runtime_spatial_graph import ( SpatialGraph, SpatialGraphEdge, @@ -117,7 +117,7 @@ def graph_and_plan(*, plane, graph_type=SpatialGraph, extra_features=None): def contextualize(graph, plan): metadata = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("/engineering/nuclear.tif", "/engineering/body.tif"), component_metadata=tuple( @@ -182,7 +182,7 @@ def test_declared_source_plane_survives_graph_writer_zip_and_subject_features( ) assert metadata.source_component_metadata["channel"] == plane + 1 assert metadata.source_voxel_spacing == graph.source_voxel_spacing - assert metadata.plane_axis is None + assert metadata.plane_axis.unstacked assert metadata.source_spatial_domain == output.metadata.source_spatial_domain assert path.name == "misleading_w99_z999.graph.roi.zip" geometry = ROIArchiveSourceMetadata.geometry(restored) diff --git a/tests/unit/test_gray_to_color_binding_axis_432.py b/tests/unit/test_gray_to_color_binding_axis_432.py index 5bb11ef1d0..81948dbfdf 100644 --- a/tests/unit/test_gray_to_color_binding_axis_432.py +++ b/tests/unit/test_gray_to_color_binding_axis_432.py @@ -1,5 +1,6 @@ """Source-only #432: declared primary binding is not a runtime grouping axis.""" +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis from dataclasses import replace import numpy as np @@ -13,7 +14,6 @@ from openhcs.core.config import StepSourceBindingsConfig from openhcs.core.function_patterns import InvocationArtifactInputEdgePlan from openhcs.core.runtime_image_values import ImagePayloadMetadata -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.runtime_plane_projection import RuntimePlaneAxisValueProjection from openhcs.core.runtime_slice_projection import RuntimeSliceProjection from openhcs.core.runtime_stores import RuntimeValueStore @@ -61,7 +61,7 @@ def test_declared_fitc_runtime_plane_retains_values_and_physical_identity(explic pixels = np.arange(20, dtype=np.float32).reshape(4, 5) * 1000 physical = {"well": "A01", "site": "1", "channel": "2", "z_index": "1", "timepoint": "1"} metadata = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("/synthetic/A01_s1_w2_z1_t1.tif",), component_metadata=(physical,), ), @@ -90,10 +90,10 @@ def test_declared_fitc_runtime_plane_retains_values_and_physical_identity(explic # The source's acquisition axis remains distinct from the named input axis. assert isinstance(request.payload, AlignedImageStack) assert request.execution_mode is AlignedStackExecution - assert request.plane_projection.axis is RuntimePlaneAxis.RUNTIME_SLICE + assert request.plane_projection.axis is RuntimeSlicePlaneAxis assert request.plane_projection.axis_size == 1 - assert request.plane_projection.plane_index is None - assert request.payload.slices[0].metadata.plane_axis is RuntimePlaneAxis.SOURCE_BINDING + assert request.plane_projection.preserves_complete_axis + assert request.payload.slices[0].metadata.plane_axis is SourceBindingPlaneAxis assert request.payload.slices[0].metadata.source_image_names == ("FITC",) execution = executor._invocation_request( image_request=request, adapter=runtime, current_image=source, @@ -105,9 +105,9 @@ def test_declared_fitc_runtime_plane_retains_values_and_physical_identity(explic execution_mode=execution.execution_mode, plane_projection=execution.plane_projection, ) # Float32 is the original Stack runner's output contract, not unit normalization. - assert result.metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert result.metadata.plane_axis is RuntimeSlicePlaneAxis plane = RuntimeSliceProjection.value_for_slice(result, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=0, axis_size=1, + axis=RuntimeSlicePlaneAxis, plane_index=0, axis_size=1, )) assert plane.data.shape == (4, 5, 1) np.testing.assert_array_equal(plane.data[..., 0], pixels) @@ -136,7 +136,7 @@ def _execute_bound_stack(bundle): {"color_scheme": GrayToColorModule.Scheme.STACK, "rescale_intensity": False}, execution_mode=FullStackExecution, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.SOURCE_BINDING, + axis=SourceBindingPlaneAxis, axis_size=bundle.data.shape[0], source_aliases=bundle.metadata.source_image_names, ), @@ -149,13 +149,13 @@ def test_two_processing_roles_share_physical_channel_without_fabrication(aliases capped = np.clip(raw / 6000, 0, 1) inputs = tuple(_source_plane(pixels, alias) for pixels, alias in zip((raw, capped), aliases, strict=True)) bundle = ImagePayloadBundleContext.from_payloads(inputs).compose() - assert bundle.metadata.plane_axis is RuntimePlaneAxis.SOURCE_BINDING + assert bundle.metadata.plane_axis is SourceBindingPlaneAxis result = _execute_bound_stack(bundle) np.testing.assert_array_equal(result.data[..., 0], raw) np.testing.assert_array_equal(result.data[..., 1], capped) np.testing.assert_array_equal(inputs[0].data, raw) metadata = result.metadata - assert metadata.plane_axis is None + assert metadata.plane_axis.unstacked assert metadata.axis_position(ColourAxis) == -1 assert metadata.source_component_metadata["channel"] == "2" assert set(metadata.source_image_paths) == {"/synthetic/A01_s1_w2_z1_t1.tif"} @@ -167,13 +167,13 @@ def test_projected_runtime_plane_needs_a_separate_named_binding_axis(): pixels = np.arange(20, dtype=np.float32).reshape(4, 5) * 1000 scalar = _source_plane(pixels, "FITC") metadata = scalar.metadata - retained = replace(metadata, plane_axis=RuntimePlaneAxis.RUNTIME_SLICE).payload_with(pixels[None], None) + retained = replace(metadata, plane_axis=RuntimeSlicePlaneAxis).payload_with(pixels[None], None) projected = RuntimeSliceProjection.value_for_slice(retained, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=0, axis_size=1, + axis=RuntimeSlicePlaneAxis, plane_index=0, axis_size=1, )) - assert projected.metadata.plane_axis is None + assert projected.metadata.plane_axis.unstacked bundle = ImagePayloadBundleContext.from_payloads((projected,)).compose() - assert bundle.metadata.plane_axis is RuntimePlaneAxis.SOURCE_BINDING + assert bundle.metadata.plane_axis is SourceBindingPlaneAxis result = _execute_bound_stack(bundle) assert result.data.shape == (4, 5, 1) np.testing.assert_array_equal(result.data[..., 0], pixels) diff --git a/tests/unit/test_image_file_serialization.py b/tests/unit/test_image_file_serialization.py index b1128ef42a..c894878692 100644 --- a/tests/unit/test_image_file_serialization.py +++ b/tests/unit/test_image_file_serialization.py @@ -1,3 +1,4 @@ +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis import os from hashlib import md5 from pathlib import Path @@ -27,7 +28,6 @@ ImageMetadataPayload, ImagePayloadMetadata, ) -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.payload_axes import PayloadAxes @@ -654,7 +654,7 @@ def test_png_disk_serialization_unwraps_image_metadata_payload() -> None: def test_png_disk_serialization_rejects_declared_unprojected_plane_axis() -> None: payload = ImageMetadataPayload( np.zeros((1, 3, 5), dtype=np.uint8), - ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE), + ImagePayloadMetadata(plane_axis=RuntimeSlicePlaneAxis), ) with pytest.raises(ValueError, match="projected off.*runtime_slice"): @@ -699,7 +699,7 @@ def test_intrinsic_tiff_write_uses_declared_axes_not_rgb_shaped_dimensions( ) pixels = np.arange(np.prod(shape), dtype=np.uint16).reshape(shape) metadata = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, axes=PayloadAxes.colour_samples(channel_axis), source_spatial_domain=VolumeSourceSpatialDomain(source_depth=3), ) @@ -729,7 +729,7 @@ def test_runtime_plane_tiff_write_retains_scalar_frames_and_declared_channels( ) pixels = np.arange(np.prod(shape), dtype=dtype).reshape(shape) metadata = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, axes=PayloadAxes.colour_samples(channel_axis), ) path = tmp_path / "artifact.labels.tif" @@ -756,7 +756,7 @@ def test_runtime_plane_tiff_write_retains_scalar_frames_and_declared_channels( def test_runtime_plane_tiff_rejects_undeclared_extra_payload_axis(tmp_path): - metadata = ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE) + metadata = ImagePayloadMetadata(plane_axis=RuntimeSlicePlaneAxis) with pytest.raises(ValueError, match='declared plane'): TiffImageFileFormat().write( tmp_path / 'invalid.tif', metadata.payload_with(np.zeros((4, 5, 7, 2))) diff --git a/tests/unit/test_image_output_context_projection_proof.py b/tests/unit/test_image_output_context_projection_proof.py index 957757a55c..90815c195f 100644 --- a/tests/unit/test_image_output_context_projection_proof.py +++ b/tests/unit/test_image_output_context_projection_proof.py @@ -1,3 +1,4 @@ +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis import numpy as np import pytest @@ -7,7 +8,6 @@ from openhcs.core.artifacts.kinds import ImageArtifactType from openhcs.core.runtime_image_values import ImagePayloadMetadata from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisValueProjection, RuntimePlaneProjection, ) @@ -82,7 +82,7 @@ def test_projected_variable_stack_proves_source_ownership_once( output = ( source.metadata .replace_fields( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) .payload_with( np.ones((2, 4, 5), dtype=np.uint16), @@ -108,12 +108,12 @@ def test_projected_variable_stack_proves_source_ownership_once( assert len(proof_calls) == 1 projection = proof_calls[0][3] assert isinstance(projection, RuntimePlaneAxisValueProjection) - assert projection.axis is RuntimePlaneAxis.RUNTIME_SLICE + assert projection.axis is RuntimeSlicePlaneAxis assert projection.axis_size == 2 np.testing.assert_array_equal( result.data, output.data ) - assert result.metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert result.metadata.plane_axis is RuntimeSlicePlaneAxis def test_derived_2d_image_does_not_inherit_input_runtime_plane_axis() -> None: @@ -130,7 +130,7 @@ def test_derived_2d_image_does_not_inherit_input_runtime_plane_axis() -> None: {"well": "A01", "z_index": "2"}, ), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.ones((2, 4, 5), dtype=np.float32), None) output_plan = ArtifactOutputPlan( name="Projection", @@ -150,7 +150,7 @@ def test_derived_2d_image_does_not_inherit_input_runtime_plane_axis() -> None: ) assert result.data.shape == (4, 5) - assert result.metadata.plane_axis is None + assert result.metadata.plane_axis.unstacked assert result.metadata.source_image_provenance_planes.count == 2 @@ -158,7 +158,7 @@ def test_bare_full_stack_image_inherits_source_runtime_plane_axis() -> None: source_spec = ArtifactSpec.input("Volume", ImageArtifactType) source = ImagePayloadMetadata( source_image_names=(source_spec.name,), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.ones((2, 4, 5), dtype=np.float32), None) output_plan = ArtifactOutputPlan( name="FilteredVolume", @@ -176,17 +176,17 @@ def test_bare_full_stack_image_inherits_source_runtime_plane_axis() -> None: ) assert result.data.shape == (2, 4, 5) - assert result.metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert result.metadata.plane_axis is RuntimeSlicePlaneAxis def test_derived_image_preserves_explicit_output_runtime_plane_axis() -> None: source_spec = ArtifactSpec.input("Volume", ImageArtifactType) source = ImagePayloadMetadata( source_image_names=(source_spec.name,), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.ones((2, 4, 5), dtype=np.float32), None) output = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.ones((2, 4, 5), dtype=np.float32), None) output_plan = ArtifactOutputPlan( name="FilteredVolume", @@ -204,7 +204,7 @@ def test_derived_image_preserves_explicit_output_runtime_plane_axis() -> None: ) assert result.data.shape == (2, 4, 5) - assert result.metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert result.metadata.plane_axis is RuntimeSlicePlaneAxis def test_projected_crop_preserves_complete_spatial_domain_after_plane_composition() -> ( @@ -223,7 +223,7 @@ def test_projected_crop_preserves_complete_spatial_domain_after_plane_compositio {"well": "A01", "site": "2", "channel": "1"}, ), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) source = source_metadata.payload_with( np.ones((2, 10, 12), dtype=np.float32), @@ -281,10 +281,10 @@ def test_projected_scalar_rgb_proves_complete_identity_once( assert len(proof_calls) == 1 projection = proof_calls[0][3] assert isinstance(projection, RuntimePlaneAxisValueProjection) - assert projection.axis is RuntimePlaneAxis.RUNTIME_SLICE + assert projection.axis is RuntimeSlicePlaneAxis assert projection.axis_size == 1 assert result is output - assert result.metadata.plane_axis is None + assert result.metadata.plane_axis.unstacked assert result.metadata.axis_index(ColourAxis, result) == 2 diff --git a/tests/unit/test_image_payload_projection_algebra.py b/tests/unit/test_image_payload_projection_algebra.py index 44c4e25b00..774513db92 100644 --- a/tests/unit/test_image_payload_projection_algebra.py +++ b/tests/unit/test_image_payload_projection_algebra.py @@ -1,5 +1,7 @@ """Physical source-plane projection preserves composition and mutable isolation.""" +from openhcs.core.runtime_plane_projection import NoPlaneAxis +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis from dataclasses import fields import numpy as np @@ -43,7 +45,7 @@ def _metadata(axis, channel_axis): ) -@pytest.mark.parametrize("axis", tuple(RuntimePlaneAxis)) +@pytest.mark.parametrize("axis", RuntimePlaneAxis.leading_axes()) @pytest.mark.parametrize("channel_axis", (None, 1, 3, -1)) @pytest.mark.parametrize("plane", (0, 1)) def test_physical_projection_agrees_with_separate_semantic_operations( @@ -60,14 +62,14 @@ def test_physical_projection_agrees_with_separate_semantic_operations( assert projected.unit_interval_intensity.scale == (255, 65535)[plane] assert projected.source_plane_intensity_scales == () assert projected.source_plane_dtypes == () - assert projected.plane_axis is None + assert projected.plane_axis.unstacked projected.source_provenance.source_identity.path = "/tmp/changed.tif" assert metadata.source_path == "/tmp/parent.tif" assert metadata.source_image_provenance_planes.paths[plane] != "/tmp/changed.tif" def test_physical_projection_constructs_only_the_final_metadata(monkeypatch): - metadata = _metadata(RuntimePlaneAxis.RUNTIME_SLICE, None) + metadata = _metadata(RuntimeSlicePlaneAxis, None) calls = [] original = ImagePayloadMetadata.__post_init__ @@ -81,13 +83,13 @@ def observe(self, *values): def test_physical_projection_rejects_leading_channel_axis(): - metadata = _metadata(RuntimePlaneAxis.RUNTIME_SLICE, 0) + metadata = _metadata(RuntimeSlicePlaneAxis, 0) with pytest.raises(ValueError, match="both plane and"): metadata.for_leading_source_plane(1) def test_physical_projection_requires_declared_plane_axis(): - metadata = _metadata(None, None) + metadata = _metadata(NoPlaneAxis, None) with pytest.raises(ValueError, match="declared plane axis"): metadata.for_leading_source_plane(1) @@ -98,22 +100,13 @@ def test_declared_axis_positions_must_be_int(channel_axis): PayloadAxes.colour_samples(channel_axis) -def test_physical_projection_preserves_invalid_mutated_plane_error(): - metadata = _metadata(RuntimePlaneAxis.RUNTIME_SLICE, None) - metadata.plane_axis = "invalid-axis" - with pytest.raises(ValueError): - metadata.for_source_plane(1).without_leading_plane_axis() - with pytest.raises(ValueError): - metadata.for_leading_source_plane(1) - - def test_projection_preserves_selected_spacing_and_nested_contributors(): contributor = SourceImageProvenanceContributor( SourceImageIdentity("/tmp/contributor.tif", {"site": "3"}), source_image_name="Original", ) metadata = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes( ( RuntimeSourceImageProvenancePlane( @@ -135,7 +128,7 @@ def test_projection_preserves_selected_spacing_and_nested_contributors(): def test_projection_observes_mask_and_metadata_mutation(): - metadata = _metadata(RuntimePlaneAxis.RUNTIME_SLICE, None) + metadata = _metadata(RuntimeSlicePlaneAxis, None) mask = np.ones((2, 3, 4), dtype=bool) projector = ImagePayloadSliceProjector(mask, metadata) data = np.zeros((3, 4)) @@ -167,7 +160,7 @@ def __getitem__(self, index): return super().__getitem__(index) metadata = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_plane_intensity_scales=ObservedTuple((2.0,)), unit_interval_intensity=ImageUnitIntervalIntensityMetadata( source_plane_scales=(ObservedInt(1),) @@ -212,7 +205,6 @@ def test_source_plane_preserves_intensity_then_proof_effect_order(): AttributeError, "with_missing_from", ), - ("for_leading_source_plane", 0, "plane", ValueError, "invalid-axis"), ( "without_leading_plane_axis", 0, @@ -234,13 +226,6 @@ def test_source_plane_preserves_intensity_then_proof_effect_order(): ValueError, "both plane and", ), - ( - "without_leading_plane_axis", - 0, - "plane", - ValueError, - "both plane and", - ), ), ) def test_projection_preserves_cross_invalid_constructor_guard_order( @@ -248,17 +233,15 @@ def test_projection_preserves_cross_invalid_constructor_guard_order( ): metadata = ImagePayloadMetadata( source_path="/tmp/source.tif", - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) metadata.axes = PayloadAxes.colour_samples(channel_axis) if broken == "domain": metadata.source_spatial_domain = SourceSpatialDomain(origin_yx=(1,)) elif broken == "spacing": metadata.source_component_metadata = {"OpenHCSSourceVoxelSpacingZYX": "0,1,1"} - elif broken == "spacing-value": - metadata.source_voxel_spacing = None else: - metadata.plane_axis = "invalid-axis" + metadata.source_voxel_spacing = None with pytest.raises(error, match=message): if operation == "without_leading_plane_axis": metadata.without_leading_plane_axis() @@ -268,7 +251,7 @@ def test_projection_preserves_cross_invalid_constructor_guard_order( def test_missing_leading_axis_precedes_plane_field_effects(): metadata, events = _metadata_with_observed_plane_fields(proof_raises=True) - metadata.plane_axis = None + metadata.plane_axis = NoPlaneAxis metadata.axes = PayloadAxes.colour_samples(0) with pytest.raises(ValueError, match="requires a declared plane axis"): metadata.for_leading_source_plane(0) @@ -276,10 +259,9 @@ def test_missing_leading_axis_precedes_plane_field_effects(): def test_projection_spacing_failure_precedes_other_invalid_source_fields(): - metadata = ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE) + metadata = ImagePayloadMetadata(plane_axis=RuntimeSlicePlaneAxis) metadata.source_component_metadata = {"OpenHCSSourceVoxelSpacingZYX": "0,1,1"} metadata.source_spatial_domain = SourceSpatialDomain(origin_yx=(1,)) - metadata.plane_axis = "invalid-axis" with pytest.raises(ValueError, match="finite and positive"): metadata.for_leading_source_plane(0) @@ -287,17 +269,17 @@ def test_projection_spacing_failure_precedes_other_invalid_source_fields(): @pytest.mark.parametrize( "operation,normalization_axes", ( - ("without_leading_plane_axis", (RuntimePlaneAxis.RUNTIME_SLICE, None)), + ("without_leading_plane_axis", (RuntimeSlicePlaneAxis, NoPlaneAxis)), ( "for_leading_source_plane", - (RuntimePlaneAxis.RUNTIME_SLICE, RuntimePlaneAxis.RUNTIME_SLICE, None), + (RuntimeSlicePlaneAxis, RuntimeSlicePlaneAxis, NoPlaneAxis), ), ), ) def test_projection_normalizes_owned_metadata_at_each_historical_phase( monkeypatch, operation, normalization_axes ): - metadata = _metadata(RuntimePlaneAxis.RUNTIME_SLICE, 2) + metadata = _metadata(RuntimeSlicePlaneAxis, 2) input_provenance = metadata.source_provenance input_proof = metadata.unit_interval_intensity births = [] @@ -323,7 +305,7 @@ def observe_normalization(self): else metadata.for_leading_source_plane(1) ) - assert births == [(projected, RuntimePlaneAxis.RUNTIME_SLICE, 2)] + assert births == [(projected, RuntimeSlicePlaneAxis, 2)] assert tuple(axis for _, axis, _ in phases) == normalization_axes assert all(owner is projected for owner, _, _ in phases) assert tuple(channel for _, _, channel in phases) == (2,) * ( @@ -332,7 +314,7 @@ def observe_normalization(self): assert projected is not metadata assert metadata.source_provenance is input_provenance assert metadata.unit_interval_intensity is input_proof - assert metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert metadata.plane_axis is RuntimeSlicePlaneAxis assert metadata.axis_position(ColourAxis) == 2 assert metadata.source_plane_intensity_scales == (255.0, 65535.0) assert projected.source_provenance is not input_provenance @@ -343,7 +325,7 @@ def observe_normalization(self): def test_projection_preserves_contributor_derivation_constructor_error_order( monkeypatch, combined ): - metadata = _metadata(RuntimePlaneAxis.RUNTIME_SLICE, None) + metadata = _metadata(RuntimeSlicePlaneAxis, None) invalid_domain = SourceSpatialDomain(origin_yx=(1,)) metadata.source_spatial_domain = invalid_domain provenance = metadata.source_provenance @@ -366,13 +348,13 @@ def fail_contributors(self): assert calls == [provenance] assert metadata.source_provenance is provenance assert metadata.source_spatial_domain is invalid_domain - assert metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert metadata.plane_axis is RuntimeSlicePlaneAxis assert metadata.source_plane_intensity_scales == (255.0, 65535.0) @pytest.mark.parametrize("combined", (False, True)) def test_failed_owned_axis_transform_does_not_mutate_source(monkeypatch, combined): - metadata = _metadata(RuntimePlaneAxis.RUNTIME_SLICE, 2) + metadata = _metadata(RuntimeSlicePlaneAxis, 2) provenance = metadata.source_provenance proof = metadata.unit_interval_intensity @@ -389,7 +371,7 @@ def fail_proof(self): metadata.without_leading_plane_axis() assert metadata.source_provenance is provenance assert metadata.unit_interval_intensity is proof - assert metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert metadata.plane_axis is RuntimeSlicePlaneAxis assert metadata.axis_position(ColourAxis) == 2 assert metadata.source_plane_intensity_scales == (255.0, 65535.0) assert metadata.source_plane_dtypes == ("uint8", "uint16") diff --git a/tests/unit/test_image_plane_contracts.py b/tests/unit/test_image_plane_contracts.py index 033419b84f..28f9ff2f42 100644 --- a/tests/unit/test_image_plane_contracts.py +++ b/tests/unit/test_image_plane_contracts.py @@ -1,3 +1,4 @@ +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis import numpy as np import pytest @@ -8,7 +9,6 @@ unstack_runtime_slices, ) from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisProjector, RuntimePlaneAxisValueProjection, RuntimePlaneProjection, @@ -18,7 +18,6 @@ from openhcs.core.runtime_image_values import ( ImagePayloadSliceProjector, ImagePayloadMetadata, - ImagePayloadMetadataCompositionMode, MaskedImagePayload, ) from openhcs.core.payload_axes import PayloadAxes @@ -90,13 +89,13 @@ def test_runtime_plane_projection_requires_matching_payload_axis() -> None: ) mask = np.ones_like(data, dtype=bool) payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(data, mask) selected = RuntimeSliceProjection.value_for_slice( payload, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=2, ), @@ -104,7 +103,7 @@ def test_runtime_plane_projection_requires_matching_payload_axis() -> None: np.testing.assert_array_equal(selected.data, data[1]) np.testing.assert_array_equal(selected.mask, mask[1]) - assert selected.metadata.plane_axis is None + assert selected.metadata.plane_axis.unstacked def test_source_binding_slice_projection_preserves_shared_spatial_mask() -> None: @@ -112,7 +111,7 @@ def test_source_binding_slice_projection_preserves_shared_spatial_mask() -> None mask = np.ones((4, 5), dtype=bool) mask[1, 2] = False metadata = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_spatial_domain=SourceSpatialDomain(source_shape_yx=(4, 5)), ) @@ -127,7 +126,7 @@ def test_source_binding_slice_projection_preserves_shared_spatial_mask() -> None def test_scalar_and_batch_projection_preserve_distinct_mask_contracts() -> None: data = np.zeros((2, 4, 5), dtype=np.float32) metadata = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_spatial_domain=SourceSpatialDomain(source_shape_yx=(4, 5)), ) shared_mask = np.ones((4, 5), dtype=np.uint8) @@ -154,7 +153,7 @@ def test_batch_projection_checks_mask_cardinality_before_child_axis_errors( mask_shape: tuple[int, ...], ) -> None: metadata = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, axes=PayloadAxes.colour_samples(0), ) projector = ImagePayloadSliceProjector( @@ -192,7 +191,7 @@ def test_slice_projection_keeps_fresh_source_metadata_across_calls(batch: bool) data = np.zeros((2, 4, 5), dtype=np.float32) masks = np.ones(data.shape, dtype=bool) metadata = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_component_metadata={"well": "A01"}, ) payload = metadata.payload_with(data, masks) @@ -201,7 +200,7 @@ def test_slice_projection_keeps_fresh_source_metadata_across_calls(batch: bool) def project(): if batch: return projector.payloads_for_slices(tuple(data))[1] - return payload.slice_payload(data[1], 1, plane_axis=RuntimePlaneAxis.RUNTIME_SLICE) + return payload.slice_payload(data[1], 1, plane_axis=RuntimeSlicePlaneAxis) first = project() metadata.source_component_metadata = {"well": "B02"} @@ -209,8 +208,8 @@ def project(): assert first.metadata.source_component_metadata["well"] == "A01" assert second.metadata.source_component_metadata["well"] == "B02" - assert first.metadata.plane_axis is None - assert second.metadata.plane_axis is None + assert first.metadata.plane_axis.unstacked + assert second.metadata.plane_axis.unstacked np.testing.assert_array_equal(first.mask, masks[1]) np.testing.assert_array_equal(second.mask, masks[1]) @@ -220,14 +219,14 @@ def test_source_binding_runtime_projection_preserves_shared_spatial_mask() -> No mask = np.ones((4, 5), dtype=bool) mask[1, 2] = False payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_spatial_domain=SourceSpatialDomain(source_shape_yx=(4, 5)), ).payload_with(data, mask) projected = RuntimeSliceProjection.value_for_slice( payload, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.SOURCE_BINDING, + axis=SourceBindingPlaneAxis, plane_index=1, axis_size=2, ), @@ -243,7 +242,7 @@ def test_runtime_plane_projection_does_not_infer_undeclared_ndarray_axis() -> No projected = RuntimeSliceProjection.value_for_slice( value, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, plane_index=0, axis_size=2, ), @@ -276,14 +275,14 @@ def test_image_bundle_composition_declares_source_binding_axis() -> None: assert composition.execution_mode is FullStackExecution assert composition.payload.metadata.plane_axis is ( - RuntimePlaneAxis.SOURCE_BINDING + SourceBindingPlaneAxis ) assert composition.payload.data.shape == (2, 4, 5) def test_single_runtime_slice_payload_is_not_repacked_as_source_binding() -> None: payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.zeros((2, 4, 5), dtype=np.uint16), None) composition = compose_aligned_image_payload("MeasureImage", (payload,)) @@ -291,14 +290,14 @@ def test_single_runtime_slice_payload_is_not_repacked_as_source_binding() -> Non assert composition.execution_mode is NaturalExecution assert composition.payload is payload assert composition.payload.metadata.plane_axis is ( - RuntimePlaneAxis.RUNTIME_SLICE + RuntimeSlicePlaneAxis ) def test_composed_runtime_slice_projection_ignores_outer_file_cardinality() -> None: payloads = tuple( ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.zeros((2, 4, 5), dtype=np.uint16), None) for _ in range(2) ) @@ -310,7 +309,7 @@ def test_composed_runtime_slice_projection_ignores_outer_file_cardinality() -> N ) assert projection is not None - assert projection.axis is RuntimePlaneAxis.RUNTIME_SLICE + assert projection.axis is RuntimeSlicePlaneAxis assert projection.axis_size == 2 assert projection.source_aliases == () @@ -349,7 +348,7 @@ def test_source_bundle_projection_uses_payload_cardinality_not_outer_bindings() ) assert projection is not None - assert projection.axis is RuntimePlaneAxis.SOURCE_BINDING + assert projection.axis is SourceBindingPlaneAxis assert projection.axis_size == 2 assert projection.source_aliases == ("CropBlue", "CropGreen") @@ -454,7 +453,7 @@ def test_named_outputs_keep_active_identity_when_restacked_for_adjacent_step() - restacked = stack_image_payloads( packed.slices, - metadata_mode=ImagePayloadMetadataCompositionMode.STACK, + metadata_mode=RuntimeSlicePlaneAxis, ) provenance = restacked.metadata.source_provenance diff --git a/tests/unit/test_image_stream_request_projection.py b/tests/unit/test_image_stream_request_projection.py index 3b395fbfde..411067101b 100644 --- a/tests/unit/test_image_stream_request_projection.py +++ b/tests/unit/test_image_stream_request_projection.py @@ -1,5 +1,6 @@ """Request-owned display projection uses declarations, never array-rank guesses.""" +from openhcs.core.runtime_plane_projection import SourceBindingPlaneAxis from types import SimpleNamespace from dataclasses import replace @@ -68,7 +69,7 @@ def request(request_type=ImageStreamingRequest): ) -@pytest.mark.parametrize("axis", tuple(RuntimePlaneAxis)) +@pytest.mark.parametrize("axis", RuntimePlaneAxis.leading_axes()) @pytest.mark.parametrize("color,masked", ((False, False), (False, True), (True, True))) def test_declared_singleton_projection_retains_color_mask_and_source_context( axis, color, masked @@ -79,7 +80,7 @@ def test_declared_singleton_projection_retains_color_mask_and_source_context( projected.data, original.data[0] ) metadata = projected.metadata - assert metadata.plane_axis is None + assert metadata.plane_axis.unstacked assert metadata.axis_position(ColourAxis) == (-1 if color else None) assert metadata.source_component_metadata["channel"] == 2 assert metadata.source_image_names == ("FITC",) @@ -94,7 +95,7 @@ def test_declared_singleton_projection_retains_color_mask_and_source_context( ) -@pytest.mark.parametrize("axis", tuple(RuntimePlaneAxis)) +@pytest.mark.parametrize("axis", RuntimePlaneAxis.leading_axes()) def test_multi_plane_stack_is_not_collapsed(axis): original = image_with_declaration(axis, count=2) assert request().project_image(original) is original @@ -112,7 +113,7 @@ def test_unbound_arrays_are_never_reinterpreted_as_source_planes(shape): assert request().project_image(original) is original -@pytest.mark.parametrize("axis", tuple(RuntimePlaneAxis)) +@pytest.mark.parametrize("axis", RuntimePlaneAxis.leading_axes()) def test_singleton_declaration_rejects_mismatched_native_shape(axis): original = image_with_declaration(axis) metadata = original.metadata @@ -123,7 +124,7 @@ def test_singleton_declaration_rejects_mismatched_native_shape(axis): def test_declared_axis_without_runtime_provenance_is_not_fabricated(): original = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.SOURCE_BINDING + plane_axis=SourceBindingPlaneAxis ).payload_with(np.zeros((1, 5, 6))) assert request().project_image(original) is original @@ -145,7 +146,7 @@ def singleton_plane_projection(self): class CalibratedMetadata(ObservedProof, PhysicalProof, ImagePayloadMetadata): pass - original = image_with_declaration(RuntimePlaneAxis.SOURCE_BINDING, masked=True) + original = image_with_declaration(SourceBindingPlaneAxis, masked=True) metadata = CalibratedMetadata.from_mapping( to_jsonable(original.metadata) ) @@ -188,7 +189,7 @@ class CalibratedFullWindow( ): pass - image = image_with_declaration(RuntimePlaneAxis.SOURCE_BINDING, masked=True) + image = image_with_declaration(SourceBindingPlaneAxis, masked=True) metadata = image.metadata filename = "image.tif" declaration = SourcePlaneProjection( diff --git a/tests/unit/test_k1_plane_mode_guards.py b/tests/unit/test_k1_plane_mode_guards.py new file mode 100644 index 0000000000..62e39cc782 --- /dev/null +++ b/tests/unit/test_k1_plane_mode_guards.py @@ -0,0 +1,218 @@ +"""K1 guards: plane modes are families and their absence is a member, not None. + +``RuntimePlaneAxis`` (with ``NoPlaneAxis``), ``ObjectLabelDomainScope`` and the +whole-axis / selected-plane projections are class families. Callers ask a +member (``leads_data``, ``one_plane_per_runtime_slice``, ``ids_per_plane``, +``selects_one_plane``) or call its methods; they never compare against a +member or against ``None``. +""" + +from __future__ import annotations + +import ast +from dataclasses import fields +from functools import lru_cache +from pathlib import Path + +import pytest + +from openhcs.core.runtime_object_label_domains import ObjectLabelDomainScope +from openhcs.core.runtime_plane_projection import ( + NoPlaneAxis, + RuntimePlaneAxis, + RuntimePlaneAxisValueProjection, + RuntimePlaneProjection, + RuntimeSliceProjectableValue, + SelectedPlaneProjection, + StackPlaneSelection, + WholeAxisProjection, + WholeStackProjection, +) + +ROOT = Path(__file__).resolve().parents[2] +OPENHCS = ROOT / "openhcs" + +DELETED_NAMES = { + "ImagePayloadMetadataCompositionMode", + "RuntimePlaneAxisStrategy", + "RuntimeSlicePlaneAxisStrategy", + "SourceBindingPlaneAxisStrategy", + "ObjectLabelPlaneDomainStrategy", + "PayloadObjectLabelPlaneDomainStrategy", + "PlaneObjectLabelPlaneDomainStrategy", + "image_metadata_of", + "array_data_of", +} + + +@lru_cache(maxsize=1) +def _trees() -> dict[str, ast.Module]: + return { + path.relative_to(ROOT).as_posix(): ast.parse(path.read_text(encoding="utf-8")) + for path in sorted(OPENHCS.rglob("*.py")) + } + + +def _member_names() -> frozenset[str]: + """Every member class of the two plane-mode families, from their registries.""" + return frozenset( + member.__name__ + for family in (RuntimePlaneAxis, ObjectLabelDomainScope) + for member in family.__registry__.values() + ) + + +def _names(node: ast.AST) -> set[str]: + return { + child.id if isinstance(child, ast.Name) else child.attr + for child in ast.walk(node) + if isinstance(child, ast.Name | ast.Attribute) + } + + +def test_deleted_plane_mode_names_are_gone() -> None: + found = sorted( + f"{module}:{node.lineno}:{name}" + for module, tree in _trees().items() + for node in ast.walk(tree) + for name in ( + getattr(node, "id", None), + getattr(node, "attr", None), + getattr(node, "name", None), + ) + if name in DELETED_NAMES + ) + assert not found, "\n".join(found) + + +def test_no_code_compares_against_a_plane_mode_member() -> None: + members = _member_names() + assert {"NoPlaneAxis", "PayloadLabelScope"} <= members + found = sorted( + f"{module}:{node.lineno}" + for module, tree in _trees().items() + for node in ast.walk(tree) + if isinstance(node, ast.Compare) + and any( + isinstance(operand, ast.Name | ast.Attribute | ast.Tuple | ast.List | ast.Set) + and _names(operand) & members + for operand in (node.left, *node.comparators) + ) + ) + assert not found, "\n".join(found) + + +def test_no_plane_axis_is_compared_with_none() -> None: + def is_plane_axis(node: ast.expr) -> bool: + name = ( + node.id if isinstance(node, ast.Name) + else node.attr if isinstance(node, ast.Attribute) + else "" + ) + # ``indexed_source_plane_axis`` is a component name in materialization (K8). + return name.endswith("plane_axis") and name != "indexed_source_plane_axis" + + found = sorted( + f"{module}:{node.lineno}" + for module, tree in _trees().items() + for node in ast.walk(tree) + if isinstance(node, ast.Compare) + and is_plane_axis(node.left) + and any( + isinstance(comparator, ast.Constant) and comparator.value is None + for comparator in node.comparators + ) + ) + assert not found, "\n".join(found) + + +def test_no_projection_plane_index_is_compared_with_none() -> None: + found = sorted( + f"{module}:{node.lineno}" + for module, tree in _trees().items() + for node in ast.walk(tree) + if isinstance(node, ast.Compare) + and isinstance(node.left, ast.Attribute) + and node.left.attr == "plane_index" + and isinstance(node.left.value, ast.Name | ast.Attribute) + and ( + node.left.value.id + if isinstance(node.left.value, ast.Name) + else node.left.value.attr + ).endswith("projection") + and any( + isinstance(comparator, ast.Constant) and comparator.value is None + for comparator in node.comparators + ) + ) + assert not found, "\n".join(found) + + +def test_plane_index_exists_only_on_the_selecting_members() -> None: + def field_names(cls: type) -> set[str]: + return {member.name for member in fields(cls)} + + assert "plane_index" not in field_names(WholeAxisProjection) + assert "plane_index" not in field_names(WholeStackProjection) + assert "plane_index" in field_names(SelectedPlaneProjection) + assert "plane_index" in field_names(StackPlaneSelection) + for family in (RuntimePlaneAxisValueProjection, RuntimePlaneProjection): + with pytest.raises(TypeError): + family() # abstract: every projection is one of the two members + + +def test_every_plane_axis_answers_the_family_questions() -> None: + assert set(RuntimePlaneAxis.leading_axes()) == { + axis for axis in RuntimePlaneAxis.__registry__.values() if axis.leads_data + } + assert NoPlaneAxis not in RuntimePlaneAxis.leading_axes() + for name, axis in RuntimePlaneAxis.__registry__.items(): + assert RuntimePlaneAxis.named(name) is axis + assert axis.axis_when_stacked().leads_data + assert axis.unstacked == (not axis.leads_data) + assert ( + axis.one_plane_per_runtime_slice + axis.one_plane_per_source_binding + == axis.leads_data + ) + for scope in ObjectLabelDomainScope.__registry__.values(): + assert scope.ids_span_payload == (not scope.ids_per_plane) + + +FAMILY_FLAGS = { + "leads_data", + "unstacked", + "one_plane_per_runtime_slice", + "one_plane_per_source_binding", + "keeps_image_whole", + "ids_per_plane", + "ids_span_payload", + "preserves_complete_axis", + "selects_one_plane", +} + + +def test_family_flags_are_read_positively() -> None: + """Each question has a positive spelling or a ``require_*`` method; none is negated.""" + found = sorted( + f"{module}:{node.lineno}" + for module, tree in _trees().items() + for node in ast.walk(tree) + if isinstance(node, ast.UnaryOp) + and isinstance(node.op, ast.Not) + and isinstance(node.operand, ast.Attribute) + and node.operand.attr in FAMILY_FLAGS + and not ( + isinstance(node.operand.value, ast.Name) and node.operand.value.id == "self" + ) + ) + assert not found, "\n".join(found) + + +def test_image_metadata_and_array_data_are_runtime_value_methods() -> None: + for name in ( + "image_metadata", + "array_data", + "slice_source_provenances", + "scalar_source_provenance", + ): + assert name in vars(RuntimeSliceProjectableValue) diff --git a/tests/unit/test_loaded_pattern_group_ownership.py b/tests/unit/test_loaded_pattern_group_ownership.py index 7201392529..d23365d439 100644 --- a/tests/unit/test_loaded_pattern_group_ownership.py +++ b/tests/unit/test_loaded_pattern_group_ownership.py @@ -1,5 +1,8 @@ """Loaded cohorts retain original input facts while plans and call images remain live.""" +from openhcs.core.runtime_plane_projection import NoPlaneAxis +from openhcs.core.runtime_object_label_domains import PlaneLabelScope +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from dataclasses import FrozenInstanceError, fields, replace import inspect import pickle @@ -379,7 +382,6 @@ def _stored_primary_fixture(*, preserves_main_flow=False): from openhcs.core.pipeline.path_planner import PathPlanner, PathPlannerArtifactStage from openhcs.core.runtime_adapters import runtime_adapter from openhcs.core.runtime_artifact_values import RuntimeValue - from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.component_group_scope import RuntimeExecutionAxisScope request, _, payload = _fixture() @@ -443,7 +445,7 @@ def consume(image, *, runtime): payload = ( payload.metadata .replace_fields( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) .payload_with(payload.data, np.ones((2, 3, 4), dtype=bool)) ) @@ -780,7 +782,7 @@ def test_initial_source_input_uses_loaded_declared_cohort_without_workspace_read from openhcs.core.artifacts.kinds import ImageArtifactType from openhcs.core.pipeline.function_contracts import artifact_inputs from openhcs.core.memory.decorators import numpy as numpy_memory - from openhcs.core.runtime_plane_projection import RuntimePlaneAxis, RuntimePlaneProjection + from openhcs.core.runtime_plane_projection import RuntimePlaneProjection from openhcs.core.source_bindings import NamedSourceBinding request, paths, payload = _fixture() @@ -817,7 +819,7 @@ def consume(image): request.execution_plan.compiled_function_pattern = pattern request = replace(request, compiled_group=pattern.default_group) payload = payload.metadata.replace_fields( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(payload.data, np.ones((2, 3, 4), dtype=bool)) payload = binding.apply_loaded_payload(payload, source_context=None) loaded = PatternGroupData.from_loaded_group(request, paths, payload) @@ -828,8 +830,10 @@ def consume(image): RuntimePlaneProjection.stack(2), payload, "numpy", ) adapter = executor.runtime_adapter_request(payload) - adapter = replace(adapter, plane_projection=RuntimePlaneProjection( - plane_index=plane_index, plane_count=2, + adapter = replace(adapter, plane_projection=( + RuntimePlaneProjection.stack(2) + if plane_index is None + else RuntimePlaneProjection.selected(plane_index, 2) )) def forbidden_original_read(*args, **kwargs): @@ -844,7 +848,7 @@ def forbidden_original_read(*args, **kwargs): np.testing.assert_array_equal(value.mask, np.ones(expected.shape, dtype=bool)) assert value.metadata.source_provenance.has_values assert value.metadata.plane_axis is ( - RuntimePlaneAxis.RUNTIME_SLICE if plane_index is None else None + RuntimeSlicePlaneAxis if plane_index is None else NoPlaneAxis ) changed = replace(edge, main_flow_projection=None) @@ -880,10 +884,8 @@ def test_original_source_admission_retains_singleton_alignment_and_label_project AlignedImageStack, aligned_image_stack_kwargs, stack_image_payloads, ) from openhcs.core.callable_contract import ImagePayloadConsumption - from openhcs.core.runtime_image_values import ImagePayloadMetadataCompositionMode - from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.runtime_object_labels import ObjectLabelSet, ObjectLabelVariantData - from openhcs.core.runtime_object_label_domains import ObjectLabelDomain, ObjectLabelDomainScope + from openhcs.core.runtime_object_label_domains import ObjectLabelDomain from openhcs.core.source_binding_selection import SourceUniverseRequest from openhcs.core.source_spatial_domain import VolumeSourceSpatialDomain @@ -894,7 +896,7 @@ def test_original_source_admission_retains_singleton_alignment_and_label_project source_path="source.tif", source_image_names=("Original",), ).payload_with(pixels, mask) original = stack_image_payloads( - (scalar,), metadata_mode=ImagePayloadMetadataCompositionMode.STACK, + (scalar,), metadata_mode=RuntimeSlicePlaneAxis, ) held = SourceUniverseRequest.admit_source_artifact_cohort( scalar, source_binding_plan=plan, member_count=1, @@ -908,14 +910,14 @@ def test_original_source_admission_retains_singleton_alignment_and_label_project assert isinstance(composition.payload, AlignedImageStack) labels = ObjectLabelSet( name="Objects", variant_data=ObjectLabelVariantData(labels=(pixels > 5)[None].astype(np.int32)), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, - domain=ObjectLabelDomain(scope=ObjectLabelDomainScope.PLANE, declared_object_id_domains=((1,),)), + plane_axis=RuntimeSlicePlaneAxis, + domain=ObjectLabelDomain(scope=PlaneLabelScope, declared_object_id_domains=((1,),)), ) projected = aligned_image_stack_kwargs( {"object_labels": labels}, 0, 1, reference_payload=composition.payload.slices[0], )["object_labels"] assert projected.shape == (3, 4) - assert projected.plane_axis is None + assert projected.plane_axis.unstacked # Already held multi-plane cohorts retain their pixel buffer, and literal # whole-image volumes retain their intrinsic axes independently of transport. retained = SourceUniverseRequest.admit_source_artifact_cohort( @@ -929,4 +931,4 @@ def test_original_source_admission_retains_singleton_alignment_and_label_project volume, source_binding_plan=plan, member_count=1, ) assert admitted_volume.data.shape == (2, 3, 4) - assert admitted_volume.metadata.plane_axis is None + assert admitted_volume.metadata.plane_axis.unstacked diff --git a/tests/unit/test_logical_image_output_snapshot.py b/tests/unit/test_logical_image_output_snapshot.py index 1ce1998e68..0621c69532 100644 --- a/tests/unit/test_logical_image_output_snapshot.py +++ b/tests/unit/test_logical_image_output_snapshot.py @@ -1,4 +1,5 @@ +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from tests.unit.saved_output_dialect import SAVED_OUTPUT_DIALECT """Typed image sets retain physical coverage across volume/plane exports.""" @@ -23,7 +24,6 @@ ) from openhcs.core.runtime_exports import RuntimeExportObservation from openhcs.core.runtime_image_values import ImageMaskDomain, ImagePayloadMetadata -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.source_matching import SourceImageSetIdentityPolicy from openhcs.core.source_projection import ( OpenHCSPlaneAddress, @@ -140,7 +140,7 @@ def test_projection_owner_partitions_whole_exports_and_scalar_cohorts(exported_v projections[0], address=None, ref=SourcePixelRef("disk", "whole-volume.tiff"), - image_metadata=ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE), + image_metadata=ImagePayloadMetadata(plane_axis=RuntimeSlicePlaneAxis), ) declared = SourceProjectionSet((whole_image, *reversed(projections))) whole, planes = declared.image_export_groups(Z_STACK) @@ -211,7 +211,7 @@ def test_producer_and_execution_cohorts_remain_distinct(exported_volume, change) @pytest.mark.parametrize( "metadata_changes, message", ( - ({"plane_axis": RuntimePlaneAxis.RUNTIME_SLICE}, "scalar image metadata"), + ({"plane_axis": RuntimeSlicePlaneAxis}, "scalar image metadata"), ({"axes": PayloadAxes.colour_samples(0)}, "declared non-spatial axes"), ({"source_dtype": "uint8"}, "declared dtype"), ({"mask_defines_border": True}, "incompatible image metadata"), diff --git a/tests/unit/test_materialization_core.py b/tests/unit/test_materialization_core.py index 0ddc8075b0..eed7b4b678 100644 --- a/tests/unit/test_materialization_core.py +++ b/tests/unit/test_materialization_core.py @@ -1,3 +1,6 @@ +from openhcs.core.runtime_object_label_domains import PayloadLabelScope, PlaneLabelScope +from openhcs.core.runtime_plane_projection import NoPlaneAxis +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis import json from pathlib import Path from functools import partial @@ -69,7 +72,6 @@ ) from openhcs.core.runtime_object_label_domains import ( ObjectLabelDomain, - ObjectLabelDomainScope, ) from openhcs.core.runtime_object_labels import ( ObjectLabelRepresentation, @@ -78,7 +80,6 @@ ObjectLabelVariantData, ) from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisValueProjection, ) from openhcs.core.runtime_sparse_labels import SparseIJVLabelRows @@ -151,7 +152,7 @@ def test_retained_full_stack_stream_preserves_absolute_calibration_and_integer_p labels[1, 6, 7] = 2 spacing = SourceVoxelSpacing((1.3556, 1.3556)) metadata = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_voxel_spacing=spacing, source_spatial_domain=SourceSpatialDomain( origin_yx=(0, 0), source_shape_yx=(8, 9) @@ -183,7 +184,7 @@ def test_retained_full_stack_stream_preserves_absolute_calibration_and_integer_p image=metadata.payload_with(np.zeros_like(labels)), labels=labels, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.SOURCE_BINDING, axis_size=2 + axis=SourceBindingPlaneAxis, axis_size=2 ), ).payload() filemanager = FileManager({"memory": MemoryStorageBackend()}) @@ -556,7 +557,7 @@ def test_indexed_image_materialization_streams_each_declared_component_plane( payload = ImageMetadataPayload( data=data, metadata=ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, axes=PayloadAxes.colour_samples(-1), source_image_provenance_planes=( SourceImageProvenancePlanes.from_components( @@ -656,10 +657,10 @@ def _addressable_roi_label_payload( ): return ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=_two_plane_roi_labels()), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=(first_path, second_path), @@ -671,10 +672,10 @@ def _addressable_roi_label_payload( def _unaddressed_roi_label_payload(): return ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=_two_plane_roi_labels()), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=(None, None), component_metadata=(None, None) @@ -888,8 +889,8 @@ def test_payload_label_roi_reopen_preserves_geometric_plane_domain( labels[2, 1:4, 2:5] = 7 payload = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - domain=ObjectLabelDomain(scope=ObjectLabelDomainScope.PAYLOAD), - plane_axis=None, + domain=ObjectLabelDomain(scope=PayloadLabelScope), + plane_axis=NoPlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("/source/volume.ome.tif",) * 4, component_metadata=tuple( @@ -908,9 +909,9 @@ def test_payload_label_roi_reopen_preserves_geometric_plane_domain( ) rois = load_rois_from_zip(Path(archive)) metadata = ROIArchiveSourceMetadata.decode(rois) - assert payload.plane_axis is None + assert payload.plane_axis.unstacked assert metadata.plane_axis is ( - RuntimePlaneAxis.RUNTIME_SLICE if variable_components else None + RuntimeSlicePlaneAxis if variable_components else NoPlaneAxis ) assert metadata.source_voxel_spacing.values_zyx == (2.0, 0.65, 0.65) domain = ROIArchiveSourceMetadata.source_component_domain(rois, metadata) @@ -1176,7 +1177,7 @@ def test_tiff_stack_splits_scalar_3d_stack_by_plane() -> None: stack = ImageMetadataPayload( data=np.zeros((3, 5, 7), dtype=np.uint8), metadata=ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ), ) @@ -1201,9 +1202,9 @@ def test_tiff_stack_streaming_saves_per_slice_component_metadata() -> None: fm = _RecordingFileManager() payload = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=np.zeros((2, 5, 7), dtype=np.int32)), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((), ()), ), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( @@ -1242,7 +1243,7 @@ def test_tiff_stack_streaming_projects_scalar_metadata_over_declared_stack_axis( payload = ImageMetadataPayload( data=np.zeros((2, 5, 7), dtype=np.float32), metadata=ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_component_metadata={ "well": "A01", "site": 1, @@ -1365,7 +1366,7 @@ def test_tiff_stack_projection_uses_artifact_scalar_z_origin() -> None: def test_tiff_stack_streaming_preserves_runtime_source_plane_metadata() -> None: fm = _RecordingFileManager() payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_path="/tmp/A01_s001_w3_z001_t001.tif", source_component_metadata={ "well": "A01", @@ -1413,7 +1414,7 @@ def test_tiff_stack_streaming_projects_artifact_identity_over_declared_stack_axi data=ImageMetadataPayload( data=np.zeros((2, 5, 7), dtype=np.float32), metadata=ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ), ), path="/tmp/A01_s001_w3_z001_t001_DerivedStack_step6", @@ -1611,10 +1612,10 @@ def test_roi_materialization_extracts_each_plane_from_object_label_stack() -> No labels[1, 4:7, 4:7] = 2 payload = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) @@ -1660,7 +1661,7 @@ def test_roi_materialization_preserves_payload_scoped_volume_in_one_archive() -> ) rois = fm.load(out, "memory") - assert payload.object_label_domain().scope is ObjectLabelDomainScope.PAYLOAD + assert payload.object_label_domain().scope.ids_span_payload assert out == "/tmp/A01_Nuclei_step3_rois.roi.zip" assert [roi.metadata["label"] for roi in rois] == [1, 2] assert [roi.metadata["plane_indices"] for roi in rois] == [(0,), (1,)] @@ -1711,7 +1712,7 @@ def test_roi_streaming_maps_payload_scoped_volume_planes_from_provenance(viewer_ roi_content, roi_path, _backend, stream_kwargs = roi_saves[0] stream_request = stream_kwargs["stream_request"] item_fields = stream_request.source.item_fields - assert item_fields["plane_axis"] == RuntimePlaneAxis.RUNTIME_SLICE.value + assert item_fields["plane_axis"] == RuntimeSlicePlaneAxis.name assert item_fields["plane_component_values"] == {"z_index": ["1", "2"]} napari_backend = NapariStreamingBackend() @@ -1728,7 +1729,7 @@ def test_roi_streaming_maps_payload_scoped_volume_planes_from_provenance(viewer_ display_payload_extra=napari_backend.display_payload_extra(stream_request), ), ).batch_images[0] - assert streamed_item["plane_axis"] == RuntimePlaneAxis.RUNTIME_SLICE.value + assert streamed_item["plane_axis"] == RuntimeSlicePlaneAxis.name assert streamed_item["plane_component_values"] == {"z_index": ["1", "2"]} @@ -1739,10 +1740,10 @@ def test_roi_materialization_projects_singleton_object_label_stack() -> None: labels[0, 2:6, 3:7] = 1 payload = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,),), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( component_metadata=({"well": "A01", "site": 1, "channel": 1},), @@ -1770,10 +1771,10 @@ def test_roi_streaming_preserves_singleton_projected_source_metadata() -> None: labels[0, 2:6, 3:7] = 1 payload = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,),), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("/input/A01_s001_w1_z001_t001.tif",), @@ -1816,7 +1817,7 @@ def test_roi_streaming_maps_singleton_plane_from_exact_output_component( "well": "A01", }, ) - assert payload.object_label_domain().scope is ObjectLabelDomainScope.PAYLOAD + assert payload.object_label_domain().scope.ids_span_payload materialize( MaterializationSpec(ROIOptions(min_area=0)), @@ -1833,7 +1834,7 @@ def test_roi_streaming_maps_singleton_plane_from_exact_output_component( roi_content, roi_path, _backend, stream_kwargs = roi_saves[0] stream_request = stream_kwargs["stream_request"] assert stream_request.source.item_fields == { - "plane_axis": RuntimePlaneAxis.RUNTIME_SLICE.value, + "plane_axis": RuntimeSlicePlaneAxis.name, "plane_component_values": {"site": ["1"]}, "spatial_origin_yx": [0, 0], "source_spatial_shape_yx": [8, 8], @@ -1843,7 +1844,7 @@ def test_roi_streaming_maps_singleton_plane_from_exact_output_component( source_shape_yx=(8, 8), value_name="ROI materialization source image", ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).to_viewer_image_metadata(), } @@ -1861,7 +1862,7 @@ def test_roi_streaming_maps_singleton_plane_from_exact_output_component( display_payload_extra=napari_backend.display_payload_extra(stream_request), ), ).batch_images[0] - assert streamed_item["plane_axis"] == RuntimePlaneAxis.RUNTIME_SLICE.value + assert streamed_item["plane_axis"] == RuntimeSlicePlaneAxis.name assert streamed_item["plane_component_values"] == {"site": ["1"]} @@ -1929,10 +1930,10 @@ def test_roi_materialization_splits_addressable_label_planes_for_streaming() -> labels[1, 4:7, 4:7] = 2 payload = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=( @@ -1983,10 +1984,10 @@ def test_roi_materialization_replaces_parser_equivalent_reference_source_prefix( labels[1, 4:7, 4:7] = 2 payload = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=( @@ -2130,10 +2131,10 @@ def test_roi_streaming_applies_target_metadata_without_scalar_stream_metadata() labels[1, 4:7, 4:7] = 2 payload = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=( @@ -2171,10 +2172,10 @@ def test_roi_materialization_coalesces_duplicate_stream_targets() -> None: labels[1, 4:7, 4:7] = 2 payload = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=( @@ -2221,10 +2222,10 @@ def test_roi_materialization_coalesces_source_stack_archive_with_artifact_identi labels[1, 4:7, 4:7] = 2 payload = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=( @@ -2482,10 +2483,10 @@ def test_roi_materialization_spec_reports_projected_source_identities() -> None: labels[1, 4:7, 4:7] = 2 payload = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=( @@ -2590,7 +2591,7 @@ def test_declared_volume_save_preserves_pixels_and_exact_plane_selection( else SourceSpatialDomain(source_shape_yx=(4, 5), origin_yx=(0, 0)) ) metadata = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_spatial_domain=domain, source_voxel_spacing=SourceVoxelSpacing((2.0, 1.0, 0.5)), source_image_names=("DNA",), @@ -2687,7 +2688,7 @@ def test_declared_volume_save_preserves_pixels_and_exact_plane_selection( plane = RuntimeSliceProjection.value_for_slice( reloaded, RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, axis_size=3 + axis=RuntimeSlicePlaneAxis, axis_size=3 ).selected_plane(index), ) np.testing.assert_array_equal(plane.data, pixels[index]) diff --git a/tests/unit/test_materialization_source_extension.py b/tests/unit/test_materialization_source_extension.py index 7d37b93b14..7d8f1ef029 100644 --- a/tests/unit/test_materialization_source_extension.py +++ b/tests/unit/test_materialization_source_extension.py @@ -1,5 +1,6 @@ """Issues257/264: preserve source identity into ordinary image publication.""" +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis from pathlib import Path from dataclasses import replace from types import SimpleNamespace @@ -11,7 +12,6 @@ from openhcs.core.runtime_image_values import ( ImagePayloadMetadata, - ImagePayloadMetadataCompositionMode, ) from openhcs.core.source_image_provenance import ( SourceImageIdentity, @@ -248,7 +248,7 @@ def test_issue264_header_only_metadata_reaches_actual_image_writer(): @pytest.mark.parametrize("well", ("A01", "image.ome.tif")) @pytest.mark.parametrize("extension", (".tif", ".ome.tif")) @pytest.mark.parametrize("retain_declared_extension", (False, True)) -@pytest.mark.parametrize("mode", tuple(ImagePayloadMetadataCompositionMode)) +@pytest.mark.parametrize("mode", (RuntimeSlicePlaneAxis, SourceBindingPlaneAxis)) def test_real_source_schema_composition_to_named_output_and_image_materializer( well, extension, diff --git a/tests/unit/test_measurement_image_alignment.py b/tests/unit/test_measurement_image_alignment.py index 11c8431024..3548a9d045 100644 --- a/tests/unit/test_measurement_image_alignment.py +++ b/tests/unit/test_measurement_image_alignment.py @@ -1,3 +1,5 @@ +from openhcs.core.runtime_object_label_domains import PlaneLabelScope +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis from dataclasses import dataclass, replace import numpy as np @@ -20,14 +22,11 @@ ) from openhcs.core.runtime_object_label_domains import ( ObjectLabelDomain, - ObjectLabelDomainScope, - ObjectLabelPlaneDomainStrategy, ) from openhcs.core.runtime_object_labels import ( ObjectLabelRepresentation, ) from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisProjector, ) from openhcs.core.runtime_slice_alignment import RuntimeSliceAlignedValues @@ -96,7 +95,7 @@ def source_binding_axis_size( def _plane_domain(*ids: tuple[int, ...]) -> ObjectLabelDomain: return ObjectLabelDomain.declared( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=ids, ) @@ -128,7 +127,7 @@ def test_payload_scoped_labels_consume_declared_singleton_runtime_image_plane() image = ImageMetadataPayload( data=np.ones((1, 4, 5), dtype=np.float32), metadata=ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ), ) labels = ObjectLabelPayload( @@ -148,7 +147,7 @@ def test_payload_scoped_labels_preserve_declared_runtime_volume() -> None: image = ImageMetadataPayload( data=np.ones((2, 4, 5), dtype=np.float32), metadata=ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ), ) labels = ObjectLabelPayload( @@ -167,7 +166,7 @@ def test_payload_scoped_labels_reject_mismatched_declared_runtime_volume() -> No image = ImageMetadataPayload( data=np.ones((2, 4, 5), dtype=np.float32), metadata=ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ), ) labels = ObjectLabelPayload( @@ -185,7 +184,7 @@ def test_payload_scoped_labels_reject_non_runtime_image_plane_axis() -> None: image = ImageMetadataPayload( data=np.ones((1, 4, 5), dtype=np.float32), metadata=ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, ), ) labels = ObjectLabelPayload( @@ -244,14 +243,14 @@ def test_object_reference_preserves_nominal_image_payload_context() -> None: ) ) ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=_plane_domain((1,), (2,)), ) source = _MeasurementImageSource( ImageMetadataPayload( data=image, metadata=ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ), ), reference_domain=ObjectLabelsReference, @@ -260,7 +259,7 @@ def test_object_reference_preserves_nominal_image_payload_context() -> None: aligned = source.object_label_alignment_request(labels).aligned().image assert isinstance(aligned, ImageMetadataPayload) - assert aligned.metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert aligned.metadata.plane_axis is RuntimeSlicePlaneAxis np.testing.assert_array_equal(aligned.data, image) @@ -280,14 +279,14 @@ def test_explicit_projector_selects_declared_source_binding_planes() -> None: ) ) ), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, domain=_plane_domain((1,), (2,)), ) source = _MeasurementImageSource( ImageMetadataPayload( data=image, metadata=ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, ), ), source_aliases=("marker", "brightfield"), @@ -313,7 +312,7 @@ def test_object_reference_projects_declared_runtime_slice_only_with_context() -> ) ) ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=_plane_domain((1,), (2,)), ) source = _MeasurementImageSource( @@ -337,7 +336,7 @@ def test_object_reference_projects_declared_single_runtime_slice() -> None: variant_data=ObjectLabelVariantData( labels=np.full((1, 4, 5), 7, dtype=np.int32) ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=_plane_domain((7,)), ) source = _MeasurementImageSource( @@ -352,7 +351,7 @@ def test_object_reference_projects_declared_single_runtime_slice() -> None: ) assert ( - prepared.source_projected_payload.domain.scope is ObjectLabelDomainScope.PAYLOAD + prepared.source_projected_payload.domain.scope.ids_span_payload ) np.testing.assert_array_equal( prepared.measurement_labels, @@ -366,14 +365,14 @@ def test_object_reference_projects_image_with_declared_single_runtime_slice() -> variant_data=ObjectLabelVariantData( labels=np.full((1, 4, 5), 7, dtype=np.int32) ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=_plane_domain((7,)), ) source = _MeasurementImageSource( ImageMetadataPayload( data=image, metadata=ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ), ), reference_domain=ObjectLabelsReference, @@ -394,7 +393,7 @@ def test_object_reference_projects_local_singleton_in_wider_runtime_scope() -> N variant_data=ObjectLabelVariantData( labels=np.full((1, 4, 5), 7, dtype=np.int32) ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=_plane_domain((7,)), ) source = _MeasurementImageSource( @@ -409,7 +408,7 @@ def test_object_reference_projects_local_singleton_in_wider_runtime_scope() -> N ) assert ( - prepared.source_projected_payload.domain.scope is ObjectLabelDomainScope.PAYLOAD + prepared.source_projected_payload.domain.scope.ids_span_payload ) np.testing.assert_array_equal( prepared.measurement_labels, @@ -428,7 +427,7 @@ def test_projected_payload_domain_is_not_projected_twice() -> None: variant_data=ObjectLabelVariantData( labels=np.stack((runtime_planes, runtime_planes)) ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=_plane_domain((1, 2), (1, 2)), ) source = _MeasurementImageSource( @@ -488,7 +487,7 @@ def test_aligned_image_stack_accepts_nominal_runtime_slice_payload() -> None: ) ) ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=_plane_domain((1,), (2,)), ) source = _MeasurementImageSource(image) @@ -525,7 +524,7 @@ def test_aligned_image_stack_preserves_sparse_runtime_slice_payload() -> None: ) ), representation=ObjectLabelRepresentation.SPARSE_IJV, - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=_plane_domain((1,), (2,)), source_spatial_domain=SourceSpatialDomain(source_shape_yx=(4, 5)), ) @@ -546,12 +545,10 @@ def test_plane_domain_measurement_projection_preserves_sparse_storage() -> None: labels = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=sparse_labels), representation=ObjectLabelRepresentation.SPARSE_IJV, - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=_plane_domain((1,), (2,)), ) - strategy = ObjectLabelPlaneDomainStrategy.for_enum_member( - ObjectLabelDomainScope.PLANE - ) + strategy = PlaneLabelScope projection = strategy.measurement_projection( labels, _PlaneProjector(runtime_count=2), diff --git a/tests/unit/test_measurement_image_plane_projection.py b/tests/unit/test_measurement_image_plane_projection.py index 3c8724ef1f..7515de05c2 100644 --- a/tests/unit/test_measurement_image_plane_projection.py +++ b/tests/unit/test_measurement_image_plane_projection.py @@ -1,3 +1,5 @@ +from openhcs.core.runtime_object_label_domains import PlaneLabelScope +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis import numpy as np from openhcs.core.artifacts.specs import ArtifactSpec @@ -13,10 +15,8 @@ from openhcs.core.measurement_image_alignment import ObjectLabelsReference from openhcs.core.runtime_object_label_domains import ( ObjectLabelDomain, - ObjectLabelDomainScope, ) from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisProjector, RuntimePlaneAxisValueProjection, ) @@ -41,15 +41,15 @@ def runtime_slice_axis_size(self) -> int: def test_singleton_object_projection_consumes_measurement_image_plane_proof() -> None: image = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.ones((1, 4, 5), dtype=np.float32), None) labels = ObjectLabelPayload( variant_data=ObjectLabelVariantData( labels=np.ones((1, 4, 5), dtype=np.int32), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,),), ), ) @@ -57,7 +57,7 @@ def test_singleton_object_projection_consumes_measurement_image_plane_proof() -> source_image_name=None, payload=image, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=1, ), reference_domain=ObjectLabelsReference, @@ -82,5 +82,5 @@ def test_singleton_object_projection_consumes_measurement_image_plane_proof() -> ) assert aligned_source.plane_projection is None - assert aligned_source.payload.metadata.plane_axis is None + assert aligned_source.payload.metadata.plane_axis.unstacked assert np.shape(aligned_source.payload) == (4, 5) diff --git a/tests/unit/test_measureobjectintensitydistribution.py b/tests/unit/test_measureobjectintensitydistribution.py index ee78d01a33..2fdd15f5c5 100644 --- a/tests/unit/test_measureobjectintensitydistribution.py +++ b/tests/unit/test_measureobjectintensitydistribution.py @@ -1,3 +1,5 @@ +from openhcs.core.runtime_object_label_domains import PlaneLabelScope +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from dataclasses import replace import numpy as np @@ -20,9 +22,7 @@ ) from openhcs.core.runtime_object_label_domains import ( ObjectLabelDomain, - ObjectLabelDomainScope, ) -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.runtime_object_labels import ( ObjectLabelVariantData, ObjectLabelPayload, @@ -570,9 +570,9 @@ def test_measure_object_intensity_distribution_rejects_repeated_plane_domains(): variant_data=ObjectLabelVariantData(labels=labels), domain=ObjectLabelDomain( declared_object_id_domains=((1, 2), (1, 2)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) with pytest.raises(ValueError, match="already projected to one 2-D plane"): diff --git a/tests/unit/test_mixed_carrier_intensity_domain.py b/tests/unit/test_mixed_carrier_intensity_domain.py index 91e9e58209..b3b6ea7c53 100644 --- a/tests/unit/test_mixed_carrier_intensity_domain.py +++ b/tests/unit/test_mixed_carrier_intensity_domain.py @@ -1,5 +1,8 @@ """Current intensity units survive composition, projection and native storage.""" +from openhcs.core.runtime_plane_projection import NoPlaneAxis +from openhcs.core.runtime_plane_projection import SourceBindingPlaneAxis +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from dataclasses import dataclass import numpy as np @@ -12,7 +15,6 @@ from openhcs.core.image_file_serialization import ImageFileFormat, ImageFileSourceMetadata from openhcs.core.runtime_image_values import ( ImagePayloadMetadata, - ImagePayloadMetadataCompositionMode, ImageUnitIntervalIntensityMetadata, ) from openhcs.core.source_bindings import NamedSourceBinding @@ -33,7 +35,7 @@ def source(data, *, scale=255, channel_axis=None): @pytest.mark.parametrize('reverse', (False, True)) @pytest.mark.parametrize('color', ('replicated', 'distinct')) -@pytest.mark.parametrize('mode', tuple(ImagePayloadMetadataCompositionMode)) +@pytest.mark.parametrize('mode', (RuntimeSlicePlaneAxis, SourceBindingPlaneAxis)) def test_original_source_binding_composition_and_selected_cp_units(reverse, color, mode): from openhcs.interop.cellprofiler.image_normalization import normalize_cellprofiler_image_payload @@ -72,7 +74,7 @@ def test_raw_heterogeneous_declared_scales_survive_integer_float_promotion(rever source(np.full((2, 3), 15, dtype=np.float32), scale=30)] if reverse: inputs.reverse() - stack = stack_image_payloads(inputs, metadata_mode=ImagePayloadMetadataCompositionMode.STACK) + stack = stack_image_payloads(inputs, metadata_mode=RuntimeSlicePlaneAxis) assert stack.metadata.unit_interval_intensity is None np.testing.assert_allclose(stack.normalize_intensity_payload(), 0.5) for index, original in enumerate(inputs): @@ -153,7 +155,7 @@ class DeclaredComposition(*capabilities, ImagePayloadStackContext): np.full((2, 3), 32, dtype=np.uint8)) processed = ImagePayloadMetadata(unit_interval_intensity=ImageUnitIntervalIntensityMetadata()).payload_with( np.full((2, 3), 0.5, dtype=np.float32)) - result = DeclaredComposition((raw, processed), ImagePayloadMetadataCompositionMode.STACK).compose() + result = DeclaredComposition((raw, processed), RuntimeSlicePlaneAxis).compose() np.testing.assert_array_equal(result.data, 0.5) assert calls == ['normalization', 'pixels', 'mask'] assert DeclaredMetadata.__mro__[:3] == (DeclaredMetadata, NormalizationAudit, ImagePayloadMetadata) @@ -171,10 +173,10 @@ def test_saved_output_context_retains_the_independent_composed_buffer_domain(): raw = source(np.full((2, 3), 128, dtype=np.uint8)) normalized = raw.normalize_intensity_payload() inputs = (raw, normalized) - copied = stack_image_payloads(inputs, metadata_mode=ImagePayloadMetadataCompositionMode.STACK) + copied = stack_image_payloads(inputs, metadata_mode=RuntimeSlicePlaneAxis) restored = ImagePayloadStackComposition.with_saved_output_context( copied, inputs, tuple(value.metadata for value in inputs), - single_output_plane_axis=None, + single_output_plane_axis=NoPlaneAxis, ) assert restored.data is copied.data for index in (0, 1): @@ -189,7 +191,7 @@ def test_common_quantization_requires_every_current_plane_not_only_known_members normalized = raw.normalize_intensity_payload() remapped = normalized.metadata.without_unit_interval_intensity_scale().payload_with( np.full((2, 3), 0.123456, dtype=np.float32)) - mixed = stack_image_payloads((raw, remapped), metadata_mode=ImagePayloadMetadataCompositionMode.STACK) + mixed = stack_image_payloads((raw, remapped), metadata_mode=RuntimeSlicePlaneAxis) assert mixed.metadata.common_unit_interval_intensity_scale() is None assert mixed.metadata.for_leading_source_plane(0).unit_interval_intensity_scale == 255 diff --git a/tests/unit/test_named_checkpoint_publication.py b/tests/unit/test_named_checkpoint_publication.py index bf2a5462c4..c71767093d 100644 --- a/tests/unit/test_named_checkpoint_publication.py +++ b/tests/unit/test_named_checkpoint_publication.py @@ -56,7 +56,7 @@ def with_data(self, data): return replace(self, data=data) def resolve_source_context(self, source, projection): - assert projection is not None and projection.plane_index is None + assert projection is not None and projection.preserves_complete_axis assert projection.axis_size == 24 return source.metadata.collapse_leading_plane_axis().payload_with(self.data, None) @@ -145,7 +145,7 @@ def test_named_and_ordinary_checkpoint_inventory_owns_every_address(tmp_path, ma for item in metadata["subdirectories"]["checkpoints_results"]["source_projection"]: retained = ImagePayloadMetadata.from_mapping(item["image_metadata"]) assert retained.source_image_provenance_planes.contributor_count == 24 - assert retained.plane_axis is None + assert retained.plane_axis.unstacked # The workspace index exposes both relative and absolute lookup aliases. # Verify canonical storage inventory, not the number of lookup keys. all_inventory = { diff --git a/tests/unit/test_napari_streaming_handlers.py b/tests/unit/test_napari_streaming_handlers.py index 2a7db40304..2c509d96ec 100644 --- a/tests/unit/test_napari_streaming_handlers.py +++ b/tests/unit/test_napari_streaming_handlers.py @@ -1,4 +1,5 @@ from __future__ import annotations +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from dataclasses import dataclass, replace from itertools import product @@ -24,7 +25,6 @@ from openhcs.core.runtime_image_values import ( ImagePayloadMetadata, ) -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.source_spatial_domain import SourceSpatialDomain from openhcs.runtime.viewer_protocol import ( ViewerControlResponseField, @@ -1723,11 +1723,11 @@ def test_napari_display_pipeline_rejects_unbound_payload_local_image_axes(): with pytest.raises(ValueError, match="component-axis binding"): policy.axis_labels( np.zeros((8, 16, 16)), - ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE), + ImagePayloadMetadata(plane_axis=RuntimeSlicePlaneAxis), ) color_stack_metadata = ImagePayloadMetadata( axes=PayloadAxes.colour_samples(-1), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) with pytest.raises(ValueError, match="component-axis binding"): policy.axis_labels(np.zeros((4, 16, 16, 3)), color_stack_metadata) @@ -4648,7 +4648,7 @@ def __init__(self): np.zeros((4, 16, 16, 3)), ImagePayloadMetadata( axes=PayloadAxes.colour_samples(-1), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ), ), ): @@ -4743,7 +4743,7 @@ def __init__(self): ), image_metadata=ImagePayloadMetadata( axes=PayloadAxes.colour_samples(-1), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ), plane_component_domain=ViewerComponentValueDomainPayload(()), display_config=NapariDisplayConfig(), @@ -5829,7 +5829,7 @@ def test_napari_aggregate_axis_binding_uses_declared_component_not_equal_extent( {"site": 1, "channel": 1}, np.zeros((2, 3, 3), dtype=np.float32), image_metadata=ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ), plane_component_domain=_component_value_domain({"channel": [1, 2]}), ) diff --git a/tests/unit/test_native_viewer_presentation.py b/tests/unit/test_native_viewer_presentation.py index f384f17f4e..17043afa41 100644 --- a/tests/unit/test_native_viewer_presentation.py +++ b/tests/unit/test_native_viewer_presentation.py @@ -646,7 +646,7 @@ def test_selected_file_stream_restores_persisted_crop_calibration_and_native_bat assert ( len(payload.metadata.source_provenance.represented_source_identities) == 9 ) - assert payload.metadata.plane_axis is None + assert payload.metadata.plane_axis.unstacked np.testing.assert_array_equal(payload.data, array) # Native image secondaries have their own stored pixels, not the main-image @@ -666,7 +666,7 @@ def test_selected_file_stream_restores_persisted_crop_calibration_and_native_bat np.testing.assert_array_equal(artifact.data, label_pixels) assert artifact.metadata.source_dtype == "int64" assert artifact.metadata.axis_position(ColourAxis) is None - assert artifact.metadata.plane_axis is None + assert artifact.metadata.plane_axis.unstacked assert artifact.metadata.source_spatial_domain.source_shape_yx == (8, 9) assert ( artifact_components[str(artifact_path)]["well"] == components[paths[0]]["well"] @@ -701,7 +701,7 @@ def test_selected_file_stream_restores_persisted_crop_calibration_and_native_bat restored = ImagePayloadMetadata.from_viewer_image_metadata(wire_metadata) assert restored.source_voxel_spacing == spacing assert restored.source_spatial_domain == domain - assert restored.plane_axis is None + assert restored.plane_axis.unstacked stream_request = kwargs[ViewerStreamKwarg.STREAM_REQUEST.value] declared_domain = stream_request.message_extra["component_value_domain"] projected_metadata = stream_request.source.metadata.metadata_by_path diff --git a/tests/unit/test_opaque_main_flow_image_domain.py b/tests/unit/test_opaque_main_flow_image_domain.py index 5b72bd33b8..9a4705b6a8 100644 --- a/tests/unit/test_opaque_main_flow_image_domain.py +++ b/tests/unit/test_opaque_main_flow_image_domain.py @@ -1,5 +1,7 @@ """Whole image domains survive the producer/cache/next-load handoff.""" +from openhcs.core.runtime_plane_projection import NoPlaneAxis +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis from dataclasses import replace from types import SimpleNamespace @@ -14,7 +16,6 @@ ) from openhcs.core.function_patterns import MainFlowInputProjection, compile_function_pattern from openhcs.core.runtime_image_values import ImagePayloadMetadata -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.runtime_slice_projection import RuntimeSliceProjection from openhcs.core.source_spatial_domain import SourceSpatialDomain from openhcs.core.steps.function_runtime import PatternGroupExecutionRequest @@ -102,18 +103,18 @@ def test_opaque_whole_image_is_copied_without_a_new_execution_axis(shape, channe @pytest.mark.parametrize("count", [1, 3]) def test_declared_runtime_axis_retains_its_exact_count(count): pixels = np.arange(count * 4 * 5, dtype=np.float32).reshape(count, 4, 5) - payload = ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE).payload_with(pixels) + payload = ImagePayloadMetadata(plane_axis=RuntimeSlicePlaneAxis).payload_with(pixels) projected = _runtime()._project_output_slices(payload, ["source-1.tif", "source-2.tif"]) assert len(projected) == count - assert all(value.metadata.plane_axis is None for value, _context in projected) + assert all(value.metadata.plane_axis.unstacked for value, _context in projected) assert payload.data.shape == (count, 4, 5) - assert payload.metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert payload.metadata.plane_axis is RuntimeSlicePlaneAxis @pytest.mark.parametrize("named", [False, True]) -@pytest.mark.parametrize("axis", [None, RuntimePlaneAxis.RUNTIME_SLICE, RuntimePlaneAxis.SOURCE_BINDING]) +@pytest.mark.parametrize("axis", [NoPlaneAxis, RuntimeSlicePlaneAxis, SourceBindingPlaneAxis]) def test_nominal_alignment_owner_preserves_single_output_topology(named, axis): - shape = (3, 4, 5) if axis is None else (1, 4, 5) + shape = (3, 4, 5) if axis.unstacked else (1, 4, 5) payload = ImagePayloadMetadata(plane_axis=axis).payload_with(np.ones(shape, dtype=np.float32)) context = AlignedImageSliceContext.main_flow("Image") value = (ImageOutputBundle if named else AlignedImageStack)((payload,), (context,)) @@ -121,11 +122,11 @@ def test_nominal_alignment_owner_preserves_single_output_topology(named, axis): stack_payload = value.copy_projected_output_stack( projected, memory_type=MEMORY_TYPE_NUMPY, device_id=None, ) - if named and axis is not RuntimePlaneAxis.RUNTIME_SLICE: + if named and axis is not RuntimeSlicePlaneAxis: expected_shape, expected_axis = shape, axis else: - expected_shape = (1, 4, 5) if axis is RuntimePlaneAxis.RUNTIME_SLICE else (1, *shape) - expected_axis = RuntimePlaneAxis.RUNTIME_SLICE + expected_shape = (1, 4, 5) if axis is RuntimeSlicePlaneAxis else (1, *shape) + expected_axis = RuntimeSlicePlaneAxis assert stack_payload.data.shape == expected_shape assert stack_payload.metadata.plane_axis is expected_axis @@ -145,10 +146,10 @@ def test_raw_array_fallback_keeps_explicit_runtime_unstack_contract(count): payloads = tuple(payload for payload, _context in projected) cached = ImagePayloadStackComposition.with_saved_output_context( pixels, payloads, [payload.metadata for payload in payloads], - single_output_plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + single_output_plane_axis=RuntimeSlicePlaneAxis, ) assert cached.data.shape == (count, 4, 5) - assert cached.metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert cached.metadata.plane_axis is RuntimeSlicePlaneAxis class _MemoryFiles: @@ -172,12 +173,12 @@ def delete(self, path, backend): @pytest.mark.parametrize("cache_hit", [False, True]) -@pytest.mark.parametrize("axis", [None, RuntimePlaneAxis.RUNTIME_SLICE, RuntimePlaneAxis.SOURCE_BINDING]) +@pytest.mark.parametrize("axis", [NoPlaneAxis, RuntimeSlicePlaneAxis, SourceBindingPlaneAxis]) @pytest.mark.parametrize("shape", [(4, 5), (4, 5, 3), (3, 4, 5), (1, 4, 5)]) def test_save_and_next_load_preserve_domain_and_independent_cache(tmp_path, monkeypatch, cache_hit, axis, shape, raw_named=False): pixels = np.arange(np.prod(shape), dtype=np.float32).reshape(shape) channel_axis = -1 if shape == (4, 5, 3) else None - if axis is not None: + if axis.leads_data: pixels = pixels[None] channel_axis = 3 if channel_axis is not None else None mask = np.ones(pixels.shape[:-1] if channel_axis is not None else pixels.shape, dtype=bool) @@ -188,7 +189,7 @@ def test_save_and_next_load_preserve_domain_and_independent_cache(tmp_path, monk axes=PayloadAxes.colour_samples(channel_axis), plane_axis=axis, source_image_provenance_planes=( SourceImageProvenancePlanes.from_components(paths=(path,), component_metadata=(components,)) - if axis is not None else SourceImageProvenancePlanes() + if axis.leads_data else SourceImageProvenancePlanes() ), ) payload = pixels if raw_named else metadata.payload_with(pixels, mask) @@ -205,7 +206,7 @@ def test_save_and_next_load_preserve_domain_and_independent_cache(tmp_path, monk producer = replace(producer, pattern_group_info="whole") output = ( ImageOutputBundle((payload,), (AlignedImageSliceContext.main_flow("WholeImage"),)) - if axis is not None or raw_named else payload + if axis.leads_data or raw_named else payload ) records = producer._save_outputs(output, [path]) manifest = step_output_manifest(context) @@ -228,10 +229,10 @@ def test_save_and_next_load_preserve_domain_and_independent_cache(tmp_path, monk loaded = consumer.load_input_stack()[1] # A member saved without a plane axis is a one-member runtime-slice cohort, # exactly as it is when several members match. - expected_pixels = pixels if axis is not None else pixels[None] - expected_mask = mask if axis is not None or mask is None else mask[None] + expected_pixels = pixels if axis.leads_data else pixels[None] + expected_mask = mask if axis.leads_data or mask is None else mask[None] assert loaded.data.shape == expected_pixels.shape - assert loaded.metadata.plane_axis is (axis or RuntimePlaneAxis.RUNTIME_SLICE) + assert loaded.metadata.plane_axis is axis.axis_when_stacked() np.testing.assert_array_equal(loaded.data, expected_pixels) np.testing.assert_array_equal(loaded.mask, expected_mask) assert not np.shares_memory(loaded.data, pixels) @@ -254,14 +255,14 @@ def test_save_and_next_load_preserve_domain_and_independent_cache(tmp_path, monk @pytest.mark.parametrize("runtime_count", [1, 3]) def test_mixed_named_outputs_retain_each_original_axis_before_projection(runtime_count): contexts = (AlignedImageSliceContext.main_flow("Planes"), AlignedImageSliceContext.main_flow("Volume")) - planes = ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE).payload_with(np.ones((runtime_count, 4, 5), dtype=np.float32)) + planes = ImagePayloadMetadata(plane_axis=RuntimeSlicePlaneAxis).payload_with(np.ones((runtime_count, 4, 5), dtype=np.float32)) volume = ImagePayloadMetadata().payload_with(np.ones((4, 5) if runtime_count == 1 else (2, 4, 5), dtype=np.float32)) bundle = ImageOutputBundle((planes, volume), contexts) projected = _runtime()._project_output_slices(bundle, []) assert bundle.copy_projected_output_stack( projected, memory_type=MEMORY_TYPE_NUMPY, device_id=None, ) is None - assert [bundle.plane_axis_for_output_context(context) for _payload, context in projected] == [RuntimePlaneAxis.RUNTIME_SLICE] * runtime_count + [None] + assert [bundle.plane_axis_for_output_context(context) for _payload, context in projected] == [RuntimeSlicePlaneAxis] * runtime_count + [NoPlaneAxis] def test_passed_through_record_preserves_collapsed_and_filename_coordinates(tmp_path): @@ -271,12 +272,12 @@ def test_passed_through_record_preserves_collapsed_and_filename_coordinates(tmp_ record = ProducedOutputSemantics.from_output( plan, tmp_path / "A01_s001_w1_z001_t001.tif", FunctionOutputIdentity(component_values={"well": "A01"}, filename_component_values={"well": "A01", "z_index": 1}, extension=".tif", source="test"), - main_flow_plane_axis=None, + main_flow_plane_axis=NoPlaneAxis, ) passed = record.passed_through(replace(plan, step_index=1, step_scope_id="next")) assert passed.component_values == record.component_values assert passed.filename_component_values == record.filename_component_values - assert passed.main_flow_plane_axis is None + assert passed.main_flow_plane_axis.unstacked def test_duplicate_physical_path_rejects_contradictory_whole_axis(tmp_path): @@ -286,17 +287,17 @@ def test_duplicate_physical_path_rejects_contradictory_whole_axis(tmp_path): opaque = ProducedOutputSemantics.from_output( plan, tmp_path / "A01_s001_w1_z001_t001.tif", FunctionOutputIdentity(component_values={"well": "A01"}, extension=".tif", source="test"), - main_flow_plane_axis=None, + main_flow_plane_axis=NoPlaneAxis, ) with pytest.raises(ValueError, match="conflicting main-flow image axes"): - StepOutputManifestStore._unique_output_path_records((opaque, replace(opaque, main_flow_plane_axis=RuntimePlaneAxis.RUNTIME_SLICE))) + StepOutputManifestStore._unique_output_path_records((opaque, replace(opaque, main_flow_plane_axis=RuntimeSlicePlaneAxis))) @pytest.mark.parametrize("cache_hit", [False, True]) @pytest.mark.parametrize("shape", [(4, 5), (4, 5, 3), (3, 4, 5), (1, 4, 5)]) def test_named_raw_image_actual_save_and_reload_retains_opaque_domain(tmp_path, monkeypatch, cache_hit, shape): test_save_and_next_load_preserve_domain_and_independent_cache( - tmp_path, monkeypatch, cache_hit, None, shape, raw_named=True, + tmp_path, monkeypatch, cache_hit, NoPlaneAxis, shape, raw_named=True, ) @@ -305,7 +306,7 @@ def test_record_axis_survives_replace_and_pickle(tmp_path): from openhcs.core.steps.function_output_manifest import ProducedOutputSemantics from openhcs.core.steps.function_output_identity import FunctionOutputIdentity plan = replace(_runtime().execution_plan, output_dir=tmp_path) - for axis in (None, RuntimePlaneAxis.RUNTIME_SLICE, RuntimePlaneAxis.SOURCE_BINDING): + for axis in (NoPlaneAxis, RuntimeSlicePlaneAxis, SourceBindingPlaneAxis): record = ProducedOutputSemantics.from_output( plan, tmp_path / "A01_s001_w1_z001_t001.tif", FunctionOutputIdentity(component_values={"well": "A01"}, extension=".tif", source="test"), @@ -345,7 +346,7 @@ def test_saved_mixed_named_cohort_rejects_joint_load_and_preserves_selected_doma contexts = tuple(AlignedImageSliceContext.main_flow(name, artifact_kind=ImageArtifactType.value) for name in ("Planes", "Volume")) scalar_metadata = ImagePayloadMetadata(source_path=source, source_component_metadata=components) declared_metadata = scalar_metadata.replace_fields( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components(paths=(source,), component_metadata=(components,)), ) planes = declared_metadata.payload_with(np.arange(20, dtype=np.float32).reshape(1, 4, 5)) @@ -371,7 +372,7 @@ def test_saved_mixed_named_cohort_rejects_joint_load_and_preserves_selected_doma manifest = step_output_manifest(context) manifest.record_outputs(plan, records) assert tuple(r.output_path for r in records) == tuple(r.output_path for r in old_records) - assert [r.main_flow_plane_axis for r in records] == [RuntimePlaneAxis.RUNTIME_SLICE, None] + assert [r.main_flow_plane_axis for r in records] == [RuntimeSlicePlaneAxis, NoPlaneAxis] assert context.runtime_image_stack_cache.get(tuple(r.output_path for r in records), memory_type=MEMORY_TYPE_NUMPY) is None def consumer_for(compiled): @@ -390,7 +391,7 @@ def consumer_for(compiled): with pytest.raises(ValueError, match="different declared image axes"): consumer_for(pattern).load_input_stack() - for name, expected, axis in (("Planes", planes.data, RuntimePlaneAxis.RUNTIME_SLICE), ("Volume", opaque_pixels[None], RuntimePlaneAxis.RUNTIME_SLICE)): + for name, expected, axis in (("Planes", planes.data, RuntimeSlicePlaneAxis), ("Volume", opaque_pixels[None], RuntimeSlicePlaneAxis)): selected = consumer_for(_selected_pattern(name)) actual = selected.load_input_stack()[1] np.testing.assert_array_equal(actual.data, expected) @@ -407,7 +408,7 @@ def consumer_for(compiled): def test_multiple_unprojected_named_binding_domains_fail_existing_bundle_grammar(): contexts = tuple(AlignedImageSliceContext.main_flow(name) for name in ("First", "Second")) - payloads = tuple(ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.SOURCE_BINDING).payload_with( + payloads = tuple(ImagePayloadMetadata(plane_axis=SourceBindingPlaneAxis).payload_with( np.ones((1, 4, 5), dtype=np.float32)) for _ in contexts) with pytest.raises(ValueError, match="plane axis to be projected"): bundle = ImageOutputBundle(payloads, contexts) @@ -426,7 +427,7 @@ def test_opaque_named_bundle_uses_existing_same_slice_mask_composition(): bundle = ImageOutputBundle(payloads, contexts) stack_payload = bundle.copy_projected_output_stack(tuple(bundle.projected_output_slices()), memory_type=MEMORY_TYPE_NUMPY, device_id=None) expected = ImagePayloadBundleContext.from_payloads(payloads).compose() - assert stack_payload.metadata.plane_axis is RuntimePlaneAxis.SOURCE_BINDING + assert stack_payload.metadata.plane_axis is SourceBindingPlaneAxis np.testing.assert_array_equal(stack_payload.data, expected.data) np.testing.assert_array_equal(stack_payload.mask, expected.mask) @@ -446,7 +447,7 @@ def test_scalar_rgb_occurrences_reload_on_runtime_slice_axis(tmp_path, sites): component_values={"well": "A01", "site": site, "channel": 3}, extension=".tif", source="test", ), - output_context=context, main_flow_plane_axis=None, + output_context=context, main_flow_plane_axis=NoPlaneAxis, ) for site in sites) payloads = tuple(ImagePayloadMetadata( source_image_names=("RGBImage",), axes=PayloadAxes.colour_samples(-1), @@ -455,7 +456,7 @@ def test_scalar_rgb_occurrences_reload_on_runtime_slice_axis(tmp_path, sites): payloads, producer_records=records, execution_plan=plan, source_projection=None, workspace_source_lookups=(), ) - assert output.metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert output.metadata.plane_axis is RuntimeSlicePlaneAxis assert output.metadata.axis_position(ColourAxis) == 3 np.testing.assert_array_equal(output.data, np.stack([p.data for p in payloads])) @@ -468,7 +469,7 @@ def test_input_bundle_composition_retains_lazy_plan_device_resolution(tmp_path, plan, tmp_path / f"A01_s001_w1_z001_t001_{name}.tif", FunctionOutputIdentity(component_values={"well": "A01"}, extension=".tif", source="test"), output_context=AlignedImageSliceContext.main_flow(name), - main_flow_plane_axis=None, + main_flow_plane_axis=NoPlaneAxis, ) for name in ("First", "Second")) payloads = tuple(ImagePayloadMetadata(source_image_names=(name,)).payload_with( np.ones((4, 5), dtype=np.float32) * i) for i, name in enumerate(("First", "Second"))) @@ -482,5 +483,5 @@ def unexpected_device_resolution(self, memory_type): producer_records=records, execution_plan=plan, source_projection=None, workspace_source_lookups=(), ) - assert output.metadata.plane_axis is RuntimePlaneAxis.SOURCE_BINDING + assert output.metadata.plane_axis is SourceBindingPlaneAxis np.testing.assert_array_equal(output.data, np.stack([p.data for p in payloads])) diff --git a/tests/unit/test_persisted_image_metadata.py b/tests/unit/test_persisted_image_metadata.py index 125cadcbf9..50e3f94086 100644 --- a/tests/unit/test_persisted_image_metadata.py +++ b/tests/unit/test_persisted_image_metadata.py @@ -1,5 +1,6 @@ """Saved-pixel facts and complete, nominal metadata replay.""" +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis import json from concurrent.futures import ThreadPoolExecutor from dataclasses import fields @@ -20,7 +21,6 @@ ImagePayloadMetadata, ImageUnitIntervalIntensityMetadata, ) -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.source_image_provenance import ( SourceImageIdentity, SourceImageProvenance, @@ -80,7 +80,7 @@ def test_complete_metadata_codec_uses_declarations_and_preserves_contributors(): restored = ImagePayloadMetadata.from_mapping(wire) assert restored == metadata assert len(restored.source_provenance.represented_source_identities) == 9 - assert restored.plane_axis is None # Contributors are not a runtime SITE axis. + assert restored.plane_axis.unstacked # Contributors are not a runtime SITE axis. assert restored.source_image_names == ("neurite", "nucleus") @@ -108,7 +108,7 @@ def test_saved_format_metadata_is_native_and_does_not_reencode( assert restored.source_voxel_spacing == metadata.source_voxel_spacing assert restored.source_spatial_domain == metadata.source_spatial_domain assert restored.source_provenance == metadata.source_provenance - assert restored.plane_axis is None + assert restored.plane_axis.unstacked if extension in (".png", ".bmp"): assert restored.unit_interval_intensity is None assert restored.physical_border_edges_yx is None @@ -125,7 +125,7 @@ def test_saved_two_channel_planes_keep_runtime_axis_not_contributor_axis( ): contributors = metadata_fixture().source_image_provenance_planes.planes metadata = metadata_fixture().replace_fields( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes( tuple( RuntimeSourceImageProvenancePlane( @@ -146,7 +146,7 @@ def test_saved_two_channel_planes_keep_runtime_axis_not_contributor_axis( to_jsonable(image_format.persisted_metadata(path, payload)) ) assert image_format.read(path).shape == (2, 4, 5) - assert restored.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert restored.plane_axis is RuntimeSlicePlaneAxis assert restored.axis_position(ColourAxis) is None assert restored.source_provenance.source_plane_count == 2 assert all( diff --git a/tests/unit/test_plane_nlm_declaration.py b/tests/unit/test_plane_nlm_declaration.py index 7af11cad80..d9c9c25690 100644 --- a/tests/unit/test_plane_nlm_declaration.py +++ b/tests/unit/test_plane_nlm_declaration.py @@ -1,6 +1,7 @@ """Independent-plane NLM uses the original contract, kernel and source owners.""" from __future__ import annotations +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis import inspect from dataclasses import replace @@ -21,7 +22,6 @@ ) from openhcs.core.memory import numpy as numpy_contract from openhcs.core.runtime_image_values import ImagePayloadMetadata -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.source_image_provenance import SourceImageProvenancePlanes from openhcs.core.source_metadata import SourceVoxelSpacing from openhcs.core.source_spatial_domain import SourceSpatialDomain @@ -57,7 +57,7 @@ def _source(plane_count): replace(plane, source_image_name="C1") for plane in provenance.planes )) metadata = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_names=("C1",) * plane_count, source_image_provenance_planes=provenance, source_plane_dtypes=("float32",) * plane_count, @@ -105,7 +105,7 @@ def observed_kernel(image, **kernel_kwargs): assert result.data.shape == (plane_count, 16, 20) current = result.metadata prior = source.metadata - assert current.plane_axis is prior.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert current.plane_axis is prior.plane_axis is RuntimeSlicePlaneAxis assert current.source_voxel_spacing == prior.source_voxel_spacing assert current.source_spatial_domain == prior.source_spatial_domain assert current.source_image_provenance_planes.identity == prior.source_image_provenance_planes.identity diff --git a/tests/unit/test_primary_object_diagnostics.py b/tests/unit/test_primary_object_diagnostics.py index c7866c5bfd..33e549ffa9 100644 --- a/tests/unit/test_primary_object_diagnostics.py +++ b/tests/unit/test_primary_object_diagnostics.py @@ -1,5 +1,6 @@ """Lightweight ownership/payload checks; these do not execute segmentation.""" +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis import ast from openhcs.interop.cellprofiler.object_label_variants import ( SmallRemovedLabels, @@ -69,17 +70,17 @@ def _objects(source): def test_selected_response_keeps_source_frame_without_acquisition_intensity_scale(): from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisValueProjection, + RuntimePlaneAxisValueProjection, ) source = _source() stack = source.metadata.replace_fields( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_dtype="uint16", ).payload_with(source.data[None]) pixels = np.linspace(0, 3, 30, dtype=np.float32).reshape(1, 5, 6) projection = RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, axis_size=1 + axis=RuntimeSlicePlaneAxis, axis_size=1 ) output = SelectedDiagnosticPlaneImageOutput(pixels, (0,)) resolved = output.resolve_source_context(stack, projection) @@ -204,7 +205,6 @@ def test_ordinary_image_contextualization_preserves_unexecuted_masks(): def test_existing_pure2d_aggregation_retains_stage_pixels_masks_and_plane_identity(): - from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.processing_contracts import ( Pure2DAuxiliaryOutputAggregator, ) @@ -221,7 +221,7 @@ def test_existing_pure2d_aggregation_retains_stage_pixels_masks_and_plane_identi planes = [DiagnosticPlaneSource.from_image(source).plane(source.data) for source in (first, second)] value = Pure2DAuxiliaryOutputAggregator.aggregate( - planes, "numpy", plane_axis=RuntimePlaneAxis.RUNTIME_SLICE + planes, "numpy", plane_axis=RuntimeSlicePlaneAxis ) import pickle from openhcs.core.aligned_image_payload import ProducedImageStack @@ -236,7 +236,7 @@ def test_existing_pure2d_aggregation_retains_stage_pixels_masks_and_plane_identi assert RuntimeSliceProjection.full_stack_value(value) is composed np.testing.assert_array_equal(composed.data, np.stack([first.data, second.data])) np.testing.assert_array_equal(composed.mask, np.stack([first.mask, second.mask])) - assert value.metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert value.metadata.plane_axis is RuntimeSlicePlaneAxis assert value.metadata.source_provenance.source_plane_count == 2 reloaded = pickle.loads(pickle.dumps(value)) assert np.shares_memory(reloaded.slices[0].data, reloaded.compose().data) @@ -251,7 +251,6 @@ def test_existing_pure2d_aggregation_retains_stage_pixels_masks_and_plane_identi def test_produced_input_snapshot_owns_canonical_pixels_and_masks(shared_mask, spatial_mask): import pickle from openhcs.core.aligned_image_payload import ProducedImageStack - from openhcs.core.runtime_plane_projection import RuntimePlaneAxis pixels = (np.full((3, 4), 7, dtype=np.uint16), np.full((3, 4), 0.25, dtype=np.float32)) @@ -264,8 +263,8 @@ def test_produced_input_snapshot_owns_canonical_pixels_and_masks(shared_mask, sp tuple(MaskedImagePayload(data, mask, metadata) for data, mask in zip(pixels, masks, strict=True)), memory_type="numpy", plane_axis=( - RuntimePlaneAxis.SOURCE_BINDING if spatial_mask - else RuntimePlaneAxis.RUNTIME_SLICE + SourceBindingPlaneAxis if spatial_mask + else RuntimeSlicePlaneAxis ), ) expected = np.stack(pixels) diff --git a/tests/unit/test_projected_image_output.py b/tests/unit/test_projected_image_output.py index 54255dcd96..b45a6180e7 100644 --- a/tests/unit/test_projected_image_output.py +++ b/tests/unit/test_projected_image_output.py @@ -1,5 +1,6 @@ """Reject ambiguous or invalid image-source selections at their owner.""" +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis import numpy as np import pytest @@ -10,7 +11,7 @@ ) -@pytest.mark.parametrize("axis", tuple(RuntimePlaneAxis)) +@pytest.mark.parametrize("axis", RuntimePlaneAxis.leading_axes()) def test_complete_projection_admission_is_owned_by_each_runtime_axis(axis): projection = RuntimePlaneAxisValueProjection.preserve(axis=axis, axis_size=3) assert RuntimePlaneAxisValueProjection.require_complete_projection( @@ -41,8 +42,8 @@ def test_selected_output_requires_complete_projection(selected_plane): projection = ( None if selected_plane is None - else RuntimePlaneAxisValueProjection( - RuntimePlaneAxis.RUNTIME_SLICE, (), selected_plane, 2 + else RuntimePlaneAxisValueProjection.from_selected_plane( + axis=RuntimeSlicePlaneAxis, plane_index=selected_plane, axis_size=2 ) ) with pytest.raises(ValueError, match="complete input stack projection"): @@ -51,8 +52,8 @@ def test_selected_output_requires_complete_projection(selected_plane): def test_selected_output_rejects_out_of_range_source_plane(): output = SelectedPlaneImageOutput(np.zeros((1, 3, 4)), (2,)) - projection = RuntimePlaneAxisValueProjection( - RuntimePlaneAxis.RUNTIME_SLICE, (), None, 2 + projection = RuntimePlaneAxisValueProjection.preserve( + axis=RuntimeSlicePlaneAxis, axis_size=2 ) with pytest.raises(ValueError, match="outside the input stack"): output.resolve_source_context(np.zeros((2, 3, 4)), projection) diff --git a/tests/unit/test_rescale_stream_plane_declaration.py b/tests/unit/test_rescale_stream_plane_declaration.py index 4c9a9cf4c0..80a9ee0559 100644 --- a/tests/unit/test_rescale_stream_plane_declaration.py +++ b/tests/unit/test_rescale_stream_plane_declaration.py @@ -1,5 +1,8 @@ """Synthetic public RescaleIntensity declaration -> producer -> native binding.""" +from openhcs.core.runtime_plane_projection import NoPlaneAxis +from openhcs.core.runtime_object_label_domains import PayloadLabelScope, PlaneLabelScope +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from dataclasses import replace import numpy as np @@ -14,8 +17,7 @@ from openhcs.core.component_group_scope import RuntimeExecutionAxisScope from openhcs.core.function_patterns import InvocationArtifactInputEdgePlan from openhcs.core.runtime_object_label_building import SourceImageObjectLabelBuildRequest -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis, RuntimePlaneAxisValueProjection -from openhcs.core.runtime_object_label_domains import ObjectLabelDomainScope +from openhcs.core.runtime_plane_projection import RuntimePlaneAxisValueProjection from openhcs.core.source_image_provenance import SourceImageProvenancePlanes from openhcs.core.config import StepSourceBindingsConfig from openhcs.core.source_bindings import NamedSourceBinding @@ -157,13 +159,13 @@ def test_declared_source_plane_has_area_not_volume(retained_site): paths=("/synthetic/site.tif",), component_metadata=({"site": "7"},), ), source_spatial_domain=metadata.source_spatial_domain, - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) payload = metadata.payload_with(pixels, None) objects = SourceImageObjectLabelBuildRequest( image=payload, labels=labels, - domain_scope=ObjectLabelDomainScope.PLANE if retained_site else None, + domain_scope=PlaneLabelScope if retained_site else None, plane_projection=( RuntimePlaneAxisValueProjection.from_source_declaration( metadata.plane_axis, metadata.source_provenance, @@ -172,10 +174,10 @@ def test_declared_source_plane_has_area_not_volume(retained_site): ), ).payload() assert objects.plane_axis is ( - RuntimePlaneAxis.RUNTIME_SLICE if retained_site else None + RuntimeSlicePlaneAxis if retained_site else NoPlaneAxis ) assert objects.object_label_domain().scope is ( - ObjectLabelDomainScope.PLANE if retained_site else ObjectLabelDomainScope.PAYLOAD + PlaneLabelScope if retained_site else PayloadLabelScope ) _, rows = measure_object_size_shape.__wrapped__( payload, objects, calculate_advanced=False, calculate_zernikes=False, diff --git a/tests/unit/test_roi_source_metadata.py b/tests/unit/test_roi_source_metadata.py index 53e6c106fd..bcb508c922 100644 --- a/tests/unit/test_roi_source_metadata.py +++ b/tests/unit/test_roi_source_metadata.py @@ -1,3 +1,4 @@ +from openhcs.core.runtime_plane_projection import SourceBindingPlaneAxis from dataclasses import replace from pathlib import Path @@ -9,7 +10,6 @@ from openhcs.core.roi_source_metadata import ROIArchiveSourceMetadata from openhcs.core.runtime_image_values import ImagePayloadMetadata -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.source_image_provenance import SourceImageProvenancePlanes from openhcs.core.runtime_object_labels import ( ObjectLabelPayload, @@ -68,7 +68,7 @@ def test_source_metadata_is_optional_only_for_external_shapes(): def test_native_sidecar_preserves_retained_source_planes(tmp_path): metadata = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("/source/z1.tif", "/source/z2.tif"), component_metadata=( @@ -89,7 +89,7 @@ def test_native_sidecar_preserves_retained_source_planes(tmp_path): decoded.retained_plane_component_values() == metadata.retained_plane_component_values() ) - assert decoded.plane_axis is RuntimePlaneAxis.SOURCE_BINDING + assert decoded.plane_axis is SourceBindingPlaneAxis assert decoded.source_voxel_spacing == metadata.source_voxel_spacing assert ROIArchiveSourceMetadata.geometry(restored) == rois diff --git a/tests/unit/test_runtime_artifact_queries.py b/tests/unit/test_runtime_artifact_queries.py index 8c5122be3e..4f73e45b00 100644 --- a/tests/unit/test_runtime_artifact_queries.py +++ b/tests/unit/test_runtime_artifact_queries.py @@ -1,4 +1,6 @@ from __future__ import annotations +from openhcs.core.runtime_object_label_domains import PayloadLabelScope, PlaneLabelScope +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from dataclasses import dataclass import weakref @@ -46,10 +48,8 @@ ) from openhcs.core.runtime_object_label_domains import ( ObjectLabelDomain, - ObjectLabelDomainScope, ) from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneProjection, ) from openhcs.core.runtime_stores import RuntimeValueStore @@ -192,10 +192,10 @@ def object_labels( name="Labels", variant_data=ObjectLabelVariantData(labels=labels), domain=ObjectLabelDomain.declared( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=declared_object_id_domains, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) @@ -771,7 +771,7 @@ def test_heterogeneous_shape_rows_prefer_area_over_later_volume_alias() -> None: labels=pytest.importorskip("numpy").array([[1, 2]], dtype="int32") ), domain=ObjectLabelDomain.declared( - scope=ObjectLabelDomainScope.PAYLOAD, + scope=PayloadLabelScope, declared_object_ids=(1, 2), ), ) @@ -834,7 +834,7 @@ def test_projected_payload_measurement_lookup_uses_selected_runtime_slice() -> N name="Cells", variant_data=ObjectLabelVariantData(labels=np.array(((1, 2),), dtype="int32")), domain=ObjectLabelDomain.declared( - scope=ObjectLabelDomainScope.PAYLOAD, + scope=PayloadLabelScope, declared_object_ids=(1, 2), ), ) @@ -876,7 +876,7 @@ def test_payload_measurement_projection_ignores_source_contributor_alias_count() name="Cells", variant_data=ObjectLabelVariantData(labels=np.array(((1, 2),), dtype="int32")), domain=ObjectLabelDomain.declared( - scope=ObjectLabelDomainScope.PAYLOAD, + scope=PayloadLabelScope, declared_object_ids=(1, 2), ), source_image_names=("Image", "Mask"), @@ -915,7 +915,7 @@ def test_payload_measurement_lookup_rejects_unselected_runtime_stack() -> None: name="Cells", variant_data=ObjectLabelVariantData(labels=np.array(((1,),), dtype="int32")), domain=ObjectLabelDomain.declared( - scope=ObjectLabelDomainScope.PAYLOAD, + scope=PayloadLabelScope, declared_object_ids=(1,), ), ) @@ -937,7 +937,7 @@ def test_payload_measurement_lookup_accepts_declared_singleton_runtime_stack() - name="Cells", variant_data=ObjectLabelVariantData(labels=np.array(((1,),), dtype="int32")), domain=ObjectLabelDomain.declared( - scope=ObjectLabelDomainScope.PAYLOAD, + scope=PayloadLabelScope, declared_object_ids=(1,), ), ) diff --git a/tests/unit/test_runtime_context_declaration.py b/tests/unit/test_runtime_context_declaration.py index cd1de0c5fa..816e272803 100644 --- a/tests/unit/test_runtime_context_declaration.py +++ b/tests/unit/test_runtime_context_declaration.py @@ -150,7 +150,7 @@ def test_runtime_binding_consumes_captured_selection(func, parameter): artifact_inputs=artifacts[0], artifact_outputs=artifacts[1], group_key=None, - plane_projection=RuntimePlaneProjection(), + plane_projection=RuntimePlaneProjection.stack(), main_data_arg=np.zeros((2, 3)), source_memory_type="numpy", ) diff --git a/tests/unit/test_runtime_equivalence.py b/tests/unit/test_runtime_equivalence.py index 014beef145..94e17a2d55 100644 --- a/tests/unit/test_runtime_equivalence.py +++ b/tests/unit/test_runtime_equivalence.py @@ -1,4 +1,6 @@ from __future__ import annotations +from openhcs.core.runtime_object_label_domains import PayloadLabelScope, PlaneLabelScope +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from openhcs.interop.cellprofiler.measurement_dialect import CellProfilerEquivalencePolicy @@ -106,10 +108,6 @@ ) from openhcs.core.runtime_object_label_domains import ( ObjectLabelDomain, - ObjectLabelDomainScope, -) -from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, ) from openhcs.core.source_image_provenance import ( SourceImageProvenance, @@ -821,9 +819,9 @@ def test_runtime_reference_artifact_equivalence_projects_object_numbers_per_plan variant_data=ObjectLabelVariantData(labels=labels), domain=ObjectLabelDomain( declared_object_id_domains=((1, 2), (1, 2)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) payload = label_set value = RuntimeValue( @@ -874,7 +872,7 @@ def test_runtime_reference_artifact_equivalence_projects_payload_object_numbers_ variant_data=ObjectLabelVariantData(labels=labels), domain=ObjectLabelDomain( declared_object_ids=(1, 2), - scope=ObjectLabelDomainScope.PAYLOAD, + scope=PayloadLabelScope, ), ) payload = label_set @@ -4851,9 +4849,9 @@ def test_runtime_reference_artifact_equivalence_derives_relationship_child_locat ), domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) store.record( RuntimeValue( @@ -7701,9 +7699,9 @@ def test_runtime_measurement_snapshot_uses_declared_plane_object_domains( ), domain=ObjectLabelDomain( declared_object_id_domains=((1, 2), (1, 2, 3, 4)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ), ), ), @@ -7766,11 +7764,11 @@ def test_runtime_measurement_snapshot_preserves_multi_source_declared_object_dom "SSC", ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_names=("BF_image", "MorphBf"), domain=ObjectLabelDomain( declared_object_id_domains=((1, 2), (1, 2, 3, 4)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ), ), @@ -7923,10 +7921,10 @@ def test_runtime_measurement_snapshot_preserves_repeated_declared_plane_domains( "SSC", ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1, 2), (1, 2)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ), ), @@ -9398,7 +9396,7 @@ def test_runtime_reference_artifact_equivalence_uses_payload_3d_label_locations_ variant_data=ObjectLabelVariantData(labels=labels), domain=ObjectLabelDomain( declared_object_ids=(1, 2), - scope=ObjectLabelDomainScope.PAYLOAD, + scope=PayloadLabelScope, ), ), ), @@ -10690,7 +10688,7 @@ def test_runtime_reference_artifact_equivalence_prefers_explicit_object_location ), domain=ObjectLabelDomain( declared_object_count=2, - scope=ObjectLabelDomainScope.PAYLOAD, + scope=PayloadLabelScope, ), ), ), diff --git a/tests/unit/test_runtime_image_singleton_alignment.py b/tests/unit/test_runtime_image_singleton_alignment.py index 0bdb70631c..838f26b8d6 100644 --- a/tests/unit/test_runtime_image_singleton_alignment.py +++ b/tests/unit/test_runtime_image_singleton_alignment.py @@ -1,4 +1,5 @@ from __future__ import annotations +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis import numpy as np import pytest @@ -8,7 +9,6 @@ ImagePayloadMetadata, MaskedImagePayload, ) -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.runtime_slice_projection import RuntimeSliceProjection from openhcs.core.source_spatial_domain import SourceSpatialDomain from openhcs.core.axes import ColourAxis @@ -43,7 +43,7 @@ def _singleton_owner(owner_kind: str): if owner_kind == "runtime_payload": return ImagePayloadMetadata( source_spatial_domain=_SPATIAL_DOMAIN, - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with( np.full((1, 4, 5), 11, dtype=np.float32), _SINGLETON_MASK[np.newaxis, ...], @@ -69,7 +69,7 @@ def test_scalar_image_aligns_with_nominal_singleton_owner( assert isinstance(composition.payload, AlignedImageStack) assert len(composition.payload.slices) == 1 bundle = composition.payload.slices[0] - assert bundle.metadata.plane_axis is RuntimePlaneAxis.SOURCE_BINDING + assert bundle.metadata.plane_axis is SourceBindingPlaneAxis assert bundle.data.shape == (2, 4, 5) expected_values = (11, 7) if singleton_first else (7, 11) for index, expected_value in enumerate(expected_values): @@ -89,7 +89,7 @@ def test_scalar_image_still_requires_owner_for_multi_slice_alignment( else: aligned = ImagePayloadMetadata( source_spatial_domain=_SPATIAL_DOMAIN, - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with( np.stack( ( @@ -116,7 +116,7 @@ def test_same_shaped_scalar_images_do_not_invent_runtime_alignment() -> None: assert composition.execution_mode is FullStackExecution assert not isinstance(composition.payload, AlignedImageStack) assert composition.payload.metadata.plane_axis is ( - RuntimePlaneAxis.SOURCE_BINDING + SourceBindingPlaneAxis ) @@ -126,7 +126,7 @@ def test_singleton_runtime_projection_consumes_rgb_image_and_mask_axis_together( payload = ImagePayloadMetadata( source_spatial_domain=_SPATIAL_DOMAIN, axes=PayloadAxes.colour_samples(3), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with( np.zeros((1, 4, 5, 3), dtype=np.float32), _SINGLETON_MASK[np.newaxis, ...], @@ -141,14 +141,14 @@ def test_singleton_runtime_projection_consumes_rgb_image_and_mask_axis_together( assert projected.data.shape == (4, 5, 3) np.testing.assert_array_equal(projected.mask, _SINGLETON_MASK) projected_metadata = projected.metadata - assert projected_metadata.plane_axis is None + assert projected_metadata.plane_axis.unstacked assert projected_metadata.axis_index(ColourAxis, projected) == 2 def test_singleton_runtime_projection_rejects_multi_slice_payload() -> None: payload = ImagePayloadMetadata( source_spatial_domain=_SPATIAL_DOMAIN, - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with( np.zeros((2, 4, 5), dtype=np.float32), None, diff --git a/tests/unit/test_runtime_object_label_domain_transitions.py b/tests/unit/test_runtime_object_label_domain_transitions.py index 901a16f78e..ad5c111801 100644 --- a/tests/unit/test_runtime_object_label_domain_transitions.py +++ b/tests/unit/test_runtime_object_label_domain_transitions.py @@ -1,9 +1,10 @@ +from openhcs.core.runtime_object_label_domains import PayloadLabelScope, PlaneLabelScope +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis import numpy as np import pytest from openhcs.core.runtime_object_label_domains import ( ObjectLabelDomain, - ObjectLabelDomainScope, PresentObjectLabelIdsDomainDeclaration, ) from openhcs.core.runtime_object_labels import ( @@ -13,9 +14,7 @@ ) from openhcs.core.runtime_image_values import ( ImagePayloadMetadata, - ImagePayloadMetadataCompositionMode, ) -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.source_image_provenance import ( RuntimeSourceImageProvenancePlane, SourceImageIdentity, @@ -36,10 +35,10 @@ def test_payload_domain_replacement_drops_inherited_plane_axis() -> None: name="erodedDownsizedNuclei", variant_data=ObjectLabelVariantData(labels=labels), domain=ObjectLabelDomain.declared( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,), (1, 2)), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) transformed = object_label_value_with_dense_labels( @@ -49,10 +48,10 @@ def test_payload_domain_replacement_drops_inherited_plane_axis() -> None: ) assert transformed.domain == ObjectLabelDomain.declared( - scope=ObjectLabelDomainScope.PAYLOAD, + scope=PayloadLabelScope, declared_object_ids=(1, 2), ) - assert transformed.plane_axis is None + assert transformed.plane_axis.unstacked np.testing.assert_array_equal(transformed.labels, labels) @@ -67,10 +66,10 @@ def test_explicit_payload_domain_plane_axis_remains_invalid() -> None: labels=np.zeros((2, 2, 2), dtype=np.int32) ), domain=ObjectLabelDomain.declared( - scope=ObjectLabelDomainScope.PAYLOAD, + scope=PayloadLabelScope, declared_object_count=0, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) @@ -92,7 +91,7 @@ def test_payload_measurement_reference_keeps_volume_planes_as_contributors() -> source_image_names=("MembFinal",) * 3, ), domain=ObjectLabelDomain.declared( - scope=ObjectLabelDomainScope.PAYLOAD, + scope=PayloadLabelScope, declared_object_count=0, ), ) @@ -100,7 +99,7 @@ def test_payload_measurement_reference_keeps_volume_planes_as_contributors() -> reference_image = labels.measurement_reference_image() reference_metadata = reference_image.metadata - assert reference_metadata.plane_axis is None + assert reference_metadata.plane_axis.unstacked assert reference_metadata.source_provenance.source_plane_count == 0 assert reference_metadata.source_image_provenance_planes.contributor_count == 3 assert reference_metadata.source_image_names == () @@ -109,12 +108,12 @@ def test_payload_measurement_reference_keeps_volume_planes_as_contributors() -> ) composed = ImagePayloadMetadata.compose( (reference_image,), - mode=ImagePayloadMetadataCompositionMode.BUNDLE, + mode=SourceBindingPlaneAxis, ) assert composed.source_provenance.represented_source_image_names == ("MembFinal",) -@pytest.mark.parametrize("axis", [RuntimePlaneAxis.RUNTIME_SLICE, RuntimePlaneAxis.SOURCE_BINDING]) +@pytest.mark.parametrize("axis", [RuntimeSlicePlaneAxis, SourceBindingPlaneAxis]) @pytest.mark.parametrize("plane_count", [1, 2]) def test_sparse_label_slice_projection_preserves_overlap_and_axis_domain( monkeypatch, axis, plane_count @@ -136,7 +135,7 @@ def test_sparse_label_slice_projection_preserves_overlap_and_axis_domain( representation=ObjectLabelRepresentation.SPARSE_IJV, domain=ObjectLabelDomain( declared_object_id_domains=((1, 2),) if plane_count == 1 else ((1, 2), (3,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), plane_axis=axis, ) @@ -147,7 +146,7 @@ def reject_dense(*args, **kwargs): axis=axis, axis_size=plane_count, ) projected = RuntimeSliceProjection.value_for_slice(labels, preserved.selected_plane(0)) - assert projected.plane_axis is None + assert projected.plane_axis.unstacked assert projected.domain.declared_object_ids == (1, 2) np.testing.assert_array_equal(projected.labels.as_array(), first.as_array()) assert labels.plane_axis is axis diff --git a/tests/unit/test_runtime_output_matching.py b/tests/unit/test_runtime_output_matching.py index a9cf8a7bad..9923f1a5be 100644 --- a/tests/unit/test_runtime_output_matching.py +++ b/tests/unit/test_runtime_output_matching.py @@ -1,4 +1,5 @@ from __future__ import annotations +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis import numpy as np import pytest @@ -20,7 +21,6 @@ ) from openhcs.core.runtime_image_values import ImagePayloadMetadata from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisValueProjection, ) from openhcs.core.runtime_slice_projection import RuntimeSliceProjectionDeclarationError @@ -68,13 +68,13 @@ def test_matcher_contextualizes_declared_axis_before_resolving_complete_abi() -> measurements = ArtifactSpec.output("Measurements", MeasurementsArtifactType) data = np.arange(24, dtype=np.float32).reshape((2, 3, 4)) payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE + plane_axis=RuntimeSlicePlaneAxis ).payload_with(data, None) trailing = object() contract = _contract(first, second, measurements) matcher_returned_output = (payload, trailing) projection = RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ) @@ -101,7 +101,7 @@ def test_matcher_contextualizes_declared_axis_before_resolving_complete_abi() -> contract.contextualize_returned_canonical_output( matcher_returned_output, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=3, ), ) @@ -109,7 +109,7 @@ def test_matcher_contextualizes_declared_axis_before_resolving_complete_abi() -> contract.contextualize_returned_canonical_output( matcher_returned_output, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.SOURCE_BINDING, + axis=SourceBindingPlaneAxis, axis_size=2, ), ) diff --git a/tests/unit/test_runtime_semantics.py b/tests/unit/test_runtime_semantics.py index 28dcf0b5ba..1753f39878 100644 --- a/tests/unit/test_runtime_semantics.py +++ b/tests/unit/test_runtime_semantics.py @@ -1,4 +1,6 @@ from __future__ import annotations +from openhcs.core.runtime_object_label_domains import PlaneLabelScope +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis import numpy as np import pytest @@ -12,7 +14,6 @@ from openhcs.core.source_spatial_domain import SourceSpatialDomain from openhcs.core.runtime_object_label_domains import ( ObjectLabelDomain, - ObjectLabelDomainScope, DenseObjectLabelConsecutiveRelabelingStrategy, DenseObjectLabelExtentDomainDeclaration, ObjectLabelIdDomainStrategy, @@ -32,7 +33,6 @@ object_label_parent_child_payload, ) from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisValueProjection, RuntimePlaneProjection, ) @@ -78,7 +78,7 @@ def test_runtime_plane_projection_is_nominal_and_validated() -> None: def test_runtime_plane_projection_preserves_payload_declared_axis() -> None: payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.ones((2, 4, 5), dtype=np.float32), None) projection = preserve_declared_image_payload_axis( @@ -87,14 +87,14 @@ def test_runtime_plane_projection_preserves_payload_declared_axis() -> None: ) assert projection == RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ) def test_runtime_plane_projection_preserves_payload_owned_axis_cardinality() -> None: payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.ones((1, 4, 5), dtype=np.float32), None) projection = preserve_declared_image_payload_axis( @@ -103,17 +103,17 @@ def test_runtime_plane_projection_preserves_payload_owned_axis_cardinality() -> ) assert projection == RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=1, ) def test_runtime_plane_projection_prefers_declared_output_axis_cardinality() -> None: source = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.ones((2, 4, 5), dtype=np.float32), None) output = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.ones((1, 4, 5), dtype=np.float32), None) projection = preserve_declared_image_payload_axis( @@ -123,7 +123,7 @@ def test_runtime_plane_projection_prefers_declared_output_axis_cardinality() -> ) assert projection == RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=1, ) @@ -142,14 +142,14 @@ def test_runtime_plane_projection_preserves_nominal_aligned_output_axis() -> Non ) assert projection == RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ) def test_runtime_plane_projection_does_not_preserve_selected_payload_axis() -> None: payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.ones((2, 4, 5), dtype=np.float32), None) projection = preserve_declared_image_payload_axis( @@ -162,7 +162,7 @@ def test_runtime_plane_projection_does_not_preserve_selected_payload_axis() -> N def test_runtime_plane_projection_preserves_source_binding_axis_from_payload() -> None: payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_image_names=("DNA", "RNA"), ).payload_with(np.ones((2, 4, 5), dtype=np.float32), None) @@ -172,17 +172,17 @@ def test_runtime_plane_projection_preserves_source_binding_axis_from_payload() - ) assert projection == RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.SOURCE_BINDING, + axis=SourceBindingPlaneAxis, axis_size=2, ) def test_runtime_plane_projection_preserves_output_axis_over_source_axis() -> None: source = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.ones((2, 4, 5), dtype=np.float32), None) output = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, ).payload_with(np.ones((2, 4, 5), dtype=np.float32), None) projection = preserve_declared_image_payload_axis( @@ -192,7 +192,7 @@ def test_runtime_plane_projection_preserves_output_axis_over_source_axis() -> No ) assert projection == RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.SOURCE_BINDING, + axis=SourceBindingPlaneAxis, axis_size=2, ) @@ -234,20 +234,20 @@ def test_aligned_dense_object_label_arrays_rejects_implicit_stack_projection() - def test_object_label_domain_project_slice_selects_declared_plane_domain() -> None: domain = ObjectLabelDomain( declared_object_id_domains=((1, 2), (1, 2, 3, 4)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ) projected = domain.project_slice(slice_index=1, slice_count=2) assert projected.declared_object_ids == (1, 2, 3, 4) assert projected.declared_object_id_domains == () - assert projected.scope is ObjectLabelDomainScope.PAYLOAD + assert projected.scope.ids_span_payload def test_object_label_domain_project_slice_rejects_single_domain_broadcast() -> None: domain = ObjectLabelDomain( declared_object_id_domains=((1, 2),), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ) with pytest.raises(ValueError, match="must match PURE_2D slice count"): @@ -257,7 +257,7 @@ def test_object_label_domain_project_slice_rejects_single_domain_broadcast() -> def test_object_label_domain_project_slice_rejects_grouped_plane_domains() -> None: domain = ObjectLabelDomain( declared_object_id_domains=((1,), (2,), (3,), (4,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ) with pytest.raises(ValueError, match="must match PURE_2D slice count"): @@ -267,7 +267,7 @@ def test_object_label_domain_project_slice_rejects_grouped_plane_domains() -> No def test_object_label_domain_project_slice_rejects_repeated_plane_domains() -> None: domain = ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ) with pytest.raises(ValueError, match="must match PURE_2D slice count"): @@ -277,14 +277,14 @@ def test_object_label_domain_project_slice_rejects_repeated_plane_domains() -> N def test_object_label_domain_project_planes_selects_noncontiguous_domains() -> None: domain = ObjectLabelDomain( declared_object_id_domains=((1,), (2,), (3,), (4,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ) projected = domain.project_planes((1, 3)) assert projected.declared_object_ids == () assert projected.declared_object_id_domains == ((2,), (4,)) - assert projected.scope is ObjectLabelDomainScope.PLANE + assert projected.scope.ids_per_plane def test_plane_domain_strategy_rejects_mismatched_single_output_plane() -> None: @@ -293,9 +293,9 @@ def test_plane_domain_strategy_rejects_mismatched_single_output_plane() -> None: with pytest.raises(ValueError, match=r"declares 3 plane\(s\)"): ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((1,), (2,), (3,)), ), ) @@ -304,7 +304,7 @@ def test_plane_domain_strategy_rejects_mismatched_single_output_plane() -> None: def test_object_label_domain_project_slice_rejects_mismatched_plane_domains() -> None: domain = ObjectLabelDomain( declared_object_id_domains=((1, 2), (1, 2, 3, 4)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ) with pytest.raises(ValueError, match="must match PURE_2D slice count"): @@ -352,7 +352,7 @@ def test_aligned_image_stack_kwarg_resolver_selects_nominal_stack_slice() -> Non resolved = AlignedImageStackKwargResolver( projection_axis=RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=1, axis_size=2 + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=2 ), reference_payload=second, ).resolve(stack) @@ -364,7 +364,7 @@ def test_aligned_image_stack_kwarg_resolver_requires_exact_cardinality() -> None stack = AlignedImageStack((np.zeros((3, 4), dtype=np.int32),)) resolver = AlignedImageStackKwargResolver( projection_axis=RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=0, axis_size=2 + axis=RuntimeSlicePlaneAxis, plane_index=0, axis_size=2 ), ) @@ -389,7 +389,7 @@ def test_aligned_kwarg_resolution_preserves_image_payload_metadata() -> None: resolved = AlignedImageStackKwargResolver( projection_axis=RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=0, axis_size=1 + axis=RuntimeSlicePlaneAxis, plane_index=0, axis_size=1 ), reference_payload=reference, ).resolve(source) @@ -418,7 +418,7 @@ def test_aligned_kwarg_resolution_preserves_image_payload_mask() -> None: resolved = AlignedImageStackKwargResolver( projection_axis=RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=0, axis_size=1 + axis=RuntimeSlicePlaneAxis, plane_index=0, axis_size=1 ), reference_payload=reference, ).resolve(source) @@ -500,10 +500,10 @@ def test_object_label_parent_child_payload_preserves_plane_scoped_stack_identity dtype=np.int32, ) ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) child_stack = ObjectLabelPayload( @@ -516,10 +516,10 @@ def test_object_label_parent_child_payload_preserves_plane_scoped_stack_identity dtype=np.int32, ) ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (1,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) @@ -844,10 +844,10 @@ def test_dense_object_label_plane_id_domains_use_explicit_plane_declarations() - dtype=np.int32, ) ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1, 3), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) @@ -870,10 +870,10 @@ def test_dense_object_label_plane_id_domains_preserve_explicit_single_plane() -> dtype=np.int32, ) ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1, 3),), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) diff --git a/tests/unit/test_runtime_slice_projection.py b/tests/unit/test_runtime_slice_projection.py index 880f0c79cf..3a60f6bd4a 100644 --- a/tests/unit/test_runtime_slice_projection.py +++ b/tests/unit/test_runtime_slice_projection.py @@ -1,3 +1,5 @@ +from openhcs.core.runtime_object_label_domains import PlaneLabelScope +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis from dataclasses import dataclass import inspect from typing import Any, get_args, get_origin @@ -21,7 +23,6 @@ ) from openhcs.core.runtime_object_label_domains import ( ObjectLabelDomain, - ObjectLabelDomainScope, ) from openhcs.core.runtime_object_labels import ( ObjectLabelPayload, @@ -30,7 +31,6 @@ object_label_dense_array, ) from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisValueProjection, ) from openhcs.core.runtime_slice_alignment import RuntimeSliceAlignedValues @@ -109,7 +109,7 @@ def test_cellprofiler_runtime_parameter_types_declare_slice_projection() -> None def _declared_plane_metadata(count: int) -> ImagePayloadMetadata: return ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=tuple(f"/tmp/source_{index}.tif" for index in range(count)), component_metadata=tuple( @@ -127,7 +127,7 @@ class UndeclaredValue: RuntimeSliceProjection.value_for_slice( UndeclaredValue(), RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=0, axis_size=1, + axis=RuntimeSlicePlaneAxis, plane_index=0, axis_size=1, ), ) @@ -148,7 +148,7 @@ class ExternalArray: RuntimeSliceProjection.value_for_slice( value, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=2, ), @@ -170,7 +170,7 @@ def test_spatial_graph_declares_scalar_pass_through_projection() -> None: RuntimeSliceProjection.value_for_slice( graph, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=2, ), @@ -181,7 +181,7 @@ def test_spatial_graph_declares_scalar_pass_through_projection() -> None: RuntimeSliceProjection.identity_projected_value( graph, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=2, ), @@ -200,7 +200,7 @@ def test_object_label_payload_selects_nominal_object_label_projection() -> None: projected = RuntimeSliceProjection.value_for_slice( payload, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=0, axis_size=1, + axis=RuntimeSlicePlaneAxis, plane_index=0, axis_size=1, ), ) assert projected is payload @@ -211,7 +211,7 @@ def test_runtime_slice_projection_preserves_nominal_dtype_config() -> None: projected = RuntimeSliceProjection.kwargs_for_slice( {"dtype_config": PRESERVE_INPUT_DTYPE_CONFIG}, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=1, axis_size=2 + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=2 ), ) @@ -229,7 +229,7 @@ class ProjectionRow: projected = RuntimeSliceProjection.value_for_slice( rows, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=2, ), @@ -242,11 +242,11 @@ class ProjectionRow: def test_runtime_slice_projection_selects_runtime_plane_projection_parameter() -> None: preserved = RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ) selected = RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=2, ) @@ -267,7 +267,7 @@ def test_runtime_slice_projection_does_not_infer_high_rank_array_axis() -> None: RuntimeSliceProjection.value_for_slice( value, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=2, axis_size=3 + axis=RuntimeSlicePlaneAxis, plane_index=2, axis_size=3 ), ) is value @@ -279,24 +279,24 @@ def test_runtime_slice_projection_projects_declared_color_image_stack() -> None: tuple(np.full((4, 5, 3), index, dtype=np.float32) for index in range(2)) ) payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, axes=PayloadAxes.colour_samples(-1), ).payload_with(data, None) projected = RuntimeSliceProjection.value_for_slice( payload, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=2, ), ) np.testing.assert_array_equal(projected.data, data[1]) - assert projected.metadata.plane_axis is None + assert projected.metadata.plane_axis.unstacked assert RuntimeSliceProjection.preserved_context_for_value(payload) == ( RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ) ) @@ -326,14 +326,14 @@ def __array__(self, dtype: object = None) -> np.ndarray: data = DeviceArray(np.zeros((2, 4, 5), dtype=np.float32)) payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE + plane_axis=RuntimeSlicePlaneAxis ).payload_with(data, None) assert RuntimeSliceProjection.slice_count_from_values((payload,)) == 2 projected = RuntimeSliceProjection.value_for_slice( payload, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=2, ), @@ -345,7 +345,7 @@ def __array__(self, dtype: object = None) -> np.ndarray: def test_aligned_image_stack_projects_inner_source_axis_across_runtime_slices() -> None: runtime_slices = tuple( ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_image_provenance_planes=( SourceImageProvenancePlanes.from_components( component_metadata=( @@ -370,7 +370,7 @@ def test_aligned_image_stack_projects_inner_source_axis_across_runtime_slices() selected_source = RuntimeSliceProjection.value_for_slice( aligned, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.SOURCE_BINDING, + axis=SourceBindingPlaneAxis, plane_index=0, axis_size=2, ), @@ -413,7 +413,7 @@ def test_image_output_bundle_projects_shared_runtime_axis_not_output_count() -> projected = RuntimeSliceProjection.value_for_slice( bundle, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=3, ), @@ -426,7 +426,7 @@ def test_image_output_bundle_projects_shared_runtime_axis_not_output_count() -> output.data, np.full((2, 3), output_index * 10 + 1, dtype=np.float32), ) - assert output.metadata.plane_axis is None + assert output.metadata.plane_axis.unstacked def test_image_output_bundle_uses_outer_axis_without_inner_declaration() -> None: @@ -449,7 +449,7 @@ def test_image_output_bundle_uses_outer_axis_without_inner_declaration() -> None execution_mode=AlignedStackExecution, ) - assert composition.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert composition.plane_axis is RuntimeSlicePlaneAxis assert RuntimeSliceProjection.slice_count_from_values((bundle,)) == 2 assert RuntimeSliceProjection.preserved_context_for_value(bundle) == ( RuntimePlaneAxisValueProjection.preserve( @@ -461,7 +461,7 @@ def test_image_output_bundle_uses_outer_axis_without_inner_declaration() -> None RuntimeSliceProjection.value_for_slice( bundle, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=2, ), @@ -476,7 +476,7 @@ def test_selected_image_projection_stays_consumed_on_derived_output() -> None: ) payload = _declared_plane_metadata(2).payload_with(data, None) projection = RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=2, ) @@ -488,21 +488,21 @@ def test_selected_image_projection_stays_consumed_on_derived_output() -> None: plane_projection=projection, ) - assert projected.metadata.plane_axis is None - assert derived.metadata.plane_axis is None + assert projected.metadata.plane_axis.unstacked + assert derived.metadata.plane_axis.unstacked assert derived.metadata.source_path == "/tmp/source_1.tif" def test_runtime_slice_projection_validates_declared_image_cardinality() -> None: payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.zeros((3, 4, 5), dtype=np.float32), None) with pytest.raises(ValueError, match="axis of size 2"): RuntimeSliceProjection.value_for_slice( payload, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=2, ), @@ -524,7 +524,7 @@ def test_runtime_plane_projection_owns_dense_axis_shape_validation( expected: bool, ) -> None: projection = RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=axis_size, ) @@ -566,7 +566,7 @@ def test_runtime_slice_projection_projects_aligned_kwargs_from_one_coordinate() "shape_choice": "natural_shape_and_location", }, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=2, ), @@ -582,7 +582,7 @@ def test_runtime_slice_projection_rejects_aligned_kwarg_cardinality_mismatch() - RuntimeSliceProjection.kwargs_for_slice( {"values": RuntimeSliceAlignedValues((np.asarray([1.0]),))}, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=2, ), @@ -599,9 +599,9 @@ def test_runtime_slice_projection_projects_declared_object_label_plane() -> None labels = ObjectLabelSet( name="Cells", variant_data=ObjectLabelVariantData(labels=label_planes), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, declared_object_id_domains=((3,), (7,)), ), ) @@ -609,15 +609,15 @@ def test_runtime_slice_projection_projects_declared_object_label_plane() -> None projected = RuntimeSliceProjection.value_for_slice( labels, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=2, ), ) assert isinstance(projected, ObjectLabelSet) - assert projected.domain.scope is ObjectLabelDomainScope.PAYLOAD - assert projected.plane_axis is None + assert projected.domain.scope.ids_span_payload + assert projected.plane_axis.unstacked np.testing.assert_array_equal(projected.labels, label_planes[1]) @@ -633,7 +633,7 @@ def test_runtime_slice_projection_preserves_image_mask_and_plane_metadata() -> N projected = RuntimeSliceProjection.value_for_slice( payload, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=2, ), @@ -642,7 +642,7 @@ def test_runtime_slice_projection_preserves_image_mask_and_plane_metadata() -> N np.testing.assert_array_equal(projected.data, data[1]) np.testing.assert_array_equal(projected.mask, mask[1]) assert projected.metadata.source_path == "/tmp/source_1.tif" - assert projected.metadata.plane_axis is None + assert projected.metadata.plane_axis.unstacked def test_variable_component_projection_requires_declared_plane_provenance() -> None: @@ -654,7 +654,7 @@ def test_variable_component_projection_requires_declared_plane_provenance() -> N with pytest.raises( RuntimeSliceProjectionDeclarationError, - match="nominal payload.*RUNTIME_SLICE", + match="nominal payload.*RuntimeSlicePlaneAxis", ): OptionalSourceIdentity.project_payload_items( request @@ -670,7 +670,7 @@ def test_variable_component_projection_validates_declared_cardinality() -> None: source_description="mismatched image stack", variable_components=(Microscopy.Site,), plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), ) @@ -690,7 +690,7 @@ def test_variable_component_projection_rejects_expanded_source_provenance() -> N source_description="expanded-provenance image stack", variable_components=(Microscopy.Site,), plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=3, ), ) @@ -717,7 +717,7 @@ def test_variable_component_projection_uses_declared_plane_axis() -> None: source_description="declared image stack", variable_components=(Microscopy.Site,), plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), ) @@ -744,15 +744,15 @@ def test_source_binding_projection_projects_object_labels_through_same_axis() -> labels = ObjectLabelSet( name="Objects", variant_data=ObjectLabelVariantData(labels=planes), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_image_names=("First", "Second"), domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) projection = RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.SOURCE_BINDING, + axis=SourceBindingPlaneAxis, axis_size=2, source_aliases=("First", "Second"), ).selected_plane(1) @@ -761,7 +761,7 @@ def test_source_binding_projection_projects_object_labels_through_same_axis() -> assert isinstance(projected, ObjectLabelSet) np.testing.assert_array_equal(projected.labels, planes[1]) - assert projected.plane_axis is None + assert projected.plane_axis.unstacked def test_source_identity_request_projects_explicit_source_binding_label_planes() -> ( @@ -777,11 +777,11 @@ def test_source_identity_request_projects_explicit_source_binding_label_planes() labels = ObjectLabelSet( name="Objects", variant_data=ObjectLabelVariantData(labels=planes), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_image_names=("First", "Second"), domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) @@ -840,7 +840,7 @@ def test_axisless_object_measurement_is_not_runtime_slice_projected() -> None: subject=MeasurementSubject(MeasurementScope.OBJECT, name="Objects"), ) projection = RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, plane_index=0, axis_size=1, ) diff --git a/tests/unit/test_runtime_stack_cache_lifetime.py b/tests/unit/test_runtime_stack_cache_lifetime.py index 6a38b4fbb6..07e4f94d2a 100644 --- a/tests/unit/test_runtime_stack_cache_lifetime.py +++ b/tests/unit/test_runtime_stack_cache_lifetime.py @@ -1,5 +1,6 @@ """Execution-boundary release of image reuse storage.""" +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis import gc import weakref @@ -10,7 +11,6 @@ from openhcs.core.artifacts.kinds import ImageArtifactType from openhcs.core.runtime_artifact_values import RuntimeValue from openhcs.core.runtime_image_values import ImagePayloadMetadata -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.runtime_stack_cache import RuntimeImageStackCache from tests.context_support import ( executing_context, @@ -55,7 +55,7 @@ def test_context_image_cache_release_preserves_runtime_artifacts_and_observation ) retained_array = np.ones((1, 2, 2), dtype=np.uint16) retained_payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE + plane_axis=RuntimeSlicePlaneAxis ).payload_with(retained_array, None) value = RuntimeValue.normalize(output_plan, retained_payload, axis_id="A01") store = context.runtime_value_store diff --git a/tests/unit/test_runtime_value_store.py b/tests/unit/test_runtime_value_store.py index 429666d689..3241b89104 100644 --- a/tests/unit/test_runtime_value_store.py +++ b/tests/unit/test_runtime_value_store.py @@ -1,3 +1,4 @@ +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from openhcs.core.measurement_dialect import PlainMeasurementDialect from dataclasses import replace import pickle @@ -64,9 +65,6 @@ RuntimeObjectLabelMeasurementQueryCache, ) from openhcs.core.runtime_artifact_queries import RuntimeMeasurementTablesQueryCache -from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, -) from openhcs.core.source_image_provenance import SourceImageProvenancePlanes from openhcs.core.runtime_artifact_values import ArtifactKey, RuntimeValue from openhcs.core.runtime_object_labels import ObjectLabelSet, ObjectLabelVariantData @@ -901,7 +899,7 @@ def test_runtime_artifact_input_projection_transposes_producer_stack_axis() -> N for channel_index, channel in enumerate(("1", "2", "3"), start=1): group_plan = output_plan.for_group(channel) payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=( SourceImageProvenancePlanes.from_components( paths=tuple(f"/source/site_{site}.tif" for site in (1, 2, 3)), @@ -988,7 +986,7 @@ def test_artifact_candidate_scopes_preserve_projected_site_time_correlation() -> variable_components=variables, ) payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("/source/original_first.tif", "/source/original_second.tif"), component_metadata=( @@ -1162,7 +1160,7 @@ def test_runtime_artifact_input_projection_ignores_scalar_pixel_contributors() - stacked_store = RuntimeValueStore() stacked_payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=runtime_planes, ).payload_with( np.stack( @@ -1204,7 +1202,7 @@ def test_runtime_artifact_input_projection_collapses_excluded_singleton_axis() - for site_index, site in enumerate(("1", "2"), start=1): group_plan = output_plan.for_group(site) payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=( SourceImageProvenancePlanes.from_components( paths=(f"/source/site_{site}.tif",), @@ -1263,7 +1261,7 @@ def test_runtime_artifact_input_projection_collapses_excluded_singleton_axis() - ) ), ) - assert payload.metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert payload.metadata.plane_axis is RuntimeSlicePlaneAxis def test_runtime_artifact_input_reconstructs_singleton_producer_group_axis() -> None: @@ -1315,7 +1313,7 @@ def test_runtime_artifact_input_reconstructs_singleton_producer_group_axis() -> payload.data, np.full((1, 2, 2), 1, dtype=np.float32), ) - assert payload.metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert payload.metadata.plane_axis is RuntimeSlicePlaneAxis def test_runtime_artifact_input_keeps_singleton_scalar_selection_unstacked() -> None: @@ -1365,7 +1363,7 @@ def test_runtime_artifact_input_keeps_singleton_scalar_selection_unstacked() -> payload = runtime_input.resolve_value(store) assert payload.data.shape == (2, 2) - assert payload.metadata.plane_axis is None + assert payload.metadata.plane_axis.unstacked def test_runtime_artifact_input_projection_selects_compiler_owned_group_for_ungrouped_invocation(): diff --git a/tests/unit/test_runtime_values.py b/tests/unit/test_runtime_values.py index 24565f2909..86ee5ea327 100644 --- a/tests/unit/test_runtime_values.py +++ b/tests/unit/test_runtime_values.py @@ -1,3 +1,5 @@ +from openhcs.core.runtime_object_label_domains import PayloadLabelScope, PlaneLabelScope +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis import pickle from openhcs.interop.cellprofiler.object_label_variants import ( SmallRemovedLabels, @@ -33,7 +35,6 @@ from openhcs.core.runtime_image_values import ( ImageMetadataPayload, ImagePayloadMetadata, - ImagePayloadMetadataCompositionMode, ImageUnitIntervalIntensityMetadata, MaskedImagePayload, ) @@ -73,7 +74,6 @@ ObjectLabelDomain, ObjectLabelDomainMetadata, ObjectLabelDomainMetadataStrategy, - ObjectLabelDomainScope, PresentObjectLabelIdsDomainDeclaration, ) from openhcs.core.runtime_tabular_values import ( @@ -90,7 +90,6 @@ FinalLabels, ) from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisValueProjection, ) from openhcs.core.runtime_spatial_grid import ( @@ -364,7 +363,7 @@ def test_object_label_pure_2d_aggregator_preserves_dense_payload_domains() -> No ), domain=ObjectLabelDomain( declared_object_ids=(1,), - scope=ObjectLabelDomainScope.PAYLOAD, + scope=PayloadLabelScope, ), ) second = ObjectLabelPayload( @@ -373,19 +372,19 @@ def test_object_label_pure_2d_aggregator_preserves_dense_payload_domains() -> No ), domain=ObjectLabelDomain( declared_object_ids=(2,), - scope=ObjectLabelDomainScope.PAYLOAD, + scope=PayloadLabelScope, ), ) aggregated = ObjectLabelPure2DSliceAggregator.aggregate( (first, second), "numpy", - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) assert isinstance(aggregated, ObjectLabelPayload) - assert aggregated.domain.scope is ObjectLabelDomainScope.PLANE - assert aggregated.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert aggregated.domain.scope.ids_per_plane + assert aggregated.plane_axis is RuntimeSlicePlaneAxis np.testing.assert_array_equal( aggregated.labels, np.asarray( @@ -412,7 +411,7 @@ def test_object_label_aggregation_preserves_variant_plane_writes_and_transport() for index, plane in enumerate(planes) ) aggregate = ObjectLabelPure2DSliceAggregator.aggregate( - values, "numpy", plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + values, "numpy", plane_axis=RuntimeSlicePlaneAxis, ) assert aggregate.shape == (2, 1, 2) assert aggregate.dtype == np.dtype(np.int32) @@ -442,17 +441,17 @@ def test_object_label_pure_2d_aggregator_preserves_single_payload_identity() -> ), domain=ObjectLabelDomain( declared_object_ids=(1,), - scope=ObjectLabelDomainScope.PAYLOAD, + scope=PayloadLabelScope, ), ) aggregated = ObjectLabelPure2DSliceAggregator.aggregate( (payload,), "numpy", - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) - assert aggregated.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert aggregated.plane_axis is RuntimeSlicePlaneAxis assert aggregated.labels.ndim == 3 assert aggregated.domain.declared_object_id_domains == ((1,),) @@ -470,16 +469,16 @@ def test_object_label_pure_2d_aggregator_rejects_unprojected_plane_stack() -> No ), domain=ObjectLabelDomain( declared_object_id_domains=((1,), ()), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) with pytest.raises(ValueError, match=r"declares 2 plane\(s\)"): ObjectLabelPure2DSliceAggregator.aggregate( (payload,), "numpy", - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) @@ -502,7 +501,7 @@ def test_object_label_pure_2d_aggregator_preserves_slice_source_paths() -> None: aggregated = ObjectLabelPure2DSliceAggregator.aggregate( (first, second), "numpy", - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) assert isinstance(aggregated, ObjectLabelPayload) @@ -550,13 +549,13 @@ def test_object_label_pure_2d_aggregator_uses_image_metadata_composition() -> No ) expected_metadata = ImagePayloadMetadata.compose( (first, second), - mode=ImagePayloadMetadataCompositionMode.STACK, + mode=RuntimeSlicePlaneAxis, ) aggregated = ObjectLabelPure2DSliceAggregator.aggregate( (first, second), "numpy", - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) assert aggregated.source_provenance == expected_metadata.source_provenance @@ -606,12 +605,12 @@ def test_object_label_pure_2d_aggregator_declares_source_binding_plane_axis() -> aggregated = ObjectLabelPure2DSliceAggregator.aggregate( (first_set, second_set), "numpy", - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, ) assert isinstance(aggregated, ObjectLabelSet) - assert aggregated.domain.scope is ObjectLabelDomainScope.PLANE - assert aggregated.plane_axis is RuntimePlaneAxis.SOURCE_BINDING + assert aggregated.domain.scope.ids_per_plane + assert aggregated.plane_axis is SourceBindingPlaneAxis assert aggregated.source_image_name == "rawDNA" assert aggregated.source_image_provenance_planes.paths == ( "/input/rawDNA.tif", @@ -639,13 +638,13 @@ def test_source_image_object_label_build_accepts_matching_runtime_slice_axis() - image=image, labels=labels, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), ).payload() - assert payload.domain.scope is ObjectLabelDomainScope.PLANE - assert payload.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert payload.domain.scope.ids_per_plane + assert payload.plane_axis is RuntimeSlicePlaneAxis assert payload.source_image_names == ("OrigHoechst", "OrigHoechst") @@ -681,13 +680,13 @@ def test_source_image_object_label_build_accepts_matching_source_binding_axis() image=image, labels=labels, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.SOURCE_BINDING, + axis=SourceBindingPlaneAxis, axis_size=2, ), ).payload() - assert payload.domain.scope is ObjectLabelDomainScope.PLANE - assert payload.plane_axis is RuntimePlaneAxis.SOURCE_BINDING + assert payload.domain.scope.ids_per_plane + assert payload.plane_axis is SourceBindingPlaneAxis assert payload.source_image_names == ("OrigHoechst", "OrigER") @@ -716,11 +715,11 @@ def test_object_label_pure_2d_aggregator_prefers_explicit_source_plane_aliases() aggregated = ObjectLabelPure2DSliceAggregator.aggregate( (first_set, second_set), "numpy", - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, ) assert isinstance(aggregated, ObjectLabelSet) - assert aggregated.plane_axis is RuntimePlaneAxis.SOURCE_BINDING + assert aggregated.plane_axis is SourceBindingPlaneAxis assert aggregated.source_image_names == ("rawDNA", "rawGFP") assert aggregated.source_image_name == "CellProfilerInternalImage" @@ -750,11 +749,11 @@ def test_object_label_pure_2d_aggregator_respects_declared_source_binding_axis() aggregated = ObjectLabelPure2DSliceAggregator.aggregate( (first_set, second_set), "numpy", - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, ) assert isinstance(aggregated, ObjectLabelSet) - assert aggregated.plane_axis is RuntimePlaneAxis.SOURCE_BINDING + assert aggregated.plane_axis is SourceBindingPlaneAxis assert aggregated.source_image_names == ("rawDNA", "rawDNA") @@ -770,32 +769,32 @@ def test_object_label_projected_plane_reduces_domain_to_payload_scope() -> None: dtype=np.int32, ) ), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_image_names=("rawDNA", "rawGFP"), domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) projected = labels.with_projected_plane(labels.labels[1], 1) - assert projected.domain.scope is ObjectLabelDomainScope.PAYLOAD + assert projected.domain.scope.ids_span_payload assert projected.domain.declared_object_ids == (2,) assert projected.domain.declared_object_id_domains == () - assert projected.plane_axis is None + assert projected.plane_axis.unstacked assert projected.source_image_names == ("rawGFP",) def test_object_label_domain_payload_scope_collapses_plane_id_domains() -> None: domain = ObjectLabelDomain( declared_object_id_domains=((1,), (1, 3), (2, 3)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ) - payload_domain = domain.with_scope(ObjectLabelDomainScope.PAYLOAD) + payload_domain = domain.with_scope(PayloadLabelScope) - assert payload_domain.scope is ObjectLabelDomainScope.PAYLOAD + assert payload_domain.scope.ids_span_payload assert payload_domain.declared_object_ids == (1, 2, 3) assert payload_domain.declared_object_id_domains == () @@ -811,7 +810,7 @@ def test_object_label_projected_plane_promotes_channel_provenance_to_scalar() -> dtype=np.int32, ) ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=( "/input/A01_s1_w1.tif", @@ -825,7 +824,7 @@ def test_object_label_projected_plane_promotes_channel_provenance_to_scalar() -> source_image_names=("OrigHoechst", "OrigHoechst"), domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) @@ -845,7 +844,7 @@ def test_object_label_projected_plane_promotes_channel_provenance_to_scalar() -> ).payload_with(np.zeros((2, 2, 2), dtype=np.float32), None) enriched = projected.with_source_image_context(source_image) - assert projected.domain.scope is ObjectLabelDomainScope.PAYLOAD + assert projected.domain.scope.ids_span_payload assert projected.domain.declared_object_ids == (2,) assert projected.source_path == "/input/A01_s2_w1.tif" assert dict(projected.source_component_metadata) == { @@ -904,11 +903,11 @@ def test_object_label_measurement_replacement_rejects_implicit_plane_selection() dtype=np.int32, ) ), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_image_names=("rawDNA", "rawGFP", "rawFarRed"), domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,), (3,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) @@ -931,11 +930,11 @@ def test_object_label_source_plane_projection_preserves_projected_variants() -> variant_data=ObjectLabelVariantData( labels=labels, variants={SmallRemovedLabels: small_removed} ), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_image_names=("rawDNA", "rawGFP"), domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2, 3)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) @@ -943,7 +942,7 @@ def test_object_label_source_plane_projection_preserves_projected_variants() -> np.testing.assert_array_equal(projected.labels, labels[1]) np.testing.assert_array_equal(projected.variant_labels(SmallRemovedLabels), small_removed[1]) - assert projected.domain.scope is ObjectLabelDomainScope.PAYLOAD + assert projected.domain.scope.ids_span_payload assert projected.domain.declared_object_ids == (2, 3) assert projected.source_image_names == ("rawGFP",) @@ -981,13 +980,13 @@ def test_object_label_plane_projection_preserves_ordered_domains_and_variants() variant_data=ObjectLabelVariantData( labels=labels, variants={SmallRemovedLabels: small_removed} ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("/input/one.tif", "/input/two.tif", "/input/three.tif"), ), domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,), (3, 4)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) @@ -1020,11 +1019,11 @@ def test_sparse_object_label_plane_projection_preserves_representation() -> None labels=SparseIJVLabelRows.from_dense_stack(dense) ), representation=ObjectLabelRepresentation.SPARSE_IJV, - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_spatial_domain=SourceSpatialDomain(source_shape_yx=(2, 2)), domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,), (3,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) @@ -1062,14 +1061,14 @@ def test_object_label_pure_2d_aggregator_preserves_sparse_ijv_sets() -> None: aggregated = ObjectLabelPure2DSliceAggregator.aggregate( (first, second), "numpy", - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) assert isinstance(aggregated, ObjectLabelSet) assert aggregated.representation is ObjectLabelRepresentation.SPARSE_IJV assert isinstance(aggregated.labels, SparseIJVLabelRows) assert aggregated.labels.has_slice_index - assert aggregated.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert aggregated.plane_axis is RuntimeSlicePlaneAxis def test_object_label_pure_2d_aggregator_preserves_sparse_ijv_payloads() -> None: @@ -1093,14 +1092,14 @@ def test_object_label_pure_2d_aggregator_preserves_sparse_ijv_payloads() -> None aggregated = ObjectLabelPure2DSliceAggregator.aggregate( (first, second), "numpy", - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) assert isinstance(aggregated, ObjectLabelPayload) assert aggregated.representation is ObjectLabelRepresentation.SPARSE_IJV assert isinstance(aggregated.labels, SparseIJVLabelRows) assert aggregated.labels.has_slice_index - assert aggregated.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert aggregated.plane_axis is RuntimeSlicePlaneAxis def test_empty_sparse_ijv_stack_preserves_runtime_slice_count() -> None: @@ -1129,7 +1128,7 @@ def test_empty_sparse_ijv_stack_preserves_runtime_slice_count() -> None: aggregated = ObjectLabelPure2DSliceAggregator.aggregate( (first, second), "numpy", - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) assert isinstance(aggregated, ObjectLabelSet) @@ -1336,7 +1335,7 @@ def test_object_label_payload_builder_uses_nominal_payload_registry() -> None: assert rebuilt.labels is transformed_labels assert rebuilt.domain.declared_object_count == 1 assert rebuilt.domain.declared_object_ids == (2,) - assert rebuilt.plane_axis is None + assert rebuilt.plane_axis.unstacked assert rebuilt.spatial_origin_yx == (3, 5) assert rebuilt.source_spatial_shape_yx == (20, 30) @@ -1455,7 +1454,7 @@ def test_composed_image_metadata_preserves_declared_source_channel_axis() -> Non def test_collapse_leading_plane_axis_keeps_common_identity_and_contributors() -> None: metadata = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_names=("DNA", "Actin"), source_plane_dtypes=("uint16", "uint16"), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( @@ -1472,7 +1471,7 @@ def test_collapse_leading_plane_axis_keeps_common_identity_and_contributors() -> collapsed = metadata.collapse_leading_plane_axis() - assert collapsed.plane_axis is None + assert collapsed.plane_axis.unstacked assert dict(collapsed.source_component_metadata or {}) == { "well": "A01", "site": "1", @@ -1519,7 +1518,7 @@ def test_source_context_preserves_target_payload_channel_axis_domain() -> None: ).payload_with(np.zeros((2, 4, 5, 3), dtype=np.float32), None) target = ImagePayloadMetadata( axes=PayloadAxes.colour_samples(4), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.zeros((1, 2, 4, 5, 3), dtype=np.float32), None) contextualized = ( @@ -1530,7 +1529,7 @@ def test_source_context_preserves_target_payload_channel_axis_domain() -> None: metadata = contextualized.metadata assert metadata.axis_index(ColourAxis, contextualized) == 4 - assert metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert metadata.plane_axis is RuntimeSlicePlaneAxis assert metadata.source_path == "/input/A01_s001_color.tif" assert dict(metadata.source_component_metadata or {}) == { "well": "A01", @@ -1560,18 +1559,18 @@ def test_composed_image_metadata_distinguishes_bundle_union_from_stack_consensus bundle = ImagePayloadMetadata.compose( (first, second), - mode=ImagePayloadMetadataCompositionMode.BUNDLE, + mode=SourceBindingPlaneAxis, ) stack = ImagePayloadMetadata.compose((first, second)) - assert bundle.plane_axis is RuntimePlaneAxis.SOURCE_BINDING + assert bundle.plane_axis is SourceBindingPlaneAxis assert dict(bundle.source_component_metadata) == { "well": "A01", "site": "1", "channel": "1", "timepoint": "7", } - assert stack.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert stack.plane_axis is RuntimeSlicePlaneAxis assert dict(stack.source_component_metadata) == { "well": "A01", "site": "1", @@ -1629,7 +1628,7 @@ def test_bundle_image_metadata_preserves_payload_source_provenance() -> None: bundle = ImagePayloadMetadata.compose( (first, second), - mode=ImagePayloadMetadataCompositionMode.BUNDLE, + mode=SourceBindingPlaneAxis, ) stack = ImagePayloadMetadata.compose((first, second)) @@ -1660,7 +1659,7 @@ def test_bundle_image_metadata_preserves_declared_source_alias_axis() -> None: metadata = ImagePayloadMetadata.compose( payloads, - mode=ImagePayloadMetadataCompositionMode.BUNDLE, + mode=SourceBindingPlaneAxis, ) assert metadata.source_image_names == ( @@ -1691,7 +1690,7 @@ def test_removed_source_binding_planes_become_nested_stack_contributors() -> Non for channel_index in range(3) ), ), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_plane_intensity_scales=(255.0, 255.0, 255.0), source_plane_dtypes=("uint8", "uint8", "uint8"), unit_interval_intensity=ImageUnitIntervalIntensityMetadata( @@ -1751,12 +1750,12 @@ def test_object_label_payload_from_composed_source_image_keeps_site_axis_metadat image=image, labels=labels, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), ).payload() - assert payload.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert payload.plane_axis is RuntimeSlicePlaneAxis assert payload.source_image_provenance_planes.paths == ( "/input/A01_s001_w1_z001_t001.TIF", "/input/A01_s002_w1_z001_t001.TIF", @@ -1790,8 +1789,8 @@ def test_object_label_payload_from_source_image_keeps_ambiguous_3d_domain_payloa declared_object_count=4, ).payload() - assert payload.domain.scope is ObjectLabelDomainScope.PAYLOAD - assert payload.plane_axis is None + assert payload.domain.scope.ids_span_payload + assert payload.plane_axis.unstacked assert payload.domain.declared_object_count == 4 assert payload.domain.declared_object_id_domains == () @@ -1823,13 +1822,13 @@ def test_object_label_payload_from_source_image_declares_source_binding_plane_do image=image, labels=labels, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.SOURCE_BINDING, + axis=SourceBindingPlaneAxis, axis_size=2, ), ).payload() - assert payload.domain.scope is ObjectLabelDomainScope.PLANE - assert payload.plane_axis is RuntimePlaneAxis.SOURCE_BINDING + assert payload.domain.scope.ids_per_plane + assert payload.plane_axis is SourceBindingPlaneAxis assert payload.domain.declared_object_count is None assert payload.domain.declared_object_id_domains == ((1, 2), (3, 4)) @@ -1856,12 +1855,12 @@ def test_object_label_payload_domain_declaration_overrides_source_binding_planes payload = SourceImageObjectLabelBuildRequest( image=image, labels=labels, - domain_scope=ObjectLabelDomainScope.PAYLOAD, + domain_scope=PayloadLabelScope, declared_object_count=4, ).payload() - assert payload.domain.scope is ObjectLabelDomainScope.PAYLOAD - assert payload.plane_axis is None + assert payload.domain.scope.ids_span_payload + assert payload.plane_axis.unstacked assert payload.domain.declared_object_count == 4 assert payload.domain.declared_object_id_domains == () @@ -1876,7 +1875,7 @@ def test_source_image_metadata_does_not_guess_plane_axis_from_paths() -> None: ), ) - assert image.metadata.plane_axis is None + assert image.metadata.plane_axis.unstacked def test_source_image_metadata_does_not_guess_axis_from_channel_components() -> None: @@ -1892,14 +1891,14 @@ def test_source_image_metadata_does_not_guess_axis_from_channel_components() -> ), ) - assert image.metadata.plane_axis is None + assert image.metadata.plane_axis.unstacked def test_source_image_metadata_preserves_explicit_runtime_slice_axis() -> None: image = ImageMetadataPayload( data=np.zeros((2, 4, 5), dtype=np.float32), metadata=ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( component_metadata=( {"well": "A01", "site": "1", "channel": "1"}, @@ -1915,17 +1914,17 @@ def test_source_image_metadata_preserves_explicit_runtime_slice_axis() -> None: ) ) - assert image.metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert image.metadata.plane_axis is RuntimeSlicePlaneAxis payload = SourceImageObjectLabelBuildRequest( image=image, labels=labels, plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), ).payload() - assert payload.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert payload.plane_axis is RuntimeSlicePlaneAxis def test_object_label_set_from_source_image_uses_image_metadata() -> None: @@ -2025,10 +2024,10 @@ def test_object_label_payload_source_image_context_fills_partial_stack_metadata( ) labels = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=np.zeros((2, 4, 5), dtype=np.int32)), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((), ()), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=(None, None), component_metadata=(None, None) @@ -2064,10 +2063,10 @@ def test_plane_scoped_object_labels_promote_scalar_source_identity() -> None: ).payload_with(np.zeros((4, 5), dtype=np.float32), None) labels = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=np.ones((1, 4, 5), dtype=np.int32)), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,),), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) @@ -2107,10 +2106,10 @@ def test_plane_scoped_object_labels_expand_indexed_scalar_source_identity() -> N ).payload_with(np.zeros((2, 4, 5), dtype=np.float32), None) labels = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=np.ones((2, 4, 5), dtype=np.int32)), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (1,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) @@ -2147,10 +2146,10 @@ def test_source_aligned_object_labels_replace_stale_plane_provenance() -> None: ) labels = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=np.zeros((2, 4, 5), dtype=np.int32)), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((), ()), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=(source_paths[0], source_paths[0]), @@ -2302,7 +2301,7 @@ def test_declared_source_image_projection_selects_pixels_and_metadata_together() source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("blue.tif", "green.tif"), ), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, ).payload_with(planes, None) projected = payload.metadata.project_declared_source_image( @@ -2312,7 +2311,7 @@ def test_declared_source_image_projection_selects_pixels_and_metadata_together() np.testing.assert_array_equal(projected.data, planes[1]) metadata = projected.metadata - assert metadata.plane_axis is None + assert metadata.plane_axis.unstacked assert metadata.source_image_names == ("Green",) assert metadata.source_path == "green.tif" @@ -2572,10 +2571,10 @@ def test_runtime_projection_requirement_projects_singleton_object_label_stack() labels = np.arange(4, dtype=np.int32).reshape(1, 2, 2) payload = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1, 2, 3),), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( component_metadata=({"well": "A01", "site": 1, "channel": 1},), @@ -2606,10 +2605,10 @@ def test_runtime_projection_requirement_tracks_multi_plane_runtime_coordinates() labels = np.arange(50, dtype=np.int32).reshape(2, 5, 5) payload = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=(tuple(range(1, 25)), tuple(range(25, 50))), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( component_metadata=( @@ -2716,10 +2715,10 @@ def test_runtime_slice_axis_rejects_multi_plane_projection() -> None: ) payload = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,), (3,), (4,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) @@ -2738,17 +2737,17 @@ def test_object_label_runtime_slice_projection_keeps_volume_plane_domain() -> No labels[1, :, 1:4, 1:4] = 2 payload = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) projected = RuntimeSliceProjection.value_for_slice( payload, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=1, axis_size=2 + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=2 ), ) @@ -2767,10 +2766,10 @@ def test_object_label_runtime_slice_projection_preserves_grouped_source_planes() ) payload = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), source_component_metadata={ "well": "A01", @@ -2810,7 +2809,7 @@ def test_object_label_runtime_slice_projection_preserves_grouped_source_planes() ) assert isinstance(projected, ObjectLabelPayload) - assert projected.plane_axis is RuntimePlaneAxis.SOURCE_BINDING + assert projected.plane_axis is SourceBindingPlaneAxis assert projected.domain.declared_object_id_domains == ((1,), (2,)) assert projected.source_path is None assert dict(projected.source_component_metadata) == { @@ -2882,7 +2881,7 @@ def test_normalize_artifact_value_builds_key_for_nominal_payload(): def _declared_runtime_slice_image(slice_count: int) -> ImageMetadataPayload: return ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.zeros((slice_count, 4, 5), dtype=np.float32), None) @@ -2901,7 +2900,7 @@ def test_group_scoped_artifact_consumes_singleton_payload_axis() -> None: ) assert value.data.data.shape == (4, 5) - assert value.data.metadata.plane_axis is None + assert value.data.metadata.plane_axis.unstacked def test_group_scoped_artifact_rejects_multi_plane_payload_axis() -> None: @@ -2937,7 +2936,7 @@ def test_group_scoped_stack_artifact_preserves_declared_payload_axis() -> None: assert value.data.data.shape == (2, 4, 5) assert ( - value.data.metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + value.data.metadata.plane_axis is RuntimeSlicePlaneAxis ) @@ -2991,7 +2990,7 @@ def test_grouped_scalar_artifact_records_compose_one_runtime_axis() -> None: composed = RuntimeValue.compose(records) assert composed.data.shape == (2, 4, 5) - assert composed.metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert composed.metadata.plane_axis is RuntimeSlicePlaneAxis def test_normalize_artifact_value_accepts_dataclass_measurement_rows(): @@ -3062,12 +3061,12 @@ def test_normalize_artifact_value_aggregates_slice_aligned_object_label_domains( assert payload.name == output_plan.name assert payload.dimensions == () assert payload.source_image_name is None - assert payload.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert payload.plane_axis is RuntimeSlicePlaneAxis assert payload.representation is ObjectLabelRepresentation.DENSE_LABELS assert payload.dtype == np.dtype(np.int32) assert payload.domain == ObjectLabelDomain( declared_object_id_domains=((1, 2, 3, 4), (1, 2, 3, 4)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ) assert payload.source_spatial_domain == SourceSpatialDomain() assert payload.source_path is None @@ -3134,12 +3133,12 @@ def test_normalize_artifact_value_preserves_slice_aligned_object_label_sources() assert payload.name == output_plan.name assert payload.dimensions == () assert payload.source_image_name is None - assert payload.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert payload.plane_axis is RuntimeSlicePlaneAxis assert payload.representation is ObjectLabelRepresentation.DENSE_LABELS assert payload.dtype == np.dtype(np.int32) assert payload.domain == ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ) assert payload.source_spatial_domain == SourceSpatialDomain() assert payload.source_path is None @@ -3203,9 +3202,9 @@ def test_object_label_payload_validator_accepts_nominal_slice_aggregate(): ), domain=ObjectLabelDomain( declared_object_id_domains=((1, 2), (3, 4)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) value = RuntimeValue( key=ArtifactKey( @@ -3590,7 +3589,7 @@ def test_derived_image_payload_context_preserves_explicit_channel_axis_removal() None ): projection = RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=1, ) source = ImageMetadataPayload( @@ -3598,14 +3597,14 @@ def test_derived_image_payload_context_preserves_explicit_channel_axis_removal() metadata=ImagePayloadMetadata( source_dtype="uint8", axes=PayloadAxes.colour_samples(3), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ), ) output = ImageMetadataPayload( data=np.ones((1, 4, 5), dtype=np.float32), metadata=ImagePayloadMetadata( source_dtype="float32", - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ), ) @@ -3619,7 +3618,7 @@ def test_derived_image_payload_context_preserves_explicit_channel_axis_removal() def test_derived_image_payload_context_inherits_channel_axis_for_plain_array() -> None: projection = RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=1, ) source = ImageMetadataPayload( @@ -3627,7 +3626,7 @@ def test_derived_image_payload_context_inherits_channel_axis_for_plain_array() - metadata=ImagePayloadMetadata( source_dtype="uint8", axes=PayloadAxes.colour_samples(3), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ), ) @@ -4035,9 +4034,9 @@ def test_object_label_source_context_keeps_source_aligned_stack_planes() -> None variant_data=ObjectLabelVariantData(labels=np.ones((60, 4, 5), dtype=np.int32)), domain=ObjectLabelDomain( declared_object_id_domains=tuple((1,) for _path in paths), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, ) contextualized = labels.with_source_image_context(image) @@ -4093,9 +4092,9 @@ def test_object_label_source_context_keeps_source_aligned_stack_planes_with_scal variant_data=ObjectLabelVariantData(labels=np.ones((3, 4, 5), dtype=np.int32)), domain=ObjectLabelDomain( declared_object_id_domains=((1,), (1,), (1,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_path=source_path, source_component_metadata={ "well": "A01", @@ -4246,7 +4245,7 @@ def test_masked_image_payload_omits_only_declared_channel_axis() -> None: data=data, mask=np.ones((2, 4, 5), dtype=bool), metadata=ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, axes=PayloadAxes.colour_samples(-1), ), ) @@ -4423,10 +4422,10 @@ def test_image_payload_metadata_reads_object_label_spatial_context() -> None: def test_object_label_runtime_slice_projection_projects_source_path() -> None: payload = ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=np.zeros((3, 4, 5), dtype=np.int32)), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((), (), ()), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), source_spatial_domain=SourceSpatialDomain(source_shape_yx=(10, 12)), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( @@ -4446,7 +4445,7 @@ def test_object_label_runtime_slice_projection_projects_source_path() -> None: projected = RuntimeSliceProjection.value_for_slice( payload, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=1, axis_size=3 + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=3 ), ) @@ -4470,7 +4469,7 @@ def test_runtime_slice_projection_does_not_infer_raw_array_axis() -> None: projected = RuntimeSliceProjection.value_for_slice( value, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=1, axis_size=2 + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=2 ), ) @@ -4481,10 +4480,10 @@ def test_object_label_set_runtime_slice_projection_projects_source_path() -> Non label_set = ObjectLabelSet( name="Nuclei", variant_data=ObjectLabelVariantData(labels=np.zeros((3, 4, 5), dtype=np.int32)), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, domain=ObjectLabelDomain( declared_object_id_domains=((), (), ()), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), source_spatial_domain=SourceSpatialDomain(source_shape_yx=(10, 12)), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( @@ -4504,7 +4503,7 @@ def test_object_label_set_runtime_slice_projection_projects_source_path() -> Non projected = RuntimeSliceProjection.value_for_slice( label_set, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=2, axis_size=3 + axis=RuntimeSlicePlaneAxis, plane_index=2, axis_size=3 ), ) @@ -4553,7 +4552,7 @@ def test_measurement_table_runtime_slice_projection_projects_source_identity() - projected = RuntimeSliceProjection.value_for_slice( table, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=1, axis_size=2 + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=2 ), ) @@ -4606,7 +4605,7 @@ def test_relationship_runtime_slice_projection_projects_source_identity() -> Non projected = RuntimeSliceProjection.value_for_slice( relationship, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=1, axis_size=2 + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=2 ), ) @@ -4807,7 +4806,7 @@ def test_pure_2d_auxiliary_aggregator_uses_runtime_object_label_domains() -> Non stacked = Pure2DAuxiliaryOutputAggregator.aggregate([first, second], "numpy") assert isinstance(stacked, ObjectLabelPayload) - assert stacked.domain.scope is ObjectLabelDomainScope.PLANE + assert stacked.domain.scope.ids_per_plane assert stacked.domain.declared_object_id_domains == ((4,), (7,)) assert stacked.source_image_names == ("rawDNA", "rawActin") @@ -4820,11 +4819,11 @@ def test_object_label_domain_rejects_square_diagonal_plane_inference() -> None: with pytest.raises(ValueError, match=r"declares 4 plane\(s\)"): ObjectLabelPayload( variant_data=ObjectLabelVariantData(labels=labels), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_names=("A", "B"), domain=ObjectLabelDomain( declared_object_id_domains=((1, 2), (10,), (20,), (3, 4, 5)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) @@ -4841,18 +4840,18 @@ def test_pure_2d_slice_identity_does_not_select_object_label_planes() -> None: dtype=np.int32, ) ), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_image_names=("rawDNA", "rawActin", "rawMito"), domain=ObjectLabelDomain( declared_object_id_domains=((1,), (7,), (9,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) projected = RuntimeSliceProjection.identity_projected_value( payload, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=1, axis_size=3 + axis=RuntimeSlicePlaneAxis, plane_index=1, axis_size=3 ), ) @@ -4873,12 +4872,12 @@ def test_pure_2d_slice_index_projector_preserves_explicit_payload_domain() -> No projected = RuntimeSliceProjection.identity_projected_value( payload, RuntimePlaneAxisValueProjection.from_selected_plane( - axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_index=0, axis_size=1 + axis=RuntimeSlicePlaneAxis, plane_index=0, axis_size=1 ), ) assert isinstance(projected, ObjectLabelPayload) - assert projected.domain.scope is ObjectLabelDomainScope.PAYLOAD + assert projected.domain.scope.ids_span_payload assert projected.domain.declared_object_count == 9 assert projected.domain.declared_object_ids == () assert projected.source_image_names == ("rawActin",) diff --git a/tests/unit/test_saved_source_label_plane_domain.py b/tests/unit/test_saved_source_label_plane_domain.py index 3e7d2c527a..d34f2f4917 100644 --- a/tests/unit/test_saved_source_label_plane_domain.py +++ b/tests/unit/test_saved_source_label_plane_domain.py @@ -1,6 +1,10 @@ """Saved label admission retains declared runtime axes, never guessed Z axes.""" from __future__ import annotations +from openhcs.core.runtime_plane_projection import WholeAxisProjection +from openhcs.core.runtime_object_label_domains import PayloadLabelScope, PlaneLabelScope +from openhcs.core.runtime_plane_projection import NoPlaneAxis +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis import numpy as np import pytest @@ -11,7 +15,6 @@ from openhcs.core.runtime_image_values import ImagePayloadMetadata from openhcs.core.runtime_measurements import MeasurementRowAxisField from openhcs.core.runtime_object_label_building import SourceImageObjectLabelBuildRequest -from openhcs.core.runtime_object_label_domains import ObjectLabelDomainScope from openhcs.core.runtime_tabular_values import MeasurementObjectRowIdentity from openhcs.core.runtime_plane_projection import RuntimePlaneAxis, RuntimePlaneAxisValueProjection from openhcs.core.source_image_provenance import SourceImageProvenancePlanes @@ -26,7 +29,7 @@ ) -def _source(labels, *, axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_count=None, spacing=(1.3556, 1.3556)): +def _source(labels, *, axis=RuntimeSlicePlaneAxis, plane_count=None, spacing=(1.3556, 1.3556)): count = labels.shape[0] if plane_count is None else plane_count return ImagePayloadMetadata( plane_axis=axis, @@ -46,7 +49,7 @@ def _saved_label_input(source): ) -@pytest.mark.parametrize("axis", tuple(RuntimePlaneAxis)) +@pytest.mark.parametrize("axis", RuntimePlaneAxis.leading_axes()) @pytest.mark.parametrize("plane_count", (1, 2)) def test_explicit_object_projection_preserves_axis_without_acquisition_provenance( axis, plane_count, @@ -64,7 +67,7 @@ def test_explicit_object_projection_preserves_axis_without_acquisition_provenanc assert payload.domain.declared_object_id_domains == ((1,),) * plane_count assert payload.source_provenance == source.metadata.source_provenance np.testing.assert_array_equal(payload.labels, labels) - other_axis = next(member for member in RuntimePlaneAxis if member is not axis) + other_axis = next(member for member in RuntimePlaneAxis.leading_axes() if member is not axis) with pytest.raises(ValueError, match="conflicts with the source-image axis"): SourceImageObjectLabelBuildRequest( image=source, labels=labels, @@ -74,7 +77,7 @@ def test_explicit_object_projection_preserves_axis_without_acquisition_provenanc ).payload() -@pytest.mark.parametrize("axis", tuple(RuntimePlaneAxis)) +@pytest.mark.parametrize("axis", RuntimePlaneAxis.leading_axes()) def test_original_artifact_admission_and_full_stack_shape_keep_2d_planes(axis): labels = np.zeros((2, 8, 9), dtype=np.int32) labels[0, 1:3, 2:5] = 29 @@ -85,7 +88,7 @@ def test_original_artifact_admission_and_full_stack_shape_keep_2d_planes(axis): value = CellProfilerOutputRecorder.for_artifact_type(spec.artifact_type).runtime_input_value( spec=spec, value=source ) - assert value.domain.scope is ObjectLabelDomainScope.PLANE + assert value.domain.scope.ids_per_plane assert value.plane_axis is axis assert value.domain.declared_object_id_domains == ((29,), (106,)) assert value.source_provenance.identity() == source.metadata.source_provenance.identity() @@ -135,12 +138,12 @@ def test_declared_runtime_planes_can_each_contain_a_genuine_calibrated_volume(): def test_undeclared_volume_and_explicit_payload_override_are_not_guessed_planar(): labels = np.ones((2, 6, 7), dtype=np.int32) for source, options in ( - (_source(labels, axis=None, spacing=(2.0, 1.3556, 1.3556)), {}), - (_source(labels), {"domain_scope": ObjectLabelDomainScope.PAYLOAD}), + (_source(labels, axis=NoPlaneAxis, spacing=(2.0, 1.3556, 1.3556)), {}), + (_source(labels), {"domain_scope": PayloadLabelScope}), ): value = SourceImageObjectLabelBuildRequest(image=source, labels=labels, **options).payload() - assert value.domain.scope is ObjectLabelDomainScope.PAYLOAD - assert value.plane_axis is None + assert value.domain.scope.ids_span_payload + assert value.plane_axis.unstacked assert value.measurement_planes()[0].labels.shape == (2, 6, 7) with pytest.raises(ValueError, match="Cannot project 2-D source voxel spacing onto 3-D data"): SourceVoxelSpacing((1.3556, 1.3556)).spacing_for_ndim(3) @@ -176,7 +179,7 @@ def test_explicit_projection_conflicting_with_source_declaration_is_rejected(): with pytest.raises(ValueError, match="conflicts"): SourceImageObjectLabelBuildRequest( image=_source(labels), labels=labels, - plane_projection=RuntimePlaneAxisValueProjection.preserve(axis=RuntimePlaneAxis.SOURCE_BINDING, axis_size=2), + plane_projection=RuntimePlaneAxisValueProjection.preserve(axis=SourceBindingPlaneAxis, axis_size=2), ).payload() @@ -201,11 +204,11 @@ def test_new_calibrated_leaf_executes_cooperative_admission_and_plane_building() value = CalibratedSavedLabels( image=source, labels=labels, plane_projection=projection, ).payload() - assert value.domain.scope is ObjectLabelDomainScope.PLANE + assert value.domain.scope.ids_per_plane assert len(value.measurement_planes()) == 2 with pytest.raises(ValueError, match="Physical scalar pixel size requires"): CalibratedSavedLabels(image=_source(labels, spacing=()), labels=labels, plane_projection=projection).payload() - assert _saved_label_input(_source(labels, spacing=())).domain.scope is ObjectLabelDomainScope.PLANE + assert _saved_label_input(_source(labels, spacing=())).domain.scope.ids_per_plane class PairedSourceAdmission: @@ -217,11 +220,11 @@ def __post_init__(self): raise ValueError("Paired projection accepts at most two source planes.") -class PairedSourceProjection(PairedSourceAdmission, RuntimePlaneAxisValueProjection): +class PairedSourceProjection(PairedSourceAdmission, WholeAxisProjection): """An independent projection leaf using the unchanged source constructor.""" -@pytest.mark.parametrize("axis", tuple(RuntimePlaneAxis)) +@pytest.mark.parametrize("axis", RuntimePlaneAxis.leading_axes()) def test_source_constructor_admits_a_new_cooperative_projection_capability(axis): source = _source(np.ones((2, 6, 7), dtype=np.int32), axis=axis) value = PairedSourceProjection.from_source_declaration(axis, source.metadata.source_provenance) @@ -234,10 +237,10 @@ def test_source_constructor_admits_a_new_cooperative_projection_capability(axis) PairedSourceProjection.from_source_declaration(axis, bigger.metadata.source_provenance) assert RuntimePlaneAxisValueProjection.from_source_declaration(axis, bigger.metadata.source_provenance).axis_size == 3 # Absence is a declaration, not inferred from three source records. - assert PairedSourceProjection.from_source_declaration(None, bigger.metadata.source_provenance) is None + assert PairedSourceProjection.from_source_declaration(NoPlaneAxis, bigger.metadata.source_provenance) is None -@pytest.mark.parametrize("axis", tuple(RuntimePlaneAxis)) +@pytest.mark.parametrize("axis", RuntimePlaneAxis.leading_axes()) def test_produced_volume_global_object_domain_is_independent_of_source_plane_axis(axis): labels = np.ones((2, 6, 7), dtype=np.int32) source = _source(labels, axis=axis, spacing=(2.0, 1.3556, 1.3556)) @@ -248,18 +251,18 @@ def test_produced_volume_global_object_domain_is_independent_of_source_plane_axi declared_object_ids=(1, 2), ).payload() - assert value.domain.scope is ObjectLabelDomainScope.PAYLOAD + assert value.domain.scope.ids_span_payload assert value.domain.declared_object_count == 2 assert value.domain.declared_object_ids == (1, 2) assert value.domain.declared_object_id_domains == () - assert value.plane_axis is None + assert value.plane_axis.unstacked assert len(value.measurement_planes()) == 1 np.testing.assert_array_equal(value.measurement_planes()[0].labels, labels) assert value.source_provenance.identity() == source.metadata.source_provenance.identity() assert value.parent_image_source_voxel_spacing == source.metadata.source_voxel_spacing -@pytest.mark.parametrize("axis", tuple(RuntimePlaneAxis)) +@pytest.mark.parametrize("axis", RuntimePlaneAxis.leading_axes()) def test_produced_volume_without_explicit_ids_keeps_one_global_present_id_domain(axis): labels = np.zeros((2, 6, 7), dtype=np.int32) labels[0, 1:3, 2:4] = 29 @@ -269,32 +272,32 @@ def test_produced_volume_without_explicit_ids_keeps_one_global_present_id_domain labels=labels, ).payload() - assert value.domain.scope is ObjectLabelDomainScope.PAYLOAD + assert value.domain.scope.ids_span_payload assert value.domain.declared_object_ids == (29, 106) assert value.domain.declared_object_id_domains == () - assert value.plane_axis is None + assert value.plane_axis.unstacked assert len(value.measurement_planes()) == 1 -@pytest.mark.parametrize("axis", tuple(RuntimePlaneAxis)) +@pytest.mark.parametrize("axis", RuntimePlaneAxis.leading_axes()) def test_explicit_plane_domain_still_requires_projection_and_rejects_global_count(axis): labels = np.ones((2, 6, 7), dtype=np.int32) source = _source(labels, axis=axis) with pytest.raises(ValueError, match="requires an exact plane projection"): SourceImageObjectLabelBuildRequest( - image=source, labels=labels, domain_scope=ObjectLabelDomainScope.PLANE, + image=source, labels=labels, domain_scope=PlaneLabelScope, ).payload() projection = RuntimePlaneAxisValueProjection.from_source_declaration( source.metadata.plane_axis, source.metadata.source_provenance, ) with pytest.raises(ValueError, match="per-plane domains"): SourceImageObjectLabelBuildRequest( - image=source, labels=labels, domain_scope=ObjectLabelDomainScope.PLANE, + image=source, labels=labels, domain_scope=PlaneLabelScope, plane_projection=projection, declared_object_count=1, ).payload() -@pytest.mark.parametrize("axis", tuple(RuntimePlaneAxis)) +@pytest.mark.parametrize("axis", RuntimePlaneAxis.leading_axes()) def test_generic_output_context_uses_execution_projection_not_image_storage_axis(axis): from openhcs.core.runtime_image_values import ImagePayload @@ -303,14 +306,14 @@ def test_generic_output_context_uses_execution_projection_not_image_storage_axis labels[1, 2:4, 3:5] = 106 source = _source(labels, axis=axis, spacing=(2.0, 1.3556, 1.3556)) volume = ImagePayload.of(labels).object_label_output(source, None) - assert volume.domain.scope is ObjectLabelDomainScope.PAYLOAD + assert volume.domain.scope.ids_span_payload assert volume.domain.declared_object_ids == (29, 106) np.testing.assert_array_equal(volume.measurement_planes()[0].labels, labels) projection = RuntimePlaneAxisValueProjection.from_source_declaration( source.metadata.plane_axis, source.metadata.source_provenance, ) planes = ImagePayload.of(labels).object_label_output(source, projection) - assert planes.domain.scope is ObjectLabelDomainScope.PLANE + assert planes.domain.scope.ids_per_plane assert planes.domain.declared_object_id_domains == ((29,), (106,)) assert planes.plane_axis is axis for index, plane in enumerate(planes.measurement_planes()): @@ -326,6 +329,6 @@ def test_saved_ingress_preserves_already_typed_object_domain_and_identity(): ).label_set(name="saved_labels") admitted = _saved_label_input(produced) assert admitted is produced - assert admitted.domain.scope is ObjectLabelDomainScope.PAYLOAD + assert admitted.domain.scope.ids_span_payload assert admitted.domain.declared_object_count == 1 np.testing.assert_array_equal(admitted.measurement_planes()[0].labels, labels) diff --git a/tests/unit/test_selected_plane_stream_materialization.py b/tests/unit/test_selected_plane_stream_materialization.py index 4df0ccb6d6..eda16a29e6 100644 --- a/tests/unit/test_selected_plane_stream_materialization.py +++ b/tests/unit/test_selected_plane_stream_materialization.py @@ -1,5 +1,6 @@ """Selected image declarations must materialize with their exact source identity.""" +from openhcs.core.runtime_plane_projection import SourceBindingPlaneAxis from openhcs.core.artifacts.kinds import ImageArtifactType from pathlib import Path @@ -92,7 +93,7 @@ def _source_stack(axis, component=Microscopy.Channel.name): return metadata.payload_with(np.arange(112, dtype=np.uint16).reshape(2, 7, 8)) -@pytest.mark.parametrize("axis", tuple(RuntimePlaneAxis)) +@pytest.mark.parametrize("axis", RuntimePlaneAxis.leading_axes()) @pytest.mark.parametrize( "output_type,artifact_name,dtype", ( @@ -182,22 +183,22 @@ def capture_stream(data_list, paths, backend, **kwargs): def test_selection_contract_keeps_exact_source_order_without_channel_inference( component, indices ): - source = _source_stack(RuntimePlaneAxis.SOURCE_BINDING, component) + source = _source_stack(SourceBindingPlaneAxis, component) pixels = np.arange(len(indices) * 56).reshape(len(indices), 7, 8) payload = SelectedPlaneImageOutput(pixels, indices).resolve_source_context( source, RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.SOURCE_BINDING, axis_size=2 + axis=SourceBindingPlaneAxis, axis_size=2 ), ) metadata = payload.metadata if len(indices) == 1: assert payload.shape == (7, 8) - assert metadata.plane_axis is None + assert metadata.plane_axis.unstacked assert dict(metadata.source_component_metadata)[component] == "2" else: assert payload.shape == (2, 7, 8) - assert metadata.plane_axis is RuntimePlaneAxis.SOURCE_BINDING + assert metadata.plane_axis is SourceBindingPlaneAxis assert metadata.retained_plane_component_values() == {component: ("2", "1")} assert metadata.source_voxel_spacing == SourceVoxelSpacing((1.3556, 1.3556)) np.testing.assert_array_equal(np.asarray(payload).reshape(-1), pixels.reshape(-1)) @@ -232,11 +233,11 @@ def with_data(self, data): selected = DeclaredCrop(np.arange(112).reshape(2, 7, 8)) calls.clear() payload = ImageArtifactType.contextualize_output( - _source_stack(RuntimePlaneAxis.SOURCE_BINDING), + _source_stack(SourceBindingPlaneAxis), selected, MAIN_FLOW, RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.SOURCE_BINDING, axis_size=2 + axis=SourceBindingPlaneAxis, axis_size=2 ), ) assert calls == ["observe", "reverse", "declaration"] @@ -264,14 +265,14 @@ def selected_source_plane_indices(self): class DeclaredResponse(ObservedProjection, SelectedDiagnosticPlaneImageOutput): pass - source = _source_stack(RuntimePlaneAxis.SOURCE_BINDING) + source = _source_stack(SourceBindingPlaneAxis) pixels = np.full((1, 7, 8), .75, dtype=np.float32) response = DeclaredResponse(pixels, (1,)) calls.clear() payload = ImageArtifactType.contextualize_output( source, response, MAIN_FLOW, RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.SOURCE_BINDING, axis_size=2 + axis=SourceBindingPlaneAxis, axis_size=2 ), ) assert calls == ["before", "selection", "after"] @@ -284,9 +285,9 @@ class DeclaredResponse(ObservedProjection, SelectedDiagnosticPlaneImageOutput): def test_all_public_declared_outputs_persist_with_selected_qa_streams( tmp_path, monkeypatch, viewer_ack_return_route ): - source = _source_stack(RuntimePlaneAxis.SOURCE_BINDING) + source = _source_stack(SourceBindingPlaneAxis) projection = RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.SOURCE_BINDING, axis_size=2 + axis=SourceBindingPlaneAxis, axis_size=2 ) labels = np.zeros((2, 7, 8), dtype=np.int32) labels[1, 2:5, 3:6] = 1 @@ -422,7 +423,7 @@ def capture_stream(data_list, paths, backend, **kwargs): request.source.item_fields["image_metadata"] ) assert metadata.source_voxel_spacing == SourceVoxelSpacing((1.3556, 1.3556)) - assert metadata.plane_axis is None + assert metadata.plane_axis.unstacked assert data.shape == (7, 8) batch = StreamingBatchMessageBuilder.build( viewer, diff --git a/tests/unit/test_singleton_rgb_label_invocation_477.py b/tests/unit/test_singleton_rgb_label_invocation_477.py index c771043b81..afe5734951 100644 --- a/tests/unit/test_singleton_rgb_label_invocation_477.py +++ b/tests/unit/test_singleton_rgb_label_invocation_477.py @@ -1,4 +1,6 @@ """Scalar RGB invocations consume only an explicitly declared singleton root.""" +from openhcs.core.runtime_object_label_domains import PayloadLabelScope, PlaneLabelScope +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis import numpy as np import pytest @@ -6,9 +8,9 @@ from openhcs.core.artifacts.kinds import ImageArtifactType, ObjectLabelsArtifactType from openhcs.core.function_contract_metadata import FunctionContractAttribute from openhcs.core.runtime_image_values import ImageMetadataPayload, ImagePayloadMetadata -from openhcs.core.runtime_object_label_domains import ObjectLabelDomain, ObjectLabelDomainScope +from openhcs.core.runtime_object_label_domains import ObjectLabelDomain from openhcs.core.runtime_object_labels import ObjectLabelSet, ObjectLabelVariantData, object_label_dense_array -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis, RuntimePlaneProjection +from openhcs.core.runtime_plane_projection import RuntimePlaneProjection from openhcs.core.runtime_slice_projection import RuntimeSliceProjectionDeclarationError from openhcs.interop.cellprofiler.runtime.function_contract_execution import CellProfilerFunctionContractExecutor from openhcs.processing.backends.cellprofiler.outlines import OverlayOutlinesModule @@ -40,8 +42,8 @@ def _invocation_case(*, root_count=1, label_count=1): label_data[:, 2:5, 3:6] = 1 labels = ObjectLabelSet( name='Nuclei', variant_data=ObjectLabelVariantData(labels=label_data), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, - domain=ObjectLabelDomain(declared_object_id_domains=((1,),) * label_count, scope=ObjectLabelDomainScope.PLANE), + plane_axis=RuntimeSlicePlaneAxis, + domain=ObjectLabelDomain(declared_object_id_domains=((1,),) * label_count, scope=PlaneLabelScope), ) adapter = _FakeCellProfilerRuntime( {'GFPandDNA': image}, objects={'Nuclei': labels}, callable_contract=contract, @@ -79,23 +81,23 @@ def test_match_image_labels_consume_declared_singleton_root_for_scalar_rgb(): assert invocation.execution_mode is NaturalExecution assert invocation.plane_projection is None assert invocation.payload.metadata.axis_position(ColourAxis) == 2 - assert selected.plane_axis is None + assert selected.plane_axis.unstacked assert object_label_dense_array(selected).shape == (8, 9) output = _execute(case, invocation) - scalar_labels = ObjectLabelSet(name='Nuclei', variant_data=ObjectLabelVariantData(labels=labels_before[0]), domain=ObjectLabelDomain(declared_object_ids=(1,), scope=ObjectLabelDomainScope.PAYLOAD)) + scalar_labels = ObjectLabelSet(name='Nuclei', variant_data=ObjectLabelVariantData(labels=labels_before[0]), domain=ObjectLabelDomain(declared_object_ids=(1,), scope=PayloadLabelScope)) expected = raw(image, object_labels=(scalar_labels,), outline_colors=('#21FFFF',)) np.testing.assert_array_equal(output.data, expected.data) np.testing.assert_array_equal(image.data, image_before) np.testing.assert_array_equal(object_label_dense_array(labels), labels_before) - assert labels.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE - assert image.metadata.plane_axis is None + assert labels.plane_axis is RuntimeSlicePlaneAxis + assert image.metadata.plane_axis.unstacked @pytest.mark.parametrize('root_count', (None, 2)) def test_scalar_rgb_cannot_authorize_unknown_or_multiple_root_planes(root_count): case = _invocation_case(root_count=root_count) invocation = _bind(case) - assert invocation.kwargs['object_labels'][0].plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert invocation.kwargs['object_labels'][0].plane_axis is RuntimeSlicePlaneAxis with pytest.raises(RuntimeSliceProjectionDeclarationError, match='Kwargs cannot create image-axis execution semantics'): _execute(case, invocation) @@ -105,7 +107,7 @@ def test_explicit_full_stack_label_contract_preserves_scalar_boundary_refusal(mo monkeypatch.setattr(raw, FunctionContractAttribute.object_label_input_execution_mode, FullStackLabels) case = _invocation_case() invocation = _bind(case) - assert invocation.kwargs['object_labels'][0].plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert invocation.kwargs['object_labels'][0].plane_axis is RuntimeSlicePlaneAxis with pytest.raises(RuntimeSliceProjectionDeclarationError, match='Kwargs cannot create image-axis execution semantics'): _execute(case, invocation) @@ -119,13 +121,13 @@ def test_declared_singleton_root_refuses_mismatched_label_cardinality(): def test_final_module_mode_sees_full_labels_before_scalar_projection(monkeypatch): case = _invocation_case() def mode(cls, default, *, image, kwargs, variable_components): - assert kwargs['object_labels'][0].plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert kwargs['object_labels'][0].plane_axis is RuntimeSlicePlaneAxis assert object_label_dense_array(kwargs['object_labels'][0]).shape == (1, 8, 9) return FullStackExecution monkeypatch.setattr(OverlayOutlinesModule, 'execution_mode', classmethod(mode)) invocation = _bind(case) assert invocation.execution_mode is FullStackExecution - assert invocation.kwargs['object_labels'][0].plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert invocation.kwargs['object_labels'][0].plane_axis is RuntimeSlicePlaneAxis def test_module_mode_failure_precedes_projection_guard(monkeypatch): diff --git a/tests/unit/test_source_image_provenance.py b/tests/unit/test_source_image_provenance.py index e9be4e0aab..e97648cbcd 100644 --- a/tests/unit/test_source_image_provenance.py +++ b/tests/unit/test_source_image_provenance.py @@ -1,11 +1,11 @@ """Focused source-image provenance projection regressions.""" +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis import numpy as np import pytest from openhcs.core.runtime_image_values import ImagePayloadMetadata from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisValueProjection, ) from openhcs.core.source_image_provenance import ( @@ -155,7 +155,7 @@ def test_declared_source_projection_resolves_singleton_plane_contributor() -> No ) projected = projected_payload.metadata - assert projected.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert projected.plane_axis is RuntimeSlicePlaneAxis assert projected_payload.data.shape == (1, 4, 5) assert projected.source_provenance.represented_source_image_names == ( "OrigOverlay", @@ -178,14 +178,14 @@ def test_complete_source_identity_accepts_nested_contributor() -> None: ), ) ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) payload = metadata.payload_with(np.zeros((1, 4, 5), dtype=np.float32)) assert metadata.has_complete_source_identity( payload, RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=1, ), ) @@ -207,7 +207,7 @@ def test_complete_source_identity_requires_multi_plane_axis_declaration() -> Non assert not metadata.has_complete_source_identity( payload, RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=2, ), ) @@ -220,18 +220,18 @@ def test_derived_singleton_runtime_plane_retains_declared_source_name() -> None: component_metadata=({"well": "A01", "channel": "1"},), ), source_image_names=("DNA",), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.zeros((1, 4, 5), dtype=np.float32)) output = ImagePayloadMetadata( source_image_names=("OrigOverlay",), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.ones((1, 4, 5), dtype=np.float32)) derived = source.metadata.derive_payload( ImagePayload.of(source), ImagePayload.of(output), plane_projection=RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis=RuntimeSlicePlaneAxis, axis_size=1, ), ) @@ -241,7 +241,7 @@ def test_derived_singleton_runtime_plane_retains_declared_source_name() -> None: ) projected = projected_payload.metadata - assert projected.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert projected.plane_axis is RuntimeSlicePlaneAxis assert projected_payload.data.shape == (1, 4, 5) assert projected.source_provenance.represented_source_image_names == ( "OrigOverlay", @@ -300,7 +300,7 @@ def _repeated_source_alias_payload( "uint8", "uint16", ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) return metadata.payload_with(pixels, mask), pixels, mask @@ -321,7 +321,7 @@ def test_declared_source_projection_selects_ordered_repeated_alias_planes() -> N projected_payload.mask, np.stack((mask[1], mask[6])), ) - assert projected.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert projected.plane_axis is RuntimeSlicePlaneAxis assert projected.source_plane_intensity_scales == (255.0, 65535.0) assert projected.source_plane_dtypes == ("uint8", "uint16") assert projected.source_image_names == ("OrigER", "OrigER") @@ -348,7 +348,7 @@ def test_declared_source_projection_drops_axis_for_single_selected_plane() -> No np.testing.assert_array_equal(projected_payload.data, pixels[2]) np.testing.assert_array_equal(projected_payload.mask, mask[2]) - assert projected.plane_axis is None + assert projected.plane_axis.unstacked assert projected.intensity_scale == 30.0 assert projected.source_dtype == "uint8" assert projected.source_path == "/input/plane_2.tif" diff --git a/tests/unit/test_source_projection_family.py b/tests/unit/test_source_projection_family.py index a03e574da2..4aeab8c626 100644 --- a/tests/unit/test_source_projection_family.py +++ b/tests/unit/test_source_projection_family.py @@ -1,5 +1,6 @@ """Selection and aggregation own different source-projection contracts.""" +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from dataclasses import dataclass, field, replace import numpy as np @@ -12,7 +13,6 @@ from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.runtime_image_values import ImagePayloadMetadata from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, RuntimePlaneAxisValueProjection, ) from openhcs.core.source_image_provenance import SourceImageProvenancePlanes @@ -26,7 +26,7 @@ def with_data(self, data: RuntimeArrayData) -> "AggregateField": return replace(self, data=data) def resolve_source_context(self, source, projection): - assert projection is not None and projection.plane_index is None + assert projection is not None and projection.preserves_complete_axis assert projection.axis_size == source.shape[0] return source.metadata.collapse_leading_plane_axis().payload_with( self.data, None @@ -35,7 +35,7 @@ def resolve_source_context(self, source, projection): def observation_source(): metadata = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=tuple(f"/synthetic/observation_{i}.tif" for i in range(3)), component_metadata=tuple( @@ -61,12 +61,12 @@ def test_aggregate_context_retains_all_contributors_without_a_pixel_axis(): result = field.resolve_source_context( source, RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, axis_size=3 + axis=RuntimeSlicePlaneAxis, axis_size=3 ), ) metadata = result.metadata assert result.shape == (3, 4) - assert metadata.plane_axis is None + assert metadata.plane_axis.unstacked assert metadata.source_image_provenance_planes.contributor_count == 3 assert metadata.source_image_paths == source.metadata.source_image_paths np.testing.assert_array_equal(np.asarray(result), np.asarray(field)) @@ -101,7 +101,7 @@ def test_independent_selection_member_composes_the_shared_projection(indices): result = output.resolve_source_context( source, RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, axis_size=3 + axis=RuntimeSlicePlaneAxis, axis_size=3 ), ) metadata = result.metadata @@ -110,9 +110,9 @@ def test_independent_selection_member_composes_the_shared_projection(indices): assert output.calls == ["before", "after"] if len(indices) == 1: assert result.shape == (3, 4) - assert metadata.plane_axis is None + assert metadata.plane_axis.unstacked np.testing.assert_array_equal(np.asarray(result), data[0]) else: assert result.shape == (2, 3, 4) - assert metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert metadata.plane_axis is RuntimeSlicePlaneAxis np.testing.assert_array_equal(np.asarray(result), data) diff --git a/tests/unit/test_source_tile_geometry.py b/tests/unit/test_source_tile_geometry.py index b643536d64..852ba54e95 100644 --- a/tests/unit/test_source_tile_geometry.py +++ b/tests/unit/test_source_tile_geometry.py @@ -1,5 +1,6 @@ """Structural, synthetic-only checks of embedded acquisition tile placement.""" +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from openhcs.core.progress import ProgressExecutionContext import json from pathlib import Path @@ -18,7 +19,6 @@ from openhcs.core.memory import numpy from openhcs.core.runtime_adapters import runtime_adapter from openhcs.core.runtime_image_values import ImageMetadataPayload, ImagePayloadMetadata -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.utils.environment import OpenHCSProcessEnvironment from openhcs.core.source_binding_workspace import SourceBindingWorkspaceProjector from openhcs.core.source_bindings import ( @@ -121,7 +121,7 @@ def _metadata(geometries, order, channel="1"): paths=tuple(f"source-{site}" for site in order), component_metadata=records, ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) @@ -441,7 +441,7 @@ def for_source_plane(self, plane_index): original = _metadata(geometries, order, channel) metadata = ProjectionAuditMetadata( source_provenance=original.source_provenance, - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_voxel_spacing=SourceVoxelSpacing((0.25, 0.5)), ) pixels = np.stack([np.full((4, 6), site + 1, np.uint16) for site in order]) @@ -662,7 +662,7 @@ def test_synthetic_pipeline_compiles_and_executes_positions_artifact_then_paired assert metadata.source_voxel_spacing == SourceVoxelSpacing( (0.25, 0.5) ) - assert metadata.plane_axis is None + assert metadata.plane_axis.unstacked replay_config = PreparedWorkspaceSource.pipeline_config_for_source( config ) diff --git a/tests/unit/test_spatial_graph_materialization.py b/tests/unit/test_spatial_graph_materialization.py index 1209f39d8e..2e9cff9af5 100644 --- a/tests/unit/test_spatial_graph_materialization.py +++ b/tests/unit/test_spatial_graph_materialization.py @@ -1,4 +1,5 @@ +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from openhcs.core.artifacts.kinds import ImageArtifactType import numpy as np import pytest @@ -23,7 +24,6 @@ ) from openhcs.core.roi_source_metadata import ROIArchiveSourceMetadata from openhcs.core.runtime_image_values import ImagePayloadMetadata -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.runtime_spatial_graph import ( SpatialGraph, SpatialGraphEdge, @@ -204,7 +204,7 @@ def test_spatial_graph_output_context_preserves_exact_source_identity() -> None: def test_spatial_graph_output_context_projects_declared_source_plane() -> None: graph = _branched_graph().replace_fields(source_plane_index=1) source = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("/tmp/hoechst.tif", "/tmp/smi312.tif"), component_metadata=({"channel": "1"}, {"channel": "4"}), diff --git a/tests/unit/test_step_input_artifact_origin.py b/tests/unit/test_step_input_artifact_origin.py index c6960cc20b..f44bd861c1 100644 --- a/tests/unit/test_step_input_artifact_origin.py +++ b/tests/unit/test_step_input_artifact_origin.py @@ -1,5 +1,6 @@ """Named source origins select pixel authority, not just a provenance alias.""" +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis from types import SimpleNamespace import numpy as np @@ -8,7 +9,6 @@ from openhcs.core.component_group_scope import RuntimeExecutionAxisScope from openhcs.core.runtime_adapters import RuntimeAdapterRequest from openhcs.core.runtime_image_values import ImagePayloadMetadata -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.source_binding_selection import ( PipelineStartSourceUniverseRequest, SourceUniverseRequest, @@ -51,7 +51,7 @@ def test_step_input_names_current_pixels_without_loading_workspace(alias): def test_step_input_selects_current_component_planes_not_original_aliases(): data = np.stack((np.full((4, 5), 7), np.full((4, 5), 91))) payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_names=("Raw1", "Raw2"), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("/synthetic/ch1.tif", "/synthetic/ch2.tif"), @@ -83,7 +83,7 @@ def test_binding_origin_uses_existing_registered_universe_family(): ) is PipelineStartSourceUniverseRequest -@pytest.mark.parametrize("axis", (RuntimePlaneAxis.RUNTIME_SLICE, RuntimePlaneAxis.SOURCE_BINDING)) +@pytest.mark.parametrize("axis", (RuntimeSlicePlaneAxis, SourceBindingPlaneAxis)) @pytest.mark.parametrize("aliases", (("Process", "Nuclear"), ("Nuclear", "Process"))) def test_named_step_inputs_select_distinct_current_pixels_independent_of_order(axis, aliases): values = {"Process": 7, "Nuclear": 91} @@ -110,13 +110,13 @@ def test_named_step_inputs_select_distinct_current_pixels_independent_of_order(a metadata = result.metadata assert metadata.source_image_names == (binding.alias,) assert metadata.source_path == f"/synthetic/{binding.alias}.tif" - assert metadata.plane_axis is None + assert metadata.plane_axis.unstacked np.testing.assert_array_equal(payload.data, data) def test_existing_alias_and_explicit_selector_must_agree(): payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_names=("Raw1", "Raw2"), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("/synthetic/ch1.tif", "/synthetic/ch2.tif"), @@ -134,7 +134,7 @@ def test_existing_alias_and_explicit_selector_must_agree(): def test_named_step_input_preserves_all_planes_of_a_multi_plane_image(): data = np.stack(tuple(np.full((4, 5), value) for value in (7, 11, 91))) payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_names=("Volume", "Volume", "Other"), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("/synthetic/z1.tif", "/synthetic/z2.tif", "/synthetic/other.tif"), @@ -145,14 +145,14 @@ def test_named_step_input_preserves_all_planes_of_a_multi_plane_image(): result = NamedSourceBinding(alias="Volume").project_step_input_payload(payload) np.testing.assert_array_equal(result.data, data[:2]) - assert result.metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert result.metadata.plane_axis is RuntimeSlicePlaneAxis assert result.metadata.source_provenance.source_plane_count == 2 def test_new_alias_without_selectors_still_names_the_complete_current_stack(): data = np.ones((2, 4, 5)) payload = ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_names=("Raw1", "Raw2"), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("/synthetic/ch1.tif", "/synthetic/ch2.tif"), diff --git a/tests/unit/test_stream_component_semantics.py b/tests/unit/test_stream_component_semantics.py index 1900c04cfb..8c4bd80449 100644 --- a/tests/unit/test_stream_component_semantics.py +++ b/tests/unit/test_stream_component_semantics.py @@ -1,3 +1,5 @@ +from openhcs.core.runtime_plane_projection import NoPlaneAxis +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis from types import MappingProxyType import pytest @@ -129,7 +131,7 @@ def test_stream_route_metadata_excludes_payload_local_plane_component() -> None: projector = StreamViewerComponentMetadataProjector.for_item_fields( ("well", "site", "channel", "z_index", "timepoint"), { - "plane_axis": RuntimePlaneAxis.RUNTIME_SLICE.value, + "plane_axis": RuntimeSlicePlaneAxis.name, "plane_component_values": {"channel": ("1", "2")}, }, ) @@ -212,7 +214,7 @@ def _plane_metadata(axis, coordinates): @pytest.mark.parametrize("component", ("site", "channel", "z_index")) def test_retained_stream_domain_includes_exact_planes_not_scalar_address(component): metadata = _plane_metadata( - RuntimePlaneAxis.RUNTIME_SLICE, + RuntimeSlicePlaneAxis, ({component: "3"}, {component: "1"}), ) scalar = SourceImageIdentity(component_metadata={"well": "A01"}) @@ -235,7 +237,7 @@ def test_retained_stream_domain_includes_exact_planes_not_scalar_address(compone def test_collapsed_contributors_do_not_restore_a_stream_pixel_axis(): metadata = _plane_metadata( - RuntimePlaneAxis.RUNTIME_SLICE, ({"site": "3"}, {"site": "1"}), + RuntimeSlicePlaneAxis, ({"site": "3"}, {"site": "1"}), ).collapse_leading_plane_axis() observations = StreamSourceComponentMetadataItems.from_image_metadata( metadata, fallback_source_identity=SourceImageIdentity( @@ -246,7 +248,7 @@ def test_collapsed_contributors_do_not_restore_a_stream_pixel_axis(): assert observations.domain_metadata_items(("well", "site")) == ({"well": "A01"},) -@pytest.mark.parametrize("axis", tuple(RuntimePlaneAxis)) +@pytest.mark.parametrize("axis", RuntimePlaneAxis.leading_axes()) @pytest.mark.parametrize("component", AxisFamily.active().axes) def test_retained_image_plane_domain_does_not_require_artifact_storage_axes( axis, component @@ -260,7 +262,7 @@ def test_retained_image_plane_domain_does_not_require_artifact_storage_axes( metadata, () ) - assert fields["plane_axis"] == axis.value + assert fields["plane_axis"] == axis.name assert fields["plane_component_values"] == {component.name: ("1", "2")} assert ( metadata.retained_plane_component_values() == fields["plane_component_values"] @@ -268,7 +270,7 @@ def test_retained_image_plane_domain_does_not_require_artifact_storage_axes( def test_projected_contributors_do_not_declare_a_retained_pixel_plane_domain(): - metadata = _plane_metadata(None, ({"channel": "1"}, {"channel": "2"})) + metadata = _plane_metadata(NoPlaneAxis, ({"channel": "1"}, {"channel": "2"})) assert metadata.retained_plane_component_values() == {} wire = StreamImagePayloadMetadataProjector.item_fields_for_plane_components( @@ -280,14 +282,14 @@ def test_projected_contributors_do_not_declare_a_retained_pixel_plane_domain(): ImagePayloadMetadata.from_viewer_image_metadata( wire["image_metadata"] ).plane_axis - is None + is NoPlaneAxis ) @pytest.mark.parametrize("storage_components", [(), (Microscopy.Channel,)]) def test_retained_plane_domain_rejects_multiple_varying_components(storage_components): metadata = _plane_metadata( - RuntimePlaneAxis.SOURCE_BINDING, + SourceBindingPlaneAxis, ({"channel": "1", "z_index": "1"}, {"channel": "2", "z_index": "2"}), ) @@ -299,7 +301,7 @@ def test_retained_plane_domain_rejects_multiple_varying_components(storage_compo def test_singleton_plane_projection_remains_exactly_compiler_owned(): metadata = _plane_metadata( - RuntimePlaneAxis.RUNTIME_SLICE, + RuntimeSlicePlaneAxis, ({"site": "3", "channel": "7"},), ) @@ -314,7 +316,7 @@ def test_singleton_plane_projection_remains_exactly_compiler_owned(): def test_singleton_plane_projection_rejects_ambiguous_compiler_components(): metadata = _plane_metadata( - RuntimePlaneAxis.RUNTIME_SLICE, + RuntimeSlicePlaneAxis, ({"site": "3", "channel": "7"},), ) diff --git a/tests/unit/test_streaming_service.py b/tests/unit/test_streaming_service.py index d692cf46da..26e2d2def7 100644 --- a/tests/unit/test_streaming_service.py +++ b/tests/unit/test_streaming_service.py @@ -1,4 +1,6 @@ from __future__ import annotations +from openhcs.core.runtime_plane_projection import NoPlaneAxis +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from dataclasses import replace from pathlib import Path @@ -47,7 +49,6 @@ ObjectCoreMeasurementFeature, ) from openhcs.core.runtime_tabular_values import FieldSpec -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.source_image_provenance import SourceImageProvenancePlanes from openhcs.core.source_metadata import SourceVoxelSpacing, SourceVoxelSpacingUnit from openhcs.core.source_projection import SourceArtifactProjection @@ -640,7 +641,7 @@ def physical_source_path( stream_request = kwargs[ViewerStreamKwarg.STREAM_REQUEST.value] item_fields = dict(stream_request.source.item_fields) image_metadata = item_fields.pop("image_metadata") - assert image_metadata["plane_axis"] is None + assert image_metadata["plane_axis"] == NoPlaneAxis.name assert "source_channel_axis" not in image_metadata assert item_fields == { "source_spatial_shape_yx": [8, 9], @@ -690,7 +691,7 @@ def source_path( ), ), source_dtype="int32", - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) projection = SourceArtifactProjection( address=None, diff --git a/tests/unit/test_unified_registry_runtime_payloads.py b/tests/unit/test_unified_registry_runtime_payloads.py index 00fba5b09f..9d514578d0 100644 --- a/tests/unit/test_unified_registry_runtime_payloads.py +++ b/tests/unit/test_unified_registry_runtime_payloads.py @@ -1,4 +1,6 @@ from __future__ import annotations +from openhcs.core.runtime_object_label_domains import PlaneLabelScope +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis, SourceBindingPlaneAxis from dataclasses import dataclass from functools import wraps @@ -31,13 +33,11 @@ ) from openhcs.core.runtime_object_label_domains import ( ObjectLabelDomain, - ObjectLabelDomainScope, ) from openhcs.core.runtime_object_labels import ( ObjectLabelPayload, ObjectLabelVariantData, ) -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.source_image_provenance import ( SourceImageProvenancePlanes, ) @@ -196,7 +196,7 @@ def test_pure_2d_contract_slices_image_metadata_payload_nominally() -> None: payload = ImageMetadataPayload( data=stack, metadata=ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=( SourceImageProvenancePlanes.from_components(paths=("z0.tif", "z1.tif")) ), @@ -234,7 +234,7 @@ def test_native_flexible_callable_receives_declared_array_abi_and_keeps_slice_co ) -> None: stack = np.arange(40, dtype=np.float32).reshape(2, 4, 5) mask = np.ones_like(stack, dtype=bool) - payload = ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE).payload_with(stack, mask) + payload = ImagePayloadMetadata(plane_axis=RuntimeSlicePlaneAxis).payload_with(stack, mask) seen_shapes = [] @numpy @@ -281,7 +281,7 @@ def test_flexible_runtime_argument_preserves_plane_context_until_mode_selection( payload = ImageMetadataPayload( data=stack, metadata=ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("z0.tif", "z1.tif"), ), @@ -304,7 +304,7 @@ def add_one(image: np.ndarray) -> np.ndarray: result = wrapped(call_argument, slice_by_slice=slice_by_slice) assert seen_shapes == ([(4, 5), (4, 5)] if slice_by_slice else [(2, 4, 5)]) np.testing.assert_array_equal(result.data, stack + 1) - assert result.metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + assert result.metadata.plane_axis is RuntimeSlicePlaneAxis assert result.metadata.source_image_provenance_planes.paths == ( "z0.tif", "z1.tif", ) @@ -317,11 +317,11 @@ def test_pure_2d_contract_projects_stack_shaped_kwargs_per_slice() -> None: mask_data[1, 1, 1] = True stack = ImageMetadataPayload( data=stack_data, - metadata=ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE), + metadata=ImagePayloadMetadata(plane_axis=RuntimeSlicePlaneAxis), ) mask = ImageMetadataPayload( data=mask_data, - metadata=ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE), + metadata=ImagePayloadMetadata(plane_axis=RuntimeSlicePlaneAxis), ) seen_shapes: list[tuple[int, ...]] = [] seen_true_indices: list[tuple[int, int]] = [] @@ -359,7 +359,7 @@ def test_pure_2d_contract_preserves_declared_source_binding_axis() -> None: stack = ImageMetadataPayload( data=np.arange(24, dtype=np.float32).reshape(2, 3, 4), metadata=ImagePayloadMetadata( - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_image_names=source_aliases, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("blue.tif", "green.tif") @@ -376,11 +376,11 @@ def test_pure_2d_contract_preserves_declared_source_binding_axis() -> None: dtype=np.int32, ) ), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + plane_axis=SourceBindingPlaneAxis, source_image_names=source_aliases, domain=ObjectLabelDomain( declared_object_id_domains=((1,), (2,)), - scope=ObjectLabelDomainScope.PLANE, + scope=PlaneLabelScope, ), ) seen_label_shapes: list[tuple[int, ...]] = [] @@ -402,7 +402,7 @@ def consume_labels( ) assert seen_label_shapes == [(3, 4), (3, 4)] - assert result.metadata.plane_axis is RuntimePlaneAxis.SOURCE_BINDING + assert result.metadata.plane_axis is SourceBindingPlaneAxis assert result.metadata.source_image_names == source_aliases @@ -439,7 +439,7 @@ def test_volumetric_projection_accepts_metadata_payload_array_methods() -> None: data=stack, metadata=ImagePayloadMetadata( source_dtype="float32", - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ), ) @@ -454,7 +454,7 @@ def test_volumetric_projection_accepts_metadata_payload_array_methods() -> None: np.testing.assert_array_equal(result.data, stack.max(axis=0)) result_metadata = result.metadata assert result_metadata.source_dtype == "float32" - assert result_metadata.plane_axis is None + assert result_metadata.plane_axis.unstacked def test_with_pixels_rejects_implicit_channel_mask_collapse() -> None: diff --git a/tests/unit/test_volume_projection_fixture.py b/tests/unit/test_volume_projection_fixture.py index 9ab2a753b4..e65509d117 100644 --- a/tests/unit/test_volume_projection_fixture.py +++ b/tests/unit/test_volume_projection_fixture.py @@ -4,6 +4,8 @@ execution, saved CSV/ROI publication, an installed MCP journey, or biology. """ +from openhcs.core.runtime_plane_projection import NoPlaneAxis +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis from openhcs.core.artifacts.kinds import ( MeasurementsArtifactType, ObjectLabelsArtifactType, @@ -21,7 +23,7 @@ from openhcs.core.artifacts.kinds import ImageArtifactType from openhcs.core.runtime_image_values import ImagePayloadMetadata from openhcs.core.runtime_object_labels import object_label_dense_array -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis, RuntimePlaneAxisValueProjection +from openhcs.core.runtime_plane_projection import RuntimePlaneAxisValueProjection from openhcs.core.source_image_provenance import SourceImageProvenancePlanes from openhcs.core.source_projection import OpenHCSPlaneAddress from openhcs.core.source_matching import source_component_metadata_value @@ -50,7 +52,7 @@ def source_volume(): paths=("/synthetic-unit-input/image.ome.tif",) * 3, component_metadata=tuple(address.as_component_metadata() for address in addresses), ), - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + plane_axis=RuntimeSlicePlaneAxis, ) return metadata.payload_with(pixels) @@ -65,26 +67,39 @@ def output_plan(declaration): ) -@pytest.mark.parametrize("indices", [(), (2, 0, 1), (2, 0), (1,)]) +def test_one_selected_plane_is_that_source_plane(source_volume): + """One selected plane consumes the stack axis and takes that plane's identity (#395).""" + selection = fixture.select_volume_fixture_planes_v2(source_volume, (1,)) + projected = ImageArtifactType.contextualize_output( + source_volume, selection, output_plan(fixture.SELECTED_VOLUME), + RuntimePlaneAxisValueProjection.preserve(axis=RuntimeSlicePlaneAxis, axis_size=3), + ) + np.testing.assert_array_equal(projected.data, source_volume.data[1]) + assert projected.metadata.plane_axis is NoPlaneAxis + assert ( + projected.metadata.source_provenance + == source_volume.metadata.for_leading_source_plane(1).source_provenance + ) + + +@pytest.mark.parametrize("indices", [(), (2, 0, 1), (2, 0)]) def test_selected_then_full_stack_outputs_keep_exact_source_context(source_volume, indices): original_pixels = source_volume.data selected_indices = indices or (0, 1, 2) selection = fixture.select_volume_fixture_planes_v2(source_volume, indices) projected = ImageArtifactType.contextualize_output( source_volume, selection, output_plan(fixture.SELECTED_VOLUME), - RuntimePlaneAxisValueProjection(RuntimePlaneAxis.RUNTIME_SLICE, (), None, 3), + RuntimePlaneAxisValueProjection.preserve(axis=RuntimeSlicePlaneAxis, axis_size=3), ) expected = original_pixels[list(selected_indices)] source_metadata = source_volume.metadata - # for_source_planes intentionally consumes a singleton plane axis. This - # declaration instead returns a 3-D stack, including for one selected plane. expected_provenance = source_metadata.for_source_planes(selected_indices).source_provenance.with_source_image_provenance_planes( source_metadata.source_image_provenance_planes.select(selected_indices), ) np.testing.assert_array_equal(projected.data, expected) assert projected.metadata.source_provenance == expected_provenance - assert projected.metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE - projection = RuntimePlaneAxisValueProjection(RuntimePlaneAxis.RUNTIME_SLICE, (), None, len(selected_indices)) + assert projected.metadata.plane_axis is RuntimeSlicePlaneAxis + projection = RuntimePlaneAxisValueProjection.preserve(axis=RuntimeSlicePlaneAxis, axis_size=len(selected_indices)) for _ in range(2): image, labels, rows = fixture.inspect_volume_fixture_v2(projected) contextual_image = ImageArtifactType.contextualize_output( @@ -128,7 +143,7 @@ def test_projection_owner_rejects_invalid_selection(source_volume, indices): def test_empty_label_stack_keeps_schema_and_exact_plane_context(source_volume): empty = source_volume.metadata.payload_with(np.zeros((3, 8, 9), dtype=np.uint16)) image, labels, rows = fixture.inspect_volume_fixture_v2(empty) - projection = RuntimePlaneAxisValueProjection(RuntimePlaneAxis.RUNTIME_SLICE, (), None, 3) + projection = RuntimePlaneAxisValueProjection.preserve(axis=RuntimeSlicePlaneAxis, axis_size=3) contextual = ObjectLabelsArtifactType.contextualize_output( image, labels, output_plan(fixture.VOLUME_LABELS), projection, ) diff --git a/tests/unit/test_weighted_projection_consumers.py b/tests/unit/test_weighted_projection_consumers.py index 203aa21629..b09692237a 100644 --- a/tests/unit/test_weighted_projection_consumers.py +++ b/tests/unit/test_weighted_projection_consumers.py @@ -1,12 +1,12 @@ """Shared weighted arithmetic preserves each production consumer's recipe.""" +from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis import inspect import numpy as np import pytest from openhcs.core.runtime_image_values import ImagePayloadMetadata -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.autoregister_preparation import AutoRegisterRegistryPreparation from openhcs.processing.backends.cellprofiler.color import combine_color_to_gray from openhcs.processing.backends.processors.numpy_processor import ( @@ -55,7 +55,7 @@ def test_color_retains_selected_order_duplicates_and_negative_indices(shape, str for channel, weight in zip(channels, weights, strict=True): expected += pixels[..., channel].astype(np.float64) * weight payload = ImagePayloadMetadata( - axes=PayloadAxes.colour_samples(3), plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + axes=PayloadAxes.colour_samples(3), plane_axis=RuntimeSlicePlaneAxis, ).payload_with(pixels) actual = combine_color_to_gray(payload, channels, contributions) np.testing.assert_array_equal(actual, expected) @@ -64,7 +64,7 @@ def test_color_retains_selected_order_duplicates_and_negative_indices(shape, str def test_nonfinite_products_are_not_reassociated(): pixels = np.array([[[[np.inf, -np.inf, np.nan], [1.0, 2.0, 3.0]]]], np.float32) payload = ImagePayloadMetadata( - axes=PayloadAxes.colour_samples(3), plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + axes=PayloadAxes.colour_samples(3), plane_axis=RuntimeSlicePlaneAxis, ).payload_with(pixels) with np.errstate(invalid="ignore"): actual = combine_color_to_gray(payload, (0, 1, 2), (0.0, 1.0, 1.0)) @@ -73,7 +73,7 @@ def test_nonfinite_products_are_not_reassociated(): def test_empty_spatial_domain_still_validates_channel_indices(): payload = ImagePayloadMetadata( - axes=PayloadAxes.colour_samples(3), plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + axes=PayloadAxes.colour_samples(3), plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.empty((1, 0, 2, 3), np.float32)) with pytest.raises(IndexError): combine_color_to_gray(payload, (3,), (1.0,)) @@ -82,7 +82,7 @@ def test_empty_spatial_domain_still_validates_channel_indices(): @pytest.mark.parametrize("channels", [(), (0, 1, 2)]) def test_zero_sum_outputs_retain_positive_zero(channels): payload = ImagePayloadMetadata( - axes=PayloadAxes.colour_samples(3), plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + axes=PayloadAxes.colour_samples(3), plane_axis=RuntimeSlicePlaneAxis, ).payload_with(np.full((2, 2, 2, 3), -0.0, np.float32)) actual = combine_color_to_gray(payload, channels, (1.0,) * len(channels)) np.testing.assert_array_equal(actual, np.zeros((2, 2, 2), np.float64)) @@ -103,7 +103,7 @@ def test_registry_preparation_covers_accelerated_consumer_signatures(): inspect.unwrap(create_composite)(np.ones((3, 4, 6), np.float64), [1, 2, 3]) pixels = np.ones((2, 4, 6, 3), np.float32)[:, :, ::-1] payload = ImagePayloadMetadata( - axes=PayloadAxes.colour_samples(3), plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + axes=PayloadAxes.colour_samples(3), plane_axis=RuntimeSlicePlaneAxis, ).payload_with(pixels) combine_color_to_gray(payload, (0, 1, 2), (1, 2, 3)) pixels.flags.writeable = False @@ -111,7 +111,7 @@ def test_registry_preparation_covers_accelerated_consumer_signatures(): np.testing.assert_array_equal(readonly, np.ones((2, 4, 6))) degenerate = np.ones((1, 1, 6, 3), np.float64) degenerate_payload = ImagePayloadMetadata( - axes=PayloadAxes.colour_samples(3), plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + axes=PayloadAxes.colour_samples(3), plane_axis=RuntimeSlicePlaneAxis, ).payload_with(degenerate) combine_color_to_gray(degenerate_payload, (0, 1, 2), (1, 2, 3)) degenerate.flags.writeable = False